summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitmodules3
-rw-r--r--.vscode/tasks.json33
-rw-r--r--API_CHANGES.md36
-rw-r--r--AUTHORS1
-rw-r--r--Makefile96
-rw-r--r--README20
-rwxr-xr-xbootstrap5
-rw-r--r--build-system/configure.py2
m---------build-system/taler-build-scripts0
-rw-r--r--contrib/articles/spending.txt2
-rw-r--r--contrib/articles/ui/ui-cameraready.tex6
-rw-r--r--contrib/articles/ui/ui.tex6
-rw-r--r--contrib/articles/ui/ui_short.tex2
-rwxr-xr-xcontrib/bump-taler-version.mjs119
-rw-r--r--contrib/ci/Containerfile27
-rwxr-xr-xcontrib/ci/ci.sh34
-rw-r--r--contrib/ci/jobs/0-codespell/config.ini6
-rw-r--r--contrib/ci/jobs/0-codespell/dictionary.txt20
-rwxr-xr-xcontrib/ci/jobs/0-codespell/job.sh6
-rwxr-xr-xcontrib/ci/jobs/1-build/build.sh6
-rwxr-xr-xcontrib/ci/jobs/1-build/job.sh6
-rwxr-xr-xcontrib/ci/jobs/2-docs/docs.sh8
-rwxr-xr-xcontrib/ci/jobs/2-docs/job.sh6
-rw-r--r--contrib/ci/jobs/3-wallet-cli-deb-package/config.ini6
-rwxr-xr-xcontrib/ci/jobs/3-wallet-cli-deb-package/job.sh36
-rwxr-xr-xcontrib/ci/jobs/3-wallet-cli-deb-package/version.sh17
-rw-r--r--contrib/ci/jobs/4-taler-harness-deb-package/config.ini6
-rwxr-xr-xcontrib/ci/jobs/4-taler-harness-deb-package/job.sh36
-rwxr-xr-xcontrib/ci/jobs/4-taler-harness-deb-package/version.sh17
-rw-r--r--contrib/ci/jobs/5-deploy-packages/config.ini6
-rwxr-xr-xcontrib/ci/jobs/5-deploy-packages/job.sh14
-rwxr-xr-xcontrib/cleanup-prebuilt-dir.sh11
-rwxr-xr-xcontrib/copy-aml-backoffice-into-prebuilt.sh10
-rwxr-xr-xcontrib/copy-auditor-backoffice-into-prebuilt.sh10
-rwxr-xr-xcontrib/copy-backend-into-prebuilt.sh8
-rwxr-xr-xcontrib/copy-backoffice-into-prebuilt.sh10
-rwxr-xr-xcontrib/copy-bank-into-prebuilt.sh10
-rwxr-xr-xcontrib/copy-challenger-into-prebuilt.sh10
-rwxr-xr-xcontrib/publish-prebuilt-dir.sh15
m---------contrib/wallet-testdata0
-rw-r--r--package.json10
-rw-r--r--packages/aml-backoffice-ui/.eslintrc.cjs28
-rw-r--r--packages/aml-backoffice-ui/.gitignore4
-rw-r--r--packages/aml-backoffice-ui/Makefile36
-rw-r--r--packages/aml-backoffice-ui/README.md4
-rwxr-xr-xpackages/aml-backoffice-ui/build.mjs28
-rw-r--r--packages/aml-backoffice-ui/copyleft-header.js15
-rwxr-xr-x[-rw-r--r--]packages/aml-backoffice-ui/dev.mjs (renamed from packages/taler-wallet-webextension/src/hooks/useLang.ts)34
-rw-r--r--packages/aml-backoffice-ui/package.json59
-rw-r--r--packages/aml-backoffice-ui/postcss.config.js6
-rw-r--r--packages/aml-backoffice-ui/src/App.tsx74
-rw-r--r--packages/aml-backoffice-ui/src/Dashboard.tsx234
-rw-r--r--packages/aml-backoffice-ui/src/assets/home.svg3
-rw-r--r--packages/aml-backoffice-ui/src/assets/logo-2021.svg (renamed from packages/taler-wallet-webextension/src/svg/logo-2021.svg)0
-rw-r--r--packages/aml-backoffice-ui/src/assets/people.svg3
-rw-r--r--packages/aml-backoffice-ui/src/context/config.ts100
-rw-r--r--packages/aml-backoffice-ui/src/declaration.d.ts31
-rw-r--r--packages/aml-backoffice-ui/src/forms.ts24
-rw-r--r--packages/aml-backoffice-ui/src/forms/902_11e.ts133
-rw-r--r--packages/aml-backoffice-ui/src/forms/902_12e.ts421
-rw-r--r--packages/aml-backoffice-ui/src/forms/902_13e.ts510
-rw-r--r--packages/aml-backoffice-ui/src/forms/902_15e.ts172
-rw-r--r--packages/aml-backoffice-ui/src/forms/902_1e.ts660
-rw-r--r--packages/aml-backoffice-ui/src/forms/902_4e.ts787
-rw-r--r--packages/aml-backoffice-ui/src/forms/902_5e.ts255
-rw-r--r--packages/aml-backoffice-ui/src/forms/902_9e.ts119
-rw-r--r--packages/aml-backoffice-ui/src/forms/declaration.ts70
-rw-r--r--packages/aml-backoffice-ui/src/forms/icons.tsx10
-rw-r--r--packages/aml-backoffice-ui/src/forms/index.ts202
-rw-r--r--packages/aml-backoffice-ui/src/forms/simplest.ts85
-rw-r--r--packages/aml-backoffice-ui/src/hooks/useBackend.ts48
-rw-r--r--packages/aml-backoffice-ui/src/hooks/useCaseDetails.ts95
-rw-r--r--packages/aml-backoffice-ui/src/hooks/useCases.ts86
-rw-r--r--packages/aml-backoffice-ui/src/hooks/useOfficer.ts135
-rw-r--r--packages/aml-backoffice-ui/src/hooks/useSettings.ts74
-rw-r--r--packages/aml-backoffice-ui/src/i18n/bank.pot486
-rw-r--r--packages/aml-backoffice-ui/src/i18n/de.po486
-rw-r--r--packages/aml-backoffice-ui/src/i18n/en.po511
-rw-r--r--packages/aml-backoffice-ui/src/i18n/es.po497
-rw-r--r--packages/aml-backoffice-ui/src/i18n/fr.po486
-rw-r--r--packages/aml-backoffice-ui/src/i18n/it.po521
-rw-r--r--packages/aml-backoffice-ui/src/i18n/poheader26
-rw-r--r--packages/aml-backoffice-ui/src/i18n/strings-prelude (renamed from packages/demobank-ui/src/i18n/strings-prelude)2
-rw-r--r--packages/aml-backoffice-ui/src/i18n/strings.ts510
-rw-r--r--packages/aml-backoffice-ui/src/index.html42
-rw-r--r--packages/aml-backoffice-ui/src/index.tsx22
-rw-r--r--packages/aml-backoffice-ui/src/pages.ts44
-rw-r--r--packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.stories.tsx104
-rw-r--r--packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.tsx160
-rw-r--r--packages/aml-backoffice-ui/src/pages/CaseDetails.tsx314
-rw-r--r--packages/aml-backoffice-ui/src/pages/Cases.stories.tsx (renamed from packages/taler-wallet-webextension/src/cta/Tip/stories.tsx)36
-rw-r--r--packages/aml-backoffice-ui/src/pages/Cases.tsx287
-rw-r--r--packages/aml-backoffice-ui/src/pages/CreateAccount.tsx106
-rw-r--r--packages/aml-backoffice-ui/src/pages/HandleAccountNotReady.tsx35
-rw-r--r--packages/aml-backoffice-ui/src/pages/NewFormEntry.tsx104
-rw-r--r--packages/aml-backoffice-ui/src/pages/Officer.tsx60
-rw-r--r--packages/aml-backoffice-ui/src/pages/ShowConsolidated.stories.tsx117
-rw-r--r--packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx173
-rw-r--r--packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx79
-rw-r--r--packages/aml-backoffice-ui/src/pages/index.stories.ts3
-rw-r--r--packages/aml-backoffice-ui/src/route.ts197
-rw-r--r--packages/aml-backoffice-ui/src/scss/main.css3
-rw-r--r--packages/aml-backoffice-ui/src/settings.ts (renamed from packages/taler-wallet-webextension/src/components/JustInDevMode.tsx)24
-rw-r--r--packages/aml-backoffice-ui/src/stories.test.ts71
-rw-r--r--packages/aml-backoffice-ui/src/stories.tsx66
-rw-r--r--packages/aml-backoffice-ui/src/utils/QR.tsx54
-rw-r--r--packages/aml-backoffice-ui/src/utils/converter.ts31
-rw-r--r--packages/aml-backoffice-ui/src/utils/types.ts124
-rw-r--r--packages/aml-backoffice-ui/tailwind.config.js14
-rwxr-xr-x[-rw-r--r--]packages/aml-backoffice-ui/test.mjs (renamed from packages/taler-wallet-webextension/src/serviceWorkerCryptoWorkerFactory.ts)31
-rw-r--r--packages/aml-backoffice-ui/tsconfig.json (renamed from packages/demobank-ui/tsconfig.json)17
-rw-r--r--packages/anastasis-cli/Makefile42
-rw-r--r--packages/anastasis-cli/README.md4
-rwxr-xr-xpackages/anastasis-cli/bin/anastasis-cli.mjs20
-rwxr-xr-xpackages/anastasis-cli/build-node.mjs70
-rw-r--r--packages/anastasis-cli/package.json44
-rw-r--r--packages/anastasis-cli/src/import-meta-url.js2
-rw-r--r--packages/anastasis-cli/src/index.ts87
-rw-r--r--packages/anastasis-cli/tsconfig.json33
-rw-r--r--packages/anastasis-core/README.md3
-rw-r--r--packages/anastasis-core/package.json18
-rw-r--r--packages/anastasis-core/src/anastasis-data.ts12
-rw-r--r--packages/anastasis-core/src/cli-entry.ts7
-rw-r--r--packages/anastasis-core/src/cli.ts64
-rw-r--r--packages/anastasis-core/src/crypto.ts38
-rw-r--r--packages/anastasis-core/src/index.node.ts2
-rw-r--r--packages/anastasis-core/src/index.ts19
-rw-r--r--packages/anastasis-core/src/policy-suggestion.test.ts6
-rw-r--r--packages/anastasis-core/src/reducer-types.ts2
-rw-r--r--packages/anastasis-core/tsconfig.json6
-rw-r--r--packages/anastasis-webui/README.md3
-rwxr-xr-xpackages/anastasis-webui/build.mjs138
-rwxr-xr-xpackages/anastasis-webui/dev.mjs29
-rw-r--r--packages/anastasis-webui/package.json25
-rw-r--r--packages/anastasis-webui/src/components/menu/SideBar.tsx5
-rw-r--r--packages/anastasis-webui/src/components/menu/index.tsx2
-rw-r--r--packages/anastasis-webui/src/components/picker/DurationPicker.stories.tsx12
-rw-r--r--packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts7
-rw-r--r--packages/anastasis-webui/src/index.html2
-rw-r--r--packages/anastasis-webui/src/index.test.ts75
-rw-r--r--packages/anastasis-webui/src/index.ts2
-rw-r--r--packages/anastasis-webui/src/pages/home/AddingProviderScreen/index.ts89
-rw-r--r--packages/anastasis-webui/src/pages/home/AddingProviderScreen/state.ts63
-rw-r--r--packages/anastasis-webui/src/pages/home/AddingProviderScreen/stories.tsx10
-rw-r--r--packages/anastasis-webui/src/pages/home/AddingProviderScreen/test.ts33
-rw-r--r--packages/anastasis-webui/src/pages/home/AddingProviderScreen/views.tsx4
-rw-r--r--packages/anastasis-webui/src/pages/home/AttributeEntryScreen.stories.tsx57
-rw-r--r--packages/anastasis-webui/src/pages/home/AttributeEntryScreen.tsx1
-rw-r--r--packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.stories.tsx116
-rw-r--r--packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx2
-rw-r--r--packages/anastasis-webui/src/pages/home/BackupFinishedScreen.stories.tsx10
-rw-r--r--packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.stories.tsx148
-rw-r--r--packages/anastasis-webui/src/pages/home/ChallengePayingScreen.stories.tsx6
-rw-r--r--packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.stories.tsx13
-rw-r--r--packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.tsx4
-rw-r--r--packages/anastasis-webui/src/pages/home/EditPoliciesScreen.stories.tsx11
-rw-r--r--packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.stories.tsx8
-rw-r--r--packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.stories.tsx8
-rw-r--r--packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.tsx13
-rw-r--r--packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.stories.tsx457
-rw-r--r--packages/anastasis-webui/src/pages/home/SecretEditorScreen.stories.tsx17
-rw-r--r--packages/anastasis-webui/src/pages/home/SecretSelectionScreen.stories.tsx33
-rw-r--r--packages/anastasis-webui/src/pages/home/SolveScreen.stories.tsx9
-rw-r--r--packages/anastasis-webui/src/pages/home/StartScreen.stories.tsx6
-rw-r--r--packages/anastasis-webui/src/pages/home/TruthsPayingScreen.stories.tsx8
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSetup.stories.tsx15
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSolve.stories.tsx19
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSetup.stories.tsx15
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSolve.stories.tsx13
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSetup.stories.tsx15
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSolve.stories.tsx11
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSetup.stories.tsx15
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSolve.stories.tsx223
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSetup.stories.tsx15
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSolve.stories.tsx11
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSetup.stories.tsx15
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSolve.stories.tsx13
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/totp.ts2
-rw-r--r--packages/anastasis-webui/src/pages/home/index.stories.tsx (renamed from packages/anastasis-webui/src/pages/home/index.storiesNo.tsx)2
-rw-r--r--packages/anastasis-webui/src/pages/home/index.tsx2
-rw-r--r--packages/anastasis-webui/src/stories.tsx4
-rw-r--r--packages/anastasis-webui/src/test-utils.ts205
-rw-r--r--packages/anastasis-webui/src/utils/index.tsx57
-rwxr-xr-x[-rw-r--r--]packages/anastasis-webui/test.mjs (renamed from packages/demobank-ui/src/stories.tsx)46
-rw-r--r--packages/anastasis-webui/tsconfig.json43
-rw-r--r--packages/auditor-backoffice-ui/.gitignore6
-rw-r--r--packages/auditor-backoffice-ui/DESIGN.md195
-rw-r--r--packages/auditor-backoffice-ui/Makefile35
-rw-r--r--packages/auditor-backoffice-ui/README.md64
-rwxr-xr-xpackages/auditor-backoffice-ui/build.mjs28
-rwxr-xr-xpackages/auditor-backoffice-ui/contrib/po2ts42
-rw-r--r--packages/auditor-backoffice-ui/copyleft-header.js15
-rwxr-xr-x[-rw-r--r--]packages/auditor-backoffice-ui/dev.mjs (renamed from packages/merchant-backend-ui/src/pages/DepletedTip.stories.tsx)38
-rw-r--r--packages/auditor-backoffice-ui/package.json84
-rw-r--r--packages/auditor-backoffice-ui/preact.config.js70
-rw-r--r--packages/auditor-backoffice-ui/preact.single-config.js62
-rw-r--r--packages/auditor-backoffice-ui/remove-link-stylesheet.sh8
-rw-r--r--packages/auditor-backoffice-ui/src/AdminRoutes.tsx53
-rw-r--r--packages/auditor-backoffice-ui/src/Application.tsx165
-rw-r--r--packages/auditor-backoffice-ui/src/ApplicationReadyRoutes.tsx175
-rw-r--r--packages/auditor-backoffice-ui/src/InstanceRoutes.tsx (renamed from packages/merchant-backoffice-ui/src/InstanceRoutes.tsx)375
-rw-r--r--packages/auditor-backoffice-ui/src/assets/empty.png (renamed from packages/demobank-ui/src/assets/empty.png)bin2785 -> 2785 bytes
-rw-r--r--packages/auditor-backoffice-ui/src/assets/icons/android-chrome-192x192.png (renamed from packages/demobank-ui/src/assets/icons/android-chrome-192x192.png)bin14058 -> 14058 bytes
-rw-r--r--packages/auditor-backoffice-ui/src/assets/icons/android-chrome-512x512.png (renamed from packages/demobank-ui/src/assets/icons/android-chrome-512x512.png)bin51484 -> 51484 bytes
-rw-r--r--packages/auditor-backoffice-ui/src/assets/icons/apple-touch-icon.png (renamed from packages/demobank-ui/src/assets/icons/apple-touch-icon.png)bin12746 -> 12746 bytes
-rw-r--r--packages/auditor-backoffice-ui/src/assets/icons/favicon-16x16.png (renamed from packages/demobank-ui/src/assets/icons/favicon-16x16.png)bin626 -> 626 bytes
-rw-r--r--packages/auditor-backoffice-ui/src/assets/icons/favicon-32x32.png (renamed from packages/demobank-ui/src/assets/icons/favicon-32x32.png)bin1487 -> 1487 bytes
-rw-r--r--packages/auditor-backoffice-ui/src/assets/icons/languageicon.svg (renamed from packages/demobank-ui/src/assets/icons/languageicon.svg)0
-rw-r--r--packages/auditor-backoffice-ui/src/assets/icons/mstile-150x150.png (renamed from packages/demobank-ui/src/assets/icons/mstile-150x150.png)bin9050 -> 9050 bytes
-rw-r--r--packages/auditor-backoffice-ui/src/assets/logo-2021.svg9
-rw-r--r--packages/auditor-backoffice-ui/src/assets/logo.jpeg (renamed from packages/demobank-ui/src/assets/logo.jpeg)bin39336 -> 39336 bytes
-rw-r--r--packages/auditor-backoffice-ui/src/components/exception/AsyncButton.tsx (renamed from packages/demobank-ui/src/components/AsyncButton.tsx)49
-rw-r--r--packages/auditor-backoffice-ui/src/components/exception/QR.tsx49
-rw-r--r--packages/auditor-backoffice-ui/src/components/exception/loading.tsx48
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/FormProvider.tsx109
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/Input.tsx116
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/InputArray.tsx139
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/InputBoolean.tsx91
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/InputCurrency.tsx67
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/InputDate.tsx164
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/InputDuration.tsx186
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/InputGroup.tsx86
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/InputImage.tsx122
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/InputLocation.tsx53
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/InputNumber.tsx60
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/InputPayto.tsx52
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/InputPaytoForm.stories.tsx47
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/InputPaytoForm.tsx397
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/InputSearchOnList.tsx204
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/InputSecured.stories.tsx61
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/InputSecured.tsx186
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/InputSelector.tsx94
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/InputStock.stories.tsx162
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/InputStock.tsx224
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/InputTab.tsx90
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/InputTaxes.tsx147
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/InputToggle.tsx91
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/InputWithAddon.tsx116
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/JumpToElementById.tsx59
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/TextField.tsx71
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/useField.tsx92
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/useGroupField.tsx41
-rw-r--r--packages/auditor-backoffice-ui/src/components/index.stories.ts (renamed from packages/merchant-backend-ui/.storybook/.babelrc)12
-rw-r--r--packages/auditor-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx124
-rw-r--r--packages/auditor-backoffice-ui/src/components/menu/LangSelector.tsx (renamed from packages/demobank-ui/src/components/menu/LangSelector.tsx)78
-rw-r--r--packages/auditor-backoffice-ui/src/components/menu/NavigationBar.tsx (renamed from packages/demobank-ui/src/components/menu/NavigationBar.tsx)27
-rw-r--r--packages/auditor-backoffice-ui/src/components/menu/SideBar.tsx284
-rw-r--r--packages/auditor-backoffice-ui/src/components/menu/index.tsx237
-rw-r--r--packages/auditor-backoffice-ui/src/components/modal/index.tsx496
-rw-r--r--packages/auditor-backoffice-ui/src/components/notifications/CreatedSuccessfully.tsx57
-rw-r--r--packages/auditor-backoffice-ui/src/components/notifications/Notifications.stories.tsx62
-rw-r--r--packages/auditor-backoffice-ui/src/components/notifications/index.tsx (renamed from packages/demobank-ui/src/components/Notifications.tsx)23
-rw-r--r--packages/auditor-backoffice-ui/src/components/picker/DatePicker.tsx (renamed from packages/demobank-ui/src/components/picker/DatePicker.tsx)114
-rw-r--r--packages/auditor-backoffice-ui/src/components/picker/DurationPicker.stories.tsx (renamed from packages/demobank-ui/src/components/picker/DurationPicker.stories.tsx)4
-rw-r--r--packages/auditor-backoffice-ui/src/components/picker/DurationPicker.tsx (renamed from packages/demobank-ui/src/components/picker/DurationPicker.tsx)9
-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.ts70
-rw-r--r--packages/auditor-backoffice-ui/src/context/config.ts (renamed from packages/merchant-backend-ui/src/context/config.ts)16
-rw-r--r--packages/auditor-backoffice-ui/src/context/instance.ts (renamed from packages/merchant-backend-ui/src/context/instance.ts)21
-rw-r--r--packages/auditor-backoffice-ui/src/custom.d.ts42
-rw-r--r--packages/auditor-backoffice-ui/src/declaration.d.ts1793
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/async.ts (renamed from packages/demobank-ui/src/hooks/async.ts)7
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/backend.ts477
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/bank.ts217
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/deposit_confirmations.ts161
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/index.ts151
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/instance.test.ts741
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/instance.ts313
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/listener.ts (renamed from packages/merchant-backend-ui/src/hooks/listener.ts)53
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/notifications.ts (renamed from packages/merchant-backend-ui/src/hooks/notifications.ts)42
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/order.test.ts587
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/order.ts289
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/otp.ts223
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/product.test.ts362
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/product.ts177
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/reserve.test.ts448
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/reserves.ts181
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/templates.ts266
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/testing.tsx180
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/transfer.test.ts254
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/transfer.ts188
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/urls.ts303
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/useSettings.ts73
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/webhooks.ts178
-rw-r--r--packages/auditor-backoffice-ui/src/i18n/de.po2742
-rw-r--r--packages/auditor-backoffice-ui/src/i18n/en.po2741
-rw-r--r--packages/auditor-backoffice-ui/src/i18n/es.po2854
-rw-r--r--packages/auditor-backoffice-ui/src/i18n/fr.po2741
-rw-r--r--packages/auditor-backoffice-ui/src/i18n/it.po2742
-rw-r--r--packages/auditor-backoffice-ui/src/i18n/poheader (renamed from packages/merchant-backend-ui/src/i18n/poheader)2
-rw-r--r--packages/auditor-backoffice-ui/src/i18n/strings-prelude (renamed from packages/merchant-backend-ui/src/i18n/strings-prelude)2
-rw-r--r--packages/auditor-backoffice-ui/src/i18n/strings.ts9655
-rw-r--r--packages/auditor-backoffice-ui/src/i18n/sv.po2741
-rw-r--r--packages/auditor-backoffice-ui/src/i18n/taler-merchant-backoffice.pot2726
-rw-r--r--packages/auditor-backoffice-ui/src/index.html45
-rw-r--r--packages/auditor-backoffice-ui/src/index.tsx24
-rw-r--r--packages/auditor-backoffice-ui/src/paths/admin/create/Create.stories.tsx57
-rw-r--r--packages/auditor-backoffice-ui/src/paths/admin/create/CreatePage.tsx257
-rw-r--r--packages/auditor-backoffice-ui/src/paths/admin/create/InstanceCreatedSuccessfully.tsx74
-rw-r--r--packages/auditor-backoffice-ui/src/paths/admin/create/index.tsx82
-rw-r--r--packages/auditor-backoffice-ui/src/paths/admin/create/stories.tsx52
-rw-r--r--packages/auditor-backoffice-ui/src/paths/admin/index.stories.ts18
-rw-r--r--packages/auditor-backoffice-ui/src/paths/admin/list/TableActive.tsx287
-rw-r--r--packages/auditor-backoffice-ui/src/paths/admin/list/View.stories.tsx90
-rw-r--r--packages/auditor-backoffice-ui/src/paths/admin/list/View.tsx110
-rw-r--r--packages/auditor-backoffice-ui/src/paths/admin/list/index.tsx140
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/accounts/create/Create.stories.tsx28
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx173
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/accounts/create/index.tsx65
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/accounts/list/List.stories.tsx (renamed from packages/merchant-backoffice-ui/tests/__mocks__/setupTests.ts)14
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/accounts/list/ListPage.tsx64
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/accounts/list/Table.tsx385
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/accounts/list/index.tsx107
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/accounts/update/Update.stories.tsx32
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx195
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/accounts/update/index.tsx96
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/Create.stories.tsx43
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/CreatePage.tsx80
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/CreatedSuccessfully.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.tsx (renamed from packages/taler-wallet-webextension/src/wallet/ExchangeAddConfirm.stories.tsx)32
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/Table.tsx249
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/index.tsx126
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/Update.stories.tsx73
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/UpdatePage.tsx99
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/index.tsx95
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/details/DetailPage.tsx83
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/details/index.tsx87
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/details/stories.tsx68
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/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/otp_devices/update/index.tsx102
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/products/create/Create.stories.tsx43
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/products/create/CreatePage.tsx80
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/products/create/CreatedSuccessfully.tsx72
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/products/create/index.tsx47
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/products/list/List.stories.tsx61
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/products/list/Table.tsx496
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/products/list/index.tsx150
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/products/update/Update.stories.tsx73
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/products/update/UpdatePage.tsx99
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/products/update/index.tsx95
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/reserves/create/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.tsx (renamed from packages/merchant-backoffice-ui/src/paths/instance/reserves/create/index.tsx)16
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/reserves/details/DetailPage.tsx (renamed from packages/merchant-backoffice-ui/src/paths/instance/reserves/details/DetailPage.tsx)120
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/reserves/details/Details.stories.tsx (renamed from packages/merchant-backoffice-ui/src/paths/instance/reserves/details/Details.stories.tsx)37
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/reserves/details/RewardInfo.tsx (renamed from packages/merchant-backoffice-ui/src/paths/instance/reserves/details/TipInfo.tsx)27
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/reserves/details/index.tsx (renamed from packages/merchant-backoffice-ui/src/paths/instance/reserves/details/index.tsx)24
-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.tsx (renamed from packages/merchant-backoffice-ui/src/paths/instance/reserves/list/CreatedSuccessfully.tsx)20
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/reserves/list/List.stories.tsx (renamed from packages/merchant-backoffice-ui/src/paths/instance/reserves/list/List.stories.tsx)10
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/reserves/list/Table.tsx (renamed from packages/merchant-backoffice-ui/src/paths/instance/reserves/list/Table.tsx)83
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/reserves/list/index.tsx171
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/create/Create.stories.tsx28
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx259
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/create/index.tsx61
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/list/List.stories.tsx28
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx68
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/list/Table.tsx235
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/list/index.tsx152
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/qr/Qr.stories.tsx27
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx172
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/qr/index.tsx80
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/update/Update.stories.tsx32
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx254
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/update/index.tsx99
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/use/Use.stories.tsx27
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx143
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/use/index.tsx101
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/token/DetailPage.tsx183
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/token/index.tsx106
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/token/stories.tsx28
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/transfers/create/Create.stories.tsx (renamed from packages/merchant-backoffice-ui/src/paths/instance/reserves/create/Create.stories.tsx)29
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx146
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/transfers/create/index.tsx68
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/transfers/list/List.stories.tsx93
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx134
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/transfers/list/Table.tsx229
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/transfers/list/index.tsx118
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/transfers/update/index.tsx (renamed from packages/merchant-backoffice-ui/tests/declarations.d.ts)12
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/update/Update.stories.tsx (renamed from packages/merchant-backoffice-ui/src/paths/instance/details/Details.stories.tsx)18
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/update/UpdatePage.tsx176
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/update/index.tsx118
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/Create.stories.tsx28
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx183
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/index.tsx61
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/List.stories.tsx28
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/ListPage.tsx64
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx218
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/index.tsx109
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/Update.stories.tsx32
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx146
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/index.tsx99
-rw-r--r--packages/auditor-backoffice-ui/src/paths/login/index.tsx202
-rw-r--r--packages/auditor-backoffice-ui/src/paths/notfound/index.tsx34
-rw-r--r--packages/auditor-backoffice-ui/src/paths/settings/index.tsx112
-rw-r--r--packages/auditor-backoffice-ui/src/schemas/index.ts245
-rw-r--r--packages/auditor-backoffice-ui/src/scss/DurationPicker.scss (renamed from packages/demobank-ui/src/scss/DurationPicker.scss)0
-rw-r--r--packages/auditor-backoffice-ui/src/scss/_aside.scss (renamed from packages/demobank-ui/src/scss/_aside.scss)97
-rw-r--r--packages/auditor-backoffice-ui/src/scss/_card.scss (renamed from packages/demobank-ui/src/scss/_card.scss)2
-rw-r--r--packages/auditor-backoffice-ui/src/scss/_custom-calendar.scss (renamed from packages/demobank-ui/src/scss/_custom-calendar.scss)6
-rw-r--r--packages/auditor-backoffice-ui/src/scss/_footer.scss (renamed from packages/demobank-ui/src/scss/_footer.scss)2
-rw-r--r--packages/auditor-backoffice-ui/src/scss/_form.scss (renamed from packages/demobank-ui/src/scss/_form.scss)2
-rw-r--r--packages/auditor-backoffice-ui/src/scss/_hero-bar.scss (renamed from packages/demobank-ui/src/scss/_hero-bar.scss)2
-rw-r--r--packages/auditor-backoffice-ui/src/scss/_loading.scss (renamed from packages/demobank-ui/src/scss/_loading.scss)2
-rw-r--r--packages/auditor-backoffice-ui/src/scss/_main-section.scss (renamed from packages/demobank-ui/src/scss/_main-section.scss)2
-rw-r--r--packages/auditor-backoffice-ui/src/scss/_misc.scss (renamed from packages/demobank-ui/src/scss/_misc.scss)2
-rw-r--r--packages/auditor-backoffice-ui/src/scss/_mixins.scss (renamed from packages/demobank-ui/src/scss/_mixins.scss)4
-rw-r--r--packages/auditor-backoffice-ui/src/scss/_modal.scss (renamed from packages/demobank-ui/src/scss/_modal.scss)2
-rw-r--r--packages/auditor-backoffice-ui/src/scss/_nav-bar.scss (renamed from packages/demobank-ui/src/scss/_nav-bar.scss)2
-rw-r--r--packages/auditor-backoffice-ui/src/scss/_table.scss (renamed from packages/demobank-ui/src/scss/_table.scss)2
-rw-r--r--packages/auditor-backoffice-ui/src/scss/_theme-default.scss (renamed from packages/demobank-ui/src/scss/_theme-default.scss)2
-rw-r--r--packages/auditor-backoffice-ui/src/scss/_tiles.scss (renamed from packages/demobank-ui/src/scss/_tiles.scss)2
-rw-r--r--packages/auditor-backoffice-ui/src/scss/_title-bar.scss (renamed from packages/demobank-ui/src/scss/_title-bar.scss)2
-rw-r--r--packages/auditor-backoffice-ui/src/scss/fonts/XRXV3I6Li01BKofINeaE.ttf (renamed from packages/demobank-ui/src/scss/fonts/XRXV3I6Li01BKofINeaE.ttf)bin43752 -> 43752 bytes
-rw-r--r--packages/auditor-backoffice-ui/src/scss/fonts/nunito.css (renamed from packages/demobank-ui/src/scss/fonts/nunito.css)2
-rw-r--r--packages/auditor-backoffice-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.eot (renamed from packages/demobank-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.eot)bin844600 -> 844600 bytes
-rw-r--r--packages/auditor-backoffice-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.ttf (renamed from packages/demobank-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.ttf)bin844380 -> 844380 bytes
-rw-r--r--packages/auditor-backoffice-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.woff (renamed from packages/demobank-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.woff)bin404384 -> 404384 bytes
-rw-r--r--packages/auditor-backoffice-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.woff2 (renamed from packages/demobank-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.woff2)bin283040 -> 283040 bytes
-rw-r--r--packages/auditor-backoffice-ui/src/scss/icons/materialdesignicons-4.9.95.min.css (renamed from packages/demobank-ui/src/scss/icons/materialdesignicons-4.9.95.min.css)0
-rw-r--r--packages/auditor-backoffice-ui/src/scss/libs/_all.scss (renamed from packages/demobank-ui/src/scss/libs/_all.scss)6
-rw-r--r--packages/auditor-backoffice-ui/src/scss/main.scss195
-rw-r--r--packages/auditor-backoffice-ui/src/scss/toggle.scss51
-rw-r--r--packages/auditor-backoffice-ui/src/stories.test.ts44
-rw-r--r--packages/auditor-backoffice-ui/src/stories.tsx48
-rw-r--r--packages/auditor-backoffice-ui/src/sw.js (renamed from packages/merchant-backoffice-ui/.storybook/.babelrc)14
-rw-r--r--packages/auditor-backoffice-ui/src/utils/amount.ts71
-rw-r--r--packages/auditor-backoffice-ui/src/utils/constants.ts197
-rw-r--r--packages/auditor-backoffice-ui/src/utils/regex.test.ts88
-rw-r--r--packages/auditor-backoffice-ui/src/utils/table.ts57
-rw-r--r--packages/auditor-backoffice-ui/src/utils/types.ts (renamed from packages/merchant-backend-ui/src/utils/types.ts)10
-rwxr-xr-xpackages/auditor-backoffice-ui/test.mjs31
-rw-r--r--packages/auditor-backoffice-ui/tsconfig.json58
-rw-r--r--packages/bank-ui/.eslintrc.cjs28
-rw-r--r--packages/bank-ui/.gitignore4
-rw-r--r--packages/bank-ui/Makefile37
-rw-r--r--packages/bank-ui/README.md24
-rwxr-xr-xpackages/bank-ui/build.mjs28
-rwxr-xr-xpackages/bank-ui/contrib/po2ts (renamed from packages/demobank-ui/contrib/po2ts)0
-rw-r--r--packages/bank-ui/copyleft-header.js15
-rwxr-xr-xpackages/bank-ui/dev.mjs41
-rw-r--r--packages/bank-ui/package.json52
-rw-r--r--packages/bank-ui/postcss.config.js (renamed from packages/taler-wallet-core/src/util/assertUnreachable.ts)10
-rw-r--r--packages/bank-ui/src/Routing.tsx612
-rw-r--r--packages/bank-ui/src/app.tsx231
-rw-r--r--packages/bank-ui/src/assets/empty.pngbin0 -> 2785 bytes
-rw-r--r--packages/bank-ui/src/assets/example/id1.jpg (renamed from packages/demobank-ui/src/assets/example/id1.jpg)bin103558 -> 103558 bytes
-rw-r--r--packages/bank-ui/src/assets/favicon.ico (renamed from packages/demobank-ui/src/assets/favicon.ico)bin15086 -> 15086 bytes
-rw-r--r--packages/bank-ui/src/assets/icons/android-chrome-192x192.pngbin0 -> 14058 bytes
-rw-r--r--packages/bank-ui/src/assets/icons/android-chrome-512x512.pngbin0 -> 51484 bytes
-rw-r--r--packages/bank-ui/src/assets/icons/apple-touch-icon.pngbin0 -> 12746 bytes
-rw-r--r--packages/bank-ui/src/assets/icons/favicon-16x16.pngbin0 -> 626 bytes
-rw-r--r--packages/bank-ui/src/assets/icons/favicon-32x32.pngbin0 -> 1487 bytes
-rw-r--r--packages/bank-ui/src/assets/icons/languageicon.svg48
-rw-r--r--packages/bank-ui/src/assets/icons/mstile-150x150.pngbin0 -> 9050 bytes
-rw-r--r--packages/bank-ui/src/assets/logo-2021.svg9
-rw-r--r--packages/bank-ui/src/assets/logo-white.svg (renamed from packages/demobank-ui/src/assets/logo-white.svg)0
-rw-r--r--packages/bank-ui/src/assets/logo.jpegbin0 -> 39336 bytes
-rw-r--r--packages/bank-ui/src/components/Cashouts/index.ts85
-rw-r--r--packages/bank-ui/src/components/Cashouts/state.ts51
-rw-r--r--packages/bank-ui/src/components/Cashouts/stories.tsx29
-rw-r--r--packages/bank-ui/src/components/Cashouts/test.ts68
-rw-r--r--packages/bank-ui/src/components/Cashouts/views.tsx218
-rw-r--r--packages/bank-ui/src/components/EmptyComponentExample/index.ts56
-rw-r--r--packages/bank-ui/src/components/EmptyComponentExample/state.ts25
-rw-r--r--packages/bank-ui/src/components/EmptyComponentExample/stories.tsx (renamed from packages/merchant-backend-ui/tests/__mocks__/setupTests.ts)17
-rw-r--r--packages/bank-ui/src/components/EmptyComponentExample/test.ts28
-rw-r--r--packages/bank-ui/src/components/EmptyComponentExample/views.tsx25
-rw-r--r--packages/bank-ui/src/components/ErrorLoadingWithDebug.tsx24
-rw-r--r--packages/bank-ui/src/components/QR.tsx (renamed from packages/demobank-ui/src/components/QR.tsx)7
-rw-r--r--packages/bank-ui/src/components/Time.tsx80
-rw-r--r--packages/bank-ui/src/components/Transactions/index.ts84
-rw-r--r--packages/bank-ui/src/components/Transactions/state.ts90
-rw-r--r--packages/bank-ui/src/components/Transactions/stories.tsx (renamed from packages/merchant-backoffice-ui/tests/__mocks__/browserMocks.ts)48
-rw-r--r--packages/bank-ui/src/components/Transactions/test.ts202
-rw-r--r--packages/bank-ui/src/components/Transactions/views.tsx252
-rw-r--r--packages/bank-ui/src/components/index.examples.ts17
-rw-r--r--packages/bank-ui/src/context/settings.ts (renamed from packages/merchant-backoffice-ui/src/context/fetch.ts)36
-rw-r--r--packages/bank-ui/src/context/wallet-integration.ts83
-rw-r--r--packages/bank-ui/src/declaration.d.ts35
-rw-r--r--packages/bank-ui/src/hooks/account.ts313
-rw-r--r--packages/bank-ui/src/hooks/bank-state.ts185
-rw-r--r--packages/bank-ui/src/hooks/form.ts115
-rw-r--r--packages/bank-ui/src/hooks/preferences.ts111
-rw-r--r--packages/bank-ui/src/hooks/regional.ts507
-rw-r--r--packages/bank-ui/src/hooks/session.ts134
-rw-r--r--packages/bank-ui/src/i18n/bank.pot1740
-rw-r--r--packages/bank-ui/src/i18n/de.po1780
-rw-r--r--packages/bank-ui/src/i18n/en.po1784
-rw-r--r--packages/bank-ui/src/i18n/es.po2063
-rw-r--r--packages/bank-ui/src/i18n/fr.po1752
-rw-r--r--packages/bank-ui/src/i18n/it.po1843
-rw-r--r--packages/bank-ui/src/i18n/poheader26
-rw-r--r--packages/bank-ui/src/i18n/strings.ts2295
-rw-r--r--packages/bank-ui/src/i18n/uk.po1743
-rw-r--r--packages/bank-ui/src/index.html (renamed from packages/demobank-ui/src/index.html)14
-rw-r--r--packages/bank-ui/src/index.tsx27
-rw-r--r--packages/bank-ui/src/manifest.json (renamed from packages/demobank-ui/src/manifest.json)0
-rw-r--r--packages/bank-ui/src/pages/AccountPage/index.ts135
-rw-r--r--packages/bank-ui/src/pages/AccountPage/state.ts122
-rw-r--r--packages/bank-ui/src/pages/AccountPage/stories.tsx29
-rw-r--r--packages/bank-ui/src/pages/AccountPage/test.ts31
-rw-r--r--packages/bank-ui/src/pages/AccountPage/views.tsx156
-rw-r--r--packages/bank-ui/src/pages/BankFrame.stories.tsx29
-rw-r--r--packages/bank-ui/src/pages/BankFrame.tsx368
-rw-r--r--packages/bank-ui/src/pages/LoginForm.tsx230
-rw-r--r--packages/bank-ui/src/pages/OperationState/index.ts157
-rw-r--r--packages/bank-ui/src/pages/OperationState/state.ts234
-rw-r--r--packages/bank-ui/src/pages/OperationState/stories.tsx29
-rw-r--r--packages/bank-ui/src/pages/OperationState/test.ts31
-rw-r--r--packages/bank-ui/src/pages/OperationState/views.tsx447
-rw-r--r--packages/bank-ui/src/pages/PaymentOptions.stories.tsx35
-rw-r--r--packages/bank-ui/src/pages/PaymentOptions.tsx239
-rw-r--r--packages/bank-ui/src/pages/PaytoWireTransferForm.stories.tsx35
-rw-r--r--packages/bank-ui/src/pages/PaytoWireTransferForm.tsx1000
-rw-r--r--packages/bank-ui/src/pages/ProfileNavigation.tsx202
-rw-r--r--packages/bank-ui/src/pages/PublicHistoriesPage.tsx98
-rw-r--r--packages/bank-ui/src/pages/QrCodeSection.stories.tsx32
-rw-r--r--packages/bank-ui/src/pages/QrCodeSection.tsx152
-rw-r--r--packages/bank-ui/src/pages/RegistrationPage.tsx425
-rw-r--r--packages/bank-ui/src/pages/ShowNotifications.tsx55
-rw-r--r--packages/bank-ui/src/pages/SolveChallengePage.tsx793
-rw-r--r--packages/bank-ui/src/pages/WalletWithdrawForm.tsx404
-rw-r--r--packages/bank-ui/src/pages/WireTransfer.tsx119
-rw-r--r--packages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx425
-rw-r--r--packages/bank-ui/src/pages/WithdrawalOperationPage.tsx73
-rw-r--r--packages/bank-ui/src/pages/WithdrawalQRCode.tsx310
-rw-r--r--packages/bank-ui/src/pages/account/CashoutListForAccount.tsx86
-rw-r--r--packages/bank-ui/src/pages/account/ShowAccountDetails.tsx491
-rw-r--r--packages/bank-ui/src/pages/account/UpdateAccountPassword.tsx319
-rw-r--r--packages/bank-ui/src/pages/admin/AccountForm.tsx804
-rw-r--r--packages/bank-ui/src/pages/admin/AccountList.tsx234
-rw-r--r--packages/bank-ui/src/pages/admin/AdminHome.tsx623
-rw-r--r--packages/bank-ui/src/pages/admin/CreateNewAccount.tsx217
-rw-r--r--packages/bank-ui/src/pages/admin/DownloadStats.tsx588
-rw-r--r--packages/bank-ui/src/pages/admin/RemoveAccount.tsx273
-rw-r--r--packages/bank-ui/src/pages/index.stories.tsx20
-rw-r--r--packages/bank-ui/src/pages/regional/ConversionConfig.tsx1170
-rw-r--r--packages/bank-ui/src/pages/regional/CreateCashout.tsx717
-rw-r--r--packages/bank-ui/src/pages/regional/ShowCashoutDetails.tsx194
-rw-r--r--packages/bank-ui/src/pages/rnd.ts2907
-rw-r--r--packages/bank-ui/src/scss/main.css3
-rw-r--r--packages/bank-ui/src/settings.json11
-rw-r--r--packages/bank-ui/src/settings.ts112
-rw-r--r--packages/bank-ui/src/stories.test.ts83
-rw-r--r--packages/bank-ui/src/stories.tsx41
-rw-r--r--packages/bank-ui/src/utils.ts439
-rw-r--r--packages/bank-ui/tailwind.config.js28
-rwxr-xr-xpackages/bank-ui/test.mjs32
-rw-r--r--packages/bank-ui/tsconfig.json46
-rw-r--r--packages/challenger-ui/.gitignore4
-rw-r--r--packages/challenger-ui/Makefile36
-rw-r--r--packages/challenger-ui/README.md4
-rwxr-xr-xpackages/challenger-ui/build.mjs43
-rw-r--r--packages/challenger-ui/copyleft-header.js15
-rwxr-xr-xpackages/challenger-ui/create_must.sh25
-rwxr-xr-xpackages/challenger-ui/dev.mjs56
-rw-r--r--packages/challenger-ui/package.json67
-rw-r--r--packages/challenger-ui/postcss.config.js21
-rw-r--r--packages/challenger-ui/src/Routing.tsx270
-rw-r--r--packages/challenger-ui/src/app.tsx168
-rw-r--r--packages/challenger-ui/src/assets/home.svg3
-rw-r--r--packages/challenger-ui/src/assets/logo-2021.svg9
-rw-r--r--packages/challenger-ui/src/assets/people.svg3
-rw-r--r--packages/challenger-ui/src/attempts-exhausted.html88
-rw-r--r--packages/challenger-ui/src/components/CheckChallengeIsUpToDate.tsx132
-rw-r--r--packages/challenger-ui/src/context/settings.ts44
-rw-r--r--packages/challenger-ui/src/enter-address-form.html133
-rw-r--r--packages/challenger-ui/src/enter-email-form.html127
-rw-r--r--packages/challenger-ui/src/enter-file-access-form.html102
-rw-r--r--packages/challenger-ui/src/enter-phone-form.html126
-rw-r--r--packages/challenger-ui/src/enter-tan-form.html117
-rw-r--r--packages/challenger-ui/src/hooks/challenge.ts58
-rw-r--r--packages/challenger-ui/src/hooks/session.ts143
-rw-r--r--packages/challenger-ui/src/i18n/challenger-ui.pot199
-rw-r--r--packages/challenger-ui/src/i18n/poheader26
-rw-r--r--packages/challenger-ui/src/i18n/strings.ts90
-rw-r--r--packages/challenger-ui/src/index.html41
-rw-r--r--packages/challenger-ui/src/index.tsx27
-rw-r--r--packages/challenger-ui/src/internal-error.html89
-rw-r--r--packages/challenger-ui/src/invalid-pin.html87
-rw-r--r--packages/challenger-ui/src/invalid-request.html88
-rw-r--r--packages/challenger-ui/src/main.js2
-rw-r--r--packages/challenger-ui/src/pages/AnswerChallenge.tsx279
-rw-r--r--packages/challenger-ui/src/pages/AskChallenge.tsx263
-rw-r--r--packages/challenger-ui/src/pages/CallengeCompleted.tsx26
-rw-r--r--packages/challenger-ui/src/pages/Frame.tsx69
-rw-r--r--packages/challenger-ui/src/pages/MissingParams.tsx22
-rw-r--r--packages/challenger-ui/src/pages/NonceNotFound.tsx42
-rw-r--r--packages/challenger-ui/src/pages/Setup.tsx82
-rw-r--r--packages/challenger-ui/src/scss/main.css3
-rw-r--r--packages/challenger-ui/src/settings.json3
-rw-r--r--packages/challenger-ui/src/settings.ts83
-rw-r--r--packages/challenger-ui/src/validation-unknown.html89
-rw-r--r--packages/challenger-ui/tailwind.config.js28
-rw-r--r--packages/challenger-ui/tsconfig.json46
-rw-r--r--packages/demobank-ui/.gitignore5
-rw-r--r--packages/demobank-ui/.storybook/main.js57
-rw-r--r--packages/demobank-ui/.storybook/preview.js55
-rw-r--r--packages/demobank-ui/Makefile17
-rw-r--r--packages/demobank-ui/README.md49
-rw-r--r--packages/demobank-ui/TODO49
-rwxr-xr-xpackages/demobank-ui/build-bank-translations.sh32
-rw-r--r--packages/demobank-ui/package.json39
-rw-r--r--packages/demobank-ui/src/.babelrc3
-rw-r--r--packages/demobank-ui/src/components/FileButton.tsx55
-rw-r--r--packages/demobank-ui/src/components/app.tsx44
-rw-r--r--packages/demobank-ui/src/components/fields/DateInput.tsx86
-rw-r--r--packages/demobank-ui/src/components/fields/EmailInput.tsx53
-rw-r--r--packages/demobank-ui/src/components/fields/FileInput.tsx102
-rw-r--r--packages/demobank-ui/src/components/fields/ImageInput.tsx90
-rw-r--r--packages/demobank-ui/src/components/fields/NumberInput.tsx52
-rw-r--r--packages/demobank-ui/src/components/fields/TextInput.tsx64
-rw-r--r--packages/demobank-ui/src/components/menu/SideBar.tsx74
-rw-r--r--packages/demobank-ui/src/components/menu/index.tsx135
-rw-r--r--packages/demobank-ui/src/context/pageState.ts115
-rw-r--r--packages/demobank-ui/src/declaration.d.ts59
-rw-r--r--packages/demobank-ui/src/hooks/backend.ts58
-rw-r--r--packages/demobank-ui/src/hooks/index.ts71
-rw-r--r--packages/demobank-ui/src/i18n/bank.pot258
-rw-r--r--packages/demobank-ui/src/i18n/de.po257
-rw-r--r--packages/demobank-ui/src/i18n/en.po266
-rw-r--r--packages/demobank-ui/src/i18n/it.po258
-rw-r--r--packages/demobank-ui/src/i18n/poheader27
-rw-r--r--packages/demobank-ui/src/i18n/strings.ts221
-rw-r--r--packages/demobank-ui/src/index.tsx8
-rw-r--r--packages/demobank-ui/src/pages/Routing.tsx42
-rw-r--r--packages/demobank-ui/src/pages/home/AccountPage.tsx266
-rw-r--r--packages/demobank-ui/src/pages/home/BankFrame.tsx192
-rw-r--r--packages/demobank-ui/src/pages/home/LoginForm.tsx139
-rw-r--r--packages/demobank-ui/src/pages/home/PaymentOptions.tsx54
-rw-r--r--packages/demobank-ui/src/pages/home/PaytoWireTransferForm.tsx424
-rw-r--r--packages/demobank-ui/src/pages/home/PublicHistoriesPage.tsx185
-rw-r--r--packages/demobank-ui/src/pages/home/QrCodeSection.tsx57
-rw-r--r--packages/demobank-ui/src/pages/home/RegistrationPage.tsx247
-rw-r--r--packages/demobank-ui/src/pages/home/Transactions.tsx90
-rw-r--r--packages/demobank-ui/src/pages/home/WalletWithdrawForm.tsx180
-rw-r--r--packages/demobank-ui/src/pages/home/WithdrawalConfirmationQuestion.tsx304
-rw-r--r--packages/demobank-ui/src/pages/home/WithdrawalQRCode.tsx104
-rw-r--r--packages/demobank-ui/src/pages/home/index.stories.tsx1
-rw-r--r--packages/demobank-ui/src/scss/bank.scss270
-rw-r--r--packages/demobank-ui/src/scss/colors-bank.scss31
-rw-r--r--packages/demobank-ui/src/scss/demo.scss158
-rw-r--r--packages/demobank-ui/src/scss/main.scss4
-rw-r--r--packages/demobank-ui/src/scss/pure.scss1397
-rw-r--r--packages/demobank-ui/src/settings.ts27
-rw-r--r--packages/demobank-ui/src/style/index.css0
-rw-r--r--packages/demobank-ui/src/utils.ts56
-rw-r--r--packages/idb-bridge/.gitignore1
-rw-r--r--packages/idb-bridge/package-lock.json2595
-rw-r--r--packages/idb-bridge/package.json29
-rw-r--r--packages/idb-bridge/src/MemoryBackend.test.ts602
-rw-r--r--packages/idb-bridge/src/MemoryBackend.ts367
-rw-r--r--packages/idb-bridge/src/SqliteBackend.test.ts83
-rw-r--r--packages/idb-bridge/src/SqliteBackend.ts2329
-rw-r--r--packages/idb-bridge/src/backend-common.ts29
-rw-r--r--packages/idb-bridge/src/backend-interface.ts142
-rw-r--r--packages/idb-bridge/src/backends.test.ts740
-rw-r--r--packages/idb-bridge/src/bench.ts110
-rw-r--r--packages/idb-bridge/src/bridge-idb.ts536
-rw-r--r--packages/idb-bridge/src/idb-wpt-ported/abort-in-initial-upgradeneeded.test.ts4
-rw-r--r--packages/idb-bridge/src/idb-wpt-ported/close-in-upgradeneeded.test.ts4
-rw-r--r--packages/idb-bridge/src/idb-wpt-ported/cursor-overloads.test.ts4
-rw-r--r--packages/idb-bridge/src/idb-wpt-ported/event-dispatch-active-flag.test.ts11
-rw-r--r--packages/idb-bridge/src/idb-wpt-ported/idbcursor-advance-index.test.ts4
-rw-r--r--packages/idb-bridge/src/idb-wpt-ported/idbcursor-continue-index.test.ts7
-rw-r--r--packages/idb-bridge/src/idb-wpt-ported/idbcursor-continue-objectstore.test.ts4
-rw-r--r--packages/idb-bridge/src/idb-wpt-ported/idbcursor-delete-exception-order.test.ts4
-rw-r--r--packages/idb-bridge/src/idb-wpt-ported/idbcursor-delete-index.test.ts4
-rw-r--r--packages/idb-bridge/src/idb-wpt-ported/idbcursor-delete-objectstore.test.ts4
-rw-r--r--packages/idb-bridge/src/idb-wpt-ported/idbcursor-reused.test.ts8
-rw-r--r--packages/idb-bridge/src/idb-wpt-ported/idbcursor-update-index.test.ts3
-rw-r--r--packages/idb-bridge/src/idb-wpt-ported/idbfactory-cmp.test.ts6
-rw-r--r--packages/idb-bridge/src/idb-wpt-ported/idbfactory-open.test.ts27
-rw-r--r--packages/idb-bridge/src/idb-wpt-ported/idbindex-get.test.ts6
-rw-r--r--packages/idb-bridge/src/idb-wpt-ported/idbindex-openCursor.test.ts4
-rw-r--r--packages/idb-bridge/src/idb-wpt-ported/idbobjectstore-add-put-exception-order.test.ts6
-rw-r--r--packages/idb-bridge/src/idb-wpt-ported/idbobjectstore-add.test.ts4
-rw-r--r--packages/idb-bridge/src/idb-wpt-ported/idbobjectstore-get.test.ts4
-rw-r--r--packages/idb-bridge/src/idb-wpt-ported/idbobjectstore-put.test.ts4
-rw-r--r--packages/idb-bridge/src/idb-wpt-ported/idbobjectstore-rename-store.test.ts3
-rw-r--r--packages/idb-bridge/src/idb-wpt-ported/idbtransaction-oncomplete.test.ts4
-rw-r--r--packages/idb-bridge/src/idb-wpt-ported/keypath.test.ts6
-rw-r--r--packages/idb-bridge/src/idb-wpt-ported/request-bubble-and-capture.test.ts4
-rw-r--r--packages/idb-bridge/src/idb-wpt-ported/transaction-requestqueue.test.ts4
-rw-r--r--packages/idb-bridge/src/idb-wpt-ported/value.test.ts8
-rw-r--r--packages/idb-bridge/src/idb-wpt-ported/wptsupport.ts27
-rw-r--r--packages/idb-bridge/src/idbpromutil.ts26
-rw-r--r--packages/idb-bridge/src/idbtypes.ts23
-rw-r--r--packages/idb-bridge/src/index.ts11
-rw-r--r--packages/idb-bridge/src/node-sqlite3-impl.ts84
-rw-r--r--packages/idb-bridge/src/sqlite3-interface.ts34
-rw-r--r--packages/idb-bridge/src/testingdb.ts43
-rw-r--r--packages/idb-bridge/src/util/FakeDomEvent.ts103
-rw-r--r--packages/idb-bridge/src/util/FakeEventTarget.ts2
-rw-r--r--packages/idb-bridge/src/util/extractKey.ts4
-rw-r--r--packages/idb-bridge/src/util/fakeDOMStringList.ts2
-rw-r--r--packages/idb-bridge/src/util/key-storage.test.ts39
-rw-r--r--packages/idb-bridge/src/util/key-storage.ts363
-rw-r--r--packages/idb-bridge/src/util/makeStoreKeyValue.test.ts66
-rw-r--r--packages/idb-bridge/src/util/makeStoreKeyValue.ts20
-rw-r--r--packages/idb-bridge/src/util/queueTask.ts5
-rw-r--r--packages/idb-bridge/src/util/structuredClone.test.ts61
-rw-r--r--packages/idb-bridge/src/util/structuredClone.ts251
-rw-r--r--packages/idb-bridge/src/util/valueToKey.ts6
-rw-r--r--packages/idb-bridge/tsconfig.json6
-rw-r--r--packages/merchant-backend-ui/.storybook/main.js82
-rw-r--r--packages/merchant-backend-ui/.storybook/preview.js73
-rw-r--r--packages/merchant-backend-ui/README.md8
-rw-r--r--packages/merchant-backend-ui/babel.config-linaria.json (renamed from packages/taler-wallet-webextension/babel.config-linaria.json)6
-rwxr-xr-xpackages/merchant-backend-ui/build.mjs (renamed from packages/demobank-ui/build.mjs)121
-rw-r--r--packages/merchant-backend-ui/package.json105
-rw-r--r--packages/merchant-backend-ui/render-examples.ts83
-rw-r--r--packages/merchant-backend-ui/rollup.config.js112
-rw-r--r--packages/merchant-backend-ui/src/components/Footer.tsx27
-rw-r--r--packages/merchant-backend-ui/src/components/QR.tsx49
-rw-r--r--packages/merchant-backend-ui/src/context/backend.ts82
-rw-r--r--packages/merchant-backend-ui/src/context/fetch.ts40
-rw-r--r--packages/merchant-backend-ui/src/context/translation.ts59
-rw-r--r--packages/merchant-backend-ui/src/declaration.d.ts5
-rw-r--r--packages/merchant-backend-ui/src/hooks/async.ts76
-rw-r--r--packages/merchant-backend-ui/src/hooks/backend.ts262
-rw-r--r--packages/merchant-backend-ui/src/hooks/index.ts110
-rw-r--r--packages/merchant-backend-ui/src/hooks/instance.ts187
-rw-r--r--packages/merchant-backend-ui/src/hooks/notification.ts43
-rw-r--r--packages/merchant-backend-ui/src/hooks/order.ts217
-rw-r--r--packages/merchant-backend-ui/src/hooks/product.ts223
-rw-r--r--packages/merchant-backend-ui/src/hooks/tips.ts159
-rw-r--r--packages/merchant-backend-ui/src/hooks/transfer.ts150
-rw-r--r--packages/merchant-backend-ui/src/i18n/de.po1057
-rw-r--r--packages/merchant-backend-ui/src/i18n/en.po1057
-rw-r--r--packages/merchant-backend-ui/src/i18n/es.po1065
-rw-r--r--packages/merchant-backend-ui/src/i18n/fr.po1057
-rw-r--r--packages/merchant-backend-ui/src/i18n/index.tsx203
-rw-r--r--packages/merchant-backend-ui/src/i18n/it.po1057
-rw-r--r--packages/merchant-backend-ui/src/i18n/strings.ts3445
-rw-r--r--packages/merchant-backend-ui/src/i18n/sv.po1057
-rw-r--r--packages/merchant-backend-ui/src/i18n/taler-merchant-backoffice.pot1054
-rw-r--r--packages/merchant-backend-ui/src/index.tsx61
-rw-r--r--packages/merchant-backend-ui/src/pages/DepletedTip.tsx60
-rw-r--r--packages/merchant-backend-ui/src/pages/OfferRefund.tsx8
-rw-r--r--packages/merchant-backend-ui/src/pages/OfferTip.stories.tsx45
-rw-r--r--packages/merchant-backend-ui/src/pages/OfferTip.tsx141
-rw-r--r--packages/merchant-backend-ui/src/pages/RequestPayment.tsx19
-rw-r--r--packages/merchant-backend-ui/src/pages/ShowOrderDetails.examples.ts50
-rw-r--r--packages/merchant-backend-ui/src/pages/ShowOrderDetails.tsx97
-rw-r--r--packages/merchant-backend-ui/src/render-examples.ts112
-rw-r--r--packages/merchant-backend-ui/src/utils.ts (renamed from packages/merchant-backend-ui/src/utils/table.ts)32
-rw-r--r--packages/merchant-backend-ui/src/utils/amount.ts69
-rw-r--r--packages/merchant-backend-ui/src/utils/constants.ts47
-rw-r--r--packages/merchant-backend-ui/tests/__mocks__/fileTransformer.js31
-rw-r--r--packages/merchant-backend-ui/tests/funcitons/regex.test.ts87
-rw-r--r--packages/merchant-backend-ui/tests/util.ts62
-rw-r--r--packages/merchant-backend-ui/trim-extension.cjs23
-rw-r--r--packages/merchant-backend-ui/tsconfig.back.json23
-rw-r--r--packages/merchant-backend-ui/tsconfig.json2
-rw-r--r--packages/merchant-backoffice-ui/.eslintrc.cjs28
-rw-r--r--packages/merchant-backoffice-ui/.storybook/main.js57
-rw-r--r--packages/merchant-backoffice-ui/.storybook/preview.js74
-rw-r--r--packages/merchant-backoffice-ui/DESIGN.md195
-rw-r--r--packages/merchant-backoffice-ui/Makefile31
-rw-r--r--packages/merchant-backoffice-ui/README.md64
-rwxr-xr-xpackages/merchant-backoffice-ui/build.mjs139
-rw-r--r--packages/merchant-backoffice-ui/copyleft-header.js2
-rwxr-xr-xpackages/merchant-backoffice-ui/dev.mjs30
-rw-r--r--packages/merchant-backoffice-ui/package.json130
-rw-r--r--packages/merchant-backoffice-ui/preact.config.js2
-rw-r--r--packages/merchant-backoffice-ui/preact.single-config.js2
-rw-r--r--packages/merchant-backoffice-ui/src/AdminRoutes.tsx68
-rw-r--r--packages/merchant-backoffice-ui/src/Application.tsx364
-rw-r--r--packages/merchant-backoffice-ui/src/ApplicationReadyRoutes.tsx120
-rw-r--r--packages/merchant-backoffice-ui/src/Routing.tsx754
-rw-r--r--packages/merchant-backoffice-ui/src/assets/logo-2021.svg9
-rw-r--r--packages/merchant-backoffice-ui/src/components/ErrorLoadingMerchant.tsx146
-rw-r--r--packages/merchant-backoffice-ui/src/components/exception/AsyncButton.tsx34
-rw-r--r--packages/merchant-backoffice-ui/src/components/exception/QR.tsx2
-rw-r--r--packages/merchant-backoffice-ui/src/components/exception/loading.tsx34
-rw-r--r--packages/merchant-backoffice-ui/src/components/exception/login.tsx143
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/FormProvider.tsx84
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/Input.tsx121
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputArray.tsx162
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputBoolean.tsx91
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputCurrency.tsx61
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputDate.tsx27
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputDuration.tsx155
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputGroup.tsx84
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputImage.tsx150
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputLocation.tsx58
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputNumber.tsx47
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputPayto.tsx43
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.stories.tsx47
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx334
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputSearchOnList.tsx204
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputSearchProduct.tsx138
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputSecured.stories.tsx58
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputSecured.tsx227
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputSelector.tsx32
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputStock.stories.tsx2
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputStock.tsx254
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputTab.tsx90
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputTaxes.tsx130
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx91
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputWithAddon.tsx123
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/JumpToElementById.tsx63
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/TextField.tsx66
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/useField.tsx70
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/useGroupField.tsx19
-rw-r--r--packages/merchant-backoffice-ui/src/components/index.stories.ts17
-rw-r--r--packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx101
-rw-r--r--packages/merchant-backoffice-ui/src/components/menu/LangSelector.tsx103
-rw-r--r--packages/merchant-backoffice-ui/src/components/menu/NavigationBar.tsx76
-rw-r--r--packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx203
-rw-r--r--packages/merchant-backoffice-ui/src/components/menu/index.tsx195
-rw-r--r--packages/merchant-backoffice-ui/src/components/modal/index.tsx588
-rw-r--r--packages/merchant-backoffice-ui/src/components/notifications/CreatedSuccessfully.tsx50
-rw-r--r--packages/merchant-backoffice-ui/src/components/notifications/Notifications.stories.tsx57
-rw-r--r--packages/merchant-backoffice-ui/src/components/notifications/index.tsx57
-rw-r--r--packages/merchant-backoffice-ui/src/components/picker/DatePicker.tsx321
-rw-r--r--packages/merchant-backoffice-ui/src/components/picker/DurationPicker.stories.tsx45
-rw-r--r--packages/merchant-backoffice-ui/src/components/picker/DurationPicker.tsx16
-rw-r--r--packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.stories.tsx48
-rw-r--r--packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.tsx118
-rw-r--r--packages/merchant-backoffice-ui/src/components/product/NonInventoryProductForm.tsx225
-rw-r--r--packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx65
-rw-r--r--packages/merchant-backoffice-ui/src/components/product/ProductList.tsx26
-rw-r--r--packages/merchant-backoffice-ui/src/context/backend.ts82
-rw-r--r--packages/merchant-backoffice-ui/src/context/listener.ts35
-rw-r--r--packages/merchant-backoffice-ui/src/context/session.ts246
-rw-r--r--packages/merchant-backoffice-ui/src/context/settings.ts (renamed from packages/merchant-backend-ui/tests/__mocks__/browserMocks.ts)48
-rw-r--r--packages/merchant-backoffice-ui/src/context/translation.ts59
-rw-r--r--packages/merchant-backoffice-ui/src/custom.d.ts12
-rw-r--r--packages/merchant-backoffice-ui/src/declaration.d.ts1429
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/async.ts45
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/backend.ts319
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/bank.ts86
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/index.ts110
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/instance.test.ts741
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/instance.ts326
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/listener.ts44
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/notifications.ts40
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/order.test.ts581
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/order.ts351
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/otp.ts80
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/preference.ts89
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/product.test.ts362
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/product.ts230
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/reserves.ts218
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/templates.ts87
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/testing.tsx192
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/transfer.test.ts260
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/transfer.ts229
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/urls.ts235
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/webhooks.ts118
-rw-r--r--packages/merchant-backoffice-ui/src/i18n/de.po2529
-rw-r--r--packages/merchant-backoffice-ui/src/i18n/en.po2516
-rw-r--r--packages/merchant-backoffice-ui/src/i18n/es.po2959
-rw-r--r--packages/merchant-backoffice-ui/src/i18n/fr.po2529
-rw-r--r--packages/merchant-backoffice-ui/src/i18n/index.tsx215
-rw-r--r--packages/merchant-backoffice-ui/src/i18n/it.po2529
-rw-r--r--packages/merchant-backoffice-ui/src/i18n/poheader2
-rw-r--r--packages/merchant-backoffice-ui/src/i18n/strings-prelude2
-rw-r--r--packages/merchant-backoffice-ui/src/i18n/strings.ts8090
-rw-r--r--packages/merchant-backoffice-ui/src/i18n/sv.po2516
-rw-r--r--packages/merchant-backoffice-ui/src/i18n/taler-merchant-backoffice.pot2518
-rw-r--r--packages/merchant-backoffice-ui/src/index.html45
-rw-r--r--packages/merchant-backoffice-ui/src/index.tsx98
-rw-r--r--packages/merchant-backoffice-ui/src/manifest.json21
-rw-r--r--packages/merchant-backoffice-ui/src/paths/admin/create/Create.stories.tsx62
-rw-r--r--packages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx208
-rw-r--r--packages/merchant-backoffice-ui/src/paths/admin/create/InstanceCreatedSuccessfully.tsx87
-rw-r--r--packages/merchant-backoffice-ui/src/paths/admin/create/index.tsx68
-rw-r--r--packages/merchant-backoffice-ui/src/paths/admin/create/stories.tsx71
-rw-r--r--packages/merchant-backoffice-ui/src/paths/admin/index.stories.ts18
-rw-r--r--packages/merchant-backoffice-ui/src/paths/admin/list/TableActive.tsx310
-rw-r--r--packages/merchant-backoffice-ui/src/paths/admin/list/View.stories.tsx106
-rw-r--r--packages/merchant-backoffice-ui/src/paths/admin/list/View.tsx91
-rw-r--r--packages/merchant-backoffice-ui/src/paths/admin/list/index.tsx78
-rw-r--r--packages/merchant-backoffice-ui/src/paths/index.stories.ts2
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/accounts/create/Create.stories.tsx28
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx197
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx236
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/accounts/list/List.stories.tsx (renamed from packages/merchant-backend-ui/tests/__mocks__/fileMocks.ts)14
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/accounts/list/ListPage.tsx61
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/accounts/list/Table.tsx359
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/accounts/list/index.tsx111
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/accounts/update/Update.stories.tsx32
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx232
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx153
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/details/DetailPage.tsx94
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/details/index.tsx99
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/details/stories.tsx87
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/index.stories.ts (renamed from packages/demobank-ui/.storybook/.babelrc)13
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.stories.tsx57
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx84
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/kyc/list/index.tsx57
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/orders/create/Create.stories.tsx13
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx719
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx162
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/orders/create/index.tsx136
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/orders/details/Detail.stories.tsx41
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx386
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/orders/details/Timeline.tsx25
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/orders/details/index.tsx115
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/orders/list/List.stories.tsx15
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx282
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx139
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx305
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/Create.stories.tsx28
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx181
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatedSuccessfully.tsx98
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/index.tsx70
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/List.stories.tsx28
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/ListPage.tsx59
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx196
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx106
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/Update.stories.tsx32
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/UpdatePage.tsx185
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/index.tsx147
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/products/create/Create.stories.tsx31
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/products/create/CreatePage.tsx77
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/products/create/CreatedSuccessfully.tsx75
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/products/create/index.tsx64
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/products/list/List.stories.tsx62
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx190
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx173
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/products/update/Update.stories.tsx57
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/products/update/UpdatePage.tsx98
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/products/update/index.tsx105
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/reserves/create/CreatePage.tsx168
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.stories.tsx53
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.tsx79
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/reserves/list/AutorizeTipModal.tsx85
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/reserves/list/index.tsx117
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/create/Create.stories.tsx28
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx292
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/create/index.tsx62
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/list/List.stories.tsx28
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx63
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx220
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx152
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/qr/Qr.stories.tsx27
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx154
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/qr/index.tsx66
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/update/Update.stories.tsx32
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx305
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/update/index.tsx97
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/use/Use.stories.tsx27
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx144
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/use/index.tsx108
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx188
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx152
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/token/stories.tsx28
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/transfers/create/Create.stories.tsx30
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx164
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/transfers/create/index.tsx80
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/transfers/list/List.stories.tsx19
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx146
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/transfers/list/Table.tsx85
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx125
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/transfers/update/index.tsx16
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/update/Update.stories.tsx16
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx178
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx98
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/Create.stories.tsx28
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx179
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/index.tsx62
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/List.stories.tsx28
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/ListPage.tsx62
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx203
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/index.tsx105
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/Update.stories.tsx32
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx145
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/index.tsx97
-rw-r--r--packages/merchant-backoffice-ui/src/paths/login/index.tsx169
-rw-r--r--packages/merchant-backoffice-ui/src/paths/notfound/index.tsx62
-rw-r--r--packages/merchant-backoffice-ui/src/paths/settings/index.tsx141
-rw-r--r--packages/merchant-backoffice-ui/src/schemas/index.ts264
-rw-r--r--packages/merchant-backoffice-ui/src/scss/DurationPicker.scss1
-rw-r--r--packages/merchant-backoffice-ui/src/scss/_aside.scss32
-rw-r--r--packages/merchant-backoffice-ui/src/scss/_card.scss6
-rw-r--r--packages/merchant-backoffice-ui/src/scss/_custom-calendar.scss85
-rw-r--r--packages/merchant-backoffice-ui/src/scss/_footer.scss4
-rw-r--r--packages/merchant-backoffice-ui/src/scss/_form.scss35
-rw-r--r--packages/merchant-backoffice-ui/src/scss/_hero-bar.scss10
-rw-r--r--packages/merchant-backoffice-ui/src/scss/_loading.scss2
-rw-r--r--packages/merchant-backoffice-ui/src/scss/_main-section.scss4
-rw-r--r--packages/merchant-backoffice-ui/src/scss/_misc.scss2
-rw-r--r--packages/merchant-backoffice-ui/src/scss/_mixins.scss8
-rw-r--r--packages/merchant-backoffice-ui/src/scss/_modal.scss4
-rw-r--r--packages/merchant-backoffice-ui/src/scss/_nav-bar.scss18
-rw-r--r--packages/merchant-backoffice-ui/src/scss/_table.scss30
-rw-r--r--packages/merchant-backoffice-ui/src/scss/_theme-default.scss2
-rw-r--r--packages/merchant-backoffice-ui/src/scss/_tiles.scss5
-rw-r--r--packages/merchant-backoffice-ui/src/scss/_title-bar.scss10
-rw-r--r--packages/merchant-backoffice-ui/src/scss/fonts/nunito.css6
-rw-r--r--packages/merchant-backoffice-ui/src/scss/icons/materialdesignicons-4.9.95.min.css15108
-rw-r--r--packages/merchant-backoffice-ui/src/scss/libs/_all.scss4
-rw-r--r--packages/merchant-backoffice-ui/src/scss/main.scss16
-rw-r--r--packages/merchant-backoffice-ui/src/scss/toggle.scss67
-rw-r--r--packages/merchant-backoffice-ui/src/settings.ts84
-rw-r--r--packages/merchant-backoffice-ui/src/stories.test.ts44
-rw-r--r--packages/merchant-backoffice-ui/src/stories.tsx24
-rw-r--r--packages/merchant-backoffice-ui/src/sw.js4
-rw-r--r--packages/merchant-backoffice-ui/src/template.html52
-rw-r--r--packages/merchant-backoffice-ui/src/utils/amount.ts69
-rw-r--r--packages/merchant-backoffice-ui/src/utils/constants.ts42
-rw-r--r--packages/merchant-backoffice-ui/src/utils/regex.test.ts88
-rw-r--r--packages/merchant-backoffice-ui/src/utils/switchableAxios.ts66
-rw-r--r--packages/merchant-backoffice-ui/src/utils/table.ts39
-rw-r--r--packages/merchant-backoffice-ui/src/utils/types.ts4
-rwxr-xr-xpackages/merchant-backoffice-ui/test.mjs31
-rw-r--r--packages/merchant-backoffice-ui/tests/__mocks__/fileMocks.ts24
-rw-r--r--packages/merchant-backoffice-ui/tests/__mocks__/fileTransformer.js31
-rw-r--r--packages/merchant-backoffice-ui/tests/axiosMock.ts445
-rw-r--r--packages/merchant-backoffice-ui/tests/context/backend.test.tsx172
-rw-r--r--packages/merchant-backoffice-ui/tests/functions/regex.test.ts87
-rw-r--r--packages/merchant-backoffice-ui/tests/header.test.tsx63
-rw-r--r--packages/merchant-backoffice-ui/tests/hooks/async.test.ts158
-rw-r--r--packages/merchant-backoffice-ui/tests/hooks/listener.test.ts62
-rw-r--r--packages/merchant-backoffice-ui/tests/hooks/notification.test.ts51
-rw-r--r--packages/merchant-backoffice-ui/tests/hooks/swr/index.tsx46
-rw-r--r--packages/merchant-backoffice-ui/tests/hooks/swr/instance.test.ts636
-rw-r--r--packages/merchant-backoffice-ui/tests/hooks/swr/order.test.ts567
-rw-r--r--packages/merchant-backoffice-ui/tests/hooks/swr/product.test.ts338
-rw-r--r--packages/merchant-backoffice-ui/tests/hooks/swr/reserve.test.ts470
-rw-r--r--packages/merchant-backoffice-ui/tests/hooks/swr/transfer.test.ts268
-rw-r--r--packages/merchant-backoffice-ui/tests/stories.test.tsx86
-rw-r--r--packages/merchant-backoffice-ui/tsconfig.json115
-rwxr-xr-xpackages/pogen/bin/pogen2
-rw-r--r--packages/pogen/example/proj1/package.json5
-rw-r--r--packages/pogen/example/proj1/src/i18n/test.pot (renamed from packages/pogen/example/messages.po)105
-rw-r--r--packages/pogen/example/proj1/src/test.ts (renamed from packages/pogen/example/test.ts)3
-rw-r--r--packages/pogen/example/proj1/src/test2.tsx (renamed from packages/pogen/example/test2.tsx)2
-rw-r--r--packages/pogen/example/proj1/tsconfig.json2
-rw-r--r--packages/pogen/package.json10
-rw-r--r--packages/pogen/src/po2ts.ts104
-rw-r--r--packages/pogen/src/potextract.ts58
-rw-r--r--packages/pogen/tsconfig.json4
-rw-r--r--packages/taler-harness/Makefile47
-rw-r--r--packages/taler-harness/README.md13
-rwxr-xr-xpackages/taler-harness/bin/taler-harness.mjs19
-rwxr-xr-xpackages/taler-harness/build.mjs76
-rw-r--r--packages/taler-harness/debian/README8
-rw-r--r--packages/taler-harness/debian/changelog57
-rw-r--r--packages/taler-harness/debian/control16
-rwxr-xr-xpackages/taler-harness/debian/rules19
-rw-r--r--packages/taler-harness/package.json45
-rw-r--r--packages/taler-harness/src/bench1.ts (renamed from packages/taler-wallet-cli/src/bench1.ts)50
-rw-r--r--packages/taler-harness/src/bench2.ts (renamed from packages/taler-wallet-cli/src/bench2.ts)44
-rw-r--r--packages/taler-harness/src/bench3.ts (renamed from packages/taler-wallet-cli/src/bench3.ts)54
-rw-r--r--packages/taler-harness/src/benchMerchantIDGenerator.ts (renamed from packages/taler-wallet-cli/src/benchMerchantIDGenerator.ts)23
-rw-r--r--packages/taler-harness/src/env-full.ts101
-rw-r--r--packages/taler-harness/src/env1.ts (renamed from packages/taler-wallet-cli/src/env1.ts)0
-rw-r--r--packages/taler-harness/src/harness/denomStructures.ts (renamed from packages/taler-wallet-cli/src/harness/denomStructures.ts)2
-rw-r--r--packages/taler-harness/src/harness/faultInjection.ts (renamed from packages/taler-wallet-cli/src/harness/faultInjection.ts)17
-rw-r--r--packages/taler-harness/src/harness/harness.ts (renamed from packages/taler-wallet-cli/src/harness/harness.ts)1649
-rw-r--r--packages/taler-harness/src/harness/helpers.ts711
-rw-r--r--packages/taler-harness/src/harness/sync.ts (renamed from packages/taler-wallet-cli/src/harness/sync.ts)2
-rw-r--r--packages/taler-harness/src/import-meta-url.js2
-rw-r--r--packages/taler-harness/src/index.ts1288
-rw-r--r--packages/taler-harness/src/integrationtests/test-age-restrictions-deposit.ts114
-rw-r--r--packages/taler-harness/src/integrationtests/test-age-restrictions-merchant.ts174
-rw-r--r--packages/taler-harness/src/integrationtests/test-age-restrictions-mixed-merchant.ts (renamed from packages/taler-wallet-cli/src/integrationtests/test-age-restrictions-mixed-merchant.ts)75
-rw-r--r--packages/taler-harness/src/integrationtests/test-age-restrictions-peer.ts135
-rw-r--r--packages/taler-harness/src/integrationtests/test-bank-api.ts (renamed from packages/taler-wallet-cli/src/integrationtests/test-bank-api.ts)65
-rw-r--r--packages/taler-harness/src/integrationtests/test-claim-loop.ts (renamed from packages/taler-wallet-cli/src/integrationtests/test-claim-loop.ts)47
-rw-r--r--packages/taler-harness/src/integrationtests/test-clause-schnorr.ts (renamed from packages/taler-wallet-cli/src/integrationtests/test-clause-schnorr.ts)34
-rw-r--r--packages/taler-harness/src/integrationtests/test-currency-scope.ts192
-rw-r--r--packages/taler-harness/src/integrationtests/test-denom-lost.ts81
-rw-r--r--packages/taler-harness/src/integrationtests/test-denom-unoffered.ts163
-rw-r--r--packages/taler-harness/src/integrationtests/test-deposit.ts122
-rw-r--r--packages/taler-harness/src/integrationtests/test-exchange-deposit.ts159
-rw-r--r--packages/taler-harness/src/integrationtests/test-exchange-management-fault.ts (renamed from packages/taler-wallet-cli/src/integrationtests/test-exchange-management.ts)67
-rw-r--r--packages/taler-harness/src/integrationtests/test-exchange-management.ts82
-rw-r--r--packages/taler-harness/src/integrationtests/test-exchange-purse.ts224
-rw-r--r--packages/taler-harness/src/integrationtests/test-exchange-timetravel.ts (renamed from packages/taler-wallet-cli/src/integrationtests/test-exchange-timetravel.ts)157
-rw-r--r--packages/taler-harness/src/integrationtests/test-fee-regression.ts (renamed from packages/taler-wallet-cli/src/integrationtests/test-fee-regression.ts)44
-rw-r--r--packages/taler-harness/src/integrationtests/test-forced-selection.ts (renamed from packages/taler-wallet-cli/src/integrationtests/test-forced-selection.ts)41
-rw-r--r--packages/taler-harness/src/integrationtests/test-kyc.ts422
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-bank.ts231
-rw-r--r--packages/taler-harness/src/integrationtests/test-merchant-exchange-confusion.ts (renamed from packages/taler-wallet-cli/src/integrationtests/test-merchant-exchange-confusion.ts)67
-rw-r--r--packages/taler-harness/src/integrationtests/test-merchant-instances-delete.ts (renamed from packages/taler-wallet-cli/src/integrationtests/test-merchant-instances-delete.ts)50
-rw-r--r--packages/taler-harness/src/integrationtests/test-merchant-instances-urls.ts (renamed from packages/taler-wallet-cli/src/integrationtests/test-merchant-instances-urls.ts)28
-rw-r--r--packages/taler-harness/src/integrationtests/test-merchant-instances.ts (renamed from packages/taler-wallet-cli/src/integrationtests/test-merchant-instances.ts)85
-rw-r--r--packages/taler-harness/src/integrationtests/test-merchant-longpolling.ts (renamed from packages/taler-wallet-cli/src/integrationtests/test-merchant-longpolling.ts)67
-rw-r--r--packages/taler-harness/src/integrationtests/test-merchant-refund-api.ts (renamed from packages/taler-wallet-cli/src/integrationtests/test-merchant-refund-api.ts)128
-rw-r--r--packages/taler-harness/src/integrationtests/test-merchant-spec-public-orders.ts (renamed from packages/taler-wallet-cli/src/integrationtests/test-merchant-spec-public-orders.ts)193
-rw-r--r--packages/taler-harness/src/integrationtests/test-multiexchange.ts172
-rw-r--r--packages/taler-harness/src/integrationtests/test-otp.ts119
-rw-r--r--packages/taler-harness/src/integrationtests/test-pay-paid.ts (renamed from packages/taler-wallet-cli/src/integrationtests/test-pay-paid.ts)81
-rw-r--r--packages/taler-harness/src/integrationtests/test-payment-abort.ts164
-rw-r--r--packages/taler-harness/src/integrationtests/test-payment-claim.ts (renamed from packages/taler-wallet-cli/src/integrationtests/test-payment-claim.ts)59
-rw-r--r--packages/taler-harness/src/integrationtests/test-payment-deleted.ts104
-rw-r--r--packages/taler-harness/src/integrationtests/test-payment-expired.ts132
-rw-r--r--packages/taler-harness/src/integrationtests/test-payment-fault.ts (renamed from packages/taler-wallet-cli/src/integrationtests/test-payment-fault.ts)123
-rw-r--r--packages/taler-harness/src/integrationtests/test-payment-forgettable.ts (renamed from packages/taler-wallet-cli/src/integrationtests/test-payment-forgettable.ts)30
-rw-r--r--packages/taler-harness/src/integrationtests/test-payment-idempotency.ts (renamed from packages/taler-wallet-cli/src/integrationtests/test-payment-idempotency.ts)41
-rw-r--r--packages/taler-harness/src/integrationtests/test-payment-multiple.ts (renamed from packages/taler-wallet-cli/src/integrationtests/test-payment-multiple.ts)58
-rw-r--r--packages/taler-harness/src/integrationtests/test-payment-share.ts308
-rw-r--r--packages/taler-harness/src/integrationtests/test-payment-template.ts105
-rw-r--r--packages/taler-harness/src/integrationtests/test-payment-transient.ts (renamed from packages/taler-wallet-cli/src/integrationtests/test-payment-transient.ts)72
-rw-r--r--packages/taler-harness/src/integrationtests/test-payment-zero.ts (renamed from packages/taler-wallet-cli/src/integrationtests/test-payment-zero.ts)29
-rw-r--r--packages/taler-harness/src/integrationtests/test-payment.ts (renamed from packages/taler-wallet-cli/src/integrationtests/test-payment.ts)39
-rw-r--r--packages/taler-harness/src/integrationtests/test-paywall-flow.ts (renamed from packages/taler-wallet-cli/src/integrationtests/test-paywall-flow.ts)94
-rw-r--r--packages/taler-harness/src/integrationtests/test-peer-pull-large.ts194
-rw-r--r--packages/taler-harness/src/integrationtests/test-peer-push-large.ts177
-rw-r--r--packages/taler-harness/src/integrationtests/test-peer-repair.ts213
-rw-r--r--packages/taler-harness/src/integrationtests/test-peer-to-peer-pull.ts285
-rw-r--r--packages/taler-harness/src/integrationtests/test-peer-to-peer-push.ts264
-rw-r--r--packages/taler-harness/src/integrationtests/test-refund-auto.ts (renamed from packages/taler-wallet-cli/src/integrationtests/test-refund-auto.ts)46
-rw-r--r--packages/taler-harness/src/integrationtests/test-refund-gone.ts (renamed from packages/taler-wallet-cli/src/integrationtests/test-refund-gone.ts)79
-rw-r--r--packages/taler-harness/src/integrationtests/test-refund-incremental.ts (renamed from packages/taler-wallet-cli/src/integrationtests/test-refund-incremental.ts)89
-rw-r--r--packages/taler-harness/src/integrationtests/test-refund.ts (renamed from packages/taler-wallet-cli/src/integrationtests/test-refund.ts)93
-rw-r--r--packages/taler-harness/src/integrationtests/test-revocation.ts (renamed from packages/taler-wallet-cli/src/integrationtests/test-revocation.ts)95
-rw-r--r--packages/taler-harness/src/integrationtests/test-simple-payment.ts58
-rw-r--r--packages/taler-harness/src/integrationtests/test-stored-backups.ts112
-rw-r--r--packages/taler-harness/src/integrationtests/test-timetravel-autorefresh.ts (renamed from packages/taler-wallet-cli/src/integrationtests/test-timetravel-autorefresh.ts)148
-rw-r--r--packages/taler-harness/src/integrationtests/test-timetravel-withdraw.ts (renamed from packages/taler-wallet-cli/src/integrationtests/test-timetravel-withdraw.ts)53
-rw-r--r--packages/taler-harness/src/integrationtests/test-tos-format.ts101
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-backup-basic.ts (renamed from packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-basic.ts)85
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-backup-doublespend.ts (renamed from packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-doublespend.ts)93
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-balance-notifications.ts111
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-balance-zero.ts64
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-balance.ts129
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-blocked-deposit.ts150
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-blocked-pay-merchant.ts142
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-blocked-pay-peer-pull.ts177
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-blocked-pay-peer-push.ts149
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-cli-termination.ts (renamed from packages/taler-wallet-cli/src/integrationtests/test-wallet-balance.ts)79
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-config.ts67
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-cryptoworker.ts (renamed from packages/taler-wallet-cli/src/integrationtests/test-wallet-cryptoworker.ts)13
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-dbless.ts (renamed from packages/taler-wallet-cli/src/integrationtests/test-wallet-dbless.ts)94
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-dd48.ts183
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-denom-expire.ts154
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-dev-experiments.ts48
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-exchange-update.ts165
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-gendb.ts111
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-insufficient-balance.ts166
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-notifications.ts176
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-observability.ts119
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-refresh-errors.ts107
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-refresh.ts200
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-wirefees.ts184
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallettesting.ts (renamed from packages/taler-wallet-cli/src/integrationtests/test-wallettesting.ts)94
-rw-r--r--packages/taler-harness/src/integrationtests/test-withdrawal-abort-bank.ts (renamed from packages/taler-wallet-cli/src/integrationtests/test-withdrawal-abort-bank.ts)42
-rw-r--r--packages/taler-harness/src/integrationtests/test-withdrawal-bank-integrated.ts192
-rw-r--r--packages/taler-harness/src/integrationtests/test-withdrawal-conversion.ts304
-rw-r--r--packages/taler-harness/src/integrationtests/test-withdrawal-fakebank.ts (renamed from packages/taler-wallet-cli/src/integrationtests/test-withdrawal-fakebank.ts)30
-rw-r--r--packages/taler-harness/src/integrationtests/test-withdrawal-fees.ts171
-rw-r--r--packages/taler-harness/src/integrationtests/test-withdrawal-huge.ts (renamed from packages/taler-wallet-cli/src/integrationtests/test-withdrawal-high.ts)55
-rw-r--r--packages/taler-harness/src/integrationtests/test-withdrawal-manual.ts (renamed from packages/taler-wallet-cli/src/integrationtests/test-withdrawal-manual.ts)59
-rw-r--r--packages/taler-harness/src/integrationtests/testrunner.ts (renamed from packages/taler-wallet-cli/src/integrationtests/testrunner.ts)269
-rw-r--r--packages/taler-harness/src/lint.ts (renamed from packages/taler-wallet-cli/src/lint.ts)22
-rw-r--r--packages/taler-harness/src/sandcastle-config.ts10
-rw-r--r--packages/taler-harness/tsconfig.json33
-rw-r--r--packages/taler-util/.eslintrc.cjs28
-rw-r--r--packages/taler-util/Makefile42
-rw-r--r--packages/taler-util/package.json76
-rw-r--r--packages/taler-util/src/MerchantApiClient.ts380
-rw-r--r--packages/taler-util/src/ReserveStatus.ts14
-rw-r--r--packages/taler-util/src/ReserveTransaction.ts13
-rw-r--r--packages/taler-util/src/TaskThrottler.ts160
-rw-r--r--packages/taler-util/src/amounts.test.ts75
-rw-r--r--packages/taler-util/src/amounts.ts229
-rw-r--r--packages/taler-util/src/argon2-impl.missing.ts9
-rw-r--r--packages/taler-util/src/argon2-impl.wasm.ts19
-rw-r--r--packages/taler-util/src/argon2.ts17
-rw-r--r--packages/taler-util/src/backup-types.ts1259
-rw-r--r--packages/taler-util/src/bank-api-client.ts440
-rw-r--r--packages/taler-util/src/bitcoin.ts8
-rw-r--r--packages/taler-util/src/clk.ts61
-rw-r--r--packages/taler-util/src/codec.ts76
-rw-r--r--packages/taler-util/src/compat.d.ts (renamed from packages/merchant-backoffice-ui/src/context/config.ts)25
-rw-r--r--packages/taler-util/src/compat.node.ts64
-rw-r--r--packages/taler-util/src/compat.qtart.ts57
-rw-r--r--packages/taler-util/src/contract-terms.ts2
-rw-r--r--packages/taler-util/src/errors.ts (renamed from packages/taler-wallet-core/src/errors.ts)169
-rw-r--r--packages/taler-util/src/globbing/minimatch.ts14
-rw-r--r--packages/taler-util/src/helpers.ts16
-rw-r--r--packages/taler-util/src/http-client/README.md19
-rw-r--r--packages/taler-util/src/http-client/authentication.ts137
-rw-r--r--packages/taler-util/src/http-client/bank-conversion.ts223
-rw-r--r--packages/taler-util/src/http-client/bank-core.ts1032
-rw-r--r--packages/taler-util/src/http-client/bank-integration.ts179
-rw-r--r--packages/taler-util/src/http-client/bank-revenue.ts130
-rw-r--r--packages/taler-util/src/http-client/bank-wire.ts226
-rw-r--r--packages/taler-util/src/http-client/challenger.ts291
-rw-r--r--packages/taler-util/src/http-client/exchange.ts242
-rw-r--r--packages/taler-util/src/http-client/merchant.ts2343
-rw-r--r--packages/taler-util/src/http-client/officer-account.ts105
-rw-r--r--packages/taler-util/src/http-client/types.ts5392
-rw-r--r--packages/taler-util/src/http-client/utils.ts116
-rw-r--r--packages/taler-util/src/http-common.ts (renamed from packages/taler-wallet-core/src/util/http.ts)292
-rw-r--r--packages/taler-util/src/http-impl.missing.ts (renamed from packages/merchant-backend-ui/src/context/listener.ts)37
-rw-r--r--packages/taler-util/src/http-impl.node.d.ts25
-rw-r--r--packages/taler-util/src/http-impl.node.ts324
-rw-r--r--packages/taler-util/src/http-impl.qtart.ts211
-rw-r--r--packages/taler-util/src/http.ts37
-rw-r--r--packages/taler-util/src/i18n.ts21
-rw-r--r--packages/taler-util/src/iban.test.ts (renamed from packages/merchant-backoffice-ui/src/context/instance.ts)31
-rw-r--r--packages/taler-util/src/iban.ts296
-rw-r--r--packages/taler-util/src/index.browser.ts4
-rw-r--r--packages/taler-util/src/index.node.ts2
-rw-r--r--packages/taler-util/src/index.qtart.ts27
-rw-r--r--packages/taler-util/src/index.ts68
-rw-r--r--packages/taler-util/src/invariants.ts (renamed from packages/taler-wallet-core/src/util/invariants.ts)19
-rw-r--r--packages/taler-util/src/iso-4217.ts1717
-rw-r--r--packages/taler-util/src/kdf.ts35
-rw-r--r--packages/taler-util/src/libeufin-api-types.ts (renamed from packages/taler-wallet-core/src/util/debugFlags.ts)29
-rw-r--r--packages/taler-util/src/libtool-version.test.ts2
-rw-r--r--packages/taler-util/src/logging.ts118
-rw-r--r--packages/taler-util/src/merchant-api-types.ts (renamed from packages/taler-wallet-cli/src/harness/merchantApiTypes.ts)189
-rw-r--r--packages/taler-util/src/notifications.ts456
-rw-r--r--packages/taler-util/src/observability.ts98
-rw-r--r--packages/taler-util/src/operation.ts198
-rw-r--r--packages/taler-util/src/payto.ts133
-rw-r--r--packages/taler-util/src/promises.ts112
-rw-r--r--packages/taler-util/src/qtart.ts35
-rw-r--r--packages/taler-util/src/rfc3548.ts60
-rw-r--r--packages/taler-util/src/sha256.ts2
-rw-r--r--packages/taler-util/src/taler-crypto.test.ts21
-rw-r--r--packages/taler-util/src/taler-crypto.ts338
-rw-r--r--packages/taler-util/src/taler-error-codes.ts1581
-rw-r--r--packages/taler-util/src/taler-types.ts1037
-rw-r--r--packages/taler-util/src/talerconfig.ts227
-rw-r--r--packages/taler-util/src/taleruri.test.ts464
-rw-r--r--packages/taler-util/src/taleruri.ts653
-rw-r--r--packages/taler-util/src/time.test.ts39
-rw-r--r--packages/taler-util/src/time.ts343
-rw-r--r--packages/taler-util/src/timer.ts (renamed from packages/taler-wallet-core/src/util/timer.ts)2
-rw-r--r--packages/taler-util/src/transaction-test-data.ts113
-rw-r--r--packages/taler-util/src/transactions-types.ts384
-rw-r--r--packages/taler-util/src/twrpc-impl.missing.ts26
-rw-r--r--packages/taler-util/src/twrpc-impl.node.ts216
-rw-r--r--packages/taler-util/src/twrpc-impl.qtart.ts26
-rw-r--r--packages/taler-util/src/twrpc.ts63
-rw-r--r--packages/taler-util/src/wallet-types.ts1937
-rw-r--r--packages/taler-util/src/whatwg-url.ts9
-rw-r--r--packages/taler-util/tsconfig.json6
-rw-r--r--packages/taler-wallet-cli/Makefile49
-rw-r--r--packages/taler-wallet-cli/README.md9
-rw-r--r--packages/taler-wallet-cli/assets/.gitkeep0
-rwxr-xr-xpackages/taler-wallet-cli/bin/taler-wallet-cli-local.mjs8
-rwxr-xr-xpackages/taler-wallet-cli/bin/taler-wallet-cli.mjs2
-rwxr-xr-xpackages/taler-wallet-cli/build-node.mjs75
-rwxr-xr-xpackages/taler-wallet-cli/build-qtart.mjs77
-rwxr-xr-xpackages/taler-wallet-cli/build.mjs79
-rw-r--r--packages/taler-wallet-cli/debian/README7
-rw-r--r--packages/taler-wallet-cli/debian/changelog60
-rw-r--r--packages/taler-wallet-cli/debian/control2
-rwxr-xr-xpackages/taler-wallet-cli/debian/rules27
-rw-r--r--packages/taler-wallet-cli/package.json31
-rw-r--r--packages/taler-wallet-cli/rollup.config.js58
-rw-r--r--packages/taler-wallet-cli/src/assets.ts52
-rw-r--r--packages/taler-wallet-cli/src/harness/helpers.ts444
-rw-r--r--packages/taler-wallet-cli/src/harness/libeufin-apis.ts860
-rw-r--r--packages/taler-wallet-cli/src/harness/libeufin.ts886
-rw-r--r--packages/taler-wallet-cli/src/import-meta-url.js2
-rw-r--r--packages/taler-wallet-cli/src/index.ts1327
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/scenario-prompt-payment.ts60
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-age-restrictions-merchant.ts201
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-age-restrictions-peer.ts92
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-denom-unoffered.ts126
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-deposit.ts71
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-bankaccount.ts110
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-bankconnection.ts56
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-facade-bad-request.ts71
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-facade.ts70
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-permissions.ts64
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-sandbox-camt.ts77
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-sandbox-transactions.ts69
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-scheduling.ts106
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-users.ts63
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-libeufin-bad-gateway.ts74
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-libeufin-basic.ts312
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-libeufin-c5x.ts137
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-libeufin-facade-anastasis.ts169
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-libeufin-keyrotation.ts79
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-libeufin-nexus-balance.ts115
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-libeufin-refund-multiple-users.ts104
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-libeufin-refund.ts101
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-libeufin-sandbox-wire-transfer-cli.ts85
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-libeufin-tutorial.ts128
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-payment-on-demo.ts114
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-peer-to-peer-pull.ts101
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-peer-to-peer-push.ts119
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-tipping.ts129
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-withdrawal-bank-integrated.ts91
-rw-r--r--packages/taler-wallet-cli/tsconfig.json8
-rw-r--r--packages/taler-wallet-core/.gitignore1
-rw-r--r--packages/taler-wallet-core/README.md4
-rw-r--r--packages/taler-wallet-core/package.json52
-rw-r--r--packages/taler-wallet-core/src/attention.ts (renamed from packages/taler-wallet-core/src/operations/attention.ts)88
-rw-r--r--packages/taler-wallet-core/src/backup/index.ts (renamed from packages/taler-wallet-core/src/operations/backup/index.ts)810
-rw-r--r--packages/taler-wallet-core/src/backup/state.ts15
-rw-r--r--packages/taler-wallet-core/src/balance.ts769
-rw-r--r--packages/taler-wallet-core/src/bank-api-client.ts279
-rw-r--r--packages/taler-wallet-core/src/coinSelection.test.ts281
-rw-r--r--packages/taler-wallet-core/src/coinSelection.ts1258
-rw-r--r--packages/taler-wallet-core/src/common.ts823
-rw-r--r--packages/taler-wallet-core/src/crypto/cryptoImplementation.ts258
-rw-r--r--packages/taler-wallet-core/src/crypto/cryptoTypes.ts45
-rw-r--r--packages/taler-wallet-core/src/crypto/workers/crypto-dispatcher.test.ts128
-rw-r--r--packages/taler-wallet-core/src/crypto/workers/crypto-dispatcher.ts (renamed from packages/taler-wallet-core/src/crypto/workers/cryptoDispatcher.ts)68
-rw-r--r--packages/taler-wallet-core/src/crypto/workers/nodeThreadWorker.ts8
-rw-r--r--packages/taler-wallet-core/src/crypto/workers/rpcClient.ts92
-rw-r--r--packages/taler-wallet-core/src/crypto/workers/synchronousWorkerFactoryPlain.ts2
-rw-r--r--packages/taler-wallet-core/src/crypto/workers/synchronousWorkerNode.ts174
-rw-r--r--packages/taler-wallet-core/src/crypto/workers/synchronousWorkerPlain.ts4
-rw-r--r--packages/taler-wallet-core/src/crypto/workers/worker-common.ts14
-rw-r--r--packages/taler-wallet-core/src/db-utils.ts236
-rw-r--r--packages/taler-wallet-core/src/db.ts2211
-rw-r--r--packages/taler-wallet-core/src/dbless.ts225
-rw-r--r--packages/taler-wallet-core/src/denomSelection.ts199
-rw-r--r--packages/taler-wallet-core/src/denominations.test.ts (renamed from packages/taler-wallet-core/src/util/denominations.test.ts)4
-rw-r--r--packages/taler-wallet-core/src/denominations.ts (renamed from packages/taler-wallet-core/src/util/denominations.ts)44
-rw-r--r--packages/taler-wallet-core/src/deposits.ts1775
-rw-r--r--packages/taler-wallet-core/src/dev-experiments.ts171
-rw-r--r--packages/taler-wallet-core/src/exchanges.ts2591
-rw-r--r--packages/taler-wallet-core/src/headless/NodeHttpLib.ts183
-rw-r--r--packages/taler-wallet-core/src/host-common.ts60
-rw-r--r--packages/taler-wallet-core/src/host-impl.missing.ts (renamed from packages/taler-wallet-core/src/crypto/workers/synchronousWorkerFactoryNode.ts)33
-rw-r--r--packages/taler-wallet-core/src/host-impl.node.ts (renamed from packages/taler-wallet-core/src/headless/helpers.ts)183
-rw-r--r--packages/taler-wallet-core/src/host-impl.qtart.ts219
-rw-r--r--packages/taler-wallet-core/src/host.ts46
-rw-r--r--packages/taler-wallet-core/src/index.browser.ts2
-rw-r--r--packages/taler-wallet-core/src/index.node.ts11
-rw-r--r--packages/taler-wallet-core/src/index.ts60
-rw-r--r--packages/taler-wallet-core/src/instructedAmountConversion.test.ts767
-rw-r--r--packages/taler-wallet-core/src/instructedAmountConversion.ts872
-rw-r--r--packages/taler-wallet-core/src/internal-wallet-state.ts224
-rw-r--r--packages/taler-wallet-core/src/observable-wrappers.ts291
-rw-r--r--packages/taler-wallet-core/src/operations/README.md7
-rw-r--r--packages/taler-wallet-core/src/operations/backup/export.ts580
-rw-r--r--packages/taler-wallet-core/src/operations/backup/import.ts863
-rw-r--r--packages/taler-wallet-core/src/operations/backup/state.ts109
-rw-r--r--packages/taler-wallet-core/src/operations/balance.ts163
-rw-r--r--packages/taler-wallet-core/src/operations/common.ts411
-rw-r--r--packages/taler-wallet-core/src/operations/deposits.ts655
-rw-r--r--packages/taler-wallet-core/src/operations/exchanges.ts964
-rw-r--r--packages/taler-wallet-core/src/operations/merchants.ts66
-rw-r--r--packages/taler-wallet-core/src/operations/pay-merchant.ts2836
-rw-r--r--packages/taler-wallet-core/src/operations/pay-peer.ts901
-rw-r--r--packages/taler-wallet-core/src/operations/pending.ts377
-rw-r--r--packages/taler-wallet-core/src/operations/refresh.ts1079
-rw-r--r--packages/taler-wallet-core/src/operations/testing.ts478
-rw-r--r--packages/taler-wallet-core/src/operations/tip.ts370
-rw-r--r--packages/taler-wallet-core/src/operations/transactions.ts1165
-rw-r--r--packages/taler-wallet-core/src/operations/withdraw.ts1951
-rw-r--r--packages/taler-wallet-core/src/pay-merchant.ts3503
-rw-r--r--packages/taler-wallet-core/src/pay-peer-common.ts163
-rw-r--r--packages/taler-wallet-core/src/pay-peer-pull-credit.ts1215
-rw-r--r--packages/taler-wallet-core/src/pay-peer-pull-debit.ts1019
-rw-r--r--packages/taler-wallet-core/src/pay-peer-push-credit.ts1036
-rw-r--r--packages/taler-wallet-core/src/pay-peer-push-debit.ts1322
-rw-r--r--packages/taler-wallet-core/src/pending-types.ts209
-rw-r--r--packages/taler-wallet-core/src/query.ts (renamed from packages/taler-wallet-core/src/util/query.ts)554
-rw-r--r--packages/taler-wallet-core/src/recoup.ts (renamed from packages/taler-wallet-core/src/operations/recoup.ts)438
-rw-r--r--packages/taler-wallet-core/src/refresh.ts1883
-rw-r--r--packages/taler-wallet-core/src/remote.ts191
-rw-r--r--packages/taler-wallet-core/src/shepherd.ts1119
-rw-r--r--packages/taler-wallet-core/src/testing.ts871
-rw-r--r--packages/taler-wallet-core/src/transactions.ts2039
-rw-r--r--packages/taler-wallet-core/src/util/asyncMemo.ts87
-rw-r--r--packages/taler-wallet-core/src/util/coinSelection.ts192
-rw-r--r--packages/taler-wallet-core/src/util/promiseUtils.ts60
-rw-r--r--packages/taler-wallet-core/src/util/retries.ts263
-rw-r--r--packages/taler-wallet-core/src/versions.ts58
-rw-r--r--packages/taler-wallet-core/src/wallet-api-types.ts785
-rw-r--r--packages/taler-wallet-core/src/wallet.ts2367
-rw-r--r--packages/taler-wallet-core/src/withdraw.test.ts (renamed from packages/taler-wallet-core/src/operations/withdraw.test.ts)134
-rw-r--r--packages/taler-wallet-core/src/withdraw.ts3258
-rw-r--r--packages/taler-wallet-core/tsconfig.json12
-rwxr-xr-xpackages/taler-wallet-core/watch_test.sh3
-rw-r--r--packages/taler-wallet-embedded/README.md4
-rwxr-xr-xpackages/taler-wallet-embedded/build.mjs68
-rw-r--r--packages/taler-wallet-embedded/package.json31
-rw-r--r--packages/taler-wallet-embedded/rollup.config.js63
-rw-r--r--packages/taler-wallet-embedded/src/index.ts294
-rw-r--r--packages/taler-wallet-embedded/src/wallet-qjs.ts511
-rw-r--r--packages/taler-wallet-embedded/test-embedded.cjs68
-rw-r--r--packages/taler-wallet-embedded/tsconfig.json6
-rw-r--r--packages/taler-wallet-webextension/.eslintrc.cjs28
-rw-r--r--packages/taler-wallet-webextension/README.md4
-rwxr-xr-xpackages/taler-wallet-webextension/build-fast-with-linaria.mjs132
-rwxr-xr-xpackages/taler-wallet-webextension/build.mjs49
-rwxr-xr-xpackages/taler-wallet-webextension/clean_and_build.sh2
-rw-r--r--packages/taler-wallet-webextension/dev-html/.gitignore4
-rw-r--r--packages/taler-wallet-webextension/dev-html/index.html68
-rw-r--r--packages/taler-wallet-webextension/dev-html/static/font/import.css35
-rwxr-xr-xpackages/taler-wallet-webextension/dev.mjs52
-rw-r--r--packages/taler-wallet-webextension/manifest-common.json6
-rw-r--r--packages/taler-wallet-webextension/manifest-v2.json50
-rw-r--r--packages/taler-wallet-webextension/manifest-v3.json36
-rwxr-xr-xpackages/taler-wallet-webextension/pack.sh10
-rw-r--r--packages/taler-wallet-webextension/package.json62
-rw-r--r--packages/taler-wallet-webextension/src/NavigationBar.tsx147
-rw-r--r--packages/taler-wallet-webextension/src/background.dev.ts17
-rw-r--r--packages/taler-wallet-webextension/src/background.ts13
-rw-r--r--packages/taler-wallet-webextension/src/browserWorkerEntry.ts7
-rw-r--r--packages/taler-wallet-webextension/src/chromeBadge.ts2
-rw-r--r--packages/taler-wallet-webextension/src/components/Amount.stories.tsx5
-rw-r--r--packages/taler-wallet-webextension/src/components/AmountField.stories.tsx23
-rw-r--r--packages/taler-wallet-webextension/src/components/AmountField.tsx178
-rw-r--r--packages/taler-wallet-webextension/src/components/BalanceTable.tsx62
-rw-r--r--packages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx246
-rw-r--r--packages/taler-wallet-webextension/src/components/Banner.stories.tsx78
-rw-r--r--packages/taler-wallet-webextension/src/components/Banner.tsx43
-rw-r--r--packages/taler-wallet-webextension/src/components/Checkbox.tsx6
-rw-r--r--packages/taler-wallet-webextension/src/components/CurrentAlerts.tsx147
-rw-r--r--packages/taler-wallet-webextension/src/components/Diagnostics.tsx2
-rw-r--r--packages/taler-wallet-webextension/src/components/EditableText.tsx2
-rw-r--r--packages/taler-wallet-webextension/src/components/EnabledBySettings.tsx38
-rw-r--r--packages/taler-wallet-webextension/src/components/ErrorMessage.tsx19
-rw-r--r--packages/taler-wallet-webextension/src/components/ErrorTalerOperation.tsx13
-rw-r--r--packages/taler-wallet-webextension/src/components/HistoryItem.tsx432
-rw-r--r--packages/taler-wallet-webextension/src/components/Loading.tsx98
-rw-r--r--packages/taler-wallet-webextension/src/components/LogoHeader.tsx2
-rw-r--r--packages/taler-wallet-webextension/src/components/Modal.tsx76
-rw-r--r--packages/taler-wallet-webextension/src/components/MultiActionButton.tsx6
-rw-r--r--packages/taler-wallet-webextension/src/components/Part.tsx14
-rw-r--r--packages/taler-wallet-webextension/src/components/PaymentButtons.tsx239
-rw-r--r--packages/taler-wallet-webextension/src/components/PendingTransactions.stories.tsx8
-rw-r--r--packages/taler-wallet-webextension/src/components/PendingTransactions.tsx180
-rw-r--r--packages/taler-wallet-webextension/src/components/ProductList.tsx89
-rw-r--r--packages/taler-wallet-webextension/src/components/QR.stories.tsx4
-rw-r--r--packages/taler-wallet-webextension/src/components/SelectList.tsx5
-rw-r--r--packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.stories.tsx38
-rw-r--r--packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.tsx106
-rw-r--r--packages/taler-wallet-webextension/src/components/TermsOfService/index.ts39
-rw-r--r--packages/taler-wallet-webextension/src/components/TermsOfService/state.ts146
-rw-r--r--packages/taler-wallet-webextension/src/components/TermsOfService/stories.tsx36
-rw-r--r--packages/taler-wallet-webextension/src/components/TermsOfService/utils.ts31
-rw-r--r--packages/taler-wallet-webextension/src/components/TermsOfService/views.tsx233
-rw-r--r--packages/taler-wallet-webextension/src/components/Time.tsx5
-rw-r--r--packages/taler-wallet-webextension/src/components/TransactionItem.tsx284
-rw-r--r--packages/taler-wallet-webextension/src/components/WalletActivity.tsx1100
-rw-r--r--packages/taler-wallet-webextension/src/components/index.stories.tsx2
-rw-r--r--packages/taler-wallet-webextension/src/components/styled/index.tsx50
-rw-r--r--packages/taler-wallet-webextension/src/context/alert.ts277
-rw-r--r--packages/taler-wallet-webextension/src/context/backend.ts (renamed from packages/demobank-ui/src/context/backend.ts)50
-rw-r--r--packages/taler-wallet-webextension/src/context/devContext.ts69
-rw-r--r--packages/taler-wallet-webextension/src/context/iocContext.ts2
-rw-r--r--packages/taler-wallet-webextension/src/context/translation.ts92
-rw-r--r--packages/taler-wallet-webextension/src/cta/Deposit/index.ts14
-rw-r--r--packages/taler-wallet-webextension/src/cta/Deposit/state.ts27
-rw-r--r--packages/taler-wallet-webextension/src/cta/Deposit/stories.tsx4
-rw-r--r--packages/taler-wallet-webextension/src/cta/Deposit/test.ts105
-rw-r--r--packages/taler-wallet-webextension/src/cta/Deposit/views.tsx31
-rw-r--r--packages/taler-wallet-webextension/src/cta/DevExperiment/index.ts73
-rw-r--r--packages/taler-wallet-webextension/src/cta/DevExperiment/state.ts83
-rw-r--r--packages/taler-wallet-webextension/src/cta/DevExperiment/stories.tsx (renamed from packages/demobank-ui/src/pages/home/QrCodeSection.stories.tsx)16
-rw-r--r--packages/taler-wallet-webextension/src/cta/DevExperiment/test.ts65
-rw-r--r--packages/taler-wallet-webextension/src/cta/DevExperiment/views.tsx74
-rw-r--r--packages/taler-wallet-webextension/src/cta/InvoiceCreate/index.ts23
-rw-r--r--packages/taler-wallet-webextension/src/cta/InvoiceCreate/state.ts103
-rw-r--r--packages/taler-wallet-webextension/src/cta/InvoiceCreate/stories.tsx9
-rw-r--r--packages/taler-wallet-webextension/src/cta/InvoiceCreate/views.tsx98
-rw-r--r--packages/taler-wallet-webextension/src/cta/InvoicePay/index.ts18
-rw-r--r--packages/taler-wallet-webextension/src/cta/InvoicePay/state.ts92
-rw-r--r--packages/taler-wallet-webextension/src/cta/InvoicePay/stories.tsx24
-rw-r--r--packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx79
-rw-r--r--packages/taler-wallet-webextension/src/cta/Payment/index.ts16
-rw-r--r--packages/taler-wallet-webextension/src/cta/Payment/state.ts83
-rw-r--r--packages/taler-wallet-webextension/src/cta/Payment/stories.tsx269
-rw-r--r--packages/taler-wallet-webextension/src/cta/Payment/test.ts542
-rw-r--r--packages/taler-wallet-webextension/src/cta/Payment/views.tsx331
-rw-r--r--packages/taler-wallet-webextension/src/cta/PaymentTemplate/index.ts (renamed from packages/taler-wallet-webextension/src/cta/Tip/index.ts)74
-rw-r--r--packages/taler-wallet-webextension/src/cta/PaymentTemplate/state.ts174
-rw-r--r--packages/taler-wallet-webextension/src/cta/PaymentTemplate/stories.tsx34
-rw-r--r--packages/taler-wallet-webextension/src/cta/PaymentTemplate/test.ts59
-rw-r--r--packages/taler-wallet-webextension/src/cta/PaymentTemplate/views.tsx77
-rw-r--r--packages/taler-wallet-webextension/src/cta/Recovery/index.ts14
-rw-r--r--packages/taler-wallet-webextension/src/cta/Recovery/state.ts54
-rw-r--r--packages/taler-wallet-webextension/src/cta/Recovery/stories.tsx2
-rw-r--r--packages/taler-wallet-webextension/src/cta/Recovery/views.tsx28
-rw-r--r--packages/taler-wallet-webextension/src/cta/Refund/index.ts39
-rw-r--r--packages/taler-wallet-webextension/src/cta/Refund/state.ts99
-rw-r--r--packages/taler-wallet-webextension/src/cta/Refund/stories.tsx62
-rw-r--r--packages/taler-wallet-webextension/src/cta/Refund/test.ts631
-rw-r--r--packages/taler-wallet-webextension/src/cta/Refund/views.tsx128
-rw-r--r--packages/taler-wallet-webextension/src/cta/Tip/state.ts84
-rw-r--r--packages/taler-wallet-webextension/src/cta/Tip/test.ts259
-rw-r--r--packages/taler-wallet-webextension/src/cta/Tip/views.tsx121
-rw-r--r--packages/taler-wallet-webextension/src/cta/TransferCreate/index.ts19
-rw-r--r--packages/taler-wallet-webextension/src/cta/TransferCreate/state.ts134
-rw-r--r--packages/taler-wallet-webextension/src/cta/TransferCreate/stories.tsx9
-rw-r--r--packages/taler-wallet-webextension/src/cta/TransferCreate/views.tsx73
-rw-r--r--packages/taler-wallet-webextension/src/cta/TransferPickup/index.ts19
-rw-r--r--packages/taler-wallet-webextension/src/cta/TransferPickup/state.ts84
-rw-r--r--packages/taler-wallet-webextension/src/cta/TransferPickup/stories.tsx18
-rw-r--r--packages/taler-wallet-webextension/src/cta/TransferPickup/views.tsx82
-rw-r--r--packages/taler-wallet-webextension/src/cta/Withdraw/index.ts74
-rw-r--r--packages/taler-wallet-webextension/src/cta/Withdraw/state.ts292
-rw-r--r--packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx182
-rw-r--r--packages/taler-wallet-webextension/src/cta/Withdraw/test.ts313
-rw-r--r--packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx221
-rw-r--r--packages/taler-wallet-webextension/src/cta/index.stories.ts1
-rw-r--r--packages/taler-wallet-webextension/src/hooks/useAsyncAsHook.ts24
-rw-r--r--packages/taler-wallet-webextension/src/hooks/useAutoOpenPermissions.ts74
-rw-r--r--packages/taler-wallet-webextension/src/hooks/useBackupDeviceName.ts7
-rw-r--r--packages/taler-wallet-webextension/src/hooks/useClipboardPermissions.ts93
-rw-r--r--packages/taler-wallet-webextension/src/hooks/useDiagnostics.ts43
-rw-r--r--packages/taler-wallet-webextension/src/hooks/useIsOnline.ts14
-rw-r--r--packages/taler-wallet-webextension/src/hooks/useLocalStorage.ts80
-rw-r--r--packages/taler-wallet-webextension/src/hooks/useProviderStatus.ts13
-rw-r--r--packages/taler-wallet-webextension/src/hooks/useSelectedExchange.ts41
-rw-r--r--packages/taler-wallet-webextension/src/hooks/useSettings.ts64
-rw-r--r--packages/taler-wallet-webextension/src/hooks/useTalerActionURL.test.ts56
-rw-r--r--packages/taler-wallet-webextension/src/hooks/useWalletDevMode.ts57
-rw-r--r--packages/taler-wallet-webextension/src/i18n/de.po37
-rw-r--r--packages/taler-wallet-webextension/src/i18n/es.po36
-rw-r--r--packages/taler-wallet-webextension/src/i18n/fi.po1967
-rw-r--r--packages/taler-wallet-webextension/src/i18n/fr.po16
-rw-r--r--packages/taler-wallet-webextension/src/i18n/it.po32
-rw-r--r--packages/taler-wallet-webextension/src/i18n/ja.po10
-rw-r--r--packages/taler-wallet-webextension/src/i18n/nl.po1953
-rw-r--r--packages/taler-wallet-webextension/src/i18n/sv.po10
-rw-r--r--packages/taler-wallet-webextension/src/i18n/taler-wallet-webextension.pot366
-rw-r--r--packages/taler-wallet-webextension/src/i18n/tr.po32
-rw-r--r--packages/taler-wallet-webextension/src/i18n/uk.po1956
-rw-r--r--packages/taler-wallet-webextension/src/mui/Alert.stories.tsx33
-rw-r--r--packages/taler-wallet-webextension/src/mui/Alert.tsx26
-rw-r--r--packages/taler-wallet-webextension/src/mui/Avatar.tsx2
-rw-r--r--packages/taler-wallet-webextension/src/mui/Button.stories.tsx4
-rw-r--r--packages/taler-wallet-webextension/src/mui/Button.tsx11
-rw-r--r--packages/taler-wallet-webextension/src/mui/Grid.tsx2
-rw-r--r--packages/taler-wallet-webextension/src/mui/InputFile.tsx2
-rw-r--r--packages/taler-wallet-webextension/src/mui/Menu.stories.tsx3
-rw-r--r--packages/taler-wallet-webextension/src/mui/Menu.tsx2
-rw-r--r--packages/taler-wallet-webextension/src/mui/Modal.tsx4
-rw-r--r--packages/taler-wallet-webextension/src/mui/Paper.tsx7
-rw-r--r--packages/taler-wallet-webextension/src/mui/Popover.tsx6
-rw-r--r--packages/taler-wallet-webextension/src/mui/Portal.tsx8
-rw-r--r--packages/taler-wallet-webextension/src/mui/TextField.tsx4
-rw-r--r--packages/taler-wallet-webextension/src/mui/Typography.tsx4
-rw-r--r--packages/taler-wallet-webextension/src/mui/colors/manipulation.ts2
-rw-r--r--packages/taler-wallet-webextension/src/mui/handlers.ts51
-rw-r--r--packages/taler-wallet-webextension/src/mui/input/FormControl.tsx6
-rw-r--r--packages/taler-wallet-webextension/src/mui/input/FormHelperText.tsx4
-rw-r--r--packages/taler-wallet-webextension/src/mui/input/FormLabel.tsx2
-rw-r--r--packages/taler-wallet-webextension/src/mui/input/InputBase.tsx2
-rw-r--r--packages/taler-wallet-webextension/src/mui/input/InputFilled.tsx24
-rw-r--r--packages/taler-wallet-webextension/src/mui/input/InputLabel.tsx2
-rw-r--r--packages/taler-wallet-webextension/src/mui/input/InputStandard.tsx19
-rw-r--r--packages/taler-wallet-webextension/src/mui/style.tsx8
-rw-r--r--packages/taler-wallet-webextension/src/platform/api.ts269
-rw-r--r--packages/taler-wallet-webextension/src/platform/background.ts23
-rw-r--r--packages/taler-wallet-webextension/src/platform/chrome.ts602
-rw-r--r--packages/taler-wallet-webextension/src/platform/dev.ts126
-rw-r--r--packages/taler-wallet-webextension/src/platform/firefox.ts53
-rw-r--r--packages/taler-wallet-webextension/src/platform/foreground.ts22
-rw-r--r--packages/taler-wallet-webextension/src/popup/Application.tsx266
-rw-r--r--packages/taler-wallet-webextension/src/popup/Balance.stories.tsx181
-rw-r--r--packages/taler-wallet-webextension/src/popup/BalancePage.tsx71
-rw-r--r--packages/taler-wallet-webextension/src/popup/NoBalanceHelp.tsx11
-rw-r--r--packages/taler-wallet-webextension/src/popup/TalerActionFound.stories.tsx16
-rw-r--r--packages/taler-wallet-webextension/src/popup/TalerActionFound.tsx143
-rw-r--r--packages/taler-wallet-webextension/src/popupEntryPoint.dev.tsx3
-rw-r--r--packages/taler-wallet-webextension/src/popupEntryPoint.tsx4
-rw-r--r--packages/taler-wallet-webextension/src/pwa/index.html114
-rw-r--r--packages/taler-wallet-webextension/src/pwa/manifest.json35
-rw-r--r--packages/taler-wallet-webextension/src/pwa/popup.html (renamed from packages/taler-wallet-webextension/dev-html/popup.html)4
-rw-r--r--packages/taler-wallet-webextension/src/pwa/static/font/import.css35
-rw-r--r--packages/taler-wallet-webextension/src/pwa/static/font/roboto-italic-400.ttf (renamed from packages/taler-wallet-webextension/dev-html/static/font/roboto-italic-400.ttf)bin130872 -> 130872 bytes
-rw-r--r--packages/taler-wallet-webextension/src/pwa/static/font/roboto-normal-300.tff (renamed from packages/taler-wallet-webextension/dev-html/static/font/roboto-normal-300.tff)bin128256 -> 128256 bytes
-rw-r--r--packages/taler-wallet-webextension/src/pwa/static/font/roboto-normal-400.ttf (renamed from packages/taler-wallet-webextension/dev-html/static/font/roboto-normal-400.ttf)bin129584 -> 129584 bytes
-rw-r--r--packages/taler-wallet-webextension/src/pwa/static/font/roboto-normal-500.ttf (renamed from packages/taler-wallet-webextension/dev-html/static/font/roboto-normal-500.ttf)bin129768 -> 129768 bytes
-rw-r--r--packages/taler-wallet-webextension/src/pwa/static/font/roboto-normal-700.ttf (renamed from packages/taler-wallet-webextension/dev-html/static/font/roboto-normal-700.ttf)bin128676 -> 128676 bytes
-rw-r--r--packages/taler-wallet-webextension/src/pwa/static/img/taler-logo-128.pngbin0 -> 8941 bytes
-rw-r--r--packages/taler-wallet-webextension/src/pwa/static/img/taler-logo-2022.svg468
-rw-r--r--packages/taler-wallet-webextension/src/pwa/static/img/taler-logo-48.pngbin0 -> 2790 bytes
-rw-r--r--packages/taler-wallet-webextension/src/pwa/static/img/taler-logo-512.pngbin0 -> 39994 bytes
-rw-r--r--packages/taler-wallet-webextension/src/pwa/stories.html (renamed from packages/taler-wallet-webextension/dev-html/stories.html)4
-rw-r--r--packages/taler-wallet-webextension/src/pwa/sw.js6
-rw-r--r--packages/taler-wallet-webextension/src/pwa/tests.html (renamed from packages/taler-wallet-webextension/dev-html/tests.html)4
-rw-r--r--packages/taler-wallet-webextension/src/pwa/wallet.html (renamed from packages/taler-wallet-webextension/dev-html/wallet.html)4
-rw-r--r--packages/taler-wallet-webextension/src/serviceWorkerHttpLib.ts168
-rw-r--r--packages/taler-wallet-webextension/src/stories.test.ts42
-rw-r--r--packages/taler-wallet-webextension/src/stories.tsx23
-rw-r--r--packages/taler-wallet-webextension/src/svg/check_24px.inline.svg (renamed from packages/taler-wallet-webextension/src/svg/check_24px.svg)0
-rw-r--r--packages/taler-wallet-webextension/src/svg/chevron-down.inline.svg (renamed from packages/taler-wallet-webextension/src/svg/chevron-down.svg)0
-rw-r--r--packages/taler-wallet-webextension/src/svg/close_24px.inline.svg (renamed from packages/taler-wallet-webextension/src/svg/close_24px.svg)0
-rw-r--r--packages/taler-wallet-webextension/src/svg/delete_24px.inline.svg (renamed from packages/taler-wallet-webextension/src/svg/delete_24px.svg)0
-rw-r--r--packages/taler-wallet-webextension/src/svg/download_24px.inline.svg (renamed from packages/taler-wallet-webextension/src/svg/download_24px.svg)0
-rw-r--r--packages/taler-wallet-webextension/src/svg/edit_24px.inline.svg (renamed from packages/taler-wallet-webextension/src/svg/edit_24px.svg)0
-rw-r--r--packages/taler-wallet-webextension/src/svg/error_outline_outlined_24px.inline.svg (renamed from packages/taler-wallet-webextension/src/svg/error_outline_outlined_24px.svg)0
-rw-r--r--packages/taler-wallet-webextension/src/svg/info_outlined_24px.inline.svg (renamed from packages/taler-wallet-webextension/src/svg/info_outlined_24px.svg)0
-rw-r--r--packages/taler-wallet-webextension/src/svg/logo-2021.inline.svg9
-rw-r--r--packages/taler-wallet-webextension/src/svg/progress.inline.svg12
-rw-r--r--packages/taler-wallet-webextension/src/svg/qr_code_24px.inline.svg (renamed from packages/taler-wallet-webextension/src/svg/qr_code_24px.svg)0
-rw-r--r--packages/taler-wallet-webextension/src/svg/refresh_24px.inline.svg1
-rw-r--r--packages/taler-wallet-webextension/src/svg/refresh_outlined_24px.inline.svg1
-rw-r--r--packages/taler-wallet-webextension/src/svg/refresh_rounded_24px.inline.svg1
-rw-r--r--packages/taler-wallet-webextension/src/svg/refresh_sharp_24px.inline.svg1
-rw-r--r--packages/taler-wallet-webextension/src/svg/refresh_two_tone_24px.inline.svg1
-rw-r--r--packages/taler-wallet-webextension/src/svg/report_problem_outlined_24px.inline.svg (renamed from packages/taler-wallet-webextension/src/svg/report_problem_outlined_24px.svg)0
-rw-r--r--packages/taler-wallet-webextension/src/svg/ri-bank-line.inline.svg (renamed from packages/taler-wallet-webextension/src/svg/ri-bank-line.svg)0
-rw-r--r--packages/taler-wallet-webextension/src/svg/search_24px.inline.svg4
-rw-r--r--packages/taler-wallet-webextension/src/svg/send_24px.inline.svg (renamed from packages/taler-wallet-webextension/src/svg/send_24px.svg)0
-rw-r--r--packages/taler-wallet-webextension/src/svg/settings_black_24dp.inline.svg (renamed from packages/taler-wallet-webextension/src/svg/settings_black_24dp.svg)0
-rw-r--r--packages/taler-wallet-webextension/src/svg/success_outlined_24px.inline.svg (renamed from packages/taler-wallet-webextension/src/svg/success_outlined_24px.svg)0
-rw-r--r--packages/taler-wallet-webextension/src/svg/upload_24px.inline.svg (renamed from packages/taler-wallet-webextension/src/svg/upload_24px.svg)0
-rw-r--r--packages/taler-wallet-webextension/src/svg/warning_24px.inline.svg (renamed from packages/taler-wallet-webextension/src/svg/warning_24px.svg)0
-rw-r--r--packages/taler-wallet-webextension/src/svg/wifi.inline.svg (renamed from packages/taler-wallet-webextension/src/svg/wifi.svg)0
-rw-r--r--packages/taler-wallet-webextension/src/taler-wallet-interaction-loader.ts372
-rw-r--r--packages/taler-wallet-webextension/src/taler-wallet-interaction-support.ts200
-rw-r--r--packages/taler-wallet-webextension/src/test-utils.ts253
-rw-r--r--packages/taler-wallet-webextension/src/utils/index.ts11
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddBackupProvider/index.ts27
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddBackupProvider/state.ts117
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddBackupProvider/stories.tsx21
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddBackupProvider/test.ts75
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddBackupProvider/views.tsx18
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddExchange/index.ts92
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddExchange/state.ts198
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddExchange/stories.tsx (renamed from packages/merchant-backoffice-ui/src/.babelrc)15
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddExchange/test.ts209
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddExchange/views.tsx251
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddNewActionView.stories.tsx4
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddNewActionView.tsx26
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Application.tsx932
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Backup.stories.tsx66
-rw-r--r--packages/taler-wallet-webextension/src/wallet/BackupPage.tsx65
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DepositPage/index.ts15
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts242
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DepositPage/stories.tsx51
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts531
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DepositPage/views.tsx45
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DestinationSelection/index.ts21
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DestinationSelection/state.ts84
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DestinationSelection/stories.tsx10
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DestinationSelection/test.ts151
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx57
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DeveloperPage.stories.tsx16
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx502
-rw-r--r--packages/taler-wallet-webextension/src/wallet/EmptyComponentExample/index.ts14
-rw-r--r--packages/taler-wallet-webextension/src/wallet/EmptyComponentExample/state.ts3
-rw-r--r--packages/taler-wallet-webextension/src/wallet/EmptyComponentExample/stories.tsx4
-rw-r--r--packages/taler-wallet-webextension/src/wallet/EmptyComponentExample/views.tsx14
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ExchangeAddConfirm.tsx75
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ExchangeAddPage.tsx86
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ExchangeAddSetUrl.stories.tsx68
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ExchangeSelection/index.ts24
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ExchangeSelection/state.ts145
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ExchangeSelection/stories.tsx33
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ExchangeSelection/views.tsx255
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ExchangeSetUrl.tsx211
-rw-r--r--packages/taler-wallet-webextension/src/wallet/History.stories.tsx515
-rw-r--r--packages/taler-wallet-webextension/src/wallet/History.tsx350
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ManageAccount/index.ts14
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ManageAccount/state.ts60
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ManageAccount/stories.tsx32
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx178
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Notifications/index.ts16
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Notifications/state.ts17
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Notifications/stories.tsx21
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Notifications/views.tsx23
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ProviderAddConfirmProvider.stories.tsx6
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ProviderAddPage.tsx25
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ProviderAddSetUrl.stories.tsx12
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ProviderDetail.stories.tsx147
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx72
-rw-r--r--packages/taler-wallet-webextension/src/wallet/QrReader.stories.tsx4
-rw-r--r--packages/taler-wallet-webextension/src/wallet/QrReader.tsx438
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ReserveCreated.stories.tsx107
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ReserveCreated.tsx130
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Settings.stories.tsx96
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Settings.tsx376
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx353
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Transaction.tsx2060
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Welcome.stories.tsx16
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Welcome.tsx92
-rw-r--r--packages/taler-wallet-webextension/src/wallet/index.stories.tsx3
-rw-r--r--packages/taler-wallet-webextension/src/walletEntryPoint.dev.tsx3
-rw-r--r--packages/taler-wallet-webextension/src/walletEntryPoint.tsx4
-rw-r--r--packages/taler-wallet-webextension/src/wxApi.ts224
-rw-r--r--packages/taler-wallet-webextension/src/wxBackend.ts531
-rw-r--r--packages/taler-wallet-webextension/static/img/icon.pngbin830 -> 0 bytes
-rw-r--r--packages/taler-wallet-webextension/static/wallet.html67
-rwxr-xr-xpackages/taler-wallet-webextension/test.mjs (renamed from packages/demobank-ui/dev.mjs)24
-rw-r--r--packages/taler-wallet-webextension/trim-extension.cjs25
-rw-r--r--packages/taler-wallet-webextension/tsconfig.json28
-rw-r--r--packages/web-util/README0
-rw-r--r--packages/web-util/README.md3
-rwxr-xr-xpackages/web-util/build.mjs198
-rw-r--r--packages/web-util/package.json58
-rw-r--r--packages/web-util/src/assets/lang.svg48
-rw-r--r--packages/web-util/src/assets/logo-2021.svg9
-rw-r--r--packages/web-util/src/assets/logo-white.svg45
-rw-r--r--packages/web-util/src/cli.ts4
-rw-r--r--packages/web-util/src/components/Attention.tsx80
-rw-r--r--packages/web-util/src/components/Button.tsx167
-rw-r--r--packages/web-util/src/components/CopyButton.tsx56
-rw-r--r--packages/web-util/src/components/ErrorLoading.tsx147
-rw-r--r--packages/web-util/src/components/ErrorLoadingMerchant.tsx147
-rw-r--r--packages/web-util/src/components/Footer.tsx48
-rw-r--r--packages/web-util/src/components/Header.tsx183
-rw-r--r--packages/web-util/src/components/LangSelector.tsx115
-rw-r--r--packages/web-util/src/components/Loading.tsx (renamed from packages/taler-wallet-webextension/src/components/LoadingError.tsx)37
-rw-r--r--packages/web-util/src/components/NotificationBanner.tsx33
-rw-r--r--packages/web-util/src/components/ShowInputErrorLabel.tsx (renamed from packages/demobank-ui/src/pages/home/ShowInputErrorLabel.tsx)4
-rw-r--r--packages/web-util/src/components/ToastBanner.tsx61
-rw-r--r--packages/web-util/src/components/index.ts12
-rw-r--r--packages/web-util/src/components/utils.ts111
-rw-r--r--packages/web-util/src/context/activity.ts72
-rw-r--r--packages/web-util/src/context/api.ts49
-rw-r--r--packages/web-util/src/context/bank-api.ts224
-rw-r--r--packages/web-util/src/context/challenger-api.ts213
-rw-r--r--packages/web-util/src/context/index.ts11
-rw-r--r--packages/web-util/src/context/merchant-api.ts228
-rw-r--r--packages/web-util/src/context/navigation.ts114
-rw-r--r--packages/web-util/src/context/translation.ts (renamed from packages/demobank-ui/src/context/translation.ts)75
-rw-r--r--packages/web-util/src/context/wallet-integration.ts83
-rw-r--r--packages/web-util/src/declaration.d.ts35
-rw-r--r--packages/web-util/src/forms/Calendar.tsx119
-rw-r--r--packages/web-util/src/forms/Caption.tsx32
-rw-r--r--packages/web-util/src/forms/DefaultForm.tsx83
-rw-r--r--packages/web-util/src/forms/Dialog.tsx15
-rw-r--r--packages/web-util/src/forms/FormProvider.tsx148
-rw-r--r--packages/web-util/src/forms/Group.tsx41
-rw-r--r--packages/web-util/src/forms/InputAbsoluteTime.stories.tsx60
-rw-r--r--packages/web-util/src/forms/InputAbsoluteTime.tsx77
-rw-r--r--packages/web-util/src/forms/InputAmount.stories.tsx59
-rw-r--r--packages/web-util/src/forms/InputAmount.tsx36
-rw-r--r--packages/web-util/src/forms/InputArray.stories.tsx79
-rw-r--r--packages/web-util/src/forms/InputArray.tsx186
-rw-r--r--packages/web-util/src/forms/InputChoiceHorizontal.stories.tsx69
-rw-r--r--packages/web-util/src/forms/InputChoiceHorizontal.tsx87
-rw-r--r--packages/web-util/src/forms/InputChoiceStacked.stories.tsx69
-rw-r--r--packages/web-util/src/forms/InputChoiceStacked.tsx113
-rw-r--r--packages/web-util/src/forms/InputFile.stories.tsx64
-rw-r--r--packages/web-util/src/forms/InputFile.tsx106
-rw-r--r--packages/web-util/src/forms/InputInteger.stories.tsx55
-rw-r--r--packages/web-util/src/forms/InputInteger.tsx24
-rw-r--r--packages/web-util/src/forms/InputLine.stories.tsx59
-rw-r--r--packages/web-util/src/forms/InputLine.tsx268
-rw-r--r--packages/web-util/src/forms/InputSelectMultiple.stories.tsx90
-rw-r--r--packages/web-util/src/forms/InputSelectMultiple.tsx154
-rw-r--r--packages/web-util/src/forms/InputSelectOne.stories.tsx70
-rw-r--r--packages/web-util/src/forms/InputSelectOne.tsx135
-rw-r--r--packages/web-util/src/forms/InputText.stories.tsx59
-rw-r--r--packages/web-util/src/forms/InputText.tsx9
-rw-r--r--packages/web-util/src/forms/InputTextArea.stories.tsx59
-rw-r--r--packages/web-util/src/forms/InputTextArea.tsx9
-rw-r--r--packages/web-util/src/forms/InputToggle.stories.tsx59
-rw-r--r--packages/web-util/src/forms/InputToggle.tsx38
-rw-r--r--packages/web-util/src/forms/TimePicker.tsx109
-rw-r--r--packages/web-util/src/forms/forms.ts134
-rw-r--r--packages/web-util/src/forms/index.stories.ts13
-rw-r--r--packages/web-util/src/forms/index.ts23
-rw-r--r--packages/web-util/src/forms/useField.ts93
-rw-r--r--packages/web-util/src/hooks/index.ts14
-rw-r--r--packages/web-util/src/hooks/useAsyncAsHook.ts91
-rw-r--r--packages/web-util/src/hooks/useLang.ts49
-rw-r--r--packages/web-util/src/hooks/useLocalStorage.ts179
-rw-r--r--packages/web-util/src/hooks/useMemoryStorage.ts71
-rw-r--r--packages/web-util/src/hooks/useNotifications.ts337
-rw-r--r--packages/web-util/src/index.browser.ts10
-rw-r--r--packages/web-util/src/index.build.ts327
-rw-r--r--packages/web-util/src/index.testing.ts3
-rw-r--r--packages/web-util/src/index.ts1
-rw-r--r--packages/web-util/src/live-reload.ts33
-rw-r--r--packages/web-util/src/serve.ts126
-rw-r--r--packages/web-util/src/stories.tsx40
-rw-r--r--packages/web-util/src/tests/hook.ts325
-rw-r--r--packages/web-util/src/tests/mock.ts503
-rw-r--r--packages/web-util/src/tests/swr.ts105
-rw-r--r--packages/web-util/src/utils/base64.ts243
-rw-r--r--packages/web-util/src/utils/http-impl.browser.ts (renamed from packages/taler-wallet-webextension/src/browserHttpLib.ts)146
-rw-r--r--packages/web-util/src/utils/http-impl.sw.ts217
-rw-r--r--packages/web-util/src/utils/observable.ts283
-rw-r--r--packages/web-util/src/utils/request.ts477
-rw-r--r--packages/web-util/src/utils/route.ts126
-rw-r--r--packages/web-util/tsconfig.json22
-rw-r--r--pnpm-lock.yaml21286
-rw-r--r--tsconfig.build.json3
1810 files changed, 282172 insertions, 97934 deletions
diff --git a/.gitmodules b/.gitmodules
index 7cde9e53d..e96bbcfc1 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -4,3 +4,6 @@
[submodule "vendor"]
path = vendor
url = https://git.taler.net/node-vendor.git
+[submodule "contrib/wallet-testdata"]
+ path = contrib/wallet-testdata
+ url = https://git.taler.net/wallet-testdata.git
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index 00c61e8a4..4c931ad04 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -1,18 +1,17 @@
{
- // See https://go.microsoft.com/fwlink/?LinkId=733558
- // for the documentation about the tasks.json format
- "version": "2.0.0",
- "tasks": [
- {
- "type": "typescript",
- "tsconfig": "tsconfig.build.json",
- "problemMatcher": [
- "$tsc"
- ],
- "group": {
- "kind": "build",
- "isDefault": true,
- },
- }
- ]
-} \ No newline at end of file
+ // See https://go.microsoft.com/fwlink/?LinkId=733558
+ // for the documentation about the tasks.json format
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "type": "typescript",
+ "tsconfig": "tsconfig.build.json",
+ "problemMatcher": ["$tsc"],
+ "group": {
+ "kind": "build",
+ "isDefault": true
+ },
+ "label": "tsc: build - tsconfig.build.json"
+ }
+ ]
+}
diff --git a/API_CHANGES.md b/API_CHANGES.md
new file mode 100644
index 000000000..f6fbf17f5
--- /dev/null
+++ b/API_CHANGES.md
@@ -0,0 +1,36 @@
+# API Changes
+
+This files contains all the API changes for the current release:
+
+## wallet-core
+
+- AcceptManualWithdrawalResult.exchangePaytoUris is deprecated
+- WithdrawalExchangeAccountDetails.transferAmount is now optional (if conversion applies)
+- added WithdrawalExchangeAccountDetails.currencySpecification about the transferAmount currency
+- 2023-12-05 dold: added WithdrawalExchangeAccountDetails.{status,conversionError} to inform the client
+ about errors with a particular conversion account instead of failing the whole withdrawal(-info) request.
+- 2023-12-06 dold: added the exchangeBaseUrl to PreparePeerPushCreditResponse, allowing the UI
+ to check the exchange status for the peer push credit.
+- 2023-12-06 dold: added a new getExchangeEntryForUri request, which allows the client to
+ get information about an existing exchange entry with DD48 semantics.
+ The older call "getExchangeDetailedInfo" also computes loads of information
+ for fee comparison and we should eventually rename it to something more appropriate
+ (like getExchangeFeeDetailsForUri).
+- 2023-12-06 dold: Deprecate the tosStatus in the withdrawal details response.
+ This field does not conform to DD48 semantics and the client should
+ request the ToS status separately via a getExchangeEntryForUri request.
+- 2023-12-07 dold: Add the prepareWithdrawExchange request for withdrawals
+ via a taler://withdraw-exchange URI.
+- 2023-12-11 dold: Add exchangeBaseUrl to the checkPeerPushDebit response.
+- 2023-12-11 dold: Add scopeInfo to exchange entry list items.
+- BREAK 2023-12-12 dold: Remove forceUpdate and masterPub arguments from addExchange
+ request. This request has previously been overloaded both to update an
+ exchange entry as well as to add it.
+ To update the entry, updateExchangeEntry should be used instead.
+- 2023-12-12 dold: the getExchangeTos request not accepts an additional
+ acceptLanguage field in the request. The response now contains an optional
+ contentLanguage field that is returned if the exchange reports it.
+- 2023-12-12 2:0:1 dold: The checkPeerPushDebit now returns a maximum
+ expiration date based on the expiry of selected coins.
+- 2023-12-13 3:0:2 dold: getVersion now returns the supported API version
+ ranges for all bank APIs separately.
diff --git a/AUTHORS b/AUTHORS
index 1d3ba2cc7..4c9a53431 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -1,2 +1,3 @@
Florian Dold
Gabor Toth
+Sebastian Marchano
diff --git a/Makefile b/Makefile
index 59655cea5..39a949c51 100644
--- a/Makefile
+++ b/Makefile
@@ -9,6 +9,11 @@ git-archive-all = ./build-system/taler-build-scripts/archive-with-submodules/git
include .config.mk
+# Let recursive Makefiles know that they're being invoked
+# from the top-level makefile.
+export TOPLEVEL := yes
+export TOP_DESTDIR := $(abspath $(DESTDIR))
+
.PHONY: compile
compile:
pnpm install -r --frozen-lockfile
@@ -20,7 +25,10 @@ dist:
$(git-archive-all) \
--include ./configure \
--include ./packages/taler-wallet-cli/configure \
- --include ./packages/demobank-ui/configure \
+ --include ./packages/anastasis-cli/configure \
+ --include ./packages/bank-ui/configure \
+ --include ./packages/taler-harness/configure \
+ --include ./packages/merchant-backoffice-ui/configure \
taler-wallet-$(shell git describe --tags --abbrev=0).tar.gz
# Create tarball with git hash prefix in name
@@ -34,6 +42,53 @@ publish:
pnpm run compile
pnpm publish -r --no-git-checks
+
+.PHONY: prebuilt
+prebuilt:
+ ./contrib/cleanup-prebuilt-dir.sh
+ make backoffice-prebuilt
+ make backend-prebuilt
+ make bank-prebuilt
+ make challenger-prebuilt
+ make aml-backoffice-prebuilt
+ ./contrib/publish-prebuilt-dir.sh
+
+.PHONY: backoffice-prebuilt
+backoffice-prebuilt:
+ pnpm install --frozen-lockfile --filter @gnu-taler/merchant-backoffice-ui...
+ pnpm run --filter @gnu-taler/merchant-backoffice-ui... build
+ ./contrib/copy-backoffice-into-prebuilt.sh
+
+.PHONY: backend-prebuilt
+backend-prebuilt:
+ pnpm install --frozen-lockfile --filter @gnu-taler/merchant-backend-ui...
+ pnpm run --filter @gnu-taler/merchant-backend-ui... build
+ ./contrib/copy-backend-into-prebuilt.sh
+
+.PHONY: aml-backoffice-prebuilt
+aml-backoffice-prebuilt:
+ pnpm install --frozen-lockfile --filter @gnu-taler/aml-backoffice-ui...
+ pnpm run --filter @gnu-taler/aml-backoffice-ui... build
+ ./contrib/copy-aml-backoffice-into-prebuilt.sh
+
+#.PHONY: auditor-backoffice-prebuilt
+#auditor-backoffice-prebuilt:
+# pnpm install --frozen-lockfile --filter @gnu-taler/auditor-backoffice-ui...
+# pnpm run --filter @gnu-taler/auditor-backoffice-ui... build
+# ./contrib/copy-auditor-backoffice-into-prebuilt.sh
+
+.PHONY: challenger-prebuilt
+challenger-prebuilt:
+ pnpm install --frozen-lockfile --filter @gnu-taler/challenger-ui...
+ pnpm run --filter @gnu-taler/challenger-ui... build
+ ./contrib/copy-challenger-into-prebuilt.sh
+
+.PHONY: bank-prebuilt
+bank-prebuilt:
+ pnpm install --frozen-lockfile --filter @gnu-taler/bank-ui...
+ pnpm run --filter @gnu-taler/bank-ui... build
+ ./contrib/copy-bank-into-prebuilt.sh
+
# make documentation from docstrings
.PHONY: typedoc
typedoc:
@@ -65,7 +120,7 @@ anastasis-webui:
.PHONY: anastasis-webui-dist
anastasis-webui-dist: anastasis-webui
- (cd packages/anastasis-webui/dist && zip -r - fonts ui.html) > anastasis-webui.zip
+ (cd packages/anastasis-webui/dist/prod && zip -r - ./*) > anastasis-webui.zip
.PHONY: anastasis-webui-dev
@@ -77,17 +132,48 @@ anastasis-webui-dev:
webextension:
pnpm install --frozen-lockfile --filter @gnu-taler/taler-wallet-webextension...
pnpm run --filter @gnu-taler/taler-wallet-webextension... compile
- cd ./packages/taler-wallet-webextension/ && ./pack.sh dev
+ cd ./packages/taler-wallet-webextension/ && ./pack.sh prod
.PHONY: webextension-dev
webextension-dev:
pnpm install --frozen-lockfile --filter @gnu-taler/taler-wallet-webextension...
pnpm run --filter @gnu-taler/taler-wallet-webextension... dev
+.PHONY: embedded
+embedded:
+ pnpm install --frozen-lockfile --filter @gnu-taler/taler-wallet-embedded...
+ pnpm run --filter @gnu-taler/taler-wallet-embedded... compile
+ @echo built packages/taler-wallet-embedded/dist/taler-wallet-core-qjs.mjs
+
.PHONY: lint
lint:
./node_modules/.bin/eslint --ext '.js,.ts,.tsx' 'src'
-install: compile
- @echo Please run \'make install\' from one of the directories in packages/\'
+.PHONY: install
+# Build and install everything
+install:
+ pnpm install --frozen-lockfile
+ pnpm run compile
+ $(MAKE) -C packages/taler-wallet-cli install-nodeps
+ $(MAKE) -C packages/anastasis-cli install-nodeps
+ $(MAKE) -C packages/taler-harness install-nodeps
+ $(MAKE) -C packages/bank-ui install-nodeps
+ $(MAKE) -C packages/merchant-backoffice-ui install-nodeps
+ $(MAKE) -C packages/aml-backoffice-ui install-nodeps
+ $(MAKE) -C packages/auditor-backoffice-ui install-nodeps
+
+
+.PHONY: install-tools
+# Install taler-wallet-cli, anastasis-cli and taler-harness
+install-tools:
+ pnpm install --frozen-lockfile --filter @gnu-taler/taler-wallet-cli... --filter @gnu-taler/anastasis-cli... --filter @gnu-taler/taler-harness...
+ pnpm run --filter @gnu-taler/taler-wallet-cli... --filter @gnu-taler/anastasis-cli... --filter @gnu-taler/taler-harness... compile
+ $(MAKE) -C packages/taler-wallet-cli install-nodeps
+ $(MAKE) -C packages/anastasis-cli install-nodeps
+ $(MAKE) -C packages/taler-harness install-nodeps
+
+.PHONY: check-migration
+
+check-migration:
+ taler-harness advanced wallet-dbcheck contrib/wallet-testdata/wallet-dbgen-0.9.4-dev.8
diff --git a/README b/README
index 4bf368726..471815c0b 100644
--- a/README
+++ b/README
@@ -27,6 +27,26 @@ The CLI version of the wallet supports the normal GNU installation process.
If you are compiling the code from git, you have to run `./bootstrap` before
running `./configure`.
+## Pushing a new prebuilt version
+
+After compiling run
+
+```shell
+make prebuilt
+```
+
+This will create a directory `prebuilt` with a git subtree,
+compile every prebuilt project, copy everything into this subtree
+and create a commit with the default message mentioning from
+which revision the prebuilt was created.
+When the script completes the prebuilt version can should
+be manually pushed.
+
+```shell
+cd prebuilt
+git push
+```
+
### Building the WebExtension
The WebExtension can be built via the 'webextension' make target:
diff --git a/bootstrap b/bootstrap
index 12eb0afa7..7e5140ee7 100755
--- a/bootstrap
+++ b/bootstrap
@@ -27,5 +27,8 @@ copy_configure() {
our_configure=build-system/taler-build-scripts/configure
copy_configure "$our_configure" ./configure
copy_configure "$our_configure" ./packages/taler-wallet-cli/configure
-copy_configure "$our_configure" ./packages/demobank-ui/configure
+copy_configure "$our_configure" ./packages/anastasis-cli/configure
+copy_configure "$our_configure" ./packages/bank-ui/configure
+copy_configure "$our_configure" ./packages/taler-harness/configure
+copy_configure "$our_configure" ./packages/taler-util/configure
copy_configure "$our_configure" ./packages/merchant-backoffice-ui/configure
diff --git a/build-system/configure.py b/build-system/configure.py
index b127ba780..3c5096240 100644
--- a/build-system/configure.py
+++ b/build-system/configure.py
@@ -19,7 +19,7 @@ b.add_tool(tbc.PosixTool("make"))
b.add_tool(tbc.PosixTool("zip"))
b.add_tool(tbc.PosixTool("find"))
b.add_tool(tbc.PosixTool("jq"))
-b.add_tool(tbc.NodeJsTool(version_spec=">=12"))
+b.add_tool(tbc.NodeJsTool(version_spec=">=18"))
b.add_tool(tbc.GenericTool("npm"))
b.add_tool(tbc.GenericTool("pnpm", hint="Use 'sudo npm install -g pnpm' to install."))
b.run()
diff --git a/build-system/taler-build-scripts b/build-system/taler-build-scripts
-Subproject 23538677f6c6be2a62f38dc6137ecdd1c76b7b1
+Subproject 001f5dd081fc8729ff8def90c4a1c3f93eb8689
diff --git a/contrib/articles/spending.txt b/contrib/articles/spending.txt
index 7f2c716f8..887a80910 100644
--- a/contrib/articles/spending.txt
+++ b/contrib/articles/spending.txt
@@ -45,7 +45,7 @@ If the above parameters have an optimal assignment, then replacing
v'[x] := 0
gives another optimal solution, as otherwise we'd get a better one for the first situation.
-There is however no assurence that t[x] = price mod v[x] for some x in D, so nievely such solutions give you running times like O(price * |D|), which kinda sucks actually. Just one simplified example :
+There is however no assurence that t[x] = price mod v[x] for some x in D, so naively such solutions give you running times like O(price * |D|), which kinda sucks actually. Just one simplified example :
http://www.codeproject.com/Articles/31002/Coin-Change-Problem-Using-Dynamic-Programming
diff --git a/contrib/articles/ui/ui-cameraready.tex b/contrib/articles/ui/ui-cameraready.tex
index ef79d91ba..b7015b8d3 100644
--- a/contrib/articles/ui/ui-cameraready.tex
+++ b/contrib/articles/ui/ui-cameraready.tex
@@ -881,7 +881,7 @@ the page. Then the wallet inspects the response as it may contain
error reports about a failed payment which the wallet has to handle.
By submitting the payment this way, we also ensure that this
intermediate request does not require JavaScript and still does not
-interfer with navigation. Once the Web shop confirms the payment, the
+interfere with navigation. Once the Web shop confirms the payment, the
wallet causes the fulfillment URL to be reloaded.
If the contract hash does not match a payment which the user
@@ -937,7 +937,7 @@ it has the following key advantages:
other users has the expected behavior
of asking the other user to pay for the resource.
\item Asynchronously transmitting coins from injected JavaScript costs
- one roundtrip, but does not interfer with navigation and allows
+ one roundtrip, but does not interfere with navigation and allows
proper error handling.
\item The different pages of the merchant have clear
delineations: the shopping pages conclude by making an offer, and
@@ -1137,7 +1137,7 @@ passes it to the wallet (sample code for this is shown in
Figure~\ref{listing:http-contract}).
Instead of adding any cryptographic logic to the merchant frontend,
-the Taler merchant backend allows the implementor to delegate
+the Taler merchant backend allows the implementer to delegate
coin handling to the payment backend, which validates the coins,
deposits them at the exchange, and finally validates and persists the
receipt from the exchange. The merchant backend then communicates the
diff --git a/contrib/articles/ui/ui.tex b/contrib/articles/ui/ui.tex
index 37cd8b076..4aef43bd4 100644
--- a/contrib/articles/ui/ui.tex
+++ b/contrib/articles/ui/ui.tex
@@ -933,7 +933,7 @@ the page. Then the wallet inspects the response as it may contain
error reports about a failed payment which the wallet has to handle.
By submitting the payment this way, we also ensure that this
intermediate request does not require JavaScript and still does not
-interfer with navigation. Once the Web shop confirms the payment, the
+interfere with navigation. Once the Web shop confirms the payment, the
wallet causes the fulfillment URL to be reloaded.
If the contract hash does not match a payment which the user
@@ -989,7 +989,7 @@ it has the following key advantages:
other users has the expected behavior
of asking the other user to pay for the resource.
\item Asynchronously transmitting coins from injected JavaScript costs
- one roundtrip, but does not interfer with navigation and allows
+ one roundtrip, but does not interfere with navigation and allows
proper error handling.
\item The different pages of the merchant have clear
delineations: the shopping pages conclude by making an offer, and
@@ -1189,7 +1189,7 @@ passes it to the wallet (sample code for this is shown in
Figure~\ref{listing:contract}).
Instead of adding any cryptographic logic to the merchant frontend,
-the Taler merchant backend allows the implementor to delegate
+the Taler merchant backend allows the implementer to delegate
coin handling to the payment backend, which validates the coins,
deposits them at the exchange, and finally validates and persists the
receipt from the exchange. The merchant backend then communicates the
diff --git a/contrib/articles/ui/ui_short.tex b/contrib/articles/ui/ui_short.tex
index 18f0c1de0..9257a997c 100644
--- a/contrib/articles/ui/ui_short.tex
+++ b/contrib/articles/ui/ui_short.tex
@@ -221,7 +221,7 @@ existing infrastructure.
Instead of adding any cryptographic logic to the merchant front-end,
-the generic Taler merchant backend allows the implementor to delegate
+the generic Taler merchant backend allows the implementer to delegate
handling of the coins to the payment backend, which validates the
coins, deposits them at the exchange, and finally validates and
persists the receipt from the exchange. The merchant backend then
diff --git a/contrib/bump-taler-version.mjs b/contrib/bump-taler-version.mjs
new file mode 100755
index 000000000..3a57bd01d
--- /dev/null
+++ b/contrib/bump-taler-version.mjs
@@ -0,0 +1,119 @@
+#!/usr/bin/env node
+
+// Bump the package.json versions in Taler-related packagesin
+// this repository.
+// The version must be in one of the following formats:
+// - x.y.z
+// - x.y.z-dev.n
+
+import * as child_process from "child_process";
+import * as fs from "fs";
+
+let requestedVersion = process.argv[2];
+
+let dry = false;
+if (process.argv.includes("--dry")) {
+ dry = true;
+}
+
+let verMatched = false;
+let verMajor = 0;
+let verMinor = 0;
+let verPatch = 0;
+let verDev = undefined;
+
+// Parse the requested version
+const releaseVerRegex = /^(\d+)[.](\d+)[.](\d+)$/;
+const devVerRegex = /^(\d+)[.](\d+)[.](\d+)-dev[.](\d+)$/;
+
+const releaseMatch = requestedVersion.match(releaseVerRegex);
+if (releaseMatch) {
+ verMatched = true;
+ verMajor = releaseMatch[1];
+ verMinor = releaseMatch[2];
+ verPatch = releaseMatch[3];
+}
+
+if (!verMatched) {
+ const devMatch = requestedVersion.match(devVerRegex);
+ if (devMatch) {
+ verMatched = true;
+ verMajor = devMatch[1];
+ verMinor = devMatch[2];
+ verPatch = devMatch[3];
+ verDev = devMatch[4];
+ }
+}
+
+const packages = fs.readdirSync("packages")
+
+for (const pkg of packages) {
+ const p = `packages/${pkg}/package.json`;
+ const data = JSON.parse(fs.readFileSync(p));
+ console.log(p, data.version);
+ if (!dry) {
+ data.version = requestedVersion;
+ fs.writeFileSync(p, JSON.stringify(data, undefined, 2) + "\n");
+ }
+}
+
+{
+ const p = "packages/taler-wallet-webextension/manifest-common.json";
+ const data = JSON.parse(fs.readFileSync(p));
+ console.log("manifest version", data.version);
+ console.log("manifest version_name", data.version_name);
+ // In manifest.json, we transform x.y.z-dev.n info x.y.z.n.
+ // It's necessary because browsers only allow decimals and dots
+ // in that field.
+ let dottedVer = undefined;
+ if (verDev != null) {
+ dottedVer = `${verMajor}.${verMinor}.${verPatch}.${verDev}`;
+ } else {
+ dottedVer = `${verMajor}.${verMinor}.${verPatch}`;
+ }
+ console.log("new manifest version", dottedVer);
+ if (!dry) {
+ data.version = dottedVer;
+ data.version_name = requestedVersion;
+ fs.writeFileSync(p, JSON.stringify(data, undefined, 2) + "\n");
+ }
+}
+
+let debs = ["taler-wallet-cli", "taler-harness"];
+
+for (const deb of debs) {
+ const p = `packages/${deb}/debian/changelog`;
+ const data = fs.readFileSync(p, {
+ encoding: "utf-8",
+ });
+ const lines = data.split("\n");
+ for (const line of lines) {
+ const s = line.trim();
+ if (s == "") {
+ continue;
+ }
+ const re = /\((.*)\)/;
+ const m = s.match(re);
+ const version = m[1];
+ let pfx = "";
+ if (version != requestedVersion) {
+ pfx = "[!] ";
+ if (!dry) {
+ const dateStr = child_process.execSync("date -R", {
+ encoding: "utf-8",
+ });
+ const entryLines = [
+ `${deb} (${requestedVersion}) unstable; urgency=low`,
+ "",
+ ` * Release ${requestedVersion}`,
+ "",
+ ` -- Florian Dold <dold@taler.net> ${dateStr}`,
+ ``,
+ ];
+ fs.writeFileSync(p, `${entryLines.join("\n")}${data}`);
+ }
+ }
+ console.log(`${pfx}${p} is ${version}`);
+ break;
+ }
+}
diff --git a/contrib/ci/Containerfile b/contrib/ci/Containerfile
new file mode 100644
index 000000000..a9da11ae8
--- /dev/null
+++ b/contrib/ci/Containerfile
@@ -0,0 +1,27 @@
+FROM docker.io/library/node:18-slim
+
+ENV DEBIAN_FRONTEND=noninteractive
+
+RUN apt-get update -yq && \
+ apt-get install -yqq \
+ git \
+ python3 \
+ codespell \
+ python3-distutils \
+ make \
+ zip \
+ jq \
+ # Debian packaging tools \
+ po-debconf \
+ build-essential \
+ debhelper-compat \
+ devscripts \
+ git-buildpackage \
+ && rm -rf /var/lib/apt/lists/*
+
+RUN npm install -g pnpm
+
+# Set our workdir. All subsequent commands will be relative to this path.
+WORKDIR /workdir
+
+CMD ["bash", "/workdir/ci/ci.sh"]
diff --git a/contrib/ci/ci.sh b/contrib/ci/ci.sh
new file mode 100755
index 000000000..0719015b9
--- /dev/null
+++ b/contrib/ci/ci.sh
@@ -0,0 +1,34 @@
+#!/bin/bash
+set -exvuo pipefail
+
+# Requires podman
+# Fails if not found in PATH
+OCI_RUNTIME=$(which podman)
+REPO_NAME=$(basename "${PWD}")
+JOB_NAME="${1}"
+JOB_ARCH=$((grep CONTAINER_ARCH contrib/ci/jobs/${JOB_NAME}/config.ini | cut -d' ' -f 3) || echo "${2:-amd64}")
+JOB_CONTAINER=$((grep CONTAINER_NAME contrib/ci/jobs/${JOB_NAME}/config.ini | cut -d' ' -f 3) || echo "localhost/${REPO_NAME}:${JOB_ARCH}")
+CONTAINER_BUILD=$((grep CONTAINER_BUILD contrib/ci/jobs/${JOB_NAME}/config.ini | cut -d' ' -f 3) || echo "True")
+
+echo "Image name: ${JOB_CONTAINER}"
+
+if [ "${CONTAINER_BUILD}" = "True" ] ; then
+ "${OCI_RUNTIME}" build \
+ --arch "${JOB_ARCH}" \
+ -t "${JOB_CONTAINER}" \
+ -f contrib/ci/Containerfile .
+fi
+
+"${OCI_RUNTIME}" run \
+ --rm \
+ -ti \
+ --arch "${JOB_ARCH}" \
+ --env CI_COMMIT_REF="$(git rev-parse HEAD)" \
+ --volume "${PWD}":/workdir \
+ --workdir /workdir \
+ "${JOB_CONTAINER}" \
+ contrib/ci/jobs/"${JOB_NAME}"/job.sh
+
+top_dir=$(dirname "${BASH_SOURCE[0]}")
+
+#"${top_dir}"/build.sh
diff --git a/contrib/ci/jobs/0-codespell/config.ini b/contrib/ci/jobs/0-codespell/config.ini
new file mode 100644
index 000000000..bd7d73860
--- /dev/null
+++ b/contrib/ci/jobs/0-codespell/config.ini
@@ -0,0 +1,6 @@
+[build]
+HALT_ON_FAILURE = False
+WARN_ON_FAILURE = True
+CONTAINER_BUILD = False
+CONTAINER_NAME = nixery.dev/shell/codespell
+CONTAINER_ARCH = amd64
diff --git a/contrib/ci/jobs/0-codespell/dictionary.txt b/contrib/ci/jobs/0-codespell/dictionary.txt
new file mode 100644
index 000000000..aeace0e9e
--- /dev/null
+++ b/contrib/ci/jobs/0-codespell/dictionary.txt
@@ -0,0 +1,20 @@
+# List of "words" that codespell should ignore in our sources.
+#
+# Note: The word sensitivity depends on how the to-be-ignored word is
+# spelled in codespell_lib/data/dictionary.txt. F.e. if there is a word
+# 'foo' and you add 'Foo' _here_, codespell will continue to complain
+# about 'Foo'.
+#
+aci
+cant
+ect
+fo
+som
+te
+ths
+updateing
+vie
+zar
+nam
+pares
+kwanza
diff --git a/contrib/ci/jobs/0-codespell/job.sh b/contrib/ci/jobs/0-codespell/job.sh
new file mode 100755
index 000000000..9271343e6
--- /dev/null
+++ b/contrib/ci/jobs/0-codespell/job.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+set -exuo pipefail
+
+job_dir=$(dirname "${BASH_SOURCE[0]}")
+
+codespell -q 0 -I "${job_dir}"/dictionary.txt -S "*.bib,*.bst,*.cls,*.json,*.png,*.svg,*.wav,*.gz,*/templating/test?/**,**/auditor/*.sql,**/templating/mustach**,*.fees,*key,*.tag,*.info,*.latexmkrc,*.ecc,*.jpg,*.zkey,*.sqlite,*/contrib/hellos/**,*/vpn/tests/**,*.priv,*.file,*.tgz,*.woff,*.gif,*.odt,*.fee,*.deflate,*.dat,*.jpeg,*.eps,*.odg,*/m4/ax_lib_postgresql.m4,*/m4/libgcrypt.m4,*.rpath,config.status,ABOUT-NLS,*/doc/texinfo.tex,*.PNG,*.??.json,*.docx,*.ods,*.doc,*.docx,*.xcf,*.xlsx,*.ecc,*.ttf,*.woff2,*.eot,*.ttf,*.eot,*.mp4,*.pptx,*.epgz,*.min.js,**/*.map,**/fonts/**,*.pack.js,*.po,*.bbl,*/afl-tests/*,*/.git/**,*.pdf,*.epub,**/signing-key.asc,**/pnpm-lock.yaml,**/*.svg,**/*.cls,**/rfc.bib,**/*.bst,*/cbdc-es.tex,*/cbdc-it.tex,**/ExchangeSelection/example.ts,*/testcurl/test_tricky.c,*/i18n/strings.ts,*/src/anastasis-data.ts,**/doc/flows/main.de.tex,*/vendor/**,*/node_modules/**,*.pnpm-store/**"
diff --git a/contrib/ci/jobs/1-build/build.sh b/contrib/ci/jobs/1-build/build.sh
new file mode 100755
index 000000000..25a38946d
--- /dev/null
+++ b/contrib/ci/jobs/1-build/build.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+set -exuo pipefail
+
+./bootstrap
+./configure --prefix=$HOME/local/
+make
diff --git a/contrib/ci/jobs/1-build/job.sh b/contrib/ci/jobs/1-build/job.sh
new file mode 100755
index 000000000..8d79902c5
--- /dev/null
+++ b/contrib/ci/jobs/1-build/job.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+set -exuo pipefail
+
+job_dir=$(dirname "${BASH_SOURCE[0]}")
+
+"${job_dir}"/build.sh
diff --git a/contrib/ci/jobs/2-docs/docs.sh b/contrib/ci/jobs/2-docs/docs.sh
new file mode 100755
index 000000000..ee6abc2ac
--- /dev/null
+++ b/contrib/ci/jobs/2-docs/docs.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+set -exuo pipefail
+
+./bootstrap
+./configure
+make
+
+pnpm run typedoc
diff --git a/contrib/ci/jobs/2-docs/job.sh b/contrib/ci/jobs/2-docs/job.sh
new file mode 100755
index 000000000..a72bca4ba
--- /dev/null
+++ b/contrib/ci/jobs/2-docs/job.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+set -exuo pipefail
+
+job_dir=$(dirname "${BASH_SOURCE[0]}")
+
+"${job_dir}"/docs.sh
diff --git a/contrib/ci/jobs/3-wallet-cli-deb-package/config.ini b/contrib/ci/jobs/3-wallet-cli-deb-package/config.ini
new file mode 100644
index 000000000..fe4fd36a6
--- /dev/null
+++ b/contrib/ci/jobs/3-wallet-cli-deb-package/config.ini
@@ -0,0 +1,6 @@
+[build]
+HALT_ON_FAILURE = False
+WARN_ON_FAILURE = True
+CONTAINER_BUILD = True
+CONTAINER_NAME = localhost/wallet-core
+CONTAINER_ARCH = amd64
diff --git a/contrib/ci/jobs/3-wallet-cli-deb-package/job.sh b/contrib/ci/jobs/3-wallet-cli-deb-package/job.sh
new file mode 100755
index 000000000..ac9132929
--- /dev/null
+++ b/contrib/ci/jobs/3-wallet-cli-deb-package/job.sh
@@ -0,0 +1,36 @@
+#!/bin/bash
+set -exuo pipefail
+# This file is in the public domain.
+# Helper script to build the latest DEB packages in the container.
+
+
+unset LD_LIBRARY_PATH
+
+# Run the bootstrap script in case it hasn't been done already at this point
+./bootstrap
+
+# gbp wants the debian directory in the root of the git repo
+# so we symlink in the debian directory for the package we want to create
+rm -f ./debian # in case we already have a symlink there
+ln -s packages/taler-wallet-cli/debian .
+
+# Change to our package directory
+cd packages/taler-wallet-cli
+
+# Install build-time dependencies.
+# Update apt cache first
+apt-get update
+apt-get upgrade -y
+mk-build-deps --install --tool='apt-get -o Debug::pkgProblemResolver=yes --no-install-recommends --yes' debian/control
+
+export VERSION="$(../../contrib/ci/jobs/3-wallet-cli-deb-package/version.sh)"
+echo "Building package version ${VERSION}"
+
+EMAIL=none gbp dch --ignore-branch --debian-tag="%(version)s" --git-author --new-version="${VERSION}"
+echo "Current PWD is $PWD"
+./configure --prefix=$HOME/local/
+dpkg-buildpackage -rfakeroot -b -uc -us
+
+ls -alh ../*.deb
+mkdir -p /artifacts/wallet-core/${CI_COMMIT_REF} # Variable comes from CI environment
+mv ../*.deb /artifacts/wallet-core/${CI_COMMIT_REF}/
diff --git a/contrib/ci/jobs/3-wallet-cli-deb-package/version.sh b/contrib/ci/jobs/3-wallet-cli-deb-package/version.sh
new file mode 100755
index 000000000..52031b23a
--- /dev/null
+++ b/contrib/ci/jobs/3-wallet-cli-deb-package/version.sh
@@ -0,0 +1,17 @@
+#!/bin/sh
+set -ex
+
+BRANCH=$(git name-rev --name-only HEAD)
+if [ -z "${BRANCH}" ]; then
+ exit 1
+else
+ # "Unshallow" our checkout, but only our current branch, and exclude the submodules.
+ git fetch --no-recurse-submodules --tags --depth=1000 origin "${BRANCH}"
+ RECENT_VERSION_TAG=$(git describe --tags --match 'v*.*.*' --exclude '*-dev*' --always --abbrev=0 HEAD || exit 1)
+ commits="$(git rev-list ${RECENT_VERSION_TAG}..HEAD --count)"
+ if [ "${commits}" = "0" ]; then
+ git describe --tag HEAD | sed -r 's/^v//' || exit 1
+ else
+ echo $(echo ${RECENT_VERSION_TAG} | sed -r 's/^v//')-${commits}-$(git rev-parse --short=8 HEAD)
+ fi
+fi
diff --git a/contrib/ci/jobs/4-taler-harness-deb-package/config.ini b/contrib/ci/jobs/4-taler-harness-deb-package/config.ini
new file mode 100644
index 000000000..fe4fd36a6
--- /dev/null
+++ b/contrib/ci/jobs/4-taler-harness-deb-package/config.ini
@@ -0,0 +1,6 @@
+[build]
+HALT_ON_FAILURE = False
+WARN_ON_FAILURE = True
+CONTAINER_BUILD = True
+CONTAINER_NAME = localhost/wallet-core
+CONTAINER_ARCH = amd64
diff --git a/contrib/ci/jobs/4-taler-harness-deb-package/job.sh b/contrib/ci/jobs/4-taler-harness-deb-package/job.sh
new file mode 100755
index 000000000..c58c643e6
--- /dev/null
+++ b/contrib/ci/jobs/4-taler-harness-deb-package/job.sh
@@ -0,0 +1,36 @@
+#!/bin/bash
+set -exuo pipefail
+# This file is in the public domain.
+# Helper script to build the latest DEB packages in the container.
+
+
+unset LD_LIBRARY_PATH
+
+# Run the bootstrap script in case it hasn't been done already at this point
+./bootstrap
+
+# gbp wants the debian directory in the root of the git repo
+# so we symlink in the debian directory for the package we want to create
+rm -f ./debian # in case we already have a symlink there
+ln -s packages/taler-harness/debian .
+
+# Change to our package directory
+cd packages/taler-harness
+
+# Install build-time dependencies.
+# Update apt cache first
+apt-get update
+apt-get upgrade -y
+mk-build-deps --install --tool='apt-get -o Debug::pkgProblemResolver=yes --no-install-recommends --yes' debian/control
+
+export VERSION="$(../../contrib/ci/jobs/4-taler-harness-deb-package/version.sh)"
+echo "Building package version ${VERSION}"
+
+EMAIL=none gbp dch --ignore-branch --debian-tag="%(version)s" --git-author --new-version="${VERSION}"
+echo "Current PWD is $PWD"
+./configure --prefix=$HOME/local/
+dpkg-buildpackage -rfakeroot -b -uc -us
+
+ls -alh ../*.deb
+mkdir -p /artifacts/wallet-core/${CI_COMMIT_REF} # Variable comes from CI environment
+mv ../*.deb /artifacts/wallet-core/${CI_COMMIT_REF}/
diff --git a/contrib/ci/jobs/4-taler-harness-deb-package/version.sh b/contrib/ci/jobs/4-taler-harness-deb-package/version.sh
new file mode 100755
index 000000000..52031b23a
--- /dev/null
+++ b/contrib/ci/jobs/4-taler-harness-deb-package/version.sh
@@ -0,0 +1,17 @@
+#!/bin/sh
+set -ex
+
+BRANCH=$(git name-rev --name-only HEAD)
+if [ -z "${BRANCH}" ]; then
+ exit 1
+else
+ # "Unshallow" our checkout, but only our current branch, and exclude the submodules.
+ git fetch --no-recurse-submodules --tags --depth=1000 origin "${BRANCH}"
+ RECENT_VERSION_TAG=$(git describe --tags --match 'v*.*.*' --exclude '*-dev*' --always --abbrev=0 HEAD || exit 1)
+ commits="$(git rev-list ${RECENT_VERSION_TAG}..HEAD --count)"
+ if [ "${commits}" = "0" ]; then
+ git describe --tag HEAD | sed -r 's/^v//' || exit 1
+ else
+ echo $(echo ${RECENT_VERSION_TAG} | sed -r 's/^v//')-${commits}-$(git rev-parse --short=8 HEAD)
+ fi
+fi
diff --git a/contrib/ci/jobs/5-deploy-packages/config.ini b/contrib/ci/jobs/5-deploy-packages/config.ini
new file mode 100644
index 000000000..08c106f9c
--- /dev/null
+++ b/contrib/ci/jobs/5-deploy-packages/config.ini
@@ -0,0 +1,6 @@
+[build]
+HALT_ON_FAILURE = True
+WARN_ON_FAILURE = True
+CONTAINER_BUILD = False
+CONTAINER_NAME = nixery.dev/shell/rsync
+CONTAINER_ARCH = amd64
diff --git a/contrib/ci/jobs/5-deploy-packages/job.sh b/contrib/ci/jobs/5-deploy-packages/job.sh
new file mode 100755
index 000000000..46ed90f70
--- /dev/null
+++ b/contrib/ci/jobs/5-deploy-packages/job.sh
@@ -0,0 +1,14 @@
+#!/bin/bash
+set -exuo pipefail
+
+ARTIFACT_PATH="/artifacts/wallet-core/${CI_COMMIT_REF}/*.deb"
+
+RSYNC_HOST="taler.host.internal"
+RSYNC_PORT=424242
+RSYNC_PATH="incoming_packages/bookworm-taler-ci/"
+RSYNC_DEST="rsync://${RSYNC_HOST}/${RSYNC_PATH}"
+
+
+rsync -vP \
+ --port ${RSYNC_PORT} \
+ ${ARTIFACT_PATH} ${RSYNC_DEST}
diff --git a/contrib/cleanup-prebuilt-dir.sh b/contrib/cleanup-prebuilt-dir.sh
new file mode 100755
index 000000000..5553fb467
--- /dev/null
+++ b/contrib/cleanup-prebuilt-dir.sh
@@ -0,0 +1,11 @@
+#!/bin/bash
+set -e
+
+[ ! -d prebuilt ] && git worktree add -f prebuilt prebuilt && exit 1
+
+# make sure that the prebuilt directory is clean
+# before building
+# this script is part of the make prebuilt
+cd prebuilt
+git checkout -- .
+git pull
diff --git a/contrib/copy-aml-backoffice-into-prebuilt.sh b/contrib/copy-aml-backoffice-into-prebuilt.sh
new file mode 100755
index 000000000..2b94327e6
--- /dev/null
+++ b/contrib/copy-aml-backoffice-into-prebuilt.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+
+[ ! -d prebuilt ] && git worktree add -f prebuilt prebuilt && exit 1
+
+find packages/aml-backoffice-ui/dist/prod/ -type f -printf '%P\n' | sort > prebuilt/aml-backoffice/bof
+
+while IFS= read -r file; do
+ cp packages/aml-backoffice-ui/dist/prod/$file prebuilt/aml-backoffice/$file
+done < prebuilt/aml-backoffice/bof
+
diff --git a/contrib/copy-auditor-backoffice-into-prebuilt.sh b/contrib/copy-auditor-backoffice-into-prebuilt.sh
new file mode 100755
index 000000000..6b6544183
--- /dev/null
+++ b/contrib/copy-auditor-backoffice-into-prebuilt.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+
+[ ! -d prebuilt ] && git worktree add -f prebuilt prebuilt && exit 1
+
+find packages/auditor-backoffice-ui/dist/prod/ -type f -printf '%P\n' | sort > prebuilt/auditor-backoffice/bof
+
+while IFS= read -r file; do
+ cp packages/auditor-backoffice-ui/dist/prod/$file prebuilt/auditor-backoffice/$file
+done < prebuilt/auditor-backoffice/bof
+
diff --git a/contrib/copy-backend-into-prebuilt.sh b/contrib/copy-backend-into-prebuilt.sh
new file mode 100755
index 000000000..81d38cdea
--- /dev/null
+++ b/contrib/copy-backend-into-prebuilt.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+
+[ ! -d prebuilt ] && git worktree add -f prebuilt prebuilt && exit 1
+
+for file in offer_refund.en.html request_payment.en.html show_order_details.en.html; do
+ cp packages/merchant-backend-ui/dist/pages/$file prebuilt/backend/
+done
+
diff --git a/contrib/copy-backoffice-into-prebuilt.sh b/contrib/copy-backoffice-into-prebuilt.sh
new file mode 100755
index 000000000..d21b91096
--- /dev/null
+++ b/contrib/copy-backoffice-into-prebuilt.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+
+[ ! -d prebuilt ] && git worktree add -f prebuilt prebuilt && exit 1
+
+find packages/merchant-backoffice-ui/dist/prod/ -type f -printf '%P\n' | sort > prebuilt/backoffice/bof
+
+while IFS= read -r file; do
+ cp packages/merchant-backoffice-ui/dist/prod/$file prebuilt/backoffice/$file
+done < prebuilt/backoffice/bof
+
diff --git a/contrib/copy-bank-into-prebuilt.sh b/contrib/copy-bank-into-prebuilt.sh
new file mode 100755
index 000000000..5ad375767
--- /dev/null
+++ b/contrib/copy-bank-into-prebuilt.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+
+[ ! -d prebuilt ] && git worktree add -f prebuilt prebuilt && exit 1
+
+find packages/bank-ui/dist/prod/ -type f -printf '%P\n' | sort > prebuilt/bank/bof
+
+while IFS= read -r file; do
+ cp packages/bank-ui/dist/prod/$file prebuilt/bank/$file
+done < prebuilt/bank/bof
+
diff --git a/contrib/copy-challenger-into-prebuilt.sh b/contrib/copy-challenger-into-prebuilt.sh
new file mode 100755
index 000000000..ebc39192c
--- /dev/null
+++ b/contrib/copy-challenger-into-prebuilt.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+
+[ ! -d prebuilt ] && git worktree add -f prebuilt prebuilt && exit 1
+
+find packages/challenger-ui/dist/prod/ -type f -printf '%P\n' | sort > prebuilt/challenger/bof
+
+while IFS= read -r file; do
+ cp packages/challenger-ui/dist/prod/$file prebuilt/challenger/$file
+done < prebuilt/challenger/bof
+
diff --git a/contrib/publish-prebuilt-dir.sh b/contrib/publish-prebuilt-dir.sh
new file mode 100755
index 000000000..a636c6de6
--- /dev/null
+++ b/contrib/publish-prebuilt-dir.sh
@@ -0,0 +1,15 @@
+#!/bin/bash
+
+# this script is part of the make prebuilt procedure
+# is expected on the root folder
+
+# create a commit message with the commit id
+COMMIT=$(git rev-parse --verify HEAD)
+MSG="built from ${COMMIT}"
+
+# after building process has copy everything into
+# the prebuilt folder
+cd prebuilt
+git commit -m "$MSG" -a
+git show --stat
+echo "ready to push"
diff --git a/contrib/wallet-testdata b/contrib/wallet-testdata
new file mode 160000
+Subproject 7ca3d9b4751cbd513b3a45dfa9e337d4c5980ea
diff --git a/package.json b/package.json
index 635fc6b72..f53cf87a5 100644
--- a/package.json
+++ b/package.json
@@ -4,15 +4,17 @@
"preinstall": "npx only-allow pnpm",
"compile": "pnpm run --filter '@gnu-taler/*' compile",
"clean": "pnpm run --filter '@gnu-taler/*' clean",
+ "typedoc": "pnpm run --filter '@gnu-taler/*' typedoc",
"pretty": "pnpm run --filter '@gnu-taler/*' pretty",
- "check": "pnpm run --filter '@gnu-taler/*' --if-present test"
+ "check": "pnpm run --filter '@gnu-taler/*' --if-present --sequential test"
},
"devDependencies": {
"@babel/core": "7.13.16",
"@linaria/esbuild": "^3.0.0-beta.15",
"@linaria/shaker": "^3.0.0-beta.15",
- "esbuild": "^0.15.13",
- "nx": "15.0.1",
- "prettier": "^2.5.1"
+ "esbuild": "^0.19.9",
+ "prettier": "^3.1.1",
+ "typedoc": "^0.25.4",
+ "typescript": "^5.3.3"
}
}
diff --git a/packages/aml-backoffice-ui/.eslintrc.cjs b/packages/aml-backoffice-ui/.eslintrc.cjs
new file mode 100644
index 000000000..05618b499
--- /dev/null
+++ b/packages/aml-backoffice-ui/.eslintrc.cjs
@@ -0,0 +1,28 @@
+module.exports = {
+ extends: [
+ 'eslint:recommended',
+ 'plugin:@typescript-eslint/recommended',
+ 'plugin:react/recommended',
+ ],
+ parser: '@typescript-eslint/parser',
+ plugins: ['@typescript-eslint', 'header'],
+ root: true,
+ rules: {
+ "react/no-unknown-property": 0,
+ "react/no-unescaped-entities": 0,
+ "@typescript-eslint/no-namespace": 0,
+ "@typescript-eslint/no-unused-vars": [2,{argsIgnorePattern:"^_"}],
+ "header/header": [2,"copyleft-header.js"]
+ },
+ parserOptions: {
+ ecmaVersion: 6,
+ sourceType: 'module',
+ jsx: true,
+ },
+ settings: {
+ react: {
+ version: "18",
+ pragma: "h",
+ }
+ },
+};
diff --git a/packages/aml-backoffice-ui/.gitignore b/packages/aml-backoffice-ui/.gitignore
new file mode 100644
index 000000000..30cb2774c
--- /dev/null
+++ b/packages/aml-backoffice-ui/.gitignore
@@ -0,0 +1,4 @@
+node_modules
+/build
+/*.log
+/demobank-ui-settings.js
diff --git a/packages/aml-backoffice-ui/Makefile b/packages/aml-backoffice-ui/Makefile
new file mode 100644
index 000000000..64f9f83d1
--- /dev/null
+++ b/packages/aml-backoffice-ui/Makefile
@@ -0,0 +1,36 @@
+# This Makefile has been placed in the public domain
+
+ifeq ($(TOPLEVEL), yes)
+ $(info top-level build)
+ -include ../../.config.mk
+ override DESTDIR := $(TOP_DESTDIR)
+else
+ $(info package-level build)
+ -include ../../.config.mk
+ -include .config.mk
+endif
+
+$(info prefix is $(prefix))
+
+.PHONY: all
+all:
+ @echo run \'make install\' to install
+
+spa_dir=$(DESTDIR)$(prefix)/share/taler/aml-backoffice-ui
+
+.PHONY: install-nodeps
+install-nodeps:
+ install -d $(spa_dir)
+ install ./dist/prod/* $(spa_dir)
+
+.PHONY: deps
+deps:
+ pnpm install --frozen-lockfile --filter @gnu-taler/aml-backoffice-ui...
+ pnpm run check
+ pnpm run build
+
+.PHONY: install
+install:
+ $(MAKE) deps
+ $(MAKE) install-nodeps
+
diff --git a/packages/aml-backoffice-ui/README.md b/packages/aml-backoffice-ui/README.md
new file mode 100644
index 000000000..855addd74
--- /dev/null
+++ b/packages/aml-backoffice-ui/README.md
@@ -0,0 +1,4 @@
+# Taler Exchange Backoffice UI
+
+Web-based user interface for the GNU Taler exchange.
+
diff --git a/packages/aml-backoffice-ui/build.mjs b/packages/aml-backoffice-ui/build.mjs
new file mode 100755
index 000000000..bd7a088cf
--- /dev/null
+++ b/packages/aml-backoffice-ui/build.mjs
@@ -0,0 +1,28 @@
+#!/usr/bin/env node
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { build } from "@gnu-taler/web-util/build";
+
+await build({
+ type: "production",
+ source: {
+ js: ["src/index.tsx", "src/forms.ts"],
+ assets: [{ base: "src", files: ["src/index.html"] }],
+ },
+ destination: "./dist/prod",
+ css: "postcss",
+});
diff --git a/packages/aml-backoffice-ui/copyleft-header.js b/packages/aml-backoffice-ui/copyleft-header.js
new file mode 100644
index 000000000..2635717c5
--- /dev/null
+++ b/packages/aml-backoffice-ui/copyleft-header.js
@@ -0,0 +1,15 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
diff --git a/packages/taler-wallet-webextension/src/hooks/useLang.ts b/packages/aml-backoffice-ui/dev.mjs
index 269fe6239..bc6fcd6c1 100644..100755
--- a/packages/taler-wallet-webextension/src/hooks/useLang.ts
+++ b/packages/aml-backoffice-ui/dev.mjs
@@ -1,3 +1,4 @@
+#!/usr/bin/env node
/*
This file is part of GNU Taler
(C) 2022 Taler Systems S.A.
@@ -14,17 +15,26 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { useNotNullLocalStorage } from "./useLocalStorage.js";
+import { serve } from "@gnu-taler/web-util/node";
+import { initializeDev } from "@gnu-taler/web-util/build";
-function getBrowserLang(): string | undefined {
- if (window.navigator.languages) return window.navigator.languages[0];
- if (window.navigator.language) return window.navigator.language;
- return undefined;
-}
+const devEntryPoints = ["src/stories.tsx", "src/index.tsx", "src/forms.ts"];
-export function useLang(
- initial?: string,
-): [string, (s: string) => void, boolean] {
- const defaultLang = (getBrowserLang() || initial || "en").substring(0, 2);
- return useNotNullLocalStorage("lang-preference", defaultLang);
-}
+const build = initializeDev({
+ type: "development",
+ source: {
+ js: devEntryPoints,
+ assets: [{ base: "src", files: ["src/index.html"] }],
+ },
+ destination: "./dist/dev",
+ css: "postcss",
+});
+
+await build();
+
+serve({
+ folder: "./dist/dev",
+ port: 8080,
+ source: "./src",
+ onSourceUpdate: build,
+});
diff --git a/packages/aml-backoffice-ui/package.json b/packages/aml-backoffice-ui/package.json
new file mode 100644
index 000000000..749565946
--- /dev/null
+++ b/packages/aml-backoffice-ui/package.json
@@ -0,0 +1,59 @@
+{
+ "private": true,
+ "name": "@gnu-taler/aml-backoffice-ui",
+ "version": "0.10.7",
+ "author": "sebasjm",
+ "license": "AGPL-3.0-OR-LATER",
+ "description": "Back-office SPA for GNU Taler Exchange.",
+ "type": "module",
+ "scripts": {
+ "build": "./build.mjs",
+ "typedoc": "typedoc --out dist/typedoc ./src/",
+ "check": "tsc",
+ "clean": "rm -rf dist lib",
+ "compile": "tsc && ./build.mjs",
+ "test": "./test.mjs && mocha --require source-map-support/register 'dist/test/**/*.test.js' 'dist/test/**/test.js'",
+ "lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
+ "i18n:extract": "pogen extract",
+ "i18n:merge": "pogen merge",
+ "i18n:emit": "pogen emit",
+ "i18n": "pnpm i18n:extract && pnpm i18n:merge && pnpm i18n:emit",
+ "pretty": "prettier --write src"
+ },
+ "dependencies": {
+ "@gnu-taler/taler-util": "workspace:*",
+ "@gnu-taler/web-util": "workspace:*",
+ "@headlessui/react": "^1.7.14",
+ "@heroicons/react": "^2.0.17",
+ "date-fns": "2.29.3",
+ "history": "4.10.1",
+ "jed": "1.1.1",
+ "preact": "10.11.3",
+ "swr": "2.2.2"
+ },
+ "devDependencies": {
+ "@gnu-taler/pogen": "workspace:*",
+ "@tailwindcss/forms": "^0.5.3",
+ "@tailwindcss/typography": "^0.5.9",
+ "@types/chai": "^4.3.0",
+ "@types/history": "^4.7.8",
+ "@types/mocha": "^10.0.1",
+ "@typescript-eslint/eslint-plugin": "^6.19.0",
+ "@typescript-eslint/parser": "^6.19.0",
+ "autoprefixer": "^10.4.14",
+ "chai": "^4.3.6",
+ "esbuild": "^0.19.9",
+ "eslint": "^8.56.0",
+ "eslint-config-prettier": "^9.1.0",
+ "eslint-plugin-react": "^7.33.2",
+ "mocha": "^9.2.0",
+ "po2json": "^0.4.5",
+ "postcss": "^8.4.23",
+ "postcss-cli": "^10.1.0",
+ "tailwindcss": "^3.3.2",
+ "typescript": "5.3.3"
+ },
+ "pogen": {
+ "domain": "aml-backoffice"
+ }
+}
diff --git a/packages/aml-backoffice-ui/postcss.config.js b/packages/aml-backoffice-ui/postcss.config.js
new file mode 100644
index 000000000..2e7af2b7f
--- /dev/null
+++ b/packages/aml-backoffice-ui/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/packages/aml-backoffice-ui/src/App.tsx b/packages/aml-backoffice-ui/src/App.tsx
new file mode 100644
index 000000000..5244476d7
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/App.tsx
@@ -0,0 +1,74 @@
+import { TranslationProvider } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { ExchangeAmlFrame } from "./Dashboard.js";
+import "./scss/main.css";
+import { ExchangeApiProvider } from "./context/config.js";
+import { getInitialBackendBaseURL } from "./hooks/useBackend.js";
+import { HashPathProvider, Router } from "./route.js";
+import { Pages } from "./pages.js";
+import { SWRConfig } from "swr";
+
+const WITH_LOCAL_STORAGE_CACHE = false;
+
+const pageList = Object.values(Pages);
+
+export function App(): VNode {
+ const baseUrl = getInitialBackendBaseURL();
+ return (
+ <TranslationProvider source={{}}>
+ <ExchangeApiProvider baseUrl={baseUrl} frameOnError={ExchangeAmlFrame}>
+ <HashPathProvider>
+ <SWRConfig
+ value={{
+ provider: WITH_LOCAL_STORAGE_CACHE
+ ? localStorageProvider
+ : undefined,
+ // normally, do not revalidate
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ revalidateIfStale: false,
+ revalidateOnMount: undefined,
+ focusThrottleInterval: undefined,
+
+ // normally, do not refresh
+ refreshInterval: undefined,
+ dedupingInterval: 2000,
+ refreshWhenHidden: false,
+ refreshWhenOffline: false,
+
+ // ignore errors
+ shouldRetryOnError: false,
+ errorRetryCount: 0,
+ errorRetryInterval: undefined,
+
+ // do not go to loading again if already has data
+ keepPreviousData: true,
+ }}
+ >
+
+ <ExchangeAmlFrame>
+ <Router
+ pageList={pageList}
+ onNotFound={() => {
+ window.location.href = Pages.cases.url
+ return <div>not found</div>;
+ }}
+ />
+ </ExchangeAmlFrame>
+ </SWRConfig>
+ </HashPathProvider>
+ </ExchangeApiProvider>
+ </TranslationProvider>
+ );
+}
+
+
+function localStorageProvider(): Map<unknown, unknown> {
+ const map = new Map(JSON.parse(localStorage.getItem("app-cache") || "[]"));
+
+ window.addEventListener("beforeunload", () => {
+ const appCache = JSON.stringify(Array.from(map.entries()));
+ localStorage.setItem("app-cache", appCache);
+ });
+ return map;
+}
diff --git a/packages/aml-backoffice-ui/src/Dashboard.tsx b/packages/aml-backoffice-ui/src/Dashboard.tsx
new file mode 100644
index 000000000..3951b48c7
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/Dashboard.tsx
@@ -0,0 +1,234 @@
+import { TranslatedString } from "@gnu-taler/taler-util";
+import { Footer, ToastBanner, Header, notifyError, notifyException, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { ComponentChildren, Fragment, VNode, h } from "preact";
+import { useEffect, useErrorBoundary } from "preact/hooks";
+import { useOfficer } from "./hooks/useOfficer.js";
+import { getAllBooleanSettings, getLabelForSetting, useSettings } from "./hooks/useSettings.js";
+import { Pages } from "./pages.js";
+import { PageEntry, useChangeLocation } from "./route.js";
+import { uiSettings } from "./settings.js";
+
+function classNames(...classes: string[]) {
+ return classes.filter(Boolean).join(" ");
+}
+
+/**
+ * mapping route to view
+ * not found (error page)
+ * nested, index element, relative routes
+ * link interception
+ * form POST interception, call action
+ * fromData => Object.fromEntries
+ * segments in the URL
+ * navigationState: idle, submitting, loading
+ * form GET interception: does a navigateTo
+ * form GET Sync:
+ * 1.- back after submit: useEffect to sync URL to form
+ * 2.- refresh after submit: input default value
+ * useSubmit for form submission onChange, history replace
+ *
+ * post form without redirect
+ *
+ *
+ * @param param0
+ * @returns
+ */
+
+const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined;
+const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined;
+
+const versionText = VERSION
+ ? GIT_HASH
+ ? `v${VERSION} (${GIT_HASH.substring(0, 8)})`
+ : VERSION
+ : "";
+
+/**
+ * TO BE FIXED:
+ *
+ * 1.- when the form change to other form and both form share the same structure
+ * the same input component may be rendered in the same place,
+ * since input are uncontrolled the are not re-rendered and since they are
+ * uncontrolled it will keep the value of the previous form.
+ * One solutions could be to remove the form when unloading and when the new
+ * form load it will start without previous vdom, preventing the cache
+ * to create this behavior.
+ * Other solutions could be using IDs in the fields that are constructed
+ * with the ID of the form, so two fields of different form will need to re-render
+ * cleaning up the state of the previous form.
+ *
+ * 2.- currently the design prop and the behavior prop of the flexible form
+ * are two side of the same coin. From the design point of view, it is important
+ * to design the form in a list-of-field manner and there may be additional
+ * content that is not directly mapped to the form structure (object)
+ * So maybe we want to change the current shape so the computation of the state
+ * of the form is in a field level, but this computation required the field value and
+ * the whole form values and state (since one field may be disabled/hidden) because
+ * of the value of other field.
+ *
+ * 3.- given the previous requirement, maybe the name of the field of the form could be
+ * a function (P: F -> V) where F is the form (or parent object) and V is the type of the
+ * property. That will help with the typing of the forms props
+ *
+ * 4.- tooltip are not placed correctly: the arrow should point the question mark
+ * and the text area should be bigger
+ *
+ */
+
+/**
+ * check this fields
+ *
+ * Signature of Contracting partner, 902_9e
+ * Currency and amount of deposited assets, 902_5e
+ * Signature on declaration of trust, 902.13e
+ * also fundations
+ * also life insurance
+ *
+ * no all state are handled by all the inputs
+ * all the input implementation should respect
+ * ui props and state
+ */
+
+export function ExchangeAmlFrame({
+ children,
+}: {
+ children?: ComponentChildren;
+}): VNode {
+ const { i18n } = useTranslationContext();
+
+ const [error, resetError] = useErrorBoundary();
+
+ useEffect(() => {
+ if (error) {
+ if (error instanceof Error) {
+ notifyException(i18n.str`Internal error, please report.`, error)
+ } else {
+ notifyError(i18n.str`Internal error, please report.`, String(error) as TranslatedString)
+ }
+ console.log(error)
+ // resetError()
+ }
+ }, [error])
+
+ const officer = useOfficer();
+ const [settings, updateSettings] = useSettings();
+
+ return (<div class="min-h-full flex flex-col m-0 bg-slate-200" style="min-height: 100vh;">
+ <div class="bg-indigo-600 pb-32">
+ <Header
+ title="Exchange"
+ iconLinkURL={uiSettings.backendBaseURL ?? "#"}
+ onLogout={officer.state !== "ready" ? undefined : () => {
+ officer.lock()
+ }}
+ sites={[]}
+ supportedLangs={["en", "es", "de"]}
+ >
+ <li>
+ <div class="text-xs font-semibold leading-6 text-gray-400">
+ <i18n.Translate>Preferences</i18n.Translate>
+ </div>
+ <ul role="list" class="space-y-1">
+ {getAllBooleanSettings().map(set => {
+ const isOn: boolean = !!settings[set]
+ return <li class="mt-2 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">
+ {getLabelForSetting(set, i18n)}
+ </span>
+ </span>
+ <button type="button" 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={() => { updateSettings(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>
+
+ <div class="fixed z-20 w-full">
+ <div class="mx-auto w-4/5">
+ <ToastBanner />
+ </div>
+ </div>
+
+ <div class="-mt-32 flex grow ">
+ {officer.state !== "ready" ? undefined :
+ <Navigation />
+ }
+ <div class="flex mx-auto my-4">
+ <main class="rounded-lg bg-white px-5 py-6 shadow">
+ {children}
+ </main>
+ </div>
+
+ </div>
+
+ <Footer
+ testingUrlKey="exchange-base-url"
+ GIT_HASH={GIT_HASH}
+ VERSION={VERSION}
+ />
+ </div>
+ );
+}
+
+function Navigation(): VNode {
+ const { i18n } = useTranslationContext()
+ const pageList: Array<PageEntry> = [
+ Pages.officer,
+ Pages.cases
+ ]
+ const location = useChangeLocation();
+ return (
+ <div class="hidden sm:block min-w-min bg-indigo-600 divide-y rounded-r-lg divide-cyan-800 overflow-y-auto overflow-x-clip">
+
+ <nav class="flex flex-1 flex-col mx-4 mt-4 mb-2">
+ <ul role="list" class="flex flex-1 flex-col gap-y-7">
+ <li>
+ <ul role="list" class="-mx-2 space-y-1">
+ {pageList.map(p => {
+
+ return <li>
+ <a href={p.url} data-selected={location == p.url}
+ class="data-[selected=true]:bg-indigo-700 pr-4 data-[selected=true]:text-white text-indigo-200 hover:text-white hover:bg-indigo-700 group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold">
+ {p.Icon && <p.Icon />}
+ <span class="hidden md:inline">
+ {p.name}
+ </span>
+ </a>
+ </li>
+
+ })}
+ {/* <li>
+ <a href="#" class="text-indigo-200 hover:text-white hover:bg-indigo-700 group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold">
+
+ <i18n.Translate>Officer</i18n.Translate>
+ </a>
+ </li> */}
+ </ul>
+ </li>
+
+ {/* <li class="mt-auto ">
+ <a href="#" class="group -mx-2 flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 text-indigo-200 hover:bg-indigo-700 hover:text-white">
+ <svg class="h-6 w-6 shrink-0 text-indigo-200 group-hover:text-white" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" />
+ <path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
+ </svg>
+ Settings
+ </a>
+ </li> */}
+
+ </ul>
+ </nav>
+ </div>
+ )
+
+}
+
+
diff --git a/packages/aml-backoffice-ui/src/assets/home.svg b/packages/aml-backoffice-ui/src/assets/home.svg
new file mode 100644
index 000000000..35f340162
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/assets/home.svg
@@ -0,0 +1,3 @@
+<svg class="h-6 w-6 shrink-0 text-white" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" />
+</svg> \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/svg/logo-2021.svg b/packages/aml-backoffice-ui/src/assets/logo-2021.svg
index 8c5ff3e5b..8c5ff3e5b 100644
--- a/packages/taler-wallet-webextension/src/svg/logo-2021.svg
+++ b/packages/aml-backoffice-ui/src/assets/logo-2021.svg
diff --git a/packages/aml-backoffice-ui/src/assets/people.svg b/packages/aml-backoffice-ui/src/assets/people.svg
new file mode 100644
index 000000000..1dc878b81
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/assets/people.svg
@@ -0,0 +1,3 @@
+<svg class="h-6 w-6 shrink-0 text-indigo-200 group-hover:text-white" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z" />
+</svg> \ No newline at end of file
diff --git a/packages/aml-backoffice-ui/src/context/config.ts b/packages/aml-backoffice-ui/src/context/config.ts
new file mode 100644
index 000000000..42f73428a
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/context/config.ts
@@ -0,0 +1,100 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { TalerExchangeApi, TalerExchangeHttpClient, TalerError } from "@gnu-taler/taler-util";
+import { BrowserFetchHttpLib, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { ComponentChildren, createContext, FunctionComponent, h, VNode } from "preact";
+import { useContext, useEffect, useState } from "preact/hooks";
+import { ErrorLoading } from "@gnu-taler/web-util/browser";
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+export type Type = {
+ url: URL,
+ config: TalerExchangeApi.ExchangeVersionResponse,
+ api: TalerExchangeHttpClient,
+};
+
+const Context = createContext<Type>(undefined as any);
+
+export const useExchangeApiContext = (): Type => useContext(Context);
+export const useMaybeExchangeApiContext = (): Type | undefined => useContext(Context);
+
+export function ExchangeApiContextTesting({ config, children }: { config: TalerExchangeApi.ExchangeVersionResponse, children?: ComponentChildren; }): VNode {
+ return h(Context.Provider, {
+ value: { url: new URL("http://testing"), config, api: null as any },
+ children
+ }
+ )
+}
+
+export type ConfigResult = undefined
+ | { type: "ok", config: TalerExchangeApi.ExchangeVersionResponse }
+ | { type: "incompatible", result: TalerExchangeApi.ExchangeVersionResponse, supported: string }
+ | { type: "error", error: TalerError }
+
+export const ExchangeApiProvider = ({
+ baseUrl,
+ children,
+ frameOnError,
+}: {
+ baseUrl: string,
+ children: ComponentChildren;
+ frameOnError: FunctionComponent<{ children: ComponentChildren }>,
+}): VNode => {
+ const [checked, setChecked] = useState<ConfigResult>()
+ const { i18n } = useTranslationContext();
+ const url = new URL(baseUrl)
+ const api = new TalerExchangeHttpClient(url.href, new BrowserFetchHttpLib())
+ useEffect(() => {
+ api.getConfig()
+ .then((resp) => {
+ if (resp.type === "fail") {
+ setChecked({ type: "error", error: TalerError.fromUncheckedDetail(resp.detail) });
+ } else if (api.isCompatible(resp.body.version)) {
+ setChecked({ type: "ok", config: resp.body });
+ } else {
+ setChecked({ type: "incompatible", result: resp.body, supported: api.PROTOCOL_VERSION })
+ }
+ })
+ .catch((error: unknown) => {
+ if (error instanceof TalerError) {
+ setChecked({ type: "error", error });
+ }
+ });
+ }, []);
+
+ if (checked === undefined) {
+ return h(frameOnError, { children: h("div", {}, "loading...") })
+ }
+ if (checked.type === "error") {
+ return h(frameOnError, { children: h(ErrorLoading, { error: checked.error, showDetail: true }) })
+ }
+ if (checked.type === "incompatible") {
+ return h(frameOnError, { children: h("div", {}, i18n.str`the bank backend is not supported. supported version "${checked.supported}", server version "${checked.result.version}"`) })
+ }
+ const value: Type = {
+ url, config: checked.config, api
+ }
+ return h(Context.Provider, {
+ value,
+ children,
+ });
+};
+
diff --git a/packages/aml-backoffice-ui/src/declaration.d.ts b/packages/aml-backoffice-ui/src/declaration.d.ts
new file mode 100644
index 000000000..6af72042c
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/declaration.d.ts
@@ -0,0 +1,31 @@
+
+declare const __VERSION__: string;
+declare const __GIT_HASH__: string;
+
+declare module "*.po" {
+ const content: any;
+ export default content;
+}
+declare module "jed" {
+ const x: any;
+ export = x;
+}
+declare module "*.jpeg" {
+ const content: any;
+ export default content;
+}
+declare module "*.png" {
+ const content: any;
+ export default content;
+}
+declare module "*.svg" {
+ const content: any;
+ export default content;
+}
+
+declare module "*.scss" {
+ const content: Record<string, string>;
+ export default content;
+}
+declare const __VERSION__: string;
+declare const __GIT_HASH__: string;
diff --git a/packages/aml-backoffice-ui/src/forms.ts b/packages/aml-backoffice-ui/src/forms.ts
new file mode 100644
index 000000000..cc9e4c7e8
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/forms.ts
@@ -0,0 +1,24 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+export * from "./forms/index.js";
+
+/**
+ * this file is here to have a flat dist folder
+ *
+ * this file is being build in a bundle separated
+ * from the main one.
+ */
diff --git a/packages/aml-backoffice-ui/src/forms/902_11e.ts b/packages/aml-backoffice-ui/src/forms/902_11e.ts
new file mode 100644
index 000000000..71ca8bcf4
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/forms/902_11e.ts
@@ -0,0 +1,133 @@
+import type { TranslatedString } from "@gnu-taler/taler-util";
+import type { FlexibleForm, FormState, InternationalizationAPI } from "@gnu-taler/web-util/browser";
+import { BaseForm } from "./declaration.js";
+import { resolutionSection } from "./simplest.js";
+
+export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): FlexibleForm<Form902_11.Form> => ({
+ design: [
+ {
+ title:
+ i18n.str`Establishing of the controlling person of operating legal entities and partnerships both not quoted on the stock exchange`,
+ description:
+ i18n.str`for operating legal entities and partnership that are contracting partner as well as analogously for operating legal entities and partnership that are beneficial owners.`,
+ fields: [
+ {
+ type: "textArea",
+ props: {
+ name: "contractingPartner",
+ label: i18n.str`Contracting partner`,
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ name: "declares",
+ label:
+ i18n.str`The contracting partner hereby declares that`,
+ required: true,
+ choices: [
+ {
+ label:
+ i18n.str`the person(s) listed below is/are holding 25% or more of the contracting partner's shares (capital shares or voting rights)`,
+ value: "25-or-more",
+ },
+ {
+ label:
+ i18n.str`if the capital shares or voting rights cannot be determined or in case there are no capital shares or voting rights 25% or more, the contracting partner hereby declares that the person(s) listed below is/are controlling the contracting partner in other ways`,
+ value: "controlling-in-other-ways",
+ },
+ {
+ label:
+ i18n.str`in case this/these person(s) cannot be determined or this/these person(s) does/do not exist, the contracting partner hereby declares that the person(s) listed below is/are the managing director(s)`,
+ value: "managing-director",
+ },
+ ],
+ },
+ },
+ {
+ type: "array",
+ props: {
+ name: "people",
+ label: i18n.str`People`,
+ required: true,
+ placeholder: i18n.str`this is the placeholder`,
+ fields: [
+ {
+ type: "text",
+ props: {
+ name: "lastName",
+ label: i18n.str`Last name(s)`,
+ required: true,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "firstName",
+ label: i18n.str`First name(s)`,
+ required: true,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "address",
+ label: i18n.str`Actual address of domicile`,
+ required: true,
+ },
+ },
+ ],
+ labelField: "lastName",
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ name: "fiduciaryAssets",
+ label: i18n.str`Fiduciary holding assets`,
+ help: i18n.str`Is a third person the beneficial owner of the assets held in the account/securities account?`,
+ required: true,
+ choices: [
+ {
+ label: i18n.str`No`,
+ value: "no",
+ },
+ {
+ label: i18n.str`Yes`,
+ value: "yes",
+ description:
+ i18n.str`The relevant information regarding the beneficial owner has to be obtained by filling in a separate VQF doc. No. 902.9`,
+ },
+ ],
+ },
+ },
+ ],
+ },
+ resolutionSection(current, i18n),
+ ],
+ behavior: function formBehavior(
+ v: Partial<Form902_11.Form>,
+ ): FormState<Form902_11.Form> {
+ return {
+ people: {
+ hidden: v.declares !== "controlling-in-other-ways" &&
+ v.declares !== "managing-director",
+ }
+ };
+ }
+});
+
+namespace Form902_11 {
+ interface Person {
+ lastName: string;
+ firstName: string;
+ address: string;
+ }
+ export interface Form extends BaseForm {
+ contractingPartner: string;
+ declares: "25-or-more" | "controlling-in-other-ways" | "managing-director";
+ people: Person[];
+ fiduciaryAssets: "no" | "yes";
+ signature: string;
+ }
+}
diff --git a/packages/aml-backoffice-ui/src/forms/902_12e.ts b/packages/aml-backoffice-ui/src/forms/902_12e.ts
new file mode 100644
index 000000000..0c08d274c
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/forms/902_12e.ts
@@ -0,0 +1,421 @@
+import type { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
+import type { FlexibleForm, FormState, InternationalizationAPI } from "@gnu-taler/web-util/browser";
+import { resolutionSection } from "./simplest.js";
+import { BaseForm } from "./declaration.js";
+
+export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): FlexibleForm<Form902_12.Form> => ({
+ design: [
+ {
+ title: i18n.str`Foundations`,
+ fields: [
+ {
+ type: "textArea",
+ props: {
+ name: "contractingPartner",
+ label: i18n.str`Contracting partner`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "knownAs",
+ label:
+ i18n.str`The undersigned hereby declare(s) that as board member of the foundation, or of the highest supervisory body of an underlying company of a foundation, known as`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "foundation.name",
+ label:
+ i18n.str`Name and information pertaining to the foundation`,
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ name: "foundation.type",
+ label: i18n.str`Type of foundation`,
+ choices: [
+ {
+ label: i18n.str`Discretionary foundation`,
+ value: "discretionary",
+ },
+ {
+ label: i18n.str`Non-discretionary foundation`,
+ value: "non-discretionary",
+ },
+ ],
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ name: "foundation.revocability",
+ label: i18n.str`Revocability`,
+ choices: [
+ {
+ label: i18n.str`Revocable foundation`,
+ value: "revocable",
+ },
+ {
+ label: i18n.str`Irrevocable foundation`,
+ value: "irrevocable",
+ },
+ ],
+ },
+ },
+ {
+ type: "array",
+ props: {
+ label:
+ i18n.str`Information pertaining to the (ultimate economic, not fiduciary) founder (individual(s) or entity/ies)`,
+ labelField: "fullName",
+ name: "founders",
+ fields: [
+ {
+ type: "text",
+ props: {
+ name: "fullName",
+ label:
+ i18n.str`Last name(s), first name(s)/entity`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "address",
+ label:
+ i18n.str`Actual address of domicile/registered office`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "country",
+ label: i18n.str`Country`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "dateOfBirth",
+ label: i18n.str`Date of birth`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "nationality",
+ label: i18n.str`Nationality`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "dateOfDeath",
+ label: i18n.str`Date of death`,
+ help: i18n.str`if deceased`,
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ name: "rightToRevoke",
+ required: true,
+ label:
+ i18n.str`Does the founder have the right to revoke the foundation?`,
+ choices: [
+ {
+ label: i18n.str`Yes`,
+ value: "yes",
+ },
+ {
+ label: i18n.str`No`,
+ value: "no",
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "array",
+ props: {
+ label:
+ i18n.str`If the foundation results from the restructuring of pre-existing foundation (re-settlement) or the merger of pre-existing foundations, the following information pertaining to the (actual) founder(s) of the pre-existing foundation(s) has to be given`,
+ labelField: "fullName",
+ name: "preExistingFounders",
+ fields: [
+ {
+ type: "text",
+ props: {
+ name: "fullName",
+ label:
+ i18n.str`Last name(s), first name(s)/entity`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "address",
+ label:
+ i18n.str`Actual address of domicile/registered office`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "country",
+ label: i18n.str`Country`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "dateOfBirth",
+ label: i18n.str`Date of birth`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "nationality",
+ label: i18n.str`Nationality`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "dateOfDeath",
+ label: i18n.str`Date of death`,
+ help: i18n.str`if deceased`,
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "array",
+ props: {
+ label:
+ i18n.str`Pertaining to the beneficiary/-ies at the time of the signing of this form`,
+ labelField: "fullName",
+ name: "beneficiaryWhenSigning",
+ fields: [
+ {
+ type: "text",
+ props: {
+ name: "fullName",
+ label:
+ i18n.str`Last name(s), first name(s)/entity`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "address",
+ label:
+ i18n.str`Actual address of domicile/registered office`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "country",
+ label: i18n.str`Country`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "dateOfBirth",
+ label: i18n.str`Date of birth`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "nationality",
+ label: i18n.str`Nationality`,
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ name: "rightToClaim",
+ label:
+ i18n.str`Has the beneficiary an actual right to claim distribution?`,
+ choices: [
+ {
+ label: i18n.str`Yes`,
+ value: "yes",
+ },
+ {
+ label: i18n.str`No`,
+ value: "no",
+ },
+ ],
+ },
+ },
+ {
+ type: "textArea",
+ props: {
+ label:
+ i18n.str`in addition to certain beneficiaries or if there is/are no defined beneficiary/ies pertaining to (a) group(s) of beneficiaries (e.g. descendants of the founder) known at the time of the signing of this form`,
+ name: "beneficiaryExtra",
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "array",
+ props: {
+ label:
+ i18n.str`Information pertaining to further persons having the right to determine or nominate representatives (e.g.) members of the foundation board), if these representatives may dispose over the assets or have the right to change the distribution of the assets or the nomination of beneficiaries`,
+ labelField: "fullName",
+ name: "withRightToNominate",
+ fields: [
+ {
+ type: "text",
+ props: {
+ name: "fullName",
+ label:
+ i18n.str`Last name(s), first name(s)/entity`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "address",
+ label:
+ i18n.str`Actual address of domicile/registered office`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "country",
+ label: i18n.str`Country`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "dateOfBirth",
+ label: i18n.str`Date of birth`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "nationality",
+ label: i18n.str`Nationality`,
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ name: "rightToClaim",
+ label:
+ i18n.str`has the person the right to revoke the foundation?`,
+ choices: [
+ {
+ label: i18n.str`Yes`,
+ value: "yes",
+ },
+ {
+ label: i18n.str`No`,
+ value: "no",
+ },
+ ],
+ },
+ },
+ {
+ type: "textArea",
+ props: {
+ label:
+ i18n.str`in addition to certain beneficiaries or if there is/are no defined beneficiary/ies pertaining to (a) group(s) of beneficiaries (e.g. descendants of the founder) known at the time of the signing of this form`,
+ name: "beneficiaryExtra",
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "signature",
+ label: i18n.str`Signature`,
+ },
+ },
+ ],
+ },
+ resolutionSection(current, i18n),
+ ],
+ behavior: function formBehavior(
+ v: Partial<Form902_12.Form>,
+ ): FormState<Form902_12.Form> {
+ return {
+ founders: {
+ elements: (v.founders ?? []).map((f) => {
+ return {
+ rightToRevoke: {
+ hidden: v.foundation?.revocability !== "revocable",
+ },
+ };
+ }),
+ },
+ withRightToNominate: {
+ elements: (v.withRightToNominate ?? []).map((f) => {
+ return {
+ rightToRevoke: {
+ hidden: v.foundation?.revocability !== "revocable",
+ },
+ };
+ }),
+ },
+ };
+ },
+});
+
+namespace Form902_12 {
+ interface Foundation {
+ name: string;
+ type: "discretionary" | "non-discretionary";
+ revocability: "revocable" | "irrevocable";
+ }
+ interface Person {
+ fullName: string;
+ address: string;
+ country: string;
+ dateOfBirth: AbsoluteTime;
+ nationality: string;
+ }
+ type WithRevoke<T> = {
+ rightToRevoke: "yes" | "no";
+ } & T;
+ type WithClaim<T> = {
+ rightToClaim: "yes" | "no";
+ } & T;
+ type WithDeath<T> = {
+ dateOfDeath: AbsoluteTime;
+ } & T;
+
+ type Founder = WithRevoke<WithDeath<Person>>;
+ type Beneficiary = WithClaim<Person>;
+
+ export interface Form extends BaseForm {
+ contractingPartner: string;
+ knownAs: string;
+ boardMember: string;
+ foundation: Foundation;
+ founders: Array<Founder>;
+ preExistingFounders: Array<Founder>;
+ beneficiaryWhenSigning: Array<Beneficiary>;
+ beneficiaryExtra: Array<Beneficiary>;
+ withRightToNominate: Array<WithRevoke<Person>>;
+ }
+}
diff --git a/packages/aml-backoffice-ui/src/forms/902_13e.ts b/packages/aml-backoffice-ui/src/forms/902_13e.ts
new file mode 100644
index 000000000..f69884e0e
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/forms/902_13e.ts
@@ -0,0 +1,510 @@
+import type { AbsoluteTime } from "@gnu-taler/taler-util";
+import type { FlexibleForm, FormState, InternationalizationAPI } from "@gnu-taler/web-util/browser";
+import { BaseForm } from "./declaration.js";
+import { resolutionSection } from "./simplest.js";
+
+export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): FlexibleForm<Form902_13.Form> => ({
+ design: [
+ {
+ title: i18n.str`Declaration for trusts`,
+ fields: [
+ {
+ type: "textArea",
+ props: {
+ name: "contractingPartner",
+ label: i18n.str`Contracting partner`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "knownAs",
+ label:
+ i18n.str`The undersigned hereby declare(s) that as trustee or a member of highest supervisory body of an underlying company of a trust known as`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "trust.name",
+ label:
+ i18n.str`Name and information pertaining to the trust`,
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ name: "trust.type",
+ label: i18n.str`Type of trust`,
+ choices: [
+ {
+ label: i18n.str`Discretionary trust`,
+ value: "discretionary",
+ },
+ {
+ label: i18n.str`Non-discretionary trust`,
+ value: "non-discretionary",
+ },
+ ],
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ name: "trust.revocability",
+ label: i18n.str`Revocability`,
+ choices: [
+ {
+ label: i18n.str`Revocable foundation`,
+ value: "revocable",
+ },
+ {
+ label: i18n.str`Irrevocable foundation`,
+ value: "irrevocable",
+ },
+ ],
+ },
+ },
+ {
+ type: "array",
+ props: {
+ label:
+ i18n.str`Information pertaining to the (ultimate economic, not fiduciary) settlor of the trust (individual(s) or entity/ies)`,
+ labelField: "fullName",
+ name: "settlors",
+ fields: [
+ {
+ type: "text",
+ props: {
+ name: "fullName",
+ label:
+ i18n.str`Last name(s), first name(s)/entity`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "address",
+ label:
+ i18n.str`Actual address of domicile/registered office`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "country",
+ label: i18n.str`Country`,
+ },
+ },
+ {
+ type: "absoluteTime",
+ props: {
+ name: "dateOfBirth",
+ label: i18n.str`Date of birth`,
+ pattern: "dd/MM/yyyy",
+ // help: i18n.str`format 'dd/MM/yyyy'`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "nationality",
+ label: i18n.str`Nationality`,
+ },
+ },
+ {
+ type: "absoluteTime",
+ props: {
+ name: "dateOfDeath",
+ label: i18n.str`Date of death`,
+ pattern: "dd/MM/yyyy",
+ // help: i18n.str`if deceased. format 'dd/MM/yyyy'`,
+ help: i18n.str`if deceased'`,
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ name: "rightToRevoke",
+ required: true,
+ label:
+ i18n.str`Does the founder have the right to revoke the trust?`,
+ choices: [
+ {
+ label: i18n.str`Yes`,
+ value: "yes",
+ },
+ {
+ label: i18n.str`No`,
+ value: "no",
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "array",
+ props: {
+ label:
+ i18n.str`If the trust results from the restructuring of pre-existing trust (re-settlement) or the merger of pre-existing trusts, the following information pertaining to the (actual) settlor of the pre-existing trust(s) has to be given`,
+ labelField: "fullName",
+ name: "preExistingSettlors",
+ fields: [
+ {
+ type: "text",
+ props: {
+ name: "fullName",
+ label:
+ i18n.str`Last name(s), first name(s)/entity`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "address",
+ label:
+ i18n.str`Actual address of domicile/registered office`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "country",
+ label: i18n.str`Country`,
+ },
+ },
+ {
+ type: "absoluteTime",
+ props: {
+ name: "dateOfBirth",
+ label: i18n.str`Date of birth`,
+ pattern: "dd/MM/yyyy",
+ // help: i18n.str`format 'dd/MM/yyyy'`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "nationality",
+ label: i18n.str`Nationality`,
+ },
+ },
+ {
+ type: "absoluteTime",
+ props: {
+ name: "dateOfDeath",
+ label: i18n.str`Date of death`,
+ pattern: "dd/MM/yyyy",
+ help: i18n.str`if deceased.`,
+ // help: i18n.str`if deceased. format 'dd/MM/yyyy'`,
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "array",
+ props: {
+ label:
+ i18n.str`Pertaining to the beneficiary/-ies at the time of the signing of this form`,
+ labelField: "fullName",
+ name: "beneficiaryWhenSigning",
+ fields: [
+ {
+ type: "text",
+ props: {
+ name: "fullName",
+ label:
+ i18n.str`Last name(s), first name(s)/entity`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "address",
+ label:
+ i18n.str`Actual address of domicile/registered office`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "country",
+ label: i18n.str`Country`,
+ },
+ },
+ {
+ type: "absoluteTime",
+ props: {
+ name: "dateOfBirth",
+ label: i18n.str`Date of birth`,
+ pattern: "dd/MM/yyyy",
+ // help: i18n.str`format 'dd/MM/yyyy'`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "nationality",
+ label: i18n.str`Nationality`,
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ name: "rightToClaim",
+ label:
+ i18n.str`Has the beneficiary an actual right to claim distribution?`,
+ choices: [
+ {
+ label: i18n.str`Yes`,
+ value: "yes",
+ },
+ {
+ label: i18n.str`No`,
+ value: "no",
+ },
+ ],
+ },
+ },
+ {
+ type: "textArea",
+ props: {
+ label:
+ i18n.str`in addition to certain beneficiaries or if there is/are no defined beneficiary/ies pertaining to (a) group(s) of beneficiaries (e.g. descendants of the settlor) known at the time of the signing of this form`,
+ name: "beneficiaryExtra",
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "array",
+ props: {
+ label:
+ i18n.str`Information pertaining to the protector(s) as well as (a) further person(s) having the right to revoke the trust (in case of revocable trusts) or to appoint the trustee of a trust`,
+ labelField: "asd",
+ name: "nothing",
+ fields: [],
+ },
+ },
+
+ {
+ type: "array",
+ props: {
+ label:
+ i18n.str`Information pertaining to the protectors`,
+ labelField: "fullName",
+ name: "protectors",
+ fields: [
+ {
+ type: "text",
+ props: {
+ name: "fullName",
+ label:
+ i18n.str`Last name(s), first name(s)/entity`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "address",
+ label:
+ i18n.str`Actual address of domicile/registered office`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "country",
+ label: i18n.str`Country`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "dateOfBirth",
+ label: i18n.str`Date of birth`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "nationality",
+ label: i18n.str`Nationality`,
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ name: "rightToClaim",
+ label:
+ i18n.str`Does the protector have the right to revoke the trust?`,
+ choices: [
+ {
+ label: i18n.str`Yes`,
+ value: "yes",
+ },
+ {
+ label: i18n.str`No`,
+ value: "no",
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "array",
+ props: {
+ label:
+ i18n.str`Information pertaining to further persons`,
+ labelField: "fullName",
+ name: "furtherPersons",
+ fields: [
+ {
+ type: "text",
+ props: {
+ name: "fullName",
+ label:
+ i18n.str`Last name(s), first name(s)/entity`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "address",
+ label:
+ i18n.str`Actual address of domicile/registered office`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "country",
+ label: i18n.str`Country`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "dateOfBirth",
+ label: i18n.str`Date of birth`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "nationality",
+ label: i18n.str`Nationality`,
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ name: "rightToClaim",
+ label:
+ i18n.str`Has this further person the right to revoke the trust?`,
+ choices: [
+ {
+ label: i18n.str`Yes`,
+ value: "yes",
+ },
+ {
+ label: i18n.str`No`,
+ value: "no",
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "signature",
+ label: i18n.str`Signature`,
+ },
+ },
+ ],
+ },
+ resolutionSection(current, i18n),
+ ],
+ behavior: function formBehavior(
+ v: Partial<Form902_13.Form>,
+ ): FormState<Form902_13.Form> {
+ return {
+ settlors: {
+ elements: (v.settlors ?? []).map((f) => {
+ return {
+ rightToRevoke: {
+ hidden: v.foundation?.revocability !== "revocable",
+ },
+ };
+ }),
+ },
+ protectors: {
+ elements: (v.protectors ?? []).map((f) => {
+ return {
+ rightToRevoke: {
+ hidden: v.foundation?.revocability !== "revocable",
+ },
+ };
+ }),
+ },
+ furtherPersons: {
+ elements: (v.furtherPersons ?? []).map((f) => {
+ return {
+ rightToRevoke: {
+ hidden: v.foundation?.revocability !== "revocable",
+ },
+ };
+ }),
+ },
+ };
+ },
+});
+
+namespace Form902_13 {
+ interface Foundation {
+ name: string;
+ type: "discretionary" | "non-discretionary";
+ revocability: "revocable" | "irrevocable";
+ }
+ interface Person {
+ fullName: string;
+ address: string;
+ country: string;
+ dateOfBirth: AbsoluteTime;
+ nationality: string;
+ }
+ type WithRevoke<T> = {
+ rightToRevoke: "yes" | "no";
+ } & T;
+ type WithClaim<T> = {
+ rightToClaim: "yes" | "no";
+ } & T;
+ type WithDeath<T> = {
+ dateOfDeath: AbsoluteTime;
+ } & T;
+
+ type Founder = WithRevoke<WithDeath<Person>>;
+ type Beneficiary = WithClaim<Person>;
+
+ export interface Form extends BaseForm {
+ contractingPartner: string;
+ knownAs: string;
+ boardMember: string;
+ foundation: Foundation;
+ settlors: Array<Founder>;
+ preExistingSettlors: Array<Founder>;
+ beneficiaryWhenSigning: Array<Beneficiary>;
+ beneficiaryExtra: Array<Beneficiary>;
+ protectors: Array<WithRevoke<Person>>;
+ furtherPersons: Array<WithRevoke<Person>>;
+ }
+}
diff --git a/packages/aml-backoffice-ui/src/forms/902_15e.ts b/packages/aml-backoffice-ui/src/forms/902_15e.ts
new file mode 100644
index 000000000..2375de389
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/forms/902_15e.ts
@@ -0,0 +1,172 @@
+import type { AbsoluteTime } from "@gnu-taler/taler-util";
+import type { FlexibleForm, InternationalizationAPI } from "@gnu-taler/web-util/browser";
+import { BaseForm } from "./declaration.js";
+import { resolutionSection } from "./simplest.js";
+
+export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): FlexibleForm<Form902_15.Form> => ({
+ design: [
+ {
+ title:
+ i18n.str`Information on life insurance policies with separately managed accounts/securities accounts`,
+ fields: [
+ {
+ type: "textArea",
+ props: {
+ name: "contractingPartner",
+ label: i18n.str`Contracting partner`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "contractualRelationship",
+ label:
+ i18n.str`Name or number of the contractual relationship between the contracting party and the financial intermediary`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "insurancePolicy",
+ label: i18n.str`Insurance policy`,
+ },
+ },
+ {
+ type: "caption",
+ props: {
+ label:
+ i18n.str`The contracting partner confirms in accordance with Art. 41a SRO Regulations that it is a licensed and state-supervised insurance company and that it has entered into the above-mentioned contractual relationship the assets connected to the life insurance policy also mentioned above.`,
+ },
+ },
+ {
+ type: "caption",
+ props: {
+ label:
+ i18n.str`In relation with the above insurance policy, the contracting partner gives the following further details`,
+ },
+ },
+ {
+ type: "group",
+ props: {
+ before: i18n.str`Policy holder`,
+ fields: [
+ {
+ type: "text",
+ props: {
+ name: "holder.fullName",
+ label:
+ i18n.str`Last name(s), first name(s)/entity`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "holder.address",
+ label:
+ i18n.str`Actual address of domicile/registered office (incl. country)`,
+ },
+ },
+ {
+ type: "absoluteTime",
+ props: {
+ name: "holder.dateOfBirth",
+ label: i18n.str`Date of birth`,
+ pattern: "dd/MM/yyyy",
+ // help: i18n.str`format 'dd/MM/yyyy'`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "holder.nationality",
+ label: i18n.str`Nationality`,
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "group",
+ props: {
+ before:
+ i18n.str`Person actually (not in a fiduciary capacity) paying the premiums (to be filled in if not identical with point 1 above)`,
+ fields: [
+ {
+ type: "text",
+ props: {
+ name: "premiumPayer.fullName",
+ label:
+ i18n.str`Last name(s), first name(s)/entity`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "premiumPayer.address",
+ label:
+ i18n.str`Actual address of domicile/registered office (incl. country)`,
+ },
+ },
+ {
+ type: "absoluteTime",
+ props: {
+ name: "premiumPayer.dateOfBirth",
+ label: i18n.str`Date of birth`,
+ pattern: "dd/MM/yyyy",
+ // help: i18n.str`format 'dd/MM/yyyy'`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "premiumPayer.nationality",
+ label: i18n.str`Nationality`,
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "caption",
+ props: {
+ label:
+ i18n.str`The contracting partner hereby undertakes to automatically inform the financial intermediary of any changes. The contracting partner hereby also declares having been given permission by the above individuals and/or entities to transmit their data to the financial intermediary`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "signature",
+ label: i18n.str`Signature`,
+ },
+ },
+ {
+ type: "caption",
+ props: {
+ label:
+ i18n.str`It is a criminal offense to deliberately provide false information on this form (article 251 of the Swiss Criminal Code, document forgery)`,
+ },
+ },
+ ],
+ },
+ resolutionSection(current, i18n),
+ ],
+});
+
+namespace Form902_15 {
+ interface Person {
+ fullName: string;
+ address: string;
+ dateOfBirth: AbsoluteTime;
+ nationality: string;
+ }
+
+ export interface Form extends BaseForm {
+ contractingPartner: string;
+ contractualRelationship: string;
+ insurancePolicy: string;
+ holder: Person;
+ premiumsPayer: Person;
+ signature: string;
+ }
+}
diff --git a/packages/aml-backoffice-ui/src/forms/902_1e.ts b/packages/aml-backoffice-ui/src/forms/902_1e.ts
new file mode 100644
index 000000000..2287db369
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/forms/902_1e.ts
@@ -0,0 +1,660 @@
+import type { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
+import type { FlexibleForm, FormState, InternationalizationAPI } from "@gnu-taler/web-util/browser";
+import { BaseForm, uiForms } from "./declaration.js";
+import { resolutionSection } from "./simplest.js";
+
+export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): FlexibleForm<Form902_1.Form> => ({
+ design: [
+ {
+ title: i18n.str`Information on customer`,
+ description:
+ i18n.str`The customer is the person with whom the member concludes the contract with regard to the financial service provided (civil law). Does the member act as director of a domiciliary company, this domiciliary company is the customer.`,
+ fields: [
+ {
+ type: "choiceStacked",
+ props: {
+ name: "customerType",
+ label: i18n.str`Type of customer`,
+ required: true,
+ choices: [
+ {
+ label: i18n.str`Natural person`,
+ value: "natural",
+ },
+ {
+ label: i18n.str`Legal entity`,
+ value: "legal",
+ },
+ ],
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "naturalCustomer.fullName",
+ label: i18n.str`Full name`,
+ required: true,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "naturalCustomer.address",
+ label: i18n.str`Residential address`,
+ required: true,
+ },
+ },
+ {
+ type: "integer",
+ props: {
+ name: "naturalCustomer.telephone",
+ label: i18n.str`Telephone`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "naturalCustomer.email",
+ label: i18n.str`E-mail`,
+ },
+ },
+ {
+ type: "absoluteTime",
+ props: {
+ name: "naturalCustomer.dateOfBirth",
+ label: i18n.str`Date of birth`,
+ required: true,
+ // help: i18n.str`format 'dd/MM/yyyy'`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "naturalCustomer.nationality",
+ label: i18n.str`Nationality`,
+ required: true,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "naturalCustomer.document",
+ label: i18n.str`Identification document`,
+ required: true,
+ },
+ },
+ {
+ type: "file",
+ props: {
+ name: "naturalCustomer.documentAttachment",
+ label: i18n.str`Document attachment`,
+ required: true,
+ maxBites: 2 * 1024 * 1024,
+ accept: ".png",
+ help: i18n.str`Max size of 2 mega bytes`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "naturalCustomer.companyName",
+ label: i18n.str`Company name`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "naturalCustomer.office",
+ label: i18n.str`Registered office`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "naturalCustomer.companyDocument",
+ label: i18n.str`Company identification document`,
+ },
+ },
+ {
+ type: "file",
+ props: {
+ name: "naturalCustomer.companyDocumentAttachment",
+ label: i18n.str`Document attachment`,
+ required: true,
+ maxBites: 2 * 1024 * 1024,
+ accept: ".png",
+ help: i18n.str`Max size of 2 mega bytes`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "legalCustomer.companyName",
+ label: i18n.str`Company name`,
+ required: true,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "legalCustomer.domicile",
+ label: i18n.str`Domicile`,
+ required: true,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "legalCustomer.contactPerson",
+ label: i18n.str`Contact person`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "legalCustomer.telephone",
+ label: i18n.str`Telephone`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "legalCustomer.email",
+ label: i18n.str`E-mail`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "legalCustomer.document",
+ label: i18n.str`Identification document`,
+ help: i18n.str`Not older than 12 month`,
+ },
+ },
+ {
+ type: "file",
+ props: {
+ name: "legalCustomer.documentAttachment",
+ label: i18n.str`Document attachment`,
+ required: true,
+ maxBites: 2 * 1024 * 1024,
+ accept: ".png",
+ help: i18n.str`Max size of 2 mega bytes`,
+ },
+ },
+ ],
+ },
+ {
+ title:
+ i18n.str`Information on the natural persons who establish the business relationship for legal entities and partnerships`,
+ description:
+ i18n.str`For legal entities and partnerships the identity of the natural persons who establish the business relationship must be verified.`,
+ fields: [
+ {
+ type: "array",
+ props: {
+ name: "businessEstablisher",
+ label: i18n.str`Persons`,
+ required: true,
+ placeholder: i18n.str`this is the placeholder`,
+ fields: [
+ {
+ type: "text",
+ props: {
+ name: "fullName",
+ label: i18n.str`Full name`,
+ required: true,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "address",
+ label: i18n.str`Residential address`,
+ required: true,
+ },
+ },
+ {
+ type: "absoluteTime",
+ props: {
+ name: "dateOfBirth",
+ label: i18n.str`Date of birth`,
+ required: true,
+ // help: i18n.str`format 'dd/MM/yyyy'`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "nationality",
+ label: i18n.str`Nationality`,
+ required: true,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "typeOfAuthorization",
+ label:
+ i18n.str`Type of authorization (signatory of representation)`,
+ required: true,
+ },
+ },
+ {
+ type: "file",
+ props: {
+ name: "documentAttachment",
+ label:
+ i18n.str`Identification document attachment`,
+ required: true,
+ maxBites: 2 * 1024 * 1024,
+ accept: ".png",
+ help: i18n.str`Max size of 2 mega bytes`,
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ name: "powerOfAttorneyArrangements",
+ label: i18n.str`Power of attorney arrangements`,
+ required: true,
+ choices: [
+ {
+ label: i18n.str`CR extract`,
+ value: "cr",
+ },
+ {
+ label: i18n.str`Mandate`,
+ value: "mandate",
+ },
+ {
+ label: i18n.str`Other`,
+ value: "other",
+ },
+ ],
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "powerOfAttorneyArrangementsOther",
+ label: i18n.str`Power of attorney arrangements`,
+ required: true,
+ },
+ },
+ ],
+ labelField: "fullName",
+ },
+ },
+ ],
+ },
+ {
+ title: i18n.str`Acceptance of business relationship`,
+ fields: [
+ {
+ type: "absoluteTime",
+ props: {
+ name: "acceptance.when",
+ pattern: "dd/MM/yyyy",
+ label: i18n.str`Date (conclusion of contract)`,
+ // help: i18n.str`format 'dd/MM/yyyy'`,
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ name: "acceptance.acceptedBy",
+ label: i18n.str`Accepted by`,
+ required: true,
+ choices: [
+ {
+ label: i18n.str`Face-to-face meeting with customer`,
+ value: "face-to-face",
+ },
+ {
+ label:
+ i18n.str`Correspondence: authenticated copy of identification document obtained`,
+ value: "correspondence-document",
+ },
+ {
+ label:
+ i18n.str`Correspondence: residential address validated`,
+ value: "correspondence-address",
+ },
+ ],
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ name: "acceptance.typeOfCorrespondence",
+ label: i18n.str`Type of correspondence service`,
+ choices: [
+ {
+ label: i18n.str`to the customer`,
+ value: "customer",
+ },
+ {
+ label: i18n.str`hold at bank`,
+ value: "bank",
+ },
+ {
+ label: i18n.str`to the member`,
+ value: "member",
+ },
+ {
+ label: i18n.str`to a third party`,
+ value: "third-party",
+ },
+ ],
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "acceptance.thirdPartyFullName",
+ label: i18n.str`Third party full name`,
+ required: true,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "acceptance.thirdPartyAddress",
+ label: i18n.str`Third party address`,
+ required: true,
+ },
+ },
+ {
+ type: "selectMultiple",
+ props: {
+ name: "acceptance.language",
+ label: i18n.str`Languages`,
+ choices: uiForms.currencies(i18n),
+ unique: true,
+ },
+ },
+ {
+ type: "textArea",
+ props: {
+ name: "acceptance.furtherInformation",
+ label: i18n.str`Further information`,
+ },
+ },
+ ],
+ },
+ {
+ title:
+ i18n.str`Information on the beneficial owner of the assets and/or controlling person`,
+ description:
+ i18n.str`Establishment of the beneficial owner of the assets and/or controlling person`,
+ fields: [
+ {
+ type: "choiceStacked",
+ props: {
+ name: "establishment",
+ label: i18n.str`The customer is`,
+ required: true,
+ choices: [
+ {
+ label:
+ i18n.str`a natural person and there are no doubts that this person is the sole beneficial owner of the assets`,
+ value: "natural",
+ },
+ {
+ label:
+ i18n.str`a foundation (or a similar construct; incl. underlying companies)`,
+ value: "foundation",
+ },
+ {
+ label:
+ i18n.str`a trust (incl. underlying companies)`,
+ value: "trust",
+ },
+ {
+ label:
+ i18n.str`a life insurance policy with separately managed accounts/securities accounts`,
+ value: "insurance-wrapper",
+ },
+ {
+ label: i18n.str`all other cases`,
+ value: "other",
+ },
+ ],
+ },
+ },
+ ],
+ },
+ {
+ title:
+ i18n.str`Evaluation with regard to embargo procedures/terrorism lists on establishing the business relationship`,
+ description:
+ i18n.str`Verification whether the customer, beneficial owners of the assets, controlling persons, authorized representatives or other involved persons are listed on an embargo/terrorism list (date of verification/result)`,
+ fields: [
+ {
+ type: "textArea",
+ props: {
+ name: "embargoEvaluation",
+ help: i18n.str`The evaluation must be made at the beginning of the business relationship and has to be repeated in the case of permanent business relationship every time the according lists are updated.`,
+ label: i18n.str`Evaluation`,
+ },
+ },
+ ],
+ },
+ {
+ title:
+ i18n.str`In the case of cash transactions/occasional customers: Information on type and purpose of business relationship`,
+ description:
+ i18n.str`These details are only necessary for occasional customers, i.e. money exchange, money and asset transfer or other cash transactions provided that no customer profile (VQF doc. No. 902.5) is created`,
+ fields: [
+ {
+ type: "choiceStacked",
+ props: {
+ name: "cashTransactions.typeOfBusiness",
+ label: i18n.str`Type of business relationship`,
+ choices: [
+ {
+ label: i18n.str`Money exchange`,
+ value: "money-exchange",
+ },
+ {
+ label: i18n.str`Money and asset transfer`,
+ value: "money-and-asset-transfer",
+ },
+ {
+ label:
+ i18n.str`Other cash transactions. Specify below`,
+ value: "other",
+ },
+ ],
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "cashTransactions.otherTypeOfBusiness",
+ required: true,
+ label: i18n.str`Specify other cash transactions:`,
+ },
+ },
+ {
+ type: "textArea",
+ props: {
+ name: "cashTransactions.purpose",
+ label:
+ i18n.str`Purpose of the business relationship (purpose of service requested)`,
+ },
+ },
+ ],
+ },
+ resolutionSection(current, i18n),
+ ],
+ behavior: function formBehavior(
+ v: Partial<Form902_1.Form>,
+ ): FormState<Form902_1.Form> {
+ return {
+ fullName: {
+ disabled: true,
+ },
+ businessEstablisher: {
+ elements: (v.businessEstablisher ?? []).map((be) => {
+ return {
+ powerOfAttorneyArrangementsOther: {
+ hidden: be.powerOfAttorneyArrangements !== "other",
+ },
+ };
+ }),
+ },
+ acceptance: {
+ thirdPartyFullName: {
+ hidden: v.acceptance?.typeOfCorrespondence !== "third-party",
+ },
+ thirdPartyAddress: {
+ hidden: v.acceptance?.typeOfCorrespondence !== "third-party",
+ },
+ },
+ cashTransactions: {
+ otherTypeOfBusiness: {
+ hidden: v.cashTransactions?.typeOfBusiness !== "other",
+ },
+ },
+ naturalCustomer: {
+ fullName: {
+ hidden: v.customerType !== "natural",
+ },
+ address: {
+ hidden: v.customerType !== "natural",
+ },
+ telephone: {
+ hidden: v.customerType !== "natural",
+ },
+ email: {
+ hidden: v.customerType !== "natural",
+ },
+ dateOfBirth: {
+ hidden: v.customerType !== "natural",
+ },
+ nationality: {
+ hidden: v.customerType !== "natural",
+ },
+ document: {
+ hidden: v.customerType !== "natural",
+ },
+ companyName: {
+ hidden: v.customerType !== "natural",
+ },
+ office: {
+ hidden: v.customerType !== "natural",
+ },
+ companyDocument: {
+ hidden: v.customerType !== "natural",
+ },
+ companyDocumentAttachment: {
+ hidden: v.customerType !== "natural",
+ },
+ documentAttachment: {
+ hidden: v.customerType !== "natural",
+ },
+ },
+ legalCustomer: {
+ companyName: {
+ hidden: v.customerType !== "legal",
+ },
+ contactPerson: {
+ hidden: v.customerType !== "legal",
+ },
+ document: {
+ hidden: v.customerType !== "legal",
+ },
+ domicile: {
+ hidden: v.customerType !== "legal",
+ },
+ email: {
+ hidden: v.customerType !== "legal",
+ },
+ telephone: {
+ hidden: v.customerType !== "legal",
+ },
+ documentAttachment: {
+ hidden: v.customerType !== "legal",
+ },
+ },
+ };
+ },
+});
+
+namespace Form902_1 {
+ interface LegalEntityCustomer {
+ companyName: string;
+ domicile: string;
+ contactPerson: string;
+ telephone: string;
+ email: string;
+ document: string;
+ documentAttachment: string;
+ }
+ interface NaturalCustomer {
+ fullName: string;
+ address: string;
+ telephone: string;
+ email: string;
+ dateOfBirth: AbsoluteTime;
+ nationality: string;
+ document: string;
+ documentAttachment: string;
+ companyName: string;
+ office: string;
+ companyDocument: string;
+ companyDocumentAttachment: string;
+ }
+
+ interface Person {
+ fullName: string;
+ address: string;
+ dateOfBirth: AbsoluteTime;
+ nationality: string;
+ typeOfAuthorization: string;
+ document: string;
+ documentAttachment: string;
+ powerOfAttorneyArrangements: "cr" | "mandate" | "other";
+ powerOfAttorneyArrangementsOther: string;
+ }
+
+ interface Acceptance {
+ when: AbsoluteTime;
+ acceptedBy: "face-to-face" | "authenticated-copy";
+ typeOfCorrespondence: string;
+ language: string[];
+ furtherInformation: string;
+ thirdPartyFullName: string;
+ thirdPartyAddress: string;
+ }
+
+ interface BeneficialOwner {
+ establishment:
+ | "natural-person"
+ | "foundation"
+ | "trust"
+ | "insurance-wrapper"
+ | "other";
+ }
+
+ interface CashTransactions {
+ typeOfBusiness: "money-exchange" | "money-and-asset-transfer" | "other";
+ otherTypeOfBusiness: string;
+ purpose: string;
+ }
+
+ export interface Form extends BaseForm {
+ fullName: string;
+ customerType: "natural" | "legal";
+ naturalCustomer: NaturalCustomer;
+ legalCustomer: LegalEntityCustomer;
+ businessEstablisher: Array<Person>;
+ acceptance: Acceptance;
+ beneficialOwner: BeneficialOwner;
+ embargoEvaluation: string;
+ cashTransactions: CashTransactions;
+ // enclosures: Enclosures;
+ }
+}
diff --git a/packages/aml-backoffice-ui/src/forms/902_4e.ts b/packages/aml-backoffice-ui/src/forms/902_4e.ts
new file mode 100644
index 000000000..b31a8dcba
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/forms/902_4e.ts
@@ -0,0 +1,787 @@
+import type { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
+import type { FlexibleForm, FormState, InternationalizationAPI } from "@gnu-taler/web-util/browser";
+import { h as create } from "preact";
+import { resolutionSection } from "./simplest.js";
+import { BaseForm } from "./declaration.js";
+import { ArrowRightIcon, ChevronRightIcon } from "./icons.js";
+
+export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): FlexibleForm<Form902_4.Form> => ({
+ design: [
+ {
+ title: i18n.str`Risk Profile AMLA`,
+ description:
+ i18n.str`Evaluation of business relationship with increased risk and definition of criteria for transaction monitoring.`,
+ fields: [
+ {
+ type: "caption",
+ props: {
+ label:
+ i18n.str`The member performs additional clarifications if the business relationship or the transaction is classified as increased risk (Art. 56 SRO Regulations)`,
+ before: create(ArrowRightIcon, { class: "h-6 w-6" }),
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "customer",
+ label: i18n.str`Customer`,
+ help: i18n.str`Pursuant identification form (VQF doc. Nr. 902.1) numeral 1`,
+ },
+ },
+ ],
+ },
+ {
+ title:
+ i18n.str`Evaluation of politically exposed persons (PEP-Check)`,
+ fields: [
+ {
+ type: "caption",
+ props: {
+ label:
+ i18n.str`This evaluation has to be completed by all members for every business relationship`,
+ before: create(ArrowRightIcon, { class: "h-6 w-6" }),
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ label: i18n.str`Foreign PEP`,
+ // tooltip:
+ // i18n.str`Definition see Art. 7 lit. g numeral 1 SRO Regulations`,
+ help: i18n.str`Is the customer, the beneficial owner or the controlling person or authorized representative a foreign PEP or closely related to such a person?`,
+ name: "pep.foreign",
+ choices: [
+ {
+ label: i18n.str`No`,
+ value: "no",
+ },
+ {
+ label: i18n.str`Yes`,
+ description:
+ i18n.str`The business relationship is compulsory classified as increased risk`,
+ value: "yes",
+ },
+ ],
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ label:
+ i18n.str`Domestic PEP and PEP of International Organizations`,
+ // tooltip:
+ // i18n.str`Definition see Art. 7 lit. g numeral 2 and 3 SRO Regulations `,
+ help: i18n.str`Is the customer, the beneficial owner or the controlling person or authorized representative a domestic PEP or PEP in International Organizations or closely related to such a person?`,
+ name: "pep.domestic",
+ choices: [
+ {
+ label: i18n.str`No`,
+ value: "no",
+ },
+ {
+ label:
+ i18n.str`Yes, but NOT risk criterion pursuant to numeral 3 subsequently increased.`,
+ value: "yes-but-no-risk",
+ },
+ {
+ label:
+ i18n.str`Yes, AND a risk criterion pursuant to numeral 3 subsequently increased.`,
+ description:
+ i18n.str`Classification of the business relationship as increased risk is compulsory`,
+ value: "yes",
+ },
+ ],
+ },
+ },
+ {
+ type: "absoluteTime",
+ props: {
+ label:
+ i18n.str`The decision of the Senior executive body on the acceptance of a business relationship with a PEP was obtained on`,
+ name: "pep.when",
+ pattern: "dd/MM/yyyy",
+ // placeholder: i18n.str`dd/MM/yyyy`,
+ },
+ },
+ ],
+ },
+ {
+ title:
+ 'Evaluation "high risk" or non-cooperative country' as TranslatedString,
+ fields: [
+ {
+ type: "caption",
+ props: {
+ label:
+ i18n.str`This evaluation has to be completed by all members for every business relationship`,
+ before: create(ArrowRightIcon, { class: "h-6 w-6" }),
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ label: '"High risk" or non-cooperative country' as TranslatedString,
+ help: 'Is the customer, the beneficial owner or the controlling person or authorized representative in a country considered by the FATF "high risk" or non-cooperative and for which FATF requires increased diligence?' as TranslatedString,
+ name: "highRisk.evaluation",
+ choices: [
+ {
+ label: i18n.str`No`,
+ value: "no",
+ },
+ {
+ label: i18n.str`Yes`,
+ description:
+ i18n.str`considered as business relationship with increased risk`,
+ value: "yes",
+ },
+ ],
+ },
+ },
+ {
+ type: "absoluteTime",
+ props: {
+ label:
+ i18n.str`The decision of the Senior executive body on the acceptance of a business relationship with a PEP was obtained on`,
+ name: "highRisk.when",
+ pattern: "dd/MM/yyyy",
+ // placeholder: i18n.str`dd/MM/yyyy`,
+ },
+ },
+ ],
+ },
+ {
+ title: i18n.str`Evaluation of business relationship risk`,
+ fields: [
+ {
+ type: "caption",
+ props: {
+ label:
+ i18n.str`This evaluation has to be completed by all members who have in total more than 20 customers for every business relationship. At least two risk categories have to be chosen and assessed`,
+ before: create(ArrowRightIcon, { class: "h-6 w-6" }),
+ },
+ },
+ {
+ type: "group",
+ props: {
+ before: i18n.str`a) Country risk (nationality)`,
+ fields: [
+ {
+ type: "choiceStacked",
+ props: {
+ label: i18n.str`Domicile/residential address`,
+ name: "evaluation.nationality.address",
+ choices: [
+ {
+ label: i18n.str`Customer`,
+ value: "customer",
+ },
+ {
+ label:
+ i18n.str`Beneficial owner of the assets`,
+ value: "owner",
+ },
+ {
+ label: i18n.str`Controlling person`,
+ value: "controlling",
+ },
+ ],
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ label: i18n.str`Nationality`,
+ name: "evaluation.nationality.nationality",
+ choices: [
+ {
+ label: i18n.str`Customer`,
+ value: "customer",
+ },
+ {
+ label:
+ i18n.str`Beneficial owner of the assets`,
+ value: "owner",
+ },
+ ],
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ label: i18n.str`Risk level`,
+ name: "evaluation.nationality.risk",
+ choices: [
+ {
+ label:
+ i18n.str`Risk 0 acc. to VQF country list (VQF doc. no. 902.4.1)`,
+ value: "low",
+ },
+ {
+ label:
+ i18n.str`Risk 1 acc. to VQF country list (VQF doc. no. 902.4.1)`,
+ value: "medium",
+ },
+ {
+ label:
+ i18n.str`Risk 2 acc. to VQF country list (VQF doc. no. 902.4.1)`,
+ value: "high",
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "group",
+ props: {
+ before: i18n.str`b) Country risk (business activity)`,
+ fields: [
+ {
+ type: "choiceStacked",
+ props: {
+ label: i18n.str`Place of business activity`,
+ name: "evaluation.business.place",
+ choices: [
+ {
+ label: i18n.str`Customer`,
+ value: "customer",
+ },
+ {
+ label:
+ i18n.str`Beneficial owner of the assets`,
+ value: "owner",
+ },
+ ],
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ label: i18n.str`Risk level`,
+ name: "evaluation.business.risk",
+ choices: [
+ {
+ label:
+ i18n.str`Risk 0 acc. to VQF country list (VQF doc. no. 902.4.1)`,
+ value: "low",
+ },
+ {
+ label:
+ i18n.str`Risk 1 acc. to VQF country list (VQF doc. no. 902.4.1)`,
+ value: "medium",
+ },
+ {
+ label:
+ i18n.str`Risk 2 acc. to VQF country list (VQF doc. no. 902.4.1)`,
+ value: "high",
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "group",
+ props: {
+ before: i18n.str`c) Country risk (payments)`,
+ fields: [
+ {
+ type: "caption",
+ props: {
+ label:
+ i18n.str`Country of origin and destination of frequent payments (if known)`,
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ label: i18n.str`Risk level`,
+ name: "evaluation.payments.risk",
+ choices: [
+ {
+ label:
+ i18n.str`Risk 0 acc. to VQF country list (VQF doc. no. 902.4.1)`,
+ value: "low",
+ },
+ {
+ label:
+ i18n.str`Risk 1 acc. to VQF country list (VQF doc. no. 902.4.1)`,
+ value: "medium",
+ },
+ {
+ label:
+ i18n.str`Risk 2 acc. to VQF country list (VQF doc. no. 902.4.1)`,
+ value: "high",
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "group",
+ props: {
+ before: i18n.str`d) Industry risk`,
+ fields: [
+ {
+ type: "choiceStacked",
+ props: {
+ label:
+ i18n.str`Nature of customer's business activity`,
+ name: "evaluation.industry.nature",
+ choices: [
+ {
+ label: i18n.str`Customer`,
+ value: "customer",
+ },
+ {
+ label:
+ i18n.str`Beneficial owner of the assets`,
+ value: "owner",
+ },
+ ],
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ label: i18n.str`Risk level`,
+ name: "evaluation.payments.risk",
+ choices: [
+ {
+ label:
+ i18n.str`Clearly defined, transparent, easily comprehensible business activity well known to the member`,
+ value: "low",
+ },
+ {
+ label:
+ i18n.str`Business activity with a high level of cash transactions`,
+ value: "medium-cash",
+ },
+ {
+ label:
+ i18n.str`Business activity not well known to the member`,
+ value: "medium-unknown",
+ },
+ {
+ label:
+ i18n.str`Trade in munitions/arms, raw gem stones/diamonds, jewelry, international trade in exotic animals, casino and lottery business, trade in erotic wares`,
+ value: "high-restricted",
+ },
+ {
+ label:
+ i18n.str`Member has no personal knowledge of the customer's industry`,
+ value: "high-unknown",
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "group",
+ props: {
+ before: i18n.str`e) Contact risk`,
+ fields: [
+ {
+ type: "caption",
+ props: {
+ label:
+ i18n.str`Types of contact to the customer/ beneficial owner of the assets`,
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ label: i18n.str`Risk level`,
+ name: "evaluation.contact.risk",
+ choices: [
+ {
+ label:
+ i18n.str`Personal acquaintance between member and customer/beneficial owner of the assets over several years (at least 2) prior to entering into the business relationship`,
+ value: "low",
+ },
+ {
+ label:
+ i18n.str`The customer/beneficial owner was not personally known to the member for several years (at least 2) prior to entering into the business relationship; however (a) no business was entered into in the absence of the customer/beneficial owner, or (b) the customer was at least introduced/brokered by a trusted third party`,
+ value: "medium",
+ },
+ {
+ label:
+ i18n.str`The customer/beneficial owner was not personally known to the member and business was entered into in the absence of the former (relationship by correspondence) and the customer was not introduced/brokered by a trusted third party`,
+ value: "high",
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "group",
+ props: {
+ before: i18n.str`f) Product risk`,
+ fields: [
+ {
+ type: "caption",
+ props: {
+ label:
+ i18n.str`Nature of services and products requested by the customer`,
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ label: i18n.str`Risk level`,
+ name: "evaluation.product.risk",
+ choices: [
+ {
+ label:
+ i18n.str`Easy to understand, transparent services and products whose financial background is easy to comprehend and verify`,
+ value: "low",
+ },
+ {
+ label:
+ i18n.str`More sophisticated services/products whose financial background is not readily easy to comprehend and verify`,
+ value: "medium",
+ },
+ {
+ label:
+ i18n.str`Main focus on offshore business (especially: relationships with domiciliary companies or other such offshore organisations)`,
+ value: "high-offshore",
+ },
+ {
+ label:
+ i18n.str`Complex structures in particular by using a domiciliary company with fiduciary shareholders in a non-transparent jurisdiction, without comprehensible reason or for the purpose of short-term asset placement`,
+ value: "high-structure",
+ },
+ {
+ label:
+ i18n.str`The customer or beneficial owner of the assets has a large number of accounts with pass-through transactions (pass-through accounts)`,
+ value: "high-accounts",
+ },
+ {
+ label:
+ i18n.str`Complex services/products whose financial background can’t be understood or verified with considerable effort`,
+ value: "high-service",
+ },
+ {
+ label:
+ i18n.str`Frequent transactions with increased risks`,
+ value: "high-freq-tx",
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "group",
+ props: {
+ before: i18n.str`g) Criteria defined by the member`,
+ fields: [
+ {
+ type: "text",
+ props: {
+ label: i18n.str`Criteria definition`,
+ name: "evaluation.custom.definition",
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ label: i18n.str`Risk level`,
+ name: "evaluation.custom.risk",
+ choices: [
+ {
+ label: i18n.str`Low`,
+ value: "low",
+ },
+ {
+ label: i18n.str`Medium`,
+ value: "medium",
+ },
+ {
+ label: i18n.str`High`,
+ value: "high",
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "caption",
+ props: {
+ label:
+ i18n.str`Overall assessment of the business relationship`,
+ },
+ },
+ {
+ type: "group",
+ props: {
+ before:
+ i18n.str`A business relationship is classified as increased risk if:`,
+ fields: [
+ {
+ type: "caption",
+ props: {
+ label:
+ i18n.str`Business relationship with PEP pursuant to numeral 1 (no exception possible)`,
+ before: create(ChevronRightIcon, { class: "h-6 w-6" }),
+ },
+ },
+ {
+ type: "caption",
+ props: {
+ label:
+ 'Relationship with a person from a "high risk" or non-cooperative country according to numeral 2 (no exceptions possible)' as TranslatedString,
+ before: create(ChevronRightIcon, { class: "h-6 w-6" }),
+ },
+ },
+ {
+ type: "caption",
+ props: {
+ label:
+ i18n.str`Min. one criterion pursuant to numeral 3 was assessed with risk 2 or min. two criteria pursuant to numeral 3 were assessed with risk 1 (exception: justification by the member below why the business relationship overall does not have to be classified as increased risk despite the fact that individual risk criteria are increased)`,
+ before: create(ChevronRightIcon, { class: "h-6 w-6" }),
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "textArea",
+ props: {
+ label:
+ i18n.str`Justification for differing risk assessment`,
+ name: "evaluation.overall.justification",
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ label: i18n.str`Risk classified`,
+ name: "evaluation.overall.risk",
+ choices: [
+ {
+ label:
+ i18n.str`Business relationship _without_ increased risk`,
+ value: "without",
+ },
+ {
+ label:
+ i18n.str`Business relationship __with__ increased risk`,
+ value: "with",
+ },
+ ],
+ },
+ },
+ {
+ type: "absoluteTime",
+ props: {
+ label:
+ i18n.str`The decision of the Senior executive body on the acceptance of a business relationship with a PEP was obtained on`,
+ name: "evaluation.when",
+ pattern: "dd/MM/yyyy",
+ // placeholder: i18n.str`dd/MM/yyyy`,
+ },
+ },
+ ],
+ },
+ {
+ title:
+ i18n.str`Criteria for identification of increased risk transactions (transaction monitoring)`,
+ fields: [
+ {
+ type: "group",
+ props: {
+ before: i18n.str`Criteria`,
+ fields: [
+ {
+ type: "caption",
+ props: {
+ label:
+ i18n.str`Classification as as increased risk is compulsory if`,
+ },
+ },
+ {
+ type: "caption",
+ props: {
+ before: create(ChevronRightIcon, { class: "w-6 h-6" }),
+ label:
+ i18n.str`Transactions for which assets with an equivalent value of CHF 100'000.- or more are physically introduced at the beginning of the business relationship, either at once or in a staggered manner`,
+ },
+ },
+ {
+ type: "caption",
+ props: {
+ before: create(ChevronRightIcon, { class: "w-6 h-6" }),
+ label:
+ 'Money and asset transfers ("money transfer") whereby a single transaction or multiple transactions which appear to be related reach or exceed the amount of CHF 5,000.-' as TranslatedString,
+ },
+ },
+ {
+ type: "caption",
+ props: {
+ before: create(ChevronRightIcon, { class: "w-6 h-6" }),
+ label:
+ 'Payments from or to a country that is considered to be "high risk" or non-cooperative by the FATF and for which increased diligence is required' as TranslatedString,
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "group",
+ props: {
+ before:
+ i18n.str`Additional criteria defined by the member`,
+ fields: [
+ {
+ type: "caption",
+ props: {
+ before: create(ArrowRightIcon, { class: "w-6 h-6" }),
+ label:
+ i18n.str`All members have to define min. 1 additional criterion for every business relationship to identify unusual transactions`,
+ },
+ },
+ {
+ type: "textArea",
+ props: {
+ label: i18n.str`Description`,
+ name: "criteria.additional",
+ },
+ },
+ {
+ type: "group",
+ props: {
+ before:
+ i18n.str`Possible criteria (Art. 59 para. 2 SRO Regulations)`,
+ fields: [
+ {
+ type: "caption",
+ props: {
+ before: create(ChevronRightIcon, { class: "w-4 h-4" }),
+ label:
+ i18n.str`the amount of inflowing and outflowing assets`,
+ },
+ },
+ {
+ type: "caption",
+ props: {
+ before: create(ChevronRightIcon, { class: "w-4 h-4" }),
+ label:
+ i18n.str`type, volume and frequency of transactions usual to the business relationship (considerable variance would be unusual)`,
+ },
+ },
+ {
+ type: "caption",
+ props: {
+ before: create(ChevronRightIcon, { class: "w-4 h-4" }),
+ label:
+ i18n.str`type, volume and frequency of transactions usual to comparable business relationships (considerable variance would be unusual)`,
+ },
+ },
+ {
+ type: "caption",
+ props: {
+ before: create(ChevronRightIcon, { class: "w-4 h-4" }),
+ label:
+ i18n.str`description of expected transaction patterns which the client notify the member of (considerable variance would be unusual)`,
+ },
+ },
+ {
+ type: "caption",
+ props: {
+ before: create(ChevronRightIcon, { class: "w-4 h-4" }),
+ label:
+ 'The country of origin or destination of payments, especially in the case of payments from or to a country considered by the FATF as "high risk" or non-cooperative' as TranslatedString,
+ },
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ ],
+ },
+ resolutionSection(current, i18n),
+ ],
+ behavior: function formBehavior(
+ v: Partial<Form902_4.Form>,
+ ): FormState<Form902_4.Form> {
+ return {
+ };
+ },
+});
+
+namespace Form902_4 {
+ export interface Form extends BaseForm {
+ customer: string;
+ fullName: string;
+ pep: {
+ foreign: "yes" | "no";
+ domestic: "yes" | "no" | "yes-but-no-risk";
+ when: AbsoluteTime;
+ };
+ highRisk: {
+ evaluation: "yes" | "no";
+ when: AbsoluteTime;
+ };
+ evaluation: {
+ nationality: {
+ address: "customer" | "owner" | "controlling";
+ nationality: "customer" | "owner";
+ risk: "low" | "medium" | "high";
+ };
+ business: {
+ place: "customer" | "owner";
+ risk: "low" | "medium" | "high";
+ };
+ payments: {
+ risk: "low" | "medium" | "high";
+ };
+ industry: {
+ nature: "customer" | "owner";
+ risk:
+ | "low"
+ | "medium-cash"
+ | "medium-unknown"
+ | "high-restricted"
+ | "high-unknown";
+ };
+ contact: {
+ risk: "low" | "medium" | "high";
+ };
+ product: {
+ risk:
+ | "low"
+ | "medium"
+ | "high-offshore"
+ | "high-structure"
+ | "high-accounts"
+ | "high-service"
+ | "high-freq-tx";
+ };
+ custom: {
+ definition: string;
+ risk: "low" | "medium" | "high";
+ };
+ overall: {
+ justification: string;
+ risk: "with" | "without";
+ };
+ };
+ criteria: {
+ additional: string;
+ };
+ }
+}
diff --git a/packages/aml-backoffice-ui/src/forms/902_5e.ts b/packages/aml-backoffice-ui/src/forms/902_5e.ts
new file mode 100644
index 000000000..3af03ed22
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/forms/902_5e.ts
@@ -0,0 +1,255 @@
+import type { TranslatedString } from "@gnu-taler/taler-util";
+import type { FlexibleForm, FormState, InternationalizationAPI } from "@gnu-taler/web-util/browser";
+import { BaseForm, uiForms } from "./declaration.js";
+import { resolutionSection } from "./simplest.js";
+
+export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): FlexibleForm<Form902_5.Form> => ({
+ design: [
+ {
+ title: i18n.str`Customer Profile`,
+ description:
+ i18n.str`The information below has to refer to the persons from whom the assets originate ultimately (e.g. beneficial owner of the assets, founder/creator of a trust or foundation). Is the customer an operational legal entity or partnership the information may refer to the entity itself (not to the controlling person), unless the entity holds the assets in trust for a third party.`,
+ fields: [
+ {
+ type: "text",
+ props: {
+ name: "customer",
+ label: i18n.str`Customer`,
+ help: i18n.str`Pursuant Identification Form (VQF doc. No. 902.1) numeral 1`,
+ },
+ },
+ ],
+ },
+ {
+ title: i18n.str`Business activity`,
+ fields: [
+ {
+ type: "textArea",
+ props: {
+ label: i18n.str`Profession, business activities`,
+ name: "businessActivity",
+ help: i18n.str`former, current, potentially planned`,
+ },
+ },
+ ],
+ },
+ {
+ title: i18n.str`Financial circumstances`,
+ fields: [
+ {
+ type: "textArea",
+ props: {
+ label: i18n.str`Income and assets, liabilities`,
+ name: "financial",
+ help: i18n.str`estimated`,
+ },
+ },
+ ],
+ },
+ {
+ title: i18n.str`Origin of the deposited assets involved`,
+ fields: [
+ {
+ type: "text",
+ props: {
+ label: i18n.str`Nature`,
+ name: "originOfAssets.nature",
+ help: i18n.str`nature of the involved assets`,
+ },
+ },
+ {
+ type: "selectOne",
+ props: {
+ name: "originOfAssets.currency",
+ label: i18n.str`Currency`,
+ choices: uiForms.currencies(i18n),
+ },
+ },
+ {
+ type: "integer",
+ props: {
+ label: i18n.str`Amount`,
+ name: "originOfAssets.amount",
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ label: i18n.str`Category`,
+ name: "originOfAssets.category",
+ choices: [
+ {
+ label: i18n.str`Savings`,
+ value: "savings",
+ },
+ {
+ label: i18n.str`Own business operations`,
+ value: "own-business",
+ },
+ {
+ label: i18n.str`Inheritance`,
+ value: "inheritance",
+ },
+ {
+ label: i18n.str`Other, what?`,
+ value: "other",
+ },
+ ],
+ },
+ },
+ {
+ type: "text",
+ props: {
+ label: i18n.str`Other category`,
+ name: "originOfAssets.categoryOther",
+ required: true,
+ },
+ },
+ {
+ type: "textArea",
+ props: {
+ label:
+ i18n.str`Detailed description of the origins/economical background of the assets involved in the business relationship`,
+ name: "originOfAssets.details",
+ },
+ },
+ ],
+ },
+ {
+ title:
+ i18n.str`Nature and purpose of the business relationship`,
+ fields: [
+ {
+ type: "textArea",
+ props: {
+ label: i18n.str`Purpose of the business relationship`,
+ name: "nature.purpose",
+ help: i18n.str`nature of the involved assets`,
+ },
+ },
+ {
+ type: "textArea",
+ props: {
+ label:
+ i18n.str`Information on the planned development of the business relationship and the assets`,
+ name: "nature.plan",
+ },
+ },
+ {
+ type: "textArea",
+ props: {
+ label:
+ i18n.str`Especially in the case of cash or money and asset transfer transactions with regular customers: Details on usual business volume, Information on the beneficiaries, (Full name, address, bank account)`,
+ name: "nature.cashOrMoneyTransfer",
+ },
+ },
+ ],
+ },
+ {
+ title: i18n.str`Relationship with third parties`,
+ fields: [
+ {
+ type: "textArea",
+ props: {
+ label:
+ i18n.str`Relation of the customer to the beneficial owner involved in the business relationship`,
+ name: "relations.beneficialOwners",
+ },
+ },
+ {
+ type: "textArea",
+ props: {
+ label:
+ i18n.str`Relation of the customer to the controlling persons involved in the business relationship`,
+ name: "relations.controllingPersons",
+ },
+ },
+ {
+ type: "textArea",
+ props: {
+ label:
+ i18n.str`Relation of the customer to the authorized signatories involved in the business relationship`,
+ name: "relations.authorizedSignatories",
+ },
+ },
+ {
+ type: "textArea",
+ props: {
+ label:
+ i18n.str`Relation of the customer to other persons involved in the business relationship`,
+ name: "relations.otherPersons",
+ },
+ },
+ {
+ type: "textArea",
+ props: {
+ label: i18n.str`Relation to other AMLA-Files`,
+ name: "relations.withOtherAmlaFiles",
+ },
+ },
+ {
+ type: "textArea",
+ props: {
+ label: i18n.str`Introducer / agents / references`,
+ name: "relations.references",
+ },
+ },
+ ],
+ },
+ {
+ title: i18n.str`Further information`,
+ fields: [
+ {
+ type: "textArea",
+ props: {
+ label: i18n.str`Other relevant information`,
+ name: "furtherInformation",
+ },
+ },
+ ],
+ },
+ resolutionSection(current, i18n),
+ ],
+ behavior: function formBehavior(
+ v: Partial<Form902_5.Form>,
+ ): FormState<Form902_5.Form> {
+ return {
+ originOfAssets: {
+ categoryOther: {
+ hidden: v.originOfAssets?.category !== "other",
+ },
+ },
+ };
+ },
+});
+
+namespace Form902_5 {
+ export interface Form extends BaseForm {
+ customer: string;
+ fullName: string;
+ businessActivity: string;
+ financial: string;
+ originOfAssets: {
+ nature: string;
+ currency: string;
+ amount: number;
+ category: "savings" | "own-business" | "inheritance" | "other";
+ categoryOther: string;
+ details: string;
+ };
+ nature: {
+ purpose: string;
+ plan: string;
+ cashOrMoneyTransfer: string;
+ };
+ relations: {
+ beneficialOwners: string;
+ controllingPersons: string;
+ authorizedSignatories: string;
+ otherPersons: string;
+ withOtherAmlaFiles: string;
+ references: string;
+ };
+ furtherInformation: string;
+ }
+}
diff --git a/packages/aml-backoffice-ui/src/forms/902_9e.ts b/packages/aml-backoffice-ui/src/forms/902_9e.ts
new file mode 100644
index 000000000..e0e7a6d65
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/forms/902_9e.ts
@@ -0,0 +1,119 @@
+import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
+import { FlexibleForm, FormState, InternationalizationAPI } from "@gnu-taler/web-util/browser";
+import { resolutionSection } from "./simplest.js";
+import { BaseForm } from "./declaration.js";
+
+export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): FlexibleForm<Form902_9.Form> => ({
+ design: [
+ {
+ title:
+ i18n.str`Declaration of identity of the beneficial owner`,
+ fields: [
+ {
+ type: "textArea",
+ props: {
+ name: "contractingPartner",
+ label: i18n.str`Contracting partner`,
+ },
+ },
+ {
+ type: "caption",
+ props: {
+ label:
+ i18n.str`The contracting partner hereby declares that the person(s) listed below is/are the beneficial owner(s) of the assets involved in the business relationship. If the contracting partner is also the sole beneficial owner of the assets, the contracting partner's detail must be set out below`,
+ },
+ },
+ {
+ type: "array",
+ props: {
+ label: i18n.str`Persons`,
+ labelField: "surname",
+ name: "persons",
+ fields: [
+ {
+ type: "text",
+ props: {
+ name: "surname",
+ label: i18n.str`Surname(s)`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "firstName",
+ label: i18n.str`First name(s)`,
+ },
+ },
+ {
+ type: "absoluteTime",
+ props: {
+ name: "dateOfBirth",
+ label: i18n.str`Date of birth`,
+ pattern: "dd/MM/yyyy",
+ // help: i18n.str`format 'dd/MM/yyyy'`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "nationality",
+ label: i18n.str`Nationality`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "address",
+ label: i18n.str`Actual address of domicile`,
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "caption",
+ props: {
+ label:
+ i18n.str`The contracting partner hereby undertakes to inform automatically of any changes to the information contained herein`,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "signature",
+ label: i18n.str`Signature`,
+ },
+ },
+ {
+ type: "caption",
+ props: {
+ label:
+ i18n.str`It is a criminal offense to deliberately provide false information on this form (article 251 of the Swiss Criminal Code, document forgery)`,
+ },
+ },
+ ],
+ },
+ resolutionSection(current, i18n),
+ ],
+ behavior: function formBehavior(
+ v: Partial<Form902_9.Form>,
+ ): FormState<Form902_9.Form> {
+ return {
+ };
+ },
+});
+
+namespace Form902_9 {
+ interface Person {
+ surname: string;
+ firstName: string;
+ dateOfBirth: AbsoluteTime;
+ nationality: string;
+ address: string;
+ }
+ export interface Form extends BaseForm {
+ contractingPartner: string;
+ persons: Person;
+ signature: string;
+ }
+}
diff --git a/packages/aml-backoffice-ui/src/forms/declaration.ts b/packages/aml-backoffice-ui/src/forms/declaration.ts
new file mode 100644
index 000000000..ec3bc5189
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/forms/declaration.ts
@@ -0,0 +1,70 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import type { AmountJson, TranslatedString } from "@gnu-taler/taler-util";
+import type { FlexibleForm, InternationalizationAPI } from "@gnu-taler/web-util/browser";
+import { AmlExchangeBackend } from "../utils/types.js";
+
+/**
+ * import entry point without hard reference.
+ *
+ * This file just export types and UI Forms
+ * based on what `globalThis` contains.
+ *
+ * `./index.js` must be imported first before
+ * so `globaThis` will have the correct value.
+ */
+
+export interface BaseForm {
+ state: AmlExchangeBackend.AmlState;
+ threshold: AmountJson;
+}
+
+export type FormMetadata<T extends BaseForm> = {
+ label: TranslatedString,
+ id: string,
+ version: number,
+ impl: (current: T) => FlexibleForm<T>
+}
+
+interface LabelValue {
+ label: TranslatedString;
+ value: string,
+}
+
+export interface UiForms {
+ currencies: (i18n: InternationalizationAPI) => LabelValue[],
+ languages: (i18n: InternationalizationAPI) => LabelValue[],
+ forms: (i18n: InternationalizationAPI) => Array<FormMetadata<BaseForm>>,
+}
+
+/**
+ * Global settings for the UI.
+ */
+const defaultUIForms: UiForms = {
+ currencies: () => [],
+ languages: () => [],
+ forms: () => [],
+};
+
+declare global {
+ var amlExchangeBackoffice: UiForms;
+}
+
+export const uiForms: UiForms =
+ "amlExchangeBackoffice" in globalThis
+ ? (globalThis as any).amlExchangeBackoffice
+ : defaultUIForms;
diff --git a/packages/aml-backoffice-ui/src/forms/icons.tsx b/packages/aml-backoffice-ui/src/forms/icons.tsx
new file mode 100644
index 000000000..392790c9c
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/forms/icons.tsx
@@ -0,0 +1,10 @@
+import { h } from "preact";
+
+export const ChevronRightIcon = () => <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
+</svg>
+
+
+export const ArrowRightIcon = () => <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" />
+</svg>
diff --git a/packages/aml-backoffice-ui/src/forms/index.ts b/packages/aml-backoffice-ui/src/forms/index.ts
new file mode 100644
index 000000000..f41122bc7
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/forms/index.ts
@@ -0,0 +1,202 @@
+import type { InternationalizationAPI } from "@gnu-taler/web-util/browser";
+import { v1 as form_902_11e_v1 } from "./902_11e.js";
+import { v1 as form_902_12e_v1 } from "./902_12e.js";
+import { v1 as form_902_13e_v1 } from "./902_13e.js";
+import { v1 as form_902_15e_v1 } from "./902_15e.js";
+import { v1 as form_902_1e_v1 } from "./902_1e.js";
+import { v1 as form_902_4e_v1 } from "./902_4e.js";
+import { v1 as form_902_5e_v1 } from "./902_5e.js";
+import { v1 as form_902_9e_v1 } from "./902_9e.js";
+import { v1 as simplest } from "./simplest.js";
+import { BaseForm, FormMetadata } from "./declaration.js";
+
+const languages = (i18n: InternationalizationAPI) => [
+ {
+ label: i18n.str`Mandarin Chinese`,
+ value: "cmn",
+ },
+ {
+ label: i18n.str`Spanish`,
+ value: "spa",
+ },
+ {
+ label: i18n.str`English`,
+ value: "eng",
+ },
+ {
+ label: i18n.str`Hindi`,
+ value: "hin",
+ },
+ {
+ label: i18n.str`Portuguese`,
+ value: "por",
+ },
+ {
+ label: i18n.str`Bengali`,
+ value: "ben",
+ },
+ {
+ label: i18n.str`Russian`,
+ value: "rus",
+ },
+ {
+ label: i18n.str`Japanese`,
+ value: "jpn",
+ },
+ {
+ label: i18n.str`Yue`,
+ value: "yue",
+ },
+ {
+ label: i18n.str`Vietnamese`,
+ value: "vie",
+ },
+ {
+ label: i18n.str`Turkish`,
+ value: "tur",
+ },
+ {
+ label: i18n.str`Wu`,
+ value: "wuu",
+ },
+ {
+ label: i18n.str`Marathi`,
+ value: "mar",
+ },
+ {
+ label: i18n.str`Telugu`,
+ value: "ten",
+ },
+ {
+ label: i18n.str`Korean`,
+ value: "kor",
+ },
+ {
+ label: i18n.str`French`,
+ value: "fra",
+ },
+ {
+ label: i18n.str`Tamil`,
+ value: "tam",
+ },
+ {
+ label: i18n.str`Egyptian Arabic`,
+ value: "arz",
+ },
+ {
+ label: i18n.str`Standard German`,
+ value: "deu",
+ },
+ {
+ label: i18n.str`Urdu`,
+ value: "urd",
+ },
+ {
+ label: i18n.str`Javanese`,
+ value: "jav",
+ },
+ {
+ label: i18n.str`Punjabi`,
+ value: "pan",
+ },
+ {
+ label: i18n.str`Italian`,
+ value: "ita",
+ },
+ {
+ label: i18n.str`Gujarati`,
+ value: "guj",
+ },
+ {
+ label: i18n.str`Iranian Persian`,
+ value: "pes",
+ },
+ {
+ label: i18n.str`Bhojpuri`,
+ value: "bho",
+ },
+ {
+ label: i18n.str`Hausa`,
+ value: "hau",
+ },
+];
+
+
+const forms: (i18n: InternationalizationAPI) => Array<FormMetadata<BaseForm>> = (i18n) => [
+ {
+ label: i18n.str`Simple comment`,
+ id: "simple_comment",
+ version: 1,
+ impl: simplest(i18n),
+ }, {
+ label: i18n.str`Identification form`,
+ id: "902.1e",
+ version: 1,
+ impl: form_902_1e_v1(i18n),
+ }, {
+ label: i18n.str`Operational legal entity or partnership`,
+ id: "902.11e",
+ version: 1,
+ impl: form_902_11e_v1(i18n),
+ }, {
+ label: i18n.str`Foundations`,
+ id: "902.12e",
+ version: 1,
+ impl: form_902_12e_v1(i18n),
+ }, {
+ label: i18n.str`Declaration for trusts`,
+ id: "902.13e",
+ version: 1,
+ impl: form_902_13e_v1(i18n),
+ }, {
+ label: i18n.str`Information on life insurance policies`,
+ id: "902.15e",
+ version: 1,
+ impl: form_902_15e_v1(i18n),
+ }, {
+ label: i18n.str`Declaration of beneficial owner`,
+ id: "902.9e",
+ version: 1,
+ impl: form_902_9e_v1(i18n),
+ }, {
+ label: i18n.str`Customer profile`,
+ id: "902.5e",
+ version: 1,
+ impl: form_902_5e_v1(i18n),
+ }, {
+ label: i18n.str`Risk profile`,
+ id: "902.4e",
+ version: 1,
+ impl: form_902_4e_v1(i18n),
+ },
+];
+
+
+const currencies = (i18n: InternationalizationAPI) => [
+ {
+ label: i18n.str`United States dollar`,
+ value: "usd",
+ },
+ {
+ label: i18n.str`Euro`,
+ value: "eur",
+ },
+ {
+ label: i18n.str`Swiss franc`,
+ value: "chf",
+ },
+ {
+ label: i18n.str`Argentine peso`,
+ value: "ars",
+ },
+ {
+ label: i18n.str`Mexican peso`,
+ value: "mxn",
+ },
+ {
+ label: i18n.str`Brazilian real`,
+ value: "brl",
+ },
+];
+
+globalThis.amlExchangeBackoffice = { currencies, languages, forms }
diff --git a/packages/aml-backoffice-ui/src/forms/simplest.ts b/packages/aml-backoffice-ui/src/forms/simplest.ts
new file mode 100644
index 000000000..735ca9bfc
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/forms/simplest.ts
@@ -0,0 +1,85 @@
+import type {
+ TranslatedString
+} from "@gnu-taler/taler-util";
+
+import type { DoubleColumnFormSection, FlexibleForm, FormState, InternationalizationAPI } from "@gnu-taler/web-util/browser";
+import { BaseForm } from "./declaration.js";
+import { amlStateConverter } from "../utils/converter.js";
+import { AmlExchangeBackend } from "../utils/types.js";
+
+export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): FlexibleForm<Simplest.Form> => ({
+ design: [
+ {
+ title: i18n.str`Simple form`,
+ fields: [
+ {
+ type: "textArea",
+ props: {
+ name: "comment",
+ label: i18n.str`Comments`,
+ },
+ },
+ ],
+ },
+ resolutionSection(current, i18n),
+ ],
+ behavior: function formBehavior(
+ v: Partial<Simplest.Form>,
+ ): FormState<Simplest.Form> {
+ return {
+ comment: {
+ help: ((v.comment?.length ?? 0) > 100 ? "keep it short" : "") as TranslatedString,
+
+ },
+ threshold: {
+ disabled: v.state === AmlExchangeBackend.AmlState.frozen,
+ },
+ };
+ },
+});
+
+export namespace Simplest {
+ export interface Form extends BaseForm {
+ comment: string;
+ }
+}
+
+export function resolutionSection(current: BaseForm, i18n: InternationalizationAPI): DoubleColumnFormSection {
+ return {
+ title: i18n.str`Resolution`,
+ description: `Current state is ${amlStateConverter.toStringUI(
+ current.state,
+ )} and threshold at ` as TranslatedString,
+ fields: [
+ {
+ type: "choiceHorizontal",
+ props: {
+ name: "state",
+ label: i18n.str`New state`,
+ converter: amlStateConverter,
+ choices: [
+ {
+ value: AmlExchangeBackend.AmlState.frozen,
+ label: i18n.str`Frozen`,
+ },
+ {
+ value: AmlExchangeBackend.AmlState.pending,
+ label: i18n.str`Pending`,
+ },
+ {
+ value: AmlExchangeBackend.AmlState.normal,
+ label: i18n.str`Normal`,
+ },
+ ],
+ },
+ },
+ {
+ type: "amount",
+ props: {
+ name: "threshold",
+ label: i18n.str`New threshold`,
+ },
+ },
+ ],
+ };
+}
diff --git a/packages/aml-backoffice-ui/src/hooks/useBackend.ts b/packages/aml-backoffice-ui/src/hooks/useBackend.ts
new file mode 100644
index 000000000..7b55568c8
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/hooks/useBackend.ts
@@ -0,0 +1,48 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+ import { canonicalizeBaseUrl } from "@gnu-taler/taler-util";
+import { uiSettings } from "../settings.js";
+
+
+export function getInitialBackendBaseURL(): string {
+ const overrideUrl =
+ typeof localStorage !== "undefined"
+ ? localStorage.getItem("exchange-base-url")
+ : undefined;
+
+ let result: string;
+
+ if (!overrideUrl) {
+ //normal path
+ if (!uiSettings.backendBaseURL) {
+ console.error(
+ "ERROR: backendBaseURL was overridden by a setting file and missing. Setting value to 'window.origin'",
+ );
+ result = typeof (window as any) !== "undefined" ? window.origin : "localhost"
+ } else {
+ result = uiSettings.backendBaseURL;
+ }
+ } else {
+ // testing/development path
+ result = overrideUrl
+ }
+ try {
+ return canonicalizeBaseUrl(result)
+ } catch (e) {
+ //fall back
+ return canonicalizeBaseUrl(window.origin)
+ }
+}
diff --git a/packages/aml-backoffice-ui/src/hooks/useCaseDetails.ts b/packages/aml-backoffice-ui/src/hooks/useCaseDetails.ts
new file mode 100644
index 000000000..dbc6763ba
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/hooks/useCaseDetails.ts
@@ -0,0 +1,95 @@
+
+// FIX default import https://github.com/microsoft/TypeScript/issues/49189
+import { AmountString, OfficerAccount, PaytoString, TalerExchangeApi, TalerExchangeResultByMethod, TalerHttpError } from "@gnu-taler/taler-util";
+import _useSWR, { SWRHook } from "swr";
+import { useExchangeApiContext } from "../context/config.js";
+import { useOfficer } from "./useOfficer.js";
+const useSWR = _useSWR as unknown as SWRHook;
+
+export function useCaseDetails(paytoHash: string) {
+ const officer = useOfficer();
+ const session = officer.state === "ready" ? officer.account : undefined;
+
+ const { api } = useExchangeApiContext();
+
+ async function fetcher([officer, account]: [OfficerAccount, PaytoString]) {
+ return await api.getDecisionDetails(officer, account)
+ }
+
+ const { data, error } = useSWR<TalerExchangeResultByMethod<"getDecisionDetails">, TalerHttpError>(
+ !session ? undefined : [session, paytoHash], fetcher, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ errorRetryCount: 0,
+ errorRetryInterval: 1,
+ shouldRetryOnError: false,
+ keepPreviousData: true,
+ });
+
+ if (data) return data;
+ if (error) return error;
+ return undefined;
+}
+
+const example1: TalerExchangeApi.AmlDecisionDetails = {
+ aml_history: [
+ {
+ justification: "Lack of documentation",
+ decider_pub: "ASDASDASD",
+ decision_time: {
+ t_s: Date.now() / 1000,
+ },
+ new_state: 2,
+ new_threshold: "USD:0" as AmountString,
+ },
+ {
+ justification: "Doing a transfer of high amount",
+ decider_pub: "ASDASDASD",
+ decision_time: {
+ t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 6,
+ },
+ new_state: 1,
+ new_threshold: "USD:2000" as AmountString,
+ },
+ {
+ justification: "Account is known to the system",
+ decider_pub: "ASDASDASD",
+ decision_time: {
+ t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 9,
+ },
+ new_state: 0,
+ new_threshold: "USD:100" as AmountString,
+ },
+ ],
+ kyc_attributes: [
+ {
+ collection_time: {
+ t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 8,
+ },
+ expiration_time: {
+ t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 4,
+ },
+ provider_section: "asdasd",
+ attributes: {
+ name: "Sebastian",
+ },
+ },
+ {
+ collection_time: {
+ t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 5,
+ },
+ expiration_time: {
+ t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 2,
+ },
+ provider_section: "asdasd",
+ attributes: {
+ creditCard: "12312312312",
+ },
+ },
+ ],
+};
+
+
diff --git a/packages/aml-backoffice-ui/src/hooks/useCases.ts b/packages/aml-backoffice-ui/src/hooks/useCases.ts
new file mode 100644
index 000000000..2bc9b5f0f
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/hooks/useCases.ts
@@ -0,0 +1,86 @@
+import { useState } from "preact/hooks";
+
+// FIX default import https://github.com/microsoft/TypeScript/issues/49189
+import { OfficerAccount, OfficerId, OperationOk, TalerExchangeResultByMethod, TalerHttpError, decodeCrock, encodeCrock } from "@gnu-taler/taler-util";
+import _useSWR, { SWRHook } from "swr";
+import { useExchangeApiContext } from "../context/config.js";
+import { AmlExchangeBackend } from "../utils/types.js";
+import { useOfficer } from "./useOfficer.js";
+const useSWR = _useSWR as unknown as SWRHook;
+
+export const PAGINATED_LIST_SIZE = 10;
+// when doing paginated request, ask for one more
+// and use it to know if there are more to request
+export const PAGINATED_LIST_REQUEST = PAGINATED_LIST_SIZE + 1;
+
+/**
+ * FIXME: mutate result when balance change (transaction )
+ * @param account
+ * @param args
+ * @returns
+ */
+export function useCases(state: AmlExchangeBackend.AmlState) {
+ const officer = useOfficer();
+ const session = officer.state === "ready" ? officer.account : undefined;
+ const { api } = useExchangeApiContext();
+
+ const [offset, setOffset] = useState<string>();
+
+ async function fetcher([officer, state, offset]: [OfficerAccount, AmlExchangeBackend.AmlState, string | undefined]) {
+ return await api.getDecisionsByState(officer, state, {
+ order: "asc", offset, limit: PAGINATED_LIST_REQUEST
+ })
+ }
+
+ const { data, error } = useSWR<TalerExchangeResultByMethod<"getDecisionsByState">, TalerHttpError>(
+ !session ? undefined : [session, state, offset, "getDecisionsByState"],
+ fetcher,
+ );
+
+ if (error) return error;
+ if (data === undefined) return undefined;
+ if (data.type !== "ok") return data;
+
+ return buildPaginatedResult(data.body.records, offset, setOffset, (d) => String(d.rowid));
+}
+
+type PaginatedResult<T> = OperationOk<T> & {
+ isLastPage: boolean;
+ isFirstPage: boolean;
+ loadNext(): void;
+ loadFirst(): void;
+}
+
+//TODO: consider sending this to web-util
+export function buildPaginatedResult<R, OffId>(data: R[], offset: OffId | undefined, setOffset: (o: OffId | undefined) => void, getId: (r: R) => OffId): PaginatedResult<R[]> {
+
+ const isLastPage = data.length < PAGINATED_LIST_REQUEST;
+ const isFirstPage = offset === undefined;
+
+ const result = structuredClone(data);
+ if (result.length == PAGINATED_LIST_REQUEST) {
+ result.pop();
+ }
+ return {
+ type: "ok",
+ body: result,
+ isLastPage,
+ isFirstPage,
+ loadNext: () => {
+ if (!result.length) return;
+ const id = getId(result[result.length - 1])
+ setOffset(id);
+ },
+ loadFirst: () => {
+ setOffset(undefined);
+ },
+ };
+}
+
+
+function removeLastElement<T>(list: Array<T>): Array<T> {
+ if (list.length === 0) {
+ return list;
+ }
+ return list.slice(0, -1)
+} \ No newline at end of file
diff --git a/packages/aml-backoffice-ui/src/hooks/useOfficer.ts b/packages/aml-backoffice-ui/src/hooks/useOfficer.ts
new file mode 100644
index 000000000..1bf2b308b
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/hooks/useOfficer.ts
@@ -0,0 +1,135 @@
+import {
+ AbsoluteTime,
+ Codec,
+ LockedAccount,
+ OfficerAccount,
+ OfficerId,
+ SigningKey,
+ buildCodecForObject,
+ codecForAbsoluteTime,
+ codecForString,
+ createNewOfficerAccount,
+ decodeCrock,
+ encodeCrock,
+ unlockOfficerAccount
+} from "@gnu-taler/taler-util";
+import {
+ buildStorageKey,
+ useLocalStorage
+} from "@gnu-taler/web-util/browser";
+import { useMemo } from "preact/hooks";
+import { useMaybeExchangeApiContext } from "../context/config.js";
+
+export interface Officer {
+ account: LockedAccount;
+ when: AbsoluteTime;
+}
+
+const codecForLockedAccount = codecForString() as Codec<LockedAccount>;
+
+type OfficerAccountString = {
+ id: string,
+ strKey: string;
+}
+
+export const codecForOfficerAccount = (): Codec<OfficerAccountString> =>
+ buildCodecForObject<OfficerAccountString>()
+ .property("id", codecForString()) // FIXME
+ .property("strKey", codecForString()) // FIXME
+ .build("OfficerAccount");
+
+export const codecForOfficer = (): Codec<Officer> =>
+ buildCodecForObject<Officer>()
+ .property("account", codecForLockedAccount) // FIXME
+ .property("when", codecForAbsoluteTime) // FIXME
+ .build("Officer");
+
+export type OfficerState = OfficerNotReady | OfficerReady;
+export type OfficerNotReady = OfficerNotFound | OfficerLocked;
+interface OfficerNotFound {
+ state: "not-found";
+ create: (password: string) => Promise<void>;
+}
+interface OfficerLocked {
+ state: "locked";
+ forget: () => void;
+ tryUnlock: (password: string) => Promise<void>;
+}
+interface OfficerReady {
+ state: "ready";
+ account: OfficerAccount;
+ forget: () => void;
+ lock: () => void;
+}
+
+const OFFICER_KEY = buildStorageKey("officer", codecForOfficer());
+const DEV_ACCOUNT_KEY = buildStorageKey("account-dev", codecForOfficerAccount());
+
+export function useOfficer(): OfficerState {
+ const exchangeContext = useMaybeExchangeApiContext();
+ // dev account, is save when reloaded.
+ const accountStorage = useLocalStorage(DEV_ACCOUNT_KEY);
+ const account = useMemo(() => {
+ if (!accountStorage.value) return undefined
+
+ return {
+ id: accountStorage.value.id as OfficerId,
+ signingKey: decodeCrock(accountStorage.value.strKey) as SigningKey
+ }
+ }, [accountStorage.value?.id, accountStorage.value?.strKey])
+
+ const officerStorage = useLocalStorage(OFFICER_KEY);
+ const officer = useMemo(() => {
+ if (!officerStorage.value) return undefined
+ return officerStorage.value
+ }, [officerStorage.value?.account, officerStorage.value?.when.t_ms])
+
+ if (officer === undefined) {
+ return {
+ state: "not-found",
+ create: async (pwd: string) => {
+ if (!exchangeContext) return;
+ const req = await fetch(new URL("seed", exchangeContext.api.baseUrl).href)
+ const b = await req.blob()
+ const ar = await b.arrayBuffer()
+ const uintar = new Uint8Array(ar)
+
+ const { id, safe, signingKey } = await createNewOfficerAccount(pwd, uintar);
+ officerStorage.update({
+ account: safe,
+ when: AbsoluteTime.now(),
+ });
+
+ // accountStorage.update({ id, signingKey });
+ const strKey = encodeCrock(signingKey)
+ accountStorage.update({ id, strKey })
+ },
+ };
+ }
+
+ if (account === undefined) {
+ return {
+ state: "locked",
+ forget: () => {
+ officerStorage.reset();
+ },
+ tryUnlock: async (pwd: string) => {
+ const ac = await unlockOfficerAccount(officer.account, pwd);
+ // accountStorage.update(ac);
+ accountStorage.update({ id: ac.id, strKey: encodeCrock(ac.signingKey) })
+ },
+ };
+ }
+
+ return {
+ state: "ready",
+ account,
+ lock: () => {
+ accountStorage.reset();
+ },
+ forget: () => {
+ officerStorage.reset();
+ accountStorage.reset();
+ },
+ };
+}
diff --git a/packages/aml-backoffice-ui/src/hooks/useSettings.ts b/packages/aml-backoffice-ui/src/hooks/useSettings.ts
new file mode 100644
index 000000000..f1610576e
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/hooks/useSettings.ts
@@ -0,0 +1,74 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ Codec,
+ TranslatedString,
+ buildCodecForObject,
+ codecForBoolean,
+ codecForNumber,
+ codecForString,
+ codecOptional
+} from "@gnu-taler/taler-util";
+import { buildStorageKey, useLocalStorage, useTranslationContext } from "@gnu-taler/web-util/browser";
+
+interface Settings {
+ allowInsecurePassword: boolean;
+ keepSessionAfterReload: boolean;
+}
+
+export function getAllBooleanSettings(): Array<keyof Settings> {
+ return ["allowInsecurePassword", "keepSessionAfterReload"]
+}
+
+export function getLabelForSetting(k: keyof Settings, i18n: ReturnType<typeof useTranslationContext>["i18n"]): TranslatedString {
+ switch (k) {
+ case "allowInsecurePassword": return i18n.str`Allow Insecure password`
+ case "keepSessionAfterReload": return i18n.str`Keep session after reload`
+ }
+}
+
+export const codecForSettings = (): Codec<Settings> =>
+ buildCodecForObject<Settings>()
+ .property("allowInsecurePassword", (codecForBoolean()))
+ .property("keepSessionAfterReload", (codecForBoolean()))
+ .build("Settings");
+
+const defaultSettings: Settings = {
+ allowInsecurePassword: false,
+ keepSessionAfterReload: false,
+};
+
+const EXCHANGE_SETTINGS_KEY = buildStorageKey(
+ "exchange-settings",
+ codecForSettings(),
+);
+
+export function useSettings(): [
+ Readonly<Settings>,
+ <T extends keyof Settings>(key: T, value: Settings[T]) => void,
+] {
+ const { value, update } = useLocalStorage(
+ EXCHANGE_SETTINGS_KEY,
+ defaultSettings,
+ );
+
+ function updateField<T extends keyof Settings>(k: T, v: Settings[T]) {
+ const newValue = { ...value, [k]: v };
+ update(newValue);
+ }
+ return [value, updateField];
+}
diff --git a/packages/aml-backoffice-ui/src/i18n/bank.pot b/packages/aml-backoffice-ui/src/i18n/bank.pot
new file mode 100644
index 000000000..66e98976f
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/i18n/bank.pot
@@ -0,0 +1,486 @@
+# This file is part of GNU Taler
+# (C) 2022 Taler Systems S.A.
+#
+# GNU Taler is free software; you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 3, or (at your option) any later version.
+#
+# GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Bank\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+#: src/pages/home/BankFrame.tsx:55
+#, c-format
+msgid "Logout"
+msgstr ""
+
+#: src/pages/home/BankFrame.tsx:73
+#, c-format
+msgid "Skip to main content"
+msgstr ""
+
+#: src/pages/home/BankFrame.tsx:82
+#, c-format
+msgid ""
+"This part of the demo shows how a bank that supports Taler directly would work. "
+"In addition to using your own bank account, you can also see the transaction "
+"history of some %1$s."
+msgstr ""
+
+#: src/pages/home/BankFrame.tsx:94
+#, c-format
+msgid "Taler logo"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:41
+#, c-format
+msgid "Missing username"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:42
+#, c-format
+msgid "Missing password"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:49
+#, c-format
+msgid "Please login!"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:51
+#, c-format
+msgid "Username:"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:71
+#, c-format
+msgid "Password:"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:100
+#, c-format
+msgid "Login"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:110
+#, c-format
+msgid "Register"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:60
+#, c-format
+msgid "Missing IBAN"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:62
+#, c-format
+msgid "IBAN should have just uppercased letters and numbers"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:64
+#, c-format
+msgid "Missing subject"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:66
+#, c-format
+msgid "Missing amount"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:68
+#, c-format
+msgid "Amount is not valid"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:70
+#, c-format
+msgid "Should be greater than 0"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:79
+#, c-format
+msgid "Receiver IBAN:"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:102
+#, c-format
+msgid "Transfer subject:"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:123
+#, c-format
+msgid "Amount:"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:177
+#, c-format
+msgid "Field(s) missing."
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:227
+#, c-format
+msgid "Want to try the raw payto://-format?"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:235
+#, c-format
+msgid "Missing payto address"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:237
+#, c-format
+msgid "Payto does not follow the pattern"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:243
+#, c-format
+msgid "Transfer money to account identified by payto:// URI:"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:246
+#, c-format
+msgid "payto URI:"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:255
+#, c-format
+msgid "payto address"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:279
+#, c-format
+msgid "Send"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:314
+#, c-format
+msgid "Use wire-transfer form?"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:373
+#, c-format
+msgid "No credentials found."
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:397
+#, c-format
+msgid "Could not create the wire transfer"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:414
+#, c-format
+msgid "Transfer creation gave response error"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:426
+#, c-format
+msgid "Wire transfer created!"
+msgstr ""
+
+#: src/pages/home/WalletWithdrawForm.tsx:50
+#, c-format
+msgid "Amount to withdraw:"
+msgstr ""
+
+#: src/pages/home/WalletWithdrawForm.tsx:84
+#, c-format
+msgid "Withdraw"
+msgstr ""
+
+#: src/pages/home/WalletWithdrawForm.tsx:128
+#, c-format
+msgid "No credentials given."
+msgstr ""
+
+#: src/pages/home/WalletWithdrawForm.tsx:155
+#, c-format
+msgid "Could not create withdrawal operation"
+msgstr ""
+
+#: src/pages/home/WalletWithdrawForm.tsx:171
+#, c-format
+msgid "Withdrawal creation gave response error"
+msgstr ""
+
+#: src/pages/home/PaymentOptions.tsx:44
+#, c-format
+msgid "Obtain digital cash"
+msgstr ""
+
+#: src/pages/home/PaymentOptions.tsx:52
+#, c-format
+msgid "Transfer to bank account"
+msgstr ""
+
+#: src/pages/home/Transactions.tsx:69
+#, c-format
+msgid "Date"
+msgstr ""
+
+#: src/pages/home/Transactions.tsx:70
+#, c-format
+msgid "Amount"
+msgstr ""
+
+#: src/pages/home/Transactions.tsx:71
+#, c-format
+msgid "Counterpart"
+msgstr ""
+
+#: src/pages/home/Transactions.tsx:72
+#, c-format
+msgid "Subject"
+msgstr ""
+
+#: src/pages/home/QrCodeSection.tsx:41
+#, c-format
+msgid "Transfer to Taler Wallet"
+msgstr ""
+
+#: src/pages/home/QrCodeSection.tsx:44
+#, c-format
+msgid "Use this QR code to withdraw to your mobile wallet:"
+msgstr ""
+
+#: src/pages/home/QrCodeSection.tsx:47
+#, c-format
+msgid "Click %1$s to open your Taler wallet!"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:47
+#, c-format
+msgid "Confirm Withdrawal"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:52
+#, c-format
+msgid "Authorize withdrawal by solving challenge"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:55
+#, c-format
+msgid "What is"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:94
+#, c-format
+msgid "Answer is wrong."
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:99
+#, c-format
+msgid "Confirm"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:113
+#, c-format
+msgid "Cancel"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:120
+#, c-format
+msgid ""
+"A this point, a %1$s bank would ask for an additional authentication proof "
+"(PIN/TAN, one time password, ..), instead of a simple calculation."
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:166
+#, c-format
+msgid "No withdrawal ID found."
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:201
+#, c-format
+msgid "Could not confirm the withdrawal"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:219
+#, c-format
+msgid "Withdrawal confirmation gave response error"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:231
+#, c-format
+msgid "Withdrawal confirmed!"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:294
+#, c-format
+msgid "Could not abort the withdrawal."
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:311
+#, c-format
+msgid "Withdrawal abortion failed."
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:324
+#, c-format
+msgid "Withdrawal aborted!"
+msgstr ""
+
+#: src/pages/home/WithdrawalQRCode.tsx:54
+#, c-format
+msgid "Abort"
+msgstr ""
+
+#: src/pages/home/WithdrawalQRCode.tsx:74
+#, c-format
+msgid "withdrawal (%1$s) was never (correctly) created at the bank..."
+msgstr ""
+
+#: src/pages/home/WithdrawalQRCode.tsx:88
+#, c-format
+msgid "Waiting the bank to create the operation..."
+msgstr ""
+
+#: src/pages/home/WithdrawalQRCode.tsx:102
+#, c-format
+msgid "This withdrawal was aborted!"
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:40
+#, c-format
+msgid "Welcome to %1$s!"
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:133
+#, c-format
+msgid "Username or account label '%1$s' not found. Won't login."
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:159
+#, c-format
+msgid "Wrong credentials given."
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:169
+#, c-format
+msgid "Account information could not be retrieved."
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:210
+#, c-format
+msgid "Welcome, %1$s !"
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:221
+#, c-format
+msgid "Bank account balance"
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:237
+#, c-format
+msgid "Payments"
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:243
+#, c-format
+msgid "Latest transactions:"
+msgstr ""
+
+#: src/pages/home/PublicHistoriesPage.tsx:83
+#, c-format
+msgid "List of public accounts was not found."
+msgstr ""
+
+#: src/pages/home/PublicHistoriesPage.tsx:95
+#, c-format
+msgid "List of public accounts could not be retrieved."
+msgstr ""
+
+#: src/pages/home/PublicHistoriesPage.tsx:143
+#, c-format
+msgid "History of public accounts"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:39
+#, c-format
+msgid "Currently, the bank is not accepting new registrations!"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:68
+#, c-format
+msgid "Use only letter and numbers starting with a lower case letter"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:78
+#, c-format
+msgid "Password don't match"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:89
+#, c-format
+msgid "Please register!"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:126
+#, c-format
+msgid "Repeat Password:"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:226
+#, c-format
+msgid "Registration failed, please report"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:239
+#, c-format
+msgid "That username is already taken"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:248
+#, c-format
+msgid "New registration gave response error"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:53
+#, c-format
+msgid "Bank menu"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:59
+#, c-format
+msgid "Select option1"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:66
+#, c-format
+msgid "Select option2"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:55
+#, c-format
+msgid "days"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:65
+#, c-format
+msgid "hours"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:76
+#, c-format
+msgid "minutes"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:87
+#, c-format
+msgid "seconds"
+msgstr ""
+
diff --git a/packages/aml-backoffice-ui/src/i18n/de.po b/packages/aml-backoffice-ui/src/i18n/de.po
new file mode 100644
index 000000000..dc76f83e2
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/i18n/de.po
@@ -0,0 +1,486 @@
+# 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/>
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Wallet\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"POT-Creation-Date: 2016-11-23 00:00+0100\n"
+"PO-Revision-Date: 2022-12-26 23:30+0000\n"
+"Last-Translator: Stefan Kügel <skuegel@web.de>\n"
+"Language-Team: German <https://weblate.taler.net/projects/gnu-taler/"
+"taler-bank-spa/de/>\n"
+"Language: de\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 4.13.1\n"
+
+#: src/pages/home/BankFrame.tsx:55
+#, c-format
+msgid "Logout"
+msgstr "Abmelden"
+
+#: src/pages/home/BankFrame.tsx:73
+#, c-format
+msgid "Skip to main content"
+msgstr "Navigationsmenü überspringen"
+
+#: src/pages/home/BankFrame.tsx:82
+#, c-format
+msgid ""
+"This part of the demo shows how a bank that supports Taler directly would "
+"work. In addition to using your own bank account, you can also see the "
+"transaction history of some %1$s."
+msgstr ""
+
+#: src/pages/home/BankFrame.tsx:94
+#, c-format
+msgid "Taler logo"
+msgstr "Taler-Logo"
+
+#: src/pages/home/LoginForm.tsx:41
+#, c-format
+msgid "Missing username"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:42
+#, c-format
+msgid "Missing password"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:49
+#, c-format
+msgid "Please login!"
+msgstr "Bitte melden Sie sich an!"
+
+#: src/pages/home/LoginForm.tsx:51
+#, c-format
+msgid "Username:"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:71
+#, c-format
+msgid "Password:"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:100
+#, c-format
+msgid "Login"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:110
+#, c-format
+msgid "Register"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:60
+#, c-format
+msgid "Missing IBAN"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:62
+#, c-format
+msgid "IBAN should have just uppercased letters and numbers"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:64
+#, c-format
+msgid "Missing subject"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:66
+#, c-format
+msgid "Missing amount"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:68
+#, c-format
+msgid "Amount is not valid"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:70
+#, c-format
+msgid "Should be greater than 0"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:79
+#, c-format
+msgid "Receiver IBAN:"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:102
+#, c-format
+msgid "Transfer subject:"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:123
+#, c-format
+msgid "Amount:"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:177
+#, c-format
+msgid "Field(s) missing."
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:227
+#, c-format
+msgid "Want to try the raw payto://-format?"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:235
+#, c-format
+msgid "Missing payto address"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:237
+#, c-format
+msgid "Payto does not follow the pattern"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:243
+#, c-format
+msgid "Transfer money to account identified by payto:// URI:"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:246
+#, c-format
+msgid "payto URI:"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:255
+#, c-format
+msgid "payto address"
+msgstr "payto-Adresse"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:279
+#, c-format
+msgid "Send"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:314
+#, c-format
+msgid "Use wire-transfer form?"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:373
+#, c-format
+msgid "No credentials found."
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:397
+#, c-format
+msgid "Could not create the wire transfer"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:414
+#, c-format
+msgid "Transfer creation gave response error"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:426
+#, c-format
+msgid "Wire transfer created!"
+msgstr ""
+
+#: src/pages/home/WalletWithdrawForm.tsx:50
+#, c-format
+msgid "Amount to withdraw:"
+msgstr ""
+
+#: src/pages/home/WalletWithdrawForm.tsx:84
+#, c-format
+msgid "Withdraw"
+msgstr ""
+
+#: src/pages/home/WalletWithdrawForm.tsx:128
+#, c-format
+msgid "No credentials given."
+msgstr ""
+
+#: src/pages/home/WalletWithdrawForm.tsx:155
+#, c-format
+msgid "Could not create withdrawal operation"
+msgstr ""
+
+#: src/pages/home/WalletWithdrawForm.tsx:171
+#, c-format
+msgid "Withdrawal creation gave response error"
+msgstr ""
+
+#: src/pages/home/PaymentOptions.tsx:44
+#, c-format
+msgid "Obtain digital cash"
+msgstr ""
+
+#: src/pages/home/PaymentOptions.tsx:52
+#, c-format
+msgid "Transfer to bank account"
+msgstr ""
+
+#: src/pages/home/Transactions.tsx:69
+#, c-format
+msgid "Date"
+msgstr "Datum"
+
+#: src/pages/home/Transactions.tsx:70
+#, c-format
+msgid "Amount"
+msgstr "Betrag"
+
+#: src/pages/home/Transactions.tsx:71
+#, c-format
+msgid "Counterpart"
+msgstr "Empfänger"
+
+#: src/pages/home/Transactions.tsx:72
+#, c-format
+msgid "Subject"
+msgstr "Verwendungszweck"
+
+#: src/pages/home/QrCodeSection.tsx:41
+#, c-format
+msgid "Transfer to Taler Wallet"
+msgstr ""
+
+#: src/pages/home/QrCodeSection.tsx:44
+#, c-format
+msgid "Use this QR code to withdraw to your mobile wallet:"
+msgstr ""
+
+#: src/pages/home/QrCodeSection.tsx:47
+#, c-format
+msgid "Click %1$s to open your Taler wallet!"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:47
+#, c-format
+msgid "Confirm Withdrawal"
+msgstr "Abhebung bestätigen"
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:52
+#, c-format
+msgid "Authorize withdrawal by solving challenge"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:55
+#, c-format
+msgid "What is"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:94
+#, c-format
+msgid "Answer is wrong."
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:99
+#, c-format
+msgid "Confirm"
+msgstr "Bestätigen"
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:113
+#, c-format
+msgid "Cancel"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:120
+#, c-format
+msgid ""
+"A this point, a %1$s bank would ask for an additional authentication proof "
+"(PIN/TAN, one time password, ..), instead of a simple calculation."
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:166
+#, c-format
+msgid "No withdrawal ID found."
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:201
+#, c-format
+msgid "Could not confirm the withdrawal"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:219
+#, c-format
+msgid "Withdrawal confirmation gave response error"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:231
+#, c-format
+msgid "Withdrawal confirmed!"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:294
+#, c-format
+msgid "Could not abort the withdrawal."
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:311
+#, c-format
+msgid "Withdrawal abortion failed."
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:324
+#, c-format
+msgid "Withdrawal aborted!"
+msgstr ""
+
+#: src/pages/home/WithdrawalQRCode.tsx:54
+#, c-format
+msgid "Abort"
+msgstr ""
+
+#: src/pages/home/WithdrawalQRCode.tsx:74
+#, c-format
+msgid "withdrawal (%1$s) was never (correctly) created at the bank..."
+msgstr ""
+
+#: src/pages/home/WithdrawalQRCode.tsx:88
+#, c-format
+msgid "Waiting the bank to create the operation..."
+msgstr ""
+
+#: src/pages/home/WithdrawalQRCode.tsx:102
+#, c-format
+msgid "This withdrawal was aborted!"
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:40
+#, c-format
+msgid "Welcome to %1$s!"
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:133
+#, c-format
+msgid "Username or account label '%1$s' not found. Won't login."
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:159
+#, c-format
+msgid "Wrong credentials given."
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:169
+#, c-format
+msgid "Account information could not be retrieved."
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:210
+#, c-format
+msgid "Welcome, %1$s !"
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:221
+#, c-format
+msgid "Bank account balance"
+msgstr "Kontostand"
+
+#: src/pages/home/AccountPage.tsx:237
+#, c-format
+msgid "Payments"
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:243
+#, c-format
+msgid "Latest transactions:"
+msgstr ""
+
+#: src/pages/home/PublicHistoriesPage.tsx:83
+#, c-format
+msgid "List of public accounts was not found."
+msgstr ""
+
+#: src/pages/home/PublicHistoriesPage.tsx:95
+#, c-format
+msgid "List of public accounts could not be retrieved."
+msgstr ""
+
+#: src/pages/home/PublicHistoriesPage.tsx:143
+#, c-format
+msgid "History of public accounts"
+msgstr "Buchungen auf öffentlich sichtbaren Konten"
+
+#: src/pages/home/RegistrationPage.tsx:39
+#, c-format
+msgid "Currently, the bank is not accepting new registrations!"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:68
+#, c-format
+msgid "Use only letter and numbers starting with a lower case letter"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:78
+#, c-format
+msgid "Password don't match"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:89
+#, c-format
+msgid "Please register!"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:126
+#, c-format
+msgid "Repeat Password:"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:226
+#, c-format
+msgid "Registration failed, please report"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:239
+#, c-format
+msgid "That username is already taken"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:248
+#, c-format
+msgid "New registration gave response error"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:53
+#, c-format
+msgid "Bank menu"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:59
+#, c-format
+msgid "Select option1"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:66
+#, c-format
+msgid "Select option2"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:55
+#, c-format
+msgid "days"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:65
+#, c-format
+msgid "hours"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:76
+#, c-format
+msgid "minutes"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:87
+#, c-format
+msgid "seconds"
+msgstr ""
diff --git a/packages/aml-backoffice-ui/src/i18n/en.po b/packages/aml-backoffice-ui/src/i18n/en.po
new file mode 100644
index 000000000..83778f785
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/i18n/en.po
@@ -0,0 +1,511 @@
+# 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/>
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Wallet\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"POT-Creation-Date: 2016-11-23 00:00+0100\n"
+"PO-Revision-Date: 2022-01-08 09:57+0100\n"
+"Last-Translator: <translate@taler.net>\n"
+"Language-Team: English\n"
+"Language: en\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: src/pages/home/BankFrame.tsx:55
+#, c-format
+msgid "Logout"
+msgstr ""
+
+#: src/pages/home/BankFrame.tsx:73
+#, c-format
+msgid "Skip to main content"
+msgstr ""
+
+#: src/pages/home/BankFrame.tsx:82
+#, c-format
+msgid ""
+"This part of the demo shows how a bank that supports Taler directly would "
+"work. In addition to using your own bank account, you can also see the "
+"transaction history of some %1$s."
+msgstr ""
+
+#: src/pages/home/BankFrame.tsx:94
+#, c-format
+msgid "Taler logo"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:41
+#, c-format
+msgid "Missing username"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:42
+#, c-format
+msgid "Missing password"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:49
+#, c-format
+msgid "Please login!"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:51
+#, c-format
+msgid "Username:"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:71
+#, c-format
+msgid "Password:"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:100
+#, c-format
+msgid "Login"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:110
+#, c-format
+msgid "Register"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:60
+#, c-format
+msgid "Missing IBAN"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:62
+#, c-format
+msgid "IBAN should have just uppercased letters and numbers"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:64
+#, c-format
+msgid "Missing subject"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:66
+#, c-format
+msgid "Missing amount"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:68
+#, c-format
+msgid "Amount is not valid"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:70
+#, c-format
+msgid "Should be greater than 0"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:79
+#, c-format
+msgid "Receiver IBAN:"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:102
+#, c-format
+msgid "Transfer subject:"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:123
+#, c-format
+msgid "Amount:"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:177
+#, c-format
+msgid "Field(s) missing."
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:227
+#, c-format
+msgid "Want to try the raw payto://-format?"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:235
+#, c-format
+msgid "Missing payto address"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:237
+#, c-format
+msgid "Payto does not follow the pattern"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:243
+#, c-format
+msgid "Transfer money to account identified by payto:// URI:"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:246
+#, c-format
+msgid "payto URI:"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:255
+#, c-format
+msgid "payto address"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:279
+#, c-format
+msgid "Send"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:314
+#, c-format
+msgid "Use wire-transfer form?"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:373
+#, c-format
+msgid "No credentials found."
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:397
+#, c-format
+msgid "Could not create the wire transfer"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:414
+#, c-format
+msgid "Transfer creation gave response error"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:426
+#, c-format
+msgid "Wire transfer created!"
+msgstr ""
+
+#: src/pages/home/WalletWithdrawForm.tsx:50
+#, fuzzy, c-format
+msgid "Amount to withdraw:"
+msgstr "Amount to withdraw"
+
+#: src/pages/home/WalletWithdrawForm.tsx:84
+#, fuzzy, c-format
+msgid "Withdraw"
+msgstr "Confirm withdrawal"
+
+#: src/pages/home/WalletWithdrawForm.tsx:128
+#, c-format
+msgid "No credentials given."
+msgstr ""
+
+#: src/pages/home/WalletWithdrawForm.tsx:155
+#, c-format
+msgid "Could not create withdrawal operation"
+msgstr ""
+
+#: src/pages/home/WalletWithdrawForm.tsx:171
+#, c-format
+msgid "Withdrawal creation gave response error"
+msgstr ""
+
+#: src/pages/home/PaymentOptions.tsx:44
+#, c-format
+msgid "Obtain digital cash"
+msgstr ""
+
+#: src/pages/home/PaymentOptions.tsx:52
+#, c-format
+msgid "Transfer to bank account"
+msgstr ""
+
+#: src/pages/home/Transactions.tsx:69
+#, c-format
+msgid "Date"
+msgstr ""
+
+#: src/pages/home/Transactions.tsx:70
+#, c-format
+msgid "Amount"
+msgstr ""
+
+#: src/pages/home/Transactions.tsx:71
+#, c-format
+msgid "Counterpart"
+msgstr ""
+
+#: src/pages/home/Transactions.tsx:72
+#, c-format
+msgid "Subject"
+msgstr ""
+
+#: src/pages/home/QrCodeSection.tsx:41
+#, fuzzy, c-format
+msgid "Transfer to Taler Wallet"
+msgstr "Charge Taler wallet"
+
+#: src/pages/home/QrCodeSection.tsx:44
+#, c-format
+msgid "Use this QR code to withdraw to your mobile wallet:"
+msgstr ""
+
+#: src/pages/home/QrCodeSection.tsx:47
+#, c-format
+msgid "Click %1$s to open your Taler wallet!"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:47
+#, fuzzy, c-format
+msgid "Confirm Withdrawal"
+msgstr "Confirm withdrawal"
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:52
+#, c-format
+msgid "Authorize withdrawal by solving challenge"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:55
+#, c-format
+msgid "What is"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:94
+#, c-format
+msgid "Answer is wrong."
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:99
+#, c-format
+msgid "Confirm"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:113
+#, c-format
+msgid "Cancel"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:120
+#, c-format
+msgid ""
+"A this point, a %1$s bank would ask for an additional authentication proof "
+"(PIN/TAN, one time password, ..), instead of a simple calculation."
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:166
+#, c-format
+msgid "No withdrawal ID found."
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:201
+#, fuzzy, c-format
+msgid "Could not confirm the withdrawal"
+msgstr "Confirm withdrawal"
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:219
+#, c-format
+msgid "Withdrawal confirmation gave response error"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:231
+#, c-format
+msgid "Withdrawal confirmed!"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:294
+#, fuzzy, c-format
+msgid "Could not abort the withdrawal."
+msgstr "Close Taler withdrawal"
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:311
+#, c-format
+msgid "Withdrawal abortion failed."
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:324
+#, c-format
+msgid "Withdrawal aborted!"
+msgstr ""
+
+#: src/pages/home/WithdrawalQRCode.tsx:54
+#, c-format
+msgid "Abort"
+msgstr ""
+
+#: src/pages/home/WithdrawalQRCode.tsx:74
+#, c-format
+msgid "withdrawal (%1$s) was never (correctly) created at the bank..."
+msgstr ""
+
+#: src/pages/home/WithdrawalQRCode.tsx:88
+#, c-format
+msgid "Waiting the bank to create the operation..."
+msgstr ""
+
+#: src/pages/home/WithdrawalQRCode.tsx:102
+#, c-format
+msgid "This withdrawal was aborted!"
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:40
+#, c-format
+msgid "Welcome to %1$s!"
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:133
+#, c-format
+msgid "Username or account label '%1$s' not found. Won't login."
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:159
+#, c-format
+msgid "Wrong credentials given."
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:169
+#, c-format
+msgid "Account information could not be retrieved."
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:210
+#, c-format
+msgid "Welcome, %1$s !"
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:221
+#, c-format
+msgid "Bank account balance"
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:237
+#, c-format
+msgid "Payments"
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:243
+#, c-format
+msgid "Latest transactions:"
+msgstr ""
+
+#: src/pages/home/PublicHistoriesPage.tsx:83
+#, c-format
+msgid "List of public accounts was not found."
+msgstr ""
+
+#: src/pages/home/PublicHistoriesPage.tsx:95
+#, c-format
+msgid "List of public accounts could not be retrieved."
+msgstr ""
+
+#: src/pages/home/PublicHistoriesPage.tsx:143
+#, c-format
+msgid "History of public accounts"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:39
+#, c-format
+msgid "Currently, the bank is not accepting new registrations!"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:68
+#, c-format
+msgid "Use only letter and numbers starting with a lower case letter"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:78
+#, c-format
+msgid "Password don't match"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:89
+#, c-format
+msgid "Please register!"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:126
+#, c-format
+msgid "Repeat Password:"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:226
+#, c-format
+msgid "Registration failed, please report"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:239
+#, c-format
+msgid "That username is already taken"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:248
+#, c-format
+msgid "New registration gave response error"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:53
+#, c-format
+msgid "Bank menu"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:59
+#, c-format
+msgid "Select option1"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:66
+#, c-format
+msgid "Select option2"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:55
+#, c-format
+msgid "days"
+msgstr "days"
+
+#: src/components/picker/DurationPicker.tsx:65
+#, c-format
+msgid "hours"
+msgstr "hours"
+
+#: src/components/picker/DurationPicker.tsx:76
+#, c-format
+msgid "minutes"
+msgstr "minutes"
+
+#: src/components/picker/DurationPicker.tsx:87
+#, c-format
+msgid "seconds"
+msgstr "seconds"
+
+#~ msgid "Go back"
+#~ msgstr "Go back"
+
+#, fuzzy
+#~ msgid "Start withdrawal"
+#~ msgstr "Start withdrawal"
+
+#, fuzzy
+#~ msgid "Withdraw Money into a Taler wallet"
+#~ msgstr "Charge Taler wallet"
+
+#~ msgid "Page has a problem: logged in but backend state is lost."
+#~ msgstr "Page has a problem: logged in but backend state is lost."
+
+#, fuzzy
+#~ msgid "Welcome to the euFin bank!"
+#~ msgstr "Welcome to euFin bank: Taler+IBAN now possible!"
+
+#~ msgid "Page has a problem:"
+#~ msgstr "Page has a problem:"
+
+#~ msgid "Close"
+#~ msgstr "Close"
+
+#~ msgid "Sign in"
+#~ msgstr "Sign in"
diff --git a/packages/aml-backoffice-ui/src/i18n/es.po b/packages/aml-backoffice-ui/src/i18n/es.po
new file mode 100644
index 000000000..0787b1035
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/i18n/es.po
@@ -0,0 +1,497 @@
+# 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/>
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Wallet\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"POT-Creation-Date: 2016-11-23 00:00+0100\n"
+"PO-Revision-Date: 2022-12-09 14:13+0000\n"
+"Last-Translator: Sebastian Marchano <sebasjm@gmail.com>\n"
+"Language-Team: Spanish <https://weblate.taler.net/projects/gnu-taler/taler-"
+"bank-spa/es/>\n"
+"Language: es\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 4.13.1\n"
+
+#: src/pages/home/BankFrame.tsx:55
+#, c-format
+msgid "Logout"
+msgstr "Cierre de sesión"
+
+#: src/pages/home/BankFrame.tsx:73
+#, c-format
+msgid "Skip to main content"
+msgstr "Saltar el menú de navegación"
+
+#: src/pages/home/BankFrame.tsx:82
+#, c-format
+msgid ""
+"This part of the demo shows how a bank that supports Taler directly would "
+"work. In addition to using your own bank account, you can also see the "
+"transaction history of some %1$s."
+msgstr ""
+"Esta parte de la demostración muestra cómo funciona un banco que soporta "
+"Taler directamente. Además de usar tu propia cuenta de banco, también podrás "
+"ver el historial de transacciones de algunas %1$s."
+
+#: src/pages/home/BankFrame.tsx:94
+#, c-format
+msgid "Taler logo"
+msgstr "Logo Taler"
+
+#: src/pages/home/LoginForm.tsx:41
+#, c-format
+msgid "Missing username"
+msgstr "Falta nombre de usuario"
+
+#: src/pages/home/LoginForm.tsx:42
+#, c-format
+msgid "Missing password"
+msgstr "Falta contraseña"
+
+#: src/pages/home/LoginForm.tsx:49
+#, c-format
+msgid "Please login!"
+msgstr "Por favor inicia sesión!"
+
+#: src/pages/home/LoginForm.tsx:51
+#, c-format
+msgid "Username:"
+msgstr "Nombre de usuario:"
+
+#: src/pages/home/LoginForm.tsx:71
+#, c-format
+msgid "Password:"
+msgstr "Password:"
+
+#: src/pages/home/LoginForm.tsx:100
+#, c-format
+msgid "Login"
+msgstr "Iniciar sesión"
+
+#: src/pages/home/LoginForm.tsx:110
+#, c-format
+msgid "Register"
+msgstr "Registrarse"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:60
+#, c-format
+msgid "Missing IBAN"
+msgstr "Falta IBAN"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:62
+#, c-format
+msgid "IBAN should have just uppercased letters and numbers"
+msgstr "IBAN debería tener letras mayúsculas y números"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:64
+#, c-format
+msgid "Missing subject"
+msgstr "Falta asunto"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:66
+#, c-format
+msgid "Missing amount"
+msgstr "Falta monto"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:68
+#, c-format
+msgid "Amount is not valid"
+msgstr "Monto no válido"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:70
+#, c-format
+msgid "Should be greater than 0"
+msgstr "Debería ser mas grande que 0"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:79
+#, c-format
+msgid "Receiver IBAN:"
+msgstr "IBAN receptor:"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:102
+#, c-format
+msgid "Transfer subject:"
+msgstr "Asunto de transferencia:"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:123
+#, c-format
+msgid "Amount:"
+msgstr "Monto:"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:177
+#, c-format
+msgid "Field(s) missing."
+msgstr "Faltan campo(s)."
+
+#: src/pages/home/PaytoWireTransferForm.tsx:227
+#, c-format
+msgid "Want to try the raw payto://-format?"
+msgstr "Quieres probar el formato payto:// ?"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:235
+#, c-format
+msgid "Missing payto address"
+msgstr "Falta direccion payto"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:237
+#, c-format
+msgid "Payto does not follow the pattern"
+msgstr "Payto no sigue el patrón"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:243
+#, c-format
+msgid "Transfer money to account identified by payto:// URI:"
+msgstr "Transferir dinero a la cuenta identificada por la URI payto://:"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:246
+#, c-format
+msgid "payto URI:"
+msgstr "payto URI:"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:255
+#, c-format
+msgid "payto address"
+msgstr "direccion payto"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:279
+#, c-format
+msgid "Send"
+msgstr "Envíar"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:314
+#, c-format
+msgid "Use wire-transfer form?"
+msgstr "Usar el formulario de transferencia bancaria?"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:373
+#, c-format
+msgid "No credentials found."
+msgstr "Se dieron las credenciales incorrectas."
+
+#: src/pages/home/PaytoWireTransferForm.tsx:397
+#, c-format
+msgid "Could not create the wire transfer"
+msgstr "No se pudo create la transferencia bancaria"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:414
+#, c-format
+msgid "Transfer creation gave response error"
+msgstr "La creación de la transferencia dió una respuesta erronea"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:426
+#, c-format
+msgid "Wire transfer created!"
+msgstr "Transferencia bancaria creada!"
+
+#: src/pages/home/WalletWithdrawForm.tsx:50
+#, c-format
+msgid "Amount to withdraw:"
+msgstr "Monto a retirar:"
+
+#: src/pages/home/WalletWithdrawForm.tsx:84
+#, c-format
+msgid "Withdraw"
+msgstr "Retirar"
+
+#: src/pages/home/WalletWithdrawForm.tsx:128
+#, c-format
+msgid "No credentials given."
+msgstr "Se dieron las credenciales incorrectas."
+
+#: src/pages/home/WalletWithdrawForm.tsx:155
+#, c-format
+msgid "Could not create withdrawal operation"
+msgstr "No se pude create la operación de retiro"
+
+#: src/pages/home/WalletWithdrawForm.tsx:171
+#, c-format
+msgid "Withdrawal creation gave response error"
+msgstr "La creación de retiro dió una respuesta errónea"
+
+#: src/pages/home/PaymentOptions.tsx:44
+#, c-format
+msgid "Obtain digital cash"
+msgstr "Obtener dinero digital"
+
+#: src/pages/home/PaymentOptions.tsx:52
+#, c-format
+msgid "Transfer to bank account"
+msgstr "Transferir a una cuenta bancaria"
+
+#: src/pages/home/Transactions.tsx:69
+#, c-format
+msgid "Date"
+msgstr "Fecha"
+
+#: src/pages/home/Transactions.tsx:70
+#, c-format
+msgid "Amount"
+msgstr "Monto"
+
+#: src/pages/home/Transactions.tsx:71
+#, c-format
+msgid "Counterpart"
+msgstr "Contraparte"
+
+#: src/pages/home/Transactions.tsx:72
+#, c-format
+msgid "Subject"
+msgstr "Asunto"
+
+#: src/pages/home/QrCodeSection.tsx:41
+#, c-format
+msgid "Transfer to Taler Wallet"
+msgstr "Transferir a una cartera Taler"
+
+#: src/pages/home/QrCodeSection.tsx:44
+#, c-format
+msgid "Use this QR code to withdraw to your mobile wallet:"
+msgstr "Usar el código QR para retirar a tu cartera móvil:"
+
+#: src/pages/home/QrCodeSection.tsx:47
+#, c-format
+msgid "Click %1$s to open your Taler wallet!"
+msgstr "Click %1$s para abrir una cartera Taler!"
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:47
+#, c-format
+msgid "Confirm Withdrawal"
+msgstr "Confirmar retirada"
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:52
+#, c-format
+msgid "Authorize withdrawal by solving challenge"
+msgstr "Autorizar retiro resolviendo una pregunta"
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:55
+#, c-format
+msgid "What is"
+msgstr "Cuanto es"
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:94
+#, c-format
+msgid "Answer is wrong."
+msgstr "La respuesta es incorrecta."
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:99
+#, c-format
+msgid "Confirm"
+msgstr "Confirmar"
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:113
+#, c-format
+msgid "Cancel"
+msgstr "Cancelar"
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:120
+#, c-format
+msgid ""
+"A this point, a %1$s bank would ask for an additional authentication proof "
+"(PIN/TAN, one time password, ..), instead of a simple calculation."
+msgstr ""
+"En este punto, un banco %1$s preguntaría por una prueba adicional de "
+"autenticación (PIN/TAN, password de un solo uso, ....), en vez de un simple "
+"cálculo."
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:166
+#, c-format
+msgid "No withdrawal ID found."
+msgstr "No ID de retiro encontrado."
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:201
+#, c-format
+msgid "Could not confirm the withdrawal"
+msgstr "No se pudo confirmar la retirada"
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:219
+#, c-format
+msgid "Withdrawal confirmation gave response error"
+msgstr "La confirmación de retiro dió una respuesta errónea"
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:231
+#, c-format
+msgid "Withdrawal confirmed!"
+msgstr "El retiro fue confirmado!"
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:294
+#, c-format
+msgid "Could not abort the withdrawal."
+msgstr "No se pudo cancelar el retiro."
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:311
+#, c-format
+msgid "Withdrawal abortion failed."
+msgstr "La cancelación del retiro falló."
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:324
+#, c-format
+msgid "Withdrawal aborted!"
+msgstr "Este retiro fue cancelado!"
+
+#: src/pages/home/WithdrawalQRCode.tsx:54
+#, c-format
+msgid "Abort"
+msgstr "Cancelar"
+
+#: src/pages/home/WithdrawalQRCode.tsx:74
+#, c-format
+msgid "withdrawal (%1$s) was never (correctly) created at the bank..."
+msgstr "retiro (%1$s) nunca fue (correctamente) generado en el banco..."
+
+#: src/pages/home/WithdrawalQRCode.tsx:88
+#, c-format
+msgid "Waiting the bank to create the operation..."
+msgstr "Esperando que el banco genere la operación...."
+
+#: src/pages/home/WithdrawalQRCode.tsx:102
+#, c-format
+msgid "This withdrawal was aborted!"
+msgstr "Este retiro fue cancelado!"
+
+#: src/pages/home/AccountPage.tsx:40
+#, c-format
+msgid "Welcome to %1$s!"
+msgstr "Bienvenido a %1$s!"
+
+#: src/pages/home/AccountPage.tsx:133
+#, c-format
+msgid "Username or account label '%1$s' not found. Won't login."
+msgstr ""
+"Nombre de usuario o etiqueta de cuenta '%1$s' no encontrada. No se iniciará "
+"sesión."
+
+#: src/pages/home/AccountPage.tsx:159
+#, c-format
+msgid "Wrong credentials given."
+msgstr "Se dieron las credenciales incorrectas."
+
+#: src/pages/home/AccountPage.tsx:169
+#, c-format
+msgid "Account information could not be retrieved."
+msgstr "La información de la cuenta no pudo ser accedida."
+
+#: src/pages/home/AccountPage.tsx:210
+#, c-format
+msgid "Welcome, %1$s !"
+msgstr "Bienvenido/a, %1$s!"
+
+#: src/pages/home/AccountPage.tsx:221
+#, c-format
+msgid "Bank account balance"
+msgstr "Balance de cuenta bancaria"
+
+#: src/pages/home/AccountPage.tsx:237
+#, c-format
+msgid "Payments"
+msgstr "Pagos"
+
+#: src/pages/home/AccountPage.tsx:243
+#, c-format
+msgid "Latest transactions:"
+msgstr "Últimas transacciones:"
+
+#: src/pages/home/PublicHistoriesPage.tsx:83
+#, c-format
+msgid "List of public accounts was not found."
+msgstr "La lista de cuentas públicas no fue encontrada."
+
+#: src/pages/home/PublicHistoriesPage.tsx:95
+#, c-format
+msgid "List of public accounts could not be retrieved."
+msgstr "La lista de cuentas públicas no pudo ser accedida."
+
+#: src/pages/home/PublicHistoriesPage.tsx:143
+#, c-format
+msgid "History of public accounts"
+msgstr "Historial de cuentas públicas"
+
+#: src/pages/home/RegistrationPage.tsx:39
+#, c-format
+msgid "Currently, the bank is not accepting new registrations!"
+msgstr "Actualmente, el banco no está aceptado nuevos registros!"
+
+#: src/pages/home/RegistrationPage.tsx:68
+#, c-format
+msgid "Use only letter and numbers starting with a lower case letter"
+msgstr "Solo use letras y números comenzando con una letra minúscula"
+
+#: src/pages/home/RegistrationPage.tsx:78
+#, c-format
+msgid "Password don't match"
+msgstr "La contraseña no coincide"
+
+#: src/pages/home/RegistrationPage.tsx:89
+#, c-format
+msgid "Please register!"
+msgstr "Por favor, registrese!"
+
+#: src/pages/home/RegistrationPage.tsx:126
+#, c-format
+msgid "Repeat Password:"
+msgstr "Repita la contraseña:"
+
+#: src/pages/home/RegistrationPage.tsx:226
+#, c-format
+msgid "Registration failed, please report"
+msgstr "El registro falló, por favor reportelo"
+
+#: src/pages/home/RegistrationPage.tsx:239
+#, c-format
+msgid "That username is already taken"
+msgstr "El nombre del usuario ya está tomado"
+
+#: src/pages/home/RegistrationPage.tsx:248
+#, c-format
+msgid "New registration gave response error"
+msgstr "Nuevo registro dió una respuesta errónea"
+
+#: src/components/menu/SideBar.tsx:53
+#, c-format
+msgid "Bank menu"
+msgstr "Menu del banco"
+
+#: src/components/menu/SideBar.tsx:59
+#, c-format
+msgid "Select option1"
+msgstr "Seleccione opción 1"
+
+#: src/components/menu/SideBar.tsx:66
+#, c-format
+msgid "Select option2"
+msgstr "Seleccione opción 2"
+
+#: src/components/picker/DurationPicker.tsx:55
+#, c-format
+msgid "days"
+msgstr "días"
+
+#: src/components/picker/DurationPicker.tsx:65
+#, c-format
+msgid "hours"
+msgstr "horas"
+
+#: src/components/picker/DurationPicker.tsx:76
+#, c-format
+msgid "minutes"
+msgstr "minutos"
+
+#: src/components/picker/DurationPicker.tsx:87
+#, c-format
+msgid "seconds"
+msgstr "segundos"
+
+#~ msgid "this link"
+#~ msgstr "este link"
diff --git a/packages/aml-backoffice-ui/src/i18n/fr.po b/packages/aml-backoffice-ui/src/i18n/fr.po
new file mode 100644
index 000000000..203d55343
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/i18n/fr.po
@@ -0,0 +1,486 @@
+# This file is part of GNU Taler
+# (C) 2022 Taler Systems S.A.
+#
+# GNU Taler is free software; you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 3, or (at your option) any later version.
+#
+# GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Bank\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: Automatically generated\n"
+"Language-Team: none\n"
+"Language: fr\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n > 1;\n"
+
+#: src/pages/home/BankFrame.tsx:55
+#, c-format
+msgid "Logout"
+msgstr ""
+
+#: src/pages/home/BankFrame.tsx:73
+#, c-format
+msgid "Skip to main content"
+msgstr ""
+
+#: src/pages/home/BankFrame.tsx:82
+#, c-format
+msgid ""
+"This part of the demo shows how a bank that supports Taler directly would work. "
+"In addition to using your own bank account, you can also see the transaction "
+"history of some %1$s."
+msgstr ""
+
+#: src/pages/home/BankFrame.tsx:94
+#, c-format
+msgid "Taler logo"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:41
+#, c-format
+msgid "Missing username"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:42
+#, c-format
+msgid "Missing password"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:49
+#, c-format
+msgid "Please login!"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:51
+#, c-format
+msgid "Username:"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:71
+#, c-format
+msgid "Password:"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:100
+#, c-format
+msgid "Login"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:110
+#, c-format
+msgid "Register"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:60
+#, c-format
+msgid "Missing IBAN"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:62
+#, c-format
+msgid "IBAN should have just uppercased letters and numbers"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:64
+#, c-format
+msgid "Missing subject"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:66
+#, c-format
+msgid "Missing amount"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:68
+#, c-format
+msgid "Amount is not valid"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:70
+#, c-format
+msgid "Should be greater than 0"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:79
+#, c-format
+msgid "Receiver IBAN:"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:102
+#, c-format
+msgid "Transfer subject:"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:123
+#, c-format
+msgid "Amount:"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:177
+#, c-format
+msgid "Field(s) missing."
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:227
+#, c-format
+msgid "Want to try the raw payto://-format?"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:235
+#, c-format
+msgid "Missing payto address"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:237
+#, c-format
+msgid "Payto does not follow the pattern"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:243
+#, c-format
+msgid "Transfer money to account identified by payto:// URI:"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:246
+#, c-format
+msgid "payto URI:"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:255
+#, c-format
+msgid "payto address"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:279
+#, c-format
+msgid "Send"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:314
+#, c-format
+msgid "Use wire-transfer form?"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:373
+#, c-format
+msgid "No credentials found."
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:397
+#, c-format
+msgid "Could not create the wire transfer"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:414
+#, c-format
+msgid "Transfer creation gave response error"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:426
+#, c-format
+msgid "Wire transfer created!"
+msgstr ""
+
+#: src/pages/home/WalletWithdrawForm.tsx:50
+#, c-format
+msgid "Amount to withdraw:"
+msgstr ""
+
+#: src/pages/home/WalletWithdrawForm.tsx:84
+#, c-format
+msgid "Withdraw"
+msgstr ""
+
+#: src/pages/home/WalletWithdrawForm.tsx:128
+#, c-format
+msgid "No credentials given."
+msgstr ""
+
+#: src/pages/home/WalletWithdrawForm.tsx:155
+#, c-format
+msgid "Could not create withdrawal operation"
+msgstr ""
+
+#: src/pages/home/WalletWithdrawForm.tsx:171
+#, c-format
+msgid "Withdrawal creation gave response error"
+msgstr ""
+
+#: src/pages/home/PaymentOptions.tsx:44
+#, c-format
+msgid "Obtain digital cash"
+msgstr ""
+
+#: src/pages/home/PaymentOptions.tsx:52
+#, c-format
+msgid "Transfer to bank account"
+msgstr ""
+
+#: src/pages/home/Transactions.tsx:69
+#, c-format
+msgid "Date"
+msgstr ""
+
+#: src/pages/home/Transactions.tsx:70
+#, c-format
+msgid "Amount"
+msgstr ""
+
+#: src/pages/home/Transactions.tsx:71
+#, c-format
+msgid "Counterpart"
+msgstr ""
+
+#: src/pages/home/Transactions.tsx:72
+#, c-format
+msgid "Subject"
+msgstr ""
+
+#: src/pages/home/QrCodeSection.tsx:41
+#, c-format
+msgid "Transfer to Taler Wallet"
+msgstr ""
+
+#: src/pages/home/QrCodeSection.tsx:44
+#, c-format
+msgid "Use this QR code to withdraw to your mobile wallet:"
+msgstr ""
+
+#: src/pages/home/QrCodeSection.tsx:47
+#, c-format
+msgid "Click %1$s to open your Taler wallet!"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:47
+#, c-format
+msgid "Confirm Withdrawal"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:52
+#, c-format
+msgid "Authorize withdrawal by solving challenge"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:55
+#, c-format
+msgid "What is"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:94
+#, c-format
+msgid "Answer is wrong."
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:99
+#, c-format
+msgid "Confirm"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:113
+#, c-format
+msgid "Cancel"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:120
+#, c-format
+msgid ""
+"A this point, a %1$s bank would ask for an additional authentication proof "
+"(PIN/TAN, one time password, ..), instead of a simple calculation."
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:166
+#, c-format
+msgid "No withdrawal ID found."
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:201
+#, c-format
+msgid "Could not confirm the withdrawal"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:219
+#, c-format
+msgid "Withdrawal confirmation gave response error"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:231
+#, c-format
+msgid "Withdrawal confirmed!"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:294
+#, c-format
+msgid "Could not abort the withdrawal."
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:311
+#, c-format
+msgid "Withdrawal abortion failed."
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:324
+#, c-format
+msgid "Withdrawal aborted!"
+msgstr ""
+
+#: src/pages/home/WithdrawalQRCode.tsx:54
+#, c-format
+msgid "Abort"
+msgstr ""
+
+#: src/pages/home/WithdrawalQRCode.tsx:74
+#, c-format
+msgid "withdrawal (%1$s) was never (correctly) created at the bank..."
+msgstr ""
+
+#: src/pages/home/WithdrawalQRCode.tsx:88
+#, c-format
+msgid "Waiting the bank to create the operation..."
+msgstr ""
+
+#: src/pages/home/WithdrawalQRCode.tsx:102
+#, c-format
+msgid "This withdrawal was aborted!"
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:40
+#, c-format
+msgid "Welcome to %1$s!"
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:133
+#, c-format
+msgid "Username or account label '%1$s' not found. Won't login."
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:159
+#, c-format
+msgid "Wrong credentials given."
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:169
+#, c-format
+msgid "Account information could not be retrieved."
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:210
+#, c-format
+msgid "Welcome, %1$s !"
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:221
+#, c-format
+msgid "Bank account balance"
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:237
+#, c-format
+msgid "Payments"
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:243
+#, c-format
+msgid "Latest transactions:"
+msgstr ""
+
+#: src/pages/home/PublicHistoriesPage.tsx:83
+#, c-format
+msgid "List of public accounts was not found."
+msgstr ""
+
+#: src/pages/home/PublicHistoriesPage.tsx:95
+#, c-format
+msgid "List of public accounts could not be retrieved."
+msgstr ""
+
+#: src/pages/home/PublicHistoriesPage.tsx:143
+#, c-format
+msgid "History of public accounts"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:39
+#, c-format
+msgid "Currently, the bank is not accepting new registrations!"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:68
+#, c-format
+msgid "Use only letter and numbers starting with a lower case letter"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:78
+#, c-format
+msgid "Password don't match"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:89
+#, c-format
+msgid "Please register!"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:126
+#, c-format
+msgid "Repeat Password:"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:226
+#, c-format
+msgid "Registration failed, please report"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:239
+#, c-format
+msgid "That username is already taken"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:248
+#, c-format
+msgid "New registration gave response error"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:53
+#, c-format
+msgid "Bank menu"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:59
+#, c-format
+msgid "Select option1"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:66
+#, c-format
+msgid "Select option2"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:55
+#, c-format
+msgid "days"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:65
+#, c-format
+msgid "hours"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:76
+#, c-format
+msgid "minutes"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:87
+#, c-format
+msgid "seconds"
+msgstr ""
diff --git a/packages/aml-backoffice-ui/src/i18n/it.po b/packages/aml-backoffice-ui/src/i18n/it.po
new file mode 100644
index 000000000..a3a599376
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/i18n/it.po
@@ -0,0 +1,521 @@
+# 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/>
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Wallet\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"POT-Creation-Date: 2016-11-23 00:00+0100\n"
+"PO-Revision-Date: 2022-12-26 23:30+0000\n"
+"Last-Translator: Stefan Kügel <skuegel@web.de>\n"
+"Language-Team: Italian <https://weblate.taler.net/projects/gnu-taler/"
+"taler-bank-spa/it/>\n"
+"Language: it\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 4.13.1\n"
+
+#: src/pages/home/BankFrame.tsx:55
+#, c-format
+msgid "Logout"
+msgstr ""
+
+#: src/pages/home/BankFrame.tsx:73
+#, c-format
+msgid "Skip to main content"
+msgstr "Saltare il menu di navigazione"
+
+#: src/pages/home/BankFrame.tsx:82
+#, c-format
+msgid ""
+"This part of the demo shows how a bank that supports Taler directly would "
+"work. In addition to using your own bank account, you can also see the "
+"transaction history of some %1$s."
+msgstr ""
+
+#: src/pages/home/BankFrame.tsx:94
+#, c-format
+msgid "Taler logo"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:41
+#, c-format
+msgid "Missing username"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:42
+#, c-format
+msgid "Missing password"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:49
+#, c-format
+msgid "Please login!"
+msgstr "Accedi!"
+
+#: src/pages/home/LoginForm.tsx:51
+#, c-format
+msgid "Username:"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:71
+#, c-format
+msgid "Password:"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:100
+#, c-format
+msgid "Login"
+msgstr "Accedi"
+
+#: src/pages/home/LoginForm.tsx:110
+#, c-format
+msgid "Register"
+msgstr "Registrati"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:60
+#, c-format
+msgid "Missing IBAN"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:62
+#, c-format
+msgid "IBAN should have just uppercased letters and numbers"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:64
+#, c-format
+msgid "Missing subject"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:66
+#, c-format
+msgid "Missing amount"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:68
+#, c-format
+msgid "Amount is not valid"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:70
+#, c-format
+msgid "Should be greater than 0"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:79
+#, c-format
+msgid "Receiver IBAN:"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:102
+#, c-format
+msgid "Transfer subject:"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:123
+#, fuzzy, c-format
+msgid "Amount:"
+msgstr "Somma"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:177
+#, c-format
+msgid "Field(s) missing."
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:227
+#, c-format
+msgid "Want to try the raw payto://-format?"
+msgstr "Prova il trasferimento tramite il formato Payto!"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:235
+#, fuzzy, c-format
+msgid "Missing payto address"
+msgstr "indirizzo Payto"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:237
+#, c-format
+msgid "Payto does not follow the pattern"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:243
+#, fuzzy, c-format
+msgid "Transfer money to account identified by payto:// URI:"
+msgstr "Trasferisci fondi a un altro conto di questa banca:"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:246
+#, c-format
+msgid "payto URI:"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:255
+#, c-format
+msgid "payto address"
+msgstr "indirizzo Payto"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:279
+#, c-format
+msgid "Send"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:314
+#, fuzzy, c-format
+msgid "Use wire-transfer form?"
+msgstr "Chiudi il bonifico"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:373
+#, fuzzy, c-format
+msgid "No credentials found."
+msgstr "Credenziali invalide."
+
+#: src/pages/home/PaytoWireTransferForm.tsx:397
+#, c-format
+msgid "Could not create the wire transfer"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:414
+#, c-format
+msgid "Transfer creation gave response error"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:426
+#, fuzzy, c-format
+msgid "Wire transfer created!"
+msgstr "Bonifico"
+
+#: src/pages/home/WalletWithdrawForm.tsx:50
+#, fuzzy, c-format
+msgid "Amount to withdraw:"
+msgstr "Somma da ritirare"
+
+#: src/pages/home/WalletWithdrawForm.tsx:84
+#, fuzzy, c-format
+msgid "Withdraw"
+msgstr "Conferma il ritiro"
+
+#: src/pages/home/WalletWithdrawForm.tsx:128
+#, fuzzy, c-format
+msgid "No credentials given."
+msgstr "Credenziali invalide."
+
+#: src/pages/home/WalletWithdrawForm.tsx:155
+#, c-format
+msgid "Could not create withdrawal operation"
+msgstr ""
+
+#: src/pages/home/WalletWithdrawForm.tsx:171
+#, c-format
+msgid "Withdrawal creation gave response error"
+msgstr ""
+
+#: src/pages/home/PaymentOptions.tsx:44
+#, c-format
+msgid "Obtain digital cash"
+msgstr ""
+
+#: src/pages/home/PaymentOptions.tsx:52
+#, fuzzy, c-format
+msgid "Transfer to bank account"
+msgstr "Trasferisci fondi a un altro conto di questa banca:"
+
+#: src/pages/home/Transactions.tsx:69
+#, c-format
+msgid "Date"
+msgstr ""
+
+#: src/pages/home/Transactions.tsx:70
+#, c-format
+msgid "Amount"
+msgstr "Somma"
+
+#: src/pages/home/Transactions.tsx:71
+#, c-format
+msgid "Counterpart"
+msgstr "Controparte"
+
+#: src/pages/home/Transactions.tsx:72
+#, c-format
+msgid "Subject"
+msgstr "Causale"
+
+#: src/pages/home/QrCodeSection.tsx:41
+#, fuzzy, c-format
+msgid "Transfer to Taler Wallet"
+msgstr "Ritira contante nel portafoglio Taler"
+
+#: src/pages/home/QrCodeSection.tsx:44
+#, fuzzy, c-format
+msgid "Use this QR code to withdraw to your mobile wallet:"
+msgstr "Usa questo codice QR per ritirare contante nel tuo wallet:"
+
+#: src/pages/home/QrCodeSection.tsx:47
+#, c-format
+msgid "Click %1$s to open your Taler wallet!"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:47
+#, c-format
+msgid "Confirm Withdrawal"
+msgstr "Conferma il ritiro"
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:52
+#, c-format
+msgid "Authorize withdrawal by solving challenge"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:55
+#, c-format
+msgid "What is"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:94
+#, c-format
+msgid "Answer is wrong."
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:99
+#, c-format
+msgid "Confirm"
+msgstr "Conferma"
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:113
+#, c-format
+msgid "Cancel"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:120
+#, c-format
+msgid ""
+"A this point, a %1$s bank would ask for an additional authentication proof "
+"(PIN/TAN, one time password, ..), instead of a simple calculation."
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:166
+#, c-format
+msgid "No withdrawal ID found."
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:201
+#, fuzzy, c-format
+msgid "Could not confirm the withdrawal"
+msgstr "Conferma il ritiro"
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:219
+#, c-format
+msgid "Withdrawal confirmation gave response error"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:231
+#, fuzzy, c-format
+msgid "Withdrawal confirmed!"
+msgstr "Questo ritiro è stato annullato!"
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:294
+#, fuzzy, c-format
+msgid "Could not abort the withdrawal."
+msgstr "Chiudi il ritiro Taler"
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:311
+#, fuzzy, c-format
+msgid "Withdrawal abortion failed."
+msgstr "Questo ritiro è stato annullato!"
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:324
+#, fuzzy, c-format
+msgid "Withdrawal aborted!"
+msgstr "Questo ritiro è stato annullato!"
+
+#: src/pages/home/WithdrawalQRCode.tsx:54
+#, c-format
+msgid "Abort"
+msgstr "Annulla"
+
+#: src/pages/home/WithdrawalQRCode.tsx:74
+#, c-format
+msgid "withdrawal (%1$s) was never (correctly) created at the bank..."
+msgstr ""
+
+#: src/pages/home/WithdrawalQRCode.tsx:88
+#, fuzzy, c-format
+msgid "Waiting the bank to create the operation..."
+msgstr "La banca sta creando l'operazione..."
+
+#: src/pages/home/WithdrawalQRCode.tsx:102
+#, c-format
+msgid "This withdrawal was aborted!"
+msgstr "Questo ritiro è stato annullato!"
+
+#: src/pages/home/AccountPage.tsx:40
+#, c-format
+msgid "Welcome to %1$s!"
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:133
+#, c-format
+msgid "Username or account label '%1$s' not found. Won't login."
+msgstr "L'utente '%1$s' non esiste. Login impossibile"
+
+#: src/pages/home/AccountPage.tsx:159
+#, c-format
+msgid "Wrong credentials given."
+msgstr "Credenziali invalide."
+
+#: src/pages/home/AccountPage.tsx:169
+#, c-format
+msgid "Account information could not be retrieved."
+msgstr "Impossibile ricevere le informazioni relative al conto."
+
+#: src/pages/home/AccountPage.tsx:210
+#, c-format
+msgid "Welcome, %1$s !"
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:221
+#, fuzzy, c-format
+msgid "Bank account balance"
+msgstr "Bilancio:"
+
+#: src/pages/home/AccountPage.tsx:237
+#, c-format
+msgid "Payments"
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:243
+#, c-format
+msgid "Latest transactions:"
+msgstr "Ultime transazioni:"
+
+#: src/pages/home/PublicHistoriesPage.tsx:83
+#, c-format
+msgid "List of public accounts was not found."
+msgstr "Lista conti pubblici non trovata."
+
+#: src/pages/home/PublicHistoriesPage.tsx:95
+#, c-format
+msgid "List of public accounts could not be retrieved."
+msgstr "Lista conti pubblici non pervenuta."
+
+#: src/pages/home/PublicHistoriesPage.tsx:143
+#, c-format
+msgid "History of public accounts"
+msgstr "Storico dei conti pubblici"
+
+#: src/pages/home/RegistrationPage.tsx:39
+#, c-format
+msgid "Currently, the bank is not accepting new registrations!"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:68
+#, c-format
+msgid "Use only letter and numbers starting with a lower case letter"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:78
+#, c-format
+msgid "Password don't match"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:89
+#, fuzzy, c-format
+msgid "Please register!"
+msgstr "Accedi!"
+
+#: src/pages/home/RegistrationPage.tsx:126
+#, c-format
+msgid "Repeat Password:"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:226
+#, fuzzy, c-format
+msgid "Registration failed, please report"
+msgstr "Registrazione"
+
+#: src/pages/home/RegistrationPage.tsx:239
+#, c-format
+msgid "That username is already taken"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:248
+#, c-format
+msgid "New registration gave response error"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:53
+#, c-format
+msgid "Bank menu"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:59
+#, c-format
+msgid "Select option1"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:66
+#, c-format
+msgid "Select option2"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:55
+#, c-format
+msgid "days"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:65
+#, c-format
+msgid "hours"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:76
+#, c-format
+msgid "minutes"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:87
+#, c-format
+msgid "seconds"
+msgstr ""
+
+#~ msgid "this link"
+#~ msgstr "questo link"
+
+#~ msgid "Clear"
+#~ msgstr "Cancella"
+
+#~ msgid "Demo Bank"
+#~ msgstr "Banca 'demo'"
+
+#~ msgid "Go back"
+#~ msgstr "Indietro"
+
+#~ msgid "Transfer money via the Payto system:"
+#~ msgstr "Effettua un bonifico tramite il sistema Payto:"
+
+#~ msgid "Start withdrawal"
+#~ msgstr "Ritira contante"
+
+#~ msgid "Withdraw Money into a Taler wallet"
+#~ msgstr "Ritira contante nel portafoglio Taler"
+
+#~ msgid "Register to the euFin bank!"
+#~ msgstr "Apri un conto in banca euFin!"
+
+#~ msgid "Transfer money manually"
+#~ msgstr "Effettua un bonifico"
+
+#~ msgid "Page has a problem: logged in but backend state is lost."
+#~ msgstr ""
+#~ "Stato inconsistente: accesso utente effettuato ma stato con server perso."
+
+#, fuzzy
+#~ msgid "Welcome to the euFin bank!"
+#~ msgstr "Benvenuti in banca euFin!"
diff --git a/packages/aml-backoffice-ui/src/i18n/poheader b/packages/aml-backoffice-ui/src/i18n/poheader
new file mode 100644
index 000000000..a251e9584
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/i18n/poheader
@@ -0,0 +1,26 @@
+# This file is part of GNU Taler
+# (C) 2022 Taler Systems S.A.
+#
+# GNU Taler is free software; you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 3, or (at your option) any later version.
+#
+# GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Bank\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
diff --git a/packages/demobank-ui/src/i18n/strings-prelude b/packages/aml-backoffice-ui/src/i18n/strings-prelude
index cca13afad..a0aeb8268 100644
--- a/packages/demobank-ui/src/i18n/strings-prelude
+++ b/packages/aml-backoffice-ui/src/i18n/strings-prelude
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
diff --git a/packages/aml-backoffice-ui/src/i18n/strings.ts b/packages/aml-backoffice-ui/src/i18n/strings.ts
new file mode 100644
index 000000000..a779bbc49
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/i18n/strings.ts
@@ -0,0 +1,510 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/*eslint quote-props: ["error", "consistent"]*/
+export const strings: { [s: string]: any } = {};
+
+strings["de"] = {
+ domain: "messages",
+ locale_data: {
+ messages: {
+ "": {
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=(n != 1);",
+ lang: "de",
+ },
+ Logout: [""],
+ "Skip to main content": [""],
+ "This part of the demo shows how a bank that supports Taler directly would work. In addition to using your own bank account, you can also see the transaction history of some %1$s.":
+ [""],
+ "Taler logo": [""],
+ "Missing username": [""],
+ "Missing password": [""],
+ "Please login!": [""],
+ "Username:": [""],
+ "Password:": [""],
+ Login: [""],
+ Register: [""],
+ "Missing IBAN": [""],
+ "IBAN should have just uppercased letters and numbers": [""],
+ "Missing subject": [""],
+ "Missing amount": [""],
+ "Amount is not valid": [""],
+ "Should be greater than 0": [""],
+ "Receiver IBAN:": [""],
+ "Transfer subject:": [""],
+ "Amount:": [""],
+ "Field(s) missing.": [""],
+ "Want to try the raw payto://-format?": [""],
+ "Missing payto address": [""],
+ "Payto does not follow the pattern": [""],
+ "Transfer money to account identified by payto:// URI:": [""],
+ "payto URI:": [""],
+ "payto address": [""],
+ Send: [""],
+ "Use wire-transfer form?": [""],
+ "No credentials found.": [""],
+ "Could not create the wire transfer": [""],
+ "Transfer creation gave response error": [""],
+ "Wire transfer created!": [""],
+ "Amount to withdraw:": [""],
+ Withdraw: [""],
+ "No credentials given.": [""],
+ "Could not create withdrawal operation": [""],
+ "Withdrawal creation gave response error": [""],
+ "Obtain digital cash": [""],
+ "Transfer to bank account": [""],
+ Date: [""],
+ Amount: [""],
+ Counterpart: [""],
+ Subject: [""],
+ "Transfer to Taler Wallet": [""],
+ "Use this QR code to withdraw to your mobile wallet:": [""],
+ "Click %1$s to open your Taler wallet!": [""],
+ "Confirm Withdrawal": [""],
+ "Authorize withdrawal by solving challenge": [""],
+ "What is": [""],
+ "Answer is wrong.": [""],
+ Confirm: [""],
+ Cancel: [""],
+ "A this point, a %1$s bank would ask for an additional authentication proof (PIN/TAN, one time password, ..), instead of a simple calculation.":
+ [""],
+ "No withdrawal ID found.": [""],
+ "Could not confirm the withdrawal": [""],
+ "Withdrawal confirmation gave response error": [""],
+ "Withdrawal confirmed!": [""],
+ "Could not abort the withdrawal.": [""],
+ "Withdrawal abortion failed.": [""],
+ "Withdrawal aborted!": [""],
+ Abort: [""],
+ "withdrawal (%1$s) was never (correctly) created at the bank...": [""],
+ "Waiting the bank to create the operation...": [""],
+ "This withdrawal was aborted!": [""],
+ "Welcome to %1$s!": [""],
+ "Username or account label '%1$s' not found. Won't login.": [""],
+ "Wrong credentials given.": [""],
+ "Account information could not be retrieved.": [""],
+ "Welcome, %1$s !": [""],
+ "Bank account balance": [""],
+ Payments: [""],
+ "Latest transactions:": [""],
+ "List of public accounts was not found.": [""],
+ "List of public accounts could not be retrieved.": [""],
+ "History of public accounts": [""],
+ "Currently, the bank is not accepting new registrations!": [""],
+ "Use only letter and numbers starting with a lower case letter": [""],
+ "Password don't match": [""],
+ "Please register!": [""],
+ "Repeat Password:": [""],
+ "Registration failed, please report": [""],
+ "That username is already taken": [""],
+ "New registration gave response error": [""],
+ "Bank menu": [""],
+ "Select option1": [""],
+ "Select option2": [""],
+ days: [""],
+ hours: [""],
+ minutes: [""],
+ seconds: [""],
+ },
+ },
+};
+
+strings["en"] = {
+ domain: "messages",
+ locale_data: {
+ messages: {
+ "": {
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=(n != 1);",
+ lang: "en",
+ },
+ Logout: [""],
+ "Skip to main content": [""],
+ "This part of the demo shows how a bank that supports Taler directly would work. In addition to using your own bank account, you can also see the transaction history of some %1$s.":
+ [""],
+ "Taler logo": [""],
+ "Missing username": [""],
+ "Missing password": [""],
+ "Please login!": [""],
+ "Username:": [""],
+ "Password:": [""],
+ Login: [""],
+ Register: [""],
+ "Missing IBAN": [""],
+ "IBAN should have just uppercased letters and numbers": [""],
+ "Missing subject": [""],
+ "Missing amount": [""],
+ "Amount is not valid": [""],
+ "Should be greater than 0": [""],
+ "Receiver IBAN:": [""],
+ "Transfer subject:": [""],
+ "Amount:": [""],
+ "Field(s) missing.": [""],
+ "Want to try the raw payto://-format?": [""],
+ "Missing payto address": [""],
+ "Payto does not follow the pattern": [""],
+ "Transfer money to account identified by payto:// URI:": [""],
+ "payto URI:": [""],
+ "payto address": [""],
+ Send: [""],
+ "Use wire-transfer form?": [""],
+ "No credentials found.": [""],
+ "Could not create the wire transfer": [""],
+ "Transfer creation gave response error": [""],
+ "Wire transfer created!": [""],
+ "Amount to withdraw:": ["Amount to withdraw"],
+ Withdraw: ["Confirm withdrawal"],
+ "No credentials given.": [""],
+ "Could not create withdrawal operation": [""],
+ "Withdrawal creation gave response error": [""],
+ "Obtain digital cash": [""],
+ "Transfer to bank account": [""],
+ Date: [""],
+ Amount: [""],
+ Counterpart: [""],
+ Subject: [""],
+ "Transfer to Taler Wallet": ["Charge Taler wallet"],
+ "Use this QR code to withdraw to your mobile wallet:": [""],
+ "Click %1$s to open your Taler wallet!": [""],
+ "Confirm Withdrawal": ["Confirm withdrawal"],
+ "Authorize withdrawal by solving challenge": [""],
+ "What is": [""],
+ "Answer is wrong.": [""],
+ Confirm: [""],
+ Cancel: [""],
+ "A this point, a %1$s bank would ask for an additional authentication proof (PIN/TAN, one time password, ..), instead of a simple calculation.":
+ [""],
+ "No withdrawal ID found.": [""],
+ "Could not confirm the withdrawal": ["Confirm withdrawal"],
+ "Withdrawal confirmation gave response error": [""],
+ "Withdrawal confirmed!": [""],
+ "Could not abort the withdrawal.": ["Close Taler withdrawal"],
+ "Withdrawal abortion failed.": [""],
+ "Withdrawal aborted!": [""],
+ Abort: [""],
+ "withdrawal (%1$s) was never (correctly) created at the bank...": [""],
+ "Waiting the bank to create the operation...": [""],
+ "This withdrawal was aborted!": [""],
+ "Welcome to %1$s!": [""],
+ "Username or account label '%1$s' not found. Won't login.": [""],
+ "Wrong credentials given.": [""],
+ "Account information could not be retrieved.": [""],
+ "Welcome, %1$s !": [""],
+ "Bank account balance": [""],
+ Payments: [""],
+ "Latest transactions:": [""],
+ "List of public accounts was not found.": [""],
+ "List of public accounts could not be retrieved.": [""],
+ "History of public accounts": [""],
+ "Currently, the bank is not accepting new registrations!": [""],
+ "Use only letter and numbers starting with a lower case letter": [""],
+ "Password don't match": [""],
+ "Please register!": [""],
+ "Repeat Password:": [""],
+ "Registration failed, please report": [""],
+ "That username is already taken": [""],
+ "New registration gave response error": [""],
+ "Bank menu": [""],
+ "Select option1": [""],
+ "Select option2": [""],
+ days: ["days"],
+ hours: ["hours"],
+ minutes: ["minutes"],
+ seconds: ["seconds"],
+ },
+ },
+};
+
+strings["es"] = {
+ domain: "messages",
+ locale_data: {
+ messages: {
+ "": {
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=n != 1;",
+ lang: "es",
+ },
+ Logout: ["Cierre de sesión"],
+ "Skip to main content": ["Saltar el menú de navegación"],
+ "This part of the demo shows how a bank that supports Taler directly would work. In addition to using your own bank account, you can also see the transaction history of some %1$s.":
+ [
+ "Esta parte de la demostración muestra cómo funciona un banco que soporta Taler directamente. Además de usar tu propia cuenta de banco, también podrás ver el historial de transacciones de algunas %1$s.",
+ ],
+ "Taler logo": ["Logo Taler"],
+ "Missing username": ["Falta nombre de usuario"],
+ "Missing password": ["Falta contraseña"],
+ "Please login!": ["Por favor inicia sesión!"],
+ "Username:": ["Nombre de usuario:"],
+ "Password:": ["Password:"],
+ Login: ["Iniciar sesión"],
+ Register: ["Registrarse"],
+ "Missing IBAN": ["Falta IBAN"],
+ "IBAN should have just uppercased letters and numbers": [
+ "IBAN debería tener letras mayúsculas y números",
+ ],
+ "Missing subject": ["Falta asunto"],
+ "Missing amount": ["Falta monto"],
+ "Amount is not valid": ["Monto no válido"],
+ "Should be greater than 0": ["Debería ser mas grande que 0"],
+ "Receiver IBAN:": ["IBAN receptor:"],
+ "Transfer subject:": ["Asunto de transferencia:"],
+ "Amount:": ["Monto:"],
+ "Field(s) missing.": ["Faltan campo(s)."],
+ "Want to try the raw payto://-format?": [
+ "Quieres probar el formato payto:// ?",
+ ],
+ "Missing payto address": ["Falta direccion payto"],
+ "Payto does not follow the pattern": ["Payto no sigue el patrón"],
+ "Transfer money to account identified by payto:// URI:": [
+ "Transferir dinero a la cuenta identificada por la URI payto://:",
+ ],
+ "payto URI:": ["payto URI:"],
+ "payto address": ["direccion payto"],
+ Send: ["Envíar"],
+ "Use wire-transfer form?": [
+ "Usar el formulario de transferencia bancaria?",
+ ],
+ "No credentials found.": ["Se dieron las credenciales incorrectas."],
+ "Could not create the wire transfer": [
+ "No se pudo create la transferencia bancaria",
+ ],
+ "Transfer creation gave response error": [
+ "La creación de la transferencia dió una respuesta erronea",
+ ],
+ "Wire transfer created!": ["Transferencia bancaria creada!"],
+ "Amount to withdraw:": ["Monto a retirar:"],
+ Withdraw: ["Retirar"],
+ "No credentials given.": ["Se dieron las credenciales incorrectas."],
+ "Could not create withdrawal operation": [
+ "No se pude create la operación de retiro",
+ ],
+ "Withdrawal creation gave response error": [
+ "La creación de retiro dió una respuesta errónea",
+ ],
+ "Obtain digital cash": ["Obtener dinero digital"],
+ "Transfer to bank account": ["Transferir a una cuenta bancaria"],
+ Date: ["Fecha"],
+ Amount: ["Monto"],
+ Counterpart: ["Contraparte"],
+ Subject: ["Asunto"],
+ "Transfer to Taler Wallet": ["Transferir a una cartera Taler"],
+ "Use this QR code to withdraw to your mobile wallet:": [
+ "Usar el código QR para retirar a tu cartera móvil:",
+ ],
+ "Click %1$s to open your Taler wallet!": [
+ "Click %1$s para abrir una cartera Taler!",
+ ],
+ "Confirm Withdrawal": ["Confirmar retirada"],
+ "Authorize withdrawal by solving challenge": [
+ "Autorizar retiro resolviendo una pregunta",
+ ],
+ "What is": ["Cuanto es"],
+ "Answer is wrong.": ["La respuesta es incorrecta."],
+ Confirm: ["Confirmar"],
+ Cancel: ["Cancelar"],
+ "A this point, a %1$s bank would ask for an additional authentication proof (PIN/TAN, one time password, ..), instead of a simple calculation.":
+ [
+ "En este punto, un banco %1$s preguntaría por una prueba adicional de autenticación (PIN/TAN, password de un solo uso, ....), en vez de un simple cálculo.",
+ ],
+ "No withdrawal ID found.": ["No ID de retiro encontrado."],
+ "Could not confirm the withdrawal": ["No se pudo confirmar la retirada"],
+ "Withdrawal confirmation gave response error": [
+ "La confirmación de retiro dió una respuesta errónea",
+ ],
+ "Withdrawal confirmed!": ["El retiro fue confirmado!"],
+ "Could not abort the withdrawal.": ["No se pudo cancelar el retiro."],
+ "Withdrawal abortion failed.": ["La cancelación del retiro falló."],
+ "Withdrawal aborted!": ["Este retiro fue cancelado!"],
+ Abort: ["Cancelar"],
+ "withdrawal (%1$s) was never (correctly) created at the bank...": [
+ "retiro (%1$s) nunca fue (correctamente) generado en el banco...",
+ ],
+ "Waiting the bank to create the operation...": [
+ "Esperando que el banco genere la operación....",
+ ],
+ "This withdrawal was aborted!": ["Este retiro fue cancelado!"],
+ "Welcome to %1$s!": ["Bienvenido a %1$s!"],
+ "Username or account label '%1$s' not found. Won't login.": [
+ "Nombre de usuario o etiqueta de cuenta '%1$s' no encontrada. No se iniciará sesión.",
+ ],
+ "Wrong credentials given.": ["Se dieron las credenciales incorrectas."],
+ "Account information could not be retrieved.": [
+ "La información de la cuenta no pudo ser accedida.",
+ ],
+ "Welcome, %1$s !": ["Bienvenido/a, %1$s!"],
+ "Bank account balance": ["Balance de cuenta bancaria"],
+ Payments: ["Pagos"],
+ "Latest transactions:": ["Últimas transacciones:"],
+ "List of public accounts was not found.": [
+ "La lista de cuentas públicas no fue encontrada.",
+ ],
+ "List of public accounts could not be retrieved.": [
+ "La lista de cuentas públicas no pudo ser accedida.",
+ ],
+ "History of public accounts": ["Historial de cuentas públicas"],
+ "Currently, the bank is not accepting new registrations!": [
+ "Actualmente, el banco no está aceptado nuevos registros!",
+ ],
+ "Use only letter and numbers starting with a lower case letter": [
+ "Solo use letras y números comenzando con una letra minúscula",
+ ],
+ "Password don't match": ["La contraseña no coincide"],
+ "Please register!": ["Por favor, registrese!"],
+ "Repeat Password:": ["Repita la contraseña:"],
+ "Registration failed, please report": [
+ "El registro falló, por favor reportelo",
+ ],
+ "That username is already taken": [
+ "El nombre del usuario ya está tomado",
+ ],
+ "New registration gave response error": [
+ "Nuevo registro dió una respuesta errónea",
+ ],
+ "Bank menu": ["Menu del banco"],
+ "Select option1": ["Seleccione opción 1"],
+ "Select option2": ["Seleccione opción 2"],
+ days: ["días"],
+ hours: ["horas"],
+ minutes: ["minutos"],
+ seconds: ["segundos"],
+ },
+ },
+};
+
+strings["it"] = {
+ domain: "messages",
+ locale_data: {
+ messages: {
+ "": {
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=(n != 1);",
+ lang: "it",
+ },
+ Logout: [""],
+ "Skip to main content": [""],
+ "This part of the demo shows how a bank that supports Taler directly would work. In addition to using your own bank account, you can also see the transaction history of some %1$s.":
+ [""],
+ "Taler logo": [""],
+ "Missing username": [""],
+ "Missing password": [""],
+ "Please login!": ["Accedi!"],
+ "Username:": [""],
+ "Password:": [""],
+ Login: ["Accedi"],
+ Register: ["Registrati"],
+ "Missing IBAN": [""],
+ "IBAN should have just uppercased letters and numbers": [""],
+ "Missing subject": [""],
+ "Missing amount": [""],
+ "Amount is not valid": [""],
+ "Should be greater than 0": [""],
+ "Receiver IBAN:": [""],
+ "Transfer subject:": [""],
+ "Amount:": ["Somma"],
+ "Field(s) missing.": [""],
+ "Want to try the raw payto://-format?": [
+ "Prova il trasferimento tramite il formato Payto!",
+ ],
+ "Missing payto address": ["indirizzo Payto"],
+ "Payto does not follow the pattern": [""],
+ "Transfer money to account identified by payto:// URI:": [
+ "Trasferisci fondi a un altro conto di questa banca:",
+ ],
+ "payto URI:": [""],
+ "payto address": ["indirizzo Payto"],
+ Send: [""],
+ "Use wire-transfer form?": ["Chiudi il bonifico"],
+ "No credentials found.": ["Credenziali invalide."],
+ "Could not create the wire transfer": [""],
+ "Transfer creation gave response error": [""],
+ "Wire transfer created!": ["Bonifico"],
+ "Amount to withdraw:": ["Somma da ritirare"],
+ Withdraw: ["Conferma il ritiro"],
+ "No credentials given.": ["Credenziali invalide."],
+ "Could not create withdrawal operation": [""],
+ "Withdrawal creation gave response error": [""],
+ "Obtain digital cash": [""],
+ "Transfer to bank account": [
+ "Trasferisci fondi a un altro conto di questa banca:",
+ ],
+ Date: [""],
+ Amount: ["Somma"],
+ Counterpart: ["Controparte"],
+ Subject: ["Causale"],
+ "Transfer to Taler Wallet": ["Ritira contante nel portafoglio Taler"],
+ "Use this QR code to withdraw to your mobile wallet:": [
+ "Usa questo codice QR per ritirare contante nel tuo wallet:",
+ ],
+ "Click %1$s to open your Taler wallet!": [""],
+ "Confirm Withdrawal": ["Conferma il ritiro"],
+ "Authorize withdrawal by solving challenge": [""],
+ "What is": [""],
+ "Answer is wrong.": [""],
+ Confirm: ["Conferma"],
+ Cancel: [""],
+ "A this point, a %1$s bank would ask for an additional authentication proof (PIN/TAN, one time password, ..), instead of a simple calculation.":
+ [""],
+ "No withdrawal ID found.": [""],
+ "Could not confirm the withdrawal": ["Conferma il ritiro"],
+ "Withdrawal confirmation gave response error": [""],
+ "Withdrawal confirmed!": ["Questo ritiro è stato annullato!"],
+ "Could not abort the withdrawal.": ["Chiudi il ritiro Taler"],
+ "Withdrawal abortion failed.": ["Questo ritiro è stato annullato!"],
+ "Withdrawal aborted!": ["Questo ritiro è stato annullato!"],
+ Abort: ["Annulla"],
+ "withdrawal (%1$s) was never (correctly) created at the bank...": [""],
+ "Waiting the bank to create the operation...": [
+ "La banca sta creando l'operazione...",
+ ],
+ "This withdrawal was aborted!": ["Questo ritiro è stato annullato!"],
+ "Welcome to %1$s!": [""],
+ "Username or account label '%1$s' not found. Won't login.": [
+ "L'utente '%1$s' non esiste. Login impossibile",
+ ],
+ "Wrong credentials given.": ["Credenziali invalide."],
+ "Account information could not be retrieved.": [
+ "Impossibile ricevere le informazioni relative al conto.",
+ ],
+ "Welcome, %1$s !": [""],
+ "Bank account balance": ["Bilancio:"],
+ Payments: [""],
+ "Latest transactions:": ["Ultime transazioni:"],
+ "List of public accounts was not found.": [
+ "Lista conti pubblici non trovata.",
+ ],
+ "List of public accounts could not be retrieved.": [
+ "Lista conti pubblici non pervenuta.",
+ ],
+ "History of public accounts": ["Storico dei conti pubblici"],
+ "Currently, the bank is not accepting new registrations!": [""],
+ "Use only letter and numbers starting with a lower case letter": [""],
+ "Password don't match": [""],
+ "Please register!": ["Accedi!"],
+ "Repeat Password:": [""],
+ "Registration failed, please report": ["Registrazione"],
+ "That username is already taken": [""],
+ "New registration gave response error": [""],
+ "Bank menu": [""],
+ "Select option1": [""],
+ "Select option2": [""],
+ days: [""],
+ hours: [""],
+ minutes: [""],
+ seconds: [""],
+ },
+ },
+};
diff --git a/packages/aml-backoffice-ui/src/index.html b/packages/aml-backoffice-ui/src/index.html
new file mode 100644
index 000000000..c1de73520
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/index.html
@@ -0,0 +1,42 @@
+<!--
+ This file is part of GNU Taler
+ (C) 2021--2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+
+ @author Sebastian Javier Marchano
+-->
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
+ <meta name="taler-support" content="uri">
+ <meta name="mobile-web-app-capable" content="yes" />
+ <meta name="apple-mobile-web-app-capable" content="yes" />
+ <link rel="icon"
+ href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/Qn3//3ryn/795Tf/WrpP/2LCW/8B6T//w4Nb///////Pn4P+/d0v/9u3n/+7d0v/EhV7//v///+HDr//fxLD/zph2/+TJt//8/Pv/woBX//Lm3f/y5dz/v3hN//bu6f/JjGn/4sW0///////Df1j/8OLZ//v6+P+/elH/+vj1//jy7f+/elL//////+zYzP/Eg13//////967p//MlHT/wn5X///////v4Nb/yY1s///////jw7H/06KG////////////z5t9/+fNvf//////x4pn//Pp4v/8+vn/w39X/8WEX///////5s/A/9CbfP//////27Oc/9y2n////////////9itlf/gu6f//////86Vdf/r2Mz//////8SCXP/Df1j//////+7d0v/KkG7//////+HBrf/VpYr////////////RnoH/5sq6///////Ii2n/8ubf//39/P/Cf1j/xohk/+bNvv//////wn5W//Tq4//58/D/wHxV//7+/f/59fH/v3xU//39/P/w4Nf/xIFb///////hw7H/yo9t/+/f1f/AeU3/+/n2/+nSxP/FhmD//////9qzm//Upon/4MSx/96+qf//////xINc/+3bz//48e3/v3hN//Pn3///////6M+//752S//gw6//06aK/8J+VP/kzLr/zZd1/8OCWv/q18r/17KZ/9Ooi//fv6r/v3dK/+vWyP///////v39///////27un/1aeK/9Opjv/m1cf/1KCC/9a0nP/n08T/0Jx8/82YdP/QnHz/16yR//jx7P///////v39///////+/f3///7+///////+//7//v7+///////+/v7//v/+/////////////////////////v7//v79///////////////////+/v/+/Pv//v39///+/v/+/Pv///7+//7+/f/+/Pv//v39//79/P/+/Pv///7+////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" />
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
+ <title>Exchange Backoffice</title>
+ <!-- Entry point for the SPA. -->
+ <script type="module" src="forms.js"></script>
+ <script type="module" src="index.js"></script>
+ <link rel="stylesheet" href="index.css" />
+</head>
+
+<body>
+ <div id="app"></div>
+</body>
+
+</html>
diff --git a/packages/aml-backoffice-ui/src/index.tsx b/packages/aml-backoffice-ui/src/index.tsx
new file mode 100644
index 000000000..c2ac4c84b
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/index.tsx
@@ -0,0 +1,22 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { App } from "./App.js";
+import { h, render } from "preact";
+
+const app = document.getElementById("app");
+
+render(<App />, app as any);
diff --git a/packages/aml-backoffice-ui/src/pages.ts b/packages/aml-backoffice-ui/src/pages.ts
new file mode 100644
index 000000000..109cd31d0
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/pages.ts
@@ -0,0 +1,44 @@
+import { TranslatedString } from "@gnu-taler/taler-util";
+import { AntiMoneyLaunderingForm } from "./pages/AntiMoneyLaunderingForm.js";
+import { CaseDetails } from "./pages/CaseDetails.js";
+import { Cases, HomeIcon, PeopleIcon } from "./pages/Cases.js";
+import { NewFormEntry } from "./pages/NewFormEntry.js";
+import { Officer } from "./pages/Officer.js";
+import { PageEntry, pageDefinition } from "./route.js";
+// import homeLogo from "./assets/home.svg";
+// import peopleLogo from "./assets/people.svg";
+const cases: PageEntry = {
+ url: "#/cases",
+ view: Cases,
+ name: "Cases" as TranslatedString,
+ Icon: HomeIcon,
+};
+
+const officer: PageEntry = {
+ url: "#/officer",
+ view: Officer,
+ name: "Officer" as TranslatedString,
+ Icon: PeopleIcon,
+};
+
+const account: PageEntry<{ account: string }> = {
+ url: pageDefinition("#/account/:account"),
+ view: CaseDetails,
+ name: "Account" as TranslatedString,
+ // icon: () => undefined,
+};
+
+const newFormEntry: PageEntry<{ account?: string; type?: string }> = {
+ url: pageDefinition("#/account/:account/new/:type?"),
+ view: NewFormEntry,
+ name: "New Form" as TranslatedString,
+ // icon: () => undefined,
+};
+
+
+export const Pages = {
+ cases,
+ officer,
+ account,
+ newFormEntry,
+};
diff --git a/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.stories.tsx b/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.stories.tsx
new file mode 100644
index 000000000..0b055f682
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.stories.tsx
@@ -0,0 +1,104 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import * as tests from "@gnu-taler/web-util/testing";
+import {
+ AntiMoneyLaunderingForm as TestedComponent,
+} from "./AntiMoneyLaunderingForm.js";
+
+export default {
+ title: "aml form",
+};
+
+export const SimpleComment = tests.createExample(TestedComponent, {
+ account: "the_account",
+ formId: "simple_comment",
+ onSubmit: async (justification, newState, newThreshold) => {
+ alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 2))
+ }
+});
+
+export const Identification = tests.createExample(TestedComponent, {
+ account: "the_account",
+ formId: "902.1e",
+ onSubmit: async (justification, newState, newThreshold) => {
+ alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 2))
+ }
+});
+
+export const OperationalLegalEntity = tests.createExample(TestedComponent, {
+ account: "the_account",
+ formId: "902.11e",
+
+ onSubmit: async (justification, newState, newThreshold) => {
+ alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 2))
+ }
+});
+export const Foundations = tests.createExample(TestedComponent, {
+ account: "the_account",
+ formId: "902.12e",
+
+ onSubmit: async (justification, newState, newThreshold) => {
+ alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 2))
+ }
+});
+export const DelcarationOfTrusts = tests.createExample(TestedComponent, {
+ account: "the_account",
+ formId: "902.13e",
+
+ onSubmit: async (justification, newState, newThreshold) => {
+ alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 2))
+ }
+});
+
+export const InformationOnLifeInsurance = tests.createExample(TestedComponent, {
+ account: "the_account",
+ formId: "902.15e",
+
+ onSubmit: async (justification, newState, newThreshold) => {
+ alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 2))
+ }
+});
+export const DeclarationOfBeneficialOwner = tests.createExample(TestedComponent, {
+ account: "the_account",
+ formId: "902.9e",
+
+ onSubmit: async (justification, newState, newThreshold) => {
+ alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 2))
+ }
+});
+export const CustomerProfile = tests.createExample(TestedComponent, {
+ account: "the_account",
+ formId: "902.5e",
+
+ onSubmit: async (justification, newState, newThreshold) => {
+ alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 2))
+ }
+});
+export const RiskProfile = tests.createExample(TestedComponent, {
+ account: "the_account",
+ formId: "902.4e",
+
+ onSubmit: async (justification, newState, newThreshold) => {
+ alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 2))
+ }
+});
+
diff --git a/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.tsx b/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.tsx
new file mode 100644
index 000000000..c42b1e7af
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.tsx
@@ -0,0 +1,160 @@
+import {
+ AbsoluteTime,
+ AmountJson,
+ Amounts,
+ Codec,
+ OperationFail,
+ OperationOk,
+ OperationResult,
+ buildCodecForObject,
+ codecForNumber,
+ codecForString,
+ codecOptional,
+} from "@gnu-taler/taler-util";
+import {
+ DefaultForm,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { h } from "preact";
+import { useExchangeApiContext } from "../context/config.js";
+import { FormMetadata, uiForms } from "../forms/declaration.js";
+import { Pages } from "../pages.js";
+import { AmlExchangeBackend } from "../utils/types.js";
+
+export function AntiMoneyLaunderingForm({
+ account,
+ formId,
+ onSubmit,
+}: {
+ account: string;
+ formId: string;
+ onSubmit: (
+ justification: Justification,
+ state: AmlExchangeBackend.AmlState,
+ threshold: AmountJson,
+ ) => Promise<void>;
+}) {
+ const { i18n } = useTranslationContext();
+ const theForm = uiForms.forms(i18n).find((v) => v.id === formId);
+ if (!theForm) {
+ return <div>form with id {formId} not found</div>;
+ }
+
+ const { config } = useExchangeApiContext();
+
+ const initial = {
+ when: AbsoluteTime.now(),
+ state: AmlExchangeBackend.AmlState.pending,
+ threshold: Amounts.zeroOfCurrency(config.currency),
+ };
+ return (
+ <DefaultForm
+ initial={initial}
+ form={theForm.impl(initial)}
+ onUpdate={() => { }}
+ onSubmit={(formValue) => {
+ if (formValue.state === undefined || formValue.threshold === undefined)
+ return;
+ const st = formValue.state;
+ const amount = formValue.threshold;
+
+ const justification: Justification = {
+ id: theForm.id,
+ label: theForm.label,
+ version: theForm.version,
+ value: formValue,
+ };
+
+ onSubmit(justification, st, amount);
+ }}
+ >
+ <div class="mt-6 flex items-center justify-end gap-x-6">
+ <a
+ href={Pages.account.url({ account })}
+ class="text-sm font-semibold leading-6 text-gray-900"
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </a>
+ <button
+ type="submit"
+ class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </button>
+ </div>
+ </DefaultForm>
+ );
+}
+
+export type Justification<T = any> = {
+ // form values
+ value: T;
+} & Omit<Omit<FormMetadata<any>, "icon">, "impl">;
+
+export function stringifyJustification(j: Justification): string {
+ return JSON.stringify(j);
+}
+
+type SimpleFormMetadata = {
+ version?: number;
+ id?: string;
+};
+
+export const codecForSimpleFormMetadata = (): Codec<SimpleFormMetadata> =>
+ buildCodecForObject<SimpleFormMetadata>()
+ .property("id", codecOptional(codecForString()))
+ .property("version", codecOptional(codecForNumber()))
+ .build("SimpleFormMetadata");
+
+type ParseJustificationFail =
+ | "not-json"
+ | "id-not-found"
+ | "form-not-found"
+ | "version-not-found";
+
+export function parseJustification(
+ s: string,
+ listOfAllKnownForms: FormMetadata<any>[],
+): OperationOk<{ justification: Justification; metadata: FormMetadata<any> }> | OperationFail<ParseJustificationFail> {
+ try {
+ const justification = JSON.parse(s);
+ const info = codecForSimpleFormMetadata().decode(justification);
+ if (!info.id) {
+ return {
+ type: "fail",
+ case: "id-not-found",
+ detail: {} as any,
+ };
+ }
+ if (!info.version) {
+ return {
+ type: "fail",
+ case: "version-not-found",
+ detail: {} as any,
+ };
+ }
+ const found = listOfAllKnownForms.find((f) => {
+ return f.id === info.id && f.version === info.version;
+ });
+ if (!found) {
+ return {
+ type: "fail",
+ case: "form-not-found",
+ detail: {} as any,
+ };
+ }
+ return {
+ type: "ok",
+ body: {
+ justification,
+ metadata: found,
+ },
+ };
+ } catch (e) {
+ return {
+ type: "fail",
+ case: "not-json",
+ detail: {} as any,
+ };
+ }
+}
diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
new file mode 100644
index 000000000..0875f047b
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
@@ -0,0 +1,314 @@
+import {
+ AbsoluteTime,
+ AmountJson,
+ Amounts,
+ HttpStatusCode,
+ TalerError,
+ TranslatedString,
+ assertUnreachable
+} from "@gnu-taler/taler-util";
+import { DefaultForm, ErrorLoading, InternationalizationAPI, Loading, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { format } from "date-fns";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { FormMetadata } from "../forms/declaration.js";
+import { useCaseDetails } from "../hooks/useCaseDetails.js";
+import { Pages } from "../pages.js";
+import { Justification, parseJustification } from "./AntiMoneyLaunderingForm.js";
+import { ShowConsolidated } from "./ShowConsolidated.js";
+import { AmlExchangeBackend } from "../utils/types.js";
+import { uiForms } from "../forms/declaration.js";
+
+export type AmlEvent = AmlFormEvent | AmlFormEventError | KycCollectionEvent | KycExpirationEvent;
+type AmlFormEvent = {
+ type: "aml-form";
+ when: AbsoluteTime;
+ title: TranslatedString;
+ justification: Justification;
+ metadata: FormMetadata<any>;
+ state: AmlExchangeBackend.AmlState;
+ threshold: AmountJson;
+};
+type AmlFormEventError = {
+ type: "aml-form-error";
+ when: AbsoluteTime;
+ title: TranslatedString;
+ justification: undefined,
+ metadata: undefined,
+ state: AmlExchangeBackend.AmlState;
+ threshold: AmountJson;
+};
+type KycCollectionEvent = {
+ type: "kyc-collection";
+ when: AbsoluteTime;
+ title: TranslatedString;
+ values: object;
+ provider: string;
+};
+type KycExpirationEvent = {
+ type: "kyc-expiration";
+ when: AbsoluteTime;
+ title: TranslatedString;
+ fields: string[];
+};
+
+type WithTime = { when: AbsoluteTime };
+
+function selectSooner(a: WithTime, b: WithTime) {
+ return AbsoluteTime.cmp(a.when, b.when);
+}
+
+function titleForJustification(op: ReturnType<typeof parseJustification>, i18n: InternationalizationAPI): TranslatedString {
+ if (op.type === "ok") {
+ return op.body.justification.label as TranslatedString;
+ }
+ switch (op.case) {
+ case "not-json": return "error: the justification is not a form" as TranslatedString
+ case "id-not-found": return "error: justification form's id not found" as TranslatedString
+ case "version-not-found": return "error: justification form's version not found" as TranslatedString
+ case "form-not-found": return `error: justification form not found` as TranslatedString
+ default: {
+ assertUnreachable(op.case)
+ }
+ }
+}
+
+export function getEventsFromAmlHistory(
+ aml: AmlExchangeBackend.AmlDecisionDetail[],
+ kyc: AmlExchangeBackend.KycDetail[],
+ i18n: InternationalizationAPI,
+): AmlEvent[] {
+ const ae: AmlEvent[] = aml.map((a) => {
+
+ const just = parseJustification(a.justification, uiForms.forms(i18n))
+ return {
+ type: just.type === "ok" ? "aml-form" : "aml-form-error",
+ state: a.new_state,
+ threshold: Amounts.parseOrThrow(a.new_threshold),
+ title: titleForJustification(just, i18n),
+ metadata: just.type === "ok" ? just.body.metadata : undefined,
+ justification: just.type === "ok" ? just.body.justification : undefined,
+ when: {
+ t_ms:
+ a.decision_time.t_s === "never"
+ ? "never"
+ : a.decision_time.t_s * 1000,
+ },
+ } as AmlEvent;
+ });
+ const ke = kyc.reduce((prev, k) => {
+ prev.push({
+ type: "kyc-collection",
+ title: i18n.str`collection`,
+ when: AbsoluteTime.fromProtocolTimestamp(k.collection_time),
+ values: !k.attributes ? {} : k.attributes,
+ provider: k.provider_section,
+ });
+ prev.push({
+ type: "kyc-expiration",
+ title: i18n.str`expiration`,
+ when: AbsoluteTime.fromProtocolTimestamp(k.expiration_time),
+ fields: !k.attributes ? [] : Object.keys(k.attributes),
+ });
+ return prev;
+ }, [] as AmlEvent[]);
+ return ae.concat(ke).sort(selectSooner);
+}
+
+export function CaseDetails({ account }: { account: string }) {
+ const [selected, setSelected] = useState<AbsoluteTime>(AbsoluteTime.now());
+ const [showForm, setShowForm] = useState<{ justification: Justification, metadata: FormMetadata<any> }>()
+
+ const { i18n } = useTranslationContext();
+ const details = useCaseDetails(account)
+ if (!details) {
+ return <Loading />
+ }
+ if (details instanceof TalerError) {
+ return <ErrorLoading error={details} />
+ }
+ if (details.type === "fail") {
+ switch (details.case) {
+ case HttpStatusCode.Unauthorized:
+ case HttpStatusCode.Forbidden:
+ case HttpStatusCode.NotFound:
+ case HttpStatusCode.Conflict: return <div />
+ default: assertUnreachable(details)
+ }
+ }
+ const { aml_history, kyc_attributes } = details.body
+
+ const events = getEventsFromAmlHistory(aml_history, kyc_attributes, i18n);
+
+ if (showForm !== undefined) {
+ return <DefaultForm
+ readOnly={true}
+ initial={showForm.justification.value}
+ form={showForm.metadata.impl(showForm.justification.value)}
+ >
+ <div class="mt-6 flex items-center justify-end gap-x-6">
+ <button
+ class="text-sm font-semibold leading-6 text-gray-900"
+ onClick={() => {
+ setShowForm(undefined)
+ }}
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ </div>
+
+ </DefaultForm>
+ }
+ return (
+ <div>
+ <a
+ href={Pages.newFormEntry.url({ account })}
+ class="m-4 block rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700"
+ >
+ <i18n.Translate>
+ New AML form
+ </i18n.Translate>
+ </a>
+
+ <header class="flex items-center justify-between border-b border-white/5 px-4 py-4 sm:px-6 sm:py-6 lg:px-8">
+ <h1 class="text-base font-semibold leading-7 text-black">
+ <i18n.Translate>
+ Case history for account <span title={account}>{account.substring(0, 16)}...</span>
+ </i18n.Translate>
+ </h1>
+ </header>
+ <ShowTimeline history={events} onSelect={(e) => {
+ switch (e.type) {
+ case "aml-form": {
+ const { justification, metadata } = e
+ setShowForm({ justification, metadata })
+ break;
+ }
+ case "kyc-collection":
+ case "kyc-expiration": {
+ setSelected(e.when);
+ break;
+ }
+ case "aml-form-error":
+ }
+ }} />
+ {/* {selected && <ShowEventDetails event={selected} />} */}
+ {selected && <ShowConsolidated history={events} until={selected} />}
+ </div>
+ );
+}
+
+function AmlStateBadge({ state }: { state: AmlExchangeBackend.AmlState }): VNode {
+ switch (state) {
+ case AmlExchangeBackend.AmlState.normal: {
+ return (
+ <span class="inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20">
+ Normal
+ </span>
+ );
+ }
+ case AmlExchangeBackend.AmlState.pending: {
+ return (
+ <span class="inline-flex items-center rounded-md bg-yellow-50 px-2 py-1 text-xs font-medium text-yellow-700 ring-1 ring-inset ring-green-600/20">
+ Pending
+ </span>
+ );
+ }
+ case AmlExchangeBackend.AmlState.frozen: {
+ return (
+ <span class="inline-flex items-center rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-green-600/20">
+ Frozen
+ </span>
+ );
+ }
+ }
+ assertUnreachable(state)
+}
+
+function ShowTimeline({ history, onSelect }: { onSelect: (e: AmlEvent) => void, history: AmlEvent[] }): VNode {
+ return <div class="flow-root">
+ <ul role="list">
+ {history.map((e, idx) => {
+ const isLast = history.length - 1 === idx;
+ return (
+ <li
+ data-ok={e.type !== "aml-form-error"}
+ class="hover:bg-gray-200 p-2 rounded data-[ok=true]:cursor-pointer"
+ onClick={() => {
+ onSelect(e);
+ }}
+ >
+ <div class="relative pb-6">
+ {!isLast ? (
+ <span
+ class="absolute left-4 top-4 -ml-px h-full w-1 bg-gray-200"
+ aria-hidden="true"
+ ></span>
+ ) : undefined}
+ <div class="relative flex space-x-3">
+ {(() => {
+ switch (e.type) {
+ case "aml-form-error":
+ case "aml-form": {
+ return <div>
+ <AmlStateBadge state={e.state} />
+ <span class="inline-flex items-center px-2 py-1 text-xs font-medium text-gray-700 ">
+ {e.threshold.currency}{" "}
+ {Amounts.stringifyValue(e.threshold)}
+ </span>
+ </div>
+ }
+ case "kyc-collection": {
+ return (
+ // <ArrowDownCircleIcon class="h-8 w-8 text-green-700" />
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75l3 3m0 0l3-3m-3 3v-7.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
+ </svg>
+ );
+ }
+ case "kyc-expiration": {
+ // return <ClockIcon class="h-8 w-8 text-gray-700" />;
+ return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
+ </svg>
+
+ }
+ }
+ assertUnreachable(e)
+ })()}
+ <div class="flex min-w-0 flex-1 justify-between space-x-4 pt-1.5">
+ {e.type === "aml-form" ?
+ <span
+ // href={Pages.newFormEntry.url({ account })}
+ class="block rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700"
+ >
+ {e.title}
+ </span>
+ :
+ <p class="text-sm text-gray-900">{e.title}</p>
+ }
+ <div class="whitespace-nowrap text-right text-sm text-gray-500">
+ {e.when.t_ms === "never" ? (
+ "never"
+ ) : (
+ <time dateTime={format(e.when.t_ms, "dd MMM yyyy")}>
+ {format(e.when.t_ms, "dd MMM yyyy")}
+ </time>
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ </li>
+ );
+ })}
+ </ul>
+ </div>
+
+}
+
+function ShowEventDetails({ event }: { event: AmlEvent }): VNode {
+ return <div>type {event.type}</div>;
+}
+
+
diff --git a/packages/taler-wallet-webextension/src/cta/Tip/stories.tsx b/packages/aml-backoffice-ui/src/pages/Cases.stories.tsx
index 86bdd27a9..3b9c8dacf 100644
--- a/packages/taler-wallet-webextension/src/cta/Tip/stories.tsx
+++ b/packages/aml-backoffice-ui/src/pages/Cases.stories.tsx
@@ -19,28 +19,24 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { Amounts } from "@gnu-taler/taler-util";
-import { createExample } from "../../test-utils.js";
-import { AcceptedView, ReadyView } from "./views.js";
+import * as tests from "@gnu-taler/web-util/testing";
+import {
+ CasesUI as TestedComponent,
+} from "./Cases.js";
+import { AmountString } from "@gnu-taler/taler-util";
+import { AmlExchangeBackend } from "../utils/types.js";
export default {
- title: "tip",
+ title: "cases",
};
-export const Accepted = createExample(AcceptedView, {
- status: "accepted",
- error: undefined,
- amount: Amounts.parseOrThrow("EUR:1"),
- exchangeBaseUrl: "",
- merchantBaseUrl: "",
-});
-
-export const Ready = createExample(ReadyView, {
- status: "ready",
- error: undefined,
- amount: Amounts.parseOrThrow("EUR:1"),
- merchantBaseUrl: "http://merchant.url/",
- exchangeBaseUrl: "http://exchange.url/",
- accept: {},
- cancel: {},
+export const OneRow = tests.createExample(TestedComponent, {
+ filter: AmlExchangeBackend.AmlState.normal,
+ onChangeFilter: () => null,
+ records: [{
+ current_state: AmlExchangeBackend.AmlState.normal,
+ h_payto: "QWEQWEQWEQWE",
+ rowid: 1,
+ threshold: "USD:1" as AmountString
+ }]
});
diff --git a/packages/aml-backoffice-ui/src/pages/Cases.tsx b/packages/aml-backoffice-ui/src/pages/Cases.tsx
new file mode 100644
index 000000000..061286f51
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/pages/Cases.tsx
@@ -0,0 +1,287 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ HttpStatusCode,
+ TalerError,
+ TalerExchangeApi,
+ assertUnreachable
+} from "@gnu-taler/taler-util";
+import {
+ ErrorLoading,
+ Loading,
+ createNewForm,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { useCases } from "../hooks/useCases.js";
+import { Pages } from "../pages.js";
+
+import { amlStateConverter } from "../utils/converter.js";
+import { AmlExchangeBackend } from "../utils/types.js";
+import { Officer } from "./Officer.js";
+
+export function CasesUI({
+ records,
+ filter,
+ onChangeFilter,
+ onFirstPage,
+ onNext,
+}: {
+ onFirstPage?: () => void;
+ onNext?: () => void;
+ filter: AmlExchangeBackend.AmlState;
+ onChangeFilter: (f: AmlExchangeBackend.AmlState) => void;
+ records: TalerExchangeApi.AmlRecord[];
+}): VNode {
+ const { i18n } = useTranslationContext();
+
+ const form = createNewForm<{ state: AmlExchangeBackend.AmlState }>();
+
+ return (
+ <div>
+ <div class="sm:flex sm:items-center">
+ <div class="px-2 sm:flex-auto">
+ <h1 class="text-base font-semibold leading-6 text-gray-900">
+ <i18n.Translate>Cases</i18n.Translate>
+ </h1>
+ <p class="mt-2 text-sm text-gray-700 w-80">
+ <i18n.Translate>
+ A list of all the account with the status
+ </i18n.Translate>
+ </p>
+ </div>
+ <div class="px-2">
+ <form.Provider
+ initial={{ state: filter }}
+ onUpdate={(v) => {
+ onChangeFilter(v.state ?? filter);
+ }}
+ onSubmit={(_v) => { }}
+ >
+ <form.InputChoiceHorizontal
+ name="state"
+ label={i18n.str`Filter`}
+ converter={amlStateConverter}
+ choices={[
+ {
+ label: i18n.str`Pending`,
+ value: AmlExchangeBackend.AmlState.pending,
+ },
+ {
+ label: i18n.str`Frozen`,
+ value: AmlExchangeBackend.AmlState.frozen,
+ },
+ {
+ label: i18n.str`Normal`,
+ value: AmlExchangeBackend.AmlState.normal,
+ },
+ ]}
+ />
+ </form.Provider>
+ </div>
+ </div>
+ <div class="mt-8 flow-root">
+ <div class="overflow-x-auto">
+ {!records.length ? (
+ <div>empty result </div>
+ ) : (
+ <div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
+ <table class="min-w-full divide-y divide-gray-300">
+ <thead>
+ <tr>
+ <th
+ scope="col"
+ class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 w-80"
+ >
+ <i18n.Translate>Account Id</i18n.Translate>
+ </th>
+ <th
+ scope="col"
+ class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 w-40"
+ >
+ <i18n.Translate>Status</i18n.Translate>
+ </th>
+ <th
+ scope="col"
+ class="sm:hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 w-40"
+ >
+ <i18n.Translate>Threshold</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody class="divide-y divide-gray-200 bg-white">
+ {records.map((r) => {
+ return (
+ <tr key={r.h_payto} class="hover:bg-gray-100 ">
+ <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-500 ">
+ <div class="text-gray-900">
+ <a
+ href={Pages.account.url({ account: r.h_payto })}
+ class="text-indigo-600 hover:text-indigo-900"
+ >
+ {r.h_payto.substring(0, 16)}...
+ </a>
+ </div>
+ </td>
+ <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-500">
+ {((state: AmlExchangeBackend.AmlState): VNode => {
+ switch (state) {
+ case AmlExchangeBackend.AmlState.normal: {
+ return (
+ <span class="inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20">
+ Normal
+ </span>
+ );
+ }
+ case AmlExchangeBackend.AmlState.pending: {
+ return (
+ <span class="inline-flex items-center rounded-md bg-yellow-50 px-2 py-1 text-xs font-medium text-yellow-700 ring-1 ring-inset ring-green-600/20">
+ Pending
+ </span>
+ );
+ }
+ case AmlExchangeBackend.AmlState.frozen: {
+ return (
+ <span class="inline-flex items-center rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-green-600/20">
+ Frozen
+ </span>
+ );
+ }
+ }
+ })(r.current_state)}
+ </td>
+ <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-900">
+ {r.threshold}
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ <Pagination onFirstPage={onFirstPage} onNext={onNext} />
+ </div>
+ )}
+ </div>
+ </div>
+ </div>
+ );
+}
+
+export function Cases() {
+
+ const [stateFilter, setStateFilter] = useState(
+ AmlExchangeBackend.AmlState.pending,
+ );
+
+ const list = useCases(stateFilter);
+
+ if (!list) {
+ return <Loading />;
+ }
+ if (list instanceof TalerError) {
+ return <ErrorLoading error={list} />;
+ }
+
+ if (list.type === "fail") {
+ switch (list.case) {
+ case HttpStatusCode.Unauthorized:
+ case HttpStatusCode.Forbidden:
+ case HttpStatusCode.NotFound:
+ case HttpStatusCode.Conflict:
+ return <Officer />;
+ default:
+ assertUnreachable(list);
+ }
+ }
+
+ return (
+ <CasesUI
+ records={list.body}
+ onFirstPage={list.isFirstPage ? undefined : list.loadFirst}
+ onNext={list.isLastPage ? undefined : list.loadNext}
+ filter={stateFilter}
+ onChangeFilter={setStateFilter}
+ />
+ );
+}
+
+export const PeopleIcon = () => (
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="1.5"
+ stroke="currentColor"
+ class="w-6 h-6"
+ >
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z"
+ />
+ </svg>
+);
+
+export const HomeIcon = () => (
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="1.5"
+ stroke="currentColor"
+ class="w-6 h-6"
+ >
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"
+ />
+ </svg>
+);
+
+function Pagination({
+ onFirstPage,
+ onNext,
+}: {
+ onFirstPage?: () => void;
+ onNext?: () => void;
+}) {
+ const { i18n } = useTranslationContext();
+ return (
+ <nav
+ class="flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6 rounded-lg"
+ aria-label="Pagination"
+ >
+ <div class="flex flex-1 justify-between sm:justify-end">
+ <button
+ 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"
+ disabled={!onFirstPage}
+ onClick={onFirstPage}
+ >
+ <i18n.Translate>First page</i18n.Translate>
+ </button>
+ <button
+ class="relative disabled:bg-gray-100 disabled:text-gray-500 ml-3 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"
+ disabled={!onNext}
+ onClick={onNext}
+ >
+ <i18n.Translate>Next</i18n.Translate>
+ </button>
+ </div>
+ </nav>
+ );
+}
diff --git a/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx b/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx
new file mode 100644
index 000000000..603813f8e
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx
@@ -0,0 +1,106 @@
+import { TranslatedString } from "@gnu-taler/taler-util";
+import {
+ createNewForm,
+ notifyError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
+import { useSettings } from "../hooks/useSettings.js";
+
+export function CreateAccount({
+ onNewAccount,
+}: {
+ onNewAccount: (password: string) => void;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const Form = createNewForm<{
+ password: string;
+ repeat: string;
+ }>();
+ const [settings] = useSettings()
+
+ return (
+ <div class="flex min-h-full flex-col ">
+ <div class="sm:mx-auto sm:w-full sm:max-w-md">
+ <h2 class="mt-6 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">
+ <i18n.Translate>Create account</i18n.Translate>
+ </h2>
+ </div>
+
+ <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px] ">
+ <div class="bg-gray-100 px-6 py-6 shadow sm:rounded-lg sm:px-12">
+ <Form.Provider
+ computeFormState={(v) => {
+ return {
+ password: {
+ error: !v.password
+ ? i18n.str`required`
+ : settings.allowInsecurePassword
+ ? undefined
+ : v.password.length < 8
+ ? i18n.str`should have at least 8 characters`
+ : !v.password.match(/[a-z]/) && v.password.match(/[A-Z]/)
+ ? i18n.str`should have lowercase and uppercase characters`
+ : !v.password.match(/\d/)
+ ? i18n.str`should have numbers`
+ : !v.password.match(/[^a-zA-Z\d]/)
+ ? i18n.str`should have at least one character which is not a number or letter`
+ : undefined,
+ },
+ repeat: {
+ error: !v.repeat
+ ? i18n.str`required`
+ : v.repeat !== v.password
+ ? i18n.str`doesn't match`
+ : undefined,
+ },
+ };
+ }}
+ onSubmit={async (v, s) => {
+ const error = s?.password?.error ?? s?.repeat?.error;
+ if (error) {
+ notifyError(
+ i18n.str`Can't create account`,
+ error as TranslatedString,
+ );
+ } else {
+ onNewAccount(v.password!);
+ }
+ }}
+ >
+ <div class="mb-4">
+ <Form.InputLine
+ label={i18n.str`Password`}
+ name="password"
+ type="password"
+ help={
+ settings.allowInsecurePassword
+ ? i18n.str`short password are insecure, turn off insecure password in settings`
+ : i18n.str`lower and upper case letters, number and special character`
+ }
+ required
+ />
+ </div>
+ <div class="mb-4">
+ <Form.InputLine
+ label={i18n.str`Repeat password`}
+ name="repeat"
+ type="password"
+ required
+ />
+ </div>
+
+ <div class="mt-8">
+ <button
+ type="submit"
+ class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 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>Create</i18n.Translate>
+ </button>
+ </div>
+ </Form.Provider>
+ </div>
+ </div>
+ </div>
+ );
+}
diff --git a/packages/aml-backoffice-ui/src/pages/HandleAccountNotReady.tsx b/packages/aml-backoffice-ui/src/pages/HandleAccountNotReady.tsx
new file mode 100644
index 000000000..ff800ebdc
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/pages/HandleAccountNotReady.tsx
@@ -0,0 +1,35 @@
+import { VNode, h } from "preact";
+import { OfficerNotReady } from "../hooks/useOfficer.js";
+import { CreateAccount } from "./CreateAccount.js";
+import { UnlockAccount } from "./UnlockAccount.js";
+import { assertUnreachable } from "@gnu-taler/taler-util";
+
+export function HandleAccountNotReady({
+ officer,
+}: {
+ officer: OfficerNotReady;
+}): VNode {
+ if (officer.state === "not-found") {
+ return (
+ <CreateAccount
+ onNewAccount={(password) => {
+ officer.create(password);
+ }}
+ />
+ );
+ }
+
+ if (officer.state === "locked") {
+ return (
+ <UnlockAccount
+ onRemoveAccount={() => {
+ officer.forget();
+ }}
+ onAccountUnlocked={async (pwd) => {
+ await officer.tryUnlock(pwd);
+ }}
+ />
+ );
+ }
+ assertUnreachable(officer)
+}
diff --git a/packages/aml-backoffice-ui/src/pages/NewFormEntry.tsx b/packages/aml-backoffice-ui/src/pages/NewFormEntry.tsx
new file mode 100644
index 000000000..df97cc3a4
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/pages/NewFormEntry.tsx
@@ -0,0 +1,104 @@
+import { AbsoluteTime, Amounts, HttpStatusCode, TalerExchangeApi, TalerProtocolTimestamp, TranslatedString } from "@gnu-taler/taler-util";
+import { LocalNotificationBanner, useLocalNotification, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useExchangeApiContext } from "../context/config.js";
+import { useOfficer } from "../hooks/useOfficer.js";
+import { Pages } from "../pages.js";
+import { AntiMoneyLaunderingForm } from "./AntiMoneyLaunderingForm.js";
+import { HandleAccountNotReady } from "./HandleAccountNotReady.js";
+import { uiForms } from "../forms/declaration.js";
+
+export function NewFormEntry({
+ account,
+ type,
+}: {
+ account?: string;
+ type?: string;
+}): VNode {
+ const { i18n } = useTranslationContext()
+ const officer = useOfficer();
+ const { api } = useExchangeApiContext()
+ const [notification, notify, handleError] = useLocalNotification()
+
+ if (!account) {
+ return <div>no account</div>;
+ }
+ if (!type) {
+ return <SelectForm account={account} />;
+ }
+ if (officer.state !== "ready") {
+ return <HandleAccountNotReady officer={officer} />;
+ }
+
+ return (
+ <Fragment>
+ <LocalNotificationBanner notification={notification} />
+
+ <AntiMoneyLaunderingForm
+ account={account}
+ formId={type}
+ onSubmit={async (justification, new_state, new_threshold) => {
+
+ const decision: Omit<TalerExchangeApi.AmlDecision, "officer_sig"> = {
+ justification: JSON.stringify(justification),
+ decision_time: TalerProtocolTimestamp.now(),
+ h_payto: account,
+ new_state,
+ new_threshold: Amounts.stringify(new_threshold),
+ kyc_requirements: undefined
+ }
+ await handleError(async () => {
+ const resp = await api.addDecisionDetails(officer.account, decision);
+ if (resp.type === "ok") {
+ window.location.href = Pages.cases.url;
+ return;
+ }
+ switch (resp.case) {
+ case HttpStatusCode.Forbidden:
+ case HttpStatusCode.Unauthorized: return notify({
+ type: "error",
+ title: i18n.str`Wrong credentials for "${officer.account}"`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ })
+ case HttpStatusCode.NotFound: return notify({
+ type: "error",
+ title: i18n.str`Officer or account not found`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ })
+ case HttpStatusCode.Conflict: return notify({
+ type: "error",
+ title: i18n.str`Officer disabled or more recent decision was already submitted.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ })
+ }
+ })
+ }}
+ />
+ </Fragment>
+ );
+}
+
+function SelectForm({ account }: { account: string }) {
+ const { i18n } = useTranslationContext()
+ return (
+ <div>
+ <pre>New form for account: {account.substring(0, 16)}...</pre>
+ {uiForms.forms(i18n).map((form, idx) => {
+ return (
+ <a
+ href={Pages.newFormEntry.url({ account, type: form.id })}
+ class="m-4 block rounded-md w-fit border-0 p-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-600"
+ >
+ {form.label}
+ </a>
+ );
+ })}
+ </div>
+ );
+}
diff --git a/packages/aml-backoffice-ui/src/pages/Officer.tsx b/packages/aml-backoffice-ui/src/pages/Officer.tsx
new file mode 100644
index 000000000..ec8327814
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/pages/Officer.tsx
@@ -0,0 +1,60 @@
+import { Fragment, h } from "preact";
+import { useOfficer } from "../hooks/useOfficer.js";
+import { HandleAccountNotReady } from "./HandleAccountNotReady.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { uiSettings } from "../settings.js";
+import { getInitialBackendBaseURL } from "../hooks/useBackend.js";
+
+export function Officer() {
+ const officer = useOfficer();
+ const { i18n } = useTranslationContext()
+ if (officer.state !== "ready") {
+ return <HandleAccountNotReady officer={officer} />;
+ }
+
+ const url = new URL(getInitialBackendBaseURL())
+ const signupEmail = uiSettings.signupEmail ?? `aml-signup@${url.hostname}`
+
+ return (
+ <div>
+ <h1 class="my-2 text-3xl font-bold tracking-tight text-gray-900 ">
+ <i18n.Translate>Public key</i18n.Translate>
+ </h1>
+ <div class="max-w-xl text-base leading-7 text-gray-700 lg:max-w-lg">
+ <p class="mt-6 font-mono break-all">{officer.account.id}</p>
+ </div>
+ <p>
+ <a
+ href={`mailto:${signupEmail}?subject=${encodeURIComponent("Request AML signup")}&body=${encodeURIComponent(`I want my AML account\n\n\nPubKey: ${officer.account.id}`)}`}
+ target="_blank"
+ rel="noreferrer"
+ class="m-4 block rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700"
+ >
+ <i18n.Translate>Request account activation</i18n.Translate>
+ </a>
+ </p>
+ <p>
+ <button
+ type="button"
+ onClick={() => {
+ officer.lock();
+ }}
+ class="m-4 block rounded-md border-0 bg-gray-200 px-3 py-2 text-center text-sm text-black shadow-sm "
+ >
+ <i18n.Translate>Lock account</i18n.Translate>
+ </button>
+ </p>
+ <p>
+ <button
+ type="button"
+ onClick={() => {
+ officer.forget();
+ }}
+ class="m-4 block rounded-md bg-red-600 px-3 py-2 text-center text-sm text-white shadow-sm hover:bg-red-500 "
+ >
+ <i18n.Translate>Forget account</i18n.Translate>
+ </button>
+ </p>
+ </div>
+ );
+}
diff --git a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.stories.tsx b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.stories.tsx
new file mode 100644
index 000000000..f985e6ff5
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.stories.tsx
@@ -0,0 +1,117 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { addDays } from "date-fns";
+import {
+ ShowConsolidated as TestedComponent,
+} from "./ShowConsolidated.js";
+import * as tests from "@gnu-taler/web-util/testing";
+import { getEventsFromAmlHistory } from "./CaseDetails.js";
+import { AbsoluteTime, Duration } from "@gnu-taler/taler-util";
+import { InternationalizationAPI } from "@gnu-taler/web-util/browser";
+
+export default {
+ title: "show consolidated",
+};
+
+const nullTranslator: InternationalizationAPI = {
+ str: (str: any) => str,
+ singular: (str: any) => str,
+ translate: (str: any) => str,
+ Translate: (str: any) => str,
+}
+
+export const WithEmptyHistory = tests.createExample(TestedComponent, {
+ history: getEventsFromAmlHistory([], [], nullTranslator),
+ until: AbsoluteTime.now()
+});
+
+export const WithSomeEvents = tests.createExample(TestedComponent, {
+ history: getEventsFromAmlHistory([
+ {
+ "decider_pub": "JD70N2XZ8FZKB7C146ZWR6XBDCS4Z84PJKJMPB73PMJ2B1X35ZFG",
+ "justification": "{\"index\":0,\"name\":\"Simple comment\",\"value\":{\"fullName\":\"loggedIn_user_fullname\",\"when\":{\"t_ms\":1700207199558},\"state\":1,\"threshold\":{\"currency\":\"STATER\",\"fraction\":0,\"value\":0},\"comment\":\"test\"}}",
+ "new_threshold": "STATER:0",
+ "new_state": 1,
+ "decision_time": {
+ "t_s": 1700208199
+ }
+ },
+ {
+ "decider_pub": "JD70N2XZ8FZKB7C146ZWR6XBDCS4Z84PJKJMPB73PMJ2B1X35ZFG",
+ "justification": "{\"index\":0,\"name\":\"Simple comment\",\"value\":{\"fullName\":\"loggedIn_user_fullname\",\"when\":{\"t_ms\":1700207199558},\"state\":1,\"threshold\":{\"currency\":\"STATER\",\"fraction\":0,\"value\":0},\"comment\":\"test\"}}",
+ "new_threshold": "STATER:0",
+ "new_state": 1,
+ "decision_time": {
+ "t_s": 1700208211
+ }
+ },
+ {
+ "decider_pub": "JD70N2XZ8FZKB7C146ZWR6XBDCS4Z84PJKJMPB73PMJ2B1X35ZFG",
+ "justification": "{\"index\":0,\"name\":\"Simple comment\",\"value\":{\"fullName\":\"loggedIn_user_fullname\",\"when\":{\"t_ms\":1700207199558},\"state\":1,\"threshold\":{\"currency\":\"STATER\",\"fraction\":0,\"value\":0},\"comment\":\"test\"}}",
+ "new_threshold": "STATER:0",
+ "new_state": 1,
+ "decision_time": {
+ "t_s": 1700208220
+ }
+ },
+ {
+ "decider_pub": "JD70N2XZ8FZKB7C146ZWR6XBDCS4Z84PJKJMPB73PMJ2B1X35ZFG",
+ "justification": "{\"index\":4,\"name\":\"Declaration for trusts (902.13e)\",\"value\":{\"fullName\":\"loggedIn_user_fullname\",\"when\":{\"t_ms\":1700208362854},\"state\":1,\"threshold\":{\"currency\":\"STATER\",\"fraction\":0,\"value\":0},\"contractingPartner\":\"f\",\"knownAs\":\"a\",\"trust\":{\"name\":\"b\",\"type\":\"discretionary\",\"revocability\":\"irrevocable\"}}}",
+ "new_threshold": "STATER:0",
+ "new_state": 1,
+ "decision_time": {
+ "t_s": 1700208385
+ }
+ },
+ {
+ "decider_pub": "6CD3J8XSKWQPFFDJY4SP4RK2D7T7WW7JRJDTXHNZY7YKGXDCE2QG",
+ "justification": "{\"id\":\"simple_comment\",\"label\":\"Simple comment\",\"version\":1,\"value\":{\"when\":{\"t_ms\":1700488420810},\"state\":1,\"threshold\":{\"currency\":\"STATER\",\"fraction\":0,\"value\":0},\"comment\":\"qwe\"}}",
+ "new_threshold": "STATER:0",
+ "new_state": 1,
+ "decision_time": {
+ "t_s": 1700488423
+ }
+ },
+ {
+ "decider_pub": "6CD3J8XSKWQPFFDJY4SP4RK2D7T7WW7JRJDTXHNZY7YKGXDCE2QG",
+ "justification": "{\"id\":\"simple_comment\",\"label\":\"Simple comment\",\"version\":1,\"value\":{\"when\":{\"t_ms\":1700488671251},\"state\":1,\"threshold\":{\"currency\":\"STATER\",\"fraction\":0,\"value\":0},\"comment\":\"asd asd asd \"}}",
+ "new_threshold": "STATER:0",
+ "new_state": 1,
+ "decision_time": {
+ "t_s": 1700488677
+ }
+ }
+ ], [{
+ collection_time: AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.subtractDuraction(AbsoluteTime.now(), Duration.fromPrettyString("1d"))
+ ),
+ expiration_time: { t_s: "never" },
+ provider_section: "asd",
+ attributes: {
+ email: "sebasjm@qwdde.com"
+ }
+ }], nullTranslator),
+ until: AbsoluteTime.now()
+});
+
+
+
diff --git a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx
new file mode 100644
index 000000000..ad350c0e6
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx
@@ -0,0 +1,173 @@
+import { AbsoluteTime, AmountJson, TranslatedString } from "@gnu-taler/taler-util";
+import { format } from "date-fns";
+import { Fragment, VNode, h } from "preact";
+import { AmlEvent } from "./CaseDetails.js";
+import { DefaultForm, FlexibleForm, UIFormField, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { amlStateConverter } from "../utils/converter.js";
+import { AmlExchangeBackend } from "../utils/types.js";
+
+export function ShowConsolidated({
+ history,
+ until,
+}: {
+ history: AmlEvent[];
+ until: AbsoluteTime;
+}): VNode {
+ const { i18n } = useTranslationContext();
+
+ const cons = getConsolidated(history, until);
+
+ const form: FlexibleForm<Consolidated> = {
+ behavior: (form) => {
+ return {
+ aml: {
+ threshold: {
+ hidden: !form.aml
+ },
+ since: {
+ hidden: !form.aml
+ },
+ state: {
+ hidden: !form.aml
+ }
+ }
+ };
+ },
+ design: [
+ {
+ title: i18n.str`AML`,
+ fields: [
+ {
+ type: "amount",
+ props: {
+ label: i18n.str`Threshold`,
+ name: "aml.threshold",
+ },
+ },
+ {
+ type: "choiceHorizontal",
+ props: {
+ label: i18n.str`State`,
+ name: "aml.state",
+ converter: amlStateConverter,
+ choices: [
+ {
+ label: i18n.str`Frozen`,
+ value: AmlExchangeBackend.AmlState.frozen,
+ },
+ {
+ label: i18n.str`Pending`,
+ value: AmlExchangeBackend.AmlState.pending,
+ },
+ {
+ label: i18n.str`Normal`,
+ value: AmlExchangeBackend.AmlState.normal,
+ },
+ ],
+ },
+ },
+ ],
+ },
+ Object.entries(cons.kyc).length > 0
+ ? {
+ title: i18n.str`KYC`,
+ fields: Object.entries(cons.kyc).map(([key, field]) => {
+ const result: UIFormField = {
+ type: "text",
+ props: {
+ label: key as TranslatedString,
+ name: `kyc.${key}.value`,
+ help: `${field.provider} since ${field.since.t_ms === "never"
+ ? "never"
+ : format(field.since.t_ms, "dd/MM/yyyy")
+ }` as TranslatedString,
+ },
+ };
+ return result;
+ }),
+ }
+ : undefined,
+ ],
+ };
+ return (
+ <Fragment>
+ <h1 class="text-base font-semibold leading-7 text-black">
+ Consolidated information {until.t_ms === "never"
+ ? ""
+ : `after ${format(until.t_ms, "dd MMMM yyyy")}`}
+ </h1>
+ <DefaultForm
+ key={`${String(Date.now())}`}
+ form={form}
+ initial={cons}
+ readOnly
+ onUpdate={() => { }}
+ />
+ </Fragment>
+ );
+}
+
+interface Consolidated {
+ aml: {
+ state: AmlExchangeBackend.AmlState;
+ threshold: AmountJson;
+ since: AbsoluteTime;
+ };
+ kyc: {
+ [field: string]: {
+ value: any;
+ provider: string;
+ since: AbsoluteTime;
+ };
+ };
+}
+
+function getConsolidated(
+ history: AmlEvent[],
+ when: AbsoluteTime,
+): Consolidated {
+ const initial: Consolidated = {
+ aml: {
+ state: AmlExchangeBackend.AmlState.normal,
+ threshold: {
+ currency: "ARS",
+ value: 1000,
+ fraction: 0,
+ },
+ since: AbsoluteTime.never()
+ },
+ kyc: {},
+ };
+ return history.reduce((prev, cur) => {
+ if (AbsoluteTime.cmp(when, cur.when) < 0) {
+ return prev;
+ }
+ switch (cur.type) {
+ case "kyc-expiration": {
+ cur.fields.forEach((field) => {
+ delete prev.kyc[field];
+ });
+ break;
+ }
+ case "aml-form": {
+ prev.aml = {
+ since: cur.when,
+ state: cur.state,
+ threshold: cur.threshold
+ }
+ break;
+ }
+ case "kyc-collection": {
+ Object.keys(cur.values).forEach((field) => {
+ prev.kyc[field] = {
+ value: (cur.values as any)[field],
+ provider: cur.provider,
+ since: cur.when,
+ };
+ });
+ break;
+ }
+ }
+ return prev;
+ }, initial);
+} \ No newline at end of file
diff --git a/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx b/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx
new file mode 100644
index 000000000..1b0342b12
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx
@@ -0,0 +1,79 @@
+import { TranslatedString, UnwrapKeyError } from "@gnu-taler/taler-util";
+import { createNewForm, notifyError, notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
+
+export function UnlockAccount({
+ onAccountUnlocked,
+ onRemoveAccount,
+}: {
+ onAccountUnlocked: (password: string) => Promise<void>;
+ onRemoveAccount: () => void;
+}): VNode {
+ const { i18n } = useTranslationContext()
+ const Form = createNewForm<{
+ password: string;
+ }>();
+
+ return (
+ <div class="flex min-h-full flex-col ">
+ <div class="sm:mx-auto sm:w-full sm:max-w-md">
+ <h1 class="mt-6 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">
+ <i18n.Translate>Account locked</i18n.Translate>
+ </h1>
+ <p class="mt-6 text-lg leading-8 text-gray-600">
+ <i18n.Translate>Your account is normally locked anytime you reload. To unlock type
+ your password again.</i18n.Translate>
+ </p>
+ </div>
+
+ <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px] ">
+ <div class="bg-gray-100 px-6 py-6 shadow sm:rounded-lg sm:px-12">
+ <Form.Provider
+ onSubmit={async (v) => {
+ try {
+ await onAccountUnlocked(v.password!);
+ notifyInfo(i18n.str`Account unlocked`);
+ } catch (e) {
+ if (e instanceof UnwrapKeyError) {
+ notifyError(
+ "Could not unlock account" as any,
+ e.message as any,
+ );
+ } else {
+ throw e;
+ }
+ }
+ }}
+ >
+ <div class="mb-4">
+ <Form.InputLine
+ label={i18n.str`Password`}
+ name="password"
+ type="password"
+ required
+ />
+ </div>
+
+ <div class="mt-8">
+ <button
+ type="submit"
+ class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 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>Unlock</i18n.Translate>
+ </button>
+ </div>
+ </Form.Provider>
+ </div>
+ <button
+ type="button"
+ onClick={() => {
+ onRemoveAccount();
+ }}
+ class="m-4 block rounded-md bg-red-600 px-3 py-2 text-center text-sm text-white shadow-sm hover:bg-red-500 "
+ >
+ <i18n.Translate>Forget account</i18n.Translate>
+ </button>
+ </div>
+ </div>
+ );
+}
diff --git a/packages/aml-backoffice-ui/src/pages/index.stories.ts b/packages/aml-backoffice-ui/src/pages/index.stories.ts
new file mode 100644
index 000000000..afe73227a
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/pages/index.stories.ts
@@ -0,0 +1,3 @@
+export * as a1 from "./ShowConsolidated.stories.js";
+export * as a2 from "./AntiMoneyLaunderingForm.stories.js";
+export * as a3 from "./Cases.stories.js";
diff --git a/packages/aml-backoffice-ui/src/route.ts b/packages/aml-backoffice-ui/src/route.ts
new file mode 100644
index 000000000..f515a590a
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/route.ts
@@ -0,0 +1,197 @@
+import { TranslatedString } from "@gnu-taler/taler-util";
+import { createHashHistory } from "history";
+import { ComponentChildren, h as create, createContext, VNode } from "preact";
+import { useContext, useEffect, useState } from "preact/hooks";
+
+type ContextType = {
+ onChange: (listener: () => void) => VoidFunction
+}
+const nullChangeListener = { onChange: () => () => { } }
+const Context = createContext<ContextType>(nullChangeListener);
+
+export const usePathChangeContext = (): ContextType => useContext(Context);
+
+export function HashPathProvider({ children }: { children: ComponentChildren }): VNode {
+ const history = createHashHistory();
+ return create(Context.Provider, { value: { onChange: history.listen }, children }, children)
+}
+
+type PageDefinition<DynamicPart extends Record<string, string>> = {
+ pattern: string;
+ (params: DynamicPart): string;
+};
+
+function replaceAll(
+ pattern: string,
+ vars: Record<string, string>,
+ values: Record<string, string>,
+): string {
+ let result = pattern;
+ for (const v in vars) {
+ result = result.replace(vars[v], !values[v] ? "" : values[v]);
+ }
+ return result;
+}
+
+export function pageDefinition<T extends Record<string, string>>(
+ pattern: string,
+): PageDefinition<T> {
+ const patternParams = pattern.match(/(:[\w?]*)/g);
+ if (!patternParams)
+ throw Error(
+ `page definition pattern ${pattern} doesn't have any parameter`,
+ );
+
+ const vars = patternParams.reduce((prev, cur) => {
+ const pName = cur.match(/(\w+)/g);
+
+ //skip things like :? in the path pattern
+ if (!pName || !pName[0]) return prev;
+ const name = pName[0];
+ return { ...prev, [name]: cur };
+ }, {} as Record<string, string>);
+
+ const f = (values: T): string => replaceAll(pattern, vars, values);
+ f.pattern = pattern;
+ return f;
+}
+
+export type PageEntry<T = unknown> = T extends Record<string, string>
+ ? {
+ url: PageDefinition<T>;
+ view: (props: T) => VNode;
+ name: TranslatedString,
+ Icon?: () => VNode,
+ }
+ : T extends unknown
+ ? {
+ url: string;
+ view: (props: {}) => VNode;
+ name: TranslatedString,
+ Icon?: () => VNode,
+ }
+ : never;
+
+export function Router({
+ pageList,
+ onNotFound,
+}: {
+ pageList: Array<PageEntry<any>>;
+ onNotFound: () => VNode;
+}): VNode {
+ const current = useCurrentLocation(pageList);
+ if (current !== undefined) {
+ return create(current.page.view, current.values);
+ }
+ return onNotFound();
+}
+
+type Location = {
+ page: PageEntry<any>;
+ path: string;
+ values: Record<string, string>;
+};
+export function useCurrentLocation(pageList: Array<PageEntry<any>>): Location | undefined {
+ const [currentLocation, setCurrentLocation] = useState<Location | null | undefined>(null);
+ const path = usePathChangeContext();
+ useEffect(() => {
+ return path.onChange(() => {
+ const result = doSync(window.location.hash, new URLSearchParams(window.location.search), pageList);
+ setCurrentLocation(result);
+ });
+ }, []);
+ if (currentLocation === null) {
+ return doSync(window.location.hash, new URLSearchParams(window.location.search), pageList);
+ }
+ return currentLocation;
+}
+
+export function useChangeLocation() {
+ const [location, setLocation] = useState(window.location.hash)
+ const path = usePathChangeContext()
+ useEffect(() => {
+ return path.onChange(() => {
+ setLocation(window.location.hash)
+ });
+ }, []);
+ return location;
+}
+
+/**
+ * Search path in the pageList
+ * get the values from the path found
+ * add params from searchParams
+ *
+ * @param path
+ * @param params
+ */
+export function doSync(path: string, params: URLSearchParams, pageList: Array<PageEntry<any>>): Location | undefined {
+ for (let idx = 0; idx < pageList.length; idx++) {
+ const page = pageList[idx];
+ if (typeof page.url === "string") {
+ if (page.url === path) {
+ const values: Record<string, string> = {};
+ params.forEach((v, k) => {
+ values[k] = v;
+ });
+ return { page, values, path };
+ }
+ } else {
+ const values = doestUrlMatchToRoute(path, page.url.pattern);
+ if (values !== undefined) {
+ params.forEach((v, k) => {
+ values[k] = v;
+ });
+ return { page, values, path };
+ }
+ }
+ }
+ return undefined;
+}
+
+function doestUrlMatchToRoute(
+ url: string,
+ route: string,
+): undefined | Record<string, string> {
+ const paramsPattern = /(?:\?([^#]*))?$/;
+ // const paramsPattern = /(?:\?([^#]*))?(#.*)?$/;
+ const params = url.match(paramsPattern);
+ const urlWithoutParams = url.replace(paramsPattern, "");
+
+ const result: Record<string, string> = {};
+ if (params && params[1]) {
+ const paramList = params[1].split("&");
+ for (let i = 0; i < paramList.length; i++) {
+ const idx = paramList[i].indexOf("=");
+ const name = paramList[i].substring(0, idx);
+ const value = paramList[i].substring(idx + 1);
+ result[decodeURIComponent(name)] = decodeURIComponent(value);
+ }
+ }
+ const urlSeg = urlWithoutParams.split("/");
+ const routeSeg = route.split("/");
+ let max = Math.max(urlSeg.length, routeSeg.length);
+ for (let i = 0; i < max; i++) {
+ if (routeSeg[i] && routeSeg[i].charAt(0) === ":") {
+ const param = routeSeg[i].replace(/(^:|[+*?]+$)/g, "");
+
+ const flags = (routeSeg[i].match(/[+*?]+$/) || EMPTY)[0] || "";
+ const plus = ~flags.indexOf("+");
+ const star = ~flags.indexOf("*");
+ const val = urlSeg[i] || "";
+
+ if (!val && !star && (flags.indexOf("?") < 0 || plus)) {
+ return undefined;
+ }
+ result[param] = decodeURIComponent(val);
+ if (plus || star) {
+ result[param] = urlSeg.slice(i).map(decodeURIComponent).join("/");
+ break;
+ }
+ } else if (routeSeg[i] !== urlSeg[i]) {
+ return undefined;
+ }
+ }
+ return result;
+}
+const EMPTY: Record<string, string> = {};
diff --git a/packages/aml-backoffice-ui/src/scss/main.css b/packages/aml-backoffice-ui/src/scss/main.css
new file mode 100644
index 000000000..b5c61c956
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/scss/main.css
@@ -0,0 +1,3 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
diff --git a/packages/taler-wallet-webextension/src/components/JustInDevMode.tsx b/packages/aml-backoffice-ui/src/settings.ts
index a29bea791..68f44b4df 100644
--- a/packages/taler-wallet-webextension/src/components/JustInDevMode.tsx
+++ b/packages/aml-backoffice-ui/src/settings.ts
@@ -13,15 +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 { ComponentChildren, Fragment, h, VNode } from "preact";
-import { useDevContext } from "../context/devContext.js";
-export function JustInDevMode({
- children,
-}: {
- children: ComponentChildren;
-}): VNode {
- const { devMode } = useDevContext();
- if (!devMode) return <Fragment />;
- return <Fragment>{children}</Fragment>;
+export interface UiSettings {
+ backendBaseURL?: string;
+ signupEmail?: string;
}
+
+/**
+ * Global settings for the UI.
+ */
+const defaultSettings: UiSettings = {
+};
+
+export const uiSettings: UiSettings =
+ "talerExchangeAmlSettings" in globalThis
+ ? (globalThis as any).talerExchangeAmlSettings
+ : defaultSettings;
diff --git a/packages/aml-backoffice-ui/src/stories.test.ts b/packages/aml-backoffice-ui/src/stories.test.ts
new file mode 100644
index 000000000..eca66cb18
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/stories.test.ts
@@ -0,0 +1,71 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { TalerExchangeApi, setupI18n } from "@gnu-taler/taler-util";
+import { parseGroupImport } from "@gnu-taler/web-util/browser";
+import * as tests from "@gnu-taler/web-util/testing";
+
+// import * as components from "./components/index.examples.js";
+import * as pages from "./pages/index.stories.js";
+
+import { ComponentChildren, Fragment, VNode, h as create } from "preact";
+import { ExchangeApiContextTesting } from "./context/config.js";
+// import { BackendStateProviderTesting } from "./context/backend.js";
+
+setupI18n("en", { en: {} });
+
+describe("All the examples:", () => {
+ const cms = parseGroupImport({ pages });
+ 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, DefaultTestingContext);
+ });
+ });
+ });
+ });
+ });
+ });
+});
+
+
+function DefaultTestingContext({
+ children,
+}: {
+ children: ComponentChildren;
+}): VNode {
+ const config: TalerExchangeApi.ExchangeVersionResponse = {
+ currency: "ARS",
+ currency_specification: {
+ alt_unit_names: {},
+ name: "ARS",
+ num_fractional_input_digits: 2,
+ num_fractional_normal_digits: 2,
+ num_fractional_trailing_zero_digits: 2
+ },
+ name: "taler-exchange",
+ supported_kyc_requirements: [],
+ version: "asd",
+ }
+ return create(ExchangeApiContextTesting, { config, children });
+}
diff --git a/packages/aml-backoffice-ui/src/stories.tsx b/packages/aml-backoffice-ui/src/stories.tsx
new file mode 100644
index 000000000..1aa6a44ac
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/stories.tsx
@@ -0,0 +1,66 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { strings } from "./i18n/strings.js";
+
+import * as pages from "./pages/index.stories.js";
+
+import { renderStories } from "@gnu-taler/web-util/browser";
+
+import "./scss/main.css";
+import { h, ComponentChildren, FunctionComponent, VNode } from "preact";
+import { ExchangeApiContextTesting } from "./context/config.js";
+
+function main(): void {
+ renderStories(
+ { pages },
+ {
+ strings,
+ getWrapperForGroup
+ },
+ );
+}
+
+function getWrapperForGroup(): FunctionComponent {
+ return function All({ children }: { children?: ComponentChildren }): VNode {
+ return <ExchangeApiContextTesting
+ config={{
+ currency: "ARS",
+ currency_specification: {
+ alt_unit_names: {},
+ name: "ARS",
+ num_fractional_input_digits: 2,
+ num_fractional_normal_digits: 2,
+ num_fractional_trailing_zero_digits: 2
+ },
+ name: "taler-exchange",
+ supported_kyc_requirements: [],
+ version: "asd",
+ }}>
+ {children}
+ </ExchangeApiContextTesting>
+ }
+}
+
+if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", main);
+} else {
+ main();
+}
diff --git a/packages/aml-backoffice-ui/src/utils/QR.tsx b/packages/aml-backoffice-ui/src/utils/QR.tsx
new file mode 100644
index 000000000..1dc1712b7
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/utils/QR.tsx
@@ -0,0 +1,54 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { 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: "left",
+ }}
+ >
+ <div
+ style={{
+ width: "50%",
+ minWidth: 200,
+ maxWidth: 300,
+ marginRight: "auto",
+ marginLeft: "auto",
+ }}
+ ref={divRef}
+ />
+ </div>
+ );
+}
diff --git a/packages/aml-backoffice-ui/src/utils/converter.ts b/packages/aml-backoffice-ui/src/utils/converter.ts
new file mode 100644
index 000000000..d2f05ed84
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/utils/converter.ts
@@ -0,0 +1,31 @@
+import { AmlExchangeBackend } from "./types.js";
+
+export const amlStateConverter = {
+ toStringUI: stringifyAmlState,
+ fromStringUI: parseAmlState,
+};
+
+function stringifyAmlState(s: AmlExchangeBackend.AmlState | undefined): string {
+ if (s === undefined) return "";
+ switch (s) {
+ case AmlExchangeBackend.AmlState.normal:
+ return "normal";
+ case AmlExchangeBackend.AmlState.pending:
+ return "pending";
+ case AmlExchangeBackend.AmlState.frozen:
+ return "frozen";
+ }
+}
+
+function parseAmlState(s: string | undefined): AmlExchangeBackend.AmlState {
+ switch (s) {
+ case "normal":
+ return AmlExchangeBackend.AmlState.normal;
+ case "pending":
+ return AmlExchangeBackend.AmlState.pending;
+ case "frozen":
+ return AmlExchangeBackend.AmlState.frozen;
+ default:
+ throw Error(`unknown AML state: ${s}`);
+ }
+}
diff --git a/packages/aml-backoffice-ui/src/utils/types.ts b/packages/aml-backoffice-ui/src/utils/types.ts
new file mode 100644
index 000000000..fd70d4e4d
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/utils/types.ts
@@ -0,0 +1,124 @@
+export namespace AmlExchangeBackend {
+ // FIXME: placeholder
+ export interface AmlError {
+ code: number;
+ hint: string;
+ }
+ export interface AmlDecisionDetails {
+ // Array of AML decisions made for this account. Possibly
+ // contains only the most recent decision if "history" was
+ // not set to 'true'.
+ aml_history: AmlDecisionDetail[];
+
+ // Array of KYC attributes obtained for this account.
+ kyc_attributes: KycDetail[];
+ }
+
+ type AmlOfficerPublicKeyP = string;
+
+ export interface AmlDecisionDetail {
+ // What was the justification given?
+ justification: string;
+
+ // What is the new AML state.
+ new_state: Integer;
+
+ // When was this decision made?
+ decision_time: Timestamp;
+
+ // What is the new AML decision threshold (in monthly transaction volume)?
+ new_threshold: Amount;
+
+ // Who made the decision?
+ decider_pub: AmlOfficerPublicKeyP;
+ }
+ export interface KycDetail {
+ // Name of the configuration section that specifies the provider
+ // which was used to collect the KYC details
+ provider_section: string;
+
+ // The collected KYC data. NULL if the attribute data could not
+ // be decrypted (internal error of the exchange, likely the
+ // attribute key was changed).
+ attributes?: Object;
+
+ // Time when the KYC data was collected
+ collection_time: Timestamp;
+
+ // Time when the validity of the KYC data will expire
+ expiration_time: Timestamp;
+ }
+
+ interface Timestamp {
+ // Seconds since epoch, or the special
+ // value "never" to represent an event that will
+ // never happen.
+ t_s: number | "never";
+ }
+
+ type PaytoHash = string;
+ type Integer = number;
+ type Amount = string;
+ // EdDSA signatures are transmitted as 64-bytes base32
+ // binary-encoded objects with just the R and S values (base32_ binary-only).
+ type EddsaSignature = string;
+
+ export interface AmlRecords {
+ // Array of AML records matching the query.
+ records: AmlRecord[];
+ }
+
+ interface AmlRecord {
+ // Which payto-address is this record about.
+ // Identifies a GNU Taler wallet or an affected bank account.
+ h_payto: PaytoHash;
+
+ // What is the current AML state.
+ current_state: AmlState;
+
+ // Monthly transaction threshold before a review will be triggered
+ threshold: Amount;
+
+ // RowID of the record.
+ rowid: Integer;
+ }
+
+ export enum AmlState {
+ normal = 0,
+ pending = 1,
+ frozen = 2,
+ }
+
+
+ export interface AmlDecision {
+
+ // Human-readable justification for the decision.
+ justification: string;
+
+ // At what monthly transaction volume should the
+ // decision be automatically reviewed?
+ new_threshold: Amount;
+
+ // Which payto-address is the decision about?
+ // Identifies a GNU Taler wallet or an affected bank account.
+ h_payto: PaytoHash;
+
+ // What is the new AML state (e.g. frozen, unfrozen, etc.)
+ // Numerical values are defined in AmlDecisionState.
+ new_state: Integer;
+
+ // Signature by the AML officer over a
+ // TALER_MasterAmlOfficerStatusPS.
+ // Must have purpose TALER_SIGNATURE_MASTER_AML_KEY.
+ officer_sig: EddsaSignature;
+
+ // When was the decision made?
+ decision_time: Timestamp;
+
+ // Optional argument to impose new KYC requirements
+ // that the customer has to satisfy to unblock transactions.
+ kyc_requirements?: string[];
+ }
+
+
+}
diff --git a/packages/aml-backoffice-ui/tailwind.config.js b/packages/aml-backoffice-ui/tailwind.config.js
new file mode 100644
index 000000000..ec51dfbb8
--- /dev/null
+++ b/packages/aml-backoffice-ui/tailwind.config.js
@@ -0,0 +1,14 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: {
+ relative: true,
+ files: [
+ "./src/**/*.{html,tsx}",
+ "./node_modules/@gnu-taler/web-util/src/**/*.{html,tsx}"
+ ],
+ },
+ theme: {
+ extend: {},
+ },
+ plugins: [require("@tailwindcss/typography"), require("@tailwindcss/forms")],
+};
diff --git a/packages/taler-wallet-webextension/src/serviceWorkerCryptoWorkerFactory.ts b/packages/aml-backoffice-ui/test.mjs
index 0742d5ccd..9df844fce 100644..100755
--- a/packages/taler-wallet-webextension/src/serviceWorkerCryptoWorkerFactory.ts
+++ b/packages/aml-backoffice-ui/test.mjs
@@ -1,3 +1,4 @@
+#!/usr/bin/env node
/*
This file is part of GNU Taler
(C) 2022 Taler Systems S.A.
@@ -14,23 +15,17 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-/**
- * API to access the Taler crypto worker thread.
- * @author Florian Dold
- */
-
-import {
- CryptoWorker,
- CryptoWorkerFactory,
- SynchronousCryptoWorker,
-} from "@gnu-taler/taler-wallet-core";
+import { build } from "@gnu-taler/web-util/build";
+import { getFilesInDirectory } from "@gnu-taler/web-util/build";
-export class SynchronousCryptoWorkerFactory implements CryptoWorkerFactory {
- startWorker(): CryptoWorker {
- return new SynchronousCryptoWorker();
- }
+const allTestFiles = getFilesInDirectory("./src", /.test.tsx?$/);
- getConcurrency(): number {
- return 1;
- }
-}
+await build({
+ type: "test",
+ source: {
+ js: allTestFiles.files,
+ assets: [{base:"src",files:["src/index.html"]}],
+ },
+ destination: "./dist/test",
+ css: "postcss",
+});
diff --git a/packages/demobank-ui/tsconfig.json b/packages/aml-backoffice-ui/tsconfig.json
index daa274983..9826fac07 100644
--- a/packages/demobank-ui/tsconfig.json
+++ b/packages/aml-backoffice-ui/tsconfig.json
@@ -1,12 +1,9 @@
{
"compilerOptions": {
/* Basic Options */
- "target": "ES5",
- "module": "ES6",
- "lib": [
- "DOM",
- "ES2016"
- ],
+ "target": "ES2020",
+ "module": "Node16",
+ "lib": ["DOM", "ES2020"],
"allowJs": true /* Allow javascript files to be compiled. */,
// "checkJs": true, /* Report errors in .js files. */
"jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */,
@@ -25,7 +22,7 @@
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
- "moduleResolution": "Node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
+ "moduleResolution": "Node16" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
"esModuleInterop": true /* */,
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
@@ -45,7 +42,5 @@
/* Advanced Options */
"skipLibCheck": true /* Skip type checking of declaration files. */
},
- "include": [
- "src/**/*"
- ]
-} \ No newline at end of file
+ "include": ["src/**/*"]
+}
diff --git a/packages/anastasis-cli/Makefile b/packages/anastasis-cli/Makefile
new file mode 100644
index 000000000..724a5e40d
--- /dev/null
+++ b/packages/anastasis-cli/Makefile
@@ -0,0 +1,42 @@
+# This Makefile has been placed in the public domain.
+
+ifeq ($(TOPLEVEL), yes)
+ $(info top-level build)
+ -include ../../.config.mk
+ override DESTDIR := $(TOP_DESTDIR)
+else
+ $(info package-level build)
+ -include ../../.config.mk
+ -include .config.mk
+endif
+
+$(info prefix is $(prefix))
+
+all:
+ @echo use 'make install' to build and install anastasis-cli
+
+ifndef prefix
+.PHONY: warn-noprefix install
+warn-noprefix:
+ @echo "no prefix configured, did you run ./configure?"
+install: warn-noprefix
+else
+bindir = $(prefix)/bin
+libdir = $(prefix)/lib/anastasis-cli
+nodedir = $(libdir)/node_modules/anastasis-cli
+.PHONY: install install-nodeps deps
+install-nodeps:
+ ./build-node.mjs
+ install -d $(DESTDIR)$(bindir)
+ install -d $(DESTDIR)$(nodedir)/bin
+ install -d $(DESTDIR)$(nodedir)/dist
+ install ./dist/anastasis-cli-bundled.cjs $(DESTDIR)$(nodedir)/dist/
+ install ./dist/anastasis-cli-bundled.cjs.map $(DESTDIR)$(nodedir)/dist/
+ install ./bin/anastasis-cli.mjs $(DESTDIR)$(nodedir)/bin/
+ ln -sf ../lib/anastasis-cli/node_modules/anastasis-cli/bin/anastasis-cli.mjs $(DESTDIR)$(bindir)/anastasis-cli
+deps:
+ pnpm install --frozen-lockfile --filter @gnu-taler/anastasis-cli...
+install:
+ $(MAKE) deps
+ $(MAKE) install-nodeps
+endif
diff --git a/packages/anastasis-cli/README.md b/packages/anastasis-cli/README.md
new file mode 100644
index 000000000..a48fd3c51
--- /dev/null
+++ b/packages/anastasis-cli/README.md
@@ -0,0 +1,4 @@
+# anastasis-cli
+
+This package provides `anastasis-cli`, the command-line interface for the
+Anastasis backup system.
diff --git a/packages/anastasis-cli/bin/anastasis-cli.mjs b/packages/anastasis-cli/bin/anastasis-cli.mjs
new file mode 100755
index 000000000..7506e4ba7
--- /dev/null
+++ b/packages/anastasis-cli/bin/anastasis-cli.mjs
@@ -0,0 +1,20 @@
+#!/usr/bin/env node
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { reducerCliMain } from '../dist/anastasis-cli-bundled.cjs';
+
+reducerCliMain();
diff --git a/packages/anastasis-cli/build-node.mjs b/packages/anastasis-cli/build-node.mjs
new file mode 100755
index 000000000..04b1c5256
--- /dev/null
+++ b/packages/anastasis-cli/build-node.mjs
@@ -0,0 +1,70 @@
+#!/usr/bin/env node
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import esbuild from "esbuild";
+import path from "path";
+import fs from "fs";
+
+const BASE = process.cwd();
+
+let GIT_ROOT = BASE;
+while (!fs.existsSync(path.join(GIT_ROOT, ".git")) && GIT_ROOT !== "/") {
+ GIT_ROOT = path.join(GIT_ROOT, "../");
+}
+if (GIT_ROOT === "/") {
+ console.log("not found");
+ process.exit(1);
+}
+const GIT_HASH = GIT_ROOT === "/" ? undefined : git_hash();
+
+let _package = JSON.parse(fs.readFileSync(path.join(BASE, "package.json")));
+
+function git_hash() {
+ const rev = fs
+ .readFileSync(path.join(GIT_ROOT, ".git", "HEAD"))
+ .toString()
+ .trim()
+ .split(/.*[: ]/)
+ .slice(-1)[0];
+ if (rev.indexOf("/") === -1) {
+ return rev;
+ } else {
+ return fs.readFileSync(path.join(GIT_ROOT, ".git", rev)).toString().trim();
+ }
+}
+
+export const buildConfig = {
+ entryPoints: ["src/index.ts"],
+ outfile: "dist/anastasis-cli-bundled.cjs",
+ bundle: true,
+ minify: false,
+ target: ["es2020"],
+ format: "cjs",
+ platform: "node",
+ sourcemap: true,
+ inject: ["src/import-meta-url.js"],
+ define: {
+ __VERSION__: `"${_package.version}"`,
+ __GIT_HASH__: `"${GIT_HASH}"`,
+ ["import.meta.url"]: "import_meta_url",
+ },
+};
+
+esbuild.build(buildConfig).catch((e) => {
+ console.log(e);
+ process.exit(1);
+});
diff --git a/packages/anastasis-cli/package.json b/packages/anastasis-cli/package.json
new file mode 100644
index 000000000..5a9d6abea
--- /dev/null
+++ b/packages/anastasis-cli/package.json
@@ -0,0 +1,44 @@
+{
+ "name": "@gnu-taler/anastasis-cli",
+ "version": "0.10.7",
+ "description": "",
+ "engines": {
+ "node": ">=0.18.0"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git://git.taler.net/wallet-core.git"
+ },
+ "author": "Florian Dold",
+ "license": "GPL-3.0",
+ "bin": {
+ "anastasis-cli": "./bin/anastasis-cli.mjs"
+ },
+ "type": "module",
+ "scripts": {
+ "compile": "tsc && ./build-node.mjs",
+ "test": "tsc",
+ "clean": "rm -rf lib dist tsconfig.tsbuildinfo",
+ "typedoc": "typedoc --out dist/typedoc ./src/",
+ "pretty": "prettier --write src"
+ },
+ "files": [
+ "AUTHORS",
+ "README",
+ "COPYING",
+ "bin/",
+ "dist/node",
+ "src/"
+ ],
+ "devDependencies": {
+ "@types/node": "^18.11.17",
+ "prettier": "^3.1.1",
+ "typedoc": "^0.25.4",
+ "typescript": "^5.3.3"
+ },
+ "dependencies": {
+ "@gnu-taler/anastasis-core": "workspace:*",
+ "@gnu-taler/taler-util": "workspace:*",
+ "tslib": "^2.6.2"
+ }
+}
diff --git a/packages/anastasis-cli/src/import-meta-url.js b/packages/anastasis-cli/src/import-meta-url.js
new file mode 100644
index 000000000..c0e657160
--- /dev/null
+++ b/packages/anastasis-cli/src/import-meta-url.js
@@ -0,0 +1,2 @@
+// Helper to make 'import.meta.url' available in esbuild-bundled code as well.
+export const import_meta_url = require("url").pathToFileURL(__filename);
diff --git a/packages/anastasis-cli/src/index.ts b/packages/anastasis-cli/src/index.ts
new file mode 100644
index 000000000..7c011569f
--- /dev/null
+++ b/packages/anastasis-cli/src/index.ts
@@ -0,0 +1,87 @@
+import { clk } from "@gnu-taler/taler-util/clk";
+import {
+ discoverPolicies,
+ getBackupStartState,
+ getRecoveryStartState,
+ reduceAction,
+} from "@gnu-taler/anastasis-core";
+import fs from "fs";
+import { j2s } from "@gnu-taler/taler-util";
+
+export const reducerCli = clk.program("anastasis-cli", {
+ help: "Command line interface for Anastasis.",
+});
+
+reducerCli
+ .subcommand("reducer", "reduce", {
+ help: "Run the anastasis reducer",
+ })
+ .flag("initBackup", ["-b", "--backup"])
+ .flag("initRecovery", ["-r", "--restore"])
+ .maybeOption("argumentsJson", ["-a", "--arguments"], clk.STRING)
+ .maybeArgument("action", clk.STRING)
+ .maybeArgument("stateFile", clk.STRING)
+ .action(async (x) => {
+ if (x.reducer.initBackup) {
+ console.log(JSON.stringify(await getBackupStartState()));
+ return;
+ } else if (x.reducer.initRecovery) {
+ console.log(JSON.stringify(await getRecoveryStartState()));
+ return;
+ }
+
+ const action = x.reducer.action;
+ if (!action) {
+ console.log("action required");
+ return;
+ }
+
+ let lastState: any;
+ if (x.reducer.stateFile) {
+ const s = fs.readFileSync(x.reducer.stateFile, { encoding: "utf-8" });
+ lastState = JSON.parse(s);
+ } else {
+ const s = await read(process.stdin);
+ lastState = JSON.parse(s);
+ }
+
+ let args: any;
+ if (x.reducer.argumentsJson) {
+ args = JSON.parse(x.reducer.argumentsJson);
+ } else {
+ args = {};
+ }
+
+ const nextState = await reduceAction(lastState, action, args);
+ console.log(JSON.stringify(nextState));
+ });
+
+reducerCli
+ .subcommand("discover", "discover", {
+ help: "Run the anastasis reducer",
+ })
+ .maybeArgument("stateFile", clk.STRING)
+ .action(async (args) => {
+ let lastState: any;
+ if (args.discover.stateFile) {
+ const s = fs.readFileSync(args.discover.stateFile, { encoding: "utf-8" });
+ lastState = JSON.parse(s);
+ } else {
+ const s = await read(process.stdin);
+ lastState = JSON.parse(s);
+ }
+ const res = await discoverPolicies(lastState);
+ console.log(j2s(res));
+ });
+
+async function read(stream: NodeJS.ReadStream): Promise<string> {
+ const chunks = [];
+ for await (const chunk of stream) {
+ chunks.push(chunk);
+ }
+ return Buffer.concat(chunks).toString("utf8");
+}
+
+export function reducerCliMain() {
+ reducerCli.run();
+}
diff --git a/packages/anastasis-cli/tsconfig.json b/packages/anastasis-cli/tsconfig.json
new file mode 100644
index 000000000..7675edfbc
--- /dev/null
+++ b/packages/anastasis-cli/tsconfig.json
@@ -0,0 +1,33 @@
+{
+ "compileOnSave": true,
+ "compilerOptions": {
+ "composite": true,
+ "target": "ES2020",
+ "module": "Node16",
+ "moduleResolution": "Node16",
+ "sourceMap": true,
+ "lib": ["ES2020"],
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "strict": true,
+ "strictPropertyInitialization": false,
+ "outDir": "lib",
+ "noImplicitAny": true,
+ "noImplicitThis": true,
+ "incremental": true,
+ "esModuleInterop": true,
+ "importHelpers": true,
+ "rootDir": "src",
+ "baseUrl": "./src",
+ "typeRoots": ["./node_modules/@types"]
+ },
+ "include": ["src/**/*"],
+ "references": [
+ {
+ "path": "../anastasis-core/"
+ },
+ {
+ "path": "../taler-util/"
+ }
+ ]
+}
diff --git a/packages/anastasis-core/README.md b/packages/anastasis-core/README.md
new file mode 100644
index 000000000..f3696c768
--- /dev/null
+++ b/packages/anastasis-core/README.md
@@ -0,0 +1,3 @@
+# anastasis-core
+
+This package implements the core client logic of Anastasis in TypeScript.
diff --git a/packages/anastasis-core/package.json b/packages/anastasis-core/package.json
index 33d7898eb..576acc988 100644
--- a/packages/anastasis-core/package.json
+++ b/packages/anastasis-core/package.json
@@ -1,33 +1,29 @@
{
"name": "@gnu-taler/anastasis-core",
- "version": "0.0.2",
+ "version": "0.10.7",
"description": "",
"main": "./lib/index.js",
"module": "./lib/index.js",
"types": "./lib/index.d.ts",
"scripts": {
- "prepare": "tsc",
"compile": "tsc",
"pretty": "prettier --write src",
"test": "tsc && ava",
"coverage": "tsc && nyc ava",
- "clean": "rimraf dist lib tsconfig.tsbuildinfo"
+ "typedoc": "typedoc --out dist/typedoc ./src/",
+ "clean": "rm -rf dist lib tsconfig.tsbuildinfo"
},
"author": "Florian Dold <dold@taler.net>",
"license": "AGPL-3-or-later",
"type": "module",
"devDependencies": {
- "ava": "^4.3.3",
- "rimraf": "^3.0.2",
- "typescript": "^4.8.4"
+ "ava": "^6.0.1",
+ "typescript": "^5.3.3"
},
"dependencies": {
"@gnu-taler/taler-util": "workspace:*",
- "fetch-ponyfill": "^7.1.0",
- "fflate": "^0.7.4",
- "hash-wasm": "^4.9.0",
- "node-fetch": "^3.2.0",
- "tslib": "^2.4.0"
+ "fflate": "^0.8.1",
+ "tslib": "^2.6.2"
},
"ava": {
"files": [
diff --git a/packages/anastasis-core/src/anastasis-data.ts b/packages/anastasis-core/src/anastasis-data.ts
index d69bb319b..9cbf5f594 100644
--- a/packages/anastasis-core/src/anastasis-data.ts
+++ b/packages/anastasis-core/src/anastasis-data.ts
@@ -11,14 +11,10 @@ export const anastasisData = {
url: "https://v1.anastasis.taler.net/",
name: "Bern University of Applied Sciences, Switzerland",
},
- {
- url: "https://v1.anastasis.codeblau.de/",
- name: "Codeblau GmbH, Germany",
- },
- // {
- // url: "https://v1.anastasis.openw3b.org/",
- // name: "Openw3b Foundation, India",
- // },
+// {
+// url: "https://v1.anastasis.codeblau.de/",
+// name: "Codeblau GmbH, Germany",
+// },
{
url: "https://v1.anastasis.lu/",
name: "Anastasis SARL, Luxembourg",
diff --git a/packages/anastasis-core/src/cli-entry.ts b/packages/anastasis-core/src/cli-entry.ts
deleted file mode 100644
index 8eea42a18..000000000
--- a/packages/anastasis-core/src/cli-entry.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { reducerCliMain } from "./cli.js";
-
-async function r() {
- reducerCliMain();
-}
-
-r();
diff --git a/packages/anastasis-core/src/cli.ts b/packages/anastasis-core/src/cli.ts
deleted file mode 100644
index 517f2876d..000000000
--- a/packages/anastasis-core/src/cli.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import { clk } from "@gnu-taler/taler-util";
-import {
- getBackupStartState,
- getRecoveryStartState,
- reduceAction,
-} from "./index.js";
-import fs from "fs";
-
-export const reducerCli = clk
- .program("reducer", {
- help: "Command line interface for Anastasis.",
- })
- .flag("initBackup", ["-b", "--backup"])
- .flag("initRecovery", ["-r", "--restore"])
- .maybeOption("argumentsJson", ["-a", "--arguments"], clk.STRING)
- .maybeArgument("action", clk.STRING)
- .maybeArgument("stateFile", clk.STRING);
-
-async function read(stream: NodeJS.ReadStream): Promise<string> {
- const chunks = [];
- for await (const chunk of stream) {
- chunks.push(chunk);
- }
- return Buffer.concat(chunks).toString("utf8");
-}
-
-reducerCli.action(async (x) => {
- if (x.reducer.initBackup) {
- console.log(JSON.stringify(await getBackupStartState()));
- return;
- } else if (x.reducer.initRecovery) {
- console.log(JSON.stringify(await getRecoveryStartState()));
- return;
- }
-
- const action = x.reducer.action;
- if (!action) {
- console.log("action required");
- return;
- }
-
- let lastState: any;
- if (x.reducer.stateFile) {
- const s = fs.readFileSync(x.reducer.stateFile, { encoding: "utf-8" });
- lastState = JSON.parse(s);
- } else {
- const s = await read(process.stdin);
- lastState = JSON.parse(s);
- }
-
- let args: any;
- if (x.reducer.argumentsJson) {
- args = JSON.parse(x.reducer.argumentsJson);
- } else {
- args = {};
- }
-
- const nextState = await reduceAction(lastState, action, args);
- console.log(JSON.stringify(nextState));
-});
-
-export function reducerCliMain() {
- reducerCli.run();
-}
diff --git a/packages/anastasis-core/src/crypto.ts b/packages/anastasis-core/src/crypto.ts
index 5e45f995f..8bc004e95 100644
--- a/packages/anastasis-core/src/crypto.ts
+++ b/packages/anastasis-core/src/crypto.ts
@@ -26,8 +26,8 @@ import {
secretbox_open,
hash,
bytesToString,
+ hashArgon2id,
} from "@gnu-taler/taler-util";
-import { argon2id } from "hash-wasm";
export type Flavor<T, FlavorT extends string> = T & {
_flavor?: `anastasis.${FlavorT}`;
@@ -71,15 +71,13 @@ export async function userIdentifierDerive(
): Promise<UserIdentifier> {
const canonIdData = canonicalJson(idData);
const hashInput = stringToBytes(canonIdData);
- const result = await argon2id({
- hashLength: 64,
- iterations: 3,
- memorySize: 1024 /* kibibytes */,
- parallelism: 1,
- password: hashInput,
- salt: decodeCrock(serverSalt),
- outputType: "binary",
- });
+ const result = await hashArgon2id(
+ hashInput, // password
+ decodeCrock(serverSalt), // salt
+ 3, // iterations
+ 1024, // memoryLimit (kibibytes)
+ 64, // hashLength
+ );
return encodeCrock(result);
}
@@ -153,7 +151,11 @@ export async function decryptPolicyMetadata(
userId: UserIdentifier,
metadataEnc: OpaqueData,
): Promise<PolicyMetadata> {
+ // @ts-ignore
+ console.log("metadataEnc", metadataEnc);
const plain = await anastasisDecrypt(asOpaque(userId), metadataEnc, "rmd");
+ // @ts-ignore
+ console.log("plain:", plain);
const metadataBytes = decodeCrock(plain);
const policyHash = encodeCrock(metadataBytes.slice(0, 64));
const secretName = bytesToString(metadataBytes.slice(64));
@@ -343,15 +345,13 @@ export async function secureAnswerHash(
truthUuid: TruthUuid,
questionSalt: TruthSalt,
): Promise<SecureAnswerHash> {
- const powResult = await argon2id({
- hashLength: 64,
- iterations: 3,
- memorySize: 1024 /* kibibytes */,
- parallelism: 1,
- password: stringToBytes(answer),
- salt: decodeCrock(questionSalt),
- outputType: "binary",
- });
+ const powResult = await hashArgon2id(
+ stringToBytes(answer), // password
+ decodeCrock(questionSalt), // salt
+ 3, // iterations
+ 1024, // memorySize (kibibytes)
+ 64, // hashLength
+ );
const kdfResult = kdfKw({
outputLength: 64,
salt: decodeCrock(truthUuid),
diff --git a/packages/anastasis-core/src/index.node.ts b/packages/anastasis-core/src/index.node.ts
deleted file mode 100644
index d08906a22..000000000
--- a/packages/anastasis-core/src/index.node.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export * from "./index.js";
-export { reducerCliMain } from "./cli.js";
diff --git a/packages/anastasis-core/src/index.ts b/packages/anastasis-core/src/index.ts
index 8cb86cd85..05fa4a49f 100644
--- a/packages/anastasis-core/src/index.ts
+++ b/packages/anastasis-core/src/index.ts
@@ -43,6 +43,7 @@ import {
URL,
j2s,
} from "@gnu-taler/taler-util";
+import { HttpResponse } from "@gnu-taler/taler-util/http";
import { anastasisData } from "./anastasis-data.js";
import {
codecForChallengeInstructionMessage,
@@ -96,7 +97,6 @@ import {
AggregatedPolicyMetaInfo,
AuthenticationProviderStatusMap,
} from "./reducer-types.js";
-import fetchPonyfill from "fetch-ponyfill";
import {
accountKeypairDerive,
asOpaque,
@@ -133,8 +133,6 @@ import {
ChallengeFeedbackStatus,
} from "./challenge-feedback-types.js";
-const { fetch } = fetchPonyfill({});
-
export * from "./reducer-types.js";
export * as validators from "./validators.js";
export * from "./challenge-feedback-types.js";
@@ -285,13 +283,18 @@ async function getProviderInfo(
try {
resp = await fetch(new URL("config", providerBaseUrl).href);
} catch (e) {
+ console.warn(
+ "Encountered an HTTP error whilst trying to get the provider's config: ",
+ e,
+ );
return {
status: "error",
code: TalerErrorCode.ANASTASIS_REDUCER_NETWORK_FAILED,
- hint: "request to provider failed",
+ hint: "request to anastasis provider failed",
};
}
- if (resp.status !== 200) {
+ if (!resp.ok) {
+ console.warn("Got bad response code whilst getting provider config", resp);
return {
status: "error",
code: TalerErrorCode.ANASTASIS_REDUCER_NETWORK_FAILED,
@@ -647,7 +650,7 @@ async function uploadSecret(
method: "POST",
headers: {
"Anastasis-Policy-Signature": encodeCrock(sig),
- "If-None-Match": encodeCrock(bodyHash),
+ "If-None-Match": JSON.stringify(encodeCrock(bodyHash)),
[ANASTASIS_HTTP_HEADER_POLICY_META_DATA]: metadataEnc,
...(paySecret
? {
@@ -1473,7 +1476,7 @@ async function updateUploadFees(
x,
).amount;
};
- const expirationTime = AbsoluteTime.fromTimestamp(expiration);
+ const expirationTime = AbsoluteTime.fromProtocolTimestamp(expiration);
const years = Duration.toIntegerYears(Duration.getRemaining(expirationTime));
logger.info(`computing fees for ${years} years`);
// For now, we compute fees for *all* available providers.
@@ -1655,7 +1658,7 @@ export function mergeDiscoveryAggregate(
newPolicies: PolicyMetaInfo[],
oldAgg: AggregatedPolicyMetaInfo[],
): AggregatedPolicyMetaInfo[] {
- const aggregatedPolicies: AggregatedPolicyMetaInfo[] = [...oldAgg] ?? [];
+ const aggregatedPolicies: AggregatedPolicyMetaInfo[] = [...oldAgg];
const polHashToIndex: Record<string, number> = {};
for (const pol of newPolicies) {
const oldIndex = polHashToIndex[pol.policy_hash];
diff --git a/packages/anastasis-core/src/policy-suggestion.test.ts b/packages/anastasis-core/src/policy-suggestion.test.ts
index 6370825da..fd42b708f 100644
--- a/packages/anastasis-core/src/policy-suggestion.test.ts
+++ b/packages/anastasis-core/src/policy-suggestion.test.ts
@@ -1,4 +1,4 @@
-import { j2s } from "@gnu-taler/taler-util";
+import { AmountString, j2s } from "@gnu-taler/taler-util";
import test from "ava";
import { ProviderInfo, suggestPolicies } from "./policy-suggestion.js";
@@ -23,13 +23,13 @@ test("policy suggestion", async (t) => {
const providers: ProviderInfo[] = [
{
methodCost: {
- sms: "KUDOS:1",
+ sms: "KUDOS:1" as AmountString,
},
url: "prov1",
},
{
methodCost: {
- question: "KUDOS:1",
+ question: "KUDOS:1" as AmountString,
},
url: "prov2",
},
diff --git a/packages/anastasis-core/src/reducer-types.ts b/packages/anastasis-core/src/reducer-types.ts
index 4b87d3ae6..ad88f40ed 100644
--- a/packages/anastasis-core/src/reducer-types.ts
+++ b/packages/anastasis-core/src/reducer-types.ts
@@ -295,7 +295,7 @@ export enum RecoveryStates {
export interface MethodSpec {
type: string;
- usage_fee: string;
+ usage_fee: AmountString;
}
export type AuthenticationProviderStatusNotContacted = {
diff --git a/packages/anastasis-core/tsconfig.json b/packages/anastasis-core/tsconfig.json
index 7cab21017..e463201e7 100644
--- a/packages/anastasis-core/tsconfig.json
+++ b/packages/anastasis-core/tsconfig.json
@@ -2,11 +2,11 @@
"compileOnSave": true,
"compilerOptions": {
"composite": true,
- "target": "ES2018",
- "module": "ESNext",
+ "target": "ES2020",
+ "module": "Node16",
"moduleResolution": "Node16",
"sourceMap": true,
- "lib": ["es6", "DOM"],
+ "lib": ["ES2020", "DOM"],
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"strict": true,
diff --git a/packages/anastasis-webui/README.md b/packages/anastasis-webui/README.md
new file mode 100644
index 000000000..e56da8a1c
--- /dev/null
+++ b/packages/anastasis-webui/README.md
@@ -0,0 +1,3 @@
+# anastasis-webui
+
+This package implements a web-based client UI for GNU Anastasis.
diff --git a/packages/anastasis-webui/build.mjs b/packages/anastasis-webui/build.mjs
index ebe914541..c52d5e718 100755
--- a/packages/anastasis-webui/build.mjs
+++ b/packages/anastasis-webui/build.mjs
@@ -14,134 +14,14 @@
You should have received a copy of the GNU Affero General Public License along with
GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-/* eslint-disable no-undef */
-import esbuild from 'esbuild'
-import fs from 'fs';
-import path from "path"
-import sass from "sass";
+import { build } from "@gnu-taler/web-util/build";
-// eslint-disable-next-line no-undef
-const BASE = process.cwd();
-
-const preact = path.join(
- BASE,
- "node_modules",
- "preact",
- "compat",
- "dist",
- "compat.module.js",
-);
-
-const preactCompatPlugin = {
- name: "preact-compat",
- setup(build) {
- build.onResolve({ filter: /^(react-dom|react)$/ }, (args) => {
- return {
- path: preact,
- };
- });
- },
-};
-
-
-let GIT_ROOT = BASE
-while (!fs.existsSync(path.join(GIT_ROOT, '.git')) && GIT_ROOT !== '/') {
- GIT_ROOT = path.join(GIT_ROOT, '../')
-}
-if (GIT_ROOT === '/') {
- // eslint-disable-next-line no-undef
- console.log("not found")
- // eslint-disable-next-line no-undef
- process.exit(1);
-}
-const GIT_HASH = GIT_ROOT === '/' ? undefined : git_hash()
-
-
-let _package = JSON.parse(fs.readFileSync(path.join(BASE, 'package.json')));
-
-function git_hash() {
- const rev = fs.readFileSync(path.join(GIT_ROOT, '.git', 'HEAD')).toString().trim().split(/.*[: ]/).slice(-1)[0];
- if (rev.indexOf('/') === -1) {
- return rev;
- } else {
- return fs.readFileSync(path.join(GIT_ROOT, '.git', rev)).toString().trim();
- }
-}
-
-const DEFAULT_SASS_FILTER = /\.(s[ac]ss|css)$/
-
-const buildSassPlugin = {
- name: "custom-build-sass",
- setup(build) {
-
- build.onLoad({ filter: DEFAULT_SASS_FILTER }, ({ path: file }) => {
- const resolveDir = path.dirname(file)
- const { css: contents } = sass.compile(file, { loadPaths: ["./"] })
-
- return {
- resolveDir,
- loader: 'css',
- contents
- }
- });
-
- },
-};
-
-function copyFilesPlugin(options) {
- if (!options.basedir) {
- options.basedir = process.cwd()
- }
- return {
- name: "copy-files",
- setup(build) {
- build.onEnd(() => {
- for (const fop of options) {
- fs.copyFileSync(path.join(options.basedir, fop.src), path.join(options.basedir, fop.dest));
- }
- });
- },
- };
-}
-
-export const buildConfig = {
- entryPoints: ['src/index.ts', 'src/stories.tsx'],
- bundle: true,
- outdir: 'dist',
- minify: false,
- loader: {
- '.svg': 'dataurl',
- '.ttf': 'file',
- '.woff': 'file',
- '.woff2': 'file',
- '.eot': 'file',
- },
- target: [
- 'es6'
- ],
- format: 'esm',
- platform: 'browser',
- sourcemap: true,
- jsxFactory: 'h',
- jsxFragment: 'Fragment',
- define: {
- '__VERSION__': `"${_package.version}"`,
- '__GIT_HASH__': `"${GIT_HASH}"`,
+await build({
+ type: "production",
+ source: {
+ js: ["src/index.ts"],
+ assets: [{base:"src",files:["src/index.html"]}],
},
- plugins: [
- preactCompatPlugin,
- copyFilesPlugin([
- {
- src: "./src/index.html",
- dest: "./dist/index.html",
- },
- ]),
- buildSassPlugin
- ],
-}
-
-await esbuild.build(buildConfig)
-
-
-
-
+ destination: "./dist/prod",
+ css: "sass",
+});
diff --git a/packages/anastasis-webui/dev.mjs b/packages/anastasis-webui/dev.mjs
index 0446603dc..91fcc6a07 100755
--- a/packages/anastasis-webui/dev.mjs
+++ b/packages/anastasis-webui/dev.mjs
@@ -15,17 +15,26 @@
GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { serve } from "@gnu-taler/web-util/lib/index.node";
-import esbuild from 'esbuild';
-import { buildConfig } from "./build.mjs";
+import { serve } from "@gnu-taler/web-util/node";
+import { initializeDev } from "@gnu-taler/web-util/build";
-buildConfig.inject = ['./node_modules/@gnu-taler/web-util/lib/live-reload.mjs']
+const devEntryPoints = ["src/stories.tsx", "src/index.ts"];
+
+const build = initializeDev({
+ type: "development",
+ source: {
+ js: devEntryPoints,
+ assets: [{base:"src",files:["src/index.html"]}],
+ },
+ css: "sass",
+ destination: "./dist/dev",
+});
+
+await build();
serve({
- folder: './dist',
+ folder: "./dist/dev",
port: 8080,
- source: './src',
- development: true,
- onUpdate: async () => esbuild.build(buildConfig)
-})
-
+ source: "./src",
+ onSourceUpdate: build,
+});
diff --git a/packages/anastasis-webui/package.json b/packages/anastasis-webui/package.json
index c01856243..108b1476e 100644
--- a/packages/anastasis-webui/package.json
+++ b/packages/anastasis-webui/package.json
@@ -1,27 +1,27 @@
{
"private": true,
"name": "@gnu-taler/anastasis-webui",
- "version": "0.2.99",
+ "version": "0.10.7",
"license": "MIT",
+ "type": "module",
"scripts": {
- "build": "./clean_and_build.sh",
- "compile": "tsc",
- "dev": "./clean_and_build.sh WATCH",
- "prepare": "pnpm compile",
+ "build": "./build.mjs",
+ "compile": "tsc && ./build.mjs",
+ "dev": "./dev.mjs",
+ "clean": "rm -rf dist lib tsconfig.tsbuildinfo",
"lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
- "test": "mocha --enable-source-maps 'dist/**/*test.js'",
+ "typedoc": "typedoc --out dist/typedoc ./src/",
+ "test": "./test.mjs && mocha --require source-map-support/register --enable-source-maps 'dist/**/*test.js'",
"pretty": "prettier --write src"
},
"dependencies": {
"@gnu-taler/anastasis-core": "workspace:*",
"@gnu-taler/taler-util": "workspace:*",
"@gnu-taler/web-util": "workspace:*",
- "@types/chai": "^4.3.0",
- "chai": "^4.3.6",
"date-fns": "2.29.2",
"jed": "1.1.1",
+ "jssha": "^3.3.0",
"preact": "10.11.3",
- "preact-render-to-string": "^5.1.19",
"preact-router": "^3.2.1",
"qrcode-generator": "^1.4.4"
},
@@ -38,13 +38,14 @@
},
"devDependencies": {
"@creativebulma/bulma-tooltip": "^1.2.0",
+ "@types/chai": "^4.3.0",
"@types/mocha": "^9.0.0",
"bulma": "^0.9.3",
"bulma-checkbox": "^1.1.1",
"bulma-radio": "^1.1.1",
- "jssha": "^3.2.0",
+ "chai": "^4.3.6",
"mocha": "^9.2.0",
"sass": "1.56.1",
- "typescript": "^4.8.4"
+ "typescript": "^5.3.3"
}
-} \ No newline at end of file
+}
diff --git a/packages/anastasis-webui/src/components/menu/SideBar.tsx b/packages/anastasis-webui/src/components/menu/SideBar.tsx
index 3dac73e04..31bc3c7a7 100644
--- a/packages/anastasis-webui/src/components/menu/SideBar.tsx
+++ b/packages/anastasis-webui/src/components/menu/SideBar.tsx
@@ -29,7 +29,10 @@ interface Props {
}
const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : "dev";
-const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined;
+const GIT_HASH =
+ typeof __GIT_HASH__ !== "undefined"
+ ? __GIT_HASH__.substring(0, 7)
+ : undefined;
const VERSION_WITH_HASH = GIT_HASH ? `${VERSION}-${GIT_HASH}` : VERSION;
export function Sidebar({ mobile }: Props): VNode {
diff --git a/packages/anastasis-webui/src/components/menu/index.tsx b/packages/anastasis-webui/src/components/menu/index.tsx
index b1d22eeb4..957ab2977 100644
--- a/packages/anastasis-webui/src/components/menu/index.tsx
+++ b/packages/anastasis-webui/src/components/menu/index.tsx
@@ -15,7 +15,7 @@
*/
import { ComponentChildren, Fragment, h, VNode } from "preact";
-import Match from "preact-router/match";
+import { Match } from "preact-router/match.js";
import { useEffect, useState } from "preact/hooks";
import { NavigationBar } from "./NavigationBar.js";
import { Sidebar } from "./SideBar.js";
diff --git a/packages/anastasis-webui/src/components/picker/DurationPicker.stories.tsx b/packages/anastasis-webui/src/components/picker/DurationPicker.stories.tsx
index 0339c9ec0..94bce4038 100644
--- a/packages/anastasis-webui/src/components/picker/DurationPicker.stories.tsx
+++ b/packages/anastasis-webui/src/components/picker/DurationPicker.stories.tsx
@@ -22,6 +22,7 @@
import { h, FunctionalComponent } from "preact";
import { useState } from "preact/hooks";
import { DurationPicker as TestedComponent } from "./DurationPicker.js";
+import * as tests from "@gnu-taler/web-util/testing";
export default {
component: TestedComponent,
@@ -31,16 +32,7 @@ export default {
},
};
-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, {
+export const Example = tests.createExample(TestedComponent, {
days: true,
minutes: true,
hours: true,
diff --git a/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts b/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts
index 3ad563ee6..fcc380775 100644
--- a/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts
+++ b/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts
@@ -136,6 +136,7 @@ export interface DiscoveryUiState {
export interface AnastasisReducerApi {
currentReducerState: ReducerState | undefined;
+ // FIXME: Explain better!
currentError: any;
discoveryState: DiscoveryUiState;
dismissError: () => void;
@@ -302,7 +303,7 @@ export function useAnastasisReducer(): AnastasisReducerApi {
},
});
} catch (e) {
- throw Error("could not restore the state");
+ throw new Error("could not restore the state");
}
},
async discoverStart(): Promise<void> {
@@ -398,7 +399,7 @@ export function useAnastasisReducer(): AnastasisReducerApi {
}
class ReducerTxImpl implements ReducerTransactionHandle {
- constructor(public transactionState: ReducerState) { }
+ constructor(public transactionState: ReducerState) {}
async transition(action: string, args: any): Promise<ReducerState> {
let s: ReducerState;
if (remoteReducer) {
@@ -409,7 +410,7 @@ class ReducerTxImpl implements ReducerTransactionHandle {
this.transactionState = s;
// Abort transaction as soon as we transition into an error state.
if (this.transactionState.reducer_type === "error") {
- throw Error("transition resulted in error");
+ throw new Error("transition resulted in error");
}
return this.transactionState;
}
diff --git a/packages/anastasis-webui/src/index.html b/packages/anastasis-webui/src/index.html
index 90a795ae3..d64b627e4 100644
--- a/packages/anastasis-webui/src/index.html
+++ b/packages/anastasis-webui/src/index.html
@@ -32,7 +32,7 @@
/>
<link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
<title>Anastasis</title>
- <!-- Entry point for the demobank SPA. -->
+ <!-- Entry point for the SPA. -->
<script type="module" src="index.js"></script>
<link rel="stylesheet" href="index.css" />
</head>
diff --git a/packages/anastasis-webui/src/index.test.ts b/packages/anastasis-webui/src/index.test.ts
index 1a87e3857..2f865bc1d 100644
--- a/packages/anastasis-webui/src/index.test.ts
+++ b/packages/anastasis-webui/src/index.test.ts
@@ -19,31 +19,64 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
import { setupI18n } from "@gnu-taler/taler-util";
-import { renderNodeOrBrowser } from "./test-utils.js";
-import * as pages from "./pages/home/index.storiesNo.js";
+import { parseGroupImport } from "@gnu-taler/web-util/browser";
+import * as tests from "@gnu-taler/web-util/testing";
+import * as pages from "./pages/home/index.stories.js";
+import { ComponentChildren, VNode, h as create } from "preact";
+import { AnastasisProvider } from "./context/anastasis.js";
+import { AnastasisReducerApi } from "./hooks/use-anastasis-reducer.js";
+import { ReducerState } from "@gnu-taler/anastasis-core";
setupI18n("en", { en: {} });
-function testThisStory(key: string, st: any): any {
- describe(`render examples for ${key}`, () => {
- Object.keys(st).forEach((k) => {
- const Component = (st as any)[k];
- if (k === "default" || !Component) return;
-
- it(`example: ${k}`, () => {
- renderNodeOrBrowser(Component, Component.args);
+describe("All the examples:", () => {
+ const cms = parseGroupImport({ pages });
+ 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, DefaultTestingContext);
+ });
+ });
+ });
});
});
});
-}
-
-describe("render every storybook example", () => {
- Object.entries(pages).forEach(function testAll([key, value]) {
- const st: any = value;
- if (Array.isArray(st.default)) {
- st.default.forEach(testAll);
- } else {
- testThisStory(key, st);
- }
- });
});
+
+const noop = async (): Promise<void> => {
+ return;
+};
+
+function DefaultTestingContext({
+ children,
+ ...rest
+}: {
+ children: ComponentChildren;
+}): VNode {
+ //some UI example can specify the state of the reducer
+ const currentReducerState = rest as ReducerState;
+ const value: AnastasisReducerApi = {
+ currentReducerState,
+ discoverMore: noop,
+ discoverStart: noop,
+ discoveryState: {
+ state: "finished",
+ },
+ currentError: undefined,
+ back: noop,
+ dismissError: noop,
+ reset: noop,
+ runTransaction: noop,
+ startBackup: noop,
+ startRecover: noop,
+ transition: noop,
+ exportState: () => {
+ return "{}";
+ },
+ importState: noop,
+ };
+ return create(AnastasisProvider, { value, children });
+}
diff --git a/packages/anastasis-webui/src/index.ts b/packages/anastasis-webui/src/index.ts
index d7b2164ab..f614e4f54 100644
--- a/packages/anastasis-webui/src/index.ts
+++ b/packages/anastasis-webui/src/index.ts
@@ -22,7 +22,7 @@ function main(): void {
try {
const container = document.getElementById("container");
if (!container) {
- throw Error("container not found, can't mount page contents");
+ throw new Error("container not found, can't mount page contents");
}
render(h(App, {}), container);
} catch (e) {
diff --git a/packages/anastasis-webui/src/pages/home/AddingProviderScreen/index.ts b/packages/anastasis-webui/src/pages/home/AddingProviderScreen/index.ts
index 0ab275f54..ed8301d65 100644
--- a/packages/anastasis-webui/src/pages/home/AddingProviderScreen/index.ts
+++ b/packages/anastasis-webui/src/pages/home/AddingProviderScreen/index.ts
@@ -24,7 +24,7 @@ import { WithoutProviderType, WithProviderType } from "./views.js";
export type AuthProvByStatusMap = Record<
AuthenticationProviderStatus["status"],
(AuthenticationProviderStatus & { url: string })[]
->
+>;
export type State = NoReducer | InvalidState | WithType | WithoutType;
@@ -63,42 +63,69 @@ const map: StateViewMap<State> = {
"without-type": WithoutProviderType,
};
-export default compose("AddingProviderScreen", useComponentState, map)
-
+export default compose("AddingProviderScreen", useComponentState, map);
+const providerResponseCache = new Map<string, any>(); // `any` is the return type of res.json()
export async function testProvider(
url: string,
expectedMethodType?: string,
): Promise<void> {
+ const testFatalPrefix = `Encountered a fatal error whilst testing the provider ${url}`;
+ let configUrl = "";
try {
- const response = await fetch(new URL("config", url).href);
- const json = await response.json().catch((d) => ({}));
- if (!("methods" in json) || !Array.isArray(json.methods)) {
- throw Error(
- "This provider doesn't have authentication method. Check the provider URL",
- );
- }
- if (!expectedMethodType) {
- return;
- }
- let found = false;
- for (let i = 0; i < json.methods.length && !found; i++) {
- found = json.methods[i].type === expectedMethodType;
- }
- if (!found) {
- throw Error(
- `This provider does not support authentication method ${expectedMethodType}`,
- );
- }
+ configUrl = new URL("config", url).href;
+ } catch (error) {
+ throw new Error(`${testFatalPrefix}: Invalid Provider URL: ${url}
+Error: ${error}`);
+ }
+ // TODO: look into using core.getProviderInfo :)
+ const providerHasUrl = providerResponseCache.has(url);
+ const json = providerHasUrl
+ ? providerResponseCache.get(url)
+ : await fetch(configUrl)
+ .catch((error) => {
+ throw new Error(`${testFatalPrefix}: Could not connect: ${error}
+Please check the URL.`);
+ })
+ .then(async (response) => {
+ if (!response.ok)
+ throw new Error(
+ `${testFatalPrefix}: The server ${response.url} responded with a non-2xx response.`,
+ );
+ try {
+ return await response.json();
+ } catch (error) {
+ throw new Error(
+ `${testFatalPrefix}: The server responded with malformed JSON.\nError: ${error}`,
+ );
+ }
+ });
+ if (typeof json !== "object")
+ throw new Error(
+ `${testFatalPrefix}: Did not get an object after decoding.`,
+ );
+ if (!("name" in json) || json.name !== "anastasis") {
+ throw new Error(
+ `${testFatalPrefix}: The provider does not appear to be an Anastasis provider. Please check the provider's URL.`,
+ );
+ }
+ if (!("methods" in json) || !Array.isArray(json.methods)) {
+ throw new Error(
+ "This provider doesn't have authentication method. Please check the provider's URL and ensure it is properly configured.",
+ );
+ }
+ if (!providerHasUrl) providerResponseCache.set(url, json);
+ if (!expectedMethodType) {
return;
- } catch (e) {
- console.log("ERROR testProvider", e);
- const error =
- e instanceof Error
- ? Error(
- `There was an error testing this provider, try another one. ${e.message}`,
- )
- : Error(`There was an error testing this provider, try another one.`);
- throw error;
}
+ let found = false;
+ for (let i = 0; i < json.methods.length && !found; i++) {
+ found = json.methods[i].type === expectedMethodType;
+ }
+ if (!found) {
+ throw new Error(
+ `${testFatalPrefix}: This provider does not support authentication method ${expectedMethodType}`,
+ );
+ }
+ return;
}
diff --git a/packages/anastasis-webui/src/pages/home/AddingProviderScreen/state.ts b/packages/anastasis-webui/src/pages/home/AddingProviderScreen/state.ts
index 009ab20a2..30e4d750d 100644
--- a/packages/anastasis-webui/src/pages/home/AddingProviderScreen/state.ts
+++ b/packages/anastasis-webui/src/pages/home/AddingProviderScreen/state.ts
@@ -25,7 +25,11 @@ interface Props {
notifications?: Notification[];
}
-export default function useComponentState({ providerType, onCancel, notifications = [] }: Props): State {
+export default function useComponentState({
+ providerType,
+ onCancel,
+ notifications = [],
+}: Props): State {
const reducer = useAnastasisContext();
const [providerURL, setProviderURL] = useState("");
@@ -39,9 +43,9 @@ export default function useComponentState({ providerType, onCancel, notification
const allAuthProviders =
!reducer ||
- !reducer.currentReducerState ||
- reducer.currentReducerState.reducer_type === "error" ||
- !reducer.currentReducerState.authentication_providers
+ !reducer.currentReducerState ||
+ reducer.currentReducerState.reducer_type === "error" ||
+ !reducer.currentReducerState.authentication_providers
? {}
: reducer.currentReducerState.authentication_providers;
@@ -58,7 +62,12 @@ export default function useComponentState({ providerType, onCancel, notification
prev[p.status].push({ ...p, url });
return prev;
},
- { "not-contacted": [], disabled: [], error: [], ok: [] } as AuthProvByStatusMap,
+ {
+ "not-contacted": [],
+ disabled: [],
+ error: [],
+ ok: [],
+ } as AuthProvByStatusMap,
);
const authProviders = authProvidersByStatus["ok"].map((p) => p.url);
@@ -67,14 +76,23 @@ export default function useComponentState({ providerType, onCancel, notification
useEffect(() => {
if (timeout.current) clearTimeout(timeout.current);
timeout.current = setTimeout(async () => {
- const url = providerURL.endsWith("/") ? providerURL : providerURL + "/";
- if (!providerURL || authProviders.includes(url)) return;
+ let url = providerURL;
+ if (!url || authProviders.includes(url)) return;
+ if (url && !url.match(/^(https?:)\/\/.+\/(?:config)?$/iu))
+ return setError(
+ "Malformed URL: Must be an HTTP(S) URL ending with a /",
+ );
+ if (url.endsWith("/config")) url = url.substring(0, url.length - 6);
try {
setTesting(true);
await testProvider(url, providerType);
setError("");
} catch (e) {
if (e instanceof Error) setError(e.message);
+ else
+ throw new Error(
+ `Unexpected Error Type: ${typeof e} - Cannot handle. Error: ${e}`,
+ );
}
setTesting(false);
}, 200);
@@ -98,19 +116,20 @@ export default function useComponentState({ providerType, onCancel, notification
const addProvider = async (provider_url: string): Promise<void> => {
await reducer.transition("add_provider", { provider_url });
onCancel();
- }
+ };
const deleteProvider = async (provider_url: string): Promise<void> => {
reducer.transition("delete_provider", { provider_url });
- }
+ };
let errors = !providerURL ? "Add provider URL" : undefined;
let url: string | undefined;
- try {
- url = new URL("", providerURL).href;
- } catch {
- errors = "Check the URL";
- }
- const _url = url
+ // We'll validate it in testProvider & via a regex above - there's no need in this :)
+ // try {
+ // url = new URL("", providerURL).href;
+ // } catch {
+ // errors = "Check the URL";
+ // }
+ const _url = url;
if (!!error && !errors) {
errors = error;
@@ -130,21 +149,19 @@ export default function useComponentState({ providerType, onCancel, notification
setProviderURL: async (s: string) => setProviderURL(s),
errors,
error,
- notifications
- }
+ notifications,
+ };
if (!providerLabel) {
return {
status: "without-type",
- ...commonState
- }
+ ...commonState,
+ };
} else {
return {
status: "with-type",
providerLabel,
- ...commonState
- }
+ ...commonState,
+ };
}
-
}
-
diff --git a/packages/anastasis-webui/src/pages/home/AddingProviderScreen/stories.tsx b/packages/anastasis-webui/src/pages/home/AddingProviderScreen/stories.tsx
index 268189ed8..548fc01a5 100644
--- a/packages/anastasis-webui/src/pages/home/AddingProviderScreen/stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/AddingProviderScreen/stories.tsx
@@ -20,7 +20,7 @@
*/
import { AuthenticationProviderStatusOk } from "@gnu-taler/anastasis-core";
-import { createExampleWithoutAnastasis } from "../../../utils/index.jsx";
+import * as tests from "@gnu-taler/web-util/testing";
import { WithoutProviderType, WithProviderType } from "./views.jsx";
export default {
@@ -34,7 +34,7 @@ export default {
},
};
-export const NewProvider = createExampleWithoutAnastasis(WithoutProviderType, {
+export const NewProvider = tests.createExample(WithoutProviderType, {
authProvidersByStatus: {
ok: [
{
@@ -57,7 +57,7 @@ export const NewProvider = createExampleWithoutAnastasis(WithoutProviderType, {
notifications: [],
});
-export const NewProviderWithoutProviderList = createExampleWithoutAnastasis(
+export const NewProviderWithoutProviderList = tests.createExample(
WithoutProviderType,
{
authProvidersByStatus: {
@@ -70,7 +70,7 @@ export const NewProviderWithoutProviderList = createExampleWithoutAnastasis(
},
);
-export const NewSmsProvider = createExampleWithoutAnastasis(WithProviderType, {
+export const NewSmsProvider = tests.createExample(WithProviderType, {
authProvidersByStatus: {
ok: [],
"not-contacted": [],
@@ -81,7 +81,7 @@ export const NewSmsProvider = createExampleWithoutAnastasis(WithProviderType, {
notifications: [],
});
-export const NewIBANProvider = createExampleWithoutAnastasis(WithProviderType, {
+export const NewIBANProvider = tests.createExample(WithProviderType, {
authProvidersByStatus: {
ok: [],
"not-contacted": [],
diff --git a/packages/anastasis-webui/src/pages/home/AddingProviderScreen/test.ts b/packages/anastasis-webui/src/pages/home/AddingProviderScreen/test.ts
index d051d7c0b..0aebbdc6c 100644
--- a/packages/anastasis-webui/src/pages/home/AddingProviderScreen/test.ts
+++ b/packages/anastasis-webui/src/pages/home/AddingProviderScreen/test.ts
@@ -20,23 +20,26 @@
*/
import { expect } from "chai";
-import { mountHook } from "../../../test-utils.js";
import useComponentState from "./state.js";
+import * as tests from "@gnu-taler/web-util/testing";
describe("AddingProviderScreen states", () => {
- it("should have status 'no-balance' when balance is empty", async () => {
- const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
- mountHook(() =>
- useComponentState({ onCancel: async () => { null } }),
- );
-
- {
- const { status } = getLastResultOrThrow();
- expect(status).equal("no-reducer");
- }
-
- await assertNoPendingUpdate();
-
+ it("should not load more if has reach the end", async () => {
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ return useComponentState({
+ providerType: "email",
+ async onCancel() {},
+ });
+ },
+ {},
+ [
+ ({ status }) => {
+ expect(status).eq("no-reducer");
+ },
+ ],
+ );
+
+ expect(hookBehavior).deep.eq({ result: "ok" });
});
-
});
diff --git a/packages/anastasis-webui/src/pages/home/AddingProviderScreen/views.tsx b/packages/anastasis-webui/src/pages/home/AddingProviderScreen/views.tsx
index 19557a12f..00a42a949 100644
--- a/packages/anastasis-webui/src/pages/home/AddingProviderScreen/views.tsx
+++ b/packages/anastasis-webui/src/pages/home/AddingProviderScreen/views.tsx
@@ -121,13 +121,13 @@ export function WithoutProviderType(props: WithoutType): VNode {
<div class="container">
<TextInput
label="Provider URL"
- placeholder="https://provider.com"
+ placeholder="https://provider.com/"
grabFocus
error={props.errors}
bind={[props.providerURL, props.setProviderURL]}
/>
</div>
- <p class="block">Example: https://kudos.demo.anastasis.lu</p>
+ <p class="block">Example: https://kudos.demo.anastasis.lu/</p>
{props.testing && <p class="has-text-info">Testing</p>}
<div
diff --git a/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.stories.tsx
index 38fc1b56b..e6bc5f340 100644
--- a/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.stories.tsx
@@ -20,7 +20,8 @@
*/
import { ReducerState } from "@gnu-taler/anastasis-core";
-import { createExample, reducerStatesExample } from "../../utils/index.js";
+import * as tests from "@gnu-taler/web-util/testing";
+import { reducerStatesExample } from "../../utils/index.js";
import { AttributeEntryScreen as TestedComponent } from "./AttributeEntryScreen.js";
export default {
@@ -35,7 +36,7 @@ export default {
},
};
-export const Backup = createExample(TestedComponent, {
+export const Backup = tests.createExample(TestedComponent, {}, {
...reducerStatesExample.backupAttributeEditing,
required_attributes: [
{
@@ -62,7 +63,7 @@ export const Backup = createExample(TestedComponent, {
],
} as ReducerState);
-export const Recovery = createExample(TestedComponent, {
+export const Recovery = tests.createExample(TestedComponent, {}, {
...reducerStatesExample.recoveryAttributeEditing,
required_attributes: [
{
@@ -89,10 +90,14 @@ export const Recovery = createExample(TestedComponent, {
],
} as ReducerState);
-export const WithNoRequiredAttribute = createExample(TestedComponent, {
- ...reducerStatesExample.backupAttributeEditing,
- required_attributes: undefined,
-} as ReducerState);
+export const WithNoRequiredAttribute = tests.createExample(
+ TestedComponent,
+ {},
+ {
+ ...reducerStatesExample.backupAttributeEditing,
+ required_attributes: undefined,
+ } as ReducerState,
+);
const allWidgets = [
"anastasis_gtk_ia_aadhar_in",
@@ -123,7 +128,7 @@ function typeForWidget(name: string): string {
return "string";
}
-export const WithAllPosibleWidget = createExample(TestedComponent, {
+export const WithAllPosibleWidget = tests.createExample(TestedComponent, {}, {
...reducerStatesExample.backupAttributeEditing,
required_attributes: allWidgets.map((w) => ({
name: w,
@@ -134,19 +139,23 @@ export const WithAllPosibleWidget = createExample(TestedComponent, {
})),
} as ReducerState);
-export const WithAutocompleteFeature = createExample(TestedComponent, {
- ...reducerStatesExample.backupAttributeEditing,
- required_attributes: [
- {
- name: "ahv_number",
- label: "AHV Number",
- type: "string",
- uuid: "asdasdsa1",
- widget: "wid",
- "validation-regex":
- "^(756)\\.[0-9]{4}\\.[0-9]{4}\\.[0-9]{2}|(756)[0-9]{10}$",
- "validation-logic": "CH_AHV_check",
- autocomplete: "???.????.????.??",
- },
- ],
-} as ReducerState);
+export const WithAutocompleteFeature = tests.createExample(
+ TestedComponent,
+ {},
+ {
+ ...reducerStatesExample.backupAttributeEditing,
+ required_attributes: [
+ {
+ name: "ahv_number",
+ label: "AHV Number",
+ type: "string",
+ uuid: "asdasdsa1",
+ widget: "wid",
+ "validation-regex":
+ "^(756)\\.[0-9]{4}\\.[0-9]{4}\\.[0-9]{2}|(756)[0-9]{10}$",
+ "validation-logic": "CH_AHV_check",
+ autocomplete: "???.????.????.??",
+ },
+ ],
+ } as ReducerState,
+);
diff --git a/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.tsx b/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.tsx
index 228186a2d..e38b91b12 100644
--- a/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.tsx
@@ -132,6 +132,7 @@ export function AttributeEntryScreen(): VNode {
secret will be safely stored. If you forget what you have entered or
if there is a misspell you will be unable to recover your secret.
<p>
+ {/* TODO: make this actually work reliably cross-browser lol (opens about:blank for me) */}
<a onClick={saveAsPDF}>Save the personal information as PDF</a>
</p>
</ConfirmModal>
diff --git a/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.stories.tsx
index ba48e2d5c..22f8dd697 100644
--- a/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.stories.tsx
@@ -20,7 +20,8 @@
*/
import { ReducerState } from "@gnu-taler/anastasis-core";
-import { createExample, reducerStatesExample } from "../../utils/index.js";
+import * as tests from "@gnu-taler/web-util/testing";
+import { reducerStatesExample } from "../../utils/index.js";
import { AuthenticationEditorScreen as TestedComponent } from "./AuthenticationEditorScreen.js";
export default {
@@ -35,63 +36,72 @@ export default {
},
};
-export const InitialState = createExample(
+export const InitialState = tests.createExample(
TestedComponent,
+ {},
reducerStatesExample.authEditing,
);
-export const OneAuthMethodConfigured = createExample(TestedComponent, {
- ...reducerStatesExample.authEditing,
- authentication_methods: [
- {
- type: "question",
- instructions: "what time is it?",
- challenge: "asd",
- },
- ],
-} as ReducerState);
+export const OneAuthMethodConfigured = tests.createExample(
+ TestedComponent,
+ {},
+ {
+ ...reducerStatesExample.authEditing,
+ authentication_methods: [
+ {
+ type: "question",
+ instructions: "what time is it?",
+ challenge: "asd",
+ },
+ ],
+ } as ReducerState,
+);
-export const SomeMoreAuthMethodConfigured = createExample(TestedComponent, {
- ...reducerStatesExample.authEditing,
- authentication_methods: [
- {
- type: "question",
- instructions: "what time is it?",
- challenge: "asd",
- },
- {
- type: "question",
- instructions: "what time is it?",
- challenge: "qwe",
- },
- {
- type: "sms",
- instructions: "what time is it?",
- challenge: "asd",
- },
- {
- type: "email",
- instructions: "what time is it?",
- challenge: "asd",
- },
- {
- type: "email",
- instructions: "what time is it?",
- challenge: "asd",
- },
- {
- type: "email",
- instructions: "what time is it?",
- challenge: "asd",
- },
- {
- type: "email",
- instructions: "what time is it?",
- challenge: "asd",
- },
- ],
-} as ReducerState);
+export const SomeMoreAuthMethodConfigured = tests.createExample(
+ TestedComponent,
+ {},
+ {
+ ...reducerStatesExample.authEditing,
+ authentication_methods: [
+ {
+ type: "question",
+ instructions: "what time is it?",
+ challenge: "asd",
+ },
+ {
+ type: "question",
+ instructions: "what time is it?",
+ challenge: "qwe",
+ },
+ {
+ type: "sms",
+ instructions: "what time is it?",
+ challenge: "asd",
+ },
+ {
+ type: "email",
+ instructions: "what time is it?",
+ challenge: "asd",
+ },
+ {
+ type: "email",
+ instructions: "what time is it?",
+ challenge: "asd",
+ },
+ {
+ type: "email",
+ instructions: "what time is it?",
+ challenge: "asd",
+ },
+ {
+ type: "email",
+ instructions: "what time is it?",
+ challenge: "asd",
+ },
+ ],
+ } as ReducerState,
+);
-export const NoAuthMethodProvided = createExample(TestedComponent, {
+export const NoAuthMethodProvided = tests.createExample(TestedComponent, {}, {
...reducerStatesExample.authEditing,
authentication_providers: {},
authentication_methods: [],
diff --git a/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx b/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx
index 3018f88dd..54bbc626d 100644
--- a/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx
@@ -241,7 +241,7 @@ export function AuthenticationEditorScreen(): VNode {
</p>
{authAvailableSet.size > 0 && (
<p class="block">
- We couldn&apos;t find provider for some of the authentication
+ We couldn't find provider for some of the authentication
methods.
</p>
)}
diff --git a/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.stories.tsx
index 8aeaec25c..a51940615 100644
--- a/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.stories.tsx
@@ -20,8 +20,9 @@
*/
import { ReducerState } from "@gnu-taler/anastasis-core";
-import { createExample, reducerStatesExample } from "../../utils/index.js";
+import { reducerStatesExample } from "../../utils/index.js";
import { BackupFinishedScreen as TestedComponent } from "./BackupFinishedScreen.js";
+import * as tests from "@gnu-taler/web-util/testing";
export default {
title: "Backup finish",
@@ -35,17 +36,18 @@ export default {
},
};
-export const WithoutName = createExample(
+export const WithoutName = tests.createExample(
TestedComponent,
+ {},
reducerStatesExample.backupFinished,
);
-export const WithName = createExample(TestedComponent, {
+export const WithName = tests.createExample(TestedComponent, {}, {
...reducerStatesExample.backupFinished,
secret_name: "super_secret",
} as ReducerState);
-export const WithDetails = createExample(TestedComponent, {
+export const WithDetails = tests.createExample(TestedComponent, {}, {
...reducerStatesExample.backupFinished,
secret_name: "super_secret",
success_details: {
diff --git a/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.stories.tsx
index d2471755a..84df615f3 100644
--- a/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.stories.tsx
@@ -24,8 +24,10 @@ import {
RecoveryStates,
ReducerState,
} from "@gnu-taler/anastasis-core";
-import { createExample, reducerStatesExample } from "../../utils/index.js";
+import { reducerStatesExample } from "../../utils/index.js";
import { ChallengeOverviewScreen as TestedComponent } from "./ChallengeOverviewScreen.js";
+import * as tests from "@gnu-taler/web-util/testing";
+import { AmountString } from "@gnu-taler/taler-util";
export default {
title: "Challenge overview",
@@ -39,7 +41,7 @@ export default {
},
};
-export const OneUnsolvedPolicy = createExample(TestedComponent, {
+export const OneUnsolvedPolicy = tests.createExample(TestedComponent, {}, {
...reducerStatesExample.challengeSelecting,
recovery_information: {
policies: [[{ uuid: "1" }]],
@@ -53,7 +55,7 @@ export const OneUnsolvedPolicy = createExample(TestedComponent, {
},
} as ReducerState);
-export const SomePoliciesOneSolved = createExample(TestedComponent, {
+export const SomePoliciesOneSolved = tests.createExample(TestedComponent, {}, {
...reducerStatesExample.challengeSelecting,
recovery_information: {
policies: [[{ uuid: "1" }, { uuid: "2" }], [{ uuid: "uuid-3" }]],
@@ -82,7 +84,7 @@ export const SomePoliciesOneSolved = createExample(TestedComponent, {
},
} as ReducerState);
-export const OneBadConfiguredPolicy = createExample(TestedComponent, {
+export const OneBadConfiguredPolicy = tests.createExample(TestedComponent, {}, {
...reducerStatesExample.challengeSelecting,
recovery_information: {
policies: [[{ uuid: "1" }, { uuid: "2" }]],
@@ -96,72 +98,76 @@ export const OneBadConfiguredPolicy = createExample(TestedComponent, {
},
} as ReducerState);
-export const OnePolicyWithAllTheChallenges = createExample(TestedComponent, {
- ...reducerStatesExample.challengeSelecting,
- recovery_information: {
- policies: [
- [
- { uuid: "1" },
- { uuid: "2" },
- { uuid: "3" },
- { uuid: "4" },
- { uuid: "5" },
- { uuid: "6" },
- { uuid: "7" },
- { uuid: "8" },
- ],
- ],
- challenges: [
- {
- instructions: "Does P equals NP?",
- type: "question",
- uuid: "1",
- },
- {
- instructions: "SMS to 555-555",
- type: "sms",
- uuid: "2",
- },
- {
- instructions: "Email to qwe@asd.com",
- type: "email",
- uuid: "3",
- },
- {
- instructions: 'Enter 8 digits code for "Anastasis"',
- type: "totp",
- uuid: "4",
- },
- {
- //
- instructions: "Wire transfer from ASDXCVQWE123123 with holder Florian",
- type: "iban",
- uuid: "5",
- },
- {
- instructions: "Join a video call",
- type: "video", //Enter 8 digits code for "Anastasis"
- uuid: "7",
- },
- {},
- {
- instructions: "Letter to address in postal code DE123123",
- type: "post", //Enter 8 digits code for "Anastasis"
- uuid: "8",
- },
- {
- instructions: "instruction for an unknown type of challenge",
- type: "new-type-of-challenge",
- uuid: "6",
- },
- ],
- },
-} as ReducerState);
-
-export const OnePolicyWithAllTheChallengesInDifferentState = createExample(
+export const OnePolicyWithAllTheChallenges = tests.createExample(
TestedComponent,
+ {},
{
...reducerStatesExample.challengeSelecting,
+ recovery_information: {
+ policies: [
+ [
+ { uuid: "1" },
+ { uuid: "2" },
+ { uuid: "3" },
+ { uuid: "4" },
+ { uuid: "5" },
+ { uuid: "6" },
+ { uuid: "7" },
+ { uuid: "8" },
+ ],
+ ],
+ challenges: [
+ {
+ instructions: "Does P equals NP?",
+ type: "question",
+ uuid: "1",
+ },
+ {
+ instructions: "SMS to 555-555",
+ type: "sms",
+ uuid: "2",
+ },
+ {
+ instructions: "Email to qwe@asd.com",
+ type: "email",
+ uuid: "3",
+ },
+ {
+ instructions: 'Enter 8 digits code for "Anastasis"',
+ type: "totp",
+ uuid: "4",
+ },
+ {
+ //
+ instructions:
+ "Wire transfer from ASDXCVQWE123123 with holder Florian",
+ type: "iban",
+ uuid: "5",
+ },
+ {
+ instructions: "Join a video call",
+ type: "video", //Enter 8 digits code for "Anastasis"
+ uuid: "7",
+ },
+ {},
+ {
+ instructions: "Letter to address in postal code DE123123",
+ type: "post", //Enter 8 digits code for "Anastasis"
+ uuid: "8",
+ },
+ {
+ instructions: "instruction for an unknown type of challenge",
+ type: "new-type-of-challenge",
+ uuid: "6",
+ },
+ ],
+ },
+ } as ReducerState,
+);
+
+export const OnePolicyWithAllTheChallengesInDifferentState =
+ tests.createExample(TestedComponent, {}, {
+ ...reducerStatesExample.challengeSelecting,
recovery_state: RecoveryStates.ChallengeSelecting,
recovery_information: {
policies: [
@@ -251,16 +257,16 @@ export const OnePolicyWithAllTheChallengesInDifferentState = createExample(
"uuid-8": { state: ChallengeFeedbackStatus.RateLimitExceeded.toString() },
"uuid-9": {
state: ChallengeFeedbackStatus.IbanInstructions.toString(),
- challenge_amount: "EUR:1",
+ challenge_amount: "EUR:1" as AmountString,
target_iban: "DE12345789000",
target_business_name: "Data Loss Incorporated",
wire_transfer_subject: "Anastasis 987654321",
},
"uuid-10": { state: ChallengeFeedbackStatus.IncorrectAnswer.toString() },
},
- } as ReducerState,
-);
-export const NoPolicies = createExample(
+ } as ReducerState);
+export const NoPolicies = tests.createExample(
TestedComponent,
+ {},
reducerStatesExample.challengeSelecting,
);
diff --git a/packages/anastasis-webui/src/pages/home/ChallengePayingScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/ChallengePayingScreen.stories.tsx
index cd41fe03a..0489e5a11 100644
--- a/packages/anastasis-webui/src/pages/home/ChallengePayingScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/ChallengePayingScreen.stories.tsx
@@ -19,7 +19,8 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { createExample, reducerStatesExample } from "../../utils/index.js";
+import * as tests from "@gnu-taler/web-util/testing";
+import { reducerStatesExample } from "../../utils/index.js";
import { ChallengePayingScreen as TestedComponent } from "./ChallengePayingScreen.js";
export default {
@@ -34,7 +35,8 @@ export default {
},
};
-export const Example = createExample(
+export const Example = tests.createExample(
TestedComponent,
+ {},
reducerStatesExample.challengePaying,
);
diff --git a/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.stories.tsx
index 12a79c56c..646165341 100644
--- a/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.stories.tsx
@@ -20,8 +20,9 @@
*/
import { ReducerState } from "@gnu-taler/anastasis-core";
-import { createExample, reducerStatesExample } from "../../utils/index.js";
+import { reducerStatesExample } from "../../utils/index.js";
import { ContinentSelectionScreen as TestedComponent } from "./ContinentSelectionScreen.js";
+import * as tests from "@gnu-taler/web-util/testing";
export default {
title: "Continent selection",
@@ -35,22 +36,24 @@ export default {
},
};
-export const BackupSelectContinent = createExample(
+export const BackupSelectContinent = tests.createExample(
TestedComponent,
+ {},
reducerStatesExample.backupSelectContinent,
);
-export const BackupSelectCountry = createExample(TestedComponent, {
+export const BackupSelectCountry = tests.createExample(TestedComponent, {}, {
...reducerStatesExample.backupSelectContinent,
selected_continent: "Testcontinent",
} as ReducerState);
-export const RecoverySelectContinent = createExample(
+export const RecoverySelectContinent = tests.createExample(
TestedComponent,
+ {},
reducerStatesExample.recoverySelectContinent,
);
-export const RecoverySelectCountry = createExample(TestedComponent, {
+export const RecoverySelectCountry = tests.createExample(TestedComponent, {}, {
...reducerStatesExample.recoverySelectContinent,
selected_continent: "Testcontinent",
} as ReducerState);
diff --git a/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.tsx b/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.tsx
index fc9c0f097..3231e61e4 100644
--- a/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.tsx
@@ -46,8 +46,9 @@ export function ContinentSelectionScreen(): VNode {
// const cc = reducer.currentReducerState.selected_country || "";
const theCountry = countryList.find((c) => c.code === countryCode);
const selectCountryAction = async () => {
- //selection should be when the select box changes it value
+ // selection should be when the select box changes it value
if (!theCountry) return;
+ // FIXME: Why is there no await?
reducer.transition("select_country", {
country_code: countryCode,
});
@@ -56,6 +57,7 @@ export function ContinentSelectionScreen(): VNode {
// const step1 = reducer.currentReducerState.backup_state === BackupStates.ContinentSelecting ||
// reducer.currentReducerState.recovery_state === RecoveryStates.ContinentSelecting;
+ // FIXME: i18n
const errors = !theCountry ? "Select a country" : undefined;
const handleBack = async () => {
diff --git a/packages/anastasis-webui/src/pages/home/EditPoliciesScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/EditPoliciesScreen.stories.tsx
index 1e3650300..3c9fd7f50 100644
--- a/packages/anastasis-webui/src/pages/home/EditPoliciesScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/EditPoliciesScreen.stories.tsx
@@ -20,7 +20,8 @@
*/
import { ReducerState } from "@gnu-taler/anastasis-core";
-import { createExample, reducerStatesExample } from "../../utils/index.js";
+import * as tests from "@gnu-taler/web-util/testing";
+import { reducerStatesExample } from "../../utils/index.js";
import { EditPoliciesScreen as TestedComponent } from "./EditPoliciesScreen.js";
export default {
@@ -35,8 +36,9 @@ export default {
},
};
-export const EditingAPolicy = createExample(
+export const EditingAPolicy = tests.createExample(
TestedComponent,
+ { index: 0 },
{
...reducerStatesExample.policyReview,
policies: [
@@ -84,11 +86,11 @@ export const EditingAPolicy = createExample(
},
],
} as ReducerState,
- { index: 0 },
);
-export const CreatingAPolicy = createExample(
+export const CreatingAPolicy = tests.createExample(
TestedComponent,
+ { index: 3 },
{
...reducerStatesExample.policyReview,
policies: [
@@ -136,5 +138,4 @@ export const CreatingAPolicy = createExample(
},
],
} as ReducerState,
- { index: 3 },
);
diff --git a/packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.stories.tsx
index 56c224d34..ea88b74a0 100644
--- a/packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.stories.tsx
@@ -20,8 +20,9 @@
*/
import { ReducerState } from "@gnu-taler/anastasis-core";
-import { createExample, reducerStatesExample } from "../../utils/index.js";
+import { reducerStatesExample } from "../../utils/index.js";
import { PoliciesPayingScreen as TestedComponent } from "./PoliciesPayingScreen.js";
+import * as tests from "@gnu-taler/web-util/testing";
export default {
title: "Policies paying",
@@ -35,11 +36,12 @@ export default {
},
};
-export const Example = createExample(
+export const Example = tests.createExample(
TestedComponent,
+ {},
reducerStatesExample.policyPay,
);
-export const WithSomePaymentRequest = createExample(TestedComponent, {
+export const WithSomePaymentRequest = tests.createExample(TestedComponent, {}, {
...reducerStatesExample.policyPay,
policy_payment_requests: [
{
diff --git a/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.stories.tsx
index 1eb2ae50c..97e0821fd 100644
--- a/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.stories.tsx
@@ -21,8 +21,9 @@
import { ReducerState } from "@gnu-taler/anastasis-core";
import { encodeCrock, stringToBytes } from "@gnu-taler/taler-util";
-import { createExample, reducerStatesExample } from "../../utils/index.js";
+import { reducerStatesExample } from "../../utils/index.js";
import { RecoveryFinishedScreen as TestedComponent } from "./RecoveryFinishedScreen.js";
+import * as tests from "@gnu-taler/web-util/testing";
export default {
title: "Recovery Finished",
@@ -36,7 +37,7 @@ export default {
},
};
-export const GoodEnding = createExample(TestedComponent, {
+export const GoodEnding = tests.createExample(TestedComponent, {}, {
...reducerStatesExample.recoveryFinished,
recovery_document: {
secret_name: "the_name_of_the_secret",
@@ -49,7 +50,8 @@ export const GoodEnding = createExample(TestedComponent, {
},
} as ReducerState);
-export const BadEnding = createExample(
+export const BadEnding = tests.createExample(
TestedComponent,
+ {},
reducerStatesExample.recoveryFinished,
);
diff --git a/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.tsx b/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.tsx
index 62ac410a2..f528bc207 100644
--- a/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.tsx
@@ -58,9 +58,14 @@ export function RecoveryFinishedScreen(): VNode {
const secret = bytesToString(decodeCrock(encodedSecret.value));
const plainText =
encodedSecret.value.length < 1000 && encodedSecret.mime === "text/plain";
- const contentURI = !plainText
- ? secret
- : `data:${encodedSecret.mime},${secret}`;
+
+ let [uri, setUri] = useState(`data:${encodedSecret.mime},${secret}`);
+ fetch(`data:${encodedSecret.mime},${secret}`) // TODO: look into using new Blob
+ .then((v) => v.blob())
+ .then((blob) => URL.createObjectURL(blob))
+ .then((newUri) => {
+ setUri(newUri);
+ });
return (
<AnastasisClientFrame title="Recovery Success" hideNav>
<h2 class="subtitle">Your secret was recovered</h2>
@@ -87,7 +92,7 @@ export function RecoveryFinishedScreen(): VNode {
download={
encodedSecret.filename ? encodedSecret.filename : "secret.file"
}
- href={contentURI}
+ href={uri}
>
<div class="icon is-small ">
<i class="mdi mdi-download" />
diff --git a/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.stories.tsx
index c5003d6a0..71144917a 100644
--- a/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.stories.tsx
@@ -20,8 +20,9 @@
*/
import { ReducerState } from "@gnu-taler/anastasis-core";
-import { createExample, reducerStatesExample } from "../../utils/index.js";
+import { reducerStatesExample } from "../../utils/index.js";
import { ReviewPoliciesScreen as TestedComponent } from "./ReviewPoliciesScreen.js";
+import * as tests from "@gnu-taler/web-util/testing";
export default {
title: "Reviewing Policies",
@@ -35,227 +36,235 @@ export default {
},
};
-export const HasPoliciesButMethodListIsEmpty = createExample(TestedComponent, {
- ...reducerStatesExample.policyReview,
- policies: [
- {
- methods: [
- {
- authentication_method: 0,
- provider: "asd",
- },
- {
- authentication_method: 1,
- provider: "asd",
- },
- ],
- },
- {
- methods: [
- {
- authentication_method: 1,
- provider: "asd",
- },
- ],
- },
- ],
- authentication_methods: [],
-} as ReducerState);
+export const HasPoliciesButMethodListIsEmpty = tests.createExample(
+ TestedComponent,
+ {},
+ {
+ ...reducerStatesExample.policyReview,
+ policies: [
+ {
+ methods: [
+ {
+ authentication_method: 0,
+ provider: "asd",
+ },
+ {
+ authentication_method: 1,
+ provider: "asd",
+ },
+ ],
+ },
+ {
+ methods: [
+ {
+ authentication_method: 1,
+ provider: "asd",
+ },
+ ],
+ },
+ ],
+ authentication_methods: [],
+ } as ReducerState,
+);
-export const SomePoliciesWithMethods = createExample(TestedComponent, {
- ...reducerStatesExample.policyReview,
- policies: [
- {
- methods: [
- {
- authentication_method: 0,
- provider: "https://kudos.demo.anastasis.lu/",
- },
- {
- authentication_method: 1,
- provider: "https://kudos.demo.anastasis.lu/",
- },
- {
- authentication_method: 2,
- provider: "https://kudos.demo.anastasis.lu/",
- },
- ],
- },
- {
- methods: [
- {
- authentication_method: 0,
- provider: "https://kudos.demo.anastasis.lu/",
- },
- {
- authentication_method: 1,
- provider: "https://kudos.demo.anastasis.lu/",
- },
- {
- authentication_method: 3,
- provider: "https://anastasis.demo.taler.net/",
- },
- ],
- },
- {
- methods: [
- {
- authentication_method: 0,
- provider: "https://kudos.demo.anastasis.lu/",
- },
- {
- authentication_method: 1,
- provider: "https://kudos.demo.anastasis.lu/",
- },
- {
- authentication_method: 4,
- provider: "https://anastasis.demo.taler.net/",
- },
- ],
- },
- {
- methods: [
- {
- authentication_method: 0,
- provider: "https://kudos.demo.anastasis.lu/",
- },
- {
- authentication_method: 2,
- provider: "https://kudos.demo.anastasis.lu/",
- },
- {
- authentication_method: 3,
- provider: "https://anastasis.demo.taler.net/",
- },
- ],
- },
- {
- methods: [
- {
- authentication_method: 0,
- provider: "https://kudos.demo.anastasis.lu/",
- },
- {
- authentication_method: 2,
- provider: "https://kudos.demo.anastasis.lu/",
- },
- {
- authentication_method: 4,
- provider: "https://anastasis.demo.taler.net/",
- },
- ],
- },
- {
- methods: [
- {
- authentication_method: 0,
- provider: "https://kudos.demo.anastasis.lu/",
- },
- {
- authentication_method: 3,
- provider: "https://anastasis.demo.taler.net/",
- },
- {
- authentication_method: 4,
- provider: "https://anastasis.demo.taler.net/",
- },
- ],
- },
- {
- methods: [
- {
- authentication_method: 1,
- provider: "https://kudos.demo.anastasis.lu/",
- },
- {
- authentication_method: 2,
- provider: "https://kudos.demo.anastasis.lu/",
- },
- {
- authentication_method: 3,
- provider: "https://anastasis.demo.taler.net/",
- },
- ],
- },
- {
- methods: [
- {
- authentication_method: 1,
- provider: "https://kudos.demo.anastasis.lu/",
- },
- {
- authentication_method: 2,
- provider: "https://kudos.demo.anastasis.lu/",
- },
- {
- authentication_method: 4,
- provider: "https://anastasis.demo.taler.net/",
- },
- ],
- },
- {
- methods: [
- {
- authentication_method: 1,
- provider: "https://kudos.demo.anastasis.lu/",
- },
- {
- authentication_method: 3,
- provider: "https://anastasis.demo.taler.net/",
- },
- {
- authentication_method: 4,
- provider: "https://anastasis.demo.taler.net/",
- },
- ],
- },
- {
- methods: [
- {
- authentication_method: 2,
- provider: "https://kudos.demo.anastasis.lu/",
- },
- {
- authentication_method: 3,
- provider: "https://anastasis.demo.taler.net/",
- },
- {
- authentication_method: 4,
- provider: "https://anastasis.demo.taler.net/",
- },
- ],
- },
- ],
- authentication_methods: [
- {
- type: "email",
- instructions: "Email to qwe@asd.com",
- challenge: "E5VPA",
- },
- {
- type: "sms",
- instructions: "SMS to 555-555",
- challenge: "",
- },
- {
- type: "question",
- instructions: "Does P equal NP?",
- challenge: "C5SP8",
- },
- {
- type: "totp",
- instructions: "Response code for 'Anastasis'",
- challenge: "E5VPA",
- },
- {
- type: "sms",
- instructions: "SMS to 6666-6666",
- challenge: "",
- },
- {
- type: "question",
- instructions: "How did the chicken cross the road?",
- challenge: "C5SP8",
- },
- ],
-} as ReducerState);
+export const SomePoliciesWithMethods = tests.createExample(
+ TestedComponent,
+ {},
+ {
+ ...reducerStatesExample.policyReview,
+ policies: [
+ {
+ methods: [
+ {
+ authentication_method: 0,
+ provider: "https://kudos.demo.anastasis.lu/",
+ },
+ {
+ authentication_method: 1,
+ provider: "https://kudos.demo.anastasis.lu/",
+ },
+ {
+ authentication_method: 2,
+ provider: "https://kudos.demo.anastasis.lu/",
+ },
+ ],
+ },
+ {
+ methods: [
+ {
+ authentication_method: 0,
+ provider: "https://kudos.demo.anastasis.lu/",
+ },
+ {
+ authentication_method: 1,
+ provider: "https://kudos.demo.anastasis.lu/",
+ },
+ {
+ authentication_method: 3,
+ provider: "https://anastasis.demo.taler.net/",
+ },
+ ],
+ },
+ {
+ methods: [
+ {
+ authentication_method: 0,
+ provider: "https://kudos.demo.anastasis.lu/",
+ },
+ {
+ authentication_method: 1,
+ provider: "https://kudos.demo.anastasis.lu/",
+ },
+ {
+ authentication_method: 4,
+ provider: "https://anastasis.demo.taler.net/",
+ },
+ ],
+ },
+ {
+ methods: [
+ {
+ authentication_method: 0,
+ provider: "https://kudos.demo.anastasis.lu/",
+ },
+ {
+ authentication_method: 2,
+ provider: "https://kudos.demo.anastasis.lu/",
+ },
+ {
+ authentication_method: 3,
+ provider: "https://anastasis.demo.taler.net/",
+ },
+ ],
+ },
+ {
+ methods: [
+ {
+ authentication_method: 0,
+ provider: "https://kudos.demo.anastasis.lu/",
+ },
+ {
+ authentication_method: 2,
+ provider: "https://kudos.demo.anastasis.lu/",
+ },
+ {
+ authentication_method: 4,
+ provider: "https://anastasis.demo.taler.net/",
+ },
+ ],
+ },
+ {
+ methods: [
+ {
+ authentication_method: 0,
+ provider: "https://kudos.demo.anastasis.lu/",
+ },
+ {
+ authentication_method: 3,
+ provider: "https://anastasis.demo.taler.net/",
+ },
+ {
+ authentication_method: 4,
+ provider: "https://anastasis.demo.taler.net/",
+ },
+ ],
+ },
+ {
+ methods: [
+ {
+ authentication_method: 1,
+ provider: "https://kudos.demo.anastasis.lu/",
+ },
+ {
+ authentication_method: 2,
+ provider: "https://kudos.demo.anastasis.lu/",
+ },
+ {
+ authentication_method: 3,
+ provider: "https://anastasis.demo.taler.net/",
+ },
+ ],
+ },
+ {
+ methods: [
+ {
+ authentication_method: 1,
+ provider: "https://kudos.demo.anastasis.lu/",
+ },
+ {
+ authentication_method: 2,
+ provider: "https://kudos.demo.anastasis.lu/",
+ },
+ {
+ authentication_method: 4,
+ provider: "https://anastasis.demo.taler.net/",
+ },
+ ],
+ },
+ {
+ methods: [
+ {
+ authentication_method: 1,
+ provider: "https://kudos.demo.anastasis.lu/",
+ },
+ {
+ authentication_method: 3,
+ provider: "https://anastasis.demo.taler.net/",
+ },
+ {
+ authentication_method: 4,
+ provider: "https://anastasis.demo.taler.net/",
+ },
+ ],
+ },
+ {
+ methods: [
+ {
+ authentication_method: 2,
+ provider: "https://kudos.demo.anastasis.lu/",
+ },
+ {
+ authentication_method: 3,
+ provider: "https://anastasis.demo.taler.net/",
+ },
+ {
+ authentication_method: 4,
+ provider: "https://anastasis.demo.taler.net/",
+ },
+ ],
+ },
+ ],
+ authentication_methods: [
+ {
+ type: "email",
+ instructions: "Email to qwe@asd.com",
+ challenge: "E5VPA",
+ },
+ {
+ type: "sms",
+ instructions: "SMS to 555-555",
+ challenge: "",
+ },
+ {
+ type: "question",
+ instructions: "Does P equal NP?",
+ challenge: "C5SP8",
+ },
+ {
+ type: "totp",
+ instructions: "Response code for 'Anastasis'",
+ challenge: "E5VPA",
+ },
+ {
+ type: "sms",
+ instructions: "SMS to 6666-6666",
+ challenge: "",
+ },
+ {
+ type: "question",
+ instructions: "How did the chicken cross the road?",
+ challenge: "C5SP8",
+ },
+ ],
+ } as ReducerState,
+);
diff --git a/packages/anastasis-webui/src/pages/home/SecretEditorScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/SecretEditorScreen.stories.tsx
index dbf8bf128..24bbb2927 100644
--- a/packages/anastasis-webui/src/pages/home/SecretEditorScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/SecretEditorScreen.stories.tsx
@@ -20,8 +20,9 @@
*/
import { ReducerState } from "@gnu-taler/anastasis-core";
-import { createExample, reducerStatesExample } from "../../utils/index.js";
+import { reducerStatesExample } from "../../utils/index.js";
import { SecretEditorScreen as TestedComponent } from "./SecretEditorScreen.js";
+import * as tests from "@gnu-taler/web-util/testing";
export default {
title: "Secret editor",
@@ -35,11 +36,15 @@ export default {
},
};
-export const WithSecretNamePreselected = createExample(TestedComponent, {
- ...reducerStatesExample.secretEdition,
- secret_name: "someSecretName",
-} as ReducerState);
+export const WithSecretNamePreselected = tests.createExample(
+ TestedComponent,
+ {},
+ {
+ ...reducerStatesExample.secretEdition,
+ secret_name: "someSecretName",
+ } as ReducerState,
+);
-export const WithoutName = createExample(TestedComponent, {
+export const WithoutName = tests.createExample(TestedComponent, {}, {
...reducerStatesExample.secretEdition,
} as ReducerState);
diff --git a/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.stories.tsx
index 7669668ee..fb3b26e15 100644
--- a/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.stories.tsx
@@ -20,7 +20,8 @@
*/
import { ReducerState } from "@gnu-taler/anastasis-core";
-import { createExample, reducerStatesExample } from "../../utils/index.js";
+import * as tests from "@gnu-taler/web-util/testing";
+import { reducerStatesExample } from "../../utils/index.js";
import {
SecretSelectionScreen,
SecretSelectionScreenFound,
@@ -34,17 +35,9 @@ export default {
},
};
-export const Example = createExample(
+export const Example = tests.createExample(
SecretSelectionScreenFound,
{
- ...reducerStatesExample.secretSelection,
- recovery_document: {
- provider_url: "https://kudos.demo.anastasis.lu/",
- secret_name: "secretName",
- version: 1,
- },
- } as ReducerState,
- {
policies: [
{
secret_name: "The secret name 1",
@@ -70,9 +63,21 @@ export const Example = createExample(
},
],
},
+ {
+ ...reducerStatesExample.secretSelection,
+ recovery_document: {
+ provider_url: "https://kudos.demo.anastasis.lu/",
+ secret_name: "secretName",
+ version: 1,
+ },
+ } as ReducerState,
);
-export const NoRecoveryDocumentFound = createExample(SecretSelectionScreen, {
- ...reducerStatesExample.secretSelection,
- recovery_document: undefined,
-} as ReducerState);
+export const NoRecoveryDocumentFound = tests.createExample(
+ SecretSelectionScreen,
+ {},
+ {
+ ...reducerStatesExample.secretSelection,
+ recovery_document: undefined,
+ } as ReducerState,
+);
diff --git a/packages/anastasis-webui/src/pages/home/SolveScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/SolveScreen.stories.tsx
index 1058ae126..dc707a052 100644
--- a/packages/anastasis-webui/src/pages/home/SolveScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/SolveScreen.stories.tsx
@@ -20,8 +20,9 @@
*/
import { ReducerState } from "@gnu-taler/anastasis-core";
-import { createExample, reducerStatesExample } from "../../utils/index.js";
+import { reducerStatesExample } from "../../utils/index.js";
import { SolveScreen as TestedComponent } from "./SolveScreen.js";
+import * as tests from "@gnu-taler/web-util/testing";
export default {
title: "Solve Screen",
@@ -35,12 +36,12 @@ export default {
},
};
-export const NoInformation = createExample(
+export const NoInformation = tests.createExample(
TestedComponent,
reducerStatesExample.challengeSolving,
);
-export const NotSupportedChallenge = createExample(TestedComponent, {
+export const NotSupportedChallenge = tests.createExample(TestedComponent, {}, {
...reducerStatesExample.challengeSolving,
recovery_information: {
challenges: [
@@ -55,7 +56,7 @@ export const NotSupportedChallenge = createExample(TestedComponent, {
selected_challenge_uuid: "ASDASDSAD!1",
} as ReducerState);
-export const MismatchedChallengeId = createExample(TestedComponent, {
+export const MismatchedChallengeId = tests.createExample(TestedComponent, {}, {
...reducerStatesExample.challengeSolving,
recovery_information: {
challenges: [
diff --git a/packages/anastasis-webui/src/pages/home/StartScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/StartScreen.stories.tsx
index 960426098..1f6145345 100644
--- a/packages/anastasis-webui/src/pages/home/StartScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/StartScreen.stories.tsx
@@ -19,7 +19,8 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { createExample, reducerStatesExample } from "../../utils/index.js";
+import * as tests from "@gnu-taler/web-util/testing";
+import { reducerStatesExample } from "../../utils/index.js";
import { StartScreen as TestedComponent } from "./StartScreen.js";
export default {
@@ -34,7 +35,8 @@ export default {
},
};
-export const InitialState = createExample(
+export const InitialState = tests.createExample(
TestedComponent,
+ {},
reducerStatesExample.initial,
);
diff --git a/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.stories.tsx
index 40ed5117c..424c4884a 100644
--- a/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.stories.tsx
@@ -20,8 +20,9 @@
*/
import { ReducerState } from "@gnu-taler/anastasis-core";
-import { createExample, reducerStatesExample } from "../../utils/index.js";
+import { reducerStatesExample } from "../../utils/index.js";
import { TruthsPayingScreen as TestedComponent } from "./TruthsPayingScreen.js";
+import * as tests from "@gnu-taler/web-util/testing";
export default {
title: "Truths Paying",
@@ -35,11 +36,12 @@ export default {
},
};
-export const Example = createExample(
+export const Example = tests.createExample(
TestedComponent,
+ {},
reducerStatesExample.truthsPaying,
);
-export const WithPaytoList = createExample(TestedComponent, {
+export const WithPaytoList = tests.createExample(TestedComponent, {}, {
...reducerStatesExample.truthsPaying,
payments: ["payto://x-taler-bank/bank/account"],
} as ReducerState);
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSetup.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSetup.stories.tsx
index 4a2d76ca3..aee7829ff 100644
--- a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSetup.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSetup.stories.tsx
@@ -19,7 +19,8 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { createExample, reducerStatesExample } from "../../../utils/index.js";
+import * as tests from "@gnu-taler/web-util/testing";
+import { reducerStatesExample } from "../../../utils/index.js";
import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
export default {
@@ -36,17 +37,16 @@ export default {
const type: KnownAuthMethods = "email";
-export const Empty = createExample(
+export const Empty = tests.createExample(
TestedComponent[type].setup,
- reducerStatesExample.authEditing,
{
configured: [],
},
+ reducerStatesExample.authEditing,
);
-export const WithOneExample = createExample(
+export const WithOneExample = tests.createExample(
TestedComponent[type].setup,
- reducerStatesExample.authEditing,
{
configured: [
{
@@ -57,11 +57,11 @@ export const WithOneExample = createExample(
},
],
},
+ reducerStatesExample.authEditing,
);
-export const WithMoreExamples = createExample(
+export const WithMoreExamples = tests.createExample(
TestedComponent[type].setup,
- reducerStatesExample.authEditing,
{
configured: [
{
@@ -78,4 +78,5 @@ export const WithMoreExamples = createExample(
},
],
},
+ reducerStatesExample.authEditing,
);
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSolve.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSolve.stories.tsx
index cc378d8f6..075bab2a7 100644
--- a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSolve.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSolve.stories.tsx
@@ -23,8 +23,9 @@ import {
ChallengeFeedbackStatus,
ReducerState,
} from "@gnu-taler/anastasis-core";
-import { createExample, reducerStatesExample } from "../../../utils/index.js";
+import { reducerStatesExample } from "../../../utils/index.js";
import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
+import * as tests from "@gnu-taler/web-util/testing";
export default {
title: "Auth method: Email solve",
@@ -40,9 +41,12 @@ export default {
const type: KnownAuthMethods = "email";
-export const WithoutFeedback = createExample(
+export const WithoutFeedback = tests.createExample(
TestedComponent[type].solve,
{
+ id: "uuid-1",
+ },
+ {
...reducerStatesExample.challengeSolving,
recovery_information: {
challenges: [
@@ -56,14 +60,14 @@ export const WithoutFeedback = createExample(
},
selected_challenge_uuid: "uuid-1",
} as ReducerState,
- {
- id: "uuid-1",
- },
);
-export const PaymentFeedback = createExample(
+export const PaymentFeedback = tests.createExample(
TestedComponent[type].solve,
{
+ id: "uuid-1",
+ },
+ {
...reducerStatesExample.challengeSolving,
recovery_information: {
challenges: [
@@ -85,7 +89,4 @@ export const PaymentFeedback = createExample(
},
},
} as ReducerState,
- {
- id: "uuid-1",
- },
);
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSetup.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSetup.stories.tsx
index dfe3850f1..d571093f7 100644
--- a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSetup.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSetup.stories.tsx
@@ -19,7 +19,8 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { createExample, reducerStatesExample } from "../../../utils/index.js";
+import * as tests from "@gnu-taler/web-util/testing";
+import { reducerStatesExample } from "../../../utils/index.js";
import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
export default {
@@ -36,17 +37,16 @@ export default {
const type: KnownAuthMethods = "iban";
-export const Empty = createExample(
+export const Empty = tests.createExample(
TestedComponent[type].setup,
- reducerStatesExample.authEditing,
{
configured: [],
},
+ reducerStatesExample.authEditing,
);
-export const WithOneExample = createExample(
+export const WithOneExample = tests.createExample(
TestedComponent[type].setup,
- reducerStatesExample.authEditing,
{
configured: [
{
@@ -57,10 +57,10 @@ export const WithOneExample = createExample(
},
],
},
+ reducerStatesExample.authEditing,
);
-export const WithMoreExamples = createExample(
+export const WithMoreExamples = tests.createExample(
TestedComponent[type].setup,
- reducerStatesExample.authEditing,
{
configured: [
{
@@ -77,4 +77,5 @@ export const WithMoreExamples = createExample(
},
],
},
+ reducerStatesExample.authEditing,
);
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSolve.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSolve.stories.tsx
index 8a9a3f7a0..2a16c8456 100644
--- a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSolve.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSolve.stories.tsx
@@ -20,8 +20,9 @@
*/
import { ReducerState } from "@gnu-taler/anastasis-core";
-import { createExample, reducerStatesExample } from "../../../utils/index.js";
-import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
+import * as tests from "@gnu-taler/web-util/testing";
+import { reducerStatesExample } from "../../../utils/index.js";
+import { KnownAuthMethods, authMethods as TestedComponent } from "./index.js";
export default {
title: "Auth method: IBAN Solve",
@@ -37,9 +38,12 @@ export default {
const type: KnownAuthMethods = "iban";
-export const WithoutFeedback = createExample(
+export const WithoutFeedback = tests.createExample(
TestedComponent[type].solve,
{
+ id: "uuid-1",
+ },
+ {
...reducerStatesExample.challengeSolving,
recovery_information: {
challenges: [
@@ -53,7 +57,4 @@ export const WithoutFeedback = createExample(
},
selected_challenge_uuid: "uuid-1",
} as ReducerState,
- {
- id: "uuid-1",
- },
);
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSetup.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSetup.stories.tsx
index 8a32c45c1..a893c923e 100644
--- a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSetup.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSetup.stories.tsx
@@ -19,7 +19,8 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { createExample, reducerStatesExample } from "../../../utils/index.js";
+import * as tests from "@gnu-taler/web-util/testing";
+import { reducerStatesExample } from "../../../utils/index.js";
import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
export default {
@@ -36,17 +37,16 @@ export default {
const type: KnownAuthMethods = "post";
-export const Empty = createExample(
+export const Empty = tests.createExample(
TestedComponent[type].setup,
- reducerStatesExample.authEditing,
{
configured: [],
},
+ reducerStatesExample.authEditing,
);
-export const WithOneExample = createExample(
+export const WithOneExample = tests.createExample(
TestedComponent[type].setup,
- reducerStatesExample.authEditing,
{
configured: [
{
@@ -57,11 +57,11 @@ export const WithOneExample = createExample(
},
],
},
+ reducerStatesExample.authEditing,
);
-export const WithMoreExamples = createExample(
+export const WithMoreExamples = tests.createExample(
TestedComponent[type].setup,
- reducerStatesExample.authEditing,
{
configured: [
{
@@ -78,4 +78,5 @@ export const WithMoreExamples = createExample(
},
],
},
+ reducerStatesExample.authEditing,
);
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSolve.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSolve.stories.tsx
index 702ba2810..3495f7f63 100644
--- a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSolve.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSolve.stories.tsx
@@ -20,8 +20,9 @@
*/
import { ReducerState } from "@gnu-taler/anastasis-core";
-import { createExample, reducerStatesExample } from "../../../utils/index.js";
+import { reducerStatesExample } from "../../../utils/index.js";
import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
+import * as tests from "@gnu-taler/web-util/testing";
export default {
title: "Auth method: Post solve",
@@ -37,9 +38,12 @@ export default {
const type: KnownAuthMethods = "post";
-export const WithoutFeedback = createExample(
+export const WithoutFeedback = tests.createExample(
TestedComponent[type].solve,
{
+ id: "uuid-1",
+ },
+ {
...reducerStatesExample.challengeSolving,
recovery_information: {
challenges: [
@@ -53,7 +57,4 @@ export const WithoutFeedback = createExample(
},
selected_challenge_uuid: "uuid-1",
} as ReducerState,
- {
- id: "uuid-1",
- },
);
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSetup.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSetup.stories.tsx
index 2e108b4e6..c9bc127f7 100644
--- a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSetup.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSetup.stories.tsx
@@ -19,7 +19,8 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { createExample, reducerStatesExample } from "../../../utils/index.js";
+import * as tests from "@gnu-taler/web-util/testing";
+import { reducerStatesExample } from "../../../utils/index.js";
import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
export default {
@@ -36,17 +37,16 @@ export default {
const type: KnownAuthMethods = "question";
-export const Empty = createExample(
+export const Empty = tests.createExample(
TestedComponent[type].setup,
- reducerStatesExample.authEditing,
{
configured: [],
},
+ reducerStatesExample.authEditing,
);
-export const WithOneExample = createExample(
+export const WithOneExample = tests.createExample(
TestedComponent[type].setup,
- reducerStatesExample.authEditing,
{
configured: [
{
@@ -58,11 +58,11 @@ export const WithOneExample = createExample(
},
],
},
+ reducerStatesExample.authEditing,
);
-export const WithMoreExamples = createExample(
+export const WithMoreExamples = tests.createExample(
TestedComponent[type].setup,
- reducerStatesExample.authEditing,
{
configured: [
{
@@ -80,4 +80,5 @@ export const WithMoreExamples = createExample(
},
],
},
+ reducerStatesExample.authEditing,
);
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSolve.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSolve.stories.tsx
index f7116bf6f..dbb17ddab 100644
--- a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSolve.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSolve.stories.tsx
@@ -23,8 +23,10 @@ import {
ChallengeFeedbackStatus,
ReducerState,
} from "@gnu-taler/anastasis-core";
-import { createExample, reducerStatesExample } from "../../../utils/index.js";
+import { reducerStatesExample } from "../../../utils/index.js";
import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
+import * as tests from "@gnu-taler/web-util/testing";
+import { AmountString } from "@gnu-taler/taler-util";
export default {
title: "Auth method: Question solve",
@@ -40,9 +42,12 @@ export default {
const type: KnownAuthMethods = "question";
-export const WithoutFeedback = createExample(
+export const WithoutFeedback = tests.createExample(
TestedComponent[type].solve,
{
+ id: "uuid-1",
+ },
+ {
...reducerStatesExample.challengeSolving,
recovery_information: {
challenges: [
@@ -56,9 +61,6 @@ export const WithoutFeedback = createExample(
},
selected_challenge_uuid: "uuid-1",
} as ReducerState,
- {
- id: "uuid-1",
- },
);
const recovery_information = {
@@ -72,45 +74,58 @@ const recovery_information = {
policies: [],
};
-export const CodeInFileFeedback = createExample(TestedComponent[type].solve, {
- ...reducerStatesExample.challengeSolving,
- recovery_information,
- selected_challenge_uuid: "ASDASDSAD!1",
- challenge_feedback: {
- "ASDASDSAD!1": {
- state: ChallengeFeedbackStatus.CodeInFile,
- filename: "asd",
- display_hint: "hint",
+export const CodeInFileFeedback = tests.createExample(
+ TestedComponent[type].solve,
+ {},
+ {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information,
+ selected_challenge_uuid: "ASDASDSAD!1",
+ challenge_feedback: {
+ "ASDASDSAD!1": {
+ state: ChallengeFeedbackStatus.CodeInFile,
+ filename: "asd",
+ display_hint: "hint",
+ },
},
- },
-} as ReducerState);
-
-export const CodeSentFeedback = createExample(TestedComponent[type].solve, {
- ...reducerStatesExample.challengeSolving,
- recovery_information,
- selected_challenge_uuid: "ASDASDSAD!1",
- challenge_feedback: {
- "ASDASDSAD!1": {
- state: ChallengeFeedbackStatus.CodeSent,
- address_hint: "asdasd",
- display_hint: "qweqweqw",
+ } as ReducerState,
+);
+
+export const CodeSentFeedback = tests.createExample(
+ TestedComponent[type].solve,
+ {},
+ {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information,
+ selected_challenge_uuid: "ASDASDSAD!1",
+ challenge_feedback: {
+ "ASDASDSAD!1": {
+ state: ChallengeFeedbackStatus.CodeSent,
+ address_hint: "asdasd",
+ display_hint: "qweqweqw",
+ },
},
- },
-} as ReducerState);
-
-export const SolvedFeedback = createExample(TestedComponent[type].solve, {
- ...reducerStatesExample.challengeSolving,
- recovery_information,
- selected_challenge_uuid: "ASDASDSAD!1",
- challenge_feedback: {
- "ASDASDSAD!1": {
- state: ChallengeFeedbackStatus.Solved,
+ } as ReducerState,
+);
+
+export const SolvedFeedback = tests.createExample(
+ TestedComponent[type].solve,
+ {},
+ {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information,
+ selected_challenge_uuid: "ASDASDSAD!1",
+ challenge_feedback: {
+ "ASDASDSAD!1": {
+ state: ChallengeFeedbackStatus.Solved,
+ },
},
- },
-} as ReducerState);
+ } as ReducerState,
+);
-export const ServerFailureFeedback = createExample(
+export const ServerFailureFeedback = tests.createExample(
TestedComponent[type].solve,
+ {},
{
...reducerStatesExample.challengeSolving,
recovery_information,
@@ -124,45 +139,58 @@ export const ServerFailureFeedback = createExample(
} as ReducerState,
);
-export const TruthUnknownFeedback = createExample(TestedComponent[type].solve, {
- ...reducerStatesExample.challengeSolving,
- recovery_information,
- selected_challenge_uuid: "ASDASDSAD!1",
- challenge_feedback: {
- "ASDASDSAD!1": {
- state: ChallengeFeedbackStatus.TruthUnknown,
+export const TruthUnknownFeedback = tests.createExample(
+ TestedComponent[type].solve,
+ {},
+ {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information,
+ selected_challenge_uuid: "ASDASDSAD!1",
+ challenge_feedback: {
+ "ASDASDSAD!1": {
+ state: ChallengeFeedbackStatus.TruthUnknown,
+ },
},
- },
-} as ReducerState);
-
-export const TalerPaymentFeedback = createExample(TestedComponent[type].solve, {
- ...reducerStatesExample.challengeSolving,
- recovery_information,
- selected_challenge_uuid: "ASDASDSAD!1",
- challenge_feedback: {
- "ASDASDSAD!1": {
- state: ChallengeFeedbackStatus.TalerPayment,
- payment_secret: "secret",
- provider: "asdasdas",
- taler_pay_uri: "taler://pay/...",
+ } as ReducerState,
+);
+
+export const TalerPaymentFeedback = tests.createExample(
+ TestedComponent[type].solve,
+ {},
+ {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information,
+ selected_challenge_uuid: "ASDASDSAD!1",
+ challenge_feedback: {
+ "ASDASDSAD!1": {
+ state: ChallengeFeedbackStatus.TalerPayment,
+ payment_secret: "secret",
+ provider: "asdasdas",
+ taler_pay_uri: "taler://pay/...",
+ },
},
- },
-} as ReducerState);
-
-export const UnsupportedFeedback = createExample(TestedComponent[type].solve, {
- ...reducerStatesExample.challengeSolving,
- recovery_information,
- selected_challenge_uuid: "ASDASDSAD!1",
- challenge_feedback: {
- "ASDASDSAD!1": {
- state: ChallengeFeedbackStatus.Unsupported,
- unsupported_method: "method",
+ } as ReducerState,
+);
+
+export const UnsupportedFeedback = tests.createExample(
+ TestedComponent[type].solve,
+ {},
+ {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information,
+ selected_challenge_uuid: "ASDASDSAD!1",
+ challenge_feedback: {
+ "ASDASDSAD!1": {
+ state: ChallengeFeedbackStatus.Unsupported,
+ unsupported_method: "method",
+ },
},
- },
-} as ReducerState);
+ } as ReducerState,
+);
-export const RateLimitExceededFeedback = createExample(
+export const RateLimitExceededFeedback = tests.createExample(
TestedComponent[type].solve,
+ {},
{
...reducerStatesExample.challengeSolving,
recovery_information,
@@ -175,8 +203,9 @@ export const RateLimitExceededFeedback = createExample(
} as ReducerState,
);
-export const IbanInstructionsFeedback = createExample(
+export const IbanInstructionsFeedback = tests.createExample(
TestedComponent[type].solve,
+ {},
{
...reducerStatesExample.challengeSolving,
recovery_information,
@@ -184,7 +213,7 @@ export const IbanInstructionsFeedback = createExample(
challenge_feedback: {
"ASDASDSAD!1": {
state: ChallengeFeedbackStatus.IbanInstructions,
- challenge_amount: "EUR:1",
+ challenge_amount: "EUR:1" as AmountString,
target_iban: "DE12345789000",
target_business_name: "Data Loss Incorporated",
wire_transfer_subject: "Anastasis 987654321",
@@ -194,8 +223,9 @@ export const IbanInstructionsFeedback = createExample(
} as ReducerState,
);
-export const IncorrectAnswerFeedback = createExample(
+export const IncorrectAnswerFeedback = tests.createExample(
TestedComponent[type].solve,
+ {},
{
...reducerStatesExample.challengeSolving,
recovery_information,
@@ -207,44 +237,3 @@ export const IncorrectAnswerFeedback = createExample(
},
} as ReducerState,
);
-
-// export const AuthIbanFeedback = createExample(TestedComponent[type].solve, {
-// ...reducerStatesExample.challengeSolving,
-// recovery_information: {
-// challenges: [
-// {
-// instructions: "does P equals NP?",
-// type: "question",
-// uuid: "ASDASDSAD!1",
-// },
-// ],
-// policies: [],
-// },
-// selected_challenge_uuid: "ASDASDSAD!1",
-// challenge_feedback: {
-// "ASDASDSAD!1": ibanFeedback,
-// },
-// } as ReducerState);
-
-// export const PaymentFeedback = createExample(TestedComponent[type].solve, {
-// ...reducerStatesExample.challengeSolving,
-// recovery_information: {
-// challenges: [
-// {
-// instructions: "does P equals NP?",
-// type: "question",
-// uuid: "ASDASDSAD!1",
-// },
-// ],
-// policies: [],
-// },
-// selected_challenge_uuid: "ASDASDSAD!1",
-// challenge_feedback: {
-// "ASDASDSAD!1": {
-// state: ChallengeFeedbackStatus.TalerPayment,
-// taler_pay_uri: "taler://pay/...",
-// provider: "https://localhost:8080/",
-// payment_secret: "3P4561HAMHRRYEYD6CM6J7TS5VTD5SR2K2EXJDZEFSX92XKHR4KG",
-// },
-// },
-// } as ReducerState);
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSetup.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSetup.stories.tsx
index b2c6cb61d..fbf345779 100644
--- a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSetup.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSetup.stories.tsx
@@ -19,7 +19,8 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { createExample, reducerStatesExample } from "../../../utils/index.js";
+import * as tests from "@gnu-taler/web-util/testing";
+import { reducerStatesExample } from "../../../utils/index.js";
import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
export default {
@@ -36,17 +37,16 @@ export default {
const type: KnownAuthMethods = "sms";
-export const Empty = createExample(
+export const Empty = tests.createExample(
TestedComponent[type].setup,
- reducerStatesExample.authEditing,
{
configured: [],
},
+ reducerStatesExample.authEditing,
);
-export const WithOneExample = createExample(
+export const WithOneExample = tests.createExample(
TestedComponent[type].setup,
- reducerStatesExample.authEditing,
{
configured: [
{
@@ -57,11 +57,11 @@ export const WithOneExample = createExample(
},
],
},
+ reducerStatesExample.authEditing,
);
-export const WithMoreExamples = createExample(
+export const WithMoreExamples = tests.createExample(
TestedComponent[type].setup,
- reducerStatesExample.authEditing,
{
configured: [
{
@@ -78,4 +78,5 @@ export const WithMoreExamples = createExample(
},
],
},
+ reducerStatesExample.authEditing,
);
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSolve.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSolve.stories.tsx
index 2064f12ff..8e3fb1a16 100644
--- a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSolve.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSolve.stories.tsx
@@ -20,8 +20,9 @@
*/
import { ReducerState } from "@gnu-taler/anastasis-core";
-import { createExample, reducerStatesExample } from "../../../utils/index.js";
+import { reducerStatesExample } from "../../../utils/index.js";
import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
+import * as tests from "@gnu-taler/web-util/testing";
export default {
title: "Auth method: SMS solve",
@@ -37,9 +38,12 @@ export default {
const type: KnownAuthMethods = "sms";
-export const WithoutFeedback = createExample(
+export const WithoutFeedback = tests.createExample(
TestedComponent[type].solve,
{
+ id: "AHCC4ZJ3Z1AF8TWBKGVGEKCQ3R7HXHJ51MJ45NHNZMHYZTKJ9NW0",
+ },
+ {
...reducerStatesExample.challengeSolving,
recovery_information: {
challenges: [
@@ -54,7 +58,4 @@ export const WithoutFeedback = createExample(
selected_challenge_uuid:
"AHCC4ZJ3Z1AF8TWBKGVGEKCQ3R7HXHJ51MJ45NHNZMHYZTKJ9NW0",
} as ReducerState,
- {
- id: "AHCC4ZJ3Z1AF8TWBKGVGEKCQ3R7HXHJ51MJ45NHNZMHYZTKJ9NW0",
- },
);
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSetup.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSetup.stories.tsx
index 5582590f7..ee66fcee1 100644
--- a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSetup.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSetup.stories.tsx
@@ -19,7 +19,8 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { createExample, reducerStatesExample } from "../../../utils/index.js";
+import * as tests from "@gnu-taler/web-util/testing";
+import { reducerStatesExample } from "../../../utils/index.js";
import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
export default {
@@ -36,16 +37,15 @@ export default {
const type: KnownAuthMethods = "totp";
-export const Empty = createExample(
+export const Empty = tests.createExample(
TestedComponent[type].setup,
- reducerStatesExample.authEditing,
{
configured: [],
},
+ reducerStatesExample.authEditing,
);
-export const WithOneExample = createExample(
+export const WithOneExample = tests.createExample(
TestedComponent[type].setup,
- reducerStatesExample.authEditing,
{
configured: [
{
@@ -56,10 +56,10 @@ export const WithOneExample = createExample(
},
],
},
+ reducerStatesExample.authEditing,
);
-export const WithMoreExample = createExample(
+export const WithMoreExample = tests.createExample(
TestedComponent[type].setup,
- reducerStatesExample.authEditing,
{
configured: [
{
@@ -76,4 +76,5 @@ export const WithMoreExample = createExample(
},
],
},
+ reducerStatesExample.authEditing,
);
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSolve.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSolve.stories.tsx
index 20cd7e3c9..c120aaadc 100644
--- a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSolve.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSolve.stories.tsx
@@ -20,8 +20,9 @@
*/
import { ReducerState } from "@gnu-taler/anastasis-core";
-import { createExample, reducerStatesExample } from "../../../utils/index.js";
-import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
+import * as tests from "@gnu-taler/web-util/testing";
+import { reducerStatesExample } from "../../../utils/index.js";
+import { KnownAuthMethods, authMethods as TestedComponent } from "./index.js";
export default {
title: "Auth method: Totp solve",
@@ -37,9 +38,12 @@ export default {
const type: KnownAuthMethods = "totp";
-export const WithoutFeedback = createExample(
+export const WithoutFeedback = tests.createExample(
TestedComponent[type].solve,
{
+ id: "uuid-1",
+ },
+ {
...reducerStatesExample.challengeSolving,
recovery_information: {
challenges: [
@@ -53,7 +57,4 @@ export const WithoutFeedback = createExample(
},
selected_challenge_uuid: "uuid-1",
} as ReducerState,
- {
- id: "uuid-1",
- },
);
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/totp.ts b/packages/anastasis-webui/src/pages/home/authMethod/totp.ts
index 434dd92fc..ff8027ced 100644
--- a/packages/anastasis-webui/src/pages/home/authMethod/totp.ts
+++ b/packages/anastasis-webui/src/pages/home/authMethod/totp.ts
@@ -13,6 +13,8 @@
You should have received a copy of the GNU Affero General Public License along with
GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+
+//@ts-ignore
import jssha from "jssha";
const SEARCH_RANGE = 16;
diff --git a/packages/anastasis-webui/src/pages/home/index.storiesNo.tsx b/packages/anastasis-webui/src/pages/home/index.stories.tsx
index 0dad73724..b4525b423 100644
--- a/packages/anastasis-webui/src/pages/home/index.storiesNo.tsx
+++ b/packages/anastasis-webui/src/pages/home/index.stories.tsx
@@ -35,6 +35,7 @@ export * as authMethod_AuthMethodSmsSetup from "./authMethod/AuthMethodSmsSetup.
export * as authMethod_AuthMethodSmsSolve from "./authMethod/AuthMethodSmsSolve.stories.js";
export * as authMethod_AuthMethodTotpSetup from "./authMethod/AuthMethodTotpSetup.stories.js";
export * as authMethod_AuthMethodTotpSolve from "./authMethod/AuthMethodTotpSolve.stories.js";
+
export * as BackupFinishedScreen from "./BackupFinishedScreen.stories.js";
export * as ChallengeOverviewScreen from "./ChallengeOverviewScreen.stories.js";
export * as ChallengePayingScreen from "./ChallengePayingScreen.stories.js";
@@ -42,6 +43,7 @@ export * as ContinentSelectionScreen from "./ContinentSelectionScreen.stories.js
export * as EditPoliciesScreen from "./EditPoliciesScreen.stories.js";
export * as PoliciesPayingScreen from "./PoliciesPayingScreen.stories.js";
export * as RecoveryFinishedScreen from "./RecoveryFinishedScreen.stories.js";
+
export * as ReviewPoliciesScreen from "./ReviewPoliciesScreen.stories.js";
export * as SecretEditorScreen from "./SecretEditorScreen.stories.js";
export * as SecretSelectionScreen from "./SecretSelectionScreen.stories.js";
diff --git a/packages/anastasis-webui/src/pages/home/index.tsx b/packages/anastasis-webui/src/pages/home/index.tsx
index 44e065807..c665144a4 100644
--- a/packages/anastasis-webui/src/pages/home/index.tsx
+++ b/packages/anastasis-webui/src/pages/home/index.tsx
@@ -228,6 +228,8 @@ function AnastasisClientImpl(): VNode {
return <StartScreen />;
}
+ // FIXME: Use switch statements here!
+
if (
(state.reducer_type === "backup" &&
state.backup_state === BackupStates.ContinentSelecting) ||
diff --git a/packages/anastasis-webui/src/stories.tsx b/packages/anastasis-webui/src/stories.tsx
index f345f082d..cdaa4022f 100644
--- a/packages/anastasis-webui/src/stories.tsx
+++ b/packages/anastasis-webui/src/stories.tsx
@@ -20,9 +20,9 @@
*/
import { strings } from "./i18n/strings.js";
-import * as pages from "./pages/home/index.storiesNo.js";
+import * as pages from "./pages/home/index.stories.js";
-import { renderStories } from "@gnu-taler/web-util/lib/index.browser";
+import { renderStories } from "@gnu-taler/web-util/browser";
import "./scss/main.scss";
diff --git a/packages/anastasis-webui/src/test-utils.ts b/packages/anastasis-webui/src/test-utils.ts
deleted file mode 100644
index f220540f1..000000000
--- a/packages/anastasis-webui/src/test-utils.ts
+++ /dev/null
@@ -1,205 +0,0 @@
-/*
- This file is part of GNU Anastasis
- (C) 2021-2022 Anastasis SARL
-
- GNU Anastasis is free software; you can redistribute it and/or modify it under the
- terms of the GNU Affero General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Anastasis is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License along with
- GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-import {
- ComponentChildren,
- Fragment,
- FunctionalComponent,
- h as create,
- options,
- render as renderIntoDom,
- VNode,
-} from "preact";
-import { render as renderToString } from "preact-render-to-string";
-
-// When doing tests we want the requestAnimationFrame to be as fast as possible.
-// without this option the RAF will timeout after 100ms making the tests slower
-options.requestAnimationFrame = (fn: () => void) => {
- // console.log("RAF called")
- return fn();
-};
-
-export function createExample<Props>(
- Component: FunctionalComponent<Props>,
- props: Partial<Props> | (() => Partial<Props>),
-): ComponentChildren {
- //FIXME: props are evaluated on build time
- // in some cases we want to evaluated the props on render time so we can get some relative timestamp
- // check how we can build evaluatedProps in render time
- const evaluatedProps = typeof props === "function" ? props() : props;
- const Render = (args: any): VNode => create(Component, args);
- return {
- component: Render,
- props: evaluatedProps
- };
-}
-
-export function createExampleWithCustomContext<Props, ContextProps>(
- Component: FunctionalComponent<Props>,
- props: Partial<Props> | (() => Partial<Props>),
- ContextProvider: FunctionalComponent<ContextProps>,
- contextProps: Partial<ContextProps>,
-): ComponentChildren {
- const evaluatedProps = typeof props === "function" ? props() : props;
- const Render = (args: any): VNode => create(Component, args);
- const WithContext = (args: any): VNode =>
- create(ContextProvider, {
- ...contextProps,
- children: [Render(args)],
- } as any);
- return {
- component: WithContext,
- props: evaluatedProps
- };
-}
-
-export function NullLink({
- children,
-}: {
- children?: ComponentChildren;
-}): VNode {
- return create("a", { children, href: "javascript:void(0);" });
-}
-
-export function renderNodeOrBrowser(Component: any, args: any): void {
- const vdom = create(Component, args);
- if (typeof window === "undefined") {
- renderToString(vdom);
- } else {
- const div = document.createElement("div");
- document.body.appendChild(div);
- renderIntoDom(vdom, div);
- renderIntoDom(null, div);
- document.body.removeChild(div);
- }
-}
-
-interface Mounted<T> {
- unmount: () => void;
- getLastResultOrThrow: () => T;
- assertNoPendingUpdate: () => void;
- waitNextUpdate: (s?: string) => Promise<void>;
-}
-
-const isNode = typeof window === "undefined";
-
-export function mountHook<T>(
- callback: () => T,
- Context?: ({ children }: { children: any }) => VNode,
-): Mounted<T> {
- // const result: { current: T | null } = {
- // current: null
- // }
- let lastResult: T | Error | null = null;
-
- const listener: Array<() => void> = [];
-
- // component that's going to hold the hook
- function Component(): VNode {
- try {
- lastResult = callback();
- } catch (e) {
- if (e instanceof Error) {
- lastResult = e;
- } else {
- lastResult = new Error(`mounting the hook throw an exception: ${e}`);
- }
- }
-
- // notify to everyone waiting for an update and clean the queue
- listener.splice(0, listener.length).forEach((cb) => cb());
- return create(Fragment, {});
- }
-
- // create the vdom with context if required
- const vdom = !Context
- ? create(Component, {})
- : create(Context, { children: [create(Component, {})] });
-
- // waiter callback
- async function waitNextUpdate(_label = ""): Promise<void> {
- if (_label) _label = `. label: "${_label}"`;
- await new Promise((res, rej) => {
- const tid = setTimeout(() => {
- rej(
- Error(`waiting for an update but the hook didn't make one${_label}`),
- );
- }, 100);
-
- listener.push(() => {
- clearTimeout(tid);
- res(undefined);
- });
- });
- }
-
- const customElement = {} as Element;
- const parentElement = isNode ? customElement : document.createElement("div");
- if (!isNode) {
- document.body.appendChild(parentElement);
- }
-
- renderIntoDom(vdom, parentElement);
-
- // clean up callback
- function unmount(): void {
- if (!isNode) {
- document.body.removeChild(parentElement);
- }
- }
-
- function getLastResult(): T | Error | null {
- const copy = lastResult;
- lastResult = null;
- return copy;
- }
-
- function getLastResultOrThrow(): T {
- const r = getLastResult();
- if (r instanceof Error) throw r;
- if (!r) throw Error("there was no last result");
- return r;
- }
-
- async function assertNoPendingUpdate(): Promise<void> {
- await new Promise((res, rej) => {
- const tid = setTimeout(() => {
- res(undefined);
- }, 10);
-
- listener.push(() => {
- clearTimeout(tid);
- rej(
- Error(`Expecting no pending result but the hook got updated.
- If the update was not intended you need to check the hook dependencies
- (or dependencies of the internal state) but otherwise make
- sure to consume the result before ending the test.`),
- );
- });
- });
-
- const r = getLastResult();
- if (r)
- throw Error(`There are still pending results.
- This may happen because the hook did a new update but the test didn't consume the result using getLastResult`);
- }
- return {
- unmount,
- getLastResultOrThrow,
- waitNextUpdate,
- assertNoPendingUpdate,
- };
-}
diff --git a/packages/anastasis-webui/src/utils/index.tsx b/packages/anastasis-webui/src/utils/index.tsx
index 4cf839473..88bcac551 100644
--- a/packages/anastasis-webui/src/utils/index.tsx
+++ b/packages/anastasis-webui/src/utils/index.tsx
@@ -21,67 +21,12 @@ import {
ReducerState,
ReducerStateRecovery,
} from "@gnu-taler/anastasis-core";
-import { ComponentChildren, FunctionalComponent, h, VNode } from "preact";
-import { AnastasisProvider } from "../context/anastasis.js";
+import { VNode } from "preact";
const noop = async (): Promise<void> => {
return;
};
-export function createExampleWithoutAnastasis<Props>(
- Component: FunctionalComponent<Props>,
- props: Partial<Props> | (() => Partial<Props>),
-): ComponentChildren {
- //FIXME: props are evaluated on build time
- // in some cases we want to evaluated the props on render time so we can get some relative timestamp
- // check how we can build evaluatedProps in render time
- const evaluatedProps = typeof props === "function" ? props() : props;
- const Render = (args: any): VNode => h(Component, args);
- return {
- component: Render,
- props: evaluatedProps,
- };
-}
-
-export function createExample<Props>(
- Component: FunctionalComponent<Props>,
- currentReducerState?: ReducerState,
- props?: Partial<Props>,
-): ComponentChildren {
- const Render = (args: Props): VNode => {
- return (
- <AnastasisProvider
- value={{
- currentReducerState,
- discoverMore: noop,
- discoverStart: noop,
- discoveryState: {
- state: "finished",
- },
- currentError: undefined,
- back: noop,
- dismissError: noop,
- reset: noop,
- runTransaction: noop,
- startBackup: noop,
- startRecover: noop,
- transition: noop,
- exportState: () => {
- return "{}";
- },
- importState: noop,
- }}
- >
- <Component {...(args as any)} />
- </AnastasisProvider>
- );
- };
- return {
- component: Render,
- props: props,
- };
-}
-
const base = {
continents: [
{
diff --git a/packages/demobank-ui/src/stories.tsx b/packages/anastasis-webui/test.mjs
index 52d42577d..ba3257de8 100644..100755
--- a/packages/demobank-ui/src/stories.tsx
+++ b/packages/anastasis-webui/test.mjs
@@ -1,3 +1,4 @@
+#!/usr/bin/env node
/*
This file is part of GNU Anastasis
(C) 2021-2022 Anastasis SARL
@@ -13,34 +14,17 @@
You should have received a copy of the GNU Affero General Public License along with
GNU Anastasis; 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 pages from "./pages/home/index.stories.js";
-
-import { renderStories } from "@gnu-taler/web-util/lib/index.browser";
-
-import "./scss/main.scss";
-
-function SortStories(a: any, b: any): number {
- return (a?.order ?? 0) - (b?.order ?? 0);
-}
-
-function main(): void {
- renderStories(
- { pages },
- {
- strings,
- },
- );
-}
-
-if (document.readyState === "loading") {
- document.addEventListener("DOMContentLoaded", main);
-} else {
- main();
-}
+import { build } from "@gnu-taler/web-util/build";
+import { getFilesInDirectory } from "@gnu-taler/web-util/build";
+
+const allTestFiles = getFilesInDirectory("src", /.test.tsx?$/);
+
+await build({
+ type: "test",
+ source: {
+ js: allTestFiles.files,
+ assets: [{base:"src",files:["src/index.html"]}],
+ },
+ destination: "./dist/prod",
+ css: "sass",
+});
diff --git a/packages/anastasis-webui/tsconfig.json b/packages/anastasis-webui/tsconfig.json
index 2d60fd47a..9e52f2b7e 100644
--- a/packages/anastasis-webui/tsconfig.json
+++ b/packages/anastasis-webui/tsconfig.json
@@ -1,46 +1,38 @@
{
"compilerOptions": {
/* Basic Options */
- "target": "ES6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */,
- "module": "ESNext" /* Specify module code generation: 'none', commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
+ "target": "ES2020",
+ "module": "Node16",
"lib": [
- "es2021",
- "dom"
- ], /* Specify library files to be included in the compilation: */
- // "allowJs": true /* Allow javascript files to be compiled. */,
+ "DOM",
+ "ES2020"
+ ],
+ "allowJs": true /* Allow javascript files to be compiled. */,
// "checkJs": true, /* Report errors in .js files. */
"jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */,
- "jsxFactory": "h" /* Specify the JSX factory function to use when targeting react JSX emit, e.g. React.createElement or h. */,
- // "declaration": true, /* Generates corresponding '.d.ts' file. */
- // "sourceMap": true, /* Generates corresponding '.map' file. */
- // "outFile": "./", /* Concatenate and emit output to single file. */
- // "outDir": "./", /* Redirect output structure to the directory. */
- // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
- // "removeComments": true, /* Do not emit comments to output. */
+ "jsxFactory": "h",
+ "jsxFragmentFactory": "Fragment",
"noEmit": true /* Do not emit outputs. */,
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true /* Enable all strict type-checking options. */,
- // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
- // "strictNullChecks": true, /* Enable strict null checks. */
- // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
- // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
+ "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
- "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
+ "moduleResolution": "Node16" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
"esModuleInterop": true /* */,
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
- // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
+ "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */,
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
/* Source Map Options */
// "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
@@ -53,16 +45,7 @@
/* Advanced Options */
"skipLibCheck": true /* Skip type checking of declaration files. */
},
- "references": [
- {
- "path": "../taler-util/"
- },
- {
- "path": "../anastasis-core/"
- }
- ],
"include": [
- "src/**/*",
- "tests/**/*"
+ "src/**/*"
]
-} \ No newline at end of file
+}
diff --git a/packages/auditor-backoffice-ui/.gitignore b/packages/auditor-backoffice-ui/.gitignore
new file mode 100644
index 000000000..df149101c
--- /dev/null
+++ b/packages/auditor-backoffice-ui/.gitignore
@@ -0,0 +1,6 @@
+/build
+/size-plugin.json
+/storybook-static
+/docs
+/single
+/coverage
diff --git a/packages/auditor-backoffice-ui/DESIGN.md b/packages/auditor-backoffice-ui/DESIGN.md
new file mode 100644
index 000000000..d6252ccdc
--- /dev/null
+++ b/packages/auditor-backoffice-ui/DESIGN.md
@@ -0,0 +1,195 @@
+# Page internal routing
+
+* The SPA is loaded from the BACKOFFICE_URL
+
+* The view to be rendered is decided by the URL fragment
+
+* Query parameters that may affect routing
+
+ - instance: use from the default instance to mimic another instance management
+
+* The user must provide BACKEND_URL or BACKOFFICE_URL will use as default
+
+* Token for querying the backend will be saved in localStorage under
+ backend-token-${name}
+
+# HTTP queries to the backend
+
+HTTP queries will have 4 states:
+
+* loading: request did not end yet. data and error are undefined
+
+* ok: data has information, http response status == 200
+
+* clientError: http response status is between 400 and 499
+
+ - notfound: http status 404
+
+ - unauthorized: http status 401
+
+* serverError: http response status is grater than 500
+
+There are categories of queries:
+
+ * sync: getting information for the page rendering
+
+ * async: performing an CRUD operation
+
+## Loading the page information (sync)
+
+In this scenario, a failed request will make the app flow to break.
+
+When receiving an not found error a generic not found page will be shown. If the
+BACKEND_URL points to a default instance it should send the user to create the
+instance.
+
+When receiving an unauthorized error, the user should be prompted with a login form.
+
+When receiving an another error (400 < http status < 600), the login form should
+be shown with an error message using the hint from the backend.
+
+On other unexpected error (like network error), the login form should be shown
+with an error message.
+
+## CRUD operation (async)
+
+In this scenario, a failed request does not break the flow but a message will be
+prompted.
+
+# Forms
+
+All the input components should be placed in the folder `src/components/from`.
+
+The core concepts are:
+
+ * <FormProvider<T> /> places instead of <form /> it should be mapped to an
+ object of type T
+
+ * <Input /> an others: defines UI, create <input /> DOM controls and access the
+ form with useField()
+
+To use it you will need a state somewhere with the object holding all the form
+information.
+
+```
+const [state, setState] = useState({ name: '', age: 11 })
+```
+
+Optionally an error object an be built with the error messages
+
+```
+const errors = {
+ field1: undefined,
+ field2: 'should be greater than 18',
+}
+```
+
+These 3 elements are used to setup the FormProvider
+
+```
+<FormProvider errors={errors} object={state} valueHandler={setState}>
+...inputs
+</FormProvider>
+```
+
+Inputs should handle UI rendering and use `useField(name)` to get:
+
+ * error: the field has been modified and the value is not correct
+ * required: the field need to be corrected
+ * value: the current value of the object
+ * initial: original value before change
+ * onChange: function to update the current field
+
+Also, every input must be ready to receive these properties
+
+ * name: property of the form object being manipulated
+ * label: how the name of the property will be shown in the UI
+ * placeholder: optional, inplace text when there is no value yet
+ * readonly: default to false, will prevent change the value
+ * help: optional, example text below the input text to help the user
+ * tooltip: optional, will add a (i) with a popup to describe the field
+
+
+# Custom Hooks
+
+All the general purpose hooks should be placed in folder `src/hooks` and tests
+under `tests/hooks`. Starts with the `use` word.
+
+# Contexts
+
+All the contexts should be placed in the folder `src/context` as a function.
+Should expose provider as a component `<XxxContextProvider />` and consumer as a
+hook function `useXxxContext()` (where XXX is the name)
+
+# Components
+
+Type of components:
+
+ * main entry point: src/index.tsx, mostly initialization
+
+ * routing: in the `src` folder, deciding who is going to take the work. That's
+ when the page is loading but also create navigation handlers
+
+ * pages: in the `paths` folder, setup page information (like querying the
+ backend for the list of things), handlers for CRUD events, delegated routing
+ to parent and UI to children.
+
+Some other guidelines:
+
+ * Hooks over classes are preferred
+
+ * Components that are ready to be reused on any place should be in
+ `src/components` folder
+
+ * Since one of the build targets is a single bundle with all the pages, we are
+ avoiding route based code splitting
+ https://github.com/preactjs/preact-cli#route-based-code-splitting
+
+
+# Testing
+
+Every components should have examples using storybook (xxx.stories.tsx). There
+is an automated test that check that every example can be rendered so we make
+sure that we do not add a regression.
+
+Every hook should have examples under `tests/hooks` with common usage trying to
+follow this structure:
+
+ * (Given) set some context of the initial condition
+
+ * (When) some action to be tested. May be the initialization of a hook or an
+ action associated with it
+
+ * (Then) a particular set of observable consequences should be expected
+
+# Accessibility
+
+Pages and components should be built with accessibility in mind.
+
+https://github.com/nickcolley/jest-axe
+https://orkhanhuseyn.medium.com/accessibility-testing-in-react-with-jest-axe-e08c2a3f3289
+http://accesibilidadweb.dlsi.ua.es/?menu=jaws
+https://webaim.org/projects/screenreadersurvey8/#intro
+https://www.gov.uk/service-manual/technology/testing-with-assistive-technologies#how-to-test
+https://es.reactjs.org/docs/accessibility.html
+
+# Internationalization
+
+Every non translated message should be written in English and wrapped into:
+
+ * i18n function from useTranslator() hook
+ * <Translate /> component
+
+Makefile has a i18n that will parse source files and update the po template.
+When *.po are updated, running the i18n target will create the strings.ts that
+the application will use in runtime.
+
+# Documentation Conventions
+
+* labels
+ * begin w/ a capital letter
+ * acronyms (e.g., "URL") are upper case
+* tooltips
+ * begin w/ a lower case letter
+ * do not end w/ punctuation (period)
+ * avoid leading article ("a", "an", "the")
diff --git a/packages/auditor-backoffice-ui/Makefile b/packages/auditor-backoffice-ui/Makefile
new file mode 100644
index 000000000..57b3e0cb5
--- /dev/null
+++ b/packages/auditor-backoffice-ui/Makefile
@@ -0,0 +1,35 @@
+# This Makefile has been placed in the public domain
+
+ifeq ($(TOPLEVEL), yes)
+ $(info top-level build)
+ -include ../../.config.mk
+ override DESTDIR := $(TOP_DESTDIR)
+else
+ $(info package-level build)
+ -include ../../.config.mk
+ -include .config.mk
+endif
+
+$(info prefix is $(prefix))
+
+.PHONY: all
+all:
+ @echo run \'make install\' to install
+
+spa_dir=$(DESTDIR)$(prefix)/share/taler/auditor-backoffice
+
+.PHONY: deps
+deps:
+ pnpm install --frozen-lockfile --filter @gnu-taler/auditor-backoffice...
+ pnpm run build
+
+.PHONY: install-nodeps
+install-nodeps:
+ (cd dist/prod && find . -type f -exec install -D "{}" "$(spa_dir)/{}" \;)
+
+
+.PHONY: install
+install:
+ $(MAKE) deps
+ $(MAKE) install-nodeps
+
diff --git a/packages/auditor-backoffice-ui/README.md b/packages/auditor-backoffice-ui/README.md
new file mode 100644
index 000000000..b10fa6a94
--- /dev/null
+++ b/packages/auditor-backoffice-ui/README.md
@@ -0,0 +1,64 @@
+## AUditor Admin Frontend
+
+Auditor Admin Frontend is a Single Page Application (SPA) that connects with a running Auditor Backend and lets you audit the exchange.
+
+## System requirements
+
+- Node: v16.15.0
+- pnpm: 7.14.2
+- make
+
+## Compiling from source
+
+Run `pnpm install --frozen-lockfile --filter @gnu-taler/auditor-backoffice...` to install all the nodejs dependencies.
+
+Then the command `pnpm build` create the distribution in the `dist` folder.
+
+By default the installation prefix will be `/usr/local/share/taler/auditor-backoffice/` but it can be overridden by `--prefix` in the configuration process:
+
+```shell
+./configure --prefix=/another/directory
+```
+
+To install run `make install`
+
+## Running develop
+
+To run a development server run:
+
+```shell
+./dev.mjs
+```
+
+This should start a watch process that will reload the server every time that a file is saved.
+
+The application need to connect to a auditor-backend properly configured to run.
+
+## Building for deploy
+
+To build and deploy the SPA in your local server run the install script:
+
+```shell
+make install
+```
+
+## Runtime dependencies
+
+* preact: Fast 3kB alternative to React with the same modern API
+
+* preact-router: URL component router for Preact
+
+* SWR: React Hooks library for data fetching (stale-while-revalidate)
+
+* Yup: schema builder for value parsing and validation (to be deprecated)
+
+* Date-fns: library for manipulating javascript date
+
+* qrcode-generator: simplest qr implementation based on JIS X 0510:1999
+
+* @gnu-taler/taler-util: types and tooling
+
+* history: manage the history stack, navigate, and persist state between sessions
+
+* jed: gettext like library for internationalization
+
diff --git a/packages/auditor-backoffice-ui/build.mjs b/packages/auditor-backoffice-ui/build.mjs
new file mode 100755
index 000000000..b6d6e5127
--- /dev/null
+++ b/packages/auditor-backoffice-ui/build.mjs
@@ -0,0 +1,28 @@
+#!/usr/bin/env node
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { build } from "@gnu-taler/web-util/build";
+
+await build({
+ type: "production",
+ source: {
+ js: ["src/index.tsx"],
+ assets: [{base:"src",files:["src/index.html"]}],
+ },
+ destination: "./dist/prod",
+ css: "sass",
+});
diff --git a/packages/auditor-backoffice-ui/contrib/po2ts b/packages/auditor-backoffice-ui/contrib/po2ts
new file mode 100755
index 000000000..d32e922ba
--- /dev/null
+++ b/packages/auditor-backoffice-ui/contrib/po2ts
@@ -0,0 +1,42 @@
+#!/usr/bin/env node
+/*
+ This file is part of GNU Taler
+ (C) 2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Convert a <lang>.po file into a JavaScript / TypeScript expression.
+ */
+
+const po2json = require("po2json");
+
+const filename = process.argv[2];
+
+if (!filename) {
+ console.error("error: missing filename");
+ process.exit(1);
+}
+
+const m = filename.match(/([a-zA-Z0-9-_]+).po/);
+
+if (!m) {
+ console.error("error: unexpected filename (expected <lang>.po)");
+ process.exit(1);
+}
+
+const lang = m[1];
+const pojson = po2json.parseFileSync(filename, { format: "jed1.x", fuzzy: true });
+const s =
+ "strings['" + lang + "'] = " + JSON.stringify(pojson, null, " ") + ";\n";
+console.log(s);
diff --git a/packages/auditor-backoffice-ui/copyleft-header.js b/packages/auditor-backoffice-ui/copyleft-header.js
new file mode 100644
index 000000000..2589fdc92
--- /dev/null
+++ b/packages/auditor-backoffice-ui/copyleft-header.js
@@ -0,0 +1,15 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
diff --git a/packages/merchant-backend-ui/src/pages/DepletedTip.stories.tsx b/packages/auditor-backoffice-ui/dev.mjs
index c20f6dc18..14d5737de 100644..100755
--- a/packages/merchant-backend-ui/src/pages/DepletedTip.stories.tsx
+++ b/packages/auditor-backoffice-ui/dev.mjs
@@ -1,6 +1,7 @@
+#!/usr/bin/env node
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2021-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -14,27 +15,26 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+import { serve } from "@gnu-taler/web-util/node";
+import { initializeDev } from "@gnu-taler/web-util/build";
-import { h, VNode, FunctionalComponent } from 'preact';
-import { DepletedTip as TestedComponent } from './DepletedTip';
+const devEntryPoints = ["src/stories.tsx", "src/index.tsx"];
-
-export default {
- title: 'DepletedTip',
- component: TestedComponent,
- argTypes: {
+const build = initializeDev({
+ type: "development",
+ source: {
+ js: devEntryPoints,
+ assets: [{base:"src",files:["src/index.html"]}],
},
-};
+ css: "sass",
+ destination: "./dist/dev",
+});
-function createExample<Props>(Component: FunctionalComponent<Props>, props: Partial<Props>) {
- const r = (args: any) => <Component {...args} />
- r.args = props
- return r
-}
+await build();
-export const Example = createExample(TestedComponent, {
+serve({
+ folder: "./dist/dev",
+ port: 8080,
+ source: "./src",
+ onSourceUpdate: build,
});
diff --git a/packages/auditor-backoffice-ui/package.json b/packages/auditor-backoffice-ui/package.json
new file mode 100644
index 000000000..776c179b4
--- /dev/null
+++ b/packages/auditor-backoffice-ui/package.json
@@ -0,0 +1,84 @@
+{
+ "private": true,
+ "name": "@gnu-taler/auditor-backoffice-ui",
+ "version": "0.10.7",
+ "license": "AGPL-3.0-or-later",
+ "type": "module",
+ "scripts": {
+ "clean": "rm -rf dist lib tsconfig.tsbuildinfo",
+ "build": "./build.mjs",
+ "check": "tsc",
+ "compile": "tsc && ./build.mjs",
+ "dev": "preact watch --port ${PORT:=8080} --no-sw --no-esm",
+ "test": "./test.mjs && mocha --require source-map-support/register 'dist/**/*.test.js' 'dist/**/test.js'",
+ "lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
+ "i18n:extract": "pogen extract",
+ "i18n:merge": "pogen merge",
+ "i18n:emit": "pogen emit",
+ "i18n": "pnpm i18n:extract && pnpm i18n:merge && pnpm i18n:emit",
+ "typedoc": "typedoc --out dist/typedoc ./src/",
+ "pretty": "prettier --write src"
+ },
+ "eslintConfig": {
+ "plugins": [
+ "header"
+ ],
+ "rules": {
+ "header/header": [
+ 2,
+ "copyleft-header.js"
+ ]
+ },
+ "extends": [
+ "prettier"
+ ]
+ },
+ "dependencies": {
+ "@gnu-taler/taler-util": "workspace:*",
+ "@gnu-taler/web-util": "workspace:*",
+ "date-fns": "2.29.3",
+ "history": "4.10.1",
+ "jed": "1.1.1",
+ "preact": "10.11.3",
+ "preact-router": "3.2.1",
+ "qrcode-generator": "1.4.4",
+ "swr": "2.2.2",
+ "yup": "^0.32.9"
+ },
+ "devDependencies": {
+ "@creativebulma/bulma-tooltip": "^1.2.0",
+ "@gnu-taler/pogen": "workspace:*",
+ "@types/chai": "^4.3.0",
+ "@types/history": "^4.7.8",
+ "@types/mocha": "^8.2.3",
+ "@types/node": "^18.11.17",
+ "@typescript-eslint/eslint-plugin": "^4.22.0",
+ "@typescript-eslint/parser": "^4.22.0",
+ "base64-inline-loader": "^1.1.1",
+ "bulma": "^0.9.2",
+ "bulma-checkbox": "^1.1.1",
+ "bulma-radio": "^1.1.1",
+ "bulma-responsive-tables": "^1.2.3",
+ "bulma-switch-control": "^1.1.1",
+ "bulma-timeline": "^3.0.4",
+ "bulma-upload-control": "^1.2.0",
+ "chai": "^4.3.6",
+ "dotenv": "^8.2.0",
+ "eslint": "^7.25.0",
+ "eslint-config-preact": "^1.1.4",
+ "eslint-plugin-header": "^3.1.1",
+ "html-webpack-inline-chunk-plugin": "^1.1.1",
+ "html-webpack-inline-source-plugin": "0.0.10",
+ "html-webpack-skip-assets-plugin": "^1.0.1",
+ "inline-chunk-html-plugin": "^1.1.1",
+ "mocha": "^9.2.0",
+ "preact-render-to-string": "^5.2.6",
+ "sass": "1.56.1",
+ "source-map-support": "^0.5.21",
+ "typedoc": "^0.25.4",
+ "typescript": "5.3.3"
+ },
+ "pogen": {
+ "domain": "taler-auditor-backoffice"
+ }
+}
diff --git a/packages/auditor-backoffice-ui/preact.config.js b/packages/auditor-backoffice-ui/preact.config.js
new file mode 100644
index 000000000..9b65d3ec7
--- /dev/null
+++ b/packages/auditor-backoffice-ui/preact.config.js
@@ -0,0 +1,70 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+
+import { DefinePlugin } from 'webpack';
+
+import pack from './package.json';
+import * as cp from 'child_process';
+
+const commitHash = cp.execSync('git rev-parse --short HEAD').toString();
+
+export default {
+ webpack(config, env, helpers) {
+ // ensure that process.env will not be undefined on runtime
+ config.node.process = 'mock'
+
+ // add __VERSION__ to be use in the html
+ config.plugins.push(
+ new DefinePlugin({
+ 'process.env.__VERSION__': JSON.stringify(env.isProd ? pack.version : `dev-${commitHash}`) ,
+ }),
+ );
+
+ // suddenly getting out of memory error from build process, error below [1]
+ // FIXME: remove preact-cli, use rollup
+ let { index } = helpers.getPluginsByName(config, 'WebpackFixStyleOnlyEntriesPlugin')[0]
+ config.plugins.splice(index, 1)
+ }
+}
+
+
+
+/* [1] from this error decided to remove plugin 'webpack-fix-style-only-entries
+ leaving this error for future reference
+
+
+<--- Last few GCs --->
+
+[32479:0x2e01870] 19969 ms: Mark-sweep 1869.4 (1950.2) -> 1443.1 (1504.1) MB, 497.5 / 0.0 ms (average mu = 0.631, current mu = 0.455) allocation failure scavenge might not succeed
+[32479:0x2e01870] 21907 ms: Mark-sweep 2016.9 (2077.9) -> 1628.6 (1681.4) MB, 1596.0 / 0.0 ms (average mu = 0.354, current mu = 0.176) allocation failure scavenge might not succeed
+
+<--- JS stacktrace --->
+
+==== JS stack trace =========================================
+
+ 0: ExitFrame [pc: 0x13cf099]
+Security context: 0x2f4ca66c08d1 <JSObject>
+ 1: /* anonymous * / [0x35d05555b4b9] [...path/merchant-backoffice/node_modules/.pnpm/webpack-fix-style-only-entries@0.5.2/node_modules/webpack-fix-style-only-entries/index.js:~80] [pc=0x2145e699d1a4](this=0x1149465410e9 <GlobalObject Object map = 0xff481b5b5f9>,0x047e52e36a49 <Dependency map = 0x1ed1fe41cd19>)
+ 2: arguments adaptor frame: 3...
+
+FATAL ERROR: invalid array length Allocation failed - JavaScript heap out of memory
+
+*/ \ No newline at end of file
diff --git a/packages/auditor-backoffice-ui/preact.single-config.js b/packages/auditor-backoffice-ui/preact.single-config.js
new file mode 100644
index 000000000..849269d6e
--- /dev/null
+++ b/packages/auditor-backoffice-ui/preact.single-config.js
@@ -0,0 +1,62 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+
+import defaultConfig from './preact.config'
+
+export default {
+ webpack(config, env, helpers, options) {
+ defaultConfig.webpack(config, env, helpers, options)
+
+ //1. check no file is under /routers or /component/{routers,async} to prevent async components
+ // https://github.com/preactjs/preact-cli#route-based-code-splitting
+
+ //2. remove devtools to prevent sourcemaps
+ config.devtool = false
+
+ //3. change assetLoader to load assets inline
+ const loaders = helpers.getLoaders(config)
+ const assetsLoader = loaders.find(lo => lo.rule.test.test('something.woff'))
+ if (assetsLoader) {
+ assetsLoader.rule.use = 'base64-inline-loader'
+ assetsLoader.rule.loader = undefined
+ }
+
+ //4. remove critters
+ //critters remove the css bundle from htmlWebpackPlugin.files.css
+ //for now, pushing all the content into the html is enough
+ const crittersWrapper = helpers.getPluginsByName(config, 'Critters')
+ if (crittersWrapper && crittersWrapper.length > 0) {
+ const [{ index }] = crittersWrapper
+ config.plugins.splice(index, 1)
+ }
+
+ //5. remove favicon from src/assets
+
+ //6. remove performance hints since we now that this is going to be big
+ if (config.performance) {
+ config.performance.hints = false
+ }
+
+ //7. template.html should have a favicon and add js/css content
+
+ //last, after building remove the mysterious link to stylesheet with remove-link-stylesheet.sh
+ }
+}
diff --git a/packages/auditor-backoffice-ui/remove-link-stylesheet.sh b/packages/auditor-backoffice-ui/remove-link-stylesheet.sh
new file mode 100644
index 000000000..fdf8f241c
--- /dev/null
+++ b/packages/auditor-backoffice-ui/remove-link-stylesheet.sh
@@ -0,0 +1,8 @@
+# This script has been placed in the public domain.
+
+FILE=$(ls single/bundle.*.css)
+BUNDLE=${FILE#single}
+grep -q '<link href="'$BUNDLE'" rel="stylesheet">' single/index.html || { echo bundle $BUNDLE not found in index.html; exit 1; }
+echo -n Removing link from index.html ...
+sed 's_<link href="'$BUNDLE'" rel="stylesheet">__' -i single/index.html
+echo done
diff --git a/packages/auditor-backoffice-ui/src/AdminRoutes.tsx b/packages/auditor-backoffice-ui/src/AdminRoutes.tsx
new file mode 100644
index 000000000..91dec09b0
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/AdminRoutes.tsx
@@ -0,0 +1,53 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { h, VNode } from "preact";
+import { Router, route, Route } from "preact-router";
+import InstanceCreatePage from "./paths/admin/create/index.js";
+import InstanceListPage from "./paths/admin/list/index.js";
+
+export enum AdminPaths {
+ list_instances = "/instances",
+ new_instance = "/instance/new",
+}
+
+export function AdminRoutes(): VNode {
+ return (
+ <Router>
+ <Route
+ path={AdminPaths.list_instances}
+ component={InstanceListPage}
+ onCreate={() => {
+ route(AdminPaths.new_instance);
+ }}
+ onUpdate={(id: string): void => {
+ route(`/instance/${id}/update`);
+ }}
+ />
+
+ <Route
+ path={AdminPaths.new_instance}
+ component={InstanceCreatePage}
+ onBack={() => route(AdminPaths.list_instances)}
+ onConfirm={() => {
+ // route(AdminPaths.list_instances);
+ }}
+
+ // onError={(error: any) => {
+ // }}
+ />
+ </Router>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/Application.tsx b/packages/auditor-backoffice-ui/src/Application.tsx
new file mode 100644
index 000000000..3e5cfc273
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/Application.tsx
@@ -0,0 +1,165 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { HttpStatusCode, LibtoolVersion } from "@gnu-taler/taler-util";
+import {
+ ErrorType,
+ TranslationProvider,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useMemo } from "preact/hooks";
+import { ApplicationReadyRoutes } from "./ApplicationReadyRoutes.js";
+import { Loading } from "./components/exception/loading.js";
+import {
+ NotConnectedAppMenu,
+ NotificationCard
+} from "./components/menu/index.js";
+import {
+ BackendContextProvider
+} from "./context/backend.js";
+import { ConfigContextProvider } from "./context/config.js";
+import { useBackendConfig } from "./hooks/backend.js";
+import { strings } from "./i18n/strings.js";
+
+export function Application(): VNode {
+ return (
+ <BackendContextProvider>
+ <TranslationProvider source={strings}>
+ <ApplicationStatusRoutes />
+ </TranslationProvider>
+ </BackendContextProvider>
+ );
+}
+
+/**
+ * Check connection testing against /config
+ *
+ * @returns
+ */
+function ApplicationStatusRoutes(): VNode {
+ const result = useBackendConfig();
+ const { i18n } = useTranslationContext();
+
+ const { currency, version } = result.ok && result.data
+ ? result.data
+ : { currency: "unknown", version: "unknown" };
+ const ctx = useMemo(() => ({ currency, version }), [currency, version]);
+
+ if (!result.ok) {
+ if (result.loading) return <Loading />;
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ ) {
+ return (
+ <Fragment>
+ <NotConnectedAppMenu title="Login" />
+ <NotificationCard
+ notification={{
+ message: i18n.str`Checking the /config endpoint got authorization error`,
+ type: "ERROR",
+ description: `The /config endpoint of the backend server should be 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>
+ );
+ }
+
+ 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>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/ApplicationReadyRoutes.tsx b/packages/auditor-backoffice-ui/src/ApplicationReadyRoutes.tsx
new file mode 100644
index 000000000..414eee39d
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/ApplicationReadyRoutes.tsx
@@ -0,0 +1,175 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+import { ErrorType, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { createHashHistory } from "history";
+import { Fragment, VNode, h } from "preact";
+import { Route, Router, route } from "preact-router";
+import { useState } from "preact/hooks";
+import { InstanceRoutes } from "./InstanceRoutes.js";
+import {
+ NotConnectedAppMenu,
+ NotYetReadyAppMenu,
+ NotificationCard,
+} from "./components/menu/index.js";
+import { useBackendContext } from "./context/backend.js";
+import { LoginToken } from "./declaration.js";
+import { useBackendInstancesTestForAdmin } from "./hooks/backend.js";
+import { LoginPage } from "./paths/login/index.js";
+import { Settings } from "./paths/settings/index.js";
+import { INSTANCE_ID_LOOKUP } from "./utils/constants.js";
+
+/**
+ * Check if admin against /management/instances
+ * @returns
+ */
+export function ApplicationReadyRoutes(): VNode {
+ const { i18n } = useTranslationContext();
+ const [unauthorized, setUnauthorized] = useState(false)
+ const {
+ url: backendURL,
+ updateToken,
+ alreadyTriedLogin,
+ } = useBackendContext();
+
+ function updateLoginStatus(token: LoginToken | undefined) {
+ updateToken(token)
+ setUnauthorized(false)
+ }
+
+ const result = useBackendInstancesTestForAdmin();
+
+ const clearTokenAndGoToRoot = () => {
+ route("/");
+ };
+ const [showSettings, setShowSettings] = useState(false)
+ const unauthorizedAdmin = !result.loading
+ && !result.ok
+ && result.type === ErrorType.CLIENT
+ && result.status === HttpStatusCode.Unauthorized;
+
+ if (!alreadyTriedLogin && !result.ok) {
+ return (
+ <Fragment>
+ <NotConnectedAppMenu title="Welcome!" />
+ <LoginPage onConfirm={updateToken} />
+ </Fragment>
+ );
+ }
+
+ if (showSettings) {
+ return <Fragment>
+ <NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="UI Settings" onLogout={clearTokenAndGoToRoot} isPasswordOk={false} />
+ <Settings onClose={() => setShowSettings(false)} />
+ </Fragment>
+ }
+
+ if (result.loading) {
+ return <NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="Loading..." isPasswordOk={false} />;
+ }
+
+ let admin = result.ok || unauthorizedAdmin;
+ let instanceNameByBackendURL: string | undefined;
+
+ if (!admin) {
+ // * the testing against admin endpoint failed and it's not
+ // an authorization problem
+ // * merchant backend will return this SPA under the main
+ // endpoint or /instance/<id> endpoint
+ // => trying to infer the instance id
+ const path = new URL(backendURL).pathname;
+ const match = INSTANCE_ID_LOOKUP.exec(path);
+ if (!match || !match[1]) {
+ // this should be rare because
+ // query to /config is ok but the URL
+ // does not match our pattern
+ return (
+ <Fragment>
+ <NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="Error" onLogout={clearTokenAndGoToRoot} isPasswordOk={false} />
+ <NotificationCard
+ notification={{
+ message: i18n.str`Couldn't access the server.`,
+ description: i18n.str`Could not infer instance id from url ${backendURL}`,
+ type: "ERROR",
+ }}
+ />
+ {/* <ConnectionPage onConfirm={changeBackend} /> */}
+ </Fragment>
+ );
+ }
+
+ instanceNameByBackendURL = match[1];
+ }
+
+ if (unauthorized || unauthorizedAdmin) {
+ return <Fragment>
+ <NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="Login" onLogout={clearTokenAndGoToRoot} isPasswordOk={false} />
+ <NotificationCard
+ notification={{
+ message: i18n.str`Access denied`,
+ description: i18n.str`Check your token is valid`,
+ type: "ERROR",
+ }}
+ />
+ <LoginPage onConfirm={updateLoginStatus} />
+ </Fragment>
+ }
+
+ const history = createHashHistory();
+ return (
+ <Router history={history}>
+ <Route
+ default
+ component={DefaultMainRoute}
+ admin={admin}
+ onUnauthorized={() => setUnauthorized(true)}
+ onLoginPass={() => {
+ setUnauthorized(false)
+ }}
+ instanceNameByBackendURL={instanceNameByBackendURL}
+ />
+ </Router>
+ );
+}
+
+function DefaultMainRoute({
+ instance,
+ admin,
+ onUnauthorized,
+ onLoginPass,
+ instanceNameByBackendURL,
+ url, //from preact-router
+}: any): VNode {
+ const [instanceName, setInstanceName] = useState(
+ instanceNameByBackendURL || instance || "default",
+ );
+
+ return (
+ <InstanceRoutes
+ admin={admin}
+ path={url}
+ onUnauthorized={onUnauthorized}
+ onLoginPass={onLoginPass}
+ id={instanceName}
+ setInstanceName={setInstanceName}
+ />
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx b/packages/auditor-backoffice-ui/src/InstanceRoutes.tsx
index 9e1593fe5..163438654 100644
--- a/packages/merchant-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 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,8 +17,15 @@
/**
*
* @author Sebastian Javier Marchano (sebasjm)
+ * @author Nic Eigel
*/
+import {
+ useTranslationContext,
+ HttpError,
+ ErrorType,
+} from "@gnu-taler/web-util/browser";
+import { format } from "date-fns";
import { Fragment, FunctionComponent, h, VNode } from "preact";
import { Route, route, Router } from "preact-router";
import { useCallback, useEffect, useMemo, useState } from "preact/hooks";
@@ -29,59 +36,70 @@ import { InstanceContextProvider } from "./context/instance.js";
import {
useBackendDefaultToken,
useBackendInstanceToken,
- useLocalStorage,
+ useSimpleLocalStorage,
} from "./hooks/index.js";
-import { HttpError } from "./hooks/backend.js";
-import { Translate, useTranslator } from "./i18n/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 TransferListPage from "./paths/instance/transfers/list/index.js";
-import TransferCreatePage from "./paths/instance/transfers/create/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 ListKYCPage from "./paths/instance/kyc/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, {
- Props as InstanceUpdatePageProps,
AdminUpdate as InstanceAdminUpdatePage,
+ Props as InstanceUpdatePageProps,
} from "./paths/instance/update/index.js";
-import LoginPage from "./paths/login/index.js";
+import { LoginPage } from "./paths/login/index.js";
import NotFoundPage from "./paths/notfound/index.js";
import { Notification } from "./utils/types.js";
-import { useInstanceKYCDetails } from "./hooks/instance.js";
-import { format } from "date-fns";
+import { LoginToken, MerchantBackend } from "./declaration.js";
+import { Settings } from "./paths/settings/index.js";
+import { dateFormatForSettings, useSettings } from "./hooks/useSettings.js";
export enum InstancePaths {
- // details = '/',
error = "/error",
- update = "/update",
-
- product_list = "/products",
- product_update = "/product/:pid/update",
- product_new = "/product/new",
-
- order_list = "/orders",
- order_new = "/order/new",
- order_details = "/order/:oid/details",
+ settings = "/settings",
+ token = "/token",
- reserves_list = "/reserves",
- reserves_details = "/reserves/:rid/details",
- reserves_new = "/reserves/new",
+ inventory_list = "/inventory",
+ inventory_update = "/inventory/:pid/update",
+ inventory_new = "/inventory/new",
- kyc = "/kyc",
+ deposit_confirmation_list = "/deposit-confirmation",
+ deposit_confirmation_update = "/deposit-confirmation/:pid/update",
+ deposit_confirmation_new = "/deposit-confirmation/new",
- transfers_list = "/transfers",
- transfers_new = "/transfer/new",
+ interface = "/interface",
}
// eslint-disable-next-line @typescript-eslint/no-empty-function
-const noop = () => {};
+const noop = () => { };
export enum AdminPaths {
list_instances = "/instances",
@@ -92,76 +110,86 @@ export enum AdminPaths {
export interface Props {
id: string;
admin?: boolean;
+ path: string;
+ onUnauthorized: () => void;
+ onLoginPass: () => void;
setInstanceName: (s: string) => void;
}
-export function InstanceRoutes({ id, admin, setInstanceName }: Props): VNode {
- const [_, updateDefaultToken] = useBackendDefaultToken();
+export function InstanceRoutes({
+ id,
+ admin,
+ path,
+ // onUnauthorized,
+ onLoginPass,
+ setInstanceName,
+}: Props): VNode {
+ const [defaultToken, updateDefaultToken] = useBackendDefaultToken();
const [token, updateToken] = useBackendInstanceToken(id);
- const {
- updateLoginStatus: changeBackend,
- addTokenCleaner,
- clearAllTokens,
- } = useBackendContext();
- const cleaner = useCallback(() => {
- updateToken(undefined);
- }, [id]);
- const i18n = useTranslator();
+ const { i18n } = useTranslationContext();
type GlobalNotifState = (Notification & { to: string }) | undefined;
const [globalNotification, setGlobalNotification] =
useState<GlobalNotifState>(undefined);
- useEffect(() => {
- addTokenCleaner(cleaner);
- }, [addTokenCleaner, cleaner]);
-
- const changeToken = (token?: string) => {
+ const changeToken = (token?: LoginToken) => {
if (admin) {
updateToken(token);
} else {
updateDefaultToken(token);
}
+ onLoginPass()
};
- const updateLoginStatus = (url: string, token?: string) => {
- changeBackend(url);
- if (!token) return;
- changeToken(token);
- };
+ // const updateLoginStatus = (url: string, token?: string) => {
+ // changeToken(token);
+ // };
const value = useMemo(
() => ({ id, token, admin, changeToken }),
- [id, token, admin]
+ [id, token, admin],
);
function ServerErrorRedirectTo(to: InstancePaths | AdminPaths) {
- return function ServerErrorRedirectToImpl(error: HttpError) {
- setGlobalNotification({
- message: i18n`The backend reported a problem: HTTP status #${error.status}`,
- description: i18n`Diagnostic from ${error.info?.url} is "${error.message}"`,
- details:
- error.clientError || error.serverError
- ? error.error?.detail
- : undefined,
- type: "ERROR",
- to,
- });
+ 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 LoginPageAccessDenied = () => (
- <Fragment>
+ // const LoginPageAccessDeniend = onUnauthorized
+ const LoginPageAccessDenied = () => {
+ return <Fragment>
<NotificationCard
notification={{
- message: i18n`Access denied`,
- description: i18n`The access token provided is invalid.`,
+ message: i18n.str`Access denied`,
+ description: i18n.str`Session expired or password changed.`,
type: "ERROR",
}}
/>
- <LoginPage onConfirm={updateLoginStatus} />
+ <LoginPage onConfirm={changeToken} />
</Fragment>
- );
+
+ }
function IfAdminCreateDefaultOr<T>(Next: FunctionComponent<any>) {
return function IfAdminCreateDefaultOrImpl(props?: T) {
@@ -170,17 +198,11 @@ export function InstanceRoutes({ id, admin, setInstanceName }: Props): VNode {
<Fragment>
<NotificationCard
notification={{
- message: i18n`No 'default' instance configured yet.`,
- description: i18n`Create a 'default' instance to begin using the merchant backoffice.`,
+ message: i18n.str`No 'default' instance configured yet.`,
+ description: i18n.str`Create a 'default' instance to begin using the merchant backoffice.`,
type: "INFO",
}}
/>
- <InstanceCreatePage
- forceId="default"
- onConfirm={() => {
- route(AdminPaths.list_instances);
- }}
- />
</Fragment>
);
}
@@ -192,8 +214,10 @@ export function InstanceRoutes({ id, admin, setInstanceName }: Props): VNode {
}
const clearTokenAndGoToRoot = () => {
- clearAllTokens();
route("/");
+ // clear all tokens
+ updateToken(undefined)
+ updateDefaultToken(undefined)
};
return (
@@ -201,8 +225,13 @@ export function InstanceRoutes({ id, admin, setInstanceName }: Props): VNode {
<Menu
instance={id}
admin={admin}
+ onShowSettings={() => {
+ route(InstancePaths.interface)
+ }}
+ path={path}
onLogout={clearTokenAndGoToRoot}
setInstanceName={setInstanceName}
+ isPasswordOk={defaultToken !== undefined}
/>
<KycBanner />
<NotificationCard notification={globalNotification} />
@@ -216,8 +245,6 @@ export function InstanceRoutes({ id, admin, setInstanceName }: Props): VNode {
}
}}
>
- <Route path="/" component={Redirect} to={InstancePaths.order_list} />
-
{/**
* Admin pages
*/}
@@ -236,18 +263,6 @@ export function InstanceRoutes({ id, admin, setInstanceName }: Props): VNode {
onLoadError={ServerErrorRedirectTo(InstancePaths.error)}
/>
)}
-
- {admin && (
- <Route
- path={AdminPaths.new_instance}
- component={InstanceCreatePage}
- onBack={() => route(AdminPaths.list_instances)}
- onConfirm={() => {
- route(AdminPaths.list_instances);
- }}
- />
- )}
-
{admin && (
<Route
path={AdminPaths.update_instance}
@@ -261,12 +276,11 @@ export function InstanceRoutes({ id, admin, setInstanceName }: Props): VNode {
onNotFound={NotFoundPage}
/>
)}
-
{/**
* Update instance page
*/}
<Route
- path={InstancePaths.update}
+ path={InstancePaths.settings}
component={InstanceUpdatePage}
onBack={() => {
route(`/`);
@@ -279,149 +293,85 @@ export function InstanceRoutes({ id, admin, setInstanceName }: Props): VNode {
onUnauthorized={LoginPageAccessDenied}
onLoadError={ServerErrorRedirectTo(InstancePaths.error)}
/>
-
{/**
- * Product pages
+ * Inventory pages
*/}
<Route
- path={InstancePaths.product_list}
+ path={InstancePaths.inventory_list}
component={ProductListPage}
onUnauthorized={LoginPageAccessDenied}
- onLoadError={ServerErrorRedirectTo(InstancePaths.update)}
+ onLoadError={ServerErrorRedirectTo(InstancePaths.settings)}
onCreate={() => {
- route(InstancePaths.product_new);
+ route(InstancePaths.inventory_new);
}}
onSelect={(id: string) => {
- route(InstancePaths.product_update.replace(":pid", id));
+ route(InstancePaths.inventory_update.replace(":pid", id));
}}
onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
/>
<Route
- path={InstancePaths.product_update}
+ path={InstancePaths.inventory_update}
component={ProductUpdatePage}
onUnauthorized={LoginPageAccessDenied}
- onLoadError={ServerErrorRedirectTo(InstancePaths.product_list)}
+ onLoadError={ServerErrorRedirectTo(InstancePaths.inventory_list)}
onConfirm={() => {
- route(InstancePaths.product_list);
+ route(InstancePaths.inventory_list);
}}
onBack={() => {
- route(InstancePaths.product_list);
+ route(InstancePaths.inventory_list);
}}
onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
/>
<Route
- path={InstancePaths.product_new}
+ path={InstancePaths.inventory_new}
component={ProductCreatePage}
onConfirm={() => {
- route(InstancePaths.product_list);
+ route(InstancePaths.inventory_list);
}}
onBack={() => {
- route(InstancePaths.product_list);
+ route(InstancePaths.inventory_list);
}}
/>
-
{/**
- * Order pages
+ * Deposit confirmation pages
*/}
<Route
- path={InstancePaths.order_list}
- component={OrderListPage}
+ path={InstancePaths.deposit_confirmation_list}
+ component={DepositConfirmationListPage}
+ onUnauthorized={LoginPageAccessDenied}
+ onLoadError={ServerErrorRedirectTo(InstancePaths.settings)}
onCreate={() => {
- route(InstancePaths.order_new);
+ route(InstancePaths.deposit_confirmation_new);
}}
onSelect={(id: string) => {
- route(InstancePaths.order_details.replace(":oid", id));
+ route(InstancePaths.deposit_confirmation_update.replace(":pid", id));
}}
- onUnauthorized={LoginPageAccessDenied}
- onLoadError={ServerErrorRedirectTo(InstancePaths.update)}
onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
/>
<Route
- path={InstancePaths.order_details}
- component={OrderDetailsPage}
+ path={InstancePaths.deposit_confirmation_update}
+ component={DepositConfirmationUpdatePage}
onUnauthorized={LoginPageAccessDenied}
- onLoadError={ServerErrorRedirectTo(InstancePaths.order_list)}
- onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
- onBack={() => {
- route(InstancePaths.order_list);
- }}
- />
- <Route
- path={InstancePaths.order_new}
- component={OrderCreatePage}
+ onLoadError={ServerErrorRedirectTo(InstancePaths.deposit_confirmation_list)}
onConfirm={() => {
- route(InstancePaths.order_list);
+ route(InstancePaths.deposit_confirmation_list);
}}
onBack={() => {
- route(InstancePaths.order_list);
+ route(InstancePaths.deposit_confirmation_list);
}}
- />
-
- {/**
- * Transfer pages
- */}
- <Route
- path={InstancePaths.transfers_list}
- component={TransferListPage}
- onUnauthorized={LoginPageAccessDenied}
onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
- onLoadError={ServerErrorRedirectTo(InstancePaths.update)}
- onCreate={() => {
- route(InstancePaths.transfers_new);
- }}
/>
-
<Route
- path={InstancePaths.transfers_new}
- component={TransferCreatePage}
+ path={InstancePaths.deposit_confirmation_new}
+ component={DepositConfirmationCreatePage}
onConfirm={() => {
- route(InstancePaths.transfers_list);
+ route(InstancePaths.deposit_confirmation_list);
}}
onBack={() => {
- route(InstancePaths.transfers_list);
+ route(InstancePaths.deposit_confirmation_list);
}}
/>
-
- {/**
- * reserves pages
- */}
- <Route
- path={InstancePaths.reserves_list}
- component={ReservesListPage}
- onUnauthorized={LoginPageAccessDenied}
- onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
- onLoadError={ServerErrorRedirectTo(InstancePaths.update)}
- onSelect={(id: string) => {
- route(InstancePaths.reserves_details.replace(":rid", id));
- }}
- onCreate={() => {
- route(InstancePaths.reserves_new);
- }}
- />
-
- <Route
- path={InstancePaths.reserves_details}
- component={ReservesDetailsPage}
- onUnauthorized={LoginPageAccessDenied}
- onLoadError={ServerErrorRedirectTo(InstancePaths.reserves_list)}
- onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
- onBack={() => {
- route(InstancePaths.reserves_list);
- }}
- />
-
- <Route
- path={InstancePaths.reserves_new}
- component={ReservesCreatePage}
- onConfirm={() => {
- route(InstancePaths.reserves_list);
- }}
- onBack={() => {
- route(InstancePaths.reserves_list);
- }}
- />
-
- <Route path={InstancePaths.kyc} component={ListKYCPage} />
+ <Route path={InstancePaths.interface} component={Settings} />
{/**
* Example pages
*/}
@@ -442,38 +392,43 @@ export function Redirect({ to }: { to: string }): null {
function AdminInstanceUpdatePage({
id,
...rest
-}: { id: string } & InstanceUpdatePageProps) {
+}: { id: string } & InstanceUpdatePageProps): VNode {
const [token, changeToken] = useBackendInstanceToken(id);
- const { updateLoginStatus: changeBackend } = useBackendContext();
- const updateLoginStatus = (url: string, token?: string) => {
- changeBackend(url);
- if (token) changeToken(token);
+ const updateLoginStatus = (token?: LoginToken): void => {
+ changeToken(token);
};
const value = useMemo(
() => ({ id, token, admin: true, changeToken }),
- [id, token]
+ [id, token],
);
- const i18n = useTranslator();
+ const { i18n } = useTranslationContext();
return (
<InstanceContextProvider value={value}>
<InstanceAdminUpdatePage
{...rest}
instanceId={id}
- onLoadError={(error: HttpError) => {
+ 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={{
- message: i18n`The backend reported a problem: HTTP status #${error.status}`,
- description: i18n`Diagnostic from ${error.info?.url} is "${error.message}"`,
- details:
- error.clientError || error.serverError
- ? error.error?.detail
- : undefined,
- type: "ERROR",
- }}
- />
+ <NotificationCard notification={notif} />
<LoginPage onConfirm={updateLoginStatus} />
</Fragment>
);
@@ -483,8 +438,8 @@ function AdminInstanceUpdatePage({
<Fragment>
<NotificationCard
notification={{
- message: i18n`Access denied`,
- description: i18n`The access token provided is invalid`,
+ message: i18n.str`Access denied`,
+ description: i18n.str`The access token provided is invalid`,
type: "ERROR",
}}
/>
@@ -499,8 +454,10 @@ function AdminInstanceUpdatePage({
function KycBanner(): VNode {
const kycStatus = useInstanceKYCDetails();
- const today = format(new Date(), "yyyy-MM-dd");
- const [lastHide, setLastHide] = useLocalStorage("kyc-last-hide");
+ 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 />;
@@ -517,7 +474,7 @@ function KycBanner(): VNode {
</p>
<div class="buttons is-right">
<button class="button" onClick={() => setLastHide(today)}>
- <Translate>Hide for today</Translate>
+ <i18n.Translate>Hide for today</i18n.Translate>
</button>
</div>
</div>
diff --git a/packages/demobank-ui/src/assets/empty.png b/packages/auditor-backoffice-ui/src/assets/empty.png
index 5120d3138..5120d3138 100644
--- a/packages/demobank-ui/src/assets/empty.png
+++ b/packages/auditor-backoffice-ui/src/assets/empty.png
Binary files differ
diff --git a/packages/demobank-ui/src/assets/icons/android-chrome-192x192.png b/packages/auditor-backoffice-ui/src/assets/icons/android-chrome-192x192.png
index 93ebe2e2c..93ebe2e2c 100644
--- a/packages/demobank-ui/src/assets/icons/android-chrome-192x192.png
+++ b/packages/auditor-backoffice-ui/src/assets/icons/android-chrome-192x192.png
Binary files differ
diff --git a/packages/demobank-ui/src/assets/icons/android-chrome-512x512.png b/packages/auditor-backoffice-ui/src/assets/icons/android-chrome-512x512.png
index 52d1623ea..52d1623ea 100644
--- a/packages/demobank-ui/src/assets/icons/android-chrome-512x512.png
+++ b/packages/auditor-backoffice-ui/src/assets/icons/android-chrome-512x512.png
Binary files differ
diff --git a/packages/demobank-ui/src/assets/icons/apple-touch-icon.png b/packages/auditor-backoffice-ui/src/assets/icons/apple-touch-icon.png
index 254e4bb4d..254e4bb4d 100644
--- a/packages/demobank-ui/src/assets/icons/apple-touch-icon.png
+++ b/packages/auditor-backoffice-ui/src/assets/icons/apple-touch-icon.png
Binary files differ
diff --git a/packages/demobank-ui/src/assets/icons/favicon-16x16.png b/packages/auditor-backoffice-ui/src/assets/icons/favicon-16x16.png
index e81177dcb..e81177dcb 100644
--- a/packages/demobank-ui/src/assets/icons/favicon-16x16.png
+++ b/packages/auditor-backoffice-ui/src/assets/icons/favicon-16x16.png
Binary files differ
diff --git a/packages/demobank-ui/src/assets/icons/favicon-32x32.png b/packages/auditor-backoffice-ui/src/assets/icons/favicon-32x32.png
index 40e9b5b47..40e9b5b47 100644
--- a/packages/demobank-ui/src/assets/icons/favicon-32x32.png
+++ b/packages/auditor-backoffice-ui/src/assets/icons/favicon-32x32.png
Binary files differ
diff --git a/packages/demobank-ui/src/assets/icons/languageicon.svg b/packages/auditor-backoffice-ui/src/assets/icons/languageicon.svg
index 22d58da65..22d58da65 100644
--- a/packages/demobank-ui/src/assets/icons/languageicon.svg
+++ b/packages/auditor-backoffice-ui/src/assets/icons/languageicon.svg
diff --git a/packages/demobank-ui/src/assets/icons/mstile-150x150.png b/packages/auditor-backoffice-ui/src/assets/icons/mstile-150x150.png
index 9cfb889be..9cfb889be 100644
--- a/packages/demobank-ui/src/assets/icons/mstile-150x150.png
+++ b/packages/auditor-backoffice-ui/src/assets/icons/mstile-150x150.png
Binary files differ
diff --git a/packages/auditor-backoffice-ui/src/assets/logo-2021.svg b/packages/auditor-backoffice-ui/src/assets/logo-2021.svg
new file mode 100644
index 000000000..8c5ff3e5b
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/assets/logo-2021.svg
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 201 90">
+ <g fill="#0042b3" fill-rule="evenodd" stroke-width=".3">
+ <path d="M86.7 1.1c15.6 0 29 9.4 36 23.2h-5.9A35.1 35.1 0 0086.7 6.5C67 6.5 51 23.6 51 44.7c0 10.4 3.8 19.7 10 26.6a31.4 31.4 0 01-4.2 3A45.2 45.2 0 0146 44.7c0-24 18.2-43.6 40.7-43.6zm35.8 64.3a40.4 40.4 0 01-39 22.8c3-1.5 6-3.5 8.6-5.7a35.6 35.6 0 0024.6-17.1z" />
+ <path d="M64.2 1.1l3.1.1c-3 1.6-5.9 3.5-8.5 5.8a37.5 37.5 0 00-30.2 37.7c0 14.3 7.3 26.7 18 33.3a29.6 29.6 0 01-8.5.2c-9-8-14.6-20-14.6-33.5 0-24 18.2-43.6 40.7-43.6zm5.4 81.4a35.6 35.6 0 0024.6-17.1h5.9a40.4 40.4 0 01-39 22.8c3-1.5 5.9-3.5 8.5-5.7zm24.8-58.2a37 37 0 00-12.6-12.8 29.6 29.6 0 018.5-.2c4 3.6 7.4 8 9.9 13z" />
+ <path d="M41.8 1.1c1 0 2 0 3.1.2-3 1.5-5.9 3.4-8.5 5.6A37.5 37.5 0 006.1 44.7c0 21.1 16 38.3 35.7 38.3 12.6 0 23.6-7 30-17.6h5.8a40.4 40.4 0 01-35.8 23C19.3 88.4 1 68.8 1 44.7c0-24 18.2-43.6 40.7-43.6zm30.1 23.2a38.1 38.1 0 00-4.5-6.1c1.3-1.2 2.7-2.2 4.3-3 2.3 2.7 4.4 5.8 6 9.1z" />
+ </g>
+ <path d="M76.1 34.4h9.2v-5H61.9v5H71v26h5.1zM92.6 52.9h13.7l3 7.4h5.3l-12.7-31.2h-4.7L84.5 60.3h5.2zm11.8-4.9h-9.9l5-12.4zM123.8 29.4h-4.6v31h20.6v-5h-16zM166.5 29.4H145v31h21.6v-5H150v-8.3h14.5v-4.9h-14.5v-8h16.4zM191.2 39.5c0 1.6-.5 2.8-1.6 3.8s-2.6 1.4-4.4 1.4h-7.4V34.3h7.4c1.9 0 3.4.4 4.4 1.3 1 .9 1.6 2.2 1.6 3.9zm6 20.8l-7.7-11.7c1-.3 1.9-.7 2.7-1.3a8.8 8.8 0 003.6-4.6c.4-1 .5-2.2.5-3.5 0-1.5-.2-2.9-.7-4.1a8.4 8.4 0 00-2.1-3.1c-1-.8-2-1.5-3.4-2-1.3-.4-2.8-.6-4.5-.6h-12.9v31h5V49.4h6.5l7 10.8z" />
+</svg> \ No newline at end of file
diff --git a/packages/demobank-ui/src/assets/logo.jpeg b/packages/auditor-backoffice-ui/src/assets/logo.jpeg
index 489832f7c..489832f7c 100644
--- a/packages/demobank-ui/src/assets/logo.jpeg
+++ b/packages/auditor-backoffice-ui/src/assets/logo.jpeg
Binary files differ
diff --git a/packages/demobank-ui/src/components/AsyncButton.tsx b/packages/auditor-backoffice-ui/src/components/exception/AsyncButton.tsx
index eec11f4a1..b1fc33877 100644
--- a/packages/demobank-ui/src/components/AsyncButton.tsx
+++ b/packages/auditor-backoffice-ui/src/components/exception/AsyncButton.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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
@@ -19,42 +19,35 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { ComponentChildren, h, VNode } from "preact";
-import { useLayoutEffect, useRef } from "preact/hooks";
-// import { LoadingModal } from "../modal";
-import { useAsync } from "../hooks/async";
-// import { Translate } from "../../i18n";
+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;
+ disabled: boolean;
onClick?: () => Promise<void>;
- grabFocus?: boolean;
[rest: string]: any;
};
-export function AsyncButton({
- onClick,
- grabFocus,
- disabled,
- children,
- ...rest
-}: Props): VNode {
- const { isLoading, request } = useAsync(onClick);
-
- const buttonRef = useRef<HTMLButtonElement>(null);
- useLayoutEffect(() => {
- if (grabFocus) buttonRef.current?.focus();
- }, [grabFocus]);
-
- // if (isSlow) {
- // return <LoadingModal onCancel={cancel} />;
- // }
- if (isLoading) return <button class="button">Loading...</button>;
+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 data-tooltip={rest["data-tooltip"]} style={{ marginLeft: 5 }}>
- <button {...rest} ref={buttonRef} onClick={request} disabled={disabled}>
+ <span {...rest}>
+ <button class="button is-success" onClick={request} disabled={disabled}>
{children}
</button>
</span>
diff --git a/packages/auditor-backoffice-ui/src/components/exception/QR.tsx b/packages/auditor-backoffice-ui/src/components/exception/QR.tsx
new file mode 100644
index 000000000..c9340ea76
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/exception/QR.tsx
@@ -0,0 +1,49 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { h, VNode } from "preact";
+import { useEffect, useRef } from "preact/hooks";
+import qrcode from "qrcode-generator";
+
+export function QR({ text }: { text: string }): VNode {
+ const divRef = useRef<HTMLDivElement>(null);
+ useEffect(() => {
+ const qr = qrcode(0, "L");
+ qr.addData(text);
+ qr.make();
+ if (divRef.current) {
+ divRef.current.innerHTML = qr.createSvgTag({
+ scalable: true,
+ });
+ }
+ });
+
+ return (
+ <div
+ style={{
+ width: "100%",
+ display: "flex",
+ flexDirection: "column",
+ alignItems: "center",
+ }}
+ >
+ <div
+ style={{ width: "50%", minWidth: 200, maxWidth: 300 }}
+ ref={divRef}
+ />
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/exception/loading.tsx b/packages/auditor-backoffice-ui/src/components/exception/loading.tsx
new file mode 100644
index 000000000..a043b81eb
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/exception/loading.tsx
@@ -0,0 +1,48 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode } from "preact";
+
+export function Loading(): VNode {
+ return (
+ <div
+ class="columns is-centered is-vcentered"
+ style={{
+ height: "calc(100% - 3rem)",
+ position: "absolute",
+ width: "100%",
+ }}
+ >
+ <Spinner />
+ </div>
+ );
+}
+
+export function Spinner(): VNode {
+ return (
+ <div class="lds-ring">
+ <div />
+ <div />
+ <div />
+ <div />
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/form/FormProvider.tsx b/packages/auditor-backoffice-ui/src/components/form/FormProvider.tsx
new file mode 100644
index 000000000..0d53c4d08
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/FormProvider.tsx
@@ -0,0 +1,109 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { ComponentChildren, createContext, h, VNode } from "preact";
+import { useContext, useMemo } from "preact/hooks";
+
+type Updater<S> = (value: (prevState: S) => S) => void;
+
+export interface Props<T> {
+ object?: Partial<T>;
+ errors?: FormErrors<T>;
+ name?: string;
+ valueHandler: Updater<Partial<T>> | null;
+ children: ComponentChildren;
+}
+
+const noUpdater: Updater<Partial<unknown>> = () => (s: unknown) => s;
+
+export function FormProvider<T>({
+ object = {},
+ errors = {},
+ name = "",
+ valueHandler,
+ children,
+}: Props<T>): VNode {
+ const initialObject = useMemo(() => object, []);
+ const value = useMemo<FormType<T>>(
+ () => ({
+ errors,
+ object,
+ initialObject,
+ valueHandler: valueHandler ? valueHandler : noUpdater,
+ name,
+ toStr: {},
+ fromStr: {},
+ }),
+ [errors, object, valueHandler],
+ );
+
+ return (
+ <FormContext.Provider value={value}>
+ <form
+ class="field"
+ onSubmit={(e) => {
+ e.preventDefault();
+ // if (valueHandler) valueHandler(object);
+ }}
+ >
+ {children}
+ </form>
+ </FormContext.Provider>
+ );
+}
+
+export interface FormType<T> {
+ object: Partial<T>;
+ initialObject: Partial<T>;
+ errors: FormErrors<T>;
+ toStr: FormtoStr<T>;
+ name: string;
+ fromStr: FormfromStr<T>;
+ valueHandler: Updater<Partial<T>>;
+}
+
+const FormContext = createContext<FormType<unknown>>(null!);
+
+/**
+ * FIXME:
+ * USE MEMORY EVENTS INSTEAD OF CONTEXT
+ * @deprecated
+ */
+
+export function useFormContext<T>() {
+ return useContext<FormType<T>>(FormContext);
+}
+
+export type FormErrors<T> = {
+ [P in keyof T]?: string | FormErrors<T[P]>;
+};
+
+export type FormtoStr<T> = {
+ [P in keyof T]?: (f?: T[P]) => string;
+};
+
+export type FormfromStr<T> = {
+ [P in keyof T]?: (f: string) => T[P];
+};
+
+export type FormUpdater<T> = {
+ [P in keyof T]?: (f: keyof T) => (v: T[P]) => void;
+};
diff --git a/packages/auditor-backoffice-ui/src/components/form/Input.tsx b/packages/auditor-backoffice-ui/src/components/form/Input.tsx
new file mode 100644
index 000000000..c1ddcb064
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/Input.tsx
@@ -0,0 +1,116 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { ComponentChildren, h, VNode } from "preact";
+import { useField, InputProps } from "./useField.js";
+
+interface Props<T> extends InputProps<T> {
+ inputType?: "text" | "number" | "multiline" | "password";
+ expand?: boolean;
+ toStr?: (v?: any) => string;
+ fromStr?: (s: string) => any;
+ inputExtra?: any;
+ side?: ComponentChildren;
+ children?: ComponentChildren;
+}
+
+const defaultToString = (f?: any): string => f || "";
+const defaultFromString = (v: string): any => v as any;
+
+const TextInput = ({ inputType, error, ...rest }: any) =>
+ inputType === "multiline" ? (
+ <textarea
+ {...rest}
+ class={error ? "textarea is-danger" : "textarea"}
+ rows="3"
+ />
+ ) : (
+ <input
+ {...rest}
+ class={error ? "input is-danger" : "input"}
+ type={inputType}
+ />
+ );
+
+export function Input<T>({
+ name,
+ readonly,
+ placeholder,
+ tooltip,
+ label,
+ expand,
+ help,
+ children,
+ inputType,
+ inputExtra,
+ side,
+ fromStr = defaultFromString,
+ toStr = defaultToString,
+}: Props<keyof T>): VNode {
+ const { error, value, onChange, required } = useField<T>(name);
+ return (
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ {label}
+ {tooltip && (
+ <span class="icon has-tooltip-right" data-tooltip={tooltip}>
+ <i class="mdi mdi-information" />
+ </span>
+ )}
+ </label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p
+ class={
+ expand
+ ? "control is-expanded has-icons-right"
+ : "control has-icons-right"
+ }
+ >
+ <TextInput
+ error={error}
+ {...inputExtra}
+ inputType={inputType}
+ placeholder={placeholder}
+ readonly={readonly}
+ disabled={readonly}
+ name={String(name)}
+ value={toStr(value)}
+ onChange={(e: h.JSX.TargetedEvent<HTMLInputElement>): void =>
+ onChange(fromStr(e.currentTarget.value))
+ }
+ />
+ {help}
+ {children}
+ {required && (
+ <span class="icon has-text-danger is-right">
+ <i class="mdi mdi-alert" />
+ </span>
+ )}
+ </p>
+ {error && <p class="help is-danger">{error}</p>}
+ </div>
+ {side}
+ </div>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/form/InputArray.tsx b/packages/auditor-backoffice-ui/src/components/form/InputArray.tsx
new file mode 100644
index 000000000..4ed4c4b28
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/InputArray.tsx
@@ -0,0 +1,139 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { InputProps, useField } from "./useField.js";
+
+export interface Props<T> extends InputProps<T> {
+ isValid?: (e: any) => boolean;
+ addonBefore?: string;
+ toStr?: (v?: any) => string;
+ fromStr?: (s: string) => any;
+}
+
+const defaultToString = (f?: any): string => f || "";
+const defaultFromString = (v: string): any => v as any;
+
+export function InputArray<T>({
+ name,
+ readonly,
+ placeholder,
+ tooltip,
+ label,
+ help,
+ addonBefore,
+ isValid = () => true,
+ fromStr = defaultFromString,
+ toStr = defaultToString,
+}: Props<keyof T>): VNode {
+ const { error: formError, value, onChange, required } = useField<T>(name);
+ const [localError, setLocalError] = useState<string | null>(null);
+
+ const error = localError || formError;
+
+ const array: any[] = (value ? value! : []) as any;
+ const [currentValue, setCurrentValue] = useState("");
+ const { i18n } = useTranslationContext();
+
+ return (
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ {label}
+ {tooltip && (
+ <span class="icon has-tooltip-right" data-tooltip={tooltip}>
+ <i class="mdi mdi-information" />
+ </span>
+ )}
+ </label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <div class="field has-addons">
+ {addonBefore && (
+ <div class="control">
+ <a class="button is-static">{addonBefore}</a>
+ </div>
+ )}
+ <p class="control is-expanded has-icons-right">
+ <input
+ class={error ? "input is-danger" : "input"}
+ type="text"
+ placeholder={placeholder}
+ readonly={readonly}
+ disabled={readonly}
+ name={String(name)}
+ value={currentValue}
+ onChange={(e): void => setCurrentValue(e.currentTarget.value)}
+ />
+ {required && (
+ <span class="icon has-text-danger is-right">
+ <i class="mdi mdi-alert" />
+ </span>
+ )}
+ </p>
+ <p class="control">
+ <button
+ class="button is-info has-tooltip-left"
+ disabled={!currentValue}
+ onClick={(): void => {
+ const v = fromStr(currentValue);
+ if (!isValid(v)) {
+ setLocalError(
+ i18n.str`The value ${v} is invalid for a payment url`,
+ );
+ return;
+ }
+ setLocalError(null);
+ onChange([v, ...array] as any);
+ setCurrentValue("");
+ }}
+ data-tooltip={i18n.str`add element to the list`}
+ >
+ <i18n.Translate>add</i18n.Translate>
+ </button>
+ </p>
+ </div>
+ {help}
+ {error && <p class="help is-danger"> {error} </p>}
+ {array.map((v, i) => (
+ <div key={i} class="tags has-addons mt-3 mb-0">
+ <span
+ class="tag is-medium is-info mb-0"
+ style={{ maxWidth: "90%" }}
+ >
+ {v}
+ </span>
+ <a
+ class="tag is-medium is-danger is-delete mb-0"
+ onClick={() => {
+ onChange(array.filter((f) => f !== v) as any);
+ setCurrentValue(toStr(v));
+ }}
+ />
+ </div>
+ ))}
+ </div>
+ </div>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/form/InputBoolean.tsx b/packages/auditor-backoffice-ui/src/components/form/InputBoolean.tsx
new file mode 100644
index 000000000..f79e16c07
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/InputBoolean.tsx
@@ -0,0 +1,91 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { h, VNode } from "preact";
+import { InputProps, useField } from "./useField.js";
+
+interface Props<T> extends InputProps<T> {
+ name: T;
+ readonly?: boolean;
+ expand?: boolean;
+ threeState?: boolean;
+ toBoolean?: (v?: any) => boolean | undefined;
+ fromBoolean?: (s: boolean | undefined) => any;
+}
+
+const defaultToBoolean = (f?: any): boolean | undefined => f || "";
+const defaultFromBoolean = (v: boolean | undefined): any => v as any;
+
+export function InputBoolean<T>({
+ name,
+ readonly,
+ placeholder,
+ tooltip,
+ label,
+ help,
+ threeState,
+ expand,
+ fromBoolean = defaultFromBoolean,
+ toBoolean = defaultToBoolean,
+}: Props<keyof T>): VNode {
+ const { error, value, onChange } = useField<T>(name);
+
+ const onCheckboxClick = (): void => {
+ const c = toBoolean(value);
+ if (c === false && threeState) return onChange(undefined as any);
+ return onChange(fromBoolean(!c));
+ };
+
+ return (
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ {label}
+ {tooltip && (
+ <span class="icon has-tooltip-right" data-tooltip={tooltip}>
+ <i class="mdi mdi-information" />
+ </span>
+ )}
+ </label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class={expand ? "control is-expanded" : "control"}>
+ <label class="b-checkbox checkbox">
+ <input
+ type="checkbox"
+ class={toBoolean(value) === undefined ? "is-indeterminate" : ""}
+ checked={toBoolean(value)}
+ placeholder={placeholder}
+ readonly={readonly}
+ name={String(name)}
+ disabled={readonly}
+ onChange={onCheckboxClick}
+ />
+ <span class="check" />
+ </label>
+ {help}
+ </p>
+ {error && <p class="help is-danger">{error}</p>}
+ </div>
+ </div>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/form/InputCurrency.tsx b/packages/auditor-backoffice-ui/src/components/form/InputCurrency.tsx
new file mode 100644
index 000000000..b02354d7c
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/InputCurrency.tsx
@@ -0,0 +1,67 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { ComponentChildren, h, VNode } from "preact";
+import { useConfigContext } from "../../context/config.js";
+import { Amount } from "../../declaration.js";
+import { InputWithAddon } from "./InputWithAddon.js";
+import { InputProps } from "./useField.js";
+
+export interface Props<T> extends InputProps<T> {
+ expand?: boolean;
+ addonAfter?: ComponentChildren;
+ children?: ComponentChildren;
+ side?: ComponentChildren;
+}
+
+export function InputCurrency<T>({
+ name,
+ readonly,
+ label,
+ placeholder,
+ help,
+ tooltip,
+ expand,
+ addonAfter,
+ children,
+ side,
+}: Props<keyof T>): VNode {
+ const config = useConfigContext();
+ return (
+ <InputWithAddon<T>
+ name={name}
+ readonly={readonly}
+ addonBefore={config.currency}
+ side={side}
+ label={label}
+ placeholder={placeholder}
+ help={help}
+ tooltip={tooltip}
+ addonAfter={addonAfter}
+ inputType="number"
+ expand={expand}
+ toStr={(v?: Amount) => v?.split(":")[1] || ""}
+ fromStr={(v: string) => (!v ? undefined : `${config.currency}:${v}`)}
+ inputExtra={{ min: 0 }}
+ >
+ {children}
+ </InputWithAddon>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/form/InputDate.tsx b/packages/auditor-backoffice-ui/src/components/form/InputDate.tsx
new file mode 100644
index 000000000..a398629dc
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/InputDate.tsx
@@ -0,0 +1,164 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { format } from "date-fns";
+import { ComponentChildren, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { DatePicker } from "../picker/DatePicker.js";
+import { InputProps, useField } from "./useField.js";
+import { dateFormatForSettings, useSettings } from "../../hooks/useSettings.js";
+
+export interface Props<T> extends InputProps<T> {
+ readonly?: boolean;
+ expand?: boolean;
+ //FIXME: create separated components InputDate and InputTimestamp
+ withTimestampSupport?: boolean;
+ side?: ComponentChildren;
+}
+
+export function InputDate<T>({
+ name,
+ readonly,
+ label,
+ placeholder,
+ help,
+ tooltip,
+ expand,
+ withTimestampSupport,
+ side,
+}: Props<keyof T>): VNode {
+ const [opened, setOpened] = useState(false);
+ const { i18n } = useTranslationContext();
+ const [settings] = useSettings()
+
+ const { error, required, value, onChange } = useField<T>(name);
+
+ let strValue = "";
+ if (!value) {
+ strValue = withTimestampSupport ? "unknown" : "";
+ } else if (value instanceof Date) {
+ strValue = format(value, dateFormatForSettings(settings));
+ } else if (value.t_s) {
+ strValue =
+ value.t_s === "never"
+ ? withTimestampSupport
+ ? "never"
+ : ""
+ : format(new Date(value.t_s * 1000), dateFormatForSettings(settings));
+ }
+
+ return (
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ {label}
+ {tooltip && (
+ <span class="icon has-tooltip-right" data-tooltip={tooltip}>
+ <i class="mdi mdi-information" />
+ </span>
+ )}
+ </label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <div class="field has-addons">
+ <p
+ class={
+ expand
+ ? "control is-expanded has-icons-right"
+ : "control has-icons-right"
+ }
+ >
+ <input
+ class="input"
+ type="text"
+ readonly
+ value={strValue}
+ placeholder={placeholder}
+ onClick={() => {
+ if (!readonly) setOpened(true);
+ }}
+ />
+ {required && (
+ <span class="icon has-text-danger is-right">
+ <i class="mdi mdi-alert" />
+ </span>
+ )}
+ {help}
+ </p>
+ <div
+ class="control"
+ onClick={() => {
+ if (!readonly) setOpened(true);
+ }}
+ >
+ <a class="button is-static">
+ <span class="icon">
+ <i class="mdi mdi-calendar" />
+ </span>
+ </a>
+ </div>
+ </div>
+ {error && <p class="help is-danger">{error}</p>}
+ </div>
+
+ {!readonly && (
+ <span
+ data-tooltip={
+ withTimestampSupport
+ ? i18n.str`change value to unknown date`
+ : i18n.str`change value to empty`
+ }
+ >
+ <button
+ class="button is-info mr-3"
+ onClick={() => onChange(undefined as any)}
+ >
+ <i18n.Translate>clear</i18n.Translate>
+ </button>
+ </span>
+ )}
+ {withTimestampSupport && (
+ <span data-tooltip={i18n.str`change value to never`}>
+ <button
+ class="button is-info"
+ onClick={() => onChange({ t_s: "never" } as any)}
+ >
+ <i18n.Translate>never</i18n.Translate>
+ </button>
+ </span>
+ )}
+ {side}
+ </div>
+ <DatePicker
+ opened={opened}
+ closeFunction={() => setOpened(false)}
+ dateReceiver={(d) => {
+ if (withTimestampSupport) {
+ onChange({ t_s: d.getTime() / 1000 } as any);
+ } else {
+ onChange(d as any);
+ }
+ }}
+ />
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/form/InputDuration.tsx b/packages/auditor-backoffice-ui/src/components/form/InputDuration.tsx
new file mode 100644
index 000000000..7aa2703a4
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/InputDuration.tsx
@@ -0,0 +1,186 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { formatDuration, intervalToDuration } from "date-fns";
+import { ComponentChildren, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { SimpleModal } from "../modal/index.js";
+import { DurationPicker } from "../picker/DurationPicker.js";
+import { InputProps, useField } from "./useField.js";
+import { Duration } from "@gnu-taler/taler-util";
+
+export interface Props<T> extends InputProps<T> {
+ expand?: boolean;
+ readonly?: boolean;
+ withForever?: boolean;
+ side?: ComponentChildren;
+ withoutClear?: boolean;
+}
+
+export function InputDuration<T>({
+ name,
+ expand,
+ placeholder,
+ tooltip,
+ label,
+ help,
+ readonly,
+ withForever,
+ withoutClear,
+ side,
+}: Props<keyof T>): VNode {
+ const [opened, setOpened] = useState(false);
+ const { i18n } = useTranslationContext();
+
+ const { error, required, value: anyValue, onChange } = useField<T>(name);
+ let strValue = "";
+ const value: Duration = anyValue
+ if (!value) {
+ strValue = "";
+ } else if (value.d_ms === "forever") {
+ strValue = i18n.str`forever`;
+ } else {
+ strValue = formatDuration(
+ intervalToDuration({ start: 0, end: value.d_ms }),
+ {
+ locale: {
+ formatDistance: (name, value) => {
+ switch (name) {
+ case "xMonths":
+ return i18n.str`${value}M`;
+ case "xYears":
+ return i18n.str`${value}Y`;
+ case "xDays":
+ return i18n.str`${value}d`;
+ case "xHours":
+ return i18n.str`${value}h`;
+ case "xMinutes":
+ return i18n.str`${value}min`;
+ case "xSeconds":
+ return i18n.str`${value}sec`;
+ }
+ },
+ localize: {
+ day: () => "s",
+ month: () => "m",
+ ordinalNumber: () => "th",
+ dayPeriod: () => "p",
+ quarter: () => "w",
+ era: () => "e",
+ },
+ },
+ },
+ );
+ }
+
+ return (
+ <div class="field is-horizontal">
+ <div class="field-label is-normal is-flex-grow-3">
+ <label class="label">
+ {label}
+ {tooltip && (
+ <span class="icon" data-tooltip={tooltip}>
+ <i class="mdi mdi-information" />
+ </span>
+ )}
+ </label>
+ </div>
+
+ <div class="is-flex-grow-3">
+ <div class="field-body ">
+ <div class="field">
+ <div class="field has-addons">
+ <p class={expand ? "control is-expanded " : "control "}>
+ <input
+ class="input"
+ type="text"
+ readonly
+ value={strValue}
+ placeholder={placeholder}
+ onClick={() => {
+ if (!readonly) setOpened(true);
+ }}
+ />
+ {required && (
+ <span class="icon has-text-danger is-right">
+ <i class="mdi mdi-alert" />
+ </span>
+ )}
+ </p>
+ <div
+ class="control"
+ onClick={() => {
+ if (!readonly) setOpened(true);
+ }}
+ >
+ <a class="button is-static">
+ <span class="icon">
+ <i class="mdi mdi-clock" />
+ </span>
+ </a>
+ </div>
+ </div>
+ {error && <p class="help is-danger">{error}</p>}
+ </div>
+ {withForever && (
+ <span data-tooltip={i18n.str`change value to never`}>
+ <button
+ class="button is-info mr-3"
+ onClick={() => onChange({ d_ms: "forever" } as any)}
+ >
+ <i18n.Translate>forever</i18n.Translate>
+ </button>
+ </span>
+ )}
+ {!readonly && !withoutClear && (
+ <span data-tooltip={i18n.str`change value to empty`}>
+ <button
+ class="button is-info "
+ onClick={() => onChange(undefined as any)}
+ >
+ <i18n.Translate>clear</i18n.Translate>
+ </button>
+ </span>
+ )}
+ {side}
+ </div>
+ <span>
+ {help}
+ </span>
+ </div>
+
+
+ {opened && (
+ <SimpleModal onCancel={() => setOpened(false)}>
+ <DurationPicker
+ days
+ hours
+ minutes
+ value={!value || value.d_ms === "forever" ? 0 : value.d_ms}
+ onChange={(v) => {
+ onChange({ d_ms: v } as any);
+ }}
+ />
+ </SimpleModal>
+ )}
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/form/InputGroup.tsx b/packages/auditor-backoffice-ui/src/components/form/InputGroup.tsx
new file mode 100644
index 000000000..b5e0bd52b
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/InputGroup.tsx
@@ -0,0 +1,86 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { ComponentChildren, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { useGroupField } from "./useGroupField.js";
+
+export interface Props<T> {
+ name: T;
+ children: ComponentChildren;
+ label: ComponentChildren;
+ tooltip?: ComponentChildren;
+ alternative?: ComponentChildren;
+ fixed?: boolean;
+ initialActive?: boolean;
+}
+
+export function InputGroup<T>({
+ name,
+ label,
+ children,
+ tooltip,
+ alternative,
+ fixed,
+ initialActive,
+}: Props<keyof T>): VNode {
+ const [active, setActive] = useState(initialActive || fixed);
+ const group = useGroupField<T>(name);
+
+ return (
+ <div class="card">
+ <header class="card-header">
+ <p class="card-header-title">
+ {label}
+ {tooltip && (
+ <span class="icon has-tooltip-right" data-tooltip={tooltip}>
+ <i class="mdi mdi-information" />
+ </span>
+ )}
+ {group?.hasError && (
+ <span class="icon has-text-danger" data-tooltip={tooltip}>
+ <i class="mdi mdi-alert" />
+ </span>
+ )}
+ </p>
+ {!fixed && (
+ <button
+ class="card-header-icon"
+ aria-label="more options"
+ onClick={(): void => setActive(!active)}
+ >
+ <span class="icon">
+ {active ? (
+ <i class="mdi mdi-arrow-up" />
+ ) : (
+ <i class="mdi mdi-arrow-down" />
+ )}
+ </span>
+ </button>
+ )}
+ </header>
+ {active ? (
+ <div class="card-content">{children}</div>
+ ) : alternative ? (
+ <div class="card-content">{alternative}</div>
+ ) : undefined}
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/form/InputImage.tsx b/packages/auditor-backoffice-ui/src/components/form/InputImage.tsx
new file mode 100644
index 000000000..b024e2c6b
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/InputImage.tsx
@@ -0,0 +1,122 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { ComponentChildren, h, VNode } from "preact";
+import { useRef, useState } from "preact/hooks";
+import { MAX_IMAGE_SIZE as MAX_IMAGE_UPLOAD_SIZE } from "../../utils/constants.js";
+import { InputProps, useField } from "./useField.js";
+
+export interface Props<T> extends InputProps<T> {
+ expand?: boolean;
+ addonAfter?: ComponentChildren;
+ children?: ComponentChildren;
+}
+
+export function InputImage<T>({
+ name,
+ readonly,
+ placeholder,
+ tooltip,
+ label,
+ help,
+ children,
+ expand,
+}: Props<keyof T>): VNode {
+ const { error, value, onChange } = useField<T>(name);
+
+ const image = useRef<HTMLInputElement>(null);
+ const { i18n } = useTranslationContext();
+ const [sizeError, setSizeError] = useState(false);
+
+ return (
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ {label}
+ {tooltip && (
+ <span class="icon has-tooltip-right" data-tooltip={tooltip}>
+ <i class="mdi mdi-information" />
+ </span>
+ )}
+ </label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class={expand ? "control is-expanded" : "control"}>
+ {value && (
+ <img
+ src={value}
+ style={{ width: 200, height: 200 }}
+ onClick={() => image.current?.click()}
+ />
+ )}
+ <input
+ ref={image}
+ style={{ display: "none" }}
+ type="file"
+ name={String(name)}
+ placeholder={placeholder}
+ readonly={readonly}
+ onChange={(e) => {
+ const f: FileList | null = e.currentTarget.files;
+ if (!f || f.length != 1) {
+ return onChange(undefined!);
+ }
+ if (f[0].size > MAX_IMAGE_UPLOAD_SIZE) {
+ setSizeError(true);
+ return onChange(undefined!);
+ }
+ setSizeError(false);
+ return f[0].arrayBuffer().then((b) => {
+ const b64 = window.btoa(
+ new Uint8Array(b).reduce(
+ (data, byte) => data + String.fromCharCode(byte),
+ "",
+ ),
+ );
+ return onChange(`data:${f[0].type};base64,${b64}` as any);
+ });
+ }}
+ />
+ {help}
+ {children}
+ </p>
+ {error && <p class="help is-danger">{error}</p>}
+ {sizeError && (
+ <p class="help is-danger">
+ <i18n.Translate>Image should be smaller than 1 MB</i18n.Translate>
+ </p>
+ )}
+ {!value && (
+ <button class="button" onClick={() => image.current?.click()}>
+ <i18n.Translate>Add</i18n.Translate>
+ </button>
+ )}
+ {value && (
+ <button class="button" onClick={() => onChange(undefined!)}>
+ <i18n.Translate>Remove</i18n.Translate>
+ </button>
+ )}
+ </div>
+ </div>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/form/InputLocation.tsx b/packages/auditor-backoffice-ui/src/components/form/InputLocation.tsx
new file mode 100644
index 000000000..a2fc8113e
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/InputLocation.tsx
@@ -0,0 +1,53 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { Fragment, h } from "preact";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Input } from "./Input.js";
+
+export function InputLocation({ name }: { name: string }) {
+ const { i18n } = useTranslationContext();
+ return (
+ <>
+ <Input name={`${name}.country`} label={i18n.str`Country`} />
+ <Input
+ name={`${name}.address_lines`}
+ inputType="multiline"
+ label={i18n.str`Address`}
+ toStr={(v: string[] | undefined) => (!v ? "" : v.join("\n"))}
+ fromStr={(v: string) => v.split("\n")}
+ />
+ <Input
+ name={`${name}.building_number`}
+ label={i18n.str`Building number`}
+ />
+ <Input name={`${name}.building_name`} label={i18n.str`Building name`} />
+ <Input name={`${name}.street`} label={i18n.str`Street`} />
+ <Input name={`${name}.post_code`} label={i18n.str`Post code`} />
+ <Input name={`${name}.town_location`} label={i18n.str`Town location`} />
+ <Input name={`${name}.town`} label={i18n.str`Town`} />
+ <Input name={`${name}.district`} label={i18n.str`District`} />
+ <Input
+ name={`${name}.country_subdivision`}
+ label={i18n.str`Country subdivision`}
+ />
+ </>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/form/InputNumber.tsx b/packages/auditor-backoffice-ui/src/components/form/InputNumber.tsx
new file mode 100644
index 000000000..3b5df1474
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/InputNumber.tsx
@@ -0,0 +1,60 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { ComponentChildren, h } from "preact";
+import { InputWithAddon } from "./InputWithAddon.js";
+import { InputProps } from "./useField.js";
+
+export interface Props<T> extends InputProps<T> {
+ readonly?: boolean;
+ expand?: boolean;
+ side?: ComponentChildren;
+ children?: ComponentChildren;
+}
+
+export function InputNumber<T>({
+ name,
+ readonly,
+ placeholder,
+ tooltip,
+ label,
+ help,
+ expand,
+ children,
+ side,
+}: Props<keyof T>) {
+ return (
+ <InputWithAddon<T>
+ name={name}
+ readonly={readonly}
+ fromStr={(v) => (!v ? undefined : parseInt(v, 10))}
+ toStr={(v) => `${v}`}
+ inputType="number"
+ expand={expand}
+ label={label}
+ placeholder={placeholder}
+ help={help}
+ tooltip={tooltip}
+ inputExtra={{ min: 0 }}
+ children={children}
+ side={side}
+ />
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/form/InputPayto.tsx b/packages/auditor-backoffice-ui/src/components/form/InputPayto.tsx
new file mode 100644
index 000000000..6e88e8f2c
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/InputPayto.tsx
@@ -0,0 +1,52 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { h, VNode } from "preact";
+import { InputArray } from "./InputArray.js";
+import { PAYTO_REGEX } from "../../utils/constants.js";
+import { InputProps } from "./useField.js";
+
+export type Props<T> = InputProps<T>;
+
+const PAYTO_START_REGEX = /^payto:\/\//;
+
+export function InputPayto<T>({
+ name,
+ readonly,
+ placeholder,
+ tooltip,
+ label,
+ help,
+}: Props<keyof T>): VNode {
+ return (
+ <InputArray<T>
+ name={name}
+ readonly={readonly}
+ addonBefore="payto://"
+ label={label}
+ placeholder={placeholder}
+ help={help}
+ tooltip={tooltip}
+ isValid={(v) => v && PAYTO_REGEX.test(v)}
+ toStr={(v?: string) => (!v ? "" : v.replace(PAYTO_START_REGEX, ""))}
+ fromStr={(v: string) => `payto://${v}`}
+ />
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/form/InputPaytoForm.stories.tsx b/packages/auditor-backoffice-ui/src/components/form/InputPaytoForm.stories.tsx
new file mode 100644
index 000000000..282e52278
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/InputPaytoForm.stories.tsx
@@ -0,0 +1,47 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h } from "preact";
+import * as tests from "@gnu-taler/web-util/testing";
+import { InputPaytoForm } from "./InputPaytoForm.js";
+import { FormProvider } from "./FormProvider.js";
+import { useState } from "preact/hooks";
+
+export default {
+ title: "Components/Form/PayTo",
+ component: InputPaytoForm,
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+export const Example = tests.createExample(() => {
+ const initial = {
+ accounts: [],
+ };
+ const [form, updateForm] = useState<Partial<typeof initial>>(initial);
+ return (
+ <FormProvider valueHandler={updateForm} object={form}>
+ <InputPaytoForm name="accounts" label="Accounts:" />
+ </FormProvider>
+ );
+}, {});
diff --git a/packages/auditor-backoffice-ui/src/components/form/InputPaytoForm.tsx b/packages/auditor-backoffice-ui/src/components/form/InputPaytoForm.tsx
new file mode 100644
index 000000000..32545c89a
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/InputPaytoForm.tsx
@@ -0,0 +1,397 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { parsePaytoUri, PaytoUriGeneric, stringifyPaytoUri } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { COUNTRY_TABLE } from "../../utils/constants.js";
+import { undefinedIfEmpty } from "../../utils/table.js";
+import { FormErrors, FormProvider } from "./FormProvider.js";
+import { Input } from "./Input.js";
+import { InputGroup } from "./InputGroup.js";
+import { InputSelector } from "./InputSelector.js";
+import { InputProps, useField } from "./useField.js";
+import { useEffect, useState } from "preact/hooks";
+
+export interface Props<T> extends InputProps<T> {
+ isValid?: (e: any) => boolean;
+}
+
+// type Entity = PaytoUriGeneric
+// https://datatracker.ietf.org/doc/html/rfc8905
+type Entity = {
+ // iban, bitcoin, x-taler-bank. it defined the format
+ target: string;
+ // path1 if the first field to be used
+ path1?: string;
+ // path2 if the second field to be used, optional
+ path2?: string;
+ // params of the payto uri
+ params: {
+ "receiver-name"?: string;
+ sender?: string;
+ message?: string;
+ amount?: string;
+ instruction?: string;
+ [name: string]: string | undefined;
+ };
+};
+
+function isEthereumAddress(address: string) {
+ if (!/^(0x)?[0-9a-f]{40}$/i.test(address)) {
+ return false;
+ } else if (
+ /^(0x|0X)?[0-9a-f]{40}$/.test(address) ||
+ /^(0x|0X)?[0-9A-F]{40}$/.test(address)
+ ) {
+ return true;
+ }
+ return checkAddressChecksum(address);
+}
+
+function checkAddressChecksum(address: string) {
+ //TODO implement ethereum checksum
+ return true;
+}
+
+function validateBitcoin(
+ addr: string,
+ i18n: ReturnType<typeof useTranslationContext>["i18n"],
+): string | undefined {
+ try {
+ const valid = /^(bc1|[13])[a-zA-HJ-NP-Z0-9]{25,39}$/.test(addr);
+ if (valid) return undefined;
+ } catch (e) {
+ console.log(e);
+ }
+ return i18n.str`This is not a valid bitcoin address.`;
+}
+
+function validateEthereum(
+ addr: string,
+ i18n: ReturnType<typeof useTranslationContext>["i18n"],
+): string | undefined {
+ try {
+ const valid = isEthereumAddress(addr);
+ if (valid) return undefined;
+ } catch (e) {
+ console.log(e);
+ }
+ return i18n.str`This is not a valid Ethereum address.`;
+}
+
+/**
+ * An IBAN is validated by converting it into an integer and performing a
+ * basic mod-97 operation (as described in ISO 7064) on it.
+ * If the IBAN is valid, the remainder equals 1.
+ *
+ * The algorithm of IBAN validation is as follows:
+ * 1.- Check that the total IBAN length is correct as per the country. If not, the IBAN is invalid
+ * 2.- Move the four initial characters to the end of the string
+ * 3.- Replace each letter in the string with two digits, thereby expanding the string, where A = 10, B = 11, ..., Z = 35
+ * 4.- Interpret the string as a decimal integer and compute the remainder of that number on division by 97
+ *
+ * If the remainder is 1, the check digit test is passed and the IBAN might be valid.
+ *
+ */
+function validateIBAN(
+ iban: string,
+ i18n: ReturnType<typeof useTranslationContext>["i18n"],
+): string | undefined {
+ // Check total length
+ if (iban.length < 4)
+ return i18n.str`IBAN numbers usually have more that 4 digits`;
+ if (iban.length > 34)
+ return i18n.str`IBAN numbers usually have less that 34 digits`;
+
+ const A_code = "A".charCodeAt(0);
+ const Z_code = "Z".charCodeAt(0);
+ const IBAN = iban.toUpperCase();
+ // check supported country
+ const code = IBAN.substr(0, 2);
+ const found = code in COUNTRY_TABLE;
+ if (!found) return i18n.str`IBAN country code not found`;
+
+ // 2.- Move the four initial characters to the end of the string
+ const step2 = IBAN.substr(4) + iban.substr(0, 4);
+ const step3 = Array.from(step2)
+ .map((letter) => {
+ const code = letter.charCodeAt(0);
+ if (code < A_code || code > Z_code) return letter;
+ return `${letter.charCodeAt(0) - "A".charCodeAt(0) + 10}`;
+ })
+ .join("");
+
+ function calculate_iban_checksum(str: string): number {
+ const numberStr = str.substr(0, 5);
+ const rest = str.substr(5);
+ const number = parseInt(numberStr, 10);
+ const result = number % 97;
+ if (rest.length > 0) {
+ return calculate_iban_checksum(`${result}${rest}`);
+ }
+ return result;
+ }
+
+ const checksum = calculate_iban_checksum(step3);
+ if (checksum !== 1)
+ return i18n.str`IBAN number is not valid, checksum is wrong`;
+ return undefined;
+}
+
+// const targets = ['ach', 'bic', 'iban', 'upi', 'bitcoin', 'ilp', 'void', 'x-taler-bank']
+const targets = [
+ "Choose one...",
+ "iban",
+ "x-taler-bank",
+ "bitcoin",
+ "ethereum",
+];
+const noTargetValue = targets[0];
+const defaultTarget: Entity = {
+ target: noTargetValue,
+ params: {},
+};
+
+export function InputPaytoForm<T>({
+ name,
+ readonly,
+ label,
+ tooltip,
+}: Props<keyof T>): VNode {
+ const { value: initialValueStr, onChange } = useField<T>(name);
+
+ const initialPayto = parsePaytoUri(initialValueStr ?? "")
+ const paths = !initialPayto ? [] : initialPayto.targetPath.split("/")
+ const initialPath1 = paths.length >= 1 ? paths[0] : undefined;
+ const initialPath2 = paths.length >= 2 ? paths[1] : undefined;
+ const initial: Entity = initialPayto === undefined ? defaultTarget : {
+ target: initialPayto.targetType,
+ params: initialPayto.params,
+ path1: initialPath1,
+ path2: initialPath2,
+ }
+ const [value, setValue] = useState<Partial<Entity>>(initial)
+
+ const { i18n } = useTranslationContext();
+
+ const errors: FormErrors<Entity> = {
+ target:
+ value.target === noTargetValue
+ ? i18n.str`required`
+ : undefined,
+ path1: !value.path1
+ ? i18n.str`required`
+ : value.target === "iban"
+ ? validateIBAN(value.path1, i18n)
+ : value.target === "bitcoin"
+ ? validateBitcoin(value.path1, i18n)
+ : value.target === "ethereum"
+ ? validateEthereum(value.path1, i18n)
+ : undefined,
+ path2:
+ value.target === "x-taler-bank"
+ ? !value.path2
+ ? i18n.str`required`
+ : undefined
+ : undefined,
+ params: undefinedIfEmpty({
+ "receiver-name": !value.params?.["receiver-name"]
+ ? i18n.str`required`
+ : undefined,
+ }),
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+ const str = hasErrors || !value.target ? undefined : stringifyPaytoUri({
+ targetType: value.target,
+ targetPath: value.path2 ? `${value.path1}/${value.path2}` : (value.path1 ?? ""),
+ params: value.params ?? {} as any,
+ isKnown: false,
+ })
+ useEffect(() => {
+ onChange(str as any)
+ }, [str])
+
+ // const submit = useCallback((): void => {
+ // // const accounts: MerchantBackend.BankAccounts.AccountAddDetails[] = paytos;
+ // // const alreadyExists =
+ // // accounts.findIndex((x) => x.payto_uri === paytoURL) !== -1;
+ // // if (!alreadyExists) {
+ // const newValue: MerchantBackend.BankAccounts.AccountAddDetails = {
+ // payto_uri: paytoURL,
+ // };
+ // if (value.auth) {
+ // if (value.auth.url) {
+ // newValue.credit_facade_url = value.auth.url;
+ // }
+ // if (value.auth.type === "none") {
+ // newValue.credit_facade_credentials = {
+ // type: "none",
+ // };
+ // }
+ // if (value.auth.type === "basic") {
+ // newValue.credit_facade_credentials = {
+ // type: "basic",
+ // username: value.auth.username ?? "",
+ // password: value.auth.password ?? "",
+ // };
+ // }
+ // }
+ // onChange(newValue as any);
+ // // }
+ // // valueHandler(defaultTarget);
+ // }, [value]);
+
+ //FIXME: translating plural singular
+ return (
+ <InputGroup name="payto" label={label} fixed tooltip={tooltip}>
+ <FormProvider<Entity>
+ name="tax"
+ errors={errors}
+ object={value}
+ valueHandler={setValue}
+ >
+ <InputSelector<Entity>
+ name="target"
+ label={i18n.str`Account type`}
+ tooltip={i18n.str`Method to use for wire transfer`}
+ values={targets}
+ readonly={readonly}
+ toStr={(v) => (v === noTargetValue ? i18n.str`Choose one...` : v)}
+ />
+
+ {value.target === "ach" && (
+ <Fragment>
+ <Input<Entity>
+ name="path1"
+ label={i18n.str`Routing`}
+ readonly={readonly}
+ tooltip={i18n.str`Routing number.`}
+ />
+ <Input<Entity>
+ name="path2"
+ label={i18n.str`Account`}
+ readonly={readonly}
+ tooltip={i18n.str`Account number.`}
+ />
+ </Fragment>
+ )}
+ {value.target === "bic" && (
+ <Fragment>
+ <Input<Entity>
+ name="path1"
+ label={i18n.str`Code`}
+ readonly={readonly}
+ tooltip={i18n.str`Business Identifier Code.`}
+ />
+ </Fragment>
+ )}
+ {value.target === "iban" && (
+ <Fragment>
+ <Input<Entity>
+ name="path1"
+ label={i18n.str`IBAN`}
+ tooltip={i18n.str`International Bank Account Number.`}
+ readonly={readonly}
+ placeholder="DE1231231231"
+ inputExtra={{ style: { textTransform: "uppercase" } }}
+ />
+ </Fragment>
+ )}
+ {value.target === "upi" && (
+ <Fragment>
+ <Input<Entity>
+ name="path1"
+ readonly={readonly}
+ label={i18n.str`Account`}
+ tooltip={i18n.str`Unified Payment Interface.`}
+ />
+ </Fragment>
+ )}
+ {value.target === "bitcoin" && (
+ <Fragment>
+ <Input<Entity>
+ name="path1"
+ readonly={readonly}
+ label={i18n.str`Address`}
+ tooltip={i18n.str`Bitcoin protocol.`}
+ />
+ </Fragment>
+ )}
+ {value.target === "ethereum" && (
+ <Fragment>
+ <Input<Entity>
+ name="path1"
+ readonly={readonly}
+ label={i18n.str`Address`}
+ tooltip={i18n.str`Ethereum protocol.`}
+ />
+ </Fragment>
+ )}
+ {value.target === "ilp" && (
+ <Fragment>
+ <Input<Entity>
+ name="path1"
+ readonly={readonly}
+ label={i18n.str`Address`}
+ tooltip={i18n.str`Interledger protocol.`}
+ />
+ </Fragment>
+ )}
+ {value.target === "void" && <Fragment />}
+ {value.target === "x-taler-bank" && (
+ <Fragment>
+ <Input<Entity>
+ name="path1"
+ readonly={readonly}
+ label={i18n.str`Host`}
+ tooltip={i18n.str`Bank host.`}
+ />
+ <Input<Entity>
+ name="path2"
+ readonly={readonly}
+ label={i18n.str`Account`}
+ tooltip={i18n.str`Bank account.`}
+ />
+ </Fragment>
+ )}
+
+ {/**
+ * Show additional fields apart from the payto
+ */}
+ {value.target !== noTargetValue && (
+ <Fragment>
+ <Input
+ name="params.receiver-name"
+ readonly={readonly}
+ label={i18n.str`Owner's name`}
+ tooltip={i18n.str`Legal name of the person holding the account.`}
+ />
+ </Fragment>
+ )}
+
+ </FormProvider>
+ </InputGroup>
+ );
+}
+
diff --git a/packages/auditor-backoffice-ui/src/components/form/InputSearchOnList.tsx b/packages/auditor-backoffice-ui/src/components/form/InputSearchOnList.tsx
new file mode 100644
index 000000000..be5800d14
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/InputSearchOnList.tsx
@@ -0,0 +1,204 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import emptyImage from "../../assets/empty.png";
+import { FormErrors, FormProvider } from "./FormProvider.js";
+import { InputWithAddon } from "./InputWithAddon.js";
+import { TranslatedString } from "@gnu-taler/taler-util";
+
+type Entity = {
+ id: string,
+ description: string;
+ image?: string;
+ extra?: string;
+};
+
+export interface Props<T extends Entity> {
+ selected?: T;
+ onChange: (p?: T) => void;
+ label: TranslatedString;
+ list: T[];
+ withImage?: boolean;
+}
+
+interface Search {
+ name: string;
+}
+
+export function InputSearchOnList<T extends Entity>({
+ selected,
+ onChange,
+ label,
+ list,
+ withImage,
+}: Props<T>): VNode {
+ const [nameForm, setNameForm] = useState<Partial<Search>>({
+ name: "",
+ });
+
+ const errors: FormErrors<Search> = {
+ name: undefined,
+ };
+ const { i18n } = useTranslationContext();
+
+ if (selected) {
+ return (
+ <article class="media">
+ {withImage &&
+ <figure class="media-left">
+ <p class="image is-128x128">
+ <img src={selected.image ? selected.image : emptyImage} />
+ </p>
+ </figure>
+ }
+ <div class="media-content">
+ <div class="content">
+ <p class="media-meta">
+ <i18n.Translate>ID</i18n.Translate>: <b>{selected.id}</b>
+ </p>
+ <p>
+ <i18n.Translate>Description</i18n.Translate>:{" "}
+ {selected.description}
+ </p>
+ <div class="buttons is-right mt-5">
+ <button
+ class="button is-info"
+ onClick={() => onChange(undefined)}
+ >
+ clear
+ </button>
+ </div>
+ </div>
+ </div>
+ </article>
+ );
+ }
+
+ return (
+ <FormProvider<Search>
+ errors={errors}
+ object={nameForm}
+ valueHandler={setNameForm}
+ >
+ <InputWithAddon<Search>
+ name="name"
+ label={label}
+ tooltip={i18n.str`enter description or id`}
+ addonAfter={
+ <span class="icon">
+ <i class="mdi mdi-magnify" />
+ </span>
+ }
+ >
+ <div>
+ <DropdownList
+ name={nameForm.name}
+ list={list}
+ onSelect={(p) => {
+ setNameForm({ name: "" });
+ onChange(p);
+ }}
+ withImage={!!withImage}
+ />
+ </div>
+ </InputWithAddon>
+ </FormProvider>
+ );
+}
+
+interface DropdownListProps<T extends Entity> {
+ name?: string;
+ onSelect: (p: T) => void;
+ list: T[];
+ withImage: boolean;
+}
+
+function DropdownList<T extends Entity>({ name, onSelect, list, withImage }: DropdownListProps<T>) {
+ const { i18n } = useTranslationContext();
+ if (!name) {
+ /* FIXME
+ this BR is added to occupy the space that will be added when the
+ dropdown appears
+ */
+ return (
+ <div>
+ <br />
+ </div>
+ );
+ }
+ const filtered = list.filter(
+ (p) => p.id.includes(name) || p.description.includes(name),
+ );
+
+ return (
+ <div class="dropdown is-active">
+ <div
+ class="dropdown-menu"
+ id="dropdown-menu"
+ role="menu"
+ style={{ minWidth: "20rem" }}
+ >
+ <div class="dropdown-content">
+ {!filtered.length ? (
+ <div class="dropdown-item">
+ <i18n.Translate>
+ no match found with that description or id
+ </i18n.Translate>
+ </div>
+ ) : (
+ filtered.map((p) => (
+ <div
+ key={p.id}
+ class="dropdown-item"
+ onClick={() => onSelect(p)}
+ style={{ cursor: "pointer" }}
+ >
+ <article class="media">
+ {withImage &&
+ <div class="media-left">
+ <div class="image" style={{ minWidth: 64 }}>
+ <img
+ src={p.image ? p.image : emptyImage}
+ style={{ width: 64, height: 64 }}
+ />
+ </div>
+ </div>
+ }
+ <div class="media-content">
+ <div class="content">
+ <p>
+ <strong>{p.id}</strong> {p.extra !== undefined ? <small>{p.extra}</small> : undefined}
+ <br />
+ {p.description}
+ </p>
+ </div>
+ </div>
+ </article>
+ </div>
+ ))
+ )}
+ </div>
+ </div>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/form/InputSecured.stories.tsx b/packages/auditor-backoffice-ui/src/components/form/InputSecured.stories.tsx
new file mode 100644
index 000000000..12ce6c6aa
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/InputSecured.stories.tsx
@@ -0,0 +1,61 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { FormProvider } from "./FormProvider.js";
+import { InputSecured } from "./InputSecured.js";
+
+export default {
+ title: "Components/Form/InputSecured",
+ component: InputSecured,
+};
+
+type T = { auth_token: string | null };
+
+export const InitialValueEmpty = (): VNode => {
+ const [state, setState] = useState<Partial<T>>({ auth_token: "" });
+ return (
+ <FormProvider<T> object={state} errors={{}} valueHandler={setState}>
+ Initial value: ''
+ <InputSecured<T> name="auth_token" label="Access token" />
+ </FormProvider>
+ );
+};
+
+export const InitialValueToken = (): VNode => {
+ const [state, setState] = useState<Partial<T>>({ auth_token: "token" });
+ return (
+ <FormProvider<T> object={state} errors={{}} valueHandler={setState}>
+ <InputSecured<T> name="auth_token" label="Access token" />
+ </FormProvider>
+ );
+};
+
+export const InitialValueNull = (): VNode => {
+ const [state, setState] = useState<Partial<T>>({ auth_token: null });
+ return (
+ <FormProvider<T> object={state} errors={{}} valueHandler={setState}>
+ Initial value: ''
+ <InputSecured<T> name="auth_token" label="Access token" />
+ </FormProvider>
+ );
+};
diff --git a/packages/auditor-backoffice-ui/src/components/form/InputSecured.tsx b/packages/auditor-backoffice-ui/src/components/form/InputSecured.tsx
new file mode 100644
index 000000000..9d1a3ab8e
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/InputSecured.tsx
@@ -0,0 +1,186 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { InputProps, useField } from "./useField.js";
+
+export type Props<T> = InputProps<T>;
+
+const TokenStatus = ({ prev, post }: any) => {
+ const { i18n } = useTranslationContext();
+ if (
+ (prev === undefined || prev === null) &&
+ (post === undefined || post === null)
+ )
+ return null;
+ return prev === post ? null : post === null ? (
+ <span class="tag is-danger is-align-self-center ml-2">
+ <i18n.Translate>Deleting</i18n.Translate>
+ </span>
+ ) : (
+ <span class="tag is-warning is-align-self-center ml-2">
+ <i18n.Translate>Changing</i18n.Translate>
+ </span>
+ );
+};
+
+export function InputSecured<T>({
+ name,
+ readonly,
+ placeholder,
+ tooltip,
+ label,
+ help,
+}: Props<keyof T>): VNode {
+ const { error, value, initial, onChange, toStr, fromStr } = useField<T>(name);
+
+ const [active, setActive] = useState(false);
+ const [newValue, setNuewValue] = useState("");
+
+ const { i18n } = useTranslationContext();
+
+ return (
+ <Fragment>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ {label}
+ {tooltip && (
+ <span class="icon has-tooltip-right" data-tooltip={tooltip}>
+ <i class="mdi mdi-information" />
+ </span>
+ )}
+ </label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ {!active ? (
+ <Fragment>
+ <div class="field has-addons">
+ <button
+ class="button"
+ onClick={(): void => {
+ setActive(!active);
+ }}
+ >
+ <div class="icon is-left">
+ <i class="mdi mdi-lock-reset" />
+ </div>
+ <span>
+ <i18n.Translate>Manage access token</i18n.Translate>
+ </span>
+ </button>
+ <TokenStatus prev={initial} post={value} />
+ </div>
+ </Fragment>
+ ) : (
+ <Fragment>
+ <div class="field has-addons">
+ <div class="control">
+ <a class="button is-static">secret-token:</a>
+ </div>
+ <div class="control is-expanded">
+ <input
+ class="input"
+ type="text"
+ placeholder={placeholder}
+ readonly={readonly || !active}
+ disabled={readonly || !active}
+ name={String(name)}
+ value={newValue}
+ onInput={(e): void => {
+ setNuewValue(e.currentTarget.value);
+ }}
+ />
+ {help}
+ </div>
+ <div class="control">
+ <button
+ class="button is-info"
+ disabled={fromStr(newValue) === value}
+ onClick={(): void => {
+ onChange(fromStr(newValue));
+ setActive(!active);
+ setNuewValue("");
+ }}
+ >
+ <div class="icon is-left">
+ <i class="mdi mdi-lock-outline" />
+ </div>
+ <span>
+ <i18n.Translate>Update</i18n.Translate>
+ </span>
+ </button>
+ </div>
+ </div>
+ </Fragment>
+ )}
+ {error ? <p class="help is-danger">{error}</p> : null}
+ </div>
+ </div>
+ {active && (
+ <div class="field is-horizontal">
+ <div class="field-body is-flex-grow-3">
+ <div class="level" style={{ width: "100%" }}>
+ <div class="level-right is-flex-grow-1">
+ <div class="level-item">
+ <button
+ class="button is-danger"
+ disabled={null === value || undefined === value}
+ onClick={(): void => {
+ onChange(null!);
+ setActive(!active);
+ setNuewValue("");
+ }}
+ >
+ <div class="icon is-left">
+ <i class="mdi mdi-lock-open-variant" />
+ </div>
+ <span>
+ <i18n.Translate>Remove</i18n.Translate>
+ </span>
+ </button>
+ </div>
+ <div class="level-item">
+ <button
+ class="button "
+ onClick={(): void => {
+ onChange(initial!);
+ setActive(!active);
+ setNuewValue("");
+ }}
+ >
+ <div class="icon is-left">
+ <i class="mdi mdi-lock-open-variant" />
+ </div>
+ <span>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </span>
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ )}
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/form/InputSelector.tsx b/packages/auditor-backoffice-ui/src/components/form/InputSelector.tsx
new file mode 100644
index 000000000..a8dad5d89
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/InputSelector.tsx
@@ -0,0 +1,94 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { h, VNode } from "preact";
+import { InputProps, useField } from "./useField.js";
+
+interface Props<T> extends InputProps<T> {
+ readonly?: boolean;
+ expand?: boolean;
+ values: any[];
+ toStr?: (v?: any) => string;
+ fromStr?: (s: string) => any;
+}
+
+const defaultToString = (f?: any): string => f || "";
+const defaultFromString = (v: string): any => v as any;
+
+export function InputSelector<T>({
+ name,
+ readonly,
+ expand,
+ placeholder,
+ tooltip,
+ label,
+ help,
+ values,
+ fromStr = defaultFromString,
+ toStr = defaultToString,
+}: Props<keyof T>): VNode {
+ const { error, value, onChange, required } = useField<T>(name);
+ return (
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ {label}
+ {tooltip && (
+ <span class="icon has-tooltip-right" data-tooltip={tooltip}>
+ <i class="mdi mdi-information" />
+ </span>
+ )}
+ </label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field has-icons-right">
+ <p class={expand ? "control is-expanded select" : "control select "}>
+ <select
+ class={error ? "select is-danger" : "select"}
+ name={String(name)}
+ disabled={readonly}
+ readonly={readonly}
+ onChange={(e) => {
+ onChange(fromStr(e.currentTarget.value));
+ }}
+ >
+ {placeholder && <option>{placeholder}</option>}
+ {values.map((v, i) => {
+ return (
+ <option key={i} value={v} selected={value === v}>
+ {toStr(v)}
+ </option>
+ );
+ })}
+ </select>
+
+ {help}
+ </p>
+ {required && (
+ <span class="icon has-text-danger is-right" style={{height: "2.5em"}}>
+ <i class="mdi mdi-alert" />
+ </span>
+ )}
+ {error && <p class="help is-danger">{error}</p>}
+ </div>
+ </div>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/form/InputStock.stories.tsx b/packages/auditor-backoffice-ui/src/components/form/InputStock.stories.tsx
new file mode 100644
index 000000000..668c65ea7
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/InputStock.stories.tsx
@@ -0,0 +1,162 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { addDays } from "date-fns";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { FormProvider } from "./FormProvider.js";
+import { InputStock, Stock } from "./InputStock.js";
+
+export default {
+ title: "Components/Form/InputStock",
+ component: InputStock,
+};
+
+type T = { stock?: Stock };
+
+export const CreateStockEmpty = () => {
+ const [state, setState] = useState<Partial<T>>({});
+ return (
+ <FormProvider<T>
+ name="product"
+ object={state}
+ errors={{}}
+ valueHandler={setState}
+ >
+ <InputStock<T> name="stock" label="Stock" />
+ <div>
+ <pre>{JSON.stringify(state, undefined, 2)}</pre>
+ </div>
+ </FormProvider>
+ );
+};
+
+export const CreateStockUnknownRestock = () => {
+ const [state, setState] = useState<Partial<T>>({
+ stock: {
+ current: 10,
+ lost: 0,
+ sold: 0,
+ },
+ });
+ return (
+ <FormProvider<T>
+ name="product"
+ object={state}
+ errors={{}}
+ valueHandler={setState}
+ >
+ <InputStock<T> name="stock" label="Stock" />
+ <div>
+ <pre>{JSON.stringify(state, undefined, 2)}</pre>
+ </div>
+ </FormProvider>
+ );
+};
+
+export const CreateStockNoRestock = () => {
+ const [state, setState] = useState<Partial<T>>({
+ stock: {
+ current: 10,
+ lost: 0,
+ sold: 0,
+ nextRestock: { t_s: "never" },
+ },
+ });
+ return (
+ <FormProvider<T>
+ name="product"
+ object={state}
+ errors={{}}
+ valueHandler={setState}
+ >
+ <InputStock<T> name="stock" label="Stock" />
+ <div>
+ <pre>{JSON.stringify(state, undefined, 2)}</pre>
+ </div>
+ </FormProvider>
+ );
+};
+
+export const CreateStockWithRestock = () => {
+ const [state, setState] = useState<Partial<T>>({
+ stock: {
+ current: 15,
+ lost: 0,
+ sold: 0,
+ nextRestock: { t_s: addDays(new Date(), 1).getTime() / 1000 },
+ },
+ });
+ return (
+ <FormProvider<T>
+ name="product"
+ object={state}
+ errors={{}}
+ valueHandler={setState}
+ >
+ <InputStock<T> name="stock" label="Stock" />
+ <div>
+ <pre>{JSON.stringify(state, undefined, 2)}</pre>
+ </div>
+ </FormProvider>
+ );
+};
+
+export const UpdatingProductWithManagedStock = () => {
+ const [state, setState] = useState<Partial<T>>({
+ stock: {
+ current: 100,
+ lost: 0,
+ sold: 0,
+ nextRestock: { t_s: addDays(new Date(), 1).getTime() / 1000 },
+ },
+ });
+ return (
+ <FormProvider<T>
+ name="product"
+ object={state}
+ errors={{}}
+ valueHandler={setState}
+ >
+ <InputStock<T> name="stock" label="Stock" alreadyExist />
+ <div>
+ <pre>{JSON.stringify(state, undefined, 2)}</pre>
+ </div>
+ </FormProvider>
+ );
+};
+
+export const UpdatingProductWithInfiniteStock = () => {
+ const [state, setState] = useState<Partial<T>>({});
+ return (
+ <FormProvider<T>
+ name="product"
+ object={state}
+ errors={{}}
+ valueHandler={setState}
+ >
+ <InputStock<T> name="stock" label="Stock" alreadyExist />
+ <div>
+ <pre>{JSON.stringify(state, undefined, 2)}</pre>
+ </div>
+ </FormProvider>
+ );
+};
diff --git a/packages/auditor-backoffice-ui/src/components/form/InputStock.tsx b/packages/auditor-backoffice-ui/src/components/form/InputStock.tsx
new file mode 100644
index 000000000..1d18685c5
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/InputStock.tsx
@@ -0,0 +1,224 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h } from "preact";
+import { useLayoutEffect, useState } from "preact/hooks";
+import { MerchantBackend, Timestamp } from "../../declaration.js";
+import { FormErrors, FormProvider } from "./FormProvider.js";
+import { InputDate } from "./InputDate.js";
+import { InputGroup } from "./InputGroup.js";
+import { InputLocation } from "./InputLocation.js";
+import { InputNumber } from "./InputNumber.js";
+import { InputProps, useField } from "./useField.js";
+
+export interface Props<T> extends InputProps<T> {
+ alreadyExist?: boolean;
+}
+
+type Entity = Stock;
+
+export interface Stock {
+ current: number;
+ lost: number;
+ sold: number;
+ address?: MerchantBackend.Location;
+ nextRestock?: Timestamp;
+}
+
+interface StockDelta {
+ incoming: number;
+ lost: number;
+}
+
+export function InputStock<T>({
+ name,
+ tooltip,
+ label,
+ alreadyExist,
+}: Props<keyof T>) {
+ const { error, value, onChange } = useField<T>(name);
+
+ const [errors, setErrors] = useState<FormErrors<Entity>>({});
+
+ const [formValue, valueHandler] = useState<Partial<Entity>>(value);
+ const [addedStock, setAddedStock] = useState<StockDelta>({
+ incoming: 0,
+ lost: 0,
+ });
+ const { i18n } = useTranslationContext();
+
+ useLayoutEffect(() => {
+ if (!formValue) {
+ onChange(undefined as any);
+ } else {
+ onChange({
+ ...formValue,
+ current: (formValue?.current || 0) + addedStock.incoming,
+ lost: (formValue?.lost || 0) + addedStock.lost,
+ } as any);
+ }
+ }, [formValue, addedStock]);
+
+ if (!formValue) {
+ return (
+ <Fragment>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ {label}
+ {tooltip && (
+ <span class="icon has-tooltip-right" data-tooltip={tooltip}>
+ <i class="mdi mdi-information" />
+ </span>
+ )}
+ </label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field has-addons">
+ {!alreadyExist ? (
+ <button
+ class="button"
+ data-tooltip={i18n.str`click here to configure the stock of the product, leave it as is and the backend will not control stock`}
+ onClick={(): void => {
+ valueHandler({
+ current: 0,
+ lost: 0,
+ sold: 0,
+ } as Stock as any);
+ }}
+ >
+ <span>
+ <i18n.Translate>Manage stock</i18n.Translate>
+ </span>
+ </button>
+ ) : (
+ <button
+ class="button"
+ data-tooltip={i18n.str`this product has been configured without stock control`}
+ disabled
+ >
+ <span>
+ <i18n.Translate>Infinite</i18n.Translate>
+ </span>
+ </button>
+ )}
+ </div>
+ </div>
+ </div>
+ </Fragment>
+ );
+ }
+
+ const currentStock =
+ (formValue.current || 0) - (formValue.lost || 0) - (formValue.sold || 0);
+
+ const stockAddedErrors: FormErrors<typeof addedStock> = {
+ lost:
+ currentStock + addedStock.incoming < addedStock.lost
+ ? i18n.str`lost cannot be greater than current and incoming (max ${
+ currentStock + addedStock.incoming
+ })`
+ : undefined,
+ };
+
+ // const stockUpdateDescription = stockAddedErrors.lost ? '' : (
+ // !!addedStock.incoming || !!addedStock.lost ?
+ // i18n.str`current stock will change from ${currentStock} to ${currentStock + addedStock.incoming - addedStock.lost}` :
+ // i18n.str`current stock will stay at ${currentStock}`
+ // )
+
+ return (
+ <Fragment>
+ <div class="card">
+ <header class="card-header">
+ <p class="card-header-title">
+ {label}
+ {tooltip && (
+ <span class="icon" data-tooltip={tooltip}>
+ <i class="mdi mdi-information" />
+ </span>
+ )}
+ </p>
+ </header>
+ <div class="card-content">
+ <FormProvider<Entity>
+ name="stock"
+ errors={errors}
+ object={formValue}
+ valueHandler={valueHandler}
+ >
+ {alreadyExist ? (
+ <Fragment>
+ <FormProvider
+ name="added"
+ errors={stockAddedErrors}
+ object={addedStock}
+ valueHandler={setAddedStock as any}
+ >
+ <InputNumber name="incoming" label={i18n.str`Incoming`} />
+ <InputNumber name="lost" label={i18n.str`Lost`} />
+ </FormProvider>
+
+ {/* <div class="field is-horizontal">
+ <div class="field-label is-normal" />
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ {stockUpdateDescription}
+ </div>
+ </div>
+ </div> */}
+ </Fragment>
+ ) : (
+ <InputNumber<Entity>
+ name="current"
+ label={i18n.str`Current`}
+ side={
+ <button
+ class="button is-danger"
+ data-tooltip={i18n.str`remove stock control for this product`}
+ onClick={(): void => {
+ valueHandler(undefined as any);
+ }}
+ >
+ <span>
+ <i18n.Translate>without stock</i18n.Translate>
+ </span>
+ </button>
+ }
+ />
+ )}
+
+ <InputDate<Entity>
+ name="nextRestock"
+ label={i18n.str`Next restock`}
+ withTimestampSupport
+ />
+
+ <InputGroup<Entity> name="address" label={i18n.str`Warehouse address`}>
+ <InputLocation name="address" />
+ </InputGroup>
+ </FormProvider>
+ </div>
+ </div>
+ </Fragment>
+ );
+}
+// (
diff --git a/packages/auditor-backoffice-ui/src/components/form/InputTab.tsx b/packages/auditor-backoffice-ui/src/components/form/InputTab.tsx
new file mode 100644
index 000000000..2701768aa
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/InputTab.tsx
@@ -0,0 +1,90 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { h, VNode } from "preact";
+import { InputProps, useField } from "./useField.js";
+
+interface Props<T> extends InputProps<T> {
+ readonly?: boolean;
+ expand?: boolean;
+ values: any[];
+ toStr?: (v?: any) => string;
+ fromStr?: (s: string) => any;
+}
+
+const defaultToString = (f?: any): string => f || "";
+const defaultFromString = (v: string): any => v as any;
+
+export function InputTab<T>({
+ name,
+ readonly,
+ expand,
+ placeholder,
+ tooltip,
+ label,
+ help,
+ values,
+ fromStr = defaultFromString,
+ toStr = defaultToString,
+}: Props<keyof T>): VNode {
+ const { error, value, onChange, required } = useField<T>(name);
+ return (
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ {label}
+ {tooltip && (
+ <span class="icon has-tooltip-right" data-tooltip={tooltip}>
+ <i class="mdi mdi-information" />
+ </span>
+ )}
+ </label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field has-icons-right">
+ <p class={expand ? "control is-expanded " : "control "}>
+ <div class="tabs is-toggle is-fullwidth is-small">
+ <ul>
+ {values.map((v, i) => {
+ return (
+ <li key={i} class={value === v ? "is-active" : ""}
+ onClick={(e) => { onChange(v) }}
+ >
+ <a style={{ cursor: "initial" }}>
+ <span>{toStr(v)}</span>
+ </a>
+ </li>
+ );
+ })}
+ </ul>
+ </div>
+ {help}
+ </p>
+ {required && (
+ <span class="icon has-text-danger is-right" style={{ height: "2.5em" }}>
+ <i class="mdi mdi-alert" />
+ </span>
+ )}
+ {error && <p class="help is-danger">{error}</p>}
+ </div>
+ </div>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/form/InputTaxes.tsx b/packages/auditor-backoffice-ui/src/components/form/InputTaxes.tsx
new file mode 100644
index 000000000..b5722e4ec
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/InputTaxes.tsx
@@ -0,0 +1,147 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useCallback, useState } from "preact/hooks";
+import * as yup from "yup";
+import { MerchantBackend } from "../../declaration.js";
+import { TaxSchema as schema } from "../../schemas/index.js";
+import { FormErrors, FormProvider } from "./FormProvider.js";
+import { Input } from "./Input.js";
+import { InputGroup } from "./InputGroup.js";
+import { InputProps, useField } from "./useField.js";
+
+export interface Props<T> extends InputProps<T> {
+ isValid?: (e: any) => boolean;
+}
+
+type Entity = MerchantBackend.Tax;
+export function InputTaxes<T>({
+ name,
+ readonly,
+ label,
+}: Props<keyof T>): VNode {
+ const { value: taxes, onChange } = useField<T>(name);
+
+ const [value, valueHandler] = useState<Partial<Entity>>({});
+ // const [errors, setErrors] = useState<FormErrors<Entity>>({})
+
+ let errors: FormErrors<Entity> = {};
+
+ try {
+ schema.validateSync(value, { abortEarly: false });
+ } catch (err) {
+ if (err instanceof yup.ValidationError) {
+ const yupErrors = err.inner as yup.ValidationError[];
+ errors = yupErrors.reduce(
+ (prev, cur) =>
+ !cur.path ? prev : { ...prev, [cur.path]: cur.message },
+ {},
+ );
+ }
+ }
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const submit = useCallback((): void => {
+ onChange([value as any, ...taxes] as any);
+ valueHandler({});
+ }, [value]);
+
+ const { i18n } = useTranslationContext();
+
+ //FIXME: translating plural singular
+ return (
+ <InputGroup
+ name="tax"
+ label={label}
+ alternative={
+ taxes.length > 0 && (
+ <p>This product has {taxes.length} applicable taxes configured.</p>
+ )
+ }
+ >
+ <FormProvider<Entity>
+ name="tax"
+ errors={errors}
+ object={value}
+ valueHandler={valueHandler}
+ >
+ <div class="field is-horizontal">
+ <div class="field-label is-normal" />
+ <div class="field-body" style={{ display: "block" }}>
+ {taxes.map((v: any, i: number) => (
+ <div
+ key={i}
+ class="tags has-addons mt-3 mb-0 mr-3"
+ style={{ flexWrap: "nowrap" }}
+ >
+ <span
+ class="tag is-medium is-info mb-0"
+ style={{ maxWidth: "90%" }}
+ >
+ <b>{v.tax}</b>: {v.name}
+ </span>
+ <a
+ class="tag is-medium is-danger is-delete mb-0"
+ onClick={() => {
+ onChange(taxes.filter((f: any) => f !== v) as any);
+ valueHandler(v);
+ }}
+ />
+ </div>
+ ))}
+ {!taxes.length && i18n.str`No taxes configured for this product.`}
+ </div>
+ </div>
+
+ <Input<Entity>
+ name="tax"
+ label={i18n.str`Amount`}
+ tooltip={i18n.str`Taxes can be in currencies that differ from the main currency used by the merchant.`}
+ >
+ <i18n.Translate>
+ Enter currency and value separated with a colon, e.g.
+ &quot;USD:2.3&quot;.
+ </i18n.Translate>
+ </Input>
+
+ <Input<Entity>
+ name="name"
+ label={i18n.str`Description`}
+ tooltip={i18n.str`Legal name of the tax, e.g. VAT or import duties.`}
+ />
+
+ <div class="buttons is-right mt-5">
+ <button
+ class="button is-info"
+ data-tooltip={i18n.str`add tax to the tax list`}
+ disabled={hasErrors}
+ onClick={submit}
+ >
+ <i18n.Translate>Add</i18n.Translate>
+ </button>
+ </div>
+ </FormProvider>
+ </InputGroup>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/form/InputToggle.tsx b/packages/auditor-backoffice-ui/src/components/form/InputToggle.tsx
new file mode 100644
index 000000000..f95dfcd05
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/InputToggle.tsx
@@ -0,0 +1,91 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { h, VNode } from "preact";
+import { InputProps, useField } from "./useField.js";
+
+interface Props<T> extends InputProps<T> {
+ name: T;
+ readonly?: boolean;
+ expand?: boolean;
+ threeState?: boolean;
+ toBoolean?: (v?: any) => boolean | undefined;
+ fromBoolean?: (s: boolean | undefined) => any;
+}
+
+const defaultToBoolean = (f?: any): boolean | undefined => f || "";
+const defaultFromBoolean = (v: boolean | undefined): any => v as any;
+
+export function InputToggle<T>({
+ name,
+ readonly,
+ placeholder,
+ tooltip,
+ label,
+ help,
+ threeState,
+ expand,
+ fromBoolean = defaultFromBoolean,
+ toBoolean = defaultToBoolean,
+}: Props<keyof T>): VNode {
+ const { error, value, onChange } = useField<T>(name);
+
+ const onCheckboxClick = (): void => {
+ const c = toBoolean(value);
+ if (c === false && threeState) return onChange(undefined as any);
+ return onChange(fromBoolean(!c));
+ };
+
+ return (
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label" >
+ {label}
+ {tooltip && (
+ <span class="icon has-tooltip-right" data-tooltip={tooltip}>
+ <i class="mdi mdi-information" />
+ </span>
+ )}
+ </label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class={expand ? "control is-expanded" : "control"}>
+ <label class="toggle" style={{ marginLeft: 4, marginTop: 0 }}>
+ <input
+ type="checkbox"
+ class={toBoolean(value) === undefined ? "is-indeterminate" : "toggle-checkbox"}
+ checked={toBoolean(value)}
+ placeholder={placeholder}
+ readonly={readonly}
+ name={String(name)}
+ disabled={readonly}
+ onChange={onCheckboxClick}
+ />
+ <div class="toggle-switch"></div>
+ </label>
+ {help}
+ </p>
+ {error && <p class="help is-danger">{error}</p>}
+ </div>
+ </div>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/form/InputWithAddon.tsx b/packages/auditor-backoffice-ui/src/components/form/InputWithAddon.tsx
new file mode 100644
index 000000000..e9fd88770
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/InputWithAddon.tsx
@@ -0,0 +1,116 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { ComponentChildren, h, VNode } from "preact";
+import { InputProps, useField } from "./useField.js";
+
+export interface Props<T> extends InputProps<T> {
+ expand?: boolean;
+ inputType?: "text" | "number" | "password";
+ addonBefore?: ComponentChildren;
+ addonAfter?: ComponentChildren;
+ addonAfterAction?: () => void;
+ toStr?: (v?: any) => string;
+ fromStr?: (s: string) => any;
+ inputExtra?: any;
+ children?: ComponentChildren;
+ side?: ComponentChildren;
+}
+
+const defaultToString = (f?: any): string => f || "";
+const defaultFromString = (v: string): any => v as any;
+
+export function InputWithAddon<T>({
+ name,
+ readonly,
+ addonBefore,
+ children,
+ expand,
+ label,
+ placeholder,
+ help,
+ tooltip,
+ inputType,
+ inputExtra,
+ side,
+ addonAfter,
+ addonAfterAction,
+ toStr = defaultToString,
+ fromStr = defaultFromString,
+}: Props<keyof T>): VNode {
+ const { error, value, onChange, required } = useField<T>(name);
+
+ return (
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ {label}
+ {tooltip && (
+ <span class="icon has-tooltip-right" data-tooltip={tooltip}>
+ <i class="mdi mdi-information" />
+ </span>
+ )}
+ </label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <div class="field has-addons">
+ {addonBefore && (
+ <div class="control">
+ <a class="button is-static">{addonBefore}</a>
+ </div>
+ )}
+ <p
+ class={`control${expand ? " is-expanded" : ""}${required ? " has-icons-right" : ""
+ }`}
+ >
+ <input
+ {...(inputExtra || {})}
+ class={error ? "input is-danger" : "input"}
+ type={inputType}
+ placeholder={placeholder}
+ readonly={readonly}
+ disabled={readonly}
+ name={String(name)}
+ value={toStr(value)}
+ onChange={(e): void => onChange(fromStr(e.currentTarget.value))}
+ />
+ {required && (
+ <span class="icon has-text-danger is-right">
+ <i class="mdi mdi-alert" />
+ </span>
+ )}
+ {children}
+ </p>
+ {addonAfter && (
+ <div class="control" onClick={addonAfterAction} style={{ cursor: addonAfterAction ? "pointer" : undefined }}>
+ <a class="button is-static">{addonAfter}</a>
+ </div>
+ )}
+ </div>
+ {error && <p class="help is-danger">{error}</p>}
+ <span class="has-text-grey">{help}</span>
+ </div>
+ {expand ? <div>{side}</div> : side}
+ </div>
+
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/form/JumpToElementById.tsx b/packages/auditor-backoffice-ui/src/components/form/JumpToElementById.tsx
new file mode 100644
index 000000000..a0e1d6ae4
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/JumpToElementById.tsx
@@ -0,0 +1,59 @@
+import { TranslatedString } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+
+export function JumpToElementById({ testIfExist, onSelect, 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
new file mode 100644
index 000000000..03f36dcbb
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/TextField.tsx
@@ -0,0 +1,71 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { ComponentChildren, h, VNode } from "preact";
+import { useField, InputProps } from "./useField.js";
+
+interface Props<T> extends InputProps<T> {
+ inputType?: "text" | "number" | "multiline" | "password";
+ expand?: boolean;
+ side?: ComponentChildren;
+ children: ComponentChildren;
+}
+
+export function TextField<T>({
+ name,
+ tooltip,
+ label,
+ expand,
+ help,
+ children,
+ side,
+}: Props<keyof T>): VNode {
+ const { error } = useField<T>(name);
+ return (
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ {label}
+ {tooltip && (
+ <span class="icon has-tooltip-right" data-tooltip={tooltip}>
+ <i class="mdi mdi-information" />
+ </span>
+ )}
+ </label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p
+ class={
+ expand
+ ? "control is-expanded has-icons-right"
+ : "control has-icons-right"
+ }
+ >
+ {children}
+ {help}
+ </p>
+ {error && <p class="help is-danger">{error}</p>}
+ </div>
+ {side}
+ </div>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/form/useField.tsx b/packages/auditor-backoffice-ui/src/components/form/useField.tsx
new file mode 100644
index 000000000..c7559faae
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/useField.tsx
@@ -0,0 +1,92 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { ComponentChildren, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { useFormContext } from "./FormProvider.js";
+
+interface Use<V> {
+ error?: string;
+ required: boolean;
+ value: any;
+ initial: any;
+ onChange: (v: V) => void;
+ toStr: (f: V | undefined) => string;
+ fromStr: (v: string) => V;
+}
+
+export function useField<T>(name: keyof T): Use<T[typeof name]> {
+ const { errors, object, initialObject, toStr, fromStr, valueHandler } =
+ useFormContext<T>();
+ type P = typeof name;
+ type V = T[P];
+ const [isDirty, setDirty] = useState(false);
+ const updateField =
+ (field: P) =>
+ (value: V): void => {
+ setDirty(true);
+ return valueHandler((prev) => {
+ return setValueDeeper(prev, String(field).split("."), value);
+ });
+ };
+
+ const defaultToString = (f?: V): string => String(!f ? "" : f);
+ const defaultFromString = (v: string): V => v as any;
+ const value = readField(object, String(name));
+ const initial = readField(initialObject, String(name));
+ const hasError = readField(errors, String(name));
+ return {
+ error: isDirty ? hasError : undefined,
+ required: !isDirty && hasError,
+ value,
+ initial,
+ onChange: updateField(name) as any,
+ toStr: toStr[name] ? toStr[name]! : defaultToString,
+ fromStr: fromStr[name] ? fromStr[name]! : defaultFromString,
+ };
+}
+/**
+ * read the field of an object an support accessing it using '.'
+ *
+ * @param object
+ * @param name
+ * @returns
+ */
+const readField = (object: any, name: string) => {
+ return name
+ .split(".")
+ .reduce((prev, current) => prev && prev[current], object);
+};
+
+const setValueDeeper = (object: any, names: string[], value: any): any => {
+ if (names.length === 0) return value;
+ const [head, ...rest] = names;
+ return { ...object, [head]: setValueDeeper(object[head] || {}, rest, value) };
+};
+
+export interface InputProps<T> {
+ name: T;
+ label: ComponentChildren;
+ placeholder?: string;
+ tooltip?: ComponentChildren;
+ readonly?: boolean;
+ help?: ComponentChildren;
+}
diff --git a/packages/auditor-backoffice-ui/src/components/form/useGroupField.tsx b/packages/auditor-backoffice-ui/src/components/form/useGroupField.tsx
new file mode 100644
index 000000000..9a445eb32
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/useGroupField.tsx
@@ -0,0 +1,41 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useFormContext } from "./FormProvider.js";
+
+interface Use {
+ hasError?: boolean;
+}
+
+export function useGroupField<T>(name: keyof T): Use {
+ const f = useFormContext<T>();
+ if (!f) return {};
+
+ return {
+ hasError: readField(f.errors, String(name)),
+ };
+}
+
+const readField = (object: any, name: string) => {
+ return name
+ .split(".")
+ .reduce((prev, current) => prev && prev[current], object);
+};
diff --git a/packages/merchant-backend-ui/.storybook/.babelrc b/packages/auditor-backoffice-ui/src/components/index.stories.ts
index 610b6f339..c57ddab14 100644
--- a/packages/merchant-backend-ui/.storybook/.babelrc
+++ b/packages/auditor-backoffice-ui/src/components/index.stories.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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
@@ -14,12 +14,4 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- /**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-{
- "presets": [
- "preact-cli/babel"
- ]
-} \ No newline at end of file
+export * as payto from "./form/InputPaytoForm.stories.js";
diff --git a/packages/auditor-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx b/packages/auditor-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx
new file mode 100644
index 000000000..6f5881fc0
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx
@@ -0,0 +1,124 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useBackendContext } from "../../context/backend.js";
+import { Entity } from "../../paths/admin/create/CreatePage.js";
+import { Input } from "../form/Input.js";
+import { InputDuration } from "../form/InputDuration.js";
+import { InputGroup } from "../form/InputGroup.js";
+import { InputImage } from "../form/InputImage.js";
+import { InputLocation } from "../form/InputLocation.js";
+import { InputSelector } from "../form/InputSelector.js";
+import { InputToggle } from "../form/InputToggle.js";
+import { InputWithAddon } from "../form/InputWithAddon.js";
+
+export function DefaultInstanceFormFields({
+ readonlyId,
+ showId,
+}: {
+ readonlyId?: boolean;
+ showId: boolean;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const { url: backendURL } = useBackendContext()
+ return (
+ <Fragment>
+ {showId && (
+ <InputWithAddon<Entity>
+ name="id"
+ addonBefore={`${backendURL}/instances/`}
+ readonly={readonlyId}
+ label={i18n.str`Identifier`}
+ tooltip={i18n.str`Name of the instance in URLs. The 'default' instance is special in that it is used to administer other instances.`}
+ />
+ )}
+
+ <Input<Entity>
+ name="name"
+ label={i18n.str`Business name`}
+ tooltip={i18n.str`Legal name of the business represented by this instance.`}
+ />
+
+ <InputSelector<Entity>
+ name="user_type"
+ label={i18n.str`Type`}
+ tooltip={i18n.str`Different type of account can have different rules and requirements.`}
+ values={["business", "individual"]}
+ />
+
+ <Input<Entity>
+ name="email"
+ label={i18n.str`Email`}
+ tooltip={i18n.str`Contact email`}
+ />
+
+ <Input<Entity>
+ name="website"
+ label={i18n.str`Website URL`}
+ tooltip={i18n.str`URL.`}
+ />
+
+ <InputImage<Entity>
+ name="logo"
+ label={i18n.str`Logo`}
+ tooltip={i18n.str`Logo image.`}
+ />
+
+ <InputToggle<Entity>
+ name="use_stefan"
+ label={i18n.str`Pay transaction fee`}
+ tooltip={i18n.str`Assume the cost of the transaction of let the user pay for it.`}
+ />
+
+ <InputGroup
+ name="address"
+ label={i18n.str`Address`}
+ tooltip={i18n.str`Physical location of the merchant.`}
+ >
+ <InputLocation name="address" />
+ </InputGroup>
+
+ <InputGroup
+ name="jurisdiction"
+ label={i18n.str`Jurisdiction`}
+ tooltip={i18n.str`Jurisdiction for legal disputes with the merchant.`}
+ >
+ <InputLocation name="jurisdiction" />
+ </InputGroup>
+
+ <InputDuration<Entity>
+ name="default_pay_delay"
+ label={i18n.str`Default payment delay`}
+ withForever
+ tooltip={i18n.str`Time customers have to pay an order before the offer expires by default.`}
+ />
+
+ <InputDuration<Entity>
+ name="default_wire_transfer_delay"
+ label={i18n.str`Default wire transfer delay`}
+ tooltip={i18n.str`Maximum time an exchange is allowed to delay wiring funds to the merchant, enabling it to aggregate smaller payments into larger wire transfers and reducing wire fees.`}
+ withForever
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/demobank-ui/src/components/menu/LangSelector.tsx b/packages/auditor-backoffice-ui/src/components/menu/LangSelector.tsx
index 69d6ee64a..41fe1374a 100644
--- a/packages/demobank-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 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
@@ -19,11 +19,11 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { h, VNode, Fragment } from "preact";
-import { useCallback, useEffect, useState } from "preact/hooks";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
import langIcon from "../../assets/icons/languageicon.svg";
-import { useTranslationContext } from "../../context/translation";
-import { strings as messages } from "../../i18n/strings";
+import { strings as messages } from "../../i18n/strings.js";
type LangsNames = {
[P in keyof typeof messages]: string;
@@ -38,54 +38,43 @@ const names: LangsNames = {
it: "Italiano [it]",
};
-function getLangName(s: keyof LangsNames | string): string {
+function getLangName(s: keyof LangsNames | string) {
if (names[s]) return names[s];
- return String(s);
+ return s;
}
-// FIXME: explain "like py".
-export function LangSelectorLikePy(): VNode {
+export function LangSelector(): VNode {
const [updatingLang, setUpdatingLang] = useState(false);
const { lang, changeLanguage } = useTranslationContext();
- const [hidden, setHidden] = useState(true);
- useEffect(() => {
- function bodyKeyPress(event: KeyboardEvent) {
- if (event.code === "Escape") setHidden(true);
- }
- function bodyOnClick(event: Event) {
- setHidden(true);
- }
- document.body.addEventListener("click", bodyOnClick);
- document.body.addEventListener("keydown", bodyKeyPress as any);
- return () => {
- document.body.removeEventListener("keydown", bodyKeyPress as any);
- document.body.removeEventListener("click", bodyOnClick);
- };
- }, []);
+
return (
- <Fragment>
- <button
- name="language"
- onClick={(ev) => {
- setHidden((h) => !h);
- ev.stopPropagation();
- }}
- >
- {getLangName(lang)}
- </button>
- <div id="lang" class={hidden ? "hide" : ""}>
- <div style="position: relative; overflow: visible;">
- <div
- class="nav"
- style="position: absolute; max-height: 60vh; overflow-y: scroll"
- >
+ <div class="dropdown is-active ">
+ <div class="dropdown-trigger">
+ <button
+ class="button has-tooltip-left"
+ data-tooltip="change language selection"
+ aria-haspopup="true"
+ aria-controls="dropdown-menu"
+ onClick={() => setUpdatingLang(!updatingLang)}
+ >
+ <div class="icon is-small is-left">
+ <img src={langIcon} />
+ </div>
+ <span>{getLangName(lang)}</span>
+ <div class="icon is-right">
+ <i class="mdi mdi-chevron-down" />
+ </div>
+ </button>
+ </div>
+ {updatingLang && (
+ <div class="dropdown-menu" id="dropdown-menu" role="menu">
+ <div class="dropdown-content">
{Object.keys(messages)
.filter((l) => l !== lang)
.map((l) => (
<a
key={l}
- href="#"
- class="navbtn langbtn"
+ class="dropdown-item"
value={l}
onClick={() => {
changeLanguage(l);
@@ -95,10 +84,9 @@ export function LangSelectorLikePy(): VNode {
{getLangName(l)}
</a>
))}
- <br />
</div>
</div>
- </div>
- </Fragment>
+ )}
+ </div>
);
}
diff --git a/packages/demobank-ui/src/components/menu/NavigationBar.tsx b/packages/auditor-backoffice-ui/src/components/menu/NavigationBar.tsx
index d344875eb..9f1b33893 100644
--- a/packages/demobank-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 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
@@ -20,8 +20,7 @@
*/
import { h, VNode } from "preact";
-import logo from "../../assets/logo.jpeg";
-import { LangSelectorLikePy as LangSelector } from "./LangSelector";
+import logo from "../../assets/logo-2021.svg";
interface Props {
onMobileMenu: () => void;
@@ -39,12 +38,32 @@ export function NavigationBar({ onMobileMenu, title }: Props): VNode {
<span class="navbar-item" style={{ fontSize: 24, fontWeight: 900 }}>
{title}
</span>
+
+ <a
+ role="button"
+ class="navbar-burger"
+ aria-label="menu"
+ aria-expanded="false"
+ onClick={(e) => {
+ onMobileMenu();
+ e.stopPropagation();
+ }}
+ >
+ <span aria-hidden="true" />
+ <span aria-hidden="true" />
+ <span aria-hidden="true" />
+ </a>
</div>
<div class="navbar-menu ">
+ <a
+ class="navbar-start is-justify-content-center is-flex-grow-1"
+ href="https://taler.net"
+ >
+ <img src={logo} style={{ height: 35, margin: 10 }} />
+ </a>
<div class="navbar-end">
<div class="navbar-item" style={{ paddingTop: 4, paddingBottom: 4 }}>
- {/* <LangSelector /> */}
</div>
</div>
</div>
diff --git a/packages/auditor-backoffice-ui/src/components/menu/SideBar.tsx b/packages/auditor-backoffice-ui/src/components/menu/SideBar.tsx
new file mode 100644
index 000000000..cfc00148e
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/menu/SideBar.tsx
@@ -0,0 +1,284 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useBackendContext } from "../../context/backend.js";
+import { useConfigContext } from "../../context/config.js";
+import { useInstanceKYCDetails } from "../../hooks/instance.js";
+import { LangSelector } from "./LangSelector.js";
+
+const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined;
+const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined;
+
+interface Props {
+ onLogout: () => void;
+ onShowSettings: () => void;
+ mobile?: boolean;
+ instance: string;
+ admin?: boolean;
+ mimic?: boolean;
+ isPasswordOk: boolean;
+}
+
+export function Sidebar({
+ mobile,
+ instance,
+ onShowSettings,
+ onLogout,
+ admin,
+ mimic,
+ isPasswordOk
+}: Props): VNode {
+ const config = useConfigContext();
+ const { url: backendURL } = useBackendContext()
+ const { i18n } = useTranslationContext();
+ const kycStatus = useInstanceKYCDetails();
+ const needKYC = kycStatus.ok && kycStatus.data.type === "redirect";
+
+ return (
+ <aside class="aside is-placed-left is-expanded" style={{ overflowY: "scroll" }}>
+ {mobile && (
+ <div
+ class="footer"
+ onClick={(e) => {
+ return e.stopImmediatePropagation();
+ }}
+ >
+ <LangSelector />
+ </div>
+ )}
+ <div class="aside-tools">
+ <div class="aside-tools-label">
+ <div>
+ <b>Taler</b> Backoffice
+ </div>
+ <div
+ class="is-size-7 has-text-right"
+ style={{ lineHeight: 0, marginTop: -10 }}
+ >
+ {VERSION} ({config.version})
+ </div>
+ </div>
+ </div>
+ <div class="menu is-menu-main">
+ {instance ? (
+ <Fragment>
+ <ul class="menu-list">
+ <li>
+ <a href={"/orders"} class="has-icon">
+ <span class="icon">
+ <i class="mdi mdi-cash-register" />
+ </span>
+ <span class="menu-item-label">
+ <i18n.Translate>Orders</i18n.Translate>
+ </span>
+ </a>
+ </li>
+ <li>
+ <a href={"/inventory"} class="has-icon">
+ <span class="icon">
+ <i class="mdi mdi-shopping" />
+ </span>
+ <span class="menu-item-label">
+ <i18n.Translate>Inventory</i18n.Translate>
+ </span>
+ </a>
+ </li>
+ <li>
+ <a href={"/transfers"} class="has-icon">
+ <span class="icon">
+ <i class="mdi mdi-arrow-left-right" />
+ </span>
+ <span class="menu-item-label">
+ <i18n.Translate>Transfers</i18n.Translate>
+ </span>
+ </a>
+ </li>
+ <li>
+ <a href={"/templates"} class="has-icon">
+ <span class="icon">
+ <i class="mdi mdi-newspaper" />
+ </span>
+ <span class="menu-item-label">
+ <i18n.Translate>Templates</i18n.Translate>
+ </span>
+ </a>
+ </li>
+ {needKYC && (
+ <li>
+ <a href={"/kyc"} class="has-icon">
+ <span class="icon">
+ <i class="mdi mdi-account-check" />
+ </span>
+ <span class="menu-item-label">KYC Status</span>
+ </a>
+ </li>
+ )}
+ </ul>
+ <p class="menu-label">
+ <i18n.Translate>Configuration</i18n.Translate>
+ </p>
+ <ul class="menu-list">
+ <li>
+ <a href={"/bank"} class="has-icon">
+ <span class="icon">
+ <i class="mdi mdi-bank" />
+ </span>
+ <span class="menu-item-label">
+ <i18n.Translate>Bank account</i18n.Translate>
+ </span>
+ </a>
+ </li>
+ <li>
+ <a href={"/otp-devices"} class="has-icon">
+ <span class="icon">
+ <i class="mdi mdi-lock" />
+ </span>
+ <span class="menu-item-label">
+ <i18n.Translate>OTP Devices</i18n.Translate>
+ </span>
+ </a>
+ </li>
+ <li>
+ <a href={"/reserves"} class="has-icon">
+ <span class="icon">
+ <i class="mdi mdi-cash" />
+ </span>
+ <span class="menu-item-label">Reserves</span>
+ </a>
+ </li>
+ <li>
+ <a href={"/webhooks"} class="has-icon">
+ <span class="icon">
+ <i class="mdi mdi-newspaper" />
+ </span>
+ <span class="menu-item-label">
+ <i18n.Translate>Webhooks</i18n.Translate>
+ </span>
+ </a>
+ </li>
+ <li>
+ <a href={"/settings"} class="has-icon">
+ <span class="icon">
+ <i class="mdi mdi-square-edit-outline" />
+ </span>
+ <span class="menu-item-label">
+ <i18n.Translate>Settings</i18n.Translate>
+ </span>
+ </a>
+ </li>
+ <li>
+ <a href={"/token"} class="has-icon">
+ <span class="icon">
+ <i class="mdi mdi-security" />
+ </span>
+ <span class="menu-item-label">
+ <i18n.Translate>Access token</i18n.Translate>
+ </span>
+ </a>
+ </li>
+ </ul>
+ </Fragment>
+ ) : undefined}
+ <p class="menu-label">
+ <i18n.Translate>Connection</i18n.Translate>
+ </p>
+ <ul class="menu-list">
+ <li>
+ <a class="has-icon is-state-info is-hoverable"
+ onClick={(): void => onShowSettings()}
+ >
+ <span class="icon">
+ <i class="mdi mdi-newspaper" />
+ </span>
+ <span class="menu-item-label">
+ <i18n.Translate>Interface</i18n.Translate>
+ </span>
+ </a>
+ </li>
+ <li>
+ <div>
+ <span style={{ width: "3rem" }} class="icon">
+ <i class="mdi mdi-web" />
+ </span>
+ <span class="menu-item-label">
+ {new URL(backendURL).hostname}
+ </span>
+ </div>
+ </li>
+ <li>
+ <div>
+ <span style={{ width: "3rem" }} class="icon">
+ ID
+ </span>
+ <span class="menu-item-label">
+ {!instance ? "default" : instance}
+ </span>
+ </div>
+ </li>
+ {admin && !mimic && (
+ <Fragment>
+ <p class="menu-label">
+ <i18n.Translate>Instances</i18n.Translate>
+ </p>
+ <li>
+ <a href={"/instance/new"} class="has-icon">
+ <span class="icon">
+ <i class="mdi mdi-plus" />
+ </span>
+ <span class="menu-item-label">
+ <i18n.Translate>New</i18n.Translate>
+ </span>
+ </a>
+ </li>
+ <li>
+ <a href={"/instances"} class="has-icon">
+ <span class="icon">
+ <i class="mdi mdi-format-list-bulleted" />
+ </span>
+ <span class="menu-item-label">
+ <i18n.Translate>List</i18n.Translate>
+ </span>
+ </a>
+ </li>
+ </Fragment>
+ )}
+ {isPasswordOk ?
+ <li>
+ <a
+ class="has-icon is-state-info is-hoverable"
+ onClick={(): void => onLogout()}
+ >
+ <span class="icon">
+ <i class="mdi mdi-logout default" />
+ </span>
+ <span class="menu-item-label">
+ <i18n.Translate>Log out</i18n.Translate>
+ </span>
+ </a>
+ </li> : undefined
+ }
+ </ul>
+ </div>
+ </aside>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/menu/index.tsx b/packages/auditor-backoffice-ui/src/components/menu/index.tsx
new file mode 100644
index 000000000..015d3bd05
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/menu/index.tsx
@@ -0,0 +1,237 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { ComponentChildren, Fragment, h, VNode } from "preact";
+import { useEffect, useState } from "preact/hooks";
+import { AdminPaths } from "../../AdminRoutes.js";
+import { InstancePaths } from "../../InstanceRoutes.js";
+import { Notification } from "../../utils/types.js";
+import { NavigationBar } from "./NavigationBar.js";
+import { Sidebar } from "./SideBar.js";
+
+function getInstanceTitle(path: string, id: string): string {
+ switch (path) {
+ case InstancePaths.settings:
+ return `${id}: Settings`;
+ case InstancePaths.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 "";
+ }
+}
+
+function getAdminTitle(path: string, instance: string) {
+ if (path === AdminPaths.new_instance) return `New instance`;
+ if (path === AdminPaths.list_instances) return `Instances`;
+ return getInstanceTitle(path, instance);
+}
+
+interface MenuProps {
+ title?: string;
+ path: string;
+ instance: string;
+ admin?: boolean;
+ onLogout?: () => void;
+ onShowSettings: () => void;
+ setInstanceName: (s: string) => void;
+ isPasswordOk: boolean;
+}
+
+function WithTitle({
+ title,
+ children,
+}: {
+ title: string;
+ children: ComponentChildren;
+}): VNode {
+ useEffect(() => {
+ document.title = `Taler Backoffice: ${title}`;
+ }, [title]);
+ return <Fragment>{children}</Fragment>;
+}
+
+export function Menu({
+ onLogout,
+ onShowSettings,
+ title,
+ instance,
+ path,
+ admin,
+ setInstanceName,
+ isPasswordOk
+}: MenuProps): VNode {
+ const [mobileOpen, setMobileOpen] = useState(false);
+
+ const titleWithSubtitle = title
+ ? title
+ : !admin
+ ? getInstanceTitle(path, instance)
+ : getAdminTitle(path, instance);
+ const adminInstance = instance === "default";
+ const mimic = admin && !adminInstance;
+ return (
+ <WithTitle title={titleWithSubtitle}>
+ <div
+ class={mobileOpen ? "has-aside-mobile-expanded" : ""}
+ onClick={() => setMobileOpen(false)}
+ >
+ <NavigationBar
+ onMobileMenu={() => setMobileOpen(!mobileOpen)}
+ title={titleWithSubtitle}
+ />
+
+ {onLogout && (
+ <Sidebar
+ onShowSettings={onShowSettings}
+ onLogout={onLogout}
+ admin={admin}
+ mimic={mimic}
+ instance={instance}
+ mobile={mobileOpen}
+ isPasswordOk={isPasswordOk}
+ />
+ )}
+
+ {mimic && (
+ <nav class="level" style={{
+ zIndex: 100,
+ position: "fixed",
+ width: "50%",
+ marginLeft: "20%"
+ }}>
+ <div class="level-item has-text-centered has-background-warning">
+ <p class="is-size-5">
+ You are viewing the instance <b>&quot;{instance}&quot;</b>.{" "}
+ <a
+ href="#/instances"
+ onClick={(e) => {
+ setInstanceName("default");
+ }}
+ >
+ go back
+ </a>
+ </p>
+ </div>
+ </nav>
+ )}
+ </div>
+ </WithTitle>
+ );
+}
+
+interface NotYetReadyAppMenuProps {
+ title: string;
+ onShowSettings: () => void;
+ onLogout?: () => void;
+ isPasswordOk: boolean;
+}
+
+interface NotifProps {
+ notification?: Notification;
+}
+export function NotificationCard({
+ notification: n,
+}: NotifProps): VNode | null {
+ if (!n) return null;
+ return (
+ <div class="notification">
+ <div class="columns is-vcentered">
+ <div class="column is-12">
+ <article
+ class={
+ n.type === "ERROR"
+ ? "message is-danger"
+ : n.type === "WARN"
+ ? "message is-warning"
+ : "message is-info"
+ }
+ >
+ <div class="message-header">
+ <p>{n.message}</p>
+ </div>
+ {n.description && (
+ <div class="message-body">
+ <div>{n.description}</div>
+ {n.details && <pre>{n.details}</pre>}
+ </div>
+ )}
+ </article>
+ </div>
+ </div>
+ </div>
+ );
+}
+
+interface NotConnectedAppMenuProps {
+ title: string;
+}
+export function NotConnectedAppMenu({
+ title,
+}: NotConnectedAppMenuProps): VNode {
+ const [mobileOpen, setMobileOpen] = useState(false);
+
+ useEffect(() => {
+ document.title = `Taler Backoffice: ${title}`;
+ }, [title]);
+
+ return (
+ <div
+ class={mobileOpen ? "has-aside-mobile-expanded" : ""}
+ onClick={() => setMobileOpen(false)}
+ >
+ <NavigationBar
+ onMobileMenu={() => setMobileOpen(!mobileOpen)}
+ title={title}
+ />
+ </div>
+ );
+}
+
+export function NotYetReadyAppMenu({
+ onLogout,
+ onShowSettings,
+ title,
+ isPasswordOk
+}: NotYetReadyAppMenuProps): VNode {
+ const [mobileOpen, setMobileOpen] = useState(false);
+
+ useEffect(() => {
+ document.title = `Taler Backoffice: ${title}`;
+ }, [title]);
+
+ return (
+ <div
+ class={mobileOpen ? "has-aside-mobile-expanded" : ""}
+ onClick={() => setMobileOpen(false)}
+ >
+ <NavigationBar
+ onMobileMenu={() => setMobileOpen(!mobileOpen)}
+ title={title}
+ />
+ {onLogout && (
+ <Sidebar onShowSettings={onShowSettings} onLogout={onLogout} instance="" mobile={mobileOpen} isPasswordOk={isPasswordOk} />
+ )}
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/modal/index.tsx b/packages/auditor-backoffice-ui/src/components/modal/index.tsx
new file mode 100644
index 000000000..8372c84cc
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/modal/index.tsx
@@ -0,0 +1,496 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { ComponentChildren, Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { useInstanceContext } from "../../context/instance.js";
+import { DEFAULT_REQUEST_TIMEOUT } from "../../utils/constants.js";
+import { Spinner } from "../exception/loading.js";
+import { FormProvider } from "../form/FormProvider.js";
+import { Input } from "../form/Input.js";
+
+interface Props {
+ active?: boolean;
+ description?: string;
+ onCancel?: () => void;
+ onConfirm?: () => void;
+ label?: string;
+ children?: ComponentChildren;
+ danger?: boolean;
+ disabled?: boolean;
+}
+
+export function ConfirmModal({
+ active,
+ description,
+ onCancel,
+ onConfirm,
+ children,
+ danger,
+ disabled,
+ label = "Confirm",
+}: Props): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class={active ? "modal is-active" : "modal"}>
+ <div class="modal-background " onClick={onCancel} />
+ <div class="modal-card" style={{ maxWidth: 700 }}>
+ <header class="modal-card-head">
+ {!description ? null : (
+ <p class="modal-card-title">
+ <b>{description}</b>
+ </p>
+ )}
+ <button class="delete " aria-label="close" onClick={onCancel} />
+ </header>
+ <section class="modal-card-body">{children}</section>
+ <footer class="modal-card-foot">
+ <div class="buttons is-right" style={{ width: "100%" }}>
+ {onConfirm ? (
+ <Fragment>
+ <button class="button " onClick={onCancel}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+
+ <button
+ class={danger ? "button is-danger " : "button is-info "}
+ disabled={disabled}
+ onClick={onConfirm}
+ >
+ <i18n.Translate>{label}</i18n.Translate>
+ </button>
+ </Fragment>
+ ) : (
+ <button class="button " onClick={onCancel}>
+ <i18n.Translate>Close</i18n.Translate>
+ </button>
+ )}
+ </div>
+ </footer>
+ </div>
+ <button
+ class="modal-close is-large "
+ aria-label="close"
+ onClick={onCancel}
+ />
+ </div>
+ );
+}
+
+export function ContinueModal({
+ active,
+ description,
+ onCancel,
+ onConfirm,
+ children,
+ disabled,
+}: Props): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class={active ? "modal is-active" : "modal"}>
+ <div class="modal-background " onClick={onCancel} />
+ <div class="modal-card">
+ <header class="modal-card-head has-background-success">
+ {!description ? null : <p class="modal-card-title">{description}</p>}
+ <button class="delete " aria-label="close" onClick={onCancel} />
+ </header>
+ <section class="modal-card-body">{children}</section>
+ <footer class="modal-card-foot">
+ <div class="buttons is-right" style={{ width: "100%" }}>
+ <button
+ class="button is-success "
+ disabled={disabled}
+ onClick={onConfirm}
+ >
+ <i18n.Translate>Continue</i18n.Translate>
+ </button>
+ </div>
+ </footer>
+ </div>
+ <button
+ class="modal-close is-large "
+ aria-label="close"
+ onClick={onCancel}
+ />
+ </div>
+ );
+}
+
+export function SimpleModal({ onCancel, children }: any): VNode {
+ return (
+ <div class="modal is-active">
+ <div class="modal-background " onClick={onCancel} />
+ <div class="modal-card">
+ <section class="modal-card-body is-main-section">{children}</section>
+ </div>
+ <button
+ class="modal-close is-large "
+ aria-label="close"
+ onClick={onCancel}
+ />
+ </div>
+ );
+}
+
+export function ClearConfirmModal({
+ description,
+ onCancel,
+ onClear,
+ onConfirm,
+ children,
+}: Props & { onClear?: () => void }): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="modal is-active">
+ <div class="modal-background " onClick={onCancel} />
+ <div class="modal-card">
+ <header class="modal-card-head">
+ {!description ? null : <p class="modal-card-title">{description}</p>}
+ <button class="delete " aria-label="close" onClick={onCancel} />
+ </header>
+ <section class="modal-card-body is-main-section">{children}</section>
+ <footer class="modal-card-foot">
+ {onClear && (
+ <button
+ class="button is-danger"
+ onClick={onClear}
+ disabled={onClear === undefined}
+ >
+ <i18n.Translate>Clear</i18n.Translate>
+ </button>
+ )}
+ <div class="buttons is-right" style={{ width: "100%" }}>
+ <button class="button " onClick={onCancel}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ <button
+ class="button is-info"
+ onClick={onConfirm}
+ disabled={onConfirm === undefined}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </button>
+ </div>
+ </footer>
+ </div>
+ <button
+ class="modal-close is-large "
+ aria-label="close"
+ onClick={onCancel}
+ />
+ </div>
+ );
+}
+
+interface DeleteModalProps {
+ element: { id: string; name: string };
+ onCancel: () => void;
+ onConfirm: (id: string) => void;
+}
+
+export function DeleteModal({
+ element,
+ onCancel,
+ onConfirm,
+}: DeleteModalProps): VNode {
+ return (
+ <ConfirmModal
+ label={`Delete instance`}
+ description={`Delete the instance "${element.name}"`}
+ danger
+ active
+ onCancel={onCancel}
+ onConfirm={() => onConfirm(element.id)}
+ >
+ <p>
+ If you delete the instance named <b>&quot;{element.name}&quot;</b> (ID:{" "}
+ <b>{element.id}</b>), the merchant will no longer be able to process
+ orders or refunds
+ </p>
+ <p>
+ This action deletes the instance private key, but preserves all
+ transaction data. You can still access that data after deleting the
+ instance.
+ </p>
+ <p class="warning">
+ Deleting an instance <b>cannot be undone</b>.
+ </p>
+ </ConfirmModal>
+ );
+}
+
+export function PurgeModal({
+ element,
+ onCancel,
+ onConfirm,
+}: DeleteModalProps): VNode {
+ return (
+ <ConfirmModal
+ label={`Purge the instance`}
+ description={`Purge the instance "${element.name}"`}
+ danger
+ active
+ onCancel={onCancel}
+ onConfirm={() => onConfirm(element.id)}
+ >
+ <p>
+ If you purge the instance named <b>&quot;{element.name}&quot;</b> (ID:{" "}
+ <b>{element.id}</b>), you will also delete all it&apos;s transaction
+ data.
+ </p>
+ <p>
+ The instance will disappear from your list, and you will no longer be
+ able to access it&apos;s data.
+ </p>
+ <p class="warning">
+ Purging an instance <b>cannot be undone</b>.
+ </p>
+ </ConfirmModal>
+ );
+}
+
+interface UpdateTokenModalProps {
+ oldToken?: string;
+ onCancel: () => void;
+ onConfirm: (value: string) => void;
+ onClear: () => void;
+}
+
+//FIXME: merge UpdateTokenModal with SetTokenNewInstanceModal
+export function UpdateTokenModal({
+ onCancel,
+ onClear,
+ onConfirm,
+ oldToken,
+}: UpdateTokenModalProps): VNode {
+ type State = { old_token: string; new_token: string; repeat_token: string };
+ const [form, setValue] = useState<Partial<State>>({
+ old_token: "",
+ new_token: "",
+ repeat_token: "",
+ });
+ const { i18n } = useTranslationContext();
+
+ const hasInputTheCorrectOldToken = oldToken && oldToken !== form.old_token;
+ const errors = {
+ old_token: hasInputTheCorrectOldToken
+ ? i18n.str`is not the same as the current access token`
+ : undefined,
+ new_token: !form.new_token
+ ? i18n.str`cannot be empty`
+ : form.new_token === form.old_token
+ ? i18n.str`cannot be the same as the old token`
+ : undefined,
+ repeat_token:
+ form.new_token !== form.repeat_token
+ ? i18n.str`is not the same`
+ : undefined,
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const instance = useInstanceContext();
+
+ const text = i18n.str`You are updating the access token from instance with id ${instance.id}`;
+
+ return (
+ <ClearConfirmModal
+ description={text}
+ onCancel={onCancel}
+ onConfirm={!hasErrors ? () => onConfirm(form.new_token!) : undefined}
+ onClear={!hasInputTheCorrectOldToken && oldToken ? onClear : undefined}
+ >
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <FormProvider errors={errors} object={form} valueHandler={setValue}>
+ {oldToken && (
+ <Input<State>
+ name="old_token"
+ label={i18n.str`Old access token`}
+ tooltip={i18n.str`access token currently in use`}
+ inputType="password"
+ />
+ )}
+ <Input<State>
+ name="new_token"
+ label={i18n.str`New access token`}
+ tooltip={i18n.str`next access token to be used`}
+ inputType="password"
+ />
+ <Input<State>
+ name="repeat_token"
+ label={i18n.str`Repeat access token`}
+ tooltip={i18n.str`confirm the same access token`}
+ inputType="password"
+ />
+ </FormProvider>
+ <p>
+ <i18n.Translate>
+ Clearing the access token will mean public access to the instance
+ </i18n.Translate>
+ </p>
+ </div>
+ <div class="column" />
+ </div>
+ </ClearConfirmModal>
+ );
+}
+
+export function SetTokenNewInstanceModal({
+ onCancel,
+ onClear,
+ onConfirm,
+}: UpdateTokenModalProps): VNode {
+ type State = { old_token: string; new_token: string; repeat_token: string };
+ const [form, setValue] = useState<Partial<State>>({
+ new_token: "",
+ repeat_token: "",
+ });
+ const { i18n } = useTranslationContext();
+
+ const errors = {
+ new_token: !form.new_token
+ ? i18n.str`cannot be empty`
+ : form.new_token === form.old_token
+ ? i18n.str`cannot be the same as the old access token`
+ : undefined,
+ repeat_token:
+ form.new_token !== form.repeat_token
+ ? i18n.str`is not the same`
+ : undefined,
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ return (
+ <div class="modal is-active">
+ <div class="modal-background " onClick={onCancel} />
+ <div class="modal-card">
+ <header class="modal-card-head">
+ <p class="modal-card-title">{i18n.str`You are setting the access token for the new instance`}</p>
+ <button class="delete " aria-label="close" onClick={onCancel} />
+ </header>
+ <section class="modal-card-body is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <FormProvider
+ errors={errors}
+ object={form}
+ valueHandler={setValue}
+ >
+ <Input<State>
+ name="new_token"
+ label={i18n.str`New access token`}
+ tooltip={i18n.str`next access token to be used`}
+ inputType="password"
+ />
+ <Input<State>
+ name="repeat_token"
+ label={i18n.str`Repeat access token`}
+ tooltip={i18n.str`confirm the same access token`}
+ inputType="password"
+ />
+ </FormProvider>
+ <p>
+ <i18n.Translate>
+ With external authorization method no check will be done by
+ the merchant backend
+ </i18n.Translate>
+ </p>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ <footer class="modal-card-foot">
+ {onClear && (
+ <button
+ class="button is-danger"
+ onClick={onClear}
+ disabled={onClear === undefined}
+ >
+ <i18n.Translate>Set external authorization</i18n.Translate>
+ </button>
+ )}
+ <div class="buttons is-right" style={{ width: "100%" }}>
+ <button class="button " onClick={onCancel}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ <button
+ class="button is-info"
+ onClick={() => onConfirm(form.new_token!)}
+ disabled={hasErrors}
+ >
+ <i18n.Translate>Set access token</i18n.Translate>
+ </button>
+ </div>
+ </footer>
+ </div>
+ <button
+ class="modal-close is-large "
+ aria-label="close"
+ onClick={onCancel}
+ />
+ </div>
+ );
+}
+
+export function LoadingModal({ onCancel }: { onCancel: () => void }): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="modal is-active">
+ <div class="modal-background " onClick={onCancel} />
+ <div class="modal-card">
+ <header class="modal-card-head">
+ <p class="modal-card-title">
+ <i18n.Translate>Operation in progress...</i18n.Translate>
+ </p>
+ </header>
+ <section class="modal-card-body">
+ <div class="columns">
+ <div class="column" />
+ <Spinner />
+ <div class="column" />
+ </div>
+ <p>{i18n.str`The operation will be automatically canceled after ${DEFAULT_REQUEST_TIMEOUT} seconds`}</p>
+ </section>
+ <footer class="modal-card-foot">
+ <div class="buttons is-right" style={{ width: "100%" }}>
+ <button class="button " onClick={onCancel}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ </div>
+ </footer>
+ </div>
+ <button
+ class="modal-close is-large "
+ aria-label="close"
+ onClick={onCancel}
+ />
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/notifications/CreatedSuccessfully.tsx b/packages/auditor-backoffice-ui/src/components/notifications/CreatedSuccessfully.tsx
new file mode 100644
index 000000000..073382fb1
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/notifications/CreatedSuccessfully.tsx
@@ -0,0 +1,57 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { ComponentChildren, h, VNode } from "preact";
+
+interface Props {
+ onCreateAnother?: () => void;
+ onConfirm: () => void;
+ children: ComponentChildren;
+}
+
+export function CreatedSuccessfully({
+ children,
+ onConfirm,
+ onCreateAnother,
+}: Props): VNode {
+ return (
+ <div class="columns is-fullwidth is-vcentered mt-3">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <div class="card">
+ <header class="card-header has-background-success">
+ <p class="card-header-title has-text-white-ter">Success.</p>
+ </header>
+ <div class="card-content">{children}</div>
+ </div>
+ <div class="buttons is-right">
+ {onCreateAnother && (
+ <button class="button is-info" onClick={onCreateAnother}>
+ Create another
+ </button>
+ )}
+ <button class="button is-info" onClick={onConfirm}>
+ Continue
+ </button>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/notifications/Notifications.stories.tsx b/packages/auditor-backoffice-ui/src/components/notifications/Notifications.stories.tsx
new file mode 100644
index 000000000..af594de0f
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/notifications/Notifications.stories.tsx
@@ -0,0 +1,62 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h } from "preact";
+import { Notifications } from "./index.js";
+
+export default {
+ title: "Components/Notification",
+ component: Notifications,
+ argTypes: {
+ removeNotification: { action: "removeNotification" },
+ },
+};
+
+export const Info = (a: any) => <Notifications {...a} />;
+Info.args = {
+ notifications: [
+ {
+ message: "Title",
+ description: "Some large description",
+ type: "INFO",
+ },
+ ],
+};
+export const Warn = (a: any) => <Notifications {...a} />;
+Warn.args = {
+ notifications: [
+ {
+ message: "Title",
+ description: "Some large description",
+ type: "WARN",
+ },
+ ],
+};
+export const Error = (a: any) => <Notifications {...a} />;
+Error.args = {
+ notifications: [
+ {
+ message: "Title",
+ description: "Some large description",
+ type: "ERROR",
+ },
+ ],
+};
diff --git a/packages/demobank-ui/src/components/Notifications.tsx b/packages/auditor-backoffice-ui/src/components/notifications/index.tsx
index e34550386..235c75577 100644
--- a/packages/demobank-ui/src/components/Notifications.tsx
+++ b/packages/auditor-backoffice-ui/src/components/notifications/index.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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
@@ -20,14 +20,7 @@
*/
import { h, VNode } from "preact";
-
-export interface Notification {
- message: string;
- description?: string | VNode;
- type: MessageType;
-}
-
-export type MessageType = "INFO" | "WARN" | "ERROR" | "SUCCESS";
+import { MessageType, Notification } from "../../utils/types.js";
interface Props {
notifications: Notification[];
@@ -54,17 +47,15 @@ export function Notifications({
removeNotification,
}: Props): VNode {
return (
- <div class="block">
+ <div class="toast">
{notifications.map((n, i) => (
<article key={i} class={messageStyle(n.type)}>
<div class="message-header">
<p>{n.message}</p>
- {removeNotification && (
- <button
- class="delete"
- onClick={() => removeNotification && removeNotification(n)}
- />
- )}
+ <button
+ class="delete"
+ onClick={() => removeNotification && removeNotification(n)}
+ />
</div>
{n.description && <div class="message-body">{n.description}</div>}
</article>
diff --git a/packages/demobank-ui/src/components/picker/DatePicker.tsx b/packages/auditor-backoffice-ui/src/components/picker/DatePicker.tsx
index ba53578ef..0bc629d46 100644
--- a/packages/demobank-ui/src/components/picker/DatePicker.tsx
+++ b/packages/auditor-backoffice-ui/src/components/picker/DatePicker.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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
@@ -24,8 +24,6 @@ import { h, Component } from "preact";
interface Props {
closeFunction?: () => void;
dateReceiver?: (d: Date) => void;
- initialDate?: Date;
- years?: Array<number>;
opened?: boolean;
}
interface State {
@@ -34,41 +32,6 @@ interface State {
selectYearMode: boolean;
currentDate: Date;
}
-const now = new Date();
-
-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 yearArr: number[] = [];
// inspired by https://codepen.io/m4r1vs/pen/MOOxyE
export class DatePicker extends Component<Props, State> {
@@ -131,38 +94,41 @@ export class DatePicker extends Component<Props, State> {
* Display previous month by updating state
*/
displayPrevMonth() {
- if (this.state.displayedMonth <= 0)
+ if (this.state.displayedMonth <= 0) {
this.setState({
displayedMonth: 11,
displayedYear: this.state.displayedYear - 1,
});
- else
+ } else {
this.setState({
displayedMonth: this.state.displayedMonth - 1,
});
+ }
}
/**
* Display next month by updating state
*/
displayNextMonth() {
- if (this.state.displayedMonth >= 11)
+ if (this.state.displayedMonth >= 11) {
this.setState({
displayedMonth: 0,
displayedYear: this.state.displayedYear + 1,
});
- else
+ } 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.selectYearMode) {
+ this.toggleYearSelector();
+ } else {
if (!this.state.currentDate) return false;
this.setState({
displayedMonth: this.state.currentDate.getMonth(),
@@ -197,13 +163,13 @@ export class DatePicker extends Component<Props, State> {
}
componentDidUpdate() {
- // if (this.state.selectYearMode) {
- // document.getElementsByClassName('selected')[0].scrollIntoView(); // works in every browser incl. IE, replace with scrollIntoViewIfNeeded when browsers support it
- // }
+ if (this.state.selectYearMode) {
+ document.getElementsByClassName("selected")[0].scrollIntoView(); // works in every browser incl. IE, replace with scrollIntoViewIfNeeded when browsers support it
+ }
}
- constructor(props: any) {
- super(props);
+ constructor() {
+ super();
this.closeDatePicker = this.closeDatePicker.bind(this);
this.dayClicked = this.dayClicked.bind(this);
@@ -215,12 +181,10 @@ export class DatePicker extends Component<Props, State> {
this.toggleYearSelector = this.toggleYearSelector.bind(this);
this.displaySelectedMonth = this.displaySelectedMonth.bind(this);
- const initial = props.initialDate || now;
-
this.state = {
- currentDate: initial,
- displayedMonth: initial.getMonth(),
- displayedYear: initial.getFullYear(),
+ currentDate: now,
+ displayedMonth: now.getMonth(),
+ displayedYear: now.getFullYear(),
selectYearMode: false,
};
}
@@ -318,7 +282,7 @@ export class DatePicker extends Component<Props, State> {
{selectYearMode && (
<div class="datePicker--selectYear">
- {(this.props.years || yearArr).map((year) => (
+ {yearArr.map((year) => (
<span
key={year}
class={year === displayedYear ? "selected" : ""}
@@ -344,4 +308,42 @@ export class DatePicker extends Component<Props, State> {
}
}
-for (let i = 2010; i <= now.getFullYear() + 10; i++) yearArr.push(i);
+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/demobank-ui/src/components/picker/DurationPicker.stories.tsx b/packages/auditor-backoffice-ui/src/components/picker/DurationPicker.stories.tsx
index 7f96cc15b..8f74d55ac 100644
--- a/packages/demobank-ui/src/components/picker/DurationPicker.stories.tsx
+++ b/packages/auditor-backoffice-ui/src/components/picker/DurationPicker.stories.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,7 +21,7 @@
import { h, FunctionalComponent } from "preact";
import { useState } from "preact/hooks";
-import { DurationPicker as TestedComponent } from "./DurationPicker";
+import { DurationPicker as TestedComponent } from "./DurationPicker.js";
export default {
title: "Components/Picker/Duration",
diff --git a/packages/demobank-ui/src/components/picker/DurationPicker.tsx b/packages/auditor-backoffice-ui/src/components/picker/DurationPicker.tsx
index b8a7671c3..ba003cce5 100644
--- a/packages/demobank-ui/src/components/picker/DurationPicker.tsx
+++ b/packages/auditor-backoffice-ui/src/components/picker/DurationPicker.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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
@@ -19,9 +19,9 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
-import { useTranslationContext } from "../../context/translation.js";
import "../../scss/DurationPicker.scss";
export interface Props {
@@ -204,7 +204,8 @@ function DurationColumn({
}
function toTwoDigitString(n: number) {
- if (n < 10) return `0${n}`;
-
+ if (n < 10) {
+ return `0${n}`;
+ }
return `${n}`;
}
diff --git a/packages/auditor-backoffice-ui/src/components/product/InventoryProductForm.stories.tsx b/packages/auditor-backoffice-ui/src/components/product/InventoryProductForm.stories.tsx
new file mode 100644
index 000000000..2d5a54cde
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/product/InventoryProductForm.stories.tsx
@@ -0,0 +1,62 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { InventoryProductForm as TestedComponent } from "./InventoryProductForm.js";
+
+export default {
+ title: "Components/Product/Add",
+ component: TestedComponent,
+ argTypes: {
+ onAddProduct: { action: "onAddProduct" },
+ },
+};
+
+function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => <Component {...args} />;
+ r.args = props;
+ return r;
+}
+
+export const WithASimpleList = createExample(TestedComponent, {
+ inventory: [
+ {
+ id: "this id",
+ description: "this is the description",
+ } as any,
+ ],
+});
+
+export const WithAProductSelected = createExample(TestedComponent, {
+ inventory: [],
+ currentProducts: {
+ thisid: {
+ quantity: 1,
+ product: {
+ id: "asd",
+ description: "asdsadsad",
+ } as any,
+ },
+ },
+});
diff --git a/packages/auditor-backoffice-ui/src/components/product/InventoryProductForm.tsx b/packages/auditor-backoffice-ui/src/components/product/InventoryProductForm.tsx
new file mode 100644
index 000000000..377d9c1ba
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/product/InventoryProductForm.tsx
@@ -0,0 +1,127 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { MerchantBackend, WithId } from "../../declaration.js";
+import { ProductMap } from "../../paths/instance/orders/create/CreatePage.js";
+import { FormErrors, FormProvider } from "../form/FormProvider.js";
+import { InputNumber } from "../form/InputNumber.js";
+import { InputSearchOnList } from "../form/InputSearchOnList.js";
+
+type Form = {
+ product: MerchantBackend.Products.ProductDetail & WithId;
+ quantity: number;
+};
+
+interface Props {
+ currentProducts: ProductMap;
+ onAddProduct: (
+ product: MerchantBackend.Products.ProductDetail & WithId,
+ quantity: number,
+ ) => void;
+ inventory: (MerchantBackend.Products.ProductDetail & WithId)[];
+}
+
+export function InventoryProductForm({
+ currentProducts,
+ onAddProduct,
+ inventory,
+}: Props): VNode {
+ const initialState = { quantity: 1 };
+ const [state, setState] = useState<Partial<Form>>(initialState);
+ const [errors, setErrors] = useState<FormErrors<Form>>({});
+
+ const { i18n } = useTranslationContext();
+
+ const productWithInfiniteStock =
+ state.product && state.product.total_stock === -1;
+
+ const submit = (): void => {
+ if (!state.product) {
+ setErrors({
+ product: i18n.str`You must enter a valid product identifier.`,
+ });
+ return;
+ }
+ if (productWithInfiniteStock) {
+ onAddProduct(state.product, 1);
+ } else {
+ if (!state.quantity || state.quantity <= 0) {
+ setErrors({ quantity: i18n.str`Quantity must be greater than 0!` });
+ return;
+ }
+ const currentStock =
+ state.product.total_stock -
+ state.product.total_lost -
+ state.product.total_sold;
+ const p = currentProducts[state.product.id];
+ if (p) {
+ if (state.quantity + p.quantity > currentStock) {
+ const left = currentStock - p.quantity;
+ setErrors({
+ quantity: i18n.str`This quantity exceeds remaining stock. Currently, only ${left} units remain unreserved in stock.`,
+ });
+ return;
+ }
+ onAddProduct(state.product, state.quantity + p.quantity);
+ } else {
+ if (state.quantity > currentStock) {
+ const left = currentStock;
+ setErrors({
+ quantity: i18n.str`This quantity exceeds remaining stock. Currently, only ${left} units remain unreserved in stock.`,
+ });
+ return;
+ }
+ onAddProduct(state.product, state.quantity);
+ }
+ }
+
+ setState(initialState);
+ };
+
+ return (
+ <FormProvider<Form> errors={errors} object={state} valueHandler={setState}>
+ <InputSearchOnList
+ label={i18n.str`Search product`}
+ selected={state.product}
+ onChange={(p) => setState((v) => ({ ...v, product: p }))}
+ list={inventory}
+ withImage
+ />
+ {state.product && (
+ <div class="columns mt-5">
+ <div class="column is-two-thirds">
+ {!productWithInfiniteStock && (
+ <InputNumber<Form>
+ name="quantity"
+ label={i18n.str`Quantity`}
+ tooltip={i18n.str`how many products will be added`}
+ />
+ )}
+ </div>
+ <div class="column">
+ <div class="buttons is-right">
+ <button class="button is-success" onClick={submit}>
+ <i18n.Translate>Add from inventory</i18n.Translate>
+ </button>
+ </div>
+ </div>
+ </div>
+ )}
+ </FormProvider>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/product/NonInventoryProductForm.tsx b/packages/auditor-backoffice-ui/src/components/product/NonInventoryProductForm.tsx
new file mode 100644
index 000000000..c6d280f94
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/product/NonInventoryProductForm.tsx
@@ -0,0 +1,215 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useCallback, useEffect, useState } from "preact/hooks";
+import * as yup from "yup";
+import { MerchantBackend } from "../../declaration.js";
+import { useListener } from "../../hooks/listener.js";
+import { NonInventoryProductSchema as schema } from "../../schemas/index.js";
+import { FormErrors, FormProvider } from "../form/FormProvider.js";
+import { Input } from "../form/Input.js";
+import { InputCurrency } from "../form/InputCurrency.js";
+import { InputImage } from "../form/InputImage.js";
+import { InputNumber } from "../form/InputNumber.js";
+import { InputTaxes } from "../form/InputTaxes.js";
+
+type Entity = MerchantBackend.Product;
+
+interface Props {
+ onAddProduct: (p: Entity) => Promise<void>;
+ productToEdit?: Entity;
+}
+export function NonInventoryProductFrom({
+ productToEdit,
+ onAddProduct,
+}: Props): VNode {
+ const [showCreateProduct, setShowCreateProduct] = useState(false);
+
+ const isEditing = !!productToEdit;
+
+ useEffect(() => {
+ setShowCreateProduct(isEditing);
+ }, [isEditing]);
+
+ const [submitForm, addFormSubmitter] = useListener<
+ Partial<MerchantBackend.Product> | undefined
+ >((result) => {
+ if (result) {
+ setShowCreateProduct(false);
+ return onAddProduct({
+ quantity: result.quantity || 0,
+ taxes: result.taxes || [],
+ description: result.description || "",
+ image: result.image || "",
+ price: result.price || "",
+ unit: result.unit || "",
+ });
+ }
+ return Promise.resolve();
+ });
+
+ const { i18n } = useTranslationContext();
+
+ return (
+ <Fragment>
+ <div class="buttons">
+ <button
+ class="button is-success"
+ data-tooltip={i18n.str`describe and add a product that is not in the inventory list`}
+ onClick={() => setShowCreateProduct(true)}
+ >
+ <i18n.Translate>Add custom product</i18n.Translate>
+ </button>
+ </div>
+ {showCreateProduct && (
+ <div class="modal is-active">
+ <div
+ class="modal-background "
+ onClick={() => setShowCreateProduct(false)}
+ />
+ <div class="modal-card">
+ <header class="modal-card-head">
+ <p class="modal-card-title">{i18n.str`Complete information of the product`}</p>
+ <button
+ class="delete "
+ aria-label="close"
+ onClick={() => setShowCreateProduct(false)}
+ />
+ </header>
+ <section class="modal-card-body">
+ <ProductForm
+ initial={productToEdit}
+ onSubscribe={addFormSubmitter}
+ />
+ </section>
+ <footer class="modal-card-foot">
+ <div class="buttons is-right" style={{ width: "100%" }}>
+ <button
+ class="button "
+ onClick={() => setShowCreateProduct(false)}
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ <button
+ class="button is-info "
+ disabled={!submitForm}
+ onClick={submitForm}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </button>
+ </div>
+ </footer>
+ </div>
+ <button
+ class="modal-close is-large "
+ aria-label="close"
+ onClick={() => setShowCreateProduct(false)}
+ />
+ </div>
+ )}
+ </Fragment>
+ );
+}
+
+interface ProductProps {
+ onSubscribe: (c?: () => Entity | undefined) => void;
+ initial?: Partial<Entity>;
+}
+
+interface NonInventoryProduct {
+ quantity: number;
+ description: string;
+ unit: string;
+ price: string;
+ image: string;
+ taxes: MerchantBackend.Tax[];
+}
+
+export function ProductForm({ onSubscribe, initial }: ProductProps): VNode {
+ const [value, valueHandler] = useState<Partial<NonInventoryProduct>>({
+ taxes: [],
+ ...initial,
+ });
+ let errors: FormErrors<Entity> = {};
+ try {
+ schema.validateSync(value, { abortEarly: false });
+ } catch (err) {
+ if (err instanceof yup.ValidationError) {
+ const yupErrors = err.inner as yup.ValidationError[];
+ errors = yupErrors.reduce(
+ (prev, cur) =>
+ !cur.path ? prev : { ...prev, [cur.path]: cur.message },
+ {},
+ );
+ }
+ }
+
+ const submit = useCallback((): Entity | undefined => {
+ return value as MerchantBackend.Product;
+ }, [value]);
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ useEffect(() => {
+ onSubscribe(hasErrors ? undefined : submit);
+ }, [submit, hasErrors]);
+
+ const { i18n } = useTranslationContext();
+
+ return (
+ <div>
+ <FormProvider<NonInventoryProduct>
+ name="product"
+ errors={errors}
+ object={value}
+ valueHandler={valueHandler}
+ >
+ <InputImage<NonInventoryProduct>
+ name="image"
+ label={i18n.str`Image`}
+ tooltip={i18n.str`photo of the product`}
+ />
+ <Input<NonInventoryProduct>
+ name="description"
+ inputType="multiline"
+ label={i18n.str`Description`}
+ tooltip={i18n.str`full product description`}
+ />
+ <Input<NonInventoryProduct>
+ name="unit"
+ label={i18n.str`Unit`}
+ tooltip={i18n.str`name of the product unit`}
+ />
+ <InputCurrency<NonInventoryProduct>
+ name="price"
+ label={i18n.str`Price`}
+ tooltip={i18n.str`amount in the current currency`}
+ />
+
+ <InputNumber<NonInventoryProduct>
+ name="quantity"
+ label={i18n.str`Quantity`}
+ tooltip={i18n.str`how many products will be added`}
+ />
+
+ <InputTaxes<NonInventoryProduct> name="taxes" label={i18n.str`Taxes`} />
+ </FormProvider>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/product/ProductForm.tsx b/packages/auditor-backoffice-ui/src/components/product/ProductForm.tsx
new file mode 100644
index 000000000..e91e8c876
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/product/ProductForm.tsx
@@ -0,0 +1,178 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h } from "preact";
+import { useCallback, useEffect, useState } from "preact/hooks";
+import * as yup from "yup";
+import { useBackendContext } from "../../context/backend.js";
+import { MerchantBackend } from "../../declaration.js";
+import {
+ ProductCreateSchema as createSchema,
+ ProductUpdateSchema as updateSchema,
+} from "../../schemas/index.js";
+import { FormErrors, FormProvider } from "../form/FormProvider.js";
+import { Input } from "../form/Input.js";
+import { InputCurrency } from "../form/InputCurrency.js";
+import { InputImage } from "../form/InputImage.js";
+import { InputNumber } from "../form/InputNumber.js";
+import { InputStock, Stock } from "../form/InputStock.js";
+import { InputTaxes } from "../form/InputTaxes.js";
+import { InputWithAddon } from "../form/InputWithAddon.js";
+
+type Entity = MerchantBackend.Products.ProductDetail & { product_id: string };
+
+interface Props {
+ onSubscribe: (c?: () => Entity | undefined) => void;
+ initial?: Partial<Entity>;
+ alreadyExist?: boolean;
+}
+
+export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) {
+ const [value, valueHandler] = useState<Partial<Entity & { stock: Stock }>>({
+ address: {},
+ description_i18n: {},
+ taxes: [],
+ next_restock: { t_s: "never" },
+ price: ":0",
+ ...initial,
+ stock:
+ !initial || initial.total_stock === -1
+ ? undefined
+ : {
+ current: initial.total_stock || 0,
+ lost: initial.total_lost || 0,
+ sold: initial.total_sold || 0,
+ address: initial.address,
+ nextRestock: initial.next_restock,
+ },
+ });
+ let errors: FormErrors<Entity> = {};
+
+ try {
+ (alreadyExist ? updateSchema : createSchema).validateSync(value, {
+ abortEarly: false,
+ });
+ } catch (err) {
+ if (err instanceof yup.ValidationError) {
+ const yupErrors = err.inner as yup.ValidationError[];
+ errors = yupErrors.reduce(
+ (prev, cur) =>
+ !cur.path ? prev : { ...prev, [cur.path]: cur.message },
+ {},
+ );
+ }
+ }
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const submit = useCallback((): Entity | undefined => {
+ const stock: Stock = (value as any).stock;
+
+ if (!stock) {
+ value.total_stock = -1;
+ } else {
+ value.total_stock = stock.current;
+ value.total_lost = stock.lost;
+ value.next_restock =
+ stock.nextRestock instanceof Date
+ ? { t_s: stock.nextRestock.getTime() / 1000 }
+ : stock.nextRestock;
+ value.address = stock.address;
+ }
+ delete (value as any).stock;
+
+ if (typeof value.minimum_age !== "undefined" && value.minimum_age < 1) {
+ delete value.minimum_age;
+ }
+
+ return value as MerchantBackend.Products.ProductDetail & {
+ product_id: string;
+ };
+ }, [value]);
+
+ useEffect(() => {
+ onSubscribe(hasErrors ? undefined : submit);
+ }, [submit, hasErrors]);
+
+ const { url: backendURL } = useBackendContext()
+ const { i18n } = useTranslationContext();
+
+ return (
+ <div>
+ <FormProvider<Entity>
+ name="product"
+ errors={errors}
+ object={value}
+ valueHandler={valueHandler}
+ >
+ {alreadyExist ? undefined : (
+ <InputWithAddon<Entity>
+ name="product_id"
+ addonBefore={`${backendURL}/product/`}
+ label={i18n.str`ID`}
+ tooltip={i18n.str`product identification to use in URLs (for internal use only)`}
+ />
+ )}
+ <InputImage<Entity>
+ name="image"
+ label={i18n.str`Image`}
+ tooltip={i18n.str`illustration of the product for customers`}
+ />
+ <Input<Entity>
+ name="description"
+ inputType="multiline"
+ label={i18n.str`Description`}
+ tooltip={i18n.str`product description for customers`}
+ />
+ <InputNumber<Entity>
+ name="minimum_age"
+ label={i18n.str`Age restriction`}
+ tooltip={i18n.str`is this product restricted for customer below certain age?`}
+ help={i18n.str`minimum age of the buyer`}
+ />
+ <Input<Entity>
+ name="unit"
+ label={i18n.str`Unit name`}
+ tooltip={i18n.str`unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 items, 5 meters) for customers`}
+ help={i18n.str`exajmple: kg, items or liters`}
+ />
+ <InputCurrency<Entity>
+ name="price"
+ label={i18n.str`Price per unit`}
+ tooltip={i18n.str`sale price for customers, including taxes, for above units of the product`}
+ />
+ <InputStock
+ name="stock"
+ label={i18n.str`Stock`}
+ alreadyExist={alreadyExist}
+ tooltip={i18n.str`inventory for products with finite supply (for internal use only)`}
+ />
+ <InputTaxes<Entity>
+ name="taxes"
+ label={i18n.str`Taxes`}
+ tooltip={i18n.str`taxes included in the product price, exposed to customers`}
+ />
+ </FormProvider>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/product/ProductList.tsx b/packages/auditor-backoffice-ui/src/components/product/ProductList.tsx
new file mode 100644
index 000000000..25751dd96
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/product/ProductList.tsx
@@ -0,0 +1,106 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { Amounts } from "@gnu-taler/taler-util";
+import { h, VNode } from "preact";
+import emptyImage from "../../assets/empty.png";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { MerchantBackend } from "../../declaration.js";
+
+interface Props {
+ list: MerchantBackend.Product[];
+ actions?: {
+ name: string;
+ tooltip: string;
+ handler: (d: MerchantBackend.Product, index: number) => void;
+ }[];
+}
+export function ProductList({ list, actions = [] }: Props): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="table-container">
+ <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>image</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>description</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>quantity</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>unit price</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>total price</i18n.Translate>
+ </th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ {list.map((entry, index) => {
+ const unitPrice = !entry.price ? "0" : entry.price;
+ const totalPrice = !entry.price
+ ? "0"
+ : Amounts.stringify(
+ Amounts.mult(
+ Amounts.parseOrThrow(entry.price),
+ entry.quantity,
+ ).amount,
+ );
+
+ return (
+ <tr key={index}>
+ <td>
+ <img
+ style={{ height: 32, width: 32 }}
+ src={entry.image ? entry.image : emptyImage}
+ />
+ </td>
+ <td>{entry.description}</td>
+ <td>
+ {entry.quantity === 0
+ ? "--"
+ : `${entry.quantity} ${entry.unit}`}
+ </td>
+ <td>{unitPrice}</td>
+ <td>{totalPrice}</td>
+ <td class="is-actions-cell right-sticky">
+ {actions.map((a, i) => {
+ return (
+ <div key={i} class="buttons is-right">
+ <button
+ class="button is-small is-danger has-tooltip-left"
+ data-tooltip={a.tooltip}
+ type="button"
+ onClick={() => a.handler(entry, index)}
+ >
+ {a.name}
+ </button>
+ </div>
+ );
+ })}
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/context/backend.test.ts b/packages/auditor-backoffice-ui/src/context/backend.test.ts
new file mode 100644
index 000000000..359859819
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/context/backend.test.ts
@@ -0,0 +1,163 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import * as tests from "@gnu-taler/web-util/testing";
+import { ComponentChildren, h, VNode } from "preact";
+import { AccessToken, MerchantBackend } from "../declaration.js";
+import {
+ useAdminAPI,
+ useInstanceAPI,
+ useManagementAPI,
+} from "../hooks/instance.js";
+import { expect } from "chai";
+import { ApiMockEnvironment } from "../hooks/testing.js";
+import {
+ API_CREATE_INSTANCE,
+ API_NEW_LOGIN,
+ API_UPDATE_CURRENT_INSTANCE_AUTH,
+ API_UPDATE_INSTANCE_AUTH_BY_ID,
+} from "../hooks/urls.js";
+
+interface TestingContextProps {
+ children?: ComponentChildren;
+}
+
+describe("backend context api ", () => {
+ it("should use new token after updating the instance token in the settings as user", async () => {
+ const env = new ApiMockEnvironment();
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const instance = useInstanceAPI();
+ const management = useManagementAPI("default");
+ const admin = useAdminAPI();
+
+ return { instance, management, admin };
+ },
+ {},
+ [
+ ({ instance, management, admin }) => {
+ env.addRequestExpectation(API_UPDATE_INSTANCE_AUTH_BY_ID("default"), {
+ request: {
+ method: "token",
+ token: "another_token",
+ },
+ response: {
+ name: "instance_name",
+ } as MerchantBackend.Instances.QueryInstancesResponse,
+ });
+ env.addRequestExpectation(API_NEW_LOGIN, {
+ auth: "another_token",
+ request: {
+ scope: "write",
+ duration: {
+ "d_us": "forever",
+ },
+ refreshable: true,
+ },
+
+ });
+
+ management.setNewAccessToken(undefined,"another_token" as AccessToken);
+ },
+ ({ instance, management, admin }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+
+ env.addRequestExpectation(API_CREATE_INSTANCE, {
+ // auth: "another_token",
+ request: {
+ id: "new_instance_id",
+ } as MerchantBackend.Instances.InstanceConfigurationMessage,
+ });
+
+ admin.createInstance({
+ id: "new_instance_id",
+ } as MerchantBackend.Instances.InstanceConfigurationMessage);
+ },
+ ],
+ env.buildTestingContext(),
+ );
+
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+
+ it("should use new token after updating the instance token in the settings as admin", async () => {
+ const env = new ApiMockEnvironment();
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const instance = useInstanceAPI();
+ const management = useManagementAPI("default");
+ const admin = useAdminAPI();
+
+ return { instance, management, admin };
+ },
+ {},
+ [
+ ({ instance, management, admin }) => {
+ env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE_AUTH, {
+ request: {
+ method: "token",
+ token: "another_token",
+ },
+ response: {
+ name: "instance_name",
+ } as MerchantBackend.Instances.QueryInstancesResponse,
+ });
+ env.addRequestExpectation(API_NEW_LOGIN, {
+ auth: "another_token",
+ request: {
+ scope: "write",
+ duration: {
+ "d_us": "forever",
+ },
+ refreshable: true,
+ },
+ });
+ instance.setNewAccessToken(undefined, "another_token" as AccessToken);
+ },
+ ({ instance, management, admin }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+
+ env.addRequestExpectation(API_CREATE_INSTANCE, {
+ // auth: "another_token",
+ request: {
+ id: "new_instance_id",
+ } as MerchantBackend.Instances.InstanceConfigurationMessage,
+ });
+
+ admin.createInstance({
+ id: "new_instance_id",
+ } as MerchantBackend.Instances.InstanceConfigurationMessage);
+ },
+ ],
+ env.buildTestingContext(),
+ );
+
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+});
diff --git a/packages/auditor-backoffice-ui/src/context/backend.ts b/packages/auditor-backoffice-ui/src/context/backend.ts
new file mode 100644
index 000000000..b13b92c42
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/context/backend.ts
@@ -0,0 +1,70 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useMemoryStorage } from "@gnu-taler/web-util/browser";
+import { createContext, h, VNode } from "preact";
+import { useContext } from "preact/hooks";
+import { LoginToken } from "../declaration.js";
+import { useBackendDefaultToken, useBackendURL } from "../hooks/index.js";
+
+interface BackendContextType {
+ url: string,
+ alreadyTriedLogin: boolean;
+ token?: LoginToken;
+ updateToken: (token: LoginToken | undefined) => void;
+}
+
+const BackendContext = createContext<BackendContextType>({
+ url: "",
+ alreadyTriedLogin: false,
+ token: undefined,
+ updateToken: () => null,
+});
+
+function useBackendContextState(
+ defaultUrl?: string,
+): BackendContextType {
+const [url] = useBackendURL(defaultUrl);
+ //const url = "http://localhost:8081";
+ const [token, updateToken] = useBackendDefaultToken();
+
+ return {
+ url,
+ token,
+ alreadyTriedLogin: token !== undefined,
+ updateToken,
+ };
+}
+
+export const BackendContextProvider = ({
+ children,
+ defaultUrl,
+}: {
+ children: any;
+ defaultUrl?: string;
+}): VNode => {
+ const value = useBackendContextState(defaultUrl);
+
+ return h(BackendContext.Provider, { value, children });
+};
+
+export const useBackendContext = (): BackendContextType =>
+ useContext(BackendContext);
diff --git a/packages/merchant-backend-ui/src/context/config.ts b/packages/auditor-backoffice-ui/src/context/config.ts
index 5cd772380..def45ea64 100644
--- a/packages/merchant-backend-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 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,18 +15,18 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { createContext } from 'preact'
-import { useContext } from 'preact/hooks'
+import { createContext } from "preact";
+import { useContext } from "preact/hooks";
interface Type {
currency: string;
version: string;
}
-const Context = createContext<Type>(null!)
+const Context = createContext<Type>(null!);
-export const ConfigContextProvider = Context.Provider
+export const ConfigContextProvider = Context.Provider;
export const useConfigContext = (): Type => useContext(Context);
diff --git a/packages/merchant-backend-ui/src/context/instance.ts b/packages/auditor-backoffice-ui/src/context/instance.ts
index fecf36426..5800ade7e 100644
--- a/packages/merchant-backend-ui/src/context/instance.ts
+++ b/packages/auditor-backoffice-ui/src/context/instance.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,21 +15,22 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { createContext } from 'preact'
-import { useContext } from 'preact/hooks'
+import { createContext } from "preact";
+import { useContext } from "preact/hooks";
+import { LoginToken } from "../declaration.js";
interface Type {
id: string;
- token?: string;
+ token?: LoginToken;
admin?: boolean;
- changeToken: (t?:string) => void;
+ changeToken: (t?: LoginToken) => void;
}
-const Context = createContext<Type>({} as any)
+const Context = createContext<Type>({} as any);
-export const InstanceContextProvider = Context.Provider
+export const InstanceContextProvider = Context.Provider;
export const useInstanceContext = (): Type => useContext(Context);
diff --git a/packages/auditor-backoffice-ui/src/custom.d.ts b/packages/auditor-backoffice-ui/src/custom.d.ts
new file mode 100644
index 000000000..34522a2dd
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/custom.d.ts
@@ -0,0 +1,42 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+declare module "*.po" {
+ const content: any;
+ export default content;
+}
+declare module "jed" {
+ const x: any;
+ export = x;
+}
+declare module "*.jpeg" {
+ const content: any;
+ export default content;
+}
+declare module "*.png" {
+ const content: any;
+ export default content;
+}
+declare module "*.svg" {
+ const content: any;
+ export default content;
+}
+
+declare module "*.scss" {
+ const content: Record<string, string>;
+ export default content;
+}
+declare const __VERSION__: string;
+declare const __GIT_HASH__: string;
diff --git a/packages/auditor-backoffice-ui/src/declaration.d.ts b/packages/auditor-backoffice-ui/src/declaration.d.ts
new file mode 100644
index 000000000..0c6f599f7
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/declaration.d.ts
@@ -0,0 +1,1793 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+type HashCode = string;
+type EddsaPublicKey = string;
+type EddsaSignature = string;
+type WireTransferIdentifierRawP = string;
+type RelativeTime = TalerProtocolDuration;
+type ImageDataUrl = string;
+type MerchantUserType = "business" | "individual";
+
+
+export interface WithId {
+ id: string;
+}
+
+interface Timestamp {
+ // Milliseconds since epoch, or the special
+ // value "forever" to represent an event that will
+ // never happen.
+ t_s: number | "never";
+}
+interface TalerProtocolDuration {
+ d_us: number | "forever";
+}
+interface Duration {
+ d_ms: number | "forever";
+}
+
+interface WithId {
+ id: string;
+}
+
+type Amount = string;
+type UUID = string;
+type Integer = number;
+
+interface WireAccount {
+ // payto:// URI identifying the account and wire method
+ payto_uri: string;
+
+ // URI to convert amounts from or to the currency used by
+ // this wire account of the exchange. Missing if no
+ // conversion is applicable.
+ conversion_url?: string;
+
+ // Restrictions that apply to bank accounts that would send
+ // funds to the exchange (crediting this exchange bank account).
+ // Optional, empty array for unrestricted.
+ credit_restrictions: AccountRestriction[];
+
+ // Restrictions that apply to bank accounts that would receive
+ // funds from the exchange (debiting this exchange bank account).
+ // Optional, empty array for unrestricted.
+ debit_restrictions: AccountRestriction[];
+
+ // Signature using the exchange's offline key over
+ // a TALER_MasterWireDetailsPS
+ // with purpose TALER_SIGNATURE_MASTER_WIRE_DETAILS.
+ master_sig: EddsaSignature;
+}
+
+type AccountRestriction = RegexAccountRestriction | DenyAllAccountRestriction;
+
+// Account restriction that disables this type of
+// account for the indicated operation categorically.
+interface DenyAllAccountRestriction {
+ type: "deny";
+}
+
+// Accounts interacting with this type of account
+// restriction must have a payto://-URI matching
+// the given regex.
+interface RegexAccountRestriction {
+ type: "regex";
+
+ // Regular expression that the payto://-URI of the
+ // partner account must follow. The regular expression
+ // should follow posix-egrep, but without support for character
+ // classes, GNU extensions, back-references or intervals. See
+ // https://www.gnu.org/software/findutils/manual/html_node/find_html/posix_002degrep-regular-expression-syntax.html
+ // for a description of the posix-egrep syntax. Applications
+ // may support regexes with additional features, but exchanges
+ // must not use such regexes.
+ payto_regex: string;
+
+ // Hint for a human to understand the restriction
+ // (that is hopefully easier to comprehend than the regex itself).
+ human_hint: string;
+
+ // Map from IETF BCP 47 language tags to localized
+ // human hints.
+ human_hint_i18n?: { [lang_tag: string]: string };
+}
+interface LoginToken {
+ token: string,
+ expiration: Timestamp,
+}
+// token used to get loginToken
+// must forget after used
+declare const __ac_token: unique symbol;
+type AccessToken = string & {
+ [__ac_token]: true;
+};
+
+export namespace ExchangeBackend {
+ interface WireResponse {
+ // Master public key of the exchange, must match the key returned in /keys.
+ master_public_key: EddsaPublicKey;
+
+ // Array of wire accounts operated by the exchange for
+ // incoming wire transfers.
+ accounts: WireAccount[];
+
+ // Object mapping names of wire methods (i.e. "sepa" or "x-taler-bank")
+ // to wire fees.
+ fees: { method: AggregateTransferFee };
+ }
+ interface AggregateTransferFee {
+ // Per transfer wire transfer fee.
+ wire_fee: Amount;
+
+ // Per transfer closing fee.
+ closing_fee: Amount;
+
+ // What date (inclusive) does this fee go into effect?
+ // The different fees must cover the full time period in which
+ // any of the denomination keys are valid without overlap.
+ start_date: Timestamp;
+
+ // What date (exclusive) does this fee stop going into effect?
+ // The different fees must cover the full time period in which
+ // any of the denomination keys are valid without overlap.
+ end_date: Timestamp;
+
+ // Signature of TALER_MasterWireFeePS with
+ // purpose TALER_SIGNATURE_MASTER_WIRE_FEES.
+ sig: EddsaSignature;
+ }
+}
+export namespace AuditorBackend {
+ interface ErrorDetail {
+ // Numeric error code unique to the condition.
+ // The other arguments are specific to the error value reported here.
+ code: number;
+
+ // Human-readable description of the error, i.e. "missing parameter", "commitment violation", ...
+ // Should give a human-readable hint about the error's nature. Optional, may change without notice!
+ hint?: string;
+
+ // Optional detail about the specific input value that failed. May change without notice!
+ detail?: string;
+
+ // Name of the parameter that was bogus (if applicable).
+ parameter?: string;
+
+ // Path to the argument that was bogus (if applicable).
+ path?: string;
+
+ // Offset of the argument that was bogus (if applicable).
+ offset?: string;
+
+ // Index of the argument that was bogus (if applicable).
+ index?: string;
+
+ // Name of the object that was bogus (if applicable).
+ object?: string;
+
+ // Name of the currency than was problematic (if applicable).
+ currency?: string;
+
+ // Expected type (if applicable).
+ type_expected?: string;
+
+ // Type that was provided instead (if applicable).
+ type_actual?: string;
+ }
+ interface Exchange {
+ // the exchange's base URL
+ url: string;
+
+ // master public key of the exchange
+ master_pub: EddsaPublicKey;
+ }
+ namespace DepositConfirmation {
+ // POST /deposit-confirmation
+ interface ProductAddDetail {
+ // product ID to use.
+ product_id: string;
+
+ // Human-readable product description.
+ description: string;
+
+ // Map from IETF BCP 47 language tags to localized descriptions
+ description_i18n: { [lang_tag: string]: string };
+
+ // unit in which the product is measured (liters, kilograms, packages, etc.)
+ unit: string;
+
+ // The price for one unit of the product. Zero is used
+ // to imply that this product is not sold separately, or
+ // that the price is not fixed, and must be supplied by the
+ // front-end. If non-zero, this price MUST include applicable
+ // taxes.
+ price: Amount;
+
+ // An optional base64-encoded product image
+ image: ImageDataUrl;
+
+ // a list of taxes paid by the merchant for one unit of this product
+ taxes: Tax[];
+
+ // Number of units of the product in stock in sum in total,
+ // including all existing sales ever. Given in product-specific
+ // units.
+ // A value of -1 indicates "infinite" (i.e. for "electronic" books).
+ total_stock: Integer;
+
+ // Identifies where the product is in stock.
+ address: Location;
+
+ // Identifies when we expect the next restocking to happen.
+ next_restock?: Timestamp;
+
+ // Minimum age buyer must have (in years). Default is 0.
+ minimum_age?: Integer;
+ }
+ // PATCH /private/products/$PRODUCT_ID
+ interface ProductPatchDetail {
+ // Human-readable product description.
+ description: string;
+
+ // Map from IETF BCP 47 language tags to localized descriptions
+ description_i18n: { [lang_tag: string]: string };
+
+ // unit in which the product is measured (liters, kilograms, packages, etc.)
+ unit: string;
+
+ // The price for one unit of the product. Zero is used
+ // to imply that this product is not sold separately, or
+ // that the price is not fixed, and must be supplied by the
+ // front-end. If non-zero, this price MUST include applicable
+ // taxes.
+ price: Amount;
+
+ // An optional base64-encoded product image
+ image: ImageDataUrl;
+
+ // a list of taxes paid by the merchant for one unit of this product
+ taxes: Tax[];
+
+ // Number of units of the product in stock in sum in total,
+ // including all existing sales ever. Given in product-specific
+ // units.
+ // A value of -1 indicates "infinite" (i.e. for "electronic" books).
+ total_stock: Integer;
+
+ // Number of units of the product that were lost (spoiled, stolen, etc.)
+ total_lost: Integer;
+
+ // Identifies where the product is in stock.
+ address: Location;
+
+ // Identifies when we expect the next restocking to happen.
+ next_restock?: Timestamp;
+
+ // Minimum age buyer must have (in years). Default is 0.
+ minimum_age?: Integer;
+ }
+
+ // GET /deposit-confirmation
+ interface DepositConfirmationList {
+ depositConfirmations: DepositConfirmation [];
+ }
+ interface DepositConfirmation {
+ serial_id: string;
+ timestamp: string;
+ refund_deadline: string;
+ wire_deadline: string;
+ amount_without_fee: string;
+ }
+
+ // GET /deposit-confirmation/$SERIAL_ID
+ interface DepositConfirmationDetail {
+ serial_id: string;
+ timestamp: string;
+ refund_deadline: string;
+ wire_deadline: string;
+ amount_without_fee: string;
+ }
+ }
+
+}
+export namespace MerchantBackend {
+ interface ErrorDetail {
+ // Numeric error code unique to the condition.
+ // The other arguments are specific to the error value reported here.
+ code: number;
+
+ // Human-readable description of the error, i.e. "missing parameter", "commitment violation", ...
+ // Should give a human-readable hint about the error's nature. Optional, may change without notice!
+ hint?: string;
+
+ // Optional detail about the specific input value that failed. May change without notice!
+ detail?: string;
+
+ // Name of the parameter that was bogus (if applicable).
+ parameter?: string;
+
+ // Path to the argument that was bogus (if applicable).
+ path?: string;
+
+ // Offset of the argument that was bogus (if applicable).
+ offset?: string;
+
+ // Index of the argument that was bogus (if applicable).
+ index?: string;
+
+ // Name of the object that was bogus (if applicable).
+ object?: string;
+
+ // Name of the currency than was problematic (if applicable).
+ currency?: string;
+
+ // Expected type (if applicable).
+ type_expected?: string;
+
+ // Type that was provided instead (if applicable).
+ type_actual?: string;
+ }
+
+ // Delivery location, loosely modeled as a subset of
+ // ISO20022's PostalAddress25.
+ interface Tax {
+ // the name of the tax
+ name: string;
+
+ // amount paid in tax
+ tax: Amount;
+ }
+
+ interface Auditor {
+ // official name
+ name: string;
+
+ // Auditor's public key
+ auditor_pub: EddsaPublicKey;
+
+ // Base URL of the auditor
+ url: string;
+ }
+ interface Exchange {
+ // the exchange's base URL
+ url: string;
+
+ // master public key of the exchange
+ master_pub: EddsaPublicKey;
+ }
+
+ interface Product {
+ // merchant-internal identifier for the product.
+ product_id?: string;
+
+ // Human-readable product description.
+ description: string;
+
+ // Map from IETF BCP 47 language tags to localized descriptions
+ description_i18n?: { [lang_tag: string]: string };
+
+ // The number of units of the product to deliver to the customer.
+ quantity: Integer;
+
+ // The unit in which the product is measured (liters, kilograms, packages, etc.)
+ unit: string;
+
+ // The price of the product; this is the total price for quantity times unit of this product.
+ price?: Amount;
+
+ // An optional base64-encoded product image
+ image: ImageDataUrl;
+
+ // a list of taxes paid by the merchant for this product. Can be empty.
+ taxes: Tax[];
+
+ // time indicating when this product should be delivered
+ delivery_date?: TalerProtocolTimestamp;
+
+ // Minimum age buyer must have (in years). Default is 0.
+ minimum_age?: Integer;
+ }
+ interface Merchant {
+ // label for a location with the business address of the merchant
+ address: Location;
+
+ // the merchant's legal name of business
+ name: string;
+
+ // label for a location that denotes the jurisdiction for disputes.
+ // Some of the typical fields for a location (such as a street address) may be absent.
+ jurisdiction: Location;
+ }
+
+ interface VersionResponse {
+ // libtool-style representation of the Merchant protocol version, see
+ // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning
+ // The format is "current:revision:age".
+ version: string;
+
+ // Name of the protocol.
+ name: "taler-merchant";
+
+ // Currency supported by this backend.
+ currency: string;
+ }
+ interface Location {
+ // Nation with its own government.
+ country?: string;
+
+ // Identifies a subdivision of a country such as state, region, county.
+ country_subdivision?: string;
+
+ // Identifies a subdivision within a country sub-division.
+ district?: string;
+
+ // Name of a built-up area, with defined boundaries, and a local government.
+ town?: string;
+
+ // Specific location name within the town.
+ town_location?: string;
+
+ // Identifier consisting of a group of letters and/or numbers that
+ // is added to a postal address to assist the sorting of mail.
+ post_code?: string;
+
+ // Name of a street or thoroughfare.
+ street?: string;
+
+ // Name of the building or house.
+ building_name?: string;
+
+ // Number that identifies the position of a building on a street.
+ building_number?: string;
+
+ // Free-form address lines, should not exceed 7 elements.
+ address_lines?: string[];
+ }
+ namespace Instances {
+ //POST /private/instances/$INSTANCE/auth
+ interface InstanceAuthConfigurationMessage {
+ // Type of authentication.
+ // "external": The mechant backend does not do
+ // any authentication checks. Instead an API
+ // gateway must do the authentication.
+ // "token": The merchant checks an auth token.
+ // See "token" for details.
+ method: "external" | "token";
+
+ // For method "external", this field is mandatory.
+ // The token MUST begin with the string "secret-token:".
+ // After the auth token has been set (with method "token"),
+ // the value must be provided in a "Authorization: Bearer $token"
+ // header.
+ token?: string;
+ }
+ //POST /private/instances
+ interface InstanceConfigurationMessage {
+ // Name of the merchant instance to create (will become $INSTANCE).
+ id: string;
+
+ // Merchant name corresponding to this instance.
+ name: string;
+
+ // Type of the user (business or individual).
+ // Defaults to 'business'. Should become mandatory field
+ // in the future, left as optional for API compatibility for now.
+ user_type?: MerchantUserType;
+
+ // Merchant email for customer contact.
+ email?: string;
+
+ // Merchant public website.
+ website?: string;
+
+ // Merchant logo.
+ logo?: ImageDataUrl;
+
+ // "Authentication" header required to authorize management access the instance.
+ // Optional, if not given authentication will be disabled for
+ // this instance (hopefully authentication checks are still
+ // done by some reverse proxy).
+ auth: InstanceAuthConfigurationMessage;
+
+ // The merchant's physical address (to be put into contracts).
+ address: Location;
+
+ // The jurisdiction under which the merchant conducts its business
+ // (to be put into contracts).
+ jurisdiction: Location;
+
+ // Use STEFAN curves to determine default fees?
+ // If false, no fees are allowed by default.
+ // Can always be overridden by the frontend on a per-order basis.
+ use_stefan: boolean;
+
+ // If the frontend does NOT specify an execution date, how long should
+ // we tell the exchange to wait to aggregate transactions before
+ // executing the wire transfer? This delay is added to the current
+ // time when we generate the advisory execution time for the exchange.
+ default_wire_transfer_delay: RelativeTime;
+
+ // If the frontend does NOT specify a payment deadline, how long should
+ // offers we make be valid by default?
+ default_pay_delay: RelativeTime;
+ }
+
+ // PATCH /private/instances/$INSTANCE
+ interface InstanceReconfigurationMessage {
+
+ // Merchant name corresponding to this instance.
+ name: string;
+
+ // Type of the user (business or individual).
+ // Defaults to 'business'. Should become mandatory field
+ // in the future, left as optional for API compatibility for now.
+ user_type?: MerchantUserType;
+
+ // Merchant email for customer contact.
+ email?: string;
+
+ // Merchant public website.
+ website?: string;
+
+ // Merchant logo.
+ logo?: ImageDataUrl;
+
+ // The merchant's physical address (to be put into contracts).
+ address: Location;
+
+ // The jurisdiction under which the merchant conducts its business
+ // (to be put into contracts).
+ jurisdiction: Location;
+
+ // Use STEFAN curves to determine default fees?
+ // If false, no fees are allowed by default.
+ // Can always be overridden by the frontend on a per-order basis.
+ use_stefan: boolean;
+
+ // If the frontend does NOT specify an execution date, how long should
+ // we tell the exchange to wait to aggregate transactions before
+ // executing the wire transfer? This delay is added to the current
+ // time when we generate the advisory execution time for the exchange.
+ default_wire_transfer_delay: RelativeTime;
+
+ // If the frontend does NOT specify a payment deadline, how long should
+ // offers we make be valid by default?
+ default_pay_delay: RelativeTime;
+ }
+
+ // GET /private/instances
+ interface InstancesResponse {
+ // List of instances that are present in the backend (see Instance)
+ instances: Instance[];
+ }
+
+ interface Instance {
+ // Merchant name corresponding to this instance.
+ name: string;
+
+ // Type of the user ("business" or "individual").
+ user_type: MerchantUserType;
+
+ // Merchant public website.
+ website?: string;
+
+ // Merchant logo.
+ logo?: ImageDataUrl;
+
+ // Merchant instance this response is about ($INSTANCE)
+ id: string;
+
+ // Public key of the merchant/instance, in Crockford Base32 encoding.
+ merchant_pub: EddsaPublicKey;
+
+ // List of the payment targets supported by this instance. Clients can
+ // specify the desired payment target in /order requests. Note that
+ // front-ends do not have to support wallets selecting payment targets.
+ payment_targets: string[];
+
+ // Has this instance been deleted (but not purged)?
+ deleted: boolean;
+ }
+
+ //GET /private/instances/$INSTANCE
+ interface QueryInstancesResponse {
+
+ // Merchant name corresponding to this instance.
+ name: string;
+ // Type of the user ("business" or "individual").
+ user_type: MerchantUserType;
+
+ // Merchant email for customer contact.
+ email?: string;
+
+ // Merchant public website.
+ website?: string;
+
+ // Merchant logo.
+ logo?: ImageDataUrl;
+
+ // Public key of the merchant/instance, in Crockford Base32 encoding.
+ merchant_pub: EddsaPublicKey;
+
+ // The merchant's physical address (to be put into contracts).
+ address: Location;
+
+ // The jurisdiction under which the merchant conducts its business
+ // (to be put into contracts).
+ jurisdiction: Location;
+
+ // Use STEFAN curves to determine default fees?
+ // If false, no fees are allowed by default.
+ // Can always be overridden by the frontend on a per-order basis.
+ use_stefan: boolean;
+
+ // If the frontend does NOT specify an execution date, how long should
+ // we tell the exchange to wait to aggregate transactions before
+ // executing the wire transfer? This delay is added to the current
+ // time when we generate the advisory execution time for the exchange.
+ default_wire_transfer_delay: RelativeTime;
+
+ // If the frontend does NOT specify a payment deadline, how long should
+ // offers we make be valid by default?
+ default_pay_delay: RelativeTime;
+
+ // Authentication configuration.
+ // Does not contain the token when token auth is configured.
+ auth: {
+ method: "external" | "token";
+ };
+ }
+ // DELETE /private/instances/$INSTANCE
+ interface LoginTokenRequest {
+ // Scope of the token (which kinds of operations it will allow)
+ scope: "readonly" | "write";
+
+ // Server may impose its own upper bound
+ // on the token validity duration
+ duration?: RelativeTime;
+
+ // Can this token be refreshed?
+ // Defaults to false.
+ refreshable?: boolean;
+ }
+ interface LoginTokenSuccessResponse {
+ // The login token that can be used to access resources
+ // that are in scope for some time. Must be prefixed
+ // with "Bearer " when used in the "Authorization" HTTP header.
+ // Will already begin with the RFC 8959 prefix.
+ token: string;
+
+ // Scope of the token (which kinds of operations it will allow)
+ scope: "readonly" | "write";
+
+ // Server may impose its own upper bound
+ // on the token validity duration
+ expiration: Timestamp;
+
+ // Can this token be refreshed?
+ refreshable: boolean;
+ }
+ }
+
+ namespace KYC {
+ //GET /private/instances/$INSTANCE/kyc
+ interface AccountKycRedirects {
+ // Array of pending KYCs.
+ pending_kycs: MerchantAccountKycRedirect[];
+
+ // Array of exchanges with no reply.
+ timeout_kycs: ExchangeKycTimeout[];
+ }
+ interface MerchantAccountKycRedirect {
+ // URL that the user should open in a browser to
+ // proceed with the KYC process (as returned
+ // by the exchange's /kyc-check/ endpoint).
+ // Optional, missing if the account is blocked
+ // due to AML and not due to KYC.
+ kyc_url?: string;
+
+ // Base URL of the exchange this is about.
+ exchange_url: string;
+
+ // AML status of the account.
+ aml_status: number;
+
+ // Our bank wire account this is about.
+ payto_uri: string;
+ }
+ interface ExchangeKycTimeout {
+ // Base URL of the exchange this is about.
+ exchange_url: string;
+
+ // Numeric error code indicating errors the exchange
+ // returned, or TALER_EC_INVALID for none.
+ exchange_code: number;
+
+ // HTTP status code returned by the exchange when we asked for
+ // information about the KYC status.
+ // 0 if there was no response at all.
+ exchange_http_status: number;
+ }
+
+ }
+
+ namespace BankAccounts {
+
+ interface AccountAddDetails {
+
+ // payto:// URI of the account.
+ payto_uri: string;
+
+ // URL from where the merchant can download information
+ // about incoming wire transfers to this account.
+ credit_facade_url?: string;
+
+ // Credentials to use when accessing the credit facade.
+ // Never returned on a GET (as this may be somewhat
+ // sensitive data). Can be set in POST
+ // or PATCH requests to update (or delete) credentials.
+ // To really delete credentials, set them to the type: "none".
+ credit_facade_credentials?: FacadeCredentials;
+
+ }
+
+ type FacadeCredentials =
+ | NoFacadeCredentials
+ | BasicAuthFacadeCredentials;
+
+ interface NoFacadeCredentials {
+ type: "none";
+ }
+
+ interface BasicAuthFacadeCredentials {
+ type: "basic";
+
+ // Username to use to authenticate
+ username: string;
+
+ // Password to use to authenticate
+ password: string;
+ }
+
+ interface AccountAddResponse {
+ // Hash over the wire details (including over the salt).
+ h_wire: HashCode;
+
+ // Salt used to compute h_wire.
+ salt: HashCode;
+ }
+
+ interface AccountPatchDetails {
+
+ // URL from where the merchant can download information
+ // about incoming wire transfers to this account.
+ credit_facade_url?: string;
+
+ // Credentials to use when accessing the credit facade.
+ // Never returned on a GET (as this may be somewhat
+ // sensitive data). Can be set in POST
+ // or PATCH requests to update (or delete) credentials.
+ // To really delete credentials, set them to the type: "none".
+ credit_facade_credentials?: FacadeCredentials;
+ }
+
+
+ interface AccountsSummaryResponse {
+
+ // List of accounts that are known for the instance.
+ accounts: BankAccountEntry[];
+ }
+
+ interface BankAccountEntry {
+ // payto:// URI of the account.
+ payto_uri: string;
+
+ // Hash over the wire details (including over the salt)
+ h_wire: HashCode;
+
+ // salt used to compute h_wire
+ salt: HashCode;
+
+ // URL from where the merchant can download information
+ // about incoming wire transfers to this account.
+ credit_facade_url?: string;
+
+ // Credentials to use when accessing the credit facade.
+ // Never returned on a GET (as this may be somewhat
+ // sensitive data). Can be set in POST
+ // or PATCH requests to update (or delete) credentials.
+ credit_facade_credentials?: FacadeCredentials;
+
+ // true if this account is active,
+ // false if it is historic.
+ active: boolean;
+ }
+
+ }
+
+ namespace Products {
+ // POST /private/products
+ interface ProductAddDetail {
+ // product ID to use.
+ product_id: string;
+
+ // Human-readable product description.
+ description: string;
+
+ // Map from IETF BCP 47 language tags to localized descriptions
+ description_i18n: { [lang_tag: string]: string };
+
+ // unit in which the product is measured (liters, kilograms, packages, etc.)
+ unit: string;
+
+ // The price for one unit of the product. Zero is used
+ // to imply that this product is not sold separately, or
+ // that the price is not fixed, and must be supplied by the
+ // front-end. If non-zero, this price MUST include applicable
+ // taxes.
+ price: Amount;
+
+ // An optional base64-encoded product image
+ image: ImageDataUrl;
+
+ // a list of taxes paid by the merchant for one unit of this product
+ taxes: Tax[];
+
+ // Number of units of the product in stock in sum in total,
+ // including all existing sales ever. Given in product-specific
+ // units.
+ // A value of -1 indicates "infinite" (i.e. for "electronic" books).
+ total_stock: Integer;
+
+ // Identifies where the product is in stock.
+ address: Location;
+
+ // Identifies when we expect the next restocking to happen.
+ next_restock?: Timestamp;
+
+ // Minimum age buyer must have (in years). Default is 0.
+ minimum_age?: Integer;
+ }
+ // PATCH /private/products/$PRODUCT_ID
+ interface ProductPatchDetail {
+ // Human-readable product description.
+ description: string;
+
+ // Map from IETF BCP 47 language tags to localized descriptions
+ description_i18n: { [lang_tag: string]: string };
+
+ // unit in which the product is measured (liters, kilograms, packages, etc.)
+ unit: string;
+
+ // The price for one unit of the product. Zero is used
+ // to imply that this product is not sold separately, or
+ // that the price is not fixed, and must be supplied by the
+ // front-end. If non-zero, this price MUST include applicable
+ // taxes.
+ price: Amount;
+
+ // An optional base64-encoded product image
+ image: ImageDataUrl;
+
+ // a list of taxes paid by the merchant for one unit of this product
+ taxes: Tax[];
+
+ // Number of units of the product in stock in sum in total,
+ // including all existing sales ever. Given in product-specific
+ // units.
+ // A value of -1 indicates "infinite" (i.e. for "electronic" books).
+ total_stock: Integer;
+
+ // Number of units of the product that were lost (spoiled, stolen, etc.)
+ total_lost: Integer;
+
+ // Identifies where the product is in stock.
+ address: Location;
+
+ // Identifies when we expect the next restocking to happen.
+ next_restock?: Timestamp;
+
+ // Minimum age buyer must have (in years). Default is 0.
+ minimum_age?: Integer;
+ }
+
+ // GET /private/products
+ interface InventorySummaryResponse {
+ // List of products that are present in the inventory
+ products: InventoryEntry[];
+ }
+ interface InventoryEntry {
+ // Product identifier, as found in the product.
+ product_id: string;
+ }
+
+ // GET /private/products/$PRODUCT_ID
+ interface ProductDetail {
+ // Human-readable product description.
+ description: string;
+
+ // Map from IETF BCP 47 language tags to localized descriptions
+ description_i18n: { [lang_tag: string]: string };
+
+ // unit in which the product is measured (liters, kilograms, packages, etc.)
+ unit: string;
+
+ // The price for one unit of the product. Zero is used
+ // to imply that this product is not sold separately, or
+ // that the price is not fixed, and must be supplied by the
+ // front-end. If non-zero, this price MUST include applicable
+ // taxes.
+ price: Amount;
+
+ // An optional base64-encoded product image
+ image: ImageDataUrl;
+
+ // a list of taxes paid by the merchant for one unit of this product
+ taxes: Tax[];
+
+ // Number of units of the product in stock in sum in total,
+ // including all existing sales ever. Given in product-specific
+ // units.
+ // A value of -1 indicates "infinite" (i.e. for "electronic" books).
+ total_stock: Integer;
+
+ // Number of units of the product that have already been sold.
+ total_sold: Integer;
+
+ // Number of units of the product that were lost (spoiled, stolen, etc.)
+ total_lost: Integer;
+
+ // Identifies where the product is in stock.
+ address: Location;
+
+ // Identifies when we expect the next restocking to happen.
+ next_restock?: Timestamp;
+
+ // Minimum age buyer must have (in years). Default is 0.
+ minimum_age?: Integer;
+ }
+
+ // POST /private/products/$PRODUCT_ID/lock
+ interface LockRequest {
+ // UUID that identifies the frontend performing the lock
+ // It is suggested that clients use a timeflake for this,
+ // see https://github.com/anthonynsimon/timeflake
+ lock_uuid: UUID;
+
+ // How long does the frontend intend to hold the lock
+ duration: RelativeTime;
+
+ // How many units should be locked?
+ quantity: Integer;
+ }
+
+ // DELETE /private/products/$PRODUCT_ID
+ }
+
+ namespace Orders {
+ type MerchantOrderStatusResponse =
+ | CheckPaymentPaidResponse
+ | CheckPaymentClaimedResponse
+ | CheckPaymentUnpaidResponse;
+ interface CheckPaymentPaidResponse {
+ // The customer paid for this contract.
+ order_status: "paid";
+
+ // Was the payment refunded (even partially)?
+ refunded: boolean;
+
+ // True if there are any approved refunds that the wallet has
+ // not yet obtained.
+ refund_pending: boolean;
+
+ // Did the exchange wire us the funds?
+ wired: boolean;
+
+ // Total amount the exchange deposited into our bank account
+ // for this contract, excluding fees.
+ deposit_total: Amount;
+
+ // Numeric error code indicating errors the exchange
+ // encountered tracking the wire transfer for this purchase (before
+ // we even got to specific coin issues).
+ // 0 if there were no issues.
+ exchange_ec: number;
+
+ // HTTP status code returned by the exchange when we asked for
+ // information to track the wire transfer for this purchase.
+ // 0 if there were no issues.
+ exchange_hc: number;
+
+ // Total amount that was refunded, 0 if refunded is false.
+ refund_amount: Amount;
+
+ // Contract terms.
+ contract_terms: ContractTerms;
+
+ // The wire transfer status from the exchange for this order if
+ // available, otherwise empty array.
+ wire_details: TransactionWireTransfer[];
+
+ // Reports about trouble obtaining wire transfer details,
+ // empty array if no trouble were encountered.
+ wire_reports: TransactionWireReport[];
+
+ // The refund details for this order. One entry per
+ // refunded coin; empty array if there are no refunds.
+ refund_details: RefundDetails[];
+
+ // Status URL, can be used as a redirect target for the browser
+ // to show the order QR code / trigger the wallet.
+ order_status_url: string;
+ }
+ interface CheckPaymentClaimedResponse {
+ // A wallet claimed the order, but did not yet pay for the contract.
+ order_status: "claimed";
+
+ // Contract terms.
+ contract_terms: ContractTerms;
+ }
+ interface CheckPaymentUnpaidResponse {
+ // The order was neither claimed nor paid.
+ order_status: "unpaid";
+
+ // when was the order created
+ creation_time: Timestamp;
+
+ // Order summary text.
+ summary: string;
+
+ // Total amount of the order (to be paid by the customer).
+ total_amount: Amount;
+
+ // URI that the wallet must process to complete the payment.
+ taler_pay_uri: string;
+
+ // Alternative order ID which was paid for already in the same session.
+ // Only given if the same product was purchased before in the same session.
+ already_paid_order_id?: string;
+
+ // Fulfillment URL of an already paid order. Only given if under this
+ // session an already paid order with a fulfillment URL exists.
+ already_paid_fulfillment_url?: string;
+
+ // Status URL, can be used as a redirect target for the browser
+ // to show the order QR code / trigger the wallet.
+ order_status_url: string;
+
+ // We do we NOT return the contract terms here because they may not
+ // exist in case the wallet did not yet claim them.
+ }
+ interface RefundDetails {
+ // Reason given for the refund.
+ reason: string;
+
+ // When was the refund approved.
+ timestamp: Timestamp;
+
+ // Set to true if a refund is still available for the wallet for this payment.
+ pending: boolean;
+
+ // Total amount that was refunded (minus a refund fee).
+ amount: Amount;
+ }
+ interface TransactionWireTransfer {
+ // Responsible exchange.
+ exchange_url: string;
+
+ // 32-byte wire transfer identifier.
+ wtid: Base32;
+
+ // Execution time of the wire transfer.
+ execution_time: Timestamp;
+
+ // Total amount that has been wire transferred
+ // to the merchant.
+ amount: Amount;
+
+ // Was this transfer confirmed by the merchant via the
+ // POST /transfers API, or is it merely claimed by the exchange?
+ confirmed: boolean;
+ }
+ interface TransactionWireReport {
+ // Numerical error code.
+ code: number;
+
+ // Human-readable error description.
+ hint: string;
+
+ // Numerical error code from the exchange.
+ exchange_ec: number;
+
+ // HTTP status code received from the exchange.
+ exchange_hc: number;
+
+ // Public key of the coin for which we got the exchange error.
+ coin_pub: CoinPublicKey;
+ }
+
+ interface OrderHistory {
+ // timestamp-sorted array of all orders matching the query.
+ // The order of the sorting depends on the sign of delta.
+ orders: OrderHistoryEntry[];
+ }
+ interface OrderHistoryEntry {
+ // order ID of the transaction related to this entry.
+ order_id: string;
+
+ // row ID of the order in the database
+ row_id: number;
+
+ // when the order was created
+ timestamp: Timestamp;
+
+ // the amount of money the order is for
+ amount: Amount;
+
+ // the summary of the order
+ summary: string;
+
+ // whether some part of the order is refundable,
+ // that is the refund deadline has not yet expired
+ // and the total amount refunded so far is below
+ // the value of the original transaction.
+ refundable: boolean;
+
+ // whether the order has been paid or not
+ paid: boolean;
+ }
+
+ interface PostOrderRequest {
+ // The order must at least contain the minimal
+ // order detail, but can override all
+ order: Order;
+
+ // if set, the backend will then set the refund deadline to the current
+ // time plus the specified delay. If it's not set, refunds will not be
+ // possible.
+ refund_delay?: RelativeTime;
+
+ // specifies the payment target preferred by the client. Can be used
+ // to select among the various (active) wire methods supported by the instance.
+ payment_target?: string;
+
+ // specifies that some products are to be included in the
+ // order from the inventory. For these inventory management
+ // is performed (so the products must be in stock) and
+ // details are completed from the product data of the backend.
+ inventory_products?: MinimalInventoryProduct[];
+
+ // Specifies a lock identifier that was used to
+ // lock a product in the inventory. Only useful if
+ // manage_inventory is set. Used in case a frontend
+ // reserved quantities of the individual products while
+ // the shopping card was being built. Multiple UUIDs can
+ // be used in case different UUIDs were used for different
+ // products (i.e. in case the user started with multiple
+ // shopping sessions that were combined during checkout).
+ lock_uuids?: UUID[];
+
+ // Should a token for claiming the order be generated?
+ // False can make sense if the ORDER_ID is sufficiently
+ // high entropy to prevent adversarial claims (like it is
+ // if the backend auto-generates one). Default is 'true'.
+ create_token?: boolean;
+
+ // OTP device ID to associate with the order.
+ // This parameter is optional.
+ otp_id?: string;
+ }
+ type Order = MinimalOrderDetail | ContractTerms;
+
+ interface MinimalOrderDetail {
+ // Amount to be paid by the customer
+ amount: Amount;
+
+ // Short summary of the order
+ summary: string;
+
+ // URL that will show that the order was successful after
+ // it has been paid for. Optional. When POSTing to the
+ // merchant, the placeholder "${ORDER_ID}" will be
+ // replaced with the actual order ID (useful if the
+ // order ID is generated server-side and needs to be
+ // in the URL).
+ fulfillment_url?: string;
+ }
+
+ interface MinimalInventoryProduct {
+ // Which product is requested (here mandatory!)
+ product_id: string;
+
+ // How many units of the product are requested
+ quantity: Integer;
+ }
+ interface PostOrderResponse {
+ // Order ID of the response that was just created
+ order_id: string;
+
+ // Token that authorizes the wallet to claim the order.
+ // Provided only if "create_token" was set to 'true'
+ // in the request.
+ token?: ClaimToken;
+ }
+ interface OutOfStockResponse {
+ // Product ID of an out-of-stock item
+ product_id: string;
+
+ // Requested quantity
+ requested_quantity: Integer;
+
+ // Available quantity (must be below requested_quanitity)
+ available_quantity: Integer;
+
+ // When do we expect the product to be again in stock?
+ // Optional, not given if unknown.
+ restock_expected?: Timestamp;
+ }
+
+ interface ForgetRequest {
+ // Array of valid JSON paths to forgettable fields in the order's
+ // contract terms.
+ fields: string[];
+ }
+ interface RefundRequest {
+ // Amount to be refunded
+ refund: Amount;
+
+ // Human-readable refund justification
+ reason: string;
+ }
+ interface MerchantRefundResponse {
+ // URL (handled by the backend) that the wallet should access to
+ // trigger refund processing.
+ // taler://refund/...
+ taler_refund_uri: string;
+
+ // Contract hash that a client may need to authenticate an
+ // HTTP request to obtain the above URI in a wallet-friendly way.
+ h_contract: HashCode;
+ }
+ }
+
+ namespace Rewards {
+ // GET /private/reserves
+ interface RewardReserveStatus {
+ // Array of all known reserves (possibly empty!)
+ reserves: ReserveStatusEntry[];
+ }
+ interface ReserveStatusEntry {
+ // Public key of the reserve
+ reserve_pub: EddsaPublicKey;
+
+ // Timestamp when it was established
+ creation_time: Timestamp;
+
+ // Timestamp when it expires
+ expiration_time: Timestamp;
+
+ // Initial amount as per reserve creation call
+ merchant_initial_amount: Amount;
+
+ // Initial amount as per exchange, 0 if exchange did
+ // not confirm reserve creation yet.
+ exchange_initial_amount: Amount;
+
+ // Amount picked up so far.
+ pickup_amount: Amount;
+
+ // Amount approved for rewards that exceeds the pickup_amount.
+ committed_amount: Amount;
+
+ // Is this reserve active (false if it was deleted but not purged)
+ active: boolean;
+ }
+
+ interface ReserveCreateRequest {
+ // Amount that the merchant promises to put into the reserve
+ initial_balance: Amount;
+
+ // Exchange the merchant intends to use for reward
+ exchange_url: string;
+
+ // Desired wire method, for example "iban" or "x-taler-bank"
+ wire_method: string;
+ }
+ interface ReserveCreateConfirmation {
+ // Public key identifying the reserve
+ reserve_pub: EddsaPublicKey;
+
+ // Wire accounts of the exchange where to transfer the funds.
+ accounts: WireAccount[];
+ }
+ interface RewardCreateRequest {
+ // Amount that the customer should be reward
+ amount: Amount;
+
+ // Justification for giving the reward
+ justification: string;
+
+ // URL that the user should be directed to after rewarding,
+ // will be included in the reward_token.
+ next_url: string;
+ }
+ interface RewardCreateConfirmation {
+ // Unique reward identifier for the reward that was created.
+ reward_id: HashCode;
+
+ // taler://reward URI for the reward
+ taler_reward_uri: string;
+
+ // URL that will directly trigger processing
+ // the reward when the browser is redirected to it
+ reward_status_url: string;
+
+ // when does the reward expire
+ reward_expiration: Timestamp;
+ }
+
+ interface ReserveDetail {
+ // Timestamp when it was established.
+ creation_time: Timestamp;
+
+ // Timestamp when it expires.
+ expiration_time: Timestamp;
+
+ // Initial amount as per reserve creation call.
+ merchant_initial_amount: Amount;
+
+ // Initial amount as per exchange, 0 if exchange did
+ // not confirm reserve creation yet.
+ exchange_initial_amount: Amount;
+
+ // Amount picked up so far.
+ pickup_amount: Amount;
+
+ // Amount approved for rewards that exceeds the pickup_amount.
+ committed_amount: Amount;
+
+ // Array of all rewards created by this reserves (possibly empty!).
+ // Only present if asked for explicitly.
+ rewards?: RewardStatusEntry[];
+
+ // Is this reserve active (false if it was deleted but not purged)?
+ active: boolean;
+
+ // Array of wire accounts of the exchange that could
+ // be used to fill the reserve, can be NULL
+ // if the reserve is inactive or was already filled
+ accounts?: WireAccount[];
+
+ // URL of the exchange hosting the reserve,
+ // NULL if the reserve is inactive
+ exchange_url: string;
+ }
+
+ interface RewardStatusEntry {
+ // Unique identifier for the reward.
+ reward_id: HashCode;
+
+ // Total amount of the reward that can be withdrawn.
+ total_amount: Amount;
+
+ // Human-readable reason for why the reward was granted.
+ reason: string;
+ }
+
+ interface RewardDetails {
+ // Amount that we authorized for this reward.
+ total_authorized: Amount;
+
+ // Amount that was picked up by the user already.
+ total_picked_up: Amount;
+
+ // Human-readable reason given when authorizing the reward.
+ reason: string;
+
+ // Timestamp indicating when the reward is set to expire (may be in the past).
+ expiration: Timestamp;
+
+ // Reserve public key from which the reward is funded.
+ reserve_pub: EddsaPublicKey;
+
+ // Array showing the pickup operations of the wallet (possibly empty!).
+ // Only present if asked for explicitly.
+ pickups?: PickupDetail[];
+ }
+ interface PickupDetail {
+ // Unique identifier for the pickup operation.
+ pickup_id: HashCode;
+
+ // Number of planchets involved.
+ num_planchets: Integer;
+
+ // Total amount requested for this pickup_id.
+ requested_amount: Amount;
+ }
+ }
+
+ namespace Transfers {
+ interface TransferList {
+ // list of all the transfers that fit the filter that we know
+ transfers: TransferDetails[];
+ }
+ interface TransferDetails {
+ // how much was wired to the merchant (minus fees)
+ credit_amount: Amount;
+
+ // raw wire transfer identifier identifying the wire transfer (a base32-encoded value)
+ wtid: string;
+
+ // target account that received the wire transfer
+ payto_uri: string;
+
+ // base URL of the exchange that made the wire transfer
+ exchange_url: string;
+
+ // Serial number identifying the transfer in the merchant backend.
+ // Used for filgering via offset.
+ transfer_serial_id: number;
+
+ // Time of the execution of the wire transfer by the exchange, according to the exchange
+ // Only provided if we did get an answer from the exchange.
+ execution_time?: Timestamp;
+
+ // True if we checked the exchange's answer and are happy with it.
+ // False if we have an answer and are unhappy, missing if we
+ // do not have an answer from the exchange.
+ verified?: boolean;
+
+ // True if the merchant uses the POST /transfers API to confirm
+ // that this wire transfer took place (and it is thus not
+ // something merely claimed by the exchange).
+ confirmed?: boolean;
+ }
+
+ interface TransferInformation {
+ // how much was wired to the merchant (minus fees)
+ credit_amount: Amount;
+
+ // raw wire transfer identifier identifying the wire transfer (a base32-encoded value)
+ wtid: WireTransferIdentifierRawP;
+
+ // target account that received the wire transfer
+ payto_uri: string;
+
+ // base URL of the exchange that made the wire transfer
+ exchange_url: string;
+ }
+ }
+
+ namespace OTP {
+ interface OtpDeviceAddDetails {
+ // Device ID to use.
+ otp_device_id: string;
+
+ // Human-readable description for the device.
+ otp_device_description: string;
+
+ // A base64-encoded key
+ otp_key: string;
+
+ // Algorithm for computing the POS confirmation.
+ otp_algorithm: Integer;
+
+ // Counter for counter-based OTP devices.
+ otp_ctr?: Integer;
+ }
+
+ interface OtpDevicePatchDetails {
+ // Human-readable description for the device.
+ otp_device_description: string;
+
+ // A base64-encoded key
+ otp_key: string | undefined;
+
+ // Algorithm for computing the POS confirmation.
+ otp_algorithm: Integer;
+
+ // Counter for counter-based OTP devices.
+ otp_ctr?: Integer;
+ }
+
+ interface OtpDeviceSummaryResponse {
+ // Array of devices that are present in our backend.
+ otp_devices: OtpDeviceEntry[];
+ }
+ interface OtpDeviceEntry {
+ // Device identifier.
+ otp_device_id: string;
+
+ // Human-readable description for the device.
+ device_description: string;
+ }
+
+ interface OtpDeviceDetails {
+ // Human-readable description for the device.
+ device_description: string;
+
+ // Algorithm for computing the POS confirmation.
+ otp_algorithm: Integer;
+
+ // Counter for counter-based OTP devices.
+ otp_ctr?: Integer;
+ }
+
+
+ }
+ namespace Template {
+ interface TemplateAddDetails {
+ // Template ID to use.
+ template_id: string;
+
+ // Human-readable description for the template.
+ template_description: string;
+
+ // OTP device ID.
+ // This parameter is optional.
+ otp_id?: string;
+
+ // Additional information in a separate template.
+ template_contract: TemplateContractDetails;
+ }
+ interface TemplateContractDetails {
+ // Human-readable summary for the template.
+ summary?: string;
+
+ // The price is imposed by the merchant and cannot be changed by the customer.
+ // This parameter is optional.
+ amount?: Amount;
+
+ // Minimum age buyer must have (in years). Default is 0.
+ minimum_age: Integer;
+
+ // The time the customer need to pay before his order will be deleted.
+ // It is deleted if the customer did not pay and if the duration is over.
+ pay_duration: RelativeTime;
+ }
+ interface TemplatePatchDetails {
+ // Human-readable description for the template.
+ template_description: string;
+
+ // OTP device ID.
+ // This parameter is optional.
+ otp_id?: string;
+
+ // Additional information in a separate template.
+ template_contract: TemplateContractDetails;
+ }
+
+ interface TemplateSummaryResponse {
+ // List of templates that are present in our backend.
+ templates: TemplateEntry[];
+ }
+
+ interface TemplateEntry {
+ // Template identifier, as found in the template.
+ template_id: string;
+
+ // Human-readable description for the template.
+ template_description: string;
+ }
+
+ interface TemplateDetails {
+ // Human-readable description for the template.
+ template_description: string;
+
+ // OTP device ID.
+ // This parameter is optional.
+ otp_id?: string;
+
+ // Additional information in a separate template.
+ template_contract: TemplateContractDetails;
+ }
+
+ interface UsingTemplateDetails {
+ // Subject of the template
+ summary?: string;
+
+ // The amount entered by the customer.
+ amount?: Amount;
+ }
+
+ interface UsingTemplateResponse {
+ // After enter the request. The user will be pay with a taler URL.
+ order_id: string;
+ token: string;
+ }
+ }
+
+ namespace Webhooks {
+ type MerchantWebhookType = "pay" | "refund";
+ interface WebhookAddDetails {
+ // Webhook ID to use.
+ webhook_id: string;
+
+ // The event of the webhook: why the webhook is used.
+ event_type: MerchantWebhookType;
+
+ // URL of the webhook where the customer will be redirected.
+ url: string;
+
+ // Method used by the webhook
+ http_method: string;
+
+ // Header template of the webhook
+ header_template?: string;
+
+ // Body template by the webhook
+ body_template?: string;
+ }
+ interface WebhookPatchDetails {
+ // The event of the webhook: why the webhook is used.
+ event_type: string;
+
+ // URL of the webhook where the customer will be redirected.
+ url: string;
+
+ // Method used by the webhook
+ http_method: string;
+
+ // Header template of the webhook
+ header_template?: string;
+
+ // Body template by the webhook
+ body_template?: string;
+ }
+ interface WebhookSummaryResponse {
+ // List of webhooks that are present in our backend.
+ webhooks: WebhookEntry[];
+ }
+ interface WebhookEntry {
+ // Webhook identifier, as found in the webhook.
+ webhook_id: string;
+
+ // The event of the webhook: why the webhook is used.
+ event_type: string;
+ }
+ interface WebhookDetails {
+ // The event of the webhook: why the webhook is used.
+ event_type: string;
+
+ // URL of the webhook where the customer will be redirected.
+ url: string;
+
+ // Method used by the webhook
+ http_method: string;
+
+ // Header template of the webhook
+ header_template?: string;
+
+ // Body template by the webhook
+ body_template?: string;
+ }
+ }
+
+ interface ContractTerms {
+ // Human-readable description of the whole purchase
+ summary: string;
+
+ // Map from IETF BCP 47 language tags to localized summaries
+ summary_i18n?: { [lang_tag: string]: string };
+
+ // Unique, free-form identifier for the proposal.
+ // Must be unique within a merchant instance.
+ // For merchants that do not store proposals in their DB
+ // before the customer paid for them, the order_id can be used
+ // by the frontend to restore a proposal from the information
+ // encoded in it (such as a short product identifier and timestamp).
+ order_id: string;
+
+ // Total price for the transaction.
+ // The exchange will subtract deposit fees from that amount
+ // before transferring it to the merchant.
+ amount: Amount;
+
+ // The URL for this purchase. Every time is is visited, the merchant
+ // will send back to the customer the same proposal. Clearly, this URL
+ // can be bookmarked and shared by users.
+ fulfillment_url?: string;
+
+ // Maximum total deposit fee accepted by the merchant for this contract
+ max_fee: Amount;
+
+ // List of products that are part of the purchase (see Product).
+ products: Product[];
+
+ // Time when this contract was generated
+ timestamp: TalerProtocolTimestamp;
+
+ // After this deadline has passed, no refunds will be accepted.
+ refund_deadline: TalerProtocolTimestamp;
+
+ // After this deadline, the merchant won't accept payments for the contact
+ pay_deadline: TalerProtocolTimestamp;
+
+ // Transfer deadline for the exchange. Must be in the
+ // deposit permissions of coins used to pay for this order.
+ wire_transfer_deadline: TalerProtocolTimestamp;
+
+ // Merchant's public key used to sign this proposal; this information
+ // is typically added by the backend Note that this can be an ephemeral key.
+ merchant_pub: EddsaPublicKey;
+
+ // Base URL of the (public!) merchant backend API.
+ // Must be an absolute URL that ends with a slash.
+ merchant_base_url: string;
+
+ // More info about the merchant, see below
+ merchant: Merchant;
+
+ // The hash of the merchant instance's wire details.
+ h_wire: HashCode;
+
+ // Wire transfer method identifier for the wire method associated with h_wire.
+ // The wallet may only select exchanges via a matching auditor if the
+ // exchange also supports this wire method.
+ // The wire transfer fees must be added based on this wire transfer method.
+ wire_method: string;
+
+ // Any exchanges audited by these auditors are accepted by the merchant.
+ auditors: Auditor[];
+
+ // Exchanges that the merchant accepts even if it does not accept any auditors that audit them.
+ exchanges: Exchange[];
+
+ // Delivery location for (all!) products.
+ delivery_location?: Location;
+
+ // Time indicating when the order should be delivered.
+ // May be overwritten by individual products.
+ delivery_date?: TalerProtocolTimestamp;
+
+ // Nonce generated by the wallet and echoed by the merchant
+ // in this field when the proposal is generated.
+ nonce: string;
+
+ // Specifies for how long the wallet should try to get an
+ // automatic refund for the purchase. If this field is
+ // present, the wallet should wait for a few seconds after
+ // the purchase and then automatically attempt to obtain
+ // a refund. The wallet should probe until "delay"
+ // after the payment was successful (i.e. via long polling
+ // or via explicit requests with exponential back-off).
+ //
+ // In particular, if the wallet is offline
+ // at that time, it MUST repeat the request until it gets
+ // one response from the merchant after the delay has expired.
+ // If the refund is granted, the wallet MUST automatically
+ // recover the payment. This is used in case a merchant
+ // knows that it might be unable to satisfy the contract and
+ // desires for the wallet to attempt to get the refund without any
+ // customer interaction. Note that it is NOT an error if the
+ // merchant does not grant a refund.
+ auto_refund?: RelativeTime;
+
+ // Extra data that is only interpreted by the merchant frontend.
+ // Useful when the merchant needs to store extra information on a
+ // contract without storing it separately in their database.
+ extra?: any;
+
+ // Minimum age buyer must have (in years). Default is 0.
+ minimum_age?: Integer;
+ }
+}
diff --git a/packages/demobank-ui/src/hooks/async.ts b/packages/auditor-backoffice-ui/src/hooks/async.ts
index 090522d30..f22badc88 100644
--- a/packages/demobank-ui/src/hooks/async.ts
+++ b/packages/auditor-backoffice-ui/src/hooks/async.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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
@@ -19,7 +19,6 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
import { useState } from "preact/hooks";
-// import { cancelPendingRequest } from "./backend";
export interface Options {
slowTolerance: number;
@@ -46,6 +45,7 @@ export function useAsync<T>(
const request = async (...args: any) => {
if (!fn) return;
setLoading(true);
+
const handler = setTimeout(() => {
setSlow(true);
}, tooLong);
@@ -61,8 +61,7 @@ export function useAsync<T>(
clearTimeout(handler);
};
- function cancel() {
- // cancelPendingRequest()
+ function cancel(): void {
setLoading(false);
setSlow(false);
}
diff --git a/packages/auditor-backoffice-ui/src/hooks/backend.ts b/packages/auditor-backoffice-ui/src/hooks/backend.ts
new file mode 100644
index 000000000..8d99546a8
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/hooks/backend.ts
@@ -0,0 +1,477 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { AbsoluteTime, HttpStatusCode } from "@gnu-taler/taler-util";
+import {
+ ErrorType,
+ HttpError,
+ HttpResponse,
+ HttpResponseOk,
+ RequestError,
+ RequestOptions,
+ useApiContext,
+} from "@gnu-taler/web-util/browser";
+import { useCallback, useEffect, useState } from "preact/hooks";
+import { useSWRConfig } from "swr";
+import { useBackendContext } from "../context/backend.js";
+import { useInstanceContext } from "../context/instance.js";
+import { AccessToken, LoginToken, MerchantBackend, Timestamp } from "../declaration.js";
+
+
+export function useMatchMutate(): (
+ re?: RegExp,
+ value?: unknown,
+) => Promise<any> {
+ const { cache, mutate } = useSWRConfig();
+
+ if (!(cache instanceof Map)) {
+ throw new Error(
+ "matchMutate requires the cache provider to be a Map instance",
+ );
+ }
+
+ return function matchRegexMutate(re?: RegExp) {
+ return mutate((key) => {
+ // evict if no key or regex === all
+ if (!key || !re) return true
+ // match string
+ if (typeof key === 'string' && re.test(key)) return true
+ // record or object have the path at [0]
+ if (typeof key === 'object' && re.test(key[0])) return true
+ //key didn't match regex
+ return false
+ }, undefined, {
+ revalidate: true,
+ });
+ };
+}
+
+export function useBackendInstancesTestForAdmin(): HttpResponse<
+ MerchantBackend.Instances.InstancesResponse,
+ MerchantBackend.ErrorDetail
+> {
+ const { request } = useBackendBaseRequest();
+
+ type Type = MerchantBackend.Instances.InstancesResponse;
+
+ const [result, setResult] = useState<
+ HttpResponse<Type, MerchantBackend.ErrorDetail>
+ >({ loading: true });
+
+ useEffect(() => {
+ request<Type>(`/management/instances`)
+ .then((data) => setResult(data))
+ .catch((error: RequestError<MerchantBackend.ErrorDetail>) =>
+ setResult(error.cause),
+ );
+ }, [request]);
+
+ return result;
+}
+
+const CHECK_CONFIG_INTERVAL_OK = 5 * 60 * 1000;
+const CHECK_CONFIG_INTERVAL_FAIL = 2 * 1000;
+
+export function useBackendConfig(): HttpResponse<
+ MerchantBackend.VersionResponse | undefined,
+ RequestError<MerchantBackend.ErrorDetail>
+> {
+ const { request } = useBackendBaseRequest();
+
+ type Type = MerchantBackend.VersionResponse;
+ type State = { data: HttpResponse<Type, RequestError<MerchantBackend.ErrorDetail>>, timer: number }
+ const [result, setResult] = useState<State>({ data: { loading: true }, timer: 0 });
+
+ useEffect(() => {
+ if (result.timer) {
+ clearTimeout(result.timer)
+ }
+ function tryConfig(): void {
+ request<Type>(`/config`)
+ .then((data) => {
+ const timer: any = setTimeout(() => {
+ tryConfig()
+ }, CHECK_CONFIG_INTERVAL_OK)
+ setResult({ data, timer })
+ })
+ .catch((error) => {
+ const timer: any = setTimeout(() => {
+ tryConfig()
+ }, CHECK_CONFIG_INTERVAL_FAIL)
+ const data = error.cause
+ setResult({ data, timer })
+ });
+ }
+ tryConfig()
+ }, [request]);
+
+ return result.data;
+}
+
+interface useBackendInstanceRequestType {
+ request: <T>(
+ endpoint: string,
+ options?: RequestOptions,
+ ) => Promise<HttpResponseOk<T>>;
+ fetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>;
+ reserveDetailFetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>;
+ rewardsDetailFetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>;
+ multiFetcher: <T>(params: [url: string[]]) => Promise<HttpResponseOk<T>[]>;
+ orderFetcher: <T>(
+ params: [endpoint: string,
+ paid?: YesOrNo,
+ refunded?: YesOrNo,
+ wired?: YesOrNo,
+ searchDate?: Date,
+ delta?: number,]
+ ) => Promise<HttpResponseOk<T>>;
+ transferFetcher: <T>(
+ params: [endpoint: string,
+ payto_uri?: string,
+ verified?: string,
+ position?: string,
+ delta?: number,]
+ ) => Promise<HttpResponseOk<T>>;
+ templateFetcher: <T>(
+ params: [endpoint: string,
+ position?: string,
+ delta?: number]
+ ) => Promise<HttpResponseOk<T>>;
+ webhookFetcher: <T>(
+ params: [endpoint: string,
+ position?: string,
+ delta?: number]
+ ) => Promise<HttpResponseOk<T>>;
+}
+interface useBackendBaseRequestType {
+ request: <T>(
+ endpoint: string,
+ options?: RequestOptions,
+ ) => Promise<HttpResponseOk<T>>;
+}
+
+type YesOrNo = "yes" | "no";
+type LoginResult = {
+ valid: true;
+ token: string;
+ expiration: Timestamp;
+} | {
+ valid: false;
+ cause: HttpError<{}>;
+}
+
+export function useCredentialsChecker() {
+ const { request } = useApiContext();
+ //check against instance details endpoint
+ //while merchant backend doesn't have a login endpoint
+ async function requestNewLoginToken(
+ baseUrl: string,
+ token: AccessToken,
+ ): Promise<LoginResult> {
+ const data: MerchantBackend.Instances.LoginTokenRequest = {
+ scope: "write",
+ duration: {
+ d_us: "forever"
+ },
+ refreshable: true,
+ }
+ try {
+ const response = await request<MerchantBackend.Instances.LoginTokenSuccessResponse>(baseUrl, `/private/token`, {
+ method: "POST",
+ token,
+ data
+ });
+ return { valid: true, token: response.data.token, expiration: response.data.expiration };
+ } catch (error) {
+ if (error instanceof RequestError) {
+ return { valid: false, cause: error.cause };
+ }
+
+ return {
+ valid: false, cause: {
+ type: ErrorType.UNEXPECTED,
+ loading: false,
+ info: {
+ hasToken: true,
+ status: 0,
+ options: {},
+ url: `/private/token`,
+ payload: {}
+ },
+ exception: error,
+ message: (error instanceof Error ? error.message : "unpexepected error")
+ }
+ };
+ }
+ };
+
+ async function refreshLoginToken(
+ baseUrl: string,
+ token: LoginToken
+ ): Promise<LoginResult> {
+
+ if (AbsoluteTime.isExpired(AbsoluteTime.fromProtocolTimestamp(token.expiration))) {
+ return {
+ valid: false, cause: {
+ type: ErrorType.CLIENT,
+ status: HttpStatusCode.Unauthorized,
+ message: "login token expired, login again.",
+ info: {
+ hasToken: true,
+ status: 401,
+ options: {},
+ url: `/private/token`,
+ payload: {}
+ },
+ payload: {}
+ },
+ }
+ }
+
+ return requestNewLoginToken(baseUrl, token.token as AccessToken)
+ }
+ return { requestNewLoginToken, refreshLoginToken }
+}
+
+/**
+ *
+ * @param root the request is intended to the base URL and no the instance URL
+ * @returns request handler to
+ */
+export function useBackendBaseRequest(): useBackendBaseRequestType {
+ const { url: backend, token: loginToken } = useBackendContext();
+ const { request: requestHandler } = useApiContext();
+ const token = loginToken?.token;
+
+ const request = useCallback(
+ function requestImpl<T>(
+ endpoint: string,
+ options: RequestOptions = {},
+ ): Promise<HttpResponseOk<T>> {
+ return requestHandler<T>(backend, endpoint, { ...options, token }).then(res => {
+ return res
+ }).catch(err => {
+ throw err
+ });
+ },
+ [backend, token],
+ );
+
+ return { request };
+}
+
+export function useBackendInstanceRequest(): useBackendInstanceRequestType {
+ const { url: rootBackendUrl, token: rootToken } = useBackendContext();
+ const { token: instanceToken, id, admin } = useInstanceContext();
+ const { request: requestHandler } = useApiContext();
+
+ const { baseUrl, token: loginToken } = !admin
+ ? { baseUrl: rootBackendUrl, token: rootToken }
+ : { baseUrl: `${rootBackendUrl}/instances/${id}`, token: instanceToken };
+
+ const token = loginToken?.token;
+
+ const request = useCallback(
+ function requestImpl<T>(
+ endpoint: string,
+ options: RequestOptions = {},
+ ): Promise<HttpResponseOk<T>> {
+ return requestHandler<T>(baseUrl, endpoint, { token, ...options });
+ },
+ [baseUrl, token],
+ );
+
+ const multiFetcher = useCallback(
+ function multiFetcherImpl<T>(
+ args: [endpoints: string[]],
+ ): Promise<HttpResponseOk<T>[]> {
+ const [endpoints] = args
+ return Promise.all(
+ endpoints.map((endpoint) =>
+ requestHandler<T>(baseUrl, endpoint, { token }),
+ ),
+ );
+ },
+ [baseUrl, token],
+ );
+
+ const fetcher = useCallback(
+ function fetcherImpl<T>(endpoint: string): Promise<HttpResponseOk<T>> {
+ return requestHandler<T>(baseUrl, endpoint, { token });
+ },
+ [baseUrl, token],
+ );
+
+ const orderFetcher = useCallback(
+ function orderFetcherImpl<T>(
+ args: [endpoint: string,
+ paid?: YesOrNo,
+ refunded?: YesOrNo,
+ wired?: YesOrNo,
+ searchDate?: Date,
+ delta?: number,]
+ ): Promise<HttpResponseOk<T>> {
+ const [endpoint, paid, refunded, wired, searchDate, delta] = args
+ const date_s =
+ delta && delta < 0 && searchDate
+ ? Math.floor(searchDate.getTime() / 1000) + 1
+ : searchDate !== undefined ? Math.floor(searchDate.getTime() / 1000) : undefined;
+ const params: any = {};
+ if (paid !== undefined) params.paid = paid;
+ if (delta !== undefined) params.delta = delta;
+ if (refunded !== undefined) params.refunded = refunded;
+ if (wired !== undefined) params.wired = wired;
+ if (date_s !== undefined) params.date_s = date_s;
+ if (delta === 0) {
+ //in this case we can already assume the response
+ //and avoid network
+ return Promise.resolve({
+ ok: true,
+ data: { orders: [] } as T,
+ })
+ }
+ return requestHandler<T>(baseUrl, endpoint, { params, token });
+ },
+ [baseUrl, token],
+ );
+
+ const reserveDetailFetcher = useCallback(
+ function reserveDetailFetcherImpl<T>(
+ endpoint: string,
+ ): Promise<HttpResponseOk<T>> {
+ return requestHandler<T>(baseUrl, endpoint, {
+ params: {
+ rewards: "yes",
+ },
+ token,
+ });
+ },
+ [baseUrl, token],
+ );
+
+ const rewardsDetailFetcher = useCallback(
+ function rewardsDetailFetcherImpl<T>(
+ endpoint: string,
+ ): Promise<HttpResponseOk<T>> {
+ return requestHandler<T>(baseUrl, endpoint, {
+ params: {
+ pickups: "yes",
+ },
+ token,
+ });
+ },
+ [baseUrl, token],
+ );
+
+ const transferFetcher = useCallback(
+ function transferFetcherImpl<T>(
+ args: [endpoint: string,
+ payto_uri?: string,
+ verified?: string,
+ position?: string,
+ delta?: number,]
+ ): Promise<HttpResponseOk<T>> {
+ const [endpoint, payto_uri, verified, position, delta] = args
+ const params: any = {};
+ if (payto_uri !== undefined) params.payto_uri = payto_uri;
+ if (verified !== undefined) params.verified = verified;
+ if (delta === 0) {
+ //in this case we can already assume the response
+ //and avoid network
+ return Promise.resolve({
+ ok: true,
+ data: { transfers: [] } as T,
+ })
+ }
+ if (delta !== undefined) {
+ params.limit = delta;
+ }
+ if (position !== undefined) params.offset = position;
+
+ return requestHandler<T>(baseUrl, endpoint, { params, token });
+ },
+ [baseUrl, token],
+ );
+
+ const templateFetcher = useCallback(
+ function templateFetcherImpl<T>(
+ args: [endpoint: string,
+ position?: string,
+ delta?: number,]
+ ): Promise<HttpResponseOk<T>> {
+ const [endpoint, position, delta] = args
+ const params: any = {};
+ if (delta === 0) {
+ //in this case we can already assume the response
+ //and avoid network
+ return Promise.resolve({
+ ok: true,
+ data: { templates: [] } as T,
+ })
+ }
+ if (delta !== undefined) {
+ params.limit = delta;
+ }
+ if (position !== undefined) params.offset = position;
+
+ return requestHandler<T>(baseUrl, endpoint, { params, token });
+ },
+ [baseUrl, token],
+ );
+
+ const webhookFetcher = useCallback(
+ function webhookFetcherImpl<T>(
+ args: [endpoint: string,
+ position?: string,
+ delta?: number,]
+ ): Promise<HttpResponseOk<T>> {
+ const [endpoint, position, delta] = args
+ const params: any = {};
+ if (delta === 0) {
+ //in this case we can already assume the response
+ //and avoid network
+ return Promise.resolve({
+ ok: true,
+ data: { webhooks: [] } as T,
+ })
+ }
+ if (delta !== undefined) {
+ params.limit = delta;
+ }
+ if (position !== undefined) params.offset = position;
+
+ return requestHandler<T>(baseUrl, endpoint, { params, token });
+ },
+ [baseUrl, token],
+ );
+
+ return {
+ request,
+ fetcher,
+ multiFetcher,
+ orderFetcher,
+ reserveDetailFetcher,
+ rewardsDetailFetcher,
+ transferFetcher,
+ templateFetcher,
+ webhookFetcher,
+ };
+}
diff --git a/packages/auditor-backoffice-ui/src/hooks/bank.ts b/packages/auditor-backoffice-ui/src/hooks/bank.ts
new file mode 100644
index 000000000..03b064646
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/hooks/bank.ts
@@ -0,0 +1,217 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ HttpResponse,
+ HttpResponseOk,
+ HttpResponsePaginated,
+ RequestError,
+} from "@gnu-taler/web-util/browser";
+import { useEffect, useState } from "preact/hooks";
+import { MerchantBackend } from "../declaration.js";
+import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js";
+import { useBackendInstanceRequest, useMatchMutate } from "./backend.js";
+
+// FIX default import https://github.com/microsoft/TypeScript/issues/49189
+import _useSWR, { SWRHook } from "swr";
+const useSWR = _useSWR as unknown as SWRHook;
+
+// const MOCKED_ACCOUNTS: Record<string, MerchantBackend.BankAccounts.AccountAddDetails> = {
+// "hwire1": {
+// h_wire: "hwire1",
+// payto_uri: "payto://fake/iban/123",
+// salt: "qwe",
+// },
+// "hwire2": {
+// h_wire: "hwire2",
+// payto_uri: "payto://fake/iban/123",
+// salt: "qwe2",
+// },
+// }
+
+export function useBankAccountAPI(): BankAccountAPI {
+ const mutateAll = useMatchMutate();
+ const { request } = useBackendInstanceRequest();
+
+ const createBankAccount = async (
+ data: MerchantBackend.BankAccounts.AccountAddDetails,
+ ): Promise<HttpResponseOk<void>> => {
+ // MOCKED_ACCOUNTS[data.h_wire] = data
+ // return Promise.resolve({ ok: true, data: undefined });
+ const res = await request<void>(`/private/accounts`, {
+ method: "POST",
+ data,
+ });
+ await mutateAll(/.*private\/accounts.*/);
+ return res;
+ };
+
+ const updateBankAccount = async (
+ h_wire: string,
+ data: MerchantBackend.BankAccounts.AccountPatchDetails,
+ ): Promise<HttpResponseOk<void>> => {
+ // MOCKED_ACCOUNTS[h_wire].credit_facade_credentials = data.credit_facade_credentials
+ // MOCKED_ACCOUNTS[h_wire].credit_facade_url = data.credit_facade_url
+ // return Promise.resolve({ ok: true, data: undefined });
+ const res = await request<void>(`/private/accounts/${h_wire}`, {
+ method: "PATCH",
+ data,
+ });
+ await mutateAll(/.*private\/accounts.*/);
+ return res;
+ };
+
+ const deleteBankAccount = async (
+ h_wire: string,
+ ): Promise<HttpResponseOk<void>> => {
+ // delete MOCKED_ACCOUNTS[h_wire]
+ // return Promise.resolve({ ok: true, data: undefined });
+ const res = await request<void>(`/private/accounts/${h_wire}`, {
+ method: "DELETE",
+ });
+ await mutateAll(/.*private\/accounts.*/);
+ return res;
+ };
+
+ return {
+ createBankAccount,
+ updateBankAccount,
+ deleteBankAccount,
+ };
+}
+
+export interface BankAccountAPI {
+ createBankAccount: (
+ data: MerchantBackend.BankAccounts.AccountAddDetails,
+ ) => Promise<HttpResponseOk<void>>;
+ updateBankAccount: (
+ id: string,
+ data: MerchantBackend.BankAccounts.AccountPatchDetails,
+ ) => Promise<HttpResponseOk<void>>;
+ deleteBankAccount: (id: string) => Promise<HttpResponseOk<void>>;
+}
+
+export interface InstanceBankAccountFilter {
+}
+
+export function useInstanceBankAccounts(
+ args?: InstanceBankAccountFilter,
+ updatePosition?: (id: string) => void,
+): HttpResponsePaginated<
+ MerchantBackend.BankAccounts.AccountsSummaryResponse,
+ MerchantBackend.ErrorDetail
+> {
+ // return {
+ // ok: true,
+ // loadMore() { },
+ // loadMorePrev() { },
+ // data: {
+ // accounts: Object.values(MOCKED_ACCOUNTS).map(e => ({
+ // ...e,
+ // active: true,
+ // }))
+ // }
+ // }
+ const { fetcher } = useBackendInstanceRequest();
+
+ const [pageAfter, setPageAfter] = useState(1);
+
+ const totalAfter = pageAfter * PAGE_SIZE;
+ const {
+ data: afterData,
+ error: afterError,
+ isValidating: loadingAfter,
+ } = useSWR<
+ HttpResponseOk<MerchantBackend.BankAccounts.AccountsSummaryResponse>,
+ RequestError<MerchantBackend.ErrorDetail>
+ >([`/private/accounts`], fetcher);
+
+ const [lastAfter, setLastAfter] = useState<
+ HttpResponse<
+ MerchantBackend.BankAccounts.AccountsSummaryResponse,
+ MerchantBackend.ErrorDetail
+ >
+ >({ loading: true });
+ useEffect(() => {
+ if (afterData) setLastAfter(afterData);
+ }, [afterData /*, beforeData*/]);
+
+ if (afterError) return afterError.cause;
+
+ // if the query returns less that we ask, then we have reach the end or beginning
+ const isReachingEnd =
+ afterData && afterData.data.accounts.length < totalAfter;
+ const isReachingStart = false;
+
+ const pagination = {
+ isReachingEnd,
+ isReachingStart,
+ loadMore: () => {
+ if (!afterData || isReachingEnd) return;
+ if (afterData.data.accounts.length < MAX_RESULT_SIZE) {
+ setPageAfter(pageAfter + 1);
+ } else {
+ const from = `${afterData.data.accounts[afterData.data.accounts.length - 1]
+ .h_wire
+ }`;
+ if (from && updatePosition) updatePosition(from);
+ }
+ },
+ loadMorePrev: () => {
+ },
+ };
+
+ const accounts = !afterData ? [] : (afterData || lastAfter).data.accounts;
+ if (loadingAfter /* || loadingBefore */)
+ return { loading: true, data: { accounts } };
+ if (/*beforeData &&*/ afterData) {
+ return { ok: true, data: { accounts }, ...pagination };
+ }
+ return { loading: true };
+}
+
+export function useBankAccountDetails(
+ h_wire: string,
+): HttpResponse<
+ MerchantBackend.BankAccounts.BankAccountEntry,
+ MerchantBackend.ErrorDetail
+> {
+ // return {
+ // ok: true,
+ // data: {
+ // ...MOCKED_ACCOUNTS[h_wire],
+ // active: true,
+ // }
+ // }
+ const { fetcher } = useBackendInstanceRequest();
+
+ const { data, error, isValidating } = useSWR<
+ HttpResponseOk<MerchantBackend.BankAccounts.BankAccountEntry>,
+ RequestError<MerchantBackend.ErrorDetail>
+ >([`/private/accounts/${h_wire}`], fetcher, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ });
+
+ if (isValidating) return { loading: true, data: data?.data };
+ if (data) {
+ return data;
+ }
+ if (error) return error.cause;
+ return { loading: true };
+}
diff --git a/packages/auditor-backoffice-ui/src/hooks/deposit_confirmations.ts b/packages/auditor-backoffice-ui/src/hooks/deposit_confirmations.ts
new file mode 100644
index 000000000..e4ec9a2f2
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/hooks/deposit_confirmations.ts
@@ -0,0 +1,161 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ HttpResponse,
+ HttpResponseOk,
+ RequestError,
+} from "@gnu-taler/web-util/browser";
+import { AuditorBackend, MerchantBackend, WithId } from "../declaration.js";
+import { useBackendInstanceRequest, useMatchMutate } from "./backend.js";
+
+// FIX default import https://github.com/microsoft/TypeScript/issues/49189
+import _useSWR, { SWRHook, useSWRConfig } from "swr";
+const useSWR = _useSWR as unknown as SWRHook;
+
+export interface DepositConfirmationAPI {
+ getDepositConfirmation: (
+ id: string,
+ ) => Promise<void>;
+ createDepositConfirmation: (
+ data: MerchantBackend.Products.ProductAddDetail,
+ ) => Promise<void>;
+ updateDepositConfirmation: (
+ id: string,
+ data: MerchantBackend.Products.ProductPatchDetail,
+ ) => Promise<void>;
+ deleteDepositConfirmation: (id: string) => Promise<void>;
+}
+
+export function useDepositConfirmationAPI(): DepositConfirmationAPI {
+ const mutateAll = useMatchMutate();
+ const { mutate } = useSWRConfig();
+
+ const { request } = useBackendInstanceRequest();
+
+ const createDepositConfirmation = async (
+ data: MerchantBackend.Products.ProductAddDetail,
+ ): Promise<void> => {
+ const res = await request(`/private/products`, {
+ method: "POST",
+ data,
+ });
+
+ return await mutateAll(/.*\/private\/products.*/);
+ };
+
+ const updateDepositConfirmation = async (
+ productId: string,
+ data: MerchantBackend.Products.ProductPatchDetail,
+ ): Promise<void> => {
+ const r = await request(`/private/products/${productId}`, {
+ method: "PATCH",
+ data,
+ });
+
+ return await mutateAll(/.*\/private\/products.*/);
+ };
+
+ const deleteDepositConfirmation = async (productId: string): Promise<void> => {
+ await request(`/private/products/${productId}`, {
+ method: "DELETE",
+ });
+ await mutate([`/private/products`]);
+ };
+
+ const getDepositConfirmation = async (
+ serialId: string,
+ ): Promise<void> => {
+ await request(`/deposit-confirmation/${serialId}`, {
+ method: "GET",
+ });
+
+ return
+ };
+
+ return {createDepositConfirmation, updateDepositConfirmation, deleteDepositConfirmation, getDepositConfirmation};
+}
+
+export function useDepositConfirmation(): HttpResponse<
+ (AuditorBackend.DepositConfirmation.DepositConfirmationDetail & WithId)[],
+ AuditorBackend.ErrorDetail
+> {
+ const { fetcher, multiFetcher } = useBackendInstanceRequest();
+
+ const { data: list, error: listError } = useSWR<
+ HttpResponseOk<AuditorBackend.DepositConfirmation.DepositConfirmationList>,
+ RequestError<AuditorBackend.ErrorDetail>
+ >([`/deposit-confirmation`], fetcher, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ });
+
+ const paths = (list?.data.depositConfirmations || []).map(
+ (p) => `/deposit-confirmation/${p.serial_id}`,
+ );
+ const { data: depositConfirmations, error: depositConfirmationError } = useSWR<
+ HttpResponseOk<AuditorBackend.DepositConfirmation.DepositConfirmationDetail>[],
+ RequestError<AuditorBackend.ErrorDetail>
+ >([paths], multiFetcher, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ });
+
+ if (listError) return listError.cause;
+ if (depositConfirmationError) return depositConfirmationError.cause;
+
+ if (depositConfirmations) {
+ const dataWithId = depositConfirmations.map((d) => {
+ //take the id from the queried url
+ return {
+ ...d.data,
+ id: d.info?.url.replace(/.*\/deposit-confirmation\//, "") || "",
+ };
+ });
+ return { ok: true, data: dataWithId };
+ }
+ return { loading: true };
+}
+
+export function useDepositConfirmationDetails(
+ serialId: string,
+): HttpResponse<
+ AuditorBackend.DepositConfirmation.DepositConfirmationDetail,
+ AuditorBackend.ErrorDetail
+> {
+ const { fetcher } = useBackendInstanceRequest();
+
+ const { data, error, isValidating } = useSWR<
+ HttpResponseOk<AuditorBackend.DepositConfirmation.DepositConfirmationDetail>,
+ RequestError<AuditorBackend.ErrorDetail>
+ >([`/deposit-confirmation/${serialId}`], fetcher, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ });
+
+ if (isValidating) return { loading: true, data: data?.data };
+ if (data) return data;
+ if (error) return error.cause;
+ return { loading: true };
+}
diff --git a/packages/auditor-backoffice-ui/src/hooks/index.ts b/packages/auditor-backoffice-ui/src/hooks/index.ts
new file mode 100644
index 000000000..61afbc94a
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/hooks/index.ts
@@ -0,0 +1,151 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { buildCodecForObject, codecForMap, codecForString, codecForTimestamp } from "@gnu-taler/taler-util";
+import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser";
+import { StateUpdater, useEffect, useState } from "preact/hooks";
+import { LoginToken } from "../declaration.js";
+import { ValueOrFunction } from "../utils/types.js";
+import { useMatchMutate } from "./backend.js";
+
+const calculateRootPath = () => {
+ const rootPath =
+ typeof window !== undefined
+ ? window.location.origin + window.location.pathname
+ : "/";
+
+ /**
+ * By default, merchant backend serves the html content
+ * from the /webui root. This should cover most of the
+ * cases and the rootPath will be the merchant backend
+ * URL where the instances are
+ */
+ return rootPath.replace("/webui/", "");
+};
+
+const loginTokenCodec = buildCodecForObject<LoginToken>()
+ .property("token", codecForString())
+ .property("expiration", codecForTimestamp)
+ .build("loginToken")
+const TOKENS_KEY = buildStorageKey("auditor-token", codecForMap(loginTokenCodec));
+
+
+export function useBackendURL(
+ url?: string,
+): [string, StateUpdater<string>] {
+ const [value, setter] = useSimpleLocalStorage(
+ "auditor-base-url",
+ url || calculateRootPath(),
+ );
+
+ const checkedSetter = (v: ValueOrFunction<string>) => {
+ return setter((p) => (v instanceof Function ? v(p ?? "") : v).replace(/\/$/, ""));
+ };
+
+ return [value!, checkedSetter];
+}
+
+export function useBackendDefaultToken(
+): [LoginToken | undefined, ((d: LoginToken | undefined) => void)] {
+ const { update: setToken, value: tokenMap, reset } = useLocalStorage(TOKENS_KEY, {})
+
+ const tokenOfDefaultInstance = tokenMap["default"]
+ const clearCache = useMatchMutate()
+ useEffect(() => {
+ clearCache()
+ }, [tokenOfDefaultInstance])
+
+ function updateToken(
+ value: (LoginToken | undefined)
+ ): void {
+ if (value === undefined) {
+ reset()
+ } else {
+ const res = { ...tokenMap, "default": value }
+ setToken(res)
+ }
+ }
+ return [tokenMap["default"], updateToken];
+}
+
+export function useBackendInstanceToken(
+ id: string,
+): [LoginToken | undefined, ((d: LoginToken | undefined) => void)] {
+ const { update: setToken, value: tokenMap, reset } = useLocalStorage(TOKENS_KEY, {})
+ const [defaultToken, defaultSetToken] = useBackendDefaultToken();
+
+ // instance named 'default' use the default token
+ if (id === "default") {
+ return [defaultToken, defaultSetToken];
+ }
+ function updateToken(
+ value: (LoginToken | undefined)
+ ): void {
+ if (value === undefined) {
+ reset()
+ } else {
+ const res = { ...tokenMap, [id]: value }
+ setToken(res)
+ }
+ }
+
+ return [tokenMap[id], updateToken];
+}
+
+export function useLang(initial?: string): [string, StateUpdater<string>] {
+ const browserLang =
+ typeof window !== "undefined"
+ ? navigator.language || (navigator as any).userLanguage
+ : undefined;
+ const defaultLang = (browserLang || initial || "en").substring(0, 2);
+ return useSimpleLocalStorage("lang-preference", defaultLang) as [string, StateUpdater<string>];
+}
+
+export function useSimpleLocalStorage(
+ key: string,
+ initialValue?: string,
+): [string | undefined, StateUpdater<string | undefined>] {
+ const [storedValue, setStoredValue] = useState<string | undefined>(
+ (): string | undefined => {
+ return typeof window !== "undefined"
+ ? window.localStorage.getItem(key) || initialValue
+ : initialValue;
+ },
+ );
+
+ const setValue = (
+ value?: string | ((val?: string) => string | undefined),
+ ) => {
+ setStoredValue((p) => {
+ const toStore = value instanceof Function ? value(p) : value;
+ if (typeof window !== "undefined") {
+ if (!toStore) {
+ window.localStorage.removeItem(key);
+ } else {
+ window.localStorage.setItem(key, toStore);
+ }
+ }
+ return toStore;
+ });
+ };
+
+ return [storedValue, setValue];
+}
diff --git a/packages/auditor-backoffice-ui/src/hooks/instance.test.ts b/packages/auditor-backoffice-ui/src/hooks/instance.test.ts
new file mode 100644
index 000000000..ee1576764
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/hooks/instance.test.ts
@@ -0,0 +1,741 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import * as tests from "@gnu-taler/web-util/testing";
+import { expect } from "chai";
+import { AccessToken, MerchantBackend } from "../declaration.js";
+import {
+ useAdminAPI,
+ useBackendInstances,
+ useInstanceAPI,
+ useInstanceDetails,
+ useManagementAPI,
+} from "./instance.js";
+import { ApiMockEnvironment } from "./testing.js";
+import {
+ API_CREATE_INSTANCE,
+ API_DELETE_INSTANCE,
+ API_GET_CURRENT_INSTANCE,
+ API_LIST_INSTANCES,
+ API_NEW_LOGIN,
+ API_UPDATE_CURRENT_INSTANCE,
+ API_UPDATE_CURRENT_INSTANCE_AUTH,
+ API_UPDATE_INSTANCE_BY_ID,
+} from "./urls.js";
+
+describe("instance api interaction with details", () => {
+ it("should evict cache when updating an instance", async () => {
+ const env = new ApiMockEnvironment();
+
+ env.addRequestExpectation(API_GET_CURRENT_INSTANCE, {
+ response: {
+ name: "instance_name",
+ } as MerchantBackend.Instances.QueryInstancesResponse,
+ });
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const api = useInstanceAPI();
+ const query = useInstanceDetails();
+ return { query, api };
+ },
+ {},
+ [
+ ({ query, api }) => {
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ name: "instance_name",
+ });
+ env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE, {
+ request: {
+ name: "other_name",
+ } as MerchantBackend.Instances.InstanceReconfigurationMessage,
+ });
+ env.addRequestExpectation(API_GET_CURRENT_INSTANCE, {
+ response: {
+ name: "other_name",
+ } as MerchantBackend.Instances.QueryInstancesResponse,
+ });
+ api.updateInstance({
+ name: "other_name",
+ } as MerchantBackend.Instances.InstanceReconfigurationMessage);
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ name: "other_name",
+ });
+ },
+ ],
+ env.buildTestingContext(),
+ );
+
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+
+ it("should evict cache when setting the instance's token", async () => {
+ const env = new ApiMockEnvironment();
+
+ env.addRequestExpectation(API_GET_CURRENT_INSTANCE, {
+ response: {
+ name: "instance_name",
+ auth: {
+ method: "token",
+ // token: "not-secret",
+ },
+ } as MerchantBackend.Instances.QueryInstancesResponse,
+ });
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const api = useInstanceAPI();
+ const query = useInstanceDetails();
+ return { query, api };
+ },
+ {},
+ [
+ ({ query, api }) => {
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ name: "instance_name",
+ auth: {
+ method: "token",
+ },
+ });
+ env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE_AUTH, {
+ request: {
+ method: "token",
+ token: "secret",
+ } as MerchantBackend.Instances.InstanceAuthConfigurationMessage,
+ });
+ env.addRequestExpectation(API_NEW_LOGIN, {
+ auth: "secret",
+ request: {
+ scope: "write",
+ duration: {
+ "d_us": "forever",
+ },
+ refreshable: true,
+ },
+ });
+ env.addRequestExpectation(API_GET_CURRENT_INSTANCE, {
+ response: {
+ name: "instance_name",
+ auth: {
+ method: "token",
+ // token: "secret",
+ },
+ } as MerchantBackend.Instances.QueryInstancesResponse,
+ });
+ api.setNewAccessToken(undefined, "secret" as AccessToken);
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ name: "instance_name",
+ auth: {
+ method: "token",
+ // token: "secret",
+ },
+ });
+ },
+ ],
+ env.buildTestingContext(),
+ );
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+
+ it("should evict cache when clearing the instance's token", async () => {
+ const env = new ApiMockEnvironment();
+
+ env.addRequestExpectation(API_GET_CURRENT_INSTANCE, {
+ response: {
+ name: "instance_name",
+ auth: {
+ method: "token",
+ // token: "not-secret",
+ },
+ } as MerchantBackend.Instances.QueryInstancesResponse,
+ });
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const api = useInstanceAPI();
+ const query = useInstanceDetails();
+ return { query, api };
+ },
+ {},
+ [
+ ({ query, api }) => {
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ name: "instance_name",
+ auth: {
+ method: "token",
+ // token: "not-secret",
+ },
+ });
+ env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE_AUTH, {
+ request: {
+ method: "external",
+ } as MerchantBackend.Instances.InstanceAuthConfigurationMessage,
+ });
+ env.addRequestExpectation(API_GET_CURRENT_INSTANCE, {
+ response: {
+ name: "instance_name",
+ auth: {
+ method: "external",
+ },
+ } as MerchantBackend.Instances.QueryInstancesResponse,
+ });
+
+ api.clearAccessToken(undefined);
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ name: "instance_name",
+ auth: {
+ method: "external",
+ },
+ });
+ },
+ ],
+ env.buildTestingContext(),
+ );
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ // const { result, waitForNextUpdate } = renderHook(
+ // () => {
+ // const api = useInstanceAPI();
+ // const query = useInstanceDetails();
+
+ // return { query, api };
+ // },
+ // { wrapper: TestingContext }
+ // );
+
+ // expect(result.current).not.undefined;
+ // if (!result.current) {
+ // return;
+ // }
+ // expect(result.current.query.loading).true;
+
+ // await waitForNextUpdate({ timeout: 1 });
+
+ // expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+
+ // expect(result.current.query.loading).false;
+
+ // expect(result.current?.query.ok).true;
+ // if (!result.current?.query.ok) return;
+
+ // expect(result.current.query.data).equals({
+ // name: 'instance_name',
+ // auth: {
+ // method: 'token',
+ // token: 'not-secret',
+ // }
+ // });
+
+ // act(async () => {
+ // await result.current?.api.clearToken();
+ // });
+
+ // expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+
+ // expect(result.current.query.loading).false;
+
+ // await waitForNextUpdate({ timeout: 1 });
+
+ // expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+
+ // expect(result.current.query.loading).false;
+ // expect(result.current.query.ok).true;
+
+ // expect(result.current.query.data).equals({
+ // name: 'instance_name',
+ // auth: {
+ // method: 'external',
+ // }
+ // });
+ });
+});
+
+describe("instance admin api interaction with listing", () => {
+ it("should evict cache when creating a new instance", async () => {
+ const env = new ApiMockEnvironment();
+
+ env.addRequestExpectation(API_LIST_INSTANCES, {
+ response: {
+ instances: [
+ {
+ name: "instance_name",
+ } as MerchantBackend.Instances.Instance,
+ ],
+ },
+ });
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const api = useAdminAPI();
+ const query = useBackendInstances();
+ return { query, api };
+ },
+ {},
+ [
+ ({ query, api }) => {
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ instances: [
+ {
+ name: "instance_name",
+ },
+ ],
+ });
+
+ env.addRequestExpectation(API_CREATE_INSTANCE, {
+ request: {
+ name: "other_name",
+ } as MerchantBackend.Instances.InstanceConfigurationMessage,
+ });
+ env.addRequestExpectation(API_LIST_INSTANCES, {
+ response: {
+ instances: [
+ {
+ name: "instance_name",
+ } as MerchantBackend.Instances.Instance,
+ {
+ name: "other_name",
+ } as MerchantBackend.Instances.Instance,
+ ],
+ },
+ });
+
+ api.createInstance({
+ name: "other_name",
+ } as MerchantBackend.Instances.InstanceConfigurationMessage);
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ instances: [
+ {
+ name: "instance_name",
+ },
+ {
+ name: "other_name",
+ },
+ ],
+ });
+ },
+ ],
+ env.buildTestingContext(),
+ );
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+
+ it("should evict cache when deleting an instance", async () => {
+ const env = new ApiMockEnvironment();
+
+ env.addRequestExpectation(API_LIST_INSTANCES, {
+ response: {
+ instances: [
+ {
+ id: "default",
+ name: "instance_name",
+ } as MerchantBackend.Instances.Instance,
+ {
+ id: "the_id",
+ name: "second_instance",
+ } as MerchantBackend.Instances.Instance,
+ ],
+ },
+ });
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const api = useAdminAPI();
+ const query = useBackendInstances();
+ return { query, api };
+ },
+ {},
+ [
+ ({ query, api }) => {
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ instances: [
+ {
+ id: "default",
+ name: "instance_name",
+ },
+ {
+ id: "the_id",
+ name: "second_instance",
+ },
+ ],
+ });
+
+ env.addRequestExpectation(API_DELETE_INSTANCE("the_id"), {});
+ env.addRequestExpectation(API_LIST_INSTANCES, {
+ response: {
+ instances: [
+ {
+ id: "default",
+ name: "instance_name",
+ } as MerchantBackend.Instances.Instance,
+ ],
+ },
+ });
+
+ api.deleteInstance("the_id");
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ instances: [
+ {
+ id: "default",
+ name: "instance_name",
+ },
+ ],
+ });
+ },
+ ],
+ env.buildTestingContext(),
+ );
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+
+ // const { result, waitForNextUpdate } = renderHook(
+ // () => {
+ // const api = useAdminAPI();
+ // const query = useBackendInstances();
+
+ // return { query, api };
+ // },
+ // { wrapper: TestingContext }
+ // );
+
+ // expect(result.current).not.undefined;
+ // if (!result.current) {
+ // return;
+ // }
+ // expect(result.current.query.loading).true;
+
+ // await waitForNextUpdate({ timeout: 1 });
+
+ // expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+
+ // expect(result.current.query.loading).false;
+
+ // expect(result.current?.query.ok).true;
+ // if (!result.current?.query.ok) return;
+
+ // expect(result.current.query.data).equals({
+ // instances: [{
+ // id: 'default',
+ // name: 'instance_name'
+ // }, {
+ // id: 'the_id',
+ // name: 'second_instance'
+ // }]
+ // });
+
+ // env.addRequestExpectation(API_DELETE_INSTANCE('the_id'), {});
+
+ // act(async () => {
+ // await result.current?.api.deleteInstance('the_id');
+ // });
+
+ // expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+
+ // env.addRequestExpectation(API_LIST_INSTANCES, {
+ // response: {
+ // instances: [{
+ // id: 'default',
+ // name: 'instance_name'
+ // } as MerchantBackend.Instances.Instance]
+ // },
+ // });
+
+ // expect(result.current.query.loading).false;
+
+ // await waitForNextUpdate({ timeout: 1 });
+
+ // expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+
+ // expect(result.current.query.loading).false;
+ // expect(result.current.query.ok).true;
+
+ // expect(result.current.query.data).equals({
+ // instances: [{
+ // id: 'default',
+ // name: 'instance_name'
+ // }]
+ // });
+ });
+
+ it("should evict cache when deleting (purge) an instance", async () => {
+ const env = new ApiMockEnvironment();
+
+ env.addRequestExpectation(API_LIST_INSTANCES, {
+ response: {
+ instances: [
+ {
+ id: "default",
+ name: "instance_name",
+ } as MerchantBackend.Instances.Instance,
+ {
+ id: "the_id",
+ name: "second_instance",
+ } as MerchantBackend.Instances.Instance,
+ ],
+ },
+ });
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const api = useAdminAPI();
+ const query = useBackendInstances();
+ return { query, api };
+ },
+ {},
+ [
+ ({ query, api }) => {
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ instances: [
+ {
+ id: "default",
+ name: "instance_name",
+ },
+ {
+ id: "the_id",
+ name: "second_instance",
+ },
+ ],
+ });
+
+ env.addRequestExpectation(API_DELETE_INSTANCE("the_id"), {
+ qparam: {
+ purge: "YES",
+ },
+ });
+ env.addRequestExpectation(API_LIST_INSTANCES, {
+ response: {
+ instances: [
+ {
+ id: "default",
+ name: "instance_name",
+ } as MerchantBackend.Instances.Instance,
+ ],
+ },
+ });
+
+ api.purgeInstance("the_id");
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ instances: [
+ {
+ id: "default",
+ name: "instance_name",
+ },
+ ],
+ });
+ },
+ ],
+ env.buildTestingContext(),
+ );
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+});
+
+describe("instance management api interaction with listing", () => {
+ it("should evict cache when updating an instance", async () => {
+ const env = new ApiMockEnvironment();
+
+ env.addRequestExpectation(API_LIST_INSTANCES, {
+ response: {
+ instances: [
+ {
+ id: "managed",
+ name: "instance_name",
+ } as MerchantBackend.Instances.Instance,
+ ],
+ },
+ });
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const api = useManagementAPI("managed");
+ const query = useBackendInstances();
+ return { query, api };
+ },
+ {},
+ [
+ ({ query, api }) => {
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ instances: [
+ {
+ id: "managed",
+ name: "instance_name",
+ },
+ ],
+ });
+
+ env.addRequestExpectation(API_UPDATE_INSTANCE_BY_ID("managed"), {
+ request: {
+ name: "other_name",
+ } as MerchantBackend.Instances.InstanceReconfigurationMessage,
+ });
+ env.addRequestExpectation(API_LIST_INSTANCES, {
+ response: {
+ instances: [
+ {
+ id: "managed",
+ name: "other_name",
+ } as MerchantBackend.Instances.Instance,
+ ],
+ },
+ });
+
+ api.updateInstance({
+ name: "other_name",
+ } as MerchantBackend.Instances.InstanceConfigurationMessage);
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ instances: [
+ {
+ id: "managed",
+ name: "other_name",
+ },
+ ],
+ });
+ },
+ ],
+ env.buildTestingContext(),
+ );
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+});
diff --git a/packages/auditor-backoffice-ui/src/hooks/instance.ts b/packages/auditor-backoffice-ui/src/hooks/instance.ts
new file mode 100644
index 000000000..0677191db
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/hooks/instance.ts
@@ -0,0 +1,313 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ HttpResponse,
+ HttpResponseOk,
+ RequestError,
+} from "@gnu-taler/web-util/browser";
+import { useBackendContext } from "../context/backend.js";
+import { AccessToken, MerchantBackend } from "../declaration.js";
+import {
+ useBackendBaseRequest,
+ useBackendInstanceRequest,
+ useCredentialsChecker,
+ useMatchMutate,
+} from "./backend.js";
+
+// FIX default import https://github.com/microsoft/TypeScript/issues/49189
+import _useSWR, { SWRHook, useSWRConfig } from "swr";
+const useSWR = _useSWR as unknown as SWRHook;
+
+interface InstanceAPI {
+ updateInstance: (
+ data: MerchantBackend.Instances.InstanceReconfigurationMessage,
+ ) => Promise<void>;
+ deleteInstance: () => Promise<void>;
+ clearAccessToken: (currentToken: AccessToken | undefined) => Promise<void>;
+ setNewAccessToken: (currentToken: AccessToken | undefined, token: AccessToken) => Promise<void>;
+}
+
+export function useAdminAPI(): AdminAPI {
+ const { request } = useBackendBaseRequest();
+ const mutateAll = useMatchMutate();
+
+ const createInstance = async (
+ instance: MerchantBackend.Instances.InstanceConfigurationMessage,
+ ): Promise<void> => {
+ await request(`/management/instances`, {
+ method: "POST",
+ data: instance,
+ });
+
+ mutateAll(/\/management\/instances/);
+ };
+
+ const deleteInstance = async (id: string): Promise<void> => {
+ await request(`/management/instances/${id}`, {
+ method: "DELETE",
+ });
+
+ mutateAll(/\/management\/instances/);
+ };
+
+ const purgeInstance = async (id: string): Promise<void> => {
+ await request(`/management/instances/${id}`, {
+ method: "DELETE",
+ params: {
+ purge: "YES",
+ },
+ });
+
+ mutateAll(/\/management\/instances/);
+ };
+
+ return { createInstance, deleteInstance, purgeInstance };
+}
+
+export interface AdminAPI {
+ createInstance: (
+ data: MerchantBackend.Instances.InstanceConfigurationMessage,
+ ) => Promise<void>;
+ deleteInstance: (id: string) => Promise<void>;
+ purgeInstance: (id: string) => Promise<void>;
+}
+
+export function useManagementAPI(instanceId: string): InstanceAPI {
+ const mutateAll = useMatchMutate();
+ const { url: backendURL } = useBackendContext()
+ const { updateToken } = useBackendContext();
+ const { request } = useBackendBaseRequest();
+ const { requestNewLoginToken } = useCredentialsChecker()
+
+ const updateInstance = async (
+ instance: MerchantBackend.Instances.InstanceReconfigurationMessage,
+ ): Promise<void> => {
+ await request(`/management/instances/${instanceId}`, {
+ method: "PATCH",
+ data: instance,
+ });
+
+ mutateAll(/\/management\/instances/);
+ };
+
+ const deleteInstance = async (): Promise<void> => {
+ await request(`/management/instances/${instanceId}`, {
+ method: "DELETE",
+ });
+
+ mutateAll(/\/management\/instances/);
+ };
+
+ const clearAccessToken = async (currentToken: AccessToken | undefined): Promise<void> => {
+ await request(`/management/instances/${instanceId}/auth`, {
+ method: "POST",
+ token: currentToken,
+ data: { method: "external" },
+ });
+
+ mutateAll(/\/management\/instances/);
+ };
+
+ const setNewAccessToken = async (currentToken: AccessToken | undefined, newToken: AccessToken): Promise<void> => {
+ await request(`/management/instances/${instanceId}/auth`, {
+ method: "POST",
+ token: currentToken,
+ data: { method: "token", token: newToken },
+ });
+
+ const resp = await requestNewLoginToken(backendURL, newToken)
+ if (resp.valid) {
+ const { token, expiration } = resp
+ updateToken({ token, expiration });
+ } else {
+ updateToken(undefined)
+ }
+
+ mutateAll(/\/management\/instances/);
+ };
+
+ return { updateInstance, deleteInstance, setNewAccessToken, clearAccessToken };
+}
+
+export function useInstanceAPI(): InstanceAPI {
+ const { mutate } = useSWRConfig();
+ const { url: backendURL, updateToken } = useBackendContext()
+
+ const {
+ token: adminToken,
+ } = useBackendContext();
+ const { request } = useBackendInstanceRequest();
+ const { requestNewLoginToken } = useCredentialsChecker()
+
+ const updateInstance = async (
+ instance: MerchantBackend.Instances.InstanceReconfigurationMessage,
+ ): Promise<void> => {
+ await request(`/private/`, {
+ method: "PATCH",
+ data: instance,
+ });
+
+ if (adminToken) mutate(["/private/instances", adminToken, backendURL], null);
+ mutate([`/private/`], null);
+ };
+
+ const deleteInstance = async (): Promise<void> => {
+ await request(`/private/`, {
+ method: "DELETE",
+ // token: adminToken,
+ });
+
+ if (adminToken) mutate(["/private/instances", adminToken, backendURL], null);
+ mutate([`/private/`], null);
+ };
+
+ const clearAccessToken = async (currentToken: AccessToken | undefined): Promise<void> => {
+ await request(`/private/auth`, {
+ method: "POST",
+ token: currentToken,
+ data: { method: "external" },
+ });
+
+ mutate([`/private/`], null);
+ };
+
+ const setNewAccessToken = async (currentToken: AccessToken | undefined, newToken: AccessToken): Promise<void> => {
+ await request(`/private/auth`, {
+ method: "POST",
+ token: currentToken,
+ data: { method: "token", token: newToken },
+ });
+
+ const resp = await requestNewLoginToken(backendURL, newToken)
+ if (resp.valid) {
+ const { token, expiration } = resp
+ updateToken({ token, expiration });
+ } else {
+ updateToken(undefined)
+ }
+
+ mutate([`/private/`], null);
+ };
+
+ return { updateInstance, deleteInstance, setNewAccessToken, clearAccessToken };
+}
+
+export function useInstanceDetails(): HttpResponse<
+ MerchantBackend.Instances.QueryInstancesResponse,
+ MerchantBackend.ErrorDetail
+> {
+ const { fetcher } = useBackendInstanceRequest();
+
+ const { data, error, isValidating } = useSWR<
+ HttpResponseOk<MerchantBackend.Instances.QueryInstancesResponse>,
+ RequestError<MerchantBackend.ErrorDetail>
+ >([`/private/`], fetcher, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ revalidateIfStale: false,
+ errorRetryCount: 0,
+ errorRetryInterval: 1,
+ shouldRetryOnError: false,
+ });
+
+ if (isValidating) return { loading: true, data: data?.data };
+ if (data) return data;
+ if (error) return error.cause;
+ return { loading: true };
+}
+
+type KYCStatus =
+ | { type: "ok" }
+ | { type: "redirect"; status: MerchantBackend.KYC.AccountKycRedirects };
+
+export function useInstanceKYCDetails(): HttpResponse<
+ KYCStatus,
+ MerchantBackend.ErrorDetail
+> {
+ const { fetcher } = useBackendInstanceRequest();
+
+ const { data, error } = useSWR<
+ HttpResponseOk<MerchantBackend.KYC.AccountKycRedirects>,
+ RequestError<MerchantBackend.ErrorDetail>
+ >([`/private/kyc`], fetcher, {
+ refreshInterval: 60 * 1000,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateIfStale: false,
+ revalidateOnMount: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ errorRetryCount: 0,
+ errorRetryInterval: 1,
+ shouldRetryOnError: false,
+ });
+
+ if (data) {
+ if (data.info?.status === 202)
+ return { ok: true, data: { type: "redirect", status: data.data } };
+ return { ok: true, data: { type: "ok" } };
+ }
+ if (error) return error.cause;
+ return { loading: true };
+}
+
+export function useManagedInstanceDetails(
+ instanceId: string,
+): HttpResponse<
+ MerchantBackend.Instances.QueryInstancesResponse,
+ MerchantBackend.ErrorDetail
+> {
+ const { request } = useBackendBaseRequest();
+
+ const { data, error, isValidating } = useSWR<
+ HttpResponseOk<MerchantBackend.Instances.QueryInstancesResponse>,
+ RequestError<MerchantBackend.ErrorDetail>
+ >([`/management/instances/${instanceId}`], request, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ errorRetryCount: 0,
+ errorRetryInterval: 1,
+ shouldRetryOnError: false,
+ });
+
+ if (isValidating) return { loading: true, data: data?.data };
+ if (data) return data;
+ if (error) return error.cause;
+ return { loading: true };
+}
+
+export function useBackendInstances(): HttpResponse<
+ MerchantBackend.Instances.InstancesResponse,
+ MerchantBackend.ErrorDetail
+> {
+ const { request } = useBackendBaseRequest();
+
+ const { data, error, isValidating } = useSWR<
+ HttpResponseOk<MerchantBackend.Instances.InstancesResponse>,
+ RequestError<MerchantBackend.ErrorDetail>
+ >(["/management/instances"], request);
+
+ if (isValidating) return { loading: true, data: data?.data };
+ if (data) return data;
+ if (error) return error.cause;
+ return { loading: true };
+}
diff --git a/packages/merchant-backend-ui/src/hooks/listener.ts b/packages/auditor-backoffice-ui/src/hooks/listener.ts
index 231ed6c87..d101f7bb8 100644
--- a/packages/merchant-backend-ui/src/hooks/listener.ts
+++ b/packages/auditor-backoffice-ui/src/hooks/listener.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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
@@ -15,23 +15,38 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
import { useState } from "preact/hooks";
/**
- * returns subscriber and activator
- * subscriber will receive a method (listener) that will be call when the activator runs.
- * the result of calling the listener will be sent to @action
+ * 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>; };
+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>({});
/**
@@ -45,24 +60,26 @@ export function useListener<T, R = any>(action: (r: T) => Promise<R>): [undefine
toBeRan: () => {
const whatWeGetFromTheListener = listener();
return action(whatWeGetFromTheListener);
- }
+ },
});
} else {
setState({
- toBeRan: undefined
- })
+ 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;
+ const activator = state.toBeRan
+ ? async () => {
+ if (state.toBeRan) {
+ return state.toBeRan();
+ }
+ return Promise.reject();
+ }
+ : undefined;
return [activator, subscriber];
}
diff --git a/packages/merchant-backend-ui/src/hooks/notifications.ts b/packages/auditor-backoffice-ui/src/hooks/notifications.ts
index 1c0c37308..133ddd80b 100644
--- a/packages/merchant-backend-ui/src/hooks/notifications.ts
+++ b/packages/auditor-backoffice-ui/src/hooks/notifications.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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
@@ -15,12 +15,12 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
import { useState } from "preact/hooks";
-import { Notification } from '../utils/types';
+import { Notification } from "../utils/types.js";
interface Result {
notifications: Notification[];
@@ -28,21 +28,29 @@ interface Result {
removeNotification: (n: Notification) => void;
}
-type NotificationWithDate = Notification & { since: Date }
+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() })))
+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 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 }
+ setNotifications((ns: NotificationWithDate[]) =>
+ ns.filter((n) => n !== notif),
+ );
+ };
+ return { notifications, pushNotification, removeNotification };
}
diff --git a/packages/auditor-backoffice-ui/src/hooks/order.test.ts b/packages/auditor-backoffice-ui/src/hooks/order.test.ts
new file mode 100644
index 000000000..c243309a8
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/hooks/order.test.ts
@@ -0,0 +1,587 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import * as tests from "@gnu-taler/web-util/testing";
+import { expect } from "chai";
+import { MerchantBackend } from "../declaration.js";
+import { useInstanceOrders, useOrderAPI, useOrderDetails } from "./order.js";
+import { ApiMockEnvironment } from "./testing.js";
+import {
+ API_CREATE_ORDER,
+ API_DELETE_ORDER,
+ API_FORGET_ORDER_BY_ID,
+ API_GET_ORDER_BY_ID,
+ API_LIST_ORDERS,
+ API_REFUND_ORDER_BY_ID,
+} from "./urls.js";
+
+describe("order api interaction with listing", () => {
+ it("should evict cache when creating an order", async () => {
+ const env = new ApiMockEnvironment();
+
+ env.addRequestExpectation(API_LIST_ORDERS, {
+ qparam: { delta: -20, paid: "yes" },
+ response: {
+ orders: [{ order_id: "1" }, { order_id: "2" } as MerchantBackend.Orders.OrderHistoryEntry],
+ },
+ });
+
+ const newDate = (d: Date) => {
+ //console.log("new date", d);
+ };
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const query = useInstanceOrders({ paid: "yes" }, newDate);
+ const api = useOrderAPI();
+ return { query, api };
+ },
+ {},
+ [
+ ({ query, api }) => {
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).undefined;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ orders: [{ order_id: "1" }, { order_id: "2" }],
+ });
+
+ env.addRequestExpectation(API_CREATE_ORDER, {
+ request: {
+ order: { amount: "ARS:12", summary: "pay me" },
+ },
+ response: { order_id: "3" },
+ });
+
+ env.addRequestExpectation(API_LIST_ORDERS, {
+ qparam: { delta: -20, paid: "yes" },
+ response: {
+ orders: [{ order_id: "1" }, { order_id: "2" } as any, { order_id: "3" } as any],
+ },
+ });
+
+ api.createOrder({
+ order: { amount: "ARS:12", summary: "pay me" },
+ } as any);
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+
+ expect(query.loading).undefined;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ orders: [{ order_id: "1" }, { order_id: "2" }, { order_id: "3" }],
+ });
+ },
+ ],
+ env.buildTestingContext(),
+ );
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+
+ it("should evict cache when doing a refund", async () => {
+ const env = new ApiMockEnvironment();
+
+ env.addRequestExpectation(API_LIST_ORDERS, {
+ qparam: { delta: -20, paid: "yes" },
+ response: { orders: [{
+ order_id: "1",
+ amount: "EUR:12",
+ refundable: true,
+ } as MerchantBackend.Orders.OrderHistoryEntry] },
+ });
+
+ const newDate = (d: Date) => {
+ //console.log("new date", d);
+ };
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const query = useInstanceOrders({ paid: "yes" }, newDate);
+ const api = useOrderAPI();
+ return { query, api };
+ },
+ {},
+ [
+ ({ query, api }) => {
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+
+ expect(query.loading).undefined;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ orders: [
+ {
+ order_id: "1",
+ amount: "EUR:12",
+ refundable: true,
+ },
+ ],
+ });
+ env.addRequestExpectation(API_REFUND_ORDER_BY_ID("1"), {
+ request: {
+ reason: "double pay",
+ refund: "EUR:1",
+ },
+ });
+
+ env.addRequestExpectation(API_LIST_ORDERS, {
+ qparam: { delta: -20, paid: "yes" },
+ response: { orders: [
+ { order_id: "1", amount: "EUR:12", refundable: false } as any,
+ ] },
+ });
+
+ api.refundOrder("1", {
+ reason: "double pay",
+ refund: "EUR:1",
+ });
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+
+ expect(query.loading).undefined;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ orders: [
+ {
+ order_id: "1",
+ amount: "EUR:12",
+ refundable: false,
+ },
+ ],
+ });
+ },
+ ],
+ env.buildTestingContext(),
+ );
+
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+
+ it("should evict cache when deleting an order", async () => {
+ const env = new ApiMockEnvironment();
+
+ env.addRequestExpectation(API_LIST_ORDERS, {
+ qparam: { delta: -20, paid: "yes" },
+ response: {
+ orders: [{ order_id: "1" }, { order_id: "2" } as MerchantBackend.Orders.OrderHistoryEntry],
+ },
+ });
+
+ const newDate = (d: Date) => {
+ //console.log("new date", d);
+ };
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const query = useInstanceOrders({ paid: "yes" }, newDate);
+ const api = useOrderAPI();
+ return { query, api };
+ },
+ {},
+ [
+ ({ query, api }) => {
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).undefined;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ orders: [{ order_id: "1" }, { order_id: "2" }],
+ });
+
+ env.addRequestExpectation(API_DELETE_ORDER("1"), {});
+
+ env.addRequestExpectation(API_LIST_ORDERS, {
+ qparam: { delta: -20, paid: "yes" },
+ response: {
+ orders: [{ order_id: "2" } as any],
+ },
+ });
+
+ api.deleteOrder("1");
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).undefined;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ orders: [{ order_id: "2" }],
+ });
+ },
+ ],
+ env.buildTestingContext(),
+ );
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+});
+
+describe("order api interaction with details", () => {
+ it("should evict cache when doing a refund", async () => {
+ const env = new ApiMockEnvironment();
+
+ env.addRequestExpectation(API_GET_ORDER_BY_ID("1"), {
+ // qparam: { delta: 0, paid: "yes" },
+ response: {
+ summary: "description",
+ refund_amount: "EUR:0",
+ } as unknown as MerchantBackend.Orders.CheckPaymentPaidResponse,
+ });
+
+ const newDate = (d: Date) => {
+ //console.log("new date", d);
+ };
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const query = useOrderDetails("1");
+ const api = useOrderAPI();
+ return { query, api };
+ },
+ {},
+ [
+ ({ query, api }) => {
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ summary: "description",
+ refund_amount: "EUR:0",
+ });
+ env.addRequestExpectation(API_REFUND_ORDER_BY_ID("1"), {
+ request: {
+ reason: "double pay",
+ refund: "EUR:1",
+ },
+ });
+
+ env.addRequestExpectation(API_GET_ORDER_BY_ID("1"), {
+ response: {
+ summary: "description",
+ refund_amount: "EUR:1",
+ } as unknown as MerchantBackend.Orders.CheckPaymentPaidResponse,
+ });
+
+ api.refundOrder("1", {
+ reason: "double pay",
+ refund: "EUR:1",
+ });
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ summary: "description",
+ refund_amount: "EUR:1",
+ });
+ },
+ ],
+ env.buildTestingContext(),
+ );
+
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+
+ it("should evict cache when doing a forget", async () => {
+ const env = new ApiMockEnvironment();
+
+ env.addRequestExpectation(API_GET_ORDER_BY_ID("1"), {
+ // qparam: { delta: 0, paid: "yes" },
+ response: {
+ summary: "description",
+ refund_amount: "EUR:0",
+ } as unknown as MerchantBackend.Orders.CheckPaymentPaidResponse,
+ });
+
+ const newDate = (d: Date) => {
+ //console.log("new date", d);
+ };
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const query = useOrderDetails("1");
+ const api = useOrderAPI();
+ return { query, api };
+ },
+ {},
+ [
+ ({ query, api }) => {
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ summary: "description",
+ refund_amount: "EUR:0",
+ });
+ env.addRequestExpectation(API_FORGET_ORDER_BY_ID("1"), {
+ request: {
+ fields: ["$.summary"],
+ },
+ });
+
+ env.addRequestExpectation(API_GET_ORDER_BY_ID("1"), {
+ response: {
+ summary: undefined,
+ } as unknown as MerchantBackend.Orders.CheckPaymentPaidResponse,
+ });
+
+ api.forgetOrder("1", {
+ fields: ["$.summary"],
+ });
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ summary: undefined,
+ });
+ },
+ ],
+ env.buildTestingContext(),
+ );
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+});
+
+describe("order listing pagination", () => {
+ it("should not load more if has reach the end", async () => {
+ const env = new ApiMockEnvironment();
+ env.addRequestExpectation(API_LIST_ORDERS, {
+ qparam: { delta: 20, wired: "yes", date_s: 12 },
+ response: {
+ orders: [{ order_id: "1" } as any],
+ },
+ });
+
+ env.addRequestExpectation(API_LIST_ORDERS, {
+ qparam: { delta: -20, wired: "yes", date_s: 13 },
+ response: {
+ orders: [{ order_id: "2" } as any],
+ },
+ });
+
+ const newDate = (d: Date) => {
+ //console.log("new date", d);
+ };
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const date = new Date(12000);
+ const query = useInstanceOrders({ wired: "yes", date }, newDate);
+ const api = useOrderAPI();
+ return { query, api };
+ },
+ {},
+ [
+ ({ query, api }) => {
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).undefined;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ orders: [{ order_id: "1" }, { order_id: "2" }],
+ });
+ expect(query.isReachingEnd).true;
+ expect(query.isReachingStart).true;
+
+ // should not trigger new state update or query
+ query.loadMore();
+ query.loadMorePrev();
+ },
+ ],
+ env.buildTestingContext(),
+ );
+
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+
+ it("should load more if result brings more that PAGE_SIZE", async () => {
+ const env = new ApiMockEnvironment();
+
+ const ordersFrom0to20 = Array.from({ length: 20 }).map((e, i) => ({
+ order_id: String(i),
+ }));
+ const ordersFrom20to40 = Array.from({ length: 20 }).map((e, i) => ({
+ order_id: String(i + 20),
+ }));
+ const ordersFrom20to0 = [...ordersFrom0to20].reverse();
+
+ env.addRequestExpectation(API_LIST_ORDERS, {
+ qparam: { delta: 20, wired: "yes", date_s: 12 },
+ response: {
+ orders: ordersFrom0to20,
+ },
+ });
+
+ env.addRequestExpectation(API_LIST_ORDERS, {
+ qparam: { delta: -20, wired: "yes", date_s: 13 },
+ response: {
+ orders: ordersFrom20to40,
+ },
+ });
+
+ const newDate = (d: Date) => {
+ //console.log("new date", d);
+ };
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const date = new Date(12000);
+ const query = useInstanceOrders({ wired: "yes", date }, newDate);
+ const api = useOrderAPI();
+ return { query, api };
+ },
+ {},
+ [
+ ({ query, api }) => {
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).undefined;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ orders: [...ordersFrom20to0, ...ordersFrom20to40],
+ });
+ expect(query.isReachingEnd).false;
+ expect(query.isReachingStart).false;
+
+ env.addRequestExpectation(API_LIST_ORDERS, {
+ qparam: { delta: -40, wired: "yes", date_s: 13 },
+ response: {
+ orders: [...ordersFrom20to40, { order_id: "41" }],
+ },
+ });
+
+ query.loadMore();
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(query.loading).undefined;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ orders: [
+ ...ordersFrom20to0,
+ ...ordersFrom20to40,
+ { order_id: "41" },
+ ],
+ });
+
+ env.addRequestExpectation(API_LIST_ORDERS, {
+ qparam: { delta: 40, wired: "yes", date_s: 12 },
+ response: {
+ orders: [...ordersFrom0to20, { order_id: "-1" }],
+ },
+ });
+
+ query.loadMorePrev();
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(query.loading).undefined;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ orders: [
+ { order_id: "-1" },
+ ...ordersFrom20to0,
+ ...ordersFrom20to40,
+ { order_id: "41" },
+ ],
+ });
+ },
+ ],
+ env.buildTestingContext(),
+ );
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+});
diff --git a/packages/auditor-backoffice-ui/src/hooks/order.ts b/packages/auditor-backoffice-ui/src/hooks/order.ts
new file mode 100644
index 000000000..e7a893f2c
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/hooks/order.ts
@@ -0,0 +1,289 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ HttpResponse,
+ HttpResponseOk,
+ HttpResponsePaginated,
+ RequestError,
+} from "@gnu-taler/web-util/browser";
+import { useEffect, useState } from "preact/hooks";
+import { MerchantBackend } from "../declaration.js";
+import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js";
+import { useBackendInstanceRequest, useMatchMutate } from "./backend.js";
+
+// FIX default import https://github.com/microsoft/TypeScript/issues/49189
+import _useSWR, { SWRHook } from "swr";
+const useSWR = _useSWR as unknown as SWRHook;
+
+export interface OrderAPI {
+ //FIXME: add OutOfStockResponse on 410
+ createOrder: (
+ data: MerchantBackend.Orders.PostOrderRequest,
+ ) => Promise<HttpResponseOk<MerchantBackend.Orders.PostOrderResponse>>;
+ forgetOrder: (
+ id: string,
+ data: MerchantBackend.Orders.ForgetRequest,
+ ) => Promise<HttpResponseOk<void>>;
+ refundOrder: (
+ id: string,
+ data: MerchantBackend.Orders.RefundRequest,
+ ) => Promise<HttpResponseOk<MerchantBackend.Orders.MerchantRefundResponse>>;
+ deleteOrder: (id: string) => Promise<HttpResponseOk<void>>;
+ getPaymentURL: (id: string) => Promise<HttpResponseOk<string>>;
+}
+
+type YesOrNo = "yes" | "no";
+
+export function useOrderAPI(): OrderAPI {
+ const mutateAll = useMatchMutate();
+ const { request } = useBackendInstanceRequest();
+
+ const createOrder = async (
+ data: MerchantBackend.Orders.PostOrderRequest,
+ ): Promise<HttpResponseOk<MerchantBackend.Orders.PostOrderResponse>> => {
+ const res = await request<MerchantBackend.Orders.PostOrderResponse>(
+ `/private/orders`,
+ {
+ method: "POST",
+ data,
+ },
+ );
+ await mutateAll(/.*private\/orders.*/);
+ // mutate('')
+ return res;
+ };
+ const refundOrder = async (
+ orderId: string,
+ data: MerchantBackend.Orders.RefundRequest,
+ ): Promise<HttpResponseOk<MerchantBackend.Orders.MerchantRefundResponse>> => {
+ mutateAll(/@"\/private\/orders"@/);
+ const res = request<MerchantBackend.Orders.MerchantRefundResponse>(
+ `/private/orders/${orderId}/refund`,
+ {
+ method: "POST",
+ data,
+ },
+ );
+
+ // order list returns refundable information, so we must evict everything
+ await mutateAll(/.*private\/orders.*/);
+ return res;
+ };
+
+ const forgetOrder = async (
+ orderId: string,
+ data: MerchantBackend.Orders.ForgetRequest,
+ ): Promise<HttpResponseOk<void>> => {
+ mutateAll(/@"\/private\/orders"@/);
+ const res = request<void>(`/private/orders/${orderId}/forget`, {
+ method: "PATCH",
+ data,
+ });
+ // we may be forgetting some fields that are pare of the listing, so we must evict everything
+ await mutateAll(/.*private\/orders.*/);
+ return res;
+ };
+ const deleteOrder = async (
+ orderId: string,
+ ): Promise<HttpResponseOk<void>> => {
+ mutateAll(/@"\/private\/orders"@/);
+ const res = request<void>(`/private/orders/${orderId}`, {
+ method: "DELETE",
+ });
+ await mutateAll(/.*private\/orders.*/);
+ return res;
+ };
+
+ const getPaymentURL = async (
+ orderId: string,
+ ): Promise<HttpResponseOk<string>> => {
+ return request<MerchantBackend.Orders.MerchantOrderStatusResponse>(
+ `/private/orders/${orderId}`,
+ {
+ method: "GET",
+ },
+ ).then((res) => {
+ const url =
+ res.data.order_status === "unpaid"
+ ? res.data.taler_pay_uri
+ : res.data.contract_terms.fulfillment_url;
+ const response: HttpResponseOk<string> = res as any;
+ response.data = url || "";
+ return response;
+ });
+ };
+
+ return { createOrder, forgetOrder, deleteOrder, refundOrder, getPaymentURL };
+}
+
+export function useOrderDetails(
+ oderId: string,
+): HttpResponse<
+ MerchantBackend.Orders.MerchantOrderStatusResponse,
+ MerchantBackend.ErrorDetail
+> {
+ const { fetcher } = useBackendInstanceRequest();
+
+ const { data, error, isValidating } = useSWR<
+ HttpResponseOk<MerchantBackend.Orders.MerchantOrderStatusResponse>,
+ RequestError<MerchantBackend.ErrorDetail>
+ >([`/private/orders/${oderId}`], fetcher, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ });
+
+ if (isValidating) return { loading: true, data: data?.data };
+ if (data) return data;
+ if (error) return error.cause;
+ return { loading: true };
+}
+
+export interface InstanceOrderFilter {
+ paid?: YesOrNo;
+ refunded?: YesOrNo;
+ wired?: YesOrNo;
+ date?: Date;
+}
+
+export function useInstanceOrders(
+ args?: InstanceOrderFilter,
+ updateFilter?: (d: Date) => void,
+): HttpResponsePaginated<
+ MerchantBackend.Orders.OrderHistory,
+ MerchantBackend.ErrorDetail
+> {
+ const { orderFetcher } = useBackendInstanceRequest();
+
+ const [pageBefore, setPageBefore] = useState(1);
+ const [pageAfter, setPageAfter] = useState(1);
+
+ const totalAfter = pageAfter * PAGE_SIZE;
+ const totalBefore = args?.date ? pageBefore * PAGE_SIZE : 0;
+
+ /**
+ * FIXME: this can be cleaned up a little
+ *
+ * the logic of double query should be inside the orderFetch so from the hook perspective and cache
+ * is just one query and one error status
+ */
+ const {
+ data: beforeData,
+ error: beforeError,
+ isValidating: loadingBefore,
+ } = useSWR<
+ HttpResponseOk<MerchantBackend.Orders.OrderHistory>,
+ RequestError<MerchantBackend.ErrorDetail>
+ >(
+ [
+ `/private/orders`,
+ args?.paid,
+ args?.refunded,
+ args?.wired,
+ args?.date,
+ totalBefore,
+ ],
+ orderFetcher,
+ );
+ const {
+ data: afterData,
+ error: afterError,
+ isValidating: loadingAfter,
+ } = useSWR<
+ HttpResponseOk<MerchantBackend.Orders.OrderHistory>,
+ RequestError<MerchantBackend.ErrorDetail>
+ >(
+ [
+ `/private/orders`,
+ args?.paid,
+ args?.refunded,
+ args?.wired,
+ args?.date,
+ -totalAfter,
+ ],
+ orderFetcher,
+ );
+
+ //this will save last result
+ const [lastBefore, setLastBefore] = useState<
+ HttpResponse<
+ MerchantBackend.Orders.OrderHistory,
+ MerchantBackend.ErrorDetail
+ >
+ >({ loading: true });
+ const [lastAfter, setLastAfter] = useState<
+ HttpResponse<
+ MerchantBackend.Orders.OrderHistory,
+ MerchantBackend.ErrorDetail
+ >
+ >({ loading: true });
+ useEffect(() => {
+ if (afterData) setLastAfter(afterData);
+ if (beforeData) setLastBefore(beforeData);
+ }, [afterData, beforeData]);
+
+ if (beforeError) return beforeError.cause;
+ if (afterError) return afterError.cause;
+
+ // if the query returns less that we ask, then we have reach the end or beginning
+ const isReachingEnd = afterData && afterData.data.orders.length < totalAfter;
+ const isReachingStart =
+ args?.date === undefined ||
+ (beforeData && beforeData.data.orders.length < totalBefore);
+
+ const pagination = {
+ isReachingEnd,
+ isReachingStart,
+ loadMore: () => {
+ if (!afterData || isReachingEnd) return;
+ if (afterData.data.orders.length < MAX_RESULT_SIZE) {
+ setPageAfter(pageAfter + 1);
+ } else {
+ const from =
+ afterData.data.orders[afterData.data.orders.length - 1].timestamp.t_s;
+ if (from && from !== "never" && updateFilter)
+ updateFilter(new Date(from * 1000));
+ }
+ },
+ loadMorePrev: () => {
+ if (!beforeData || isReachingStart) return;
+ if (beforeData.data.orders.length < MAX_RESULT_SIZE) {
+ setPageBefore(pageBefore + 1);
+ } else if (beforeData) {
+ const from =
+ beforeData.data.orders[beforeData.data.orders.length - 1].timestamp
+ .t_s;
+ if (from && from !== "never" && updateFilter)
+ updateFilter(new Date(from * 1000));
+ }
+ },
+ };
+
+ const orders =
+ !beforeData || !afterData
+ ? []
+ : (beforeData || lastBefore).data.orders
+ .slice()
+ .reverse()
+ .concat((afterData || lastAfter).data.orders);
+ if (loadingAfter || loadingBefore) return { loading: true, data: { orders } };
+ if (beforeData && afterData) {
+ return { ok: true, data: { orders }, ...pagination };
+ }
+ return { loading: true };
+}
diff --git a/packages/auditor-backoffice-ui/src/hooks/otp.ts b/packages/auditor-backoffice-ui/src/hooks/otp.ts
new file mode 100644
index 000000000..b045e365a
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/hooks/otp.ts
@@ -0,0 +1,223 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ HttpResponse,
+ HttpResponseOk,
+ HttpResponsePaginated,
+ RequestError,
+} from "@gnu-taler/web-util/browser";
+import { useEffect, useState } from "preact/hooks";
+import { MerchantBackend } from "../declaration.js";
+import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js";
+import { useBackendInstanceRequest, useMatchMutate } from "./backend.js";
+
+// FIX default import https://github.com/microsoft/TypeScript/issues/49189
+import _useSWR, { SWRHook } from "swr";
+const useSWR = _useSWR as unknown as SWRHook;
+
+const MOCKED_DEVICES: Record<string, MerchantBackend.OTP.OtpDeviceAddDetails> = {
+ "1": {
+ otp_device_description: "first device",
+ otp_algorithm: 1,
+ otp_device_id: "1",
+ otp_key: "123",
+ },
+ "2": {
+ otp_device_description: "second device",
+ otp_algorithm: 0,
+ otp_device_id: "2",
+ otp_key: "456",
+ }
+}
+
+export function useOtpDeviceAPI(): OtpDeviceAPI {
+ const mutateAll = useMatchMutate();
+ const { request } = useBackendInstanceRequest();
+
+ const createOtpDevice = async (
+ data: MerchantBackend.OTP.OtpDeviceAddDetails,
+ ): Promise<HttpResponseOk<void>> => {
+ // MOCKED_DEVICES[data.otp_device_id] = data
+ // return Promise.resolve({ ok: true, data: undefined });
+ const res = await request<void>(`/private/otp-devices`, {
+ method: "POST",
+ data,
+ });
+ await mutateAll(/.*private\/otp-devices.*/);
+ return res;
+ };
+
+ const updateOtpDevice = async (
+ deviceId: string,
+ data: MerchantBackend.OTP.OtpDevicePatchDetails,
+ ): Promise<HttpResponseOk<void>> => {
+ // MOCKED_DEVICES[deviceId].otp_algorithm = data.otp_algorithm
+ // MOCKED_DEVICES[deviceId].otp_ctr = data.otp_ctr
+ // MOCKED_DEVICES[deviceId].otp_device_description = data.otp_device_description
+ // MOCKED_DEVICES[deviceId].otp_key = data.otp_key
+ // return Promise.resolve({ ok: true, data: undefined });
+ const res = await request<void>(`/private/otp-devices/${deviceId}`, {
+ method: "PATCH",
+ data,
+ });
+ await mutateAll(/.*private\/otp-devices.*/);
+ return res;
+ };
+
+ const deleteOtpDevice = async (
+ deviceId: string,
+ ): Promise<HttpResponseOk<void>> => {
+ // delete MOCKED_DEVICES[deviceId]
+ // return Promise.resolve({ ok: true, data: undefined });
+ const res = await request<void>(`/private/otp-devices/${deviceId}`, {
+ method: "DELETE",
+ });
+ await mutateAll(/.*private\/otp-devices.*/);
+ return res;
+ };
+
+ return {
+ createOtpDevice,
+ updateOtpDevice,
+ deleteOtpDevice,
+ };
+}
+
+export interface OtpDeviceAPI {
+ createOtpDevice: (
+ data: MerchantBackend.OTP.OtpDeviceAddDetails,
+ ) => Promise<HttpResponseOk<void>>;
+ updateOtpDevice: (
+ id: string,
+ data: MerchantBackend.OTP.OtpDevicePatchDetails,
+ ) => Promise<HttpResponseOk<void>>;
+ deleteOtpDevice: (id: string) => Promise<HttpResponseOk<void>>;
+}
+
+export interface InstanceOtpDeviceFilter {
+}
+
+export function useInstanceOtpDevices(
+ args?: InstanceOtpDeviceFilter,
+ updatePosition?: (id: string) => void,
+): HttpResponsePaginated<
+ MerchantBackend.OTP.OtpDeviceSummaryResponse,
+ MerchantBackend.ErrorDetail
+> {
+ // return {
+ // ok: true,
+ // loadMore: () => { },
+ // loadMorePrev: () => { },
+ // data: {
+ // otp_devices: Object.values(MOCKED_DEVICES).map(d => ({
+ // device_description: d.otp_device_description,
+ // otp_device_id: d.otp_device_id
+ // }))
+ // }
+ // }
+
+ const { fetcher } = useBackendInstanceRequest();
+
+ const [pageAfter, setPageAfter] = useState(1);
+
+ const totalAfter = pageAfter * PAGE_SIZE;
+ const {
+ data: afterData,
+ error: afterError,
+ isValidating: loadingAfter,
+ } = useSWR<
+ HttpResponseOk<MerchantBackend.OTP.OtpDeviceSummaryResponse>,
+ RequestError<MerchantBackend.ErrorDetail>
+ >([`/private/otp-devices`], fetcher);
+
+ const [lastAfter, setLastAfter] = useState<
+ HttpResponse<
+ MerchantBackend.OTP.OtpDeviceSummaryResponse,
+ MerchantBackend.ErrorDetail
+ >
+ >({ loading: true });
+ useEffect(() => {
+ if (afterData) setLastAfter(afterData);
+ }, [afterData /*, beforeData*/]);
+
+ if (afterError) return afterError.cause;
+
+ // if the query returns less that we ask, then we have reach the end or beginning
+ const isReachingEnd =
+ afterData && afterData.data.otp_devices.length < totalAfter;
+ const isReachingStart = true;
+
+ const pagination = {
+ isReachingEnd,
+ isReachingStart,
+ loadMore: () => {
+ if (!afterData || isReachingEnd) return;
+ if (afterData.data.otp_devices.length < MAX_RESULT_SIZE) {
+ setPageAfter(pageAfter + 1);
+ } else {
+ const from = `${afterData.data.otp_devices[afterData.data.otp_devices.length - 1]
+ .otp_device_id
+ }`;
+ if (from && updatePosition) updatePosition(from);
+ }
+ },
+ loadMorePrev: () => {
+ },
+ };
+
+ const otp_devices = !afterData ? [] : (afterData || lastAfter).data.otp_devices;
+ if (loadingAfter /* || loadingBefore */)
+ return { loading: true, data: { otp_devices } };
+ if (/*beforeData &&*/ afterData) {
+ return { ok: true, data: { otp_devices }, ...pagination };
+ }
+ return { loading: true };
+}
+
+export function useOtpDeviceDetails(
+ deviceId: string,
+): HttpResponse<
+ MerchantBackend.OTP.OtpDeviceDetails,
+ MerchantBackend.ErrorDetail
+> {
+ // return {
+ // ok: true,
+ // data: {
+ // device_description: MOCKED_DEVICES[deviceId].otp_device_description,
+ // otp_algorithm: MOCKED_DEVICES[deviceId].otp_algorithm,
+ // otp_ctr: MOCKED_DEVICES[deviceId].otp_ctr
+ // }
+ // }
+ const { fetcher } = useBackendInstanceRequest();
+
+ const { data, error, isValidating } = useSWR<
+ HttpResponseOk<MerchantBackend.OTP.OtpDeviceDetails>,
+ RequestError<MerchantBackend.ErrorDetail>
+ >([`/private/otp-devices/${deviceId}`], fetcher, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ });
+
+ if (isValidating) return { loading: true, data: data?.data };
+ if (data) {
+ return data;
+ }
+ if (error) return error.cause;
+ return { loading: true };
+}
diff --git a/packages/auditor-backoffice-ui/src/hooks/product.test.ts b/packages/auditor-backoffice-ui/src/hooks/product.test.ts
new file mode 100644
index 000000000..7cac10e25
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/hooks/product.test.ts
@@ -0,0 +1,362 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import * as tests from "@gnu-taler/web-util/testing";
+import { expect } from "chai";
+import { MerchantBackend } from "../declaration.js";
+import {
+ useInstanceProducts,
+ useProductAPI,
+ useProductDetails,
+} from "./product.js";
+import { ApiMockEnvironment } from "./testing.js";
+import {
+ API_CREATE_PRODUCT,
+ API_DELETE_PRODUCT,
+ API_GET_PRODUCT_BY_ID,
+ API_LIST_PRODUCTS,
+ API_UPDATE_PRODUCT_BY_ID,
+} from "./urls.js";
+
+describe("product api interaction with listing", () => {
+ it("should evict cache when creating a product", async () => {
+ const env = new ApiMockEnvironment();
+
+ env.addRequestExpectation(API_LIST_PRODUCTS, {
+ response: {
+ products: [{ product_id: "1234" }],
+ },
+ });
+ env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), {
+ response: { price: "ARS:12" } as MerchantBackend.Products.ProductDetail,
+ });
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const query = useInstanceProducts();
+ const api = useProductAPI();
+ return { query, api };
+ },
+ {},
+ [
+ ({ query, api }) => {
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(query.loading).undefined;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals([{ id: "1234", price: "ARS:12" }]);
+
+ env.addRequestExpectation(API_CREATE_PRODUCT, {
+ request: {
+ price: "ARS:23",
+ } as MerchantBackend.Products.ProductAddDetail,
+ });
+
+ env.addRequestExpectation(API_LIST_PRODUCTS, {
+ response: {
+ products: [{ product_id: "1234" }, { product_id: "2345" }],
+ },
+ });
+ env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), {
+ response: {
+ price: "ARS:12",
+ } as MerchantBackend.Products.ProductDetail,
+ });
+ env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), {
+ response: {
+ price: "ARS:12",
+ } as MerchantBackend.Products.ProductDetail,
+ });
+ env.addRequestExpectation(API_GET_PRODUCT_BY_ID("2345"), {
+ response: {
+ price: "ARS:23",
+ } as MerchantBackend.Products.ProductDetail,
+ });
+
+ api.createProduct({
+ price: "ARS:23",
+ } as any);
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).undefined;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals([
+ {
+ id: "1234",
+ price: "ARS:12",
+ },
+ {
+ id: "2345",
+ price: "ARS:23",
+ },
+ ]);
+ },
+ ],
+ env.buildTestingContext(),
+ );
+
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+
+ it("should evict cache when updating a product", async () => {
+ const env = new ApiMockEnvironment();
+
+ env.addRequestExpectation(API_LIST_PRODUCTS, {
+ response: {
+ products: [{ product_id: "1234" }],
+ },
+ });
+ env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), {
+ response: { price: "ARS:12" } as MerchantBackend.Products.ProductDetail,
+ });
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const query = useInstanceProducts();
+ const api = useProductAPI();
+ return { query, api };
+ },
+ {},
+ [
+ ({ query, api }) => {
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).undefined;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals([{ id: "1234", price: "ARS:12" }]);
+
+ env.addRequestExpectation(API_UPDATE_PRODUCT_BY_ID("1234"), {
+ request: {
+ price: "ARS:13",
+ } as MerchantBackend.Products.ProductPatchDetail,
+ });
+
+ env.addRequestExpectation(API_LIST_PRODUCTS, {
+ response: {
+ products: [{ product_id: "1234" }],
+ },
+ });
+ env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), {
+ response: {
+ price: "ARS:13",
+ } as MerchantBackend.Products.ProductDetail,
+ });
+
+ api.updateProduct("1234", {
+ price: "ARS:13",
+ } as any);
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).undefined;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals([
+ {
+ id: "1234",
+ price: "ARS:13",
+ },
+ ]);
+ },
+ ],
+ env.buildTestingContext(),
+ );
+
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+
+ it("should evict cache when deleting a product", async () => {
+ const env = new ApiMockEnvironment();
+
+ env.addRequestExpectation(API_LIST_PRODUCTS, {
+ response: {
+ products: [{ product_id: "1234" }, { product_id: "2345" }],
+ },
+ });
+ env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), {
+ response: { price: "ARS:12" } as MerchantBackend.Products.ProductDetail,
+ });
+ env.addRequestExpectation(API_GET_PRODUCT_BY_ID("2345"), {
+ response: { price: "ARS:23" } as MerchantBackend.Products.ProductDetail,
+ });
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const query = useInstanceProducts();
+ const api = useProductAPI();
+ return { query, api };
+ },
+ {},
+ [
+ ({ query, api }) => {
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(query.loading).undefined;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals([
+ { id: "1234", price: "ARS:12" },
+ { id: "2345", price: "ARS:23" },
+ ]);
+
+ env.addRequestExpectation(API_DELETE_PRODUCT("2345"), {});
+
+ env.addRequestExpectation(API_LIST_PRODUCTS, {
+ response: {
+ products: [{ product_id: "1234" }],
+ },
+ });
+
+ env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), {
+ response: {
+ price: "ARS:12",
+ } as MerchantBackend.Products.ProductDetail,
+ });
+ api.deleteProduct("2345");
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).undefined;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals([{ id: "1234", price: "ARS:12" }]);
+ },
+ ],
+ env.buildTestingContext(),
+ );
+
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+});
+
+describe("product api interaction with details", () => {
+ it("should evict cache when updating a product", async () => {
+ const env = new ApiMockEnvironment();
+
+ env.addRequestExpectation(API_GET_PRODUCT_BY_ID("12"), {
+ response: {
+ description: "this is a description",
+ } as MerchantBackend.Products.ProductDetail,
+ });
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const query = useProductDetails("12");
+ const api = useProductAPI();
+ return { query, api };
+ },
+ {},
+ [
+ ({ query, api }) => {
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ description: "this is a description",
+ });
+
+ env.addRequestExpectation(API_UPDATE_PRODUCT_BY_ID("12"), {
+ request: {
+ description: "other description",
+ } as MerchantBackend.Products.ProductPatchDetail,
+ });
+
+ env.addRequestExpectation(API_GET_PRODUCT_BY_ID("12"), {
+ response: {
+ description: "other description",
+ } as MerchantBackend.Products.ProductDetail,
+ });
+
+ api.updateProduct("12", {
+ description: "other description",
+ } as any);
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ description: "other description",
+ });
+ },
+ ],
+ env.buildTestingContext(),
+ );
+
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+});
diff --git a/packages/auditor-backoffice-ui/src/hooks/product.ts b/packages/auditor-backoffice-ui/src/hooks/product.ts
new file mode 100644
index 000000000..8ca8d2724
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/hooks/product.ts
@@ -0,0 +1,177 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ HttpResponse,
+ HttpResponseOk,
+ RequestError,
+} from "@gnu-taler/web-util/browser";
+import { 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
new file mode 100644
index 000000000..b3eecd754
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/hooks/reserve.test.ts
@@ -0,0 +1,448 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { expect } from "chai";
+import { MerchantBackend } from "../declaration.js";
+import {
+ useInstanceReserves,
+ useReserveDetails,
+ useReservesAPI,
+ useRewardDetails,
+} from "./reserves.js";
+import { ApiMockEnvironment } from "./testing.js";
+import {
+ API_AUTHORIZE_REWARD,
+ API_AUTHORIZE_REWARD_FOR_RESERVE,
+ API_CREATE_RESERVE,
+ API_DELETE_RESERVE,
+ API_GET_RESERVE_BY_ID,
+ API_GET_REWARD_BY_ID,
+ API_LIST_RESERVES,
+} from "./urls.js";
+import * as tests from "@gnu-taler/web-util/testing";
+
+describe("reserve api interaction with listing", () => {
+ it("should evict cache when creating a reserve", async () => {
+ const env = new ApiMockEnvironment();
+
+ env.addRequestExpectation(API_LIST_RESERVES, {
+ response: {
+ reserves: [
+ {
+ reserve_pub: "11",
+ } as MerchantBackend.Rewards.ReserveStatusEntry,
+ ],
+ },
+ });
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const api = useReservesAPI();
+ const query = useInstanceReserves();
+ return { query, api };
+ },
+ {},
+ [
+ ({ query, api }) => {
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ reserves: [{ reserve_pub: "11" }],
+ });
+
+ env.addRequestExpectation(API_CREATE_RESERVE, {
+ request: {
+ initial_balance: "ARS:3333",
+ exchange_url: "http://url",
+ wire_method: "iban",
+ },
+ response: {
+ reserve_pub: "22",
+ accounts: [],
+ },
+ });
+
+ env.addRequestExpectation(API_LIST_RESERVES, {
+ response: {
+ reserves: [
+ {
+ reserve_pub: "11",
+ } as MerchantBackend.Rewards.ReserveStatusEntry,
+ {
+ reserve_pub: "22",
+ } as MerchantBackend.Rewards.ReserveStatusEntry,
+ ],
+ },
+ });
+
+ api.createReserve({
+ initial_balance: "ARS:3333",
+ exchange_url: "http://url",
+ wire_method: "iban",
+ });
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+
+ expect(query.data).deep.equals({
+ reserves: [
+ {
+ reserve_pub: "11",
+ } as MerchantBackend.Rewards.ReserveStatusEntry,
+ {
+ reserve_pub: "22",
+ } as MerchantBackend.Rewards.ReserveStatusEntry,
+ ],
+ });
+ },
+ ],
+ env.buildTestingContext(),
+ );
+
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+
+ it("should evict cache when deleting a reserve", async () => {
+ const env = new ApiMockEnvironment();
+
+ env.addRequestExpectation(API_LIST_RESERVES, {
+ response: {
+ reserves: [
+ {
+ reserve_pub: "11",
+ } as MerchantBackend.Rewards.ReserveStatusEntry,
+ {
+ reserve_pub: "22",
+ } as MerchantBackend.Rewards.ReserveStatusEntry,
+ {
+ reserve_pub: "33",
+ } as MerchantBackend.Rewards.ReserveStatusEntry,
+ ],
+ },
+ });
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const api = useReservesAPI();
+ const query = useInstanceReserves();
+ return { query, api };
+ },
+ {},
+ [
+ ({ query, api }) => {
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ reserves: [
+ { reserve_pub: "11" },
+ { reserve_pub: "22" },
+ { reserve_pub: "33" },
+ ],
+ });
+
+ env.addRequestExpectation(API_DELETE_RESERVE("11"), {});
+ env.addRequestExpectation(API_LIST_RESERVES, {
+ response: {
+ reserves: [
+ {
+ reserve_pub: "22",
+ } as MerchantBackend.Rewards.ReserveStatusEntry,
+ {
+ reserve_pub: "33",
+ } as MerchantBackend.Rewards.ReserveStatusEntry,
+ ],
+ },
+ });
+
+ api.deleteReserve("11");
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ reserves: [{ reserve_pub: "22" }, { reserve_pub: "33" }],
+ });
+ },
+ ],
+ env.buildTestingContext(),
+ );
+
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+});
+
+describe("reserve api interaction with details", () => {
+ it("should evict cache when adding a reward for a specific reserve", async () => {
+ const env = new ApiMockEnvironment();
+
+ env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), {
+ response: {
+ accounts: [{ payto_uri: "payto://here" }],
+ rewards: [{ reason: "why?", reward_id: "id1", total_amount: "USD:10" }],
+ } as MerchantBackend.Rewards.ReserveDetail,
+ qparam: {
+ rewards: "yes",
+ },
+ });
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const api = useReservesAPI();
+ const query = useReserveDetails("11");
+ return { query, api };
+ },
+ {},
+ [
+ ({ query, api }) => {
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ accounts: [{ payto_uri: "payto://here" }],
+ rewards: [{ reason: "why?", reward_id: "id1", total_amount: "USD:10" }],
+ });
+
+ env.addRequestExpectation(API_AUTHORIZE_REWARD_FOR_RESERVE("11"), {
+ request: {
+ amount: "USD:12",
+ justification: "not",
+ next_url: "http://taler.net",
+ },
+ response: {
+ reward_id: "id2",
+ taler_reward_uri: "uri",
+ reward_expiration: { t_s: 1 },
+ reward_status_url: "url",
+ },
+ });
+
+ env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), {
+ response: {
+ accounts: [{ payto_uri: "payto://here" }],
+ rewards: [
+ { reason: "why?", reward_id: "id1", total_amount: "USD:10" },
+ { reason: "not", reward_id: "id2", total_amount: "USD:12" },
+ ],
+ } as MerchantBackend.Rewards.ReserveDetail,
+ qparam: {
+ rewards: "yes",
+ },
+ });
+
+ api.authorizeRewardReserve("11", {
+ amount: "USD:12",
+ justification: "not",
+ next_url: "http://taler.net",
+ });
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).false;
+
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+
+ expect(query.data).deep.equals({
+ accounts: [{ payto_uri: "payto://here" }],
+ rewards: [
+ { reason: "why?", reward_id: "id1", total_amount: "USD:10" },
+ { reason: "not", reward_id: "id2", total_amount: "USD:12" },
+ ],
+ });
+ },
+ ],
+ env.buildTestingContext(),
+ );
+
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+
+ it("should evict cache when adding a reward for a random reserve", async () => {
+ const env = new ApiMockEnvironment();
+
+ env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), {
+ response: {
+ accounts: [{ payto_uri: "payto://here" }],
+ rewards: [{ reason: "why?", reward_id: "id1", total_amount: "USD:10" }],
+ } as MerchantBackend.Rewards.ReserveDetail,
+ qparam: {
+ rewards: "yes",
+ },
+ });
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const api = useReservesAPI();
+ const query = useReserveDetails("11");
+ return { query, api };
+ },
+ {},
+ [
+ ({ query, api }) => {
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ accounts: [{ payto_uri: "payto://here" }],
+ rewards: [{ reason: "why?", reward_id: "id1", total_amount: "USD:10" }],
+ });
+
+ env.addRequestExpectation(API_AUTHORIZE_REWARD, {
+ request: {
+ amount: "USD:12",
+ justification: "not",
+ next_url: "http://taler.net",
+ },
+ response: {
+ reward_id: "id2",
+ taler_reward_uri: "uri",
+ reward_expiration: { t_s: 1 },
+ reward_status_url: "url",
+ },
+ });
+
+ env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), {
+ response: {
+ accounts: [{ payto_uri: "payto://here" }],
+ rewards: [
+ { reason: "why?", reward_id: "id1", total_amount: "USD:10" },
+ { reason: "not", reward_id: "id2", total_amount: "USD:12" },
+ ],
+ } as MerchantBackend.Rewards.ReserveDetail,
+ qparam: {
+ rewards: "yes",
+ },
+ });
+
+ api.authorizeReward({
+ amount: "USD:12",
+ justification: "not",
+ next_url: "http://taler.net",
+ });
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+
+ expect(query.data).deep.equals({
+ accounts: [{ payto_uri: "payto://here" }],
+ rewards: [
+ { reason: "why?", reward_id: "id1", total_amount: "USD:10" },
+ { reason: "not", reward_id: "id2", total_amount: "USD:12" },
+ ],
+ });
+ },
+ ],
+ env.buildTestingContext(),
+ );
+
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+});
+
+describe("reserve api interaction with reward details", () => {
+ it("should list rewards", async () => {
+ const env = new ApiMockEnvironment();
+
+ env.addRequestExpectation(API_GET_REWARD_BY_ID("11"), {
+ response: {
+ total_picked_up: "USD:12",
+ reason: "not",
+ } as MerchantBackend.Rewards.RewardDetails,
+ qparam: {
+ pickups: "yes",
+ },
+ });
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const query = useRewardDetails("11");
+ return { query };
+ },
+ {},
+ [
+ ({ query }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).true;
+ },
+ ({ query }) => {
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ total_picked_up: "USD:12",
+ reason: "not",
+ });
+ },
+ ],
+ env.buildTestingContext(),
+ );
+
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+});
diff --git a/packages/auditor-backoffice-ui/src/hooks/reserves.ts b/packages/auditor-backoffice-ui/src/hooks/reserves.ts
new file mode 100644
index 000000000..b719bfbe6
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/hooks/reserves.ts
@@ -0,0 +1,181 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ HttpResponse,
+ HttpResponseOk,
+ RequestError,
+} from "@gnu-taler/web-util/browser";
+import { MerchantBackend } from "../declaration.js";
+import { useBackendInstanceRequest, useMatchMutate } from "./backend.js";
+
+// FIX default import https://github.com/microsoft/TypeScript/issues/49189
+import _useSWR, { SWRHook, useSWRConfig } from "swr";
+const useSWR = _useSWR as unknown as SWRHook;
+
+export function useReservesAPI(): ReserveMutateAPI {
+ const mutateAll = useMatchMutate();
+ const { mutate } = useSWRConfig();
+ const { request } = useBackendInstanceRequest();
+
+ const createReserve = async (
+ data: MerchantBackend.Rewards.ReserveCreateRequest,
+ ): Promise<
+ HttpResponseOk<MerchantBackend.Rewards.ReserveCreateConfirmation>
+ > => {
+ const res = await request<MerchantBackend.Rewards.ReserveCreateConfirmation>(
+ `/private/reserves`,
+ {
+ method: "POST",
+ data,
+ },
+ );
+
+ //evict reserve list query
+ await mutateAll(/.*private\/reserves.*/);
+
+ return res;
+ };
+
+ const authorizeRewardReserve = async (
+ pub: string,
+ data: MerchantBackend.Rewards.RewardCreateRequest,
+ ): Promise<HttpResponseOk<MerchantBackend.Rewards.RewardCreateConfirmation>> => {
+ const res = await request<MerchantBackend.Rewards.RewardCreateConfirmation>(
+ `/private/reserves/${pub}/authorize-reward`,
+ {
+ method: "POST",
+ data,
+ },
+ );
+
+ //evict reserve details query
+ await mutate([`/private/reserves/${pub}`]);
+
+ return res;
+ };
+
+ const authorizeReward = async (
+ data: MerchantBackend.Rewards.RewardCreateRequest,
+ ): Promise<HttpResponseOk<MerchantBackend.Rewards.RewardCreateConfirmation>> => {
+ const res = await request<MerchantBackend.Rewards.RewardCreateConfirmation>(
+ `/private/rewards`,
+ {
+ method: "POST",
+ data,
+ },
+ );
+
+ //evict all details query
+ await mutateAll(/.*private\/reserves\/.*/);
+
+ return res;
+ };
+
+ const deleteReserve = async (
+ pub: string,
+ ): Promise<HttpResponse<void, MerchantBackend.ErrorDetail>> => {
+ const res = await request<void>(`/private/reserves/${pub}`, {
+ method: "DELETE",
+ });
+
+ //evict reserve list query
+ await mutateAll(/.*private\/reserves.*/);
+
+ return res;
+ };
+
+ return { createReserve, authorizeReward, authorizeRewardReserve, deleteReserve };
+}
+
+export interface ReserveMutateAPI {
+ createReserve: (
+ data: MerchantBackend.Rewards.ReserveCreateRequest,
+ ) => Promise<HttpResponseOk<MerchantBackend.Rewards.ReserveCreateConfirmation>>;
+ authorizeRewardReserve: (
+ id: string,
+ data: MerchantBackend.Rewards.RewardCreateRequest,
+ ) => Promise<HttpResponseOk<MerchantBackend.Rewards.RewardCreateConfirmation>>;
+ authorizeReward: (
+ data: MerchantBackend.Rewards.RewardCreateRequest,
+ ) => Promise<HttpResponseOk<MerchantBackend.Rewards.RewardCreateConfirmation>>;
+ deleteReserve: (
+ id: string,
+ ) => Promise<HttpResponse<void, MerchantBackend.ErrorDetail>>;
+}
+
+export function useInstanceReserves(): HttpResponse<
+ MerchantBackend.Rewards.RewardReserveStatus,
+ MerchantBackend.ErrorDetail
+> {
+ const { fetcher } = useBackendInstanceRequest();
+
+ const { data, error, isValidating } = useSWR<
+ HttpResponseOk<MerchantBackend.Rewards.RewardReserveStatus>,
+ RequestError<MerchantBackend.ErrorDetail>
+ >([`/private/reserves`], fetcher);
+
+ if (isValidating) return { loading: true, data: data?.data };
+ if (data) return data;
+ if (error) return error.cause;
+ return { loading: true };
+}
+
+export function useReserveDetails(
+ reserveId: string,
+): HttpResponse<
+ MerchantBackend.Rewards.ReserveDetail,
+ MerchantBackend.ErrorDetail
+> {
+ const { reserveDetailFetcher } = useBackendInstanceRequest();
+
+ const { data, error, isValidating } = useSWR<
+ HttpResponseOk<MerchantBackend.Rewards.ReserveDetail>,
+ RequestError<MerchantBackend.ErrorDetail>
+ >([`/private/reserves/${reserveId}`], reserveDetailFetcher, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ });
+
+ if (isValidating) return { loading: true, data: data?.data };
+ if (data) return data;
+ if (error) return error.cause;
+ return { loading: true };
+}
+
+export function useRewardDetails(
+ rewardId: string,
+): HttpResponse<MerchantBackend.Rewards.RewardDetails, MerchantBackend.ErrorDetail> {
+ const { rewardsDetailFetcher } = useBackendInstanceRequest();
+
+ const { data, error, isValidating } = useSWR<
+ HttpResponseOk<MerchantBackend.Rewards.RewardDetails>,
+ RequestError<MerchantBackend.ErrorDetail>
+ >([`/private/rewards/${rewardId}`], rewardsDetailFetcher, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ });
+
+ if (isValidating) return { loading: true, data: data?.data };
+ if (data) return data;
+ if (error) return error.cause;
+ return { loading: true };
+}
diff --git a/packages/auditor-backoffice-ui/src/hooks/templates.ts b/packages/auditor-backoffice-ui/src/hooks/templates.ts
new file mode 100644
index 000000000..ee8728cc8
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/hooks/templates.ts
@@ -0,0 +1,266 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ HttpResponse,
+ HttpResponseOk,
+ HttpResponsePaginated,
+ RequestError,
+} from "@gnu-taler/web-util/browser";
+import { useEffect, useState } from "preact/hooks";
+import { MerchantBackend } from "../declaration.js";
+import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js";
+import { useBackendInstanceRequest, useMatchMutate } from "./backend.js";
+
+// FIX default import https://github.com/microsoft/TypeScript/issues/49189
+import _useSWR, { SWRHook } from "swr";
+const useSWR = _useSWR as unknown as SWRHook;
+
+export function useTemplateAPI(): TemplateAPI {
+ const mutateAll = useMatchMutate();
+ const { request } = useBackendInstanceRequest();
+
+ const createTemplate = async (
+ data: MerchantBackend.Template.TemplateAddDetails,
+ ): Promise<HttpResponseOk<void>> => {
+ const res = await request<void>(`/private/templates`, {
+ method: "POST",
+ data,
+ });
+ await mutateAll(/.*private\/templates.*/);
+ return res;
+ };
+
+ const updateTemplate = async (
+ templateId: string,
+ data: MerchantBackend.Template.TemplatePatchDetails,
+ ): Promise<HttpResponseOk<void>> => {
+ const res = await request<void>(`/private/templates/${templateId}`, {
+ method: "PATCH",
+ data,
+ });
+ await mutateAll(/.*private\/templates.*/);
+ return res;
+ };
+
+ const deleteTemplate = async (
+ templateId: string,
+ ): Promise<HttpResponseOk<void>> => {
+ const res = await request<void>(`/private/templates/${templateId}`, {
+ method: "DELETE",
+ });
+ await mutateAll(/.*private\/templates.*/);
+ return res;
+ };
+
+ const createOrderFromTemplate = async (
+ templateId: string,
+ data: MerchantBackend.Template.UsingTemplateDetails,
+ ): Promise<
+ HttpResponseOk<MerchantBackend.Template.UsingTemplateResponse>
+ > => {
+ const res = await request<MerchantBackend.Template.UsingTemplateResponse>(
+ `/templates/${templateId}`,
+ {
+ method: "POST",
+ data,
+ },
+ );
+ await mutateAll(/.*private\/templates.*/);
+ return res;
+ };
+
+ const testTemplateExist = async (
+ templateId: string,
+ ): Promise<HttpResponseOk<void>> => {
+ const res = await request<void>(`/private/templates/${templateId}`, { method: "GET", });
+ return res;
+ };
+
+
+ return {
+ createTemplate,
+ updateTemplate,
+ deleteTemplate,
+ testTemplateExist,
+ createOrderFromTemplate,
+ };
+}
+
+export interface TemplateAPI {
+ createTemplate: (
+ data: MerchantBackend.Template.TemplateAddDetails,
+ ) => Promise<HttpResponseOk<void>>;
+ updateTemplate: (
+ id: string,
+ data: MerchantBackend.Template.TemplatePatchDetails,
+ ) => Promise<HttpResponseOk<void>>;
+ testTemplateExist: (
+ id: string
+ ) => Promise<HttpResponseOk<void>>;
+ deleteTemplate: (id: string) => Promise<HttpResponseOk<void>>;
+ createOrderFromTemplate: (
+ id: string,
+ data: MerchantBackend.Template.UsingTemplateDetails,
+ ) => Promise<HttpResponseOk<MerchantBackend.Template.UsingTemplateResponse>>;
+}
+
+export interface InstanceTemplateFilter {
+ //FIXME: add filter to the template list
+ position?: string;
+}
+
+export function useInstanceTemplates(
+ args?: InstanceTemplateFilter,
+ updatePosition?: (id: string) => void,
+): HttpResponsePaginated<
+ MerchantBackend.Template.TemplateSummaryResponse,
+ MerchantBackend.ErrorDetail
+> {
+ const { templateFetcher } = useBackendInstanceRequest();
+
+ const [pageBefore, setPageBefore] = useState(1);
+ const [pageAfter, setPageAfter] = useState(1);
+
+ const totalAfter = pageAfter * PAGE_SIZE;
+ const totalBefore = args?.position ? pageBefore * PAGE_SIZE : 0;
+
+ /**
+ * FIXME: this can be cleaned up a little
+ *
+ * the logic of double query should be inside the orderFetch so from the hook perspective and cache
+ * is just one query and one error status
+ */
+ const {
+ data: beforeData,
+ error: beforeError,
+ isValidating: loadingBefore,
+ } = useSWR<
+ HttpResponseOk<MerchantBackend.Template.TemplateSummaryResponse>,
+ RequestError<MerchantBackend.ErrorDetail>>(
+ [
+ `/private/templates`,
+ args?.position,
+ totalBefore,
+ ],
+ templateFetcher,
+ );
+ const {
+ data: afterData,
+ error: afterError,
+ isValidating: loadingAfter,
+ } = useSWR<
+ HttpResponseOk<MerchantBackend.Template.TemplateSummaryResponse>,
+ RequestError<MerchantBackend.ErrorDetail>
+ >([`/private/templates`, args?.position, -totalAfter], templateFetcher);
+
+ //this will save last result
+ const [lastBefore, setLastBefore] = useState<
+ HttpResponse<
+ MerchantBackend.Template.TemplateSummaryResponse,
+ MerchantBackend.ErrorDetail
+ >
+ >({ loading: true });
+
+ const [lastAfter, setLastAfter] = useState<
+ HttpResponse<
+ MerchantBackend.Template.TemplateSummaryResponse,
+ MerchantBackend.ErrorDetail
+ >
+ >({ loading: true });
+ useEffect(() => {
+ if (afterData) setLastAfter(afterData);
+ if (beforeData) setLastBefore(beforeData);
+ }, [afterData, beforeData]);
+
+ if (beforeError) return beforeError.cause;
+ if (afterError) return afterError.cause;
+
+ // if the query returns less that we ask, then we have reach the end or beginning
+ const isReachingEnd =
+ afterData && afterData.data.templates.length < totalAfter;
+ const isReachingStart = args?.position === undefined
+ ||
+ (beforeData && beforeData.data.templates.length < totalBefore);
+
+ const pagination = {
+ isReachingEnd,
+ isReachingStart,
+ loadMore: () => {
+ if (!afterData || isReachingEnd) return;
+ if (afterData.data.templates.length < MAX_RESULT_SIZE) {
+ setPageAfter(pageAfter + 1);
+ } else {
+ const from = `${afterData.data.templates[afterData.data.templates.length - 1]
+ .template_id
+ }`;
+ if (from && updatePosition) updatePosition(from);
+ }
+ },
+ loadMorePrev: () => {
+ if (!beforeData || isReachingStart) return;
+ if (beforeData.data.templates.length < MAX_RESULT_SIZE) {
+ setPageBefore(pageBefore + 1);
+ } else if (beforeData) {
+ const from = `${beforeData.data.templates[beforeData.data.templates.length - 1]
+ .template_id
+ }`;
+ if (from && updatePosition) updatePosition(from);
+ }
+ },
+ };
+
+ // const templates = !afterData ? [] : (afterData || lastAfter).data.templates;
+ const templates =
+ !beforeData || !afterData
+ ? []
+ : (beforeData || lastBefore).data.templates
+ .slice()
+ .reverse()
+ .concat((afterData || lastAfter).data.templates);
+ if (loadingAfter || loadingBefore)
+ return { loading: true, data: { templates } };
+ if (beforeData && afterData) {
+ return { ok: true, data: { templates }, ...pagination };
+ }
+ return { loading: true };
+}
+
+export function useTemplateDetails(
+ templateId: string,
+): HttpResponse<
+ MerchantBackend.Template.TemplateDetails,
+ MerchantBackend.ErrorDetail
+> {
+ const { templateFetcher } = useBackendInstanceRequest();
+
+ const { data, error, isValidating } = useSWR<
+ HttpResponseOk<MerchantBackend.Template.TemplateDetails>,
+ RequestError<MerchantBackend.ErrorDetail>
+ >([`/private/templates/${templateId}`], templateFetcher, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ });
+
+ if (isValidating) return { loading: true, data: data?.data };
+ if (data) {
+ return data;
+ }
+ if (error) return error.cause;
+ return { loading: true };
+}
diff --git a/packages/auditor-backoffice-ui/src/hooks/testing.tsx b/packages/auditor-backoffice-ui/src/hooks/testing.tsx
new file mode 100644
index 000000000..7955f832a
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/hooks/testing.tsx
@@ -0,0 +1,180 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { MockEnvironment } from "@gnu-taler/web-util/testing";
+import { ComponentChildren, FunctionalComponent, h, VNode } from "preact";
+import { HttpRequestLibrary, HttpRequestOptions, HttpResponse } from "@gnu-taler/taler-util/http";
+import { SWRConfig } from "swr";
+import { ApiContextProvider } from "@gnu-taler/web-util/browser";
+import { BackendContextProvider } from "../context/backend.js";
+import { InstanceContextProvider } from "../context/instance.js";
+import { HttpResponseOk, RequestOptions } from "@gnu-taler/web-util/browser";
+import { TalerBankIntegrationHttpClient, TalerCoreBankHttpClient, 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
new file mode 100644
index 000000000..a7187af27
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/hooks/transfer.test.ts
@@ -0,0 +1,254 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import * as tests from "@gnu-taler/web-util/testing";
+import { expect } from "chai";
+import { MerchantBackend } from "../declaration.js";
+import { API_INFORM_TRANSFERS, API_LIST_TRANSFERS } from "./urls.js";
+import { ApiMockEnvironment } from "./testing.js";
+import { useInstanceTransfers, useTransferAPI } from "./transfer.js";
+
+describe("transfer api interaction with listing", () => {
+ it("should evict cache when informing a transfer", async () => {
+ const env = new ApiMockEnvironment();
+
+ env.addRequestExpectation(API_LIST_TRANSFERS, {
+ qparam: { limit: -20 },
+ response: {
+ transfers: [{ wtid: "2" } as MerchantBackend.Transfers.TransferDetails],
+ },
+ });
+
+ const moveCursor = (d: string) => {
+ console.log("new position", d);
+ };
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const query = useInstanceTransfers({}, moveCursor);
+ const api = useTransferAPI();
+ return { query, api };
+ },
+ {},
+ [
+ ({ query, api }) => {
+ expect(query.loading).true;
+ },
+
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).undefined;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ transfers: [{ wtid: "2" }],
+ });
+
+ env.addRequestExpectation(API_INFORM_TRANSFERS, {
+ request: {
+ wtid: "3",
+ credit_amount: "EUR:1",
+ exchange_url: "exchange.url",
+ payto_uri: "payto://",
+ },
+ response: { total: "" } as any,
+ });
+
+ env.addRequestExpectation(API_LIST_TRANSFERS, {
+ qparam: { limit: -20 },
+ response: {
+ transfers: [{ wtid: "3" } as any, { wtid: "2" } as any],
+ },
+ });
+
+ api.informTransfer({
+ wtid: "3",
+ credit_amount: "EUR:1",
+ exchange_url: "exchange.url",
+ payto_uri: "payto://",
+ });
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).undefined;
+ expect(query.ok).true;
+ if (!query.ok) return;
+
+ expect(query.data).deep.equals({
+ transfers: [{ wtid: "3" }, { wtid: "2" }],
+ });
+ },
+ ],
+ env.buildTestingContext(),
+ );
+
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+});
+
+describe("transfer listing pagination", () => {
+ it("should not load more if has reach the end", async () => {
+ const env = new ApiMockEnvironment();
+
+ env.addRequestExpectation(API_LIST_TRANSFERS, {
+ qparam: { limit: -20, payto_uri: "payto://" },
+ response: {
+ transfers: [{ wtid: "2" }, { wtid: "1" } as any],
+ },
+ });
+
+ const moveCursor = (d: string) => {
+ console.log("new position", d);
+ };
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ return useInstanceTransfers({ payto_uri: "payto://" }, moveCursor);
+ },
+ {},
+ [
+ (query) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).true;
+ },
+ (query) => {
+ expect(query.loading).undefined;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ transfers: [{ wtid: "2" }, { wtid: "1" }],
+ });
+ expect(query.isReachingEnd).true;
+ expect(query.isReachingStart).true;
+
+ //check that this button won't trigger more updates since
+ //has reach end and start
+ query.loadMore();
+ query.loadMorePrev();
+ },
+ ],
+ env.buildTestingContext(),
+ );
+
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ });
+
+ it("should load more if result brings more that PAGE_SIZE", async () => {
+ const env = new ApiMockEnvironment();
+
+ const transfersFrom0to20 = Array.from({ length: 20 }).map((e, i) => ({
+ wtid: String(i),
+ }));
+ const transfersFrom20to40 = Array.from({ length: 20 }).map((e, i) => ({
+ wtid: String(i + 20),
+ }));
+ const transfersFrom20to0 = [...transfersFrom0to20].reverse();
+
+ env.addRequestExpectation(API_LIST_TRANSFERS, {
+ qparam: { limit: 20, payto_uri: "payto://", offset: "1" },
+ response: {
+ transfers: transfersFrom0to20,
+ },
+ });
+
+ env.addRequestExpectation(API_LIST_TRANSFERS, {
+ qparam: { limit: -20, payto_uri: "payto://", offset: "1" },
+ response: {
+ transfers: transfersFrom20to40,
+ },
+ });
+
+ const moveCursor = (d: string) => {
+ console.log("new position", d);
+ };
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ return useInstanceTransfers(
+ { payto_uri: "payto://", position: "1" },
+ moveCursor,
+ );
+ },
+ {},
+ [
+ (result) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(result.loading).true;
+ },
+ (result) => {
+ expect(result.loading).undefined;
+ expect(result.ok).true;
+ if (!result.ok) return;
+ expect(result.data).deep.equals({
+ transfers: [...transfersFrom20to0, ...transfersFrom20to40],
+ });
+ expect(result.isReachingEnd).false;
+ expect(result.isReachingStart).false;
+
+ //query more
+ env.addRequestExpectation(API_LIST_TRANSFERS, {
+ qparam: { limit: -40, payto_uri: "payto://", offset: "1" },
+ response: {
+ transfers: [...transfersFrom20to40, { wtid: "41" }],
+ },
+ });
+ result.loadMore();
+ },
+ (result) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(result.loading).true;
+ },
+ (result) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(result.loading).undefined;
+ expect(result.ok).true;
+ if (!result.ok) return;
+ expect(result.data).deep.equals({
+ transfers: [
+ ...transfersFrom20to0,
+ ...transfersFrom20to40,
+ { wtid: "41" },
+ ],
+ });
+ expect(result.isReachingEnd).true;
+ expect(result.isReachingStart).false;
+ },
+ ],
+ env.buildTestingContext(),
+ );
+
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ });
+});
diff --git a/packages/auditor-backoffice-ui/src/hooks/transfer.ts b/packages/auditor-backoffice-ui/src/hooks/transfer.ts
new file mode 100644
index 000000000..27c3bdc75
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/hooks/transfer.ts
@@ -0,0 +1,188 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ HttpResponse,
+ HttpResponseOk,
+ HttpResponsePaginated,
+ RequestError,
+} from "@gnu-taler/web-util/browser";
+import { useEffect, useState } from "preact/hooks";
+import { MerchantBackend } from "../declaration.js";
+import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js";
+import { useBackendInstanceRequest, useMatchMutate } from "./backend.js";
+
+// FIX default import https://github.com/microsoft/TypeScript/issues/49189
+import _useSWR, { SWRHook } from "swr";
+const useSWR = _useSWR as unknown as SWRHook;
+
+export function useTransferAPI(): TransferAPI {
+ const mutateAll = useMatchMutate();
+ const { request } = useBackendInstanceRequest();
+
+ const informTransfer = async (
+ data: MerchantBackend.Transfers.TransferInformation,
+ ): Promise<HttpResponseOk<{}>> => {
+ const res = await request<{}>(`/private/transfers`, {
+ method: "POST",
+ data,
+ });
+
+ await mutateAll(/.*private\/transfers.*/);
+ return res;
+ };
+
+ return { informTransfer };
+}
+
+export interface TransferAPI {
+ informTransfer: (
+ data: MerchantBackend.Transfers.TransferInformation,
+ ) => Promise<HttpResponseOk<{}>>;
+}
+
+export interface InstanceTransferFilter {
+ payto_uri?: string;
+ verified?: "yes" | "no";
+ position?: string;
+}
+
+export function useInstanceTransfers(
+ args?: InstanceTransferFilter,
+ updatePosition?: (id: string) => void,
+): HttpResponsePaginated<
+ MerchantBackend.Transfers.TransferList,
+ MerchantBackend.ErrorDetail
+> {
+ const { transferFetcher } = useBackendInstanceRequest();
+
+ const [pageBefore, setPageBefore] = useState(1);
+ const [pageAfter, setPageAfter] = useState(1);
+
+ const totalAfter = pageAfter * PAGE_SIZE;
+ const totalBefore = args?.position !== undefined ? pageBefore * PAGE_SIZE : 0;
+
+ /**
+ * FIXME: this can be cleaned up a little
+ *
+ * the logic of double query should be inside the orderFetch so from the hook perspective and cache
+ * is just one query and one error status
+ */
+ const {
+ data: beforeData,
+ error: beforeError,
+ isValidating: loadingBefore,
+ } = useSWR<
+ HttpResponseOk<MerchantBackend.Transfers.TransferList>,
+ RequestError<MerchantBackend.ErrorDetail>
+ >(
+ [
+ `/private/transfers`,
+ args?.payto_uri,
+ args?.verified,
+ args?.position,
+ totalBefore,
+ ],
+ transferFetcher,
+ );
+ const {
+ data: afterData,
+ error: afterError,
+ isValidating: loadingAfter,
+ } = useSWR<
+ HttpResponseOk<MerchantBackend.Transfers.TransferList>,
+ RequestError<MerchantBackend.ErrorDetail>
+ >(
+ [
+ `/private/transfers`,
+ args?.payto_uri,
+ args?.verified,
+ args?.position,
+ -totalAfter,
+ ],
+ transferFetcher,
+ );
+
+ //this will save last result
+ const [lastBefore, setLastBefore] = useState<
+ HttpResponse<
+ MerchantBackend.Transfers.TransferList,
+ MerchantBackend.ErrorDetail
+ >
+ >({ loading: true });
+ const [lastAfter, setLastAfter] = useState<
+ HttpResponse<
+ MerchantBackend.Transfers.TransferList,
+ MerchantBackend.ErrorDetail
+ >
+ >({ loading: true });
+ useEffect(() => {
+ if (afterData) setLastAfter(afterData);
+ if (beforeData) setLastBefore(beforeData);
+ }, [afterData, beforeData]);
+
+ if (beforeError) return beforeError.cause;
+ if (afterError) return afterError.cause;
+
+ // if the query returns less that we ask, then we have reach the end or beginning
+ const isReachingEnd =
+ afterData && afterData.data.transfers.length < totalAfter;
+ const isReachingStart =
+ args?.position === undefined ||
+ (beforeData && beforeData.data.transfers.length < totalBefore);
+
+ const pagination = {
+ isReachingEnd,
+ isReachingStart,
+ loadMore: () => {
+ if (!afterData || isReachingEnd) return;
+ if (afterData.data.transfers.length < MAX_RESULT_SIZE) {
+ setPageAfter(pageAfter + 1);
+ } else {
+ const from = `${
+ afterData.data.transfers[afterData.data.transfers.length - 1]
+ .transfer_serial_id
+ }`;
+ if (from && updatePosition) updatePosition(from);
+ }
+ },
+ loadMorePrev: () => {
+ if (!beforeData || isReachingStart) return;
+ if (beforeData.data.transfers.length < MAX_RESULT_SIZE) {
+ setPageBefore(pageBefore + 1);
+ } else if (beforeData) {
+ const from = `${
+ beforeData.data.transfers[beforeData.data.transfers.length - 1]
+ .transfer_serial_id
+ }`;
+ if (from && updatePosition) updatePosition(from);
+ }
+ },
+ };
+
+ const transfers =
+ !beforeData || !afterData
+ ? []
+ : (beforeData || lastBefore).data.transfers
+ .slice()
+ .reverse()
+ .concat((afterData || lastAfter).data.transfers);
+ if (loadingAfter || loadingBefore)
+ return { loading: true, data: { transfers } };
+ if (beforeData && afterData) {
+ return { ok: true, data: { transfers }, ...pagination };
+ }
+ return { loading: true };
+}
diff --git a/packages/auditor-backoffice-ui/src/hooks/urls.ts b/packages/auditor-backoffice-ui/src/hooks/urls.ts
new file mode 100644
index 000000000..b6485259f
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/hooks/urls.ts
@@ -0,0 +1,303 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { Query } from "@gnu-taler/web-util/testing";
+import { MerchantBackend } from "../declaration.js";
+
+////////////////////
+// ORDER
+////////////////////
+
+export const API_CREATE_ORDER: Query<
+ MerchantBackend.Orders.PostOrderRequest,
+ MerchantBackend.Orders.PostOrderResponse
+> = {
+ method: "POST",
+ url: "http://backend/instances/default/private/orders",
+};
+
+export const API_GET_ORDER_BY_ID = (
+ id: string,
+): Query<unknown, MerchantBackend.Orders.MerchantOrderStatusResponse> => ({
+ method: "GET",
+ url: `http://backend/instances/default/private/orders/${id}`,
+});
+
+export const API_LIST_ORDERS: Query<
+ unknown,
+ MerchantBackend.Orders.OrderHistory
+> = {
+ method: "GET",
+ url: "http://backend/instances/default/private/orders",
+};
+
+export const API_REFUND_ORDER_BY_ID = (
+ id: string,
+): Query<
+ MerchantBackend.Orders.RefundRequest,
+ MerchantBackend.Orders.MerchantRefundResponse
+> => ({
+ method: "POST",
+ url: `http://backend/instances/default/private/orders/${id}/refund`,
+});
+
+export const API_FORGET_ORDER_BY_ID = (
+ id: string,
+): Query<MerchantBackend.Orders.ForgetRequest, unknown> => ({
+ method: "PATCH",
+ url: `http://backend/instances/default/private/orders/${id}/forget`,
+});
+
+export const API_DELETE_ORDER = (
+ id: string,
+): Query<MerchantBackend.Orders.ForgetRequest, unknown> => ({
+ method: "DELETE",
+ url: `http://backend/instances/default/private/orders/${id}`,
+});
+
+////////////////////
+// TRANSFER
+////////////////////
+
+export const API_LIST_TRANSFERS: Query<
+ unknown,
+ MerchantBackend.Transfers.TransferList
+> = {
+ method: "GET",
+ url: "http://backend/instances/default/private/transfers",
+};
+
+export const API_INFORM_TRANSFERS: Query<
+ MerchantBackend.Transfers.TransferInformation,
+ {}
+> = {
+ method: "POST",
+ url: "http://backend/instances/default/private/transfers",
+};
+
+////////////////////
+// PRODUCT
+////////////////////
+
+export const API_CREATE_PRODUCT: Query<
+ MerchantBackend.Products.ProductAddDetail,
+ unknown
+> = {
+ method: "POST",
+ url: "http://backend/instances/default/private/products",
+};
+
+export const API_LIST_PRODUCTS: Query<
+ unknown,
+ MerchantBackend.Products.InventorySummaryResponse
+> = {
+ method: "GET",
+ url: "http://backend/instances/default/private/products",
+};
+
+export const API_GET_PRODUCT_BY_ID = (
+ id: string,
+): Query<unknown, MerchantBackend.Products.ProductDetail> => ({
+ method: "GET",
+ url: `http://backend/instances/default/private/products/${id}`,
+});
+
+export const API_UPDATE_PRODUCT_BY_ID = (
+ id: string,
+): Query<
+ MerchantBackend.Products.ProductPatchDetail,
+ MerchantBackend.Products.InventorySummaryResponse
+> => ({
+ method: "PATCH",
+ url: `http://backend/instances/default/private/products/${id}`,
+});
+
+export const API_DELETE_PRODUCT = (id: string): Query<unknown, unknown> => ({
+ method: "DELETE",
+ url: `http://backend/instances/default/private/products/${id}`,
+});
+
+////////////////////
+// RESERVES
+////////////////////
+
+export const API_CREATE_RESERVE: Query<
+ MerchantBackend.Rewards.ReserveCreateRequest,
+ MerchantBackend.Rewards.ReserveCreateConfirmation
+> = {
+ method: "POST",
+ url: "http://backend/instances/default/private/reserves",
+};
+export const API_LIST_RESERVES: Query<
+ unknown,
+ MerchantBackend.Rewards.RewardReserveStatus
+> = {
+ method: "GET",
+ url: "http://backend/instances/default/private/reserves",
+};
+
+export const API_GET_RESERVE_BY_ID = (
+ pub: string,
+): Query<unknown, MerchantBackend.Rewards.ReserveDetail> => ({
+ method: "GET",
+ url: `http://backend/instances/default/private/reserves/${pub}`,
+});
+
+export const API_GET_REWARD_BY_ID = (
+ pub: string,
+): Query<unknown, MerchantBackend.Rewards.RewardDetails> => ({
+ method: "GET",
+ url: `http://backend/instances/default/private/rewards/${pub}`,
+});
+
+export const API_AUTHORIZE_REWARD_FOR_RESERVE = (
+ pub: string,
+): Query<
+ MerchantBackend.Rewards.RewardCreateRequest,
+ MerchantBackend.Rewards.RewardCreateConfirmation
+> => ({
+ method: "POST",
+ url: `http://backend/instances/default/private/reserves/${pub}/authorize-reward`,
+});
+
+export const API_AUTHORIZE_REWARD: Query<
+ MerchantBackend.Rewards.RewardCreateRequest,
+ MerchantBackend.Rewards.RewardCreateConfirmation
+> = {
+ method: "POST",
+ url: `http://backend/instances/default/private/rewards`,
+};
+
+export const API_DELETE_RESERVE = (id: string): Query<unknown, unknown> => ({
+ method: "DELETE",
+ url: `http://backend/instances/default/private/reserves/${id}`,
+});
+
+////////////////////
+// INSTANCE ADMIN
+////////////////////
+
+export const API_CREATE_INSTANCE: Query<
+ MerchantBackend.Instances.InstanceConfigurationMessage,
+ unknown
+> = {
+ method: "POST",
+ url: "http://backend/management/instances",
+};
+
+export const API_GET_INSTANCE_BY_ID = (
+ id: string,
+): Query<unknown, MerchantBackend.Instances.QueryInstancesResponse> => ({
+ method: "GET",
+ url: `http://backend/management/instances/${id}`,
+});
+
+export const API_GET_INSTANCE_KYC_BY_ID = (
+ id: string,
+): Query<unknown, MerchantBackend.KYC.AccountKycRedirects> => ({
+ method: "GET",
+ url: `http://backend/management/instances/${id}/kyc`,
+});
+
+export const API_LIST_INSTANCES: Query<
+ unknown,
+ MerchantBackend.Instances.InstancesResponse
+> = {
+ method: "GET",
+ url: "http://backend/management/instances",
+};
+
+export const API_UPDATE_INSTANCE_BY_ID = (
+ id: string,
+): Query<
+ MerchantBackend.Instances.InstanceReconfigurationMessage,
+ unknown
+> => ({
+ method: "PATCH",
+ url: `http://backend/management/instances/${id}`,
+});
+
+export const API_UPDATE_INSTANCE_AUTH_BY_ID = (
+ id: string,
+): Query<
+ MerchantBackend.Instances.InstanceAuthConfigurationMessage,
+ unknown
+> => ({
+ method: "POST",
+ url: `http://backend/management/instances/${id}/auth`,
+});
+
+export const API_DELETE_INSTANCE = (id: string): Query<unknown, unknown> => ({
+ method: "DELETE",
+ url: `http://backend/management/instances/${id}`,
+});
+
+////////////////////
+// AUTH
+////////////////////
+
+export const API_NEW_LOGIN: Query<
+ MerchantBackend.Instances.LoginTokenRequest,
+ unknown
+> = ({
+ method: "POST",
+ url: `http://backend/private/token`,
+});
+
+////////////////////
+// INSTANCE
+////////////////////
+
+export const API_GET_CURRENT_INSTANCE: Query<
+ unknown,
+ MerchantBackend.Instances.QueryInstancesResponse
+> = {
+ method: "GET",
+ url: `http://backend/instances/default/private/`,
+};
+
+export const API_GET_CURRENT_INSTANCE_KYC: Query<
+ unknown,
+ MerchantBackend.KYC.AccountKycRedirects
+> = {
+ method: "GET",
+ url: `http://backend/instances/default/private/kyc`,
+};
+
+export const API_UPDATE_CURRENT_INSTANCE: Query<
+ MerchantBackend.Instances.InstanceReconfigurationMessage,
+ unknown
+> = {
+ method: "PATCH",
+ url: `http://backend/instances/default/private/`,
+};
+
+export const API_UPDATE_CURRENT_INSTANCE_AUTH: Query<
+ MerchantBackend.Instances.InstanceAuthConfigurationMessage,
+ unknown
+> = {
+ method: "POST",
+ url: `http://backend/instances/default/private/auth`,
+};
+
+export const API_DELETE_CURRENT_INSTANCE: Query<unknown, unknown> = {
+ method: "DELETE",
+ url: `http://backend/instances/default/private`,
+};
diff --git a/packages/auditor-backoffice-ui/src/hooks/useSettings.ts b/packages/auditor-backoffice-ui/src/hooks/useSettings.ts
new file mode 100644
index 000000000..8c1ebd9f6
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/hooks/useSettings.ts
@@ -0,0 +1,73 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser";
+import {
+ Codec,
+ buildCodecForObject,
+ codecForBoolean,
+ codecForConstString,
+ codecForEither,
+ codecForString,
+} from "@gnu-taler/taler-util";
+
+export interface Settings {
+ advanceOrderMode: boolean;
+ dateFormat: "ymd" | "dmy" | "mdy";
+}
+
+const defaultSettings: Settings = {
+ advanceOrderMode: false,
+ dateFormat: "ymd",
+}
+
+export const codecForSettings = (): Codec<Settings> =>
+ buildCodecForObject<Settings>()
+ .property("advanceOrderMode", codecForBoolean())
+ .property("dateFormat", codecForEither(
+ codecForConstString("ymd"),
+ codecForConstString("dmy"),
+ codecForConstString("mdy"),
+ ))
+ .build("Settings");
+
+const SETTINGS_KEY = buildStorageKey("merchant-settings", codecForSettings());
+
+export function useSettings(): [
+ Readonly<Settings>,
+ (s: Settings) => void,
+] {
+ const { value, update } = useLocalStorage(SETTINGS_KEY, defaultSettings);
+
+ // const parsed: Settings = value ?? defaultSettings;
+ // function updateField<T extends keyof Settings>(k: T, v: Settings[T]) {
+ // const next = { ...parsed, [k]: v }
+ // update(next);
+ // }
+ return [value, update];
+}
+
+export function dateFormatForSettings(s: Settings): string {
+ switch (s.dateFormat) {
+ case "ymd": return "yyyy/MM/dd"
+ case "dmy": return "dd/MM/yyyy"
+ case "mdy": return "MM/dd/yyyy"
+ }
+}
+
+export function datetimeFormatForSettings(s: Settings): string {
+ return dateFormatForSettings(s) + " HH:mm:ss"
+} \ No newline at end of file
diff --git a/packages/auditor-backoffice-ui/src/hooks/webhooks.ts b/packages/auditor-backoffice-ui/src/hooks/webhooks.ts
new file mode 100644
index 000000000..ad6bf96e2
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/hooks/webhooks.ts
@@ -0,0 +1,178 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ HttpResponse,
+ HttpResponseOk,
+ HttpResponsePaginated,
+ RequestError,
+} from "@gnu-taler/web-util/browser";
+import { useEffect, useState } from "preact/hooks";
+import { MerchantBackend } from "../declaration.js";
+import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js";
+import { useBackendInstanceRequest, useMatchMutate } from "./backend.js";
+
+// FIX default import https://github.com/microsoft/TypeScript/issues/49189
+import _useSWR, { SWRHook } from "swr";
+const useSWR = _useSWR as unknown as SWRHook;
+
+export function useWebhookAPI(): WebhookAPI {
+ const mutateAll = useMatchMutate();
+ const { request } = useBackendInstanceRequest();
+
+ const createWebhook = async (
+ data: MerchantBackend.Webhooks.WebhookAddDetails,
+ ): Promise<HttpResponseOk<void>> => {
+ const res = await request<void>(`/private/webhooks`, {
+ method: "POST",
+ data,
+ });
+ await mutateAll(/.*private\/webhooks.*/);
+ return res;
+ };
+
+ const updateWebhook = async (
+ webhookId: string,
+ data: MerchantBackend.Webhooks.WebhookPatchDetails,
+ ): Promise<HttpResponseOk<void>> => {
+ const res = await request<void>(`/private/webhooks/${webhookId}`, {
+ method: "PATCH",
+ data,
+ });
+ await mutateAll(/.*private\/webhooks.*/);
+ return res;
+ };
+
+ const deleteWebhook = async (
+ webhookId: string,
+ ): Promise<HttpResponseOk<void>> => {
+ const res = await request<void>(`/private/webhooks/${webhookId}`, {
+ method: "DELETE",
+ });
+ await mutateAll(/.*private\/webhooks.*/);
+ return res;
+ };
+
+ return { createWebhook, updateWebhook, deleteWebhook };
+}
+
+export interface WebhookAPI {
+ createWebhook: (
+ data: MerchantBackend.Webhooks.WebhookAddDetails,
+ ) => Promise<HttpResponseOk<void>>;
+ updateWebhook: (
+ id: string,
+ data: MerchantBackend.Webhooks.WebhookPatchDetails,
+ ) => Promise<HttpResponseOk<void>>;
+ deleteWebhook: (id: string) => Promise<HttpResponseOk<void>>;
+}
+
+export interface InstanceWebhookFilter {
+ //FIXME: add filter to the webhook list
+ position?: string;
+}
+
+export function useInstanceWebhooks(
+ args?: InstanceWebhookFilter,
+ updatePosition?: (id: string) => void,
+): HttpResponsePaginated<
+ MerchantBackend.Webhooks.WebhookSummaryResponse,
+ MerchantBackend.ErrorDetail
+> {
+ const { webhookFetcher } = useBackendInstanceRequest();
+
+ const [pageBefore, setPageBefore] = useState(1);
+ const [pageAfter, setPageAfter] = useState(1);
+
+ const totalAfter = pageAfter * PAGE_SIZE;
+ const totalBefore = args?.position ? pageBefore * PAGE_SIZE : 0;
+
+ const {
+ data: afterData,
+ error: afterError,
+ isValidating: loadingAfter,
+ } = useSWR<
+ HttpResponseOk<MerchantBackend.Webhooks.WebhookSummaryResponse>,
+ RequestError<MerchantBackend.ErrorDetail>
+ >([`/private/webhooks`, args?.position, -totalAfter], webhookFetcher);
+
+ const [lastAfter, setLastAfter] = useState<
+ HttpResponse<
+ MerchantBackend.Webhooks.WebhookSummaryResponse,
+ MerchantBackend.ErrorDetail
+ >
+ >({ loading: true });
+ useEffect(() => {
+ if (afterData) setLastAfter(afterData);
+ }, [afterData]);
+
+ if (afterError) return afterError.cause;
+
+ const isReachingEnd =
+ afterData && afterData.data.webhooks.length < totalAfter;
+ const isReachingStart = true;
+
+ const pagination = {
+ isReachingEnd,
+ isReachingStart,
+ loadMore: () => {
+ if (!afterData || isReachingEnd) return;
+ if (afterData.data.webhooks.length < MAX_RESULT_SIZE) {
+ setPageAfter(pageAfter + 1);
+ } else {
+ const from = `${
+ afterData.data.webhooks[afterData.data.webhooks.length - 1].webhook_id
+ }`;
+ if (from && updatePosition) updatePosition(from);
+ }
+ },
+ loadMorePrev: () => {
+ return;
+ },
+ };
+
+ const webhooks = !afterData ? [] : (afterData || lastAfter).data.webhooks;
+
+ if (loadingAfter) return { loading: true, data: { webhooks } };
+ if (afterData) {
+ return { ok: true, data: { webhooks }, ...pagination };
+ }
+ return { loading: true };
+}
+
+export function useWebhookDetails(
+ webhookId: string,
+): HttpResponse<
+ MerchantBackend.Webhooks.WebhookDetails,
+ MerchantBackend.ErrorDetail
+> {
+ const { webhookFetcher } = useBackendInstanceRequest();
+
+ const { data, error, isValidating } = useSWR<
+ HttpResponseOk<MerchantBackend.Webhooks.WebhookDetails>,
+ RequestError<MerchantBackend.ErrorDetail>
+ >([`/private/webhooks/${webhookId}`], webhookFetcher, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ });
+
+ if (isValidating) return { loading: true, data: data?.data };
+ if (data) return data;
+ if (error) return error.cause;
+ return { loading: true };
+}
diff --git a/packages/auditor-backoffice-ui/src/i18n/de.po b/packages/auditor-backoffice-ui/src/i18n/de.po
new file mode 100644
index 000000000..2cf0a7c1c
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/i18n/de.po
@@ -0,0 +1,2742 @@
+# This file is part of TALER
+# (C) 2016 GNUnet e.V.
+#
+# TALER is free software; you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 3, or (at your option) any later version.
+#
+# TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Wallet\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"POT-Creation-Date: 2016-11-23 00:00+0100\n"
+"PO-Revision-Date: 2023-12-04 13:44+0000\n"
+"Last-Translator: Stefan Kügel <skuegel@web.de>\n"
+"Language-Team: German <https://weblate.taler.net/projects/gnu-taler/"
+"merchant-backoffice/de/>\n"
+"Language: de\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 5.2.1\n"
+
+#: src/components/modal/index.tsx:71
+#, c-format
+msgid "Cancel"
+msgstr ""
+
+#: src/components/modal/index.tsx:79
+#, c-format
+msgid "%1$s"
+msgstr ""
+
+#: src/components/modal/index.tsx:84
+#, c-format
+msgid "Close"
+msgstr ""
+
+#: src/components/modal/index.tsx:124
+#, c-format
+msgid "Continue"
+msgstr ""
+
+#: src/components/modal/index.tsx:178
+#, c-format
+msgid "Clear"
+msgstr ""
+
+#: src/components/modal/index.tsx:190
+#, c-format
+msgid "Confirm"
+msgstr ""
+
+#: src/components/modal/index.tsx:296
+#, c-format
+msgid "is not the same as the current access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:299
+#, c-format
+msgid "cannot be empty"
+msgstr ""
+
+#: src/components/modal/index.tsx:301
+#, c-format
+msgid "cannot be the same as the old token"
+msgstr ""
+
+#: src/components/modal/index.tsx:305
+#, c-format
+msgid "is not the same"
+msgstr ""
+
+#: src/components/modal/index.tsx:315
+#, c-format
+msgid "You are updating the access token from instance with id %1$s"
+msgstr ""
+
+#: src/components/modal/index.tsx:331
+#, c-format
+msgid "Old access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:332
+#, c-format
+msgid "access token currently in use"
+msgstr ""
+
+#: src/components/modal/index.tsx:338
+#, c-format
+msgid "New access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:339
+#, c-format
+msgid "next access token to be used"
+msgstr ""
+
+#: src/components/modal/index.tsx:344
+#, c-format
+msgid "Repeat access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:345
+#, c-format
+msgid "confirm the same access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:350
+#, c-format
+msgid "Clearing the access token will mean public access to the instance"
+msgstr ""
+
+#: src/components/modal/index.tsx:377
+#, c-format
+msgid "cannot be the same as the old access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:394
+#, c-format
+msgid "You are setting the access token for the new instance"
+msgstr ""
+
+#: src/components/modal/index.tsx:420
+#, c-format
+msgid ""
+"With external authorization method no check will be done by the merchant "
+"backend"
+msgstr ""
+
+#: src/components/modal/index.tsx:436
+#, c-format
+msgid "Set external authorization"
+msgstr ""
+
+#: src/components/modal/index.tsx:448
+#, c-format
+msgid "Set access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:470
+#, c-format
+msgid "Operation in progress..."
+msgstr ""
+
+#: src/components/modal/index.tsx:479
+#, c-format
+msgid "The operation will be automatically canceled after %1$s seconds"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:80
+#, c-format
+msgid "Instances"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:93
+#, c-format
+msgid "Delete"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:99
+#, c-format
+msgid "add new instance"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:178
+#, c-format
+msgid "ID"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:181
+#, c-format
+msgid "Name"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:220
+#, c-format
+msgid "Edit"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:237
+#, c-format
+msgid "Purge"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:261
+#, c-format
+msgid "There is no instances yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:68
+#, c-format
+msgid "Only show active instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:71
+#, c-format
+msgid "Active"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:78
+#, c-format
+msgid "Only show deleted instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:81
+#, c-format
+msgid "Deleted"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:88
+#, c-format
+msgid "Show all instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:91
+#, c-format
+msgid "All"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:101
+#, c-format
+msgid "Instance \"%1$s\" (ID: %2$s) has been deleted"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:106
+#, c-format
+msgid "Failed to delete instance"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:124
+#, c-format
+msgid "Instance '%1$s' (ID: %2$s) has been disabled"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:129
+#, c-format
+msgid "Failed to purge instance"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:41
+#, c-format
+msgid "Pending KYC verification"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:66
+#, c-format
+msgid "Timed out"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:103
+#, c-format
+msgid "Exchange"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:106
+#, c-format
+msgid "Target account"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:109
+#, c-format
+msgid "KYC URL"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:144
+#, c-format
+msgid "Code"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:147
+#, c-format
+msgid "Http Status"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:177
+#, c-format
+msgid "No pending kyc verification!"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:123
+#, c-format
+msgid "change value to unknown date"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:124
+#, c-format
+msgid "change value to empty"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:131
+#, c-format
+msgid "clear"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:136
+#, c-format
+msgid "change value to never"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:141
+#, c-format
+msgid "never"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:29
+#, c-format
+msgid "Country"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:33
+#, c-format
+msgid "Address"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:39
+#, c-format
+msgid "Building number"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:41
+#, c-format
+msgid "Building name"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:42
+#, c-format
+msgid "Street"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:43
+#, c-format
+msgid "Post code"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:44
+#, c-format
+msgid "Town location"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:45
+#, c-format
+msgid "Town"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:46
+#, c-format
+msgid "District"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:49
+#, c-format
+msgid "Country subdivision"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:66
+#, c-format
+msgid "Product id"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:69
+#, c-format
+msgid "Description"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:94
+#, c-format
+msgid "Product"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:95
+#, c-format
+msgid "search products by it's description or id"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:151
+#, c-format
+msgid "no products found with that description"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:56
+#, c-format
+msgid "You must enter a valid product identifier."
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:64
+#, c-format
+msgid "Quantity must be greater than 0!"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:76
+#, c-format
+msgid ""
+"This quantity exceeds remaining stock. Currently, only %1$s units remain "
+"unreserved in stock."
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:109
+#, c-format
+msgid "Quantity"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:110
+#, c-format
+msgid "how many products will be added"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:117
+#, c-format
+msgid "Add from inventory"
+msgstr ""
+
+#: src/components/form/InputImage.tsx:105
+#, c-format
+msgid "Image should be smaller than 1 MB"
+msgstr ""
+
+#: src/components/form/InputImage.tsx:110
+#, c-format
+msgid "Add"
+msgstr ""
+
+#: src/components/form/InputImage.tsx:115
+#, c-format
+msgid "Remove"
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:113
+#, c-format
+msgid "No taxes configured for this product."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:119
+#, c-format
+msgid "Amount"
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:120
+#, c-format
+msgid ""
+"Taxes can be in currencies that differ from the main currency used by the "
+"merchant."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:122
+#, c-format
+msgid ""
+"Enter currency and value separated with a colon, e.g. &quot;USD:2.3&quot;."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:131
+#, c-format
+msgid "Legal name of the tax, e.g. VAT or import duties."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:137
+#, c-format
+msgid "add tax to the tax list"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:72
+#, c-format
+msgid "describe and add a product that is not in the inventory list"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:75
+#, c-format
+msgid "Add custom product"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:86
+#, c-format
+msgid "Complete information of the product"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:185
+#, c-format
+msgid "Image"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:186
+#, c-format
+msgid "photo of the product"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:192
+#, c-format
+msgid "full product description"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:196
+#, c-format
+msgid "Unit"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:197
+#, c-format
+msgid "name of the product unit"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:201
+#, c-format
+msgid "Price"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:202
+#, c-format
+msgid "amount in the current currency"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:211
+#, c-format
+msgid "Taxes"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:38
+#, c-format
+msgid "image"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:41
+#, c-format
+msgid "description"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:44
+#, c-format
+msgid "quantity"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:47
+#, c-format
+msgid "unit price"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:50
+#, c-format
+msgid "total price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:153
+#, c-format
+msgid "required"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:157
+#, c-format
+msgid "not valid"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:159
+#, c-format
+msgid "must be greater than 0"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:164
+#, c-format
+msgid "not a valid json"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:170
+#, c-format
+msgid "should be in the future"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:173
+#, c-format
+msgid "refund deadline cannot be before pay deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:179
+#, c-format
+msgid "wire transfer deadline cannot be before refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:190
+#, c-format
+msgid "wire transfer deadline cannot be before pay deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:197
+#, c-format
+msgid "should have a refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:202
+#, c-format
+msgid "auto refund cannot be after refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:360
+#, c-format
+msgid "Manage products in order"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:369
+#, c-format
+msgid "Manage list of products in the order."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:391
+#, c-format
+msgid "Remove this product from the order."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:415
+#, c-format
+msgid "Total price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:417
+#, c-format
+msgid "total product price added up"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:430
+#, c-format
+msgid "Amount to be paid by the customer"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:436
+#, c-format
+msgid "Order price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:437
+#, c-format
+msgid "final order price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:444
+#, c-format
+msgid "Summary"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:445
+#, c-format
+msgid "Title of the order to be shown to the customer"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:450
+#, c-format
+msgid "Shipping and Fulfillment"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:455
+#, c-format
+msgid "Delivery date"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:456
+#, c-format
+msgid "Deadline for physical delivery assured by the merchant."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:461
+#, c-format
+msgid "Location"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:462
+#, c-format
+msgid "address where the products will be delivered"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:469
+#, c-format
+msgid "Fulfillment URL"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:470
+#, c-format
+msgid "URL to which the user will be redirected after successful payment."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:476
+#, c-format
+msgid "Taler payment options"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:477
+#, c-format
+msgid "Override default Taler payment settings for this order"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:481
+#, c-format
+msgid "Payment deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:482
+#, c-format
+msgid ""
+"Deadline for the customer to pay for the offer before it expires. Inventory "
+"products will be reserved until this deadline."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:486
+#, c-format
+msgid "Refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:487
+#, c-format
+msgid "Time until which the order can be refunded by the merchant."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:491
+#, c-format
+msgid "Wire transfer deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:492
+#, c-format
+msgid "Deadline for the exchange to make the wire transfer."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:496
+#, c-format
+msgid "Auto-refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:497
+#, c-format
+msgid ""
+"Time until which the wallet will automatically check for refunds without "
+"user interaction."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:502
+#, c-format
+msgid "Maximum deposit fee"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:503
+#, c-format
+msgid ""
+"Maximum deposit fees the merchant is willing to cover for this order. Higher "
+"deposit fees must be covered in full by the consumer."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:507
+#, c-format
+msgid "Maximum wire fee"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:508
+#, c-format
+msgid ""
+"Maximum aggregate wire fees the merchant is willing to cover for this order. "
+"Wire fees exceeding this amount are to be covered by the customers."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:512
+#, c-format
+msgid "Wire fee amortization"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:513
+#, c-format
+msgid ""
+"Factor by which wire fees exceeding the above threshold are divided to "
+"determine the share of excess wire fees to be paid explicitly by the "
+"consumer."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:517
+#, c-format
+msgid "Create token"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:518
+#, c-format
+msgid ""
+"Uncheck this option if the merchant backend generated an order ID with "
+"enough entropy to prevent adversarial claims."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:522
+#, c-format
+msgid "Minimum age required"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:523
+#, c-format
+msgid ""
+"Any value greater than 0 will limit the coins able be used to pay this "
+"contract. If empty the age restriction will be defined by the products"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:526
+#, c-format
+msgid "Min age defined by the producs is %1$s"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:534
+#, c-format
+msgid "Additional information"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:535
+#, c-format
+msgid "Custom information to be included in the contract for this order."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:541
+#, c-format
+msgid "You must enter a value in JavaScript Object Notation (JSON)."
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:55
+#, c-format
+msgid "days"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:65
+#, c-format
+msgid "hours"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:76
+#, c-format
+msgid "minutes"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:87
+#, c-format
+msgid "seconds"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:53
+#, c-format
+msgid "forever"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:62
+#, c-format
+msgid "%1$sM"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:64
+#, c-format
+msgid "%1$sY"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:66
+#, c-format
+msgid "%1$sd"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:68
+#, c-format
+msgid "%1$sh"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:70
+#, c-format
+msgid "%1$smin"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:72
+#, c-format
+msgid "%1$ssec"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:75
+#, c-format
+msgid "Orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:81
+#, c-format
+msgid "create order"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:147
+#, c-format
+msgid "load newer orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:154
+#, c-format
+msgid "Date"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:200
+#, c-format
+msgid "Refund"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:209
+#, c-format
+msgid "copy url"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:225
+#, c-format
+msgid "load older orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:242
+#, c-format
+msgid "No orders have been found matching your query!"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:288
+#, c-format
+msgid "duplicated"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:299
+#, c-format
+msgid "invalid format"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:301
+#, c-format
+msgid "this value exceed the refundable amount"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:346
+#, c-format
+msgid "date"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:349
+#, c-format
+msgid "amount"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:352
+#, c-format
+msgid "reason"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:389
+#, c-format
+msgid "amount to be refunded"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:391
+#, c-format
+msgid "Max refundable:"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:396
+#, c-format
+msgid "Reason"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:397
+#, c-format
+msgid "Choose one..."
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:399
+#, c-format
+msgid "requested by the customer"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:400
+#, c-format
+msgid "other"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:403
+#, c-format
+msgid "why this order is being refunded"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:409
+#, c-format
+msgid "more information to give context"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:62
+#, c-format
+msgid "Contract Terms"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:68
+#, c-format
+msgid "human-readable description of the whole purchase"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:74
+#, c-format
+msgid "total price for the transaction"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:81
+#, c-format
+msgid "URL for this purchase"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:87
+#, c-format
+msgid "Max fee"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:88
+#, c-format
+msgid "maximum total deposit fee accepted by the merchant for this contract"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:93
+#, c-format
+msgid "Max wire fee"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:94
+#, c-format
+msgid "maximum wire fee accepted by the merchant"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:100
+#, c-format
+msgid ""
+"over how many customer transactions does the merchant expect to amortize "
+"wire fees on average"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:105
+#, c-format
+msgid "Created at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:106
+#, c-format
+msgid "time when this contract was generated"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:112
+#, c-format
+msgid "after this deadline has passed no refunds will be accepted"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:118
+#, c-format
+msgid ""
+"after this deadline, the merchant won't accept payments for the contract"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:124
+#, c-format
+msgid "transfer deadline for the exchange"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:130
+#, c-format
+msgid "time indicating when the order should be delivered"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:136
+#, c-format
+msgid "where the order will be delivered"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:144
+#, c-format
+msgid "Auto-refund delay"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:145
+#, c-format
+msgid ""
+"how long the wallet should try to get an automatic refund for the purchase"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:150
+#, c-format
+msgid "Extra info"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:151
+#, c-format
+msgid "extra data that is only interpreted by the merchant frontend"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:219
+#, c-format
+msgid "Order"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:221
+#, c-format
+msgid "claimed"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:247
+#, c-format
+msgid "claimed at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:265
+#, c-format
+msgid "Timeline"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:271
+#, c-format
+msgid "Payment details"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:291
+#, c-format
+msgid "Order status"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:301
+#, c-format
+msgid "Product list"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:451
+#, c-format
+msgid "paid"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:455
+#, c-format
+msgid "wired"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:460
+#, c-format
+msgid "refunded"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:480
+#, c-format
+msgid "refund order"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:481
+#, c-format
+msgid "not refundable"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:489
+#, c-format
+msgid "refund"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:553
+#, c-format
+msgid "Refunded amount"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:560
+#, c-format
+msgid "Refund taken"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:570
+#, c-format
+msgid "Status URL"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:583
+#, c-format
+msgid "Refund URI"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:636
+#, c-format
+msgid "unpaid"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:654
+#, c-format
+msgid "pay at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:666
+#, c-format
+msgid "created at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:707
+#, c-format
+msgid "Order status URL"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:711
+#, c-format
+msgid "Payment URI"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:740
+#, c-format
+msgid ""
+"Unknown order status. This is an error, please contact the administrator."
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:767
+#, c-format
+msgid "Back"
+msgstr "Zurück"
+
+#: src/paths/instance/orders/details/index.tsx:79
+#, c-format
+msgid "refund created successfully"
+msgstr ""
+
+#: src/paths/instance/orders/details/index.tsx:85
+#, c-format
+msgid "could not create the refund"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:78
+#, c-format
+msgid "select date to show nearby orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:94
+#, c-format
+msgid "order id"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:100
+#, c-format
+msgid "jump to order with the given order ID"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:122
+#, c-format
+msgid "remove all filters"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:132
+#, c-format
+msgid "only show paid orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:135
+#, c-format
+msgid "Paid"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:142
+#, c-format
+msgid "only show orders with refunds"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:145
+#, c-format
+msgid "Refunded"
+msgstr "Rückerstattet"
+
+#: src/paths/instance/orders/list/ListPage.tsx:152
+#, c-format
+msgid ""
+"only show orders where customers paid, but wire payments from payment "
+"provider are still pending"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:155
+#, c-format
+msgid "Not wired"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:170
+#, c-format
+msgid "clear date filter"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:184
+#, c-format
+msgid "date (YYYY/MM/DD)"
+msgstr ""
+
+#: src/paths/instance/orders/list/index.tsx:103
+#, c-format
+msgid "Enter an order id"
+msgstr ""
+
+#: src/paths/instance/orders/list/index.tsx:111
+#, c-format
+msgid "order not found"
+msgstr ""
+
+#: src/paths/instance/orders/list/index.tsx:178
+#, c-format
+msgid "could not get the order to refund"
+msgstr ""
+
+#: src/components/exception/AsyncButton.tsx:43
+#, c-format
+msgid "Loading..."
+msgstr ""
+
+#: src/components/form/InputStock.tsx:99
+#, c-format
+msgid ""
+"click here to configure the stock of the product, leave it as is and the "
+"backend will not control stock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:109
+#, c-format
+msgid "Manage stock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:115
+#, c-format
+msgid "this product has been configured without stock control"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:119
+#, c-format
+msgid "Infinite"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:136
+#, c-format
+msgid "lost cannot be greater than current and incoming (max %1$s)"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:176
+#, c-format
+msgid "Incoming"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:177
+#, c-format
+msgid "Lost"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:192
+#, c-format
+msgid "Current"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:196
+#, c-format
+msgid "remove stock control for this product"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:202
+#, c-format
+msgid "without stock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:211
+#, c-format
+msgid "Next restock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:217
+#, c-format
+msgid "Delivery address"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:133
+#, c-format
+msgid "product identification to use in URLs (for internal use only)"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:139
+#, c-format
+msgid "illustration of the product for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:145
+#, c-format
+msgid "product description for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:149
+#, c-format
+msgid "Age restricted"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:150
+#, c-format
+msgid "is this product restricted for customer below certain age?"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:155
+#, c-format
+msgid ""
+"unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 "
+"items, 5 meters) for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:160
+#, c-format
+msgid ""
+"sale price for customers, including taxes, for above units of the product"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:164
+#, c-format
+msgid "Stock"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:166
+#, c-format
+msgid ""
+"product inventory for products with finite supply (for internal use only)"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:171
+#, c-format
+msgid "taxes included in the product price, exposed to customers"
+msgstr ""
+
+#: src/paths/instance/products/create/CreatePage.tsx:66
+#, c-format
+msgid "Need to complete marked fields"
+msgstr ""
+
+#: src/paths/instance/products/create/index.tsx:51
+#, c-format
+msgid "could not create product"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:68
+#, c-format
+msgid "Products"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:73
+#, c-format
+msgid "add product to inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:137
+#, c-format
+msgid "Sell"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:143
+#, c-format
+msgid "Profit"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:149
+#, c-format
+msgid "Sold"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:210
+#, c-format
+msgid "free"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:248
+#, c-format
+msgid "go to product update page"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:255
+#, c-format
+msgid "Update"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:260
+#, c-format
+msgid "remove this product from the database"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:331
+#, c-format
+msgid "update the product with new price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:341
+#, c-format
+msgid "update product with new price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:399
+#, c-format
+msgid "add more elements to the inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:404
+#, c-format
+msgid "report elements lost in the inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:409
+#, c-format
+msgid "new price for the product"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:421
+#, c-format
+msgid "the are value with errors"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:422
+#, c-format
+msgid "update product with new stock and price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:463
+#, c-format
+msgid "There is no products yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:86
+#, c-format
+msgid "product updated successfully"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:92
+#, c-format
+msgid "could not update the product"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:103
+#, c-format
+msgid "product delete successfully"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:109
+#, c-format
+msgid "could not delete the product"
+msgstr ""
+
+#: src/paths/instance/products/update/UpdatePage.tsx:56
+#, c-format
+msgid "Product id:"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:95
+#, c-format
+msgid ""
+"To complete the setup of the reserve, you must now initiate a wire transfer "
+"using the given wire transfer subject and crediting the specified amount to "
+"the indicated account of the exchange."
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:102
+#, c-format
+msgid "If your system supports RFC 8905, you can do this by opening this URI:"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:83
+#, c-format
+msgid "it should be greater than 0"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:88
+#, c-format
+msgid "must be a valid URL"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:107
+#, c-format
+msgid "Initial balance"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:108
+#, c-format
+msgid "balance prior to deposit"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:112
+#, c-format
+msgid "Exchange URL"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:113
+#, c-format
+msgid "URL of exchange"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:148
+#, c-format
+msgid "Next"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:186
+#, c-format
+msgid "Wire method"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:187
+#, c-format
+msgid "method to use for wire transfer"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:189
+#, c-format
+msgid "Select one wire method"
+msgstr ""
+
+#: src/paths/instance/reserves/create/index.tsx:62
+#, c-format
+msgid "could not create reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:77
+#, c-format
+msgid "Valid until"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:82
+#, c-format
+msgid "Created balance"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:99
+#, c-format
+msgid "Exchange balance"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:104
+#, c-format
+msgid "Picked up"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:109
+#, c-format
+msgid "Committed"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:116
+#, c-format
+msgid "Account address"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:119
+#, c-format
+msgid "Subject"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:130
+#, c-format
+msgid "Tips"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:193
+#, c-format
+msgid "No tips has been authorized from this reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:213
+#, c-format
+msgid "Authorized"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:222
+#, c-format
+msgid "Expiration"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:108
+#, c-format
+msgid "amount of tip"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:112
+#, c-format
+msgid "Justification"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:114
+#, c-format
+msgid "reason for the tip"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:118
+#, c-format
+msgid "URL after tip"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:119
+#, c-format
+msgid "URL to visit after tip payment"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:65
+#, c-format
+msgid "Reserves not yet funded"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:89
+#, c-format
+msgid "Reserves ready"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:95
+#, c-format
+msgid "add new reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:143
+#, c-format
+msgid "Expires at"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:146
+#, c-format
+msgid "Initial"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:202
+#, c-format
+msgid "delete selected reserve from the database"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:210
+#, c-format
+msgid "authorize new tip from selected reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:237
+#, c-format
+msgid ""
+"There is no ready reserves yet, add more pressing the + sign or fund them"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:264
+#, c-format
+msgid "Expected Balance"
+msgstr ""
+
+#: src/paths/instance/reserves/list/index.tsx:110
+#, c-format
+msgid "could not create the tip"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:77
+#, c-format
+msgid "should not be empty"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:93
+#, c-format
+msgid "should be greater that 0"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:96
+#, c-format
+msgid "can't be empty"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:100
+#, c-format
+msgid "to short"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:108
+#, c-format
+msgid "just letters and numbers from 2 to 7"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:110
+#, c-format
+msgid "size of the key should be 32"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:137
+#, c-format
+msgid "Identifier"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:138
+#, c-format
+msgid "Name of the template in URLs."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:144
+#, c-format
+msgid "Describe what this template stands for"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:149
+#, c-format
+msgid "Fixed summary"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:150
+#, c-format
+msgid "If specified, this template will create order with the same summary"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:154
+#, c-format
+msgid "Fixed price"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:155
+#, c-format
+msgid "If specified, this template will create order with the same price"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:159
+#, c-format
+msgid "Minimum age"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:161
+#, c-format
+msgid "Is this contract restricted to some age?"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:165
+#, c-format
+msgid "Payment timeout"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:167
+#, c-format
+msgid ""
+"How much time has the customer to complete the payment once the order was "
+"created."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:171
+#, c-format
+msgid "Verification algorithm"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:172
+#, c-format
+msgid "Algorithm to use to verify transaction in offline mode"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:180
+#, c-format
+msgid "Point-of-sale key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:182
+#, c-format
+msgid "Useful to validate the purchase"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:196
+#, c-format
+msgid "generate random secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:203
+#, c-format
+msgid "random"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:208
+#, c-format
+msgid "show secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:209
+#, c-format
+msgid "hide secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:216
+#, c-format
+msgid "hide"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:218
+#, c-format
+msgid "show"
+msgstr ""
+
+#: src/paths/instance/templates/create/index.tsx:52
+#, c-format
+msgid "could not inform template"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:54
+#, c-format
+msgid "Amount is required"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:58
+#, c-format
+msgid "Order summary is required"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:86
+#, c-format
+msgid "New order for template"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:108
+#, c-format
+msgid "Amount of the order"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:113
+#, c-format
+msgid "Order summary"
+msgstr ""
+
+#: src/paths/instance/templates/use/index.tsx:92
+#, c-format
+msgid "could not create order from template"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:131
+#, c-format
+msgid ""
+"Here you can specify a default value for fields that are not fixed. Default "
+"values can be edited by the customer before the payment."
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:148
+#, c-format
+msgid "Fixed amount"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:149
+#, c-format
+msgid "Default amount"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:161
+#, c-format
+msgid "Default summary"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:177
+#, c-format
+msgid "Print"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:184
+#, c-format
+msgid "Setup TOTP"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:65
+#, c-format
+msgid "Templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:70
+#, c-format
+msgid "add new templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:142
+#, c-format
+msgid "load more templates before the first one"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:146
+#, c-format
+msgid "load newer templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:181
+#, c-format
+msgid "delete selected templates from the database"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:188
+#, c-format
+msgid "use template to create new order"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:195
+#, c-format
+msgid "create qr code for the template"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:210
+#, c-format
+msgid "load more templates after the last one"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:214
+#, c-format
+msgid "load older templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:231
+#, c-format
+msgid "There is no templates yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/templates/list/index.tsx:104
+#, c-format
+msgid "template delete successfully"
+msgstr ""
+
+#: src/paths/instance/templates/list/index.tsx:110
+#, c-format
+msgid "could not delete the template"
+msgstr ""
+
+#: src/paths/instance/templates/update/index.tsx:90
+#, c-format
+msgid "could not update template"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:57
+#, c-format
+msgid "should be one of '%1$s'"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:85
+#, c-format
+msgid "Webhook ID to use"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:89
+#, c-format
+msgid "Event"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:90
+#, c-format
+msgid "The event of the webhook: why the webhook is used"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:94
+#, c-format
+msgid "Method"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:95
+#, c-format
+msgid "Method used by the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:99
+#, c-format
+msgid "URL"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:100
+#, c-format
+msgid "URL of the webhook where the customer will be redirected"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:104
+#, c-format
+msgid "Header"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:106
+#, c-format
+msgid "Header template of the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:111
+#, c-format
+msgid "Body"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:112
+#, c-format
+msgid "Body template by the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:61
+#, c-format
+msgid "Webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:66
+#, c-format
+msgid "add new webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:137
+#, c-format
+msgid "load more webhooks before the first one"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:141
+#, c-format
+msgid "load newer webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:151
+#, c-format
+msgid "Event type"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:176
+#, c-format
+msgid "delete selected webhook from the database"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:198
+#, c-format
+msgid "load more webhooks after the last one"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:202
+#, c-format
+msgid "load older webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:219
+#, c-format
+msgid "There is no webhooks yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/index.tsx:94
+#, c-format
+msgid "webhook delete successfully"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/index.tsx:100
+#, c-format
+msgid "could not delete the webhook"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:63
+#, c-format
+msgid "check the id, does not look valid"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:65
+#, c-format
+msgid "should have 52 characters, current %1$s"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:72
+#, c-format
+msgid "URL doesn't have the right format"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:98
+#, c-format
+msgid "Credited bank account"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:100
+#, c-format
+msgid "Select one account"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:101
+#, c-format
+msgid "Bank account of the merchant where the payment was received"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:105
+#, c-format
+msgid "Wire transfer ID"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:107
+#, c-format
+msgid ""
+"unique identifier of the wire transfer used by the exchange, must be 52 "
+"characters long"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:112
+#, c-format
+msgid ""
+"Base URL of the exchange that made the transfer, should have been in the "
+"wire transfer subject"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:117
+#, c-format
+msgid "Amount credited"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:118
+#, c-format
+msgid "Actual amount that was wired to the merchant's bank account"
+msgstr ""
+
+#: src/paths/instance/transfers/create/index.tsx:58
+#, c-format
+msgid "could not inform transfer"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:61
+#, c-format
+msgid "Transfers"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:66
+#, c-format
+msgid "add new transfer"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:129
+#, c-format
+msgid "load more transfers before the first one"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:133
+#, c-format
+msgid "load newer transfers"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:143
+#, c-format
+msgid "Credit"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:152
+#, c-format
+msgid "Confirmed"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:155
+#, c-format
+msgid "Verified"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:158
+#, c-format
+msgid "Executed at"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:171
+#, c-format
+msgid "yes"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:171
+#, c-format
+msgid "no"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:181
+#, c-format
+msgid "unknown"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:187
+#, c-format
+msgid "delete selected transfer from the database"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:202
+#, c-format
+msgid "load more transfer after the last one"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:206
+#, c-format
+msgid "load older transfers"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:223
+#, c-format
+msgid "There is no transfer yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:79
+#, c-format
+msgid "filter by account address"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:100
+#, c-format
+msgid "only show wire transfers confirmed by the merchant"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:110
+#, c-format
+msgid "only show wire transfers claimed by the exchange"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:113
+#, c-format
+msgid "Unverified"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:69
+#, c-format
+msgid "is not valid"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:94
+#, c-format
+msgid "is not a number"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:96
+#, c-format
+msgid "must be 1 or greater"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:107
+#, c-format
+msgid "max 7 lines"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:178
+#, c-format
+msgid "change authorization configuration"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:217
+#, c-format
+msgid "Need to complete marked fields and choose authorization method"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:82
+#, c-format
+msgid "This is not a valid bitcoin address."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:95
+#, c-format
+msgid "This is not a valid Ethereum address."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:118
+#, c-format
+msgid "IBAN numbers usually have more that 4 digits"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:120
+#, c-format
+msgid "IBAN numbers usually have less that 34 digits"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:128
+#, c-format
+msgid "IBAN country code not found"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:153
+#, c-format
+msgid "IBAN number is not valid, checksum is wrong"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:248
+#, c-format
+msgid "Target type"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:249
+#, c-format
+msgid "Method to use for wire transfer"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:258
+#, c-format
+msgid "Routing"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:259
+#, c-format
+msgid "Routing number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:263
+#, c-format
+msgid "Account"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:264
+#, c-format
+msgid "Account number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:273
+#, c-format
+msgid "Business Identifier Code."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:282
+#, c-format
+msgid "Bank Account Number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:292
+#, c-format
+msgid "Unified Payment Interface."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:301
+#, c-format
+msgid "Bitcoin protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:310
+#, c-format
+msgid "Ethereum protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:319
+#, c-format
+msgid "Interledger protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:328
+#, c-format
+msgid "Host"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:329
+#, c-format
+msgid "Bank host."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:334
+#, c-format
+msgid "Bank account."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:343
+#, c-format
+msgid "Bank account owner's name."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:370
+#, c-format
+msgid "No accounts yet."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:52
+#, c-format
+msgid ""
+"Name of the instance in URLs. The 'default' instance is special in that it "
+"is used to administer other instances."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:58
+#, c-format
+msgid "Business name"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:59
+#, c-format
+msgid "Legal name of the business represented by this instance."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:64
+#, c-format
+msgid "Email"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:65
+#, c-format
+msgid "Contact email"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:70
+#, c-format
+msgid "Website URL"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:71
+#, c-format
+msgid "URL."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:76
+#, c-format
+msgid "Logo"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:77
+#, c-format
+msgid "Logo image."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:82
+#, c-format
+msgid "Bank account"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:83
+#, c-format
+msgid "URI specifying bank account for crediting revenue."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:88
+#, c-format
+msgid "Default max deposit fee"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:89
+#, c-format
+msgid ""
+"Maximum deposit fees this merchant is willing to pay per order by default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:94
+#, c-format
+msgid "Default max wire fee"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:95
+#, c-format
+msgid ""
+"Maximum wire fees this merchant is willing to pay per wire transfer by "
+"default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:100
+#, c-format
+msgid "Default wire fee amortization"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:101
+#, c-format
+msgid ""
+"Number of orders excess wire transfer fees will be divided by to compute per "
+"order surcharge."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:107
+#, c-format
+msgid "Physical location of the merchant."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:114
+#, c-format
+msgid "Jurisdiction"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:115
+#, c-format
+msgid "Jurisdiction for legal disputes with the merchant."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:122
+#, c-format
+msgid "Default payment delay"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:124
+#, c-format
+msgid ""
+"Time customers have to pay an order before the offer expires by default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:129
+#, c-format
+msgid "Default wire transfer delay"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:130
+#, c-format
+msgid ""
+"Maximum time an exchange is allowed to delay wiring funds to the merchant, "
+"enabling it to aggregate smaller payments into larger wire transfers and "
+"reducing wire fees."
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:164
+#, c-format
+msgid "Instance id"
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:173
+#, c-format
+msgid "Change the authorization method use for this instance."
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:182
+#, c-format
+msgid "Manage access token"
+msgstr ""
+
+#: src/paths/instance/update/index.tsx:112
+#, c-format
+msgid "Failed to create instance"
+msgstr ""
+
+#: src/components/exception/login.tsx:74
+#, c-format
+msgid "Login required"
+msgstr ""
+
+#: src/components/exception/login.tsx:80
+#, c-format
+msgid "Please enter your access token."
+msgstr ""
+
+#: src/components/exception/login.tsx:108
+#, c-format
+msgid "Access Token"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:171
+#, c-format
+msgid "The request to the backend take too long and was cancelled"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:172
+#, c-format
+msgid "Diagnostic from %1$s is \"%2$s\""
+msgstr ""
+
+#: src/InstanceRoutes.tsx:178
+#, c-format
+msgid "The backend reported a problem: HTTP status #%1$s"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:179
+#, c-format
+msgid "Diagnostic from %1$s is '%2$s'"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:196
+#, c-format
+msgid "Access denied"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:197
+#, c-format
+msgid "The access token provided is invalid."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:212
+#, c-format
+msgid "No 'default' instance configured yet."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:213
+#, c-format
+msgid "Create a 'default' instance to begin using the merchant backoffice."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:630
+#, c-format
+msgid "The access token provided is invalid"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:664
+#, c-format
+msgid "Hide for today"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:82
+#, c-format
+msgid "Instance"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:91
+#, c-format
+msgid "Settings"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:167
+#, c-format
+msgid "Connection"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:209
+#, c-format
+msgid "New"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:219
+#, c-format
+msgid "List"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:234
+#, c-format
+msgid "Log out"
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:71
+#, c-format
+msgid "Check your token is valid"
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:90
+#, c-format
+msgid "Couldn't access the server."
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:91
+#, c-format
+msgid "Could not infer instance id from url %1$s"
+msgstr ""
+
+#: src/Application.tsx:104
+#, c-format
+msgid "Server not found"
+msgstr ""
+
+#: src/Application.tsx:118
+#, c-format
+msgid "Server response with an error code"
+msgstr ""
+
+#: src/Application.tsx:120
+#, c-format
+msgid "Got message %1$s from %2$s"
+msgstr ""
+
+#: src/Application.tsx:131
+#, c-format
+msgid "Response from server is unreadable, http status: %1$s"
+msgstr ""
+
+#: src/Application.tsx:144
+#, c-format
+msgid "Unexpected Error"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:101
+#, c-format
+msgid "The value %1$s is invalid for a payment url"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:110
+#, c-format
+msgid "add element to the list"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:112
+#, c-format
+msgid "add"
+msgstr ""
+
+#: src/components/form/InputSecured.tsx:37
+#, c-format
+msgid "Deleting"
+msgstr ""
+
+#: src/components/form/InputSecured.tsx:41
+#, c-format
+msgid "Changing"
+msgstr ""
+
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:87
+#, c-format
+msgid "Order ID"
+msgstr ""
+
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:101
+#, c-format
+msgid "Payment URL"
+msgstr ""
diff --git a/packages/auditor-backoffice-ui/src/i18n/en.po b/packages/auditor-backoffice-ui/src/i18n/en.po
new file mode 100644
index 000000000..d8d0bae29
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/i18n/en.po
@@ -0,0 +1,2741 @@
+# This file is part of TALER
+# (C) 2016 GNUnet e.V.
+#
+# TALER is free software; you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 3, or (at your option) any later version.
+#
+# TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Wallet\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"POT-Creation-Date: 2016-11-23 00:00+0100\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: src/components/modal/index.tsx:71
+#, c-format
+msgid "Cancel"
+msgstr ""
+
+#: src/components/modal/index.tsx:79
+#, c-format
+msgid "%1$s"
+msgstr ""
+
+#: src/components/modal/index.tsx:84
+#, c-format
+msgid "Close"
+msgstr ""
+
+#: src/components/modal/index.tsx:124
+#, c-format
+msgid "Continue"
+msgstr ""
+
+#: src/components/modal/index.tsx:178
+#, c-format
+msgid "Clear"
+msgstr ""
+
+#: src/components/modal/index.tsx:190
+#, c-format
+msgid "Confirm"
+msgstr ""
+
+#: src/components/modal/index.tsx:296
+#, c-format
+msgid "is not the same as the current access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:299
+#, c-format
+msgid "cannot be empty"
+msgstr ""
+
+#: src/components/modal/index.tsx:301
+#, c-format
+msgid "cannot be the same as the old token"
+msgstr ""
+
+#: src/components/modal/index.tsx:305
+#, c-format
+msgid "is not the same"
+msgstr ""
+
+#: src/components/modal/index.tsx:315
+#, c-format
+msgid "You are updating the access token from instance with id %1$s"
+msgstr ""
+
+#: src/components/modal/index.tsx:331
+#, c-format
+msgid "Old access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:332
+#, c-format
+msgid "access token currently in use"
+msgstr ""
+
+#: src/components/modal/index.tsx:338
+#, c-format
+msgid "New access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:339
+#, c-format
+msgid "next access token to be used"
+msgstr ""
+
+#: src/components/modal/index.tsx:344
+#, c-format
+msgid "Repeat access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:345
+#, c-format
+msgid "confirm the same access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:350
+#, c-format
+msgid "Clearing the access token will mean public access to the instance"
+msgstr ""
+
+#: src/components/modal/index.tsx:377
+#, c-format
+msgid "cannot be the same as the old access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:394
+#, c-format
+msgid "You are setting the access token for the new instance"
+msgstr ""
+
+#: src/components/modal/index.tsx:420
+#, c-format
+msgid ""
+"With external authorization method no check will be done by the merchant "
+"backend"
+msgstr ""
+
+#: src/components/modal/index.tsx:436
+#, c-format
+msgid "Set external authorization"
+msgstr ""
+
+#: src/components/modal/index.tsx:448
+#, c-format
+msgid "Set access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:470
+#, c-format
+msgid "Operation in progress..."
+msgstr ""
+
+#: src/components/modal/index.tsx:479
+#, c-format
+msgid "The operation will be automatically canceled after %1$s seconds"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:80
+#, c-format
+msgid "Instances"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:93
+#, c-format
+msgid "Delete"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:99
+#, c-format
+msgid "add new instance"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:178
+#, c-format
+msgid "ID"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:181
+#, c-format
+msgid "Name"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:220
+#, c-format
+msgid "Edit"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:237
+#, c-format
+msgid "Purge"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:261
+#, c-format
+msgid "There is no instances yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:68
+#, c-format
+msgid "Only show active instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:71
+#, c-format
+msgid "Active"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:78
+#, c-format
+msgid "Only show deleted instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:81
+#, c-format
+msgid "Deleted"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:88
+#, c-format
+msgid "Show all instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:91
+#, c-format
+msgid "All"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:101
+#, c-format
+msgid "Instance \"%1$s\" (ID: %2$s) has been deleted"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:106
+#, c-format
+msgid "Failed to delete instance"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:124
+#, c-format
+msgid "Instance '%1$s' (ID: %2$s) has been disabled"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:129
+#, c-format
+msgid "Failed to purge instance"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:41
+#, c-format
+msgid "Pending KYC verification"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:66
+#, c-format
+msgid "Timed out"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:103
+#, c-format
+msgid "Exchange"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:106
+#, c-format
+msgid "Target account"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:109
+#, c-format
+msgid "KYC URL"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:144
+#, c-format
+msgid "Code"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:147
+#, c-format
+msgid "Http Status"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:177
+#, c-format
+msgid "No pending kyc verification!"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:123
+#, c-format
+msgid "change value to unknown date"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:124
+#, c-format
+msgid "change value to empty"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:131
+#, c-format
+msgid "clear"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:136
+#, c-format
+msgid "change value to never"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:141
+#, c-format
+msgid "never"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:29
+#, c-format
+msgid "Country"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:33
+#, c-format
+msgid "Address"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:39
+#, c-format
+msgid "Building number"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:41
+#, c-format
+msgid "Building name"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:42
+#, c-format
+msgid "Street"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:43
+#, c-format
+msgid "Post code"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:44
+#, c-format
+msgid "Town location"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:45
+#, c-format
+msgid "Town"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:46
+#, c-format
+msgid "District"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:49
+#, c-format
+msgid "Country subdivision"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:66
+#, c-format
+msgid "Product id"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:69
+#, c-format
+msgid "Description"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:94
+#, c-format
+msgid "Product"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:95
+#, c-format
+msgid "search products by it's description or id"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:151
+#, c-format
+msgid "no products found with that description"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:56
+#, c-format
+msgid "You must enter a valid product identifier."
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:64
+#, c-format
+msgid "Quantity must be greater than 0!"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:76
+#, c-format
+msgid ""
+"This quantity exceeds remaining stock. Currently, only %1$s units remain "
+"unreserved in stock."
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:109
+#, c-format
+msgid "Quantity"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:110
+#, c-format
+msgid "how many products will be added"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:117
+#, c-format
+msgid "Add from inventory"
+msgstr ""
+
+#: src/components/form/InputImage.tsx:105
+#, c-format
+msgid "Image should be smaller than 1 MB"
+msgstr ""
+
+#: src/components/form/InputImage.tsx:110
+#, c-format
+msgid "Add"
+msgstr ""
+
+#: src/components/form/InputImage.tsx:115
+#, c-format
+msgid "Remove"
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:113
+#, c-format
+msgid "No taxes configured for this product."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:119
+#, c-format
+msgid "Amount"
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:120
+#, c-format
+msgid ""
+"Taxes can be in currencies that differ from the main currency used by the "
+"merchant."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:122
+#, c-format
+msgid ""
+"Enter currency and value separated with a colon, e.g. &quot;USD:2.3&quot;."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:131
+#, c-format
+msgid "Legal name of the tax, e.g. VAT or import duties."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:137
+#, c-format
+msgid "add tax to the tax list"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:72
+#, c-format
+msgid "describe and add a product that is not in the inventory list"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:75
+#, c-format
+msgid "Add custom product"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:86
+#, c-format
+msgid "Complete information of the product"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:185
+#, c-format
+msgid "Image"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:186
+#, c-format
+msgid "photo of the product"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:192
+#, c-format
+msgid "full product description"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:196
+#, c-format
+msgid "Unit"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:197
+#, c-format
+msgid "name of the product unit"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:201
+#, c-format
+msgid "Price"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:202
+#, c-format
+msgid "amount in the current currency"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:211
+#, c-format
+msgid "Taxes"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:38
+#, c-format
+msgid "image"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:41
+#, c-format
+msgid "description"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:44
+#, c-format
+msgid "quantity"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:47
+#, c-format
+msgid "unit price"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:50
+#, c-format
+msgid "total price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:153
+#, c-format
+msgid "required"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:157
+#, c-format
+msgid "not valid"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:159
+#, c-format
+msgid "must be greater than 0"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:164
+#, c-format
+msgid "not a valid json"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:170
+#, c-format
+msgid "should be in the future"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:173
+#, c-format
+msgid "refund deadline cannot be before pay deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:179
+#, c-format
+msgid "wire transfer deadline cannot be before refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:190
+#, c-format
+msgid "wire transfer deadline cannot be before pay deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:197
+#, c-format
+msgid "should have a refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:202
+#, c-format
+msgid "auto refund cannot be after refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:360
+#, c-format
+msgid "Manage products in order"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:369
+#, c-format
+msgid "Manage list of products in the order."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:391
+#, c-format
+msgid "Remove this product from the order."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:415
+#, c-format
+msgid "Total price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:417
+#, c-format
+msgid "total product price added up"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:430
+#, c-format
+msgid "Amount to be paid by the customer"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:436
+#, c-format
+msgid "Order price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:437
+#, c-format
+msgid "final order price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:444
+#, c-format
+msgid "Summary"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:445
+#, c-format
+msgid "Title of the order to be shown to the customer"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:450
+#, c-format
+msgid "Shipping and Fulfillment"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:455
+#, c-format
+msgid "Delivery date"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:456
+#, c-format
+msgid "Deadline for physical delivery assured by the merchant."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:461
+#, c-format
+msgid "Location"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:462
+#, c-format
+msgid "address where the products will be delivered"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:469
+#, c-format
+msgid "Fulfillment URL"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:470
+#, c-format
+msgid "URL to which the user will be redirected after successful payment."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:476
+#, c-format
+msgid "Taler payment options"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:477
+#, c-format
+msgid "Override default Taler payment settings for this order"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:481
+#, c-format
+msgid "Payment deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:482
+#, c-format
+msgid ""
+"Deadline for the customer to pay for the offer before it expires. Inventory "
+"products will be reserved until this deadline."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:486
+#, c-format
+msgid "Refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:487
+#, c-format
+msgid "Time until which the order can be refunded by the merchant."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:491
+#, c-format
+msgid "Wire transfer deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:492
+#, c-format
+msgid "Deadline for the exchange to make the wire transfer."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:496
+#, c-format
+msgid "Auto-refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:497
+#, c-format
+msgid ""
+"Time until which the wallet will automatically check for refunds without "
+"user interaction."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:502
+#, c-format
+msgid "Maximum deposit fee"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:503
+#, c-format
+msgid ""
+"Maximum deposit fees the merchant is willing to cover for this order. Higher "
+"deposit fees must be covered in full by the consumer."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:507
+#, c-format
+msgid "Maximum wire fee"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:508
+#, c-format
+msgid ""
+"Maximum aggregate wire fees the merchant is willing to cover for this order. "
+"Wire fees exceeding this amount are to be covered by the customers."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:512
+#, c-format
+msgid "Wire fee amortization"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:513
+#, c-format
+msgid ""
+"Factor by which wire fees exceeding the above threshold are divided to "
+"determine the share of excess wire fees to be paid explicitly by the "
+"consumer."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:517
+#, c-format
+msgid "Create token"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:518
+#, c-format
+msgid ""
+"Uncheck this option if the merchant backend generated an order ID with "
+"enough entropy to prevent adversarial claims."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:522
+#, c-format
+msgid "Minimum age required"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:523
+#, c-format
+msgid ""
+"Any value greater than 0 will limit the coins able be used to pay this "
+"contract. If empty the age restriction will be defined by the products"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:526
+#, c-format
+msgid "Min age defined by the producs is %1$s"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:534
+#, c-format
+msgid "Additional information"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:535
+#, c-format
+msgid "Custom information to be included in the contract for this order."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:541
+#, c-format
+msgid "You must enter a value in JavaScript Object Notation (JSON)."
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:55
+#, c-format
+msgid "days"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:65
+#, c-format
+msgid "hours"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:76
+#, c-format
+msgid "minutes"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:87
+#, c-format
+msgid "seconds"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:53
+#, c-format
+msgid "forever"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:62
+#, c-format
+msgid "%1$sM"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:64
+#, c-format
+msgid "%1$sY"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:66
+#, c-format
+msgid "%1$sd"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:68
+#, c-format
+msgid "%1$sh"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:70
+#, c-format
+msgid "%1$smin"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:72
+#, c-format
+msgid "%1$ssec"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:75
+#, c-format
+msgid "Orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:81
+#, c-format
+msgid "create order"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:147
+#, c-format
+msgid "load newer orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:154
+#, c-format
+msgid "Date"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:200
+#, c-format
+msgid "Refund"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:209
+#, c-format
+msgid "copy url"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:225
+#, c-format
+msgid "load older orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:242
+#, c-format
+msgid "No orders have been found matching your query!"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:288
+#, c-format
+msgid "duplicated"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:299
+#, c-format
+msgid "invalid format"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:301
+#, c-format
+msgid "this value exceed the refundable amount"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:346
+#, c-format
+msgid "date"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:349
+#, c-format
+msgid "amount"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:352
+#, c-format
+msgid "reason"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:389
+#, c-format
+msgid "amount to be refunded"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:391
+#, c-format
+msgid "Max refundable:"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:396
+#, c-format
+msgid "Reason"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:397
+#, c-format
+msgid "Choose one..."
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:399
+#, c-format
+msgid "requested by the customer"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:400
+#, c-format
+msgid "other"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:403
+#, c-format
+msgid "why this order is being refunded"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:409
+#, c-format
+msgid "more information to give context"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:62
+#, c-format
+msgid "Contract Terms"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:68
+#, c-format
+msgid "human-readable description of the whole purchase"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:74
+#, c-format
+msgid "total price for the transaction"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:81
+#, c-format
+msgid "URL for this purchase"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:87
+#, c-format
+msgid "Max fee"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:88
+#, c-format
+msgid "maximum total deposit fee accepted by the merchant for this contract"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:93
+#, c-format
+msgid "Max wire fee"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:94
+#, c-format
+msgid "maximum wire fee accepted by the merchant"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:100
+#, c-format
+msgid ""
+"over how many customer transactions does the merchant expect to amortize "
+"wire fees on average"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:105
+#, c-format
+msgid "Created at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:106
+#, c-format
+msgid "time when this contract was generated"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:112
+#, c-format
+msgid "after this deadline has passed no refunds will be accepted"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:118
+#, c-format
+msgid ""
+"after this deadline, the merchant won't accept payments for the contract"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:124
+#, c-format
+msgid "transfer deadline for the exchange"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:130
+#, c-format
+msgid "time indicating when the order should be delivered"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:136
+#, c-format
+msgid "where the order will be delivered"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:144
+#, c-format
+msgid "Auto-refund delay"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:145
+#, c-format
+msgid ""
+"how long the wallet should try to get an automatic refund for the purchase"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:150
+#, c-format
+msgid "Extra info"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:151
+#, c-format
+msgid "extra data that is only interpreted by the merchant frontend"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:219
+#, c-format
+msgid "Order"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:221
+#, c-format
+msgid "claimed"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:247
+#, c-format
+msgid "claimed at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:265
+#, c-format
+msgid "Timeline"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:271
+#, c-format
+msgid "Payment details"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:291
+#, c-format
+msgid "Order status"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:301
+#, c-format
+msgid "Product list"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:451
+#, c-format
+msgid "paid"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:455
+#, c-format
+msgid "wired"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:460
+#, c-format
+msgid "refunded"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:480
+#, c-format
+msgid "refund order"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:481
+#, c-format
+msgid "not refundable"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:489
+#, c-format
+msgid "refund"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:553
+#, c-format
+msgid "Refunded amount"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:560
+#, c-format
+msgid "Refund taken"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:570
+#, c-format
+msgid "Status URL"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:583
+#, c-format
+msgid "Refund URI"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:636
+#, c-format
+msgid "unpaid"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:654
+#, c-format
+msgid "pay at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:666
+#, c-format
+msgid "created at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:707
+#, c-format
+msgid "Order status URL"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:711
+#, c-format
+msgid "Payment URI"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:740
+#, c-format
+msgid ""
+"Unknown order status. This is an error, please contact the administrator."
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:767
+#, c-format
+msgid "Back"
+msgstr ""
+
+#: src/paths/instance/orders/details/index.tsx:79
+#, c-format
+msgid "refund created successfully"
+msgstr ""
+
+#: src/paths/instance/orders/details/index.tsx:85
+#, c-format
+msgid "could not create the refund"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:78
+#, c-format
+msgid "select date to show nearby orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:94
+#, c-format
+msgid "order id"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:100
+#, c-format
+msgid "jump to order with the given order ID"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:122
+#, c-format
+msgid "remove all filters"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:132
+#, c-format
+msgid "only show paid orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:135
+#, c-format
+msgid "Paid"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:142
+#, c-format
+msgid "only show orders with refunds"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:145
+#, c-format
+msgid "Refunded"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:152
+#, c-format
+msgid ""
+"only show orders where customers paid, but wire payments from payment "
+"provider are still pending"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:155
+#, c-format
+msgid "Not wired"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:170
+#, c-format
+msgid "clear date filter"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:184
+#, c-format
+msgid "date (YYYY/MM/DD)"
+msgstr ""
+
+#: src/paths/instance/orders/list/index.tsx:103
+#, c-format
+msgid "Enter an order id"
+msgstr ""
+
+#: src/paths/instance/orders/list/index.tsx:111
+#, c-format
+msgid "order not found"
+msgstr ""
+
+#: src/paths/instance/orders/list/index.tsx:178
+#, c-format
+msgid "could not get the order to refund"
+msgstr ""
+
+#: src/components/exception/AsyncButton.tsx:43
+#, c-format
+msgid "Loading..."
+msgstr ""
+
+#: src/components/form/InputStock.tsx:99
+#, c-format
+msgid ""
+"click here to configure the stock of the product, leave it as is and the "
+"backend will not control stock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:109
+#, c-format
+msgid "Manage stock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:115
+#, c-format
+msgid "this product has been configured without stock control"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:119
+#, c-format
+msgid "Infinite"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:136
+#, c-format
+msgid "lost cannot be greater than current and incoming (max %1$s)"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:176
+#, c-format
+msgid "Incoming"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:177
+#, c-format
+msgid "Lost"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:192
+#, c-format
+msgid "Current"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:196
+#, c-format
+msgid "remove stock control for this product"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:202
+#, c-format
+msgid "without stock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:211
+#, c-format
+msgid "Next restock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:217
+#, c-format
+msgid "Delivery address"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:133
+#, c-format
+msgid "product identification to use in URLs (for internal use only)"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:139
+#, c-format
+msgid "illustration of the product for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:145
+#, c-format
+msgid "product description for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:149
+#, c-format
+msgid "Age restricted"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:150
+#, c-format
+msgid "is this product restricted for customer below certain age?"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:155
+#, c-format
+msgid ""
+"unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 "
+"items, 5 meters) for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:160
+#, c-format
+msgid ""
+"sale price for customers, including taxes, for above units of the product"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:164
+#, c-format
+msgid "Stock"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:166
+#, c-format
+msgid ""
+"product inventory for products with finite supply (for internal use only)"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:171
+#, c-format
+msgid "taxes included in the product price, exposed to customers"
+msgstr ""
+
+#: src/paths/instance/products/create/CreatePage.tsx:66
+#, c-format
+msgid "Need to complete marked fields"
+msgstr ""
+
+#: src/paths/instance/products/create/index.tsx:51
+#, c-format
+msgid "could not create product"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:68
+#, c-format
+msgid "Products"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:73
+#, c-format
+msgid "add product to inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:137
+#, c-format
+msgid "Sell"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:143
+#, c-format
+msgid "Profit"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:149
+#, c-format
+msgid "Sold"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:210
+#, c-format
+msgid "free"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:248
+#, c-format
+msgid "go to product update page"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:255
+#, c-format
+msgid "Update"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:260
+#, c-format
+msgid "remove this product from the database"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:331
+#, c-format
+msgid "update the product with new price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:341
+#, c-format
+msgid "update product with new price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:399
+#, c-format
+msgid "add more elements to the inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:404
+#, c-format
+msgid "report elements lost in the inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:409
+#, c-format
+msgid "new price for the product"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:421
+#, c-format
+msgid "the are value with errors"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:422
+#, c-format
+msgid "update product with new stock and price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:463
+#, c-format
+msgid "There is no products yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:86
+#, c-format
+msgid "product updated successfully"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:92
+#, c-format
+msgid "could not update the product"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:103
+#, c-format
+msgid "product delete successfully"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:109
+#, c-format
+msgid "could not delete the product"
+msgstr ""
+
+#: src/paths/instance/products/update/UpdatePage.tsx:56
+#, c-format
+msgid "Product id:"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:95
+#, c-format
+msgid ""
+"To complete the setup of the reserve, you must now initiate a wire transfer "
+"using the given wire transfer subject and crediting the specified amount to "
+"the indicated account of the exchange."
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:102
+#, c-format
+msgid "If your system supports RFC 8905, you can do this by opening this URI:"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:83
+#, c-format
+msgid "it should be greater than 0"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:88
+#, c-format
+msgid "must be a valid URL"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:107
+#, c-format
+msgid "Initial balance"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:108
+#, c-format
+msgid "balance prior to deposit"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:112
+#, c-format
+msgid "Exchange URL"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:113
+#, c-format
+msgid "URL of exchange"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:148
+#, c-format
+msgid "Next"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:186
+#, c-format
+msgid "Wire method"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:187
+#, c-format
+msgid "method to use for wire transfer"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:189
+#, c-format
+msgid "Select one wire method"
+msgstr ""
+
+#: src/paths/instance/reserves/create/index.tsx:62
+#, c-format
+msgid "could not create reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:77
+#, c-format
+msgid "Valid until"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:82
+#, c-format
+msgid "Created balance"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:99
+#, c-format
+msgid "Exchange balance"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:104
+#, c-format
+msgid "Picked up"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:109
+#, c-format
+msgid "Committed"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:116
+#, c-format
+msgid "Account address"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:119
+#, c-format
+msgid "Subject"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:130
+#, c-format
+msgid "Tips"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:193
+#, c-format
+msgid "No tips has been authorized from this reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:213
+#, c-format
+msgid "Authorized"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:222
+#, c-format
+msgid "Expiration"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:108
+#, c-format
+msgid "amount of tip"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:112
+#, c-format
+msgid "Justification"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:114
+#, c-format
+msgid "reason for the tip"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:118
+#, c-format
+msgid "URL after tip"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:119
+#, c-format
+msgid "URL to visit after tip payment"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:65
+#, c-format
+msgid "Reserves not yet funded"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:89
+#, c-format
+msgid "Reserves ready"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:95
+#, c-format
+msgid "add new reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:143
+#, c-format
+msgid "Expires at"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:146
+#, c-format
+msgid "Initial"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:202
+#, c-format
+msgid "delete selected reserve from the database"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:210
+#, c-format
+msgid "authorize new tip from selected reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:237
+#, c-format
+msgid ""
+"There is no ready reserves yet, add more pressing the + sign or fund them"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:264
+#, c-format
+msgid "Expected Balance"
+msgstr ""
+
+#: src/paths/instance/reserves/list/index.tsx:110
+#, c-format
+msgid "could not create the tip"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:77
+#, c-format
+msgid "should not be empty"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:93
+#, c-format
+msgid "should be greater that 0"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:96
+#, c-format
+msgid "can't be empty"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:100
+#, c-format
+msgid "to short"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:108
+#, c-format
+msgid "just letters and numbers from 2 to 7"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:110
+#, c-format
+msgid "size of the key should be 32"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:137
+#, c-format
+msgid "Identifier"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:138
+#, c-format
+msgid "Name of the template in URLs."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:144
+#, c-format
+msgid "Describe what this template stands for"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:149
+#, c-format
+msgid "Fixed summary"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:150
+#, c-format
+msgid "If specified, this template will create order with the same summary"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:154
+#, c-format
+msgid "Fixed price"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:155
+#, c-format
+msgid "If specified, this template will create order with the same price"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:159
+#, c-format
+msgid "Minimum age"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:161
+#, c-format
+msgid "Is this contract restricted to some age?"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:165
+#, c-format
+msgid "Payment timeout"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:167
+#, c-format
+msgid ""
+"How much time has the customer to complete the payment once the order was "
+"created."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:171
+#, c-format
+msgid "Verification algorithm"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:172
+#, c-format
+msgid "Algorithm to use to verify transaction in offline mode"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:180
+#, c-format
+msgid "Point-of-sale key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:182
+#, c-format
+msgid "Useful to validate the purchase"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:196
+#, c-format
+msgid "generate random secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:203
+#, c-format
+msgid "random"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:208
+#, c-format
+msgid "show secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:209
+#, c-format
+msgid "hide secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:216
+#, c-format
+msgid "hide"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:218
+#, c-format
+msgid "show"
+msgstr ""
+
+#: src/paths/instance/templates/create/index.tsx:52
+#, c-format
+msgid "could not inform template"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:54
+#, c-format
+msgid "Amount is required"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:58
+#, c-format
+msgid "Order summary is required"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:86
+#, c-format
+msgid "New order for template"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:108
+#, c-format
+msgid "Amount of the order"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:113
+#, c-format
+msgid "Order summary"
+msgstr ""
+
+#: src/paths/instance/templates/use/index.tsx:92
+#, c-format
+msgid "could not create order from template"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:131
+#, c-format
+msgid ""
+"Here you can specify a default value for fields that are not fixed. Default "
+"values can be edited by the customer before the payment."
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:148
+#, c-format
+msgid "Fixed amount"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:149
+#, c-format
+msgid "Default amount"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:161
+#, c-format
+msgid "Default summary"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:177
+#, c-format
+msgid "Print"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:184
+#, c-format
+msgid "Setup TOTP"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:65
+#, c-format
+msgid "Templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:70
+#, c-format
+msgid "add new templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:142
+#, c-format
+msgid "load more templates before the first one"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:146
+#, c-format
+msgid "load newer templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:181
+#, c-format
+msgid "delete selected templates from the database"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:188
+#, c-format
+msgid "use template to create new order"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:195
+#, c-format
+msgid "create qr code for the template"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:210
+#, c-format
+msgid "load more templates after the last one"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:214
+#, c-format
+msgid "load older templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:231
+#, c-format
+msgid "There is no templates yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/templates/list/index.tsx:104
+#, c-format
+msgid "template delete successfully"
+msgstr ""
+
+#: src/paths/instance/templates/list/index.tsx:110
+#, c-format
+msgid "could not delete the template"
+msgstr ""
+
+#: src/paths/instance/templates/update/index.tsx:90
+#, c-format
+msgid "could not update template"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:57
+#, c-format
+msgid "should be one of '%1$s'"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:85
+#, c-format
+msgid "Webhook ID to use"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:89
+#, c-format
+msgid "Event"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:90
+#, c-format
+msgid "The event of the webhook: why the webhook is used"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:94
+#, c-format
+msgid "Method"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:95
+#, c-format
+msgid "Method used by the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:99
+#, c-format
+msgid "URL"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:100
+#, c-format
+msgid "URL of the webhook where the customer will be redirected"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:104
+#, c-format
+msgid "Header"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:106
+#, c-format
+msgid "Header template of the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:111
+#, c-format
+msgid "Body"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:112
+#, c-format
+msgid "Body template by the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:61
+#, c-format
+msgid "Webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:66
+#, c-format
+msgid "add new webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:137
+#, c-format
+msgid "load more webhooks before the first one"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:141
+#, c-format
+msgid "load newer webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:151
+#, c-format
+msgid "Event type"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:176
+#, c-format
+msgid "delete selected webhook from the database"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:198
+#, c-format
+msgid "load more webhooks after the last one"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:202
+#, c-format
+msgid "load older webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:219
+#, c-format
+msgid "There is no webhooks yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/index.tsx:94
+#, c-format
+msgid "webhook delete successfully"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/index.tsx:100
+#, c-format
+msgid "could not delete the webhook"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:63
+#, c-format
+msgid "check the id, does not look valid"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:65
+#, c-format
+msgid "should have 52 characters, current %1$s"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:72
+#, c-format
+msgid "URL doesn't have the right format"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:98
+#, c-format
+msgid "Credited bank account"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:100
+#, c-format
+msgid "Select one account"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:101
+#, c-format
+msgid "Bank account of the merchant where the payment was received"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:105
+#, c-format
+msgid "Wire transfer ID"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:107
+#, c-format
+msgid ""
+"unique identifier of the wire transfer used by the exchange, must be 52 "
+"characters long"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:112
+#, c-format
+msgid ""
+"Base URL of the exchange that made the transfer, should have been in the "
+"wire transfer subject"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:117
+#, c-format
+msgid "Amount credited"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:118
+#, c-format
+msgid "Actual amount that was wired to the merchant's bank account"
+msgstr ""
+
+#: src/paths/instance/transfers/create/index.tsx:58
+#, c-format
+msgid "could not inform transfer"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:61
+#, c-format
+msgid "Transfers"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:66
+#, c-format
+msgid "add new transfer"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:129
+#, c-format
+msgid "load more transfers before the first one"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:133
+#, c-format
+msgid "load newer transfers"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:143
+#, c-format
+msgid "Credit"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:152
+#, c-format
+msgid "Confirmed"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:155
+#, c-format
+msgid "Verified"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:158
+#, c-format
+msgid "Executed at"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:171
+#, c-format
+msgid "yes"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:171
+#, c-format
+msgid "no"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:181
+#, c-format
+msgid "unknown"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:187
+#, c-format
+msgid "delete selected transfer from the database"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:202
+#, c-format
+msgid "load more transfer after the last one"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:206
+#, c-format
+msgid "load older transfers"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:223
+#, c-format
+msgid "There is no transfer yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:79
+#, c-format
+msgid "filter by account address"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:100
+#, c-format
+msgid "only show wire transfers confirmed by the merchant"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:110
+#, c-format
+msgid "only show wire transfers claimed by the exchange"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:113
+#, c-format
+msgid "Unverified"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:69
+#, c-format
+msgid "is not valid"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:94
+#, c-format
+msgid "is not a number"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:96
+#, c-format
+msgid "must be 1 or greater"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:107
+#, c-format
+msgid "max 7 lines"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:178
+#, c-format
+msgid "change authorization configuration"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:217
+#, c-format
+msgid "Need to complete marked fields and choose authorization method"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:82
+#, c-format
+msgid "This is not a valid bitcoin address."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:95
+#, c-format
+msgid "This is not a valid Ethereum address."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:118
+#, c-format
+msgid "IBAN numbers usually have more that 4 digits"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:120
+#, c-format
+msgid "IBAN numbers usually have less that 34 digits"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:128
+#, c-format
+msgid "IBAN country code not found"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:153
+#, c-format
+msgid "IBAN number is not valid, checksum is wrong"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:248
+#, c-format
+msgid "Target type"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:249
+#, c-format
+msgid "Method to use for wire transfer"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:258
+#, c-format
+msgid "Routing"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:259
+#, c-format
+msgid "Routing number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:263
+#, c-format
+msgid "Account"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:264
+#, c-format
+msgid "Account number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:273
+#, c-format
+msgid "Business Identifier Code."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:282
+#, c-format
+msgid "Bank Account Number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:292
+#, c-format
+msgid "Unified Payment Interface."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:301
+#, c-format
+msgid "Bitcoin protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:310
+#, c-format
+msgid "Ethereum protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:319
+#, c-format
+msgid "Interledger protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:328
+#, c-format
+msgid "Host"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:329
+#, c-format
+msgid "Bank host."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:334
+#, c-format
+msgid "Bank account."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:343
+#, c-format
+msgid "Bank account owner's name."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:370
+#, c-format
+msgid "No accounts yet."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:52
+#, c-format
+msgid ""
+"Name of the instance in URLs. The 'default' instance is special in that it "
+"is used to administer other instances."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:58
+#, c-format
+msgid "Business name"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:59
+#, c-format
+msgid "Legal name of the business represented by this instance."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:64
+#, c-format
+msgid "Email"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:65
+#, c-format
+msgid "Contact email"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:70
+#, c-format
+msgid "Website URL"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:71
+#, c-format
+msgid "URL."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:76
+#, c-format
+msgid "Logo"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:77
+#, c-format
+msgid "Logo image."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:82
+#, c-format
+msgid "Bank account"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:83
+#, c-format
+msgid "URI specifying bank account for crediting revenue."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:88
+#, c-format
+msgid "Default max deposit fee"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:89
+#, c-format
+msgid ""
+"Maximum deposit fees this merchant is willing to pay per order by default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:94
+#, c-format
+msgid "Default max wire fee"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:95
+#, c-format
+msgid ""
+"Maximum wire fees this merchant is willing to pay per wire transfer by "
+"default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:100
+#, c-format
+msgid "Default wire fee amortization"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:101
+#, c-format
+msgid ""
+"Number of orders excess wire transfer fees will be divided by to compute per "
+"order surcharge."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:107
+#, c-format
+msgid "Physical location of the merchant."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:114
+#, c-format
+msgid "Jurisdiction"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:115
+#, c-format
+msgid "Jurisdiction for legal disputes with the merchant."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:122
+#, c-format
+msgid "Default payment delay"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:124
+#, c-format
+msgid ""
+"Time customers have to pay an order before the offer expires by default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:129
+#, c-format
+msgid "Default wire transfer delay"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:130
+#, c-format
+msgid ""
+"Maximum time an exchange is allowed to delay wiring funds to the merchant, "
+"enabling it to aggregate smaller payments into larger wire transfers and "
+"reducing wire fees."
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:164
+#, c-format
+msgid "Instance id"
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:173
+#, c-format
+msgid "Change the authorization method use for this instance."
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:182
+#, c-format
+msgid "Manage access token"
+msgstr ""
+
+#: src/paths/instance/update/index.tsx:112
+#, c-format
+msgid "Failed to create instance"
+msgstr ""
+
+#: src/components/exception/login.tsx:74
+#, c-format
+msgid "Login required"
+msgstr ""
+
+#: src/components/exception/login.tsx:80
+#, c-format
+msgid "Please enter your access token."
+msgstr ""
+
+#: src/components/exception/login.tsx:108
+#, c-format
+msgid "Access Token"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:171
+#, c-format
+msgid "The request to the backend take too long and was cancelled"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:172
+#, c-format
+msgid "Diagnostic from %1$s is \"%2$s\""
+msgstr ""
+
+#: src/InstanceRoutes.tsx:178
+#, c-format
+msgid "The backend reported a problem: HTTP status #%1$s"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:179
+#, c-format
+msgid "Diagnostic from %1$s is '%2$s'"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:196
+#, c-format
+msgid "Access denied"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:197
+#, c-format
+msgid "The access token provided is invalid."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:212
+#, c-format
+msgid "No 'default' instance configured yet."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:213
+#, c-format
+msgid "Create a 'default' instance to begin using the merchant backoffice."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:630
+#, c-format
+msgid "The access token provided is invalid"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:664
+#, c-format
+msgid "Hide for today"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:82
+#, c-format
+msgid "Instance"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:91
+#, c-format
+msgid "Settings"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:167
+#, c-format
+msgid "Connection"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:209
+#, c-format
+msgid "New"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:219
+#, c-format
+msgid "List"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:234
+#, c-format
+msgid "Log out"
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:71
+#, c-format
+msgid "Check your token is valid"
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:90
+#, c-format
+msgid "Couldn't access the server."
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:91
+#, c-format
+msgid "Could not infer instance id from url %1$s"
+msgstr ""
+
+#: src/Application.tsx:104
+#, c-format
+msgid "Server not found"
+msgstr ""
+
+#: src/Application.tsx:118
+#, c-format
+msgid "Server response with an error code"
+msgstr ""
+
+#: src/Application.tsx:120
+#, c-format
+msgid "Got message %1$s from %2$s"
+msgstr ""
+
+#: src/Application.tsx:131
+#, c-format
+msgid "Response from server is unreadable, http status: %1$s"
+msgstr ""
+
+#: src/Application.tsx:144
+#, c-format
+msgid "Unexpected Error"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:101
+#, c-format
+msgid "The value %1$s is invalid for a payment url"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:110
+#, c-format
+msgid "add element to the list"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:112
+#, c-format
+msgid "add"
+msgstr ""
+
+#: src/components/form/InputSecured.tsx:37
+#, c-format
+msgid "Deleting"
+msgstr ""
+
+#: src/components/form/InputSecured.tsx:41
+#, c-format
+msgid "Changing"
+msgstr ""
+
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:87
+#, c-format
+msgid "Order ID"
+msgstr ""
+
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:101
+#, c-format
+msgid "Payment URL"
+msgstr ""
diff --git a/packages/auditor-backoffice-ui/src/i18n/es.po b/packages/auditor-backoffice-ui/src/i18n/es.po
new file mode 100644
index 000000000..10ec0cf3b
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/i18n/es.po
@@ -0,0 +1,2854 @@
+# This file is part of TALER
+# (C) 2016 GNUnet e.V.
+#
+# TALER is free software; you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 3, or (at your option) any later version.
+#
+# TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Wallet\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"POT-Creation-Date: 2016-11-23 00:00+0100\n"
+"PO-Revision-Date: 2023-08-13 10:14+0000\n"
+"Last-Translator: Javier Sepulveda <javier.sepulveda@uv.es>\n"
+"Language-Team: Spanish <https://weblate.taler.net/projects/gnu-taler/"
+"merchant-backoffice/es/>\n"
+"Language: es\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 4.13.1\n"
+
+#: src/components/modal/index.tsx:71
+#, c-format
+msgid "Cancel"
+msgstr "Cancelar"
+
+#: src/components/modal/index.tsx:79
+#, c-format
+msgid "%1$s"
+msgstr "%1$s"
+
+#: src/components/modal/index.tsx:84
+#, c-format
+msgid "Close"
+msgstr ""
+
+#: src/components/modal/index.tsx:124
+#, c-format
+msgid "Continue"
+msgstr "Continuar"
+
+#: src/components/modal/index.tsx:178
+#, c-format
+msgid "Clear"
+msgstr "Limpiar"
+
+#: src/components/modal/index.tsx:190
+#, c-format
+msgid "Confirm"
+msgstr "Confirmar"
+
+#: src/components/modal/index.tsx:296
+#, c-format
+msgid "is not the same as the current access token"
+msgstr "no es el mismo que el token de acceso actual"
+
+#: src/components/modal/index.tsx:299
+#, c-format
+msgid "cannot be empty"
+msgstr "no puede ser vacío"
+
+#: src/components/modal/index.tsx:301
+#, c-format
+msgid "cannot be the same as the old token"
+msgstr "no puede ser igual al viejo token"
+
+#: src/components/modal/index.tsx:305
+#, c-format
+msgid "is not the same"
+msgstr "no son iguales"
+
+#: src/components/modal/index.tsx:315
+#, c-format
+msgid "You are updating the access token from instance with id %1$s"
+msgstr "Está actualizando el token de acceso para la instancia con id %1$s"
+
+#: src/components/modal/index.tsx:331
+#, c-format
+msgid "Old access token"
+msgstr "Viejo token de acceso"
+
+#: src/components/modal/index.tsx:332
+#, c-format
+msgid "access token currently in use"
+msgstr "acceder al token en uso actualmente"
+
+#: src/components/modal/index.tsx:338
+#, c-format
+msgid "New access token"
+msgstr "Nuevo token de acceso"
+
+#: src/components/modal/index.tsx:339
+#, c-format
+msgid "next access token to be used"
+msgstr "siguiente token de acceso a usar"
+
+#: src/components/modal/index.tsx:344
+#, c-format
+msgid "Repeat access token"
+msgstr "Repetir token de acceso"
+
+#: src/components/modal/index.tsx:345
+#, c-format
+msgid "confirm the same access token"
+msgstr "confirmar el mismo token de acceso"
+
+#: src/components/modal/index.tsx:350
+#, c-format
+msgid "Clearing the access token will mean public access to the instance"
+msgstr "Limpiar el token de acceso significa acceso público a la instancia"
+
+#: src/components/modal/index.tsx:377
+#, c-format
+msgid "cannot be the same as the old access token"
+msgstr "no puede ser igual al anterior token de acceso"
+
+#: src/components/modal/index.tsx:394
+#, c-format
+msgid "You are setting the access token for the new instance"
+msgstr "Está estableciendo el token de acceso para la nueva instancia"
+
+#: src/components/modal/index.tsx:420
+#, c-format
+msgid ""
+"With external authorization method no check will be done by the merchant "
+"backend"
+msgstr ""
+"Con el método de autorización externa no se hará ninguna revisión por el "
+"backend del comerciante"
+
+#: src/components/modal/index.tsx:436
+#, c-format
+msgid "Set external authorization"
+msgstr "Establecer autorización externa"
+
+#: src/components/modal/index.tsx:448
+#, c-format
+msgid "Set access token"
+msgstr "Establecer token de acceso"
+
+#: src/components/modal/index.tsx:470
+#, c-format
+msgid "Operation in progress..."
+msgstr "Operación en progreso..."
+
+#: src/components/modal/index.tsx:479
+#, c-format
+msgid "The operation will be automatically canceled after %1$s seconds"
+msgstr "La operación será automáticamente cancelada luego de %1$s segundos"
+
+#: src/paths/admin/list/TableActive.tsx:80
+#, c-format
+msgid "Instances"
+msgstr "Instancias"
+
+#: src/paths/admin/list/TableActive.tsx:93
+#, c-format
+msgid "Delete"
+msgstr "Eliminar"
+
+#: src/paths/admin/list/TableActive.tsx:99
+#, c-format
+msgid "add new instance"
+msgstr "agregar nueva instancia"
+
+#: src/paths/admin/list/TableActive.tsx:178
+#, c-format
+msgid "ID"
+msgstr "ID"
+
+#: src/paths/admin/list/TableActive.tsx:181
+#, c-format
+msgid "Name"
+msgstr "Nombre"
+
+#: src/paths/admin/list/TableActive.tsx:220
+#, c-format
+msgid "Edit"
+msgstr "Editar"
+
+#: src/paths/admin/list/TableActive.tsx:237
+#, c-format
+msgid "Purge"
+msgstr "Purgar"
+
+#: src/paths/admin/list/TableActive.tsx:261
+#, c-format
+msgid "There is no instances yet, add more pressing the + sign"
+msgstr "Todavía no hay instancias, agregue más presionando el signo +"
+
+#: src/paths/admin/list/View.tsx:68
+#, c-format
+msgid "Only show active instances"
+msgstr "Solo mostrar instancias activas"
+
+#: src/paths/admin/list/View.tsx:71
+#, c-format
+msgid "Active"
+msgstr "Activo"
+
+#: src/paths/admin/list/View.tsx:78
+#, c-format
+msgid "Only show deleted instances"
+msgstr "Mostrar solo instancias eliminadas"
+
+#: src/paths/admin/list/View.tsx:81
+#, c-format
+msgid "Deleted"
+msgstr "Eliminado"
+
+#: src/paths/admin/list/View.tsx:88
+#, c-format
+msgid "Show all instances"
+msgstr "Mostrar todas las instancias"
+
+#: src/paths/admin/list/View.tsx:91
+#, c-format
+msgid "All"
+msgstr "Todo"
+
+#: src/paths/admin/list/index.tsx:101
+#, fuzzy, c-format
+msgid "Instance \"%1$s\" (ID: %2$s) has been deleted"
+msgstr "La instancia '%1$s' (ID: %2$s) fue eliminada"
+
+#: src/paths/admin/list/index.tsx:106
+#, c-format
+msgid "Failed to delete instance"
+msgstr "Fallo al eliminar instancia"
+
+#: src/paths/admin/list/index.tsx:124
+#, c-format
+msgid "Instance '%1$s' (ID: %2$s) has been disabled"
+msgstr "Instance '%1$s' (ID: %2$s) ha sido deshabilitada"
+
+#: src/paths/admin/list/index.tsx:129
+#, c-format
+msgid "Failed to purge instance"
+msgstr "Fallo al purgar la instancia"
+
+#: src/paths/instance/kyc/list/ListPage.tsx:41
+#, c-format
+msgid "Pending KYC verification"
+msgstr "Verificación KYC pendiente"
+
+#: src/paths/instance/kyc/list/ListPage.tsx:66
+#, c-format
+msgid "Timed out"
+msgstr "Expirado"
+
+#: src/paths/instance/kyc/list/ListPage.tsx:103
+#, c-format
+msgid "Exchange"
+msgstr "Exchange"
+
+#: src/paths/instance/kyc/list/ListPage.tsx:106
+#, c-format
+msgid "Target account"
+msgstr "Cuenta objetivo"
+
+#: src/paths/instance/kyc/list/ListPage.tsx:109
+#, c-format
+msgid "KYC URL"
+msgstr "URL de KYC"
+
+#: src/paths/instance/kyc/list/ListPage.tsx:144
+#, c-format
+msgid "Code"
+msgstr "Código"
+
+#: src/paths/instance/kyc/list/ListPage.tsx:147
+#, c-format
+msgid "Http Status"
+msgstr "Estado http"
+
+#: src/paths/instance/kyc/list/ListPage.tsx:177
+#, c-format
+msgid "No pending kyc verification!"
+msgstr "¡No hay verificación kyc pendiente!"
+
+#: src/components/form/InputDate.tsx:123
+#, c-format
+msgid "change value to unknown date"
+msgstr "cambiar valor a fecha desconocida"
+
+#: src/components/form/InputDate.tsx:124
+#, c-format
+msgid "change value to empty"
+msgstr "cambiar valor a vacío"
+
+#: src/components/form/InputDate.tsx:131
+#, c-format
+msgid "clear"
+msgstr "limpiar"
+
+#: src/components/form/InputDate.tsx:136
+#, c-format
+msgid "change value to never"
+msgstr "cambiar valor a nunca"
+
+#: src/components/form/InputDate.tsx:141
+#, c-format
+msgid "never"
+msgstr "nunca"
+
+#: src/components/form/InputLocation.tsx:29
+#, c-format
+msgid "Country"
+msgstr "País"
+
+#: src/components/form/InputLocation.tsx:33
+#, c-format
+msgid "Address"
+msgstr "Dirección"
+
+#: src/components/form/InputLocation.tsx:39
+#, c-format
+msgid "Building number"
+msgstr "Número de edificio"
+
+#: src/components/form/InputLocation.tsx:41
+#, c-format
+msgid "Building name"
+msgstr "Nombre de edificio"
+
+#: src/components/form/InputLocation.tsx:42
+#, c-format
+msgid "Street"
+msgstr "Calle"
+
+#: src/components/form/InputLocation.tsx:43
+#, c-format
+msgid "Post code"
+msgstr "Código postal"
+
+#: src/components/form/InputLocation.tsx:44
+#, c-format
+msgid "Town location"
+msgstr "Ubicación de ciudad"
+
+#: src/components/form/InputLocation.tsx:45
+#, c-format
+msgid "Town"
+msgstr "Ciudad"
+
+#: src/components/form/InputLocation.tsx:46
+#, c-format
+msgid "District"
+msgstr "Distrito"
+
+#: src/components/form/InputLocation.tsx:49
+#, c-format
+msgid "Country subdivision"
+msgstr "Subdivisión de país"
+
+#: src/components/form/InputSearchProduct.tsx:66
+#, c-format
+msgid "Product id"
+msgstr "Id de producto"
+
+#: src/components/form/InputSearchProduct.tsx:69
+#, c-format
+msgid "Description"
+msgstr "Descripcion"
+
+#: src/components/form/InputSearchProduct.tsx:94
+#, fuzzy, c-format
+msgid "Product"
+msgstr "Productos"
+
+#: src/components/form/InputSearchProduct.tsx:95
+#, c-format
+msgid "search products by it's description or id"
+msgstr "buscar productos por su descripción o ID"
+
+#: src/components/form/InputSearchProduct.tsx:151
+#, c-format
+msgid "no products found with that description"
+msgstr "no se encontraron productos con esa descripción"
+
+#: src/components/product/InventoryProductForm.tsx:56
+#, c-format
+msgid "You must enter a valid product identifier."
+msgstr "Debe ingresar un identificador de producto válido."
+
+#: src/components/product/InventoryProductForm.tsx:64
+#, c-format
+msgid "Quantity must be greater than 0!"
+msgstr "¡Cantidad debe ser mayor que 0!"
+
+#: src/components/product/InventoryProductForm.tsx:76
+#, fuzzy, c-format
+msgid ""
+"This quantity exceeds remaining stock. Currently, only %1$s units remain "
+"unreserved in stock."
+msgstr ""
+"Esta cantidad excede las existencias restantes. Actualmente, solo quedan "
+"%1$s unidades sin reservar en las existencias."
+
+#: src/components/product/InventoryProductForm.tsx:109
+#, c-format
+msgid "Quantity"
+msgstr "Cantidad"
+
+#: src/components/product/InventoryProductForm.tsx:110
+#, c-format
+msgid "how many products will be added"
+msgstr "cuántos productos serán agregados"
+
+#: src/components/product/InventoryProductForm.tsx:117
+#, c-format
+msgid "Add from inventory"
+msgstr "Agregar del inventario"
+
+#: src/components/form/InputImage.tsx:105
+#, c-format
+msgid "Image should be smaller than 1 MB"
+msgstr "La imagen debe ser mas chica que 1 MB"
+
+#: src/components/form/InputImage.tsx:110
+#, c-format
+msgid "Add"
+msgstr "Agregar"
+
+#: src/components/form/InputImage.tsx:115
+#, c-format
+msgid "Remove"
+msgstr "Eliminar"
+
+#: src/components/form/InputTaxes.tsx:113
+#, c-format
+msgid "No taxes configured for this product."
+msgstr "Ningun impuesto configurado para este producto."
+
+#: src/components/form/InputTaxes.tsx:119
+#, c-format
+msgid "Amount"
+msgstr "Monto"
+
+#: src/components/form/InputTaxes.tsx:120
+#, c-format
+msgid ""
+"Taxes can be in currencies that differ from the main currency used by the "
+"merchant."
+msgstr ""
+"Impuestos pueden estar en divisas que difieren de la principal divisa usada "
+"por el comerciante."
+
+#: src/components/form/InputTaxes.tsx:122
+#, c-format
+msgid ""
+"Enter currency and value separated with a colon, e.g. &quot;USD:2.3&quot;."
+msgstr ""
+"Ingrese divisa y valor separado por dos puntos, e.g. &quot;USD:2.3&quot;."
+
+#: src/components/form/InputTaxes.tsx:131
+#, c-format
+msgid "Legal name of the tax, e.g. VAT or import duties."
+msgstr "Nombre legal del impuesto, e.g. IVA o arancel."
+
+#: src/components/form/InputTaxes.tsx:137
+#, c-format
+msgid "add tax to the tax list"
+msgstr "agregar impuesto a la lista de impuestos"
+
+#: src/components/product/NonInventoryProductForm.tsx:72
+#, c-format
+msgid "describe and add a product that is not in the inventory list"
+msgstr "describa y agregue un producto que no está en la lista de inventarios"
+
+#: src/components/product/NonInventoryProductForm.tsx:75
+#, c-format
+msgid "Add custom product"
+msgstr "Agregue un producto personalizado"
+
+#: src/components/product/NonInventoryProductForm.tsx:86
+#, c-format
+msgid "Complete information of the product"
+msgstr "Complete información del producto"
+
+#: src/components/product/NonInventoryProductForm.tsx:185
+#, c-format
+msgid "Image"
+msgstr "Imagen"
+
+#: src/components/product/NonInventoryProductForm.tsx:186
+#, c-format
+msgid "photo of the product"
+msgstr "foto del producto"
+
+#: src/components/product/NonInventoryProductForm.tsx:192
+#, c-format
+msgid "full product description"
+msgstr "descripción completa del producto"
+
+#: src/components/product/NonInventoryProductForm.tsx:196
+#, c-format
+msgid "Unit"
+msgstr "Unidad"
+
+#: src/components/product/NonInventoryProductForm.tsx:197
+#, c-format
+msgid "name of the product unit"
+msgstr "nombre de la unidad del producto"
+
+#: src/components/product/NonInventoryProductForm.tsx:201
+#, c-format
+msgid "Price"
+msgstr "Precio"
+
+#: src/components/product/NonInventoryProductForm.tsx:202
+#, c-format
+msgid "amount in the current currency"
+msgstr "monto de la divisa actual"
+
+#: src/components/product/NonInventoryProductForm.tsx:211
+#, c-format
+msgid "Taxes"
+msgstr "Impuestos"
+
+#: src/components/product/ProductList.tsx:38
+#, c-format
+msgid "image"
+msgstr "imagen"
+
+#: src/components/product/ProductList.tsx:41
+#, c-format
+msgid "description"
+msgstr "descripción"
+
+#: src/components/product/ProductList.tsx:44
+#, c-format
+msgid "quantity"
+msgstr "cantidad"
+
+#: src/components/product/ProductList.tsx:47
+#, c-format
+msgid "unit price"
+msgstr "precio unitario"
+
+#: src/components/product/ProductList.tsx:50
+#, c-format
+msgid "total price"
+msgstr "precio total"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:153
+#, c-format
+msgid "required"
+msgstr "requerido"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:157
+#, fuzzy, c-format
+msgid "not valid"
+msgstr "no es un json válido"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:159
+#, c-format
+msgid "must be greater than 0"
+msgstr "debe ser mayor que 0"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:164
+#, c-format
+msgid "not a valid json"
+msgstr "no es un json válido"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:170
+#, c-format
+msgid "should be in the future"
+msgstr "deberían ser en el futuro"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:173
+#, c-format
+msgid "refund deadline cannot be before pay deadline"
+msgstr "plazo de reembolso no puede ser antes que el plazo de pago"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:179
+#, c-format
+msgid "wire transfer deadline cannot be before refund deadline"
+msgstr ""
+"el plazo de la transferencia bancaria no puede ser antes que el plazo de "
+"reembolso"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:190
+#, c-format
+msgid "wire transfer deadline cannot be before pay deadline"
+msgstr ""
+"el plazo de la transferencia bancaria no puede ser antes que el plazo de pago"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:197
+#, c-format
+msgid "should have a refund deadline"
+msgstr "debería tener un plazo de reembolso"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:202
+#, c-format
+msgid "auto refund cannot be after refund deadline"
+msgstr "reembolso automático no puede ser después qu el plazo de reembolso"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:360
+#, c-format
+msgid "Manage products in order"
+msgstr "Manejar productos en orden"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:369
+#, c-format
+msgid "Manage list of products in the order."
+msgstr "Manejar lista de productos en la orden."
+
+#: src/paths/instance/orders/create/CreatePage.tsx:391
+#, c-format
+msgid "Remove this product from the order."
+msgstr "Remover este producto de la orden."
+
+#: src/paths/instance/orders/create/CreatePage.tsx:415
+#, c-format
+msgid "Total price"
+msgstr "Precio total"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:417
+#, c-format
+msgid "total product price added up"
+msgstr "precio total de producto agregado"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:430
+#, c-format
+msgid "Amount to be paid by the customer"
+msgstr "Monto a ser pagado por el cliente"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:436
+#, c-format
+msgid "Order price"
+msgstr "Precio de la orden"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:437
+#, c-format
+msgid "final order price"
+msgstr "Precio final de la orden"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:444
+#, c-format
+msgid "Summary"
+msgstr "Resumen"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:445
+#, c-format
+msgid "Title of the order to be shown to the customer"
+msgstr "Título de la orden a ser mostrado al cliente"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:450
+#, c-format
+msgid "Shipping and Fulfillment"
+msgstr "Envío y cumplimiento"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:455
+#, c-format
+msgid "Delivery date"
+msgstr "Fecha de entrega"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:456
+#, c-format
+msgid "Deadline for physical delivery assured by the merchant."
+msgstr "Plazo para la entrega física asegurado por el comerciante."
+
+#: src/paths/instance/orders/create/CreatePage.tsx:461
+#, c-format
+msgid "Location"
+msgstr "Ubicación"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:462
+#, c-format
+msgid "address where the products will be delivered"
+msgstr "dirección a donde los productos serán entregados"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:469
+#, c-format
+msgid "Fulfillment URL"
+msgstr "URL de cumplimiento"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:470
+#, c-format
+msgid "URL to which the user will be redirected after successful payment."
+msgstr "URL al cual el usuario será redirigido luego de pago exitoso."
+
+#: src/paths/instance/orders/create/CreatePage.tsx:476
+#, c-format
+msgid "Taler payment options"
+msgstr "Opciones de pago de Taler"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:477
+#, c-format
+msgid "Override default Taler payment settings for this order"
+msgstr "Sobreescribir pagos por omisión de Taler para esta orden"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:481
+#, fuzzy, c-format
+msgid "Payment deadline"
+msgstr "Plazo de pago"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:482
+#, c-format
+msgid ""
+"Deadline for the customer to pay for the offer before it expires. Inventory "
+"products will be reserved until this deadline."
+msgstr ""
+"Plazo límite para que el cliente pague por la oferta antes de que expire. "
+"Productos del inventario serán reservados hasta este plazo límite."
+
+#: src/paths/instance/orders/create/CreatePage.tsx:486
+#, c-format
+msgid "Refund deadline"
+msgstr "Plazo de reembolso"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:487
+#, c-format
+msgid "Time until which the order can be refunded by the merchant."
+msgstr ""
+"Tiempo hasta el cual la orden puede ser reembolsada por el comerciante."
+
+#: src/paths/instance/orders/create/CreatePage.tsx:491
+#, c-format
+msgid "Wire transfer deadline"
+msgstr "Plazo de la transferencia"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:492
+#, c-format
+msgid "Deadline for the exchange to make the wire transfer."
+msgstr "Plazo para que el exchange haga la transferencia."
+
+#: src/paths/instance/orders/create/CreatePage.tsx:496
+#, fuzzy, c-format
+msgid "Auto-refund deadline"
+msgstr "Plazo de reembolso automático"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:497
+#, c-format
+msgid ""
+"Time until which the wallet will automatically check for refunds without "
+"user interaction."
+msgstr ""
+"Tiempo hasta el cual la billetera será automáticamente revisada por "
+"reembolsos win interación por parte del usuario."
+
+#: src/paths/instance/orders/create/CreatePage.tsx:502
+#, c-format
+msgid "Maximum deposit fee"
+msgstr "Máxima tarifa de depósito"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:503
+#, c-format
+msgid ""
+"Maximum deposit fees the merchant is willing to cover for this order. Higher "
+"deposit fees must be covered in full by the consumer."
+msgstr ""
+"Máxima tarifa de depósito que el comerciante esta dispuesto a cubir para "
+"esta orden. Mayores tarifas de depósito deben ser cubiertas completamente "
+"por el consumidor."
+
+#: src/paths/instance/orders/create/CreatePage.tsx:507
+#, c-format
+msgid "Maximum wire fee"
+msgstr "Máxima tarifa de transferencia"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:508
+#, c-format
+msgid ""
+"Maximum aggregate wire fees the merchant is willing to cover for this order. "
+"Wire fees exceeding this amount are to be covered by the customers."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:512
+#, c-format
+msgid "Wire fee amortization"
+msgstr "Amortización de comisión de transferencia"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:513
+#, c-format
+msgid ""
+"Factor by which wire fees exceeding the above threshold are divided to "
+"determine the share of excess wire fees to be paid explicitly by the "
+"consumer."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:517
+#, fuzzy, c-format
+msgid "Create token"
+msgstr "Administrar token"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:518
+#, c-format
+msgid ""
+"Uncheck this option if the merchant backend generated an order ID with "
+"enough entropy to prevent adversarial claims."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:522
+#, fuzzy, c-format
+msgid "Minimum age required"
+msgstr "Login necesario"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:523
+#, c-format
+msgid ""
+"Any value greater than 0 will limit the coins able be used to pay this "
+"contract. If empty the age restriction will be defined by the products"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:526
+#, c-format
+msgid "Min age defined by the producs is %1$s"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:534
+#, fuzzy, c-format
+msgid "Additional information"
+msgstr "Información extra"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:535
+#, c-format
+msgid "Custom information to be included in the contract for this order."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:541
+#, c-format
+msgid "You must enter a value in JavaScript Object Notation (JSON)."
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:55
+#, c-format
+msgid "days"
+msgstr "días"
+
+#: src/components/picker/DurationPicker.tsx:65
+#, c-format
+msgid "hours"
+msgstr "horas"
+
+#: src/components/picker/DurationPicker.tsx:76
+#, c-format
+msgid "minutes"
+msgstr "minutos"
+
+#: src/components/picker/DurationPicker.tsx:87
+#, c-format
+msgid "seconds"
+msgstr "segundos"
+
+#: src/components/form/InputDuration.tsx:53
+#, fuzzy, c-format
+msgid "forever"
+msgstr "nunca"
+
+#: src/components/form/InputDuration.tsx:62
+#, c-format
+msgid "%1$sM"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:64
+#, c-format
+msgid "%1$sY"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:66
+#, c-format
+msgid "%1$sd"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:68
+#, c-format
+msgid "%1$sh"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:70
+#, c-format
+msgid "%1$smin"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:72
+#, c-format
+msgid "%1$ssec"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:75
+#, c-format
+msgid "Orders"
+msgstr "Órdenes"
+
+#: src/paths/instance/orders/list/Table.tsx:81
+#, fuzzy, c-format
+msgid "create order"
+msgstr "creado"
+
+#: src/paths/instance/orders/list/Table.tsx:147
+#, c-format
+msgid "load newer orders"
+msgstr "cargar nuevas ordenes"
+
+#: src/paths/instance/orders/list/Table.tsx:154
+#, c-format
+msgid "Date"
+msgstr "Fecha"
+
+#: src/paths/instance/orders/list/Table.tsx:200
+#, c-format
+msgid "Refund"
+msgstr "Devolución"
+
+#: src/paths/instance/orders/list/Table.tsx:209
+#, c-format
+msgid "copy url"
+msgstr "copiar url"
+
+#: src/paths/instance/orders/list/Table.tsx:225
+#, c-format
+msgid "load older orders"
+msgstr "cargar viejas ordenes"
+
+#: src/paths/instance/orders/list/Table.tsx:242
+#, c-format
+msgid "No orders have been found matching your query!"
+msgstr "¡No se encontraron órdenes que emparejen su búsqueda!"
+
+#: src/paths/instance/orders/list/Table.tsx:288
+#, c-format
+msgid "duplicated"
+msgstr "duplicado"
+
+#: src/paths/instance/orders/list/Table.tsx:299
+#, c-format
+msgid "invalid format"
+msgstr "formato inválido"
+
+#: src/paths/instance/orders/list/Table.tsx:301
+#, c-format
+msgid "this value exceed the refundable amount"
+msgstr "este monto excede el monto reembolsable"
+
+#: src/paths/instance/orders/list/Table.tsx:346
+#, c-format
+msgid "date"
+msgstr "fecha"
+
+#: src/paths/instance/orders/list/Table.tsx:349
+#, c-format
+msgid "amount"
+msgstr "monto"
+
+#: src/paths/instance/orders/list/Table.tsx:352
+#, c-format
+msgid "reason"
+msgstr "razón"
+
+#: src/paths/instance/orders/list/Table.tsx:389
+#, c-format
+msgid "amount to be refunded"
+msgstr "monto a ser reembolsado"
+
+#: src/paths/instance/orders/list/Table.tsx:391
+#, c-format
+msgid "Max refundable:"
+msgstr "Máximo reembolzable:"
+
+#: src/paths/instance/orders/list/Table.tsx:396
+#, c-format
+msgid "Reason"
+msgstr "Razón"
+
+#: src/paths/instance/orders/list/Table.tsx:397
+#, c-format
+msgid "Choose one..."
+msgstr "Elija uno..."
+
+#: src/paths/instance/orders/list/Table.tsx:399
+#, c-format
+msgid "requested by the customer"
+msgstr "pedido por el consumidor"
+
+#: src/paths/instance/orders/list/Table.tsx:400
+#, c-format
+msgid "other"
+msgstr "otro"
+
+#: src/paths/instance/orders/list/Table.tsx:403
+#, c-format
+msgid "why this order is being refunded"
+msgstr "por qué esta orden está siendo reembolsada"
+
+#: src/paths/instance/orders/list/Table.tsx:409
+#, c-format
+msgid "more information to give context"
+msgstr "más información para dar contexto"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:62
+#, c-format
+msgid "Contract Terms"
+msgstr "Términos de contrato"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:68
+#, c-format
+msgid "human-readable description of the whole purchase"
+msgstr "descripción legible de toda la compra"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:74
+#, c-format
+msgid "total price for the transaction"
+msgstr "precio total de la transacción"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:81
+#, c-format
+msgid "URL for this purchase"
+msgstr "URL para esta compra"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:87
+#, c-format
+msgid "Max fee"
+msgstr "Máxima comisión"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:88
+#, c-format
+msgid "maximum total deposit fee accepted by the merchant for this contract"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:93
+#, c-format
+msgid "Max wire fee"
+msgstr "Impuesto de transferencia máximo"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:94
+#, c-format
+msgid "maximum wire fee accepted by the merchant"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:100
+#, c-format
+msgid ""
+"over how many customer transactions does the merchant expect to amortize "
+"wire fees on average"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:105
+#, c-format
+msgid "Created at"
+msgstr "Creado en"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:106
+#, c-format
+msgid "time when this contract was generated"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:112
+#, c-format
+msgid "after this deadline has passed no refunds will be accepted"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:118
+#, c-format
+msgid ""
+"after this deadline, the merchant won't accept payments for the contract"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:124
+#, c-format
+msgid "transfer deadline for the exchange"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:130
+#, c-format
+msgid "time indicating when the order should be delivered"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:136
+#, c-format
+msgid "where the order will be delivered"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:144
+#, fuzzy, c-format
+msgid "Auto-refund delay"
+msgstr "Plazo de reembolso automático"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:145
+#, c-format
+msgid ""
+"how long the wallet should try to get an automatic refund for the purchase"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:150
+#, fuzzy, c-format
+msgid "Extra info"
+msgstr "Información extra"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:151
+#, c-format
+msgid "extra data that is only interpreted by the merchant frontend"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:219
+#, c-format
+msgid "Order"
+msgstr "Orden"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:221
+#, c-format
+msgid "claimed"
+msgstr "reclamado"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:247
+#, fuzzy, c-format
+msgid "claimed at"
+msgstr "reclamado"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:265
+#, c-format
+msgid "Timeline"
+msgstr "Cronología"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:271
+#, c-format
+msgid "Payment details"
+msgstr "Detalles de pago"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:291
+#, c-format
+msgid "Order status"
+msgstr "Estado de orden"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:301
+#, c-format
+msgid "Product list"
+msgstr "Lista de producto"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:451
+#, c-format
+msgid "paid"
+msgstr "pagados"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:455
+#, c-format
+msgid "wired"
+msgstr "transferido"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:460
+#, c-format
+msgid "refunded"
+msgstr "reembolzado"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:480
+#, fuzzy, c-format
+msgid "refund order"
+msgstr "reembolzado"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:481
+#, fuzzy, c-format
+msgid "not refundable"
+msgstr "Máximo reembolzable:"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:489
+#, c-format
+msgid "refund"
+msgstr "reembolzar"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:553
+#, c-format
+msgid "Refunded amount"
+msgstr "Monto reembolzado"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:560
+#, fuzzy, c-format
+msgid "Refund taken"
+msgstr "Reembolzado"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:570
+#, fuzzy, c-format
+msgid "Status URL"
+msgstr "URL de estado de orden"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:583
+#, fuzzy, c-format
+msgid "Refund URI"
+msgstr "Devolución"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:636
+#, c-format
+msgid "unpaid"
+msgstr "impago"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:654
+#, c-format
+msgid "pay at"
+msgstr "pagar en"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:666
+#, c-format
+msgid "created at"
+msgstr "creado"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:707
+#, c-format
+msgid "Order status URL"
+msgstr "URL de estado de orden"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:711
+#, fuzzy, c-format
+msgid "Payment URI"
+msgstr "URI de pago"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:740
+#, c-format
+msgid ""
+"Unknown order status. This is an error, please contact the administrator."
+msgstr ""
+"Estado de orden desconocido. Esto es un error, por favor contacte a su "
+"administrador."
+
+#: src/paths/instance/orders/details/DetailPage.tsx:767
+#, c-format
+msgid "Back"
+msgstr ""
+
+#: src/paths/instance/orders/details/index.tsx:79
+#, c-format
+msgid "refund created successfully"
+msgstr "reembolzo creado satisfactoriamente"
+
+#: src/paths/instance/orders/details/index.tsx:85
+#, c-format
+msgid "could not create the refund"
+msgstr "No se pudo create el reembolso"
+
+#: src/paths/instance/orders/list/ListPage.tsx:78
+#, c-format
+msgid "select date to show nearby orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:94
+#, fuzzy, c-format
+msgid "order id"
+msgstr "ir a id de orden"
+
+#: src/paths/instance/orders/list/ListPage.tsx:100
+#, c-format
+msgid "jump to order with the given order ID"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:122
+#, c-format
+msgid "remove all filters"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:132
+#, c-format
+msgid "only show paid orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:135
+#, c-format
+msgid "Paid"
+msgstr "Pagado"
+
+#: src/paths/instance/orders/list/ListPage.tsx:142
+#, fuzzy, c-format
+msgid "only show orders with refunds"
+msgstr "No se pudo create el reembolso"
+
+#: src/paths/instance/orders/list/ListPage.tsx:145
+#, c-format
+msgid "Refunded"
+msgstr "Reembolsado"
+
+#: src/paths/instance/orders/list/ListPage.tsx:152
+#, c-format
+msgid ""
+"only show orders where customers paid, but wire payments from payment "
+"provider are still pending"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:155
+#, c-format
+msgid "Not wired"
+msgstr "No transferido"
+
+#: src/paths/instance/orders/list/ListPage.tsx:170
+#, c-format
+msgid "clear date filter"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:184
+#, c-format
+msgid "date (YYYY/MM/DD)"
+msgstr ""
+
+#: src/paths/instance/orders/list/index.tsx:103
+#, fuzzy, c-format
+msgid "Enter an order id"
+msgstr "ir a id de orden"
+
+#: src/paths/instance/orders/list/index.tsx:111
+#, fuzzy, c-format
+msgid "order not found"
+msgstr "Servidor no encontrado"
+
+#: src/paths/instance/orders/list/index.tsx:178
+#, fuzzy, c-format
+msgid "could not get the order to refund"
+msgstr "No se pudo create el reembolso"
+
+#: src/components/exception/AsyncButton.tsx:43
+#, fuzzy, c-format
+msgid "Loading..."
+msgstr "Cargando..."
+
+#: src/components/form/InputStock.tsx:99
+#, c-format
+msgid ""
+"click here to configure the stock of the product, leave it as is and the "
+"backend will not control stock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:109
+#, c-format
+msgid "Manage stock"
+msgstr "Administrar stock"
+
+#: src/components/form/InputStock.tsx:115
+#, c-format
+msgid "this product has been configured without stock control"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:119
+#, c-format
+msgid "Infinite"
+msgstr "Inifinito"
+
+#: src/components/form/InputStock.tsx:136
+#, fuzzy, c-format
+msgid "lost cannot be greater than current and incoming (max %1$s)"
+msgstr "la pérdida no puede ser mayor al stock actual + entrante (max %1$s )"
+
+#: src/components/form/InputStock.tsx:176
+#, c-format
+msgid "Incoming"
+msgstr "Ingresando"
+
+#: src/components/form/InputStock.tsx:177
+#, c-format
+msgid "Lost"
+msgstr "Perdido"
+
+#: src/components/form/InputStock.tsx:192
+#, c-format
+msgid "Current"
+msgstr "Actual"
+
+#: src/components/form/InputStock.tsx:196
+#, c-format
+msgid "remove stock control for this product"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:202
+#, c-format
+msgid "without stock"
+msgstr "sin stock"
+
+#: src/components/form/InputStock.tsx:211
+#, c-format
+msgid "Next restock"
+msgstr "Próximo reabastecimiento"
+
+#: src/components/form/InputStock.tsx:217
+#, c-format
+msgid "Delivery address"
+msgstr "Dirección de entrega"
+
+#: src/components/product/ProductForm.tsx:133
+#, c-format
+msgid "product identification to use in URLs (for internal use only)"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:139
+#, c-format
+msgid "illustration of the product for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:145
+#, c-format
+msgid "product description for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:149
+#, c-format
+msgid "Age restricted"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:150
+#, c-format
+msgid "is this product restricted for customer below certain age?"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:155
+#, c-format
+msgid ""
+"unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 "
+"items, 5 meters) for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:160
+#, c-format
+msgid ""
+"sale price for customers, including taxes, for above units of the product"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:164
+#, c-format
+msgid "Stock"
+msgstr "Existencias"
+
+#: src/components/product/ProductForm.tsx:166
+#, c-format
+msgid ""
+"product inventory for products with finite supply (for internal use only)"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:171
+#, c-format
+msgid "taxes included in the product price, exposed to customers"
+msgstr ""
+
+#: src/paths/instance/products/create/CreatePage.tsx:66
+#, c-format
+msgid "Need to complete marked fields"
+msgstr ""
+
+#: src/paths/instance/products/create/index.tsx:51
+#, c-format
+msgid "could not create product"
+msgstr "no se pudo crear el producto"
+
+#: src/paths/instance/products/list/Table.tsx:68
+#, c-format
+msgid "Products"
+msgstr "Productos"
+
+#: src/paths/instance/products/list/Table.tsx:73
+#, c-format
+msgid "add product to inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:137
+#, c-format
+msgid "Sell"
+msgstr "Venta"
+
+#: src/paths/instance/products/list/Table.tsx:143
+#, c-format
+msgid "Profit"
+msgstr "Ganancia"
+
+#: src/paths/instance/products/list/Table.tsx:149
+#, c-format
+msgid "Sold"
+msgstr "Vendido"
+
+#: src/paths/instance/products/list/Table.tsx:210
+#, c-format
+msgid "free"
+msgstr "Gratis"
+
+#: src/paths/instance/products/list/Table.tsx:248
+#, fuzzy, c-format
+msgid "go to product update page"
+msgstr "producto actualizado correctamente"
+
+#: src/paths/instance/products/list/Table.tsx:255
+#, c-format
+msgid "Update"
+msgstr "Actualizar"
+
+#: src/paths/instance/products/list/Table.tsx:260
+#, c-format
+msgid "remove this product from the database"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:331
+#, c-format
+msgid "update the product with new price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:341
+#, c-format
+msgid "update product with new price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:399
+#, c-format
+msgid "add more elements to the inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:404
+#, c-format
+msgid "report elements lost in the inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:409
+#, fuzzy, c-format
+msgid "new price for the product"
+msgstr "no se pudo actualizar el producto"
+
+#: src/paths/instance/products/list/Table.tsx:421
+#, c-format
+msgid "the are value with errors"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:422
+#, c-format
+msgid "update product with new stock and price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:463
+#, fuzzy, c-format
+msgid "There is no products yet, add more pressing the + sign"
+msgstr "No hay propinas todavía, agregar mas presionando el signo +"
+
+#: src/paths/instance/products/list/index.tsx:86
+#, c-format
+msgid "product updated successfully"
+msgstr "producto actualizado correctamente"
+
+#: src/paths/instance/products/list/index.tsx:92
+#, c-format
+msgid "could not update the product"
+msgstr "no se pudo actualizar el producto"
+
+#: src/paths/instance/products/list/index.tsx:103
+#, c-format
+msgid "product delete successfully"
+msgstr "producto fue eliminado correctamente"
+
+#: src/paths/instance/products/list/index.tsx:109
+#, c-format
+msgid "could not delete the product"
+msgstr "no se pudo eliminar el producto"
+
+#: src/paths/instance/products/update/UpdatePage.tsx:56
+#, fuzzy, c-format
+msgid "Product id:"
+msgstr "Id de producto"
+
+#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:95
+#, c-format
+msgid ""
+"To complete the setup of the reserve, you must now initiate a wire transfer "
+"using the given wire transfer subject and crediting the specified amount to "
+"the indicated account of the exchange."
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:102
+#, c-format
+msgid "If your system supports RFC 8905, you can do this by opening this URI:"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:83
+#, fuzzy, c-format
+msgid "it should be greater than 0"
+msgstr "Debe ser mayor a 0"
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:88
+#, c-format
+msgid "must be a valid URL"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:107
+#, fuzzy, c-format
+msgid "Initial balance"
+msgstr "Instancia"
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:108
+#, c-format
+msgid "balance prior to deposit"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:112
+#, c-format
+msgid "Exchange URL"
+msgstr "URL del Exchange"
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:113
+#, c-format
+msgid "URL of exchange"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:148
+#, c-format
+msgid "Next"
+msgstr "Siguiente"
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:186
+#, c-format
+msgid "Wire method"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:187
+#, fuzzy, c-format
+msgid "method to use for wire transfer"
+msgstr "no se pudo informar la transferencia"
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:189
+#, c-format
+msgid "Select one wire method"
+msgstr ""
+
+#: src/paths/instance/reserves/create/index.tsx:62
+#, fuzzy, c-format
+msgid "could not create reserve"
+msgstr "No se pudo create el reembolso"
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:77
+#, c-format
+msgid "Valid until"
+msgstr "Válido hasta"
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:82
+#, fuzzy, c-format
+msgid "Created balance"
+msgstr "creado"
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:99
+#, fuzzy, c-format
+msgid "Exchange balance"
+msgstr "Monto inicial"
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:104
+#, c-format
+msgid "Picked up"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:109
+#, fuzzy, c-format
+msgid "Committed"
+msgstr "Monto confirmado"
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:116
+#, c-format
+msgid "Account address"
+msgstr "Dirección de cuenta"
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:119
+#, c-format
+msgid "Subject"
+msgstr "Asunto"
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:130
+#, c-format
+msgid "Tips"
+msgstr "Propinas"
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:193
+#, c-format
+msgid "No tips has been authorized from this reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:213
+#, fuzzy, c-format
+msgid "Authorized"
+msgstr "Token de autorización"
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:222
+#, fuzzy, c-format
+msgid "Expiration"
+msgstr "Información extra"
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:108
+#, fuzzy, c-format
+msgid "amount of tip"
+msgstr "monto"
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:112
+#, fuzzy, c-format
+msgid "Justification"
+msgstr "Jurisdicción"
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:114
+#, c-format
+msgid "reason for the tip"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:118
+#, c-format
+msgid "URL after tip"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:119
+#, c-format
+msgid "URL to visit after tip payment"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:65
+#, fuzzy, c-format
+msgid "Reserves not yet funded"
+msgstr "Servidor no encontrado"
+
+#: src/paths/instance/reserves/list/Table.tsx:89
+#, c-format
+msgid "Reserves ready"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:95
+#, fuzzy, c-format
+msgid "add new reserve"
+msgstr "cargar nuevas transferencias"
+
+#: src/paths/instance/reserves/list/Table.tsx:143
+#, c-format
+msgid "Expires at"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:146
+#, c-format
+msgid "Initial"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:202
+#, c-format
+msgid "delete selected reserve from the database"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:210
+#, c-format
+msgid "authorize new tip from selected reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:237
+#, fuzzy, c-format
+msgid ""
+"There is no ready reserves yet, add more pressing the + sign or fund them"
+msgstr "No hay transferencias todavía, agregar mas presionando el signo +"
+
+#: src/paths/instance/reserves/list/Table.tsx:264
+#, fuzzy, c-format
+msgid "Expected Balance"
+msgstr "Ejecutado en"
+
+#: src/paths/instance/reserves/list/index.tsx:110
+#, fuzzy, c-format
+msgid "could not create the tip"
+msgstr "No se pudo create el reembolso"
+
+#: src/paths/instance/templates/create/CreatePage.tsx:77
+#, fuzzy, c-format
+msgid "should not be empty"
+msgstr "no puede ser vacío"
+
+#: src/paths/instance/templates/create/CreatePage.tsx:93
+#, fuzzy, c-format
+msgid "should be greater that 0"
+msgstr "Debe ser mayor a 0"
+
+#: src/paths/instance/templates/create/CreatePage.tsx:96
+#, fuzzy, c-format
+msgid "can't be empty"
+msgstr "no puede ser vacío"
+
+#: src/paths/instance/templates/create/CreatePage.tsx:100
+#, c-format
+msgid "to short"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:108
+#, c-format
+msgid "just letters and numbers from 2 to 7"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:110
+#, c-format
+msgid "size of the key should be 32"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:137
+#, c-format
+msgid "Identifier"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:138
+#, c-format
+msgid "Name of the template in URLs."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:144
+#, c-format
+msgid "Describe what this template stands for"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:149
+#, fuzzy, c-format
+msgid "Fixed summary"
+msgstr "Estado de orden"
+
+#: src/paths/instance/templates/create/CreatePage.tsx:150
+#, c-format
+msgid "If specified, this template will create order with the same summary"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:154
+#, fuzzy, c-format
+msgid "Fixed price"
+msgstr "precio unitario"
+
+#: src/paths/instance/templates/create/CreatePage.tsx:155
+#, c-format
+msgid "If specified, this template will create order with the same price"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:159
+#, c-format
+msgid "Minimum age"
+msgstr "Edad mínima"
+
+#: src/paths/instance/templates/create/CreatePage.tsx:161
+#, c-format
+msgid "Is this contract restricted to some age?"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:165
+#, fuzzy, c-format
+msgid "Payment timeout"
+msgstr "Opciones de pago"
+
+#: src/paths/instance/templates/create/CreatePage.tsx:167
+#, c-format
+msgid ""
+"How much time has the customer to complete the payment once the order was "
+"created."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:171
+#, c-format
+msgid "Verification algorithm"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:172
+#, c-format
+msgid "Algorithm to use to verify transaction in offline mode"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:180
+#, c-format
+msgid "Point-of-sale key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:182
+#, c-format
+msgid "Useful to validate the purchase"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:196
+#, c-format
+msgid "generate random secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:203
+#, c-format
+msgid "random"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:208
+#, c-format
+msgid "show secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:209
+#, c-format
+msgid "hide secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:216
+#, c-format
+msgid "hide"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:218
+#, c-format
+msgid "show"
+msgstr ""
+
+#: src/paths/instance/templates/create/index.tsx:52
+#, fuzzy, c-format
+msgid "could not inform template"
+msgstr "no se pudo informar la transferencia"
+
+#: src/paths/instance/templates/use/UsePage.tsx:54
+#, fuzzy, c-format
+msgid "Amount is required"
+msgstr "Login necesario"
+
+#: src/paths/instance/templates/use/UsePage.tsx:58
+#, c-format
+msgid "Order summary is required"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:86
+#, fuzzy, c-format
+msgid "New order for template"
+msgstr "cargar viejas transferencias"
+
+#: src/paths/instance/templates/use/UsePage.tsx:108
+#, c-format
+msgid "Amount of the order"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:113
+#, fuzzy, c-format
+msgid "Order summary"
+msgstr "Estado de orden"
+
+#: src/paths/instance/templates/use/index.tsx:92
+#, fuzzy, c-format
+msgid "could not create order from template"
+msgstr "No se pudo create el reembolso"
+
+#: src/paths/instance/templates/qr/QrPage.tsx:131
+#, c-format
+msgid ""
+"Here you can specify a default value for fields that are not fixed. Default "
+"values can be edited by the customer before the payment."
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:148
+#, fuzzy, c-format
+msgid "Fixed amount"
+msgstr "Monto reembolzado"
+
+#: src/paths/instance/templates/qr/QrPage.tsx:149
+#, fuzzy, c-format
+msgid "Default amount"
+msgstr "Monto reembolzado"
+
+#: src/paths/instance/templates/qr/QrPage.tsx:161
+#, fuzzy, c-format
+msgid "Default summary"
+msgstr "Estado de orden"
+
+#: src/paths/instance/templates/qr/QrPage.tsx:177
+#, c-format
+msgid "Print"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:184
+#, c-format
+msgid "Setup TOTP"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:65
+#, c-format
+msgid "Templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:70
+#, c-format
+msgid "add new templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:142
+#, c-format
+msgid "load more templates before the first one"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:146
+#, fuzzy, c-format
+msgid "load newer templates"
+msgstr "cargar nuevas transferencias"
+
+#: src/paths/instance/templates/list/Table.tsx:181
+#, c-format
+msgid "delete selected templates from the database"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:188
+#, c-format
+msgid "use template to create new order"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:195
+#, fuzzy, c-format
+msgid "create qr code for the template"
+msgstr "No se pudo create el reembolso"
+
+#: src/paths/instance/templates/list/Table.tsx:210
+#, c-format
+msgid "load more templates after the last one"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:214
+#, fuzzy, c-format
+msgid "load older templates"
+msgstr "cargar viejas transferencias"
+
+#: src/paths/instance/templates/list/Table.tsx:231
+#, fuzzy, c-format
+msgid "There is no templates yet, add more pressing the + sign"
+msgstr "No hay propinas todavía, agregar mas presionando el signo +"
+
+#: src/paths/instance/templates/list/index.tsx:104
+#, fuzzy, c-format
+msgid "template delete successfully"
+msgstr "producto fue eliminado correctamente"
+
+#: src/paths/instance/templates/list/index.tsx:110
+#, fuzzy, c-format
+msgid "could not delete the template"
+msgstr "no se pudo eliminar el producto"
+
+#: src/paths/instance/templates/update/index.tsx:90
+#, fuzzy, c-format
+msgid "could not update template"
+msgstr "no se pudo actualizar el producto"
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:57
+#, fuzzy, c-format
+msgid "should be one of '%1$s'"
+msgstr "deberían ser iguales"
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:85
+#, c-format
+msgid "Webhook ID to use"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:89
+#, c-format
+msgid "Event"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:90
+#, c-format
+msgid "The event of the webhook: why the webhook is used"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:94
+#, c-format
+msgid "Method"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:95
+#, c-format
+msgid "Method used by the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:99
+#, c-format
+msgid "URL"
+msgstr "URL"
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:100
+#, c-format
+msgid "URL of the webhook where the customer will be redirected"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:104
+#, c-format
+msgid "Header"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:106
+#, c-format
+msgid "Header template of the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:111
+#, c-format
+msgid "Body"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:112
+#, c-format
+msgid "Body template by the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:61
+#, c-format
+msgid "Webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:66
+#, c-format
+msgid "add new webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:137
+#, c-format
+msgid "load more webhooks before the first one"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:141
+#, fuzzy, c-format
+msgid "load newer webhooks"
+msgstr "cargar nuevas ordenes"
+
+#: src/paths/instance/webhooks/list/Table.tsx:151
+#, c-format
+msgid "Event type"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:176
+#, c-format
+msgid "delete selected webhook from the database"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:198
+#, c-format
+msgid "load more webhooks after the last one"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:202
+#, fuzzy, c-format
+msgid "load older webhooks"
+msgstr "cargar viejas ordenes"
+
+#: src/paths/instance/webhooks/list/Table.tsx:219
+#, fuzzy, c-format
+msgid "There is no webhooks yet, add more pressing the + sign"
+msgstr "No hay propinas todavía, agregar mas presionando el signo +"
+
+#: src/paths/instance/webhooks/list/index.tsx:94
+#, fuzzy, c-format
+msgid "webhook delete successfully"
+msgstr "producto fue eliminado correctamente"
+
+#: src/paths/instance/webhooks/list/index.tsx:100
+#, fuzzy, c-format
+msgid "could not delete the webhook"
+msgstr "no se pudo eliminar el producto"
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:63
+#, fuzzy, c-format
+msgid "check the id, does not look valid"
+msgstr "verificar el id, no parece válido"
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:65
+#, c-format
+msgid "should have 52 characters, current %1$s"
+msgstr "debería tener 52 caracteres, actualmente %1$s"
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:72
+#, c-format
+msgid "URL doesn't have the right format"
+msgstr "La URL no tiene el formato correcto"
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:98
+#, c-format
+msgid "Credited bank account"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:100
+#, c-format
+msgid "Select one account"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:101
+#, c-format
+msgid "Bank account of the merchant where the payment was received"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:105
+#, fuzzy, c-format
+msgid "Wire transfer ID"
+msgstr "Id de transferencia"
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:107
+#, c-format
+msgid ""
+"unique identifier of the wire transfer used by the exchange, must be 52 "
+"characters long"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:112
+#, c-format
+msgid ""
+"Base URL of the exchange that made the transfer, should have been in the "
+"wire transfer subject"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:117
+#, c-format
+msgid "Amount credited"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:118
+#, c-format
+msgid "Actual amount that was wired to the merchant's bank account"
+msgstr ""
+
+#: src/paths/instance/transfers/create/index.tsx:58
+#, c-format
+msgid "could not inform transfer"
+msgstr "no se pudo informar la transferencia"
+
+#: src/paths/instance/transfers/list/Table.tsx:61
+#, c-format
+msgid "Transfers"
+msgstr "Transferencias"
+
+#: src/paths/instance/transfers/list/Table.tsx:66
+#, fuzzy, c-format
+msgid "add new transfer"
+msgstr "cargar nuevas transferencias"
+
+#: src/paths/instance/transfers/list/Table.tsx:129
+#, c-format
+msgid "load more transfers before the first one"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:133
+#, c-format
+msgid "load newer transfers"
+msgstr "cargar nuevas transferencias"
+
+#: src/paths/instance/transfers/list/Table.tsx:143
+#, c-format
+msgid "Credit"
+msgstr "Crédito"
+
+#: src/paths/instance/transfers/list/Table.tsx:152
+#, c-format
+msgid "Confirmed"
+msgstr "Confirmado"
+
+#: src/paths/instance/transfers/list/Table.tsx:155
+#, c-format
+msgid "Verified"
+msgstr "Verificado"
+
+#: src/paths/instance/transfers/list/Table.tsx:158
+#, c-format
+msgid "Executed at"
+msgstr "Ejecutado en"
+
+#: src/paths/instance/transfers/list/Table.tsx:171
+#, c-format
+msgid "yes"
+msgstr "si"
+
+#: src/paths/instance/transfers/list/Table.tsx:171
+#, c-format
+msgid "no"
+msgstr "no"
+
+#: src/paths/instance/transfers/list/Table.tsx:181
+#, c-format
+msgid "unknown"
+msgstr "desconocido"
+
+#: src/paths/instance/transfers/list/Table.tsx:187
+#, c-format
+msgid "delete selected transfer from the database"
+msgstr "eliminar transferencia seleccionada de la base de datos"
+
+#: src/paths/instance/transfers/list/Table.tsx:202
+#, c-format
+msgid "load more transfer after the last one"
+msgstr "cargue más transferencia luego de la última"
+
+#: src/paths/instance/transfers/list/Table.tsx:206
+#, c-format
+msgid "load older transfers"
+msgstr "cargar viejas transferencias"
+
+#: src/paths/instance/transfers/list/Table.tsx:223
+#, c-format
+msgid "There is no transfer yet, add more pressing the + sign"
+msgstr "No hay transferencias todavía, agregar mas presionando el signo +"
+
+#: src/paths/instance/transfers/list/ListPage.tsx:79
+#, fuzzy, c-format
+msgid "filter by account address"
+msgstr "Dirección de cuenta"
+
+#: src/paths/instance/transfers/list/ListPage.tsx:100
+#, c-format
+msgid "only show wire transfers confirmed by the merchant"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:110
+#, c-format
+msgid "only show wire transfers claimed by the exchange"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:113
+#, fuzzy, c-format
+msgid "Unverified"
+msgstr "Verificado"
+
+#: src/paths/admin/create/CreatePage.tsx:69
+#, c-format
+msgid "is not valid"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:94
+#, fuzzy, c-format
+msgid "is not a number"
+msgstr "Número de edificio"
+
+#: src/paths/admin/create/CreatePage.tsx:96
+#, c-format
+msgid "must be 1 or greater"
+msgstr "debe ser 1 o mayor"
+
+#: src/paths/admin/create/CreatePage.tsx:107
+#, c-format
+msgid "max 7 lines"
+msgstr "máximo 7 líneas"
+
+#: src/paths/admin/create/CreatePage.tsx:178
+#, c-format
+msgid "change authorization configuration"
+msgstr "cambiar configuración de autorización"
+
+#: src/paths/admin/create/CreatePage.tsx:217
+#, c-format
+msgid "Need to complete marked fields and choose authorization method"
+msgstr "Necesita completar campos marcados y escoger un método de autorización"
+
+#: src/components/form/InputPaytoForm.tsx:82
+#, c-format
+msgid "This is not a valid bitcoin address."
+msgstr "Esta no es una dirección de bitcoin válida."
+
+#: src/components/form/InputPaytoForm.tsx:95
+#, c-format
+msgid "This is not a valid Ethereum address."
+msgstr "Esta no es una dirección de Ethereum válida."
+
+#: src/components/form/InputPaytoForm.tsx:118
+#, c-format
+msgid "IBAN numbers usually have more that 4 digits"
+msgstr "Números IBAN usualmente tienen más de 4 dígitos"
+
+#: src/components/form/InputPaytoForm.tsx:120
+#, c-format
+msgid "IBAN numbers usually have less that 34 digits"
+msgstr "Número IBAN usualmente tienen menos de 34 dígitos"
+
+#: src/components/form/InputPaytoForm.tsx:128
+#, c-format
+msgid "IBAN country code not found"
+msgstr "Código IBAN de país no encontrado"
+
+#: src/components/form/InputPaytoForm.tsx:153
+#, c-format
+msgid "IBAN number is not valid, checksum is wrong"
+msgstr "Número IBAN no es válido, la suma de verificación es incorrecta"
+
+#: src/components/form/InputPaytoForm.tsx:248
+#, c-format
+msgid "Target type"
+msgstr "Tipo objetivo"
+
+#: src/components/form/InputPaytoForm.tsx:249
+#, c-format
+msgid "Method to use for wire transfer"
+msgstr "Método a usar para la transferencia"
+
+#: src/components/form/InputPaytoForm.tsx:258
+#, c-format
+msgid "Routing"
+msgstr "Enrutamiento"
+
+#: src/components/form/InputPaytoForm.tsx:259
+#, c-format
+msgid "Routing number."
+msgstr "Número de enrutamiento."
+
+#: src/components/form/InputPaytoForm.tsx:263
+#, c-format
+msgid "Account"
+msgstr "Cuenta"
+
+#: src/components/form/InputPaytoForm.tsx:264
+#, fuzzy, c-format
+msgid "Account number."
+msgstr "Dirección de cuenta"
+
+#: src/components/form/InputPaytoForm.tsx:273
+#, c-format
+msgid "Business Identifier Code."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:282
+#, c-format
+msgid "Bank Account Number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:292
+#, c-format
+msgid "Unified Payment Interface."
+msgstr "Interfaz de pago unificado."
+
+#: src/components/form/InputPaytoForm.tsx:301
+#, c-format
+msgid "Bitcoin protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:310
+#, c-format
+msgid "Ethereum protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:319
+#, c-format
+msgid "Interledger protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:328
+#, c-format
+msgid "Host"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:329
+#, c-format
+msgid "Bank host."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:334
+#, c-format
+msgid "Bank account."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:343
+#, c-format
+msgid "Bank account owner's name."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:370
+#, c-format
+msgid "No accounts yet."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:52
+#, c-format
+msgid ""
+"Name of the instance in URLs. The 'default' instance is special in that it "
+"is used to administer other instances."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:58
+#, fuzzy, c-format
+msgid "Business name"
+msgstr "Nombre de edificio"
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:59
+#, c-format
+msgid "Legal name of the business represented by this instance."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:64
+#, c-format
+msgid "Email"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:65
+#, c-format
+msgid "Contact email"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:70
+#, c-format
+msgid "Website URL"
+msgstr "URL de sitio web"
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:71
+#, c-format
+msgid "URL."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:76
+#, c-format
+msgid "Logo"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:77
+#, c-format
+msgid "Logo image."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:82
+#, c-format
+msgid "Bank account"
+msgstr "Cuenta bancaria"
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:83
+#, c-format
+msgid "URI specifying bank account for crediting revenue."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:88
+#, c-format
+msgid "Default max deposit fee"
+msgstr "Impuesto máximo de deposito por omisión"
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:89
+#, c-format
+msgid ""
+"Maximum deposit fees this merchant is willing to pay per order by default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:94
+#, c-format
+msgid "Default max wire fee"
+msgstr "Impuesto máximo de transferencia por omisión"
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:95
+#, c-format
+msgid ""
+"Maximum wire fees this merchant is willing to pay per wire transfer by "
+"default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:100
+#, c-format
+msgid "Default wire fee amortization"
+msgstr "Amortización de impuesto de transferencia por omisión"
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:101
+#, c-format
+msgid ""
+"Number of orders excess wire transfer fees will be divided by to compute per "
+"order surcharge."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:107
+#, c-format
+msgid "Physical location of the merchant."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:114
+#, c-format
+msgid "Jurisdiction"
+msgstr "Jurisdicción"
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:115
+#, c-format
+msgid "Jurisdiction for legal disputes with the merchant."
+msgstr "Jurisdicción para disputas legales con el comerciante."
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:122
+#, fuzzy, c-format
+msgid "Default payment delay"
+msgstr "Retrazo de pago por omisión"
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:124
+#, c-format
+msgid ""
+"Time customers have to pay an order before the offer expires by default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:129
+#, c-format
+msgid "Default wire transfer delay"
+msgstr "Retrazo de transferencia por omisión"
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:130
+#, c-format
+msgid ""
+"Maximum time an exchange is allowed to delay wiring funds to the merchant, "
+"enabling it to aggregate smaller payments into larger wire transfers and "
+"reducing wire fees."
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:164
+#, c-format
+msgid "Instance id"
+msgstr "ID de instancia"
+
+#: src/paths/instance/update/UpdatePage.tsx:173
+#, fuzzy, c-format
+msgid "Change the authorization method use for this instance."
+msgstr ""
+"Limpiar el token de autorización significa acceso público a la instancia"
+
+#: src/paths/instance/update/UpdatePage.tsx:182
+#, c-format
+msgid "Manage access token"
+msgstr "Administrar token de acceso"
+
+#: src/paths/instance/update/index.tsx:112
+#, c-format
+msgid "Failed to create instance"
+msgstr "Fallo al crear la instancia"
+
+#: src/components/exception/login.tsx:74
+#, c-format
+msgid "Login required"
+msgstr "Login necesario"
+
+#: src/components/exception/login.tsx:80
+#, c-format
+msgid "Please enter your access token."
+msgstr ""
+
+#: src/components/exception/login.tsx:108
+#, fuzzy, c-format
+msgid "Access Token"
+msgstr "Acceso denegado"
+
+#: src/InstanceRoutes.tsx:171
+#, c-format
+msgid "The request to the backend take too long and was cancelled"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:172
+#, c-format
+msgid "Diagnostic from %1$s is \"%2$s\""
+msgstr ""
+
+#: src/InstanceRoutes.tsx:178
+#, fuzzy, c-format
+msgid "The backend reported a problem: HTTP status #%1$s"
+msgstr "Servidir reporto un problema: HTTP status #%1$s"
+
+#: src/InstanceRoutes.tsx:179
+#, c-format
+msgid "Diagnostic from %1$s is '%2$s'"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:196
+#, c-format
+msgid "Access denied"
+msgstr "Acceso denegado"
+
+#: src/InstanceRoutes.tsx:197
+#, c-format
+msgid "The access token provided is invalid."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:212
+#, fuzzy, c-format
+msgid "No 'default' instance configured yet."
+msgstr "Sin instancia default"
+
+#: src/InstanceRoutes.tsx:213
+#, c-format
+msgid "Create a 'default' instance to begin using the merchant backoffice."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:630
+#, c-format
+msgid "The access token provided is invalid"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:664
+#, c-format
+msgid "Hide for today"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:82
+#, c-format
+msgid "Instance"
+msgstr "Instancia"
+
+#: src/components/menu/SideBar.tsx:91
+#, c-format
+msgid "Settings"
+msgstr "Configuración"
+
+#: src/components/menu/SideBar.tsx:167
+#, c-format
+msgid "Connection"
+msgstr "Conexión"
+
+#: src/components/menu/SideBar.tsx:209
+#, c-format
+msgid "New"
+msgstr "Nuevo"
+
+#: src/components/menu/SideBar.tsx:219
+#, c-format
+msgid "List"
+msgstr "Lista"
+
+#: src/components/menu/SideBar.tsx:234
+#, c-format
+msgid "Log out"
+msgstr "Salir"
+
+#: src/ApplicationReadyRoutes.tsx:71
+#, c-format
+msgid "Check your token is valid"
+msgstr "Verifica que el token sea valido"
+
+#: src/ApplicationReadyRoutes.tsx:90
+#, c-format
+msgid "Couldn't access the server."
+msgstr "No se pudo acceder al servidor."
+
+#: src/ApplicationReadyRoutes.tsx:91
+#, c-format
+msgid "Could not infer instance id from url %1$s"
+msgstr "No se pudo inferir el id de la instancia con la url %1$s"
+
+#: src/Application.tsx:104
+#, c-format
+msgid "Server not found"
+msgstr "Servidor no encontrado"
+
+#: src/Application.tsx:118
+#, c-format
+msgid "Server response with an error code"
+msgstr ""
+
+#: src/Application.tsx:120
+#, c-format
+msgid "Got message %1$s from %2$s"
+msgstr "Recibimos el mensaje %1$s desde %2$s"
+
+#: src/Application.tsx:131
+#, c-format
+msgid "Response from server is unreadable, http status: %1$s"
+msgstr ""
+
+#: src/Application.tsx:144
+#, c-format
+msgid "Unexpected Error"
+msgstr "Error inesperado"
+
+#: src/components/form/InputArray.tsx:101
+#, c-format
+msgid "The value %1$s is invalid for a payment url"
+msgstr "El valor %1$s es invalido para una URL de pago"
+
+#: src/components/form/InputArray.tsx:110
+#, c-format
+msgid "add element to the list"
+msgstr "agregar elemento a la lista"
+
+#: src/components/form/InputArray.tsx:112
+#, c-format
+msgid "add"
+msgstr "Agregar"
+
+#: src/components/form/InputSecured.tsx:37
+#, c-format
+msgid "Deleting"
+msgstr "Borrando"
+
+#: src/components/form/InputSecured.tsx:41
+#, c-format
+msgid "Changing"
+msgstr "Cambiando"
+
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:87
+#, c-format
+msgid "Order ID"
+msgstr "ID de pedido"
+
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:101
+#, c-format
+msgid "Payment URL"
+msgstr "URL de pago"
+
+#, c-format
+#~ msgid "Couldn't access the server"
+#~ msgstr "No se pudo aceder al servidor"
+
+#, c-format
+#~ msgid "HTTP status #%1$s: Server reported a problem"
+#~ msgstr "HTTP status #%1$s: Servidor reporto un problema"
+
+#, c-format
+#~ msgid "Got message: \"%1$s\" from: %2$s"
+#~ msgstr "Recibimos el mensaje: %1$s desde %2$s"
+
+#, c-format
+#~ msgid ""
+#~ "in order to use merchant backoffice, you should create the default "
+#~ "instance"
+#~ msgstr ""
+#~ "para usar el merchant backoffice, debería crear la instancia default"
+
+#, c-format
+#~ msgid "Got message: %1$s from: %2$s"
+#~ msgstr "Recibimos el mensaje %1$s desde %2$s"
+
+#, c-format
+#~ msgid ""
+#~ "Please enter your auth token. Token should have \"secret-token:\" and "
+#~ "start with Bearer or ApiKey"
+#~ msgstr ""
+#~ "Por favor ingrese su token de autorización. El token debe tener \"secret-"
+#~ "token\" y comenzar con Bearer o ApiKey"
+
+#, c-format
+#~ msgid "pick a date"
+#~ msgstr "elegir una fecha"
+
+#, c-format
+#~ msgid "no results"
+#~ msgstr "Sin resultados"
+
+#, c-format
+#~ msgid "current stock will change from %1$s to %2$s"
+#~ msgstr "stock actual cambiará desde %1$s a %2$s"
+
+#, c-format
+#~ msgid "current stock will stay at %1$s"
+#~ msgstr "stock actual seguirá en %1$s"
+
+#, c-format
+#~ msgid "this product has no taxes"
+#~ msgstr "este producto no tiene impuestos"
+
+#, c-format
+#~ msgid "Inventory products"
+#~ msgstr "Productos de inventario"
+
+#, c-format
+#~ msgid "Total tax"
+#~ msgstr "Impuesto total"
+
+#, c-format
+#~ msgid "Net"
+#~ msgstr "Neto"
+
+#, c-format
+#~ msgid "select a product first"
+#~ msgstr "seleccione un producto primero"
+
+#, c-format
+#~ msgid ""
+#~ "cannot be greater than current stock and quantity previously added. max: "
+#~ "%1$s"
+#~ msgstr ""
+#~ "no puede ser mayor al stock actual y la cantidad previamente agregada. "
+#~ "máximo: %1$s"
+
+#, c-format
+#~ msgid "cannot be greater than current stock %1$s"
+#~ msgstr "no puede ser mayor al stock actual %1$s"
+
+#, c-format
+#~ msgid "Deposit total"
+#~ msgstr "Total depositado"
+
+#, c-format
+#~ msgid "Merchant initial amount"
+#~ msgstr "Monto inicial"
+
+#, c-format
+#~ msgid "Account Address"
+#~ msgstr "Dirección de cuenta"
diff --git a/packages/auditor-backoffice-ui/src/i18n/fr.po b/packages/auditor-backoffice-ui/src/i18n/fr.po
new file mode 100644
index 000000000..d8d0bae29
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/i18n/fr.po
@@ -0,0 +1,2741 @@
+# This file is part of TALER
+# (C) 2016 GNUnet e.V.
+#
+# TALER is free software; you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 3, or (at your option) any later version.
+#
+# TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Wallet\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"POT-Creation-Date: 2016-11-23 00:00+0100\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: src/components/modal/index.tsx:71
+#, c-format
+msgid "Cancel"
+msgstr ""
+
+#: src/components/modal/index.tsx:79
+#, c-format
+msgid "%1$s"
+msgstr ""
+
+#: src/components/modal/index.tsx:84
+#, c-format
+msgid "Close"
+msgstr ""
+
+#: src/components/modal/index.tsx:124
+#, c-format
+msgid "Continue"
+msgstr ""
+
+#: src/components/modal/index.tsx:178
+#, c-format
+msgid "Clear"
+msgstr ""
+
+#: src/components/modal/index.tsx:190
+#, c-format
+msgid "Confirm"
+msgstr ""
+
+#: src/components/modal/index.tsx:296
+#, c-format
+msgid "is not the same as the current access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:299
+#, c-format
+msgid "cannot be empty"
+msgstr ""
+
+#: src/components/modal/index.tsx:301
+#, c-format
+msgid "cannot be the same as the old token"
+msgstr ""
+
+#: src/components/modal/index.tsx:305
+#, c-format
+msgid "is not the same"
+msgstr ""
+
+#: src/components/modal/index.tsx:315
+#, c-format
+msgid "You are updating the access token from instance with id %1$s"
+msgstr ""
+
+#: src/components/modal/index.tsx:331
+#, c-format
+msgid "Old access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:332
+#, c-format
+msgid "access token currently in use"
+msgstr ""
+
+#: src/components/modal/index.tsx:338
+#, c-format
+msgid "New access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:339
+#, c-format
+msgid "next access token to be used"
+msgstr ""
+
+#: src/components/modal/index.tsx:344
+#, c-format
+msgid "Repeat access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:345
+#, c-format
+msgid "confirm the same access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:350
+#, c-format
+msgid "Clearing the access token will mean public access to the instance"
+msgstr ""
+
+#: src/components/modal/index.tsx:377
+#, c-format
+msgid "cannot be the same as the old access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:394
+#, c-format
+msgid "You are setting the access token for the new instance"
+msgstr ""
+
+#: src/components/modal/index.tsx:420
+#, c-format
+msgid ""
+"With external authorization method no check will be done by the merchant "
+"backend"
+msgstr ""
+
+#: src/components/modal/index.tsx:436
+#, c-format
+msgid "Set external authorization"
+msgstr ""
+
+#: src/components/modal/index.tsx:448
+#, c-format
+msgid "Set access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:470
+#, c-format
+msgid "Operation in progress..."
+msgstr ""
+
+#: src/components/modal/index.tsx:479
+#, c-format
+msgid "The operation will be automatically canceled after %1$s seconds"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:80
+#, c-format
+msgid "Instances"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:93
+#, c-format
+msgid "Delete"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:99
+#, c-format
+msgid "add new instance"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:178
+#, c-format
+msgid "ID"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:181
+#, c-format
+msgid "Name"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:220
+#, c-format
+msgid "Edit"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:237
+#, c-format
+msgid "Purge"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:261
+#, c-format
+msgid "There is no instances yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:68
+#, c-format
+msgid "Only show active instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:71
+#, c-format
+msgid "Active"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:78
+#, c-format
+msgid "Only show deleted instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:81
+#, c-format
+msgid "Deleted"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:88
+#, c-format
+msgid "Show all instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:91
+#, c-format
+msgid "All"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:101
+#, c-format
+msgid "Instance \"%1$s\" (ID: %2$s) has been deleted"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:106
+#, c-format
+msgid "Failed to delete instance"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:124
+#, c-format
+msgid "Instance '%1$s' (ID: %2$s) has been disabled"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:129
+#, c-format
+msgid "Failed to purge instance"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:41
+#, c-format
+msgid "Pending KYC verification"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:66
+#, c-format
+msgid "Timed out"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:103
+#, c-format
+msgid "Exchange"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:106
+#, c-format
+msgid "Target account"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:109
+#, c-format
+msgid "KYC URL"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:144
+#, c-format
+msgid "Code"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:147
+#, c-format
+msgid "Http Status"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:177
+#, c-format
+msgid "No pending kyc verification!"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:123
+#, c-format
+msgid "change value to unknown date"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:124
+#, c-format
+msgid "change value to empty"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:131
+#, c-format
+msgid "clear"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:136
+#, c-format
+msgid "change value to never"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:141
+#, c-format
+msgid "never"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:29
+#, c-format
+msgid "Country"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:33
+#, c-format
+msgid "Address"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:39
+#, c-format
+msgid "Building number"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:41
+#, c-format
+msgid "Building name"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:42
+#, c-format
+msgid "Street"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:43
+#, c-format
+msgid "Post code"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:44
+#, c-format
+msgid "Town location"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:45
+#, c-format
+msgid "Town"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:46
+#, c-format
+msgid "District"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:49
+#, c-format
+msgid "Country subdivision"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:66
+#, c-format
+msgid "Product id"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:69
+#, c-format
+msgid "Description"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:94
+#, c-format
+msgid "Product"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:95
+#, c-format
+msgid "search products by it's description or id"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:151
+#, c-format
+msgid "no products found with that description"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:56
+#, c-format
+msgid "You must enter a valid product identifier."
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:64
+#, c-format
+msgid "Quantity must be greater than 0!"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:76
+#, c-format
+msgid ""
+"This quantity exceeds remaining stock. Currently, only %1$s units remain "
+"unreserved in stock."
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:109
+#, c-format
+msgid "Quantity"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:110
+#, c-format
+msgid "how many products will be added"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:117
+#, c-format
+msgid "Add from inventory"
+msgstr ""
+
+#: src/components/form/InputImage.tsx:105
+#, c-format
+msgid "Image should be smaller than 1 MB"
+msgstr ""
+
+#: src/components/form/InputImage.tsx:110
+#, c-format
+msgid "Add"
+msgstr ""
+
+#: src/components/form/InputImage.tsx:115
+#, c-format
+msgid "Remove"
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:113
+#, c-format
+msgid "No taxes configured for this product."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:119
+#, c-format
+msgid "Amount"
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:120
+#, c-format
+msgid ""
+"Taxes can be in currencies that differ from the main currency used by the "
+"merchant."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:122
+#, c-format
+msgid ""
+"Enter currency and value separated with a colon, e.g. &quot;USD:2.3&quot;."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:131
+#, c-format
+msgid "Legal name of the tax, e.g. VAT or import duties."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:137
+#, c-format
+msgid "add tax to the tax list"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:72
+#, c-format
+msgid "describe and add a product that is not in the inventory list"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:75
+#, c-format
+msgid "Add custom product"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:86
+#, c-format
+msgid "Complete information of the product"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:185
+#, c-format
+msgid "Image"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:186
+#, c-format
+msgid "photo of the product"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:192
+#, c-format
+msgid "full product description"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:196
+#, c-format
+msgid "Unit"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:197
+#, c-format
+msgid "name of the product unit"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:201
+#, c-format
+msgid "Price"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:202
+#, c-format
+msgid "amount in the current currency"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:211
+#, c-format
+msgid "Taxes"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:38
+#, c-format
+msgid "image"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:41
+#, c-format
+msgid "description"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:44
+#, c-format
+msgid "quantity"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:47
+#, c-format
+msgid "unit price"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:50
+#, c-format
+msgid "total price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:153
+#, c-format
+msgid "required"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:157
+#, c-format
+msgid "not valid"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:159
+#, c-format
+msgid "must be greater than 0"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:164
+#, c-format
+msgid "not a valid json"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:170
+#, c-format
+msgid "should be in the future"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:173
+#, c-format
+msgid "refund deadline cannot be before pay deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:179
+#, c-format
+msgid "wire transfer deadline cannot be before refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:190
+#, c-format
+msgid "wire transfer deadline cannot be before pay deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:197
+#, c-format
+msgid "should have a refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:202
+#, c-format
+msgid "auto refund cannot be after refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:360
+#, c-format
+msgid "Manage products in order"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:369
+#, c-format
+msgid "Manage list of products in the order."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:391
+#, c-format
+msgid "Remove this product from the order."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:415
+#, c-format
+msgid "Total price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:417
+#, c-format
+msgid "total product price added up"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:430
+#, c-format
+msgid "Amount to be paid by the customer"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:436
+#, c-format
+msgid "Order price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:437
+#, c-format
+msgid "final order price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:444
+#, c-format
+msgid "Summary"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:445
+#, c-format
+msgid "Title of the order to be shown to the customer"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:450
+#, c-format
+msgid "Shipping and Fulfillment"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:455
+#, c-format
+msgid "Delivery date"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:456
+#, c-format
+msgid "Deadline for physical delivery assured by the merchant."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:461
+#, c-format
+msgid "Location"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:462
+#, c-format
+msgid "address where the products will be delivered"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:469
+#, c-format
+msgid "Fulfillment URL"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:470
+#, c-format
+msgid "URL to which the user will be redirected after successful payment."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:476
+#, c-format
+msgid "Taler payment options"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:477
+#, c-format
+msgid "Override default Taler payment settings for this order"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:481
+#, c-format
+msgid "Payment deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:482
+#, c-format
+msgid ""
+"Deadline for the customer to pay for the offer before it expires. Inventory "
+"products will be reserved until this deadline."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:486
+#, c-format
+msgid "Refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:487
+#, c-format
+msgid "Time until which the order can be refunded by the merchant."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:491
+#, c-format
+msgid "Wire transfer deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:492
+#, c-format
+msgid "Deadline for the exchange to make the wire transfer."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:496
+#, c-format
+msgid "Auto-refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:497
+#, c-format
+msgid ""
+"Time until which the wallet will automatically check for refunds without "
+"user interaction."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:502
+#, c-format
+msgid "Maximum deposit fee"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:503
+#, c-format
+msgid ""
+"Maximum deposit fees the merchant is willing to cover for this order. Higher "
+"deposit fees must be covered in full by the consumer."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:507
+#, c-format
+msgid "Maximum wire fee"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:508
+#, c-format
+msgid ""
+"Maximum aggregate wire fees the merchant is willing to cover for this order. "
+"Wire fees exceeding this amount are to be covered by the customers."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:512
+#, c-format
+msgid "Wire fee amortization"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:513
+#, c-format
+msgid ""
+"Factor by which wire fees exceeding the above threshold are divided to "
+"determine the share of excess wire fees to be paid explicitly by the "
+"consumer."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:517
+#, c-format
+msgid "Create token"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:518
+#, c-format
+msgid ""
+"Uncheck this option if the merchant backend generated an order ID with "
+"enough entropy to prevent adversarial claims."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:522
+#, c-format
+msgid "Minimum age required"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:523
+#, c-format
+msgid ""
+"Any value greater than 0 will limit the coins able be used to pay this "
+"contract. If empty the age restriction will be defined by the products"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:526
+#, c-format
+msgid "Min age defined by the producs is %1$s"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:534
+#, c-format
+msgid "Additional information"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:535
+#, c-format
+msgid "Custom information to be included in the contract for this order."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:541
+#, c-format
+msgid "You must enter a value in JavaScript Object Notation (JSON)."
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:55
+#, c-format
+msgid "days"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:65
+#, c-format
+msgid "hours"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:76
+#, c-format
+msgid "minutes"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:87
+#, c-format
+msgid "seconds"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:53
+#, c-format
+msgid "forever"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:62
+#, c-format
+msgid "%1$sM"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:64
+#, c-format
+msgid "%1$sY"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:66
+#, c-format
+msgid "%1$sd"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:68
+#, c-format
+msgid "%1$sh"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:70
+#, c-format
+msgid "%1$smin"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:72
+#, c-format
+msgid "%1$ssec"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:75
+#, c-format
+msgid "Orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:81
+#, c-format
+msgid "create order"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:147
+#, c-format
+msgid "load newer orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:154
+#, c-format
+msgid "Date"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:200
+#, c-format
+msgid "Refund"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:209
+#, c-format
+msgid "copy url"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:225
+#, c-format
+msgid "load older orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:242
+#, c-format
+msgid "No orders have been found matching your query!"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:288
+#, c-format
+msgid "duplicated"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:299
+#, c-format
+msgid "invalid format"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:301
+#, c-format
+msgid "this value exceed the refundable amount"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:346
+#, c-format
+msgid "date"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:349
+#, c-format
+msgid "amount"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:352
+#, c-format
+msgid "reason"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:389
+#, c-format
+msgid "amount to be refunded"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:391
+#, c-format
+msgid "Max refundable:"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:396
+#, c-format
+msgid "Reason"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:397
+#, c-format
+msgid "Choose one..."
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:399
+#, c-format
+msgid "requested by the customer"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:400
+#, c-format
+msgid "other"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:403
+#, c-format
+msgid "why this order is being refunded"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:409
+#, c-format
+msgid "more information to give context"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:62
+#, c-format
+msgid "Contract Terms"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:68
+#, c-format
+msgid "human-readable description of the whole purchase"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:74
+#, c-format
+msgid "total price for the transaction"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:81
+#, c-format
+msgid "URL for this purchase"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:87
+#, c-format
+msgid "Max fee"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:88
+#, c-format
+msgid "maximum total deposit fee accepted by the merchant for this contract"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:93
+#, c-format
+msgid "Max wire fee"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:94
+#, c-format
+msgid "maximum wire fee accepted by the merchant"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:100
+#, c-format
+msgid ""
+"over how many customer transactions does the merchant expect to amortize "
+"wire fees on average"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:105
+#, c-format
+msgid "Created at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:106
+#, c-format
+msgid "time when this contract was generated"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:112
+#, c-format
+msgid "after this deadline has passed no refunds will be accepted"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:118
+#, c-format
+msgid ""
+"after this deadline, the merchant won't accept payments for the contract"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:124
+#, c-format
+msgid "transfer deadline for the exchange"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:130
+#, c-format
+msgid "time indicating when the order should be delivered"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:136
+#, c-format
+msgid "where the order will be delivered"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:144
+#, c-format
+msgid "Auto-refund delay"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:145
+#, c-format
+msgid ""
+"how long the wallet should try to get an automatic refund for the purchase"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:150
+#, c-format
+msgid "Extra info"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:151
+#, c-format
+msgid "extra data that is only interpreted by the merchant frontend"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:219
+#, c-format
+msgid "Order"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:221
+#, c-format
+msgid "claimed"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:247
+#, c-format
+msgid "claimed at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:265
+#, c-format
+msgid "Timeline"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:271
+#, c-format
+msgid "Payment details"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:291
+#, c-format
+msgid "Order status"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:301
+#, c-format
+msgid "Product list"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:451
+#, c-format
+msgid "paid"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:455
+#, c-format
+msgid "wired"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:460
+#, c-format
+msgid "refunded"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:480
+#, c-format
+msgid "refund order"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:481
+#, c-format
+msgid "not refundable"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:489
+#, c-format
+msgid "refund"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:553
+#, c-format
+msgid "Refunded amount"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:560
+#, c-format
+msgid "Refund taken"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:570
+#, c-format
+msgid "Status URL"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:583
+#, c-format
+msgid "Refund URI"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:636
+#, c-format
+msgid "unpaid"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:654
+#, c-format
+msgid "pay at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:666
+#, c-format
+msgid "created at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:707
+#, c-format
+msgid "Order status URL"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:711
+#, c-format
+msgid "Payment URI"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:740
+#, c-format
+msgid ""
+"Unknown order status. This is an error, please contact the administrator."
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:767
+#, c-format
+msgid "Back"
+msgstr ""
+
+#: src/paths/instance/orders/details/index.tsx:79
+#, c-format
+msgid "refund created successfully"
+msgstr ""
+
+#: src/paths/instance/orders/details/index.tsx:85
+#, c-format
+msgid "could not create the refund"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:78
+#, c-format
+msgid "select date to show nearby orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:94
+#, c-format
+msgid "order id"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:100
+#, c-format
+msgid "jump to order with the given order ID"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:122
+#, c-format
+msgid "remove all filters"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:132
+#, c-format
+msgid "only show paid orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:135
+#, c-format
+msgid "Paid"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:142
+#, c-format
+msgid "only show orders with refunds"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:145
+#, c-format
+msgid "Refunded"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:152
+#, c-format
+msgid ""
+"only show orders where customers paid, but wire payments from payment "
+"provider are still pending"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:155
+#, c-format
+msgid "Not wired"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:170
+#, c-format
+msgid "clear date filter"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:184
+#, c-format
+msgid "date (YYYY/MM/DD)"
+msgstr ""
+
+#: src/paths/instance/orders/list/index.tsx:103
+#, c-format
+msgid "Enter an order id"
+msgstr ""
+
+#: src/paths/instance/orders/list/index.tsx:111
+#, c-format
+msgid "order not found"
+msgstr ""
+
+#: src/paths/instance/orders/list/index.tsx:178
+#, c-format
+msgid "could not get the order to refund"
+msgstr ""
+
+#: src/components/exception/AsyncButton.tsx:43
+#, c-format
+msgid "Loading..."
+msgstr ""
+
+#: src/components/form/InputStock.tsx:99
+#, c-format
+msgid ""
+"click here to configure the stock of the product, leave it as is and the "
+"backend will not control stock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:109
+#, c-format
+msgid "Manage stock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:115
+#, c-format
+msgid "this product has been configured without stock control"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:119
+#, c-format
+msgid "Infinite"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:136
+#, c-format
+msgid "lost cannot be greater than current and incoming (max %1$s)"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:176
+#, c-format
+msgid "Incoming"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:177
+#, c-format
+msgid "Lost"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:192
+#, c-format
+msgid "Current"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:196
+#, c-format
+msgid "remove stock control for this product"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:202
+#, c-format
+msgid "without stock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:211
+#, c-format
+msgid "Next restock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:217
+#, c-format
+msgid "Delivery address"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:133
+#, c-format
+msgid "product identification to use in URLs (for internal use only)"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:139
+#, c-format
+msgid "illustration of the product for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:145
+#, c-format
+msgid "product description for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:149
+#, c-format
+msgid "Age restricted"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:150
+#, c-format
+msgid "is this product restricted for customer below certain age?"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:155
+#, c-format
+msgid ""
+"unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 "
+"items, 5 meters) for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:160
+#, c-format
+msgid ""
+"sale price for customers, including taxes, for above units of the product"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:164
+#, c-format
+msgid "Stock"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:166
+#, c-format
+msgid ""
+"product inventory for products with finite supply (for internal use only)"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:171
+#, c-format
+msgid "taxes included in the product price, exposed to customers"
+msgstr ""
+
+#: src/paths/instance/products/create/CreatePage.tsx:66
+#, c-format
+msgid "Need to complete marked fields"
+msgstr ""
+
+#: src/paths/instance/products/create/index.tsx:51
+#, c-format
+msgid "could not create product"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:68
+#, c-format
+msgid "Products"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:73
+#, c-format
+msgid "add product to inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:137
+#, c-format
+msgid "Sell"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:143
+#, c-format
+msgid "Profit"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:149
+#, c-format
+msgid "Sold"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:210
+#, c-format
+msgid "free"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:248
+#, c-format
+msgid "go to product update page"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:255
+#, c-format
+msgid "Update"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:260
+#, c-format
+msgid "remove this product from the database"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:331
+#, c-format
+msgid "update the product with new price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:341
+#, c-format
+msgid "update product with new price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:399
+#, c-format
+msgid "add more elements to the inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:404
+#, c-format
+msgid "report elements lost in the inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:409
+#, c-format
+msgid "new price for the product"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:421
+#, c-format
+msgid "the are value with errors"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:422
+#, c-format
+msgid "update product with new stock and price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:463
+#, c-format
+msgid "There is no products yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:86
+#, c-format
+msgid "product updated successfully"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:92
+#, c-format
+msgid "could not update the product"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:103
+#, c-format
+msgid "product delete successfully"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:109
+#, c-format
+msgid "could not delete the product"
+msgstr ""
+
+#: src/paths/instance/products/update/UpdatePage.tsx:56
+#, c-format
+msgid "Product id:"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:95
+#, c-format
+msgid ""
+"To complete the setup of the reserve, you must now initiate a wire transfer "
+"using the given wire transfer subject and crediting the specified amount to "
+"the indicated account of the exchange."
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:102
+#, c-format
+msgid "If your system supports RFC 8905, you can do this by opening this URI:"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:83
+#, c-format
+msgid "it should be greater than 0"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:88
+#, c-format
+msgid "must be a valid URL"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:107
+#, c-format
+msgid "Initial balance"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:108
+#, c-format
+msgid "balance prior to deposit"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:112
+#, c-format
+msgid "Exchange URL"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:113
+#, c-format
+msgid "URL of exchange"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:148
+#, c-format
+msgid "Next"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:186
+#, c-format
+msgid "Wire method"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:187
+#, c-format
+msgid "method to use for wire transfer"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:189
+#, c-format
+msgid "Select one wire method"
+msgstr ""
+
+#: src/paths/instance/reserves/create/index.tsx:62
+#, c-format
+msgid "could not create reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:77
+#, c-format
+msgid "Valid until"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:82
+#, c-format
+msgid "Created balance"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:99
+#, c-format
+msgid "Exchange balance"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:104
+#, c-format
+msgid "Picked up"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:109
+#, c-format
+msgid "Committed"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:116
+#, c-format
+msgid "Account address"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:119
+#, c-format
+msgid "Subject"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:130
+#, c-format
+msgid "Tips"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:193
+#, c-format
+msgid "No tips has been authorized from this reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:213
+#, c-format
+msgid "Authorized"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:222
+#, c-format
+msgid "Expiration"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:108
+#, c-format
+msgid "amount of tip"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:112
+#, c-format
+msgid "Justification"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:114
+#, c-format
+msgid "reason for the tip"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:118
+#, c-format
+msgid "URL after tip"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:119
+#, c-format
+msgid "URL to visit after tip payment"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:65
+#, c-format
+msgid "Reserves not yet funded"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:89
+#, c-format
+msgid "Reserves ready"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:95
+#, c-format
+msgid "add new reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:143
+#, c-format
+msgid "Expires at"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:146
+#, c-format
+msgid "Initial"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:202
+#, c-format
+msgid "delete selected reserve from the database"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:210
+#, c-format
+msgid "authorize new tip from selected reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:237
+#, c-format
+msgid ""
+"There is no ready reserves yet, add more pressing the + sign or fund them"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:264
+#, c-format
+msgid "Expected Balance"
+msgstr ""
+
+#: src/paths/instance/reserves/list/index.tsx:110
+#, c-format
+msgid "could not create the tip"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:77
+#, c-format
+msgid "should not be empty"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:93
+#, c-format
+msgid "should be greater that 0"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:96
+#, c-format
+msgid "can't be empty"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:100
+#, c-format
+msgid "to short"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:108
+#, c-format
+msgid "just letters and numbers from 2 to 7"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:110
+#, c-format
+msgid "size of the key should be 32"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:137
+#, c-format
+msgid "Identifier"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:138
+#, c-format
+msgid "Name of the template in URLs."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:144
+#, c-format
+msgid "Describe what this template stands for"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:149
+#, c-format
+msgid "Fixed summary"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:150
+#, c-format
+msgid "If specified, this template will create order with the same summary"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:154
+#, c-format
+msgid "Fixed price"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:155
+#, c-format
+msgid "If specified, this template will create order with the same price"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:159
+#, c-format
+msgid "Minimum age"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:161
+#, c-format
+msgid "Is this contract restricted to some age?"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:165
+#, c-format
+msgid "Payment timeout"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:167
+#, c-format
+msgid ""
+"How much time has the customer to complete the payment once the order was "
+"created."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:171
+#, c-format
+msgid "Verification algorithm"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:172
+#, c-format
+msgid "Algorithm to use to verify transaction in offline mode"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:180
+#, c-format
+msgid "Point-of-sale key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:182
+#, c-format
+msgid "Useful to validate the purchase"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:196
+#, c-format
+msgid "generate random secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:203
+#, c-format
+msgid "random"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:208
+#, c-format
+msgid "show secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:209
+#, c-format
+msgid "hide secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:216
+#, c-format
+msgid "hide"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:218
+#, c-format
+msgid "show"
+msgstr ""
+
+#: src/paths/instance/templates/create/index.tsx:52
+#, c-format
+msgid "could not inform template"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:54
+#, c-format
+msgid "Amount is required"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:58
+#, c-format
+msgid "Order summary is required"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:86
+#, c-format
+msgid "New order for template"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:108
+#, c-format
+msgid "Amount of the order"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:113
+#, c-format
+msgid "Order summary"
+msgstr ""
+
+#: src/paths/instance/templates/use/index.tsx:92
+#, c-format
+msgid "could not create order from template"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:131
+#, c-format
+msgid ""
+"Here you can specify a default value for fields that are not fixed. Default "
+"values can be edited by the customer before the payment."
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:148
+#, c-format
+msgid "Fixed amount"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:149
+#, c-format
+msgid "Default amount"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:161
+#, c-format
+msgid "Default summary"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:177
+#, c-format
+msgid "Print"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:184
+#, c-format
+msgid "Setup TOTP"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:65
+#, c-format
+msgid "Templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:70
+#, c-format
+msgid "add new templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:142
+#, c-format
+msgid "load more templates before the first one"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:146
+#, c-format
+msgid "load newer templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:181
+#, c-format
+msgid "delete selected templates from the database"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:188
+#, c-format
+msgid "use template to create new order"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:195
+#, c-format
+msgid "create qr code for the template"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:210
+#, c-format
+msgid "load more templates after the last one"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:214
+#, c-format
+msgid "load older templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:231
+#, c-format
+msgid "There is no templates yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/templates/list/index.tsx:104
+#, c-format
+msgid "template delete successfully"
+msgstr ""
+
+#: src/paths/instance/templates/list/index.tsx:110
+#, c-format
+msgid "could not delete the template"
+msgstr ""
+
+#: src/paths/instance/templates/update/index.tsx:90
+#, c-format
+msgid "could not update template"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:57
+#, c-format
+msgid "should be one of '%1$s'"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:85
+#, c-format
+msgid "Webhook ID to use"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:89
+#, c-format
+msgid "Event"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:90
+#, c-format
+msgid "The event of the webhook: why the webhook is used"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:94
+#, c-format
+msgid "Method"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:95
+#, c-format
+msgid "Method used by the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:99
+#, c-format
+msgid "URL"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:100
+#, c-format
+msgid "URL of the webhook where the customer will be redirected"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:104
+#, c-format
+msgid "Header"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:106
+#, c-format
+msgid "Header template of the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:111
+#, c-format
+msgid "Body"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:112
+#, c-format
+msgid "Body template by the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:61
+#, c-format
+msgid "Webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:66
+#, c-format
+msgid "add new webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:137
+#, c-format
+msgid "load more webhooks before the first one"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:141
+#, c-format
+msgid "load newer webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:151
+#, c-format
+msgid "Event type"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:176
+#, c-format
+msgid "delete selected webhook from the database"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:198
+#, c-format
+msgid "load more webhooks after the last one"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:202
+#, c-format
+msgid "load older webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:219
+#, c-format
+msgid "There is no webhooks yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/index.tsx:94
+#, c-format
+msgid "webhook delete successfully"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/index.tsx:100
+#, c-format
+msgid "could not delete the webhook"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:63
+#, c-format
+msgid "check the id, does not look valid"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:65
+#, c-format
+msgid "should have 52 characters, current %1$s"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:72
+#, c-format
+msgid "URL doesn't have the right format"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:98
+#, c-format
+msgid "Credited bank account"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:100
+#, c-format
+msgid "Select one account"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:101
+#, c-format
+msgid "Bank account of the merchant where the payment was received"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:105
+#, c-format
+msgid "Wire transfer ID"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:107
+#, c-format
+msgid ""
+"unique identifier of the wire transfer used by the exchange, must be 52 "
+"characters long"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:112
+#, c-format
+msgid ""
+"Base URL of the exchange that made the transfer, should have been in the "
+"wire transfer subject"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:117
+#, c-format
+msgid "Amount credited"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:118
+#, c-format
+msgid "Actual amount that was wired to the merchant's bank account"
+msgstr ""
+
+#: src/paths/instance/transfers/create/index.tsx:58
+#, c-format
+msgid "could not inform transfer"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:61
+#, c-format
+msgid "Transfers"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:66
+#, c-format
+msgid "add new transfer"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:129
+#, c-format
+msgid "load more transfers before the first one"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:133
+#, c-format
+msgid "load newer transfers"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:143
+#, c-format
+msgid "Credit"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:152
+#, c-format
+msgid "Confirmed"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:155
+#, c-format
+msgid "Verified"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:158
+#, c-format
+msgid "Executed at"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:171
+#, c-format
+msgid "yes"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:171
+#, c-format
+msgid "no"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:181
+#, c-format
+msgid "unknown"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:187
+#, c-format
+msgid "delete selected transfer from the database"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:202
+#, c-format
+msgid "load more transfer after the last one"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:206
+#, c-format
+msgid "load older transfers"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:223
+#, c-format
+msgid "There is no transfer yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:79
+#, c-format
+msgid "filter by account address"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:100
+#, c-format
+msgid "only show wire transfers confirmed by the merchant"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:110
+#, c-format
+msgid "only show wire transfers claimed by the exchange"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:113
+#, c-format
+msgid "Unverified"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:69
+#, c-format
+msgid "is not valid"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:94
+#, c-format
+msgid "is not a number"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:96
+#, c-format
+msgid "must be 1 or greater"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:107
+#, c-format
+msgid "max 7 lines"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:178
+#, c-format
+msgid "change authorization configuration"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:217
+#, c-format
+msgid "Need to complete marked fields and choose authorization method"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:82
+#, c-format
+msgid "This is not a valid bitcoin address."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:95
+#, c-format
+msgid "This is not a valid Ethereum address."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:118
+#, c-format
+msgid "IBAN numbers usually have more that 4 digits"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:120
+#, c-format
+msgid "IBAN numbers usually have less that 34 digits"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:128
+#, c-format
+msgid "IBAN country code not found"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:153
+#, c-format
+msgid "IBAN number is not valid, checksum is wrong"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:248
+#, c-format
+msgid "Target type"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:249
+#, c-format
+msgid "Method to use for wire transfer"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:258
+#, c-format
+msgid "Routing"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:259
+#, c-format
+msgid "Routing number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:263
+#, c-format
+msgid "Account"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:264
+#, c-format
+msgid "Account number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:273
+#, c-format
+msgid "Business Identifier Code."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:282
+#, c-format
+msgid "Bank Account Number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:292
+#, c-format
+msgid "Unified Payment Interface."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:301
+#, c-format
+msgid "Bitcoin protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:310
+#, c-format
+msgid "Ethereum protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:319
+#, c-format
+msgid "Interledger protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:328
+#, c-format
+msgid "Host"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:329
+#, c-format
+msgid "Bank host."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:334
+#, c-format
+msgid "Bank account."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:343
+#, c-format
+msgid "Bank account owner's name."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:370
+#, c-format
+msgid "No accounts yet."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:52
+#, c-format
+msgid ""
+"Name of the instance in URLs. The 'default' instance is special in that it "
+"is used to administer other instances."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:58
+#, c-format
+msgid "Business name"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:59
+#, c-format
+msgid "Legal name of the business represented by this instance."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:64
+#, c-format
+msgid "Email"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:65
+#, c-format
+msgid "Contact email"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:70
+#, c-format
+msgid "Website URL"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:71
+#, c-format
+msgid "URL."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:76
+#, c-format
+msgid "Logo"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:77
+#, c-format
+msgid "Logo image."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:82
+#, c-format
+msgid "Bank account"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:83
+#, c-format
+msgid "URI specifying bank account for crediting revenue."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:88
+#, c-format
+msgid "Default max deposit fee"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:89
+#, c-format
+msgid ""
+"Maximum deposit fees this merchant is willing to pay per order by default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:94
+#, c-format
+msgid "Default max wire fee"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:95
+#, c-format
+msgid ""
+"Maximum wire fees this merchant is willing to pay per wire transfer by "
+"default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:100
+#, c-format
+msgid "Default wire fee amortization"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:101
+#, c-format
+msgid ""
+"Number of orders excess wire transfer fees will be divided by to compute per "
+"order surcharge."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:107
+#, c-format
+msgid "Physical location of the merchant."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:114
+#, c-format
+msgid "Jurisdiction"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:115
+#, c-format
+msgid "Jurisdiction for legal disputes with the merchant."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:122
+#, c-format
+msgid "Default payment delay"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:124
+#, c-format
+msgid ""
+"Time customers have to pay an order before the offer expires by default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:129
+#, c-format
+msgid "Default wire transfer delay"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:130
+#, c-format
+msgid ""
+"Maximum time an exchange is allowed to delay wiring funds to the merchant, "
+"enabling it to aggregate smaller payments into larger wire transfers and "
+"reducing wire fees."
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:164
+#, c-format
+msgid "Instance id"
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:173
+#, c-format
+msgid "Change the authorization method use for this instance."
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:182
+#, c-format
+msgid "Manage access token"
+msgstr ""
+
+#: src/paths/instance/update/index.tsx:112
+#, c-format
+msgid "Failed to create instance"
+msgstr ""
+
+#: src/components/exception/login.tsx:74
+#, c-format
+msgid "Login required"
+msgstr ""
+
+#: src/components/exception/login.tsx:80
+#, c-format
+msgid "Please enter your access token."
+msgstr ""
+
+#: src/components/exception/login.tsx:108
+#, c-format
+msgid "Access Token"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:171
+#, c-format
+msgid "The request to the backend take too long and was cancelled"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:172
+#, c-format
+msgid "Diagnostic from %1$s is \"%2$s\""
+msgstr ""
+
+#: src/InstanceRoutes.tsx:178
+#, c-format
+msgid "The backend reported a problem: HTTP status #%1$s"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:179
+#, c-format
+msgid "Diagnostic from %1$s is '%2$s'"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:196
+#, c-format
+msgid "Access denied"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:197
+#, c-format
+msgid "The access token provided is invalid."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:212
+#, c-format
+msgid "No 'default' instance configured yet."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:213
+#, c-format
+msgid "Create a 'default' instance to begin using the merchant backoffice."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:630
+#, c-format
+msgid "The access token provided is invalid"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:664
+#, c-format
+msgid "Hide for today"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:82
+#, c-format
+msgid "Instance"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:91
+#, c-format
+msgid "Settings"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:167
+#, c-format
+msgid "Connection"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:209
+#, c-format
+msgid "New"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:219
+#, c-format
+msgid "List"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:234
+#, c-format
+msgid "Log out"
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:71
+#, c-format
+msgid "Check your token is valid"
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:90
+#, c-format
+msgid "Couldn't access the server."
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:91
+#, c-format
+msgid "Could not infer instance id from url %1$s"
+msgstr ""
+
+#: src/Application.tsx:104
+#, c-format
+msgid "Server not found"
+msgstr ""
+
+#: src/Application.tsx:118
+#, c-format
+msgid "Server response with an error code"
+msgstr ""
+
+#: src/Application.tsx:120
+#, c-format
+msgid "Got message %1$s from %2$s"
+msgstr ""
+
+#: src/Application.tsx:131
+#, c-format
+msgid "Response from server is unreadable, http status: %1$s"
+msgstr ""
+
+#: src/Application.tsx:144
+#, c-format
+msgid "Unexpected Error"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:101
+#, c-format
+msgid "The value %1$s is invalid for a payment url"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:110
+#, c-format
+msgid "add element to the list"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:112
+#, c-format
+msgid "add"
+msgstr ""
+
+#: src/components/form/InputSecured.tsx:37
+#, c-format
+msgid "Deleting"
+msgstr ""
+
+#: src/components/form/InputSecured.tsx:41
+#, c-format
+msgid "Changing"
+msgstr ""
+
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:87
+#, c-format
+msgid "Order ID"
+msgstr ""
+
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:101
+#, c-format
+msgid "Payment URL"
+msgstr ""
diff --git a/packages/auditor-backoffice-ui/src/i18n/it.po b/packages/auditor-backoffice-ui/src/i18n/it.po
new file mode 100644
index 000000000..4055af10e
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/i18n/it.po
@@ -0,0 +1,2742 @@
+# This file is part of TALER
+# (C) 2016 GNUnet e.V.
+#
+# TALER is free software; you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 3, or (at your option) any later version.
+#
+# TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Wallet\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"POT-Creation-Date: 2016-11-23 00:00+0100\n"
+"PO-Revision-Date: 2023-08-16 12:43+0000\n"
+"Last-Translator: Krystian Baran <kiszkot@murena.io>\n"
+"Language-Team: Italian <https://weblate.taler.net/projects/gnu-taler/"
+"merchant-backoffice/it/>\n"
+"Language: it\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 4.13.1\n"
+
+#: src/components/modal/index.tsx:71
+#, c-format
+msgid "Cancel"
+msgstr ""
+
+#: src/components/modal/index.tsx:79
+#, c-format
+msgid "%1$s"
+msgstr ""
+
+#: src/components/modal/index.tsx:84
+#, c-format
+msgid "Close"
+msgstr ""
+
+#: src/components/modal/index.tsx:124
+#, c-format
+msgid "Continue"
+msgstr ""
+
+#: src/components/modal/index.tsx:178
+#, c-format
+msgid "Clear"
+msgstr ""
+
+#: src/components/modal/index.tsx:190
+#, c-format
+msgid "Confirm"
+msgstr ""
+
+#: src/components/modal/index.tsx:296
+#, c-format
+msgid "is not the same as the current access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:299
+#, c-format
+msgid "cannot be empty"
+msgstr ""
+
+#: src/components/modal/index.tsx:301
+#, c-format
+msgid "cannot be the same as the old token"
+msgstr ""
+
+#: src/components/modal/index.tsx:305
+#, c-format
+msgid "is not the same"
+msgstr ""
+
+#: src/components/modal/index.tsx:315
+#, c-format
+msgid "You are updating the access token from instance with id %1$s"
+msgstr ""
+
+#: src/components/modal/index.tsx:331
+#, c-format
+msgid "Old access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:332
+#, c-format
+msgid "access token currently in use"
+msgstr ""
+
+#: src/components/modal/index.tsx:338
+#, c-format
+msgid "New access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:339
+#, c-format
+msgid "next access token to be used"
+msgstr ""
+
+#: src/components/modal/index.tsx:344
+#, c-format
+msgid "Repeat access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:345
+#, c-format
+msgid "confirm the same access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:350
+#, c-format
+msgid "Clearing the access token will mean public access to the instance"
+msgstr ""
+
+#: src/components/modal/index.tsx:377
+#, c-format
+msgid "cannot be the same as the old access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:394
+#, c-format
+msgid "You are setting the access token for the new instance"
+msgstr ""
+
+#: src/components/modal/index.tsx:420
+#, c-format
+msgid ""
+"With external authorization method no check will be done by the merchant "
+"backend"
+msgstr ""
+
+#: src/components/modal/index.tsx:436
+#, c-format
+msgid "Set external authorization"
+msgstr ""
+
+#: src/components/modal/index.tsx:448
+#, c-format
+msgid "Set access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:470
+#, c-format
+msgid "Operation in progress..."
+msgstr ""
+
+#: src/components/modal/index.tsx:479
+#, c-format
+msgid "The operation will be automatically canceled after %1$s seconds"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:80
+#, c-format
+msgid "Instances"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:93
+#, c-format
+msgid "Delete"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:99
+#, c-format
+msgid "add new instance"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:178
+#, c-format
+msgid "ID"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:181
+#, c-format
+msgid "Name"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:220
+#, c-format
+msgid "Edit"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:237
+#, c-format
+msgid "Purge"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:261
+#, c-format
+msgid "There is no instances yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:68
+#, c-format
+msgid "Only show active instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:71
+#, c-format
+msgid "Active"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:78
+#, c-format
+msgid "Only show deleted instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:81
+#, c-format
+msgid "Deleted"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:88
+#, c-format
+msgid "Show all instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:91
+#, c-format
+msgid "All"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:101
+#, c-format
+msgid "Instance \"%1$s\" (ID: %2$s) has been deleted"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:106
+#, c-format
+msgid "Failed to delete instance"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:124
+#, c-format
+msgid "Instance '%1$s' (ID: %2$s) has been disabled"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:129
+#, c-format
+msgid "Failed to purge instance"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:41
+#, c-format
+msgid "Pending KYC verification"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:66
+#, c-format
+msgid "Timed out"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:103
+#, c-format
+msgid "Exchange"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:106
+#, c-format
+msgid "Target account"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:109
+#, c-format
+msgid "KYC URL"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:144
+#, c-format
+msgid "Code"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:147
+#, c-format
+msgid "Http Status"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:177
+#, c-format
+msgid "No pending kyc verification!"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:123
+#, c-format
+msgid "change value to unknown date"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:124
+#, c-format
+msgid "change value to empty"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:131
+#, c-format
+msgid "clear"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:136
+#, c-format
+msgid "change value to never"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:141
+#, c-format
+msgid "never"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:29
+#, c-format
+msgid "Country"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:33
+#, c-format
+msgid "Address"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:39
+#, c-format
+msgid "Building number"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:41
+#, c-format
+msgid "Building name"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:42
+#, c-format
+msgid "Street"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:43
+#, c-format
+msgid "Post code"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:44
+#, c-format
+msgid "Town location"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:45
+#, c-format
+msgid "Town"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:46
+#, c-format
+msgid "District"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:49
+#, c-format
+msgid "Country subdivision"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:66
+#, c-format
+msgid "Product id"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:69
+#, c-format
+msgid "Description"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:94
+#, c-format
+msgid "Product"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:95
+#, c-format
+msgid "search products by it's description or id"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:151
+#, c-format
+msgid "no products found with that description"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:56
+#, c-format
+msgid "You must enter a valid product identifier."
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:64
+#, c-format
+msgid "Quantity must be greater than 0!"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:76
+#, c-format
+msgid ""
+"This quantity exceeds remaining stock. Currently, only %1$s units remain "
+"unreserved in stock."
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:109
+#, c-format
+msgid "Quantity"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:110
+#, c-format
+msgid "how many products will be added"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:117
+#, c-format
+msgid "Add from inventory"
+msgstr ""
+
+#: src/components/form/InputImage.tsx:105
+#, c-format
+msgid "Image should be smaller than 1 MB"
+msgstr ""
+
+#: src/components/form/InputImage.tsx:110
+#, c-format
+msgid "Add"
+msgstr ""
+
+#: src/components/form/InputImage.tsx:115
+#, c-format
+msgid "Remove"
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:113
+#, c-format
+msgid "No taxes configured for this product."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:119
+#, c-format
+msgid "Amount"
+msgstr "Importo"
+
+#: src/components/form/InputTaxes.tsx:120
+#, c-format
+msgid ""
+"Taxes can be in currencies that differ from the main currency used by the "
+"merchant."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:122
+#, c-format
+msgid ""
+"Enter currency and value separated with a colon, e.g. &quot;USD:2.3&quot;."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:131
+#, c-format
+msgid "Legal name of the tax, e.g. VAT or import duties."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:137
+#, c-format
+msgid "add tax to the tax list"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:72
+#, c-format
+msgid "describe and add a product that is not in the inventory list"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:75
+#, c-format
+msgid "Add custom product"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:86
+#, c-format
+msgid "Complete information of the product"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:185
+#, c-format
+msgid "Image"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:186
+#, c-format
+msgid "photo of the product"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:192
+#, c-format
+msgid "full product description"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:196
+#, c-format
+msgid "Unit"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:197
+#, c-format
+msgid "name of the product unit"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:201
+#, c-format
+msgid "Price"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:202
+#, c-format
+msgid "amount in the current currency"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:211
+#, c-format
+msgid "Taxes"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:38
+#, c-format
+msgid "image"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:41
+#, c-format
+msgid "description"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:44
+#, c-format
+msgid "quantity"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:47
+#, c-format
+msgid "unit price"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:50
+#, c-format
+msgid "total price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:153
+#, c-format
+msgid "required"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:157
+#, c-format
+msgid "not valid"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:159
+#, c-format
+msgid "must be greater than 0"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:164
+#, c-format
+msgid "not a valid json"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:170
+#, c-format
+msgid "should be in the future"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:173
+#, c-format
+msgid "refund deadline cannot be before pay deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:179
+#, c-format
+msgid "wire transfer deadline cannot be before refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:190
+#, c-format
+msgid "wire transfer deadline cannot be before pay deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:197
+#, c-format
+msgid "should have a refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:202
+#, c-format
+msgid "auto refund cannot be after refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:360
+#, c-format
+msgid "Manage products in order"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:369
+#, c-format
+msgid "Manage list of products in the order."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:391
+#, c-format
+msgid "Remove this product from the order."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:415
+#, c-format
+msgid "Total price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:417
+#, c-format
+msgid "total product price added up"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:430
+#, c-format
+msgid "Amount to be paid by the customer"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:436
+#, c-format
+msgid "Order price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:437
+#, c-format
+msgid "final order price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:444
+#, c-format
+msgid "Summary"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:445
+#, c-format
+msgid "Title of the order to be shown to the customer"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:450
+#, c-format
+msgid "Shipping and Fulfillment"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:455
+#, c-format
+msgid "Delivery date"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:456
+#, c-format
+msgid "Deadline for physical delivery assured by the merchant."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:461
+#, c-format
+msgid "Location"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:462
+#, c-format
+msgid "address where the products will be delivered"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:469
+#, c-format
+msgid "Fulfillment URL"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:470
+#, c-format
+msgid "URL to which the user will be redirected after successful payment."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:476
+#, c-format
+msgid "Taler payment options"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:477
+#, c-format
+msgid "Override default Taler payment settings for this order"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:481
+#, c-format
+msgid "Payment deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:482
+#, c-format
+msgid ""
+"Deadline for the customer to pay for the offer before it expires. Inventory "
+"products will be reserved until this deadline."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:486
+#, c-format
+msgid "Refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:487
+#, c-format
+msgid "Time until which the order can be refunded by the merchant."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:491
+#, c-format
+msgid "Wire transfer deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:492
+#, c-format
+msgid "Deadline for the exchange to make the wire transfer."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:496
+#, c-format
+msgid "Auto-refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:497
+#, c-format
+msgid ""
+"Time until which the wallet will automatically check for refunds without "
+"user interaction."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:502
+#, c-format
+msgid "Maximum deposit fee"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:503
+#, c-format
+msgid ""
+"Maximum deposit fees the merchant is willing to cover for this order. Higher "
+"deposit fees must be covered in full by the consumer."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:507
+#, c-format
+msgid "Maximum wire fee"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:508
+#, c-format
+msgid ""
+"Maximum aggregate wire fees the merchant is willing to cover for this order. "
+"Wire fees exceeding this amount are to be covered by the customers."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:512
+#, c-format
+msgid "Wire fee amortization"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:513
+#, c-format
+msgid ""
+"Factor by which wire fees exceeding the above threshold are divided to "
+"determine the share of excess wire fees to be paid explicitly by the "
+"consumer."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:517
+#, c-format
+msgid "Create token"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:518
+#, c-format
+msgid ""
+"Uncheck this option if the merchant backend generated an order ID with "
+"enough entropy to prevent adversarial claims."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:522
+#, c-format
+msgid "Minimum age required"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:523
+#, c-format
+msgid ""
+"Any value greater than 0 will limit the coins able be used to pay this "
+"contract. If empty the age restriction will be defined by the products"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:526
+#, c-format
+msgid "Min age defined by the producs is %1$s"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:534
+#, c-format
+msgid "Additional information"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:535
+#, c-format
+msgid "Custom information to be included in the contract for this order."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:541
+#, c-format
+msgid "You must enter a value in JavaScript Object Notation (JSON)."
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:55
+#, c-format
+msgid "days"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:65
+#, c-format
+msgid "hours"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:76
+#, c-format
+msgid "minutes"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:87
+#, c-format
+msgid "seconds"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:53
+#, c-format
+msgid "forever"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:62
+#, c-format
+msgid "%1$sM"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:64
+#, c-format
+msgid "%1$sY"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:66
+#, c-format
+msgid "%1$sd"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:68
+#, c-format
+msgid "%1$sh"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:70
+#, c-format
+msgid "%1$smin"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:72
+#, c-format
+msgid "%1$ssec"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:75
+#, c-format
+msgid "Orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:81
+#, c-format
+msgid "create order"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:147
+#, c-format
+msgid "load newer orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:154
+#, c-format
+msgid "Date"
+msgstr "Data"
+
+#: src/paths/instance/orders/list/Table.tsx:200
+#, c-format
+msgid "Refund"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:209
+#, c-format
+msgid "copy url"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:225
+#, c-format
+msgid "load older orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:242
+#, c-format
+msgid "No orders have been found matching your query!"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:288
+#, c-format
+msgid "duplicated"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:299
+#, c-format
+msgid "invalid format"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:301
+#, c-format
+msgid "this value exceed the refundable amount"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:346
+#, c-format
+msgid "date"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:349
+#, c-format
+msgid "amount"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:352
+#, c-format
+msgid "reason"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:389
+#, c-format
+msgid "amount to be refunded"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:391
+#, c-format
+msgid "Max refundable:"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:396
+#, c-format
+msgid "Reason"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:397
+#, c-format
+msgid "Choose one..."
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:399
+#, c-format
+msgid "requested by the customer"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:400
+#, c-format
+msgid "other"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:403
+#, c-format
+msgid "why this order is being refunded"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:409
+#, c-format
+msgid "more information to give context"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:62
+#, c-format
+msgid "Contract Terms"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:68
+#, c-format
+msgid "human-readable description of the whole purchase"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:74
+#, c-format
+msgid "total price for the transaction"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:81
+#, c-format
+msgid "URL for this purchase"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:87
+#, c-format
+msgid "Max fee"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:88
+#, c-format
+msgid "maximum total deposit fee accepted by the merchant for this contract"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:93
+#, c-format
+msgid "Max wire fee"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:94
+#, c-format
+msgid "maximum wire fee accepted by the merchant"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:100
+#, c-format
+msgid ""
+"over how many customer transactions does the merchant expect to amortize "
+"wire fees on average"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:105
+#, c-format
+msgid "Created at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:106
+#, c-format
+msgid "time when this contract was generated"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:112
+#, c-format
+msgid "after this deadline has passed no refunds will be accepted"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:118
+#, c-format
+msgid ""
+"after this deadline, the merchant won't accept payments for the contract"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:124
+#, c-format
+msgid "transfer deadline for the exchange"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:130
+#, c-format
+msgid "time indicating when the order should be delivered"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:136
+#, c-format
+msgid "where the order will be delivered"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:144
+#, c-format
+msgid "Auto-refund delay"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:145
+#, c-format
+msgid ""
+"how long the wallet should try to get an automatic refund for the purchase"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:150
+#, c-format
+msgid "Extra info"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:151
+#, c-format
+msgid "extra data that is only interpreted by the merchant frontend"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:219
+#, c-format
+msgid "Order"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:221
+#, c-format
+msgid "claimed"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:247
+#, c-format
+msgid "claimed at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:265
+#, c-format
+msgid "Timeline"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:271
+#, c-format
+msgid "Payment details"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:291
+#, c-format
+msgid "Order status"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:301
+#, c-format
+msgid "Product list"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:451
+#, c-format
+msgid "paid"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:455
+#, c-format
+msgid "wired"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:460
+#, c-format
+msgid "refunded"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:480
+#, c-format
+msgid "refund order"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:481
+#, c-format
+msgid "not refundable"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:489
+#, c-format
+msgid "refund"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:553
+#, c-format
+msgid "Refunded amount"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:560
+#, c-format
+msgid "Refund taken"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:570
+#, c-format
+msgid "Status URL"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:583
+#, c-format
+msgid "Refund URI"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:636
+#, c-format
+msgid "unpaid"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:654
+#, c-format
+msgid "pay at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:666
+#, c-format
+msgid "created at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:707
+#, c-format
+msgid "Order status URL"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:711
+#, c-format
+msgid "Payment URI"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:740
+#, c-format
+msgid ""
+"Unknown order status. This is an error, please contact the administrator."
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:767
+#, c-format
+msgid "Back"
+msgstr "Indietro"
+
+#: src/paths/instance/orders/details/index.tsx:79
+#, c-format
+msgid "refund created successfully"
+msgstr ""
+
+#: src/paths/instance/orders/details/index.tsx:85
+#, c-format
+msgid "could not create the refund"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:78
+#, c-format
+msgid "select date to show nearby orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:94
+#, c-format
+msgid "order id"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:100
+#, c-format
+msgid "jump to order with the given order ID"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:122
+#, c-format
+msgid "remove all filters"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:132
+#, c-format
+msgid "only show paid orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:135
+#, c-format
+msgid "Paid"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:142
+#, c-format
+msgid "only show orders with refunds"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:145
+#, c-format
+msgid "Refunded"
+msgstr "Rimborsato"
+
+#: src/paths/instance/orders/list/ListPage.tsx:152
+#, c-format
+msgid ""
+"only show orders where customers paid, but wire payments from payment "
+"provider are still pending"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:155
+#, c-format
+msgid "Not wired"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:170
+#, c-format
+msgid "clear date filter"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:184
+#, c-format
+msgid "date (YYYY/MM/DD)"
+msgstr ""
+
+#: src/paths/instance/orders/list/index.tsx:103
+#, c-format
+msgid "Enter an order id"
+msgstr ""
+
+#: src/paths/instance/orders/list/index.tsx:111
+#, c-format
+msgid "order not found"
+msgstr ""
+
+#: src/paths/instance/orders/list/index.tsx:178
+#, c-format
+msgid "could not get the order to refund"
+msgstr ""
+
+#: src/components/exception/AsyncButton.tsx:43
+#, c-format
+msgid "Loading..."
+msgstr ""
+
+#: src/components/form/InputStock.tsx:99
+#, c-format
+msgid ""
+"click here to configure the stock of the product, leave it as is and the "
+"backend will not control stock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:109
+#, c-format
+msgid "Manage stock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:115
+#, c-format
+msgid "this product has been configured without stock control"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:119
+#, c-format
+msgid "Infinite"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:136
+#, c-format
+msgid "lost cannot be greater than current and incoming (max %1$s)"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:176
+#, c-format
+msgid "Incoming"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:177
+#, c-format
+msgid "Lost"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:192
+#, c-format
+msgid "Current"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:196
+#, c-format
+msgid "remove stock control for this product"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:202
+#, c-format
+msgid "without stock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:211
+#, c-format
+msgid "Next restock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:217
+#, c-format
+msgid "Delivery address"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:133
+#, c-format
+msgid "product identification to use in URLs (for internal use only)"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:139
+#, c-format
+msgid "illustration of the product for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:145
+#, c-format
+msgid "product description for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:149
+#, c-format
+msgid "Age restricted"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:150
+#, c-format
+msgid "is this product restricted for customer below certain age?"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:155
+#, c-format
+msgid ""
+"unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 "
+"items, 5 meters) for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:160
+#, c-format
+msgid ""
+"sale price for customers, including taxes, for above units of the product"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:164
+#, c-format
+msgid "Stock"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:166
+#, c-format
+msgid ""
+"product inventory for products with finite supply (for internal use only)"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:171
+#, c-format
+msgid "taxes included in the product price, exposed to customers"
+msgstr ""
+
+#: src/paths/instance/products/create/CreatePage.tsx:66
+#, c-format
+msgid "Need to complete marked fields"
+msgstr ""
+
+#: src/paths/instance/products/create/index.tsx:51
+#, c-format
+msgid "could not create product"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:68
+#, c-format
+msgid "Products"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:73
+#, c-format
+msgid "add product to inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:137
+#, c-format
+msgid "Sell"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:143
+#, c-format
+msgid "Profit"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:149
+#, c-format
+msgid "Sold"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:210
+#, c-format
+msgid "free"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:248
+#, c-format
+msgid "go to product update page"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:255
+#, c-format
+msgid "Update"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:260
+#, c-format
+msgid "remove this product from the database"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:331
+#, c-format
+msgid "update the product with new price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:341
+#, c-format
+msgid "update product with new price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:399
+#, c-format
+msgid "add more elements to the inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:404
+#, c-format
+msgid "report elements lost in the inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:409
+#, c-format
+msgid "new price for the product"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:421
+#, c-format
+msgid "the are value with errors"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:422
+#, c-format
+msgid "update product with new stock and price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:463
+#, c-format
+msgid "There is no products yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:86
+#, c-format
+msgid "product updated successfully"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:92
+#, c-format
+msgid "could not update the product"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:103
+#, c-format
+msgid "product delete successfully"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:109
+#, c-format
+msgid "could not delete the product"
+msgstr ""
+
+#: src/paths/instance/products/update/UpdatePage.tsx:56
+#, c-format
+msgid "Product id:"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:95
+#, c-format
+msgid ""
+"To complete the setup of the reserve, you must now initiate a wire transfer "
+"using the given wire transfer subject and crediting the specified amount to "
+"the indicated account of the exchange."
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:102
+#, c-format
+msgid "If your system supports RFC 8905, you can do this by opening this URI:"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:83
+#, c-format
+msgid "it should be greater than 0"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:88
+#, c-format
+msgid "must be a valid URL"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:107
+#, c-format
+msgid "Initial balance"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:108
+#, c-format
+msgid "balance prior to deposit"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:112
+#, c-format
+msgid "Exchange URL"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:113
+#, c-format
+msgid "URL of exchange"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:148
+#, c-format
+msgid "Next"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:186
+#, c-format
+msgid "Wire method"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:187
+#, c-format
+msgid "method to use for wire transfer"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:189
+#, c-format
+msgid "Select one wire method"
+msgstr ""
+
+#: src/paths/instance/reserves/create/index.tsx:62
+#, c-format
+msgid "could not create reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:77
+#, c-format
+msgid "Valid until"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:82
+#, c-format
+msgid "Created balance"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:99
+#, c-format
+msgid "Exchange balance"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:104
+#, c-format
+msgid "Picked up"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:109
+#, c-format
+msgid "Committed"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:116
+#, c-format
+msgid "Account address"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:119
+#, c-format
+msgid "Subject"
+msgstr "Soggetto"
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:130
+#, c-format
+msgid "Tips"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:193
+#, c-format
+msgid "No tips has been authorized from this reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:213
+#, c-format
+msgid "Authorized"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:222
+#, c-format
+msgid "Expiration"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:108
+#, c-format
+msgid "amount of tip"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:112
+#, c-format
+msgid "Justification"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:114
+#, c-format
+msgid "reason for the tip"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:118
+#, c-format
+msgid "URL after tip"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:119
+#, c-format
+msgid "URL to visit after tip payment"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:65
+#, c-format
+msgid "Reserves not yet funded"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:89
+#, c-format
+msgid "Reserves ready"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:95
+#, c-format
+msgid "add new reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:143
+#, c-format
+msgid "Expires at"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:146
+#, c-format
+msgid "Initial"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:202
+#, c-format
+msgid "delete selected reserve from the database"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:210
+#, c-format
+msgid "authorize new tip from selected reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:237
+#, c-format
+msgid ""
+"There is no ready reserves yet, add more pressing the + sign or fund them"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:264
+#, c-format
+msgid "Expected Balance"
+msgstr ""
+
+#: src/paths/instance/reserves/list/index.tsx:110
+#, c-format
+msgid "could not create the tip"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:77
+#, c-format
+msgid "should not be empty"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:93
+#, c-format
+msgid "should be greater that 0"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:96
+#, c-format
+msgid "can't be empty"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:100
+#, c-format
+msgid "to short"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:108
+#, c-format
+msgid "just letters and numbers from 2 to 7"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:110
+#, c-format
+msgid "size of the key should be 32"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:137
+#, c-format
+msgid "Identifier"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:138
+#, c-format
+msgid "Name of the template in URLs."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:144
+#, c-format
+msgid "Describe what this template stands for"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:149
+#, c-format
+msgid "Fixed summary"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:150
+#, c-format
+msgid "If specified, this template will create order with the same summary"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:154
+#, c-format
+msgid "Fixed price"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:155
+#, c-format
+msgid "If specified, this template will create order with the same price"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:159
+#, c-format
+msgid "Minimum age"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:161
+#, c-format
+msgid "Is this contract restricted to some age?"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:165
+#, c-format
+msgid "Payment timeout"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:167
+#, c-format
+msgid ""
+"How much time has the customer to complete the payment once the order was "
+"created."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:171
+#, c-format
+msgid "Verification algorithm"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:172
+#, c-format
+msgid "Algorithm to use to verify transaction in offline mode"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:180
+#, c-format
+msgid "Point-of-sale key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:182
+#, c-format
+msgid "Useful to validate the purchase"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:196
+#, c-format
+msgid "generate random secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:203
+#, c-format
+msgid "random"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:208
+#, c-format
+msgid "show secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:209
+#, c-format
+msgid "hide secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:216
+#, c-format
+msgid "hide"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:218
+#, c-format
+msgid "show"
+msgstr ""
+
+#: src/paths/instance/templates/create/index.tsx:52
+#, c-format
+msgid "could not inform template"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:54
+#, c-format
+msgid "Amount is required"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:58
+#, c-format
+msgid "Order summary is required"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:86
+#, c-format
+msgid "New order for template"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:108
+#, c-format
+msgid "Amount of the order"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:113
+#, c-format
+msgid "Order summary"
+msgstr ""
+
+#: src/paths/instance/templates/use/index.tsx:92
+#, c-format
+msgid "could not create order from template"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:131
+#, c-format
+msgid ""
+"Here you can specify a default value for fields that are not fixed. Default "
+"values can be edited by the customer before the payment."
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:148
+#, c-format
+msgid "Fixed amount"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:149
+#, c-format
+msgid "Default amount"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:161
+#, c-format
+msgid "Default summary"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:177
+#, c-format
+msgid "Print"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:184
+#, c-format
+msgid "Setup TOTP"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:65
+#, c-format
+msgid "Templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:70
+#, c-format
+msgid "add new templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:142
+#, c-format
+msgid "load more templates before the first one"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:146
+#, c-format
+msgid "load newer templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:181
+#, c-format
+msgid "delete selected templates from the database"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:188
+#, c-format
+msgid "use template to create new order"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:195
+#, c-format
+msgid "create qr code for the template"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:210
+#, c-format
+msgid "load more templates after the last one"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:214
+#, c-format
+msgid "load older templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:231
+#, c-format
+msgid "There is no templates yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/templates/list/index.tsx:104
+#, c-format
+msgid "template delete successfully"
+msgstr ""
+
+#: src/paths/instance/templates/list/index.tsx:110
+#, c-format
+msgid "could not delete the template"
+msgstr ""
+
+#: src/paths/instance/templates/update/index.tsx:90
+#, c-format
+msgid "could not update template"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:57
+#, c-format
+msgid "should be one of '%1$s'"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:85
+#, c-format
+msgid "Webhook ID to use"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:89
+#, c-format
+msgid "Event"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:90
+#, c-format
+msgid "The event of the webhook: why the webhook is used"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:94
+#, c-format
+msgid "Method"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:95
+#, c-format
+msgid "Method used by the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:99
+#, c-format
+msgid "URL"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:100
+#, c-format
+msgid "URL of the webhook where the customer will be redirected"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:104
+#, c-format
+msgid "Header"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:106
+#, c-format
+msgid "Header template of the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:111
+#, c-format
+msgid "Body"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:112
+#, c-format
+msgid "Body template by the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:61
+#, c-format
+msgid "Webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:66
+#, c-format
+msgid "add new webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:137
+#, c-format
+msgid "load more webhooks before the first one"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:141
+#, c-format
+msgid "load newer webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:151
+#, c-format
+msgid "Event type"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:176
+#, c-format
+msgid "delete selected webhook from the database"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:198
+#, c-format
+msgid "load more webhooks after the last one"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:202
+#, c-format
+msgid "load older webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:219
+#, c-format
+msgid "There is no webhooks yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/index.tsx:94
+#, c-format
+msgid "webhook delete successfully"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/index.tsx:100
+#, c-format
+msgid "could not delete the webhook"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:63
+#, c-format
+msgid "check the id, does not look valid"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:65
+#, c-format
+msgid "should have 52 characters, current %1$s"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:72
+#, c-format
+msgid "URL doesn't have the right format"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:98
+#, c-format
+msgid "Credited bank account"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:100
+#, c-format
+msgid "Select one account"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:101
+#, c-format
+msgid "Bank account of the merchant where the payment was received"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:105
+#, c-format
+msgid "Wire transfer ID"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:107
+#, c-format
+msgid ""
+"unique identifier of the wire transfer used by the exchange, must be 52 "
+"characters long"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:112
+#, c-format
+msgid ""
+"Base URL of the exchange that made the transfer, should have been in the "
+"wire transfer subject"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:117
+#, c-format
+msgid "Amount credited"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:118
+#, c-format
+msgid "Actual amount that was wired to the merchant's bank account"
+msgstr ""
+
+#: src/paths/instance/transfers/create/index.tsx:58
+#, c-format
+msgid "could not inform transfer"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:61
+#, c-format
+msgid "Transfers"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:66
+#, c-format
+msgid "add new transfer"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:129
+#, c-format
+msgid "load more transfers before the first one"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:133
+#, c-format
+msgid "load newer transfers"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:143
+#, c-format
+msgid "Credit"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:152
+#, c-format
+msgid "Confirmed"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:155
+#, c-format
+msgid "Verified"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:158
+#, c-format
+msgid "Executed at"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:171
+#, c-format
+msgid "yes"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:171
+#, c-format
+msgid "no"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:181
+#, c-format
+msgid "unknown"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:187
+#, c-format
+msgid "delete selected transfer from the database"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:202
+#, c-format
+msgid "load more transfer after the last one"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:206
+#, c-format
+msgid "load older transfers"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:223
+#, c-format
+msgid "There is no transfer yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:79
+#, c-format
+msgid "filter by account address"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:100
+#, c-format
+msgid "only show wire transfers confirmed by the merchant"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:110
+#, c-format
+msgid "only show wire transfers claimed by the exchange"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:113
+#, c-format
+msgid "Unverified"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:69
+#, c-format
+msgid "is not valid"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:94
+#, c-format
+msgid "is not a number"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:96
+#, c-format
+msgid "must be 1 or greater"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:107
+#, c-format
+msgid "max 7 lines"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:178
+#, c-format
+msgid "change authorization configuration"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:217
+#, c-format
+msgid "Need to complete marked fields and choose authorization method"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:82
+#, c-format
+msgid "This is not a valid bitcoin address."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:95
+#, c-format
+msgid "This is not a valid Ethereum address."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:118
+#, c-format
+msgid "IBAN numbers usually have more that 4 digits"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:120
+#, c-format
+msgid "IBAN numbers usually have less that 34 digits"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:128
+#, c-format
+msgid "IBAN country code not found"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:153
+#, c-format
+msgid "IBAN number is not valid, checksum is wrong"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:248
+#, c-format
+msgid "Target type"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:249
+#, c-format
+msgid "Method to use for wire transfer"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:258
+#, c-format
+msgid "Routing"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:259
+#, c-format
+msgid "Routing number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:263
+#, c-format
+msgid "Account"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:264
+#, c-format
+msgid "Account number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:273
+#, c-format
+msgid "Business Identifier Code."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:282
+#, c-format
+msgid "Bank Account Number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:292
+#, c-format
+msgid "Unified Payment Interface."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:301
+#, c-format
+msgid "Bitcoin protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:310
+#, c-format
+msgid "Ethereum protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:319
+#, c-format
+msgid "Interledger protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:328
+#, c-format
+msgid "Host"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:329
+#, c-format
+msgid "Bank host."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:334
+#, c-format
+msgid "Bank account."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:343
+#, c-format
+msgid "Bank account owner's name."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:370
+#, c-format
+msgid "No accounts yet."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:52
+#, c-format
+msgid ""
+"Name of the instance in URLs. The 'default' instance is special in that it "
+"is used to administer other instances."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:58
+#, c-format
+msgid "Business name"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:59
+#, c-format
+msgid "Legal name of the business represented by this instance."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:64
+#, c-format
+msgid "Email"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:65
+#, c-format
+msgid "Contact email"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:70
+#, c-format
+msgid "Website URL"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:71
+#, c-format
+msgid "URL."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:76
+#, c-format
+msgid "Logo"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:77
+#, c-format
+msgid "Logo image."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:82
+#, c-format
+msgid "Bank account"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:83
+#, c-format
+msgid "URI specifying bank account for crediting revenue."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:88
+#, c-format
+msgid "Default max deposit fee"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:89
+#, c-format
+msgid ""
+"Maximum deposit fees this merchant is willing to pay per order by default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:94
+#, c-format
+msgid "Default max wire fee"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:95
+#, c-format
+msgid ""
+"Maximum wire fees this merchant is willing to pay per wire transfer by "
+"default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:100
+#, c-format
+msgid "Default wire fee amortization"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:101
+#, c-format
+msgid ""
+"Number of orders excess wire transfer fees will be divided by to compute per "
+"order surcharge."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:107
+#, c-format
+msgid "Physical location of the merchant."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:114
+#, c-format
+msgid "Jurisdiction"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:115
+#, c-format
+msgid "Jurisdiction for legal disputes with the merchant."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:122
+#, c-format
+msgid "Default payment delay"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:124
+#, c-format
+msgid ""
+"Time customers have to pay an order before the offer expires by default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:129
+#, c-format
+msgid "Default wire transfer delay"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:130
+#, c-format
+msgid ""
+"Maximum time an exchange is allowed to delay wiring funds to the merchant, "
+"enabling it to aggregate smaller payments into larger wire transfers and "
+"reducing wire fees."
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:164
+#, c-format
+msgid "Instance id"
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:173
+#, c-format
+msgid "Change the authorization method use for this instance."
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:182
+#, c-format
+msgid "Manage access token"
+msgstr ""
+
+#: src/paths/instance/update/index.tsx:112
+#, c-format
+msgid "Failed to create instance"
+msgstr ""
+
+#: src/components/exception/login.tsx:74
+#, c-format
+msgid "Login required"
+msgstr ""
+
+#: src/components/exception/login.tsx:80
+#, c-format
+msgid "Please enter your access token."
+msgstr ""
+
+#: src/components/exception/login.tsx:108
+#, c-format
+msgid "Access Token"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:171
+#, c-format
+msgid "The request to the backend take too long and was cancelled"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:172
+#, c-format
+msgid "Diagnostic from %1$s is \"%2$s\""
+msgstr ""
+
+#: src/InstanceRoutes.tsx:178
+#, c-format
+msgid "The backend reported a problem: HTTP status #%1$s"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:179
+#, c-format
+msgid "Diagnostic from %1$s is '%2$s'"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:196
+#, c-format
+msgid "Access denied"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:197
+#, c-format
+msgid "The access token provided is invalid."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:212
+#, c-format
+msgid "No 'default' instance configured yet."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:213
+#, c-format
+msgid "Create a 'default' instance to begin using the merchant backoffice."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:630
+#, c-format
+msgid "The access token provided is invalid"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:664
+#, c-format
+msgid "Hide for today"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:82
+#, c-format
+msgid "Instance"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:91
+#, c-format
+msgid "Settings"
+msgstr "Impostazioni"
+
+#: src/components/menu/SideBar.tsx:167
+#, c-format
+msgid "Connection"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:209
+#, c-format
+msgid "New"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:219
+#, c-format
+msgid "List"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:234
+#, c-format
+msgid "Log out"
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:71
+#, c-format
+msgid "Check your token is valid"
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:90
+#, c-format
+msgid "Couldn't access the server."
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:91
+#, c-format
+msgid "Could not infer instance id from url %1$s"
+msgstr ""
+
+#: src/Application.tsx:104
+#, c-format
+msgid "Server not found"
+msgstr ""
+
+#: src/Application.tsx:118
+#, c-format
+msgid "Server response with an error code"
+msgstr ""
+
+#: src/Application.tsx:120
+#, c-format
+msgid "Got message %1$s from %2$s"
+msgstr ""
+
+#: src/Application.tsx:131
+#, c-format
+msgid "Response from server is unreadable, http status: %1$s"
+msgstr ""
+
+#: src/Application.tsx:144
+#, c-format
+msgid "Unexpected Error"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:101
+#, c-format
+msgid "The value %1$s is invalid for a payment url"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:110
+#, c-format
+msgid "add element to the list"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:112
+#, c-format
+msgid "add"
+msgstr ""
+
+#: src/components/form/InputSecured.tsx:37
+#, c-format
+msgid "Deleting"
+msgstr ""
+
+#: src/components/form/InputSecured.tsx:41
+#, c-format
+msgid "Changing"
+msgstr ""
+
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:87
+#, c-format
+msgid "Order ID"
+msgstr ""
+
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:101
+#, c-format
+msgid "Payment URL"
+msgstr ""
diff --git a/packages/merchant-backend-ui/src/i18n/poheader b/packages/auditor-backoffice-ui/src/i18n/poheader
index ee3fcd7be..7ddcf49b8 100644
--- a/packages/merchant-backend-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 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/merchant-backend-ui/src/i18n/strings-prelude b/packages/auditor-backoffice-ui/src/i18n/strings-prelude
index cca13afad..6c68662de 100644
--- a/packages/merchant-backend-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 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/i18n/strings.ts b/packages/auditor-backoffice-ui/src/i18n/strings.ts
new file mode 100644
index 000000000..65dc41358
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/i18n/strings.ts
@@ -0,0 +1,9655 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/*eslint quote-props: ["error", "consistent"]*/
+export const strings: {[s: string]: any} = {};
+
+strings['de'] = {
+ "domain": "messages",
+ "locale_data": {
+ "messages": {
+ "": {
+ "domain": "messages",
+ "plural_forms": "nplurals=2; plural=(n != 1);",
+ "lang": ""
+ },
+ "Cancel": [
+ ""
+ ],
+ "%1$s": [
+ ""
+ ],
+ "Close": [
+ ""
+ ],
+ "Continue": [
+ ""
+ ],
+ "Clear": [
+ ""
+ ],
+ "Confirm": [
+ ""
+ ],
+ "is not the same as the current access token": [
+ ""
+ ],
+ "cannot be empty": [
+ ""
+ ],
+ "cannot be the same as the old token": [
+ ""
+ ],
+ "is not the same": [
+ ""
+ ],
+ "You are updating the access token from instance with id %1$s": [
+ ""
+ ],
+ "Old access token": [
+ ""
+ ],
+ "access token currently in use": [
+ ""
+ ],
+ "New access token": [
+ ""
+ ],
+ "next access token to be used": [
+ ""
+ ],
+ "Repeat access token": [
+ ""
+ ],
+ "confirm the same access token": [
+ ""
+ ],
+ "Clearing the access token will mean public access to the instance": [
+ ""
+ ],
+ "cannot be the same as the old access token": [
+ ""
+ ],
+ "You are setting the access token for the new instance": [
+ ""
+ ],
+ "With external authorization method no check will be done by the merchant backend": [
+ ""
+ ],
+ "Set external authorization": [
+ ""
+ ],
+ "Set access token": [
+ ""
+ ],
+ "Operation in progress...": [
+ ""
+ ],
+ "The operation will be automatically canceled after %1$s seconds": [
+ ""
+ ],
+ "Instances": [
+ ""
+ ],
+ "Delete": [
+ ""
+ ],
+ "add new instance": [
+ ""
+ ],
+ "ID": [
+ ""
+ ],
+ "Name": [
+ ""
+ ],
+ "Edit": [
+ ""
+ ],
+ "Purge": [
+ ""
+ ],
+ "There is no instances yet, add more pressing the + sign": [
+ ""
+ ],
+ "Only show active instances": [
+ ""
+ ],
+ "Active": [
+ ""
+ ],
+ "Only show deleted instances": [
+ ""
+ ],
+ "Deleted": [
+ ""
+ ],
+ "Show all instances": [
+ ""
+ ],
+ "All": [
+ ""
+ ],
+ "Instance \"%1$s\" (ID: %2$s) has been deleted": [
+ ""
+ ],
+ "Failed to delete instance": [
+ ""
+ ],
+ "Instance '%1$s' (ID: %2$s) has been disabled": [
+ ""
+ ],
+ "Failed to purge instance": [
+ ""
+ ],
+ "Pending KYC verification": [
+ ""
+ ],
+ "Timed out": [
+ ""
+ ],
+ "Exchange": [
+ ""
+ ],
+ "Target account": [
+ ""
+ ],
+ "KYC URL": [
+ ""
+ ],
+ "Code": [
+ ""
+ ],
+ "Http Status": [
+ ""
+ ],
+ "No pending kyc verification!": [
+ ""
+ ],
+ "change value to unknown date": [
+ ""
+ ],
+ "change value to empty": [
+ ""
+ ],
+ "clear": [
+ ""
+ ],
+ "change value to never": [
+ ""
+ ],
+ "never": [
+ ""
+ ],
+ "Country": [
+ ""
+ ],
+ "Address": [
+ ""
+ ],
+ "Building number": [
+ ""
+ ],
+ "Building name": [
+ ""
+ ],
+ "Street": [
+ ""
+ ],
+ "Post code": [
+ ""
+ ],
+ "Town location": [
+ ""
+ ],
+ "Town": [
+ ""
+ ],
+ "District": [
+ ""
+ ],
+ "Country subdivision": [
+ ""
+ ],
+ "Product id": [
+ ""
+ ],
+ "Description": [
+ ""
+ ],
+ "Product": [
+ ""
+ ],
+ "search products by it's description or id": [
+ ""
+ ],
+ "no products found with that description": [
+ ""
+ ],
+ "You must enter a valid product identifier.": [
+ ""
+ ],
+ "Quantity must be greater than 0!": [
+ ""
+ ],
+ "This quantity exceeds remaining stock. Currently, only %1$s units remain unreserved in stock.": [
+ ""
+ ],
+ "Quantity": [
+ ""
+ ],
+ "how many products will be added": [
+ ""
+ ],
+ "Add from inventory": [
+ ""
+ ],
+ "Image should be smaller than 1 MB": [
+ ""
+ ],
+ "Add": [
+ ""
+ ],
+ "Remove": [
+ ""
+ ],
+ "No taxes configured for this product.": [
+ ""
+ ],
+ "Amount": [
+ ""
+ ],
+ "Taxes can be in currencies that differ from the main currency used by the merchant.": [
+ ""
+ ],
+ "Enter currency and value separated with a colon, e.g. &quot;USD:2.3&quot;.": [
+ ""
+ ],
+ "Legal name of the tax, e.g. VAT or import duties.": [
+ ""
+ ],
+ "add tax to the tax list": [
+ ""
+ ],
+ "describe and add a product that is not in the inventory list": [
+ ""
+ ],
+ "Add custom product": [
+ ""
+ ],
+ "Complete information of the product": [
+ ""
+ ],
+ "Image": [
+ ""
+ ],
+ "photo of the product": [
+ ""
+ ],
+ "full product description": [
+ ""
+ ],
+ "Unit": [
+ ""
+ ],
+ "name of the product unit": [
+ ""
+ ],
+ "Price": [
+ ""
+ ],
+ "amount in the current currency": [
+ ""
+ ],
+ "Taxes": [
+ ""
+ ],
+ "image": [
+ ""
+ ],
+ "description": [
+ ""
+ ],
+ "quantity": [
+ ""
+ ],
+ "unit price": [
+ ""
+ ],
+ "total price": [
+ ""
+ ],
+ "required": [
+ ""
+ ],
+ "not valid": [
+ ""
+ ],
+ "must be greater than 0": [
+ ""
+ ],
+ "not a valid json": [
+ ""
+ ],
+ "should be in the future": [
+ ""
+ ],
+ "refund deadline cannot be before pay deadline": [
+ ""
+ ],
+ "wire transfer deadline cannot be before refund deadline": [
+ ""
+ ],
+ "wire transfer deadline cannot be before pay deadline": [
+ ""
+ ],
+ "should have a refund deadline": [
+ ""
+ ],
+ "auto refund cannot be after refund deadline": [
+ ""
+ ],
+ "Manage products in order": [
+ ""
+ ],
+ "Manage list of products in the order.": [
+ ""
+ ],
+ "Remove this product from the order.": [
+ ""
+ ],
+ "Total price": [
+ ""
+ ],
+ "total product price added up": [
+ ""
+ ],
+ "Amount to be paid by the customer": [
+ ""
+ ],
+ "Order price": [
+ ""
+ ],
+ "final order price": [
+ ""
+ ],
+ "Summary": [
+ ""
+ ],
+ "Title of the order to be shown to the customer": [
+ ""
+ ],
+ "Shipping and Fulfillment": [
+ ""
+ ],
+ "Delivery date": [
+ ""
+ ],
+ "Deadline for physical delivery assured by the merchant.": [
+ ""
+ ],
+ "Location": [
+ ""
+ ],
+ "address where the products will be delivered": [
+ ""
+ ],
+ "Fulfillment URL": [
+ ""
+ ],
+ "URL to which the user will be redirected after successful payment.": [
+ ""
+ ],
+ "Taler payment options": [
+ ""
+ ],
+ "Override default Taler payment settings for this order": [
+ ""
+ ],
+ "Payment deadline": [
+ ""
+ ],
+ "Deadline for the customer to pay for the offer before it expires. Inventory products will be reserved until this deadline.": [
+ ""
+ ],
+ "Refund deadline": [
+ ""
+ ],
+ "Time until which the order can be refunded by the merchant.": [
+ ""
+ ],
+ "Wire transfer deadline": [
+ ""
+ ],
+ "Deadline for the exchange to make the wire transfer.": [
+ ""
+ ],
+ "Auto-refund deadline": [
+ ""
+ ],
+ "Time until which the wallet will automatically check for refunds without user interaction.": [
+ ""
+ ],
+ "Maximum deposit fee": [
+ ""
+ ],
+ "Maximum deposit fees the merchant is willing to cover for this order. Higher deposit fees must be covered in full by the consumer.": [
+ ""
+ ],
+ "Maximum wire fee": [
+ ""
+ ],
+ "Maximum aggregate wire fees the merchant is willing to cover for this order. Wire fees exceeding this amount are to be covered by the customers.": [
+ ""
+ ],
+ "Wire fee amortization": [
+ ""
+ ],
+ "Factor by which wire fees exceeding the above threshold are divided to determine the share of excess wire fees to be paid explicitly by the consumer.": [
+ ""
+ ],
+ "Create token": [
+ ""
+ ],
+ "Uncheck this option if the merchant backend generated an order ID with enough entropy to prevent adversarial claims.": [
+ ""
+ ],
+ "Minimum age required": [
+ ""
+ ],
+ "Any value greater than 0 will limit the coins able be used to pay this contract. If empty the age restriction will be defined by the products": [
+ ""
+ ],
+ "Min age defined by the producs is %1$s": [
+ ""
+ ],
+ "Additional information": [
+ ""
+ ],
+ "Custom information to be included in the contract for this order.": [
+ ""
+ ],
+ "You must enter a value in JavaScript Object Notation (JSON).": [
+ ""
+ ],
+ "days": [
+ ""
+ ],
+ "hours": [
+ ""
+ ],
+ "minutes": [
+ ""
+ ],
+ "seconds": [
+ ""
+ ],
+ "forever": [
+ ""
+ ],
+ "%1$sM": [
+ ""
+ ],
+ "%1$sY": [
+ ""
+ ],
+ "%1$sd": [
+ ""
+ ],
+ "%1$sh": [
+ ""
+ ],
+ "%1$smin": [
+ ""
+ ],
+ "%1$ssec": [
+ ""
+ ],
+ "Orders": [
+ ""
+ ],
+ "create order": [
+ ""
+ ],
+ "load newer orders": [
+ ""
+ ],
+ "Date": [
+ ""
+ ],
+ "Refund": [
+ ""
+ ],
+ "copy url": [
+ ""
+ ],
+ "load older orders": [
+ ""
+ ],
+ "No orders have been found matching your query!": [
+ ""
+ ],
+ "duplicated": [
+ ""
+ ],
+ "invalid format": [
+ ""
+ ],
+ "this value exceed the refundable amount": [
+ ""
+ ],
+ "date": [
+ ""
+ ],
+ "amount": [
+ ""
+ ],
+ "reason": [
+ ""
+ ],
+ "amount to be refunded": [
+ ""
+ ],
+ "Max refundable:": [
+ ""
+ ],
+ "Reason": [
+ ""
+ ],
+ "Choose one...": [
+ ""
+ ],
+ "requested by the customer": [
+ ""
+ ],
+ "other": [
+ ""
+ ],
+ "why this order is being refunded": [
+ ""
+ ],
+ "more information to give context": [
+ ""
+ ],
+ "Contract Terms": [
+ ""
+ ],
+ "human-readable description of the whole purchase": [
+ ""
+ ],
+ "total price for the transaction": [
+ ""
+ ],
+ "URL for this purchase": [
+ ""
+ ],
+ "Max fee": [
+ ""
+ ],
+ "maximum total deposit fee accepted by the merchant for this contract": [
+ ""
+ ],
+ "Max wire fee": [
+ ""
+ ],
+ "maximum wire fee accepted by the merchant": [
+ ""
+ ],
+ "over how many customer transactions does the merchant expect to amortize wire fees on average": [
+ ""
+ ],
+ "Created at": [
+ ""
+ ],
+ "time when this contract was generated": [
+ ""
+ ],
+ "after this deadline has passed no refunds will be accepted": [
+ ""
+ ],
+ "after this deadline, the merchant won't accept payments for the contract": [
+ ""
+ ],
+ "transfer deadline for the exchange": [
+ ""
+ ],
+ "time indicating when the order should be delivered": [
+ ""
+ ],
+ "where the order will be delivered": [
+ ""
+ ],
+ "Auto-refund delay": [
+ ""
+ ],
+ "how long the wallet should try to get an automatic refund for the purchase": [
+ ""
+ ],
+ "Extra info": [
+ ""
+ ],
+ "extra data that is only interpreted by the merchant frontend": [
+ ""
+ ],
+ "Order": [
+ ""
+ ],
+ "claimed": [
+ ""
+ ],
+ "claimed at": [
+ ""
+ ],
+ "Timeline": [
+ ""
+ ],
+ "Payment details": [
+ ""
+ ],
+ "Order status": [
+ ""
+ ],
+ "Product list": [
+ ""
+ ],
+ "paid": [
+ ""
+ ],
+ "wired": [
+ ""
+ ],
+ "refunded": [
+ ""
+ ],
+ "refund order": [
+ ""
+ ],
+ "not refundable": [
+ ""
+ ],
+ "refund": [
+ ""
+ ],
+ "Refunded amount": [
+ ""
+ ],
+ "Refund taken": [
+ ""
+ ],
+ "Status URL": [
+ ""
+ ],
+ "Refund URI": [
+ ""
+ ],
+ "unpaid": [
+ ""
+ ],
+ "pay at": [
+ ""
+ ],
+ "created at": [
+ ""
+ ],
+ "Order status URL": [
+ ""
+ ],
+ "Payment URI": [
+ ""
+ ],
+ "Unknown order status. This is an error, please contact the administrator.": [
+ ""
+ ],
+ "Back": [
+ ""
+ ],
+ "refund created successfully": [
+ ""
+ ],
+ "could not create the refund": [
+ ""
+ ],
+ "select date to show nearby orders": [
+ ""
+ ],
+ "order id": [
+ ""
+ ],
+ "jump to order with the given order ID": [
+ ""
+ ],
+ "remove all filters": [
+ ""
+ ],
+ "only show paid orders": [
+ ""
+ ],
+ "Paid": [
+ ""
+ ],
+ "only show orders with refunds": [
+ ""
+ ],
+ "Refunded": [
+ ""
+ ],
+ "only show orders where customers paid, but wire payments from payment provider are still pending": [
+ ""
+ ],
+ "Not wired": [
+ ""
+ ],
+ "clear date filter": [
+ ""
+ ],
+ "date (YYYY/MM/DD)": [
+ ""
+ ],
+ "Enter an order id": [
+ ""
+ ],
+ "order not found": [
+ ""
+ ],
+ "could not get the order to refund": [
+ ""
+ ],
+ "Loading...": [
+ ""
+ ],
+ "click here to configure the stock of the product, leave it as is and the backend will not control stock": [
+ ""
+ ],
+ "Manage stock": [
+ ""
+ ],
+ "this product has been configured without stock control": [
+ ""
+ ],
+ "Infinite": [
+ ""
+ ],
+ "lost cannot be greater than current and incoming (max %1$s)": [
+ ""
+ ],
+ "Incoming": [
+ ""
+ ],
+ "Lost": [
+ ""
+ ],
+ "Current": [
+ ""
+ ],
+ "remove stock control for this product": [
+ ""
+ ],
+ "without stock": [
+ ""
+ ],
+ "Next restock": [
+ ""
+ ],
+ "Delivery address": [
+ ""
+ ],
+ "product identification to use in URLs (for internal use only)": [
+ ""
+ ],
+ "illustration of the product for customers": [
+ ""
+ ],
+ "product description for customers": [
+ ""
+ ],
+ "Age restricted": [
+ ""
+ ],
+ "is this product restricted for customer below certain age?": [
+ ""
+ ],
+ "unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 items, 5 meters) for customers": [
+ ""
+ ],
+ "sale price for customers, including taxes, for above units of the product": [
+ ""
+ ],
+ "Stock": [
+ ""
+ ],
+ "product inventory for products with finite supply (for internal use only)": [
+ ""
+ ],
+ "taxes included in the product price, exposed to customers": [
+ ""
+ ],
+ "Need to complete marked fields": [
+ ""
+ ],
+ "could not create product": [
+ ""
+ ],
+ "Products": [
+ ""
+ ],
+ "add product to inventory": [
+ ""
+ ],
+ "Sell": [
+ ""
+ ],
+ "Profit": [
+ ""
+ ],
+ "Sold": [
+ ""
+ ],
+ "free": [
+ ""
+ ],
+ "go to product update page": [
+ ""
+ ],
+ "Update": [
+ ""
+ ],
+ "remove this product from the database": [
+ ""
+ ],
+ "update the product with new price": [
+ ""
+ ],
+ "update product with new price": [
+ ""
+ ],
+ "add more elements to the inventory": [
+ ""
+ ],
+ "report elements lost in the inventory": [
+ ""
+ ],
+ "new price for the product": [
+ ""
+ ],
+ "the are value with errors": [
+ ""
+ ],
+ "update product with new stock and price": [
+ ""
+ ],
+ "There is no products yet, add more pressing the + sign": [
+ ""
+ ],
+ "product updated successfully": [
+ ""
+ ],
+ "could not update the product": [
+ ""
+ ],
+ "product delete successfully": [
+ ""
+ ],
+ "could not delete the product": [
+ ""
+ ],
+ "Product id:": [
+ ""
+ ],
+ "To complete the setup of the reserve, you must now initiate a wire transfer using the given wire transfer subject and crediting the specified amount to the indicated account of the exchange.": [
+ ""
+ ],
+ "If your system supports RFC 8905, you can do this by opening this URI:": [
+ ""
+ ],
+ "it should be greater than 0": [
+ ""
+ ],
+ "must be a valid URL": [
+ ""
+ ],
+ "Initial balance": [
+ ""
+ ],
+ "balance prior to deposit": [
+ ""
+ ],
+ "Exchange URL": [
+ ""
+ ],
+ "URL of exchange": [
+ ""
+ ],
+ "Next": [
+ ""
+ ],
+ "Wire method": [
+ ""
+ ],
+ "method to use for wire transfer": [
+ ""
+ ],
+ "Select one wire method": [
+ ""
+ ],
+ "could not create reserve": [
+ ""
+ ],
+ "Valid until": [
+ ""
+ ],
+ "Created balance": [
+ ""
+ ],
+ "Exchange balance": [
+ ""
+ ],
+ "Picked up": [
+ ""
+ ],
+ "Committed": [
+ ""
+ ],
+ "Account address": [
+ ""
+ ],
+ "Subject": [
+ ""
+ ],
+ "Tips": [
+ ""
+ ],
+ "No tips has been authorized from this reserve": [
+ ""
+ ],
+ "Authorized": [
+ ""
+ ],
+ "Expiration": [
+ ""
+ ],
+ "amount of tip": [
+ ""
+ ],
+ "Justification": [
+ ""
+ ],
+ "reason for the tip": [
+ ""
+ ],
+ "URL after tip": [
+ ""
+ ],
+ "URL to visit after tip payment": [
+ ""
+ ],
+ "Reserves not yet funded": [
+ ""
+ ],
+ "Reserves ready": [
+ ""
+ ],
+ "add new reserve": [
+ ""
+ ],
+ "Expires at": [
+ ""
+ ],
+ "Initial": [
+ ""
+ ],
+ "delete selected reserve from the database": [
+ ""
+ ],
+ "authorize new tip from selected reserve": [
+ ""
+ ],
+ "There is no ready reserves yet, add more pressing the + sign or fund them": [
+ ""
+ ],
+ "Expected Balance": [
+ ""
+ ],
+ "could not create the tip": [
+ ""
+ ],
+ "should not be empty": [
+ ""
+ ],
+ "should be greater that 0": [
+ ""
+ ],
+ "can't be empty": [
+ ""
+ ],
+ "to short": [
+ ""
+ ],
+ "just letters and numbers from 2 to 7": [
+ ""
+ ],
+ "size of the key should be 32": [
+ ""
+ ],
+ "Identifier": [
+ ""
+ ],
+ "Name of the template in URLs.": [
+ ""
+ ],
+ "Describe what this template stands for": [
+ ""
+ ],
+ "Fixed summary": [
+ ""
+ ],
+ "If specified, this template will create order with the same summary": [
+ ""
+ ],
+ "Fixed price": [
+ ""
+ ],
+ "If specified, this template will create order with the same price": [
+ ""
+ ],
+ "Minimum age": [
+ ""
+ ],
+ "Is this contract restricted to some age?": [
+ ""
+ ],
+ "Payment timeout": [
+ ""
+ ],
+ "How much time has the customer to complete the payment once the order was created.": [
+ ""
+ ],
+ "Verification algorithm": [
+ ""
+ ],
+ "Algorithm to use to verify transaction in offline mode": [
+ ""
+ ],
+ "Point-of-sale key": [
+ ""
+ ],
+ "Useful to validate the purchase": [
+ ""
+ ],
+ "generate random secret key": [
+ ""
+ ],
+ "random": [
+ ""
+ ],
+ "show secret key": [
+ ""
+ ],
+ "hide secret key": [
+ ""
+ ],
+ "hide": [
+ ""
+ ],
+ "show": [
+ ""
+ ],
+ "could not inform template": [
+ ""
+ ],
+ "Amount is required": [
+ ""
+ ],
+ "Order summary is required": [
+ ""
+ ],
+ "New order for template": [
+ ""
+ ],
+ "Amount of the order": [
+ ""
+ ],
+ "Order summary": [
+ ""
+ ],
+ "could not create order from template": [
+ ""
+ ],
+ "Here you can specify a default value for fields that are not fixed. Default values can be edited by the customer before the payment.": [
+ ""
+ ],
+ "Fixed amount": [
+ ""
+ ],
+ "Default amount": [
+ ""
+ ],
+ "Default summary": [
+ ""
+ ],
+ "Print": [
+ ""
+ ],
+ "Setup TOTP": [
+ ""
+ ],
+ "Templates": [
+ ""
+ ],
+ "add new templates": [
+ ""
+ ],
+ "load more templates before the first one": [
+ ""
+ ],
+ "load newer templates": [
+ ""
+ ],
+ "delete selected templates from the database": [
+ ""
+ ],
+ "use template to create new order": [
+ ""
+ ],
+ "create qr code for the template": [
+ ""
+ ],
+ "load more templates after the last one": [
+ ""
+ ],
+ "load older templates": [
+ ""
+ ],
+ "There is no templates yet, add more pressing the + sign": [
+ ""
+ ],
+ "template delete successfully": [
+ ""
+ ],
+ "could not delete the template": [
+ ""
+ ],
+ "could not update template": [
+ ""
+ ],
+ "should be one of '%1$s'": [
+ ""
+ ],
+ "Webhook ID to use": [
+ ""
+ ],
+ "Event": [
+ ""
+ ],
+ "The event of the webhook: why the webhook is used": [
+ ""
+ ],
+ "Method": [
+ ""
+ ],
+ "Method used by the webhook": [
+ ""
+ ],
+ "URL": [
+ ""
+ ],
+ "URL of the webhook where the customer will be redirected": [
+ ""
+ ],
+ "Header": [
+ ""
+ ],
+ "Header template of the webhook": [
+ ""
+ ],
+ "Body": [
+ ""
+ ],
+ "Body template by the webhook": [
+ ""
+ ],
+ "Webhooks": [
+ ""
+ ],
+ "add new webhooks": [
+ ""
+ ],
+ "load more webhooks before the first one": [
+ ""
+ ],
+ "load newer webhooks": [
+ ""
+ ],
+ "Event type": [
+ ""
+ ],
+ "delete selected webhook from the database": [
+ ""
+ ],
+ "load more webhooks after the last one": [
+ ""
+ ],
+ "load older webhooks": [
+ ""
+ ],
+ "There is no webhooks yet, add more pressing the + sign": [
+ ""
+ ],
+ "webhook delete successfully": [
+ ""
+ ],
+ "could not delete the webhook": [
+ ""
+ ],
+ "check the id, does not look valid": [
+ ""
+ ],
+ "should have 52 characters, current %1$s": [
+ ""
+ ],
+ "URL doesn't have the right format": [
+ ""
+ ],
+ "Credited bank account": [
+ ""
+ ],
+ "Select one account": [
+ ""
+ ],
+ "Bank account of the merchant where the payment was received": [
+ ""
+ ],
+ "Wire transfer ID": [
+ ""
+ ],
+ "unique identifier of the wire transfer used by the exchange, must be 52 characters long": [
+ ""
+ ],
+ "Base URL of the exchange that made the transfer, should have been in the wire transfer subject": [
+ ""
+ ],
+ "Amount credited": [
+ ""
+ ],
+ "Actual amount that was wired to the merchant's bank account": [
+ ""
+ ],
+ "could not inform transfer": [
+ ""
+ ],
+ "Transfers": [
+ ""
+ ],
+ "add new transfer": [
+ ""
+ ],
+ "load more transfers before the first one": [
+ ""
+ ],
+ "load newer transfers": [
+ ""
+ ],
+ "Credit": [
+ ""
+ ],
+ "Confirmed": [
+ ""
+ ],
+ "Verified": [
+ ""
+ ],
+ "Executed at": [
+ ""
+ ],
+ "yes": [
+ ""
+ ],
+ "no": [
+ ""
+ ],
+ "unknown": [
+ ""
+ ],
+ "delete selected transfer from the database": [
+ ""
+ ],
+ "load more transfer after the last one": [
+ ""
+ ],
+ "load older transfers": [
+ ""
+ ],
+ "There is no transfer yet, add more pressing the + sign": [
+ ""
+ ],
+ "filter by account address": [
+ ""
+ ],
+ "only show wire transfers confirmed by the merchant": [
+ ""
+ ],
+ "only show wire transfers claimed by the exchange": [
+ ""
+ ],
+ "Unverified": [
+ ""
+ ],
+ "is not valid": [
+ ""
+ ],
+ "is not a number": [
+ ""
+ ],
+ "must be 1 or greater": [
+ ""
+ ],
+ "max 7 lines": [
+ ""
+ ],
+ "change authorization configuration": [
+ ""
+ ],
+ "Need to complete marked fields and choose authorization method": [
+ ""
+ ],
+ "This is not a valid bitcoin address.": [
+ ""
+ ],
+ "This is not a valid Ethereum address.": [
+ ""
+ ],
+ "IBAN numbers usually have more that 4 digits": [
+ ""
+ ],
+ "IBAN numbers usually have less that 34 digits": [
+ ""
+ ],
+ "IBAN country code not found": [
+ ""
+ ],
+ "IBAN number is not valid, checksum is wrong": [
+ ""
+ ],
+ "Target type": [
+ ""
+ ],
+ "Method to use for wire transfer": [
+ ""
+ ],
+ "Routing": [
+ ""
+ ],
+ "Routing number.": [
+ ""
+ ],
+ "Account": [
+ ""
+ ],
+ "Account number.": [
+ ""
+ ],
+ "Business Identifier Code.": [
+ ""
+ ],
+ "Bank Account Number.": [
+ ""
+ ],
+ "Unified Payment Interface.": [
+ ""
+ ],
+ "Bitcoin protocol.": [
+ ""
+ ],
+ "Ethereum protocol.": [
+ ""
+ ],
+ "Interledger protocol.": [
+ ""
+ ],
+ "Host": [
+ ""
+ ],
+ "Bank host.": [
+ ""
+ ],
+ "Bank account.": [
+ ""
+ ],
+ "Bank account owner's name.": [
+ ""
+ ],
+ "No accounts yet.": [
+ ""
+ ],
+ "Name of the instance in URLs. The 'default' instance is special in that it is used to administer other instances.": [
+ ""
+ ],
+ "Business name": [
+ ""
+ ],
+ "Legal name of the business represented by this instance.": [
+ ""
+ ],
+ "Email": [
+ ""
+ ],
+ "Contact email": [
+ ""
+ ],
+ "Website URL": [
+ ""
+ ],
+ "URL.": [
+ ""
+ ],
+ "Logo": [
+ ""
+ ],
+ "Logo image.": [
+ ""
+ ],
+ "Bank account": [
+ ""
+ ],
+ "URI specifying bank account for crediting revenue.": [
+ ""
+ ],
+ "Default max deposit fee": [
+ ""
+ ],
+ "Maximum deposit fees this merchant is willing to pay per order by default.": [
+ ""
+ ],
+ "Default max wire fee": [
+ ""
+ ],
+ "Maximum wire fees this merchant is willing to pay per wire transfer by default.": [
+ ""
+ ],
+ "Default wire fee amortization": [
+ ""
+ ],
+ "Number of orders excess wire transfer fees will be divided by to compute per order surcharge.": [
+ ""
+ ],
+ "Physical location of the merchant.": [
+ ""
+ ],
+ "Jurisdiction": [
+ ""
+ ],
+ "Jurisdiction for legal disputes with the merchant.": [
+ ""
+ ],
+ "Default payment delay": [
+ ""
+ ],
+ "Time customers have to pay an order before the offer expires by default.": [
+ ""
+ ],
+ "Default wire transfer delay": [
+ ""
+ ],
+ "Maximum time an exchange is allowed to delay wiring funds to the merchant, enabling it to aggregate smaller payments into larger wire transfers and reducing wire fees.": [
+ ""
+ ],
+ "Instance id": [
+ ""
+ ],
+ "Change the authorization method use for this instance.": [
+ ""
+ ],
+ "Manage access token": [
+ ""
+ ],
+ "Failed to create instance": [
+ ""
+ ],
+ "Login required": [
+ ""
+ ],
+ "Please enter your access token.": [
+ ""
+ ],
+ "Access Token": [
+ ""
+ ],
+ "The request to the backend take too long and was cancelled": [
+ ""
+ ],
+ "Diagnostic from %1$s is \"%2$s\"": [
+ ""
+ ],
+ "The backend reported a problem: HTTP status #%1$s": [
+ ""
+ ],
+ "Diagnostic from %1$s is '%2$s'": [
+ ""
+ ],
+ "Access denied": [
+ ""
+ ],
+ "The access token provided is invalid.": [
+ ""
+ ],
+ "No 'default' instance configured yet.": [
+ ""
+ ],
+ "Create a 'default' instance to begin using the merchant backoffice.": [
+ ""
+ ],
+ "The access token provided is invalid": [
+ ""
+ ],
+ "Hide for today": [
+ ""
+ ],
+ "Instance": [
+ ""
+ ],
+ "Settings": [
+ ""
+ ],
+ "Connection": [
+ ""
+ ],
+ "New": [
+ ""
+ ],
+ "List": [
+ ""
+ ],
+ "Log out": [
+ ""
+ ],
+ "Check your token is valid": [
+ ""
+ ],
+ "Couldn't access the server.": [
+ ""
+ ],
+ "Could not infer instance id from url %1$s": [
+ ""
+ ],
+ "Server not found": [
+ ""
+ ],
+ "Server response with an error code": [
+ ""
+ ],
+ "Got message %1$s from %2$s": [
+ ""
+ ],
+ "Response from server is unreadable, http status: %1$s": [
+ ""
+ ],
+ "Unexpected Error": [
+ ""
+ ],
+ "The value %1$s is invalid for a payment url": [
+ ""
+ ],
+ "add element to the list": [
+ ""
+ ],
+ "add": [
+ ""
+ ],
+ "Deleting": [
+ ""
+ ],
+ "Changing": [
+ ""
+ ],
+ "Order ID": [
+ ""
+ ],
+ "Payment URL": [
+ ""
+ ]
+ }
+ }
+};
+
+strings['en'] = {
+ "domain": "messages",
+ "locale_data": {
+ "messages": {
+ "": {
+ "domain": "messages",
+ "plural_forms": "nplurals=2; plural=(n != 1);",
+ "lang": ""
+ },
+ "Cancel": [
+ ""
+ ],
+ "%1$s": [
+ ""
+ ],
+ "Close": [
+ ""
+ ],
+ "Continue": [
+ ""
+ ],
+ "Clear": [
+ ""
+ ],
+ "Confirm": [
+ ""
+ ],
+ "is not the same as the current access token": [
+ ""
+ ],
+ "cannot be empty": [
+ ""
+ ],
+ "cannot be the same as the old token": [
+ ""
+ ],
+ "is not the same": [
+ ""
+ ],
+ "You are updating the access token from instance with id %1$s": [
+ ""
+ ],
+ "Old access token": [
+ ""
+ ],
+ "access token currently in use": [
+ ""
+ ],
+ "New access token": [
+ ""
+ ],
+ "next access token to be used": [
+ ""
+ ],
+ "Repeat access token": [
+ ""
+ ],
+ "confirm the same access token": [
+ ""
+ ],
+ "Clearing the access token will mean public access to the instance": [
+ ""
+ ],
+ "cannot be the same as the old access token": [
+ ""
+ ],
+ "You are setting the access token for the new instance": [
+ ""
+ ],
+ "With external authorization method no check will be done by the merchant backend": [
+ ""
+ ],
+ "Set external authorization": [
+ ""
+ ],
+ "Set access token": [
+ ""
+ ],
+ "Operation in progress...": [
+ ""
+ ],
+ "The operation will be automatically canceled after %1$s seconds": [
+ ""
+ ],
+ "Instances": [
+ ""
+ ],
+ "Delete": [
+ ""
+ ],
+ "add new instance": [
+ ""
+ ],
+ "ID": [
+ ""
+ ],
+ "Name": [
+ ""
+ ],
+ "Edit": [
+ ""
+ ],
+ "Purge": [
+ ""
+ ],
+ "There is no instances yet, add more pressing the + sign": [
+ ""
+ ],
+ "Only show active instances": [
+ ""
+ ],
+ "Active": [
+ ""
+ ],
+ "Only show deleted instances": [
+ ""
+ ],
+ "Deleted": [
+ ""
+ ],
+ "Show all instances": [
+ ""
+ ],
+ "All": [
+ ""
+ ],
+ "Instance \"%1$s\" (ID: %2$s) has been deleted": [
+ ""
+ ],
+ "Failed to delete instance": [
+ ""
+ ],
+ "Instance '%1$s' (ID: %2$s) has been disabled": [
+ ""
+ ],
+ "Failed to purge instance": [
+ ""
+ ],
+ "Pending KYC verification": [
+ ""
+ ],
+ "Timed out": [
+ ""
+ ],
+ "Exchange": [
+ ""
+ ],
+ "Target account": [
+ ""
+ ],
+ "KYC URL": [
+ ""
+ ],
+ "Code": [
+ ""
+ ],
+ "Http Status": [
+ ""
+ ],
+ "No pending kyc verification!": [
+ ""
+ ],
+ "change value to unknown date": [
+ ""
+ ],
+ "change value to empty": [
+ ""
+ ],
+ "clear": [
+ ""
+ ],
+ "change value to never": [
+ ""
+ ],
+ "never": [
+ ""
+ ],
+ "Country": [
+ ""
+ ],
+ "Address": [
+ ""
+ ],
+ "Building number": [
+ ""
+ ],
+ "Building name": [
+ ""
+ ],
+ "Street": [
+ ""
+ ],
+ "Post code": [
+ ""
+ ],
+ "Town location": [
+ ""
+ ],
+ "Town": [
+ ""
+ ],
+ "District": [
+ ""
+ ],
+ "Country subdivision": [
+ ""
+ ],
+ "Product id": [
+ ""
+ ],
+ "Description": [
+ ""
+ ],
+ "Product": [
+ ""
+ ],
+ "search products by it's description or id": [
+ ""
+ ],
+ "no products found with that description": [
+ ""
+ ],
+ "You must enter a valid product identifier.": [
+ ""
+ ],
+ "Quantity must be greater than 0!": [
+ ""
+ ],
+ "This quantity exceeds remaining stock. Currently, only %1$s units remain unreserved in stock.": [
+ ""
+ ],
+ "Quantity": [
+ ""
+ ],
+ "how many products will be added": [
+ ""
+ ],
+ "Add from inventory": [
+ ""
+ ],
+ "Image should be smaller than 1 MB": [
+ ""
+ ],
+ "Add": [
+ ""
+ ],
+ "Remove": [
+ ""
+ ],
+ "No taxes configured for this product.": [
+ ""
+ ],
+ "Amount": [
+ ""
+ ],
+ "Taxes can be in currencies that differ from the main currency used by the merchant.": [
+ ""
+ ],
+ "Enter currency and value separated with a colon, e.g. &quot;USD:2.3&quot;.": [
+ ""
+ ],
+ "Legal name of the tax, e.g. VAT or import duties.": [
+ ""
+ ],
+ "add tax to the tax list": [
+ ""
+ ],
+ "describe and add a product that is not in the inventory list": [
+ ""
+ ],
+ "Add custom product": [
+ ""
+ ],
+ "Complete information of the product": [
+ ""
+ ],
+ "Image": [
+ ""
+ ],
+ "photo of the product": [
+ ""
+ ],
+ "full product description": [
+ ""
+ ],
+ "Unit": [
+ ""
+ ],
+ "name of the product unit": [
+ ""
+ ],
+ "Price": [
+ ""
+ ],
+ "amount in the current currency": [
+ ""
+ ],
+ "Taxes": [
+ ""
+ ],
+ "image": [
+ ""
+ ],
+ "description": [
+ ""
+ ],
+ "quantity": [
+ ""
+ ],
+ "unit price": [
+ ""
+ ],
+ "total price": [
+ ""
+ ],
+ "required": [
+ ""
+ ],
+ "not valid": [
+ ""
+ ],
+ "must be greater than 0": [
+ ""
+ ],
+ "not a valid json": [
+ ""
+ ],
+ "should be in the future": [
+ ""
+ ],
+ "refund deadline cannot be before pay deadline": [
+ ""
+ ],
+ "wire transfer deadline cannot be before refund deadline": [
+ ""
+ ],
+ "wire transfer deadline cannot be before pay deadline": [
+ ""
+ ],
+ "should have a refund deadline": [
+ ""
+ ],
+ "auto refund cannot be after refund deadline": [
+ ""
+ ],
+ "Manage products in order": [
+ ""
+ ],
+ "Manage list of products in the order.": [
+ ""
+ ],
+ "Remove this product from the order.": [
+ ""
+ ],
+ "Total price": [
+ ""
+ ],
+ "total product price added up": [
+ ""
+ ],
+ "Amount to be paid by the customer": [
+ ""
+ ],
+ "Order price": [
+ ""
+ ],
+ "final order price": [
+ ""
+ ],
+ "Summary": [
+ ""
+ ],
+ "Title of the order to be shown to the customer": [
+ ""
+ ],
+ "Shipping and Fulfillment": [
+ ""
+ ],
+ "Delivery date": [
+ ""
+ ],
+ "Deadline for physical delivery assured by the merchant.": [
+ ""
+ ],
+ "Location": [
+ ""
+ ],
+ "address where the products will be delivered": [
+ ""
+ ],
+ "Fulfillment URL": [
+ ""
+ ],
+ "URL to which the user will be redirected after successful payment.": [
+ ""
+ ],
+ "Taler payment options": [
+ ""
+ ],
+ "Override default Taler payment settings for this order": [
+ ""
+ ],
+ "Payment deadline": [
+ ""
+ ],
+ "Deadline for the customer to pay for the offer before it expires. Inventory products will be reserved until this deadline.": [
+ ""
+ ],
+ "Refund deadline": [
+ ""
+ ],
+ "Time until which the order can be refunded by the merchant.": [
+ ""
+ ],
+ "Wire transfer deadline": [
+ ""
+ ],
+ "Deadline for the exchange to make the wire transfer.": [
+ ""
+ ],
+ "Auto-refund deadline": [
+ ""
+ ],
+ "Time until which the wallet will automatically check for refunds without user interaction.": [
+ ""
+ ],
+ "Maximum deposit fee": [
+ ""
+ ],
+ "Maximum deposit fees the merchant is willing to cover for this order. Higher deposit fees must be covered in full by the consumer.": [
+ ""
+ ],
+ "Maximum wire fee": [
+ ""
+ ],
+ "Maximum aggregate wire fees the merchant is willing to cover for this order. Wire fees exceeding this amount are to be covered by the customers.": [
+ ""
+ ],
+ "Wire fee amortization": [
+ ""
+ ],
+ "Factor by which wire fees exceeding the above threshold are divided to determine the share of excess wire fees to be paid explicitly by the consumer.": [
+ ""
+ ],
+ "Create token": [
+ ""
+ ],
+ "Uncheck this option if the merchant backend generated an order ID with enough entropy to prevent adversarial claims.": [
+ ""
+ ],
+ "Minimum age required": [
+ ""
+ ],
+ "Any value greater than 0 will limit the coins able be used to pay this contract. If empty the age restriction will be defined by the products": [
+ ""
+ ],
+ "Min age defined by the producs is %1$s": [
+ ""
+ ],
+ "Additional information": [
+ ""
+ ],
+ "Custom information to be included in the contract for this order.": [
+ ""
+ ],
+ "You must enter a value in JavaScript Object Notation (JSON).": [
+ ""
+ ],
+ "days": [
+ ""
+ ],
+ "hours": [
+ ""
+ ],
+ "minutes": [
+ ""
+ ],
+ "seconds": [
+ ""
+ ],
+ "forever": [
+ ""
+ ],
+ "%1$sM": [
+ ""
+ ],
+ "%1$sY": [
+ ""
+ ],
+ "%1$sd": [
+ ""
+ ],
+ "%1$sh": [
+ ""
+ ],
+ "%1$smin": [
+ ""
+ ],
+ "%1$ssec": [
+ ""
+ ],
+ "Orders": [
+ ""
+ ],
+ "create order": [
+ ""
+ ],
+ "load newer orders": [
+ ""
+ ],
+ "Date": [
+ ""
+ ],
+ "Refund": [
+ ""
+ ],
+ "copy url": [
+ ""
+ ],
+ "load older orders": [
+ ""
+ ],
+ "No orders have been found matching your query!": [
+ ""
+ ],
+ "duplicated": [
+ ""
+ ],
+ "invalid format": [
+ ""
+ ],
+ "this value exceed the refundable amount": [
+ ""
+ ],
+ "date": [
+ ""
+ ],
+ "amount": [
+ ""
+ ],
+ "reason": [
+ ""
+ ],
+ "amount to be refunded": [
+ ""
+ ],
+ "Max refundable:": [
+ ""
+ ],
+ "Reason": [
+ ""
+ ],
+ "Choose one...": [
+ ""
+ ],
+ "requested by the customer": [
+ ""
+ ],
+ "other": [
+ ""
+ ],
+ "why this order is being refunded": [
+ ""
+ ],
+ "more information to give context": [
+ ""
+ ],
+ "Contract Terms": [
+ ""
+ ],
+ "human-readable description of the whole purchase": [
+ ""
+ ],
+ "total price for the transaction": [
+ ""
+ ],
+ "URL for this purchase": [
+ ""
+ ],
+ "Max fee": [
+ ""
+ ],
+ "maximum total deposit fee accepted by the merchant for this contract": [
+ ""
+ ],
+ "Max wire fee": [
+ ""
+ ],
+ "maximum wire fee accepted by the merchant": [
+ ""
+ ],
+ "over how many customer transactions does the merchant expect to amortize wire fees on average": [
+ ""
+ ],
+ "Created at": [
+ ""
+ ],
+ "time when this contract was generated": [
+ ""
+ ],
+ "after this deadline has passed no refunds will be accepted": [
+ ""
+ ],
+ "after this deadline, the merchant won't accept payments for the contract": [
+ ""
+ ],
+ "transfer deadline for the exchange": [
+ ""
+ ],
+ "time indicating when the order should be delivered": [
+ ""
+ ],
+ "where the order will be delivered": [
+ ""
+ ],
+ "Auto-refund delay": [
+ ""
+ ],
+ "how long the wallet should try to get an automatic refund for the purchase": [
+ ""
+ ],
+ "Extra info": [
+ ""
+ ],
+ "extra data that is only interpreted by the merchant frontend": [
+ ""
+ ],
+ "Order": [
+ ""
+ ],
+ "claimed": [
+ ""
+ ],
+ "claimed at": [
+ ""
+ ],
+ "Timeline": [
+ ""
+ ],
+ "Payment details": [
+ ""
+ ],
+ "Order status": [
+ ""
+ ],
+ "Product list": [
+ ""
+ ],
+ "paid": [
+ ""
+ ],
+ "wired": [
+ ""
+ ],
+ "refunded": [
+ ""
+ ],
+ "refund order": [
+ ""
+ ],
+ "not refundable": [
+ ""
+ ],
+ "refund": [
+ ""
+ ],
+ "Refunded amount": [
+ ""
+ ],
+ "Refund taken": [
+ ""
+ ],
+ "Status URL": [
+ ""
+ ],
+ "Refund URI": [
+ ""
+ ],
+ "unpaid": [
+ ""
+ ],
+ "pay at": [
+ ""
+ ],
+ "created at": [
+ ""
+ ],
+ "Order status URL": [
+ ""
+ ],
+ "Payment URI": [
+ ""
+ ],
+ "Unknown order status. This is an error, please contact the administrator.": [
+ ""
+ ],
+ "Back": [
+ ""
+ ],
+ "refund created successfully": [
+ ""
+ ],
+ "could not create the refund": [
+ ""
+ ],
+ "select date to show nearby orders": [
+ ""
+ ],
+ "order id": [
+ ""
+ ],
+ "jump to order with the given order ID": [
+ ""
+ ],
+ "remove all filters": [
+ ""
+ ],
+ "only show paid orders": [
+ ""
+ ],
+ "Paid": [
+ ""
+ ],
+ "only show orders with refunds": [
+ ""
+ ],
+ "Refunded": [
+ ""
+ ],
+ "only show orders where customers paid, but wire payments from payment provider are still pending": [
+ ""
+ ],
+ "Not wired": [
+ ""
+ ],
+ "clear date filter": [
+ ""
+ ],
+ "date (YYYY/MM/DD)": [
+ ""
+ ],
+ "Enter an order id": [
+ ""
+ ],
+ "order not found": [
+ ""
+ ],
+ "could not get the order to refund": [
+ ""
+ ],
+ "Loading...": [
+ ""
+ ],
+ "click here to configure the stock of the product, leave it as is and the backend will not control stock": [
+ ""
+ ],
+ "Manage stock": [
+ ""
+ ],
+ "this product has been configured without stock control": [
+ ""
+ ],
+ "Infinite": [
+ ""
+ ],
+ "lost cannot be greater than current and incoming (max %1$s)": [
+ ""
+ ],
+ "Incoming": [
+ ""
+ ],
+ "Lost": [
+ ""
+ ],
+ "Current": [
+ ""
+ ],
+ "remove stock control for this product": [
+ ""
+ ],
+ "without stock": [
+ ""
+ ],
+ "Next restock": [
+ ""
+ ],
+ "Delivery address": [
+ ""
+ ],
+ "product identification to use in URLs (for internal use only)": [
+ ""
+ ],
+ "illustration of the product for customers": [
+ ""
+ ],
+ "product description for customers": [
+ ""
+ ],
+ "Age restricted": [
+ ""
+ ],
+ "is this product restricted for customer below certain age?": [
+ ""
+ ],
+ "unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 items, 5 meters) for customers": [
+ ""
+ ],
+ "sale price for customers, including taxes, for above units of the product": [
+ ""
+ ],
+ "Stock": [
+ ""
+ ],
+ "product inventory for products with finite supply (for internal use only)": [
+ ""
+ ],
+ "taxes included in the product price, exposed to customers": [
+ ""
+ ],
+ "Need to complete marked fields": [
+ ""
+ ],
+ "could not create product": [
+ ""
+ ],
+ "Products": [
+ ""
+ ],
+ "add product to inventory": [
+ ""
+ ],
+ "Sell": [
+ ""
+ ],
+ "Profit": [
+ ""
+ ],
+ "Sold": [
+ ""
+ ],
+ "free": [
+ ""
+ ],
+ "go to product update page": [
+ ""
+ ],
+ "Update": [
+ ""
+ ],
+ "remove this product from the database": [
+ ""
+ ],
+ "update the product with new price": [
+ ""
+ ],
+ "update product with new price": [
+ ""
+ ],
+ "add more elements to the inventory": [
+ ""
+ ],
+ "report elements lost in the inventory": [
+ ""
+ ],
+ "new price for the product": [
+ ""
+ ],
+ "the are value with errors": [
+ ""
+ ],
+ "update product with new stock and price": [
+ ""
+ ],
+ "There is no products yet, add more pressing the + sign": [
+ ""
+ ],
+ "product updated successfully": [
+ ""
+ ],
+ "could not update the product": [
+ ""
+ ],
+ "product delete successfully": [
+ ""
+ ],
+ "could not delete the product": [
+ ""
+ ],
+ "Product id:": [
+ ""
+ ],
+ "To complete the setup of the reserve, you must now initiate a wire transfer using the given wire transfer subject and crediting the specified amount to the indicated account of the exchange.": [
+ ""
+ ],
+ "If your system supports RFC 8905, you can do this by opening this URI:": [
+ ""
+ ],
+ "it should be greater than 0": [
+ ""
+ ],
+ "must be a valid URL": [
+ ""
+ ],
+ "Initial balance": [
+ ""
+ ],
+ "balance prior to deposit": [
+ ""
+ ],
+ "Exchange URL": [
+ ""
+ ],
+ "URL of exchange": [
+ ""
+ ],
+ "Next": [
+ ""
+ ],
+ "Wire method": [
+ ""
+ ],
+ "method to use for wire transfer": [
+ ""
+ ],
+ "Select one wire method": [
+ ""
+ ],
+ "could not create reserve": [
+ ""
+ ],
+ "Valid until": [
+ ""
+ ],
+ "Created balance": [
+ ""
+ ],
+ "Exchange balance": [
+ ""
+ ],
+ "Picked up": [
+ ""
+ ],
+ "Committed": [
+ ""
+ ],
+ "Account address": [
+ ""
+ ],
+ "Subject": [
+ ""
+ ],
+ "Tips": [
+ ""
+ ],
+ "No tips has been authorized from this reserve": [
+ ""
+ ],
+ "Authorized": [
+ ""
+ ],
+ "Expiration": [
+ ""
+ ],
+ "amount of tip": [
+ ""
+ ],
+ "Justification": [
+ ""
+ ],
+ "reason for the tip": [
+ ""
+ ],
+ "URL after tip": [
+ ""
+ ],
+ "URL to visit after tip payment": [
+ ""
+ ],
+ "Reserves not yet funded": [
+ ""
+ ],
+ "Reserves ready": [
+ ""
+ ],
+ "add new reserve": [
+ ""
+ ],
+ "Expires at": [
+ ""
+ ],
+ "Initial": [
+ ""
+ ],
+ "delete selected reserve from the database": [
+ ""
+ ],
+ "authorize new tip from selected reserve": [
+ ""
+ ],
+ "There is no ready reserves yet, add more pressing the + sign or fund them": [
+ ""
+ ],
+ "Expected Balance": [
+ ""
+ ],
+ "could not create the tip": [
+ ""
+ ],
+ "should not be empty": [
+ ""
+ ],
+ "should be greater that 0": [
+ ""
+ ],
+ "can't be empty": [
+ ""
+ ],
+ "to short": [
+ ""
+ ],
+ "just letters and numbers from 2 to 7": [
+ ""
+ ],
+ "size of the key should be 32": [
+ ""
+ ],
+ "Identifier": [
+ ""
+ ],
+ "Name of the template in URLs.": [
+ ""
+ ],
+ "Describe what this template stands for": [
+ ""
+ ],
+ "Fixed summary": [
+ ""
+ ],
+ "If specified, this template will create order with the same summary": [
+ ""
+ ],
+ "Fixed price": [
+ ""
+ ],
+ "If specified, this template will create order with the same price": [
+ ""
+ ],
+ "Minimum age": [
+ ""
+ ],
+ "Is this contract restricted to some age?": [
+ ""
+ ],
+ "Payment timeout": [
+ ""
+ ],
+ "How much time has the customer to complete the payment once the order was created.": [
+ ""
+ ],
+ "Verification algorithm": [
+ ""
+ ],
+ "Algorithm to use to verify transaction in offline mode": [
+ ""
+ ],
+ "Point-of-sale key": [
+ ""
+ ],
+ "Useful to validate the purchase": [
+ ""
+ ],
+ "generate random secret key": [
+ ""
+ ],
+ "random": [
+ ""
+ ],
+ "show secret key": [
+ ""
+ ],
+ "hide secret key": [
+ ""
+ ],
+ "hide": [
+ ""
+ ],
+ "show": [
+ ""
+ ],
+ "could not inform template": [
+ ""
+ ],
+ "Amount is required": [
+ ""
+ ],
+ "Order summary is required": [
+ ""
+ ],
+ "New order for template": [
+ ""
+ ],
+ "Amount of the order": [
+ ""
+ ],
+ "Order summary": [
+ ""
+ ],
+ "could not create order from template": [
+ ""
+ ],
+ "Here you can specify a default value for fields that are not fixed. Default values can be edited by the customer before the payment.": [
+ ""
+ ],
+ "Fixed amount": [
+ ""
+ ],
+ "Default amount": [
+ ""
+ ],
+ "Default summary": [
+ ""
+ ],
+ "Print": [
+ ""
+ ],
+ "Setup TOTP": [
+ ""
+ ],
+ "Templates": [
+ ""
+ ],
+ "add new templates": [
+ ""
+ ],
+ "load more templates before the first one": [
+ ""
+ ],
+ "load newer templates": [
+ ""
+ ],
+ "delete selected templates from the database": [
+ ""
+ ],
+ "use template to create new order": [
+ ""
+ ],
+ "create qr code for the template": [
+ ""
+ ],
+ "load more templates after the last one": [
+ ""
+ ],
+ "load older templates": [
+ ""
+ ],
+ "There is no templates yet, add more pressing the + sign": [
+ ""
+ ],
+ "template delete successfully": [
+ ""
+ ],
+ "could not delete the template": [
+ ""
+ ],
+ "could not update template": [
+ ""
+ ],
+ "should be one of '%1$s'": [
+ ""
+ ],
+ "Webhook ID to use": [
+ ""
+ ],
+ "Event": [
+ ""
+ ],
+ "The event of the webhook: why the webhook is used": [
+ ""
+ ],
+ "Method": [
+ ""
+ ],
+ "Method used by the webhook": [
+ ""
+ ],
+ "URL": [
+ ""
+ ],
+ "URL of the webhook where the customer will be redirected": [
+ ""
+ ],
+ "Header": [
+ ""
+ ],
+ "Header template of the webhook": [
+ ""
+ ],
+ "Body": [
+ ""
+ ],
+ "Body template by the webhook": [
+ ""
+ ],
+ "Webhooks": [
+ ""
+ ],
+ "add new webhooks": [
+ ""
+ ],
+ "load more webhooks before the first one": [
+ ""
+ ],
+ "load newer webhooks": [
+ ""
+ ],
+ "Event type": [
+ ""
+ ],
+ "delete selected webhook from the database": [
+ ""
+ ],
+ "load more webhooks after the last one": [
+ ""
+ ],
+ "load older webhooks": [
+ ""
+ ],
+ "There is no webhooks yet, add more pressing the + sign": [
+ ""
+ ],
+ "webhook delete successfully": [
+ ""
+ ],
+ "could not delete the webhook": [
+ ""
+ ],
+ "check the id, does not look valid": [
+ ""
+ ],
+ "should have 52 characters, current %1$s": [
+ ""
+ ],
+ "URL doesn't have the right format": [
+ ""
+ ],
+ "Credited bank account": [
+ ""
+ ],
+ "Select one account": [
+ ""
+ ],
+ "Bank account of the merchant where the payment was received": [
+ ""
+ ],
+ "Wire transfer ID": [
+ ""
+ ],
+ "unique identifier of the wire transfer used by the exchange, must be 52 characters long": [
+ ""
+ ],
+ "Base URL of the exchange that made the transfer, should have been in the wire transfer subject": [
+ ""
+ ],
+ "Amount credited": [
+ ""
+ ],
+ "Actual amount that was wired to the merchant's bank account": [
+ ""
+ ],
+ "could not inform transfer": [
+ ""
+ ],
+ "Transfers": [
+ ""
+ ],
+ "add new transfer": [
+ ""
+ ],
+ "load more transfers before the first one": [
+ ""
+ ],
+ "load newer transfers": [
+ ""
+ ],
+ "Credit": [
+ ""
+ ],
+ "Confirmed": [
+ ""
+ ],
+ "Verified": [
+ ""
+ ],
+ "Executed at": [
+ ""
+ ],
+ "yes": [
+ ""
+ ],
+ "no": [
+ ""
+ ],
+ "unknown": [
+ ""
+ ],
+ "delete selected transfer from the database": [
+ ""
+ ],
+ "load more transfer after the last one": [
+ ""
+ ],
+ "load older transfers": [
+ ""
+ ],
+ "There is no transfer yet, add more pressing the + sign": [
+ ""
+ ],
+ "filter by account address": [
+ ""
+ ],
+ "only show wire transfers confirmed by the merchant": [
+ ""
+ ],
+ "only show wire transfers claimed by the exchange": [
+ ""
+ ],
+ "Unverified": [
+ ""
+ ],
+ "is not valid": [
+ ""
+ ],
+ "is not a number": [
+ ""
+ ],
+ "must be 1 or greater": [
+ ""
+ ],
+ "max 7 lines": [
+ ""
+ ],
+ "change authorization configuration": [
+ ""
+ ],
+ "Need to complete marked fields and choose authorization method": [
+ ""
+ ],
+ "This is not a valid bitcoin address.": [
+ ""
+ ],
+ "This is not a valid Ethereum address.": [
+ ""
+ ],
+ "IBAN numbers usually have more that 4 digits": [
+ ""
+ ],
+ "IBAN numbers usually have less that 34 digits": [
+ ""
+ ],
+ "IBAN country code not found": [
+ ""
+ ],
+ "IBAN number is not valid, checksum is wrong": [
+ ""
+ ],
+ "Target type": [
+ ""
+ ],
+ "Method to use for wire transfer": [
+ ""
+ ],
+ "Routing": [
+ ""
+ ],
+ "Routing number.": [
+ ""
+ ],
+ "Account": [
+ ""
+ ],
+ "Account number.": [
+ ""
+ ],
+ "Business Identifier Code.": [
+ ""
+ ],
+ "Bank Account Number.": [
+ ""
+ ],
+ "Unified Payment Interface.": [
+ ""
+ ],
+ "Bitcoin protocol.": [
+ ""
+ ],
+ "Ethereum protocol.": [
+ ""
+ ],
+ "Interledger protocol.": [
+ ""
+ ],
+ "Host": [
+ ""
+ ],
+ "Bank host.": [
+ ""
+ ],
+ "Bank account.": [
+ ""
+ ],
+ "Bank account owner's name.": [
+ ""
+ ],
+ "No accounts yet.": [
+ ""
+ ],
+ "Name of the instance in URLs. The 'default' instance is special in that it is used to administer other instances.": [
+ ""
+ ],
+ "Business name": [
+ ""
+ ],
+ "Legal name of the business represented by this instance.": [
+ ""
+ ],
+ "Email": [
+ ""
+ ],
+ "Contact email": [
+ ""
+ ],
+ "Website URL": [
+ ""
+ ],
+ "URL.": [
+ ""
+ ],
+ "Logo": [
+ ""
+ ],
+ "Logo image.": [
+ ""
+ ],
+ "Bank account": [
+ ""
+ ],
+ "URI specifying bank account for crediting revenue.": [
+ ""
+ ],
+ "Default max deposit fee": [
+ ""
+ ],
+ "Maximum deposit fees this merchant is willing to pay per order by default.": [
+ ""
+ ],
+ "Default max wire fee": [
+ ""
+ ],
+ "Maximum wire fees this merchant is willing to pay per wire transfer by default.": [
+ ""
+ ],
+ "Default wire fee amortization": [
+ ""
+ ],
+ "Number of orders excess wire transfer fees will be divided by to compute per order surcharge.": [
+ ""
+ ],
+ "Physical location of the merchant.": [
+ ""
+ ],
+ "Jurisdiction": [
+ ""
+ ],
+ "Jurisdiction for legal disputes with the merchant.": [
+ ""
+ ],
+ "Default payment delay": [
+ ""
+ ],
+ "Time customers have to pay an order before the offer expires by default.": [
+ ""
+ ],
+ "Default wire transfer delay": [
+ ""
+ ],
+ "Maximum time an exchange is allowed to delay wiring funds to the merchant, enabling it to aggregate smaller payments into larger wire transfers and reducing wire fees.": [
+ ""
+ ],
+ "Instance id": [
+ ""
+ ],
+ "Change the authorization method use for this instance.": [
+ ""
+ ],
+ "Manage access token": [
+ ""
+ ],
+ "Failed to create instance": [
+ ""
+ ],
+ "Login required": [
+ ""
+ ],
+ "Please enter your access token.": [
+ ""
+ ],
+ "Access Token": [
+ ""
+ ],
+ "The request to the backend take too long and was cancelled": [
+ ""
+ ],
+ "Diagnostic from %1$s is \"%2$s\"": [
+ ""
+ ],
+ "The backend reported a problem: HTTP status #%1$s": [
+ ""
+ ],
+ "Diagnostic from %1$s is '%2$s'": [
+ ""
+ ],
+ "Access denied": [
+ ""
+ ],
+ "The access token provided is invalid.": [
+ ""
+ ],
+ "No 'default' instance configured yet.": [
+ ""
+ ],
+ "Create a 'default' instance to begin using the merchant backoffice.": [
+ ""
+ ],
+ "The access token provided is invalid": [
+ ""
+ ],
+ "Hide for today": [
+ ""
+ ],
+ "Instance": [
+ ""
+ ],
+ "Settings": [
+ ""
+ ],
+ "Connection": [
+ ""
+ ],
+ "New": [
+ ""
+ ],
+ "List": [
+ ""
+ ],
+ "Log out": [
+ ""
+ ],
+ "Check your token is valid": [
+ ""
+ ],
+ "Couldn't access the server.": [
+ ""
+ ],
+ "Could not infer instance id from url %1$s": [
+ ""
+ ],
+ "Server not found": [
+ ""
+ ],
+ "Server response with an error code": [
+ ""
+ ],
+ "Got message %1$s from %2$s": [
+ ""
+ ],
+ "Response from server is unreadable, http status: %1$s": [
+ ""
+ ],
+ "Unexpected Error": [
+ ""
+ ],
+ "The value %1$s is invalid for a payment url": [
+ ""
+ ],
+ "add element to the list": [
+ ""
+ ],
+ "add": [
+ ""
+ ],
+ "Deleting": [
+ ""
+ ],
+ "Changing": [
+ ""
+ ],
+ "Order ID": [
+ ""
+ ],
+ "Payment URL": [
+ ""
+ ]
+ }
+ }
+};
+
+strings['es'] = {
+ "domain": "messages",
+ "locale_data": {
+ "messages": {
+ "": {
+ "domain": "messages",
+ "plural_forms": "nplurals=2; plural=n != 1;",
+ "lang": "es"
+ },
+ "Cancel": [
+ "Cancelar"
+ ],
+ "%1$s": [
+ "%1$s"
+ ],
+ "Close": [
+ ""
+ ],
+ "Continue": [
+ "Continuar"
+ ],
+ "Clear": [
+ "Limpiar"
+ ],
+ "Confirm": [
+ "Confirmar"
+ ],
+ "is not the same as the current access token": [
+ "no es el mismo que el token de acceso actual"
+ ],
+ "cannot be empty": [
+ "no puede ser vacío"
+ ],
+ "cannot be the same as the old token": [
+ "no puede ser igual al viejo token"
+ ],
+ "is not the same": [
+ "no son iguales"
+ ],
+ "You are updating the access token from instance with id %1$s": [
+ "Está actualizando el token de acceso para la instancia con id %1$s"
+ ],
+ "Old access token": [
+ "Viejo token de acceso"
+ ],
+ "access token currently in use": [
+ "acceder al token en uso actualmente"
+ ],
+ "New access token": [
+ "Nuevo token de acceso"
+ ],
+ "next access token to be used": [
+ "siguiente token de acceso a usar"
+ ],
+ "Repeat access token": [
+ "Repetir token de acceso"
+ ],
+ "confirm the same access token": [
+ "confirmar el mismo token de acceso"
+ ],
+ "Clearing the access token will mean public access to the instance": [
+ "Limpiar el token de acceso significa acceso público a la instancia"
+ ],
+ "cannot be the same as the old access token": [
+ "no puede ser igual al anterior token de acceso"
+ ],
+ "You are setting the access token for the new instance": [
+ "Está estableciendo el token de acceso para la nueva instancia"
+ ],
+ "With external authorization method no check will be done by the merchant backend": [
+ "Con el método de autorización externa no se hará ninguna revisión por el backend del comerciante"
+ ],
+ "Set external authorization": [
+ "Establecer autorización externa"
+ ],
+ "Set access token": [
+ "Establecer token de acceso"
+ ],
+ "Operation in progress...": [
+ "Operación en progreso..."
+ ],
+ "The operation will be automatically canceled after %1$s seconds": [
+ "La operación será automáticamente cancelada luego de %1$s segundos"
+ ],
+ "Instances": [
+ "Instancias"
+ ],
+ "Delete": [
+ "Eliminar"
+ ],
+ "add new instance": [
+ "agregar nueva instancia"
+ ],
+ "ID": [
+ "ID"
+ ],
+ "Name": [
+ "Nombre"
+ ],
+ "Edit": [
+ "Editar"
+ ],
+ "Purge": [
+ "Purgar"
+ ],
+ "There is no instances yet, add more pressing the + sign": [
+ "Todavía no hay instancias, agregue más presionando el signo +"
+ ],
+ "Only show active instances": [
+ "Solo mostrar instancias activas"
+ ],
+ "Active": [
+ "Activo"
+ ],
+ "Only show deleted instances": [
+ "Mostrar solo instancias eliminadas"
+ ],
+ "Deleted": [
+ "Eliminado"
+ ],
+ "Show all instances": [
+ "Mostrar todas las instancias"
+ ],
+ "All": [
+ "Todo"
+ ],
+ "Instance \"%1$s\" (ID: %2$s) has been deleted": [
+ "La instancia '%1$s' (ID: %2$s) fue eliminada"
+ ],
+ "Failed to delete instance": [
+ "Fallo al eliminar instancia"
+ ],
+ "Instance '%1$s' (ID: %2$s) has been disabled": [
+ "Instance '%1$s' (ID: %2$s) ha sido deshabilitada"
+ ],
+ "Failed to purge instance": [
+ "Fallo al purgar la instancia"
+ ],
+ "Pending KYC verification": [
+ "Verificación KYC pendiente"
+ ],
+ "Timed out": [
+ "Expirado"
+ ],
+ "Exchange": [
+ "Exchange"
+ ],
+ "Target account": [
+ "Cuenta objetivo"
+ ],
+ "KYC URL": [
+ "URL de KYC"
+ ],
+ "Code": [
+ "Código"
+ ],
+ "Http Status": [
+ "Estado http"
+ ],
+ "No pending kyc verification!": [
+ "¡No hay verificación kyc pendiente!"
+ ],
+ "change value to unknown date": [
+ "cambiar valor a fecha desconocida"
+ ],
+ "change value to empty": [
+ "cambiar valor a vacío"
+ ],
+ "clear": [
+ "limpiar"
+ ],
+ "change value to never": [
+ "cambiar valor a nunca"
+ ],
+ "never": [
+ "nunca"
+ ],
+ "Country": [
+ "País"
+ ],
+ "Address": [
+ "Dirección"
+ ],
+ "Building number": [
+ "Número de edificio"
+ ],
+ "Building name": [
+ "Nombre de edificio"
+ ],
+ "Street": [
+ "Calle"
+ ],
+ "Post code": [
+ "Código postal"
+ ],
+ "Town location": [
+ "Ubicación de ciudad"
+ ],
+ "Town": [
+ "Ciudad"
+ ],
+ "District": [
+ "Distrito"
+ ],
+ "Country subdivision": [
+ "Subdivisión de país"
+ ],
+ "Product id": [
+ "Id de producto"
+ ],
+ "Description": [
+ "Descripcion"
+ ],
+ "Product": [
+ "Productos"
+ ],
+ "search products by it's description or id": [
+ "buscar productos por su descripción o ID"
+ ],
+ "no products found with that description": [
+ "no se encontraron productos con esa descripción"
+ ],
+ "You must enter a valid product identifier.": [
+ "Debe ingresar un identificador de producto válido."
+ ],
+ "Quantity must be greater than 0!": [
+ "¡Cantidad debe ser mayor que 0!"
+ ],
+ "This quantity exceeds remaining stock. Currently, only %1$s units remain unreserved in stock.": [
+ "Esta cantidad excede las existencias restantes. Actualmente, solo quedan %1$s unidades sin reservar en las existencias."
+ ],
+ "Quantity": [
+ "Cantidad"
+ ],
+ "how many products will be added": [
+ "cuántos productos serán agregados"
+ ],
+ "Add from inventory": [
+ "Agregar del inventario"
+ ],
+ "Image should be smaller than 1 MB": [
+ "La imagen debe ser mas chica que 1 MB"
+ ],
+ "Add": [
+ "Agregar"
+ ],
+ "Remove": [
+ "Eliminar"
+ ],
+ "No taxes configured for this product.": [
+ "Ningun impuesto configurado para este producto."
+ ],
+ "Amount": [
+ "Monto"
+ ],
+ "Taxes can be in currencies that differ from the main currency used by the merchant.": [
+ "Impuestos pueden estar en divisas que difieren de la principal divisa usada por el comerciante."
+ ],
+ "Enter currency and value separated with a colon, e.g. &quot;USD:2.3&quot;.": [
+ "Ingrese divisa y valor separado por dos puntos, e.g. &quot;USD:2.3&quot;."
+ ],
+ "Legal name of the tax, e.g. VAT or import duties.": [
+ "Nombre legal del impuesto, e.g. IVA o arancel."
+ ],
+ "add tax to the tax list": [
+ "agregar impuesto a la lista de impuestos"
+ ],
+ "describe and add a product that is not in the inventory list": [
+ "describa y agregue un producto que no está en la lista de inventarios"
+ ],
+ "Add custom product": [
+ "Agregue un producto personalizado"
+ ],
+ "Complete information of the product": [
+ "Complete información del producto"
+ ],
+ "Image": [
+ "Imagen"
+ ],
+ "photo of the product": [
+ "foto del producto"
+ ],
+ "full product description": [
+ "descripción completa del producto"
+ ],
+ "Unit": [
+ "Unidad"
+ ],
+ "name of the product unit": [
+ "nombre de la unidad del producto"
+ ],
+ "Price": [
+ "Precio"
+ ],
+ "amount in the current currency": [
+ "monto de la divisa actual"
+ ],
+ "Taxes": [
+ "Impuestos"
+ ],
+ "image": [
+ "imagen"
+ ],
+ "description": [
+ "descripción"
+ ],
+ "quantity": [
+ "cantidad"
+ ],
+ "unit price": [
+ "precio unitario"
+ ],
+ "total price": [
+ "precio total"
+ ],
+ "required": [
+ "requerido"
+ ],
+ "not valid": [
+ "no es un json válido"
+ ],
+ "must be greater than 0": [
+ "debe ser mayor que 0"
+ ],
+ "not a valid json": [
+ "no es un json válido"
+ ],
+ "should be in the future": [
+ "deberían ser en el futuro"
+ ],
+ "refund deadline cannot be before pay deadline": [
+ "plazo de reembolso no puede ser antes que el plazo de pago"
+ ],
+ "wire transfer deadline cannot be before refund deadline": [
+ "el plazo de la transferencia bancaria no puede ser antes que el plazo de reembolso"
+ ],
+ "wire transfer deadline cannot be before pay deadline": [
+ "el plazo de la transferencia bancaria no puede ser antes que el plazo de pago"
+ ],
+ "should have a refund deadline": [
+ "debería tener un plazo de reembolso"
+ ],
+ "auto refund cannot be after refund deadline": [
+ "reembolso automático no puede ser después qu el plazo de reembolso"
+ ],
+ "Manage products in order": [
+ "Manejar productos en orden"
+ ],
+ "Manage list of products in the order.": [
+ "Manejar lista de productos en la orden."
+ ],
+ "Remove this product from the order.": [
+ "Remover este producto de la orden."
+ ],
+ "Total price": [
+ "Precio total"
+ ],
+ "total product price added up": [
+ "precio total de producto agregado"
+ ],
+ "Amount to be paid by the customer": [
+ "Monto a ser pagado por el cliente"
+ ],
+ "Order price": [
+ "Precio de la orden"
+ ],
+ "final order price": [
+ "Precio final de la orden"
+ ],
+ "Summary": [
+ "Resumen"
+ ],
+ "Title of the order to be shown to the customer": [
+ "Título de la orden a ser mostrado al cliente"
+ ],
+ "Shipping and Fulfillment": [
+ "Envío y cumplimiento"
+ ],
+ "Delivery date": [
+ "Fecha de entrega"
+ ],
+ "Deadline for physical delivery assured by the merchant.": [
+ "Plazo para la entrega física asegurado por el comerciante."
+ ],
+ "Location": [
+ "Ubicación"
+ ],
+ "address where the products will be delivered": [
+ "dirección a donde los productos serán entregados"
+ ],
+ "Fulfillment URL": [
+ "URL de cumplimiento"
+ ],
+ "URL to which the user will be redirected after successful payment.": [
+ "URL al cual el usuario será redirigido luego de pago exitoso."
+ ],
+ "Taler payment options": [
+ "Opciones de pago de Taler"
+ ],
+ "Override default Taler payment settings for this order": [
+ "Sobreescribir pagos por omisión de Taler para esta orden"
+ ],
+ "Payment deadline": [
+ "Plazo de pago"
+ ],
+ "Deadline for the customer to pay for the offer before it expires. Inventory products will be reserved until this deadline.": [
+ "Plazo límite para que el cliente pague por la oferta antes de que expire. Productos del inventario serán reservados hasta este plazo límite."
+ ],
+ "Refund deadline": [
+ "Plazo de reembolso"
+ ],
+ "Time until which the order can be refunded by the merchant.": [
+ "Tiempo hasta el cual la orden puede ser reembolsada por el comerciante."
+ ],
+ "Wire transfer deadline": [
+ "Plazo de la transferencia"
+ ],
+ "Deadline for the exchange to make the wire transfer.": [
+ "Plazo para que el exchange haga la transferencia."
+ ],
+ "Auto-refund deadline": [
+ "Plazo de reembolso automático"
+ ],
+ "Time until which the wallet will automatically check for refunds without user interaction.": [
+ "Tiempo hasta el cual la billetera será automáticamente revisada por reembolsos win interación por parte del usuario."
+ ],
+ "Maximum deposit fee": [
+ "Máxima tarifa de depósito"
+ ],
+ "Maximum deposit fees the merchant is willing to cover for this order. Higher deposit fees must be covered in full by the consumer.": [
+ "Máxima tarifa de depósito que el comerciante esta dispuesto a cubir para esta orden. Mayores tarifas de depósito deben ser cubiertas completamente por el consumidor."
+ ],
+ "Maximum wire fee": [
+ "Máxima tarifa de transferencia"
+ ],
+ "Maximum aggregate wire fees the merchant is willing to cover for this order. Wire fees exceeding this amount are to be covered by the customers.": [
+ ""
+ ],
+ "Wire fee amortization": [
+ "Amortización de comisión de transferencia"
+ ],
+ "Factor by which wire fees exceeding the above threshold are divided to determine the share of excess wire fees to be paid explicitly by the consumer.": [
+ ""
+ ],
+ "Create token": [
+ "Administrar token"
+ ],
+ "Uncheck this option if the merchant backend generated an order ID with enough entropy to prevent adversarial claims.": [
+ ""
+ ],
+ "Minimum age required": [
+ "Login necesario"
+ ],
+ "Any value greater than 0 will limit the coins able be used to pay this contract. If empty the age restriction will be defined by the products": [
+ ""
+ ],
+ "Min age defined by the producs is %1$s": [
+ ""
+ ],
+ "Additional information": [
+ "Información extra"
+ ],
+ "Custom information to be included in the contract for this order.": [
+ ""
+ ],
+ "You must enter a value in JavaScript Object Notation (JSON).": [
+ ""
+ ],
+ "days": [
+ "días"
+ ],
+ "hours": [
+ "horas"
+ ],
+ "minutes": [
+ "minutos"
+ ],
+ "seconds": [
+ "segundos"
+ ],
+ "forever": [
+ "nunca"
+ ],
+ "%1$sM": [
+ ""
+ ],
+ "%1$sY": [
+ ""
+ ],
+ "%1$sd": [
+ ""
+ ],
+ "%1$sh": [
+ ""
+ ],
+ "%1$smin": [
+ ""
+ ],
+ "%1$ssec": [
+ ""
+ ],
+ "Orders": [
+ "Órdenes"
+ ],
+ "create order": [
+ "creado"
+ ],
+ "load newer orders": [
+ "cargar nuevas ordenes"
+ ],
+ "Date": [
+ "Fecha"
+ ],
+ "Refund": [
+ "Devolución"
+ ],
+ "copy url": [
+ "copiar url"
+ ],
+ "load older orders": [
+ "cargar viejas ordenes"
+ ],
+ "No orders have been found matching your query!": [
+ "¡No se encontraron órdenes que emparejen su búsqueda!"
+ ],
+ "duplicated": [
+ "duplicado"
+ ],
+ "invalid format": [
+ "formato inválido"
+ ],
+ "this value exceed the refundable amount": [
+ "este monto excede el monto reembolsable"
+ ],
+ "date": [
+ "fecha"
+ ],
+ "amount": [
+ "monto"
+ ],
+ "reason": [
+ "razón"
+ ],
+ "amount to be refunded": [
+ "monto a ser reembolsado"
+ ],
+ "Max refundable:": [
+ "Máximo reembolzable:"
+ ],
+ "Reason": [
+ "Razón"
+ ],
+ "Choose one...": [
+ "Elija uno..."
+ ],
+ "requested by the customer": [
+ "pedido por el consumidor"
+ ],
+ "other": [
+ "otro"
+ ],
+ "why this order is being refunded": [
+ "por qué esta orden está siendo reembolsada"
+ ],
+ "more information to give context": [
+ "más información para dar contexto"
+ ],
+ "Contract Terms": [
+ "Términos de contrato"
+ ],
+ "human-readable description of the whole purchase": [
+ "descripción legible de toda la compra"
+ ],
+ "total price for the transaction": [
+ "precio total de la transacción"
+ ],
+ "URL for this purchase": [
+ "URL para esta compra"
+ ],
+ "Max fee": [
+ "Máxima comisión"
+ ],
+ "maximum total deposit fee accepted by the merchant for this contract": [
+ ""
+ ],
+ "Max wire fee": [
+ "Impuesto de transferencia máximo"
+ ],
+ "maximum wire fee accepted by the merchant": [
+ ""
+ ],
+ "over how many customer transactions does the merchant expect to amortize wire fees on average": [
+ ""
+ ],
+ "Created at": [
+ "Creado en"
+ ],
+ "time when this contract was generated": [
+ ""
+ ],
+ "after this deadline has passed no refunds will be accepted": [
+ ""
+ ],
+ "after this deadline, the merchant won't accept payments for the contract": [
+ ""
+ ],
+ "transfer deadline for the exchange": [
+ ""
+ ],
+ "time indicating when the order should be delivered": [
+ ""
+ ],
+ "where the order will be delivered": [
+ ""
+ ],
+ "Auto-refund delay": [
+ "Plazo de reembolso automático"
+ ],
+ "how long the wallet should try to get an automatic refund for the purchase": [
+ ""
+ ],
+ "Extra info": [
+ "Información extra"
+ ],
+ "extra data that is only interpreted by the merchant frontend": [
+ ""
+ ],
+ "Order": [
+ "Orden"
+ ],
+ "claimed": [
+ "reclamado"
+ ],
+ "claimed at": [
+ "reclamado"
+ ],
+ "Timeline": [
+ "Cronología"
+ ],
+ "Payment details": [
+ "Detalles de pago"
+ ],
+ "Order status": [
+ "Estado de orden"
+ ],
+ "Product list": [
+ "Lista de producto"
+ ],
+ "paid": [
+ "pagados"
+ ],
+ "wired": [
+ "transferido"
+ ],
+ "refunded": [
+ "reembolzado"
+ ],
+ "refund order": [
+ "reembolzado"
+ ],
+ "not refundable": [
+ "Máximo reembolzable:"
+ ],
+ "refund": [
+ "reembolzar"
+ ],
+ "Refunded amount": [
+ "Monto reembolzado"
+ ],
+ "Refund taken": [
+ "Reembolzado"
+ ],
+ "Status URL": [
+ "URL de estado de orden"
+ ],
+ "Refund URI": [
+ "Devolución"
+ ],
+ "unpaid": [
+ "impago"
+ ],
+ "pay at": [
+ "pagar en"
+ ],
+ "created at": [
+ "creado"
+ ],
+ "Order status URL": [
+ "URL de estado de orden"
+ ],
+ "Payment URI": [
+ "URI de pago"
+ ],
+ "Unknown order status. This is an error, please contact the administrator.": [
+ "Estado de orden desconocido. Esto es un error, por favor contacte a su administrador."
+ ],
+ "Back": [
+ ""
+ ],
+ "refund created successfully": [
+ "reembolzo creado satisfactoriamente"
+ ],
+ "could not create the refund": [
+ "No se pudo create el reembolso"
+ ],
+ "select date to show nearby orders": [
+ ""
+ ],
+ "order id": [
+ "ir a id de orden"
+ ],
+ "jump to order with the given order ID": [
+ ""
+ ],
+ "remove all filters": [
+ ""
+ ],
+ "only show paid orders": [
+ ""
+ ],
+ "Paid": [
+ "Pagado"
+ ],
+ "only show orders with refunds": [
+ "No se pudo create el reembolso"
+ ],
+ "Refunded": [
+ "Reembolzado"
+ ],
+ "only show orders where customers paid, but wire payments from payment provider are still pending": [
+ ""
+ ],
+ "Not wired": [
+ "No transferido"
+ ],
+ "clear date filter": [
+ ""
+ ],
+ "date (YYYY/MM/DD)": [
+ ""
+ ],
+ "Enter an order id": [
+ "ir a id de orden"
+ ],
+ "order not found": [
+ "Servidor no encontrado"
+ ],
+ "could not get the order to refund": [
+ "No se pudo create el reembolso"
+ ],
+ "Loading...": [
+ "Cargando..."
+ ],
+ "click here to configure the stock of the product, leave it as is and the backend will not control stock": [
+ ""
+ ],
+ "Manage stock": [
+ "Administrar stock"
+ ],
+ "this product has been configured without stock control": [
+ ""
+ ],
+ "Infinite": [
+ "Inifinito"
+ ],
+ "lost cannot be greater than current and incoming (max %1$s)": [
+ "la pérdida no puede ser mayor al stock actual + entrante (max %1$s )"
+ ],
+ "Incoming": [
+ "Ingresando"
+ ],
+ "Lost": [
+ "Perdido"
+ ],
+ "Current": [
+ "Actual"
+ ],
+ "remove stock control for this product": [
+ ""
+ ],
+ "without stock": [
+ "sin stock"
+ ],
+ "Next restock": [
+ "Próximo reabastecimiento"
+ ],
+ "Delivery address": [
+ "Dirección de entrega"
+ ],
+ "product identification to use in URLs (for internal use only)": [
+ ""
+ ],
+ "illustration of the product for customers": [
+ ""
+ ],
+ "product description for customers": [
+ ""
+ ],
+ "Age restricted": [
+ ""
+ ],
+ "is this product restricted for customer below certain age?": [
+ ""
+ ],
+ "unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 items, 5 meters) for customers": [
+ ""
+ ],
+ "sale price for customers, including taxes, for above units of the product": [
+ ""
+ ],
+ "Stock": [
+ "Existencias"
+ ],
+ "product inventory for products with finite supply (for internal use only)": [
+ ""
+ ],
+ "taxes included in the product price, exposed to customers": [
+ ""
+ ],
+ "Need to complete marked fields": [
+ ""
+ ],
+ "could not create product": [
+ "no se pudo crear el producto"
+ ],
+ "Products": [
+ "Productos"
+ ],
+ "add product to inventory": [
+ ""
+ ],
+ "Sell": [
+ "Venta"
+ ],
+ "Profit": [
+ "Ganancia"
+ ],
+ "Sold": [
+ "Vendido"
+ ],
+ "free": [
+ "Gratis"
+ ],
+ "go to product update page": [
+ "producto actualizado correctamente"
+ ],
+ "Update": [
+ "Actualizar"
+ ],
+ "remove this product from the database": [
+ ""
+ ],
+ "update the product with new price": [
+ ""
+ ],
+ "update product with new price": [
+ ""
+ ],
+ "add more elements to the inventory": [
+ ""
+ ],
+ "report elements lost in the inventory": [
+ ""
+ ],
+ "new price for the product": [
+ "no se pudo actualizar el producto"
+ ],
+ "the are value with errors": [
+ ""
+ ],
+ "update product with new stock and price": [
+ ""
+ ],
+ "There is no products yet, add more pressing the + sign": [
+ "No hay propinas todavía, agregar mas presionando el signo +"
+ ],
+ "product updated successfully": [
+ "producto actualizado correctamente"
+ ],
+ "could not update the product": [
+ "no se pudo actualizar el producto"
+ ],
+ "product delete successfully": [
+ "producto fue eliminado correctamente"
+ ],
+ "could not delete the product": [
+ "no se pudo eliminar el producto"
+ ],
+ "Product id:": [
+ "Id de producto"
+ ],
+ "To complete the setup of the reserve, you must now initiate a wire transfer using the given wire transfer subject and crediting the specified amount to the indicated account of the exchange.": [
+ ""
+ ],
+ "If your system supports RFC 8905, you can do this by opening this URI:": [
+ ""
+ ],
+ "it should be greater than 0": [
+ "Debe ser mayor a 0"
+ ],
+ "must be a valid URL": [
+ ""
+ ],
+ "Initial balance": [
+ "Instancia"
+ ],
+ "balance prior to deposit": [
+ ""
+ ],
+ "Exchange URL": [
+ "URL del Exchange"
+ ],
+ "URL of exchange": [
+ ""
+ ],
+ "Next": [
+ "Siguiente"
+ ],
+ "Wire method": [
+ ""
+ ],
+ "method to use for wire transfer": [
+ "no se pudo informar la transferencia"
+ ],
+ "Select one wire method": [
+ ""
+ ],
+ "could not create reserve": [
+ "No se pudo create el reembolso"
+ ],
+ "Valid until": [
+ "Válido hasta"
+ ],
+ "Created balance": [
+ "creado"
+ ],
+ "Exchange balance": [
+ "Monto inicial"
+ ],
+ "Picked up": [
+ ""
+ ],
+ "Committed": [
+ "Monto confirmado"
+ ],
+ "Account address": [
+ "Dirección de cuenta"
+ ],
+ "Subject": [
+ "Asunto"
+ ],
+ "Tips": [
+ "Propinas"
+ ],
+ "No tips has been authorized from this reserve": [
+ ""
+ ],
+ "Authorized": [
+ "Token de autorización"
+ ],
+ "Expiration": [
+ "Información extra"
+ ],
+ "amount of tip": [
+ "monto"
+ ],
+ "Justification": [
+ "Jurisdicción"
+ ],
+ "reason for the tip": [
+ ""
+ ],
+ "URL after tip": [
+ ""
+ ],
+ "URL to visit after tip payment": [
+ ""
+ ],
+ "Reserves not yet funded": [
+ "Servidor no encontrado"
+ ],
+ "Reserves ready": [
+ ""
+ ],
+ "add new reserve": [
+ "cargar nuevas transferencias"
+ ],
+ "Expires at": [
+ ""
+ ],
+ "Initial": [
+ ""
+ ],
+ "delete selected reserve from the database": [
+ ""
+ ],
+ "authorize new tip from selected reserve": [
+ ""
+ ],
+ "There is no ready reserves yet, add more pressing the + sign or fund them": [
+ "No hay transferencias todavía, agregar mas presionando el signo +"
+ ],
+ "Expected Balance": [
+ "Ejecutado en"
+ ],
+ "could not create the tip": [
+ "No se pudo create el reembolso"
+ ],
+ "should not be empty": [
+ "no puede ser vacío"
+ ],
+ "should be greater that 0": [
+ "Debe ser mayor a 0"
+ ],
+ "can't be empty": [
+ "no puede ser vacío"
+ ],
+ "to short": [
+ ""
+ ],
+ "just letters and numbers from 2 to 7": [
+ ""
+ ],
+ "size of the key should be 32": [
+ ""
+ ],
+ "Identifier": [
+ ""
+ ],
+ "Name of the template in URLs.": [
+ ""
+ ],
+ "Describe what this template stands for": [
+ ""
+ ],
+ "Fixed summary": [
+ "Estado de orden"
+ ],
+ "If specified, this template will create order with the same summary": [
+ ""
+ ],
+ "Fixed price": [
+ "precio unitario"
+ ],
+ "If specified, this template will create order with the same price": [
+ ""
+ ],
+ "Minimum age": [
+ "Edad mínima"
+ ],
+ "Is this contract restricted to some age?": [
+ ""
+ ],
+ "Payment timeout": [
+ "Opciones de pago"
+ ],
+ "How much time has the customer to complete the payment once the order was created.": [
+ ""
+ ],
+ "Verification algorithm": [
+ ""
+ ],
+ "Algorithm to use to verify transaction in offline mode": [
+ ""
+ ],
+ "Point-of-sale key": [
+ ""
+ ],
+ "Useful to validate the purchase": [
+ ""
+ ],
+ "generate random secret key": [
+ ""
+ ],
+ "random": [
+ ""
+ ],
+ "show secret key": [
+ ""
+ ],
+ "hide secret key": [
+ ""
+ ],
+ "hide": [
+ ""
+ ],
+ "show": [
+ ""
+ ],
+ "could not inform template": [
+ "no se pudo informar la transferencia"
+ ],
+ "Amount is required": [
+ "Login necesario"
+ ],
+ "Order summary is required": [
+ ""
+ ],
+ "New order for template": [
+ "cargar viejas transferencias"
+ ],
+ "Amount of the order": [
+ ""
+ ],
+ "Order summary": [
+ "Estado de orden"
+ ],
+ "could not create order from template": [
+ "No se pudo create el reembolso"
+ ],
+ "Here you can specify a default value for fields that are not fixed. Default values can be edited by the customer before the payment.": [
+ ""
+ ],
+ "Fixed amount": [
+ "Monto reembolzado"
+ ],
+ "Default amount": [
+ "Monto reembolzado"
+ ],
+ "Default summary": [
+ "Estado de orden"
+ ],
+ "Print": [
+ ""
+ ],
+ "Setup TOTP": [
+ ""
+ ],
+ "Templates": [
+ ""
+ ],
+ "add new templates": [
+ ""
+ ],
+ "load more templates before the first one": [
+ ""
+ ],
+ "load newer templates": [
+ "cargar nuevas transferencias"
+ ],
+ "delete selected templates from the database": [
+ ""
+ ],
+ "use template to create new order": [
+ ""
+ ],
+ "create qr code for the template": [
+ "No se pudo create el reembolso"
+ ],
+ "load more templates after the last one": [
+ ""
+ ],
+ "load older templates": [
+ "cargar viejas transferencias"
+ ],
+ "There is no templates yet, add more pressing the + sign": [
+ "No hay propinas todavía, agregar mas presionando el signo +"
+ ],
+ "template delete successfully": [
+ "producto fue eliminado correctamente"
+ ],
+ "could not delete the template": [
+ "no se pudo eliminar el producto"
+ ],
+ "could not update template": [
+ "no se pudo actualizar el producto"
+ ],
+ "should be one of '%1$s'": [
+ "deberían ser iguales"
+ ],
+ "Webhook ID to use": [
+ ""
+ ],
+ "Event": [
+ ""
+ ],
+ "The event of the webhook: why the webhook is used": [
+ ""
+ ],
+ "Method": [
+ ""
+ ],
+ "Method used by the webhook": [
+ ""
+ ],
+ "URL": [
+ "URL"
+ ],
+ "URL of the webhook where the customer will be redirected": [
+ ""
+ ],
+ "Header": [
+ ""
+ ],
+ "Header template of the webhook": [
+ ""
+ ],
+ "Body": [
+ ""
+ ],
+ "Body template by the webhook": [
+ ""
+ ],
+ "Webhooks": [
+ ""
+ ],
+ "add new webhooks": [
+ ""
+ ],
+ "load more webhooks before the first one": [
+ ""
+ ],
+ "load newer webhooks": [
+ "cargar nuevas ordenes"
+ ],
+ "Event type": [
+ ""
+ ],
+ "delete selected webhook from the database": [
+ ""
+ ],
+ "load more webhooks after the last one": [
+ ""
+ ],
+ "load older webhooks": [
+ "cargar viejas ordenes"
+ ],
+ "There is no webhooks yet, add more pressing the + sign": [
+ "No hay propinas todavía, agregar mas presionando el signo +"
+ ],
+ "webhook delete successfully": [
+ "producto fue eliminado correctamente"
+ ],
+ "could not delete the webhook": [
+ "no se pudo eliminar el producto"
+ ],
+ "check the id, does not look valid": [
+ "verificar el id, no parece válido"
+ ],
+ "should have 52 characters, current %1$s": [
+ "debería tener 52 caracteres, actualmente %1$s"
+ ],
+ "URL doesn't have the right format": [
+ "La URL no tiene el formato correcto"
+ ],
+ "Credited bank account": [
+ ""
+ ],
+ "Select one account": [
+ ""
+ ],
+ "Bank account of the merchant where the payment was received": [
+ ""
+ ],
+ "Wire transfer ID": [
+ "Id de transferencia"
+ ],
+ "unique identifier of the wire transfer used by the exchange, must be 52 characters long": [
+ ""
+ ],
+ "Base URL of the exchange that made the transfer, should have been in the wire transfer subject": [
+ ""
+ ],
+ "Amount credited": [
+ ""
+ ],
+ "Actual amount that was wired to the merchant's bank account": [
+ ""
+ ],
+ "could not inform transfer": [
+ "no se pudo informar la transferencia"
+ ],
+ "Transfers": [
+ "Transferencias"
+ ],
+ "add new transfer": [
+ "cargar nuevas transferencias"
+ ],
+ "load more transfers before the first one": [
+ ""
+ ],
+ "load newer transfers": [
+ "cargar nuevas transferencias"
+ ],
+ "Credit": [
+ "Crédito"
+ ],
+ "Confirmed": [
+ "Confirmado"
+ ],
+ "Verified": [
+ "Verificado"
+ ],
+ "Executed at": [
+ "Ejecutado en"
+ ],
+ "yes": [
+ "si"
+ ],
+ "no": [
+ "no"
+ ],
+ "unknown": [
+ "desconocido"
+ ],
+ "delete selected transfer from the database": [
+ "eliminar transferencia seleccionada de la base de datos"
+ ],
+ "load more transfer after the last one": [
+ "cargue más transferencia luego de la última"
+ ],
+ "load older transfers": [
+ "cargar viejas transferencias"
+ ],
+ "There is no transfer yet, add more pressing the + sign": [
+ "No hay transferencias todavía, agregar mas presionando el signo +"
+ ],
+ "filter by account address": [
+ "Dirección de cuenta"
+ ],
+ "only show wire transfers confirmed by the merchant": [
+ ""
+ ],
+ "only show wire transfers claimed by the exchange": [
+ ""
+ ],
+ "Unverified": [
+ "Verificado"
+ ],
+ "is not valid": [
+ ""
+ ],
+ "is not a number": [
+ "Número de edificio"
+ ],
+ "must be 1 or greater": [
+ "debe ser 1 o mayor"
+ ],
+ "max 7 lines": [
+ "máximo 7 líneas"
+ ],
+ "change authorization configuration": [
+ "cambiar configuración de autorización"
+ ],
+ "Need to complete marked fields and choose authorization method": [
+ "Necesita completar campos marcados y escoger un método de autorización"
+ ],
+ "This is not a valid bitcoin address.": [
+ "Esta no es una dirección de bitcoin válida."
+ ],
+ "This is not a valid Ethereum address.": [
+ "Esta no es una dirección de Ethereum válida."
+ ],
+ "IBAN numbers usually have more that 4 digits": [
+ "Números IBAN usualmente tienen más de 4 dígitos"
+ ],
+ "IBAN numbers usually have less that 34 digits": [
+ "Número IBAN usualmente tienen menos de 34 dígitos"
+ ],
+ "IBAN country code not found": [
+ "Código IBAN de país no encontrado"
+ ],
+ "IBAN number is not valid, checksum is wrong": [
+ "Número IBAN no es válido, la suma de verificación es incorrecta"
+ ],
+ "Target type": [
+ "Tipo objetivo"
+ ],
+ "Method to use for wire transfer": [
+ "Método a usar para la transferencia"
+ ],
+ "Routing": [
+ "Enrutamiento"
+ ],
+ "Routing number.": [
+ "Número de enrutamiento."
+ ],
+ "Account": [
+ "Cuenta"
+ ],
+ "Account number.": [
+ "Dirección de cuenta"
+ ],
+ "Business Identifier Code.": [
+ ""
+ ],
+ "Bank Account Number.": [
+ ""
+ ],
+ "Unified Payment Interface.": [
+ "Interfaz de pago unificado."
+ ],
+ "Bitcoin protocol.": [
+ ""
+ ],
+ "Ethereum protocol.": [
+ ""
+ ],
+ "Interledger protocol.": [
+ ""
+ ],
+ "Host": [
+ ""
+ ],
+ "Bank host.": [
+ ""
+ ],
+ "Bank account.": [
+ ""
+ ],
+ "Bank account owner's name.": [
+ ""
+ ],
+ "No accounts yet.": [
+ ""
+ ],
+ "Name of the instance in URLs. The 'default' instance is special in that it is used to administer other instances.": [
+ ""
+ ],
+ "Business name": [
+ "Nombre de edificio"
+ ],
+ "Legal name of the business represented by this instance.": [
+ ""
+ ],
+ "Email": [
+ ""
+ ],
+ "Contact email": [
+ ""
+ ],
+ "Website URL": [
+ "URL de sitio web"
+ ],
+ "URL.": [
+ ""
+ ],
+ "Logo": [
+ ""
+ ],
+ "Logo image.": [
+ ""
+ ],
+ "Bank account": [
+ "Cuenta bancaria"
+ ],
+ "URI specifying bank account for crediting revenue.": [
+ ""
+ ],
+ "Default max deposit fee": [
+ "Impuesto máximo de deposito por omisión"
+ ],
+ "Maximum deposit fees this merchant is willing to pay per order by default.": [
+ ""
+ ],
+ "Default max wire fee": [
+ "Impuesto máximo de transferencia por omisión"
+ ],
+ "Maximum wire fees this merchant is willing to pay per wire transfer by default.": [
+ ""
+ ],
+ "Default wire fee amortization": [
+ "Amortización de impuesto de transferencia por omisión"
+ ],
+ "Number of orders excess wire transfer fees will be divided by to compute per order surcharge.": [
+ ""
+ ],
+ "Physical location of the merchant.": [
+ ""
+ ],
+ "Jurisdiction": [
+ "Jurisdicción"
+ ],
+ "Jurisdiction for legal disputes with the merchant.": [
+ "Jurisdicción para disputas legales con el comerciante."
+ ],
+ "Default payment delay": [
+ "Retrazo de pago por omisión"
+ ],
+ "Time customers have to pay an order before the offer expires by default.": [
+ ""
+ ],
+ "Default wire transfer delay": [
+ "Retrazo de transferencia por omisión"
+ ],
+ "Maximum time an exchange is allowed to delay wiring funds to the merchant, enabling it to aggregate smaller payments into larger wire transfers and reducing wire fees.": [
+ ""
+ ],
+ "Instance id": [
+ "ID de instancia"
+ ],
+ "Change the authorization method use for this instance.": [
+ "Limpiar el token de autorización significa acceso público a la instancia"
+ ],
+ "Manage access token": [
+ "Administrar token de acceso"
+ ],
+ "Failed to create instance": [
+ "Fallo al crear la instancia"
+ ],
+ "Login required": [
+ "Login necesario"
+ ],
+ "Please enter your access token.": [
+ ""
+ ],
+ "Access Token": [
+ "Acceso denegado"
+ ],
+ "The request to the backend take too long and was cancelled": [
+ ""
+ ],
+ "Diagnostic from %1$s is \"%2$s\"": [
+ ""
+ ],
+ "The backend reported a problem: HTTP status #%1$s": [
+ "Servidir reporto un problema: HTTP status #%1$s"
+ ],
+ "Diagnostic from %1$s is '%2$s'": [
+ ""
+ ],
+ "Access denied": [
+ "Acceso denegado"
+ ],
+ "The access token provided is invalid.": [
+ ""
+ ],
+ "No 'default' instance configured yet.": [
+ "Sin instancia default"
+ ],
+ "Create a 'default' instance to begin using the merchant backoffice.": [
+ ""
+ ],
+ "The access token provided is invalid": [
+ ""
+ ],
+ "Hide for today": [
+ ""
+ ],
+ "Instance": [
+ "Instancia"
+ ],
+ "Settings": [
+ "Configuración"
+ ],
+ "Connection": [
+ "Conexión"
+ ],
+ "New": [
+ "Nuevo"
+ ],
+ "List": [
+ "Lista"
+ ],
+ "Log out": [
+ "Salir"
+ ],
+ "Check your token is valid": [
+ "Verifica que el token sea valido"
+ ],
+ "Couldn't access the server.": [
+ "No se pudo acceder al servidor."
+ ],
+ "Could not infer instance id from url %1$s": [
+ "No se pudo inferir el id de la instancia con la url %1$s"
+ ],
+ "Server not found": [
+ "Servidor no encontrado"
+ ],
+ "Server response with an error code": [
+ ""
+ ],
+ "Got message %1$s from %2$s": [
+ "Recibimos el mensaje %1$s desde %2$s"
+ ],
+ "Response from server is unreadable, http status: %1$s": [
+ ""
+ ],
+ "Unexpected Error": [
+ "Error inesperado"
+ ],
+ "The value %1$s is invalid for a payment url": [
+ "El valor %1$s es invalido para una URL de pago"
+ ],
+ "add element to the list": [
+ "agregar elemento a la lista"
+ ],
+ "add": [
+ "Agregar"
+ ],
+ "Deleting": [
+ "Borrando"
+ ],
+ "Changing": [
+ "Cambiando"
+ ],
+ "Order ID": [
+ "ID de pedido"
+ ],
+ "Payment URL": [
+ "URL de pago"
+ ]
+ }
+ }
+};
+
+strings['fr'] = {
+ "domain": "messages",
+ "locale_data": {
+ "messages": {
+ "": {
+ "domain": "messages",
+ "plural_forms": "nplurals=2; plural=(n != 1);",
+ "lang": ""
+ },
+ "Cancel": [
+ ""
+ ],
+ "%1$s": [
+ ""
+ ],
+ "Close": [
+ ""
+ ],
+ "Continue": [
+ ""
+ ],
+ "Clear": [
+ ""
+ ],
+ "Confirm": [
+ ""
+ ],
+ "is not the same as the current access token": [
+ ""
+ ],
+ "cannot be empty": [
+ ""
+ ],
+ "cannot be the same as the old token": [
+ ""
+ ],
+ "is not the same": [
+ ""
+ ],
+ "You are updating the access token from instance with id %1$s": [
+ ""
+ ],
+ "Old access token": [
+ ""
+ ],
+ "access token currently in use": [
+ ""
+ ],
+ "New access token": [
+ ""
+ ],
+ "next access token to be used": [
+ ""
+ ],
+ "Repeat access token": [
+ ""
+ ],
+ "confirm the same access token": [
+ ""
+ ],
+ "Clearing the access token will mean public access to the instance": [
+ ""
+ ],
+ "cannot be the same as the old access token": [
+ ""
+ ],
+ "You are setting the access token for the new instance": [
+ ""
+ ],
+ "With external authorization method no check will be done by the merchant backend": [
+ ""
+ ],
+ "Set external authorization": [
+ ""
+ ],
+ "Set access token": [
+ ""
+ ],
+ "Operation in progress...": [
+ ""
+ ],
+ "The operation will be automatically canceled after %1$s seconds": [
+ ""
+ ],
+ "Instances": [
+ ""
+ ],
+ "Delete": [
+ ""
+ ],
+ "add new instance": [
+ ""
+ ],
+ "ID": [
+ ""
+ ],
+ "Name": [
+ ""
+ ],
+ "Edit": [
+ ""
+ ],
+ "Purge": [
+ ""
+ ],
+ "There is no instances yet, add more pressing the + sign": [
+ ""
+ ],
+ "Only show active instances": [
+ ""
+ ],
+ "Active": [
+ ""
+ ],
+ "Only show deleted instances": [
+ ""
+ ],
+ "Deleted": [
+ ""
+ ],
+ "Show all instances": [
+ ""
+ ],
+ "All": [
+ ""
+ ],
+ "Instance \"%1$s\" (ID: %2$s) has been deleted": [
+ ""
+ ],
+ "Failed to delete instance": [
+ ""
+ ],
+ "Instance '%1$s' (ID: %2$s) has been disabled": [
+ ""
+ ],
+ "Failed to purge instance": [
+ ""
+ ],
+ "Pending KYC verification": [
+ ""
+ ],
+ "Timed out": [
+ ""
+ ],
+ "Exchange": [
+ ""
+ ],
+ "Target account": [
+ ""
+ ],
+ "KYC URL": [
+ ""
+ ],
+ "Code": [
+ ""
+ ],
+ "Http Status": [
+ ""
+ ],
+ "No pending kyc verification!": [
+ ""
+ ],
+ "change value to unknown date": [
+ ""
+ ],
+ "change value to empty": [
+ ""
+ ],
+ "clear": [
+ ""
+ ],
+ "change value to never": [
+ ""
+ ],
+ "never": [
+ ""
+ ],
+ "Country": [
+ ""
+ ],
+ "Address": [
+ ""
+ ],
+ "Building number": [
+ ""
+ ],
+ "Building name": [
+ ""
+ ],
+ "Street": [
+ ""
+ ],
+ "Post code": [
+ ""
+ ],
+ "Town location": [
+ ""
+ ],
+ "Town": [
+ ""
+ ],
+ "District": [
+ ""
+ ],
+ "Country subdivision": [
+ ""
+ ],
+ "Product id": [
+ ""
+ ],
+ "Description": [
+ ""
+ ],
+ "Product": [
+ ""
+ ],
+ "search products by it's description or id": [
+ ""
+ ],
+ "no products found with that description": [
+ ""
+ ],
+ "You must enter a valid product identifier.": [
+ ""
+ ],
+ "Quantity must be greater than 0!": [
+ ""
+ ],
+ "This quantity exceeds remaining stock. Currently, only %1$s units remain unreserved in stock.": [
+ ""
+ ],
+ "Quantity": [
+ ""
+ ],
+ "how many products will be added": [
+ ""
+ ],
+ "Add from inventory": [
+ ""
+ ],
+ "Image should be smaller than 1 MB": [
+ ""
+ ],
+ "Add": [
+ ""
+ ],
+ "Remove": [
+ ""
+ ],
+ "No taxes configured for this product.": [
+ ""
+ ],
+ "Amount": [
+ ""
+ ],
+ "Taxes can be in currencies that differ from the main currency used by the merchant.": [
+ ""
+ ],
+ "Enter currency and value separated with a colon, e.g. &quot;USD:2.3&quot;.": [
+ ""
+ ],
+ "Legal name of the tax, e.g. VAT or import duties.": [
+ ""
+ ],
+ "add tax to the tax list": [
+ ""
+ ],
+ "describe and add a product that is not in the inventory list": [
+ ""
+ ],
+ "Add custom product": [
+ ""
+ ],
+ "Complete information of the product": [
+ ""
+ ],
+ "Image": [
+ ""
+ ],
+ "photo of the product": [
+ ""
+ ],
+ "full product description": [
+ ""
+ ],
+ "Unit": [
+ ""
+ ],
+ "name of the product unit": [
+ ""
+ ],
+ "Price": [
+ ""
+ ],
+ "amount in the current currency": [
+ ""
+ ],
+ "Taxes": [
+ ""
+ ],
+ "image": [
+ ""
+ ],
+ "description": [
+ ""
+ ],
+ "quantity": [
+ ""
+ ],
+ "unit price": [
+ ""
+ ],
+ "total price": [
+ ""
+ ],
+ "required": [
+ ""
+ ],
+ "not valid": [
+ ""
+ ],
+ "must be greater than 0": [
+ ""
+ ],
+ "not a valid json": [
+ ""
+ ],
+ "should be in the future": [
+ ""
+ ],
+ "refund deadline cannot be before pay deadline": [
+ ""
+ ],
+ "wire transfer deadline cannot be before refund deadline": [
+ ""
+ ],
+ "wire transfer deadline cannot be before pay deadline": [
+ ""
+ ],
+ "should have a refund deadline": [
+ ""
+ ],
+ "auto refund cannot be after refund deadline": [
+ ""
+ ],
+ "Manage products in order": [
+ ""
+ ],
+ "Manage list of products in the order.": [
+ ""
+ ],
+ "Remove this product from the order.": [
+ ""
+ ],
+ "Total price": [
+ ""
+ ],
+ "total product price added up": [
+ ""
+ ],
+ "Amount to be paid by the customer": [
+ ""
+ ],
+ "Order price": [
+ ""
+ ],
+ "final order price": [
+ ""
+ ],
+ "Summary": [
+ ""
+ ],
+ "Title of the order to be shown to the customer": [
+ ""
+ ],
+ "Shipping and Fulfillment": [
+ ""
+ ],
+ "Delivery date": [
+ ""
+ ],
+ "Deadline for physical delivery assured by the merchant.": [
+ ""
+ ],
+ "Location": [
+ ""
+ ],
+ "address where the products will be delivered": [
+ ""
+ ],
+ "Fulfillment URL": [
+ ""
+ ],
+ "URL to which the user will be redirected after successful payment.": [
+ ""
+ ],
+ "Taler payment options": [
+ ""
+ ],
+ "Override default Taler payment settings for this order": [
+ ""
+ ],
+ "Payment deadline": [
+ ""
+ ],
+ "Deadline for the customer to pay for the offer before it expires. Inventory products will be reserved until this deadline.": [
+ ""
+ ],
+ "Refund deadline": [
+ ""
+ ],
+ "Time until which the order can be refunded by the merchant.": [
+ ""
+ ],
+ "Wire transfer deadline": [
+ ""
+ ],
+ "Deadline for the exchange to make the wire transfer.": [
+ ""
+ ],
+ "Auto-refund deadline": [
+ ""
+ ],
+ "Time until which the wallet will automatically check for refunds without user interaction.": [
+ ""
+ ],
+ "Maximum deposit fee": [
+ ""
+ ],
+ "Maximum deposit fees the merchant is willing to cover for this order. Higher deposit fees must be covered in full by the consumer.": [
+ ""
+ ],
+ "Maximum wire fee": [
+ ""
+ ],
+ "Maximum aggregate wire fees the merchant is willing to cover for this order. Wire fees exceeding this amount are to be covered by the customers.": [
+ ""
+ ],
+ "Wire fee amortization": [
+ ""
+ ],
+ "Factor by which wire fees exceeding the above threshold are divided to determine the share of excess wire fees to be paid explicitly by the consumer.": [
+ ""
+ ],
+ "Create token": [
+ ""
+ ],
+ "Uncheck this option if the merchant backend generated an order ID with enough entropy to prevent adversarial claims.": [
+ ""
+ ],
+ "Minimum age required": [
+ ""
+ ],
+ "Any value greater than 0 will limit the coins able be used to pay this contract. If empty the age restriction will be defined by the products": [
+ ""
+ ],
+ "Min age defined by the producs is %1$s": [
+ ""
+ ],
+ "Additional information": [
+ ""
+ ],
+ "Custom information to be included in the contract for this order.": [
+ ""
+ ],
+ "You must enter a value in JavaScript Object Notation (JSON).": [
+ ""
+ ],
+ "days": [
+ ""
+ ],
+ "hours": [
+ ""
+ ],
+ "minutes": [
+ ""
+ ],
+ "seconds": [
+ ""
+ ],
+ "forever": [
+ ""
+ ],
+ "%1$sM": [
+ ""
+ ],
+ "%1$sY": [
+ ""
+ ],
+ "%1$sd": [
+ ""
+ ],
+ "%1$sh": [
+ ""
+ ],
+ "%1$smin": [
+ ""
+ ],
+ "%1$ssec": [
+ ""
+ ],
+ "Orders": [
+ ""
+ ],
+ "create order": [
+ ""
+ ],
+ "load newer orders": [
+ ""
+ ],
+ "Date": [
+ ""
+ ],
+ "Refund": [
+ ""
+ ],
+ "copy url": [
+ ""
+ ],
+ "load older orders": [
+ ""
+ ],
+ "No orders have been found matching your query!": [
+ ""
+ ],
+ "duplicated": [
+ ""
+ ],
+ "invalid format": [
+ ""
+ ],
+ "this value exceed the refundable amount": [
+ ""
+ ],
+ "date": [
+ ""
+ ],
+ "amount": [
+ ""
+ ],
+ "reason": [
+ ""
+ ],
+ "amount to be refunded": [
+ ""
+ ],
+ "Max refundable:": [
+ ""
+ ],
+ "Reason": [
+ ""
+ ],
+ "Choose one...": [
+ ""
+ ],
+ "requested by the customer": [
+ ""
+ ],
+ "other": [
+ ""
+ ],
+ "why this order is being refunded": [
+ ""
+ ],
+ "more information to give context": [
+ ""
+ ],
+ "Contract Terms": [
+ ""
+ ],
+ "human-readable description of the whole purchase": [
+ ""
+ ],
+ "total price for the transaction": [
+ ""
+ ],
+ "URL for this purchase": [
+ ""
+ ],
+ "Max fee": [
+ ""
+ ],
+ "maximum total deposit fee accepted by the merchant for this contract": [
+ ""
+ ],
+ "Max wire fee": [
+ ""
+ ],
+ "maximum wire fee accepted by the merchant": [
+ ""
+ ],
+ "over how many customer transactions does the merchant expect to amortize wire fees on average": [
+ ""
+ ],
+ "Created at": [
+ ""
+ ],
+ "time when this contract was generated": [
+ ""
+ ],
+ "after this deadline has passed no refunds will be accepted": [
+ ""
+ ],
+ "after this deadline, the merchant won't accept payments for the contract": [
+ ""
+ ],
+ "transfer deadline for the exchange": [
+ ""
+ ],
+ "time indicating when the order should be delivered": [
+ ""
+ ],
+ "where the order will be delivered": [
+ ""
+ ],
+ "Auto-refund delay": [
+ ""
+ ],
+ "how long the wallet should try to get an automatic refund for the purchase": [
+ ""
+ ],
+ "Extra info": [
+ ""
+ ],
+ "extra data that is only interpreted by the merchant frontend": [
+ ""
+ ],
+ "Order": [
+ ""
+ ],
+ "claimed": [
+ ""
+ ],
+ "claimed at": [
+ ""
+ ],
+ "Timeline": [
+ ""
+ ],
+ "Payment details": [
+ ""
+ ],
+ "Order status": [
+ ""
+ ],
+ "Product list": [
+ ""
+ ],
+ "paid": [
+ ""
+ ],
+ "wired": [
+ ""
+ ],
+ "refunded": [
+ ""
+ ],
+ "refund order": [
+ ""
+ ],
+ "not refundable": [
+ ""
+ ],
+ "refund": [
+ ""
+ ],
+ "Refunded amount": [
+ ""
+ ],
+ "Refund taken": [
+ ""
+ ],
+ "Status URL": [
+ ""
+ ],
+ "Refund URI": [
+ ""
+ ],
+ "unpaid": [
+ ""
+ ],
+ "pay at": [
+ ""
+ ],
+ "created at": [
+ ""
+ ],
+ "Order status URL": [
+ ""
+ ],
+ "Payment URI": [
+ ""
+ ],
+ "Unknown order status. This is an error, please contact the administrator.": [
+ ""
+ ],
+ "Back": [
+ ""
+ ],
+ "refund created successfully": [
+ ""
+ ],
+ "could not create the refund": [
+ ""
+ ],
+ "select date to show nearby orders": [
+ ""
+ ],
+ "order id": [
+ ""
+ ],
+ "jump to order with the given order ID": [
+ ""
+ ],
+ "remove all filters": [
+ ""
+ ],
+ "only show paid orders": [
+ ""
+ ],
+ "Paid": [
+ ""
+ ],
+ "only show orders with refunds": [
+ ""
+ ],
+ "Refunded": [
+ ""
+ ],
+ "only show orders where customers paid, but wire payments from payment provider are still pending": [
+ ""
+ ],
+ "Not wired": [
+ ""
+ ],
+ "clear date filter": [
+ ""
+ ],
+ "date (YYYY/MM/DD)": [
+ ""
+ ],
+ "Enter an order id": [
+ ""
+ ],
+ "order not found": [
+ ""
+ ],
+ "could not get the order to refund": [
+ ""
+ ],
+ "Loading...": [
+ ""
+ ],
+ "click here to configure the stock of the product, leave it as is and the backend will not control stock": [
+ ""
+ ],
+ "Manage stock": [
+ ""
+ ],
+ "this product has been configured without stock control": [
+ ""
+ ],
+ "Infinite": [
+ ""
+ ],
+ "lost cannot be greater than current and incoming (max %1$s)": [
+ ""
+ ],
+ "Incoming": [
+ ""
+ ],
+ "Lost": [
+ ""
+ ],
+ "Current": [
+ ""
+ ],
+ "remove stock control for this product": [
+ ""
+ ],
+ "without stock": [
+ ""
+ ],
+ "Next restock": [
+ ""
+ ],
+ "Delivery address": [
+ ""
+ ],
+ "product identification to use in URLs (for internal use only)": [
+ ""
+ ],
+ "illustration of the product for customers": [
+ ""
+ ],
+ "product description for customers": [
+ ""
+ ],
+ "Age restricted": [
+ ""
+ ],
+ "is this product restricted for customer below certain age?": [
+ ""
+ ],
+ "unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 items, 5 meters) for customers": [
+ ""
+ ],
+ "sale price for customers, including taxes, for above units of the product": [
+ ""
+ ],
+ "Stock": [
+ ""
+ ],
+ "product inventory for products with finite supply (for internal use only)": [
+ ""
+ ],
+ "taxes included in the product price, exposed to customers": [
+ ""
+ ],
+ "Need to complete marked fields": [
+ ""
+ ],
+ "could not create product": [
+ ""
+ ],
+ "Products": [
+ ""
+ ],
+ "add product to inventory": [
+ ""
+ ],
+ "Sell": [
+ ""
+ ],
+ "Profit": [
+ ""
+ ],
+ "Sold": [
+ ""
+ ],
+ "free": [
+ ""
+ ],
+ "go to product update page": [
+ ""
+ ],
+ "Update": [
+ ""
+ ],
+ "remove this product from the database": [
+ ""
+ ],
+ "update the product with new price": [
+ ""
+ ],
+ "update product with new price": [
+ ""
+ ],
+ "add more elements to the inventory": [
+ ""
+ ],
+ "report elements lost in the inventory": [
+ ""
+ ],
+ "new price for the product": [
+ ""
+ ],
+ "the are value with errors": [
+ ""
+ ],
+ "update product with new stock and price": [
+ ""
+ ],
+ "There is no products yet, add more pressing the + sign": [
+ ""
+ ],
+ "product updated successfully": [
+ ""
+ ],
+ "could not update the product": [
+ ""
+ ],
+ "product delete successfully": [
+ ""
+ ],
+ "could not delete the product": [
+ ""
+ ],
+ "Product id:": [
+ ""
+ ],
+ "To complete the setup of the reserve, you must now initiate a wire transfer using the given wire transfer subject and crediting the specified amount to the indicated account of the exchange.": [
+ ""
+ ],
+ "If your system supports RFC 8905, you can do this by opening this URI:": [
+ ""
+ ],
+ "it should be greater than 0": [
+ ""
+ ],
+ "must be a valid URL": [
+ ""
+ ],
+ "Initial balance": [
+ ""
+ ],
+ "balance prior to deposit": [
+ ""
+ ],
+ "Exchange URL": [
+ ""
+ ],
+ "URL of exchange": [
+ ""
+ ],
+ "Next": [
+ ""
+ ],
+ "Wire method": [
+ ""
+ ],
+ "method to use for wire transfer": [
+ ""
+ ],
+ "Select one wire method": [
+ ""
+ ],
+ "could not create reserve": [
+ ""
+ ],
+ "Valid until": [
+ ""
+ ],
+ "Created balance": [
+ ""
+ ],
+ "Exchange balance": [
+ ""
+ ],
+ "Picked up": [
+ ""
+ ],
+ "Committed": [
+ ""
+ ],
+ "Account address": [
+ ""
+ ],
+ "Subject": [
+ ""
+ ],
+ "Tips": [
+ ""
+ ],
+ "No tips has been authorized from this reserve": [
+ ""
+ ],
+ "Authorized": [
+ ""
+ ],
+ "Expiration": [
+ ""
+ ],
+ "amount of tip": [
+ ""
+ ],
+ "Justification": [
+ ""
+ ],
+ "reason for the tip": [
+ ""
+ ],
+ "URL after tip": [
+ ""
+ ],
+ "URL to visit after tip payment": [
+ ""
+ ],
+ "Reserves not yet funded": [
+ ""
+ ],
+ "Reserves ready": [
+ ""
+ ],
+ "add new reserve": [
+ ""
+ ],
+ "Expires at": [
+ ""
+ ],
+ "Initial": [
+ ""
+ ],
+ "delete selected reserve from the database": [
+ ""
+ ],
+ "authorize new tip from selected reserve": [
+ ""
+ ],
+ "There is no ready reserves yet, add more pressing the + sign or fund them": [
+ ""
+ ],
+ "Expected Balance": [
+ ""
+ ],
+ "could not create the tip": [
+ ""
+ ],
+ "should not be empty": [
+ ""
+ ],
+ "should be greater that 0": [
+ ""
+ ],
+ "can't be empty": [
+ ""
+ ],
+ "to short": [
+ ""
+ ],
+ "just letters and numbers from 2 to 7": [
+ ""
+ ],
+ "size of the key should be 32": [
+ ""
+ ],
+ "Identifier": [
+ ""
+ ],
+ "Name of the template in URLs.": [
+ ""
+ ],
+ "Describe what this template stands for": [
+ ""
+ ],
+ "Fixed summary": [
+ ""
+ ],
+ "If specified, this template will create order with the same summary": [
+ ""
+ ],
+ "Fixed price": [
+ ""
+ ],
+ "If specified, this template will create order with the same price": [
+ ""
+ ],
+ "Minimum age": [
+ ""
+ ],
+ "Is this contract restricted to some age?": [
+ ""
+ ],
+ "Payment timeout": [
+ ""
+ ],
+ "How much time has the customer to complete the payment once the order was created.": [
+ ""
+ ],
+ "Verification algorithm": [
+ ""
+ ],
+ "Algorithm to use to verify transaction in offline mode": [
+ ""
+ ],
+ "Point-of-sale key": [
+ ""
+ ],
+ "Useful to validate the purchase": [
+ ""
+ ],
+ "generate random secret key": [
+ ""
+ ],
+ "random": [
+ ""
+ ],
+ "show secret key": [
+ ""
+ ],
+ "hide secret key": [
+ ""
+ ],
+ "hide": [
+ ""
+ ],
+ "show": [
+ ""
+ ],
+ "could not inform template": [
+ ""
+ ],
+ "Amount is required": [
+ ""
+ ],
+ "Order summary is required": [
+ ""
+ ],
+ "New order for template": [
+ ""
+ ],
+ "Amount of the order": [
+ ""
+ ],
+ "Order summary": [
+ ""
+ ],
+ "could not create order from template": [
+ ""
+ ],
+ "Here you can specify a default value for fields that are not fixed. Default values can be edited by the customer before the payment.": [
+ ""
+ ],
+ "Fixed amount": [
+ ""
+ ],
+ "Default amount": [
+ ""
+ ],
+ "Default summary": [
+ ""
+ ],
+ "Print": [
+ ""
+ ],
+ "Setup TOTP": [
+ ""
+ ],
+ "Templates": [
+ ""
+ ],
+ "add new templates": [
+ ""
+ ],
+ "load more templates before the first one": [
+ ""
+ ],
+ "load newer templates": [
+ ""
+ ],
+ "delete selected templates from the database": [
+ ""
+ ],
+ "use template to create new order": [
+ ""
+ ],
+ "create qr code for the template": [
+ ""
+ ],
+ "load more templates after the last one": [
+ ""
+ ],
+ "load older templates": [
+ ""
+ ],
+ "There is no templates yet, add more pressing the + sign": [
+ ""
+ ],
+ "template delete successfully": [
+ ""
+ ],
+ "could not delete the template": [
+ ""
+ ],
+ "could not update template": [
+ ""
+ ],
+ "should be one of '%1$s'": [
+ ""
+ ],
+ "Webhook ID to use": [
+ ""
+ ],
+ "Event": [
+ ""
+ ],
+ "The event of the webhook: why the webhook is used": [
+ ""
+ ],
+ "Method": [
+ ""
+ ],
+ "Method used by the webhook": [
+ ""
+ ],
+ "URL": [
+ ""
+ ],
+ "URL of the webhook where the customer will be redirected": [
+ ""
+ ],
+ "Header": [
+ ""
+ ],
+ "Header template of the webhook": [
+ ""
+ ],
+ "Body": [
+ ""
+ ],
+ "Body template by the webhook": [
+ ""
+ ],
+ "Webhooks": [
+ ""
+ ],
+ "add new webhooks": [
+ ""
+ ],
+ "load more webhooks before the first one": [
+ ""
+ ],
+ "load newer webhooks": [
+ ""
+ ],
+ "Event type": [
+ ""
+ ],
+ "delete selected webhook from the database": [
+ ""
+ ],
+ "load more webhooks after the last one": [
+ ""
+ ],
+ "load older webhooks": [
+ ""
+ ],
+ "There is no webhooks yet, add more pressing the + sign": [
+ ""
+ ],
+ "webhook delete successfully": [
+ ""
+ ],
+ "could not delete the webhook": [
+ ""
+ ],
+ "check the id, does not look valid": [
+ ""
+ ],
+ "should have 52 characters, current %1$s": [
+ ""
+ ],
+ "URL doesn't have the right format": [
+ ""
+ ],
+ "Credited bank account": [
+ ""
+ ],
+ "Select one account": [
+ ""
+ ],
+ "Bank account of the merchant where the payment was received": [
+ ""
+ ],
+ "Wire transfer ID": [
+ ""
+ ],
+ "unique identifier of the wire transfer used by the exchange, must be 52 characters long": [
+ ""
+ ],
+ "Base URL of the exchange that made the transfer, should have been in the wire transfer subject": [
+ ""
+ ],
+ "Amount credited": [
+ ""
+ ],
+ "Actual amount that was wired to the merchant's bank account": [
+ ""
+ ],
+ "could not inform transfer": [
+ ""
+ ],
+ "Transfers": [
+ ""
+ ],
+ "add new transfer": [
+ ""
+ ],
+ "load more transfers before the first one": [
+ ""
+ ],
+ "load newer transfers": [
+ ""
+ ],
+ "Credit": [
+ ""
+ ],
+ "Confirmed": [
+ ""
+ ],
+ "Verified": [
+ ""
+ ],
+ "Executed at": [
+ ""
+ ],
+ "yes": [
+ ""
+ ],
+ "no": [
+ ""
+ ],
+ "unknown": [
+ ""
+ ],
+ "delete selected transfer from the database": [
+ ""
+ ],
+ "load more transfer after the last one": [
+ ""
+ ],
+ "load older transfers": [
+ ""
+ ],
+ "There is no transfer yet, add more pressing the + sign": [
+ ""
+ ],
+ "filter by account address": [
+ ""
+ ],
+ "only show wire transfers confirmed by the merchant": [
+ ""
+ ],
+ "only show wire transfers claimed by the exchange": [
+ ""
+ ],
+ "Unverified": [
+ ""
+ ],
+ "is not valid": [
+ ""
+ ],
+ "is not a number": [
+ ""
+ ],
+ "must be 1 or greater": [
+ ""
+ ],
+ "max 7 lines": [
+ ""
+ ],
+ "change authorization configuration": [
+ ""
+ ],
+ "Need to complete marked fields and choose authorization method": [
+ ""
+ ],
+ "This is not a valid bitcoin address.": [
+ ""
+ ],
+ "This is not a valid Ethereum address.": [
+ ""
+ ],
+ "IBAN numbers usually have more that 4 digits": [
+ ""
+ ],
+ "IBAN numbers usually have less that 34 digits": [
+ ""
+ ],
+ "IBAN country code not found": [
+ ""
+ ],
+ "IBAN number is not valid, checksum is wrong": [
+ ""
+ ],
+ "Target type": [
+ ""
+ ],
+ "Method to use for wire transfer": [
+ ""
+ ],
+ "Routing": [
+ ""
+ ],
+ "Routing number.": [
+ ""
+ ],
+ "Account": [
+ ""
+ ],
+ "Account number.": [
+ ""
+ ],
+ "Business Identifier Code.": [
+ ""
+ ],
+ "Bank Account Number.": [
+ ""
+ ],
+ "Unified Payment Interface.": [
+ ""
+ ],
+ "Bitcoin protocol.": [
+ ""
+ ],
+ "Ethereum protocol.": [
+ ""
+ ],
+ "Interledger protocol.": [
+ ""
+ ],
+ "Host": [
+ ""
+ ],
+ "Bank host.": [
+ ""
+ ],
+ "Bank account.": [
+ ""
+ ],
+ "Bank account owner's name.": [
+ ""
+ ],
+ "No accounts yet.": [
+ ""
+ ],
+ "Name of the instance in URLs. The 'default' instance is special in that it is used to administer other instances.": [
+ ""
+ ],
+ "Business name": [
+ ""
+ ],
+ "Legal name of the business represented by this instance.": [
+ ""
+ ],
+ "Email": [
+ ""
+ ],
+ "Contact email": [
+ ""
+ ],
+ "Website URL": [
+ ""
+ ],
+ "URL.": [
+ ""
+ ],
+ "Logo": [
+ ""
+ ],
+ "Logo image.": [
+ ""
+ ],
+ "Bank account": [
+ ""
+ ],
+ "URI specifying bank account for crediting revenue.": [
+ ""
+ ],
+ "Default max deposit fee": [
+ ""
+ ],
+ "Maximum deposit fees this merchant is willing to pay per order by default.": [
+ ""
+ ],
+ "Default max wire fee": [
+ ""
+ ],
+ "Maximum wire fees this merchant is willing to pay per wire transfer by default.": [
+ ""
+ ],
+ "Default wire fee amortization": [
+ ""
+ ],
+ "Number of orders excess wire transfer fees will be divided by to compute per order surcharge.": [
+ ""
+ ],
+ "Physical location of the merchant.": [
+ ""
+ ],
+ "Jurisdiction": [
+ ""
+ ],
+ "Jurisdiction for legal disputes with the merchant.": [
+ ""
+ ],
+ "Default payment delay": [
+ ""
+ ],
+ "Time customers have to pay an order before the offer expires by default.": [
+ ""
+ ],
+ "Default wire transfer delay": [
+ ""
+ ],
+ "Maximum time an exchange is allowed to delay wiring funds to the merchant, enabling it to aggregate smaller payments into larger wire transfers and reducing wire fees.": [
+ ""
+ ],
+ "Instance id": [
+ ""
+ ],
+ "Change the authorization method use for this instance.": [
+ ""
+ ],
+ "Manage access token": [
+ ""
+ ],
+ "Failed to create instance": [
+ ""
+ ],
+ "Login required": [
+ ""
+ ],
+ "Please enter your access token.": [
+ ""
+ ],
+ "Access Token": [
+ ""
+ ],
+ "The request to the backend take too long and was cancelled": [
+ ""
+ ],
+ "Diagnostic from %1$s is \"%2$s\"": [
+ ""
+ ],
+ "The backend reported a problem: HTTP status #%1$s": [
+ ""
+ ],
+ "Diagnostic from %1$s is '%2$s'": [
+ ""
+ ],
+ "Access denied": [
+ ""
+ ],
+ "The access token provided is invalid.": [
+ ""
+ ],
+ "No 'default' instance configured yet.": [
+ ""
+ ],
+ "Create a 'default' instance to begin using the merchant backoffice.": [
+ ""
+ ],
+ "The access token provided is invalid": [
+ ""
+ ],
+ "Hide for today": [
+ ""
+ ],
+ "Instance": [
+ ""
+ ],
+ "Settings": [
+ ""
+ ],
+ "Connection": [
+ ""
+ ],
+ "New": [
+ ""
+ ],
+ "List": [
+ ""
+ ],
+ "Log out": [
+ ""
+ ],
+ "Check your token is valid": [
+ ""
+ ],
+ "Couldn't access the server.": [
+ ""
+ ],
+ "Could not infer instance id from url %1$s": [
+ ""
+ ],
+ "Server not found": [
+ ""
+ ],
+ "Server response with an error code": [
+ ""
+ ],
+ "Got message %1$s from %2$s": [
+ ""
+ ],
+ "Response from server is unreadable, http status: %1$s": [
+ ""
+ ],
+ "Unexpected Error": [
+ ""
+ ],
+ "The value %1$s is invalid for a payment url": [
+ ""
+ ],
+ "add element to the list": [
+ ""
+ ],
+ "add": [
+ ""
+ ],
+ "Deleting": [
+ ""
+ ],
+ "Changing": [
+ ""
+ ],
+ "Order ID": [
+ ""
+ ],
+ "Payment URL": [
+ ""
+ ]
+ }
+ }
+};
+
+strings['it'] = {
+ "domain": "messages",
+ "locale_data": {
+ "messages": {
+ "": {
+ "domain": "messages",
+ "plural_forms": "nplurals=2; plural=(n != 1);",
+ "lang": ""
+ },
+ "Cancel": [
+ ""
+ ],
+ "%1$s": [
+ ""
+ ],
+ "Close": [
+ ""
+ ],
+ "Continue": [
+ ""
+ ],
+ "Clear": [
+ ""
+ ],
+ "Confirm": [
+ ""
+ ],
+ "is not the same as the current access token": [
+ ""
+ ],
+ "cannot be empty": [
+ ""
+ ],
+ "cannot be the same as the old token": [
+ ""
+ ],
+ "is not the same": [
+ ""
+ ],
+ "You are updating the access token from instance with id %1$s": [
+ ""
+ ],
+ "Old access token": [
+ ""
+ ],
+ "access token currently in use": [
+ ""
+ ],
+ "New access token": [
+ ""
+ ],
+ "next access token to be used": [
+ ""
+ ],
+ "Repeat access token": [
+ ""
+ ],
+ "confirm the same access token": [
+ ""
+ ],
+ "Clearing the access token will mean public access to the instance": [
+ ""
+ ],
+ "cannot be the same as the old access token": [
+ ""
+ ],
+ "You are setting the access token for the new instance": [
+ ""
+ ],
+ "With external authorization method no check will be done by the merchant backend": [
+ ""
+ ],
+ "Set external authorization": [
+ ""
+ ],
+ "Set access token": [
+ ""
+ ],
+ "Operation in progress...": [
+ ""
+ ],
+ "The operation will be automatically canceled after %1$s seconds": [
+ ""
+ ],
+ "Instances": [
+ ""
+ ],
+ "Delete": [
+ ""
+ ],
+ "add new instance": [
+ ""
+ ],
+ "ID": [
+ ""
+ ],
+ "Name": [
+ ""
+ ],
+ "Edit": [
+ ""
+ ],
+ "Purge": [
+ ""
+ ],
+ "There is no instances yet, add more pressing the + sign": [
+ ""
+ ],
+ "Only show active instances": [
+ ""
+ ],
+ "Active": [
+ ""
+ ],
+ "Only show deleted instances": [
+ ""
+ ],
+ "Deleted": [
+ ""
+ ],
+ "Show all instances": [
+ ""
+ ],
+ "All": [
+ ""
+ ],
+ "Instance \"%1$s\" (ID: %2$s) has been deleted": [
+ ""
+ ],
+ "Failed to delete instance": [
+ ""
+ ],
+ "Instance '%1$s' (ID: %2$s) has been disabled": [
+ ""
+ ],
+ "Failed to purge instance": [
+ ""
+ ],
+ "Pending KYC verification": [
+ ""
+ ],
+ "Timed out": [
+ ""
+ ],
+ "Exchange": [
+ ""
+ ],
+ "Target account": [
+ ""
+ ],
+ "KYC URL": [
+ ""
+ ],
+ "Code": [
+ ""
+ ],
+ "Http Status": [
+ ""
+ ],
+ "No pending kyc verification!": [
+ ""
+ ],
+ "change value to unknown date": [
+ ""
+ ],
+ "change value to empty": [
+ ""
+ ],
+ "clear": [
+ ""
+ ],
+ "change value to never": [
+ ""
+ ],
+ "never": [
+ ""
+ ],
+ "Country": [
+ ""
+ ],
+ "Address": [
+ ""
+ ],
+ "Building number": [
+ ""
+ ],
+ "Building name": [
+ ""
+ ],
+ "Street": [
+ ""
+ ],
+ "Post code": [
+ ""
+ ],
+ "Town location": [
+ ""
+ ],
+ "Town": [
+ ""
+ ],
+ "District": [
+ ""
+ ],
+ "Country subdivision": [
+ ""
+ ],
+ "Product id": [
+ ""
+ ],
+ "Description": [
+ ""
+ ],
+ "Product": [
+ ""
+ ],
+ "search products by it's description or id": [
+ ""
+ ],
+ "no products found with that description": [
+ ""
+ ],
+ "You must enter a valid product identifier.": [
+ ""
+ ],
+ "Quantity must be greater than 0!": [
+ ""
+ ],
+ "This quantity exceeds remaining stock. Currently, only %1$s units remain unreserved in stock.": [
+ ""
+ ],
+ "Quantity": [
+ ""
+ ],
+ "how many products will be added": [
+ ""
+ ],
+ "Add from inventory": [
+ ""
+ ],
+ "Image should be smaller than 1 MB": [
+ ""
+ ],
+ "Add": [
+ ""
+ ],
+ "Remove": [
+ ""
+ ],
+ "No taxes configured for this product.": [
+ ""
+ ],
+ "Amount": [
+ ""
+ ],
+ "Taxes can be in currencies that differ from the main currency used by the merchant.": [
+ ""
+ ],
+ "Enter currency and value separated with a colon, e.g. &quot;USD:2.3&quot;.": [
+ ""
+ ],
+ "Legal name of the tax, e.g. VAT or import duties.": [
+ ""
+ ],
+ "add tax to the tax list": [
+ ""
+ ],
+ "describe and add a product that is not in the inventory list": [
+ ""
+ ],
+ "Add custom product": [
+ ""
+ ],
+ "Complete information of the product": [
+ ""
+ ],
+ "Image": [
+ ""
+ ],
+ "photo of the product": [
+ ""
+ ],
+ "full product description": [
+ ""
+ ],
+ "Unit": [
+ ""
+ ],
+ "name of the product unit": [
+ ""
+ ],
+ "Price": [
+ ""
+ ],
+ "amount in the current currency": [
+ ""
+ ],
+ "Taxes": [
+ ""
+ ],
+ "image": [
+ ""
+ ],
+ "description": [
+ ""
+ ],
+ "quantity": [
+ ""
+ ],
+ "unit price": [
+ ""
+ ],
+ "total price": [
+ ""
+ ],
+ "required": [
+ ""
+ ],
+ "not valid": [
+ ""
+ ],
+ "must be greater than 0": [
+ ""
+ ],
+ "not a valid json": [
+ ""
+ ],
+ "should be in the future": [
+ ""
+ ],
+ "refund deadline cannot be before pay deadline": [
+ ""
+ ],
+ "wire transfer deadline cannot be before refund deadline": [
+ ""
+ ],
+ "wire transfer deadline cannot be before pay deadline": [
+ ""
+ ],
+ "should have a refund deadline": [
+ ""
+ ],
+ "auto refund cannot be after refund deadline": [
+ ""
+ ],
+ "Manage products in order": [
+ ""
+ ],
+ "Manage list of products in the order.": [
+ ""
+ ],
+ "Remove this product from the order.": [
+ ""
+ ],
+ "Total price": [
+ ""
+ ],
+ "total product price added up": [
+ ""
+ ],
+ "Amount to be paid by the customer": [
+ ""
+ ],
+ "Order price": [
+ ""
+ ],
+ "final order price": [
+ ""
+ ],
+ "Summary": [
+ ""
+ ],
+ "Title of the order to be shown to the customer": [
+ ""
+ ],
+ "Shipping and Fulfillment": [
+ ""
+ ],
+ "Delivery date": [
+ ""
+ ],
+ "Deadline for physical delivery assured by the merchant.": [
+ ""
+ ],
+ "Location": [
+ ""
+ ],
+ "address where the products will be delivered": [
+ ""
+ ],
+ "Fulfillment URL": [
+ ""
+ ],
+ "URL to which the user will be redirected after successful payment.": [
+ ""
+ ],
+ "Taler payment options": [
+ ""
+ ],
+ "Override default Taler payment settings for this order": [
+ ""
+ ],
+ "Payment deadline": [
+ ""
+ ],
+ "Deadline for the customer to pay for the offer before it expires. Inventory products will be reserved until this deadline.": [
+ ""
+ ],
+ "Refund deadline": [
+ ""
+ ],
+ "Time until which the order can be refunded by the merchant.": [
+ ""
+ ],
+ "Wire transfer deadline": [
+ ""
+ ],
+ "Deadline for the exchange to make the wire transfer.": [
+ ""
+ ],
+ "Auto-refund deadline": [
+ ""
+ ],
+ "Time until which the wallet will automatically check for refunds without user interaction.": [
+ ""
+ ],
+ "Maximum deposit fee": [
+ ""
+ ],
+ "Maximum deposit fees the merchant is willing to cover for this order. Higher deposit fees must be covered in full by the consumer.": [
+ ""
+ ],
+ "Maximum wire fee": [
+ ""
+ ],
+ "Maximum aggregate wire fees the merchant is willing to cover for this order. Wire fees exceeding this amount are to be covered by the customers.": [
+ ""
+ ],
+ "Wire fee amortization": [
+ ""
+ ],
+ "Factor by which wire fees exceeding the above threshold are divided to determine the share of excess wire fees to be paid explicitly by the consumer.": [
+ ""
+ ],
+ "Create token": [
+ ""
+ ],
+ "Uncheck this option if the merchant backend generated an order ID with enough entropy to prevent adversarial claims.": [
+ ""
+ ],
+ "Minimum age required": [
+ ""
+ ],
+ "Any value greater than 0 will limit the coins able be used to pay this contract. If empty the age restriction will be defined by the products": [
+ ""
+ ],
+ "Min age defined by the producs is %1$s": [
+ ""
+ ],
+ "Additional information": [
+ ""
+ ],
+ "Custom information to be included in the contract for this order.": [
+ ""
+ ],
+ "You must enter a value in JavaScript Object Notation (JSON).": [
+ ""
+ ],
+ "days": [
+ ""
+ ],
+ "hours": [
+ ""
+ ],
+ "minutes": [
+ ""
+ ],
+ "seconds": [
+ ""
+ ],
+ "forever": [
+ ""
+ ],
+ "%1$sM": [
+ ""
+ ],
+ "%1$sY": [
+ ""
+ ],
+ "%1$sd": [
+ ""
+ ],
+ "%1$sh": [
+ ""
+ ],
+ "%1$smin": [
+ ""
+ ],
+ "%1$ssec": [
+ ""
+ ],
+ "Orders": [
+ ""
+ ],
+ "create order": [
+ ""
+ ],
+ "load newer orders": [
+ ""
+ ],
+ "Date": [
+ ""
+ ],
+ "Refund": [
+ ""
+ ],
+ "copy url": [
+ ""
+ ],
+ "load older orders": [
+ ""
+ ],
+ "No orders have been found matching your query!": [
+ ""
+ ],
+ "duplicated": [
+ ""
+ ],
+ "invalid format": [
+ ""
+ ],
+ "this value exceed the refundable amount": [
+ ""
+ ],
+ "date": [
+ ""
+ ],
+ "amount": [
+ ""
+ ],
+ "reason": [
+ ""
+ ],
+ "amount to be refunded": [
+ ""
+ ],
+ "Max refundable:": [
+ ""
+ ],
+ "Reason": [
+ ""
+ ],
+ "Choose one...": [
+ ""
+ ],
+ "requested by the customer": [
+ ""
+ ],
+ "other": [
+ ""
+ ],
+ "why this order is being refunded": [
+ ""
+ ],
+ "more information to give context": [
+ ""
+ ],
+ "Contract Terms": [
+ ""
+ ],
+ "human-readable description of the whole purchase": [
+ ""
+ ],
+ "total price for the transaction": [
+ ""
+ ],
+ "URL for this purchase": [
+ ""
+ ],
+ "Max fee": [
+ ""
+ ],
+ "maximum total deposit fee accepted by the merchant for this contract": [
+ ""
+ ],
+ "Max wire fee": [
+ ""
+ ],
+ "maximum wire fee accepted by the merchant": [
+ ""
+ ],
+ "over how many customer transactions does the merchant expect to amortize wire fees on average": [
+ ""
+ ],
+ "Created at": [
+ ""
+ ],
+ "time when this contract was generated": [
+ ""
+ ],
+ "after this deadline has passed no refunds will be accepted": [
+ ""
+ ],
+ "after this deadline, the merchant won't accept payments for the contract": [
+ ""
+ ],
+ "transfer deadline for the exchange": [
+ ""
+ ],
+ "time indicating when the order should be delivered": [
+ ""
+ ],
+ "where the order will be delivered": [
+ ""
+ ],
+ "Auto-refund delay": [
+ ""
+ ],
+ "how long the wallet should try to get an automatic refund for the purchase": [
+ ""
+ ],
+ "Extra info": [
+ ""
+ ],
+ "extra data that is only interpreted by the merchant frontend": [
+ ""
+ ],
+ "Order": [
+ ""
+ ],
+ "claimed": [
+ ""
+ ],
+ "claimed at": [
+ ""
+ ],
+ "Timeline": [
+ ""
+ ],
+ "Payment details": [
+ ""
+ ],
+ "Order status": [
+ ""
+ ],
+ "Product list": [
+ ""
+ ],
+ "paid": [
+ ""
+ ],
+ "wired": [
+ ""
+ ],
+ "refunded": [
+ ""
+ ],
+ "refund order": [
+ ""
+ ],
+ "not refundable": [
+ ""
+ ],
+ "refund": [
+ ""
+ ],
+ "Refunded amount": [
+ ""
+ ],
+ "Refund taken": [
+ ""
+ ],
+ "Status URL": [
+ ""
+ ],
+ "Refund URI": [
+ ""
+ ],
+ "unpaid": [
+ ""
+ ],
+ "pay at": [
+ ""
+ ],
+ "created at": [
+ ""
+ ],
+ "Order status URL": [
+ ""
+ ],
+ "Payment URI": [
+ ""
+ ],
+ "Unknown order status. This is an error, please contact the administrator.": [
+ ""
+ ],
+ "Back": [
+ ""
+ ],
+ "refund created successfully": [
+ ""
+ ],
+ "could not create the refund": [
+ ""
+ ],
+ "select date to show nearby orders": [
+ ""
+ ],
+ "order id": [
+ ""
+ ],
+ "jump to order with the given order ID": [
+ ""
+ ],
+ "remove all filters": [
+ ""
+ ],
+ "only show paid orders": [
+ ""
+ ],
+ "Paid": [
+ ""
+ ],
+ "only show orders with refunds": [
+ ""
+ ],
+ "Refunded": [
+ ""
+ ],
+ "only show orders where customers paid, but wire payments from payment provider are still pending": [
+ ""
+ ],
+ "Not wired": [
+ ""
+ ],
+ "clear date filter": [
+ ""
+ ],
+ "date (YYYY/MM/DD)": [
+ ""
+ ],
+ "Enter an order id": [
+ ""
+ ],
+ "order not found": [
+ ""
+ ],
+ "could not get the order to refund": [
+ ""
+ ],
+ "Loading...": [
+ ""
+ ],
+ "click here to configure the stock of the product, leave it as is and the backend will not control stock": [
+ ""
+ ],
+ "Manage stock": [
+ ""
+ ],
+ "this product has been configured without stock control": [
+ ""
+ ],
+ "Infinite": [
+ ""
+ ],
+ "lost cannot be greater than current and incoming (max %1$s)": [
+ ""
+ ],
+ "Incoming": [
+ ""
+ ],
+ "Lost": [
+ ""
+ ],
+ "Current": [
+ ""
+ ],
+ "remove stock control for this product": [
+ ""
+ ],
+ "without stock": [
+ ""
+ ],
+ "Next restock": [
+ ""
+ ],
+ "Delivery address": [
+ ""
+ ],
+ "product identification to use in URLs (for internal use only)": [
+ ""
+ ],
+ "illustration of the product for customers": [
+ ""
+ ],
+ "product description for customers": [
+ ""
+ ],
+ "Age restricted": [
+ ""
+ ],
+ "is this product restricted for customer below certain age?": [
+ ""
+ ],
+ "unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 items, 5 meters) for customers": [
+ ""
+ ],
+ "sale price for customers, including taxes, for above units of the product": [
+ ""
+ ],
+ "Stock": [
+ ""
+ ],
+ "product inventory for products with finite supply (for internal use only)": [
+ ""
+ ],
+ "taxes included in the product price, exposed to customers": [
+ ""
+ ],
+ "Need to complete marked fields": [
+ ""
+ ],
+ "could not create product": [
+ ""
+ ],
+ "Products": [
+ ""
+ ],
+ "add product to inventory": [
+ ""
+ ],
+ "Sell": [
+ ""
+ ],
+ "Profit": [
+ ""
+ ],
+ "Sold": [
+ ""
+ ],
+ "free": [
+ ""
+ ],
+ "go to product update page": [
+ ""
+ ],
+ "Update": [
+ ""
+ ],
+ "remove this product from the database": [
+ ""
+ ],
+ "update the product with new price": [
+ ""
+ ],
+ "update product with new price": [
+ ""
+ ],
+ "add more elements to the inventory": [
+ ""
+ ],
+ "report elements lost in the inventory": [
+ ""
+ ],
+ "new price for the product": [
+ ""
+ ],
+ "the are value with errors": [
+ ""
+ ],
+ "update product with new stock and price": [
+ ""
+ ],
+ "There is no products yet, add more pressing the + sign": [
+ ""
+ ],
+ "product updated successfully": [
+ ""
+ ],
+ "could not update the product": [
+ ""
+ ],
+ "product delete successfully": [
+ ""
+ ],
+ "could not delete the product": [
+ ""
+ ],
+ "Product id:": [
+ ""
+ ],
+ "To complete the setup of the reserve, you must now initiate a wire transfer using the given wire transfer subject and crediting the specified amount to the indicated account of the exchange.": [
+ ""
+ ],
+ "If your system supports RFC 8905, you can do this by opening this URI:": [
+ ""
+ ],
+ "it should be greater than 0": [
+ ""
+ ],
+ "must be a valid URL": [
+ ""
+ ],
+ "Initial balance": [
+ ""
+ ],
+ "balance prior to deposit": [
+ ""
+ ],
+ "Exchange URL": [
+ ""
+ ],
+ "URL of exchange": [
+ ""
+ ],
+ "Next": [
+ ""
+ ],
+ "Wire method": [
+ ""
+ ],
+ "method to use for wire transfer": [
+ ""
+ ],
+ "Select one wire method": [
+ ""
+ ],
+ "could not create reserve": [
+ ""
+ ],
+ "Valid until": [
+ ""
+ ],
+ "Created balance": [
+ ""
+ ],
+ "Exchange balance": [
+ ""
+ ],
+ "Picked up": [
+ ""
+ ],
+ "Committed": [
+ ""
+ ],
+ "Account address": [
+ ""
+ ],
+ "Subject": [
+ ""
+ ],
+ "Tips": [
+ ""
+ ],
+ "No tips has been authorized from this reserve": [
+ ""
+ ],
+ "Authorized": [
+ ""
+ ],
+ "Expiration": [
+ ""
+ ],
+ "amount of tip": [
+ ""
+ ],
+ "Justification": [
+ ""
+ ],
+ "reason for the tip": [
+ ""
+ ],
+ "URL after tip": [
+ ""
+ ],
+ "URL to visit after tip payment": [
+ ""
+ ],
+ "Reserves not yet funded": [
+ ""
+ ],
+ "Reserves ready": [
+ ""
+ ],
+ "add new reserve": [
+ ""
+ ],
+ "Expires at": [
+ ""
+ ],
+ "Initial": [
+ ""
+ ],
+ "delete selected reserve from the database": [
+ ""
+ ],
+ "authorize new tip from selected reserve": [
+ ""
+ ],
+ "There is no ready reserves yet, add more pressing the + sign or fund them": [
+ ""
+ ],
+ "Expected Balance": [
+ ""
+ ],
+ "could not create the tip": [
+ ""
+ ],
+ "should not be empty": [
+ ""
+ ],
+ "should be greater that 0": [
+ ""
+ ],
+ "can't be empty": [
+ ""
+ ],
+ "to short": [
+ ""
+ ],
+ "just letters and numbers from 2 to 7": [
+ ""
+ ],
+ "size of the key should be 32": [
+ ""
+ ],
+ "Identifier": [
+ ""
+ ],
+ "Name of the template in URLs.": [
+ ""
+ ],
+ "Describe what this template stands for": [
+ ""
+ ],
+ "Fixed summary": [
+ ""
+ ],
+ "If specified, this template will create order with the same summary": [
+ ""
+ ],
+ "Fixed price": [
+ ""
+ ],
+ "If specified, this template will create order with the same price": [
+ ""
+ ],
+ "Minimum age": [
+ ""
+ ],
+ "Is this contract restricted to some age?": [
+ ""
+ ],
+ "Payment timeout": [
+ ""
+ ],
+ "How much time has the customer to complete the payment once the order was created.": [
+ ""
+ ],
+ "Verification algorithm": [
+ ""
+ ],
+ "Algorithm to use to verify transaction in offline mode": [
+ ""
+ ],
+ "Point-of-sale key": [
+ ""
+ ],
+ "Useful to validate the purchase": [
+ ""
+ ],
+ "generate random secret key": [
+ ""
+ ],
+ "random": [
+ ""
+ ],
+ "show secret key": [
+ ""
+ ],
+ "hide secret key": [
+ ""
+ ],
+ "hide": [
+ ""
+ ],
+ "show": [
+ ""
+ ],
+ "could not inform template": [
+ ""
+ ],
+ "Amount is required": [
+ ""
+ ],
+ "Order summary is required": [
+ ""
+ ],
+ "New order for template": [
+ ""
+ ],
+ "Amount of the order": [
+ ""
+ ],
+ "Order summary": [
+ ""
+ ],
+ "could not create order from template": [
+ ""
+ ],
+ "Here you can specify a default value for fields that are not fixed. Default values can be edited by the customer before the payment.": [
+ ""
+ ],
+ "Fixed amount": [
+ ""
+ ],
+ "Default amount": [
+ ""
+ ],
+ "Default summary": [
+ ""
+ ],
+ "Print": [
+ ""
+ ],
+ "Setup TOTP": [
+ ""
+ ],
+ "Templates": [
+ ""
+ ],
+ "add new templates": [
+ ""
+ ],
+ "load more templates before the first one": [
+ ""
+ ],
+ "load newer templates": [
+ ""
+ ],
+ "delete selected templates from the database": [
+ ""
+ ],
+ "use template to create new order": [
+ ""
+ ],
+ "create qr code for the template": [
+ ""
+ ],
+ "load more templates after the last one": [
+ ""
+ ],
+ "load older templates": [
+ ""
+ ],
+ "There is no templates yet, add more pressing the + sign": [
+ ""
+ ],
+ "template delete successfully": [
+ ""
+ ],
+ "could not delete the template": [
+ ""
+ ],
+ "could not update template": [
+ ""
+ ],
+ "should be one of '%1$s'": [
+ ""
+ ],
+ "Webhook ID to use": [
+ ""
+ ],
+ "Event": [
+ ""
+ ],
+ "The event of the webhook: why the webhook is used": [
+ ""
+ ],
+ "Method": [
+ ""
+ ],
+ "Method used by the webhook": [
+ ""
+ ],
+ "URL": [
+ ""
+ ],
+ "URL of the webhook where the customer will be redirected": [
+ ""
+ ],
+ "Header": [
+ ""
+ ],
+ "Header template of the webhook": [
+ ""
+ ],
+ "Body": [
+ ""
+ ],
+ "Body template by the webhook": [
+ ""
+ ],
+ "Webhooks": [
+ ""
+ ],
+ "add new webhooks": [
+ ""
+ ],
+ "load more webhooks before the first one": [
+ ""
+ ],
+ "load newer webhooks": [
+ ""
+ ],
+ "Event type": [
+ ""
+ ],
+ "delete selected webhook from the database": [
+ ""
+ ],
+ "load more webhooks after the last one": [
+ ""
+ ],
+ "load older webhooks": [
+ ""
+ ],
+ "There is no webhooks yet, add more pressing the + sign": [
+ ""
+ ],
+ "webhook delete successfully": [
+ ""
+ ],
+ "could not delete the webhook": [
+ ""
+ ],
+ "check the id, does not look valid": [
+ ""
+ ],
+ "should have 52 characters, current %1$s": [
+ ""
+ ],
+ "URL doesn't have the right format": [
+ ""
+ ],
+ "Credited bank account": [
+ ""
+ ],
+ "Select one account": [
+ ""
+ ],
+ "Bank account of the merchant where the payment was received": [
+ ""
+ ],
+ "Wire transfer ID": [
+ ""
+ ],
+ "unique identifier of the wire transfer used by the exchange, must be 52 characters long": [
+ ""
+ ],
+ "Base URL of the exchange that made the transfer, should have been in the wire transfer subject": [
+ ""
+ ],
+ "Amount credited": [
+ ""
+ ],
+ "Actual amount that was wired to the merchant's bank account": [
+ ""
+ ],
+ "could not inform transfer": [
+ ""
+ ],
+ "Transfers": [
+ ""
+ ],
+ "add new transfer": [
+ ""
+ ],
+ "load more transfers before the first one": [
+ ""
+ ],
+ "load newer transfers": [
+ ""
+ ],
+ "Credit": [
+ ""
+ ],
+ "Confirmed": [
+ ""
+ ],
+ "Verified": [
+ ""
+ ],
+ "Executed at": [
+ ""
+ ],
+ "yes": [
+ ""
+ ],
+ "no": [
+ ""
+ ],
+ "unknown": [
+ ""
+ ],
+ "delete selected transfer from the database": [
+ ""
+ ],
+ "load more transfer after the last one": [
+ ""
+ ],
+ "load older transfers": [
+ ""
+ ],
+ "There is no transfer yet, add more pressing the + sign": [
+ ""
+ ],
+ "filter by account address": [
+ ""
+ ],
+ "only show wire transfers confirmed by the merchant": [
+ ""
+ ],
+ "only show wire transfers claimed by the exchange": [
+ ""
+ ],
+ "Unverified": [
+ ""
+ ],
+ "is not valid": [
+ ""
+ ],
+ "is not a number": [
+ ""
+ ],
+ "must be 1 or greater": [
+ ""
+ ],
+ "max 7 lines": [
+ ""
+ ],
+ "change authorization configuration": [
+ ""
+ ],
+ "Need to complete marked fields and choose authorization method": [
+ ""
+ ],
+ "This is not a valid bitcoin address.": [
+ ""
+ ],
+ "This is not a valid Ethereum address.": [
+ ""
+ ],
+ "IBAN numbers usually have more that 4 digits": [
+ ""
+ ],
+ "IBAN numbers usually have less that 34 digits": [
+ ""
+ ],
+ "IBAN country code not found": [
+ ""
+ ],
+ "IBAN number is not valid, checksum is wrong": [
+ ""
+ ],
+ "Target type": [
+ ""
+ ],
+ "Method to use for wire transfer": [
+ ""
+ ],
+ "Routing": [
+ ""
+ ],
+ "Routing number.": [
+ ""
+ ],
+ "Account": [
+ ""
+ ],
+ "Account number.": [
+ ""
+ ],
+ "Business Identifier Code.": [
+ ""
+ ],
+ "Bank Account Number.": [
+ ""
+ ],
+ "Unified Payment Interface.": [
+ ""
+ ],
+ "Bitcoin protocol.": [
+ ""
+ ],
+ "Ethereum protocol.": [
+ ""
+ ],
+ "Interledger protocol.": [
+ ""
+ ],
+ "Host": [
+ ""
+ ],
+ "Bank host.": [
+ ""
+ ],
+ "Bank account.": [
+ ""
+ ],
+ "Bank account owner's name.": [
+ ""
+ ],
+ "No accounts yet.": [
+ ""
+ ],
+ "Name of the instance in URLs. The 'default' instance is special in that it is used to administer other instances.": [
+ ""
+ ],
+ "Business name": [
+ ""
+ ],
+ "Legal name of the business represented by this instance.": [
+ ""
+ ],
+ "Email": [
+ ""
+ ],
+ "Contact email": [
+ ""
+ ],
+ "Website URL": [
+ ""
+ ],
+ "URL.": [
+ ""
+ ],
+ "Logo": [
+ ""
+ ],
+ "Logo image.": [
+ ""
+ ],
+ "Bank account": [
+ ""
+ ],
+ "URI specifying bank account for crediting revenue.": [
+ ""
+ ],
+ "Default max deposit fee": [
+ ""
+ ],
+ "Maximum deposit fees this merchant is willing to pay per order by default.": [
+ ""
+ ],
+ "Default max wire fee": [
+ ""
+ ],
+ "Maximum wire fees this merchant is willing to pay per wire transfer by default.": [
+ ""
+ ],
+ "Default wire fee amortization": [
+ ""
+ ],
+ "Number of orders excess wire transfer fees will be divided by to compute per order surcharge.": [
+ ""
+ ],
+ "Physical location of the merchant.": [
+ ""
+ ],
+ "Jurisdiction": [
+ ""
+ ],
+ "Jurisdiction for legal disputes with the merchant.": [
+ ""
+ ],
+ "Default payment delay": [
+ ""
+ ],
+ "Time customers have to pay an order before the offer expires by default.": [
+ ""
+ ],
+ "Default wire transfer delay": [
+ ""
+ ],
+ "Maximum time an exchange is allowed to delay wiring funds to the merchant, enabling it to aggregate smaller payments into larger wire transfers and reducing wire fees.": [
+ ""
+ ],
+ "Instance id": [
+ ""
+ ],
+ "Change the authorization method use for this instance.": [
+ ""
+ ],
+ "Manage access token": [
+ ""
+ ],
+ "Failed to create instance": [
+ ""
+ ],
+ "Login required": [
+ ""
+ ],
+ "Please enter your access token.": [
+ ""
+ ],
+ "Access Token": [
+ ""
+ ],
+ "The request to the backend take too long and was cancelled": [
+ ""
+ ],
+ "Diagnostic from %1$s is \"%2$s\"": [
+ ""
+ ],
+ "The backend reported a problem: HTTP status #%1$s": [
+ ""
+ ],
+ "Diagnostic from %1$s is '%2$s'": [
+ ""
+ ],
+ "Access denied": [
+ ""
+ ],
+ "The access token provided is invalid.": [
+ ""
+ ],
+ "No 'default' instance configured yet.": [
+ ""
+ ],
+ "Create a 'default' instance to begin using the merchant backoffice.": [
+ ""
+ ],
+ "The access token provided is invalid": [
+ ""
+ ],
+ "Hide for today": [
+ ""
+ ],
+ "Instance": [
+ ""
+ ],
+ "Settings": [
+ ""
+ ],
+ "Connection": [
+ ""
+ ],
+ "New": [
+ ""
+ ],
+ "List": [
+ ""
+ ],
+ "Log out": [
+ ""
+ ],
+ "Check your token is valid": [
+ ""
+ ],
+ "Couldn't access the server.": [
+ ""
+ ],
+ "Could not infer instance id from url %1$s": [
+ ""
+ ],
+ "Server not found": [
+ ""
+ ],
+ "Server response with an error code": [
+ ""
+ ],
+ "Got message %1$s from %2$s": [
+ ""
+ ],
+ "Response from server is unreadable, http status: %1$s": [
+ ""
+ ],
+ "Unexpected Error": [
+ ""
+ ],
+ "The value %1$s is invalid for a payment url": [
+ ""
+ ],
+ "add element to the list": [
+ ""
+ ],
+ "add": [
+ ""
+ ],
+ "Deleting": [
+ ""
+ ],
+ "Changing": [
+ ""
+ ],
+ "Order ID": [
+ ""
+ ],
+ "Payment URL": [
+ ""
+ ]
+ }
+ }
+};
+
+strings['sv'] = {
+ "domain": "messages",
+ "locale_data": {
+ "messages": {
+ "": {
+ "domain": "messages",
+ "plural_forms": "nplurals=2; plural=(n != 1);",
+ "lang": ""
+ },
+ "Cancel": [
+ ""
+ ],
+ "%1$s": [
+ ""
+ ],
+ "Close": [
+ ""
+ ],
+ "Continue": [
+ ""
+ ],
+ "Clear": [
+ ""
+ ],
+ "Confirm": [
+ ""
+ ],
+ "is not the same as the current access token": [
+ ""
+ ],
+ "cannot be empty": [
+ ""
+ ],
+ "cannot be the same as the old token": [
+ ""
+ ],
+ "is not the same": [
+ ""
+ ],
+ "You are updating the access token from instance with id %1$s": [
+ ""
+ ],
+ "Old access token": [
+ ""
+ ],
+ "access token currently in use": [
+ ""
+ ],
+ "New access token": [
+ ""
+ ],
+ "next access token to be used": [
+ ""
+ ],
+ "Repeat access token": [
+ ""
+ ],
+ "confirm the same access token": [
+ ""
+ ],
+ "Clearing the access token will mean public access to the instance": [
+ ""
+ ],
+ "cannot be the same as the old access token": [
+ ""
+ ],
+ "You are setting the access token for the new instance": [
+ ""
+ ],
+ "With external authorization method no check will be done by the merchant backend": [
+ ""
+ ],
+ "Set external authorization": [
+ ""
+ ],
+ "Set access token": [
+ ""
+ ],
+ "Operation in progress...": [
+ ""
+ ],
+ "The operation will be automatically canceled after %1$s seconds": [
+ ""
+ ],
+ "Instances": [
+ ""
+ ],
+ "Delete": [
+ ""
+ ],
+ "add new instance": [
+ ""
+ ],
+ "ID": [
+ ""
+ ],
+ "Name": [
+ ""
+ ],
+ "Edit": [
+ ""
+ ],
+ "Purge": [
+ ""
+ ],
+ "There is no instances yet, add more pressing the + sign": [
+ ""
+ ],
+ "Only show active instances": [
+ ""
+ ],
+ "Active": [
+ ""
+ ],
+ "Only show deleted instances": [
+ ""
+ ],
+ "Deleted": [
+ ""
+ ],
+ "Show all instances": [
+ ""
+ ],
+ "All": [
+ ""
+ ],
+ "Instance \"%1$s\" (ID: %2$s) has been deleted": [
+ ""
+ ],
+ "Failed to delete instance": [
+ ""
+ ],
+ "Instance '%1$s' (ID: %2$s) has been disabled": [
+ ""
+ ],
+ "Failed to purge instance": [
+ ""
+ ],
+ "Pending KYC verification": [
+ ""
+ ],
+ "Timed out": [
+ ""
+ ],
+ "Exchange": [
+ ""
+ ],
+ "Target account": [
+ ""
+ ],
+ "KYC URL": [
+ ""
+ ],
+ "Code": [
+ ""
+ ],
+ "Http Status": [
+ ""
+ ],
+ "No pending kyc verification!": [
+ ""
+ ],
+ "change value to unknown date": [
+ ""
+ ],
+ "change value to empty": [
+ ""
+ ],
+ "clear": [
+ ""
+ ],
+ "change value to never": [
+ ""
+ ],
+ "never": [
+ ""
+ ],
+ "Country": [
+ ""
+ ],
+ "Address": [
+ ""
+ ],
+ "Building number": [
+ ""
+ ],
+ "Building name": [
+ ""
+ ],
+ "Street": [
+ ""
+ ],
+ "Post code": [
+ ""
+ ],
+ "Town location": [
+ ""
+ ],
+ "Town": [
+ ""
+ ],
+ "District": [
+ ""
+ ],
+ "Country subdivision": [
+ ""
+ ],
+ "Product id": [
+ ""
+ ],
+ "Description": [
+ ""
+ ],
+ "Product": [
+ ""
+ ],
+ "search products by it's description or id": [
+ ""
+ ],
+ "no products found with that description": [
+ ""
+ ],
+ "You must enter a valid product identifier.": [
+ ""
+ ],
+ "Quantity must be greater than 0!": [
+ ""
+ ],
+ "This quantity exceeds remaining stock. Currently, only %1$s units remain unreserved in stock.": [
+ ""
+ ],
+ "Quantity": [
+ ""
+ ],
+ "how many products will be added": [
+ ""
+ ],
+ "Add from inventory": [
+ ""
+ ],
+ "Image should be smaller than 1 MB": [
+ ""
+ ],
+ "Add": [
+ ""
+ ],
+ "Remove": [
+ ""
+ ],
+ "No taxes configured for this product.": [
+ ""
+ ],
+ "Amount": [
+ ""
+ ],
+ "Taxes can be in currencies that differ from the main currency used by the merchant.": [
+ ""
+ ],
+ "Enter currency and value separated with a colon, e.g. &quot;USD:2.3&quot;.": [
+ ""
+ ],
+ "Legal name of the tax, e.g. VAT or import duties.": [
+ ""
+ ],
+ "add tax to the tax list": [
+ ""
+ ],
+ "describe and add a product that is not in the inventory list": [
+ ""
+ ],
+ "Add custom product": [
+ ""
+ ],
+ "Complete information of the product": [
+ ""
+ ],
+ "Image": [
+ ""
+ ],
+ "photo of the product": [
+ ""
+ ],
+ "full product description": [
+ ""
+ ],
+ "Unit": [
+ ""
+ ],
+ "name of the product unit": [
+ ""
+ ],
+ "Price": [
+ ""
+ ],
+ "amount in the current currency": [
+ ""
+ ],
+ "Taxes": [
+ ""
+ ],
+ "image": [
+ ""
+ ],
+ "description": [
+ ""
+ ],
+ "quantity": [
+ ""
+ ],
+ "unit price": [
+ ""
+ ],
+ "total price": [
+ ""
+ ],
+ "required": [
+ ""
+ ],
+ "not valid": [
+ ""
+ ],
+ "must be greater than 0": [
+ ""
+ ],
+ "not a valid json": [
+ ""
+ ],
+ "should be in the future": [
+ ""
+ ],
+ "refund deadline cannot be before pay deadline": [
+ ""
+ ],
+ "wire transfer deadline cannot be before refund deadline": [
+ ""
+ ],
+ "wire transfer deadline cannot be before pay deadline": [
+ ""
+ ],
+ "should have a refund deadline": [
+ ""
+ ],
+ "auto refund cannot be after refund deadline": [
+ ""
+ ],
+ "Manage products in order": [
+ ""
+ ],
+ "Manage list of products in the order.": [
+ ""
+ ],
+ "Remove this product from the order.": [
+ ""
+ ],
+ "Total price": [
+ ""
+ ],
+ "total product price added up": [
+ ""
+ ],
+ "Amount to be paid by the customer": [
+ ""
+ ],
+ "Order price": [
+ ""
+ ],
+ "final order price": [
+ ""
+ ],
+ "Summary": [
+ ""
+ ],
+ "Title of the order to be shown to the customer": [
+ ""
+ ],
+ "Shipping and Fulfillment": [
+ ""
+ ],
+ "Delivery date": [
+ ""
+ ],
+ "Deadline for physical delivery assured by the merchant.": [
+ ""
+ ],
+ "Location": [
+ ""
+ ],
+ "address where the products will be delivered": [
+ ""
+ ],
+ "Fulfillment URL": [
+ ""
+ ],
+ "URL to which the user will be redirected after successful payment.": [
+ ""
+ ],
+ "Taler payment options": [
+ ""
+ ],
+ "Override default Taler payment settings for this order": [
+ ""
+ ],
+ "Payment deadline": [
+ ""
+ ],
+ "Deadline for the customer to pay for the offer before it expires. Inventory products will be reserved until this deadline.": [
+ ""
+ ],
+ "Refund deadline": [
+ ""
+ ],
+ "Time until which the order can be refunded by the merchant.": [
+ ""
+ ],
+ "Wire transfer deadline": [
+ ""
+ ],
+ "Deadline for the exchange to make the wire transfer.": [
+ ""
+ ],
+ "Auto-refund deadline": [
+ ""
+ ],
+ "Time until which the wallet will automatically check for refunds without user interaction.": [
+ ""
+ ],
+ "Maximum deposit fee": [
+ ""
+ ],
+ "Maximum deposit fees the merchant is willing to cover for this order. Higher deposit fees must be covered in full by the consumer.": [
+ ""
+ ],
+ "Maximum wire fee": [
+ ""
+ ],
+ "Maximum aggregate wire fees the merchant is willing to cover for this order. Wire fees exceeding this amount are to be covered by the customers.": [
+ ""
+ ],
+ "Wire fee amortization": [
+ ""
+ ],
+ "Factor by which wire fees exceeding the above threshold are divided to determine the share of excess wire fees to be paid explicitly by the consumer.": [
+ ""
+ ],
+ "Create token": [
+ ""
+ ],
+ "Uncheck this option if the merchant backend generated an order ID with enough entropy to prevent adversarial claims.": [
+ ""
+ ],
+ "Minimum age required": [
+ ""
+ ],
+ "Any value greater than 0 will limit the coins able be used to pay this contract. If empty the age restriction will be defined by the products": [
+ ""
+ ],
+ "Min age defined by the producs is %1$s": [
+ ""
+ ],
+ "Additional information": [
+ ""
+ ],
+ "Custom information to be included in the contract for this order.": [
+ ""
+ ],
+ "You must enter a value in JavaScript Object Notation (JSON).": [
+ ""
+ ],
+ "days": [
+ ""
+ ],
+ "hours": [
+ ""
+ ],
+ "minutes": [
+ ""
+ ],
+ "seconds": [
+ ""
+ ],
+ "forever": [
+ ""
+ ],
+ "%1$sM": [
+ ""
+ ],
+ "%1$sY": [
+ ""
+ ],
+ "%1$sd": [
+ ""
+ ],
+ "%1$sh": [
+ ""
+ ],
+ "%1$smin": [
+ ""
+ ],
+ "%1$ssec": [
+ ""
+ ],
+ "Orders": [
+ ""
+ ],
+ "create order": [
+ ""
+ ],
+ "load newer orders": [
+ ""
+ ],
+ "Date": [
+ ""
+ ],
+ "Refund": [
+ ""
+ ],
+ "copy url": [
+ ""
+ ],
+ "load older orders": [
+ ""
+ ],
+ "No orders have been found matching your query!": [
+ ""
+ ],
+ "duplicated": [
+ ""
+ ],
+ "invalid format": [
+ ""
+ ],
+ "this value exceed the refundable amount": [
+ ""
+ ],
+ "date": [
+ ""
+ ],
+ "amount": [
+ ""
+ ],
+ "reason": [
+ ""
+ ],
+ "amount to be refunded": [
+ ""
+ ],
+ "Max refundable:": [
+ ""
+ ],
+ "Reason": [
+ ""
+ ],
+ "Choose one...": [
+ ""
+ ],
+ "requested by the customer": [
+ ""
+ ],
+ "other": [
+ ""
+ ],
+ "why this order is being refunded": [
+ ""
+ ],
+ "more information to give context": [
+ ""
+ ],
+ "Contract Terms": [
+ ""
+ ],
+ "human-readable description of the whole purchase": [
+ ""
+ ],
+ "total price for the transaction": [
+ ""
+ ],
+ "URL for this purchase": [
+ ""
+ ],
+ "Max fee": [
+ ""
+ ],
+ "maximum total deposit fee accepted by the merchant for this contract": [
+ ""
+ ],
+ "Max wire fee": [
+ ""
+ ],
+ "maximum wire fee accepted by the merchant": [
+ ""
+ ],
+ "over how many customer transactions does the merchant expect to amortize wire fees on average": [
+ ""
+ ],
+ "Created at": [
+ ""
+ ],
+ "time when this contract was generated": [
+ ""
+ ],
+ "after this deadline has passed no refunds will be accepted": [
+ ""
+ ],
+ "after this deadline, the merchant won't accept payments for the contract": [
+ ""
+ ],
+ "transfer deadline for the exchange": [
+ ""
+ ],
+ "time indicating when the order should be delivered": [
+ ""
+ ],
+ "where the order will be delivered": [
+ ""
+ ],
+ "Auto-refund delay": [
+ ""
+ ],
+ "how long the wallet should try to get an automatic refund for the purchase": [
+ ""
+ ],
+ "Extra info": [
+ ""
+ ],
+ "extra data that is only interpreted by the merchant frontend": [
+ ""
+ ],
+ "Order": [
+ ""
+ ],
+ "claimed": [
+ ""
+ ],
+ "claimed at": [
+ ""
+ ],
+ "Timeline": [
+ ""
+ ],
+ "Payment details": [
+ ""
+ ],
+ "Order status": [
+ ""
+ ],
+ "Product list": [
+ ""
+ ],
+ "paid": [
+ ""
+ ],
+ "wired": [
+ ""
+ ],
+ "refunded": [
+ ""
+ ],
+ "refund order": [
+ ""
+ ],
+ "not refundable": [
+ ""
+ ],
+ "refund": [
+ ""
+ ],
+ "Refunded amount": [
+ ""
+ ],
+ "Refund taken": [
+ ""
+ ],
+ "Status URL": [
+ ""
+ ],
+ "Refund URI": [
+ ""
+ ],
+ "unpaid": [
+ ""
+ ],
+ "pay at": [
+ ""
+ ],
+ "created at": [
+ ""
+ ],
+ "Order status URL": [
+ ""
+ ],
+ "Payment URI": [
+ ""
+ ],
+ "Unknown order status. This is an error, please contact the administrator.": [
+ ""
+ ],
+ "Back": [
+ ""
+ ],
+ "refund created successfully": [
+ ""
+ ],
+ "could not create the refund": [
+ ""
+ ],
+ "select date to show nearby orders": [
+ ""
+ ],
+ "order id": [
+ ""
+ ],
+ "jump to order with the given order ID": [
+ ""
+ ],
+ "remove all filters": [
+ ""
+ ],
+ "only show paid orders": [
+ ""
+ ],
+ "Paid": [
+ ""
+ ],
+ "only show orders with refunds": [
+ ""
+ ],
+ "Refunded": [
+ ""
+ ],
+ "only show orders where customers paid, but wire payments from payment provider are still pending": [
+ ""
+ ],
+ "Not wired": [
+ ""
+ ],
+ "clear date filter": [
+ ""
+ ],
+ "date (YYYY/MM/DD)": [
+ ""
+ ],
+ "Enter an order id": [
+ ""
+ ],
+ "order not found": [
+ ""
+ ],
+ "could not get the order to refund": [
+ ""
+ ],
+ "Loading...": [
+ ""
+ ],
+ "click here to configure the stock of the product, leave it as is and the backend will not control stock": [
+ ""
+ ],
+ "Manage stock": [
+ ""
+ ],
+ "this product has been configured without stock control": [
+ ""
+ ],
+ "Infinite": [
+ ""
+ ],
+ "lost cannot be greater than current and incoming (max %1$s)": [
+ ""
+ ],
+ "Incoming": [
+ ""
+ ],
+ "Lost": [
+ ""
+ ],
+ "Current": [
+ ""
+ ],
+ "remove stock control for this product": [
+ ""
+ ],
+ "without stock": [
+ ""
+ ],
+ "Next restock": [
+ ""
+ ],
+ "Delivery address": [
+ ""
+ ],
+ "product identification to use in URLs (for internal use only)": [
+ ""
+ ],
+ "illustration of the product for customers": [
+ ""
+ ],
+ "product description for customers": [
+ ""
+ ],
+ "Age restricted": [
+ ""
+ ],
+ "is this product restricted for customer below certain age?": [
+ ""
+ ],
+ "unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 items, 5 meters) for customers": [
+ ""
+ ],
+ "sale price for customers, including taxes, for above units of the product": [
+ ""
+ ],
+ "Stock": [
+ ""
+ ],
+ "product inventory for products with finite supply (for internal use only)": [
+ ""
+ ],
+ "taxes included in the product price, exposed to customers": [
+ ""
+ ],
+ "Need to complete marked fields": [
+ ""
+ ],
+ "could not create product": [
+ ""
+ ],
+ "Products": [
+ ""
+ ],
+ "add product to inventory": [
+ ""
+ ],
+ "Sell": [
+ ""
+ ],
+ "Profit": [
+ ""
+ ],
+ "Sold": [
+ ""
+ ],
+ "free": [
+ ""
+ ],
+ "go to product update page": [
+ ""
+ ],
+ "Update": [
+ ""
+ ],
+ "remove this product from the database": [
+ ""
+ ],
+ "update the product with new price": [
+ ""
+ ],
+ "update product with new price": [
+ ""
+ ],
+ "add more elements to the inventory": [
+ ""
+ ],
+ "report elements lost in the inventory": [
+ ""
+ ],
+ "new price for the product": [
+ ""
+ ],
+ "the are value with errors": [
+ ""
+ ],
+ "update product with new stock and price": [
+ ""
+ ],
+ "There is no products yet, add more pressing the + sign": [
+ ""
+ ],
+ "product updated successfully": [
+ ""
+ ],
+ "could not update the product": [
+ ""
+ ],
+ "product delete successfully": [
+ ""
+ ],
+ "could not delete the product": [
+ ""
+ ],
+ "Product id:": [
+ ""
+ ],
+ "To complete the setup of the reserve, you must now initiate a wire transfer using the given wire transfer subject and crediting the specified amount to the indicated account of the exchange.": [
+ ""
+ ],
+ "If your system supports RFC 8905, you can do this by opening this URI:": [
+ ""
+ ],
+ "it should be greater than 0": [
+ ""
+ ],
+ "must be a valid URL": [
+ ""
+ ],
+ "Initial balance": [
+ ""
+ ],
+ "balance prior to deposit": [
+ ""
+ ],
+ "Exchange URL": [
+ ""
+ ],
+ "URL of exchange": [
+ ""
+ ],
+ "Next": [
+ ""
+ ],
+ "Wire method": [
+ ""
+ ],
+ "method to use for wire transfer": [
+ ""
+ ],
+ "Select one wire method": [
+ ""
+ ],
+ "could not create reserve": [
+ ""
+ ],
+ "Valid until": [
+ ""
+ ],
+ "Created balance": [
+ ""
+ ],
+ "Exchange balance": [
+ ""
+ ],
+ "Picked up": [
+ ""
+ ],
+ "Committed": [
+ ""
+ ],
+ "Account address": [
+ ""
+ ],
+ "Subject": [
+ ""
+ ],
+ "Tips": [
+ ""
+ ],
+ "No tips has been authorized from this reserve": [
+ ""
+ ],
+ "Authorized": [
+ ""
+ ],
+ "Expiration": [
+ ""
+ ],
+ "amount of tip": [
+ ""
+ ],
+ "Justification": [
+ ""
+ ],
+ "reason for the tip": [
+ ""
+ ],
+ "URL after tip": [
+ ""
+ ],
+ "URL to visit after tip payment": [
+ ""
+ ],
+ "Reserves not yet funded": [
+ ""
+ ],
+ "Reserves ready": [
+ ""
+ ],
+ "add new reserve": [
+ ""
+ ],
+ "Expires at": [
+ ""
+ ],
+ "Initial": [
+ ""
+ ],
+ "delete selected reserve from the database": [
+ ""
+ ],
+ "authorize new tip from selected reserve": [
+ ""
+ ],
+ "There is no ready reserves yet, add more pressing the + sign or fund them": [
+ ""
+ ],
+ "Expected Balance": [
+ ""
+ ],
+ "could not create the tip": [
+ ""
+ ],
+ "should not be empty": [
+ ""
+ ],
+ "should be greater that 0": [
+ ""
+ ],
+ "can't be empty": [
+ ""
+ ],
+ "to short": [
+ ""
+ ],
+ "just letters and numbers from 2 to 7": [
+ ""
+ ],
+ "size of the key should be 32": [
+ ""
+ ],
+ "Identifier": [
+ ""
+ ],
+ "Name of the template in URLs.": [
+ ""
+ ],
+ "Describe what this template stands for": [
+ ""
+ ],
+ "Fixed summary": [
+ ""
+ ],
+ "If specified, this template will create order with the same summary": [
+ ""
+ ],
+ "Fixed price": [
+ ""
+ ],
+ "If specified, this template will create order with the same price": [
+ ""
+ ],
+ "Minimum age": [
+ ""
+ ],
+ "Is this contract restricted to some age?": [
+ ""
+ ],
+ "Payment timeout": [
+ ""
+ ],
+ "How much time has the customer to complete the payment once the order was created.": [
+ ""
+ ],
+ "Verification algorithm": [
+ ""
+ ],
+ "Algorithm to use to verify transaction in offline mode": [
+ ""
+ ],
+ "Point-of-sale key": [
+ ""
+ ],
+ "Useful to validate the purchase": [
+ ""
+ ],
+ "generate random secret key": [
+ ""
+ ],
+ "random": [
+ ""
+ ],
+ "show secret key": [
+ ""
+ ],
+ "hide secret key": [
+ ""
+ ],
+ "hide": [
+ ""
+ ],
+ "show": [
+ ""
+ ],
+ "could not inform template": [
+ ""
+ ],
+ "Amount is required": [
+ ""
+ ],
+ "Order summary is required": [
+ ""
+ ],
+ "New order for template": [
+ ""
+ ],
+ "Amount of the order": [
+ ""
+ ],
+ "Order summary": [
+ ""
+ ],
+ "could not create order from template": [
+ ""
+ ],
+ "Here you can specify a default value for fields that are not fixed. Default values can be edited by the customer before the payment.": [
+ ""
+ ],
+ "Fixed amount": [
+ ""
+ ],
+ "Default amount": [
+ ""
+ ],
+ "Default summary": [
+ ""
+ ],
+ "Print": [
+ ""
+ ],
+ "Setup TOTP": [
+ ""
+ ],
+ "Templates": [
+ ""
+ ],
+ "add new templates": [
+ ""
+ ],
+ "load more templates before the first one": [
+ ""
+ ],
+ "load newer templates": [
+ ""
+ ],
+ "delete selected templates from the database": [
+ ""
+ ],
+ "use template to create new order": [
+ ""
+ ],
+ "create qr code for the template": [
+ ""
+ ],
+ "load more templates after the last one": [
+ ""
+ ],
+ "load older templates": [
+ ""
+ ],
+ "There is no templates yet, add more pressing the + sign": [
+ ""
+ ],
+ "template delete successfully": [
+ ""
+ ],
+ "could not delete the template": [
+ ""
+ ],
+ "could not update template": [
+ ""
+ ],
+ "should be one of '%1$s'": [
+ ""
+ ],
+ "Webhook ID to use": [
+ ""
+ ],
+ "Event": [
+ ""
+ ],
+ "The event of the webhook: why the webhook is used": [
+ ""
+ ],
+ "Method": [
+ ""
+ ],
+ "Method used by the webhook": [
+ ""
+ ],
+ "URL": [
+ ""
+ ],
+ "URL of the webhook where the customer will be redirected": [
+ ""
+ ],
+ "Header": [
+ ""
+ ],
+ "Header template of the webhook": [
+ ""
+ ],
+ "Body": [
+ ""
+ ],
+ "Body template by the webhook": [
+ ""
+ ],
+ "Webhooks": [
+ ""
+ ],
+ "add new webhooks": [
+ ""
+ ],
+ "load more webhooks before the first one": [
+ ""
+ ],
+ "load newer webhooks": [
+ ""
+ ],
+ "Event type": [
+ ""
+ ],
+ "delete selected webhook from the database": [
+ ""
+ ],
+ "load more webhooks after the last one": [
+ ""
+ ],
+ "load older webhooks": [
+ ""
+ ],
+ "There is no webhooks yet, add more pressing the + sign": [
+ ""
+ ],
+ "webhook delete successfully": [
+ ""
+ ],
+ "could not delete the webhook": [
+ ""
+ ],
+ "check the id, does not look valid": [
+ ""
+ ],
+ "should have 52 characters, current %1$s": [
+ ""
+ ],
+ "URL doesn't have the right format": [
+ ""
+ ],
+ "Credited bank account": [
+ ""
+ ],
+ "Select one account": [
+ ""
+ ],
+ "Bank account of the merchant where the payment was received": [
+ ""
+ ],
+ "Wire transfer ID": [
+ ""
+ ],
+ "unique identifier of the wire transfer used by the exchange, must be 52 characters long": [
+ ""
+ ],
+ "Base URL of the exchange that made the transfer, should have been in the wire transfer subject": [
+ ""
+ ],
+ "Amount credited": [
+ ""
+ ],
+ "Actual amount that was wired to the merchant's bank account": [
+ ""
+ ],
+ "could not inform transfer": [
+ ""
+ ],
+ "Transfers": [
+ ""
+ ],
+ "add new transfer": [
+ ""
+ ],
+ "load more transfers before the first one": [
+ ""
+ ],
+ "load newer transfers": [
+ ""
+ ],
+ "Credit": [
+ ""
+ ],
+ "Confirmed": [
+ ""
+ ],
+ "Verified": [
+ ""
+ ],
+ "Executed at": [
+ ""
+ ],
+ "yes": [
+ ""
+ ],
+ "no": [
+ ""
+ ],
+ "unknown": [
+ ""
+ ],
+ "delete selected transfer from the database": [
+ ""
+ ],
+ "load more transfer after the last one": [
+ ""
+ ],
+ "load older transfers": [
+ ""
+ ],
+ "There is no transfer yet, add more pressing the + sign": [
+ ""
+ ],
+ "filter by account address": [
+ ""
+ ],
+ "only show wire transfers confirmed by the merchant": [
+ ""
+ ],
+ "only show wire transfers claimed by the exchange": [
+ ""
+ ],
+ "Unverified": [
+ ""
+ ],
+ "is not valid": [
+ ""
+ ],
+ "is not a number": [
+ ""
+ ],
+ "must be 1 or greater": [
+ ""
+ ],
+ "max 7 lines": [
+ ""
+ ],
+ "change authorization configuration": [
+ ""
+ ],
+ "Need to complete marked fields and choose authorization method": [
+ ""
+ ],
+ "This is not a valid bitcoin address.": [
+ ""
+ ],
+ "This is not a valid Ethereum address.": [
+ ""
+ ],
+ "IBAN numbers usually have more that 4 digits": [
+ ""
+ ],
+ "IBAN numbers usually have less that 34 digits": [
+ ""
+ ],
+ "IBAN country code not found": [
+ ""
+ ],
+ "IBAN number is not valid, checksum is wrong": [
+ ""
+ ],
+ "Target type": [
+ ""
+ ],
+ "Method to use for wire transfer": [
+ ""
+ ],
+ "Routing": [
+ ""
+ ],
+ "Routing number.": [
+ ""
+ ],
+ "Account": [
+ ""
+ ],
+ "Account number.": [
+ ""
+ ],
+ "Business Identifier Code.": [
+ ""
+ ],
+ "Bank Account Number.": [
+ ""
+ ],
+ "Unified Payment Interface.": [
+ ""
+ ],
+ "Bitcoin protocol.": [
+ ""
+ ],
+ "Ethereum protocol.": [
+ ""
+ ],
+ "Interledger protocol.": [
+ ""
+ ],
+ "Host": [
+ ""
+ ],
+ "Bank host.": [
+ ""
+ ],
+ "Bank account.": [
+ ""
+ ],
+ "Bank account owner's name.": [
+ ""
+ ],
+ "No accounts yet.": [
+ ""
+ ],
+ "Name of the instance in URLs. The 'default' instance is special in that it is used to administer other instances.": [
+ ""
+ ],
+ "Business name": [
+ ""
+ ],
+ "Legal name of the business represented by this instance.": [
+ ""
+ ],
+ "Email": [
+ ""
+ ],
+ "Contact email": [
+ ""
+ ],
+ "Website URL": [
+ ""
+ ],
+ "URL.": [
+ ""
+ ],
+ "Logo": [
+ ""
+ ],
+ "Logo image.": [
+ ""
+ ],
+ "Bank account": [
+ ""
+ ],
+ "URI specifying bank account for crediting revenue.": [
+ ""
+ ],
+ "Default max deposit fee": [
+ ""
+ ],
+ "Maximum deposit fees this merchant is willing to pay per order by default.": [
+ ""
+ ],
+ "Default max wire fee": [
+ ""
+ ],
+ "Maximum wire fees this merchant is willing to pay per wire transfer by default.": [
+ ""
+ ],
+ "Default wire fee amortization": [
+ ""
+ ],
+ "Number of orders excess wire transfer fees will be divided by to compute per order surcharge.": [
+ ""
+ ],
+ "Physical location of the merchant.": [
+ ""
+ ],
+ "Jurisdiction": [
+ ""
+ ],
+ "Jurisdiction for legal disputes with the merchant.": [
+ ""
+ ],
+ "Default payment delay": [
+ ""
+ ],
+ "Time customers have to pay an order before the offer expires by default.": [
+ ""
+ ],
+ "Default wire transfer delay": [
+ ""
+ ],
+ "Maximum time an exchange is allowed to delay wiring funds to the merchant, enabling it to aggregate smaller payments into larger wire transfers and reducing wire fees.": [
+ ""
+ ],
+ "Instance id": [
+ ""
+ ],
+ "Change the authorization method use for this instance.": [
+ ""
+ ],
+ "Manage access token": [
+ ""
+ ],
+ "Failed to create instance": [
+ ""
+ ],
+ "Login required": [
+ ""
+ ],
+ "Please enter your access token.": [
+ ""
+ ],
+ "Access Token": [
+ ""
+ ],
+ "The request to the backend take too long and was cancelled": [
+ ""
+ ],
+ "Diagnostic from %1$s is \"%2$s\"": [
+ ""
+ ],
+ "The backend reported a problem: HTTP status #%1$s": [
+ ""
+ ],
+ "Diagnostic from %1$s is '%2$s'": [
+ ""
+ ],
+ "Access denied": [
+ ""
+ ],
+ "The access token provided is invalid.": [
+ ""
+ ],
+ "No 'default' instance configured yet.": [
+ ""
+ ],
+ "Create a 'default' instance to begin using the merchant backoffice.": [
+ ""
+ ],
+ "The access token provided is invalid": [
+ ""
+ ],
+ "Hide for today": [
+ ""
+ ],
+ "Instance": [
+ ""
+ ],
+ "Settings": [
+ ""
+ ],
+ "Connection": [
+ ""
+ ],
+ "New": [
+ ""
+ ],
+ "List": [
+ ""
+ ],
+ "Log out": [
+ ""
+ ],
+ "Check your token is valid": [
+ ""
+ ],
+ "Couldn't access the server.": [
+ ""
+ ],
+ "Could not infer instance id from url %1$s": [
+ ""
+ ],
+ "Server not found": [
+ ""
+ ],
+ "Server response with an error code": [
+ ""
+ ],
+ "Got message %1$s from %2$s": [
+ ""
+ ],
+ "Response from server is unreadable, http status: %1$s": [
+ ""
+ ],
+ "Unexpected Error": [
+ ""
+ ],
+ "The value %1$s is invalid for a payment url": [
+ ""
+ ],
+ "add element to the list": [
+ ""
+ ],
+ "add": [
+ ""
+ ],
+ "Deleting": [
+ ""
+ ],
+ "Changing": [
+ ""
+ ],
+ "Order ID": [
+ ""
+ ],
+ "Payment URL": [
+ ""
+ ]
+ }
+ }
+};
+
diff --git a/packages/auditor-backoffice-ui/src/i18n/sv.po b/packages/auditor-backoffice-ui/src/i18n/sv.po
new file mode 100644
index 000000000..d8d0bae29
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/i18n/sv.po
@@ -0,0 +1,2741 @@
+# This file is part of TALER
+# (C) 2016 GNUnet e.V.
+#
+# TALER is free software; you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 3, or (at your option) any later version.
+#
+# TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Wallet\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"POT-Creation-Date: 2016-11-23 00:00+0100\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: src/components/modal/index.tsx:71
+#, c-format
+msgid "Cancel"
+msgstr ""
+
+#: src/components/modal/index.tsx:79
+#, c-format
+msgid "%1$s"
+msgstr ""
+
+#: src/components/modal/index.tsx:84
+#, c-format
+msgid "Close"
+msgstr ""
+
+#: src/components/modal/index.tsx:124
+#, c-format
+msgid "Continue"
+msgstr ""
+
+#: src/components/modal/index.tsx:178
+#, c-format
+msgid "Clear"
+msgstr ""
+
+#: src/components/modal/index.tsx:190
+#, c-format
+msgid "Confirm"
+msgstr ""
+
+#: src/components/modal/index.tsx:296
+#, c-format
+msgid "is not the same as the current access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:299
+#, c-format
+msgid "cannot be empty"
+msgstr ""
+
+#: src/components/modal/index.tsx:301
+#, c-format
+msgid "cannot be the same as the old token"
+msgstr ""
+
+#: src/components/modal/index.tsx:305
+#, c-format
+msgid "is not the same"
+msgstr ""
+
+#: src/components/modal/index.tsx:315
+#, c-format
+msgid "You are updating the access token from instance with id %1$s"
+msgstr ""
+
+#: src/components/modal/index.tsx:331
+#, c-format
+msgid "Old access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:332
+#, c-format
+msgid "access token currently in use"
+msgstr ""
+
+#: src/components/modal/index.tsx:338
+#, c-format
+msgid "New access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:339
+#, c-format
+msgid "next access token to be used"
+msgstr ""
+
+#: src/components/modal/index.tsx:344
+#, c-format
+msgid "Repeat access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:345
+#, c-format
+msgid "confirm the same access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:350
+#, c-format
+msgid "Clearing the access token will mean public access to the instance"
+msgstr ""
+
+#: src/components/modal/index.tsx:377
+#, c-format
+msgid "cannot be the same as the old access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:394
+#, c-format
+msgid "You are setting the access token for the new instance"
+msgstr ""
+
+#: src/components/modal/index.tsx:420
+#, c-format
+msgid ""
+"With external authorization method no check will be done by the merchant "
+"backend"
+msgstr ""
+
+#: src/components/modal/index.tsx:436
+#, c-format
+msgid "Set external authorization"
+msgstr ""
+
+#: src/components/modal/index.tsx:448
+#, c-format
+msgid "Set access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:470
+#, c-format
+msgid "Operation in progress..."
+msgstr ""
+
+#: src/components/modal/index.tsx:479
+#, c-format
+msgid "The operation will be automatically canceled after %1$s seconds"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:80
+#, c-format
+msgid "Instances"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:93
+#, c-format
+msgid "Delete"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:99
+#, c-format
+msgid "add new instance"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:178
+#, c-format
+msgid "ID"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:181
+#, c-format
+msgid "Name"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:220
+#, c-format
+msgid "Edit"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:237
+#, c-format
+msgid "Purge"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:261
+#, c-format
+msgid "There is no instances yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:68
+#, c-format
+msgid "Only show active instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:71
+#, c-format
+msgid "Active"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:78
+#, c-format
+msgid "Only show deleted instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:81
+#, c-format
+msgid "Deleted"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:88
+#, c-format
+msgid "Show all instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:91
+#, c-format
+msgid "All"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:101
+#, c-format
+msgid "Instance \"%1$s\" (ID: %2$s) has been deleted"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:106
+#, c-format
+msgid "Failed to delete instance"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:124
+#, c-format
+msgid "Instance '%1$s' (ID: %2$s) has been disabled"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:129
+#, c-format
+msgid "Failed to purge instance"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:41
+#, c-format
+msgid "Pending KYC verification"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:66
+#, c-format
+msgid "Timed out"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:103
+#, c-format
+msgid "Exchange"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:106
+#, c-format
+msgid "Target account"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:109
+#, c-format
+msgid "KYC URL"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:144
+#, c-format
+msgid "Code"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:147
+#, c-format
+msgid "Http Status"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:177
+#, c-format
+msgid "No pending kyc verification!"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:123
+#, c-format
+msgid "change value to unknown date"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:124
+#, c-format
+msgid "change value to empty"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:131
+#, c-format
+msgid "clear"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:136
+#, c-format
+msgid "change value to never"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:141
+#, c-format
+msgid "never"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:29
+#, c-format
+msgid "Country"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:33
+#, c-format
+msgid "Address"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:39
+#, c-format
+msgid "Building number"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:41
+#, c-format
+msgid "Building name"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:42
+#, c-format
+msgid "Street"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:43
+#, c-format
+msgid "Post code"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:44
+#, c-format
+msgid "Town location"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:45
+#, c-format
+msgid "Town"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:46
+#, c-format
+msgid "District"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:49
+#, c-format
+msgid "Country subdivision"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:66
+#, c-format
+msgid "Product id"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:69
+#, c-format
+msgid "Description"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:94
+#, c-format
+msgid "Product"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:95
+#, c-format
+msgid "search products by it's description or id"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:151
+#, c-format
+msgid "no products found with that description"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:56
+#, c-format
+msgid "You must enter a valid product identifier."
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:64
+#, c-format
+msgid "Quantity must be greater than 0!"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:76
+#, c-format
+msgid ""
+"This quantity exceeds remaining stock. Currently, only %1$s units remain "
+"unreserved in stock."
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:109
+#, c-format
+msgid "Quantity"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:110
+#, c-format
+msgid "how many products will be added"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:117
+#, c-format
+msgid "Add from inventory"
+msgstr ""
+
+#: src/components/form/InputImage.tsx:105
+#, c-format
+msgid "Image should be smaller than 1 MB"
+msgstr ""
+
+#: src/components/form/InputImage.tsx:110
+#, c-format
+msgid "Add"
+msgstr ""
+
+#: src/components/form/InputImage.tsx:115
+#, c-format
+msgid "Remove"
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:113
+#, c-format
+msgid "No taxes configured for this product."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:119
+#, c-format
+msgid "Amount"
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:120
+#, c-format
+msgid ""
+"Taxes can be in currencies that differ from the main currency used by the "
+"merchant."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:122
+#, c-format
+msgid ""
+"Enter currency and value separated with a colon, e.g. &quot;USD:2.3&quot;."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:131
+#, c-format
+msgid "Legal name of the tax, e.g. VAT or import duties."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:137
+#, c-format
+msgid "add tax to the tax list"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:72
+#, c-format
+msgid "describe and add a product that is not in the inventory list"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:75
+#, c-format
+msgid "Add custom product"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:86
+#, c-format
+msgid "Complete information of the product"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:185
+#, c-format
+msgid "Image"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:186
+#, c-format
+msgid "photo of the product"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:192
+#, c-format
+msgid "full product description"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:196
+#, c-format
+msgid "Unit"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:197
+#, c-format
+msgid "name of the product unit"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:201
+#, c-format
+msgid "Price"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:202
+#, c-format
+msgid "amount in the current currency"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:211
+#, c-format
+msgid "Taxes"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:38
+#, c-format
+msgid "image"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:41
+#, c-format
+msgid "description"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:44
+#, c-format
+msgid "quantity"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:47
+#, c-format
+msgid "unit price"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:50
+#, c-format
+msgid "total price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:153
+#, c-format
+msgid "required"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:157
+#, c-format
+msgid "not valid"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:159
+#, c-format
+msgid "must be greater than 0"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:164
+#, c-format
+msgid "not a valid json"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:170
+#, c-format
+msgid "should be in the future"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:173
+#, c-format
+msgid "refund deadline cannot be before pay deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:179
+#, c-format
+msgid "wire transfer deadline cannot be before refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:190
+#, c-format
+msgid "wire transfer deadline cannot be before pay deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:197
+#, c-format
+msgid "should have a refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:202
+#, c-format
+msgid "auto refund cannot be after refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:360
+#, c-format
+msgid "Manage products in order"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:369
+#, c-format
+msgid "Manage list of products in the order."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:391
+#, c-format
+msgid "Remove this product from the order."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:415
+#, c-format
+msgid "Total price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:417
+#, c-format
+msgid "total product price added up"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:430
+#, c-format
+msgid "Amount to be paid by the customer"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:436
+#, c-format
+msgid "Order price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:437
+#, c-format
+msgid "final order price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:444
+#, c-format
+msgid "Summary"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:445
+#, c-format
+msgid "Title of the order to be shown to the customer"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:450
+#, c-format
+msgid "Shipping and Fulfillment"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:455
+#, c-format
+msgid "Delivery date"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:456
+#, c-format
+msgid "Deadline for physical delivery assured by the merchant."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:461
+#, c-format
+msgid "Location"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:462
+#, c-format
+msgid "address where the products will be delivered"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:469
+#, c-format
+msgid "Fulfillment URL"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:470
+#, c-format
+msgid "URL to which the user will be redirected after successful payment."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:476
+#, c-format
+msgid "Taler payment options"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:477
+#, c-format
+msgid "Override default Taler payment settings for this order"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:481
+#, c-format
+msgid "Payment deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:482
+#, c-format
+msgid ""
+"Deadline for the customer to pay for the offer before it expires. Inventory "
+"products will be reserved until this deadline."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:486
+#, c-format
+msgid "Refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:487
+#, c-format
+msgid "Time until which the order can be refunded by the merchant."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:491
+#, c-format
+msgid "Wire transfer deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:492
+#, c-format
+msgid "Deadline for the exchange to make the wire transfer."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:496
+#, c-format
+msgid "Auto-refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:497
+#, c-format
+msgid ""
+"Time until which the wallet will automatically check for refunds without "
+"user interaction."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:502
+#, c-format
+msgid "Maximum deposit fee"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:503
+#, c-format
+msgid ""
+"Maximum deposit fees the merchant is willing to cover for this order. Higher "
+"deposit fees must be covered in full by the consumer."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:507
+#, c-format
+msgid "Maximum wire fee"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:508
+#, c-format
+msgid ""
+"Maximum aggregate wire fees the merchant is willing to cover for this order. "
+"Wire fees exceeding this amount are to be covered by the customers."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:512
+#, c-format
+msgid "Wire fee amortization"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:513
+#, c-format
+msgid ""
+"Factor by which wire fees exceeding the above threshold are divided to "
+"determine the share of excess wire fees to be paid explicitly by the "
+"consumer."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:517
+#, c-format
+msgid "Create token"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:518
+#, c-format
+msgid ""
+"Uncheck this option if the merchant backend generated an order ID with "
+"enough entropy to prevent adversarial claims."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:522
+#, c-format
+msgid "Minimum age required"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:523
+#, c-format
+msgid ""
+"Any value greater than 0 will limit the coins able be used to pay this "
+"contract. If empty the age restriction will be defined by the products"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:526
+#, c-format
+msgid "Min age defined by the producs is %1$s"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:534
+#, c-format
+msgid "Additional information"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:535
+#, c-format
+msgid "Custom information to be included in the contract for this order."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:541
+#, c-format
+msgid "You must enter a value in JavaScript Object Notation (JSON)."
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:55
+#, c-format
+msgid "days"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:65
+#, c-format
+msgid "hours"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:76
+#, c-format
+msgid "minutes"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:87
+#, c-format
+msgid "seconds"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:53
+#, c-format
+msgid "forever"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:62
+#, c-format
+msgid "%1$sM"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:64
+#, c-format
+msgid "%1$sY"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:66
+#, c-format
+msgid "%1$sd"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:68
+#, c-format
+msgid "%1$sh"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:70
+#, c-format
+msgid "%1$smin"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:72
+#, c-format
+msgid "%1$ssec"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:75
+#, c-format
+msgid "Orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:81
+#, c-format
+msgid "create order"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:147
+#, c-format
+msgid "load newer orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:154
+#, c-format
+msgid "Date"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:200
+#, c-format
+msgid "Refund"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:209
+#, c-format
+msgid "copy url"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:225
+#, c-format
+msgid "load older orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:242
+#, c-format
+msgid "No orders have been found matching your query!"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:288
+#, c-format
+msgid "duplicated"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:299
+#, c-format
+msgid "invalid format"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:301
+#, c-format
+msgid "this value exceed the refundable amount"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:346
+#, c-format
+msgid "date"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:349
+#, c-format
+msgid "amount"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:352
+#, c-format
+msgid "reason"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:389
+#, c-format
+msgid "amount to be refunded"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:391
+#, c-format
+msgid "Max refundable:"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:396
+#, c-format
+msgid "Reason"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:397
+#, c-format
+msgid "Choose one..."
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:399
+#, c-format
+msgid "requested by the customer"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:400
+#, c-format
+msgid "other"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:403
+#, c-format
+msgid "why this order is being refunded"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:409
+#, c-format
+msgid "more information to give context"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:62
+#, c-format
+msgid "Contract Terms"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:68
+#, c-format
+msgid "human-readable description of the whole purchase"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:74
+#, c-format
+msgid "total price for the transaction"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:81
+#, c-format
+msgid "URL for this purchase"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:87
+#, c-format
+msgid "Max fee"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:88
+#, c-format
+msgid "maximum total deposit fee accepted by the merchant for this contract"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:93
+#, c-format
+msgid "Max wire fee"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:94
+#, c-format
+msgid "maximum wire fee accepted by the merchant"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:100
+#, c-format
+msgid ""
+"over how many customer transactions does the merchant expect to amortize "
+"wire fees on average"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:105
+#, c-format
+msgid "Created at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:106
+#, c-format
+msgid "time when this contract was generated"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:112
+#, c-format
+msgid "after this deadline has passed no refunds will be accepted"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:118
+#, c-format
+msgid ""
+"after this deadline, the merchant won't accept payments for the contract"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:124
+#, c-format
+msgid "transfer deadline for the exchange"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:130
+#, c-format
+msgid "time indicating when the order should be delivered"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:136
+#, c-format
+msgid "where the order will be delivered"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:144
+#, c-format
+msgid "Auto-refund delay"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:145
+#, c-format
+msgid ""
+"how long the wallet should try to get an automatic refund for the purchase"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:150
+#, c-format
+msgid "Extra info"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:151
+#, c-format
+msgid "extra data that is only interpreted by the merchant frontend"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:219
+#, c-format
+msgid "Order"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:221
+#, c-format
+msgid "claimed"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:247
+#, c-format
+msgid "claimed at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:265
+#, c-format
+msgid "Timeline"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:271
+#, c-format
+msgid "Payment details"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:291
+#, c-format
+msgid "Order status"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:301
+#, c-format
+msgid "Product list"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:451
+#, c-format
+msgid "paid"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:455
+#, c-format
+msgid "wired"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:460
+#, c-format
+msgid "refunded"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:480
+#, c-format
+msgid "refund order"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:481
+#, c-format
+msgid "not refundable"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:489
+#, c-format
+msgid "refund"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:553
+#, c-format
+msgid "Refunded amount"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:560
+#, c-format
+msgid "Refund taken"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:570
+#, c-format
+msgid "Status URL"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:583
+#, c-format
+msgid "Refund URI"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:636
+#, c-format
+msgid "unpaid"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:654
+#, c-format
+msgid "pay at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:666
+#, c-format
+msgid "created at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:707
+#, c-format
+msgid "Order status URL"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:711
+#, c-format
+msgid "Payment URI"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:740
+#, c-format
+msgid ""
+"Unknown order status. This is an error, please contact the administrator."
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:767
+#, c-format
+msgid "Back"
+msgstr ""
+
+#: src/paths/instance/orders/details/index.tsx:79
+#, c-format
+msgid "refund created successfully"
+msgstr ""
+
+#: src/paths/instance/orders/details/index.tsx:85
+#, c-format
+msgid "could not create the refund"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:78
+#, c-format
+msgid "select date to show nearby orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:94
+#, c-format
+msgid "order id"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:100
+#, c-format
+msgid "jump to order with the given order ID"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:122
+#, c-format
+msgid "remove all filters"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:132
+#, c-format
+msgid "only show paid orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:135
+#, c-format
+msgid "Paid"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:142
+#, c-format
+msgid "only show orders with refunds"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:145
+#, c-format
+msgid "Refunded"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:152
+#, c-format
+msgid ""
+"only show orders where customers paid, but wire payments from payment "
+"provider are still pending"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:155
+#, c-format
+msgid "Not wired"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:170
+#, c-format
+msgid "clear date filter"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:184
+#, c-format
+msgid "date (YYYY/MM/DD)"
+msgstr ""
+
+#: src/paths/instance/orders/list/index.tsx:103
+#, c-format
+msgid "Enter an order id"
+msgstr ""
+
+#: src/paths/instance/orders/list/index.tsx:111
+#, c-format
+msgid "order not found"
+msgstr ""
+
+#: src/paths/instance/orders/list/index.tsx:178
+#, c-format
+msgid "could not get the order to refund"
+msgstr ""
+
+#: src/components/exception/AsyncButton.tsx:43
+#, c-format
+msgid "Loading..."
+msgstr ""
+
+#: src/components/form/InputStock.tsx:99
+#, c-format
+msgid ""
+"click here to configure the stock of the product, leave it as is and the "
+"backend will not control stock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:109
+#, c-format
+msgid "Manage stock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:115
+#, c-format
+msgid "this product has been configured without stock control"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:119
+#, c-format
+msgid "Infinite"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:136
+#, c-format
+msgid "lost cannot be greater than current and incoming (max %1$s)"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:176
+#, c-format
+msgid "Incoming"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:177
+#, c-format
+msgid "Lost"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:192
+#, c-format
+msgid "Current"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:196
+#, c-format
+msgid "remove stock control for this product"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:202
+#, c-format
+msgid "without stock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:211
+#, c-format
+msgid "Next restock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:217
+#, c-format
+msgid "Delivery address"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:133
+#, c-format
+msgid "product identification to use in URLs (for internal use only)"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:139
+#, c-format
+msgid "illustration of the product for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:145
+#, c-format
+msgid "product description for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:149
+#, c-format
+msgid "Age restricted"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:150
+#, c-format
+msgid "is this product restricted for customer below certain age?"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:155
+#, c-format
+msgid ""
+"unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 "
+"items, 5 meters) for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:160
+#, c-format
+msgid ""
+"sale price for customers, including taxes, for above units of the product"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:164
+#, c-format
+msgid "Stock"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:166
+#, c-format
+msgid ""
+"product inventory for products with finite supply (for internal use only)"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:171
+#, c-format
+msgid "taxes included in the product price, exposed to customers"
+msgstr ""
+
+#: src/paths/instance/products/create/CreatePage.tsx:66
+#, c-format
+msgid "Need to complete marked fields"
+msgstr ""
+
+#: src/paths/instance/products/create/index.tsx:51
+#, c-format
+msgid "could not create product"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:68
+#, c-format
+msgid "Products"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:73
+#, c-format
+msgid "add product to inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:137
+#, c-format
+msgid "Sell"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:143
+#, c-format
+msgid "Profit"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:149
+#, c-format
+msgid "Sold"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:210
+#, c-format
+msgid "free"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:248
+#, c-format
+msgid "go to product update page"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:255
+#, c-format
+msgid "Update"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:260
+#, c-format
+msgid "remove this product from the database"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:331
+#, c-format
+msgid "update the product with new price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:341
+#, c-format
+msgid "update product with new price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:399
+#, c-format
+msgid "add more elements to the inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:404
+#, c-format
+msgid "report elements lost in the inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:409
+#, c-format
+msgid "new price for the product"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:421
+#, c-format
+msgid "the are value with errors"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:422
+#, c-format
+msgid "update product with new stock and price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:463
+#, c-format
+msgid "There is no products yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:86
+#, c-format
+msgid "product updated successfully"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:92
+#, c-format
+msgid "could not update the product"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:103
+#, c-format
+msgid "product delete successfully"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:109
+#, c-format
+msgid "could not delete the product"
+msgstr ""
+
+#: src/paths/instance/products/update/UpdatePage.tsx:56
+#, c-format
+msgid "Product id:"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:95
+#, c-format
+msgid ""
+"To complete the setup of the reserve, you must now initiate a wire transfer "
+"using the given wire transfer subject and crediting the specified amount to "
+"the indicated account of the exchange."
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:102
+#, c-format
+msgid "If your system supports RFC 8905, you can do this by opening this URI:"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:83
+#, c-format
+msgid "it should be greater than 0"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:88
+#, c-format
+msgid "must be a valid URL"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:107
+#, c-format
+msgid "Initial balance"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:108
+#, c-format
+msgid "balance prior to deposit"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:112
+#, c-format
+msgid "Exchange URL"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:113
+#, c-format
+msgid "URL of exchange"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:148
+#, c-format
+msgid "Next"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:186
+#, c-format
+msgid "Wire method"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:187
+#, c-format
+msgid "method to use for wire transfer"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:189
+#, c-format
+msgid "Select one wire method"
+msgstr ""
+
+#: src/paths/instance/reserves/create/index.tsx:62
+#, c-format
+msgid "could not create reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:77
+#, c-format
+msgid "Valid until"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:82
+#, c-format
+msgid "Created balance"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:99
+#, c-format
+msgid "Exchange balance"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:104
+#, c-format
+msgid "Picked up"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:109
+#, c-format
+msgid "Committed"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:116
+#, c-format
+msgid "Account address"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:119
+#, c-format
+msgid "Subject"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:130
+#, c-format
+msgid "Tips"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:193
+#, c-format
+msgid "No tips has been authorized from this reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:213
+#, c-format
+msgid "Authorized"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:222
+#, c-format
+msgid "Expiration"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:108
+#, c-format
+msgid "amount of tip"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:112
+#, c-format
+msgid "Justification"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:114
+#, c-format
+msgid "reason for the tip"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:118
+#, c-format
+msgid "URL after tip"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:119
+#, c-format
+msgid "URL to visit after tip payment"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:65
+#, c-format
+msgid "Reserves not yet funded"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:89
+#, c-format
+msgid "Reserves ready"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:95
+#, c-format
+msgid "add new reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:143
+#, c-format
+msgid "Expires at"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:146
+#, c-format
+msgid "Initial"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:202
+#, c-format
+msgid "delete selected reserve from the database"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:210
+#, c-format
+msgid "authorize new tip from selected reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:237
+#, c-format
+msgid ""
+"There is no ready reserves yet, add more pressing the + sign or fund them"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:264
+#, c-format
+msgid "Expected Balance"
+msgstr ""
+
+#: src/paths/instance/reserves/list/index.tsx:110
+#, c-format
+msgid "could not create the tip"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:77
+#, c-format
+msgid "should not be empty"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:93
+#, c-format
+msgid "should be greater that 0"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:96
+#, c-format
+msgid "can't be empty"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:100
+#, c-format
+msgid "to short"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:108
+#, c-format
+msgid "just letters and numbers from 2 to 7"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:110
+#, c-format
+msgid "size of the key should be 32"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:137
+#, c-format
+msgid "Identifier"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:138
+#, c-format
+msgid "Name of the template in URLs."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:144
+#, c-format
+msgid "Describe what this template stands for"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:149
+#, c-format
+msgid "Fixed summary"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:150
+#, c-format
+msgid "If specified, this template will create order with the same summary"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:154
+#, c-format
+msgid "Fixed price"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:155
+#, c-format
+msgid "If specified, this template will create order with the same price"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:159
+#, c-format
+msgid "Minimum age"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:161
+#, c-format
+msgid "Is this contract restricted to some age?"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:165
+#, c-format
+msgid "Payment timeout"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:167
+#, c-format
+msgid ""
+"How much time has the customer to complete the payment once the order was "
+"created."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:171
+#, c-format
+msgid "Verification algorithm"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:172
+#, c-format
+msgid "Algorithm to use to verify transaction in offline mode"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:180
+#, c-format
+msgid "Point-of-sale key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:182
+#, c-format
+msgid "Useful to validate the purchase"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:196
+#, c-format
+msgid "generate random secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:203
+#, c-format
+msgid "random"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:208
+#, c-format
+msgid "show secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:209
+#, c-format
+msgid "hide secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:216
+#, c-format
+msgid "hide"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:218
+#, c-format
+msgid "show"
+msgstr ""
+
+#: src/paths/instance/templates/create/index.tsx:52
+#, c-format
+msgid "could not inform template"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:54
+#, c-format
+msgid "Amount is required"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:58
+#, c-format
+msgid "Order summary is required"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:86
+#, c-format
+msgid "New order for template"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:108
+#, c-format
+msgid "Amount of the order"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:113
+#, c-format
+msgid "Order summary"
+msgstr ""
+
+#: src/paths/instance/templates/use/index.tsx:92
+#, c-format
+msgid "could not create order from template"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:131
+#, c-format
+msgid ""
+"Here you can specify a default value for fields that are not fixed. Default "
+"values can be edited by the customer before the payment."
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:148
+#, c-format
+msgid "Fixed amount"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:149
+#, c-format
+msgid "Default amount"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:161
+#, c-format
+msgid "Default summary"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:177
+#, c-format
+msgid "Print"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:184
+#, c-format
+msgid "Setup TOTP"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:65
+#, c-format
+msgid "Templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:70
+#, c-format
+msgid "add new templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:142
+#, c-format
+msgid "load more templates before the first one"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:146
+#, c-format
+msgid "load newer templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:181
+#, c-format
+msgid "delete selected templates from the database"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:188
+#, c-format
+msgid "use template to create new order"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:195
+#, c-format
+msgid "create qr code for the template"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:210
+#, c-format
+msgid "load more templates after the last one"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:214
+#, c-format
+msgid "load older templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:231
+#, c-format
+msgid "There is no templates yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/templates/list/index.tsx:104
+#, c-format
+msgid "template delete successfully"
+msgstr ""
+
+#: src/paths/instance/templates/list/index.tsx:110
+#, c-format
+msgid "could not delete the template"
+msgstr ""
+
+#: src/paths/instance/templates/update/index.tsx:90
+#, c-format
+msgid "could not update template"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:57
+#, c-format
+msgid "should be one of '%1$s'"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:85
+#, c-format
+msgid "Webhook ID to use"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:89
+#, c-format
+msgid "Event"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:90
+#, c-format
+msgid "The event of the webhook: why the webhook is used"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:94
+#, c-format
+msgid "Method"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:95
+#, c-format
+msgid "Method used by the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:99
+#, c-format
+msgid "URL"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:100
+#, c-format
+msgid "URL of the webhook where the customer will be redirected"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:104
+#, c-format
+msgid "Header"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:106
+#, c-format
+msgid "Header template of the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:111
+#, c-format
+msgid "Body"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:112
+#, c-format
+msgid "Body template by the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:61
+#, c-format
+msgid "Webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:66
+#, c-format
+msgid "add new webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:137
+#, c-format
+msgid "load more webhooks before the first one"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:141
+#, c-format
+msgid "load newer webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:151
+#, c-format
+msgid "Event type"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:176
+#, c-format
+msgid "delete selected webhook from the database"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:198
+#, c-format
+msgid "load more webhooks after the last one"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:202
+#, c-format
+msgid "load older webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:219
+#, c-format
+msgid "There is no webhooks yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/index.tsx:94
+#, c-format
+msgid "webhook delete successfully"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/index.tsx:100
+#, c-format
+msgid "could not delete the webhook"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:63
+#, c-format
+msgid "check the id, does not look valid"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:65
+#, c-format
+msgid "should have 52 characters, current %1$s"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:72
+#, c-format
+msgid "URL doesn't have the right format"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:98
+#, c-format
+msgid "Credited bank account"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:100
+#, c-format
+msgid "Select one account"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:101
+#, c-format
+msgid "Bank account of the merchant where the payment was received"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:105
+#, c-format
+msgid "Wire transfer ID"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:107
+#, c-format
+msgid ""
+"unique identifier of the wire transfer used by the exchange, must be 52 "
+"characters long"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:112
+#, c-format
+msgid ""
+"Base URL of the exchange that made the transfer, should have been in the "
+"wire transfer subject"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:117
+#, c-format
+msgid "Amount credited"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:118
+#, c-format
+msgid "Actual amount that was wired to the merchant's bank account"
+msgstr ""
+
+#: src/paths/instance/transfers/create/index.tsx:58
+#, c-format
+msgid "could not inform transfer"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:61
+#, c-format
+msgid "Transfers"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:66
+#, c-format
+msgid "add new transfer"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:129
+#, c-format
+msgid "load more transfers before the first one"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:133
+#, c-format
+msgid "load newer transfers"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:143
+#, c-format
+msgid "Credit"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:152
+#, c-format
+msgid "Confirmed"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:155
+#, c-format
+msgid "Verified"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:158
+#, c-format
+msgid "Executed at"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:171
+#, c-format
+msgid "yes"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:171
+#, c-format
+msgid "no"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:181
+#, c-format
+msgid "unknown"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:187
+#, c-format
+msgid "delete selected transfer from the database"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:202
+#, c-format
+msgid "load more transfer after the last one"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:206
+#, c-format
+msgid "load older transfers"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:223
+#, c-format
+msgid "There is no transfer yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:79
+#, c-format
+msgid "filter by account address"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:100
+#, c-format
+msgid "only show wire transfers confirmed by the merchant"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:110
+#, c-format
+msgid "only show wire transfers claimed by the exchange"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:113
+#, c-format
+msgid "Unverified"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:69
+#, c-format
+msgid "is not valid"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:94
+#, c-format
+msgid "is not a number"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:96
+#, c-format
+msgid "must be 1 or greater"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:107
+#, c-format
+msgid "max 7 lines"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:178
+#, c-format
+msgid "change authorization configuration"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:217
+#, c-format
+msgid "Need to complete marked fields and choose authorization method"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:82
+#, c-format
+msgid "This is not a valid bitcoin address."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:95
+#, c-format
+msgid "This is not a valid Ethereum address."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:118
+#, c-format
+msgid "IBAN numbers usually have more that 4 digits"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:120
+#, c-format
+msgid "IBAN numbers usually have less that 34 digits"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:128
+#, c-format
+msgid "IBAN country code not found"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:153
+#, c-format
+msgid "IBAN number is not valid, checksum is wrong"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:248
+#, c-format
+msgid "Target type"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:249
+#, c-format
+msgid "Method to use for wire transfer"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:258
+#, c-format
+msgid "Routing"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:259
+#, c-format
+msgid "Routing number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:263
+#, c-format
+msgid "Account"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:264
+#, c-format
+msgid "Account number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:273
+#, c-format
+msgid "Business Identifier Code."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:282
+#, c-format
+msgid "Bank Account Number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:292
+#, c-format
+msgid "Unified Payment Interface."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:301
+#, c-format
+msgid "Bitcoin protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:310
+#, c-format
+msgid "Ethereum protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:319
+#, c-format
+msgid "Interledger protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:328
+#, c-format
+msgid "Host"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:329
+#, c-format
+msgid "Bank host."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:334
+#, c-format
+msgid "Bank account."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:343
+#, c-format
+msgid "Bank account owner's name."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:370
+#, c-format
+msgid "No accounts yet."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:52
+#, c-format
+msgid ""
+"Name of the instance in URLs. The 'default' instance is special in that it "
+"is used to administer other instances."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:58
+#, c-format
+msgid "Business name"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:59
+#, c-format
+msgid "Legal name of the business represented by this instance."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:64
+#, c-format
+msgid "Email"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:65
+#, c-format
+msgid "Contact email"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:70
+#, c-format
+msgid "Website URL"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:71
+#, c-format
+msgid "URL."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:76
+#, c-format
+msgid "Logo"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:77
+#, c-format
+msgid "Logo image."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:82
+#, c-format
+msgid "Bank account"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:83
+#, c-format
+msgid "URI specifying bank account for crediting revenue."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:88
+#, c-format
+msgid "Default max deposit fee"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:89
+#, c-format
+msgid ""
+"Maximum deposit fees this merchant is willing to pay per order by default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:94
+#, c-format
+msgid "Default max wire fee"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:95
+#, c-format
+msgid ""
+"Maximum wire fees this merchant is willing to pay per wire transfer by "
+"default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:100
+#, c-format
+msgid "Default wire fee amortization"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:101
+#, c-format
+msgid ""
+"Number of orders excess wire transfer fees will be divided by to compute per "
+"order surcharge."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:107
+#, c-format
+msgid "Physical location of the merchant."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:114
+#, c-format
+msgid "Jurisdiction"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:115
+#, c-format
+msgid "Jurisdiction for legal disputes with the merchant."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:122
+#, c-format
+msgid "Default payment delay"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:124
+#, c-format
+msgid ""
+"Time customers have to pay an order before the offer expires by default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:129
+#, c-format
+msgid "Default wire transfer delay"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:130
+#, c-format
+msgid ""
+"Maximum time an exchange is allowed to delay wiring funds to the merchant, "
+"enabling it to aggregate smaller payments into larger wire transfers and "
+"reducing wire fees."
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:164
+#, c-format
+msgid "Instance id"
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:173
+#, c-format
+msgid "Change the authorization method use for this instance."
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:182
+#, c-format
+msgid "Manage access token"
+msgstr ""
+
+#: src/paths/instance/update/index.tsx:112
+#, c-format
+msgid "Failed to create instance"
+msgstr ""
+
+#: src/components/exception/login.tsx:74
+#, c-format
+msgid "Login required"
+msgstr ""
+
+#: src/components/exception/login.tsx:80
+#, c-format
+msgid "Please enter your access token."
+msgstr ""
+
+#: src/components/exception/login.tsx:108
+#, c-format
+msgid "Access Token"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:171
+#, c-format
+msgid "The request to the backend take too long and was cancelled"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:172
+#, c-format
+msgid "Diagnostic from %1$s is \"%2$s\""
+msgstr ""
+
+#: src/InstanceRoutes.tsx:178
+#, c-format
+msgid "The backend reported a problem: HTTP status #%1$s"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:179
+#, c-format
+msgid "Diagnostic from %1$s is '%2$s'"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:196
+#, c-format
+msgid "Access denied"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:197
+#, c-format
+msgid "The access token provided is invalid."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:212
+#, c-format
+msgid "No 'default' instance configured yet."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:213
+#, c-format
+msgid "Create a 'default' instance to begin using the merchant backoffice."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:630
+#, c-format
+msgid "The access token provided is invalid"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:664
+#, c-format
+msgid "Hide for today"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:82
+#, c-format
+msgid "Instance"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:91
+#, c-format
+msgid "Settings"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:167
+#, c-format
+msgid "Connection"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:209
+#, c-format
+msgid "New"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:219
+#, c-format
+msgid "List"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:234
+#, c-format
+msgid "Log out"
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:71
+#, c-format
+msgid "Check your token is valid"
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:90
+#, c-format
+msgid "Couldn't access the server."
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:91
+#, c-format
+msgid "Could not infer instance id from url %1$s"
+msgstr ""
+
+#: src/Application.tsx:104
+#, c-format
+msgid "Server not found"
+msgstr ""
+
+#: src/Application.tsx:118
+#, c-format
+msgid "Server response with an error code"
+msgstr ""
+
+#: src/Application.tsx:120
+#, c-format
+msgid "Got message %1$s from %2$s"
+msgstr ""
+
+#: src/Application.tsx:131
+#, c-format
+msgid "Response from server is unreadable, http status: %1$s"
+msgstr ""
+
+#: src/Application.tsx:144
+#, c-format
+msgid "Unexpected Error"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:101
+#, c-format
+msgid "The value %1$s is invalid for a payment url"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:110
+#, c-format
+msgid "add element to the list"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:112
+#, c-format
+msgid "add"
+msgstr ""
+
+#: src/components/form/InputSecured.tsx:37
+#, c-format
+msgid "Deleting"
+msgstr ""
+
+#: src/components/form/InputSecured.tsx:41
+#, c-format
+msgid "Changing"
+msgstr ""
+
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:87
+#, c-format
+msgid "Order ID"
+msgstr ""
+
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:101
+#, c-format
+msgid "Payment URL"
+msgstr ""
diff --git a/packages/auditor-backoffice-ui/src/i18n/taler-merchant-backoffice.pot b/packages/auditor-backoffice-ui/src/i18n/taler-merchant-backoffice.pot
new file mode 100644
index 000000000..5ef56ca05
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/i18n/taler-merchant-backoffice.pot
@@ -0,0 +1,2726 @@
+# This file is part of GNU Taler
+# (C) 2021-2023 Taler Systems S.A.
+
+# GNU Taler is free software; you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 3, or (at your option) any later version.
+
+# GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License along with
+# GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Wallet\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+#: src/components/modal/index.tsx:71
+#, c-format
+msgid "Cancel"
+msgstr ""
+
+#: src/components/modal/index.tsx:79
+#, c-format
+msgid "%1$s"
+msgstr ""
+
+#: src/components/modal/index.tsx:84
+#, c-format
+msgid "Close"
+msgstr ""
+
+#: src/components/modal/index.tsx:124
+#, c-format
+msgid "Continue"
+msgstr ""
+
+#: src/components/modal/index.tsx:178
+#, c-format
+msgid "Clear"
+msgstr ""
+
+#: src/components/modal/index.tsx:190
+#, c-format
+msgid "Confirm"
+msgstr ""
+
+#: src/components/modal/index.tsx:296
+#, c-format
+msgid "is not the same as the current access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:299
+#, c-format
+msgid "cannot be empty"
+msgstr ""
+
+#: src/components/modal/index.tsx:301
+#, c-format
+msgid "cannot be the same as the old token"
+msgstr ""
+
+#: src/components/modal/index.tsx:305
+#, c-format
+msgid "is not the same"
+msgstr ""
+
+#: src/components/modal/index.tsx:315
+#, c-format
+msgid "You are updating the access token from instance with id %1$s"
+msgstr ""
+
+#: src/components/modal/index.tsx:331
+#, c-format
+msgid "Old access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:332
+#, c-format
+msgid "access token currently in use"
+msgstr ""
+
+#: src/components/modal/index.tsx:338
+#, c-format
+msgid "New access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:339
+#, c-format
+msgid "next access token to be used"
+msgstr ""
+
+#: src/components/modal/index.tsx:344
+#, c-format
+msgid "Repeat access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:345
+#, c-format
+msgid "confirm the same access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:350
+#, c-format
+msgid "Clearing the access token will mean public access to the instance"
+msgstr ""
+
+#: src/components/modal/index.tsx:377
+#, c-format
+msgid "cannot be the same as the old access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:394
+#, c-format
+msgid "You are setting the access token for the new instance"
+msgstr ""
+
+#: src/components/modal/index.tsx:420
+#, c-format
+msgid "With external authorization method no check will be done by the merchant backend"
+msgstr ""
+
+#: src/components/modal/index.tsx:436
+#, c-format
+msgid "Set external authorization"
+msgstr ""
+
+#: src/components/modal/index.tsx:448
+#, c-format
+msgid "Set access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:470
+#, c-format
+msgid "Operation in progress..."
+msgstr ""
+
+#: src/components/modal/index.tsx:479
+#, c-format
+msgid "The operation will be automatically canceled after %1$s seconds"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:80
+#, c-format
+msgid "Instances"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:93
+#, c-format
+msgid "Delete"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:99
+#, c-format
+msgid "add new instance"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:178
+#, c-format
+msgid "ID"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:181
+#, c-format
+msgid "Name"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:220
+#, c-format
+msgid "Edit"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:237
+#, c-format
+msgid "Purge"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:261
+#, c-format
+msgid "There is no instances yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:68
+#, c-format
+msgid "Only show active instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:71
+#, c-format
+msgid "Active"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:78
+#, c-format
+msgid "Only show deleted instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:81
+#, c-format
+msgid "Deleted"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:88
+#, c-format
+msgid "Show all instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:91
+#, c-format
+msgid "All"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:101
+#, c-format
+msgid "Instance \"%1$s\" (ID: %2$s) has been deleted"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:106
+#, c-format
+msgid "Failed to delete instance"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:124
+#, c-format
+msgid "Instance '%1$s' (ID: %2$s) has been disabled"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:129
+#, c-format
+msgid "Failed to purge instance"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:41
+#, c-format
+msgid "Pending KYC verification"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:66
+#, c-format
+msgid "Timed out"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:103
+#, c-format
+msgid "Exchange"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:106
+#, c-format
+msgid "Target account"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:109
+#, c-format
+msgid "KYC URL"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:144
+#, c-format
+msgid "Code"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:147
+#, c-format
+msgid "Http Status"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:177
+#, c-format
+msgid "No pending kyc verification!"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:123
+#, c-format
+msgid "change value to unknown date"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:124
+#, c-format
+msgid "change value to empty"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:131
+#, c-format
+msgid "clear"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:136
+#, c-format
+msgid "change value to never"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:141
+#, c-format
+msgid "never"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:29
+#, c-format
+msgid "Country"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:33
+#, c-format
+msgid "Address"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:39
+#, c-format
+msgid "Building number"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:41
+#, c-format
+msgid "Building name"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:42
+#, c-format
+msgid "Street"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:43
+#, c-format
+msgid "Post code"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:44
+#, c-format
+msgid "Town location"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:45
+#, c-format
+msgid "Town"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:46
+#, c-format
+msgid "District"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:49
+#, c-format
+msgid "Country subdivision"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:66
+#, c-format
+msgid "Product id"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:69
+#, c-format
+msgid "Description"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:94
+#, c-format
+msgid "Product"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:95
+#, c-format
+msgid "search products by it's description or id"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:151
+#, c-format
+msgid "no products found with that description"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:56
+#, c-format
+msgid "You must enter a valid product identifier."
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:64
+#, c-format
+msgid "Quantity must be greater than 0!"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:76
+#, c-format
+msgid ""
+"This quantity exceeds remaining stock. Currently, only %1$s units remain "
+"unreserved in stock."
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:109
+#, c-format
+msgid "Quantity"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:110
+#, c-format
+msgid "how many products will be added"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:117
+#, c-format
+msgid "Add from inventory"
+msgstr ""
+
+#: src/components/form/InputImage.tsx:105
+#, c-format
+msgid "Image should be smaller than 1 MB"
+msgstr ""
+
+#: src/components/form/InputImage.tsx:110
+#, c-format
+msgid "Add"
+msgstr ""
+
+#: src/components/form/InputImage.tsx:115
+#, c-format
+msgid "Remove"
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:113
+#, c-format
+msgid "No taxes configured for this product."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:119
+#, c-format
+msgid "Amount"
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:120
+#, c-format
+msgid ""
+"Taxes can be in currencies that differ from the main currency used by the "
+"merchant."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:122
+#, c-format
+msgid "Enter currency and value separated with a colon, e.g. &quot;USD:2.3&quot;."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:131
+#, c-format
+msgid "Legal name of the tax, e.g. VAT or import duties."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:137
+#, c-format
+msgid "add tax to the tax list"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:72
+#, c-format
+msgid "describe and add a product that is not in the inventory list"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:75
+#, c-format
+msgid "Add custom product"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:86
+#, c-format
+msgid "Complete information of the product"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:185
+#, c-format
+msgid "Image"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:186
+#, c-format
+msgid "photo of the product"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:192
+#, c-format
+msgid "full product description"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:196
+#, c-format
+msgid "Unit"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:197
+#, c-format
+msgid "name of the product unit"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:201
+#, c-format
+msgid "Price"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:202
+#, c-format
+msgid "amount in the current currency"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:211
+#, c-format
+msgid "Taxes"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:38
+#, c-format
+msgid "image"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:41
+#, c-format
+msgid "description"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:44
+#, c-format
+msgid "quantity"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:47
+#, c-format
+msgid "unit price"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:50
+#, c-format
+msgid "total price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:153
+#, c-format
+msgid "required"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:157
+#, c-format
+msgid "not valid"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:159
+#, c-format
+msgid "must be greater than 0"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:164
+#, c-format
+msgid "not a valid json"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:170
+#, c-format
+msgid "should be in the future"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:173
+#, c-format
+msgid "refund deadline cannot be before pay deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:179
+#, c-format
+msgid "wire transfer deadline cannot be before refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:190
+#, c-format
+msgid "wire transfer deadline cannot be before pay deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:197
+#, c-format
+msgid "should have a refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:202
+#, c-format
+msgid "auto refund cannot be after refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:360
+#, c-format
+msgid "Manage products in order"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:369
+#, c-format
+msgid "Manage list of products in the order."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:391
+#, c-format
+msgid "Remove this product from the order."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:415
+#, c-format
+msgid "Total price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:417
+#, c-format
+msgid "total product price added up"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:430
+#, c-format
+msgid "Amount to be paid by the customer"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:436
+#, c-format
+msgid "Order price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:437
+#, c-format
+msgid "final order price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:444
+#, c-format
+msgid "Summary"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:445
+#, c-format
+msgid "Title of the order to be shown to the customer"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:450
+#, c-format
+msgid "Shipping and Fulfillment"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:455
+#, c-format
+msgid "Delivery date"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:456
+#, c-format
+msgid "Deadline for physical delivery assured by the merchant."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:461
+#, c-format
+msgid "Location"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:462
+#, c-format
+msgid "address where the products will be delivered"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:469
+#, c-format
+msgid "Fulfillment URL"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:470
+#, c-format
+msgid "URL to which the user will be redirected after successful payment."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:476
+#, c-format
+msgid "Taler payment options"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:477
+#, c-format
+msgid "Override default Taler payment settings for this order"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:481
+#, c-format
+msgid "Payment deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:482
+#, c-format
+msgid ""
+"Deadline for the customer to pay for the offer before it expires. Inventory "
+"products will be reserved until this deadline."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:486
+#, c-format
+msgid "Refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:487
+#, c-format
+msgid "Time until which the order can be refunded by the merchant."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:491
+#, c-format
+msgid "Wire transfer deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:492
+#, c-format
+msgid "Deadline for the exchange to make the wire transfer."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:496
+#, c-format
+msgid "Auto-refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:497
+#, c-format
+msgid ""
+"Time until which the wallet will automatically check for refunds without user "
+"interaction."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:502
+#, c-format
+msgid "Maximum deposit fee"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:503
+#, c-format
+msgid ""
+"Maximum deposit fees the merchant is willing to cover for this order. Higher "
+"deposit fees must be covered in full by the consumer."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:507
+#, c-format
+msgid "Maximum wire fee"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:508
+#, c-format
+msgid ""
+"Maximum aggregate wire fees the merchant is willing to cover for this order. "
+"Wire fees exceeding this amount are to be covered by the customers."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:512
+#, c-format
+msgid "Wire fee amortization"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:513
+#, c-format
+msgid ""
+"Factor by which wire fees exceeding the above threshold are divided to determine "
+"the share of excess wire fees to be paid explicitly by the consumer."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:517
+#, c-format
+msgid "Create token"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:518
+#, c-format
+msgid ""
+"Uncheck this option if the merchant backend generated an order ID with enough "
+"entropy to prevent adversarial claims."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:522
+#, c-format
+msgid "Minimum age required"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:523
+#, c-format
+msgid ""
+"Any value greater than 0 will limit the coins able be used to pay this contract. "
+"If empty the age restriction will be defined by the products"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:526
+#, c-format
+msgid "Min age defined by the producs is %1$s"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:534
+#, c-format
+msgid "Additional information"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:535
+#, c-format
+msgid "Custom information to be included in the contract for this order."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:541
+#, c-format
+msgid "You must enter a value in JavaScript Object Notation (JSON)."
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:55
+#, c-format
+msgid "days"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:65
+#, c-format
+msgid "hours"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:76
+#, c-format
+msgid "minutes"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:87
+#, c-format
+msgid "seconds"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:53
+#, c-format
+msgid "forever"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:62
+#, c-format
+msgid "%1$sM"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:64
+#, c-format
+msgid "%1$sY"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:66
+#, c-format
+msgid "%1$sd"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:68
+#, c-format
+msgid "%1$sh"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:70
+#, c-format
+msgid "%1$smin"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:72
+#, c-format
+msgid "%1$ssec"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:75
+#, c-format
+msgid "Orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:81
+#, c-format
+msgid "create order"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:147
+#, c-format
+msgid "load newer orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:154
+#, c-format
+msgid "Date"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:200
+#, c-format
+msgid "Refund"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:209
+#, c-format
+msgid "copy url"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:225
+#, c-format
+msgid "load older orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:242
+#, c-format
+msgid "No orders have been found matching your query!"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:288
+#, c-format
+msgid "duplicated"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:299
+#, c-format
+msgid "invalid format"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:301
+#, c-format
+msgid "this value exceed the refundable amount"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:346
+#, c-format
+msgid "date"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:349
+#, c-format
+msgid "amount"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:352
+#, c-format
+msgid "reason"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:389
+#, c-format
+msgid "amount to be refunded"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:391
+#, c-format
+msgid "Max refundable:"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:396
+#, c-format
+msgid "Reason"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:397
+#, c-format
+msgid "Choose one..."
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:399
+#, c-format
+msgid "requested by the customer"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:400
+#, c-format
+msgid "other"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:403
+#, c-format
+msgid "why this order is being refunded"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:409
+#, c-format
+msgid "more information to give context"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:62
+#, c-format
+msgid "Contract Terms"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:68
+#, c-format
+msgid "human-readable description of the whole purchase"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:74
+#, c-format
+msgid "total price for the transaction"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:81
+#, c-format
+msgid "URL for this purchase"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:87
+#, c-format
+msgid "Max fee"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:88
+#, c-format
+msgid "maximum total deposit fee accepted by the merchant for this contract"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:93
+#, c-format
+msgid "Max wire fee"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:94
+#, c-format
+msgid "maximum wire fee accepted by the merchant"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:100
+#, c-format
+msgid ""
+"over how many customer transactions does the merchant expect to amortize wire "
+"fees on average"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:105
+#, c-format
+msgid "Created at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:106
+#, c-format
+msgid "time when this contract was generated"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:112
+#, c-format
+msgid "after this deadline has passed no refunds will be accepted"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:118
+#, c-format
+msgid "after this deadline, the merchant won't accept payments for the contract"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:124
+#, c-format
+msgid "transfer deadline for the exchange"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:130
+#, c-format
+msgid "time indicating when the order should be delivered"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:136
+#, c-format
+msgid "where the order will be delivered"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:144
+#, c-format
+msgid "Auto-refund delay"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:145
+#, c-format
+msgid "how long the wallet should try to get an automatic refund for the purchase"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:150
+#, c-format
+msgid "Extra info"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:151
+#, c-format
+msgid "extra data that is only interpreted by the merchant frontend"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:219
+#, c-format
+msgid "Order"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:221
+#, c-format
+msgid "claimed"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:247
+#, c-format
+msgid "claimed at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:265
+#, c-format
+msgid "Timeline"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:271
+#, c-format
+msgid "Payment details"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:291
+#, c-format
+msgid "Order status"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:301
+#, c-format
+msgid "Product list"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:451
+#, c-format
+msgid "paid"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:455
+#, c-format
+msgid "wired"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:460
+#, c-format
+msgid "refunded"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:480
+#, c-format
+msgid "refund order"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:481
+#, c-format
+msgid "not refundable"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:489
+#, c-format
+msgid "refund"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:553
+#, c-format
+msgid "Refunded amount"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:560
+#, c-format
+msgid "Refund taken"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:570
+#, c-format
+msgid "Status URL"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:583
+#, c-format
+msgid "Refund URI"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:636
+#, c-format
+msgid "unpaid"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:654
+#, c-format
+msgid "pay at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:666
+#, c-format
+msgid "created at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:707
+#, c-format
+msgid "Order status URL"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:711
+#, c-format
+msgid "Payment URI"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:740
+#, c-format
+msgid "Unknown order status. This is an error, please contact the administrator."
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:767
+#, c-format
+msgid "Back"
+msgstr ""
+
+#: src/paths/instance/orders/details/index.tsx:79
+#, c-format
+msgid "refund created successfully"
+msgstr ""
+
+#: src/paths/instance/orders/details/index.tsx:85
+#, c-format
+msgid "could not create the refund"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:78
+#, c-format
+msgid "select date to show nearby orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:94
+#, c-format
+msgid "order id"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:100
+#, c-format
+msgid "jump to order with the given order ID"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:122
+#, c-format
+msgid "remove all filters"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:132
+#, c-format
+msgid "only show paid orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:135
+#, c-format
+msgid "Paid"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:142
+#, c-format
+msgid "only show orders with refunds"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:145
+#, c-format
+msgid "Refunded"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:152
+#, c-format
+msgid ""
+"only show orders where customers paid, but wire payments from payment provider "
+"are still pending"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:155
+#, c-format
+msgid "Not wired"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:170
+#, c-format
+msgid "clear date filter"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:184
+#, c-format
+msgid "date (YYYY/MM/DD)"
+msgstr ""
+
+#: src/paths/instance/orders/list/index.tsx:103
+#, c-format
+msgid "Enter an order id"
+msgstr ""
+
+#: src/paths/instance/orders/list/index.tsx:111
+#, c-format
+msgid "order not found"
+msgstr ""
+
+#: src/paths/instance/orders/list/index.tsx:178
+#, c-format
+msgid "could not get the order to refund"
+msgstr ""
+
+#: src/components/exception/AsyncButton.tsx:43
+#, c-format
+msgid "Loading..."
+msgstr ""
+
+#: src/components/form/InputStock.tsx:99
+#, c-format
+msgid ""
+"click here to configure the stock of the product, leave it as is and the backend "
+"will not control stock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:109
+#, c-format
+msgid "Manage stock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:115
+#, c-format
+msgid "this product has been configured without stock control"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:119
+#, c-format
+msgid "Infinite"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:136
+#, c-format
+msgid "lost cannot be greater than current and incoming (max %1$s)"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:176
+#, c-format
+msgid "Incoming"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:177
+#, c-format
+msgid "Lost"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:192
+#, c-format
+msgid "Current"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:196
+#, c-format
+msgid "remove stock control for this product"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:202
+#, c-format
+msgid "without stock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:211
+#, c-format
+msgid "Next restock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:217
+#, c-format
+msgid "Delivery address"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:133
+#, c-format
+msgid "product identification to use in URLs (for internal use only)"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:139
+#, c-format
+msgid "illustration of the product for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:145
+#, c-format
+msgid "product description for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:149
+#, c-format
+msgid "Age restricted"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:150
+#, c-format
+msgid "is this product restricted for customer below certain age?"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:155
+#, c-format
+msgid ""
+"unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 items, 5 "
+"meters) for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:160
+#, c-format
+msgid "sale price for customers, including taxes, for above units of the product"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:164
+#, c-format
+msgid "Stock"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:166
+#, c-format
+msgid "product inventory for products with finite supply (for internal use only)"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:171
+#, c-format
+msgid "taxes included in the product price, exposed to customers"
+msgstr ""
+
+#: src/paths/instance/products/create/CreatePage.tsx:66
+#, c-format
+msgid "Need to complete marked fields"
+msgstr ""
+
+#: src/paths/instance/products/create/index.tsx:51
+#, c-format
+msgid "could not create product"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:68
+#, c-format
+msgid "Products"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:73
+#, c-format
+msgid "add product to inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:137
+#, c-format
+msgid "Sell"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:143
+#, c-format
+msgid "Profit"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:149
+#, c-format
+msgid "Sold"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:210
+#, c-format
+msgid "free"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:248
+#, c-format
+msgid "go to product update page"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:255
+#, c-format
+msgid "Update"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:260
+#, c-format
+msgid "remove this product from the database"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:331
+#, c-format
+msgid "update the product with new price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:341
+#, c-format
+msgid "update product with new price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:399
+#, c-format
+msgid "add more elements to the inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:404
+#, c-format
+msgid "report elements lost in the inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:409
+#, c-format
+msgid "new price for the product"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:421
+#, c-format
+msgid "the are value with errors"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:422
+#, c-format
+msgid "update product with new stock and price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:463
+#, c-format
+msgid "There is no products yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:86
+#, c-format
+msgid "product updated successfully"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:92
+#, c-format
+msgid "could not update the product"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:103
+#, c-format
+msgid "product delete successfully"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:109
+#, c-format
+msgid "could not delete the product"
+msgstr ""
+
+#: src/paths/instance/products/update/UpdatePage.tsx:56
+#, c-format
+msgid "Product id:"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:95
+#, c-format
+msgid ""
+"To complete the setup of the reserve, you must now initiate a wire transfer "
+"using the given wire transfer subject and crediting the specified amount to the "
+"indicated account of the exchange."
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:102
+#, c-format
+msgid "If your system supports RFC 8905, you can do this by opening this URI:"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:83
+#, c-format
+msgid "it should be greater than 0"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:88
+#, c-format
+msgid "must be a valid URL"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:107
+#, c-format
+msgid "Initial balance"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:108
+#, c-format
+msgid "balance prior to deposit"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:112
+#, c-format
+msgid "Exchange URL"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:113
+#, c-format
+msgid "URL of exchange"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:148
+#, c-format
+msgid "Next"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:186
+#, c-format
+msgid "Wire method"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:187
+#, c-format
+msgid "method to use for wire transfer"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:189
+#, c-format
+msgid "Select one wire method"
+msgstr ""
+
+#: src/paths/instance/reserves/create/index.tsx:62
+#, c-format
+msgid "could not create reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:77
+#, c-format
+msgid "Valid until"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:82
+#, c-format
+msgid "Created balance"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:99
+#, c-format
+msgid "Exchange balance"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:104
+#, c-format
+msgid "Picked up"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:109
+#, c-format
+msgid "Committed"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:116
+#, c-format
+msgid "Account address"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:119
+#, c-format
+msgid "Subject"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:130
+#, c-format
+msgid "Tips"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:193
+#, c-format
+msgid "No tips has been authorized from this reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:213
+#, c-format
+msgid "Authorized"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:222
+#, c-format
+msgid "Expiration"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:108
+#, c-format
+msgid "amount of tip"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:112
+#, c-format
+msgid "Justification"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:114
+#, c-format
+msgid "reason for the tip"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:118
+#, c-format
+msgid "URL after tip"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:119
+#, c-format
+msgid "URL to visit after tip payment"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:65
+#, c-format
+msgid "Reserves not yet funded"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:89
+#, c-format
+msgid "Reserves ready"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:95
+#, c-format
+msgid "add new reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:143
+#, c-format
+msgid "Expires at"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:146
+#, c-format
+msgid "Initial"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:202
+#, c-format
+msgid "delete selected reserve from the database"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:210
+#, c-format
+msgid "authorize new tip from selected reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:237
+#, c-format
+msgid "There is no ready reserves yet, add more pressing the + sign or fund them"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:264
+#, c-format
+msgid "Expected Balance"
+msgstr ""
+
+#: src/paths/instance/reserves/list/index.tsx:110
+#, c-format
+msgid "could not create the tip"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:77
+#, c-format
+msgid "should not be empty"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:93
+#, c-format
+msgid "should be greater that 0"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:96
+#, c-format
+msgid "can't be empty"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:100
+#, c-format
+msgid "to short"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:108
+#, c-format
+msgid "just letters and numbers from 2 to 7"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:110
+#, c-format
+msgid "size of the key should be 32"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:137
+#, c-format
+msgid "Identifier"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:138
+#, c-format
+msgid "Name of the template in URLs."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:144
+#, c-format
+msgid "Describe what this template stands for"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:149
+#, c-format
+msgid "Fixed summary"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:150
+#, c-format
+msgid "If specified, this template will create order with the same summary"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:154
+#, c-format
+msgid "Fixed price"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:155
+#, c-format
+msgid "If specified, this template will create order with the same price"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:159
+#, c-format
+msgid "Minimum age"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:161
+#, c-format
+msgid "Is this contract restricted to some age?"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:165
+#, c-format
+msgid "Payment timeout"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:167
+#, c-format
+msgid ""
+"How much time has the customer to complete the payment once the order was "
+"created."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:171
+#, c-format
+msgid "Verification algorithm"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:172
+#, c-format
+msgid "Algorithm to use to verify transaction in offline mode"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:180
+#, c-format
+msgid "Point-of-sale key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:182
+#, c-format
+msgid "Useful to validate the purchase"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:196
+#, c-format
+msgid "generate random secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:203
+#, c-format
+msgid "random"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:208
+#, c-format
+msgid "show secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:209
+#, c-format
+msgid "hide secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:216
+#, c-format
+msgid "hide"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:218
+#, c-format
+msgid "show"
+msgstr ""
+
+#: src/paths/instance/templates/create/index.tsx:52
+#, c-format
+msgid "could not inform template"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:54
+#, c-format
+msgid "Amount is required"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:58
+#, c-format
+msgid "Order summary is required"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:86
+#, c-format
+msgid "New order for template"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:108
+#, c-format
+msgid "Amount of the order"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:113
+#, c-format
+msgid "Order summary"
+msgstr ""
+
+#: src/paths/instance/templates/use/index.tsx:92
+#, c-format
+msgid "could not create order from template"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:131
+#, c-format
+msgid ""
+"Here you can specify a default value for fields that are not fixed. Default "
+"values can be edited by the customer before the payment."
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:148
+#, c-format
+msgid "Fixed amount"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:149
+#, c-format
+msgid "Default amount"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:161
+#, c-format
+msgid "Default summary"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:177
+#, c-format
+msgid "Print"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:184
+#, c-format
+msgid "Setup TOTP"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:65
+#, c-format
+msgid "Templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:70
+#, c-format
+msgid "add new templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:142
+#, c-format
+msgid "load more templates before the first one"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:146
+#, c-format
+msgid "load newer templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:181
+#, c-format
+msgid "delete selected templates from the database"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:188
+#, c-format
+msgid "use template to create new order"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:195
+#, c-format
+msgid "create qr code for the template"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:210
+#, c-format
+msgid "load more templates after the last one"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:214
+#, c-format
+msgid "load older templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:231
+#, c-format
+msgid "There is no templates yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/templates/list/index.tsx:104
+#, c-format
+msgid "template delete successfully"
+msgstr ""
+
+#: src/paths/instance/templates/list/index.tsx:110
+#, c-format
+msgid "could not delete the template"
+msgstr ""
+
+#: src/paths/instance/templates/update/index.tsx:90
+#, c-format
+msgid "could not update template"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:57
+#, c-format
+msgid "should be one of '%1$s'"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:85
+#, c-format
+msgid "Webhook ID to use"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:89
+#, c-format
+msgid "Event"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:90
+#, c-format
+msgid "The event of the webhook: why the webhook is used"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:94
+#, c-format
+msgid "Method"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:95
+#, c-format
+msgid "Method used by the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:99
+#, c-format
+msgid "URL"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:100
+#, c-format
+msgid "URL of the webhook where the customer will be redirected"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:104
+#, c-format
+msgid "Header"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:106
+#, c-format
+msgid "Header template of the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:111
+#, c-format
+msgid "Body"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:112
+#, c-format
+msgid "Body template by the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:61
+#, c-format
+msgid "Webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:66
+#, c-format
+msgid "add new webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:137
+#, c-format
+msgid "load more webhooks before the first one"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:141
+#, c-format
+msgid "load newer webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:151
+#, c-format
+msgid "Event type"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:176
+#, c-format
+msgid "delete selected webhook from the database"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:198
+#, c-format
+msgid "load more webhooks after the last one"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:202
+#, c-format
+msgid "load older webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:219
+#, c-format
+msgid "There is no webhooks yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/index.tsx:94
+#, c-format
+msgid "webhook delete successfully"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/index.tsx:100
+#, c-format
+msgid "could not delete the webhook"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:63
+#, c-format
+msgid "check the id, does not look valid"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:65
+#, c-format
+msgid "should have 52 characters, current %1$s"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:72
+#, c-format
+msgid "URL doesn't have the right format"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:98
+#, c-format
+msgid "Credited bank account"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:100
+#, c-format
+msgid "Select one account"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:101
+#, c-format
+msgid "Bank account of the merchant where the payment was received"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:105
+#, c-format
+msgid "Wire transfer ID"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:107
+#, c-format
+msgid ""
+"unique identifier of the wire transfer used by the exchange, must be 52 "
+"characters long"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:112
+#, c-format
+msgid ""
+"Base URL of the exchange that made the transfer, should have been in the wire "
+"transfer subject"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:117
+#, c-format
+msgid "Amount credited"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:118
+#, c-format
+msgid "Actual amount that was wired to the merchant's bank account"
+msgstr ""
+
+#: src/paths/instance/transfers/create/index.tsx:58
+#, c-format
+msgid "could not inform transfer"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:61
+#, c-format
+msgid "Transfers"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:66
+#, c-format
+msgid "add new transfer"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:129
+#, c-format
+msgid "load more transfers before the first one"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:133
+#, c-format
+msgid "load newer transfers"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:143
+#, c-format
+msgid "Credit"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:152
+#, c-format
+msgid "Confirmed"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:155
+#, c-format
+msgid "Verified"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:158
+#, c-format
+msgid "Executed at"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:171
+#, c-format
+msgid "yes"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:171
+#, c-format
+msgid "no"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:181
+#, c-format
+msgid "unknown"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:187
+#, c-format
+msgid "delete selected transfer from the database"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:202
+#, c-format
+msgid "load more transfer after the last one"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:206
+#, c-format
+msgid "load older transfers"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:223
+#, c-format
+msgid "There is no transfer yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:79
+#, c-format
+msgid "filter by account address"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:100
+#, c-format
+msgid "only show wire transfers confirmed by the merchant"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:110
+#, c-format
+msgid "only show wire transfers claimed by the exchange"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:113
+#, c-format
+msgid "Unverified"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:69
+#, c-format
+msgid "is not valid"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:94
+#, c-format
+msgid "is not a number"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:96
+#, c-format
+msgid "must be 1 or greater"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:107
+#, c-format
+msgid "max 7 lines"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:178
+#, c-format
+msgid "change authorization configuration"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:217
+#, c-format
+msgid "Need to complete marked fields and choose authorization method"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:82
+#, c-format
+msgid "This is not a valid bitcoin address."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:95
+#, c-format
+msgid "This is not a valid Ethereum address."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:118
+#, c-format
+msgid "IBAN numbers usually have more that 4 digits"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:120
+#, c-format
+msgid "IBAN numbers usually have less that 34 digits"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:128
+#, c-format
+msgid "IBAN country code not found"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:153
+#, c-format
+msgid "IBAN number is not valid, checksum is wrong"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:248
+#, c-format
+msgid "Target type"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:249
+#, c-format
+msgid "Method to use for wire transfer"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:258
+#, c-format
+msgid "Routing"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:259
+#, c-format
+msgid "Routing number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:263
+#, c-format
+msgid "Account"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:264
+#, c-format
+msgid "Account number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:273
+#, c-format
+msgid "Business Identifier Code."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:282
+#, c-format
+msgid "Bank Account Number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:292
+#, c-format
+msgid "Unified Payment Interface."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:301
+#, c-format
+msgid "Bitcoin protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:310
+#, c-format
+msgid "Ethereum protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:319
+#, c-format
+msgid "Interledger protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:328
+#, c-format
+msgid "Host"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:329
+#, c-format
+msgid "Bank host."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:334
+#, c-format
+msgid "Bank account."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:343
+#, c-format
+msgid "Bank account owner's name."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:370
+#, c-format
+msgid "No accounts yet."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:52
+#, c-format
+msgid ""
+"Name of the instance in URLs. The 'default' instance is special in that it is "
+"used to administer other instances."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:58
+#, c-format
+msgid "Business name"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:59
+#, c-format
+msgid "Legal name of the business represented by this instance."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:64
+#, c-format
+msgid "Email"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:65
+#, c-format
+msgid "Contact email"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:70
+#, c-format
+msgid "Website URL"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:71
+#, c-format
+msgid "URL."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:76
+#, c-format
+msgid "Logo"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:77
+#, c-format
+msgid "Logo image."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:82
+#, c-format
+msgid "Bank account"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:83
+#, c-format
+msgid "URI specifying bank account for crediting revenue."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:88
+#, c-format
+msgid "Default max deposit fee"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:89
+#, c-format
+msgid "Maximum deposit fees this merchant is willing to pay per order by default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:94
+#, c-format
+msgid "Default max wire fee"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:95
+#, c-format
+msgid "Maximum wire fees this merchant is willing to pay per wire transfer by default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:100
+#, c-format
+msgid "Default wire fee amortization"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:101
+#, c-format
+msgid ""
+"Number of orders excess wire transfer fees will be divided by to compute per "
+"order surcharge."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:107
+#, c-format
+msgid "Physical location of the merchant."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:114
+#, c-format
+msgid "Jurisdiction"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:115
+#, c-format
+msgid "Jurisdiction for legal disputes with the merchant."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:122
+#, c-format
+msgid "Default payment delay"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:124
+#, c-format
+msgid "Time customers have to pay an order before the offer expires by default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:129
+#, c-format
+msgid "Default wire transfer delay"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:130
+#, c-format
+msgid ""
+"Maximum time an exchange is allowed to delay wiring funds to the merchant, "
+"enabling it to aggregate smaller payments into larger wire transfers and "
+"reducing wire fees."
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:164
+#, c-format
+msgid "Instance id"
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:173
+#, c-format
+msgid "Change the authorization method use for this instance."
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:182
+#, c-format
+msgid "Manage access token"
+msgstr ""
+
+#: src/paths/instance/update/index.tsx:112
+#, c-format
+msgid "Failed to create instance"
+msgstr ""
+
+#: src/components/exception/login.tsx:74
+#, c-format
+msgid "Login required"
+msgstr ""
+
+#: src/components/exception/login.tsx:80
+#, c-format
+msgid "Please enter your access token."
+msgstr ""
+
+#: src/components/exception/login.tsx:108
+#, c-format
+msgid "Access Token"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:171
+#, c-format
+msgid "The request to the backend take too long and was cancelled"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:172
+#, c-format
+msgid "Diagnostic from %1$s is \"%2$s\""
+msgstr ""
+
+#: src/InstanceRoutes.tsx:178
+#, c-format
+msgid "The backend reported a problem: HTTP status #%1$s"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:179
+#, c-format
+msgid "Diagnostic from %1$s is '%2$s'"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:196
+#, c-format
+msgid "Access denied"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:197
+#, c-format
+msgid "The access token provided is invalid."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:212
+#, c-format
+msgid "No 'default' instance configured yet."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:213
+#, c-format
+msgid "Create a 'default' instance to begin using the merchant backoffice."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:630
+#, c-format
+msgid "The access token provided is invalid"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:664
+#, c-format
+msgid "Hide for today"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:82
+#, c-format
+msgid "Instance"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:91
+#, c-format
+msgid "Settings"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:167
+#, c-format
+msgid "Connection"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:209
+#, c-format
+msgid "New"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:219
+#, c-format
+msgid "List"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:234
+#, c-format
+msgid "Log out"
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:71
+#, c-format
+msgid "Check your token is valid"
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:90
+#, c-format
+msgid "Couldn't access the server."
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:91
+#, c-format
+msgid "Could not infer instance id from url %1$s"
+msgstr ""
+
+#: src/Application.tsx:104
+#, c-format
+msgid "Server not found"
+msgstr ""
+
+#: src/Application.tsx:118
+#, c-format
+msgid "Server response with an error code"
+msgstr ""
+
+#: src/Application.tsx:120
+#, c-format
+msgid "Got message %1$s from %2$s"
+msgstr ""
+
+#: src/Application.tsx:131
+#, c-format
+msgid "Response from server is unreadable, http status: %1$s"
+msgstr ""
+
+#: src/Application.tsx:144
+#, c-format
+msgid "Unexpected Error"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:101
+#, c-format
+msgid "The value %1$s is invalid for a payment url"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:110
+#, c-format
+msgid "add element to the list"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:112
+#, c-format
+msgid "add"
+msgstr ""
+
+#: src/components/form/InputSecured.tsx:37
+#, c-format
+msgid "Deleting"
+msgstr ""
+
+#: src/components/form/InputSecured.tsx:41
+#, c-format
+msgid "Changing"
+msgstr ""
+
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:87
+#, c-format
+msgid "Order ID"
+msgstr ""
+
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:101
+#, c-format
+msgid "Payment URL"
+msgstr ""
+
diff --git a/packages/auditor-backoffice-ui/src/index.html b/packages/auditor-backoffice-ui/src/index.html
new file mode 100644
index 000000000..c73dd1936
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/index.html
@@ -0,0 +1,45 @@
+<!--
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+
+ @author Sebastian Javier Marchano
+-->
+<!DOCTYPE html>
+<html
+ lang="en"
+ class="has-aside-left has-aside-mobile-transition has-navbar-fixed-top has-aside-expanded"
+>
+ <head>
+ <meta charset="utf-8" />
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
+ <meta name="mobile-web-app-capable" content="yes" />
+ <meta name="apple-mobile-web-app-capable" content="yes" />
+
+ <link
+ rel="icon"
+ href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/Qn3//3ryn/795Tf/WrpP/2LCW/8B6T//w4Nb///////Pn4P+/d0v/9u3n/+7d0v/EhV7//v///+HDr//fxLD/zph2/+TJt//8/Pv/woBX//Lm3f/y5dz/v3hN//bu6f/JjGn/4sW0///////Df1j/8OLZ//v6+P+/elH/+vj1//jy7f+/elL//////+zYzP/Eg13//////967p//MlHT/wn5X///////v4Nb/yY1s///////jw7H/06KG////////////z5t9/+fNvf//////x4pn//Pp4v/8+vn/w39X/8WEX///////5s/A/9CbfP//////27Oc/9y2n////////////9itlf/gu6f//////86Vdf/r2Mz//////8SCXP/Df1j//////+7d0v/KkG7//////+HBrf/VpYr////////////RnoH/5sq6///////Ii2n/8ubf//39/P/Cf1j/xohk/+bNvv//////wn5W//Tq4//58/D/wHxV//7+/f/59fH/v3xU//39/P/w4Nf/xIFb///////hw7H/yo9t/+/f1f/AeU3/+/n2/+nSxP/FhmD//////9qzm//Upon/4MSx/96+qf//////xINc/+3bz//48e3/v3hN//Pn3///////6M+//752S//gw6//06aK/8J+VP/kzLr/zZd1/8OCWv/q18r/17KZ/9Ooi//fv6r/v3dK/+vWyP///////v39///////27un/1aeK/9Opjv/m1cf/1KCC/9a0nP/n08T/0Jx8/82YdP/QnHz/16yR//jx7P///////v39///////+/f3///7+///////+//7//v7+///////+/v7//v/+/////////////////////////v7//v79///////////////////+/v/+/Pv//v39///+/v/+/Pv///7+//7+/f/+/Pv//v39//79/P/+/Pv///7+////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
+ />
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
+ <title>Auditor Backoffice</title>
+ <!-- Optional customization script. -->
+ <script src="auditor-backoffice-ui-settings.js"></script>
+ <!-- Entry point for the SPA. -->
+ <script type="module" src="index.js"></script>
+ <link rel="stylesheet" href="index.css" />
+ </head>
+ <body>
+ <div id="app"></div>
+ </body>
+</html>
diff --git a/packages/auditor-backoffice-ui/src/index.tsx b/packages/auditor-backoffice-ui/src/index.tsx
new file mode 100644
index 000000000..7fdf7c1c3
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/index.tsx
@@ -0,0 +1,24 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { Application } from "./Application.js";
+
+import { h, render } from "preact";
+import "./scss/main.scss";
+
+const app = document.getElementById("app");
+
+render(<Application />, app as any);
diff --git a/packages/auditor-backoffice-ui/src/paths/admin/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/admin/create/Create.stories.tsx
new file mode 100644
index 000000000..91b6b4b56
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/admin/create/Create.stories.tsx
@@ -0,0 +1,57 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { ConfigContextProvider } from "../../../context/config.js";
+import { CreatePage as TestedComponent } from "./CreatePage.js";
+
+export default {
+ title: "Pages/Instance/Create",
+ component: TestedComponent,
+ argTypes: {
+ onCreate: { action: "onCreate" },
+ goBack: { action: "goBack" },
+ },
+};
+
+function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => (
+ <ConfigContextProvider
+ value={{
+ currency: "ARS",
+ version: "1",
+ }}
+ >
+ <Component {...args} />
+ </ConfigContextProvider>
+ );
+ r.args = props;
+ return r;
+}
+
+export const Example = createExample(TestedComponent, {});
+// export const Example = (a: any): VNode => <CreatePage {...a} />;
+// Example.args = {
+// isLoading: false
+// }
diff --git a/packages/auditor-backoffice-ui/src/paths/admin/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/admin/create/CreatePage.tsx
new file mode 100644
index 000000000..d13b7e929
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/admin/create/CreatePage.tsx
@@ -0,0 +1,257 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../components/exception/AsyncButton.js";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../components/form/FormProvider.js";
+import { DefaultInstanceFormFields } from "../../../components/instance/DefaultInstanceFormFields.js";
+import { MerchantBackend } from "../../../declaration.js";
+import { INSTANCE_ID_REGEX } from "../../../utils/constants.js";
+import { undefinedIfEmpty } from "../../../utils/table.js";
+import { SetTokenNewInstanceModal } from "../../../components/modal/index.js";
+import { Duration } from "@gnu-taler/taler-util";
+
+export type Entity = Omit<Omit<MerchantBackend.Instances.InstanceConfigurationMessage, "default_pay_delay">, "default_wire_transfer_delay"> & {
+ auth_token?: string;
+ default_pay_delay: Duration,
+ default_wire_transfer_delay: Duration,
+};
+
+interface Props {
+ onCreate: (d: MerchantBackend.Instances.InstanceConfigurationMessage) => Promise<void>;
+ onBack?: () => void;
+ forceId?: string;
+}
+
+function with_defaults(id?: string): Partial<Entity> {
+ return {
+ id,
+ // accounts: [],
+ user_type: "business",
+ use_stefan: true,
+ default_pay_delay: { d_ms: 2 * 60 * 60 * 1000 }, // two hours
+ default_wire_transfer_delay: { d_ms: 2 * 60 * 60 * 24 * 1000 }, // two days
+ };
+}
+
+export function CreatePage({ onCreate, onBack, forceId }: Props): VNode {
+ const [value, valueHandler] = useState(with_defaults(forceId));
+ const [isTokenSet, updateIsTokenSet] = useState<boolean>(false);
+ const [isTokenDialogActive, updateIsTokenDialogActive] =
+ useState<boolean>(false);
+
+ const { i18n } = useTranslationContext();
+
+ const errors: FormErrors<Entity> = {
+ id: !value.id
+ ? i18n.str`required`
+ : !INSTANCE_ID_REGEX.test(value.id)
+ ? i18n.str`is not valid`
+ : undefined,
+ name: !value.name ? i18n.str`required` : undefined,
+
+ user_type: !value.user_type
+ ? i18n.str`required`
+ : value.user_type !== "business" && value.user_type !== "individual"
+ ? i18n.str`should be business or individual`
+ : undefined,
+ // accounts:
+ // !value.accounts || !value.accounts.length
+ // ? i18n.str`required`
+ // : undefinedIfEmpty(
+ // value.accounts.map((p) => {
+ // return !PAYTO_REGEX.test(p.payto_uri)
+ // ? i18n.str`is not valid`
+ // : undefined;
+ // }),
+ // ),
+ default_pay_delay: !value.default_pay_delay
+ ? i18n.str`required`
+ : !!value.default_wire_transfer_delay &&
+ value.default_wire_transfer_delay.d_ms !== "forever" &&
+ value.default_pay_delay.d_ms !== "forever" &&
+ value.default_pay_delay.d_ms > value.default_wire_transfer_delay.d_ms ?
+ i18n.str`pay delay can't be greater than wire transfer delay` : undefined,
+ default_wire_transfer_delay: !value.default_wire_transfer_delay
+ ? i18n.str`required`
+ : undefined,
+ address: undefinedIfEmpty({
+ address_lines:
+ value.address?.address_lines && value.address?.address_lines.length > 7
+ ? i18n.str`max 7 lines`
+ : undefined,
+ }),
+ jurisdiction: undefinedIfEmpty({
+ address_lines:
+ value.address?.address_lines && value.address?.address_lines.length > 7
+ ? i18n.str`max 7 lines`
+ : undefined,
+ }),
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const submit = (): Promise<void> => {
+ // use conversion instead of this
+ const newToken = value.auth_token;
+ value.auth_token = undefined;
+ value.auth = newToken === null || newToken === undefined
+ ? { method: "external" }
+ : { method: "token", token: `secret-token:${newToken}` };
+ if (!value.address) value.address = {};
+ if (!value.jurisdiction) value.jurisdiction = {};
+ // remove above use conversion
+ // schema.validateSync(value, { abortEarly: false })
+ value.default_pay_delay = Duration.toTalerProtocolDuration(value.default_pay_delay!) as any
+ value.default_wire_transfer_delay = Duration.toTalerProtocolDuration(value.default_wire_transfer_delay!) as any
+ // delete value.default_pay_delay;
+ // delete value.default_wire_transfer_delay;
+
+ return onCreate(value as any as MerchantBackend.Instances.InstanceConfigurationMessage);
+ };
+
+ function updateToken(token: string | null) {
+ valueHandler((old) => ({
+ ...old,
+ auth_token: token === null ? undefined : token,
+ }));
+ }
+
+ return (
+ <div>
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ {isTokenDialogActive && (
+ <SetTokenNewInstanceModal
+ onCancel={() => {
+ updateIsTokenDialogActive(false);
+ updateIsTokenSet(false);
+ }}
+ onClear={() => {
+ updateToken(null);
+ updateIsTokenDialogActive(false);
+ updateIsTokenSet(true);
+ }}
+ onConfirm={(newToken) => {
+ updateToken(newToken);
+ updateIsTokenDialogActive(false);
+ updateIsTokenSet(true);
+ }}
+ />
+ )}
+ </div>
+ <div class="column" />
+ </div>
+
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <FormProvider<Entity>
+ errors={errors}
+ object={value}
+ valueHandler={valueHandler}
+ >
+ <DefaultInstanceFormFields readonlyId={!!forceId} showId={true} />
+ </FormProvider>
+
+ <div class="level">
+ <div class="level-item has-text-centered">
+ <h1 class="title">
+ <button
+ class={
+ !isTokenSet
+ ? "button is-danger has-tooltip-bottom"
+ : !value.auth_token
+ ? "button has-tooltip-bottom"
+ : "button is-info has-tooltip-bottom"
+ }
+ data-tooltip={i18n.str`change authorization configuration`}
+ onClick={() => updateIsTokenDialogActive(true)}
+ >
+ <div class="icon is-centered">
+ <i class="mdi mdi-lock-reset" />
+ </div>
+ <span>
+ <i18n.Translate>Set access token</i18n.Translate>
+ </span>
+ </button>
+ </h1>
+ </div>
+ </div>
+ <div class="level">
+ <div class="level-item has-text-centered">
+ {!isTokenSet ? (
+ <p class="is-size-6">
+ <i18n.Translate>
+ Access token is not yet configured. This instance can't be
+ created.
+ </i18n.Translate>
+ </p>
+ ) : value.auth_token === undefined ? (
+ <p class="is-size-6">
+ <i18n.Translate>
+ No access token. Authorization must be handled externally.
+ </i18n.Translate>
+ </p>
+ ) : (
+ <p class="is-size-6">
+ <i18n.Translate>
+ Access token is set. Authorization is handled by the
+ merchant backend.
+ </i18n.Translate>
+ </p>
+ )}
+ </div>
+ </div>
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ onClick={submit}
+ disabled={hasErrors || !isTokenSet}
+ data-tooltip={
+ hasErrors
+ ? i18n.str`Need to complete marked fields and choose authorization method`
+ : "confirm operation"
+ }
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/admin/create/InstanceCreatedSuccessfully.tsx b/packages/auditor-backoffice-ui/src/paths/admin/create/InstanceCreatedSuccessfully.tsx
new file mode 100644
index 000000000..c620c6482
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/admin/create/InstanceCreatedSuccessfully.tsx
@@ -0,0 +1,74 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { h, VNode } from "preact";
+import { CreatedSuccessfully } from "../../../components/notifications/CreatedSuccessfully.js";
+import { Entity } from "./index.js";
+
+export function InstanceCreatedSuccessfully({
+ entity,
+ onConfirm,
+}: {
+ entity: Entity;
+ onConfirm: () => void;
+}): VNode {
+ return (
+ <CreatedSuccessfully onConfirm={onConfirm}>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">ID</label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input class="input" readonly value={entity.id} />
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">Business Name</label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input class="input" readonly value={entity.name} />
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">Access token</label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ {entity.auth.method === "external" && "external"}
+ {entity.auth.method === "token" && (
+ <input class="input" readonly value={entity.auth.token} />
+ )}
+ </p>
+ </div>
+ </div>
+ </div>
+ </CreatedSuccessfully>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/admin/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/admin/create/index.tsx
new file mode 100644
index 000000000..23f41ecff
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/admin/create/index.tsx
@@ -0,0 +1,82 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { NotificationCard } from "../../../components/menu/index.js";
+import { AccessToken, MerchantBackend } from "../../../declaration.js";
+import { useAdminAPI, useInstanceAPI } from "../../../hooks/instance.js";
+import { Notification } from "../../../utils/types.js";
+import { CreatePage } from "./CreatePage.js";
+import { useCredentialsChecker } from "../../../hooks/backend.js";
+import { useBackendContext } from "../../../context/backend.js";
+
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+ forceId?: string;
+}
+export type Entity = MerchantBackend.Instances.InstanceConfigurationMessage;
+
+export default function Create({ onBack, onConfirm, forceId }: Props): VNode {
+ const { createInstance } = useAdminAPI();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+ const { requestNewLoginToken } = useCredentialsChecker()
+ const { url: backendURL, updateToken } = useBackendContext()
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+
+ <CreatePage
+ onBack={onBack}
+ forceId={forceId}
+ onCreate={async (
+ d: MerchantBackend.Instances.InstanceConfigurationMessage,
+ ) => {
+ try {
+ await createInstance(d)
+ if (d.auth.token) {
+ const resp = await requestNewLoginToken(backendURL, d.auth.token as AccessToken)
+ if (resp.valid) {
+ const { token, expiration } = resp
+ updateToken({ token, expiration });
+ } else {
+ updateToken(undefined)
+ }
+ }
+ onConfirm();
+ } catch (ex) {
+ if (ex instanceof Error) {
+ setNotif({
+ message: i18n.str`Failed to create instance`,
+ type: "ERROR",
+ description: ex.message,
+ });
+ } else {
+ console.error(ex)
+ }
+ }
+ }}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/admin/create/stories.tsx b/packages/auditor-backoffice-ui/src/paths/admin/create/stories.tsx
new file mode 100644
index 000000000..0012f9b9b
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/admin/create/stories.tsx
@@ -0,0 +1,52 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { ConfigContextProvider } from "../../../context/config.js";
+import { CreatePage as TestedComponent } from "./CreatePage.js";
+
+export default {
+ title: "Pages/Instance/Create",
+ component: TestedComponent,
+ argTypes: {
+ onCreate: { action: "onCreate" },
+ goBack: { action: "goBack" },
+ },
+};
+
+function createExample<Props>(
+ Internal: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const component = (args: any) => (
+ <ConfigContextProvider
+ value={{
+ currency: "TESTKUDOS",
+ version: "1",
+ }}
+ >
+ <Internal {...(props as any)} />
+ </ConfigContextProvider>
+ );
+ return { component, props };
+}
+
+export const Example = createExample(TestedComponent, {});
diff --git a/packages/auditor-backoffice-ui/src/paths/admin/index.stories.ts b/packages/auditor-backoffice-ui/src/paths/admin/index.stories.ts
new file mode 100644
index 000000000..fdae1a24d
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/admin/index.stories.ts
@@ -0,0 +1,18 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+// export * as list from "./list/stories.js";
+export * as create from "./create/stories.js";
diff --git a/packages/auditor-backoffice-ui/src/paths/admin/list/TableActive.tsx b/packages/auditor-backoffice-ui/src/paths/admin/list/TableActive.tsx
new file mode 100644
index 000000000..885a351d2
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/admin/list/TableActive.tsx
@@ -0,0 +1,287 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { StateUpdater, useEffect, useState } from "preact/hooks";
+import { MerchantBackend } from "../../../declaration.js";
+
+interface Props {
+ instances: MerchantBackend.Instances.Instance[];
+ onUpdate: (id: string) => void;
+ onDelete: (id: MerchantBackend.Instances.Instance) => void;
+ onPurge: (id: MerchantBackend.Instances.Instance) => void;
+ onCreate: () => void;
+ selected?: boolean;
+ setInstanceName: (s: string) => void;
+}
+
+export function CardTable({
+ instances,
+ onCreate,
+ onUpdate,
+ onPurge,
+ setInstanceName,
+ onDelete,
+ selected,
+}: Props): VNode {
+ const [actionQueue, actionQueueHandler] = useState<Actions[]>([]);
+ const [rowSelection, rowSelectionHandler] = useState<string[]>([]);
+
+ useEffect(() => {
+ if (
+ actionQueue.length > 0 &&
+ !selected &&
+ actionQueue[0].type == "DELETE"
+ ) {
+ onDelete(actionQueue[0].element);
+ actionQueueHandler(actionQueue.slice(1));
+ }
+ }, [actionQueue, selected, onDelete]);
+
+ useEffect(() => {
+ if (
+ actionQueue.length > 0 &&
+ !selected &&
+ actionQueue[0].type == "UPDATE"
+ ) {
+ onUpdate(actionQueue[0].element.id);
+ actionQueueHandler(actionQueue.slice(1));
+ }
+ }, [actionQueue, selected, onUpdate]);
+
+ const { i18n } = useTranslationContext();
+
+ return (
+ <div class="card has-table">
+ <header class="card-header">
+ <p class="card-header-title">
+ <span class="icon">
+ <i class="mdi mdi-desktop-mac" />
+ </span>
+ <i18n.Translate>Instances</i18n.Translate>
+ </p>
+
+ <div class="card-header-icon" aria-label="more options">
+ <button
+ class={rowSelection.length > 0 ? "button is-danger" : "is-hidden"}
+ type="button"
+ onClick={(): void =>
+ actionQueueHandler(
+ buildActions(instances, rowSelection, "DELETE"),
+ )
+ }
+ >
+ <i18n.Translate>Delete</i18n.Translate>
+ </button>
+ </div>
+ <div class="card-header-icon" aria-label="more options">
+ <span
+ class="has-tooltip-left"
+ data-tooltip={i18n.str`add new instance`}
+ >
+ <button class="button is-info" type="button" onClick={onCreate}>
+ <span class="icon is-small">
+ <i class="mdi mdi-plus mdi-36px" />
+ </span>
+ </button>
+ </span>
+ </div>
+ </header>
+ <div class="card-content">
+ <div class="b-table has-pagination">
+ <div class="table-wrapper has-mobile-cards">
+ {instances.length > 0 ? (
+ <Table
+ instances={instances}
+ onPurge={onPurge}
+ onUpdate={onUpdate}
+ setInstanceName={setInstanceName}
+ onDelete={onDelete}
+ rowSelection={rowSelection}
+ rowSelectionHandler={rowSelectionHandler}
+ />
+ ) : (
+ <EmptyTable />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+}
+interface TableProps {
+ rowSelection: string[];
+ instances: MerchantBackend.Instances.Instance[];
+ onUpdate: (id: string) => void;
+ onDelete: (id: MerchantBackend.Instances.Instance) => void;
+ onPurge: (id: MerchantBackend.Instances.Instance) => void;
+ rowSelectionHandler: StateUpdater<string[]>;
+ setInstanceName: (s: string) => void;
+}
+
+function toggleSelected<T>(id: T): (prev: T[]) => T[] {
+ return (prev: T[]): T[] =>
+ prev.indexOf(id) == -1 ? [...prev, id] : prev.filter((e) => e != id);
+}
+
+function Table({
+ rowSelection,
+ rowSelectionHandler,
+ setInstanceName,
+ instances,
+ onUpdate,
+ onDelete,
+ onPurge,
+}: TableProps): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="table-container">
+ <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th class="is-checkbox-cell">
+ <label class="b-checkbox checkbox">
+ <input
+ type="checkbox"
+ checked={rowSelection.length === instances.length}
+ onClick={(): void =>
+ rowSelectionHandler(
+ rowSelection.length === instances.length
+ ? []
+ : instances.map((i) => i.id),
+ )
+ }
+ />
+ <span class="check" />
+ </label>
+ </th>
+ <th>
+ <i18n.Translate>ID</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Name</i18n.Translate>
+ </th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ {instances.map((i) => {
+ return (
+ <tr key={i.id}>
+ <td class="is-checkbox-cell">
+ <label class="b-checkbox checkbox">
+ <input
+ type="checkbox"
+ checked={rowSelection.indexOf(i.id) != -1}
+ onClick={(): void =>
+ rowSelectionHandler(toggleSelected(i.id))
+ }
+ />
+ <span class="check" />
+ </label>
+ </td>
+ <td>
+ <a
+ href={`#/orders?instance=${i.id}`}
+ onClick={(e) => {
+ setInstanceName(i.id);
+ }}
+ >
+ {i.id}
+ </a>
+ </td>
+ <td>{i.name}</td>
+ <td class="is-actions-cell right-sticky">
+ <div class="buttons is-right">
+ <button
+ class="button is-small is-success jb-modal"
+ type="button"
+ onClick={(): void => onUpdate(i.id)}
+ >
+ <i18n.Translate>Edit</i18n.Translate>
+ </button>
+ {!i.deleted && (
+ <button
+ class="button is-small is-danger jb-modal is-outlined"
+ type="button"
+ onClick={(): void => onDelete(i)}
+ >
+ <i18n.Translate>Delete</i18n.Translate>
+ </button>
+ )}
+ {i.deleted && (
+ <button
+ class="button is-small is-danger jb-modal"
+ type="button"
+ onClick={(): void => onPurge(i)}
+ >
+ <i18n.Translate>Purge</i18n.Translate>
+ </button>
+ )}
+ </div>
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>
+ );
+}
+
+function EmptyTable(): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="content has-text-grey has-text-centered">
+ <p>
+ <span class="icon is-large">
+ <i class="mdi mdi-emoticon-sad mdi-48px" />
+ </span>
+ </p>
+ <p>
+ <i18n.Translate>
+ There is no instances yet, add more pressing the + sign
+ </i18n.Translate>
+ </p>
+ </div>
+ );
+}
+
+interface Actions {
+ element: MerchantBackend.Instances.Instance;
+ type: "DELETE" | "UPDATE";
+}
+
+function notEmpty<TValue>(value: TValue | null | undefined): value is TValue {
+ return value !== null && value !== undefined;
+}
+
+function buildActions(
+ instances: MerchantBackend.Instances.Instance[],
+ selected: string[],
+ action: "DELETE",
+): Actions[] {
+ return selected
+ .map((id) => instances.find((i) => i.id === id))
+ .filter(notEmpty)
+ .map((id) => ({ element: id, type: action }));
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/admin/list/View.stories.tsx b/packages/auditor-backoffice-ui/src/paths/admin/list/View.stories.tsx
new file mode 100644
index 000000000..e0f5d5430
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/admin/list/View.stories.tsx
@@ -0,0 +1,90 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h } from "preact";
+import { View } from "./View.js";
+
+export default {
+ title: "Pages/Instance/List",
+ component: View,
+ argTypes: {
+ onSelect: { action: "onSelect" },
+ },
+};
+
+export const Empty = (a: any) => <View {...a} />;
+Empty.args = {
+ instances: [],
+};
+
+export const WithDefaultInstance = (a: any) => <View {...a} />;
+WithDefaultInstance.args = {
+ instances: [
+ {
+ id: "default",
+ name: "the default instance",
+ merchant_pub: "abcdef",
+ payment_targets: [],
+ },
+ ],
+};
+
+export const WithFiveInstance = (a: any) => <View {...a} />;
+WithFiveInstance.args = {
+ instances: [
+ {
+ id: "first",
+ name: "the first instance",
+ merchant_pub: "abcdefgh",
+ payment_targets: ["asd"],
+ },
+ {
+ id: "second",
+ name: "the second instance",
+ merchant_pub: "zxczxcz",
+ payment_targets: ["asd"],
+ },
+ {
+ id: "third",
+ name: "the third instance",
+ merchant_pub: "QWEQWEWQE",
+ payment_targets: ["asd"],
+ },
+ {
+ id: "other",
+ name: "the other instance",
+ merchant_pub: "FHJHGJGHJ",
+ payment_targets: ["asd"],
+ },
+ {
+ id: "another",
+ name: "the another instance",
+ merchant_pub: "abcd3423423efgh",
+ payment_targets: ["asd"],
+ },
+ {
+ id: "last",
+ name: "last instance",
+ merchant_pub: "zxcvvbnm",
+ payment_targets: ["pay-to", "asd"],
+ },
+ ],
+};
diff --git a/packages/auditor-backoffice-ui/src/paths/admin/list/View.tsx b/packages/auditor-backoffice-ui/src/paths/admin/list/View.tsx
new file mode 100644
index 000000000..b59112338
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/admin/list/View.tsx
@@ -0,0 +1,110 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { MerchantBackend } from "../../../declaration.js";
+import { CardTable as CardTableActive } from "./TableActive.js";
+
+interface Props {
+ instances: MerchantBackend.Instances.Instance[];
+ onCreate: () => void;
+ onUpdate: (id: string) => void;
+ onDelete: (id: MerchantBackend.Instances.Instance) => void;
+ onPurge: (id: MerchantBackend.Instances.Instance) => void;
+ selected?: boolean;
+ setInstanceName: (s: string) => void;
+}
+
+export function View({
+ instances,
+ onCreate,
+ onDelete,
+ onPurge,
+ onUpdate,
+ setInstanceName,
+ selected,
+}: Props): VNode {
+ const [show, setShow] = useState<"active" | "deleted" | null>("active");
+ const showIsActive = show === "active" ? "is-active" : "";
+ const showIsDeleted = show === "deleted" ? "is-active" : "";
+ const showAll = show === null ? "is-active" : "";
+ const { i18n } = useTranslationContext();
+
+ const showingInstances = showIsDeleted
+ ? instances.filter((i) => i.deleted)
+ : showIsActive
+ ? instances.filter((i) => !i.deleted)
+ : instances;
+
+ return (
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column is-two-thirds">
+ <div class="tabs" style={{ overflow: "inherit" }}>
+ <ul>
+ <li class={showIsActive}>
+ <div
+ class="has-tooltip-right"
+ data-tooltip={i18n.str`Only show active instances`}
+ >
+ <a onClick={() => setShow("active")}>
+ <i18n.Translate>Active</i18n.Translate>
+ </a>
+ </div>
+ </li>
+ <li class={showIsDeleted}>
+ <div
+ class="has-tooltip-right"
+ data-tooltip={i18n.str`Only show deleted instances`}
+ >
+ <a onClick={() => setShow("deleted")}>
+ <i18n.Translate>Deleted</i18n.Translate>
+ </a>
+ </div>
+ </li>
+ <li class={showAll}>
+ <div
+ class="has-tooltip-right"
+ data-tooltip={i18n.str`Show all instances`}
+ >
+ <a onClick={() => setShow(null)}>
+ <i18n.Translate>All</i18n.Translate>
+ </a>
+ </div>
+ </li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ <CardTableActive
+ instances={showingInstances}
+ onDelete={onDelete}
+ onPurge={onPurge}
+ setInstanceName={setInstanceName}
+ onUpdate={onUpdate}
+ selected={selected}
+ onCreate={onCreate}
+ />
+ </section>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/admin/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/admin/list/index.tsx
new file mode 100644
index 000000000..2f839291b
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/admin/list/index.tsx
@@ -0,0 +1,140 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../components/exception/loading.js";
+import { NotificationCard } from "../../../components/menu/index.js";
+import { DeleteModal, PurgeModal } from "../../../components/modal/index.js";
+import { MerchantBackend } from "../../../declaration.js";
+import { useAdminAPI, useBackendInstances } from "../../../hooks/instance.js";
+import { Notification } from "../../../utils/types.js";
+import { View } from "./View.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+
+interface Props {
+ onCreate: () => void;
+ onUpdate: (id: string) => void;
+ instances: MerchantBackend.Instances.Instance[];
+ onUnauthorized: () => VNode;
+ onNotFound: () => VNode;
+ onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ setInstanceName: (s: string) => void;
+}
+
+export default function Instances({
+ onUnauthorized,
+ onLoadError,
+ onNotFound,
+ onCreate,
+ onUpdate,
+ setInstanceName,
+}: Props): VNode {
+ const result = useBackendInstances();
+ const [deleting, setDeleting] =
+ useState<MerchantBackend.Instances.Instance | null>(null);
+ const [purging, setPurging] =
+ useState<MerchantBackend.Instances.Instance | null>(null);
+ const { deleteInstance, purgeInstance } = useAdminAPI();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+ <View
+ instances={result.data.instances}
+ onDelete={setDeleting}
+ onCreate={onCreate}
+ onPurge={setPurging}
+ onUpdate={onUpdate}
+ setInstanceName={setInstanceName}
+ selected={!!deleting}
+ />
+ {deleting && (
+ <DeleteModal
+ element={deleting}
+ onCancel={() => setDeleting(null)}
+ onConfirm={async (): Promise<void> => {
+ try {
+ await deleteInstance(deleting.id);
+ // pushNotification({ message: 'delete_success', type: 'SUCCESS' })
+ setNotif({
+ message: i18n.str`Instance "${deleting.name}" (ID: ${deleting.id}) has been deleted`,
+ type: "SUCCESS",
+ });
+ } catch (error) {
+ setNotif({
+ message: i18n.str`Failed to delete instance`,
+ type: "ERROR",
+ description: error instanceof Error ? error.message : undefined,
+ });
+ // pushNotification({ message: 'delete_error', type: 'ERROR' })
+ }
+ setDeleting(null);
+ }}
+ />
+ )}
+ {purging && (
+ <PurgeModal
+ element={purging}
+ onCancel={() => setPurging(null)}
+ onConfirm={async (): Promise<void> => {
+ try {
+ await purgeInstance(purging.id);
+ setNotif({
+ message: i18n.str`Instance '${purging.name}' (ID: ${purging.id}) has been disabled`,
+ type: "SUCCESS",
+ });
+ } catch (error) {
+ setNotif({
+ message: i18n.str`Failed to purge instance`,
+ type: "ERROR",
+ description: error instanceof Error ? error.message : undefined,
+ });
+ }
+ setPurging(null);
+ }}
+ />
+ )}
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/Create.stories.tsx
new file mode 100644
index 000000000..3336c53a4
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/Create.stories.tsx
@@ -0,0 +1,28 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { CreatePage as TestedComponent } from "./CreatePage.js";
+
+export default {
+ title: "Pages/Accounts/Create",
+ component: TestedComponent,
+};
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx
new file mode 100644
index 000000000..6e4786a47
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx
@@ -0,0 +1,173 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { InputPaytoForm } from "../../../../components/form/InputPaytoForm.js";
+import { InputSelector } from "../../../../components/form/InputSelector.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { undefinedIfEmpty } from "../../../../utils/table.js";
+
+type Entity = MerchantBackend.BankAccounts.AccountAddDetails & { repeatPassword: string };
+
+interface Props {
+ onCreate: (d: Entity) => Promise<void>;
+ onBack?: () => void;
+}
+
+const accountAuthType = ["none", "basic"];
+
+function isValidURL(s: string): boolean {
+ try {
+ const u = new URL(s)
+ return true;
+ } catch (e) {
+ return false;
+ }
+}
+
+export function CreatePage({ onCreate, onBack }: Props): VNode {
+ const { i18n } = useTranslationContext();
+
+ const [state, setState] = useState<Partial<Entity>>({});
+ const errors: FormErrors<Entity> = {
+ payto_uri: !state.payto_uri ? i18n.str`required` : undefined,
+
+ credit_facade_credentials: !state.credit_facade_credentials
+ ? undefined
+ : undefinedIfEmpty({
+ username:
+ state.credit_facade_credentials.type === "basic" && !state.credit_facade_credentials.username
+ ? i18n.str`required`
+ : undefined,
+ password:
+ state.credit_facade_credentials.type === "basic" && !state.credit_facade_credentials.password
+ ? i18n.str`required`
+ : undefined,
+ }),
+ credit_facade_url: !state.credit_facade_url
+ ? undefined
+ : !isValidURL(state.credit_facade_url) ? i18n.str`not valid url`
+ : undefined,
+ repeatPassword:
+ !state.credit_facade_credentials
+ ? undefined
+ : state.credit_facade_credentials.type === "basic" && (!state.credit_facade_credentials.password || state.credit_facade_credentials.password !== state.repeatPassword)
+ ? i18n.str`is not the same`
+ : undefined,
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const submitForm = () => {
+ if (hasErrors) return Promise.reject();
+ delete state.repeatPassword
+ return onCreate(state as any);
+ };
+
+ return (
+ <div>
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <FormProvider
+ object={state}
+ valueHandler={setState}
+ errors={errors}
+ >
+ <InputPaytoForm<Entity>
+ name="payto_uri"
+ label={i18n.str`Account`}
+ />
+ <Input<Entity>
+ name="credit_facade_url"
+ label={i18n.str`Account info URL`}
+ help="https://bank.com"
+ expand
+ tooltip={i18n.str`From where the merchant can download information about incoming wire transfers to this account`}
+ />
+ <InputSelector
+ name="credit_facade_credentials.type"
+ label={i18n.str`Auth type`}
+ tooltip={i18n.str`Choose the authentication type for the account info URL`}
+ values={accountAuthType}
+ toStr={(str) => {
+ if (str === "none") return "Without authentication";
+ return "Username and password";
+ }}
+ />
+ {state.credit_facade_credentials?.type === "basic" ? (
+ <Fragment>
+ <Input
+ name="credit_facade_credentials.username"
+ label={i18n.str`Username`}
+ tooltip={i18n.str`Username to access the account information.`}
+ />
+ <Input
+ name="credit_facade_credentials.password"
+ inputType="password"
+ label={i18n.str`Password`}
+ tooltip={i18n.str`Password to access the account information.`}
+ />
+ <Input
+ name="repeatPassword"
+ inputType="password"
+ label={i18n.str`Repeat password`}
+ />
+ </Fragment>
+ ) : undefined}
+ </FormProvider>
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ disabled={hasErrors}
+ data-tooltip={
+ hasErrors
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ onClick={submitForm}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/index.tsx
new file mode 100644
index 000000000..7d33d25ce
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/index.tsx
@@ -0,0 +1,65 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useWebhookAPI } from "../../../../hooks/webhooks.js";
+import { Notification } from "../../../../utils/types.js";
+import { CreatePage } from "./CreatePage.js";
+import { useOtpDeviceAPI } from "../../../../hooks/otp.js";
+import { useBankAccountAPI } from "../../../../hooks/bank.js";
+
+export type Entity = MerchantBackend.BankAccounts.AccountAddDetails;
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+}
+
+export default function CreateValidator({ onConfirm, onBack }: Props): VNode {
+ const { createBankAccount } = useBankAccountAPI();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+
+ return (
+ <>
+ <NotificationCard notification={notif} />
+ <CreatePage
+ onBack={onBack}
+ onCreate={(request: Entity) => {
+ return createBankAccount(request)
+ .then((d) => {
+ onConfirm()
+ })
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not create device`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/tests/__mocks__/setupTests.ts b/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/List.stories.tsx
index b08eb7fe6..6b4b63735 100644
--- a/packages/merchant-backoffice-ui/tests/__mocks__/setupTests.ts
+++ b/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/List.stories.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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
@@ -19,10 +19,10 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import "regenerator-runtime/runtime";
-// import { configure } from 'enzyme';
-// import Adapter from 'enzyme-adapter-preact-pure';
+import { FunctionalComponent, h } from "preact";
+import { ListPage as TestedComponent } from "./ListPage.js";
-// configure({
-// adapter: new Adapter()
-// });
+export default {
+ title: "Pages/Accounts/List",
+ component: TestedComponent,
+};
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/ListPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/ListPage.tsx
new file mode 100644
index 000000000..24da755b9
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/ListPage.tsx
@@ -0,0 +1,64 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode } from "preact";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { CardTable } from "./Table.js";
+
+export interface Props {
+ devices: MerchantBackend.BankAccounts.BankAccountEntry[];
+ onLoadMoreBefore?: () => void;
+ onLoadMoreAfter?: () => void;
+ onCreate: () => void;
+ onDelete: (e: MerchantBackend.BankAccounts.BankAccountEntry) => void;
+ onSelect: (e: MerchantBackend.BankAccounts.BankAccountEntry) => void;
+}
+
+export function ListPage({
+ devices,
+ onCreate,
+ onDelete,
+ onSelect,
+ onLoadMoreBefore,
+ onLoadMoreAfter,
+}: Props): VNode {
+ const form = { payto_uri: "" };
+
+ const { i18n } = useTranslationContext();
+ return (
+ <section class="section is-main-section">
+ <CardTable
+ accounts={devices.map((o) => ({
+ ...o,
+ id: String(o.h_wire),
+ }))}
+ onCreate={onCreate}
+ onDelete={onDelete}
+ onSelect={onSelect}
+ onLoadMoreBefore={onLoadMoreBefore}
+ hasMoreBefore={!onLoadMoreBefore}
+ onLoadMoreAfter={onLoadMoreAfter}
+ hasMoreAfter={!onLoadMoreAfter}
+ />
+ </section>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/Table.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/Table.tsx
new file mode 100644
index 000000000..7d6db0782
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/Table.tsx
@@ -0,0 +1,385 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { StateUpdater, useState } from "preact/hooks";
+import { MerchantBackend } from "../../../../declaration.js";
+import { parsePaytoUri, PaytoType, PaytoUri, PaytoUriBitcoin, PaytoUriIBAN, PaytoUriTalerBank, PaytoUriUnknown } from "@gnu-taler/taler-util";
+
+type Entity = MerchantBackend.BankAccounts.BankAccountEntry;
+
+interface Props {
+ accounts: Entity[];
+ onDelete: (e: Entity) => void;
+ onSelect: (e: Entity) => void;
+ onCreate: () => void;
+ onLoadMoreBefore?: () => void;
+ hasMoreBefore?: boolean;
+ hasMoreAfter?: boolean;
+ onLoadMoreAfter?: () => void;
+}
+
+export function CardTable({
+ accounts,
+ onCreate,
+ onDelete,
+ onSelect,
+ onLoadMoreAfter,
+ onLoadMoreBefore,
+ hasMoreAfter,
+ hasMoreBefore,
+}: Props): VNode {
+ const [rowSelection, rowSelectionHandler] = useState<string[]>([]);
+
+ const { i18n } = useTranslationContext();
+
+ return (
+ <div class="card has-table">
+ <header class="card-header">
+ <p class="card-header-title">
+ <span class="icon">
+ <i class="mdi mdi-newspaper" />
+ </span>
+ <i18n.Translate>Bank accounts</i18n.Translate>
+ </p>
+ <div class="card-header-icon" aria-label="more options">
+ <span
+ class="has-tooltip-left"
+ data-tooltip={i18n.str`add new accounts`}
+ >
+ <button class="button is-info" type="button" onClick={onCreate}>
+ <span class="icon is-small">
+ <i class="mdi mdi-plus mdi-36px" />
+ </span>
+ </button>
+ </span>
+ </div>
+ </header>
+ <div class="card-content">
+ <div class="b-table has-pagination">
+ <div class="table-wrapper has-mobile-cards">
+ {accounts.length > 0 ? (
+ <Table
+ accounts={accounts}
+ onDelete={onDelete}
+ onSelect={onSelect}
+ rowSelection={rowSelection}
+ rowSelectionHandler={rowSelectionHandler}
+ onLoadMoreAfter={onLoadMoreAfter}
+ onLoadMoreBefore={onLoadMoreBefore}
+ hasMoreAfter={hasMoreAfter}
+ hasMoreBefore={hasMoreBefore}
+ />
+ ) : (
+ <EmptyTable />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+}
+interface TableProps {
+ rowSelection: string[];
+ accounts: Entity[];
+ onDelete: (e: Entity) => void;
+ onSelect: (e: Entity) => void;
+ rowSelectionHandler: StateUpdater<string[]>;
+ onLoadMoreBefore?: () => void;
+ hasMoreBefore?: boolean;
+ hasMoreAfter?: boolean;
+ onLoadMoreAfter?: () => void;
+}
+
+function toggleSelected<T>(id: T): (prev: T[]) => T[] {
+ return (prev: T[]): T[] =>
+ prev.indexOf(id) == -1 ? [...prev, id] : prev.filter((e) => e != id);
+}
+
+function Table({
+ accounts,
+ onLoadMoreAfter,
+ onDelete,
+ onSelect,
+ onLoadMoreBefore,
+ hasMoreAfter,
+ hasMoreBefore,
+}: TableProps): VNode {
+ const { i18n } = useTranslationContext();
+ const emptyList: Record<PaytoType | "unknown", { parsed: PaytoUri, acc: Entity }[]> = { "bitcoin": [], "x-taler-bank": [], "iban": [], "unknown": [], }
+ const accountsByType = accounts.reduce((prev, acc) => {
+ const parsed = parsePaytoUri(acc.payto_uri)
+ if (!parsed) return prev //skip
+ if (parsed.targetType !== "bitcoin" && parsed.targetType !== "x-taler-bank" && parsed.targetType !== "iban") {
+ prev["unknown"].push({ parsed, acc })
+ } else {
+ prev[parsed.targetType].push({ parsed, acc })
+ }
+ return prev
+ }, emptyList)
+
+ const bitcoinAccounts = accountsByType["bitcoin"]
+ const talerbankAccounts = accountsByType["x-taler-bank"]
+ const ibanAccounts = accountsByType["iban"]
+ const unkownAccounts = accountsByType["unknown"]
+
+
+ return (
+ <Fragment>
+
+ {bitcoinAccounts.length > 0 && <div class="table-container">
+ <p class="card-header-title"><i18n.Translate>Bitcoin type accounts</i18n.Translate></p>
+ <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>Address</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Sewgit 1</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Sewgit 2</i18n.Translate>
+ </th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ {bitcoinAccounts.map(({ parsed, acc }, idx) => {
+ const ac = parsed as PaytoUriBitcoin
+ return (
+ <tr key={idx}>
+ <td
+ onClick={(): void => onSelect(acc)}
+ style={{ cursor: "pointer" }}
+ >
+ {ac.targetPath}
+ </td>
+ <td
+ onClick={(): void => onSelect(acc)}
+ style={{ cursor: "pointer" }}
+ >
+ {ac.segwitAddrs[0]}
+ </td>
+ <td
+ onClick={(): void => onSelect(acc)}
+ style={{ cursor: "pointer" }}
+ >
+ {ac.segwitAddrs[1]}
+ </td>
+ <td class="is-actions-cell right-sticky">
+ <div class="buttons is-right">
+ <button
+ class="button is-danger is-small has-tooltip-left"
+ data-tooltip={i18n.str`delete selected accounts from the database`}
+ onClick={() => onDelete(acc)}
+ >
+ Delete
+ </button>
+ </div>
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>}
+
+
+
+ {talerbankAccounts.length > 0 && <div class="table-container">
+ <p class="card-header-title"><i18n.Translate>Taler type accounts</i18n.Translate></p>
+ <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>Host</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Account name</i18n.Translate>
+ </th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ {talerbankAccounts.map(({ parsed, acc }, idx) => {
+ const ac = parsed as PaytoUriTalerBank
+ return (
+ <tr key={idx}>
+ <td
+ onClick={(): void => onSelect(acc)}
+ style={{ cursor: "pointer" }}
+ >
+ {ac.host}
+ </td>
+ <td
+ onClick={(): void => onSelect(acc)}
+ style={{ cursor: "pointer" }}
+ >
+ {ac.account}
+ </td>
+ <td class="is-actions-cell right-sticky">
+ <div class="buttons is-right">
+ <button
+ class="button is-danger is-small has-tooltip-left"
+ data-tooltip={i18n.str`delete selected accounts from the database`}
+ onClick={() => onDelete(acc)}
+ >
+ Delete
+ </button>
+ </div>
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>}
+
+ {ibanAccounts.length > 0 && <div class="table-container">
+ <p class="card-header-title"><i18n.Translate>IBAN type accounts</i18n.Translate></p>
+ <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>Account name</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>IBAN</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>BIC</i18n.Translate>
+ </th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ {ibanAccounts.map(({ parsed, acc }, idx) => {
+ const ac = parsed as PaytoUriIBAN
+ return (
+ <tr key={idx}>
+ <td
+ onClick={(): void => onSelect(acc)}
+ style={{ cursor: "pointer" }}
+ >
+ {ac.params["receiver-name"]}
+ </td>
+ <td
+ onClick={(): void => onSelect(acc)}
+ style={{ cursor: "pointer" }}
+ >
+ {ac.iban}
+ </td>
+ <td
+ onClick={(): void => onSelect(acc)}
+ style={{ cursor: "pointer" }}
+ >
+ {ac.bic ?? ""}
+ </td>
+ <td class="is-actions-cell right-sticky">
+ <div class="buttons is-right">
+ <button
+ class="button is-danger is-small has-tooltip-left"
+ data-tooltip={i18n.str`delete selected accounts from the database`}
+ onClick={() => onDelete(acc)}
+ >
+ Delete
+ </button>
+ </div>
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>}
+
+ {unkownAccounts.length > 0 && <div class="table-container">
+ <p class="card-header-title"><i18n.Translate>Other type accounts</i18n.Translate></p>
+ <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>Type</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Path</i18n.Translate>
+ </th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ {unkownAccounts.map(({ parsed, acc }, idx) => {
+ const ac = parsed as PaytoUriUnknown
+ return (
+ <tr key={idx}>
+ <td
+ onClick={(): void => onSelect(acc)}
+ style={{ cursor: "pointer" }}
+ >
+ {ac.targetType}
+ </td>
+ <td
+ onClick={(): void => onSelect(acc)}
+ style={{ cursor: "pointer" }}
+ >
+ {ac.targetPath}
+ </td>
+ <td class="is-actions-cell right-sticky">
+ <div class="buttons is-right">
+ <button
+ class="button is-danger is-small has-tooltip-left"
+ data-tooltip={i18n.str`delete selected accounts from the database`}
+ onClick={() => onDelete(acc)}
+ >
+ Delete
+ </button>
+ </div>
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>}
+ </Fragment>
+
+ );
+}
+
+function EmptyTable(): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="content has-text-grey has-text-centered">
+ <p>
+ <span class="icon is-large">
+ <i class="mdi mdi-emoticon-sad mdi-48px" />
+ </span>
+ </p>
+ <p>
+ <i18n.Translate>
+ There is no accounts yet, add more pressing the + sign
+ </i18n.Translate>
+ </p>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/index.tsx
new file mode 100644
index 000000000..100241e22
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/index.tsx
@@ -0,0 +1,107 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+import {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useInstanceOtpDevices, useOtpDeviceAPI } from "../../../../hooks/otp.js";
+import { Notification } from "../../../../utils/types.js";
+import { ListPage } from "./ListPage.js";
+import { useBankAccountAPI, useInstanceBankAccounts } from "../../../../hooks/bank.js";
+
+interface Props {
+ onUnauthorized: () => VNode;
+ onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ onNotFound: () => VNode;
+ onCreate: () => void;
+ onSelect: (id: string) => void;
+}
+
+export default function ListOtpDevices({
+ onUnauthorized,
+ onLoadError,
+ onCreate,
+ onSelect,
+ onNotFound,
+}: Props): VNode {
+ const [position, setPosition] = useState<string | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { deleteBankAccount } = useBankAccountAPI();
+ const result = useInstanceBankAccounts({ position }, (id) => setPosition(id));
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+
+ <ListPage
+ devices={result.data.accounts}
+ onLoadMoreBefore={
+ result.isReachingStart ? result.loadMorePrev : undefined
+ }
+ onLoadMoreAfter={result.isReachingEnd ? result.loadMore : undefined}
+ onCreate={onCreate}
+ onSelect={(e) => {
+ onSelect(e.h_wire);
+ }}
+ onDelete={(e: MerchantBackend.BankAccounts.BankAccountEntry) =>
+ deleteBankAccount(e.h_wire)
+ .then(() =>
+ setNotif({
+ message: i18n.str`bank account delete successfully`,
+ type: "SUCCESS",
+ }),
+ )
+ .catch((error) =>
+ setNotif({
+ message: i18n.str`could not delete the bank account`,
+ type: "ERROR",
+ description: error.message,
+ }),
+ )
+ }
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/update/Update.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/update/Update.stories.tsx
new file mode 100644
index 000000000..d6b1d65e0
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/accounts/update/Update.stories.tsx
@@ -0,0 +1,32 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { UpdatePage as TestedComponent } from "./UpdatePage.js";
+
+export default {
+ title: "Pages/OtpDevices/Update",
+ component: TestedComponent,
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx
new file mode 100644
index 000000000..0d20879e8
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx
@@ -0,0 +1,195 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { MerchantBackend, WithId } from "../../../../declaration.js";
+import { InputSelector } from "../../../../components/form/InputSelector.js";
+import { InputPaytoForm } from "../../../../components/form/InputPaytoForm.js";
+import { undefinedIfEmpty } from "../../../../utils/table.js";
+
+type Entity = MerchantBackend.BankAccounts.BankAccountEntry
+ & WithId;
+
+const accountAuthType = ["unedit", "none", "basic"];
+interface Props {
+ onUpdate: (d: MerchantBackend.BankAccounts.AccountPatchDetails) => Promise<void>;
+ onBack?: () => void;
+ account: Entity;
+}
+
+
+export function UpdatePage({ account, onUpdate, onBack }: Props): VNode {
+ const { i18n } = useTranslationContext();
+
+ const [state, setState] = useState<Partial<MerchantBackend.BankAccounts.AccountPatchDetails>>(account);
+
+ const errors: FormErrors<MerchantBackend.BankAccounts.AccountPatchDetails> = {
+ credit_facade_url: !state.credit_facade_url ? i18n.str`required` : !isValidURL(state.credit_facade_url) ? i18n.str`invalid url` : undefined,
+ credit_facade_credentials: undefinedIfEmpty({
+
+ username: state.credit_facade_credentials?.type !== "basic" ? undefined
+ : !state.credit_facade_credentials.username ? i18n.str`required` : undefined,
+
+ password: state.credit_facade_credentials?.type !== "basic" ? undefined
+ : !state.credit_facade_credentials.password ? i18n.str`required` : undefined,
+
+ repeatPassword: state.credit_facade_credentials?.type !== "basic" ? undefined
+ : !(state.credit_facade_credentials as any).repeatPassword ? i18n.str`required` :
+ (state.credit_facade_credentials as any).repeatPassword !== state.credit_facade_credentials.password ? i18n.str`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
new file mode 100644
index 000000000..44dee7651
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/accounts/update/index.tsx
@@ -0,0 +1,96 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+import {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend, WithId } from "../../../../declaration.js";
+import { useBankAccountAPI, useBankAccountDetails } from "../../../../hooks/bank.js";
+import { Notification } from "../../../../utils/types.js";
+import { UpdatePage } from "./UpdatePage.js";
+
+export type Entity = MerchantBackend.BankAccounts.AccountPatchDetails & WithId;
+
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+ onUnauthorized: () => VNode;
+ onNotFound: () => VNode;
+ onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ bid: string;
+}
+export default function UpdateValidator({
+ bid,
+ onConfirm,
+ onBack,
+ onUnauthorized,
+ onNotFound,
+ onLoadError,
+}: Props): VNode {
+ const { updateBankAccount } = useBankAccountAPI();
+ const result = useBankAccountDetails(bid);
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+
+ const { i18n } = useTranslationContext();
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+ <UpdatePage
+ account={{ ...result.data, id: bid }}
+ onBack={onBack}
+ onUpdate={(data) => {
+ return updateBankAccount(bid, data)
+ .then(onConfirm)
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not update account`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/Create.stories.tsx
new file mode 100644
index 000000000..2fc0819bb
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/Create.stories.tsx
@@ -0,0 +1,43 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { CreatePage as TestedComponent } from "./CreatePage.js";
+
+export default {
+ title: "Pages/Product/Create",
+ component: TestedComponent,
+ argTypes: {
+ onCreate: { action: "onCreate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => <Component {...args} />;
+ r.args = props;
+ return r;
+}
+
+export const Example = createExample(TestedComponent, {});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/CreatePage.tsx
new file mode 100644
index 000000000..becaf8f3a
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/CreatePage.tsx
@@ -0,0 +1,80 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
+import { ProductForm } from "../../../../components/product/ProductForm.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useListener } from "../../../../hooks/listener.js";
+
+type Entity = MerchantBackend.Products.ProductAddDetail & {
+ product_id: string;
+};
+
+interface Props {
+ onCreate: (d: Entity) => Promise<void>;
+ onBack?: () => void;
+}
+
+export function CreatePage({ onCreate, onBack }: Props): VNode {
+ const [submitForm, addFormSubmitter] = useListener<Entity | undefined>(
+ (result) => {
+ if (result) return onCreate(result);
+ return Promise.reject();
+ },
+ );
+
+ const { i18n } = useTranslationContext();
+
+ return (
+ <div>
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <ProductForm onSubscribe={addFormSubmitter} />
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ onClick={submitForm}
+ data-tooltip={
+ !submitForm
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ disabled={!submitForm}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/CreatedSuccessfully.tsx b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/CreatedSuccessfully.tsx
new file mode 100644
index 000000000..573064aea
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/CreatedSuccessfully.tsx
@@ -0,0 +1,69 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+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
new file mode 100644
index 000000000..99599cfab
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/index.tsx
@@ -0,0 +1,46 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General 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/taler-wallet-webextension/src/wallet/ExchangeAddConfirm.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/List.stories.tsx
index 8fbecfc4c..41c297d5b 100644
--- a/packages/taler-wallet-webextension/src/wallet/ExchangeAddConfirm.stories.tsx
+++ b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/List.stories.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2022 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
@@ -19,27 +19,25 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { createExample } from "../test-utils.js";
-import { ExchangeAddConfirmPage as TestedComponent } from "./ExchangeAddConfirm.js";
+import { h, VNode, FunctionalComponent } from "preact";
+import { CardTable as TestedComponent } from "./Table.js";
export default {
- title: "exchange add confirm",
+ title: "Pages/Product/List",
component: TestedComponent,
argTypes: {
- onRetry: { action: "onRetry" },
+ onCreate: { action: "onCreate" },
+ onSelect: { action: "onSelect" },
onDelete: { action: "onDelete" },
- onBack: { action: "onBack" },
+ onUpdate: { action: "onUpdate" },
},
};
-export const TermsNotFound = createExample(TestedComponent, {
- url: "https://exchange.demo.taler.net/",
-});
-
-export const NewTerms = createExample(TestedComponent, {
- url: "https://exchange.demo.taler.net/",
-});
-
-export const TermsChanged = createExample(TestedComponent, {
- url: "https://exchange.demo.taler.net/",
-});
+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/Table.tsx b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/Table.tsx
new file mode 100644
index 000000000..2c97b59e8
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/Table.tsx
@@ -0,0 +1,249 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { Amounts } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { format } from "date-fns";
+import { ComponentChildren, Fragment, h, VNode } from "preact";
+import { StateUpdater, useState } from "preact/hooks";
+import emptyImage from "../../../../assets/empty.png";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { InputCurrency } from "../../../../components/form/InputCurrency.js";
+import { InputNumber } from "../../../../components/form/InputNumber.js";
+import { AuditorBackend, WithId } from "../../../../declaration.js";
+import { dateFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
+
+type Entity = AuditorBackend.DepositConfirmation.DepositConfirmationDetail & WithId;
+
+interface Props {
+ instances: Entity[];
+ onDelete: (id: Entity) => void;
+ onSelect: (depositConfirmation: Entity) => void;
+ onUpdate: (
+ id: string,
+ data: AuditorBackend.DepositConfirmation.DepositConfirmationDetail,
+ ) => Promise<void>;
+ onCreate: () => void;
+ selected?: boolean;
+}
+
+export function CardTable({
+ instances,
+ onCreate,
+ onSelect,
+ onUpdate,
+ onDelete,
+}: Props): VNode {
+ const [rowSelection, rowSelectionHandler] = useState<string | undefined>(
+ undefined,
+ );
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="card has-table">
+ <header class="card-header">
+ <p class="card-header-title">
+ <span class="icon">
+ <i class="mdi mdi-shopping" />
+ </span>
+ <i18n.Translate>Deposit Confirmations</i18n.Translate>
+ </p>
+ <div class="card-header-icon" aria-label="more options">
+ <span
+ class="has-tooltip-left"
+ data-tooltip={i18n.str`add deposit-confirmation`}
+ >
+ <button class="button is-info" type="button" onClick={onCreate}>
+ <span class="icon is-small">
+ <i class="mdi mdi-plus mdi-36px" />
+ </span>
+ </button>
+ </span>
+ </div>
+ </header>
+ <div class="card-content">
+ <div class="b-table has-pagination">
+ <div class="table-wrapper has-mobile-cards">
+ {instances.length > 0 ? (
+ <Table
+ instances={instances}
+ onSelect={onSelect}
+ onDelete={onDelete}
+ onUpdate={onUpdate}
+ rowSelection={rowSelection}
+ rowSelectionHandler={rowSelectionHandler}
+ />
+ ) : (
+ <EmptyTable />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+}
+interface TableProps {
+ rowSelection: string | undefined;
+ instances: Entity[];
+ onSelect: (id: Entity) => void;
+ onUpdate: (
+ id: string,
+ data: AuditorBackend.DepositConfirmation.DepositConfirmationDetail,
+ ) => Promise<void>;
+ onDelete: (serial_id: Entity) => void;
+ rowSelectionHandler: StateUpdater<string | undefined>;
+}
+
+function Table({
+ rowSelection,
+ rowSelectionHandler,
+ instances,
+ onSelect,
+ onUpdate,
+ onDelete,
+}: TableProps): VNode {
+ const { i18n } = useTranslationContext();
+ const [settings] = useSettings();
+ return (
+ <div class="table-container">
+ <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>Image</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Description</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Price per unit</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Taxes</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Sales</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Stock</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Sold</i18n.Translate>
+ </th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ {instances.map((i) => {
+
+ return (
+ <Fragment key={i.id}>
+ <tr key="info">
+ <td
+ onClick={() =>
+ rowSelection !== i.id && rowSelectionHandler(i.id)
+ }
+ style={{ cursor: "pointer" }}
+ >
+ </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}>
+ </td>
+ </tr>
+ )}
+ </Fragment>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>
+ );
+}
+
+interface FastProductUpdate {
+ incoming: number;
+ lost: number;
+ price: string;
+}
+interface UpdatePrice {
+ price: string;
+}
+
+
+
+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(":");
+} \ No newline at end of file
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/index.tsx
new file mode 100644
index 000000000..a99cfd2ef
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/index.tsx
@@ -0,0 +1,126 @@
+/*
+ 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
new file mode 100644
index 000000000..a85b13b8b
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/Update.stories.tsx
@@ -0,0 +1,73 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { UpdatePage as TestedComponent } from "./UpdatePage.js";
+
+export default {
+ title: "Pages/Product/Update",
+ component: TestedComponent,
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => <Component {...args} />;
+ r.args = props;
+ return r;
+}
+
+export const WithManagedStock = createExample(TestedComponent, {
+ product: {
+ product_id: "20102-ASDAS-QWE",
+ description: "description1",
+ description_i18n: {} as any,
+ image: "",
+ price: "TESTKUDOS:10",
+ taxes: [],
+ total_lost: 10,
+ total_sold: 5,
+ total_stock: 15,
+ unit: "bar",
+ address: {},
+ },
+});
+
+export const WithInfiniteStock = createExample(TestedComponent, {
+ product: {
+ product_id: "20102-ASDAS-QWE",
+ description: "description1",
+ description_i18n: {} as any,
+ image: "",
+ price: "TESTKUDOS:10",
+ taxes: [],
+ total_lost: 10,
+ total_sold: 5,
+ total_stock: -1,
+ unit: "bar",
+ address: {},
+ },
+});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/UpdatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/UpdatePage.tsx
new file mode 100644
index 000000000..97715171e
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/UpdatePage.tsx
@@ -0,0 +1,99 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
+import { ProductForm } from "../../../../components/product/ProductForm.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useListener } from "../../../../hooks/listener.js";
+
+type Entity = MerchantBackend.Products.ProductDetail & { product_id: string };
+
+interface Props {
+ onUpdate: (d: Entity) => Promise<void>;
+ onBack?: () => void;
+ product: Entity;
+}
+
+export function UpdatePage({ product, onUpdate, onBack }: Props): VNode {
+ const [submitForm, addFormSubmitter] = useListener<Entity | undefined>(
+ (result) => {
+ if (result) return onUpdate(result);
+ return Promise.resolve();
+ },
+ );
+
+ const { i18n } = useTranslationContext();
+
+ return (
+ <div>
+ <section class="section">
+ <section class="hero is-hero-bar">
+ <div class="hero-body">
+ <div class="level">
+ <div class="level-left">
+ <div class="level-item">
+ <span class="is-size-4">
+ <i18n.Translate>Product id:</i18n.Translate>
+ <b>{product.product_id}</b>
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+ <hr />
+
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <ProductForm
+ initial={product}
+ onSubscribe={addFormSubmitter}
+ alreadyExist
+ />
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ onClick={submitForm}
+ data-tooltip={
+ !submitForm
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ disabled={!submitForm}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/index.tsx
new file mode 100644
index 000000000..8e0f7647f
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/index.tsx
@@ -0,0 +1,95 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useProductAPI, useProductDetails } from "../../../../hooks/product.js";
+import { Notification } from "../../../../utils/types.js";
+import { UpdatePage } from "./UpdatePage.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+
+export type Entity = MerchantBackend.Products.ProductAddDetail;
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+ onUnauthorized: () => VNode;
+ onNotFound: () => VNode;
+ onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ pid: string;
+}
+export default function UpdateProduct({
+ pid,
+ onConfirm,
+ onBack,
+ onUnauthorized,
+ onNotFound,
+ onLoadError,
+}: Props): VNode {
+ const { updateProduct } = useProductAPI();
+ const result = useProductDetails(pid);
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+
+ const { i18n } = useTranslationContext();
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+ <UpdatePage
+ product={{ ...result.data, product_id: pid }}
+ onBack={onBack}
+ onUpdate={(data) => {
+ return updateProduct(pid, data)
+ .then(onConfirm)
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not create product`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/details/DetailPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/details/DetailPage.tsx
new file mode 100644
index 000000000..21dadb1e3
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/details/DetailPage.tsx
@@ -0,0 +1,83 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { FormProvider } from "../../../components/form/FormProvider.js";
+import { Input } from "../../../components/form/Input.js";
+import { MerchantBackend } from "../../../declaration.js";
+
+type Entity = MerchantBackend.Instances.InstanceReconfigurationMessage;
+interface Props {
+ onUpdate: () => void;
+ onDelete: () => void;
+ selected: MerchantBackend.Instances.QueryInstancesResponse;
+}
+
+function convert(
+ from: MerchantBackend.Instances.QueryInstancesResponse,
+): Entity {
+ const defaults = {
+ default_wire_fee_amortization: 1,
+ use_stefan: true,
+ default_pay_delay: { d_us: 1000 * 60 * 60 * 1000 }, //one hour
+ default_wire_transfer_delay: { d_us: 1000 * 60 * 60 * 2 * 1000 }, //two hours
+ };
+ return { ...defaults, ...from };
+}
+
+export function DetailPage({ selected }: Props): VNode {
+ const [value, valueHandler] = useState<Partial<Entity>>(convert(selected));
+
+ const { i18n } = useTranslationContext();
+
+ return (
+ <div>
+ <section class="hero is-hero-bar">
+ <div class="hero-body">
+ <div class="level">
+ <div class="level-left">
+ <div class="level-item">
+ <h1 class="title">Here goes the instance description</h1>
+ </div>
+ </div>
+ <div class="level-right" style="display: none;">
+ <div class="level-item" />
+ </div>
+ </div>
+ </div>
+ </section>
+
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-6">
+ <FormProvider<Entity> object={value} valueHandler={valueHandler}>
+ <Input<Entity> name="name" readonly label={i18n.str`Name`} />
+ </FormProvider>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/details/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/details/index.tsx
new file mode 100644
index 000000000..9b393b818
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/details/index.tsx
@@ -0,0 +1,87 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { ErrorType, HttpError } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../components/exception/loading.js";
+import { DeleteModal } from "../../../components/modal/index.js";
+import { useInstanceContext } from "../../../context/instance.js";
+import { MerchantBackend } from "../../../declaration.js";
+import { useInstanceAPI, useInstanceDetails } from "../../../hooks/instance.js";
+import { DetailPage } from "./DetailPage.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+
+interface Props {
+ onUnauthorized: () => VNode;
+ onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ onUpdate: () => void;
+ onNotFound: () => VNode;
+ onDelete: () => void;
+}
+
+export default function Detail({
+ onUpdate,
+ onLoadError,
+ onUnauthorized,
+ onDelete,
+ onNotFound,
+}: Props): VNode {
+ const { id } = useInstanceContext();
+ const result = useInstanceDetails();
+ const [deleting, setDeleting] = useState<boolean>(false);
+
+ const { deleteInstance } = useInstanceAPI();
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <Fragment>
+ <DetailPage
+ selected={result.data}
+ onUpdate={onUpdate}
+ onDelete={() => setDeleting(true)}
+ />
+ {deleting && (
+ <DeleteModal
+ element={{ name: result.data.name, id }}
+ onCancel={() => setDeleting(false)}
+ onConfirm={async (): Promise<void> => {
+ try {
+ await deleteInstance();
+ onDelete();
+ } catch (error) {
+ //FIXME: show message error
+ }
+ setDeleting(false);
+ }}
+ />
+ )}
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/details/stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/details/stories.tsx
new file mode 100644
index 000000000..367fabce2
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/details/stories.tsx
@@ -0,0 +1,68 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { ConfigContextProvider } from "../../../context/config.js";
+import { DetailPage as TestedComponent } from "./DetailPage.js";
+
+export default {
+ title: "Pages/Instance/Detail",
+ component: TestedComponent,
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+function createExample<Props>(
+ Internal: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const component = (args: any) => (
+ <ConfigContextProvider
+ value={{
+ currency: "TESTKUDOS",
+ version: "1",
+ }}
+ >
+ <Internal {...(props as any)} />
+ </ConfigContextProvider>
+ );
+ return { component, props };
+}
+
+export const Example = createExample(TestedComponent, {
+ selected: {
+ name: "name",
+ auth: { method: "external" },
+ address: {},
+ user_type: "business",
+ jurisdiction: {},
+ use_stefan: true,
+ default_pay_delay: {
+ d_us: 1000 * 1000, //one second
+ },
+ default_wire_transfer_delay: {
+ d_us: 1000 * 1000, //one second
+ },
+ merchant_pub: "ASDWQEKASJDKSADJ",
+ },
+});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/index.stories.ts b/packages/auditor-backoffice-ui/src/paths/instance/index.stories.ts
new file mode 100644
index 000000000..1d8c76ff9
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/index.stories.ts
@@ -0,0 +1,19 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+export * as details from "./details/stories.js";
+export * as kycList from "./kyc/list/ListPage.stories.js";
+export * as reserve from "./reserves/create/CreatedSuccessfully.stories.js";
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/ListPage.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/ListPage.stories.tsx
new file mode 100644
index 000000000..d33f64ada
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/ListPage.stories.tsx
@@ -0,0 +1,58 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { ListPage as TestedComponent } from "./ListPage.js";
+import * as tests from "@gnu-taler/web-util/testing";
+import { MerchantBackend } from "../../../../declaration.js";
+
+export default {
+ title: "Pages/KYC/List",
+ component: TestedComponent,
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+export const Example = tests.createExample(TestedComponent, {
+ status: {
+ timeout_kycs: [],
+ pending_kycs: [
+ {
+ aml_status: 0,
+ exchange_url: "http://exchange.taler",
+ payto_uri: "payto://iban/de123123123",
+ kyc_url: "http://exchange.taler/kyc",
+ },
+ {
+ aml_status: 1,
+ exchange_url: "http://exchange.taler",
+ payto_uri: "payto://iban/de123123123",
+ },
+ {
+ aml_status: 2,
+ exchange_url: "http://exchange.taler",
+ payto_uri: "payto://iban/de123123123",
+ },
+ ],
+ } as MerchantBackend.KYC.AccountKycRedirects,
+});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx
new file mode 100644
index 000000000..338081886
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx
@@ -0,0 +1,208 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { MerchantBackend } from "../../../../declaration.js";
+
+export interface Props {
+ status: MerchantBackend.KYC.AccountKycRedirects;
+}
+
+export function ListPage({ status }: Props): VNode {
+ const { i18n } = useTranslationContext();
+
+ return (
+ <section class="section is-main-section">
+ <div class="card has-table">
+ <header class="card-header">
+ <p class="card-header-title">
+ <span class="icon">
+ <i class="mdi mdi-clock" />
+ </span>
+ <i18n.Translate>Pending KYC verification</i18n.Translate>
+ </p>
+
+ <div class="card-header-icon" aria-label="more options" />
+ </header>
+ <div class="card-content">
+ <div class="b-table has-pagination">
+ <div class="table-wrapper has-mobile-cards">
+ {status.pending_kycs.length > 0 ? (
+ <PendingTable entries={status.pending_kycs} />
+ ) : (
+ <EmptyTable />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+
+ {status.timeout_kycs.length > 0 ? (
+ <div class="card has-table">
+ <header class="card-header">
+ <p class="card-header-title">
+ <span class="icon">
+ <i class="mdi mdi-clock" />
+ </span>
+ <i18n.Translate>Timed out</i18n.Translate>
+ </p>
+
+ <div class="card-header-icon" aria-label="more options" />
+ </header>
+ <div class="card-content">
+ <div class="b-table has-pagination">
+ <div class="table-wrapper has-mobile-cards">
+ {status.timeout_kycs.length > 0 ? (
+ <TimedOutTable entries={status.timeout_kycs} />
+ ) : (
+ <EmptyTable />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ ) : undefined}
+ </section>
+ );
+}
+interface PendingTableProps {
+ entries: MerchantBackend.KYC.MerchantAccountKycRedirect[];
+}
+
+interface TimedOutTableProps {
+ entries: MerchantBackend.KYC.ExchangeKycTimeout[];
+}
+
+function PendingTable({ entries }: PendingTableProps): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="table-container">
+ <table class="table is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>Exchange</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Target account</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Reason</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ {entries.map((e, i) => {
+ if (e.kyc_url === undefined) {
+ // blocked by AML
+ return (
+ <tr key={i}>
+ <td>{e.exchange_url}</td>
+ <td>{e.payto_uri}</td>
+ <td>
+ {e.aml_status === 1 ? (
+ <i18n.Translate>
+ There is an anti-money laundering process pending to
+ complete.
+ </i18n.Translate>
+ ) : (
+ <i18n.Translate>
+ The account is frozen due to the anti-money laundering
+ rules. Contact the exchange service provider for further
+ instructions.
+ </i18n.Translate>
+ )}
+ </td>
+ </tr>
+ );
+ } else {
+ // blocked by KYC
+ return (
+ <tr key={i}>
+ <td>{e.exchange_url}</td>
+ <td>{e.payto_uri}</td>
+ <td>
+ <a href={e.kyc_url} target="_black" rel="noreferrer">
+ <i18n.Translate>
+ Pending KYC process, click here to complete
+ </i18n.Translate>
+ </a>
+ </td>
+ </tr>
+ );
+ }
+ })}
+ </tbody>
+ </table>
+ </div>
+ );
+}
+
+function TimedOutTable({ entries }: TimedOutTableProps): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="table-container">
+ <table class="table is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>Exchange</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Code</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Http Status</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ {entries.map((e, i) => {
+ return (
+ <tr key={i}>
+ <td>{e.exchange_url}</td>
+ <td>{e.exchange_code}</td>
+ <td>{e.exchange_http_status}</td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>
+ );
+}
+
+function EmptyTable(): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="content has-text-grey has-text-centered">
+ <p>
+ <span class="icon is-large">
+ <i class="mdi mdi-emoticon-happy mdi-48px" />
+ </span>
+ </p>
+ <p>
+ <i18n.Translate>No pending kyc verification!</i18n.Translate>
+ </p>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/index.tsx
new file mode 100644
index 000000000..5b93ac169
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/index.tsx
@@ -0,0 +1,63 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { ErrorType, HttpError } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { Loading } from "../../../../components/exception/loading.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useInstanceKYCDetails } from "../../../../hooks/instance.js";
+import { ListPage } from "./ListPage.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+
+interface Props {
+ onUnauthorized: () => VNode;
+ onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ onNotFound: () => VNode;
+}
+
+export default function ListKYC({
+ onUnauthorized,
+ onLoadError,
+ onNotFound,
+}: Props): VNode {
+ const result = useInstanceKYCDetails();
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ const status = result.data.type === "ok" ? undefined : result.data.status;
+
+ if (!status) {
+ return <div>no kyc required</div>;
+ }
+ return <ListPage status={status} />;
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/create/Create.stories.tsx
new file mode 100644
index 000000000..bd9f65718
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/orders/create/Create.stories.tsx
@@ -0,0 +1,71 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { CreatePage as TestedComponent } from "./CreatePage.js";
+
+export default {
+ title: "Pages/Order/Create",
+ component: TestedComponent,
+ argTypes: {
+ onCreate: { action: "onCreate" },
+ goBack: { action: "goBack" },
+ },
+};
+
+function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => <Component {...args} />;
+ r.args = props;
+ return r;
+}
+
+export const Example = createExample(TestedComponent, {
+ instanceConfig: {
+ default_pay_delay: {
+ d_us: 1000 * 1000 * 60 * 60, //one hour
+ },
+ default_wire_transfer_delay: {
+ d_us: 1000 * 1000 * 60 * 60, //one hour
+ },
+ use_stefan: true,
+ },
+ instanceInventory: [
+ {
+ id: "t-shirt-1",
+ description: "a m size t-shirt",
+ price: "TESTKUDOS:1",
+ total_stock: -1,
+ },
+ {
+ id: "t-shirt-2",
+ price: "TESTKUDOS:1",
+ description: "a xl size t-shirt",
+ } as any,
+ {
+ id: "t-shirt-3",
+ price: "TESTKUDOS:1",
+ description: "a s size t-shirt",
+ } as any,
+ ],
+});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx
new file mode 100644
index 000000000..62ceaa24b
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx
@@ -0,0 +1,705 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { AbsoluteTime, Amounts, Duration, TalerProtocolDuration } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { format, isFuture } from "date-fns";
+import { ComponentChildren, Fragment, VNode, h } from "preact";
+import { useEffect, useState } from "preact/hooks";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { InputCurrency } from "../../../../components/form/InputCurrency.js";
+import { InputDate } from "../../../../components/form/InputDate.js";
+import { InputDuration } from "../../../../components/form/InputDuration.js";
+import { InputGroup } from "../../../../components/form/InputGroup.js";
+import { InputLocation } from "../../../../components/form/InputLocation.js";
+import { InputNumber } from "../../../../components/form/InputNumber.js";
+import { InputToggle } from "../../../../components/form/InputToggle.js";
+import { InventoryProductForm } from "../../../../components/product/InventoryProductForm.js";
+import { NonInventoryProductFrom } from "../../../../components/product/NonInventoryProductForm.js";
+import { ProductList } from "../../../../components/product/ProductList.js";
+import { useConfigContext } from "../../../../context/config.js";
+import { MerchantBackend, WithId } from "../../../../declaration.js";
+import { useSettings } from "../../../../hooks/useSettings.js";
+import { OrderCreateSchema as schema } from "../../../../schemas/index.js";
+import { rate } from "../../../../utils/amount.js";
+import { undefinedIfEmpty } from "../../../../utils/table.js";
+
+interface Props {
+ onCreate: (d: MerchantBackend.Orders.PostOrderRequest) => void;
+ onBack?: () => void;
+ instanceConfig: InstanceConfig;
+ instanceInventory: (MerchantBackend.Products.ProductDetail & WithId)[];
+}
+interface InstanceConfig {
+ use_stefan: boolean;
+ default_pay_delay: TalerProtocolDuration;
+ default_wire_transfer_delay: TalerProtocolDuration;
+}
+
+function with_defaults(config: InstanceConfig, currency: string): Partial<Entity> {
+ const defaultPayDeadline = Duration.fromTalerProtocolDuration(config.default_pay_delay);
+ const defaultWireDeadline = Duration.fromTalerProtocolDuration(config.default_wire_transfer_delay);
+
+ return {
+ inventoryProducts: {},
+ products: [],
+ pricing: {},
+ payments: {
+ max_fee: undefined,
+ createToken: true,
+ pay_deadline: (defaultPayDeadline),
+ refund_deadline: (defaultPayDeadline),
+ wire_transfer_deadline: (defaultWireDeadline),
+ },
+ shipping: {},
+ extra: {},
+ };
+}
+
+interface ProductAndQuantity {
+ product: MerchantBackend.Products.ProductDetail & WithId;
+ quantity: number;
+}
+export interface ProductMap {
+ [id: string]: ProductAndQuantity;
+}
+
+interface Pricing {
+ products_price: string;
+ order_price: string;
+ summary: string;
+}
+interface Shipping {
+ delivery_date?: Date;
+ delivery_location?: MerchantBackend.Location;
+ fullfilment_url?: string;
+}
+interface Payments {
+ refund_deadline: Duration;
+ pay_deadline: Duration;
+ wire_transfer_deadline: Duration;
+ auto_refund_deadline: Duration;
+ max_fee?: string;
+ createToken: boolean;
+ minimum_age?: number;
+}
+interface Entity {
+ inventoryProducts: ProductMap;
+ products: MerchantBackend.Product[];
+ pricing: Partial<Pricing>;
+ payments: Partial<Payments>;
+ shipping: Partial<Shipping>;
+ extra: Record<string, string>;
+}
+
+const stringIsValidJSON = (value: string) => {
+ try {
+ JSON.parse(value.trim());
+ return true;
+ } catch {
+ return false;
+ }
+};
+
+export function CreatePage({
+ onCreate,
+ onBack,
+ instanceConfig,
+ instanceInventory,
+}: Props): VNode {
+ const config = useConfigContext();
+ const instance_default = with_defaults(instanceConfig, config.currency)
+ const [value, valueHandler] = useState(instance_default);
+ const zero = Amounts.zeroOfCurrency(config.currency);
+ const [settings, updateSettings] = useSettings()
+ const inventoryList = Object.values(value.inventoryProducts || {});
+ const productList = Object.values(value.products || {});
+
+ const { i18n } = useTranslationContext();
+
+ const parsedPrice = !value.pricing?.order_price
+ ? undefined
+ : Amounts.parse(value.pricing.order_price);
+
+ const errors: FormErrors<Entity> = {
+ pricing: undefinedIfEmpty({
+ summary: !value.pricing?.summary ? i18n.str`required` : undefined,
+ order_price: !value.pricing?.order_price
+ ? i18n.str`required`
+ : !parsedPrice
+ ? i18n.str`not valid`
+ : Amounts.isZero(parsedPrice)
+ ? i18n.str`must be greater than 0`
+ : undefined,
+ }),
+ payments: undefinedIfEmpty({
+ refund_deadline: !value.payments?.refund_deadline
+ ? undefined
+ : value.payments.pay_deadline &&
+ Duration.cmp(value.payments.refund_deadline, value.payments.pay_deadline) === -1
+ ? i18n.str`refund deadline cannot be before pay deadline`
+ : value.payments.wire_transfer_deadline &&
+ Duration.cmp(
+ value.payments.wire_transfer_deadline,
+ value.payments.refund_deadline,
+ ) === -1
+ ? i18n.str`wire transfer deadline cannot be before refund deadline`
+ : undefined,
+ pay_deadline: !value.payments?.pay_deadline
+ ? i18n.str`required`
+ : value.payments.wire_transfer_deadline &&
+ Duration.cmp(
+ value.payments.wire_transfer_deadline,
+ value.payments.pay_deadline,
+ ) === -1
+ ? i18n.str`wire transfer deadline cannot be before pay deadline`
+ : undefined,
+ wire_transfer_deadline: !value.payments?.wire_transfer_deadline
+ ? i18n.str`required`
+ : undefined,
+ auto_refund_deadline: !value.payments?.auto_refund_deadline
+ ? undefined
+ : !value.payments?.refund_deadline
+ ? i18n.str`should have a refund deadline`
+ : Duration.cmp(
+ value.payments.refund_deadline,
+ value.payments.auto_refund_deadline,
+ ) == -1
+ ? i18n.str`auto refund cannot be after refund deadline`
+ : undefined,
+
+ }),
+ shipping: undefinedIfEmpty({
+ delivery_date: !value.shipping?.delivery_date
+ ? undefined
+ : !isFuture(value.shipping.delivery_date)
+ ? i18n.str`should be in the future`
+ : undefined,
+ }),
+ };
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const submit = (): void => {
+ const order = value as any; //schema.cast(value);
+ if (!value.payments) return;
+ if (!value.shipping) return;
+
+ const request: MerchantBackend.Orders.PostOrderRequest = {
+ order: {
+ amount: order.pricing.order_price,
+ summary: order.pricing.summary,
+ products: productList,
+ extra: undefinedIfEmpty(value.extra),
+ pay_deadline: !value.payments.pay_deadline ?
+ i18n.str`required` :
+ AbsoluteTime.toProtocolTimestamp(AbsoluteTime.addDuration(AbsoluteTime.now(), value.payments.pay_deadline))
+ ,// : undefined,
+ wire_transfer_deadline: value.payments.wire_transfer_deadline
+ ? AbsoluteTime.toProtocolTimestamp(AbsoluteTime.addDuration(AbsoluteTime.now(), value.payments.wire_transfer_deadline))
+ : undefined,
+ refund_deadline: value.payments.refund_deadline
+ ? AbsoluteTime.toProtocolTimestamp(AbsoluteTime.addDuration(AbsoluteTime.now(), value.payments.refund_deadline))
+ : undefined,
+ auto_refund: value.payments.auto_refund_deadline
+ ? Duration.toTalerProtocolDuration(value.payments.auto_refund_deadline)
+ : undefined,
+ max_fee: value.payments.max_fee as string,
+
+ delivery_date: value.shipping.delivery_date
+ ? { t_s: value.shipping.delivery_date.getTime() / 1000 }
+ : undefined,
+ delivery_location: value.shipping.delivery_location,
+ fulfillment_url: value.shipping.fullfilment_url,
+ minimum_age: value.payments.minimum_age,
+ },
+ inventory_products: inventoryList.map((p) => ({
+ product_id: p.product.id,
+ quantity: p.quantity,
+ })),
+ create_token: value.payments.createToken,
+ };
+
+ onCreate(request);
+ };
+
+ const addProductToTheInventoryList = (
+ product: MerchantBackend.Products.ProductDetail & WithId,
+ quantity: number,
+ ) => {
+ valueHandler((v) => {
+ const inventoryProducts = { ...v.inventoryProducts };
+ inventoryProducts[product.id] = { product, quantity };
+ return { ...v, inventoryProducts };
+ });
+ };
+
+ const removeProductFromTheInventoryList = (id: string) => {
+ valueHandler((v) => {
+ const inventoryProducts = { ...v.inventoryProducts };
+ delete inventoryProducts[id];
+ return { ...v, inventoryProducts };
+ });
+ };
+
+ const addNewProduct = async (product: MerchantBackend.Product) => {
+ return valueHandler((v) => {
+ const products = v.products ? [...v.products, product] : [];
+ return { ...v, products };
+ });
+ };
+
+ const removeFromNewProduct = (index: number) => {
+ valueHandler((v) => {
+ const products = v.products ? [...v.products] : [];
+ products.splice(index, 1);
+ return { ...v, products };
+ });
+ };
+
+ const [editingProduct, setEditingProduct] = useState<
+ MerchantBackend.Product | undefined
+ >(undefined);
+
+ const totalPriceInventory = inventoryList.reduce((prev, cur) => {
+ const p = Amounts.parseOrThrow(cur.product.price);
+ return Amounts.add(prev, Amounts.mult(p, cur.quantity).amount).amount;
+ }, zero);
+
+ const totalPriceProducts = productList.reduce((prev, cur) => {
+ if (!cur.price) return zero;
+ const p = Amounts.parseOrThrow(cur.price);
+ return Amounts.add(prev, Amounts.mult(p, cur.quantity).amount).amount;
+ }, zero);
+
+ const hasProducts = inventoryList.length > 0 || productList.length > 0;
+ const totalPrice = Amounts.add(totalPriceInventory, totalPriceProducts);
+
+ const totalAsString = Amounts.stringify(totalPrice.amount);
+ const allProducts = productList.concat(inventoryList.map(asProduct));
+
+ const [newField, setNewField] = useState("")
+
+ useEffect(() => {
+ valueHandler((v) => {
+ return {
+ ...v,
+ pricing: {
+ ...v.pricing,
+ products_price: hasProducts ? totalAsString : undefined,
+ order_price: hasProducts ? totalAsString : undefined,
+ },
+ };
+ });
+ }, [hasProducts, totalAsString]);
+
+ const discountOrRise = rate(
+ parsedPrice ?? Amounts.zeroOfCurrency(config.currency),
+ totalPrice.amount,
+ );
+
+ const minAgeByProducts = allProducts.reduce(
+ (cur, prev) =>
+ !prev.minimum_age || cur > prev.minimum_age ? cur : prev.minimum_age,
+ 0,
+ );
+
+ // if there is no default pay deadline
+ const noDefault_payDeadline = !instance_default.payments || !instance_default.payments.pay_deadline
+ // and there is no 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
new file mode 100644
index 000000000..88a984c97
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx
@@ -0,0 +1,114 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useEffect, useState } from "preact/hooks";
+import { CreatedSuccessfully } from "../../../../components/notifications/CreatedSuccessfully.js";
+import { useOrderAPI } from "../../../../hooks/order.js";
+import { Entity } from "./index.js";
+
+interface Props {
+ entity: Entity;
+ onConfirm: () => void;
+ onCreateAnother?: () => void;
+}
+
+export function OrderCreatedSuccessfully({
+ entity,
+ onConfirm,
+ onCreateAnother,
+}: Props): VNode {
+ const { getPaymentURL } = useOrderAPI();
+ const [url, setURL] = useState<string | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+ useEffect(() => {
+ getPaymentURL(entity.response.order_id).then((response) => {
+ setURL(response.data);
+ });
+ }, [getPaymentURL, entity.response.order_id]);
+
+ return (
+ <CreatedSuccessfully
+ onConfirm={onConfirm}
+ onCreateAnother={onCreateAnother}
+ >
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ <i18n.Translate>Amount</i18n.Translate>
+ </label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input
+ class="input"
+ readonly
+ value={entity.request.order.amount}
+ />
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ <i18n.Translate>Summary</i18n.Translate>
+ </label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input
+ class="input"
+ readonly
+ value={entity.request.order.summary}
+ />
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ <i18n.Translate>Order ID</i18n.Translate>
+ </label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input class="input" readonly value={entity.response.order_id} />
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ <i18n.Translate>Payment URL</i18n.Translate>
+ </label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input class="input" readonly value={url} />
+ </p>
+ </div>
+ </div>
+ </div>
+ </CreatedSuccessfully>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/create/index.tsx
new file mode 100644
index 000000000..2474fd042
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/orders/create/index.tsx
@@ -0,0 +1,114 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { ErrorType, HttpError } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useInstanceDetails } from "../../../../hooks/instance.js";
+import { useOrderAPI } from "../../../../hooks/order.js";
+import { useInstanceProducts } from "../../../../hooks/product.js";
+import { Notification } from "../../../../utils/types.js";
+import { CreatePage } from "./CreatePage.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+
+export type Entity = {
+ request: MerchantBackend.Orders.PostOrderRequest;
+ response: MerchantBackend.Orders.PostOrderResponse;
+};
+interface Props {
+ onBack?: () => void;
+ onConfirm: (id: string) => void;
+ onUnauthorized: () => VNode;
+ onNotFound: () => VNode;
+ onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+}
+export default function OrderCreate({
+ onConfirm,
+ onBack,
+ onLoadError,
+ onNotFound,
+ onUnauthorized,
+}: Props): VNode {
+ const { createOrder } = useOrderAPI();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+
+ const detailsResult = useInstanceDetails();
+ const inventoryResult = useInstanceProducts();
+
+ if (detailsResult.loading) return <Loading />;
+ if (inventoryResult.loading) return <Loading />;
+
+ if (!detailsResult.ok) {
+ if (
+ detailsResult.type === ErrorType.CLIENT &&
+ detailsResult.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ detailsResult.type === ErrorType.CLIENT &&
+ detailsResult.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(detailsResult);
+ }
+
+ if (!inventoryResult.ok) {
+ if (
+ inventoryResult.type === ErrorType.CLIENT &&
+ inventoryResult.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ inventoryResult.type === ErrorType.CLIENT &&
+ inventoryResult.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(inventoryResult);
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+
+ <CreatePage
+ onBack={onBack}
+ onCreate={(request: MerchantBackend.Orders.PostOrderRequest) => {
+ createOrder(request)
+ .then((r) => {
+ return onConfirm(r.data.order_id)
+ })
+ .catch((error) => {
+ setNotif({
+ message: "could not create order",
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ instanceConfig={detailsResult.data}
+ instanceInventory={inventoryResult.data}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/details/Detail.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/details/Detail.stories.tsx
new file mode 100644
index 000000000..6e73a01a5
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/orders/details/Detail.stories.tsx
@@ -0,0 +1,135 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { addDays } from "date-fns";
+import { h, VNode, FunctionalComponent } from "preact";
+import { MerchantBackend } from "../../../../declaration.js";
+import { DetailPage as TestedComponent } from "./DetailPage.js";
+
+export default {
+ title: "Pages/Order/Detail",
+ component: TestedComponent,
+ argTypes: {
+ onRefund: { action: "onRefund" },
+ onBack: { action: "onBack" },
+ },
+};
+
+function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => <Component {...args} />;
+ r.args = props;
+ return r;
+}
+
+const defaultContractTerm = {
+ amount: "TESTKUDOS:10",
+ timestamp: {
+ t_s: new Date().getTime() / 1000,
+ },
+ auditors: [],
+ exchanges: [],
+ max_fee: "TESTKUDOS:1",
+ merchant: {} as any,
+ merchant_base_url: "http://merchant.url/",
+ order_id: "2021.165-03GDFC26Y1NNG",
+ products: [],
+ summary: "text summary",
+ wire_transfer_deadline: {
+ t_s: "never",
+ },
+ refund_deadline: { t_s: "never" },
+ merchant_pub: "ASDASDASDSd",
+ nonce: "QWEQWEQWE",
+ pay_deadline: {
+ t_s: "never",
+ },
+ wire_method: "x-taler-bank",
+ h_wire: "asd",
+} as MerchantBackend.ContractTerms;
+
+// contract_terms: defaultContracTerm,
+export const Claimed = createExample(TestedComponent, {
+ id: "2021.165-03GDFC26Y1NNG",
+ selected: {
+ order_status: "claimed",
+ contract_terms: defaultContractTerm,
+ },
+});
+
+export const PaidNotRefundable = createExample(TestedComponent, {
+ id: "2021.165-03GDFC26Y1NNG",
+ selected: {
+ order_status: "paid",
+ contract_terms: defaultContractTerm,
+ refunded: false,
+ deposit_total: "TESTKUDOS:10",
+ exchange_ec: 0,
+ order_status_url: "http://merchant.backend/status",
+ exchange_hc: 0,
+ refund_amount: "TESTKUDOS:0",
+ refund_details: [],
+ refund_pending: false,
+ wire_details: [],
+ wire_reports: [],
+ wired: false,
+ },
+});
+
+export const PaidRefundable = createExample(TestedComponent, {
+ id: "2021.165-03GDFC26Y1NNG",
+ selected: {
+ order_status: "paid",
+ contract_terms: {
+ ...defaultContractTerm,
+ refund_deadline: {
+ t_s: addDays(new Date(), 2).getTime() / 1000,
+ },
+ },
+ refunded: false,
+ deposit_total: "TESTKUDOS:10",
+ exchange_ec: 0,
+ order_status_url: "http://merchant.backend/status",
+ exchange_hc: 0,
+ refund_amount: "TESTKUDOS:0",
+ refund_details: [],
+ refund_pending: false,
+ wire_details: [],
+ wire_reports: [],
+ wired: false,
+ },
+});
+
+export const Unpaid = createExample(TestedComponent, {
+ id: "2021.165-03GDFC26Y1NNG",
+ selected: {
+ order_status: "unpaid",
+ order_status_url: "http://merchant.backend/status",
+ creation_time: {
+ t_s: new Date().getTime() / 1000,
+ },
+ summary: "text summary",
+ taler_pay_uri: "pay uri",
+ total_amount: "TESTKUDOS:10",
+ },
+});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx
new file mode 100644
index 000000000..5ff76e37a
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx
@@ -0,0 +1,770 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { AmountJson, Amounts, stringifyRefundUri } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { format, formatDistance } from "date-fns";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { FormProvider } from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { InputCurrency } from "../../../../components/form/InputCurrency.js";
+import { InputDate } from "../../../../components/form/InputDate.js";
+import { InputDuration } from "../../../../components/form/InputDuration.js";
+import { InputGroup } from "../../../../components/form/InputGroup.js";
+import { InputLocation } from "../../../../components/form/InputLocation.js";
+import { TextField } from "../../../../components/form/TextField.js";
+import { ProductList } from "../../../../components/product/ProductList.js";
+import { useBackendContext } from "../../../../context/backend.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
+import { mergeRefunds } from "../../../../utils/amount.js";
+import { RefundModal } from "../list/Table.js";
+import { Event, Timeline } from "./Timeline.js";
+
+type Entity = MerchantBackend.Orders.MerchantOrderStatusResponse;
+type CT = MerchantBackend.ContractTerms;
+
+interface Props {
+ onBack: () => void;
+ selected: Entity;
+ id: string;
+ onRefund: (id: string, value: MerchantBackend.Orders.RefundRequest) => void;
+}
+
+type Paid = MerchantBackend.Orders.CheckPaymentPaidResponse & {
+ refund_taken: string;
+};
+type Unpaid = MerchantBackend.Orders.CheckPaymentUnpaidResponse;
+type Claimed = MerchantBackend.Orders.CheckPaymentClaimedResponse;
+
+function ContractTerms({ value }: { value: CT }) {
+ const { i18n } = useTranslationContext();
+
+ return (
+ <InputGroup name="contract_terms" label={i18n.str`Contract Terms`}>
+ <FormProvider<CT> object={value} valueHandler={null}>
+ <Input<CT>
+ readonly
+ name="summary"
+ label={i18n.str`Summary`}
+ tooltip={i18n.str`human-readable description of the whole purchase`}
+ />
+ <InputCurrency<CT>
+ readonly
+ name="amount"
+ label={i18n.str`Amount`}
+ tooltip={i18n.str`total price for the transaction`}
+ />
+ {value.fulfillment_url && (
+ <Input<CT>
+ readonly
+ name="fulfillment_url"
+ label={i18n.str`Fulfillment URL`}
+ tooltip={i18n.str`URL for this purchase`}
+ />
+ )}
+ <Input<CT>
+ readonly
+ name="max_fee"
+ label={i18n.str`Max fee`}
+ tooltip={i18n.str`maximum total deposit fee accepted by the merchant for this contract`}
+ />
+ <InputDate<CT>
+ readonly
+ name="timestamp"
+ label={i18n.str`Created at`}
+ tooltip={i18n.str`time when this contract was generated`}
+ />
+ <InputDate<CT>
+ readonly
+ name="refund_deadline"
+ label={i18n.str`Refund deadline`}
+ tooltip={i18n.str`after this deadline has passed no refunds will be accepted`}
+ />
+ <InputDate<CT>
+ readonly
+ name="pay_deadline"
+ label={i18n.str`Payment deadline`}
+ tooltip={i18n.str`after this deadline, the merchant won't accept payments for the contract`}
+ />
+ <InputDate<CT>
+ readonly
+ name="wire_transfer_deadline"
+ label={i18n.str`Wire transfer deadline`}
+ tooltip={i18n.str`transfer deadline for the exchange`}
+ />
+ <InputDate<CT>
+ readonly
+ name="delivery_date"
+ label={i18n.str`Delivery date`}
+ tooltip={i18n.str`time indicating when the order should be delivered`}
+ />
+ {value.delivery_date && (
+ <InputGroup
+ name="delivery_location"
+ label={i18n.str`Location`}
+ tooltip={i18n.str`where the order will be delivered`}
+ >
+ <InputLocation name="payments.delivery_location" />
+ </InputGroup>
+ )}
+ <InputDuration<CT>
+ readonly
+ name="auto_refund"
+ label={i18n.str`Auto-refund delay`}
+ tooltip={i18n.str`how long the wallet should try to get an automatic refund for the purchase`}
+ />
+ <Input<CT>
+ readonly
+ name="extra"
+ label={i18n.str`Extra info`}
+ tooltip={i18n.str`extra data that is only interpreted by the merchant frontend`}
+ />
+ </FormProvider>
+ </InputGroup>
+ );
+}
+
+function ClaimedPage({
+ id,
+ order,
+}: {
+ id: string;
+ order: MerchantBackend.Orders.CheckPaymentClaimedResponse;
+}) {
+ const events: Event[] = [];
+ if (order.contract_terms.timestamp.t_s !== "never") {
+ events.push({
+ when: new Date(order.contract_terms.timestamp.t_s * 1000),
+ description: "order created",
+ type: "start",
+ });
+ }
+ if (order.contract_terms.pay_deadline.t_s !== "never") {
+ events.push({
+ when: new Date(order.contract_terms.pay_deadline.t_s * 1000),
+ description: "pay deadline",
+ type: "deadline",
+ });
+ }
+ if (order.contract_terms.refund_deadline.t_s !== "never") {
+ events.push({
+ when: new Date(order.contract_terms.refund_deadline.t_s * 1000),
+ description: "refund deadline",
+ type: "deadline",
+ });
+ }
+ if (order.contract_terms.wire_transfer_deadline.t_s !== "never") {
+ events.push({
+ when: new Date(order.contract_terms.wire_transfer_deadline.t_s * 1000),
+ description: "wire deadline",
+ type: "deadline",
+ });
+ }
+ if (
+ order.contract_terms.delivery_date &&
+ order.contract_terms.delivery_date.t_s !== "never"
+ ) {
+ events.push({
+ when: new Date(order.contract_terms.delivery_date?.t_s * 1000),
+ description: "delivery",
+ type: "delivery",
+ });
+ }
+
+ const [value, valueHandler] = useState<Partial<Claimed>>(order);
+ const { i18n } = useTranslationContext();
+ const [settings] = useSettings()
+
+ return (
+ <div>
+ <section class="section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-10">
+ <section class="hero is-hero-bar">
+ <div class="hero-body">
+ <div class="level">
+ <div class="level-left">
+ <div class="level-item">
+ <i18n.Translate>Order</i18n.Translate> #{id}
+ <div class="tag is-info ml-4">
+ <i18n.Translate>claimed</i18n.Translate>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="level">
+ <div class="level-left">
+ <div class="level-item">
+ <h1 class="title">{order.contract_terms.amount}</h1>
+ </div>
+ </div>
+ </div>
+
+ <div class="level">
+ <div class="level-left" style={{ maxWidth: "100%" }}>
+ <div class="level-item" style={{ maxWidth: "100%" }}>
+ <div
+ class="content"
+ style={{
+ whiteSpace: "nowrap",
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ }}
+ >
+ <p>
+ <b>
+ <i18n.Translate>claimed at</i18n.Translate>:
+ </b>{" "}
+ {format(
+ new Date(order.contract_terms.timestamp.t_s * 1000),
+ datetimeFormatForSettings(settings)
+ )}
+ </p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="columns">
+ <div class="column is-4">
+ <div class="title">
+ <i18n.Translate>Timeline</i18n.Translate>
+ </div>
+ <Timeline events={events} />
+ </div>
+ <div class="column is-8">
+ <div class="title">
+ <i18n.Translate>Payment details</i18n.Translate>
+ </div>
+ <FormProvider<Claimed>
+ object={value}
+ valueHandler={valueHandler}
+ >
+ <Input
+ name="contract_terms.summary"
+ readonly
+ inputType="multiline"
+ label={i18n.str`Summary`}
+ />
+ <InputCurrency
+ name="contract_terms.amount"
+ readonly
+ label={i18n.str`Amount`}
+ />
+ <Input<Claimed>
+ name="order_status"
+ readonly
+ label={i18n.str`Order status`}
+ />
+ </FormProvider>
+ </div>
+ </div>
+ </section>
+
+ {order.contract_terms.products.length ? (
+ <Fragment>
+ <div class="title">
+ <i18n.Translate>Product list</i18n.Translate>
+ </div>
+ <ProductList list={order.contract_terms.products} />
+ </Fragment>
+ ) : undefined}
+
+ {value.contract_terms && (
+ <ContractTerms value={value.contract_terms} />
+ )}
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
+function PaidPage({
+ id,
+ order,
+ onRefund,
+}: {
+ id: string;
+ order: MerchantBackend.Orders.CheckPaymentPaidResponse;
+ onRefund: (id: string) => void;
+}) {
+ const events: Event[] = [];
+ if (order.contract_terms.timestamp.t_s !== "never") {
+ events.push({
+ when: new Date(order.contract_terms.timestamp.t_s * 1000),
+ description: "order created",
+ type: "start",
+ });
+ }
+ if (order.contract_terms.pay_deadline.t_s !== "never") {
+ events.push({
+ when: new Date(order.contract_terms.pay_deadline.t_s * 1000),
+ description: "pay deadline",
+ type: "deadline",
+ });
+ }
+ if (order.contract_terms.refund_deadline.t_s !== "never") {
+ events.push({
+ when: new Date(order.contract_terms.refund_deadline.t_s * 1000),
+ description: "refund deadline",
+ type: "deadline",
+ });
+ }
+ if (order.contract_terms.wire_transfer_deadline.t_s !== "never") {
+ events.push({
+ when: new Date(order.contract_terms.wire_transfer_deadline.t_s * 1000),
+ description: "wire deadline",
+ type: "deadline",
+ });
+ }
+ if (
+ order.contract_terms.delivery_date &&
+ order.contract_terms.delivery_date.t_s !== "never"
+ ) {
+ if (order.contract_terms.delivery_date)
+ events.push({
+ when: new Date(order.contract_terms.delivery_date?.t_s * 1000),
+ description: "delivery",
+ type: "delivery",
+ });
+ }
+ order.refund_details.reduce(mergeRefunds, []).forEach((e) => {
+ if (e.timestamp.t_s !== "never") {
+ events.push({
+ when: new Date(e.timestamp.t_s * 1000),
+ description: `refund: ${e.amount}: ${e.reason}`,
+ type: e.pending ? "refund" : "refund-taken",
+ });
+ }
+ });
+ if (order.wire_details && order.wire_details.length) {
+ if (order.wire_details.length > 1) {
+ let last: MerchantBackend.Orders.TransactionWireTransfer | null = null;
+ let first: MerchantBackend.Orders.TransactionWireTransfer | null = null;
+ let total: AmountJson | null = null;
+
+ order.wire_details.forEach((w) => {
+ if (last === null || last.execution_time.t_s < w.execution_time.t_s) {
+ last = w;
+ }
+ if (first === null || first.execution_time.t_s > w.execution_time.t_s) {
+ first = w;
+ }
+ total =
+ total === null
+ ? Amounts.parseOrThrow(w.amount)
+ : Amounts.add(total, Amounts.parseOrThrow(w.amount)).amount;
+ });
+ const last_time = last!.execution_time.t_s;
+ if (last_time !== "never") {
+ events.push({
+ when: new Date(last_time * 1000),
+ description: `wired ${Amounts.stringify(total!)}`,
+ type: "wired-range",
+ });
+ }
+ const first_time = first!.execution_time.t_s;
+ if (first_time !== "never") {
+ events.push({
+ when: new Date(first_time * 1000),
+ description: `wire transfer started...`,
+ type: "wired-range",
+ });
+ }
+ } else {
+ order.wire_details.forEach((e) => {
+ if (e.execution_time.t_s !== "never") {
+ events.push({
+ when: new Date(e.execution_time.t_s * 1000),
+ description: `wired ${e.amount}`,
+ type: "wired",
+ });
+ }
+ });
+ }
+ }
+
+ const now = new Date()
+ const nextEvent = events.find((e) => {
+ return e.when.getTime() > now.getTime()
+ })
+
+ const [value, valueHandler] = useState<Partial<Paid>>(order);
+ const { url: backendURL } = useBackendContext()
+ const refundurl = stringifyRefundUri({
+ merchantBaseUrl: backendURL,
+ orderId: order.contract_terms.order_id
+ })
+ const refundable =
+ new Date().getTime() < order.contract_terms.refund_deadline.t_s * 1000;
+ const { i18n } = useTranslationContext();
+
+ const amount = Amounts.parseOrThrow(order.contract_terms.amount);
+ const refund_taken = order.refund_details.reduce((prev, cur) => {
+ if (cur.pending) return prev;
+ return Amounts.add(prev, Amounts.parseOrThrow(cur.amount)).amount;
+ }, Amounts.zeroOfCurrency(amount.currency));
+ value.refund_taken = Amounts.stringify(refund_taken);
+
+ return (
+ <div>
+ <section class="section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-10">
+ <section class="hero is-hero-bar">
+ <div class="hero-body">
+ <div class="level">
+ <div class="level-left">
+ <div class="level-item">
+ <i18n.Translate>Order</i18n.Translate> #{id}
+ <div class="tag is-success ml-4">
+ <i18n.Translate>paid</i18n.Translate>
+ </div>
+ {order.wired ? (
+ <div class="tag is-success ml-4">
+ <i18n.Translate>wired</i18n.Translate>
+ </div>
+ ) : null}
+ {order.refunded ? (
+ <div class="tag is-danger ml-4">
+ <i18n.Translate>refunded</i18n.Translate>
+ </div>
+ ) : null}
+ </div>
+ </div>
+ </div>
+ <div class="level">
+ <div class="level-left">
+ <div class="level-item">
+ <h1 class="title">{order.contract_terms.amount}</h1>
+ </div>
+ </div>
+ <div class="level-right">
+ <div class="level-item">
+ <h1 class="title">
+ <div class="buttons">
+ <span
+ class="has-tooltip-left"
+ data-tooltip={
+ refundable
+ ? i18n.str`refund order`
+ : i18n.str`not refundable`
+ }
+ >
+ <button
+ class="button is-danger"
+ disabled={!refundable}
+ onClick={() => onRefund(id)}
+ >
+ <i18n.Translate>refund</i18n.Translate>
+ </button>
+ </span>
+ </div>
+ </h1>
+ </div>
+ </div>
+ </div>
+
+ <div class="level">
+ <div class="level-left" style={{ maxWidth: "100%" }}>
+ <div class="level-item" style={{ maxWidth: "100%" }}>
+ <div
+ class="content"
+ style={{
+ whiteSpace: "nowrap",
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ }}
+ >
+ {nextEvent &&
+ <p>
+ <i18n.Translate>Next event in </i18n.Translate> {formatDistance(
+ nextEvent.when,
+ new Date(),
+ // "yyyy/MM/dd HH:mm:ss",
+ )}
+ </p>
+ }
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="columns">
+ <div class="column is-4">
+ <div class="title">
+ <i18n.Translate>Timeline</i18n.Translate>
+ </div>
+ <Timeline events={events} />
+ </div>
+ <div class="column is-8">
+ <div class="title">
+ <i18n.Translate>Payment details</i18n.Translate>
+ </div>
+ <FormProvider<Paid>
+ object={value}
+ valueHandler={valueHandler}
+ >
+ {/* <InputCurrency<Paid> name="deposit_total" readonly label={i18n.str`Deposit total`} /> */}
+ {order.refunded && (
+ <InputCurrency<Paid>
+ name="refund_amount"
+ readonly
+ label={i18n.str`Refunded amount`}
+ />
+ )}
+ {order.refunded && (
+ <InputCurrency<Paid>
+ name="refund_taken"
+ readonly
+ label={i18n.str`Refund taken`}
+ />
+ )}
+ <Input<Paid>
+ name="order_status"
+ readonly
+ label={i18n.str`Order status`}
+ />
+ <TextField<Paid>
+ name="order_status_url"
+ label={i18n.str`Status URL`}
+ >
+ <a
+ target="_blank"
+ rel="noreferrer"
+ href={order.order_status_url}
+ >
+ {order.order_status_url}
+ </a>
+ </TextField>
+ {order.refunded && (
+ <TextField<Paid>
+ name="order_status_url"
+ label={i18n.str`Refund URI`}
+ >
+ <a target="_blank" rel="noreferrer" href={refundurl}>
+ {refundurl}
+ </a>
+ </TextField>
+ )}
+ </FormProvider>
+ </div>
+ </div>
+ </section>
+
+ {order.contract_terms.products.length ? (
+ <Fragment>
+ <div class="title">
+ <i18n.Translate>Product list</i18n.Translate>
+ </div>
+ <ProductList list={order.contract_terms.products} />
+ </Fragment>
+ ) : undefined}
+
+ {value.contract_terms && (
+ <ContractTerms value={value.contract_terms} />
+ )}
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
+
+function UnpaidPage({
+ id,
+ order,
+}: {
+ id: string;
+ order: MerchantBackend.Orders.CheckPaymentUnpaidResponse;
+}) {
+ const [value, valueHandler] = useState<Partial<Unpaid>>(order);
+ const { i18n } = useTranslationContext();
+ const [settings] = useSettings()
+ return (
+ <div>
+ <section class="hero is-hero-bar">
+ <div class="hero-body">
+ <div class="level">
+ <div class="level-left">
+ <div class="level-item">
+ <h1 class="title">
+ <i18n.Translate>Order</i18n.Translate> #{id}
+ </h1>
+ </div>
+ <div class="tag is-dark">
+ <i18n.Translate>unpaid</i18n.Translate>
+ </div>
+ </div>
+ </div>
+
+ <div class="level">
+ <div class="level-left" style={{ maxWidth: "100%" }}>
+ <div class="level-item" style={{ maxWidth: "100%" }}>
+ <div
+ class="content"
+ style={{
+ whiteSpace: "nowrap",
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ }}
+ >
+ <p>
+ <b>
+ <i18n.Translate>pay at</i18n.Translate>:
+ </b>{" "}
+ <a
+ href={order.order_status_url}
+ rel="nofollow"
+ target="new"
+ >
+ {order.order_status_url}
+ </a>
+ </p>
+ <p>
+ <b>
+ <i18n.Translate>created at</i18n.Translate>:
+ </b>{" "}
+ {order.creation_time.t_s === "never"
+ ? "never"
+ : format(
+ new Date(order.creation_time.t_s * 1000),
+ datetimeFormatForSettings(settings)
+ )}
+ </p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <FormProvider<Unpaid> object={value} valueHandler={valueHandler}>
+ <Input<Unpaid>
+ readonly
+ name="summary"
+ label={i18n.str`Summary`}
+ tooltip={i18n.str`human-readable description of the whole purchase`}
+ />
+ <InputCurrency<Unpaid>
+ readonly
+ name="total_amount"
+ label={i18n.str`Amount`}
+ tooltip={i18n.str`total price for the transaction`}
+ />
+ <Input<Unpaid>
+ name="order_status"
+ readonly
+ label={i18n.str`Order status`}
+ />
+ <Input<Unpaid>
+ name="order_status_url"
+ readonly
+ label={i18n.str`Order status URL`}
+ />
+ <TextField<Unpaid>
+ name="taler_pay_uri"
+ label={i18n.str`Payment URI`}
+ >
+ <a target="_blank" rel="noreferrer" href={value.taler_pay_uri}>
+ {value.taler_pay_uri}
+ </a>
+ </TextField>
+ </FormProvider>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
+
+export function DetailPage({ id, selected, onRefund, onBack }: Props): VNode {
+ const [showRefund, setShowRefund] = useState<string | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+ const DetailByStatus = function () {
+ switch (selected.order_status) {
+ case "claimed":
+ return <ClaimedPage id={id} order={selected} />;
+ case "paid":
+ return <PaidPage id={id} order={selected} onRefund={setShowRefund} />;
+ case "unpaid":
+ return <UnpaidPage id={id} order={selected} />;
+ default:
+ return (
+ <div>
+ <i18n.Translate>
+ Unknown order status. This is an error, please contact the
+ administrator.
+ </i18n.Translate>
+ </div>
+ );
+ }
+ };
+
+ return (
+ <Fragment>
+ {DetailByStatus()}
+ {showRefund && (
+ <RefundModal
+ order={selected}
+ onCancel={() => setShowRefund(undefined)}
+ onConfirm={(value) => {
+ onRefund(showRefund, value);
+ setShowRefund(undefined);
+ }}
+ />
+ )}
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <div class="buttons is-right mt-5">
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Back</i18n.Translate>
+ </button>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ </Fragment>
+ );
+}
+
+async function copyToClipboard(text: string) {
+ return navigator.clipboard.writeText(text);
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/details/Timeline.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/details/Timeline.tsx
new file mode 100644
index 000000000..8c863f386
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/orders/details/Timeline.tsx
@@ -0,0 +1,129 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { format } from "date-fns";
+import { h } from "preact";
+import { useEffect, useState } from "preact/hooks";
+import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
+
+interface Props {
+ events: Event[];
+}
+
+export function Timeline({ events: e }: Props) {
+ const events = [...e];
+ events.push({
+ when: new Date(),
+ description: "now",
+ type: "now",
+ });
+
+ events.sort((a, b) => a.when.getTime() - b.when.getTime());
+ const [settings] = useSettings();
+ const [state, setState] = useState(events);
+ useEffect(() => {
+ const handle = setTimeout(() => {
+ const eventsWithoutNow = state.filter((e) => e.type !== "now");
+ eventsWithoutNow.push({
+ when: new Date(),
+ description: "now",
+ type: "now",
+ });
+ setState(eventsWithoutNow);
+ }, 1000);
+ return () => {
+ clearTimeout(handle);
+ };
+ });
+ return (
+ <div class="timeline">
+ {events.map((e, i) => {
+ return (
+ <div key={i} class="timeline-item">
+ {(() => {
+ switch (e.type) {
+ case "deadline":
+ return (
+ <div class="timeline-marker is-icon ">
+ <i class="mdi mdi-flag" />
+ </div>
+ );
+ case "delivery":
+ return (
+ <div class="timeline-marker is-icon ">
+ <i class="mdi mdi-delivery" />
+ </div>
+ );
+ case "start":
+ return (
+ <div class="timeline-marker is-icon">
+ <i class="mdi mdi-flag " />
+ </div>
+ );
+ case "wired":
+ return (
+ <div class="timeline-marker is-icon is-success">
+ <i class="mdi mdi-cash" />
+ </div>
+ );
+ case "wired-range":
+ return (
+ <div class="timeline-marker is-icon is-success">
+ <i class="mdi mdi-cash" />
+ </div>
+ );
+ case "refund":
+ return (
+ <div class="timeline-marker is-icon is-danger">
+ <i class="mdi mdi-cash" />
+ </div>
+ );
+ case "refund-taken":
+ return (
+ <div class="timeline-marker is-icon is-success">
+ <i class="mdi mdi-cash" />
+ </div>
+ );
+ case "now":
+ return (
+ <div class="timeline-marker is-icon is-info">
+ <i class="mdi mdi-clock" />
+ </div>
+ );
+ }
+ })()}
+ <div class="timeline-content">
+ {e.description !== "now" && <p class="heading">{format(e.when, datetimeFormatForSettings(settings))}</p>}
+ <p>{e.description}</p>
+ </div>
+ </div>
+ );
+ })}
+ </div>
+ );
+}
+export interface Event {
+ when: Date;
+ description: string;
+ type:
+ | "start"
+ | "refund"
+ | "refund-taken"
+ | "wired"
+ | "wired-range"
+ | "deadline"
+ | "delivery"
+ | "now";
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/details/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/details/index.tsx
new file mode 100644
index 000000000..1517a3c42
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/orders/details/index.tsx
@@ -0,0 +1,95 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ useTranslationContext,
+ HttpError,
+ ErrorType,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useOrderAPI, useOrderDetails } from "../../../../hooks/order.js";
+import { Notification } from "../../../../utils/types.js";
+import { DetailPage } from "./DetailPage.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+
+export interface Props {
+ oid: string;
+
+ onBack: () => void;
+ onUnauthorized: () => VNode;
+ onNotFound: () => VNode;
+ onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+}
+
+export default function Update({
+ oid,
+ onBack,
+ onLoadError,
+ onNotFound,
+ onUnauthorized,
+}: Props): VNode {
+ const { refundOrder } = useOrderAPI();
+ const result = useOrderDetails(oid);
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+
+ const { i18n } = useTranslationContext();
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+
+ <DetailPage
+ onBack={onBack}
+ id={oid}
+ onRefund={(id, value) =>
+ refundOrder(id, value)
+ .then(() =>
+ setNotif({
+ message: i18n.str`refund created successfully`,
+ type: "SUCCESS",
+ }),
+ )
+ .catch((error) =>
+ setNotif({
+ message: i18n.str`could not create the refund`,
+ type: "ERROR",
+ description: error.message,
+ }),
+ )
+ }
+ selected={result.data}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/list/List.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/list/List.stories.tsx
new file mode 100644
index 000000000..156c577f4
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/orders/list/List.stories.tsx
@@ -0,0 +1,107 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { ListPage as TestedComponent } from "./ListPage.js";
+
+export default {
+ title: "Pages/Order/List",
+ component: TestedComponent,
+ argTypes: {
+ onShowAll: { action: "onShowAll" },
+ onShowPaid: { action: "onShowPaid" },
+ onShowRefunded: { action: "onShowRefunded" },
+ onShowNotWired: { action: "onShowNotWired" },
+ onCopyURL: { action: "onCopyURL" },
+ onSelectDate: { action: "onSelectDate" },
+ onLoadMoreBefore: { action: "onLoadMoreBefore" },
+ onLoadMoreAfter: { action: "onLoadMoreAfter" },
+ onSelectOrder: { action: "onSelectOrder" },
+ onRefundOrder: { action: "onRefundOrder" },
+ onSearchOrderById: { action: "onSearchOrderById" },
+ onCreate: { action: "onCreate" },
+ },
+};
+
+function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => <Component {...args} />;
+ r.args = props;
+ return r;
+}
+
+export const Example = createExample(TestedComponent, {
+ orders: [
+ {
+ id: "123",
+ amount: "TESTKUDOS:10",
+ paid: false,
+ refundable: true,
+ row_id: 1,
+ summary: "summary",
+ timestamp: {
+ t_s: new Date().getTime() / 1000,
+ },
+ order_id: "123",
+ },
+ {
+ id: "234",
+ amount: "TESTKUDOS:12",
+ paid: true,
+ refundable: true,
+ row_id: 2,
+ summary:
+ "summary with long text, very very long text that someone want to add as a description of the order",
+ timestamp: {
+ t_s: new Date().getTime() / 1000,
+ },
+ order_id: "234",
+ },
+ {
+ id: "456",
+ amount: "TESTKUDOS:1",
+ paid: false,
+ refundable: false,
+ row_id: 3,
+ summary:
+ "summary with long text, very very long text that someone want to add as a description of the order",
+ timestamp: {
+ t_s: new Date().getTime() / 1000,
+ },
+ order_id: "456",
+ },
+ {
+ id: "234",
+ amount: "TESTKUDOS:12",
+ paid: false,
+ refundable: false,
+ row_id: 4,
+ summary:
+ "summary with long text, very very long text that someone want to add as a description of the order",
+ timestamp: {
+ t_s: new Date().getTime() / 1000,
+ },
+ order_id: "234",
+ },
+ ],
+});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx
new file mode 100644
index 000000000..9f80719a1
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx
@@ -0,0 +1,226 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { format } from "date-fns";
+import { h, VNode, Fragment } from "preact";
+import { useState } from "preact/hooks";
+import { DatePicker } from "../../../../components/picker/DatePicker.js";
+import { MerchantBackend, WithId } from "../../../../declaration.js";
+import { CardTable } from "./Table.js";
+import { dateFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
+
+export interface ListPageProps {
+ onShowAll: () => void;
+ onShowNotPaid: () => void;
+ onShowPaid: () => void;
+ onShowRefunded: () => void;
+ onShowNotWired: () => void;
+ onShowWired: () => void;
+ onCopyURL: (id: string) => void;
+ isAllActive: string;
+ isPaidActive: string;
+ isNotPaidActive: string;
+ isRefundedActive: string;
+ isNotWiredActive: string;
+ isWiredActive: string;
+
+ jumpToDate?: Date;
+ onSelectDate: (date?: Date) => void;
+
+ orders: (MerchantBackend.Orders.OrderHistoryEntry & WithId)[];
+ onLoadMoreBefore?: () => void;
+ hasMoreBefore?: boolean;
+ hasMoreAfter?: boolean;
+ onLoadMoreAfter?: () => void;
+
+ onSelectOrder: (o: MerchantBackend.Orders.OrderHistoryEntry & WithId) => void;
+ onRefundOrder: (o: MerchantBackend.Orders.OrderHistoryEntry & WithId) => void;
+ onCreate: () => void;
+}
+
+export function ListPage({
+ hasMoreAfter,
+ hasMoreBefore,
+ onLoadMoreAfter,
+ onLoadMoreBefore,
+ orders,
+ isAllActive,
+ onSelectOrder,
+ onRefundOrder,
+ jumpToDate,
+ onCopyURL,
+ onShowAll,
+ onShowPaid,
+ onShowNotPaid,
+ onShowRefunded,
+ onShowNotWired,
+ onShowWired,
+ onSelectDate,
+ isPaidActive,
+ isRefundedActive,
+ isNotWiredActive,
+ onCreate,
+ isNotPaidActive,
+ isWiredActive,
+}: ListPageProps): VNode {
+ const { i18n } = useTranslationContext();
+ const dateTooltip = i18n.str`select date to show nearby orders`;
+ const [pickDate, setPickDate] = useState(false);
+ const [settings] = useSettings();
+
+ return (
+ <Fragment>
+ <div class="columns">
+ <div class="column is-two-thirds">
+ <div class="tabs" style={{ overflow: "inherit" }}>
+ <ul>
+ <li class={isNotPaidActive}>
+ <div
+ class="has-tooltip-right"
+ data-tooltip={i18n.str`only show paid orders`}
+ >
+ <a onClick={onShowNotPaid}>
+ <i18n.Translate>New</i18n.Translate>
+ </a>
+ </div>
+ </li>
+ <li class={isPaidActive}>
+ <div
+ class="has-tooltip-right"
+ data-tooltip={i18n.str`only show paid orders`}
+ >
+ <a onClick={onShowPaid}>
+ <i18n.Translate>Paid</i18n.Translate>
+ </a>
+ </div>
+ </li>
+ <li class={isRefundedActive}>
+ <div
+ class="has-tooltip-right"
+ data-tooltip={i18n.str`only show orders with refunds`}
+ >
+ <a onClick={onShowRefunded}>
+ <i18n.Translate>Refunded</i18n.Translate>
+ </a>
+ </div>
+ </li>
+ <li class={isNotWiredActive}>
+ <div
+ class="has-tooltip-left"
+ data-tooltip={i18n.str`only show orders where customers paid, but wire payments from payment provider are still pending`}
+ >
+ <a onClick={onShowNotWired}>
+ <i18n.Translate>Not wired</i18n.Translate>
+ </a>
+ </div>
+ </li>
+ <li class={isWiredActive}>
+ <div
+ class="has-tooltip-left"
+ data-tooltip={i18n.str`only show orders where customers paid, but wire payments from payment provider are still pending`}
+ >
+ <a onClick={onShowWired}>
+ <i18n.Translate>Completed</i18n.Translate>
+ </a>
+ </div>
+ </li>
+ <li class={isAllActive}>
+ <div
+ class="has-tooltip-right"
+ data-tooltip={i18n.str`remove all filters`}
+ >
+ <a onClick={onShowAll}>
+ <i18n.Translate>All</i18n.Translate>
+ </a>
+ </div>
+ </li>
+ </ul>
+ </div>
+ </div>
+ <div class="column ">
+ <div class="buttons is-right">
+ <div class="field has-addons">
+ {jumpToDate && (
+ <div class="control">
+ <a class="button is-fullwidth" onClick={() => onSelectDate(undefined)}>
+ <span
+ class="icon"
+ data-tooltip={i18n.str`clear date filter`}
+ >
+ <i class="mdi mdi-close" />
+ </span>
+ </a>
+ </div>
+ )}
+ <div class="control">
+ <span class="has-tooltip-top" data-tooltip={dateTooltip}>
+ <input
+ class="input"
+ type="text"
+ readonly
+ value={!jumpToDate ? "" : format(jumpToDate, dateFormatForSettings(settings))}
+ placeholder={i18n.str`date (${dateFormatForSettings(settings)})`}
+ onClick={() => {
+ setPickDate(true);
+ }}
+ />
+ </span>
+ </div>
+ <div class="control">
+ <span class="has-tooltip-left" data-tooltip={dateTooltip}>
+ <a
+ class="button is-fullwidth"
+ onClick={() => {
+ setPickDate(true);
+ }}
+ >
+ <span class="icon">
+ <i class="mdi mdi-calendar" />
+ </span>
+ </a>
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <DatePicker
+ opened={pickDate}
+ closeFunction={() => setPickDate(false)}
+ dateReceiver={onSelectDate}
+ />
+
+ <CardTable
+ orders={orders}
+ onCreate={onCreate}
+ onCopyURL={onCopyURL}
+ onSelect={onSelectOrder}
+ onRefund={onRefundOrder}
+ hasMoreAfter={hasMoreAfter}
+ hasMoreBefore={hasMoreBefore}
+ onLoadMoreAfter={onLoadMoreAfter}
+ onLoadMoreBefore={onLoadMoreBefore}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/list/Table.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/list/Table.tsx
new file mode 100644
index 000000000..b2806bb79
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/orders/list/Table.tsx
@@ -0,0 +1,417 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { Amounts } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { format } from "date-fns";
+import { h, VNode } from "preact";
+import { StateUpdater, useState } from "preact/hooks";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { InputCurrency } from "../../../../components/form/InputCurrency.js";
+import { InputGroup } from "../../../../components/form/InputGroup.js";
+import { InputSelector } from "../../../../components/form/InputSelector.js";
+import { ConfirmModal } from "../../../../components/modal/index.js";
+import { useConfigContext } from "../../../../context/config.js";
+import { MerchantBackend, WithId } from "../../../../declaration.js";
+import { mergeRefunds } from "../../../../utils/amount.js";
+import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
+
+type Entity = MerchantBackend.Orders.OrderHistoryEntry & WithId;
+interface Props {
+ orders: Entity[];
+ onRefund: (value: Entity) => void;
+ onCopyURL: (id: string) => void;
+ onCreate: () => void;
+ onSelect: (order: Entity) => void;
+ onLoadMoreBefore?: () => void;
+ hasMoreBefore?: boolean;
+ hasMoreAfter?: boolean;
+ onLoadMoreAfter?: () => void;
+}
+
+export function CardTable({
+ orders,
+ onCreate,
+ onRefund,
+ onCopyURL,
+ onSelect,
+ onLoadMoreAfter,
+ onLoadMoreBefore,
+ hasMoreAfter,
+ hasMoreBefore,
+}: Props): VNode {
+ const [rowSelection, rowSelectionHandler] = useState<string[]>([]);
+
+ const { i18n } = useTranslationContext();
+
+ return (
+ <div class="card has-table">
+ <header class="card-header">
+ <p class="card-header-title">
+ <span class="icon">
+ <i class="mdi mdi-cash-register" />
+ </span>
+ <i18n.Translate>Orders</i18n.Translate>
+ </p>
+
+ <div class="card-header-icon" aria-label="more options" />
+
+ <div class="card-header-icon" aria-label="more options">
+ <span class="has-tooltip-left" data-tooltip={i18n.str`create order`}>
+ <button class="button is-info" type="button" onClick={onCreate}>
+ <span class="icon is-small">
+ <i class="mdi mdi-plus mdi-36px" />
+ </span>
+ </button>
+ </span>
+ </div>
+ </header>
+ <div class="card-content">
+ <div class="b-table has-pagination">
+ <div class="table-wrapper has-mobile-cards">
+ {orders.length > 0 ? (
+ <Table
+ instances={orders}
+ onSelect={onSelect}
+ onRefund={onRefund}
+ onCopyURL={(o) => onCopyURL(o.id)}
+ rowSelection={rowSelection}
+ rowSelectionHandler={rowSelectionHandler}
+ onLoadMoreAfter={onLoadMoreAfter}
+ onLoadMoreBefore={onLoadMoreBefore}
+ hasMoreAfter={hasMoreAfter}
+ hasMoreBefore={hasMoreBefore}
+ />
+ ) : (
+ <EmptyTable />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+}
+interface TableProps {
+ rowSelection: string[];
+ instances: Entity[];
+ onRefund: (id: Entity) => void;
+ onCopyURL: (id: Entity) => void;
+ onSelect: (id: Entity) => void;
+ rowSelectionHandler: StateUpdater<string[]>;
+ onLoadMoreBefore?: () => void;
+ hasMoreBefore?: boolean;
+ hasMoreAfter?: boolean;
+ onLoadMoreAfter?: () => void;
+}
+
+function Table({
+ instances,
+ onSelect,
+ onRefund,
+ onCopyURL,
+ onLoadMoreAfter,
+ onLoadMoreBefore,
+ hasMoreAfter,
+ hasMoreBefore,
+}: TableProps): VNode {
+ const { i18n } = useTranslationContext();
+ const [settings] = useSettings();
+ return (
+ <div class="table-container">
+ {hasMoreBefore && (
+ <button
+ class="button is-fullwidth"
+ onClick={onLoadMoreBefore}
+ >
+ <i18n.Translate>load newer orders</i18n.Translate>
+ </button>
+ )}
+ <table class="table is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th style={{ minWidth: 100 }}>
+ <i18n.Translate>Date</i18n.Translate>
+ </th>
+ <th style={{ minWidth: 100 }}>
+ <i18n.Translate>Amount</i18n.Translate>
+ </th>
+ <th style={{ minWidth: 400 }}>
+ <i18n.Translate>Summary</i18n.Translate>
+ </th>
+ <th style={{ minWidth: 50 }} />
+ </tr>
+ </thead>
+ <tbody>
+ {instances.map((i) => {
+ return (
+ <tr key={i.id}>
+ <td
+ onClick={(): void => onSelect(i)}
+ style={{ cursor: "pointer" }}
+ >
+ {i.timestamp.t_s === "never"
+ ? "never"
+ : format(
+ new Date(i.timestamp.t_s * 1000),
+ datetimeFormatForSettings(settings),
+ )}
+ </td>
+ <td
+ onClick={(): void => onSelect(i)}
+ style={{ cursor: "pointer" }}
+ >
+ {i.amount}
+ </td>
+ <td
+ onClick={(): void => onSelect(i)}
+ style={{ cursor: "pointer" }}
+ >
+ {i.summary}
+ </td>
+ <td class="is-actions-cell right-sticky">
+ <div class="buttons is-right">
+ {i.refundable && (
+ <button
+ class="button is-small is-danger jb-modal"
+ type="button"
+ onClick={(): void => onRefund(i)}
+ >
+ <i18n.Translate>Refund</i18n.Translate>
+ </button>
+ )}
+ {!i.paid && (
+ <button
+ class="button is-small is-info jb-modal"
+ type="button"
+ onClick={(): void => onCopyURL(i)}
+ >
+ <i18n.Translate>copy url</i18n.Translate>
+ </button>
+ )}
+ </div>
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ {hasMoreAfter && (
+ <button
+ class="button is-fullwidth"
+ onClick={onLoadMoreAfter}
+ >
+ <i18n.Translate>load older orders</i18n.Translate>
+ </button>
+ )}
+ </div>
+ );
+}
+
+function EmptyTable(): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="content has-text-grey has-text-centered">
+ <p>
+ <span class="icon is-large">
+ <i class="mdi mdi-emoticon-sad mdi-48px" />
+ </span>
+ </p>
+ <p>
+ <i18n.Translate>
+ No orders have been found matching your query!
+ </i18n.Translate>
+ </p>
+ </div>
+ );
+}
+
+interface RefundModalProps {
+ onCancel: () => void;
+ onConfirm: (value: MerchantBackend.Orders.RefundRequest) => void;
+ order: MerchantBackend.Orders.MerchantOrderStatusResponse;
+}
+
+export function RefundModal({
+ order,
+ onCancel,
+ onConfirm,
+}: RefundModalProps): VNode {
+ type State = { mainReason?: string; description?: string; refund?: string };
+ const [form, setValue] = useState<State>({});
+ const [settings] = useSettings();
+ const { i18n } = useTranslationContext();
+ // const [errors, setErrors] = useState<FormErrors<State>>({});
+
+ const refunds = (
+ order.order_status === "paid" ? order.refund_details : []
+ ).reduce(mergeRefunds, []);
+
+ const config = useConfigContext();
+ const totalRefunded = refunds
+ .map((r) => r.amount)
+ .reduce(
+ (p, c) => Amounts.add(p, Amounts.parseOrThrow(c)).amount,
+ Amounts.zeroOfCurrency(config.currency),
+ );
+ const orderPrice =
+ order.order_status === "paid"
+ ? Amounts.parseOrThrow(order.contract_terms.amount)
+ : undefined;
+ const totalRefundable = !orderPrice
+ ? Amounts.zeroOfCurrency(totalRefunded.currency)
+ : refunds.length
+ ? Amounts.sub(orderPrice, totalRefunded).amount
+ : orderPrice;
+
+ const isRefundable = Amounts.isNonZero(totalRefundable);
+ const duplicatedText = i18n.str`duplicated`;
+
+ const errors: FormErrors<State> = {
+ mainReason: !form.mainReason ? i18n.str`required` : undefined,
+ description:
+ !form.description && form.mainReason !== duplicatedText
+ ? i18n.str`required`
+ : undefined,
+ refund: !form.refund
+ ? i18n.str`required`
+ : !Amounts.parse(form.refund)
+ ? i18n.str`invalid format`
+ : Amounts.cmp(totalRefundable, Amounts.parse(form.refund)!) === -1
+ ? i18n.str`this value exceed the refundable amount`
+ : undefined,
+ };
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const validateAndConfirm = () => {
+ try {
+ if (!form.refund) return;
+ onConfirm({
+ refund: Amounts.stringify(
+ Amounts.add(Amounts.parse(form.refund)!, totalRefunded).amount,
+ ),
+ reason:
+ form.description === undefined
+ ? form.mainReason || ""
+ : `${form.mainReason}: ${form.description}`,
+ });
+ } catch (err) {
+ console.log(err);
+ }
+ };
+
+ //FIXME: parameters in the translation
+ return (
+ <ConfirmModal
+ description="refund"
+ danger
+ active
+ disabled={!isRefundable || hasErrors}
+ onCancel={onCancel}
+ onConfirm={validateAndConfirm}
+ >
+ {refunds.length > 0 && (
+ <div class="columns">
+ <div class="column is-12">
+ <InputGroup
+ name="asd"
+ label={`${Amounts.stringify(totalRefunded)} was already refunded`}
+ >
+ <table class="table is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>date</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>amount</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>reason</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ {refunds.map((r) => {
+ return (
+ <tr key={r.timestamp.t_s}>
+ <td>
+ {r.timestamp.t_s === "never"
+ ? "never"
+ : format(
+ new Date(r.timestamp.t_s * 1000),
+ datetimeFormatForSettings(settings),
+ )}
+ </td>
+ <td>{r.amount}</td>
+ <td>{r.reason}</td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </InputGroup>
+ </div>
+ </div>
+ )}
+
+ {isRefundable && (
+ <FormProvider<State>
+ errors={errors}
+ object={form}
+ valueHandler={(d) => setValue(d as any)}
+ >
+ <InputCurrency<State>
+ name="refund"
+ label={i18n.str`Refund`}
+ tooltip={i18n.str`amount to be refunded`}
+ >
+ <i18n.Translate>Max refundable:</i18n.Translate>{" "}
+ {Amounts.stringify(totalRefundable)}
+ </InputCurrency>
+ <InputSelector
+ name="mainReason"
+ label={i18n.str`Reason`}
+ values={[
+ i18n.str`Choose one...`,
+ duplicatedText,
+ i18n.str`requested by the customer`,
+ i18n.str`other`,
+ ]}
+ tooltip={i18n.str`why this order is being refunded`}
+ />
+ {form.mainReason && form.mainReason !== duplicatedText ? (
+ <Input<State>
+ label={i18n.str`Description`}
+ name="description"
+ tooltip={i18n.str`more information to give context`}
+ />
+ ) : undefined}
+ </FormProvider>
+ )}
+ </ConfirmModal>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/list/index.tsx
new file mode 100644
index 000000000..92e714fb8
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/orders/list/index.tsx
@@ -0,0 +1,231 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import {
+ InstanceOrderFilter,
+ useInstanceOrders,
+ useOrderAPI,
+ useOrderDetails,
+} from "../../../../hooks/order.js";
+import { Notification } from "../../../../utils/types.js";
+import { ListPage } from "./ListPage.js";
+import { RefundModal } from "./Table.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+import { JumpToElementById } from "../../../../components/form/JumpToElementById.js";
+
+interface Props {
+ onUnauthorized: () => VNode;
+ onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ onNotFound: () => VNode;
+ onSelect: (id: string) => void;
+ onCreate: () => void;
+}
+
+export default function OrderList({
+ onUnauthorized,
+ onLoadError,
+ onCreate,
+ onSelect,
+ onNotFound,
+}: Props): VNode {
+ const [filter, setFilter] = useState<InstanceOrderFilter>({ paid: "no" });
+ const [orderToBeRefunded, setOrderToBeRefunded] = useState<
+ MerchantBackend.Orders.OrderHistoryEntry | undefined
+ >(undefined);
+
+ const setNewDate = (date?: Date): void =>
+ setFilter((prev) => ({ ...prev, date }));
+
+ const result = useInstanceOrders(filter, setNewDate);
+ const { refundOrder, getPaymentURL } = useOrderAPI();
+
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+
+ const { i18n } = useTranslationContext();
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ const isNotPaidActive = filter.paid === "no" ? "is-active" : "";
+ const isPaidActive = filter.paid === "yes" && filter.wired === undefined ? "is-active" : "";
+ const isRefundedActive = filter.refunded === "yes" ? "is-active" : "";
+ const isNotWiredActive = filter.wired === "no" && filter.paid === "yes" ? "is-active" : "";
+ const isWiredActive = filter.wired === "yes" ? "is-active" : "";
+ const isAllActive =
+ filter.paid === undefined &&
+ filter.refunded === undefined &&
+ filter.wired === undefined
+ ? "is-active"
+ : "";
+
+ return (
+ <section class="section is-main-section">
+ <NotificationCard notification={notif} />
+
+ <JumpToElementById
+ testIfExist={getPaymentURL}
+ onSelect={onSelect}
+ description={i18n.str`jump to order with the given product ID`}
+ 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
new file mode 100644
index 000000000..26f851cc8
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/Create.stories.tsx
@@ -0,0 +1,28 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { CreatePage as TestedComponent } from "./CreatePage.js";
+
+export default {
+ title: "Pages/OtpDevices/Create",
+ component: TestedComponent,
+};
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx
new file mode 100644
index 000000000..ffeaa064a
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx
@@ -0,0 +1,179 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+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
new file mode 100644
index 000000000..db3842711
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/CreatedSuccessfully.tsx
@@ -0,0 +1,104 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { QR } from "../../../../components/exception/QR.js";
+import { CreatedSuccessfully as Template } from "../../../../components/notifications/CreatedSuccessfully.js";
+import { useInstanceContext } from "../../../../context/instance.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useBackendContext } from "../../../../context/backend.js";
+
+type Entity = MerchantBackend.OTP.OtpDeviceAddDetails;
+
+interface Props {
+ entity: Entity;
+ onConfirm: () => void;
+}
+
+function isNotUndefined<X>(x: X | undefined): x is X {
+ return !!x;
+}
+
+export function CreatedSuccessfully({
+ entity,
+ onConfirm,
+}: Props): VNode {
+ const { i18n } = useTranslationContext();
+ const { url: backendURL } = useBackendContext()
+ const { id: instanceId } = useInstanceContext();
+ const issuer = new URL(backendURL).hostname;
+ const qrText = `otpauth://totp/${instanceId}/${entity.otp_device_id}?issuer=${issuer}&algorithm=SHA1&digits=8&period=30&secret=${entity.otp_key}`;
+ const qrTextSafe = `otpauth://totp/${instanceId}/${entity.otp_device_id}?issuer=${issuer}&algorithm=SHA1&digits=8&period=30&secret=${entity.otp_key.substring(0, 6)}...`;
+
+ return (
+ <Template onConfirm={onConfirm} >
+ <p class="is-size-5">
+ <i18n.Translate>
+ You can scan the next QR code with your device or safe the key before continue.
+ </i18n.Translate>
+ </p>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">ID</label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input
+ readonly
+ class="input"
+ value={entity.otp_device_id}
+ />
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label"><i18n.Translate>Description</i18n.Translate></label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input
+ class="input"
+ readonly
+ value={entity.otp_device_description}
+ />
+ </p>
+ </div>
+ </div>
+ </div>
+ <QR
+ text={qrText}
+ />
+ <div
+ style={{
+ color: "grey",
+ fontSize: "small",
+ width: 200,
+ textAlign: "center",
+ margin: "auto",
+ wordBreak: "break-all",
+ }}
+ >
+ {qrTextSafe}
+ </div>
+ </Template>
+ );
+}
+
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/index.tsx
new file mode 100644
index 000000000..648846793
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/index.tsx
@@ -0,0 +1,70 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useWebhookAPI } from "../../../../hooks/webhooks.js";
+import { Notification } from "../../../../utils/types.js";
+import { CreatePage } from "./CreatePage.js";
+import { useOtpDeviceAPI } from "../../../../hooks/otp.js";
+import { CreatedSuccessfully } from "./CreatedSuccessfully.js";
+
+export type Entity = MerchantBackend.OTP.OtpDeviceAddDetails;
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+}
+
+export default function CreateValidator({ onConfirm, onBack }: Props): VNode {
+ const { createOtpDevice } = useOtpDeviceAPI();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+ const [created, setCreated] = useState<MerchantBackend.OTP.OtpDeviceAddDetails | null>(null)
+
+ if (created) {
+ return <CreatedSuccessfully entity={created} onConfirm={onConfirm} />
+ }
+
+ return (
+ <>
+ <NotificationCard notification={notif} />
+ <CreatePage
+ onBack={onBack}
+ onCreate={(request: Entity) => {
+ return createOtpDevice(request)
+ .then((d) => {
+ setCreated(request)
+ })
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not create device`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/List.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/List.stories.tsx
new file mode 100644
index 000000000..b18049674
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/List.stories.tsx
@@ -0,0 +1,28 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { FunctionalComponent, h } from "preact";
+import { ListPage as TestedComponent } from "./ListPage.js";
+
+export default {
+ title: "Pages/OtpDevices/List",
+ component: TestedComponent,
+};
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/ListPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/ListPage.tsx
new file mode 100644
index 000000000..4efee9781
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/ListPage.tsx
@@ -0,0 +1,64 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode } from "preact";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { CardTable } from "./Table.js";
+
+export interface Props {
+ devices: MerchantBackend.OTP.OtpDeviceEntry[];
+ onLoadMoreBefore?: () => void;
+ onLoadMoreAfter?: () => void;
+ onCreate: () => void;
+ onDelete: (e: MerchantBackend.OTP.OtpDeviceEntry) => void;
+ onSelect: (e: MerchantBackend.OTP.OtpDeviceEntry) => void;
+}
+
+export function ListPage({
+ devices,
+ onCreate,
+ onDelete,
+ onSelect,
+ onLoadMoreBefore,
+ onLoadMoreAfter,
+}: Props): VNode {
+ const form = { payto_uri: "" };
+
+ const { i18n } = useTranslationContext();
+ return (
+ <section class="section is-main-section">
+ <CardTable
+ devices={devices.map((o) => ({
+ ...o,
+ id: String(o.otp_device_id),
+ }))}
+ onCreate={onCreate}
+ onDelete={onDelete}
+ onSelect={onSelect}
+ onLoadMoreBefore={onLoadMoreBefore}
+ hasMoreBefore={!onLoadMoreBefore}
+ onLoadMoreAfter={onLoadMoreAfter}
+ hasMoreAfter={!onLoadMoreAfter}
+ />
+ </section>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx
new file mode 100644
index 000000000..0c28027fe
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx
@@ -0,0 +1,211 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { StateUpdater, useState } from "preact/hooks";
+import { MerchantBackend } from "../../../../declaration.js";
+
+type Entity = MerchantBackend.OTP.OtpDeviceEntry;
+
+interface Props {
+ devices: Entity[];
+ onDelete: (e: Entity) => void;
+ onSelect: (e: Entity) => void;
+ onCreate: () => void;
+ onLoadMoreBefore?: () => void;
+ hasMoreBefore?: boolean;
+ hasMoreAfter?: boolean;
+ onLoadMoreAfter?: () => void;
+}
+
+export function CardTable({
+ devices,
+ onCreate,
+ onDelete,
+ onSelect,
+ onLoadMoreAfter,
+ onLoadMoreBefore,
+ hasMoreAfter,
+ hasMoreBefore,
+}: Props): VNode {
+ const [rowSelection, rowSelectionHandler] = useState<string[]>([]);
+
+ const { i18n } = useTranslationContext();
+
+ return (
+ <div class="card has-table">
+ <header class="card-header">
+ <p class="card-header-title">
+ <span class="icon">
+ <i class="mdi mdi-newspaper" />
+ </span>
+ <i18n.Translate>OTP Devices</i18n.Translate>
+ </p>
+ <div class="card-header-icon" aria-label="more options">
+ <span
+ class="has-tooltip-left"
+ data-tooltip={i18n.str`add new devices`}
+ >
+ <button class="button is-info" type="button" onClick={onCreate}>
+ <span class="icon is-small">
+ <i class="mdi mdi-plus mdi-36px" />
+ </span>
+ </button>
+ </span>
+ </div>
+ </header>
+ <div class="card-content">
+ <div class="b-table has-pagination">
+ <div class="table-wrapper has-mobile-cards">
+ {devices.length > 0 ? (
+ <Table
+ instances={devices}
+ onDelete={onDelete}
+ onSelect={onSelect}
+ rowSelection={rowSelection}
+ rowSelectionHandler={rowSelectionHandler}
+ onLoadMoreAfter={onLoadMoreAfter}
+ onLoadMoreBefore={onLoadMoreBefore}
+ hasMoreAfter={hasMoreAfter}
+ hasMoreBefore={hasMoreBefore}
+ />
+ ) : (
+ <EmptyTable />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+}
+interface TableProps {
+ rowSelection: string[];
+ instances: Entity[];
+ onDelete: (e: Entity) => void;
+ onSelect: (e: Entity) => void;
+ rowSelectionHandler: StateUpdater<string[]>;
+ onLoadMoreBefore?: () => void;
+ hasMoreBefore?: boolean;
+ hasMoreAfter?: boolean;
+ onLoadMoreAfter?: () => void;
+}
+
+function toggleSelected<T>(id: T): (prev: T[]) => T[] {
+ return (prev: T[]): T[] =>
+ prev.indexOf(id) == -1 ? [...prev, id] : prev.filter((e) => e != id);
+}
+
+function Table({
+ instances,
+ onLoadMoreAfter,
+ onDelete,
+ onSelect,
+ onLoadMoreBefore,
+ hasMoreAfter,
+ hasMoreBefore,
+}: TableProps): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="table-container">
+ {hasMoreBefore && (
+ <button
+ class="button is-fullwidth"
+ data-tooltip={i18n.str`load more devices before the first one`}
+ onClick={onLoadMoreBefore}
+ >
+ <i18n.Translate>load newer devices</i18n.Translate>
+ </button>
+ )}
+ <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>ID</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Description</i18n.Translate>
+ </th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ {instances.map((i) => {
+ return (
+ <tr key={i.otp_device_id}>
+ <td
+ onClick={(): void => onSelect(i)}
+ style={{ cursor: "pointer" }}
+ >
+ {i.otp_device_id}
+ </td>
+ <td
+ onClick={(): void => onSelect(i)}
+ style={{ cursor: "pointer" }}
+ >
+ {i.otp_device_id}
+ </td>
+ <td class="is-actions-cell right-sticky">
+ <div class="buttons is-right">
+ <button
+ class="button is-danger is-small has-tooltip-left"
+ data-tooltip={i18n.str`delete selected devices from the database`}
+ onClick={() => onDelete(i)}
+ >
+ Delete
+ </button>
+ </div>
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ {hasMoreAfter && (
+ <button
+ class="button is-fullwidth"
+ data-tooltip={i18n.str`load more devices after the last one`}
+ onClick={onLoadMoreAfter}
+ >
+ <i18n.Translate>load older devices</i18n.Translate>
+ </button>
+ )}
+ </div>
+ );
+}
+
+function EmptyTable(): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="content has-text-grey has-text-centered">
+ <p>
+ <span class="icon is-large">
+ <i class="mdi mdi-emoticon-sad mdi-48px" />
+ </span>
+ </p>
+ <p>
+ <i18n.Translate>
+ There is no devices yet, add more pressing the + sign
+ </i18n.Translate>
+ </p>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx
new file mode 100644
index 000000000..2aae8738a
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx
@@ -0,0 +1,106 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+import {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useInstanceOtpDevices, useOtpDeviceAPI } from "../../../../hooks/otp.js";
+import { Notification } from "../../../../utils/types.js";
+import { ListPage } from "./ListPage.js";
+
+interface Props {
+ onUnauthorized: () => VNode;
+ onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ onNotFound: () => VNode;
+ onCreate: () => void;
+ onSelect: (id: string) => void;
+}
+
+export default function ListOtpDevices({
+ onUnauthorized,
+ onLoadError,
+ onCreate,
+ onSelect,
+ onNotFound,
+}: Props): VNode {
+ const [position, setPosition] = useState<string | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { deleteOtpDevice } = useOtpDeviceAPI();
+ const result = useInstanceOtpDevices({ position }, (id) => setPosition(id));
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+
+ <ListPage
+ devices={result.data.otp_devices}
+ onLoadMoreBefore={
+ result.isReachingStart ? result.loadMorePrev : undefined
+ }
+ onLoadMoreAfter={result.isReachingEnd ? result.loadMore : undefined}
+ onCreate={onCreate}
+ onSelect={(e) => {
+ onSelect(e.otp_device_id);
+ }}
+ onDelete={(e: MerchantBackend.OTP.OtpDeviceEntry) =>
+ deleteOtpDevice(e.otp_device_id)
+ .then(() =>
+ setNotif({
+ message: i18n.str`validator delete successfully`,
+ type: "SUCCESS",
+ }),
+ )
+ .catch((error) =>
+ setNotif({
+ message: i18n.str`could not delete the validator`,
+ type: "ERROR",
+ description: error.message,
+ }),
+ )
+ }
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/Update.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/Update.stories.tsx
new file mode 100644
index 000000000..d6b1d65e0
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/Update.stories.tsx
@@ -0,0 +1,32 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { UpdatePage as TestedComponent } from "./UpdatePage.js";
+
+export default {
+ title: "Pages/OtpDevices/Update",
+ component: TestedComponent,
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/UpdatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/UpdatePage.tsx
new file mode 100644
index 000000000..85bb272aa
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/UpdatePage.tsx
@@ -0,0 +1,186 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { randomRfc3548Base32Key } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { InputSelector } from "../../../../components/form/InputSelector.js";
+import { InputWithAddon } from "../../../../components/form/InputWithAddon.js";
+import { MerchantBackend, WithId } from "../../../../declaration.js";
+
+type Entity = MerchantBackend.OTP.OtpDevicePatchDetails & WithId;
+
+interface Props {
+ onUpdate: (d: Entity) => Promise<void>;
+ onBack?: () => void;
+ device: Entity;
+}
+const algorithms = [0, 1, 2];
+const algorithmsNames = ["off", "30s 8d TOTP-SHA1", "30s 8d eTOTP-SHA1"];
+export function UpdatePage({ device, onUpdate, onBack }: Props): VNode {
+ const { i18n } = useTranslationContext();
+
+ const [state, setState] = useState<Partial<Entity>>(device);
+ const [showKey, setShowKey] = useState(false);
+
+ const errors: FormErrors<Entity> = {};
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const submitForm = () => {
+ if (hasErrors) return Promise.reject();
+ return onUpdate(state as any);
+ };
+
+ return (
+ <div>
+ <section class="section">
+ <section class="hero is-hero-bar">
+ <div class="hero-body">
+ <div class="level">
+ <div class="level-left">
+ <div class="level-item">
+ <span class="is-size-4">
+ Device: <b>{device.id}</b>
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+ <hr />
+
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column is-four-fifths">
+ <FormProvider
+ object={state}
+ valueHandler={setState}
+ errors={errors}
+ >
+ <Input<Entity>
+ name="otp_device_description"
+ label={i18n.str`Description`}
+ tooltip={i18n.str`Useful to identify the device physically`}
+ />
+ <InputSelector<Entity>
+ name="otp_algorithm"
+ label={i18n.str`Verification algorithm`}
+ tooltip={i18n.str`Algorithm to use to verify transaction in offline mode`}
+ values={algorithms}
+ toStr={(v) => algorithmsNames[v]}
+ fromStr={(v) => Number(v)}
+ />
+ {state.otp_algorithm && state.otp_algorithm > 0 ? (
+ <Fragment>
+ <InputWithAddon<Entity>
+ name="otp_key"
+ label={i18n.str`Device key`}
+ readonly={state.otp_key === undefined}
+ inputType={showKey ? "text" : "password"}
+ help={
+ state.otp_key === undefined
+ ? "Not modified"
+ : "Be sure to be very hard to guess or use the random generator"
+ }
+ tooltip={i18n.str`Your device need to have exactly the same value`}
+ fromStr={(v) => v.toUpperCase()}
+ addonAfterAction={() => {
+ setShowKey(!showKey);
+ }}
+ addonAfter={
+ <span
+ class="icon"
+ onClick={() => {
+ setShowKey(!showKey);
+ }}
+ >
+ {showKey ? (
+ <i class="mdi mdi-eye" />
+ ) : (
+ <i class="mdi mdi-eye-off" />
+ )}
+ </span>
+ }
+ side={
+ state.otp_key === undefined ? (
+ <button
+ onClick={(e) => {
+ setState((s) => ({ ...s, otp_key: "" }));
+ }}
+ class="button"
+ >
+ change key
+ </button>
+ ) : (
+ <button
+ data-tooltip={i18n.str`generate random secret key`}
+ class="button is-info mr-3"
+ onClick={(e) => {
+ setState((s) => ({
+ ...s,
+ otp_key: randomRfc3548Base32Key(),
+ }));
+ }}
+ >
+ <i18n.Translate>random</i18n.Translate>
+ </button>
+ )
+ }
+ />
+ </Fragment>
+ ) : undefined}{" "}
+ </FormProvider>
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ disabled={hasErrors}
+ data-tooltip={
+ hasErrors
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ onClick={submitForm}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ </div>
+ </section>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/index.tsx
new file mode 100644
index 000000000..52f6c6c29
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/index.tsx
@@ -0,0 +1,102 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend, WithId } from "../../../../declaration.js";
+import { Notification } from "../../../../utils/types.js";
+import { UpdatePage } from "./UpdatePage.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+import { useOtpDeviceAPI, useOtpDeviceDetails } from "../../../../hooks/otp.js";
+
+export type Entity = MerchantBackend.OTP.OtpDevicePatchDetails & WithId;
+
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+ onUnauthorized: () => VNode;
+ onNotFound: () => VNode;
+ onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ vid: string;
+}
+export default function UpdateValidator({
+ vid,
+ onConfirm,
+ onBack,
+ onUnauthorized,
+ onNotFound,
+ onLoadError,
+}: Props): VNode {
+ const { updateOtpDevice } = useOtpDeviceAPI();
+ const result = useOtpDeviceDetails(vid);
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+
+ const { i18n } = useTranslationContext();
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+ <UpdatePage
+ device={{
+ id: vid,
+ otp_algorithm: result.data.otp_algorithm,
+ otp_device_description: result.data.device_description,
+ otp_key: undefined,
+ otp_ctr: result.data.otp_ctr
+ }}
+ onBack={onBack}
+ onUpdate={(data) => {
+ return updateOtpDevice(vid, data)
+ .then(onConfirm)
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not update template`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/products/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/products/create/Create.stories.tsx
new file mode 100644
index 000000000..2fc0819bb
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/products/create/Create.stories.tsx
@@ -0,0 +1,43 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { CreatePage as TestedComponent } from "./CreatePage.js";
+
+export default {
+ title: "Pages/Product/Create",
+ component: TestedComponent,
+ argTypes: {
+ onCreate: { action: "onCreate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => <Component {...args} />;
+ r.args = props;
+ return r;
+}
+
+export const Example = createExample(TestedComponent, {});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/products/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/products/create/CreatePage.tsx
new file mode 100644
index 000000000..becaf8f3a
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/products/create/CreatePage.tsx
@@ -0,0 +1,80 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
+import { ProductForm } from "../../../../components/product/ProductForm.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useListener } from "../../../../hooks/listener.js";
+
+type Entity = MerchantBackend.Products.ProductAddDetail & {
+ product_id: string;
+};
+
+interface Props {
+ onCreate: (d: Entity) => Promise<void>;
+ onBack?: () => void;
+}
+
+export function CreatePage({ onCreate, onBack }: Props): VNode {
+ const [submitForm, addFormSubmitter] = useListener<Entity | undefined>(
+ (result) => {
+ if (result) return onCreate(result);
+ return Promise.reject();
+ },
+ );
+
+ const { i18n } = useTranslationContext();
+
+ return (
+ <div>
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <ProductForm onSubscribe={addFormSubmitter} />
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ onClick={submitForm}
+ data-tooltip={
+ !submitForm
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ disabled={!submitForm}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/products/create/CreatedSuccessfully.tsx b/packages/auditor-backoffice-ui/src/paths/instance/products/create/CreatedSuccessfully.tsx
new file mode 100644
index 000000000..6b02430cc
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/products/create/CreatedSuccessfully.tsx
@@ -0,0 +1,72 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { h, VNode } from "preact";
+import { CreatedSuccessfully as Template } from "../../../../components/notifications/CreatedSuccessfully.js";
+import { Entity } from "./index.js";
+import emptyImage from "../../assets/empty.png";
+
+interface Props {
+ entity: Entity;
+ onConfirm: () => void;
+ onCreateAnother?: () => void;
+}
+
+export function CreatedSuccessfully({
+ entity,
+ onConfirm,
+ onCreateAnother,
+}: Props): VNode {
+ return (
+ <Template onConfirm={onConfirm} onCreateAnother={onCreateAnother}>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">Image</label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <img src={entity.image} style={{ width: 200, height: 200 }} />
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">Description</label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <textarea class="input" readonly value={entity.description} />
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">Price</label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input class="input" readonly value={entity.price} />
+ </p>
+ </div>
+ </div>
+ </div>
+ </Template>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/products/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/products/create/index.tsx
new file mode 100644
index 000000000..0c30ff14c
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/products/create/index.tsx
@@ -0,0 +1,47 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { 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
new file mode 100644
index 000000000..c2c4d548c
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/products/list/List.stories.tsx
@@ -0,0 +1,61 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { CardTable as TestedComponent } from "./Table.js";
+
+export default {
+ title: "Pages/Product/List",
+ component: TestedComponent,
+ argTypes: {
+ onCreate: { action: "onCreate" },
+ onSelect: { action: "onSelect" },
+ onDelete: { action: "onDelete" },
+ onUpdate: { action: "onUpdate" },
+ },
+};
+
+function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => <Component {...args} />;
+ r.args = props;
+ return r;
+}
+
+export const Example = createExample(TestedComponent, {
+ instances: [
+ {
+ id: "orderid",
+ description: "description1",
+ description_i18n: {} as any,
+ image: "",
+ price: "TESTKUDOS:10",
+ taxes: [],
+ total_lost: 10,
+ total_sold: 5,
+ total_stock: 15,
+ unit: "bar",
+ address: {},
+ },
+ ],
+});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/products/list/Table.tsx b/packages/auditor-backoffice-ui/src/paths/instance/products/list/Table.tsx
new file mode 100644
index 000000000..275f855cb
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/products/list/Table.tsx
@@ -0,0 +1,496 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { Amounts } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { format } from "date-fns";
+import { ComponentChildren, Fragment, h, VNode } from "preact";
+import { StateUpdater, useState } from "preact/hooks";
+import emptyImage from "../../../../assets/empty.png";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { InputCurrency } from "../../../../components/form/InputCurrency.js";
+import { InputNumber } from "../../../../components/form/InputNumber.js";
+import { MerchantBackend, WithId } from "../../../../declaration.js";
+import { dateFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
+
+type Entity = MerchantBackend.Products.ProductDetail & WithId;
+
+interface Props {
+ instances: Entity[];
+ onDelete: (id: Entity) => void;
+ onSelect: (product: Entity) => void;
+ onUpdate: (
+ id: string,
+ data: MerchantBackend.Products.ProductPatchDetail,
+ ) => Promise<void>;
+ onCreate: () => void;
+ selected?: boolean;
+}
+
+export function CardTable({
+ instances,
+ onCreate,
+ onSelect,
+ onUpdate,
+ onDelete,
+}: Props): VNode {
+ const [rowSelection, rowSelectionHandler] = useState<string | undefined>(
+ undefined,
+ );
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="card has-table">
+ <header class="card-header">
+ <p class="card-header-title">
+ <span class="icon">
+ <i class="mdi mdi-shopping" />
+ </span>
+ <i18n.Translate>Inventory</i18n.Translate>
+ </p>
+ <div class="card-header-icon" aria-label="more options">
+ <span
+ class="has-tooltip-left"
+ data-tooltip={i18n.str`add product to inventory`}
+ >
+ <button class="button is-info" type="button" onClick={onCreate}>
+ <span class="icon is-small">
+ <i class="mdi mdi-plus mdi-36px" />
+ </span>
+ </button>
+ </span>
+ </div>
+ </header>
+ <div class="card-content">
+ <div class="b-table has-pagination">
+ <div class="table-wrapper has-mobile-cards">
+ {instances.length > 0 ? (
+ <Table
+ instances={instances}
+ onSelect={onSelect}
+ onDelete={onDelete}
+ onUpdate={onUpdate}
+ rowSelection={rowSelection}
+ rowSelectionHandler={rowSelectionHandler}
+ />
+ ) : (
+ <EmptyTable />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+}
+interface TableProps {
+ rowSelection: string | undefined;
+ instances: Entity[];
+ onSelect: (id: Entity) => void;
+ onUpdate: (
+ id: string,
+ data: MerchantBackend.Products.ProductPatchDetail,
+ ) => Promise<void>;
+ onDelete: (id: Entity) => void;
+ rowSelectionHandler: StateUpdater<string | undefined>;
+}
+
+function Table({
+ rowSelection,
+ rowSelectionHandler,
+ instances,
+ onSelect,
+ onUpdate,
+ onDelete,
+}: TableProps): VNode {
+ const { i18n } = useTranslationContext();
+ const [settings] = useSettings();
+ return (
+ <div class="table-container">
+ <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>Image</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Description</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Price per unit</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Taxes</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Sales</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Stock</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Sold</i18n.Translate>
+ </th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ {instances.map((i) => {
+ const restStockInfo = !i.next_restock
+ ? ""
+ : i.next_restock.t_s === "never"
+ ? "never"
+ : `restock at ${format(
+ new Date(i.next_restock.t_s * 1000),
+ dateFormatForSettings(settings),
+ )}`;
+ let stockInfo: ComponentChildren = "";
+ if (i.total_stock < 0) {
+ stockInfo = "infinite";
+ } else {
+ const totalStock = i.total_stock - i.total_lost - i.total_sold;
+ stockInfo = (
+ <label title={restStockInfo}>
+ {totalStock} {i.unit}
+ </label>
+ );
+ }
+
+ const isFree = Amounts.isZero(Amounts.parseOrThrow(i.price));
+
+ return (
+ <Fragment key={i.id}>
+ <tr key="info">
+ <td
+ onClick={() =>
+ rowSelection !== i.id && rowSelectionHandler(i.id)
+ }
+ style={{ cursor: "pointer" }}
+ >
+ <img
+ src={i.image ? i.image : emptyImage}
+ style={{
+ border: "solid black 1px",
+ maxHeight: "2em",
+ width: "auto",
+ height: "auto",
+ }}
+ />
+ </td>
+ <td
+ class="has-tooltip-right"
+ data-tooltip={i.description}
+ onClick={() =>
+ rowSelection !== i.id && rowSelectionHandler(i.id)
+ }
+ style={{ cursor: "pointer" }}
+ >
+ {i.description.length > 30 ? i.description.substring(0, 30) + "..." : i.description}
+ </td>
+ <td
+ onClick={() =>
+ rowSelection !== i.id && rowSelectionHandler(i.id)
+ }
+ style={{ cursor: "pointer" }}
+ >
+ {isFree ? i18n.str`free` : `${i.price} / ${i.unit}`}
+ </td>
+ <td
+ onClick={() =>
+ rowSelection !== i.id && rowSelectionHandler(i.id)
+ }
+ style={{ cursor: "pointer" }}
+ >
+ {sum(i.taxes)}
+ </td>
+ <td
+ onClick={() =>
+ rowSelection !== i.id && rowSelectionHandler(i.id)
+ }
+ style={{ cursor: "pointer" }}
+ >
+ {difference(i.price, sum(i.taxes))}
+ </td>
+ <td
+ onClick={() =>
+ rowSelection !== i.id && rowSelectionHandler(i.id)
+ }
+ style={{ cursor: "pointer" }}
+ >
+ {stockInfo}
+ </td>
+ <td
+ onClick={() =>
+ rowSelection !== i.id && rowSelectionHandler(i.id)
+ }
+ style={{ cursor: "pointer" }}
+ >
+ <span style={{"whiteSpace":"nowrap"}}>
+
+ {i.total_sold} {i.unit}
+ </span>
+ </td>
+ <td class="is-actions-cell right-sticky">
+ <div class="buttons is-right">
+ <span
+ class="has-tooltip-bottom"
+ data-tooltip={i18n.str`go to product update page`}
+ >
+ <button
+ class="button is-small is-success "
+ type="button"
+ onClick={(): void => onSelect(i)}
+ >
+ <i18n.Translate>Update</i18n.Translate>
+ </button>
+ </span>
+ <span
+ class="has-tooltip-left"
+ data-tooltip={i18n.str`remove this product from the database`}
+ >
+ <button
+ class="button is-small is-danger"
+ type="button"
+ onClick={(): void => onDelete(i)}
+ >
+ <i18n.Translate>Delete</i18n.Translate>
+ </button>
+ </span>
+ </div>
+ </td>
+ </tr>
+ {rowSelection === i.id && (
+ <tr key="form">
+ <td colSpan={10}>
+ <FastProductUpdateForm
+ product={i}
+ onUpdate={(prod) =>
+ onUpdate(i.id, prod).then((r) =>
+ rowSelectionHandler(undefined),
+ )
+ }
+ onCancel={() => rowSelectionHandler(undefined)}
+ />
+ </td>
+ </tr>
+ )}
+ </Fragment>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>
+ );
+}
+
+interface FastProductUpdateFormProps {
+ product: Entity;
+ onUpdate: (
+ data: MerchantBackend.Products.ProductPatchDetail,
+ ) => Promise<void>;
+ onCancel: () => void;
+}
+interface FastProductUpdate {
+ incoming: number;
+ lost: number;
+ price: string;
+}
+interface UpdatePrice {
+ price: string;
+}
+
+function FastProductWithInfiniteStockUpdateForm({
+ product,
+ onUpdate,
+ onCancel,
+}: FastProductUpdateFormProps) {
+ const [value, valueHandler] = useState<UpdatePrice>({ price: product.price });
+ const { i18n } = useTranslationContext();
+
+ return (
+ <Fragment>
+ <FormProvider<FastProductUpdate>
+ name="added"
+ object={value}
+ valueHandler={valueHandler as any}
+ >
+ <InputCurrency<FastProductUpdate>
+ name="price"
+ label={i18n.str`Price`}
+ tooltip={i18n.str`update the product with new price`}
+ />
+ </FormProvider>
+
+ <div class="buttons is-expanded">
+
+ <div class="buttons mt-5">
+
+ <button class="button mt-5" onClick={onCancel}>
+ <i18n.Translate>Clone</i18n.Translate>
+ </button>
+ </div>
+ <div class="buttons is-right mt-5">
+ <button class="button" onClick={onCancel}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ <span
+ class="has-tooltip-left"
+ data-tooltip={i18n.str`update product with new price`}
+ >
+ <button
+ class="button is-info"
+ onClick={() =>
+ onUpdate({
+ ...product,
+ price: value.price,
+ })
+ }
+ >
+ <i18n.Translate>Confirm update</i18n.Translate>
+ </button>
+ </span>
+ </div>
+ </div>
+ </Fragment>
+ );
+}
+
+function FastProductWithManagedStockUpdateForm({
+ product,
+ onUpdate,
+ onCancel,
+}: FastProductUpdateFormProps) {
+ const [value, valueHandler] = useState<FastProductUpdate>({
+ incoming: 0,
+ lost: 0,
+ price: product.price,
+ });
+
+ const currentStock =
+ product.total_stock - product.total_sold - product.total_lost;
+
+ const errors: FormErrors<FastProductUpdate> = {
+ lost:
+ currentStock + value.incoming < value.lost
+ ? `lost cannot be greater that current + incoming (max ${currentStock + value.incoming
+ })`
+ : undefined,
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+ const { i18n } = useTranslationContext();
+
+ return (
+ <Fragment>
+ <FormProvider<FastProductUpdate>
+ name="added"
+ errors={errors}
+ object={value}
+ valueHandler={valueHandler as any}
+ >
+ <InputNumber<FastProductUpdate>
+ name="incoming"
+ label={i18n.str`Incoming`}
+ tooltip={i18n.str`add more elements to the inventory`}
+ />
+ <InputNumber<FastProductUpdate>
+ name="lost"
+ label={i18n.str`Lost`}
+ tooltip={i18n.str`report elements lost in the inventory`}
+ />
+ <InputCurrency<FastProductUpdate>
+ name="price"
+ label={i18n.str`Price`}
+ tooltip={i18n.str`new price for the product`}
+ />
+ </FormProvider>
+
+ <div class="buttons is-right mt-5">
+ <button class="button" onClick={onCancel}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ <span
+ class="has-tooltip-left"
+ data-tooltip={
+ hasErrors
+ ? i18n.str`the are value with errors`
+ : i18n.str`update product with new stock and price`
+ }
+ >
+ <button
+ class="button is-info"
+ disabled={hasErrors}
+ onClick={() =>
+ onUpdate({
+ ...product,
+ total_stock: product.total_stock + value.incoming,
+ total_lost: product.total_lost + value.lost,
+ price: value.price,
+ })
+ }
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </button>
+ </span>
+ </div>
+ </Fragment>
+ );
+}
+
+function FastProductUpdateForm(props: FastProductUpdateFormProps) {
+ return props.product.total_stock === -1 ? (
+ <FastProductWithInfiniteStockUpdateForm {...props} />
+ ) : (
+ <FastProductWithManagedStockUpdateForm {...props} />
+ );
+}
+
+function EmptyTable(): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="content has-text-grey has-text-centered">
+ <p>
+ <span class="icon is-large">
+ <i class="mdi mdi-emoticon-sad mdi-48px" />
+ </span>
+ </p>
+ <p>
+ <i18n.Translate>
+ There is no products yet, add more pressing the + sign
+ </i18n.Translate>
+ </p>
+ </div>
+ );
+}
+
+function difference(price: string, tax: number) {
+ if (!tax) return price;
+ const ps = price.split(":");
+ const p = parseInt(ps[1], 10);
+ ps[1] = `${p - tax}`;
+ return ps.join(":");
+}
+function sum(taxes: MerchantBackend.Tax[]) {
+ return taxes.reduce((p, c) => p + parseInt(c.tax.split(":")[1], 10), 0);
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/products/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/products/list/index.tsx
new file mode 100644
index 000000000..34b21daa2
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/products/list/index.tsx
@@ -0,0 +1,150 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend, WithId } from "../../../../declaration.js";
+import {
+ useInstanceProducts,
+ useProductAPI,
+} from "../../../../hooks/product.js";
+import { Notification } from "../../../../utils/types.js";
+import { CardTable } from "./Table.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+import { ConfirmModal, DeleteModal } from "../../../../components/modal/index.js";
+import { JumpToElementById } from "../../../../components/form/JumpToElementById.js";
+
+interface Props {
+ onUnauthorized: () => VNode;
+ onNotFound: () => VNode;
+ onCreate: () => void;
+ onSelect: (id: string) => void;
+ onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+}
+export default function ProductList({
+ onUnauthorized,
+ onLoadError,
+ onCreate,
+ onSelect,
+ onNotFound,
+}: Props): VNode {
+ const result = useInstanceProducts();
+ const { deleteProduct, updateProduct, getProduct } = useProductAPI();
+ const [deleting, setDeleting] =
+ useState<MerchantBackend.Products.ProductDetail & WithId | null>(null);
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+
+ const { i18n } = useTranslationContext();
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <section class="section is-main-section">
+ <NotificationCard notification={notif} />
+
+ <JumpToElementById
+ testIfExist={getProduct}
+ onSelect={onSelect}
+ description={i18n.str`jump to product with the given product ID`}
+ 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
new file mode 100644
index 000000000..a85b13b8b
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/products/update/Update.stories.tsx
@@ -0,0 +1,73 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { UpdatePage as TestedComponent } from "./UpdatePage.js";
+
+export default {
+ title: "Pages/Product/Update",
+ component: TestedComponent,
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => <Component {...args} />;
+ r.args = props;
+ return r;
+}
+
+export const WithManagedStock = createExample(TestedComponent, {
+ product: {
+ product_id: "20102-ASDAS-QWE",
+ description: "description1",
+ description_i18n: {} as any,
+ image: "",
+ price: "TESTKUDOS:10",
+ taxes: [],
+ total_lost: 10,
+ total_sold: 5,
+ total_stock: 15,
+ unit: "bar",
+ address: {},
+ },
+});
+
+export const WithInfiniteStock = createExample(TestedComponent, {
+ product: {
+ product_id: "20102-ASDAS-QWE",
+ description: "description1",
+ description_i18n: {} as any,
+ image: "",
+ price: "TESTKUDOS:10",
+ taxes: [],
+ total_lost: 10,
+ total_sold: 5,
+ total_stock: -1,
+ unit: "bar",
+ address: {},
+ },
+});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/products/update/UpdatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/products/update/UpdatePage.tsx
new file mode 100644
index 000000000..97715171e
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/products/update/UpdatePage.tsx
@@ -0,0 +1,99 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
+import { ProductForm } from "../../../../components/product/ProductForm.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useListener } from "../../../../hooks/listener.js";
+
+type Entity = MerchantBackend.Products.ProductDetail & { product_id: string };
+
+interface Props {
+ onUpdate: (d: Entity) => Promise<void>;
+ onBack?: () => void;
+ product: Entity;
+}
+
+export function UpdatePage({ product, onUpdate, onBack }: Props): VNode {
+ const [submitForm, addFormSubmitter] = useListener<Entity | undefined>(
+ (result) => {
+ if (result) return onUpdate(result);
+ return Promise.resolve();
+ },
+ );
+
+ const { i18n } = useTranslationContext();
+
+ return (
+ <div>
+ <section class="section">
+ <section class="hero is-hero-bar">
+ <div class="hero-body">
+ <div class="level">
+ <div class="level-left">
+ <div class="level-item">
+ <span class="is-size-4">
+ <i18n.Translate>Product id:</i18n.Translate>
+ <b>{product.product_id}</b>
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+ <hr />
+
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <ProductForm
+ initial={product}
+ onSubscribe={addFormSubmitter}
+ alreadyExist
+ />
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ onClick={submitForm}
+ data-tooltip={
+ !submitForm
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ disabled={!submitForm}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/products/update/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/products/update/index.tsx
new file mode 100644
index 000000000..8e0f7647f
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/products/update/index.tsx
@@ -0,0 +1,95 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useProductAPI, useProductDetails } from "../../../../hooks/product.js";
+import { Notification } from "../../../../utils/types.js";
+import { UpdatePage } from "./UpdatePage.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+
+export type Entity = MerchantBackend.Products.ProductAddDetail;
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+ onUnauthorized: () => VNode;
+ onNotFound: () => VNode;
+ onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ pid: string;
+}
+export default function UpdateProduct({
+ pid,
+ onConfirm,
+ onBack,
+ onUnauthorized,
+ onNotFound,
+ onLoadError,
+}: Props): VNode {
+ const { updateProduct } = useProductAPI();
+ const result = useProductDetails(pid);
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+
+ const { i18n } = useTranslationContext();
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+ <UpdatePage
+ product={{ ...result.data, product_id: pid }}
+ onBack={onBack}
+ onUpdate={(data) => {
+ return updateProduct(pid, data)
+ .then(onConfirm)
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not create product`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/Create.stories.tsx
new file mode 100644
index 000000000..5542c028a
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/Create.stories.tsx
@@ -0,0 +1,43 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { CreatePage as TestedComponent } from "./CreatePage.js";
+
+export default {
+ title: "Pages/Reserve/Create",
+ component: TestedComponent,
+ argTypes: {
+ onCreate: { action: "onCreate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => <Component {...args} />;
+ r.args = props;
+ return r;
+}
+
+export const Example = createExample(TestedComponent, {});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatePage.tsx
new file mode 100644
index 000000000..e46941b6d
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatePage.tsx
@@ -0,0 +1,277 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { HttpError, RequestError, useApiContext, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { StateUpdater, useEffect, useState } from "preact/hooks";
+import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { InputCurrency } from "../../../../components/form/InputCurrency.js";
+import { InputSelector } from "../../../../components/form/InputSelector.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import {
+ PAYTO_WIRE_METHOD_LOOKUP,
+ URL_REGEX,
+} from "../../../../utils/constants.js";
+import { useBackendBaseRequest } from "../../../../hooks/backend.js";
+import { parsePaytoUri } from "@gnu-taler/taler-util";
+
+type Entity = MerchantBackend.Rewards.ReserveCreateRequest;
+
+interface Props {
+ onCreate: (d: Entity) => Promise<void>;
+ onBack?: () => void;
+}
+
+enum Steps {
+ EXCHANGE,
+ WIRE_METHOD,
+}
+
+interface ViewProps {
+ step: Steps;
+ setCurrentStep: (s: Steps) => void;
+ reserve: Partial<Entity>;
+ onBack?: () => void;
+ submitForm: () => Promise<void>;
+ setReserve: StateUpdater<Partial<Entity>>;
+}
+function ViewStep({
+ step,
+ setCurrentStep,
+ reserve,
+ onBack,
+ submitForm,
+ setReserve,
+}: ViewProps): VNode {
+ const { i18n } = useTranslationContext();
+ const {request} = useApiContext()
+ const [wireMethods, setWireMethods] = useState<Array<string>>([]);
+ const [exchangeQueryError, setExchangeQueryError] = useState<
+ string | undefined
+ >(undefined);
+
+ useEffect(() => {
+ setExchangeQueryError(undefined);
+ }, [reserve.exchange_url]);
+
+ switch (step) {
+ case Steps.EXCHANGE: {
+ const errors: FormErrors<Entity> = {
+ initial_balance: !reserve.initial_balance
+ ? "cannot be empty"
+ : !(parseInt(reserve.initial_balance.split(":")[1], 10) > 0)
+ ? i18n.str`it should be greater than 0`
+ : undefined,
+ exchange_url: !reserve.exchange_url
+ ? i18n.str`cannot be empty`
+ : !URL_REGEX.test(reserve.exchange_url)
+ ? i18n.str`must be a valid URL`
+ : !exchangeQueryError
+ ? undefined
+ : exchangeQueryError,
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ return (
+ <Fragment>
+ <FormProvider<Entity>
+ object={reserve}
+ errors={errors}
+ valueHandler={setReserve}
+ >
+ <InputCurrency<Entity>
+ name="initial_balance"
+ label={i18n.str`Initial balance`}
+ tooltip={i18n.str`balance prior to deposit`}
+ />
+ <Input<Entity>
+ name="exchange_url"
+ label={i18n.str`Exchange URL`}
+ tooltip={i18n.str`URL of exchange`}
+ />
+ </FormProvider>
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ class="has-tooltip-left"
+ onClick={() => {
+ if (!reserve.exchange_url) {
+ return Promise.resolve();
+ }
+
+ return request<any>(reserve.exchange_url, "keys")
+ .then((r) => {
+ console.log(r)
+ if (r.loading) return;
+ if (r.ok) {
+ const wireMethods = r.data.accounts.map((a: any) => {
+ const p = parsePaytoUri(a.payto_uri);
+ const r = p?.targetType
+ return r
+ }).filter((x:any) => !!x);
+ setWireMethods(Array.from(new Set(wireMethods)));
+ }
+ setCurrentStep(Steps.WIRE_METHOD);
+ return;
+ })
+ .catch((r: RequestError<{}>) => {
+ console.log(r.cause)
+ setExchangeQueryError(r.cause.message);
+ });
+ }}
+ data-tooltip={
+ hasErrors
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ disabled={hasErrors}
+ >
+ <i18n.Translate>Next</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </Fragment>
+ );
+ }
+
+ case Steps.WIRE_METHOD: {
+ const errors: FormErrors<Entity> = {
+ wire_method: !reserve.wire_method
+ ? i18n.str`cannot be empty`
+ : undefined,
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+ return (
+ <Fragment>
+ <FormProvider<Entity>
+ object={reserve}
+ errors={errors}
+ valueHandler={setReserve}
+ >
+ <InputCurrency<Entity>
+ name="initial_balance"
+ label={i18n.str`Initial balance`}
+ tooltip={i18n.str`balance prior to deposit`}
+ readonly
+ />
+ <Input<Entity>
+ name="exchange_url"
+ label={i18n.str`Exchange URL`}
+ tooltip={i18n.str`URL of exchange`}
+ readonly
+ />
+ <InputSelector<Entity>
+ name="wire_method"
+ label={i18n.str`Wire method`}
+ tooltip={i18n.str`method to use for wire transfer`}
+ values={wireMethods}
+ placeholder={i18n.str`Select one wire method`}
+ />
+ </FormProvider>
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button
+ class="button"
+ onClick={() => setCurrentStep(Steps.EXCHANGE)}
+ >
+ <i18n.Translate>Back</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ onClick={submitForm}
+ data-tooltip={
+ hasErrors
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ disabled={hasErrors}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </Fragment>
+ );
+ }
+ }
+}
+
+export function CreatePage({ onCreate, onBack }: Props): VNode {
+ const [reserve, setReserve] = useState<Partial<Entity>>({});
+
+ const submitForm = () => {
+ return onCreate(reserve as Entity);
+ };
+
+ const [currentStep, setCurrentStep] = useState(Steps.EXCHANGE);
+
+ return (
+ <div>
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <div class="tabs is-toggle is-fullwidth is-small">
+ <ul>
+ <li class={currentStep === Steps.EXCHANGE ? "is-active" : ""}>
+ <a style={{ cursor: "initial" }}>
+ <span>Step 1: Specify exchange</span>
+ </a>
+ </li>
+ <li
+ class={currentStep === Steps.WIRE_METHOD ? "is-active" : ""}
+ >
+ <a style={{ cursor: "initial" }}>
+ <span>Step 2: Select wire method</span>
+ </a>
+ </li>
+ </ul>
+ </div>
+
+ <ViewStep
+ step={currentStep}
+ reserve={reserve}
+ setCurrentStep={setCurrentStep}
+ setReserve={setReserve}
+ submitForm={submitForm}
+ onBack={onBack}
+ />
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.stories.tsx
new file mode 100644
index 000000000..445ca3ef0
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.stories.tsx
@@ -0,0 +1,120 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { CreatedSuccessfully as TestedComponent } from "./CreatedSuccessfully.js";
+import * as tests from "@gnu-taler/web-util/testing";
+
+export default {
+ title: "Pages/Reserve/CreatedSuccessfully",
+ component: TestedComponent,
+ argTypes: {
+ onCreate: { action: "onCreate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+export const OneBankAccount = tests.createExample(TestedComponent, {
+ entity: {
+ request: {
+ exchange_url: "http://exchange.taler/",
+ initial_balance: "TESTKUDOS:1",
+ wire_method: "x-taler-bank",
+ },
+ response: {
+ accounts: [
+ {
+ payto_uri: "payto://x-taler-bank/bank.taler:8080/exchange_account",
+ credit_restrictions: [],
+ debit_restrictions: [],
+ master_sig: "asd",
+ conversion_url: "",
+ },
+ ],
+ reserve_pub: "WEQWDASDQWEASDADASDQWEQWEASDAS",
+ },
+ },
+});
+
+export const ThreeBankAccount = tests.createExample(TestedComponent, {
+ entity: {
+ request: {
+ exchange_url: "http://exchange.taler/",
+ initial_balance: "TESTKUDOS:1",
+ wire_method: "x-taler-bank",
+ },
+ response: {
+ accounts: [
+ {
+ payto_uri: "payto://x-taler-bank/bank.taler:8080/exchange_account",
+ credit_restrictions: [],
+ debit_restrictions: [],
+ master_sig: "asd",
+ conversion_url: "",
+ },
+ {
+ payto_uri: "payto://x-taler-bank/bank1.taler:8080/asd",
+ credit_restrictions: [],
+ debit_restrictions: [],
+ master_sig: "asd",
+ conversion_url: "",
+ },
+ {
+ payto_uri: "payto://x-taler-bank/bank2.taler:8080/qwe",
+ credit_restrictions: [],
+ debit_restrictions: [],
+ master_sig: "asd",
+ conversion_url: "",
+ },
+ ],
+ reserve_pub: "WEQWDASDQWEASDADASDQWEQWEASDAS",
+ },
+ },
+});
+
+export const NoBankAccount = tests.createExample(TestedComponent, {
+ entity: {
+ request: {
+ exchange_url: "http://exchange.taler/",
+ initial_balance: "TESTKUDOS:1",
+ wire_method: "x-taler-bank",
+ },
+ response: {
+ accounts: [
+ {
+ payto_uri: "payo://x-talr-bank/bank.taler:8080/exchange_account",
+ credit_restrictions: [],
+ debit_restrictions: [],
+ master_sig: "asd",
+ conversion_url: "",
+ },
+ {
+ payto_uri: "payto://x-taler-bank",
+ credit_restrictions: [],
+ debit_restrictions: [],
+ master_sig: "asd",
+ conversion_url: "",
+ },
+ ],
+ reserve_pub: "WEQWDASDQWEASDADASDQWEQWEASDAS",
+ },
+ },
+});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.tsx
new file mode 100644
index 000000000..1d512c843
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.tsx
@@ -0,0 +1,190 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { parsePaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { QR } from "../../../../components/exception/QR.js";
+import { CreatedSuccessfully as Template } from "../../../../components/notifications/CreatedSuccessfully.js";
+import { MerchantBackend, WireAccount } from "../../../../declaration.js";
+
+type Entity = {
+ request: MerchantBackend.Rewards.ReserveCreateRequest;
+ response: MerchantBackend.Rewards.ReserveCreateConfirmation;
+};
+
+interface Props {
+ entity: Entity;
+ onConfirm: () => void;
+ onCreateAnother?: () => void;
+}
+
+function isNotUndefined<X>(x: X | undefined): x is X {
+ return !!x;
+}
+
+export function CreatedSuccessfully({
+ entity,
+ onConfirm,
+ onCreateAnother,
+}: Props): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <Template onConfirm={onConfirm} onCreateAnother={onCreateAnother}>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">Amount</label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input
+ readonly
+ class="input"
+ value={entity.request.initial_balance}
+ />
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">Wire transfer subject</label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input
+ class="input"
+ readonly
+ value={entity.response.reserve_pub}
+ />
+ </p>
+ </div>
+ </div>
+ </div>
+ <ShowAccountsOfReserveAsQRWithLink
+ accounts={entity.response.accounts ?? []}
+ message={entity.response.reserve_pub}
+ amount={entity.request.initial_balance}
+ />
+ </Template>
+ );
+}
+
+export function ShowAccountsOfReserveAsQRWithLink({
+ accounts,
+ message,
+ amount,
+}: {
+ accounts: WireAccount[];
+ message: string;
+ amount: string;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const accountsInfo = !accounts
+ ? []
+ : accounts
+ .map((acc) => {
+ const p = parsePaytoUri(acc.payto_uri);
+ if (p) {
+ p.params["message"] = message;
+ p.params["amount"] = amount;
+ }
+ return p;
+ })
+ .filter(isNotUndefined);
+
+ const links = accountsInfo.map((a) => stringifyPaytoUri(a));
+
+ if (links.length === 0) {
+ return (
+ <Fragment>
+ <p class="is-size-5">
+ The reserve have invalid accounts. List of invalid payto URIs below:
+ </p>
+ <ul>
+ {accounts.map((a, idx) => {
+ return <li key={idx}>{a.payto_uri}</li>;
+ })}
+ </ul>
+ </Fragment>
+ );
+ }
+
+ if (links.length === 1) {
+ return (
+ <Fragment>
+ <p class="is-size-5">
+ <i18n.Translate>
+ To complete the setup of the reserve, you must now initiate a wire
+ transfer using the given wire transfer subject and crediting the
+ specified amount to the indicated account of the exchange.
+ </i18n.Translate>
+ </p>
+ <p style={{ margin: 10 }}>
+ <b>Exchange bank account</b>
+ </p>
+ <QR text={links[0]} />
+ <p class="is-size-5">
+ <i18n.Translate>
+ If your system supports RFC 8905, you can do this by opening this
+ URI:
+ </i18n.Translate>
+ </p>
+ <pre>
+ <a target="_blank" rel="noreferrer" href={links[0]}>
+ {links[0]}
+ </a>
+ </pre>
+ </Fragment>
+ );
+ }
+
+ return (
+ <div>
+ <p class="is-size-5">
+ <i18n.Translate>
+ To complete the setup of the reserve, you must now initiate a wire
+ transfer using the given wire transfer subject and crediting the
+ specified amount to one of the indicated account of the exchange.
+ </i18n.Translate>
+ </p>
+
+ <p style={{ margin: 10 }}>
+ <b>Exchange bank accounts</b>
+ </p>
+ <p class="is-size-5">
+ <i18n.Translate>
+ If your system supports RFC 8905, you can do this by clicking on the
+ URI below the QR code:
+ </i18n.Translate>
+ </p>
+ {links.map((link) => {
+ return (
+ <Fragment>
+ <QR text={link} />
+ <pre>
+ <a target="_blank" rel="noreferrer" href={link}>
+ {link}
+ </a>
+ </pre>
+ </Fragment>
+ );
+ })}
+ </div>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/index.tsx
index c00261d8c..4bbaf1459 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/index.tsx
+++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/index.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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
@@ -19,12 +19,12 @@
* @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 { useTranslator } from "../../../../i18n/index.js";
import { Notification } from "../../../../utils/types.js";
import { CreatedSuccessfully } from "./CreatedSuccessfully.js";
import { CreatePage } from "./CreatePage.js";
@@ -35,13 +35,13 @@ interface Props {
export default function CreateReserve({ onBack, onConfirm }: Props): VNode {
const { createReserve } = useReservesAPI();
const [notif, setNotif] = useState<Notification | undefined>(undefined);
- const i18n = useTranslator();
+ const { i18n } = useTranslationContext();
const [createdOk, setCreatedOk] = useState<
| {
- request: MerchantBackend.Tips.ReserveCreateRequest;
- response: MerchantBackend.Tips.ReserveCreateConfirmation;
- }
+ request: MerchantBackend.Rewards.ReserveCreateRequest;
+ response: MerchantBackend.Rewards.ReserveCreateConfirmation;
+ }
| undefined
>(undefined);
@@ -54,12 +54,12 @@ export default function CreateReserve({ onBack, onConfirm }: Props): VNode {
<NotificationCard notification={notif} />
<CreatePage
onBack={onBack}
- onCreate={(request: MerchantBackend.Tips.ReserveCreateRequest) => {
+ onCreate={(request: MerchantBackend.Rewards.ReserveCreateRequest) => {
return createReserve(request)
.then((r) => setCreatedOk({ request, response: r.data }))
.catch((error) => {
setNotif({
- message: i18n`could not create reserve`,
+ message: i18n.str`could not create reserve`,
type: "ERROR",
description: error.message,
});
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/DetailPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/DetailPage.tsx
index ad1089eb5..d8840eeac 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/DetailPage.tsx
+++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/DetailPage.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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
@@ -19,7 +19,12 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { Amounts } from "@gnu-taler/taler-util";
+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";
@@ -29,13 +34,14 @@ 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 { ContinueModal, SimpleModal } from "../../../../components/modal/index.js";
+import { SimpleModal } from "../../../../components/modal/index.js";
import { MerchantBackend } from "../../../../declaration.js";
-import { useTipDetails } from "../../../../hooks/reserves.js";
-import { Translate, useTranslator } from "../../../../i18n/index.js";
-import { TipInfo } from "./TipInfo.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.Tips.ReserveDetail;
+type Entity = MerchantBackend.Rewards.ReserveDetail;
type CT = MerchantBackend.ContractTerms;
interface Props {
@@ -45,11 +51,10 @@ interface Props {
}
export function DetailPage({ id, selected, onBack }: Props): VNode {
- const i18n = useTranslator();
+ const { i18n } = useTranslationContext();
const didExchangeAckTransfer = Amounts.isNonZero(
- Amounts.parseOrThrow(selected.exchange_initial_amount)
+ Amounts.parseOrThrow(selected.exchange_initial_amount),
);
- const link = `${selected.payto_uri}?message=${id}&amount=${selected.merchant_initial_amount}`;
return (
<div class="columns">
@@ -59,22 +64,22 @@ export function DetailPage({ id, selected, onBack }: Props): VNode {
<FormProvider object={{ ...selected, id }} valueHandler={null}>
<InputDate<Entity>
name="creation_time"
- label={i18n`Created at`}
+ label={i18n.str`Created at`}
readonly
/>
<InputDate<Entity>
name="expiration_time"
- label={i18n`Valid until`}
+ label={i18n.str`Valid until`}
readonly
/>
<InputCurrency<Entity>
name="merchant_initial_amount"
- label={i18n`Created balance`}
+ label={i18n.str`Created balance`}
readonly
/>
<TextField<Entity>
name="exchange_url"
- label={i18n`Exchange URL`}
+ label={i18n.str`Exchange URL`}
readonly
>
<a target="_blank" rel="noreferrer" href={selected.exchange_url}>
@@ -86,27 +91,22 @@ export function DetailPage({ id, selected, onBack }: Props): VNode {
<Fragment>
<InputCurrency<Entity>
name="exchange_initial_amount"
- label={i18n`Exchange balance`}
+ label={i18n.str`Exchange balance`}
readonly
/>
<InputCurrency<Entity>
name="pickup_amount"
- label={i18n`Picked up`}
+ label={i18n.str`Picked up`}
readonly
/>
<InputCurrency<Entity>
name="committed_amount"
- label={i18n`Committed`}
+ label={i18n.str`Committed`}
readonly
/>
</Fragment>
)}
- <Input<Entity>
- name="payto_uri"
- label={i18n`Account address`}
- readonly
- />
- <Input name="id" label={i18n`Subject`} readonly />
+ <Input name="id" label={i18n.str`Subject`} readonly />
</FormProvider>
{didExchangeAckTransfer ? (
@@ -117,14 +117,14 @@ export function DetailPage({ id, selected, onBack }: Props): VNode {
<span class="icon">
<i class="mdi mdi-cash-register" />
</span>
- <Translate>Tips</Translate>
+ <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.tips && selected.tips.length > 0 ? (
- <Table tips={selected.tips} />
+ {selected.rewards && selected.rewards.length > 0 ? (
+ <Table rewards={selected.rewards} />
) : (
<EmptyTable />
)}
@@ -133,34 +133,17 @@ export function DetailPage({ id, selected, onBack }: Props): VNode {
</div>
</div>
</Fragment>
- ) : (
- <Fragment>
- <p class="is-size-5">
- <Translate>
- To complete the setup of the reserve, you must now initiate a
- wire transfer using the given wire transfer subject and
- crediting the specified amount to the indicated account of the
- exchange.
- </Translate>
- </p>
- <p class="is-size-5">
- <Translate>
- If your system supports RFC 8905, you can do this by opening
- this URI:
- </Translate>
- </p>
- <pre>
- <a target="_blank" rel="noreferrer" href={link}>
- {link}
- </a>
- </pre>
- <QR text={link} />
- </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}>
- <Translate>Back</Translate>
+ <i18n.Translate>Back</i18n.Translate>
</button>
</div>
</div>
@@ -171,6 +154,7 @@ export function DetailPage({ id, selected, onBack }: Props): VNode {
}
function EmptyTable(): VNode {
+ const { i18n } = useTranslationContext();
return (
<div class="content has-text-grey has-text-centered">
<p>
@@ -179,39 +163,42 @@ function EmptyTable(): VNode {
</span>
</p>
<p>
- <Translate>No tips has been authorized from this reserve</Translate>
+ <i18n.Translate>
+ No reward has been authorized from this reserve
+ </i18n.Translate>
</p>
</div>
);
}
interface TableProps {
- tips: MerchantBackend.Tips.TipStatusEntry[];
+ rewards: MerchantBackend.Rewards.RewardStatusEntry[];
}
-function Table({ tips }: TableProps): VNode {
+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>
- <Translate>Authorized</Translate>
+ <i18n.Translate>Authorized</i18n.Translate>
</th>
<th>
- <Translate>Picked up</Translate>
+ <i18n.Translate>Picked up</i18n.Translate>
</th>
<th>
- <Translate>Reason</Translate>
+ <i18n.Translate>Reason</i18n.Translate>
</th>
<th>
- <Translate>Expiration</Translate>
+ <i18n.Translate>Expiration</i18n.Translate>
</th>
</tr>
</thead>
<tbody>
- {tips.map((t, i) => {
- return <TipRow id={t.tip_id} key={i} entry={t} />;
+ {rewards.map((t, i) => {
+ return <RewardRow id={t.reward_id} key={i} entry={t} />;
})}
</tbody>
</table>
@@ -219,15 +206,16 @@ function Table({ tips }: TableProps): VNode {
);
}
-function TipRow({
+function RewardRow({
id,
entry,
}: {
id: string;
- entry: MerchantBackend.Tips.TipStatusEntry;
+ entry: MerchantBackend.Rewards.RewardStatusEntry;
}) {
const [selected, setSelected] = useState(false);
- const result = useTipDetails(id);
+ const result = useRewardDetails(id);
+ const [settings] = useSettings();
if (result.loading) {
return (
<tr>
@@ -256,11 +244,11 @@ function TipRow({
<Fragment>
{selected && (
<SimpleModal
- description="tip"
+ description="reward"
active
onCancel={() => setSelected(false)}
>
- <TipInfo id={id} amount={info.total_authorized} entity={info} />
+ <RewardInfo id={id} amount={info.total_authorized} entity={info} />
</SimpleModal>
)}
<tr>
@@ -270,7 +258,7 @@ function TipRow({
<td onClick={onSelect}>
{info.expiration.t_s === "never"
? "never"
- : format(info.expiration.t_s * 1000, "yyyy/MM/dd HH:mm:ss")}
+ : format(info.expiration.t_s * 1000, datetimeFormatForSettings(settings))}
</td>
</tr>
</Fragment>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/Details.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/Details.stories.tsx
index 48a12f02a..41c715f20 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/Details.stories.tsx
+++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/Details.stories.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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
@@ -33,7 +33,7 @@ export default {
function createExample<Props>(
Component: FunctionalComponent<Props>,
- props: Partial<Props>
+ props: Partial<Props>,
) {
const r = (args: any) => <Component {...args} />;
r.args = props;
@@ -54,7 +54,14 @@ export const Funded = createExample(TestedComponent, {
},
merchant_initial_amount: "TESTKUDOS:10",
pickup_amount: "TESTKUDOS:10",
- payto_uri: "payto://x-taler-bank/bank.taler:8080/account",
+ accounts: [
+ {
+ payto_uri: "payto://x-taler-bank/bank.taler:8080/account",
+ credit_restrictions: [],
+ debit_restrictions: [],
+ master_sig: "",
+ },
+ ],
exchange_url: "http://exchange.taler/",
},
});
@@ -73,12 +80,19 @@ export const NotYetFunded = createExample(TestedComponent, {
},
merchant_initial_amount: "TESTKUDOS:10",
pickup_amount: "TESTKUDOS:10",
- payto_uri: "payto://x-taler-bank/bank.taler:8080/account",
+ accounts: [
+ {
+ payto_uri: "payto://x-taler-bank/bank.taler:8080/account",
+ credit_restrictions: [],
+ debit_restrictions: [],
+ master_sig: "",
+ },
+ ],
exchange_url: "http://exchange.taler/",
},
});
-export const FundedWithEmptyTips = createExample(TestedComponent, {
+export const FundedWithEmptyRewards = createExample(TestedComponent, {
id: "THISISTHERESERVEID",
selected: {
active: true,
@@ -92,12 +106,19 @@ export const FundedWithEmptyTips = createExample(TestedComponent, {
},
merchant_initial_amount: "TESTKUDOS:10",
pickup_amount: "TESTKUDOS:10",
- payto_uri: "payto://x-taler-bank/bank.taler:8080/account",
+ accounts: [
+ {
+ payto_uri: "payto://x-taler-bank/bank.taler:8080/account",
+ credit_restrictions: [],
+ debit_restrictions: [],
+ master_sig: "",
+ },
+ ],
exchange_url: "http://exchange.taler/",
- tips: [
+ rewards: [
{
reason: "asdasd",
- tip_id: "123",
+ reward_id: "123",
total_amount: "TESTKUDOS:1",
},
],
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/TipInfo.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/RewardInfo.tsx
index 02bc6f6df..491028695 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/TipInfo.tsx
+++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/RewardInfo.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,8 +17,12 @@ 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.Tips.TipDetails;
+type Entity = MerchantBackend.Rewards.RewardDetails;
interface Props {
id: string;
@@ -26,11 +30,14 @@ interface Props {
amount: string;
}
-export function TipInfo({ id, amount, entity }: Props): VNode {
- const { url } = useBackendContext();
- const tipHost = url.replace(/.*:\/\//, ""); // remove protocol part
- const proto = url.startsWith("http://") ? "taler+http" : "taler";
- const tipURL = `${proto}://tip/${tipHost}/${id}`;
+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">
@@ -52,8 +59,8 @@ export function TipInfo({ id, amount, entity }: Props): VNode {
<div class="field-body is-flex-grow-3">
<div class="field" style={{ overflowWrap: "anywhere" }}>
<p class="control">
- <a target="_blank" rel="noreferrer" href={tipURL}>
- {tipURL}
+ <a target="_blank" rel="noreferrer" href={rewardURL}>
+ {rewardURL}
</a>
</p>
</div>
@@ -74,7 +81,7 @@ export function TipInfo({ id, amount, entity }: Props): VNode {
? "never"
: format(
entity.expiration.t_s * 1000,
- "yyyy/MM/dd HH:mm:ss"
+ datetimeFormatForSettings(settings),
)
}
/>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/index.tsx
index 882c2adac..8e2a74529 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/index.tsx
+++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/index.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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
@@ -19,17 +19,19 @@
* @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 { HttpError } from "../../../../hooks/backend.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) => VNode;
+ onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
onNotFound: () => VNode;
onDelete: () => void;
onBack: () => void;
@@ -44,10 +46,20 @@ export default function DetailReserve({
}: Props): VNode {
const result = useReserveDetails(rid);
- if (result.clientError && result.isUnauthorized) return onUnauthorized();
- if (result.clientError && result.isNotfound) return onNotFound();
if (result.loading) return <Loading />;
- if (!result.ok) return onLoadError(result);
+ 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} />
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/AutorizeRewardModal.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/AutorizeRewardModal.tsx
new file mode 100644
index 000000000..e205ee621
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/AutorizeRewardModal.tsx
@@ -0,0 +1,124 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import * as yup from "yup";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { InputCurrency } from "../../../../components/form/InputCurrency.js";
+import {
+ ConfirmModal,
+ ContinueModal,
+} from "../../../../components/modal/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { AuthorizeRewardSchema } from "../../../../schemas/index.js";
+import { CreatedSuccessfully } from "./CreatedSuccessfully.js";
+
+interface AuthorizeRewardModalProps {
+ onCancel: () => void;
+ onConfirm: (value: MerchantBackend.Rewards.RewardCreateRequest) => void;
+ rewardAuthorized?: {
+ response: MerchantBackend.Rewards.RewardCreateConfirmation;
+ request: MerchantBackend.Rewards.RewardCreateRequest;
+ };
+}
+
+export function AuthorizeRewardModal({
+ onCancel,
+ onConfirm,
+ rewardAuthorized,
+}: AuthorizeRewardModalProps): VNode {
+ // const result = useOrderDetails(id)
+ type State = MerchantBackend.Rewards.RewardCreateRequest;
+ const [form, setValue] = useState<Partial<State>>({});
+ const { i18n } = useTranslationContext();
+
+ // const [errors, setErrors] = useState<FormErrors<State>>({})
+ let errors: FormErrors<State> = {};
+ try {
+ AuthorizeRewardSchema.validateSync(form, { abortEarly: false });
+ } catch (err) {
+ if (err instanceof yup.ValidationError) {
+ const yupErrors = err.inner as any[];
+ errors = yupErrors.reduce(
+ (prev, cur) =>
+ !cur.path ? prev : { ...prev, [cur.path]: cur.message },
+ {},
+ );
+ }
+ }
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const validateAndConfirm = () => {
+ onConfirm(form as State);
+ };
+ if (rewardAuthorized) {
+ return (
+ <ContinueModal description="reward" active onConfirm={onCancel}>
+ <CreatedSuccessfully
+ entity={rewardAuthorized.response}
+ request={rewardAuthorized.request}
+ onConfirm={onCancel}
+ />
+ </ContinueModal>
+ );
+ }
+
+ return (
+ <ConfirmModal
+ description="New reward"
+ active
+ onCancel={onCancel}
+ disabled={hasErrors}
+ onConfirm={validateAndConfirm}
+ >
+ <FormProvider<State>
+ errors={errors}
+ object={form}
+ valueHandler={setValue}
+ >
+ <InputCurrency<State>
+ name="amount"
+ label={i18n.str`Amount`}
+ tooltip={i18n.str`amount of reward`}
+ />
+ <Input<State>
+ name="justification"
+ label={i18n.str`Justification`}
+ inputType="multiline"
+ tooltip={i18n.str`reason for the reward`}
+ />
+ <Input<State>
+ name="next_url"
+ label={i18n.str`URL after reward`}
+ tooltip={i18n.str`URL to visit after reward payment`}
+ />
+ </FormProvider>
+ </ConfirmModal>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/CreatedSuccessfully.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/CreatedSuccessfully.tsx
index 92a21bfd1..b78236bc7 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/CreatedSuccessfully.tsx
+++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/CreatedSuccessfully.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,12 +17,13 @@ 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.Tips.TipCreateConfirmation;
+type Entity = MerchantBackend.Rewards.RewardCreateConfirmation;
interface Props {
entity: Entity;
- request: MerchantBackend.Tips.TipCreateRequest;
+ request: MerchantBackend.Rewards.RewardCreateRequest;
onConfirm: () => void;
onCreateAnother?: () => void;
}
@@ -33,6 +34,7 @@ export function CreatedSuccessfully({
onConfirm,
onCreateAnother,
}: Props): VNode {
+ const [settings] = useSettings();
return (
<Fragment>
<div class="field is-horizontal">
@@ -66,7 +68,7 @@ export function CreatedSuccessfully({
<div class="field-body is-flex-grow-3">
<div class="field">
<p class="control">
- <input readonly class="input" value={entity.tip_status_url} />
+ <input readonly class="input" value={entity.reward_status_url} />
</p>
</div>
</div>
@@ -82,13 +84,13 @@ export function CreatedSuccessfully({
class="input"
readonly
value={
- !entity.tip_expiration ||
- entity.tip_expiration.t_s === "never"
+ !entity.reward_expiration ||
+ entity.reward_expiration.t_s === "never"
? "never"
: format(
- entity.tip_expiration.t_s * 1000,
- "yyyy/MM/dd HH:mm:ss"
- )
+ entity.reward_expiration.t_s * 1000,
+ datetimeFormatForSettings(settings),
+ )
}
/>
</p>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/List.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/List.stories.tsx
index 7a4e9ff42..b070bbde3 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/List.stories.tsx
+++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/List.stories.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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
@@ -25,17 +25,11 @@ import { CardTable as TestedComponent } from "./Table.js";
export default {
title: "Pages/Reserve/List",
component: TestedComponent,
- argTypes: {
- onCreate: { action: "onCreate" },
- onDelete: { action: "onDelete" },
- onNewTip: { action: "onNewTip" },
- onSelect: { action: "onSelect" },
- },
};
function createExample<Props>(
Component: FunctionalComponent<Props>,
- props: Partial<Props>
+ props: Partial<Props>,
) {
const r = (args: any) => <Component {...args} />;
r.args = props;
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/Table.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/Table.tsx
index 0d094d9aa..795e7ec82 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/Table.tsx
+++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/Table.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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
@@ -19,16 +19,17 @@
* @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 { Translate, useTranslator } from "../../../../i18n/index.js";
+import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
-type Entity = MerchantBackend.Tips.ReserveStatusEntry & WithId;
+type Entity = MerchantBackend.Rewards.ReserveStatusEntry & WithId;
interface Props {
instances: Entity[];
- onNewTip: (id: Entity) => void;
+ onNewReward: (id: Entity) => void;
onSelect: (id: Entity) => void;
onDelete: (id: Entity) => void;
onCreate: () => void;
@@ -38,7 +39,7 @@ export function CardTable({
instances,
onCreate,
onSelect,
- onNewTip,
+ onNewReward,
onDelete,
}: Props): VNode {
const [withoutFunds, withFunds] = instances.reduce((prev, current) => {
@@ -51,7 +52,7 @@ export function CardTable({
return prev;
}, new Array<Array<Entity>>([], []));
- const i18n = useTranslator();
+ const { i18n } = useTranslationContext();
return (
<Fragment>
@@ -62,7 +63,7 @@ export function CardTable({
<span class="icon">
<i class="mdi mdi-cash" />
</span>
- <Translate>Reserves not yet funded</Translate>
+ <i18n.Translate>Reserves not yet funded</i18n.Translate>
</p>
</header>
<div class="card-content">
@@ -70,7 +71,7 @@ export function CardTable({
<div class="table-wrapper has-mobile-cards">
<TableWithoutFund
instances={withoutFunds}
- onNewTip={onNewTip}
+ onNewReward={onNewReward}
onSelect={onSelect}
onDelete={onDelete}
/>
@@ -86,11 +87,14 @@ export function CardTable({
<span class="icon">
<i class="mdi mdi-cash" />
</span>
- <Translate>Reserves ready</Translate>
+ <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`add new reserve`}>
+ <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" />
@@ -105,7 +109,7 @@ export function CardTable({
{withFunds.length > 0 ? (
<Table
instances={withFunds}
- onNewTip={onNewTip}
+ onNewReward={onNewReward}
onSelect={onSelect}
onDelete={onDelete}
/>
@@ -121,32 +125,33 @@ export function CardTable({
}
interface TableProps {
instances: Entity[];
- onNewTip: (id: Entity) => void;
+ onNewReward: (id: Entity) => void;
onDelete: (id: Entity) => void;
onSelect: (id: Entity) => void;
}
-function Table({ instances, onNewTip, onSelect, onDelete }: TableProps): VNode {
- const i18n = useTranslator();
+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>
- <Translate>Created at</Translate>
+ <i18n.Translate>Created at</i18n.Translate>
</th>
<th>
- <Translate>Expires at</Translate>
+ <i18n.Translate>Expires at</i18n.Translate>
</th>
<th>
- <Translate>Initial</Translate>
+ <i18n.Translate>Initial</i18n.Translate>
</th>
<th>
- <Translate>Picked up</Translate>
+ <i18n.Translate>Picked up</i18n.Translate>
</th>
<th>
- <Translate>Committed</Translate>
+ <i18n.Translate>Committed</i18n.Translate>
</th>
<th />
</tr>
@@ -161,7 +166,7 @@ function Table({ instances, onNewTip, onSelect, onDelete }: TableProps): VNode {
>
{i.creation_time.t_s === "never"
? "never"
- : format(i.creation_time.t_s * 1000, "yyyy/MM/dd HH:mm:ss")}
+ : format(i.creation_time.t_s * 1000, datetimeFormatForSettings(settings))}
</td>
<td
onClick={(): void => onSelect(i)}
@@ -170,9 +175,9 @@ function Table({ instances, onNewTip, onSelect, onDelete }: TableProps): VNode {
{i.expiration_time.t_s === "never"
? "never"
: format(
- i.expiration_time.t_s * 1000,
- "yyyy/MM/dd HH:mm:ss"
- )}
+ i.expiration_time.t_s * 1000,
+ datetimeFormatForSettings(settings),
+ )}
</td>
<td
onClick={(): void => onSelect(i)}
@@ -196,7 +201,7 @@ function Table({ instances, onNewTip, onSelect, onDelete }: TableProps): VNode {
<div class="buttons is-right">
<button
class="button is-small is-danger has-tooltip-left"
- data-tooltip={i18n`delete selected reserve from the database`}
+ data-tooltip={i18n.str`delete selected reserve from the database`}
type="button"
onClick={(): void => onDelete(i)}
>
@@ -204,11 +209,11 @@ function Table({ instances, onNewTip, onSelect, onDelete }: TableProps): VNode {
</button>
<button
class="button is-small is-info has-tooltip-left"
- data-tooltip={i18n`authorize new tip from selected reserve`}
+ data-tooltip={i18n.str`authorize new reward from selected reserve`}
type="button"
- onClick={(): void => onNewTip(i)}
+ onClick={(): void => onNewReward(i)}
>
- New Tip
+ New Reward
</button>
</div>
</td>
@@ -222,6 +227,7 @@ function Table({ instances, onNewTip, onSelect, onDelete }: TableProps): VNode {
}
function EmptyTable(): VNode {
+ const { i18n } = useTranslationContext();
return (
<div class="content has-text-grey has-text-centered">
<p>
@@ -230,10 +236,10 @@ function EmptyTable(): VNode {
</span>
</p>
<p>
- <Translate>
+ <i18n.Translate>
There is no ready reserves yet, add more pressing the + sign or fund
them
- </Translate>
+ </i18n.Translate>
</p>
</div>
);
@@ -244,20 +250,21 @@ function TableWithoutFund({
onSelect,
onDelete,
}: TableProps): VNode {
- const i18n = useTranslator();
+ 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>
- <Translate>Created at</Translate>
+ <i18n.Translate>Created at</i18n.Translate>
</th>
<th>
- <Translate>Expires at</Translate>
+ <i18n.Translate>Expires at</i18n.Translate>
</th>
<th>
- <Translate>Expected Balance</Translate>
+ <i18n.Translate>Expected Balance</i18n.Translate>
</th>
<th />
</tr>
@@ -272,7 +279,7 @@ function TableWithoutFund({
>
{i.creation_time.t_s === "never"
? "never"
- : format(i.creation_time.t_s * 1000, "yyyy/MM/dd HH:mm:ss")}
+ : format(i.creation_time.t_s * 1000, datetimeFormatForSettings(settings))}
</td>
<td
onClick={(): void => onSelect(i)}
@@ -281,9 +288,9 @@ function TableWithoutFund({
{i.expiration_time.t_s === "never"
? "never"
: format(
- i.expiration_time.t_s * 1000,
- "yyyy/MM/dd HH:mm:ss"
- )}
+ i.expiration_time.t_s * 1000,
+ datetimeFormatForSettings(settings),
+ )}
</td>
<td
onClick={(): void => onSelect(i)}
@@ -296,7 +303,7 @@ function TableWithoutFund({
<button
class="button is-small is-danger jb-modal has-tooltip-left"
type="button"
- data-tooltip={i18n`delete selected reserve from the database`}
+ data-tooltip={i18n.str`delete selected reserve from the database`}
onClick={(): void => onDelete(i)}
>
Delete
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/index.tsx
new file mode 100644
index 000000000..b26ff0000
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/index.tsx
@@ -0,0 +1,171 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import {
+ useInstanceReserves,
+ useReservesAPI,
+} from "../../../../hooks/reserves.js";
+import { Notification } from "../../../../utils/types.js";
+import { AuthorizeRewardModal } from "./AutorizeRewardModal.js";
+import { CardTable } from "./Table.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+import { ConfirmModal } from "../../../../components/modal/index.js";
+
+interface Props {
+ onUnauthorized: () => VNode;
+ onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ onSelect: (id: string) => void;
+ onNotFound: () => VNode;
+ onCreate: () => void;
+}
+
+interface RewardConfirmation {
+ response: MerchantBackend.Rewards.RewardCreateConfirmation;
+ request: MerchantBackend.Rewards.RewardCreateRequest;
+}
+
+export default function ListRewards({
+ onUnauthorized,
+ onLoadError,
+ onNotFound,
+ onSelect,
+ onCreate,
+}: Props): VNode {
+ const result = useInstanceReserves();
+ const { deleteReserve, authorizeRewardReserve } = useReservesAPI();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+ const [reserveForReward, setReserveForReward] = useState<string | undefined>(
+ undefined,
+ );
+ const [deleting, setDeleting] =
+ useState<MerchantBackend.Rewards.ReserveStatusEntry | null>(null);
+ const [rewardAuthorized, setRewardAuthorized] = useState<
+ RewardConfirmation | undefined
+ >(undefined);
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <section class="section is-main-section">
+ <NotificationCard notification={notif} />
+
+ {reserveForReward && (
+ <AuthorizeRewardModal
+ onCancel={() => {
+ setReserveForReward(undefined);
+ setRewardAuthorized(undefined);
+ }}
+ rewardAuthorized={rewardAuthorized}
+ onConfirm={async (request) => {
+ try {
+ const response = await authorizeRewardReserve(
+ reserveForReward,
+ request,
+ );
+ setRewardAuthorized({
+ request,
+ response: response.data,
+ });
+ } catch (error) {
+ setNotif({
+ message: i18n.str`could not create the reward`,
+ type: "ERROR",
+ description: error instanceof Error ? error.message : undefined,
+ });
+ setReserveForReward(undefined);
+ }
+ }}
+ />
+ )}
+
+ <CardTable
+ instances={result.data.reserves
+ .filter((r) => r.active)
+ .map((o) => ({ ...o, id: o.reserve_pub }))}
+ onCreate={onCreate}
+ onDelete={(reserve) => {
+ setDeleting(reserve)
+ }}
+ onSelect={(reserve) => onSelect(reserve.id)}
+ onNewReward={(reserve) => setReserveForReward(reserve.id)}
+ />
+
+ {deleting && (
+ <ConfirmModal
+ label={`Delete reserve`}
+ description={`Delete the reserve`}
+ danger
+ active
+ onCancel={() => setDeleting(null)}
+ onConfirm={async (): Promise<void> => {
+ try {
+ await deleteReserve(deleting.reserve_pub);
+ setNotif({
+ message: i18n.str`Reserve for "${deleting.merchant_initial_amount}" (ID: ${deleting.reserve_pub}) has been deleted`,
+ type: "SUCCESS",
+ });
+ } catch (error) {
+ setNotif({
+ message: i18n.str`Failed to delete reserve`,
+ type: "ERROR",
+ description: error instanceof Error ? error.message : undefined,
+ });
+ }
+ setDeleting(null);
+ }}
+ >
+ <p>
+ If you delete the reserve for <b>&quot;{deleting.merchant_initial_amount}&quot;</b> you won't be able to create more rewards. <br />
+ Reserve ID: <b>{deleting.reserve_pub}</b>
+ </p>
+ <p class="warning">
+ Deleting an template <b>cannot be undone</b>.
+ </p>
+ </ConfirmModal>
+ )}
+
+ </section>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/create/Create.stories.tsx
new file mode 100644
index 000000000..c9d17ea3b
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/create/Create.stories.tsx
@@ -0,0 +1,28 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { CreatePage as TestedComponent } from "./CreatePage.js";
+
+export default {
+ title: "Pages/Templates/Create",
+ component: TestedComponent,
+};
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx
new file mode 100644
index 000000000..947f3572c
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx
@@ -0,0 +1,259 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import {
+ Amounts,
+ MerchantTemplateContractDetails,
+} from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { InputCurrency } from "../../../../components/form/InputCurrency.js";
+import { InputDuration } from "../../../../components/form/InputDuration.js";
+import { InputNumber } from "../../../../components/form/InputNumber.js";
+import { InputSearchOnList } from "../../../../components/form/InputSearchOnList.js";
+import { InputWithAddon } from "../../../../components/form/InputWithAddon.js";
+import { useBackendContext } from "../../../../context/backend.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useInstanceOtpDevices } from "../../../../hooks/otp.js";
+import { undefinedIfEmpty } from "../../../../utils/table.js";
+import { InputTab } from "../../../../components/form/InputTab.js";
+
+enum Steps {
+ BOTH_FIXED,
+ FIXED_PRICE,
+ FIXED_SUMMARY,
+ NON_FIXED,
+}
+
+type Entity = MerchantBackend.Template.TemplateAddDetails & { type: Steps };
+
+interface Props {
+ onCreate: (d: Entity) => Promise<void>;
+ onBack?: () => void;
+}
+
+export function CreatePage({ onCreate, onBack }: Props): VNode {
+ const { i18n } = useTranslationContext();
+ const { url: backendURL } = useBackendContext()
+ const devices = useInstanceOtpDevices()
+
+ const [state, setState] = useState<Partial<Entity>>({
+ template_contract: {
+ minimum_age: 0,
+ pay_duration: {
+ d_us: 1000 * 1000 * 60 * 30, //30 min
+ },
+ },
+ type: Steps.NON_FIXED,
+ });
+
+ const parsedPrice = !state.template_contract?.amount
+ ? undefined
+ : Amounts.parse(state.template_contract?.amount);
+
+ const errors: FormErrors<Entity> = {
+ template_id: !state.template_id
+ ? i18n.str`should not be empty`
+ : !/[a-zA-Z0-9]*/.test(state.template_id)
+ ? i18n.str`no valid. only characters and numbers`
+ : undefined,
+ template_description: !state.template_description
+ ? i18n.str`should not be empty`
+ : undefined,
+ template_contract: !state.template_contract
+ ? undefined
+ : undefinedIfEmpty({
+ amount: !(state.type === Steps.FIXED_PRICE || state.type === Steps.BOTH_FIXED)
+ ? undefined
+ : !state.template_contract?.amount
+ ? i18n.str`required`
+ : !parsedPrice
+ ? i18n.str`not valid`
+ : Amounts.isZero(parsedPrice)
+ ? i18n.str`must be greater than 0`
+ : undefined,
+ summary: !(state.type === Steps.FIXED_SUMMARY || state.type === Steps.BOTH_FIXED)
+ ? undefined
+ : !state.template_contract?.summary
+ ? i18n.str`required`
+ : undefined,
+ minimum_age:
+ state.template_contract.minimum_age < 0
+ ? i18n.str`should be greater that 0`
+ : undefined,
+ pay_duration: !state.template_contract.pay_duration
+ ? i18n.str`can't be empty`
+ : state.template_contract.pay_duration.d_us === "forever"
+ ? undefined
+ : state.template_contract.pay_duration.d_us < 1000 * 1000 //less than one second
+ ? i18n.str`to short`
+ : undefined,
+ } as Partial<MerchantTemplateContractDetails>),
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const submitForm = () => {
+ if (hasErrors) return Promise.reject();
+ if (state.template_contract) {
+ if (state.type === Steps.NON_FIXED) {
+ delete state.template_contract.amount;
+ delete state.template_contract.summary;
+ } else if (state.type === Steps.FIXED_SUMMARY) {
+ delete state.template_contract.amount;
+ } else if (state.type === Steps.FIXED_PRICE) {
+ delete state.template_contract.summary;
+ }
+ }
+ delete state.type
+ return onCreate(state as any);
+ };
+
+ const deviceList = !devices.ok ? [] : devices.data.otp_devices
+
+ return (
+ <div>
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <FormProvider
+ object={state}
+ valueHandler={setState}
+ errors={errors}
+ >
+ <InputWithAddon<Entity>
+ name="template_id"
+ help={`${backendURL}/templates/${state.template_id ?? ""}`}
+ label={i18n.str`Identifier`}
+ tooltip={i18n.str`Name of the template in URLs.`}
+ />
+ <Input<Entity>
+ name="template_description"
+ label={i18n.str`Description`}
+ help=""
+ tooltip={i18n.str`Describe what this template stands for`}
+ />
+ <InputTab
+ name="type"
+ label={i18n.str`Type`}
+ help={(() => {
+ switch (state.type) {
+ case Steps.NON_FIXED: return i18n.str`User will be able to input price and summary before payment.`
+ case Steps.FIXED_PRICE: return i18n.str`User will be able to add a summary before payment.`
+ case Steps.FIXED_SUMMARY: return i18n.str`User will be able to set the price before payment.`
+ case Steps.BOTH_FIXED: return i18n.str`User will not be able to change the price or the summary.`
+ }
+ })()}
+ tooltip={i18n.str`Define what the user be allowed to modify`}
+ values={[
+ Steps.NON_FIXED,
+ Steps.FIXED_PRICE,
+ Steps.FIXED_SUMMARY,
+ Steps.BOTH_FIXED,
+ ]}
+ toStr={(v: Steps): string => {
+ switch (v) {
+ case Steps.NON_FIXED: return i18n.str`Simple`
+ case Steps.FIXED_PRICE: return i18n.str`With price`
+ case Steps.FIXED_SUMMARY: return i18n.str`With summary`
+ case Steps.BOTH_FIXED: return i18n.str`With price and summary`
+ }
+ }}
+ />
+ {state.type === Steps.BOTH_FIXED || state.type === Steps.FIXED_SUMMARY ?
+ <Input
+ name="template_contract.summary"
+ inputType="multiline"
+ label={i18n.str`Fixed summary`}
+ tooltip={i18n.str`If specified, this template will create order with the same summary`}
+ />
+ : undefined}
+ {state.type === Steps.BOTH_FIXED || state.type === Steps.FIXED_PRICE ?
+ <InputCurrency
+ name="template_contract.amount"
+ label={i18n.str`Fixed price`}
+ tooltip={i18n.str`If specified, this template will create order with the same price`}
+ />
+ : undefined}
+ <InputNumber
+ name="template_contract.minimum_age"
+ label={i18n.str`Minimum age`}
+ help=""
+ tooltip={i18n.str`Is this contract restricted to some age?`}
+ />
+ <InputDuration
+ name="template_contract.pay_duration"
+ label={i18n.str`Payment timeout`}
+ help=""
+ tooltip={i18n.str`How much time has the customer to complete the payment once the order was created.`}
+ />
+ <Input<Entity>
+ name="otp_id"
+ label={i18n.str`OTP device`}
+ readonly
+ tooltip={i18n.str`Use to verify transaction in offline mode.`}
+ />
+ <InputSearchOnList
+ label={i18n.str`Search device`}
+ onChange={(p) => setState((v) => ({ ...v, otp_id: p?.id }))}
+ list={deviceList.map(e => ({
+ description: e.device_description,
+ id: e.otp_device_id
+ }))}
+ />
+
+ </FormProvider>
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ disabled={hasErrors}
+ data-tooltip={
+ hasErrors
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ onClick={submitForm}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/create/index.tsx
new file mode 100644
index 000000000..a29ee53b6
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/create/index.tsx
@@ -0,0 +1,61 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useTemplateAPI } from "../../../../hooks/templates.js";
+import { Notification } from "../../../../utils/types.js";
+import { CreatePage } from "./CreatePage.js";
+
+export type Entity = MerchantBackend.Transfers.TransferInformation;
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+}
+
+export default function CreateTransfer({ onConfirm, onBack }: Props): VNode {
+ const { createTemplate } = useTemplateAPI();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+
+ return (
+ <>
+ <NotificationCard notification={notif} />
+ <CreatePage
+ onBack={onBack}
+ onCreate={(request: MerchantBackend.Template.TemplateAddDetails) => {
+ return createTemplate(request)
+ .then(() => onConfirm())
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not inform template`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/list/List.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/list/List.stories.tsx
new file mode 100644
index 000000000..702e9ba4a
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/list/List.stories.tsx
@@ -0,0 +1,28 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { FunctionalComponent, h } from "preact";
+import { ListPage as TestedComponent } from "./ListPage.js";
+
+export default {
+ title: "Pages/Templates/List",
+ component: TestedComponent,
+};
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx
new file mode 100644
index 000000000..bf6062c34
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx
@@ -0,0 +1,68 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode } from "preact";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { CardTable } from "./Table.js";
+
+export interface Props {
+ templates: MerchantBackend.Template.TemplateEntry[];
+ onLoadMoreBefore?: () => void;
+ onLoadMoreAfter?: () => void;
+ onCreate: () => void;
+ onDelete: (e: MerchantBackend.Template.TemplateEntry) => void;
+ onSelect: (e: MerchantBackend.Template.TemplateEntry) => void;
+ onNewOrder: (e: MerchantBackend.Template.TemplateEntry) => void;
+ onQR: (e: MerchantBackend.Template.TemplateEntry) => void;
+}
+
+export function ListPage({
+ templates,
+ onCreate,
+ onDelete,
+ onSelect,
+ onNewOrder,
+ onQR,
+ onLoadMoreBefore,
+ onLoadMoreAfter,
+}: Props): VNode {
+ const form = { payto_uri: "" };
+
+ const { i18n } = useTranslationContext();
+ return (
+ <CardTable
+ templates={templates.map((o) => ({
+ ...o,
+ id: String(o.template_id),
+ }))}
+ onQR={onQR}
+ onCreate={onCreate}
+ onDelete={onDelete}
+ onSelect={onSelect}
+ onNewOrder={onNewOrder}
+ onLoadMoreBefore={onLoadMoreBefore}
+ hasMoreBefore={!onLoadMoreBefore}
+ onLoadMoreAfter={onLoadMoreAfter}
+ hasMoreAfter={!onLoadMoreAfter}
+ />
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/list/Table.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/list/Table.tsx
new file mode 100644
index 000000000..9fdf4ead9
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/list/Table.tsx
@@ -0,0 +1,235 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { StateUpdater, useState } from "preact/hooks";
+import { MerchantBackend } from "../../../../declaration.js";
+
+type Entity = MerchantBackend.Template.TemplateEntry;
+
+interface Props {
+ templates: Entity[];
+ onDelete: (e: Entity) => void;
+ onSelect: (e: Entity) => void;
+ onNewOrder: (e: Entity) => void;
+ onQR: (e: Entity) => void;
+ onCreate: () => void;
+ onLoadMoreBefore?: () => void;
+ hasMoreBefore?: boolean;
+ hasMoreAfter?: boolean;
+ onLoadMoreAfter?: () => void;
+}
+
+export function CardTable({
+ templates,
+ onCreate,
+ onDelete,
+ onSelect,
+ onQR,
+ onNewOrder,
+ onLoadMoreAfter,
+ onLoadMoreBefore,
+ hasMoreAfter,
+ hasMoreBefore,
+}: Props): VNode {
+ const [rowSelection, rowSelectionHandler] = useState<string[]>([]);
+
+ const { i18n } = useTranslationContext();
+
+ return (
+ <div class="card has-table">
+ <header class="card-header">
+ <p class="card-header-title">
+ <span class="icon">
+ <i class="mdi mdi-newspaper" />
+ </span>
+ <i18n.Translate>Templates</i18n.Translate>
+ </p>
+ <div class="card-header-icon" aria-label="more options">
+ <span
+ class="has-tooltip-left"
+ data-tooltip={i18n.str`add new templates`}
+ >
+ <button class="button is-info" type="button" onClick={onCreate}>
+ <span class="icon is-small">
+ <i class="mdi mdi-plus mdi-36px" />
+ </span>
+ </button>
+ </span>
+ </div>
+ </header>
+ <div class="card-content">
+ <div class="b-table has-pagination">
+ <div class="table-wrapper has-mobile-cards">
+ {templates.length > 0 ? (
+ <Table
+ instances={templates}
+ onDelete={onDelete}
+ onSelect={onSelect}
+ onNewOrder={onNewOrder}
+ onQR={onQR}
+ rowSelection={rowSelection}
+ rowSelectionHandler={rowSelectionHandler}
+ onLoadMoreAfter={onLoadMoreAfter}
+ onLoadMoreBefore={onLoadMoreBefore}
+ hasMoreAfter={hasMoreAfter}
+ hasMoreBefore={hasMoreBefore}
+ />
+ ) : (
+ <EmptyTable />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+}
+interface TableProps {
+ rowSelection: string[];
+ instances: Entity[];
+ onDelete: (e: Entity) => void;
+ onNewOrder: (e: Entity) => void;
+ onQR: (e: Entity) => void;
+ onSelect: (e: Entity) => void;
+ rowSelectionHandler: StateUpdater<string[]>;
+ onLoadMoreBefore?: () => void;
+ hasMoreBefore?: boolean;
+ hasMoreAfter?: boolean;
+ onLoadMoreAfter?: () => void;
+}
+
+function toggleSelected<T>(id: T): (prev: T[]) => T[] {
+ return (prev: T[]): T[] =>
+ prev.indexOf(id) == -1 ? [...prev, id] : prev.filter((e) => e != id);
+}
+
+function Table({
+ instances,
+ onLoadMoreAfter,
+ onDelete,
+ onNewOrder,
+ onQR,
+ onSelect,
+ onLoadMoreBefore,
+ hasMoreAfter,
+ hasMoreBefore,
+}: TableProps): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="table-container">
+ {hasMoreBefore && (
+ <button
+ class="button is-fullwidth"
+ data-tooltip={i18n.str`load more templates before the first one`}
+ onClick={onLoadMoreBefore}
+ >
+ <i18n.Translate>load newer templates</i18n.Translate>
+ </button>
+ )}
+ <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>ID</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Description</i18n.Translate>
+ </th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ {instances.map((i) => {
+ return (
+ <tr key={i.template_id}>
+ <td
+ onClick={(): void => onSelect(i)}
+ style={{ cursor: "pointer" }}
+ >
+ {i.template_id}
+ </td>
+ <td
+ onClick={(): void => onSelect(i)}
+ style={{ cursor: "pointer" }}
+ >
+ {i.template_description}
+ </td>
+ <td class="is-actions-cell right-sticky">
+ <div class="buttons is-right">
+ <button
+ class="button is-danger is-small has-tooltip-left"
+ data-tooltip={i18n.str`delete selected templates from the database`}
+ onClick={() => onDelete(i)}
+ >
+ Delete
+ </button>
+ <button
+ class="button is-info is-small has-tooltip-left"
+ data-tooltip={i18n.str`use template to create new order`}
+ onClick={() => onNewOrder(i)}
+ >
+ Use template
+ </button>
+ <button
+ class="button is-info is-small has-tooltip-left"
+ data-tooltip={i18n.str`create qr code for the template`}
+ onClick={() => onQR(i)}
+ >
+ QR
+ </button>
+ </div>
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ {hasMoreAfter && (
+ <button
+ class="button is-fullwidth"
+ data-tooltip={i18n.str`load more templates after the last one`}
+ onClick={onLoadMoreAfter}
+ >
+ <i18n.Translate>load older templates</i18n.Translate>
+ </button>
+ )}
+ </div>
+ );
+}
+
+function EmptyTable(): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="content has-text-grey has-text-centered">
+ <p>
+ <span class="icon is-large">
+ <i class="mdi mdi-emoticon-sad mdi-48px" />
+ </span>
+ </p>
+ <p>
+ <i18n.Translate>
+ There is no templates yet, add more pressing the + sign
+ </i18n.Translate>
+ </p>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/list/index.tsx
new file mode 100644
index 000000000..c7927b772
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/list/index.tsx
@@ -0,0 +1,152 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import {
+ useInstanceTemplates,
+ useTemplateAPI,
+} from "../../../../hooks/templates.js";
+import { Notification } from "../../../../utils/types.js";
+import { ListPage } from "./ListPage.js";
+import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util";
+import { ConfirmModal } from "../../../../components/modal/index.js";
+import { JumpToElementById } from "../../../../components/form/JumpToElementById.js";
+
+interface Props {
+ onUnauthorized: () => VNode;
+ onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ onNotFound: () => VNode;
+ onCreate: () => void;
+ onSelect: (id: string) => void;
+ onNewOrder: (id: string) => void;
+ onQR: (id: string) => void;
+}
+
+export default function ListTemplates({
+ onUnauthorized,
+ onLoadError,
+ onCreate,
+ onQR,
+ onSelect,
+ onNewOrder,
+ onNotFound,
+}: Props): VNode {
+ const [position, setPosition] = useState<string | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { deleteTemplate, testTemplateExist } = useTemplateAPI();
+ const result = useInstanceTemplates({ position }, (id) => setPosition(id));
+ const [deleting, setDeleting] =
+ useState<MerchantBackend.Template.TemplateEntry | null>(null);
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <section class="section is-main-section">
+ <NotificationCard notification={notif} />
+
+ <JumpToElementById
+ testIfExist={testTemplateExist}
+ onSelect={onSelect}
+ description={i18n.str`jump to template with the given template ID`}
+ 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
new file mode 100644
index 000000000..eb853c8ff
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/qr/Qr.stories.tsx
@@ -0,0 +1,27 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { QrPage as TestedComponent } from "./QrPage.js";
+
+export default {
+ title: "Pages/Templates/QR",
+ component: TestedComponent,
+};
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx
new file mode 100644
index 000000000..5140aae3a
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx
@@ -0,0 +1,172 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { stringifyPayTemplateUri } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { QR } from "../../../../components/exception/QR.js";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { InputCurrency } from "../../../../components/form/InputCurrency.js";
+import { useBackendContext } from "../../../../context/backend.js";
+import { useConfigContext } from "../../../../context/config.js";
+import { useInstanceContext } from "../../../../context/instance.js";
+import { MerchantBackend } from "../../../../declaration.js";
+
+type Entity = MerchantBackend.Template.UsingTemplateDetails;
+
+interface Props {
+ contract: MerchantBackend.Template.TemplateContractDetails;
+ id: string;
+ onBack?: () => void;
+}
+
+export function QrPage({ contract, id: templateId, onBack }: Props): VNode {
+ const { i18n } = useTranslationContext();
+ const { url: backendURL } = useBackendContext()
+ const { id: instanceId } = useInstanceContext();
+ const config = useConfigContext();
+
+ const [state, setState] = useState<Partial<Entity>>({
+ amount: contract.amount,
+ summary: contract.summary,
+ });
+
+ const errors: FormErrors<Entity> = {};
+
+ const fixedAmount = !!contract.amount;
+ const fixedSummary = !!contract.summary;
+
+ const templateParams: Record<string, string> = {}
+ if (!fixedAmount) {
+ if (state.amount) {
+ templateParams.amount = state.amount
+ } else {
+ templateParams.amount = config.currency
+ }
+ }
+
+ if (!fixedSummary) {
+ templateParams.summary = state.summary ?? ""
+ }
+
+ const merchantBaseUrl = new URL(backendURL).href;
+
+ const payTemplateUri = stringifyPayTemplateUri({
+ merchantBaseUrl,
+ templateId,
+ templateParams
+ })
+
+ const issuer = encodeURIComponent(
+ `${new URL(backendURL).host}/${instanceId}`,
+ );
+
+ return (
+ <div>
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <p class="is-size-5 mt-5 mb-5">
+ <i18n.Translate>
+ Here you can specify a default value for fields that are not
+ fixed. Default values can be edited by the customer before the
+ payment.
+ </i18n.Translate>
+ </p>
+
+ <p></p>
+ <FormProvider
+ object={state}
+ valueHandler={setState}
+ errors={errors}
+ >
+ <InputCurrency<Entity>
+ name="amount"
+ label={
+ fixedAmount
+ ? i18n.str`Fixed amount`
+ : i18n.str`Default amount`
+ }
+ readonly={fixedAmount}
+ tooltip={i18n.str`Amount of the order`}
+ />
+ <Input<Entity>
+ name="summary"
+ inputType="multiline"
+ readonly={fixedSummary}
+ label={
+ fixedSummary
+ ? i18n.str`Fixed summary`
+ : i18n.str`Default summary`
+ }
+ tooltip={i18n.str`Title of the order to be shown to the customer`}
+ />
+ </FormProvider>
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <button
+ class="button is-info"
+ onClick={() => saveAsPDF(templateId)}
+ >
+ <i18n.Translate>Print</i18n.Translate>
+ </button>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ <section id="printThis">
+ <QR text={payTemplateUri} />
+ <pre style={{ textAlign: "center" }}>
+ <a href={payTemplateUri}>{payTemplateUri}</a>
+ </pre>
+ </section>
+ </div>
+ );
+}
+
+function saveAsPDF(name: string): void {
+ const printWindow = window.open("", "", "height=400,width=800");
+ if (!printWindow) return;
+ const divContents = document.getElementById("printThis");
+ if (!divContents) return;
+ printWindow.document.write(
+ `<html><head><title>Order template for ${name}</title><style>`,
+ );
+ printWindow.document.write("</style></head><body>&nbsp;</body></html>");
+ printWindow.document.close();
+ printWindow.document.body.appendChild(divContents.cloneNode(true));
+ printWindow.addEventListener("load", () => {
+ printWindow.print();
+ printWindow.close();
+ });
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/qr/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/qr/index.tsx
new file mode 100644
index 000000000..7db7478f7
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/qr/index.tsx
@@ -0,0 +1,80 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import {
+ useTemplateAPI,
+ useTemplateDetails,
+} from "../../../../hooks/templates.js";
+import { Notification } from "../../../../utils/types.js";
+import { QrPage } from "./QrPage.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+
+export type Entity = MerchantBackend.Transfers.TransferInformation;
+interface Props {
+ onBack?: () => void;
+ onUnauthorized: () => VNode;
+ onNotFound: () => VNode;
+ onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ tid: string;
+}
+
+export default function TemplateQrPage({
+ tid,
+ onBack,
+ onLoadError,
+ onNotFound,
+ onUnauthorized,
+}: Props): VNode {
+ const result = useTemplateDetails(tid);
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <>
+ <NotificationCard notification={notif} />
+ <QrPage contract={result.data.template_contract} id={tid} onBack={onBack} />
+ </>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/update/Update.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/update/Update.stories.tsx
new file mode 100644
index 000000000..8d07cb31f
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/update/Update.stories.tsx
@@ -0,0 +1,32 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { UpdatePage as TestedComponent } from "./UpdatePage.js";
+
+export default {
+ title: "Pages/Templates/Update",
+ component: TestedComponent,
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx
new file mode 100644
index 000000000..b578d4664
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx
@@ -0,0 +1,254 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import {
+ Amounts,
+ MerchantTemplateContractDetails,
+} from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { InputCurrency } from "../../../../components/form/InputCurrency.js";
+import { InputDuration } from "../../../../components/form/InputDuration.js";
+import { InputNumber } from "../../../../components/form/InputNumber.js";
+import { InputWithAddon } from "../../../../components/form/InputWithAddon.js";
+import { useBackendContext } from "../../../../context/backend.js";
+import { MerchantBackend, WithId } from "../../../../declaration.js";
+import { undefinedIfEmpty } from "../../../../utils/table.js";
+import { InputTab } from "../../../../components/form/InputTab.js";
+
+enum Steps {
+ BOTH_FIXED,
+ FIXED_PRICE,
+ FIXED_SUMMARY,
+ NON_FIXED,
+}
+
+type Entity = MerchantBackend.Template.TemplatePatchDetails & WithId;
+
+interface Props {
+ onUpdate: (d: Entity) => Promise<void>;
+ onBack?: () => void;
+ template: Entity;
+}
+
+export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
+ const { i18n } = useTranslationContext();
+ const { url: backendURL } = useBackendContext()
+
+ const intialStep =
+ template.template_contract?.amount === undefined && template.template_contract?.summary === undefined
+ ? Steps.NON_FIXED
+ : template.template_contract?.summary === undefined
+ ? Steps.FIXED_PRICE
+ : template.template_contract?.amount === undefined
+ ? Steps.FIXED_SUMMARY
+ : Steps.BOTH_FIXED;
+
+ const [state, setState] = useState<Partial<Entity & { type: Steps }>>({ ...template, type: intialStep });
+
+ const parsedPrice = !state.template_contract?.amount
+ ? undefined
+ : Amounts.parse(state.template_contract?.amount);
+
+ const errors: FormErrors<Entity> = {
+ template_description: !state.template_description
+ ? i18n.str`should not be empty`
+ : undefined,
+ template_contract: !state.template_contract
+ ? undefined
+ : undefinedIfEmpty({
+ amount: !(state.type === Steps.FIXED_PRICE || state.type === Steps.BOTH_FIXED)
+ ? undefined
+ : !state.template_contract?.amount
+ ? i18n.str`required`
+ : !parsedPrice
+ ? i18n.str`not valid`
+ : Amounts.isZero(parsedPrice)
+ ? i18n.str`must be greater than 0`
+ : undefined,
+ summary: !(state.type === Steps.FIXED_SUMMARY || state.type === Steps.BOTH_FIXED)
+ ? undefined
+ : !state.template_contract?.summary
+ ? i18n.str`required`
+ : undefined,
+ minimum_age:
+ state.template_contract.minimum_age < 0
+ ? i18n.str`should be greater that 0`
+ : undefined,
+ pay_duration: !state.template_contract.pay_duration
+ ? i18n.str`can't be empty`
+ : state.template_contract.pay_duration.d_us === "forever"
+ ? undefined
+ : state.template_contract.pay_duration.d_us < 1000 * 1000 // less than one second
+ ? i18n.str`to short`
+ : undefined,
+ } as Partial<MerchantTemplateContractDetails>),
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const submitForm = () => {
+ if (hasErrors) return Promise.reject();
+ if (state.template_contract) {
+ if (state.type === Steps.NON_FIXED) {
+ delete state.template_contract.amount;
+ delete state.template_contract.summary;
+ } else if (state.type === Steps.FIXED_SUMMARY) {
+ delete state.template_contract.amount;
+ } else if (state.type === Steps.FIXED_PRICE) {
+ delete state.template_contract.summary;
+ }
+ }
+ delete state.type
+ return onUpdate(state as any);
+ };
+
+
+ return (
+ <div>
+ <section class="section">
+ <section class="hero is-hero-bar">
+ <div class="hero-body">
+ <div class="level">
+ <div class="level-left">
+ <div class="level-item">
+ <span class="is-size-4">
+ {backendURL}/templates/{template.id}
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+ <hr />
+
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column is-four-fifths">
+ <FormProvider
+ object={state}
+ valueHandler={setState}
+ errors={errors}
+ >
+ <InputWithAddon<Entity>
+ name="id"
+ addonBefore={`templates/`}
+ readonly
+ label={i18n.str`Identifier`}
+ tooltip={i18n.str`Name of the template in URLs.`}
+ />
+
+ <Input<Entity>
+ name="template_description"
+ label={i18n.str`Description`}
+ help=""
+ tooltip={i18n.str`Describe what this template stands for`}
+ />
+ <InputTab
+ name="type"
+ label={i18n.str`Type`}
+ help={(() => {
+ switch (state.type) {
+ case Steps.NON_FIXED: return i18n.str`User will be able to input price and summary before payment.`
+ case Steps.FIXED_PRICE: return i18n.str`User will be able to add a summary before payment.`
+ case Steps.FIXED_SUMMARY: return i18n.str`User will be able to set the price before payment.`
+ case Steps.BOTH_FIXED: return i18n.str`User will not be able to change the price or the summary.`
+ }
+ })()}
+ tooltip={i18n.str`Define what the user be allowed to modify`}
+ values={[
+ Steps.NON_FIXED,
+ Steps.FIXED_PRICE,
+ Steps.FIXED_SUMMARY,
+ Steps.BOTH_FIXED,
+ ]}
+ toStr={(v: Steps): string => {
+ switch (v) {
+ case Steps.NON_FIXED: return i18n.str`Simple`
+ case Steps.FIXED_PRICE: return i18n.str`With price`
+ case Steps.FIXED_SUMMARY: return i18n.str`With summary`
+ case Steps.BOTH_FIXED: return i18n.str`With price and summary`
+ }
+ }}
+ />
+ {state.type === Steps.BOTH_FIXED || state.type === Steps.FIXED_SUMMARY ?
+ <Input
+ name="template_contract.summary"
+ inputType="multiline"
+ label={i18n.str`Fixed summary`}
+ tooltip={i18n.str`If specified, this template will create order with the same summary`}
+ />
+ : undefined}
+ {state.type === Steps.BOTH_FIXED || state.type === Steps.FIXED_PRICE ?
+ <InputCurrency
+ name="template_contract.amount"
+ label={i18n.str`Fixed price`}
+ tooltip={i18n.str`If specified, this template will create order with the same price`}
+ />
+ : undefined}
+ <InputNumber
+ name="template_contract.minimum_age"
+ label={i18n.str`Minimum age`}
+ help=""
+ tooltip={i18n.str`Is this contract restricted to some age?`}
+ />
+ <InputDuration
+ name="template_contract.pay_duration"
+ label={i18n.str`Payment timeout`}
+ help=""
+ tooltip={i18n.str`How much time has the customer to complete the payment once the order was created.`}
+ />
+ </FormProvider>
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ disabled={hasErrors}
+ data-tooltip={
+ hasErrors
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ onClick={submitForm}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ </div>
+ </section>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/update/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/update/index.tsx
new file mode 100644
index 000000000..3adca45db
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/update/index.tsx
@@ -0,0 +1,99 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend, WithId } from "../../../../declaration.js";
+import {
+ useTemplateAPI,
+ useTemplateDetails,
+} from "../../../../hooks/templates.js";
+import { Notification } from "../../../../utils/types.js";
+import { UpdatePage } from "./UpdatePage.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+
+export type Entity = MerchantBackend.Template.TemplatePatchDetails & WithId;
+
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+ onUnauthorized: () => VNode;
+ onNotFound: () => VNode;
+ onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ tid: string;
+}
+export default function UpdateTemplate({
+ tid,
+ onConfirm,
+ onBack,
+ onUnauthorized,
+ onNotFound,
+ onLoadError,
+}: Props): VNode {
+ const { updateTemplate } = useTemplateAPI();
+ const result = useTemplateDetails(tid);
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+
+ const { i18n } = useTranslationContext();
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+ <UpdatePage
+ template={{ ...result.data, id: tid }}
+ onBack={onBack}
+ onUpdate={(data) => {
+ return updateTemplate(tid, data)
+ .then(onConfirm)
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not update template`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/use/Use.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/use/Use.stories.tsx
new file mode 100644
index 000000000..13576d94d
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/use/Use.stories.tsx
@@ -0,0 +1,27 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { UsePage as TestedComponent } from "./UsePage.js";
+
+export default {
+ title: "Pages/Templates/Create",
+ component: TestedComponent,
+};
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx
new file mode 100644
index 000000000..983804d3e
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx
@@ -0,0 +1,143 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { InputCurrency } from "../../../../components/form/InputCurrency.js";
+import { MerchantBackend } from "../../../../declaration.js";
+
+type Entity = MerchantBackend.Template.UsingTemplateDetails;
+
+interface Props {
+ id: string;
+ template: MerchantBackend.Template.TemplateDetails;
+ onCreateOrder: (d: Entity) => Promise<void>;
+ onBack?: () => void;
+}
+
+export function UsePage({ id, template, onCreateOrder, onBack }: Props): VNode {
+ const { i18n } = useTranslationContext();
+
+ const [state, setState] = useState<Partial<Entity>>({
+ amount: template.template_contract.amount,
+ summary: template.template_contract.summary,
+ });
+
+ const errors: FormErrors<Entity> = {
+ amount:
+ !template.template_contract.amount && !state.amount
+ ? i18n.str`Amount is required`
+ : undefined,
+ summary:
+ !template.template_contract.summary && !state.summary
+ ? i18n.str`Order summary is required`
+ : undefined,
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const submitForm = () => {
+ if (hasErrors) return Promise.reject();
+ if (template.template_contract.amount) {
+ delete state.amount;
+ }
+ if (template.template_contract.summary) {
+ delete state.summary;
+ }
+ return onCreateOrder(state as any);
+ };
+
+ return (
+ <div>
+ <section class="section">
+ <section class="hero is-hero-bar">
+ <div class="hero-body">
+ <div class="level">
+ <div class="level-left">
+ <div class="level-item">
+ <span class="is-size-4">
+ <i18n.Translate>New order for template</i18n.Translate>:{" "}
+ <b>{id}</b>
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+ </section>
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <FormProvider
+ object={state}
+ valueHandler={setState}
+ errors={errors}
+ >
+ <InputCurrency<Entity>
+ name="amount"
+ label={i18n.str`Amount`}
+ readonly={!!template.template_contract.amount}
+ tooltip={i18n.str`Amount of the order`}
+ />
+ <Input<Entity>
+ name="summary"
+ inputType="multiline"
+ label={i18n.str`Order summary`}
+ readonly={!!template.template_contract.summary}
+ tooltip={i18n.str`Title of the order to be shown to the customer`}
+ />
+ </FormProvider>
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ disabled={hasErrors}
+ data-tooltip={
+ hasErrors
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ onClick={submitForm}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/use/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/use/index.tsx
new file mode 100644
index 000000000..ed1242ef5
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/use/index.tsx
@@ -0,0 +1,101 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import {
+ useTemplateAPI,
+ useTemplateDetails,
+} from "../../../../hooks/templates.js";
+import { Notification } from "../../../../utils/types.js";
+import { UsePage } from "./UsePage.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+
+export type Entity = MerchantBackend.Transfers.TransferInformation;
+interface Props {
+ onBack?: () => void;
+ onOrderCreated: (id: string) => void;
+ onUnauthorized: () => VNode;
+ onNotFound: () => VNode;
+ onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ tid: string;
+}
+
+export default function TemplateUsePage({
+ tid,
+ onOrderCreated,
+ onBack,
+ onLoadError,
+ onNotFound,
+ onUnauthorized,
+}: Props): VNode {
+ const { createOrderFromTemplate } = useTemplateAPI();
+ const result = useTemplateDetails(tid);
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <>
+ <NotificationCard notification={notif} />
+ <UsePage
+ template={result.data}
+ id={tid}
+ onBack={onBack}
+ onCreateOrder={(
+ request: MerchantBackend.Template.UsingTemplateDetails,
+ ) => {
+ return createOrderFromTemplate(tid, request)
+ .then((res) => onOrderCreated(res.data.order_id))
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not create order from template`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/token/DetailPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/token/DetailPage.tsx
new file mode 100644
index 000000000..549e7581f
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/token/DetailPage.tsx
@@ -0,0 +1,183 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../components/exception/AsyncButton.js";
+import { FormProvider } from "../../../components/form/FormProvider.js";
+import { Input } from "../../../components/form/Input.js";
+import { useInstanceContext } from "../../../context/instance.js";
+import { AccessToken } from "../../../declaration.js";
+import { NotificationCard } from "../../../components/menu/index.js";
+
+interface Props {
+ instanceId: string;
+ hasToken: boolean | undefined;
+ onClearToken: (c: AccessToken | undefined) => void;
+ onNewToken: (c: AccessToken | undefined, s: AccessToken) => void;
+ onBack?: () => void;
+}
+
+export function DetailPage({ instanceId, hasToken, onBack, onNewToken, onClearToken }: Props): VNode {
+ type State = { old_token: string; new_token: string; repeat_token: string };
+ const [form, setValue] = useState<Partial<State>>({
+ old_token: "",
+ new_token: "",
+ repeat_token: "",
+ });
+ const { i18n } = useTranslationContext();
+
+ const errors = {
+ old_token: hasToken && !form.old_token
+ ? i18n.str`you need your access token to perform the operation`
+ : undefined,
+ new_token: !form.new_token
+ ? i18n.str`cannot be empty`
+ : form.new_token === form.old_token
+ ? i18n.str`cannot be the same as the old token`
+ : undefined,
+ repeat_token:
+ form.new_token !== form.repeat_token
+ ? i18n.str`is not the same`
+ : undefined,
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const instance = useInstanceContext();
+
+ const text = i18n.str`You are updating the access token from instance with id "${instance.id}"`;
+
+ async function submitForm() {
+ if (hasErrors) return;
+ const 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
new file mode 100644
index 000000000..22365c9e1
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/token/index.tsx
@@ -0,0 +1,106 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+import { ErrorType, HttpError, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { Loading } from "../../../components/exception/loading.js";
+import { AccessToken, MerchantBackend } from "../../../declaration.js";
+import { useInstanceAPI, useInstanceDetails } from "../../../hooks/instance.js";
+import { DetailPage } from "./DetailPage.js";
+import { useInstanceContext } from "../../../context/instance.js";
+import { useState } from "preact/hooks";
+import { NotificationCard } from "../../../components/menu/index.js";
+import { Notification } from "../../../utils/types.js";
+import { useBackendContext } from "../../../context/backend.js";
+
+interface Props {
+ onUnauthorized: () => VNode;
+ onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ onChange: () => void;
+ onNotFound: () => VNode;
+ onCancel: () => void;
+}
+
+export default function Token({
+ onLoadError,
+ onChange,
+ onUnauthorized,
+ onNotFound,
+ onCancel,
+}: Props): VNode {
+ const { i18n } = useTranslationContext();
+
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { clearAccessToken, setNewAccessToken } = useInstanceAPI();
+ const { id } = useInstanceContext();
+ const result = useInstanceDetails()
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ const hasToken = result.data.auth.method === "token"
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+ <DetailPage
+ instanceId={id}
+ onBack={onCancel}
+ hasToken={hasToken}
+ onClearToken={async (currentToken): Promise<void> => {
+ try {
+ await clearAccessToken(currentToken);
+ onChange();
+ } catch (error) {
+ if (error instanceof Error) {
+ setNotif({
+ message: i18n.str`Failed to clear token`,
+ type: "ERROR",
+ description: error.message,
+ });
+ }
+ }
+ }}
+ onNewToken={async (currentToken, newToken): Promise<void> => {
+ try {
+ await setNewAccessToken(currentToken, newToken);
+ onChange();
+ } catch (error) {
+ if (error instanceof Error) {
+ setNotif({
+ message: i18n.str`Failed to set new token`,
+ type: "ERROR",
+ description: error.message,
+ });
+ }
+ }
+ }}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/token/stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/token/stories.tsx
new file mode 100644
index 000000000..5f0f56f2d
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/token/stories.tsx
@@ -0,0 +1,28 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { DetailPage as TestedComponent } from "./DetailPage.js";
+
+export default {
+ title: "Pages/Token",
+ component: TestedComponent,
+};
+
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/transfers/create/Create.stories.tsx
index 586881318..64b67335c 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/Create.stories.tsx
+++ b/packages/auditor-backoffice-ui/src/paths/instance/transfers/create/Create.stories.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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
@@ -15,28 +15,31 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { h, VNode, FunctionalComponent } from 'preact';
+import { h, VNode, FunctionalComponent } from "preact";
import { CreatePage as TestedComponent } from "./CreatePage.js";
-
export default {
- title: 'Pages/Reserve/Create',
+ title: "Pages/Transfer/Create",
component: TestedComponent,
argTypes: {
- onCreate: { action: 'onCreate' },
- onBack: { action: 'onBack' },
+ 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
+function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => <Component {...args} />;
+ r.args = props;
+ return r;
}
export const Example = createExample(TestedComponent, {
+ accounts: ["payto://x-taler-bank/account1", "payto://x-taler-bank/account2"],
});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx
new file mode 100644
index 000000000..13f5f3c12
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx
@@ -0,0 +1,146 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { InputCurrency } from "../../../../components/form/InputCurrency.js";
+import { InputSelector } from "../../../../components/form/InputSelector.js";
+import { useConfigContext } from "../../../../context/config.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import {
+ CROCKFORD_BASE32_REGEX,
+ URL_REGEX,
+} from "../../../../utils/constants.js";
+
+type Entity = MerchantBackend.Transfers.TransferInformation;
+
+interface Props {
+ onCreate: (d: Entity) => Promise<void>;
+ onBack?: () => void;
+ accounts: string[];
+}
+
+export function CreatePage({ accounts, onCreate, onBack }: Props): VNode {
+ const { i18n } = useTranslationContext();
+ const { currency } = useConfigContext();
+
+ const [state, setState] = useState<Partial<Entity>>({
+ wtid: "",
+ // payto_uri: ,
+ // exchange_url: 'http://exchange.taler:8081/',
+ credit_amount: ``,
+ });
+
+ const errors: FormErrors<Entity> = {
+ wtid: !state.wtid
+ ? i18n.str`cannot be empty`
+ : !CROCKFORD_BASE32_REGEX.test(state.wtid)
+ ? i18n.str`check the id, does not look valid`
+ : state.wtid.length !== 52
+ ? i18n.str`should have 52 characters, current ${state.wtid.length}`
+ : undefined,
+ payto_uri: !state.payto_uri ? i18n.str`cannot be empty` : undefined,
+ credit_amount: !state.credit_amount ? i18n.str`cannot be empty` : undefined,
+ exchange_url: !state.exchange_url
+ ? i18n.str`cannot be empty`
+ : !URL_REGEX.test(state.exchange_url)
+ ? i18n.str`URL doesn't have the right format`
+ : undefined,
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const submitForm = () => {
+ if (hasErrors) return Promise.reject();
+ return onCreate(state as any);
+ };
+
+ return (
+ <div>
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <FormProvider
+ object={state}
+ valueHandler={setState}
+ errors={errors}
+ >
+ <InputSelector
+ name="payto_uri"
+ label={i18n.str`Credited bank account`}
+ values={accounts}
+ placeholder={i18n.str`Select one account`}
+ tooltip={i18n.str`Bank account of the merchant where the payment was received`}
+ />
+ <Input<Entity>
+ name="wtid"
+ label={i18n.str`Wire transfer ID`}
+ help=""
+ tooltip={i18n.str`unique identifier of the wire transfer used by the exchange, must be 52 characters long`}
+ />
+ <Input<Entity>
+ name="exchange_url"
+ label={i18n.str`Exchange URL`}
+ tooltip={i18n.str`Base URL of the exchange that made the transfer, should have been in the wire transfer subject`}
+ help="http://exchange.taler:8081/"
+ />
+ <InputCurrency<Entity>
+ name="credit_amount"
+ label={i18n.str`Amount credited`}
+ tooltip={i18n.str`Actual amount that was wired to the merchant's bank account`}
+ />
+ </FormProvider>
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ disabled={hasErrors}
+ data-tooltip={
+ hasErrors
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ onClick={submitForm}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/transfers/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/transfers/create/index.tsx
new file mode 100644
index 000000000..25551a031
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/transfers/create/index.tsx
@@ -0,0 +1,68 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useInstanceDetails } from "../../../../hooks/instance.js";
+import { useTransferAPI } from "../../../../hooks/transfer.js";
+import { Notification } from "../../../../utils/types.js";
+import { CreatePage } from "./CreatePage.js";
+import { useBankAccountDetails, useInstanceBankAccounts } from "../../../../hooks/bank.js";
+
+export type Entity = MerchantBackend.Transfers.TransferInformation;
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+}
+
+export default function CreateTransfer({ onConfirm, onBack }: Props): VNode {
+ const { informTransfer } = useTransferAPI();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+ const instance = useInstanceBankAccounts();
+ const accounts = !instance.ok
+ ? []
+ : instance.data.accounts.map((a) => a.payto_uri);
+
+ return (
+ <>
+ <NotificationCard notification={notif} />
+ <CreatePage
+ onBack={onBack}
+ accounts={accounts}
+ onCreate={(request: MerchantBackend.Transfers.TransferInformation) => {
+ return informTransfer(request)
+ .then(() => onConfirm())
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not inform transfer`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/List.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/List.stories.tsx
new file mode 100644
index 000000000..92b3f9853
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/List.stories.tsx
@@ -0,0 +1,93 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { ListPage as TestedComponent } from "./ListPage.js";
+
+export default {
+ title: "Pages/Transfer/List",
+ component: TestedComponent,
+ argTypes: {
+ onCreate: { action: "onCreate" },
+ onDelete: { action: "onDelete" },
+ onLoadMoreBefore: { action: "onLoadMoreBefore" },
+ onLoadMoreAfter: { action: "onLoadMoreAfter" },
+ onShowAll: { action: "onShowAll" },
+ onShowVerified: { action: "onShowVerified" },
+ onShowUnverified: { action: "onShowUnverified" },
+ onChangePayTo: { action: "onChangePayTo" },
+ },
+};
+
+function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => <Component {...args} />;
+ r.args = props;
+ return r;
+}
+
+export const Example = createExample(TestedComponent, {
+ transfers: [
+ {
+ exchange_url: "http://exchange.url/",
+ credit_amount: "TESTKUDOS:10",
+ payto_uri: "payto//x-taler-bank/bank:8080/account",
+ transfer_serial_id: 123123123,
+ wtid: "!@KJELQKWEJ!L@K#!J@",
+ confirmed: true,
+ execution_time: {
+ t_s: new Date().getTime() / 1000,
+ },
+ verified: false,
+ },
+ {
+ exchange_url: "http://exchange.url/",
+ credit_amount: "TESTKUDOS:10",
+ payto_uri: "payto//x-taler-bank/bank:8080/account",
+ transfer_serial_id: 123123123,
+ wtid: "!@KJELQKWEJ!L@K#!J@",
+ confirmed: true,
+ execution_time: {
+ t_s: new Date().getTime() / 1000,
+ },
+ verified: false,
+ },
+ {
+ exchange_url: "http://exchange.url/",
+ credit_amount: "TESTKUDOS:10",
+ payto_uri: "payto//x-taler-bank/bank:8080/account",
+ transfer_serial_id: 123123123,
+ wtid: "!@KJELQKWEJ!L@K#!J@",
+ confirmed: true,
+ execution_time: {
+ t_s: new Date().getTime() / 1000,
+ },
+ verified: false,
+ },
+ ],
+ accounts: ["payto://x-taler-bank/bank/some_account"],
+});
+export const Empty = createExample(TestedComponent, {
+ transfers: [],
+ accounts: [],
+});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx
new file mode 100644
index 000000000..02b12c4c2
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx
@@ -0,0 +1,134 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { FormProvider } from "../../../../components/form/FormProvider.js";
+import { InputSelector } from "../../../../components/form/InputSelector.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { CardTable } from "./Table.js";
+
+export interface Props {
+ transfers: MerchantBackend.Transfers.TransferDetails[];
+ onLoadMoreBefore?: () => void;
+ onLoadMoreAfter?: () => void;
+ onShowAll: () => void;
+ onShowVerified: () => void;
+ onShowUnverified: () => void;
+ isVerifiedTransfers?: boolean;
+ isNonVerifiedTransfers?: boolean;
+ isAllTransfers?: boolean;
+ accounts: string[];
+ onChangePayTo: (p?: string) => void;
+ payTo?: string;
+ onCreate: () => void;
+ onDelete: () => void;
+}
+
+export function ListPage({
+ payTo,
+ onChangePayTo,
+ transfers,
+ onCreate,
+ onDelete,
+ accounts,
+ onLoadMoreBefore,
+ onLoadMoreAfter,
+ isAllTransfers,
+ isNonVerifiedTransfers,
+ isVerifiedTransfers,
+ onShowAll,
+ onShowUnverified,
+ onShowVerified,
+}: Props): VNode {
+ const form = { payto_uri: payTo };
+
+ const { i18n } = useTranslationContext();
+ return (
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-10">
+ <FormProvider
+ object={form}
+ valueHandler={(updater) => onChangePayTo(updater(form).payto_uri)}
+ >
+ <InputSelector
+ name="payto_uri"
+ label={i18n.str`Account URI`}
+ values={accounts}
+ placeholder={i18n.str`Select one account`}
+ tooltip={i18n.str`filter by account address`}
+ />
+ </FormProvider>
+ </div>
+ <div class="column" />
+ </div>
+ <div class="tabs">
+ <ul>
+ <li class={isAllTransfers ? "is-active" : ""}>
+ <div
+ class="has-tooltip-right"
+ data-tooltip={i18n.str`remove all filters`}
+ >
+ <a onClick={onShowAll}>
+ <i18n.Translate>All</i18n.Translate>
+ </a>
+ </div>
+ </li>
+ <li class={isVerifiedTransfers ? "is-active" : ""}>
+ <div
+ class="has-tooltip-right"
+ data-tooltip={i18n.str`only show wire transfers confirmed by the merchant`}
+ >
+ <a onClick={onShowVerified}>
+ <i18n.Translate>Verified</i18n.Translate>
+ </a>
+ </div>
+ </li>
+ <li class={isNonVerifiedTransfers ? "is-active" : ""}>
+ <div
+ class="has-tooltip-right"
+ data-tooltip={i18n.str`only show wire transfers claimed by the exchange`}
+ >
+ <a onClick={onShowUnverified}>
+ <i18n.Translate>Unverified</i18n.Translate>
+ </a>
+ </div>
+ </li>
+ </ul>
+ </div>
+ <CardTable
+ transfers={transfers.map((o) => ({
+ ...o,
+ id: String(o.transfer_serial_id),
+ }))}
+ accounts={accounts}
+ onCreate={onCreate}
+ onDelete={onDelete}
+ onLoadMoreBefore={onLoadMoreBefore}
+ hasMoreBefore={!onLoadMoreBefore}
+ onLoadMoreAfter={onLoadMoreAfter}
+ hasMoreAfter={!onLoadMoreAfter}
+ />
+ </section>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/Table.tsx b/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/Table.tsx
new file mode 100644
index 000000000..b6b1cf328
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/Table.tsx
@@ -0,0 +1,229 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { format } from "date-fns";
+import { h, VNode } from "preact";
+import { StateUpdater, useState } from "preact/hooks";
+import { MerchantBackend, WithId } from "../../../../declaration.js";
+import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
+
+type Entity = MerchantBackend.Transfers.TransferDetails & WithId;
+
+interface Props {
+ transfers: Entity[];
+ onDelete: (id: Entity) => void;
+ onCreate: () => void;
+ accounts: string[];
+ onLoadMoreBefore?: () => void;
+ hasMoreBefore?: boolean;
+ hasMoreAfter?: boolean;
+ onLoadMoreAfter?: () => void;
+}
+
+export function CardTable({
+ transfers,
+ onCreate,
+ onDelete,
+ onLoadMoreAfter,
+ onLoadMoreBefore,
+ hasMoreAfter,
+ hasMoreBefore,
+}: Props): VNode {
+ const [rowSelection, rowSelectionHandler] = useState<string[]>([]);
+
+ const { i18n } = useTranslationContext();
+
+ return (
+ <div class="card has-table">
+ <header class="card-header">
+ <p class="card-header-title">
+ <span class="icon">
+ <i class="mdi mdi-arrow-left-right" />
+ </span>
+ <i18n.Translate>Transfers</i18n.Translate>
+ </p>
+ <div class="card-header-icon" aria-label="more options">
+ <span
+ class="has-tooltip-left"
+ data-tooltip={i18n.str`add new transfer`}
+ >
+ <button class="button is-info" type="button" onClick={onCreate}>
+ <span class="icon is-small">
+ <i class="mdi mdi-plus mdi-36px" />
+ </span>
+ </button>
+ </span>
+ </div>
+ </header>
+ <div class="card-content">
+ <div class="b-table has-pagination">
+ <div class="table-wrapper has-mobile-cards">
+ {transfers.length > 0 ? (
+ <Table
+ instances={transfers}
+ onDelete={onDelete}
+ rowSelection={rowSelection}
+ rowSelectionHandler={rowSelectionHandler}
+ onLoadMoreAfter={onLoadMoreAfter}
+ onLoadMoreBefore={onLoadMoreBefore}
+ hasMoreAfter={hasMoreAfter}
+ hasMoreBefore={hasMoreBefore}
+ />
+ ) : (
+ <EmptyTable />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+}
+interface TableProps {
+ rowSelection: string[];
+ instances: Entity[];
+ onDelete: (id: Entity) => void;
+ rowSelectionHandler: StateUpdater<string[]>;
+ onLoadMoreBefore?: () => void;
+ hasMoreBefore?: boolean;
+ hasMoreAfter?: boolean;
+ onLoadMoreAfter?: () => void;
+}
+
+function toggleSelected<T>(id: T): (prev: T[]) => T[] {
+ return (prev: T[]): T[] =>
+ prev.indexOf(id) == -1 ? [...prev, id] : prev.filter((e) => e != id);
+}
+
+function Table({
+ instances,
+ onLoadMoreAfter,
+ onDelete,
+ onLoadMoreBefore,
+ hasMoreAfter,
+ hasMoreBefore,
+}: TableProps): VNode {
+ const { i18n } = useTranslationContext();
+ const [settings] = useSettings();
+ return (
+ <div class="table-container">
+ {hasMoreBefore && (
+ <button
+ class="button is-fullwidth"
+ data-tooltip={i18n.str`load more transfers before the first one`}
+ onClick={onLoadMoreBefore}
+ >
+ <i18n.Translate>load newer transfers</i18n.Translate>
+ </button>
+ )}
+ <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>ID</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Credit</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Address</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Exchange URL</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Confirmed</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Verified</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Executed at</i18n.Translate>
+ </th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ {instances.map((i) => {
+ return (
+ <tr key={i.id}>
+ <td>{i.id}</td>
+ <td>{i.credit_amount}</td>
+ <td>{i.payto_uri}</td>
+ <td>{i.exchange_url}</td>
+ <td>{i.confirmed ? i18n.str`yes` : i18n.str`no`}</td>
+ <td>{i.verified ? i18n.str`yes` : i18n.str`no`}</td>
+ <td>
+ {i.execution_time
+ ? i.execution_time.t_s == "never"
+ ? i18n.str`never`
+ : format(
+ i.execution_time.t_s * 1000,
+ datetimeFormatForSettings(settings),
+ )
+ : i18n.str`unknown`}
+ </td>
+ <td>
+ {i.verified === undefined ? (
+ <button
+ class="button is-danger is-small has-tooltip-left"
+ data-tooltip={i18n.str`delete selected transfer from the database`}
+ onClick={() => onDelete(i)}
+ >
+ Delete
+ </button>
+ ) : undefined}
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ {hasMoreAfter && (
+ <button
+ class="button is-fullwidth"
+ data-tooltip={i18n.str`load more transfer after the last one`}
+ onClick={onLoadMoreAfter}
+ >
+ <i18n.Translate>load older transfers</i18n.Translate>
+ </button>
+ )}
+ </div>
+ );
+}
+
+function EmptyTable(): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="content has-text-grey has-text-centered">
+ <p>
+ <span class="icon is-large">
+ <i class="mdi mdi-emoticon-sad mdi-48px" />
+ </span>
+ </p>
+ <p>
+ <i18n.Translate>
+ There is no transfer yet, add more pressing the + sign
+ </i18n.Translate>
+ </p>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/index.tsx
new file mode 100644
index 000000000..0fdbb9bc3
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/index.tsx
@@ -0,0 +1,118 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { ErrorType, HttpError } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useEffect, useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useInstanceDetails } from "../../../../hooks/instance.js";
+import { useInstanceTransfers } from "../../../../hooks/transfer.js";
+import { ListPage } from "./ListPage.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+import { useInstanceBankAccounts } from "../../../../hooks/bank.js";
+
+interface Props {
+ onUnauthorized: () => VNode;
+ onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ onNotFound: () => VNode;
+ onCreate: () => void;
+}
+interface Form {
+ verified?: "yes" | "no";
+ payto_uri?: string;
+}
+
+export default function ListTransfer({
+ onUnauthorized,
+ onLoadError,
+ onCreate,
+ onNotFound,
+}: Props): VNode {
+ const setFilter = (s?: "yes" | "no") => setForm({ ...form, verified: s });
+
+ const [position, setPosition] = useState<string | undefined>(undefined);
+
+ const instance = useInstanceBankAccounts();
+ const accounts = !instance.ok
+ ? []
+ : instance.data.accounts.map((a) => a.payto_uri);
+ const [form, setForm] = useState<Form>({ payto_uri: "" });
+
+ const shoulUseDefaultAccount = accounts.length === 1
+ useEffect(() => {
+ if (shoulUseDefaultAccount) {
+ setForm({...form, payto_uri: accounts[0]})
+ }
+ }, [shoulUseDefaultAccount])
+
+ const isVerifiedTransfers = form.verified === "yes";
+ const isNonVerifiedTransfers = form.verified === "no";
+ const isAllTransfers = form.verified === undefined;
+
+ const result = useInstanceTransfers(
+ {
+ position,
+ payto_uri: form.payto_uri === "" ? undefined : form.payto_uri,
+ verified: form.verified,
+ },
+ (id) => setPosition(id),
+ );
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <ListPage
+ accounts={accounts}
+ transfers={result.data.transfers}
+ onLoadMoreBefore={
+ result.isReachingStart ? result.loadMorePrev : undefined
+ }
+ onLoadMoreAfter={result.isReachingEnd ? result.loadMore : undefined}
+ onCreate={onCreate}
+ onDelete={() => {
+ null;
+ }}
+ // position={position} setPosition={setPosition}
+ onShowAll={() => setFilter(undefined)}
+ onShowUnverified={() => setFilter("no")}
+ onShowVerified={() => setFilter("yes")}
+ isAllTransfers={isAllTransfers}
+ isVerifiedTransfers={isVerifiedTransfers}
+ isNonVerifiedTransfers={isNonVerifiedTransfers}
+ payTo={form.payto_uri}
+ onChangePayTo={(p) => setForm((v) => ({ ...v, payto_uri: p }))}
+ />
+ );
+}
diff --git a/packages/merchant-backoffice-ui/tests/declarations.d.ts b/packages/auditor-backoffice-ui/src/paths/instance/transfers/update/index.tsx
index 61a53dc69..84cc95e72 100644
--- a/packages/merchant-backoffice-ui/tests/declarations.d.ts
+++ b/packages/auditor-backoffice-ui/src/paths/instance/transfers/update/index.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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
@@ -19,10 +19,8 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-declare global {
- namespace jest {
- interface Matchers<R> {
- toBeWithinRange(a: number, b: number): R;
- }
- }
+import { h, VNode } from "preact";
+
+export default function UpdateTransfer(): VNode {
+ return <div>order transfer page</div>;
}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/details/Details.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/update/Update.stories.tsx
index 068dd55c0..817a7025c 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/details/Details.stories.tsx
+++ b/packages/auditor-backoffice-ui/src/paths/instance/update/Update.stories.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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
@@ -20,10 +20,10 @@
*/
import { h, VNode, FunctionalComponent } from "preact";
-import { DetailPage as TestedComponent } from "./DetailPage.js";
+import { UpdatePage as TestedComponent } from "./UpdatePage.js";
export default {
- title: "Pages/Instance/Detail",
+ title: "Pages/Instance/Update",
component: TestedComponent,
argTypes: {
onUpdate: { action: "onUpdate" },
@@ -33,7 +33,7 @@ export default {
function createExample<Props>(
Component: FunctionalComponent<Props>,
- props: Partial<Props>
+ props: Partial<Props>,
) {
const r = (args: any) => <Component {...args} />;
r.args = props;
@@ -42,19 +42,17 @@ function createExample<Props>(
export const Example = createExample(TestedComponent, {
selected: {
- accounts: [],
name: "name",
auth: { method: "external" },
address: {},
+ user_type: "business",
+ use_stefan: true,
jurisdiction: {},
- default_max_deposit_fee: "TESTKUDOS:2",
- default_max_wire_fee: "TESTKUDOS:1",
default_pay_delay: {
- d_us: 1000000,
+ d_us: 1000 * 1000, //one second
},
- default_wire_fee_amortization: 1,
default_wire_transfer_delay: {
- d_us: 100000,
+ d_us: 1000 * 1000, //one second
},
merchant_pub: "ASDWQEKASJDKSADJ",
},
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/update/UpdatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/update/UpdatePage.tsx
new file mode 100644
index 000000000..a27a0cb06
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/update/UpdatePage.tsx
@@ -0,0 +1,176 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../components/exception/AsyncButton.js";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../components/form/FormProvider.js";
+import { DefaultInstanceFormFields } from "../../../components/instance/DefaultInstanceFormFields.js";
+import { useInstanceContext } from "../../../context/instance.js";
+import { MerchantBackend } from "../../../declaration.js";
+import { undefinedIfEmpty } from "../../../utils/table.js";
+import { Duration } from "@gnu-taler/taler-util";
+
+export type Entity = Omit<Omit<MerchantBackend.Instances.InstanceReconfigurationMessage, "default_pay_delay">, "default_wire_transfer_delay"> & {
+ default_pay_delay: Duration,
+ default_wire_transfer_delay: Duration,
+};
+
+//MerchantBackend.Instances.InstanceAuthConfigurationMessage
+interface Props {
+ onUpdate: (d: MerchantBackend.Instances.InstanceReconfigurationMessage) => void;
+ selected: MerchantBackend.Instances.QueryInstancesResponse;
+ isLoading: boolean;
+ onBack: () => void;
+}
+
+function convert(
+ from: MerchantBackend.Instances.QueryInstancesResponse,
+): Entity {
+ const { default_pay_delay, default_wire_transfer_delay, ...rest } = from;
+
+ const defaults = {
+ use_stefan: false,
+ default_pay_delay: Duration.fromTalerProtocolDuration(default_pay_delay),
+ default_wire_transfer_delay: Duration.fromTalerProtocolDuration(default_wire_transfer_delay),
+ };
+ return { ...defaults, ...rest };
+}
+
+export function UpdatePage({
+ onUpdate,
+ selected,
+ onBack,
+}: Props): VNode {
+ const { id } = useInstanceContext();
+
+ const [value, valueHandler] = useState<Partial<Entity>>(convert(selected));
+
+ const { i18n } = useTranslationContext();
+
+ const errors: FormErrors<Entity> = {
+ name: !value.name ? i18n.str`required` : undefined,
+ user_type: !value.user_type
+ ? i18n.str`required`
+ : value.user_type !== "business" && value.user_type !== "individual"
+ ? i18n.str`should be business or individual`
+ : undefined,
+ default_pay_delay: !value.default_pay_delay
+ ? i18n.str`required`
+ : !!value.default_wire_transfer_delay &&
+ value.default_wire_transfer_delay.d_ms !== "forever" &&
+ value.default_pay_delay.d_ms !== "forever" &&
+ value.default_pay_delay.d_ms > value.default_wire_transfer_delay.d_ms ?
+ i18n.str`pay delay can't be greater than wire transfer delay` : undefined,
+ default_wire_transfer_delay: !value.default_wire_transfer_delay
+ ? i18n.str`required`
+ : undefined,
+ address: undefinedIfEmpty({
+ address_lines:
+ value.address?.address_lines && value.address?.address_lines.length > 7
+ ? i18n.str`max 7 lines`
+ : undefined,
+ }),
+ jurisdiction: undefinedIfEmpty({
+ address_lines:
+ value.address?.address_lines && value.address?.address_lines.length > 7
+ ? i18n.str`max 7 lines`
+ : undefined,
+ }),
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const submit = async (): Promise<void> => {
+ const { default_pay_delay, default_wire_transfer_delay, ...rest } = value as Required<Entity>;
+ const result: MerchantBackend.Instances.InstanceReconfigurationMessage = {
+ default_pay_delay: Duration.toTalerProtocolDuration(default_pay_delay),
+ default_wire_transfer_delay: Duration.toTalerProtocolDuration(default_wire_transfer_delay),
+ ...rest,
+ }
+ await onUpdate(result);
+ };
+ // const [active, setActive] = useState(false);
+
+ return (
+ <div>
+ <section class="section">
+ <section class="hero is-hero-bar">
+ <div class="hero-body">
+ <div class="level">
+ <div class="level-left">
+ <div class="level-item">
+ <span class="is-size-4">
+ <i18n.Translate>Instance id</i18n.Translate>: <b>{id}</b>
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+
+ <hr />
+
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <FormProvider<Entity>
+ errors={errors}
+ object={value}
+ valueHandler={valueHandler}
+ >
+ <DefaultInstanceFormFields showId={false} />
+ </FormProvider>
+
+ <div class="buttons is-right mt-4">
+ <button
+ class="button"
+ onClick={onBack}
+ data-tooltip="cancel operation"
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+
+ <AsyncButton
+ onClick={submit}
+ data-tooltip={
+ hasErrors
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ disabled={hasErrors}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/update/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/update/index.tsx
new file mode 100644
index 000000000..e44cf5c0f
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/update/index.tsx
@@ -0,0 +1,118 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+import {
+ ErrorType,
+ HttpError,
+ HttpResponse,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../components/exception/loading.js";
+import { NotificationCard } from "../../../components/menu/index.js";
+import { useInstanceContext } from "../../../context/instance.js";
+import { AccessToken, MerchantBackend } from "../../../declaration.js";
+import {
+ useInstanceAPI,
+ useInstanceDetails,
+ useManagedInstanceDetails,
+ useManagementAPI,
+} from "../../../hooks/instance.js";
+import { Notification } from "../../../utils/types.js";
+import { UpdatePage } from "./UpdatePage.js";
+
+export interface Props {
+ onBack: () => void;
+ onConfirm: () => void;
+
+ onUnauthorized: () => VNode;
+ onNotFound: () => VNode;
+ onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ onUpdateError: (e: HttpError<MerchantBackend.ErrorDetail>) => void;
+}
+
+export default function Update(props: Props): VNode {
+ const { updateInstance } = useInstanceAPI();
+ const result = useInstanceDetails();
+ return CommonUpdate(props, result, updateInstance, );
+}
+
+export function AdminUpdate(props: Props & { instanceId: string }): VNode {
+ const { updateInstance } = useManagementAPI(
+ props.instanceId,
+ );
+ const result = useManagedInstanceDetails(props.instanceId);
+ return CommonUpdate(props, result, updateInstance, );
+}
+
+function CommonUpdate(
+ {
+ onBack,
+ onConfirm,
+ onLoadError,
+ onNotFound,
+ onUpdateError,
+ onUnauthorized,
+ }: Props,
+ result: HttpResponse<
+ MerchantBackend.Instances.QueryInstancesResponse,
+ MerchantBackend.ErrorDetail
+ >,
+ updateInstance: any,
+): VNode {
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+ <UpdatePage
+ onBack={onBack}
+ isLoading={false}
+ selected={result.data}
+ onUpdate={(
+ d: MerchantBackend.Instances.InstanceReconfigurationMessage,
+ ): Promise<void> => {
+ return updateInstance(d)
+ .then(onConfirm)
+ .catch((error: Error) =>
+ setNotif({
+ message: i18n.str`Failed to create instance`,
+ type: "ERROR",
+ description: error.message,
+ }),
+ );
+ }}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/Create.stories.tsx
new file mode 100644
index 000000000..4857ede97
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/Create.stories.tsx
@@ -0,0 +1,28 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { CreatePage as TestedComponent } from "./CreatePage.js";
+
+export default {
+ title: "Pages/Webhooks/Create",
+ component: TestedComponent,
+};
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx
new file mode 100644
index 000000000..bfa2a883e
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx
@@ -0,0 +1,183 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { InputCurrency } from "../../../../components/form/InputCurrency.js";
+import { InputDuration } from "../../../../components/form/InputDuration.js";
+import { InputNumber } from "../../../../components/form/InputNumber.js";
+import { useBackendContext } from "../../../../context/backend.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { InputSelector } from "../../../../components/form/InputSelector.js";
+
+type Entity = MerchantBackend.Webhooks.WebhookAddDetails;
+
+interface Props {
+ onCreate: (d: Entity) => Promise<void>;
+ onBack?: () => void;
+}
+
+const validMethod = ["GET", "POST", "PUT", "PATCH", "HEAD"];
+
+export function CreatePage({ onCreate, onBack }: Props): VNode {
+ const { i18n } = useTranslationContext();
+
+ const [state, setState] = useState<Partial<Entity>>({});
+
+ const errors: FormErrors<Entity> = {
+ webhook_id: !state.webhook_id ? i18n.str`required` : undefined,
+ event_type: !state.event_type ? i18n.str`required`
+ : state.event_type !== "pay" && state.event_type !== "refund" ? i18n.str`it should be "pay" or "refund"`
+ : undefined,
+ http_method: !state.http_method
+ ? i18n.str`required`
+ : !validMethod.includes(state.http_method)
+ ? i18n.str`should be one of '${validMethod.join(", ")}'`
+ : undefined,
+ url: !state.url ? i18n.str`required` : undefined,
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const submitForm = () => {
+ if (hasErrors) return Promise.reject();
+ return onCreate(state as any);
+ };
+
+ return (
+ <div>
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <FormProvider
+ object={state}
+ valueHandler={setState}
+ errors={errors}
+ >
+ <Input<Entity>
+ name="webhook_id"
+ label={i18n.str`ID`}
+ tooltip={i18n.str`Webhook ID to use`}
+ />
+ <InputSelector
+ name="event_type"
+ label={i18n.str`Event`}
+ values={[
+ i18n.str`Choose one...`,
+ i18n.str`pay`,
+ i18n.str`refund`,
+ ]}
+ tooltip={i18n.str`The event of the webhook: why the webhook is used`}
+ />
+ <InputSelector
+ name="http_method"
+ label={i18n.str`Method`}
+ values={[
+ i18n.str`Choose one...`,
+ i18n.str`GET`,
+ i18n.str`POST`,
+ i18n.str`PUT`,
+ i18n.str`PATCH`,
+ i18n.str`HEAD`,
+ ]}
+ tooltip={i18n.str`Method used by the webhook`}
+ />
+
+ <Input<Entity>
+ name="url"
+ label={i18n.str`URL`}
+ tooltip={i18n.str`URL of the webhook where the customer will be redirected`}
+ />
+
+ <p>
+ The text below support <a target="_blank" rel="noreferrer" href="https://mustache.github.io/mustache.5.html">mustache</a> template engine. Any string
+ between <pre style={{ display: "inline", padding: 0 }}>&#123;&#123;</pre> and <pre style={{ display: "inline", padding: 0 }}>&#125;&#125;</pre> will
+ be replaced with replaced with the value of the corresponding variable.
+ </p>
+ <p>
+ For example <pre style={{ display: "inline", padding: 0 }}>&#123;&#123;contract_terms.amount&#125;&#125;</pre> will be replaced
+ with the the order's price
+ </p>
+ <p>
+ The short list of variables are:
+ </p>
+ <div class="menu">
+
+ <ul class="menu-list" style={{ listStyleType: "disc", marginLeft: 20 }}>
+ <li><b>contract_terms.summary:</b> order's description </li>
+ <li><b>contract_terms.amount:</b> order's price </li>
+ <li><b>order_id:</b> order's unique identification </li>
+ {state.event_type === "refund" && <Fragment>
+ <li><b>refund_amout:</b> the amount that was being refunded</li>
+ <li><b>reason:</b> the reason entered by the merchant staff for granting the refund</li>
+ <li><b>timestamp:</b> time of the refund in nanoseconds since 1970</li>
+ </Fragment>}
+ </ul>
+ </div>
+ {/* <Input<Entity>
+ name="header_template"
+ label={i18n.str`Http header`}
+ inputType="multiline"
+ tooltip={i18n.str`Header template of the webhook`}
+ /> */}
+ <Input<Entity>
+ name="body_template"
+ inputType="multiline"
+ label={i18n.str`Http body`}
+ tooltip={i18n.str`Body template by the webhook`}
+ />
+ </FormProvider>
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ disabled={hasErrors}
+ data-tooltip={
+ hasErrors
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ onClick={submitForm}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/index.tsx
new file mode 100644
index 000000000..924e6d9b8
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/index.tsx
@@ -0,0 +1,61 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useWebhookAPI } from "../../../../hooks/webhooks.js";
+import { Notification } from "../../../../utils/types.js";
+import { CreatePage } from "./CreatePage.js";
+
+export type Entity = MerchantBackend.Webhooks.WebhookAddDetails;
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+}
+
+export default function CreateWebhook({ onConfirm, onBack }: Props): VNode {
+ const { createWebhook } = useWebhookAPI();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+
+ return (
+ <>
+ <NotificationCard notification={notif} />
+ <CreatePage
+ onBack={onBack}
+ onCreate={(request: MerchantBackend.Webhooks.WebhookAddDetails) => {
+ return createWebhook(request)
+ .then(() => onConfirm())
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not inform template`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/List.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/List.stories.tsx
new file mode 100644
index 000000000..702e9ba4a
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/List.stories.tsx
@@ -0,0 +1,28 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { FunctionalComponent, h } from "preact";
+import { ListPage as TestedComponent } from "./ListPage.js";
+
+export default {
+ title: "Pages/Templates/List",
+ component: TestedComponent,
+};
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/ListPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/ListPage.tsx
new file mode 100644
index 000000000..87e221e3c
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/ListPage.tsx
@@ -0,0 +1,64 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode } from "preact";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { CardTable } from "./Table.js";
+
+export interface Props {
+ webhooks: MerchantBackend.Webhooks.WebhookEntry[];
+ onLoadMoreBefore?: () => void;
+ onLoadMoreAfter?: () => void;
+ onCreate: () => void;
+ onDelete: (e: MerchantBackend.Webhooks.WebhookEntry) => void;
+ onSelect: (e: MerchantBackend.Webhooks.WebhookEntry) => void;
+}
+
+export function ListPage({
+ webhooks,
+ onCreate,
+ onDelete,
+ onSelect,
+ onLoadMoreBefore,
+ onLoadMoreAfter,
+}: Props): VNode {
+ const form = { payto_uri: "" };
+
+ const { i18n } = useTranslationContext();
+ return (
+ <section class="section is-main-section">
+ <CardTable
+ webhooks={webhooks.map((o) => ({
+ ...o,
+ id: String(o.webhook_id),
+ }))}
+ onCreate={onCreate}
+ onDelete={onDelete}
+ onSelect={onSelect}
+ onLoadMoreBefore={onLoadMoreBefore}
+ hasMoreBefore={!onLoadMoreBefore}
+ onLoadMoreAfter={onLoadMoreAfter}
+ hasMoreAfter={!onLoadMoreAfter}
+ />
+ </section>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx
new file mode 100644
index 000000000..42a179d2c
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx
@@ -0,0 +1,218 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { StateUpdater, useState } from "preact/hooks";
+import { MerchantBackend } from "../../../../declaration.js";
+
+type Entity = MerchantBackend.Webhooks.WebhookEntry;
+
+interface Props {
+ webhooks: Entity[];
+ onDelete: (e: Entity) => void;
+ onSelect: (e: Entity) => void;
+ onCreate: () => void;
+ onLoadMoreBefore?: () => void;
+ hasMoreBefore?: boolean;
+ hasMoreAfter?: boolean;
+ onLoadMoreAfter?: () => void;
+}
+
+export function CardTable({
+ webhooks,
+ onCreate,
+ onDelete,
+ onSelect,
+ onLoadMoreAfter,
+ onLoadMoreBefore,
+ hasMoreAfter,
+ hasMoreBefore,
+}: Props): VNode {
+ const [rowSelection, rowSelectionHandler] = useState<string[]>([]);
+
+ const { i18n } = useTranslationContext();
+
+ return (
+ <div class="card has-table">
+ <header class="card-header">
+ <p class="card-header-title">
+ <span class="icon">
+ <i class="mdi mdi-newspaper" />
+ </span>
+ <i18n.Translate>Webhooks</i18n.Translate>
+ </p>
+ <div class="card-header-icon" aria-label="more options">
+ <span
+ class="has-tooltip-left"
+ data-tooltip={i18n.str`add new webhooks`}
+ >
+ <button class="button is-info" type="button" onClick={onCreate}>
+ <span class="icon is-small">
+ <i class="mdi mdi-plus mdi-36px" />
+ </span>
+ </button>
+ </span>
+ </div>
+ </header>
+ <div class="card-content">
+ <div class="b-table has-pagination">
+ <div class="table-wrapper has-mobile-cards">
+ {webhooks.length > 0 ? (
+ <Table
+ instances={webhooks}
+ onDelete={onDelete}
+ onSelect={onSelect}
+ rowSelection={rowSelection}
+ rowSelectionHandler={rowSelectionHandler}
+ onLoadMoreAfter={onLoadMoreAfter}
+ onLoadMoreBefore={onLoadMoreBefore}
+ hasMoreAfter={hasMoreAfter}
+ hasMoreBefore={hasMoreBefore}
+ />
+ ) : (
+ <EmptyTable />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+}
+interface TableProps {
+ rowSelection: string[];
+ instances: Entity[];
+ onDelete: (e: Entity) => void;
+ onSelect: (e: Entity) => void;
+ rowSelectionHandler: StateUpdater<string[]>;
+ onLoadMoreBefore?: () => void;
+ hasMoreBefore?: boolean;
+ hasMoreAfter?: boolean;
+ onLoadMoreAfter?: () => void;
+}
+
+function toggleSelected<T>(id: T): (prev: T[]) => T[] {
+ return (prev: T[]): T[] =>
+ prev.indexOf(id) == -1 ? [...prev, id] : prev.filter((e) => e != id);
+}
+
+function Table({
+ instances,
+ onLoadMoreAfter,
+ onDelete,
+ onSelect,
+ onLoadMoreBefore,
+ hasMoreAfter,
+ hasMoreBefore,
+}: TableProps): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="table-container">
+ {hasMoreBefore && (
+ <button
+ class="button is-fullwidth"
+ data-tooltip={i18n.str`load more webhooks before the first one`}
+ onClick={onLoadMoreBefore}
+ >
+ <i18n.Translate>load newer webhooks</i18n.Translate>
+ </button>
+ )}
+ <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>ID</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Event type</i18n.Translate>
+ </th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ {instances.map((i) => {
+ return (
+ <tr key={i.webhook_id}>
+ <td
+ onClick={(): void => onSelect(i)}
+ style={{ cursor: "pointer" }}
+ >
+ {i.webhook_id}
+ </td>
+ <td
+ onClick={(): void => onSelect(i)}
+ style={{ cursor: "pointer" }}
+ >
+ {i.event_type}
+ </td>
+ <td class="is-actions-cell right-sticky">
+ <div class="buttons is-right">
+ <button
+ class="button is-danger is-small has-tooltip-left"
+ data-tooltip={i18n.str`delete selected webhook from the database`}
+ onClick={() => onDelete(i)}
+ >
+ Delete
+ </button>
+ {/* <button
+ class="button is-info is-small has-tooltip-left"
+ data-tooltip={i18n.str`test webhook`}
+ onClick={() => onNewOrder(i)}
+ >
+ Test
+ </button> */}
+ </div>
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ {hasMoreAfter && (
+ <button
+ class="button is-fullwidth"
+ data-tooltip={i18n.str`load more webhooks after the last one`}
+ onClick={onLoadMoreAfter}
+ >
+ <i18n.Translate>load older webhooks</i18n.Translate>
+ </button>
+ )}
+ </div>
+ );
+}
+
+function EmptyTable(): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="content has-text-grey has-text-centered">
+ <p>
+ <span class="icon is-large">
+ <i class="mdi mdi-emoticon-sad mdi-48px" />
+ </span>
+ </p>
+ <p>
+ <i18n.Translate>
+ There is no webhooks yet, add more pressing the + sign
+ </i18n.Translate>
+ </p>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/index.tsx
new file mode 100644
index 000000000..a6f6f1511
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/index.tsx
@@ -0,0 +1,109 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import {
+ useInstanceWebhooks,
+ useWebhookAPI,
+} from "../../../../hooks/webhooks.js";
+import { Notification } from "../../../../utils/types.js";
+import { ListPage } from "./ListPage.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+
+interface Props {
+ onUnauthorized: () => VNode;
+ onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ onNotFound: () => VNode;
+ onCreate: () => void;
+ onSelect: (id: string) => void;
+}
+
+export default function ListWebhooks({
+ onUnauthorized,
+ onLoadError,
+ onCreate,
+ onSelect,
+ onNotFound,
+}: Props): VNode {
+ const [position, setPosition] = useState<string | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { deleteWebhook } = useWebhookAPI();
+ const result = useInstanceWebhooks({ position }, (id) => setPosition(id));
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+
+ <ListPage
+ webhooks={result.data.webhooks}
+ onLoadMoreBefore={
+ result.isReachingStart ? result.loadMorePrev : undefined
+ }
+ onLoadMoreAfter={result.isReachingEnd ? result.loadMore : undefined}
+ onCreate={onCreate}
+ onSelect={(e) => {
+ onSelect(e.webhook_id);
+ }}
+ onDelete={(e: MerchantBackend.Webhooks.WebhookEntry) =>
+ deleteWebhook(e.webhook_id)
+ .then(() =>
+ setNotif({
+ message: i18n.str`webhook delete successfully`,
+ type: "SUCCESS",
+ }),
+ )
+ .catch((error) =>
+ setNotif({
+ message: i18n.str`could not delete the webhook`,
+ type: "ERROR",
+ description: error.message,
+ }),
+ )
+ }
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/Update.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/Update.stories.tsx
new file mode 100644
index 000000000..8d07cb31f
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/Update.stories.tsx
@@ -0,0 +1,32 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { UpdatePage as TestedComponent } from "./UpdatePage.js";
+
+export default {
+ title: "Pages/Templates/Update",
+ component: TestedComponent,
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx
new file mode 100644
index 000000000..76a23b6e5
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx
@@ -0,0 +1,146 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { useBackendContext } from "../../../../context/backend.js";
+import { MerchantBackend, WithId } from "../../../../declaration.js";
+
+type Entity = MerchantBackend.Webhooks.WebhookPatchDetails & WithId;
+
+interface Props {
+ onUpdate: (d: Entity) => Promise<void>;
+ onBack?: () => void;
+ webhook: Entity;
+}
+const validMethod = ["GET", "POST", "PUT", "PATCH", "HEAD"];
+
+export function UpdatePage({ webhook, onUpdate, onBack }: Props): VNode {
+ const { i18n } = useTranslationContext();
+
+ const [state, setState] = useState<Partial<Entity>>(webhook);
+
+ const errors: FormErrors<Entity> = {
+ event_type: !state.event_type ? i18n.str`required` : undefined,
+ http_method: !state.http_method
+ ? i18n.str`required`
+ : !validMethod.includes(state.http_method)
+ ? i18n.str`should be one of '${validMethod.join(", ")}'`
+ : undefined,
+ url: !state.url ? i18n.str`required` : undefined,
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const submitForm = () => {
+ if (hasErrors) return Promise.reject();
+ return onUpdate(state as any);
+ };
+
+ return (
+ <div>
+ <section class="section">
+ <section class="hero is-hero-bar">
+ <div class="hero-body">
+ <div class="level">
+ <div class="level-left">
+ <div class="level-item">
+ <span class="is-size-4">
+ Webhook: <b>{webhook.id}</b>
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+ <hr />
+
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column is-four-fifths">
+ <FormProvider
+ object={state}
+ valueHandler={setState}
+ errors={errors}
+ >
+ <Input<Entity>
+ name="event_type"
+ label={i18n.str`Event`}
+ tooltip={i18n.str`The event of the webhook: why the webhook is used`}
+ />
+ <Input<Entity>
+ name="http_method"
+ label={i18n.str`Method`}
+ tooltip={i18n.str`Method used by the webhook`}
+ />
+ <Input<Entity>
+ name="url"
+ label={i18n.str`URL`}
+ tooltip={i18n.str`URL of the webhook where the customer will be redirected`}
+ />
+ <Input<Entity>
+ name="header_template"
+ label={i18n.str`Header`}
+ inputType="multiline"
+ tooltip={i18n.str`Header template of the webhook`}
+ />
+ <Input<Entity>
+ name="body_template"
+ inputType="multiline"
+ label={i18n.str`Body`}
+ tooltip={i18n.str`Body template by the webhook`}
+ />
+ </FormProvider>
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ disabled={hasErrors}
+ data-tooltip={
+ hasErrors
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ onClick={submitForm}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ </div>
+ </section>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/index.tsx
new file mode 100644
index 000000000..3f723ed87
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/index.tsx
@@ -0,0 +1,99 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend, WithId } from "../../../../declaration.js";
+import {
+ useWebhookAPI,
+ useWebhookDetails,
+} from "../../../../hooks/webhooks.js";
+import { Notification } from "../../../../utils/types.js";
+import { UpdatePage } from "./UpdatePage.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+
+export type Entity = MerchantBackend.Webhooks.WebhookPatchDetails & WithId;
+
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+ onUnauthorized: () => VNode;
+ onNotFound: () => VNode;
+ onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ tid: string;
+}
+export default function UpdateWebhook({
+ tid,
+ onConfirm,
+ onBack,
+ onUnauthorized,
+ onNotFound,
+ onLoadError,
+}: Props): VNode {
+ const { updateWebhook } = useWebhookAPI();
+ const result = useWebhookDetails(tid);
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+
+ const { i18n } = useTranslationContext();
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+ <UpdatePage
+ webhook={{ ...result.data, id: tid }}
+ onBack={onBack}
+ onUpdate={(data) => {
+ return updateWebhook(tid, data)
+ .then(onConfirm)
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not update template`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/login/index.tsx b/packages/auditor-backoffice-ui/src/paths/login/index.tsx
new file mode 100644
index 000000000..1c98b7c9b
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/login/index.tsx
@@ -0,0 +1,202 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { ComponentChildren, h, VNode } from "preact";
+import { useCallback, useEffect, useState } from "preact/hooks";
+import { useBackendContext } from "../../context/backend.js";
+import { useInstanceContext } from "../../context/instance.js";
+import { AccessToken, LoginToken } from "../../declaration.js";
+import { useCredentialsChecker } from "../../hooks/backend.js";
+
+interface Props {
+ onConfirm: (token: LoginToken | undefined) => void;
+}
+
+function normalizeToken(r: string): AccessToken {
+ return `secret-token:${r}` as AccessToken;
+}
+
+export function LoginPage({ onConfirm }: Props): VNode {
+ const { url: backendURL } = useBackendContext();
+ const { admin, id } = useInstanceContext();
+ const { requestNewLoginToken } = useCredentialsChecker();
+ const [token, setToken] = useState("");
+
+ const { i18n } = useTranslationContext();
+
+
+ const doLogin = useCallback(async function doLoginImpl() {
+ const secretToken = normalizeToken(token);
+ const baseUrl = id === undefined ? backendURL : `${backendURL}/instances/${id}`
+ const result = await requestNewLoginToken(baseUrl, secretToken);
+ if (result.valid) {
+ const { token, expiration } = result
+ onConfirm({ token, expiration });
+ } else {
+ onConfirm(undefined);
+ }
+ }, [id, token])
+
+ if (admin && id !== "default") {
+ //admin trying to access another instance
+ return (<div class="columns is-centered" style={{ margin: "auto" }}>
+ <div class="column is-two-thirds ">
+ <div class="modal-card" style={{ width: "100%", margin: 0 }}>
+ <header
+ class="modal-card-head"
+ style={{ border: "1px solid", borderBottom: 0 }}
+ >
+ <p class="modal-card-title">{i18n.str`Login required`}</p>
+ </header>
+ <section
+ class="modal-card-body"
+ style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }}
+ >
+ <p>
+ <i18n.Translate>Need the access token for the instance.</i18n.Translate>
+ </p>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ <i18n.Translate>Access Token</i18n.Translate>
+ </label>
+ </div>
+ <div class="field-body">
+ <div class="field">
+ <p class="control is-expanded">
+ <input
+ class="input"
+ type="password"
+ placeholder={"current access token"}
+ name="token"
+ onKeyPress={(e) =>
+ e.keyCode === 13
+ ? doLogin()
+ : null
+ }
+ value={token}
+ onInput={(e): void => setToken(e?.currentTarget.value)}
+ />
+ </p>
+ </div>
+ </div>
+ </div>
+ </section>
+ <footer
+ class="modal-card-foot "
+ style={{
+ justifyContent: "flex-end",
+ border: "1px solid",
+ borderTop: 0,
+ }}
+ >
+ <AsyncButton
+ onClick={doLogin}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </footer>
+ </div>
+ </div>
+ </div>)
+ }
+
+ return (
+ <div class="columns is-centered" style={{ margin: "auto" }}>
+ <div class="column is-two-thirds ">
+ <div class="modal-card" style={{ width: "100%", margin: 0 }}>
+ <header
+ class="modal-card-head"
+ style={{ border: "1px solid", borderBottom: 0 }}
+ >
+ <p class="modal-card-title">{i18n.str`Login required`}</p>
+ </header>
+ <section
+ class="modal-card-body"
+ style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }}
+ >
+ <i18n.Translate>Please enter your access token.</i18n.Translate>
+
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ <i18n.Translate>Access Token</i18n.Translate>
+ </label>
+ </div>
+ <div class="field-body">
+ <div class="field">
+ <p class="control is-expanded">
+ <input
+ class="input"
+ type="password"
+ placeholder={"current access token"}
+ name="token"
+ onKeyPress={(e) =>
+ e.keyCode === 13
+ ? doLogin()
+ : null
+ }
+ value={token}
+ onInput={(e): void => setToken(e?.currentTarget.value)}
+ />
+ </p>
+ </div>
+ </div>
+ </div>
+ </section>
+ <footer
+ class="modal-card-foot "
+ style={{
+ justifyContent: "space-between",
+ border: "1px solid",
+ borderTop: 0,
+ }}
+ >
+ <div />
+ <AsyncButton
+ type="is-info"
+ onClick={doLogin}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </footer>
+ </div>
+ </div>
+ </div>
+ );
+}
+
+function AsyncButton({ onClick, disabled, type = "", children }: { type?: string, disabled?: boolean, onClick: () => Promise<void>, children: ComponentChildren }): VNode {
+ const [running, setRunning] = useState(false)
+ return <button class={"button " + type} disabled={disabled || running} onClick={() => {
+ setRunning(true)
+ onClick().then(() => {
+ setRunning(false)
+ }).catch(() => {
+ setRunning(false)
+ })
+ }}>
+ {children}
+ </button>
+}
+
+
diff --git a/packages/auditor-backoffice-ui/src/paths/notfound/index.tsx b/packages/auditor-backoffice-ui/src/paths/notfound/index.tsx
new file mode 100644
index 000000000..061a67025
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/notfound/index.tsx
@@ -0,0 +1,34 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode } from "preact";
+import { Link } from "preact-router";
+
+export default function NotFoundPage(): VNode {
+ return (
+ <div>
+ <p>That page doesn&apos;t exist.</p>
+ <Link href="/">
+ <h4>Back to Home</h4>
+ </Link>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/settings/index.tsx b/packages/auditor-backoffice-ui/src/paths/settings/index.tsx
new file mode 100644
index 000000000..093c3d09d
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/settings/index.tsx
@@ -0,0 +1,112 @@
+import { useLang, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
+import { FormErrors, FormProvider } from "../../components/form/FormProvider.js";
+import { InputSelector } from "../../components/form/InputSelector.js";
+import { InputToggle } from "../../components/form/InputToggle.js";
+import { LangSelector } from "../../components/menu/LangSelector.js";
+import { Settings, useSettings } from "../../hooks/useSettings.js";
+
+function getBrowserLang(): string | undefined {
+ if (typeof window === "undefined") return undefined;
+ if (window.navigator.languages) return window.navigator.languages[0];
+ if (window.navigator.language) return window.navigator.language;
+ return undefined;
+}
+
+export function Settings({ onClose }: { onClose?: () => void }): VNode {
+ const { i18n } = useTranslationContext()
+ const borwserLang = getBrowserLang()
+ //const { update } = useLang()
+
+ const [value, updateValue] = useSettings()
+ const errors: FormErrors<Settings> = {
+ }
+
+ function valueHandler(s: (d: Partial<Settings>) => Partial<Settings>): void {
+ const next = s(value)
+ const v: Settings = {
+ advanceOrderMode: next.advanceOrderMode ?? false,
+ dateFormat: next.dateFormat ?? "ymd"
+ }
+ updateValue(v)
+ }
+
+ return <div>
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <div>
+
+ <FormProvider<Settings>
+ name="settings"
+ errors={errors}
+ object={value}
+ valueHandler={valueHandler}
+ >
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ <i18n.Translate>Language</i18n.Translate>
+ <span class="icon has-tooltip-right" data-tooltip={"Force language setting instance of taking the browser"}>
+ <i class="mdi mdi-information" />
+ </span>
+ </label>
+ </div>
+ <div class="field field-body has-addons is-flex-grow-3">
+ <LangSelector />
+ &nbsp;
+ {borwserLang !== undefined && <button
+ data-tooltip={i18n.str`generate random secret key`}
+ class="button is-info mr-2"
+ onClick={(e) => {
+ //update(borwserLang.substring(0, 2))
+ }}
+ >
+ <i18n.Translate>Set default</i18n.Translate>
+ </button>}
+ </div>
+ </div>
+ <InputToggle<Settings>
+ label={i18n.str`Advance order creation`}
+ tooltip={i18n.str`Shows more options in the order creation form`}
+ name="advanceOrderMode"
+ />
+ <InputSelector<Settings>
+ name="dateFormat"
+ label={i18n.str`Date format`}
+ expand={true}
+ help={
+ value.dateFormat === "dmy" ? "31/12/2001" : value.dateFormat === "mdy" ? "12/31/2001" : value.dateFormat === "ymd" ? "2001/12/31" : ""
+ }
+ toStr={(e) => {
+ if (e === "ymd") return "year month day"
+ if (e === "mdy") return "month day year"
+ if (e === "dmy") return "day month year"
+ return "choose one"
+ }}
+ values={[
+ "ymd",
+ "mdy",
+ "dmy",
+ ]}
+ tooltip={i18n.str`how the date is going to be displayed`}
+ />
+ </FormProvider>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ </section >
+ {onClose &&
+ <section class="section is-main-section">
+ <button
+ class="button"
+ onClick={onClose}
+ >
+ <i18n.Translate>Close</i18n.Translate>
+ </button>
+ </section>
+ }
+ </div >
+} \ No newline at end of file
diff --git a/packages/auditor-backoffice-ui/src/schemas/index.ts b/packages/auditor-backoffice-ui/src/schemas/index.ts
new file mode 100644
index 000000000..380466e13
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/schemas/index.ts
@@ -0,0 +1,245 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { isAfter, isFuture } from "date-fns";
+import * as yup from "yup";
+import { AMOUNT_REGEX, PAYTO_REGEX } from "../utils/constants.js";
+import { Amounts } from "@gnu-taler/taler-util";
+
+yup.setLocale({
+ mixed: {
+ default: "field_invalid",
+ },
+ number: {
+ min: ({ min }: any) => ({ key: "field_too_short", values: { min } }),
+ max: ({ max }: any) => ({ key: "field_too_big", values: { max } }),
+ },
+});
+
+function listOfPayToUrisAreValid(values?: (string | undefined)[]): boolean {
+ return !!values && values.every((v) => v && PAYTO_REGEX.test(v));
+}
+
+function currencyWithAmountIsValid(value?: string): boolean {
+ return !!value && Amounts.parse(value) !== undefined;
+}
+function currencyGreaterThan0(value?: string) {
+ if (value) {
+ try {
+ const [, amount] = value.split(":");
+ const intAmount = parseInt(amount, 10);
+ return intAmount > 0;
+ } catch {
+ return false;
+ }
+ }
+ return true;
+}
+
+export const InstanceSchema = yup.object().shape({
+ id: yup.string().required().meta({ type: "url" }),
+ name: yup.string().required(),
+ auth: yup.object().shape({
+ method: yup.string().matches(/^(external|token)$/),
+ token: yup.string().optional().nullable(),
+ }),
+ payto_uris: yup
+ .array()
+ .of(yup.string())
+ .min(1)
+ .meta({ type: "array" })
+ .test("payto", "{path} is not valid", listOfPayToUrisAreValid),
+ default_max_deposit_fee: yup
+ .string()
+ .required()
+ .test("amount", "the amount is not valid", currencyWithAmountIsValid)
+ .meta({ type: "amount" }),
+ default_max_wire_fee: yup
+ .string()
+ .required()
+ .test("amount", "{path} is not valid", currencyWithAmountIsValid)
+ .meta({ type: "amount" }),
+ default_wire_fee_amortization: yup.number().required(),
+ address: yup
+ .object()
+ .shape({
+ country: yup.string().optional(),
+ address_lines: yup.array().of(yup.string()).max(7).optional(),
+ building_number: yup.string().optional(),
+ building_name: yup.string().optional(),
+ street: yup.string().optional(),
+ post_code: yup.string().optional(),
+ town_location: yup.string().optional(),
+ town: yup.string(),
+ district: yup.string().optional(),
+ country_subdivision: yup.string().optional(),
+ })
+ .meta({ type: "group" }),
+ jurisdiction: yup
+ .object()
+ .shape({
+ country: yup.string().optional(),
+ address_lines: yup.array().of(yup.string()).max(7).optional(),
+ building_number: yup.string().optional(),
+ building_name: yup.string().optional(),
+ street: yup.string().optional(),
+ post_code: yup.string().optional(),
+ town_location: yup.string().optional(),
+ town: yup.string(),
+ district: yup.string().optional(),
+ country_subdivision: yup.string().optional(),
+ })
+ .meta({ type: "group" }),
+ // default_pay_delay: yup.object()
+ // .shape({ d_us: yup.number() })
+ // .required()
+ // .meta({ type: 'duration' }),
+ // .transform(numberToDuration),
+ default_wire_transfer_delay: yup
+ .object()
+ .shape({ d_us: yup.number() })
+ .required()
+ .meta({ type: "duration" }),
+ // .transform(numberToDuration),
+});
+
+export const InstanceUpdateSchema = InstanceSchema.clone().omit(["id"]);
+export const InstanceCreateSchema = InstanceSchema.clone();
+
+export const AuthorizeRewardSchema = yup.object().shape({
+ justification: yup.string().required(),
+ amount: yup
+ .string()
+ .required()
+ .test("amount", "the amount is not valid", currencyWithAmountIsValid)
+ .test("amount_positive", "the amount is not valid", currencyGreaterThan0),
+ next_url: yup.string().required(),
+});
+
+const stringIsValidJSON = (value?: string) => {
+ const p = value?.trim();
+ if (!p) return true;
+ try {
+ JSON.parse(p);
+ return true;
+ } catch {
+ return false;
+ }
+};
+
+export const OrderCreateSchema = yup.object().shape({
+ pricing: yup
+ .object()
+ .required()
+ .shape({
+ summary: yup.string().ensure().required(),
+ order_price: yup
+ .string()
+ .ensure()
+ .required()
+ .test("amount", "the amount is not valid", currencyWithAmountIsValid)
+ .test(
+ "amount_positive",
+ "the amount should be greater than 0",
+ currencyGreaterThan0,
+ ),
+ }),
+ // extra: yup.object().test("extra", "is not a JSON format", stringIsValidJSON),
+ payments: yup
+ .object()
+ .required()
+ .shape({
+ refund_deadline: yup
+ .date()
+ .test("future", "should be in the future", (d) =>
+ d ? isFuture(d) : true,
+ ),
+ pay_deadline: yup
+ .date()
+ .test("future", "should be in the future", (d) =>
+ d ? isFuture(d) : true,
+ ),
+ auto_refund_deadline: yup
+ .date()
+ .test("future", "should be in the future", (d) =>
+ d ? isFuture(d) : true,
+ ),
+ delivery_date: yup
+ .date()
+ .test("future", "should be in the future", (d) =>
+ d ? isFuture(d) : true,
+ ),
+ })
+ .test("payment", "dates", (d) => {
+ if (
+ d.pay_deadline &&
+ d.refund_deadline &&
+ isAfter(d.refund_deadline, d.pay_deadline)
+ ) {
+ return new yup.ValidationError(
+ "pay deadline should be greater than refund",
+ "asd",
+ "payments.pay_deadline",
+ );
+ }
+ return true;
+ }),
+});
+
+export const ProductCreateSchema = yup.object().shape({
+ product_id: yup.string().ensure().required(),
+ description: yup.string().required(),
+ unit: yup.string().ensure().required(),
+ price: yup
+ .string()
+ .required()
+ .test("amount", "the amount is not valid", currencyWithAmountIsValid),
+ stock: yup.object({}).optional(),
+ minimum_age: yup.number().optional().min(0),
+});
+
+export const ProductUpdateSchema = yup.object().shape({
+ description: yup.string().required(),
+ price: yup
+ .string()
+ .required()
+ .test("amount", "the amount is not valid", currencyWithAmountIsValid),
+ stock: yup.object({}).optional(),
+ minimum_age: yup.number().optional().min(0),
+});
+
+export const TaxSchema = yup.object().shape({
+ name: yup.string().required().ensure(),
+ tax: yup
+ .string()
+ .required()
+ .test("amount", "the amount is not valid", currencyWithAmountIsValid),
+});
+
+export const NonInventoryProductSchema = yup.object().shape({
+ quantity: yup.number().required().positive(),
+ description: yup.string().required(),
+ unit: yup.string().ensure().required(),
+ price: yup
+ .string()
+ .required()
+ .test("amount", "the amount is not valid", currencyWithAmountIsValid),
+});
diff --git a/packages/demobank-ui/src/scss/DurationPicker.scss b/packages/auditor-backoffice-ui/src/scss/DurationPicker.scss
index aa75b9916..aa75b9916 100644
--- a/packages/demobank-ui/src/scss/DurationPicker.scss
+++ b/packages/auditor-backoffice-ui/src/scss/DurationPicker.scss
diff --git a/packages/demobank-ui/src/scss/_aside.scss b/packages/auditor-backoffice-ui/src/scss/_aside.scss
index 11809990b..e0922093b 100644
--- a/packages/demobank-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 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
@@ -19,35 +19,36 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-html {
- &.has-aside-left {
- &.has-aside-expanded {
- nav.navbar,
- body {
- padding-left: $aside-width;
+@include desktop {
+ html {
+ &.has-aside-left {
+ &.has-aside-expanded {
+ nav.navbar,
+ body {
+ padding-left: $aside-width;
+ }
+ }
+ aside.is-placed-left {
+ display: block;
}
- }
- aside.is-placed-left {
- display: block;
}
}
-}
-aside.aside.is-expanded {
- width: $aside-width;
+ aside.aside.is-expanded {
+ width: $aside-width;
- .menu-list {
- @include icon-with-update-mark($aside-icon-width);
+ .menu-list {
+ @include icon-with-update-mark($aside-icon-width);
- span.menu-item-label {
- display: inline-block;
- }
+ span.menu-item-label {
+ display: inline-block;
+ }
- li.is-active {
- ul {
- display: block;
+ li.is-active {
+ ul {
+ display: block;
+ }
}
- background-color: $body-background-color;
}
}
}
@@ -126,3 +127,55 @@ aside.aside {
margin-bottom: $default-padding * 0.5;
}
}
+
+@include touch {
+ nav.navbar {
+ @include transition(margin-left);
+ }
+ aside.aside {
+ @include transition(left);
+ }
+ html.has-aside-mobile-transition {
+ body {
+ overflow-x: hidden;
+ }
+ body,
+ nav.navbar {
+ width: 100vw;
+ }
+ aside.aside {
+ width: $aside-mobile-width;
+ display: block;
+ left: $aside-mobile-width * -1;
+
+ .image {
+ img {
+ max-width: $aside-mobile-width * 0.33;
+ }
+ }
+
+ .menu-list {
+ li.is-active {
+ ul {
+ display: block;
+ }
+ }
+ a {
+ @include icon-with-update-mark($aside-icon-width);
+
+ span.menu-item-label {
+ display: inline-block;
+ }
+ }
+ }
+ }
+ }
+ div.has-aside-mobile-expanded {
+ nav.navbar {
+ margin-left: $aside-mobile-width;
+ }
+ aside.aside {
+ left: 0;
+ }
+ }
+}
diff --git a/packages/demobank-ui/src/scss/_card.scss b/packages/auditor-backoffice-ui/src/scss/_card.scss
index 3f71aeb6a..62db7f457 100644
--- a/packages/demobank-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 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/demobank-ui/src/scss/_custom-calendar.scss b/packages/auditor-backoffice-ui/src/scss/_custom-calendar.scss
index 463cd88d3..34c40092b 100644
--- a/packages/demobank-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 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
@@ -40,10 +40,6 @@
0 10px 10px rgba(0, 0, 0, 0.22);
}
-.home .datePicker div {
- margin-top: 0px;
- margin-bottom: 0px;
-}
.datePicker {
text-align: left;
background: var(--primary-card-color);
diff --git a/packages/demobank-ui/src/scss/_footer.scss b/packages/auditor-backoffice-ui/src/scss/_footer.scss
index 112522ed8..5855af742 100644
--- a/packages/demobank-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 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/demobank-ui/src/scss/_form.scss b/packages/auditor-backoffice-ui/src/scss/_form.scss
index 786044eff..bd28a17cf 100644
--- a/packages/demobank-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 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/demobank-ui/src/scss/_hero-bar.scss b/packages/auditor-backoffice-ui/src/scss/_hero-bar.scss
index 31b7e623e..0276468d7 100644
--- a/packages/demobank-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 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/demobank-ui/src/scss/_loading.scss b/packages/auditor-backoffice-ui/src/scss/_loading.scss
index d25bf8048..d88d8c355 100644
--- a/packages/demobank-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 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/demobank-ui/src/scss/_main-section.scss b/packages/auditor-backoffice-ui/src/scss/_main-section.scss
index 01edc24bf..5a8b20ba0 100644
--- a/packages/demobank-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 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/demobank-ui/src/scss/_misc.scss b/packages/auditor-backoffice-ui/src/scss/_misc.scss
index 65bd28dbd..045d087e2 100644
--- a/packages/demobank-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 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/demobank-ui/src/scss/_mixins.scss b/packages/auditor-backoffice-ui/src/scss/_mixins.scss
index b52e590e3..f119ec68a 100644
--- a/packages/demobank-ui/src/scss/_mixins.scss
+++ b/packages/auditor-backoffice-ui/src/scss/_mixins.scss
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -28,7 +28,7 @@
width: $icon-base-width;
&.has-update-mark:after {
- right: ($icon-base-width / 2) - 0.85;
+ right: calc($icon-base-width / 2) - 0.85;
}
}
}
diff --git a/packages/demobank-ui/src/scss/_modal.scss b/packages/auditor-backoffice-ui/src/scss/_modal.scss
index b3a31ebf1..b2bfd3e9e 100644
--- a/packages/demobank-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 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/demobank-ui/src/scss/_nav-bar.scss b/packages/auditor-backoffice-ui/src/scss/_nav-bar.scss
index c6dd04263..406e0392f 100644
--- a/packages/demobank-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 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/demobank-ui/src/scss/_table.scss b/packages/auditor-backoffice-ui/src/scss/_table.scss
index b68d50e4f..e4fbfc7b3 100644
--- a/packages/demobank-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 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/demobank-ui/src/scss/_theme-default.scss b/packages/auditor-backoffice-ui/src/scss/_theme-default.scss
index 538dfd4da..e74ece0e9 100644
--- a/packages/demobank-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 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/demobank-ui/src/scss/_tiles.scss b/packages/auditor-backoffice-ui/src/scss/_tiles.scss
index e69d995f0..94dd6c21d 100644
--- a/packages/demobank-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 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/demobank-ui/src/scss/_title-bar.scss b/packages/auditor-backoffice-ui/src/scss/_title-bar.scss
index 932f8e65d..bac3f6b42 100644
--- a/packages/demobank-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 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/demobank-ui/src/scss/fonts/XRXV3I6Li01BKofINeaE.ttf b/packages/auditor-backoffice-ui/src/scss/fonts/XRXV3I6Li01BKofINeaE.ttf
index 7665ee336..7665ee336 100644
--- a/packages/demobank-ui/src/scss/fonts/XRXV3I6Li01BKofINeaE.ttf
+++ b/packages/auditor-backoffice-ui/src/scss/fonts/XRXV3I6Li01BKofINeaE.ttf
Binary files differ
diff --git a/packages/demobank-ui/src/scss/fonts/nunito.css b/packages/auditor-backoffice-ui/src/scss/fonts/nunito.css
index 8d45df9a1..a578506e8 100644
--- a/packages/demobank-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 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/demobank-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.eot b/packages/auditor-backoffice-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.eot
index ab6b25ded..ab6b25ded 100644
--- a/packages/demobank-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.eot
+++ b/packages/auditor-backoffice-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.eot
Binary files differ
diff --git a/packages/demobank-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.ttf b/packages/auditor-backoffice-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.ttf
index 824be10fa..824be10fa 100644
--- a/packages/demobank-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.ttf
+++ b/packages/auditor-backoffice-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.ttf
Binary files differ
diff --git a/packages/demobank-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.woff b/packages/auditor-backoffice-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.woff
index 7e087c1de..7e087c1de 100644
--- a/packages/demobank-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.woff
+++ b/packages/auditor-backoffice-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.woff
Binary files differ
diff --git a/packages/demobank-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.woff2 b/packages/auditor-backoffice-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.woff2
index b5caa4ddc..b5caa4ddc 100644
--- a/packages/demobank-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.woff2
+++ b/packages/auditor-backoffice-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.woff2
Binary files differ
diff --git a/packages/demobank-ui/src/scss/icons/materialdesignicons-4.9.95.min.css b/packages/auditor-backoffice-ui/src/scss/icons/materialdesignicons-4.9.95.min.css
index 2b8a2b244..2b8a2b244 100644
--- a/packages/demobank-ui/src/scss/icons/materialdesignicons-4.9.95.min.css
+++ b/packages/auditor-backoffice-ui/src/scss/icons/materialdesignicons-4.9.95.min.css
diff --git a/packages/demobank-ui/src/scss/libs/_all.scss b/packages/auditor-backoffice-ui/src/scss/libs/_all.scss
index d33f8acc4..cba6f26eb 100644
--- a/packages/demobank-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 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
@@ -22,8 +22,8 @@
@import "node_modules/bulma-radio/bulma-radio";
// @import "node_modules/bulma-responsive-tables/bulma-responsive-tables";
@import "node_modules/bulma-checkbox/bulma-checkbox";
-// @import "node_modules/bulma-switch-control/bulma-switch-control";
-// @import "node_modules/bulma-upload-control/bulma-upload-control";
+@import "node_modules/bulma-switch-control/bulma-switch-control";
+@import "node_modules/bulma-upload-control/bulma-upload-control";
/* Bulma */
@import "node_modules/bulma/bulma";
diff --git a/packages/auditor-backoffice-ui/src/scss/main.scss b/packages/auditor-backoffice-ui/src/scss/main.scss
new file mode 100644
index 000000000..c4be8aa73
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/scss/main.scss
@@ -0,0 +1,195 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+/* Theme style (colors & sizes) */
+@import "theme-default";
+
+/* Core Libs & Lib configs */
+@import "libs/all";
+
+/* Mixins */
+@import "mixins";
+
+/* Theme components */
+@import "nav-bar";
+@import "aside";
+@import "title-bar";
+@import "hero-bar";
+@import "card";
+@import "table";
+@import "tiles";
+@import "form";
+@import "main-section";
+@import "modal";
+@import "footer";
+@import "misc";
+@import "custom-calendar";
+@import "loading";
+
+@import "fonts/nunito.css";
+@import "icons/materialdesignicons-4.9.95.min.css";
+
+$tooltip-color: red;
+
+@import "../../node_modules/@creativebulma/bulma-tooltip/dist/bulma-tooltip.min.css";
+@import "../../node_modules/bulma-timeline/dist/css/bulma-timeline.min.css";
+
+@import "toggle";
+
+.notification {
+ background-color: transparent;
+}
+
+.timeline .timeline-item .timeline-content {
+ padding-top: 0;
+}
+
+.timeline .timeline-item:last-child::before {
+ display: none;
+}
+
+.timeline .timeline-item .timeline-marker {
+ top: 0;
+}
+
+.toast {
+ position: absolute;
+ width: 60%;
+ margin-left: 10%;
+ margin-right: 10%;
+ z-index: 999;
+
+ display: flex;
+ flex-direction: column;
+ padding: 15px;
+ text-align: center;
+ pointer-events: none;
+}
+
+.toast>.message {
+ white-space: pre-wrap;
+ opacity: 80%;
+}
+
+div {
+ &.is-loading {
+ position: relative;
+ pointer-events: none;
+ opacity: 0.5;
+
+ &:after {
+ // @include loader;
+ position: absolute;
+ top: calc(50% - 2.5em);
+ left: calc(50% - 2.5em);
+ width: 5em;
+ height: 5em;
+ border-width: 0.25em;
+ }
+ }
+}
+
+input[type="checkbox"]:indeterminate+.check {
+ background: red !important;
+}
+
+.right-sticky {
+ position: sticky;
+ right: 0px;
+ background-color: $white;
+}
+
+.right-sticky .buttons {
+ flex-wrap: nowrap;
+}
+
+.table.is-striped tbody tr:not(.is-selected):nth-child(even) .right-sticky {
+ background-color: #fafafa;
+}
+
+tr:hover .right-sticky {
+ background-color: hsl(0, 0%, 80%);
+}
+
+.table.is-striped tbody tr:nth-child(even):hover .right-sticky {
+ background-color: hsl(0, 0%, 95%);
+}
+
+.content-full-size {
+ height: calc(100% - 3rem);
+ position: absolute;
+ width: calc(100% - 14rem);
+ display: flex;
+}
+
+.content-full-size .column .card {
+ min-width: 200px;
+}
+
+@include touch {
+ .content-full-size {
+ height: 100%;
+ position: absolute;
+ width: 100%;
+ }
+}
+
+.column.is-half {
+ flex: none;
+ width: 50%;
+}
+
+input:read-only {
+ cursor: initial;
+}
+
+[data-tooltip]:before {
+ max-width: 15rem;
+ width: max-content;
+ text-align: left;
+ transition: opacity 0.1s linear 1s;
+ // transform: inherit !important;
+ white-space: pre-wrap !important;
+ font-weight: normal;
+ // position: relative;
+}
+
+.icon[data-tooltip]:before {
+ transition: none;
+ z-index: 5;
+}
+
+span[data-tooltip] {
+ border-bottom: none;
+}
+
+div[data-tooltip]::before {
+ position: absolute;
+}
+
+.modal-card-body>p {
+ padding: 1em;
+}
+
+.modal-card-body>p.warning {
+ background-color: #fffbdd;
+ border: solid 1px #f2e9bf;
+} \ No newline at end of file
diff --git a/packages/auditor-backoffice-ui/src/scss/toggle.scss b/packages/auditor-backoffice-ui/src/scss/toggle.scss
new file mode 100644
index 000000000..24636da2f
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/scss/toggle.scss
@@ -0,0 +1,51 @@
+$green: #56c080;
+
+.toggle {
+ cursor: pointer;
+ display: inline-block;
+}
+.toggle-switch {
+ display: inline-block;
+ background: #ccc;
+ border-radius: 16px;
+ width: 58px;
+ height: 32px;
+ position: relative;
+ vertical-align: middle;
+ transition: background 0.25s;
+ &:before,
+ &:after {
+ content: "";
+ }
+ &:before {
+ display: block;
+ background: linear-gradient(to bottom, #fff 0%, #eee 100%);
+ border-radius: 50%;
+ box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25);
+ width: 24px;
+ height: 24px;
+ position: absolute;
+ top: 4px;
+ left: 4px;
+ transition: left 0.25s;
+ }
+ .toggle:hover &:before {
+ background: linear-gradient(to bottom, #fff 0%, #fff 100%);
+ box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.5);
+ }
+ .toggle-checkbox:checked + & {
+ background: $green;
+ &:before {
+ left: 30px;
+ }
+ }
+}
+.toggle-checkbox {
+ position: absolute;
+ visibility: hidden;
+}
+.toggle-label {
+ margin-left: 5px;
+ position: relative;
+ top: 2px;
+}
diff --git a/packages/auditor-backoffice-ui/src/stories.test.ts b/packages/auditor-backoffice-ui/src/stories.test.ts
new file mode 100644
index 000000000..abd993550
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/stories.test.ts
@@ -0,0 +1,44 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { setupI18n } from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
+import { parseGroupImport } from "@gnu-taler/web-util/browser";
+import * as admin from "./paths/admin/index.stories.js";
+import * as instance from "./paths/instance/index.stories.js";
+
+setupI18n("en", { en: {} });
+
+describe("All the examples:", () => {
+ const cms = parseGroupImport({ admin, instance });
+ cms.forEach((group) => {
+ describe(`Example for group: ${group.title}`, () => {
+ group.list.forEach((component) => {
+ describe(`Component: ${component.name}`, () => {
+ component.examples.forEach((example) => {
+ it(`should render example: ${example.name}`, () => {
+ tests.renderUI(example.render);
+ });
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/packages/auditor-backoffice-ui/src/stories.tsx b/packages/auditor-backoffice-ui/src/stories.tsx
new file mode 100644
index 000000000..8bb06b8cb
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/stories.tsx
@@ -0,0 +1,48 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { strings } from "./i18n/strings.js";
+
+import * as admin from "./paths/admin/index.stories.js";
+import * as instance from "./paths/instance/index.stories.js";
+import * as components from "./components/index.stories.js";
+
+import { renderStories } from "@gnu-taler/web-util/browser";
+
+import "./scss/main.scss";
+
+function SortStories(a: any, b: any): number {
+ return (a?.order ?? 0) - (b?.order ?? 0);
+}
+
+function main(): void {
+ renderStories(
+ { admin, instance, components },
+ {
+ strings,
+ },
+ );
+}
+
+if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", main);
+} else {
+ main();
+}
diff --git a/packages/merchant-backoffice-ui/.storybook/.babelrc b/packages/auditor-backoffice-ui/src/sw.js
index 610b6f339..bf52db6fa 100644
--- a/packages/merchant-backoffice-ui/.storybook/.babelrc
+++ b/packages/auditor-backoffice-ui/src/sw.js
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2021-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -14,12 +14,12 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- /**
+/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
-{
- "presets": [
- "preact-cli/babel"
- ]
-} \ No newline at end of file
+
+// import { getFiles, setupPrecaching, setupRouting } from 'preact-cli/sw/';
+
+// setupRouting();
+// setupPrecaching(getFiles());
diff --git a/packages/auditor-backoffice-ui/src/utils/amount.ts b/packages/auditor-backoffice-ui/src/utils/amount.ts
new file mode 100644
index 000000000..475489d3e
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/utils/amount.ts
@@ -0,0 +1,71 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ amountFractionalBase,
+ AmountJson,
+ Amounts,
+} from "@gnu-taler/taler-util";
+import { MerchantBackend } from "../declaration.js";
+
+/**
+ * merge refund with the same description and a difference less than one minute
+ * @param prev list of refunds that will hold the merged refunds
+ * @param cur new refund to add to the list
+ * @returns list with the new refund, may be merged with the last
+ */
+export function mergeRefunds(
+ prev: MerchantBackend.Orders.RefundDetails[],
+ cur: MerchantBackend.Orders.RefundDetails,
+): MerchantBackend.Orders.RefundDetails[] {
+ let tail;
+
+ if (
+ prev.length === 0 || //empty list
+ cur.timestamp.t_s === "never" || //current does not have timestamp
+ (tail = prev[prev.length - 1]).timestamp.t_s === "never" || // last does not have timestamp
+ cur.reason !== tail.reason || //different reason
+ cur.pending !== tail.pending || //different pending state
+ Math.abs(cur.timestamp.t_s - tail.timestamp.t_s) > 1000 * 60
+ ) {
+ //more than 1 minute difference
+
+ //can't merge refunds, they are different or to distant in time
+ prev.push(cur);
+ return prev;
+ }
+
+ const a = Amounts.parseOrThrow(tail.amount);
+ const b = Amounts.parseOrThrow(cur.amount);
+ const r = Amounts.add(a, b).amount;
+
+ prev[prev.length - 1] = {
+ ...tail,
+ amount: Amounts.stringify(r),
+ };
+
+ return prev;
+}
+
+export function rate(a: AmountJson, b: AmountJson): number {
+ const af = toFloat(a);
+ const bf = toFloat(b);
+ if (bf === 0) return 0;
+ return af / bf;
+}
+
+function toFloat(amount: AmountJson): number {
+ return amount.value + amount.fraction / amountFractionalBase;
+}
diff --git a/packages/auditor-backoffice-ui/src/utils/constants.ts b/packages/auditor-backoffice-ui/src/utils/constants.ts
new file mode 100644
index 000000000..7c4e288b3
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/utils/constants.ts
@@ -0,0 +1,197 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+//https://tools.ietf.org/html/rfc8905
+export const PAYTO_REGEX =
+ /^payto:\/\/[a-zA-Z][a-zA-Z0-9-.]+(\/[a-zA-Z0-9\-\.\~\(\)@_%:!$&'*+,;=]*)*\??((amount|receiver-name|sender-name|instruction|message)=[a-zA-Z0-9\-\.\~\(\)@_%:!$'*+,;=]*&?)*$/;
+export const PAYTO_WIRE_METHOD_LOOKUP =
+ /payto:\/\/([a-zA-Z][a-zA-Z0-9-.]+)\/.*/;
+
+export const AMOUNT_REGEX = /^[a-zA-Z][a-zA-Z]{1,11}:[0-9][0-9,]*\.?[0-9,]*$/;
+
+export const INSTANCE_ID_LOOKUP = /\/instances\/([^/]*)\/?$/;
+
+export const AMOUNT_ZERO_REGEX = /^[a-zA-Z][a-zA-Z]*:0$/;
+
+export const CROCKFORD_BASE32_REGEX =
+ /^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]+[*~$=U]*$/;
+
+export const URL_REGEX =
+ /^((https?:)(\/\/\/?)([\w]*(?::[\w]*)?@)?([\d\w\.-]+)(?::(\d+))?)\/$/;
+
+// how much rows we add every time user hit load more
+export const PAGE_SIZE = 20;
+// how bigger can be the result set
+// after this threshold, load more with move the cursor
+export const MAX_RESULT_SIZE = PAGE_SIZE * 2 - 1;
+
+// how much we will wait for all request, in seconds
+export const DEFAULT_REQUEST_TIMEOUT = 10;
+
+export const MAX_IMAGE_SIZE = 1024 * 1024;
+
+export const INSTANCE_ID_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9_.@-]+$/;
+
+export const COUNTRY_TABLE = {
+ AE: "U.A.E.",
+ AF: "Afghanistan",
+ AL: "Albania",
+ AM: "Armenia",
+ AN: "Netherlands Antilles",
+ AR: "Argentina",
+ AT: "Austria",
+ AU: "Australia",
+ AZ: "Azerbaijan",
+ BA: "Bosnia and Herzegovina",
+ BD: "Bangladesh",
+ BE: "Belgium",
+ BG: "Bulgaria",
+ BH: "Bahrain",
+ BN: "Brunei Darussalam",
+ BO: "Bolivia",
+ BR: "Brazil",
+ BT: "Bhutan",
+ BY: "Belarus",
+ BZ: "Belize",
+ CA: "Canada",
+ CG: "Congo",
+ CH: "Switzerland",
+ CI: "Cote d'Ivoire",
+ CL: "Chile",
+ CM: "Cameroon",
+ CN: "People's Republic of China",
+ CO: "Colombia",
+ CR: "Costa Rica",
+ CS: "Serbia and Montenegro",
+ CZ: "Czech Republic",
+ DE: "Germany",
+ DK: "Denmark",
+ DO: "Dominican Republic",
+ DZ: "Algeria",
+ EC: "Ecuador",
+ EE: "Estonia",
+ EG: "Egypt",
+ ER: "Eritrea",
+ ES: "Spain",
+ ET: "Ethiopia",
+ FI: "Finland",
+ FO: "Faroe Islands",
+ FR: "France",
+ GB: "United Kingdom",
+ GD: "Caribbean",
+ GE: "Georgia",
+ GL: "Greenland",
+ GR: "Greece",
+ GT: "Guatemala",
+ HK: "Hong Kong",
+ // HK: "Hong Kong S.A.R.",
+ HN: "Honduras",
+ HR: "Croatia",
+ HT: "Haiti",
+ HU: "Hungary",
+ ID: "Indonesia",
+ IE: "Ireland",
+ IL: "Israel",
+ IN: "India",
+ IQ: "Iraq",
+ IR: "Iran",
+ IS: "Iceland",
+ IT: "Italy",
+ JM: "Jamaica",
+ JO: "Jordan",
+ JP: "Japan",
+ KE: "Kenya",
+ KG: "Kyrgyzstan",
+ KH: "Cambodia",
+ KR: "South Korea",
+ KW: "Kuwait",
+ KZ: "Kazakhstan",
+ LA: "Laos",
+ LB: "Lebanon",
+ LI: "Liechtenstein",
+ LK: "Sri Lanka",
+ LT: "Lithuania",
+ LU: "Luxembourg",
+ LV: "Latvia",
+ LY: "Libya",
+ MA: "Morocco",
+ MC: "Principality of Monaco",
+ MD: "Moldava",
+ // MD: "Moldova",
+ ME: "Montenegro",
+ MK: "Former Yugoslav Republic of Macedonia",
+ ML: "Mali",
+ MM: "Myanmar",
+ MN: "Mongolia",
+ MO: "Macau S.A.R.",
+ MT: "Malta",
+ MV: "Maldives",
+ MX: "Mexico",
+ MY: "Malaysia",
+ NG: "Nigeria",
+ NI: "Nicaragua",
+ NL: "Netherlands",
+ NO: "Norway",
+ NP: "Nepal",
+ NZ: "New Zealand",
+ OM: "Oman",
+ PA: "Panama",
+ PE: "Peru",
+ PH: "Philippines",
+ PK: "Islamic Republic of Pakistan",
+ PL: "Poland",
+ PR: "Puerto Rico",
+ PT: "Portugal",
+ PY: "Paraguay",
+ QA: "Qatar",
+ RE: "Reunion",
+ RO: "Romania",
+ RS: "Serbia",
+ RU: "Russia",
+ RW: "Rwanda",
+ SA: "Saudi Arabia",
+ SE: "Sweden",
+ SG: "Singapore",
+ SI: "Slovenia",
+ SK: "Slovak",
+ SN: "Senegal",
+ SO: "Somalia",
+ SR: "Suriname",
+ SV: "El Salvador",
+ SY: "Syria",
+ TH: "Thailand",
+ TJ: "Tajikistan",
+ TM: "Turkmenistan",
+ TN: "Tunisia",
+ TR: "Turkey",
+ TT: "Trinidad and Tobago",
+ TW: "Taiwan",
+ TZ: "Tanzania",
+ UA: "Ukraine",
+ US: "United States",
+ UY: "Uruguay",
+ VA: "Vatican",
+ VE: "Venezuela",
+ VN: "Viet Nam",
+ YE: "Yemen",
+ ZA: "South Africa",
+ ZW: "Zimbabwe",
+};
diff --git a/packages/auditor-backoffice-ui/src/utils/regex.test.ts b/packages/auditor-backoffice-ui/src/utils/regex.test.ts
new file mode 100644
index 000000000..984f1a472
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/utils/regex.test.ts
@@ -0,0 +1,88 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { expect } from "chai";
+import { AMOUNT_REGEX, PAYTO_REGEX } from "./constants.js";
+
+describe("payto uri format", () => {
+ const valids = [
+ "payto://iban/DE75512108001245126199?amount=EUR:200.0&message=hello",
+ "payto://ach/122000661/1234",
+ "payto://upi/alice@example.com?receiver-name=Alice&amount=INR:200",
+ "payto://void/?amount=EUR:10.5",
+ "payto://ilp/g.acme.bob",
+ ];
+
+ it("should be valid", () => {
+ valids.forEach((v) => expect(v).match(PAYTO_REGEX));
+ });
+
+ const invalids = [
+ // has two question marks
+ "payto://iban/DE75?512108001245126199?amount=EUR:200.0&message=hello",
+ // has a space
+ "payto://ach /122000661/1234",
+ // has a space
+ "payto://upi/alice@ example.com?receiver-name=Alice&amount=INR:200",
+ // invalid field name (mount instead of amount)
+ "payto://void/?mount=EUR:10.5",
+ // payto:// is incomplete
+ "payto: //ilp/g.acme.bob",
+ ];
+
+ it("should not be valid", () => {
+ invalids.forEach((v) => expect(v).not.match(PAYTO_REGEX));
+ });
+});
+
+describe("amount format", () => {
+ const valids = [
+ "ARS:10",
+ "COL:10.2",
+ "UY:1,000.2",
+ "ARS:10.123,123",
+ "ARS:1,000,000",
+ "ARSCOL:10",
+ "LONGESTCURR:1,000,000.123,123",
+ ];
+
+
+ it("should be valid", () => {
+ valids.forEach((v) => expect(v).match(AMOUNT_REGEX));
+ });
+
+ const invalids = [
+ //no currency name
+ ":10",
+ //use . instead of ,
+ "ARS:1.000.000",
+ //currency name with numbers
+ "1ARS:10",
+ //currency name with numbers
+ "AR5:10",
+ //missing value
+ "USD:",
+ ];
+
+ it("should not be valid", () => {
+ invalids.forEach((v) => expect(v).not.match(AMOUNT_REGEX));
+ });
+});
diff --git a/packages/auditor-backoffice-ui/src/utils/table.ts b/packages/auditor-backoffice-ui/src/utils/table.ts
new file mode 100644
index 000000000..db2b2021c
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/utils/table.ts
@@ -0,0 +1,57 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { WithId } from "../declaration.js";
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+export interface Actions<T extends WithId> {
+ element: T;
+ type: "DELETE" | "UPDATE";
+}
+
+function notEmpty<TValue>(value: TValue | null | undefined): value is TValue {
+ return value !== null && value !== undefined;
+}
+
+export function buildActions<T extends WithId>(
+ instances: T[],
+ selected: string[],
+ action: "DELETE",
+): Actions<T>[] {
+ return selected
+ .map((id) => instances.find((i) => i.id === id))
+ .filter(notEmpty)
+ .map((id) => ({ element: id, type: action }));
+}
+
+/**
+ * For any object or array, return the same object if is not empty.
+ * not empty:
+ * - for arrays: at least one element not undefined
+ * - for objects: at least one property not undefined
+ * @param obj
+ * @returns
+ */
+export function undefinedIfEmpty<
+ T extends Record<string, unknown> | Array<unknown>,
+>(obj: T | undefined): T | undefined {
+ if (obj === undefined) return undefined;
+ return Object.values(obj).some((v) => v !== undefined) ? obj : undefined;
+}
diff --git a/packages/merchant-backend-ui/src/utils/types.ts b/packages/auditor-backoffice-ui/src/utils/types.ts
index 9e49d39e1..0d249f3c4 100644
--- a/packages/merchant-backend-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 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
@@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { VNode } from "preact"
+import { VNode } from "preact";
export interface KeyValue {
[key: string]: string;
@@ -23,9 +23,9 @@ export interface KeyValue {
export interface Notification {
message: string;
description?: string | VNode;
+ details?: string | VNode;
type: MessageType;
}
-export type ValueOrFunction<T> = T | ((p: T) => T)
-export type MessageType = 'INFO' | 'WARN' | 'ERROR' | 'SUCCESS'
-
+export type ValueOrFunction<T> = T | ((p: T) => T);
+export type MessageType = "INFO" | "WARN" | "ERROR" | "SUCCESS";
diff --git a/packages/auditor-backoffice-ui/test.mjs b/packages/auditor-backoffice-ui/test.mjs
new file mode 100755
index 000000000..be76348e5
--- /dev/null
+++ b/packages/auditor-backoffice-ui/test.mjs
@@ -0,0 +1,31 @@
+#!/usr/bin/env node
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { build } from "@gnu-taler/web-util/build";
+import { getFilesInDirectory } from "@gnu-taler/web-util/build";
+
+const allTestFiles = getFilesInDirectory("src", /.test.tsx?$/);
+
+await build({
+ type: "test",
+ source: {
+ js: allTestFiles.files,
+ assets: [{base:"src",files:["src/index.html"]}],
+ },
+ destination: "./dist/test",
+ css: "sass",
+});
diff --git a/packages/auditor-backoffice-ui/tsconfig.json b/packages/auditor-backoffice-ui/tsconfig.json
new file mode 100644
index 000000000..396f1e9e7
--- /dev/null
+++ b/packages/auditor-backoffice-ui/tsconfig.json
@@ -0,0 +1,58 @@
+{
+ "compilerOptions": {
+ /* Basic Options */
+ "target": "ES2020" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */,
+ "module": "Node16" /* Specify module code generation: 'none', commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
+ "lib": [
+ "es2020",
+ "dom"
+ ] /* Specify library files to be included in the compilation: */,
+ // "allowJs": true, /* Allow javascript files to be compiled. */
+ // "checkJs": true, /* Report errors in .js files. */
+ "jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */,
+ "jsxFactory": "h" /* Specify the JSX factory function to use when targeting react JSX emit, e.g. React.createElement or h. */,
+ "jsxFragmentFactory": "Fragment", // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-0.html#custom-jsx-factories
+ // "declaration": true, /* Generates corresponding '.d.ts' file. */
+ // "sourceMap": true, /* Generates corresponding '.map' file. */
+ // "outFile": "./", /* Concatenate and emit output to single file. */
+ // "outDir": "./", /* Redirect output structure to the directory. */
+ // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
+ // "removeComments": true, /* Do not emit comments to output. */
+ "noEmit": true /* Do not emit outputs. */,
+ // "importHelpers": true, /* Import emit helpers from 'tslib'. */
+ // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
+ // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
+ /* Strict Type-Checking Options */
+ "strict": true /* Enable all strict type-checking options. */,
+ // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
+ // "strictNullChecks": true, /* Enable strict null checks. */
+ // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
+ // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
+ /* Additional Checks */
+ // "noUnusedLocals": true, /* Report errors on unused locals. */
+ // "noUnusedParameters": true, /* Report errors on unused parameters. */
+ // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
+ // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
+ /* Module Resolution Options */
+ "moduleResolution": "node16" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
+ "esModuleInterop": true /* */,
+ // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
+ // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
+ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
+ // "typeRoots": [], /* List of folders to include type definitions from. */
+ // "types": [], /* Type declaration files to be included in compilation. */
+ // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
+ // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
+ /* Source Map Options */
+ // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
+ // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */
+ // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
+ // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
+ /* Experimental Options */
+ // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
+ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
+ /* Advanced Options */
+ "skipLibCheck": true /* Skip type checking of declaration files. */
+ },
+ "include": ["src/**/*", "tests/**/*"]
+}
diff --git a/packages/bank-ui/.eslintrc.cjs b/packages/bank-ui/.eslintrc.cjs
new file mode 100644
index 000000000..05618b499
--- /dev/null
+++ b/packages/bank-ui/.eslintrc.cjs
@@ -0,0 +1,28 @@
+module.exports = {
+ extends: [
+ 'eslint:recommended',
+ 'plugin:@typescript-eslint/recommended',
+ 'plugin:react/recommended',
+ ],
+ parser: '@typescript-eslint/parser',
+ plugins: ['@typescript-eslint', 'header'],
+ root: true,
+ rules: {
+ "react/no-unknown-property": 0,
+ "react/no-unescaped-entities": 0,
+ "@typescript-eslint/no-namespace": 0,
+ "@typescript-eslint/no-unused-vars": [2,{argsIgnorePattern:"^_"}],
+ "header/header": [2,"copyleft-header.js"]
+ },
+ parserOptions: {
+ ecmaVersion: 6,
+ sourceType: 'module',
+ jsx: true,
+ },
+ settings: {
+ react: {
+ version: "18",
+ pragma: "h",
+ }
+ },
+};
diff --git a/packages/bank-ui/.gitignore b/packages/bank-ui/.gitignore
new file mode 100644
index 000000000..30cb2774c
--- /dev/null
+++ b/packages/bank-ui/.gitignore
@@ -0,0 +1,4 @@
+node_modules
+/build
+/*.log
+/demobank-ui-settings.js
diff --git a/packages/bank-ui/Makefile b/packages/bank-ui/Makefile
new file mode 100644
index 000000000..036e6fd3e
--- /dev/null
+++ b/packages/bank-ui/Makefile
@@ -0,0 +1,37 @@
+# This Makefile has been placed in the public domain
+
+ifeq ($(TOPLEVEL), yes)
+ $(info top-level build)
+ -include ../../.config.mk
+ override DESTDIR := $(TOP_DESTDIR)
+else
+ $(info package-level build)
+ -include ../../.config.mk
+ -include .config.mk
+endif
+
+$(info prefix is $(prefix))
+
+.PHONY: all
+all:
+ @echo run \'make install\' to install
+
+spa_dir=$(DESTDIR)$(prefix)/share/taler/bank-ui
+
+.PHONY: deps
+deps:
+ pnpm install --frozen-lockfile --filter @gnu-taler/bank-ui...
+ pnpm run --filter @gnu-taler/bank-ui... compile
+ pnpm run check
+ pnpm run build
+
+.PHONY: install-nodeps
+install-nodeps:
+ install -d $(spa_dir)
+ install ./dist/prod/* $(spa_dir)
+
+.PHONY: install
+install:
+ $(MAKE) deps
+ $(MAKE) install-nodeps
+
diff --git a/packages/bank-ui/README.md b/packages/bank-ui/README.md
new file mode 100644
index 000000000..4275cce57
--- /dev/null
+++ b/packages/bank-ui/README.md
@@ -0,0 +1,24 @@
+# Taler Bank UI
+
+Web-based user interface for the libeufin bank ui.
+
+## CLI Commands
+
+- `./dev.mjs` development setup. Will listen in :8080 and reload every time a file is save.
+- `./build.mjs` build for production.
+- `./test.mjs` build and run unit test
+
+## Testing
+
+By default, the bank-ui will expect the backend to be in `window.origin` but that can be overridden using the `settings.json` file or by session in the localStorage.
+
+```
+localStorage.setItem("bank-base-url", OTHER_URL);
+```
+
+## Customizing Per-Deployment Settings
+
+To customize per-deployment settings, make sure that the
+`settings.json` file is served alongside the UI.
+
+For more information about the values check the file `settings.ts` in the src folder.
diff --git a/packages/bank-ui/build.mjs b/packages/bank-ui/build.mjs
new file mode 100755
index 000000000..04a6f646b
--- /dev/null
+++ b/packages/bank-ui/build.mjs
@@ -0,0 +1,28 @@
+#!/usr/bin/env node
+/*
+ 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 { build } from "@gnu-taler/web-util/build";
+
+await build({
+ type: "production",
+ source: {
+ js: ["src/index.tsx"],
+ assets: [{ base: "src", files: ["src/index.html"] }],
+ },
+ destination: "./dist/prod",
+ css: "postcss",
+});
diff --git a/packages/demobank-ui/contrib/po2ts b/packages/bank-ui/contrib/po2ts
index a135da61b..a135da61b 100755
--- a/packages/demobank-ui/contrib/po2ts
+++ b/packages/bank-ui/contrib/po2ts
diff --git a/packages/bank-ui/copyleft-header.js b/packages/bank-ui/copyleft-header.js
new file mode 100644
index 000000000..7fa276bea
--- /dev/null
+++ b/packages/bank-ui/copyleft-header.js
@@ -0,0 +1,15 @@
+/*
+ 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/>
+ */
diff --git a/packages/bank-ui/dev.mjs b/packages/bank-ui/dev.mjs
new file mode 100755
index 000000000..7b4f719ae
--- /dev/null
+++ b/packages/bank-ui/dev.mjs
@@ -0,0 +1,41 @@
+#!/usr/bin/env node
+/*
+ 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 { serve } from "@gnu-taler/web-util/node";
+import { initializeDev } from "@gnu-taler/web-util/build";
+
+const devEntryPoints = ["src/stories.tsx", "src/index.tsx"];
+
+const build = initializeDev({
+ type: "development",
+ source: {
+ js: devEntryPoints,
+ assets: [{ base: "src", files: ["src/index.html", "src/settings.json"] }],
+ },
+ destination: "./dist/dev",
+ public: "/app",
+ css: "postcss",
+});
+
+await build();
+
+serve({
+ folder: "./dist/dev",
+ port: 8080,
+ source: "./src",
+ onSourceUpdate: build,
+});
diff --git a/packages/bank-ui/package.json b/packages/bank-ui/package.json
new file mode 100644
index 000000000..f06905a93
--- /dev/null
+++ b/packages/bank-ui/package.json
@@ -0,0 +1,52 @@
+{
+ "private": true,
+ "name": "@gnu-taler/bank-ui",
+ "version": "0.10.7",
+ "license": "AGPL-3.0-OR-LATER",
+ "type": "module",
+ "scripts": {
+ "build": "./build.mjs",
+ "check": "tsc",
+ "clean": "rm -rf dist lib tsconfig.tsbuildinfo",
+ "compile": "tsc && ./build.mjs",
+ "test": "./test.mjs && mocha --require source-map-support/register 'dist/test/**/*.test.js' 'dist/test/**/test.js'",
+ "lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
+ "typedoc": "typedoc --out dist/typedoc ./src/",
+ "i18n:strings": "pogen extract && pogen merge",
+ "i18n:translations": "pogen emit",
+ "pretty": "prettier --write src"
+ },
+ "dependencies": {
+ "@gnu-taler/taler-util": "workspace:*",
+ "@gnu-taler/web-util": "workspace:*",
+ "date-fns": "2.29.3",
+ "jed": "1.1.1",
+ "preact": "10.11.3",
+ "qrcode-generator": "^1.4.4",
+ "swr": "2.0.3"
+ },
+ "devDependencies": {
+ "eslint": "^8.56.0",
+ "@typescript-eslint/eslint-plugin": "^6.19.0",
+ "@typescript-eslint/parser": "^6.19.0",
+ "eslint-config-prettier": "^9.1.0",
+ "eslint-plugin-react": "^7.33.2",
+ "@gnu-taler/pogen": "workspace:*",
+ "@tailwindcss/forms": "^0.5.3",
+ "@tailwindcss/typography": "^0.5.9",
+ "@types/chai": "^4.3.0",
+ "@types/history": "^4.7.8",
+ "@types/mocha": "^10.0.1",
+ "@types/node": "^18.11.17",
+ "autoprefixer": "^10.4.14",
+ "chai": "^4.3.6",
+ "esbuild": "^0.19.9",
+ "mocha": "9.2.0",
+ "po2json": "^0.4.5",
+ "tailwindcss": "^3.3.2",
+ "typescript": "5.3.3"
+ },
+ "pogen": {
+ "domain": "bank"
+ }
+}
diff --git a/packages/taler-wallet-core/src/util/assertUnreachable.ts b/packages/bank-ui/postcss.config.js
index 1819fd09e..c9a60a43c 100644
--- a/packages/taler-wallet-core/src/util/assertUnreachable.ts
+++ b/packages/bank-ui/postcss.config.js
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2019 GNUnet e.V.
+ (C) 2022-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -13,7 +13,9 @@
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-
-export function assertUnreachable(x: never): never {
- throw new Error(`Didn't expect to get here ${x}`);
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
}
diff --git a/packages/bank-ui/src/Routing.tsx b/packages/bank-ui/src/Routing.tsx
new file mode 100644
index 000000000..23635d4cd
--- /dev/null
+++ b/packages/bank-ui/src/Routing.tsx
@@ -0,0 +1,612 @@
+/*
+ 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 {
+ LocalNotificationBanner,
+ urlPattern,
+ useBankCoreApiContext,
+ useCurrentLocation,
+ useLocalNotification,
+ useNavigationContext,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+
+import {
+ AbsoluteTime,
+ AccessToken,
+ HttpStatusCode,
+ TranslatedString,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
+import { useEffect } from "preact/hooks";
+import { useSessionState } from "./hooks/session.js";
+import { AccountPage } from "./pages/AccountPage/index.js";
+import { BankFrame } from "./pages/BankFrame.js";
+import { LoginForm } from "./pages/LoginForm.js";
+import { PublicHistoriesPage } from "./pages/PublicHistoriesPage.js";
+import { RegistrationPage } from "./pages/RegistrationPage.js";
+import { ShowNotifications } from "./pages/ShowNotifications.js";
+import { SolveChallengePage } from "./pages/SolveChallengePage.js";
+import { WireTransfer } from "./pages/WireTransfer.js";
+import { WithdrawalOperationPage } from "./pages/WithdrawalOperationPage.js";
+import { CashoutListForAccount } from "./pages/account/CashoutListForAccount.js";
+import { ShowAccountDetails } from "./pages/account/ShowAccountDetails.js";
+import { UpdateAccountPassword } from "./pages/account/UpdateAccountPassword.js";
+import { AdminHome } from "./pages/admin/AdminHome.js";
+import { CreateNewAccount } from "./pages/admin/CreateNewAccount.js";
+import { DownloadStats } from "./pages/admin/DownloadStats.js";
+import { RemoveAccount } from "./pages/admin/RemoveAccount.js";
+import { ConversionConfig } from "./pages/regional/ConversionConfig.js";
+import { CreateCashout } from "./pages/regional/CreateCashout.js";
+import { ShowCashoutDetails } from "./pages/regional/ShowCashoutDetails.js";
+
+export function Routing(): VNode {
+ const session = useSessionState();
+
+ if (session.state.status === "loggedIn") {
+ const { isUserAdministrator, username } = session.state;
+ return (
+ <BankFrame
+ account={username}
+ routeAccountDetails={privatePages.myAccountDetails}
+ >
+ <PrivateRouting username={username} isAdmin={isUserAdministrator} />
+ </BankFrame>
+ );
+ }
+ return (
+ <BankFrame>
+ <PublicRounting
+ onLoggedUser={(username, token) => {
+ session.logIn({ username, token: token });
+ }}
+ />
+ </BankFrame>
+ );
+}
+
+const publicPages = {
+ login: urlPattern(/\/login/, () => "#/login"),
+ register: urlPattern(/\/register/, () => "#/register"),
+ publicAccounts: urlPattern(/\/public-accounts/, () => "#/public-accounts"),
+ operationDetails: urlPattern<{ wopid: string }>(
+ /\/operation\/(?<wopid>[a-zA-Z0-9-]+)/,
+ ({ wopid }) => `#/operation/${wopid}`,
+ ),
+ solveSecondFactor: urlPattern(/\/2fa/, () => "#/2fa"),
+};
+
+function PublicRounting({
+ onLoggedUser,
+}: {
+ onLoggedUser: (username: string, token: AccessToken) => void;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const location = useCurrentLocation(publicPages);
+ const { navigateTo } = useNavigationContext();
+ const { config, lib } = useBankCoreApiContext();
+ const [notification, notify, handleError] = useLocalNotification();
+
+ useEffect(() => {
+ if (location === undefined) {
+ navigateTo(publicPages.login.url({}));
+ }
+ }, [location]);
+
+ if (location === undefined) {
+ return <Fragment />;
+ }
+
+ async function doAutomaticLogin(username: string, password: string) {
+ await handleError(async () => {
+ const resp = await lib
+ .auth(username)
+ .createAccessTokenBasic(username, password, {
+ scope: "readwrite",
+ duration: { d_us: "forever" },
+ refreshable: true,
+ });
+ if (resp.type === "ok") {
+ onLoggedUser(username, resp.body.access_token);
+ } else {
+ switch (resp.case) {
+ case HttpStatusCode.Unauthorized:
+ return notify({
+ type: "error",
+ title: i18n.str`Wrong credentials for "${username}"`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case HttpStatusCode.NotFound:
+ return notify({
+ type: "error",
+ title: i18n.str`Account not found`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ default:
+ assertUnreachable(resp);
+ }
+ }
+ });
+ }
+
+ switch (location.name) {
+ case "login": {
+ return (
+ <Fragment>
+ <div class="sm:mx-auto sm:w-full sm:max-w-sm">
+ <h2 class="text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">{i18n.str`Welcome to ${config.bank_name}!`}</h2>
+ </div>
+ <LoginForm routeRegister={publicPages.register} />
+ </Fragment>
+ );
+ }
+ case "publicAccounts": {
+ return <PublicHistoriesPage />;
+ }
+ case "operationDetails": {
+ return (
+ <WithdrawalOperationPage
+ operationId={location.values.wopid}
+ routeWithdrawalDetails={publicPages.operationDetails}
+ purpose="after-confirmation"
+ onOperationAborted={() => navigateTo(publicPages.login.url({}))}
+ routeClose={publicPages.login}
+ onAuthorizationRequired={() =>
+ navigateTo(publicPages.solveSecondFactor.url({}))
+ }
+ />
+ );
+ }
+ case "register": {
+ return (
+ <Fragment>
+ <LocalNotificationBanner notification={notification} />
+ <RegistrationPage
+ onRegistrationSuccesful={doAutomaticLogin}
+ routeCancel={publicPages.login}
+ />
+ </Fragment>
+ );
+ }
+ case "solveSecondFactor": {
+ return (
+ <SolveChallengePage
+ onChallengeCompleted={() => navigateTo(publicPages.login.url({}))}
+ routeClose={publicPages.login}
+ />
+ );
+ }
+ default:
+ assertUnreachable(location);
+ }
+}
+
+export const privatePages = {
+ homeChargeWallet: urlPattern(
+ /\/account\/charge-wallet/,
+ () => "#/account/charge-wallet",
+ ),
+ homeWireTransfer: urlPattern<{
+ account?: string;
+ subject?: string;
+ amount?: string;
+ }>(/\/account\/wire-transfer/, () => "#/account/wire-transfer"),
+ home: urlPattern(/\/account/, () => "#/account"),
+ notifications: urlPattern(/\/notifications/, () => "#/notifications"),
+ solveSecondFactor: urlPattern(/\/2fa/, () => "#/2fa"),
+ cashoutCreate: urlPattern(/\/new-cashout/, () => "#/new-cashout"),
+ cashoutDetails: urlPattern<{ cid: string }>(
+ /\/cashout\/(?<cid>[a-zA-Z0-9]+)/,
+ ({ cid }) => `#/cashout/${cid}`,
+ ),
+ wireTranserCreate: urlPattern<{
+ account?: string;
+ subject?: string;
+ amount?: string;
+ }>(
+ /\/wire-transfer\/(?<account>[a-zA-Z0-9]+)/,
+ ({ account }) => `#/wire-transfer/${account}`,
+ ),
+ publicAccountList: urlPattern(/\/public-accounts/, () => "#/public-accounts"),
+ statsDownload: urlPattern(/\/download-stats/, () => "#/download-stats"),
+ accountCreate: urlPattern(/\/new-account/, () => "#/new-account"),
+ myAccountDelete: urlPattern(
+ /\/delete-my-account/,
+ () => "#/delete-my-account",
+ ),
+ myAccountDetails: urlPattern(/\/my-profile/, () => "#/my-profile"),
+ myAccountPassword: urlPattern(/\/my-password/, () => "#/my-password"),
+ myAccountCashouts: urlPattern(/\/my-cashouts/, () => "#/my-cashouts"),
+ conversionConfig: urlPattern(/\/conversion/, () => "#/conversion"),
+ accountDetails: urlPattern<{ account: string }>(
+ /\/profile\/(?<account>[a-zA-Z0-9_-]+)\/details/,
+ ({ account }) => `#/profile/${account}/details`,
+ ),
+ accountChangePassword: urlPattern<{ account: string }>(
+ /\/profile\/(?<account>[a-zA-Z0-9_-]+)\/change-password/,
+ ({ account }) => `#/profile/${account}/change-password`,
+ ),
+ accountDelete: urlPattern<{ account: string }>(
+ /\/profile\/(?<account>[a-zA-Z0-9_-]+)\/delete/,
+ ({ account }) => `#/profile/${account}/delete`,
+ ),
+ accountCashouts: urlPattern<{ account: string }>(
+ /\/profile\/(?<account>[a-zA-Z0-9_-]+)\/cashouts/,
+ ({ account }) => `#/profile/${account}/cashouts`,
+ ),
+ startOperation: urlPattern<{ wopid: string }>(
+ /\/start-operation\/(?<wopid>[a-zA-Z0-9-]+)/,
+ ({ wopid }) => `#/start-operation/${wopid}`,
+ ),
+ operationDetails: urlPattern<{ wopid: string }>(
+ /\/operation\/(?<wopid>[a-zA-Z0-9-]+)/,
+ ({ wopid }) => `#/operation/${wopid}`,
+ ),
+};
+
+function PrivateRouting({
+ username,
+ isAdmin,
+}: {
+ username: string;
+ isAdmin: boolean;
+}): VNode {
+ const { navigateTo } = useNavigationContext();
+ const location = useCurrentLocation(privatePages);
+ useEffect(() => {
+ if (location === undefined) {
+ navigateTo(privatePages.home.url({}));
+ }
+ }, [location]);
+
+ if (location === undefined) {
+ return <Fragment />;
+ }
+
+ switch (location.name) {
+ case "operationDetails": {
+ return (
+ <WithdrawalOperationPage
+ operationId={location.values.wopid}
+ routeWithdrawalDetails={privatePages.operationDetails}
+ purpose="after-confirmation"
+ onOperationAborted={() => navigateTo(privatePages.home.url({}))}
+ routeClose={privatePages.home}
+ onAuthorizationRequired={() =>
+ navigateTo(privatePages.solveSecondFactor.url({}))
+ }
+ />
+ );
+ }
+ case "startOperation": {
+ return (
+ <WithdrawalOperationPage
+ operationId={location.values.wopid}
+ routeWithdrawalDetails={privatePages.operationDetails}
+ purpose="after-creation"
+ onOperationAborted={() => navigateTo(privatePages.home.url({}))}
+ routeClose={privatePages.home}
+ onAuthorizationRequired={() =>
+ navigateTo(privatePages.solveSecondFactor.url({}))
+ }
+ />
+ );
+ }
+ case "solveSecondFactor": {
+ return (
+ <SolveChallengePage
+ onChallengeCompleted={() => navigateTo(privatePages.home.url({}))}
+ routeClose={privatePages.home}
+ />
+ );
+ }
+ case "publicAccountList": {
+ return <PublicHistoriesPage />;
+ }
+ case "statsDownload": {
+ return <DownloadStats routeCancel={privatePages.home} />;
+ }
+ case "accountCreate": {
+ return (
+ <CreateNewAccount
+ routeCancel={privatePages.home}
+ onCreateSuccess={() => navigateTo(privatePages.home.url({}))}
+ />
+ );
+ }
+ case "accountDetails": {
+ return (
+ <ShowAccountDetails
+ account={location.values.account}
+ onUpdateSuccess={() => navigateTo(privatePages.home.url({}))}
+ routeHere={privatePages.accountDetails}
+ routeMyAccountCashout={privatePages.myAccountCashouts}
+ routeMyAccountDelete={privatePages.myAccountDelete}
+ routeMyAccountDetails={privatePages.myAccountDetails}
+ routeMyAccountPassword={privatePages.myAccountPassword}
+ routeConversionConfig={privatePages.conversionConfig}
+ onAuthorizationRequired={() =>
+ navigateTo(privatePages.solveSecondFactor.url({}))
+ }
+ routeClose={privatePages.home}
+ />
+ );
+ }
+ case "accountChangePassword": {
+ return (
+ <UpdateAccountPassword
+ focus
+ account={location.values.account}
+ routeHere={privatePages.accountChangePassword}
+ onUpdateSuccess={() => navigateTo(privatePages.home.url({}))}
+ routeMyAccountCashout={privatePages.myAccountCashouts}
+ routeMyAccountDelete={privatePages.myAccountDelete}
+ routeMyAccountDetails={privatePages.myAccountDetails}
+ routeMyAccountPassword={privatePages.myAccountPassword}
+ routeConversionConfig={privatePages.conversionConfig}
+ onAuthorizationRequired={() =>
+ navigateTo(privatePages.solveSecondFactor.url({}))
+ }
+ routeClose={privatePages.home}
+ />
+ );
+ }
+ case "accountDelete": {
+ return (
+ <RemoveAccount
+ account={location.values.account}
+ routeHere={privatePages.accountDelete}
+ onUpdateSuccess={() => navigateTo(privatePages.home.url({}))}
+ onAuthorizationRequired={() =>
+ navigateTo(privatePages.solveSecondFactor.url({}))
+ }
+ routeCancel={privatePages.home}
+ />
+ );
+ }
+ case "accountCashouts": {
+ return (
+ <CashoutListForAccount
+ account={location.values.account}
+ routeCreateCashout={privatePages.cashoutCreate}
+ routeCashoutDetails={privatePages.cashoutDetails}
+ routeClose={privatePages.home}
+ routeMyAccountCashout={privatePages.myAccountCashouts}
+ routeMyAccountDelete={privatePages.myAccountDelete}
+ routeMyAccountDetails={privatePages.myAccountDetails}
+ routeMyAccountPassword={privatePages.myAccountPassword}
+ routeConversionConfig={privatePages.conversionConfig}
+ onAuthorizationRequired={() =>
+ navigateTo(privatePages.solveSecondFactor.url({}))
+ }
+ />
+ );
+ }
+ case "myAccountDelete": {
+ return (
+ <RemoveAccount
+ account={username}
+ routeHere={privatePages.accountDelete}
+ onUpdateSuccess={() => navigateTo(privatePages.home.url({}))}
+ onAuthorizationRequired={() =>
+ navigateTo(privatePages.solveSecondFactor.url({}))
+ }
+ routeCancel={privatePages.home}
+ />
+ );
+ }
+ case "myAccountDetails": {
+ return (
+ <ShowAccountDetails
+ account={username}
+ routeHere={privatePages.accountDetails}
+ onUpdateSuccess={() => navigateTo(privatePages.home.url({}))}
+ routeMyAccountCashout={privatePages.myAccountCashouts}
+ routeConversionConfig={privatePages.conversionConfig}
+ routeMyAccountDelete={privatePages.myAccountDelete}
+ routeMyAccountDetails={privatePages.myAccountDetails}
+ routeMyAccountPassword={privatePages.myAccountPassword}
+ onAuthorizationRequired={() =>
+ navigateTo(privatePages.solveSecondFactor.url({}))
+ }
+ routeClose={privatePages.home}
+ />
+ );
+ }
+ case "myAccountPassword": {
+ return (
+ <UpdateAccountPassword
+ focus
+ account={username}
+ routeHere={privatePages.accountChangePassword}
+ onUpdateSuccess={() => navigateTo(privatePages.home.url({}))}
+ routeMyAccountCashout={privatePages.myAccountCashouts}
+ routeMyAccountDelete={privatePages.myAccountDelete}
+ routeMyAccountDetails={privatePages.myAccountDetails}
+ routeMyAccountPassword={privatePages.myAccountPassword}
+ routeConversionConfig={privatePages.conversionConfig}
+ onAuthorizationRequired={() =>
+ navigateTo(privatePages.solveSecondFactor.url({}))
+ }
+ routeClose={privatePages.home}
+ />
+ );
+ }
+ case "myAccountCashouts": {
+ return (
+ <CashoutListForAccount
+ account={username}
+ routeCashoutDetails={privatePages.cashoutDetails}
+ routeCreateCashout={privatePages.cashoutCreate}
+ routeMyAccountCashout={privatePages.myAccountCashouts}
+ routeMyAccountDelete={privatePages.myAccountDelete}
+ routeMyAccountDetails={privatePages.myAccountDetails}
+ routeMyAccountPassword={privatePages.myAccountPassword}
+ routeConversionConfig={privatePages.conversionConfig}
+ onAuthorizationRequired={() =>
+ navigateTo(privatePages.solveSecondFactor.url({}))
+ }
+ routeClose={privatePages.home}
+ />
+ );
+ }
+ case "home": {
+ if (isAdmin) {
+ return (
+ <AdminHome
+ onAuthorizationRequired={() =>
+ navigateTo(privatePages.solveSecondFactor.url({}))
+ }
+ routeCreate={privatePages.accountCreate}
+ routeRemoveAccount={privatePages.accountDelete}
+ routeShowAccount={privatePages.accountDetails}
+ routeShowCashoutsAccount={privatePages.accountCashouts}
+ routeUpdatePasswordAccount={privatePages.accountChangePassword}
+ routeCreateWireTransfer={privatePages.wireTranserCreate}
+ routeDownloadStats={privatePages.statsDownload}
+ />
+ );
+ }
+ return (
+ <AccountPage
+ account={username}
+ tab={undefined}
+ routeCreateWireTransfer={privatePages.wireTranserCreate}
+ routePublicAccounts={privatePages.publicAccountList}
+ routeOperationDetails={privatePages.startOperation}
+ routeChargeWallet={privatePages.homeChargeWallet}
+ routeWireTransfer={privatePages.homeWireTransfer}
+ routeSolveSecondFactor={privatePages.solveSecondFactor}
+ routeCashout={privatePages.myAccountCashouts}
+ routeClose={privatePages.home}
+ onClose={() => navigateTo(privatePages.home.url({}))}
+ onAuthorizationRequired={() =>
+ navigateTo(privatePages.solveSecondFactor.url({}))
+ }
+ onOperationCreated={(wopid) =>
+ navigateTo(privatePages.startOperation.url({ wopid }))
+ }
+ />
+ );
+ }
+ case "cashoutCreate": {
+ return (
+ <CreateCashout
+ account={username}
+ routeHere={privatePages.cashoutCreate}
+ onAuthorizationRequired={() =>
+ navigateTo(privatePages.solveSecondFactor.url({}))
+ }
+ routeClose={privatePages.home}
+ />
+ );
+ }
+ case "cashoutDetails": {
+ return (
+ <ShowCashoutDetails
+ id={location.values.cid}
+ routeClose={privatePages.myAccountCashouts}
+ />
+ );
+ }
+ case "wireTranserCreate": {
+ return (
+ <WireTransfer
+ toAccount={location.values.account}
+ withAmount={location.values.amount}
+ withSubject={location.values.subject}
+ routeHere={privatePages.wireTranserCreate}
+ onAuthorizationRequired={() =>
+ navigateTo(privatePages.solveSecondFactor.url({}))
+ }
+ routeCancel={privatePages.home}
+ onSuccess={() => navigateTo(privatePages.home.url({}))}
+ />
+ );
+ }
+ case "homeChargeWallet": {
+ return (
+ <AccountPage
+ account={username}
+ tab="charge-wallet"
+ routeChargeWallet={privatePages.homeChargeWallet}
+ routeWireTransfer={privatePages.homeWireTransfer}
+ routeCreateWireTransfer={privatePages.wireTranserCreate}
+ routePublicAccounts={privatePages.publicAccountList}
+ routeOperationDetails={privatePages.startOperation}
+ routeCashout={privatePages.myAccountCashouts}
+ routeSolveSecondFactor={privatePages.solveSecondFactor}
+ routeClose={privatePages.home}
+ onClose={() => navigateTo(privatePages.home.url({}))}
+ onAuthorizationRequired={() =>
+ navigateTo(privatePages.solveSecondFactor.url({}))
+ }
+ onOperationCreated={(wopid) =>
+ navigateTo(privatePages.startOperation.url({ wopid }))
+ }
+ />
+ );
+ }
+ case "conversionConfig": {
+ return (
+ <ConversionConfig
+ routeMyAccountCashout={privatePages.myAccountCashouts}
+ routeMyAccountDelete={privatePages.myAccountDelete}
+ routeMyAccountDetails={privatePages.myAccountDetails}
+ routeMyAccountPassword={privatePages.myAccountPassword}
+ routeConversionConfig={privatePages.conversionConfig}
+ routeCancel={privatePages.home}
+ onUpdateSuccess={() => {
+ navigateTo(privatePages.home.url({}));
+ }}
+ />
+ );
+ }
+ case "homeWireTransfer": {
+ return (
+ <AccountPage
+ account={username}
+ tab="wire-transfer"
+ routeChargeWallet={privatePages.homeChargeWallet}
+ routeWireTransfer={privatePages.homeWireTransfer}
+ routeCreateWireTransfer={privatePages.wireTranserCreate}
+ routePublicAccounts={privatePages.publicAccountList}
+ routeOperationDetails={privatePages.startOperation}
+ routeSolveSecondFactor={privatePages.solveSecondFactor}
+ routeCashout={privatePages.myAccountCashouts}
+ routeClose={privatePages.home}
+ onClose={() => navigateTo(privatePages.home.url({}))}
+ onAuthorizationRequired={() =>
+ navigateTo(privatePages.solveSecondFactor.url({}))
+ }
+ onOperationCreated={(wopid) =>
+ navigateTo(privatePages.startOperation.url({ wopid }))
+ }
+ />
+ );
+ }
+ case "notifications": {
+ return <ShowNotifications />;
+ }
+ default:
+ assertUnreachable(location);
+ }
+}
diff --git a/packages/bank-ui/src/app.tsx b/packages/bank-ui/src/app.tsx
new file mode 100644
index 000000000..29dabddd6
--- /dev/null
+++ b/packages/bank-ui/src/app.tsx
@@ -0,0 +1,231 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ CacheEvictor,
+ TalerBankConversionCacheEviction,
+ TalerCoreBankCacheEviction,
+ assertUnreachable,
+ canonicalizeBaseUrl,
+ getGlobalLogLevel,
+ setGlobalLogLevelFromString,
+} from "@gnu-taler/taler-util";
+import {
+ BankApiProvider,
+ BrowserHashNavigationProvider,
+ Loading,
+ TalerWalletIntegrationBrowserProvider,
+ TranslationProvider,
+} from "@gnu-taler/web-util/browser";
+import { h } from "preact";
+import { useEffect, useState } from "preact/hooks";
+import { SWRConfig } from "swr";
+import { Routing } from "./Routing.js";
+import { SettingsProvider } from "./context/settings.js";
+import { strings } from "./i18n/strings.js";
+import { BankFrame } from "./pages/BankFrame.js";
+import { BankUiSettings, fetchSettings } from "./settings.js";
+import {
+ revalidateAccountDetails,
+ revalidatePublicAccounts,
+ revalidateTransactions,
+} from "./hooks/account.js";
+import {
+ revalidateBusinessAccounts,
+ revalidateCashouts,
+ revalidateConversionInfo,
+} from "./hooks/regional.js";
+const WITH_LOCAL_STORAGE_CACHE = false;
+
+export function App() {
+ const [settings, setSettings] = useState<BankUiSettings>();
+ useEffect(() => {
+ fetchSettings(setSettings);
+ }, []);
+ if (!settings) return <Loading />;
+
+ const baseUrl = getInitialBackendBaseURL(settings.backendBaseURL);
+ return (
+ <SettingsProvider value={settings}>
+ <TranslationProvider
+ source={strings}
+ completeness={{
+ es: strings["es"].completeness,
+ de: strings["de"].completeness,
+ }}
+ >
+ <BankApiProvider
+ baseUrl={new URL("/", baseUrl)}
+ frameOnError={BankFrame}
+ evictors={{
+ bank: evictBankSwrCache,
+ conversion: evictConversionSwrCache,
+ }}
+ >
+ <SWRConfig
+ value={{
+ provider: WITH_LOCAL_STORAGE_CACHE
+ ? localStorageProvider
+ : undefined,
+ // normally, do not revalidate
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ revalidateIfStale: false,
+ revalidateOnMount: undefined,
+ focusThrottleInterval: undefined,
+
+ // normally, do not refresh
+ refreshInterval: undefined,
+ dedupingInterval: 2000,
+ refreshWhenHidden: false,
+ refreshWhenOffline: false,
+
+ // ignore errors
+ shouldRetryOnError: false,
+ errorRetryCount: 0,
+ errorRetryInterval: undefined,
+
+ // do not go to loading again if already has data
+ keepPreviousData: true,
+ }}
+ >
+ <TalerWalletIntegrationBrowserProvider>
+ <BrowserHashNavigationProvider>
+ <Routing />
+ </BrowserHashNavigationProvider>
+ </TalerWalletIntegrationBrowserProvider>
+ </SWRConfig>
+ </BankApiProvider>
+ </TranslationProvider>
+ </SettingsProvider>
+ );
+}
+
+// @ts-expect-error creating a new property for window object
+window.setGlobalLogLevelFromString = setGlobalLogLevelFromString;
+// @ts-expect-error creating a new property for window object
+window.getGlobalLevel = getGlobalLogLevel;
+
+function localStorageProvider(): Map<unknown, unknown> {
+ const map = new Map(JSON.parse(localStorage.getItem("app-cache") || "[]"));
+
+ window.addEventListener("beforeunload", () => {
+ const appCache = JSON.stringify(Array.from(map.entries()));
+ localStorage.setItem("app-cache", appCache);
+ });
+ return map;
+}
+
+function getInitialBackendBaseURL(
+ backendFromSettings: string | undefined,
+): string {
+ const overrideUrl =
+ typeof localStorage !== "undefined"
+ ? localStorage.getItem("corebank-api-base-url")
+ : undefined;
+ let result: string;
+
+ if (!overrideUrl) {
+ // normal path
+ if (!backendFromSettings) {
+ console.error(
+ "ERROR: backendBaseURL was overridden by a setting file and missing. Setting value to 'window.origin'",
+ );
+ result = window.origin;
+ } else {
+ result = backendFromSettings;
+ }
+ } else {
+ // testing/development path
+ result = overrideUrl;
+ }
+ try {
+ return canonicalizeBaseUrl(result);
+ } catch (e) {
+ // fall back
+ return canonicalizeBaseUrl(window.origin);
+ }
+}
+
+const evictBankSwrCache: CacheEvictor<TalerCoreBankCacheEviction> = {
+ async notifySuccess(op) {
+ switch (op) {
+ case TalerCoreBankCacheEviction.DELETE_ACCOUNT: {
+ await Promise.all([
+ revalidatePublicAccounts(),
+ revalidateBusinessAccounts(),
+ ]);
+ return;
+ }
+ case TalerCoreBankCacheEviction.CREATE_ACCOUNT: {
+ // admin balance change on new account
+ await Promise.all([
+ revalidateAccountDetails(),
+ revalidateTransactions(),
+ revalidatePublicAccounts(),
+ revalidateBusinessAccounts(),
+ ]);
+ return;
+ }
+ case TalerCoreBankCacheEviction.UPDATE_ACCOUNT: {
+ await Promise.all([revalidateAccountDetails()]);
+ return;
+ }
+ case TalerCoreBankCacheEviction.CREATE_TRANSACTION: {
+ await Promise.all([
+ revalidateAccountDetails(),
+ revalidateTransactions(),
+ ]);
+ return;
+ }
+ case TalerCoreBankCacheEviction.CONFIRM_WITHDRAWAL: {
+ await Promise.all([
+ revalidateAccountDetails(),
+ revalidateTransactions(),
+ ]);
+ return;
+ }
+ case TalerCoreBankCacheEviction.CREATE_CASHOUT: {
+ await Promise.all([
+ revalidateAccountDetails(),
+ revalidateCashouts(),
+ revalidateTransactions(),
+ ]);
+ return;
+ }
+ case TalerCoreBankCacheEviction.UPDATE_PASSWORD:
+ case TalerCoreBankCacheEviction.ABORT_WITHDRAWAL:
+ case TalerCoreBankCacheEviction.CREATE_WITHDRAWAL:
+ return;
+ default:
+ assertUnreachable(op);
+ }
+ },
+};
+
+const evictConversionSwrCache: CacheEvictor<TalerBankConversionCacheEviction> =
+ {
+ async notifySuccess(op) {
+ switch (op) {
+ case TalerBankConversionCacheEviction.UPDATE_RATE: {
+ await revalidateConversionInfo();
+ return;
+ }
+ default:
+ assertUnreachable(op);
+ }
+ },
+ };
diff --git a/packages/bank-ui/src/assets/empty.png b/packages/bank-ui/src/assets/empty.png
new file mode 100644
index 000000000..5120d3138
--- /dev/null
+++ b/packages/bank-ui/src/assets/empty.png
Binary files differ
diff --git a/packages/demobank-ui/src/assets/example/id1.jpg b/packages/bank-ui/src/assets/example/id1.jpg
index 5d022a379..5d022a379 100644
--- a/packages/demobank-ui/src/assets/example/id1.jpg
+++ b/packages/bank-ui/src/assets/example/id1.jpg
Binary files differ
diff --git a/packages/demobank-ui/src/assets/favicon.ico b/packages/bank-ui/src/assets/favicon.ico
index 07419145b..07419145b 100644
--- a/packages/demobank-ui/src/assets/favicon.ico
+++ b/packages/bank-ui/src/assets/favicon.ico
Binary files differ
diff --git a/packages/bank-ui/src/assets/icons/android-chrome-192x192.png b/packages/bank-ui/src/assets/icons/android-chrome-192x192.png
new file mode 100644
index 000000000..93ebe2e2c
--- /dev/null
+++ b/packages/bank-ui/src/assets/icons/android-chrome-192x192.png
Binary files differ
diff --git a/packages/bank-ui/src/assets/icons/android-chrome-512x512.png b/packages/bank-ui/src/assets/icons/android-chrome-512x512.png
new file mode 100644
index 000000000..52d1623ea
--- /dev/null
+++ b/packages/bank-ui/src/assets/icons/android-chrome-512x512.png
Binary files differ
diff --git a/packages/bank-ui/src/assets/icons/apple-touch-icon.png b/packages/bank-ui/src/assets/icons/apple-touch-icon.png
new file mode 100644
index 000000000..254e4bb4d
--- /dev/null
+++ b/packages/bank-ui/src/assets/icons/apple-touch-icon.png
Binary files differ
diff --git a/packages/bank-ui/src/assets/icons/favicon-16x16.png b/packages/bank-ui/src/assets/icons/favicon-16x16.png
new file mode 100644
index 000000000..e81177dcb
--- /dev/null
+++ b/packages/bank-ui/src/assets/icons/favicon-16x16.png
Binary files differ
diff --git a/packages/bank-ui/src/assets/icons/favicon-32x32.png b/packages/bank-ui/src/assets/icons/favicon-32x32.png
new file mode 100644
index 000000000..40e9b5b47
--- /dev/null
+++ b/packages/bank-ui/src/assets/icons/favicon-32x32.png
Binary files differ
diff --git a/packages/bank-ui/src/assets/icons/languageicon.svg b/packages/bank-ui/src/assets/icons/languageicon.svg
new file mode 100644
index 000000000..22d58da65
--- /dev/null
+++ b/packages/bank-ui/src/assets/icons/languageicon.svg
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 2411.2 2794" style="enable-background:new 0 0 2411.2 2794;" xml:space="preserve">
+<style type="text/css">
+ .st0{fill:#FFFFFF;}
+ .st1{fill-rule:evenodd;clip-rule:evenodd;}
+ .st2{fill-rule:evenodd;clip-rule:evenodd;fill:#FFFFFF;}
+</style>
+<g id="Layer_2">
+</g>
+<g id="Layer_x5F_1_x5F_1">
+ <g>
+ <polygon points="1204.6,359.2 271.8,30 271.8,2060.1 1204.6,1758.3 "/>
+ <polygon class="st0" points="1182.2,358.1 2150.6,29 2150.6,2059 1182.2,1757.3 "/>
+ <polygon class="st0" points="30,2415.4 1182.2,2031.4 1182.2,357.9 30,742 "/>
+ <polygon points="1707.2,2440.7 1870.5,2709.4 1956.6,2459.8 "/>
+ <g>
+ <path d="M421.7,934.8c-6.1-6,8,49.1,27.6,68.9c34.8,35.1,61.9,39.6,76.4,40.2c32,1.3,71.5-8,94.9-17.8
+ c22.7-9.7,62.4-30,77.5-59.6c3.2-6.3,11.9-17,6.4-43.2c-4.2-20.2-17-27.3-32.7-26.2c-15.7,1.1-63.2,13.7-86.1,20.8
+ c-23,7-70.3,21.4-90.9,25.8C474.3,948.2,429,941.7,421.7,934.8z"/>
+ <path d="M1003.1,1593.7c-9.1-3.3-196.9-81.1-223.6-93.9c-21.8-10.5-75.2-33.1-100.4-43.3c70.8-109.2,115.5-191.6,121.5-204.1
+ c11-23,86-169.6,87.7-178.7c1.7-9.1,3.8-42.9,2.2-51c-1.7-8.2-29.1,7.6-66.4,20.2c-37.4,12.6-108.4,58.8-135.8,64.6
+ c-27.5,5.7-115.5,39.1-160.5,54c-45,14.9-130.2,40.9-165.2,50.4c-35.1,9.5-65.7,10.2-85.3,16.2c0,0,2.6,27.5,7.8,35.7
+ c5.2,8.2,23.7,28.4,45.3,34.1c21.6,5.7,57.3,3.4,73.6-0.3c16.3-3.8,44.4-17.5,48.2-23.6c3.8-6.1-2-24.9,4.5-30.6
+ c6.5-5.6,92.2-25.7,124.6-35.4c32.4-10,156.3-52.6,173.1-50.5c-5.3,17.7-105,215.1-137.1,274c-32.1,58.9-218.6,318-258.3,363.6
+ c-30.1,34.7-103.2,123.5-128.5,143.6c6.4,1.8,51.6-2.1,59.9-7.2c51.3-31.6,136.9-138.1,164.4-170.5
+ c81.9-96,153.8-196.8,210.8-283.4h0.1c11.1,4.6,100.9,77.8,124.4,94c23.4,16.2,115.9,67.8,136,76.4c20,8.7,97.1,44.2,100.3,32.2
+ C1029.4,1668,1012.2,1597.1,1003.1,1593.7z"/>
+ </g>
+ <path class="st1" d="M569,2572c18,11,35,20,54,29c38,19,81,39,122,54c56,21,112,38,168,51c31,7,65,13,98,18c3,0,92,11,110,11h90
+ c35-3,68-5,103-10c28-4,59-9,89-16c22-5,45-10,67-17c21-6,45-14,68-22c15-5,31-12,47-18c13-6,29-13,44-19c18-8,39-19,59-29
+ c16-8,34-18,51-28c13-7,43-30,59-30c18,0,30,16,30,30c0,29-39,38-57,51c-19,13-42,23-62,34c-40,21-81,39-120,54
+ c-51,19-107,37-157,49c-19,4-38,9-57,12c-10,2-114,18-143,18h-132c-35-3-72-7-107-12c-31-5-64-11-95-18c-24-5-50-12-73-19
+ c-40-11-79-25-117-40c-69-26-141-60-209-105c-12-8-13-16-13-25c0-15,11-29,29-29C531,2546,563,2569,569,2572z"/>
+ <path class="st1" d="M1151,2009L61,2372V764l1090-363V2009z M1212,354v1680c-1,5-3,10-7,15c-2,3-6,7-9,8c-25,10-1151,388-1166,388
+ c-12,0-23-8-29-21c0-1-1-2-1-4V739c2-5,3-12,7-16c8-11,22-13,31-16c17-6,1126-378,1142-378C1190,329,1212,336,1212,354z"/>
+ <path class="st1" d="M2120,2017l-907-282V380l907-308V2017z M2181,32v2023c-1,23-17,33-32,33c-13,0-107-32-123-37
+ c-126-39-253-78-378-117c-28-9-57-18-84-27c-24-7-50-15-74-23c-107-33-216-66-323-102c-4-1-14-15-14-18V351c2-5,4-11,9-15
+ c8-9,351-123,486-168c36-13,487-168,501-168C2167,0,2181,13,2181,32z"/>
+ <polygon points="2411.2,2440.7 1199.5,2054.5 1204.6,373.2 2411.2,757.2 "/>
+ <g>
+ <path class="st2" d="M1800.3,1124.6L1681.4,1412l218.6,66.3L1800.3,1124.6z M1729,853.2l156.1,47.3l284.4,1025l-160.3-48.7
+ l-57.6-210.4L1620.2,1566l-71.3,171.4l-160.4-48.7L1729,853.2z"/>
+ </g>
+ </g>
+</g>
+</svg>
diff --git a/packages/bank-ui/src/assets/icons/mstile-150x150.png b/packages/bank-ui/src/assets/icons/mstile-150x150.png
new file mode 100644
index 000000000..9cfb889be
--- /dev/null
+++ b/packages/bank-ui/src/assets/icons/mstile-150x150.png
Binary files differ
diff --git a/packages/bank-ui/src/assets/logo-2021.svg b/packages/bank-ui/src/assets/logo-2021.svg
new file mode 100644
index 000000000..8c5ff3e5b
--- /dev/null
+++ b/packages/bank-ui/src/assets/logo-2021.svg
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 201 90">
+ <g fill="#0042b3" fill-rule="evenodd" stroke-width=".3">
+ <path d="M86.7 1.1c15.6 0 29 9.4 36 23.2h-5.9A35.1 35.1 0 0086.7 6.5C67 6.5 51 23.6 51 44.7c0 10.4 3.8 19.7 10 26.6a31.4 31.4 0 01-4.2 3A45.2 45.2 0 0146 44.7c0-24 18.2-43.6 40.7-43.6zm35.8 64.3a40.4 40.4 0 01-39 22.8c3-1.5 6-3.5 8.6-5.7a35.6 35.6 0 0024.6-17.1z" />
+ <path d="M64.2 1.1l3.1.1c-3 1.6-5.9 3.5-8.5 5.8a37.5 37.5 0 00-30.2 37.7c0 14.3 7.3 26.7 18 33.3a29.6 29.6 0 01-8.5.2c-9-8-14.6-20-14.6-33.5 0-24 18.2-43.6 40.7-43.6zm5.4 81.4a35.6 35.6 0 0024.6-17.1h5.9a40.4 40.4 0 01-39 22.8c3-1.5 5.9-3.5 8.5-5.7zm24.8-58.2a37 37 0 00-12.6-12.8 29.6 29.6 0 018.5-.2c4 3.6 7.4 8 9.9 13z" />
+ <path d="M41.8 1.1c1 0 2 0 3.1.2-3 1.5-5.9 3.4-8.5 5.6A37.5 37.5 0 006.1 44.7c0 21.1 16 38.3 35.7 38.3 12.6 0 23.6-7 30-17.6h5.8a40.4 40.4 0 01-35.8 23C19.3 88.4 1 68.8 1 44.7c0-24 18.2-43.6 40.7-43.6zm30.1 23.2a38.1 38.1 0 00-4.5-6.1c1.3-1.2 2.7-2.2 4.3-3 2.3 2.7 4.4 5.8 6 9.1z" />
+ </g>
+ <path d="M76.1 34.4h9.2v-5H61.9v5H71v26h5.1zM92.6 52.9h13.7l3 7.4h5.3l-12.7-31.2h-4.7L84.5 60.3h5.2zm11.8-4.9h-9.9l5-12.4zM123.8 29.4h-4.6v31h20.6v-5h-16zM166.5 29.4H145v31h21.6v-5H150v-8.3h14.5v-4.9h-14.5v-8h16.4zM191.2 39.5c0 1.6-.5 2.8-1.6 3.8s-2.6 1.4-4.4 1.4h-7.4V34.3h7.4c1.9 0 3.4.4 4.4 1.3 1 .9 1.6 2.2 1.6 3.9zm6 20.8l-7.7-11.7c1-.3 1.9-.7 2.7-1.3a8.8 8.8 0 003.6-4.6c.4-1 .5-2.2.5-3.5 0-1.5-.2-2.9-.7-4.1a8.4 8.4 0 00-2.1-3.1c-1-.8-2-1.5-3.4-2-1.3-.4-2.8-.6-4.5-.6h-12.9v31h5V49.4h6.5l7 10.8z" />
+</svg> \ No newline at end of file
diff --git a/packages/demobank-ui/src/assets/logo-white.svg b/packages/bank-ui/src/assets/logo-white.svg
index cb1f023c5..cb1f023c5 100644
--- a/packages/demobank-ui/src/assets/logo-white.svg
+++ b/packages/bank-ui/src/assets/logo-white.svg
diff --git a/packages/bank-ui/src/assets/logo.jpeg b/packages/bank-ui/src/assets/logo.jpeg
new file mode 100644
index 000000000..489832f7c
--- /dev/null
+++ b/packages/bank-ui/src/assets/logo.jpeg
Binary files differ
diff --git a/packages/bank-ui/src/components/Cashouts/index.ts b/packages/bank-ui/src/components/Cashouts/index.ts
new file mode 100644
index 000000000..99a946865
--- /dev/null
+++ b/packages/bank-ui/src/components/Cashouts/index.ts
@@ -0,0 +1,85 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { Loading, RouteDefinition, utils } from "@gnu-taler/web-util/browser";
+import {
+ AbsoluteTime,
+ AmountJson,
+ TalerCoreBankErrorsByMethod,
+ TalerCorebankApi,
+ TalerError,
+} from "@gnu-taler/taler-util";
+import { ErrorLoadingWithDebug } from "../ErrorLoadingWithDebug.js";
+import { useComponentState } from "./state.js";
+import { FailedView, ReadyView } from "./views.js";
+
+export interface Props {
+ account: string;
+ routeCashoutDetails: RouteDefinition<{ cid: string }>;
+}
+
+export type State =
+ | State.Loading
+ | State.Failed
+ | State.LoadingUriError
+ | State.Ready;
+
+export namespace State {
+ export interface Loading {
+ status: "loading";
+ error: undefined;
+ }
+
+ export interface LoadingUriError {
+ status: "loading-error";
+ error: TalerError;
+ }
+
+ export interface Failed {
+ status: "failed";
+ error: TalerCoreBankErrorsByMethod<"getAccountCashouts">;
+ }
+
+ export interface BaseInfo {
+ error: undefined;
+ }
+ export interface Ready extends BaseInfo {
+ status: "ready";
+ error: undefined;
+ cashouts: (TalerCorebankApi.CashoutStatusResponse & { id: number })[];
+ routeCashoutDetails: RouteDefinition<{ cid: string }>;
+ }
+}
+
+export interface Transaction {
+ negative: boolean;
+ counterpart: string;
+ when: AbsoluteTime;
+ amount: AmountJson | undefined;
+ subject: string;
+}
+
+const viewMapping: utils.StateViewMap<State> = {
+ loading: Loading,
+ "loading-error": ErrorLoadingWithDebug,
+ failed: FailedView,
+ ready: ReadyView,
+};
+
+export const Cashouts = utils.compose(
+ (p: Props) => useComponentState(p),
+ viewMapping,
+);
diff --git a/packages/bank-ui/src/components/Cashouts/state.ts b/packages/bank-ui/src/components/Cashouts/state.ts
new file mode 100644
index 000000000..8616faa1b
--- /dev/null
+++ b/packages/bank-ui/src/components/Cashouts/state.ts
@@ -0,0 +1,51 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { TalerError } from "@gnu-taler/taler-util";
+import { useCashouts } from "../../hooks/regional.js";
+import { Props, State } from "./index.js";
+
+export function useComponentState({
+ account,
+ routeCashoutDetails,
+}: Props): State {
+ const result = useCashouts(account);
+ if (!result) {
+ return {
+ status: "loading",
+ error: undefined,
+ };
+ }
+ if (result instanceof TalerError) {
+ return {
+ status: "loading-error",
+ error: result,
+ };
+ }
+ if (result.type === "fail") {
+ return {
+ status: "failed",
+ error: result,
+ };
+ }
+
+ return {
+ status: "ready",
+ error: undefined,
+ cashouts: result.body.cashouts,
+ routeCashoutDetails,
+ };
+}
diff --git a/packages/bank-ui/src/components/Cashouts/stories.tsx b/packages/bank-ui/src/components/Cashouts/stories.tsx
new file mode 100644
index 000000000..37ab64108
--- /dev/null
+++ b/packages/bank-ui/src/components/Cashouts/stories.tsx
@@ -0,0 +1,29 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import * as tests from "@gnu-taler/web-util/testing";
+import { ReadyView } from "./views.js";
+
+export default {
+ title: "transaction list",
+};
+
+export const Ready = tests.createExample(ReadyView, {});
diff --git a/packages/bank-ui/src/components/Cashouts/test.ts b/packages/bank-ui/src/components/Cashouts/test.ts
new file mode 100644
index 000000000..4ed0d7c11
--- /dev/null
+++ b/packages/bank-ui/src/components/Cashouts/test.ts
@@ -0,0 +1,68 @@
+/*
+ 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/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import * as tests from "@gnu-taler/web-util/testing";
+import { SwrMockEnvironment } from "@gnu-taler/web-util/testing";
+import { expect } from "chai";
+import { Props } from "./index.js";
+import { useComponentState } from "./state.js";
+import { buildNullRoutDefinition } from "@gnu-taler/web-util/browser";
+
+describe("Cashout states", () => {
+ it.skip("should query backend and render transactions", async () => {
+ const env = new SwrMockEnvironment();
+
+ const props: Props = {
+ account: "123",
+ routeCashoutDetails: buildNullRoutDefinition(),
+ };
+
+ // env.addRequestExpectation(CASHOUT_API_EXAMPLE.LIST_FIRST_PAGE, {
+ // response: {
+ // cashouts: [],
+ // },
+ // });
+
+ // env.addRequestExpectation(CASHOUT_API_EXAMPLE.MULTI_GET_EMPTY_FIRST_PAGE, {
+ // response: [],
+ // });
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentState,
+ props,
+ [
+ ({ status, error }) => {
+ expect(status).equals("loading");
+ expect(error).undefined;
+ },
+ ({ status, error }) => {
+ expect(status).equals("ready");
+ expect(error).undefined;
+ },
+ ],
+ env.buildTestingContext(),
+ );
+
+ expect(hookBehavior).deep.eq({ result: "ok" });
+
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+});
diff --git a/packages/bank-ui/src/components/Cashouts/views.tsx b/packages/bank-ui/src/components/Cashouts/views.tsx
new file mode 100644
index 000000000..22b8d8c1b
--- /dev/null
+++ b/packages/bank-ui/src/components/Cashouts/views.tsx
@@ -0,0 +1,218 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ AbsoluteTime,
+ Amounts,
+ HttpStatusCode,
+ TalerError,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
+import {
+ Attention,
+ Loading,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { format } from "date-fns";
+import { Fragment, VNode, h } from "preact";
+import { useConversionInfo } from "../../hooks/regional.js";
+import { RenderAmount } from "../../pages/PaytoWireTransferForm.js";
+import { ErrorLoadingWithDebug } from "../ErrorLoadingWithDebug.js";
+import { Time } from "../Time.js";
+import { State } from "./index.js";
+
+export function FailedView({ error }: State.Failed) {
+ const { i18n } = useTranslationContext();
+ switch (error.case) {
+ case HttpStatusCode.NotImplemented: {
+ return (
+ <Attention type="danger" title={i18n.str`Cashout are disabled`}>
+ <i18n.Translate>
+ Cashout should be enable by configuration and the conversion rate
+ should be initialized with fee, ratio and rounding mode.
+ </i18n.Translate>
+ </Attention>
+ );
+ }
+ default:
+ assertUnreachable(error.case);
+ }
+}
+
+export function ReadyView({
+ cashouts,
+ routeCashoutDetails,
+}: State.Ready): VNode {
+ const { i18n, dateLocale } = useTranslationContext();
+ const resp = useConversionInfo();
+ if (!resp) {
+ return <Loading />;
+ }
+ if (resp instanceof TalerError) {
+ return <ErrorLoadingWithDebug error={resp} />;
+ }
+ if (resp.type === "fail") {
+ switch (resp.case) {
+ case HttpStatusCode.NotImplemented: {
+ return (
+ <Attention type="danger" title={i18n.str`Cashout are disabled`}>
+ <i18n.Translate>
+ Cashout should be enable by configuration and the conversion rate
+ should be initialized with fee, ratio and rounding mode.
+ </i18n.Translate>
+ </Attention>
+ );
+ }
+ default:
+ assertUnreachable(resp.case);
+ }
+ }
+
+ if (!cashouts.length) return <div />;
+ const txByDate = cashouts.reduce(
+ (prev, cur) => {
+ const d =
+ cur.creation_time.t_s === "never"
+ ? ""
+ : format(cur.creation_time.t_s * 1000, "dd/MM/yyyy", {
+ locale: dateLocale,
+ });
+ if (!prev[d]) {
+ prev[d] = [];
+ }
+ prev[d].push(cur);
+ return prev;
+ },
+ {} as Record<string, typeof cashouts>,
+ );
+ return (
+ <div class="px-4 mt-4">
+ <div class="sm:flex sm:items-center">
+ <div class="sm:flex-auto">
+ <h1 class="text-base font-semibold leading-6 text-gray-900">
+ <i18n.Translate>Latest cashouts</i18n.Translate>
+ </h1>
+ </div>
+ </div>
+ <div class="-mx-4 mt-5 ring-1 ring-gray-300 sm:mx-0 rounded-lg min-w-fit bg-white">
+ <table class="min-w-full divide-y divide-gray-300">
+ <thead>
+ <tr>
+ <th
+ scope="col"
+ class=" pl-2 py-3.5 text-left text-sm font-semibold text-gray-900"
+ >{i18n.str`Created`}</th>
+ <th
+ scope="col"
+ class="hidden sm:table-cell pl-2 py-3.5 text-left text-sm font-semibold text-gray-900"
+ >{i18n.str`Total debit`}</th>
+ <th
+ scope="col"
+ class="hidden sm:table-cell pl-2 py-3.5 text-left text-sm font-semibold text-gray-900"
+ >{i18n.str`Total credit`}</th>
+ <th
+ scope="col"
+ class="hidden sm:table-cell pl-2 py-3.5 text-left text-sm font-semibold text-gray-900"
+ >{i18n.str`Subject`}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {Object.entries(txByDate).map(([date, txs], idx) => {
+ return (
+ <Fragment key={idx}>
+ <tr class="border-t border-gray-200">
+ <th
+ colSpan={6}
+ scope="colgroup"
+ class="bg-gray-50 py-2 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-3"
+ >
+ {date}
+ </th>
+ </tr>
+ {txs.map((item) => {
+ return (
+ <a
+ name="cashout details"
+ key={idx}
+ class="table-row border-b border-gray-200 hover:bg-gray-200 last:border-none"
+ // class="table-row"
+ href={routeCashoutDetails.url({
+ cid: String(item.id),
+ })}
+ >
+ <td class="relative py-2 pl-2 pr-2 text-sm ">
+ <div class="font-medium text-gray-900">
+ <Time
+ format="HH:mm:ss"
+ timestamp={AbsoluteTime.fromProtocolTimestamp(
+ item.creation_time,
+ )}
+ // relative={Duration.fromSpec({ days: 1 })}
+ />
+ </div>
+ {
+ //FIXME: implement responsive view
+ }
+ {/* <dl class="font-normal sm:hidden">
+ <dt class="sr-only sm:hidden"><i18n.Translate>Amount</i18n.Translate></dt>
+ <dd class="mt-1 truncate text-gray-700">
+ {item.negative ? i18n.str`sent` : i18n.str`received`} {item.amount ? (
+ <span data-negative={item.negative ? "true" : "false"} class="data-[negative=false]:text-green-600 data-[negative=true]:text-red-600">
+ <RenderAmount value={item.amount} />
+ </span>
+ ) : (
+ <span style={{ color: "grey" }}>&lt;{i18n.str`invalid value`}&gt;</span>
+ )}</dd>
+
+ <dt class="sr-only sm:hidden"><i18n.Translate>Counterpart</i18n.Translate></dt>
+ <dd class="mt-1 truncate text-gray-500 sm:hidden">
+ {item.negative ? i18n.str`to` : i18n.str`from`} {item.counterpart}
+ </dd>
+ <dd class="mt-1 text-gray-500 sm:hidden" >
+ <pre class="break-words w-56 whitespace-break-spaces p-2 rounded-md mx-auto my-2 bg-gray-100">
+ {item.subject}
+ </pre>
+ </dd>
+ </dl> */}
+ </td>
+ <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-red-600 cursor-pointer">
+ <RenderAmount
+ value={Amounts.parseOrThrow(item.amount_debit)}
+ spec={resp.body.regional_currency_specification}
+ />
+ </td>
+ <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-green-600 cursor-pointer">
+ <RenderAmount
+ value={Amounts.parseOrThrow(item.amount_credit)}
+ spec={resp.body.fiat_currency_specification}
+ />
+ </td>
+
+ <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500 break-all min-w-md">
+ {item.subject}
+ </td>
+ </a>
+ );
+ })}
+ </Fragment>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>
+ </div>
+ );
+}
diff --git a/packages/bank-ui/src/components/EmptyComponentExample/index.ts b/packages/bank-ui/src/components/EmptyComponentExample/index.ts
new file mode 100644
index 000000000..da84f9921
--- /dev/null
+++ b/packages/bank-ui/src/components/EmptyComponentExample/index.ts
@@ -0,0 +1,56 @@
+/*
+ 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 { HookError, Loading, utils } from "@gnu-taler/web-util/browser";
+import { useComponentState } from "./state.js";
+import { LoadingUriView, ReadyView } from "./views.js";
+
+export interface Props {
+ p: string;
+}
+
+export type State = State.Loading | State.LoadingUriError | State.Ready;
+
+export namespace State {
+ export interface Loading {
+ status: "loading";
+ error: undefined;
+ }
+
+ export interface LoadingUriError {
+ status: "loading-error";
+ error: HookError;
+ }
+
+ export interface BaseInfo {
+ error: undefined;
+ }
+ export interface Ready extends BaseInfo {
+ status: "ready";
+ error: undefined;
+ }
+}
+
+const viewMapping: utils.StateViewMap<State> = {
+ loading: Loading,
+ "loading-error": LoadingUriView,
+ ready: ReadyView,
+};
+
+export const ComponentName = utils.compose(
+ (p: Props) => useComponentState(p),
+ viewMapping,
+);
diff --git a/packages/bank-ui/src/components/EmptyComponentExample/state.ts b/packages/bank-ui/src/components/EmptyComponentExample/state.ts
new file mode 100644
index 000000000..057664983
--- /dev/null
+++ b/packages/bank-ui/src/components/EmptyComponentExample/state.ts
@@ -0,0 +1,25 @@
+/*
+ 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 { wxApi } from "../../wxApi.js";
+import { Props, State } from "./index.js";
+
+export function useComponentState({ p: _p }: Props): State {
+ return {
+ status: "ready",
+ error: undefined,
+ };
+}
diff --git a/packages/merchant-backend-ui/tests/__mocks__/setupTests.ts b/packages/bank-ui/src/components/EmptyComponentExample/stories.tsx
index ab0f08b2f..160acdf79 100644
--- a/packages/merchant-backend-ui/tests/__mocks__/setupTests.ts
+++ b/packages/bank-ui/src/components/EmptyComponentExample/stories.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2022-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -14,15 +14,16 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- /**
+/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
-import 'regenerator-runtime/runtime'
-import { configure } from 'enzyme';
-import Adapter from 'enzyme-adapter-preact-pure';
+import * as tests from "@gnu-taler/web-util/testing";
+import { ReadyView } from "./views.js";
-configure({
- adapter: new Adapter()
-});
+export default {
+ title: "example",
+};
+
+export const Ready = tests.createExample(ReadyView, {});
diff --git a/packages/bank-ui/src/components/EmptyComponentExample/test.ts b/packages/bank-ui/src/components/EmptyComponentExample/test.ts
new file mode 100644
index 000000000..629948d91
--- /dev/null
+++ b/packages/bank-ui/src/components/EmptyComponentExample/test.ts
@@ -0,0 +1,28 @@
+/*
+ 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/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { expect } from "chai";
+
+describe("test description", () => {
+ it("should assert", () => {
+ expect([]).deep.equals([]);
+ });
+});
diff --git a/packages/bank-ui/src/components/EmptyComponentExample/views.tsx b/packages/bank-ui/src/components/EmptyComponentExample/views.tsx
new file mode 100644
index 000000000..457933a5f
--- /dev/null
+++ b/packages/bank-ui/src/components/EmptyComponentExample/views.tsx
@@ -0,0 +1,25 @@
+/*
+ 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 { h, VNode } from "preact";
+
+export function LoadingUriView(): VNode {
+ return <div></div>;
+}
+
+export function ReadyView(): VNode {
+ return <div />;
+}
diff --git a/packages/bank-ui/src/components/ErrorLoadingWithDebug.tsx b/packages/bank-ui/src/components/ErrorLoadingWithDebug.tsx
new file mode 100644
index 000000000..8679af050
--- /dev/null
+++ b/packages/bank-ui/src/components/ErrorLoadingWithDebug.tsx
@@ -0,0 +1,24 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { TalerError } from "@gnu-taler/taler-util";
+import { ErrorLoading } from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
+import { usePreferences } from "../hooks/preferences.js";
+
+export function ErrorLoadingWithDebug({ error }: { error: TalerError }): VNode {
+ const [pref] = usePreferences();
+ return <ErrorLoading error={error} showDetail={pref.showDebugInfo} />;
+}
diff --git a/packages/demobank-ui/src/components/QR.tsx b/packages/bank-ui/src/components/QR.tsx
index 4e95137e1..b039bbd1e 100644
--- a/packages/demobank-ui/src/components/QR.tsx
+++ b/packages/bank-ui/src/components/QR.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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
@@ -33,7 +33,6 @@ export function QR({ text }: { text: string }): VNode {
return (
<div
style={{
- width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "left",
@@ -41,9 +40,7 @@ export function QR({ text }: { text: string }): VNode {
>
<div
style={{
- width: "50%",
- minWidth: 200,
- maxWidth: 300,
+ width: "100%",
marginRight: "auto",
marginLeft: "auto",
}}
diff --git a/packages/bank-ui/src/components/Time.tsx b/packages/bank-ui/src/components/Time.tsx
new file mode 100644
index 000000000..5c8afe212
--- /dev/null
+++ b/packages/bank-ui/src/components/Time.tsx
@@ -0,0 +1,80 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { AbsoluteTime, Duration } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import {
+ formatISO,
+ format,
+ formatDuration,
+ intervalToDuration,
+} from "date-fns";
+import { Fragment, h, VNode } from "preact";
+
+/**
+ *
+ * @param timestamp time to be formatted
+ * @param relative duration threshold, if the difference is lower
+ * the timestamp will be formatted as relative time from "now"
+ *
+ * @returns
+ */
+export function Time({
+ timestamp,
+ relative,
+ format: formatString,
+}: {
+ timestamp: AbsoluteTime | undefined;
+ relative?: Duration;
+ format: string;
+}): VNode {
+ const { i18n, dateLocale } = useTranslationContext();
+ if (!timestamp) return <Fragment />;
+
+ if (timestamp.t_ms === "never") {
+ return <time>{i18n.str`never`}</time>;
+ }
+
+ const now = AbsoluteTime.now();
+ const diff = AbsoluteTime.difference(now, timestamp);
+ if (relative && now.t_ms !== "never" && Duration.cmp(diff, relative) === -1) {
+ const d = intervalToDuration({
+ start: now.t_ms,
+ end: timestamp.t_ms,
+ });
+ d.seconds = 0;
+ const duration = formatDuration(d, { locale: dateLocale });
+ const isFuture = AbsoluteTime.cmp(now, timestamp) < 0;
+ if (isFuture) {
+ return (
+ <time dateTime={formatISO(timestamp.t_ms)}>
+ <i18n.Translate>in {duration}</i18n.Translate>
+ </time>
+ );
+ } else {
+ return (
+ <time dateTime={formatISO(timestamp.t_ms)}>
+ <i18n.Translate>{duration} ago</i18n.Translate>
+ </time>
+ );
+ }
+ }
+ return (
+ <time dateTime={formatISO(timestamp.t_ms)}>
+ {format(timestamp.t_ms, formatString, { locale: dateLocale })}
+ </time>
+ );
+}
diff --git a/packages/bank-ui/src/components/Transactions/index.ts b/packages/bank-ui/src/components/Transactions/index.ts
new file mode 100644
index 000000000..6fccfcd79
--- /dev/null
+++ b/packages/bank-ui/src/components/Transactions/index.ts
@@ -0,0 +1,84 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { AbsoluteTime, AmountJson, TalerError } from "@gnu-taler/taler-util";
+import { Loading, utils } from "@gnu-taler/web-util/browser";
+import { ErrorLoadingWithDebug } from "../ErrorLoadingWithDebug.js";
+import { useComponentState } from "./state.js";
+import { ReadyView } from "./views.js";
+import { RouteDefinition } from "@gnu-taler/web-util/browser";
+
+export interface Props {
+ account: string;
+ routeCreateWireTransfer:
+ | RouteDefinition<{
+ account?: string;
+ subject?: string;
+ amount?: string;
+ }>
+ | undefined;
+}
+
+export type State = State.Loading | State.LoadingUriError | State.Ready;
+
+export namespace State {
+ export interface Loading {
+ status: "loading";
+ error: undefined;
+ }
+
+ export interface LoadingUriError {
+ status: "loading-error";
+ error: TalerError;
+ }
+
+ export interface BaseInfo {
+ error: undefined;
+ }
+ export interface Ready extends BaseInfo {
+ status: "ready";
+ error: undefined;
+ routeCreateWireTransfer:
+ | RouteDefinition<{
+ account?: string;
+ subject?: string;
+ amount?: string;
+ }>
+ | undefined;
+ transactions: Transaction[];
+ onGoStart?: () => void;
+ onGoNext?: () => void;
+ }
+}
+
+export interface Transaction {
+ negative: boolean;
+ counterpart: string;
+ when: AbsoluteTime;
+ amount: AmountJson | undefined;
+ subject: string;
+}
+
+const viewMapping: utils.StateViewMap<State> = {
+ loading: Loading,
+ "loading-error": ErrorLoadingWithDebug,
+ ready: ReadyView,
+};
+
+export const Transactions = utils.compose(
+ (p: Props) => useComponentState(p),
+ viewMapping,
+);
diff --git a/packages/bank-ui/src/components/Transactions/state.ts b/packages/bank-ui/src/components/Transactions/state.ts
new file mode 100644
index 000000000..ce6338e57
--- /dev/null
+++ b/packages/bank-ui/src/components/Transactions/state.ts
@@ -0,0 +1,90 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ AbsoluteTime,
+ Amounts,
+ HttpStatusCode,
+ TalerError,
+ assertUnreachable,
+ parsePaytoUri,
+} from "@gnu-taler/taler-util";
+import { useTransactions } from "../../hooks/account.js";
+import { Props, State, Transaction } from "./index.js";
+
+export function useComponentState({
+ account,
+ routeCreateWireTransfer,
+}: Props): State {
+ const result = useTransactions(account);
+ if (!result) {
+ return {
+ status: "loading",
+ error: undefined,
+ };
+ }
+ if (result instanceof TalerError) {
+ return {
+ status: "loading-error",
+ error: result,
+ };
+ }
+ if (result.type === "fail") {
+ return {
+ status: "loading",
+ error: undefined,
+ };
+ }
+
+ const transactions = result.body
+ .map((tx) => {
+ const negative = tx.direction === "debit";
+ const cp = parsePaytoUri(
+ negative ? tx.creditor_payto_uri : tx.debtor_payto_uri,
+ );
+ const counterpart =
+ (cp === undefined || !cp.isKnown
+ ? undefined
+ : cp.targetType === "iban"
+ ? cp.iban
+ : cp.targetType === "x-taler-bank"
+ ? cp.account
+ : cp.targetType === "bitcoin"
+ ? `${cp.address.substring(0, 6)}...`
+ : undefined) ?? "unknown";
+
+ const when = AbsoluteTime.fromProtocolTimestamp(tx.date);
+ const amount = Amounts.parse(tx.amount);
+ const subject = tx.subject;
+ return {
+ negative,
+ counterpart,
+ when,
+ amount,
+ subject,
+ };
+ })
+ .filter((x): x is Transaction => x !== undefined);
+
+ return {
+ status: "ready",
+ error: undefined,
+ routeCreateWireTransfer,
+ transactions,
+ onGoNext: result.isLastPage ? undefined : result.loadNext,
+ onGoStart: result.isFirstPage ? undefined : result.loadFirst,
+ };
+}
diff --git a/packages/merchant-backoffice-ui/tests/__mocks__/browserMocks.ts b/packages/bank-ui/src/components/Transactions/stories.tsx
index ee6bba505..95014574b 100644
--- a/packages/merchant-backoffice-ui/tests/__mocks__/browserMocks.ts
+++ b/packages/bank-ui/src/components/Transactions/stories.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,29 +14,31 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- /**
+/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
-// Mock Browser API's which are not supported by JSDOM, e.g. ServiceWorker, LocalStorage
-/**
- * An example how to mock localStorage is given below 👇
- */
-
-/*
-// Mocks localStorage
-const localStorageMock = (function() {
- let store = {};
-
- return {
- getItem: (key) => store[key] || null,
- setItem: (key, value) => store[key] = value.toString(),
- clear: () => store = {}
- };
-
-})();
-
-Object.defineProperty(window, 'localStorage', {
- value: localStorageMock
-}); */
+import * as tests from "@gnu-taler/web-util/testing";
+import { ReadyView } from "./views.js";
+import { AbsoluteTime } from "@gnu-taler/taler-util";
+
+export default {
+ title: "transaction list",
+};
+
+export const Ready = tests.createExample(ReadyView, {
+ transactions: [
+ {
+ amount: {
+ currency: "USD",
+ fraction: 0,
+ value: 1,
+ },
+ counterpart: "ASD",
+ negative: false,
+ subject: "Some",
+ when: AbsoluteTime.now(),
+ },
+ ],
+});
diff --git a/packages/bank-ui/src/components/Transactions/test.ts b/packages/bank-ui/src/components/Transactions/test.ts
new file mode 100644
index 000000000..d9442c742
--- /dev/null
+++ b/packages/bank-ui/src/components/Transactions/test.ts
@@ -0,0 +1,202 @@
+/*
+ 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/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { TalerErrorCode } from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
+import { SwrMockEnvironment } from "@gnu-taler/web-util/testing";
+import { expect } from "chai";
+import { Props } from "./index.js";
+import { useComponentState } from "./state.js";
+
+describe("Transaction states", () => {
+ it.skip("should query backend and render transactions", async () => {
+ const env = new SwrMockEnvironment();
+
+ const props: Props = {
+ account: "myAccount",
+ routeCreateWireTransfer: undefined,
+ };
+
+ // env.addRequestExpectation(TRANSACTION_API_EXAMPLE.LIST_FIRST_PAGE, {
+ // response: {
+ // data: {
+ // transactions: [
+ // {
+ // creditorIban: "DE159593",
+ // creditorBic: "SANDBOXX",
+ // creditorName: "exchange company",
+ // debtorIban: "DE118695",
+ // debtorBic: "SANDBOXX",
+ // debtorName: "Name unknown",
+ // amount: "1",
+ // currency: "KUDOS",
+ // subject:
+ // "Taler Withdrawal N588V8XE9TR49HKAXFQ20P0EQ0EYW2AC9NNANV8ZP5P59N6N0410",
+ // date: "2022-12-12Z",
+ // uid: "8PPFR9EM",
+ // direction: "DBIT",
+ // pmtInfId: null,
+ // msgId: null,
+ // },
+ // {
+ // creditorIban: "DE159593",
+ // creditorBic: "SANDBOXX",
+ // creditorName: "exchange company",
+ // debtorIban: "DE118695",
+ // debtorBic: "SANDBOXX",
+ // debtorName: "Name unknown",
+ // amount: "5.00",
+ // currency: "KUDOS",
+ // subject: "HNEWWT679TQC5P1BVXJS48FX9NW18FWM6PTK2N80Z8GVT0ACGNK0",
+ // date: "2022-12-07Z",
+ // uid: "7FZJC3RJ",
+ // direction: "DBIT",
+ // pmtInfId: null,
+ // msgId: null,
+ // },
+ // {
+ // creditorIban: "DE118695",
+ // creditorBic: "SANDBOXX",
+ // creditorName: "Name unknown",
+ // debtorIban: "DE579516",
+ // debtorBic: "SANDBOXX",
+ // debtorName: "The Bank",
+ // amount: "100",
+ // currency: "KUDOS",
+ // subject: "Sign-up bonus",
+ // date: "2022-12-07Z",
+ // uid: "I31A06J8",
+ // direction: "CRDT",
+ // pmtInfId: null,
+ // msgId: null,
+ // },
+ // ],
+ // },
+ // },
+ // });
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentState,
+ props,
+ [
+ ({ status, error }) => {
+ expect(status).equals("loading");
+ expect(error).undefined;
+ },
+ ({ status, error }) => {
+ expect(status).equals("ready");
+ expect(error).undefined;
+ },
+ ],
+ env.buildTestingContext(),
+ );
+
+ expect(hookBehavior).deep.eq({ result: "ok" });
+
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+
+ // it("should show error message on not found", async () => {
+ // const env = new SwrMockEnvironment();
+
+ // const props: Props = {
+ // account: "myAccount",
+ // };
+
+ // env.addRequestExpectation(TRANSACTION_API_EXAMPLE.LIST_NOT_FOUND, {
+ // response: {
+ // error: {
+ // description: "Transaction page 0 could not be retrieved.",
+ // },
+ // },
+ // });
+
+ // const hookBehavior = await tests.hookBehaveLikeThis(
+ // useComponentState,
+ // props,
+ // [
+ // ({ status, error }) => {
+ // expect(status).equals("loading");
+ // expect(error).undefined;
+ // },
+ // ({ status, error }) => {
+ // expect(status).equals("loading-error");
+ // if (error === undefined || error.type !== ErrorType.CLIENT) {
+ // throw Error("not the expected error");
+ // }
+ // expect(error.payload).deep.equal({
+ // error: {
+ // description: "Transaction page 0 could not be retrieved.",
+ // },
+ // });
+ // },
+ // ],
+ // env.buildTestingContext(),
+ // );
+
+ // expect(hookBehavior).deep.eq({ result: "ok" });
+ // expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ // });
+
+ it.skip("should show error message on server error", async () => {
+ const env = new SwrMockEnvironment();
+
+ const props: Props = {
+ account: "myAccount",
+ routeCreateWireTransfer: undefined,
+ };
+
+ // env.addRequestExpectation(TRANSACTION_API_EXAMPLE.LIST_ERROR, {
+ // response: {
+ // error: {
+ // code: TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED,
+ // },
+ // },
+ // });
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentState,
+ props,
+ [
+ ({ status, error }) => {
+ expect(status).equals("loading");
+ expect(error).undefined;
+ },
+ ({ status, error }) => {
+ expect(status).equals("loading-error");
+ if (
+ error === undefined ||
+ !error.hasErrorCode(TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED)
+ ) {
+ throw Error("not the expected error");
+ }
+ expect(error.errorDetail.code).deep.equal(
+ TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED,
+ );
+ },
+ ],
+ env.buildTestingContext(),
+ );
+
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+});
diff --git a/packages/bank-ui/src/components/Transactions/views.tsx b/packages/bank-ui/src/components/Transactions/views.tsx
new file mode 100644
index 000000000..10d63e6af
--- /dev/null
+++ b/packages/bank-ui/src/components/Transactions/views.tsx
@@ -0,0 +1,252 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ Attention,
+ useBankCoreApiContext,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { format } from "date-fns";
+import { Fragment, VNode, h } from "preact";
+import { RenderAmount } from "../../pages/PaytoWireTransferForm.js";
+import { Time } from "../Time.js";
+import { State } from "./index.js";
+
+export function ReadyView({
+ transactions,
+ routeCreateWireTransfer,
+ onGoNext,
+ onGoStart,
+}: State.Ready): VNode {
+ const { i18n, dateLocale } = useTranslationContext();
+ const { config } = useBankCoreApiContext();
+
+ if (!transactions.length) {
+ return (
+ <div class="px-4 mt-4">
+ <div class="sm:flex sm:items-center">
+ <div class="sm:flex-auto">
+ <h1 class="text-base font-semibold leading-6 text-gray-900">
+ <i18n.Translate>Transactions history</i18n.Translate>
+ </h1>
+ </div>
+ </div>
+
+ <Attention type="low" title={i18n.str`No transactions yet.`}>
+ <i18n.Translate>
+ You can start sending a wire transfer or withdrawing to your wallet.
+ </i18n.Translate>
+ </Attention>
+ </div>
+ );
+ }
+
+ const txByDate = transactions.reduce(
+ (prev, cur) => {
+ const d =
+ cur.when.t_ms === "never"
+ ? ""
+ : format(cur.when.t_ms, "dd/MM/yyyy", { locale: dateLocale });
+ if (!prev[d]) {
+ prev[d] = [];
+ }
+ prev[d].push(cur);
+ return prev;
+ },
+ {} as Record<string, typeof transactions>,
+ );
+ return (
+ <div class="px-4 mt-8">
+ <div class="sm:flex sm:items-center">
+ <div class="sm:flex-auto">
+ <h1 class="text-base font-semibold leading-6 text-gray-900">
+ <i18n.Translate>Transactions history</i18n.Translate>
+ </h1>
+ </div>
+ </div>
+ <div class="-mx-4 mt-5 ring-1 ring-gray-300 sm:mx-0 rounded-lg min-w-fit bg-white">
+ <table class="min-w-full divide-y divide-gray-300">
+ <thead>
+ <tr>
+ <th
+ scope="col"
+ class="pl-2 py-3.5 text-left text-sm font-semibold text-gray-900 "
+ >{i18n.str`Date`}</th>
+ <th
+ scope="col"
+ class="hidden sm:table-cell pl-2 py-3.5 text-left text-sm font-semibold text-gray-900 "
+ >{i18n.str`Amount`}</th>
+ <th
+ scope="col"
+ class="hidden sm:table-cell pl-2 py-3.5 text-left text-sm font-semibold text-gray-900 "
+ >{i18n.str`Counterpart`}</th>
+ <th
+ scope="col"
+ class="hidden sm:table-cell pl-2 py-3.5 text-left text-sm font-semibold text-gray-900 "
+ >{i18n.str`Subject`}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {Object.entries(txByDate).map(([date, txs], idx) => {
+ return (
+ <Fragment key={idx}>
+ <tr class="border-t border-gray-200">
+ <th
+ colSpan={4}
+ scope="colgroup"
+ class="bg-gray-50 py-2 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-3"
+ >
+ {date}
+ </th>
+ </tr>
+ {txs.map((item) => {
+ return (
+ <tr
+ key={idx}
+ class="border-b border-gray-200 last:border-none"
+ >
+ <td class="relative py-2 pl-2 pr-2 text-sm ">
+ <div class="font-medium text-gray-900">
+ <Time
+ format="HH:mm:ss"
+ timestamp={item.when}
+ // relative={Duration.fromSpec({ days: 1 })}
+ />
+ </div>
+ <dl class="font-normal sm:hidden">
+ <dt class="sr-only sm:hidden">
+ <i18n.Translate>Amount</i18n.Translate>
+ </dt>
+ <dd class="mt-1 truncate text-gray-700">
+ {item.negative
+ ? i18n.str`sent`
+ : i18n.str`received`}{" "}
+ {item.amount ? (
+ <span
+ data-negative={
+ item.negative ? "true" : "false"
+ }
+ class="data-[negative=false]:text-green-600 data-[negative=true]:text-red-600"
+ >
+ <RenderAmount
+ value={item.amount}
+ spec={config.currency_specification}
+ />
+ </span>
+ ) : (
+ <span style={{ color: "grey" }}>
+ &lt;{i18n.str`Invalid value`}&gt;
+ </span>
+ )}
+ </dd>
+
+ <dt class="sr-only sm:hidden">
+ <i18n.Translate>Counterpart</i18n.Translate>
+ </dt>
+ <dd class="mt-1 truncate text-gray-500 sm:hidden">
+ {item.negative ? i18n.str`to` : i18n.str`from`}{" "}
+ {!routeCreateWireTransfer ? (
+ item.counterpart
+ ) : (
+ <a
+ name={`transfer to ${item.counterpart}`}
+ href={routeCreateWireTransfer.url({
+ account: item.counterpart,
+ })}
+ class="text-indigo-600 hover:text-indigo-900"
+ >
+ {item.counterpart}
+ </a>
+ )}
+ </dd>
+ <dd class="mt-1 text-gray-500 sm:hidden">
+ <pre class="break-words w-56 whitespace-break-spaces p-2 rounded-md mx-auto my-2 bg-gray-100">
+ {item.subject}
+ </pre>
+ </dd>
+ </dl>
+ </td>
+ <td
+ data-negative={item.negative ? "true" : "false"}
+ class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500 "
+ >
+ {item.amount ? (
+ <RenderAmount
+ value={item.amount}
+ negative={item.negative}
+ withColor
+ spec={config.currency_specification}
+ />
+ ) : (
+ <span style={{ color: "grey" }}>
+ &lt;{i18n.str`Invalid value`}&gt;
+ </span>
+ )}
+ </td>
+ <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500">
+ {!routeCreateWireTransfer ? (
+ item.counterpart
+ ) : (
+ <a
+ name={`wire transfer to ${item.counterpart}`}
+ href={routeCreateWireTransfer.url({
+ account: item.counterpart,
+ })}
+ class="text-indigo-600 hover:text-indigo-900"
+ >
+ {item.counterpart}
+ </a>
+ )}
+ </td>
+ <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500 break-all min-w-md">
+ {item.subject}
+ </td>
+ </tr>
+ );
+ })}
+ </Fragment>
+ );
+ })}
+ </tbody>
+ </table>
+
+ <nav
+ class="flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6 rounded-lg"
+ aria-label="Pagination"
+ >
+ <div class="flex flex-1 justify-between sm:justify-end">
+ <button
+ name="first page"
+ 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"
+ disabled={!onGoStart}
+ onClick={onGoStart}
+ >
+ <i18n.Translate>First page</i18n.Translate>
+ </button>
+ <button
+ name="next page"
+ class="relative disabled:bg-gray-100 disabled:text-gray-500 ml-3 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"
+ disabled={!onGoNext}
+ onClick={onGoNext}
+ >
+ <i18n.Translate>Next</i18n.Translate>
+ </button>
+ </div>
+ </nav>
+ </div>
+ </div>
+ );
+}
diff --git a/packages/bank-ui/src/components/index.examples.ts b/packages/bank-ui/src/components/index.examples.ts
new file mode 100644
index 000000000..20e013070
--- /dev/null
+++ b/packages/bank-ui/src/components/index.examples.ts
@@ -0,0 +1,17 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+export * as tx from "./Transactions/stories.js";
diff --git a/packages/merchant-backoffice-ui/src/context/fetch.ts b/packages/bank-ui/src/context/settings.ts
index ef4dfb7ae..053fcbd12 100644
--- a/packages/merchant-backoffice-ui/src/context/fetch.ts
+++ b/packages/bank-ui/src/context/settings.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,41 +14,31 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+import { ComponentChildren, createContext, h, VNode } from "preact";
+import { useContext } from "preact/hooks";
+import { BankUiSettings } from "../settings.js";
+
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { h, createContext, VNode, ComponentChildren } from "preact";
-import { useContext } from "preact/hooks";
-import useSWR from "swr";
-import useSWRInfinite from "swr/infinite";
+export type Type = BankUiSettings;
-interface Type {
- useSWR: typeof useSWR;
- useSWRInfinite: typeof useSWRInfinite;
-}
+const initial: BankUiSettings = {};
+const Context = createContext<Type>(initial);
-const Context = createContext<Type>({} as any);
-
-export const useFetchContext = (): Type => useContext(Context);
-export const FetchContextProvider = ({
- children,
-}: {
- children: ComponentChildren;
-}): VNode => {
- return h(Context.Provider, { value: { useSWR, useSWRInfinite }, children });
-};
+export const useSettingsContext = (): Type => useContext(Context);
-export const FetchContextProviderTesting = ({
+export const SettingsProvider = ({
children,
- data,
+ value,
}: {
+ value: BankUiSettings;
children: ComponentChildren;
- data: any;
}): VNode => {
return h(Context.Provider, {
- value: { useSWR: () => data, useSWRInfinite },
+ value,
children,
});
};
diff --git a/packages/bank-ui/src/context/wallet-integration.ts b/packages/bank-ui/src/context/wallet-integration.ts
new file mode 100644
index 000000000..e14988ed1
--- /dev/null
+++ b/packages/bank-ui/src/context/wallet-integration.ts
@@ -0,0 +1,83 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { stringifyTalerUri, TalerUri } from "@gnu-taler/taler-util";
+import { ComponentChildren, createContext, h, VNode } from "preact";
+import { useContext } from "preact/hooks";
+
+/**
+ * https://docs.taler.net/design-documents/039-taler-browser-integration.html
+ *
+ * @param uri
+ */
+function createHeadMetaTag(uri: TalerUri, onNotFound?: () => void) {
+ const meta = document.createElement("meta");
+ meta.setAttribute("name", "taler-uri");
+ meta.setAttribute("content", stringifyTalerUri(uri));
+
+ document.head.appendChild(meta);
+
+ let walletFound = false;
+ window.addEventListener("beforeunload", () => {
+ walletFound = true;
+ });
+ setTimeout(() => {
+ if (!walletFound && onNotFound) {
+ onNotFound();
+ }
+ }, 10); //very short timeout
+}
+interface Type {
+ /**
+ * Tell the active wallet that an action is found
+ *
+ * @param uri
+ * @returns
+ */
+ publishTalerAction: (uri: TalerUri, onNotFound?: () => void) => void;
+}
+
+// @ts-expect-error default value to undefined, should it be another thing?
+const Context = createContext<Type>(undefined);
+
+export const useTalerWalletIntegrationAPI = (): Type => useContext(Context);
+
+export const TalerWalletIntegrationBrowserProvider = ({
+ children,
+}: {
+ children: ComponentChildren;
+}): VNode => {
+ const value: Type = {
+ publishTalerAction: createHeadMetaTag,
+ };
+ return h(Context.Provider, {
+ value,
+ children,
+ });
+};
+
+export const TalerWalletIntegrationTestingProvider = ({
+ children,
+ value,
+}: {
+ children: ComponentChildren;
+ value: Type;
+}): VNode => {
+ return h(Context.Provider, {
+ value,
+ children,
+ });
+};
diff --git a/packages/bank-ui/src/declaration.d.ts b/packages/bank-ui/src/declaration.d.ts
new file mode 100644
index 000000000..581cbcd07
--- /dev/null
+++ b/packages/bank-ui/src/declaration.d.ts
@@ -0,0 +1,35 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+declare module "*.css" {
+ const mapping: Record<string, string>;
+ export default mapping;
+}
+declare module "*.svg" {
+ const content: string;
+ export default content;
+}
+declare module "*.jpeg" {
+ const content: string;
+ export default content;
+}
+declare module "*.png" {
+ const content: string;
+ export default content;
+}
+
+declare const __VERSION__: string;
+declare const __GIT_HASH__: string;
diff --git a/packages/bank-ui/src/hooks/account.ts b/packages/bank-ui/src/hooks/account.ts
new file mode 100644
index 000000000..43d43a3f2
--- /dev/null
+++ b/packages/bank-ui/src/hooks/account.ts
@@ -0,0 +1,313 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ AccessToken,
+ OperationOk,
+ TalerCoreBankResultByMethod,
+ TalerHttpError,
+ WithdrawalOperationStatus,
+} from "@gnu-taler/taler-util";
+import { useEffect, useState } from "preact/hooks";
+import { useSessionState } from "./session.js";
+
+// FIX default import https://github.com/microsoft/TypeScript/issues/49189
+import _useSWR, { SWRHook, mutate } from "swr";
+import { useBankCoreApiContext } from "@gnu-taler/web-util/browser";
+import { PAGINATED_LIST_REQUEST } from "../utils.js";
+const useSWR = _useSWR as unknown as SWRHook;
+
+export interface InstanceTemplateFilter {
+ // FIXME: add filter to the template list
+ position?: string;
+}
+
+export function revalidateAccountDetails() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "getAccount",
+ undefined,
+ { revalidate: true },
+ );
+}
+
+export function useAccountDetails(account: string) {
+ const { state: credentials } = useSessionState();
+ const {
+ lib: { bank: api },
+ } = useBankCoreApiContext();
+
+ async function fetcher([username, token]: [string, AccessToken]) {
+ return await api.getAccount({ username, token });
+ }
+ const token =
+ credentials.status !== "loggedIn" ? undefined : credentials.token;
+ const { data, error } = useSWR<
+ TalerCoreBankResultByMethod<"getAccount">,
+ TalerHttpError
+ >([account, token, "getAccount"], fetcher, {});
+
+ if (data) return data;
+ if (error) return error;
+ return undefined;
+}
+
+export function revalidateWithdrawalDetails() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "getWithdrawalById",
+ undefined,
+ { revalidate: true },
+ );
+}
+
+export function useWithdrawalDetails(wid: string) {
+ const {
+ lib: { bank: api },
+ } = useBankCoreApiContext();
+ const [latestStatus, setLatestStatus] = useState<WithdrawalOperationStatus>();
+
+ async function fetcher([wid, old_state]: [
+ string,
+ WithdrawalOperationStatus | undefined,
+ ]) {
+ return await api.getWithdrawalById(
+ wid,
+ old_state === undefined ? undefined : { old_state, timeoutMs: 15000 },
+ );
+ }
+
+ const { data, error } = useSWR<
+ TalerCoreBankResultByMethod<"getWithdrawalById">,
+ TalerHttpError
+ >([wid, latestStatus, "getWithdrawalById"], fetcher, {
+ refreshInterval: 3000,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ errorRetryCount: 0,
+ errorRetryInterval: 1,
+ shouldRetryOnError: false,
+ keepPreviousData: true,
+ });
+
+ const currentStatus =
+ data !== undefined && data.type === "ok" ? data.body.status : undefined;
+
+ useEffect(() => {
+ if (currentStatus !== undefined && currentStatus !== latestStatus) {
+ setLatestStatus(currentStatus);
+ }
+ }, [currentStatus]);
+
+ if (data) return data;
+ if (error) return error;
+ return undefined;
+}
+
+export function revalidateTransactionDetails() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "getTransactionById",
+ undefined,
+ { revalidate: true },
+ );
+}
+export function useTransactionDetails(account: string, tid: number) {
+ const { state: credentials } = useSessionState();
+ const token =
+ credentials.status !== "loggedIn" ? undefined : credentials.token;
+ const {
+ lib: { bank: api },
+ } = useBankCoreApiContext();
+
+ async function fetcher([username, token, txid]: [
+ string,
+ AccessToken,
+ number,
+ ]) {
+ return await api.getTransactionById({ username, token }, txid);
+ }
+
+ const { data, error } = useSWR<
+ TalerCoreBankResultByMethod<"getTransactionById">,
+ TalerHttpError
+ >([account, token, tid, "getTransactionById"], fetcher, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ errorRetryCount: 0,
+ errorRetryInterval: 1,
+ shouldRetryOnError: false,
+ keepPreviousData: true,
+ });
+
+ if (data) return data;
+ if (error) return error;
+ return undefined;
+}
+
+export async function revalidatePublicAccounts() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "getPublicAccounts",
+ undefined,
+ { revalidate: true },
+ );
+}
+export function usePublicAccounts(
+ filterAccount: string | undefined,
+ initial?: number,
+) {
+ const [offset, setOffset] = useState<number | undefined>(initial);
+
+ const {
+ lib: { bank: api },
+ } = useBankCoreApiContext();
+
+ async function fetcher([account, txid]: [
+ string | undefined,
+ number | undefined,
+ ]) {
+ return await api.getPublicAccounts(
+ { account },
+ {
+ limit: PAGINATED_LIST_REQUEST,
+ offset: txid ? String(txid) : undefined,
+ order: "asc",
+ },
+ );
+ }
+
+ const { data, error } = useSWR<
+ TalerCoreBankResultByMethod<"getPublicAccounts">,
+ TalerHttpError
+ >([filterAccount, offset, "getPublicAccounts"], fetcher, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ errorRetryCount: 0,
+ errorRetryInterval: 1,
+ shouldRetryOnError: false,
+ keepPreviousData: true,
+ });
+
+ if (error) return error;
+ if (data === undefined) return undefined;
+ // if (data.type !== "ok") return data;
+
+ //TODO: row_id should not be optional
+ return buildPaginatedResult(
+ data.body.public_accounts,
+ offset,
+ setOffset,
+ (d) => d.row_id ?? 0,
+ );
+}
+
+type PaginatedResult<T> = OperationOk<T> & {
+ isLastPage: boolean;
+ isFirstPage: boolean;
+ loadNext(): void;
+ loadFirst(): void;
+};
+//TODO: consider sending this to web-util
+export function buildPaginatedResult<DataType, OffsetId>(
+ data: DataType[],
+ offset: OffsetId | undefined,
+ setOffset: (o: OffsetId | undefined) => void,
+ getId: (r: DataType) => OffsetId,
+): PaginatedResult<DataType[]> {
+ const isLastPage = data.length < PAGINATED_LIST_REQUEST;
+ const isFirstPage = offset === undefined;
+
+ const result = structuredClone(data);
+ if (result.length == PAGINATED_LIST_REQUEST) {
+ //do now show the last element, used to know if this is the last page
+ result.pop();
+ }
+ return {
+ type: "ok",
+ body: result,
+ isLastPage,
+ isFirstPage,
+ loadNext: () => {
+ if (!result.length) return;
+ const id = getId(result[result.length - 1]);
+ setOffset(id);
+ },
+ loadFirst: () => {
+ setOffset(undefined);
+ },
+ };
+}
+
+export function revalidateTransactions() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "getTransactions",
+ undefined,
+ { revalidate: true },
+ );
+}
+export function useTransactions(account: string, initial?: number) {
+ const { state: credentials } = useSessionState();
+ const token =
+ credentials.status !== "loggedIn" ? undefined : credentials.token;
+
+ const [offset, setOffset] = useState<number | undefined>(initial);
+ const {
+ lib: { bank: api },
+ } = useBankCoreApiContext();
+
+ async function fetcher([username, token, txid]: [
+ string,
+ AccessToken,
+ number | undefined,
+ ]) {
+ return await api.getTransactions(
+ { username, token },
+ {
+ limit: PAGINATED_LIST_REQUEST,
+ offset: txid ? String(txid) : undefined,
+ order: "dec",
+ },
+ );
+ }
+
+ const { data, error } = useSWR<
+ TalerCoreBankResultByMethod<"getTransactions">,
+ TalerHttpError
+ >([account, token, offset, "getTransactions"], fetcher, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ refreshWhenOffline: false,
+ // revalidateOnMount: false,
+ revalidateIfStale: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ });
+ if (error) return error;
+ if (data === undefined) return undefined;
+ if (data.type !== "ok") return data;
+
+ return buildPaginatedResult(
+ data.body.transactions,
+ offset,
+ setOffset,
+ (d) => d.row_id,
+ );
+}
diff --git a/packages/bank-ui/src/hooks/bank-state.ts b/packages/bank-ui/src/hooks/bank-state.ts
new file mode 100644
index 000000000..616678ddc
--- /dev/null
+++ b/packages/bank-ui/src/hooks/bank-state.ts
@@ -0,0 +1,185 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ AbsoluteTime,
+ Codec,
+ TalerCorebankApi,
+ buildCodecForObject,
+ buildCodecForUnion,
+ codecForAbsoluteTime,
+ codecForAny,
+ codecForConstString,
+ codecForString,
+ codecForTanTransmission,
+ codecOptional,
+} from "@gnu-taler/taler-util";
+import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser";
+import { AppLocation } from "@gnu-taler/web-util/browser";
+
+export type ChallengeInProgess =
+ | DeleteAccountChallenge
+ | UpdateAccountChallenge
+ | UpdatePasswordChallenge
+ | CreateTransactionChallenge
+ | ConfirmWithdrawalChallenge
+ | CashoutChallenge;
+
+type BaseChallenge<OpType extends string, ReqType> = {
+ id: string;
+ operation: OpType;
+ sent: AbsoluteTime;
+ location: AppLocation;
+ info?: TalerCorebankApi.TanTransmission;
+ request: ReqType;
+};
+
+type DeleteAccountChallenge = BaseChallenge<"delete-account", string>;
+type UpdateAccountChallenge = BaseChallenge<
+ "update-account",
+ TalerCorebankApi.AccountReconfiguration
+>;
+type UpdatePasswordChallenge = BaseChallenge<
+ "update-password",
+ TalerCorebankApi.AccountPasswordChange
+>;
+type CreateTransactionChallenge = BaseChallenge<
+ "create-transaction",
+ TalerCorebankApi.CreateTransactionRequest
+>;
+type ConfirmWithdrawalChallenge = BaseChallenge<"confirm-withdrawal", string>;
+type CashoutChallenge = BaseChallenge<
+ "create-cashout",
+ TalerCorebankApi.CashoutRequest
+>;
+
+const codecForChallengeUpdatePassword = (): Codec<UpdatePasswordChallenge> =>
+ buildCodecForObject<UpdatePasswordChallenge>()
+ .property("operation", codecForConstString("update-password"))
+ .property("id", codecForString())
+ .property("location", codecForAppLocation())
+ .property("sent", codecForAbsoluteTime)
+ .property("info", codecOptional(codecForTanTransmission()))
+ .property("request", codecForAny())
+ .build("UpdatePasswordChallenge");
+
+const codecForChallengeDeleteAccount = (): Codec<DeleteAccountChallenge> =>
+ buildCodecForObject<DeleteAccountChallenge>()
+ .property("operation", codecForConstString("delete-account"))
+ .property("id", codecForString())
+ .property("location", codecForAppLocation())
+ .property("sent", codecForAbsoluteTime)
+ .property("request", codecForString())
+ .property("info", codecOptional(codecForTanTransmission()))
+ .build("DeleteAccountChallenge");
+
+const codecForChallengeUpdateAccount = (): Codec<UpdateAccountChallenge> =>
+ buildCodecForObject<UpdateAccountChallenge>()
+ .property("operation", codecForConstString("update-account"))
+ .property("id", codecForString())
+ .property("location", codecForAppLocation())
+ .property("sent", codecForAbsoluteTime)
+ .property("info", codecOptional(codecForTanTransmission()))
+ .property("request", codecForAny())
+ .build("UpdateAccountChallenge");
+
+const codecForChallengeCreateTransaction =
+ (): Codec<CreateTransactionChallenge> =>
+ buildCodecForObject<CreateTransactionChallenge>()
+ .property("operation", codecForConstString("create-transaction"))
+ .property("id", codecForString())
+ .property("location", codecForAppLocation())
+ .property("sent", codecForAbsoluteTime)
+ .property("info", codecOptional(codecForTanTransmission()))
+ .property("request", codecForAny())
+ .build("CreateTransactionChallenge");
+
+const codecForChallengeConfirmWithdrawal =
+ (): Codec<ConfirmWithdrawalChallenge> =>
+ buildCodecForObject<ConfirmWithdrawalChallenge>()
+ .property("operation", codecForConstString("confirm-withdrawal"))
+ .property("id", codecForString())
+ .property("location", codecForAppLocation())
+ .property("sent", codecForAbsoluteTime)
+ .property("info", codecOptional(codecForTanTransmission()))
+ .property("request", codecForString())
+ .build("ConfirmWithdrawalChallenge");
+
+const codecForAppLocation = codecForString as () => Codec<AppLocation>;
+
+const codecForChallengeCashout = (): Codec<CashoutChallenge> =>
+ buildCodecForObject<CashoutChallenge>()
+ .property("operation", codecForConstString("create-cashout"))
+ .property("id", codecForString())
+ .property("location", codecForAppLocation())
+ .property("sent", codecForAbsoluteTime)
+ .property("info", codecOptional(codecForTanTransmission()))
+ .property("request", codecForAny())
+ .build("CashoutChallenge");
+
+const codecForChallenge = (): Codec<ChallengeInProgess> =>
+ buildCodecForUnion<ChallengeInProgess>()
+ .discriminateOn("operation")
+ .alternative("confirm-withdrawal", codecForChallengeConfirmWithdrawal())
+ .alternative("create-cashout", codecForChallengeCashout())
+ .alternative("create-transaction", codecForChallengeCreateTransaction())
+ .alternative("delete-account", codecForChallengeDeleteAccount())
+ .alternative("update-account", codecForChallengeUpdateAccount())
+ .alternative("update-password", codecForChallengeUpdatePassword())
+ .build("ChallengeInProgess");
+
+interface BankState {
+ currentWithdrawalOperationId: string | undefined;
+ currentChallenge: ChallengeInProgess | undefined;
+}
+
+export const codecForBankState = (): Codec<BankState> =>
+ buildCodecForObject<BankState>()
+ .property("currentWithdrawalOperationId", codecOptional(codecForString()))
+ .property("currentChallenge", codecOptional(codecForChallenge()))
+ .build("BankState");
+
+const defaultBankState: BankState = {
+ currentWithdrawalOperationId: undefined,
+ currentChallenge: undefined,
+};
+
+const BANK_STATE_KEY = buildStorageKey("bank-app-state", codecForBankState());
+
+/**
+ * Client state saved in local storage.
+ *
+ * This information is saved in the client because
+ * the backend server session API is not enough.
+ *
+ * @returns tuple of [state, update(), reset()]
+ */
+export function useBankState(): [
+ Readonly<BankState>,
+ <T extends keyof BankState>(key: T, value: BankState[T]) => void,
+ () => void,
+] {
+ const { value, update } = useLocalStorage(BANK_STATE_KEY, defaultBankState);
+
+ function updateField<T extends keyof BankState>(k: T, v: BankState[T]) {
+ const newValue = { ...value, [k]: v };
+ update(newValue);
+ }
+ function reset() {
+ update(defaultBankState);
+ }
+ return [value, updateField, reset];
+}
diff --git a/packages/bank-ui/src/hooks/form.ts b/packages/bank-ui/src/hooks/form.ts
new file mode 100644
index 000000000..afa4912eb
--- /dev/null
+++ b/packages/bank-ui/src/hooks/form.ts
@@ -0,0 +1,115 @@
+/*
+ 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 { AmountJson, TranslatedString } from "@gnu-taler/taler-util";
+import { useState } from "preact/hooks";
+
+export type UIField = {
+ value: string | undefined;
+ onUpdate: (s: string) => void;
+ error: TranslatedString | undefined;
+};
+
+type FormHandler<T> = {
+ [k in keyof T]?: T[k] extends string
+ ? UIField
+ : T[k] extends AmountJson
+ ? UIField
+ : FormHandler<T[k]>;
+};
+
+export type FormValues<T> = {
+ [k in keyof T]: T[k] extends string
+ ? string | undefined
+ : T[k] extends AmountJson
+ ? string | undefined
+ : FormValues<T[k]>;
+};
+
+export type RecursivePartial<T> = {
+ [k in keyof T]?: T[k] extends string
+ ? string
+ : T[k] extends AmountJson
+ ? AmountJson
+ : RecursivePartial<T[k]>;
+};
+
+export type FormErrors<T> = {
+ [k in keyof T]?: T[k] extends string
+ ? TranslatedString
+ : T[k] extends AmountJson
+ ? TranslatedString
+ : FormErrors<T[k]>;
+};
+
+export type FormStatus<T> =
+ | {
+ status: "ok";
+ result: T;
+ errors: undefined;
+ }
+ | {
+ status: "fail";
+ result: RecursivePartial<T>;
+ errors: FormErrors<T>;
+ };
+
+function constructFormHandler<T>(
+ form: FormValues<T>,
+ updateForm: (d: FormValues<T>) => void,
+ errors: FormErrors<T> | undefined,
+): FormHandler<T> {
+ const keys = Object.keys(form) as Array<keyof T>;
+
+ const handler = keys.reduce((prev, fieldName) => {
+ const currentValue: unknown = form[fieldName];
+ const currentError: unknown = errors ? errors[fieldName] : undefined;
+ function updater(newValue: unknown) {
+ updateForm({ ...form, [fieldName]: newValue });
+ }
+ if (typeof currentValue === "object") {
+ // @ts-expect-error FIXME better typing
+ const group = constructFormHandler(currentValue, updater, currentError);
+ // @ts-expect-error FIXME better typing
+ prev[fieldName] = group;
+ return prev;
+ }
+ const field: UIField = {
+ // @ts-expect-error FIXME better typing
+ error: currentError,
+ // @ts-expect-error FIXME better typing
+ value: currentValue,
+ onUpdate: updater,
+ };
+ // @ts-expect-error FIXME better typing
+ prev[fieldName] = field;
+ return prev;
+ }, {} as FormHandler<T>);
+
+ return handler;
+}
+
+export function useFormState<T>(
+ defaultValue: FormValues<T>,
+ check: (f: FormValues<T>) => FormStatus<T>,
+): [FormHandler<T>, FormStatus<T>] {
+ const [form, updateForm] = useState<FormValues<T>>(defaultValue);
+
+ const status = check(form);
+ const handler = constructFormHandler(form, updateForm, status.errors);
+
+ return [handler, status];
+}
diff --git a/packages/bank-ui/src/hooks/preferences.ts b/packages/bank-ui/src/hooks/preferences.ts
new file mode 100644
index 000000000..bb3dcb153
--- /dev/null
+++ b/packages/bank-ui/src/hooks/preferences.ts
@@ -0,0 +1,111 @@
+/*
+ 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,
+ codecForNumber,
+} from "@gnu-taler/taler-util";
+import {
+ buildStorageKey,
+ useLocalStorage,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+
+interface Preferences {
+ showWithdrawalSuccess: boolean;
+ showDemoDescription: boolean;
+ showInstallWallet: boolean;
+ maxWithdrawalAmount: number;
+ fastWithdrawal: boolean;
+ showDebugInfo: boolean;
+}
+
+export const codecForPreferences = (): Codec<Preferences> =>
+ buildCodecForObject<Preferences>()
+ .property("showWithdrawalSuccess", codecForBoolean())
+ .property("showDemoDescription", codecForBoolean())
+ .property("showInstallWallet", codecForBoolean())
+ .property("fastWithdrawal", codecForBoolean())
+ .property("showDebugInfo", codecForBoolean())
+ .property("maxWithdrawalAmount", codecForNumber())
+ .build("Settings");
+
+const defaultPreferences: Preferences = {
+ showWithdrawalSuccess: true,
+ showDemoDescription: true,
+ showInstallWallet: true,
+ maxWithdrawalAmount: 25,
+ fastWithdrawal: false,
+ showDebugInfo: false,
+};
+
+const BANK_PREFERENCES_KEY = buildStorageKey(
+ "bank-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(
+ BANK_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 [
+ "fastWithdrawal",
+ "showDebugInfo",
+ "showDemoDescription",
+ "showInstallWallet",
+ "showWithdrawalSuccess",
+ ];
+}
+
+export function getLabelForPreferences(
+ k: keyof Preferences,
+ 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 "showDemoDescription":
+ return i18n.str`Show demo description`;
+ case "showInstallWallet":
+ return i18n.str`Show install wallet first`;
+ case "fastWithdrawal":
+ return i18n.str`Use fast withdrawal form`;
+ case "showDebugInfo":
+ return i18n.str`Show debug info`;
+ }
+}
diff --git a/packages/bank-ui/src/hooks/regional.ts b/packages/bank-ui/src/hooks/regional.ts
new file mode 100644
index 000000000..e0c861a0f
--- /dev/null
+++ b/packages/bank-ui/src/hooks/regional.ts
@@ -0,0 +1,507 @@
+/*
+ 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 { useSessionState } from "./session.js";
+
+import {
+ AbsoluteTime,
+ AccessToken,
+ AmountJson,
+ Amounts,
+ HttpStatusCode,
+ OperationOk,
+ TalerBankConversionResultByMethod,
+ TalerCoreBankErrorsByMethod,
+ TalerCoreBankResultByMethod,
+ TalerCorebankApi,
+ TalerError,
+ TalerHttpError,
+ opFixedSuccess,
+} from "@gnu-taler/taler-util";
+import { useBankCoreApiContext } from "@gnu-taler/web-util/browser";
+import { useState } from "preact/hooks";
+import _useSWR, { SWRHook, mutate } from "swr";
+import { buildPaginatedResult } from "./account.js";
+import { PAGINATED_LIST_REQUEST } from "../utils.js";
+
+// FIX default import https://github.com/microsoft/TypeScript/issues/49189
+const useSWR = _useSWR as unknown as SWRHook;
+
+export type TransferCalculation =
+ | {
+ debit: AmountJson;
+ credit: AmountJson;
+ beforeFee: AmountJson;
+ }
+ | "amount-is-too-small";
+type EstimatorFunction = (
+ amount: AmountJson,
+ fee: AmountJson,
+) => Promise<TransferCalculation>;
+
+type ConversionEstimators = {
+ estimateByCredit: EstimatorFunction;
+ estimateByDebit: EstimatorFunction;
+};
+
+export function revalidateConversionInfo() {
+ return mutate(
+ (key) =>
+ Array.isArray(key) && key[key.length - 1] === "getConversionInfoAPI",
+ );
+}
+export function useConversionInfo() {
+ const {
+ lib: { conversion },
+ config,
+ } = useBankCoreApiContext();
+
+ async function fetcher() {
+ return await conversion.getConfig();
+ }
+ const { data, error } = useSWR<
+ TalerBankConversionResultByMethod<"getConfig">,
+ TalerHttpError
+ >(!config.allow_conversion ? undefined : ["getConversionInfoAPI"], fetcher, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ errorRetryCount: 0,
+ errorRetryInterval: 1,
+ shouldRetryOnError: false,
+ keepPreviousData: true,
+ });
+
+ if (data) return data;
+ if (error) return error;
+ return undefined;
+}
+
+export function useCashinEstimator(): ConversionEstimators {
+ const {
+ lib: { conversion },
+ } = useBankCoreApiContext();
+ return {
+ estimateByCredit: async (fiatAmount, fee) => {
+ const resp = await conversion.getCashinRate({
+ credit: fiatAmount,
+ });
+ if (resp.type === "fail") {
+ switch (resp.case) {
+ case HttpStatusCode.Conflict: {
+ return "amount-is-too-small";
+ }
+ // this below can't happen
+ case HttpStatusCode.NotImplemented: //it should not be able to call this function
+ case HttpStatusCode.BadRequest: //we are using just one parameter
+ throw TalerError.fromDetail(resp.detail.code, {}, resp.detail.hint);
+ }
+ }
+ const credit = Amounts.parseOrThrow(resp.body.amount_credit);
+ const debit = Amounts.parseOrThrow(resp.body.amount_debit);
+ const beforeFee = Amounts.sub(credit, fee).amount;
+
+ return {
+ debit,
+ beforeFee,
+ credit,
+ };
+ },
+ estimateByDebit: async (regionalAmount, fee) => {
+ const resp = await conversion.getCashinRate({
+ debit: regionalAmount,
+ });
+ if (resp.type === "fail") {
+ switch (resp.case) {
+ case HttpStatusCode.Conflict: {
+ return "amount-is-too-small";
+ }
+ // this below can't happen
+ case HttpStatusCode.NotImplemented: //it should not be able to call this function
+ case HttpStatusCode.BadRequest: //we are using just one parameter
+ throw TalerError.fromDetail(resp.detail.code, {}, resp.detail.hint);
+ }
+ }
+ const credit = Amounts.parseOrThrow(resp.body.amount_credit);
+ const debit = Amounts.parseOrThrow(resp.body.amount_debit);
+ const beforeFee = Amounts.add(credit, fee).amount;
+
+ return {
+ debit,
+ beforeFee,
+ credit,
+ };
+ },
+ };
+}
+
+export function useCashoutEstimator(): ConversionEstimators {
+ const {
+ lib: { conversion },
+ } = useBankCoreApiContext();
+ return {
+ estimateByCredit: async (fiatAmount, fee) => {
+ const resp = await conversion.getCashoutRate({
+ credit: fiatAmount,
+ });
+ if (resp.type === "fail") {
+ switch (resp.case) {
+ case HttpStatusCode.Conflict: {
+ return "amount-is-too-small";
+ }
+ // this below can't happen
+ case HttpStatusCode.NotImplemented: //it should not be able to call this function
+ case HttpStatusCode.BadRequest: //we are using just one parameter
+ throw TalerError.fromDetail(resp.detail.code, {}, resp.detail.hint);
+ }
+ }
+ const credit = Amounts.parseOrThrow(resp.body.amount_credit);
+ const debit = Amounts.parseOrThrow(resp.body.amount_debit);
+ const beforeFee = Amounts.sub(credit, fee).amount;
+
+ return {
+ debit,
+ beforeFee,
+ credit,
+ };
+ },
+ estimateByDebit: async (regionalAmount, fee) => {
+ const resp = await conversion.getCashoutRate({
+ debit: regionalAmount,
+ });
+ if (resp.type === "fail") {
+ switch (resp.case) {
+ case HttpStatusCode.Conflict: {
+ return "amount-is-too-small";
+ }
+ // this below can't happen
+ case HttpStatusCode.NotImplemented: //it should not be able to call this function
+ case HttpStatusCode.BadRequest: //we are using just one parameter
+ throw TalerError.fromDetail(resp.detail.code, {}, resp.detail.hint);
+ }
+ }
+ const credit = Amounts.parseOrThrow(resp.body.amount_credit);
+ const debit = Amounts.parseOrThrow(resp.body.amount_debit);
+ const beforeFee = Amounts.add(credit, fee).amount;
+
+ return {
+ debit,
+ beforeFee,
+ credit,
+ };
+ },
+ };
+}
+
+/**
+ * @deprecated use useCashoutEstimator
+ */
+export function useEstimator(): ConversionEstimators {
+ return useCashoutEstimator();
+}
+
+export async function revalidateBusinessAccounts() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "getAccounts",
+ undefined,
+ { revalidate: true },
+ );
+}
+export function useBusinessAccounts() {
+ const { state: credentials } = useSessionState();
+ const token =
+ credentials.status !== "loggedIn" ? undefined : credentials.token;
+ const {
+ lib: { bank: api },
+ } = useBankCoreApiContext();
+
+ const [offset, setOffset] = useState<number | undefined>();
+
+ function fetcher([token, aid]: [AccessToken, number]) {
+ // FIXME: add account name filter
+ return api.getAccounts(
+ token,
+ {},
+ {
+ limit: PAGINATED_LIST_REQUEST,
+ offset: aid ? String(aid) : undefined,
+ order: "asc",
+ },
+ );
+ }
+
+ const { data, error } = useSWR<
+ TalerCoreBankResultByMethod<"getAccounts">,
+ TalerHttpError
+ >([token, offset ?? 0, "getAccounts"], fetcher, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ errorRetryCount: 0,
+ errorRetryInterval: 1,
+ shouldRetryOnError: false,
+ keepPreviousData: true,
+ });
+
+ if (error) return error;
+ if (data === undefined) return undefined;
+ if (data.type !== "ok") return data;
+
+ //TODO: row_id should not be optional
+ return buildPaginatedResult(
+ data.body.accounts,
+ offset,
+ setOffset,
+ (d) => d.row_id ?? 0,
+ );
+}
+
+type CashoutWithId = TalerCorebankApi.CashoutStatusResponse & { id: number };
+function notUndefined(c: CashoutWithId | undefined): c is CashoutWithId {
+ return c !== undefined;
+}
+export function revalidateOnePendingCashouts() {
+ return mutate(
+ (key) =>
+ Array.isArray(key) && key[key.length - 1] === "useOnePendingCashouts",
+ undefined,
+ { revalidate: true },
+ );
+}
+export function useOnePendingCashouts(account: string) {
+ const { state: credentials } = useSessionState();
+ const {
+ lib: { bank: api },
+ config,
+ } = useBankCoreApiContext();
+ const token =
+ credentials.status !== "loggedIn" ? undefined : credentials.token;
+
+ async function fetcher([username, token]: [string, AccessToken]) {
+ const list = await api.getAccountCashouts({ username, token });
+ if (list.type !== "ok") {
+ return list;
+ }
+ const pendingCashout =
+ list.body.cashouts.length > 0 ? list.body.cashouts[0] : undefined;
+ if (!pendingCashout) return opFixedSuccess(undefined);
+ const cashoutInfo = await api.getCashoutById(
+ { username, token },
+ pendingCashout.cashout_id,
+ );
+ if (cashoutInfo.type !== "ok") {
+ return cashoutInfo;
+ }
+ return opFixedSuccess({
+ ...cashoutInfo.body,
+ id: pendingCashout.cashout_id,
+ });
+ }
+
+ const { data, error } = useSWR<
+ | OperationOk<CashoutWithId | undefined>
+ | TalerCoreBankErrorsByMethod<"getAccountCashouts">
+ | TalerCoreBankErrorsByMethod<"getCashoutById">,
+ TalerHttpError
+ >(
+ !config.allow_conversion
+ ? undefined
+ : [account, token, "useOnePendingCashouts"],
+ fetcher,
+ {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ errorRetryCount: 0,
+ errorRetryInterval: 1,
+ shouldRetryOnError: false,
+ keepPreviousData: true,
+ },
+ );
+
+ if (data) return data;
+ if (error) return error;
+ return undefined;
+}
+
+export function revalidateCashouts() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "useCashouts",
+ );
+}
+export function useCashouts(account: string) {
+ const { state: credentials } = useSessionState();
+ const {
+ lib: { bank: api },
+ config,
+ } = useBankCoreApiContext();
+ const token =
+ credentials.status !== "loggedIn" ? undefined : credentials.token;
+
+ async function fetcher([username, token]: [string, AccessToken]) {
+ const list = await api.getAccountCashouts({ username, token });
+ if (list.type !== "ok") {
+ return list;
+ }
+ const all: Array<CashoutWithId | undefined> = await Promise.all(
+ list.body.cashouts.map(async (c) => {
+ const r = await api.getCashoutById({ username, token }, c.cashout_id);
+ if (r.type === "fail") {
+ return undefined;
+ }
+ return { ...r.body, id: c.cashout_id };
+ }),
+ );
+ const cashouts = all.filter(notUndefined);
+ return { type: "ok" as const, body: { cashouts } };
+ }
+ const { data, error } = useSWR<
+ | OperationOk<{ cashouts: CashoutWithId[] }>
+ | TalerCoreBankErrorsByMethod<"getAccountCashouts">,
+ TalerHttpError
+ >(
+ !config.allow_conversion ? undefined : [account, token, "useCashouts"],
+ fetcher,
+ {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ errorRetryCount: 0,
+ errorRetryInterval: 1,
+ shouldRetryOnError: false,
+ keepPreviousData: true,
+ },
+ );
+
+ if (data) return data;
+ if (error) return error;
+ return undefined;
+}
+
+export function revalidateCashoutDetails() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "getCashoutById",
+ undefined,
+ { revalidate: true },
+ );
+}
+export function useCashoutDetails(cashoutId: number | undefined) {
+ const { state: credentials } = useSessionState();
+ const creds = credentials.status !== "loggedIn" ? undefined : credentials;
+ const {
+ lib: { bank: api },
+ } = useBankCoreApiContext();
+
+ async function fetcher([username, token, id]: [string, AccessToken, number]) {
+ return api.getCashoutById({ username, token }, id);
+ }
+
+ const { data, error } = useSWR<
+ TalerCoreBankResultByMethod<"getCashoutById">,
+ TalerHttpError
+ >(
+ cashoutId === undefined
+ ? undefined
+ : [creds?.username, creds?.token, cashoutId, "getCashoutById"],
+ fetcher,
+ {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ errorRetryCount: 0,
+ errorRetryInterval: 1,
+ shouldRetryOnError: false,
+ keepPreviousData: true,
+ },
+ );
+
+ if (data) return data;
+ if (error) return error;
+ return undefined;
+}
+export type MonitorMetrics = {
+ lastHour: TalerCoreBankResultByMethod<"getMonitor">;
+ lastDay: TalerCoreBankResultByMethod<"getMonitor">;
+ lastMonth: TalerCoreBankResultByMethod<"getMonitor">;
+};
+
+export type LastMonitor = {
+ current: TalerCoreBankResultByMethod<"getMonitor">;
+ previous: TalerCoreBankResultByMethod<"getMonitor">;
+};
+export function revalidateLastMonitorInfo() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "useLastMonitorInfo",
+ undefined,
+ { revalidate: true },
+ );
+}
+export function useLastMonitorInfo(
+ currentMoment: AbsoluteTime,
+ previousMoment: AbsoluteTime,
+ timeframe: TalerCorebankApi.MonitorTimeframeParam,
+) {
+ const {
+ lib: { bank: api },
+ } = useBankCoreApiContext();
+ const { state: credentials } = useSessionState();
+ const token =
+ credentials.status !== "loggedIn" ? undefined : credentials.token;
+
+ async function fetcher([token, timeframe]: [
+ AccessToken,
+ TalerCorebankApi.MonitorTimeframeParam,
+ ]) {
+ const [current, previous] = await Promise.all([
+ api.getMonitor(token, { timeframe, date: currentMoment }),
+ api.getMonitor(token, { timeframe, date: previousMoment }),
+ ]);
+ return {
+ current,
+ previous,
+ };
+ }
+
+ const { data, error } = useSWR<LastMonitor, TalerHttpError>(
+ !token ? undefined : [token, timeframe, "useLastMonitorInfo"],
+ fetcher,
+ {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ errorRetryCount: 0,
+ errorRetryInterval: 1,
+ shouldRetryOnError: false,
+ keepPreviousData: true,
+ },
+ );
+
+ if (data) return data;
+ if (error) return error;
+ return undefined;
+}
diff --git a/packages/bank-ui/src/hooks/session.ts b/packages/bank-ui/src/hooks/session.ts
new file mode 100644
index 000000000..4520d0e4a
--- /dev/null
+++ b/packages/bank-ui/src/hooks/session.ts
@@ -0,0 +1,134 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ AccessToken,
+ Codec,
+ buildCodecForObject,
+ buildCodecForUnion,
+ codecForBoolean,
+ codecForConstString,
+ codecForString,
+} from "@gnu-taler/taler-util";
+import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser";
+import { mutate } from "swr";
+
+/**
+ * Has the information to reach and
+ * authenticate at the bank's backend.
+ */
+export type SessionState = LoggedIn | LoggedOut | Expired;
+
+interface LoggedIn {
+ status: "loggedIn";
+ isUserAdministrator: boolean;
+ username: string;
+ token: AccessToken;
+}
+interface Expired {
+ status: "expired";
+ isUserAdministrator: boolean;
+ username: string;
+}
+interface LoggedOut {
+ status: "loggedOut";
+}
+
+export const codecForSessionStateLoggedIn = (): Codec<LoggedIn> =>
+ buildCodecForObject<LoggedIn>()
+ .property("status", codecForConstString("loggedIn"))
+ .property("username", codecForString())
+ .property("token", codecForString() as Codec<AccessToken>)
+ .property("isUserAdministrator", codecForBoolean())
+ .build("SessionState.LoggedIn");
+
+export const codecForSessionStateExpired = (): Codec<Expired> =>
+ buildCodecForObject<Expired>()
+ .property("status", codecForConstString("expired"))
+ .property("username", codecForString())
+ .property("isUserAdministrator", codecForBoolean())
+ .build("SessionState.Expired");
+
+export const codecForSessionStateLoggedOut = (): Codec<LoggedOut> =>
+ buildCodecForObject<LoggedOut>()
+ .property("status", codecForConstString("loggedOut"))
+ .build("SessionState.LoggedOut");
+
+export const codecForSessionState = (): Codec<SessionState> =>
+ buildCodecForUnion<SessionState>()
+ .discriminateOn("status")
+ .alternative("loggedIn", codecForSessionStateLoggedIn())
+ .alternative("loggedOut", codecForSessionStateLoggedOut())
+ .alternative("expired", codecForSessionStateExpired())
+ .build("SessionState");
+
+export const defaultState: SessionState = {
+ status: "loggedOut",
+};
+
+export interface SessionStateHandler {
+ state: SessionState;
+ logOut(): void;
+ expired(): void;
+ logIn(info: { username: string; token: AccessToken }): void;
+}
+
+const SESSION_STATE_KEY = buildStorageKey(
+ "bank-session",
+ codecForSessionState(),
+);
+
+/**
+ * Return getters and setters for
+ * login credentials and backend's
+ * base URL.
+ */
+export function useSessionState(): SessionStateHandler {
+ const { value: state, update } = useLocalStorage(
+ SESSION_STATE_KEY,
+ defaultState,
+ );
+
+ return {
+ state,
+ logOut() {
+ update(defaultState);
+ },
+ expired() {
+ if (state.status === "loggedOut") return;
+ const nextState: SessionState = {
+ status: "expired",
+ username: state.username,
+ isUserAdministrator: state.username === "admin",
+ };
+ update(nextState);
+ },
+ logIn(info) {
+ // admin is defined by the username
+ const nextState: SessionState = {
+ status: "loggedIn",
+ ...info,
+ isUserAdministrator: info.username === "admin",
+ };
+ update(nextState);
+ cleanAllCache();
+ },
+ };
+}
+
+function cleanAllCache(): void {
+ mutate(() => true, undefined, { revalidate: false });
+}
diff --git a/packages/bank-ui/src/i18n/bank.pot b/packages/bank-ui/src/i18n/bank.pot
new file mode 100644
index 000000000..1f11b8f10
--- /dev/null
+++ b/packages/bank-ui/src/i18n/bank.pot
@@ -0,0 +1,1740 @@
+# This file is part of GNU Taler
+# (C) 2022-2024 Taler Systems S.A.
+#
+# GNU Taler is free software; you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 3, or (at your option) any later version.
+#
+# GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Bank\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+#: src/utils.ts:137
+#, c-format
+msgid "Operation failed, please report"
+msgstr ""
+
+#: src/utils.ts:156
+#, c-format
+msgid "Request timeout"
+msgstr ""
+
+#: src/utils.ts:165
+#, c-format
+msgid "Request throttled"
+msgstr ""
+
+#: src/utils.ts:174
+#, c-format
+msgid "Malformed response"
+msgstr ""
+
+#: src/utils.ts:183
+#, c-format
+msgid "Network error"
+msgstr ""
+
+#: src/utils.ts:192
+#, c-format
+msgid "Unexpected request error"
+msgstr ""
+
+#: src/utils.ts:201
+#, c-format
+msgid "Unexpected error"
+msgstr ""
+
+#: src/utils.ts:377
+#, c-format
+msgid "IBAN numbers usually have more that 4 digits"
+msgstr ""
+
+#: src/utils.ts:379
+#, c-format
+msgid "IBAN numbers usually have less that 34 digits"
+msgstr ""
+
+#: src/utils.ts:387
+#, c-format
+msgid "IBAN country code not found"
+msgstr ""
+
+#: src/utils.ts:401
+#, c-format
+msgid "IBAN number is not valid, checksum is wrong"
+msgstr ""
+
+#: src/context/config.ts:136
+#, c-format
+msgid ""
+"the bank backend is not supported. supported version \"%1$s\", server version "
+"\"%2$s\""
+msgstr ""
+
+#: src/hooks/preferences.ts:55
+#, c-format
+msgid "Max withdrawal amount"
+msgstr ""
+
+#: src/hooks/preferences.ts:57
+#, c-format
+msgid "Show withdrawal confirmation"
+msgstr ""
+
+#: src/hooks/preferences.ts:59
+#, c-format
+msgid "Show demo description"
+msgstr ""
+
+#: src/hooks/preferences.ts:61
+#, c-format
+msgid "Show install wallet first"
+msgstr ""
+
+#: src/hooks/preferences.ts:63
+#, c-format
+msgid "Use fast withdrawal form"
+msgstr ""
+
+#: src/hooks/preferences.ts:65
+#, c-format
+msgid "Show debug info"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:90
+#, c-format
+msgid "required"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:92
+#, c-format
+msgid "IBAN should have just uppercased letters and numbers"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:98
+#, c-format
+msgid "not valid"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:100
+#, c-format
+msgid "should be greater than 0"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:102
+#, c-format
+msgid "balance is not enough"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:112
+#, c-format
+msgid "does not follow the pattern"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:114
+#, c-format
+msgid "only \"IBAN\" target are supported"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:116
+#, c-format
+msgid "use the \"amount\" parameter to specify the amount to be transferred"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:118
+#, c-format
+msgid "the amount is not valid"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:120
+#, c-format
+msgid "use the \"message\" parameter to specify a reference text for the transfer"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:160
+#, c-format
+msgid "The request was invalid or the payto://-URI used unacceptable features."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:167
+#, c-format
+msgid "Not enough permission to complete the operation."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:174
+#, c-format
+msgid "The destination account \"%1$s\" was not found."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:181
+#, c-format
+msgid "The origin and the destination of the transfer can't be the same."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:188
+#, c-format
+msgid "Your balance is not enough."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:195
+#, c-format
+msgid "The origin account \"%1$s\" was not found."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:212
+#, c-format
+msgid "Wire transfer created!"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:270
+#, c-format
+msgid "Using a form"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:310
+#, c-format
+msgid "Import payto:// URI"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:335
+#, c-format
+msgid "Recipient"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:359
+#, c-format
+msgid "IBAN of the recipient's account"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:369
+#, c-format
+msgid "Transfer subject"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:377
+#, c-format
+msgid "subject"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:390
+#, c-format
+msgid "some text to identify the transfer"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:400
+#, c-format
+msgid "Amount"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:415
+#, c-format
+msgid "amount to transfer"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:425
+#, c-format
+msgid "payto URI:"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:436
+#, c-format
+msgid "uniform resource identifier of the target account"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:437
+#, c-format
+msgid "payto://iban/[receiver-iban]?message=[subject]&amount=[%1$s:X.Y]"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:457
+#, c-format
+msgid "Cancel"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:471
+#, c-format
+msgid "Send"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:71
+#, c-format
+msgid "Missing username"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:75
+#, c-format
+msgid "Missing password"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:104
+#, c-format
+msgid "Wrong credentials for \"%1$s\""
+msgstr ""
+
+#: src/pages/LoginForm.tsx:111
+#, c-format
+msgid "Account not found"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:142
+#, c-format
+msgid "Username"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:156
+#, c-format
+msgid "username of the account"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:175
+#, c-format
+msgid "Password"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:188
+#, c-format
+msgid "password of the account"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:223
+#, c-format
+msgid "Check"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:237
+#, c-format
+msgid "Log in"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:249
+#, c-format
+msgid "Register"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:52
+#, c-format
+msgid "Latest transactions"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:63
+#, c-format
+msgid "Date"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:71
+#, c-format
+msgid "Counterpart"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:75
+#, c-format
+msgid "Subject"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:111
+#, c-format
+msgid "sent"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:112
+#, c-format
+msgid "received"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:127
+#, c-format
+msgid "invalid value"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:136
+#, c-format
+msgid "to"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:136
+#, c-format
+msgid "from"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:202
+#, c-format
+msgid "First page"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:209
+#, c-format
+msgid "Next"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:86
+#, c-format
+msgid "Wire transfer completed!"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:93
+#, c-format
+msgid "The withdrawal has been aborted previously and can't be confirmed"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:100
+#, c-format
+msgid ""
+"The withdrawal operation can't be confirmed before a wallet accepted the "
+"transaction."
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:107
+#, c-format
+msgid "The operation id is invalid."
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:114
+#, c-format
+msgid "The operation was not found."
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:121
+#, c-format
+msgid "Your balance is not enough for the operation."
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:155
+#, c-format
+msgid "The reserve operation has been confirmed previously and can't be aborted"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:186
+#, c-format
+msgid "Confirm the withdrawal operation"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:203
+#, c-format
+msgid "Wire transfer details"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:217
+#, c-format
+msgid "Taler Exchange operator's account"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:228
+#, c-format
+msgid "Taler Exchange operator's name"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:317
+#, c-format
+msgid "Transfer"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:342
+#, c-format
+msgid "Authentication required"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:352
+#, c-format
+msgid "This operation was created with other username"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:209
+#, c-format
+msgid ""
+"Unauthorized to make the operation, maybe the session has expired or the "
+"password changed."
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:218
+#, c-format
+msgid "The operation was rejected due to insufficient funds."
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:268
+#, c-format
+msgid "Withdrawal confirmed"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:272
+#, c-format
+msgid ""
+"The wire transfer to the Taler operator has been initiated. You will soon "
+"receive the requested amount in your Taler wallet."
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:287
+#, c-format
+msgid "Do not show this again"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:319
+#, c-format
+msgid "Close"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:399
+#, c-format
+msgid "On this device"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:404
+#, c-format
+msgid ""
+"If you are using a web browser on desktop you should access your wallet with the "
+"GNU Taler WebExtension now or click the link if your WebExtension have the "
+"\"Inject Taler support\" option enabled."
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:417
+#, c-format
+msgid "Start"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:426
+#, c-format
+msgid "On a mobile phone"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:431
+#, c-format
+msgid "Scan the QR code with your mobile device."
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:73
+#, c-format
+msgid "There is an operation already"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:75
+#, c-format
+msgid "Complete or cancel the operation in"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:84
+#, c-format
+msgid "this page"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:101
+#, c-format
+msgid "invalid"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:116
+#, c-format
+msgid "Server responded with an invalid withdraw URI"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:117
+#, c-format
+msgid "Withdraw URI: %1$s"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:132
+#, c-format
+msgid "The operation was rejected due to insufficient funds"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:253
+#, c-format
+msgid "Continue"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:282
+#, c-format
+msgid "Prepare your wallet"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:285
+#, c-format
+msgid ""
+"After using your wallet you will need to confirm or cancel the operation on this "
+"site."
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:295
+#, c-format
+msgid "You need a GNU Taler Wallet"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:300
+#, c-format
+msgid "If you don't have one yet you can follow the instruction in"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:55
+#, c-format
+msgid "Send money"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:73
+#, c-format
+msgid "to a %1$s wallet"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:95
+#, c-format
+msgid "Withdraw digital money into your mobile wallet or browser extension"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:109
+#, c-format
+msgid "operation ready"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:129
+#, c-format
+msgid "to another bank account"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:149
+#, c-format
+msgid "Make a wire transfer to an account with known bank account number."
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:171
+#, c-format
+msgid "Transfer details"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:41
+#, c-format
+msgid "This is a demo bank"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:46
+#, c-format
+msgid ""
+"This part of the demo shows how a bank that supports Taler directly would work. "
+"In addition to using your own bank account, you can also see the transaction "
+"history of some %1$s."
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:53
+#, c-format
+msgid "This part of the demo shows how a bank that supports Taler directly would work."
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:70
+#, c-format
+msgid "Pending account delete operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:72
+#, c-format
+msgid "Pending account update operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:74
+#, c-format
+msgid "Pending password update operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:76
+#, c-format
+msgid "Pending transaction operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:78
+#, c-format
+msgid "Pending withdrawal operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:80
+#, c-format
+msgid "Pending cashout operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:91
+#, c-format
+msgid "You can complete or cancel the operation in"
+msgstr ""
+
+#: src/pages/BankFrame.tsx:64
+#, c-format
+msgid "Internal error, please report."
+msgstr ""
+
+#: src/pages/BankFrame.tsx:100
+#, c-format
+msgid "Preferences"
+msgstr ""
+
+#: src/pages/BankFrame.tsx:184
+#, c-format
+msgid "Welcome, %1$s"
+msgstr ""
+
+#: src/pages/WireTransfer.tsx:79
+#, c-format
+msgid "Make a wire transfer"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:72
+#, c-format
+msgid "Accounts"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:75
+#, c-format
+msgid "A list of all business account in the bank."
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:86
+#, c-format
+msgid "Create account"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:106
+#, c-format
+msgid "Name"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:110
+#, c-format
+msgid "Balance"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:112
+#, c-format
+msgid "Actions"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:151
+#, c-format
+msgid "unknown"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:170
+#, c-format
+msgid "change password"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:179
+#, c-format
+msgid "cashouts"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:189
+#, c-format
+msgid "remove"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:168
+#, c-format
+msgid "Cashout not implemented"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:184
+#, c-format
+msgid "Select a section"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:202
+#, c-format
+msgid "Last hour"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:208
+#, c-format
+msgid "Last day"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:216
+#, c-format
+msgid "Last month"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:222
+#, c-format
+msgid "Last year"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:310
+#, c-format
+msgid "Last Year"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:325
+#, c-format
+msgid "Trading volume on %1$s compared to %2$s"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:342
+#, c-format
+msgid "Cashin"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:352
+#, c-format
+msgid "Cashout"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:364
+#, c-format
+msgid "Payin"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:374
+#, c-format
+msgid "Payout"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:388
+#, c-format
+msgid "download stats as CSV"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:494
+#, c-format
+msgid "Decreased by"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:498
+#, c-format
+msgid "Increased by"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:89
+#, c-format
+msgid "Download bank stats"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:110
+#, c-format
+msgid "Include hour metric"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:143
+#, c-format
+msgid "Include day metric"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:173
+#, c-format
+msgid "Include month metric"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:206
+#, c-format
+msgid "Include year metric"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:239
+#, c-format
+msgid "Include table header"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:272
+#, c-format
+msgid "Add previous metric for compare"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:307
+#, c-format
+msgid "Fail on first error"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:364
+#, c-format
+msgid "Download"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:381
+#, c-format
+msgid "downloading... %1$s"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:399
+#, c-format
+msgid "Download completed"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:400
+#, c-format
+msgid "click here to save the file in your computer"
+msgstr ""
+
+#: src/pages/PublicHistoriesPage.tsx:78
+#, c-format
+msgid "History of public accounts"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:48
+#, c-format
+msgid "Currently, the bank is not accepting new registrations!"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:87
+#, c-format
+msgid "Missing name"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:91
+#, c-format
+msgid "Use letters and numbers only, and start with a lowercase letter"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:107
+#, c-format
+msgid "Passwords don't match"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:130
+#, c-format
+msgid "Server replied with invalid phone or email."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:137
+#, c-format
+msgid "Registration is disabled because the bank ran out of bonus credit."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:144
+#, c-format
+msgid "No enough permission to create that account."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:151
+#, c-format
+msgid "That account id is already taken."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:158
+#, c-format
+msgid "That username is already taken."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:165
+#, c-format
+msgid "That username can't be used because is reserved."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:172
+#, c-format
+msgid "Only admin is allow to set debt limit."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:179
+#, c-format
+msgid "No information for the selected authentication channel."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:186
+#, c-format
+msgid "Authentication channel is not supported."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:193
+#, c-format
+msgid "Only admin can create accounts with second factor authentication."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:233
+#, c-format
+msgid "Account registration"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:315
+#, c-format
+msgid "Repeat password"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:457
+#, c-format
+msgid "Create a random temporary user"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:110
+#, c-format
+msgid "If you have a Taler wallet installed in this device"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:116
+#, c-format
+msgid ""
+"You will see the details of the operation in your wallet including the fees (if "
+"applies). If you still don't have one you can install it following instructions "
+"in"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:143
+#, c-format
+msgid "Withdraw"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:152
+#, c-format
+msgid "Or if you have the wallet in another device"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:157
+#, c-format
+msgid "Scan the QR below to start the withdrawal."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:79
+#, c-format
+msgid "Operation aborted"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:82
+#, c-format
+msgid ""
+"The wire transfer to the Taler Exchange operator's account was aborted, your "
+"balance was not affected."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:88
+#, c-format
+msgid "You can close this page now or continue to the account page."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:147
+#, c-format
+msgid "Done"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:158
+#, c-format
+msgid "Operation canceled"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:173
+#, c-format
+msgid "The operation is marked as 'selected' but some step in the withdrawal failed"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:175
+#, c-format
+msgid "The account is selected but no withdrawal identification found."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:188
+#, c-format
+msgid ""
+"There is a withdrawal identification but no account has been selected or the "
+"selected account is invalid."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:202
+#, c-format
+msgid ""
+"No withdrawal ID found and no account has been selected or the selected account "
+"is invalid."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:259
+#, c-format
+msgid "Operation not found"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:263
+#, c-format
+msgid ""
+"This operation is not known by the server. The operation id is wrong or the "
+"server deleted the operation information before reaching here."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:278
+#, c-format
+msgid "Cotinue to dashboard"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:98
+#, c-format
+msgid "Cashout not found. It may be also mean that it was already aborted."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:136
+#, c-format
+msgid "Challenge not found."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:143
+#, c-format
+msgid "This user is not authorized to complete this challenge."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:150
+#, c-format
+msgid "Too many attempts, try another code."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:157
+#, c-format
+msgid "The confirmation code is wrong, try again."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:164
+#, c-format
+msgid "The operation expired."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:197
+#, c-format
+msgid "The operation failed."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:212
+#, c-format
+msgid "The operation needs another confirmation to complete."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:224
+#, c-format
+msgid "Account delete"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:226
+#, c-format
+msgid "Account update"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:228
+#, c-format
+msgid "Password update"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:230
+#, c-format
+msgid "Wire transfer"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:232
+#, c-format
+msgid "Withdrawal"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:248
+#, c-format
+msgid "Confirm the operation"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:271
+#, c-format
+msgid "Enter the confirmation code"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:313
+#, c-format
+msgid "Confirm"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:348
+#, c-format
+msgid "Send again"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:359
+#, c-format
+msgid "Send code"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:369
+#, c-format
+msgid "Operation details"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:529
+#, c-format
+msgid "Challenge details"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:536
+#, c-format
+msgid "Sent at"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:551
+#, c-format
+msgid "To phone"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:553
+#, c-format
+msgid "To email"
+msgstr ""
+
+#: src/pages/WithdrawalOperationPage.tsx:49
+#, c-format
+msgid "The Withdrawal URI is not valid"
+msgstr ""
+
+#: src/components/Cashouts/views.tsx:100
+#, c-format
+msgid "Latest cashouts"
+msgstr ""
+
+#: src/components/Cashouts/views.tsx:111
+#, c-format
+msgid "Created"
+msgstr ""
+
+#: src/components/Cashouts/views.tsx:115
+#, c-format
+msgid "Total debit"
+msgstr ""
+
+#: src/components/Cashouts/views.tsx:119
+#, c-format
+msgid "Total credit"
+msgstr ""
+
+#: src/pages/ProfileNavigation.tsx:70
+#, c-format
+msgid "Details"
+msgstr ""
+
+#: src/pages/ProfileNavigation.tsx:74
+#, c-format
+msgid "Delete"
+msgstr ""
+
+#: src/pages/ProfileNavigation.tsx:78
+#, c-format
+msgid "Credentials"
+msgstr ""
+
+#: src/pages/ProfileNavigation.tsx:82
+#, c-format
+msgid "Cashouts"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:95
+#, c-format
+msgid "Unable to create a cashout"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:96
+#, c-format
+msgid "The bank configuration does not support cashout operations."
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:223
+#, c-format
+msgid "need to be higher due to fees"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:225
+#, c-format
+msgid "the total transfer at destination will be zero"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:250
+#, c-format
+msgid "Cashout created"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:272
+#, c-format
+msgid "Duplicated request detected, check if the operation succeeded or try again."
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:279
+#, c-format
+msgid "The conversion rate was incorrectly applied"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:286
+#, c-format
+msgid "The account does not have sufficient funds"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:293
+#, c-format
+msgid "Cashouts are not supported"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:300
+#, c-format
+msgid "Missing cashout URI in the profile"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:307
+#, c-format
+msgid ""
+"Sending the confirmation message failed, retry later or contact the "
+"administrator."
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:339
+#, c-format
+msgid "Conversion rate"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:360
+#, c-format
+msgid "Fee"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:374
+#, c-format
+msgid "To account"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:381
+#, c-format
+msgid "No cashout account"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:382
+#, c-format
+msgid "Before doing a cashout you need to complete your profile"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:440
+#, c-format
+msgid "Amount to send"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:441
+#, c-format
+msgid "Amount to receive"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:490
+#, c-format
+msgid "Total cost"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:505
+#, c-format
+msgid "Balance left"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:520
+#, c-format
+msgid "Before fee"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:533
+#, c-format
+msgid "Total cashout transfer"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:553
+#, c-format
+msgid "No cashout channel available"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:555
+#, c-format
+msgid ""
+"Before doing a cashout the server need to provide an second channel to confirm "
+"the operation"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:567
+#, c-format
+msgid "Second factor authentication"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:598
+#, c-format
+msgid "Email"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:600
+#, c-format
+msgid "add a email in your profile to enable this option"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:646
+#, c-format
+msgid "SMS"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:648
+#, c-format
+msgid "add a phone number in your profile to enable this option"
+msgstr ""
+
+#: src/pages/account/CashoutListForAccount.tsx:52
+#, c-format
+msgid "Cashout for account %1$s"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:165
+#, c-format
+msgid "it doesn't have the pattern of an IBAN number"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:185
+#, c-format
+msgid "it doesn't have the pattern of an email"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:190
+#, c-format
+msgid "should start with +"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:192
+#, c-format
+msgid "phone number can't have other than numbers"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:329
+#, c-format
+msgid "account identification in the bank"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:365
+#, c-format
+msgid "name of the person owner the account"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:374
+#, c-format
+msgid "Internal IBAN"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:377
+#, c-format
+msgid "if empty a random account number will be assigned"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:378
+#, c-format
+msgid "account identification for bank transfer"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:423
+#, c-format
+msgid "Phone"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:451
+#, c-format
+msgid "Cashout IBAN"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:452
+#, c-format
+msgid "account number where the money is going to be sent when doing cashouts"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:470
+#, c-format
+msgid "Max debt"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:494
+#, c-format
+msgid "how much is user able to transfer after zero balance"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:508
+#, c-format
+msgid "Is this a Taler Exchange?"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:549
+#, c-format
+msgid "This server doesn't support second factor authentication."
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:560
+#, c-format
+msgid "Enable second factor authentication"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:596
+#, c-format
+msgid "Using email"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:654
+#, c-format
+msgid "Using SMS"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:691
+#, c-format
+msgid "Is this account public?"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:719
+#, c-format
+msgid "public accounts have their balance publicly accessible"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:100
+#, c-format
+msgid "Account updated"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:107
+#, c-format
+msgid "The rights to change the account are not sufficient"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:114
+#, c-format
+msgid "The username was not found"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:121
+#, c-format
+msgid "You can't change the legal name, please contact the your account administrator."
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:128
+#, c-format
+msgid "You can't change the debt limit, please contact the your account administrator."
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:135
+#, c-format
+msgid ""
+"You can't change the cashout address, please contact the your account "
+"administrator."
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:177
+#, c-format
+msgid "Account \"%1$s\""
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:190
+#, c-format
+msgid "Change details"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:235
+#, c-format
+msgid "Update"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:78
+#, c-format
+msgid "password doesn't match"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:95
+#, c-format
+msgid "Password changed"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:102
+#, c-format
+msgid "Not authorized to change the password, maybe the session is invalid."
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:112
+#, c-format
+msgid ""
+"You need to provide the old password. If you don't have it contact your account "
+"administrator."
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:117
+#, c-format
+msgid "Your current password doesn't match, can't change to a new password."
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:149
+#, c-format
+msgid "Update password"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:167
+#, c-format
+msgid "New password"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:195
+#, c-format
+msgid "Type it again"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:217
+#, c-format
+msgid "repeat the same password"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:227
+#, c-format
+msgid "Current password"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:248
+#, c-format
+msgid "your current password, for security"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:272
+#, c-format
+msgid "Change"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:74
+#, c-format
+msgid ""
+"Account created with password \"%1$s\". The user must change the password on the "
+"next login."
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:83
+#, c-format
+msgid "Server replied that phone or email is invalid"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:90
+#, c-format
+msgid "The rights to perform the operation are not sufficient"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:97
+#, c-format
+msgid "Account username is already taken"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:104
+#, c-format
+msgid "Account id is already taken"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:111
+#, c-format
+msgid "Bank ran out of bonus credit."
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:118
+#, c-format
+msgid "Account username can't be used because is reserved"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:160
+#, c-format
+msgid "Can't create accounts"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:161
+#, c-format
+msgid "Only system admin can create accounts."
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:183
+#, c-format
+msgid "New business account"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:209
+#, c-format
+msgid "Create"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:94
+#, c-format
+msgid "Can't delete the account"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:95
+#, c-format
+msgid ""
+"The account can't be delete while still holding some balance. First make sure "
+"that the owner make a complete cashout."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:117
+#, c-format
+msgid "Account removed"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:124
+#, c-format
+msgid "No enough permission to delete the account."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:131
+#, c-format
+msgid "The username was not found."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:138
+#, c-format
+msgid "Can't delete a reserved username."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:145
+#, c-format
+msgid "Can't delete an account with balance different than zero."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:170
+#, c-format
+msgid "name doesn't match"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:180
+#, c-format
+msgid "You are going to remove the account"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:182
+#, c-format
+msgid "This step can't be undone."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:188
+#, c-format
+msgid "Deleting account \"%1$s\""
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:206
+#, c-format
+msgid "Verification"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:231
+#, c-format
+msgid "enter the account name that is going to be deleted"
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:49
+#, c-format
+msgid "cashout id should be a number"
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:65
+#, c-format
+msgid "This cashout not found. Maybe already aborted."
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:106
+#, c-format
+msgid "Cashout detail"
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:139
+#, c-format
+msgid "Debited"
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:154
+#, c-format
+msgid "Credited"
+msgstr ""
+
+#: src/Routing.tsx:140
+#, c-format
+msgid "Welcome to %1$s!"
+msgstr ""
+
diff --git a/packages/bank-ui/src/i18n/de.po b/packages/bank-ui/src/i18n/de.po
new file mode 100644
index 000000000..ccbbc8208
--- /dev/null
+++ b/packages/bank-ui/src/i18n/de.po
@@ -0,0 +1,1780 @@
+# 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/>
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Wallet\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"POT-Creation-Date: 2016-11-23 00:00+0100\n"
+"PO-Revision-Date: 2024-03-21 21:39+0000\n"
+"Last-Translator: Stefan Kügel <skuegel@web.de>\n"
+"Language-Team: German <https://weblate.taler.net/projects/gnu-taler/"
+"taler-bank-spa/de/>\n"
+"Language: de\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 5.2.1\n"
+
+#: src/utils.ts:137
+#, c-format
+msgid "Operation failed, please report"
+msgstr "Vorgang abgebrochen, bitte Fehler berichten"
+
+#: src/utils.ts:156
+#, c-format
+msgid "Request timeout"
+msgstr "Zeitüberschreitung der Anforderung"
+
+#: src/utils.ts:165
+#, c-format
+msgid "Request throttled"
+msgstr "Anfrage verzögert sich"
+
+#: src/utils.ts:174
+#, c-format
+msgid "Malformed response"
+msgstr "Unstimmige Antwort"
+
+#: src/utils.ts:183
+#, c-format
+msgid "Network error"
+msgstr "Netzwerkfehler"
+
+#: src/utils.ts:192
+#, c-format
+msgid "Unexpected request error"
+msgstr "Unerwarteter Fehler bei der Anforderung"
+
+#: src/utils.ts:201
+#, c-format
+msgid "Unexpected error"
+msgstr "Unerwarteter Fehler"
+
+#: src/utils.ts:377
+#, c-format
+msgid "IBAN numbers usually have more that 4 digits"
+msgstr "IBAN-Nummern haben normalerweise mehr als 4 Ziffern"
+
+#: src/utils.ts:379
+#, c-format
+msgid "IBAN numbers usually have less that 34 digits"
+msgstr "IBAN-Nummern haben normalerweise weniger als 34 Ziffern"
+
+#: src/utils.ts:387
+#, c-format
+msgid "IBAN country code not found"
+msgstr "IBAN-Ländercode wurde nicht gefunden"
+
+#: src/utils.ts:401
+#, c-format
+msgid "IBAN number is not valid, checksum is wrong"
+msgstr "IBAN-Nummer ist ungültig, die Prüfsumme ist falsch"
+
+#: src/context/config.ts:136
+#, c-format
+msgid ""
+"the bank backend is not supported. supported version \"%1$s\", server "
+"version \"%2$s\""
+msgstr ""
+"Das Bank-Backend wird nicht unterstützt. Unterstützte Version \"%1$s\", "
+"Serverversion \"%2$s\""
+
+#: src/hooks/preferences.ts:55
+#, c-format
+msgid "Max withdrawal amount"
+msgstr "Höchste Abhebesumme"
+
+#: src/hooks/preferences.ts:57
+#, c-format
+msgid "Show withdrawal confirmation"
+msgstr ""
+
+#: src/hooks/preferences.ts:59
+#, c-format
+msgid "Show demo description"
+msgstr ""
+
+#: src/hooks/preferences.ts:61
+#, c-format
+msgid "Show install wallet first"
+msgstr ""
+
+#: src/hooks/preferences.ts:63
+#, c-format
+msgid "Use fast withdrawal form"
+msgstr ""
+
+#: src/hooks/preferences.ts:65
+#, c-format
+msgid "Show debug info"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:90
+#, c-format
+msgid "required"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:92
+#, c-format
+msgid "IBAN should have just uppercased letters and numbers"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:98
+#, c-format
+msgid "not valid"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:100
+#, c-format
+msgid "should be greater than 0"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:102
+#, c-format
+msgid "balance is not enough"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:112
+#, c-format
+msgid "does not follow the pattern"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:114
+#, c-format
+msgid "only \"IBAN\" target are supported"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:116
+#, c-format
+msgid "use the \"amount\" parameter to specify the amount to be transferred"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:118
+#, c-format
+msgid "the amount is not valid"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:120
+#, c-format
+msgid ""
+"use the \"message\" parameter to specify a reference text for the transfer"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:160
+#, c-format
+msgid "The request was invalid or the payto://-URI used unacceptable features."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:167
+#, c-format
+msgid "Not enough permission to complete the operation."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:174
+#, c-format
+msgid "The destination account \"%1$s\" was not found."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:181
+#, c-format
+msgid "The origin and the destination of the transfer can't be the same."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:188
+#, c-format
+msgid "Your balance is not enough."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:195
+#, c-format
+msgid "The origin account \"%1$s\" was not found."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:212
+#, c-format
+msgid "Wire transfer created!"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:270
+#, c-format
+msgid "Using a form"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:310
+#, c-format
+msgid "Import payto:// URI"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:335
+#, c-format
+msgid "Recipient"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:359
+#, c-format
+msgid "IBAN of the recipient's account"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:369
+#, c-format
+msgid "Transfer subject"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:377
+#, fuzzy, c-format
+msgid "subject"
+msgstr "Verwendungszweck"
+
+#: src/pages/PaytoWireTransferForm.tsx:390
+#, c-format
+msgid "some text to identify the transfer"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:400
+#, c-format
+msgid "Amount"
+msgstr "Betrag"
+
+#: src/pages/PaytoWireTransferForm.tsx:415
+#, fuzzy, c-format
+msgid "amount to transfer"
+msgstr "Betrag"
+
+#: src/pages/PaytoWireTransferForm.tsx:425
+#, c-format
+msgid "payto URI:"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:436
+#, c-format
+msgid "uniform resource identifier of the target account"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:437
+#, c-format
+msgid "payto://iban/[receiver-iban]?message=[subject]&amount=[%1$s:X.Y]"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:457
+#, c-format
+msgid "Cancel"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:471
+#, c-format
+msgid "Send"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:71
+#, c-format
+msgid "Missing username"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:75
+#, c-format
+msgid "Missing password"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:104
+#, c-format
+msgid "Wrong credentials for \"%1$s\""
+msgstr ""
+
+#: src/pages/LoginForm.tsx:111
+#, c-format
+msgid "Account not found"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:142
+#, c-format
+msgid "Username"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:156
+#, c-format
+msgid "username of the account"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:175
+#, c-format
+msgid "Password"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:188
+#, fuzzy, c-format
+msgid "password of the account"
+msgstr "Buchungen auf öffentlich sichtbaren Konten"
+
+#: src/pages/LoginForm.tsx:223
+#, c-format
+msgid "Check"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:237
+#, c-format
+msgid "Log in"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:249
+#, c-format
+msgid "Register"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:52
+#, c-format
+msgid "Latest transactions"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:63
+#, c-format
+msgid "Date"
+msgstr "Datum"
+
+#: src/components/Transactions/views.tsx:71
+#, c-format
+msgid "Counterpart"
+msgstr "Empfänger"
+
+#: src/components/Transactions/views.tsx:75
+#, c-format
+msgid "Subject"
+msgstr "Verwendungszweck"
+
+#: src/components/Transactions/views.tsx:111
+#, c-format
+msgid "sent"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:112
+#, c-format
+msgid "received"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:127
+#, c-format
+msgid "invalid value"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:136
+#, c-format
+msgid "to"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:136
+#, c-format
+msgid "from"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:202
+#, c-format
+msgid "First page"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:209
+#, c-format
+msgid "Next"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:86
+#, c-format
+msgid "Wire transfer completed!"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:93
+#, c-format
+msgid "The withdrawal has been aborted previously and can't be confirmed"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:100
+#, c-format
+msgid ""
+"The withdrawal operation can't be confirmed before a wallet accepted the "
+"transaction."
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:107
+#, c-format
+msgid "The operation id is invalid."
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:114
+#, c-format
+msgid "The operation was not found."
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:121
+#, c-format
+msgid "Your balance is not enough for the operation."
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:155
+#, c-format
+msgid ""
+"The reserve operation has been confirmed previously and can't be aborted"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:186
+#, fuzzy, c-format
+msgid "Confirm the withdrawal operation"
+msgstr "Abhebung bestätigen"
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:203
+#, c-format
+msgid "Wire transfer details"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:217
+#, c-format
+msgid "Taler Exchange operator's account"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:228
+#, c-format
+msgid "Taler Exchange operator's name"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:317
+#, c-format
+msgid "Transfer"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:342
+#, c-format
+msgid "Authentication required"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:352
+#, c-format
+msgid "This operation was created with other username"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:209
+#, c-format
+msgid ""
+"Unauthorized to make the operation, maybe the session has expired or the "
+"password changed."
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:218
+#, c-format
+msgid "The operation was rejected due to insufficient funds."
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:268
+#, c-format
+msgid "Withdrawal confirmed"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:272
+#, c-format
+msgid ""
+"The wire transfer to the Taler operator has been initiated. You will soon "
+"receive the requested amount in your Taler wallet."
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:287
+#, c-format
+msgid "Do not show this again"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:319
+#, c-format
+msgid "Close"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:399
+#, c-format
+msgid "On this device"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:404
+#, c-format
+msgid ""
+"If you are using a web browser on desktop you should access your wallet with "
+"the GNU Taler WebExtension now or click the link if your WebExtension have "
+"the \"Inject Taler support\" option enabled."
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:417
+#, c-format
+msgid "Start"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:426
+#, c-format
+msgid "On a mobile phone"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:431
+#, c-format
+msgid "Scan the QR code with your mobile device."
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:73
+#, c-format
+msgid "There is an operation already"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:75
+#, fuzzy, c-format
+msgid "Complete or cancel the operation in"
+msgstr "Abhebung bestätigen"
+
+#: src/pages/WalletWithdrawForm.tsx:84
+#, c-format
+msgid "this page"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:101
+#, c-format
+msgid "invalid"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:116
+#, c-format
+msgid "Server responded with an invalid withdraw URI"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:117
+#, c-format
+msgid "Withdraw URI: %1$s"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:132
+#, c-format
+msgid "The operation was rejected due to insufficient funds"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:253
+#, c-format
+msgid "Continue"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:282
+#, c-format
+msgid "Prepare your wallet"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:285
+#, c-format
+msgid ""
+"After using your wallet you will need to confirm or cancel the operation on "
+"this site."
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:295
+#, c-format
+msgid "You need a GNU Taler Wallet"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:300
+#, c-format
+msgid "If you don't have one yet you can follow the instruction in"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:55
+#, c-format
+msgid "Send money"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:73
+#, c-format
+msgid "to a %1$s wallet"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:95
+#, c-format
+msgid "Withdraw digital money into your mobile wallet or browser extension"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:109
+#, c-format
+msgid "operation ready"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:129
+#, c-format
+msgid "to another bank account"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:149
+#, c-format
+msgid "Make a wire transfer to an account with known bank account number."
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:171
+#, c-format
+msgid "Transfer details"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:41
+#, c-format
+msgid "This is a demo bank"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:46
+#, c-format
+msgid ""
+"This part of the demo shows how a bank that supports Taler directly would "
+"work. In addition to using your own bank account, you can also see the "
+"transaction history of some %1$s."
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:53
+#, c-format
+msgid ""
+"This part of the demo shows how a bank that supports Taler directly would "
+"work."
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:70
+#, c-format
+msgid "Pending account delete operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:72
+#, c-format
+msgid "Pending account update operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:74
+#, c-format
+msgid "Pending password update operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:76
+#, c-format
+msgid "Pending transaction operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:78
+#, c-format
+msgid "Pending withdrawal operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:80
+#, c-format
+msgid "Pending cashout operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:91
+#, c-format
+msgid "You can complete or cancel the operation in"
+msgstr ""
+
+#: src/pages/BankFrame.tsx:64
+#, c-format
+msgid "Internal error, please report."
+msgstr ""
+
+#: src/pages/BankFrame.tsx:100
+#, c-format
+msgid "Preferences"
+msgstr ""
+
+#: src/pages/BankFrame.tsx:184
+#, c-format
+msgid "Welcome, %1$s"
+msgstr ""
+
+#: src/pages/WireTransfer.tsx:79
+#, c-format
+msgid "Make a wire transfer"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:72
+#, fuzzy, c-format
+msgid "Accounts"
+msgstr "Betrag"
+
+#: src/pages/admin/AccountList.tsx:75
+#, c-format
+msgid "A list of all business account in the bank."
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:86
+#, c-format
+msgid "Create account"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:106
+#, c-format
+msgid "Name"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:110
+#, c-format
+msgid "Balance"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:112
+#, c-format
+msgid "Actions"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:151
+#, c-format
+msgid "unknown"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:170
+#, c-format
+msgid "change password"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:179
+#, c-format
+msgid "cashouts"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:189
+#, c-format
+msgid "remove"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:168
+#, c-format
+msgid "Cashout not implemented"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:184
+#, c-format
+msgid "Select a section"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:202
+#, c-format
+msgid "Last hour"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:208
+#, c-format
+msgid "Last day"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:216
+#, c-format
+msgid "Last month"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:222
+#, c-format
+msgid "Last year"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:310
+#, c-format
+msgid "Last Year"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:325
+#, c-format
+msgid "Trading volume on %1$s compared to %2$s"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:342
+#, c-format
+msgid "Cashin"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:352
+#, c-format
+msgid "Cashout"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:364
+#, c-format
+msgid "Payin"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:374
+#, c-format
+msgid "Payout"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:388
+#, c-format
+msgid "download stats as CSV"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:494
+#, c-format
+msgid "Decreased by"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:498
+#, c-format
+msgid "Increased by"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:89
+#, c-format
+msgid "Download bank stats"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:110
+#, c-format
+msgid "Include hour metric"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:143
+#, c-format
+msgid "Include day metric"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:173
+#, c-format
+msgid "Include month metric"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:206
+#, c-format
+msgid "Include year metric"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:239
+#, c-format
+msgid "Include table header"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:272
+#, c-format
+msgid "Add previous metric for compare"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:307
+#, c-format
+msgid "Fail on first error"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:364
+#, c-format
+msgid "Download"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:381
+#, c-format
+msgid "downloading... %1$s"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:399
+#, c-format
+msgid "Download completed"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:400
+#, c-format
+msgid "click here to save the file in your computer"
+msgstr ""
+
+#: src/pages/PublicHistoriesPage.tsx:78
+#, c-format
+msgid "History of public accounts"
+msgstr "Buchungen auf öffentlich sichtbaren Konten"
+
+#: src/pages/RegistrationPage.tsx:48
+#, c-format
+msgid "Currently, the bank is not accepting new registrations!"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:87
+#, c-format
+msgid "Missing name"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:91
+#, c-format
+msgid "Use letters and numbers only, and start with a lowercase letter"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:107
+#, c-format
+msgid "Passwords don't match"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:130
+#, c-format
+msgid "Server replied with invalid phone or email."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:137
+#, c-format
+msgid "Registration is disabled because the bank ran out of bonus credit."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:144
+#, c-format
+msgid "No enough permission to create that account."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:151
+#, c-format
+msgid "That account id is already taken."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:158
+#, c-format
+msgid "That username is already taken."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:165
+#, c-format
+msgid "That username can't be used because is reserved."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:172
+#, c-format
+msgid "Only admin is allow to set debt limit."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:179
+#, c-format
+msgid "No information for the selected authentication channel."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:186
+#, c-format
+msgid "Authentication channel is not supported."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:193
+#, c-format
+msgid "Only admin can create accounts with second factor authentication."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:233
+#, c-format
+msgid "Account registration"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:315
+#, c-format
+msgid "Repeat password"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:457
+#, c-format
+msgid "Create a random temporary user"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:110
+#, c-format
+msgid "If you have a Taler wallet installed in this device"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:116
+#, c-format
+msgid ""
+"You will see the details of the operation in your wallet including the fees "
+"(if applies). If you still don't have one you can install it following "
+"instructions in"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:143
+#, c-format
+msgid "Withdraw"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:152
+#, c-format
+msgid "Or if you have the wallet in another device"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:157
+#, c-format
+msgid "Scan the QR below to start the withdrawal."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:79
+#, c-format
+msgid "Operation aborted"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:82
+#, c-format
+msgid ""
+"The wire transfer to the Taler Exchange operator's account was aborted, your "
+"balance was not affected."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:88
+#, c-format
+msgid "You can close this page now or continue to the account page."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:147
+#, c-format
+msgid "Done"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:158
+#, c-format
+msgid "Operation canceled"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:173
+#, c-format
+msgid ""
+"The operation is marked as 'selected' but some step in the withdrawal failed"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:175
+#, c-format
+msgid "The account is selected but no withdrawal identification found."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:188
+#, c-format
+msgid ""
+"There is a withdrawal identification but no account has been selected or the "
+"selected account is invalid."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:202
+#, c-format
+msgid ""
+"No withdrawal ID found and no account has been selected or the selected "
+"account is invalid."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:259
+#, c-format
+msgid "Operation not found"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:263
+#, c-format
+msgid ""
+"This operation is not known by the server. The operation id is wrong or the "
+"server deleted the operation information before reaching here."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:278
+#, c-format
+msgid "Cotinue to dashboard"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:98
+#, c-format
+msgid "Cashout not found. It may be also mean that it was already aborted."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:136
+#, c-format
+msgid "Challenge not found."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:143
+#, c-format
+msgid "This user is not authorized to complete this challenge."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:150
+#, c-format
+msgid "Too many attempts, try another code."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:157
+#, c-format
+msgid "The confirmation code is wrong, try again."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:164
+#, c-format
+msgid "The operation expired."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:197
+#, c-format
+msgid "The operation failed."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:212
+#, c-format
+msgid "The operation needs another confirmation to complete."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:224
+#, c-format
+msgid "Account delete"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:226
+#, c-format
+msgid "Account update"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:228
+#, c-format
+msgid "Password update"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:230
+#, c-format
+msgid "Wire transfer"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:232
+#, fuzzy, c-format
+msgid "Withdrawal"
+msgstr "Abhebung bestätigen"
+
+#: src/pages/SolveChallengePage.tsx:248
+#, fuzzy, c-format
+msgid "Confirm the operation"
+msgstr "Abhebung bestätigen"
+
+#: src/pages/SolveChallengePage.tsx:271
+#, c-format
+msgid "Enter the confirmation code"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:313
+#, c-format
+msgid "Confirm"
+msgstr "Bestätigen"
+
+#: src/pages/SolveChallengePage.tsx:348
+#, c-format
+msgid "Send again"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:359
+#, c-format
+msgid "Send code"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:369
+#, c-format
+msgid "Operation details"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:529
+#, c-format
+msgid "Challenge details"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:536
+#, c-format
+msgid "Sent at"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:551
+#, c-format
+msgid "To phone"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:553
+#, c-format
+msgid "To email"
+msgstr ""
+
+#: src/pages/WithdrawalOperationPage.tsx:49
+#, c-format
+msgid "The Withdrawal URI is not valid"
+msgstr ""
+
+#: src/components/Cashouts/views.tsx:100
+#, c-format
+msgid "Latest cashouts"
+msgstr ""
+
+#: src/components/Cashouts/views.tsx:111
+#, c-format
+msgid "Created"
+msgstr ""
+
+#: src/components/Cashouts/views.tsx:115
+#, c-format
+msgid "Total debit"
+msgstr ""
+
+#: src/components/Cashouts/views.tsx:119
+#, c-format
+msgid "Total credit"
+msgstr ""
+
+#: src/pages/ProfileNavigation.tsx:70
+#, c-format
+msgid "Details"
+msgstr ""
+
+#: src/pages/ProfileNavigation.tsx:74
+#, c-format
+msgid "Delete"
+msgstr ""
+
+#: src/pages/ProfileNavigation.tsx:78
+#, c-format
+msgid "Credentials"
+msgstr ""
+
+#: src/pages/ProfileNavigation.tsx:82
+#, c-format
+msgid "Cashouts"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:95
+#, c-format
+msgid "Unable to create a cashout"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:96
+#, c-format
+msgid "The bank configuration does not support cashout operations."
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:223
+#, c-format
+msgid "need to be higher due to fees"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:225
+#, c-format
+msgid "the total transfer at destination will be zero"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:250
+#, c-format
+msgid "Cashout created"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:272
+#, c-format
+msgid ""
+"Duplicated request detected, check if the operation succeeded or try again."
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:279
+#, c-format
+msgid "The conversion rate was incorrectly applied"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:286
+#, c-format
+msgid "The account does not have sufficient funds"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:293
+#, c-format
+msgid "Cashouts are not supported"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:300
+#, c-format
+msgid "Missing cashout URI in the profile"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:307
+#, c-format
+msgid ""
+"Sending the confirmation message failed, retry later or contact the "
+"administrator."
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:339
+#, c-format
+msgid "Conversion rate"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:360
+#, c-format
+msgid "Fee"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:374
+#, c-format
+msgid "To account"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:381
+#, c-format
+msgid "No cashout account"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:382
+#, c-format
+msgid "Before doing a cashout you need to complete your profile"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:440
+#, fuzzy, c-format
+msgid "Amount to send"
+msgstr "Betrag"
+
+#: src/pages/business/CreateCashout.tsx:441
+#, c-format
+msgid "Amount to receive"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:490
+#, c-format
+msgid "Total cost"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:505
+#, c-format
+msgid "Balance left"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:520
+#, c-format
+msgid "Before fee"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:533
+#, c-format
+msgid "Total cashout transfer"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:553
+#, c-format
+msgid "No cashout channel available"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:555
+#, c-format
+msgid ""
+"Before doing a cashout the server need to provide an second channel to "
+"confirm the operation"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:567
+#, c-format
+msgid "Second factor authentication"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:598
+#, c-format
+msgid "Email"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:600
+#, c-format
+msgid "add a email in your profile to enable this option"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:646
+#, c-format
+msgid "SMS"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:648
+#, c-format
+msgid "add a phone number in your profile to enable this option"
+msgstr ""
+
+#: src/pages/account/CashoutListForAccount.tsx:52
+#, c-format
+msgid "Cashout for account %1$s"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:165
+#, c-format
+msgid "it doesn't have the pattern of an IBAN number"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:185
+#, c-format
+msgid "it doesn't have the pattern of an email"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:190
+#, c-format
+msgid "should start with +"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:192
+#, c-format
+msgid "phone number can't have other than numbers"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:329
+#, c-format
+msgid "account identification in the bank"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:365
+#, c-format
+msgid "name of the person owner the account"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:374
+#, c-format
+msgid "Internal IBAN"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:377
+#, c-format
+msgid "if empty a random account number will be assigned"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:378
+#, c-format
+msgid "account identification for bank transfer"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:423
+#, c-format
+msgid "Phone"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:451
+#, c-format
+msgid "Cashout IBAN"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:452
+#, c-format
+msgid "account number where the money is going to be sent when doing cashouts"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:470
+#, c-format
+msgid "Max debt"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:494
+#, c-format
+msgid "how much is user able to transfer after zero balance"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:508
+#, c-format
+msgid "Is this a Taler Exchange?"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:549
+#, c-format
+msgid "This server doesn't support second factor authentication."
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:560
+#, c-format
+msgid "Enable second factor authentication"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:596
+#, c-format
+msgid "Using email"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:654
+#, c-format
+msgid "Using SMS"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:691
+#, c-format
+msgid "Is this account public?"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:719
+#, c-format
+msgid "public accounts have their balance publicly accessible"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:100
+#, c-format
+msgid "Account updated"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:107
+#, c-format
+msgid "The rights to change the account are not sufficient"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:114
+#, c-format
+msgid "The username was not found"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:121
+#, c-format
+msgid ""
+"You can't change the legal name, please contact the your account "
+"administrator."
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:128
+#, c-format
+msgid ""
+"You can't change the debt limit, please contact the your account "
+"administrator."
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:135
+#, c-format
+msgid ""
+"You can't change the cashout address, please contact the your account "
+"administrator."
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:177
+#, c-format
+msgid "Account \"%1$s\""
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:190
+#, c-format
+msgid "Change details"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:235
+#, c-format
+msgid "Update"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:78
+#, c-format
+msgid "password doesn't match"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:95
+#, c-format
+msgid "Password changed"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:102
+#, c-format
+msgid "Not authorized to change the password, maybe the session is invalid."
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:112
+#, c-format
+msgid ""
+"You need to provide the old password. If you don't have it contact your "
+"account administrator."
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:117
+#, c-format
+msgid "Your current password doesn't match, can't change to a new password."
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:149
+#, c-format
+msgid "Update password"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:167
+#, c-format
+msgid "New password"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:195
+#, c-format
+msgid "Type it again"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:217
+#, c-format
+msgid "repeat the same password"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:227
+#, c-format
+msgid "Current password"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:248
+#, c-format
+msgid "your current password, for security"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:272
+#, c-format
+msgid "Change"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:74
+#, c-format
+msgid ""
+"Account created with password \"%1$s\". The user must change the password on "
+"the next login."
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:83
+#, c-format
+msgid "Server replied that phone or email is invalid"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:90
+#, c-format
+msgid "The rights to perform the operation are not sufficient"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:97
+#, c-format
+msgid "Account username is already taken"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:104
+#, c-format
+msgid "Account id is already taken"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:111
+#, c-format
+msgid "Bank ran out of bonus credit."
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:118
+#, c-format
+msgid "Account username can't be used because is reserved"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:160
+#, c-format
+msgid "Can't create accounts"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:161
+#, c-format
+msgid "Only system admin can create accounts."
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:183
+#, c-format
+msgid "New business account"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:209
+#, c-format
+msgid "Create"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:94
+#, c-format
+msgid "Can't delete the account"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:95
+#, c-format
+msgid ""
+"The account can't be delete while still holding some balance. First make "
+"sure that the owner make a complete cashout."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:117
+#, c-format
+msgid "Account removed"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:124
+#, c-format
+msgid "No enough permission to delete the account."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:131
+#, c-format
+msgid "The username was not found."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:138
+#, c-format
+msgid "Can't delete a reserved username."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:145
+#, c-format
+msgid "Can't delete an account with balance different than zero."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:170
+#, c-format
+msgid "name doesn't match"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:180
+#, c-format
+msgid "You are going to remove the account"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:182
+#, c-format
+msgid "This step can't be undone."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:188
+#, c-format
+msgid "Deleting account \"%1$s\""
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:206
+#, c-format
+msgid "Verification"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:231
+#, c-format
+msgid "enter the account name that is going to be deleted"
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:49
+#, c-format
+msgid "cashout id should be a number"
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:65
+#, c-format
+msgid "This cashout not found. Maybe already aborted."
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:106
+#, c-format
+msgid "Cashout detail"
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:139
+#, c-format
+msgid "Debited"
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:154
+#, c-format
+msgid "Credited"
+msgstr ""
+
+#: src/Routing.tsx:140
+#, c-format
+msgid "Welcome to %1$s!"
+msgstr ""
+
+#, fuzzy, c-format
+#~ msgid "Confirmed"
+#~ msgstr "Bestätigen"
+
+#, c-format
+#~ msgid "Logout"
+#~ msgstr "Abmelden"
+
+#, c-format
+#~ msgid "Skip to main content"
+#~ msgstr "Navigationsmenü überspringen"
+
+#, c-format
+#~ msgid "Taler logo"
+#~ msgstr "Taler-Logo"
+
+#, c-format
+#~ msgid "Please login!"
+#~ msgstr "Bitte melden Sie sich an!"
+
+#, c-format
+#~ msgid "payto address"
+#~ msgstr "payto-Adresse"
+
+#, c-format
+#~ msgid "Bank account balance"
+#~ msgstr "Kontostand"
diff --git a/packages/bank-ui/src/i18n/en.po b/packages/bank-ui/src/i18n/en.po
new file mode 100644
index 000000000..a9657bd32
--- /dev/null
+++ b/packages/bank-ui/src/i18n/en.po
@@ -0,0 +1,1784 @@
+# 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/>
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Wallet\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"POT-Creation-Date: 2016-11-23 00:00+0100\n"
+"PO-Revision-Date: 2022-01-08 09:57+0100\n"
+"Last-Translator: <translate@taler.net>\n"
+"Language-Team: English\n"
+"Language: en\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: src/utils.ts:137
+#, c-format
+msgid "Operation failed, please report"
+msgstr ""
+
+#: src/utils.ts:156
+#, c-format
+msgid "Request timeout"
+msgstr ""
+
+#: src/utils.ts:165
+#, c-format
+msgid "Request throttled"
+msgstr ""
+
+#: src/utils.ts:174
+#, c-format
+msgid "Malformed response"
+msgstr ""
+
+#: src/utils.ts:183
+#, c-format
+msgid "Network error"
+msgstr ""
+
+#: src/utils.ts:192
+#, c-format
+msgid "Unexpected request error"
+msgstr ""
+
+#: src/utils.ts:201
+#, c-format
+msgid "Unexpected error"
+msgstr ""
+
+#: src/utils.ts:377
+#, c-format
+msgid "IBAN numbers usually have more that 4 digits"
+msgstr ""
+
+#: src/utils.ts:379
+#, c-format
+msgid "IBAN numbers usually have less that 34 digits"
+msgstr ""
+
+#: src/utils.ts:387
+#, c-format
+msgid "IBAN country code not found"
+msgstr ""
+
+#: src/utils.ts:401
+#, c-format
+msgid "IBAN number is not valid, checksum is wrong"
+msgstr ""
+
+#: src/context/config.ts:136
+#, c-format
+msgid ""
+"the bank backend is not supported. supported version \"%1$s\", server "
+"version \"%2$s\""
+msgstr ""
+
+#: src/hooks/preferences.ts:55
+#, fuzzy, c-format
+msgid "Max withdrawal amount"
+msgstr ""
+
+#: src/hooks/preferences.ts:57
+#, c-format
+msgid "Show withdrawal confirmation"
+msgstr ""
+
+#: src/hooks/preferences.ts:59
+#, c-format
+msgid "Show demo description"
+msgstr ""
+
+#: src/hooks/preferences.ts:61
+#, c-format
+msgid "Show install wallet first"
+msgstr ""
+
+#: src/hooks/preferences.ts:63
+#, fuzzy, c-format
+msgid "Use fast withdrawal form"
+msgstr ""
+
+#: src/hooks/preferences.ts:65
+#, c-format
+msgid "Show debug info"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:90
+#, c-format
+msgid "required"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:92
+#, c-format
+msgid "IBAN should have just uppercased letters and numbers"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:98
+#, c-format
+msgid "not valid"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:100
+#, c-format
+msgid "should be greater than 0"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:102
+#, c-format
+msgid "balance is not enough"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:112
+#, c-format
+msgid "does not follow the pattern"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:114
+#, c-format
+msgid "only \"IBAN\" target are supported"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:116
+#, c-format
+msgid "use the \"amount\" parameter to specify the amount to be transferred"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:118
+#, c-format
+msgid "the amount is not valid"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:120
+#, c-format
+msgid ""
+"use the \"message\" parameter to specify a reference text for the transfer"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:160
+#, c-format
+msgid "The request was invalid or the payto://-URI used unacceptable features."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:167
+#, c-format
+msgid "Not enough permission to complete the operation."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:174
+#, c-format
+msgid "The destination account \"%1$s\" was not found."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:181
+#, c-format
+msgid "The origin and the destination of the transfer can't be the same."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:188
+#, c-format
+msgid "Your balance is not enough."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:195
+#, c-format
+msgid "The origin account \"%1$s\" was not found."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:212
+#, c-format
+msgid "Wire transfer created!"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:270
+#, c-format
+msgid "Using a form"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:310
+#, c-format
+msgid "Import payto:// URI"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:335
+#, c-format
+msgid "Recipient"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:359
+#, c-format
+msgid "IBAN of the recipient's account"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:369
+#, c-format
+msgid "Transfer subject"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:377
+#, c-format
+msgid "subject"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:390
+#, c-format
+msgid "some text to identify the transfer"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:400
+#, c-format
+msgid "Amount"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:415
+#, fuzzy, c-format
+msgid "amount to transfer"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:425
+#, c-format
+msgid "payto URI:"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:436
+#, c-format
+msgid "uniform resource identifier of the target account"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:437
+#, c-format
+msgid "payto://iban/[receiver-iban]?message=[subject]&amount=[%1$s:X.Y]"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:457
+#, c-format
+msgid "Cancel"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:471
+#, c-format
+msgid "Send"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:71
+#, c-format
+msgid "Missing username"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:75
+#, c-format
+msgid "Missing password"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:104
+#, c-format
+msgid "Wrong credentials for \"%1$s\""
+msgstr ""
+
+#: src/pages/LoginForm.tsx:111
+#, c-format
+msgid "Account not found"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:142
+#, c-format
+msgid "Username"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:156
+#, c-format
+msgid "username of the account"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:175
+#, c-format
+msgid "Password"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:188
+#, c-format
+msgid "password of the account"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:223
+#, c-format
+msgid "Check"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:237
+#, c-format
+msgid "Log in"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:249
+#, c-format
+msgid "Register"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:52
+#, c-format
+msgid "Latest transactions"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:63
+#, c-format
+msgid "Date"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:71
+#, c-format
+msgid "Counterpart"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:75
+#, c-format
+msgid "Subject"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:111
+#, c-format
+msgid "sent"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:112
+#, c-format
+msgid "received"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:127
+#, c-format
+msgid "invalid value"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:136
+#, c-format
+msgid "to"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:136
+#, c-format
+msgid "from"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:202
+#, c-format
+msgid "First page"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:209
+#, c-format
+msgid "Next"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:86
+#, c-format
+msgid "Wire transfer completed!"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:93
+#, c-format
+msgid "The withdrawal has been aborted previously and can't be confirmed"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:100
+#, c-format
+msgid ""
+"The withdrawal operation can't be confirmed before a wallet accepted the "
+"transaction."
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:107
+#, c-format
+msgid "The operation id is invalid."
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:114
+#, c-format
+msgid "The operation was not found."
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:121
+#, c-format
+msgid "Your balance is not enough for the operation."
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:155
+#, c-format
+msgid ""
+"The reserve operation has been confirmed previously and can't be aborted"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:186
+#, fuzzy, c-format
+msgid "Confirm the withdrawal operation"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:203
+#, c-format
+msgid "Wire transfer details"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:217
+#, c-format
+msgid "Taler Exchange operator's account"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:228
+#, c-format
+msgid "Taler Exchange operator's name"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:317
+#, c-format
+msgid "Transfer"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:342
+#, c-format
+msgid "Authentication required"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:352
+#, c-format
+msgid "This operation was created with other username"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:209
+#, c-format
+msgid ""
+"Unauthorized to make the operation, maybe the session has expired or the "
+"password changed."
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:218
+#, c-format
+msgid "The operation was rejected due to insufficient funds."
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:268
+#, c-format
+msgid "Withdrawal confirmed"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:272
+#, c-format
+msgid ""
+"The wire transfer to the Taler operator has been initiated. You will soon "
+"receive the requested amount in your Taler wallet."
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:287
+#, c-format
+msgid "Do not show this again"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:319
+#, c-format
+msgid "Close"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:399
+#, c-format
+msgid "On this device"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:404
+#, c-format
+msgid ""
+"If you are using a web browser on desktop you should access your wallet with "
+"the GNU Taler WebExtension now or click the link if your WebExtension have "
+"the \"Inject Taler support\" option enabled."
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:417
+#, c-format
+msgid "Start"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:426
+#, c-format
+msgid "On a mobile phone"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:431
+#, c-format
+msgid "Scan the QR code with your mobile device."
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:73
+#, c-format
+msgid "There is an operation already"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:75
+#, fuzzy, c-format
+msgid "Complete or cancel the operation in"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:84
+#, c-format
+msgid "this page"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:101
+#, c-format
+msgid "invalid"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:116
+#, c-format
+msgid "Server responded with an invalid withdraw URI"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:117
+#, fuzzy, c-format
+msgid "Withdraw URI: %1$s"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:132
+#, c-format
+msgid "The operation was rejected due to insufficient funds"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:253
+#, c-format
+msgid "Continue"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:282
+#, c-format
+msgid "Prepare your wallet"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:285
+#, c-format
+msgid ""
+"After using your wallet you will need to confirm or cancel the operation on "
+"this site."
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:295
+#, fuzzy, c-format
+msgid "You need a GNU Taler Wallet"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:300
+#, c-format
+msgid "If you don't have one yet you can follow the instruction in"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:55
+#, c-format
+msgid "Send money"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:73
+#, c-format
+msgid "to a %1$s wallet"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:95
+#, c-format
+msgid "Withdraw digital money into your mobile wallet or browser extension"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:109
+#, c-format
+msgid "operation ready"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:129
+#, c-format
+msgid "to another bank account"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:149
+#, c-format
+msgid "Make a wire transfer to an account with known bank account number."
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:171
+#, fuzzy, c-format
+msgid "Transfer details"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:41
+#, c-format
+msgid "This is a demo bank"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:46
+#, c-format
+msgid ""
+"This part of the demo shows how a bank that supports Taler directly would "
+"work. In addition to using your own bank account, you can also see the "
+"transaction history of some %1$s."
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:53
+#, c-format
+msgid ""
+"This part of the demo shows how a bank that supports Taler directly would "
+"work."
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:70
+#, c-format
+msgid "Pending account delete operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:72
+#, c-format
+msgid "Pending account update operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:74
+#, c-format
+msgid "Pending password update operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:76
+#, c-format
+msgid "Pending transaction operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:78
+#, c-format
+msgid "Pending withdrawal operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:80
+#, c-format
+msgid "Pending cashout operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:91
+#, c-format
+msgid "You can complete or cancel the operation in"
+msgstr ""
+
+#: src/pages/BankFrame.tsx:64
+#, c-format
+msgid "Internal error, please report."
+msgstr ""
+
+#: src/pages/BankFrame.tsx:100
+#, c-format
+msgid "Preferences"
+msgstr ""
+
+#: src/pages/BankFrame.tsx:184
+#, c-format
+msgid "Welcome, %1$s"
+msgstr ""
+
+#: src/pages/WireTransfer.tsx:79
+#, c-format
+msgid "Make a wire transfer"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:72
+#, c-format
+msgid "Accounts"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:75
+#, c-format
+msgid "A list of all business account in the bank."
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:86
+#, c-format
+msgid "Create account"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:106
+#, c-format
+msgid "Name"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:110
+#, c-format
+msgid "Balance"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:112
+#, c-format
+msgid "Actions"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:151
+#, c-format
+msgid "unknown"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:170
+#, c-format
+msgid "change password"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:179
+#, c-format
+msgid "cashouts"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:189
+#, c-format
+msgid "remove"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:168
+#, c-format
+msgid "Cashout not implemented"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:184
+#, c-format
+msgid "Select a section"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:202
+#, c-format
+msgid "Last hour"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:208
+#, c-format
+msgid "Last day"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:216
+#, c-format
+msgid "Last month"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:222
+#, c-format
+msgid "Last year"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:310
+#, c-format
+msgid "Last Year"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:325
+#, c-format
+msgid "Trading volume on %1$s compared to %2$s"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:342
+#, c-format
+msgid "Cashin"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:352
+#, c-format
+msgid "Cashout"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:364
+#, c-format
+msgid "Payin"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:374
+#, c-format
+msgid "Payout"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:388
+#, c-format
+msgid "download stats as CSV"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:494
+#, c-format
+msgid "Decreased by"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:498
+#, c-format
+msgid "Increased by"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:89
+#, c-format
+msgid "Download bank stats"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:110
+#, c-format
+msgid "Include hour metric"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:143
+#, c-format
+msgid "Include day metric"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:173
+#, c-format
+msgid "Include month metric"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:206
+#, c-format
+msgid "Include year metric"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:239
+#, c-format
+msgid "Include table header"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:272
+#, c-format
+msgid "Add previous metric for compare"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:307
+#, c-format
+msgid "Fail on first error"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:364
+#, c-format
+msgid "Download"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:381
+#, c-format
+msgid "downloading... %1$s"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:399
+#, c-format
+msgid "Download completed"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:400
+#, c-format
+msgid "click here to save the file in your computer"
+msgstr ""
+
+#: src/pages/PublicHistoriesPage.tsx:78
+#, c-format
+msgid "History of public accounts"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:48
+#, c-format
+msgid "Currently, the bank is not accepting new registrations!"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:87
+#, c-format
+msgid "Missing name"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:91
+#, c-format
+msgid "Use letters and numbers only, and start with a lowercase letter"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:107
+#, c-format
+msgid "Passwords don't match"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:130
+#, c-format
+msgid "Server replied with invalid phone or email."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:137
+#, c-format
+msgid "Registration is disabled because the bank ran out of bonus credit."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:144
+#, c-format
+msgid "No enough permission to create that account."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:151
+#, c-format
+msgid "That account id is already taken."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:158
+#, c-format
+msgid "That username is already taken."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:165
+#, c-format
+msgid "That username can't be used because is reserved."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:172
+#, c-format
+msgid "Only admin is allow to set debt limit."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:179
+#, c-format
+msgid "No information for the selected authentication channel."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:186
+#, c-format
+msgid "Authentication channel is not supported."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:193
+#, c-format
+msgid "Only admin can create accounts with second factor authentication."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:233
+#, c-format
+msgid "Account registration"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:315
+#, c-format
+msgid "Repeat password"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:457
+#, c-format
+msgid "Create a random temporary user"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:110
+#, c-format
+msgid "If you have a Taler wallet installed in this device"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:116
+#, c-format
+msgid ""
+"You will see the details of the operation in your wallet including the fees "
+"(if applies). If you still don't have one you can install it following "
+"instructions in"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:143
+#, fuzzy, c-format
+msgid "Withdraw"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:152
+#, c-format
+msgid "Or if you have the wallet in another device"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:157
+#, fuzzy, c-format
+msgid "Scan the QR below to start the withdrawal."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:79
+#, c-format
+msgid "Operation aborted"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:82
+#, c-format
+msgid ""
+"The wire transfer to the Taler Exchange operator's account was aborted, your "
+"balance was not affected."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:88
+#, c-format
+msgid "You can close this page now or continue to the account page."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:147
+#, c-format
+msgid "Done"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:158
+#, c-format
+msgid "Operation canceled"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:173
+#, c-format
+msgid ""
+"The operation is marked as 'selected' but some step in the withdrawal failed"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:175
+#, c-format
+msgid "The account is selected but no withdrawal identification found."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:188
+#, c-format
+msgid ""
+"There is a withdrawal identification but no account has been selected or the "
+"selected account is invalid."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:202
+#, c-format
+msgid ""
+"No withdrawal ID found and no account has been selected or the selected "
+"account is invalid."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:259
+#, c-format
+msgid "Operation not found"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:263
+#, c-format
+msgid ""
+"This operation is not known by the server. The operation id is wrong or the "
+"server deleted the operation information before reaching here."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:278
+#, c-format
+msgid "Cotinue to dashboard"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:98
+#, c-format
+msgid "Cashout not found. It may be also mean that it was already aborted."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:136
+#, c-format
+msgid "Challenge not found."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:143
+#, c-format
+msgid "This user is not authorized to complete this challenge."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:150
+#, c-format
+msgid "Too many attempts, try another code."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:157
+#, c-format
+msgid "The confirmation code is wrong, try again."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:164
+#, c-format
+msgid "The operation expired."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:197
+#, c-format
+msgid "The operation failed."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:212
+#, c-format
+msgid "The operation needs another confirmation to complete."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:224
+#, c-format
+msgid "Account delete"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:226
+#, c-format
+msgid "Account update"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:228
+#, c-format
+msgid "Password update"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:230
+#, c-format
+msgid "Wire transfer"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:232
+#, fuzzy, c-format
+msgid "Withdrawal"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:248
+#, fuzzy, c-format
+msgid "Confirm the operation"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:271
+#, c-format
+msgid "Enter the confirmation code"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:313
+#, c-format
+msgid "Confirm"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:348
+#, c-format
+msgid "Send again"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:359
+#, c-format
+msgid "Send code"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:369
+#, c-format
+msgid "Operation details"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:529
+#, c-format
+msgid "Challenge details"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:536
+#, c-format
+msgid "Sent at"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:551
+#, c-format
+msgid "To phone"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:553
+#, c-format
+msgid "To email"
+msgstr ""
+
+#: src/pages/WithdrawalOperationPage.tsx:49
+#, c-format
+msgid "The Withdrawal URI is not valid"
+msgstr ""
+
+#: src/components/Cashouts/views.tsx:100
+#, c-format
+msgid "Latest cashouts"
+msgstr ""
+
+#: src/components/Cashouts/views.tsx:111
+#, c-format
+msgid "Created"
+msgstr ""
+
+#: src/components/Cashouts/views.tsx:115
+#, c-format
+msgid "Total debit"
+msgstr ""
+
+#: src/components/Cashouts/views.tsx:119
+#, c-format
+msgid "Total credit"
+msgstr ""
+
+#: src/pages/ProfileNavigation.tsx:70
+#, c-format
+msgid "Details"
+msgstr ""
+
+#: src/pages/ProfileNavigation.tsx:74
+#, c-format
+msgid "Delete"
+msgstr ""
+
+#: src/pages/ProfileNavigation.tsx:78
+#, c-format
+msgid "Credentials"
+msgstr ""
+
+#: src/pages/ProfileNavigation.tsx:82
+#, c-format
+msgid "Cashouts"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:95
+#, c-format
+msgid "Unable to create a cashout"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:96
+#, c-format
+msgid "The bank configuration does not support cashout operations."
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:223
+#, c-format
+msgid "need to be higher due to fees"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:225
+#, c-format
+msgid "the total transfer at destination will be zero"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:250
+#, c-format
+msgid "Cashout created"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:272
+#, c-format
+msgid ""
+"Duplicated request detected, check if the operation succeeded or try again."
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:279
+#, c-format
+msgid "The conversion rate was incorrectly applied"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:286
+#, c-format
+msgid "The account does not have sufficient funds"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:293
+#, c-format
+msgid "Cashouts are not supported"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:300
+#, c-format
+msgid "Missing cashout URI in the profile"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:307
+#, c-format
+msgid ""
+"Sending the confirmation message failed, retry later or contact the "
+"administrator."
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:339
+#, c-format
+msgid "Conversion rate"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:360
+#, c-format
+msgid "Fee"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:374
+#, c-format
+msgid "To account"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:381
+#, c-format
+msgid "No cashout account"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:382
+#, c-format
+msgid "Before doing a cashout you need to complete your profile"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:440
+#, fuzzy, c-format
+msgid "Amount to send"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:441
+#, fuzzy, c-format
+msgid "Amount to receive"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:490
+#, c-format
+msgid "Total cost"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:505
+#, c-format
+msgid "Balance left"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:520
+#, c-format
+msgid "Before fee"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:533
+#, c-format
+msgid "Total cashout transfer"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:553
+#, c-format
+msgid "No cashout channel available"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:555
+#, c-format
+msgid ""
+"Before doing a cashout the server need to provide an second channel to "
+"confirm the operation"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:567
+#, c-format
+msgid "Second factor authentication"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:598
+#, c-format
+msgid "Email"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:600
+#, c-format
+msgid "add a email in your profile to enable this option"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:646
+#, c-format
+msgid "SMS"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:648
+#, c-format
+msgid "add a phone number in your profile to enable this option"
+msgstr ""
+
+#: src/pages/account/CashoutListForAccount.tsx:52
+#, c-format
+msgid "Cashout for account %1$s"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:165
+#, c-format
+msgid "it doesn't have the pattern of an IBAN number"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:185
+#, c-format
+msgid "it doesn't have the pattern of an email"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:190
+#, c-format
+msgid "should start with +"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:192
+#, c-format
+msgid "phone number can't have other than numbers"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:329
+#, c-format
+msgid "account identification in the bank"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:365
+#, c-format
+msgid "name of the person owner the account"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:374
+#, c-format
+msgid "Internal IBAN"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:377
+#, c-format
+msgid "if empty a random account number will be assigned"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:378
+#, c-format
+msgid "account identification for bank transfer"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:423
+#, c-format
+msgid "Phone"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:451
+#, c-format
+msgid "Cashout IBAN"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:452
+#, c-format
+msgid "account number where the money is going to be sent when doing cashouts"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:470
+#, c-format
+msgid "Max debt"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:494
+#, c-format
+msgid "how much is user able to transfer after zero balance"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:508
+#, c-format
+msgid "Is this a Taler Exchange?"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:549
+#, c-format
+msgid "This server doesn't support second factor authentication."
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:560
+#, c-format
+msgid "Enable second factor authentication"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:596
+#, c-format
+msgid "Using email"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:654
+#, c-format
+msgid "Using SMS"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:691
+#, c-format
+msgid "Is this account public?"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:719
+#, c-format
+msgid "public accounts have their balance publicly accessible"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:100
+#, c-format
+msgid "Account updated"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:107
+#, c-format
+msgid "The rights to change the account are not sufficient"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:114
+#, c-format
+msgid "The username was not found"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:121
+#, c-format
+msgid ""
+"You can't change the legal name, please contact the your account "
+"administrator."
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:128
+#, c-format
+msgid ""
+"You can't change the debt limit, please contact the your account "
+"administrator."
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:135
+#, c-format
+msgid ""
+"You can't change the cashout address, please contact the your account "
+"administrator."
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:177
+#, c-format
+msgid "Account \"%1$s\""
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:190
+#, c-format
+msgid "Change details"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:235
+#, c-format
+msgid "Update"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:78
+#, c-format
+msgid "password doesn't match"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:95
+#, c-format
+msgid "Password changed"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:102
+#, c-format
+msgid "Not authorized to change the password, maybe the session is invalid."
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:112
+#, c-format
+msgid ""
+"You need to provide the old password. If you don't have it contact your "
+"account administrator."
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:117
+#, c-format
+msgid "Your current password doesn't match, can't change to a new password."
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:149
+#, c-format
+msgid "Update password"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:167
+#, c-format
+msgid "New password"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:195
+#, c-format
+msgid "Type it again"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:217
+#, c-format
+msgid "repeat the same password"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:227
+#, c-format
+msgid "Current password"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:248
+#, c-format
+msgid "your current password, for security"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:272
+#, c-format
+msgid "Change"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:74
+#, c-format
+msgid ""
+"Account created with password \"%1$s\". The user must change the password on "
+"the next login."
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:83
+#, c-format
+msgid "Server replied that phone or email is invalid"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:90
+#, c-format
+msgid "The rights to perform the operation are not sufficient"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:97
+#, c-format
+msgid "Account username is already taken"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:104
+#, c-format
+msgid "Account id is already taken"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:111
+#, c-format
+msgid "Bank ran out of bonus credit."
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:118
+#, c-format
+msgid "Account username can't be used because is reserved"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:160
+#, c-format
+msgid "Can't create accounts"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:161
+#, c-format
+msgid "Only system admin can create accounts."
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:183
+#, c-format
+msgid "New business account"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:209
+#, c-format
+msgid "Create"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:94
+#, c-format
+msgid "Can't delete the account"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:95
+#, c-format
+msgid ""
+"The account can't be delete while still holding some balance. First make "
+"sure that the owner make a complete cashout."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:117
+#, c-format
+msgid "Account removed"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:124
+#, c-format
+msgid "No enough permission to delete the account."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:131
+#, c-format
+msgid "The username was not found."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:138
+#, c-format
+msgid "Can't delete a reserved username."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:145
+#, c-format
+msgid "Can't delete an account with balance different than zero."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:170
+#, c-format
+msgid "name doesn't match"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:180
+#, c-format
+msgid "You are going to remove the account"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:182
+#, c-format
+msgid "This step can't be undone."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:188
+#, c-format
+msgid "Deleting account \"%1$s\""
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:206
+#, c-format
+msgid "Verification"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:231
+#, c-format
+msgid "enter the account name that is going to be deleted"
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:49
+#, c-format
+msgid "cashout id should be a number"
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:65
+#, c-format
+msgid "This cashout not found. Maybe already aborted."
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:106
+#, c-format
+msgid "Cashout detail"
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:139
+#, c-format
+msgid "Debited"
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:154
+#, c-format
+msgid "Credited"
+msgstr ""
+
+#: src/Routing.tsx:140
+#, c-format
+msgid "Welcome to %1$s!"
+msgstr ""
+
+#, c-format
+#~ msgid "days"
+#~ msgstr ""
+
+#, c-format
+#~ msgid "hours"
+#~ msgstr ""
+
+#, c-format
+#~ msgid "minutes"
+#~ msgstr ""
+
+#, c-format
+#~ msgid "seconds"
+#~ msgstr ""
+
+#~ msgid "Go back"
+#~ msgstr ""
+
+#, fuzzy
+#~ msgid "Withdraw Money into a Taler wallet"
+#~ msgstr ""
+
+#~ msgid "Page has a problem: logged in but backend state is lost."
+#~ msgstr ""
+
+#, fuzzy
+#~ msgid "Welcome to the euFin bank!"
+#~ msgstr ""
+
+#~ msgid "Page has a problem:"
+#~ msgstr ""
+
+#~ msgid "Sign in"
+#~ msgstr ""
diff --git a/packages/bank-ui/src/i18n/es.po b/packages/bank-ui/src/i18n/es.po
new file mode 100644
index 000000000..fb69822c5
--- /dev/null
+++ b/packages/bank-ui/src/i18n/es.po
@@ -0,0 +1,2063 @@
+# 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/>
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Wallet\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"POT-Creation-Date: 2016-11-23 00:00+0100\n"
+"PO-Revision-Date: 2024-02-13 14:40+0000\n"
+"Last-Translator: Stefan Kügel <skuegel@web.de>\n"
+"Language-Team: Spanish <https://weblate.taler.net/projects/gnu-taler/"
+"taler-bank-spa/es/>\n"
+"Language: es\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 5.2.1\n"
+
+#: src/utils.ts:137
+#, c-format
+msgid "Operation failed, please report"
+msgstr "La operaicón falló, por favor reportelo"
+
+#: src/utils.ts:156
+#, c-format
+msgid "Request timeout"
+msgstr "La petición al servidor agoto su tiempo"
+
+#: src/utils.ts:165
+#, c-format
+msgid "Request throttled"
+msgstr "La petición al servidor interrumpida"
+
+#: src/utils.ts:174
+#, c-format
+msgid "Malformed response"
+msgstr "Respuesta malformada"
+
+#: src/utils.ts:183
+#, c-format
+msgid "Network error"
+msgstr "Error de conexión"
+
+#: src/utils.ts:192
+#, c-format
+msgid "Unexpected request error"
+msgstr "Error de pedido inesperado"
+
+#: src/utils.ts:201
+#, c-format
+msgid "Unexpected error"
+msgstr "Error inesperado"
+
+#: src/utils.ts:377
+#, c-format
+msgid "IBAN numbers usually have more that 4 digits"
+msgstr "Los números IBAN usualmente tienen mas de 4 digitos"
+
+#: src/utils.ts:379
+#, c-format
+msgid "IBAN numbers usually have less that 34 digits"
+msgstr "Los números IBAN usualmente tienen menos de 34 digitos"
+
+#: src/utils.ts:387
+#, c-format
+msgid "IBAN country code not found"
+msgstr "Código de pais de IBAN no encontrado"
+
+#: src/utils.ts:401
+#, c-format
+msgid "IBAN number is not valid, checksum is wrong"
+msgstr "El número IBAN no es válido, falló la verificación"
+
+#: src/context/config.ts:136
+#, c-format
+msgid ""
+"the bank backend is not supported. supported version \"%1$s\", server "
+"version \"%2$s\""
+msgstr ""
+"El servidor de bank no esta spoportado. Version soportada \"%1$s\", version "
+"del server \"%2$s\""
+
+#: src/hooks/preferences.ts:55
+#, c-format
+msgid "Max withdrawal amount"
+msgstr "Monto máximo de extracción"
+
+#: src/hooks/preferences.ts:57
+#, c-format
+msgid "Show withdrawal confirmation"
+msgstr "Mostrar confirmación de extracción"
+
+#: src/hooks/preferences.ts:59
+#, c-format
+msgid "Show demo description"
+msgstr "Mostrar descripción de demo"
+
+#: src/hooks/preferences.ts:61
+#, c-format
+msgid "Show install wallet first"
+msgstr "Mostrar instalar la billetera primero"
+
+#: src/hooks/preferences.ts:63
+#, c-format
+msgid "Use fast withdrawal form"
+msgstr "Usar formulario de extracción rápida"
+
+#: src/hooks/preferences.ts:65
+#, c-format
+msgid "Show debug info"
+msgstr "Mostrar información de depuración"
+
+#: src/pages/PaytoWireTransferForm.tsx:90
+#, c-format
+msgid "required"
+msgstr "requerido"
+
+#: src/pages/PaytoWireTransferForm.tsx:92
+#, c-format
+msgid "IBAN should have just uppercased letters and numbers"
+msgstr "IBAN debería tener letras mayúsculas y números"
+
+#: src/pages/PaytoWireTransferForm.tsx:98
+#, c-format
+msgid "not valid"
+msgstr "no válido"
+
+#: src/pages/PaytoWireTransferForm.tsx:100
+#, c-format
+msgid "should be greater than 0"
+msgstr "Debería ser mas grande que 0"
+
+#: src/pages/PaytoWireTransferForm.tsx:102
+#, c-format
+msgid "balance is not enough"
+msgstr "el saldo no es suficiente"
+
+#: src/pages/PaytoWireTransferForm.tsx:112
+#, c-format
+msgid "does not follow the pattern"
+msgstr "no tiene un patrón valido"
+
+#: src/pages/PaytoWireTransferForm.tsx:114
+#, c-format
+msgid "only \"IBAN\" target are supported"
+msgstr "solo cuentas \"IBAN\" son soportadas"
+
+#: src/pages/PaytoWireTransferForm.tsx:116
+#, c-format
+msgid "use the \"amount\" parameter to specify the amount to be transferred"
+msgstr "usa el parámetro \"amount\" para indicar el monto a ser transferido"
+
+#: src/pages/PaytoWireTransferForm.tsx:118
+#, c-format
+msgid "the amount is not valid"
+msgstr "el monto no es válido"
+
+#: src/pages/PaytoWireTransferForm.tsx:120
+#, c-format
+msgid ""
+"use the \"message\" parameter to specify a reference text for the transfer"
+msgstr ""
+"usa el parámetro \"message\" para indicar un texto de referencia en la "
+"transferencia"
+
+#: src/pages/PaytoWireTransferForm.tsx:160
+#, c-format
+msgid "The request was invalid or the payto://-URI used unacceptable features."
+msgstr ""
+"El pedido era inválido o el URI payto:// usado tiene características "
+"inaceptables."
+
+#: src/pages/PaytoWireTransferForm.tsx:167
+#, c-format
+msgid "Not enough permission to complete the operation."
+msgstr "Sin permisos suficientes para completar la operación."
+
+#: src/pages/PaytoWireTransferForm.tsx:174
+#, c-format
+msgid "The destination account \"%1$s\" was not found."
+msgstr "La cuenta de destino \"%1$s\" no fue encontrada."
+
+#: src/pages/PaytoWireTransferForm.tsx:181
+#, c-format
+msgid "The origin and the destination of the transfer can't be the same."
+msgstr "El origen y destino de la transferencia no puede ser la misma."
+
+#: src/pages/PaytoWireTransferForm.tsx:188
+#, c-format
+msgid "Your balance is not enough."
+msgstr "El saldo no es suficiente."
+
+#: src/pages/PaytoWireTransferForm.tsx:195
+#, c-format
+msgid "The origin account \"%1$s\" was not found."
+msgstr "La cuenta origen \"%1$s\" no fue encontrada."
+
+#: src/pages/PaytoWireTransferForm.tsx:212
+#, c-format
+msgid "Wire transfer created!"
+msgstr "Transferencia bancaria creada!"
+
+#: src/pages/PaytoWireTransferForm.tsx:270
+#, c-format
+msgid "Using a form"
+msgstr "Usando un formulario"
+
+#: src/pages/PaytoWireTransferForm.tsx:310
+#, c-format
+msgid "Import payto:// URI"
+msgstr "Importando un URI payto://"
+
+#: src/pages/PaytoWireTransferForm.tsx:335
+#, c-format
+msgid "Recipient"
+msgstr "Destinatario"
+
+#: src/pages/PaytoWireTransferForm.tsx:359
+#, c-format
+msgid "IBAN of the recipient's account"
+msgstr "Numero IBAN de la cuenta destinataria"
+
+#: src/pages/PaytoWireTransferForm.tsx:369
+#, c-format
+msgid "Transfer subject"
+msgstr "Asunto de transferencia"
+
+#: src/pages/PaytoWireTransferForm.tsx:377
+#, c-format
+msgid "subject"
+msgstr "asunto"
+
+#: src/pages/PaytoWireTransferForm.tsx:390
+#, c-format
+msgid "some text to identify the transfer"
+msgstr "algún texto para identificar la transferencia"
+
+#: src/pages/PaytoWireTransferForm.tsx:400
+#, c-format
+msgid "Amount"
+msgstr "Monto"
+
+#: src/pages/PaytoWireTransferForm.tsx:415
+#, c-format
+msgid "amount to transfer"
+msgstr "monto a transferir"
+
+#: src/pages/PaytoWireTransferForm.tsx:425
+#, c-format
+msgid "payto URI:"
+msgstr "payto URI:"
+
+#: src/pages/PaytoWireTransferForm.tsx:436
+#, c-format
+msgid "uniform resource identifier of the target account"
+msgstr "identificador de recurso uniforme de la cuenta destino"
+
+#: src/pages/PaytoWireTransferForm.tsx:437
+#, c-format
+msgid "payto://iban/[receiver-iban]?message=[subject]&amount=[%1$s:X.Y]"
+msgstr "payto://iban/[iban-destinatario]?message=[asunto]&amount=[%1$s:X.Y]"
+
+#: src/pages/PaytoWireTransferForm.tsx:457
+#, c-format
+msgid "Cancel"
+msgstr "Cancelar"
+
+#: src/pages/PaytoWireTransferForm.tsx:471
+#, c-format
+msgid "Send"
+msgstr "Envíar"
+
+#: src/pages/LoginForm.tsx:71
+#, c-format
+msgid "Missing username"
+msgstr "Falta nombre de usuario"
+
+#: src/pages/LoginForm.tsx:75
+#, c-format
+msgid "Missing password"
+msgstr "Falta contraseña"
+
+#: src/pages/LoginForm.tsx:104
+#, c-format
+msgid "Wrong credentials for \"%1$s\""
+msgstr "Credenciales incorrectas para \"%1$s\""
+
+#: src/pages/LoginForm.tsx:111
+#, c-format
+msgid "Account not found"
+msgstr "Cuenta no encontrada"
+
+#: src/pages/LoginForm.tsx:142
+#, c-format
+msgid "Username"
+msgstr "Nombre de usuario"
+
+#: src/pages/LoginForm.tsx:156
+#, c-format
+msgid "username of the account"
+msgstr "nombre de usuario de la cuenta"
+
+#: src/pages/LoginForm.tsx:175
+#, c-format
+msgid "Password"
+msgstr "Contraseña"
+
+#: src/pages/LoginForm.tsx:188
+#, c-format
+msgid "password of the account"
+msgstr "contraseña de la cuenta"
+
+#: src/pages/LoginForm.tsx:223
+#, c-format
+msgid "Check"
+msgstr "Verificar"
+
+#: src/pages/LoginForm.tsx:237
+#, c-format
+msgid "Log in"
+msgstr "Acceso"
+
+#: src/pages/LoginForm.tsx:249
+#, c-format
+msgid "Register"
+msgstr "Registrarse"
+
+#: src/components/Transactions/views.tsx:52
+#, c-format
+msgid "Latest transactions"
+msgstr "Últimas transacciones"
+
+#: src/components/Transactions/views.tsx:63
+#, c-format
+msgid "Date"
+msgstr "Fecha"
+
+#: src/components/Transactions/views.tsx:71
+#, c-format
+msgid "Counterpart"
+msgstr "Contraparte"
+
+#: src/components/Transactions/views.tsx:75
+#, c-format
+msgid "Subject"
+msgstr "Asunto"
+
+#: src/components/Transactions/views.tsx:111
+#, c-format
+msgid "sent"
+msgstr "enviado"
+
+#: src/components/Transactions/views.tsx:112
+#, c-format
+msgid "received"
+msgstr "recibido"
+
+#: src/components/Transactions/views.tsx:127
+#, c-format
+msgid "invalid value"
+msgstr "valor inválido"
+
+#: src/components/Transactions/views.tsx:136
+#, c-format
+msgid "to"
+msgstr "hacia"
+
+#: src/components/Transactions/views.tsx:136
+#, c-format
+msgid "from"
+msgstr "desde"
+
+#: src/components/Transactions/views.tsx:202
+#, c-format
+msgid "First page"
+msgstr "Primera página"
+
+#: src/components/Transactions/views.tsx:209
+#, c-format
+msgid "Next"
+msgstr "Siguiente"
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:86
+#, c-format
+msgid "Wire transfer completed!"
+msgstr "Transferencia bancaria completada!"
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:93
+#, c-format
+msgid "The withdrawal has been aborted previously and can't be confirmed"
+msgstr "La extracción fue abortada anteriormente y no puede ser confirmada"
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:100
+#, c-format
+msgid ""
+"The withdrawal operation can't be confirmed before a wallet accepted the "
+"transaction."
+msgstr ""
+"La operación de extracción no puede ser confirmada antes de que una "
+"billetera acepte la transaccion."
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:107
+#, c-format
+msgid "The operation id is invalid."
+msgstr "El id de operación es invalido."
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:114
+#, c-format
+msgid "The operation was not found."
+msgstr "La operación no se encontró."
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:121
+#, c-format
+msgid "Your balance is not enough for the operation."
+msgstr "El saldo no es suficiente para la operación."
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:155
+#, c-format
+msgid ""
+"The reserve operation has been confirmed previously and can't be aborted"
+msgstr ""
+"La operación en la reserva ya ha sido confirmada previamente y no puede ser "
+"abortada"
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:186
+#, c-format
+msgid "Confirm the withdrawal operation"
+msgstr "Confirme la operación de extracción"
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:203
+#, c-format
+msgid "Wire transfer details"
+msgstr "Detalle de transferencia bancaria"
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:217
+#, c-format
+msgid "Taler Exchange operator's account"
+msgstr "Cuenta del operador del Taler Exchange"
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:228
+#, c-format
+msgid "Taler Exchange operator's name"
+msgstr "Nombre del operador del Taler Exchange"
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:317
+#, c-format
+msgid "Transfer"
+msgstr "Transferencia"
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:342
+#, c-format
+msgid "Authentication required"
+msgstr "Autenticación requerida"
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:352
+#, c-format
+msgid "This operation was created with other username"
+msgstr "Esta operación fue creada con otro usuario"
+
+#: src/pages/OperationState/views.tsx:209
+#, c-format
+msgid ""
+"Unauthorized to make the operation, maybe the session has expired or the "
+"password changed."
+msgstr ""
+"No autorizado para hacer la operación, quizá la sesión haya expirado or "
+"cambió la contraseña."
+
+#: src/pages/OperationState/views.tsx:218
+#, c-format
+msgid "The operation was rejected due to insufficient funds."
+msgstr "La operación fue rechazada debido a saldo insuficiente."
+
+#: src/pages/OperationState/views.tsx:268
+#, c-format
+msgid "Withdrawal confirmed"
+msgstr "La extracción fue confirmada"
+
+#: src/pages/OperationState/views.tsx:272
+#, c-format
+msgid ""
+"The wire transfer to the Taler operator has been initiated. You will soon "
+"receive the requested amount in your Taler wallet."
+msgstr ""
+"La transferencia bancaria al operador Taler fue iniciada. Pronto recibirás "
+"el monto pedido en tu billetera Taler."
+
+#: src/pages/OperationState/views.tsx:287
+#, c-format
+msgid "Do not show this again"
+msgstr "No mostrar otra vez"
+
+#: src/pages/OperationState/views.tsx:319
+#, c-format
+msgid "Close"
+msgstr "Cerrar"
+
+#: src/pages/OperationState/views.tsx:399
+#, c-format
+msgid "On this device"
+msgstr "En este dispositivo"
+
+#: src/pages/OperationState/views.tsx:404
+#, c-format
+msgid ""
+"If you are using a web browser on desktop you should access your wallet with "
+"the GNU Taler WebExtension now or click the link if your WebExtension have "
+"the \"Inject Taler support\" option enabled."
+msgstr ""
+"Si esta usando un explorador web de escritorio deberías acceder ahora a tu "
+"billletera con la GNU Taler WebExtension o hacer click en el link si tu "
+"extensión tiene la configuración \"Inyectar soporte para Taler\" habilitada."
+
+#: src/pages/OperationState/views.tsx:417
+#, c-format
+msgid "Start"
+msgstr "Comenzar"
+
+#: src/pages/OperationState/views.tsx:426
+#, c-format
+msgid "On a mobile phone"
+msgstr "En un dispotivo mobile"
+
+#: src/pages/OperationState/views.tsx:431
+#, c-format
+msgid "Scan the QR code with your mobile device."
+msgstr "Escanear el código QR con tu dispotivo móvil."
+
+#: src/pages/WalletWithdrawForm.tsx:73
+#, c-format
+msgid "There is an operation already"
+msgstr "Ya hay una operación"
+
+#: src/pages/WalletWithdrawForm.tsx:75
+#, c-format
+msgid "Complete or cancel the operation in"
+msgstr "Completa o cancela la operación en"
+
+#: src/pages/WalletWithdrawForm.tsx:84
+#, c-format
+msgid "this page"
+msgstr "esta página"
+
+#: src/pages/WalletWithdrawForm.tsx:101
+#, c-format
+msgid "invalid"
+msgstr "inválido"
+
+#: src/pages/WalletWithdrawForm.tsx:116
+#, c-format
+msgid "Server responded with an invalid withdraw URI"
+msgstr "El servidor respondió con una URI de extracción inválida"
+
+#: src/pages/WalletWithdrawForm.tsx:117
+#, c-format
+msgid "Withdraw URI: %1$s"
+msgstr "URI de extracción: %1$s"
+
+#: src/pages/WalletWithdrawForm.tsx:132
+#, c-format
+msgid "The operation was rejected due to insufficient funds"
+msgstr "La operación fue rechazada debido a fundos insuficientes"
+
+#: src/pages/WalletWithdrawForm.tsx:253
+#, c-format
+msgid "Continue"
+msgstr "Continuar"
+
+#: src/pages/WalletWithdrawForm.tsx:282
+#, c-format
+msgid "Prepare your wallet"
+msgstr "Prepare su billetera"
+
+#: src/pages/WalletWithdrawForm.tsx:285
+#, c-format
+msgid ""
+"After using your wallet you will need to confirm or cancel the operation on "
+"this site."
+msgstr ""
+"Despues de usar tu billetera necesitarás confirmar o cancelar la operación "
+"en este sitio."
+
+#: src/pages/WalletWithdrawForm.tsx:295
+#, c-format
+msgid "You need a GNU Taler Wallet"
+msgstr "Necesitas una GNU Taler Wallet"
+
+#: src/pages/WalletWithdrawForm.tsx:300
+#, c-format
+msgid "If you don't have one yet you can follow the instruction in"
+msgstr "Si no tienes una todavía puedes seguir las instrucciones en"
+
+#: src/pages/PaymentOptions.tsx:55
+#, c-format
+msgid "Send money"
+msgstr "Enviar dinero"
+
+#: src/pages/PaymentOptions.tsx:73
+#, c-format
+msgid "to a %1$s wallet"
+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"
+
+#: src/pages/PaymentOptions.tsx:109
+#, c-format
+msgid "operation ready"
+msgstr "operación lista"
+
+#: src/pages/PaymentOptions.tsx:129
+#, c-format
+msgid "to another bank account"
+msgstr "a otra cuenta bancaria"
+
+#: src/pages/PaymentOptions.tsx:149
+#, c-format
+msgid "Make a wire transfer to an account with known bank account number."
+msgstr ""
+"Hacer una transferencia bancaria a una cuenta con un número de cuenta "
+"conocido."
+
+#: src/pages/PaymentOptions.tsx:171
+#, c-format
+msgid "Transfer details"
+msgstr "Detalles de transferencia"
+
+#: src/pages/AccountPage/views.tsx:41
+#, c-format
+msgid "This is a demo bank"
+msgstr "Este es un banco de demostración"
+
+#: src/pages/AccountPage/views.tsx:46
+#, c-format
+msgid ""
+"This part of the demo shows how a bank that supports Taler directly would "
+"work. In addition to using your own bank account, you can also see the "
+"transaction history of some %1$s."
+msgstr ""
+"Esta parte de la demostración muestra cómo funciona un banco que soporta "
+"Taler directamente. Además de usar tu propia cuenta de banco, también podrás "
+"ver el historial de transacciones de algunas %1$s."
+
+#: src/pages/AccountPage/views.tsx:53
+#, c-format
+msgid ""
+"This part of the demo shows how a bank that supports Taler directly would "
+"work."
+msgstr ""
+"Esta parte de la demostración muetra como un banco que soporta Taler "
+"directamente funcionaría."
+
+#: src/pages/AccountPage/views.tsx:70
+#, c-format
+msgid "Pending account delete operation"
+msgstr "Operación pendiente de eliminación de cuenta"
+
+#: src/pages/AccountPage/views.tsx:72
+#, c-format
+msgid "Pending account update operation"
+msgstr "Operación pendiente de actualización de cuenta"
+
+#: src/pages/AccountPage/views.tsx:74
+#, c-format
+msgid "Pending password update operation"
+msgstr "Operación pendiente de actualización de password"
+
+#: src/pages/AccountPage/views.tsx:76
+#, c-format
+msgid "Pending transaction operation"
+msgstr "Operación pendiente de transacción"
+
+#: src/pages/AccountPage/views.tsx:78
+#, c-format
+msgid "Pending withdrawal operation"
+msgstr "Operación pendiente de extracción"
+
+#: src/pages/AccountPage/views.tsx:80
+#, c-format
+msgid "Pending cashout operation"
+msgstr "Operación pendiente de egreso"
+
+#: src/pages/AccountPage/views.tsx:91
+#, c-format
+msgid "You can complete or cancel the operation in"
+msgstr "Puedes completar o cancelar la operación en"
+
+#: src/pages/BankFrame.tsx:64
+#, c-format
+msgid "Internal error, please report."
+msgstr "Error interno, por favor reporte el error."
+
+#: src/pages/BankFrame.tsx:100
+#, c-format
+msgid "Preferences"
+msgstr "Preferencias"
+
+#: src/pages/BankFrame.tsx:184
+#, c-format
+msgid "Welcome, %1$s"
+msgstr "Bienvenido/a, %1$s"
+
+#: src/pages/WireTransfer.tsx:79
+#, c-format
+msgid "Make a wire transfer"
+msgstr "Hacer una transferencia bancaria"
+
+#: src/pages/admin/AccountList.tsx:72
+#, c-format
+msgid "Accounts"
+msgstr "Cuentas"
+
+#: src/pages/admin/AccountList.tsx:75
+#, c-format
+msgid "A list of all business account in the bank."
+msgstr "Una lista de todas las cuentas en el banco."
+
+#: src/pages/admin/AccountList.tsx:86
+#, c-format
+msgid "Create account"
+msgstr "Crear cuenta"
+
+#: src/pages/admin/AccountList.tsx:106
+#, c-format
+msgid "Name"
+msgstr "Nombre"
+
+#: src/pages/admin/AccountList.tsx:110
+#, c-format
+msgid "Balance"
+msgstr "Saldo"
+
+#: src/pages/admin/AccountList.tsx:112
+#, c-format
+msgid "Actions"
+msgstr "Acciones"
+
+#: src/pages/admin/AccountList.tsx:151
+#, c-format
+msgid "unknown"
+msgstr "desconocido"
+
+#: src/pages/admin/AccountList.tsx:170
+#, c-format
+msgid "change password"
+msgstr "cambiar contraseña"
+
+#: src/pages/admin/AccountList.tsx:179
+#, c-format
+msgid "cashouts"
+msgstr "egresos"
+
+#: src/pages/admin/AccountList.tsx:189
+#, c-format
+msgid "remove"
+msgstr "elimiar"
+
+#: src/pages/admin/AdminHome.tsx:168
+#, c-format
+msgid "Cashout not implemented"
+msgstr "Egreso no soportado"
+
+#: src/pages/admin/AdminHome.tsx:184
+#, c-format
+msgid "Select a section"
+msgstr "Seleccione una sección"
+
+#: src/pages/admin/AdminHome.tsx:202
+#, c-format
+msgid "Last hour"
+msgstr "Última hora"
+
+#: src/pages/admin/AdminHome.tsx:208
+#, c-format
+msgid "Last day"
+msgstr "Último día"
+
+#: src/pages/admin/AdminHome.tsx:216
+#, c-format
+msgid "Last month"
+msgstr "Último mes"
+
+#: src/pages/admin/AdminHome.tsx:222
+#, c-format
+msgid "Last year"
+msgstr "Último año"
+
+#: src/pages/admin/AdminHome.tsx:310
+#, c-format
+msgid "Last Year"
+msgstr "Último Año"
+
+#: src/pages/admin/AdminHome.tsx:325
+#, c-format
+msgid "Trading volume on %1$s compared to %2$s"
+msgstr "Vólumen de comercio en %1$s comparado con %2$s"
+
+#: src/pages/admin/AdminHome.tsx:342
+#, c-format
+msgid "Cashin"
+msgstr "Ingreso"
+
+#: src/pages/admin/AdminHome.tsx:352
+#, c-format
+msgid "Cashout"
+msgstr "Egreso"
+
+#: src/pages/admin/AdminHome.tsx:364
+#, c-format
+msgid "Payin"
+msgstr "Envios de dinero"
+
+#: src/pages/admin/AdminHome.tsx:374
+#, c-format
+msgid "Payout"
+msgstr "Recibos de dinero"
+
+#: src/pages/admin/AdminHome.tsx:388
+#, c-format
+msgid "download stats as CSV"
+msgstr "descargar estadísticas en CSV"
+
+#: src/pages/admin/AdminHome.tsx:494
+#, c-format
+msgid "Decreased by"
+msgstr "Descendiente por"
+
+#: src/pages/admin/AdminHome.tsx:498
+#, c-format
+msgid "Increased by"
+msgstr "Ascendente por"
+
+#: src/pages/DownloadStats.tsx:89
+#, c-format
+msgid "Download bank stats"
+msgstr "Descargar estadísticas del banco"
+
+#: src/pages/DownloadStats.tsx:110
+#, c-format
+msgid "Include hour metric"
+msgstr "Incluir métrica horaria"
+
+#: src/pages/DownloadStats.tsx:143
+#, c-format
+msgid "Include day metric"
+msgstr "Incluir métrica diaria"
+
+#: src/pages/DownloadStats.tsx:173
+#, c-format
+msgid "Include month metric"
+msgstr "Incluir métrica mensual"
+
+#: src/pages/DownloadStats.tsx:206
+#, c-format
+msgid "Include year metric"
+msgstr "Incluir métrica anual"
+
+#: src/pages/DownloadStats.tsx:239
+#, c-format
+msgid "Include table header"
+msgstr "Incluir encabezado de tabla"
+
+#: src/pages/DownloadStats.tsx:272
+#, c-format
+msgid "Add previous metric for compare"
+msgstr "Agregar métrica previa para comparar"
+
+#: src/pages/DownloadStats.tsx:307
+#, c-format
+msgid "Fail on first error"
+msgstr "Fallar en el primer error"
+
+#: src/pages/DownloadStats.tsx:364
+#, c-format
+msgid "Download"
+msgstr "Descargar"
+
+#: src/pages/DownloadStats.tsx:381
+#, c-format
+msgid "downloading... %1$s"
+msgstr "descargando... %1$s"
+
+#: src/pages/DownloadStats.tsx:399
+#, c-format
+msgid "Download completed"
+msgstr "Descarga completada"
+
+#: src/pages/DownloadStats.tsx:400
+#, c-format
+msgid "click here to save the file in your computer"
+msgstr "click aquí para guardar el archivo en su computadora"
+
+#: src/pages/PublicHistoriesPage.tsx:78
+#, c-format
+msgid "History of public accounts"
+msgstr "Historial de cuentas públicas"
+
+#: src/pages/RegistrationPage.tsx:48
+#, c-format
+msgid "Currently, the bank is not accepting new registrations!"
+msgstr "Actualmente, el banco no está aceptado nuevos registros!"
+
+#: src/pages/RegistrationPage.tsx:87
+#, c-format
+msgid "Missing name"
+msgstr "Falta nombre"
+
+#: src/pages/RegistrationPage.tsx:91
+#, c-format
+msgid "Use letters and numbers only, and start with a lowercase letter"
+msgstr "Solo use letras y números, y comience con una letra minúscula"
+
+#: src/pages/RegistrationPage.tsx:107
+#, c-format
+msgid "Passwords don't match"
+msgstr "La contraseña no coincide"
+
+#: src/pages/RegistrationPage.tsx:130
+#, c-format
+msgid "Server replied with invalid phone or email."
+msgstr "El servidor repondio con teléfono o dirección de correo inválido."
+
+#: src/pages/RegistrationPage.tsx:137
+#, c-format
+msgid "Registration is disabled because the bank ran out of bonus credit."
+msgstr ""
+"El registro está deshabilitado porque el banco se quedó sin crédito bonus."
+
+#: src/pages/RegistrationPage.tsx:144
+#, c-format
+msgid "No enough permission to create that account."
+msgstr "Sin permisos suficientes para crear esa cuenta."
+
+#: src/pages/RegistrationPage.tsx:151
+#, c-format
+msgid "That account id is already taken."
+msgstr "El identificador de cuenta ya está tomado."
+
+#: src/pages/RegistrationPage.tsx:158
+#, c-format
+msgid "That username is already taken."
+msgstr "El nombre de usuario ya está tomado."
+
+#: src/pages/RegistrationPage.tsx:165
+#, c-format
+msgid "That username can't be used because is reserved."
+msgstr "El nombre de usuario no puede ser usado porque esta reservado."
+
+#: src/pages/RegistrationPage.tsx:172
+#, c-format
+msgid "Only admin is allow to set debt limit."
+msgstr "Solo el administrador tiene permitido cambiar el límite de deuda."
+
+#: src/pages/RegistrationPage.tsx:179
+#, c-format
+msgid "No information for the selected authentication channel."
+msgstr "No hay información para el canal de autenticación seleccionado."
+
+#: src/pages/RegistrationPage.tsx:186
+#, c-format
+msgid "Authentication channel is not supported."
+msgstr "Canal de autenticación no esta soportado."
+
+#: src/pages/RegistrationPage.tsx:193
+#, c-format
+msgid "Only admin can create accounts with second factor authentication."
+msgstr ""
+"Solo el administrador puede crear cuentas con el segundo factor de "
+"autenticación."
+
+#: src/pages/RegistrationPage.tsx:233
+#, c-format
+msgid "Account registration"
+msgstr "Registro de cuenta"
+
+#: src/pages/RegistrationPage.tsx:315
+#, c-format
+msgid "Repeat password"
+msgstr "Repita la contraseña"
+
+#: src/pages/RegistrationPage.tsx:457
+#, c-format
+msgid "Create a random temporary user"
+msgstr "Crear un usuario aleatorio temporal"
+
+#: src/pages/QrCodeSection.tsx:110
+#, c-format
+msgid "If you have a Taler wallet installed in this device"
+msgstr "Si tienes una billetera Taler instalada en este dispositivo"
+
+#: src/pages/QrCodeSection.tsx:116
+#, c-format
+msgid ""
+"You will see the details of the operation in your wallet including the fees "
+"(if applies). If you still don't have one you can install it following "
+"instructions in"
+msgstr ""
+"Veras los detalles de la operación en tu billetera incluyendo comisiones (si "
+"aplicán). Si todavía no tienes una puedes instalarla siguiendo las "
+"instrucciones en"
+
+#: src/pages/QrCodeSection.tsx:143
+#, c-format
+msgid "Withdraw"
+msgstr "Retirar"
+
+#: src/pages/QrCodeSection.tsx:152
+#, c-format
+msgid "Or if you have the wallet in another device"
+msgstr "O si tienes la billetera en otro dispositivo"
+
+#: src/pages/QrCodeSection.tsx:157
+#, c-format
+msgid "Scan the QR below to start the withdrawal."
+msgstr "Escanea el QR debajo para comenzar la extracción."
+
+#: src/pages/WithdrawalQRCode.tsx:79
+#, c-format
+msgid "Operation aborted"
+msgstr "Operación abortada"
+
+#: src/pages/WithdrawalQRCode.tsx:82
+#, c-format
+msgid ""
+"The wire transfer to the Taler Exchange operator's account was aborted, your "
+"balance was not affected."
+msgstr ""
+"La transferencia bancaria a la cuenta del operador del Taler Exchange fue "
+"abortada, su saldo no fue afectado."
+
+#: src/pages/WithdrawalQRCode.tsx:88
+#, c-format
+msgid "You can close this page now or continue to the account page."
+msgstr ""
+"Ya puedes cerrar esta pagina or continuar a la página de estado de cuenta."
+
+#: src/pages/WithdrawalQRCode.tsx:147
+#, c-format
+msgid "Done"
+msgstr "Listo"
+
+#: src/pages/WithdrawalQRCode.tsx:158
+#, c-format
+msgid "Operation canceled"
+msgstr "Operación cancelada"
+
+#: src/pages/WithdrawalQRCode.tsx:173
+#, c-format
+msgid ""
+"The operation is marked as 'selected' but some step in the withdrawal failed"
+msgstr ""
+"La operación está marcada como 'seleccionada' pero algunos pasos en la "
+"extracción fallaron"
+
+#: src/pages/WithdrawalQRCode.tsx:175
+#, c-format
+msgid "The account is selected but no withdrawal identification found."
+msgstr ""
+"La cuenta está seleccionada pero no se encontró el identificador de "
+"extracción."
+
+#: src/pages/WithdrawalQRCode.tsx:188
+#, c-format
+msgid ""
+"There is a withdrawal identification but no account has been selected or the "
+"selected account is invalid."
+msgstr ""
+"Hay un identificador de extracción pero la cuenta no ha sido seleccionada o "
+"la selccionada es inválida."
+
+#: src/pages/WithdrawalQRCode.tsx:202
+#, c-format
+msgid ""
+"No withdrawal ID found and no account has been selected or the selected "
+"account is invalid."
+msgstr ""
+"No hay un identificador de extracción y ninguna cuenta a sido seleccionada o "
+"la seleccionada es inválida."
+
+#: src/pages/WithdrawalQRCode.tsx:259
+#, c-format
+msgid "Operation not found"
+msgstr "Operación no encontrada"
+
+#: src/pages/WithdrawalQRCode.tsx:263
+#, c-format
+msgid ""
+"This operation is not known by the server. The operation id is wrong or the "
+"server deleted the operation information before reaching here."
+msgstr ""
+"Esta operación no es conocida por el servidor. El identificador de operación "
+"es incorrecto o el server borró la información de la operación antes de "
+"llegar hasta aquí."
+
+#: src/pages/WithdrawalQRCode.tsx:278
+#, c-format
+msgid "Cotinue to dashboard"
+msgstr "Continuar al panel"
+
+#: src/pages/SolveChallengePage.tsx:98
+#, c-format
+msgid "Cashout not found. It may be also mean that it was already aborted."
+msgstr "Egreso no econtrado. También puede significar que ya ha sido abortado."
+
+#: src/pages/SolveChallengePage.tsx:136
+#, c-format
+msgid "Challenge not found."
+msgstr "Desafío no encontrado."
+
+#: src/pages/SolveChallengePage.tsx:143
+#, c-format
+msgid "This user is not authorized to complete this challenge."
+msgstr "Este usuario no está autorizado para completar este desafío."
+
+#: src/pages/SolveChallengePage.tsx:150
+#, c-format
+msgid "Too many attempts, try another code."
+msgstr "Demasiados intentos, intente otro código."
+
+#: src/pages/SolveChallengePage.tsx:157
+#, c-format
+msgid "The confirmation code is wrong, try again."
+msgstr "El código de confirmación es erroneo, intente otra vez."
+
+#: src/pages/SolveChallengePage.tsx:164
+#, c-format
+msgid "The operation expired."
+msgstr "La operación expiró."
+
+#: src/pages/SolveChallengePage.tsx:197
+#, c-format
+msgid "The operation failed."
+msgstr "La operación falló."
+
+#: src/pages/SolveChallengePage.tsx:212
+#, c-format
+msgid "The operation needs another confirmation to complete."
+msgstr "La operación necesita otra confirmación para completar."
+
+#: src/pages/SolveChallengePage.tsx:224
+#, c-format
+msgid "Account delete"
+msgstr "Eliminación de cuenta"
+
+#: src/pages/SolveChallengePage.tsx:226
+#, c-format
+msgid "Account update"
+msgstr "Actualización de cuenta"
+
+#: src/pages/SolveChallengePage.tsx:228
+#, c-format
+msgid "Password update"
+msgstr "Actualización de contraseña"
+
+#: src/pages/SolveChallengePage.tsx:230
+#, c-format
+msgid "Wire transfer"
+msgstr "Transferencia bancaria"
+
+#: src/pages/SolveChallengePage.tsx:232
+#, c-format
+msgid "Withdrawal"
+msgstr "Extracción"
+
+#: src/pages/SolveChallengePage.tsx:248
+#, c-format
+msgid "Confirm the operation"
+msgstr "Confirmar la operación"
+
+#: src/pages/SolveChallengePage.tsx:271
+#, c-format
+msgid "Enter the confirmation code"
+msgstr "Ingresar el código de confirmación"
+
+#: src/pages/SolveChallengePage.tsx:313
+#, c-format
+msgid "Confirm"
+msgstr "Confirmar"
+
+#: src/pages/SolveChallengePage.tsx:348
+#, c-format
+msgid "Send again"
+msgstr "Enviar otra vez"
+
+#: src/pages/SolveChallengePage.tsx:359
+#, c-format
+msgid "Send code"
+msgstr "Enviar código"
+
+#: src/pages/SolveChallengePage.tsx:369
+#, c-format
+msgid "Operation details"
+msgstr "Detalles de operación"
+
+#: src/pages/SolveChallengePage.tsx:529
+#, c-format
+msgid "Challenge details"
+msgstr "Detalles del desafío"
+
+#: src/pages/SolveChallengePage.tsx:536
+#, c-format
+msgid "Sent at"
+msgstr "Enviado a"
+
+#: src/pages/SolveChallengePage.tsx:551
+#, c-format
+msgid "To phone"
+msgstr "Al teléfono"
+
+#: src/pages/SolveChallengePage.tsx:553
+#, c-format
+msgid "To email"
+msgstr "Al email"
+
+#: src/pages/WithdrawalOperationPage.tsx:49
+#, c-format
+msgid "The Withdrawal URI is not valid"
+msgstr "El URI de estracción no es válido"
+
+#: src/components/Cashouts/views.tsx:100
+#, c-format
+msgid "Latest cashouts"
+msgstr "Últimos egresos"
+
+#: src/components/Cashouts/views.tsx:111
+#, c-format
+msgid "Created"
+msgstr "Creado"
+
+#: src/components/Cashouts/views.tsx:115
+#, c-format
+msgid "Total debit"
+msgstr "Débito total"
+
+#: src/components/Cashouts/views.tsx:119
+#, c-format
+msgid "Total credit"
+msgstr "Crédito total"
+
+#: src/pages/ProfileNavigation.tsx:70
+#, c-format
+msgid "Details"
+msgstr "Detalles"
+
+#: src/pages/ProfileNavigation.tsx:74
+#, c-format
+msgid "Delete"
+msgstr "Borrar"
+
+#: src/pages/ProfileNavigation.tsx:78
+#, c-format
+msgid "Credentials"
+msgstr "Credenciales"
+
+#: src/pages/ProfileNavigation.tsx:82
+#, c-format
+msgid "Cashouts"
+msgstr "Egresos"
+
+#: src/pages/business/CreateCashout.tsx:95
+#, c-format
+msgid "Unable to create a cashout"
+msgstr "Imposible crear un egreso"
+
+#: src/pages/business/CreateCashout.tsx:96
+#, c-format
+msgid "The bank configuration does not support cashout operations."
+msgstr "La configuración del banco no soporta operaciones de egreso."
+
+#: src/pages/business/CreateCashout.tsx:223
+#, c-format
+msgid "need to be higher due to fees"
+msgstr "necesita ser mayor debido a las comisiones"
+
+#: src/pages/business/CreateCashout.tsx:225
+#, c-format
+msgid "the total transfer at destination will be zero"
+msgstr "el total de la transferencia en destino será cero"
+
+#: src/pages/business/CreateCashout.tsx:250
+#, c-format
+msgid "Cashout created"
+msgstr "Egreso creado"
+
+#: src/pages/business/CreateCashout.tsx:272
+#, c-format
+msgid ""
+"Duplicated request detected, check if the operation succeeded or try again."
+msgstr ""
+"Se detectó una petición duplicada, verifique si la operación tuvo éxito o "
+"intente otra vez."
+
+#: src/pages/business/CreateCashout.tsx:279
+#, c-format
+msgid "The conversion rate was incorrectly applied"
+msgstr "La tasa de conversión se aplicó incorrectamente"
+
+#: src/pages/business/CreateCashout.tsx:286
+#, c-format
+msgid "The account does not have sufficient funds"
+msgstr "La cuenta no tiene fondos suficientes"
+
+#: src/pages/business/CreateCashout.tsx:293
+#, c-format
+msgid "Cashouts are not supported"
+msgstr "Egresos no están soportados"
+
+#: src/pages/business/CreateCashout.tsx:300
+#, c-format
+msgid "Missing cashout URI in the profile"
+msgstr "Falta dirección de egreso en el perfíl"
+
+#: src/pages/business/CreateCashout.tsx:307
+#, c-format
+msgid ""
+"Sending the confirmation message failed, retry later or contact the "
+"administrator."
+msgstr ""
+"El envío del mensaje de confirmación falló, intente mas tarde o contacte al "
+"administrador."
+
+#: src/pages/business/CreateCashout.tsx:339
+#, c-format
+msgid "Conversion rate"
+msgstr "Tasa de conversión"
+
+#: src/pages/business/CreateCashout.tsx:360
+#, c-format
+msgid "Fee"
+msgstr "Comisión"
+
+#: src/pages/business/CreateCashout.tsx:374
+#, c-format
+msgid "To account"
+msgstr "Hacia cuenta"
+
+#: src/pages/business/CreateCashout.tsx:381
+#, c-format
+msgid "No cashout account"
+msgstr "No hay cuenta de egreso"
+
+#: src/pages/business/CreateCashout.tsx:382
+#, c-format
+msgid "Before doing a cashout you need to complete your profile"
+msgstr "Antes de hacer un egreso necesita completar su perfíl"
+
+#: src/pages/business/CreateCashout.tsx:440
+#, c-format
+msgid "Amount to send"
+msgstr "Monto a enviar"
+
+#: src/pages/business/CreateCashout.tsx:441
+#, c-format
+msgid "Amount to receive"
+msgstr "Monto a recibir"
+
+#: src/pages/business/CreateCashout.tsx:490
+#, c-format
+msgid "Total cost"
+msgstr "Costo total"
+
+#: src/pages/business/CreateCashout.tsx:505
+#, c-format
+msgid "Balance left"
+msgstr "Saldo remanente"
+
+#: src/pages/business/CreateCashout.tsx:520
+#, c-format
+msgid "Before fee"
+msgstr "Antes de comisión"
+
+#: src/pages/business/CreateCashout.tsx:533
+#, c-format
+msgid "Total cashout transfer"
+msgstr "Total de egreso"
+
+#: src/pages/business/CreateCashout.tsx:553
+#, c-format
+msgid "No cashout channel available"
+msgstr "No hay canal de egreso disponible"
+
+#: src/pages/business/CreateCashout.tsx:555
+#, c-format
+msgid ""
+"Before doing a cashout the server need to provide an second channel to "
+"confirm the operation"
+msgstr ""
+"Antes de hacer un egreso el servidor necesita proveer un segundo canal para "
+"confirmar la operación"
+
+#: src/pages/business/CreateCashout.tsx:567
+#, c-format
+msgid "Second factor authentication"
+msgstr "Segundo factor de autenticación"
+
+#: src/pages/business/CreateCashout.tsx:598
+#, c-format
+msgid "Email"
+msgstr "Correo eletrónico"
+
+#: src/pages/business/CreateCashout.tsx:600
+#, c-format
+msgid "add a email in your profile to enable this option"
+msgstr "agrege un correo en su perfíl para habilitar esta opción"
+
+#: src/pages/business/CreateCashout.tsx:646
+#, c-format
+msgid "SMS"
+msgstr "SMS"
+
+#: src/pages/business/CreateCashout.tsx:648
+#, c-format
+msgid "add a phone number in your profile to enable this option"
+msgstr "agregue un número de teléfono para habilitar esta opción"
+
+#: src/pages/account/CashoutListForAccount.tsx:52
+#, c-format
+msgid "Cashout for account %1$s"
+msgstr "Egreso para cuenta %1$s"
+
+#: src/pages/admin/AccountForm.tsx:165
+#, c-format
+msgid "it doesn't have the pattern of an IBAN number"
+msgstr "no tiene el patrón de un número IBAN"
+
+#: src/pages/admin/AccountForm.tsx:185
+#, c-format
+msgid "it doesn't have the pattern of an email"
+msgstr "no tiene el patrón de un correo electrónico"
+
+#: src/pages/admin/AccountForm.tsx:190
+#, c-format
+msgid "should start with +"
+msgstr "debería comenzar con un +"
+
+#: src/pages/admin/AccountForm.tsx:192
+#, c-format
+msgid "phone number can't have other than numbers"
+msgstr "número de teléfono no puede tener otra cosa que numeros"
+
+#: src/pages/admin/AccountForm.tsx:329
+#, c-format
+msgid "account identification in the bank"
+msgstr "identificador de cuenta en el banco"
+
+#: src/pages/admin/AccountForm.tsx:365
+#, c-format
+msgid "name of the person owner the account"
+msgstr "nombre de la persona dueña de la cuenta"
+
+#: src/pages/admin/AccountForm.tsx:374
+#, c-format
+msgid "Internal IBAN"
+msgstr "IBAN interno"
+
+#: src/pages/admin/AccountForm.tsx:377
+#, c-format
+msgid "if empty a random account number will be assigned"
+msgstr "si está vacío un número de cuenta aleatorio será asignado"
+
+#: src/pages/admin/AccountForm.tsx:378
+#, c-format
+msgid "account identification for bank transfer"
+msgstr "identificador de cuenta para transferencia bancaria"
+
+#: src/pages/admin/AccountForm.tsx:423
+#, c-format
+msgid "Phone"
+msgstr "Teléfono"
+
+#: src/pages/admin/AccountForm.tsx:451
+#, c-format
+msgid "Cashout IBAN"
+msgstr "IBAN de egreso"
+
+#: src/pages/admin/AccountForm.tsx:452
+#, c-format
+msgid "account number where the money is going to be sent when doing cashouts"
+msgstr ""
+"numero de cuenta donde el dinero será enviado cuando se ejecuten egresos"
+
+#: src/pages/admin/AccountForm.tsx:470
+#, c-format
+msgid "Max debt"
+msgstr "Máxima deuda"
+
+#: src/pages/admin/AccountForm.tsx:494
+#, c-format
+msgid "how much is user able to transfer after zero balance"
+msgstr "cuanto tiene habilitado a transferir despues de un saldo en cero"
+
+#: src/pages/admin/AccountForm.tsx:508
+#, c-format
+msgid "Is this a Taler Exchange?"
+msgstr "Es un Taler Exchange?"
+
+#: src/pages/admin/AccountForm.tsx:549
+#, c-format
+msgid "This server doesn't support second factor authentication."
+msgstr "Este servidor no tiene soporte para segundo factor de autenticación."
+
+#: src/pages/admin/AccountForm.tsx:560
+#, c-format
+msgid "Enable second factor authentication"
+msgstr "Hábilitar segundo factor de autenticación"
+
+#: src/pages/admin/AccountForm.tsx:596
+#, c-format
+msgid "Using email"
+msgstr "Usando correo eletrónico"
+
+#: src/pages/admin/AccountForm.tsx:654
+#, c-format
+msgid "Using SMS"
+msgstr "Usando SMS"
+
+#: src/pages/admin/AccountForm.tsx:691
+#, c-format
+msgid "Is this account public?"
+msgstr "Es una cuenta pública?"
+
+#: src/pages/admin/AccountForm.tsx:719
+#, c-format
+msgid "public accounts have their balance publicly accessible"
+msgstr "las cuentas públicas tienen su saldo accesible al público"
+
+#: src/pages/account/ShowAccountDetails.tsx:100
+#, c-format
+msgid "Account updated"
+msgstr "Cuenta actualizada"
+
+#: src/pages/account/ShowAccountDetails.tsx:107
+#, c-format
+msgid "The rights to change the account are not sufficient"
+msgstr "Los permisos para cambiar la cuenta no son suficientes"
+
+#: src/pages/account/ShowAccountDetails.tsx:114
+#, c-format
+msgid "The username was not found"
+msgstr "El nombre de usaurio no se encontró"
+
+#: src/pages/account/ShowAccountDetails.tsx:121
+#, c-format
+msgid ""
+"You can't change the legal name, please contact the your account "
+"administrator."
+msgstr ""
+"No puede cambiar el nombre legal, por favor contacte el administrador de la "
+"cuenta."
+
+#: src/pages/account/ShowAccountDetails.tsx:128
+#, c-format
+msgid ""
+"You can't change the debt limit, please contact the your account "
+"administrator."
+msgstr ""
+"No puede cambiar el límite de deuda, por favor contacte el administrador de "
+"la cuenta."
+
+#: src/pages/account/ShowAccountDetails.tsx:135
+#, c-format
+msgid ""
+"You can't change the cashout address, please contact the your account "
+"administrator."
+msgstr ""
+"No puede cambiar la dirección de egreso, por favor contacte al administrador "
+"de la cuenta."
+
+#: src/pages/account/ShowAccountDetails.tsx:177
+#, c-format
+msgid "Account \"%1$s\""
+msgstr "Cuenta \"%1$s\""
+
+#: src/pages/account/ShowAccountDetails.tsx:190
+#, c-format
+msgid "Change details"
+msgstr "Cambiar detalles"
+
+#: src/pages/account/ShowAccountDetails.tsx:235
+#, c-format
+msgid "Update"
+msgstr "Actualizar"
+
+#: src/pages/account/UpdateAccountPassword.tsx:78
+#, c-format
+msgid "password doesn't match"
+msgstr "la contraseña no coincide"
+
+#: src/pages/account/UpdateAccountPassword.tsx:95
+#, c-format
+msgid "Password changed"
+msgstr "La contraseña cambió"
+
+#: src/pages/account/UpdateAccountPassword.tsx:102
+#, c-format
+msgid "Not authorized to change the password, maybe the session is invalid."
+msgstr "No está autorizado a cambiar el password, quizá la sesión es invalida."
+
+#: src/pages/account/UpdateAccountPassword.tsx:112
+#, c-format
+msgid ""
+"You need to provide the old password. If you don't have it contact your "
+"account administrator."
+msgstr ""
+"Se necesita el password viejo para cambiar la contraseña. Si no lo tiene "
+"contacte a su administrador."
+
+#: src/pages/account/UpdateAccountPassword.tsx:117
+#, c-format
+msgid "Your current password doesn't match, can't change to a new password."
+msgstr ""
+"Su actual contraseña no coincide, no puede cambiar a una nueva contraseña."
+
+#: src/pages/account/UpdateAccountPassword.tsx:149
+#, c-format
+msgid "Update password"
+msgstr "Actualizar contraseña"
+
+#: src/pages/account/UpdateAccountPassword.tsx:167
+#, c-format
+msgid "New password"
+msgstr "Nueva contraseña"
+
+#: src/pages/account/UpdateAccountPassword.tsx:195
+#, c-format
+msgid "Type it again"
+msgstr "Escribalo otra vez"
+
+#: src/pages/account/UpdateAccountPassword.tsx:217
+#, c-format
+msgid "repeat the same password"
+msgstr "repita la misma contraseña"
+
+#: src/pages/account/UpdateAccountPassword.tsx:227
+#, c-format
+msgid "Current password"
+msgstr "Contraseña actual"
+
+#: src/pages/account/UpdateAccountPassword.tsx:248
+#, c-format
+msgid "your current password, for security"
+msgstr "su actual contraseña, por seguridad"
+
+#: src/pages/account/UpdateAccountPassword.tsx:272
+#, c-format
+msgid "Change"
+msgstr "Cambiar"
+
+#: src/pages/admin/CreateNewAccount.tsx:74
+#, c-format
+msgid ""
+"Account created with password \"%1$s\". The user must change the password on "
+"the next login."
+msgstr ""
+"Cuenta creada con contraseña \"%1$s\". El usuario debe cambiar la contraseña "
+"en el siguiente ingreso."
+
+#: src/pages/admin/CreateNewAccount.tsx:83
+#, c-format
+msgid "Server replied that phone or email is invalid"
+msgstr "El servidor respondió que el teléfono o correo eletrónico es invalido"
+
+#: src/pages/admin/CreateNewAccount.tsx:90
+#, c-format
+msgid "The rights to perform the operation are not sufficient"
+msgstr "Los permisos para ejecutar la operación no son suficientes"
+
+#: src/pages/admin/CreateNewAccount.tsx:97
+#, c-format
+msgid "Account username is already taken"
+msgstr "El nombre del usuario ya está tomado"
+
+#: src/pages/admin/CreateNewAccount.tsx:104
+#, c-format
+msgid "Account id is already taken"
+msgstr "El id de cuenta ya está tomado"
+
+#: src/pages/admin/CreateNewAccount.tsx:111
+#, c-format
+msgid "Bank ran out of bonus credit."
+msgstr "El banco no tiene mas crédito de bonus."
+
+#: src/pages/admin/CreateNewAccount.tsx:118
+#, c-format
+msgid "Account username can't be used because is reserved"
+msgstr ""
+"El nombre de usuario de la cuenta no puede userse porque está reservado"
+
+#: src/pages/admin/CreateNewAccount.tsx:160
+#, c-format
+msgid "Can't create accounts"
+msgstr "No puede crear cuentas"
+
+#: src/pages/admin/CreateNewAccount.tsx:161
+#, c-format
+msgid "Only system admin can create accounts."
+msgstr "Solo los administradores del sistema pueden crear cuentas."
+
+#: src/pages/admin/CreateNewAccount.tsx:183
+#, c-format
+msgid "New business account"
+msgstr "Nueva cuenta"
+
+#: src/pages/admin/CreateNewAccount.tsx:209
+#, c-format
+msgid "Create"
+msgstr "Crear"
+
+#: src/pages/admin/RemoveAccount.tsx:94
+#, c-format
+msgid "Can't delete the account"
+msgstr "No se puede eliminar la cuenta"
+
+#: src/pages/admin/RemoveAccount.tsx:95
+#, c-format
+msgid ""
+"The account can't be delete while still holding some balance. First make "
+"sure that the owner make a complete cashout."
+msgstr ""
+"La cuenta no puede ser eliminada mientras tiene saldo. Primero aseguresé que "
+"el dueño haga un egreso completo."
+
+#: src/pages/admin/RemoveAccount.tsx:117
+#, c-format
+msgid "Account removed"
+msgstr "Cuenta eliminada"
+
+#: src/pages/admin/RemoveAccount.tsx:124
+#, c-format
+msgid "No enough permission to delete the account."
+msgstr "No tiene permisos suficientes para eliminar la cuenta."
+
+#: src/pages/admin/RemoveAccount.tsx:131
+#, c-format
+msgid "The username was not found."
+msgstr "El nombr ede usuario no se encontró."
+
+#: src/pages/admin/RemoveAccount.tsx:138
+#, c-format
+msgid "Can't delete a reserved username."
+msgstr "No se puede eliminar un nombre de usuario reservado."
+
+#: src/pages/admin/RemoveAccount.tsx:145
+#, c-format
+msgid "Can't delete an account with balance different than zero."
+msgstr "No se puede eliminar una cuenta con saldo diferente a cero."
+
+#: src/pages/admin/RemoveAccount.tsx:170
+#, c-format
+msgid "name doesn't match"
+msgstr "el nombre no coincide"
+
+#: src/pages/admin/RemoveAccount.tsx:180
+#, c-format
+msgid "You are going to remove the account"
+msgstr "Está por eliminar la cuenta"
+
+#: src/pages/admin/RemoveAccount.tsx:182
+#, c-format
+msgid "This step can't be undone."
+msgstr "Este paso no puede ser deshecho."
+
+#: src/pages/admin/RemoveAccount.tsx:188
+#, c-format
+msgid "Deleting account \"%1$s\""
+msgstr "Borrando cuenta \"%1$s\""
+
+#: src/pages/admin/RemoveAccount.tsx:206
+#, c-format
+msgid "Verification"
+msgstr "Verificación"
+
+#: src/pages/admin/RemoveAccount.tsx:231
+#, c-format
+msgid "enter the account name that is going to be deleted"
+msgstr "ingrese el nombre de cuenta que será eliminado"
+
+#: src/pages/business/ShowCashoutDetails.tsx:49
+#, c-format
+msgid "cashout id should be a number"
+msgstr "debería ser un correo electrónico"
+
+#: src/pages/business/ShowCashoutDetails.tsx:65
+#, c-format
+msgid "This cashout not found. Maybe already aborted."
+msgstr "Este egreso no se encontró. Quizá fue abortado."
+
+#: src/pages/business/ShowCashoutDetails.tsx:106
+#, c-format
+msgid "Cashout detail"
+msgstr "Detalles de egreso"
+
+#: src/pages/business/ShowCashoutDetails.tsx:139
+#, c-format
+msgid "Debited"
+msgstr "Debitado"
+
+#: src/pages/business/ShowCashoutDetails.tsx:154
+#, c-format
+msgid "Credited"
+msgstr "Acreditado"
+
+#: src/Routing.tsx:140
+#, c-format
+msgid "Welcome to %1$s!"
+msgstr "Bienvenido a %1$s!"
+
+#, c-format
+#~ msgid ""
+#~ "You can't change the contact data, please contact the your account "
+#~ "administrator."
+#~ msgstr ""
+#~ "No puede cambiar los datos de contacto, por favor contacte al "
+#~ "administrador de la cuenta."
+
+#, c-format
+#~ msgid "Account not found."
+#~ msgstr "Cuenta no encontrada."
+
+#, c-format
+#~ msgid "Confirmed"
+#~ msgstr "Confirmado"
+
+#, c-format
+#~ msgid "Status"
+#~ msgstr "Estado"
+
+#, c-format
+#~ msgid "never"
+#~ msgstr "nunca"
+
+#, c-format
+#~ msgid "Cashout was already confimed."
+#~ msgstr "Egreso ya fue confirmado."
+
+#, c-format
+#~ msgid "Cashout operation is not supported."
+#~ msgstr "Operación de egreso no soportada."
+
+#, c-format
+#~ msgid "The cashout operation is already aborted."
+#~ msgstr "La operación de egreso ya está abortada."
+
+#, c-format
+#~ msgid "Missing destination account."
+#~ msgstr "Falta cuenta destino."
+
+#, c-format
+#~ msgid "Too many failed attempts."
+#~ msgstr "Demasiados intentos fallidos."
+
+#, c-format
+#~ msgid "The code for this cashout is invalid."
+#~ msgstr "El código para este egreso es invalido."
+
+#, c-format
+#~ msgid "Abort"
+#~ msgstr "Abortar"
+
+#, fuzzy, c-format
+#~ msgid ""
+#~ "The Taler Exchange operator is selected but the Taler Exchange operator "
+#~ "account is missing or invalid."
+#~ msgstr ""
+#~ "El operador esta seleccionado pero la URI payto del operador falta o es "
+#~ "invalida."
+
+#, c-format
+#~ msgid "could not be parsed"
+#~ msgstr "inválido"
+
+#, c-format
+#~ msgid "Confirmation the operation using"
+#~ msgstr "Confirme la operación usando"
+
+#, c-format
+#~ msgid "Is public"
+#~ msgstr "es publica"
+
+#, c-format
+#~ msgid "Logout"
+#~ msgstr "Cierre de sesión"
+
+#, c-format
+#~ msgid "Skip to main content"
+#~ msgstr "Saltar el menú de navegación"
+
+#, c-format
+#~ msgid "Taler logo"
+#~ msgstr "Logo Taler"
+
+#, c-format
+#~ msgid "Please login!"
+#~ msgstr "Por favor inicia sesión!"
+
+#, c-format
+#~ msgid "Login"
+#~ msgstr "Iniciar sesión"
+
+#, c-format
+#~ msgid "Missing IBAN"
+#~ msgstr "Falta IBAN"
+
+#, c-format
+#~ msgid "Missing subject"
+#~ msgstr "Falta asunto"
+
+#, c-format
+#~ msgid "Receiver IBAN:"
+#~ msgstr "IBAN receptor:"
+
+#, c-format
+#~ msgid "Amount:"
+#~ msgstr "Monto:"
+
+#, c-format
+#~ msgid "Field(s) missing."
+#~ msgstr "Faltan campo(s)."
+
+#, c-format
+#~ msgid "Want to try the raw payto://-format?"
+#~ msgstr "Quieres probar el formato payto:// ?"
+
+#, c-format
+#~ msgid "Missing payto address"
+#~ msgstr "Falta direccion payto"
+
+#, c-format
+#~ msgid "Transfer money to account identified by payto:// URI:"
+#~ msgstr "Transferir dinero a la cuenta identificada por la URI payto://:"
+
+#, c-format
+#~ msgid "payto address"
+#~ msgstr "direccion payto"
+
+#, c-format
+#~ msgid "Could not create the wire transfer"
+#~ msgstr "No se pudo create la transferencia bancaria"
+
+#, c-format
+#~ msgid "Transfer creation gave response error"
+#~ msgstr "La creación de la transferencia dió una respuesta erronea"
+
+#, c-format
+#~ msgid "No credentials given."
+#~ msgstr "Se dieron las credenciales incorrectas."
+
+#, c-format
+#~ msgid "Withdrawal creation gave response error"
+#~ msgstr "La creación de retiro dió una respuesta errónea"
+
+#, c-format
+#~ msgid "Obtain digital cash"
+#~ msgstr "Obtener dinero digital"
+
+#, c-format
+#~ msgid "Click %1$s to open your Taler wallet!"
+#~ msgstr "Click %1$s para abrir una cartera Taler!"
+
+#, c-format
+#~ msgid "Authorize withdrawal by solving challenge"
+#~ msgstr "Autorizar retiro resolviendo una pregunta"
+
+#, c-format
+#~ msgid "What is"
+#~ msgstr "Cuanto es"
+
+#, c-format
+#~ msgid "Answer is wrong."
+#~ msgstr "La respuesta es incorrecta."
+
+#, c-format
+#~ msgid ""
+#~ "A this point, a %1$s bank would ask for an additional authentication "
+#~ "proof (PIN/TAN, one time password, ..), instead of a simple calculation."
+#~ msgstr ""
+#~ "En este punto, un banco %1$s preguntaría por una prueba adicional de "
+#~ "autenticación (PIN/TAN, password de un solo uso, ....), en vez de un "
+#~ "simple cálculo."
+
+#, c-format
+#~ msgid "Could not confirm the withdrawal"
+#~ msgstr "No se pudo confirmar la retirada"
+
+#, c-format
+#~ msgid "Withdrawal confirmation gave response error"
+#~ msgstr "La confirmación de retiro dió una respuesta errónea"
+
+#, c-format
+#~ msgid "Withdrawal aborted!"
+#~ msgstr "Este retiro fue cancelado!"
+
+#, c-format
+#~ msgid "withdrawal (%1$s) was never (correctly) created at the bank..."
+#~ msgstr "retiro (%1$s) nunca fue (correctamente) generado en el banco..."
+
+#, c-format
+#~ msgid "Username or account label '%1$s' not found. Won't login."
+#~ msgstr ""
+#~ "Nombre de usuario o etiqueta de cuenta '%1$s' no encontrada. No se "
+#~ "iniciará sesión."
+
+#, c-format
+#~ msgid "Account information could not be retrieved."
+#~ msgstr "La información de la cuenta no pudo ser accedida."
+
+#, c-format
+#~ msgid "Bank account balance"
+#~ msgstr "Balance de cuenta bancaria"
+
+#, c-format
+#~ msgid "Payments"
+#~ msgstr "Pagos"
+
+#, c-format
+#~ msgid "List of public accounts could not be retrieved."
+#~ msgstr "La lista de cuentas públicas no pudo ser accedida."
+
+#, c-format
+#~ msgid "Please register!"
+#~ msgstr "Por favor, registrese!"
+
+#, c-format
+#~ msgid "New registration gave response error"
+#~ msgstr "Nuevo registro dió una respuesta errónea"
+
+#, c-format
+#~ msgid "Bank menu"
+#~ msgstr "Menu del banco"
+
+#, c-format
+#~ msgid "Select option2"
+#~ msgstr "Seleccione opción 2"
+
+#, c-format
+#~ msgid "days"
+#~ msgstr "días"
+
+#, c-format
+#~ msgid "hours"
+#~ msgstr "horas"
+
+#, c-format
+#~ msgid "minutes"
+#~ msgstr "minutos"
+
+#, c-format
+#~ msgid "seconds"
+#~ msgstr "segundos"
+
+#~ msgid "this link"
+#~ msgstr "este link"
diff --git a/packages/bank-ui/src/i18n/fr.po b/packages/bank-ui/src/i18n/fr.po
new file mode 100644
index 000000000..b02cbe618
--- /dev/null
+++ b/packages/bank-ui/src/i18n/fr.po
@@ -0,0 +1,1752 @@
+# 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/>
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Bank\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"PO-Revision-Date: 2024-02-28 08:07+0000\n"
+"Last-Translator: d0p1 <contact@d0p1.eu>\n"
+"Language-Team: French <https://weblate.taler.net/projects/gnu-taler/"
+"taler-bank-spa/fr/>\n"
+"Language: fr\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n > 1;\n"
+"X-Generator: Weblate 5.2.1\n"
+
+#: src/utils.ts:137
+#, c-format
+msgid "Operation failed, please report"
+msgstr "L'opération a échoué, veuillez le signaler"
+
+#: src/utils.ts:156
+#, c-format
+msgid "Request timeout"
+msgstr ""
+
+#: src/utils.ts:165
+#, c-format
+msgid "Request throttled"
+msgstr ""
+
+#: src/utils.ts:174
+#, c-format
+msgid "Malformed response"
+msgstr ""
+
+#: src/utils.ts:183
+#, c-format
+msgid "Network error"
+msgstr "Erreur réseau"
+
+#: src/utils.ts:192
+#, c-format
+msgid "Unexpected request error"
+msgstr ""
+
+#: src/utils.ts:201
+#, c-format
+msgid "Unexpected error"
+msgstr ""
+
+#: src/utils.ts:377
+#, c-format
+msgid "IBAN numbers usually have more that 4 digits"
+msgstr ""
+
+#: src/utils.ts:379
+#, c-format
+msgid "IBAN numbers usually have less that 34 digits"
+msgstr ""
+
+#: src/utils.ts:387
+#, c-format
+msgid "IBAN country code not found"
+msgstr ""
+
+#: src/utils.ts:401
+#, c-format
+msgid "IBAN number is not valid, checksum is wrong"
+msgstr ""
+
+#: src/context/config.ts:136
+#, c-format
+msgid ""
+"the bank backend is not supported. supported version \"%1$s\", server "
+"version \"%2$s\""
+msgstr ""
+
+#: src/hooks/preferences.ts:55
+#, c-format
+msgid "Max withdrawal amount"
+msgstr ""
+
+#: src/hooks/preferences.ts:57
+#, c-format
+msgid "Show withdrawal confirmation"
+msgstr ""
+
+#: src/hooks/preferences.ts:59
+#, c-format
+msgid "Show demo description"
+msgstr ""
+
+#: src/hooks/preferences.ts:61
+#, c-format
+msgid "Show install wallet first"
+msgstr ""
+
+#: src/hooks/preferences.ts:63
+#, c-format
+msgid "Use fast withdrawal form"
+msgstr ""
+
+#: src/hooks/preferences.ts:65
+#, c-format
+msgid "Show debug info"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:90
+#, c-format
+msgid "required"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:92
+#, c-format
+msgid "IBAN should have just uppercased letters and numbers"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:98
+#, c-format
+msgid "not valid"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:100
+#, c-format
+msgid "should be greater than 0"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:102
+#, c-format
+msgid "balance is not enough"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:112
+#, c-format
+msgid "does not follow the pattern"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:114
+#, c-format
+msgid "only \"IBAN\" target are supported"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:116
+#, c-format
+msgid "use the \"amount\" parameter to specify the amount to be transferred"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:118
+#, c-format
+msgid "the amount is not valid"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:120
+#, c-format
+msgid ""
+"use the \"message\" parameter to specify a reference text for the transfer"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:160
+#, c-format
+msgid "The request was invalid or the payto://-URI used unacceptable features."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:167
+#, c-format
+msgid "Not enough permission to complete the operation."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:174
+#, c-format
+msgid "The destination account \"%1$s\" was not found."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:181
+#, c-format
+msgid "The origin and the destination of the transfer can't be the same."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:188
+#, c-format
+msgid "Your balance is not enough."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:195
+#, c-format
+msgid "The origin account \"%1$s\" was not found."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:212
+#, c-format
+msgid "Wire transfer created!"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:270
+#, c-format
+msgid "Using a form"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:310
+#, c-format
+msgid "Import payto:// URI"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:335
+#, c-format
+msgid "Recipient"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:359
+#, c-format
+msgid "IBAN of the recipient's account"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:369
+#, c-format
+msgid "Transfer subject"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:377
+#, c-format
+msgid "subject"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:390
+#, c-format
+msgid "some text to identify the transfer"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:400
+#, c-format
+msgid "Amount"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:415
+#, c-format
+msgid "amount to transfer"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:425
+#, c-format
+msgid "payto URI:"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:436
+#, c-format
+msgid "uniform resource identifier of the target account"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:437
+#, c-format
+msgid "payto://iban/[receiver-iban]?message=[subject]&amount=[%1$s:X.Y]"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:457
+#, c-format
+msgid "Cancel"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:471
+#, c-format
+msgid "Send"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:71
+#, c-format
+msgid "Missing username"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:75
+#, c-format
+msgid "Missing password"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:104
+#, c-format
+msgid "Wrong credentials for \"%1$s\""
+msgstr ""
+
+#: src/pages/LoginForm.tsx:111
+#, c-format
+msgid "Account not found"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:142
+#, c-format
+msgid "Username"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:156
+#, c-format
+msgid "username of the account"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:175
+#, c-format
+msgid "Password"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:188
+#, c-format
+msgid "password of the account"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:223
+#, c-format
+msgid "Check"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:237
+#, c-format
+msgid "Log in"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:249
+#, c-format
+msgid "Register"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:52
+#, c-format
+msgid "Latest transactions"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:63
+#, c-format
+msgid "Date"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:71
+#, c-format
+msgid "Counterpart"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:75
+#, c-format
+msgid "Subject"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:111
+#, c-format
+msgid "sent"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:112
+#, c-format
+msgid "received"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:127
+#, c-format
+msgid "invalid value"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:136
+#, c-format
+msgid "to"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:136
+#, c-format
+msgid "from"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:202
+#, c-format
+msgid "First page"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:209
+#, c-format
+msgid "Next"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:86
+#, c-format
+msgid "Wire transfer completed!"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:93
+#, c-format
+msgid "The withdrawal has been aborted previously and can't be confirmed"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:100
+#, c-format
+msgid ""
+"The withdrawal operation can't be confirmed before a wallet accepted the "
+"transaction."
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:107
+#, c-format
+msgid "The operation id is invalid."
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:114
+#, c-format
+msgid "The operation was not found."
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:121
+#, c-format
+msgid "Your balance is not enough for the operation."
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:155
+#, c-format
+msgid ""
+"The reserve operation has been confirmed previously and can't be aborted"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:186
+#, c-format
+msgid "Confirm the withdrawal operation"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:203
+#, c-format
+msgid "Wire transfer details"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:217
+#, c-format
+msgid "Taler Exchange operator's account"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:228
+#, c-format
+msgid "Taler Exchange operator's name"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:317
+#, c-format
+msgid "Transfer"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:342
+#, c-format
+msgid "Authentication required"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:352
+#, c-format
+msgid "This operation was created with other username"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:209
+#, c-format
+msgid ""
+"Unauthorized to make the operation, maybe the session has expired or the "
+"password changed."
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:218
+#, c-format
+msgid "The operation was rejected due to insufficient funds."
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:268
+#, c-format
+msgid "Withdrawal confirmed"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:272
+#, c-format
+msgid ""
+"The wire transfer to the Taler operator has been initiated. You will soon "
+"receive the requested amount in your Taler wallet."
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:287
+#, c-format
+msgid "Do not show this again"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:319
+#, c-format
+msgid "Close"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:399
+#, c-format
+msgid "On this device"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:404
+#, c-format
+msgid ""
+"If you are using a web browser on desktop you should access your wallet with "
+"the GNU Taler WebExtension now or click the link if your WebExtension have "
+"the \"Inject Taler support\" option enabled."
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:417
+#, c-format
+msgid "Start"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:426
+#, c-format
+msgid "On a mobile phone"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:431
+#, c-format
+msgid "Scan the QR code with your mobile device."
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:73
+#, c-format
+msgid "There is an operation already"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:75
+#, c-format
+msgid "Complete or cancel the operation in"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:84
+#, c-format
+msgid "this page"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:101
+#, c-format
+msgid "invalid"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:116
+#, c-format
+msgid "Server responded with an invalid withdraw URI"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:117
+#, c-format
+msgid "Withdraw URI: %1$s"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:132
+#, c-format
+msgid "The operation was rejected due to insufficient funds"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:253
+#, c-format
+msgid "Continue"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:282
+#, c-format
+msgid "Prepare your wallet"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:285
+#, c-format
+msgid ""
+"After using your wallet you will need to confirm or cancel the operation on "
+"this site."
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:295
+#, c-format
+msgid "You need a GNU Taler Wallet"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:300
+#, c-format
+msgid "If you don't have one yet you can follow the instruction in"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:55
+#, c-format
+msgid "Send money"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:73
+#, c-format
+msgid "to a %1$s wallet"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:95
+#, c-format
+msgid "Withdraw digital money into your mobile wallet or browser extension"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:109
+#, c-format
+msgid "operation ready"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:129
+#, c-format
+msgid "to another bank account"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:149
+#, c-format
+msgid "Make a wire transfer to an account with known bank account number."
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:171
+#, c-format
+msgid "Transfer details"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:41
+#, c-format
+msgid "This is a demo bank"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:46
+#, c-format
+msgid ""
+"This part of the demo shows how a bank that supports Taler directly would "
+"work. In addition to using your own bank account, you can also see the "
+"transaction history of some %1$s."
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:53
+#, c-format
+msgid ""
+"This part of the demo shows how a bank that supports Taler directly would "
+"work."
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:70
+#, c-format
+msgid "Pending account delete operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:72
+#, c-format
+msgid "Pending account update operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:74
+#, c-format
+msgid "Pending password update operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:76
+#, c-format
+msgid "Pending transaction operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:78
+#, c-format
+msgid "Pending withdrawal operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:80
+#, c-format
+msgid "Pending cashout operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:91
+#, c-format
+msgid "You can complete or cancel the operation in"
+msgstr ""
+
+#: src/pages/BankFrame.tsx:64
+#, c-format
+msgid "Internal error, please report."
+msgstr ""
+
+#: src/pages/BankFrame.tsx:100
+#, c-format
+msgid "Preferences"
+msgstr ""
+
+#: src/pages/BankFrame.tsx:184
+#, c-format
+msgid "Welcome, %1$s"
+msgstr ""
+
+#: src/pages/WireTransfer.tsx:79
+#, c-format
+msgid "Make a wire transfer"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:72
+#, c-format
+msgid "Accounts"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:75
+#, c-format
+msgid "A list of all business account in the bank."
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:86
+#, c-format
+msgid "Create account"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:106
+#, c-format
+msgid "Name"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:110
+#, c-format
+msgid "Balance"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:112
+#, c-format
+msgid "Actions"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:151
+#, c-format
+msgid "unknown"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:170
+#, c-format
+msgid "change password"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:179
+#, c-format
+msgid "cashouts"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:189
+#, c-format
+msgid "remove"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:168
+#, c-format
+msgid "Cashout not implemented"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:184
+#, c-format
+msgid "Select a section"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:202
+#, c-format
+msgid "Last hour"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:208
+#, c-format
+msgid "Last day"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:216
+#, c-format
+msgid "Last month"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:222
+#, c-format
+msgid "Last year"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:310
+#, c-format
+msgid "Last Year"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:325
+#, c-format
+msgid "Trading volume on %1$s compared to %2$s"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:342
+#, c-format
+msgid "Cashin"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:352
+#, c-format
+msgid "Cashout"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:364
+#, c-format
+msgid "Payin"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:374
+#, c-format
+msgid "Payout"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:388
+#, c-format
+msgid "download stats as CSV"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:494
+#, c-format
+msgid "Decreased by"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:498
+#, c-format
+msgid "Increased by"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:89
+#, c-format
+msgid "Download bank stats"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:110
+#, c-format
+msgid "Include hour metric"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:143
+#, c-format
+msgid "Include day metric"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:173
+#, c-format
+msgid "Include month metric"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:206
+#, c-format
+msgid "Include year metric"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:239
+#, c-format
+msgid "Include table header"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:272
+#, c-format
+msgid "Add previous metric for compare"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:307
+#, c-format
+msgid "Fail on first error"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:364
+#, c-format
+msgid "Download"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:381
+#, c-format
+msgid "downloading... %1$s"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:399
+#, c-format
+msgid "Download completed"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:400
+#, c-format
+msgid "click here to save the file in your computer"
+msgstr ""
+
+#: src/pages/PublicHistoriesPage.tsx:78
+#, c-format
+msgid "History of public accounts"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:48
+#, c-format
+msgid "Currently, the bank is not accepting new registrations!"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:87
+#, c-format
+msgid "Missing name"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:91
+#, c-format
+msgid "Use letters and numbers only, and start with a lowercase letter"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:107
+#, c-format
+msgid "Passwords don't match"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:130
+#, c-format
+msgid "Server replied with invalid phone or email."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:137
+#, c-format
+msgid "Registration is disabled because the bank ran out of bonus credit."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:144
+#, c-format
+msgid "No enough permission to create that account."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:151
+#, c-format
+msgid "That account id is already taken."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:158
+#, c-format
+msgid "That username is already taken."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:165
+#, c-format
+msgid "That username can't be used because is reserved."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:172
+#, c-format
+msgid "Only admin is allow to set debt limit."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:179
+#, c-format
+msgid "No information for the selected authentication channel."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:186
+#, c-format
+msgid "Authentication channel is not supported."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:193
+#, c-format
+msgid "Only admin can create accounts with second factor authentication."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:233
+#, c-format
+msgid "Account registration"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:315
+#, c-format
+msgid "Repeat password"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:457
+#, c-format
+msgid "Create a random temporary user"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:110
+#, c-format
+msgid "If you have a Taler wallet installed in this device"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:116
+#, c-format
+msgid ""
+"You will see the details of the operation in your wallet including the fees "
+"(if applies). If you still don't have one you can install it following "
+"instructions in"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:143
+#, c-format
+msgid "Withdraw"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:152
+#, c-format
+msgid "Or if you have the wallet in another device"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:157
+#, c-format
+msgid "Scan the QR below to start the withdrawal."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:79
+#, c-format
+msgid "Operation aborted"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:82
+#, c-format
+msgid ""
+"The wire transfer to the Taler Exchange operator's account was aborted, your "
+"balance was not affected."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:88
+#, c-format
+msgid "You can close this page now or continue to the account page."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:147
+#, c-format
+msgid "Done"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:158
+#, c-format
+msgid "Operation canceled"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:173
+#, c-format
+msgid ""
+"The operation is marked as 'selected' but some step in the withdrawal failed"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:175
+#, c-format
+msgid "The account is selected but no withdrawal identification found."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:188
+#, c-format
+msgid ""
+"There is a withdrawal identification but no account has been selected or the "
+"selected account is invalid."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:202
+#, c-format
+msgid ""
+"No withdrawal ID found and no account has been selected or the selected "
+"account is invalid."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:259
+#, c-format
+msgid "Operation not found"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:263
+#, c-format
+msgid ""
+"This operation is not known by the server. The operation id is wrong or the "
+"server deleted the operation information before reaching here."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:278
+#, c-format
+msgid "Cotinue to dashboard"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:98
+#, c-format
+msgid "Cashout not found. It may be also mean that it was already aborted."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:136
+#, c-format
+msgid "Challenge not found."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:143
+#, c-format
+msgid "This user is not authorized to complete this challenge."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:150
+#, c-format
+msgid "Too many attempts, try another code."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:157
+#, c-format
+msgid "The confirmation code is wrong, try again."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:164
+#, c-format
+msgid "The operation expired."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:197
+#, c-format
+msgid "The operation failed."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:212
+#, c-format
+msgid "The operation needs another confirmation to complete."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:224
+#, c-format
+msgid "Account delete"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:226
+#, c-format
+msgid "Account update"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:228
+#, c-format
+msgid "Password update"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:230
+#, c-format
+msgid "Wire transfer"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:232
+#, c-format
+msgid "Withdrawal"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:248
+#, c-format
+msgid "Confirm the operation"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:271
+#, c-format
+msgid "Enter the confirmation code"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:313
+#, c-format
+msgid "Confirm"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:348
+#, c-format
+msgid "Send again"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:359
+#, c-format
+msgid "Send code"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:369
+#, c-format
+msgid "Operation details"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:529
+#, c-format
+msgid "Challenge details"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:536
+#, c-format
+msgid "Sent at"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:551
+#, c-format
+msgid "To phone"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:553
+#, c-format
+msgid "To email"
+msgstr ""
+
+#: src/pages/WithdrawalOperationPage.tsx:49
+#, c-format
+msgid "The Withdrawal URI is not valid"
+msgstr ""
+
+#: src/components/Cashouts/views.tsx:100
+#, c-format
+msgid "Latest cashouts"
+msgstr ""
+
+#: src/components/Cashouts/views.tsx:111
+#, c-format
+msgid "Created"
+msgstr ""
+
+#: src/components/Cashouts/views.tsx:115
+#, c-format
+msgid "Total debit"
+msgstr ""
+
+#: src/components/Cashouts/views.tsx:119
+#, c-format
+msgid "Total credit"
+msgstr ""
+
+#: src/pages/ProfileNavigation.tsx:70
+#, c-format
+msgid "Details"
+msgstr ""
+
+#: src/pages/ProfileNavigation.tsx:74
+#, c-format
+msgid "Delete"
+msgstr ""
+
+#: src/pages/ProfileNavigation.tsx:78
+#, c-format
+msgid "Credentials"
+msgstr ""
+
+#: src/pages/ProfileNavigation.tsx:82
+#, c-format
+msgid "Cashouts"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:95
+#, c-format
+msgid "Unable to create a cashout"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:96
+#, c-format
+msgid "The bank configuration does not support cashout operations."
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:223
+#, c-format
+msgid "need to be higher due to fees"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:225
+#, c-format
+msgid "the total transfer at destination will be zero"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:250
+#, c-format
+msgid "Cashout created"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:272
+#, c-format
+msgid ""
+"Duplicated request detected, check if the operation succeeded or try again."
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:279
+#, c-format
+msgid "The conversion rate was incorrectly applied"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:286
+#, c-format
+msgid "The account does not have sufficient funds"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:293
+#, c-format
+msgid "Cashouts are not supported"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:300
+#, c-format
+msgid "Missing cashout URI in the profile"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:307
+#, c-format
+msgid ""
+"Sending the confirmation message failed, retry later or contact the "
+"administrator."
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:339
+#, c-format
+msgid "Conversion rate"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:360
+#, c-format
+msgid "Fee"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:374
+#, c-format
+msgid "To account"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:381
+#, c-format
+msgid "No cashout account"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:382
+#, c-format
+msgid "Before doing a cashout you need to complete your profile"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:440
+#, c-format
+msgid "Amount to send"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:441
+#, c-format
+msgid "Amount to receive"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:490
+#, c-format
+msgid "Total cost"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:505
+#, c-format
+msgid "Balance left"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:520
+#, c-format
+msgid "Before fee"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:533
+#, c-format
+msgid "Total cashout transfer"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:553
+#, c-format
+msgid "No cashout channel available"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:555
+#, c-format
+msgid ""
+"Before doing a cashout the server need to provide an second channel to "
+"confirm the operation"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:567
+#, c-format
+msgid "Second factor authentication"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:598
+#, c-format
+msgid "Email"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:600
+#, c-format
+msgid "add a email in your profile to enable this option"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:646
+#, c-format
+msgid "SMS"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:648
+#, c-format
+msgid "add a phone number in your profile to enable this option"
+msgstr ""
+
+#: src/pages/account/CashoutListForAccount.tsx:52
+#, c-format
+msgid "Cashout for account %1$s"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:165
+#, c-format
+msgid "it doesn't have the pattern of an IBAN number"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:185
+#, c-format
+msgid "it doesn't have the pattern of an email"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:190
+#, c-format
+msgid "should start with +"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:192
+#, c-format
+msgid "phone number can't have other than numbers"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:329
+#, c-format
+msgid "account identification in the bank"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:365
+#, c-format
+msgid "name of the person owner the account"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:374
+#, c-format
+msgid "Internal IBAN"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:377
+#, c-format
+msgid "if empty a random account number will be assigned"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:378
+#, c-format
+msgid "account identification for bank transfer"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:423
+#, c-format
+msgid "Phone"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:451
+#, c-format
+msgid "Cashout IBAN"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:452
+#, c-format
+msgid "account number where the money is going to be sent when doing cashouts"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:470
+#, c-format
+msgid "Max debt"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:494
+#, c-format
+msgid "how much is user able to transfer after zero balance"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:508
+#, c-format
+msgid "Is this a Taler Exchange?"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:549
+#, c-format
+msgid "This server doesn't support second factor authentication."
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:560
+#, c-format
+msgid "Enable second factor authentication"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:596
+#, c-format
+msgid "Using email"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:654
+#, c-format
+msgid "Using SMS"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:691
+#, c-format
+msgid "Is this account public?"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:719
+#, c-format
+msgid "public accounts have their balance publicly accessible"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:100
+#, c-format
+msgid "Account updated"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:107
+#, c-format
+msgid "The rights to change the account are not sufficient"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:114
+#, c-format
+msgid "The username was not found"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:121
+#, c-format
+msgid ""
+"You can't change the legal name, please contact the your account "
+"administrator."
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:128
+#, c-format
+msgid ""
+"You can't change the debt limit, please contact the your account "
+"administrator."
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:135
+#, c-format
+msgid ""
+"You can't change the cashout address, please contact the your account "
+"administrator."
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:177
+#, c-format
+msgid "Account \"%1$s\""
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:190
+#, c-format
+msgid "Change details"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:235
+#, c-format
+msgid "Update"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:78
+#, c-format
+msgid "password doesn't match"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:95
+#, c-format
+msgid "Password changed"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:102
+#, c-format
+msgid "Not authorized to change the password, maybe the session is invalid."
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:112
+#, c-format
+msgid ""
+"You need to provide the old password. If you don't have it contact your "
+"account administrator."
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:117
+#, c-format
+msgid "Your current password doesn't match, can't change to a new password."
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:149
+#, c-format
+msgid "Update password"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:167
+#, c-format
+msgid "New password"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:195
+#, c-format
+msgid "Type it again"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:217
+#, c-format
+msgid "repeat the same password"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:227
+#, c-format
+msgid "Current password"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:248
+#, c-format
+msgid "your current password, for security"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:272
+#, c-format
+msgid "Change"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:74
+#, c-format
+msgid ""
+"Account created with password \"%1$s\". The user must change the password on "
+"the next login."
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:83
+#, c-format
+msgid "Server replied that phone or email is invalid"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:90
+#, c-format
+msgid "The rights to perform the operation are not sufficient"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:97
+#, c-format
+msgid "Account username is already taken"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:104
+#, c-format
+msgid "Account id is already taken"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:111
+#, c-format
+msgid "Bank ran out of bonus credit."
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:118
+#, c-format
+msgid "Account username can't be used because is reserved"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:160
+#, c-format
+msgid "Can't create accounts"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:161
+#, c-format
+msgid "Only system admin can create accounts."
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:183
+#, c-format
+msgid "New business account"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:209
+#, c-format
+msgid "Create"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:94
+#, c-format
+msgid "Can't delete the account"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:95
+#, c-format
+msgid ""
+"The account can't be delete while still holding some balance. First make "
+"sure that the owner make a complete cashout."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:117
+#, c-format
+msgid "Account removed"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:124
+#, c-format
+msgid "No enough permission to delete the account."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:131
+#, c-format
+msgid "The username was not found."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:138
+#, c-format
+msgid "Can't delete a reserved username."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:145
+#, c-format
+msgid "Can't delete an account with balance different than zero."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:170
+#, c-format
+msgid "name doesn't match"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:180
+#, c-format
+msgid "You are going to remove the account"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:182
+#, c-format
+msgid "This step can't be undone."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:188
+#, c-format
+msgid "Deleting account \"%1$s\""
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:206
+#, c-format
+msgid "Verification"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:231
+#, c-format
+msgid "enter the account name that is going to be deleted"
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:49
+#, c-format
+msgid "cashout id should be a number"
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:65
+#, c-format
+msgid "This cashout not found. Maybe already aborted."
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:106
+#, c-format
+msgid "Cashout detail"
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:139
+#, c-format
+msgid "Debited"
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:154
+#, c-format
+msgid "Credited"
+msgstr ""
+
+#: src/Routing.tsx:140
+#, c-format
+msgid "Welcome to %1$s!"
+msgstr ""
diff --git a/packages/bank-ui/src/i18n/it.po b/packages/bank-ui/src/i18n/it.po
new file mode 100644
index 000000000..7aeaca3a8
--- /dev/null
+++ b/packages/bank-ui/src/i18n/it.po
@@ -0,0 +1,1843 @@
+# 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/>
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Wallet\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"POT-Creation-Date: 2016-11-23 00:00+0100\n"
+"PO-Revision-Date: 2023-08-15 07:28+0000\n"
+"Last-Translator: Krystian Baran <kiszkot@murena.io>\n"
+"Language-Team: Italian <https://weblate.taler.net/projects/gnu-taler/taler-"
+"bank-spa/it/>\n"
+"Language: it\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 4.13.1\n"
+
+#: src/utils.ts:137
+#, fuzzy, c-format
+msgid "Operation failed, please report"
+msgstr "Registrazione"
+
+#: src/utils.ts:156
+#, c-format
+msgid "Request timeout"
+msgstr ""
+
+#: src/utils.ts:165
+#, c-format
+msgid "Request throttled"
+msgstr ""
+
+#: src/utils.ts:174
+#, c-format
+msgid "Malformed response"
+msgstr ""
+
+#: src/utils.ts:183
+#, c-format
+msgid "Network error"
+msgstr ""
+
+#: src/utils.ts:192
+#, c-format
+msgid "Unexpected request error"
+msgstr ""
+
+#: src/utils.ts:201
+#, c-format
+msgid "Unexpected error"
+msgstr ""
+
+#: src/utils.ts:377
+#, c-format
+msgid "IBAN numbers usually have more that 4 digits"
+msgstr ""
+
+#: src/utils.ts:379
+#, c-format
+msgid "IBAN numbers usually have less that 34 digits"
+msgstr ""
+
+#: src/utils.ts:387
+#, c-format
+msgid "IBAN country code not found"
+msgstr ""
+
+#: src/utils.ts:401
+#, c-format
+msgid "IBAN number is not valid, checksum is wrong"
+msgstr ""
+
+#: src/context/config.ts:136
+#, c-format
+msgid ""
+"the bank backend is not supported. supported version \"%1$s\", server "
+"version \"%2$s\""
+msgstr ""
+
+#: src/hooks/preferences.ts:55
+#, fuzzy, c-format
+msgid "Max withdrawal amount"
+msgstr "Questo ritiro è stato annullato!"
+
+#: src/hooks/preferences.ts:57
+#, fuzzy, c-format
+msgid "Show withdrawal confirmation"
+msgstr "Questo ritiro è stato annullato!"
+
+#: src/hooks/preferences.ts:59
+#, c-format
+msgid "Show demo description"
+msgstr ""
+
+#: src/hooks/preferences.ts:61
+#, c-format
+msgid "Show install wallet first"
+msgstr ""
+
+#: src/hooks/preferences.ts:63
+#, fuzzy, c-format
+msgid "Use fast withdrawal form"
+msgstr "Ritira contante"
+
+#: src/hooks/preferences.ts:65
+#, c-format
+msgid "Show debug info"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:90
+#, c-format
+msgid "required"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:92
+#, c-format
+msgid "IBAN should have just uppercased letters and numbers"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:98
+#, c-format
+msgid "not valid"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:100
+#, c-format
+msgid "should be greater than 0"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:102
+#, c-format
+msgid "balance is not enough"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:112
+#, c-format
+msgid "does not follow the pattern"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:114
+#, c-format
+msgid "only \"IBAN\" target are supported"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:116
+#, c-format
+msgid "use the \"amount\" parameter to specify the amount to be transferred"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:118
+#, c-format
+msgid "the amount is not valid"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:120
+#, c-format
+msgid ""
+"use the \"message\" parameter to specify a reference text for the transfer"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:160
+#, c-format
+msgid "The request was invalid or the payto://-URI used unacceptable features."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:167
+#, fuzzy, c-format
+msgid "Not enough permission to complete the operation."
+msgstr "La banca sta creando l'operazione..."
+
+#: src/pages/PaytoWireTransferForm.tsx:174
+#, fuzzy, c-format
+msgid "The destination account \"%1$s\" was not found."
+msgstr "Lista conti pubblici non trovata."
+
+#: src/pages/PaytoWireTransferForm.tsx:181
+#, c-format
+msgid "The origin and the destination of the transfer can't be the same."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:188
+#, c-format
+msgid "Your balance is not enough."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:195
+#, fuzzy, c-format
+msgid "The origin account \"%1$s\" was not found."
+msgstr "Lista conti pubblici non trovata."
+
+#: src/pages/PaytoWireTransferForm.tsx:212
+#, fuzzy, c-format
+msgid "Wire transfer created!"
+msgstr "Bonifico"
+
+#: src/pages/PaytoWireTransferForm.tsx:270
+#, c-format
+msgid "Using a form"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:310
+#, c-format
+msgid "Import payto:// URI"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:335
+#, c-format
+msgid "Recipient"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:359
+#, c-format
+msgid "IBAN of the recipient's account"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:369
+#, fuzzy, c-format
+msgid "Transfer subject"
+msgstr "Trasferisci fondi a un altro conto di questa banca:"
+
+#: src/pages/PaytoWireTransferForm.tsx:377
+#, fuzzy, c-format
+msgid "subject"
+msgstr "Soggetto"
+
+#: src/pages/PaytoWireTransferForm.tsx:390
+#, c-format
+msgid "some text to identify the transfer"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:400
+#, c-format
+msgid "Amount"
+msgstr "Importo"
+
+#: src/pages/PaytoWireTransferForm.tsx:415
+#, fuzzy, c-format
+msgid "amount to transfer"
+msgstr "Somma da ritirare"
+
+#: src/pages/PaytoWireTransferForm.tsx:425
+#, c-format
+msgid "payto URI:"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:436
+#, c-format
+msgid "uniform resource identifier of the target account"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:437
+#, c-format
+msgid "payto://iban/[receiver-iban]?message=[subject]&amount=[%1$s:X.Y]"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:457
+#, c-format
+msgid "Cancel"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:471
+#, c-format
+msgid "Send"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:71
+#, c-format
+msgid "Missing username"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:75
+#, c-format
+msgid "Missing password"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:104
+#, fuzzy, c-format
+msgid "Wrong credentials for \"%1$s\""
+msgstr "Credenziali invalide."
+
+#: src/pages/LoginForm.tsx:111
+#, c-format
+msgid "Account not found"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:142
+#, c-format
+msgid "Username"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:156
+#, fuzzy, c-format
+msgid "username of the account"
+msgstr "Trasferisci fondi a un altro conto di questa banca:"
+
+#: src/pages/LoginForm.tsx:175
+#, c-format
+msgid "Password"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:188
+#, fuzzy, c-format
+msgid "password of the account"
+msgstr "Storico dei conti pubblici"
+
+#: src/pages/LoginForm.tsx:223
+#, c-format
+msgid "Check"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:237
+#, c-format
+msgid "Log in"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:249
+#, c-format
+msgid "Register"
+msgstr "Registrati"
+
+#: src/components/Transactions/views.tsx:52
+#, fuzzy, c-format
+msgid "Latest transactions"
+msgstr "Ultime transazioni:"
+
+#: src/components/Transactions/views.tsx:63
+#, c-format
+msgid "Date"
+msgstr "Data"
+
+#: src/components/Transactions/views.tsx:71
+#, c-format
+msgid "Counterpart"
+msgstr "Controparte"
+
+#: src/components/Transactions/views.tsx:75
+#, c-format
+msgid "Subject"
+msgstr "Soggetto"
+
+#: src/components/Transactions/views.tsx:111
+#, c-format
+msgid "sent"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:112
+#, c-format
+msgid "received"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:127
+#, c-format
+msgid "invalid value"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:136
+#, c-format
+msgid "to"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:136
+#, c-format
+msgid "from"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:202
+#, c-format
+msgid "First page"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:209
+#, c-format
+msgid "Next"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:86
+#, fuzzy, c-format
+msgid "Wire transfer completed!"
+msgstr "Bonifico"
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:93
+#, c-format
+msgid "The withdrawal has been aborted previously and can't be confirmed"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:100
+#, c-format
+msgid ""
+"The withdrawal operation can't be confirmed before a wallet accepted the "
+"transaction."
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:107
+#, c-format
+msgid "The operation id is invalid."
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:114
+#, fuzzy, c-format
+msgid "The operation was not found."
+msgstr "Lista conti pubblici non trovata."
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:121
+#, c-format
+msgid "Your balance is not enough for the operation."
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:155
+#, c-format
+msgid ""
+"The reserve operation has been confirmed previously and can't be aborted"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:186
+#, fuzzy, c-format
+msgid "Confirm the withdrawal operation"
+msgstr "Conferma il ritiro"
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:203
+#, fuzzy, c-format
+msgid "Wire transfer details"
+msgstr "Bonifico"
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:217
+#, c-format
+msgid "Taler Exchange operator's account"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:228
+#, c-format
+msgid "Taler Exchange operator's name"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:317
+#, c-format
+msgid "Transfer"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:342
+#, c-format
+msgid "Authentication required"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:352
+#, c-format
+msgid "This operation was created with other username"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:209
+#, c-format
+msgid ""
+"Unauthorized to make the operation, maybe the session has expired or the "
+"password changed."
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:218
+#, c-format
+msgid "The operation was rejected due to insufficient funds."
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:268
+#, fuzzy, c-format
+msgid "Withdrawal confirmed"
+msgstr "Questo ritiro è stato annullato!"
+
+#: src/pages/OperationState/views.tsx:272
+#, c-format
+msgid ""
+"The wire transfer to the Taler operator has been initiated. You will soon "
+"receive the requested amount in your Taler wallet."
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:287
+#, c-format
+msgid "Do not show this again"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:319
+#, c-format
+msgid "Close"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:399
+#, c-format
+msgid "On this device"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:404
+#, c-format
+msgid ""
+"If you are using a web browser on desktop you should access your wallet with "
+"the GNU Taler WebExtension now or click the link if your WebExtension have "
+"the \"Inject Taler support\" option enabled."
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:417
+#, c-format
+msgid "Start"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:426
+#, c-format
+msgid "On a mobile phone"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:431
+#, fuzzy, c-format
+msgid "Scan the QR code with your mobile device."
+msgstr "Usa questo codice QR per ritirare contante nel tuo wallet:"
+
+#: src/pages/WalletWithdrawForm.tsx:73
+#, c-format
+msgid "There is an operation already"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:75
+#, fuzzy, c-format
+msgid "Complete or cancel the operation in"
+msgstr "Conferma il ritiro"
+
+#: src/pages/WalletWithdrawForm.tsx:84
+#, c-format
+msgid "this page"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:101
+#, c-format
+msgid "invalid"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:116
+#, c-format
+msgid "Server responded with an invalid withdraw URI"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:117
+#, fuzzy, c-format
+msgid "Withdraw URI: %1$s"
+msgstr "Prelevare"
+
+#: src/pages/WalletWithdrawForm.tsx:132
+#, c-format
+msgid "The operation was rejected due to insufficient funds"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:253
+#, c-format
+msgid "Continue"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:282
+#, c-format
+msgid "Prepare your wallet"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:285
+#, c-format
+msgid ""
+"After using your wallet you will need to confirm or cancel the operation on "
+"this site."
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:295
+#, fuzzy, c-format
+msgid "You need a GNU Taler Wallet"
+msgstr "Ritira contante nel portafoglio Taler"
+
+#: src/pages/WalletWithdrawForm.tsx:300
+#, c-format
+msgid "If you don't have one yet you can follow the instruction in"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:55
+#, c-format
+msgid "Send money"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:73
+#, c-format
+msgid "to a %1$s wallet"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:95
+#, c-format
+msgid "Withdraw digital money into your mobile wallet or browser extension"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:109
+#, c-format
+msgid "operation ready"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:129
+#, fuzzy, c-format
+msgid "to another bank account"
+msgstr "Trasferisci fondi a un altro conto di questa banca:"
+
+#: src/pages/PaymentOptions.tsx:149
+#, c-format
+msgid "Make a wire transfer to an account with known bank account number."
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:171
+#, fuzzy, c-format
+msgid "Transfer details"
+msgstr "Effettua un bonifico"
+
+#: src/pages/AccountPage/views.tsx:41
+#, c-format
+msgid "This is a demo bank"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:46
+#, c-format
+msgid ""
+"This part of the demo shows how a bank that supports Taler directly would "
+"work. In addition to using your own bank account, you can also see the "
+"transaction history of some %1$s."
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:53
+#, c-format
+msgid ""
+"This part of the demo shows how a bank that supports Taler directly would "
+"work."
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:70
+#, c-format
+msgid "Pending account delete operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:72
+#, c-format
+msgid "Pending account update operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:74
+#, c-format
+msgid "Pending password update operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:76
+#, c-format
+msgid "Pending transaction operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:78
+#, c-format
+msgid "Pending withdrawal operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:80
+#, c-format
+msgid "Pending cashout operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:91
+#, c-format
+msgid "You can complete or cancel the operation in"
+msgstr ""
+
+#: src/pages/BankFrame.tsx:64
+#, fuzzy, c-format
+msgid "Internal error, please report."
+msgstr "Registrazione"
+
+#: src/pages/BankFrame.tsx:100
+#, c-format
+msgid "Preferences"
+msgstr ""
+
+#: src/pages/BankFrame.tsx:184
+#, c-format
+msgid "Welcome, %1$s"
+msgstr ""
+
+#: src/pages/WireTransfer.tsx:79
+#, fuzzy, c-format
+msgid "Make a wire transfer"
+msgstr "Chiudi il bonifico"
+
+#: src/pages/admin/AccountList.tsx:72
+#, fuzzy, c-format
+msgid "Accounts"
+msgstr "Importo"
+
+#: src/pages/admin/AccountList.tsx:75
+#, c-format
+msgid "A list of all business account in the bank."
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:86
+#, c-format
+msgid "Create account"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:106
+#, c-format
+msgid "Name"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:110
+#, c-format
+msgid "Balance"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:112
+#, c-format
+msgid "Actions"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:151
+#, c-format
+msgid "unknown"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:170
+#, c-format
+msgid "change password"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:179
+#, fuzzy, c-format
+msgid "cashouts"
+msgstr "Ultime transazioni:"
+
+#: src/pages/admin/AccountList.tsx:189
+#, c-format
+msgid "remove"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:168
+#, c-format
+msgid "Cashout not implemented"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:184
+#, c-format
+msgid "Select a section"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:202
+#, c-format
+msgid "Last hour"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:208
+#, c-format
+msgid "Last day"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:216
+#, c-format
+msgid "Last month"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:222
+#, c-format
+msgid "Last year"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:310
+#, c-format
+msgid "Last Year"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:325
+#, c-format
+msgid "Trading volume on %1$s compared to %2$s"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:342
+#, c-format
+msgid "Cashin"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:352
+#, c-format
+msgid "Cashout"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:364
+#, c-format
+msgid "Payin"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:374
+#, c-format
+msgid "Payout"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:388
+#, c-format
+msgid "download stats as CSV"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:494
+#, c-format
+msgid "Decreased by"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:498
+#, c-format
+msgid "Increased by"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:89
+#, c-format
+msgid "Download bank stats"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:110
+#, c-format
+msgid "Include hour metric"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:143
+#, c-format
+msgid "Include day metric"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:173
+#, c-format
+msgid "Include month metric"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:206
+#, c-format
+msgid "Include year metric"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:239
+#, c-format
+msgid "Include table header"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:272
+#, c-format
+msgid "Add previous metric for compare"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:307
+#, c-format
+msgid "Fail on first error"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:364
+#, c-format
+msgid "Download"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:381
+#, c-format
+msgid "downloading... %1$s"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:399
+#, c-format
+msgid "Download completed"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:400
+#, c-format
+msgid "click here to save the file in your computer"
+msgstr ""
+
+#: src/pages/PublicHistoriesPage.tsx:78
+#, c-format
+msgid "History of public accounts"
+msgstr "Storico dei conti pubblici"
+
+#: src/pages/RegistrationPage.tsx:48
+#, c-format
+msgid "Currently, the bank is not accepting new registrations!"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:87
+#, fuzzy, c-format
+msgid "Missing name"
+msgstr "indirizzo Payto"
+
+#: src/pages/RegistrationPage.tsx:91
+#, c-format
+msgid "Use letters and numbers only, and start with a lowercase letter"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:107
+#, c-format
+msgid "Passwords don't match"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:130
+#, c-format
+msgid "Server replied with invalid phone or email."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:137
+#, c-format
+msgid "Registration is disabled because the bank ran out of bonus credit."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:144
+#, c-format
+msgid "No enough permission to create that account."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:151
+#, c-format
+msgid "That account id is already taken."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:158
+#, c-format
+msgid "That username is already taken."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:165
+#, c-format
+msgid "That username can't be used because is reserved."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:172
+#, c-format
+msgid "Only admin is allow to set debt limit."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:179
+#, c-format
+msgid "No information for the selected authentication channel."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:186
+#, c-format
+msgid "Authentication channel is not supported."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:193
+#, c-format
+msgid "Only admin can create accounts with second factor authentication."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:233
+#, c-format
+msgid "Account registration"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:315
+#, c-format
+msgid "Repeat password"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:457
+#, c-format
+msgid "Create a random temporary user"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:110
+#, c-format
+msgid "If you have a Taler wallet installed in this device"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:116
+#, c-format
+msgid ""
+"You will see the details of the operation in your wallet including the fees "
+"(if applies). If you still don't have one you can install it following "
+"instructions in"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:143
+#, c-format
+msgid "Withdraw"
+msgstr "Prelevare"
+
+#: src/pages/QrCodeSection.tsx:152
+#, c-format
+msgid "Or if you have the wallet in another device"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:157
+#, fuzzy, c-format
+msgid "Scan the QR below to start the withdrawal."
+msgstr "Chiudi il ritiro Taler"
+
+#: src/pages/WithdrawalQRCode.tsx:79
+#, c-format
+msgid "Operation aborted"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:82
+#, c-format
+msgid ""
+"The wire transfer to the Taler Exchange operator's account was aborted, your "
+"balance was not affected."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:88
+#, c-format
+msgid "You can close this page now or continue to the account page."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:147
+#, c-format
+msgid "Done"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:158
+#, c-format
+msgid "Operation canceled"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:173
+#, c-format
+msgid ""
+"The operation is marked as 'selected' but some step in the withdrawal failed"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:175
+#, c-format
+msgid "The account is selected but no withdrawal identification found."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:188
+#, c-format
+msgid ""
+"There is a withdrawal identification but no account has been selected or the "
+"selected account is invalid."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:202
+#, c-format
+msgid ""
+"No withdrawal ID found and no account has been selected or the selected "
+"account is invalid."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:259
+#, c-format
+msgid "Operation not found"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:263
+#, c-format
+msgid ""
+"This operation is not known by the server. The operation id is wrong or the "
+"server deleted the operation information before reaching here."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:278
+#, c-format
+msgid "Cotinue to dashboard"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:98
+#, c-format
+msgid "Cashout not found. It may be also mean that it was already aborted."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:136
+#, c-format
+msgid "Challenge not found."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:143
+#, c-format
+msgid "This user is not authorized to complete this challenge."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:150
+#, c-format
+msgid "Too many attempts, try another code."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:157
+#, c-format
+msgid "The confirmation code is wrong, try again."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:164
+#, c-format
+msgid "The operation expired."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:197
+#, fuzzy, c-format
+msgid "The operation failed."
+msgstr "Questo ritiro è stato annullato!"
+
+#: src/pages/SolveChallengePage.tsx:212
+#, c-format
+msgid "The operation needs another confirmation to complete."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:224
+#, c-format
+msgid "Account delete"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:226
+#, c-format
+msgid "Account update"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:228
+#, c-format
+msgid "Password update"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:230
+#, fuzzy, c-format
+msgid "Wire transfer"
+msgstr "Bonifico"
+
+#: src/pages/SolveChallengePage.tsx:232
+#, fuzzy, c-format
+msgid "Withdrawal"
+msgstr "Prelevare"
+
+#: src/pages/SolveChallengePage.tsx:248
+#, fuzzy, c-format
+msgid "Confirm the operation"
+msgstr "Conferma il ritiro"
+
+#: src/pages/SolveChallengePage.tsx:271
+#, c-format
+msgid "Enter the confirmation code"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:313
+#, c-format
+msgid "Confirm"
+msgstr "Conferma"
+
+#: src/pages/SolveChallengePage.tsx:348
+#, c-format
+msgid "Send again"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:359
+#, c-format
+msgid "Send code"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:369
+#, c-format
+msgid "Operation details"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:529
+#, c-format
+msgid "Challenge details"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:536
+#, c-format
+msgid "Sent at"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:551
+#, c-format
+msgid "To phone"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:553
+#, c-format
+msgid "To email"
+msgstr ""
+
+#: src/pages/WithdrawalOperationPage.tsx:49
+#, fuzzy, c-format
+msgid "The Withdrawal URI is not valid"
+msgstr "Questo ritiro è stato annullato!"
+
+#: src/components/Cashouts/views.tsx:100
+#, fuzzy, c-format
+msgid "Latest cashouts"
+msgstr "Ultime transazioni:"
+
+#: src/components/Cashouts/views.tsx:111
+#, c-format
+msgid "Created"
+msgstr ""
+
+#: src/components/Cashouts/views.tsx:115
+#, c-format
+msgid "Total debit"
+msgstr ""
+
+#: src/components/Cashouts/views.tsx:119
+#, c-format
+msgid "Total credit"
+msgstr ""
+
+#: src/pages/ProfileNavigation.tsx:70
+#, c-format
+msgid "Details"
+msgstr ""
+
+#: src/pages/ProfileNavigation.tsx:74
+#, c-format
+msgid "Delete"
+msgstr ""
+
+#: src/pages/ProfileNavigation.tsx:78
+#, fuzzy, c-format
+msgid "Credentials"
+msgstr "Credenziali invalide."
+
+#: src/pages/ProfileNavigation.tsx:82
+#, c-format
+msgid "Cashouts"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:95
+#, c-format
+msgid "Unable to create a cashout"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:96
+#, c-format
+msgid "The bank configuration does not support cashout operations."
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:223
+#, c-format
+msgid "need to be higher due to fees"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:225
+#, c-format
+msgid "the total transfer at destination will be zero"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:250
+#, c-format
+msgid "Cashout created"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:272
+#, c-format
+msgid ""
+"Duplicated request detected, check if the operation succeeded or try again."
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:279
+#, c-format
+msgid "The conversion rate was incorrectly applied"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:286
+#, c-format
+msgid "The account does not have sufficient funds"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:293
+#, c-format
+msgid "Cashouts are not supported"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:300
+#, c-format
+msgid "Missing cashout URI in the profile"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:307
+#, c-format
+msgid ""
+"Sending the confirmation message failed, retry later or contact the "
+"administrator."
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:339
+#, c-format
+msgid "Conversion rate"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:360
+#, c-format
+msgid "Fee"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:374
+#, c-format
+msgid "To account"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:381
+#, c-format
+msgid "No cashout account"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:382
+#, c-format
+msgid "Before doing a cashout you need to complete your profile"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:440
+#, fuzzy, c-format
+msgid "Amount to send"
+msgstr "Somma da ritirare"
+
+#: src/pages/business/CreateCashout.tsx:441
+#, fuzzy, c-format
+msgid "Amount to receive"
+msgstr "Somma da ritirare"
+
+#: src/pages/business/CreateCashout.tsx:490
+#, c-format
+msgid "Total cost"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:505
+#, c-format
+msgid "Balance left"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:520
+#, c-format
+msgid "Before fee"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:533
+#, c-format
+msgid "Total cashout transfer"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:553
+#, c-format
+msgid "No cashout channel available"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:555
+#, c-format
+msgid ""
+"Before doing a cashout the server need to provide an second channel to "
+"confirm the operation"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:567
+#, c-format
+msgid "Second factor authentication"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:598
+#, c-format
+msgid "Email"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:600
+#, c-format
+msgid "add a email in your profile to enable this option"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:646
+#, c-format
+msgid "SMS"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:648
+#, c-format
+msgid "add a phone number in your profile to enable this option"
+msgstr ""
+
+#: src/pages/account/CashoutListForAccount.tsx:52
+#, c-format
+msgid "Cashout for account %1$s"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:165
+#, c-format
+msgid "it doesn't have the pattern of an IBAN number"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:185
+#, c-format
+msgid "it doesn't have the pattern of an email"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:190
+#, c-format
+msgid "should start with +"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:192
+#, c-format
+msgid "phone number can't have other than numbers"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:329
+#, c-format
+msgid "account identification in the bank"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:365
+#, c-format
+msgid "name of the person owner the account"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:374
+#, c-format
+msgid "Internal IBAN"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:377
+#, c-format
+msgid "if empty a random account number will be assigned"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:378
+#, c-format
+msgid "account identification for bank transfer"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:423
+#, c-format
+msgid "Phone"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:451
+#, c-format
+msgid "Cashout IBAN"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:452
+#, c-format
+msgid "account number where the money is going to be sent when doing cashouts"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:470
+#, c-format
+msgid "Max debt"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:494
+#, c-format
+msgid "how much is user able to transfer after zero balance"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:508
+#, c-format
+msgid "Is this a Taler Exchange?"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:549
+#, c-format
+msgid "This server doesn't support second factor authentication."
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:560
+#, c-format
+msgid "Enable second factor authentication"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:596
+#, c-format
+msgid "Using email"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:654
+#, c-format
+msgid "Using SMS"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:691
+#, c-format
+msgid "Is this account public?"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:719
+#, c-format
+msgid "public accounts have their balance publicly accessible"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:100
+#, c-format
+msgid "Account updated"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:107
+#, c-format
+msgid "The rights to change the account are not sufficient"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:114
+#, c-format
+msgid "The username was not found"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:121
+#, c-format
+msgid ""
+"You can't change the legal name, please contact the your account "
+"administrator."
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:128
+#, c-format
+msgid ""
+"You can't change the debt limit, please contact the your account "
+"administrator."
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:135
+#, c-format
+msgid ""
+"You can't change the cashout address, please contact the your account "
+"administrator."
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:177
+#, c-format
+msgid "Account \"%1$s\""
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:190
+#, c-format
+msgid "Change details"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:235
+#, c-format
+msgid "Update"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:78
+#, c-format
+msgid "password doesn't match"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:95
+#, c-format
+msgid "Password changed"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:102
+#, c-format
+msgid "Not authorized to change the password, maybe the session is invalid."
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:112
+#, c-format
+msgid ""
+"You need to provide the old password. If you don't have it contact your "
+"account administrator."
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:117
+#, c-format
+msgid "Your current password doesn't match, can't change to a new password."
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:149
+#, c-format
+msgid "Update password"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:167
+#, c-format
+msgid "New password"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:195
+#, c-format
+msgid "Type it again"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:217
+#, c-format
+msgid "repeat the same password"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:227
+#, c-format
+msgid "Current password"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:248
+#, c-format
+msgid "your current password, for security"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:272
+#, c-format
+msgid "Change"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:74
+#, c-format
+msgid ""
+"Account created with password \"%1$s\". The user must change the password on "
+"the next login."
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:83
+#, c-format
+msgid "Server replied that phone or email is invalid"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:90
+#, c-format
+msgid "The rights to perform the operation are not sufficient"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:97
+#, c-format
+msgid "Account username is already taken"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:104
+#, c-format
+msgid "Account id is already taken"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:111
+#, c-format
+msgid "Bank ran out of bonus credit."
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:118
+#, c-format
+msgid "Account username can't be used because is reserved"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:160
+#, c-format
+msgid "Can't create accounts"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:161
+#, c-format
+msgid "Only system admin can create accounts."
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:183
+#, c-format
+msgid "New business account"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:209
+#, c-format
+msgid "Create"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:94
+#, c-format
+msgid "Can't delete the account"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:95
+#, c-format
+msgid ""
+"The account can't be delete while still holding some balance. First make "
+"sure that the owner make a complete cashout."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:117
+#, c-format
+msgid "Account removed"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:124
+#, c-format
+msgid "No enough permission to delete the account."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:131
+#, c-format
+msgid "The username was not found."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:138
+#, c-format
+msgid "Can't delete a reserved username."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:145
+#, c-format
+msgid "Can't delete an account with balance different than zero."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:170
+#, c-format
+msgid "name doesn't match"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:180
+#, c-format
+msgid "You are going to remove the account"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:182
+#, c-format
+msgid "This step can't be undone."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:188
+#, c-format
+msgid "Deleting account \"%1$s\""
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:206
+#, c-format
+msgid "Verification"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:231
+#, c-format
+msgid "enter the account name that is going to be deleted"
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:49
+#, c-format
+msgid "cashout id should be a number"
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:65
+#, c-format
+msgid "This cashout not found. Maybe already aborted."
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:106
+#, c-format
+msgid "Cashout detail"
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:139
+#, c-format
+msgid "Debited"
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:154
+#, c-format
+msgid "Credited"
+msgstr ""
+
+#: src/Routing.tsx:140
+#, c-format
+msgid "Welcome to %1$s!"
+msgstr ""
+
+#, fuzzy, c-format
+#~ msgid "Account not found."
+#~ msgstr "Lista conti pubblici non trovata."
+
+#, fuzzy, c-format
+#~ msgid "Confirmed"
+#~ msgstr "Conferma"
+
+#, c-format
+#~ msgid "Abort"
+#~ msgstr "Annulla"
+
+#, c-format
+#~ msgid "Skip to main content"
+#~ msgstr "Saltare il menu di navigazione"
+
+#, c-format
+#~ msgid "Please login!"
+#~ msgstr "Accedi!"
+
+#, c-format
+#~ msgid "Login"
+#~ msgstr "Accedi"
+
+#, fuzzy, c-format
+#~ msgid "Amount:"
+#~ msgstr "Somma"
+
+#, c-format
+#~ msgid "Want to try the raw payto://-format?"
+#~ msgstr "Prova il trasferimento tramite il formato Payto!"
+
+#, fuzzy, c-format
+#~ msgid "Transfer money to account identified by payto:// URI:"
+#~ msgstr "Trasferisci fondi a un altro conto di questa banca:"
+
+#, c-format
+#~ msgid "payto address"
+#~ msgstr "indirizzo Payto"
+
+#, fuzzy, c-format
+#~ msgid "No credentials given."
+#~ msgstr "Credenziali invalide."
+
+#, c-format
+#~ msgid "Username or account label '%1$s' not found. Won't login."
+#~ msgstr "L'utente '%1$s' non esiste. Login impossibile"
+
+#, c-format
+#~ msgid "Account information could not be retrieved."
+#~ msgstr "Impossibile ricevere le informazioni relative al conto."
+
+#, fuzzy, c-format
+#~ msgid "Bank account balance"
+#~ msgstr "Bilancio:"
+
+#, c-format
+#~ msgid "List of public accounts could not be retrieved."
+#~ msgstr "Lista conti pubblici non pervenuta."
+
+#, fuzzy, c-format
+#~ msgid "Please register!"
+#~ msgstr "Accedi!"
+
+#~ msgid "this link"
+#~ msgstr "questo link"
+
+#~ msgid "Clear"
+#~ msgstr "Cancella"
+
+#~ msgid "Demo Bank"
+#~ msgstr "Banca 'demo'"
+
+#~ msgid "Go back"
+#~ msgstr "Indietro"
+
+#~ msgid "Transfer money via the Payto system:"
+#~ msgstr "Effettua un bonifico tramite il sistema Payto:"
+
+#~ msgid "Withdraw Money into a Taler wallet"
+#~ msgstr "Ritira contante nel portafoglio Taler"
+
+#~ msgid "Register to the euFin bank!"
+#~ msgstr "Apri un conto in banca euFin!"
+
+#~ msgid "Page has a problem: logged in but backend state is lost."
+#~ msgstr ""
+#~ "Stato inconsistente: accesso utente effettuato ma stato con server perso."
+
+#, fuzzy
+#~ msgid "Welcome to the euFin bank!"
+#~ msgstr "Benvenuti in banca euFin!"
diff --git a/packages/bank-ui/src/i18n/poheader b/packages/bank-ui/src/i18n/poheader
new file mode 100644
index 000000000..d7a371934
--- /dev/null
+++ b/packages/bank-ui/src/i18n/poheader
@@ -0,0 +1,26 @@
+# This file is part of GNU Taler
+# (C) 2022-2024 Taler Systems S.A.
+#
+# GNU Taler is free software; you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 3, or (at your option) any later version.
+#
+# GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Bank\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
diff --git a/packages/bank-ui/src/i18n/strings.ts b/packages/bank-ui/src/i18n/strings.ts
new file mode 100644
index 000000000..f55e5efbc
--- /dev/null
+++ b/packages/bank-ui/src/i18n/strings.ts
@@ -0,0 +1,2295 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+export interface StringsType {
+ // X-Domain or 'messages'
+ domain: string;
+ lang: string;
+ completeness: number;
+ plural_forms: string;
+ locale_data: {
+ messages: Record<string, unknown>;
+ };
+}
+export const strings: Record<string, StringsType> = {};
+
+strings["it"] = {
+ locale_data: {
+ messages: {
+ "": {
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=n != 1;",
+ lang: "it",
+ },
+ "Operation failed, please report": ["Registrazione"],
+ "Request timeout": [""],
+ "Request throttled": [""],
+ "Malformed response": [""],
+ "Network error": [""],
+ "Unexpected request error": [""],
+ "Unexpected error": [""],
+ "IBAN numbers usually have more that 4 digits": [""],
+ "IBAN numbers usually have less that 34 digits": [""],
+ "IBAN country code not found": [""],
+ "IBAN number is not valid, checksum is wrong": [""],
+ "Max withdrawal amount": ["Questo ritiro è stato annullato!"],
+ "Show withdrawal confirmation": ["Questo ritiro è stato annullato!"],
+ "Show demo description": [""],
+ "Show install wallet first": [""],
+ "Use fast withdrawal form": ["Ritira contante"],
+ "Show debug info": [""],
+ "The reserve operation has been confirmed previously and can't be aborted":
+ [""],
+ "The operation id is invalid.": [""],
+ "The operation was not found.": ["Lista conti pubblici non trovata."],
+ "If you have a Taler wallet installed in this device": [""],
+ "You will see the details of the operation in your wallet including the fees (if applies). If you still don't have one you can install it following instructions in":
+ [""],
+ "this page": [""],
+ Withdraw: ["Prelevare"],
+ "Or if you have the wallet in another device": [""],
+ "Scan the QR below to start the withdrawal.": ["Chiudi il ritiro Taler"],
+ required: [""],
+ "IBAN should have just uppercased letters and numbers": [""],
+ "not valid": [""],
+ "should be greater than 0": [""],
+ "balance is not enough": [""],
+ "does not follow the pattern": [""],
+ 'only "IBAN" target are supported': [""],
+ 'use the "amount" parameter to specify the amount to be transferred': [
+ "",
+ ],
+ "the amount is not valid": [""],
+ 'use the "message" parameter to specify a reference text for the transfer':
+ [""],
+ "The request was invalid or the payto://-URI used unacceptable features.":
+ [""],
+ "Not enough permission to complete the operation.": [
+ "La banca sta creando l'operazione...",
+ ],
+ 'The destination account "%1$s" was not found.': [
+ "Lista conti pubblici non trovata.",
+ ],
+ "The origin and the destination of the transfer can't be the same.": [""],
+ "Your balance is not enough.": [""],
+ 'The origin account "%1$s" was not found.': [
+ "Lista conti pubblici non trovata.",
+ ],
+ "Using a form": [""],
+ "Import payto:// URI": [""],
+ Recipient: [""],
+ "IBAN of the recipient's account": [""],
+ "Transfer subject": [
+ "Trasferisci fondi a un altro conto di questa banca:",
+ ],
+ subject: ["Soggetto"],
+ "some text to identify the transfer": [""],
+ Amount: ["Importo"],
+ "amount to transfer": ["Somma da ritirare"],
+ "payto URI:": [""],
+ "uniform resource identifier of the target account": [""],
+ "payto://iban/[receiver-iban]?message=[subject]&amount=[%1$s:X.Y]": [""],
+ Cancel: [""],
+ Send: [""],
+ "Missing username": [""],
+ "Missing password": [""],
+ 'Wrong credentials for "%1$s"': ["Credenziali invalide."],
+ "Account not found": [""],
+ Username: [""],
+ "username of the account": [
+ "Trasferisci fondi a un altro conto di questa banca:",
+ ],
+ Password: [""],
+ "password of the account": ["Storico dei conti pubblici"],
+ Check: [""],
+ "Log in": [""],
+ Register: ["Registrati"],
+ "Wire transfer completed!": ["Bonifico"],
+ "The withdrawal has been aborted previously and can't be confirmed": [""],
+ "The withdrawal operation can't be confirmed before a wallet accepted the transaction.":
+ [""],
+ "Your balance is not enough for the operation.": [""],
+ "Confirm the withdrawal operation": ["Conferma il ritiro"],
+ "Wire transfer details": ["Bonifico"],
+ "Taler Exchange operator's account": [""],
+ "Taler Exchange operator's name": [""],
+ Transfer: [""],
+ "Authentication required": [""],
+ "This operation was created with other username": [""],
+ "Operation aborted": [""],
+ "The wire transfer to the Taler Exchange operator's account was aborted, your balance was not affected.":
+ [""],
+ "You can close this page now or continue to the account page.": [""],
+ Continue: [""],
+ "Withdrawal confirmed": ["Questo ritiro è stato annullato!"],
+ "The wire transfer to the Taler operator has been initiated. You will soon receive the requested amount in your Taler wallet.":
+ [""],
+ Done: [""],
+ "Operation canceled": [""],
+ "The operation is marked as 'selected' but some step in the withdrawal failed":
+ [""],
+ "The account is selected but no withdrawal identification found.": [""],
+ "There is a withdrawal identification but no account has been selected or the selected account is invalid.":
+ [""],
+ "No withdrawal ID found and no account has been selected or the selected account is invalid.":
+ [""],
+ "Operation not found": [""],
+ "This operation is not known by the server. The operation id is wrong or the server deleted the operation information before reaching here.":
+ [""],
+ "Cotinue to dashboard": [""],
+ "The Withdrawal URI is not valid": ["Questo ritiro è stato annullato!"],
+ 'the bank backend is not supported. supported version "%1$s", server version "%2$s"':
+ [""],
+ "Internal error, please report.": ["Registrazione"],
+ Preferences: [""],
+ "Welcome, %1$s": [""],
+ "Latest transactions": ["Ultime transazioni:"],
+ Date: ["Data"],
+ Counterpart: ["Controparte"],
+ Subject: ["Soggetto"],
+ sent: [""],
+ received: [""],
+ "invalid value": [""],
+ to: [""],
+ from: [""],
+ "First page": [""],
+ Next: [""],
+ "History of public accounts": ["Storico dei conti pubblici"],
+ "Currently, the bank is not accepting new registrations!": [""],
+ "Missing name": ["indirizzo Payto"],
+ "Use letters and numbers only, and start with a lowercase letter": [""],
+ "Passwords don't match": [""],
+ "Server replied with invalid phone or email.": [""],
+ "Registration is disabled because the bank ran out of bonus credit.": [
+ "",
+ ],
+ "No enough permission to create that account.": [""],
+ "That account id is already taken.": [""],
+ "That username is already taken.": [""],
+ "That username can't be used because is reserved.": [""],
+ "Only admin is allow to set debt limit.": [""],
+ "No information for the selected authentication channel.": [""],
+ "Authentication channel is not supported.": [""],
+ "Only admin can create accounts with second factor authentication.": [""],
+ "Account registration": [""],
+ "Repeat password": [""],
+ Name: [""],
+ "Create a random temporary user": [""],
+ "Make a wire transfer": ["Chiudi il bonifico"],
+ "Wire transfer created!": ["Bonifico"],
+ Accounts: ["Importo"],
+ "A list of all business account in the bank.": [""],
+ "Create account": [""],
+ Balance: [""],
+ Actions: [""],
+ unknown: [""],
+ "change password": [""],
+ remove: [""],
+ "Select a section": [""],
+ "Last hour": [""],
+ "Last day": [""],
+ "Last month": [""],
+ "Last year": [""],
+ "Last Year": [""],
+ "Trading volume on %1$s compared to %2$s": [""],
+ Cashin: [""],
+ Cashout: [""],
+ Payin: [""],
+ Payout: [""],
+ "download stats as CSV": [""],
+ "Descreased by": [""],
+ "Increased by": [""],
+ "Unable to create a cashout": [""],
+ "The bank configuration does not support cashout operations.": [""],
+ invalid: [""],
+ "need to be higher due to fees": [""],
+ "the total transfer at destination will be zero": [""],
+ "Cashout created": [""],
+ "Duplicated request detected, check if the operation succeded or try again.":
+ [""],
+ "The conversion rate was incorrectly applied": [""],
+ "The account does not have sufficient funds": [""],
+ "Cashouts are not supported": [""],
+ "Missing cashout URI in the profile": [""],
+ "Sending the confirmation message failed, retry later or contact the administrator.":
+ [""],
+ "Convertion rate": [""],
+ Fee: [""],
+ "To account": [""],
+ "No cashout account": [""],
+ "Before doing a cashout you need to complete your profile": [""],
+ "Amount to send": ["Somma da ritirare"],
+ "Amount to receive": ["Somma da ritirare"],
+ "Total cost": [""],
+ "Balance left": [""],
+ "Before fee": [""],
+ "Total cashout transfer": [""],
+ "No cashout channel available": [""],
+ "Before doing a cashout the server need to provide an second channel to confirm the operation":
+ [""],
+ "Second factor authentication": [""],
+ Email: [""],
+ "add a email in your profile to enable this option": [""],
+ SMS: [""],
+ "add a phone number in your profile to enable this option": [""],
+ Details: [""],
+ Delete: [""],
+ Credentials: ["Credenziali invalide."],
+ Cashouts: [""],
+ "it doesnt have the pattern of an IBAN number": [""],
+ "it doesnt have the pattern of an email": [""],
+ "should start with +": [""],
+ "phone number can't have other than numbers": [""],
+ "account identification in the bank": [""],
+ "name of the person owner the account": [""],
+ "Internal IBAN": [""],
+ "if empty a random account number will be assigned": [""],
+ "account identification for bank transfer": [""],
+ Phone: [""],
+ "Cashout IBAN": [""],
+ "account number where the money is going to be sent when doing cashouts":
+ [""],
+ "Max debt": [""],
+ "how much is user able to transfer after zero balance": [""],
+ "Is this a Taler Exchange?": [""],
+ "This server doesn't support second factor authentication.": [""],
+ "Enable second factor authentication": [""],
+ "Using email": [""],
+ "Using SMS": [""],
+ "Is this account public?": [""],
+ "public accounts have their balance publicly accesible": [""],
+ "Account updated": [""],
+ "The rights to change the account are not sufficient": [""],
+ "The username was not found": [""],
+ "You can't change the legal name, please contact the your account administrator.":
+ [""],
+ "You can't change the debt limit, please contact the your account administrator.":
+ [""],
+ "You can't change the cashout address, please contact the your account administrator.":
+ [""],
+ "You can't change the contact data, please contact the your account administrator.":
+ [""],
+ 'Account "%1$s"': [""],
+ "Change details": [""],
+ Update: [""],
+ "password doesn't match": [""],
+ "Password changed": [""],
+ "Not authorized to change the password, maybe the session is invalid.": [
+ "",
+ ],
+ "You need to provide the old password. If you don't have it contact your account administrator.":
+ [""],
+ "Your current password doesn't match, can't change to a new password.": [
+ "",
+ ],
+ "Update password": [""],
+ "New password": [""],
+ "Type it again": [""],
+ "repeat the same password": [""],
+ "Current password": [""],
+ "your current password, for security": [""],
+ Change: [""],
+ "Can't delete the account": [""],
+ "The account can't be delete while still holding some balance. First make sure that the owner make a complete cashout.":
+ [""],
+ "Account removed": [""],
+ "No enough permission to delete the account.": [""],
+ "The username was not found.": [""],
+ "Can't delete a reserved username.": [""],
+ "Can't delete an account with balance different than zero.": [""],
+ "name doesn't match": [""],
+ "You are going to remove the account": [""],
+ "This step can't be undone.": [""],
+ 'Deleting account "%1$s"': [""],
+ Verification: [""],
+ "enter the account name that is going to be deleted": [""],
+ 'Account created with password "%1$s". The user must change the password on the next login.':
+ [""],
+ "Server replied that phone or email is invalid": [""],
+ "The rights to perform the operation are not sufficient": [""],
+ "Account username is already taken": [""],
+ "Account id is already taken": [""],
+ "Bank ran out of bonus credit.": [""],
+ "Account username can't be used because is reserved": [""],
+ "Can't create accounts": [""],
+ "Only system admin can create accounts.": [""],
+ "New business account": [""],
+ Create: [""],
+ "Cashout not supported.": [""],
+ "Account not found.": ["Lista conti pubblici non trovata."],
+ "Latest cashouts": ["Ultime transazioni:"],
+ Created: [""],
+ Confirmed: ["Conferma"],
+ "Total debit": [""],
+ "Total credit": [""],
+ Status: [""],
+ never: [""],
+ "Cashout for account %1$s": [""],
+ "This cashout not found. Maybe already aborted.": [""],
+ "Cashout not found. It may be also mean that it was already aborted.": [
+ "",
+ ],
+ "Cashout was already confimed.": [""],
+ "Cashout operation is not supported.": [""],
+ "The cashout operation is already aborted.": [""],
+ "Missing destination account.": [""],
+ "Too many failed attempts.": [""],
+ "The code for this cashout is invalid.": [""],
+ "Cashout detail": [""],
+ Debited: [""],
+ Credited: [""],
+ "Enter the confirmation code": [""],
+ Abort: ["Annulla"],
+ Confirm: ["Conferma"],
+ "Unauthorized to make the operation, maybe the session has expired or the password changed.":
+ [""],
+ "The operation was rejected due to insufficient funds.": [""],
+ "Do not show this again": [""],
+ Close: [""],
+ "On this device": [""],
+ 'If you are using a web browser on desktop you should access your wallet with the GNU Taler WebExtension now or click the link if your WebExtension have the "Inject Taler support" option enabled.':
+ [""],
+ Start: [""],
+ "On a mobile phone": [""],
+ "Scan the QR code with your mobile device.": [
+ "Usa questo codice QR per ritirare contante nel tuo wallet:",
+ ],
+ "There is an operation already": [""],
+ "Complete or cancel the operation in": ["Conferma il ritiro"],
+ "Server responded with an invalid withdraw URI": [""],
+ "Withdraw URI: %1$s": ["Prelevare"],
+ "The operation was rejected due to insufficient funds": [""],
+ "Prepare your wallet": [""],
+ "After using your wallet you will need to confirm or cancel the operation on this site.":
+ [""],
+ "You need a GNU Taler Wallet": ["Ritira contante nel portafoglio Taler"],
+ "If you don't have one yet you can follow the instruction in": [""],
+ "Send money": [""],
+ "to a %1$s wallet": [""],
+ "Withdraw digital money into your mobile wallet or browser extension": [
+ "",
+ ],
+ "operation ready": [""],
+ "to another bank account": [
+ "Trasferisci fondi a un altro conto di questa banca:",
+ ],
+ "Make a wire transfer to an account with known bank account number.": [
+ "",
+ ],
+ "Transfer details": ["Effettua un bonifico"],
+ "This is a demo bank": [""],
+ "This part of the demo shows how a bank that supports Taler directly would work. In addition to using your own bank account, you can also see the transaction history of some %1$s.":
+ [""],
+ "This part of the demo shows how a bank that supports Taler directly would work.":
+ [""],
+ "Pending account delete operation": [""],
+ "Pending account update operation": [""],
+ "Pending password update operation": [""],
+ "Pending transaction operation": [""],
+ "Pending withdrawal operation": [""],
+ "Pending cashout operation": [""],
+ "You can complete or cancel the operation in": [""],
+ "Download bank stats": [""],
+ "Include hour metric": [""],
+ "Include day metric": [""],
+ "Include month metric": [""],
+ "Include year metric": [""],
+ "Include table header": [""],
+ "Add previous metric for compare": [""],
+ "Fail on first error": [""],
+ Download: [""],
+ "downloading... %1$s": [""],
+ "Download completed": [""],
+ "click here to save the file in your computer": [""],
+ "Challenge not found.": [""],
+ "This user is not authorized to complete this challenge.": [""],
+ "Too many attemps, try another code.": [""],
+ "The confirmation code is wrong, try again.": [""],
+ "The operation expired.": [""],
+ "The operation failed.": ["Questo ritiro è stato annullato!"],
+ "The operation needs another confirmation to complete.": [""],
+ "Account delete": [""],
+ "Account update": [""],
+ "Password update": [""],
+ "Wire transfer": ["Bonifico"],
+ Withdrawal: ["Prelevare"],
+ "Confirm the operation": ["Conferma il ritiro"],
+ "Send again": [""],
+ "Send code": [""],
+ "Operation details": [""],
+ "Challenge details": [""],
+ "Sent at": [""],
+ "To phone": [""],
+ "To email": [""],
+ "Welcome to %1$s!": [""],
+ },
+ },
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=n != 1;",
+ lang: "it",
+ completeness: 14,
+};
+
+strings["fr"] = {
+ locale_data: {
+ messages: {
+ "": {
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=n > 1;",
+ lang: "fr",
+ },
+ "Operation failed, please report": [""],
+ "Request timeout": [""],
+ "Request throttled": [""],
+ "Malformed response": [""],
+ "Network error": [""],
+ "Unexpected request error": [""],
+ "Unexpected error": [""],
+ "IBAN numbers usually have more that 4 digits": [""],
+ "IBAN numbers usually have less that 34 digits": [""],
+ "IBAN country code not found": [""],
+ "IBAN number is not valid, checksum is wrong": [""],
+ "Max withdrawal amount": [""],
+ "Show withdrawal confirmation": [""],
+ "Show demo description": [""],
+ "Show install wallet first": [""],
+ "Use fast withdrawal form": [""],
+ "Show debug info": [""],
+ "The reserve operation has been confirmed previously and can't be aborted":
+ [""],
+ "The operation id is invalid.": [""],
+ "The operation was not found.": [""],
+ "If you have a Taler wallet installed in this device": [""],
+ "You will see the details of the operation in your wallet including the fees (if applies). If you still don't have one you can install it following instructions in":
+ [""],
+ "this page": [""],
+ Withdraw: [""],
+ "Or if you have the wallet in another device": [""],
+ "Scan the QR below to start the withdrawal.": [""],
+ required: [""],
+ "IBAN should have just uppercased letters and numbers": [""],
+ "not valid": [""],
+ "should be greater than 0": [""],
+ "balance is not enough": [""],
+ "does not follow the pattern": [""],
+ 'only "IBAN" target are supported': [""],
+ 'use the "amount" parameter to specify the amount to be transferred': [
+ "",
+ ],
+ "the amount is not valid": [""],
+ 'use the "message" parameter to specify a reference text for the transfer':
+ [""],
+ "The request was invalid or the payto://-URI used unacceptable features.":
+ [""],
+ "Not enough permission to complete the operation.": [""],
+ 'The destination account "%1$s" was not found.': [""],
+ "The origin and the destination of the transfer can't be the same.": [""],
+ "Your balance is not enough.": [""],
+ 'The origin account "%1$s" was not found.': [""],
+ "Using a form": [""],
+ "Import payto:// URI": [""],
+ Recipient: [""],
+ "IBAN of the recipient's account": [""],
+ "Transfer subject": [""],
+ subject: [""],
+ "some text to identify the transfer": [""],
+ Amount: [""],
+ "amount to transfer": [""],
+ "payto URI:": [""],
+ "uniform resource identifier of the target account": [""],
+ "payto://iban/[receiver-iban]?message=[subject]&amount=[%1$s:X.Y]": [""],
+ Cancel: [""],
+ Send: [""],
+ "Missing username": [""],
+ "Missing password": [""],
+ 'Wrong credentials for "%1$s"': [""],
+ "Account not found": [""],
+ Username: [""],
+ "username of the account": [""],
+ Password: [""],
+ "password of the account": [""],
+ Check: [""],
+ "Log in": [""],
+ Register: [""],
+ "Wire transfer completed!": [""],
+ "The withdrawal has been aborted previously and can't be confirmed": [""],
+ "The withdrawal operation can't be confirmed before a wallet accepted the transaction.":
+ [""],
+ "Your balance is not enough for the operation.": [""],
+ "Confirm the withdrawal operation": [""],
+ "Wire transfer details": [""],
+ "Taler Exchange operator's account": [""],
+ "Taler Exchange operator's name": [""],
+ Transfer: [""],
+ "Authentication required": [""],
+ "This operation was created with other username": [""],
+ "Operation aborted": [""],
+ "The wire transfer to the Taler Exchange operator's account was aborted, your balance was not affected.":
+ [""],
+ "You can close this page now or continue to the account page.": [""],
+ Continue: [""],
+ "Withdrawal confirmed": [""],
+ "The wire transfer to the Taler operator has been initiated. You will soon receive the requested amount in your Taler wallet.":
+ [""],
+ Done: [""],
+ "Operation canceled": [""],
+ "The operation is marked as 'selected' but some step in the withdrawal failed":
+ [""],
+ "The account is selected but no withdrawal identification found.": [""],
+ "There is a withdrawal identification but no account has been selected or the selected account is invalid.":
+ [""],
+ "No withdrawal ID found and no account has been selected or the selected account is invalid.":
+ [""],
+ "Operation not found": [""],
+ "This operation is not known by the server. The operation id is wrong or the server deleted the operation information before reaching here.":
+ [""],
+ "Cotinue to dashboard": [""],
+ "The Withdrawal URI is not valid": [""],
+ 'the bank backend is not supported. supported version "%1$s", server version "%2$s"':
+ [""],
+ "Internal error, please report.": [""],
+ Preferences: [""],
+ "Welcome, %1$s": [""],
+ "Latest transactions": [""],
+ Date: [""],
+ Counterpart: [""],
+ Subject: [""],
+ sent: [""],
+ received: [""],
+ "invalid value": [""],
+ to: [""],
+ from: [""],
+ "First page": [""],
+ Next: [""],
+ "History of public accounts": [""],
+ "Currently, the bank is not accepting new registrations!": [""],
+ "Missing name": [""],
+ "Use letters and numbers only, and start with a lowercase letter": [""],
+ "Passwords don't match": [""],
+ "Server replied with invalid phone or email.": [""],
+ "Registration is disabled because the bank ran out of bonus credit.": [
+ "",
+ ],
+ "No enough permission to create that account.": [""],
+ "That account id is already taken.": [""],
+ "That username is already taken.": [""],
+ "That username can't be used because is reserved.": [""],
+ "Only admin is allow to set debt limit.": [""],
+ "No information for the selected authentication channel.": [""],
+ "Authentication channel is not supported.": [""],
+ "Only admin can create accounts with second factor authentication.": [""],
+ "Account registration": [""],
+ "Repeat password": [""],
+ Name: [""],
+ "Create a random temporary user": [""],
+ "Make a wire transfer": [""],
+ "Wire transfer created!": [""],
+ Accounts: [""],
+ "A list of all business account in the bank.": [""],
+ "Create account": [""],
+ Balance: [""],
+ Actions: [""],
+ unknown: [""],
+ "change password": [""],
+ remove: [""],
+ "Select a section": [""],
+ "Last hour": [""],
+ "Last day": [""],
+ "Last month": [""],
+ "Last year": [""],
+ "Last Year": [""],
+ "Trading volume on %1$s compared to %2$s": [""],
+ Cashin: [""],
+ Cashout: [""],
+ Payin: [""],
+ Payout: [""],
+ "download stats as CSV": [""],
+ "Descreased by": [""],
+ "Increased by": [""],
+ "Unable to create a cashout": [""],
+ "The bank configuration does not support cashout operations.": [""],
+ invalid: [""],
+ "need to be higher due to fees": [""],
+ "the total transfer at destination will be zero": [""],
+ "Cashout created": [""],
+ "Duplicated request detected, check if the operation succeded or try again.":
+ [""],
+ "The conversion rate was incorrectly applied": [""],
+ "The account does not have sufficient funds": [""],
+ "Cashouts are not supported": [""],
+ "Missing cashout URI in the profile": [""],
+ "Sending the confirmation message failed, retry later or contact the administrator.":
+ [""],
+ "Convertion rate": [""],
+ Fee: [""],
+ "To account": [""],
+ "No cashout account": [""],
+ "Before doing a cashout you need to complete your profile": [""],
+ "Amount to send": [""],
+ "Amount to receive": [""],
+ "Total cost": [""],
+ "Balance left": [""],
+ "Before fee": [""],
+ "Total cashout transfer": [""],
+ "No cashout channel available": [""],
+ "Before doing a cashout the server need to provide an second channel to confirm the operation":
+ [""],
+ "Second factor authentication": [""],
+ Email: [""],
+ "add a email in your profile to enable this option": [""],
+ SMS: [""],
+ "add a phone number in your profile to enable this option": [""],
+ Details: [""],
+ Delete: [""],
+ Credentials: [""],
+ Cashouts: [""],
+ "it doesnt have the pattern of an IBAN number": [""],
+ "it doesnt have the pattern of an email": [""],
+ "should start with +": [""],
+ "phone number can't have other than numbers": [""],
+ "account identification in the bank": [""],
+ "name of the person owner the account": [""],
+ "Internal IBAN": [""],
+ "if empty a random account number will be assigned": [""],
+ "account identification for bank transfer": [""],
+ Phone: [""],
+ "Cashout IBAN": [""],
+ "account number where the money is going to be sent when doing cashouts":
+ [""],
+ "Max debt": [""],
+ "how much is user able to transfer after zero balance": [""],
+ "Is this a Taler Exchange?": [""],
+ "This server doesn't support second factor authentication.": [""],
+ "Enable second factor authentication": [""],
+ "Using email": [""],
+ "Using SMS": [""],
+ "Is this account public?": [""],
+ "public accounts have their balance publicly accesible": [""],
+ "Account updated": [""],
+ "The rights to change the account are not sufficient": [""],
+ "The username was not found": [""],
+ "You can't change the legal name, please contact the your account administrator.":
+ [""],
+ "You can't change the debt limit, please contact the your account administrator.":
+ [""],
+ "You can't change the cashout address, please contact the your account administrator.":
+ [""],
+ "You can't change the contact data, please contact the your account administrator.":
+ [""],
+ 'Account "%1$s"': [""],
+ "Change details": [""],
+ Update: [""],
+ "password doesn't match": [""],
+ "Password changed": [""],
+ "Not authorized to change the password, maybe the session is invalid.": [
+ "",
+ ],
+ "You need to provide the old password. If you don't have it contact your account administrator.":
+ [""],
+ "Your current password doesn't match, can't change to a new password.": [
+ "",
+ ],
+ "Update password": [""],
+ "New password": [""],
+ "Type it again": [""],
+ "repeat the same password": [""],
+ "Current password": [""],
+ "your current password, for security": [""],
+ Change: [""],
+ "Can't delete the account": [""],
+ "The account can't be delete while still holding some balance. First make sure that the owner make a complete cashout.":
+ [""],
+ "Account removed": [""],
+ "No enough permission to delete the account.": [""],
+ "The username was not found.": [""],
+ "Can't delete a reserved username.": [""],
+ "Can't delete an account with balance different than zero.": [""],
+ "name doesn't match": [""],
+ "You are going to remove the account": [""],
+ "This step can't be undone.": [""],
+ 'Deleting account "%1$s"': [""],
+ Verification: [""],
+ "enter the account name that is going to be deleted": [""],
+ 'Account created with password "%1$s". The user must change the password on the next login.':
+ [""],
+ "Server replied that phone or email is invalid": [""],
+ "The rights to perform the operation are not sufficient": [""],
+ "Account username is already taken": [""],
+ "Account id is already taken": [""],
+ "Bank ran out of bonus credit.": [""],
+ "Account username can't be used because is reserved": [""],
+ "Can't create accounts": [""],
+ "Only system admin can create accounts.": [""],
+ "New business account": [""],
+ Create: [""],
+ "Cashout not supported.": [""],
+ "Account not found.": [""],
+ "Latest cashouts": [""],
+ Created: [""],
+ Confirmed: [""],
+ "Total debit": [""],
+ "Total credit": [""],
+ Status: [""],
+ never: [""],
+ "Cashout for account %1$s": [""],
+ "This cashout not found. Maybe already aborted.": [""],
+ "Cashout not found. It may be also mean that it was already aborted.": [
+ "",
+ ],
+ "Cashout was already confimed.": [""],
+ "Cashout operation is not supported.": [""],
+ "The cashout operation is already aborted.": [""],
+ "Missing destination account.": [""],
+ "Too many failed attempts.": [""],
+ "The code for this cashout is invalid.": [""],
+ "Cashout detail": [""],
+ Debited: [""],
+ Credited: [""],
+ "Enter the confirmation code": [""],
+ Abort: [""],
+ Confirm: [""],
+ "Unauthorized to make the operation, maybe the session has expired or the password changed.":
+ [""],
+ "The operation was rejected due to insufficient funds.": [""],
+ "Do not show this again": [""],
+ Close: [""],
+ "On this device": [""],
+ 'If you are using a web browser on desktop you should access your wallet with the GNU Taler WebExtension now or click the link if your WebExtension have the "Inject Taler support" option enabled.':
+ [""],
+ Start: [""],
+ "On a mobile phone": [""],
+ "Scan the QR code with your mobile device.": [""],
+ "There is an operation already": [""],
+ "Complete or cancel the operation in": [""],
+ "Server responded with an invalid withdraw URI": [""],
+ "Withdraw URI: %1$s": [""],
+ "The operation was rejected due to insufficient funds": [""],
+ "Prepare your wallet": [""],
+ "After using your wallet you will need to confirm or cancel the operation on this site.":
+ [""],
+ "You need a GNU Taler Wallet": [""],
+ "If you don't have one yet you can follow the instruction in": [""],
+ "Send money": [""],
+ "to a %1$s wallet": [""],
+ "Withdraw digital money into your mobile wallet or browser extension": [
+ "",
+ ],
+ "operation ready": [""],
+ "to another bank account": [""],
+ "Make a wire transfer to an account with known bank account number.": [
+ "",
+ ],
+ "Transfer details": [""],
+ "This is a demo bank": [""],
+ "This part of the demo shows how a bank that supports Taler directly would work. In addition to using your own bank account, you can also see the transaction history of some %1$s.":
+ [""],
+ "This part of the demo shows how a bank that supports Taler directly would work.":
+ [""],
+ "Pending account delete operation": [""],
+ "Pending account update operation": [""],
+ "Pending password update operation": [""],
+ "Pending transaction operation": [""],
+ "Pending withdrawal operation": [""],
+ "Pending cashout operation": [""],
+ "You can complete or cancel the operation in": [""],
+ "Download bank stats": [""],
+ "Include hour metric": [""],
+ "Include day metric": [""],
+ "Include month metric": [""],
+ "Include year metric": [""],
+ "Include table header": [""],
+ "Add previous metric for compare": [""],
+ "Fail on first error": [""],
+ Download: [""],
+ "downloading... %1$s": [""],
+ "Download completed": [""],
+ "click here to save the file in your computer": [""],
+ "Challenge not found.": [""],
+ "This user is not authorized to complete this challenge.": [""],
+ "Too many attemps, try another code.": [""],
+ "The confirmation code is wrong, try again.": [""],
+ "The operation expired.": [""],
+ "The operation failed.": [""],
+ "The operation needs another confirmation to complete.": [""],
+ "Account delete": [""],
+ "Account update": [""],
+ "Password update": [""],
+ "Wire transfer": [""],
+ Withdrawal: [""],
+ "Confirm the operation": [""],
+ "Send again": [""],
+ "Send code": [""],
+ "Operation details": [""],
+ "Challenge details": [""],
+ "Sent at": [""],
+ "To phone": [""],
+ "To email": [""],
+ "Welcome to %1$s!": [""],
+ },
+ },
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=n > 1;",
+ lang: "fr",
+ completeness: 0,
+};
+
+strings["es"] = {
+ locale_data: {
+ messages: {
+ "": {
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=n != 1;",
+ lang: "es",
+ },
+ "Operation failed, please report": [
+ "La operaicón falló, por favor reportelo",
+ ],
+ "Request timeout": ["La petición al servidor agoto su tiempo"],
+ "Request throttled": ["La petición al servidor interrumpida"],
+ "Malformed response": ["Respuesta malformada"],
+ "Network error": ["Error de conexión"],
+ "Unexpected request error": ["Error de pedido inesperado"],
+ "Unexpected error": ["Error inesperado"],
+ "IBAN numbers usually have more that 4 digits": [
+ "Los números IBAN usualmente tienen mas de 4 digitos",
+ ],
+ "IBAN numbers usually have less that 34 digits": [
+ "Los números IBAN usualmente tienen menos de 34 digitos",
+ ],
+ "IBAN country code not found": ["Código de pais de IBAN no encontrado"],
+ "IBAN number is not valid, checksum is wrong": [
+ "El número IBAN no es válido, falló la verificación",
+ ],
+ "Max withdrawal amount": ["Monto máximo de extracción"],
+ "Show withdrawal confirmation": ["Mostrar confirmación de extracción"],
+ "Show demo description": ["Mostrar descripción de demo"],
+ "Show install wallet first": ["Mostrar instalar la billetera primero"],
+ "Use fast withdrawal form": ["Usar formulario de extracción rápida"],
+ "Show debug info": ["Mostrar información de depuración"],
+ "The reserve operation has been confirmed previously and can't be aborted":
+ [
+ "La operación en la reserva ya ha sido confirmada previamente y no puede ser abortada",
+ ],
+ "The operation id is invalid.": ["El id de operación es invalido."],
+ "The operation was not found.": ["La operación no se encontró."],
+ "If you have a Taler wallet installed in this device": [
+ "Si tienes una billetera Taler instalada en este dispositivo",
+ ],
+ "You will see the details of the operation in your wallet including the fees (if applies). If you still don't have one you can install it following instructions in":
+ [
+ "Veras los detalles de la operación en tu billetera incluyendo comisiones (si aplicán). Si todavía no tienes una puedes instalarla siguiendo las instrucciones en",
+ ],
+ "this page": ["esta página"],
+ Withdraw: ["Retirar"],
+ "Or if you have the wallet in another device": [
+ "O si tienes la billetera en otro dispositivo",
+ ],
+ "Scan the QR below to start the withdrawal.": [
+ "Escanea el QR debajo para comenzar la extracción.",
+ ],
+ required: ["requerido"],
+ "IBAN should have just uppercased letters and numbers": [
+ "IBAN debería tener letras mayúsculas y números",
+ ],
+ "not valid": ["no válido"],
+ "should be greater than 0": ["Debería ser mas grande que 0"],
+ "balance is not enough": ["el saldo no es suficiente"],
+ "does not follow the pattern": ["no tiene un patrón valido"],
+ 'only "IBAN" target are supported': [
+ 'solo cuentas "IBAN" son soportadas',
+ ],
+ 'use the "amount" parameter to specify the amount to be transferred': [
+ 'usa el parámetro "amount" para indicar el monto a ser transferido',
+ ],
+ "the amount is not valid": ["el monto no es válido"],
+ 'use the "message" parameter to specify a reference text for the transfer':
+ [
+ 'usa el parámetro "message" para indicar un texto de referencia en la transferencia',
+ ],
+ "The request was invalid or the payto://-URI used unacceptable features.":
+ [
+ "El pedido era inválido o el URI payto:// usado tiene características inaceptables.",
+ ],
+ "Not enough permission to complete the operation.": [
+ "Sin permisos suficientes para completar la operación.",
+ ],
+ 'The destination account "%1$s" was not found.': [
+ 'La cuenta de destino "%1$s" no fue encontrada.',
+ ],
+ "The origin and the destination of the transfer can't be the same.": [
+ "El origen y destino de la transferencia no puede ser la misma.",
+ ],
+ "Your balance is not enough.": ["El saldo no es suficiente."],
+ 'The origin account "%1$s" was not found.': [
+ 'La cuenta origen "%1$s" no fue encontrada.',
+ ],
+ "Using a form": ["Usando un formulario"],
+ "Import payto:// URI": ["Importando un URI payto://"],
+ Recipient: ["Destinatario"],
+ "IBAN of the recipient's account": [
+ "Numero IBAN de la cuenta destinataria",
+ ],
+ "Transfer subject": ["Asunto de transferencia"],
+ subject: ["asunto"],
+ "some text to identify the transfer": [
+ "algún texto para identificar la transferencia",
+ ],
+ Amount: ["Monto"],
+ "amount to transfer": ["monto a transferir"],
+ "payto URI:": ["payto URI:"],
+ "uniform resource identifier of the target account": [
+ "identificador de recurso uniforme de la cuenta destino",
+ ],
+ "payto://iban/[receiver-iban]?message=[subject]&amount=[%1$s:X.Y]": [
+ "payto://iban/[iban-destinatario]?message=[asunto]&amount=[%1$s:X.Y]",
+ ],
+ Cancel: ["Cancelar"],
+ Send: ["Envíar"],
+ "Missing username": ["Falta nombre de usuario"],
+ "Missing password": ["Falta contraseña"],
+ 'Wrong credentials for "%1$s"': ['Credenciales incorrectas para "%1$s"'],
+ "Account not found": ["Cuenta no encontrada"],
+ Username: ["Nombre de usuario"],
+ "username of the account": ["nombre de usuario de la cuenta"],
+ Password: ["Contraseña"],
+ "password of the account": ["contraseña de la cuenta"],
+ Check: ["Verificar"],
+ "Log in": ["Acceso"],
+ Register: ["Registrarse"],
+ "Wire transfer completed!": ["Transferencia bancaria completada!"],
+ "The withdrawal has been aborted previously and can't be confirmed": [
+ "La extracción fue abortada anteriormente y no puede ser confirmada",
+ ],
+ "The withdrawal operation can't be confirmed before a wallet accepted the transaction.":
+ [
+ "La operación de extracción no puede ser confirmada antes de que una billetera acepte la transaccion.",
+ ],
+ "Your balance is not enough for the operation.": [
+ "El saldo no es suficiente para la operación.",
+ ],
+ "Confirm the withdrawal operation": [
+ "Confirme la operación de extracción",
+ ],
+ "Wire transfer details": ["Detalle de transferencia bancaria"],
+ "Taler Exchange operator's account": [
+ "Cuenta del operador del Taler Exchange",
+ ],
+ "Taler Exchange operator's name": [
+ "Nombre del operador del Taler Exchange",
+ ],
+ Transfer: ["Transferencia"],
+ "Authentication required": ["Autenticación requerida"],
+ "This operation was created with other username": [
+ "Esta operación fue creada con otro usuario",
+ ],
+ "Operation aborted": ["Operación abortada"],
+ "The wire transfer to the Taler Exchange operator's account was aborted, your balance was not affected.":
+ [
+ "La transferencia bancaria a la cuenta del operador del Taler Exchange fue abortada, su saldo no fue afectado.",
+ ],
+ "You can close this page now or continue to the account page.": [
+ "Ya puedes cerrar esta pagina or continuar a la página de estado de cuenta.",
+ ],
+ Continue: ["Continuar"],
+ "Withdrawal confirmed": ["La extracción fue confirmada"],
+ "The wire transfer to the Taler operator has been initiated. You will soon receive the requested amount in your Taler wallet.":
+ [
+ "La transferencia bancaria al operador Taler fue iniciada. Pronto recibirás el monto pedido en tu billetera Taler.",
+ ],
+ Done: ["Listo"],
+ "Operation canceled": ["Operación cancelada"],
+ "The operation is marked as 'selected' but some step in the withdrawal failed":
+ [
+ "La operación está marcada como 'seleccionada' pero algunos pasos en la extracción fallaron",
+ ],
+ "The account is selected but no withdrawal identification found.": [
+ "La cuenta está seleccionada pero no se encontró el identificador de extracción.",
+ ],
+ "There is a withdrawal identification but no account has been selected or the selected account is invalid.":
+ [
+ "Hay un identificador de extracción pero la cuenta no ha sido seleccionada o la selccionada es inválida.",
+ ],
+ "No withdrawal ID found and no account has been selected or the selected account is invalid.":
+ [
+ "No hay un identificador de extracción y ninguna cuenta a sido seleccionada o la seleccionada es inválida.",
+ ],
+ "Operation not found": ["Operación no encontrada"],
+ "This operation is not known by the server. The operation id is wrong or the server deleted the operation information before reaching here.":
+ [
+ "Esta operación no es conocida por el servidor. El identificador de operación es incorrecto o el server borró la información de la operación antes de llegar hasta aquí.",
+ ],
+ "Cotinue to dashboard": ["Continuar al panel"],
+ "The Withdrawal URI is not valid": ["El URI de estracción no es válido"],
+ 'the bank backend is not supported. supported version "%1$s", server version "%2$s"':
+ [
+ 'El servidor de bank no esta spoportado. Version soportada "%1$s", version del server "%2$s"',
+ ],
+ "Internal error, please report.": [
+ "Error interno, por favor reporte el error.",
+ ],
+ Preferences: ["Preferencias"],
+ "Welcome, %1$s": ["Bienvenido/a, %1$s"],
+ "Latest transactions": ["Últimas transacciones"],
+ Date: ["Fecha"],
+ Counterpart: ["Contraparte"],
+ Subject: ["Asunto"],
+ sent: ["enviado"],
+ received: ["recibido"],
+ "invalid value": ["valor inválido"],
+ to: ["hacia"],
+ from: ["desde"],
+ "First page": ["Primera página"],
+ Next: ["Siguiente"],
+ "History of public accounts": ["Historial de cuentas públicas"],
+ "Currently, the bank is not accepting new registrations!": [
+ "Actualmente, el banco no está aceptado nuevos registros!",
+ ],
+ "Missing name": ["Falta nombre"],
+ "Use letters and numbers only, and start with a lowercase letter": [
+ "Solo use letras y números, y comience con una letra minúscula",
+ ],
+ "Passwords don't match": ["La contraseña no coincide"],
+ "Server replied with invalid phone or email.": [
+ "El servidor repondio con teléfono o dirección de correo inválido.",
+ ],
+ "Registration is disabled because the bank ran out of bonus credit.": [
+ "El registro está deshabilitado porque el banco se quedó sin crédito bonus.",
+ ],
+ "No enough permission to create that account.": [
+ "Sin permisos suficientes para crear esa cuenta.",
+ ],
+ "That account id is already taken.": [
+ "El identificador de cuenta ya está tomado.",
+ ],
+ "That username is already taken.": [
+ "El nombre de usuario ya está tomado.",
+ ],
+ "That username can't be used because is reserved.": [
+ "El nombre de usuario no puede ser usado porque esta reservado.",
+ ],
+ "Only admin is allow to set debt limit.": [
+ "Solo el administrador tiene permitido cambiar el límite de deuda.",
+ ],
+ "No information for the selected authentication channel.": [
+ "No hay información para el canal de autenticación seleccionado.",
+ ],
+ "Authentication channel is not supported.": [
+ "Canal de autenticación no esta soportado.",
+ ],
+ "Only admin can create accounts with second factor authentication.": [
+ "Solo el administrador puede crear cuentas con el segundo factor de autenticación.",
+ ],
+ "Account registration": ["Registro de cuenta"],
+ "Repeat password": ["Repita la contraseña"],
+ Name: ["Nombre"],
+ "Create a random temporary user": ["Crear un usuario aleatorio temporal"],
+ "Make a wire transfer": ["Hacer una transferencia bancaria"],
+ "Wire transfer created!": ["Transferencia bancaria creada!"],
+ Accounts: ["Cuentas"],
+ "A list of all business account in the bank.": [
+ "Una lista de todas las cuentas en el banco.",
+ ],
+ "Create account": ["Crear cuenta"],
+ Balance: ["Saldo"],
+ Actions: ["Acciones"],
+ unknown: ["desconocido"],
+ "change password": ["cambiar contraseña"],
+ remove: ["elimiar"],
+ "Select a section": ["Seleccione una sección"],
+ "Last hour": ["Última hora"],
+ "Last day": ["Último día"],
+ "Last month": ["Último mes"],
+ "Last year": ["Último año"],
+ "Last Year": ["Último Año"],
+ "Trading volume on %1$s compared to %2$s": [
+ "Vólumen de comercio en %1$s comparado con %2$s",
+ ],
+ Cashin: ["Ingreso"],
+ Cashout: ["Egreso"],
+ Payin: ["Envios de dinero"],
+ Payout: ["Recibos de dinero"],
+ "download stats as CSV": ["descargar estadísticas en CSV"],
+ "Descreased by": ["Descendiente por"],
+ "Increased by": ["Ascendente por"],
+ "Unable to create a cashout": ["Imposible crear un egreso"],
+ "The bank configuration does not support cashout operations.": [
+ "La configuración del banco no soporta operaciones de egreso.",
+ ],
+ invalid: ["inválido"],
+ "need to be higher due to fees": [
+ "necesita ser mayor debido a las comisiones",
+ ],
+ "the total transfer at destination will be zero": [
+ "el total de la transferencia en destino será cero",
+ ],
+ "Cashout created": ["Egreso creado"],
+ "Duplicated request detected, check if the operation succeded or try again.":
+ [
+ "Se detectó una petición duplicada, verifique si la operación tuvo éxito o intente otra vez.",
+ ],
+ "The conversion rate was incorrectly applied": [
+ "La tasa de conversión se aplicó incorrectamente",
+ ],
+ "The account does not have sufficient funds": [
+ "La cuenta no tiene fondos suficientes",
+ ],
+ "Cashouts are not supported": ["Egresos no están soportados"],
+ "Missing cashout URI in the profile": [
+ "Falta dirección de egreso en el perfíl",
+ ],
+ "Sending the confirmation message failed, retry later or contact the administrator.":
+ [
+ "El envío del mensaje de confirmación falló, intente mas tarde o contacte al administrador.",
+ ],
+ "Convertion rate": ["Tasa de conversión"],
+ Fee: ["Comisión"],
+ "To account": ["Hacia cuenta"],
+ "No cashout account": ["No hay cuenta de egreso"],
+ "Before doing a cashout you need to complete your profile": [
+ "Antes de hacer un egreso necesita completar su perfíl",
+ ],
+ "Amount to send": ["Monto a enviar"],
+ "Amount to receive": ["Monto a recibir"],
+ "Total cost": ["Costo total"],
+ "Balance left": ["Saldo remanente"],
+ "Before fee": ["Antes de comisión"],
+ "Total cashout transfer": ["Total de egreso"],
+ "No cashout channel available": ["No hay canal de egreso disponible"],
+ "Before doing a cashout the server need to provide an second channel to confirm the operation":
+ [
+ "Antes de hacer un egreso el servidor necesita proveer un segundo canal para confirmar la operación",
+ ],
+ "Second factor authentication": ["Segundo factor de autenticación"],
+ Email: ["Correo eletrónico"],
+ "add a email in your profile to enable this option": [
+ "agrege un correo en su perfíl para habilitar esta opción",
+ ],
+ SMS: ["SMS"],
+ "add a phone number in your profile to enable this option": [
+ "agregue un número de teléfono para habilitar esta opción",
+ ],
+ Details: ["Detalles"],
+ Delete: ["Borrar"],
+ Credentials: ["Credenciales"],
+ Cashouts: ["Egresos"],
+ "it doesnt have the pattern of an IBAN number": [
+ "no tiene el patrón de un número IBAN",
+ ],
+ "it doesnt have the pattern of an email": [
+ "no tiene el patrón de un correo electrónico",
+ ],
+ "should start with +": ["debería comenzar con un +"],
+ "phone number can't have other than numbers": [
+ "número de teléfono no puede tener otra cosa que numeros",
+ ],
+ "account identification in the bank": [
+ "identificador de cuenta en el banco",
+ ],
+ "name of the person owner the account": [
+ "nombre de la persona dueña de la cuenta",
+ ],
+ "Internal IBAN": ["IBAN interno"],
+ "if empty a random account number will be assigned": [
+ "si está vacío un número de cuenta aleatorio será asignado",
+ ],
+ "account identification for bank transfer": [
+ "identificador de cuenta para transferencia bancaria",
+ ],
+ Phone: ["Teléfono"],
+ "Cashout IBAN": ["IBAN de egreso"],
+ "account number where the money is going to be sent when doing cashouts":
+ [
+ "numero de cuenta donde el dinero será enviado cuando se ejecuten egresos",
+ ],
+ "Max debt": ["Máxima deuda"],
+ "how much is user able to transfer after zero balance": [
+ "cuanto tiene habilitado a transferir despues de un saldo en cero",
+ ],
+ "Is this a Taler Exchange?": ["Es un Taler Exchange?"],
+ "This server doesn't support second factor authentication.": [
+ "Este servidor no tiene soporte para segundo factor de autenticación.",
+ ],
+ "Enable second factor authentication": [
+ "Hábilitar segundo factor de autenticación",
+ ],
+ "Using email": ["Usando correo eletrónico"],
+ "Using SMS": ["Usando SMS"],
+ "Is this account public?": ["Es una cuenta pública?"],
+ "public accounts have their balance publicly accesible": [
+ "las cuentas públicas tienen su saldo accesible al público",
+ ],
+ "Account updated": ["Cuenta actualizada"],
+ "The rights to change the account are not sufficient": [
+ "Los permisos para cambiar la cuenta no son suficientes",
+ ],
+ "The username was not found": ["El nombre de usaurio no se encontró"],
+ "You can't change the legal name, please contact the your account administrator.":
+ [
+ "No puede cambiar el nombre legal, por favor contacte el administrador de la cuenta.",
+ ],
+ "You can't change the debt limit, please contact the your account administrator.":
+ [
+ "No puede cambiar el límite de deuda, por favor contacte el administrador de la cuenta.",
+ ],
+ "You can't change the cashout address, please contact the your account administrator.":
+ [
+ "No puede cambiar la dirección de egreso, por favor contacte al administrador de la cuenta.",
+ ],
+ "You can't change the contact data, please contact the your account administrator.":
+ [
+ "No puede cambiar los datos de contacto, por favor contacte al administrador de la cuenta.",
+ ],
+ 'Account "%1$s"': ['Cuenta "%1$s"'],
+ "Change details": ["Cambiar detalles"],
+ Update: ["Actualizar"],
+ "password doesn't match": ["la contraseña no coincide"],
+ "Password changed": ["La contraseña cambió"],
+ "Not authorized to change the password, maybe the session is invalid.": [
+ "No está autorizado a cambiar el password, quizá la sesión es invalida.",
+ ],
+ "You need to provide the old password. If you don't have it contact your account administrator.":
+ [
+ "Se necesita el password viejo para cambiar la contraseña. Si no lo tiene contacte a su administrador.",
+ ],
+ "Your current password doesn't match, can't change to a new password.": [
+ "Su actual contraseña no coincide, no puede cambiar a una nueva contraseña.",
+ ],
+ "Update password": ["Actualizar contraseña"],
+ "New password": ["Nueva contraseña"],
+ "Type it again": ["Escribalo otra vez"],
+ "repeat the same password": ["repita la misma contraseña"],
+ "Current password": ["Contraseña actual"],
+ "your current password, for security": [
+ "su actual contraseña, por seguridad",
+ ],
+ Change: ["Cambiar"],
+ "Can't delete the account": ["No se puede eliminar la cuenta"],
+ "The account can't be delete while still holding some balance. First make sure that the owner make a complete cashout.":
+ [
+ "La cuenta no puede ser eliminada mientras tiene saldo. Primero aseguresé que el dueño haga un egreso completo.",
+ ],
+ "Account removed": ["Cuenta eliminada"],
+ "No enough permission to delete the account.": [
+ "No tiene permisos suficientes para eliminar la cuenta.",
+ ],
+ "The username was not found.": ["El nombr ede usuario no se encontró."],
+ "Can't delete a reserved username.": [
+ "No se puede eliminar un nombre de usuario reservado.",
+ ],
+ "Can't delete an account with balance different than zero.": [
+ "No se puede eliminar una cuenta con saldo diferente a cero.",
+ ],
+ "name doesn't match": ["el nombre no coincide"],
+ "You are going to remove the account": ["Está por eliminar la cuenta"],
+ "This step can't be undone.": ["Este paso no puede ser deshecho."],
+ 'Deleting account "%1$s"': ['Borrando cuenta "%1$s"'],
+ Verification: ["Verificación"],
+ "enter the account name that is going to be deleted": [
+ "ingrese el nombre de cuenta que será eliminado",
+ ],
+ 'Account created with password "%1$s". The user must change the password on the next login.':
+ [
+ 'Cuenta creada con contraseña "%1$s". El usuario debe cambiar la contraseña en el siguiente ingreso.',
+ ],
+ "Server replied that phone or email is invalid": [
+ "El servidor respondió que el teléfono o correo eletrónico es invalido",
+ ],
+ "The rights to perform the operation are not sufficient": [
+ "Los permisos para ejecutar la operación no son suficientes",
+ ],
+ "Account username is already taken": [
+ "El nombre del usuario ya está tomado",
+ ],
+ "Account id is already taken": ["El id de cuenta ya está tomado"],
+ "Bank ran out of bonus credit.": [
+ "El banco no tiene mas crédito de bonus.",
+ ],
+ "Account username can't be used because is reserved": [
+ "El nombre de usuario de la cuenta no puede userse porque está reservado",
+ ],
+ "Can't create accounts": ["No puede crear cuentas"],
+ "Only system admin can create accounts.": [
+ "Solo los administradores del sistema pueden crear cuentas.",
+ ],
+ "New business account": ["Nueva cuenta"],
+ Create: ["Crear"],
+ "Cashout not supported.": ["Egreso no soportado."],
+ "Account not found.": ["Cuenta no encontrada."],
+ "Latest cashouts": ["Últimos egresos"],
+ Created: ["Creado"],
+ Confirmed: ["Confirmado"],
+ "Total debit": ["Débito total"],
+ "Total credit": ["Crédito total"],
+ Status: ["Estado"],
+ never: ["nunca"],
+ "Cashout for account %1$s": ["Egreso para cuenta %1$s"],
+ "This cashout not found. Maybe already aborted.": [
+ "Este egreso no se encontró. Quizá fue abortado.",
+ ],
+ "Cashout not found. It may be also mean that it was already aborted.": [
+ "Egreso no econtrado. También puede significar que ya ha sido abortado.",
+ ],
+ "Cashout was already confimed.": ["Egreso ya fue confirmado."],
+ "Cashout operation is not supported.": [
+ "Operación de egreso no soportada.",
+ ],
+ "The cashout operation is already aborted.": [
+ "La operación de egreso ya está abortada.",
+ ],
+ "Missing destination account.": ["Falta cuenta destino."],
+ "Too many failed attempts.": ["Demasiados intentos fallidos."],
+ "The code for this cashout is invalid.": [
+ "El código para este egreso es invalido.",
+ ],
+ "Cashout detail": ["Detalles de egreso"],
+ Debited: ["Debitado"],
+ Credited: ["Acreditado"],
+ "Enter the confirmation code": ["Ingresar el código de confirmación"],
+ Abort: ["Abortar"],
+ Confirm: ["Confirmar"],
+ "Unauthorized to make the operation, maybe the session has expired or the password changed.":
+ [
+ "No autorizado para hacer la operación, quizá la sesión haya expirado or cambió la contraseña.",
+ ],
+ "The operation was rejected due to insufficient funds.": [
+ "La operación fue rechazada debido a saldo insuficiente.",
+ ],
+ "Do not show this again": ["No mostrar otra vez"],
+ Close: ["Cerrar"],
+ "On this device": ["En este dispositivo"],
+ 'If you are using a web browser on desktop you should access your wallet with the GNU Taler WebExtension now or click the link if your WebExtension have the "Inject Taler support" option enabled.':
+ [
+ 'Si esta usando un explorador web de escritorio deberías acceder ahora a tu billletera con la GNU Taler WebExtension o hacer click en el link si tu extensión tiene la configuración "Inyectar soporte para Taler" habilitada.',
+ ],
+ Start: ["Comenzar"],
+ "On a mobile phone": ["En un dispotivo mobile"],
+ "Scan the QR code with your mobile device.": [
+ "Escanear el código QR con tu dispotivo móvil.",
+ ],
+ "There is an operation already": ["Ya hay una operación"],
+ "Complete or cancel the operation in": [
+ "Completa o cancela la operación en",
+ ],
+ "Server responded with an invalid withdraw URI": [
+ "El servidor respondió con una URI de extracción inválida",
+ ],
+ "Withdraw URI: %1$s": ["URI de extracción: %1$s"],
+ "The operation was rejected due to insufficient funds": [
+ "La operación fue rechazada debido a fundos insuficientes",
+ ],
+ "Prepare your wallet": ["Prepare su billetera"],
+ "After using your wallet you will need to confirm or cancel the operation on this site.":
+ [
+ "Despues de usar tu billetera necesitarás confirmar o cancelar la operación en este sitio.",
+ ],
+ "You need a GNU Taler Wallet": ["Necesitas una GNU Taler Wallet"],
+ "If you don't have one yet you can follow the instruction in": [
+ "Si no tienes una todavía puedes seguir las instrucciones en",
+ ],
+ "Send money": ["Enviar dinero"],
+ "to a %1$s wallet": ["a una billetera %1$s"],
+ "Withdraw digital money into your mobile wallet or browser extension": [
+ "Extraer dinero digital a tu billetera móvil o extesión web",
+ ],
+ "operation ready": ["operación lista"],
+ "to another bank account": ["a otra cuenta bancaria"],
+ "Make a wire transfer to an account with known bank account number.": [
+ "Hacer una transferencia bancaria a una cuenta con un número de cuenta conocido.",
+ ],
+ "Transfer details": ["Detalles de transferencia"],
+ "This is a demo bank": ["Este es un banco de demostración"],
+ "This part of the demo shows how a bank that supports Taler directly would work. In addition to using your own bank account, you can also see the transaction history of some %1$s.":
+ [
+ "Esta parte de la demostración muestra cómo funciona un banco que soporta Taler directamente. Además de usar tu propia cuenta de banco, también podrás ver el historial de transacciones de algunas %1$s.",
+ ],
+ "This part of the demo shows how a bank that supports Taler directly would work.":
+ [
+ "Esta parte de la demostración muetra como un banco que soporta Taler directamente funcionaría.",
+ ],
+ "Pending account delete operation": [
+ "Operación pendiente de eliminación de cuenta",
+ ],
+ "Pending account update operation": [
+ "Operación pendiente de actualización de cuenta",
+ ],
+ "Pending password update operation": [
+ "Operación pendiente de actualización de password",
+ ],
+ "Pending transaction operation": ["Operación pendiente de transacción"],
+ "Pending withdrawal operation": ["Operación pendiente de extracción"],
+ "Pending cashout operation": ["Operación pendiente de egreso"],
+ "You can complete or cancel the operation in": [
+ "Puedes completar o cancelar la operación en",
+ ],
+ "Download bank stats": ["Descargar estadísticas del banco"],
+ "Include hour metric": ["Incluir métrica horaria"],
+ "Include day metric": ["Incluir métrica diaria"],
+ "Include month metric": ["Incluir métrica mensual"],
+ "Include year metric": ["Incluir métrica anual"],
+ "Include table header": ["Incluir encabezado de tabla"],
+ "Add previous metric for compare": [
+ "Agregar métrica previa para comparar",
+ ],
+ "Fail on first error": ["Fallar en el primer error"],
+ Download: ["Descargar"],
+ "downloading... %1$s": ["descargando... %1$s"],
+ "Download completed": ["Descarga completada"],
+ "click here to save the file in your computer": [
+ "click aquí para guardar el archivo en su computadora",
+ ],
+ "Challenge not found.": ["Desafío no encontrado."],
+ "This user is not authorized to complete this challenge.": [
+ "Este usuario no está autorizado para completar este desafío.",
+ ],
+ "Too many attemps, try another code.": [
+ "Demasiados intentos, intente otro código.",
+ ],
+ "The confirmation code is wrong, try again.": [
+ "El código de confirmación es erroneo, intente otra vez.",
+ ],
+ "The operation expired.": ["La operación expiró."],
+ "The operation failed.": ["La operación falló."],
+ "The operation needs another confirmation to complete.": [
+ "La operación necesita otra confirmación para completar.",
+ ],
+ "Account delete": ["Eliminación de cuenta"],
+ "Account update": ["Actualización de cuenta"],
+ "Password update": ["Actualización de contraseña"],
+ "Wire transfer": ["Transferencia bancaria"],
+ Withdrawal: ["Extracción"],
+ "Confirm the operation": ["Confirmar la operación"],
+ "Send again": ["Enviar otra vez"],
+ "Send code": ["Enviar código"],
+ "Operation details": ["Detalles de operación"],
+ "Challenge details": ["Detalles del desafío"],
+ "Sent at": ["Enviado a"],
+ "To phone": ["Al teléfono"],
+ "To email": ["Al email"],
+ "Welcome to %1$s!": ["Bienvenido a %1$s!"],
+ },
+ },
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=n != 1;",
+ lang: "es",
+ completeness: 100,
+};
+
+strings["en"] = {
+ locale_data: {
+ messages: {
+ "": {
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=(n != 1);",
+ lang: "en",
+ },
+ "Operation failed, please report": [""],
+ "Request timeout": [""],
+ "Request throttled": [""],
+ "Malformed response": [""],
+ "Network error": [""],
+ "Unexpected request error": [""],
+ "Unexpected error": [""],
+ "IBAN numbers usually have more that 4 digits": [""],
+ "IBAN numbers usually have less that 34 digits": [""],
+ "IBAN country code not found": [""],
+ "IBAN number is not valid, checksum is wrong": [""],
+ "Max withdrawal amount": [""],
+ "Show withdrawal confirmation": [""],
+ "Show demo description": [""],
+ "Show install wallet first": [""],
+ "Use fast withdrawal form": [""],
+ "Show debug info": [""],
+ "The reserve operation has been confirmed previously and can't be aborted":
+ [""],
+ "The operation id is invalid.": [""],
+ "The operation was not found.": [""],
+ "If you have a Taler wallet installed in this device": [""],
+ "You will see the details of the operation in your wallet including the fees (if applies). If you still don't have one you can install it following instructions in":
+ [""],
+ "this page": [""],
+ Withdraw: [""],
+ "Or if you have the wallet in another device": [""],
+ "Scan the QR below to start the withdrawal.": [""],
+ required: [""],
+ "IBAN should have just uppercased letters and numbers": [""],
+ "not valid": [""],
+ "should be greater than 0": [""],
+ "balance is not enough": [""],
+ "does not follow the pattern": [""],
+ 'only "IBAN" target are supported': [""],
+ 'use the "amount" parameter to specify the amount to be transferred': [
+ "",
+ ],
+ "the amount is not valid": [""],
+ 'use the "message" parameter to specify a reference text for the transfer':
+ [""],
+ "The request was invalid or the payto://-URI used unacceptable features.":
+ [""],
+ "Not enough permission to complete the operation.": [""],
+ 'The destination account "%1$s" was not found.': [""],
+ "The origin and the destination of the transfer can't be the same.": [""],
+ "Your balance is not enough.": [""],
+ 'The origin account "%1$s" was not found.': [""],
+ "Using a form": [""],
+ "Import payto:// URI": [""],
+ Recipient: [""],
+ "IBAN of the recipient's account": [""],
+ "Transfer subject": [""],
+ subject: [""],
+ "some text to identify the transfer": [""],
+ Amount: [""],
+ "amount to transfer": [""],
+ "payto URI:": [""],
+ "uniform resource identifier of the target account": [""],
+ "payto://iban/[receiver-iban]?message=[subject]&amount=[%1$s:X.Y]": [""],
+ Cancel: [""],
+ Send: [""],
+ "Missing username": [""],
+ "Missing password": [""],
+ 'Wrong credentials for "%1$s"': [""],
+ "Account not found": [""],
+ Username: [""],
+ "username of the account": [""],
+ Password: [""],
+ "password of the account": [""],
+ Check: [""],
+ "Log in": [""],
+ Register: [""],
+ "Wire transfer completed!": [""],
+ "The withdrawal has been aborted previously and can't be confirmed": [""],
+ "The withdrawal operation can't be confirmed before a wallet accepted the transaction.":
+ [""],
+ "Your balance is not enough for the operation.": [""],
+ "Confirm the withdrawal operation": [""],
+ "Wire transfer details": [""],
+ "Taler Exchange operator's account": [""],
+ "Taler Exchange operator's name": [""],
+ Transfer: [""],
+ "Authentication required": [""],
+ "This operation was created with other username": [""],
+ "Operation aborted": [""],
+ "The wire transfer to the Taler Exchange operator's account was aborted, your balance was not affected.":
+ [""],
+ "You can close this page now or continue to the account page.": [""],
+ Continue: [""],
+ "Withdrawal confirmed": [""],
+ "The wire transfer to the Taler operator has been initiated. You will soon receive the requested amount in your Taler wallet.":
+ [""],
+ Done: [""],
+ "Operation canceled": [""],
+ "The operation is marked as 'selected' but some step in the withdrawal failed":
+ [""],
+ "The account is selected but no withdrawal identification found.": [""],
+ "There is a withdrawal identification but no account has been selected or the selected account is invalid.":
+ [""],
+ "No withdrawal ID found and no account has been selected or the selected account is invalid.":
+ [""],
+ "Operation not found": [""],
+ "This operation is not known by the server. The operation id is wrong or the server deleted the operation information before reaching here.":
+ [""],
+ "Cotinue to dashboard": [""],
+ "The Withdrawal URI is not valid": [""],
+ 'the bank backend is not supported. supported version "%1$s", server version "%2$s"':
+ [""],
+ "Internal error, please report.": [""],
+ Preferences: [""],
+ "Welcome, %1$s": [""],
+ "Latest transactions": [""],
+ Date: [""],
+ Counterpart: [""],
+ Subject: [""],
+ sent: [""],
+ received: [""],
+ "invalid value": [""],
+ to: [""],
+ from: [""],
+ "First page": [""],
+ Next: [""],
+ "History of public accounts": [""],
+ "Currently, the bank is not accepting new registrations!": [""],
+ "Missing name": [""],
+ "Use letters and numbers only, and start with a lowercase letter": [""],
+ "Passwords don't match": [""],
+ "Server replied with invalid phone or email.": [""],
+ "Registration is disabled because the bank ran out of bonus credit.": [
+ "",
+ ],
+ "No enough permission to create that account.": [""],
+ "That account id is already taken.": [""],
+ "That username is already taken.": [""],
+ "That username can't be used because is reserved.": [""],
+ "Only admin is allow to set debt limit.": [""],
+ "No information for the selected authentication channel.": [""],
+ "Authentication channel is not supported.": [""],
+ "Only admin can create accounts with second factor authentication.": [""],
+ "Account registration": [""],
+ "Repeat password": [""],
+ Name: [""],
+ "Create a random temporary user": [""],
+ "Make a wire transfer": [""],
+ "Wire transfer created!": [""],
+ Accounts: [""],
+ "A list of all business account in the bank.": [""],
+ "Create account": [""],
+ Balance: [""],
+ Actions: [""],
+ unknown: [""],
+ "change password": [""],
+ remove: [""],
+ "Select a section": [""],
+ "Last hour": [""],
+ "Last day": [""],
+ "Last month": [""],
+ "Last year": [""],
+ "Last Year": [""],
+ "Trading volume on %1$s compared to %2$s": [""],
+ Cashin: [""],
+ Cashout: [""],
+ Payin: [""],
+ Payout: [""],
+ "download stats as CSV": [""],
+ "Descreased by": [""],
+ "Increased by": [""],
+ "Unable to create a cashout": [""],
+ "The bank configuration does not support cashout operations.": [""],
+ invalid: [""],
+ "need to be higher due to fees": [""],
+ "the total transfer at destination will be zero": [""],
+ "Cashout created": [""],
+ "Duplicated request detected, check if the operation succeded or try again.":
+ [""],
+ "The conversion rate was incorrectly applied": [""],
+ "The account does not have sufficient funds": [""],
+ "Cashouts are not supported": [""],
+ "Missing cashout URI in the profile": [""],
+ "Sending the confirmation message failed, retry later or contact the administrator.":
+ [""],
+ "Convertion rate": [""],
+ Fee: [""],
+ "To account": [""],
+ "No cashout account": [""],
+ "Before doing a cashout you need to complete your profile": [""],
+ "Amount to send": [""],
+ "Amount to receive": [""],
+ "Total cost": [""],
+ "Balance left": [""],
+ "Before fee": [""],
+ "Total cashout transfer": [""],
+ "No cashout channel available": [""],
+ "Before doing a cashout the server need to provide an second channel to confirm the operation":
+ [""],
+ "Second factor authentication": [""],
+ Email: [""],
+ "add a email in your profile to enable this option": [""],
+ SMS: [""],
+ "add a phone number in your profile to enable this option": [""],
+ Details: [""],
+ Delete: [""],
+ Credentials: [""],
+ Cashouts: [""],
+ "it doesnt have the pattern of an IBAN number": [""],
+ "it doesnt have the pattern of an email": [""],
+ "should start with +": [""],
+ "phone number can't have other than numbers": [""],
+ "account identification in the bank": [""],
+ "name of the person owner the account": [""],
+ "Internal IBAN": [""],
+ "if empty a random account number will be assigned": [""],
+ "account identification for bank transfer": [""],
+ Phone: [""],
+ "Cashout IBAN": [""],
+ "account number where the money is going to be sent when doing cashouts":
+ [""],
+ "Max debt": [""],
+ "how much is user able to transfer after zero balance": [""],
+ "Is this a Taler Exchange?": [""],
+ "This server doesn't support second factor authentication.": [""],
+ "Enable second factor authentication": [""],
+ "Using email": [""],
+ "Using SMS": [""],
+ "Is this account public?": [""],
+ "public accounts have their balance publicly accesible": [""],
+ "Account updated": [""],
+ "The rights to change the account are not sufficient": [""],
+ "The username was not found": [""],
+ "You can't change the legal name, please contact the your account administrator.":
+ [""],
+ "You can't change the debt limit, please contact the your account administrator.":
+ [""],
+ "You can't change the cashout address, please contact the your account administrator.":
+ [""],
+ "You can't change the contact data, please contact the your account administrator.":
+ [""],
+ 'Account "%1$s"': [""],
+ "Change details": [""],
+ Update: [""],
+ "password doesn't match": [""],
+ "Password changed": [""],
+ "Not authorized to change the password, maybe the session is invalid.": [
+ "",
+ ],
+ "You need to provide the old password. If you don't have it contact your account administrator.":
+ [""],
+ "Your current password doesn't match, can't change to a new password.": [
+ "",
+ ],
+ "Update password": [""],
+ "New password": [""],
+ "Type it again": [""],
+ "repeat the same password": [""],
+ "Current password": [""],
+ "your current password, for security": [""],
+ Change: [""],
+ "Can't delete the account": [""],
+ "The account can't be delete while still holding some balance. First make sure that the owner make a complete cashout.":
+ [""],
+ "Account removed": [""],
+ "No enough permission to delete the account.": [""],
+ "The username was not found.": [""],
+ "Can't delete a reserved username.": [""],
+ "Can't delete an account with balance different than zero.": [""],
+ "name doesn't match": [""],
+ "You are going to remove the account": [""],
+ "This step can't be undone.": [""],
+ 'Deleting account "%1$s"': [""],
+ Verification: [""],
+ "enter the account name that is going to be deleted": [""],
+ 'Account created with password "%1$s". The user must change the password on the next login.':
+ [""],
+ "Server replied that phone or email is invalid": [""],
+ "The rights to perform the operation are not sufficient": [""],
+ "Account username is already taken": [""],
+ "Account id is already taken": [""],
+ "Bank ran out of bonus credit.": [""],
+ "Account username can't be used because is reserved": [""],
+ "Can't create accounts": [""],
+ "Only system admin can create accounts.": [""],
+ "New business account": [""],
+ Create: [""],
+ "Cashout not supported.": [""],
+ "Account not found.": [""],
+ "Latest cashouts": [""],
+ Created: [""],
+ Confirmed: [""],
+ "Total debit": [""],
+ "Total credit": [""],
+ Status: [""],
+ never: [""],
+ "Cashout for account %1$s": [""],
+ "This cashout not found. Maybe already aborted.": [""],
+ "Cashout not found. It may be also mean that it was already aborted.": [
+ "",
+ ],
+ "Cashout was already confimed.": [""],
+ "Cashout operation is not supported.": [""],
+ "The cashout operation is already aborted.": [""],
+ "Missing destination account.": [""],
+ "Too many failed attempts.": [""],
+ "The code for this cashout is invalid.": [""],
+ "Cashout detail": [""],
+ Debited: [""],
+ Credited: [""],
+ "Enter the confirmation code": [""],
+ Abort: [""],
+ Confirm: [""],
+ "Unauthorized to make the operation, maybe the session has expired or the password changed.":
+ [""],
+ "The operation was rejected due to insufficient funds.": [""],
+ "Do not show this again": [""],
+ Close: [""],
+ "On this device": [""],
+ 'If you are using a web browser on desktop you should access your wallet with the GNU Taler WebExtension now or click the link if your WebExtension have the "Inject Taler support" option enabled.':
+ [""],
+ Start: [""],
+ "On a mobile phone": [""],
+ "Scan the QR code with your mobile device.": [""],
+ "There is an operation already": [""],
+ "Complete or cancel the operation in": [""],
+ "Server responded with an invalid withdraw URI": [""],
+ "Withdraw URI: %1$s": [""],
+ "The operation was rejected due to insufficient funds": [""],
+ "Prepare your wallet": [""],
+ "After using your wallet you will need to confirm or cancel the operation on this site.":
+ [""],
+ "You need a GNU Taler Wallet": [""],
+ "If you don't have one yet you can follow the instruction in": [""],
+ "Send money": [""],
+ "to a %1$s wallet": [""],
+ "Withdraw digital money into your mobile wallet or browser extension": [
+ "",
+ ],
+ "operation ready": [""],
+ "to another bank account": [""],
+ "Make a wire transfer to an account with known bank account number.": [
+ "",
+ ],
+ "Transfer details": [""],
+ "This is a demo bank": [""],
+ "This part of the demo shows how a bank that supports Taler directly would work. In addition to using your own bank account, you can also see the transaction history of some %1$s.":
+ [""],
+ "This part of the demo shows how a bank that supports Taler directly would work.":
+ [""],
+ "Pending account delete operation": [""],
+ "Pending account update operation": [""],
+ "Pending password update operation": [""],
+ "Pending transaction operation": [""],
+ "Pending withdrawal operation": [""],
+ "Pending cashout operation": [""],
+ "You can complete or cancel the operation in": [""],
+ "Download bank stats": [""],
+ "Include hour metric": [""],
+ "Include day metric": [""],
+ "Include month metric": [""],
+ "Include year metric": [""],
+ "Include table header": [""],
+ "Add previous metric for compare": [""],
+ "Fail on first error": [""],
+ Download: [""],
+ "downloading... %1$s": [""],
+ "Download completed": [""],
+ "click here to save the file in your computer": [""],
+ "Challenge not found.": [""],
+ "This user is not authorized to complete this challenge.": [""],
+ "Too many attemps, try another code.": [""],
+ "The confirmation code is wrong, try again.": [""],
+ "The operation expired.": [""],
+ "The operation failed.": [""],
+ "The operation needs another confirmation to complete.": [""],
+ "Account delete": [""],
+ "Account update": [""],
+ "Password update": [""],
+ "Wire transfer": [""],
+ Withdrawal: [""],
+ "Confirm the operation": [""],
+ "Send again": [""],
+ "Send code": [""],
+ "Operation details": [""],
+ "Challenge details": [""],
+ "Sent at": [""],
+ "To phone": [""],
+ "To email": [""],
+ "Welcome to %1$s!": [""],
+ },
+ },
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=(n != 1);",
+ lang: "en",
+ completeness: 100,
+};
+
+strings["de"] = {
+ locale_data: {
+ messages: {
+ "": {
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=n != 1;",
+ lang: "de",
+ },
+ "Operation failed, please report": [""],
+ "Request timeout": [""],
+ "Request throttled": [""],
+ "Malformed response": [""],
+ "Network error": [""],
+ "Unexpected request error": [""],
+ "Unexpected error": [""],
+ "IBAN numbers usually have more that 4 digits": [""],
+ "IBAN numbers usually have less that 34 digits": [""],
+ "IBAN country code not found": [""],
+ "IBAN number is not valid, checksum is wrong": [""],
+ "Max withdrawal amount": [""],
+ "Show withdrawal confirmation": [""],
+ "Show demo description": [""],
+ "Show install wallet first": [""],
+ "Use fast withdrawal form": [""],
+ "Show debug info": [""],
+ "The reserve operation has been confirmed previously and can't be aborted":
+ [""],
+ "The operation id is invalid.": [""],
+ "The operation was not found.": [""],
+ "If you have a Taler wallet installed in this device": [""],
+ "You will see the details of the operation in your wallet including the fees (if applies). If you still don't have one you can install it following instructions in":
+ [""],
+ "this page": [""],
+ Withdraw: [""],
+ "Or if you have the wallet in another device": [""],
+ "Scan the QR below to start the withdrawal.": [""],
+ required: [""],
+ "IBAN should have just uppercased letters and numbers": [""],
+ "not valid": [""],
+ "should be greater than 0": [""],
+ "balance is not enough": [""],
+ "does not follow the pattern": [""],
+ 'only "IBAN" target are supported': [""],
+ 'use the "amount" parameter to specify the amount to be transferred': [
+ "",
+ ],
+ "the amount is not valid": [""],
+ 'use the "message" parameter to specify a reference text for the transfer':
+ [""],
+ "The request was invalid or the payto://-URI used unacceptable features.":
+ [""],
+ "Not enough permission to complete the operation.": [""],
+ 'The destination account "%1$s" was not found.': [""],
+ "The origin and the destination of the transfer can't be the same.": [""],
+ "Your balance is not enough.": [""],
+ 'The origin account "%1$s" was not found.': [""],
+ "Using a form": [""],
+ "Import payto:// URI": [""],
+ Recipient: [""],
+ "IBAN of the recipient's account": [""],
+ "Transfer subject": [""],
+ subject: ["Verwendungszweck"],
+ "some text to identify the transfer": [""],
+ Amount: ["Betrag"],
+ "amount to transfer": ["Betrag"],
+ "payto URI:": [""],
+ "uniform resource identifier of the target account": [""],
+ "payto://iban/[receiver-iban]?message=[subject]&amount=[%1$s:X.Y]": [""],
+ Cancel: [""],
+ Send: [""],
+ "Missing username": [""],
+ "Missing password": [""],
+ 'Wrong credentials for "%1$s"': [""],
+ "Account not found": [""],
+ Username: [""],
+ "username of the account": [""],
+ Password: [""],
+ "password of the account": ["Buchungen auf öffentlich sichtbaren Konten"],
+ Check: [""],
+ "Log in": [""],
+ Register: [""],
+ "Wire transfer completed!": [""],
+ "The withdrawal has been aborted previously and can't be confirmed": [""],
+ "The withdrawal operation can't be confirmed before a wallet accepted the transaction.":
+ [""],
+ "Your balance is not enough for the operation.": [""],
+ "Confirm the withdrawal operation": ["Abhebung bestätigen"],
+ "Wire transfer details": [""],
+ "Taler Exchange operator's account": [""],
+ "Taler Exchange operator's name": [""],
+ Transfer: [""],
+ "Authentication required": [""],
+ "This operation was created with other username": [""],
+ "Operation aborted": [""],
+ "The wire transfer to the Taler Exchange operator's account was aborted, your balance was not affected.":
+ [""],
+ "You can close this page now or continue to the account page.": [""],
+ Continue: [""],
+ "Withdrawal confirmed": [""],
+ "The wire transfer to the Taler operator has been initiated. You will soon receive the requested amount in your Taler wallet.":
+ [""],
+ Done: [""],
+ "Operation canceled": [""],
+ "The operation is marked as 'selected' but some step in the withdrawal failed":
+ [""],
+ "The account is selected but no withdrawal identification found.": [""],
+ "There is a withdrawal identification but no account has been selected or the selected account is invalid.":
+ [""],
+ "No withdrawal ID found and no account has been selected or the selected account is invalid.":
+ [""],
+ "Operation not found": [""],
+ "This operation is not known by the server. The operation id is wrong or the server deleted the operation information before reaching here.":
+ [""],
+ "Cotinue to dashboard": [""],
+ "The Withdrawal URI is not valid": [""],
+ 'the bank backend is not supported. supported version "%1$s", server version "%2$s"':
+ [""],
+ "Internal error, please report.": [""],
+ Preferences: [""],
+ "Welcome, %1$s": [""],
+ "Latest transactions": [""],
+ Date: ["Datum"],
+ Counterpart: ["Empfänger"],
+ Subject: ["Verwendungszweck"],
+ sent: [""],
+ received: [""],
+ "invalid value": [""],
+ to: [""],
+ from: [""],
+ "First page": [""],
+ Next: [""],
+ "History of public accounts": [
+ "Buchungen auf öffentlich sichtbaren Konten",
+ ],
+ "Currently, the bank is not accepting new registrations!": [""],
+ "Missing name": [""],
+ "Use letters and numbers only, and start with a lowercase letter": [""],
+ "Passwords don't match": [""],
+ "Server replied with invalid phone or email.": [""],
+ "Registration is disabled because the bank ran out of bonus credit.": [
+ "",
+ ],
+ "No enough permission to create that account.": [""],
+ "That account id is already taken.": [""],
+ "That username is already taken.": [""],
+ "That username can't be used because is reserved.": [""],
+ "Only admin is allow to set debt limit.": [""],
+ "No information for the selected authentication channel.": [""],
+ "Authentication channel is not supported.": [""],
+ "Only admin can create accounts with second factor authentication.": [""],
+ "Account registration": [""],
+ "Repeat password": [""],
+ Name: [""],
+ "Create a random temporary user": [""],
+ "Make a wire transfer": [""],
+ "Wire transfer created!": [""],
+ Accounts: ["Betrag"],
+ "A list of all business account in the bank.": [""],
+ "Create account": [""],
+ Balance: [""],
+ Actions: [""],
+ unknown: [""],
+ "change password": [""],
+ remove: [""],
+ "Select a section": [""],
+ "Last hour": [""],
+ "Last day": [""],
+ "Last month": [""],
+ "Last year": [""],
+ "Last Year": [""],
+ "Trading volume on %1$s compared to %2$s": [""],
+ Cashin: [""],
+ Cashout: [""],
+ Payin: [""],
+ Payout: [""],
+ "download stats as CSV": [""],
+ "Descreased by": [""],
+ "Increased by": [""],
+ "Unable to create a cashout": [""],
+ "The bank configuration does not support cashout operations.": [""],
+ invalid: [""],
+ "need to be higher due to fees": [""],
+ "the total transfer at destination will be zero": [""],
+ "Cashout created": [""],
+ "Duplicated request detected, check if the operation succeded or try again.":
+ [""],
+ "The conversion rate was incorrectly applied": [""],
+ "The account does not have sufficient funds": [""],
+ "Cashouts are not supported": [""],
+ "Missing cashout URI in the profile": [""],
+ "Sending the confirmation message failed, retry later or contact the administrator.":
+ [""],
+ "Convertion rate": [""],
+ Fee: [""],
+ "To account": [""],
+ "No cashout account": [""],
+ "Before doing a cashout you need to complete your profile": [""],
+ "Amount to send": ["Betrag"],
+ "Amount to receive": [""],
+ "Total cost": [""],
+ "Balance left": [""],
+ "Before fee": [""],
+ "Total cashout transfer": [""],
+ "No cashout channel available": [""],
+ "Before doing a cashout the server need to provide an second channel to confirm the operation":
+ [""],
+ "Second factor authentication": [""],
+ Email: [""],
+ "add a email in your profile to enable this option": [""],
+ SMS: [""],
+ "add a phone number in your profile to enable this option": [""],
+ Details: [""],
+ Delete: [""],
+ Credentials: [""],
+ Cashouts: [""],
+ "it doesnt have the pattern of an IBAN number": [""],
+ "it doesnt have the pattern of an email": [""],
+ "should start with +": [""],
+ "phone number can't have other than numbers": [""],
+ "account identification in the bank": [""],
+ "name of the person owner the account": [""],
+ "Internal IBAN": [""],
+ "if empty a random account number will be assigned": [""],
+ "account identification for bank transfer": [""],
+ Phone: [""],
+ "Cashout IBAN": [""],
+ "account number where the money is going to be sent when doing cashouts":
+ [""],
+ "Max debt": [""],
+ "how much is user able to transfer after zero balance": [""],
+ "Is this a Taler Exchange?": [""],
+ "This server doesn't support second factor authentication.": [""],
+ "Enable second factor authentication": [""],
+ "Using email": [""],
+ "Using SMS": [""],
+ "Is this account public?": [""],
+ "public accounts have their balance publicly accesible": [""],
+ "Account updated": [""],
+ "The rights to change the account are not sufficient": [""],
+ "The username was not found": [""],
+ "You can't change the legal name, please contact the your account administrator.":
+ [""],
+ "You can't change the debt limit, please contact the your account administrator.":
+ [""],
+ "You can't change the cashout address, please contact the your account administrator.":
+ [""],
+ "You can't change the contact data, please contact the your account administrator.":
+ [""],
+ 'Account "%1$s"': [""],
+ "Change details": [""],
+ Update: [""],
+ "password doesn't match": [""],
+ "Password changed": [""],
+ "Not authorized to change the password, maybe the session is invalid.": [
+ "",
+ ],
+ "You need to provide the old password. If you don't have it contact your account administrator.":
+ [""],
+ "Your current password doesn't match, can't change to a new password.": [
+ "",
+ ],
+ "Update password": [""],
+ "New password": [""],
+ "Type it again": [""],
+ "repeat the same password": [""],
+ "Current password": [""],
+ "your current password, for security": [""],
+ Change: [""],
+ "Can't delete the account": [""],
+ "The account can't be delete while still holding some balance. First make sure that the owner make a complete cashout.":
+ [""],
+ "Account removed": [""],
+ "No enough permission to delete the account.": [""],
+ "The username was not found.": [""],
+ "Can't delete a reserved username.": [""],
+ "Can't delete an account with balance different than zero.": [""],
+ "name doesn't match": [""],
+ "You are going to remove the account": [""],
+ "This step can't be undone.": [""],
+ 'Deleting account "%1$s"': [""],
+ Verification: [""],
+ "enter the account name that is going to be deleted": [""],
+ 'Account created with password "%1$s". The user must change the password on the next login.':
+ [""],
+ "Server replied that phone or email is invalid": [""],
+ "The rights to perform the operation are not sufficient": [""],
+ "Account username is already taken": [""],
+ "Account id is already taken": [""],
+ "Bank ran out of bonus credit.": [""],
+ "Account username can't be used because is reserved": [""],
+ "Can't create accounts": [""],
+ "Only system admin can create accounts.": [""],
+ "New business account": [""],
+ Create: [""],
+ "Cashout not supported.": [""],
+ "Account not found.": [""],
+ "Latest cashouts": [""],
+ Created: [""],
+ Confirmed: ["Bestätigen"],
+ "Total debit": [""],
+ "Total credit": [""],
+ Status: [""],
+ never: [""],
+ "Cashout for account %1$s": [""],
+ "This cashout not found. Maybe already aborted.": [""],
+ "Cashout not found. It may be also mean that it was already aborted.": [
+ "",
+ ],
+ "Cashout was already confimed.": [""],
+ "Cashout operation is not supported.": [""],
+ "The cashout operation is already aborted.": [""],
+ "Missing destination account.": [""],
+ "Too many failed attempts.": [""],
+ "The code for this cashout is invalid.": [""],
+ "Cashout detail": [""],
+ Debited: [""],
+ Credited: [""],
+ "Enter the confirmation code": [""],
+ Abort: [""],
+ Confirm: ["Bestätigen"],
+ "Unauthorized to make the operation, maybe the session has expired or the password changed.":
+ [""],
+ "The operation was rejected due to insufficient funds.": [""],
+ "Do not show this again": [""],
+ Close: [""],
+ "On this device": [""],
+ 'If you are using a web browser on desktop you should access your wallet with the GNU Taler WebExtension now or click the link if your WebExtension have the "Inject Taler support" option enabled.':
+ [""],
+ Start: [""],
+ "On a mobile phone": [""],
+ "Scan the QR code with your mobile device.": [""],
+ "There is an operation already": [""],
+ "Complete or cancel the operation in": ["Abhebung bestätigen"],
+ "Server responded with an invalid withdraw URI": [""],
+ "Withdraw URI: %1$s": [""],
+ "The operation was rejected due to insufficient funds": [""],
+ "Prepare your wallet": [""],
+ "After using your wallet you will need to confirm or cancel the operation on this site.":
+ [""],
+ "You need a GNU Taler Wallet": [""],
+ "If you don't have one yet you can follow the instruction in": [""],
+ "Send money": [""],
+ "to a %1$s wallet": [""],
+ "Withdraw digital money into your mobile wallet or browser extension": [
+ "",
+ ],
+ "operation ready": [""],
+ "to another bank account": [""],
+ "Make a wire transfer to an account with known bank account number.": [
+ "",
+ ],
+ "Transfer details": [""],
+ "This is a demo bank": [""],
+ "This part of the demo shows how a bank that supports Taler directly would work. In addition to using your own bank account, you can also see the transaction history of some %1$s.":
+ [""],
+ "This part of the demo shows how a bank that supports Taler directly would work.":
+ [""],
+ "Pending account delete operation": [""],
+ "Pending account update operation": [""],
+ "Pending password update operation": [""],
+ "Pending transaction operation": [""],
+ "Pending withdrawal operation": [""],
+ "Pending cashout operation": [""],
+ "You can complete or cancel the operation in": [""],
+ "Download bank stats": [""],
+ "Include hour metric": [""],
+ "Include day metric": [""],
+ "Include month metric": [""],
+ "Include year metric": [""],
+ "Include table header": [""],
+ "Add previous metric for compare": [""],
+ "Fail on first error": [""],
+ Download: [""],
+ "downloading... %1$s": [""],
+ "Download completed": [""],
+ "click here to save the file in your computer": [""],
+ "Challenge not found.": [""],
+ "This user is not authorized to complete this challenge.": [""],
+ "Too many attemps, try another code.": [""],
+ "The confirmation code is wrong, try again.": [""],
+ "The operation expired.": [""],
+ "The operation failed.": [""],
+ "The operation needs another confirmation to complete.": [""],
+ "Account delete": [""],
+ "Account update": [""],
+ "Password update": [""],
+ "Wire transfer": [""],
+ Withdrawal: ["Abhebung bestätigen"],
+ "Confirm the operation": ["Abhebung bestätigen"],
+ "Send again": [""],
+ "Send code": [""],
+ "Operation details": [""],
+ "Challenge details": [""],
+ "Sent at": [""],
+ "To phone": [""],
+ "To email": [""],
+ "Welcome to %1$s!": [""],
+ },
+ },
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=n != 1;",
+ lang: "de",
+ completeness: 4,
+};
diff --git a/packages/bank-ui/src/i18n/uk.po b/packages/bank-ui/src/i18n/uk.po
new file mode 100644
index 000000000..a8b41e32f
--- /dev/null
+++ b/packages/bank-ui/src/i18n/uk.po
@@ -0,0 +1,1743 @@
+# 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/>
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Bank\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"PO-Revision-Date: 2024-03-07 07:04+0000\n"
+"Last-Translator: Tim Vutor <flukes.ostrich0p@icloud.com>\n"
+"Language-Team: Ukrainian <https://weblate.taler.net/projects/gnu-taler/"
+"taler-bank-spa/uk/>\n"
+"Language: uk\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
+"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
+"X-Generator: Weblate 5.2.1\n"
+
+#: src/utils.ts:137
+#, c-format, fuzzy
+msgid "Operation failed, please report"
+msgstr "Помилка операції, повідомте"
+
+#: src/utils.ts:156
+#, c-format
+msgid "Request timeout"
+msgstr "Тайм-аут запиту"
+
+#: src/utils.ts:165
+#, c-format, fuzzy
+msgid "Request throttled"
+msgstr "Запит до сервера перервано"
+
+#: src/utils.ts:174
+#, c-format
+msgid "Malformed response"
+msgstr "Некоректна відповідь"
+
+#: src/utils.ts:183
+#, c-format
+msgid "Network error"
+msgstr "Мережева помилка"
+
+#: src/utils.ts:192
+#, c-format
+msgid "Unexpected request error"
+msgstr "Неочікувана помилка запиту"
+
+#: src/utils.ts:201
+#, c-format
+msgid "Unexpected error"
+msgstr "Неочікувана помилка"
+
+#: src/utils.ts:377
+#, c-format, fuzzy
+msgid "IBAN numbers usually have more that 4 digits"
+msgstr "Номера IBAN зазвичай мають більше 4ьох цифр"
+
+#: src/utils.ts:379
+#, c-format, fuzzy
+msgid "IBAN numbers usually have less that 34 digits"
+msgstr "Номера IBAN зазвичай мають менше 34ьох цифр"
+
+#: src/utils.ts:387
+#, c-format, fuzzy
+msgid "IBAN country code not found"
+msgstr "Код країни IBAN не знайдено"
+
+#: src/utils.ts:401
+#, c-format, fuzzy
+msgid "IBAN number is not valid, checksum is wrong"
+msgstr "Номер IBAN невірний, контрольна сума не сходиться"
+
+#: src/context/config.ts:136
+#, c-format
+msgid ""
+"the bank backend is not supported. supported version \"%1$s\", server version "
+"\"%2$s\""
+msgstr ""
+
+#: src/hooks/preferences.ts:55
+#, c-format
+msgid "Max withdrawal amount"
+msgstr "Максимальна сумма для виведення"
+
+#: src/hooks/preferences.ts:57
+#, c-format
+msgid "Show withdrawal confirmation"
+msgstr "Показати підтвердження виводу"
+
+#: src/hooks/preferences.ts:59
+#, c-format
+msgid "Show demo description"
+msgstr "Показати демо опис"
+
+#: src/hooks/preferences.ts:61
+#, c-format
+msgid "Show install wallet first"
+msgstr ""
+
+#: src/hooks/preferences.ts:63
+#, c-format
+msgid "Use fast withdrawal form"
+msgstr ""
+
+#: src/hooks/preferences.ts:65
+#, c-format
+msgid "Show debug info"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:90
+#, c-format
+msgid "required"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:92
+#, c-format
+msgid "IBAN should have just uppercased letters and numbers"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:98
+#, c-format
+msgid "not valid"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:100
+#, c-format
+msgid "should be greater than 0"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:102
+#, c-format
+msgid "balance is not enough"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:112
+#, c-format
+msgid "does not follow the pattern"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:114
+#, c-format
+msgid "only \"IBAN\" target are supported"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:116
+#, c-format
+msgid "use the \"amount\" parameter to specify the amount to be transferred"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:118
+#, c-format
+msgid "the amount is not valid"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:120
+#, c-format
+msgid "use the \"message\" parameter to specify a reference text for the transfer"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:160
+#, c-format
+msgid "The request was invalid or the payto://-URI used unacceptable features."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:167
+#, c-format
+msgid "Not enough permission to complete the operation."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:174
+#, c-format
+msgid "The destination account \"%1$s\" was not found."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:181
+#, c-format
+msgid "The origin and the destination of the transfer can't be the same."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:188
+#, c-format
+msgid "Your balance is not enough."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:195
+#, c-format
+msgid "The origin account \"%1$s\" was not found."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:212
+#, c-format
+msgid "Wire transfer created!"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:270
+#, c-format
+msgid "Using a form"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:310
+#, c-format
+msgid "Import payto:// URI"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:335
+#, c-format
+msgid "Recipient"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:359
+#, c-format
+msgid "IBAN of the recipient's account"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:369
+#, c-format
+msgid "Transfer subject"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:377
+#, c-format
+msgid "subject"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:390
+#, c-format
+msgid "some text to identify the transfer"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:400
+#, c-format
+msgid "Amount"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:415
+#, c-format
+msgid "amount to transfer"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:425
+#, c-format
+msgid "payto URI:"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:436
+#, c-format
+msgid "uniform resource identifier of the target account"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:437
+#, c-format
+msgid "payto://iban/[receiver-iban]?message=[subject]&amount=[%1$s:X.Y]"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:457
+#, c-format
+msgid "Cancel"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:471
+#, c-format
+msgid "Send"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:71
+#, c-format
+msgid "Missing username"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:75
+#, c-format
+msgid "Missing password"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:104
+#, c-format
+msgid "Wrong credentials for \"%1$s\""
+msgstr ""
+
+#: src/pages/LoginForm.tsx:111
+#, c-format
+msgid "Account not found"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:142
+#, c-format
+msgid "Username"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:156
+#, c-format
+msgid "username of the account"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:175
+#, c-format
+msgid "Password"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:188
+#, c-format
+msgid "password of the account"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:223
+#, c-format
+msgid "Check"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:237
+#, c-format
+msgid "Log in"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:249
+#, c-format
+msgid "Register"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:52
+#, c-format
+msgid "Latest transactions"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:63
+#, c-format
+msgid "Date"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:71
+#, c-format
+msgid "Counterpart"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:75
+#, c-format
+msgid "Subject"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:111
+#, c-format
+msgid "sent"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:112
+#, c-format
+msgid "received"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:127
+#, c-format
+msgid "invalid value"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:136
+#, c-format
+msgid "to"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:136
+#, c-format
+msgid "from"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:202
+#, c-format
+msgid "First page"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:209
+#, c-format
+msgid "Next"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:86
+#, c-format
+msgid "Wire transfer completed!"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:93
+#, c-format
+msgid "The withdrawal has been aborted previously and can't be confirmed"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:100
+#, c-format
+msgid ""
+"The withdrawal operation can't be confirmed before a wallet accepted the "
+"transaction."
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:107
+#, c-format
+msgid "The operation id is invalid."
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:114
+#, c-format
+msgid "The operation was not found."
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:121
+#, c-format
+msgid "Your balance is not enough for the operation."
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:155
+#, c-format
+msgid "The reserve operation has been confirmed previously and can't be aborted"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:186
+#, c-format
+msgid "Confirm the withdrawal operation"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:203
+#, c-format
+msgid "Wire transfer details"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:217
+#, c-format
+msgid "Taler Exchange operator's account"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:228
+#, c-format
+msgid "Taler Exchange operator's name"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:317
+#, c-format
+msgid "Transfer"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:342
+#, c-format
+msgid "Authentication required"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:352
+#, c-format
+msgid "This operation was created with other username"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:209
+#, c-format
+msgid ""
+"Unauthorized to make the operation, maybe the session has expired or the "
+"password changed."
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:218
+#, c-format
+msgid "The operation was rejected due to insufficient funds."
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:268
+#, c-format
+msgid "Withdrawal confirmed"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:272
+#, c-format
+msgid ""
+"The wire transfer to the Taler operator has been initiated. You will soon "
+"receive the requested amount in your Taler wallet."
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:287
+#, c-format
+msgid "Do not show this again"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:319
+#, c-format
+msgid "Close"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:399
+#, c-format
+msgid "On this device"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:404
+#, c-format
+msgid ""
+"If you are using a web browser on desktop you should access your wallet with the "
+"GNU Taler WebExtension now or click the link if your WebExtension have the "
+"\"Inject Taler support\" option enabled."
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:417
+#, c-format
+msgid "Start"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:426
+#, c-format
+msgid "On a mobile phone"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:431
+#, c-format
+msgid "Scan the QR code with your mobile device."
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:73
+#, c-format
+msgid "There is an operation already"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:75
+#, c-format
+msgid "Complete or cancel the operation in"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:84
+#, c-format
+msgid "this page"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:101
+#, c-format
+msgid "invalid"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:116
+#, c-format
+msgid "Server responded with an invalid withdraw URI"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:117
+#, c-format
+msgid "Withdraw URI: %1$s"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:132
+#, c-format
+msgid "The operation was rejected due to insufficient funds"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:253
+#, c-format
+msgid "Continue"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:282
+#, c-format
+msgid "Prepare your wallet"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:285
+#, c-format
+msgid ""
+"After using your wallet you will need to confirm or cancel the operation on this "
+"site."
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:295
+#, c-format
+msgid "You need a GNU Taler Wallet"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:300
+#, c-format
+msgid "If you don't have one yet you can follow the instruction in"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:55
+#, c-format
+msgid "Send money"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:73
+#, c-format
+msgid "to a %1$s wallet"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:95
+#, c-format
+msgid "Withdraw digital money into your mobile wallet or browser extension"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:109
+#, c-format
+msgid "operation ready"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:129
+#, c-format
+msgid "to another bank account"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:149
+#, c-format
+msgid "Make a wire transfer to an account with known bank account number."
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:171
+#, c-format
+msgid "Transfer details"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:41
+#, c-format
+msgid "This is a demo bank"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:46
+#, c-format
+msgid ""
+"This part of the demo shows how a bank that supports Taler directly would work. "
+"In addition to using your own bank account, you can also see the transaction "
+"history of some %1$s."
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:53
+#, c-format
+msgid "This part of the demo shows how a bank that supports Taler directly would work."
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:70
+#, c-format
+msgid "Pending account delete operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:72
+#, c-format
+msgid "Pending account update operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:74
+#, c-format
+msgid "Pending password update operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:76
+#, c-format
+msgid "Pending transaction operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:78
+#, c-format
+msgid "Pending withdrawal operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:80
+#, c-format
+msgid "Pending cashout operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:91
+#, c-format
+msgid "You can complete or cancel the operation in"
+msgstr ""
+
+#: src/pages/BankFrame.tsx:64
+#, c-format
+msgid "Internal error, please report."
+msgstr ""
+
+#: src/pages/BankFrame.tsx:100
+#, c-format
+msgid "Preferences"
+msgstr ""
+
+#: src/pages/BankFrame.tsx:184
+#, c-format
+msgid "Welcome, %1$s"
+msgstr ""
+
+#: src/pages/WireTransfer.tsx:79
+#, c-format
+msgid "Make a wire transfer"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:72
+#, c-format
+msgid "Accounts"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:75
+#, c-format
+msgid "A list of all business account in the bank."
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:86
+#, c-format
+msgid "Create account"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:106
+#, c-format
+msgid "Name"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:110
+#, c-format
+msgid "Balance"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:112
+#, c-format
+msgid "Actions"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:151
+#, c-format
+msgid "unknown"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:170
+#, c-format
+msgid "change password"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:179
+#, c-format
+msgid "cashouts"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:189
+#, c-format
+msgid "remove"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:168
+#, c-format
+msgid "Cashout not implemented"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:184
+#, c-format
+msgid "Select a section"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:202
+#, c-format
+msgid "Last hour"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:208
+#, c-format
+msgid "Last day"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:216
+#, c-format
+msgid "Last month"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:222
+#, c-format
+msgid "Last year"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:310
+#, c-format
+msgid "Last Year"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:325
+#, c-format
+msgid "Trading volume on %1$s compared to %2$s"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:342
+#, c-format
+msgid "Cashin"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:352
+#, c-format
+msgid "Cashout"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:364
+#, c-format
+msgid "Payin"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:374
+#, c-format
+msgid "Payout"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:388
+#, c-format
+msgid "download stats as CSV"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:494
+#, c-format
+msgid "Decreased by"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:498
+#, c-format
+msgid "Increased by"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:89
+#, c-format
+msgid "Download bank stats"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:110
+#, c-format
+msgid "Include hour metric"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:143
+#, c-format
+msgid "Include day metric"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:173
+#, c-format
+msgid "Include month metric"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:206
+#, c-format
+msgid "Include year metric"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:239
+#, c-format
+msgid "Include table header"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:272
+#, c-format
+msgid "Add previous metric for compare"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:307
+#, c-format
+msgid "Fail on first error"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:364
+#, c-format
+msgid "Download"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:381
+#, c-format
+msgid "downloading... %1$s"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:399
+#, c-format
+msgid "Download completed"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:400
+#, c-format
+msgid "click here to save the file in your computer"
+msgstr ""
+
+#: src/pages/PublicHistoriesPage.tsx:78
+#, c-format
+msgid "History of public accounts"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:48
+#, c-format
+msgid "Currently, the bank is not accepting new registrations!"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:87
+#, c-format
+msgid "Missing name"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:91
+#, c-format
+msgid "Use letters and numbers only, and start with a lowercase letter"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:107
+#, c-format
+msgid "Passwords don't match"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:130
+#, c-format
+msgid "Server replied with invalid phone or email."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:137
+#, c-format
+msgid "Registration is disabled because the bank ran out of bonus credit."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:144
+#, c-format
+msgid "No enough permission to create that account."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:151
+#, c-format
+msgid "That account id is already taken."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:158
+#, c-format
+msgid "That username is already taken."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:165
+#, c-format
+msgid "That username can't be used because is reserved."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:172
+#, c-format
+msgid "Only admin is allow to set debt limit."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:179
+#, c-format
+msgid "No information for the selected authentication channel."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:186
+#, c-format
+msgid "Authentication channel is not supported."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:193
+#, c-format
+msgid "Only admin can create accounts with second factor authentication."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:233
+#, c-format
+msgid "Account registration"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:315
+#, c-format
+msgid "Repeat password"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:457
+#, c-format
+msgid "Create a random temporary user"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:110
+#, c-format
+msgid "If you have a Taler wallet installed in this device"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:116
+#, c-format
+msgid ""
+"You will see the details of the operation in your wallet including the fees (if "
+"applies). If you still don't have one you can install it following instructions "
+"in"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:143
+#, c-format
+msgid "Withdraw"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:152
+#, c-format
+msgid "Or if you have the wallet in another device"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:157
+#, c-format
+msgid "Scan the QR below to start the withdrawal."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:79
+#, c-format
+msgid "Operation aborted"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:82
+#, c-format
+msgid ""
+"The wire transfer to the Taler Exchange operator's account was aborted, your "
+"balance was not affected."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:88
+#, c-format
+msgid "You can close this page now or continue to the account page."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:147
+#, c-format
+msgid "Done"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:158
+#, c-format
+msgid "Operation canceled"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:173
+#, c-format
+msgid "The operation is marked as 'selected' but some step in the withdrawal failed"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:175
+#, c-format
+msgid "The account is selected but no withdrawal identification found."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:188
+#, c-format
+msgid ""
+"There is a withdrawal identification but no account has been selected or the "
+"selected account is invalid."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:202
+#, c-format
+msgid ""
+"No withdrawal ID found and no account has been selected or the selected account "
+"is invalid."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:259
+#, c-format
+msgid "Operation not found"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:263
+#, c-format
+msgid ""
+"This operation is not known by the server. The operation id is wrong or the "
+"server deleted the operation information before reaching here."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:278
+#, c-format
+msgid "Cotinue to dashboard"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:98
+#, c-format
+msgid "Cashout not found. It may be also mean that it was already aborted."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:136
+#, c-format
+msgid "Challenge not found."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:143
+#, c-format
+msgid "This user is not authorized to complete this challenge."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:150
+#, c-format
+msgid "Too many attempts, try another code."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:157
+#, c-format
+msgid "The confirmation code is wrong, try again."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:164
+#, c-format
+msgid "The operation expired."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:197
+#, c-format
+msgid "The operation failed."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:212
+#, c-format
+msgid "The operation needs another confirmation to complete."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:224
+#, c-format
+msgid "Account delete"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:226
+#, c-format
+msgid "Account update"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:228
+#, c-format
+msgid "Password update"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:230
+#, c-format
+msgid "Wire transfer"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:232
+#, c-format
+msgid "Withdrawal"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:248
+#, c-format
+msgid "Confirm the operation"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:271
+#, c-format
+msgid "Enter the confirmation code"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:313
+#, c-format
+msgid "Confirm"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:348
+#, c-format
+msgid "Send again"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:359
+#, c-format
+msgid "Send code"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:369
+#, c-format
+msgid "Operation details"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:529
+#, c-format
+msgid "Challenge details"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:536
+#, c-format
+msgid "Sent at"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:551
+#, c-format
+msgid "To phone"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:553
+#, c-format
+msgid "To email"
+msgstr ""
+
+#: src/pages/WithdrawalOperationPage.tsx:49
+#, c-format
+msgid "The Withdrawal URI is not valid"
+msgstr ""
+
+#: src/components/Cashouts/views.tsx:100
+#, c-format
+msgid "Latest cashouts"
+msgstr ""
+
+#: src/components/Cashouts/views.tsx:111
+#, c-format
+msgid "Created"
+msgstr ""
+
+#: src/components/Cashouts/views.tsx:115
+#, c-format
+msgid "Total debit"
+msgstr ""
+
+#: src/components/Cashouts/views.tsx:119
+#, c-format
+msgid "Total credit"
+msgstr ""
+
+#: src/pages/ProfileNavigation.tsx:70
+#, c-format
+msgid "Details"
+msgstr ""
+
+#: src/pages/ProfileNavigation.tsx:74
+#, c-format
+msgid "Delete"
+msgstr ""
+
+#: src/pages/ProfileNavigation.tsx:78
+#, c-format
+msgid "Credentials"
+msgstr ""
+
+#: src/pages/ProfileNavigation.tsx:82
+#, c-format
+msgid "Cashouts"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:95
+#, c-format
+msgid "Unable to create a cashout"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:96
+#, c-format
+msgid "The bank configuration does not support cashout operations."
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:223
+#, c-format
+msgid "need to be higher due to fees"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:225
+#, c-format
+msgid "the total transfer at destination will be zero"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:250
+#, c-format
+msgid "Cashout created"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:272
+#, c-format
+msgid "Duplicated request detected, check if the operation succeeded or try again."
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:279
+#, c-format
+msgid "The conversion rate was incorrectly applied"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:286
+#, c-format
+msgid "The account does not have sufficient funds"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:293
+#, c-format
+msgid "Cashouts are not supported"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:300
+#, c-format
+msgid "Missing cashout URI in the profile"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:307
+#, c-format
+msgid ""
+"Sending the confirmation message failed, retry later or contact the "
+"administrator."
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:339
+#, c-format
+msgid "Conversion rate"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:360
+#, c-format
+msgid "Fee"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:374
+#, c-format
+msgid "To account"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:381
+#, c-format
+msgid "No cashout account"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:382
+#, c-format
+msgid "Before doing a cashout you need to complete your profile"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:440
+#, c-format
+msgid "Amount to send"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:441
+#, c-format
+msgid "Amount to receive"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:490
+#, c-format
+msgid "Total cost"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:505
+#, c-format
+msgid "Balance left"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:520
+#, c-format
+msgid "Before fee"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:533
+#, c-format
+msgid "Total cashout transfer"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:553
+#, c-format
+msgid "No cashout channel available"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:555
+#, c-format
+msgid ""
+"Before doing a cashout the server need to provide an second channel to confirm "
+"the operation"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:567
+#, c-format
+msgid "Second factor authentication"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:598
+#, c-format
+msgid "Email"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:600
+#, c-format
+msgid "add a email in your profile to enable this option"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:646
+#, c-format
+msgid "SMS"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:648
+#, c-format
+msgid "add a phone number in your profile to enable this option"
+msgstr ""
+
+#: src/pages/account/CashoutListForAccount.tsx:52
+#, c-format
+msgid "Cashout for account %1$s"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:165
+#, c-format
+msgid "it doesn't have the pattern of an IBAN number"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:185
+#, c-format
+msgid "it doesn't have the pattern of an email"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:190
+#, c-format
+msgid "should start with +"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:192
+#, c-format
+msgid "phone number can't have other than numbers"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:329
+#, c-format
+msgid "account identification in the bank"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:365
+#, c-format
+msgid "name of the person owner the account"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:374
+#, c-format
+msgid "Internal IBAN"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:377
+#, c-format
+msgid "if empty a random account number will be assigned"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:378
+#, c-format
+msgid "account identification for bank transfer"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:423
+#, c-format
+msgid "Phone"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:451
+#, c-format
+msgid "Cashout IBAN"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:452
+#, c-format
+msgid "account number where the money is going to be sent when doing cashouts"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:470
+#, c-format
+msgid "Max debt"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:494
+#, c-format
+msgid "how much is user able to transfer after zero balance"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:508
+#, c-format
+msgid "Is this a Taler Exchange?"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:549
+#, c-format
+msgid "This server doesn't support second factor authentication."
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:560
+#, c-format
+msgid "Enable second factor authentication"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:596
+#, c-format
+msgid "Using email"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:654
+#, c-format
+msgid "Using SMS"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:691
+#, c-format
+msgid "Is this account public?"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:719
+#, c-format
+msgid "public accounts have their balance publicly accessible"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:100
+#, c-format
+msgid "Account updated"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:107
+#, c-format
+msgid "The rights to change the account are not sufficient"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:114
+#, c-format
+msgid "The username was not found"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:121
+#, c-format
+msgid "You can't change the legal name, please contact the your account administrator."
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:128
+#, c-format
+msgid "You can't change the debt limit, please contact the your account administrator."
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:135
+#, c-format
+msgid ""
+"You can't change the cashout address, please contact the your account "
+"administrator."
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:177
+#, c-format
+msgid "Account \"%1$s\""
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:190
+#, c-format
+msgid "Change details"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:235
+#, c-format
+msgid "Update"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:78
+#, c-format
+msgid "password doesn't match"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:95
+#, c-format
+msgid "Password changed"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:102
+#, c-format
+msgid "Not authorized to change the password, maybe the session is invalid."
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:112
+#, c-format
+msgid ""
+"You need to provide the old password. If you don't have it contact your account "
+"administrator."
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:117
+#, c-format
+msgid "Your current password doesn't match, can't change to a new password."
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:149
+#, c-format
+msgid "Update password"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:167
+#, c-format
+msgid "New password"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:195
+#, c-format
+msgid "Type it again"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:217
+#, c-format
+msgid "repeat the same password"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:227
+#, c-format
+msgid "Current password"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:248
+#, c-format
+msgid "your current password, for security"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:272
+#, c-format
+msgid "Change"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:74
+#, c-format
+msgid ""
+"Account created with password \"%1$s\". The user must change the password on the "
+"next login."
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:83
+#, c-format
+msgid "Server replied that phone or email is invalid"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:90
+#, c-format
+msgid "The rights to perform the operation are not sufficient"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:97
+#, c-format
+msgid "Account username is already taken"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:104
+#, c-format
+msgid "Account id is already taken"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:111
+#, c-format
+msgid "Bank ran out of bonus credit."
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:118
+#, c-format
+msgid "Account username can't be used because is reserved"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:160
+#, c-format
+msgid "Can't create accounts"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:161
+#, c-format
+msgid "Only system admin can create accounts."
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:183
+#, c-format
+msgid "New business account"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:209
+#, c-format
+msgid "Create"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:94
+#, c-format
+msgid "Can't delete the account"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:95
+#, c-format
+msgid ""
+"The account can't be delete while still holding some balance. First make sure "
+"that the owner make a complete cashout."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:117
+#, c-format
+msgid "Account removed"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:124
+#, c-format
+msgid "No enough permission to delete the account."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:131
+#, c-format
+msgid "The username was not found."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:138
+#, c-format
+msgid "Can't delete a reserved username."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:145
+#, c-format
+msgid "Can't delete an account with balance different than zero."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:170
+#, c-format
+msgid "name doesn't match"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:180
+#, c-format
+msgid "You are going to remove the account"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:182
+#, c-format
+msgid "This step can't be undone."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:188
+#, c-format
+msgid "Deleting account \"%1$s\""
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:206
+#, c-format
+msgid "Verification"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:231
+#, c-format
+msgid "enter the account name that is going to be deleted"
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:49
+#, c-format
+msgid "cashout id should be a number"
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:65
+#, c-format
+msgid "This cashout not found. Maybe already aborted."
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:106
+#, c-format
+msgid "Cashout detail"
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:139
+#, c-format
+msgid "Debited"
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:154
+#, c-format
+msgid "Credited"
+msgstr ""
+
+#: src/Routing.tsx:140
+#, c-format
+msgid "Welcome to %1$s!"
+msgstr ""
diff --git a/packages/demobank-ui/src/index.html b/packages/bank-ui/src/index.html
index 4b3c89a66..0789ecf89 100644
--- a/packages/demobank-ui/src/index.html
+++ b/packages/bank-ui/src/index.html
@@ -15,12 +15,13 @@
@author Sebastian Javier Marchano
-->
-<!DOCTYPE html>
-<html lang="en">
+<!doctype html>
+<html lang="en" class="h-full bg-gray-100">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
+ <meta name="taler-support" content="uri,api" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<link
@@ -28,14 +29,13 @@
href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/Qn3//3ryn/795Tf/WrpP/2LCW/8B6T//w4Nb///////Pn4P+/d0v/9u3n/+7d0v/EhV7//v///+HDr//fxLD/zph2/+TJt//8/Pv/woBX//Lm3f/y5dz/v3hN//bu6f/JjGn/4sW0///////Df1j/8OLZ//v6+P+/elH/+vj1//jy7f+/elL//////+zYzP/Eg13//////967p//MlHT/wn5X///////v4Nb/yY1s///////jw7H/06KG////////////z5t9/+fNvf//////x4pn//Pp4v/8+vn/w39X/8WEX///////5s/A/9CbfP//////27Oc/9y2n////////////9itlf/gu6f//////86Vdf/r2Mz//////8SCXP/Df1j//////+7d0v/KkG7//////+HBrf/VpYr////////////RnoH/5sq6///////Ii2n/8ubf//39/P/Cf1j/xohk/+bNvv//////wn5W//Tq4//58/D/wHxV//7+/f/59fH/v3xU//39/P/w4Nf/xIFb///////hw7H/yo9t/+/f1f/AeU3/+/n2/+nSxP/FhmD//////9qzm//Upon/4MSx/96+qf//////xINc/+3bz//48e3/v3hN//Pn3///////6M+//752S//gw6//06aK/8J+VP/kzLr/zZd1/8OCWv/q18r/17KZ/9Ooi//fv6r/v3dK/+vWyP///////v39///////27un/1aeK/9Opjv/m1cf/1KCC/9a0nP/n08T/0Jx8/82YdP/QnHz/16yR//jx7P///////v39///////+/f3///7+///////+//7//v7+///////+/v7//v/+/////////////////////////v7//v79///////////////////+/v/+/Pv//v39///+/v/+/Pv///7+//7+/f/+/Pv//v39//79/P/+/Pv///7+////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
/>
<link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
- <title>Demobank</title>
- <!-- Optional customization script. -->
- <script src="demobank-ui-settings.js"></script>
- <!-- Entry point for the demobank SPA. -->
+ <title>Bank</title>
+ <!-- Entry point for the bank SPA. -->
<script type="module" src="index.js"></script>
<link rel="stylesheet" href="index.css" />
</head>
- <body>
+
+ <body class="h-full">
<div id="app"></div>
</body>
</html>
diff --git a/packages/bank-ui/src/index.tsx b/packages/bank-ui/src/index.tsx
new file mode 100644
index 000000000..f559288a3
--- /dev/null
+++ b/packages/bank-ui/src/index.tsx
@@ -0,0 +1,27 @@
+/*
+ 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 { App } from "./app.js";
+import { h, render } from "preact";
+import "./scss/main.css";
+
+const app = document.getElementById("app");
+
+if (app) {
+ render(<App />, app);
+} else {
+ console.error("HTML element with id 'app' not found.");
+}
diff --git a/packages/demobank-ui/src/manifest.json b/packages/bank-ui/src/manifest.json
index 8790b10c9..8790b10c9 100644
--- a/packages/demobank-ui/src/manifest.json
+++ b/packages/bank-ui/src/manifest.json
diff --git a/packages/bank-ui/src/pages/AccountPage/index.ts b/packages/bank-ui/src/pages/AccountPage/index.ts
new file mode 100644
index 000000000..8a9471ef4
--- /dev/null
+++ b/packages/bank-ui/src/pages/AccountPage/index.ts
@@ -0,0 +1,135 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ AbsoluteTime,
+ AmountJson,
+ TalerCorebankApi,
+ TalerError,
+} from "@gnu-taler/taler-util";
+import { Loading, utils } from "@gnu-taler/web-util/browser";
+import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js";
+import { LoginForm } from "../LoginForm.js";
+import { useComponentState } from "./state.js";
+import { InvalidIbanView, ReadyView } from "./views.js";
+import { RouteDefinition } from "@gnu-taler/web-util/browser";
+
+export interface Props {
+ account: string;
+ onAuthorizationRequired: () => void;
+ onOperationCreated: (wopid: string) => void;
+ onClose: () => void;
+ tab: "charge-wallet" | "wire-transfer" | undefined;
+ routeClose: RouteDefinition;
+ routeCashout: RouteDefinition;
+ routeChargeWallet: RouteDefinition;
+ routeWireTransfer: RouteDefinition<{
+ account?: string;
+ subject?: string;
+ amount?: string;
+ }>;
+ routePublicAccounts: RouteDefinition;
+ routeCreateWireTransfer: RouteDefinition<{
+ account?: string;
+ subject?: string;
+ amount?: string;
+ }>;
+ routeOperationDetails: RouteDefinition<{ wopid: string }>;
+ routeSolveSecondFactor: RouteDefinition;
+}
+
+export type State =
+ | State.Loading
+ | State.LoadingError
+ | State.Ready
+ | State.InvalidIban
+ | State.UserNotFound;
+
+export namespace State {
+ export interface Loading {
+ status: "loading";
+ error: undefined;
+ }
+
+ export interface LoadingError {
+ status: "loading-error";
+ error: TalerError;
+ }
+
+ export interface BaseInfo {
+ error: undefined;
+ }
+
+ export interface Ready extends BaseInfo {
+ status: "ready";
+ error: undefined;
+ account: string;
+ tab: "charge-wallet" | "wire-transfer" | undefined;
+ limit: AmountJson;
+ balance: AmountJson;
+ onAuthorizationRequired: () => void;
+ onOperationCreated: (wopid: string) => void;
+ onClose: () => void;
+ routeClose: RouteDefinition;
+ routeCashout: RouteDefinition;
+ routeChargeWallet: RouteDefinition;
+ routePublicAccounts: RouteDefinition;
+ routeWireTransfer: RouteDefinition<{
+ account?: string;
+ subject?: string;
+ amount?: string;
+ }>;
+ routeCreateWireTransfer: RouteDefinition<{
+ account?: string;
+ subject?: string;
+ amount?: string;
+ }>;
+ routeOperationDetails: RouteDefinition<{ wopid: string }>;
+ routeSolveSecondFactor: RouteDefinition;
+ }
+
+ export interface InvalidIban {
+ status: "invalid-iban";
+ error: TalerCorebankApi.AccountData;
+ }
+
+ export interface UserNotFound {
+ status: "login";
+ reason: "not-found" | "forbidden";
+ routeRegister?: RouteDefinition;
+ }
+}
+
+export interface Transaction {
+ negative: boolean;
+ counterpart: string;
+ when: AbsoluteTime;
+ amount: AmountJson | undefined;
+ subject: string;
+}
+
+const viewMapping: utils.StateViewMap<State> = {
+ loading: Loading,
+ login: LoginForm,
+ "invalid-iban": InvalidIbanView,
+ "loading-error": ErrorLoadingWithDebug,
+ ready: ReadyView,
+};
+
+export const AccountPage = utils.compose(
+ (p: Props) => useComponentState(p),
+ viewMapping,
+);
diff --git a/packages/bank-ui/src/pages/AccountPage/state.ts b/packages/bank-ui/src/pages/AccountPage/state.ts
new file mode 100644
index 000000000..f8b91a2ce
--- /dev/null
+++ b/packages/bank-ui/src/pages/AccountPage/state.ts
@@ -0,0 +1,122 @@
+/*
+ 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 {
+ Amounts,
+ HttpStatusCode,
+ TalerError,
+ assertUnreachable,
+ parsePaytoUri,
+} from "@gnu-taler/taler-util";
+import { useAccountDetails } from "../../hooks/account.js";
+import { Props, State } from "./index.js";
+
+export function useComponentState({
+ account,
+ tab,
+ routeChargeWallet,
+ routeCreateWireTransfer,
+ routePublicAccounts,
+ routeSolveSecondFactor,
+ routeOperationDetails,
+ routeWireTransfer,
+ routeCashout,
+ onOperationCreated,
+ onClose,
+ routeClose,
+ onAuthorizationRequired,
+}: Props): State {
+ const result = useAccountDetails(account);
+
+ if (!result) {
+ return {
+ status: "loading",
+ error: undefined,
+ };
+ }
+
+ if (result instanceof TalerError) {
+ return {
+ status: "loading-error",
+ error: result,
+ };
+ }
+
+ if (result.type === "fail") {
+ switch (result.case) {
+ case HttpStatusCode.Unauthorized:
+ return {
+ status: "login",
+ reason: "forbidden",
+ };
+ case HttpStatusCode.NotFound:
+ return {
+ status: "login",
+ reason: "not-found",
+ };
+ default: {
+ assertUnreachable(result);
+ }
+ }
+ }
+
+ const { body: data } = result;
+
+ const balance = Amounts.parseOrThrow(data.balance.amount);
+
+ const debitThreshold = Amounts.parseOrThrow(data.debit_threshold);
+ const payto = parsePaytoUri(data.payto_uri);
+
+ if (
+ !payto ||
+ !payto.isKnown ||
+ (payto.targetType !== "iban" && payto.targetType !== "x-taler-bank")
+ ) {
+ return {
+ status: "invalid-iban",
+ error: data,
+ };
+ }
+
+ const balanceIsDebit = data.balance.credit_debit_indicator == "debit";
+ const limit = balanceIsDebit
+ ? Amounts.sub(debitThreshold, balance).amount
+ : Amounts.add(balance, debitThreshold).amount;
+
+ const positiveBalance = balanceIsDebit
+ ? Amounts.zeroOfAmount(balance)
+ : balance;
+
+ return {
+ status: "ready",
+ onOperationCreated,
+ error: undefined,
+ tab,
+ routeCashout,
+ routeOperationDetails,
+ routeCreateWireTransfer,
+ routePublicAccounts,
+ routeSolveSecondFactor,
+ onAuthorizationRequired,
+ onClose,
+ routeClose,
+ routeChargeWallet,
+ routeWireTransfer,
+ account,
+ limit,
+ balance: positiveBalance,
+ };
+}
diff --git a/packages/bank-ui/src/pages/AccountPage/stories.tsx b/packages/bank-ui/src/pages/AccountPage/stories.tsx
new file mode 100644
index 000000000..fe09a4f89
--- /dev/null
+++ b/packages/bank-ui/src/pages/AccountPage/stories.tsx
@@ -0,0 +1,29 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import * as tests from "@gnu-taler/web-util/testing";
+import { ReadyView } from "./views.js";
+
+export default {
+ title: "account page",
+};
+
+export const Ready = tests.createExample(ReadyView, {});
diff --git a/packages/bank-ui/src/pages/AccountPage/test.ts b/packages/bank-ui/src/pages/AccountPage/test.ts
new file mode 100644
index 000000000..14c8be948
--- /dev/null
+++ b/packages/bank-ui/src/pages/AccountPage/test.ts
@@ -0,0 +1,31 @@
+/*
+ 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/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+// import * as tests from "@gnu-taler/web-util/testing";
+// import { SwrMockEnvironment } from "@gnu-taler/web-util/testing";
+// import { expect } from "chai";
+// import { CASHOUT_API_EXAMPLE } from "../../endpoints.js";
+// import { Props } from "./index.js";
+// import { useComponentState } from "./state.js";
+
+describe("Account states", () => {
+ it("should do some tests", async () => {});
+});
diff --git a/packages/bank-ui/src/pages/AccountPage/views.tsx b/packages/bank-ui/src/pages/AccountPage/views.tsx
new file mode 100644
index 000000000..42892f536
--- /dev/null
+++ b/packages/bank-ui/src/pages/AccountPage/views.tsx
@@ -0,0 +1,156 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { TranslatedString } from "@gnu-taler/taler-util";
+import { Attention, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { Transactions } from "../../components/Transactions/index.js";
+import { useBankState } from "../../hooks/bank-state.js";
+import { usePreferences } from "../../hooks/preferences.js";
+import { PaymentOptions } from "../PaymentOptions.js";
+import { State } from "./index.js";
+import { RouteDefinition } from "@gnu-taler/web-util/browser";
+
+export function InvalidIbanView({ error }: State.InvalidIban) {
+ return (
+ <div>Payto from server is not valid &quot;{error.payto_uri}&quot;</div>
+ );
+}
+
+const IS_PUBLIC_ACCOUNT_ENABLED = false;
+
+function ShowDemoInfo({
+ routePublicAccounts,
+}: {
+ routePublicAccounts: RouteDefinition;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const [settings, updateSettings] = usePreferences();
+ if (!settings.showDemoDescription) return <Fragment />;
+ return (
+ <Attention
+ title={i18n.str`This is a demo bank`}
+ onClose={() => {
+ updateSettings("showDemoDescription", false);
+ }}
+ >
+ {IS_PUBLIC_ACCOUNT_ENABLED ? (
+ <i18n.Translate>
+ This part of the demo shows how a bank that supports Taler directly
+ would work. In addition to using your own bank account, you can also
+ see the transaction history of some{" "}
+ <a name="public account" href={routePublicAccounts.url({})}>
+ Public Accounts
+ </a>
+ .
+ </i18n.Translate>
+ ) : (
+ <i18n.Translate>
+ This part of the demo shows how a bank that supports Taler directly
+ would work.
+ </i18n.Translate>
+ )}
+ </Attention>
+ );
+}
+
+function ShowPedingOperation({
+ routeSolveSecondFactor,
+}: {
+ routeSolveSecondFactor: RouteDefinition;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const [bankState, updateBankState] = useBankState();
+ if (!bankState.currentChallenge) return <Fragment />;
+ const title = ((op): TranslatedString => {
+ switch (op) {
+ case "delete-account":
+ return i18n.str`Pending account delete operation`;
+ case "update-account":
+ return i18n.str`Pending account update operation`;
+ case "update-password":
+ return i18n.str`Pending password update operation`;
+ case "create-transaction":
+ return i18n.str`Pending transaction operation`;
+ case "confirm-withdrawal":
+ return i18n.str`Pending withdrawal operation`;
+ case "create-cashout":
+ return i18n.str`Pending cashout operation`;
+ }
+ })(bankState.currentChallenge.operation);
+ return (
+ <Attention
+ title={title}
+ type="warning"
+ onClose={() => {
+ updateBankState("currentChallenge", undefined);
+ }}
+ >
+ <i18n.Translate>
+ You can complete or cancel the operation in
+ </i18n.Translate>{" "}
+ <a
+ class="font-semibold text-yellow-700 hover:text-yellow-600"
+ name="complete operation"
+ href={routeSolveSecondFactor.url({})}
+ >
+ <i18n.Translate>this page</i18n.Translate>
+ </a>
+ </Attention>
+ );
+}
+
+export function ReadyView({
+ tab,
+ account,
+ routeChargeWallet,
+ routeWireTransfer,
+ limit,
+ balance,
+ routeCashout,
+ routeCreateWireTransfer,
+ routePublicAccounts,
+ routeOperationDetails,
+ routeSolveSecondFactor,
+ onClose,
+ routeClose,
+ onOperationCreated,
+ onAuthorizationRequired,
+}: State.Ready): VNode {
+ return (
+ <Fragment>
+ <ShowPedingOperation routeSolveSecondFactor={routeSolveSecondFactor} />
+ <ShowDemoInfo routePublicAccounts={routePublicAccounts} />
+ <PaymentOptions
+ tab={tab}
+ routeOperationDetails={routeOperationDetails}
+ routeCashout={routeCashout}
+ routeChargeWallet={routeChargeWallet}
+ routeWireTransfer={routeWireTransfer}
+ limit={limit}
+ balance={balance}
+ routeClose={routeClose}
+ onClose={onClose}
+ onOperationCreated={onOperationCreated}
+ onAuthorizationRequired={onAuthorizationRequired}
+ />
+ <Transactions
+ account={account}
+ routeCreateWireTransfer={routeCreateWireTransfer}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/bank-ui/src/pages/BankFrame.stories.tsx b/packages/bank-ui/src/pages/BankFrame.stories.tsx
new file mode 100644
index 000000000..c874ac4ca
--- /dev/null
+++ b/packages/bank-ui/src/pages/BankFrame.stories.tsx
@@ -0,0 +1,29 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import * as tests from "@gnu-taler/web-util/testing";
+import { BankFrame } from "./BankFrame.js";
+
+export default {
+ title: "bank frame",
+};
+
+export const Ready = tests.createExample(BankFrame, {});
diff --git a/packages/bank-ui/src/pages/BankFrame.tsx b/packages/bank-ui/src/pages/BankFrame.tsx
new file mode 100644
index 000000000..db757ee07
--- /dev/null
+++ b/packages/bank-ui/src/pages/BankFrame.tsx
@@ -0,0 +1,368 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ AbsoluteTime,
+ Amounts,
+ ObservabilityEventType,
+ TalerError,
+ TranslatedString,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
+import {
+ Footer,
+ Header,
+ Loading,
+ RouteDefinition,
+ ToastBanner,
+ notifyError,
+ notifyException,
+ useBankCoreApiContext,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { ComponentChildren, Fragment, VNode, h } from "preact";
+import { useEffect, useErrorBoundary, useState } from "preact/hooks";
+import { privatePages } from "../Routing.js";
+import { useSettingsContext } from "../context/settings.js";
+import { useAccountDetails } from "../hooks/account.js";
+import { useBankState } from "../hooks/bank-state.js";
+import {
+ getAllBooleanPreferences,
+ getLabelForPreferences,
+ usePreferences,
+} from "../hooks/preferences.js";
+import { useSessionState } from "../hooks/session.js";
+import { RenderAmount } from "./PaytoWireTransferForm.js";
+
+const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined;
+const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined;
+
+export function BankFrame({
+ children,
+ account,
+ routeAccountDetails,
+}: {
+ account?: string;
+ routeAccountDetails?: RouteDefinition;
+ children: ComponentChildren;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const session = useSessionState();
+ const settings = useSettingsContext();
+ const [preferences, updatePreferences] = usePreferences();
+ const [, , resetBankState] = useBankState();
+
+ const [error, resetError] = useErrorBoundary();
+
+ useEffect(() => {
+ if (error) {
+ if (error instanceof Error) {
+ console.log("Internal error, please report", error);
+ notifyException(i18n.str`Internal error, please report.`, error);
+ } else {
+ console.log("Internal error, please report", error);
+ notifyError(
+ i18n.str`Internal error, please report.`,
+ String(error) as TranslatedString,
+ );
+ }
+ resetError();
+ }
+ }, [error]);
+
+ return (
+ <div
+ class="min-h-full flex flex-col m-0 bg-slate-200"
+ style="min-height: 100vh;"
+ >
+ <div class="bg-indigo-600 pb-32">
+ <Header
+ title="Bank"
+ iconLinkURL={settings.iconLinkURL ?? "#"}
+ profileURL={routeAccountDetails?.url({})}
+ notificationURL={
+ preferences.showDebugInfo
+ ? privatePages.notifications.url({})
+ : undefined
+ }
+ onLogout={
+ session.state.status !== "loggedIn"
+ ? undefined
+ : () => {
+ session.logOut();
+ resetBankState();
+ }
+ }
+ sites={
+ !settings.topNavSites ? [] : Object.entries(settings.topNavSites)
+ }
+ supportedLangs={["en", "es", "de"]}
+ >
+ <li>
+ <div class="text-xs font-semibold leading-6 text-gray-400">
+ <i18n.Translate>Preferences</i18n.Translate>
+ </div>
+ <ul role="list" class="space-y-4">
+ {getAllBooleanPreferences().map((set) => {
+ const isOn: boolean = !!preferences[set];
+ return (
+ <li key={set} class="pl-2">
+ <div class="flex items-center justify-between">
+ <span class="flex flex-grow flex-col">
+ <span
+ class="text-sm text-black font-medium leading-6 "
+ id="availability-label"
+ >
+ {getLabelForPreferences(set, i18n)}
+ </span>
+ </span>
+ <button
+ type="button"
+ name={`${set} switch`}
+ data-enabled={isOn}
+ class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2"
+ role="switch"
+ aria-checked="false"
+ aria-labelledby="availability-label"
+ aria-describedby="availability-description"
+ onClick={() => {
+ updatePreferences(set, !isOn);
+ }}
+ >
+ <span
+ aria-hidden="true"
+ data-enabled={isOn}
+ class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
+ ></span>
+ </button>
+ </div>
+ </li>
+ );
+ })}
+ </ul>
+ </li>
+ </Header>
+ </div>
+
+ <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>
+
+ <main class="-mt-32 flex-1">
+ {account && routeAccountDetails && (
+ <header class="py-6 bg-indigo-600">
+ <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
+ <h1 class=" flex flex-wrap items-center justify-between sm:flex-nowrap">
+ <span class="text-2xl font-bold tracking-tight text-white">
+ <WelcomeAccount
+ account={account}
+ routeAccountDetails={routeAccountDetails}
+ />
+ </span>
+ <span class="text-2xl font-bold tracking-tight text-white">
+ <AccountBalance account={account} />
+ </span>
+ </h1>
+ </div>
+ </header>
+ )}
+
+ <div class="mx-auto max-w-7xl px-4 pb-4 sm:px-6 lg:px-8">
+ <div class="rounded-lg bg-white px-5 py-6 shadow sm:px-6">
+ {children}
+ </div>
+ </div>
+ </main>
+
+ <AppActivity />
+
+ <Footer
+ testingUrlKey="corebank-api-base-url"
+ GIT_HASH={GIT_HASH}
+ VERSION={VERSION}
+ />
+ </div>
+ );
+}
+
+function Wait({ class: clazz }: { class?: string }): VNode {
+ return (
+ <Fragment>
+ <style>{`
+ .animated-loader {
+ display: inline-block;
+ --b: 5px;
+ border-radius: 50%;
+ aspect-ratio: 1;
+ padding: 1px;
+ background: conic-gradient(#0000 10%,#4f46e5) content-box;
+ -webkit-mask:
+ repeating-conic-gradient(#0000 0deg,#000 1deg 20deg,#0000 21deg 36deg),
+ radial-gradient(farthest-side,#0000 calc(100% - var(--b) - 1px),#000 calc(100% - var(--b)));
+ -webkit-mask-composite: destination-in;
+ mask-composite: intersect;
+ animation:spinning-loader 1s infinite steps(10);
+ }
+ @keyframes spinning-loader {to{transform: rotate(1turn)}}
+ `}</style>
+ <div class={`animated-loader ${clazz}`} />
+ </Fragment>
+ );
+}
+
+function AppActivity(): VNode {
+ const [lastEvent, setLastEvent] = useState<{
+ url: string;
+ id: string;
+ when: AbsoluteTime;
+ }>();
+ const [status, setStatus] = useState<"ok" | "fail">();
+ const d = useBankCoreApiContext();
+ const onBackendActivity = !d ? undefined : d.onActivity;
+ const cancelRequest = !d ? undefined : d.cancelRequest;
+ const [pref] = usePreferences();
+ useEffect(() => {
+ // console.log("ASDASDS", onBackendActivity)
+ if (!pref.showDebugInfo) return;
+ if (!onBackendActivity) return;
+ return onBackendActivity((ev) => {
+ switch (ev.type) {
+ case ObservabilityEventType.HttpFetchStart: {
+ setLastEvent(ev);
+ setStatus(undefined);
+ return;
+ }
+ case ObservabilityEventType.HttpFetchFinishError: {
+ setStatus("fail");
+ return;
+ }
+ case ObservabilityEventType.HttpFetchFinishSuccess: {
+ setStatus("ok");
+ return;
+ }
+ /**
+ * all of this are ignored
+ */
+ case ObservabilityEventType.DbQueryStart:
+ case ObservabilityEventType.DbQueryFinishSuccess:
+ case ObservabilityEventType.DbQueryFinishError:
+ case ObservabilityEventType.RequestStart:
+ case ObservabilityEventType.RequestFinishSuccess:
+ case ObservabilityEventType.RequestFinishError:
+ case ObservabilityEventType.TaskStart:
+ case ObservabilityEventType.TaskStop:
+ case ObservabilityEventType.TaskReset:
+ case ObservabilityEventType.ShepherdTaskResult:
+ case ObservabilityEventType.DeclareTaskDependency:
+ case ObservabilityEventType.CryptoStart:
+ case ObservabilityEventType.CryptoFinishSuccess:
+ case ObservabilityEventType.CryptoFinishError:
+ case ObservabilityEventType.Message:
+ return;
+ default: {
+ assertUnreachable(ev);
+ }
+ }
+ });
+ });
+ if (!pref.showDebugInfo || !lastEvent) return <Fragment />;
+ return (
+ <div
+ data-status={status}
+ class="fixed z-20 bottom-0 w-full ease-in-out delay-1000 transition-transform data-[status=ok]:scale-y-0"
+ >
+ <div
+ data-status={status}
+ class="mx-auto w-4/5 center flex p-1 bg-gray-300 m-1 data-[status=fail]:bg-red-200 data-[status=ok]:bg-green-200 "
+ >
+ {!status ? <Wait class="w-6 h-6" /> : <div class="w-6 h-6" />}
+
+ <p class="ml-2 my-auto text-sm text-gray-500">{lastEvent.url}</p>
+ {!status ? (
+ <button
+ onClick={() => {
+ if (cancelRequest) cancelRequest(lastEvent.id);
+ }}
+ >
+ cancel
+ </button>
+ ) : undefined}
+ </div>
+ </div>
+ );
+}
+
+function WelcomeAccount({
+ account,
+ routeAccountDetails,
+}: {
+ account: string;
+ routeAccountDetails: RouteDefinition;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const result = useAccountDetails(account);
+ if (!result) {
+ return <Loading />;
+ }
+ if (result instanceof TalerError) {
+ return <div />;
+ }
+ if (result.type === "fail") {
+ return (
+ <a
+ name="account details"
+ href={routeAccountDetails.url({})}
+ class="underline underline-offset-2"
+ >
+ <i18n.Translate>Welcome</i18n.Translate>
+ </a>
+ );
+ }
+ return (
+ <a
+ name="account details"
+ href={routeAccountDetails.url({})}
+ class="underline underline-offset-2"
+ >
+ <i18n.Translate>
+ Welcome, <span class="whitespace-nowrap">{result.body.name}</span>
+ </i18n.Translate>
+ </a>
+ );
+}
+
+function AccountBalance({ account }: { account: string }): VNode {
+ const result = useAccountDetails(account);
+ const { config } = useBankCoreApiContext();
+ if (!result) {
+ return <Loading />;
+ }
+ if (result instanceof TalerError) {
+ return <div />;
+ }
+ if (result.type === "fail") return <div />;
+
+ return (
+ <RenderAmount
+ value={Amounts.parseOrThrow(result.body.balance.amount)}
+ negative={result.body.balance.credit_debit_indicator === "debit"}
+ spec={config.currency_specification}
+ />
+ );
+}
diff --git a/packages/bank-ui/src/pages/LoginForm.tsx b/packages/bank-ui/src/pages/LoginForm.tsx
new file mode 100644
index 000000000..2f967895c
--- /dev/null
+++ b/packages/bank-ui/src/pages/LoginForm.tsx
@@ -0,0 +1,230 @@
+/*
+ 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 { HttpStatusCode } from "@gnu-taler/taler-util";
+import {
+ Button,
+ LocalNotificationBanner,
+ ShowInputErrorLabel,
+ useLocalNotificationHandler,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
+import { useEffect, useRef, useState } from "preact/hooks";
+import { useBankCoreApiContext } from "@gnu-taler/web-util/browser";
+import { useSessionState } from "../hooks/session.js";
+import { RouteDefinition } from "@gnu-taler/web-util/browser";
+import { undefinedIfEmpty } from "../utils.js";
+import { doAutoFocus } from "./PaytoWireTransferForm.js";
+import { USERNAME_REGEX } from "./RegistrationPage.js";
+
+/**
+ * Collect and submit login data.
+ */
+export function LoginForm({
+ currentUser,
+ fixedUser,
+ routeRegister,
+}: {
+ fixedUser?: boolean;
+ currentUser?: string;
+ routeRegister?: RouteDefinition;
+}): VNode {
+ const session = useSessionState();
+
+ const sessionUser =
+ session.state.status !== "loggedOut" ? session.state.username : undefined;
+ const [username, setUsername] = useState<string | undefined>(
+ currentUser ?? sessionUser,
+ );
+ const [password, setPassword] = useState<string | undefined>();
+ const { i18n } = useTranslationContext();
+ const {
+ lib: { auth: authenticator },
+ } = useBankCoreApiContext();
+ const [notification, withErrorHandler] = useLocalNotificationHandler();
+ const { config } = useBankCoreApiContext();
+
+ const ref = useRef<HTMLInputElement>(null);
+ useEffect(function focusInput() {
+ ref.current?.focus();
+ }, []);
+
+ const errors = undefinedIfEmpty({
+ username: !username
+ ? i18n.str`Missing username`
+ : !USERNAME_REGEX.test(username)
+ ? i18n.str`Use letters, numbers or any of these characters: - . _ ~`
+ : undefined,
+ password: !password ? i18n.str`Missing password` : undefined,
+ });
+
+ async function doLogout() {
+ session.logOut();
+ }
+
+ const loginHandler =
+ !username || !password
+ ? undefined
+ : withErrorHandler(
+ async () =>
+ authenticator(username).createAccessTokenBasic(username, password, {
+ scope: "readwrite",
+ duration: { d_us: "forever" },
+ refreshable: true,
+ }),
+ (result) => {
+ session.logIn({ username, token: result.body.access_token });
+ },
+ (fail) => {
+ switch (fail.case) {
+ case HttpStatusCode.Unauthorized:
+ return i18n.str`Wrong credentials for "${username}"`;
+ case HttpStatusCode.NotFound:
+ return i18n.str`Account not found`;
+ }
+ },
+ );
+
+ return (
+ <div class="flex min-h-full flex-col justify-center ">
+ <LocalNotificationBanner notification={notification} />
+ <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
+ <form
+ class="space-y-6"
+ noValidate
+ onSubmit={(e) => {
+ e.preventDefault();
+ }}
+ autoCapitalize="none"
+ autoCorrect="off"
+ >
+ <div>
+ <label
+ for="username"
+ class="block text-sm font-medium leading-6 text-gray-900"
+ >
+ <i18n.Translate>Username</i18n.Translate>
+ </label>
+ <div class="mt-2">
+ <input
+ ref={doAutoFocus}
+ type="text"
+ name="username"
+ id="username"
+ class="block w-full disabled:bg-gray-200 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ value={username ?? ""}
+ disabled={fixedUser}
+ enterkeyhint="next"
+ placeholder="identification"
+ autocomplete="username"
+ title={i18n.str`Username of the account`}
+ required
+ onInput={(e): void => {
+ setUsername(e.currentTarget.value);
+ }}
+ />
+ <ShowInputErrorLabel
+ message={errors?.username}
+ isDirty={username !== undefined}
+ />
+ </div>
+ </div>
+
+ <div>
+ <div class="flex items-center justify-between">
+ <label
+ for="password"
+ class="block text-sm font-medium leading-6 text-gray-900"
+ >
+ <i18n.Translate>Password</i18n.Translate>
+ </label>
+ </div>
+ <div class="mt-2">
+ <input
+ type="password"
+ name="password"
+ id="password"
+ autocomplete="current-password"
+ class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ enterkeyhint="send"
+ value={password ?? ""}
+ placeholder="Password"
+ title={i18n.str`Password of the account`}
+ required
+ onInput={(e): void => {
+ setPassword(e.currentTarget.value);
+ }}
+ />
+ <ShowInputErrorLabel
+ message={errors?.password}
+ isDirty={password !== undefined}
+ />
+ </div>
+ </div>
+
+ {session.state.status !== "loggedOut" ? (
+ <div class="flex justify-between">
+ <button
+ type="submit"
+ name="cancel"
+ class="rounded-md bg-white-600 px-3 py-1.5 text-sm font-semibold leading-6 text-black shadow-sm hover:bg-gray-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-600"
+ onClick={(e) => {
+ e.preventDefault();
+ doLogout();
+ }}
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+
+ <Button
+ type="submit"
+ name="check"
+ class="rounded-md bg-indigo-600 disabled:bg-gray-300 px-3 py-1.5 text-sm font-semibold leading-6 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"
+ disabled={!!errors}
+ handler={loginHandler}
+ >
+ <i18n.Translate>Check</i18n.Translate>
+ </Button>
+ </div>
+ ) : (
+ <div>
+ <Button
+ type="submit"
+ name="login"
+ class="flex w-full justify-center rounded-md bg-indigo-600 disabled:bg-gray-300 px-3 py-1.5 text-sm font-semibold leading-6 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"
+ disabled={!!errors}
+ handler={loginHandler}
+ >
+ <i18n.Translate>Log in</i18n.Translate>
+ </Button>
+ </div>
+ )}
+ </form>
+
+ {config.allow_registrations && routeRegister && (
+ <a
+ name="register"
+ href={routeRegister.url({})}
+ class="flex justify-center border-t mt-4 rounded-md bg-blue-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
+ >
+ <i18n.Translate>Register</i18n.Translate>
+ </a>
+ )}
+ </div>
+ </div>
+ );
+}
diff --git a/packages/bank-ui/src/pages/OperationState/index.ts b/packages/bank-ui/src/pages/OperationState/index.ts
new file mode 100644
index 000000000..38f698a04
--- /dev/null
+++ b/packages/bank-ui/src/pages/OperationState/index.ts
@@ -0,0 +1,157 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ AbsoluteTime,
+ AmountJson,
+ TalerCoreBankErrorsByMethod,
+ TalerError,
+ WithdrawUriResult,
+} from "@gnu-taler/taler-util";
+import { Loading, utils } from "@gnu-taler/web-util/browser";
+import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js";
+import { useComponentState } from "./state.js";
+import {
+ AbortedView,
+ ConfirmedView,
+ FailedView,
+ InvalidPaytoView,
+ InvalidReserveView,
+ InvalidWithdrawalView,
+ NeedConfirmationView,
+ ReadyView,
+} from "./views.js";
+import { RouteDefinition } from "@gnu-taler/web-util/browser";
+
+export interface Props {
+ currency: string;
+ onAuthorizationRequired: () => void;
+ routeClose: RouteDefinition;
+ onAbort: () => void;
+ routeHere: RouteDefinition<{ wopid: string }>;
+}
+
+export type State =
+ | State.Loading
+ | State.LoadingError
+ | State.Ready
+ | State.Failed
+ | State.Aborted
+ | State.Confirmed
+ | State.InvalidPayto
+ | State.InvalidWithdrawal
+ | State.InvalidReserve
+ | State.NeedConfirmation;
+
+export namespace State {
+ export interface Loading {
+ status: "loading";
+ error: undefined;
+ }
+
+ export interface Failed {
+ status: "failed";
+ error: TalerCoreBankErrorsByMethod<"createWithdrawal">;
+ }
+
+ export interface LoadingError {
+ status: "loading-error";
+ error: TalerError;
+ }
+
+ /**
+ * Need to open the wallet
+ */
+ export interface Ready {
+ status: "ready";
+ error: undefined;
+ uri: WithdrawUriResult;
+ onAbort: () => Promise<
+ TalerCoreBankErrorsByMethod<"abortWithdrawalById"> | undefined
+ >;
+ routeClose: RouteDefinition;
+ }
+
+ export interface InvalidPayto {
+ status: "invalid-payto";
+ error: undefined;
+ payto: string | undefined;
+ }
+ export interface InvalidWithdrawal {
+ status: "invalid-withdrawal";
+ error: undefined;
+ uri: string;
+ }
+ export interface InvalidReserve {
+ status: "invalid-reserve";
+ error: undefined;
+ reserve: string | undefined;
+ }
+ export interface NeedConfirmation {
+ status: "need-confirmation";
+ onAuthorizationRequired: () => void;
+ account: string;
+ routeHere: RouteDefinition<{ wopid: string }>;
+ onAbort:
+ | undefined
+ | (() => Promise<
+ TalerCoreBankErrorsByMethod<"abortWithdrawalById"> | undefined
+ >);
+ onConfirm:
+ | undefined
+ | (() => Promise<
+ TalerCoreBankErrorsByMethod<"confirmWithdrawalById"> | undefined
+ >);
+ error: undefined;
+ id: string;
+ }
+ export interface Aborted {
+ status: "aborted";
+ error: undefined;
+ routeClose: RouteDefinition;
+ }
+ export interface Confirmed {
+ status: "confirmed";
+ error: undefined;
+ routeClose: RouteDefinition;
+ }
+}
+
+export interface Transaction {
+ negative: boolean;
+ counterpart: string;
+ when: AbsoluteTime;
+ amount: AmountJson | undefined;
+ subject: string;
+}
+
+const viewMapping: utils.StateViewMap<State> = {
+ loading: Loading,
+ failed: FailedView,
+ "invalid-payto": InvalidPaytoView,
+ "invalid-withdrawal": InvalidWithdrawalView,
+ "invalid-reserve": InvalidReserveView,
+ "need-confirmation": NeedConfirmationView,
+ aborted: AbortedView,
+ confirmed: ConfirmedView,
+ "loading-error": ErrorLoadingWithDebug,
+ ready: ReadyView,
+};
+
+export const OperationState = utils.compose(
+ (p: Props) => useComponentState(p),
+ viewMapping,
+);
diff --git a/packages/bank-ui/src/pages/OperationState/state.ts b/packages/bank-ui/src/pages/OperationState/state.ts
new file mode 100644
index 000000000..19c097d18
--- /dev/null
+++ b/packages/bank-ui/src/pages/OperationState/state.ts
@@ -0,0 +1,234 @@
+/*
+ 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 {
+ Amounts,
+ HttpStatusCode,
+ TalerCoreBankErrorsByMethod,
+ TalerError,
+ assertUnreachable,
+ parsePaytoUri,
+ parseWithdrawUri,
+ stringifyWithdrawUri,
+} from "@gnu-taler/taler-util";
+import { utils } from "@gnu-taler/web-util/browser";
+import { useEffect, useState } from "preact/hooks";
+import { mutate } from "swr";
+import { useBankCoreApiContext } from "@gnu-taler/web-util/browser";
+import { useWithdrawalDetails } from "../../hooks/account.js";
+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";
+
+export function useComponentState({
+ currency,
+ routeClose,
+ onAbort,
+ routeHere,
+ onAuthorizationRequired,
+}: Props): utils.RecursiveState<State> {
+ const [settings] = usePreferences();
+ const [bankState, updateBankState] = useBankState();
+ const { state: credentials } = useSessionState();
+ const creds = credentials.status !== "loggedIn" ? undefined : credentials;
+ const {
+ lib: { bank },
+ } = useBankCoreApiContext();
+
+ const [failure, setFailure] = useState<
+ TalerCoreBankErrorsByMethod<"createWithdrawal"> | undefined
+ >();
+ const amount = settings.maxWithdrawalAmount;
+
+ async function doSilentStart() {
+ // FIXME: if amount is not enough use balance
+ const parsedAmount = Amounts.parseOrThrow(`${currency}:${amount}`);
+ if (!creds) return;
+ const resp = await bank.createWithdrawal(creds, {
+ amount: Amounts.stringify(parsedAmount),
+ });
+ if (resp.type === "fail") {
+ setFailure(resp);
+ return;
+ }
+ updateBankState("currentWithdrawalOperationId", resp.body.withdrawal_id);
+ }
+
+ const withdrawalOperationId = bankState.currentWithdrawalOperationId;
+ useEffect(() => {
+ if (withdrawalOperationId === undefined) {
+ doSilentStart();
+ }
+ }, [settings.fastWithdrawal, amount]);
+
+ if (failure) {
+ return {
+ status: "failed",
+ error: failure,
+ };
+ }
+
+ if (!withdrawalOperationId) {
+ return {
+ status: "loading",
+ error: undefined,
+ };
+ }
+
+ const wid = withdrawalOperationId;
+
+ async function doAbort() {
+ if (!creds) return;
+ const resp = await bank.abortWithdrawalById(creds, wid);
+ if (resp.type === "ok") {
+ // updateBankState("currentWithdrawalOperationId", undefined)
+ onAbort();
+ } else {
+ return resp;
+ }
+ }
+
+ async function doConfirm(): Promise<
+ TalerCoreBankErrorsByMethod<"confirmWithdrawalById"> | undefined
+ > {
+ if (!creds) return;
+ const resp = await bank.confirmWithdrawalById(creds, wid);
+ if (resp.type === "ok") {
+ mutate(() => true); //clean withdrawal state
+ } else {
+ return resp;
+ }
+ }
+
+ const uri = stringifyWithdrawUri({
+ bankIntegrationApiBaseUrl: bank.getIntegrationAPI().href,
+ withdrawalOperationId,
+ });
+ const parsedUri = parseWithdrawUri(uri);
+ if (!parsedUri) {
+ return {
+ status: "invalid-withdrawal",
+ error: undefined,
+ uri,
+ };
+ }
+
+ return (): utils.RecursiveState<State> => {
+ const result = useWithdrawalDetails(withdrawalOperationId);
+ const shouldCreateNewOperation = result && !(result instanceof TalerError);
+
+ useEffect(() => {
+ if (shouldCreateNewOperation) {
+ doSilentStart();
+ }
+ }, []);
+ if (!result) {
+ return {
+ status: "loading",
+ error: undefined,
+ };
+ }
+ if (result instanceof TalerError) {
+ return {
+ status: "loading-error",
+ error: result,
+ };
+ }
+
+ if (result.type === "fail") {
+ switch (result.case) {
+ case HttpStatusCode.BadRequest:
+ case HttpStatusCode.NotFound: {
+ return {
+ status: "aborted",
+ error: undefined,
+ routeClose,
+ };
+ }
+ default:
+ assertUnreachable(result);
+ }
+ }
+
+ const { body: data } = result;
+ if (data.status === "aborted") {
+ return {
+ status: "aborted",
+ error: undefined,
+ routeClose,
+ };
+ }
+
+ if (data.status === "confirmed") {
+ if (!settings.showWithdrawalSuccess) {
+ updateBankState("currentWithdrawalOperationId", undefined);
+ // onClose()
+ }
+ return {
+ status: "confirmed",
+ error: undefined,
+ routeClose,
+ };
+ }
+
+ if (data.status === "pending") {
+ return {
+ status: "ready",
+ error: undefined,
+ uri: parsedUri,
+ routeClose,
+ onAbort: !creds
+ ? async () => {
+ onAbort();
+ return undefined;
+ }
+ : doAbort,
+ };
+ }
+
+ if (!data.selected_reserve_pub) {
+ return {
+ status: "invalid-reserve",
+ error: undefined,
+ reserve: data.selected_reserve_pub,
+ };
+ }
+
+ const account = !data.selected_exchange_account
+ ? undefined
+ : parsePaytoUri(data.selected_exchange_account);
+
+ if (!account) {
+ return {
+ status: "invalid-payto",
+ error: undefined,
+ payto: data.selected_exchange_account,
+ };
+ }
+
+ return {
+ status: "need-confirmation",
+ error: undefined,
+ routeHere,
+ onAuthorizationRequired,
+ account: data.username,
+ id: withdrawalOperationId,
+ onAbort: !creds ? undefined : doAbort,
+ onConfirm: !creds ? undefined : doConfirm,
+ };
+ };
+}
diff --git a/packages/bank-ui/src/pages/OperationState/stories.tsx b/packages/bank-ui/src/pages/OperationState/stories.tsx
new file mode 100644
index 000000000..82253b82c
--- /dev/null
+++ b/packages/bank-ui/src/pages/OperationState/stories.tsx
@@ -0,0 +1,29 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import * as tests from "@gnu-taler/web-util/testing";
+import { ReadyView } from "./views.js";
+
+export default {
+ title: "operation status page",
+};
+
+export const Ready = tests.createExample(ReadyView, {});
diff --git a/packages/bank-ui/src/pages/OperationState/test.ts b/packages/bank-ui/src/pages/OperationState/test.ts
new file mode 100644
index 000000000..d47cb64a2
--- /dev/null
+++ b/packages/bank-ui/src/pages/OperationState/test.ts
@@ -0,0 +1,31 @@
+/*
+ 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/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+// import * as tests from "@gnu-taler/web-util/testing";
+// import { SwrMockEnvironment } from "@gnu-taler/web-util/testing";
+// import { expect } from "chai";
+// import { CASHOUT_API_EXAMPLE } from "../../endpoints.js";
+// import { Props } from "./index.js";
+// import { useComponentState } from "./state.js";
+
+describe("Withdrawal operation states", () => {
+ it("should do some tests", async () => {});
+});
diff --git a/packages/bank-ui/src/pages/OperationState/views.tsx b/packages/bank-ui/src/pages/OperationState/views.tsx
new file mode 100644
index 000000000..62308eca6
--- /dev/null
+++ b/packages/bank-ui/src/pages/OperationState/views.tsx
@@ -0,0 +1,447 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ AbsoluteTime,
+ HttpStatusCode,
+ TalerErrorCode,
+ TranslatedString,
+ assertUnreachable,
+ stringifyWithdrawUri,
+} from "@gnu-taler/taler-util";
+import {
+ Attention,
+ LocalNotificationBanner,
+ notifyInfo,
+ useLocalNotification,
+ useTalerWalletIntegrationAPI,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useEffect } from "preact/hooks";
+import { QR } from "../../components/QR.js";
+import { useBankState } from "../../hooks/bank-state.js";
+import { usePreferences } from "../../hooks/preferences.js";
+import { ShouldBeSameUser } from "../WithdrawalConfirmationQuestion.js";
+import { State } from "./index.js";
+
+export function InvalidPaytoView({ payto }: State.InvalidPayto) {
+ return <div>Payto from server is not valid &quot;{payto}&quot;</div>;
+}
+export function InvalidWithdrawalView({ uri }: State.InvalidWithdrawal) {
+ return <div>Withdrawal uri from server is not valid &quot;{uri}&quot;</div>;
+}
+export function InvalidReserveView({ reserve }: State.InvalidReserve) {
+ return <div>Reserve from server is not valid &quot;{reserve}&quot;</div>;
+}
+
+export function NeedConfirmationView({
+ onAbort: doAbort,
+ onConfirm: doConfirm,
+ routeHere,
+ account,
+ id,
+ onAuthorizationRequired,
+}: State.NeedConfirmation) {
+ const { i18n } = useTranslationContext();
+ const [settings] = usePreferences();
+ const [notification, notify, errorHandler] = useLocalNotification();
+ const [, updateBankState] = useBankState();
+
+ async function onCancel() {
+ errorHandler(async () => {
+ if (!doAbort) return;
+ const resp = await doAbort();
+ if (!resp) return;
+ switch (resp.case) {
+ case HttpStatusCode.Conflict:
+ return notify({
+ type: "error",
+ title: i18n.str`The reserve operation has been confirmed previously and can't be aborted`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case HttpStatusCode.BadRequest:
+ return notify({
+ type: "error",
+ title: i18n.str`The operation id is invalid.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case HttpStatusCode.NotFound:
+ return notify({
+ type: "error",
+ title: i18n.str`The operation was not found.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ default:
+ assertUnreachable(resp);
+ }
+ });
+ }
+
+ async function onConfirm() {
+ errorHandler(async () => {
+ if (!doConfirm) return;
+ const resp = await doConfirm();
+ if (!resp) {
+ if (!settings.showWithdrawalSuccess) {
+ notifyInfo(i18n.str`Wire transfer completed!`);
+ }
+ return;
+ }
+ switch (resp.case) {
+ case TalerErrorCode.BANK_CONFIRM_ABORT_CONFLICT:
+ return notify({
+ type: "error",
+ title: i18n.str`The withdrawal has been aborted previously and can't be confirmed`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case TalerErrorCode.BANK_CONFIRM_INCOMPLETE:
+ return notify({
+ type: "error",
+ title: i18n.str`The withdrawal operation can't be confirmed before a wallet accepted the transaction.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case HttpStatusCode.BadRequest:
+ return notify({
+ type: "error",
+ title: i18n.str`The operation id is invalid.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case HttpStatusCode.NotFound:
+ return notify({
+ type: "error",
+ title: i18n.str`The operation was not found.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case TalerErrorCode.BANK_UNALLOWED_DEBIT:
+ return notify({
+ type: "error",
+ title: i18n.str`Your balance is not enough.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case HttpStatusCode.Accepted: {
+ updateBankState("currentChallenge", {
+ operation: "confirm-withdrawal",
+ id: String(resp.body.challenge_id),
+ sent: AbsoluteTime.never(),
+ location: routeHere.url({ wopid: id }),
+ request: id,
+ });
+ return onAuthorizationRequired();
+ }
+ default:
+ assertUnreachable(resp);
+ }
+ });
+ }
+
+ return (
+ <div class="bg-white shadow sm:rounded-lg">
+ <LocalNotificationBanner notification={notification} />
+ <div class="px-4 py-5 sm:p-6">
+ <h3 class="text-base font-semibold text-gray-900">
+ <i18n.Translate>Confirm the withdrawal operation</i18n.Translate>
+ </h3>
+ <div class="mt-3 text-sm leading-6">
+ <ShouldBeSameUser username={account}>
+ <form
+ class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2"
+ autoCapitalize="none"
+ autoCorrect="off"
+ onSubmit={(e) => {
+ e.preventDefault();
+ }}
+ >
+ <div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
+ <button
+ type="button"
+ name="cancel"
+ class="text-sm font-semibold leading-6 text-gray-900"
+ onClick={(e) => {
+ e.preventDefault();
+ onCancel();
+ }}
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ <button
+ type="submit"
+ name="transfer"
+ class="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"
+ onClick={(e) => {
+ e.preventDefault();
+ onConfirm();
+ }}
+ >
+ <i18n.Translate>Transfer</i18n.Translate>
+ </button>
+ </div>
+ </form>
+ </ShouldBeSameUser>
+ </div>
+ </div>
+ </div>
+ );
+}
+export function FailedView({ error }: State.Failed) {
+ const { i18n } = useTranslationContext();
+ switch (error.case) {
+ case HttpStatusCode.Unauthorized:
+ return (
+ <Attention
+ type="danger"
+ title={i18n.str`Unauthorized to make the operation, maybe the session has expired or the password changed.`}
+ >
+ <div class="mt-2 text-sm text-red-700">{error.detail.hint}</div>
+ </Attention>
+ );
+ case HttpStatusCode.Conflict:
+ return (
+ <Attention
+ type="danger"
+ title={i18n.str`The operation was rejected due to insufficient funds.`}
+ >
+ <div class="mt-2 text-sm text-red-700">{error.detail.hint}</div>
+ </Attention>
+ );
+ case HttpStatusCode.NotFound:
+ return (
+ <Attention
+ type="danger"
+ title={i18n.str`The operation was rejected due to insufficient funds.`}
+ >
+ <div class="mt-2 text-sm text-red-700">{error.detail.hint}</div>
+ </Attention>
+ );
+ default:
+ assertUnreachable(error);
+ }
+}
+
+export function AbortedView() {
+ return <div>aborted</div>;
+}
+
+export function ConfirmedView({ routeClose }: State.Confirmed) {
+ const { i18n } = useTranslationContext();
+ const [settings, updateSettings] = usePreferences();
+ return (
+ <Fragment>
+ <div class="relative ml-auto mr-auto transform overflow-hidden rounded-lg bg-white p-4 text-left shadow-xl transition-all ">
+ <div class="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-green-100">
+ <svg
+ class="h-6 w-6 text-green-600"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="1.5"
+ stroke="currentColor"
+ aria-hidden="true"
+ >
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="M4.5 12.75l6 6 9-13.5"
+ />
+ </svg>
+ </div>
+ <div class="mt-3 text-center sm:mt-5">
+ <h3
+ class="text-base font-semibold leading-6 text-gray-900"
+ id="modal-title"
+ >
+ <i18n.Translate>Withdrawal confirmed</i18n.Translate>
+ </h3>
+ <div class="mt-2">
+ <p class="text-sm text-gray-500">
+ <i18n.Translate>
+ The wire transfer to the Taler operator has been initiated. You
+ will soon receive the requested amount in your Taler wallet.
+ </i18n.Translate>
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="mt-4">
+ <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"
+ >
+ <i18n.Translate>Do not show this again</i18n.Translate>
+ </span>
+ </span>
+ <button
+ type="button"
+ name="toggle withdrawal"
+ data-enabled={!settings.showWithdrawalSuccess}
+ 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={() => {
+ updateSettings(
+ "showWithdrawalSuccess",
+ !settings.showWithdrawalSuccess,
+ );
+ }}
+ >
+ <span
+ aria-hidden="true"
+ data-enabled={!settings.showWithdrawalSuccess}
+ 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>
+ </div>
+ <div class="mt-5 sm:mt-6">
+ <a
+ href={routeClose.url({})}
+ type="button"
+ name="close"
+ 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"
+ >
+ <i18n.Translate>Close</i18n.Translate>
+ </a>
+ </div>
+ </Fragment>
+ );
+}
+
+export function ReadyView({ uri, onAbort: doAbort }: State.Ready): VNode {
+ const { i18n } = useTranslationContext();
+ const walletInegrationApi = useTalerWalletIntegrationAPI();
+ const [notification, notify, errorHandler] = useLocalNotification();
+
+ const talerWithdrawUri = stringifyWithdrawUri(uri);
+ useEffect(() => {
+ walletInegrationApi.publishTalerAction(uri);
+ }, []);
+
+ async function onAbort() {
+ errorHandler(async () => {
+ const hasError = await doAbort();
+ if (!hasError) return;
+ switch (hasError.case) {
+ case HttpStatusCode.Conflict:
+ return notify({
+ type: "error",
+ title: i18n.str`The reserve operation has been confirmed previously and can't be aborted`,
+ description: hasError.detail.hint as TranslatedString,
+ debug: hasError.detail,
+ when: AbsoluteTime.now(),
+ });
+ case HttpStatusCode.BadRequest:
+ return notify({
+ type: "error",
+ title: i18n.str`The operation id is invalid.`,
+ description: hasError.detail.hint as TranslatedString,
+ debug: hasError.detail,
+ when: AbsoluteTime.now(),
+ });
+ case HttpStatusCode.NotFound:
+ return notify({
+ type: "error",
+ title: i18n.str`The operation was not found.`,
+ description: hasError.detail.hint as TranslatedString,
+ debug: hasError.detail,
+ when: AbsoluteTime.now(),
+ });
+ default:
+ assertUnreachable(hasError);
+ }
+ });
+ }
+
+ return (
+ <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">
+ <h3 class="text-base font-semibold leading-6 text-gray-900">
+ <i18n.Translate>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">
+ <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"
+ >
+ <i18n.Translate>Start</i18n.Translate>
+ </a>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="bg-white shadow sm:rounded-lg mt-2">
+ <div class="p-4">
+ <h3 class="text-base font-semibold leading-6 text-gray-900">
+ <i18n.Translate>On a mobile phone</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>
+ <div class="mt-2 max-w-md ml-auto mr-auto">
+ <QR text={talerWithdrawUri} />
+ </div>
+ </div>
+ </div>
+ </Fragment>
+ );
+}
diff --git a/packages/bank-ui/src/pages/PaymentOptions.stories.tsx b/packages/bank-ui/src/pages/PaymentOptions.stories.tsx
new file mode 100644
index 000000000..78af886a8
--- /dev/null
+++ b/packages/bank-ui/src/pages/PaymentOptions.stories.tsx
@@ -0,0 +1,35 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import * as tests from "@gnu-taler/web-util/testing";
+import { PaymentOptions } from "./PaymentOptions.js";
+
+export default {
+ title: "PaymentOptions",
+};
+
+export const USD = tests.createExample(PaymentOptions, {
+ limit: {
+ currency: "USD",
+ fraction: 0,
+ value: 1,
+ },
+});
diff --git a/packages/bank-ui/src/pages/PaymentOptions.tsx b/packages/bank-ui/src/pages/PaymentOptions.tsx
new file mode 100644
index 000000000..386fe31bc
--- /dev/null
+++ b/packages/bank-ui/src/pages/PaymentOptions.tsx
@@ -0,0 +1,239 @@
+/*
+ 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 { AmountJson, TalerError } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useEffect } from "preact/hooks";
+import { useWithdrawalDetails } from "../hooks/account.js";
+import { useBankState } from "../hooks/bank-state.js";
+import { useSessionState } from "../hooks/session.js";
+import { RouteDefinition } from "@gnu-taler/web-util/browser";
+import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js";
+import { WalletWithdrawForm } from "./WalletWithdrawForm.js";
+
+function ShowOperationPendingTag({
+ woid,
+ onOperationAlreadyCompleted,
+}: {
+ woid: string;
+ onOperationAlreadyCompleted?: () => void;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const { state: credentials } = useSessionState();
+ const result = useWithdrawalDetails(woid);
+ const loading = !result;
+ const error =
+ !loading && (result instanceof TalerError || result.type === "fail");
+ const pending =
+ !loading &&
+ !error &&
+ (result.body.status === "pending" || result.body.status === "selected") &&
+ credentials.status === "loggedIn" &&
+ credentials.username === result.body.username;
+ useEffect(() => {
+ if (!loading && !pending && onOperationAlreadyCompleted) {
+ onOperationAlreadyCompleted();
+ }
+ }, [pending]);
+
+ if (error || !pending) {
+ return <Fragment />;
+ }
+
+ return (
+ <span class="flex items-center gap-x-1.5 w-fit rounded-md bg-green-100 px-2 py-1 text-xs font-medium text-green-700 whitespace-pre">
+ <svg
+ class="h-1.5 w-1.5 fill-green-500"
+ viewBox="0 0 6 6"
+ aria-hidden="true"
+ >
+ <circle cx="3" cy="3" r="3" />
+ </svg>
+ <i18n.Translate>Operation ready</i18n.Translate>
+ </span>
+ );
+}
+
+/**
+ * Let the user choose a payment option,
+ * then specify the details trigger the action.
+ */
+export function PaymentOptions({
+ routeClose,
+ routeCashout,
+ routeChargeWallet,
+ routeWireTransfer,
+ tab,
+ limit,
+ balance,
+ onOperationCreated,
+ onClose,
+ routeOperationDetails,
+ onAuthorizationRequired,
+}: {
+ limit: AmountJson;
+ balance: AmountJson;
+ tab: "charge-wallet" | "wire-transfer" | undefined;
+ onAuthorizationRequired: () => void;
+ onOperationCreated: (wopid: string) => void;
+ onClose: () => void;
+
+ routeOperationDetails: RouteDefinition<{ wopid: string }>;
+ routeClose: RouteDefinition;
+ routeCashout: RouteDefinition;
+ routeChargeWallet: RouteDefinition;
+ routeWireTransfer: RouteDefinition<{
+ account?: string;
+ subject?: string;
+ amount?: string;
+ }>;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const [bankState, updateBankState] = useBankState();
+
+ return (
+ <div class="mt-4">
+ <fieldset>
+ <legend class="px-4 text-base font-semibold leading-6 text-gray-900">
+ <i18n.Translate>Send money</i18n.Translate>
+ </legend>
+
+ <div class="px-4 mt-4 grid grid-cols-1 gap-y-6 sm:grid-cols-2 sm:gap-x-4">
+ {/* <!-- Active: "border-indigo-600 ring-2 ring-indigo-600", Not Active: "border-gray-300" --> */}
+ <a name="charge wallet" href={routeChargeWallet.url({})}>
+ <label
+ class={
+ "relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none" +
+ (tab === "charge-wallet"
+ ? "border-indigo-600 ring-2 ring-indigo-600"
+ : "border-gray-300")
+ }
+ >
+ <div class="flex flex-col">
+ <span class="flex">
+ <div class="text-4xl mr-4 my-auto">&#x1F4B5;</div>
+ <span class="grow self-center text-lg text-gray-900 align-middle text-center">
+ <i18n.Translate>to a Taler wallet</i18n.Translate>
+ </span>
+ <svg
+ class="self-center flex-none h-5 w-5 text-indigo-600"
+ style={{
+ visibility:
+ tab === "charge-wallet" ? "visible" : "hidden",
+ }}
+ viewBox="0 0 20 20"
+ fill="currentColor"
+ aria-hidden="true"
+ >
+ <path
+ fill-rule="evenodd"
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
+ clip-rule="evenodd"
+ />
+ </svg>
+ </span>
+ <div class="mt-1 flex items-center text-sm text-gray-500">
+ <i18n.Translate>
+ Withdraw digital money into your mobile wallet or browser
+ extension
+ </i18n.Translate>
+ </div>
+ {!!bankState.currentWithdrawalOperationId && (
+ <ShowOperationPendingTag
+ woid={bankState.currentWithdrawalOperationId}
+ onOperationAlreadyCompleted={() => {
+ updateBankState(
+ "currentWithdrawalOperationId",
+ undefined,
+ );
+ }}
+ />
+ )}
+ </div>
+ </label>
+ </a>
+
+ <a name="wire transfer" href={routeWireTransfer.url({})}>
+ <label
+ class={
+ "relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none" +
+ (tab === "wire-transfer"
+ ? "border-indigo-600 ring-2 ring-indigo-600"
+ : "border-gray-300")
+ }
+ >
+ <div class="flex flex-col">
+ <span class="flex">
+ <div class="text-4xl mr-4 my-auto">&#x2194;</div>
+ <span class="grow self-center text-lg font-medium text-gray-900 align-middle text-center">
+ <i18n.Translate>to another bank account</i18n.Translate>
+ </span>
+ <svg
+ class="self-center flex-none h-5 w-5 text-indigo-600"
+ style={{
+ visibility:
+ tab === "wire-transfer" ? "visible" : "hidden",
+ }}
+ viewBox="0 0 20 20"
+ fill="currentColor"
+ aria-hidden="true"
+ >
+ <path
+ fill-rule="evenodd"
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
+ clip-rule="evenodd"
+ />
+ </svg>
+ </span>
+ <div class="mt-1 flex items-center text-sm text-gray-500">
+ <i18n.Translate>
+ Make a wire transfer to an account with known bank account
+ number.
+ </i18n.Translate>
+ </div>
+ </div>
+ </label>
+ </a>
+ </div>
+ {tab === "charge-wallet" && (
+ <WalletWithdrawForm
+ routeOperationDetails={routeOperationDetails}
+ focus
+ limit={limit}
+ balance={balance}
+ onAuthorizationRequired={onAuthorizationRequired}
+ onOperationCreated={onOperationCreated}
+ onOperationAborted={onClose}
+ routeCancel={routeClose}
+ />
+ )}
+ {tab === "wire-transfer" && (
+ <PaytoWireTransferForm
+ focus
+ routeHere={routeWireTransfer}
+ limit={limit}
+ balance={balance}
+ onAuthorizationRequired={onAuthorizationRequired}
+ onSuccess={onClose}
+ routeCashout={routeCashout}
+ routeCancel={routeClose}
+ />
+ )}
+ </fieldset>
+ </div>
+ );
+}
diff --git a/packages/bank-ui/src/pages/PaytoWireTransferForm.stories.tsx b/packages/bank-ui/src/pages/PaytoWireTransferForm.stories.tsx
new file mode 100644
index 000000000..61cfb5629
--- /dev/null
+++ b/packages/bank-ui/src/pages/PaytoWireTransferForm.stories.tsx
@@ -0,0 +1,35 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import * as tests from "@gnu-taler/web-util/testing";
+import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js";
+
+export default {
+ title: "PaytoWireTransferForm",
+};
+
+export const USD = tests.createExample(PaytoWireTransferForm, {
+ limit: {
+ currency: "USD",
+ fraction: 0,
+ value: 1,
+ },
+});
diff --git a/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx b/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx
new file mode 100644
index 000000000..a3bb091c1
--- /dev/null
+++ b/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx
@@ -0,0 +1,1000 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ AbsoluteTime,
+ AmountJson,
+ AmountString,
+ Amounts,
+ CurrencySpecification,
+ FRAC_SEPARATOR,
+ HttpStatusCode,
+ PaytoString,
+ PaytoUri,
+ TalerCorebankApi,
+ TalerErrorCode,
+ TranslatedString,
+ assertUnreachable,
+ buildPayto,
+ parsePaytoUri,
+ stringifyPaytoUri
+} from "@gnu-taler/taler-util";
+import {
+ InternationalizationAPI,
+ LocalNotificationBanner,
+ RouteDefinition,
+ ShowInputErrorLabel,
+ notifyInfo,
+ useBankCoreApiContext,
+ useLocalNotification,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { ComponentChildren, Fragment, Ref, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { mutate } from "swr";
+import { IdempotencyRetry } from "../../../taler-util/lib/http-client/utils.js";
+import { useBankState } from "../hooks/bank-state.js";
+import { useSessionState } from "../hooks/session.js";
+import { undefinedIfEmpty, validateIBAN, validateTalerBank } from "../utils.js";
+
+interface Props {
+ focus?: boolean;
+ withAccount?: string;
+ withSubject?: string;
+ withAmount?: string;
+ onSuccess: () => void;
+ onAuthorizationRequired: () => void;
+ routeCancel?: RouteDefinition;
+ routeCashout?: RouteDefinition;
+ routeHere: RouteDefinition<{
+ account?: string;
+ subject?: string;
+ amount?: string;
+ }>;
+ limit: AmountJson;
+ balance: AmountJson;
+}
+
+export function PaytoWireTransferForm({
+ focus,
+ withAccount,
+ withSubject,
+ withAmount,
+ onSuccess,
+ routeCancel,
+ routeCashout,
+ routeHere,
+ onAuthorizationRequired,
+ limit,
+}: Props): VNode {
+ const [inputType, setInputType] = useState<"form" | "payto" | "qr">("form");
+ const isRawPayto = inputType !== "form";
+
+ const { state: credentials } = useSessionState();
+ const {
+ lib: { bank: api },
+ config,
+ url,
+ } = useBankCoreApiContext();
+
+ const sendingToFixedAccount = withAccount !== undefined;
+
+ const [account, setAccount] = useState<string | undefined>(withAccount);
+ const [subject, setSubject] = useState<string | undefined>(withSubject);
+ const [amount, setAmount] = useState<string | undefined>(withAmount);
+ const [, updateBankState] = useBankState();
+
+ const [rawPaytoInput, rawPaytoInputSetter] = useState<string | undefined>(
+ undefined,
+ );
+ const { i18n } = useTranslationContext();
+
+ const trimmedAmountStr = amount?.trim();
+ const parsedAmount = Amounts.parse(`${limit.currency}:${trimmedAmountStr}`);
+ const [notification, notify, handleError] = useLocalNotification();
+
+ const paytoType =
+ config.wire_type === "X_TALER_BANK"
+ ? ("x-taler-bank" as const)
+ : ("iban" as const);
+
+ const errorsWire = undefinedIfEmpty({
+ account: !account
+ ? i18n.str`Required`
+ : paytoType === "iban"
+ ? validateIBAN(account, i18n)
+ : paytoType === "x-taler-bank"
+ ? validateTalerBank(account, i18n)
+ : undefined,
+ subject: !subject ? i18n.str`Required` : validateSubject(subject, i18n),
+ amount: !trimmedAmountStr
+ ? i18n.str`Required`
+ : !parsedAmount
+ ? i18n.str`Not valid`
+ : validateAmount(parsedAmount, limit, i18n),
+ });
+
+ const parsed = !rawPaytoInput ? undefined : parsePaytoUri(rawPaytoInput);
+
+ const errorsPayto = undefinedIfEmpty({
+ rawPaytoInput: !rawPaytoInput
+ ? i18n.str`Required`
+ : !parsed
+ ? i18n.str`Does not follow the pattern`
+ : validateRawPayto(parsed, limit, url.host, i18n, paytoType),
+ });
+
+ async function doSend() {
+ let payto_uri: PaytoString | undefined;
+ let sendingAmount: AmountString | undefined;
+
+ if (credentials.status !== "loggedIn") return;
+ let acName: string | undefined;
+ if (isRawPayto) {
+ const p = parsePaytoUri(rawPaytoInput!);
+ if (!p) return;
+ sendingAmount = p.params.amount as AmountString;
+ delete p.params.amount;
+ // if this payto is valid then it already have message
+ payto_uri = stringifyPaytoUri(p);
+ acName = !p.isKnown
+ ? undefined
+ : p.targetType === "iban"
+ ? p.iban
+ : p.targetType === "bitcoin"
+ ? p.address
+ : p.targetType === "x-taler-bank"
+ ? p.account
+ : assertUnreachable(p);
+ } else {
+ if (!account || !subject) return;
+ let payto;
+ acName = account;
+ switch (paytoType) {
+ case "x-taler-bank": {
+ payto = buildPayto("x-taler-bank", url.host, account);
+
+ break;
+ }
+ case "iban": {
+ payto = buildPayto("iban", account, undefined);
+ break;
+ }
+ default:
+ assertUnreachable(paytoType);
+ }
+
+ payto.params.message = encodeURIComponent(subject);
+ payto_uri = stringifyPaytoUri(payto);
+ sendingAmount = `${limit.currency}:${trimmedAmountStr}` as AmountString;
+ }
+ const puri = payto_uri;
+ const sAmount = sendingAmount;
+
+ await handleError(async function createTransactionHandleError() {
+ const request: TalerCorebankApi.CreateTransactionRequest = {
+ payto_uri: puri,
+ amount: sAmount,
+ };
+ const check = IdempotencyRetry.tryFiveTimes();
+ const resp = await api.createTransaction(
+ credentials,
+ request,
+ check,
+ );
+ mutate(() => true);
+ if (resp.type === "fail") {
+ switch (resp.case) {
+ case HttpStatusCode.BadRequest:
+ return notify({
+ type: "error",
+ title: i18n.str`The request was invalid or the payto://-URI used unacceptable features.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case HttpStatusCode.Unauthorized:
+ return notify({
+ type: "error",
+ title: i18n.str`Not enough permission to complete the operation.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case TalerErrorCode.BANK_ADMIN_CREDITOR:
+ return notify({
+ type: "error",
+ title: i18n.str`Bank administrator can't be the transfer creditor.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case TalerErrorCode.BANK_UNKNOWN_CREDITOR:
+ return notify({
+ type: "error",
+ title: i18n.str`The destination account "${
+ acName ?? puri
+ }" was not found.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case TalerErrorCode.BANK_SAME_ACCOUNT:
+ return notify({
+ type: "error",
+ title: i18n.str`The origin and the destination of the transfer can't be the same.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case TalerErrorCode.BANK_UNALLOWED_DEBIT:
+ return notify({
+ type: "error",
+ title: i18n.str`Your balance is not enough.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case HttpStatusCode.NotFound:
+ return notify({
+ type: "error",
+ title: i18n.str`The origin account "${puri}" was not found.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED: {
+ return notify({
+ type: "error",
+ title: i18n.str`Tried to create the transaction ${check.maxTries} times with different UID but failed.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ }
+ case HttpStatusCode.Accepted: {
+ updateBankState("currentChallenge", {
+ operation: "create-transaction",
+ id: String(resp.body.challenge_id),
+ location: routeHere.url({
+ account: account ?? "",
+ amount,
+ subject,
+ }),
+ sent: AbsoluteTime.never(),
+ request,
+ });
+ return onAuthorizationRequired();
+ }
+ default:
+ assertUnreachable(resp);
+ }
+ }
+ notifyInfo(i18n.str`Wire transfer created!`);
+ onSuccess();
+ setAmount(undefined);
+ setAccount(undefined);
+ setSubject(undefined);
+ rawPaytoInputSetter(undefined);
+ });
+ }
+
+ return (
+ <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-6 my-4 md:grid-cols-3 bg-gray-100 px-4 pb-4 rounded-lg">
+ {/* <div class="">
+ <div class="px-2 grid grid-cols-1 gap-y-4 sm:gap-x-4">
+ <label
+ class={
+ "relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none" +
+ (!isRawPayto
+ ? "border-indigo-600 ring-2 ring-indigo-600"
+ : "border-gray-300")
+ }
+ >
+ <input
+ type="radio"
+ name="project-type"
+ value="Newsletter"
+ class="sr-only"
+ aria-labelledby="project-type-0-label"
+ aria-describedby="project-type-0-description-0 project-type-0-description-1"
+ onChange={() => {
+ setIsRawPayto(false);
+ }}
+ />
+ <span class="flex flex-1">
+ <span class="flex flex-col">
+ <span class="block text-sm font-medium text-gray-900">
+ <i18n.Translate>Using a form</i18n.Translate>
+ </span>
+ </span>
+ </span>
+ </label>
+
+ {sendingToFixedAccount ? undefined : (
+ <label
+ class={
+ "relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none" +
+ (isRawPayto
+ ? "border-indigo-600 ring-2 ring-indigo-600"
+ : "border-gray-300")
+ }
+ >
+ <input
+ type="radio"
+ name="project-type"
+ value="Existing Customers"
+ class="sr-only"
+ aria-labelledby="project-type-1-label"
+ aria-describedby="project-type-1-description-0 project-type-1-description-1"
+ onChange={() => {
+
+ setIsRawPayto(true);
+ }}
+ />
+ <span class="flex flex-1">
+ <span class="flex flex-col">
+ <span class="block text-sm font-medium text-gray-900">
+ <i18n.Translate>Import payto:// URI</i18n.Translate>
+ </span>
+ </span>
+ </span>
+ </label>
+ )}
+ {routeCashout ? (
+ <a
+ name="do cashout"
+ href={routeCashout.url({})}
+ class="bg-white p-4 rounded-lg text-sm font-semibold leading-6 text-gray-900"
+ >
+ <i18n.Translate>Cashout</i18n.Translate>
+ </a>
+ ) : undefined}
+ </div>
+ </div> */}
+
+ <div>
+ <fieldset class="px-2 grid grid-cols-1 gap-y-4 sm:gap-x-4">
+ <legend class="sr-only">
+ <i18n.Translate>Input wire transfer detail</i18n.Translate>
+ </legend>
+ <div class="-space-y-px rounded-md ">
+ <label
+ data-checked={inputType === "form"}
+ class="group rounded-tl-md rounded-tr-md relative flex cursor-pointer border p-4 focus:outline-none bg-white data-[checked=true]:z-10 data-[checked=true]:border-indigo-200 data-[checked=true]:bg-indigo-50"
+ >
+ <input
+ type="radio"
+ name="input-type"
+ onChange={() => {
+ if (parsed && parsed.isKnown) {
+ switch (parsed.targetType) {
+ case "iban": {
+ setAccount(parsed.iban);
+ break;
+ }
+ case "x-taler-bank": {
+ setAccount(parsed.account);
+ break;
+ }
+ case "bitcoin": {
+ break;
+ }
+ default: {
+ assertUnreachable(parsed);
+ }
+ }
+ const amountStr = !parsed.params
+ ? undefined
+ : parsed.params["amount"];
+ if (amountStr) {
+ const amount = Amounts.parse(amountStr);
+ if (amount) {
+ setAmount(Amounts.stringifyValue(amount));
+ }
+ }
+ const subject = parsed.params["message"];
+ if (subject) {
+ setSubject(subject);
+ }
+ }
+ setInputType("form");
+ }}
+ checked={inputType === "form"}
+ value="form"
+ class="mt-0.5 h-4 w-4 shrink-0 cursor-pointer text-indigo-600 border-gray-300 focus:ring-indigo-600 active:ring-2 active:ring-offset-2 active:ring-indigo-600"
+ />
+ <span class="ml-3 flex flex-col">
+ {/* <!-- Checked: "text-indigo-900", Not Checked: "text-gray-900" --> */}
+ <span
+ data-checked={inputType === "form"}
+ class="block text-sm font-medium data-[checked=true]:text-indigo-900"
+ >
+ <i18n.Translate>Using a form</i18n.Translate>
+ </span>
+ </span>
+ </label>
+ {sendingToFixedAccount ? undefined : (
+ <Fragment>
+ <label
+ data-checked={inputType === "payto"}
+ class="relative flex cursor-pointer border p-4 focus:outline-none bg-white data-[checked=true]:z-10 data-[checked=true]:border-indigo-200 data-[checked=true]:bg-indigo-50"
+ >
+ <input
+ type="radio"
+ name="input-type"
+ onChange={() => {
+ if (account) {
+ let payto;
+ switch (paytoType) {
+ case "x-taler-bank": {
+ payto = buildPayto(
+ "x-taler-bank",
+ url.host,
+ account,
+ );
+ if (parsedAmount) {
+ payto.params["amount"] =
+ Amounts.stringify(parsedAmount);
+ }
+ if (subject) {
+ payto.params["message"] = subject;
+ }
+ break;
+ }
+ case "iban": {
+ payto = buildPayto("iban", account, undefined);
+ if (parsedAmount) {
+ payto.params["amount"] =
+ Amounts.stringify(parsedAmount);
+ }
+ if (subject) {
+ payto.params["message"] = subject;
+ }
+ break;
+ }
+ default:
+ assertUnreachable(paytoType);
+ }
+ rawPaytoInputSetter(stringifyPaytoUri(payto));
+ }
+ setInputType("payto");
+ }}
+ checked={inputType === "payto"}
+ value="payto"
+ class="mt-0.5 h-4 w-4 shrink-0 cursor-pointer text-indigo-600 border-gray-300 focus:ring-indigo-600 active:ring-2 active:ring-offset-2 active:ring-indigo-600"
+ />
+ <span class="ml-3 flex flex-col">
+ <span
+ data-checked={inputType === "payto"}
+ class="block font-medium data-[checked=true]:text-indigo-900"
+ >
+ payto:// URI
+ </span>
+ <span
+ data-checked={inputType === "payto"}
+ class="block text-sm text-gray-500 data-[checked=true]:text-indigo-600"
+ >
+ <i18n.Translate>
+ A special URI that indicate the transfer amount and
+ account target.
+ </i18n.Translate>
+ </span>
+ </span>
+ </label>
+ {
+ //FIXME: add QR support
+ false && (
+ <label
+ data-checked={inputType === "qr"}
+ class="rounded-bl-md rounded-br-md relative flex cursor-pointer border p-4 focus:outline-none bg-white data-[checked=true]:z-10 data-[checked=true]:border-indigo-200 data-[checked=true]:bg-indigo-50"
+ >
+ <input
+ type="radio"
+ name="input-type"
+ onChange={() => {
+ setInputType("qr");
+ }}
+ checked={inputType === "qr"}
+ value="qr"
+ class="mt-0.5 h-4 w-4 shrink-0 cursor-pointer text-indigo-600 border-gray-300 focus:ring-indigo-600 active:ring-2 active:ring-offset-2 active:ring-indigo-600"
+ />
+ <span class="ml-3 flex flex-col">
+ <span
+ data-checked={inputType === "qr"}
+ class="block font-medium data-[checked=true]:text-indigo-900"
+ >
+ <i18n.Translate>QR code</i18n.Translate>
+ </span>
+ <span
+ data-checked={inputType === "qr"}
+ class="block text-sm text-gray-500 data-[checked=true]:text-indigo-600"
+ >
+ <i18n.Translate>
+ If you have a camera in this device you can import a
+ payto:// URI from a QR code.
+ </i18n.Translate>
+ </span>
+ </span>
+ </label>
+ )
+ }
+ </Fragment>
+ )}
+ </div>
+ {routeCashout ? (
+ <a
+ name="do cashout"
+ href={routeCashout.url({})}
+ class="bg-white p-4 rounded-lg text-sm font-semibold leading-6 text-gray-900"
+ >
+ <i18n.Translate>Cashout</i18n.Translate>
+ </a>
+ ) : undefined}
+ </fieldset>
+ </div>
+
+ <form
+ class="bg-white shadow-sm ring-1 ring-gray-900/5 rounded-md sm:rounded-xl md:col-span-2 w-fit mx-auto"
+ autoCapitalize="none"
+ autoCorrect="off"
+ onSubmit={(e) => {
+ e.preventDefault();
+ }}
+ >
+ <div class="p-4 sm:p-8">
+ {!isRawPayto ? (
+ <div class="grid max-w-xs grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
+ {(() => {
+ switch (paytoType) {
+ case "x-taler-bank": {
+ return (
+ <TextField
+ id="x-taler-bank"
+ required
+ label={i18n.str`Recipient`}
+ help={i18n.str`Id of the recipient's account`}
+ error={errorsWire?.account}
+ onChange={setAccount}
+ value={account}
+ placeholder={i18n.str`username`}
+ focus={focus}
+ disabled={sendingToFixedAccount}
+ />
+ );
+ }
+ case "iban": {
+ return (
+ <TextField
+ id="iban"
+ required
+ label={i18n.str`Recipient`}
+ help={i18n.str`IBAN of the recipient's account`}
+ placeholder={"CC0123456789" as TranslatedString}
+ error={errorsWire?.account}
+ onChange={(v) => setAccount(v.toUpperCase())}
+ value={account}
+ focus={focus}
+ disabled={sendingToFixedAccount}
+ />
+ );
+ }
+ default:
+ assertUnreachable(paytoType);
+ }
+ })()}
+
+ <div class="sm:col-span-5">
+ <label
+ for="subject"
+ class="block text-sm font-medium leading-6 text-gray-900"
+ >
+ {i18n.str`Transfer subject`}
+ <b style={{ color: "red" }}> *</b>
+ </label>
+ <div class="mt-2">
+ <input
+ type="text"
+ class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ name="subject"
+ id="subject"
+ autocomplete="off"
+ placeholder={i18n.str`Subject`}
+ value={subject ?? ""}
+ required
+ onInput={(e): void => {
+ setSubject(e.currentTarget.value);
+ }}
+ />
+ <ShowInputErrorLabel
+ message={errorsWire?.subject}
+ isDirty={subject !== undefined}
+ />
+ </div>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ Some text to identify the transfer
+ </i18n.Translate>
+ </p>
+ </div>
+
+ <div class="sm:col-span-5">
+ <label
+ for="amount"
+ class="block text-sm font-medium leading-6 text-gray-900"
+ >
+ {i18n.str`Amount`}
+ <b style={{ color: "red" }}> *</b>
+ </label>
+ <InputAmount
+ name="amount"
+ left
+ currency={limit.currency}
+ value={trimmedAmountStr}
+ onChange={(d) => {
+ setAmount(d);
+ }}
+ />
+ <ShowInputErrorLabel
+ message={errorsWire?.amount}
+ isDirty={trimmedAmountStr !== undefined}
+ />
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>Amount to transfer</i18n.Translate>
+ </p>
+ </div>
+ </div>
+ ) : (
+ <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6 w-full">
+ <div class="sm:col-span-6">
+ <label
+ for="address"
+ class="block text-sm font-medium leading-6 text-gray-900"
+ >
+ {i18n.str`Payto URI:`}
+ <b style={{ color: "red" }}> *</b>
+ </label>
+ <div class="mt-2">
+ <textarea
+ ref={focus ? doAutoFocus : undefined}
+ name="address"
+ id="address"
+ type="textarea"
+ rows={5}
+ class="block overflow-hidden w-44 sm:w-96 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ value={rawPaytoInput ?? ""}
+ required
+ title={i18n.str`Uniform resource identifier of the target account`}
+ placeholder={((): TranslatedString => {
+ switch (paytoType) {
+ case "x-taler-bank":
+ return i18n.str`payto://x-taler-bank/[bank-host]/[receiver-account]?message=[subject]&amount=[${limit.currency}:X.Y]`;
+ case "iban":
+ return i18n.str`payto://iban/[receiver-iban]?message=[subject]&amount=[${limit.currency}:X.Y]`;
+ }
+ })()}
+ onInput={(e): void => {
+ rawPaytoInputSetter(e.currentTarget.value);
+ }}
+ />
+ <ShowInputErrorLabel
+ message={errorsPayto?.rawPaytoInput}
+ isDirty={rawPaytoInput !== undefined}
+ />
+ </div>
+ </div>
+ </div>
+ )}
+ </div>
+ <div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
+ {routeCancel ? (
+ <a
+ name="cancel"
+ href={routeCancel.url({})}
+ class="text-sm font-semibold leading-6 text-gray-900"
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </a>
+ ) : (
+ <div />
+ )}
+ <button
+ type="submit"
+ name="send"
+ class="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"
+ disabled={isRawPayto ? !!errorsPayto : !!errorsWire}
+ onClick={(e) => {
+ e.preventDefault();
+ doSend();
+ }}
+ >
+ <i18n.Translate>Send</i18n.Translate>
+ </button>
+ </div>
+ <LocalNotificationBanner notification={notification} />
+ </form>
+ </div>
+ );
+}
+
+/**
+ * Show the element when the load ended
+ * @param element
+ */
+export function doAutoFocus(element: HTMLElement | null) {
+ if (element) {
+ setTimeout(() => {
+ element.focus({ preventScroll: true });
+ element.scrollIntoView({
+ behavior: "smooth",
+ block: "center",
+ inline: "center",
+ });
+ }, 100);
+ }
+}
+
+export function InputAmount(
+ {
+ currency,
+ name,
+ value,
+ error,
+ left,
+ onChange,
+ }: {
+ error?: string;
+ currency: string;
+ name: string;
+ left?: boolean | undefined;
+ value: string | undefined;
+ onChange?: (s: string) => void;
+ },
+ ref: Ref<HTMLInputElement>,
+): VNode {
+ const { config } = useBankCoreApiContext();
+ return (
+ <div class="mt-2">
+ <div class="flex rounded-md shadow-sm border-0 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600">
+ <div class="pointer-events-none inset-y-0 flex items-center px-3">
+ <span class="text-gray-500 sm:text-sm">{currency}</span>
+ </div>
+ <input
+ type="number"
+ data-left={left}
+ class="disabled:bg-gray-200 text-right rounded-md rounded-l-none data-[left=true]:text-left w-full py-1.5 pl-3 text-gray-900 placeholder:text-gray-400 sm:text-sm sm:leading-6"
+ placeholder="0.00"
+ aria-describedby="price-currency"
+ ref={ref}
+ name={name}
+ id={name}
+ autocomplete="off"
+ value={value ?? ""}
+ disabled={!onChange}
+ onInput={(e) => {
+ if (!onChange) return;
+ const l = e.currentTarget.value.length;
+ const sep_pos = e.currentTarget.value.indexOf(FRAC_SEPARATOR);
+ if (
+ sep_pos !== -1 &&
+ l - sep_pos - 1 >
+ config.currency_specification.num_fractional_input_digits
+ ) {
+ e.currentTarget.value = e.currentTarget.value.substring(
+ 0,
+ sep_pos +
+ config.currency_specification.num_fractional_input_digits +
+ 1,
+ );
+ }
+ onChange(e.currentTarget.value);
+ }}
+ />
+ </div>
+ <ShowInputErrorLabel message={error} isDirty={value !== undefined} />
+ </div>
+ );
+}
+
+export function RenderAmount({
+ value,
+ spec,
+ negative,
+ withColor,
+ hideSmall,
+}: {
+ spec: CurrencySpecification;
+ value: AmountJson;
+ hideSmall?: boolean;
+ negative?: boolean;
+ withColor?: boolean;
+}): VNode {
+ const neg = !!negative; // convert to true or false
+
+ const { currency, normal, small } = Amounts.stringifyValueWithSpec(
+ value,
+ spec,
+ );
+
+ return (
+ <span
+ data-negative={withColor ? neg : undefined}
+ class="whitespace-nowrap data-[negative=false]:text-green-600 data-[negative=true]:text-red-600"
+ >
+ {negative ? "- " : undefined}
+ {currency} {normal}{" "}
+ {!hideSmall && small && <sup class="-ml-1">{small}</sup>}
+ </span>
+ );
+}
+
+function validateRawPayto(
+ parsed: PaytoUri,
+ limit: AmountJson,
+ host: string,
+ i18n: InternationalizationAPI,
+ type: "iban" | "x-taler-bank",
+): TranslatedString | undefined {
+ if (!parsed.isKnown) {
+ return i18n.str`The target type is unknown, use "${type}"`;
+ }
+ let result: TranslatedString | undefined;
+ switch (type) {
+ case "x-taler-bank": {
+ if (parsed.targetType !== "x-taler-bank") {
+ return i18n.str`Only "x-taler-bank" target are supported`;
+ }
+
+ if (parsed.host !== host) {
+ return i18n.str`Only this host is allowed. Use "${host}"`;
+ }
+
+ if (!parsed.account) {
+ return i18n.str`Missing account name`;
+ }
+ const result = validateTalerBank(parsed.account, i18n);
+ if (result) return result;
+ break;
+ }
+ case "iban": {
+ if (parsed.targetType !== "iban") {
+ return i18n.str`Only "IBAN" target are supported`;
+ }
+ const result = validateIBAN(parsed.iban, i18n);
+ if (result) return result;
+ break;
+ }
+ default:
+ assertUnreachable(type);
+ }
+ if (!parsed.params.amount) {
+ return i18n.str`Missing "amount" parameter to specify the amount to be transferred`;
+ }
+ const amount = Amounts.parse(parsed.params.amount);
+ if (!amount) {
+ return i18n.str`The "amount" parameter is not valid`;
+ }
+ result = validateAmount(amount, limit, i18n);
+ if (result) return result;
+
+ if (!parsed.params.message) {
+ return i18n.str`Missing the "message" parameter to specify a reference text for the transfer`;
+ }
+ const subject = parsed.params.message;
+ result = validateSubject(subject, i18n);
+ if (result) return result;
+
+ return undefined;
+}
+
+function validateAmount(
+ amount: AmountJson,
+ limit: AmountJson,
+ i18n: InternationalizationAPI,
+): TranslatedString | undefined {
+ if (amount.currency !== limit.currency) {
+ return i18n.str`The only currency allowed is "${limit.currency}"`;
+ }
+ if (Amounts.isZero(amount)) {
+ return i18n.str`Can't transfer zero amount`;
+ }
+ if (Amounts.cmp(limit, amount) === -1) {
+ return i18n.str`Balance is not enough`;
+ }
+ return undefined;
+}
+
+function validateSubject(
+ text: string,
+ i18n: InternationalizationAPI,
+): TranslatedString | undefined {
+ if (text.length < 2) {
+ return i18n.str`Use a longer subject`;
+ }
+ return undefined;
+}
+
+interface PaytoFieldProps {
+ id: string;
+ label: TranslatedString;
+ required?: boolean;
+ help?: TranslatedString;
+ placeholder?: TranslatedString;
+ error: string | undefined;
+ value: string | undefined;
+ rightIcons?: VNode;
+ onChange: (p: string) => void;
+ focus?: boolean;
+ disabled?: boolean;
+}
+
+function Wrapper({
+ withIcon,
+ children,
+}: {
+ withIcon: boolean;
+ children: ComponentChildren;
+}): VNode {
+ if (withIcon) {
+ return <div class="flex justify-between">{children}</div>;
+ }
+ return <Fragment>{children}</Fragment>;
+}
+
+export function TextField({
+ id,
+ label,
+ help,
+ focus,
+ disabled,
+ onChange,
+ placeholder,
+ rightIcons,
+ required,
+ value,
+ error,
+}: PaytoFieldProps): VNode {
+ return (
+ <div class="sm:col-span-5">
+ <label for={id} class="block text-sm font-medium leading-6 text-gray-900">
+ {label}
+ {required && <b style={{ color: "red" }}> *</b>}
+ </label>
+ <div class="mt-2">
+ <Wrapper withIcon={rightIcons !== undefined}>
+ <input
+ ref={focus ? doAutoFocus : undefined}
+ type="text"
+ class="block w-full disabled:bg-gray-200 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ name={id}
+ id={id}
+ disabled={disabled}
+ value={value ?? ""}
+ placeholder={placeholder}
+ autocomplete="off"
+ required
+ onInput={(e): void => {
+ onChange(e.currentTarget.value);
+ }}
+ />
+ {rightIcons}
+ </Wrapper>
+ <ShowInputErrorLabel message={error} isDirty={value !== undefined} />
+ </div>
+ {help && <p class="mt-2 text-sm text-gray-500">{help}</p>}
+ </div>
+ );
+}
diff --git a/packages/bank-ui/src/pages/ProfileNavigation.tsx b/packages/bank-ui/src/pages/ProfileNavigation.tsx
new file mode 100644
index 000000000..3e81e307c
--- /dev/null
+++ b/packages/bank-ui/src/pages/ProfileNavigation.tsx
@@ -0,0 +1,202 @@
+/*
+ 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 { assertUnreachable } from "@gnu-taler/taler-util";
+import {
+ useNavigationContext,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useBankCoreApiContext } from "@gnu-taler/web-util/browser";
+import { useSessionState } from "../hooks/session.js";
+import { RouteDefinition } from "@gnu-taler/web-util/browser";
+
+export function ProfileNavigation({
+ current,
+ routeMyAccountCashout,
+ routeMyAccountDelete,
+ routeMyAccountDetails,
+ routeMyAccountPassword,
+ routeConversionConfig,
+}: {
+ current: "details" | "delete" | "credentials" | "cashouts" | "conversion";
+ routeMyAccountDetails: RouteDefinition;
+ routeMyAccountDelete: RouteDefinition;
+ routeMyAccountPassword: RouteDefinition;
+ routeMyAccountCashout: RouteDefinition;
+ routeConversionConfig: RouteDefinition;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const { config } = useBankCoreApiContext();
+ const { state: credentials } = useSessionState();
+ const isAdminUser =
+ credentials.status !== "loggedIn" ? false : credentials.isUserAdministrator;
+ const nonAdminUser = !isAdminUser;
+
+ const { navigateTo } = useNavigationContext();
+ return (
+ <div>
+ <div class="sm:hidden">
+ <label for="tabs" class="sr-only">
+ <i18n.Translate>Select a section</i18n.Translate>
+ </label>
+ <select
+ id="tabs"
+ name="tabs"
+ class="block w-full rounded-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500"
+ onChange={(e) => {
+ const op = e.currentTarget.value as typeof current;
+ switch (op) {
+ case "details": {
+ navigateTo(routeMyAccountDetails.url({}));
+ return;
+ }
+ case "delete": {
+ navigateTo(routeMyAccountDelete.url({}));
+ return;
+ }
+ case "credentials": {
+ navigateTo(routeMyAccountPassword.url({}));
+ return;
+ }
+ case "cashouts": {
+ navigateTo(routeMyAccountCashout.url({}));
+ return;
+ }
+ case "conversion": {
+ navigateTo(routeConversionConfig.url({}));
+ return;
+ }
+ default:
+ assertUnreachable(op);
+ }
+ }}
+ >
+ <option value="details" selected={current == "details"}>
+ <i18n.Translate>Details</i18n.Translate>
+ </option>
+ {!config.allow_deletions ? undefined : (
+ <option value="delete" selected={current == "delete"}>
+ <i18n.Translate>Delete</i18n.Translate>
+ </option>
+ )}
+ <option value="credentials" selected={current == "credentials"}>
+ <i18n.Translate>Credentials</i18n.Translate>
+ </option>
+ {config.allow_conversion ? (
+ <Fragment>
+ <option value="cashouts" selected={current == "cashouts"}>
+ <i18n.Translate>Cashouts</i18n.Translate>
+ </option>
+ <option value="conversion" selected={current == "cashouts"}>
+ <i18n.Translate>Conversion</i18n.Translate>
+ </option>
+ </Fragment>
+ ) : undefined}
+ </select>
+ </div>
+ <div class="hidden sm:block">
+ <nav
+ class="isolate flex divide-x divide-gray-200 rounded-lg shadow"
+ aria-label="Tabs"
+ >
+ <a
+ name="my account details"
+ href={routeMyAccountDetails.url({})}
+ data-selected={current == "details"}
+ class="rounded-l-lg text-gray-500 hover:text-gray-700 data-[selected=true]:text-gray-900 group relative min-w-0 flex-1 overflow-hidden bg-white py-4 px-4 text-center text-sm font-medium hover:bg-gray-50 focus:z-10"
+ >
+ <span>
+ <i18n.Translate>Details</i18n.Translate>
+ </span>
+ <span
+ aria-hidden="true"
+ data-selected={current == "details"}
+ class="bg-transparent data-[selected=true]:bg-indigo-500 absolute inset-x-0 bottom-0 h-0.5"
+ ></span>
+ </a>
+ {!config.allow_deletions ? undefined : (
+ <a
+ name="my account delete"
+ href={routeMyAccountDelete.url({})}
+ data-selected={current == "delete"}
+ aria-current="page"
+ class=" text-gray-500 hover:text-gray-700 data-[selected=true]:text-gray-900 group relative min-w-0 flex-1 overflow-hidden bg-white py-4 px-4 text-center text-sm font-medium hover:bg-gray-50 focus:z-10"
+ >
+ <span>
+ <i18n.Translate>Delete</i18n.Translate>
+ </span>
+ <span
+ aria-hidden="true"
+ data-selected={current == "delete"}
+ class="bg-transparent data-[selected=true]:bg-indigo-500 absolute inset-x-0 bottom-0 h-0.5"
+ ></span>
+ </a>
+ )}
+ <a
+ name="my account password"
+ href={routeMyAccountPassword.url({})}
+ data-selected={current == "credentials"}
+ aria-current="page"
+ class=" text-gray-500 hover:text-gray-700 data-[selected=true]:text-gray-900 group relative min-w-0 flex-1 overflow-hidden bg-white py-4 px-4 text-center text-sm font-medium hover:bg-gray-50 focus:z-10"
+ >
+ <span>
+ <i18n.Translate>Credentials</i18n.Translate>
+ </span>
+ <span
+ aria-hidden="true"
+ data-selected={current == "credentials"}
+ class="bg-transparent data-[selected=true]:bg-indigo-500 absolute inset-x-0 bottom-0 h-0.5"
+ ></span>
+ </a>
+ {config.allow_conversion && nonAdminUser ? (
+ <a
+ name="my account cashout"
+ href={routeMyAccountCashout.url({})}
+ data-selected={current == "cashouts"}
+ class="rounded-r-lg text-gray-500 hover:text-gray-700 data-[selected=true]:text-gray-900 group relative min-w-0 flex-1 overflow-hidden bg-white py-4 px-4 text-center text-sm font-medium hover:bg-gray-50 focus:z-10"
+ >
+ <span>
+ <i18n.Translate>Cashouts</i18n.Translate>
+ </span>
+ <span
+ aria-hidden="true"
+ data-selected={current == "cashouts"}
+ class="bg-transparent data-[selected=true]:bg-indigo-500 absolute inset-x-0 bottom-0 h-0.5"
+ ></span>
+ </a>
+ ) : undefined}
+ {config.allow_conversion && isAdminUser ? (
+ <a
+ name="conversion config"
+ href={routeConversionConfig.url({})}
+ data-selected={current == "conversion"}
+ class="rounded-r-lg text-gray-500 hover:text-gray-700 data-[selected=true]:text-gray-900 group relative min-w-0 flex-1 overflow-hidden bg-white py-4 px-4 text-center text-sm font-medium hover:bg-gray-50 focus:z-10"
+ >
+ <span>
+ <i18n.Translate>Conversion</i18n.Translate>
+ </span>
+ <span
+ aria-hidden="true"
+ data-selected={current == "conversion"}
+ class="bg-transparent data-[selected=true]:bg-indigo-500 absolute inset-x-0 bottom-0 h-0.5"
+ ></span>
+ </a>
+ ) : undefined}
+ </nav>
+ </div>
+ </div>
+ );
+}
diff --git a/packages/bank-ui/src/pages/PublicHistoriesPage.tsx b/packages/bank-ui/src/pages/PublicHistoriesPage.tsx
new file mode 100644
index 000000000..80ae28dde
--- /dev/null
+++ b/packages/bank-ui/src/pages/PublicHistoriesPage.tsx
@@ -0,0 +1,98 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { TalerError } from "@gnu-taler/taler-util";
+import { Loading, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { Transactions } from "../components/Transactions/index.js";
+import { usePublicAccounts } from "../hooks/account.js";
+
+/**
+ * Show histories of public accounts.
+ */
+export function PublicHistoriesPage(): VNode {
+ const { i18n } = useTranslationContext();
+
+ // TODO: implemented filter by account name
+ const result = usePublicAccounts(undefined);
+ const firstAccount =
+ result && !(result instanceof TalerError) && result.body.length > 0
+ ? result.body[0].username
+ : undefined;
+
+ const [showAccount, setShowAccount] = useState(firstAccount);
+
+ if (!result) {
+ return <Loading />;
+ }
+ if (result instanceof TalerError) {
+ return <Loading />;
+ }
+
+ const { body: accountList } = result;
+
+ const txs: Record<string, h.JSX.Element> = {};
+ const accountsBar = [];
+
+ // Ask story of all the public accounts.
+ for (const account of accountList) {
+ const isSelected = account.username == showAccount;
+ accountsBar.push(
+ <li
+ class={
+ isSelected
+ ? "pure-menu-selected pure-menu-item"
+ : "pure-menu-item pure-menu"
+ }
+ >
+ <a
+ href="#"
+ name={`show account ${account.username}`}
+ class="pure-menu-link"
+ onClick={() => setShowAccount(account.username)}
+ >
+ {account.username}
+ </a>
+ </li>,
+ );
+ txs[account.username] = (
+ <Transactions
+ account={account.username}
+ routeCreateWireTransfer={undefined}
+ />
+ );
+ }
+
+ return (
+ <Fragment>
+ <h1 class="nav">{i18n.str`History of public accounts`}</h1>
+ <section id="main">
+ <article>
+ <div class="pure-menu pure-menu-horizontal" name="accountMenu">
+ <ul class="pure-menu-list">{accountsBar}</ul>
+ {typeof showAccount !== "undefined" ? (
+ txs[showAccount]
+ ) : (
+ <p>No public transactions found.</p>
+ )}
+ <br />
+ </div>
+ </article>
+ </section>
+ </Fragment>
+ );
+}
diff --git a/packages/bank-ui/src/pages/QrCodeSection.stories.tsx b/packages/bank-ui/src/pages/QrCodeSection.stories.tsx
new file mode 100644
index 000000000..d53d2e7b4
--- /dev/null
+++ b/packages/bank-ui/src/pages/QrCodeSection.stories.tsx
@@ -0,0 +1,32 @@
+/*
+ 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/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import * as tests from "@gnu-taler/web-util/testing";
+import { QrCodeSection } from "./QrCodeSection.js";
+import { parseWithdrawUri } from "@gnu-taler/taler-util";
+
+export default {
+ title: "Qr Code Selection",
+};
+
+export const SimpleExample = tests.createExample(QrCodeSection, {
+ withdrawUri: parseWithdrawUri("taler://withdraw/bank.com/operationId"),
+});
diff --git a/packages/bank-ui/src/pages/QrCodeSection.tsx b/packages/bank-ui/src/pages/QrCodeSection.tsx
new file mode 100644
index 000000000..359d4c18f
--- /dev/null
+++ b/packages/bank-ui/src/pages/QrCodeSection.tsx
@@ -0,0 +1,152 @@
+/*
+ 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 {
+ HttpStatusCode,
+ stringifyWithdrawUri,
+ WithdrawUriResult,
+} from "@gnu-taler/taler-util";
+import {
+ Button,
+ LocalNotificationBanner,
+ useLocalNotificationHandler,
+ useTalerWalletIntegrationAPI,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useEffect } from "preact/hooks";
+import { QR } from "../components/QR.js";
+import { useBankCoreApiContext } from "@gnu-taler/web-util/browser";
+import { useSessionState } from "../hooks/session.js";
+
+export function QrCodeSection({
+ withdrawUri,
+ onAborted,
+}: {
+ withdrawUri: WithdrawUriResult;
+ onAborted: () => void;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const walletInegrationApi = useTalerWalletIntegrationAPI();
+ const talerWithdrawUri = stringifyWithdrawUri(withdrawUri);
+ const { state: credentials } = useSessionState();
+ const creds = credentials.status !== "loggedIn" ? undefined : credentials;
+
+ useEffect(() => {
+ walletInegrationApi.publishTalerAction(withdrawUri);
+ }, []);
+
+ const [notification, handleError] = useLocalNotificationHandler();
+
+ const {
+ lib: { bank: api },
+ } = useBankCoreApiContext();
+
+ const onAbortHandler = handleError(
+ async () => {
+ if (!creds) return undefined;
+ return api.abortWithdrawalById(creds, withdrawUri.withdrawalOperationId);
+ },
+ onAborted,
+ (fail) => {
+ switch (fail.case) {
+ case HttpStatusCode.BadRequest:
+ return i18n.str`The operation id is invalid.`;
+ case HttpStatusCode.NotFound:
+ return i18n.str`The operation was not found.`;
+ case HttpStatusCode.Conflict:
+ return i18n.str`The reserve operation has been confirmed previously and can't be aborted`;
+ }
+ },
+ );
+
+ 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
+ </i18n.Translate>
+ </h3>
+ <div class="mt-4 mb-4 text-sm text-gray-500">
+ <p>
+ <i18n.Translate>
+ You will see the details of the operation in your wallet
+ including the fees (if applies). If you still don't have one you
+ can install it following instructions in
+ </i18n.Translate>{" "}
+ <a
+ class="font-semibold text-gray-500 hover:text-gray-400"
+ name="wallet page"
+ href="https://taler.net/en/wallet.html"
+ >
+ <i18n.Translate>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 ">
+ <Button
+ type="button"
+ name="cancel"
+ class="text-sm font-semibold leading-6 text-gray-900"
+ handler={onAbortHandler}
+ >
+ <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-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>
+ Or if you have the Taler wallet in another device
+ </i18n.Translate>
+ </h3>
+ <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}
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </Button>
+ </div>
+ </div>
+ </Fragment>
+ );
+}
diff --git a/packages/bank-ui/src/pages/RegistrationPage.tsx b/packages/bank-ui/src/pages/RegistrationPage.tsx
new file mode 100644
index 000000000..18f926e00
--- /dev/null
+++ b/packages/bank-ui/src/pages/RegistrationPage.tsx
@@ -0,0 +1,425 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ AccessToken,
+ HttpStatusCode,
+ TalerErrorCode,
+} from "@gnu-taler/taler-util";
+import {
+ LocalNotificationBanner,
+ ShowInputErrorLabel,
+ useLocalNotification,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { useBankCoreApiContext } from "@gnu-taler/web-util/browser";
+import { useSettingsContext } from "../context/settings.js";
+import { RouteDefinition } from "@gnu-taler/web-util/browser";
+import { undefinedIfEmpty } from "../utils.js";
+import { getRandomPassword, getRandomUsername } from "./rnd.js";
+
+export function RegistrationPage({
+ onRegistrationSuccesful,
+ routeCancel,
+}: {
+ onRegistrationSuccesful: (user: string, password: string) => void;
+ routeCancel: RouteDefinition;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const { config } = useBankCoreApiContext();
+ if (!config.allow_registrations) {
+ return (
+ <p>{i18n.str`Currently, the bank is not accepting new registrations!`}</p>
+ );
+ }
+ return (
+ <RegistrationForm
+ onRegistrationSuccesful={onRegistrationSuccesful}
+ routeCancel={routeCancel}
+ />
+ );
+}
+
+// eslint-disable-next-line no-useless-escape
+export const USERNAME_REGEX = /^[a-zA-Z0-9\-\.\_\~]*$/;
+export const PHONE_REGEX = /^(\+\d{1,2}\s)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}$/;
+export const EMAIL_REGEX = /^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/;
+
+/**
+ * Collect and submit registration data.
+ */
+function RegistrationForm({
+ onRegistrationSuccesful,
+ routeCancel,
+}: {
+ onRegistrationSuccesful: (user: string, password: string) => void;
+ routeCancel: RouteDefinition;
+}): VNode {
+ const [username, setUsername] = useState<string | undefined>();
+ const [name, setName] = useState<string | undefined>();
+ const [password, setPassword] = useState<string | undefined>();
+ // const [phone, setPhone] = useState<string | undefined>();
+ // const [email, setEmail] = useState<string | undefined>();
+ const [repeatPassword, setRepeatPassword] = useState<string | undefined>();
+ const [notification, , handleError] = useLocalNotification();
+ const settings = useSettingsContext();
+
+ const {
+ lib: { bank: api },
+ } = useBankCoreApiContext();
+ // const { register } = useTestingAPI();
+ const { i18n } = useTranslationContext();
+
+ const errors = undefinedIfEmpty({
+ name: !name ? i18n.str`Missing name` : undefined,
+ username: !username
+ ? i18n.str`Missing username`
+ : !USERNAME_REGEX.test(username)
+ ? i18n.str`Use letters, numbers or any of these characters: - . _ ~`
+ : undefined,
+ // phone: !phone
+ // ? undefined
+ // : !PHONE_REGEX.test(phone)
+ // ? i18n.str`Use letters and numbers only, and start with a lowercase letter`
+ // : undefined,
+ // email: !email
+ // ? undefined
+ // : !EMAIL_REGEX.test(email)
+ // ? i18n.str`Use letters and numbers only, and start with a lowercase letter`
+ // : undefined,
+ password: !password ? i18n.str`Missing password` : undefined,
+ repeatPassword: !repeatPassword
+ ? i18n.str`Missing password`
+ : repeatPassword !== password
+ ? i18n.str`Passwords don't match`
+ : undefined,
+ });
+
+ async function doRegistrationAndLogin(
+ name: string,
+ username: string,
+ password: string,
+ onComplete: () => void,
+ ) {
+ await handleError(async (onError) => {
+ const resp = await api.createAccount(undefined, {
+ name,
+ username,
+ password,
+ });
+ if (resp.type === "ok") {
+ onComplete();
+ } else {
+ onError(resp, (_case) => {
+ switch (_case) {
+ case HttpStatusCode.BadRequest:
+ return i18n.str`Server replied with invalid phone or email.`;
+ case HttpStatusCode.Unauthorized:
+ return i18n.str`No enough permission to create that account.`;
+ case TalerErrorCode.BANK_UNALLOWED_DEBIT:
+ return i18n.str`Registration is disabled because the bank ran out of bonus credit.`;
+ case TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT:
+ return i18n.str`That username can't be used because is reserved.`;
+ case TalerErrorCode.BANK_REGISTER_USERNAME_REUSE:
+ return i18n.str`That username is already taken.`;
+ case TalerErrorCode.BANK_REGISTER_PAYTO_URI_REUSE:
+ return i18n.str`That account id is already taken.`;
+ case TalerErrorCode.BANK_MISSING_TAN_INFO:
+ return i18n.str`No information for the selected authentication channel.`;
+ case TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED:
+ return i18n.str`Authentication channel is not supported.`;
+ case TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT:
+ return i18n.str`Only admin is allow to set debt limit.`;
+ case TalerErrorCode.BANK_NON_ADMIN_SET_TAN_CHANNEL:
+ return i18n.str`Only admin can create accounts with second factor authentication.`;
+ }
+ });
+ }
+ });
+ }
+
+ async function doRegistrationStep() {
+ if (!username || !password || !name) return;
+ await doRegistrationAndLogin(name, username, password, () => {
+ setUsername(undefined);
+ setPassword(undefined);
+ setRepeatPassword(undefined);
+ onRegistrationSuccesful(username, password);
+ });
+ }
+
+ async function doRandomRegistration() {
+ const user = getRandomUsername();
+
+ const password = settings.simplePasswordForRandomAccounts
+ ? "123"
+ : getRandomPassword();
+ const username = `_${user.first}-${user.second}_`;
+ const name = `${capitalizeFirstLetter(user.first)} ${capitalizeFirstLetter(
+ user.second,
+ )}`;
+ await doRegistrationAndLogin(name, username, password, () => {
+ onRegistrationSuccesful(username, password);
+ });
+ }
+
+ return (
+ <Fragment>
+ <LocalNotificationBanner notification={notification} />
+
+ <div class="flex min-h-full flex-col justify-center">
+ <div class="sm:mx-auto sm:w-full sm:max-w-sm">
+ <h2 class="text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">{i18n.str`Account registration`}</h2>
+ </div>
+
+ <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
+ <form
+ class="space-y-6"
+ noValidate
+ onSubmit={(e) => {
+ e.preventDefault();
+ }}
+ autoCapitalize="none"
+ autoCorrect="off"
+ >
+ <div>
+ <label
+ for="username"
+ class="block text-sm font-medium leading-6 text-gray-900"
+ >
+ <i18n.Translate>Login username</i18n.Translate>
+ <b style={{ color: "red" }}> *</b>
+ </label>
+ <div class="mt-2">
+ <input
+ autoFocus
+ type="text"
+ name="username"
+ id="username"
+ class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ value={username ?? ""}
+ enterkeyhint="next"
+ placeholder="account identification to login"
+ autocomplete="username"
+ required
+ onInput={(e): void => {
+ setUsername(e.currentTarget.value);
+ }}
+ />
+ <ShowInputErrorLabel
+ message={errors?.username}
+ isDirty={username !== undefined}
+ />
+ </div>
+ </div>
+
+ <div>
+ <div class="flex items-center justify-between">
+ <label
+ for="password"
+ class="block text-sm font-medium leading-6 text-gray-900"
+ >
+ <i18n.Translate>Password</i18n.Translate>
+ <b style={{ color: "red" }}> *</b>
+ </label>
+ </div>
+ <div class="mt-2">
+ <input
+ type="password"
+ name="password"
+ id="password"
+ autocomplete="current-password"
+ class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ enterkeyhint="send"
+ value={password ?? ""}
+ placeholder="Password"
+ required
+ onInput={(e): void => {
+ setPassword(e.currentTarget.value);
+ }}
+ />
+ <ShowInputErrorLabel
+ message={errors?.password}
+ isDirty={password !== undefined}
+ />
+ </div>
+ </div>
+
+ <div>
+ <div class="flex items-center justify-between">
+ <label
+ for="register-repeat"
+ class="block text-sm font-medium leading-6 text-gray-900"
+ >
+ <i18n.Translate>Repeat password</i18n.Translate>
+ <b style={{ color: "red" }}> *</b>
+ </label>
+ </div>
+ <div class="mt-2">
+ <input
+ type="password"
+ name="register-repeat"
+ id="register-repeat"
+ autocomplete="current-password"
+ class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ enterkeyhint="send"
+ value={repeatPassword ?? ""}
+ placeholder="Same password"
+ required
+ onInput={(e): void => {
+ setRepeatPassword(e.currentTarget.value);
+ }}
+ />
+ <ShowInputErrorLabel
+ message={errors?.repeatPassword}
+ isDirty={repeatPassword !== undefined}
+ />
+ </div>
+ </div>
+
+ <div>
+ <div class="flex items-center justify-between">
+ <label
+ for="name"
+ class="block text-sm font-medium leading-6 text-gray-900"
+ >
+ <i18n.Translate>Full name</i18n.Translate>
+ <b style={{ color: "red" }}> *</b>
+ </label>
+ </div>
+ <div class="mt-2">
+ <input
+ autoFocus
+ type="text"
+ name="name"
+ id="name"
+ class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ value={name ?? ""}
+ enterkeyhint="next"
+ placeholder="John Doe"
+ autocomplete="name"
+ required
+ onInput={(e): void => {
+ setName(e.currentTarget.value);
+ }}
+ />
+ {/* <ShowInputErrorLabel
+ message={errors?.name}
+ isDirty={name !== undefined}
+ /> */}
+ </div>
+ </div>
+
+ {/* <div>
+ <label for="phone" class="block text-sm font-medium leading-6 text-gray-900">
+ <i18n.Translate>Phone</i18n.Translate>
+ </label>
+ <div class="mt-2">
+ <input
+ autoFocus
+ type="text"
+ name="phone"
+ id="phone"
+ class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ value={phone ?? ""}
+ enterkeyhint="next"
+ placeholder="your phone"
+ autocomplete="none"
+ onInput={(e): void => {
+ setPhone(e.currentTarget.value);
+ }}
+ />
+ <ShowInputErrorLabel
+ message={errors?.phone}
+ isDirty={phone !== undefined}
+ />
+ </div>
+ </div>
+ <div>
+ <label for="email" class="block text-sm font-medium leading-6 text-gray-900">
+ <i18n.Translate>Email</i18n.Translate>
+ </label>
+ <div class="mt-2">
+ <input
+ autoFocus
+ type="text"
+ name="email"
+ id="email"
+ class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ value={email ?? ""}
+ enterkeyhint="next"
+ placeholder="your email"
+ autocomplete="email"
+ onInput={(e): void => {
+ setEmail(e.currentTarget.value);
+ }}
+ />
+ <ShowInputErrorLabel
+ message={errors?.email}
+ isDirty={email !== undefined}
+ />
+ </div>
+ </div> */}
+
+ <div class="flex w-full justify-between">
+ <a
+ name="cancel"
+ href={routeCancel.url({})}
+ class="ring-1 ring-gray-600 rounded-md bg-white disabled:bg-gray-300 px-3 py-1.5 text-sm font-semibold leading-6 text-black shadow-sm hover:bg-white-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2"
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </a>
+ <button
+ type="submit"
+ name="register"
+ class=" rounded-md bg-indigo-600 disabled:bg-gray-300 px-3 py-1.5 text-sm font-semibold leading-6 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"
+ disabled={!!errors}
+ onClick={async (e) => {
+ e.preventDefault();
+
+ doRegistrationStep();
+ }}
+ >
+ <i18n.Translate>Register</i18n.Translate>
+ </button>
+ </div>
+ </form>
+
+ {settings.allowRandomAccountCreation && (
+ <p class="mt-10 text-center text-sm text-gray-500 border-t">
+ <button
+ type="submit"
+ name="create random"
+ class="flex mt-4 w-full justify-center rounded-md bg-green-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-green-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600"
+ onClick={(e) => {
+ e.preventDefault();
+ doRandomRegistration();
+ }}
+ >
+ <i18n.Translate>Create a random temporary user</i18n.Translate>
+ </button>
+ </p>
+ )}
+ </div>
+ </div>
+ </Fragment>
+ );
+}
+
+function capitalizeFirstLetter(str: string) {
+ return str.charAt(0).toUpperCase() + str.slice(1);
+}
diff --git a/packages/bank-ui/src/pages/ShowNotifications.tsx b/packages/bank-ui/src/pages/ShowNotifications.tsx
new file mode 100644
index 000000000..fe041fb19
--- /dev/null
+++ b/packages/bank-ui/src/pages/ShowNotifications.tsx
@@ -0,0 +1,55 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { useNotifications } from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
+import { Time } from "../components/Time.js";
+
+export function ShowNotifications(): VNode {
+ const ns = useNotifications();
+ if (!ns.length) {
+ return <div>no notifications</div>;
+ }
+ return (
+ <div>
+ <p>Notifications</p>
+ <table>
+ <thead></thead>
+ <tbody>
+ {ns.map((n, idx) => {
+ return (
+ <tr key={idx}>
+ <td>
+ <Time
+ timestamp={n.message.when}
+ format="dd/MM/yyyy HH:mm:ss"
+ />
+ </td>
+ <td>{n.message.title}</td>
+ <td>
+ {n.message.type === "error"
+ ? n.message.description
+ : undefined}
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ {/* <ToastBanner all /> */}
+ </div>
+ );
+}
diff --git a/packages/bank-ui/src/pages/SolveChallengePage.tsx b/packages/bank-ui/src/pages/SolveChallengePage.tsx
new file mode 100644
index 000000000..624890468
--- /dev/null
+++ b/packages/bank-ui/src/pages/SolveChallengePage.tsx
@@ -0,0 +1,793 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ AbsoluteTime,
+ Amounts,
+ Duration,
+ HttpStatusCode,
+ TalerCorebankApi,
+ TalerError,
+ TalerErrorCode,
+ TranslatedString,
+ assertUnreachable,
+ parsePaytoUri,
+} from "@gnu-taler/taler-util";
+import {
+ Attention,
+ Loading,
+ LocalNotificationBanner,
+ ShowInputErrorLabel,
+ useLocalNotification,
+ useNavigationContext,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useEffect, useState } from "preact/hooks";
+import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js";
+import { Time } from "../components/Time.js";
+import { useBankCoreApiContext } from "@gnu-taler/web-util/browser";
+import { useWithdrawalDetails } from "../hooks/account.js";
+import { ChallengeInProgess, useBankState } from "../hooks/bank-state.js";
+import { useConversionInfo } from "../hooks/regional.js";
+import { useSessionState } from "../hooks/session.js";
+import { RouteDefinition } from "@gnu-taler/web-util/browser";
+import { undefinedIfEmpty } from "../utils.js";
+import { RenderAmount } from "./PaytoWireTransferForm.js";
+import { OperationNotFound } from "./WithdrawalQRCode.js";
+import { IdempotencyRetry } from "../../../taler-util/lib/http-client/utils.js";
+
+const TAN_PREFIX = "T-";
+const TAN_REGEX = /^([Tt](-)?)?[0-9]*$/;
+export function SolveChallengePage({
+ onChallengeCompleted,
+ routeClose,
+}: {
+ onChallengeCompleted: () => void;
+ routeClose: RouteDefinition;
+}): VNode {
+ const {
+ lib: { bank: api },
+ } = useBankCoreApiContext();
+ const { i18n } = useTranslationContext();
+ const [bankState, updateBankState] = useBankState();
+ const [code, setCode] = useState<string | undefined>(undefined);
+ const [notification, notify, handleError] = useLocalNotification();
+ const { state } = useSessionState();
+ const creds = state.status !== "loggedIn" ? undefined : state;
+ const { navigateTo } = useNavigationContext();
+
+ if (!bankState.currentChallenge) {
+ return (
+ <div>
+ <span>no challenge to solve </span>
+ <a
+ href={routeClose.url({})}
+ name="close"
+ 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"
+ >
+ <i18n.Translate>Continue</i18n.Translate>
+ </a>
+ </div>
+ );
+ }
+
+ const ch = bankState.currentChallenge;
+ const errors = undefinedIfEmpty({
+ code: !code
+ ? i18n.str`Required`
+ : !TAN_REGEX.test(code)
+ ? i18n.str`Confirmation codes are numerical, possibly beginning with 'T-.'`
+ : undefined,
+ });
+
+ async function startChallenge() {
+ if (!creds) return;
+ await handleError(async () => {
+ const resp = await api.sendChallenge(creds, ch.id);
+ if (resp.type === "ok") {
+ const newCh = structuredClone(ch);
+ newCh.sent = AbsoluteTime.now();
+ newCh.info = resp.body;
+ updateBankState("currentChallenge", newCh);
+ } else {
+ const newCh = structuredClone(ch);
+ newCh.sent = AbsoluteTime.now();
+ newCh.info = undefined;
+ updateBankState("currentChallenge", newCh);
+ switch (resp.case) {
+ case HttpStatusCode.NotFound:
+ return notify({
+ type: "error",
+ title: i18n.str`Cashout not found. It may be also mean that it was already aborted.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case HttpStatusCode.Unauthorized:
+ return notify({
+ type: "error",
+ title: i18n.str`Cashout not found. It may be also mean that it was already aborted.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case TalerErrorCode.BANK_TAN_CHANNEL_SCRIPT_FAILED:
+ return notify({
+ type: "error",
+ title: i18n.str`Cashout not found. It may be also mean that it was already aborted.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ default:
+ assertUnreachable(resp);
+ }
+ }
+ });
+ }
+
+ async function completeChallenge() {
+ if (!creds || !code) return;
+ const tan = code.toUpperCase().startsWith(TAN_PREFIX)
+ ? code.substring(TAN_PREFIX.length)
+ : code;
+ await handleError(async () => {
+ {
+ const resp = await api.confirmChallenge(creds, ch.id, { tan });
+ if (resp.type === "fail") {
+ setCode("");
+ switch (resp.case) {
+ case HttpStatusCode.NotFound:
+ return notify({
+ type: "error",
+ title: i18n.str`Challenge not found.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case HttpStatusCode.Unauthorized:
+ return notify({
+ type: "error",
+ title: i18n.str`This user is not authorized to complete this challenge.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case HttpStatusCode.TooManyRequests:
+ return notify({
+ type: "error",
+ title: i18n.str`Too many attempts, try another code.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case TalerErrorCode.BANK_TAN_CHALLENGE_FAILED:
+ return notify({
+ type: "error",
+ title: i18n.str`The confirmation code is wrong, try again.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case TalerErrorCode.BANK_TAN_CHALLENGE_EXPIRED:
+ return notify({
+ type: "error",
+ title: i18n.str`The operation expired.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ default:
+ assertUnreachable(resp);
+ }
+ }
+ }
+ {
+ const resp = await (async (ch: ChallengeInProgess) => {
+ switch (ch.operation) {
+ case "delete-account":
+ return await api.deleteAccount(creds, ch.id);
+ case "update-account":
+ return await api.updateAccount(creds, ch.request, ch.id);
+ 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);
+ case "confirm-withdrawal":
+ return await api.confirmWithdrawalById(creds, ch.request, ch.id);
+ case "create-cashout":
+ return await api.createCashout(creds, ch.request, ch.id);
+ default:
+ assertUnreachable(ch);
+ }
+ })(ch);
+
+ if (resp.type === "fail") {
+ if (resp.case !== HttpStatusCode.Accepted) {
+ return notify({
+ type: "error",
+ title: i18n.str`The operation failed.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ }
+ // another challenge required, save the request and the ID
+ // @ts-expect-error no need to check the type of request, since it will be the same as the previous request
+ updateBankState("currentChallenge", {
+ operation: ch.operation,
+ id: String(resp.body.challenge_id),
+ location: ch.location,
+ sent: AbsoluteTime.never(),
+ request: ch.request,
+ });
+ return notify({
+ type: "info",
+ title: i18n.str`The operation needs another confirmation to complete.`,
+ when: AbsoluteTime.now(),
+ });
+ }
+ updateBankState("currentChallenge", undefined);
+ return onChallengeCompleted();
+ }
+ });
+ }
+
+ return (
+ <Fragment>
+ <LocalNotificationBanner notification={notification} />
+ <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-6 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg">
+ <div class="px-4 sm:px-0">
+ <h2 class="text-base font-semibold leading-7 text-gray-900">
+ <span
+ class="text-sm text-black font-semibold leading-6 "
+ id="availability-label"
+ >
+ <i18n.Translate>Confirm the operation</i18n.Translate>
+ </span>
+ </h2>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ This operation is protected with second factor authentication. In
+ order to complete it we need to verify your identity using the
+ authentication channel you provided.
+ </i18n.Translate>
+ </p>
+ </div>
+
+ <div class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2">
+ <ChallengeDetails
+ challenge={bankState.currentChallenge}
+ onStart={startChallenge}
+ onCancel={() => {
+ updateBankState("currentChallenge", undefined);
+ navigateTo(ch.location);
+ }}
+ />
+ {ch.info && (
+ <div class="mt-2">
+ <form
+ class="bg-white shadow-sm ring-1 ring-gray-900/5"
+ autoCapitalize="none"
+ autoCorrect="off"
+ onSubmit={(e) => {
+ e.preventDefault();
+ }}
+ >
+ <div class="px-4 py-4">
+ <label for="withdraw-amount">
+ <i18n.Translate>Enter the confirmation code</i18n.Translate>
+ </label>
+ <div class="mt-2">
+ <div class="relative rounded-md shadow-sm">
+ <input
+ type="text"
+ // class="block w-full rounded-md border-0 py-1.5 pl-16 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ aria-describedby="answer"
+ autoFocus
+ class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ value={code ?? ""}
+ required
+ onPaste={(e) => {
+ e.preventDefault();
+ const pasted = e.clipboardData?.getData("text/plain");
+ if (!pasted) return;
+ if (pasted.toUpperCase().startsWith(TAN_PREFIX)) {
+ const sub = pasted.substring(TAN_PREFIX.length);
+ setCode(sub);
+ return;
+ }
+ setCode(pasted);
+ }}
+ name="answer"
+ id="answer"
+ autocomplete="off"
+ onChange={(e): void => {
+ setCode(e.currentTarget.value);
+ }}
+ />
+ </div>
+ <ShowInputErrorLabel
+ message={errors?.code}
+ isDirty={code !== undefined}
+ />
+ </div>
+ <p class="mt-2 text-sm text-gray-500">
+ {((ch: TalerCorebankApi.TanChannel): VNode => {
+ switch (ch) {
+ case TalerCorebankApi.TanChannel.SMS:
+ return (
+ <i18n.Translate>
+ You should have received a code in your phone.
+ </i18n.Translate>
+ );
+ case TalerCorebankApi.TanChannel.EMAIL:
+ return (
+ <i18n.Translate>
+ You should have received a code in your email.
+ </i18n.Translate>
+ );
+ default:
+ assertUnreachable(ch);
+ }
+ })(ch.info.tan_channel)}
+ </p>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ The confirmation code starts with "{TAN_PREFIX}" followed
+ by numbers.
+ </i18n.Translate>
+ </p>
+ </div>
+ <div class="flex items-center justify-between border-gray-900/10 px-4 py-4 ">
+ <div />
+ <button
+ type="submit"
+ name="confirm"
+ class="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"
+ disabled={!!errors}
+ onClick={(e) => {
+ completeChallenge();
+ e.preventDefault();
+ }}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </button>
+ </div>
+ </form>
+ </div>
+ )}
+ </div>
+ </div>
+ </Fragment>
+ );
+}
+
+function ChallengeDetails({
+ challenge,
+ onStart,
+ onCancel,
+}: {
+ challenge: ChallengeInProgess;
+ onStart: () => void;
+ onCancel: () => void;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const { config } = useBankCoreApiContext();
+
+ const firstTime = AbsoluteTime.isNever(challenge.sent);
+ useEffect(() => {
+ if (firstTime) {
+ onStart();
+ }
+ }, []);
+
+ const subtitle = ((op): TranslatedString => {
+ switch (op) {
+ case "delete-account":
+ return i18n.str`Removing account`;
+ case "update-account":
+ return i18n.str`Updating account values`;
+ case "update-password":
+ return i18n.str`Updating password`;
+ case "create-transaction":
+ return i18n.str`Making a wire transfer`;
+ case "confirm-withdrawal":
+ return i18n.str`Confirming withdrawal`;
+ case "create-cashout":
+ return i18n.str`Making a cashout`;
+ }
+ })(challenge.operation);
+
+ return (
+ <div class="px-4 mt-4 ">
+ <div class="w-full">
+ <div class="border-gray-100">
+ <h2 class="text-base font-semibold leading-10 text-gray-900">
+ <span class=" text-black font-semibold leading-6 ">
+ <i18n.Translate>Operation:</i18n.Translate>
+ </span>{" "}
+ &nbsp;
+ <span class=" text-black font-normal leading-6 ">{subtitle}</span>
+ </h2>
+ <dl class="divide-y divide-gray-100">
+ {((): VNode => {
+ switch (challenge.operation) {
+ case "delete-account":
+ return (
+ <Fragment>
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">
+ <i18n.Translate>Type</i18n.Translate>
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ <i18n.Translate>
+ Updating account settings
+ </i18n.Translate>
+ </dd>
+ </div>
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">
+ <i18n.Translate>Account</i18n.Translate>
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {challenge.request}
+ </dd>
+ </div>
+ </Fragment>
+ );
+ case "create-transaction": {
+ const payto = parsePaytoUri(challenge.request.payto_uri)!;
+ return (
+ <Fragment>
+ {challenge.request.amount && (
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">
+ <i18n.Translate>Amount</i18n.Translate>
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ <RenderAmount
+ value={Amounts.parseOrThrow(
+ challenge.request.amount,
+ )}
+ spec={config.currency_specification}
+ />
+ </dd>
+ </div>
+ )}
+ {payto.isKnown && payto.targetType === "iban" && (
+ <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>To account</i18n.Translate>
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {payto.iban}
+ </dd>
+ </div>
+ )}
+ </Fragment>
+ );
+ }
+ case "confirm-withdrawal":
+ return <ShowWithdrawalDetails id={challenge.request} />;
+ case "create-cashout": {
+ return <ShowCashoutDetails request={challenge.request} />;
+ }
+ case "update-account": {
+ return (
+ <Fragment>
+ {challenge.request.cashout_payto_uri !== undefined && (
+ <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>Cashout account</i18n.Translate>
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {challenge.request.cashout_payto_uri}
+ </dd>
+ </div>
+ )}
+ {challenge.request.contact_data?.email !== undefined && (
+ <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>Email</i18n.Translate>
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {challenge.request.contact_data?.email}
+ </dd>
+ </div>
+ )}
+ {challenge.request.contact_data?.phone !== undefined && (
+ <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>Phone</i18n.Translate>
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {challenge.request.contact_data?.phone}
+ </dd>
+ </div>
+ )}
+ {challenge.request.debit_threshold !== undefined && (
+ <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>Debit threshold</i18n.Translate>
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ <RenderAmount
+ value={Amounts.parseOrThrow(
+ challenge.request.debit_threshold,
+ )}
+ spec={config.currency_specification}
+ />
+ </dd>
+ </div>
+ )}
+ {challenge.request.is_public !== undefined && (
+ <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>
+ Is this account public?
+ </i18n.Translate>
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {challenge.request.is_public
+ ? i18n.str`Enable`
+ : i18n.str`Disable`}
+ </dd>
+ </div>
+ )}
+ {challenge.request.name !== undefined && (
+ <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>Name</i18n.Translate>
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {challenge.request.name}
+ </dd>
+ </div>
+ )}
+ {challenge.request.tan_channel !== undefined && (
+ <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>
+ Authentication channel
+ </i18n.Translate>
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {challenge.request.tan_channel ?? i18n.str`Remove`}
+ </dd>
+ </div>
+ )}
+ </Fragment>
+ );
+ }
+ case "update-password": {
+ return (
+ <Fragment>
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">
+ <i18n.Translate>New password</i18n.Translate>
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {challenge.request.new_password}
+ </dd>
+ </div>
+ </Fragment>
+ );
+ }
+ default:
+ assertUnreachable(challenge);
+ }
+ })()}
+ </dl>
+ {challenge.info && (
+ <h2 class="text-base font-semibold leading-7 text-gray-900 mt-4">
+ <span
+ class="text-sm text-black font-semibold leading-6 "
+ id="availability-label"
+ >
+ <i18n.Translate>Challenge details</i18n.Translate>
+ </span>
+ </h2>
+ )}
+ <dl class="divide-y divide-gray-100">
+ {challenge.sent.t_ms !== "never" && (
+ <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>Sent at</i18n.Translate>
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ <Time
+ format="dd/MM/yyyy HH:mm:ss"
+ timestamp={challenge.sent}
+ relative={Duration.fromSpec({ days: 1 })}
+ />
+ </dd>
+ </div>
+ )}
+ {challenge.info && (
+ <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">
+ {((ch: TalerCorebankApi.TanChannel): VNode => {
+ switch (ch) {
+ case TalerCorebankApi.TanChannel.SMS:
+ return <i18n.Translate>To phone</i18n.Translate>;
+ case TalerCorebankApi.TanChannel.EMAIL:
+ return <i18n.Translate>To email</i18n.Translate>;
+ default:
+ assertUnreachable(ch);
+ }
+ })(challenge.info.tan_channel)}
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {challenge.info.tan_info}
+ </dd>
+ </div>
+ )}
+ </dl>
+ </div>
+ <div class="mt-6 mb-4 flex justify-between">
+ <button
+ type="button"
+ name="cancel"
+ class="text-sm font-semibold leading-6 text-gray-900"
+ onClick={onCancel}
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ {challenge.info ? (
+ <button
+ type="submit"
+ name="send again"
+ class="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"
+ onClick={(e) => {
+ onStart();
+ e.preventDefault();
+ }}
+ >
+ <i18n.Translate>Send again</i18n.Translate>
+ </button>
+ ) : (
+ <div> sending code ...</div>
+ )}
+ </div>
+ </div>
+ </div>
+ );
+}
+
+function ShowWithdrawalDetails({ id }: { id: string }): VNode {
+ const details = useWithdrawalDetails(id);
+ const { i18n } = useTranslationContext();
+ const { config } = useBankCoreApiContext();
+ if (!details) {
+ return <Loading />;
+ }
+ if (details instanceof TalerError) {
+ return <ErrorLoadingWithDebug error={details} />;
+ }
+ if (details.type === "fail") {
+ switch (details.case) {
+ case HttpStatusCode.BadRequest:
+ case HttpStatusCode.NotFound:
+ return <OperationNotFound routeClose={undefined} />;
+ default:
+ assertUnreachable(details);
+ }
+ }
+
+ return (
+ <Fragment>
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">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}
+ />
+ </dd>
+ </div>
+ {details.body.selected_reserve_pub !== undefined && (
+ <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>Withdraw id</i18n.Translate>
+ </dt>
+ <dd
+ class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"
+ title={details.body.selected_reserve_pub}
+ >
+ {details.body.selected_reserve_pub.substring(0, 16)}...
+ </dd>
+ </div>
+ )}
+ {details.body.selected_exchange_account !== undefined && (
+ <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>To account</i18n.Translate>
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {details.body.selected_exchange_account}
+ </dd>
+ </div>
+ )}
+ </Fragment>
+ );
+}
+
+function ShowCashoutDetails({
+ request,
+}: {
+ request: TalerCorebankApi.CashoutRequest;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const info = useConversionInfo();
+ if (!info) {
+ return <Loading />;
+ }
+
+ if (info instanceof TalerError) {
+ return <ErrorLoadingWithDebug error={info} />;
+ }
+ if (info.type === "fail") {
+ switch (info.case) {
+ case HttpStatusCode.NotImplemented: {
+ return (
+ <Attention type="danger" title={i18n.str`Cashout are disabled`}>
+ <i18n.Translate>
+ Cashout should be enable by configuration and the conversion rate
+ should be initialized with fee, ratio and rounding mode.
+ </i18n.Translate>
+ </Attention>
+ );
+ }
+ default:
+ assertUnreachable(info.case);
+ }
+ }
+
+ return (
+ <Fragment>
+ {request.subject !== undefined && (
+ <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>Subject</i18n.Translate>
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {request.subject}
+ </dd>
+ </div>
+ )}
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">Debit</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ <RenderAmount
+ value={Amounts.parseOrThrow(request.amount_credit)}
+ spec={info.body.regional_currency_specification}
+ />
+ </dd>
+ </div>
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">Credit</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ <RenderAmount
+ value={Amounts.parseOrThrow(request.amount_credit)}
+ spec={info.body.fiat_currency_specification}
+ />
+ </dd>
+ </div>
+ </Fragment>
+ );
+}
diff --git a/packages/bank-ui/src/pages/WalletWithdrawForm.tsx b/packages/bank-ui/src/pages/WalletWithdrawForm.tsx
new file mode 100644
index 000000000..a9c652643
--- /dev/null
+++ b/packages/bank-ui/src/pages/WalletWithdrawForm.tsx
@@ -0,0 +1,404 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ AbsoluteTime,
+ AmountJson,
+ Amounts,
+ HttpStatusCode,
+ TranslatedString,
+ assertUnreachable,
+ parseWithdrawUri,
+} from "@gnu-taler/taler-util";
+import {
+ Attention,
+ LocalNotificationBanner,
+ notifyError,
+ useLocalNotification,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
+import { forwardRef } from "preact/compat";
+import { useState } from "preact/hooks";
+import { useBankCoreApiContext } from "@gnu-taler/web-util/browser";
+import { useSessionState } from "../hooks/session.js";
+import { useBankState } from "../hooks/bank-state.js";
+import { usePreferences } from "../hooks/preferences.js";
+import { RouteDefinition } from "@gnu-taler/web-util/browser";
+import { undefinedIfEmpty } from "../utils.js";
+import { OperationState } from "./OperationState/index.js";
+import {
+ InputAmount,
+ RenderAmount,
+ doAutoFocus,
+} from "./PaytoWireTransferForm.js";
+
+const RefAmount = forwardRef(InputAmount);
+
+function OldWithdrawalForm({
+ onOperationCreated,
+ limit,
+ balance,
+ routeCancel,
+ focus,
+ routeOperationDetails,
+}: {
+ limit: AmountJson;
+ balance: AmountJson;
+ focus?: boolean;
+ routeOperationDetails: RouteDefinition<{ wopid: string }>;
+ onOperationCreated: (wopid: string) => void;
+ routeCancel: RouteDefinition;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const [settings] = usePreferences();
+
+ // const walletInegrationApi = useTalerWalletIntegrationAPI()
+ // const { navigateTo } = useNavigationContext();
+
+ const [bankState, updateBankState] = useBankState();
+ const {
+ lib: { bank: api },
+ config,
+ } = useBankCoreApiContext();
+
+ const { state: credentials } = useSessionState();
+ const creds = credentials.status !== "loggedIn" ? undefined : credentials;
+
+ const [amountStr, setAmountStr] = useState<string | undefined>(
+ `${settings.maxWithdrawalAmount}`,
+ );
+ const [notification, notify, handleError] = useLocalNotification();
+
+ if (bankState.currentWithdrawalOperationId) {
+ // FIXME: doing the preventDefault is not optimal
+
+ // const suri = stringifyWithdrawUri({
+ // bankIntegrationApiBaseUrl: api.getIntegrationAPI().baseUrl,
+ // withdrawalOperationId: bankState.currentWithdrawalOperationId,
+ // });
+ // const uri = parseWithdrawUri(suri)!
+ const url = routeOperationDetails.url({
+ wopid: bankState.currentWithdrawalOperationId,
+ });
+ return (
+ <Attention
+ type="warning"
+ title={i18n.str`There is an operation already`}
+ onClose={() => {
+ updateBankState("currentWithdrawalOperationId", undefined);
+ }}
+ >
+ <span ref={focus ? doAutoFocus : undefined} />
+ <i18n.Translate>Complete the operation in</i18n.Translate>{" "}
+ <a
+ class="font-semibold text-yellow-700 hover:text-yellow-600"
+ name="complete operation"
+ href={url}
+ // onClick={(e) => {
+ // e.preventDefault()
+ // walletInegrationApi.publishTalerAction(uri, () => {
+ // navigateTo(url)
+ // })
+ // }}
+ >
+ <i18n.Translate>this page</i18n.Translate>
+ </a>
+ </Attention>
+ );
+ }
+
+ const trimmedAmountStr = amountStr?.trim();
+
+ const parsedAmount = trimmedAmountStr
+ ? Amounts.parse(`${limit.currency}:${trimmedAmountStr}`)
+ : undefined;
+
+ const errors = undefinedIfEmpty({
+ amount:
+ trimmedAmountStr == null
+ ? i18n.str`Required`
+ : !parsedAmount
+ ? i18n.str`Invalid`
+ : Amounts.cmp(limit, parsedAmount) === -1
+ ? i18n.str`Balance is not enough`
+ : undefined,
+ });
+
+ async function doStart() {
+ if (!parsedAmount || !creds) return;
+ await handleError(async () => {
+ const resp = await api.createWithdrawal(creds, {
+ amount: Amounts.stringify(parsedAmount),
+ });
+ if (resp.type === "ok") {
+ const uri = parseWithdrawUri(resp.body.taler_withdraw_uri);
+ if (!uri) {
+ return notifyError(
+ i18n.str`Server responded with an invalid withdraw URI`,
+ i18n.str`Withdraw URI: ${resp.body.taler_withdraw_uri}`,
+ );
+ } else {
+ updateBankState(
+ "currentWithdrawalOperationId",
+ uri.withdrawalOperationId,
+ );
+ onOperationCreated(uri.withdrawalOperationId);
+ }
+ } else {
+ switch (resp.case) {
+ case HttpStatusCode.Conflict: {
+ notify({
+ type: "error",
+ title: i18n.str`The operation was rejected due to insufficient funds`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ break;
+ }
+ case HttpStatusCode.Unauthorized: {
+ notify({
+ type: "error",
+ title: i18n.str`The operation was rejected due to insufficient funds`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ break;
+ }
+ case HttpStatusCode.NotFound: {
+ notify({
+ type: "error",
+ title: i18n.str`Account not found`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ break;
+ }
+ default:
+ assertUnreachable(resp);
+ }
+ }
+ });
+ }
+
+ return (
+ <form
+ class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2 mt-4"
+ autoCapitalize="none"
+ autoCorrect="off"
+ onSubmit={(e) => {
+ e.preventDefault();
+ }}
+ >
+ <LocalNotificationBanner notification={notification} />
+
+ <div class="px-4 py-6 ">
+ <div class="grid max-w-xs grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
+ <div class="sm:col-span-5">
+ <label for="withdraw-amount">{i18n.str`Amount`}</label>
+ <RefAmount
+ currency={limit.currency}
+ value={amountStr}
+ name="withdraw-amount"
+ onChange={(v) => {
+ setAmountStr(v);
+ }}
+ error={errors?.amount}
+ ref={focus ? doAutoFocus : undefined}
+ />
+ </div>
+ </div>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ Current balance is{" "}
+ <RenderAmount
+ value={balance}
+ spec={config.currency_specification}
+ />
+ </i18n.Translate>
+ </p>
+ {Amounts.cmp(limit, balance) > 0 ? (
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ Your account allows you to withdraw{" "}
+ <RenderAmount
+ value={limit}
+ spec={config.currency_specification}
+ />
+ </i18n.Translate>
+ </p>
+ ) : undefined}
+ <div class="mt-4">
+ <div class="sm:inline">
+ <button
+ type="button"
+ name="set 50"
+ class=" inline-flex px-6 py-4 text-sm items-center rounded-l-md bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10"
+ onClick={(e) => {
+ e.preventDefault();
+ setAmountStr("50.00");
+ }}
+ >
+ 50.00
+ </button>
+ <button
+ type="button"
+ name="set 25"
+ class=" -ml-px -mr-px inline-flex px-6 py-4 text-sm items-center rounded-r-md sm:rounded-none bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10"
+ onClick={(e) => {
+ e.preventDefault();
+ setAmountStr("25.00");
+ }}
+ >
+ 25.00
+ </button>
+ </div>
+ <div class="mt-4 sm:inline">
+ <button
+ type="button"
+ name="set 10"
+ class=" -ml-px -mr-px inline-flex px-6 py-4 text-sm items-center rounded-l-md sm:rounded-none bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10"
+ onClick={(e) => {
+ e.preventDefault();
+ setAmountStr("10.00");
+ }}
+ >
+ 10.00
+ </button>
+ <button
+ type="button"
+ name="set 5"
+ class=" inline-flex px-6 py-4 text-sm items-center rounded-r-md bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10"
+ onClick={(e) => {
+ e.preventDefault();
+ setAmountStr("5.00");
+ }}
+ >
+ 5.00
+ </button>
+ </div>
+ </div>
+ </div>
+ <div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
+ <a
+ href={routeCancel.url({})}
+ name="cancel"
+ class="text-sm font-semibold leading-6 text-gray-900"
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </a>
+ <button
+ type="submit"
+ name="continue"
+ class="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"
+ // disabled={isRawPayto ? !!errorsPayto : !!errorsWire}
+ onClick={(e) => {
+ e.preventDefault();
+ doStart();
+ }}
+ >
+ <i18n.Translate>Continue</i18n.Translate>
+ </button>
+ </div>
+ </form>
+ );
+}
+
+export function WalletWithdrawForm({
+ focus,
+ limit,
+ balance,
+ routeCancel,
+ onAuthorizationRequired,
+ onOperationCreated,
+ onOperationAborted,
+ routeOperationDetails,
+}: {
+ limit: AmountJson;
+ balance: AmountJson;
+ focus?: boolean;
+ routeOperationDetails: RouteDefinition<{ wopid: string }>;
+ onAuthorizationRequired: () => void;
+ onOperationCreated: (wopid: string) => void;
+ onOperationAborted: () => void;
+ routeCancel: RouteDefinition;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const [settings, updateSettings] = usePreferences();
+
+ return (
+ <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-6 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg">
+ <div class="px-4 sm:px-0">
+ <h2 class="text-base font-semibold leading-7 text-gray-900">
+ <i18n.Translate>Prepare your Taler wallet</i18n.Translate>
+ </h2>
+ <p class="mt-1 text-sm text-gray-500">
+ <i18n.Translate>
+ After using your wallet you will need to confirm or cancel the
+ operation on this site.
+ </i18n.Translate>
+ </p>
+ </div>
+
+ <div class="col-span-2">
+ {settings.showInstallWallet && (
+ <Attention
+ title={i18n.str`You need a Taler wallet`}
+ onClose={() => {
+ updateSettings("showInstallWallet", false);
+ }}
+ >
+ <i18n.Translate>
+ If you don't have one yet you can follow the instruction in
+ </i18n.Translate>{" "}
+ <a
+ target="_blank"
+ name="wallet page"
+ rel="noreferrer noopener"
+ class="font-semibold text-blue-700 hover:text-blue-600"
+ href="https://taler.net/en/wallet.html"
+ >
+ <i18n.Translate>this page</i18n.Translate>
+ </a>
+ </Attention>
+ )}
+
+ {!settings.fastWithdrawal ? (
+ <OldWithdrawalForm
+ focus={focus}
+ routeOperationDetails={routeOperationDetails}
+ limit={limit}
+ balance={balance}
+ routeCancel={routeCancel}
+ onOperationCreated={onOperationCreated}
+ />
+ ) : (
+ <OperationState
+ currency={limit.currency}
+ onAuthorizationRequired={onAuthorizationRequired}
+ routeClose={routeCancel}
+ routeHere={routeOperationDetails}
+ onAbort={onOperationAborted}
+ // route={routeCancel}
+ />
+ )}
+ </div>
+ </div>
+ );
+}
diff --git a/packages/bank-ui/src/pages/WireTransfer.tsx b/packages/bank-ui/src/pages/WireTransfer.tsx
new file mode 100644
index 000000000..f45390938
--- /dev/null
+++ b/packages/bank-ui/src/pages/WireTransfer.tsx
@@ -0,0 +1,119 @@
+/*
+ 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 {
+ Amounts,
+ HttpStatusCode,
+ TalerError,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
+import {
+ Loading,
+ notifyInfo,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js";
+import { useAccountDetails } from "../hooks/account.js";
+import { useSessionState } from "../hooks/session.js";
+import { LoginForm } from "./LoginForm.js";
+import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js";
+import { RouteDefinition } from "@gnu-taler/web-util/browser";
+
+export function WireTransfer({
+ toAccount,
+ withSubject,
+ withAmount,
+ onAuthorizationRequired,
+ routeCancel,
+ routeHere,
+ onSuccess,
+}: {
+ onSuccess?: () => void;
+ routeHere: RouteDefinition<{
+ account?: string;
+ subject?: string;
+ amount?: string;
+ }>;
+ toAccount?: string;
+ withSubject?: string;
+ withAmount?: string;
+ routeCancel?: RouteDefinition;
+ onAuthorizationRequired: () => void;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const r = useSessionState();
+ const account = r.state.status !== "loggedOut" ? r.state.username : "admin";
+ const result = useAccountDetails(account);
+
+ if (!result) {
+ return <Loading />;
+ }
+ if (result instanceof TalerError) {
+ return <ErrorLoadingWithDebug error={result} />;
+ }
+ if (result.type === "fail") {
+ switch (result.case) {
+ case HttpStatusCode.Unauthorized:
+ return <LoginForm currentUser={account} />;
+ case HttpStatusCode.NotFound:
+ return <LoginForm currentUser={account} />;
+ default:
+ assertUnreachable(result);
+ }
+ }
+ const { body: data } = result;
+
+ const balance = Amounts.parseOrThrow(data.balance.amount);
+ const balanceIsDebit = data.balance.credit_debit_indicator == "debit";
+
+ const debitThreshold = Amounts.parseOrThrow(data.debit_threshold);
+
+ if (!balance) return <Fragment />;
+
+ const limit = balanceIsDebit
+ ? Amounts.sub(debitThreshold, balance).amount
+ : Amounts.add(balance, debitThreshold).amount;
+
+ const positiveBalance = balanceIsDebit
+ ? Amounts.zeroOfAmount(balance)
+ : balance;
+ return (
+ <div class="px-4 mt-8">
+ <div class="sm:flex sm:items-center mb-4">
+ <div class="sm:flex-auto">
+ <h1 class="text-base font-semibold leading-6 text-gray-900">
+ <i18n.Translate>Make a wire transfer</i18n.Translate>
+ </h1>
+ </div>
+ </div>
+
+ <PaytoWireTransferForm
+ withAccount={toAccount}
+ withAmount={withAmount}
+ balance={positiveBalance}
+ withSubject={withSubject}
+ routeHere={routeHere}
+ limit={limit}
+ onAuthorizationRequired={onAuthorizationRequired}
+ onSuccess={() => {
+ notifyInfo(i18n.str`Wire transfer created!`);
+ if (onSuccess) onSuccess();
+ }}
+ routeCancel={routeCancel}
+ />
+ </div>
+ );
+}
diff --git a/packages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx b/packages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
new file mode 100644
index 000000000..853dd7bae
--- /dev/null
+++ b/packages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
@@ -0,0 +1,425 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ AbsoluteTime,
+ AmountJson,
+ HttpStatusCode,
+ PaytoUri,
+ PaytoUriIBAN,
+ PaytoUriTalerBank,
+ TalerErrorCode,
+ TranslatedString,
+ WithdrawUriResult,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
+import {
+ Attention,
+ LocalNotificationBanner,
+ notifyInfo,
+ useLocalNotification,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { ComponentChildren, Fragment, VNode, h } from "preact";
+import { mutate } from "swr";
+import { useBankCoreApiContext } from "@gnu-taler/web-util/browser";
+import { useBankState } from "../hooks/bank-state.js";
+import { usePreferences } from "../hooks/preferences.js";
+import { useSessionState } from "../hooks/session.js";
+import { RouteDefinition } from "@gnu-taler/web-util/browser";
+import { LoginForm } from "./LoginForm.js";
+import { RenderAmount } from "./PaytoWireTransferForm.js";
+
+interface Props {
+ onAborted: () => void;
+ withdrawUri: WithdrawUriResult;
+ routeHere: RouteDefinition<{ wopid: string }>;
+ details: {
+ account: PaytoUri;
+ reserve: string;
+ username: string;
+ amount: AmountJson;
+ };
+ onAuthorizationRequired: () => void;
+}
+/**
+ * Additional authentication required to complete the operation.
+ * Not providing a back button, only abort.
+ */
+export function WithdrawalConfirmationQuestion({
+ onAborted,
+ details,
+ onAuthorizationRequired,
+ routeHere,
+ withdrawUri,
+}: Props): VNode {
+ const { i18n } = useTranslationContext();
+ const [settings] = usePreferences();
+ const { state: credentials } = useSessionState();
+ const creds = credentials.status !== "loggedIn" ? undefined : credentials;
+ const [, updateBankState] = useBankState();
+
+ const [notification, notify, handleError] = useLocalNotification();
+
+ const {
+ config,
+ lib: { bank: api },
+ } = useBankCoreApiContext();
+
+ async function doTransfer() {
+ await handleError(async () => {
+ if (!creds) return;
+ const resp = await api.confirmWithdrawalById(
+ creds,
+ withdrawUri.withdrawalOperationId,
+ );
+ if (resp.type === "ok") {
+ mutate(() => true); // clean any info that we have
+ if (!settings.showWithdrawalSuccess) {
+ notifyInfo(i18n.str`Wire transfer completed!`);
+ }
+ } else {
+ switch (resp.case) {
+ case TalerErrorCode.BANK_CONFIRM_ABORT_CONFLICT:
+ return notify({
+ type: "error",
+ title: i18n.str`The withdrawal has been aborted previously and can't be confirmed`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case TalerErrorCode.BANK_CONFIRM_INCOMPLETE:
+ return notify({
+ type: "error",
+ title: i18n.str`The withdrawal operation can't be confirmed before a wallet accepted the transaction.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case HttpStatusCode.BadRequest:
+ return notify({
+ type: "error",
+ title: i18n.str`The operation id is invalid.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case HttpStatusCode.NotFound:
+ return notify({
+ type: "error",
+ title: i18n.str`The operation was not found.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case TalerErrorCode.BANK_UNALLOWED_DEBIT:
+ return notify({
+ type: "error",
+ title: i18n.str`Your balance is not enough for the operation.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case HttpStatusCode.Accepted: {
+ updateBankState("currentChallenge", {
+ operation: "confirm-withdrawal",
+ id: String(resp.body.challenge_id),
+ location: routeHere.url({
+ wopid: withdrawUri.withdrawalOperationId,
+ }),
+ sent: AbsoluteTime.never(),
+ request: withdrawUri.withdrawalOperationId,
+ });
+ return onAuthorizationRequired();
+ }
+ default:
+ assertUnreachable(resp);
+ }
+ }
+ });
+ }
+
+ async function doCancel() {
+ await handleError(async () => {
+ if (!creds) return;
+ const resp = await api.abortWithdrawalById(
+ creds,
+ withdrawUri.withdrawalOperationId,
+ );
+ if (resp.type === "ok") {
+ onAborted();
+ } else {
+ switch (resp.case) {
+ case HttpStatusCode.Conflict:
+ return notify({
+ type: "error",
+ title: i18n.str`The reserve operation has been confirmed previously and can't be aborted`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case HttpStatusCode.BadRequest:
+ return notify({
+ type: "error",
+ title: i18n.str`The operation id is invalid.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case HttpStatusCode.NotFound:
+ return notify({
+ type: "error",
+ title: i18n.str`The operation was not found.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ default: {
+ assertUnreachable(resp);
+ }
+ }
+ }
+ });
+ }
+
+ return (
+ <Fragment>
+ <LocalNotificationBanner notification={notification} />
+
+ <div class="bg-white shadow sm:rounded-lg">
+ <div class="px-4 py-5 sm:p-6">
+ <h3 class="text-base font-semibold text-gray-900">
+ <i18n.Translate>Confirm the withdrawal operation</i18n.Translate>
+ </h3>
+ <div class="mt-3 text-sm leading-6">
+ <ShouldBeSameUser username={details.username}>
+ <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-2 bg-gray-100 my-4 px-4 pb-4 rounded-lg">
+ <form
+ class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2"
+ autoCapitalize="none"
+ autoCorrect="off"
+ onSubmit={(e) => {
+ e.preventDefault();
+ }}
+ >
+ <div class="px-4 mt-4">
+ <div class="w-full">
+ <div class="px-4 sm:px-0 text-sm">
+ <p>
+ <i18n.Translate>Wire transfer details</i18n.Translate>
+ </p>
+ </div>
+ <div class="mt-6 border-t border-gray-100">
+ <dl class="divide-y divide-gray-100">
+ {((): VNode => {
+ if (!details.account.isKnown) {
+ return (
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">
+ <i18n.Translate>
+ Payment provider's account
+ </i18n.Translate>
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {details.account.targetPath}
+ </dd>
+ </div>
+ );
+ }
+ switch (details.account.targetType) {
+ case "iban": {
+ const name =
+ details.account.params["receiver-name"];
+ return (
+ <Fragment>
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">
+ <i18n.Translate>
+ Payment provider's account number
+ </i18n.Translate>
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {details.account.iban}
+ </dd>
+ </div>
+ {name && (
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">
+ <i18n.Translate>
+ Payment provider's name
+ </i18n.Translate>
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {name}
+ </dd>
+ </div>
+ )}
+ </Fragment>
+ );
+ }
+ case "x-taler-bank": {
+ const name =
+ details.account.params["receiver-name"];
+ return (
+ <Fragment>
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">
+ <i18n.Translate>
+ Payment provider's account bank
+ hostname
+ </i18n.Translate>
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {details.account.host}
+ </dd>
+ </div>
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">
+ <i18n.Translate>
+ Payment provider's account id
+ </i18n.Translate>
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {details.account.account}
+ </dd>
+ </div>
+ {name && (
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">
+ <i18n.Translate>
+ Payment provider's name
+ </i18n.Translate>
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {name}
+ </dd>
+ </div>
+ )}
+ </Fragment>
+ );
+ }
+ case "bitcoin": {
+ const name =
+ details.account.params["receiver-name"];
+ return (
+ <Fragment>
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">
+ <i18n.Translate>
+ Payment provider's account address
+ </i18n.Translate>
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {details.account.address}
+ </dd>
+ </div>
+ {name && (
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">
+ <i18n.Translate>
+ Payment provider's name
+ </i18n.Translate>
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {name}
+ </dd>
+ </div>
+ )}
+ </Fragment>
+ );
+ }
+ default: {
+ assertUnreachable(details.account);
+ }
+ }
+ })()}
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">
+ <i18n.Translate>Amount</i18n.Translate>
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ <RenderAmount
+ value={details.amount}
+ spec={config.currency_specification}
+ />
+ </dd>
+ </div>
+ </dl>
+ </div>
+ </div>
+ </div>
+
+ <div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
+ <button
+ type="button"
+ name="cancel"
+ class="text-sm font-semibold leading-6 text-gray-900"
+ onClick={doCancel}
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ <button
+ type="submit"
+ name="transfer"
+ class="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"
+ onClick={(e) => {
+ e.preventDefault();
+ doTransfer();
+ }}
+ >
+ <i18n.Translate>Transfer</i18n.Translate>
+ </button>
+ </div>
+ </form>
+ </div>
+ </ShouldBeSameUser>
+ </div>
+ </div>
+ </div>
+ </Fragment>
+ );
+}
+
+export function ShouldBeSameUser({
+ username,
+ children,
+}: {
+ username: string;
+ children: ComponentChildren;
+}): VNode {
+ const { state: credentials } = useSessionState();
+ const { i18n } = useTranslationContext();
+ if (credentials.status === "loggedOut") {
+ return (
+ <Fragment>
+ <Attention type="info" title={i18n.str`Authentication required`} />
+ <LoginForm currentUser={username} fixedUser />
+ </Fragment>
+ );
+ }
+ if (credentials.username !== username) {
+ return (
+ <Fragment>
+ <Attention
+ type="warning"
+ title={i18n.str`This operation was created with other username`}
+ />
+ <LoginForm currentUser={username} fixedUser />
+ </Fragment>
+ );
+ }
+ return <Fragment>{children}</Fragment>;
+}
diff --git a/packages/bank-ui/src/pages/WithdrawalOperationPage.tsx b/packages/bank-ui/src/pages/WithdrawalOperationPage.tsx
new file mode 100644
index 000000000..c0c55f14b
--- /dev/null
+++ b/packages/bank-ui/src/pages/WithdrawalOperationPage.tsx
@@ -0,0 +1,73 @@
+/*
+ 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 { parseWithdrawUri, stringifyWithdrawUri } from "@gnu-taler/taler-util";
+import { Attention, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
+import { useBankCoreApiContext } from "@gnu-taler/web-util/browser";
+import { useBankState } from "../hooks/bank-state.js";
+import { RouteDefinition } from "@gnu-taler/web-util/browser";
+import { WithdrawalQRCode } from "./WithdrawalQRCode.js";
+
+export function WithdrawalOperationPage({
+ operationId,
+ onAuthorizationRequired,
+ onOperationAborted,
+ routeClose,
+ routeWithdrawalDetails,
+}: {
+ onAuthorizationRequired: () => void;
+ operationId: string;
+ purpose: "after-creation" | "after-confirmation";
+ onOperationAborted: () => void;
+ routeClose: RouteDefinition;
+ routeWithdrawalDetails: RouteDefinition<{ wopid: string }>;
+}): VNode {
+ const {
+ lib: { bank: api },
+ } = useBankCoreApiContext();
+ const uri = stringifyWithdrawUri({
+ bankIntegrationApiBaseUrl: api.getIntegrationAPI().href,
+ withdrawalOperationId: operationId,
+ });
+ const parsedUri = parseWithdrawUri(uri);
+ const { i18n } = useTranslationContext();
+ const [, updateBankState] = useBankState();
+
+ if (!parsedUri) {
+ return (
+ <Attention
+ type="danger"
+ title={i18n.str`The Withdrawal URI is not valid`}
+ >
+ {uri}
+ </Attention>
+ );
+ }
+
+ return (
+ <WithdrawalQRCode
+ withdrawUri={parsedUri}
+ routeWithdrawalDetails={routeWithdrawalDetails}
+ onAuthorizationRequired={onAuthorizationRequired}
+ onOperationAborted={() => {
+ updateBankState("currentWithdrawalOperationId", undefined);
+ onOperationAborted();
+ }}
+ routeClose={routeClose}
+ />
+ );
+}
diff --git a/packages/bank-ui/src/pages/WithdrawalQRCode.tsx b/packages/bank-ui/src/pages/WithdrawalQRCode.tsx
new file mode 100644
index 000000000..b61f0cc8f
--- /dev/null
+++ b/packages/bank-ui/src/pages/WithdrawalQRCode.tsx
@@ -0,0 +1,310 @@
+/*
+ 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 {
+ Amounts,
+ HttpStatusCode,
+ TalerError,
+ WithdrawUriResult,
+ assertUnreachable,
+ parsePaytoUri,
+} from "@gnu-taler/taler-util";
+import {
+ Attention,
+ Loading,
+ notifyInfo,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
+import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js";
+import { useWithdrawalDetails } from "../hooks/account.js";
+import { RouteDefinition } from "@gnu-taler/web-util/browser";
+import { QrCodeSection } from "./QrCodeSection.js";
+import { WithdrawalConfirmationQuestion } from "./WithdrawalConfirmationQuestion.js";
+
+interface Props {
+ withdrawUri: WithdrawUriResult;
+ onOperationAborted: () => void;
+ routeClose: RouteDefinition;
+ routeWithdrawalDetails: RouteDefinition<{ wopid: string }>;
+ onAuthorizationRequired: () => void;
+}
+/**
+ * Offer the QR code (and a clickable taler://-link) to
+ * permit the passing of exchange and reserve details to
+ * the bank. Poll the backend until such operation is done.
+ */
+export function WithdrawalQRCode({
+ withdrawUri,
+ onOperationAborted,
+ routeClose,
+ routeWithdrawalDetails,
+ onAuthorizationRequired,
+}: Props): VNode {
+ const { i18n } = useTranslationContext();
+ const result = useWithdrawalDetails(withdrawUri.withdrawalOperationId);
+
+ if (!result) {
+ return <Loading />;
+ }
+ if (result instanceof TalerError) {
+ return <ErrorLoadingWithDebug error={result} />;
+ }
+ if (result.type === "fail") {
+ switch (result.case) {
+ case HttpStatusCode.BadRequest:
+ case HttpStatusCode.NotFound:
+ return <OperationNotFound routeClose={routeClose} />;
+ default:
+ assertUnreachable(result);
+ }
+ }
+
+ const { body: data } = result;
+
+ if (data.status === "aborted") {
+ return (
+ <div class="relative ml-auto mr-auto transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-sm sm:p-6">
+ <div>
+ <div class="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-yellow-100">
+ <svg
+ class="h-5 w-5 text-yellow-400"
+ viewBox="0 0 20 20"
+ fill="currentColor"
+ aria-hidden="true"
+ >
+ <path
+ fill-rule="evenodd"
+ d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z"
+ clip-rule="evenodd"
+ />
+ </svg>
+ </div>
+ <div class="mt-3 text-center sm:mt-5">
+ <h3
+ class="text-base font-semibold leading-6 text-gray-900"
+ id="modal-title"
+ >
+ <i18n.Translate>Operation aborted</i18n.Translate>
+ </h3>
+ <div class="mt-2">
+ <p class="text-sm text-gray-500">
+ <i18n.Translate>
+ The wire transfer to the payment provider's account was
+ aborted from somewhere else, your balance was not affected.
+ </i18n.Translate>
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="mt-5 sm:mt-6">
+ <a
+ href={routeClose.url({})}
+ name="continue"
+ 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"
+ >
+ <i18n.Translate>Continue</i18n.Translate>
+ </a>
+ </div>
+ </div>
+ );
+ }
+
+ if (data.status === "confirmed") {
+ return (
+ <div class="relative ml-auto mr-auto transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-sm sm:p-6">
+ <div>
+ <div class="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-green-100">
+ <svg
+ class="h-6 w-6 text-green-600"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="1.5"
+ stroke="currentColor"
+ aria-hidden="true"
+ >
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="M4.5 12.75l6 6 9-13.5"
+ />
+ </svg>
+ </div>
+ <div class="mt-3 text-center sm:mt-5">
+ <h3
+ class="text-base font-semibold leading-6 text-gray-900"
+ id="modal-title"
+ >
+ <i18n.Translate>Withdrawal confirmed</i18n.Translate>
+ </h3>
+ <div class="mt-2">
+ <p class="text-sm text-gray-500">
+ <i18n.Translate>
+ The wire transfer to the Taler operator has been initiated.
+ You will soon receive the requested amount in your Taler
+ wallet.
+ </i18n.Translate>
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="mt-5 sm:mt-6">
+ <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"
+ >
+ <i18n.Translate>Done</i18n.Translate>
+ </a>
+ </div>
+ </div>
+ );
+ }
+
+ if (data.status === "pending") {
+ return (
+ <QrCodeSection
+ withdrawUri={withdrawUri}
+ onAborted={() => {
+ notifyInfo(i18n.str`Operation canceled`);
+ onOperationAborted();
+ }}
+ />
+ );
+ }
+
+ const account = !data.selected_exchange_account
+ ? undefined
+ : parsePaytoUri(data.selected_exchange_account);
+
+ if (!data.selected_reserve_pub && account) {
+ return (
+ <Attention
+ type="danger"
+ title={i18n.str`The operation is marked as 'selected' but some step in the withdrawal failed`}
+ >
+ <i18n.Translate>
+ The account is selected but no withdrawal identification found.
+ </i18n.Translate>
+ </Attention>
+ );
+ }
+
+ if (!account && data.selected_reserve_pub) {
+ return (
+ <Attention
+ type="danger"
+ title={i18n.str`The operation is marked as 'selected' but some step in the withdrawal failed`}
+ >
+ <i18n.Translate>
+ There is a withdrawal identification but no account has been selected
+ or the selected account is invalid.
+ </i18n.Translate>
+ </Attention>
+ );
+ }
+
+ if (!account || !data.selected_reserve_pub) {
+ return (
+ <Attention
+ type="danger"
+ title={i18n.str`The operation is marked as 'selected' but some step in the withdrawal failed`}
+ >
+ <i18n.Translate>
+ No withdrawal ID found and no account has been selected or the
+ selected account is invalid.
+ </i18n.Translate>
+ </Attention>
+ );
+ }
+
+ return (
+ <WithdrawalConfirmationQuestion
+ withdrawUri={withdrawUri}
+ routeHere={routeWithdrawalDetails}
+ details={{
+ username: data.username,
+ account,
+ reserve: data.selected_reserve_pub,
+ amount: Amounts.parseOrThrow(data.amount),
+ }}
+ onAuthorizationRequired={onAuthorizationRequired}
+ onAborted={() => {
+ notifyInfo(i18n.str`Operation canceled`);
+ onOperationAborted();
+ }}
+ />
+ );
+}
+
+export function OperationNotFound({
+ routeClose,
+}: {
+ routeClose: RouteDefinition | undefined;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="relative ml-auto mr-auto transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-sm sm:p-6">
+ <div>
+ <div class="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-red-100 ">
+ <svg
+ class="h-6 w-6 text-red-600"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="1.5"
+ stroke="currentColor"
+ aria-hidden="true"
+ >
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"
+ />
+ </svg>
+ </div>
+
+ <div class="mt-3 text-center sm:mt-5">
+ <h3
+ class="text-base font-semibold leading-6 text-gray-900"
+ id="modal-title"
+ >
+ <i18n.Translate>Operation not found</i18n.Translate>
+ </h3>
+ <div class="mt-2">
+ <p class="text-sm text-gray-500">
+ <i18n.Translate>
+ This operation is not known by the server. The operation id is
+ wrong or the server deleted the operation information before
+ reaching here.
+ </i18n.Translate>
+ </p>
+ </div>
+ </div>
+ </div>
+ {routeClose && (
+ <div class="mt-5 sm:mt-6">
+ <a
+ href={routeClose.url({})}
+ name="continue to dashboard"
+ 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"
+ >
+ <i18n.Translate>Cotinue to dashboard</i18n.Translate>
+ </a>
+ </div>
+ )}
+ </div>
+ );
+}
diff --git a/packages/bank-ui/src/pages/account/CashoutListForAccount.tsx b/packages/bank-ui/src/pages/account/CashoutListForAccount.tsx
new file mode 100644
index 000000000..301978eaa
--- /dev/null
+++ b/packages/bank-ui/src/pages/account/CashoutListForAccount.tsx
@@ -0,0 +1,86 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { Cashouts } from "../../components/Cashouts/index.js";
+import { useSessionState } from "../../hooks/session.js";
+import { ProfileNavigation } from "../ProfileNavigation.js";
+import { CreateCashout } from "../regional/CreateCashout.js";
+import { RouteDefinition } from "@gnu-taler/web-util/browser";
+
+interface Props {
+ account: string;
+ routeClose: RouteDefinition;
+ onAuthorizationRequired: () => void;
+ routeCashoutDetails: RouteDefinition<{ cid: string }>;
+ routeMyAccountDetails: RouteDefinition;
+ routeMyAccountDelete: RouteDefinition;
+ routeMyAccountPassword: RouteDefinition;
+ routeMyAccountCashout: RouteDefinition;
+ routeCreateCashout: RouteDefinition;
+ routeConversionConfig: RouteDefinition;
+}
+
+export function CashoutListForAccount({
+ account,
+ onAuthorizationRequired,
+ routeCreateCashout,
+ routeCashoutDetails,
+ routeMyAccountCashout,
+ routeMyAccountDelete,
+ routeMyAccountDetails,
+ routeConversionConfig,
+ routeMyAccountPassword,
+ routeClose,
+}: Props): VNode {
+ const { i18n } = useTranslationContext();
+
+ const { state: credentials } = useSessionState();
+
+ const accountIsTheCurrentUser =
+ credentials.status === "loggedIn"
+ ? credentials.username === account
+ : false;
+
+ return (
+ <Fragment>
+ {accountIsTheCurrentUser ? (
+ <ProfileNavigation
+ current="cashouts"
+ routeMyAccountCashout={routeMyAccountCashout}
+ routeMyAccountDelete={routeMyAccountDelete}
+ routeMyAccountDetails={routeMyAccountDetails}
+ routeMyAccountPassword={routeMyAccountPassword}
+ routeConversionConfig={routeConversionConfig}
+ />
+ ) : (
+ <h1 class="text-base font-semibold leading-6 text-gray-900">
+ <i18n.Translate>Cashout for account {account}</i18n.Translate>
+ </h1>
+ )}
+
+ <CreateCashout
+ focus
+ routeHere={routeCreateCashout}
+ routeClose={routeClose}
+ onAuthorizationRequired={onAuthorizationRequired}
+ account={account}
+ />
+
+ <Cashouts account={account} routeCashoutDetails={routeCashoutDetails} />
+ </Fragment>
+ );
+}
diff --git a/packages/bank-ui/src/pages/account/ShowAccountDetails.tsx b/packages/bank-ui/src/pages/account/ShowAccountDetails.tsx
new file mode 100644
index 000000000..69a186ca1
--- /dev/null
+++ b/packages/bank-ui/src/pages/account/ShowAccountDetails.tsx
@@ -0,0 +1,491 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ AbsoluteTime,
+ HttpStatusCode,
+ TalerCorebankApi,
+ TalerError,
+ TalerErrorCode,
+ TranslatedString,
+ assertUnreachable,
+ parsePaytoUri
+} from "@gnu-taler/taler-util";
+import {
+ CopyButton,
+ Loading,
+ LocalNotificationBanner,
+ RouteDefinition,
+ notifyInfo,
+ useBankCoreApiContext,
+ useLocalNotification,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js";
+import { useAccountDetails } from "../../hooks/account.js";
+import { useBankState } from "../../hooks/bank-state.js";
+import { useSessionState } from "../../hooks/session.js";
+import { LoginForm } from "../LoginForm.js";
+import { ProfileNavigation } from "../ProfileNavigation.js";
+import { AccountForm } from "../admin/AccountForm.js";
+
+export function ShowAccountDetails({
+ account,
+ routeClose,
+ onUpdateSuccess,
+ onAuthorizationRequired,
+ routeMyAccountCashout,
+ routeMyAccountDelete,
+ routeMyAccountDetails,
+ routeHere,
+ routeMyAccountPassword,
+ routeConversionConfig,
+}: {
+ routeClose: RouteDefinition;
+ routeHere: RouteDefinition<{ account: string }>;
+ routeMyAccountDetails: RouteDefinition;
+ routeMyAccountDelete: RouteDefinition;
+ routeMyAccountPassword: RouteDefinition;
+ routeMyAccountCashout: RouteDefinition;
+ routeConversionConfig: RouteDefinition;
+ onUpdateSuccess: () => void;
+ onAuthorizationRequired: () => void;
+ account: string;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const { state: credentials } = useSessionState();
+ const creds = credentials.status !== "loggedIn" ? undefined : credentials;
+ const {
+ lib: { bank },
+ } = useBankCoreApiContext();
+ const accountIsTheCurrentUser =
+ credentials.status === "loggedIn"
+ ? credentials.username === account
+ : false;
+
+ const [submitAccount, setSubmitAccount] = useState<
+ TalerCorebankApi.AccountReconfiguration | undefined
+ >();
+ const [notification, notify, handleError] = useLocalNotification();
+ const [, updateBankState] = useBankState();
+
+ const result = useAccountDetails(account);
+ if (!result) {
+ return <Loading />;
+ }
+ if (result instanceof TalerError) {
+ return <ErrorLoadingWithDebug error={result} />;
+ }
+ if (result.type === "fail") {
+ switch (result.case) {
+ case HttpStatusCode.Unauthorized:
+ case HttpStatusCode.NotFound:
+ return <LoginForm currentUser={account} />;
+ default:
+ assertUnreachable(result);
+ }
+ }
+
+ async function doUpdate() {
+ if (!submitAccount || !creds) return;
+ await handleError(async () => {
+ const resp = await bank.updateAccount(
+ {
+ token: creds.token,
+ username: account,
+ },
+ submitAccount,
+ );
+
+ if (resp.type === "ok") {
+ notifyInfo(i18n.str`Account updated`);
+ onUpdateSuccess();
+ } else {
+ switch (resp.case) {
+ case HttpStatusCode.Unauthorized:
+ return notify({
+ type: "error",
+ title: i18n.str`The rights to change the account are not sufficient`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case HttpStatusCode.NotFound:
+ return notify({
+ type: "error",
+ title: i18n.str`The username was not found`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case TalerErrorCode.BANK_NON_ADMIN_PATCH_LEGAL_NAME:
+ return notify({
+ type: "error",
+ title: i18n.str`You can't change the legal name, please contact the your account administrator.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT:
+ return notify({
+ type: "error",
+ title: i18n.str`You can't change the debt limit, please contact the your account administrator.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case TalerErrorCode.BANK_NON_ADMIN_PATCH_CASHOUT:
+ return notify({
+ type: "error",
+ title: i18n.str`You can't change the cashout address, please contact the your account administrator.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case TalerErrorCode.BANK_MISSING_TAN_INFO:
+ return notify({
+ type: "error",
+ title: i18n.str`No information for the selected authentication channel.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case HttpStatusCode.Accepted: {
+ updateBankState("currentChallenge", {
+ operation: "update-account",
+ id: String(resp.body.challenge_id),
+ location: routeHere.url({ account }),
+ sent: AbsoluteTime.never(),
+ request: submitAccount,
+ });
+ return onAuthorizationRequired();
+ }
+ case TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED: {
+ return notify({
+ type: "error",
+ title: i18n.str`Authentication channel is not supported.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ }
+ default:
+ assertUnreachable(resp);
+ }
+ }
+ });
+ }
+
+ const url = bank.getRevenueAPI(account);
+ url.username = account;
+ const baseURL = url.href;
+
+ const ac = parsePaytoUri(result.body.payto_uri);
+ const payto = !ac?.isKnown ? undefined : ac;
+ let accountLetter: string | undefined = undefined;
+ if (payto) {
+ switch (payto.targetType) {
+ case "iban": {
+ accountLetter = `account-info-url=${url.href}\naccount-type=${payto.targetType}\niban=${payto.iban}\nreceiver-name=${result.body.name}\n`;
+ break;
+ }
+ case "x-taler-bank": {
+ accountLetter = `account-info-url=${url.href}\naccount-type=${payto.targetType}\naccount=${payto.account}\nhost=${payto.host}\nreceiver-name=${result.body.name}\n`;
+ break;
+ }
+ case "bitcoin": {
+ accountLetter = `account-info-url=${url.href}\naccount-type=${payto.targetType}\naddress=${payto.address}\nreceiver-name=${result.body.name}\n`;
+ break;
+ }
+ }
+ }
+
+ return (
+ <Fragment>
+ <LocalNotificationBanner notification={notification} showDebug={true} />
+ {accountIsTheCurrentUser ? (
+ <ProfileNavigation
+ current="details"
+ routeMyAccountCashout={routeMyAccountCashout}
+ routeMyAccountDelete={routeMyAccountDelete}
+ routeConversionConfig={routeConversionConfig}
+ routeMyAccountDetails={routeMyAccountDetails}
+ routeMyAccountPassword={routeMyAccountPassword}
+ />
+ ) : (
+ <h1 class="text-base font-semibold leading-6 text-gray-900">
+ <i18n.Translate>Account "{account}"</i18n.Translate>
+ </h1>
+ )}
+
+ <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-6 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg">
+ <div class="px-4 sm:px-0">
+ <h2 class="text-base font-semibold leading-7 text-gray-900">
+ <div class="flex items-center justify-between">
+ <span class="flex flex-grow flex-col">
+ <span
+ class="text-sm text-black font-semibold leading-6 "
+ id="availability-label"
+ >
+ <i18n.Translate>Change details</i18n.Translate>
+ </span>
+ </span>
+ </div>
+ </h2>
+ </div>
+
+ <AccountForm
+ focus={true}
+ username={account}
+ template={result.body}
+ purpose="update"
+ onChange={(a) => setSubmitAccount(a)}
+ >
+ <div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
+ <a
+ href={routeClose.url({})}
+ name="cancel"
+ class="text-sm font-semibold leading-6 text-gray-900"
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </a>
+ <button
+ type="submit"
+ name="update"
+ class="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"
+ disabled={!submitAccount}
+ onClick={doUpdate}
+ >
+ <i18n.Translate>Update</i18n.Translate>
+ </button>
+ </div>
+ </AccountForm>
+ </div>
+ <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-6 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg">
+ <div class="px-4 sm:px-0">
+ <h2 class="text-base font-semibold leading-7 text-gray-900">
+ <div class="flex items-center justify-between">
+ <span class="flex flex-grow flex-col">
+ <span
+ class="text-sm text-black font-semibold leading-6 "
+ id="availability-label"
+ >
+ <i18n.Translate>Merchant integration</i18n.Translate>
+ </span>
+ </span>
+ </div>
+ </h2>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ Use this information to link your Taler Merchant Backoffice
+ account with the current bank account. You can start by copying
+ the values, then go to your merchant backoffice service provider,
+ login into your account and look for the "import" button in the
+ "bank account" section.
+ </i18n.Translate>
+ </p>
+ </div>
+
+ {payto !== undefined && (
+ <div class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2">
+ <div class="px-4 py-6 sm:p-8">
+ <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="account-type"
+ >
+ {i18n.str`Account type`}
+ </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-type"
+ id="account-type"
+ disabled={true}
+ value={account}
+ autocomplete="off"
+ />
+ </div>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ Method to use for wire transfer.
+ </i18n.Translate>
+ </p>
+ </div>
+ {((payto) => {
+ switch (payto.targetType) {
+ case "iban": {
+ return (
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="iban"
+ >
+ {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>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ International Bank Account Number.
+ </i18n.Translate>
+ </p>
+ </div>
+ );
+ }
+ case "x-taler-bank": {
+ return (
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="iban"
+ >
+ {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.account}
+ autocomplete="off"
+ />
+ </div>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ International Bank Account Number.
+ </i18n.Translate>
+ </p>
+ </div>
+ );
+ }
+ case "bitcoin": {
+ return (
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="iban"
+ >
+ {i18n.str`Address`}
+ </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={"DE1231231231"}
+ autocomplete="off"
+ />
+ </div>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ International Bank Account Number.
+ </i18n.Translate>
+ </p>
+ </div>
+ );
+ }
+ }
+ })(payto)}
+
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="iban"
+ >
+ {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>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ Legal name of the person holding the account.
+ </i18n.Translate>
+ </p>
+ </div>
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="iban"
+ >
+ {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>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ From where the merchant can download information about
+ incoming wire transfers to this account.
+ </i18n.Translate>
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
+ <a
+ href={routeClose.url({})}
+ name="cancel"
+ class="text-sm font-semibold leading-6 text-gray-900"
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </a>
+ <CopyButton
+ getContent={() => 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>
+ )}
+ </div>
+ </Fragment>
+ );
+}
diff --git a/packages/bank-ui/src/pages/account/UpdateAccountPassword.tsx b/packages/bank-ui/src/pages/account/UpdateAccountPassword.tsx
new file mode 100644
index 000000000..2724fba11
--- /dev/null
+++ b/packages/bank-ui/src/pages/account/UpdateAccountPassword.tsx
@@ -0,0 +1,319 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ AbsoluteTime,
+ HttpStatusCode,
+ TalerErrorCode,
+ TranslatedString,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
+import {
+ LocalNotificationBanner,
+ ShowInputErrorLabel,
+ notifyInfo,
+ useLocalNotification,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { useBankCoreApiContext } from "@gnu-taler/web-util/browser";
+import { useSessionState } from "../../hooks/session.js";
+import { useBankState } from "../../hooks/bank-state.js";
+import { RouteDefinition } from "@gnu-taler/web-util/browser";
+import { undefinedIfEmpty } from "../../utils.js";
+import { doAutoFocus } from "../PaytoWireTransferForm.js";
+import { ProfileNavigation } from "../ProfileNavigation.js";
+
+export function UpdateAccountPassword({
+ account: accountName,
+ routeClose,
+ onUpdateSuccess,
+ onAuthorizationRequired,
+ routeMyAccountCashout,
+ routeMyAccountDelete,
+ routeMyAccountDetails,
+ routeMyAccountPassword,
+ routeConversionConfig,
+ focus,
+ routeHere,
+}: {
+ routeClose: RouteDefinition;
+ routeHere: RouteDefinition<{ account: string }>;
+ routeMyAccountDetails: RouteDefinition;
+ routeMyAccountDelete: RouteDefinition;
+ routeMyAccountPassword: RouteDefinition;
+ routeMyAccountCashout: RouteDefinition;
+ routeConversionConfig: RouteDefinition;
+ focus?: boolean;
+ onAuthorizationRequired: () => void;
+ onUpdateSuccess: () => void;
+ account: string;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const { state: credentials } = useSessionState();
+ const token =
+ credentials.status !== "loggedIn" ? undefined : credentials.token;
+ const {
+ lib: { bank: api },
+ } = useBankCoreApiContext();
+
+ const [current, setCurrent] = useState<string | undefined>();
+ const [password, setPassword] = useState<string | undefined>();
+ const [repeat, setRepeat] = useState<string | undefined>();
+ const [, updateBankState] = useBankState();
+
+ const accountIsTheCurrentUser =
+ credentials.status === "loggedIn"
+ ? credentials.username === accountName
+ : false;
+
+ const errors = undefinedIfEmpty({
+ current: !accountIsTheCurrentUser
+ ? undefined
+ : !current
+ ? i18n.str`Required`
+ : undefined,
+ password: !password ? i18n.str`Required` : undefined,
+ repeat: !repeat
+ ? i18n.str`Required`
+ : password !== repeat
+ ? i18n.str`Repeated password doesn't match`
+ : undefined,
+ });
+ const [notification, notify, handleError] = useLocalNotification();
+
+ async function doChangePassword() {
+ if (!!errors || !password || !token) return;
+ await handleError(async () => {
+ const request = {
+ old_password: current,
+ new_password: password,
+ };
+ const resp = await api.updatePassword(
+ { username: accountName, token },
+ request,
+ );
+ if (resp.type === "ok") {
+ notifyInfo(i18n.str`Password changed`);
+ onUpdateSuccess();
+ } else {
+ switch (resp.case) {
+ case HttpStatusCode.Unauthorized:
+ return notify({
+ type: "error",
+ title: i18n.str`Not authorized to change the password, maybe the session is invalid.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case HttpStatusCode.NotFound:
+ return notify({
+ type: "error",
+ title: i18n.str`Account not found`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case TalerErrorCode.BANK_NON_ADMIN_PATCH_MISSING_OLD_PASSWORD:
+ return notify({
+ type: "error",
+ title: i18n.str`You need to provide the old password. If you don't have it contact your account administrator.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case TalerErrorCode.BANK_PATCH_BAD_OLD_PASSWORD:
+ return notify({
+ type: "error",
+ title: i18n.str`Your current password doesn't match, can't change to a new password.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case HttpStatusCode.Accepted: {
+ updateBankState("currentChallenge", {
+ operation: "update-password",
+ id: String(resp.body.challenge_id),
+ location: routeHere.url({ account: accountName }),
+ sent: AbsoluteTime.never(),
+ request,
+ });
+ return onAuthorizationRequired();
+ }
+ default:
+ assertUnreachable(resp);
+ }
+ }
+ });
+ }
+
+ return (
+ <Fragment>
+ <LocalNotificationBanner notification={notification} />
+ {accountIsTheCurrentUser ? (
+ <ProfileNavigation
+ current="credentials"
+ routeMyAccountCashout={routeMyAccountCashout}
+ routeMyAccountDelete={routeMyAccountDelete}
+ routeMyAccountDetails={routeMyAccountDetails}
+ routeMyAccountPassword={routeMyAccountPassword}
+ routeConversionConfig={routeConversionConfig}
+ />
+ ) : (
+ <h1 class="text-base font-semibold leading-6 text-gray-900">
+ <i18n.Translate>Account "{accountName}"</i18n.Translate>
+ </h1>
+ )}
+
+ <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-6 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg">
+ <div class="px-4 sm:px-0">
+ <h2 class="text-base font-semibold leading-7 text-gray-900">
+ <i18n.Translate>Update password</i18n.Translate>
+ </h2>
+ </div>
+ <form
+ class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2"
+ autoCapitalize="none"
+ autoCorrect="off"
+ onSubmit={(e) => {
+ e.preventDefault();
+ }}
+ >
+ <div class="px-4 py-6 sm:p-8">
+ <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
+ {accountIsTheCurrentUser ? (
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="password"
+ >
+ {i18n.str`Current password`}
+ <b style={{ color: "red" }}> *</b>
+ </label>
+ <div class="mt-2">
+ <input
+ type="password"
+ class="block w-full 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="current"
+ id="current-password"
+ data-error={!!errors?.current && current !== undefined}
+ value={current ?? ""}
+ onChange={(e) => {
+ setCurrent(e.currentTarget.value);
+ }}
+ autocomplete="off"
+ />
+ <ShowInputErrorLabel
+ message={errors?.current}
+ isDirty={current !== undefined}
+ />
+ </div>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ Your current password, for security
+ </i18n.Translate>
+ </p>
+ </div>
+ ) : undefined}
+
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="password"
+ >
+ {i18n.str`New password`}
+ <b style={{ color: "red" }}> *</b>
+ </label>
+ <div class="mt-2">
+ <input
+ ref={focus ? doAutoFocus : undefined}
+ type="password"
+ class="block w-full 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="password"
+ id="password"
+ data-error={!!errors?.password && password !== undefined}
+ value={password ?? ""}
+ onChange={(e) => {
+ setPassword(e.currentTarget.value);
+ }}
+ autocomplete="off"
+ />
+ <ShowInputErrorLabel
+ message={errors?.password}
+ isDirty={password !== undefined}
+ />
+ </div>
+ </div>
+
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="repeat"
+ >
+ {i18n.str`Type it again`}
+ <b style={{ color: "red" }}> *</b>
+ </label>
+ <div class="mt-2">
+ <input
+ type="password"
+ class="block w-full 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="repeat"
+ id="repeat"
+ data-error={!!errors?.repeat && repeat !== undefined}
+ value={repeat ?? ""}
+ onChange={(e) => {
+ setRepeat(e.currentTarget.value);
+ }}
+ // placeholder=""
+ autocomplete="off"
+ />
+ <ShowInputErrorLabel
+ message={errors?.repeat}
+ isDirty={repeat !== undefined}
+ />
+ </div>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>Repeat the same password</i18n.Translate>
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
+ <a
+ href={routeClose.url({})}
+ name="cancel"
+ class="text-sm font-semibold leading-6 text-gray-900"
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </a>
+ <button
+ type="submit"
+ name="change"
+ class="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"
+ disabled={!!errors}
+ onClick={(e) => {
+ e.preventDefault();
+ doChangePassword();
+ }}
+ >
+ <i18n.Translate>Change</i18n.Translate>
+ </button>
+ </div>
+ </form>
+ </div>
+ </Fragment>
+ );
+}
diff --git a/packages/bank-ui/src/pages/admin/AccountForm.tsx b/packages/bank-ui/src/pages/admin/AccountForm.tsx
new file mode 100644
index 000000000..c8195ddb0
--- /dev/null
+++ b/packages/bank-ui/src/pages/admin/AccountForm.tsx
@@ -0,0 +1,804 @@
+/*
+ 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 {
+ AmountString,
+ Amounts,
+ PaytoString,
+ TalerCorebankApi,
+ assertUnreachable,
+ buildPayto,
+ parsePaytoUri,
+ stringifyPaytoUri,
+} from "@gnu-taler/taler-util";
+import {
+ CopyButton,
+ ShowInputErrorLabel,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { ComponentChildren, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { useBankCoreApiContext } from "@gnu-taler/web-util/browser";
+import { useSessionState } from "../../hooks/session.js";
+import {
+ ErrorMessageMappingFor,
+ TanChannel,
+ undefinedIfEmpty,
+ validateIBAN,
+ validateTalerBank,
+} from "../../utils.js";
+import {
+ InputAmount,
+ TextField,
+ doAutoFocus,
+} from "../PaytoWireTransferForm.js";
+import { getRandomPassword } from "../rnd.js";
+
+const EMAIL_REGEX =
+ /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
+const REGEX_JUST_NUMBERS_REGEX = /^\+[0-9 ]*$/;
+
+export type AccountFormData = {
+ debit_threshold?: string;
+ isExchange?: boolean;
+ isPublic?: boolean;
+ name?: string;
+ username?: string;
+ payto_uri?: string;
+ cashout_payto_uri?: string;
+ email?: string;
+ phone?: string;
+ tan_channel?: TanChannel | "remove";
+};
+
+type ChangeByPurposeType = {
+ create: (a: TalerCorebankApi.RegisterAccountRequest | undefined) => void;
+ update: (a: TalerCorebankApi.AccountReconfiguration | undefined) => void;
+ show: undefined;
+};
+/**
+ * FIXME:
+ * is_public is missing on PATCH
+ * account email/password should require 2FA
+ *
+ *
+ * @param param0
+ * @returns
+ */
+export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
+ template,
+ username,
+ purpose,
+ onChange,
+ focus,
+ children,
+}: {
+ focus?: boolean;
+ children: ComponentChildren;
+ username?: string;
+ template: TalerCorebankApi.AccountData | undefined;
+ onChange: ChangeByPurposeType[PurposeType];
+ purpose: PurposeType;
+}): VNode {
+ const { config, url } = useBankCoreApiContext();
+ const { i18n } = useTranslationContext();
+ const { state: credentials } = useSessionState();
+ const [form, setForm] = useState<AccountFormData>({});
+
+ const [errors, setErrors] = useState<
+ ErrorMessageMappingFor<typeof defaultValue> | undefined
+ >(undefined);
+
+ const paytoType =
+ config.wire_type === "X_TALER_BANK"
+ ? ("x-taler-bank" as const)
+ : ("iban" as const);
+ const cashoutPaytoType: typeof paytoType = "iban" as const;
+
+ const defaultValue: AccountFormData = {
+ debit_threshold: Amounts.stringifyValue(
+ template?.debit_threshold ?? config.default_debit_threshold,
+ ),
+ isExchange: template?.is_taler_exchange,
+ isPublic: template?.is_public,
+ name: template?.name ?? "",
+ cashout_payto_uri:
+ getAccountId(cashoutPaytoType, template?.cashout_payto_uri) ??
+ ("" as PaytoString),
+ payto_uri:
+ getAccountId(paytoType, template?.payto_uri) ?? ("" as PaytoString),
+ email: template?.contact_data?.email ?? "",
+ phone: template?.contact_data?.phone ?? "",
+ username: username ?? "",
+ tan_channel: template?.tan_channel,
+ };
+
+ const userIsAdmin =
+ credentials.status !== "loggedIn" ? false : credentials.isUserAdministrator;
+
+ const editableUsername = purpose === "create";
+ const editableName =
+ purpose === "create" ||
+ (purpose === "update" && (config.allow_edit_name || userIsAdmin));
+
+ const isCashoutEnabled = config.allow_conversion;
+ const editableCashout =
+ purpose === "create" ||
+ (purpose === "update" &&
+ (config.allow_edit_cashout_payto_uri || userIsAdmin));
+ const editableThreshold =
+ userIsAdmin && (purpose === "create" || purpose === "update");
+ const editableAccount = purpose === "create" && userIsAdmin;
+
+ function updateForm(newForm: typeof defaultValue): void {
+ const trimmedAmountStr = newForm.debit_threshold?.trim();
+ const parsedAmount = Amounts.parse(
+ `${config.currency}:${trimmedAmountStr}`,
+ );
+
+ const errors = undefinedIfEmpty<
+ ErrorMessageMappingFor<typeof defaultValue>
+ >({
+ cashout_payto_uri: !newForm.cashout_payto_uri
+ ? undefined
+ : !editableCashout
+ ? undefined
+ : !newForm.cashout_payto_uri
+ ? undefined
+ : cashoutPaytoType === "iban"
+ ? validateIBAN(newForm.cashout_payto_uri, i18n)
+ : cashoutPaytoType === "x-taler-bank"
+ ? validateTalerBank(newForm.cashout_payto_uri, i18n)
+ : undefined,
+
+ payto_uri: !newForm.payto_uri
+ ? undefined
+ : !editableAccount
+ ? undefined
+ : !newForm.payto_uri
+ ? undefined
+ : paytoType === "iban"
+ ? validateIBAN(newForm.payto_uri, i18n)
+ : paytoType === "x-taler-bank"
+ ? validateTalerBank(newForm.payto_uri, i18n)
+ : undefined,
+
+ email: !newForm.email
+ ? undefined
+ : !EMAIL_REGEX.test(newForm.email)
+ ? i18n.str`Doesn't have the pattern of an email`
+ : undefined,
+ phone: !newForm.phone
+ ? undefined
+ : !newForm.phone.startsWith("+") // FIXME: better phone number check
+ ? i18n.str`Should start with +`
+ : !REGEX_JUST_NUMBERS_REGEX.test(newForm.phone)
+ ? i18n.str`Phone number can't have other than numbers`
+ : undefined,
+ debit_threshold: !editableThreshold
+ ? undefined
+ : !trimmedAmountStr
+ ? undefined
+ : !parsedAmount
+ ? i18n.str`Not valid`
+ : undefined,
+ name: !editableName
+ ? undefined // disabled
+ : !newForm.name
+ ? i18n.str`Required`
+ : undefined,
+ username: !editableUsername
+ ? undefined
+ : !newForm.username
+ ? i18n.str`Required`
+ : undefined,
+ });
+ setErrors(errors);
+
+ setForm(newForm);
+ if (!onChange) return;
+
+ if (errors) {
+ onChange(undefined);
+ } else {
+ let cashout;
+ if (newForm.cashout_payto_uri)
+ switch (cashoutPaytoType) {
+ case "x-taler-bank": {
+ cashout = buildPayto(
+ "x-taler-bank",
+ url.host,
+ newForm.cashout_payto_uri,
+ );
+ break;
+ }
+ case "iban": {
+ cashout = buildPayto("iban", newForm.cashout_payto_uri, undefined);
+ break;
+ }
+ default:
+ assertUnreachable(cashoutPaytoType);
+ }
+ const cashoutURI = !cashout ? undefined : stringifyPaytoUri(cashout);
+ let internal;
+ if (newForm.payto_uri)
+ switch (paytoType) {
+ case "x-taler-bank": {
+ internal = buildPayto("x-taler-bank", url.host, newForm.payto_uri);
+ break;
+ }
+ case "iban": {
+ internal = buildPayto("iban", newForm.payto_uri, undefined);
+ break;
+ }
+ default:
+ assertUnreachable(paytoType);
+ }
+ const internalURI = !internal ? undefined : stringifyPaytoUri(internal);
+
+ const threshold = !parsedAmount
+ ? undefined
+ : Amounts.stringify(parsedAmount);
+
+ switch (purpose) {
+ case "create": {
+ // typescript doesn't correctly narrow a generic type
+ const callback = onChange as ChangeByPurposeType["create"];
+ const result: TalerCorebankApi.RegisterAccountRequest = {
+ name: newForm.name!,
+ password: getRandomPassword(),
+ username: newForm.username!,
+ contact_data: undefinedIfEmpty({
+ email: !newForm.email ? undefined : newForm.email,
+ phone: !newForm.phone ? undefined : newForm.phone,
+ }),
+ debit_threshold: threshold ?? config.default_debit_threshold,
+ cashout_payto_uri: cashoutURI,
+ payto_uri: internalURI,
+ is_public: newForm.isPublic,
+ is_taler_exchange: newForm.isExchange,
+ tan_channel:
+ newForm.tan_channel === "remove"
+ ? undefined
+ : newForm.tan_channel,
+ };
+ callback(result);
+ return;
+ }
+ case "update": {
+ // typescript doesn't correctly narrow a generic type
+ const callback = onChange as ChangeByPurposeType["update"];
+
+ const result: TalerCorebankApi.AccountReconfiguration = {
+ cashout_payto_uri: cashoutURI,
+ contact_data: undefinedIfEmpty({
+ email: !newForm.email ? undefined : newForm.email,
+ phone: !newForm.phone ? undefined : newForm.phone,
+ }),
+ debit_threshold: threshold,
+ is_public: newForm.isPublic,
+ name: newForm.name,
+ tan_channel:
+ newForm.tan_channel === "remove" ? null : newForm.tan_channel,
+ };
+ callback(result);
+ return;
+ }
+ case "show": {
+ return;
+ }
+ default: {
+ assertUnreachable(purpose);
+ }
+ }
+ }
+ }
+ return (
+ <form
+ class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2"
+ autoCapitalize="none"
+ autoCorrect="off"
+ onSubmit={(e) => {
+ e.preventDefault();
+ }}
+ >
+ <div class="px-4 py-6 sm:p-8">
+ <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="username"
+ >
+ {i18n.str`Login username`}
+ {editableUsername && <b style={{ color: "red" }}> *</b>}
+ </label>
+ <div class="mt-2">
+ <input
+ ref={focus && purpose === "create" ? doAutoFocus : undefined}
+ 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="username"
+ id="username"
+ data-error={!!errors?.username && form.username !== undefined}
+ disabled={!editableUsername}
+ value={form.username ?? defaultValue.username}
+ onChange={(e) => {
+ form.username = e.currentTarget.value;
+ updateForm(structuredClone(form));
+ }}
+ // placeholder=""
+ autocomplete="off"
+ />
+ <ShowInputErrorLabel
+ message={errors?.username}
+ isDirty={form.username !== undefined}
+ />
+ </div>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>Account id for authentication</i18n.Translate>
+ </p>
+ </div>
+
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="name"
+ >
+ {i18n.str`Full name`}
+ {editableName && <b style={{ color: "red" }}> *</b>}
+ </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="name"
+ data-error={!!errors?.name && form.name !== undefined}
+ id="name"
+ disabled={!editableName}
+ value={form.name ?? defaultValue.name}
+ onChange={(e) => {
+ form.name = e.currentTarget.value;
+ updateForm(structuredClone(form));
+ }}
+ // placeholder=""
+ autocomplete="off"
+ />
+ <ShowInputErrorLabel
+ message={errors?.name}
+ isDirty={form.name !== undefined}
+ />
+ </div>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>Name of the account holder</i18n.Translate>
+ </p>
+ </div>
+
+ {purpose === "create" ? undefined : (
+ <TextField
+ id="internal-account"
+ label={i18n.str`Internal account`}
+ help={
+ purpose === "create"
+ ? i18n.str`If empty a random account id will be assigned`
+ : i18n.str`Share this id to receive bank transfers`
+ }
+ error={errors?.payto_uri}
+ onChange={(e) => {
+ form.payto_uri = e as PaytoString;
+ updateForm(structuredClone(form));
+ }}
+ rightIcons={
+ <CopyButton
+ class="p-2 rounded-full text-black shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
+ getContent={() =>
+ form.payto_uri ?? defaultValue.payto_uri ?? ""
+ }
+ />
+ }
+ value={(form.payto_uri ?? defaultValue.payto_uri) as PaytoString}
+ disabled={!editableAccount}
+ />
+ )}
+
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="email"
+ >
+ {i18n.str`Email`}
+ </label>
+ <div class="mt-2">
+ <input
+ type="email"
+ 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="email"
+ id="email"
+ data-error={!!errors?.email && form.email !== undefined}
+ disabled={purpose === "show"}
+ value={form.email ?? defaultValue.email}
+ onChange={(e) => {
+ form.email = e.currentTarget.value;
+ updateForm(structuredClone(form));
+ }}
+ autocomplete="off"
+ />
+ <ShowInputErrorLabel
+ message={errors?.email}
+ isDirty={form.email !== undefined}
+ />
+ </div>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ To be used when second factor authentication is enabled
+ </i18n.Translate>
+ </p>
+ </div>
+
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="phone"
+ >
+ {i18n.str`Phone`}
+ </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="phone"
+ id="phone"
+ disabled={purpose === "show"}
+ value={form.phone ?? defaultValue.phone}
+ data-error={!!errors?.phone && form.phone !== undefined}
+ onChange={(e) => {
+ form.phone = e.currentTarget.value;
+ updateForm(structuredClone(form));
+ }}
+ autocomplete="off"
+ />
+ <ShowInputErrorLabel
+ message={errors?.phone}
+ isDirty={form.phone !== undefined}
+ />
+ </div>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ To be used when second factor authentication is enabled
+ </i18n.Translate>
+ </p>
+ </div>
+
+ {isCashoutEnabled && (
+ <TextField
+ id="cashout-account"
+ label={i18n.str`Cashout account`}
+ help={i18n.str`External account number where the money is going to be sent when doing cashouts`}
+ error={errors?.cashout_payto_uri}
+ onChange={(e) => {
+ form.cashout_payto_uri = e as PaytoString;
+ updateForm(structuredClone(form));
+ }}
+ value={
+ (form.cashout_payto_uri ??
+ defaultValue.cashout_payto_uri) as PaytoString
+ }
+ disabled={!editableCashout}
+ />
+ )}
+
+ <div class="sm:col-span-5">
+ <label
+ for="debit"
+ class="block text-sm font-medium leading-6 text-gray-900"
+ >{i18n.str`Max debt`}</label>
+ <InputAmount
+ name="debit"
+ left
+ currency={config.currency}
+ value={form.debit_threshold ?? defaultValue.debit_threshold}
+ onChange={
+ !editableThreshold
+ ? undefined
+ : (e) => {
+ form.debit_threshold = e as AmountString;
+ updateForm(structuredClone(form));
+ }
+ }
+ />
+ <ShowInputErrorLabel
+ message={
+ errors?.debit_threshold
+ ? String(errors?.debit_threshold)
+ : undefined
+ }
+ isDirty={form.debit_threshold !== undefined}
+ />
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ How much the balance can go below zero.
+ </i18n.Translate>
+ </p>
+ </div>
+
+ <div class="sm:col-span-5">
+ <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"
+ >
+ <i18n.Translate>Is this account public?</i18n.Translate>
+ </span>
+ </span>
+ <button
+ type="button"
+ name="is public"
+ data-enabled={
+ form.isPublic ?? defaultValue.isPublic ? "true" : "false"
+ }
+ 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={() => {
+ form.isPublic = !(form.isPublic ?? defaultValue.isPublic);
+ updateForm(structuredClone(form));
+ }}
+ >
+ <span
+ aria-hidden="true"
+ data-enabled={
+ form.isPublic ?? defaultValue.isPublic ? "true" : "false"
+ }
+ 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>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ Public accounts have their balance publicly accessible
+ </i18n.Translate>
+ </p>
+ </div>
+
+ {purpose !== "create" || !userIsAdmin ? undefined : (
+ <div class="sm:col-span-5">
+ <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"
+ >
+ <i18n.Translate>
+ Is this account a payment provider?
+ </i18n.Translate>
+ </span>
+ </span>
+ <button
+ type="button"
+ name="is exchange"
+ data-enabled={
+ form.isExchange ?? defaultValue.isExchange
+ ? "true"
+ : "false"
+ }
+ 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={() => {
+ form.isExchange = !form.isExchange;
+ updateForm(structuredClone(form));
+ }}
+ >
+ <span
+ aria-hidden="true"
+ data-enabled={
+ form.isExchange ?? defaultValue.isExchange
+ ? "true"
+ : "false"
+ }
+ 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>
+ </div>
+ )}
+ </div>
+ </div>
+ {children}
+ </form>
+ );
+}
+
+function getAccountId(
+ type: "iban" | "x-taler-bank",
+ s: PaytoString | undefined,
+): string | undefined {
+ if (s === undefined) return undefined;
+ const p = parsePaytoUri(s);
+ if (p === undefined) return undefined;
+ if (!p.isKnown) return "<unknown>";
+ if (type === "iban" && p.targetType === "iban") return p.iban;
+ if (type === "x-taler-bank" && p.targetType === "x-taler-bank")
+ return p.account;
+ return "<unsupported>";
+}
+
+{
+ /* <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="cashout"
+ >
+ {}
+ </label>
+ <div class="mt-2">
+ <input
+ type="text"
+ ref={focus && purpose === "update" ? doAutoFocus : undefined}
+ data-error={!!errors?.cashout_payto_uri && form.cashout_payto_uri !== undefined}
+ 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="cashout"
+ id="cashout"
+ disabled={purpose === "show"}
+ value={form.cashout_payto_uri ?? defaultValue.cashout_payto_uri}
+ onChange={(e) => {
+ form.cashout_payto_uri = e.currentTarget.value as PaytoString;
+ if (!form.cashout_payto_uri) {
+ form.cashout_payto_uri = undefined
+ }
+ updateForm(structuredClone(form));
+ }}
+ autocomplete="off"
+ />
+ <ShowInputErrorLabel
+ message={errors?.cashout_payto_uri}
+ isDirty={form.cashout_payto_uri !== undefined}
+ />
+ </div>
+ <p class="mt-2 text-sm text-gray-500" >
+ <i18n.Translate></i18n.Translate>
+ </p>
+ </div> */
+}
+
+// function PaytoField({
+// name,
+// label,
+// help,
+// type,
+// value,
+// disabled,
+// onChange,
+// error,
+// }: {
+// error: TranslatedString | undefined;
+// name: string;
+// label: TranslatedString;
+// help: TranslatedString;
+// onChange: (s: string) => void;
+// type: "iban" | "x-taler-bank" | "bitcoin";
+// disabled?: boolean;
+// value: string | undefined;
+// }): VNode {
+// if (type === "iban") {
+// return (
+// <div class="sm:col-span-5">
+// <label
+// class="block text-sm font-medium leading-6 text-gray-900"
+// for={name}
+// >
+// {label}
+// </label>
+// <div class="mt-2">
+// <div class="flex justify-between">
+// <input
+// type="text"
+// class="mr-4 w-full block-inline 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={name}
+// id={name}
+// disabled={disabled}
+// value={value ?? ""}
+// onChange={(e) => {
+// onChange(e.currentTarget.value);
+// }}
+// />
+// <CopyButton
+// class="p-2 rounded-full text-black shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
+// getContent={() => value ?? ""}
+// />
+// </div>
+// <ShowInputErrorLabel message={error} isDirty={value !== undefined} />
+// </div>
+// <p class="mt-2 text-sm text-gray-500">{help}</p>
+// </div>
+// );
+// }
+// if (type === "x-taler-bank") {
+// return (
+// <div class="sm:col-span-5">
+// <label
+// class="block text-sm font-medium leading-6 text-gray-900"
+// for={name}
+// >
+// {label}
+// </label>
+// <div class="mt-2">
+// <div class="flex justify-between">
+// <input
+// type="text"
+// class="mr-4 w-full block-inline 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={name}
+// id={name}
+// disabled={disabled}
+// value={value ?? ""}
+// onChange={(e) => {
+// onChange(e.currentTarget.value);
+// }}
+// />
+// <CopyButton
+// class="p-2 rounded-full text-black shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
+// getContent={() => value ?? ""}
+// />
+// </div>
+// <ShowInputErrorLabel message={error} isDirty={value !== undefined} />
+// </div>
+// <p class="mt-2 text-sm text-gray-500">
+// {help}
+// </p>
+// </div>
+// );
+// }
+// if (type === "bitcoin") {
+// return (
+// <div class="sm:col-span-5">
+// <label
+// class="block text-sm font-medium leading-6 text-gray-900"
+// for={name}
+// >
+// {label}
+// </label>
+// <div class="mt-2">
+// <div class="flex justify-between">
+// <input
+// type="text"
+// class="mr-4 w-full block-inline 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={name}
+// id={name}
+// disabled={disabled}
+// value={value ?? ""}
+// />
+// <CopyButton
+// class="p-2 rounded-full text-black shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
+// getContent={() => value ?? ""}
+// />
+// <ShowInputErrorLabel
+// message={error}
+// isDirty={value !== undefined}
+// />
+// </div>
+// </div>
+// <p class="mt-2 text-sm text-gray-500">
+// {/* <i18n.Translate>bitcoin address</i18n.Translate> */}
+// {help}
+// </p>
+// </div>
+// );
+// }
+// assertUnreachable(type);
+// }
diff --git a/packages/bank-ui/src/pages/admin/AccountList.tsx b/packages/bank-ui/src/pages/admin/AccountList.tsx
new file mode 100644
index 000000000..6402c2bcd
--- /dev/null
+++ b/packages/bank-ui/src/pages/admin/AccountList.tsx
@@ -0,0 +1,234 @@
+/*
+ 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 {
+ Amounts,
+ HttpStatusCode,
+ TalerError,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
+import { Loading, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js";
+import { useBankCoreApiContext } from "@gnu-taler/web-util/browser";
+import { useBusinessAccounts } from "../../hooks/regional.js";
+import { RouteDefinition } from "@gnu-taler/web-util/browser";
+import { RenderAmount } from "../PaytoWireTransferForm.js";
+
+interface Props {
+ routeCreate: RouteDefinition;
+
+ routeShowAccount: RouteDefinition<{ account: string }>;
+ routeRemoveAccount: RouteDefinition<{ account: string }>;
+ routeUpdatePasswordAccount: RouteDefinition<{ account: string }>;
+}
+
+export function AccountList({
+ routeCreate,
+ routeRemoveAccount,
+ routeShowAccount,
+ routeUpdatePasswordAccount,
+}: Props): VNode {
+ const result = useBusinessAccounts();
+ const { i18n } = useTranslationContext();
+ const { config } = useBankCoreApiContext();
+
+ if (!result) {
+ return <Loading />;
+ }
+ if (result instanceof TalerError) {
+ return <ErrorLoadingWithDebug error={result} />;
+ }
+ if (result.type === "fail") {
+ switch (result.case) {
+ case HttpStatusCode.Unauthorized:
+ return <Fragment />;
+ default:
+ assertUnreachable(result.case);
+ }
+ }
+
+ const onGoStart = result.isFirstPage ? undefined : result.loadFirst;
+ const onGoNext = result.isLastPage ? undefined : result.loadNext;
+
+ const accounts = result.body;
+ return (
+ <Fragment>
+ <div class="px-4 sm:px-6 lg:px-8 mt-8">
+ <div class="sm:flex sm:items-center">
+ <div class="sm:flex-auto">
+ <h1 class="text-base font-semibold leading-6 text-gray-900">
+ <i18n.Translate>Accounts</i18n.Translate>
+ </h1>
+ </div>
+ <div class="mt-4 sm:ml-16 sm:mt-0 sm:flex-none">
+ <a
+ href={routeCreate.url({})}
+ name="create account"
+ type="button"
+ class="block rounded-md bg-indigo-600 px-3 py-2 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ >
+ <i18n.Translate>Create account</i18n.Translate>
+ </a>
+ </div>
+ </div>
+ <div class="mt-4 flow-root">
+ <div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
+ <div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
+ {!accounts.length ? (
+ <div>{/* FIXME: ADD empty list */}</div>
+ ) : (
+ <table class="min-w-full divide-y divide-gray-300">
+ <thead>
+ <tr>
+ <th
+ scope="col"
+ class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-0"
+ >{i18n.str`Username`}</th>
+ <th
+ scope="col"
+ class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
+ >{i18n.str`Name`}</th>
+ <th
+ scope="col"
+ class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
+ >{i18n.str`Balance`}</th>
+ <th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-0">
+ <span class="sr-only">{i18n.str`Actions`}</span>
+ </th>
+ </tr>
+ </thead>
+ <tbody class="divide-y divide-gray-200">
+ {accounts.map((item, idx) => {
+ const balance = !item.balance
+ ? undefined
+ : Amounts.parse(item.balance.amount);
+ const noBalance = Amounts.isZero(item.balance.amount);
+ const balanceIsDebit =
+ item.balance &&
+ item.balance.credit_debit_indicator == "debit";
+
+ return (
+ <tr key={idx}>
+ <td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-0">
+ <a
+ name={`show account ${item.username}`}
+ href={routeShowAccount.url({
+ account: item.username,
+ })}
+ class="text-indigo-600 hover:text-indigo-900"
+ >
+ {item.username}
+ </a>
+ </td>
+ <td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
+ {item.name}
+ </td>
+ <td
+ data-negative={
+ noBalance
+ ? undefined
+ : balanceIsDebit
+ ? "true"
+ : "false"
+ }
+ class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 data-[negative=false]:text-green-600 data-[negative=true]:text-red-600 "
+ >
+ {!balance ? (
+ i18n.str`Unknown`
+ ) : (
+ <span class="amount">
+ <RenderAmount
+ value={balance}
+ negative={balanceIsDebit}
+ spec={config.currency_specification}
+ />
+ </span>
+ )}
+ </td>
+ <td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-0">
+ <a
+ name={`update password ${item.username}`}
+ href={routeUpdatePasswordAccount.url({
+ account: item.username,
+ })}
+ class="text-indigo-600 hover:text-indigo-900"
+ >
+ <i18n.Translate>Change password</i18n.Translate>
+ </a>
+ <br />
+ {/* {config.allow_conversion ?
+ <Fragment>
+
+ <a
+ name={`show cashout ${item.username}`}
+ href={routeShowCashoutsAccount.url({
+ account: item.username,
+ })}
+ class="text-indigo-600 hover:text-indigo-900"
+ >
+ <i18n.Translate>Cashouts</i18n.Translate>
+ </a>
+ <br />
+ </Fragment>
+ : undefined} */}
+ {noBalance ? (
+ <a
+ name={`remove account ${item.username}`}
+ href={routeRemoveAccount.url({
+ account: item.username,
+ })}
+ class="text-indigo-600 hover:text-indigo-900"
+ >
+ <i18n.Translate>Remove</i18n.Translate>
+ </a>
+ ) : undefined}
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ )}
+ </div>
+ <nav
+ class="flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6 rounded-lg"
+ aria-label="Pagination"
+ >
+ <div class="flex flex-1 justify-between sm:justify-end">
+ <button
+ name="first page"
+ 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"
+ disabled={!onGoStart}
+ onClick={onGoStart}
+ >
+ <i18n.Translate>First page</i18n.Translate>
+ </button>
+ <button
+ name="next page"
+ class="relative disabled:bg-gray-100 disabled:text-gray-500 ml-3 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"
+ disabled={!onGoNext}
+ onClick={onGoNext}
+ >
+ <i18n.Translate>Next</i18n.Translate>
+ </button>
+ </div>
+ </nav>
+ </div>
+ </div>
+ </div>
+ </Fragment>
+ );
+}
diff --git a/packages/bank-ui/src/pages/admin/AdminHome.tsx b/packages/bank-ui/src/pages/admin/AdminHome.tsx
new file mode 100644
index 000000000..acae09b40
--- /dev/null
+++ b/packages/bank-ui/src/pages/admin/AdminHome.tsx
@@ -0,0 +1,623 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ AbsoluteTime,
+ AmountString,
+ Amounts,
+ CurrencySpecification,
+ HttpStatusCode,
+ TalerCorebankApi,
+ TalerError,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
+import {
+ Attention,
+ RouteDefinition,
+ useBankCoreApiContext,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import {
+ format,
+ sub
+} from "date-fns";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js";
+import { Transactions } from "../../components/Transactions/index.js";
+import { useConversionInfo, useLastMonitorInfo } from "../../hooks/regional.js";
+import { RenderAmount } from "../PaytoWireTransferForm.js";
+import { WireTransfer } from "../WireTransfer.js";
+import { AccountList } from "./AccountList.js";
+
+/**
+ * Query account information and show QR code if there is pending withdrawal
+ */
+interface Props {
+ routeCreate: RouteDefinition;
+ routeDownloadStats: RouteDefinition;
+ routeCreateWireTransfer: RouteDefinition<{
+ account?: string;
+ subject?: string;
+ amount?: string;
+ }>;
+
+ routeShowAccount: RouteDefinition<{ account: string }>;
+ routeRemoveAccount: RouteDefinition<{ account: string }>;
+ routeUpdatePasswordAccount: RouteDefinition<{ account: string }>;
+ routeShowCashoutsAccount: RouteDefinition<{ account: string }>;
+ onAuthorizationRequired: () => void;
+}
+export function AdminHome({
+ routeCreate,
+ routeRemoveAccount,
+ routeShowAccount,
+ routeUpdatePasswordAccount,
+ routeDownloadStats,
+ routeCreateWireTransfer,
+ onAuthorizationRequired,
+}: Props): VNode {
+ return (
+ <Fragment>
+ <Metrics routeDownloadStats={routeDownloadStats} />
+ <WireTransfer
+ routeHere={routeCreateWireTransfer}
+ onAuthorizationRequired={onAuthorizationRequired}
+ />
+ <Transactions
+ account="admin"
+ routeCreateWireTransfer={routeCreateWireTransfer}
+ />
+ <AccountList
+ routeCreate={routeCreate}
+ routeRemoveAccount={routeRemoveAccount}
+ routeShowAccount={routeShowAccount}
+ routeUpdatePasswordAccount={routeUpdatePasswordAccount}
+ />
+ </Fragment>
+ );
+}
+
+function getDateForTimeframe(
+ date: AbsoluteTime,
+ timeframe: TalerCorebankApi.MonitorTimeframeParam,
+ locale: Locale,
+): string {
+ if (date.t_ms === "never") return "--";
+ switch (timeframe) {
+ case TalerCorebankApi.MonitorTimeframeParam.hour:
+ return `${format(date.t_ms, "HH", { locale })}hs`;
+ case TalerCorebankApi.MonitorTimeframeParam.day:
+ return format(date.t_ms, "EEEE", { locale });
+ case TalerCorebankApi.MonitorTimeframeParam.month:
+ return format(date.t_ms, "MMMM", { locale });
+ case TalerCorebankApi.MonitorTimeframeParam.year:
+ return format(date.t_ms, "yyyy", { locale });
+ case TalerCorebankApi.MonitorTimeframeParam.decade:
+ return format(date.t_ms, "yyyy", { locale });
+ }
+ assertUnreachable(timeframe);
+}
+
+export function getTimeframesForDate(
+ time: Date,
+ timeframe: TalerCorebankApi.MonitorTimeframeParam,
+): { current: AbsoluteTime; previous: AbsoluteTime } {
+ switch (timeframe) {
+ case TalerCorebankApi.MonitorTimeframeParam.hour:
+ return {
+ current: AbsoluteTime.fromMilliseconds(
+ sub(time, { hours: 1 }).getTime(),
+ ),
+ previous: AbsoluteTime.fromMilliseconds(
+ sub(time, { hours: 2 }).getTime(),
+ ),
+ };
+ case TalerCorebankApi.MonitorTimeframeParam.day:
+ return {
+ current: AbsoluteTime.fromMilliseconds(
+ sub(time, { days: 1 }).getTime(),
+ ),
+ previous: AbsoluteTime.fromMilliseconds(
+ sub(time, { days: 4 }).getTime(),
+ ),
+ };
+ case TalerCorebankApi.MonitorTimeframeParam.month:
+ return {
+ current: AbsoluteTime.fromMilliseconds(
+ sub(time, { months: 1 }).getTime(),
+ ),
+ previous: AbsoluteTime.fromMilliseconds(
+ sub(time, { months: 2 }).getTime(),
+ ),
+ };
+ case TalerCorebankApi.MonitorTimeframeParam.year:
+ return {
+ current: AbsoluteTime.fromMilliseconds(
+ sub(time, { years: 1 }).getTime(),
+ ),
+ previous: AbsoluteTime.fromMilliseconds(
+ sub(time, { years: 2 }).getTime(),
+ ),
+ };
+ case TalerCorebankApi.MonitorTimeframeParam.decade:
+ return {
+ current: AbsoluteTime.fromMilliseconds(
+ sub(time, { years: 10 }).getTime(),
+ ),
+ previous: AbsoluteTime.fromMilliseconds(
+ sub(time, { years: 20 }).getTime(),
+ ),
+ };
+ default:
+ assertUnreachable(timeframe);
+ }
+}
+
+function Metrics({
+ routeDownloadStats,
+}: {
+ routeDownloadStats: RouteDefinition;
+}): VNode {
+ const { i18n, dateLocale } = useTranslationContext();
+ const [metricType, setMetricType] =
+ useState<TalerCorebankApi.MonitorTimeframeParam>(
+ TalerCorebankApi.MonitorTimeframeParam.hour,
+ );
+ const { config } = useBankCoreApiContext();
+ const respInfo = useConversionInfo();
+ const params = getTimeframesForDate(new Date(), metricType);
+
+ const resp = useLastMonitorInfo(params.current, params.previous, metricType);
+ if (!resp) return <Fragment />;
+ if (resp instanceof TalerError) {
+ return <ErrorLoadingWithDebug error={resp} />;
+ }
+ if (!respInfo) return <Fragment />;
+ if (respInfo instanceof TalerError) {
+ return <ErrorLoadingWithDebug error={respInfo} />;
+ }
+ if (respInfo.type === "fail") {
+ switch (respInfo.case) {
+ case HttpStatusCode.NotImplemented: {
+ return (
+ <Attention type="danger" title={i18n.str`Cashout are disabled`}>
+ <i18n.Translate>
+ Cashout should be enable by configuration and the conversion rate
+ should be initialized with fee, ratio and rounding mode.
+ </i18n.Translate>
+ </Attention>
+ );
+ }
+ default: {
+ assertUnreachable(respInfo.case);
+ }
+ }
+ }
+
+ if (resp.current.type !== "ok") {
+ switch (resp.current.case) {
+ case HttpStatusCode.BadRequest:
+ return (
+ <Attention
+ type="warning"
+ title={i18n.str`Querying for the current stats failed`}
+ >
+ <i18n.Translate>The request parameters are wrong</i18n.Translate>
+ </Attention>
+ );
+ case HttpStatusCode.Unauthorized:
+ return (
+ <Attention
+ type="warning"
+ title={i18n.str`Querying for the current stats failed`}
+ >
+ <i18n.Translate>The user is unauthorized</i18n.Translate>
+ </Attention>
+ );
+ default: {
+ assertUnreachable(resp.current);
+ }
+ }
+ }
+ if (resp.previous.type !== "ok") {
+ switch (resp.previous.case) {
+ case HttpStatusCode.BadRequest:
+ return (
+ <Attention
+ type="warning"
+ title={i18n.str`Querying for the previous stats failed`}
+ >
+ <i18n.Translate>The request parameters are wrong</i18n.Translate>
+ </Attention>
+ );
+ case HttpStatusCode.Unauthorized:
+ return (
+ <Attention
+ type="warning"
+ title={i18n.str`Querying for the previous stats failed`}
+ >
+ <i18n.Translate>The user is unauthorized</i18n.Translate>
+ </Attention>
+ );
+ default: {
+ assertUnreachable(resp.previous);
+ }
+ }
+ }
+ return (
+ <div class="px-4 mt-4">
+ <div class="sm:flex sm:items-center mb-4">
+ <div class="sm:flex-auto">
+ <h1 class="text-base font-semibold leading-6 text-gray-900">
+ <i18n.Translate>Transaction volume report</i18n.Translate>
+ </h1>
+ </div>
+ </div>
+
+ <div class="sm:hidden">
+ <label for="tabs" class="sr-only">
+ <i18n.Translate>Select a section</i18n.Translate>
+ </label>
+ <select
+ id="tabs"
+ name="tabs"
+ class="block w-full rounded-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500"
+ onChange={(e) => {
+ // const op = e.currentTarget.value as typeof metricType
+ setMetricType(
+ e.currentTarget
+ .value as unknown as TalerCorebankApi.MonitorTimeframeParam,
+ );
+ }}
+ >
+ <option
+ value={TalerCorebankApi.MonitorTimeframeParam.hour}
+ selected={metricType == TalerCorebankApi.MonitorTimeframeParam.hour}
+ >
+ <i18n.Translate>Last hour</i18n.Translate>
+ </option>
+ <option
+ value={TalerCorebankApi.MonitorTimeframeParam.day}
+ selected={metricType == TalerCorebankApi.MonitorTimeframeParam.day}
+ >
+ <i18n.Translate>Previous day</i18n.Translate>
+ </option>
+ <option
+ value={TalerCorebankApi.MonitorTimeframeParam.month}
+ selected={
+ metricType == TalerCorebankApi.MonitorTimeframeParam.month
+ }
+ >
+ <i18n.Translate>Last month</i18n.Translate>
+ </option>
+ <option
+ value={TalerCorebankApi.MonitorTimeframeParam.year}
+ selected={metricType == TalerCorebankApi.MonitorTimeframeParam.year}
+ >
+ <i18n.Translate>Last year</i18n.Translate>
+ </option>
+ </select>
+ </div>
+ <div class="hidden sm:block">
+ {/* FIXME: This should be LINKS */}
+ <nav
+ class="isolate flex divide-x divide-gray-200 rounded-lg shadow"
+ aria-label="Tabs"
+ >
+ <button
+ type="button"
+ name="set last hour"
+ onClick={(e) => {
+ e.preventDefault();
+ setMetricType(TalerCorebankApi.MonitorTimeframeParam.hour);
+ }}
+ data-selected={
+ metricType == TalerCorebankApi.MonitorTimeframeParam.hour
+ }
+ class="rounded-l-lg text-gray-500 hover:text-gray-700 data-[selected=true]:text-gray-900 group relative min-w-0 flex-1 overflow-hidden bg-white py-4 px-4 text-center text-sm font-medium hover:bg-gray-50 focus:z-10"
+ >
+ <span>
+ <i18n.Translate>Last hour</i18n.Translate>
+ </span>
+ <span
+ aria-hidden="true"
+ data-selected={
+ metricType == TalerCorebankApi.MonitorTimeframeParam.hour
+ }
+ class="bg-transparent data-[selected=true]:bg-indigo-500 absolute inset-x-0 bottom-0 h-0.5"
+ ></span>
+ </button>
+ <button
+ type="button"
+ name="set previous day"
+ onClick={(e) => {
+ e.preventDefault();
+ setMetricType(TalerCorebankApi.MonitorTimeframeParam.day);
+ }}
+ data-selected={
+ metricType == TalerCorebankApi.MonitorTimeframeParam.day
+ }
+ class=" text-gray-500 hover:text-gray-700 data-[selected=true]:text-gray-900 group relative min-w-0 flex-1 overflow-hidden bg-white py-4 px-4 text-center text-sm font-medium hover:bg-gray-50 focus:z-10"
+ >
+ <span>
+ <i18n.Translate>Previous day</i18n.Translate>
+ </span>
+ <span
+ aria-hidden="true"
+ data-selected={
+ metricType == TalerCorebankApi.MonitorTimeframeParam.day
+ }
+ class="bg-transparent data-[selected=true]:bg-indigo-500 absolute inset-x-0 bottom-0 h-0.5"
+ ></span>
+ </button>
+ <button
+ type="button"
+ name="set last month"
+ onClick={(e) => {
+ e.preventDefault();
+ setMetricType(TalerCorebankApi.MonitorTimeframeParam.month);
+ }}
+ data-selected={
+ metricType == TalerCorebankApi.MonitorTimeframeParam.month
+ }
+ class="rounded-r-lg text-gray-500 hover:text-gray-700 data-[selected=true]:text-gray-900 group relative min-w-0 flex-1 overflow-hidden bg-white py-4 px-4 text-center text-sm font-medium hover:bg-gray-50 focus:z-10"
+ >
+ <span>
+ <i18n.Translate>Last month</i18n.Translate>
+ </span>
+ <span
+ aria-hidden="true"
+ data-selected={
+ metricType == TalerCorebankApi.MonitorTimeframeParam.month
+ }
+ class="bg-transparent data-[selected=true]:bg-indigo-500 absolute inset-x-0 bottom-0 h-0.5"
+ ></span>
+ </button>
+ <button
+ type="button"
+ name="set last year"
+ onClick={(e) => {
+ e.preventDefault();
+ setMetricType(TalerCorebankApi.MonitorTimeframeParam.year);
+ }}
+ data-selected={
+ metricType == TalerCorebankApi.MonitorTimeframeParam.year
+ }
+ class="rounded-r-lg text-gray-500 hover:text-gray-700 data-[selected=true]:text-gray-900 group relative min-w-0 flex-1 overflow-hidden bg-white py-4 px-4 text-center text-sm font-medium hover:bg-gray-50 focus:z-10"
+ >
+ <span>
+ <i18n.Translate>Last Year</i18n.Translate>
+ </span>
+ <span
+ aria-hidden="true"
+ data-selected={
+ metricType == TalerCorebankApi.MonitorTimeframeParam.year
+ }
+ class="bg-transparent data-[selected=true]:bg-indigo-500 absolute inset-x-0 bottom-0 h-0.5"
+ ></span>
+ </button>
+ </nav>
+ </div>
+
+ <div class="w-full flex justify-between">
+ <h1 class="text-base text-gray-900 mt-5">
+ {i18n.str`Trading volume on ${getDateForTimeframe(
+ params.current,
+ metricType,
+ dateLocale,
+ )} compared to ${getDateForTimeframe(
+ params.previous,
+ metricType,
+ dateLocale,
+ )}`}
+ </h1>
+ </div>
+ <dl class="mt-5 grid grid-cols-1 md:grid-cols-2 divide-y divide-gray-200 overflow-hidden rounded-lg bg-white shadow-lg md:divide-x md:divide-y-0">
+ {resp.current.body.type !== "with-conversions" ||
+ resp.previous.body.type !== "with-conversions" ? undefined : (
+ <Fragment>
+ <div class="px-4 py-5 sm:p-6">
+ <dt class="text-base font-normal text-gray-900">
+ <i18n.Translate>Cashin</i18n.Translate>
+ <div class="text-xs text-gray-500">
+ <i18n.Translate>
+ Transferred from an external account to an account in this
+ bank.
+ </i18n.Translate>
+ </div>
+ </dt>
+ <MetricValue
+ current={resp.current.body.cashinFiatVolume}
+ previous={resp.previous.body.cashinFiatVolume}
+ spec={respInfo.body.fiat_currency_specification}
+ />
+ </div>
+ <div class="px-4 py-5 sm:p-6">
+ <dt class="text-base font-normal text-gray-900">
+ <i18n.Translate>Cashout</i18n.Translate>
+ </dt>
+ <div class="text-xs text-gray-500">
+ <i18n.Translate>
+ Transferred from an account in this bank to an external
+ account.
+ </i18n.Translate>
+ </div>
+ <MetricValue
+ current={resp.current.body.cashoutFiatVolume}
+ previous={resp.previous.body.cashoutFiatVolume}
+ spec={respInfo.body.fiat_currency_specification}
+ />
+ </div>
+ </Fragment>
+ )}
+ <div class="px-4 py-5 sm:p-6">
+ <dt class="text-base font-normal text-gray-900">
+ <i18n.Translate>Payin</i18n.Translate>
+ <div class="text-xs text-gray-500">
+ <i18n.Translate>
+ Transferred from an account to a Taler exchange.
+ </i18n.Translate>
+ </div>
+ </dt>
+ <MetricValue
+ current={resp.current.body.talerInVolume}
+ previous={resp.previous.body.talerInVolume}
+ spec={config.currency_specification}
+ />
+ </div>
+ <div class="px-4 py-5 sm:p-6">
+ <dt class="text-base font-normal text-gray-900">
+ <i18n.Translate>Payout</i18n.Translate>
+ <div class="text-xs text-gray-500">
+ <i18n.Translate>
+ Transferred from a Taler exchange to another account.
+ </i18n.Translate>
+ </div>
+ </dt>
+ <MetricValue
+ current={resp.current.body.talerOutVolume}
+ previous={resp.previous.body.talerOutVolume}
+ spec={config.currency_specification}
+ />
+ </div>
+ </dl>
+ <div class="flex justify-end mt-4">
+ <a
+ href={routeDownloadStats.url({})}
+ name="download stats"
+ class="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>Download stats as CSV</i18n.Translate>
+ </a>
+ </div>
+ </div>
+ );
+}
+
+function MetricValue({
+ current,
+ previous,
+ spec,
+}: {
+ spec: CurrencySpecification;
+ current: AmountString | undefined;
+ previous: AmountString | undefined;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const cmp = current && previous ? Amounts.cmp(current, previous) : 0;
+ const cv = !current ? undefined : Amounts.stringifyValue(current);
+ const currAmount = !cv ? undefined : Number.parseFloat(cv);
+ const prevAmount = !previous
+ ? undefined
+ : Number.parseFloat(Amounts.stringifyValue(previous));
+
+ const rate =
+ !currAmount ||
+ Number.isNaN(currAmount) ||
+ !prevAmount ||
+ Number.isNaN(prevAmount)
+ ? 0
+ : cmp === -1
+ ? 1 - Math.round(currAmount) / Math.round(prevAmount)
+ : cmp === 1
+ ? Math.round(currAmount) / Math.round(prevAmount) - 1
+ : 0;
+
+ const negative = cmp === 0 ? undefined : cmp === -1;
+ const rateStr = `${(Math.abs(rate) * 100).toFixed(2)}%`;
+ return (
+ <Fragment>
+ <dd class="mt-1 block ">
+ <div class="flex justify-start text-2xl items-baseline font-semibold text-indigo-600">
+ {!current ? (
+ "-"
+ ) : (
+ <RenderAmount
+ value={Amounts.parseOrThrow(current)}
+ spec={spec}
+ hideSmall
+ />
+ )}
+ </div>
+ <div class="flex flex-col">
+ <div class="flex justify-end items-baseline text-2xl font-semibold text-indigo-600">
+ <small class="ml-2 text-sm font-medium text-gray-500">
+ <i18n.Translate>from</i18n.Translate>{" "}
+ {!previous ? (
+ "-"
+ ) : (
+ <RenderAmount
+ value={Amounts.parseOrThrow(previous)}
+ spec={spec}
+ hideSmall
+ />
+ )}
+ </small>
+ </div>
+ {!!rate && (
+ <span
+ data-negative={negative}
+ class="flex items-center gap-x-1.5 w-fit rounded-md bg-green-100 text-green-800 data-[negative=true]:bg-red-100 px-2 py-1 text-xs font-medium data-[negative=true]:text-red-700 whitespace-pre"
+ >
+ {negative ? (
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="1.5"
+ stroke="currentColor"
+ class="w-6 h-6"
+ >
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="M12 4.5v15m0 0l6.75-6.75M12 19.5l-6.75-6.75"
+ />
+ </svg>
+ ) : (
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="1.5"
+ stroke="currentColor"
+ class="w-6 h-6"
+ >
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="M12 19.5v-15m0 0l-6.75 6.75M12 4.5l6.75 6.75"
+ />
+ </svg>
+ )}
+
+ {negative ? (
+ <span class="sr-only">
+ <i18n.Translate>Decreased by</i18n.Translate>
+ </span>
+ ) : (
+ <span class="sr-only">
+ <i18n.Translate>Increased by</i18n.Translate>
+ </span>
+ )}
+ {rateStr}
+ </span>
+ )}
+ </div>
+ </dd>
+ </Fragment>
+ );
+}
diff --git a/packages/bank-ui/src/pages/admin/CreateNewAccount.tsx b/packages/bank-ui/src/pages/admin/CreateNewAccount.tsx
new file mode 100644
index 000000000..7d2d449b0
--- /dev/null
+++ b/packages/bank-ui/src/pages/admin/CreateNewAccount.tsx
@@ -0,0 +1,217 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ AbsoluteTime,
+ HttpStatusCode,
+ TalerCorebankApi,
+ TalerErrorCode,
+ TranslatedString,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
+import {
+ Attention,
+ LocalNotificationBanner,
+ notifyInfo,
+ useLocalNotification,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { useBankCoreApiContext } from "@gnu-taler/web-util/browser";
+import { useSessionState } from "../../hooks/session.js";
+import { RouteDefinition } from "@gnu-taler/web-util/browser";
+import { AccountForm } from "./AccountForm.js";
+
+export function CreateNewAccount({
+ routeCancel,
+ onCreateSuccess,
+}: {
+ routeCancel: RouteDefinition;
+ onCreateSuccess: () => void;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const { state: credentials } = useSessionState();
+ const token =
+ credentials.status !== "loggedIn" ? undefined : credentials.token;
+ const {
+ lib: { bank: api },
+ } = useBankCoreApiContext();
+
+ const [submitAccount, setSubmitAccount] = useState<
+ TalerCorebankApi.RegisterAccountRequest | undefined
+ >();
+ const [notification, notify, handleError] = useLocalNotification();
+
+ async function doCreate() {
+ if (!submitAccount || !token) return;
+ await handleError(async () => {
+ const resp = await api.createAccount(token, submitAccount);
+ if (resp.type === "ok") {
+ notifyInfo(
+ i18n.str`Account created with password "${submitAccount.password}".`,
+ );
+ onCreateSuccess();
+ } else {
+ switch (resp.case) {
+ case HttpStatusCode.BadRequest:
+ return notify({
+ type: "error",
+ title: i18n.str`Server replied that phone or email is invalid`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case HttpStatusCode.Unauthorized:
+ return notify({
+ type: "error",
+ title: i18n.str`The rights to perform the operation are not sufficient`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case TalerErrorCode.BANK_REGISTER_USERNAME_REUSE:
+ return notify({
+ type: "error",
+ title: i18n.str`Account username is already taken`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case TalerErrorCode.BANK_REGISTER_PAYTO_URI_REUSE:
+ return notify({
+ type: "error",
+ title: i18n.str`Account id is already taken`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case TalerErrorCode.BANK_UNALLOWED_DEBIT:
+ return notify({
+ type: "error",
+ title: i18n.str`Bank ran out of bonus credit.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT:
+ return notify({
+ type: "error",
+ title: i18n.str`Account username can't be used because is reserved`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT:
+ return notify({
+ type: "error",
+ title: i18n.str`Only admin is allow to set debt limit.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case TalerErrorCode.BANK_MISSING_TAN_INFO:
+ return notify({
+ type: "error",
+ title: i18n.str`No information for the selected authentication channel.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED:
+ return notify({
+ type: "error",
+ title: i18n.str`Authentication channel is not supported.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case TalerErrorCode.BANK_NON_ADMIN_SET_TAN_CHANNEL:
+ return notify({
+ type: "error",
+ title: i18n.str`Only admin can create accounts with second factor authentication.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ default:
+ assertUnreachable(resp);
+ }
+ }
+ });
+ }
+
+ if (!(credentials.status === "loggedIn" && credentials.isUserAdministrator)) {
+ return (
+ <Fragment>
+ <Attention type="warning" title={i18n.str`Can't create accounts`}>
+ <i18n.Translate>
+ Only system admin can create accounts.
+ </i18n.Translate>
+ </Attention>
+ <div class="mt-5 sm:mt-6">
+ <a
+ href={routeCancel.url({})}
+ name="close"
+ 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"
+ >
+ <i18n.Translate>Close</i18n.Translate>
+ </a>
+ </div>
+ </Fragment>
+ );
+ }
+
+ 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">
+ <LocalNotificationBanner notification={notification} />
+
+ <div class="px-4 sm:px-0">
+ <h2 class="text-base font-semibold leading-7 text-gray-900">
+ <i18n.Translate>New bank account</i18n.Translate>
+ </h2>
+ </div>
+ <AccountForm
+ template={undefined}
+ purpose="create"
+ onChange={(a) => {
+ setSubmitAccount(a);
+ }}
+ >
+ <div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
+ <a
+ href={routeCancel.url({})}
+ name="cancel"
+ class="text-sm font-semibold leading-6 text-gray-900"
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </a>
+ <button
+ type="submit"
+ name="create"
+ class="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"
+ disabled={!submitAccount}
+ onClick={(e) => {
+ e.preventDefault();
+ doCreate();
+ }}
+ >
+ <i18n.Translate>Create</i18n.Translate>
+ </button>
+ </div>
+ </AccountForm>
+ </div>
+ );
+}
diff --git a/packages/bank-ui/src/pages/admin/DownloadStats.tsx b/packages/bank-ui/src/pages/admin/DownloadStats.tsx
new file mode 100644
index 000000000..8f6bb7c23
--- /dev/null
+++ b/packages/bank-ui/src/pages/admin/DownloadStats.tsx
@@ -0,0 +1,588 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ AccessToken,
+ AmountString,
+ TalerCoreBankHttpClient,
+ TalerCorebankApi,
+ TalerError,
+} from "@gnu-taler/taler-util";
+import {
+ Attention,
+ LocalNotificationBanner,
+ useLocalNotification,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { useBankCoreApiContext } from "@gnu-taler/web-util/browser";
+import { useSessionState } from "../../hooks/session.js";
+import { RouteDefinition } from "@gnu-taler/web-util/browser";
+import { getTimeframesForDate } from "./AdminHome.js";
+
+interface Props {
+ routeCancel: RouteDefinition;
+}
+
+type Options = {
+ dayMetric: boolean;
+ hourMetric: boolean;
+ monthMetric: boolean;
+ yearMetric: boolean;
+ compareWithPrevious: boolean;
+ endOnFirstFail: boolean;
+ includeHeader: boolean;
+};
+
+/**
+ * Show histories of public accounts.
+ */
+export function DownloadStats({ routeCancel }: Props): VNode {
+ const { i18n } = useTranslationContext();
+
+ const { state: credentials } = useSessionState();
+ const creds =
+ credentials.status !== "loggedIn" || !credentials.isUserAdministrator
+ ? undefined
+ : credentials;
+ const {
+ lib: { bank: api },
+ } = useBankCoreApiContext();
+
+ const [options, setOptions] = useState<Options>({
+ compareWithPrevious: true,
+ dayMetric: true,
+ endOnFirstFail: false,
+ hourMetric: true,
+ includeHeader: true,
+ monthMetric: true,
+ yearMetric: true,
+ });
+ const [lastStep, setLastStep] = useState<{ step: number; total: number }>();
+ const [downloaded, setDownloaded] = useState<string>();
+ const referenceDates = [new Date()];
+ const [notification, , handleError] = useLocalNotification();
+
+ if (!creds) {
+ return <div>only admin can download stats</div>;
+ }
+
+ return (
+ <div>
+ <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">
+ <LocalNotificationBanner notification={notification} />
+
+ <div class="px-4 sm:px-0">
+ <h2 class="text-base font-semibold leading-7 text-gray-900">
+ <i18n.Translate>Download bank stats</i18n.Translate>
+ </h2>
+ </div>
+
+ <form
+ class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2"
+ autoCapitalize="none"
+ autoCorrect="off"
+ onSubmit={(e) => {
+ e.preventDefault();
+ }}
+ >
+ <div class="px-4 py-6 sm:p-8">
+ <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
+ <div class="sm:col-span-5">
+ <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"
+ >
+ <i18n.Translate>Include hour metric</i18n.Translate>
+ </span>
+ </span>
+ <button
+ type="button"
+ name={`hour switch`}
+ data-enabled={options.hourMetric}
+ 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={() => {
+ setOptions({
+ ...options,
+ hourMetric: !options.hourMetric,
+ });
+ }}
+ >
+ <span
+ aria-hidden="true"
+ data-enabled={options.hourMetric}
+ 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>
+ </div>
+ <div class="sm:col-span-5">
+ <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"
+ >
+ <i18n.Translate>Include day metric</i18n.Translate>
+ </span>
+ </span>
+ <button
+ type="button"
+ name={`day switch`}
+ data-enabled={!!options.dayMetric}
+ 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={() => {
+ setOptions({ ...options, dayMetric: !options.dayMetric });
+ }}
+ >
+ <span
+ aria-hidden="true"
+ data-enabled={options.dayMetric}
+ 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>
+ </div>
+ <div class="sm:col-span-5">
+ <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"
+ >
+ <i18n.Translate>Include month metric</i18n.Translate>
+ </span>
+ </span>
+ <button
+ type="button"
+ name={`month switch`}
+ data-enabled={!!options.monthMetric}
+ 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={() => {
+ setOptions({
+ ...options,
+ monthMetric: !options.monthMetric,
+ });
+ }}
+ >
+ <span
+ aria-hidden="true"
+ data-enabled={options.monthMetric}
+ 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>
+ </div>
+ <div class="sm:col-span-5">
+ <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"
+ >
+ <i18n.Translate>Include year metric</i18n.Translate>
+ </span>
+ </span>
+ <button
+ type="button"
+ name={`year switch`}
+ data-enabled={!!options.yearMetric}
+ 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={() => {
+ setOptions({
+ ...options,
+ yearMetric: !options.yearMetric,
+ });
+ }}
+ >
+ <span
+ aria-hidden="true"
+ data-enabled={options.yearMetric}
+ 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>
+ </div>
+ <div class="sm:col-span-5">
+ <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"
+ >
+ <i18n.Translate>Include table header</i18n.Translate>
+ </span>
+ </span>
+ <button
+ type="button"
+ name={`header switch`}
+ data-enabled={!!options.includeHeader}
+ 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={() => {
+ setOptions({
+ ...options,
+ includeHeader: !options.includeHeader,
+ });
+ }}
+ >
+ <span
+ aria-hidden="true"
+ data-enabled={options.includeHeader}
+ 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>
+ </div>
+ <div class="sm:col-span-5">
+ <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"
+ >
+ <i18n.Translate>
+ Add previous metric for compare
+ </i18n.Translate>
+ </span>
+ </span>
+ <button
+ type="button"
+ name={`compare switch`}
+ data-enabled={!!options.compareWithPrevious}
+ 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={() => {
+ setOptions({
+ ...options,
+ compareWithPrevious: !options.compareWithPrevious,
+ });
+ }}
+ >
+ <span
+ aria-hidden="true"
+ data-enabled={options.compareWithPrevious}
+ 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>
+ </div>
+ <div class="sm:col-span-5">
+ <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"
+ >
+ <i18n.Translate>Fail on first error</i18n.Translate>
+ </span>
+ </span>
+ <button
+ type="button"
+ name={`fail switch`}
+ data-enabled={!!options.endOnFirstFail}
+ 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={() => {
+ setOptions({
+ ...options,
+ endOnFirstFail: !options.endOnFirstFail,
+ });
+ }}
+ >
+ <span
+ aria-hidden="true"
+ data-enabled={options.endOnFirstFail}
+ 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>
+ </div>
+ </div>
+ </div>
+
+ <div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
+ <a
+ name="cancel"
+ href={routeCancel.url({})}
+ class="text-sm font-semibold leading-6 text-gray-900"
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </a>
+ <button
+ type="submit"
+ name="download"
+ class="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"
+ disabled={lastStep !== undefined}
+ onClick={async () => {
+ setDownloaded(undefined);
+ await handleError(async () => {
+ const csv = await fetchAllStatus(
+ api,
+ creds.token,
+ options,
+ referenceDates,
+ (step, total) => {
+ setLastStep({ step, total });
+ },
+ );
+ setDownloaded(csv);
+ });
+ setLastStep(undefined);
+ }}
+ >
+ <i18n.Translate>Download</i18n.Translate>
+ </button>
+ </div>
+ </form>
+ </div>
+ {!lastStep || lastStep.step === lastStep.total ? (
+ <div class="h-5 mb-5" />
+ ) : (
+ <div>
+ <div class="relative mb-5 h-5 rounded-full bg-gray-200">
+ <div
+ class="h-full animate-pulse rounded-full bg-blue-500"
+ style={{
+ width: `${Math.round((lastStep.step / lastStep.total) * 100)}%`,
+ }}
+ >
+ <span class="absolute inset-0 flex items-center justify-center text-xs font-semibold text-white">
+ <i18n.Translate>
+ downloading...{" "}
+ {Math.round((lastStep.step / lastStep.total) * 100)}
+ </i18n.Translate>
+ </span>
+ </div>
+ </div>
+ </div>
+ )}
+ {!downloaded ? (
+ <div class="h-5 mb-5" />
+ ) : (
+ <a
+ href={
+ "data:text/plain;charset=utf-8," + encodeURIComponent(downloaded)
+ }
+ name="save file"
+ download={"bank-stats.csv"}
+ >
+ <Attention title={i18n.str`Download completed`}>
+ <i18n.Translate>
+ Click here to save the file in your computer.
+ </i18n.Translate>
+ </Attention>
+ </a>
+ )}
+ </div>
+ );
+}
+
+async function fetchAllStatus(
+ api: TalerCoreBankHttpClient,
+ token: AccessToken,
+ options: Options,
+ references: Date[],
+ progress: (current: number, total: number) => void,
+): Promise<string> {
+ const allMetrics: TalerCorebankApi.MonitorTimeframeParam[] = [];
+ if (options.hourMetric) {
+ allMetrics.push(TalerCorebankApi.MonitorTimeframeParam.hour);
+ }
+ if (options.dayMetric) {
+ allMetrics.push(TalerCorebankApi.MonitorTimeframeParam.day);
+ }
+ if (options.monthMetric) {
+ allMetrics.push(TalerCorebankApi.MonitorTimeframeParam.month);
+ }
+ if (options.yearMetric) {
+ allMetrics.push(TalerCorebankApi.MonitorTimeframeParam.year);
+ }
+
+ /**
+ * convert request into frames
+ */
+ const allFrames = allMetrics.flatMap((timeframe) =>
+ references.map((reference) => ({
+ reference,
+ timeframe,
+ moment: getTimeframesForDate(reference, timeframe),
+ })),
+ );
+ const total = allFrames.length;
+
+ /**
+ * call API for info
+ */
+ const allInfo = await allFrames.reduce(
+ async (prev, frame, index) => {
+ const accumulatedMap = await prev;
+ progress(index, total);
+ // await delay()
+ const previous = options.compareWithPrevious
+ ? await api.getMonitor(token, {
+ timeframe: frame.timeframe,
+ date: frame.moment.previous,
+ })
+ : undefined;
+
+ if (previous && previous.type === "fail" && options.endOnFirstFail) {
+ throw TalerError.fromUncheckedDetail(previous.detail);
+ }
+
+ const current = await api.getMonitor(token, {
+ timeframe: frame.timeframe,
+ date: frame.moment.current,
+ });
+
+ if (current.type === "fail" && options.endOnFirstFail) {
+ throw TalerError.fromUncheckedDetail(current.detail);
+ }
+
+ const metricName =
+ TalerCorebankApi.MonitorTimeframeParam[allMetrics[index]];
+ accumulatedMap[metricName] = {
+ reference: frame.reference,
+ current: current.type !== "ok" ? undefined : current.body,
+ previous:
+ !previous || previous.type !== "ok" ? undefined : previous.body,
+ };
+ return accumulatedMap;
+ },
+ Promise.resolve({} as Record<string, Data>),
+ );
+ progress(total, total);
+
+ /**
+ * convert into table format
+ *
+ */
+ const table: Array<string[]> = [];
+ if (options.includeHeader) {
+ table.push([
+ "date",
+ "metric",
+ "reference",
+ "talerInCount",
+ "talerInVolume",
+ "talerOutCount",
+ "talerOutVolume",
+ "cashinCount",
+ "cashinFiatVolume",
+ "cashinRegionalVolume",
+ "cashoutCount",
+ "cashoutFiatVolume",
+ "cashoutRegionalVolume",
+ ]);
+ }
+ Object.entries(allInfo).forEach(([name, data]) => {
+ if (data.current) {
+ const row: TableRow = {
+ date: data.reference.getTime(),
+ metric: name,
+ reference: "current",
+ ...dataToRow(data.current),
+ };
+ table.push(Object.values(row) as string[]);
+ }
+
+ if (data.previous) {
+ const row: TableRow = {
+ date: data.reference.getTime(),
+ metric: name,
+ reference: "previous",
+ ...dataToRow(data.previous),
+ };
+ table.push(Object.values(row) as string[]);
+ }
+ });
+
+ const csv = table.reduce((acc, row) => {
+ return acc + row.join(",") + "\n";
+ }, "");
+
+ return csv;
+}
+
+type JustData = Omit<Omit<Omit<TableRow, "metric">, "date">, "reference">;
+function dataToRow(info: TalerCorebankApi.MonitorResponse): JustData {
+ return {
+ talerInCount: info.talerInCount,
+ talerInVolume: info.talerInVolume,
+ talerOutCount: info.talerOutCount,
+ talerOutVolume: info.talerOutVolume,
+ cashinCount: info.type === "no-conversions" ? undefined : info.cashinCount,
+ cashinFiatVolume:
+ info.type === "no-conversions" ? undefined : info.cashinFiatVolume,
+ cashinRegionalVolume:
+ info.type === "no-conversions" ? undefined : info.cashinRegionalVolume,
+ cashoutCount:
+ info.type === "no-conversions" ? undefined : info.cashoutCount,
+ cashoutFiatVolume:
+ info.type === "no-conversions" ? undefined : info.cashoutFiatVolume,
+ cashoutRegionalVolume:
+ info.type === "no-conversions" ? undefined : info.cashoutRegionalVolume,
+ };
+}
+
+type Data = {
+ reference: Date;
+ previous: TalerCorebankApi.MonitorResponse | undefined;
+ current: TalerCorebankApi.MonitorResponse | undefined;
+};
+type TableRow = {
+ date: number;
+ metric: string;
+ reference: "current" | "previous";
+ cashinCount?: number;
+ cashinRegionalVolume?: AmountString;
+ cashinFiatVolume?: AmountString;
+ cashoutCount?: number;
+ cashoutRegionalVolume?: AmountString;
+ cashoutFiatVolume?: AmountString;
+ talerInCount: number;
+ talerInVolume: AmountString;
+ talerOutCount: number;
+ talerOutVolume: AmountString;
+};
diff --git a/packages/bank-ui/src/pages/admin/RemoveAccount.tsx b/packages/bank-ui/src/pages/admin/RemoveAccount.tsx
new file mode 100644
index 000000000..dbeebf719
--- /dev/null
+++ b/packages/bank-ui/src/pages/admin/RemoveAccount.tsx
@@ -0,0 +1,273 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ AbsoluteTime,
+ Amounts,
+ HttpStatusCode,
+ TalerError,
+ TalerErrorCode,
+ TranslatedString,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
+import {
+ Attention,
+ Loading,
+ LocalNotificationBanner,
+ ShowInputErrorLabel,
+ notifyInfo,
+ useLocalNotification,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js";
+import { useBankCoreApiContext } from "@gnu-taler/web-util/browser";
+import { useAccountDetails } from "../../hooks/account.js";
+import { useSessionState } from "../../hooks/session.js";
+import { undefinedIfEmpty } from "../../utils.js";
+import { LoginForm } from "../LoginForm.js";
+import { doAutoFocus } from "../PaytoWireTransferForm.js";
+import { useBankState } from "../../hooks/bank-state.js";
+import { RouteDefinition } from "@gnu-taler/web-util/browser";
+
+export function RemoveAccount({
+ account,
+ routeCancel,
+ onUpdateSuccess,
+ onAuthorizationRequired,
+ focus,
+ routeHere,
+}: {
+ focus?: boolean;
+ routeHere: RouteDefinition<{ account: string }>;
+ onAuthorizationRequired: () => void;
+ routeCancel: RouteDefinition;
+ onUpdateSuccess: () => void;
+ account: string;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const result = useAccountDetails(account);
+ const [accountName, setAccountName] = useState<string | undefined>();
+
+ const { state } = useSessionState();
+ const token = state.status !== "loggedIn" ? undefined : state.token;
+ const {
+ lib: { bank: api },
+ } = useBankCoreApiContext();
+ const [notification, notify, handleError] = useLocalNotification();
+ const [, updateBankState] = useBankState();
+
+ if (!result) {
+ return <Loading />;
+ }
+ if (result instanceof TalerError) {
+ return <ErrorLoadingWithDebug error={result} />;
+ }
+ if (result.type === "fail") {
+ switch (result.case) {
+ case HttpStatusCode.Unauthorized:
+ return <LoginForm currentUser={account} />;
+ case HttpStatusCode.NotFound:
+ return <LoginForm currentUser={account} />;
+ default:
+ assertUnreachable(result);
+ }
+ }
+
+ const balance = Amounts.parse(result.body.balance.amount);
+ if (!balance) {
+ return <div>there was an error reading the balance</div>;
+ }
+ const isBalanceEmpty = Amounts.isZero(balance);
+ if (!isBalanceEmpty) {
+ return (
+ <Fragment>
+ <Attention type="warning" title={i18n.str`Can't delete the account`}>
+ <i18n.Translate>
+ The account can't be delete while still holding some balance. First
+ make sure that the owner make a complete cashout.
+ </i18n.Translate>
+ </Attention>
+ <div class="mt-5 sm:mt-6">
+ <a
+ href={routeCancel.url({})}
+ name="close"
+ 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"
+ >
+ <i18n.Translate>Close</i18n.Translate>
+ </a>
+ </div>
+ </Fragment>
+ );
+ }
+
+ async function doRemove() {
+ if (!token) return;
+ await handleError(async () => {
+ const resp = await api.deleteAccount({ username: account, token });
+ if (resp.type === "ok") {
+ notifyInfo(i18n.str`Account removed`);
+ onUpdateSuccess();
+ } else {
+ switch (resp.case) {
+ case HttpStatusCode.Unauthorized:
+ return notify({
+ type: "error",
+ title: i18n.str`No enough permission to delete the account.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case HttpStatusCode.NotFound:
+ return notify({
+ type: "error",
+ title: i18n.str`The username was not found.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT:
+ return notify({
+ type: "error",
+ title: i18n.str`Can't delete a reserved username.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case TalerErrorCode.BANK_ACCOUNT_BALANCE_NOT_ZERO:
+ return notify({
+ type: "error",
+ title: i18n.str`Can't delete an account with balance different than zero.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case HttpStatusCode.Accepted: {
+ updateBankState("currentChallenge", {
+ operation: "delete-account",
+ id: String(resp.body.challenge_id),
+ sent: AbsoluteTime.never(),
+ location: routeHere.url({ account }),
+ request: account,
+ });
+ return onAuthorizationRequired();
+ }
+ default: {
+ assertUnreachable(resp);
+ }
+ }
+ }
+ });
+ }
+
+ const errors = undefinedIfEmpty({
+ accountName: !accountName
+ ? i18n.str`Required`
+ : account !== accountName
+ ? i18n.str`Name doesn't match`
+ : undefined,
+ });
+
+ return (
+ <div>
+ <LocalNotificationBanner notification={notification} />
+
+ <Attention
+ type="warning"
+ title={i18n.str`You are going to remove the account`}
+ >
+ <i18n.Translate>This step can't be undone.</i18n.Translate>
+ </Attention>
+
+ <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-6 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg">
+ <div class="px-4 sm:px-0">
+ <h2 class="text-base font-semibold leading-7 text-gray-900">
+ <i18n.Translate>Deleting account "{account}"</i18n.Translate>
+ </h2>
+ </div>
+ <form
+ class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2"
+ autoCapitalize="none"
+ autoCorrect="off"
+ onSubmit={(e) => {
+ e.preventDefault();
+ }}
+ >
+ <div class="px-4 py-6 sm:p-8">
+ <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="password"
+ >
+ {i18n.str`Verification`}
+ </label>
+ <div class="mt-2">
+ <input
+ ref={focus ? doAutoFocus : undefined}
+ type="text"
+ class="block w-full 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="password"
+ id="password"
+ data-error={
+ !!errors?.accountName && accountName !== undefined
+ }
+ value={accountName ?? ""}
+ onChange={(e) => {
+ setAccountName(e.currentTarget.value);
+ }}
+ placeholder={account}
+ autocomplete="off"
+ />
+ <ShowInputErrorLabel
+ message={errors?.accountName}
+ isDirty={accountName !== undefined}
+ />
+ </div>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ Enter the account name that is going to be deleted
+ </i18n.Translate>
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
+ <a
+ href={routeCancel.url({})}
+ name="cancel"
+ class="text-sm font-semibold leading-6 text-gray-900"
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </a>
+ <button
+ type="submit"
+ name="delete"
+ class="disabled:opacity-50 disabled:cursor-default cursor-pointer 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-600"
+ disabled={!!errors}
+ onClick={(e) => {
+ e.preventDefault();
+ doRemove();
+ }}
+ >
+ <i18n.Translate>Delete</i18n.Translate>
+ </button>
+ </div>
+ </form>
+ </div>
+ </div>
+ );
+}
diff --git a/packages/bank-ui/src/pages/index.stories.tsx b/packages/bank-ui/src/pages/index.stories.tsx
new file mode 100644
index 000000000..823def5d7
--- /dev/null
+++ b/packages/bank-ui/src/pages/index.stories.tsx
@@ -0,0 +1,20 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+export * as qr from "./QrCodeSection.stories.js";
+export * as po from "./PaymentOptions.stories.js";
+export * as ptf from "./PaytoWireTransferForm.stories.js";
+export * as frame from "./BankFrame.stories.js";
diff --git a/packages/bank-ui/src/pages/regional/ConversionConfig.tsx b/packages/bank-ui/src/pages/regional/ConversionConfig.tsx
new file mode 100644
index 000000000..485ef5490
--- /dev/null
+++ b/packages/bank-ui/src/pages/regional/ConversionConfig.tsx
@@ -0,0 +1,1170 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ AbsoluteTime,
+ AmountJson,
+ Amounts,
+ HttpStatusCode,
+ TalerBankConversionApi,
+ TalerError,
+ TranslatedString,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
+import {
+ Attention,
+ InternationalizationAPI,
+ LocalNotificationBanner,
+ ShowInputErrorLabel,
+ useLocalNotification,
+ useTranslationContext,
+ utils,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useEffect, useState } from "preact/hooks";
+import { useBankCoreApiContext } from "@gnu-taler/web-util/browser";
+import { useSessionState } from "../../hooks/session.js";
+import {
+ TransferCalculation,
+ useCashinEstimator,
+ useCashoutEstimator,
+ useConversionInfo,
+} from "../../hooks/regional.js";
+import { RouteDefinition } from "@gnu-taler/web-util/browser";
+import { undefinedIfEmpty } from "../../utils.js";
+import { InputAmount, RenderAmount } from "../PaytoWireTransferForm.js";
+import { ProfileNavigation } from "../ProfileNavigation.js";
+import {
+ FormErrors,
+ FormStatus,
+ FormValues,
+ RecursivePartial,
+ UIField,
+ useFormState,
+} from "../../hooks/form.js";
+
+interface Props {
+ routeMyAccountDetails: RouteDefinition;
+ routeMyAccountDelete: RouteDefinition;
+ routeMyAccountPassword: RouteDefinition;
+ routeMyAccountCashout: RouteDefinition;
+ routeConversionConfig: RouteDefinition;
+ routeCancel: RouteDefinition;
+ onUpdateSuccess: () => void;
+}
+
+type FormType = {
+ amount: AmountJson;
+ conv: TalerBankConversionApi.ConversionRate;
+};
+
+function useComponentState({
+ routeCancel,
+ routeConversionConfig,
+ routeMyAccountCashout,
+ routeMyAccountDelete,
+ routeMyAccountDetails,
+ routeMyAccountPassword,
+}: Props): utils.RecursiveState<VNode> {
+ const { i18n } = useTranslationContext();
+
+ const result = useConversionInfo();
+ const info =
+ result && !(result instanceof TalerError) && result.type === "ok"
+ ? result.body
+ : undefined;
+
+ const { state: credentials } = useSessionState();
+ const creds =
+ credentials.status !== "loggedIn" || !credentials.isUserAdministrator
+ ? undefined
+ : credentials;
+
+ if (!info) {
+ return <i18n.Translate>loading...</i18n.Translate>;
+ }
+
+ if (!creds) {
+ return <i18n.Translate>only admin can setup conversion</i18n.Translate>;
+ }
+
+ return function afterComponentLoads() {
+ const { i18n } = useTranslationContext();
+
+ const {
+ lib: { conversion },
+ } = useBankCoreApiContext();
+
+ const [notification, notify, handleError] = useLocalNotification();
+
+ const initalState: FormValues<FormType> = {
+ amount: "100",
+ conv: {
+ cashin_min_amount: info.conversion_rate.cashin_min_amount.split(":")[1],
+ cashin_tiny_amount:
+ info.conversion_rate.cashin_tiny_amount.split(":")[1],
+ cashin_fee: info.conversion_rate.cashin_fee.split(":")[1],
+ cashin_ratio: info.conversion_rate.cashin_ratio,
+ cashin_rounding_mode: info.conversion_rate.cashin_rounding_mode,
+ cashout_min_amount:
+ info.conversion_rate.cashout_min_amount.split(":")[1],
+ cashout_tiny_amount:
+ info.conversion_rate.cashout_tiny_amount.split(":")[1],
+ cashout_fee: info.conversion_rate.cashout_fee.split(":")[1],
+ cashout_ratio: info.conversion_rate.cashout_ratio,
+ cashout_rounding_mode: info.conversion_rate.cashout_rounding_mode,
+ },
+ };
+
+ const [form, status] = useFormState<FormType>(
+ initalState,
+ createFormValidator(i18n, info.regional_currency, info.fiat_currency),
+ );
+
+ const { estimateByDebit: calculateCashoutFromDebit } =
+ useCashoutEstimator();
+
+ const { estimateByDebit: calculateCashinFromDebit } = useCashinEstimator();
+
+ const [calculationResult, setCalc] = useState<{
+ cashin: TransferCalculation;
+ cashout: TransferCalculation;
+ }>();
+
+ useEffect(() => {
+ async function doAsync() {
+ await handleError(async () => {
+ if (!info) return;
+ if (!form.amount?.value || form.amount.error) return;
+ const in_amount = Amounts.parseOrThrow(
+ `${info.fiat_currency}:${form.amount.value}`,
+ );
+ const in_fee = Amounts.parseOrThrow(info.conversion_rate.cashin_fee);
+ const cashin = await calculateCashinFromDebit(in_amount, in_fee);
+
+ if (cashin === "amount-is-too-small") {
+ setCalc(undefined);
+ return;
+ }
+ // const out_amount = Amounts.parseOrThrow(`${info.regional_currency}:${form.amount.value}`)
+ const out_fee = Amounts.parseOrThrow(
+ info.conversion_rate.cashout_fee,
+ );
+ const cashout = await calculateCashoutFromDebit(
+ cashin.credit,
+ out_fee,
+ );
+
+ setCalc({ cashin, cashout });
+ });
+ }
+ doAsync();
+ }, [
+ form.amount?.value,
+ form.conv?.cashin_fee?.value,
+ form.conv?.cashout_fee?.value,
+ ]);
+
+ const [section, setSection] = useState<"detail" | "cashout" | "cashin">(
+ "detail",
+ );
+ const cashinCalc =
+ calculationResult?.cashin === "amount-is-too-small"
+ ? undefined
+ : calculationResult?.cashin;
+ const cashoutCalc =
+ calculationResult?.cashout === "amount-is-too-small"
+ ? undefined
+ : calculationResult?.cashout;
+ async function doUpdate() {
+ if (!creds) return;
+ await handleError(async () => {
+ if (status.status === "fail") return;
+ const resp = await conversion.updateConversionRate(
+ creds.token,
+ status.result.conv,
+ );
+ if (resp.type === "ok") {
+ setSection("detail");
+ } else {
+ switch (resp.case) {
+ case HttpStatusCode.Unauthorized: {
+ return notify({
+ type: "error",
+ title: i18n.str`Wrong credentials`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ }
+ case HttpStatusCode.NotImplemented: {
+ return notify({
+ type: "error",
+ title: i18n.str`Conversion is disabled`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ }
+ default:
+ assertUnreachable(resp);
+ }
+ }
+ });
+ }
+
+ const in_ratio = Number.parseFloat(info.conversion_rate.cashin_ratio);
+ const out_ratio = Number.parseFloat(info.conversion_rate.cashout_ratio);
+
+ const both_high = in_ratio > 1 && out_ratio > 1;
+ const both_low = in_ratio < 1 && out_ratio < 1;
+
+ return (
+ <div>
+ <ProfileNavigation
+ current="conversion"
+ routeMyAccountCashout={routeMyAccountCashout}
+ routeMyAccountDelete={routeMyAccountDelete}
+ routeMyAccountDetails={routeMyAccountDetails}
+ routeMyAccountPassword={routeMyAccountPassword}
+ routeConversionConfig={routeConversionConfig}
+ />
+
+ <LocalNotificationBanner notification={notification} />
+ <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-6 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg">
+ <div class="px-4 sm:px-0">
+ <h2 class="text-base font-semibold leading-7 text-gray-900">
+ <i18n.Translate>Conversion</i18n.Translate>
+ </h2>
+ <div class="px-2 mt-2 grid grid-cols-1 gap-y-4 sm:gap-x-4">
+ <label
+ data-enabled={section === "detail"}
+ class="relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none border-gray-300 data-[enabled=true]:border-indigo-600 data-[enabled=true]:ring-2 data-[enabled=true]:ring-indigo-600"
+ >
+ <input
+ type="radio"
+ name="project-type"
+ value="Newsletter"
+ class="sr-only"
+ aria-labelledby="project-type-0-label"
+ aria-describedby="project-type-0-description-0 project-type-0-description-1"
+ onChange={() => {
+ setSection("detail");
+ }}
+ />
+ <span class="flex flex-1">
+ <span class="flex flex-col">
+ <span class="block text-sm font-medium text-gray-900">
+ <i18n.Translate>Details</i18n.Translate>
+ </span>
+ </span>
+ </span>
+ </label>
+
+ <label
+ data-enabled={section === "cashout"}
+ class="relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none border-gray-300 -- data-[enabled=true]:border-indigo-600 data-[enabled=true]:ring-2 data-[enabled=true]:ring-indigo-600"
+ >
+ <input
+ type="radio"
+ name="project-type"
+ value="Existing Customers"
+ class="sr-only"
+ aria-labelledby="project-type-1-label"
+ aria-describedby="project-type-1-description-0 project-type-1-description-1"
+ onChange={() => {
+ setSection("cashout");
+ }}
+ />
+ <span class="flex flex-1">
+ <span class="flex flex-col">
+ <span class="block text-sm font-medium text-gray-900">
+ <i18n.Translate>Config cashout</i18n.Translate>
+ </span>
+ </span>
+ </span>
+ </label>
+ <label
+ data-enabled={section === "cashin"}
+ class="relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none border-gray-300 -- data-[enabled=true]:border-indigo-600 data-[enabled=true]:ring-2 data-[enabled=true]:ring-indigo-600"
+ >
+ <input
+ type="radio"
+ name="project-type"
+ value="Existing Customers"
+ class="sr-only"
+ aria-labelledby="project-type-1-label"
+ aria-describedby="project-type-1-description-0 project-type-1-description-1"
+ onChange={() => {
+ setSection("cashin");
+ }}
+ />
+ <span class="flex flex-1">
+ <span class="flex flex-col">
+ <span class="block text-sm font-medium text-gray-900">
+ <i18n.Translate>Config cashin</i18n.Translate>
+ </span>
+ </span>
+ </span>
+ </label>
+ </div>
+ </div>
+
+ <form
+ class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2"
+ autoCapitalize="none"
+ autoCorrect="off"
+ onSubmit={(e) => {
+ e.preventDefault();
+ }}
+ >
+ {section == "cashin" && (
+ <ConversionForm
+ id="cashin"
+ inputCurrency={info.fiat_currency}
+ outputCurrency={info.regional_currency}
+ fee={form?.conv?.cashin_fee}
+ minimum={form?.conv?.cashin_min_amount}
+ ratio={form?.conv?.cashin_ratio}
+ rounding={form?.conv?.cashin_rounding_mode}
+ tiny={form?.conv?.cashin_tiny_amount}
+ />
+ )}
+
+ {section == "cashout" && (
+ <Fragment>
+ <ConversionForm
+ id="cashout"
+ inputCurrency={info.regional_currency}
+ outputCurrency={info.fiat_currency}
+ fee={form?.conv?.cashout_fee}
+ minimum={form?.conv?.cashout_min_amount}
+ ratio={form?.conv?.cashout_ratio}
+ rounding={form?.conv?.cashout_rounding_mode}
+ tiny={form?.conv?.cashout_tiny_amount}
+ />
+ </Fragment>
+ )}
+
+ {section == "detail" && (
+ <Fragment>
+ <div class="px-6 pt-6">
+ <div class="justify-between items-center flex ">
+ <dt class="text-sm text-gray-600">
+ <i18n.Translate>Cashin ratio</i18n.Translate>
+ </dt>
+ <dd class="text-sm text-gray-900">
+ {info.conversion_rate.cashin_ratio}
+ </dd>
+ </div>
+ </div>
+
+ <div class="px-6 pt-6">
+ <div class="justify-between items-center flex ">
+ <dt class="text-sm text-gray-600">
+ <i18n.Translate>Cashout ratio</i18n.Translate>
+ </dt>
+ <dd class="text-sm text-gray-900">
+ {info.conversion_rate.cashout_ratio}
+ </dd>
+ </div>
+ </div>
+
+ {both_low || both_high ? (
+ <div class="p-4">
+ <Attention title={i18n.str`Bad ratios`} type="warning">
+ <i18n.Translate>
+ One of the ratios should be higher or equal than 1 an
+ the other should be lower or equal than 1.
+ </i18n.Translate>
+ </Attention>
+ </div>
+ ) : undefined}
+
+ <div class="px-6 pt-6">
+ <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
+ <div class="sm:col-span-5">
+ <label
+ for="amount"
+ class="block text-sm font-medium leading-6 text-gray-900"
+ >{i18n.str`Initial amount`}</label>
+ <InputAmount
+ name="amount"
+ left
+ currency={info.fiat_currency}
+ value={form.amount?.value ?? ""}
+ onChange={form.amount?.onUpdate}
+ />
+ <ShowInputErrorLabel
+ message={form.amount?.error}
+ isDirty={form.amount?.value !== undefined}
+ />
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ Use it to test how the conversion will affect the
+ amount.
+ </i18n.Translate>
+ </p>
+ </div>
+ </div>
+ </div>
+
+ {!cashoutCalc || !cashinCalc ? undefined : (
+ <div class="px-6 pt-6">
+ <div class="sm:col-span-5">
+ <dl class="mt-4 space-y-4">
+ <div class="justify-between items-center flex ">
+ <dt class="text-sm text-gray-600">
+ <i18n.Translate>
+ Sending to this bank
+ </i18n.Translate>
+ </dt>
+ <dd class="text-sm text-gray-900">
+ <RenderAmount
+ value={cashinCalc.debit}
+ negative
+ withColor
+ spec={info.regional_currency_specification}
+ />
+ </dd>
+ </div>
+
+ {Amounts.isZero(cashinCalc.beforeFee) ? undefined : (
+ <div class="flex items-center justify-between afu ">
+ <dt class="flex items-center text-sm text-gray-600">
+ <span>
+ <i18n.Translate>Converted</i18n.Translate>
+ </span>
+ </dt>
+ <dd class="text-sm text-gray-900">
+ <RenderAmount
+ value={cashinCalc.beforeFee}
+ spec={info.fiat_currency_specification}
+ />
+ </dd>
+ </div>
+ )}
+ <div class="flex justify-between items-center border-t-2 afu pt-4">
+ <dt class="text-lg text-gray-900 font-medium">
+ <i18n.Translate>Cashin after fee</i18n.Translate>
+ </dt>
+ <dd class="text-lg text-gray-900 font-medium">
+ <RenderAmount
+ value={cashinCalc.credit}
+ withColor
+ spec={info.fiat_currency_specification}
+ />
+ </dd>
+ </div>
+ </dl>
+ </div>
+
+ <div class="sm:col-span-5">
+ <dl class="mt-4 space-y-4">
+ <div class="justify-between items-center flex ">
+ <dt class="text-sm text-gray-600">
+ <i18n.Translate>
+ Sending from this bank
+ </i18n.Translate>
+ </dt>
+ <dd class="text-sm text-gray-900">
+ <RenderAmount
+ value={cashoutCalc.debit}
+ negative
+ withColor
+ spec={info.fiat_currency_specification}
+ />
+ </dd>
+ </div>
+
+ {Amounts.isZero(cashoutCalc.beforeFee) ? undefined : (
+ <div class="flex items-center justify-between afu">
+ <dt class="flex items-center text-sm text-gray-600">
+ <span>
+ <i18n.Translate>Converted</i18n.Translate>
+ </span>
+ </dt>
+ <dd class="text-sm text-gray-900">
+ <RenderAmount
+ value={cashoutCalc.beforeFee}
+ spec={info.regional_currency_specification}
+ />
+ </dd>
+ </div>
+ )}
+ <div class="flex justify-between items-center border-t-2 afu pt-4">
+ <dt class="text-lg text-gray-900 font-medium">
+ <i18n.Translate>Cashout after fee</i18n.Translate>
+ </dt>
+ <dd class="text-lg text-gray-900 font-medium">
+ <RenderAmount
+ value={cashoutCalc.credit}
+ withColor
+ spec={info.regional_currency_specification}
+ />
+ </dd>
+ </div>
+ </dl>
+ </div>
+
+ {cashoutCalc &&
+ status.status === "ok" &&
+ Amounts.cmp(status.result.amount, cashoutCalc.credit) <
+ 0 ? (
+ <div class="p-4">
+ <Attention
+ title={i18n.str`Bad configuration`}
+ type="warning"
+ >
+ <i18n.Translate>
+ This configuration allows users to cash out more of
+ what has been cashed in.
+ </i18n.Translate>
+ </Attention>
+ </div>
+ ) : undefined}
+ </div>
+ )}
+ </Fragment>
+ )}
+
+ <div class="flex items-center justify-between mt-4 gap-x-6 border-t border-gray-900/10 px-4 py-4">
+ <a
+ name="cancel"
+ href={routeCancel.url({})}
+ class="text-sm font-semibold leading-6 text-gray-900"
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </a>
+ {section == "cashin" || section == "cashout" ? (
+ <Fragment>
+ <button
+ type="submit"
+ name="update conversion"
+ class="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"
+ onClick={async () => {
+ doUpdate();
+ }}
+ >
+ <i18n.Translate>Update</i18n.Translate>
+ </button>
+ </Fragment>
+ ) : (
+ <div />
+ )}
+ </div>
+ </form>
+ </div>
+ </div>
+ );
+ };
+}
+
+export const ConversionConfig = utils.recursive(useComponentState);
+
+/**
+ *
+ * @param i18n
+ * @param regional
+ * @param fiat
+ * @returns form validator
+ */
+function createFormValidator(
+ i18n: InternationalizationAPI,
+ regional: string,
+ fiat: string,
+) {
+ return function check(state: FormValues<FormType>): FormStatus<FormType> {
+ const cashin_min_amount = Amounts.parse(
+ `${fiat}:${state.conv.cashin_min_amount}`,
+ );
+ const cashin_tiny_amount = Amounts.parse(
+ `${regional}:${state.conv.cashin_tiny_amount}`,
+ );
+ const cashin_fee = Amounts.parse(`${regional}:${state.conv.cashin_fee}`);
+
+ const cashout_min_amount = Amounts.parse(
+ `${regional}:${state.conv.cashout_min_amount}`,
+ );
+ const cashout_tiny_amount = Amounts.parse(
+ `${fiat}:${state.conv.cashout_tiny_amount}`,
+ );
+ const cashout_fee = Amounts.parse(`${fiat}:${state.conv.cashout_fee}`);
+
+ const am = Amounts.parse(`${fiat}:${state.amount}`);
+
+ const cashin_ratio = Number.parseFloat(state.conv.cashin_ratio ?? "");
+ const cashout_ratio = Number.parseFloat(state.conv.cashout_ratio ?? "");
+
+ const errors = undefinedIfEmpty<FormErrors<FormType>>({
+ conv: undefinedIfEmpty<FormErrors<FormType["conv"]>>({
+ cashin_min_amount: !state.conv.cashin_min_amount
+ ? i18n.str`required`
+ : !cashin_min_amount
+ ? i18n.str`invalid`
+ : undefined,
+ cashin_tiny_amount: !state.conv.cashin_tiny_amount
+ ? i18n.str`required`
+ : !cashin_tiny_amount
+ ? i18n.str`invalid`
+ : undefined,
+ cashin_fee: !state.conv.cashin_fee
+ ? i18n.str`required`
+ : !cashin_fee
+ ? i18n.str`invalid`
+ : undefined,
+
+ cashout_min_amount: !state.conv.cashout_min_amount
+ ? i18n.str`required`
+ : !cashout_min_amount
+ ? i18n.str`invalid`
+ : undefined,
+ cashout_tiny_amount: !state.conv.cashin_tiny_amount
+ ? i18n.str`required`
+ : !cashout_tiny_amount
+ ? i18n.str`invalid`
+ : undefined,
+ cashout_fee: !state.conv.cashin_fee
+ ? i18n.str`required`
+ : !cashout_fee
+ ? i18n.str`invalid`
+ : undefined,
+
+ cashin_rounding_mode: !state.conv.cashin_rounding_mode
+ ? i18n.str`required`
+ : undefined,
+ cashout_rounding_mode: !state.conv.cashout_rounding_mode
+ ? i18n.str`required`
+ : undefined,
+
+ cashin_ratio: !state.conv.cashin_ratio
+ ? i18n.str`required`
+ : Number.isNaN(cashin_ratio)
+ ? i18n.str`invalid`
+ : undefined,
+ cashout_ratio: !state.conv.cashout_ratio
+ ? i18n.str`required`
+ : Number.isNaN(cashout_ratio)
+ ? i18n.str`invalid`
+ : undefined,
+ }),
+
+ amount: !state.amount
+ ? i18n.str`required`
+ : !am
+ ? i18n.str`invalid`
+ : undefined,
+ });
+
+ const result: RecursivePartial<FormType> = {
+ amount: am,
+ conv: {
+ cashin_fee: !errors?.conv?.cashin_fee
+ ? Amounts.stringify(cashin_fee!)
+ : undefined,
+ cashin_min_amount: !errors?.conv?.cashin_min_amount
+ ? Amounts.stringify(cashin_min_amount!)
+ : undefined,
+ cashin_ratio: !errors?.conv?.cashin_ratio
+ ? String(cashin_ratio!)
+ : undefined,
+ cashin_rounding_mode: !errors?.conv?.cashin_rounding_mode
+ ? state.conv.cashin_rounding_mode!
+ : undefined,
+ cashin_tiny_amount: !errors?.conv?.cashin_tiny_amount
+ ? Amounts.stringify(cashin_tiny_amount!)
+ : undefined,
+ cashout_fee: !errors?.conv?.cashout_fee
+ ? Amounts.stringify(cashout_fee!)
+ : undefined,
+ cashout_min_amount: !errors?.conv?.cashout_min_amount
+ ? Amounts.stringify(cashout_min_amount!)
+ : undefined,
+ cashout_ratio: !errors?.conv?.cashout_ratio
+ ? String(cashout_ratio!)
+ : undefined,
+ cashout_rounding_mode: !errors?.conv?.cashout_rounding_mode
+ ? state.conv.cashout_rounding_mode!
+ : undefined,
+ cashout_tiny_amount: !errors?.conv?.cashout_tiny_amount
+ ? Amounts.stringify(cashout_tiny_amount!)
+ : undefined,
+ },
+ };
+ return errors === undefined
+ ? { status: "ok", result: result as FormType, errors }
+ : { status: "fail", result, errors };
+ };
+}
+
+function ConversionForm({
+ id,
+ inputCurrency,
+ outputCurrency,
+ fee,
+ minimum,
+ ratio,
+ rounding,
+ tiny,
+}: {
+ inputCurrency: string;
+ outputCurrency: string;
+ minimum: UIField | undefined;
+ tiny: UIField | undefined;
+ fee: UIField | undefined;
+ rounding: UIField | undefined;
+ ratio: UIField | undefined;
+ id: string;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <Fragment>
+ <div class="px-6 pt-6">
+ <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
+ <div class="sm:col-span-5">
+ <label
+ for={`${id}_min_amount`}
+ class="block text-sm font-medium leading-6 text-gray-900"
+ >{i18n.str`Minimum amount`}</label>
+ <InputAmount
+ name={`${id}_min_amount`}
+ left
+ currency={inputCurrency}
+ value={minimum?.value ?? ""}
+ onChange={minimum?.onUpdate}
+ />
+ <ShowInputErrorLabel
+ message={minimum?.error}
+ isDirty={minimum?.value !== undefined}
+ />
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ Only cashout operation above this threshold will be allowed
+ </i18n.Translate>
+ </p>
+ </div>
+ </div>
+ </div>
+
+ <div class="px-6 pt-6">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for={`${id}_ratio`}
+ >
+ {i18n.str`Ratio`}
+ </label>
+ <div class="mt-2">
+ <input
+ type="number"
+ class="block 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="current"
+ id={`${id}_ratio`}
+ data-error={!!ratio?.error && ratio?.value !== undefined}
+ value={ratio?.value ?? ""}
+ onChange={(e) => {
+ ratio?.onUpdate(e.currentTarget.value);
+ }}
+ autocomplete="off"
+ />
+ <ShowInputErrorLabel
+ message={ratio?.error}
+ isDirty={ratio?.value !== undefined}
+ />
+ </div>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>Conversion ratio between currencies</i18n.Translate>
+ </p>
+ </div>
+
+ <div class="px-6 pt-4">
+ <Attention title={i18n.str`Example conversion`}>
+ <i18n.Translate>
+ 1 {inputCurrency} will be converted into {ratio?.value}{" "}
+ {outputCurrency}
+ </i18n.Translate>
+ </Attention>
+ </div>
+
+ <div class="px-6 pt-6">
+ <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
+ <div class="sm:col-span-5">
+ <label
+ for={`${id}_tiny_amount`}
+ class="block text-sm font-medium leading-6 text-gray-900"
+ >{i18n.str`Rounding value`}</label>
+ <InputAmount
+ name={`${id}_tiny_amount`}
+ left
+ currency={outputCurrency}
+ value={tiny?.value ?? ""}
+ onChange={tiny?.onUpdate}
+ />
+ <ShowInputErrorLabel
+ message={tiny?.error}
+ isDirty={tiny?.value !== undefined}
+ />
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ Smallest difference between two amounts after the ratio is
+ applied.
+ </i18n.Translate>
+ </p>
+ </div>
+ </div>
+ </div>
+
+ <div class="px-6 pt-6">
+ <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for={`${id}_channel`}
+ >
+ {i18n.str`Rounding mode`}
+ </label>
+ <div class="mt-2 max-w-xl text-sm text-gray-500">
+ <div class="px-4 mt-4 grid grid-cols-1 gap-y-6">
+ <label
+ onClick={(e) => {
+ e.preventDefault();
+ rounding?.onUpdate("zero");
+ }}
+ data-selected={rounding?.value === "zero"}
+ class="relative flex data-[disabled=false]:cursor-pointer rounded-lg border bg-white data-[disabled=true]:bg-gray-200 p-4 shadow-sm focus:outline-none border-gray-300 data-[selected=true]:ring-2 data-[selected=true]:ring-indigo-600"
+ >
+ <input
+ type="radio"
+ name="channel"
+ value="Newsletter"
+ class="sr-only"
+ />
+ <span class="flex flex-1">
+ <span class="flex flex-col">
+ <span class="block text-sm font-medium text-gray-900 ">
+ <i18n.Translate>Zero</i18n.Translate>
+ </span>
+ <i18n.Translate>
+ Amount will be round below to the largest possible value
+ smaller than the input.
+ </i18n.Translate>
+ </span>
+ </span>
+ <svg
+ data-selected={rounding?.value === "zero"}
+ class="h-5 w-5 text-indigo-600 data-[selected=false]:hidden"
+ viewBox="0 0 20 20"
+ fill="currentColor"
+ aria-hidden="true"
+ >
+ <path
+ fill-rule="evenodd"
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
+ clip-rule="evenodd"
+ />
+ </svg>
+ </label>
+
+ <label
+ onClick={(e) => {
+ e.preventDefault();
+ rounding?.onUpdate("up");
+ }}
+ data-selected={rounding?.value === "up"}
+ class="relative flex data-[disabled=false]:cursor-pointer rounded-lg border data-[disabled=true]:bg-gray-200 p-4 shadow-sm focus:outline-none border-gray-300 data-[selected=true]:ring-2 data-[selected=true]:ring-indigo-600"
+ >
+ <input
+ type="radio"
+ name="channel"
+ value="Existing Customers"
+ class="sr-only"
+ />
+ <span class="flex flex-1">
+ <span class="flex flex-col">
+ <span class="block text-sm font-medium text-gray-900 ">
+ <i18n.Translate>Up</i18n.Translate>
+ </span>
+ <i18n.Translate>
+ Amount will be round up to the smallest possible value
+ larger than the input.
+ </i18n.Translate>
+ </span>
+ </span>
+ <svg
+ data-selected={rounding?.value === "up"}
+ class="h-5 w-5 text-indigo-600 data-[selected=false]:hidden"
+ viewBox="0 0 20 20"
+ fill="currentColor"
+ aria-hidden="true"
+ >
+ <path
+ fill-rule="evenodd"
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
+ clip-rule="evenodd"
+ />
+ </svg>
+ </label>
+ <label
+ onClick={(e) => {
+ e.preventDefault();
+ rounding?.onUpdate("nearest");
+ }}
+ data-selected={rounding?.value === "nearest"}
+ class="relative flex data-[disabled=false]:cursor-pointer rounded-lg border data-[disabled=true]:bg-gray-200 p-4 shadow-sm focus:outline-none border-gray-300 data-[selected=true]:ring-2 data-[selected=true]:ring-indigo-600"
+ >
+ <input
+ type="radio"
+ name="channel"
+ value="Existing Customers"
+ class="sr-only"
+ />
+ <span class="flex flex-1">
+ <span class="flex flex-col">
+ <span class="block text-sm font-medium text-gray-900 ">
+ <i18n.Translate>Nearest</i18n.Translate>
+ </span>
+ <i18n.Translate>
+ Amount will be round to the closest possible value.
+ </i18n.Translate>
+ </span>
+ </span>
+ <svg
+ data-selected={rounding?.value === "nearest"}
+ class="h-5 w-5 text-indigo-600 data-[selected=false]:hidden"
+ viewBox="0 0 20 20"
+ fill="currentColor"
+ aria-hidden="true"
+ >
+ <path
+ fill-rule="evenodd"
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
+ clip-rule="evenodd"
+ />
+ </svg>
+ </label>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="px-6 pt-4">
+ <Attention title={i18n.str`Examples`}>
+ <section class="grid grid-cols-1 gap-y-3 text-gray-600">
+ <details class="group text-sm">
+ <summary class="flex cursor-pointer flex-row items-center justify-between ">
+ <i18n.Translate>
+ Rounding an amount of 1.24 with rounding value 0.1
+ </i18n.Translate>
+ <svg
+ class="h-6 w-6 rotate-0 transform group-open:rotate-180"
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="2"
+ stroke="currentColor"
+ aria-hidden="true"
+ >
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="M19 9l-7 7-7-7"
+ ></path>
+ </svg>
+ </summary>
+ <p class="text-gray-900 my-4">
+ <i18n.Translate>
+ Given the rounding value of 0.1 the possible values closest to
+ 1.24 are: 1.1, 1.2, 1.3, 1.4.
+ </i18n.Translate>
+ </p>
+ <p class="text-gray-900 my-4">
+ <i18n.Translate>
+ With the "zero" mode the value will be rounded to 1.2
+ </i18n.Translate>
+ </p>
+ <p class="text-gray-900 my-4">
+ <i18n.Translate>
+ With the "nearest" mode the value will be rounded to 1.2
+ </i18n.Translate>
+ </p>
+ <p class="text-gray-900 mt-4">
+ <i18n.Translate>
+ With the "up" mode the value will be rounded to 1.3
+ </i18n.Translate>
+ </p>
+ </details>
+ <details class="group ">
+ <summary class="flex cursor-pointer flex-row items-center justify-between ">
+ <i18n.Translate>
+ Rounding an amount of 1.26 with rounding value 0.1
+ </i18n.Translate>
+ <svg
+ class="h-6 w-6 rotate-0 transform group-open:rotate-180"
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="2"
+ stroke="currentColor"
+ aria-hidden="true"
+ >
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="M19 9l-7 7-7-7"
+ ></path>
+ </svg>
+ </summary>
+ <p class="text-gray-900 my-4">
+ <i18n.Translate>
+ Given the rounding value of 0.1 the possible values closest to
+ 1.24 are: 1.1, 1.2, 1.3, 1.4.
+ </i18n.Translate>
+ </p>
+ <p class="text-gray-900 my-4">
+ <i18n.Translate>
+ With the "zero" mode the value will be rounded to 1.2
+ </i18n.Translate>
+ </p>
+ <p class="text-gray-900 my-4">
+ <i18n.Translate>
+ With the "nearest" mode the value will be rounded to 1.3
+ </i18n.Translate>
+ </p>
+ <p class="text-gray-900 my-4">
+ <i18n.Translate>
+ With the "up" mode the value will be rounded to 1.3
+ </i18n.Translate>
+ </p>
+ </details>
+ <details class="group ">
+ <summary class="flex cursor-pointer flex-row items-center justify-between ">
+ <i18n.Translate>
+ Rounding an amount of 1.24 with rounding value 0.3
+ </i18n.Translate>
+ <svg
+ class="h-6 w-6 rotate-0 transform group-open:rotate-180"
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="2"
+ stroke="currentColor"
+ aria-hidden="true"
+ >
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="M19 9l-7 7-7-7"
+ ></path>
+ </svg>
+ </summary>
+ <p class="text-gray-900 my-4">
+ <i18n.Translate>
+ Given the rounding value of 0.3 the possible values closest to
+ 1.24 are: 0.9, 1.2, 1.5, 1.8.
+ </i18n.Translate>
+ </p>
+ <p class="text-gray-900 my-4">
+ <i18n.Translate>
+ With the "zero" mode the value will be rounded to 1.2
+ </i18n.Translate>
+ </p>
+ <p class="text-gray-900 my-4">
+ <i18n.Translate>
+ With the "nearest" mode the value will be rounded to 1.2
+ </i18n.Translate>
+ </p>
+ <p class="text-gray-900 my-4">
+ <i18n.Translate>
+ With the "up" mode the value will be rounded to 1.5
+ </i18n.Translate>
+ </p>
+ </details>
+ <details class="group ">
+ <summary class="flex cursor-pointer flex-row items-center justify-between ">
+ <i18n.Translate>
+ Rounding an amount of 1.26 with rounding value 0.3
+ </i18n.Translate>
+ <svg
+ class="h-6 w-6 rotate-0 transform group-open:rotate-180"
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="2"
+ stroke="currentColor"
+ aria-hidden="true"
+ >
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="M19 9l-7 7-7-7"
+ ></path>
+ </svg>
+ </summary>
+ <p class="text-gray-900 my-4">
+ <i18n.Translate>
+ Given the rounding value of 0.3 the possible values closest to
+ 1.24 are: 0.9, 1.2, 1.5, 1.8.
+ </i18n.Translate>
+ </p>
+ <p class="text-gray-900 my-4">
+ <i18n.Translate>
+ With the "zero" mode the value will be rounded to 1.2
+ </i18n.Translate>
+ </p>
+ <p class="text-gray-900 my-4">
+ <i18n.Translate>
+ With the "nearest" mode the value will be rounded to 1.3
+ </i18n.Translate>
+ </p>
+ <p class="text-gray-900 my-4">
+ <i18n.Translate>
+ With the "up" mode the value will be rounded to 1.3
+ </i18n.Translate>
+ </p>
+ </details>
+ </section>
+ </Attention>
+ </div>
+
+ <div class="px-6 pt-6">
+ <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
+ <div class="sm:col-span-5">
+ <label
+ for={`${id}_fee`}
+ class="block text-sm font-medium leading-6 text-gray-900"
+ >{i18n.str`Fee`}</label>
+ <InputAmount
+ name={`${id}_fee`}
+ left
+ currency={outputCurrency}
+ value={fee?.value ?? ""}
+ onChange={fee?.onUpdate}
+ />
+ <ShowInputErrorLabel
+ message={fee?.error}
+ isDirty={fee?.value !== undefined}
+ />
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ Amount to be deducted before amount is credited.
+ </i18n.Translate>
+ </p>
+ </div>
+ </div>
+ </div>
+ </Fragment>
+ );
+}
diff --git a/packages/bank-ui/src/pages/regional/CreateCashout.tsx b/packages/bank-ui/src/pages/regional/CreateCashout.tsx
new file mode 100644
index 000000000..8e54bbd4e
--- /dev/null
+++ b/packages/bank-ui/src/pages/regional/CreateCashout.tsx
@@ -0,0 +1,717 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ AbsoluteTime,
+ Amounts,
+ HttpStatusCode,
+ TalerError,
+ TalerErrorCode,
+ TranslatedString,
+ assertUnreachable,
+ encodeCrock,
+ getRandomBytes,
+ parsePaytoUri,
+} from "@gnu-taler/taler-util";
+import {
+ Attention,
+ Loading,
+ LocalNotificationBanner,
+ ShowInputErrorLabel,
+ notifyInfo,
+ useLocalNotification,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useEffect, useState } from "preact/hooks";
+import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js";
+import { useBankCoreApiContext } from "@gnu-taler/web-util/browser";
+import { useAccountDetails } from "../../hooks/account.js";
+import { useBankState } from "../../hooks/bank-state.js";
+import {
+ TransferCalculation,
+ useCashoutEstimator,
+ useConversionInfo,
+} from "../../hooks/regional.js";
+import { useSessionState } from "../../hooks/session.js";
+import { RouteDefinition } from "@gnu-taler/web-util/browser";
+import { TanChannel, undefinedIfEmpty } from "../../utils.js";
+import { LoginForm } from "../LoginForm.js";
+import {
+ InputAmount,
+ RenderAmount,
+ doAutoFocus,
+} from "../PaytoWireTransferForm.js";
+
+interface Props {
+ account: string;
+ focus?: boolean;
+ onAuthorizationRequired: () => void;
+ routeClose: RouteDefinition;
+ routeHere: RouteDefinition;
+}
+
+type FormType = {
+ isDebit: boolean;
+ amount: string;
+ subject: string;
+ channel: TanChannel;
+};
+type ErrorFrom<T> = {
+ [P in keyof T]+?: string;
+};
+
+export function CreateCashout({
+ account: accountName,
+ onAuthorizationRequired,
+ focus,
+ routeHere,
+ routeClose,
+}: Props): VNode {
+ const { i18n } = useTranslationContext();
+ const resultAccount = useAccountDetails(accountName);
+ const {
+ estimateByCredit: calculateFromCredit,
+ estimateByDebit: calculateFromDebit,
+ } = useCashoutEstimator();
+ const { state: credentials } = useSessionState();
+ const creds = credentials.status !== "loggedIn" ? undefined : credentials;
+ const [, updateBankState] = useBankState();
+
+ const {
+ lib: { bank: api },
+ config,
+ hints,
+ } = useBankCoreApiContext();
+ const [form, setForm] = useState<Partial<FormType>>({ isDebit: true });
+ const [notification, notify, handleError] = useLocalNotification();
+ const info = useConversionInfo();
+
+ if (!config.allow_conversion) {
+ return (
+ <Fragment>
+ <Attention type="warning" title={i18n.str`Unable to create a cashout`}>
+ <i18n.Translate>
+ The bank configuration does not support cashout operations.
+ </i18n.Translate>
+ </Attention>
+ <div class="mt-5 sm:mt-6">
+ <a
+ href={routeClose.url({})}
+ name="close"
+ 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"
+ >
+ <i18n.Translate>Close</i18n.Translate>
+ </a>
+ </div>
+ </Fragment>
+ );
+ }
+
+ if (!resultAccount) {
+ return <Loading />;
+ }
+ if (resultAccount instanceof TalerError) {
+ return <ErrorLoadingWithDebug error={resultAccount} />;
+ }
+ if (resultAccount.type === "fail") {
+ switch (resultAccount.case) {
+ case HttpStatusCode.Unauthorized:
+ return <LoginForm currentUser={accountName} />;
+ case HttpStatusCode.NotFound:
+ return <LoginForm currentUser={accountName} />;
+ default:
+ assertUnreachable(resultAccount);
+ }
+ }
+ if (!info) {
+ return <Loading />;
+ }
+
+ if (info instanceof TalerError) {
+ return <ErrorLoadingWithDebug error={info} />;
+ }
+ if (info.type === "fail") {
+ switch (info.case) {
+ case HttpStatusCode.NotImplemented: {
+ return (
+ <Attention type="danger" title={i18n.str`Cashout are disabled`}>
+ <i18n.Translate>
+ Cashout should be enable by configuration and the conversion rate
+ should be initialized with fee, ratio and rounding mode.
+ </i18n.Translate>
+ </Attention>
+ );
+ }
+ default:
+ assertUnreachable(info.case);
+ }
+ }
+
+ const conversionInfo = info.body.conversion_rate;
+ if (!conversionInfo) {
+ return (
+ <div>conversion enabled but server replied without conversion_rate</div>
+ );
+ }
+
+ const account = {
+ balance: Amounts.parseOrThrow(resultAccount.body.balance.amount),
+ balanceIsDebit:
+ resultAccount.body.balance.credit_debit_indicator == "debit",
+ debitThreshold: Amounts.parseOrThrow(resultAccount.body.debit_threshold),
+ };
+
+ const {
+ fiat_currency,
+ regional_currency,
+ fiat_currency_specification,
+ regional_currency_specification,
+ } = info.body;
+ const regionalZero = Amounts.zeroOfCurrency(regional_currency);
+ const fiatZero = Amounts.zeroOfCurrency(fiat_currency);
+ const limit = account.balanceIsDebit
+ ? Amounts.sub(account.debitThreshold, account.balance).amount
+ : Amounts.add(account.balance, account.debitThreshold).amount;
+
+ const zeroCalc = {
+ debit: regionalZero,
+ credit: fiatZero,
+ beforeFee: fiatZero,
+ };
+ const [calculationResult, setCalculation] =
+ useState<TransferCalculation>(zeroCalc);
+ const sellFee = Amounts.parseOrThrow(conversionInfo.cashout_fee);
+ const sellRate = conversionInfo.cashout_ratio;
+ /**
+ * can be in regional currency or fiat currency
+ * depending on the isDebit flag
+ */
+ const inputAmount = Amounts.parseOrThrow(
+ `${form.isDebit ? regional_currency : fiat_currency}:${
+ !form.amount ? "0" : form.amount
+ }`,
+ );
+
+ useEffect(() => {
+ async function doAsync() {
+ await handleError(async () => {
+ const higerThanMin = form.isDebit
+ ? Amounts.cmp(inputAmount, conversionInfo.cashout_min_amount) === 1
+ : true;
+ const notZero = Amounts.isNonZero(inputAmount);
+ if (notZero && higerThanMin) {
+ const resp = await (form.isDebit
+ ? calculateFromDebit(inputAmount, sellFee)
+ : calculateFromCredit(inputAmount, sellFee));
+ setCalculation(resp);
+ } else {
+ setCalculation(zeroCalc);
+ }
+ });
+ }
+ doAsync();
+ }, [form.amount, form.isDebit]);
+
+ const calc =
+ calculationResult === "amount-is-too-small" ? zeroCalc : calculationResult;
+
+ const balanceAfter = Amounts.sub(account.balance, calc.debit).amount;
+
+ function updateForm(newForm: typeof form): void {
+ setForm(newForm);
+ }
+ const errors = undefinedIfEmpty<ErrorFrom<typeof form>>({
+ subject: !form.subject ? i18n.str`Required` : undefined,
+ amount: !form.amount
+ ? i18n.str`Required`
+ : !inputAmount
+ ? i18n.str`Invalid`
+ : Amounts.cmp(limit, calc.debit) === -1
+ ? i18n.str`Balance is not enough`
+ : form.isDebit &&
+ Amounts.cmp(inputAmount, conversionInfo.cashout_min_amount) < 1
+ ? i18n.str`Needs to be higher than ${
+ Amounts.stringifyValueWithSpec(
+ Amounts.parseOrThrow(conversionInfo.cashout_min_amount),
+ regional_currency_specification,
+ ).normal
+ }`
+ : calculationResult === "amount-is-too-small"
+ ? i18n.str`Amount needs to be higher`
+ : Amounts.isZero(calc.credit)
+ ? i18n.str`The total transfer at destination will be zero`
+ : undefined,
+ });
+ const trimmedAmountStr = form.amount?.trim();
+
+ async function createCashout() {
+ const request_uid = encodeCrock(getRandomBytes(32));
+ await handleError(async () => {
+ // new cashout api doesn't require channel
+ const validChannel =
+ config.supported_tan_channels.length === 0 || form.channel;
+
+ if (!creds || !form.subject || !validChannel) return;
+ const request = {
+ request_uid,
+ amount_credit: Amounts.stringify(calc.credit),
+ amount_debit: Amounts.stringify(calc.debit),
+ subject: form.subject,
+ tan_channel: form.channel,
+ };
+ const resp = await api.createCashout(creds, request);
+ if (resp.type === "ok") {
+ notifyInfo(i18n.str`Cashout created`);
+ } else {
+ switch (resp.case) {
+ case HttpStatusCode.Accepted: {
+ updateBankState("currentChallenge", {
+ operation: "create-cashout",
+ id: String(resp.body.challenge_id),
+ sent: AbsoluteTime.never(),
+ location: routeHere.url({}),
+ request,
+ });
+ return onAuthorizationRequired();
+ }
+ case HttpStatusCode.NotFound:
+ return notify({
+ type: "error",
+ title: i18n.str`Account not found`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED:
+ return notify({
+ type: "error",
+ title: i18n.str`Duplicated request detected, check if the operation succeeded or try again.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case TalerErrorCode.BANK_BAD_CONVERSION:
+ return notify({
+ type: "error",
+ title: i18n.str`The conversion rate was incorrectly applied`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case TalerErrorCode.BANK_UNALLOWED_DEBIT:
+ return notify({
+ type: "error",
+ title: i18n.str`The account does not have sufficient funds`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case HttpStatusCode.NotImplemented:
+ return notify({
+ type: "error",
+ title: i18n.str`Cashout are disabled`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case TalerErrorCode.BANK_CONFIRM_INCOMPLETE:
+ return notify({
+ type: "error",
+ title: i18n.str`Missing cashout URI in the profile`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case TalerErrorCode.BANK_TAN_CHANNEL_SCRIPT_FAILED:
+ return notify({
+ type: "error",
+ title: i18n.str`Sending the confirmation message failed, retry later or contact the administrator.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ }
+ assertUnreachable(resp);
+ }
+ });
+ }
+ const cashoutDisabled =
+ config.supported_tan_channels.length < 1 ||
+ !resultAccount.body.cashout_payto_uri;
+
+ const cashoutAccount = !resultAccount.body.cashout_payto_uri
+ ? undefined
+ : parsePaytoUri(resultAccount.body.cashout_payto_uri);
+ const cashoutAccountName = !cashoutAccount
+ ? undefined
+ : cashoutAccount.targetPath;
+
+ const cashoutLegalName = !cashoutAccount
+ ? undefined
+ : cashoutAccount.params["receiver-name"];
+
+ return (
+ <div>
+ <LocalNotificationBanner notification={notification} />
+
+ <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">
+ <section class="mt-4 rounded-sm px-4 py-6 p-8 ">
+ <h2 id="summary-heading" class="font-medium text-lg">
+ <i18n.Translate>Cashout</i18n.Translate>
+ </h2>
+
+ <dl class="mt-4 space-y-4">
+ <div class="justify-between items-center flex">
+ <dt class="text-sm text-gray-600">
+ <i18n.Translate>Conversion rate</i18n.Translate>
+ </dt>
+ <dd class="text-sm text-gray-900">{sellRate}</dd>
+ </div>
+
+ <div class="flex items-center justify-between border-t-2 afu pt-4">
+ <dt class="flex items-center text-sm text-gray-600">
+ <span>
+ <i18n.Translate>Balance</i18n.Translate>
+ </span>
+ </dt>
+ <dd class="text-sm text-gray-900">
+ <RenderAmount
+ value={account.balance}
+ spec={regional_currency_specification}
+ />
+ </dd>
+ </div>
+ <div class="flex items-center justify-between border-t-2 afu pt-4">
+ <dt class="flex items-center text-sm text-gray-600">
+ <span>
+ <i18n.Translate>Fee</i18n.Translate>
+ </span>
+ </dt>
+ <dd class="text-sm text-gray-900">
+ <RenderAmount
+ value={sellFee}
+ spec={fiat_currency_specification}
+ />
+ </dd>
+ </div>
+ {cashoutAccountName && cashoutLegalName ? (
+ <Fragment>
+ <div class="flex items-center justify-between border-t-2 afu pt-4">
+ <dt class="flex items-center text-sm text-gray-600">
+ <span>
+ <i18n.Translate>To account</i18n.Translate>
+ </span>
+ </dt>
+ <dd class="text-sm text-gray-900">{cashoutAccountName}</dd>
+ </div>
+ <div class="flex items-center justify-between border-t-2 afu pt-4">
+ <dt class="flex items-center text-sm text-gray-600">
+ <span>
+ <i18n.Translate>Legal name</i18n.Translate>
+ </span>
+ </dt>
+ <dd class="text-sm text-gray-900">{cashoutLegalName}</dd>
+ </div>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ If this name doesn't match the account holder's name your
+ transaction may fail.
+ </i18n.Translate>
+ </p>
+ </Fragment>
+ ) : (
+ <div class="flex items-center justify-between border-t-2 afu pt-4">
+ <Attention type="warning" title={i18n.str`No cashout account`}>
+ <i18n.Translate>
+ Before doing a cashout you need to complete your profile
+ </i18n.Translate>
+ </Attention>
+ </div>
+ )}
+ </dl>
+ </section>
+ <form
+ class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2"
+ autoCapitalize="none"
+ autoCorrect="off"
+ onSubmit={(e) => {
+ e.preventDefault();
+ }}
+ >
+ <div class="px-4 py-6 sm:p-8">
+ <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
+ {/* subject */}
+
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="subject"
+ >
+ {i18n.str`Transfer subject`}
+ <b style={{ color: "red" }}> *</b>
+ </label>
+ <div class="mt-2">
+ <input
+ ref={focus ? doAutoFocus : undefined}
+ type="text"
+ class="block w-full rounded-md disabled:bg-gray-200 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="subject"
+ id="subject"
+ disabled={cashoutDisabled}
+ data-error={!!errors?.subject && form.subject !== undefined}
+ value={form.subject ?? ""}
+ onChange={(e) => {
+ form.subject = e.currentTarget.value;
+ updateForm(structuredClone(form));
+ }}
+ autocomplete="off"
+ />
+ <ShowInputErrorLabel
+ message={errors?.subject}
+ isDirty={form.subject !== undefined}
+ />
+ </div>
+ </div>
+
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="subject"
+ >
+ {i18n.str`Currency`}
+ </label>
+
+ <div class="mt-2">
+ <button
+ type="button"
+ name="set 50"
+ class=" inline-flex p-4 text-sm items-center rounded-l-md bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10"
+ onClick={(e) => {
+ e.preventDefault();
+ form.isDebit = true;
+ updateForm(structuredClone(form));
+ }}
+ >
+ {form.isDebit ? (
+ <svg
+ class="self-center flex-none h-5 w-5 text-indigo-600"
+ viewBox="0 0 20 20"
+ fill="currentColor"
+ aria-hidden="true"
+ >
+ <path
+ fill-rule="evenodd"
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
+ clip-rule="evenodd"
+ />
+ </svg>
+ ) : (
+ <svg
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="1.5"
+ stroke="currentColor"
+ class="w-5 h-5"
+ >
+ <path d="M15 12H9m12 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
+ </svg>
+ )}
+
+ <i18n.Translate>Send {regional_currency}</i18n.Translate>
+ </button>
+ <button
+ type="button"
+ name="set 25"
+ class=" -ml-px -mr-px inline-flex p-4 text-sm items-center rounded-r-md bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10"
+ onClick={(e) => {
+ e.preventDefault();
+ form.isDebit = false;
+ updateForm(structuredClone(form));
+ }}
+ >
+ {!form.isDebit ? (
+ <svg
+ class="self-center flex-none h-5 w-5 text-indigo-600"
+ viewBox="0 0 20 20"
+ fill="currentColor"
+ aria-hidden="true"
+ >
+ <path
+ fill-rule="evenodd"
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
+ clip-rule="evenodd"
+ />
+ </svg>
+ ) : (
+ <svg
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="1.5"
+ stroke="currentColor"
+ class="w-5 h-5"
+ >
+ <path d="M15 12H9m12 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
+ </svg>
+ )}
+
+ <i18n.Translate>Receive {fiat_currency}</i18n.Translate>
+ </button>
+ </div>
+ </div>
+
+ {/* amount */}
+ <div class="sm:col-span-5">
+ <div class="flex justify-between">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="amount"
+ >
+ {i18n.str`Amount`}
+ <b style={{ color: "red" }}> *</b>
+ </label>
+ {/* <button
+ type="button"
+ data-enabled={form.isDebit}
+ 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={() => {
+ form.isDebit = !form.isDebit;
+ updateForm(structuredClone(form));
+ }}
+ >
+ <span
+ aria-hidden="true"
+ data-enabled={form.isDebit}
+ 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>
+ <div class="mt-2">
+ <InputAmount
+ name="amount"
+ left
+ currency={form.isDebit ? regional_currency : fiat_currency}
+ value={trimmedAmountStr}
+ onChange={
+ cashoutDisabled
+ ? undefined
+ : (value) => {
+ form.amount = value;
+ updateForm(structuredClone(form));
+ }
+ }
+ />
+ <ShowInputErrorLabel
+ message={errors?.amount}
+ isDirty={form.amount !== undefined}
+ />
+ </div>
+ </div>
+
+ {Amounts.isZero(calc.credit) ? undefined : (
+ <div class="sm:col-span-5">
+ <dl class="mt-4 space-y-4">
+ <div class="justify-between items-center flex ">
+ <dt class="text-sm text-gray-600">
+ <i18n.Translate>Total cost</i18n.Translate>
+ </dt>
+ <dd class="text-sm text-gray-900">
+ <RenderAmount
+ value={calc.debit}
+ negative
+ withColor
+ spec={regional_currency_specification}
+ />
+ </dd>
+ </div>
+
+ <div class="flex items-center justify-between border-t-2 afu pt-4">
+ <dt class="flex items-center text-sm text-gray-600">
+ <span>
+ <i18n.Translate>Balance left</i18n.Translate>
+ </span>
+ </dt>
+ <dd class="text-sm text-gray-900">
+ <RenderAmount
+ value={balanceAfter}
+ spec={regional_currency_specification}
+ />
+ </dd>
+ </div>
+ {Amounts.isZero(sellFee) ||
+ Amounts.isZero(calc.beforeFee) ? undefined : (
+ <div class="flex items-center justify-between border-t-2 afu pt-4">
+ <dt class="flex items-center text-sm text-gray-600">
+ <span>
+ <i18n.Translate>Before fee</i18n.Translate>
+ </span>
+ </dt>
+ <dd class="text-sm text-gray-900">
+ <RenderAmount
+ value={calc.beforeFee}
+ spec={fiat_currency_specification}
+ />
+ </dd>
+ </div>
+ )}
+ <div class="flex justify-between items-center border-t-2 afu pt-4">
+ <dt class="text-lg text-gray-900 font-medium">
+ <i18n.Translate>Total cashout transfer</i18n.Translate>
+ </dt>
+ <dd class="text-lg text-gray-900 font-medium">
+ <RenderAmount
+ value={calc.credit}
+ withColor
+ spec={fiat_currency_specification}
+ />
+ </dd>
+ </div>
+ </dl>
+ </div>
+ )}
+ </div>
+ </div>
+
+ <div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
+ <a
+ href={routeClose.url({})}
+ name="cancel"
+ type="button"
+ class="text-sm font-semibold leading-6 text-gray-900"
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </a>
+ <button
+ type="submit"
+ name="cashout"
+ class="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"
+ disabled={!!errors}
+ onClick={(e) => {
+ e.preventDefault();
+ createCashout();
+ }}
+ >
+ <i18n.Translate>Cashout</i18n.Translate>
+ </button>
+ </div>
+ </form>
+ </div>
+ </div>
+ );
+}
diff --git a/packages/bank-ui/src/pages/regional/ShowCashoutDetails.tsx b/packages/bank-ui/src/pages/regional/ShowCashoutDetails.tsx
new file mode 100644
index 000000000..aba00ad7a
--- /dev/null
+++ b/packages/bank-ui/src/pages/regional/ShowCashoutDetails.tsx
@@ -0,0 +1,194 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ AbsoluteTime,
+ Amounts,
+ HttpStatusCode,
+ TalerError,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
+import {
+ Attention,
+ Loading,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
+import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js";
+import { Time } from "../../components/Time.js";
+import { useCashoutDetails, useConversionInfo } from "../../hooks/regional.js";
+import { RouteDefinition } from "@gnu-taler/web-util/browser";
+import { RenderAmount } from "../PaytoWireTransferForm.js";
+
+interface Props {
+ id: string;
+ routeClose: RouteDefinition;
+}
+export function ShowCashoutDetails({ id, routeClose }: Props): VNode {
+ const { i18n } = useTranslationContext();
+ const cid = Number.parseInt(id, 10);
+
+ const result = useCashoutDetails(Number.isNaN(cid) ? undefined : cid);
+ const info = useConversionInfo();
+
+ if (Number.isNaN(cid)) {
+ return (
+ <Attention
+ type="danger"
+ title={i18n.str`Cashout id should be a number`}
+ />
+ );
+ }
+ if (!result) {
+ return <Loading />;
+ }
+ if (result instanceof TalerError) {
+ return <ErrorLoadingWithDebug error={result} />;
+ }
+ if (result.type === "fail") {
+ switch (result.case) {
+ case HttpStatusCode.NotFound:
+ return (
+ <Attention
+ type="warning"
+ title={i18n.str`This cashout not found. Maybe already aborted.`}
+ ></Attention>
+ );
+ case HttpStatusCode.NotImplemented:
+ return (
+ <Attention type="warning" title={i18n.str`Cashout are disabled`}>
+ <i18n.Translate>
+ Cashout should be enable by configuration and the conversion rate
+ should be initialized with fee, ratio and rounding mode.
+ </i18n.Translate>
+ </Attention>
+ );
+ default:
+ assertUnreachable(result);
+ }
+ }
+ if (!info) {
+ return <Loading />;
+ }
+
+ if (info instanceof TalerError) {
+ return <ErrorLoadingWithDebug error={info} />;
+ }
+ if (info.type === "fail") {
+ switch (info.case) {
+ case HttpStatusCode.NotImplemented: {
+ return (
+ <Attention type="danger" title={i18n.str`Cashout are disabled`}>
+ <i18n.Translate>
+ Cashout should be enable by configuration and the conversion rate
+ should be initialized with fee, ratio and rounding mode.
+ </i18n.Translate>
+ </Attention>
+ );
+ }
+ default:
+ assertUnreachable(info.case);
+ }
+ }
+
+ const { fiat_currency_specification, regional_currency_specification } =
+ info.body;
+
+ return (
+ <div>
+ <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">
+ <section class="rounded-sm px-4">
+ <h2 id="summary-heading" class="font-medium text-lg">
+ <i18n.Translate>Cashout detail</i18n.Translate>
+ </h2>
+ <dl class="mt-8 space-y-4">
+ <div class="justify-between items-center flex">
+ <dt class="text-sm text-gray-600">
+ <i18n.Translate>Subject</i18n.Translate>
+ </dt>
+ <dd class="text-sm ">{result.body.subject}</dd>
+ </div>
+ </dl>
+ </section>
+ <div class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2">
+ <div class="px-4 py-6 sm:p-8">
+ <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
+ <div class="sm:col-span-5">
+ <dl class="space-y-4">
+ {result.body.creation_time.t_s !== "never" ? (
+ <div class="justify-between items-center flex ">
+ <dt class=" text-gray-600">
+ <i18n.Translate>Created</i18n.Translate>
+ </dt>
+ <dd class="text-sm ">
+ <Time
+ format="dd/MM/yyyy HH:mm:ss"
+ timestamp={AbsoluteTime.fromProtocolTimestamp(
+ result.body.creation_time,
+ )}
+ // relative={Duration.fromSpec({ days: 1 })}
+ />
+ </dd>
+ </div>
+ ) : undefined}
+
+ <div class="flex justify-between items-center border-t-2 afu pt-4">
+ <dt class="text-gray-600">
+ <i18n.Translate>Debited</i18n.Translate>
+ </dt>
+ <dd class=" font-medium">
+ <RenderAmount
+ value={Amounts.parseOrThrow(result.body.amount_debit)}
+ negative
+ withColor
+ spec={regional_currency_specification}
+ />
+ </dd>
+ </div>
+
+ <div class="flex items-center justify-between border-t-2 afu pt-4">
+ <dt class="flex items-center text-gray-600">
+ <span>
+ <i18n.Translate>Credited</i18n.Translate>
+ </span>
+ </dt>
+ <dd class="text-sm ">
+ <RenderAmount
+ value={Amounts.parseOrThrow(result.body.amount_credit)}
+ withColor
+ spec={fiat_currency_specification}
+ />
+ </dd>
+ </div>
+ </dl>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <br />
+ <div style={{ display: "flex", justifyContent: "space-between" }}>
+ <a
+ href={routeClose.url({})}
+ name="close"
+ class="text-sm font-semibold leading-6 text-gray-900"
+ >
+ <i18n.Translate>Close</i18n.Translate>
+ </a>
+ </div>
+ </div>
+ );
+}
diff --git a/packages/bank-ui/src/pages/rnd.ts b/packages/bank-ui/src/pages/rnd.ts
new file mode 100644
index 000000000..d66fb005b
--- /dev/null
+++ b/packages/bank-ui/src/pages/rnd.ts
@@ -0,0 +1,2907 @@
+/*
+ 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 { encodeCrock, getRandomBytes } from "@gnu-taler/taler-util";
+
+const noun = [
+ "people",
+ "history",
+ "way",
+ "art",
+ "world",
+ "information",
+ "map",
+ "two",
+ "family",
+ "government",
+ "health",
+ "system",
+ "computer",
+ "meat",
+ "year",
+ "thanks",
+ "music",
+ "person",
+ "reading",
+ "method",
+ "data",
+ "food",
+ "understanding",
+ "theory",
+ "law",
+ "bird",
+ "literature",
+ "problem",
+ "software",
+ "control",
+ "knowledge",
+ "power",
+ "ability",
+ "economics",
+ "love",
+ "internet",
+ "television",
+ "science",
+ "library",
+ "nature",
+ "fact",
+ "product",
+ "idea",
+ "temperature",
+ "investment",
+ "area",
+ "society",
+ "activity",
+ "story",
+ "industry",
+ "media",
+ "thing",
+ "oven",
+ "community",
+ "definition",
+ "safety",
+ "quality",
+ "development",
+ "language",
+ "management",
+ "player",
+ "variety",
+ "video",
+ "week",
+ "security",
+ "country",
+ "exam",
+ "movie",
+ "organization",
+ "equipment",
+ "physics",
+ "analysis",
+ "policy",
+ "series",
+ "thought",
+ "basis",
+ "boyfriend",
+ "direction",
+ "strategy",
+ "technology",
+ "army",
+ "camera",
+ "freedom",
+ "paper",
+ "environment",
+ "child",
+ "instance",
+ "month",
+ "truth",
+ "marketing",
+ "university",
+ "writing",
+ "article",
+ "department",
+ "difference",
+ "goal",
+ "news",
+ "audience",
+ "fishing",
+ "growth",
+ "income",
+ "marriage",
+ "user",
+ "combination",
+ "failure",
+ "meaning",
+ "medicine",
+ "philosophy",
+ "teacher",
+ "communication",
+ "night",
+ "chemistry",
+ "disease",
+ "disk",
+ "energy",
+ "nation",
+ "road",
+ "role",
+ "soup",
+ "advertising",
+ "location",
+ "success",
+ "addition",
+ "apartment",
+ "education",
+ "math",
+ "moment",
+ "painting",
+ "politics",
+ "attention",
+ "decision",
+ "event",
+ "property",
+ "shopping",
+ "student",
+ "wood",
+ "competition",
+ "distribution",
+ "entertainment",
+ "office",
+ "population",
+ "president",
+ "unit",
+ "category",
+ "cigarette",
+ "context",
+ "introduction",
+ "opportunity",
+ "performance",
+ "driver",
+ "flight",
+ "length",
+ "magazine",
+ "newspaper",
+ "relationship",
+ "teaching",
+ "cell",
+ "dealer",
+ "finding",
+ "lake",
+ "member",
+ "message",
+ "phone",
+ "scene",
+ "appearance",
+ "association",
+ "concept",
+ "customer",
+ "death",
+ "discussion",
+ "housing",
+ "inflation",
+ "insurance",
+ "mood",
+ "woman",
+ "advice",
+ "blood",
+ "effort",
+ "expression",
+ "importance",
+ "opinion",
+ "payment",
+ "reality",
+ "responsibility",
+ "situation",
+ "skill",
+ "statement",
+ "wealth",
+ "application",
+ "city",
+ "county",
+ "depth",
+ "estate",
+ "foundation",
+ "grandmother",
+ "heart",
+ "perspective",
+ "photo",
+ "recipe",
+ "studio",
+ "topic",
+ "collection",
+ "depression",
+ "imagination",
+ "passion",
+ "percentage",
+ "resource",
+ "setting",
+ "ad",
+ "agency",
+ "college",
+ "connection",
+ "criticism",
+ "debt",
+ "description",
+ "memory",
+ "patience",
+ "secretary",
+ "solution",
+ "administration",
+ "aspect",
+ "attitude",
+ "director",
+ "personality",
+ "psychology",
+ "recommendation",
+ "response",
+ "selection",
+ "storage",
+ "version",
+ "alcohol",
+ "argument",
+ "complaint",
+ "contract",
+ "emphasis",
+ "highway",
+ "loss",
+ "membership",
+ "possession",
+ "preparation",
+ "steak",
+ "union",
+ "agreement",
+ "cancer",
+ "currency",
+ "employment",
+ "engineering",
+ "entry",
+ "interaction",
+ "mixture",
+ "preference",
+ "region",
+ "republic",
+ "tradition",
+ "virus",
+ "actor",
+ "classroom",
+ "delivery",
+ "device",
+ "difficulty",
+ "drama",
+ "election",
+ "engine",
+ "football",
+ "guidance",
+ "hotel",
+ "owner",
+ "priority",
+ "protection",
+ "suggestion",
+ "tension",
+ "variation",
+ "anxiety",
+ "atmosphere",
+ "awareness",
+ "bath",
+ "bread",
+ "candidate",
+ "climate",
+ "comparison",
+ "confusion",
+ "construction",
+ "elevator",
+ "emotion",
+ "employee",
+ "employer",
+ "guest",
+ "height",
+ "leadership",
+ "mall",
+ "manager",
+ "operation",
+ "recording",
+ "sample",
+ "transportation",
+ "charity",
+ "cousin",
+ "disaster",
+ "editor",
+ "efficiency",
+ "excitement",
+ "extent",
+ "feedback",
+ "guitar",
+ "homework",
+ "leader",
+ "mom",
+ "outcome",
+ "permission",
+ "presentation",
+ "promotion",
+ "reflection",
+ "refrigerator",
+ "resolution",
+ "revenue",
+ "session",
+ "singer",
+ "tennis",
+ "basket",
+ "bonus",
+ "cabinet",
+ "childhood",
+ "church",
+ "clothes",
+ "coffee",
+ "dinner",
+ "drawing",
+ "hair",
+ "hearing",
+ "initiative",
+ "judgment",
+ "lab",
+ "measurement",
+ "mode",
+ "mud",
+ "orange",
+ "poetry",
+ "police",
+ "possibility",
+ "procedure",
+ "queen",
+ "ratio",
+ "relation",
+ "restaurant",
+ "satisfaction",
+ "sector",
+ "signature",
+ "significance",
+ "song",
+ "tooth",
+ "town",
+ "vehicle",
+ "volume",
+ "wife",
+ "accident",
+ "airport",
+ "appointment",
+ "arrival",
+ "assumption",
+ "baseball",
+ "chapter",
+ "committee",
+ "conversation",
+ "database",
+ "enthusiasm",
+ "error",
+ "explanation",
+ "farmer",
+ "gate",
+ "girl",
+ "hall",
+ "historian",
+ "hospital",
+ "injury",
+ "instruction",
+ "maintenance",
+ "manufacturer",
+ "meal",
+ "perception",
+ "pie",
+ "poem",
+ "presence",
+ "proposal",
+ "reception",
+ "replacement",
+ "revolution",
+ "river",
+ "son",
+ "speech",
+ "tea",
+ "village",
+ "warning",
+ "winner",
+ "worker",
+ "writer",
+ "assistance",
+ "breath",
+ "buyer",
+ "chest",
+ "chocolate",
+ "conclusion",
+ "contribution",
+ "cookie",
+ "courage",
+ "dad",
+ "desk",
+ "drawer",
+ "establishment",
+ "examination",
+ "garbage",
+ "grocery",
+ "honey",
+ "impression",
+ "improvement",
+ "independence",
+ "insect",
+ "inspection",
+ "inspector",
+ "king",
+ "ladder",
+ "menu",
+ "penalty",
+ "piano",
+ "potato",
+ "profession",
+ "professor",
+ "quantity",
+ "reaction",
+ "requirement",
+ "salad",
+ "sister",
+ "supermarket",
+ "tongue",
+ "weakness",
+ "wedding",
+ "affair",
+ "ambition",
+ "analyst",
+ "apple",
+ "assignment",
+ "assistant",
+ "bathroom",
+ "bedroom",
+ "beer",
+ "birthday",
+ "celebration",
+ "championship",
+ "cheek",
+ "client",
+ "consequence",
+ "departure",
+ "diamond",
+ "dirt",
+ "ear",
+ "fortune",
+ "friendship",
+ "funeral",
+ "gene",
+ "girlfriend",
+ "hat",
+ "indication",
+ "intention",
+ "lady",
+ "midnight",
+ "negotiation",
+ "obligation",
+ "passenger",
+ "pizza",
+ "platform",
+ "poet",
+ "pollution",
+ "recognition",
+ "reputation",
+ "shirt",
+ "sir",
+ "speaker",
+ "stranger",
+ "surgery",
+ "sympathy",
+ "tale",
+ "throat",
+ "trainer",
+ "uncle",
+ "youth",
+ "time",
+ "work",
+ "film",
+ "water",
+ "money",
+ "example",
+ "while",
+ "business",
+ "study",
+ "game",
+ "life",
+ "form",
+ "air",
+ "day",
+ "place",
+ "number",
+ "part",
+ "field",
+ "fish",
+ "back",
+ "process",
+ "heat",
+ "hand",
+ "experience",
+ "job",
+ "book",
+ "end",
+ "point",
+ "type",
+ "home",
+ "economy",
+ "value",
+ "body",
+ "market",
+ "guide",
+ "interest",
+ "state",
+ "radio",
+ "course",
+ "company",
+ "price",
+ "size",
+ "card",
+ "list",
+ "mind",
+ "trade",
+ "line",
+ "care",
+ "group",
+ "risk",
+ "word",
+ "fat",
+ "force",
+ "key",
+ "light",
+ "training",
+ "name",
+ "school",
+ "top",
+ "amount",
+ "level",
+ "order",
+ "practice",
+ "research",
+ "sense",
+ "service",
+ "piece",
+ "web",
+ "boss",
+ "sport",
+ "fun",
+ "house",
+ "page",
+ "term",
+ "test",
+ "answer",
+ "sound",
+ "focus",
+ "matter",
+ "kind",
+ "soil",
+ "board",
+ "oil",
+ "picture",
+ "access",
+ "garden",
+ "range",
+ "rate",
+ "reason",
+ "future",
+ "site",
+ "demand",
+ "exercise",
+ "image",
+ "case",
+ "cause",
+ "coast",
+ "action",
+ "age",
+ "bad",
+ "boat",
+ "record",
+ "result",
+ "section",
+ "building",
+ "mouse",
+ "cash",
+ "class",
+ "nothing",
+ "period",
+ "plan",
+ "store",
+ "tax",
+ "side",
+ "subject",
+ "space",
+ "rule",
+ "stock",
+ "weather",
+ "chance",
+ "figure",
+ "man",
+ "model",
+ "source",
+ "beginning",
+ "earth",
+ "program",
+ "chicken",
+ "design",
+ "feature",
+ "head",
+ "material",
+ "purpose",
+ "question",
+ "rock",
+ "salt",
+ "act",
+ "birth",
+ "car",
+ "dog",
+ "object",
+ "scale",
+ "sun",
+ "note",
+ "profit",
+ "rent",
+ "speed",
+ "style",
+ "war",
+ "bank",
+ "craft",
+ "half",
+ "inside",
+ "outside",
+ "standard",
+ "bus",
+ "exchange",
+ "eye",
+ "fire",
+ "position",
+ "pressure",
+ "stress",
+ "advantage",
+ "benefit",
+ "box",
+ "frame",
+ "issue",
+ "step",
+ "cycle",
+ "face",
+ "item",
+ "metal",
+ "paint",
+ "review",
+ "room",
+ "screen",
+ "structure",
+ "view",
+ "account",
+ "ball",
+ "discipline",
+ "medium",
+ "share",
+ "balance",
+ "bit",
+ "black",
+ "bottom",
+ "choice",
+ "gift",
+ "impact",
+ "machine",
+ "shape",
+ "tool",
+ "wind",
+ "address",
+ "average",
+ "career",
+ "culture",
+ "morning",
+ "pot",
+ "sign",
+ "table",
+ "task",
+ "condition",
+ "contact",
+ "credit",
+ "egg",
+ "hope",
+ "ice",
+ "network",
+ "north",
+ "square",
+ "attempt",
+ "date",
+ "effect",
+ "link",
+ "post",
+ "star",
+ "voice",
+ "capital",
+ "challenge",
+ "friend",
+ "self",
+ "shot",
+ "brush",
+ "couple",
+ "debate",
+ "exit",
+ "front",
+ "function",
+ "lack",
+ "living",
+ "plant",
+ "plastic",
+ "spot",
+ "summer",
+ "taste",
+ "theme",
+ "track",
+ "wing",
+ "brain",
+ "button",
+ "click",
+ "desire",
+ "foot",
+ "gas",
+ "influence",
+ "notice",
+ "rain",
+ "wall",
+ "base",
+ "damage",
+ "distance",
+ "feeling",
+ "pair",
+ "savings",
+ "staff",
+ "sugar",
+ "target",
+ "text",
+ "animal",
+ "author",
+ "budget",
+ "discount",
+ "file",
+ "ground",
+ "lesson",
+ "minute",
+ "officer",
+ "phase",
+ "reference",
+ "register",
+ "sky",
+ "stage",
+ "stick",
+ "title",
+ "trouble",
+ "bowl",
+ "bridge",
+ "campaign",
+ "character",
+ "club",
+ "edge",
+ "evidence",
+ "fan",
+ "letter",
+ "lock",
+ "maximum",
+ "novel",
+ "option",
+ "pack",
+ "park",
+ "plenty",
+ "quarter",
+ "skin",
+ "sort",
+ "weight",
+ "baby",
+ "background",
+ "carry",
+ "dish",
+ "factor",
+ "fruit",
+ "glass",
+ "joint",
+ "master",
+ "muscle",
+ "red",
+ "strength",
+ "traffic",
+ "trip",
+ "vegetable",
+ "appeal",
+ "chart",
+ "gear",
+ "ideal",
+ "kitchen",
+ "land",
+ "log",
+ "mother",
+ "net",
+ "party",
+ "principle",
+ "relative",
+ "sale",
+ "season",
+ "signal",
+ "spirit",
+ "street",
+ "tree",
+ "wave",
+ "belt",
+ "bench",
+ "commission",
+ "copy",
+ "drop",
+ "minimum",
+ "path",
+ "progress",
+ "project",
+ "sea",
+ "south",
+ "status",
+ "stuff",
+ "ticket",
+ "tour",
+ "angle",
+ "blue",
+ "breakfast",
+ "confidence",
+ "daughter",
+ "degree",
+ "doctor",
+ "dot",
+ "dream",
+ "duty",
+ "essay",
+ "father",
+ "fee",
+ "finance",
+ "hour",
+ "juice",
+ "limit",
+ "luck",
+ "milk",
+ "mouth",
+ "peace",
+ "pipe",
+ "seat",
+ "stable",
+ "storm",
+ "substance",
+ "team",
+ "trick",
+ "afternoon",
+ "bat",
+ "beach",
+ "blank",
+ "catch",
+ "chain",
+ "consideration",
+ "cream",
+ "crew",
+ "detail",
+ "gold",
+ "interview",
+ "kid",
+ "mark",
+ "match",
+ "mission",
+ "pain",
+ "pleasure",
+ "score",
+ "screw",
+ "sex",
+ "shop",
+ "shower",
+ "suit",
+ "tone",
+ "window",
+ "agent",
+ "band",
+ "block",
+ "bone",
+ "calendar",
+ "cap",
+ "coat",
+ "contest",
+ "corner",
+ "court",
+ "cup",
+ "district",
+ "door",
+ "east",
+ "finger",
+ "garage",
+ "guarantee",
+ "hole",
+ "hook",
+ "implement",
+ "layer",
+ "lecture",
+ "lie",
+ "manner",
+ "meeting",
+ "nose",
+ "parking",
+ "partner",
+ "profile",
+ "respect",
+ "rice",
+ "routine",
+ "schedule",
+ "swimming",
+ "telephone",
+ "tip",
+ "winter",
+ "airline",
+ "bag",
+ "battle",
+ "bed",
+ "bill",
+ "bother",
+ "cake",
+ "code",
+ "curve",
+ "designer",
+ "dimension",
+ "dress",
+ "ease",
+ "emergency",
+ "evening",
+ "extension",
+ "farm",
+ "fight",
+ "gap",
+ "grade",
+ "holiday",
+ "horror",
+ "horse",
+ "host",
+ "husband",
+ "loan",
+ "mistake",
+ "mountain",
+ "nail",
+ "noise",
+ "occasion",
+ "package",
+ "patient",
+ "pause",
+ "phrase",
+ "proof",
+ "race",
+ "relief",
+ "sand",
+ "sentence",
+ "shoulder",
+ "smoke",
+ "stomach",
+ "string",
+ "tourist",
+ "towel",
+ "vacation",
+ "west",
+ "wheel",
+ "wine",
+ "arm",
+ "aside",
+ "associate",
+ "bet",
+ "blow",
+ "border",
+ "branch",
+ "breast",
+ "brother",
+ "buddy",
+ "bunch",
+ "chip",
+ "coach",
+ "cross",
+ "document",
+ "draft",
+ "dust",
+ "expert",
+ "floor",
+ "god",
+ "golf",
+ "habit",
+ "iron",
+ "judge",
+ "knife",
+ "landscape",
+ "league",
+ "mail",
+ "mess",
+ "native",
+ "opening",
+ "parent",
+ "pattern",
+ "pin",
+ "pool",
+ "pound",
+ "request",
+ "salary",
+ "shame",
+ "shelter",
+ "shoe",
+ "silver",
+ "tackle",
+ "tank",
+ "trust",
+ "assist",
+ "bake",
+ "bar",
+ "bell",
+ "bike",
+ "blame",
+ "boy",
+ "brick",
+ "chair",
+ "closet",
+ "clue",
+ "collar",
+ "comment",
+ "conference",
+ "devil",
+ "diet",
+ "fear",
+ "fuel",
+ "glove",
+ "jacket",
+ "lunch",
+ "monitor",
+ "mortgage",
+ "nurse",
+ "pace",
+ "panic",
+ "peak",
+ "plane",
+ "reward",
+ "row",
+ "sandwich",
+ "shock",
+ "spite",
+ "spray",
+ "surprise",
+ "till",
+ "transition",
+ "weekend",
+ "welcome",
+ "yard",
+ "alarm",
+ "bend",
+ "bicycle",
+ "bite",
+ "blind",
+ "bottle",
+ "cable",
+ "candle",
+ "clerk",
+ "cloud",
+ "concert",
+ "counter",
+ "flower",
+ "grandfather",
+ "harm",
+ "knee",
+ "lawyer",
+ "leather",
+ "load",
+ "mirror",
+ "neck",
+ "pension",
+ "plate",
+ "purple",
+ "ruin",
+ "ship",
+ "skirt",
+ "slice",
+ "snow",
+ "specialist",
+ "stroke",
+ "switch",
+ "trash",
+ "tune",
+ "zone",
+ "anger",
+ "award",
+ "bid",
+ "bitter",
+ "boot",
+ "bug",
+ "camp",
+ "candy",
+ "carpet",
+ "cat",
+ "champion",
+ "channel",
+ "clock",
+ "comfort",
+ "cow",
+ "crack",
+ "engineer",
+ "entrance",
+ "fault",
+ "grass",
+ "guy",
+ "hell",
+ "highlight",
+ "incident",
+ "island",
+ "joke",
+ "jury",
+ "leg",
+ "lip",
+ "mate",
+ "motor",
+ "nerve",
+ "passage",
+ "pen",
+ "pride",
+ "priest",
+ "prize",
+ "promise",
+ "resident",
+ "resort",
+ "ring",
+ "roof",
+ "rope",
+ "sail",
+ "scheme",
+ "script",
+ "sock",
+ "station",
+ "toe",
+ "tower",
+ "truck",
+ "witness",
+ "a",
+ "you",
+ "it",
+ "can",
+ "will",
+ "if",
+ "one",
+ "many",
+ "most",
+ "other",
+ "use",
+ "make",
+ "good",
+ "look",
+ "help",
+ "go",
+ "great",
+ "being",
+ "few",
+ "might",
+ "still",
+ "public",
+ "read",
+ "keep",
+ "start",
+ "give",
+ "human",
+ "local",
+ "general",
+ "she",
+ "specific",
+ "long",
+ "play",
+ "feel",
+ "high",
+ "tonight",
+ "put",
+ "common",
+ "set",
+ "change",
+ "simple",
+ "past",
+ "big",
+ "possible",
+ "particular",
+ "today",
+ "major",
+ "personal",
+ "current",
+ "national",
+ "cut",
+ "natural",
+ "physical",
+ "show",
+ "try",
+ "check",
+ "second",
+ "call",
+ "move",
+ "pay",
+ "let",
+ "increase",
+ "single",
+ "individual",
+ "turn",
+ "ask",
+ "buy",
+ "guard",
+ "hold",
+ "main",
+ "offer",
+ "potential",
+ "professional",
+ "international",
+ "travel",
+ "cook",
+ "alternative",
+ "following",
+ "special",
+ "working",
+ "whole",
+ "dance",
+ "excuse",
+ "cold",
+ "commercial",
+ "low",
+ "purchase",
+ "deal",
+ "primary",
+ "worth",
+ "fall",
+ "necessary",
+ "positive",
+ "produce",
+ "search",
+ "present",
+ "spend",
+ "talk",
+ "creative",
+ "tell",
+ "cost",
+ "drive",
+ "green",
+ "support",
+ "glad",
+ "remove",
+ "return",
+ "run",
+ "complex",
+ "due",
+ "effective",
+ "middle",
+ "regular",
+ "reserve",
+ "independent",
+ "leave",
+ "original",
+ "reach",
+ "rest",
+ "serve",
+ "watch",
+ "beautiful",
+ "charge",
+ "active",
+ "break",
+ "negative",
+ "safe",
+ "stay",
+ "visit",
+ "visual",
+ "affect",
+ "cover",
+ "report",
+ "rise",
+ "walk",
+ "white",
+ "beyond",
+ "junior",
+ "pick",
+ "unique",
+ "anything",
+ "classic",
+ "final",
+ "lift",
+ "mix",
+ "private",
+ "stop",
+ "teach",
+ "western",
+ "concern",
+ "familiar",
+ "fly",
+ "official",
+ "broad",
+ "comfortable",
+ "gain",
+ "maybe",
+ "rich",
+ "save",
+ "stand",
+ "young",
+ "fail",
+ "heavy",
+ "hello",
+ "lead",
+ "listen",
+ "valuable",
+ "worry",
+ "handle",
+ "leading",
+ "meet",
+ "release",
+ "sell",
+ "finish",
+ "normal",
+ "press",
+ "ride",
+ "secret",
+ "spread",
+ "spring",
+ "tough",
+ "wait",
+ "brown",
+ "deep",
+ "display",
+ "flow",
+ "hit",
+ "objective",
+ "shoot",
+ "touch",
+ "cancel",
+ "chemical",
+ "cry",
+ "dump",
+ "extreme",
+ "push",
+ "conflict",
+ "eat",
+ "fill",
+ "formal",
+ "jump",
+ "kick",
+ "opposite",
+ "pass",
+ "pitch",
+ "remote",
+ "total",
+ "treat",
+ "vast",
+ "abuse",
+ "beat",
+ "burn",
+ "deposit",
+ "print",
+ "raise",
+ "sleep",
+ "somewhere",
+ "advance",
+ "anywhere",
+ "consist",
+ "dark",
+ "double",
+ "draw",
+ "equal",
+ "fix",
+ "hire",
+ "internal",
+ "join",
+ "kill",
+ "sensitive",
+ "tap",
+ "win",
+ "attack",
+ "claim",
+ "constant",
+ "drag",
+ "drink",
+ "guess",
+ "minor",
+ "pull",
+ "raw",
+ "soft",
+ "solid",
+ "wear",
+ "weird",
+ "wonder",
+ "annual",
+ "count",
+ "dead",
+ "doubt",
+ "feed",
+ "forever",
+ "impress",
+ "nobody",
+ "repeat",
+ "round",
+ "sing",
+ "slide",
+ "strip",
+ "whereas",
+ "wish",
+ "combine",
+ "command",
+ "dig",
+ "divide",
+ "equivalent",
+ "hang",
+ "hunt",
+ "initial",
+ "march",
+ "mention",
+ "smell",
+ "spiritual",
+ "survey",
+ "tie",
+ "adult",
+ "brief",
+ "crazy",
+ "escape",
+ "gather",
+ "hate",
+ "prior",
+ "repair",
+ "rough",
+ "sad",
+ "scratch",
+ "sick",
+ "strike",
+ "employ",
+ "external",
+ "hurt",
+ "illegal",
+ "laugh",
+ "lay",
+ "mobile",
+ "nasty",
+ "ordinary",
+ "respond",
+ "royal",
+ "senior",
+ "split",
+ "strain",
+ "struggle",
+ "swim",
+ "train",
+ "upper",
+ "wash",
+ "yellow",
+ "convert",
+ "crash",
+ "dependent",
+ "fold",
+ "funny",
+ "grab",
+ "hide",
+ "miss",
+ "permit",
+ "quote",
+ "recover",
+ "resolve",
+ "roll",
+ "sink",
+ "slip",
+ "spare",
+ "suspect",
+ "sweet",
+ "swing",
+ "twist",
+ "upstairs",
+ "usual",
+ "abroad",
+ "brave",
+ "calm",
+ "concentrate",
+ "estimate",
+ "grand",
+ "male",
+ "mine",
+ "prompt",
+ "quiet",
+ "refuse",
+ "regret",
+ "reveal",
+ "rush",
+ "shake",
+ "shift",
+ "shine",
+ "steal",
+ "suck",
+ "surround",
+ "anybody",
+ "bear",
+ "brilliant",
+ "dare",
+ "dear",
+ "delay",
+ "drunk",
+ "female",
+ "hurry",
+ "inevitable",
+ "invite",
+ "kiss",
+ "neat",
+ "pop",
+ "punch",
+ "quit",
+ "reply",
+ "representative",
+ "resist",
+ "rip",
+ "rub",
+ "silly",
+ "smile",
+ "spell",
+ "stretch",
+ "stupid",
+ "tear",
+ "temporary",
+ "tomorrow",
+ "wake",
+ "wrap",
+ "yesterday",
+];
+
+const adj = [
+ "abandoned",
+ "able",
+ "absolute",
+ "adorable",
+ "adventurous",
+ "academic",
+ "acceptable",
+ "acclaimed",
+ "accomplished",
+ "accurate",
+ "aching",
+ "acidic",
+ "acrobatic",
+ "active",
+ "actual",
+ "adept",
+ "admirable",
+ "admired",
+ "adolescent",
+ "adorable",
+ "adored",
+ "advanced",
+ "afraid",
+ "affectionate",
+ "aged",
+ "aggravating",
+ "aggressive",
+ "agile",
+ "agitated",
+ "agonizing",
+ "agreeable",
+ "ajar",
+ "alarmed",
+ "alarming",
+ "alert",
+ "alienated",
+ "alive",
+ "all",
+ "altruistic",
+ "amazing",
+ "ambitious",
+ "ample",
+ "amused",
+ "amusing",
+ "anchored",
+ "ancient",
+ "angelic",
+ "angry",
+ "anguished",
+ "animated",
+ "annual",
+ "another",
+ "antique",
+ "anxious",
+ "any",
+ "apprehensive",
+ "appropriate",
+ "apt",
+ "arctic",
+ "arid",
+ "aromatic",
+ "artistic",
+ "ashamed",
+ "assured",
+ "astonishing",
+ "athletic",
+ "attached",
+ "attentive",
+ "attractive",
+ "austere",
+ "authentic",
+ "authorized",
+ "automatic",
+ "avaricious",
+ "average",
+ "aware",
+ "awesome",
+ "awful",
+ "awkward",
+ "babyish",
+ "bad",
+ "back",
+ "baggy",
+ "bare",
+ "barren",
+ "basic",
+ "beautiful",
+ "belated",
+ "beloved",
+ "beneficial",
+ "better",
+ "best",
+ "bewitched",
+ "big",
+ "big-hearted",
+ "biodegradable",
+ "bite-sized",
+ "bitter",
+ "black",
+ "black-and-white",
+ "bland",
+ "blank",
+ "blaring",
+ "bleak",
+ "blind",
+ "blissful",
+ "blond",
+ "blue",
+ "blushing",
+ "bogus",
+ "boiling",
+ "bold",
+ "bony",
+ "boring",
+ "bossy",
+ "both",
+ "bouncy",
+ "bountiful",
+ "bowed",
+ "brave",
+ "breakable",
+ "brief",
+ "bright",
+ "brilliant",
+ "brisk",
+ "broken",
+ "bronze",
+ "brown",
+ "bruised",
+ "bubbly",
+ "bulky",
+ "bumpy",
+ "buoyant",
+ "burdensome",
+ "burly",
+ "bustling",
+ "busy",
+ "buttery",
+ "buzzing",
+ "calculating",
+ "calm",
+ "candid",
+ "canine",
+ "capital",
+ "carefree",
+ "careful",
+ "careless",
+ "caring",
+ "cautious",
+ "cavernous",
+ "celebrated",
+ "charming",
+ "cheap",
+ "cheerful",
+ "cheery",
+ "chief",
+ "chilly",
+ "chubby",
+ "circular",
+ "classic",
+ "clean",
+ "clear",
+ "clear-cut",
+ "clever",
+ "close",
+ "closed",
+ "cloudy",
+ "clueless",
+ "clumsy",
+ "cluttered",
+ "coarse",
+ "cold",
+ "colorful",
+ "colorless",
+ "colossal",
+ "comfortable",
+ "common",
+ "compassionate",
+ "competent",
+ "complete",
+ "complex",
+ "complicated",
+ "composed",
+ "concerned",
+ "concrete",
+ "confused",
+ "conscious",
+ "considerate",
+ "constant",
+ "content",
+ "conventional",
+ "cooked",
+ "cool",
+ "cooperative",
+ "coordinated",
+ "corny",
+ "corrupt",
+ "costly",
+ "courageous",
+ "courteous",
+ "crafty",
+ "crazy",
+ "creamy",
+ "creative",
+ "creepy",
+ "criminal",
+ "crisp",
+ "critical",
+ "crooked",
+ "crowded",
+ "cruel",
+ "crushing",
+ "cuddly",
+ "cultivated",
+ "cultured",
+ "cumbersome",
+ "curly",
+ "curvy",
+ "cute",
+ "cylindrical",
+ "damaged",
+ "damp",
+ "dangerous",
+ "dapper",
+ "daring",
+ "darling",
+ "dark",
+ "dazzling",
+ "dead",
+ "deadly",
+ "deafening",
+ "dear",
+ "dearest",
+ "decent",
+ "decimal",
+ "decisive",
+ "deep",
+ "defenseless",
+ "defensive",
+ "defiant",
+ "deficient",
+ "definite",
+ "definitive",
+ "delayed",
+ "delectable",
+ "delicious",
+ "delightful",
+ "delirious",
+ "demanding",
+ "dense",
+ "dental",
+ "dependable",
+ "dependent",
+ "descriptive",
+ "deserted",
+ "detailed",
+ "determined",
+ "devoted",
+ "different",
+ "difficult",
+ "digital",
+ "diligent",
+ "dim",
+ "dimpled",
+ "dimwitted",
+ "direct",
+ "disastrous",
+ "discrete",
+ "disfigured",
+ "disgusting",
+ "disloyal",
+ "dismal",
+ "distant",
+ "downright",
+ "dreary",
+ "dirty",
+ "disguised",
+ "dishonest",
+ "dismal",
+ "distant",
+ "distinct",
+ "distorted",
+ "dizzy",
+ "dopey",
+ "doting",
+ "double",
+ "downright",
+ "drab",
+ "drafty",
+ "dramatic",
+ "dreary",
+ "droopy",
+ "dry",
+ "dual",
+ "dull",
+ "dutiful",
+ "each",
+ "eager",
+ "earnest",
+ "early",
+ "easy",
+ "easy-going",
+ "ecstatic",
+ "edible",
+ "educated",
+ "elaborate",
+ "elastic",
+ "elated",
+ "elderly",
+ "electric",
+ "elegant",
+ "elementary",
+ "elliptical",
+ "embarrassed",
+ "embellished",
+ "eminent",
+ "emotional",
+ "empty",
+ "enchanted",
+ "enchanting",
+ "energetic",
+ "enlightened",
+ "enormous",
+ "enraged",
+ "entire",
+ "envious",
+ "equal",
+ "equatorial",
+ "essential",
+ "esteemed",
+ "ethical",
+ "euphoric",
+ "even",
+ "evergreen",
+ "everlasting",
+ "every",
+ "evil",
+ "exalted",
+ "excellent",
+ "exemplary",
+ "exhausted",
+ "excitable",
+ "excited",
+ "exciting",
+ "exotic",
+ "expensive",
+ "experienced",
+ "expert",
+ "extraneous",
+ "extroverted",
+ "extra-large",
+ "extra-small",
+ "fabulous",
+ "failing",
+ "faint",
+ "fair",
+ "faithful",
+ "fake",
+ "false",
+ "familiar",
+ "famous",
+ "fancy",
+ "fantastic",
+ "far",
+ "faraway",
+ "far-flung",
+ "far-off",
+ "fast",
+ "fat",
+ "fatal",
+ "fatherly",
+ "favorable",
+ "favorite",
+ "fearful",
+ "fearless",
+ "feisty",
+ "feline",
+ "female",
+ "feminine",
+ "few",
+ "fickle",
+ "filthy",
+ "fine",
+ "finished",
+ "firm",
+ "first",
+ "firsthand",
+ "fitting",
+ "fixed",
+ "flaky",
+ "flamboyant",
+ "flashy",
+ "flat",
+ "flawed",
+ "flawless",
+ "flickering",
+ "flimsy",
+ "flippant",
+ "flowery",
+ "fluffy",
+ "fluid",
+ "flustered",
+ "focused",
+ "fond",
+ "foolhardy",
+ "foolish",
+ "forceful",
+ "forked",
+ "formal",
+ "forsaken",
+ "forthright",
+ "fortunate",
+ "fragrant",
+ "frail",
+ "frank",
+ "frayed",
+ "free",
+ "French",
+ "fresh",
+ "frequent",
+ "friendly",
+ "frightened",
+ "frightening",
+ "frigid",
+ "frilly",
+ "frizzy",
+ "frivolous",
+ "front",
+ "frosty",
+ "frozen",
+ "frugal",
+ "fruitful",
+ "full",
+ "fumbling",
+ "functional",
+ "funny",
+ "fussy",
+ "fuzzy",
+ "gargantuan",
+ "gaseous",
+ "general",
+ "generous",
+ "gentle",
+ "genuine",
+ "giant",
+ "giddy",
+ "gigantic",
+ "gifted",
+ "giving",
+ "glamorous",
+ "glaring",
+ "glass",
+ "gleaming",
+ "gleeful",
+ "glistening",
+ "glittering",
+ "gloomy",
+ "glorious",
+ "glossy",
+ "glum",
+ "golden",
+ "good",
+ "good-natured",
+ "gorgeous",
+ "graceful",
+ "gracious",
+ "grand",
+ "grandiose",
+ "granular",
+ "grateful",
+ "grave",
+ "gray",
+ "great",
+ "greedy",
+ "green",
+ "gregarious",
+ "grim",
+ "grimy",
+ "gripping",
+ "grizzled",
+ "gross",
+ "grotesque",
+ "grouchy",
+ "grounded",
+ "growing",
+ "growling",
+ "grown",
+ "grubby",
+ "gruesome",
+ "grumpy",
+ "guilty",
+ "gullible",
+ "gummy",
+ "hairy",
+ "half",
+ "handmade",
+ "handsome",
+ "handy",
+ "happy",
+ "happy-go-lucky",
+ "hard",
+ "hard-to-find",
+ "harmful",
+ "harmless",
+ "harmonious",
+ "harsh",
+ "hasty",
+ "hateful",
+ "haunting",
+ "healthy",
+ "heartfelt",
+ "hearty",
+ "heavenly",
+ "heavy",
+ "hefty",
+ "helpful",
+ "helpless",
+ "hidden",
+ "hideous",
+ "high",
+ "high-level",
+ "hilarious",
+ "hoarse",
+ "hollow",
+ "homely",
+ "honest",
+ "honorable",
+ "honored",
+ "hopeful",
+ "horrible",
+ "hospitable",
+ "hot",
+ "huge",
+ "humble",
+ "humiliating",
+ "humming",
+ "humongous",
+ "hungry",
+ "hurtful",
+ "husky",
+ "icky",
+ "icy",
+ "ideal",
+ "idealistic",
+ "identical",
+ "idle",
+ "idiotic",
+ "idolized",
+ "ignorant",
+ "ill",
+ "illegal",
+ "ill-fated",
+ "ill-informed",
+ "illiterate",
+ "illustrious",
+ "imaginary",
+ "imaginative",
+ "immaculate",
+ "immaterial",
+ "immediate",
+ "immense",
+ "impassioned",
+ "impeccable",
+ "impartial",
+ "imperfect",
+ "imperturbable",
+ "impish",
+ "impolite",
+ "important",
+ "impossible",
+ "impractical",
+ "impressionable",
+ "impressive",
+ "improbable",
+ "impure",
+ "inborn",
+ "incomparable",
+ "incompatible",
+ "incomplete",
+ "inconsequential",
+ "incredible",
+ "indelible",
+ "inexperienced",
+ "indolent",
+ "infamous",
+ "infantile",
+ "infatuated",
+ "inferior",
+ "infinite",
+ "informal",
+ "innocent",
+ "insecure",
+ "insidious",
+ "insignificant",
+ "insistent",
+ "instructive",
+ "insubstantial",
+ "intelligent",
+ "intent",
+ "intentional",
+ "interesting",
+ "internal",
+ "international",
+ "intrepid",
+ "ironclad",
+ "irresponsible",
+ "irritating",
+ "itchy",
+ "jaded",
+ "jagged",
+ "jam-packed",
+ "jaunty",
+ "jealous",
+ "jittery",
+ "joint",
+ "jolly",
+ "jovial",
+ "joyful",
+ "joyous",
+ "jubilant",
+ "judicious",
+ "juicy",
+ "jumbo",
+ "junior",
+ "jumpy",
+ "juvenile",
+ "kaleidoscopic",
+ "keen",
+ "key",
+ "kind",
+ "kindhearted",
+ "kindly",
+ "klutzy",
+ "knobby",
+ "knotty",
+ "knowledgeable",
+ "knowing",
+ "known",
+ "kooky",
+ "kosher",
+ "lame",
+ "lanky",
+ "large",
+ "last",
+ "lasting",
+ "late",
+ "lavish",
+ "lawful",
+ "lazy",
+ "leading",
+ "lean",
+ "leafy",
+ "left",
+ "legal",
+ "legitimate",
+ "light",
+ "lighthearted",
+ "likable",
+ "likely",
+ "limited",
+ "limp",
+ "limping",
+ "linear",
+ "lined",
+ "liquid",
+ "little",
+ "live",
+ "lively",
+ "livid",
+ "loathsome",
+ "lone",
+ "lonely",
+ "long",
+ "long-term",
+ "loose",
+ "lopsided",
+ "lost",
+ "loud",
+ "lovable",
+ "lovely",
+ "loving",
+ "low",
+ "loyal",
+ "lucky",
+ "lumbering",
+ "luminous",
+ "lumpy",
+ "lustrous",
+ "luxurious",
+ "mad",
+ "made-up",
+ "magnificent",
+ "majestic",
+ "major",
+ "male",
+ "mammoth",
+ "married",
+ "marvelous",
+ "masculine",
+ "massive",
+ "mature",
+ "meager",
+ "mealy",
+ "mean",
+ "measly",
+ "meaty",
+ "medical",
+ "mediocre",
+ "medium",
+ "meek",
+ "mellow",
+ "melodic",
+ "memorable",
+ "menacing",
+ "merry",
+ "messy",
+ "metallic",
+ "mild",
+ "milky",
+ "mindless",
+ "miniature",
+ "minor",
+ "minty",
+ "miserable",
+ "miserly",
+ "misguided",
+ "misty",
+ "mixed",
+ "modern",
+ "modest",
+ "moist",
+ "monstrous",
+ "monthly",
+ "monumental",
+ "moral",
+ "mortified",
+ "motherly",
+ "motionless",
+ "mountainous",
+ "muddy",
+ "muffled",
+ "multicolored",
+ "mundane",
+ "murky",
+ "mushy",
+ "musty",
+ "muted",
+ "mysterious",
+ "naive",
+ "narrow",
+ "nasty",
+ "natural",
+ "naughty",
+ "nautical",
+ "near",
+ "neat",
+ "necessary",
+ "needy",
+ "negative",
+ "neglected",
+ "negligible",
+ "neighboring",
+ "nervous",
+ "new",
+ "next",
+ "nice",
+ "nifty",
+ "nimble",
+ "nippy",
+ "nocturnal",
+ "noisy",
+ "nonstop",
+ "normal",
+ "notable",
+ "noted",
+ "noteworthy",
+ "novel",
+ "noxious",
+ "numb",
+ "nutritious",
+ "nutty",
+ "obedient",
+ "obese",
+ "oblong",
+ "oily",
+ "oblong",
+ "obvious",
+ "occasional",
+ "odd",
+ "oddball",
+ "offbeat",
+ "offensive",
+ "official",
+ "old",
+ "old-fashioned",
+ "only",
+ "open",
+ "optimal",
+ "optimistic",
+ "opulent",
+ "orange",
+ "orderly",
+ "organic",
+ "ornate",
+ "ornery",
+ "ordinary",
+ "original",
+ "other",
+ "our",
+ "outlying",
+ "outgoing",
+ "outlandish",
+ "outrageous",
+ "outstanding",
+ "oval",
+ "overcooked",
+ "overdue",
+ "overjoyed",
+ "overlooked",
+ "palatable",
+ "pale",
+ "paltry",
+ "parallel",
+ "parched",
+ "partial",
+ "passionate",
+ "past",
+ "pastel",
+ "peaceful",
+ "peppery",
+ "perfect",
+ "perfumed",
+ "periodic",
+ "perky",
+ "personal",
+ "pertinent",
+ "pesky",
+ "pessimistic",
+ "petty",
+ "phony",
+ "physical",
+ "piercing",
+ "pink",
+ "pitiful",
+ "plain",
+ "plaintive",
+ "plastic",
+ "playful",
+ "pleasant",
+ "pleased",
+ "pleasing",
+ "plump",
+ "plush",
+ "polished",
+ "polite",
+ "political",
+ "pointed",
+ "pointless",
+ "poised",
+ "poor",
+ "popular",
+ "portly",
+ "posh",
+ "positive",
+ "possible",
+ "potable",
+ "powerful",
+ "powerless",
+ "practical",
+ "precious",
+ "present",
+ "prestigious",
+ "pretty",
+ "precious",
+ "previous",
+ "pricey",
+ "prickly",
+ "primary",
+ "prime",
+ "pristine",
+ "private",
+ "prize",
+ "probable",
+ "productive",
+ "profitable",
+ "profuse",
+ "proper",
+ "proud",
+ "prudent",
+ "punctual",
+ "pungent",
+ "puny",
+ "pure",
+ "purple",
+ "pushy",
+ "putrid",
+ "puzzled",
+ "puzzling",
+ "quaint",
+ "qualified",
+ "quarrelsome",
+ "quarterly",
+ "queasy",
+ "querulous",
+ "questionable",
+ "quick",
+ "quick-witted",
+ "quiet",
+ "quintessential",
+ "quirky",
+ "quixotic",
+ "quizzical",
+ "radiant",
+ "ragged",
+ "rapid",
+ "rare",
+ "rash",
+ "raw",
+ "recent",
+ "reckless",
+ "rectangular",
+ "ready",
+ "real",
+ "realistic",
+ "reasonable",
+ "red",
+ "reflecting",
+ "regal",
+ "regular",
+ "reliable",
+ "relieved",
+ "remarkable",
+ "remorseful",
+ "remote",
+ "repentant",
+ "required",
+ "respectful",
+ "responsible",
+ "repulsive",
+ "revolving",
+ "rewarding",
+ "rich",
+ "rigid",
+ "right",
+ "ringed",
+ "ripe",
+ "roasted",
+ "robust",
+ "rosy",
+ "rotating",
+ "rotten",
+ "rough",
+ "round",
+ "rowdy",
+ "royal",
+ "rubbery",
+ "rundown",
+ "ruddy",
+ "rude",
+ "runny",
+ "rural",
+ "rusty",
+ "sad",
+ "safe",
+ "salty",
+ "same",
+ "sandy",
+ "sane",
+ "sarcastic",
+ "sardonic",
+ "satisfied",
+ "scaly",
+ "scarce",
+ "scared",
+ "scary",
+ "scented",
+ "scholarly",
+ "scientific",
+ "scornful",
+ "scratchy",
+ "scrawny",
+ "second",
+ "secondary",
+ "second-hand",
+ "secret",
+ "self-assured",
+ "self-reliant",
+ "selfish",
+ "sentimental",
+ "separate",
+ "serene",
+ "serious",
+ "serpentine",
+ "several",
+ "severe",
+ "shabby",
+ "shadowy",
+ "shady",
+ "shallow",
+ "shameful",
+ "shameless",
+ "sharp",
+ "shimmering",
+ "shiny",
+ "shocked",
+ "shocking",
+ "shoddy",
+ "short",
+ "short-term",
+ "showy",
+ "shrill",
+ "shy",
+ "sick",
+ "silent",
+ "silky",
+ "silly",
+ "silver",
+ "similar",
+ "simple",
+ "simplistic",
+ "sinful",
+ "single",
+ "sizzling",
+ "skeletal",
+ "skinny",
+ "sleepy",
+ "slight",
+ "slim",
+ "slimy",
+ "slippery",
+ "slow",
+ "slushy",
+ "small",
+ "smart",
+ "smoggy",
+ "smooth",
+ "smug",
+ "snappy",
+ "snarling",
+ "sneaky",
+ "sniveling",
+ "snoopy",
+ "sociable",
+ "soft",
+ "soggy",
+ "solid",
+ "somber",
+ "some",
+ "spherical",
+ "sophisticated",
+ "sore",
+ "sorrowful",
+ "soulful",
+ "soupy",
+ "sour",
+ "Spanish",
+ "sparkling",
+ "sparse",
+ "specific",
+ "spectacular",
+ "speedy",
+ "spicy",
+ "spiffy",
+ "spirited",
+ "spiteful",
+ "splendid",
+ "spotless",
+ "spotted",
+ "spry",
+ "square",
+ "squeaky",
+ "squiggly",
+ "stable",
+ "staid",
+ "stained",
+ "stale",
+ "standard",
+ "starchy",
+ "stark",
+ "starry",
+ "steep",
+ "sticky",
+ "stiff",
+ "stimulating",
+ "stingy",
+ "stormy",
+ "straight",
+ "strange",
+ "steel",
+ "strict",
+ "strident",
+ "striking",
+ "striped",
+ "strong",
+ "studious",
+ "stunning",
+ "stupendous",
+ "stupid",
+ "sturdy",
+ "stylish",
+ "subdued",
+ "submissive",
+ "substantial",
+ "subtle",
+ "suburban",
+ "sudden",
+ "sugary",
+ "sunny",
+ "super",
+ "superb",
+ "superficial",
+ "superior",
+ "supportive",
+ "sure-footed",
+ "surprised",
+ "suspicious",
+ "svelte",
+ "sweaty",
+ "sweet",
+ "sweltering",
+ "swift",
+ "sympathetic",
+ "tall",
+ "talkative",
+ "tame",
+ "tan",
+ "tangible",
+ "tart",
+ "tasty",
+ "tattered",
+ "taut",
+ "tedious",
+ "teeming",
+ "tempting",
+ "tender",
+ "tense",
+ "tepid",
+ "terrible",
+ "terrific",
+ "testy",
+ "thankful",
+ "that",
+ "these",
+ "thick",
+ "thin",
+ "third",
+ "thirsty",
+ "this",
+ "thorough",
+ "thorny",
+ "those",
+ "thoughtful",
+ "threadbare",
+ "thrifty",
+ "thunderous",
+ "tidy",
+ "tight",
+ "timely",
+ "tinted",
+ "tiny",
+ "tired",
+ "torn",
+ "total",
+ "tough",
+ "traumatic",
+ "treasured",
+ "tremendous",
+ "tragic",
+ "trained",
+ "tremendous",
+ "triangular",
+ "tricky",
+ "trifling",
+ "trim",
+ "trivial",
+ "troubled",
+ "true",
+ "trusting",
+ "trustworthy",
+ "trusty",
+ "truthful",
+ "tubby",
+ "turbulent",
+ "twin",
+ "ugly",
+ "ultimate",
+ "unacceptable",
+ "unaware",
+ "uncomfortable",
+ "uncommon",
+ "unconscious",
+ "understated",
+ "unequaled",
+ "uneven",
+ "unfinished",
+ "unfit",
+ "unfolded",
+ "unfortunate",
+ "unhappy",
+ "unhealthy",
+ "uniform",
+ "unimportant",
+ "unique",
+ "united",
+ "unkempt",
+ "unknown",
+ "unlawful",
+ "unlined",
+ "unlucky",
+ "unnatural",
+ "unpleasant",
+ "unrealistic",
+ "unripe",
+ "unruly",
+ "unselfish",
+ "unsightly",
+ "unsteady",
+ "unsung",
+ "untidy",
+ "untimely",
+ "untried",
+ "untrue",
+ "unused",
+ "unusual",
+ "unwelcome",
+ "unwieldy",
+ "unwilling",
+ "unwitting",
+ "unwritten",
+ "upbeat",
+ "upright",
+ "upset",
+ "urban",
+ "usable",
+ "used",
+ "useful",
+ "useless",
+ "utilized",
+ "utter",
+ "vacant",
+ "vague",
+ "vain",
+ "valid",
+ "valuable",
+ "vapid",
+ "variable",
+ "vast",
+ "velvety",
+ "venerated",
+ "vengeful",
+ "verifiable",
+ "vibrant",
+ "vicious",
+ "victorious",
+ "vigilant",
+ "vigorous",
+ "villainous",
+ "violet",
+ "violent",
+ "virtual",
+ "virtuous",
+ "visible",
+ "vital",
+ "vivacious",
+ "vivid",
+ "voluminous",
+ "wan",
+ "warlike",
+ "warm",
+ "warmhearted",
+ "warped",
+ "wary",
+ "wasteful",
+ "watchful",
+ "waterlogged",
+ "watery",
+ "wavy",
+ "wealthy",
+ "weak",
+ "weary",
+ "webbed",
+ "weed",
+ "weekly",
+ "weepy",
+ "weighty",
+ "weird",
+ "welcome",
+ "well-documented",
+ "well-groomed",
+ "well-informed",
+ "well-lit",
+ "well-made",
+ "well-off",
+ "well-to-do",
+ "well-worn",
+ "wet",
+ "which",
+ "whimsical",
+ "whirlwind",
+ "whispered",
+ "white",
+ "whole",
+ "whopping",
+ "wicked",
+ "wide",
+ "wide-eyed",
+ "wiggly",
+ "wild",
+ "willing",
+ "wilted",
+ "winding",
+ "windy",
+ "winged",
+ "wiry",
+ "wise",
+ "witty",
+ "wobbly",
+ "woeful",
+ "wonderful",
+ "wooden",
+ "woozy",
+ "wordy",
+ "worldly",
+ "worn",
+ "worried",
+ "worrisome",
+ "worse",
+ "worst",
+ "worthless",
+ "worthwhile",
+ "worthy",
+ "wrathful",
+ "wretched",
+ "writhing",
+ "wrong",
+ "wry",
+ "yawning",
+ "yearly",
+ "yellow",
+ "yellowish",
+ "young",
+ "youthful",
+ "yummy",
+ "zany",
+ "zealous",
+ "zesty",
+ "zigzag",
+];
+
+export function getRandomUsername(): { first: string; second: string } {
+ const n = Math.floor(Math.random() * noun.length);
+ const a = Math.floor(Math.random() * adj.length);
+ return {
+ first: adj[a],
+ second: noun[n],
+ };
+}
+
+export function getRandomPassword(): string {
+ return encodeCrock(getRandomBytes(16));
+}
diff --git a/packages/bank-ui/src/scss/main.css b/packages/bank-ui/src/scss/main.css
new file mode 100644
index 000000000..b5c61c956
--- /dev/null
+++ b/packages/bank-ui/src/scss/main.css
@@ -0,0 +1,3 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
diff --git a/packages/bank-ui/src/settings.json b/packages/bank-ui/src/settings.json
new file mode 100644
index 000000000..df5fe75ce
--- /dev/null
+++ b/packages/bank-ui/src/settings.json
@@ -0,0 +1,11 @@
+{
+ "backendBaseURL": "http://bank.taler.test:1180/",
+ "simplePasswordForRandomAccounts": true,
+ "allowRandomAccountCreation": true,
+ "bankName": "Taler DEVELOPMENT Bank",
+ "topNavSites": {
+ "Exchange": "http://Exchnage.taler.test:1180/",
+ "Bank": "http://bank-ui.taler.test:1180/",
+ "Merchant": "http://merchant.taler.test:1180/"
+ }
+}
diff --git a/packages/bank-ui/src/settings.ts b/packages/bank-ui/src/settings.ts
new file mode 100644
index 000000000..968fe6248
--- /dev/null
+++ b/packages/bank-ui/src/settings.ts
@@ -0,0 +1,112 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ Codec,
+ buildCodecForObject,
+ canonicalizeBaseUrl,
+ codecForBoolean,
+ codecForMap,
+ codecForString,
+ codecOptional,
+} from "@gnu-taler/taler-util";
+
+export interface BankUiSettings {
+ // Where libeufin backend is localted
+ // default: window.origin without "webui/"
+ backendBaseURL?: string;
+ // Shows a button "create random account" in the registration form
+ // Useful for testing
+ // default: false
+ allowRandomAccountCreation?: boolean;
+ // Create all random accounts with password "123"
+ // Useful for testing
+ // default: false
+ simplePasswordForRandomAccounts?: boolean;
+ // URL where the user is going to be redirected after
+ // clicking in Taler Logo
+ // default: home page
+ iconLinkURL?: string;
+ // Mapping for every link shown in the top navitation bar
+ // - key: link label, what the user will read
+ // - value: link target, where the user is going to be redirected
+ // default: empty list
+ topNavSites?: Record<string, string>;
+}
+
+/**
+ * Global settings for the bank UI.
+ */
+const defaultSettings: BankUiSettings = {
+ backendBaseURL: buildDefaultBackendBaseURL(),
+ iconLinkURL: undefined,
+ simplePasswordForRandomAccounts: false,
+ allowRandomAccountCreation: false,
+ topNavSites: {},
+};
+
+const codecForBankUISettings = (): Codec<BankUiSettings> =>
+ buildCodecForObject<BankUiSettings>()
+ .property("backendBaseURL", codecOptional(codecForString()))
+ .property("allowRandomAccountCreation", codecOptional(codecForBoolean()))
+ .property(
+ "simplePasswordForRandomAccounts",
+ codecOptional(codecForBoolean()),
+ )
+ .property("iconLinkURL", codecOptional(codecForString()))
+ .property("topNavSites", codecOptional(codecForMap(codecForString())))
+ .build("BankUiSettings");
+
+function removeUndefineField<T extends object>(obj: T): T {
+ const keys = Object.keys(obj) as Array<keyof T>;
+ return keys.reduce((prev, cur) => {
+ if (typeof prev[cur] === "undefined") {
+ delete prev[cur];
+ }
+ return prev;
+ }, obj);
+}
+
+export function fetchSettings(listener: (s: BankUiSettings) => void): void {
+ fetch("./settings.json")
+ .then((resp) => resp.json())
+ .then((json) => codecForBankUISettings().decode(json))
+ .then((result) =>
+ listener({
+ ...defaultSettings,
+ ...removeUndefineField(result),
+ }),
+ )
+ .catch((e) => {
+ console.log("failed to fetch settings", e);
+ listener(defaultSettings);
+ });
+}
+
+function buildDefaultBackendBaseURL(): string | undefined {
+ if (typeof window !== "undefined") {
+ const currentLocation = new URL(
+ window.location.pathname,
+ window.location.origin,
+ ).href;
+ /**
+ * By default, bank backend serves the html content
+ * from the /webui root.
+ */
+ return canonicalizeBaseUrl(currentLocation.replace("/webui", ""));
+ }
+ throw Error("No default URL");
+}
diff --git a/packages/bank-ui/src/stories.test.ts b/packages/bank-ui/src/stories.test.ts
new file mode 100644
index 000000000..921f9f9ea
--- /dev/null
+++ b/packages/bank-ui/src/stories.test.ts
@@ -0,0 +1,83 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import {
+ AmountString,
+ TalerCorebankApi,
+ setupI18n,
+} from "@gnu-taler/taler-util";
+import {
+ BankApiProviderTesting,
+ parseGroupImport,
+} from "@gnu-taler/web-util/browser";
+import * as tests from "@gnu-taler/web-util/testing";
+import * as components from "./components/index.examples.js";
+import * as pages from "./pages/index.stories.js";
+
+import { ComponentChildren, VNode, h as create } from "preact";
+// import { BankCoreApiProviderTesting } from "./context/config.js";
+
+setupI18n("en", { en: {} });
+
+describe("All the examples:", () => {
+ const cms = parseGroupImport({ pages, components });
+ 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, DefaultTestingContext);
+ });
+ });
+ });
+ });
+ });
+ });
+});
+
+function DefaultTestingContext(_props: { children: ComponentChildren }): VNode {
+ const cfg: TalerCorebankApi.Config = {
+ name: "libeufin-bank",
+ allow_deletions: true,
+ bank_name: "taler bank",
+ wire_type: "wire t",
+ supported_tan_channels: [],
+ allow_registrations: true,
+ allow_conversion: true,
+ allow_edit_cashout_payto_uri: false,
+ allow_edit_name: false,
+ currency: "ASR",
+ currency_specification: {
+ name: "ARS",
+ alt_unit_names: {},
+ num_fractional_input_digits: 2,
+ num_fractional_normal_digits: 2,
+ num_fractional_trailing_zero_digits: 2,
+ },
+ default_debit_threshold: "ARS:10" as AmountString,
+ version: "1:0:0",
+ };
+ const ctx2 = create(BankApiProviderTesting, {
+ children: [],
+ value: cfg as any,
+ });
+ return ctx2;
+}
diff --git a/packages/bank-ui/src/stories.tsx b/packages/bank-ui/src/stories.tsx
new file mode 100644
index 000000000..8342a8434
--- /dev/null
+++ b/packages/bank-ui/src/stories.tsx
@@ -0,0 +1,41 @@
+/*
+ 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/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { strings } from "./i18n/strings.js";
+
+import * as pages from "./pages/index.stories.js";
+import * as components from "./components/index.examples.js";
+
+import { renderStories } from "@gnu-taler/web-util/browser";
+
+function main(): void {
+ renderStories(
+ { pages, components },
+ {
+ strings,
+ },
+ );
+}
+
+if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", main);
+} else {
+ main();
+}
diff --git a/packages/bank-ui/src/utils.ts b/packages/bank-ui/src/utils.ts
new file mode 100644
index 000000000..2cc502416
--- /dev/null
+++ b/packages/bank-ui/src/utils.ts
@@ -0,0 +1,439 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ AbsoluteTime,
+ AmountString,
+ PaytoString,
+ TalerError,
+ TalerErrorCode,
+ TranslatedString,
+} from "@gnu-taler/taler-util";
+import {
+ ErrorNotification,
+ InternationalizationAPI,
+ notify,
+ notifyError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+
+/**
+ * Validate (the number part of) an amount. If needed,
+ * replace comma with a dot. Returns 'false' whenever
+ * the input is invalid, the valid amount otherwise.
+ */
+const amountRegex = /^[0-9]+(.[0-9]+)?$/;
+export function validateAmount(
+ maybeAmount: string | undefined,
+): string | undefined {
+ if (!maybeAmount || !amountRegex.test(maybeAmount)) {
+ return;
+ }
+ return maybeAmount;
+}
+
+/**
+ * Extract IBAN from a Payto URI.
+ */
+export function getIbanFromPayto(url: string): string {
+ const pathSplit = new URL(url).pathname.split("/");
+ let lastIndex = pathSplit.length - 1;
+ // Happens if the path ends with "/".
+ if (pathSplit[lastIndex] === "") lastIndex--;
+ const iban = pathSplit[lastIndex];
+ return iban;
+}
+
+export function undefinedIfEmpty<T extends object>(obj: T): T | undefined {
+ return Object.keys(obj).some(
+ (k) => (obj as Record<string, T>)[k] !== undefined,
+ )
+ ? obj
+ : undefined;
+}
+
+export type PartialButDefined<T> = {
+ [P in keyof T]: T[P] | undefined;
+};
+
+/**
+ * every non-map field can be undefined
+ */
+export type WithIntermediate<Type> = {
+ [prop in keyof Type]: Type[prop] extends PaytoString
+ ? Type[prop] | undefined
+ : Type[prop] extends AmountString
+ ? Type[prop] | undefined
+ : Type[prop] extends TranslatedString
+ ? Type[prop] | undefined
+ : Type[prop] extends object
+ ? WithIntermediate<Type[prop]>
+ : Type[prop] | undefined;
+};
+export type RecursivePartial<Type> = {
+ [P in keyof Type]?: Type[P] extends (infer U)[]
+ ? RecursivePartial<U>[]
+ : Type[P] extends object
+ ? RecursivePartial<Type[P]>
+ : Type[P];
+};
+export type ErrorMessageMappingFor<Type> = {
+ [prop in keyof Type]+?: Exclude<Type[prop], undefined> extends PaytoString // enumerate known object
+ ? TranslatedString
+ : Exclude<Type[prop], undefined> extends AmountString
+ ? TranslatedString
+ : Exclude<Type[prop], undefined> extends TranslatedString
+ ? TranslatedString
+ : // arrays: every element
+ Exclude<Type[prop], undefined> extends (infer U)[]
+ ? ErrorMessageMappingFor<U>[]
+ : // map: every field
+ Exclude<Type[prop], undefined> extends object
+ ? ErrorMessageMappingFor<Type[prop]>
+ : TranslatedString;
+};
+
+export enum TanChannel {
+ SMS = "sms",
+ EMAIL = "email",
+}
+export enum CashoutStatus {
+ // The payment was initiated after a valid
+ // TAN was received by the bank.
+ CONFIRMED = "confirmed",
+
+ // The cashout was created and now waits
+ // for the TAN by the author.
+ PENDING = "pending",
+}
+
+
+export const PAGINATED_LIST_SIZE = 5;
+// when doing paginated request, ask for one more
+// and use it to know if there are more to request
+export const PAGINATED_LIST_REQUEST = PAGINATED_LIST_SIZE + 1;
+
+type Translator = ReturnType<typeof useTranslationContext>["i18n"];
+
+export async function withRuntimeErrorHandling<T>(
+ i18n: Translator,
+ cb: () => Promise<T>,
+): Promise<void> {
+ try {
+ await cb();
+ } catch (error: unknown) {
+ if (error instanceof TalerError) {
+ notify(buildRequestErrorMessage(i18n, error));
+ } else {
+ notifyError(
+ i18n.str`Operation failed, please report`,
+ (error instanceof Error
+ ? error.message
+ : JSON.stringify(error)) as TranslatedString,
+ );
+ }
+ }
+}
+
+export function buildRequestErrorMessage(
+ i18n: Translator,
+ cause: TalerError,
+): ErrorNotification {
+ let result: ErrorNotification;
+ switch (cause.errorDetail.code) {
+ case TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT: {
+ result = {
+ type: "error",
+ title: i18n.str`Request timeout`,
+ description: cause.message as TranslatedString,
+ debug: JSON.stringify(cause.errorDetail, undefined, 2),
+ when: AbsoluteTime.now(),
+ };
+ break;
+ }
+ case TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED: {
+ result = {
+ type: "error",
+ title: i18n.str`Request throttled`,
+ description: cause.message as TranslatedString,
+ debug: JSON.stringify(cause.errorDetail, undefined, 2),
+ when: AbsoluteTime.now(),
+ };
+ break;
+ }
+ case TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE: {
+ result = {
+ type: "error",
+ title: i18n.str`Malformed response`,
+ description: cause.message as TranslatedString,
+ debug: JSON.stringify(cause.errorDetail, undefined, 2),
+ when: AbsoluteTime.now(),
+ };
+ break;
+ }
+ case TalerErrorCode.WALLET_NETWORK_ERROR: {
+ result = {
+ type: "error",
+ title: i18n.str`Network error`,
+ description: cause.message as TranslatedString,
+ debug: JSON.stringify(cause.errorDetail, undefined, 2),
+ when: AbsoluteTime.now(),
+ };
+ break;
+ }
+ case TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR: {
+ result = {
+ type: "error",
+ title: i18n.str`Unexpected request error`,
+ description: cause.message as TranslatedString,
+ debug: JSON.stringify(cause.errorDetail, undefined, 2),
+ when: AbsoluteTime.now(),
+ };
+ break;
+ }
+ default: {
+ result = {
+ type: "error",
+ title: i18n.str`Unexpected error`,
+ description: cause.message as TranslatedString,
+ debug: JSON.stringify(cause.errorDetail, undefined, 2),
+ when: AbsoluteTime.now(),
+ };
+ break;
+ }
+ }
+ return result;
+}
+
+export const COUNTRY_TABLE = {
+ AE: "U.A.E.",
+ AF: "Afghanistan",
+ AL: "Albania",
+ AM: "Armenia",
+ AN: "Netherlands Antilles",
+ AR: "Argentina",
+ AT: "Austria",
+ AU: "Australia",
+ AZ: "Azerbaijan",
+ BA: "Bosnia and Herzegovina",
+ BD: "Bangladesh",
+ BE: "Belgium",
+ BG: "Bulgaria",
+ BH: "Bahrain",
+ BN: "Brunei Darussalam",
+ BO: "Bolivia",
+ BR: "Brazil",
+ BT: "Bhutan",
+ BY: "Belarus",
+ BZ: "Belize",
+ CA: "Canada",
+ CG: "Congo",
+ CH: "Switzerland",
+ CI: "Cote d'Ivoire",
+ CL: "Chile",
+ CM: "Cameroon",
+ CN: "People's Republic of China",
+ CO: "Colombia",
+ CR: "Costa Rica",
+ CS: "Serbia and Montenegro",
+ CZ: "Czech Republic",
+ DE: "Germany",
+ DK: "Denmark",
+ DO: "Dominican Republic",
+ DZ: "Algeria",
+ EC: "Ecuador",
+ EE: "Estonia",
+ EG: "Egypt",
+ ER: "Eritrea",
+ ES: "Spain",
+ ET: "Ethiopia",
+ FI: "Finland",
+ FO: "Faroe Islands",
+ FR: "France",
+ GB: "United Kingdom",
+ GD: "Caribbean",
+ GE: "Georgia",
+ GL: "Greenland",
+ GR: "Greece",
+ GT: "Guatemala",
+ HK: "Hong Kong",
+ // HK: "Hong Kong S.A.R.",
+ HN: "Honduras",
+ HR: "Croatia",
+ HT: "Haiti",
+ HU: "Hungary",
+ ID: "Indonesia",
+ IE: "Ireland",
+ IL: "Israel",
+ IN: "India",
+ IQ: "Iraq",
+ IR: "Iran",
+ IS: "Iceland",
+ IT: "Italy",
+ JM: "Jamaica",
+ JO: "Jordan",
+ JP: "Japan",
+ KE: "Kenya",
+ KG: "Kyrgyzstan",
+ KH: "Cambodia",
+ KR: "South Korea",
+ KW: "Kuwait",
+ KZ: "Kazakhstan",
+ LA: "Laos",
+ LB: "Lebanon",
+ LI: "Liechtenstein",
+ LK: "Sri Lanka",
+ LT: "Lithuania",
+ LU: "Luxembourg",
+ LV: "Latvia",
+ LY: "Libya",
+ MA: "Morocco",
+ MC: "Principality of Monaco",
+ MD: "Moldava",
+ // MD: "Moldova",
+ ME: "Montenegro",
+ MK: "Former Yugoslav Republic of Macedonia",
+ ML: "Mali",
+ MM: "Myanmar",
+ MN: "Mongolia",
+ MO: "Macau S.A.R.",
+ MT: "Malta",
+ MV: "Maldives",
+ MX: "Mexico",
+ MY: "Malaysia",
+ NG: "Nigeria",
+ NI: "Nicaragua",
+ NL: "Netherlands",
+ NO: "Norway",
+ NP: "Nepal",
+ NZ: "New Zealand",
+ OM: "Oman",
+ PA: "Panama",
+ PE: "Peru",
+ PH: "Philippines",
+ PK: "Islamic Republic of Pakistan",
+ PL: "Poland",
+ PR: "Puerto Rico",
+ PT: "Portugal",
+ PY: "Paraguay",
+ QA: "Qatar",
+ RE: "Reunion",
+ RO: "Romania",
+ RS: "Serbia",
+ RU: "Russia",
+ RW: "Rwanda",
+ SA: "Saudi Arabia",
+ SE: "Sweden",
+ SG: "Singapore",
+ SI: "Slovenia",
+ SK: "Slovak",
+ SN: "Senegal",
+ SO: "Somalia",
+ SR: "Suriname",
+ SV: "El Salvador",
+ SY: "Syria",
+ TH: "Thailand",
+ TJ: "Tajikistan",
+ TM: "Turkmenistan",
+ TN: "Tunisia",
+ TR: "Turkey",
+ TT: "Trinidad and Tobago",
+ TW: "Taiwan",
+ TZ: "Tanzania",
+ UA: "Ukraine",
+ US: "United States",
+ UY: "Uruguay",
+ VA: "Vatican",
+ VE: "Venezuela",
+ VN: "Viet Nam",
+ YE: "Yemen",
+ ZA: "South Africa",
+ ZW: "Zimbabwe",
+};
+
+/**
+ * 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.
+ *
+ */
+const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/;
+export function validateIBAN(
+ account: string,
+ i18n: InternationalizationAPI,
+): TranslatedString | undefined {
+ if (!IBAN_REGEX.test(account)) {
+ return i18n.str`IBAN only have uppercased letters and numbers`;
+ }
+ // Check total length
+ if (account.length < 4) return i18n.str`IBAN numbers have more that 4 digits`;
+ if (account.length > 34)
+ return i18n.str`IBAN numbers have less that 34 digits`;
+
+ const A_code = "A".charCodeAt(0);
+ const Z_code = "Z".charCodeAt(0);
+ const IBAN = account.toUpperCase();
+ // check supported country
+ const code = IBAN.substring(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.substring(4) + account.substring(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("");
+
+ const checksum = calculate_iban_checksum(step3);
+ if (checksum !== 1)
+ return i18n.str`IBAN number is not valid, checksum is wrong`;
+ return undefined;
+}
+
+function calculate_iban_checksum(str: string): number {
+ const numberStr = str.substring(0, 5);
+ const rest = str.substring(5);
+ const number = parseInt(numberStr, 10);
+ const result = number % 97;
+ if (rest.length > 0) {
+ return calculate_iban_checksum(`${result}${rest}`);
+ }
+ return result;
+}
+
+const USERNAME_REGEX = /^[A-Za-z][A-Za-z0-9]*$/;
+
+export function validateTalerBank(
+ account: string,
+ i18n: InternationalizationAPI,
+): TranslatedString | undefined {
+ if (!USERNAME_REGEX.test(account)) {
+ return i18n.str`Account only have letters and numbers`;
+ }
+ return undefined;
+}
diff --git a/packages/bank-ui/tailwind.config.js b/packages/bank-ui/tailwind.config.js
new file mode 100644
index 000000000..d384690e2
--- /dev/null
+++ b/packages/bank-ui/tailwind.config.js
@@ -0,0 +1,28 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+export default {
+ content: {
+ relative: true,
+ files: [
+ "./src/**/*.{html,tsx}",
+ "./node_modules/@gnu-taler/web-util/src/**/*.{html,tsx}"
+ ],
+ },
+ theme: {
+ extend: {},
+ },
+ plugins: [require("@tailwindcss/typography"), require("@tailwindcss/forms")],
+};
diff --git a/packages/bank-ui/test.mjs b/packages/bank-ui/test.mjs
new file mode 100755
index 000000000..baaaaa3ef
--- /dev/null
+++ b/packages/bank-ui/test.mjs
@@ -0,0 +1,32 @@
+#!/usr/bin/env node
+/*
+ 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 { build } from "@gnu-taler/web-util/build";
+import { getFilesInDirectory } from "@gnu-taler/web-util/build";
+
+const allTestFiles = getFilesInDirectory("src", /.test.tsx?$/);
+
+await build({
+ type: "test",
+ source: {
+ js: allTestFiles.files,
+ assets: [{ base: "src", files: ["src/index.html"] }],
+
+ },
+ destination: "./dist/test",
+ css: "sass",
+});
diff --git a/packages/bank-ui/tsconfig.json b/packages/bank-ui/tsconfig.json
new file mode 100644
index 000000000..9826fac07
--- /dev/null
+++ b/packages/bank-ui/tsconfig.json
@@ -0,0 +1,46 @@
+{
+ "compilerOptions": {
+ /* Basic Options */
+ "target": "ES2020",
+ "module": "Node16",
+ "lib": ["DOM", "ES2020"],
+ "allowJs": true /* Allow javascript files to be compiled. */,
+ // "checkJs": true, /* Report errors in .js files. */
+ "jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */,
+ "jsxFactory": "h",
+ "jsxFragmentFactory": "Fragment",
+ "noEmit": true /* Do not emit outputs. */,
+ // "importHelpers": true, /* Import emit helpers from 'tslib'. */
+ // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
+ // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
+ /* Strict Type-Checking Options */
+ "strict": true /* Enable all strict type-checking options. */,
+ "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
+ /* Additional Checks */
+ // "noUnusedLocals": true, /* Report errors on unused locals. */
+ // "noUnusedParameters": true, /* Report errors on unused parameters. */
+ // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
+ // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
+ /* Module Resolution Options */
+ "moduleResolution": "Node16" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
+ "esModuleInterop": true /* */,
+ // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
+ // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
+ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
+ // "typeRoots": [], /* List of folders to include type definitions from. */
+ // "types": [], /* Type declaration files to be included in compilation. */
+ "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */,
+ // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
+ /* Source Map Options */
+ // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
+ // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */
+ // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
+ // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
+ /* Experimental Options */
+ // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
+ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
+ /* Advanced Options */
+ "skipLibCheck": true /* Skip type checking of declaration files. */
+ },
+ "include": ["src/**/*"]
+}
diff --git a/packages/challenger-ui/.gitignore b/packages/challenger-ui/.gitignore
new file mode 100644
index 000000000..30cb2774c
--- /dev/null
+++ b/packages/challenger-ui/.gitignore
@@ -0,0 +1,4 @@
+node_modules
+/build
+/*.log
+/demobank-ui-settings.js
diff --git a/packages/challenger-ui/Makefile b/packages/challenger-ui/Makefile
new file mode 100644
index 000000000..64f9f83d1
--- /dev/null
+++ b/packages/challenger-ui/Makefile
@@ -0,0 +1,36 @@
+# This Makefile has been placed in the public domain
+
+ifeq ($(TOPLEVEL), yes)
+ $(info top-level build)
+ -include ../../.config.mk
+ override DESTDIR := $(TOP_DESTDIR)
+else
+ $(info package-level build)
+ -include ../../.config.mk
+ -include .config.mk
+endif
+
+$(info prefix is $(prefix))
+
+.PHONY: all
+all:
+ @echo run \'make install\' to install
+
+spa_dir=$(DESTDIR)$(prefix)/share/taler/aml-backoffice-ui
+
+.PHONY: install-nodeps
+install-nodeps:
+ install -d $(spa_dir)
+ install ./dist/prod/* $(spa_dir)
+
+.PHONY: deps
+deps:
+ pnpm install --frozen-lockfile --filter @gnu-taler/aml-backoffice-ui...
+ pnpm run check
+ pnpm run build
+
+.PHONY: install
+install:
+ $(MAKE) deps
+ $(MAKE) install-nodeps
+
diff --git a/packages/challenger-ui/README.md b/packages/challenger-ui/README.md
new file mode 100644
index 000000000..ac589ace6
--- /dev/null
+++ b/packages/challenger-ui/README.md
@@ -0,0 +1,4 @@
+# Challenger Backoffice UI
+
+Web-based user interface for the GNU Challenger.
+
diff --git a/packages/challenger-ui/build.mjs b/packages/challenger-ui/build.mjs
new file mode 100755
index 000000000..166647f79
--- /dev/null
+++ b/packages/challenger-ui/build.mjs
@@ -0,0 +1,43 @@
+#!/usr/bin/env node
+/*
+ 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 { build } from "@gnu-taler/web-util/build";
+
+await build({
+ type: "production",
+ source: {
+ js: ["src/main.js","src/index.tsx"],
+ assets: [{
+ base: "src",
+ files: [
+ "src/index.html",
+ "src/attempts-exhausted.html",
+ "src/enter-address-form.html",
+ "src/enter-email-form.html",
+ "src/enter-file-access-form.html",
+ "src/enter-phone-form.html",
+ "src/enter-tan-form.html",
+ "src/internal-error.html",
+ "src/invalid-pin.html",
+ "src/invalid-request.html",
+ "src/validation-unknown.html",
+ ]
+ }],
+ },
+ destination: "./dist/prod",
+ css: "postcss",
+});
diff --git a/packages/challenger-ui/copyleft-header.js b/packages/challenger-ui/copyleft-header.js
new file mode 100644
index 000000000..7fa276bea
--- /dev/null
+++ b/packages/challenger-ui/copyleft-header.js
@@ -0,0 +1,15 @@
+/*
+ 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/>
+ */
diff --git a/packages/challenger-ui/create_must.sh b/packages/challenger-ui/create_must.sh
new file mode 100755
index 000000000..a4d78b2cc
--- /dev/null
+++ b/packages/challenger-ui/create_must.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+# This file is in the public domain.
+
+# After the compilation succeeded
+# some changes needs to be made
+# in the html/js files to match the
+# what the service expects
+
+cd dist/prod
+
+for file in *.html; do
+
+ # 1. remove the js reference used for dev
+ sed /main.js/d -i $file
+
+ # 2. change the location of css since
+ #challenger backend wants them in the root path
+ sed 's/="main.css"/="..\/main.css"/' -i $file
+
+ # 3. rename the extension to must template
+ mv $file ${file:0:-5}.en.must
+done
+
+#delete unused files
+rm *.js *.map
diff --git a/packages/challenger-ui/dev.mjs b/packages/challenger-ui/dev.mjs
new file mode 100755
index 000000000..595c3e99e
--- /dev/null
+++ b/packages/challenger-ui/dev.mjs
@@ -0,0 +1,56 @@
+#!/usr/bin/env node
+/*
+ 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 { serve } from "@gnu-taler/web-util/node";
+import { initializeDev } from "@gnu-taler/web-util/build";
+
+const devEntryPoints = ["src/index.tsx", "src/main.js"];
+
+const build = initializeDev({
+ type: "development",
+ source: {
+ js: devEntryPoints,
+ assets: [{
+ base: "src",
+ files: [
+ "src/index.html",
+ "src/attempts-exhausted.html",
+ "src/enter-address-form.html",
+ "src/enter-email-form.html",
+ "src/enter-file-access-form.html",
+ "src/enter-phone-form.html",
+ "src/enter-tan-form.html",
+ "src/internal-error.html",
+ "src/invalid-pin.html",
+ "src/invalid-request.html",
+ "src/validation-unknown.html",
+ ]
+ }],
+ },
+ destination: "./dist/dev",
+ public: "/app",
+ css: "postcss",
+});
+
+await build();
+
+serve({
+ folder: "./dist/dev",
+ port: 8080,
+ source: "./src",
+ onSourceUpdate: build,
+});
diff --git a/packages/challenger-ui/package.json b/packages/challenger-ui/package.json
new file mode 100644
index 000000000..8234e2385
--- /dev/null
+++ b/packages/challenger-ui/package.json
@@ -0,0 +1,67 @@
+{
+ "private": true,
+ "name": "@gnu-taler/challenger-ui",
+ "version": "0.10.7",
+ "author": "sebasjm",
+ "license": "AGPL-3.0-OR-LATER",
+ "description": "UI for GNU Challenger.",
+ "type": "module",
+ "scripts": {
+ "build": "./build.mjs && ./create_must.sh",
+ "check": "tsc",
+ "compile": "tsc && ./build.mjs",
+ "test": "./test.mjs && mocha --require source-map-support/register 'dist/test/**/*.test.js' 'dist/test/**/test.js'",
+ "lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
+ "clean": "rm -rf dist lib tsconfig.tsbuildinfo",
+ "i18n:strings": "pogen extract && pogen merge",
+ "i18n:translations": "pogen emit",
+ "pretty": "prettier --write src"
+ },
+ "eslintConfig": {
+ "plugins": [
+ "header"
+ ],
+ "rules": {
+ "header/header": [
+ 2,
+ "copyleft-header.js"
+ ]
+ },
+ "extends": [
+ "prettier"
+ ]
+ },
+ "devDependencies": {
+ "eslint": "^8.56.0",
+ "@typescript-eslint/eslint-plugin": "^6.19.0",
+ "@typescript-eslint/parser": "^6.19.0",
+ "eslint-config-prettier": "^9.1.0",
+ "eslint-plugin-react": "^7.33.2",
+ "@gnu-taler/pogen": "workspace:*",
+ "@tailwindcss/forms": "^0.5.3",
+ "@tailwindcss/typography": "^0.5.9",
+ "@types/chai": "^4.3.0",
+ "@types/history": "^4.7.8",
+ "@types/mocha": "^10.0.1",
+ "@types/node": "^18.11.17",
+ "autoprefixer": "^10.4.14",
+ "chai": "^4.3.6",
+ "esbuild": "^0.19.9",
+ "mocha": "9.2.0",
+ "po2json": "^0.4.5",
+ "tailwindcss": "^3.3.2",
+ "typescript": "5.3.3"
+ },
+ "pogen": {
+ "domain": "challenger-ui"
+ },
+ "dependencies": {
+ "swr": "2.0.3",
+ "@gnu-taler/taler-util": "workspace:*",
+ "@gnu-taler/web-util": "workspace:*",
+ "date-fns": "2.29.3",
+ "jed": "1.1.1",
+ "qrcode-generator": "^1.4.4",
+ "preact": "10.11.3"
+ }
+}
diff --git a/packages/challenger-ui/postcss.config.js b/packages/challenger-ui/postcss.config.js
new file mode 100644
index 000000000..c9a60a43c
--- /dev/null
+++ b/packages/challenger-ui/postcss.config.js
@@ -0,0 +1,21 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/packages/challenger-ui/src/Routing.tsx b/packages/challenger-ui/src/Routing.tsx
new file mode 100644
index 000000000..6166f159a
--- /dev/null
+++ b/packages/challenger-ui/src/Routing.tsx
@@ -0,0 +1,270 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ Loading,
+ urlPattern,
+ useCurrentLocation,
+ useNavigationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+
+import { assertUnreachable } from "@gnu-taler/taler-util";
+import { CheckChallengeIsUpToDate } from "./components/CheckChallengeIsUpToDate.js";
+import { SessionId, useSessionState } from "./hooks/session.js";
+import { AnswerChallenge } from "./pages/AnswerChallenge.js";
+import { AskChallenge } from "./pages/AskChallenge.js";
+import { CallengeCompleted } from "./pages/CallengeCompleted.js";
+import { Frame } from "./pages/Frame.js";
+import { MissingParams } from "./pages/MissingParams.js";
+import { NonceNotFound } from "./pages/NonceNotFound.js";
+import { Setup } from "./pages/Setup.js";
+
+export function Routing(): VNode {
+ // check session and defined if this is
+ // public routing or private
+ return (
+ <Frame>
+ <PublicRounting />
+ </Frame>
+ );
+}
+
+const publicPages = {
+ noinfo: urlPattern<{ nonce: string }>(
+ /\/noinfo\/(?<nonce>[a-zA-Z0-9]+)/,
+ ({ nonce }) => `#/noinfo/${nonce}`,
+ ),
+ authorize: urlPattern<{ nonce: string }>(
+ /\/authorize\/(?<nonce>[a-zA-Z0-9]+)/,
+ ({ nonce }) => `#/authorize/${nonce}`,
+ ),
+ ask: urlPattern<{ nonce: string }>(
+ /\/ask\/(?<nonce>[a-zA-Z0-9]+)/,
+ ({ nonce }) => `#/ask/${nonce}`,
+ ),
+ answer: urlPattern<{ nonce: string }>(
+ /\/answer\/(?<nonce>[a-zA-Z0-9]+)/,
+ ({ nonce }) => `#/answer/${nonce}`,
+ ),
+ completed: urlPattern<{ nonce: string }>(
+ /\/completed\/(?<nonce>[a-zA-Z0-9]+)/,
+ ({ nonce }) => `#/completed/${nonce}`,
+ ),
+ setup: urlPattern<{ client: string }>(
+ /\/setup\/(?<client>[0-9]+)/,
+ ({ client }) => `#/setup/${client}`,
+ ),
+};
+
+function safeGetParam(
+ ps: Record<string, string[]>,
+ n: string,
+): string | undefined {
+ if (!ps[n] || ps[n].length == 0) return undefined;
+ return ps[n][0];
+}
+
+function safeToURL(s: string | undefined): URL | undefined {
+ if (s === undefined) return undefined;
+ try {
+ return new URL(s);
+ } catch (e) {
+ return undefined;
+ }
+}
+
+function PublicRounting(): VNode {
+ const location = useCurrentLocation(publicPages);
+ const { navigateTo } = useNavigationContext();
+ const { start } = useSessionState();
+
+ if (location === undefined) {
+ return <NonceNotFound />;
+ }
+
+ switch (location.name) {
+ case "noinfo": {
+ return <div>no info</div>;
+ }
+ case "setup": {
+ return (
+ <Setup
+ clientId={location.values.client}
+ onCreated={(nonce) => {
+ navigateTo(publicPages.ask.url({ nonce }));
+ //response_type=code
+ //client_id=1
+ //redirect_uri=http://exchange.taler.test:1180/kyc-proof/kyc-provider-wallet
+ //state=123
+ }}
+ />
+ );
+ }
+ case "authorize": {
+ const responseType = safeGetParam(location.params, "response_type");
+ const clientId = safeGetParam(location.params, "client_id");
+ const redirectURL = safeToURL(
+ safeGetParam(location.params, "redirect_uri"),
+ );
+ const state = safeGetParam(location.params, "state");
+ // http://localhost:8080/app/#/authorize/ASDASD123?response_type=code&client_id=1&redirect_uri=goog.ecom&state=123
+ //
+
+ // http://localhost:8080/app/?response_type=code&client_id=1&redirect_uri=http://exchange.taler.test:1180/kyc-proof/kyc-provider-wallet&state=123#/authorize/X9668AR2CFC26X55H0M87GJZXGM45VD4SZE05C5SNS5FADPWN220
+
+ if (
+ !responseType ||
+ !clientId ||
+ !redirectURL ||
+ !state ||
+ responseType !== "code"
+ ) {
+ return <MissingParams />;
+ }
+ const sessionId: SessionId = {
+ clientId,
+ redirectURL: redirectURL.href,
+ state,
+ };
+ return (
+ <CheckChallengeIsUpToDate
+ sessionId={sessionId}
+ nonce={location.values.nonce}
+ onNoInfo={() => {
+ navigateTo(
+ publicPages.noinfo.url({
+ nonce: location.values.nonce,
+ }),
+ );
+ }}
+ onCompleted={() => {
+ start(sessionId);
+ navigateTo(
+ publicPages.completed.url({
+ nonce: location.values.nonce,
+ }),
+ );
+ }}
+ onChangeLeft={() => {
+ start(sessionId);
+ navigateTo(
+ publicPages.ask.url({
+ nonce: location.values.nonce,
+ }),
+ );
+ }}
+ onNoMoreChanges={() => {
+ start(sessionId);
+ navigateTo(
+ publicPages.ask.url({
+ nonce: location.values.nonce,
+ }),
+ );
+ }}
+ >
+ <Loading />
+ </CheckChallengeIsUpToDate>
+ );
+ }
+ case "ask": {
+ return (
+ <CheckChallengeIsUpToDate
+ nonce={location.values.nonce}
+ onNoInfo={() => {
+ navigateTo(
+ publicPages.noinfo.url({
+ nonce: location.values.nonce,
+ }),
+ );
+ }}
+ onCompleted={() => {
+ navigateTo(
+ publicPages.completed.url({
+ nonce: location.values.nonce,
+ }),
+ );
+ }}
+ >
+ <AskChallenge
+ focus
+ nonce={location.values.nonce}
+ routeSolveChallenge={publicPages.answer}
+ onSendSuccesful={() => {
+ navigateTo(
+ publicPages.answer.url({
+ nonce: location.values.nonce,
+ }),
+ );
+ }}
+ />
+ </CheckChallengeIsUpToDate>
+ );
+ }
+ case "answer": {
+ return (
+ <CheckChallengeIsUpToDate
+ nonce={location.values.nonce}
+ onNoInfo={() => {
+ navigateTo(
+ publicPages.noinfo.url({
+ nonce: location.values.nonce,
+ }),
+ );
+ }}
+ onCompleted={() => {
+ navigateTo(
+ publicPages.completed.url({
+ nonce: location.values.nonce,
+ }),
+ );
+ }}
+ >
+ <AnswerChallenge
+ focus
+ nonce={location.values.nonce}
+ routeAsk={publicPages.ask}
+ onComplete={() => {
+ navigateTo(
+ publicPages.completed.url({
+ nonce: location.values.nonce,
+ }),
+ );
+ }}
+ />
+ </CheckChallengeIsUpToDate>
+ );
+ }
+ case "completed": {
+ return (
+ <CheckChallengeIsUpToDate
+ nonce={location.values.nonce}
+ onNoInfo={() => {
+ navigateTo(
+ publicPages.noinfo.url({
+ nonce: location.values.nonce,
+ }),
+ );
+ }}
+ >
+ <CallengeCompleted nonce={location.values.nonce} />
+ </CheckChallengeIsUpToDate>
+ );
+ }
+ default:
+ assertUnreachable(location);
+ }
+}
diff --git a/packages/challenger-ui/src/app.tsx b/packages/challenger-ui/src/app.tsx
new file mode 100644
index 000000000..2b5c5c815
--- /dev/null
+++ b/packages/challenger-ui/src/app.tsx
@@ -0,0 +1,168 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ CacheEvictor,
+ ChallengerCacheEviction,
+ assertUnreachable,
+ canonicalizeBaseUrl,
+ getGlobalLogLevel,
+ setGlobalLogLevelFromString,
+} from "@gnu-taler/taler-util";
+import {
+ BrowserHashNavigationProvider,
+ ChallengerApiProvider,
+ Loading,
+ TalerWalletIntegrationBrowserProvider,
+ TranslationProvider,
+} from "@gnu-taler/web-util/browser";
+import { useEffect, useState } from "preact/hooks";
+import { SWRConfig } from "swr";
+import { Routing } from "./Routing.js";
+// import { BankCoreApiProvider } from "./context/config.js";
+// import { BrowserHashNavigationProvider } from "./context/navigation.js";
+import { SettingsProvider } from "./context/settings.js";
+// import { TalerWalletIntegrationBrowserProvider } from "./context/wallet-integration.js";
+import { VNode, h } from "preact";
+import { 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> = {
+ async notifySuccess(op) {
+ switch (op) {
+ case ChallengerCacheEviction.CREATE_CHALLENGE: {
+ await Promise.all([revalidateChallengeSession()]);
+ return;
+ }
+ default: {
+ assertUnreachable(op);
+ }
+ }
+ },
+};
+
+export function App(): VNode {
+ const [settings, setSettings] = useState<ChallengerUiSettings>();
+ useEffect(() => {
+ fetchSettings(setSettings);
+ }, []);
+ if (!settings) return <Loading />;
+
+ const baseUrl = getInitialBackendBaseURL(settings.backendBaseURL);
+ return (
+ <SettingsProvider value={settings}>
+ <TranslationProvider
+ source={strings}
+ forceLang="en"
+ completeness={{
+ es: strings["es"].completeness,
+ de: strings["de"].completeness,
+ }}
+ >
+ <ChallengerApiProvider
+ baseUrl={new URL("/", baseUrl)}
+ frameOnError={Frame}
+ evictors={{
+ challenger: evictBankSwrCache,
+ }}
+ >
+ <SWRConfig
+ value={{
+ provider: WITH_LOCAL_STORAGE_CACHE
+ ? localStorageProvider
+ : undefined,
+ // normally, do not revalidate
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ revalidateIfStale: false,
+ revalidateOnMount: undefined,
+ focusThrottleInterval: undefined,
+
+ // normally, do not refresh
+ refreshInterval: undefined,
+ dedupingInterval: 2000,
+ refreshWhenHidden: false,
+ refreshWhenOffline: false,
+
+ // ignore errors
+ shouldRetryOnError: false,
+ errorRetryCount: 0,
+ errorRetryInterval: undefined,
+
+ // do not go to loading again if already has data
+ keepPreviousData: true,
+ }}
+ >
+ <TalerWalletIntegrationBrowserProvider>
+ <BrowserHashNavigationProvider>
+ <Routing />
+ </BrowserHashNavigationProvider>
+ </TalerWalletIntegrationBrowserProvider>
+ </SWRConfig>
+ </ChallengerApiProvider>
+ </TranslationProvider>
+ </SettingsProvider>
+ );
+}
+
+// @ts-expect-error creating a new property for window object
+window.setGlobalLogLevelFromString = setGlobalLogLevelFromString;
+// @ts-expect-error creating a new property for window object
+window.getGlobalLevel = getGlobalLogLevel;
+
+function localStorageProvider(): Map<unknown, unknown> {
+ const map = new Map(JSON.parse(localStorage.getItem("app-cache") || "[]"));
+
+ window.addEventListener("beforeunload", () => {
+ const appCache = JSON.stringify(Array.from(map.entries()));
+ localStorage.setItem("app-cache", appCache);
+ });
+ return map;
+}
+
+function getInitialBackendBaseURL(
+ backendFromSettings: string | undefined,
+): string {
+ const overrideUrl =
+ typeof localStorage !== "undefined"
+ ? localStorage.getItem("challenger-base-url")
+ : undefined;
+ let result: string;
+
+ if (!overrideUrl) {
+ // normal path
+ if (!backendFromSettings) {
+ console.error(
+ "ERROR: backendBaseURL was overridden by a setting file and missing. Setting value to 'window.origin'",
+ );
+ result = window.origin;
+ } else {
+ result = backendFromSettings;
+ }
+ } else {
+ // testing/development path
+ result = overrideUrl;
+ }
+ try {
+ return canonicalizeBaseUrl(result);
+ } catch (e) {
+ // fall back
+ return canonicalizeBaseUrl(window.origin);
+ }
+}
diff --git a/packages/challenger-ui/src/assets/home.svg b/packages/challenger-ui/src/assets/home.svg
new file mode 100644
index 000000000..35f340162
--- /dev/null
+++ b/packages/challenger-ui/src/assets/home.svg
@@ -0,0 +1,3 @@
+<svg class="h-6 w-6 shrink-0 text-white" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" />
+</svg> \ No newline at end of file
diff --git a/packages/challenger-ui/src/assets/logo-2021.svg b/packages/challenger-ui/src/assets/logo-2021.svg
new file mode 100644
index 000000000..8c5ff3e5b
--- /dev/null
+++ b/packages/challenger-ui/src/assets/logo-2021.svg
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 201 90">
+ <g fill="#0042b3" fill-rule="evenodd" stroke-width=".3">
+ <path d="M86.7 1.1c15.6 0 29 9.4 36 23.2h-5.9A35.1 35.1 0 0086.7 6.5C67 6.5 51 23.6 51 44.7c0 10.4 3.8 19.7 10 26.6a31.4 31.4 0 01-4.2 3A45.2 45.2 0 0146 44.7c0-24 18.2-43.6 40.7-43.6zm35.8 64.3a40.4 40.4 0 01-39 22.8c3-1.5 6-3.5 8.6-5.7a35.6 35.6 0 0024.6-17.1z" />
+ <path d="M64.2 1.1l3.1.1c-3 1.6-5.9 3.5-8.5 5.8a37.5 37.5 0 00-30.2 37.7c0 14.3 7.3 26.7 18 33.3a29.6 29.6 0 01-8.5.2c-9-8-14.6-20-14.6-33.5 0-24 18.2-43.6 40.7-43.6zm5.4 81.4a35.6 35.6 0 0024.6-17.1h5.9a40.4 40.4 0 01-39 22.8c3-1.5 5.9-3.5 8.5-5.7zm24.8-58.2a37 37 0 00-12.6-12.8 29.6 29.6 0 018.5-.2c4 3.6 7.4 8 9.9 13z" />
+ <path d="M41.8 1.1c1 0 2 0 3.1.2-3 1.5-5.9 3.4-8.5 5.6A37.5 37.5 0 006.1 44.7c0 21.1 16 38.3 35.7 38.3 12.6 0 23.6-7 30-17.6h5.8a40.4 40.4 0 01-35.8 23C19.3 88.4 1 68.8 1 44.7c0-24 18.2-43.6 40.7-43.6zm30.1 23.2a38.1 38.1 0 00-4.5-6.1c1.3-1.2 2.7-2.2 4.3-3 2.3 2.7 4.4 5.8 6 9.1z" />
+ </g>
+ <path d="M76.1 34.4h9.2v-5H61.9v5H71v26h5.1zM92.6 52.9h13.7l3 7.4h5.3l-12.7-31.2h-4.7L84.5 60.3h5.2zm11.8-4.9h-9.9l5-12.4zM123.8 29.4h-4.6v31h20.6v-5h-16zM166.5 29.4H145v31h21.6v-5H150v-8.3h14.5v-4.9h-14.5v-8h16.4zM191.2 39.5c0 1.6-.5 2.8-1.6 3.8s-2.6 1.4-4.4 1.4h-7.4V34.3h7.4c1.9 0 3.4.4 4.4 1.3 1 .9 1.6 2.2 1.6 3.9zm6 20.8l-7.7-11.7c1-.3 1.9-.7 2.7-1.3a8.8 8.8 0 003.6-4.6c.4-1 .5-2.2.5-3.5 0-1.5-.2-2.9-.7-4.1a8.4 8.4 0 00-2.1-3.1c-1-.8-2-1.5-3.4-2-1.3-.4-2.8-.6-4.5-.6h-12.9v31h5V49.4h6.5l7 10.8z" />
+</svg> \ No newline at end of file
diff --git a/packages/challenger-ui/src/assets/people.svg b/packages/challenger-ui/src/assets/people.svg
new file mode 100644
index 000000000..1dc878b81
--- /dev/null
+++ b/packages/challenger-ui/src/assets/people.svg
@@ -0,0 +1,3 @@
+<svg class="h-6 w-6 shrink-0 text-indigo-200 group-hover:text-white" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z" />
+</svg> \ No newline at end of file
diff --git a/packages/challenger-ui/src/attempts-exhausted.html b/packages/challenger-ui/src/attempts-exhausted.html
new file mode 100644
index 000000000..c2468b98b
--- /dev/null
+++ b/packages/challenger-ui/src/attempts-exhausted.html
@@ -0,0 +1,88 @@
+<!--
+ This file is part of GNU Taler
+ (C) 2021--2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+
+ @author Sebastian Javier Marchano
+-->
+<!DOCTYPE html>
+<html lang="en" class="h-full">
+
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
+ <meta name="taler-support" content="uri">
+ <meta name="mobile-web-app-capable" content="yes" />
+ <meta name="apple-mobile-web-app-capable" content="yes" />
+ <link rel="icon"
+ href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/Qn3//3ryn/795Tf/WrpP/2LCW/8B6T//w4Nb///////Pn4P+/d0v/9u3n/+7d0v/EhV7//v///+HDr//fxLD/zph2/+TJt//8/Pv/woBX//Lm3f/y5dz/v3hN//bu6f/JjGn/4sW0///////Df1j/8OLZ//v6+P+/elH/+vj1//jy7f+/elL//////+zYzP/Eg13//////967p//MlHT/wn5X///////v4Nb/yY1s///////jw7H/06KG////////////z5t9/+fNvf//////x4pn//Pp4v/8+vn/w39X/8WEX///////5s/A/9CbfP//////27Oc/9y2n////////////9itlf/gu6f//////86Vdf/r2Mz//////8SCXP/Df1j//////+7d0v/KkG7//////+HBrf/VpYr////////////RnoH/5sq6///////Ii2n/8ubf//39/P/Cf1j/xohk/+bNvv//////wn5W//Tq4//58/D/wHxV//7+/f/59fH/v3xU//39/P/w4Nf/xIFb///////hw7H/yo9t/+/f1f/AeU3/+/n2/+nSxP/FhmD//////9qzm//Upon/4MSx/96+qf//////xINc/+3bz//48e3/v3hN//Pn3///////6M+//752S//gw6//06aK/8J+VP/kzLr/zZd1/8OCWv/q18r/17KZ/9Ooi//fv6r/v3dK/+vWyP///////v39///////27un/1aeK/9Opjv/m1cf/1KCC/9a0nP/n08T/0Jx8/82YdP/QnHz/16yR//jx7P///////v39///////+/f3///7+///////+//7//v7+///////+/v7//v/+/////////////////////////v7//v79///////////////////+/v/+/Pv//v39///+/v/+/Pv///7+//7+/f/+/Pv//v39//79/P/+/Pv///7+////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" />
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
+ <link rel="stylesheet" href="main.css" />
+ <script type="module" src="main.js"></script>
+ <title>Attempts exhausted (#{{ec}})</title>
+</head>
+
+<body class="min-h-full flex flex-col">
+ <header class="bg-indigo-600 w-full mx-auto px-2 border-b border-opacity-25 border-indigo-400">
+ <div class="flex flex-row h-16 items-center ">
+ <div class="flex px-2 justify-start">
+ <div class="flex-shrink-0 bg-white rounded-lg"><a href="#"><img class="h-8 w-auto"
+ src="data:image/svg+xml,<?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?>%0A<svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 201 90&quot;>%0A <g fill=&quot;%230042b3&quot; fill-rule=&quot;evenodd&quot; stroke-width=&quot;.3&quot;>%0A <path d=&quot;M86.7 1.1c15.6 0 29 9.4 36 23.2h-5.9A35.1 35.1 0 0086.7 6.5C67 6.5 51 23.6 51 44.7c0 10.4 3.8 19.7 10 26.6a31.4 31.4 0 01-4.2 3A45.2 45.2 0 0146 44.7c0-24 18.2-43.6 40.7-43.6zm35.8 64.3a40.4 40.4 0 01-39 22.8c3-1.5 6-3.5 8.6-5.7a35.6 35.6 0 0024.6-17.1z&quot; />%0A <path d=&quot;M64.2 1.1l3.1.1c-3 1.6-5.9 3.5-8.5 5.8a37.5 37.5 0 00-30.2 37.7c0 14.3 7.3 26.7 18 33.3a29.6 29.6 0 01-8.5.2c-9-8-14.6-20-14.6-33.5 0-24 18.2-43.6 40.7-43.6zm5.4 81.4a35.6 35.6 0 0024.6-17.1h5.9a40.4 40.4 0 01-39 22.8c3-1.5 5.9-3.5 8.5-5.7zm24.8-58.2a37 37 0 00-12.6-12.8 29.6 29.6 0 018.5-.2c4 3.6 7.4 8 9.9 13z&quot; />%0A <path d=&quot;M41.8 1.1c1 0 2 0 3.1.2-3 1.5-5.9 3.4-8.5 5.6A37.5 37.5 0 006.1 44.7c0 21.1 16 38.3 35.7 38.3 12.6 0 23.6-7 30-17.6h5.8a40.4 40.4 0 01-35.8 23C19.3 88.4 1 68.8 1 44.7c0-24 18.2-43.6 40.7-43.6zm30.1 23.2a38.1 38.1 0 00-4.5-6.1c1.3-1.2 2.7-2.2 4.3-3 2.3 2.7 4.4 5.8 6 9.1z&quot; />%0A </g>%0A <path d=&quot;M76.1 34.4h9.2v-5H61.9v5H71v26h5.1zM92.6 52.9h13.7l3 7.4h5.3l-12.7-31.2h-4.7L84.5 60.3h5.2zm11.8-4.9h-9.9l5-12.4zM123.8 29.4h-4.6v31h20.6v-5h-16zM166.5 29.4H145v31h21.6v-5H150v-8.3h14.5v-4.9h-14.5v-8h16.4zM191.2 39.5c0 1.6-.5 2.8-1.6 3.8s-2.6 1.4-4.4 1.4h-7.4V34.3h7.4c1.9 0 3.4.4 4.4 1.3 1 .9 1.6 2.2 1.6 3.9zm6 20.8l-7.7-11.7c1-.3 1.9-.7 2.7-1.3a8.8 8.8 0 003.6-4.6c.4-1 .5-2.2.5-3.5 0-1.5-.2-2.9-.7-4.1a8.4 8.4 0 00-2.1-3.1c-1-.8-2-1.5-3.4-2-1.3-.4-2.8-.6-4.5-.6h-12.9v31h5V49.4h6.5l7 10.8z&quot; />%0A</svg>"
+ alt="GNU Taler" style="height: 1.5rem; margin: 0.5rem;"></a></div><span
+ class="flex items-center text-white text-lg font-bold ml-4">Challenger</span>
+ </div>
+ <div class="block flex-1 ml-6 "></div>
+ <div class="flex justify-end">
+ </div>
+ </div>
+ </header>
+
+ <main class="flex-1">
+ <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 items-center mt-4">
+ <div class="rounded-md bg-red-50 p-4 shadow-xl">
+ <div class="flex">
+ <div class="flex-shrink-0">
+ <svg xmlns="http://www.w3.org/2000/svg" stroke="none" viewBox="0 0 24 24" fill="currentColor"
+ class="w-8 h-8 text-red-400">
+ <path fill-rule="evenodd"
+ d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z" />
+ </svg>
+ </div>
+ <div class="ml-3">
+ <h3 class="text-sm font-medium text-red-800">
+ You have tried too many times
+ </h3>
+ <div class="mt-2 text-sm text-red-700">
+ <p>More attempts are not allowed</p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </main>
+
+ <footer class="bottom-4 mb-4">
+ <div class="mt-8 mx-8 md:order-1 md:mt-0">
+ <div>
+ <p class="text-xs leading-5 text-gray-400">Learn more about <a target="_blank" rel="noreferrer noopener"
+ class="font-semibold text-gray-500 hover:text-gray-400" href="https://taler.net">GNU Taler</a></p>
+ </div>
+ <div style="flex-grow: 1;"></div>
+ <p class="text-xs leading-5 text-gray-400">Copyright © 2014—2023 Taler Systems SA. </p>
+ </div>
+ </footer>
+
+</body>
+
+</html> \ No newline at end of file
diff --git a/packages/challenger-ui/src/components/CheckChallengeIsUpToDate.tsx b/packages/challenger-ui/src/components/CheckChallengeIsUpToDate.tsx
new file mode 100644
index 000000000..70e41bf1e
--- /dev/null
+++ b/packages/challenger-ui/src/components/CheckChallengeIsUpToDate.tsx
@@ -0,0 +1,132 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ HttpStatusCode,
+ TalerError,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
+import {
+ Attention,
+ Loading,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { ComponentChildren, Fragment, VNode, h } from "preact";
+import { useChallengeSession } from "../hooks/challenge.js";
+import { SessionId, useSessionState } from "../hooks/session.js";
+
+interface Props {
+ nonce: string;
+ children: ComponentChildren;
+ sessionId?: SessionId;
+ onCompleted?: () => void;
+ onChangeLeft?: () => void;
+ onNoMoreChanges?: () => void;
+ onNoInfo: () => void;
+}
+export function CheckChallengeIsUpToDate({
+ sessionId: sessionFromParam,
+ nonce,
+ children,
+ onCompleted,
+ onChangeLeft,
+ onNoMoreChanges,
+ onNoInfo,
+}: Props): VNode {
+ const { state, updateStatus } = useSessionState();
+ const { i18n } = useTranslationContext();
+
+ const sessionId = sessionFromParam
+ ? sessionFromParam
+ : !state
+ ? undefined
+ : {
+ clientId: state.clientId,
+ redirectURL: state.redirectURL,
+ state: state.state,
+ };
+
+ const result = useChallengeSession(nonce, sessionId);
+ console.log("asd");
+ if (!sessionId) {
+ onNoInfo();
+ return <Loading />;
+ }
+
+ if (!result) {
+ return <Loading />;
+ }
+ if (result instanceof TalerError) {
+ return <pre>{JSON.stringify(result, undefined, 2)}</pre>;
+ }
+
+ if (result.type === "fail") {
+ switch (result.case) {
+ case HttpStatusCode.BadRequest: {
+ return (
+ <Attention type="danger" title={i18n.str`Bad request`}>
+ <i18n.Translate>
+ Could not start the challenge, check configuration.
+ </i18n.Translate>
+ </Attention>
+ );
+ }
+ case HttpStatusCode.NotFound: {
+ return (
+ <Attention type="danger" title={i18n.str`Not found`}>
+ <i18n.Translate>Nonce not found</i18n.Translate>
+ </Attention>
+ );
+ }
+ case HttpStatusCode.NotAcceptable: {
+ return (
+ <Attention type="danger" title={i18n.str`Not acceptable`}>
+ <i18n.Translate>
+ Server has wrong template configuration
+ </i18n.Translate>
+ </Attention>
+ );
+ }
+ case HttpStatusCode.InternalServerError: {
+ return (
+ <Attention type="danger" title={i18n.str`Internal error`}>
+ <i18n.Translate>Check logs</i18n.Translate>
+ </Attention>
+ );
+ }
+ default:
+ assertUnreachable(result);
+ }
+ }
+
+ updateStatus(result.body);
+
+ if (onCompleted && "redirectURL" in result.body) {
+ onCompleted();
+ return <Loading />;
+ }
+
+ if (onNoMoreChanges && !result.body.changes_left) {
+ onNoMoreChanges();
+ return <Loading />;
+ }
+
+ if (onChangeLeft && !result.body.changes_left) {
+ onChangeLeft();
+ return <Loading />;
+ }
+
+ return <Fragment>{children}</Fragment>;
+}
diff --git a/packages/challenger-ui/src/context/settings.ts b/packages/challenger-ui/src/context/settings.ts
new file mode 100644
index 000000000..679359200
--- /dev/null
+++ b/packages/challenger-ui/src/context/settings.ts
@@ -0,0 +1,44 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { ComponentChildren, createContext, h, VNode } from "preact";
+import { useContext } from "preact/hooks";
+import { ChallengerUiSettings } from "../settings.js";
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+export type Type = ChallengerUiSettings;
+
+const initial: ChallengerUiSettings = {};
+const Context = createContext<Type>(initial);
+
+export const useSettingsContext = (): Type => useContext(Context);
+
+export const SettingsProvider = ({
+ children,
+ value,
+}: {
+ value: ChallengerUiSettings;
+ children: ComponentChildren;
+}): VNode => {
+ return h(Context.Provider, {
+ value,
+ children,
+ });
+};
diff --git a/packages/challenger-ui/src/enter-address-form.html b/packages/challenger-ui/src/enter-address-form.html
new file mode 100644
index 000000000..76b4d2262
--- /dev/null
+++ b/packages/challenger-ui/src/enter-address-form.html
@@ -0,0 +1,133 @@
+<!--
+ This file is part of GNU Taler
+ (C) 2021--2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+
+ @author Sebastian Javier Marchano
+-->
+<!DOCTYPE html>
+<html lang="en" class="h-full">
+
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
+ <meta name="taler-support" content="uri">
+ <meta name="mobile-web-app-capable" content="yes" />
+ <meta name="apple-mobile-web-app-capable" content="yes" />
+ <link rel="icon"
+ href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/Qn3//3ryn/795Tf/WrpP/2LCW/8B6T//w4Nb///////Pn4P+/d0v/9u3n/+7d0v/EhV7//v///+HDr//fxLD/zph2/+TJt//8/Pv/woBX//Lm3f/y5dz/v3hN//bu6f/JjGn/4sW0///////Df1j/8OLZ//v6+P+/elH/+vj1//jy7f+/elL//////+zYzP/Eg13//////967p//MlHT/wn5X///////v4Nb/yY1s///////jw7H/06KG////////////z5t9/+fNvf//////x4pn//Pp4v/8+vn/w39X/8WEX///////5s/A/9CbfP//////27Oc/9y2n////////////9itlf/gu6f//////86Vdf/r2Mz//////8SCXP/Df1j//////+7d0v/KkG7//////+HBrf/VpYr////////////RnoH/5sq6///////Ii2n/8ubf//39/P/Cf1j/xohk/+bNvv//////wn5W//Tq4//58/D/wHxV//7+/f/59fH/v3xU//39/P/w4Nf/xIFb///////hw7H/yo9t/+/f1f/AeU3/+/n2/+nSxP/FhmD//////9qzm//Upon/4MSx/96+qf//////xINc/+3bz//48e3/v3hN//Pn3///////6M+//752S//gw6//06aK/8J+VP/kzLr/zZd1/8OCWv/q18r/17KZ/9Ooi//fv6r/v3dK/+vWyP///////v39///////27un/1aeK/9Opjv/m1cf/1KCC/9a0nP/n08T/0Jx8/82YdP/QnHz/16yR//jx7P///////v39///////+/f3///7+///////+//7//v7+///////+/v7//v/+/////////////////////////v7//v79///////////////////+/v/+/Pv//v39///+/v/+/Pv///7+//7+/f/+/Pv//v39//79/P/+/Pv///7+////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" />
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
+ <link rel="stylesheet" href="main.css" />
+ <script type="module" src="main.js"></script>
+ <title>Enter contact details</title>
+</head>
+
+<body class="min-h-full flex flex-col">
+ <header class="bg-indigo-600 w-full mx-auto px-2 border-b border-opacity-25 border-indigo-400">
+ <div class="flex flex-row h-16 items-center ">
+ <div class="flex px-2 justify-start">
+ <div class="flex-shrink-0 bg-white rounded-lg"><a href="#"><img class="h-8 w-auto"
+ src="data:image/svg+xml,<?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?>%0A<svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 201 90&quot;>%0A <g fill=&quot;%230042b3&quot; fill-rule=&quot;evenodd&quot; stroke-width=&quot;.3&quot;>%0A <path d=&quot;M86.7 1.1c15.6 0 29 9.4 36 23.2h-5.9A35.1 35.1 0 0086.7 6.5C67 6.5 51 23.6 51 44.7c0 10.4 3.8 19.7 10 26.6a31.4 31.4 0 01-4.2 3A45.2 45.2 0 0146 44.7c0-24 18.2-43.6 40.7-43.6zm35.8 64.3a40.4 40.4 0 01-39 22.8c3-1.5 6-3.5 8.6-5.7a35.6 35.6 0 0024.6-17.1z&quot; />%0A <path d=&quot;M64.2 1.1l3.1.1c-3 1.6-5.9 3.5-8.5 5.8a37.5 37.5 0 00-30.2 37.7c0 14.3 7.3 26.7 18 33.3a29.6 29.6 0 01-8.5.2c-9-8-14.6-20-14.6-33.5 0-24 18.2-43.6 40.7-43.6zm5.4 81.4a35.6 35.6 0 0024.6-17.1h5.9a40.4 40.4 0 01-39 22.8c3-1.5 5.9-3.5 8.5-5.7zm24.8-58.2a37 37 0 00-12.6-12.8 29.6 29.6 0 018.5-.2c4 3.6 7.4 8 9.9 13z&quot; />%0A <path d=&quot;M41.8 1.1c1 0 2 0 3.1.2-3 1.5-5.9 3.4-8.5 5.6A37.5 37.5 0 006.1 44.7c0 21.1 16 38.3 35.7 38.3 12.6 0 23.6-7 30-17.6h5.8a40.4 40.4 0 01-35.8 23C19.3 88.4 1 68.8 1 44.7c0-24 18.2-43.6 40.7-43.6zm30.1 23.2a38.1 38.1 0 00-4.5-6.1c1.3-1.2 2.7-2.2 4.3-3 2.3 2.7 4.4 5.8 6 9.1z&quot; />%0A </g>%0A <path d=&quot;M76.1 34.4h9.2v-5H61.9v5H71v26h5.1zM92.6 52.9h13.7l3 7.4h5.3l-12.7-31.2h-4.7L84.5 60.3h5.2zm11.8-4.9h-9.9l5-12.4zM123.8 29.4h-4.6v31h20.6v-5h-16zM166.5 29.4H145v31h21.6v-5H150v-8.3h14.5v-4.9h-14.5v-8h16.4zM191.2 39.5c0 1.6-.5 2.8-1.6 3.8s-2.6 1.4-4.4 1.4h-7.4V34.3h7.4c1.9 0 3.4.4 4.4 1.3 1 .9 1.6 2.2 1.6 3.9zm6 20.8l-7.7-11.7c1-.3 1.9-.7 2.7-1.3a8.8 8.8 0 003.6-4.6c.4-1 .5-2.2.5-3.5 0-1.5-.2-2.9-.7-4.1a8.4 8.4 0 00-2.1-3.1c-1-.8-2-1.5-3.4-2-1.3-.4-2.8-.6-4.5-.6h-12.9v31h5V49.4h6.5l7 10.8z&quot; />%0A</svg>"
+ alt="GNU Taler" style="height: 1.5rem; margin: 0.5rem;"></a></div><span
+ class="flex items-center text-white text-lg font-bold ml-4">Challenger</span>
+ </div>
+ <div class="block flex-1 ml-6 "></div>
+ <div class="flex justify-end">
+ </div>
+ </div>
+ </header>
+
+ <main class="flex-1">
+
+ <div class="isolate bg-white px-6 py-12">
+ <div class="mx-auto max-w-2xl text-center">
+ <h2 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
+ Enter contact details
+ </h2>
+ <p class="mt-2 text-lg leading-8 text-gray-600">
+ You will receive a letter with a TAN code that must be provided on the next page.
+ </p>
+ </div>
+ <form action="/challenge/{{nonce}}" method="POST" class="mx-auto mt-16 max-w-xl sm:mt-20">
+ <div class="grid grid-cols-1 gap-x-8 gap-y-6">
+ <div class="sm:col-span-2">
+ <label for="address" class="block text-sm font-semibold leading-6 text-gray-900">
+ Street address
+ </label>
+ <div class="mt-2.5">
+ <textarea name="address" id="address" rows="3" autocomplete="shipping street-address"
+ class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"></textarea>
+
+ </div>
+ </div>
+
+ <div class="sm:col-span-2">
+ <label for="city" class="block text-sm font-semibold leading-6 text-gray-900">
+ City
+ </label>
+ <div class="mt-2.5">
+ <input type="text" name="city" id="city" maxlength="512" autocomplete="address-level2"
+ class="block w-full rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6">
+ </div>
+ </div>
+
+ <div class="sm:col-span-2">
+ <label for="postal-code" class="block text-sm font-semibold leading-6 text-gray-900">
+ Postal code
+ </label>
+ <div class="mt-2.5">
+ <input type="text" name="postal-code" id="postal-code" maxlength="512" autocomplete="postal-code"
+ class="block w-full rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6">
+ </div>
+ </div>
+
+ <div class="sm:col-span-2">
+ <label for="country" class="block text-sm font-semibold leading-6 text-gray-900">
+ Country
+ </label>
+ <div class="mt-2.5">
+ <input type="text" name="country" id="country" maxlength="512" autocomplete="country"
+ class="block w-full rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6">
+ </div>
+ </div>
+
+ <p class="mt-3 text-sm leading-6 text-gray-400">
+ You can change address another {{changes_left}} times.
+ </p>
+ </div>
+
+ <div class="mt-10">
+ <button type="submit"
+ class="block w-full rounded-md bg-indigo-600 px-3.5 py-2.5 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">
+ Send mail
+ </button>
+ </div>
+ </form>
+ </div>
+ </main>
+
+
+ <footer class="bottom-4 mb-4">
+ <div class="mt-8 mx-8 md:order-1 md:mt-0">
+ <div>
+ <p class="text-xs leading-5 text-gray-400">Learn more about <a target="_blank" rel="noreferrer noopener"
+ class="font-semibold text-gray-500 hover:text-gray-400" href="https://taler.net">GNU Taler</a></p>
+ </div>
+ <div style="flex-grow: 1;"></div>
+ <p class="text-xs leading-5 text-gray-400">Copyright © 2014—2023 Taler Systems SA. </p>
+ </div>
+ </footer>
+</body>
+
+</html>
diff --git a/packages/challenger-ui/src/enter-email-form.html b/packages/challenger-ui/src/enter-email-form.html
new file mode 100644
index 000000000..3b8720244
--- /dev/null
+++ b/packages/challenger-ui/src/enter-email-form.html
@@ -0,0 +1,127 @@
+<!--
+ This file is part of GNU Taler
+ (C) 2021--2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+
+ @author Sebastian Javier Marchano
+-->
+<!DOCTYPE html>
+<html lang="en" class="h-full">
+
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
+ <meta name="taler-support" content="uri">
+ <meta name="mobile-web-app-capable" content="yes" />
+ <meta name="apple-mobile-web-app-capable" content="yes" />
+ <link rel="icon"
+ href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/Qn3//3ryn/795Tf/WrpP/2LCW/8B6T//w4Nb///////Pn4P+/d0v/9u3n/+7d0v/EhV7//v///+HDr//fxLD/zph2/+TJt//8/Pv/woBX//Lm3f/y5dz/v3hN//bu6f/JjGn/4sW0///////Df1j/8OLZ//v6+P+/elH/+vj1//jy7f+/elL//////+zYzP/Eg13//////967p//MlHT/wn5X///////v4Nb/yY1s///////jw7H/06KG////////////z5t9/+fNvf//////x4pn//Pp4v/8+vn/w39X/8WEX///////5s/A/9CbfP//////27Oc/9y2n////////////9itlf/gu6f//////86Vdf/r2Mz//////8SCXP/Df1j//////+7d0v/KkG7//////+HBrf/VpYr////////////RnoH/5sq6///////Ii2n/8ubf//39/P/Cf1j/xohk/+bNvv//////wn5W//Tq4//58/D/wHxV//7+/f/59fH/v3xU//39/P/w4Nf/xIFb///////hw7H/yo9t/+/f1f/AeU3/+/n2/+nSxP/FhmD//////9qzm//Upon/4MSx/96+qf//////xINc/+3bz//48e3/v3hN//Pn3///////6M+//752S//gw6//06aK/8J+VP/kzLr/zZd1/8OCWv/q18r/17KZ/9Ooi//fv6r/v3dK/+vWyP///////v39///////27un/1aeK/9Opjv/m1cf/1KCC/9a0nP/n08T/0Jx8/82YdP/QnHz/16yR//jx7P///////v39///////+/f3///7+///////+//7//v7+///////+/v7//v/+/////////////////////////v7//v79///////////////////+/v/+/Pv//v39///+/v/+/Pv///7+//7+/f/+/Pv//v39//79/P/+/Pv///7+////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" />
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
+ <link rel="stylesheet" href="main.css" />
+ <script type="module" src="main.js"></script>
+ <title>Enter contact details</title>
+</head>
+
+<body class="min-h-full flex flex-col">
+ <header class="bg-indigo-600 w-full mx-auto px-2 border-b border-opacity-25 border-indigo-400">
+ <div class="flex flex-row h-16 items-center ">
+ <div class="flex px-2 justify-start">
+ <div class="flex-shrink-0 bg-white rounded-lg"><a href="#"><img class="h-8 w-auto"
+ src="data:image/svg+xml,<?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?>%0A<svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 201 90&quot;>%0A <g fill=&quot;%230042b3&quot; fill-rule=&quot;evenodd&quot; stroke-width=&quot;.3&quot;>%0A <path d=&quot;M86.7 1.1c15.6 0 29 9.4 36 23.2h-5.9A35.1 35.1 0 0086.7 6.5C67 6.5 51 23.6 51 44.7c0 10.4 3.8 19.7 10 26.6a31.4 31.4 0 01-4.2 3A45.2 45.2 0 0146 44.7c0-24 18.2-43.6 40.7-43.6zm35.8 64.3a40.4 40.4 0 01-39 22.8c3-1.5 6-3.5 8.6-5.7a35.6 35.6 0 0024.6-17.1z&quot; />%0A <path d=&quot;M64.2 1.1l3.1.1c-3 1.6-5.9 3.5-8.5 5.8a37.5 37.5 0 00-30.2 37.7c0 14.3 7.3 26.7 18 33.3a29.6 29.6 0 01-8.5.2c-9-8-14.6-20-14.6-33.5 0-24 18.2-43.6 40.7-43.6zm5.4 81.4a35.6 35.6 0 0024.6-17.1h5.9a40.4 40.4 0 01-39 22.8c3-1.5 5.9-3.5 8.5-5.7zm24.8-58.2a37 37 0 00-12.6-12.8 29.6 29.6 0 018.5-.2c4 3.6 7.4 8 9.9 13z&quot; />%0A <path d=&quot;M41.8 1.1c1 0 2 0 3.1.2-3 1.5-5.9 3.4-8.5 5.6A37.5 37.5 0 006.1 44.7c0 21.1 16 38.3 35.7 38.3 12.6 0 23.6-7 30-17.6h5.8a40.4 40.4 0 01-35.8 23C19.3 88.4 1 68.8 1 44.7c0-24 18.2-43.6 40.7-43.6zm30.1 23.2a38.1 38.1 0 00-4.5-6.1c1.3-1.2 2.7-2.2 4.3-3 2.3 2.7 4.4 5.8 6 9.1z&quot; />%0A </g>%0A <path d=&quot;M76.1 34.4h9.2v-5H61.9v5H71v26h5.1zM92.6 52.9h13.7l3 7.4h5.3l-12.7-31.2h-4.7L84.5 60.3h5.2zm11.8-4.9h-9.9l5-12.4zM123.8 29.4h-4.6v31h20.6v-5h-16zM166.5 29.4H145v31h21.6v-5H150v-8.3h14.5v-4.9h-14.5v-8h16.4zM191.2 39.5c0 1.6-.5 2.8-1.6 3.8s-2.6 1.4-4.4 1.4h-7.4V34.3h7.4c1.9 0 3.4.4 4.4 1.3 1 .9 1.6 2.2 1.6 3.9zm6 20.8l-7.7-11.7c1-.3 1.9-.7 2.7-1.3a8.8 8.8 0 003.6-4.6c.4-1 .5-2.2.5-3.5 0-1.5-.2-2.9-.7-4.1a8.4 8.4 0 00-2.1-3.1c-1-.8-2-1.5-3.4-2-1.3-.4-2.8-.6-4.5-.6h-12.9v31h5V49.4h6.5l7 10.8z&quot; />%0A</svg>"
+ alt="GNU Taler" style="height: 1.5rem; margin: 0.5rem;"></a></div><span
+ class="flex items-center text-white text-lg font-bold ml-4">Challenger</span>
+ </div>
+ <div class="block flex-1 ml-6 "></div>
+ <div class="flex justify-end">
+ </div>
+ </div>
+ </header>
+
+ <main class="flex-1">
+
+ <div class="isolate bg-white px-6 py-12">
+ <div class="mx-auto max-w-2xl text-center">
+ <h2 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
+ Enter contact details
+ </h2>
+ <p class="mt-2 text-lg leading-8 text-gray-600">
+ You will receive an email with a TAN code that must be provided on the next page.
+ </p>
+ </div>
+ <form action="/challenge/{{nonce}}" method="POST" class="mx-auto mt-16 max-w-xl sm:mt-20">
+ <div class="grid grid-cols-1 gap-x-8 gap-y-6">
+ <div class="sm:col-span-2">
+ <label for="email" class="block text-sm font-semibold leading-6 text-gray-900">
+ Email
+ </label>
+ <div class="mt-2.5">
+ <input type="email" name="email" id="email" maxlength="512" autocomplete="email" value="{{last_address}}"
+ {{#fixed_address}}readonly{{/fixed_address}}
+ class="block w-full rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6">
+ </div>
+ </div>
+
+ <script>
+ function check() {
+ var email = document.getElementById('email');
+ var emailRepeat = document.getElementById('repeat-email');
+
+ if (email.value != emailRepeat.value) {
+ emailRepeat.setCustomValidity('The two email addresses must match.');
+ } else {
+ // input is valid -- reset the error message
+ emailRepeat.setCustomValidity('');
+ }
+ }
+ </script>
+
+ <div class="sm:col-span-2">
+ <label for="repeat-email" class="block text-sm font-semibold leading-6 text-gray-900">
+ Repeat email
+ </label>
+ <div class="mt-2.5">
+ <input oninput="check(this)" type="email" name="repeat-email" id="repeat-email" autocomplete="email"
+ class="block w-full rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6">
+ </div>
+ </div>
+
+ <p class="mt-3 text-sm leading-6 text-gray-400">
+ You can change your email address another {{changes_left}} times.
+ </p>
+ </div>
+
+ <div class="mt-10">
+ <button type="submit"
+ class="block w-full rounded-md bg-indigo-600 px-3.5 py-2.5 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">
+ Send email
+ </button>
+ </div>
+ </form>
+ </div>
+ </main>
+
+
+ <footer class="bottom-4 mb-4">
+ <div class="mt-8 mx-8 md:order-1 md:mt-0">
+ <div>
+ <p class="text-xs leading-5 text-gray-400">Learn more about <a target="_blank" rel="noreferrer noopener"
+ class="font-semibold text-gray-500 hover:text-gray-400" href="https://taler.net">GNU Taler</a></p>
+ </div>
+ <div style="flex-grow: 1;"></div>
+ <p class="text-xs leading-5 text-gray-400">Copyright © 2014—2023 Taler Systems SA. </p>
+ </div>
+ </footer>
+</body>
+
+</html>
diff --git a/packages/challenger-ui/src/enter-file-access-form.html b/packages/challenger-ui/src/enter-file-access-form.html
new file mode 100644
index 000000000..b79d1dada
--- /dev/null
+++ b/packages/challenger-ui/src/enter-file-access-form.html
@@ -0,0 +1,102 @@
+<!--
+ This file is part of GNU Taler
+ (C) 2021--2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+
+ @author Sebastian Javier Marchano
+-->
+<!DOCTYPE html>
+<html lang="en" class="h-full">
+
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
+ <meta name="taler-support" content="uri">
+ <meta name="mobile-web-app-capable" content="yes" />
+ <meta name="apple-mobile-web-app-capable" content="yes" />
+ <link rel="icon"
+ href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/Qn3//3ryn/795Tf/WrpP/2LCW/8B6T//w4Nb///////Pn4P+/d0v/9u3n/+7d0v/EhV7//v///+HDr//fxLD/zph2/+TJt//8/Pv/woBX//Lm3f/y5dz/v3hN//bu6f/JjGn/4sW0///////Df1j/8OLZ//v6+P+/elH/+vj1//jy7f+/elL//////+zYzP/Eg13//////967p//MlHT/wn5X///////v4Nb/yY1s///////jw7H/06KG////////////z5t9/+fNvf//////x4pn//Pp4v/8+vn/w39X/8WEX///////5s/A/9CbfP//////27Oc/9y2n////////////9itlf/gu6f//////86Vdf/r2Mz//////8SCXP/Df1j//////+7d0v/KkG7//////+HBrf/VpYr////////////RnoH/5sq6///////Ii2n/8ubf//39/P/Cf1j/xohk/+bNvv//////wn5W//Tq4//58/D/wHxV//7+/f/59fH/v3xU//39/P/w4Nf/xIFb///////hw7H/yo9t/+/f1f/AeU3/+/n2/+nSxP/FhmD//////9qzm//Upon/4MSx/96+qf//////xINc/+3bz//48e3/v3hN//Pn3///////6M+//752S//gw6//06aK/8J+VP/kzLr/zZd1/8OCWv/q18r/17KZ/9Ooi//fv6r/v3dK/+vWyP///////v39///////27un/1aeK/9Opjv/m1cf/1KCC/9a0nP/n08T/0Jx8/82YdP/QnHz/16yR//jx7P///////v39///////+/f3///7+///////+//7//v7+///////+/v7//v/+/////////////////////////v7//v79///////////////////+/v/+/Pv//v39///+/v/+/Pv///7+//7+/f/+/Pv//v39//79/P/+/Pv///7+////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" />
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
+ <link rel="stylesheet" href="main.css" />
+ <script type="module" src="main.js"></script>
+ <title>Enter local file name</title>
+</head>
+
+<body class="min-h-full flex flex-col">
+ <header class="bg-indigo-600 w-full mx-auto px-2 border-b border-opacity-25 border-indigo-400">
+ <div class="flex flex-row h-16 items-center ">
+ <div class="flex px-2 justify-start">
+ <div class="flex-shrink-0 bg-white rounded-lg"><a href="#"><img class="h-8 w-auto"
+ src="data:image/svg+xml,<?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?>%0A<svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 201 90&quot;>%0A <g fill=&quot;%230042b3&quot; fill-rule=&quot;evenodd&quot; stroke-width=&quot;.3&quot;>%0A <path d=&quot;M86.7 1.1c15.6 0 29 9.4 36 23.2h-5.9A35.1 35.1 0 0086.7 6.5C67 6.5 51 23.6 51 44.7c0 10.4 3.8 19.7 10 26.6a31.4 31.4 0 01-4.2 3A45.2 45.2 0 0146 44.7c0-24 18.2-43.6 40.7-43.6zm35.8 64.3a40.4 40.4 0 01-39 22.8c3-1.5 6-3.5 8.6-5.7a35.6 35.6 0 0024.6-17.1z&quot; />%0A <path d=&quot;M64.2 1.1l3.1.1c-3 1.6-5.9 3.5-8.5 5.8a37.5 37.5 0 00-30.2 37.7c0 14.3 7.3 26.7 18 33.3a29.6 29.6 0 01-8.5.2c-9-8-14.6-20-14.6-33.5 0-24 18.2-43.6 40.7-43.6zm5.4 81.4a35.6 35.6 0 0024.6-17.1h5.9a40.4 40.4 0 01-39 22.8c3-1.5 5.9-3.5 8.5-5.7zm24.8-58.2a37 37 0 00-12.6-12.8 29.6 29.6 0 018.5-.2c4 3.6 7.4 8 9.9 13z&quot; />%0A <path d=&quot;M41.8 1.1c1 0 2 0 3.1.2-3 1.5-5.9 3.4-8.5 5.6A37.5 37.5 0 006.1 44.7c0 21.1 16 38.3 35.7 38.3 12.6 0 23.6-7 30-17.6h5.8a40.4 40.4 0 01-35.8 23C19.3 88.4 1 68.8 1 44.7c0-24 18.2-43.6 40.7-43.6zm30.1 23.2a38.1 38.1 0 00-4.5-6.1c1.3-1.2 2.7-2.2 4.3-3 2.3 2.7 4.4 5.8 6 9.1z&quot; />%0A </g>%0A <path d=&quot;M76.1 34.4h9.2v-5H61.9v5H71v26h5.1zM92.6 52.9h13.7l3 7.4h5.3l-12.7-31.2h-4.7L84.5 60.3h5.2zm11.8-4.9h-9.9l5-12.4zM123.8 29.4h-4.6v31h20.6v-5h-16zM166.5 29.4H145v31h21.6v-5H150v-8.3h14.5v-4.9h-14.5v-8h16.4zM191.2 39.5c0 1.6-.5 2.8-1.6 3.8s-2.6 1.4-4.4 1.4h-7.4V34.3h7.4c1.9 0 3.4.4 4.4 1.3 1 .9 1.6 2.2 1.6 3.9zm6 20.8l-7.7-11.7c1-.3 1.9-.7 2.7-1.3a8.8 8.8 0 003.6-4.6c.4-1 .5-2.2.5-3.5 0-1.5-.2-2.9-.7-4.1a8.4 8.4 0 00-2.1-3.1c-1-.8-2-1.5-3.4-2-1.3-.4-2.8-.6-4.5-.6h-12.9v31h5V49.4h6.5l7 10.8z&quot; />%0A</svg>"
+ alt="GNU Taler" style="height: 1.5rem; margin: 0.5rem;"></a></div><span
+ class="flex items-center text-white text-lg font-bold ml-4">Challenger</span>
+ </div>
+ <div class="block flex-1 ml-6 "></div>
+ <div class="flex justify-end">
+ </div>
+ </div>
+ </header>
+
+ <main class="flex-1">
+
+ <div class="isolate bg-white px-6 py-12">
+ <div class="mx-auto max-w-2xl text-center">
+ <h2 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
+ Enter file name
+ </h2>
+ <p class="mt-2 text-lg leading-8 text-gray-600">
+ The file will be overwritten with a code which need to be entered in the next page.
+ </p>
+ </div>
+ <form action="/challenge/{{nonce}}" method="POST" class="mx-auto mt-16 max-w-xl sm:mt-20">
+ <div class="grid grid-cols-1 gap-x-8 gap-y-6">
+ <div class="sm:col-span-2">
+ <label for="phone" class="block text-sm font-semibold leading-6 text-gray-900">
+ Phone number
+ </label>
+ <div class="mt-2.5">
+ <input type="filename" name="filename" id="filename" maxlength="200"
+ class="block w-full rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6">
+ </div>
+ </div>
+
+ <p class="mt-3 text-sm leading-6 text-gray-400">
+ You can change the filename another {{changes_left}} times.
+ </p>
+ </div>
+
+ <div class="mt-10">
+ <button type="submit"
+ class="block w-full rounded-md bg-indigo-600 px-3.5 py-2.5 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">
+ Write file
+ </button>
+ </div>
+ </form>
+ </div>
+ </main>
+
+
+ <footer class="bottom-4 mb-4">
+ <div class="mt-8 mx-8 md:order-1 md:mt-0">
+ <div>
+ <p class="text-xs leading-5 text-gray-400">Learn more about <a target="_blank" rel="noreferrer noopener"
+ class="font-semibold text-gray-500 hover:text-gray-400" href="https://taler.net">GNU Taler</a></p>
+ </div>
+ <div style="flex-grow: 1;"></div>
+ <p class="text-xs leading-5 text-gray-400">Copyright © 2014—2023 Taler Systems SA. </p>
+ </div>
+ </footer>
+</body>
+
+</html>
diff --git a/packages/challenger-ui/src/enter-phone-form.html b/packages/challenger-ui/src/enter-phone-form.html
new file mode 100644
index 000000000..ca06fb94e
--- /dev/null
+++ b/packages/challenger-ui/src/enter-phone-form.html
@@ -0,0 +1,126 @@
+<!--
+ This file is part of GNU Taler
+ (C) 2021--2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+
+ @author Sebastian Javier Marchano
+-->
+<!DOCTYPE html>
+<html lang="en" class="h-full">
+
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
+ <meta name="taler-support" content="uri">
+ <meta name="mobile-web-app-capable" content="yes" />
+ <meta name="apple-mobile-web-app-capable" content="yes" />
+ <link rel="icon"
+ href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/Qn3//3ryn/795Tf/WrpP/2LCW/8B6T//w4Nb///////Pn4P+/d0v/9u3n/+7d0v/EhV7//v///+HDr//fxLD/zph2/+TJt//8/Pv/woBX//Lm3f/y5dz/v3hN//bu6f/JjGn/4sW0///////Df1j/8OLZ//v6+P+/elH/+vj1//jy7f+/elL//////+zYzP/Eg13//////967p//MlHT/wn5X///////v4Nb/yY1s///////jw7H/06KG////////////z5t9/+fNvf//////x4pn//Pp4v/8+vn/w39X/8WEX///////5s/A/9CbfP//////27Oc/9y2n////////////9itlf/gu6f//////86Vdf/r2Mz//////8SCXP/Df1j//////+7d0v/KkG7//////+HBrf/VpYr////////////RnoH/5sq6///////Ii2n/8ubf//39/P/Cf1j/xohk/+bNvv//////wn5W//Tq4//58/D/wHxV//7+/f/59fH/v3xU//39/P/w4Nf/xIFb///////hw7H/yo9t/+/f1f/AeU3/+/n2/+nSxP/FhmD//////9qzm//Upon/4MSx/96+qf//////xINc/+3bz//48e3/v3hN//Pn3///////6M+//752S//gw6//06aK/8J+VP/kzLr/zZd1/8OCWv/q18r/17KZ/9Ooi//fv6r/v3dK/+vWyP///////v39///////27un/1aeK/9Opjv/m1cf/1KCC/9a0nP/n08T/0Jx8/82YdP/QnHz/16yR//jx7P///////v39///////+/f3///7+///////+//7//v7+///////+/v7//v/+/////////////////////////v7//v79///////////////////+/v/+/Pv//v39///+/v/+/Pv///7+//7+/f/+/Pv//v39//79/P/+/Pv///7+////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" />
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
+ <link rel="stylesheet" href="main.css" />
+ <script type="module" src="main.js"></script>
+ <title>Enter contact details</title>
+</head>
+
+<body class="min-h-full flex flex-col">
+ <header class="bg-indigo-600 w-full mx-auto px-2 border-b border-opacity-25 border-indigo-400">
+ <div class="flex flex-row h-16 items-center ">
+ <div class="flex px-2 justify-start">
+ <div class="flex-shrink-0 bg-white rounded-lg"><a href="#"><img class="h-8 w-auto"
+ src="data:image/svg+xml,<?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?>%0A<svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 201 90&quot;>%0A <g fill=&quot;%230042b3&quot; fill-rule=&quot;evenodd&quot; stroke-width=&quot;.3&quot;>%0A <path d=&quot;M86.7 1.1c15.6 0 29 9.4 36 23.2h-5.9A35.1 35.1 0 0086.7 6.5C67 6.5 51 23.6 51 44.7c0 10.4 3.8 19.7 10 26.6a31.4 31.4 0 01-4.2 3A45.2 45.2 0 0146 44.7c0-24 18.2-43.6 40.7-43.6zm35.8 64.3a40.4 40.4 0 01-39 22.8c3-1.5 6-3.5 8.6-5.7a35.6 35.6 0 0024.6-17.1z&quot; />%0A <path d=&quot;M64.2 1.1l3.1.1c-3 1.6-5.9 3.5-8.5 5.8a37.5 37.5 0 00-30.2 37.7c0 14.3 7.3 26.7 18 33.3a29.6 29.6 0 01-8.5.2c-9-8-14.6-20-14.6-33.5 0-24 18.2-43.6 40.7-43.6zm5.4 81.4a35.6 35.6 0 0024.6-17.1h5.9a40.4 40.4 0 01-39 22.8c3-1.5 5.9-3.5 8.5-5.7zm24.8-58.2a37 37 0 00-12.6-12.8 29.6 29.6 0 018.5-.2c4 3.6 7.4 8 9.9 13z&quot; />%0A <path d=&quot;M41.8 1.1c1 0 2 0 3.1.2-3 1.5-5.9 3.4-8.5 5.6A37.5 37.5 0 006.1 44.7c0 21.1 16 38.3 35.7 38.3 12.6 0 23.6-7 30-17.6h5.8a40.4 40.4 0 01-35.8 23C19.3 88.4 1 68.8 1 44.7c0-24 18.2-43.6 40.7-43.6zm30.1 23.2a38.1 38.1 0 00-4.5-6.1c1.3-1.2 2.7-2.2 4.3-3 2.3 2.7 4.4 5.8 6 9.1z&quot; />%0A </g>%0A <path d=&quot;M76.1 34.4h9.2v-5H61.9v5H71v26h5.1zM92.6 52.9h13.7l3 7.4h5.3l-12.7-31.2h-4.7L84.5 60.3h5.2zm11.8-4.9h-9.9l5-12.4zM123.8 29.4h-4.6v31h20.6v-5h-16zM166.5 29.4H145v31h21.6v-5H150v-8.3h14.5v-4.9h-14.5v-8h16.4zM191.2 39.5c0 1.6-.5 2.8-1.6 3.8s-2.6 1.4-4.4 1.4h-7.4V34.3h7.4c1.9 0 3.4.4 4.4 1.3 1 .9 1.6 2.2 1.6 3.9zm6 20.8l-7.7-11.7c1-.3 1.9-.7 2.7-1.3a8.8 8.8 0 003.6-4.6c.4-1 .5-2.2.5-3.5 0-1.5-.2-2.9-.7-4.1a8.4 8.4 0 00-2.1-3.1c-1-.8-2-1.5-3.4-2-1.3-.4-2.8-.6-4.5-.6h-12.9v31h5V49.4h6.5l7 10.8z&quot; />%0A</svg>"
+ alt="GNU Taler" style="height: 1.5rem; margin: 0.5rem;"></a></div><span
+ class="flex items-center text-white text-lg font-bold ml-4">Challenger</span>
+ </div>
+ <div class="block flex-1 ml-6 "></div>
+ <div class="flex justify-end">
+ </div>
+ </div>
+ </header>
+
+ <main class="flex-1">
+
+ <div class="isolate bg-white px-6 py-12">
+ <div class="mx-auto max-w-2xl text-center">
+ <h2 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
+ Enter contact details
+ </h2>
+ <p class="mt-2 text-lg leading-8 text-gray-600">
+ You will receive an SMS with a TAN code that must be provided on the next page.
+ </p>
+ </div>
+ <form action="/challenge/{{nonce}}" method="POST" class="mx-auto mt-16 max-w-xl sm:mt-20">
+ <div class="grid grid-cols-1 gap-x-8 gap-y-6">
+ <div class="sm:col-span-2">
+ <label for="phone" class="block text-sm font-semibold leading-6 text-gray-900">
+ Phone number
+ </label>
+ <div class="mt-2.5">
+ <input type="phone" name="phone" id="phone" maxlength="20" autocomplete="tel"
+ class="block w-full rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6">
+ </div>
+ </div>
+
+ <script>
+ function check() {
+ var phone = document.getElementById('phone');
+ var phoneRepeat = document.getElementById('repeat-phone');
+
+ if (phone.value != phoneRepeat.value) {
+ phoneRepeat.setCustomValidity('The two phone numbers must match.');
+ } else {
+ // input is valid -- reset the error message
+ phoneRepeat.setCustomValidity('');
+ }
+ }
+ </script>
+
+ <div class="sm:col-span-2">
+ <label for="repeat-phone" class="block text-sm font-semibold leading-6 text-gray-900">
+ Repeat phone
+ </label>
+ <div class="mt-2.5">
+ <input oninput="check(this)" type="number" name="repeat-phone" id="repeat-phone" autocomplete="tel"
+ class="block w-full rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6">
+ </div>
+ </div>
+
+ <p class="mt-3 text-sm leading-6 text-gray-400">
+ You can change your phone number another {{changes_left}} times.
+ </p>
+ </div>
+
+ <div class="mt-10">
+ <button type="submit"
+ class="block w-full rounded-md bg-indigo-600 px-3.5 py-2.5 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">
+ Send SMS
+ </button>
+ </div>
+ </form>
+ </div>
+ </main>
+
+
+ <footer class="bottom-4 mb-4">
+ <div class="mt-8 mx-8 md:order-1 md:mt-0">
+ <div>
+ <p class="text-xs leading-5 text-gray-400">Learn more about <a target="_blank" rel="noreferrer noopener"
+ class="font-semibold text-gray-500 hover:text-gray-400" href="https://taler.net">GNU Taler</a></p>
+ </div>
+ <div style="flex-grow: 1;"></div>
+ <p class="text-xs leading-5 text-gray-400">Copyright © 2014—2023 Taler Systems SA. </p>
+ </div>
+ </footer>
+</body>
+
+</html>
diff --git a/packages/challenger-ui/src/enter-tan-form.html b/packages/challenger-ui/src/enter-tan-form.html
new file mode 100644
index 000000000..965f8e9d2
--- /dev/null
+++ b/packages/challenger-ui/src/enter-tan-form.html
@@ -0,0 +1,117 @@
+<!--
+ This file is part of GNU Taler
+ (C) 2021--2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+
+ @author Sebastian Javier Marchano
+-->
+<!DOCTYPE html>
+<html lang="en" class="h-full">
+
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
+ <meta name="taler-support" content="uri">
+ <meta name="mobile-web-app-capable" content="yes" />
+ <meta name="apple-mobile-web-app-capable" content="yes" />
+ <link rel="icon"
+ href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/Qn3//3ryn/795Tf/WrpP/2LCW/8B6T//w4Nb///////Pn4P+/d0v/9u3n/+7d0v/EhV7//v///+HDr//fxLD/zph2/+TJt//8/Pv/woBX//Lm3f/y5dz/v3hN//bu6f/JjGn/4sW0///////Df1j/8OLZ//v6+P+/elH/+vj1//jy7f+/elL//////+zYzP/Eg13//////967p//MlHT/wn5X///////v4Nb/yY1s///////jw7H/06KG////////////z5t9/+fNvf//////x4pn//Pp4v/8+vn/w39X/8WEX///////5s/A/9CbfP//////27Oc/9y2n////////////9itlf/gu6f//////86Vdf/r2Mz//////8SCXP/Df1j//////+7d0v/KkG7//////+HBrf/VpYr////////////RnoH/5sq6///////Ii2n/8ubf//39/P/Cf1j/xohk/+bNvv//////wn5W//Tq4//58/D/wHxV//7+/f/59fH/v3xU//39/P/w4Nf/xIFb///////hw7H/yo9t/+/f1f/AeU3/+/n2/+nSxP/FhmD//////9qzm//Upon/4MSx/96+qf//////xINc/+3bz//48e3/v3hN//Pn3///////6M+//752S//gw6//06aK/8J+VP/kzLr/zZd1/8OCWv/q18r/17KZ/9Ooi//fv6r/v3dK/+vWyP///////v39///////27un/1aeK/9Opjv/m1cf/1KCC/9a0nP/n08T/0Jx8/82YdP/QnHz/16yR//jx7P///////v39///////+/f3///7+///////+//7//v7+///////+/v7//v/+/////////////////////////v7//v79///////////////////+/v/+/Pv//v39///+/v/+/Pv///7+//7+/f/+/Pv//v39//79/P/+/Pv///7+////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" />
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
+ <link rel="stylesheet" href="main.css" />
+ <script type="module" src="main.js"></script>
+ <title>Enter your TAN</title>
+</head>
+
+<body class="min-h-full flex flex-col">
+ <header class="bg-indigo-600 w-full mx-auto px-2 border-b border-opacity-25 border-indigo-400">
+ <div class="flex flex-row h-16 items-center ">
+ <div class="flex px-2 justify-start">
+ <div class="flex-shrink-0 bg-white rounded-lg"><a href="#"><img class="h-8 w-auto"
+ src="data:image/svg+xml,<?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?>%0A<svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 201 90&quot;>%0A <g fill=&quot;%230042b3&quot; fill-rule=&quot;evenodd&quot; stroke-width=&quot;.3&quot;>%0A <path d=&quot;M86.7 1.1c15.6 0 29 9.4 36 23.2h-5.9A35.1 35.1 0 0086.7 6.5C67 6.5 51 23.6 51 44.7c0 10.4 3.8 19.7 10 26.6a31.4 31.4 0 01-4.2 3A45.2 45.2 0 0146 44.7c0-24 18.2-43.6 40.7-43.6zm35.8 64.3a40.4 40.4 0 01-39 22.8c3-1.5 6-3.5 8.6-5.7a35.6 35.6 0 0024.6-17.1z&quot; />%0A <path d=&quot;M64.2 1.1l3.1.1c-3 1.6-5.9 3.5-8.5 5.8a37.5 37.5 0 00-30.2 37.7c0 14.3 7.3 26.7 18 33.3a29.6 29.6 0 01-8.5.2c-9-8-14.6-20-14.6-33.5 0-24 18.2-43.6 40.7-43.6zm5.4 81.4a35.6 35.6 0 0024.6-17.1h5.9a40.4 40.4 0 01-39 22.8c3-1.5 5.9-3.5 8.5-5.7zm24.8-58.2a37 37 0 00-12.6-12.8 29.6 29.6 0 018.5-.2c4 3.6 7.4 8 9.9 13z&quot; />%0A <path d=&quot;M41.8 1.1c1 0 2 0 3.1.2-3 1.5-5.9 3.4-8.5 5.6A37.5 37.5 0 006.1 44.7c0 21.1 16 38.3 35.7 38.3 12.6 0 23.6-7 30-17.6h5.8a40.4 40.4 0 01-35.8 23C19.3 88.4 1 68.8 1 44.7c0-24 18.2-43.6 40.7-43.6zm30.1 23.2a38.1 38.1 0 00-4.5-6.1c1.3-1.2 2.7-2.2 4.3-3 2.3 2.7 4.4 5.8 6 9.1z&quot; />%0A </g>%0A <path d=&quot;M76.1 34.4h9.2v-5H61.9v5H71v26h5.1zM92.6 52.9h13.7l3 7.4h5.3l-12.7-31.2h-4.7L84.5 60.3h5.2zm11.8-4.9h-9.9l5-12.4zM123.8 29.4h-4.6v31h20.6v-5h-16zM166.5 29.4H145v31h21.6v-5H150v-8.3h14.5v-4.9h-14.5v-8h16.4zM191.2 39.5c0 1.6-.5 2.8-1.6 3.8s-2.6 1.4-4.4 1.4h-7.4V34.3h7.4c1.9 0 3.4.4 4.4 1.3 1 .9 1.6 2.2 1.6 3.9zm6 20.8l-7.7-11.7c1-.3 1.9-.7 2.7-1.3a8.8 8.8 0 003.6-4.6c.4-1 .5-2.2.5-3.5 0-1.5-.2-2.9-.7-4.1a8.4 8.4 0 00-2.1-3.1c-1-.8-2-1.5-3.4-2-1.3-.4-2.8-.6-4.5-.6h-12.9v31h5V49.4h6.5l7 10.8z&quot; />%0A</svg>"
+ alt="GNU Taler" style="height: 1.5rem; margin: 0.5rem;"></a></div><span
+ class="flex items-center text-white text-lg font-bold ml-4">Challenger</span>
+ </div>
+ <div class="block flex-1 ml-6 "></div>
+ <div class="flex justify-end">
+ </div>
+ </div>
+ </header>
+
+ <main class="flex-1">
+
+ <div class="isolate bg-white px-6 py-12">
+ <div class="mx-auto max-w-2xl text-center">
+ <h2 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
+ Please enter the TAN you received to authenticate.
+ </h2>
+ <p class="mt-2 text-lg leading-8 text-gray-600">
+ <!-- {{#transmitted}}
+ A TAN was sent to your address &quot;{{address}}&quot;.
+ {{/transmitted}} -->
+ <!-- {{^transmitted}} -->
+ We recently already sent a TAN to your address &quot;{{address}}&quot;.
+ A new TAN will not be transmitted again before {{next_tx_time}}.
+ <!-- {{/transmitted}} -->
+ </p>
+ </div>
+
+
+ <form action="/solve/{{nonce}}" method="POST" class="mx-auto mt-16 max-w-xl sm:mt-20">
+ <div class="grid grid-cols-1 gap-x-8 gap-y-6">
+ <div class="sm:col-span-2">
+ <label for="pin" class="block text-sm font-semibold leading-6 text-gray-900">
+ TAN code
+ </label>
+ <div class="mt-2.5">
+ <div
+ class="flex rounded-md shadow-sm ring-1 ring-inset ring-gray-300 focus-within:ring-2 focus-within:ring-inset focus-within:ring-indigo-600">
+ <span class="flex select-none items-center pl-3 text-gray-500 sm:text-sm">TAN:</span>
+ <input type="number" name="pin" id="pin" maxlength="64"
+ class="block flex-1 border-0 bg-transparent py-1.5 pl-1 text-gray-900 placeholder:text-gray-400 focus:ring-0 sm:text-sm sm:leading-6"
+ placeholder="12345678">
+ </div>
+
+ </div>
+ </div>
+
+ <p class="mt-3 text-sm leading-6 text-gray-400">
+ You have {{attempts_left}} attempts left.
+ </p>
+ </div>
+
+ <div class="mt-10">
+ <button type="submit"
+ class="block w-full rounded-md bg-indigo-600 px-3.5 py-2.5 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">
+ Check
+ </button>
+ </div>
+ </form>
+
+ </div>
+ </main>
+
+ <footer class="bottom-4 mb-4">
+ <div class="mt-8 mx-8 md:order-1 md:mt-0">
+ <div>
+ <p class="text-xs leading-5 text-gray-400">Learn more about <a target="_blank" rel="noreferrer noopener"
+ class="font-semibold text-gray-500 hover:text-gray-400" href="https://taler.net">GNU Taler</a></p>
+ </div>
+ <div style="flex-grow: 1;"></div>
+ <p class="text-xs leading-5 text-gray-400">Copyright © 2014—2023 Taler Systems SA. </p>
+ </div>
+ </footer>
+
+</body>
+
+</html>
diff --git a/packages/challenger-ui/src/hooks/challenge.ts b/packages/challenger-ui/src/hooks/challenge.ts
new file mode 100644
index 000000000..846242816
--- /dev/null
+++ b/packages/challenger-ui/src/hooks/challenge.ts
@@ -0,0 +1,58 @@
+/*
+ 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 {
+ ChallengerResultByMethod,
+ TalerHttpError,
+} from "@gnu-taler/taler-util";
+import { useChallengerApiContext } from "@gnu-taler/web-util/browser";
+import _useSWR, { SWRHook, mutate } from "swr";
+import { SessionId } from "./session.js";
+const useSWR = _useSWR as unknown as SWRHook;
+
+export function revalidateChallengeSession() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "login",
+ undefined,
+ { revalidate: true },
+ );
+}
+
+export function useChallengeSession(
+ nonce: string,
+ session: SessionId | undefined,
+) {
+ const {
+ lib: { challenger: api },
+ } = useChallengerApiContext();
+
+ async function fetcher([n, c, r, s]: [string, string, string, string]) {
+ return await api.login(n, c, r, s);
+ }
+ const { data, error } = useSWR<
+ ChallengerResultByMethod<"login">,
+ TalerHttpError
+ >(
+ !session
+ ? undefined
+ : [nonce, session.clientId, session.redirectURL, session.state, "login"],
+ fetcher,
+ {},
+ );
+
+ if (data) return data;
+ if (error) return error;
+ return undefined;
+}
diff --git a/packages/challenger-ui/src/hooks/session.ts b/packages/challenger-ui/src/hooks/session.ts
new file mode 100644
index 000000000..ed7ea8986
--- /dev/null
+++ b/packages/challenger-ui/src/hooks/session.ts
@@ -0,0 +1,143 @@
+/*
+ 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 {
+ ChallengerApi,
+ Codec,
+ buildCodecForObject,
+ codecForBoolean,
+ codecForChallengeStatus,
+ codecForNumber,
+ codecForString,
+ codecForStringURL,
+ codecOptional,
+} from "@gnu-taler/taler-util";
+import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser";
+import { mutate } from "swr";
+
+/**
+ * Has the information to reach and
+ * authenticate at the bank's backend.
+ */
+export type SessionId = {
+ clientId: string;
+ redirectURL: string;
+ state: string;
+};
+
+export type LastChallengeResponse = {
+ attemptsLeft: number;
+ nextSend: string;
+ transmitted: boolean;
+};
+
+export type SessionState = SessionId & {
+ lastTry: LastChallengeResponse | undefined;
+ lastStatus: ChallengerApi.ChallengeStatus | undefined;
+ completedURL: string | undefined;
+};
+export const codecForLastChallengeResponse = (): Codec<LastChallengeResponse> =>
+ buildCodecForObject<LastChallengeResponse>()
+ .property("attemptsLeft", codecForNumber())
+ .property("nextSend", codecForString())
+ .property("transmitted", codecForBoolean())
+ .build("LastChallengeResponse");
+
+export const codecForSessionState = (): Codec<SessionState> =>
+ buildCodecForObject<SessionState>()
+ .property("clientId", codecForString())
+ .property("redirectURL", codecForStringURL())
+ .property("completedURL", codecOptional(codecForStringURL()))
+ .property("state", codecForString())
+ .property("lastStatus", codecOptional(codecForChallengeStatus()))
+ .property("lastTry", codecOptional(codecForLastChallengeResponse()))
+ .build("SessionState");
+
+export interface SessionStateHandler {
+ state: SessionState | undefined;
+ start(s: SessionId): void;
+ accepted(l: LastChallengeResponse): void;
+ completed(e: URL): void;
+ updateStatus(s: ChallengerApi.ChallengeStatus): void;
+}
+
+const SESSION_STATE_KEY = buildStorageKey(
+ "challenger-session",
+ codecForSessionState(),
+);
+
+/**
+ * Return getters and setters for
+ * login credentials and backend's
+ * base URL.
+ */
+export function useSessionState(): SessionStateHandler {
+ const { value: state, update } = useLocalStorage(SESSION_STATE_KEY);
+
+ return {
+ state,
+ start(info) {
+ update({
+ ...info,
+ lastTry: undefined,
+ completedURL: undefined,
+ lastStatus: undefined,
+ });
+ cleanAllCache();
+ },
+ accepted(lastTry) {
+ if (!state) return;
+ update({
+ ...state,
+ lastTry,
+ });
+ },
+ completed(url) {
+ if (!state) return;
+ update({
+ ...state,
+ completedURL: url.href,
+ });
+ },
+ updateStatus(st: ChallengerApi.ChallengeStatus) {
+ if (!state) return;
+ if (!state.lastStatus) {
+ update({
+ ...state,
+ lastStatus: st,
+ });
+ return;
+ }
+ // current status
+ const ls = state.lastStatus;
+ if (
+ ls.changes_left !== st.changes_left ||
+ ls.fix_address !== st.fix_address ||
+ ls.last_address !== st.last_address
+ ) {
+ update({
+ ...state,
+ lastStatus: st,
+ });
+ return;
+ }
+ },
+ };
+}
+
+function cleanAllCache(): void {
+ mutate(() => true, undefined, { revalidate: false });
+}
diff --git a/packages/challenger-ui/src/i18n/challenger-ui.pot b/packages/challenger-ui/src/i18n/challenger-ui.pot
new file mode 100644
index 000000000..5d2497acf
--- /dev/null
+++ b/packages/challenger-ui/src/i18n/challenger-ui.pot
@@ -0,0 +1,199 @@
+# This file is part of GNU Taler
+# (C) 2022-2024 Taler Systems S.A.
+#
+# GNU Taler is free software; you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 3, or (at your option) any later version.
+#
+# GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Bank\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+#: src/pages/AnswerChallenge.tsx:55
+#, c-format
+msgid "Can't be empty"
+msgstr ""
+
+#: src/pages/AnswerChallenge.tsx:81
+#, c-format
+msgstr ""
+
+#: src/pages/AnswerChallenge.tsx:108
+#, c-format
+msgid "Invalid request"
+msgstr ""
+
+#: src/pages/AnswerChallenge.tsx:111
+#, c-format
+msgid "Invalid pin"
+msgstr ""
+
+#: src/pages/AnswerChallenge.tsx:142
+#, c-format
+msgid "Please enter the TAN you received to authenticate."
+msgstr ""
+
+#: src/pages/AnswerChallenge.tsx:147
+#, c-format
+msgid "A TAN was sent to your address &quot;%1$s&quot;."
+msgstr ""
+
+#: src/pages/AnswerChallenge.tsx:151
+#, c-format
+msgid ""
+"We recently already sent a TAN to your address &quot; %1$s&quot;. A new TAN will "
+"not be transmitted again before %2$s."
+msgstr ""
+
+#: src/pages/AnswerChallenge.tsx:161
+#, c-format
+msgid "You can try another PIN but just %1$s times more."
+msgstr ""
+
+#: src/pages/AnswerChallenge.tsx:181
+#, c-format
+msgid "TAN code"
+msgstr ""
+
+#: src/pages/AnswerChallenge.tsx:204
+#, c-format
+msgid "You have %1$s attempts left."
+msgstr ""
+
+#: src/pages/AnswerChallenge.tsx:217
+#, c-format
+msgid "Check"
+msgstr ""
+
+#: src/pages/AnswerChallenge.tsx:227
+#, c-format
+msgid "Send again"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:76
+#, c-format
+msgid "required"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:84
+#, c-format
+msgid "invalid email"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:86
+#, c-format
+msgid "emails don't match"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:130
+#, c-format
+msgid "Enter contact details"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:133
+#, c-format
+msgid ""
+"You will receive an email with a TAN code that must be provided on the next "
+"page."
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:152
+#, c-format
+msgid "Email"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:180
+#, c-format
+msgid "Repeat email"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:198
+#, c-format
+msgid "You can change your email address another %1$s times."
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:211
+#, c-format
+msgid "Send email"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:237
+#, c-format
+msgid "Bad request"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:238
+#, c-format
+msgid "Could not start the challenge, check configuration."
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:246
+#, c-format
+msgid "Not found"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:247
+#, c-format
+msgid "Nonce not found"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:253
+#, c-format
+msgid "Not acceptable"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:254
+#, c-format
+msgid "Server has wrong template configuration"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:262
+#, c-format
+msgid "Internal error"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:263
+#, c-format
+msgid "Check logs"
+msgstr ""
+
+#: src/pages/NonceNotFound.tsx:33
+#, c-format
+msgid "The URL is wrong"
+msgstr ""
+
+#: src/pages/NonceNotFound.tsx:36
+#, c-format
+msgid "Maybe the validation check expired."
+msgstr ""
+
+#: src/pages/Setup.tsx:53
+#, c-format
+msgid "Client doesn't exist."
+msgstr ""
+
+#: src/pages/Setup.tsx:65
+#, c-format
+msgid "Setup new challenge with client ID: &quot;%1$s&quot;"
+msgstr ""
+
+#: src/pages/Setup.tsx:76
+#, c-format
+msgid "Start"
+msgstr ""
+
diff --git a/packages/challenger-ui/src/i18n/poheader b/packages/challenger-ui/src/i18n/poheader
new file mode 100644
index 000000000..f2c9d10dd
--- /dev/null
+++ b/packages/challenger-ui/src/i18n/poheader
@@ -0,0 +1,26 @@
+# This file is part of GNU Taler
+# (C) 2022-2024 Taler Systems S.A.
+#
+# GNU Taler is free software; you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 3, or (at your option) any later version.
+#
+# GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: Challenger UI\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
diff --git a/packages/challenger-ui/src/i18n/strings.ts b/packages/challenger-ui/src/i18n/strings.ts
new file mode 100644
index 000000000..ea13fed2e
--- /dev/null
+++ b/packages/challenger-ui/src/i18n/strings.ts
@@ -0,0 +1,90 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+export interface StringsType {
+ // X-Domain or 'messages'
+ domain: string;
+ lang: string;
+ completeness: number;
+ plural_forms: string;
+ locale_data: {
+ messages: Record<string, unknown>;
+ };
+}
+export const strings: Record<string, StringsType> = {};
+
+strings["it"] = {
+ locale_data: {
+ messages: {
+ "": {
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=n != 1;",
+ lang: "it",
+ },
+ }
+ },
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=n != 1;",
+ lang: "it",
+ completeness: 14,
+};
+
+strings["es"] = {
+ locale_data: {
+ messages: {
+ "": {
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=n != 1;",
+ lang: "es",
+ },
+ }
+ },
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=n != 1;",
+ lang: "es",
+ completeness: 100,
+};
+
+strings["en"] = {
+ locale_data: {
+ messages: {
+ "": {
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=n != 1;",
+ lang: "en",
+ },
+ }
+ },
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=(n != 1);",
+ lang: "en",
+ completeness: 100,
+};
+
+strings["de"] = {
+ locale_data: {
+ messages: {
+ "": {
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=n != 1;",
+ lang: "de",
+ },
+ }
+ },
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=n != 1;",
+ lang: "de",
+ completeness: 4,
+};
diff --git a/packages/challenger-ui/src/index.html b/packages/challenger-ui/src/index.html
new file mode 100644
index 000000000..18f472045
--- /dev/null
+++ b/packages/challenger-ui/src/index.html
@@ -0,0 +1,41 @@
+<!--
+ This file is part of GNU Taler
+ (C) 2021--2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+
+ @author Sebastian Javier Marchano
+-->
+<!doctype html>
+<html lang="en" class="h-full bg-gray-100">
+ <head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
+ <meta name="taler-support" content="uri,api" />
+ <meta name="mobile-web-app-capable" content="yes" />
+ <meta name="apple-mobile-web-app-capable" content="yes" />
+ <link
+ rel="icon"
+ href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/Qn3//3ryn/795Tf/WrpP/2LCW/8B6T//w4Nb///////Pn4P+/d0v/9u3n/+7d0v/EhV7//v///+HDr//fxLD/zph2/+TJt//8/Pv/woBX//Lm3f/y5dz/v3hN//bu6f/JjGn/4sW0///////Df1j/8OLZ//v6+P+/elH/+vj1//jy7f+/elL//////+zYzP/Eg13//////967p//MlHT/wn5X///////v4Nb/yY1s///////jw7H/06KG////////////z5t9/+fNvf//////x4pn//Pp4v/8+vn/w39X/8WEX///////5s/A/9CbfP//////27Oc/9y2n////////////9itlf/gu6f//////86Vdf/r2Mz//////8SCXP/Df1j//////+7d0v/KkG7//////+HBrf/VpYr////////////RnoH/5sq6///////Ii2n/8ubf//39/P/Cf1j/xohk/+bNvv//////wn5W//Tq4//58/D/wHxV//7+/f/59fH/v3xU//39/P/w4Nf/xIFb///////hw7H/yo9t/+/f1f/AeU3/+/n2/+nSxP/FhmD//////9qzm//Upon/4MSx/96+qf//////xINc/+3bz//48e3/v3hN//Pn3///////6M+//752S//gw6//06aK/8J+VP/kzLr/zZd1/8OCWv/q18r/17KZ/9Ooi//fv6r/v3dK/+vWyP///////v39///////27un/1aeK/9Opjv/m1cf/1KCC/9a0nP/n08T/0Jx8/82YdP/QnHz/16yR//jx7P///////v39///////+/f3///7+///////+//7//v7+///////+/v7//v/+/////////////////////////v7//v79///////////////////+/v/+/Pv//v39///+/v/+/Pv///7+//7+/f/+/Pv//v39//79/P/+/Pv///7+////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
+ />
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
+ <title>Challenger</title>
+ <!-- Entry point for the SPA. -->
+ <script type="module" src="index.js"></script>
+ <link rel="stylesheet" href="index.css" />
+ </head>
+
+ <body class="h-full">
+ <div id="app"></div>
+ </body>
+</html>
diff --git a/packages/challenger-ui/src/index.tsx b/packages/challenger-ui/src/index.tsx
new file mode 100644
index 000000000..f559288a3
--- /dev/null
+++ b/packages/challenger-ui/src/index.tsx
@@ -0,0 +1,27 @@
+/*
+ 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 { App } from "./app.js";
+import { h, render } from "preact";
+import "./scss/main.css";
+
+const app = document.getElementById("app");
+
+if (app) {
+ render(<App />, app);
+} else {
+ console.error("HTML element with id 'app' not found.");
+}
diff --git a/packages/challenger-ui/src/internal-error.html b/packages/challenger-ui/src/internal-error.html
new file mode 100644
index 000000000..521a2b69e
--- /dev/null
+++ b/packages/challenger-ui/src/internal-error.html
@@ -0,0 +1,89 @@
+<!--
+ This file is part of GNU Taler
+ (C) 2021--2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+
+ @author Sebastian Javier Marchano
+-->
+<!DOCTYPE html>
+<html lang="en" class="h-full">
+
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
+ <meta name="taler-support" content="uri">
+ <meta name="mobile-web-app-capable" content="yes" />
+ <meta name="apple-mobile-web-app-capable" content="yes" />
+ <link rel="icon"
+ href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/Qn3//3ryn/795Tf/WrpP/2LCW/8B6T//w4Nb///////Pn4P+/d0v/9u3n/+7d0v/EhV7//v///+HDr//fxLD/zph2/+TJt//8/Pv/woBX//Lm3f/y5dz/v3hN//bu6f/JjGn/4sW0///////Df1j/8OLZ//v6+P+/elH/+vj1//jy7f+/elL//////+zYzP/Eg13//////967p//MlHT/wn5X///////v4Nb/yY1s///////jw7H/06KG////////////z5t9/+fNvf//////x4pn//Pp4v/8+vn/w39X/8WEX///////5s/A/9CbfP//////27Oc/9y2n////////////9itlf/gu6f//////86Vdf/r2Mz//////8SCXP/Df1j//////+7d0v/KkG7//////+HBrf/VpYr////////////RnoH/5sq6///////Ii2n/8ubf//39/P/Cf1j/xohk/+bNvv//////wn5W//Tq4//58/D/wHxV//7+/f/59fH/v3xU//39/P/w4Nf/xIFb///////hw7H/yo9t/+/f1f/AeU3/+/n2/+nSxP/FhmD//////9qzm//Upon/4MSx/96+qf//////xINc/+3bz//48e3/v3hN//Pn3///////6M+//752S//gw6//06aK/8J+VP/kzLr/zZd1/8OCWv/q18r/17KZ/9Ooi//fv6r/v3dK/+vWyP///////v39///////27un/1aeK/9Opjv/m1cf/1KCC/9a0nP/n08T/0Jx8/82YdP/QnHz/16yR//jx7P///////v39///////+/f3///7+///////+//7//v7+///////+/v7//v/+/////////////////////////v7//v79///////////////////+/v/+/Pv//v39///+/v/+/Pv///7+//7+/f/+/Pv//v39//79/P/+/Pv///7+////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" />
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
+ <link rel="stylesheet" href="main.css" />
+ <script type="module" src="main.js"></script>
+ <title>Internal server error (#{{ec}})</title>
+</head>
+
+<body class="min-h-full flex flex-col">
+ <header class="bg-indigo-600 w-full mx-auto px-2 border-b border-opacity-25 border-indigo-400">
+ <div class="flex flex-row h-16 items-center ">
+ <div class="flex px-2 justify-start">
+ <div class="flex-shrink-0 bg-white rounded-lg"><a href="#"><img class="h-8 w-auto"
+ src="data:image/svg+xml,<?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?>%0A<svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 201 90&quot;>%0A <g fill=&quot;%230042b3&quot; fill-rule=&quot;evenodd&quot; stroke-width=&quot;.3&quot;>%0A <path d=&quot;M86.7 1.1c15.6 0 29 9.4 36 23.2h-5.9A35.1 35.1 0 0086.7 6.5C67 6.5 51 23.6 51 44.7c0 10.4 3.8 19.7 10 26.6a31.4 31.4 0 01-4.2 3A45.2 45.2 0 0146 44.7c0-24 18.2-43.6 40.7-43.6zm35.8 64.3a40.4 40.4 0 01-39 22.8c3-1.5 6-3.5 8.6-5.7a35.6 35.6 0 0024.6-17.1z&quot; />%0A <path d=&quot;M64.2 1.1l3.1.1c-3 1.6-5.9 3.5-8.5 5.8a37.5 37.5 0 00-30.2 37.7c0 14.3 7.3 26.7 18 33.3a29.6 29.6 0 01-8.5.2c-9-8-14.6-20-14.6-33.5 0-24 18.2-43.6 40.7-43.6zm5.4 81.4a35.6 35.6 0 0024.6-17.1h5.9a40.4 40.4 0 01-39 22.8c3-1.5 5.9-3.5 8.5-5.7zm24.8-58.2a37 37 0 00-12.6-12.8 29.6 29.6 0 018.5-.2c4 3.6 7.4 8 9.9 13z&quot; />%0A <path d=&quot;M41.8 1.1c1 0 2 0 3.1.2-3 1.5-5.9 3.4-8.5 5.6A37.5 37.5 0 006.1 44.7c0 21.1 16 38.3 35.7 38.3 12.6 0 23.6-7 30-17.6h5.8a40.4 40.4 0 01-35.8 23C19.3 88.4 1 68.8 1 44.7c0-24 18.2-43.6 40.7-43.6zm30.1 23.2a38.1 38.1 0 00-4.5-6.1c1.3-1.2 2.7-2.2 4.3-3 2.3 2.7 4.4 5.8 6 9.1z&quot; />%0A </g>%0A <path d=&quot;M76.1 34.4h9.2v-5H61.9v5H71v26h5.1zM92.6 52.9h13.7l3 7.4h5.3l-12.7-31.2h-4.7L84.5 60.3h5.2zm11.8-4.9h-9.9l5-12.4zM123.8 29.4h-4.6v31h20.6v-5h-16zM166.5 29.4H145v31h21.6v-5H150v-8.3h14.5v-4.9h-14.5v-8h16.4zM191.2 39.5c0 1.6-.5 2.8-1.6 3.8s-2.6 1.4-4.4 1.4h-7.4V34.3h7.4c1.9 0 3.4.4 4.4 1.3 1 .9 1.6 2.2 1.6 3.9zm6 20.8l-7.7-11.7c1-.3 1.9-.7 2.7-1.3a8.8 8.8 0 003.6-4.6c.4-1 .5-2.2.5-3.5 0-1.5-.2-2.9-.7-4.1a8.4 8.4 0 00-2.1-3.1c-1-.8-2-1.5-3.4-2-1.3-.4-2.8-.6-4.5-.6h-12.9v31h5V49.4h6.5l7 10.8z&quot; />%0A</svg>"
+ alt="GNU Taler" style="height: 1.5rem; margin: 0.5rem;"></a></div><span
+ class="flex items-center text-white text-lg font-bold ml-4">Challenger</span>
+ </div>
+ <div class="block flex-1 ml-6 "></div>
+ <div class="flex justify-end">
+ </div>
+ </div>
+ </header>
+
+ <main class="flex-1">
+ <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 items-center mt-4">
+ <div class="rounded-md bg-red-50 p-4 shadow-xl">
+ <div class="flex">
+ <div class="flex-shrink-0">
+ <svg xmlns="http://www.w3.org/2000/svg" stroke="none" viewBox="0 0 24 24" fill="currentColor"
+ class="w-8 h-8 text-red-400">
+ <path fill-rule="evenodd"
+ d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z" />
+ </svg>
+ </div>
+ <div class="ml-3">
+ <h3 class="text-sm font-medium text-red-800">
+ Internal error
+ </h3>
+ <div class="mt-2 text-sm text-red-700">
+ <p>{{hint}} ({{detail}})</p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ </main>
+
+ <footer class="bottom-4 mb-4">
+ <div class="mt-8 mx-8 md:order-1 md:mt-0">
+ <div>
+ <p class="text-xs leading-5 text-gray-400">Learn more about <a target="_blank" rel="noreferrer noopener"
+ class="font-semibold text-gray-500 hover:text-gray-400" href="https://taler.net">GNU Taler</a></p>
+ </div>
+ <div style="flex-grow: 1;"></div>
+ <p class="text-xs leading-5 text-gray-400">Copyright © 2014—2023 Taler Systems SA. </p>
+ </div>
+ </footer>
+
+</body>
+
+</html> \ No newline at end of file
diff --git a/packages/challenger-ui/src/invalid-pin.html b/packages/challenger-ui/src/invalid-pin.html
new file mode 100644
index 000000000..1229b8095
--- /dev/null
+++ b/packages/challenger-ui/src/invalid-pin.html
@@ -0,0 +1,87 @@
+<!--
+ This file is part of GNU Taler
+ (C) 2021--2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+
+ @author Sebastian Javier Marchano
+-->
+<!DOCTYPE html>
+<html lang="en" class="h-full">
+
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
+ <meta name="taler-support" content="uri">
+ <meta name="mobile-web-app-capable" content="yes" />
+ <meta name="apple-mobile-web-app-capable" content="yes" />
+ <link rel="icon"
+ href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/Qn3//3ryn/795Tf/WrpP/2LCW/8B6T//w4Nb///////Pn4P+/d0v/9u3n/+7d0v/EhV7//v///+HDr//fxLD/zph2/+TJt//8/Pv/woBX//Lm3f/y5dz/v3hN//bu6f/JjGn/4sW0///////Df1j/8OLZ//v6+P+/elH/+vj1//jy7f+/elL//////+zYzP/Eg13//////967p//MlHT/wn5X///////v4Nb/yY1s///////jw7H/06KG////////////z5t9/+fNvf//////x4pn//Pp4v/8+vn/w39X/8WEX///////5s/A/9CbfP//////27Oc/9y2n////////////9itlf/gu6f//////86Vdf/r2Mz//////8SCXP/Df1j//////+7d0v/KkG7//////+HBrf/VpYr////////////RnoH/5sq6///////Ii2n/8ubf//39/P/Cf1j/xohk/+bNvv//////wn5W//Tq4//58/D/wHxV//7+/f/59fH/v3xU//39/P/w4Nf/xIFb///////hw7H/yo9t/+/f1f/AeU3/+/n2/+nSxP/FhmD//////9qzm//Upon/4MSx/96+qf//////xINc/+3bz//48e3/v3hN//Pn3///////6M+//752S//gw6//06aK/8J+VP/kzLr/zZd1/8OCWv/q18r/17KZ/9Ooi//fv6r/v3dK/+vWyP///////v39///////27un/1aeK/9Opjv/m1cf/1KCC/9a0nP/n08T/0Jx8/82YdP/QnHz/16yR//jx7P///////v39///////+/f3///7+///////+//7//v7+///////+/v7//v/+/////////////////////////v7//v79///////////////////+/v/+/Pv//v39///+/v/+/Pv///7+//7+/f/+/Pv//v39//79/P/+/Pv///7+////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" />
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
+ <link rel="stylesheet" href="main.css" />
+ <script type="module" src="main.js"></script>
+ <title>Invalid solution (#{{ec}})</title>
+</head>
+
+<body class="min-h-full flex flex-col">
+ <header class="bg-indigo-600 w-full mx-auto px-2 border-b border-opacity-25 border-indigo-400">
+ <div class="flex flex-row h-16 items-center ">
+ <div class="flex px-2 justify-start">
+ <div class="flex-shrink-0 bg-white rounded-lg"><a href="#"><img class="h-8 w-auto"
+ src="data:image/svg+xml,<?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?>%0A<svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 201 90&quot;>%0A <g fill=&quot;%230042b3&quot; fill-rule=&quot;evenodd&quot; stroke-width=&quot;.3&quot;>%0A <path d=&quot;M86.7 1.1c15.6 0 29 9.4 36 23.2h-5.9A35.1 35.1 0 0086.7 6.5C67 6.5 51 23.6 51 44.7c0 10.4 3.8 19.7 10 26.6a31.4 31.4 0 01-4.2 3A45.2 45.2 0 0146 44.7c0-24 18.2-43.6 40.7-43.6zm35.8 64.3a40.4 40.4 0 01-39 22.8c3-1.5 6-3.5 8.6-5.7a35.6 35.6 0 0024.6-17.1z&quot; />%0A <path d=&quot;M64.2 1.1l3.1.1c-3 1.6-5.9 3.5-8.5 5.8a37.5 37.5 0 00-30.2 37.7c0 14.3 7.3 26.7 18 33.3a29.6 29.6 0 01-8.5.2c-9-8-14.6-20-14.6-33.5 0-24 18.2-43.6 40.7-43.6zm5.4 81.4a35.6 35.6 0 0024.6-17.1h5.9a40.4 40.4 0 01-39 22.8c3-1.5 5.9-3.5 8.5-5.7zm24.8-58.2a37 37 0 00-12.6-12.8 29.6 29.6 0 018.5-.2c4 3.6 7.4 8 9.9 13z&quot; />%0A <path d=&quot;M41.8 1.1c1 0 2 0 3.1.2-3 1.5-5.9 3.4-8.5 5.6A37.5 37.5 0 006.1 44.7c0 21.1 16 38.3 35.7 38.3 12.6 0 23.6-7 30-17.6h5.8a40.4 40.4 0 01-35.8 23C19.3 88.4 1 68.8 1 44.7c0-24 18.2-43.6 40.7-43.6zm30.1 23.2a38.1 38.1 0 00-4.5-6.1c1.3-1.2 2.7-2.2 4.3-3 2.3 2.7 4.4 5.8 6 9.1z&quot; />%0A </g>%0A <path d=&quot;M76.1 34.4h9.2v-5H61.9v5H71v26h5.1zM92.6 52.9h13.7l3 7.4h5.3l-12.7-31.2h-4.7L84.5 60.3h5.2zm11.8-4.9h-9.9l5-12.4zM123.8 29.4h-4.6v31h20.6v-5h-16zM166.5 29.4H145v31h21.6v-5H150v-8.3h14.5v-4.9h-14.5v-8h16.4zM191.2 39.5c0 1.6-.5 2.8-1.6 3.8s-2.6 1.4-4.4 1.4h-7.4V34.3h7.4c1.9 0 3.4.4 4.4 1.3 1 .9 1.6 2.2 1.6 3.9zm6 20.8l-7.7-11.7c1-.3 1.9-.7 2.7-1.3a8.8 8.8 0 003.6-4.6c.4-1 .5-2.2.5-3.5 0-1.5-.2-2.9-.7-4.1a8.4 8.4 0 00-2.1-3.1c-1-.8-2-1.5-3.4-2-1.3-.4-2.8-.6-4.5-.6h-12.9v31h5V49.4h6.5l7 10.8z&quot; />%0A</svg>"
+ alt="GNU Taler" style="height: 1.5rem; margin: 0.5rem;"></a></div><span
+ class="flex items-center text-white text-lg font-bold ml-4">Challenger</span>
+ </div>
+ <div class="block flex-1 ml-6 "></div>
+ <div class="flex justify-end">
+ </div>
+ </div>
+ </header>
+
+ <main class="flex-1">
+ <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 items-center mt-4">
+ <div class="rounded-md bg-red-50 p-4 shadow-xl">
+ <div class="flex">
+ <div class="flex-shrink-0">
+ <svg xmlns="http://www.w3.org/2000/svg" stroke="none" viewBox="0 0 24 24" fill="currentColor"
+ class="w-8 h-8 text-red-400">
+ <path fill-rule="evenodd"
+ d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z" />
+ </svg>
+ </div>
+ <div class="ml-3">
+ <h3 class="text-sm font-medium text-red-800">
+ Invalid PIN
+ </h3>
+ <div class="mt-2 text-sm text-red-700">
+ <p>{{hint}}</p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </main>
+
+ <footer class="bottom-4 mb-4">
+ <div class="mt-8 mx-8 md:order-1 md:mt-0">
+ <div>
+ <p class="text-xs leading-5 text-gray-400">Learn more about <a target="_blank" rel="noreferrer noopener"
+ class="font-semibold text-gray-500 hover:text-gray-400" href="https://taler.net">GNU Taler</a></p>
+ </div>
+ <div style="flex-grow: 1;"></div>
+ <p class="text-xs leading-5 text-gray-400">Copyright © 2014—2023 Taler Systems SA. </p>
+ </div>
+ </footer>
+</body>
+
+</html> \ No newline at end of file
diff --git a/packages/challenger-ui/src/invalid-request.html b/packages/challenger-ui/src/invalid-request.html
new file mode 100644
index 000000000..89e6b125c
--- /dev/null
+++ b/packages/challenger-ui/src/invalid-request.html
@@ -0,0 +1,88 @@
+<!--
+ This file is part of GNU Taler
+ (C) 2021--2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+
+ @author Sebastian Javier Marchano
+-->
+<!DOCTYPE html>
+<html lang="en" class="h-full">
+
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
+ <meta name="taler-support" content="uri">
+ <meta name="mobile-web-app-capable" content="yes" />
+ <meta name="apple-mobile-web-app-capable" content="yes" />
+ <link rel="icon"
+ href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/Qn3//3ryn/795Tf/WrpP/2LCW/8B6T//w4Nb///////Pn4P+/d0v/9u3n/+7d0v/EhV7//v///+HDr//fxLD/zph2/+TJt//8/Pv/woBX//Lm3f/y5dz/v3hN//bu6f/JjGn/4sW0///////Df1j/8OLZ//v6+P+/elH/+vj1//jy7f+/elL//////+zYzP/Eg13//////967p//MlHT/wn5X///////v4Nb/yY1s///////jw7H/06KG////////////z5t9/+fNvf//////x4pn//Pp4v/8+vn/w39X/8WEX///////5s/A/9CbfP//////27Oc/9y2n////////////9itlf/gu6f//////86Vdf/r2Mz//////8SCXP/Df1j//////+7d0v/KkG7//////+HBrf/VpYr////////////RnoH/5sq6///////Ii2n/8ubf//39/P/Cf1j/xohk/+bNvv//////wn5W//Tq4//58/D/wHxV//7+/f/59fH/v3xU//39/P/w4Nf/xIFb///////hw7H/yo9t/+/f1f/AeU3/+/n2/+nSxP/FhmD//////9qzm//Upon/4MSx/96+qf//////xINc/+3bz//48e3/v3hN//Pn3///////6M+//752S//gw6//06aK/8J+VP/kzLr/zZd1/8OCWv/q18r/17KZ/9Ooi//fv6r/v3dK/+vWyP///////v39///////27un/1aeK/9Opjv/m1cf/1KCC/9a0nP/n08T/0Jx8/82YdP/QnHz/16yR//jx7P///////v39///////+/f3///7+///////+//7//v7+///////+/v7//v/+/////////////////////////v7//v79///////////////////+/v/+/Pv//v39///+/v/+/Pv///7+//7+/f/+/Pv//v39//79/P/+/Pv///7+////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" />
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
+ <link rel="stylesheet" href="main.css" />
+ <script type="module" src="main.js"></script>
+ <title>Invalid request (#{{ec}})</title>
+</head>
+
+<body class="min-h-full flex flex-col">
+ <header class="bg-indigo-600 w-full mx-auto px-2 border-b border-opacity-25 border-indigo-400">
+ <div class="flex flex-row h-16 items-center ">
+ <div class="flex px-2 justify-start">
+ <div class="flex-shrink-0 bg-white rounded-lg"><a href="#"><img class="h-8 w-auto"
+ src="data:image/svg+xml,<?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?>%0A<svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 201 90&quot;>%0A <g fill=&quot;%230042b3&quot; fill-rule=&quot;evenodd&quot; stroke-width=&quot;.3&quot;>%0A <path d=&quot;M86.7 1.1c15.6 0 29 9.4 36 23.2h-5.9A35.1 35.1 0 0086.7 6.5C67 6.5 51 23.6 51 44.7c0 10.4 3.8 19.7 10 26.6a31.4 31.4 0 01-4.2 3A45.2 45.2 0 0146 44.7c0-24 18.2-43.6 40.7-43.6zm35.8 64.3a40.4 40.4 0 01-39 22.8c3-1.5 6-3.5 8.6-5.7a35.6 35.6 0 0024.6-17.1z&quot; />%0A <path d=&quot;M64.2 1.1l3.1.1c-3 1.6-5.9 3.5-8.5 5.8a37.5 37.5 0 00-30.2 37.7c0 14.3 7.3 26.7 18 33.3a29.6 29.6 0 01-8.5.2c-9-8-14.6-20-14.6-33.5 0-24 18.2-43.6 40.7-43.6zm5.4 81.4a35.6 35.6 0 0024.6-17.1h5.9a40.4 40.4 0 01-39 22.8c3-1.5 5.9-3.5 8.5-5.7zm24.8-58.2a37 37 0 00-12.6-12.8 29.6 29.6 0 018.5-.2c4 3.6 7.4 8 9.9 13z&quot; />%0A <path d=&quot;M41.8 1.1c1 0 2 0 3.1.2-3 1.5-5.9 3.4-8.5 5.6A37.5 37.5 0 006.1 44.7c0 21.1 16 38.3 35.7 38.3 12.6 0 23.6-7 30-17.6h5.8a40.4 40.4 0 01-35.8 23C19.3 88.4 1 68.8 1 44.7c0-24 18.2-43.6 40.7-43.6zm30.1 23.2a38.1 38.1 0 00-4.5-6.1c1.3-1.2 2.7-2.2 4.3-3 2.3 2.7 4.4 5.8 6 9.1z&quot; />%0A </g>%0A <path d=&quot;M76.1 34.4h9.2v-5H61.9v5H71v26h5.1zM92.6 52.9h13.7l3 7.4h5.3l-12.7-31.2h-4.7L84.5 60.3h5.2zm11.8-4.9h-9.9l5-12.4zM123.8 29.4h-4.6v31h20.6v-5h-16zM166.5 29.4H145v31h21.6v-5H150v-8.3h14.5v-4.9h-14.5v-8h16.4zM191.2 39.5c0 1.6-.5 2.8-1.6 3.8s-2.6 1.4-4.4 1.4h-7.4V34.3h7.4c1.9 0 3.4.4 4.4 1.3 1 .9 1.6 2.2 1.6 3.9zm6 20.8l-7.7-11.7c1-.3 1.9-.7 2.7-1.3a8.8 8.8 0 003.6-4.6c.4-1 .5-2.2.5-3.5 0-1.5-.2-2.9-.7-4.1a8.4 8.4 0 00-2.1-3.1c-1-.8-2-1.5-3.4-2-1.3-.4-2.8-.6-4.5-.6h-12.9v31h5V49.4h6.5l7 10.8z&quot; />%0A</svg>"
+ alt="GNU Taler" style="height: 1.5rem; margin: 0.5rem;"></a></div><span
+ class="flex items-center text-white text-lg font-bold ml-4">Challenger</span>
+ </div>
+ <div class="block flex-1 ml-6 "></div>
+ <div class="flex justify-end">
+ </div>
+ </div>
+ </header>
+
+ <main class="flex-1">
+
+ <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 items-center mt-4">
+ <div class="rounded-md bg-red-50 p-4 shadow-xl">
+ <div class="flex">
+ <div class="flex-shrink-0">
+ <svg xmlns="http://www.w3.org/2000/svg" stroke="none" viewBox="0 0 24 24" fill="currentColor"
+ class="w-8 h-8 text-red-400">
+ <path fill-rule="evenodd"
+ d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z" />
+ </svg>
+ </div>
+ <div class="ml-3">
+ <h3 class="text-sm font-medium text-red-800">
+ Request error
+ </h3>
+ <div class="mt-2 text-sm text-red-700">
+ <p>{{hint}}</p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </main>
+
+ <footer class="bottom-4 mb-4">
+ <div class="mt-8 mx-8 md:order-1 md:mt-0">
+ <div>
+ <p class="text-xs leading-5 text-gray-400">Learn more about <a target="_blank" rel="noreferrer noopener"
+ class="font-semibold text-gray-500 hover:text-gray-400" href="https://taler.net">GNU Taler</a></p>
+ </div>
+ <div style="flex-grow: 1;"></div>
+ <p class="text-xs leading-5 text-gray-400">Copyright © 2014—2023 Taler Systems SA. </p>
+ </div>
+ </footer>
+</body>
+
+</html> \ No newline at end of file
diff --git a/packages/challenger-ui/src/main.js b/packages/challenger-ui/src/main.js
new file mode 100644
index 000000000..3272bd0bb
--- /dev/null
+++ b/packages/challenger-ui/src/main.js
@@ -0,0 +1,2 @@
+// intentionally empty
+import "./scss/main.css"; \ No newline at end of file
diff --git a/packages/challenger-ui/src/pages/AnswerChallenge.tsx b/packages/challenger-ui/src/pages/AnswerChallenge.tsx
new file mode 100644
index 000000000..73a79c51f
--- /dev/null
+++ b/packages/challenger-ui/src/pages/AnswerChallenge.tsx
@@ -0,0 +1,279 @@
+/*
+ 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 {
+ ChallengerApi,
+ HttpStatusCode,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
+import {
+ Attention,
+ Button,
+ LocalNotificationBanner,
+ RouteDefinition,
+ ShowInputErrorLabel,
+ useChallengerApiContext,
+ useLocalNotificationHandler,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { useSessionState } from "../hooks/session.js";
+
+export const EMAIL_REGEX = /^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/;
+
+type Props = {
+ nonce: string;
+ focus?: boolean;
+ onComplete: () => void;
+ routeAsk: RouteDefinition<{ nonce: string }>;
+};
+
+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,
+ });
+
+ const lastEmail = !state
+ ? undefined
+ : !state.lastStatus
+ ? undefined
+ : !state.lastStatus.last_address
+ ? undefined
+ : state.lastStatus.last_address["email"];
+
+ const onSendAgain =
+ !state || lastEmail === undefined
+ ? undefined
+ : withErrorHandler(
+ async () => {
+ if (!lastEmail) return;
+ return await lib.challenger.challenge(nonce, { email: lastEmail });
+ },
+ (ok) => {
+ if ("redirectURL" in ok.body) {
+ completed(ok.body.redirectURL);
+ } else {
+ accepted({
+ attemptsLeft: ok.body.attempts_left,
+ nextSend: ok.body.next_tx_time,
+ transmitted: ok.body.transmitted,
+ });
+ }
+ return undefined;
+ },
+ (fail) => {
+ switch (fail.case) {
+ case HttpStatusCode.BadRequest:
+ return i18n.str``;
+ case HttpStatusCode.NotFound:
+ return i18n.str``;
+ case HttpStatusCode.NotAcceptable:
+ return i18n.str``;
+ case HttpStatusCode.TooManyRequests:
+ return i18n.str``;
+ case HttpStatusCode.InternalServerError:
+ return i18n.str``;
+ }
+ },
+ );
+
+ const onCheck =
+ errors !== undefined || (lastTryError && lastTryError.exhausted)
+ ? undefined
+ : withErrorHandler(
+ async () => {
+ return lib.challenger.solve(nonce, { pin: pin! });
+ },
+ (ok) => {
+ completed(ok.body.redirectURL as URL);
+ onComplete();
+ },
+ (fail) => {
+ switch (fail.case) {
+ case HttpStatusCode.BadRequest:
+ return i18n.str`Invalid request`;
+ case HttpStatusCode.Forbidden: {
+ setLastTryError(fail.body);
+ return i18n.str`Invalid pin`;
+ }
+ case HttpStatusCode.NotFound:
+ return i18n.str``;
+ case HttpStatusCode.NotAcceptable:
+ return i18n.str``;
+ case HttpStatusCode.TooManyRequests:
+ return i18n.str``;
+ case HttpStatusCode.InternalServerError:
+ return i18n.str``;
+ default:
+ assertUnreachable(fail);
+ }
+ },
+ );
+
+ if (!state) {
+ return <div>no state</div>;
+ }
+
+ if (!state.lastTry) {
+ return <div>you should do a challenge first</div>;
+ }
+
+ return (
+ <Fragment>
+ <LocalNotificationBanner notification={notification} />
+
+ <div class="isolate bg-white px-6 py-12">
+ <div class="mx-auto max-w-2xl text-center">
+ <h2 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
+ <i18n.Translate>
+ Enter the TAN you received to authenticate.
+ </i18n.Translate>
+ </h2>
+ <p class="mt-2 text-lg leading-8 text-gray-600">
+ {state.lastTry.transmitted ? (
+ <i18n.Translate>
+ A TAN was sent to your address &quot;{lastEmail}&quot;.
+ </i18n.Translate>
+ ) : (
+ <Attention title={i18n.str`Resend failed`} type="warning">
+ <i18n.Translate>
+ We recently already sent a TAN to your address &quot;
+ {lastEmail}&quot;. A new TAN will not be transmitted again
+ before &quot;{state.lastTry.nextSend}&quot;.
+ </i18n.Translate>
+ </Attention>
+ )}
+ </p>
+ {!lastTryError ? undefined : (
+ <p class="mt-2 text-lg leading-8 text-gray-600">
+ <i18n.Translate>
+ You can try another PIN but just{" "}
+ {lastTryError.auth_attempts_left} times more.
+ </i18n.Translate>
+ </p>
+ )}
+ </div>
+ <form
+ method="POST"
+ class="mx-auto mt-16 max-w-xl sm:mt-20"
+ onSubmit={(e) => {
+ e.preventDefault();
+ }}
+ >
+ <div class="grid grid-cols-1 gap-x-8 gap-y-6">
+ <div class="sm:col-span-2">
+ <label
+ for="pin"
+ class="block text-sm font-semibold leading-6 text-gray-900"
+ >
+ <i18n.Translate>TAN code</i18n.Translate>
+ </label>
+ <div class="mt-2.5">
+ <input
+ autoFocus
+ ref={focus ? doAutoFocus : undefined}
+ type="number"
+ name="pin"
+ id="pin"
+ maxLength={64}
+ value={pin}
+ onChange={(e) => {
+ setPin(e.currentTarget.value);
+ }}
+ placeholder="12345678"
+ class="block w-full rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ />
+ <ShowInputErrorLabel
+ message={errors?.pin}
+ isDirty={pin !== undefined}
+ />
+ </div>
+ </div>
+
+ <p class="mt-3 text-sm leading-6 text-gray-400">
+ <i18n.Translate>
+ You have {state.lastTry.attemptsLeft} attempts left.
+ </i18n.Translate>
+ </p>
+ </div>
+
+ <div class="mt-10">
+ <Button
+ type="submit"
+ class="block w-full disabled:bg-gray-300 rounded-md bg-indigo-600 px-3.5 py-2.5 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ disabled={!onCheck}
+ handler={onCheck}
+ >
+ <i18n.Translate>Check</i18n.Translate>
+ </Button>
+ </div>
+ <div class="mt-10 flex justify-between">
+ <div>
+ <a
+ href={routeAsk.url({ nonce })}
+ class="relative disabled:bg-gray-100 disabled:text-gray-500 inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:outline-offset-0"
+ >
+ <i18n.Translate>Change email</i18n.Translate>
+ </a>
+ </div>
+ <div>
+ <Button
+ type="submit"
+ disabled={!onSendAgain}
+ class="block w-full disabled:bg-gray-300 rounded-md bg-indigo-600 px-3.5 py-2.5 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ handler={onSendAgain}
+ >
+ <i18n.Translate>Send code again</i18n.Translate>
+ </Button>
+ </div>
+ </div>
+ </form>
+ </div>
+ </Fragment>
+ );
+}
+
+/**
+ * Show the element when the load ended
+ * @param element
+ */
+export function doAutoFocus(element: HTMLElement | null): void {
+ if (element) {
+ setTimeout(() => {
+ element.focus({ preventScroll: true });
+ element.scrollIntoView({
+ behavior: "smooth",
+ block: "center",
+ inline: "center",
+ });
+ }, 100);
+ }
+}
+
+export function undefinedIfEmpty<T extends object>(obj: T): T | undefined {
+ return Object.keys(obj).some(
+ (k) => (obj as Record<string, T>)[k] !== undefined,
+ )
+ ? obj
+ : undefined;
+}
diff --git a/packages/challenger-ui/src/pages/AskChallenge.tsx b/packages/challenger-ui/src/pages/AskChallenge.tsx
new file mode 100644
index 000000000..30b50d707
--- /dev/null
+++ b/packages/challenger-ui/src/pages/AskChallenge.tsx
@@ -0,0 +1,263 @@
+/*
+ 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 { HttpStatusCode } from "@gnu-taler/taler-util";
+import {
+ Attention,
+ Button,
+ LocalNotificationBanner,
+ RouteDefinition,
+ ShowInputErrorLabel,
+ useChallengerApiContext,
+ useLocalNotificationHandler,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { useSessionState } from "../hooks/session.js";
+import { doAutoFocus } from "./AnswerChallenge.js";
+
+type Form = {
+ email: string;
+};
+export const EMAIL_REGEX = /^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/;
+
+type Props = {
+ nonce: string;
+ onSendSuccesful: () => void;
+ routeSolveChallenge: RouteDefinition<{ nonce: string }>;
+ focus?: boolean;
+};
+
+export function AskChallenge({
+ nonce,
+ onSendSuccesful,
+ routeSolveChallenge,
+ focus,
+}: Props): VNode {
+ const { state, accepted, completed } = useSessionState();
+ const status = state?.lastStatus;
+ const prevEmail =
+ !status || !status.last_address ? undefined : status.last_address["email"];
+ const regexEmail =
+ !status || !status.restrictions ? undefined : status.restrictions["email"];
+
+ const { lib } = useChallengerApiContext();
+ const { i18n } = useTranslationContext();
+ const [notification, withErrorHandler] = useLocalNotificationHandler();
+ const [email, setEmail] = useState<string | undefined>();
+ const [repeat, setRepeat] = useState<string | undefined>();
+
+ const regexTest =
+ regexEmail && regexEmail.regex ? new RegExp(regexEmail.regex) : EMAIL_REGEX;
+ const regexHint =
+ regexEmail && regexEmail.hint ? regexEmail.hint : i18n.str`invalid email`;
+
+ const errors = undefinedIfEmpty({
+ email: !email
+ ? i18n.str`required`
+ : !regexTest.test(email)
+ ? regexHint
+ : prevEmail !== undefined && email === prevEmail
+ ? i18n.str`email should be different`
+ : undefined,
+ repeat: !repeat
+ ? i18n.str`required`
+ : email !== repeat
+ ? i18n.str`emails doesn't match`
+ : undefined,
+ });
+
+ const onSend = errors
+ ? undefined
+ : withErrorHandler(
+ async () => {
+ return lib.challenger.challenge(nonce, { email: email! });
+ },
+ (ok) => {
+ if ("redirectURL" in ok.body) {
+ completed(ok.body.redirectURL);
+ } else {
+ accepted({
+ attemptsLeft: ok.body.attempts_left,
+ nextSend: ok.body.next_tx_time,
+ transmitted: ok.body.transmitted,
+ });
+ }
+ onSendSuccesful();
+ },
+ (fail) => {
+ switch (fail.case) {
+ case HttpStatusCode.BadRequest:
+ return i18n.str``;
+ case HttpStatusCode.NotFound:
+ return i18n.str``;
+ case HttpStatusCode.NotAcceptable:
+ return i18n.str``;
+ case HttpStatusCode.TooManyRequests:
+ return i18n.str``;
+ case HttpStatusCode.InternalServerError:
+ return i18n.str``;
+ }
+ },
+ );
+
+ if (!status) {
+ return <div>no status loaded</div>;
+ }
+
+ return (
+ <Fragment>
+ <LocalNotificationBanner notification={notification} />
+
+ <div class="isolate bg-white px-6 py-12">
+ <div class="mx-auto max-w-2xl text-center">
+ <h2 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
+ <i18n.Translate>Enter contact details</i18n.Translate>
+ </h2>
+ <p class="mt-2 text-lg leading-8 text-gray-600">
+ <i18n.Translate>
+ You will receive an email with a TAN code that must be provided on
+ the next page.
+ </i18n.Translate>
+ </p>
+ </div>
+ {state.lastTry && (
+ <Fragment>
+ <Attention title={i18n.str`A code has been sent to ${prevEmail}`}>
+ <i18n.Translate>
+ <a href={routeSolveChallenge.url({ nonce })} class="underline">
+ <i18n.Translate>Complete the challenge here.</i18n.Translate>
+ </a>
+ </i18n.Translate>
+ </Attention>
+ </Fragment>
+ )}
+ <form
+ method="POST"
+ class="mx-auto mt-16 max-w-xl sm:mt-20"
+ onSubmit={(e) => {
+ e.preventDefault();
+ }}
+ >
+ <div class="grid grid-cols-1 gap-x-8 gap-y-6">
+ <div class="sm:col-span-2">
+ <label
+ for="email"
+ class="block text-sm font-semibold leading-6 text-gray-900"
+ >
+ <i18n.Translate>Email</i18n.Translate>
+ </label>
+ <div class="mt-2.5">
+ <input
+ type="email"
+ name="email"
+ id="email"
+ ref={focus ? doAutoFocus : undefined}
+ maxLength={512}
+ autocomplete="email"
+ value={email}
+ onChange={(e) => {
+ setEmail(e.currentTarget.value);
+ }}
+ placeholder={prevEmail}
+ readOnly={status.fix_address}
+ class="block w-full read-only:bg-slate-200 rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ />
+ <ShowInputErrorLabel
+ message={errors?.email}
+ isDirty={email !== undefined}
+ />
+ </div>
+ </div>
+
+ {status.fix_address ? undefined : (
+ <div class="sm:col-span-2">
+ <label
+ for="repeat-email"
+ class="block text-sm font-semibold leading-6 text-gray-900"
+ >
+ <i18n.Translate>Repeat email</i18n.Translate>
+ </label>
+ <div class="mt-2.5">
+ <input
+ type="email"
+ name="repeat-email"
+ id="repeat-email"
+ value={repeat}
+ onChange={(e) => {
+ setRepeat(e.currentTarget.value);
+ }}
+ autocomplete="email"
+ class="block w-full rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ />
+ <ShowInputErrorLabel
+ message={errors?.repeat}
+ isDirty={repeat !== undefined}
+ />
+ </div>
+ </div>
+ )}
+
+ {!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>
+ </p>
+ )}
+ </div>
+
+ {!prevEmail ? (
+ <div class="mt-10">
+ <Button
+ type="submit"
+ disabled={!onSend}
+ class="block w-full disabled:bg-gray-300 rounded-md bg-indigo-600 px-3.5 py-2.5 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ handler={onSend}
+ >
+ <i18n.Translate>Send email</i18n.Translate>
+ </Button>
+ </div>
+ ) : (
+ <div class="mt-10">
+ <Button
+ type="submit"
+ disabled={!onSend}
+ class="block w-full disabled:bg-gray-300 rounded-md bg-indigo-600 px-3.5 py-2.5 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ handler={onSend}
+ >
+ <i18n.Translate>Change email</i18n.Translate>
+ </Button>
+ </div>
+ )}
+ </form>
+ </div>
+ </Fragment>
+ );
+}
+
+export function undefinedIfEmpty<T extends object>(obj: T): T | undefined {
+ return Object.keys(obj).some(
+ (k) => (obj as Record<string, T>)[k] !== undefined,
+ )
+ ? obj
+ : undefined;
+}
diff --git a/packages/challenger-ui/src/pages/CallengeCompleted.tsx b/packages/challenger-ui/src/pages/CallengeCompleted.tsx
new file mode 100644
index 000000000..f8cd7ce60
--- /dev/null
+++ b/packages/challenger-ui/src/pages/CallengeCompleted.tsx
@@ -0,0 +1,26 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { VNode, h } from "preact";
+
+type Props = {
+ nonce: string;
+}
+export function CallengeCompleted({nonce}:Props):VNode {
+
+ return <div>
+ completed {nonce}
+ </div>
+} \ No newline at end of file
diff --git a/packages/challenger-ui/src/pages/Frame.tsx b/packages/challenger-ui/src/pages/Frame.tsx
new file mode 100644
index 000000000..612eced0b
--- /dev/null
+++ b/packages/challenger-ui/src/pages/Frame.tsx
@@ -0,0 +1,69 @@
+/*
+ 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 { ComponentChildren, Fragment, h, VNode } from "preact";
+
+export function Frame({ children }: { children: ComponentChildren }): VNode {
+ 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>
+ <div class="block flex-1 ml-6 "></div>
+ <div class="flex justify-end"></div>
+ </div>
+ </header>
+
+ <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>
+ );
+}
diff --git a/packages/challenger-ui/src/pages/MissingParams.tsx b/packages/challenger-ui/src/pages/MissingParams.tsx
new file mode 100644
index 000000000..5eb1e434e
--- /dev/null
+++ b/packages/challenger-ui/src/pages/MissingParams.tsx
@@ -0,0 +1,22 @@
+/*
+ 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 { VNode, h } from "preact";
+
+export function MissingParams() :VNode {
+ return <div>
+ missing params: {window.location.href}
+ </div>
+} \ No newline at end of file
diff --git a/packages/challenger-ui/src/pages/NonceNotFound.tsx b/packages/challenger-ui/src/pages/NonceNotFound.tsx
new file mode 100644
index 000000000..16b3d90ef
--- /dev/null
+++ b/packages/challenger-ui/src/pages/NonceNotFound.tsx
@@ -0,0 +1,42 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ useTranslationContext
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+
+type Form = {
+ email: string;
+};
+
+export function NonceNotFound(): VNode {
+ const { i18n } = useTranslationContext();
+
+ return (
+ <Fragment>
+ <div class="isolate bg-white px-6 py-12">
+ <div class="mx-auto max-w-2xl text-center">
+ <h2 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
+ <i18n.Translate>The URL is wrong</i18n.Translate>
+ </h2>
+ <p class="mt-2 text-lg leading-8 text-gray-600">
+ <i18n.Translate>Maybe the validation check expired.</i18n.Translate>
+ </p>
+ </div>
+ </div>
+ </Fragment>
+ );
+}
diff --git a/packages/challenger-ui/src/pages/Setup.tsx b/packages/challenger-ui/src/pages/Setup.tsx
new file mode 100644
index 000000000..f431835aa
--- /dev/null
+++ b/packages/challenger-ui/src/pages/Setup.tsx
@@ -0,0 +1,82 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { AccessToken, HttpStatusCode, encodeCrock, randomBytes } from "@gnu-taler/taler-util";
+import {
+ Button,
+ LocalNotificationBanner,
+ useChallengerApiContext,
+ useLocalNotificationHandler,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useSessionState } from "../hooks/session.js";
+
+type Props = {
+ clientId: string;
+ onCreated: (nonce:string) => void;
+};
+export function Setup({ clientId, onCreated }: Props): VNode {
+ const { i18n } = useTranslationContext();
+ const [notification, withErrorHandler] = useLocalNotificationHandler();
+ const { lib } = useChallengerApiContext();
+ const { start } = useSessionState();
+
+ const onStart = withErrorHandler(
+ async () => {
+ return lib.challenger.setup(clientId, "secret-token:chal-secret" as AccessToken);
+ },
+ (ok) => {
+ start({
+ clientId,
+ redirectURL: "http://exchange.taler.test:1180/kyc-proof/kyc-provider-wallet",
+ state: encodeCrock(randomBytes(32)),
+ });
+
+ onCreated(ok.body.nonce);
+ },
+ (fail) => {
+ switch (fail.case) {
+ case HttpStatusCode.NotFound:
+ return i18n.str`Client doesn't exist.`;
+ }
+ },
+ );
+
+ return (
+ <Fragment>
+ <LocalNotificationBanner notification={notification} />
+
+ <div class="isolate bg-white px-6 py-12">
+ <div class="mx-auto max-w-2xl text-center">
+ <h2 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
+ <i18n.Translate>
+ Setup new challenge with client ID: &quot;{clientId}&quot;
+ </i18n.Translate>
+ </h2>
+ </div>
+ <div class="mt-10">
+ <Button
+ type="submit"
+ class="block w-full disabled:bg-gray-300 rounded-md bg-indigo-600 px-3.5 py-2.5 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ handler={onStart}
+ >
+ <i18n.Translate>Start</i18n.Translate>
+ </Button>
+ </div>
+ </div>
+ </Fragment>
+ );
+}
diff --git a/packages/challenger-ui/src/scss/main.css b/packages/challenger-ui/src/scss/main.css
new file mode 100644
index 000000000..b5c61c956
--- /dev/null
+++ b/packages/challenger-ui/src/scss/main.css
@@ -0,0 +1,3 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
diff --git a/packages/challenger-ui/src/settings.json b/packages/challenger-ui/src/settings.json
new file mode 100644
index 000000000..b3d0476aa
--- /dev/null
+++ b/packages/challenger-ui/src/settings.json
@@ -0,0 +1,3 @@
+{
+ "backendBaseURL": "http://challenger.taler.test:1180/"
+}
diff --git a/packages/challenger-ui/src/settings.ts b/packages/challenger-ui/src/settings.ts
new file mode 100644
index 000000000..61d2117fa
--- /dev/null
+++ b/packages/challenger-ui/src/settings.ts
@@ -0,0 +1,83 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ Codec,
+ buildCodecForObject,
+ canonicalizeBaseUrl,
+ codecForString,
+ codecOptional
+} from "@gnu-taler/taler-util";
+
+export interface ChallengerUiSettings {
+ // Where challenger backend is localted
+ // default: window.origin without "webui/"
+ backendBaseURL?: string;
+
+}
+
+/**
+ * Global settings for the bank UI.
+ */
+const defaultSettings: ChallengerUiSettings = {
+ backendBaseURL: buildDefaultBackendBaseURL(),
+};
+
+const codecForChallengerUISettings = (): Codec<ChallengerUiSettings> =>
+ buildCodecForObject<ChallengerUiSettings>()
+ .property("backendBaseURL", codecOptional(codecForString()))
+ .build("codecForChallengerUISettings");
+
+function removeUndefineField<T extends object>(obj: T): T {
+ const keys = Object.keys(obj) as Array<keyof T>;
+ return keys.reduce((prev, cur) => {
+ if (typeof prev[cur] === "undefined") {
+ delete prev[cur];
+ }
+ return prev;
+ }, obj);
+}
+
+export function fetchSettings(listener: (s: ChallengerUiSettings) => void): void {
+ fetch("./settings.json")
+ .then((resp) => resp.json())
+ .then((json) => codecForChallengerUISettings().decode(json))
+ .then((result) =>
+ listener({
+ ...defaultSettings,
+ ...removeUndefineField(result),
+ }),
+ )
+ .catch((e) => {
+ console.log("failed to fetch settings", e);
+ listener(defaultSettings);
+ });
+}
+
+function buildDefaultBackendBaseURL(): string | undefined {
+ if (typeof window !== "undefined") {
+ const currentLocation = new URL(
+ window.location.pathname,
+ window.location.origin,
+ ).href;
+ /**
+ * By default, bank backend serves the html content
+ * from the /webui root.
+ */
+ return canonicalizeBaseUrl(currentLocation.replace("/webui", ""));
+ }
+ throw Error("No default URL");
+}
diff --git a/packages/challenger-ui/src/validation-unknown.html b/packages/challenger-ui/src/validation-unknown.html
new file mode 100644
index 000000000..56c8d156c
--- /dev/null
+++ b/packages/challenger-ui/src/validation-unknown.html
@@ -0,0 +1,89 @@
+<!--
+ This file is part of GNU Taler
+ (C) 2021--2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+
+ @author Sebastian Javier Marchano
+-->
+<!DOCTYPE html>
+<html lang="en" class="h-full">
+
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
+ <meta name="taler-support" content="uri">
+ <meta name="mobile-web-app-capable" content="yes" />
+ <meta name="apple-mobile-web-app-capable" content="yes" />
+ <link rel="icon"
+ href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/Qn3//3ryn/795Tf/WrpP/2LCW/8B6T//w4Nb///////Pn4P+/d0v/9u3n/+7d0v/EhV7//v///+HDr//fxLD/zph2/+TJt//8/Pv/woBX//Lm3f/y5dz/v3hN//bu6f/JjGn/4sW0///////Df1j/8OLZ//v6+P+/elH/+vj1//jy7f+/elL//////+zYzP/Eg13//////967p//MlHT/wn5X///////v4Nb/yY1s///////jw7H/06KG////////////z5t9/+fNvf//////x4pn//Pp4v/8+vn/w39X/8WEX///////5s/A/9CbfP//////27Oc/9y2n////////////9itlf/gu6f//////86Vdf/r2Mz//////8SCXP/Df1j//////+7d0v/KkG7//////+HBrf/VpYr////////////RnoH/5sq6///////Ii2n/8ubf//39/P/Cf1j/xohk/+bNvv//////wn5W//Tq4//58/D/wHxV//7+/f/59fH/v3xU//39/P/w4Nf/xIFb///////hw7H/yo9t/+/f1f/AeU3/+/n2/+nSxP/FhmD//////9qzm//Upon/4MSx/96+qf//////xINc/+3bz//48e3/v3hN//Pn3///////6M+//752S//gw6//06aK/8J+VP/kzLr/zZd1/8OCWv/q18r/17KZ/9Ooi//fv6r/v3dK/+vWyP///////v39///////27un/1aeK/9Opjv/m1cf/1KCC/9a0nP/n08T/0Jx8/82YdP/QnHz/16yR//jx7P///////v39///////+/f3///7+///////+//7//v7+///////+/v7//v/+/////////////////////////v7//v79///////////////////+/v/+/Pv//v39///+/v/+/Pv///7+//7+/f/+/Pv//v39//79/P/+/Pv///7+////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" />
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
+ <link rel="stylesheet" href="main.css" />
+ <script type="module" src="main.js"></script>
+ <title>Validation process unknown (#{{ec}})</title>
+</head>
+
+<body class="min-h-full flex flex-col">
+ <header class="bg-indigo-600 w-full mx-auto px-2 border-b border-opacity-25 border-indigo-400">
+ <div class="flex flex-row h-16 items-center ">
+ <div class="flex px-2 justify-start">
+ <div class="flex-shrink-0 bg-white rounded-lg"><a href="#"><img class="h-8 w-auto"
+ src="data:image/svg+xml,<?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?>%0A<svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 201 90&quot;>%0A <g fill=&quot;%230042b3&quot; fill-rule=&quot;evenodd&quot; stroke-width=&quot;.3&quot;>%0A <path d=&quot;M86.7 1.1c15.6 0 29 9.4 36 23.2h-5.9A35.1 35.1 0 0086.7 6.5C67 6.5 51 23.6 51 44.7c0 10.4 3.8 19.7 10 26.6a31.4 31.4 0 01-4.2 3A45.2 45.2 0 0146 44.7c0-24 18.2-43.6 40.7-43.6zm35.8 64.3a40.4 40.4 0 01-39 22.8c3-1.5 6-3.5 8.6-5.7a35.6 35.6 0 0024.6-17.1z&quot; />%0A <path d=&quot;M64.2 1.1l3.1.1c-3 1.6-5.9 3.5-8.5 5.8a37.5 37.5 0 00-30.2 37.7c0 14.3 7.3 26.7 18 33.3a29.6 29.6 0 01-8.5.2c-9-8-14.6-20-14.6-33.5 0-24 18.2-43.6 40.7-43.6zm5.4 81.4a35.6 35.6 0 0024.6-17.1h5.9a40.4 40.4 0 01-39 22.8c3-1.5 5.9-3.5 8.5-5.7zm24.8-58.2a37 37 0 00-12.6-12.8 29.6 29.6 0 018.5-.2c4 3.6 7.4 8 9.9 13z&quot; />%0A <path d=&quot;M41.8 1.1c1 0 2 0 3.1.2-3 1.5-5.9 3.4-8.5 5.6A37.5 37.5 0 006.1 44.7c0 21.1 16 38.3 35.7 38.3 12.6 0 23.6-7 30-17.6h5.8a40.4 40.4 0 01-35.8 23C19.3 88.4 1 68.8 1 44.7c0-24 18.2-43.6 40.7-43.6zm30.1 23.2a38.1 38.1 0 00-4.5-6.1c1.3-1.2 2.7-2.2 4.3-3 2.3 2.7 4.4 5.8 6 9.1z&quot; />%0A </g>%0A <path d=&quot;M76.1 34.4h9.2v-5H61.9v5H71v26h5.1zM92.6 52.9h13.7l3 7.4h5.3l-12.7-31.2h-4.7L84.5 60.3h5.2zm11.8-4.9h-9.9l5-12.4zM123.8 29.4h-4.6v31h20.6v-5h-16zM166.5 29.4H145v31h21.6v-5H150v-8.3h14.5v-4.9h-14.5v-8h16.4zM191.2 39.5c0 1.6-.5 2.8-1.6 3.8s-2.6 1.4-4.4 1.4h-7.4V34.3h7.4c1.9 0 3.4.4 4.4 1.3 1 .9 1.6 2.2 1.6 3.9zm6 20.8l-7.7-11.7c1-.3 1.9-.7 2.7-1.3a8.8 8.8 0 003.6-4.6c.4-1 .5-2.2.5-3.5 0-1.5-.2-2.9-.7-4.1a8.4 8.4 0 00-2.1-3.1c-1-.8-2-1.5-3.4-2-1.3-.4-2.8-.6-4.5-.6h-12.9v31h5V49.4h6.5l7 10.8z&quot; />%0A</svg>"
+ alt="GNU Taler" style="height: 1.5rem; margin: 0.5rem;"></a></div><span
+ class="flex items-center text-white text-lg font-bold ml-4">Challenger</span>
+ </div>
+ <div class="block flex-1 ml-6 "></div>
+ <div class="flex justify-end">
+ </div>
+ </div>
+ </header>
+
+ <main class="flex-1">
+
+ <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 items-center mt-4">
+ <div class="rounded-md bg-red-50 p-4 shadow-xl">
+ <div class="flex">
+ <div class="flex-shrink-0">
+ <svg xmlns="http://www.w3.org/2000/svg" stroke="none" viewBox="0 0 24 24" fill="currentColor"
+ class="w-8 h-8 text-red-400">
+ <path fill-rule="evenodd"
+ d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z" />
+ </svg>
+ </div>
+ <div class="ml-3">
+ <h3 class="text-sm font-medium text-red-800">
+ Validation error
+ </h3>
+ <div class="mt-2 text-sm text-red-700">
+ <p>{{hint}}</p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ </main>
+
+ <footer class="bottom-4 mb-4">
+ <div class="mt-8 mx-8 md:order-1 md:mt-0">
+ <div>
+ <p class="text-xs leading-5 text-gray-400">Learn more about <a target="_blank" rel="noreferrer noopener"
+ class="font-semibold text-gray-500 hover:text-gray-400" href="https://taler.net">GNU Taler</a></p>
+ </div>
+ <div style="flex-grow: 1;"></div>
+ <p class="text-xs leading-5 text-gray-400">Copyright © 2014—2023 Taler Systems SA. </p>
+ </div>
+ </footer>
+</body>
+
+</html> \ No newline at end of file
diff --git a/packages/challenger-ui/tailwind.config.js b/packages/challenger-ui/tailwind.config.js
new file mode 100644
index 000000000..d384690e2
--- /dev/null
+++ b/packages/challenger-ui/tailwind.config.js
@@ -0,0 +1,28 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+export default {
+ content: {
+ relative: true,
+ files: [
+ "./src/**/*.{html,tsx}",
+ "./node_modules/@gnu-taler/web-util/src/**/*.{html,tsx}"
+ ],
+ },
+ theme: {
+ extend: {},
+ },
+ plugins: [require("@tailwindcss/typography"), require("@tailwindcss/forms")],
+};
diff --git a/packages/challenger-ui/tsconfig.json b/packages/challenger-ui/tsconfig.json
new file mode 100644
index 000000000..9826fac07
--- /dev/null
+++ b/packages/challenger-ui/tsconfig.json
@@ -0,0 +1,46 @@
+{
+ "compilerOptions": {
+ /* Basic Options */
+ "target": "ES2020",
+ "module": "Node16",
+ "lib": ["DOM", "ES2020"],
+ "allowJs": true /* Allow javascript files to be compiled. */,
+ // "checkJs": true, /* Report errors in .js files. */
+ "jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */,
+ "jsxFactory": "h",
+ "jsxFragmentFactory": "Fragment",
+ "noEmit": true /* Do not emit outputs. */,
+ // "importHelpers": true, /* Import emit helpers from 'tslib'. */
+ // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
+ // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
+ /* Strict Type-Checking Options */
+ "strict": true /* Enable all strict type-checking options. */,
+ "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
+ /* Additional Checks */
+ // "noUnusedLocals": true, /* Report errors on unused locals. */
+ // "noUnusedParameters": true, /* Report errors on unused parameters. */
+ // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
+ // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
+ /* Module Resolution Options */
+ "moduleResolution": "Node16" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
+ "esModuleInterop": true /* */,
+ // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
+ // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
+ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
+ // "typeRoots": [], /* List of folders to include type definitions from. */
+ // "types": [], /* Type declaration files to be included in compilation. */
+ "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */,
+ // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
+ /* Source Map Options */
+ // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
+ // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */
+ // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
+ // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
+ /* Experimental Options */
+ // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
+ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
+ /* Advanced Options */
+ "skipLibCheck": true /* Skip type checking of declaration files. */
+ },
+ "include": ["src/**/*"]
+}
diff --git a/packages/demobank-ui/.gitignore b/packages/demobank-ui/.gitignore
deleted file mode 100644
index 32d0a5057..000000000
--- a/packages/demobank-ui/.gitignore
+++ /dev/null
@@ -1,5 +0,0 @@
-node_modules
-/build
-/*.log
-/size-plugin.json
-/storybook-static/
diff --git a/packages/demobank-ui/.storybook/main.js b/packages/demobank-ui/.storybook/main.js
deleted file mode 100644
index f8e4bbcc7..000000000
--- a/packages/demobank-ui/.storybook/main.js
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-
-module.exports = {
- "stories": [
- "../src/**/*.stories.mdx",
- "../src/**/*.stories.@(js|jsx|ts|tsx)"
- ],
- "addons": [
- "@storybook/preset-scss",
- "@storybook/addon-a11y",
- "@storybook/addon-essentials" //docs, control, actions, viewpot, toolbar, background
- ],
- // sb does not yet support new jsx transform by default
- // https://github.com/storybookjs/storybook/issues/12881
- // https://github.com/storybookjs/storybook/issues/12952
- babel: async (options) => ({
- ...options,
- presets: [
- ...options.presets,
- [
- '@babel/preset-react', {
- runtime: 'automatic',
- },
- 'preset-react-jsx-transform'
- ],
- ],
- }),
- webpackFinal: (config) => {
- // should be removed after storybook 6.3
- // https://github.com/storybookjs/storybook/issues/12853#issuecomment-821576113
- config.resolve.alias = {
- react: "preact/compat",
- "react-dom": "preact/compat",
- };
- return config;
- },
-} \ No newline at end of file
diff --git a/packages/demobank-ui/.storybook/preview.js b/packages/demobank-ui/.storybook/preview.js
deleted file mode 100644
index 9ab4d9404..000000000
--- a/packages/demobank-ui/.storybook/preview.js
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- 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/>
- */
-
-import "../src/scss/main.scss"
-import { TranslationProvider } from '../src/context/translation'
-import { h } from 'preact';
-
-
-export const parameters = {
- controls: { expanded: true },
- options: {
- storySort: (a, b) => {
- return (a[1].args.order ?? 0) - (b[1].args.order ?? 0)
- // return a[1].kind === b[1].kind ? 0 : a[1].id.localeCompare(b[1].id, undefined, { numeric: true })
- }
- },
-}
-
-export const globalTypes = {
- locale: {
- name: 'Locale',
- description: 'Internationalization locale',
- defaultValue: 'en',
- toolbar: {
- icon: 'globe',
- items: [
- { value: 'en', right: '🇺🇸', title: 'English' },
- { value: 'es', right: '🇪🇸', title: 'Spanish' },
- ],
- },
- },
-};
-
-export const decorators = [
- (Story, { globals }) => {
- document.body.parentElement.classList = "has-aside-left has-aside-mobile-transition has-navbar-fixed-top has-aside-expanded"
- return <Story />
- },
- (Story, { globals }) => <TranslationProvider initial='en' forceLang={globals.locale}>
- <Story />
- </TranslationProvider>,
-];
diff --git a/packages/demobank-ui/Makefile b/packages/demobank-ui/Makefile
deleted file mode 100644
index 3c3a3f602..000000000
--- a/packages/demobank-ui/Makefile
+++ /dev/null
@@ -1,17 +0,0 @@
-# This Makefile has been placed in the public domain
-
-# Settings from "./configure"
-include .config.mk
-
-all:
- @echo run \'make install\' to install
-
-spa_dir=$(prefix)/share/taler/demobank-ui
-
-install:
- pnpm install --frozen-lockfile --filter @gnu-taler/demobank-ui...
- pnpm run check
- pnpm run build
- install -d $(spa_dir)
- install ./dist/* $(spa_dir)
-
diff --git a/packages/demobank-ui/README.md b/packages/demobank-ui/README.md
deleted file mode 100644
index 7f582c5ba..000000000
--- a/packages/demobank-ui/README.md
+++ /dev/null
@@ -1,49 +0,0 @@
-# Taler Demobank UI
-
-## CLI Commands
-
-- `pnpm install`: Installs dependencies
-
-- `pnpm run build`: Create a production-ready build under `dist/`.
-
-- `pnpm run check`: Run type checker
-
-- `pnpm run lint`: Pass TypeScript files using ESLint
-
-## Testing
-
-By default, the demobank-ui points to `https://bank.demo.taler.net/demobanks/default/`
-as the bank access API base URL.
-
-This can be changed for testing by setting the URL via local storage (via your browser's devtools):
-```
-localStorage.setItem("bank-base-url", OTHER_URL);
-```
-
-## Customizing Per-Deployment Settings
-
-To customize per-deployment settings, make sure that the
-`demobank-ui-settings.js` file is served alongside the UI.
-
-This file is loaded before the SPA and can do customizations by
-changing `globalThis.`.
-
-For example, the following settings would correspond
-to the default settings:
-
-```
-globalThis.talerDemobankSettings = {
- allowRegistrations: true,
- bankName: "Taler Bank",
- // Show explainer text and navbar to other demo sites
- showDemoNav: true,
- // Names and links for other demo sites to show in the navbar
- demoSites: [
- ["Landing", "https://demo.taler.net/"],
- ["Bank", "https://bank.demo.taler.net/"],
- ["Essay Shop", "https://shop.demo.taler.net/"],
- ["Donations", "https://donations.demo.taler.net/"],
- ["Survey", "https://donations.demo.taler.net/"],
- ],
-};
-```
diff --git a/packages/demobank-ui/TODO b/packages/demobank-ui/TODO
deleted file mode 100644
index cc578cce0..000000000
--- a/packages/demobank-ui/TODO
+++ /dev/null
@@ -1,49 +0,0 @@
-Urgent TODOs:
-
-- General:
-
- - not only Nora dark-theme, but default light! (CSS)
- - DONE: auto-focus on input fields is not working well
- - DONE: buttons should be visibly insensitive
- as long as required input fields are not
- working
- - DONE: next required invalid/missing input field is
- not properly highlighted in red
- - Logout button needs more padding to the right (CSS)
-
-- Error bar:
- - DONE: hows JSON, should only show good error message
- and numeric code, not JSON syntax
- - should auto-hide after next action, no need for
- "clear"!
- - need variant "status bar" in green (or blue)
- which shows status of last operation
-
-* H1-Titles:
- - Center more (currently way on the left) (CSS)
-
-- Assets:
-
- - Numeric amount needs to be shown MUCH bigger (CSS)
- - Center more? (CSS)
-
-- Payments:
-
- - Amount to withdraw currently shown in white-on-white (CSS)
- - Big frame drawn around notebook-tabs is not nice (CSS)
- - Center more? (CSS)
- - "Wire to bank account"
- - maybe split two types (payto and IBAN) into
- two tabs?
- - currently cannot switch back from payto to IBAN
-
-- Withdraw:
-
- - Should use new 'status' bar at the end, instead
- of extra dialog with "close" button
- - ditto for bank-wire-transfer final stage
-
-- Footer:
- - overlaps with transaction history or other
- content, needs to consistently show at the
- end! => change rendering logic!? (CSS?)
diff --git a/packages/demobank-ui/build-bank-translations.sh b/packages/demobank-ui/build-bank-translations.sh
deleted file mode 100755
index 85c8ad0c1..000000000
--- a/packages/demobank-ui/build-bank-translations.sh
+++ /dev/null
@@ -1,32 +0,0 @@
-#!/bin/bash
-
-set -eu
-
-# NOTE: the <Translate> node somehow didn't get
-# the strings extracted. Only i18n`` did
-
-function build {
- POTGEN=node_modules/@gnu-taler/pogen/bin/pogen
- PACKAGE_NAME=$1
-
- find src/ \( -type f -name "*.ts" -or -name "*.tsx" \) ! -name "*.d.ts" \
- | xargs node $POTGEN \
- | msguniq \
- | msgmerge src/i18n/poheader - \
- > src/i18n/$PACKAGE_NAME.pot
-
- # merge existing translations: fails when NO .po-files were found.
- for pofile in $(ls src/i18n/*.po 2> /dev/null || true); do
- echo merging $pofile;
- msgmerge -o $pofile $pofile src/i18n/$PACKAGE_NAME.pot;
- done;
-
- # generate .ts file containing all translations
- cat src/i18n/strings-prelude > src/i18n/strings.ts
- for pofile in $(ls src/i18n/*.po 2> /dev/null || true); do \
- echo appending $pofile; \
- ./contrib/po2ts $pofile >> src/i18n/strings.ts; \
- done;
-}
-
-build bank
diff --git a/packages/demobank-ui/package.json b/packages/demobank-ui/package.json
deleted file mode 100644
index 41031977f..000000000
--- a/packages/demobank-ui/package.json
+++ /dev/null
@@ -1,39 +0,0 @@
-{
- "private": true,
- "name": "@gnu-taler/demobank-ui",
- "version": "0.1.0",
- "license": "AGPL-3.0-OR-LATER",
- "scripts": {
- "build": "./build.mjs",
- "check": "tsc",
- "lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
- "pretty": "prettier --write src"
- },
- "dependencies": {
- "@gnu-taler/taler-util": "workspace:*",
- "@gnu-taler/web-util": "workspace:*",
- "date-fns": "2.29.3",
- "history": "4.10.1",
- "jed": "1.1.1",
- "preact": "10.11.3",
- "preact-router": "3.2.1",
- "qrcode-generator": "^1.4.4",
- "swr": "1.3.0"
- },
- "devDependencies": {
- "@creativebulma/bulma-tooltip": "^1.2.0",
- "@gnu-taler/pogen": "^0.0.5",
- "@types/history": "^4.7.8",
- "@typescript-eslint/eslint-plugin": "^5.41.0",
- "@typescript-eslint/parser": "^5.41.0",
- "bulma": "^0.9.4",
- "bulma-checkbox": "^1.1.1",
- "bulma-radio": "^1.1.1",
- "esbuild": "^0.15.12",
- "eslint": "^8.26.0",
- "eslint-config-preact": "^1.2.0",
- "po2json": "^0.4.5",
- "sass": "1.56.1",
- "typescript": "^4.4.4"
- }
-}
diff --git a/packages/demobank-ui/src/.babelrc b/packages/demobank-ui/src/.babelrc
deleted file mode 100644
index 05f4dcc81..000000000
--- a/packages/demobank-ui/src/.babelrc
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "presets": ["preact-cli/babel"]
-}
diff --git a/packages/demobank-ui/src/components/FileButton.tsx b/packages/demobank-ui/src/components/FileButton.tsx
deleted file mode 100644
index 61fe0975d..000000000
--- a/packages/demobank-ui/src/components/FileButton.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import { h, VNode } from "preact";
-import { useRef, useState } from "preact/hooks";
-
-const MAX_IMAGE_UPLOAD_SIZE = 1024 * 1024;
-
-export interface FileTypeContent {
- content: string;
- type: string;
- name: string;
-}
-
-interface Props {
- label: string;
- onChange: (v: FileTypeContent | undefined) => void;
-}
-export function FileButton(props: Props): VNode {
- const fileInputRef = useRef<HTMLInputElement>(null);
- const [sizeError, setSizeError] = useState(false);
- return (
- <div>
- <button class="button" onClick={(e) => fileInputRef.current?.click()}>
- <span>{props.label}</span>
- </button>
- <input
- ref={fileInputRef}
- style={{ display: "none" }}
- type="file"
- onChange={(e) => {
- const f: FileList | null = e.currentTarget.files;
- if (!f || f.length != 1) return props.onChange(undefined);
-
- if (f[0].size > MAX_IMAGE_UPLOAD_SIZE) {
- setSizeError(true);
- return props.onChange(undefined);
- }
- setSizeError(false);
- return f[0].arrayBuffer().then((b) => {
- const content = new Uint8Array(b).reduce(
- (data, byte) => data + String.fromCharCode(byte),
- "",
- );
- return props.onChange({
- content,
- name: f[0].name,
- type: f[0].type,
- });
- });
- }}
- />
- {sizeError && (
- <p class="help is-danger">File should be smaller than 1 MB</p>
- )}
- </div>
- );
-}
diff --git a/packages/demobank-ui/src/components/app.tsx b/packages/demobank-ui/src/components/app.tsx
deleted file mode 100644
index 07ac9b8f3..000000000
--- a/packages/demobank-ui/src/components/app.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import {
- globalLogLevel,
- setGlobalLogLevelFromString,
-} from "@gnu-taler/taler-util";
-import { h, FunctionalComponent } from "preact";
-import { BackendStateProvider } from "../context/backend.js";
-import { PageStateProvider } from "../context/pageState.js";
-import { TranslationProvider } from "../context/translation.js";
-import { Routing } from "../pages/Routing.js";
-
-/**
- * FIXME:
- *
- * - INPUT elements have their 'required' attribute ignored.
- *
- * - the page needs a "home" button that either redirects to
- * the profile page (when the user is logged in), or to
- * the very initial home page.
- *
- * - histories 'pages' are grouped in UL elements that cause
- * the rendering to visually separate each UL. History elements
- * should instead line up without any separation caused by
- * a implementation detail.
- *
- * - Many strings need to be i18n-wrapped.
- */
-
-const App: FunctionalComponent = () => {
- return (
- <TranslationProvider>
- <PageStateProvider>
- <BackendStateProvider>
- <Routing />
- </BackendStateProvider>
- </PageStateProvider>
- </TranslationProvider>
- );
-};
-(window as any).setGlobalLogLevelFromString = setGlobalLogLevelFromString;
-(window as any).getGlobaLevel = () => {
- return globalLogLevel;
-};
-
-export default App;
diff --git a/packages/demobank-ui/src/components/fields/DateInput.tsx b/packages/demobank-ui/src/components/fields/DateInput.tsx
deleted file mode 100644
index 22a83c93c..000000000
--- a/packages/demobank-ui/src/components/fields/DateInput.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-import { format, subYears } from "date-fns";
-import { h, VNode } from "preact";
-import { useLayoutEffect, useRef, useState } from "preact/hooks";
-import { DatePicker } from "../picker/DatePicker";
-
-export interface DateInputProps {
- label: string;
- grabFocus?: boolean;
- tooltip?: string;
- error?: string;
- years?: Array<number>;
- onConfirm?: () => void;
- bind: [string, (x: string) => void];
-}
-
-export function DateInput(props: DateInputProps): VNode {
- const inputRef = useRef<HTMLInputElement>(null);
- useLayoutEffect(() => {
- if (props.grabFocus) inputRef.current?.focus();
- }, [props.grabFocus]);
- const [opened, setOpened] = useState(false);
-
- const value = props.bind[0] || "";
- const [dirty, setDirty] = useState(false);
- const showError = dirty && props.error;
-
- const calendar = subYears(new Date(), 30);
-
- return (
- <div class="field">
- <label class="label">
- {props.label}
- {props.tooltip && (
- <span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
- <i class="mdi mdi-information" />
- </span>
- )}
- </label>
- <div class="control">
- <div class="field has-addons">
- <p class="control">
- <input
- type="text"
- class={showError ? "input is-danger" : "input"}
- value={value}
- onKeyPress={(e) => {
- if (e.key === "Enter" && props.onConfirm) props.onConfirm();
- }}
- onInput={(e) => {
- const text = e.currentTarget.value;
- setDirty(true);
- props.bind[1](text);
- }}
- ref={inputRef}
- />
- </p>
- <p class="control">
- <a
- class="button"
- onClick={() => {
- setOpened(true);
- }}
- >
- <span class="icon">
- <i class="mdi mdi-calendar" />
- </span>
- </a>
- </p>
- </div>
- </div>
- <p class="help">Using the format yyyy-mm-dd</p>
- {showError && <p class="help is-danger">{props.error}</p>}
- <DatePicker
- opened={opened}
- initialDate={calendar}
- years={props.years}
- closeFunction={() => setOpened(false)}
- dateReceiver={(d) => {
- setDirty(true);
- const v = format(d, "yyyy-MM-dd");
- props.bind[1](v);
- }}
- />
- </div>
- );
-}
diff --git a/packages/demobank-ui/src/components/fields/EmailInput.tsx b/packages/demobank-ui/src/components/fields/EmailInput.tsx
deleted file mode 100644
index 2a22b26e8..000000000
--- a/packages/demobank-ui/src/components/fields/EmailInput.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import { h, VNode } from "preact";
-import { useLayoutEffect, useRef, useState } from "preact/hooks";
-
-export interface TextInputProps {
- label: string;
- grabFocus?: boolean;
- error?: string;
- placeholder?: string;
- tooltip?: string;
- onConfirm?: () => void;
- bind: [string, (x: string) => void];
-}
-
-export function EmailInput(props: TextInputProps): VNode {
- const inputRef = useRef<HTMLInputElement>(null);
- useLayoutEffect(() => {
- if (props.grabFocus) inputRef.current?.focus();
- }, [props.grabFocus]);
- const value = props.bind[0];
- const [dirty, setDirty] = useState(false);
- const showError = dirty && props.error;
- return (
- <div class="field">
- <label class="label">
- {props.label}
- {props.tooltip && (
- <span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
- <i class="mdi mdi-information" />
- </span>
- )}
- </label>
- <div class="control has-icons-right">
- <input
- value={value}
- required
- placeholder={props.placeholder}
- type="email"
- class={showError ? "input is-danger" : "input"}
- onKeyPress={(e) => {
- if (e.key === "Enter" && props.onConfirm) props.onConfirm();
- }}
- onInput={(e) => {
- setDirty(true);
- props.bind[1]((e.target as HTMLInputElement).value);
- }}
- ref={inputRef}
- style={{ display: "block" }}
- />
- </div>
- {showError && <p class="help is-danger">{props.error}</p>}
- </div>
- );
-}
diff --git a/packages/demobank-ui/src/components/fields/FileInput.tsx b/packages/demobank-ui/src/components/fields/FileInput.tsx
deleted file mode 100644
index 8c5269039..000000000
--- a/packages/demobank-ui/src/components/fields/FileInput.tsx
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-import { h, VNode } from "preact";
-import { useLayoutEffect, useRef, useState } from "preact/hooks";
-
-const MAX_IMAGE_UPLOAD_SIZE = 1024 * 1024;
-
-export interface FileTypeContent {
- content: string;
- type: string;
- name: string;
-}
-
-export interface FileInputProps {
- label: string;
- grabFocus?: boolean;
- disabled?: boolean;
- error?: string;
- placeholder?: string;
- tooltip?: string;
- onChange: (v: FileTypeContent | undefined) => void;
-}
-
-export function FileInput(props: FileInputProps): VNode {
- const inputRef = useRef<HTMLInputElement>(null);
- useLayoutEffect(() => {
- if (props.grabFocus) inputRef.current?.focus();
- }, [props.grabFocus]);
-
- const fileInputRef = useRef<HTMLInputElement>(null);
- const [sizeError, setSizeError] = useState(false);
- return (
- <div class="field">
- <label class="label">
- <a class="button" onClick={(e) => fileInputRef.current?.click()}>
- <div class="icon is-small ">
- <i class="mdi mdi-folder" />
- </div>
- <span>{props.label}</span>
- </a>
- {props.tooltip && (
- <span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
- <i class="mdi mdi-information" />
- </span>
- )}
- </label>
- <div class="control">
- <input
- ref={fileInputRef}
- style={{ display: "none" }}
- type="file"
- // name={String(name)}
- onChange={(e) => {
- const f: FileList | null = e.currentTarget.files;
- if (!f || f.length != 1) return props.onChange(undefined);
-
- if (f[0].size > MAX_IMAGE_UPLOAD_SIZE) {
- setSizeError(true);
- return props.onChange(undefined);
- }
- setSizeError(false);
- return f[0].arrayBuffer().then((b) => {
- const b64 = btoa(
- new Uint8Array(b).reduce(
- (data, byte) => data + String.fromCharCode(byte),
- "",
- ),
- );
- return props.onChange({
- content: `data:${f[0].type};base64,${b64}`,
- name: f[0].name,
- type: f[0].type,
- });
- });
- }}
- />
- {props.error && <p class="help is-danger">{props.error}</p>}
- {sizeError && (
- <p class="help is-danger">File should be smaller than 1 MB</p>
- )}
- </div>
- </div>
- );
-}
diff --git a/packages/demobank-ui/src/components/fields/ImageInput.tsx b/packages/demobank-ui/src/components/fields/ImageInput.tsx
deleted file mode 100644
index c190de9a9..000000000
--- a/packages/demobank-ui/src/components/fields/ImageInput.tsx
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-import { h, VNode } from "preact";
-import { useLayoutEffect, useRef, useState } from "preact/hooks";
-import emptyImage from "../../assets/empty.png";
-import { TextInputProps } from "./TextInput";
-
-const MAX_IMAGE_UPLOAD_SIZE = 1024 * 1024;
-
-export function ImageInput(props: TextInputProps): VNode {
- const inputRef = useRef<HTMLInputElement>(null);
- useLayoutEffect(() => {
- if (props.grabFocus) inputRef.current?.focus();
- }, [props.grabFocus]);
-
- const value = props.bind[0];
- // const [dirty, setDirty] = useState(false)
- const image = useRef<HTMLInputElement>(null);
- const [sizeError, setSizeError] = useState(false);
- function onChange(v: string): void {
- // setDirty(true);
- props.bind[1](v);
- }
- return (
- <div class="field">
- <label class="label">
- {props.label}
- {props.tooltip && (
- <span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
- <i class="mdi mdi-information" />
- </span>
- )}
- </label>
- <div class="control">
- <img
- src={!value ? emptyImage : value}
- style={{ width: 200, height: 200 }}
- onClick={() => image.current?.click()}
- />
- <input
- ref={image}
- style={{ display: "none" }}
- type="file"
- name={String(name)}
- onChange={(e) => {
- const f: FileList | null = e.currentTarget.files;
- if (!f || f.length != 1) return onChange(emptyImage);
-
- if (f[0].size > MAX_IMAGE_UPLOAD_SIZE) {
- setSizeError(true);
- return onChange(emptyImage);
- }
- setSizeError(false);
- return f[0].arrayBuffer().then((b) => {
- const b64 = btoa(
- new Uint8Array(b).reduce(
- (data, byte) => data + String.fromCharCode(byte),
- "",
- ),
- );
- return onChange(`data:${f[0].type};base64,${b64}` as any);
- });
- }}
- />
- {props.error && <p class="help is-danger">{props.error}</p>}
- {sizeError && (
- <p class="help is-danger">Image should be smaller than 1 MB</p>
- )}
- </div>
- </div>
- );
-}
diff --git a/packages/demobank-ui/src/components/fields/NumberInput.tsx b/packages/demobank-ui/src/components/fields/NumberInput.tsx
deleted file mode 100644
index 1a54d24b6..000000000
--- a/packages/demobank-ui/src/components/fields/NumberInput.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-import { h, VNode } from "preact";
-import { useLayoutEffect, useRef, useState } from "preact/hooks";
-
-export interface TextInputProps {
- label: string;
- grabFocus?: boolean;
- error?: string;
- placeholder?: string;
- tooltip?: string;
- onConfirm?: () => void;
- bind: [string, (x: string) => void];
-}
-
-export function PhoneNumberInput(props: TextInputProps): VNode {
- const inputRef = useRef<HTMLInputElement>(null);
- useLayoutEffect(() => {
- if (props.grabFocus) inputRef.current?.focus();
- }, [props.grabFocus]);
- const value = props.bind[0];
- const [dirty, setDirty] = useState(false);
- const showError = dirty && props.error;
- return (
- <div class="field">
- <label class="label">
- {props.label}
- {props.tooltip && (
- <span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
- <i class="mdi mdi-information" />
- </span>
- )}
- </label>
- <div class="control has-icons-right">
- <input
- value={value}
- type="tel"
- placeholder={props.placeholder}
- class={showError ? "input is-danger" : "input"}
- onKeyPress={(e) => {
- if (e.key === "Enter" && props.onConfirm) props.onConfirm();
- }}
- onInput={(e) => {
- setDirty(true);
- props.bind[1]((e.target as HTMLInputElement).value);
- }}
- ref={inputRef}
- style={{ display: "block" }}
- />
- </div>
- {showError && <p class="help is-danger">{props.error}</p>}
- </div>
- );
-}
diff --git a/packages/demobank-ui/src/components/fields/TextInput.tsx b/packages/demobank-ui/src/components/fields/TextInput.tsx
deleted file mode 100644
index cc7104cf5..000000000
--- a/packages/demobank-ui/src/components/fields/TextInput.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-import { h, VNode } from "preact";
-import { useLayoutEffect, useRef, useState } from "preact/hooks";
-
-export interface TextInputProps {
- inputType?: "text" | "number" | "multiline" | "password";
- label: string;
- grabFocus?: boolean;
- disabled?: boolean;
- error?: string;
- placeholder?: string;
- tooltip?: string;
- onConfirm?: () => void;
- bind: [string, (x: string) => void];
-}
-
-const TextInputType = function ({ inputType, grabFocus, ...rest }: any): VNode {
- const inputRef = useRef<HTMLInputElement>(null);
- useLayoutEffect(() => {
- if (grabFocus) inputRef.current?.focus();
- }, [grabFocus]);
-
- return inputType === "multiline" ? (
- <textarea {...rest} rows={5} ref={inputRef} style={{ height: "unset" }} />
- ) : (
- <input {...rest} type={inputType} ref={inputRef} />
- );
-};
-
-export function TextInput(props: TextInputProps): VNode {
- const value = props.bind[0];
- const [dirty, setDirty] = useState(false);
- const showError = dirty && props.error;
- return (
- <div class="field">
- <label class="label">
- {props.label}
- {props.tooltip && (
- <span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
- <i class="mdi mdi-information" />
- </span>
- )}
- </label>
- <div class="control has-icons-right">
- <TextInputType
- inputType={props.inputType}
- value={value}
- grabFocus={props.grabFocus}
- disabled={props.disabled}
- placeholder={props.placeholder}
- class={showError ? "input is-danger" : "input"}
- onKeyPress={(e: any) => {
- if (e.key === "Enter" && props.onConfirm) props.onConfirm();
- }}
- onInput={(e: any) => {
- setDirty(true);
- props.bind[1]((e.target as HTMLInputElement).value);
- }}
- style={{ display: "block" }}
- />
- </div>
- {showError && <p class="help is-danger">{props.error}</p>}
- </div>
- );
-}
diff --git a/packages/demobank-ui/src/components/menu/SideBar.tsx b/packages/demobank-ui/src/components/menu/SideBar.tsx
deleted file mode 100644
index 7bfba2a75..000000000
--- a/packages/demobank-ui/src/components/menu/SideBar.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import { h, VNode } from "preact";
-import { useTranslationContext } from "../../context/translation.js";
-
-interface Props {
- mobile?: boolean;
-}
-
-export function Sidebar({ mobile }: Props): VNode {
- // const config = useConfigContext();
- const config = { version: "none" };
- // FIXME: add replacement for __VERSION__ with the current version
- const process = { env: { __VERSION__: "0.0.0" } };
- const { i18n } = useTranslationContext();
-
- return (
- <aside class="aside is-placed-left is-expanded">
- <div class="aside-tools">
- <div class="aside-tools-label">
- <div>
- <b>euFin bank</b>
- </div>
- <div
- class="is-size-7 has-text-right"
- style={{ lineHeight: 0, marginTop: -10 }}
- >
- Version {process.env.__VERSION__} ({config.version})
- </div>
- </div>
- </div>
- <div class="menu is-menu-main">
- <p class="menu-label">
- <i18n.Translate>Bank menu</i18n.Translate>
- </p>
- <ul class="menu-list">
- <li>
- <div class="ml-4">
- <span class="menu-item-label">
- <i18n.Translate>Select option1</i18n.Translate>
- </span>
- </div>
- </li>
- <li>
- <div class="ml-4">
- <span class="menu-item-label">
- <i18n.Translate>Select option2</i18n.Translate>
- </span>
- </div>
- </li>
- </ul>
- </div>
- </aside>
- );
-}
diff --git a/packages/demobank-ui/src/components/menu/index.tsx b/packages/demobank-ui/src/components/menu/index.tsx
deleted file mode 100644
index 6c8292a0c..000000000
--- a/packages/demobank-ui/src/components/menu/index.tsx
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- 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/>
- */
-
-import { ComponentChildren, Fragment, h, VNode } from "preact";
-import Match from "preact-router/match";
-import { useEffect, useState } from "preact/hooks";
-import { NavigationBar } from "./NavigationBar.js";
-import { Sidebar } from "./SideBar.js";
-
-interface MenuProps {
- title: string;
-}
-
-function WithTitle({
- title,
- children,
-}: {
- title: string;
- children: ComponentChildren;
-}): VNode {
- useEffect(() => {
- document.title = `${title}`;
- }, [title]);
- return <Fragment>{children}</Fragment>;
-}
-
-export function Menu({ title }: MenuProps): VNode {
- const [mobileOpen, setMobileOpen] = useState(false);
-
- return (
- <Match>
- {({ path }: { path: string }) => {
- const titleWithSubtitle = title; // title ? title : (!admin ? getInstanceTitle(path, instance) : getAdminTitle(path, instance))
- return (
- <WithTitle title={titleWithSubtitle}>
- <div
- class={mobileOpen ? "has-aside-mobile-expanded" : ""}
- onClick={() => setMobileOpen(false)}
- >
- <NavigationBar
- onMobileMenu={() => setMobileOpen(!mobileOpen)}
- title={titleWithSubtitle}
- />
-
- <Sidebar mobile={mobileOpen} />
- </div>
- </WithTitle>
- );
- }}
- </Match>
- );
-}
-
-interface NotYetReadyAppMenuProps {
- title: string;
- onLogout?: () => void;
-}
-
-interface NotifProps {
- notification?: Notification;
-}
-export function NotificationCard({
- notification: n,
-}: NotifProps): VNode | null {
- if (!n) return null;
- return (
- <div class="notification">
- <div class="columns is-vcentered">
- <div class="column is-12">
- <article
- class={
- n.type === "ERROR"
- ? "message is-danger"
- : n.type === "WARN"
- ? "message is-warning"
- : "message is-info"
- }
- >
- <div class="message-header">
- <p>{n.message}</p>
- </div>
- {n.description && <div class="message-body">{n.description}</div>}
- </article>
- </div>
- </div>
- </div>
- );
-}
-
-export function NotYetReadyAppMenu({
- onLogout,
- title,
-}: NotYetReadyAppMenuProps): VNode {
- const [mobileOpen, setMobileOpen] = useState(false);
-
- useEffect(() => {
- document.title = `Taler Backoffice: ${title}`;
- }, [title]);
-
- return (
- <div
- class="has-aside-mobile-expanded"
- // class={mobileOpen ? "has-aside-mobile-expanded" : ""}
- onClick={() => setMobileOpen(false)}
- >
- <NavigationBar
- onMobileMenu={() => setMobileOpen(!mobileOpen)}
- title={title}
- />
- {onLogout && <Sidebar mobile={mobileOpen} />}
- </div>
- );
-}
-
-export interface Notification {
- message: string;
- description?: string | VNode;
- type: MessageType;
-}
-
-export type ValueOrFunction<T> = T | ((p: T) => T);
-export type MessageType = "INFO" | "WARN" | "ERROR" | "SUCCESS";
diff --git a/packages/demobank-ui/src/context/pageState.ts b/packages/demobank-ui/src/context/pageState.ts
deleted file mode 100644
index b954ad20e..000000000
--- a/packages/demobank-ui/src/context/pageState.ts
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- 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/>
- */
-
-import { hooks } from "@gnu-taler/web-util/lib/index.browser";
-import { ComponentChildren, createContext, h, VNode } from "preact";
-import { StateUpdater, useContext } from "preact/hooks";
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-export type Type = {
- pageState: PageStateType;
- pageStateSetter: StateUpdater<PageStateType>;
-};
-const initial: Type = {
- pageState: {
- isRawPayto: false,
- withdrawalInProgress: false,
- },
- pageStateSetter: () => {
- null;
- },
-};
-const Context = createContext<Type>(initial);
-
-export const usePageContext = (): Type => useContext(Context);
-
-export const PageStateProvider = ({
- children,
-}: {
- children: ComponentChildren;
-}): VNode => {
- const [pageState, pageStateSetter] = usePageState();
-
- return h(Context.Provider, {
- value: { pageState, pageStateSetter },
- children,
- });
-};
-
-/**
- * Wrapper providing defaults.
- */
-function usePageState(
- state: PageStateType = {
- isRawPayto: false,
- withdrawalInProgress: false,
- },
-): [PageStateType, StateUpdater<PageStateType>] {
- const ret = hooks.useNotNullLocalStorage("page-state", JSON.stringify(state));
- const retObj: PageStateType = JSON.parse(ret[0]);
-
- const retSetter: StateUpdater<PageStateType> = function (val) {
- const newVal =
- val instanceof Function
- ? JSON.stringify(val(retObj))
- : JSON.stringify(val);
-
- ret[1](newVal);
- };
-
- //when moving from one page to another
- //clean up the info and error bar
- function removeLatestInfo(val: any): ReturnType<typeof retSetter> {
- const updater = typeof val === "function" ? val : (c: any) => val;
- return retSetter((current: any) => {
- const cleanedCurrent: PageStateType = {
- ...current,
- info: undefined,
- errors: undefined,
- timestamp: new Date().getTime(),
- };
- return updater(cleanedCurrent);
- });
- }
-
- return [retObj, removeLatestInfo];
-}
-
-/**
- * Track page state.
- */
-export interface PageStateType {
- isRawPayto: boolean;
- withdrawalInProgress: boolean;
- error?: {
- description?: string;
- title: string;
- debug?: string;
- };
-
- info?: string;
- talerWithdrawUri?: string;
- /**
- * Not strictly a presentational value, could
- * be moved in a future "withdrawal state" object.
- */
- withdrawalId?: string;
- timestamp?: number;
-}
diff --git a/packages/demobank-ui/src/declaration.d.ts b/packages/demobank-ui/src/declaration.d.ts
deleted file mode 100644
index 1b56f1358..000000000
--- a/packages/demobank-ui/src/declaration.d.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-declare module "*.css" {
- const mapping: Record<string, string>;
- export default mapping;
-}
-declare module "*.svg" {
- const content: any;
- export default content;
-}
-declare module "*.jpeg" {
- const content: any;
- export default content;
-}
-declare module "*.png" {
- const content: any;
- export default content;
-}
-declare module "jed" {
- const x: any;
- export = x;
-}
-
-/**********************************************
- * Type definitions for states and API calls. *
- *********************************************/
-
-/**
- * Request body of POST /transactions.
- *
- * If the amount appears twice: both as a Payto parameter and
- * in the JSON dedicate field, the one on the Payto URI takes
- * precedence.
- */
-interface TransactionRequestType {
- paytoUri: string;
- amount?: string; // with currency.
-}
-
-/**
- * Request body of /register.
- */
-interface CredentialsRequestType {
- username?: string;
- password?: string;
- repeatPassword?: string;
-}
-
-/**
- * Request body of /register.
- */
-// interface LoginRequestType {
-// username: string;
-// password: string;
-// }
-
-interface WireTransferRequestType {
- iban?: string;
- subject?: string;
- amount?: string;
-}
diff --git a/packages/demobank-ui/src/hooks/backend.ts b/packages/demobank-ui/src/hooks/backend.ts
deleted file mode 100644
index babdcd830..000000000
--- a/packages/demobank-ui/src/hooks/backend.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-import { hooks } from "@gnu-taler/web-util/lib/index.browser";
-
-/**
- * Has the information to reach and
- * authenticate at the bank's backend.
- */
-export type BackendState = LoggedIn | LoggedOut;
-
-export interface BackendInfo {
- url: string;
- username: string;
- password: string;
-}
-
-interface LoggedIn extends BackendInfo {
- status: "loggedIn";
-}
-interface LoggedOut {
- status: "loggedOut";
-}
-
-export const defaultState: BackendState = { status: "loggedOut" };
-
-export interface BackendStateHandler {
- state: BackendState;
- clear(): void;
- save(info: BackendInfo): void;
-}
-/**
- * Return getters and setters for
- * login credentials and backend's
- * base URL.
- */
-export function useBackendState(): BackendStateHandler {
- const [value, update] = hooks.useLocalStorage(
- "backend-state",
- JSON.stringify(defaultState),
- );
- // const parsed = value !== undefined ? JSON.parse(value) : value;
- let parsed;
- try {
- parsed = JSON.parse(value!);
- } catch {
- parsed = undefined;
- }
- const state: BackendState = !parsed?.status ? defaultState : parsed;
-
- return {
- state,
- clear() {
- update(JSON.stringify(defaultState));
- },
- save(info) {
- const nextState: BackendState = { status: "loggedIn", ...info };
- update(JSON.stringify(nextState));
- },
- };
-}
diff --git a/packages/demobank-ui/src/hooks/index.ts b/packages/demobank-ui/src/hooks/index.ts
deleted file mode 100644
index c6e3fe8c1..000000000
--- a/packages/demobank-ui/src/hooks/index.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import { StateUpdater } from "preact/hooks";
-import { hooks } from "@gnu-taler/web-util/lib/index.browser";
-export type ValueOrFunction<T> = T | ((p: T) => T);
-
-const calculateRootPath = () => {
- const rootPath =
- typeof window !== undefined
- ? window.location.origin + window.location.pathname
- : "/";
- return rootPath;
-};
-
-export function useBackendURL(
- url?: string,
-): [string, boolean, StateUpdater<string>, () => void] {
- const [value, setter] = hooks.useNotNullLocalStorage(
- "backend-url",
- url || calculateRootPath(),
- );
- const [triedToLog, setTriedToLog] = hooks.useLocalStorage("tried-login");
-
- const checkedSetter = (v: ValueOrFunction<string>) => {
- setTriedToLog("yes");
- return setter((p) => (v instanceof Function ? v(p) : v).replace(/\/$/, ""));
- };
-
- const resetBackend = () => {
- setTriedToLog(undefined);
- };
- return [value, !!triedToLog, checkedSetter, resetBackend];
-}
-
-export function useBackendDefaultToken(): [
- string | undefined,
- StateUpdater<string | undefined>,
-] {
- return hooks.useLocalStorage("backend-token");
-}
-
-export function useBackendInstanceToken(
- id: string,
-): [string | undefined, StateUpdater<string | undefined>] {
- const [token, setToken] = hooks.useLocalStorage(`backend-token-${id}`);
- const [defaultToken, defaultSetToken] = useBackendDefaultToken();
-
- // instance named 'default' use the default token
- if (id === "default") return [defaultToken, defaultSetToken];
-
- return [token, setToken];
-}
diff --git a/packages/demobank-ui/src/i18n/bank.pot b/packages/demobank-ui/src/i18n/bank.pot
deleted file mode 100644
index 862aa4d97..000000000
--- a/packages/demobank-ui/src/i18n/bank.pot
+++ /dev/null
@@ -1,258 +0,0 @@
-#: /home/job/backoffice/packages/bank/src/components/picker/DurationPicker.tsx:55
-#, c-format
-msgid "days"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/components/picker/DurationPicker.tsx:65
-#, c-format
-msgid "hours"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/components/picker/DurationPicker.tsx:76
-#, c-format
-msgid "minutes"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/components/picker/DurationPicker.tsx:87
-#, c-format
-msgid "seconds"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:734
-#, c-format
-msgid "Clear"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:761
-#, c-format
-msgid "Logout"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:782
-#, c-format
-msgid "Demo Bank"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:837
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:840
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1189
-#, c-format
-msgid "Go back"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:845
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:906
-#, c-format
-msgid "Wire transfer"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:846
-#, c-format
-msgid "Transfer money to another account of this bank:"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:897
-#, c-format
-msgid "Want to try the raw payto://-format?"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:907
-#, c-format
-msgid "Transfer money via the Payto system:"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:916
-#, c-format
-msgid "payto address"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:926
-#, c-format
-msgid "Confirm"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:956
-#, c-format
-msgid "Confirm Withdrawal"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1026
-#, c-format
-msgid "Waiting the bank to create the operation..."
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1044
-#, c-format
-msgid "This withdrawal was aborted!"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1051
-#, c-format
-msgid "Withdraw to a Taler Wallet"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1052
-#, c-format
-msgid "You can use this QR code to withdraw to your mobile wallet:"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1054
-#, c-format
-msgid "this link"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1060
-#, c-format
-msgid "Abort"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1084
-#, c-format
-msgid "Start withdrawal"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1101
-#, c-format
-msgid "Withdraw Money into a Taler wallet"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1105
-#, c-format
-msgid "Amount to withdraw"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1137
-#, c-format
-msgid "Please login!"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1169
-#, c-format
-msgid "Login"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1184
-#, c-format
-msgid "Register to the euFin bank!"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1194
-#, c-format
-msgid "Registration form"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1232
-#, c-format
-msgid "Register"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1272
-#, c-format
-msgid "Date"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1273
-#, c-format
-msgid "Amount"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1274
-#, c-format
-msgid "Counterpart"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1275
-#, c-format
-msgid "Subject"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1343
-#, c-format
-msgid "Username or account label '%1$s' not found. Won't login."
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1365
-#, c-format
-msgid "Wrong credentials given."
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1374
-#, c-format
-msgid "Account information could not be retrieved."
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1394
-#, c-format
-msgid "Close wire transfer"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1412
-#, c-format
-msgid "Close Taler withdrawal"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1457
-#, c-format
-msgid "Bank account balance:"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1469
-#, c-format
-msgid "Latest transactions:"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1474
-#, c-format
-msgid "Transfer money manually"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1543
-#, c-format
-msgid "List of public accounts was not found."
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1552
-#, c-format
-msgid "List of public accounts could not be retrieved."
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1584
-#, c-format
-msgid "History of public accounts"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1643
-#, c-format
-msgid "Page has a problem: logged in but backend state is lost."
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1667
-#, c-format
-msgid "Welcome to the euFin bank!"
-msgstr ""
-
-# 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/>
-#
-#, fuzzy
-msgid ""
-msgstr ""
-"Project-Id-Version: Taler Wallet\n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-11-23 00:00+0100\n"
-"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
-"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
-"Language-Team: LANGUAGE <LL@li.org>\n"
-"Language: \n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=2; plural=(n != 1);\n"
diff --git a/packages/demobank-ui/src/i18n/de.po b/packages/demobank-ui/src/i18n/de.po
deleted file mode 100644
index bd4158037..000000000
--- a/packages/demobank-ui/src/i18n/de.po
+++ /dev/null
@@ -1,257 +0,0 @@
-#: /home/job/backoffice/packages/bank/src/components/picker/DurationPicker.tsx:55
-#, c-format
-msgid "days"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/components/picker/DurationPicker.tsx:65
-#, c-format
-msgid "hours"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/components/picker/DurationPicker.tsx:76
-#, c-format
-msgid "minutes"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/components/picker/DurationPicker.tsx:87
-#, c-format
-msgid "seconds"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:734
-#, c-format
-msgid "Clear"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:761
-#, c-format
-msgid "Logout"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:782
-#, c-format
-msgid "Demo Bank"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:837
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:840
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1189
-#, c-format
-msgid "Go back"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:845
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:906
-#, c-format
-msgid "Wire transfer"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:846
-#, c-format
-msgid "Transfer money to another account of this bank:"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:897
-#, c-format
-msgid "Want to try the raw payto://-format?"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:907
-#, c-format
-msgid "Transfer money via the Payto system:"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:916
-#, c-format
-msgid "payto address"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:926
-#, c-format
-msgid "Confirm"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:956
-#, c-format
-msgid "Confirm Withdrawal"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1026
-#, c-format
-msgid "Waiting the bank to create the operaion..."
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1044
-#, c-format
-msgid "This withdrawal was aborted!"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1051
-#, c-format
-msgid "Withdraw to a Taler Wallet"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1052
-#, c-format
-msgid "You can use this QR code to withdraw to your mobile wallet:"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1054
-#, c-format
-msgid "this link"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1060
-#, c-format
-msgid "Abort"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1084
-#, c-format
-msgid "Start withdrawal"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1101
-#, c-format
-msgid "Withdraw Money into a Taler wallet"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1105
-#, c-format
-msgid "Amount to withdraw"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1137
-#, c-format
-msgid "Please login!"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1169
-#, c-format
-msgid "Login"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1184
-#, c-format
-msgid "Register to the euFin bank!"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1194
-#, c-format
-msgid "Registration form"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1232
-#, c-format
-msgid "Register"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1272
-#, c-format
-msgid "Date"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1273
-#, c-format
-msgid "Amount"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1274
-#, c-format
-msgid "Counterpart"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1275
-#, c-format
-msgid "Subject"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1343
-#, c-format
-msgid "Username or account label '%1$s' not found. Won't login."
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1365
-#, c-format
-msgid "Wrong credentials given."
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1374
-#, c-format
-msgid "Account information could not be retrieved."
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1394
-#, c-format
-msgid "Close wire transfer"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1412
-#, c-format
-msgid "Close Taler withdrawal"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1457
-#, c-format
-msgid "Bank account balance:"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1469
-#, c-format
-msgid "Latest transactions:"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1474
-#, c-format
-msgid "Transfer money manually"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1543
-#, c-format
-msgid "List of public accounts was not found."
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1552
-#, c-format
-msgid "List of public accounts could not be retrieved."
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1584
-#, c-format
-msgid "History of public accounts"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1643
-#, c-format
-msgid "Page has a problem: logged in but backend state is lost."
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1667
-#, c-format
-msgid "Welcome to the euFin bank!"
-msgstr ""
-
-# 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/>
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: Taler Wallet\n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-11-23 00:00+0100\n"
-"PO-Revision-Date: 2022-01-08 09:57+0100\n"
-"Last-Translator: <translate@taler.net>\n"
-"Language-Team: German\n"
-"Language: de\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=2; plural=(n != 1);\n"
diff --git a/packages/demobank-ui/src/i18n/en.po b/packages/demobank-ui/src/i18n/en.po
deleted file mode 100644
index 4cbc9e74c..000000000
--- a/packages/demobank-ui/src/i18n/en.po
+++ /dev/null
@@ -1,266 +0,0 @@
-#: /home/job/backoffice/packages/bank/src/components/picker/DurationPicker.tsx:55
-#, c-format
-msgid "days"
-msgstr "days"
-
-#: /home/job/backoffice/packages/bank/src/components/picker/DurationPicker.tsx:65
-#, c-format
-msgid "hours"
-msgstr "hours"
-
-#: /home/job/backoffice/packages/bank/src/components/picker/DurationPicker.tsx:76
-#, c-format
-msgid "minutes"
-msgstr "minutes"
-
-#: /home/job/backoffice/packages/bank/src/components/picker/DurationPicker.tsx:87
-#, c-format
-msgid "seconds"
-msgstr "seconds"
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:734
-#, c-format
-msgid "Clear"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:761
-#, c-format
-msgid "Logout"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:782
-#, c-format
-msgid "Demo Bank"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:837
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:840
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1189
-#, c-format
-msgid "Go back"
-msgstr "Go back"
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:845
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:906
-#, c-format
-msgid "Wire transfer"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:846
-#, c-format
-msgid "Transfer money to another account of this bank:"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:897
-#, c-format
-msgid "Want to try the raw payto://-format?"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:907
-#, c-format
-msgid "Transfer money via the Payto system:"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:916
-#, c-format
-msgid "payto address"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:926
-#, c-format
-msgid "Confirm"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:956
-#, fuzzy, c-format
-msgid "Confirm Withdrawal"
-msgstr "Confirm withdrawal"
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1026
-#, c-format
-msgid "Waiting the bank to create the operaion..."
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1044
-#, c-format
-msgid "This withdrawal was aborted!"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1051
-#, fuzzy, c-format
-msgid "Withdraw to a Taler Wallet"
-msgstr "Charge Taler wallet"
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1052
-#, c-format
-msgid "You can use this QR code to withdraw to your mobile wallet:"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1054
-#, c-format
-msgid "this link"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1060
-#, c-format
-msgid "Abort"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1084
-#, fuzzy, c-format
-msgid "Start withdrawal"
-msgstr "Start withdrawal"
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1101
-#, fuzzy, c-format
-msgid "Withdraw Money into a Taler wallet"
-msgstr "Charge Taler wallet"
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1105
-#, fuzzy, c-format
-msgid "Amount to withdraw"
-msgstr "Amount to withdraw"
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1137
-#, c-format
-msgid "Please login!"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1169
-#, c-format
-msgid "Login"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1184
-#, c-format
-msgid "Register to the euFin bank!"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1194
-#, c-format
-msgid "Registration form"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1232
-#, c-format
-msgid "Register"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1272
-#, c-format
-msgid "Date"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1273
-#, c-format
-msgid "Amount"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1274
-#, c-format
-msgid "Counterpart"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1275
-#, c-format
-msgid "Subject"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1343
-#, c-format
-msgid "Username or account label '%1$s' not found. Won't login."
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1365
-#, c-format
-msgid "Wrong credentials given."
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1374
-#, c-format
-msgid "Account information could not be retrieved."
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1394
-#, c-format
-msgid "Close wire transfer"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1412
-#, fuzzy, c-format
-msgid "Close Taler withdrawal"
-msgstr "Close Taler withdrawal"
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1457
-#, c-format
-msgid "Bank account balance:"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1469
-#, c-format
-msgid "Latest transactions:"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1474
-#, c-format
-msgid "Transfer money manually"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1543
-#, c-format
-msgid "List of public accounts was not found."
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1552
-#, c-format
-msgid "List of public accounts could not be retrieved."
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1584
-#, c-format
-msgid "History of public accounts"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1643
-#, c-format
-msgid "Page has a problem: logged in but backend state is lost."
-msgstr "Page has a problem: logged in but backend state is lost."
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1667
-#, fuzzy, c-format
-msgid "Welcome to the euFin bank!"
-msgstr "Welcome to euFin bank: Taler+IBAN now possible!"
-
-# 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/>
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: Taler Wallet\n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-11-23 00:00+0100\n"
-"PO-Revision-Date: 2022-01-08 09:57+0100\n"
-"Last-Translator: <translate@taler.net>\n"
-"Language-Team: English\n"
-"Language: en\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-
-#~ msgid "Page has a problem:"
-#~ msgstr "Page has a problem:"
-
-#~ msgid "Close"
-#~ msgstr "Close"
-
-#~ msgid "Sign in"
-#~ msgstr "Sign in"
diff --git a/packages/demobank-ui/src/i18n/it.po b/packages/demobank-ui/src/i18n/it.po
deleted file mode 100644
index 91a30b947..000000000
--- a/packages/demobank-ui/src/i18n/it.po
+++ /dev/null
@@ -1,258 +0,0 @@
-#: /home/job/backoffice/packages/bank/src/components/picker/DurationPicker.tsx:55
-#, c-format
-msgid "days"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/components/picker/DurationPicker.tsx:65
-#, c-format
-msgid "hours"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/components/picker/DurationPicker.tsx:76
-#, c-format
-msgid "minutes"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/components/picker/DurationPicker.tsx:87
-#, c-format
-msgid "seconds"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:734
-#, c-format
-msgid "Clear"
-msgstr "Cancella"
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:761
-#, c-format
-msgid "Logout"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:782
-#, c-format
-msgid "Demo Bank"
-msgstr "Banca 'demo'"
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:837
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:840
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1189
-#, c-format
-msgid "Go back"
-msgstr "Indietro"
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:845
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:906
-#, c-format
-msgid "Wire transfer"
-msgstr "Bonifico"
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:846
-#, c-format
-msgid "Transfer money to another account of this bank:"
-msgstr "Trasferisci fondi a un altro conto di questa banca:"
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:897
-#, c-format
-msgid "Want to try the raw payto://-format?"
-msgstr "Prova il trasferimento tramite il formato Payto!"
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:907
-#, c-format
-msgid "Transfer money via the Payto system:"
-msgstr "Effettua un bonifico tramite il sistema Payto:"
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:916
-#, c-format
-msgid "payto address"
-msgstr "indirizzo Payto"
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:926
-#, c-format
-msgid "Confirm"
-msgstr "Conferma"
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:956
-#, c-format
-msgid "Confirm Withdrawal"
-msgstr "Conferma il ritiro"
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1026
-#, c-format
-msgid "Waiting the bank to create the operaion..."
-msgstr "La banca sta creando l'operazione..."
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1044
-#, c-format
-msgid "This withdrawal was aborted!"
-msgstr "Questo ritiro è stato annullato!"
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1051
-#, c-format
-msgid "Withdraw to a Taler Wallet"
-msgstr "Ritira contante nel portafoglio Taler"
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1052
-#, c-format
-msgid "You can use this QR code to withdraw to your mobile wallet:"
-msgstr "Usa questo codice QR per ritirare contante nel tuo wallet:"
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1054
-#, c-format
-msgid "this link"
-msgstr "questo link"
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1060
-#, c-format
-msgid "Abort"
-msgstr "Annulla"
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1084
-#, c-format
-msgid "Start withdrawal"
-msgstr "Ritira contante"
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1101
-#, c-format
-msgid "Withdraw Money into a Taler wallet"
-msgstr "Ritira contante nel portafoglio Taler"
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1105
-#, c-format
-msgid "Amount to withdraw"
-msgstr "Somma da ritirare"
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1137
-#, c-format
-msgid "Please login!"
-msgstr "Accedi!"
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1169
-#, c-format
-msgid "Login"
-msgstr "Accedi"
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1184
-#, c-format
-msgid "Register to the euFin bank!"
-msgstr "Apri un conto in banca euFin!"
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1194
-#, c-format
-msgid "Registration form"
-msgstr "Registrazione"
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1232
-#, c-format
-msgid "Register"
-msgstr "Registrati"
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1272
-#, c-format
-msgid "Date"
-msgstr ""
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1273
-#, c-format
-msgid "Amount"
-msgstr "Somma"
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1274
-#, c-format
-msgid "Counterpart"
-msgstr "Controparte"
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1275
-#, c-format
-msgid "Subject"
-msgstr "Causale"
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1343
-#, c-format
-msgid "Username or account label '%1$s' not found. Won't login."
-msgstr "L'utente '%1$s' non esiste. Login impossibile"
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1365
-#, c-format
-msgid "Wrong credentials given."
-msgstr "Credenziali invalide."
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1374
-#, c-format
-msgid "Account information could not be retrieved."
-msgstr "Impossibile ricevere le informazioni relative al conto."
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1394
-#, c-format
-msgid "Close wire transfer"
-msgstr "Chiudi il bonifico"
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1412
-#, c-format
-msgid "Close Taler withdrawal"
-msgstr "Chiudi il ritiro Taler"
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1457
-#, c-format
-msgid "Bank account balance:"
-msgstr "Bilancio:"
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1469
-#, c-format
-msgid "Latest transactions:"
-msgstr "Ultime transazioni:"
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1474
-#, c-format
-msgid "Transfer money manually"
-msgstr "Effettua un bonifico"
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1543
-#, c-format
-msgid "List of public accounts was not found."
-msgstr "Lista conti pubblici non trovata."
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1552
-#, c-format
-msgid "List of public accounts could not be retrieved."
-msgstr "Lista conti pubblici non pervenuta."
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1584
-#, c-format
-msgid "History of public accounts"
-msgstr "Storico dei conti pubblici"
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1643
-#, c-format
-msgid "Page has a problem: logged in but backend state is lost."
-msgstr ""
-"Stato inconsistente: accesso utente effettuato ma stato con server perso."
-
-#: /home/job/backoffice/packages/bank/src/pages/home/index.tsx:1667
-#, fuzzy, c-format
-msgid "Welcome to the euFin bank!"
-msgstr "Benvenuti in banca euFin!"
-
-# 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/>
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: Taler Wallet\n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-11-23 00:00+0100\n"
-"PO-Revision-Date: 2022-01-08 10:05+0100\n"
-"Last-Translator: <translate@taler.net>\n"
-"Language-Team: Italian\n"
-"Language: it\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=2; plural=(n != 1);\n"
diff --git a/packages/demobank-ui/src/i18n/poheader b/packages/demobank-ui/src/i18n/poheader
deleted file mode 100644
index ee3fcd7be..000000000
--- a/packages/demobank-ui/src/i18n/poheader
+++ /dev/null
@@ -1,27 +0,0 @@
-# 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/>
-
-#
-#, fuzzy
-msgid ""
-msgstr ""
-"Project-Id-Version: Taler Wallet\n"
-"Report-Msgid-Bugs-To: taler@gnu.org\n"
-"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
-"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
-"Language-Team: LANGUAGE <LL@li.org>\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=2; plural=(n != 1);\n"
diff --git a/packages/demobank-ui/src/i18n/strings.ts b/packages/demobank-ui/src/i18n/strings.ts
deleted file mode 100644
index d9af71657..000000000
--- a/packages/demobank-ui/src/i18n/strings.ts
+++ /dev/null
@@ -1,221 +0,0 @@
-/*
- 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/>
- */
-
-/*eslint quote-props: ["error", "consistent"]*/
-export const strings: { [s: string]: any } = {};
-
-strings["de"] = {
- domain: "messages",
- locale_data: {
- messages: {
- days: [""],
- hours: [""],
- minutes: [""],
- seconds: [""],
- Clear: [""],
- Logout: [""],
- "Demo Bank": [""],
- "Go back": [""],
- "Wire transfer": [""],
- "Transfer money to another account of this bank:": [""],
- "Want to try the raw payto://-format?": [""],
- "Transfer money via the Payto system:": [""],
- "payto address": [""],
- Confirm: [""],
- "Confirm Withdrawal": [""],
- "Waiting the bank to create the operaion...": [""],
- "This withdrawal was aborted!": [""],
- "Withdraw to a Taler Wallet": [""],
- "You can use this QR code to withdraw to your mobile wallet:": [""],
- "this link": [""],
- Abort: [""],
- "Start withdrawal": [""],
- "Withdraw Money into a Taler wallet": [""],
- "Amount to withdraw": [""],
- "Please login!": [""],
- Login: [""],
- "Register to the euFin bank!": [""],
- "Registration form": [""],
- Register: [""],
- Date: [""],
- Amount: [""],
- Counterpart: [""],
- Subject: [""],
- "Username or account label '%1$s' not found. Won't login.": [""],
- "Wrong credentials given.": [""],
- "Account information could not be retrieved.": [""],
- "Close wire transfer": [""],
- "Close Taler withdrawal": [""],
- "Bank account balance:": [""],
- "Latest transactions:": [""],
- "Transfer money manually": [""],
- "List of public accounts was not found.": [""],
- "List of public accounts could not be retrieved.": [""],
- "History of public accounts": [""],
- "Page has a problem: logged in but backend state is lost.": [""],
- "Welcome to the euFin bank!": [""],
- "": {
- domain: "messages",
- plural_forms: "nplurals=2; plural=(n != 1);",
- lang: "de",
- },
- },
- },
-};
-
-strings["en"] = {
- domain: "messages",
- locale_data: {
- messages: {
- days: ["days"],
- hours: ["hours"],
- minutes: ["minutes"],
- seconds: ["seconds"],
- Clear: [""],
- Logout: [""],
- "Demo Bank": [""],
- "Go back": ["Go back"],
- "Wire transfer": [""],
- "Transfer money to another account of this bank:": [""],
- "Want to try the raw payto://-format?": [""],
- "Transfer money via the Payto system:": [""],
- "payto address": [""],
- Confirm: [""],
- "Confirm Withdrawal": ["Confirm withdrawal"],
- "Waiting the bank to create the operaion...": [""],
- "This withdrawal was aborted!": [""],
- "Withdraw to a Taler Wallet": ["Charge Taler wallet"],
- "You can use this QR code to withdraw to your mobile wallet:": [""],
- "this link": [""],
- Abort: [""],
- "Start withdrawal": ["Start withdrawal"],
- "Withdraw Money into a Taler wallet": ["Charge Taler wallet"],
- "Amount to withdraw": ["Amount to withdraw"],
- "Please login!": [""],
- Login: [""],
- "Register to the euFin bank!": [""],
- "Registration form": [""],
- Register: [""],
- Date: [""],
- Amount: [""],
- Counterpart: [""],
- Subject: [""],
- "Username or account label '%1$s' not found. Won't login.": [""],
- "Wrong credentials given.": [""],
- "Account information could not be retrieved.": [""],
- "Close wire transfer": [""],
- "Close Taler withdrawal": ["Close Taler withdrawal"],
- "Bank account balance:": [""],
- "Latest transactions:": [""],
- "Transfer money manually": [""],
- "List of public accounts was not found.": [""],
- "List of public accounts could not be retrieved.": [""],
- "History of public accounts": [""],
- "Page has a problem: logged in but backend state is lost.": [
- "Page has a problem: logged in but backend state is lost.",
- ],
- "Welcome to the euFin bank!": [
- "Welcome to euFin bank: Taler+IBAN now possible!",
- ],
- "": {
- domain: "messages",
- plural_forms: "nplurals=2; plural=(n != 1);",
- lang: "en",
- },
- },
- },
-};
-
-strings["it"] = {
- domain: "messages",
- locale_data: {
- messages: {
- days: [""],
- hours: [""],
- minutes: [""],
- seconds: [""],
- Clear: ["Cancella"],
- Logout: [""],
- "Demo Bank": ["Banca 'demo'"],
- "Go back": ["Indietro"],
- "Wire transfer": ["Bonifico"],
- "Transfer money to another account of this bank:": [
- "Trasferisci fondi a un altro conto di questa banca:",
- ],
- "Want to try the raw payto://-format?": [
- "Prova il trasferimento tramite il formato Payto!",
- ],
- "Transfer money via the Payto system:": [
- "Effettua un bonifico tramite il sistema Payto:",
- ],
- "payto address": ["indirizzo Payto"],
- Confirm: ["Conferma"],
- "Confirm Withdrawal": ["Conferma il ritiro"],
- "Waiting the bank to create the operaion...": [
- "La banca sta creando l'operazione...",
- ],
- "This withdrawal was aborted!": ["Questo ritiro è stato annullato!"],
- "Withdraw to a Taler Wallet": ["Ritira contante nel portafoglio Taler"],
- "You can use this QR code to withdraw to your mobile wallet:": [
- "Usa questo codice QR per ritirare contante nel tuo wallet:",
- ],
- "this link": ["questo link"],
- Abort: ["Annulla"],
- "Start withdrawal": ["Ritira contante"],
- "Withdraw Money into a Taler wallet": [
- "Ritira contante nel portafoglio Taler",
- ],
- "Amount to withdraw": ["Somma da ritirare"],
- "Please login!": ["Accedi!"],
- Login: ["Accedi"],
- "Register to the euFin bank!": ["Apri un conto in banca euFin!"],
- "Registration form": ["Registrazione"],
- Register: ["Registrati"],
- Date: [""],
- Amount: ["Somma"],
- Counterpart: ["Controparte"],
- Subject: ["Causale"],
- "Username or account label '%1$s' not found. Won't login.": [
- "L'utente '%1$s' non esiste. Login impossibile",
- ],
- "Wrong credentials given.": ["Credenziali invalide."],
- "Account information could not be retrieved.": [
- "Impossibile ricevere le informazioni relative al conto.",
- ],
- "Close wire transfer": ["Chiudi il bonifico"],
- "Close Taler withdrawal": ["Chiudi il ritiro Taler"],
- "Bank account balance:": ["Bilancio:"],
- "Latest transactions:": ["Ultime transazioni:"],
- "Transfer money manually": ["Effettua un bonifico"],
- "List of public accounts was not found.": [
- "Lista conti pubblici non trovata.",
- ],
- "List of public accounts could not be retrieved.": [
- "Lista conti pubblici non pervenuta.",
- ],
- "History of public accounts": ["Storico dei conti pubblici"],
- "Page has a problem: logged in but backend state is lost.": [
- "Stato inconsistente: accesso utente effettuato ma stato con server perso.",
- ],
- "Welcome to the euFin bank!": ["Benvenuti in banca euFin!"],
- "": {
- domain: "messages",
- plural_forms: "nplurals=2; plural=(n != 1);",
- lang: "it",
- },
- },
- },
-};
diff --git a/packages/demobank-ui/src/index.tsx b/packages/demobank-ui/src/index.tsx
deleted file mode 100644
index 0b88b0393..000000000
--- a/packages/demobank-ui/src/index.tsx
+++ /dev/null
@@ -1,8 +0,0 @@
-import App from "./components/app.js";
-export default App;
-import { render, h } from "preact";
-import "./scss/main.scss";
-
-const app = document.getElementById("app");
-
-render(<App />, app as any);
diff --git a/packages/demobank-ui/src/pages/Routing.tsx b/packages/demobank-ui/src/pages/Routing.tsx
deleted file mode 100644
index 6b00df97b..000000000
--- a/packages/demobank-ui/src/pages/Routing.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-import { createHashHistory } from "history";
-import { h, VNode } from "preact";
-import Router, { route, Route } from "preact-router";
-import { useEffect } from "preact/hooks";
-import { AccountPage } from "./home/AccountPage.js";
-import { PublicHistoriesPage } from "./home/PublicHistoriesPage.js";
-import { RegistrationPage } from "./home/RegistrationPage.js";
-
-export function Routing(): VNode {
- const history = createHashHistory();
- return (
- <Router history={history}>
- <Route path="/public-accounts" component={PublicHistoriesPage} />
- <Route path="/register" component={RegistrationPage} />
- <Route path="/account" component={AccountPage} />
- <Route default component={Redirect} to="/account" />
- </Router>
- );
-}
-
-function Redirect({ to }: { to: string }): VNode {
- useEffect(() => {
- route(to, true);
- }, []);
- return <div>being redirected to {to}</div>;
-}
diff --git a/packages/demobank-ui/src/pages/home/AccountPage.tsx b/packages/demobank-ui/src/pages/home/AccountPage.tsx
deleted file mode 100644
index f2745e45c..000000000
--- a/packages/demobank-ui/src/pages/home/AccountPage.tsx
+++ /dev/null
@@ -1,266 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-import { Amounts, HttpStatusCode, Logger } from "@gnu-taler/taler-util";
-import { ComponentChildren, Fragment, h, VNode } from "preact";
-import { useEffect } from "preact/hooks";
-import useSWR, { SWRConfig, useSWRConfig } from "swr";
-import { useBackendContext } from "../../context/backend.js";
-import { PageStateType, usePageContext } from "../../context/pageState.js";
-import { useTranslationContext } from "../../context/translation.js";
-import { BackendInfo } from "../../hooks/backend.js";
-import { bankUiSettings } from "../../settings.js";
-import { getIbanFromPayto, prepareHeaders } from "../../utils.js";
-import { BankFrame } from "./BankFrame.js";
-import { LoginForm } from "./LoginForm.js";
-import { PaymentOptions } from "./PaymentOptions.js";
-import { Transactions } from "./Transactions.js";
-import { WithdrawalQRCode } from "./WithdrawalQRCode.js";
-
-export function AccountPage(): VNode {
- const backend = useBackendContext();
- const { i18n } = useTranslationContext();
-
- if (backend.state.status === "loggedOut") {
- return (
- <BankFrame>
- <h1 class="nav">{i18n.str`Welcome to ${bankUiSettings.bankName}!`}</h1>
- <LoginForm />
- </BankFrame>
- );
- }
-
- return (
- <SWRWithCredentials info={backend.state}>
- <Account accountLabel={backend.state.username} />
- </SWRWithCredentials>
- );
-}
-
-/**
- * Factor out login credentials.
- */
-function SWRWithCredentials({
- children,
- info,
-}: {
- children: ComponentChildren;
- info: BackendInfo;
-}): VNode {
- const { username, password, url: backendUrl } = info;
- const headers = prepareHeaders(username, password);
- return (
- <SWRConfig
- value={{
- fetcher: (url: string) => {
- return fetch(new URL(url, backendUrl).href, { headers }).then((r) => {
- if (!r.ok) throw { status: r.status, json: r.json() };
-
- return r.json();
- });
- },
- }}
- >
- {children as any}
- </SWRConfig>
- );
-}
-
-const logger = new Logger("AccountPage");
-
-/**
- * Show only the account's balance. NOTE: the backend state
- * is mostly needed to provide the user's credentials to POST
- * to the bank.
- */
-function Account({ accountLabel }: { accountLabel: string }): VNode {
- const { cache } = useSWRConfig();
-
- // Getting the bank account balance:
- const endpoint = `access-api/accounts/${accountLabel}`;
- const { data, error, mutate } = useSWR(endpoint, {
- // refreshInterval: 0,
- // revalidateIfStale: false,
- // revalidateOnMount: false,
- // revalidateOnFocus: false,
- // revalidateOnReconnect: false,
- });
- const backend = useBackendContext();
- const { pageState, pageStateSetter: setPageState } = usePageContext();
- const { withdrawalId, talerWithdrawUri, timestamp } = pageState;
- const { i18n } = useTranslationContext();
- useEffect(() => {
- mutate();
- }, [timestamp]);
-
- /**
- * This part shows a list of transactions: with 5 elements by
- * default and offers a "load more" button.
- */
- // const [txPageNumber, setTxPageNumber] = useTransactionPageNumber();
- // const txsPages = [];
- // for (let i = 0; i <= txPageNumber; i++) {
- // txsPages.push(<Transactions accountLabel={accountLabel} pageNumber={i} />);
- // }
-
- if (typeof error !== "undefined") {
- logger.error("account error", error, endpoint);
- /**
- * FIXME: to minimize the code, try only one invocation
- * of pageStateSetter, after having decided the error
- * message in the case-branch.
- */
- switch (error.status) {
- case 404: {
- backend.clear();
- setPageState((prevState: PageStateType) => ({
- ...prevState,
-
- error: {
- title: i18n.str`Username or account label '${accountLabel}' not found. Won't login.`,
- },
- }));
-
- /**
- * 404 should never stick to the cache, because they
- * taint successful future registrations. How? After
- * registering, the user gets navigated to this page,
- * therefore a previous 404 on this SWR key (the requested
- * resource) would still appear as valid and cause this
- * page not to be shown! A typical case is an attempted
- * login of a unregistered user X, and then a registration
- * attempt of the same user X: in this case, the failed
- * login would cache a 404 error to X's profile, resulting
- * in the legitimate request after the registration to still
- * be flagged as 404. Clearing the cache should prevent
- * this. */
- (cache as any).clear();
- return <p>Profile not found...</p>;
- }
- case HttpStatusCode.Unauthorized:
- case HttpStatusCode.Forbidden: {
- backend.clear();
- setPageState((prevState: PageStateType) => ({
- ...prevState,
- error: {
- title: i18n.str`Wrong credentials given.`,
- },
- }));
- return <p>Wrong credentials...</p>;
- }
- default: {
- backend.clear();
- setPageState((prevState: PageStateType) => ({
- ...prevState,
- error: {
- title: i18n.str`Account information could not be retrieved.`,
- debug: JSON.stringify(error),
- },
- }));
- return <p>Unknown problem...</p>;
- }
- }
- }
- const balance = !data ? undefined : Amounts.parseOrThrow(data.balance.amount);
- const accountNumber = !data ? undefined : getIbanFromPayto(data.paytoUri);
- const balanceIsDebit = data && data.balance.credit_debit_indicator == "debit";
-
- /**
- * This block shows the withdrawal QR code.
- *
- * A withdrawal operation replaces everything in the page and
- * (ToDo:) starts polling the backend until either the wallet
- * selected a exchange and reserve public key, or a error / abort
- * happened.
- *
- * After reaching one of the above states, the user should be
- * brought to this ("Account") page where they get informed about
- * the outcome.
- */
- if (talerWithdrawUri && withdrawalId) {
- logger.trace("Bank created a new Taler withdrawal");
- return (
- <BankFrame>
- <WithdrawalQRCode
- withdrawalId={withdrawalId}
- talerWithdrawUri={talerWithdrawUri}
- />
- </BankFrame>
- );
- }
- const balanceValue = !balance ? undefined : Amounts.stringifyValue(balance);
-
- return (
- <BankFrame>
- <div>
- <h1 class="nav welcome-text">
- <i18n.Translate>
- Welcome,
- {accountNumber
- ? `${accountLabel} (${accountNumber})`
- : accountLabel}
- !
- </i18n.Translate>
- </h1>
- </div>
- <section id="assets">
- <div class="asset-summary">
- <h2>{i18n.str`Bank account balance`}</h2>
- {!balance ? (
- <div class="large-amount" style={{ color: "gray" }}>
- Waiting server response...
- </div>
- ) : (
- <div class="large-amount amount">
- {balanceIsDebit ? <b>-</b> : null}
- <span class="value">{`${balanceValue}`}</span>&nbsp;
- <span class="currency">{`${balance.currency}`}</span>
- </div>
- )}
- </div>
- </section>
- <section id="payments">
- <div class="payments">
- <h2>{i18n.str`Payments`}</h2>
- <PaymentOptions currency={balance?.currency} />
- </div>
- </section>
- <section id="main">
- <article>
- <h2>{i18n.str`Latest transactions:`}</h2>
- <Transactions
- balanceValue={balanceValue}
- pageNumber={0}
- accountLabel={accountLabel}
- />
- </article>
- </section>
- </BankFrame>
- );
-}
-
-// function useTransactionPageNumber(): [number, StateUpdater<number>] {
-// const ret = hooks.useNotNullLocalStorage("transaction-page", "0");
-// const retObj = JSON.parse(ret[0]);
-// const retSetter: StateUpdater<number> = function (val) {
-// const newVal =
-// val instanceof Function
-// ? JSON.stringify(val(retObj))
-// : JSON.stringify(val);
-// ret[1](newVal);
-// };
-// return [retObj, retSetter];
-// }
diff --git a/packages/demobank-ui/src/pages/home/BankFrame.tsx b/packages/demobank-ui/src/pages/home/BankFrame.tsx
deleted file mode 100644
index 2a0c797f2..000000000
--- a/packages/demobank-ui/src/pages/home/BankFrame.tsx
+++ /dev/null
@@ -1,192 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-import { Logger } from "@gnu-taler/taler-util";
-import { ComponentChildren, Fragment, h, VNode } from "preact";
-import talerLogo from "../../assets/logo-white.svg";
-import { LangSelectorLikePy as LangSelector } from "../../components/menu/LangSelector.js";
-import { useBackendContext } from "../../context/backend.js";
-import { PageStateType, usePageContext } from "../../context/pageState.js";
-import { useTranslationContext } from "../../context/translation.js";
-import { bankUiSettings } from "../../settings.js";
-
-const logger = new Logger("BankFrame");
-
-export function BankFrame({
- children,
-}: {
- children: ComponentChildren;
-}): VNode {
- const { i18n } = useTranslationContext();
- const backend = useBackendContext();
- const { pageState, pageStateSetter } = usePageContext();
- logger.trace("state", pageState);
- const logOut = (
- <div class="logout">
- <a
- href="#"
- class="pure-button logout-button"
- onClick={() => {
- pageStateSetter((prevState: PageStateType) => {
- const { talerWithdrawUri, withdrawalId, ...rest } = prevState;
- backend.clear();
- return {
- ...rest,
- withdrawalInProgress: false,
- error: undefined,
- info: undefined,
- isRawPayto: false,
- };
- });
- }}
- >{i18n.str`Logout`}</a>
- </div>
- );
-
- const demo_sites = [];
- for (const i in bankUiSettings.demoSites)
- demo_sites.push(
- <a href={bankUiSettings.demoSites[i][1]}>
- {bankUiSettings.demoSites[i][0]}
- </a>,
- );
-
- return (
- <Fragment>
- <header
- class="demobar"
- style="display: flex; flex-direction: row; justify-content: space-between;"
- >
- <a href="#main" class="skip">{i18n.str`Skip to main content`}</a>
- <div style="max-width: 50em; margin-left: 2em;">
- <h1>
- <span class="it">
- <a href="/">{bankUiSettings.bankName}</a>
- </span>
- </h1>
- {maybeDemoContent(
- <p>
- <i18n.Translate>
- This part of the demo shows how a bank that supports Taler
- directly would work. In addition to using your own bank account,
- you can also see the transaction history of some{" "}
- <a href="/public-accounts">Public Accounts</a>.
- </i18n.Translate>
- </p>,
- )}
- </div>
- <a href="https://taler.net/">
- <img
- src={talerLogo}
- alt={i18n.str`Taler logo`}
- height="100"
- width="224"
- style="margin: 2em 2em"
- />
- </a>
- </header>
- <div style="display:flex; flex-direction: column;" class="navcontainer">
- <nav class="demolist">
- {maybeDemoContent(<Fragment>{demo_sites}</Fragment>)}
- <div class="right">
- <LangSelector />
- </div>
- </nav>
- </div>
- <section id="main" class="content">
- <ErrorBanner />
- <StatusBanner />
- {backend.state.status === "loggedIn" ? logOut : null}
- {children}
- </section>
- <section id="footer" class="footer">
- <div class="footer">
- <hr />
- <div>
- <p>
- You can learn more about GNU Taler on our{" "}
- <a href="https://taler.net">main website</a>.
- </p>
- </div>
- <div style="flex-grow:1" />
- <p>Copyright &copy; 2014&mdash;2022 Taler Systems SA</p>
- </div>
- </section>
- </Fragment>
- );
-}
-
-function maybeDemoContent(content: VNode): VNode {
- if (bankUiSettings.showDemoNav) {
- return content;
- }
- return <Fragment />;
-}
-
-function ErrorBanner(): VNode | null {
- const { pageState, pageStateSetter } = usePageContext();
-
- if (!pageState.error) return null;
-
- const rval = (
- <div class="informational informational-fail" style={{ marginTop: 8 }}>
- <div style={{ display: "flex", justifyContent: "space-between" }}>
- <p>
- <b>{pageState.error.title}</b>
- </p>
- <div>
- <input
- type="button"
- class="pure-button"
- value="Clear"
- onClick={async () => {
- pageStateSetter((prev) => ({ ...prev, error: undefined }));
- }}
- />
- </div>
- </div>
- <p>{pageState.error.description}</p>
- </div>
- );
- delete pageState.error;
- return rval;
-}
-
-function StatusBanner(): VNode | null {
- const { pageState, pageStateSetter } = usePageContext();
- if (!pageState.info) return null;
-
- const rval = (
- <div class="informational informational-ok" style={{ marginTop: 8 }}>
- <div style={{ display: "flex", justifyContent: "space-between" }}>
- <p>
- <b>{pageState.info}</b>
- </p>
- <div>
- <input
- type="button"
- class="pure-button"
- value="Clear"
- onClick={async () => {
- pageStateSetter((prev) => ({ ...prev, info: undefined }));
- }}
- />
- </div>
- </div>
- </div>
- );
- return rval;
-}
diff --git a/packages/demobank-ui/src/pages/home/LoginForm.tsx b/packages/demobank-ui/src/pages/home/LoginForm.tsx
deleted file mode 100644
index f31f91190..000000000
--- a/packages/demobank-ui/src/pages/home/LoginForm.tsx
+++ /dev/null
@@ -1,139 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-import { h, VNode } from "preact";
-import { route } from "preact-router";
-import { useEffect, useRef, useState } from "preact/hooks";
-import { useBackendContext } from "../../context/backend.js";
-import { useTranslationContext } from "../../context/translation.js";
-import { BackendStateHandler } from "../../hooks/backend.js";
-import { bankUiSettings } from "../../settings.js";
-import { getBankBackendBaseUrl, undefinedIfEmpty } from "../../utils.js";
-import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
-
-/**
- * Collect and submit login data.
- */
-export function LoginForm(): VNode {
- const backend = useBackendContext();
- const [username, setUsername] = useState<string | undefined>();
- const [password, setPassword] = useState<string | undefined>();
- const { i18n } = useTranslationContext();
- const ref = useRef<HTMLInputElement>(null);
- useEffect(() => {
- ref.current?.focus();
- }, []);
-
- const errors = undefinedIfEmpty({
- username: !username ? i18n.str`Missing username` : undefined,
- password: !password ? i18n.str`Missing password` : undefined,
- });
-
- return (
- <div class="login-div">
- <form action="javascript:void(0);" class="login-form" noValidate>
- <div class="pure-form">
- <h2>{i18n.str`Please login!`}</h2>
- <p class="unameFieldLabel loginFieldLabel formFieldLabel">
- <label for="username">{i18n.str`Username:`}</label>
- </p>
- <input
- ref={ref}
- autoFocus
- type="text"
- name="username"
- id="username"
- value={username ?? ""}
- placeholder="Username"
- required
- onInput={(e): void => {
- setUsername(e.currentTarget.value);
- }}
- />
- <ShowInputErrorLabel
- message={errors?.username}
- isDirty={username !== undefined}
- />
- <p class="passFieldLabel loginFieldLabel formFieldLabel">
- <label for="password">{i18n.str`Password:`}</label>
- </p>
- <input
- type="password"
- name="password"
- id="password"
- value={password ?? ""}
- placeholder="Password"
- required
- onInput={(e): void => {
- setPassword(e.currentTarget.value);
- }}
- />
- <ShowInputErrorLabel
- message={errors?.password}
- isDirty={password !== undefined}
- />
- <br />
- <button
- type="submit"
- class="pure-button pure-button-primary"
- disabled={!!errors}
- onClick={() => {
- if (!username || !password) return;
- loginCall({ username, password }, backend);
- setUsername(undefined);
- setPassword(undefined);
- }}
- >
- {i18n.str`Login`}
- </button>
-
- {bankUiSettings.allowRegistrations ? (
- <button
- class="pure-button pure-button-secondary btn-cancel"
- onClick={() => {
- route("/register");
- }}
- >
- {i18n.str`Register`}
- </button>
- ) : (
- <div />
- )}
- </div>
- </form>
- </div>
- );
-}
-
-async function loginCall(
- req: { username: string; password: string },
- /**
- * FIXME: figure out if the two following
- * functions can be retrieved from the state.
- */
- backend: BackendStateHandler,
-): Promise<void> {
- /**
- * Optimistically setting the state as 'logged in', and
- * let the Account component request the balance to check
- * whether the credentials are valid. */
-
- backend.save({
- url: getBankBackendBaseUrl(),
- username: req.username,
- password: req.password,
- });
-}
diff --git a/packages/demobank-ui/src/pages/home/PaymentOptions.tsx b/packages/demobank-ui/src/pages/home/PaymentOptions.tsx
deleted file mode 100644
index 69c8d383e..000000000
--- a/packages/demobank-ui/src/pages/home/PaymentOptions.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-import { h, VNode } from "preact";
-import { useState } from "preact/hooks";
-import { useTranslationContext } from "../../context/translation.js";
-import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js";
-import { WalletWithdrawForm } from "./WalletWithdrawForm.js";
-
-/**
- * Let the user choose a payment option,
- * then specify the details trigger the action.
- */
-export function PaymentOptions({ currency }: { currency?: string }): VNode {
- const { i18n } = useTranslationContext();
-
- const [tab, setTab] = useState<"charge-wallet" | "wire-transfer">(
- "charge-wallet",
- );
-
- return (
- <article>
- <div class="payments">
- <div class="tab">
- <button
- class={tab === "charge-wallet" ? "tablinks active" : "tablinks"}
- onClick={(): void => {
- setTab("charge-wallet");
- }}
- >
- {i18n.str`Obtain digital cash`}
- </button>
- <button
- class={tab === "wire-transfer" ? "tablinks active" : "tablinks"}
- onClick={(): void => {
- setTab("wire-transfer");
- }}
- >
- {i18n.str`Transfer to bank account`}
- </button>
- </div>
- {tab === "charge-wallet" && (
- <div id="charge-wallet" class="tabcontent active">
- <h3>{i18n.str`Obtain digital cash`}</h3>
- <WalletWithdrawForm focus currency={currency} />
- </div>
- )}
- {tab === "wire-transfer" && (
- <div id="wire-transfer" class="tabcontent active">
- <h3>{i18n.str`Transfer to bank account`}</h3>
- <PaytoWireTransferForm focus currency={currency} />
- </div>
- )}
- </div>
- </article>
- );
-}
diff --git a/packages/demobank-ui/src/pages/home/PaytoWireTransferForm.tsx b/packages/demobank-ui/src/pages/home/PaytoWireTransferForm.tsx
deleted file mode 100644
index bfb2f2fef..000000000
--- a/packages/demobank-ui/src/pages/home/PaytoWireTransferForm.tsx
+++ /dev/null
@@ -1,424 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-import { Amounts, Logger, parsePaytoUri } from "@gnu-taler/taler-util";
-import { hooks } from "@gnu-taler/web-util/lib/index.browser";
-import { h, VNode } from "preact";
-import { StateUpdater, useEffect, useRef, useState } from "preact/hooks";
-import { useBackendContext } from "../../context/backend.js";
-import { PageStateType, usePageContext } from "../../context/pageState.js";
-import { useTranslationContext } from "../../context/translation.js";
-import { BackendState } from "../../hooks/backend.js";
-import { prepareHeaders, undefinedIfEmpty } from "../../utils.js";
-import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
-
-const logger = new Logger("PaytoWireTransferForm");
-
-export function PaytoWireTransferForm({
- focus,
- currency,
-}: {
- focus?: boolean;
- currency?: string;
-}): VNode {
- const backend = useBackendContext();
- const { pageState, pageStateSetter } = usePageContext(); // NOTE: used for go-back button?
-
- const [submitData, submitDataSetter] = useWireTransferRequestType();
-
- const [rawPaytoInput, rawPaytoInputSetter] = useState<string | undefined>(
- undefined,
- );
- const { i18n } = useTranslationContext();
- const ibanRegex = "^[A-Z][A-Z][0-9]+$";
- let transactionData: TransactionRequestType;
- const ref = useRef<HTMLInputElement>(null);
- useEffect(() => {
- if (focus) ref.current?.focus();
- }, [focus, pageState.isRawPayto]);
-
- let parsedAmount = undefined;
-
- const errorsWire = {
- iban: !submitData?.iban
- ? i18n.str`Missing IBAN`
- : !/^[A-Z0-9]*$/.test(submitData.iban)
- ? i18n.str`IBAN should have just uppercased letters and numbers`
- : undefined,
- subject: !submitData?.subject ? i18n.str`Missing subject` : undefined,
- amount: !submitData?.amount
- ? i18n.str`Missing amount`
- : !(parsedAmount = Amounts.parse(`${currency}:${submitData.amount}`))
- ? i18n.str`Amount is not valid`
- : Amounts.isZero(parsedAmount)
- ? i18n.str`Should be greater than 0`
- : undefined,
- };
-
- if (!pageState.isRawPayto)
- return (
- <div>
- <form class="pure-form" name="wire-transfer-form">
- <p>
- <label for="iban">{i18n.str`Receiver IBAN:`}</label>&nbsp;
- <input
- ref={ref}
- type="text"
- id="iban"
- name="iban"
- value={submitData?.iban ?? ""}
- placeholder="CC0123456789"
- required
- pattern={ibanRegex}
- onInput={(e): void => {
- submitDataSetter((submitData) => ({
- ...submitData,
- iban: e.currentTarget.value,
- }));
- }}
- />
- <br />
- <ShowInputErrorLabel
- message={errorsWire?.iban}
- isDirty={submitData?.iban !== undefined}
- />
- <br />
- <label for="subject">{i18n.str`Transfer subject:`}</label>&nbsp;
- <input
- type="text"
- name="subject"
- id="subject"
- placeholder="subject"
- value={submitData?.subject ?? ""}
- required
- onInput={(e): void => {
- submitDataSetter((submitData) => ({
- ...submitData,
- subject: e.currentTarget.value,
- }));
- }}
- />
- <br />
- <ShowInputErrorLabel
- message={errorsWire?.subject}
- isDirty={submitData?.subject !== undefined}
- />
- <br />
- <label for="amount">{i18n.str`Amount:`}</label>&nbsp;
- <input
- type="text"
- readonly
- class="currency-indicator"
- size={currency?.length}
- maxLength={currency?.length}
- tabIndex={-1}
- value={currency}
- />
- &nbsp;
- <input
- type="number"
- name="amount"
- id="amount"
- placeholder="amount"
- required
- value={submitData?.amount ?? ""}
- onInput={(e): void => {
- submitDataSetter((submitData) => ({
- ...submitData,
- amount: e.currentTarget.value,
- }));
- }}
- />
- <ShowInputErrorLabel
- message={errorsWire?.amount}
- isDirty={submitData?.amount !== undefined}
- />
- </p>
-
- <p style={{ display: "flex", justifyContent: "space-between" }}>
- <input
- type="submit"
- class="pure-button pure-button-primary"
- disabled={!!errorsWire}
- value="Send"
- onClick={async () => {
- if (
- typeof submitData === "undefined" ||
- typeof submitData.iban === "undefined" ||
- submitData.iban === "" ||
- typeof submitData.subject === "undefined" ||
- submitData.subject === "" ||
- typeof submitData.amount === "undefined" ||
- submitData.amount === ""
- ) {
- logger.error("Not all the fields were given.");
- pageStateSetter((prevState: PageStateType) => ({
- ...prevState,
-
- error: {
- title: i18n.str`Field(s) missing.`,
- },
- }));
- return;
- }
- transactionData = {
- paytoUri: `payto://iban/${
- submitData.iban
- }?message=${encodeURIComponent(submitData.subject)}`,
- amount: `${currency}:${submitData.amount}`,
- };
- return await createTransactionCall(
- transactionData,
- backend.state,
- pageStateSetter,
- () =>
- submitDataSetter((p) => ({
- amount: undefined,
- iban: undefined,
- subject: undefined,
- })),
- );
- }}
- />
- <input
- type="button"
- class="pure-button"
- value="Clear"
- onClick={async () => {
- submitDataSetter((p) => ({
- amount: undefined,
- iban: undefined,
- subject: undefined,
- }));
- }}
- />
- </p>
- </form>
- <p>
- <a
- href="/account"
- onClick={() => {
- logger.trace("switch to raw payto form");
- pageStateSetter((prevState) => ({
- ...prevState,
- isRawPayto: true,
- }));
- }}
- >
- {i18n.str`Want to try the raw payto://-format?`}
- </a>
- </p>
- </div>
- );
-
- const errorsPayto = undefinedIfEmpty({
- rawPaytoInput: !rawPaytoInput
- ? i18n.str`Missing payto address`
- : !parsePaytoUri(rawPaytoInput)
- ? i18n.str`Payto does not follow the pattern`
- : undefined,
- });
-
- return (
- <div>
- <p>{i18n.str`Transfer money to account identified by payto:// URI:`}</p>
- <div class="pure-form" name="payto-form">
- <p>
- <label for="address">{i18n.str`payto URI:`}</label>&nbsp;
- <input
- name="address"
- type="text"
- size={50}
- ref={ref}
- id="address"
- value={rawPaytoInput ?? ""}
- required
- placeholder={i18n.str`payto address`}
- // pattern={`payto://iban/[A-Z][A-Z][0-9]+?message=[a-zA-Z0-9 ]+&amount=${currency}:[0-9]+(.[0-9]+)?`}
- onInput={(e): void => {
- rawPaytoInputSetter(e.currentTarget.value);
- }}
- />
- <ShowInputErrorLabel
- message={errorsPayto?.rawPaytoInput}
- isDirty={rawPaytoInput !== undefined}
- />
- <br />
- <div class="hint">
- Hint:
- <code>
- payto://iban/[receiver-iban]?message=[subject]&amount=[{currency}
- :X.Y]
- </code>
- </div>
- </p>
- <p>
- <input
- class="pure-button pure-button-primary"
- type="submit"
- disabled={!!errorsPayto}
- value={i18n.str`Send`}
- onClick={async () => {
- // empty string evaluates to false.
- if (!rawPaytoInput) {
- logger.error("Didn't get any raw Payto string!");
- return;
- }
- transactionData = { paytoUri: rawPaytoInput };
- if (
- typeof transactionData.paytoUri === "undefined" ||
- transactionData.paytoUri.length === 0
- )
- return;
-
- return await createTransactionCall(
- transactionData,
- backend.state,
- pageStateSetter,
- () => rawPaytoInputSetter(undefined),
- );
- }}
- />
- </p>
- <p>
- <a
- href="/account"
- onClick={() => {
- logger.trace("switch to wire-transfer-form");
- pageStateSetter((prevState) => ({
- ...prevState,
- isRawPayto: false,
- }));
- }}
- >
- {i18n.str`Use wire-transfer form?`}
- </a>
- </p>
- </div>
- </div>
- );
-}
-
-/**
- * Stores in the state a object representing a wire transfer,
- * in order to avoid losing the handle of the data entered by
- * the user in <input> fields. FIXME: name not matching the
- * purpose, as this is not a HTTP request body but rather the
- * state of the <input>-elements.
- */
-type WireTransferRequestTypeOpt = WireTransferRequestType | undefined;
-function useWireTransferRequestType(
- state?: WireTransferRequestType,
-): [WireTransferRequestTypeOpt, StateUpdater<WireTransferRequestTypeOpt>] {
- const ret = hooks.useLocalStorage(
- "wire-transfer-request-state",
- JSON.stringify(state),
- );
- const retObj: WireTransferRequestTypeOpt = ret[0]
- ? JSON.parse(ret[0])
- : ret[0];
- const retSetter: StateUpdater<WireTransferRequestTypeOpt> = function (val) {
- const newVal =
- val instanceof Function
- ? JSON.stringify(val(retObj))
- : JSON.stringify(val);
- ret[1](newVal);
- };
- return [retObj, retSetter];
-}
-
-/**
- * This function creates a new transaction. It reads a Payto
- * address entered by the user and POSTs it to the bank. No
- * sanity-check of the input happens before the POST as this is
- * already conducted by the backend.
- */
-async function createTransactionCall(
- req: TransactionRequestType,
- backendState: BackendState,
- pageStateSetter: StateUpdater<PageStateType>,
- /**
- * Optional since the raw payto form doesn't have
- * a stateful management of the input data yet.
- */
- cleanUpForm: () => void,
-): Promise<void> {
- if (backendState.status === "loggedOut") {
- logger.error("No credentials found.");
- pageStateSetter((prevState) => ({
- ...prevState,
-
- error: {
- title: "No credentials found.",
- },
- }));
- return;
- }
- let res: Response;
- try {
- const { username, password } = backendState;
- const headers = prepareHeaders(username, password);
- const url = new URL(
- `access-api/accounts/${backendState.username}/transactions`,
- backendState.url,
- );
- res = await fetch(url.href, {
- method: "POST",
- headers,
- body: JSON.stringify(req),
- });
- } catch (error) {
- logger.error("Could not POST transaction request to the bank", error);
- pageStateSetter((prevState) => ({
- ...prevState,
-
- error: {
- title: `Could not create the wire transfer`,
- description: (error as any).error.description,
- debug: JSON.stringify(error),
- },
- }));
- return;
- }
- // POST happened, status not sure yet.
- if (!res.ok) {
- const response = await res.json();
- logger.error(
- `Transfer creation gave response error: ${response} (${res.status})`,
- );
- pageStateSetter((prevState) => ({
- ...prevState,
-
- error: {
- title: `Transfer creation gave response error`,
- description: response.error.description,
- debug: JSON.stringify(response),
- },
- }));
- return;
- }
- // status is 200 OK here, tell the user.
- logger.trace("Wire transfer created!");
- pageStateSetter((prevState) => ({
- ...prevState,
-
- info: "Wire transfer created!",
- }));
-
- // Only at this point the input data can
- // be discarded.
- cleanUpForm();
-}
diff --git a/packages/demobank-ui/src/pages/home/PublicHistoriesPage.tsx b/packages/demobank-ui/src/pages/home/PublicHistoriesPage.tsx
deleted file mode 100644
index a0fb8493b..000000000
--- a/packages/demobank-ui/src/pages/home/PublicHistoriesPage.tsx
+++ /dev/null
@@ -1,185 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-import { Logger } from "@gnu-taler/taler-util";
-import { hooks } from "@gnu-taler/web-util/lib/index.browser";
-import { ComponentChildren, Fragment, h, VNode } from "preact";
-import { route } from "preact-router";
-import { StateUpdater } from "preact/hooks";
-import useSWR, { SWRConfig } from "swr";
-import { PageStateType, usePageContext } from "../../context/pageState.js";
-import { useTranslationContext } from "../../context/translation.js";
-import { getBankBackendBaseUrl } from "../../utils.js";
-import { BankFrame } from "./BankFrame.js";
-import { Transactions } from "./Transactions.js";
-
-const logger = new Logger("PublicHistoriesPage");
-
-export function PublicHistoriesPage(): VNode {
- return (
- <SWRWithoutCredentials baseUrl={getBankBackendBaseUrl()}>
- <BankFrame>
- <PublicHistories />
- </BankFrame>
- </SWRWithoutCredentials>
- );
-}
-
-function SWRWithoutCredentials({
- baseUrl,
- children,
-}: {
- children: ComponentChildren;
- baseUrl: string;
-}): VNode {
- logger.trace("Base URL", baseUrl);
- return (
- <SWRConfig
- value={{
- fetcher: (url: string) =>
- fetch(baseUrl + url || "").then((r) => {
- if (!r.ok) throw { status: r.status, json: r.json() };
-
- return r.json();
- }),
- }}
- >
- {children as any}
- </SWRConfig>
- );
-}
-
-/**
- * Show histories of public accounts.
- */
-function PublicHistories(): VNode {
- const { pageState, pageStateSetter } = usePageContext();
- const [showAccount, setShowAccount] = useShowPublicAccount();
- const { data, error } = useSWR("access-api/public-accounts");
- const { i18n } = useTranslationContext();
-
- if (typeof error !== "undefined") {
- switch (error.status) {
- case 404:
- logger.error("public accounts: 404", error);
- route("/account");
- pageStateSetter((prevState: PageStateType) => ({
- ...prevState,
-
- error: {
- title: i18n.str`List of public accounts was not found.`,
- debug: JSON.stringify(error),
- },
- }));
- break;
- default:
- logger.error("public accounts: non-404 error", error);
- route("/account");
- pageStateSetter((prevState: PageStateType) => ({
- ...prevState,
-
- error: {
- title: i18n.str`List of public accounts could not be retrieved.`,
- debug: JSON.stringify(error),
- },
- }));
- break;
- }
- }
- if (!data) return <p>Waiting public accounts list...</p>;
- const txs: Record<string, h.JSX.Element> = {};
- const accountsBar = [];
-
- /**
- * Show the account specified in the props, or just one
- * from the list if that's not given.
- */
- if (typeof showAccount === "undefined" && data.publicAccounts.length > 0) {
- setShowAccount(data.publicAccounts[1].accountLabel);
- }
- logger.trace(`Public history tab: ${showAccount}`);
-
- // Ask story of all the public accounts.
- for (const account of data.publicAccounts) {
- logger.trace("Asking transactions for", account.accountLabel);
- const isSelected = account.accountLabel == showAccount;
- accountsBar.push(
- <li
- class={
- isSelected
- ? "pure-menu-selected pure-menu-item"
- : "pure-menu-item pure-menu"
- }
- >
- <a
- href="#"
- class="pure-menu-link"
- onClick={() => setShowAccount(account.accountLabel)}
- >
- {account.accountLabel}
- </a>
- </li>,
- );
- txs[account.accountLabel] = (
- <Transactions accountLabel={account.accountLabel} pageNumber={0} />
- );
- }
-
- return (
- <Fragment>
- <h1 class="nav">{i18n.str`History of public accounts`}</h1>
- <section id="main">
- <article>
- <div class="pure-menu pure-menu-horizontal" name="accountMenu">
- <ul class="pure-menu-list">{accountsBar}</ul>
- {typeof showAccount !== "undefined" ? (
- txs[showAccount]
- ) : (
- <p>No public transactions found.</p>
- )}
- <br />
- <a href="/account" class="pure-button">
- Go back
- </a>
- </div>
- </article>
- </section>
- </Fragment>
- );
-}
-
-/**
- * Stores in the state a object containing a 'username'
- * and 'password' field, in order to avoid losing the
- * handle of the data entered by the user in <input> fields.
- */
-function useShowPublicAccount(
- state?: string,
-): [string | undefined, StateUpdater<string | undefined>] {
- const ret = hooks.useLocalStorage(
- "show-public-account",
- JSON.stringify(state),
- );
- const retObj: string | undefined = ret[0] ? JSON.parse(ret[0]) : ret[0];
- const retSetter: StateUpdater<string | undefined> = function (val) {
- const newVal =
- val instanceof Function
- ? JSON.stringify(val(retObj))
- : JSON.stringify(val);
- ret[1](newVal);
- };
- return [retObj, retSetter];
-}
diff --git a/packages/demobank-ui/src/pages/home/QrCodeSection.tsx b/packages/demobank-ui/src/pages/home/QrCodeSection.tsx
deleted file mode 100644
index 7c262fdc6..000000000
--- a/packages/demobank-ui/src/pages/home/QrCodeSection.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-import { h, VNode } from "preact";
-import { useEffect } from "preact/hooks";
-import { QR } from "../../components/QR.js";
-import { useTranslationContext } from "../../context/translation.js";
-
-export function QrCodeSection({
- talerWithdrawUri,
- abortButton,
-}: {
- talerWithdrawUri: string;
- abortButton: h.JSX.Element;
-}): VNode {
- const { i18n } = useTranslationContext();
- useEffect(() => {
- //Taler Wallet WebExtension is listening to headers response and tab updates.
- //In the SPA there is no header response with the Taler URI so
- //this hack manually triggers the tab update after the QR is in the DOM.
- // WebExtension will be using
- // https://developer.chrome.com/docs/extensions/reference/tabs/#event-onUpdated
- document.title = `${document.title} ${talerWithdrawUri}`;
- }, []);
-
- return (
- <section id="main" class="content">
- <h1 class="nav">{i18n.str`Transfer to Taler Wallet`}</h1>
- <article>
- <div class="qr-div">
- <p>{i18n.str`Use this QR code to withdraw to your mobile wallet:`}</p>
- {QR({ text: talerWithdrawUri })}
- <p>
- Click{" "}
- <a id="linkqr" href={talerWithdrawUri}>{i18n.str`this link`}</a> to
- open your Taler wallet!
- </p>
- <br />
- {abortButton}
- </div>
- </article>
- </section>
- );
-}
diff --git a/packages/demobank-ui/src/pages/home/RegistrationPage.tsx b/packages/demobank-ui/src/pages/home/RegistrationPage.tsx
deleted file mode 100644
index c91eef7a0..000000000
--- a/packages/demobank-ui/src/pages/home/RegistrationPage.tsx
+++ /dev/null
@@ -1,247 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-import { Logger } from "@gnu-taler/taler-util";
-import { Fragment, h, VNode } from "preact";
-import { route } from "preact-router";
-import { StateUpdater, useState } from "preact/hooks";
-import { useBackendContext } from "../../context/backend.js";
-import { PageStateType, usePageContext } from "../../context/pageState.js";
-import { useTranslationContext } from "../../context/translation.js";
-import { BackendStateHandler } from "../../hooks/backend.js";
-import { bankUiSettings } from "../../settings.js";
-import { getBankBackendBaseUrl, undefinedIfEmpty } from "../../utils.js";
-import { BankFrame } from "./BankFrame.js";
-import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
-
-const logger = new Logger("RegistrationPage");
-
-export function RegistrationPage(): VNode {
- const { i18n } = useTranslationContext();
- if (!bankUiSettings.allowRegistrations) {
- return (
- <BankFrame>
- <p>{i18n.str`Currently, the bank is not accepting new registrations!`}</p>
- </BankFrame>
- );
- }
- return (
- <BankFrame>
- <RegistrationForm />
- </BankFrame>
- );
-}
-
-/**
- * Collect and submit registration data.
- */
-function RegistrationForm(): VNode {
- const backend = useBackendContext();
- const { pageState, pageStateSetter } = usePageContext();
- const [username, setUsername] = useState<string | undefined>();
- const [password, setPassword] = useState<string | undefined>();
- const [repeatPassword, setRepeatPassword] = useState<string | undefined>();
-
- const { i18n } = useTranslationContext();
-
- const errors = undefinedIfEmpty({
- username: !username ? i18n.str`Missing username` : undefined,
- password: !password ? i18n.str`Missing password` : undefined,
- repeatPassword: !repeatPassword
- ? i18n.str`Missing password`
- : repeatPassword !== password
- ? i18n.str`Password don't match`
- : undefined,
- });
-
- return (
- <Fragment>
- <h1 class="nav">{i18n.str`Welcome to ${bankUiSettings.bankName}!`}</h1>
- <article>
- <div class="register-div">
- <form action="javascript:void(0);" class="register-form" noValidate>
- <div class="pure-form">
- <h2>{i18n.str`Please register!`}</h2>
- <p class="unameFieldLabel registerFieldLabel formFieldLabel">
- <label for="register-un">{i18n.str`Username:`}</label>
- </p>
- <input
- id="register-un"
- name="register-un"
- type="text"
- placeholder="Username"
- value={username ?? ""}
- onInput={(e): void => {
- setUsername(e.currentTarget.value);
- }}
- />
- <ShowInputErrorLabel
- message={errors?.username}
- isDirty={username !== undefined}
- />
- <p class="unameFieldLabel registerFieldLabel formFieldLabel">
- <label for="register-pw">{i18n.str`Password:`}</label>
- </p>
- <input
- type="password"
- name="register-pw"
- id="register-pw"
- placeholder="Password"
- value={password ?? ""}
- required
- onInput={(e): void => {
- setPassword(e.currentTarget.value);
- }}
- />
- <ShowInputErrorLabel
- message={errors?.password}
- isDirty={password !== undefined}
- />
- <p class="unameFieldLabel registerFieldLabel formFieldLabel">
- <label for="register-repeat">{i18n.str`Repeat Password:`}</label>
- </p>
- <input
- type="password"
- style={{ marginBottom: 8 }}
- name="register-repeat"
- id="register-repeat"
- placeholder="Same password"
- value={repeatPassword ?? ""}
- required
- onInput={(e): void => {
- setRepeatPassword(e.currentTarget.value);
- }}
- />
- <ShowInputErrorLabel
- message={errors?.repeatPassword}
- isDirty={repeatPassword !== undefined}
- />
- <br />
- <button
- class="pure-button pure-button-primary btn-register"
- disabled={!!errors}
- onClick={() => {
- if (!username || !password) return;
- registrationCall(
- { username, password },
- backend, // will store BE URL, if OK.
- pageStateSetter,
- );
-
- setUsername(undefined);
- setPassword(undefined);
- setRepeatPassword(undefined);
- }}
- >
- {i18n.str`Register`}
- </button>
- {/* FIXME: should use a different color */}
- <button
- class="pure-button pure-button-secondary btn-cancel"
- onClick={() => {
- setUsername(undefined);
- setPassword(undefined);
- setRepeatPassword(undefined);
- route("/account");
- }}
- >
- {i18n.str`Cancel`}
- </button>
- </div>
- </form>
- </div>
- </article>
- </Fragment>
- );
-}
-
-/**
- * This function requests /register.
- *
- * This function is responsible to change two states:
- * the backend's (to store the login credentials) and
- * the page's (to indicate a successful login or a problem).
- */
-async function registrationCall(
- req: { username: string; password: string },
- /**
- * FIXME: figure out if the two following
- * functions can be retrieved somewhat from
- * the state.
- */
- backend: BackendStateHandler,
- pageStateSetter: StateUpdater<PageStateType>,
-): Promise<void> {
- const url = getBankBackendBaseUrl();
-
- const headers = new Headers();
- headers.append("Content-Type", "application/json");
- const registerEndpoint = new URL("access-api/testing/register", url);
- let res: Response;
- try {
- res = await fetch(registerEndpoint.href, {
- method: "POST",
- body: JSON.stringify({
- username: req.username,
- password: req.password,
- }),
- headers,
- });
- } catch (error) {
- logger.error(
- `Could not POST new registration to the bank (${registerEndpoint.href})`,
- error,
- );
- pageStateSetter((prevState) => ({
- ...prevState,
-
- error: {
- title: `Registration failed, please report`,
- debug: JSON.stringify(error),
- },
- }));
- return;
- }
- if (!res.ok) {
- const response = await res.json();
- if (res.status === 409) {
- pageStateSetter((prevState) => ({
- ...prevState,
-
- error: {
- title: `That username is already taken`,
- debug: JSON.stringify(response),
- },
- }));
- } else {
- pageStateSetter((prevState) => ({
- ...prevState,
-
- error: {
- title: `New registration gave response error`,
- debug: JSON.stringify(response),
- },
- }));
- }
- } else {
- // registration was ok
- backend.save({
- url,
- username: req.username,
- password: req.password,
- });
- route("/account");
- }
-}
diff --git a/packages/demobank-ui/src/pages/home/Transactions.tsx b/packages/demobank-ui/src/pages/home/Transactions.tsx
deleted file mode 100644
index 295bfe0e6..000000000
--- a/packages/demobank-ui/src/pages/home/Transactions.tsx
+++ /dev/null
@@ -1,90 +0,0 @@
-import { Logger } from "@gnu-taler/taler-util";
-import { h, VNode } from "preact";
-import { useEffect } from "preact/hooks";
-import useSWR from "swr";
-import { useTranslationContext } from "../../context/translation.js";
-
-const logger = new Logger("Transactions");
-/**
- * Show one page of transactions.
- */
-export function Transactions({
- pageNumber,
- accountLabel,
- balanceValue,
-}: {
- pageNumber: number;
- accountLabel: string;
- balanceValue?: string;
-}): VNode {
- const { i18n } = useTranslationContext();
- const { data, error, mutate } = useSWR(
- `access-api/accounts/${accountLabel}/transactions?page=${pageNumber}`,
- );
- useEffect(() => {
- if (balanceValue) {
- mutate();
- }
- }, [balanceValue ?? ""]);
- if (typeof error !== "undefined") {
- logger.error("transactions not found error", error);
- switch (error.status) {
- case 404: {
- return <p>Transactions page {pageNumber} was not found.</p>;
- }
- case 401: {
- return <p>Wrong credentials given.</p>;
- }
- default: {
- return <p>Transaction page {pageNumber} could not be retrieved.</p>;
- }
- }
- }
- if (!data) {
- logger.trace(`History data of ${accountLabel} not arrived`);
- return <p>Transactions page loading...</p>;
- }
- logger.trace(`History data of ${accountLabel}`, data);
- return (
- <div class="results">
- <table class="pure-table pure-table-striped">
- <thead>
- <tr>
- <th>{i18n.str`Date`}</th>
- <th>{i18n.str`Amount`}</th>
- <th>{i18n.str`Counterpart`}</th>
- <th>{i18n.str`Subject`}</th>
- </tr>
- </thead>
- <tbody>
- {data.transactions.map((item: any, idx: number) => {
- const sign = item.direction == "DBIT" ? "-" : "";
- const counterpart =
- item.direction == "DBIT" ? item.creditorIban : item.debtorIban;
- // Pattern:
- //
- // DD/MM YYYY subject -5 EUR
- // DD/MM YYYY subject 5 EUR
- const dateRegex = /^([0-9]{4})-([0-9]{2})-([0-9]{1,2})/;
- const dateParse = dateRegex.exec(item.date);
- const date =
- dateParse !== null
- ? `${dateParse[3]}/${dateParse[2]} ${dateParse[1]}`
- : "date not found";
- return (
- <tr key={idx}>
- <td>{date}</td>
- <td>
- {sign}
- {item.amount} {item.currency}
- </td>
- <td>{counterpart}</td>
- <td>{item.subject}</td>
- </tr>
- );
- })}
- </tbody>
- </table>
- </div>
- );
-}
diff --git a/packages/demobank-ui/src/pages/home/WalletWithdrawForm.tsx b/packages/demobank-ui/src/pages/home/WalletWithdrawForm.tsx
deleted file mode 100644
index 29fc1eb6a..000000000
--- a/packages/demobank-ui/src/pages/home/WalletWithdrawForm.tsx
+++ /dev/null
@@ -1,180 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-import { Logger } from "@gnu-taler/taler-util";
-import { h, VNode } from "preact";
-import { StateUpdater, useEffect, useRef } from "preact/hooks";
-import { useBackendContext } from "../../context/backend.js";
-import { PageStateType, usePageContext } from "../../context/pageState.js";
-import { useTranslationContext } from "../../context/translation.js";
-import { BackendState } from "../../hooks/backend.js";
-import { prepareHeaders, validateAmount } from "../../utils.js";
-
-const logger = new Logger("WalletWithdrawForm");
-
-export function WalletWithdrawForm({
- focus,
- currency,
-}: {
- currency?: string;
- focus?: boolean;
-}): VNode {
- const backend = useBackendContext();
- const { pageState, pageStateSetter } = usePageContext();
- const { i18n } = useTranslationContext();
- let submitAmount: string | undefined = "5.00";
-
- const ref = useRef<HTMLInputElement>(null);
- useEffect(() => {
- if (focus) ref.current?.focus();
- }, [focus]);
- return (
- <form id="reserve-form" class="pure-form" name="tform">
- <p>
- <label for="withdraw-amount">{i18n.str`Amount to withdraw:`}</label>
- &nbsp;
- <input
- type="text"
- readonly
- class="currency-indicator"
- size={currency?.length ?? 5}
- maxLength={currency?.length}
- tabIndex={-1}
- value={currency}
- />
- &nbsp;
- <input
- type="number"
- ref={ref}
- id="withdraw-amount"
- name="withdraw-amount"
- value={submitAmount}
- onChange={(e): void => {
- // FIXME: validate using 'parseAmount()',
- // deactivate submit button as long as
- // amount is not valid
- submitAmount = e.currentTarget.value;
- }}
- />
- </p>
- <p>
- <div>
- <input
- id="select-exchange"
- class="pure-button pure-button-primary"
- type="submit"
- value={i18n.str`Withdraw`}
- onClick={() => {
- submitAmount = validateAmount(submitAmount);
- /**
- * By invalid amounts, the validator prints error messages
- * on the console, and the browser colourizes the amount input
- * box to indicate a error.
- */
- if (!submitAmount && currency) return;
- createWithdrawalCall(
- `${currency}:${submitAmount}`,
- backend.state,
- pageStateSetter,
- );
- }}
- />
- </div>
- </p>
- </form>
- );
-}
-
-/**
- * This function creates a withdrawal operation via the Access API.
- *
- * After having successfully created the withdrawal operation, the
- * user should receive a QR code of the "taler://withdraw/" type and
- * supposed to scan it with their phone.
- *
- * TODO: (1) after the scan, the page should refresh itself and inform
- * the user about the operation's outcome. (2) use POST helper. */
-async function createWithdrawalCall(
- amount: string,
- backendState: BackendState,
- pageStateSetter: StateUpdater<PageStateType>,
-): Promise<void> {
- if (backendState?.status === "loggedOut") {
- logger.error("Page has a problem: no credentials found in the state.");
- pageStateSetter((prevState) => ({
- ...prevState,
-
- error: {
- title: "No credentials given.",
- },
- }));
- return;
- }
-
- let res: Response;
- try {
- const { username, password } = backendState;
- const headers = prepareHeaders(username, password);
-
- // Let bank generate withdraw URI:
- const url = new URL(
- `access-api/accounts/${backendState.username}/withdrawals`,
- backendState.url,
- );
- res = await fetch(url.href, {
- method: "POST",
- headers,
- body: JSON.stringify({ amount }),
- });
- } catch (error) {
- logger.trace("Could not POST withdrawal request to the bank", error);
- pageStateSetter((prevState) => ({
- ...prevState,
-
- error: {
- title: `Could not create withdrawal operation`,
- description: (error as any).error.description,
- debug: JSON.stringify(error),
- },
- }));
- return;
- }
- if (!res.ok) {
- const response = await res.json();
- logger.error(
- `Withdrawal creation gave response error: ${response} (${res.status})`,
- );
- pageStateSetter((prevState) => ({
- ...prevState,
-
- error: {
- title: `Withdrawal creation gave response error`,
- description: response.error.description,
- debug: JSON.stringify(response),
- },
- }));
- return;
- }
-
- logger.trace("Withdrawal operation created!");
- const resp = await res.json();
- pageStateSetter((prevState: PageStateType) => ({
- ...prevState,
- withdrawalInProgress: true,
- talerWithdrawUri: resp.taler_withdraw_uri,
- withdrawalId: resp.withdrawal_id,
- }));
-}
diff --git a/packages/demobank-ui/src/pages/home/WithdrawalConfirmationQuestion.tsx b/packages/demobank-ui/src/pages/home/WithdrawalConfirmationQuestion.tsx
deleted file mode 100644
index 3dbe8708e..000000000
--- a/packages/demobank-ui/src/pages/home/WithdrawalConfirmationQuestion.tsx
+++ /dev/null
@@ -1,304 +0,0 @@
-import { Logger } from "@gnu-taler/taler-util";
-import { Fragment, h, VNode } from "preact";
-import { StateUpdater } from "preact/hooks";
-import { useBackendContext } from "../../context/backend.js";
-import { PageStateType, usePageContext } from "../../context/pageState.js";
-import { useTranslationContext } from "../../context/translation.js";
-import { BackendState } from "../../hooks/backend.js";
-import { prepareHeaders } from "../../utils.js";
-
-const logger = new Logger("WithdrawalConfirmationQuestion");
-
-/**
- * Additional authentication required to complete the operation.
- * Not providing a back button, only abort.
- */
-export function WithdrawalConfirmationQuestion(): VNode {
- const { pageState, pageStateSetter } = usePageContext();
- const backend = useBackendContext();
- const { i18n } = useTranslationContext();
- const captchaNumbers = {
- a: Math.floor(Math.random() * 10),
- b: Math.floor(Math.random() * 10),
- };
- let captchaAnswer = "";
-
- return (
- <Fragment>
- <h1 class="nav">{i18n.str`Confirm Withdrawal`}</h1>
- <article>
- <div class="challenge-div">
- <form class="challenge-form" noValidate>
- <div class="pure-form" id="captcha" name="capcha-form">
- <h2>{i18n.str`Authorize withdrawal by solving challenge`}</h2>
- <p>
- <label for="answer">
- {i18n.str`What is`}&nbsp;
- <em>
- {captchaNumbers.a}&nbsp;+&nbsp;{captchaNumbers.b}
- </em>
- ?&nbsp;
- </label>
- &nbsp;
- <input
- name="answer"
- id="answer"
- type="text"
- autoFocus
- required
- onInput={(e): void => {
- captchaAnswer = e.currentTarget.value;
- }}
- />
- </p>
- <p>
- <button
- class="pure-button pure-button-primary btn-confirm"
- onClick={(e) => {
- e.preventDefault();
- if (
- captchaAnswer ==
- (captchaNumbers.a + captchaNumbers.b).toString()
- ) {
- confirmWithdrawalCall(
- backend.state,
- pageState.withdrawalId,
- pageStateSetter,
- );
- return;
- }
- pageStateSetter((prevState: PageStateType) => ({
- ...prevState,
-
- error: {
- title: i18n.str`Answer is wrong.`,
- },
- }));
- }}
- >
- {i18n.str`Confirm`}
- </button>
- &nbsp;
- <button
- class="pure-button pure-button-secondary btn-cancel"
- onClick={async () =>
- await abortWithdrawalCall(
- backend.state,
- pageState.withdrawalId,
- pageStateSetter,
- )
- }
- >
- {i18n.str`Cancel`}
- </button>
- </p>
- </div>
- </form>
- <div class="hint">
- <p>
- <i18n.Translate>
- A this point, a <b>real</b> bank would ask for an additional
- authentication proof (PIN/TAN, one time password, ..), instead
- of a simple calculation.
- </i18n.Translate>
- </p>
- </div>
- </div>
- </article>
- </Fragment>
- );
-}
-
-/**
- * This function confirms a withdrawal operation AFTER
- * the wallet has given the exchange's payment details
- * to the bank (via the Integration API). Such details
- * can be given by scanning a QR code or by passing the
- * raw taler://withdraw-URI to the CLI wallet.
- *
- * This function will set the confirmation status in the
- * 'page state' and let the related components refresh.
- */
-async function confirmWithdrawalCall(
- backendState: BackendState,
- withdrawalId: string | undefined,
- pageStateSetter: StateUpdater<PageStateType>,
-): Promise<void> {
- if (backendState.status === "loggedOut") {
- logger.error("No credentials found.");
- pageStateSetter((prevState) => ({
- ...prevState,
-
- error: {
- title: "No credentials found.",
- },
- }));
- return;
- }
- if (typeof withdrawalId === "undefined") {
- logger.error("No withdrawal ID found.");
- pageStateSetter((prevState) => ({
- ...prevState,
-
- error: {
- title: "No withdrawal ID found.",
- },
- }));
- return;
- }
- let res: Response;
- try {
- const { username, password } = backendState;
- const headers = prepareHeaders(username, password);
- /**
- * NOTE: tests show that when a same object is being
- * POSTed, caching might prevent same requests from being
- * made. Hence, trying to POST twice the same amount might
- * get silently ignored.
- *
- * headers.append("cache-control", "no-store");
- * headers.append("cache-control", "no-cache");
- * headers.append("pragma", "no-cache");
- * */
-
- // Backend URL must have been stored _with_ a final slash.
- const url = new URL(
- `access-api/accounts/${backendState.username}/withdrawals/${withdrawalId}/confirm`,
- backendState.url,
- );
- res = await fetch(url.href, {
- method: "POST",
- headers,
- });
- } catch (error) {
- logger.error("Could not POST withdrawal confirmation to the bank", error);
- pageStateSetter((prevState) => ({
- ...prevState,
-
- error: {
- title: `Could not confirm the withdrawal`,
- description: (error as any).error.description,
- debug: JSON.stringify(error),
- },
- }));
- return;
- }
- if (!res || !res.ok) {
- const response = await res.json();
- // assume not ok if res is null
- logger.error(
- `Withdrawal confirmation gave response error (${res.status})`,
- res.statusText,
- );
- pageStateSetter((prevState) => ({
- ...prevState,
-
- error: {
- title: `Withdrawal confirmation gave response error`,
- debug: JSON.stringify(response),
- },
- }));
- return;
- }
- logger.trace("Withdrawal operation confirmed!");
- pageStateSetter((prevState) => {
- const { talerWithdrawUri, ...rest } = prevState;
- return {
- ...rest,
-
- info: "Withdrawal confirmed!",
- };
- });
-}
-
-/**
- * Abort a withdrawal operation via the Access API's /abort.
- */
-async function abortWithdrawalCall(
- backendState: BackendState,
- withdrawalId: string | undefined,
- pageStateSetter: StateUpdater<PageStateType>,
-): Promise<void> {
- if (backendState.status === "loggedOut") {
- logger.error("No credentials found.");
- pageStateSetter((prevState) => ({
- ...prevState,
-
- error: {
- title: `No credentials found.`,
- },
- }));
- return;
- }
- if (typeof withdrawalId === "undefined") {
- logger.error("No withdrawal ID found.");
- pageStateSetter((prevState) => ({
- ...prevState,
-
- error: {
- title: `No withdrawal ID found.`,
- },
- }));
- return;
- }
- let res: Response;
- try {
- const { username, password } = backendState;
- const headers = prepareHeaders(username, password);
- /**
- * NOTE: tests show that when a same object is being
- * POSTed, caching might prevent same requests from being
- * made. Hence, trying to POST twice the same amount might
- * get silently ignored. Needs more observation!
- *
- * headers.append("cache-control", "no-store");
- * headers.append("cache-control", "no-cache");
- * headers.append("pragma", "no-cache");
- * */
-
- // Backend URL must have been stored _with_ a final slash.
- const url = new URL(
- `access-api/accounts/${backendState.username}/withdrawals/${withdrawalId}/abort`,
- backendState.url,
- );
- res = await fetch(url.href, { method: "POST", headers });
- } catch (error) {
- logger.error("Could not abort the withdrawal", error);
- pageStateSetter((prevState) => ({
- ...prevState,
-
- error: {
- title: `Could not abort the withdrawal.`,
- description: (error as any).error.description,
- debug: JSON.stringify(error),
- },
- }));
- return;
- }
- if (!res.ok) {
- const response = await res.json();
- logger.error(
- `Withdrawal abort gave response error (${res.status})`,
- res.statusText,
- );
- pageStateSetter((prevState) => ({
- ...prevState,
-
- error: {
- title: `Withdrawal abortion failed.`,
- description: response.error.description,
- debug: JSON.stringify(response),
- },
- }));
- return;
- }
- logger.trace("Withdrawal operation aborted!");
- pageStateSetter((prevState) => {
- const { ...rest } = prevState;
- return {
- ...rest,
-
- info: "Withdrawal aborted!",
- };
- });
-}
diff --git a/packages/demobank-ui/src/pages/home/WithdrawalQRCode.tsx b/packages/demobank-ui/src/pages/home/WithdrawalQRCode.tsx
deleted file mode 100644
index d5b8794d3..000000000
--- a/packages/demobank-ui/src/pages/home/WithdrawalQRCode.tsx
+++ /dev/null
@@ -1,104 +0,0 @@
-import { Logger } from "@gnu-taler/taler-util";
-import { Fragment, h, VNode } from "preact";
-import useSWR from "swr";
-import { PageStateType, usePageContext } from "../../context/pageState.js";
-import { useTranslationContext } from "../../context/translation.js";
-import { QrCodeSection } from "./QrCodeSection.js";
-import { WithdrawalConfirmationQuestion } from "./WithdrawalConfirmationQuestion.js";
-
-const logger = new Logger("WithdrawalQRCode");
-/**
- * Offer the QR code (and a clickable taler://-link) to
- * permit the passing of exchange and reserve details to
- * the bank. Poll the backend until such operation is done.
- */
-export function WithdrawalQRCode({
- withdrawalId,
- talerWithdrawUri,
-}: {
- withdrawalId: string;
- talerWithdrawUri: string;
-}): VNode {
- // turns true when the wallet POSTed the reserve details:
- const { pageState, pageStateSetter } = usePageContext();
- const { i18n } = useTranslationContext();
- const abortButton = (
- <a
- class="pure-button btn-cancel"
- onClick={() => {
- pageStateSetter((prevState: PageStateType) => {
- return {
- ...prevState,
- withdrawalId: undefined,
- talerWithdrawUri: undefined,
- withdrawalInProgress: false,
- };
- });
- }}
- >{i18n.str`Abort`}</a>
- );
-
- logger.trace(`Showing withdraw URI: ${talerWithdrawUri}`);
- // waiting for the wallet:
-
- const { data, error } = useSWR(
- `integration-api/withdrawal-operation/${withdrawalId}`,
- { refreshInterval: 1000 },
- );
-
- if (typeof error !== "undefined") {
- logger.error(
- `withdrawal (${withdrawalId}) was never (correctly) created at the bank...`,
- error,
- );
- pageStateSetter((prevState: PageStateType) => ({
- ...prevState,
-
- error: {
- title: i18n.str`withdrawal (${withdrawalId}) was never (correctly) created at the bank...`,
- },
- }));
- return (
- <Fragment>
- <br />
- <br />
- {abortButton}
- </Fragment>
- );
- }
-
- // data didn't arrive yet and wallet didn't communicate:
- if (typeof data === "undefined")
- return <p>{i18n.str`Waiting the bank to create the operation...`}</p>;
-
- /**
- * Wallet didn't communicate withdrawal details yet:
- */
- logger.trace("withdrawal status", data);
- if (data.aborted)
- pageStateSetter((prevState: PageStateType) => {
- const { withdrawalId, talerWithdrawUri, ...rest } = prevState;
- return {
- ...rest,
- withdrawalInProgress: false,
-
- error: {
- title: i18n.str`This withdrawal was aborted!`,
- },
- };
- });
-
- if (!data.selection_done) {
- return (
- <QrCodeSection
- talerWithdrawUri={talerWithdrawUri}
- abortButton={abortButton}
- />
- );
- }
- /**
- * Wallet POSTed the withdrawal details! Ask the
- * user to authorize the operation (here CAPTCHA).
- */
- return <WithdrawalConfirmationQuestion />;
-}
diff --git a/packages/demobank-ui/src/pages/home/index.stories.tsx b/packages/demobank-ui/src/pages/home/index.stories.tsx
deleted file mode 100644
index e9ac00a76..000000000
--- a/packages/demobank-ui/src/pages/home/index.stories.tsx
+++ /dev/null
@@ -1 +0,0 @@
-export * as qr from "./QrCodeSection.stories.js";
diff --git a/packages/demobank-ui/src/scss/bank.scss b/packages/demobank-ui/src/scss/bank.scss
deleted file mode 100644
index dba2ee3d3..000000000
--- a/packages/demobank-ui/src/scss/bank.scss
+++ /dev/null
@@ -1,270 +0,0 @@
-.navcontainer:not(.default-navcontainer) {
- margin-bottom: 0 !important;
-}
-
-.abort-button {
- margin-left: 2px;
- border: 2px solid rgb(0, 120, 231);
- color: rgb(0, 120, 231);
- font-size: 87%;
- margin-top: 1px;
- background: white;
-}
-
-div.pages-list {
- margin-top: 15px;
-}
-
-.footer {
- margin-left: 2em;
- margin-right: 2em;
-}
-
-.qr-div,
-.login-div,
-.register-div {
- display: block;
- text-align: center;
-}
-
-a.page-number {
- color: blue;
-}
-
-a.current-page-number {
- color: inherit;
- background-color: inherit;
-}
-
-.cancelled {
- text-decoration: line-through;
-}
-
-input[type="number"]::-webkit-outer-spin-button,
-input[type="number"]::-webkit-inner-spin-button {
- -webkit-appearance: none;
- margin: 0;
-}
-
-/* This CSS code styles the tab */
-.tab {
- overflow: hidden;
-}
-
-.logout {
- float: right;
- border: 20px;
- margin-right: 15px;
- margin-top: 15px;
-}
-
-.tab button {
- background-color: lightgray;
- color: black;
- float: left;
- border: none;
- outline: none;
- cursor: pointer;
- padding: 18px 19px;
- border: 2px solid #c1c1c1;
- transition: 0.5s;
- font-weight: bold;
-}
-
-.tab button:hover {
- background-color: yellow;
- border: 2px solid #c1c1c1;
- color: black;
-}
-
-.tab button.active {
- background-color: orange;
- border: 2px solid #c1c1c1;
- color: black;
- font-weight: bold;
-}
-
-.tabcontent {
- display: none;
- padding: 8px 16px;
- border: 2px solid #c1c1c1;
- width: max-content;
-}
-
-.tabcontent.active {
- display: block;
-}
-
-input[type="number"] {
- -moz-appearance: textfield;
-}
-
-#transfer-fields {
- display: flex;
- flex-wrap: wrap;
-}
-
-#id_amount {
- width: 6em;
- display: inline-block;
- border-radius: 4px 0px 0px 4px;
-}
-
-/**
- * Amount without the currency,
- * placed left to a .currency-indicator.
- */
-#main .amount {
- width: 6em;
- display: inline-block;
- border-radius: 4px 0px 0px 4px;
-}
-
-input {
- background-color: inherit;
-}
-
-.large-amount {
- font-weight: bold;
- font-size: xxx-large;
-}
-
-.currency {
- font-style: oblique;
-}
-
-/*
- * Currency indicator to the right of input fields,
- * with non-rounded corners to the left.
- */
-#main .currency-indicator {
- color: black;
- border-radius: 4px 0px 0px 4px;
- position: relative;
-}
-
-#main .fieldlabel {
- display: block;
- padding-bottom: 0.5em;
-}
-
-#main .fieldbox {
- margin-right: 1em;
- margin-bottom: 0.5em;
-}
-
-#logout-button {
- display: block;
- width: fit-content;
-}
-
-.register-form > .pure-form,
-.login-form > .pure-form {
- background: #4a4a4a;
- color: #ffffff;
- display: inline-block;
- text-align: left;
- margin-left: auto;
- margin-right: auto;
- padding: 16px 16px;
- border-radius: 8px;
- width: max-content;
- .formFieldLabel {
- margin: 2px 2px;
- }
- input[type="text"],
- input[type="password"] {
- border: none;
- border-radius: 4px;
- background: #6a6a6a;
- color: #fefefe;
- box-shadow: none;
- }
- input[placeholder="Password"][type="password"] {
- margin-bottom: 8px;
- }
- .btn-register,
- .btn-login {
- float: left;
- }
- .btn-cancel {
- float: right;
- }
- h2 {
- margin-top: 0;
- margin-bottom: 10px;
- }
-}
-
-.challenge-div {
- display: block;
- text-align: center;
-}
-
-.challenge-form > .pure-form {
- background: #4a4a4a;
- color: #ffffff;
- display: inline-block;
- text-align: left;
- margin-left: auto;
- margin-right: auto;
- padding: 16px 16px;
- border-radius: 8px;
- width: max-content;
- .formFieldLabel {
- margin: 2px 2px;
- }
- input[type="text"] {
- border: none;
- border-radius: 4px;
- background: #6a6a6a;
- color: #fefefe;
- box-shadow: none;
- }
- .btn-confirm {
- float: left;
- }
- .btn-cancel {
- float: right;
- }
- h2 {
- margin-top: 0;
- margin-bottom: 10px;
- }
-}
-
-.wire-transfer-form > .pure-form,
-.payto-form > .pure-form,
-.reserve-form > .pure-form {
- background: #4a4a4a;
- color: #ffffff;
- display: inline-block;
- text-align: left;
- margin-left: auto;
- margin-right: auto;
- padding: 16px 16px;
- border-radius: 8px;
- width: max-content;
- .formFieldLabel {
- margin: 2px 2px;
- }
- input[type="text"] {
- border: none;
- border-radius: 4px;
- background: #6a6a6a;
- color: #fefefe;
- box-shadow: none;
- }
-}
-
-html {
- background: #ffffff;
- color: #2a2a2a;
-}
-
-.hint {
- scale: 0.7;
-}
-h1.nav {
- text-align: center;
-}
diff --git a/packages/demobank-ui/src/scss/colors-bank.scss b/packages/demobank-ui/src/scss/colors-bank.scss
deleted file mode 100644
index e11bbe203..000000000
--- a/packages/demobank-ui/src/scss/colors-bank.scss
+++ /dev/null
@@ -1,31 +0,0 @@
-nav,
-nav a,
-nav span,
-.navcontainer,
-nav button,
-.demobar,
-.navbtn {
- color: white;
- background: #a00000;
-}
-
-nav a.active,
-nav button,
-nav span.active,
-.navbtn.active {
- background-color: #7a0606;
-}
-
-nav a.active:hover,
-nav span.active:hover,
-.navbtn.active:hover,
-nav button:hover,
-nav a:hover,
-nav span:hover,
-.navbtn:hover {
- background: #df3d3d;
-}
-
-nav a.navbtn.langbtn:focus {
- background-color: #df3d3d;
-}
diff --git a/packages/demobank-ui/src/scss/demo.scss b/packages/demobank-ui/src/scss/demo.scss
deleted file mode 100644
index 3b7acaa1f..000000000
--- a/packages/demobank-ui/src/scss/demo.scss
+++ /dev/null
@@ -1,158 +0,0 @@
-@charset "UTF-8";
-/*
-Style common to all demo pages.
-
-Colors:
-- #1e2739 (dark blue)
-- #0042b2 (default blue)
-- #3daee9 (highlight blue)
-*/
-
-.demobar h1 {
- text-align: center;
-}
-
-.demobar > p {
- padding: 0.5em;
-}
-
-.demobar a,
-.demobar a:visited {
- color: inherit;
- background-color: inherit;
-}
-
-.tt {
- font-family: "Lucida Console", Monaco, monospace;
-}
-
-.informational-ok {
- background: lightgreen;
- border-radius: 1em;
- padding: 0.5em;
-}
-
-.informational-fail {
- background: lightpink;
- border-radius: 1em;
- padding: 0.5em;
-}
-
-.content {
- margin-left: 2em;
- margin-right: 2em;
- overflow-x: auto;
-}
-
-.demobar {
- overflow-x: auto;
- background-color: #0042b2;
- color: white;
-}
-
-body {
- overflow-x: hidden;
- overflow-y: auto;
-}
-
-.navcontainer {
- background: #0042b2;
- margin-bottom: 50px;
- width: 100%;
- color: white;
- position: -webkit-sticky;
- position: sticky;
- top: 0px;
- width: 100vw;
- backdrop-filter: blur(10px);
- opacity: 1;
- z-index: 10000;
-}
-
-nav {
- left: 1vw;
- position: relative;
- background: #0042b2;
- z-index: 10000;
-}
-
-nav a,
-nav button,
-nav span,
-.navbtn {
- border: none;
- color: white;
- text-align: center;
- text-decoration: none;
- display: inline-block;
- font-size: 16px;
- background: #0042b2;
- height: inherit;
-}
-
-nav a,
-nav button,
-nav span,
-.navbtn {
- padding: 15px 32px;
-}
-
-nav a:hover,
-nav span:hover,
-.navbtn:hover {
- background: #3daee9;
-}
-
-nav a.active,
-nav span.active,
-.navbtn.active {
- background-color: #1e2739;
-}
-
-nav a.active:hover,
-nav button.active:hover,
-nav span.active:hover,
-.navbtn.active:hover {
- background: #3daee9;
-}
-
-nav a,
-nav span,
-.navbtn {
- cursor: pointer;
-}
-
-nav .right {
- float: right;
- margin-right: 5vw;
-}
-nav .hide div.nav {
- display: none;
-}
-// nav .right div.nav:hover {
-// display: block;
-// }
-
-// nav .right:hover div.nav {
-// display: block;
-// }
-
-.langbtn {
- width: 100%;
- text-align: left;
-}
-
-.skip {
- position: absolute;
- left: -10000px;
- top: auto;
- width: 1px;
- height: 1px;
- overflow: hidden;
-}
-
-.skip:focus {
- position: static;
- width: auto;
- height: auto;
-}
diff --git a/packages/demobank-ui/src/scss/main.scss b/packages/demobank-ui/src/scss/main.scss
deleted file mode 100644
index b92260af0..000000000
--- a/packages/demobank-ui/src/scss/main.scss
+++ /dev/null
@@ -1,4 +0,0 @@
-@use "pure";
-@use "bank";
-@use "demo";
-@use "colors-bank";
diff --git a/packages/demobank-ui/src/scss/pure.scss b/packages/demobank-ui/src/scss/pure.scss
deleted file mode 100644
index 652d20edf..000000000
--- a/packages/demobank-ui/src/scss/pure.scss
+++ /dev/null
@@ -1,1397 +0,0 @@
-/*!
-Pure v2.2.0
-Copyright 2013 Yahoo!
-Licensed under the BSD License.
-https://github.com/pure-css/pure/blob/master/LICENSE
-*/
-/*!
-normalize.css v | MIT License | https://necolas.github.io/normalize.css/
-Copyright (c) Nicolas Gallagher and Jonathan Neal
-*/
-/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
-
-/* Document
- ========================================================================== */
-
-/**
- * 1. Correct the line height in all browsers.
- * 2. Prevent adjustments of font size after orientation changes in iOS.
- */
-
-html {
- line-height: 1.15; /* 1 */
- -webkit-text-size-adjust: 100%; /* 2 */
-}
-
-/* Sections
- ========================================================================== */
-
-/**
- * Remove the margin in all browsers.
- */
-
-body {
- margin: 0;
-}
-
-/**
- * Render the `main` element consistently in IE.
- */
-
-main {
- display: block;
-}
-
-/**
- * Correct the font size and margin on `h1` elements within `section` and
- * `article` contexts in Chrome, Firefox, and Safari.
- */
-
-h1 {
- font-size: 2em;
- margin: 0.67em 0;
-}
-
-/* Grouping content
- ========================================================================== */
-
-/**
- * 1. Add the correct box sizing in Firefox.
- * 2. Show the overflow in Edge and IE.
- */
-
-hr {
- -webkit-box-sizing: content-box;
- box-sizing: content-box; /* 1 */
- height: 0; /* 1 */
- overflow: visible; /* 2 */
-}
-
-/**
- * 1. Correct the inheritance and scaling of font size in all browsers.
- * 2. Correct the odd `em` font sizing in all browsers.
- */
-
-pre {
- font-family: monospace, monospace; /* 1 */
- font-size: 1em; /* 2 */
-}
-
-/* Text-level semantics
- ========================================================================== */
-
-/**
- * Remove the gray background on active links in IE 10.
- */
-
-a {
- background-color: transparent;
-}
-
-/**
- * 1. Remove the bottom border in Chrome 57-
- * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
- */
-
-abbr[title] {
- border-bottom: none; /* 1 */
- text-decoration: underline; /* 2 */
- -webkit-text-decoration: underline dotted;
- text-decoration: underline dotted; /* 2 */
-}
-
-/**
- * Add the correct font weight in Chrome, Edge, and Safari.
- */
-
-b,
-strong {
- font-weight: bolder;
-}
-
-/**
- * 1. Correct the inheritance and scaling of font size in all browsers.
- * 2. Correct the odd `em` font sizing in all browsers.
- */
-
-code,
-kbd,
-samp {
- font-family: monospace, monospace; /* 1 */
- font-size: 1em; /* 2 */
-}
-
-/**
- * Add the correct font size in all browsers.
- */
-
-small {
- font-size: 80%;
-}
-
-/**
- * Prevent `sub` and `sup` elements from affecting the line height in
- * all browsers.
- */
-
-sub,
-sup {
- font-size: 75%;
- line-height: 0;
- position: relative;
- vertical-align: baseline;
-}
-
-sub {
- bottom: -0.25em;
-}
-
-sup {
- top: -0.5em;
-}
-
-/* Embedded content
- ========================================================================== */
-
-/**
- * Remove the border on images inside links in IE 10.
- */
-
-img {
- border-style: none;
-}
-
-/* Forms
- ========================================================================== */
-
-/**
- * 1. Change the font styles in all browsers.
- * 2. Remove the margin in Firefox and Safari.
- */
-
-button,
-input,
-optgroup,
-select,
-textarea {
- font-family: inherit; /* 1 */
- font-size: 100%; /* 1 */
- line-height: 1.15; /* 1 */
- margin: 0; /* 2 */
-}
-
-/**
- * Show the overflow in IE.
- * 1. Show the overflow in Edge.
- */
-
-button,
-input {
- /* 1 */
- overflow: visible;
-}
-
-/**
- * Remove the inheritance of text transform in Edge, Firefox, and IE.
- * 1. Remove the inheritance of text transform in Firefox.
- */
-
-button,
-select {
- /* 1 */
- text-transform: none;
-}
-
-/**
- * Correct the inability to style clickable types in iOS and Safari.
- */
-
-button,
-[type="button"],
-[type="reset"],
-[type="submit"] {
- -webkit-appearance: button;
-}
-
-/**
- * Remove the inner border and padding in Firefox.
- */
-
-button::-moz-focus-inner,
-[type="button"]::-moz-focus-inner,
-[type="reset"]::-moz-focus-inner,
-[type="submit"]::-moz-focus-inner {
- border-style: none;
- padding: 0;
-}
-
-/**
- * Restore the focus styles unset by the previous rule.
- */
-
-button:-moz-focusring,
-[type="button"]:-moz-focusring,
-[type="reset"]:-moz-focusring,
-[type="submit"]:-moz-focusring {
- outline: 1px dotted ButtonText;
-}
-
-/**
- * Correct the padding in Firefox.
- */
-
-fieldset {
- padding: 0.35em 0.75em 0.625em;
-}
-
-/**
- * 1. Correct the text wrapping in Edge and IE.
- * 2. Correct the color inheritance from `fieldset` elements in IE.
- * 3. Remove the padding so developers are not caught out when they zero out
- * `fieldset` elements in all browsers.
- */
-
-legend {
- -webkit-box-sizing: border-box;
- box-sizing: border-box; /* 1 */
- color: inherit; /* 2 */
- display: table; /* 1 */
- max-width: 100%; /* 1 */
- padding: 0; /* 3 */
- white-space: normal; /* 1 */
-}
-
-/**
- * Add the correct vertical alignment in Chrome, Firefox, and Opera.
- */
-
-progress {
- vertical-align: baseline;
-}
-
-/**
- * Remove the default vertical scrollbar in IE 10+.
- */
-
-textarea {
- overflow: auto;
-}
-
-/**
- * 1. Add the correct box sizing in IE 10.
- * 2. Remove the padding in IE 10.
- */
-
-[type="checkbox"],
-[type="radio"] {
- -webkit-box-sizing: border-box;
- box-sizing: border-box; /* 1 */
- padding: 0; /* 2 */
-}
-
-/**
- * Correct the cursor style of increment and decrement buttons in Chrome.
- */
-
-[type="number"]::-webkit-inner-spin-button,
-[type="number"]::-webkit-outer-spin-button {
- height: auto;
-}
-
-/**
- * 1. Correct the odd appearance in Chrome and Safari.
- * 2. Correct the outline style in Safari.
- */
-
-[type="search"] {
- -webkit-appearance: textfield; /* 1 */
- outline-offset: -2px; /* 2 */
-}
-
-/**
- * Remove the inner padding in Chrome and Safari on macOS.
- */
-
-[type="search"]::-webkit-search-decoration {
- -webkit-appearance: none;
-}
-
-/**
- * 1. Correct the inability to style clickable types in iOS and Safari.
- * 2. Change font properties to `inherit` in Safari.
- */
-
-::-webkit-file-upload-button {
- -webkit-appearance: button; /* 1 */
- font: inherit; /* 2 */
-}
-
-/* Interactive
- ========================================================================== */
-
-/*
- * Add the correct display in Edge, IE 10+, and Firefox.
- */
-
-details {
- display: block;
-}
-
-/*
- * Add the correct display in all browsers.
- */
-
-summary {
- display: list-item;
-}
-
-/* Misc
- ========================================================================== */
-
-/**
- * Add the correct display in IE 10+.
- */
-
-template {
- display: none;
-}
-
-/**
- * Add the correct display in IE 10.
- */
-
-[hidden] {
- display: none;
-}
-
-/*csslint important:false*/
-
-/* ==========================================================================
- Pure Base Extras
- ========================================================================== */
-
-/**
- * Extra rules that Pure adds on top of Normalize.css
- */
-
-html {
- font-family: sans-serif;
-}
-
-/**
- * Always hide an element when it has the `hidden` HTML attribute.
- */
-
-.hidden,
-[hidden] {
- display: none !important;
-}
-
-/**
- * Add this class to an image to make it fit within it's fluid parent wrapper while maintaining
- * aspect ratio.
- */
-.pure-img {
- max-width: 100%;
- height: auto;
- display: block;
-}
-
-/*csslint regex-selectors:false, known-properties:false, duplicate-properties:false*/
-
-.pure-g {
- letter-spacing: -0.31em; /* Webkit: collapse white-space between units */
- text-rendering: optimizespeed; /* Webkit: fixes text-rendering: optimizeLegibility */
-
- /*
- Sets the font stack to fonts known to work properly with the above letter
- and word spacings. See: https://github.com/pure-css/pure/issues/41/
-
- The following font stack makes Pure Grids work on all known environments.
-
- * FreeSans: Ships with many Linux distros, including Ubuntu
-
- * Arimo: Ships with Chrome OS. Arimo has to be defined before Helvetica and
- Arial to get picked up by the browser, even though neither is available
- in Chrome OS.
-
- * Droid Sans: Ships with all versions of Android.
-
- * Helvetica, Arial, sans-serif: Common font stack on OS X and Windows.
- */
- font-family: FreeSans, Arimo, "Droid Sans", Helvetica, Arial, sans-serif;
-
- /* Use flexbox when possible to avoid `letter-spacing` side-effects. */
- display: -webkit-box;
- display: -ms-flexbox;
- display: flex;
- -webkit-box-orient: horizontal;
- -webkit-box-direction: normal;
- -ms-flex-flow: row wrap;
- flex-flow: row wrap;
-
- /* Prevents distributing space between rows */
- -ms-flex-line-pack: start;
- align-content: flex-start;
-}
-
-/* IE10 display: -ms-flexbox (and display: flex in IE 11) does not work inside a table; fall back to block and rely on font hack */
-@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
- table .pure-g {
- display: block;
- }
-}
-
-/* Opera as of 12 on Windows needs word-spacing.
- The ".opera-only" selector is used to prevent actual prefocus styling
- and is not required in markup.
-*/
-.opera-only :-o-prefocus,
-.pure-g {
- word-spacing: -0.43em;
-}
-
-.pure-u {
- display: inline-block;
- letter-spacing: normal;
- word-spacing: normal;
- vertical-align: top;
- text-rendering: auto;
-}
-
-/*
-Resets the font family back to the OS/browser's default sans-serif font,
-this the same font stack that Normalize.css sets for the `body`.
-*/
-.pure-g [class*="pure-u"] {
- font-family: sans-serif;
-}
-
-.pure-u-1,
-.pure-u-1-1,
-.pure-u-1-2,
-.pure-u-1-3,
-.pure-u-2-3,
-.pure-u-1-4,
-.pure-u-3-4,
-.pure-u-1-5,
-.pure-u-2-5,
-.pure-u-3-5,
-.pure-u-4-5,
-.pure-u-5-5,
-.pure-u-1-6,
-.pure-u-5-6,
-.pure-u-1-8,
-.pure-u-3-8,
-.pure-u-5-8,
-.pure-u-7-8,
-.pure-u-1-12,
-.pure-u-5-12,
-.pure-u-7-12,
-.pure-u-11-12,
-.pure-u-1-24,
-.pure-u-2-24,
-.pure-u-3-24,
-.pure-u-4-24,
-.pure-u-5-24,
-.pure-u-6-24,
-.pure-u-7-24,
-.pure-u-8-24,
-.pure-u-9-24,
-.pure-u-10-24,
-.pure-u-11-24,
-.pure-u-12-24,
-.pure-u-13-24,
-.pure-u-14-24,
-.pure-u-15-24,
-.pure-u-16-24,
-.pure-u-17-24,
-.pure-u-18-24,
-.pure-u-19-24,
-.pure-u-20-24,
-.pure-u-21-24,
-.pure-u-22-24,
-.pure-u-23-24,
-.pure-u-24-24 {
- display: inline-block;
- letter-spacing: normal;
- word-spacing: normal;
- vertical-align: top;
- text-rendering: auto;
-}
-
-.pure-u-1-24 {
- width: 4.1667%;
-}
-
-.pure-u-1-12,
-.pure-u-2-24 {
- width: 8.3333%;
-}
-
-.pure-u-1-8,
-.pure-u-3-24 {
- width: 12.5%;
-}
-
-.pure-u-1-6,
-.pure-u-4-24 {
- width: 16.6667%;
-}
-
-.pure-u-1-5 {
- width: 20%;
-}
-
-.pure-u-5-24 {
- width: 20.8333%;
-}
-
-.pure-u-1-4,
-.pure-u-6-24 {
- width: 25%;
-}
-
-.pure-u-7-24 {
- width: 29.1667%;
-}
-
-.pure-u-1-3,
-.pure-u-8-24 {
- width: 33.3333%;
-}
-
-.pure-u-3-8,
-.pure-u-9-24 {
- width: 37.5%;
-}
-
-.pure-u-2-5 {
- width: 40%;
-}
-
-.pure-u-5-12,
-.pure-u-10-24 {
- width: 41.6667%;
-}
-
-.pure-u-11-24 {
- width: 45.8333%;
-}
-
-.pure-u-1-2,
-.pure-u-12-24 {
- width: 50%;
-}
-
-.pure-u-13-24 {
- width: 54.1667%;
-}
-
-.pure-u-7-12,
-.pure-u-14-24 {
- width: 58.3333%;
-}
-
-.pure-u-3-5 {
- width: 60%;
-}
-
-.pure-u-5-8,
-.pure-u-15-24 {
- width: 62.5%;
-}
-
-.pure-u-2-3,
-.pure-u-16-24 {
- width: 66.6667%;
-}
-
-.pure-u-17-24 {
- width: 70.8333%;
-}
-
-.pure-u-3-4,
-.pure-u-18-24 {
- width: 75%;
-}
-
-.pure-u-19-24 {
- width: 79.1667%;
-}
-
-.pure-u-4-5 {
- width: 80%;
-}
-
-.pure-u-5-6,
-.pure-u-20-24 {
- width: 83.3333%;
-}
-
-.pure-u-7-8,
-.pure-u-21-24 {
- width: 87.5%;
-}
-
-.pure-u-11-12,
-.pure-u-22-24 {
- width: 91.6667%;
-}
-
-.pure-u-23-24 {
- width: 95.8333%;
-}
-
-.pure-u-1,
-.pure-u-1-1,
-.pure-u-5-5,
-.pure-u-24-24 {
- width: 100%;
-}
-.pure-button {
- /* Structure */
- display: inline-block;
- line-height: normal;
- white-space: nowrap;
- vertical-align: middle;
- text-align: center;
- cursor: pointer;
- -webkit-user-drag: none;
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
- -webkit-box-sizing: border-box;
- box-sizing: border-box;
-}
-
-/* Firefox: Get rid of the inner focus border */
-.pure-button::-moz-focus-inner {
- padding: 0;
- border: 0;
-}
-
-/* Inherit .pure-g styles */
-.pure-button-group {
- letter-spacing: -0.31em; /* Webkit: collapse white-space between units */
- text-rendering: optimizespeed; /* Webkit: fixes text-rendering: optimizeLegibility */
-}
-
-.opera-only :-o-prefocus,
-.pure-button-group {
- word-spacing: -0.43em;
-}
-
-.pure-button-group .pure-button {
- letter-spacing: normal;
- word-spacing: normal;
- vertical-align: top;
- text-rendering: auto;
-}
-
-/*csslint outline-none:false*/
-
-.pure-button {
- font-family: inherit;
- font-size: 100%;
- padding: 0.5em 1em;
- color: rgba(0, 0, 0, 0.8);
- border: none rgba(0, 0, 0, 0);
- background-color: #e6e6e6;
- text-decoration: none;
- border-radius: 2px;
-}
-
-.pure-button-hover,
-.pure-button:hover,
-.pure-button:focus {
- background-image: -webkit-gradient(
- linear,
- left top,
- left bottom,
- from(transparent),
- color-stop(40%, rgba(0, 0, 0, 0.05)),
- to(rgba(0, 0, 0, 0.1))
- );
- background-image: linear-gradient(
- transparent,
- rgba(0, 0, 0, 0.05) 40%,
- rgba(0, 0, 0, 0.1)
- );
-}
-.pure-button:focus {
- outline: 0;
-}
-.pure-button-active,
-.pure-button:active {
- -webkit-box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.15) inset,
- 0 0 6px rgba(0, 0, 0, 0.2) inset;
- box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.15) inset,
- 0 0 6px rgba(0, 0, 0, 0.2) inset;
- border-color: #000;
-}
-
-.pure-button[disabled],
-.pure-button-disabled,
-.pure-button-disabled:hover,
-.pure-button-disabled:focus,
-.pure-button-disabled:active {
- border: none;
- background-image: none;
- opacity: 0.4;
- cursor: not-allowed;
- -webkit-box-shadow: none;
- box-shadow: none;
- pointer-events: none;
-}
-
-.pure-button-hidden {
- display: none;
-}
-
-.pure-button-primary,
-.pure-button-selected,
-a.pure-button-primary,
-a.pure-button-selected {
- background-color: rgb(0, 120, 231);
- color: #fff;
-}
-
-/* Button Groups */
-.pure-button-group .pure-button {
- margin: 0;
- border-radius: 0;
- border-right: 1px solid rgba(0, 0, 0, 0.2);
-}
-
-.pure-button-group .pure-button:first-child {
- border-top-left-radius: 2px;
- border-bottom-left-radius: 2px;
-}
-.pure-button-group .pure-button:last-child {
- border-top-right-radius: 2px;
- border-bottom-right-radius: 2px;
- border-right: none;
-}
-
-/*csslint box-model:false*/
-/*
-Box-model set to false because we're setting a height on select elements, which
-also have border and padding. This is done because some browsers don't render
-the padding. We explicitly set the box-model for select elements to border-box,
-so we can ignore the csslint warning.
-*/
-
-.pure-form input[type="text"],
-.pure-form input[type="password"],
-.pure-form input[type="email"],
-.pure-form input[type="url"],
-.pure-form input[type="date"],
-.pure-form input[type="month"],
-.pure-form input[type="time"],
-.pure-form input[type="datetime"],
-.pure-form input[type="datetime-local"],
-.pure-form input[type="week"],
-.pure-form input[type="number"],
-.pure-form input[type="search"],
-.pure-form input[type="tel"],
-.pure-form input[type="color"],
-.pure-form select,
-.pure-form textarea {
- padding: 0.5em 0.6em;
- display: inline-block;
- border: 1px solid #ccc;
- -webkit-box-shadow: inset 0 1px 3px #ddd;
- box-shadow: inset 0 1px 3px #ddd;
- border-radius: 4px;
- vertical-align: middle;
- -webkit-box-sizing: border-box;
- box-sizing: border-box;
-}
-
-/*
-Need to separate out the :not() selector from the rest of the CSS 2.1 selectors
-since IE8 won't execute CSS that contains a CSS3 selector.
-*/
-.pure-form input:not([type]) {
- padding: 0.5em 0.6em;
- display: inline-block;
- border: 1px solid #ccc;
- -webkit-box-shadow: inset 0 1px 3px #ddd;
- box-shadow: inset 0 1px 3px #ddd;
- border-radius: 4px;
- -webkit-box-sizing: border-box;
- box-sizing: border-box;
-}
-
-/* Chrome (as of v.32/34 on OS X) needs additional room for color to display. */
-/* May be able to remove this tweak as color inputs become more standardized across browsers. */
-.pure-form input[type="color"] {
- padding: 0.2em 0.5em;
-}
-
-.pure-form input[type="text"]:focus,
-.pure-form input[type="password"]:focus,
-.pure-form input[type="email"]:focus,
-.pure-form input[type="url"]:focus,
-.pure-form input[type="date"]:focus,
-.pure-form input[type="month"]:focus,
-.pure-form input[type="time"]:focus,
-.pure-form input[type="datetime"]:focus,
-.pure-form input[type="datetime-local"]:focus,
-.pure-form input[type="week"]:focus,
-.pure-form input[type="number"]:focus,
-.pure-form input[type="search"]:focus,
-.pure-form input[type="tel"]:focus,
-.pure-form input[type="color"]:focus,
-.pure-form select:focus,
-.pure-form textarea:focus {
- outline: 0;
- border-color: #129fea;
-}
-
-/*
-Need to separate out the :not() selector from the rest of the CSS 2.1 selectors
-since IE8 won't execute CSS that contains a CSS3 selector.
-*/
-.pure-form input:not([type]):focus {
- outline: 0;
- border-color: #129fea;
-}
-
-.pure-form input[type="file"]:focus,
-.pure-form input[type="radio"]:focus,
-.pure-form input[type="checkbox"]:focus {
- outline: thin solid #129fea;
- outline: 1px auto #129fea;
-}
-.pure-form .pure-checkbox,
-.pure-form .pure-radio {
- margin: 0.5em 0;
- display: block;
-}
-
-.pure-form input[type="text"][disabled],
-.pure-form input[type="password"][disabled],
-.pure-form input[type="email"][disabled],
-.pure-form input[type="url"][disabled],
-.pure-form input[type="date"][disabled],
-.pure-form input[type="month"][disabled],
-.pure-form input[type="time"][disabled],
-.pure-form input[type="datetime"][disabled],
-.pure-form input[type="datetime-local"][disabled],
-.pure-form input[type="week"][disabled],
-.pure-form input[type="number"][disabled],
-.pure-form input[type="search"][disabled],
-.pure-form input[type="tel"][disabled],
-.pure-form input[type="color"][disabled],
-.pure-form select[disabled],
-.pure-form textarea[disabled] {
- cursor: not-allowed;
- background-color: #eaeded;
- color: #cad2d3;
-}
-
-/*
-Need to separate out the :not() selector from the rest of the CSS 2.1 selectors
-since IE8 won't execute CSS that contains a CSS3 selector.
-*/
-.pure-form input:not([type])[disabled] {
- cursor: not-allowed;
- background-color: #eaeded;
- color: #cad2d3;
-}
-.pure-form input[readonly],
-.pure-form select[readonly],
-.pure-form textarea[readonly] {
- background-color: #eee; /* menu hover bg color */
- color: #777; /* menu text color */
- border-color: #ccc;
-}
-
-/**
- * Even if we add novalidate property
- * in the form, the styles are applied for
- * invalid elements so we need to remove this styles
- *
- */
-// .pure-form input:focus:invalid,
-// .pure-form textarea:focus:invalid,
-// .pure-form select:focus:invalid {
-// color: #b94a48;
-// border-color: #e9322d;
-// }
-// .pure-form input[type="file"]:focus:invalid:focus,
-// .pure-form input[type="radio"]:focus:invalid:focus,
-// .pure-form input[type="checkbox"]:focus:invalid:focus {
-// outline-color: #e9322d;
-// }
-.pure-form select {
- /* Normalizes the height; padding is not sufficient. */
- height: 2.25em;
- border: 1px solid #ccc;
- background-color: white;
-}
-.pure-form select[multiple] {
- height: auto;
-}
-.pure-form label {
- margin: 0.5em 0 0.2em;
-}
-.pure-form fieldset {
- margin: 0;
- padding: 0.35em 0 0.75em;
- border: 0;
-}
-.pure-form legend {
- display: block;
- width: 100%;
- padding: 0.3em 0;
- margin-bottom: 0.3em;
- color: #333;
- border-bottom: 1px solid #e5e5e5;
-}
-
-.pure-form-stacked input[type="text"],
-.pure-form-stacked input[type="password"],
-.pure-form-stacked input[type="email"],
-.pure-form-stacked input[type="url"],
-.pure-form-stacked input[type="date"],
-.pure-form-stacked input[type="month"],
-.pure-form-stacked input[type="time"],
-.pure-form-stacked input[type="datetime"],
-.pure-form-stacked input[type="datetime-local"],
-.pure-form-stacked input[type="week"],
-.pure-form-stacked input[type="number"],
-.pure-form-stacked input[type="search"],
-.pure-form-stacked input[type="tel"],
-.pure-form-stacked input[type="color"],
-.pure-form-stacked input[type="file"],
-.pure-form-stacked select,
-.pure-form-stacked label,
-.pure-form-stacked textarea {
- display: block;
- margin: 0.25em 0;
-}
-
-/*
-Need to separate out the :not() selector from the rest of the CSS 2.1 selectors
-since IE8 won't execute CSS that contains a CSS3 selector.
-*/
-.pure-form-stacked input:not([type]) {
- display: block;
- margin: 0.25em 0;
-}
-.pure-form-aligned input,
-.pure-form-aligned textarea,
-.pure-form-aligned select,
-.pure-form-message-inline {
- display: inline-block;
- vertical-align: middle;
-}
-.pure-form-aligned textarea {
- vertical-align: top;
-}
-
-/* Aligned Forms */
-.pure-form-aligned .pure-control-group {
- margin-bottom: 0.5em;
-}
-.pure-form-aligned .pure-control-group label {
- text-align: right;
- display: inline-block;
- vertical-align: middle;
- width: 10em;
- margin: 0 1em 0 0;
-}
-.pure-form-aligned .pure-controls {
- margin: 1.5em 0 0 11em;
-}
-
-/* Rounded Inputs */
-.pure-form input.pure-input-rounded,
-.pure-form .pure-input-rounded {
- border-radius: 2em;
- padding: 0.5em 1em;
-}
-
-/* Grouped Inputs */
-.pure-form .pure-group fieldset {
- margin-bottom: 10px;
-}
-.pure-form .pure-group input,
-.pure-form .pure-group textarea {
- display: block;
- padding: 10px;
- margin: 0 0 -1px;
- border-radius: 0;
- position: relative;
- top: -1px;
-}
-.pure-form .pure-group input:focus,
-.pure-form .pure-group textarea:focus {
- z-index: 3;
-}
-.pure-form .pure-group input:first-child,
-.pure-form .pure-group textarea:first-child {
- top: 1px;
- border-radius: 4px 4px 0 0;
- margin: 0;
-}
-.pure-form .pure-group input:first-child:last-child,
-.pure-form .pure-group textarea:first-child:last-child {
- top: 1px;
- border-radius: 4px;
- margin: 0;
-}
-.pure-form .pure-group input:last-child,
-.pure-form .pure-group textarea:last-child {
- top: -2px;
- border-radius: 0 0 4px 4px;
- margin: 0;
-}
-.pure-form .pure-group button {
- margin: 0.35em 0;
-}
-
-.pure-form .pure-input-1 {
- width: 100%;
-}
-.pure-form .pure-input-3-4 {
- width: 75%;
-}
-.pure-form .pure-input-2-3 {
- width: 66%;
-}
-.pure-form .pure-input-1-2 {
- width: 50%;
-}
-.pure-form .pure-input-1-3 {
- width: 33%;
-}
-.pure-form .pure-input-1-4 {
- width: 25%;
-}
-
-/* Inline help for forms */
-.pure-form-message-inline {
- display: inline-block;
- padding-left: 0.3em;
- color: #666;
- vertical-align: middle;
- font-size: 0.875em;
-}
-
-/* Block help for forms */
-.pure-form-message {
- display: block;
- color: #666;
- font-size: 0.875em;
-}
-
-@media only screen and (max-width: 480px) {
- .pure-form button[type="submit"] {
- margin: 0.7em 0 0;
- }
-
- .pure-form input:not([type]),
- .pure-form input[type="text"],
- .pure-form input[type="password"],
- .pure-form input[type="email"],
- .pure-form input[type="url"],
- .pure-form input[type="date"],
- .pure-form input[type="month"],
- .pure-form input[type="time"],
- .pure-form input[type="datetime"],
- .pure-form input[type="datetime-local"],
- .pure-form input[type="week"],
- .pure-form input[type="number"],
- .pure-form input[type="search"],
- .pure-form input[type="tel"],
- .pure-form input[type="color"],
- .pure-form label {
- margin-bottom: 0.3em;
- display: block;
- }
-
- .pure-group input:not([type]),
- .pure-group input[type="text"],
- .pure-group input[type="password"],
- .pure-group input[type="email"],
- .pure-group input[type="url"],
- .pure-group input[type="date"],
- .pure-group input[type="month"],
- .pure-group input[type="time"],
- .pure-group input[type="datetime"],
- .pure-group input[type="datetime-local"],
- .pure-group input[type="week"],
- .pure-group input[type="number"],
- .pure-group input[type="search"],
- .pure-group input[type="tel"],
- .pure-group input[type="color"] {
- margin-bottom: 0;
- }
-
- .pure-form-aligned .pure-control-group label {
- margin-bottom: 0.3em;
- text-align: left;
- display: block;
- width: 100%;
- }
-
- .pure-form-aligned .pure-controls {
- margin: 1.5em 0 0 0;
- }
-
- .pure-form-message-inline,
- .pure-form-message {
- display: block;
- font-size: 0.75em;
- /* Increased bottom padding to make it group with its related input element. */
- padding: 0.2em 0 0.8em;
- }
-}
-
-/*csslint adjoining-classes: false, box-model:false*/
-.pure-menu {
- -webkit-box-sizing: border-box;
- box-sizing: border-box;
-}
-
-.pure-menu-fixed {
- position: fixed;
- left: 0;
- top: 0;
- z-index: 3;
-}
-
-.pure-menu-list,
-.pure-menu-item {
- position: relative;
-}
-
-.pure-menu-list {
- list-style: none;
- margin: 0;
- padding: 0;
-}
-
-.pure-menu-item {
- padding: 0;
- margin: 0;
- height: 100%;
-}
-
-.pure-menu-link,
-.pure-menu-heading {
- display: block;
- text-decoration: none;
- white-space: nowrap;
-}
-
-/* HORIZONTAL MENU */
-.pure-menu-horizontal {
- width: 100%;
- white-space: nowrap;
-}
-
-.pure-menu-horizontal .pure-menu-list {
- display: inline-block;
-}
-
-/* Initial menus should be inline-block so that they are horizontal */
-.pure-menu-horizontal .pure-menu-item,
-.pure-menu-horizontal .pure-menu-heading,
-.pure-menu-horizontal .pure-menu-separator {
- display: inline-block;
- vertical-align: middle;
-}
-
-/* Submenus should still be display: block; */
-.pure-menu-item .pure-menu-item {
- display: block;
-}
-
-.pure-menu-children {
- display: none;
- position: absolute;
- left: 100%;
- top: 0;
- margin: 0;
- padding: 0;
- z-index: 3;
-}
-
-.pure-menu-horizontal .pure-menu-children {
- left: 0;
- top: auto;
- width: inherit;
-}
-
-.pure-menu-allow-hover:hover > .pure-menu-children,
-.pure-menu-active > .pure-menu-children {
- display: block;
- position: absolute;
-}
-
-/* Vertical Menus - show the dropdown arrow */
-.pure-menu-has-children > .pure-menu-link:after {
- padding-left: 0.5em;
- content: "\25B8";
- font-size: small;
-}
-
-/* Horizontal Menus - show the dropdown arrow */
-.pure-menu-horizontal .pure-menu-has-children > .pure-menu-link:after {
- content: "\25BE";
-}
-
-/* scrollable menus */
-.pure-menu-scrollable {
- overflow-y: scroll;
- overflow-x: hidden;
-}
-
-.pure-menu-scrollable .pure-menu-list {
- display: block;
-}
-
-.pure-menu-horizontal.pure-menu-scrollable .pure-menu-list {
- display: inline-block;
-}
-
-.pure-menu-horizontal.pure-menu-scrollable {
- white-space: nowrap;
- overflow-y: hidden;
- overflow-x: auto;
- /* a little extra padding for this style to allow for scrollbars */
- padding: 0.5em 0;
-}
-
-/* misc default styling */
-
-.pure-menu-separator,
-.pure-menu-horizontal .pure-menu-children .pure-menu-separator {
- background-color: #ccc;
- height: 1px;
- margin: 0.3em 0;
-}
-
-.pure-menu-horizontal .pure-menu-separator {
- width: 1px;
- height: 1.3em;
- margin: 0 0.3em;
-}
-
-/* Need to reset the separator since submenu is vertical */
-.pure-menu-horizontal .pure-menu-children .pure-menu-separator {
- display: block;
- width: auto;
-}
-
-.pure-menu-heading {
- text-transform: uppercase;
- color: #565d64;
-}
-
-.pure-menu-link {
- color: #777;
-}
-
-.pure-menu-children {
- background-color: #fff;
-}
-
-.pure-menu-link,
-.pure-menu-heading {
- padding: 0.5em 1em;
-}
-
-.pure-menu-disabled {
- opacity: 0.5;
-}
-
-.pure-menu-disabled .pure-menu-link:hover {
- background-color: transparent;
- cursor: default;
-}
-
-.pure-menu-active > .pure-menu-link,
-.pure-menu-link:hover,
-.pure-menu-link:focus {
- background-color: #eee;
-}
-
-.pure-menu-selected > .pure-menu-link,
-.pure-menu-selected > .pure-menu-link:visited {
- color: #000;
-}
-
-.pure-table {
- /* Remove spacing between table cells (from Normalize.css) */
- border-collapse: collapse;
- border-spacing: 0;
- empty-cells: show;
- border: 1px solid #cbcbcb;
-}
-
-.pure-table caption {
- color: #000;
- font: italic 85%/1 arial, sans-serif;
- padding: 1em 0;
- text-align: center;
-}
-
-.pure-table td,
-.pure-table th {
- border-left: 1px solid #cbcbcb; /* inner column border */
- border-width: 0 0 0 1px;
- font-size: inherit;
- margin: 0;
- overflow: visible; /*to make ths where the title is really long work*/
- padding: 0.5em 1em; /* cell padding */
-}
-
-.pure-table thead {
- background-color: #e0e0e0;
- color: #000;
- text-align: left;
- vertical-align: bottom;
-}
-
-/*
-striping:
- even - #fff (white)
- odd - #f2f2f2 (light gray)
-*/
-.pure-table td {
- background-color: transparent;
-}
-.pure-table-odd td {
- background-color: #f2f2f2;
-}
-
-/* nth-child selector for modern browsers */
-.pure-table-striped tr:nth-child(2n-1) td {
- background-color: #f2f2f2;
-}
-
-/* BORDERED TABLES */
-.pure-table-bordered td {
- border-bottom: 1px solid #cbcbcb;
-}
-.pure-table-bordered tbody > tr:last-child > td {
- border-bottom-width: 0;
-}
-
-/* HORIZONTAL BORDERED TABLES */
-
-.pure-table-horizontal td,
-.pure-table-horizontal th {
- border-width: 0 0 1px 0;
- border-bottom: 1px solid #cbcbcb;
-}
-.pure-table-horizontal tbody > tr:last-child > td {
- border-bottom-width: 0;
-}
diff --git a/packages/demobank-ui/src/settings.ts b/packages/demobank-ui/src/settings.ts
deleted file mode 100644
index a63ce347e..000000000
--- a/packages/demobank-ui/src/settings.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-export interface BankUiSettings {
- allowRegistrations: boolean;
- showDemoNav: boolean;
- bankName: string;
- demoSites: [string, string][];
-}
-
-/**
- * Global settings for the demobank UI.
- */
-const defaultSettings: BankUiSettings = {
- allowRegistrations: true,
- bankName: "Taler Bank",
- showDemoNav: true,
- demoSites: [
- ["Landing", "https://demo.taler.net/"],
- ["Bank", "https://bank.demo.taler.net/"],
- ["Essay Shop", "https://shop.demo.taler.net/"],
- ["Donations", "https://donations.demo.taler.net/"],
- ["Survey", "https://survey.demo.taler.net/"],
- ],
-};
-
-export const bankUiSettings: BankUiSettings =
- "talerDemobankSettings" in globalThis
- ? (globalThis as any).talerDemobankSettings
- : defaultSettings;
diff --git a/packages/demobank-ui/src/style/index.css b/packages/demobank-ui/src/style/index.css
deleted file mode 100644
index e69de29bb..000000000
--- a/packages/demobank-ui/src/style/index.css
+++ /dev/null
diff --git a/packages/demobank-ui/src/utils.ts b/packages/demobank-ui/src/utils.ts
deleted file mode 100644
index f769bca9a..000000000
--- a/packages/demobank-ui/src/utils.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import { canonicalizeBaseUrl } from "@gnu-taler/taler-util";
-
-/**
- * Validate (the number part of) an amount. If needed,
- * replace comma with a dot. Returns 'false' whenever
- * the input is invalid, the valid amount otherwise.
- */
-const amountRegex = /^[0-9]+(.[0-9]+)?$/;
-export function validateAmount(
- maybeAmount: string | undefined,
-): string | undefined {
- if (!maybeAmount || !amountRegex.test(maybeAmount)) {
- return;
- }
- return maybeAmount;
-}
-
-/**
- * Extract IBAN from a Payto URI.
- */
-export function getIbanFromPayto(url: string): string {
- const pathSplit = new URL(url).pathname.split("/");
- let lastIndex = pathSplit.length - 1;
- // Happens if the path ends with "/".
- if (pathSplit[lastIndex] === "") lastIndex--;
- const iban = pathSplit[lastIndex];
- return iban;
-}
-
-const maybeRootPath = "https://bank.demo.taler.net/demobanks/default/";
-
-export function getBankBackendBaseUrl(): string {
- const overrideUrl = localStorage.getItem("bank-base-url");
- return canonicalizeBaseUrl(overrideUrl ? overrideUrl : maybeRootPath);
-}
-
-export function undefinedIfEmpty<T extends object>(obj: T): T | undefined {
- return Object.keys(obj).some((k) => (obj as any)[k] !== undefined)
- ? obj
- : undefined;
-}
-
-/**
- * Craft headers with Authorization and Content-Type.
- */
-export function prepareHeaders(username?: string, password?: string): Headers {
- const headers = new Headers();
- if (username && password) {
- headers.append(
- "Authorization",
- `Basic ${window.btoa(`${username}:${password}`)}`,
- );
- }
- headers.append("Content-Type", "application/json");
- return headers;
-}
diff --git a/packages/idb-bridge/.gitignore b/packages/idb-bridge/.gitignore
index 796b96d1c..4ea7691dc 100644
--- a/packages/idb-bridge/.gitignore
+++ b/packages/idb-bridge/.gitignore
@@ -1 +1,2 @@
/build
+mytestdb.sqlite3
diff --git a/packages/idb-bridge/package-lock.json b/packages/idb-bridge/package-lock.json
deleted file mode 100644
index 13fff38b6..000000000
--- a/packages/idb-bridge/package-lock.json
+++ /dev/null
@@ -1,2595 +0,0 @@
-{
- "name": "@gnu-taler/idb-bridge",
- "version": "0.0.16",
- "lockfileVersion": 1,
- "requires": true,
- "dependencies": {
- "@types/node": {
- "version": "14.14.22",
- "dev": true
- },
- "ava": {
- "version": "3.15.0",
- "dev": true,
- "requires": {
- "@concordance/react": "^2.0.0",
- "acorn": "^8.0.4",
- "acorn-walk": "^8.0.0",
- "ansi-styles": "^5.0.0",
- "arrgv": "^1.0.2",
- "arrify": "^2.0.1",
- "callsites": "^3.1.0",
- "chalk": "^4.1.0",
- "chokidar": "^3.4.3",
- "chunkd": "^2.0.1",
- "ci-info": "^2.0.0",
- "ci-parallel-vars": "^1.0.1",
- "clean-yaml-object": "^0.1.0",
- "cli-cursor": "^3.1.0",
- "cli-truncate": "^2.1.0",
- "code-excerpt": "^3.0.0",
- "common-path-prefix": "^3.0.0",
- "concordance": "^5.0.1",
- "convert-source-map": "^1.7.0",
- "currently-unhandled": "^0.4.1",
- "debug": "^4.3.1",
- "del": "^6.0.0",
- "emittery": "^0.8.0",
- "equal-length": "^1.0.0",
- "figures": "^3.2.0",
- "globby": "^11.0.1",
- "ignore-by-default": "^2.0.0",
- "import-local": "^3.0.2",
- "indent-string": "^4.0.0",
- "is-error": "^2.2.2",
- "is-plain-object": "^5.0.0",
- "is-promise": "^4.0.0",
- "lodash": "^4.17.20",
- "matcher": "^3.0.0",
- "md5-hex": "^3.0.1",
- "mem": "^8.0.0",
- "ms": "^2.1.3",
- "ora": "^5.2.0",
- "p-event": "^4.2.0",
- "p-map": "^4.0.0",
- "picomatch": "^2.2.2",
- "pkg-conf": "^3.1.0",
- "plur": "^4.0.0",
- "pretty-ms": "^7.0.1",
- "read-pkg": "^5.2.0",
- "resolve-cwd": "^3.0.0",
- "slash": "^3.0.0",
- "source-map-support": "^0.5.19",
- "stack-utils": "^2.0.3",
- "strip-ansi": "^6.0.0",
- "supertap": "^2.0.0",
- "temp-dir": "^2.0.0",
- "trim-off-newlines": "^1.0.1",
- "update-notifier": "^5.0.1",
- "write-file-atomic": "^3.0.3",
- "yargs": "^16.2.0"
- },
- "dependencies": {
- "@babel/code-frame": {
- "version": "7.12.13",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz",
- "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==",
- "dev": true,
- "requires": {
- "@babel/highlight": "^7.12.13"
- }
- },
- "@babel/helper-validator-identifier": {
- "version": "7.12.11",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz",
- "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==",
- "dev": true
- },
- "@babel/highlight": {
- "version": "7.12.13",
- "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.12.13.tgz",
- "integrity": "sha512-kocDQvIbgMKlWxXe9fof3TQ+gkIPOUSEYhJjqUjvKMez3krV7vbzYCDq39Oj11UAVK7JqPVGQPlgE85dPNlQww==",
- "dev": true,
- "requires": {
- "@babel/helper-validator-identifier": "^7.12.11",
- "chalk": "^2.0.0",
- "js-tokens": "^4.0.0"
- },
- "dependencies": {
- "ansi-styles": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
- "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
- "dev": true,
- "requires": {
- "color-convert": "^1.9.0"
- }
- },
- "chalk": {
- "version": "2.4.2",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
- "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
- "dev": true,
- "requires": {
- "ansi-styles": "^3.2.1",
- "escape-string-regexp": "^1.0.5",
- "supports-color": "^5.3.0"
- }
- },
- "color-convert": {
- "version": "1.9.3",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
- "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
- "dev": true,
- "requires": {
- "color-name": "1.1.3"
- }
- },
- "color-name": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
- "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
- "dev": true
- },
- "has-flag": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
- "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
- "dev": true
- },
- "supports-color": {
- "version": "5.5.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
- "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
- "dev": true,
- "requires": {
- "has-flag": "^3.0.0"
- }
- }
- }
- },
- "@concordance/react": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/@concordance/react/-/react-2.0.0.tgz",
- "integrity": "sha512-huLSkUuM2/P+U0uy2WwlKuixMsTODD8p4JVQBI4VKeopkiN0C7M3N9XYVawb4M+4spN5RrO/eLhk7KoQX6nsfA==",
- "dev": true,
- "requires": {
- "arrify": "^1.0.1"
- },
- "dependencies": {
- "arrify": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz",
- "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=",
- "dev": true
- }
- }
- },
- "@nodelib/fs.scandir": {
- "version": "2.1.4",
- "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz",
- "integrity": "sha512-33g3pMJk3bg5nXbL/+CY6I2eJDzZAni49PfJnL5fghPTggPvBd/pFNSgJsdAgWptuFu7qq/ERvOYFlhvsLTCKA==",
- "dev": true,
- "requires": {
- "@nodelib/fs.stat": "2.0.4",
- "run-parallel": "^1.1.9"
- }
- },
- "@nodelib/fs.stat": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz",
- "integrity": "sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q==",
- "dev": true
- },
- "@nodelib/fs.walk": {
- "version": "1.2.6",
- "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.6.tgz",
- "integrity": "sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow==",
- "dev": true,
- "requires": {
- "@nodelib/fs.scandir": "2.1.4",
- "fastq": "^1.6.0"
- }
- },
- "@sindresorhus/is": {
- "version": "0.14.0",
- "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
- "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==",
- "dev": true
- },
- "@szmarczak/http-timer": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz",
- "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==",
- "dev": true,
- "requires": {
- "defer-to-connect": "^1.0.1"
- }
- },
- "@types/normalize-package-data": {
- "version": "2.4.0",
- "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz",
- "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==",
- "dev": true
- },
- "acorn": {
- "version": "8.0.5",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.0.5.tgz",
- "integrity": "sha512-v+DieK/HJkJOpFBETDJioequtc3PfxsWMaxIdIwujtF7FEV/MAyDQLlm6/zPvr7Mix07mLh6ccVwIsloceodlg==",
- "dev": true
- },
- "acorn-walk": {
- "version": "8.0.2",
- "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.0.2.tgz",
- "integrity": "sha512-+bpA9MJsHdZ4bgfDcpk0ozQyhhVct7rzOmO0s1IIr0AGGgKBljss8n2zp11rRP2wid5VGeh04CgeKzgat5/25A==",
- "dev": true
- },
- "aggregate-error": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
- "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
- "dev": true,
- "requires": {
- "clean-stack": "^2.0.0",
- "indent-string": "^4.0.0"
- }
- },
- "ansi-align": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz",
- "integrity": "sha512-ZpClVKqXN3RGBmKibdfWzqCY4lnjEuoNzU5T0oEFpfd/z5qJHVarukridD4juLO2FXMiwUQxr9WqQtaYa8XRYw==",
- "dev": true,
- "requires": {
- "string-width": "^3.0.0"
- },
- "dependencies": {
- "ansi-regex": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
- "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
- "dev": true
- },
- "emoji-regex": {
- "version": "7.0.3",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
- "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==",
- "dev": true
- },
- "is-fullwidth-code-point": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
- "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
- "dev": true
- },
- "string-width": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
- "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
- "dev": true,
- "requires": {
- "emoji-regex": "^7.0.1",
- "is-fullwidth-code-point": "^2.0.0",
- "strip-ansi": "^5.1.0"
- }
- },
- "strip-ansi": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
- "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
- "dev": true,
- "requires": {
- "ansi-regex": "^4.1.0"
- }
- }
- }
- },
- "ansi-regex": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
- "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
- "dev": true
- },
- "ansi-styles": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.1.0.tgz",
- "integrity": "sha512-osxifZo3ar56+e8tdYreU6p8FZGciBHo5O0JoDAxMUqZuyNUb+yHEwYtJZ+Z32R459jEgtwVf1u8D7qYwU0l6w==",
- "dev": true
- },
- "anymatch": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz",
- "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==",
- "dev": true,
- "requires": {
- "normalize-path": "^3.0.0",
- "picomatch": "^2.0.4"
- }
- },
- "argparse": {
- "version": "1.0.10",
- "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
- "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
- "dev": true,
- "requires": {
- "sprintf-js": "~1.0.2"
- }
- },
- "array-find-index": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz",
- "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=",
- "dev": true
- },
- "array-union": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
- "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
- "dev": true
- },
- "arrgv": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/arrgv/-/arrgv-1.0.2.tgz",
- "integrity": "sha512-a4eg4yhp7mmruZDQFqVMlxNRFGi/i1r87pt8SDHy0/I8PqSXoUTlWZRdAZo0VXgvEARcujbtTk8kiZRi1uDGRw==",
- "dev": true
- },
- "arrify": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz",
- "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==",
- "dev": true
- },
- "astral-regex": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
- "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
- "dev": true
- },
- "balanced-match": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
- "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
- "dev": true
- },
- "base64-js": {
- "version": "1.5.1",
- "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
- "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
- "dev": true
- },
- "binary-extensions": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
- "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
- "dev": true
- },
- "bl": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
- "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
- "dev": true,
- "requires": {
- "buffer": "^5.5.0",
- "inherits": "^2.0.4",
- "readable-stream": "^3.4.0"
- }
- },
- "blueimp-md5": {
- "version": "2.18.0",
- "resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.18.0.tgz",
- "integrity": "sha512-vE52okJvzsVWhcgUHOv+69OG3Mdg151xyn41aVQN/5W5S+S43qZhxECtYLAEHMSFWX6Mv5IZrzj3T5+JqXfj5Q==",
- "dev": true
- },
- "boxen": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.0.0.tgz",
- "integrity": "sha512-5bvsqw+hhgUi3oYGK0Vf4WpIkyemp60WBInn7+WNfoISzAqk/HX4L7WNROq38E6UR/y3YADpv6pEm4BfkeEAdA==",
- "dev": true,
- "requires": {
- "ansi-align": "^3.0.0",
- "camelcase": "^6.2.0",
- "chalk": "^4.1.0",
- "cli-boxes": "^2.2.1",
- "string-width": "^4.2.0",
- "type-fest": "^0.20.2",
- "widest-line": "^3.1.0",
- "wrap-ansi": "^7.0.0"
- },
- "dependencies": {
- "type-fest": {
- "version": "0.20.2",
- "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
- "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
- "dev": true
- }
- }
- },
- "brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
- "dev": true,
- "requires": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
- "braces": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
- "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
- "dev": true,
- "requires": {
- "fill-range": "^7.0.1"
- }
- },
- "buffer": {
- "version": "5.7.1",
- "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
- "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
- "dev": true,
- "requires": {
- "base64-js": "^1.3.1",
- "ieee754": "^1.1.13"
- }
- },
- "buffer-from": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
- "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
- "dev": true
- },
- "cacheable-request": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz",
- "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==",
- "dev": true,
- "requires": {
- "clone-response": "^1.0.2",
- "get-stream": "^5.1.0",
- "http-cache-semantics": "^4.0.0",
- "keyv": "^3.0.0",
- "lowercase-keys": "^2.0.0",
- "normalize-url": "^4.1.0",
- "responselike": "^1.0.2"
- },
- "dependencies": {
- "get-stream": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
- "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
- "dev": true,
- "requires": {
- "pump": "^3.0.0"
- }
- },
- "lowercase-keys": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
- "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==",
- "dev": true
- }
- }
- },
- "callsites": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
- "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
- "dev": true
- },
- "camelcase": {
- "version": "6.2.0",
- "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz",
- "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==",
- "dev": true
- },
- "chalk": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
- "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
- "dev": true,
- "requires": {
- "ansi-styles": "^4.1.0",
- "supports-color": "^7.1.0"
- },
- "dependencies": {
- "ansi-styles": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
- "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
- "dev": true,
- "requires": {
- "color-convert": "^2.0.1"
- }
- }
- }
- },
- "chokidar": {
- "version": "3.5.1",
- "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz",
- "integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==",
- "dev": true,
- "requires": {
- "anymatch": "~3.1.1",
- "braces": "~3.0.2",
- "fsevents": "~2.3.1",
- "glob-parent": "~5.1.0",
- "is-binary-path": "~2.1.0",
- "is-glob": "~4.0.1",
- "normalize-path": "~3.0.0",
- "readdirp": "~3.5.0"
- }
- },
- "chunkd": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/chunkd/-/chunkd-2.0.1.tgz",
- "integrity": "sha512-7d58XsFmOq0j6el67Ug9mHf9ELUXsQXYJBkyxhH/k+6Ke0qXRnv0kbemx+Twc6fRJ07C49lcbdgm9FL1Ei/6SQ==",
- "dev": true
- },
- "ci-info": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz",
- "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==",
- "dev": true
- },
- "ci-parallel-vars": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/ci-parallel-vars/-/ci-parallel-vars-1.0.1.tgz",
- "integrity": "sha512-uvzpYrpmidaoxvIQHM+rKSrigjOe9feHYbw4uOI2gdfe1C3xIlxO+kVXq83WQWNniTf8bAxVpy+cQeFQsMERKg==",
- "dev": true
- },
- "clean-stack": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
- "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
- "dev": true
- },
- "clean-yaml-object": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/clean-yaml-object/-/clean-yaml-object-0.1.0.tgz",
- "integrity": "sha1-Y/sRDcLOGoTcIfbZM0h20BCui2g=",
- "dev": true
- },
- "cli-boxes": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz",
- "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==",
- "dev": true
- },
- "cli-cursor": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
- "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
- "dev": true,
- "requires": {
- "restore-cursor": "^3.1.0"
- }
- },
- "cli-spinners": {
- "version": "2.5.0",
- "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.5.0.tgz",
- "integrity": "sha512-PC+AmIuK04E6aeSs/pUccSujsTzBhu4HzC2dL+CfJB/Jcc2qTRbEwZQDfIUpt2Xl8BodYBEq8w4fc0kU2I9DjQ==",
- "dev": true
- },
- "cli-truncate": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz",
- "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==",
- "dev": true,
- "requires": {
- "slice-ansi": "^3.0.0",
- "string-width": "^4.2.0"
- }
- },
- "cliui": {
- "version": "7.0.4",
- "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
- "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
- "dev": true,
- "requires": {
- "string-width": "^4.2.0",
- "strip-ansi": "^6.0.0",
- "wrap-ansi": "^7.0.0"
- }
- },
- "clone": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
- "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=",
- "dev": true
- },
- "clone-response": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz",
- "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=",
- "dev": true,
- "requires": {
- "mimic-response": "^1.0.0"
- }
- },
- "code-excerpt": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/code-excerpt/-/code-excerpt-3.0.0.tgz",
- "integrity": "sha512-VHNTVhd7KsLGOqfX3SyeO8RyYPMp1GJOg194VITk04WMYCv4plV68YWe6TJZxd9MhobjtpMRnVky01gqZsalaw==",
- "dev": true,
- "requires": {
- "convert-to-spaces": "^1.0.1"
- }
- },
- "color-convert": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
- "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
- "dev": true,
- "requires": {
- "color-name": "~1.1.4"
- }
- },
- "color-name": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "dev": true
- },
- "common-path-prefix": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz",
- "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==",
- "dev": true
- },
- "concat-map": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
- "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
- "dev": true
- },
- "concordance": {
- "version": "5.0.2",
- "resolved": "https://registry.npmjs.org/concordance/-/concordance-5.0.2.tgz",
- "integrity": "sha512-hC63FKdGM9tBcd4VQIa+LQjmrgorrnxESb8B3J21Qe/FzL0blBv0pb8iNyymt+bmsvGSUqO0uhPi2ZSLgLtLdg==",
- "dev": true,
- "requires": {
- "date-time": "^3.1.0",
- "esutils": "^2.0.3",
- "fast-diff": "^1.2.0",
- "js-string-escape": "^1.0.1",
- "lodash": "^4.17.15",
- "md5-hex": "^3.0.1",
- "semver": "^7.3.2",
- "well-known-symbols": "^2.0.0"
- }
- },
- "configstore": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz",
- "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==",
- "dev": true,
- "requires": {
- "dot-prop": "^5.2.0",
- "graceful-fs": "^4.1.2",
- "make-dir": "^3.0.0",
- "unique-string": "^2.0.0",
- "write-file-atomic": "^3.0.0",
- "xdg-basedir": "^4.0.0"
- }
- },
- "convert-source-map": {
- "version": "1.7.0",
- "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz",
- "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==",
- "dev": true,
- "requires": {
- "safe-buffer": "~5.1.1"
- }
- },
- "convert-to-spaces": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/convert-to-spaces/-/convert-to-spaces-1.0.2.tgz",
- "integrity": "sha1-fj5Iu+bZl7FBfdyihoIEtNPYVxU=",
- "dev": true
- },
- "crypto-random-string": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
- "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==",
- "dev": true
- },
- "currently-unhandled": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",
- "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=",
- "dev": true,
- "requires": {
- "array-find-index": "^1.0.1"
- }
- },
- "date-time": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/date-time/-/date-time-3.1.0.tgz",
- "integrity": "sha512-uqCUKXE5q1PNBXjPqvwhwJf9SwMoAHBgWJ6DcrnS5o+W2JOiIILl0JEdVD8SGujrNS02GGxgwAg2PN2zONgtjg==",
- "dev": true,
- "requires": {
- "time-zone": "^1.0.0"
- }
- },
- "debug": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
- "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
- "dev": true,
- "requires": {
- "ms": "2.1.2"
- },
- "dependencies": {
- "ms": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
- "dev": true
- }
- }
- },
- "decompress-response": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz",
- "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=",
- "dev": true,
- "requires": {
- "mimic-response": "^1.0.0"
- }
- },
- "deep-extend": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
- "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
- "dev": true
- },
- "defaults": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz",
- "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=",
- "dev": true,
- "requires": {
- "clone": "^1.0.2"
- }
- },
- "defer-to-connect": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz",
- "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==",
- "dev": true
- },
- "del": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/del/-/del-6.0.0.tgz",
- "integrity": "sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ==",
- "dev": true,
- "requires": {
- "globby": "^11.0.1",
- "graceful-fs": "^4.2.4",
- "is-glob": "^4.0.1",
- "is-path-cwd": "^2.2.0",
- "is-path-inside": "^3.0.2",
- "p-map": "^4.0.0",
- "rimraf": "^3.0.2",
- "slash": "^3.0.0"
- }
- },
- "dir-glob": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
- "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
- "dev": true,
- "requires": {
- "path-type": "^4.0.0"
- }
- },
- "dot-prop": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz",
- "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==",
- "dev": true,
- "requires": {
- "is-obj": "^2.0.0"
- }
- },
- "duplexer3": {
- "version": "0.1.4",
- "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz",
- "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=",
- "dev": true
- },
- "emittery": {
- "version": "0.8.1",
- "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.8.1.tgz",
- "integrity": "sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg==",
- "dev": true
- },
- "emoji-regex": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
- "dev": true
- },
- "end-of-stream": {
- "version": "1.4.4",
- "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
- "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
- "dev": true,
- "requires": {
- "once": "^1.4.0"
- }
- },
- "equal-length": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/equal-length/-/equal-length-1.0.1.tgz",
- "integrity": "sha1-IcoRLUirJLTh5//A5TOdMf38J0w=",
- "dev": true
- },
- "error-ex": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
- "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
- "dev": true,
- "requires": {
- "is-arrayish": "^0.2.1"
- }
- },
- "escalade": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
- "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
- "dev": true
- },
- "escape-goat": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz",
- "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==",
- "dev": true
- },
- "escape-string-regexp": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
- "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
- "dev": true
- },
- "esprima": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
- "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
- "dev": true
- },
- "esutils": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
- "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
- "dev": true
- },
- "fast-diff": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz",
- "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==",
- "dev": true
- },
- "fast-glob": {
- "version": "3.2.5",
- "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.5.tgz",
- "integrity": "sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg==",
- "dev": true,
- "requires": {
- "@nodelib/fs.stat": "^2.0.2",
- "@nodelib/fs.walk": "^1.2.3",
- "glob-parent": "^5.1.0",
- "merge2": "^1.3.0",
- "micromatch": "^4.0.2",
- "picomatch": "^2.2.1"
- }
- },
- "fastq": {
- "version": "1.10.1",
- "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.10.1.tgz",
- "integrity": "sha512-AWuv6Ery3pM+dY7LYS8YIaCiQvUaos9OB1RyNgaOWnaX+Tik7Onvcsf8x8c+YtDeT0maYLniBip2hox5KtEXXA==",
- "dev": true,
- "requires": {
- "reusify": "^1.0.4"
- }
- },
- "figures": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
- "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==",
- "dev": true,
- "requires": {
- "escape-string-regexp": "^1.0.5"
- }
- },
- "fill-range": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
- "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
- "dev": true,
- "requires": {
- "to-regex-range": "^5.0.1"
- }
- },
- "find-up": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
- "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
- "dev": true,
- "requires": {
- "locate-path": "^5.0.0",
- "path-exists": "^4.0.0"
- }
- },
- "fs.realpath": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
- "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
- "dev": true
- },
- "fsevents": {
- "version": "2.3.2",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
- "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
- "dev": true,
- "optional": true
- },
- "function-bind": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
- "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
- "dev": true
- },
- "get-caller-file": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
- "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
- "dev": true
- },
- "get-stream": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
- "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==",
- "dev": true,
- "requires": {
- "pump": "^3.0.0"
- }
- },
- "glob": {
- "version": "7.1.6",
- "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
- "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
- "dev": true,
- "requires": {
- "fs.realpath": "^1.0.0",
- "inflight": "^1.0.4",
- "inherits": "2",
- "minimatch": "^3.0.4",
- "once": "^1.3.0",
- "path-is-absolute": "^1.0.0"
- }
- },
- "glob-parent": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz",
- "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==",
- "dev": true,
- "requires": {
- "is-glob": "^4.0.1"
- }
- },
- "global-dirs": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.0.tgz",
- "integrity": "sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA==",
- "dev": true,
- "requires": {
- "ini": "2.0.0"
- }
- },
- "globby": {
- "version": "11.0.2",
- "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.2.tgz",
- "integrity": "sha512-2ZThXDvvV8fYFRVIxnrMQBipZQDr7MxKAmQK1vujaj9/7eF0efG7BPUKJ7jP7G5SLF37xKDXvO4S/KKLj/Z0og==",
- "dev": true,
- "requires": {
- "array-union": "^2.1.0",
- "dir-glob": "^3.0.1",
- "fast-glob": "^3.1.1",
- "ignore": "^5.1.4",
- "merge2": "^1.3.0",
- "slash": "^3.0.0"
- }
- },
- "got": {
- "version": "9.6.0",
- "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz",
- "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==",
- "dev": true,
- "requires": {
- "@sindresorhus/is": "^0.14.0",
- "@szmarczak/http-timer": "^1.1.2",
- "cacheable-request": "^6.0.0",
- "decompress-response": "^3.3.0",
- "duplexer3": "^0.1.4",
- "get-stream": "^4.1.0",
- "lowercase-keys": "^1.0.1",
- "mimic-response": "^1.0.1",
- "p-cancelable": "^1.0.0",
- "to-readable-stream": "^1.0.0",
- "url-parse-lax": "^3.0.0"
- }
- },
- "graceful-fs": {
- "version": "4.2.6",
- "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz",
- "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==",
- "dev": true
- },
- "has": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
- "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
- "dev": true,
- "requires": {
- "function-bind": "^1.1.1"
- }
- },
- "has-flag": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
- "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
- "dev": true
- },
- "has-yarn": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz",
- "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==",
- "dev": true
- },
- "hosted-git-info": {
- "version": "2.8.8",
- "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
- "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==",
- "dev": true
- },
- "http-cache-semantics": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz",
- "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==",
- "dev": true
- },
- "ieee754": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
- "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
- "dev": true
- },
- "ignore": {
- "version": "5.1.8",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz",
- "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==",
- "dev": true
- },
- "ignore-by-default": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-2.0.0.tgz",
- "integrity": "sha512-+mQSgMRiFD3L3AOxLYOCxjIq4OnAmo5CIuC+lj5ehCJcPtV++QacEV7FdpzvYxH6DaOySWzQU6RR0lPLy37ckA==",
- "dev": true
- },
- "import-lazy": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz",
- "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=",
- "dev": true
- },
- "import-local": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.2.tgz",
- "integrity": "sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA==",
- "dev": true,
- "requires": {
- "pkg-dir": "^4.2.0",
- "resolve-cwd": "^3.0.0"
- }
- },
- "imurmurhash": {
- "version": "0.1.4",
- "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
- "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
- "dev": true
- },
- "indent-string": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
- "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
- "dev": true
- },
- "inflight": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
- "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
- "dev": true,
- "requires": {
- "once": "^1.3.0",
- "wrappy": "1"
- }
- },
- "inherits": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
- "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
- "dev": true
- },
- "ini": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz",
- "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==",
- "dev": true
- },
- "irregular-plurals": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-3.2.0.tgz",
- "integrity": "sha512-YqTdPLfwP7YFN0SsD3QUVCkm9ZG2VzOXv3DOrw5G5mkMbVwptTwVcFv7/C0vOpBmgTxAeTG19XpUs1E522LW9Q==",
- "dev": true
- },
- "is-arrayish": {
- "version": "0.2.1",
- "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
- "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=",
- "dev": true
- },
- "is-binary-path": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
- "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
- "dev": true,
- "requires": {
- "binary-extensions": "^2.0.0"
- }
- },
- "is-ci": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz",
- "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==",
- "dev": true,
- "requires": {
- "ci-info": "^2.0.0"
- }
- },
- "is-core-module": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.2.0.tgz",
- "integrity": "sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==",
- "dev": true,
- "requires": {
- "has": "^1.0.3"
- }
- },
- "is-error": {
- "version": "2.2.2",
- "resolved": "https://registry.npmjs.org/is-error/-/is-error-2.2.2.tgz",
- "integrity": "sha512-IOQqts/aHWbiisY5DuPJQ0gcbvaLFCa7fBa9xoLfxBZvQ+ZI/Zh9xoI7Gk+G64N0FdK4AbibytHht2tWgpJWLg==",
- "dev": true
- },
- "is-extglob": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
- "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
- "dev": true
- },
- "is-fullwidth-code-point": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
- "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
- "dev": true
- },
- "is-glob": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
- "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
- "dev": true,
- "requires": {
- "is-extglob": "^2.1.1"
- }
- },
- "is-installed-globally": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz",
- "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==",
- "dev": true,
- "requires": {
- "global-dirs": "^3.0.0",
- "is-path-inside": "^3.0.2"
- }
- },
- "is-interactive": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz",
- "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==",
- "dev": true
- },
- "is-npm": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-5.0.0.tgz",
- "integrity": "sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA==",
- "dev": true
- },
- "is-number": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
- "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
- "dev": true
- },
- "is-obj": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz",
- "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==",
- "dev": true
- },
- "is-path-cwd": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz",
- "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==",
- "dev": true
- },
- "is-path-inside": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.2.tgz",
- "integrity": "sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg==",
- "dev": true
- },
- "is-plain-object": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
- "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
- "dev": true
- },
- "is-promise": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
- "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
- "dev": true
- },
- "is-typedarray": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
- "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=",
- "dev": true
- },
- "is-yarn-global": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz",
- "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==",
- "dev": true
- },
- "js-string-escape": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz",
- "integrity": "sha1-4mJbrbwNZ8dTPp7cEGjFh65BN+8=",
- "dev": true
- },
- "js-tokens": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
- "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
- "dev": true
- },
- "js-yaml": {
- "version": "3.14.1",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
- "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
- "dev": true,
- "requires": {
- "argparse": "^1.0.7",
- "esprima": "^4.0.0"
- }
- },
- "json-buffer": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz",
- "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=",
- "dev": true
- },
- "json-parse-better-errors": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
- "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==",
- "dev": true
- },
- "json-parse-even-better-errors": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
- "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
- "dev": true
- },
- "keyv": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz",
- "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==",
- "dev": true,
- "requires": {
- "json-buffer": "3.0.0"
- }
- },
- "latest-version": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz",
- "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==",
- "dev": true,
- "requires": {
- "package-json": "^6.3.0"
- }
- },
- "lines-and-columns": {
- "version": "1.1.6",
- "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz",
- "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=",
- "dev": true
- },
- "load-json-file": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-5.3.0.tgz",
- "integrity": "sha512-cJGP40Jc/VXUsp8/OrnyKyTZ1y6v/dphm3bioS+RrKXjK2BB6wHUd6JptZEFDGgGahMT+InnZO5i1Ei9mpC8Bw==",
- "dev": true,
- "requires": {
- "graceful-fs": "^4.1.15",
- "parse-json": "^4.0.0",
- "pify": "^4.0.1",
- "strip-bom": "^3.0.0",
- "type-fest": "^0.3.0"
- }
- },
- "locate-path": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
- "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
- "dev": true,
- "requires": {
- "p-locate": "^4.1.0"
- }
- },
- "lodash": {
- "version": "4.17.21",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
- "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
- "dev": true
- },
- "log-symbols": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz",
- "integrity": "sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==",
- "dev": true,
- "requires": {
- "chalk": "^4.0.0"
- }
- },
- "lowercase-keys": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz",
- "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==",
- "dev": true
- },
- "lru-cache": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
- "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
- "dev": true,
- "requires": {
- "yallist": "^4.0.0"
- }
- },
- "make-dir": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
- "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
- "dev": true,
- "requires": {
- "semver": "^6.0.0"
- },
- "dependencies": {
- "semver": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
- "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
- "dev": true
- }
- }
- },
- "map-age-cleaner": {
- "version": "0.1.3",
- "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz",
- "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==",
- "dev": true,
- "requires": {
- "p-defer": "^1.0.0"
- }
- },
- "matcher": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
- "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==",
- "dev": true,
- "requires": {
- "escape-string-regexp": "^4.0.0"
- },
- "dependencies": {
- "escape-string-regexp": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
- "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
- "dev": true
- }
- }
- },
- "md5-hex": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/md5-hex/-/md5-hex-3.0.1.tgz",
- "integrity": "sha512-BUiRtTtV39LIJwinWBjqVsU9xhdnz7/i889V859IBFpuqGAj6LuOvHv5XLbgZ2R7ptJoJaEcxkv88/h25T7Ciw==",
- "dev": true,
- "requires": {
- "blueimp-md5": "^2.10.0"
- }
- },
- "mem": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/mem/-/mem-8.0.0.tgz",
- "integrity": "sha512-qrcJOe6uD+EW8Wrci1Vdiua/15Xw3n/QnaNXE7varnB6InxSk7nu3/i5jfy3S6kWxr8WYJ6R1o0afMUtvorTsA==",
- "dev": true,
- "requires": {
- "map-age-cleaner": "^0.1.3",
- "mimic-fn": "^3.1.0"
- },
- "dependencies": {
- "mimic-fn": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz",
- "integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==",
- "dev": true
- }
- }
- },
- "merge2": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
- "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
- "dev": true
- },
- "micromatch": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz",
- "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==",
- "dev": true,
- "requires": {
- "braces": "^3.0.1",
- "picomatch": "^2.0.5"
- }
- },
- "mimic-fn": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
- "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
- "dev": true
- },
- "mimic-response": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
- "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
- "dev": true
- },
- "minimatch": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
- "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
- "dev": true,
- "requires": {
- "brace-expansion": "^1.1.7"
- }
- },
- "minimist": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
- "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
- "dev": true
- },
- "ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true
- },
- "normalize-package-data": {
- "version": "2.5.0",
- "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
- "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
- "dev": true,
- "requires": {
- "hosted-git-info": "^2.1.4",
- "resolve": "^1.10.0",
- "semver": "2 || 3 || 4 || 5",
- "validate-npm-package-license": "^3.0.1"
- },
- "dependencies": {
- "semver": {
- "version": "5.7.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
- "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
- "dev": true
- }
- }
- },
- "normalize-path": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
- "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
- "dev": true
- },
- "normalize-url": {
- "version": "4.5.0",
- "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz",
- "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==",
- "dev": true
- },
- "once": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
- "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
- "dev": true,
- "requires": {
- "wrappy": "1"
- }
- },
- "onetime": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
- "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
- "dev": true,
- "requires": {
- "mimic-fn": "^2.1.0"
- }
- },
- "ora": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/ora/-/ora-5.3.0.tgz",
- "integrity": "sha512-zAKMgGXUim0Jyd6CXK9lraBnD3H5yPGBPPOkC23a2BG6hsm4Zu6OQSjQuEtV0BHDf4aKHcUFvJiGRrFuW3MG8g==",
- "dev": true,
- "requires": {
- "bl": "^4.0.3",
- "chalk": "^4.1.0",
- "cli-cursor": "^3.1.0",
- "cli-spinners": "^2.5.0",
- "is-interactive": "^1.0.0",
- "log-symbols": "^4.0.0",
- "strip-ansi": "^6.0.0",
- "wcwidth": "^1.0.1"
- }
- },
- "p-cancelable": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz",
- "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==",
- "dev": true
- },
- "p-defer": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz",
- "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=",
- "dev": true
- },
- "p-event": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/p-event/-/p-event-4.2.0.tgz",
- "integrity": "sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ==",
- "dev": true,
- "requires": {
- "p-timeout": "^3.1.0"
- }
- },
- "p-finally": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
- "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=",
- "dev": true
- },
- "p-limit": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
- "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
- "dev": true,
- "requires": {
- "p-try": "^2.0.0"
- }
- },
- "p-locate": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
- "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
- "dev": true,
- "requires": {
- "p-limit": "^2.2.0"
- }
- },
- "p-map": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz",
- "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==",
- "dev": true,
- "requires": {
- "aggregate-error": "^3.0.0"
- }
- },
- "p-timeout": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz",
- "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==",
- "dev": true,
- "requires": {
- "p-finally": "^1.0.0"
- }
- },
- "p-try": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
- "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
- "dev": true
- },
- "package-json": {
- "version": "6.5.0",
- "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz",
- "integrity": "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==",
- "dev": true,
- "requires": {
- "got": "^9.6.0",
- "registry-auth-token": "^4.0.0",
- "registry-url": "^5.0.0",
- "semver": "^6.2.0"
- },
- "dependencies": {
- "semver": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
- "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
- "dev": true
- }
- }
- },
- "parse-json": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
- "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=",
- "dev": true,
- "requires": {
- "error-ex": "^1.3.1",
- "json-parse-better-errors": "^1.0.1"
- }
- },
- "parse-ms": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.1.0.tgz",
- "integrity": "sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==",
- "dev": true
- },
- "path-exists": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
- "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
- "dev": true
- },
- "path-is-absolute": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
- "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
- "dev": true
- },
- "path-parse": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
- "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
- "dev": true
- },
- "path-type": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
- "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
- "dev": true
- },
- "picomatch": {
- "version": "2.2.2",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz",
- "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==",
- "dev": true
- },
- "pify": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
- "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
- "dev": true
- },
- "pkg-conf": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/pkg-conf/-/pkg-conf-3.1.0.tgz",
- "integrity": "sha512-m0OTbR/5VPNPqO1ph6Fqbj7Hv6QU7gR/tQW40ZqrL1rjgCU85W6C1bJn0BItuJqnR98PWzw7Z8hHeChD1WrgdQ==",
- "dev": true,
- "requires": {
- "find-up": "^3.0.0",
- "load-json-file": "^5.2.0"
- },
- "dependencies": {
- "find-up": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
- "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
- "dev": true,
- "requires": {
- "locate-path": "^3.0.0"
- }
- },
- "locate-path": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
- "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
- "dev": true,
- "requires": {
- "p-locate": "^3.0.0",
- "path-exists": "^3.0.0"
- }
- },
- "p-locate": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
- "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
- "dev": true,
- "requires": {
- "p-limit": "^2.0.0"
- }
- },
- "path-exists": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
- "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
- "dev": true
- }
- }
- },
- "pkg-dir": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
- "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
- "dev": true,
- "requires": {
- "find-up": "^4.0.0"
- }
- },
- "plur": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/plur/-/plur-4.0.0.tgz",
- "integrity": "sha512-4UGewrYgqDFw9vV6zNV+ADmPAUAfJPKtGvb/VdpQAx25X5f3xXdGdyOEVFwkl8Hl/tl7+xbeHqSEM+D5/TirUg==",
- "dev": true,
- "requires": {
- "irregular-plurals": "^3.2.0"
- }
- },
- "prepend-http": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz",
- "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=",
- "dev": true
- },
- "pretty-ms": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-7.0.1.tgz",
- "integrity": "sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==",
- "dev": true,
- "requires": {
- "parse-ms": "^2.1.0"
- }
- },
- "pump": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
- "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
- "dev": true,
- "requires": {
- "end-of-stream": "^1.1.0",
- "once": "^1.3.1"
- }
- },
- "pupa": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz",
- "integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==",
- "dev": true,
- "requires": {
- "escape-goat": "^2.0.0"
- }
- },
- "queue-microtask": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.2.tgz",
- "integrity": "sha512-dB15eXv3p2jDlbOiNLyMabYg1/sXvppd8DP2J3EOCQ0AkuSXCW2tP7mnVouVLJKgUMY6yP0kcQDVpLCN13h4Xg==",
- "dev": true
- },
- "rc": {
- "version": "1.2.8",
- "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
- "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
- "dev": true,
- "requires": {
- "deep-extend": "^0.6.0",
- "ini": "~1.3.0",
- "minimist": "^1.2.0",
- "strip-json-comments": "~2.0.1"
- },
- "dependencies": {
- "ini": {
- "version": "1.3.8",
- "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
- "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
- "dev": true
- }
- }
- },
- "read-pkg": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",
- "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==",
- "dev": true,
- "requires": {
- "@types/normalize-package-data": "^2.4.0",
- "normalize-package-data": "^2.5.0",
- "parse-json": "^5.0.0",
- "type-fest": "^0.6.0"
- },
- "dependencies": {
- "parse-json": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
- "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
- "dev": true,
- "requires": {
- "@babel/code-frame": "^7.0.0",
- "error-ex": "^1.3.1",
- "json-parse-even-better-errors": "^2.3.0",
- "lines-and-columns": "^1.1.6"
- }
- },
- "type-fest": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz",
- "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==",
- "dev": true
- }
- }
- },
- "readable-stream": {
- "version": "3.6.0",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
- "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
- "dev": true,
- "requires": {
- "inherits": "^2.0.3",
- "string_decoder": "^1.1.1",
- "util-deprecate": "^1.0.1"
- }
- },
- "readdirp": {
- "version": "3.5.0",
- "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz",
- "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==",
- "dev": true,
- "requires": {
- "picomatch": "^2.2.1"
- }
- },
- "registry-auth-token": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.1.tgz",
- "integrity": "sha512-6gkSb4U6aWJB4SF2ZvLb76yCBjcvufXBqvvEx1HbmKPkutswjW1xNVRY0+daljIYRbogN7O0etYSlbiaEQyMyw==",
- "dev": true,
- "requires": {
- "rc": "^1.2.8"
- }
- },
- "registry-url": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz",
- "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==",
- "dev": true,
- "requires": {
- "rc": "^1.2.8"
- }
- },
- "require-directory": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
- "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
- "dev": true
- },
- "resolve": {
- "version": "1.20.0",
- "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz",
- "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==",
- "dev": true,
- "requires": {
- "is-core-module": "^2.2.0",
- "path-parse": "^1.0.6"
- }
- },
- "resolve-cwd": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz",
- "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==",
- "dev": true,
- "requires": {
- "resolve-from": "^5.0.0"
- }
- },
- "resolve-from": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
- "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
- "dev": true
- },
- "responselike": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz",
- "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=",
- "dev": true,
- "requires": {
- "lowercase-keys": "^1.0.0"
- }
- },
- "restore-cursor": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
- "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
- "dev": true,
- "requires": {
- "onetime": "^5.1.0",
- "signal-exit": "^3.0.2"
- }
- },
- "reusify": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
- "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
- "dev": true
- },
- "rimraf": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
- "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
- "dev": true,
- "requires": {
- "glob": "^7.1.3"
- }
- },
- "run-parallel": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
- "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
- "dev": true,
- "requires": {
- "queue-microtask": "^1.2.2"
- }
- },
- "safe-buffer": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
- "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
- "dev": true
- },
- "semver": {
- "version": "7.3.4",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz",
- "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==",
- "dev": true,
- "requires": {
- "lru-cache": "^6.0.0"
- }
- },
- "semver-diff": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz",
- "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==",
- "dev": true,
- "requires": {
- "semver": "^6.3.0"
- },
- "dependencies": {
- "semver": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
- "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
- "dev": true
- }
- }
- },
- "serialize-error": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz",
- "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==",
- "dev": true,
- "requires": {
- "type-fest": "^0.13.1"
- },
- "dependencies": {
- "type-fest": {
- "version": "0.13.1",
- "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
- "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==",
- "dev": true
- }
- }
- },
- "signal-exit": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz",
- "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==",
- "dev": true
- },
- "slash": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
- "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
- "dev": true
- },
- "slice-ansi": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz",
- "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==",
- "dev": true,
- "requires": {
- "ansi-styles": "^4.0.0",
- "astral-regex": "^2.0.0",
- "is-fullwidth-code-point": "^3.0.0"
- },
- "dependencies": {
- "ansi-styles": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
- "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
- "dev": true,
- "requires": {
- "color-convert": "^2.0.1"
- }
- }
- }
- },
- "source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true
- },
- "source-map-support": {
- "version": "0.5.19",
- "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz",
- "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==",
- "dev": true,
- "requires": {
- "buffer-from": "^1.0.0",
- "source-map": "^0.6.0"
- }
- },
- "spdx-correct": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz",
- "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==",
- "dev": true,
- "requires": {
- "spdx-expression-parse": "^3.0.0",
- "spdx-license-ids": "^3.0.0"
- }
- },
- "spdx-exceptions": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz",
- "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==",
- "dev": true
- },
- "spdx-expression-parse": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
- "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
- "dev": true,
- "requires": {
- "spdx-exceptions": "^2.1.0",
- "spdx-license-ids": "^3.0.0"
- }
- },
- "spdx-license-ids": {
- "version": "3.0.7",
- "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz",
- "integrity": "sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ==",
- "dev": true
- },
- "sprintf-js": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
- "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
- "dev": true
- },
- "stack-utils": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.3.tgz",
- "integrity": "sha512-gL//fkxfWUsIlFL2Tl42Cl6+HFALEaB1FU76I/Fy+oZjRreP7OPMXFlGbxM7NQsI0ZpUfw76sHnv0WNYuTb7Iw==",
- "dev": true,
- "requires": {
- "escape-string-regexp": "^2.0.0"
- },
- "dependencies": {
- "escape-string-regexp": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
- "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
- "dev": true
- }
- }
- },
- "string-width": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
- "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==",
- "dev": true,
- "requires": {
- "emoji-regex": "^8.0.0",
- "is-fullwidth-code-point": "^3.0.0",
- "strip-ansi": "^6.0.0"
- }
- },
- "string_decoder": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
- "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
- "dev": true,
- "requires": {
- "safe-buffer": "~5.2.0"
- },
- "dependencies": {
- "safe-buffer": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
- "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
- "dev": true
- }
- }
- },
- "strip-ansi": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
- "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
- "dev": true,
- "requires": {
- "ansi-regex": "^5.0.0"
- }
- },
- "strip-bom": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
- "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=",
- "dev": true
- },
- "strip-json-comments": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
- "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
- "dev": true
- },
- "supertap": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/supertap/-/supertap-2.0.0.tgz",
- "integrity": "sha512-jRzcXlCeDYvKoZGA5oRhYyR3jUIYu0enkSxtmAgHRlD7HwrovTpH4bDSi0py9FtuA8si9cW/fKommJHuaoDHJA==",
- "dev": true,
- "requires": {
- "arrify": "^2.0.1",
- "indent-string": "^4.0.0",
- "js-yaml": "^3.14.0",
- "serialize-error": "^7.0.1",
- "strip-ansi": "^6.0.0"
- }
- },
- "supports-color": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
- "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
- "dev": true,
- "requires": {
- "has-flag": "^4.0.0"
- }
- },
- "temp-dir": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz",
- "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==",
- "dev": true
- },
- "time-zone": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/time-zone/-/time-zone-1.0.0.tgz",
- "integrity": "sha1-mcW/VZWJZq9tBtg73zgA3IL67F0=",
- "dev": true
- },
- "to-readable-stream": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz",
- "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==",
- "dev": true
- },
- "to-regex-range": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
- "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
- "dev": true,
- "requires": {
- "is-number": "^7.0.0"
- }
- },
- "trim-off-newlines": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/trim-off-newlines/-/trim-off-newlines-1.0.1.tgz",
- "integrity": "sha1-n5up2e+odkw4dpi8v+sshI8RrbM=",
- "dev": true
- },
- "type-fest": {
- "version": "0.3.1",
- "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz",
- "integrity": "sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==",
- "dev": true
- },
- "typedarray-to-buffer": {
- "version": "3.1.5",
- "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
- "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
- "dev": true,
- "requires": {
- "is-typedarray": "^1.0.0"
- }
- },
- "unique-string": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz",
- "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==",
- "dev": true,
- "requires": {
- "crypto-random-string": "^2.0.0"
- }
- },
- "update-notifier": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-5.1.0.tgz",
- "integrity": "sha512-ItnICHbeMh9GqUy31hFPrD1kcuZ3rpxDZbf4KUDavXwS0bW5m7SLbDQpGX3UYr072cbrF5hFUs3r5tUsPwjfHw==",
- "dev": true,
- "requires": {
- "boxen": "^5.0.0",
- "chalk": "^4.1.0",
- "configstore": "^5.0.1",
- "has-yarn": "^2.1.0",
- "import-lazy": "^2.1.0",
- "is-ci": "^2.0.0",
- "is-installed-globally": "^0.4.0",
- "is-npm": "^5.0.0",
- "is-yarn-global": "^0.3.0",
- "latest-version": "^5.1.0",
- "pupa": "^2.1.1",
- "semver": "^7.3.4",
- "semver-diff": "^3.1.1",
- "xdg-basedir": "^4.0.0"
- }
- },
- "url-parse-lax": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz",
- "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=",
- "dev": true,
- "requires": {
- "prepend-http": "^2.0.0"
- }
- },
- "util-deprecate": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
- "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
- "dev": true
- },
- "validate-npm-package-license": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
- "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
- "dev": true,
- "requires": {
- "spdx-correct": "^3.0.0",
- "spdx-expression-parse": "^3.0.0"
- }
- },
- "wcwidth": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
- "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=",
- "dev": true,
- "requires": {
- "defaults": "^1.0.3"
- }
- },
- "well-known-symbols": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/well-known-symbols/-/well-known-symbols-2.0.0.tgz",
- "integrity": "sha512-ZMjC3ho+KXo0BfJb7JgtQ5IBuvnShdlACNkKkdsqBmYw3bPAaJfPeYUo6tLUaT5tG/Gkh7xkpBhKRQ9e7pyg9Q==",
- "dev": true
- },
- "widest-line": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz",
- "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==",
- "dev": true,
- "requires": {
- "string-width": "^4.0.0"
- }
- },
- "wrap-ansi": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
- "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
- "dev": true,
- "requires": {
- "ansi-styles": "^4.0.0",
- "string-width": "^4.1.0",
- "strip-ansi": "^6.0.0"
- },
- "dependencies": {
- "ansi-styles": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
- "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
- "dev": true,
- "requires": {
- "color-convert": "^2.0.1"
- }
- }
- }
- },
- "wrappy": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
- "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
- "dev": true
- },
- "write-file-atomic": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz",
- "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==",
- "dev": true,
- "requires": {
- "imurmurhash": "^0.1.4",
- "is-typedarray": "^1.0.0",
- "signal-exit": "^3.0.2",
- "typedarray-to-buffer": "^3.1.5"
- }
- },
- "xdg-basedir": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz",
- "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==",
- "dev": true
- },
- "y18n": {
- "version": "5.0.5",
- "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.5.tgz",
- "integrity": "sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg==",
- "dev": true
- },
- "yallist": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
- "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
- "dev": true
- },
- "yargs": {
- "version": "16.2.0",
- "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
- "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
- "dev": true,
- "requires": {
- "cliui": "^7.0.2",
- "escalade": "^3.1.1",
- "get-caller-file": "^2.0.5",
- "require-directory": "^2.1.1",
- "string-width": "^4.2.0",
- "y18n": "^5.0.5",
- "yargs-parser": "^20.2.2"
- }
- },
- "yargs-parser": {
- "version": "20.2.6",
- "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.6.tgz",
- "integrity": "sha512-AP1+fQIWSM/sMiET8fyayjx/J+JmTPt2Mr0FkrgqB4todtfa53sOsrSAcIrJRD5XS20bKUwaDIuMkWKCEiQLKA==",
- "dev": true
- }
- }
- },
- "esm": {
- "version": "3.2.25",
- "dev": true
- },
- "prettier": {
- "version": "2.2.1",
- "dev": true
- },
- "rimraf": {
- "version": "3.0.2",
- "dev": true,
- "requires": {
- "glob": "^7.1.3"
- },
- "dependencies": {
- "balanced-match": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
- "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
- "dev": true
- },
- "brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
- "dev": true,
- "requires": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
- "concat-map": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
- "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
- "dev": true
- },
- "fs.realpath": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
- "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
- "dev": true
- },
- "glob": {
- "version": "7.1.6",
- "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
- "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
- "dev": true,
- "requires": {
- "fs.realpath": "^1.0.0",
- "inflight": "^1.0.4",
- "inherits": "2",
- "minimatch": "^3.0.4",
- "once": "^1.3.0",
- "path-is-absolute": "^1.0.0"
- }
- },
- "inflight": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
- "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
- "dev": true,
- "requires": {
- "once": "^1.3.0",
- "wrappy": "1"
- }
- },
- "inherits": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
- "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
- "dev": true
- },
- "minimatch": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
- "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
- "dev": true,
- "requires": {
- "brace-expansion": "^1.1.7"
- }
- },
- "once": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
- "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
- "dev": true,
- "requires": {
- "wrappy": "1"
- }
- },
- "path-is-absolute": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
- "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
- "dev": true
- },
- "wrappy": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
- "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
- "dev": true
- }
- }
- },
- "rollup": {
- "version": "2.37.1",
- "dev": true,
- "requires": {
- "fsevents": "~2.1.2"
- },
- "dependencies": {
- "fsevents": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz",
- "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==",
- "dev": true,
- "optional": true
- }
- }
- },
- "tslib": {
- "version": "2.1.0"
- },
- "typescript": {
- "version": "4.1.3",
- "dev": true
- },
- "typeson": {
- "version": "5.18.2"
- },
- "typeson-registry": {
- "version": "1.0.0-alpha.38",
- "requires": {
- "base64-arraybuffer-es6": "^0.6.0",
- "typeson": "^5.18.2",
- "whatwg-url": "^8.1.0"
- },
- "dependencies": {
- "base64-arraybuffer-es6": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/base64-arraybuffer-es6/-/base64-arraybuffer-es6-0.6.0.tgz",
- "integrity": "sha512-57nLqKj4ShsDwFJWJsM4sZx6u60WbCge35rWRSevUwqxDtRwwxiKAO800zD2upPv4CfdWjQp//wSLar35nDKvA=="
- },
- "lodash.sortby": {
- "version": "4.7.0",
- "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
- "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg="
- },
- "punycode": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
- "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
- },
- "tr46": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.0.2.tgz",
- "integrity": "sha512-3n1qG+/5kg+jrbTzwAykB5yRYtQCTqOGKq5U5PE3b0a1/mzo6snDhjGS0zJVJunO0NrT3Dg1MLy5TjWP/UJppg==",
- "requires": {
- "punycode": "^2.1.1"
- }
- },
- "typeson": {
- "version": "5.18.2",
- "resolved": "https://registry.npmjs.org/typeson/-/typeson-5.18.2.tgz",
- "integrity": "sha512-Vetd+OGX05P4qHyHiSLdHZ5Z5GuQDrHHwSdjkqho9NSCYVSLSfRMjklD/unpHH8tXBR9Z/R05rwJSuMpMFrdsw=="
- },
- "webidl-conversions": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz",
- "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w=="
- },
- "whatwg-url": {
- "version": "8.4.0",
- "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.4.0.tgz",
- "integrity": "sha512-vwTUFf6V4zhcPkWp/4CQPr1TW9Ml6SF4lVyaIMBdJw5i6qUUJ1QWM4Z6YYVkfka0OUIzVo/0aNtGVGk256IKWw==",
- "requires": {
- "lodash.sortby": "^4.7.0",
- "tr46": "^2.0.2",
- "webidl-conversions": "^6.1.0"
- }
- }
- }
- }
- }
-}
diff --git a/packages/idb-bridge/package.json b/packages/idb-bridge/package.json
index b93ca9f0d..376265c0f 100644
--- a/packages/idb-bridge/package.json
+++ b/packages/idb-bridge/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/idb-bridge",
- "version": "0.0.16",
+ "version": "0.10.7",
"description": "IndexedDB implementation that uses SQLite3 as storage",
"main": "./dist/idb-bridge.js",
"module": "./lib/index.js",
@@ -11,30 +11,33 @@
"private": false,
"scripts": {
"test": "tsc && ava",
- "prepare": "tsc",
+ "typedoc": "typedoc --out dist/typedoc ./src/",
"compile": "tsc",
- "clean": "rimraf dist lib tsconfig.tsbuildinfo",
+ "clean": "rm -rf dist lib tsconfig.tsbuildinfo",
"pretty": "prettier --write src"
},
"exports": {
".": {
"default": "./lib/index.js"
+ },
+ "./node-sqlite3-bindings": {
+ "default": "./lib/node-sqlite3-impl.js"
}
},
"devDependencies": {
- "@types/node": "^18.8.5",
- "ava": "^4.3.3",
- "esm": "^3.2.25",
- "prettier": "^2.5.1",
- "rimraf": "^3.0.2",
- "typescript": "^4.8.4"
+ "@types/better-sqlite3": "^7.6.8",
+ "@types/node": "^20.4.1",
+ "ava": "^6.0.1",
+ "prettier": "^3.1.1",
+ "typescript": "^5.3.3"
},
"dependencies": {
- "tslib": "^2.4.0"
+ "tslib": "^2.6.2"
},
"ava": {
- "require": [
- "esm"
- ]
+ "failFast": true
+ },
+ "optionalDependencies": {
+ "better-sqlite3": "9.4.0"
}
}
diff --git a/packages/idb-bridge/src/MemoryBackend.test.ts b/packages/idb-bridge/src/MemoryBackend.test.ts
index 8a544a201..a851309ed 100644
--- a/packages/idb-bridge/src/MemoryBackend.test.ts
+++ b/packages/idb-bridge/src/MemoryBackend.test.ts
@@ -15,334 +15,9 @@
*/
import test from "ava";
-import {
- BridgeIDBCursorWithValue,
- BridgeIDBDatabase,
- BridgeIDBFactory,
- BridgeIDBKeyRange,
- BridgeIDBRequest,
- BridgeIDBTransaction,
-} from "./bridge-idb.js";
-import {
- IDBCursorDirection,
- IDBCursorWithValue,
- IDBDatabase,
- IDBKeyRange,
- IDBValidKey,
-} from "./idbtypes.js";
import { MemoryBackend } from "./MemoryBackend.js";
-
-function promiseFromRequest(request: BridgeIDBRequest): Promise<any> {
- return new Promise((resolve, reject) => {
- request.onsuccess = () => {
- resolve(request.result);
- };
- request.onerror = () => {
- reject(request.error);
- };
- });
-}
-
-function promiseFromTransaction(
- transaction: BridgeIDBTransaction,
-): Promise<void> {
- return new Promise<void>((resolve, reject) => {
- transaction.oncomplete = () => {
- resolve();
- };
- transaction.onerror = () => {
- reject();
- };
- });
-}
-
-test("Spec: Example 1 Part 1", async (t) => {
- const backend = new MemoryBackend();
- const idb = new BridgeIDBFactory(backend);
-
- const request = idb.open("library");
- request.onupgradeneeded = () => {
- const db = request.result;
- const store = db.createObjectStore("books", { keyPath: "isbn" });
- const titleIndex = store.createIndex("by_title", "title", { unique: true });
- const authorIndex = store.createIndex("by_author", "author");
-
- // Populate with initial data.
- store.put({ title: "Quarry Memories", author: "Fred", isbn: 123456 });
- store.put({ title: "Water Buffaloes", author: "Fred", isbn: 234567 });
- store.put({ title: "Bedrock Nights", author: "Barney", isbn: 345678 });
- };
-
- await promiseFromRequest(request);
- t.pass();
-});
-
-test("Spec: Example 1 Part 2", async (t) => {
- const backend = new MemoryBackend();
- const idb = new BridgeIDBFactory(backend);
-
- const request = idb.open("library");
- request.onupgradeneeded = () => {
- const db = request.result;
- const store = db.createObjectStore("books", { keyPath: "isbn" });
- const titleIndex = store.createIndex("by_title", "title", { unique: true });
- const authorIndex = store.createIndex("by_author", "author");
- };
-
- const db: BridgeIDBDatabase = await promiseFromRequest(request);
-
- t.is(db.name, "library");
-
- const tx = db.transaction("books", "readwrite");
- tx.oncomplete = () => {
- console.log("oncomplete called");
- };
-
- const store = tx.objectStore("books");
-
- store.put({ title: "Quarry Memories", author: "Fred", isbn: 123456 });
- store.put({ title: "Water Buffaloes", author: "Fred", isbn: 234567 });
- store.put({ title: "Bedrock Nights", author: "Barney", isbn: 345678 });
-
- await promiseFromTransaction(tx);
-
- t.pass();
-});
-
-test("Spec: Example 1 Part 3", async (t) => {
- const backend = new MemoryBackend();
- backend.enableTracing = true;
- const idb = new BridgeIDBFactory(backend);
-
- const request = idb.open("library");
- request.onupgradeneeded = () => {
- const db = request.result;
- const store = db.createObjectStore("books", { keyPath: "isbn" });
- const titleIndex = store.createIndex("by_title", "title", { unique: true });
- const authorIndex = store.createIndex("by_author", "author");
- };
-
- const db: BridgeIDBDatabase = await promiseFromRequest(request);
-
- t.is(db.name, "library");
-
- const tx = db.transaction("books", "readwrite");
-
- const store = tx.objectStore("books");
-
- store.put({ title: "Bedrock Nights", author: "Barney", isbn: 345678 });
- store.put({ title: "Quarry Memories", author: "Fred", isbn: 123456 });
- store.put({ title: "Water Buffaloes", author: "Fred", isbn: 234567 });
-
- await promiseFromTransaction(tx);
-
- const tx2 = db.transaction("books", "readonly");
- const store2 = tx2.objectStore("books");
- var index2 = store2.index("by_title");
- const request2 = index2.get("Bedrock Nights");
- const result2: any = await promiseFromRequest(request2);
-
- t.is(result2.author, "Barney");
-
- const tx3 = db.transaction(["books"], "readonly");
- const store3 = tx3.objectStore("books");
- const index3 = store3.index("by_author");
- const request3 = index3.openCursor(BridgeIDBKeyRange.only("Fred"));
-
- await promiseFromRequest(request3);
-
- let cursor: BridgeIDBCursorWithValue | null;
- cursor = request3.result as BridgeIDBCursorWithValue;
- t.is(cursor.value.author, "Fred");
- t.is(cursor.value.isbn, 123456);
-
- cursor.continue();
-
- await promiseFromRequest(request3);
-
- cursor = request3.result as BridgeIDBCursorWithValue;
- t.is(cursor.value.author, "Fred");
- t.is(cursor.value.isbn, 234567);
-
- await promiseFromTransaction(tx3);
-
- const tx4 = db.transaction("books", "readonly");
- const store4 = tx4.objectStore("books");
- const request4 = store4.openCursor();
-
- await promiseFromRequest(request4);
-
- cursor = request4.result;
- if (!cursor) {
- throw new Error();
- }
- t.is(cursor.value.isbn, 123456);
-
- cursor.continue();
-
- await promiseFromRequest(request4);
-
- cursor = request4.result;
- if (!cursor) {
- throw new Error();
- }
- t.is(cursor.value.isbn, 234567);
-
- cursor.continue();
-
- await promiseFromRequest(request4);
-
- cursor = request4.result;
- if (!cursor) {
- throw new Error();
- }
- t.is(cursor.value.isbn, 345678);
-
- cursor.continue();
- await promiseFromRequest(request4);
-
- cursor = request4.result;
-
- t.is(cursor, null);
-
- const tx5 = db.transaction("books", "readonly");
- const store5 = tx5.objectStore("books");
- const index5 = store5.index("by_author");
-
- const request5 = index5.openCursor(null, "next");
-
- await promiseFromRequest(request5);
- cursor = request5.result;
- if (!cursor) {
- throw new Error();
- }
- t.is(cursor.value.author, "Barney");
- cursor.continue();
-
- await promiseFromRequest(request5);
- cursor = request5.result;
- if (!cursor) {
- throw new Error();
- }
- t.is(cursor.value.author, "Fred");
- cursor.continue();
-
- await promiseFromRequest(request5);
- cursor = request5.result;
- if (!cursor) {
- throw new Error();
- }
- t.is(cursor.value.author, "Fred");
- cursor.continue();
-
- await promiseFromRequest(request5);
- cursor = request5.result;
- t.is(cursor, null);
-
- const request6 = index5.openCursor(null, "nextunique");
-
- await promiseFromRequest(request6);
- cursor = request6.result;
- if (!cursor) {
- throw new Error();
- }
- t.is(cursor.value.author, "Barney");
- cursor.continue();
-
- await promiseFromRequest(request6);
- cursor = request6.result;
- if (!cursor) {
- throw new Error();
- }
- t.is(cursor.value.author, "Fred");
- t.is(cursor.value.isbn, 123456);
- cursor.continue();
-
- await promiseFromRequest(request6);
- cursor = request6.result;
- t.is(cursor, null);
-
- const request7 = index5.openCursor(null, "prevunique");
- await promiseFromRequest(request7);
- cursor = request7.result;
- if (!cursor) {
- throw new Error();
- }
- t.is(cursor.value.author, "Fred");
- t.is(cursor.value.isbn, 123456);
- cursor.continue();
-
- await promiseFromRequest(request7);
- cursor = request7.result;
- if (!cursor) {
- throw new Error();
- }
- t.is(cursor.value.author, "Barney");
- cursor.continue();
-
- await promiseFromRequest(request7);
- cursor = request7.result;
- t.is(cursor, null);
-
- db.close();
-
- t.pass();
-});
-
-test("simple deletion", async (t) => {
- const backend = new MemoryBackend();
- const idb = new BridgeIDBFactory(backend);
-
- const request = idb.open("library");
- request.onupgradeneeded = () => {
- const db = request.result;
- const store = db.createObjectStore("books", { keyPath: "isbn" });
- const titleIndex = store.createIndex("by_title", "title", { unique: true });
- const authorIndex = store.createIndex("by_author", "author");
- };
-
- const db: BridgeIDBDatabase = await promiseFromRequest(request);
-
- t.is(db.name, "library");
-
- const tx = db.transaction("books", "readwrite");
- tx.oncomplete = () => {
- console.log("oncomplete called");
- };
-
- const store = tx.objectStore("books");
-
- store.put({ title: "Quarry Memories", author: "Fred", isbn: 123456 });
- store.put({ title: "Water Buffaloes", author: "Fred", isbn: 234567 });
- store.put({ title: "Bedrock Nights", author: "Barney", isbn: 345678 });
-
- await promiseFromTransaction(tx);
-
- const tx2 = db.transaction("books", "readwrite");
-
- const store2 = tx2.objectStore("books");
-
- const req1 = store2.get(234567);
- await promiseFromRequest(req1);
- t.is(req1.readyState, "done");
- t.is(req1.result.author, "Fred");
-
- store2.delete(123456);
-
- const req2 = store2.get(123456);
- await promiseFromRequest(req2);
- t.is(req2.readyState, "done");
- t.is(req2.result, undefined);
-
- const req3 = store2.get(234567);
- await promiseFromRequest(req3);
- t.is(req3.readyState, "done");
- t.is(req3.result.author, "Fred");
-
- await promiseFromTransaction(tx2);
-
- t.pass();
-});
+import { BridgeIDBDatabase, BridgeIDBFactory } from "./bridge-idb.js";
+import { promiseFromRequest, promiseFromTransaction } from "./idbpromutil.js";
test("export", async (t) => {
const backend = new MemoryBackend();
@@ -386,276 +61,3 @@ test("export", async (t) => {
t.is(exportedData2.databases["library"].schema.databaseVersion, 42);
t.pass();
});
-
-test("update with non-existent index values", async (t) => {
- const backend = new MemoryBackend();
- backend.enableTracing = true;
- const idb = new BridgeIDBFactory(backend);
- const request = idb.open("mydb");
- request.onupgradeneeded = () => {
- const db = request.result;
- const store = db.createObjectStore("bla", { keyPath: "x" });
- store.createIndex("by_y", "y");
- store.createIndex("by_z", "z");
- };
-
- const db: BridgeIDBDatabase = await promiseFromRequest(request);
-
- t.is(db.name, "mydb");
-
- {
- const tx = db.transaction("bla", "readwrite");
- const store = tx.objectStore("bla");
- store.put({ x: 0, y: "a", z: 42 });
- const index = store.index("by_z");
- const indRes = await promiseFromRequest(index.get(42));
- t.is(indRes.x, 0);
- const res = await promiseFromRequest(store.get(0));
- t.is(res.z, 42);
- await promiseFromTransaction(tx);
- }
-
- {
- const tx = db.transaction("bla", "readwrite");
- const store = tx.objectStore("bla");
- store.put({ x: 0, y: "a" });
- const res = await promiseFromRequest(store.get(0));
- t.is(res.z, undefined);
- await promiseFromTransaction(tx);
- }
-
- {
- const tx = db.transaction("bla", "readwrite");
- const store = tx.objectStore("bla");
- const index = store.index("by_z");
- {
- const indRes = await promiseFromRequest(index.get(42));
- t.is(indRes, undefined);
- }
- const res = await promiseFromRequest(store.get(0));
- t.is(res.z, undefined);
- await promiseFromTransaction(tx);
- }
-
- t.pass();
-});
-
-test("delete from unique index", async (t) => {
- const backend = new MemoryBackend();
- backend.enableTracing = true;
- const idb = new BridgeIDBFactory(backend);
- const request = idb.open("mydb");
- request.onupgradeneeded = () => {
- const db = request.result as IDBDatabase;
- const store = db.createObjectStore("bla", { keyPath: "x" });
- store.createIndex("by_yz", ["y", "z"], {
- unique: true,
- });
- };
-
- const db: BridgeIDBDatabase = await promiseFromRequest(request);
-
- t.is(db.name, "mydb");
-
- {
- const tx = db.transaction("bla", "readwrite");
- const store = tx.objectStore("bla");
- store.put({ x: 0, y: "a", z: 42 });
- const index = store.index("by_yz");
- const indRes = await promiseFromRequest(index.get(["a", 42]));
- t.is(indRes.x, 0);
- const res = await promiseFromRequest(store.get(0));
- t.is(res.z, 42);
- await promiseFromTransaction(tx);
- }
-
- {
- const tx = db.transaction("bla", "readwrite");
- const store = tx.objectStore("bla");
- store.put({ x: 0, y: "a", z: 42, extra: 123 });
- await promiseFromTransaction(tx);
- }
-
- t.pass();
-});
-
-test("range queries", async (t) => {
- const backend = new MemoryBackend();
- backend.enableTracing = true;
- const idb = new BridgeIDBFactory(backend);
-
- const request = idb.open("mydb");
- request.onupgradeneeded = () => {
- const db = request.result;
- const store = db.createObjectStore("bla", { keyPath: "x" });
- store.createIndex("by_y", "y");
- store.createIndex("by_z", "z");
- };
-
- const db: BridgeIDBDatabase = await promiseFromRequest(request);
-
- t.is(db.name, "mydb");
-
- const tx = db.transaction("bla", "readwrite");
-
- const store = tx.objectStore("bla");
-
- store.put({ x: 0, y: "a" });
- store.put({ x: 2, y: "a" });
- store.put({ x: 4, y: "b" });
- store.put({ x: 8, y: "b" });
- store.put({ x: 10, y: "c" });
- store.put({ x: 12, y: "c" });
-
- await promiseFromTransaction(tx);
-
- async function doCursorStoreQuery(
- range: IDBKeyRange | IDBValidKey | undefined,
- direction: IDBCursorDirection | undefined,
- expected: any[],
- ): Promise<void> {
- const tx = db.transaction("bla", "readwrite");
- const store = tx.objectStore("bla");
- const vals: any[] = [];
-
- const req = store.openCursor(range, direction);
- while (1) {
- await promiseFromRequest(req);
- const cursor: IDBCursorWithValue = req.result;
- if (!cursor) {
- break;
- }
- cursor.continue();
- vals.push(cursor.value);
- }
-
- await promiseFromTransaction(tx);
-
- t.deepEqual(vals, expected);
- }
-
- async function doCursorIndexQuery(
- range: IDBKeyRange | IDBValidKey | undefined,
- direction: IDBCursorDirection | undefined,
- expected: any[],
- ): Promise<void> {
- const tx = db.transaction("bla", "readwrite");
- const store = tx.objectStore("bla");
- const index = store.index("by_y");
- const vals: any[] = [];
-
- const req = index.openCursor(range, direction);
- while (1) {
- await promiseFromRequest(req);
- const cursor: IDBCursorWithValue = req.result;
- if (!cursor) {
- break;
- }
- cursor.continue();
- vals.push(cursor.value);
- }
-
- await promiseFromTransaction(tx);
-
- t.deepEqual(vals, expected);
- }
-
- await doCursorStoreQuery(undefined, undefined, [
- {
- x: 0,
- y: "a",
- },
- {
- x: 2,
- y: "a",
- },
- {
- x: 4,
- y: "b",
- },
- {
- x: 8,
- y: "b",
- },
- {
- x: 10,
- y: "c",
- },
- {
- x: 12,
- y: "c",
- },
- ]);
-
- await doCursorStoreQuery(
- BridgeIDBKeyRange.bound(0, 12, true, true),
- undefined,
- [
- {
- x: 2,
- y: "a",
- },
- {
- x: 4,
- y: "b",
- },
- {
- x: 8,
- y: "b",
- },
- {
- x: 10,
- y: "c",
- },
- ],
- );
-
- await doCursorIndexQuery(
- BridgeIDBKeyRange.bound("a", "c", true, true),
- undefined,
- [
- {
- x: 4,
- y: "b",
- },
- {
- x: 8,
- y: "b",
- },
- ],
- );
-
- await doCursorIndexQuery(undefined, "nextunique", [
- {
- x: 0,
- y: "a",
- },
- {
- x: 4,
- y: "b",
- },
- {
- x: 10,
- y: "c",
- },
- ]);
-
- await doCursorIndexQuery(undefined, "prevunique", [
- {
- x: 10,
- y: "c",
- },
- {
- x: 4,
- y: "b",
- },
- {
- x: 0,
- y: "a",
- },
- ]);
-
- db.close();
-
- t.pass();
-});
diff --git a/packages/idb-bridge/src/MemoryBackend.ts b/packages/idb-bridge/src/MemoryBackend.ts
index f40f1c98b..526920a9f 100644
--- a/packages/idb-bridge/src/MemoryBackend.ts
+++ b/packages/idb-bridge/src/MemoryBackend.ts
@@ -14,43 +14,38 @@
permissions and limitations under the License.
*/
+import { AsyncCondition, TransactionLevel } from "./backend-common.js";
import {
Backend,
+ ConnectResult,
DatabaseConnection,
DatabaseTransaction,
- Schema,
- RecordStoreRequest,
- IndexProperties,
- RecordGetRequest,
+ IndexGetQuery,
+ IndexMeta,
+ ObjectStoreGetQuery,
+ ObjectStoreMeta,
RecordGetResponse,
+ RecordStoreRequest,
+ RecordStoreResponse,
ResultLevel,
StoreLevel,
- RecordStoreResponse,
} from "./backend-interface.js";
+import { BridgeIDBKeyRange } from "./bridge-idb.js";
+import { IDBKeyRange, IDBTransactionMode, IDBValidKey } from "./idbtypes.js";
+import BTree, { ISortedMapF, ISortedSetF } from "./tree/b+tree.js";
+import { compareKeys } from "./util/cmp.js";
+import { ConstraintError, DataError } from "./util/errors.js";
+import { getIndexKeys } from "./util/getIndexKeys.js";
+import { StoreKeyResult, makeStoreKeyValue } from "./util/makeStoreKeyValue.js";
import {
structuredClone,
structuredEncapsulate,
structuredRevive,
} from "./util/structuredClone.js";
-import { ConstraintError, DataError } from "./util/errors.js";
-import BTree, { ISortedMapF, ISortedSetF } from "./tree/b+tree.js";
-import { compareKeys } from "./util/cmp.js";
-import { StoreKeyResult, makeStoreKeyValue } from "./util/makeStoreKeyValue.js";
-import { getIndexKeys } from "./util/getIndexKeys.js";
-import { openPromise } from "./util/openPromise.js";
-import { IDBKeyRange, IDBTransactionMode, IDBValidKey } from "./idbtypes.js";
-import { BridgeIDBKeyRange } from "./bridge-idb.js";
type Key = IDBValidKey;
type Value = unknown;
-enum TransactionLevel {
- None = 0,
- Read = 1,
- Write = 2,
- VersionChange = 3,
-}
-
interface ObjectStore {
originalName: string;
modifiedName: string | undefined;
@@ -95,24 +90,39 @@ interface Database {
connectionCookies: string[];
}
-/** @public */
export interface ObjectStoreDump {
name: string;
keyGenerator: number;
records: ObjectStoreRecord[];
}
-/** @public */
export interface DatabaseDump {
schema: Schema;
objectStores: { [name: string]: ObjectStoreDump };
}
-/** @public */
export interface MemoryBackendDump {
databases: { [name: string]: DatabaseDump };
}
+export interface ObjectStoreProperties {
+ keyPath: string | string[] | null;
+ autoIncrement: boolean;
+ indexes: { [nameame: string]: IndexProperties };
+}
+
+export interface IndexProperties {
+ keyPath: string | string[];
+ multiEntry: boolean;
+ unique: boolean;
+}
+
+export interface Schema {
+ databaseName: string;
+ databaseVersion: number;
+ objectStores: { [name: string]: ObjectStoreProperties };
+}
+
interface ObjectStoreMapEntry {
store: ObjectStore;
indexMap: { [currentName: string]: Index };
@@ -142,27 +152,6 @@ export interface ObjectStoreRecord {
value: Value;
}
-class AsyncCondition {
- _waitPromise: Promise<void>;
- _resolveWaitPromise: () => void;
- constructor() {
- const op = openPromise<void>();
- this._waitPromise = op.promise;
- this._resolveWaitPromise = op.resolve;
- }
-
- wait(): Promise<void> {
- return this._waitPromise;
- }
-
- trigger(): void {
- this._resolveWaitPromise();
- const op = openPromise<void>();
- this._waitPromise = op.promise;
- this._resolveWaitPromise = op.resolve;
- }
-}
-
function nextStoreKey<T>(
forward: boolean,
data: ISortedMapF<Key, ObjectStoreRecord>,
@@ -178,12 +167,6 @@ function nextStoreKey<T>(
return res[1].primaryKey;
}
-function assertInvariant(cond: boolean): asserts cond {
- if (!cond) {
- throw Error("invariant failed");
- }
-}
-
function nextKey(
forward: boolean,
tree: ISortedSetF<IDBValidKey>,
@@ -230,6 +213,7 @@ function furthestKey(
}
export interface AccessStats {
+ primitiveStatements: number;
writeTransactions: number;
readTransactions: number;
writesPerStore: Record<string, number>;
@@ -279,6 +263,7 @@ export class MemoryBackend implements Backend {
trackStats: boolean = true;
accessStats: AccessStats = {
+ primitiveStatements: 0,
readTransactions: 0,
writeTransactions: 0,
readsPerStore: {},
@@ -459,7 +444,7 @@ export class MemoryBackend implements Backend {
delete this.databases[name];
}
- async connectDatabase(name: string): Promise<DatabaseConnection> {
+ async connectDatabase(name: string): Promise<ConnectResult> {
if (this.enableTracing) {
console.log(`TRACING: connectDatabase(${name})`);
}
@@ -498,7 +483,11 @@ export class MemoryBackend implements Backend {
this.connections[connectionCookie] = myConn;
- return { connectionCookie };
+ return {
+ conn: { connectionCookie },
+ version: database.committedSchema.databaseVersion,
+ objectStores: Object.keys(database.committedSchema.objectStores).sort(),
+ };
}
async beginTransaction(
@@ -601,14 +590,6 @@ export class MemoryBackend implements Backend {
this.disconnectCond.trigger();
}
- private requireConnection(dbConn: DatabaseConnection): Connection {
- const myConn = this.connections[dbConn.connectionCookie];
- if (!myConn) {
- throw Error(`unknown connection (${dbConn.connectionCookie})`);
- }
- return myConn;
- }
-
private requireConnectionFromTransaction(
btx: DatabaseTransaction,
): Connection {
@@ -619,36 +600,6 @@ export class MemoryBackend implements Backend {
return myConn;
}
- getSchema(dbConn: DatabaseConnection): Schema {
- if (this.enableTracing) {
- console.log(`TRACING: getSchema`);
- }
- const myConn = this.requireConnection(dbConn);
- const db = this.databases[myConn.dbName];
- if (!db) {
- throw Error("db not found");
- }
- return db.committedSchema;
- }
-
- getCurrentTransactionSchema(btx: DatabaseTransaction): Schema {
- const myConn = this.requireConnectionFromTransaction(btx);
- const db = this.databases[myConn.dbName];
- if (!db) {
- throw Error("db not found");
- }
- return myConn.modifiedSchema;
- }
-
- getInitialTransactionSchema(btx: DatabaseTransaction): Schema {
- const myConn = this.requireConnectionFromTransaction(btx);
- const db = this.databases[myConn.dbName];
- if (!db) {
- throw Error("db not found");
- }
- return db.committedSchema;
- }
-
renameIndex(
btx: DatabaseTransaction,
objectStoreName: string,
@@ -799,7 +750,7 @@ export class MemoryBackend implements Backend {
createObjectStore(
btx: DatabaseTransaction,
name: string,
- keyPath: string[] | null,
+ keyPath: string | string[] | null,
autoIncrement: boolean,
): void {
if (this.enableTracing) {
@@ -842,7 +793,7 @@ export class MemoryBackend implements Backend {
btx: DatabaseTransaction,
indexName: string,
objectStoreName: string,
- keyPath: string[],
+ keyPath: string | string[],
multiEntry: boolean,
unique: boolean,
): void {
@@ -1102,12 +1053,91 @@ export class MemoryBackend implements Backend {
}
}
- async getRecords(
+ async getObjectStoreRecords(
+ btx: DatabaseTransaction,
+ req: ObjectStoreGetQuery,
+ ): Promise<RecordGetResponse> {
+ if (this.enableTracing) {
+ console.log(`TRACING: getObjectStoreRecords`);
+ console.log("query", req);
+ }
+ const myConn = this.requireConnectionFromTransaction(btx);
+ const db = this.databases[myConn.dbName];
+ if (!db) {
+ throw Error("db not found");
+ }
+ if (db.txLevel < TransactionLevel.Read) {
+ throw Error("only allowed while running a transaction");
+ }
+ if (
+ db.txRestrictObjectStores &&
+ !db.txRestrictObjectStores.includes(req.objectStoreName)
+ ) {
+ throw Error(
+ `Not allowed to access store '${
+ req.objectStoreName
+ }', transaction is over ${JSON.stringify(db.txRestrictObjectStores)}`,
+ );
+ }
+ const objectStoreMapEntry = myConn.objectStoreMap[req.objectStoreName];
+ if (!objectStoreMapEntry) {
+ throw Error("object store not found");
+ }
+
+ let range;
+ if (req.range == null) {
+ range = new BridgeIDBKeyRange(undefined, undefined, true, true);
+ } else {
+ range = req.range;
+ }
+
+ if (typeof range !== "object") {
+ throw Error(
+ "getObjectStoreRecords was given an invalid range (sanity check failed, not an object)",
+ );
+ }
+
+ if (!("lowerOpen" in range)) {
+ throw Error(
+ "getObjectStoreRecords was given an invalid range (sanity check failed, lowerOpen missing)",
+ );
+ }
+
+ const forward: boolean =
+ req.direction === "next" || req.direction === "nextunique";
+
+ const storeData =
+ objectStoreMapEntry.store.modifiedData ||
+ objectStoreMapEntry.store.originalData;
+
+ const resp = getObjectStoreRecords({
+ forward,
+ storeData,
+ limit: req.limit,
+ range,
+ resultLevel: req.resultLevel,
+ advancePrimaryKey: req.advancePrimaryKey,
+ lastObjectStorePosition: req.lastObjectStorePosition,
+ });
+ if (this.trackStats) {
+ const k = `${req.objectStoreName}`;
+ this.accessStats.readsPerStore[k] =
+ (this.accessStats.readsPerStore[k] ?? 0) + 1;
+ this.accessStats.readItemsPerStore[k] =
+ (this.accessStats.readItemsPerStore[k] ?? 0) + resp.count;
+ }
+ if (this.enableTracing) {
+ console.log(`TRACING: getRecords got ${resp.count} results`);
+ }
+ return resp;
+ }
+
+ async getIndexRecords(
btx: DatabaseTransaction,
- req: RecordGetRequest,
+ req: IndexGetQuery,
): Promise<RecordGetResponse> {
if (this.enableTracing) {
- console.log(`TRACING: getRecords`);
+ console.log(`TRACING: getIndexRecords`);
console.log("query", req);
}
const myConn = this.requireConnectionFromTransaction(btx);
@@ -1161,58 +1191,31 @@ export class MemoryBackend implements Backend {
objectStoreMapEntry.store.modifiedData ||
objectStoreMapEntry.store.originalData;
- const haveIndex = req.indexName !== undefined;
-
- let resp: RecordGetResponse;
-
- if (haveIndex) {
- const index =
- myConn.objectStoreMap[req.objectStoreName].indexMap[req.indexName!];
- const indexData = index.modifiedData || index.originalData;
- resp = getIndexRecords({
- forward,
- indexData,
- storeData,
- limit: req.limit,
- unique,
- range,
- resultLevel: req.resultLevel,
- advanceIndexKey: req.advanceIndexKey,
- advancePrimaryKey: req.advancePrimaryKey,
- lastIndexPosition: req.lastIndexPosition,
- lastObjectStorePosition: req.lastObjectStorePosition,
- });
- if (this.trackStats) {
- const k = `${req.objectStoreName}.${req.indexName}`;
- this.accessStats.readsPerIndex[k] =
- (this.accessStats.readsPerIndex[k] ?? 0) + 1;
- this.accessStats.readItemsPerIndex[k] =
- (this.accessStats.readItemsPerIndex[k] ?? 0) + resp.count;
- }
- } else {
- if (req.advanceIndexKey !== undefined) {
- throw Error("unsupported request");
- }
- resp = getObjectStoreRecords({
- forward,
- storeData,
- limit: req.limit,
- range,
- resultLevel: req.resultLevel,
- advancePrimaryKey: req.advancePrimaryKey,
- lastIndexPosition: req.lastIndexPosition,
- lastObjectStorePosition: req.lastObjectStorePosition,
- });
- if (this.trackStats) {
- const k = `${req.objectStoreName}`;
- this.accessStats.readsPerStore[k] =
- (this.accessStats.readsPerStore[k] ?? 0) + 1;
- this.accessStats.readItemsPerStore[k] =
- (this.accessStats.readItemsPerStore[k] ?? 0) + resp.count;
- }
+ const index =
+ myConn.objectStoreMap[req.objectStoreName].indexMap[req.indexName!];
+ const indexData = index.modifiedData || index.originalData;
+ const resp = getIndexRecords({
+ forward,
+ indexData,
+ storeData,
+ limit: req.limit,
+ unique,
+ range,
+ resultLevel: req.resultLevel,
+ advanceIndexKey: req.advanceIndexKey,
+ advancePrimaryKey: req.advancePrimaryKey,
+ lastIndexPosition: req.lastIndexPosition,
+ lastObjectStorePosition: req.lastObjectStorePosition,
+ });
+ if (this.trackStats) {
+ const k = `${req.objectStoreName}.${req.indexName}`;
+ this.accessStats.readsPerIndex[k] =
+ (this.accessStats.readsPerIndex[k] ?? 0) + 1;
+ this.accessStats.readItemsPerIndex[k] =
+ (this.accessStats.readItemsPerIndex[k] ?? 0) + resp.count;
}
if (this.enableTracing) {
- console.log(`TRACING: getRecords got ${resp.count} results`);
+ console.log(`TRACING: getIndexRecords got ${resp.count} results`);
}
return resp;
}
@@ -1294,13 +1297,13 @@ export class MemoryBackend implements Backend {
let storeKeyResult: StoreKeyResult;
try {
- storeKeyResult = makeStoreKeyValue(
- storeReq.value,
- storeReq.key,
- keygen,
- autoIncrement,
- keyPath,
- );
+ storeKeyResult = makeStoreKeyValue({
+ value: storeReq.value,
+ key: storeReq.key,
+ currentKeyGenerator: keygen,
+ autoIncrement: autoIncrement,
+ keyPath: keyPath,
+ });
} catch (e) {
if (e instanceof DataError) {
const kp = JSON.stringify(keyPath);
@@ -1445,7 +1448,7 @@ export class MemoryBackend implements Backend {
}
}
- async rollback(btx: DatabaseTransaction): Promise<void> {
+ rollback(btx: DatabaseTransaction): void {
if (this.enableTracing) {
console.log(`TRACING: rollback`);
}
@@ -1536,6 +1539,57 @@ export class MemoryBackend implements Backend {
await this.afterCommitCallback();
}
}
+
+ getObjectStoreMeta(
+ dbConn: DatabaseConnection,
+ objectStoreName: string,
+ ): ObjectStoreMeta | undefined {
+ const conn = this.connections[dbConn.connectionCookie];
+ if (!conn) {
+ throw Error("db connection not found");
+ }
+ let schema = conn.modifiedSchema;
+ if (!schema) {
+ throw Error();
+ }
+ const storeInfo = schema.objectStores[objectStoreName];
+ if (!storeInfo) {
+ return undefined;
+ }
+ return {
+ autoIncrement: storeInfo.autoIncrement,
+ indexSet: Object.keys(storeInfo.indexes).sort(),
+ keyPath: structuredClone(storeInfo.keyPath),
+ };
+ }
+
+ getIndexMeta(
+ dbConn: DatabaseConnection,
+ objectStoreName: string,
+ indexName: string,
+ ): IndexMeta | undefined {
+ const conn = this.connections[dbConn.connectionCookie];
+ if (!conn) {
+ throw Error("db connection not found");
+ }
+ let schema = conn.modifiedSchema;
+ if (!schema) {
+ throw Error();
+ }
+ const storeInfo = schema.objectStores[objectStoreName];
+ if (!storeInfo) {
+ return undefined;
+ }
+ const indexInfo = storeInfo.indexes[indexName];
+ if (!indexInfo) {
+ return;
+ }
+ return {
+ keyPath: structuredClone(indexInfo.keyPath),
+ multiEntry: indexInfo.multiEntry,
+ unique: indexInfo.unique,
+ };
+ }
}
function getIndexRecords(req: {
@@ -1734,7 +1788,6 @@ function getIndexRecords(req: {
function getObjectStoreRecords(req: {
storeData: ISortedMapF<IDBValidKey, ObjectStoreRecord>;
- lastIndexPosition?: IDBValidKey;
forward: boolean;
range: IDBKeyRange;
lastObjectStorePosition?: IDBValidKey;
@@ -1743,7 +1796,6 @@ function getObjectStoreRecords(req: {
resultLevel: ResultLevel;
}): RecordGetResponse {
let numResults = 0;
- const indexKeys: Key[] = [];
const primaryKeys: Key[] = [];
const values: Value[] = [];
const { storeData, range, forward } = req;
@@ -1751,8 +1803,7 @@ function getObjectStoreRecords(req: {
function packResult(): RecordGetResponse {
return {
count: numResults,
- indexKeys:
- req.resultLevel >= ResultLevel.OnlyKeys ? indexKeys : undefined,
+ indexKeys: undefined,
primaryKeys:
req.resultLevel >= ResultLevel.OnlyKeys ? primaryKeys : undefined,
values: req.resultLevel >= ResultLevel.Full ? values : undefined,
@@ -1762,8 +1813,8 @@ function getObjectStoreRecords(req: {
const rangeStart = forward ? range.lower : range.upper;
const dataStart = forward ? storeData.minKey() : storeData.maxKey();
let storePos = req.lastObjectStorePosition;
- storePos = furthestKey(forward, storePos, rangeStart);
storePos = furthestKey(forward, storePos, dataStart);
+ storePos = furthestKey(forward, storePos, rangeStart);
storePos = furthestKey(forward, storePos, req.advancePrimaryKey);
if (storePos != null) {
diff --git a/packages/idb-bridge/src/SqliteBackend.test.ts b/packages/idb-bridge/src/SqliteBackend.test.ts
new file mode 100644
index 000000000..612cb9d4b
--- /dev/null
+++ b/packages/idb-bridge/src/SqliteBackend.test.ts
@@ -0,0 +1,83 @@
+/*
+ Copyright 2019 Florian Dold
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ or implied. See the License for the specific language governing
+ permissions and limitations under the License.
+ */
+
+import test from "ava";
+import { createSqliteBackend } from "./SqliteBackend.js";
+import { ResultLevel, StoreLevel } from "./backend-interface.js";
+import { BridgeIDBKeyRange } from "./bridge-idb.js";
+import * as fs from "node:fs";
+import { createNodeSqlite3Impl } from "./node-sqlite3-impl.js";
+
+test("sqlite3 backend", async (t) => {
+ const filename = "mytestdb.sqlite3";
+ try {
+ fs.unlinkSync(filename);
+ } catch (e) {
+ // Do nothing.
+ }
+ try {
+ const sqlite3Impl = await createNodeSqlite3Impl();
+ const backend = await createSqliteBackend(sqlite3Impl, {
+ filename,
+ });
+ const dbConnRes = await backend.connectDatabase("mydb");
+ const dbConn = dbConnRes.conn;
+ const tx = await backend.enterVersionChange(dbConn, 1);
+ backend.createObjectStore(tx, "books", "isbn", true);
+ backend.createIndex(tx, "byName", "books", "name", false, false);
+ await backend.storeRecord(tx, {
+ objectStoreName: "books",
+ storeLevel: StoreLevel.AllowOverwrite,
+ value: { name: "foo" },
+ key: undefined,
+ });
+ const res = await backend.getObjectStoreRecords(tx, {
+ direction: "next",
+ limit: 1,
+ objectStoreName: "books",
+ resultLevel: ResultLevel.Full,
+ range: BridgeIDBKeyRange.only(1),
+ });
+ t.deepEqual(res.count, 1);
+ t.deepEqual(res.primaryKeys![0], 1);
+ t.deepEqual(res.values![0].name, "foo");
+
+ const indexRes = await backend.getIndexRecords(tx, {
+ direction: "next",
+ limit: 1,
+ objectStoreName: "books",
+ indexName: "byName",
+ resultLevel: ResultLevel.Full,
+ range: BridgeIDBKeyRange.only("foo"),
+ });
+
+ t.deepEqual(indexRes.count, 1);
+ t.deepEqual(indexRes.values![0].isbn, 1);
+ t.deepEqual(indexRes.values![0].name, "foo");
+
+ await backend.commit(tx);
+
+ const tx2 = await backend.beginTransaction(dbConn, ["books"], "readwrite");
+ await backend.commit(tx2);
+
+ await backend.close(dbConn);
+
+ t.pass();
+ } catch (e: any) {
+ console.log(e);
+ throw e;
+ }
+});
diff --git a/packages/idb-bridge/src/SqliteBackend.ts b/packages/idb-bridge/src/SqliteBackend.ts
new file mode 100644
index 000000000..26ed43b0f
--- /dev/null
+++ b/packages/idb-bridge/src/SqliteBackend.ts
@@ -0,0 +1,2329 @@
+/*
+ Copyright 2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received 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 { AsyncCondition } from "./backend-common.js";
+import {
+ Backend,
+ ConnectResult,
+ DatabaseConnection,
+ DatabaseTransaction,
+ IndexGetQuery,
+ IndexMeta,
+ ObjectStoreGetQuery,
+ ObjectStoreMeta,
+ RecordGetResponse,
+ RecordStoreRequest,
+ RecordStoreResponse,
+ ResultLevel,
+ StoreLevel,
+} from "./backend-interface.js";
+import { BridgeIDBDatabaseInfo, BridgeIDBKeyRange } from "./bridge-idb.js";
+import {
+ IDBKeyPath,
+ IDBKeyRange,
+ IDBTransactionMode,
+ IDBValidKey,
+} from "./idbtypes.js";
+import {
+ AccessStats,
+ structuredEncapsulate,
+ structuredRevive,
+} from "./index.js";
+import { ConstraintError, DataError } from "./util/errors.js";
+import { getIndexKeys } from "./util/getIndexKeys.js";
+import { deserializeKey, serializeKey } from "./util/key-storage.js";
+import { makeStoreKeyValue } from "./util/makeStoreKeyValue.js";
+import {
+ Sqlite3Database,
+ Sqlite3Interface,
+ Sqlite3Statement,
+} from "./sqlite3-interface.js";
+
+function assertDbInvariant(b: boolean): asserts b {
+ if (!b) {
+ throw Error("internal invariant failed");
+ }
+}
+
+const SqliteError = {
+ constraintPrimarykey: "SQLITE_CONSTRAINT_PRIMARYKEY",
+} as const;
+
+export type SqliteRowid = number | bigint;
+
+enum TransactionLevel {
+ None = 0,
+ Read = 1,
+ Write = 2,
+ VersionChange = 3,
+}
+
+interface ConnectionInfo {
+ // Database that the connection has
+ // connected to.
+ databaseName: string;
+}
+
+interface TransactionInfo {
+ connectionCookie: string;
+}
+
+interface ScopeIndexInfo {
+ indexId: SqliteRowid;
+ keyPath: IDBKeyPath | IDBKeyPath[];
+ multiEntry: boolean;
+ unique: boolean;
+}
+
+interface ScopeInfo {
+ /**
+ * Internal ID of the object store.
+ * Used for fast retrieval, since it's the
+ * primary key / rowid of the sqlite table.
+ */
+ objectStoreId: SqliteRowid;
+
+ indexMap: Map<string, ScopeIndexInfo>;
+}
+
+interface IndexIterPos {
+ objectPos: Uint8Array;
+ indexPos: Uint8Array;
+}
+
+export function serializeKeyPath(
+ keyPath: string | string[] | null,
+): string | null {
+ if (Array.isArray(keyPath)) {
+ return "," + keyPath.join(",");
+ }
+ return keyPath;
+}
+
+export function deserializeKeyPath(
+ dbKeyPath: string | null,
+): string | string[] | null {
+ if (dbKeyPath == null) {
+ return null;
+ }
+ if (dbKeyPath[0] === ",") {
+ const elems = dbKeyPath.split(",");
+ elems.splice(0, 1);
+ return elems;
+ } else {
+ return dbKeyPath;
+ }
+}
+
+interface Boundary {
+ key: Uint8Array;
+ inclusive: boolean;
+}
+
+function getRangeEndBoundary(
+ forward: boolean,
+ range: IDBKeyRange | undefined | null,
+): Boundary | undefined {
+ let endRangeKey: Uint8Array | undefined = undefined;
+ let endRangeInclusive: boolean = false;
+ if (range) {
+ if (forward && range.upper != null) {
+ endRangeKey = serializeKey(range.upper);
+ endRangeInclusive = !range.upperOpen;
+ } else if (!forward && range.lower != null) {
+ endRangeKey = serializeKey(range.lower);
+ endRangeInclusive = !range.lowerOpen;
+ }
+ }
+ if (endRangeKey) {
+ return {
+ inclusive: endRangeInclusive,
+ key: endRangeKey,
+ };
+ }
+ return undefined;
+}
+
+function isOutsideBoundary(
+ forward: boolean,
+ endRange: Boundary,
+ currentKey: Uint8Array,
+): boolean {
+ const cmp = compareSerializedKeys(currentKey, endRange.key);
+ if (forward && endRange.inclusive && cmp > 0) {
+ return true;
+ } else if (forward && !endRange.inclusive && cmp >= 0) {
+ return true;
+ } else if (!forward && endRange.inclusive && cmp < 0) {
+ return true;
+ } else if (!forward && !endRange.inclusive && cmp <= 0) {
+ return true;
+ }
+ return false;
+}
+
+function compareSerializedKeys(k1: Uint8Array, k2: Uint8Array): number {
+ // FIXME: Simplify!
+ let i = 0;
+ while (1) {
+ let x1 = i >= k1.length ? -1 : k1[i];
+ let x2 = i >= k2.length ? -1 : k2[i];
+ if (x1 < x2) {
+ return -1;
+ }
+ if (x1 > x2) {
+ return 1;
+ }
+ if (x1 < 0 && x2 < 0) {
+ return 0;
+ }
+ i++;
+ }
+ throw Error("not reached");
+}
+
+export function expectDbNumber(
+ resultRow: unknown,
+ name: string,
+): number | bigint {
+ assertDbInvariant(typeof resultRow === "object" && resultRow != null);
+ const res = (resultRow as any)[name];
+ if (typeof res !== "number") {
+ throw Error("unexpected type from database");
+ }
+ return res;
+}
+
+export function expectDbString(resultRow: unknown, name: string): string {
+ assertDbInvariant(typeof resultRow === "object" && resultRow != null);
+ const res = (resultRow as any)[name];
+ if (typeof res !== "string") {
+ throw Error("unexpected type from database");
+ }
+ return res;
+}
+
+export function expectDbStringOrNull(
+ resultRow: unknown,
+ name: string,
+): string | null {
+ assertDbInvariant(typeof resultRow === "object" && resultRow != null);
+ const res = (resultRow as any)[name];
+ if (res == null) {
+ return null;
+ }
+ if (typeof res !== "string") {
+ throw Error("unexpected type from database");
+ }
+ return res;
+}
+
+export class SqliteBackend implements Backend {
+ private connectionIdCounter = 1;
+ private transactionIdCounter = 1;
+
+ trackStats = false;
+
+ accessStats: AccessStats = {
+ primitiveStatements: 0, // Counted by the sqlite impl
+ readTransactions: 0,
+ writeTransactions: 0,
+ readsPerStore: {},
+ readsPerIndex: {},
+ readItemsPerIndex: {},
+ readItemsPerStore: {},
+ writesPerStore: {},
+ };
+
+ /**
+ * Condition that is triggered whenever a transaction finishes.
+ */
+ private transactionDoneCond: AsyncCondition = new AsyncCondition();
+
+ /**
+ * Is the connection blocked because either an open request
+ * or delete request is being processed?
+ */
+ private connectionBlocked: boolean = false;
+
+ private txLevel: TransactionLevel = TransactionLevel.None;
+
+ private txScope: Map<string, ScopeInfo> = new Map();
+
+ private connectionMap: Map<string, ConnectionInfo> = new Map();
+
+ private transactionMap: Map<string, TransactionInfo> = new Map();
+
+ private sqlPrepCache: Map<string, Sqlite3Statement> = new Map();
+
+ enableTracing: boolean = true;
+
+ constructor(
+ public sqliteImpl: Sqlite3Interface,
+ public db: Sqlite3Database,
+ ) {}
+
+ private _prep(sql: string): Sqlite3Statement {
+ const stmt = this.sqlPrepCache.get(sql);
+ if (stmt) {
+ return stmt;
+ }
+ const newStmt = this.db.prepare(sql);
+ this.sqlPrepCache.set(sql, newStmt);
+ return newStmt;
+ }
+
+ async getIndexRecords(
+ btx: DatabaseTransaction,
+ req: IndexGetQuery,
+ ): Promise<RecordGetResponse> {
+ const txInfo = this.transactionMap.get(btx.transactionCookie);
+ if (!txInfo) {
+ throw Error("transaction not found");
+ }
+ const connInfo = this.connectionMap.get(txInfo.connectionCookie);
+ if (!connInfo) {
+ throw Error("connection not found");
+ }
+ if (this.txLevel < TransactionLevel.Read) {
+ throw Error("only allowed in read transaction");
+ }
+ const scopeInfo = this.txScope.get(req.objectStoreName);
+ if (!scopeInfo) {
+ throw Error("object store not in scope");
+ }
+ const indexInfo = scopeInfo.indexMap.get(req.indexName);
+ if (!indexInfo) {
+ throw Error("index not found");
+ }
+ if (req.advancePrimaryKey != null) {
+ if (req.advanceIndexKey == null) {
+ throw Error(
+ "invalid request (advancePrimaryKey without advanceIndexKey)",
+ );
+ }
+ }
+
+ if (this.enableTracing) {
+ console.log(
+ `querying index os=${req.objectStoreName}, idx=${req.indexName}, direction=${req.direction}`,
+ );
+ }
+
+ const forward: boolean =
+ req.direction === "next" || req.direction === "nextunique";
+
+ const queryUnique =
+ req.direction === "nextunique" || req.direction === "prevunique";
+
+ const indexId = indexInfo.indexId;
+ const indexUnique = indexInfo.unique;
+
+ let numResults = 0;
+ const encPrimaryKeys: Uint8Array[] = [];
+ const encIndexKeys: Uint8Array[] = [];
+ const indexKeys: IDBValidKey[] = [];
+ const primaryKeys: IDBValidKey[] = [];
+ const values: unknown[] = [];
+
+ const endRange = getRangeEndBoundary(forward, req.range);
+
+ const backendThis = this;
+
+ function packResult() {
+ if (req.resultLevel > ResultLevel.OnlyCount) {
+ for (let i = 0; i < encPrimaryKeys.length; i++) {
+ primaryKeys.push(deserializeKey(encPrimaryKeys[i]));
+ }
+ for (let i = 0; i < encIndexKeys.length; i++) {
+ indexKeys.push(deserializeKey(encIndexKeys[i]));
+ }
+ if (req.resultLevel === ResultLevel.Full) {
+ for (let i = 0; i < encPrimaryKeys.length; i++) {
+ const val = backendThis._getObjectValue(
+ scopeInfo!.objectStoreId,
+ encPrimaryKeys[i],
+ );
+ if (!val) {
+ throw Error("invariant failed: value not found");
+ }
+ values.push(structuredRevive(JSON.parse(val)));
+ }
+ }
+ }
+
+ if (backendThis.enableTracing) {
+ console.log(`index query returned ${numResults} results`);
+ console.log(`result prim keys:`, primaryKeys);
+ console.log(`result index keys:`, indexKeys);
+ }
+
+ if (backendThis.trackStats) {
+ const k = `${req.objectStoreName}.${req.indexName}`;
+ backendThis.accessStats.readsPerIndex[k] =
+ (backendThis.accessStats.readsPerIndex[k] ?? 0) + 1;
+ backendThis.accessStats.readItemsPerIndex[k] =
+ (backendThis.accessStats.readItemsPerIndex[k] ?? 0) + numResults;
+ }
+
+ return {
+ count: numResults,
+ indexKeys: indexKeys,
+ primaryKeys:
+ req.resultLevel >= ResultLevel.OnlyKeys ? primaryKeys : undefined,
+ values: req.resultLevel >= ResultLevel.Full ? values : undefined,
+ };
+ }
+
+ let currentPos = this._startIndex({
+ indexId,
+ indexUnique,
+ queryUnique,
+ forward,
+ });
+
+ if (!currentPos) {
+ return packResult();
+ }
+
+ if (this.enableTracing && currentPos) {
+ console.log(`starting iteration at:`);
+ console.log(`indexKey:`, deserializeKey(currentPos.indexPos));
+ console.log(`objectKey:`, deserializeKey(currentPos.objectPos));
+ }
+
+ if (req.advanceIndexKey) {
+ const advanceIndexKey = serializeKey(req.advanceIndexKey);
+ const advancePrimaryKey = req.advancePrimaryKey
+ ? serializeKey(req.advancePrimaryKey)
+ : undefined;
+ currentPos = this._continueIndex({
+ indexId,
+ indexUnique,
+ queryUnique,
+ inclusive: true,
+ currentPos,
+ forward,
+ targetIndexKey: advanceIndexKey,
+ targetObjectKey: advancePrimaryKey,
+ });
+ if (!currentPos) {
+ return packResult();
+ }
+ }
+
+ if (req.lastIndexPosition) {
+ if (this.enableTracing) {
+ console.log("index query: seeking past last index position");
+ console.log("lastObjectPosition", req.lastObjectStorePosition);
+ console.log("lastIndexPosition", req.lastIndexPosition);
+ }
+ const lastIndexPosition = serializeKey(req.lastIndexPosition);
+ const lastObjectPosition = req.lastObjectStorePosition
+ ? serializeKey(req.lastObjectStorePosition)
+ : undefined;
+ currentPos = this._continueIndex({
+ indexId,
+ indexUnique,
+ queryUnique,
+ inclusive: false,
+ currentPos,
+ forward,
+ targetIndexKey: lastIndexPosition,
+ targetObjectKey: lastObjectPosition,
+ });
+ if (!currentPos) {
+ return packResult();
+ }
+ }
+
+ if (this.enableTracing && currentPos) {
+ console.log(
+ "before range, current index pos",
+ deserializeKey(currentPos.indexPos),
+ );
+ console.log(
+ "... current object pos",
+ deserializeKey(currentPos.objectPos),
+ );
+ }
+
+ if (req.range != null) {
+ const targetKeyObj = forward ? req.range.lower : req.range.upper;
+ if (targetKeyObj != null) {
+ const targetKey = serializeKey(targetKeyObj);
+ const inclusive = forward ? !req.range.lowerOpen : !req.range.upperOpen;
+ currentPos = this._continueIndex({
+ indexId,
+ indexUnique,
+ queryUnique,
+ inclusive,
+ currentPos,
+ forward,
+ targetIndexKey: targetKey,
+ });
+ }
+ if (!currentPos) {
+ return packResult();
+ }
+ }
+
+ if (this.enableTracing && currentPos) {
+ console.log(
+ "after range, current pos",
+ deserializeKey(currentPos.indexPos),
+ );
+ console.log(
+ "after range, current obj pos",
+ deserializeKey(currentPos.objectPos),
+ );
+ }
+
+ while (1) {
+ if (req.limit != 0 && numResults == req.limit) {
+ break;
+ }
+ if (currentPos == null) {
+ break;
+ }
+ if (
+ endRange &&
+ isOutsideBoundary(forward, endRange, currentPos.indexPos)
+ ) {
+ break;
+ }
+
+ numResults++;
+
+ if (req.resultLevel > ResultLevel.OnlyCount) {
+ encPrimaryKeys.push(currentPos.objectPos);
+ encIndexKeys.push(currentPos.indexPos);
+ }
+
+ currentPos = backendThis._continueIndex({
+ indexId,
+ indexUnique,
+ forward,
+ inclusive: false,
+ currentPos: undefined,
+ queryUnique,
+ targetIndexKey: currentPos.indexPos,
+ targetObjectKey: currentPos.objectPos,
+ });
+ }
+
+ return packResult();
+ }
+
+ // Continue past targetIndexKey (and optionally targetObjectKey)
+ // in the direction specified by "forward".
+ // Do nothing if the current position is already past the
+ // target position.
+ _continueIndex(req: {
+ indexId: SqliteRowid;
+ indexUnique: boolean;
+ queryUnique: boolean;
+ forward: boolean;
+ inclusive: boolean;
+ currentPos: IndexIterPos | null | undefined;
+ targetIndexKey: Uint8Array;
+ targetObjectKey?: Uint8Array;
+ }): IndexIterPos | undefined {
+ const currentPos = req.currentPos;
+ const forward = req.forward;
+ const dir = forward ? 1 : -1;
+ if (currentPos) {
+ // Check that the target position after the current position.
+ // If not, we just stay at the current position.
+ const indexCmp = compareSerializedKeys(
+ currentPos.indexPos,
+ req.targetIndexKey,
+ );
+ if (dir * indexCmp > 0) {
+ return currentPos;
+ }
+ if (indexCmp === 0) {
+ if (req.targetObjectKey != null) {
+ const objectCmp = compareSerializedKeys(
+ currentPos.objectPos,
+ req.targetObjectKey,
+ );
+ if (req.inclusive && objectCmp === 0) {
+ return currentPos;
+ }
+ if (dir * objectCmp > 0) {
+ return currentPos;
+ }
+ } else if (req.inclusive) {
+ return currentPos;
+ }
+ }
+ }
+
+ let stmt: Sqlite3Statement;
+
+ if (req.indexUnique) {
+ if (req.forward) {
+ if (req.inclusive) {
+ stmt = this._prep(sqlUniqueIndexDataContinueForwardInclusive);
+ } else {
+ stmt = this._prep(sqlUniqueIndexDataContinueForwardStrict);
+ }
+ } else {
+ if (req.inclusive) {
+ stmt = this._prep(sqlUniqueIndexDataContinueBackwardInclusive);
+ } else {
+ stmt = this._prep(sqlUniqueIndexDataContinueBackwardStrict);
+ }
+ }
+ } else {
+ if (req.forward) {
+ if (req.queryUnique || req.targetObjectKey == null) {
+ if (req.inclusive) {
+ stmt = this._prep(sqlIndexDataContinueForwardInclusiveUnique);
+ } else {
+ stmt = this._prep(sqlIndexDataContinueForwardStrictUnique);
+ }
+ } else {
+ if (req.inclusive) {
+ stmt = this._prep(sqlIndexDataContinueForwardInclusive);
+ } else {
+ stmt = this._prep(sqlIndexDataContinueForwardStrict);
+ }
+ }
+ } else {
+ if (req.queryUnique || req.targetObjectKey == null) {
+ if (req.inclusive) {
+ stmt = this._prep(sqlIndexDataContinueBackwardInclusiveUnique);
+ } else {
+ stmt = this._prep(sqlIndexDataContinueBackwardStrictUnique);
+ }
+ } else {
+ if (req.inclusive) {
+ stmt = this._prep(sqlIndexDataContinueBackwardInclusive);
+ } else {
+ stmt = this._prep(sqlIndexDataContinueBackwardStrict);
+ }
+ }
+ }
+ }
+
+ const res = stmt.getFirst({
+ index_id: req.indexId,
+ index_key: req.targetIndexKey,
+ object_key: req.targetObjectKey,
+ });
+
+ if (res == null) {
+ return undefined;
+ }
+
+ assertDbInvariant(typeof res === "object");
+ assertDbInvariant("index_key" in res);
+ const indexKey = res.index_key;
+ if (indexKey == null) {
+ return undefined;
+ }
+ assertDbInvariant(indexKey instanceof Uint8Array);
+ assertDbInvariant("object_key" in res);
+ const objectKey = res.object_key;
+ if (objectKey == null) {
+ return undefined;
+ }
+ assertDbInvariant(objectKey instanceof Uint8Array);
+
+ return {
+ indexPos: indexKey,
+ objectPos: objectKey,
+ };
+ }
+
+ _startIndex(req: {
+ indexId: SqliteRowid;
+ indexUnique: boolean;
+ queryUnique: boolean;
+ forward: boolean;
+ }): IndexIterPos | undefined {
+ let stmt: Sqlite3Statement;
+ if (req.indexUnique) {
+ if (req.forward) {
+ stmt = this._prep(sqlUniqueIndexDataStartForward);
+ } else {
+ stmt = this._prep(sqlUniqueIndexDataStartBackward);
+ }
+ } else {
+ if (req.forward) {
+ stmt = this._prep(sqlIndexDataStartForward);
+ } else {
+ if (req.queryUnique) {
+ stmt = this._prep(sqlIndexDataStartBackwardUnique);
+ } else {
+ stmt = this._prep(sqlIndexDataStartBackward);
+ }
+ }
+ }
+
+ const res = stmt.getFirst({
+ index_id: req.indexId,
+ });
+
+ if (res == null) {
+ return undefined;
+ }
+
+ assertDbInvariant(typeof res === "object");
+ assertDbInvariant("index_key" in res);
+ const indexKey = res.index_key;
+ assertDbInvariant(indexKey instanceof Uint8Array);
+ assertDbInvariant("object_key" in res);
+ const objectKey = res.object_key;
+ assertDbInvariant(objectKey instanceof Uint8Array);
+
+ return {
+ indexPos: indexKey,
+ objectPos: objectKey,
+ };
+ }
+
+ async getObjectStoreRecords(
+ btx: DatabaseTransaction,
+ req: ObjectStoreGetQuery,
+ ): Promise<RecordGetResponse> {
+ const txInfo = this.transactionMap.get(btx.transactionCookie);
+ if (!txInfo) {
+ throw Error("transaction not found");
+ }
+ const connInfo = this.connectionMap.get(txInfo.connectionCookie);
+ if (!connInfo) {
+ throw Error("connection not found");
+ }
+ if (this.txLevel < TransactionLevel.Read) {
+ throw Error("only allowed in read transaction");
+ }
+ const scopeInfo = this.txScope.get(req.objectStoreName);
+ if (!scopeInfo) {
+ throw Error(
+ `object store ${JSON.stringify(
+ req.objectStoreName,
+ )} not in transaction scope`,
+ );
+ }
+
+ const forward: boolean =
+ req.direction === "next" || req.direction === "nextunique";
+
+ let currentKey = this._startObjectKey(scopeInfo.objectStoreId, forward);
+
+ if (req.advancePrimaryKey != null) {
+ const targetKey = serializeKey(req.advancePrimaryKey);
+ currentKey = this._continueObjectKey({
+ objectStoreId: scopeInfo.objectStoreId,
+ forward,
+ inclusive: true,
+ currentKey,
+ targetKey,
+ });
+ }
+
+ if (req.lastObjectStorePosition != null) {
+ const targetKey = serializeKey(req.lastObjectStorePosition);
+ currentKey = this._continueObjectKey({
+ objectStoreId: scopeInfo.objectStoreId,
+ forward,
+ inclusive: false,
+ currentKey,
+ targetKey,
+ });
+ }
+
+ if (req.range != null) {
+ const targetKeyObj = forward ? req.range.lower : req.range.upper;
+ if (targetKeyObj != null) {
+ const targetKey = serializeKey(targetKeyObj);
+ const inclusive = forward ? !req.range.lowerOpen : !req.range.upperOpen;
+ currentKey = this._continueObjectKey({
+ objectStoreId: scopeInfo.objectStoreId,
+ forward,
+ inclusive,
+ currentKey,
+ targetKey,
+ });
+ }
+ }
+
+ const endRange = getRangeEndBoundary(forward, req.range);
+
+ let numResults = 0;
+ const encPrimaryKeys: Uint8Array[] = [];
+ const primaryKeys: IDBValidKey[] = [];
+ const values: unknown[] = [];
+
+ while (1) {
+ if (req.limit != 0 && numResults == req.limit) {
+ break;
+ }
+ if (currentKey == null) {
+ break;
+ }
+ if (endRange && isOutsideBoundary(forward, endRange, currentKey)) {
+ break;
+ }
+
+ numResults++;
+
+ if (req.resultLevel > ResultLevel.OnlyCount) {
+ encPrimaryKeys.push(currentKey);
+ }
+
+ currentKey = this._continueObjectKey({
+ objectStoreId: scopeInfo.objectStoreId,
+ forward,
+ inclusive: false,
+ currentKey: null,
+ targetKey: currentKey,
+ });
+ }
+
+ if (req.resultLevel > ResultLevel.OnlyCount) {
+ for (let i = 0; i < encPrimaryKeys.length; i++) {
+ primaryKeys.push(deserializeKey(encPrimaryKeys[i]));
+ }
+ if (req.resultLevel === ResultLevel.Full) {
+ for (let i = 0; i < encPrimaryKeys.length; i++) {
+ const val = this._getObjectValue(
+ scopeInfo.objectStoreId,
+ encPrimaryKeys[i],
+ );
+ if (!val) {
+ throw Error("invariant failed: value not found");
+ }
+ values.push(structuredRevive(JSON.parse(val)));
+ }
+ }
+ }
+
+ if (this.trackStats) {
+ const k = `${req.objectStoreName}`;
+ this.accessStats.readsPerStore[k] =
+ (this.accessStats.readsPerStore[k] ?? 0) + 1;
+ this.accessStats.readItemsPerStore[k] =
+ (this.accessStats.readItemsPerStore[k] ?? 0) + numResults;
+ }
+
+ return {
+ count: numResults,
+ indexKeys: undefined,
+ primaryKeys:
+ req.resultLevel >= ResultLevel.OnlyKeys ? primaryKeys : undefined,
+ values: req.resultLevel >= ResultLevel.Full ? values : undefined,
+ };
+ }
+
+ _startObjectKey(
+ objectStoreId: number | bigint,
+ forward: boolean,
+ ): Uint8Array | null {
+ let stmt: Sqlite3Statement;
+ if (forward) {
+ stmt = this._prep(sqlObjectDataStartForward);
+ } else {
+ stmt = this._prep(sqlObjectDataStartBackward);
+ }
+ const res = stmt.getFirst({
+ object_store_id: objectStoreId,
+ });
+ if (!res) {
+ return null;
+ }
+ assertDbInvariant(typeof res === "object");
+ assertDbInvariant("rkey" in res);
+ const rkey = res.rkey;
+ if (!rkey) {
+ return null;
+ }
+ assertDbInvariant(rkey instanceof Uint8Array);
+ return rkey;
+ }
+
+ // Result *must* be past targetKey in the direction
+ // specified by "forward".
+ _continueObjectKey(req: {
+ objectStoreId: number | bigint;
+ forward: boolean;
+ currentKey: Uint8Array | null;
+ targetKey: Uint8Array;
+ inclusive: boolean;
+ }): Uint8Array | null {
+ const { forward, currentKey, targetKey } = req;
+ const dir = forward ? 1 : -1;
+ if (currentKey) {
+ const objCmp = compareSerializedKeys(currentKey, targetKey);
+ if (objCmp === 0 && req.inclusive) {
+ return currentKey;
+ }
+ if (dir * objCmp > 0) {
+ return currentKey;
+ }
+ }
+
+ let stmt: Sqlite3Statement;
+
+ if (req.inclusive) {
+ if (req.forward) {
+ stmt = this._prep(sqlObjectDataContinueForwardInclusive);
+ } else {
+ stmt = this._prep(sqlObjectDataContinueBackwardInclusive);
+ }
+ } else {
+ if (req.forward) {
+ stmt = this._prep(sqlObjectDataContinueForward);
+ } else {
+ stmt = this._prep(sqlObjectDataContinueBackward);
+ }
+ }
+
+ const res = stmt.getFirst({
+ object_store_id: req.objectStoreId,
+ x: req.targetKey,
+ });
+
+ if (!res) {
+ return null;
+ }
+
+ assertDbInvariant(typeof res === "object");
+ assertDbInvariant("rkey" in res);
+ const rkey = res.rkey;
+ if (!rkey) {
+ return null;
+ }
+ assertDbInvariant(rkey instanceof Uint8Array);
+ return rkey;
+ }
+
+ _getObjectValue(
+ objectStoreId: number | bigint,
+ key: Uint8Array,
+ ): string | undefined {
+ const stmt = this._prep(sqlObjectDataValueFromKey);
+ const res = stmt.getFirst({
+ object_store_id: objectStoreId,
+ key: key,
+ });
+ if (!res) {
+ return undefined;
+ }
+ assertDbInvariant(typeof res === "object");
+ assertDbInvariant("value" in res);
+ assertDbInvariant(typeof res.value === "string");
+ return res.value;
+ }
+
+ getObjectStoreMeta(
+ dbConn: DatabaseConnection,
+ objectStoreName: string,
+ ): ObjectStoreMeta | undefined {
+ // FIXME: Use cached info from the connection for this!
+ const connInfo = this.connectionMap.get(dbConn.connectionCookie);
+ if (!connInfo) {
+ throw Error("connection not found");
+ }
+ const objRes = this._prep(sqlGetObjectStoreMetaByName).getFirst({
+ name: objectStoreName,
+ database_name: connInfo.databaseName,
+ });
+ if (!objRes) {
+ throw Error("object store not found");
+ }
+ const objectStoreId = expectDbNumber(objRes, "id");
+ const keyPath = deserializeKeyPath(
+ expectDbStringOrNull(objRes, "key_path"),
+ );
+ const autoInc = expectDbNumber(objRes, "auto_increment");
+ const indexSet: string[] = [];
+ const indexRes = this._prep(sqlGetIndexesByObjectStoreId).getAll({
+ object_store_id: objectStoreId,
+ });
+ for (const idxInfo of indexRes) {
+ const indexName = expectDbString(idxInfo, "name");
+ indexSet.push(indexName);
+ }
+ return {
+ keyPath,
+ autoIncrement: autoInc != 0,
+ indexSet,
+ };
+ }
+
+ getIndexMeta(
+ dbConn: DatabaseConnection,
+ objectStoreName: string,
+ indexName: string,
+ ): IndexMeta | undefined {
+ // FIXME: Use cached info from the connection for this!
+ const connInfo = this.connectionMap.get(dbConn.connectionCookie);
+ if (!connInfo) {
+ throw Error("connection not found");
+ }
+ const objRes = this._prep(sqlGetObjectStoreMetaByName).getFirst({
+ name: objectStoreName,
+ database_name: connInfo.databaseName,
+ });
+ if (!objRes) {
+ throw Error("object store not found");
+ }
+ const objectStoreId = expectDbNumber(objRes, "id");
+ const idxInfo = this._prep(sqlGetIndexByName).getFirst({
+ object_store_id: objectStoreId,
+ name: indexName,
+ });
+ if (!idxInfo) {
+ throw Error(
+ `index ${indexName} on object store ${objectStoreName} not found`,
+ );
+ }
+ const indexUnique = expectDbNumber(idxInfo, "unique_index");
+ const indexMultiEntry = expectDbNumber(idxInfo, "multientry");
+ const indexKeyPath = deserializeKeyPath(
+ expectDbString(idxInfo, "key_path"),
+ );
+ if (!indexKeyPath) {
+ throw Error("db inconsistent");
+ }
+ return {
+ keyPath: indexKeyPath,
+ multiEntry: indexMultiEntry != 0,
+ unique: indexUnique != 0,
+ };
+ }
+
+ async getDatabases(): Promise<BridgeIDBDatabaseInfo[]> {
+ const dbList = this._prep(sqlListDatabases).getAll();
+ let res: BridgeIDBDatabaseInfo[] = [];
+ for (const r of dbList) {
+ res.push({
+ name: (r as any).name,
+ version: (r as any).version,
+ });
+ }
+
+ return res;
+ }
+
+ private _loadObjectStoreNames(databaseName: string): string[] {
+ const objectStoreNames: string[] = [];
+ const storesRes = this._prep(sqlGetObjectStoresByDatabase).getAll({
+ database_name: databaseName,
+ });
+ for (const res of storesRes) {
+ assertDbInvariant(res != null && typeof res === "object");
+ assertDbInvariant("name" in res);
+ const storeName = res.name;
+ assertDbInvariant(typeof storeName === "string");
+ objectStoreNames.push(storeName);
+ }
+ return objectStoreNames;
+ }
+
+ async connectDatabase(databaseName: string): Promise<ConnectResult> {
+ const connectionId = this.connectionIdCounter++;
+ const connectionCookie = `connection-${connectionId}`;
+
+ // Wait until no transaction is active anymore.
+ while (1) {
+ if (this.txLevel == TransactionLevel.None) {
+ break;
+ }
+ await this.transactionDoneCond.wait();
+ }
+
+ this._prep(sqlBegin).run();
+ const versionRes = this._prep(sqlGetDatabaseVersion).getFirst({
+ name: databaseName,
+ });
+ let ver: number;
+ if (versionRes == undefined) {
+ this._prep(sqlCreateDatabase).run({ name: databaseName });
+ ver = 0;
+ } else {
+ const verNum = expectDbNumber(versionRes, "version");
+ assertDbInvariant(typeof verNum === "number");
+ ver = verNum;
+ }
+ const objectStoreNames: string[] = this._loadObjectStoreNames(databaseName);
+
+ this._prep(sqlCommit).run();
+
+ this.connectionMap.set(connectionCookie, {
+ databaseName: databaseName,
+ });
+
+ return {
+ conn: {
+ connectionCookie,
+ },
+ version: ver,
+ objectStores: objectStoreNames,
+ };
+ }
+
+ private _loadScopeInfo(connInfo: ConnectionInfo, storeName: string): void {
+ const objRes = this._prep(sqlGetObjectStoreMetaByName).getFirst({
+ name: storeName,
+ database_name: connInfo.databaseName,
+ });
+ if (!objRes) {
+ throw Error("object store not found");
+ }
+ const objectStoreId = expectDbNumber(objRes, "id");
+ const indexRes = this._prep(sqlGetIndexesByObjectStoreId).getAll({
+ object_store_id: objectStoreId,
+ });
+ if (!indexRes) {
+ throw Error("db inconsistent");
+ }
+ const indexMap = new Map<string, ScopeIndexInfo>();
+ for (const idxInfo of indexRes) {
+ const indexId = expectDbNumber(idxInfo, "id");
+ const indexName = expectDbString(idxInfo, "name");
+ const indexUnique = expectDbNumber(idxInfo, "unique_index");
+ const indexMultiEntry = expectDbNumber(idxInfo, "multientry");
+ const indexKeyPath = deserializeKeyPath(
+ expectDbString(idxInfo, "key_path"),
+ );
+ if (!indexKeyPath) {
+ throw Error("db inconsistent");
+ }
+ indexMap.set(indexName, {
+ indexId,
+ keyPath: indexKeyPath,
+ multiEntry: indexMultiEntry != 0,
+ unique: indexUnique != 0,
+ });
+ }
+ this.txScope.set(storeName, {
+ objectStoreId,
+ indexMap,
+ });
+ }
+
+ async beginTransaction(
+ conn: DatabaseConnection,
+ objectStores: string[],
+ mode: IDBTransactionMode,
+ ): Promise<DatabaseTransaction> {
+ const connInfo = this.connectionMap.get(conn.connectionCookie);
+ if (!connInfo) {
+ throw Error("connection not found");
+ }
+ const transactionCookie = `tx-${this.transactionIdCounter++}`;
+
+ while (1) {
+ if (this.txLevel === TransactionLevel.None) {
+ break;
+ }
+ await this.transactionDoneCond.wait();
+ }
+
+ if (this.trackStats) {
+ if (mode === "readonly") {
+ this.accessStats.readTransactions++;
+ } else if (mode === "readwrite") {
+ this.accessStats.writeTransactions++;
+ }
+ }
+
+ this._prep(sqlBegin).run();
+ if (mode === "readonly") {
+ this.txLevel = TransactionLevel.Read;
+ } else if (mode === "readwrite") {
+ this.txLevel = TransactionLevel.Write;
+ }
+
+ this.transactionMap.set(transactionCookie, {
+ connectionCookie: conn.connectionCookie,
+ });
+
+ // FIXME: We should check this
+ // if (this.txScope.size != 0) {
+ // // Something didn't clean up!
+ // throw Error("scope not empty");
+ // }
+ this.txScope.clear();
+
+ // FIXME: Use cached info from connection?
+ for (const storeName of objectStores) {
+ this._loadScopeInfo(connInfo, storeName);
+ }
+
+ return {
+ transactionCookie,
+ };
+ }
+
+ async enterVersionChange(
+ conn: DatabaseConnection,
+ newVersion: number,
+ ): Promise<DatabaseTransaction> {
+ const connInfo = this.connectionMap.get(conn.connectionCookie);
+ if (!connInfo) {
+ throw Error("connection not found");
+ }
+ if (this.enableTracing) {
+ console.log(
+ `entering version change transaction (conn ${conn.connectionCookie}), newVersion=${newVersion}`,
+ );
+ }
+ const transactionCookie = `tx-vc-${this.transactionIdCounter++}`;
+
+ while (1) {
+ if (this.txLevel === TransactionLevel.None) {
+ break;
+ }
+ await this.transactionDoneCond.wait();
+ }
+
+ // FIXME: We should check this
+ // if (this.txScope.size != 0) {
+ // // Something didn't clean up!
+ // throw Error("scope not empty");
+ // }
+ this.txScope.clear();
+
+ if (this.enableTracing) {
+ console.log(`version change transaction unblocked`);
+ }
+
+ this._prep(sqlBegin).run();
+ this.txLevel = TransactionLevel.VersionChange;
+
+ this.transactionMap.set(transactionCookie, {
+ connectionCookie: conn.connectionCookie,
+ });
+
+ this._prep(sqlUpdateDbVersion).run({
+ name: connInfo.databaseName,
+ version: newVersion,
+ });
+
+ const objectStoreNames = this._loadObjectStoreNames(connInfo.databaseName);
+
+ // FIXME: Use cached info from connection?
+ for (const storeName of objectStoreNames) {
+ this._loadScopeInfo(connInfo, storeName);
+ }
+
+ return {
+ transactionCookie,
+ };
+ }
+
+ async deleteDatabase(databaseName: string): Promise<void> {
+ // FIXME: Wait until connection queue is not blocked
+ // FIXME: To properly implement the spec semantics, maybe
+ // split delete into prepareDelete and executeDelete?
+
+ while (this.txLevel !== TransactionLevel.None) {
+ await this.transactionDoneCond.wait();
+ }
+
+ this._prep(sqlBegin).run();
+ const objectStoreNames = this._loadObjectStoreNames(databaseName);
+ for (const storeName of objectStoreNames) {
+ const objRes = this._prep(sqlGetObjectStoreMetaByName).getFirst({
+ name: storeName,
+ database_name: databaseName,
+ });
+ if (!objRes) {
+ throw Error("object store not found");
+ }
+ const objectStoreId = expectDbNumber(objRes, "id");
+ const indexRes = this._prep(sqlGetIndexesByObjectStoreId).getAll({
+ object_store_id: objectStoreId,
+ });
+ if (!indexRes) {
+ throw Error("db inconsistent");
+ }
+ const indexMap = new Map<string, ScopeIndexInfo>();
+ for (const idxInfo of indexRes) {
+ const indexId = expectDbNumber(idxInfo, "id");
+ const indexName = expectDbString(idxInfo, "name");
+ const indexUnique = expectDbNumber(idxInfo, "unique_index");
+ const indexMultiEntry = expectDbNumber(idxInfo, "multientry");
+ const indexKeyPath = deserializeKeyPath(
+ expectDbString(idxInfo, "key_path"),
+ );
+ if (!indexKeyPath) {
+ throw Error("db inconsistent");
+ }
+ indexMap.set(indexName, {
+ indexId,
+ keyPath: indexKeyPath,
+ multiEntry: indexMultiEntry != 0,
+ unique: indexUnique != 0,
+ });
+ }
+ this.txScope.set(storeName, {
+ objectStoreId,
+ indexMap,
+ });
+
+ for (const indexInfo of indexMap.values()) {
+ let stmt: Sqlite3Statement;
+ if (indexInfo.unique) {
+ stmt = this._prep(sqlIUniqueIndexDataDeleteAll);
+ } else {
+ stmt = this._prep(sqlIndexDataDeleteAll);
+ }
+ stmt.run({
+ index_id: indexInfo.indexId,
+ });
+ this._prep(sqlIndexDelete).run({
+ index_id: indexInfo.indexId,
+ });
+ }
+ this._prep(sqlObjectDataDeleteAll).run({
+ object_store_id: objectStoreId,
+ });
+ this._prep(sqlObjectStoreDelete).run({
+ object_store_id: objectStoreId,
+ });
+ }
+ this._prep(sqlDeleteDatabase).run({
+ name: databaseName,
+ });
+ this._prep(sqlCommit).run();
+ }
+
+ async close(db: DatabaseConnection): Promise<void> {
+ const connInfo = this.connectionMap.get(db.connectionCookie);
+ if (!connInfo) {
+ throw Error("connection not found");
+ }
+ // FIXME: What if we're in a transaction? Does the backend interface allow this?
+ // if (this.txLevel !== TransactionLevel.None) {
+ // throw Error("can't close while in transaction");
+ // }
+ if (this.enableTracing) {
+ console.log(`closing connection ${db.connectionCookie}`);
+ }
+ this.connectionMap.delete(db.connectionCookie);
+ }
+
+ renameObjectStore(
+ btx: DatabaseTransaction,
+ oldName: string,
+ newName: string,
+ ): void {
+ if (this.enableTracing) {
+ console.log(`renaming object store '${oldName}' to '${newName}'`);
+ }
+ const txInfo = this.transactionMap.get(btx.transactionCookie);
+ if (!txInfo) {
+ throw Error("transaction required");
+ }
+ const connInfo = this.connectionMap.get(txInfo.connectionCookie);
+ if (!connInfo) {
+ throw Error("not connected");
+ }
+ // FIXME: Would be much nicer with numeric UID handles
+ const scopeInfo = this.txScope.get(oldName);
+ if (!scopeInfo) {
+ throw Error("object store not found");
+ }
+ this.txScope.delete(oldName);
+ this.txScope.set(newName, scopeInfo);
+ this._prep(sqlRenameObjectStore).run({
+ object_store_id: scopeInfo.objectStoreId,
+ name: newName,
+ });
+ }
+
+ renameIndex(
+ btx: DatabaseTransaction,
+ objectStoreName: string,
+ oldIndexName: string,
+ newIndexName: string,
+ ): void {
+ const txInfo = this.transactionMap.get(btx.transactionCookie);
+ if (!txInfo) {
+ throw Error("transaction required");
+ }
+ const connInfo = this.connectionMap.get(txInfo.connectionCookie);
+ if (!connInfo) {
+ throw Error("not connected");
+ }
+ // FIXME: Would be much nicer with numeric UID handles
+ const scopeInfo = this.txScope.get(objectStoreName);
+ if (!scopeInfo) {
+ throw Error("object store not found");
+ }
+ const indexInfo = scopeInfo.indexMap.get(oldIndexName);
+ if (!indexInfo) {
+ throw Error("index not found");
+ }
+ // FIXME: Would also be much nicer with numeric UID handles
+ scopeInfo.indexMap.delete(oldIndexName);
+ scopeInfo.indexMap.set(newIndexName, indexInfo);
+ this._prep(sqlRenameIndex).run({
+ index_id: indexInfo.indexId,
+ name: newIndexName,
+ });
+ }
+
+ deleteObjectStore(btx: DatabaseTransaction, name: string): void {
+ const txInfo = this.transactionMap.get(btx.transactionCookie);
+ if (!txInfo) {
+ throw Error("transaction required");
+ }
+ const connInfo = this.connectionMap.get(txInfo.connectionCookie);
+ if (!connInfo) {
+ throw Error("not connected");
+ }
+ // FIXME: Would be much nicer with numeric UID handles
+ const scopeInfo = this.txScope.get(name);
+ if (!scopeInfo) {
+ throw Error("object store not found");
+ }
+ for (const indexInfo of scopeInfo.indexMap.values()) {
+ let stmt: Sqlite3Statement;
+ if (indexInfo.unique) {
+ stmt = this._prep(sqlIUniqueIndexDataDeleteAll);
+ } else {
+ stmt = this._prep(sqlIndexDataDeleteAll);
+ }
+ stmt.run({
+ index_id: indexInfo.indexId,
+ });
+ this._prep(sqlIndexDelete).run({
+ index_id: indexInfo.indexId,
+ });
+ }
+ this._prep(sqlObjectDataDeleteAll).run({
+ object_store_id: scopeInfo.objectStoreId,
+ });
+ this._prep(sqlObjectStoreDelete).run({
+ object_store_id: scopeInfo.objectStoreId,
+ });
+ this.txScope.delete(name);
+ }
+
+ deleteIndex(
+ btx: DatabaseTransaction,
+ objectStoreName: string,
+ indexName: string,
+ ): void {
+ const txInfo = this.transactionMap.get(btx.transactionCookie);
+ if (!txInfo) {
+ throw Error("transaction required");
+ }
+ const connInfo = this.connectionMap.get(txInfo.connectionCookie);
+ if (!connInfo) {
+ throw Error("not connected");
+ }
+ // FIXME: Would be much nicer with numeric UID handles
+ const scopeInfo = this.txScope.get(objectStoreName);
+ if (!scopeInfo) {
+ throw Error("object store not found");
+ }
+ const indexInfo = scopeInfo.indexMap.get(indexName);
+ if (!indexInfo) {
+ throw Error("index not found");
+ }
+ scopeInfo.indexMap.delete(indexName);
+ let stmt: Sqlite3Statement;
+ if (indexInfo.unique) {
+ stmt = this._prep(sqlIUniqueIndexDataDeleteAll);
+ } else {
+ stmt = this._prep(sqlIndexDataDeleteAll);
+ }
+ stmt.run({
+ index_id: indexInfo.indexId,
+ });
+ this._prep(sqlIndexDelete).run({
+ index_id: indexInfo.indexId,
+ });
+ }
+
+ async rollback(btx: DatabaseTransaction): Promise<void> {
+ const txInfo = this.transactionMap.get(btx.transactionCookie);
+ if (!txInfo) {
+ throw Error("transaction not found");
+ }
+ if (this.enableTracing) {
+ console.log(`rolling back transaction ${btx.transactionCookie}`);
+ }
+ if (this.txLevel === TransactionLevel.None) {
+ return;
+ }
+ this._prep(sqlRollback).run();
+ this.txLevel = TransactionLevel.None;
+ this.transactionMap.delete(btx.transactionCookie);
+ this.txScope.clear();
+ this.transactionDoneCond.trigger();
+ }
+
+ async commit(btx: DatabaseTransaction): Promise<void> {
+ const txInfo = this.transactionMap.get(btx.transactionCookie);
+ if (!txInfo) {
+ throw Error("transaction not found");
+ }
+ if (this.enableTracing) {
+ console.log(`committing transaction ${btx.transactionCookie}`);
+ }
+ if (this.txLevel === TransactionLevel.None) {
+ return;
+ }
+ this._prep(sqlCommit).run();
+ this.txLevel = TransactionLevel.None;
+ this.txScope.clear();
+ this.transactionMap.delete(btx.transactionCookie);
+ this.transactionDoneCond.trigger();
+ }
+
+ createObjectStore(
+ btx: DatabaseTransaction,
+ name: string,
+ keyPath: string | string[] | null,
+ autoIncrement: boolean,
+ ): void {
+ const txInfo = this.transactionMap.get(btx.transactionCookie);
+ if (!txInfo) {
+ throw Error("transaction not found");
+ }
+ const connInfo = this.connectionMap.get(txInfo.connectionCookie);
+ if (!connInfo) {
+ throw Error("connection not found");
+ }
+ if (this.txLevel < TransactionLevel.VersionChange) {
+ throw Error("only allowed in versionchange transaction");
+ }
+ if (this.txScope.has(name)) {
+ throw Error("object store already exists");
+ }
+ let myKeyPath = serializeKeyPath(keyPath);
+ const runRes = this._prep(sqlCreateObjectStore).run({
+ name,
+ key_path: myKeyPath,
+ auto_increment: autoIncrement ? 1 : 0,
+ database_name: connInfo.databaseName,
+ });
+ this.txScope.set(name, {
+ objectStoreId: runRes.lastInsertRowid,
+ indexMap: new Map(),
+ });
+ }
+
+ createIndex(
+ btx: DatabaseTransaction,
+ indexName: string,
+ objectStoreName: string,
+ keyPath: string | string[],
+ multiEntry: boolean,
+ unique: boolean,
+ ): void {
+ const txInfo = this.transactionMap.get(btx.transactionCookie);
+ if (!txInfo) {
+ throw Error("transaction not found");
+ }
+ const connInfo = this.connectionMap.get(txInfo.connectionCookie);
+ if (!connInfo) {
+ throw Error("connection not found");
+ }
+ if (this.txLevel < TransactionLevel.VersionChange) {
+ throw Error("only allowed in versionchange transaction");
+ }
+ const scopeInfo = this.txScope.get(objectStoreName);
+ if (!scopeInfo) {
+ throw Error("object store does not exist, can't create index");
+ }
+ if (scopeInfo.indexMap.has(indexName)) {
+ throw Error("index already exists");
+ }
+
+ if (this.enableTracing) {
+ console.log(`creating index "${indexName}"`);
+ }
+
+ const res = this._prep(sqlCreateIndex).run({
+ object_store_id: scopeInfo.objectStoreId,
+ name: indexName,
+ key_path: serializeKeyPath(keyPath),
+ unique: unique ? 1 : 0,
+ multientry: multiEntry ? 1 : 0,
+ });
+ const scopeIndexInfo: ScopeIndexInfo = {
+ indexId: res.lastInsertRowid,
+ keyPath,
+ multiEntry,
+ unique,
+ };
+ scopeInfo.indexMap.set(indexName, scopeIndexInfo);
+
+ // FIXME: We can't use an iterator here, as it's not allowed to
+ // execute a write statement while the iterator executes.
+ // Maybe do multiple selects instead of loading everything into memory?
+ const keyRowsRes = this._prep(sqlObjectDataGetAll).getAll({
+ object_store_id: scopeInfo.objectStoreId,
+ });
+
+ for (const keyRow of keyRowsRes) {
+ assertDbInvariant(typeof keyRow === "object" && keyRow != null);
+ assertDbInvariant("key" in keyRow);
+ assertDbInvariant("value" in keyRow);
+ assertDbInvariant(typeof keyRow.value === "string");
+ const key = keyRow.key;
+ const value = structuredRevive(JSON.parse(keyRow.value));
+ assertDbInvariant(key instanceof Uint8Array);
+ try {
+ this.insertIntoIndex(scopeIndexInfo, key, value);
+ } catch (e) {
+ // FIXME: Catch this in insertIntoIndex!
+ if (e instanceof DataError) {
+ // https://www.w3.org/TR/IndexedDB-2/#object-store-storage-operation
+ // Do nothing
+ } else {
+ throw e;
+ }
+ }
+ }
+ }
+
+ async deleteRecord(
+ btx: DatabaseTransaction,
+ objectStoreName: string,
+ range: BridgeIDBKeyRange,
+ ): Promise<void> {
+ const txInfo = this.transactionMap.get(btx.transactionCookie);
+ if (!txInfo) {
+ throw Error("transaction not found");
+ }
+ const connInfo = this.connectionMap.get(txInfo.connectionCookie);
+ if (!connInfo) {
+ throw Error("connection not found");
+ }
+ if (this.txLevel < TransactionLevel.Write) {
+ throw Error("store operation only allowed while running a transaction");
+ }
+ const scopeInfo = this.txScope.get(objectStoreName);
+ if (!scopeInfo) {
+ throw Error(
+ `object store ${JSON.stringify(
+ objectStoreName,
+ )} not in transaction scope`,
+ );
+ }
+
+ // PERF: We delete keys one-by-one here.
+ // Instead, we could do it with a single
+ // delete query for the object data / index data.
+
+ let currKey: Uint8Array | null = null;
+
+ if (range.lower != null) {
+ const targetKey = serializeKey(range.lower);
+ currKey = this._continueObjectKey({
+ objectStoreId: scopeInfo.objectStoreId,
+ currentKey: null,
+ forward: true,
+ inclusive: true,
+ targetKey,
+ });
+ } else {
+ currKey = this._startObjectKey(scopeInfo.objectStoreId, true);
+ }
+
+ let upperBound: Uint8Array | undefined;
+ if (range.upper != null) {
+ upperBound = serializeKey(range.upper);
+ }
+
+ // loop invariant: (currKey is undefined) or (currKey is a valid key)
+ while (true) {
+ if (!currKey) {
+ break;
+ }
+
+ // FIXME: Check if we're past the range!
+ if (upperBound != null) {
+ const cmp = compareSerializedKeys(currKey, upperBound);
+ if (cmp > 0) {
+ break;
+ }
+ if (cmp == 0 && range.upperOpen) {
+ break;
+ }
+ }
+
+ // Now delete!
+
+ this._prep(sqlObjectDataDeleteKey).run({
+ object_store_id: scopeInfo.objectStoreId,
+ key: currKey,
+ });
+
+ for (const index of scopeInfo.indexMap.values()) {
+ let stmt: Sqlite3Statement;
+ if (index.unique) {
+ stmt = this._prep(sqlUniqueIndexDataDeleteKey);
+ } else {
+ stmt = this._prep(sqlIndexDataDeleteKey);
+ }
+ stmt.run({
+ index_id: index.indexId,
+ object_key: currKey,
+ });
+ }
+
+ currKey = this._continueObjectKey({
+ objectStoreId: scopeInfo.objectStoreId,
+ currentKey: null,
+ forward: true,
+ inclusive: false,
+ targetKey: currKey,
+ });
+ }
+ }
+
+ async storeRecord(
+ btx: DatabaseTransaction,
+ storeReq: RecordStoreRequest,
+ ): Promise<RecordStoreResponse> {
+ const txInfo = this.transactionMap.get(btx.transactionCookie);
+ if (!txInfo) {
+ throw Error("transaction not found");
+ }
+ const connInfo = this.connectionMap.get(txInfo.connectionCookie);
+ if (!connInfo) {
+ throw Error("connection not found");
+ }
+ if (this.txLevel < TransactionLevel.Write) {
+ throw Error("store operation only allowed while running a transaction");
+ }
+ const scopeInfo = this.txScope.get(storeReq.objectStoreName);
+ if (!scopeInfo) {
+ throw Error(
+ `object store ${JSON.stringify(
+ storeReq.objectStoreName,
+ )} not in transaction scope`,
+ );
+ }
+ const metaRes = this._prep(sqlGetObjectStoreMetaById).getFirst({
+ id: scopeInfo.objectStoreId,
+ });
+ if (metaRes === undefined) {
+ throw Error(
+ `object store ${JSON.stringify(
+ storeReq.objectStoreName,
+ )} does not exist`,
+ );
+ }
+ assertDbInvariant(!!metaRes && typeof metaRes === "object");
+ assertDbInvariant("key_path" in metaRes);
+ assertDbInvariant("auto_increment" in metaRes);
+ const dbKeyPath = metaRes.key_path;
+ assertDbInvariant(dbKeyPath === null || typeof dbKeyPath === "string");
+ const keyPath = deserializeKeyPath(dbKeyPath);
+ const autoIncrement = metaRes.auto_increment;
+ assertDbInvariant(typeof autoIncrement === "number");
+
+ let key;
+ let value;
+ let updatedKeyGenerator: number | undefined;
+
+ if (storeReq.storeLevel === StoreLevel.UpdateExisting) {
+ if (storeReq.key == null) {
+ throw Error("invalid update request (key not given)");
+ }
+ key = storeReq.key;
+ value = storeReq.value;
+ } else {
+ if (keyPath != null && storeReq.key !== undefined) {
+ // If in-line keys are used, a key can't be explicitly specified.
+ throw new DataError();
+ }
+
+ const storeKeyResult = makeStoreKeyValue({
+ value: storeReq.value,
+ key: storeReq.key,
+ currentKeyGenerator: autoIncrement,
+ autoIncrement: autoIncrement != 0,
+ keyPath: keyPath,
+ });
+
+ if (autoIncrement != 0) {
+ updatedKeyGenerator = storeKeyResult.updatedKeyGenerator;
+ }
+
+ key = storeKeyResult.key;
+ value = storeKeyResult.value;
+ }
+
+ const serializedObjectKey = serializeKey(key);
+
+ const existingObj = this._getObjectValue(
+ scopeInfo.objectStoreId,
+ serializedObjectKey,
+ );
+
+ if (storeReq.storeLevel === StoreLevel.NoOverwrite) {
+ if (existingObj) {
+ throw new ConstraintError();
+ }
+ }
+
+ this._prep(sqlInsertObjectData).run({
+ object_store_id: scopeInfo.objectStoreId,
+ key: serializedObjectKey,
+ value: JSON.stringify(structuredEncapsulate(value)),
+ });
+
+ if (autoIncrement != 0) {
+ this._prep(sqlUpdateAutoIncrement).run({
+ object_store_id: scopeInfo.objectStoreId,
+ auto_increment: updatedKeyGenerator,
+ });
+ }
+
+ for (const [k, indexInfo] of scopeInfo.indexMap.entries()) {
+ if (existingObj) {
+ this.deleteFromIndex(
+ indexInfo.indexId,
+ indexInfo.unique,
+ serializedObjectKey,
+ );
+ }
+
+ try {
+ this.insertIntoIndex(indexInfo, serializedObjectKey, value);
+ } catch (e) {
+ // FIXME: handle this in insertIntoIndex!
+ if (e instanceof DataError) {
+ // We don't propagate this error here.
+ continue;
+ }
+ throw e;
+ }
+ }
+
+ if (this.trackStats) {
+ this.accessStats.writesPerStore[storeReq.objectStoreName] =
+ (this.accessStats.writesPerStore[storeReq.objectStoreName] ?? 0) + 1;
+ }
+
+ return {
+ key: key,
+ };
+ }
+
+ private deleteFromIndex(
+ indexId: SqliteRowid,
+ indexUnique: boolean,
+ objectKey: Uint8Array,
+ ): void {
+ let stmt: Sqlite3Statement;
+ if (indexUnique) {
+ stmt = this._prep(sqlUniqueIndexDataDeleteKey);
+ } else {
+ stmt = this._prep(sqlIndexDataDeleteKey);
+ }
+ stmt.run({
+ index_id: indexId,
+ object_key: objectKey,
+ });
+ }
+
+ private insertIntoIndex(
+ indexInfo: ScopeIndexInfo,
+ primaryKey: Uint8Array,
+ value: any,
+ ): void {
+ const indexKeys = getIndexKeys(
+ value,
+ indexInfo.keyPath,
+ indexInfo.multiEntry,
+ );
+ if (!indexKeys.length) {
+ return;
+ }
+
+ let stmt;
+ if (indexInfo.unique) {
+ stmt = this._prep(sqlInsertUniqueIndexData);
+ } else {
+ stmt = this._prep(sqlInsertIndexData);
+ }
+
+ for (const indexKey of indexKeys) {
+ // FIXME: Re-throw correct error for unique index violations
+ const serializedIndexKey = serializeKey(indexKey);
+ try {
+ stmt.run({
+ index_id: indexInfo.indexId,
+ object_key: primaryKey,
+ index_key: serializedIndexKey,
+ });
+ } catch (e: any) {
+ if (e.code === SqliteError.constraintPrimarykey) {
+ throw new ConstraintError();
+ }
+ throw e;
+ }
+ }
+ }
+
+ async clearObjectStore(
+ btx: DatabaseTransaction,
+ objectStoreName: string,
+ ): Promise<void> {
+ const txInfo = this.transactionMap.get(btx.transactionCookie);
+ if (!txInfo) {
+ throw Error("transaction not found");
+ }
+ const connInfo = this.connectionMap.get(txInfo.connectionCookie);
+ if (!connInfo) {
+ throw Error("connection not found");
+ }
+ if (this.txLevel < TransactionLevel.Write) {
+ throw Error("store operation only allowed while running a transaction");
+ }
+ const scopeInfo = this.txScope.get(objectStoreName);
+ if (!scopeInfo) {
+ throw Error(
+ `object store ${JSON.stringify(
+ objectStoreName,
+ )} not in transaction scope`,
+ );
+ }
+
+ this._prep(sqlClearObjectStore).run({
+ object_store_id: scopeInfo.objectStoreId,
+ });
+
+ for (const index of scopeInfo.indexMap.values()) {
+ let stmt: Sqlite3Statement;
+ if (index.unique) {
+ stmt = this._prep(sqlClearUniqueIndexData);
+ } else {
+ stmt = this._prep(sqlClearIndexData);
+ }
+ stmt.run({
+ index_id: index.indexId,
+ });
+ }
+ }
+}
+
+const schemaSql = `
+CREATE TABLE IF NOT EXISTS databases
+( name TEXT PRIMARY KEY
+, version INTEGER NOT NULL
+);
+
+CREATE TABLE IF NOT EXISTS object_stores
+( id INTEGER PRIMARY KEY
+, database_name NOT NULL
+, name TEXT NOT NULL
+, key_path TEXT
+, auto_increment INTEGER NOT NULL DEFAULT 0
+, FOREIGN KEY (database_name)
+ REFERENCES databases(name)
+);
+
+CREATE TABLE IF NOT EXISTS indexes
+( id INTEGER PRIMARY KEY
+, object_store_id INTEGER NOT NULL
+, name TEXT NOT NULL
+, key_path TEXT NOT NULL
+, unique_index INTEGER NOT NULL
+, multientry INTEGER NOT NULL
+, FOREIGN KEY (object_store_id)
+ REFERENCES object_stores(id)
+);
+
+CREATE TABLE IF NOT EXISTS object_data
+( object_store_id INTEGER NOT NULL
+, key BLOB NOT NULL
+, value TEXT NOT NULL
+, PRIMARY KEY (object_store_id, key)
+);
+
+CREATE TABLE IF NOT EXISTS index_data
+( index_id INTEGER NOT NULL
+, index_key BLOB NOT NULL
+, object_key BLOB NOT NULL
+, PRIMARY KEY (index_id, index_key, object_key)
+, FOREIGN KEY (index_id)
+ REFERENCES indexes(id)
+);
+
+CREATE TABLE IF NOT EXISTS unique_index_data
+( index_id INTEGER NOT NULL
+, index_key BLOB NOT NULL
+, object_key BLOB NOT NULL
+, PRIMARY KEY (index_id, index_key)
+, FOREIGN KEY (index_id)
+ REFERENCES indexes(id)
+);
+`;
+
+const sqlClearObjectStore = `
+DELETE FROM object_data WHERE object_store_id=$object_store_id`;
+
+const sqlClearIndexData = `
+DELETE FROM index_data WHERE index_id=$index_id`;
+
+const sqlClearUniqueIndexData = `
+DELETE FROM unique_index_data WHERE index_id=$index_id`;
+
+const sqlListDatabases = `
+SELECT name, version FROM databases;
+`;
+
+const sqlGetDatabaseVersion = `
+SELECT version FROM databases WHERE name=$name;
+`;
+
+const sqlBegin = `BEGIN;`;
+const sqlCommit = `COMMIT;`;
+const sqlRollback = `ROLLBACK;`;
+
+const sqlCreateDatabase = `
+INSERT INTO databases (name, version) VALUES ($name, 1);
+`;
+
+const sqlDeleteDatabase = `
+DELETE FROM databases
+WHERE name=$name;
+`;
+
+const sqlCreateObjectStore = `
+INSERT INTO object_stores (name, database_name, key_path, auto_increment)
+ VALUES ($name, $database_name, $key_path, $auto_increment);
+`;
+
+const sqlObjectStoreDelete = `
+DELETE FROM object_stores
+WHERE id=$object_store_id;`;
+
+const sqlObjectDataDeleteAll = `
+DELETE FROM object_data
+WHERE object_store_id=$object_store_id`;
+
+const sqlIndexDelete = `
+DELETE FROM indexes
+WHERE id=$index_id;
+`;
+
+const sqlIndexDataDeleteAll = `
+DELETE FROM index_data
+WHERE index_id=$index_id;
+`;
+
+const sqlIUniqueIndexDataDeleteAll = `
+DELETE FROM unique_index_data
+WHERE index_id=$index_id;
+`;
+
+const sqlCreateIndex = `
+INSERT INTO indexes (object_store_id, name, key_path, unique_index, multientry)
+ VALUES ($object_store_id, $name, $key_path, $unique, $multientry);
+`;
+
+const sqlInsertIndexData = `
+INSERT INTO index_data (index_id, object_key, index_key)
+ VALUES ($index_id, $object_key, $index_key);`;
+
+const sqlInsertUniqueIndexData = `
+INSERT INTO unique_index_data (index_id, object_key, index_key)
+ VALUES ($index_id, $object_key, $index_key);`;
+
+const sqlUpdateDbVersion = `
+UPDATE databases
+ SET version=$version
+ WHERE name=$name;
+`;
+
+const sqlRenameObjectStore = `
+UPDATE object_stores
+ SET name=$name
+ WHERE id=$object_store_id`;
+
+const sqlRenameIndex = `
+UPDATE indexes
+ SET name=$name
+ WHERE index_id=$index_id`;
+
+const sqlGetObjectStoresByDatabase = `
+SELECT id, name, key_path, auto_increment
+FROM object_stores
+WHERE database_name=$database_name;
+`;
+
+const sqlGetObjectStoreMetaById = `
+SELECT key_path, auto_increment
+FROM object_stores
+WHERE id = $id;
+`;
+
+const sqlGetObjectStoreMetaByName = `
+SELECT id, key_path, auto_increment
+FROM object_stores
+WHERE database_name=$database_name AND name=$name;
+`;
+
+const sqlGetIndexesByObjectStoreId = `
+SELECT id, name, key_path, unique_index, multientry
+FROM indexes
+WHERE object_store_id=$object_store_id
+`;
+
+const sqlGetIndexByName = `
+SELECT id, key_path, unique_index, multientry
+FROM indexes
+WHERE object_store_id=$object_store_id
+ AND name=$name
+`;
+
+const sqlInsertObjectData = `
+INSERT OR REPLACE INTO object_data(object_store_id, key, value)
+ VALUES ($object_store_id, $key, $value);
+`;
+
+const sqlUpdateAutoIncrement = `
+UPDATE object_stores
+ SET auto_increment=$auto_increment
+ WHERE id=$object_store_id
+`;
+
+const sqlObjectDataValueFromKey = `
+SELECT value FROM object_data
+ WHERE object_store_id=$object_store_id
+ AND key=$key;
+`;
+
+const sqlObjectDataGetAll = `
+SELECT key, value FROM object_data
+ WHERE object_store_id=$object_store_id;`;
+
+const sqlObjectDataStartForward = `
+SELECT min(key) as rkey FROM object_data
+ WHERE object_store_id=$object_store_id;`;
+
+const sqlObjectDataStartBackward = `
+SELECT max(key) as rkey FROM object_data
+ WHERE object_store_id=$object_store_id;`;
+
+const sqlObjectDataContinueForward = `
+SELECT min(key) as rkey FROM object_data
+ WHERE object_store_id=$object_store_id
+ AND key > $x;`;
+
+const sqlObjectDataContinueBackward = `
+SELECT max(key) as rkey FROM object_data
+ WHERE object_store_id=$object_store_id
+ AND key < $x;`;
+
+const sqlObjectDataContinueForwardInclusive = `
+SELECT min(key) as rkey FROM object_data
+ WHERE object_store_id=$object_store_id
+ AND key >= $x;`;
+
+const sqlObjectDataContinueBackwardInclusive = `
+SELECT max(key) as rkey FROM object_data
+ WHERE object_store_id=$object_store_id
+ AND key <= $x;`;
+
+const sqlObjectDataDeleteKey = `
+DELETE FROM object_data
+ WHERE object_store_id=$object_store_id AND
+ key=$key`;
+
+const sqlIndexDataDeleteKey = `
+DELETE FROM index_data
+ WHERE index_id=$index_id AND
+ object_key=$object_key;
+`;
+
+const sqlUniqueIndexDataDeleteKey = `
+DELETE FROM unique_index_data
+ WHERE index_id=$index_id AND
+ object_key=$object_key;
+`;
+
+// "next" or "nextunique" on a non-unique index
+const sqlIndexDataStartForward = `
+SELECT index_key, object_key FROM index_data
+ WHERE index_id=$index_id
+ ORDER BY index_key, object_key
+ LIMIT 1;
+`;
+
+// start a "next" or "nextunique" on a unique index
+const sqlUniqueIndexDataStartForward = `
+SELECT index_key, object_key FROM unique_index_data
+ WHERE index_id=$index_id
+ ORDER BY index_key, object_key
+ LIMIT 1;
+`;
+
+// start a "prev" or "prevunique" on a unique index
+const sqlUniqueIndexDataStartBackward = `
+SELECT index_key, object_key FROM unique_index_data
+ WHERE index_id=$index_id
+ ORDER BY index_key DESC, object_key DESC
+ LIMIT 1
+`;
+
+// start a "prevunique" query on a non-unique index
+const sqlIndexDataStartBackwardUnique = `
+SELECT index_key, object_key FROM index_data
+ WHERE index_id=$index_id
+ ORDER BY index_key DESC, object_key ASC
+ LIMIT 1
+`;
+
+// start a "prev" query on a non-unique index
+const sqlIndexDataStartBackward = `
+SELECT index_key, object_key FROM index_data
+ WHERE index_id=$index_id
+ ORDER BY index_key DESC, object_key DESC
+ LIMIT 1
+`;
+
+// continue a "next" query, strictly go to a further key
+const sqlIndexDataContinueForwardStrict = `
+SELECT index_key, object_key FROM index_data
+ WHERE
+ index_id=$index_id AND
+ ((index_key = $index_key AND object_key > $object_key) OR
+ (index_key > $index_key))
+ ORDER BY index_key, object_key
+ LIMIT 1;
+`;
+
+// continue a "next" query, go to at least the specified key
+const sqlIndexDataContinueForwardInclusive = `
+SELECT index_key, object_key FROM index_data
+ WHERE
+ index_id=$index_id AND
+ ((index_key = $index_key AND object_key >= $object_key) OR
+ (index_key > $index_key))
+ ORDER BY index_key, object_key
+ LIMIT 1;
+`;
+
+// continue a "prev" query
+const sqlIndexDataContinueBackwardStrict = `
+SELECT index_key, object_key FROM index_data
+ WHERE
+ index_id=$index_id AND
+ ((index_key = $index_key AND object_key < $object_key) OR
+ (index_key < $index_key))
+ ORDER BY index_key DESC, object_key DESC
+ LIMIT 1;
+`;
+
+// continue a "prev" query
+const sqlIndexDataContinueBackwardInclusive = `
+SELECT index_key, object_key FROM index_data
+ WHERE
+ index_id=$index_id AND
+ ((index_key = $index_key AND object_key <= $object_key) OR
+ (index_key < $index_key))
+ ORDER BY index_key DESC, object_key DESC
+ LIMIT 1;
+`;
+
+// continue a "prevunique" query
+const sqlIndexDataContinueBackwardStrictUnique = `
+SELECT index_key, object_key FROM index_data
+ WHERE index_id=$index_id AND index_key < $index_key
+ ORDER BY index_key DESC, object_key ASC
+ LIMIT 1;
+`;
+
+// continue a "prevunique" query
+const sqlIndexDataContinueBackwardInclusiveUnique = `
+SELECT index_key, object_key FROM index_data
+ WHERE index_id=$index_id AND index_key <= $index_key
+ ORDER BY index_key DESC, object_key ASC
+ LIMIT 1;
+`;
+
+// continue a "next" query, no target object key
+const sqlIndexDataContinueForwardStrictUnique = `
+SELECT index_key, object_key FROM index_data
+ WHERE index_id=$index_id AND index_key > $index_key
+ ORDER BY index_key, object_key
+ LIMIT 1;
+`;
+
+// continue a "next" query, no target object key
+const sqlIndexDataContinueForwardInclusiveUnique = `
+SELECT index_key, object_key FROM index_data
+ WHERE index_id=$index_id AND index_key >= $index_key
+ ORDER BY index_key, object_key
+ LIMIT 1;
+`;
+
+// continue a "next" query, strictly go to a further key
+const sqlUniqueIndexDataContinueForwardStrict = `
+SELECT index_key, object_key FROM unique_index_data
+ WHERE index_id=$index_id AND index_key > $index_key
+ ORDER BY index_key, object_key
+ LIMIT 1;
+`;
+
+// continue a "next" query, go to at least the specified key
+const sqlUniqueIndexDataContinueForwardInclusive = `
+SELECT index_key, object_key FROM unique_index_data
+ WHERE index_id=$index_id AND index_key >= $index_key
+ ORDER BY index_key, object_key
+ LIMIT 1;
+`;
+
+// continue a "prev" query
+const sqlUniqueIndexDataContinueBackwardStrict = `
+SELECT index_key, object_key FROM unique_index_data
+ WHERE index_id=$index_id AND index_key < $index_key
+ ORDER BY index_key, object_key
+ LIMIT 1;
+`;
+
+// continue a "prev" query
+const sqlUniqueIndexDataContinueBackwardInclusive = `
+SELECT index_key, object_key FROM unique_index_data
+ WHERE index_id=$index_id AND index_key <= $index_key
+ ORDER BY index_key DESC, object_key DESC
+ LIMIT 1;
+`;
+
+export interface SqliteBackendOptions {
+ filename: string;
+}
+
+export async function createSqliteBackend(
+ sqliteImpl: Sqlite3Interface,
+ options: SqliteBackendOptions,
+): Promise<SqliteBackend> {
+ const db = sqliteImpl.open(options.filename);
+ db.exec("PRAGMA foreign_keys = ON;");
+ db.exec(schemaSql);
+ return new SqliteBackend(sqliteImpl, db);
+}
diff --git a/packages/idb-bridge/src/backend-common.ts b/packages/idb-bridge/src/backend-common.ts
new file mode 100644
index 000000000..d52071939
--- /dev/null
+++ b/packages/idb-bridge/src/backend-common.ts
@@ -0,0 +1,29 @@
+import { openPromise } from "./util/openPromise.js";
+
+export class AsyncCondition {
+ _waitPromise: Promise<void>;
+ _resolveWaitPromise: () => void;
+ constructor() {
+ const op = openPromise<void>();
+ this._waitPromise = op.promise;
+ this._resolveWaitPromise = op.resolve;
+ }
+
+ wait(): Promise<void> {
+ return this._waitPromise;
+ }
+
+ trigger(): void {
+ this._resolveWaitPromise();
+ const op = openPromise<void>();
+ this._waitPromise = op.promise;
+ this._resolveWaitPromise = op.resolve;
+ }
+}
+
+export enum TransactionLevel {
+ None = 0,
+ Read = 1,
+ Write = 2,
+ VersionChange = 3,
+}
diff --git a/packages/idb-bridge/src/backend-interface.ts b/packages/idb-bridge/src/backend-interface.ts
index a21515544..690f92f54 100644
--- a/packages/idb-bridge/src/backend-interface.ts
+++ b/packages/idb-bridge/src/backend-interface.ts
@@ -21,66 +21,45 @@ import {
IDBValidKey,
} from "./idbtypes.js";
-/** @public */
-export interface ObjectStoreProperties {
- keyPath: string[] | null;
- autoIncrement: boolean;
- indexes: { [nameame: string]: IndexProperties };
-}
-
-/** @public */
-export interface IndexProperties {
- keyPath: string[];
- multiEntry: boolean;
- unique: boolean;
-}
-
-/** @public */
-export interface Schema {
- databaseName: string;
- databaseVersion: number;
- objectStores: { [name: string]: ObjectStoreProperties };
+export interface ConnectResult {
+ conn: DatabaseConnection;
+ version: number;
+ objectStores: string[];
}
-/** @public */
export interface DatabaseConnection {
connectionCookie: string;
}
-/** @public */
export interface DatabaseTransaction {
transactionCookie: string;
}
-/** @public */
export enum ResultLevel {
OnlyCount,
OnlyKeys,
Full,
}
-/** @public */
export enum StoreLevel {
NoOverwrite,
AllowOverwrite,
UpdateExisting,
}
-/** @public */
-export interface RecordGetRequest {
+
+export interface IndexGetQuery {
direction: IDBCursorDirection;
objectStoreName: string;
- indexName: string | undefined;
+ indexName: string;
/**
* The range of keys to return.
- * If indexName is defined, the range refers to the index keys.
- * Otherwise it refers to the object store keys.
+ * The range refers to the index keys.
*/
range: BridgeIDBKeyRange | undefined | null;
/**
* Last cursor position in terms of the index key.
- * Can only be specified if indexName is defined and
- * lastObjectStorePosition is defined.
+ * Can only be specified if lastObjectStorePosition is defined.
*
* Must either be undefined or within range.
*/
@@ -92,8 +71,6 @@ export interface RecordGetRequest {
/**
* If specified, the index key of the results must be
* greater or equal to advanceIndexKey.
- *
- * Only applicable if indexName is specified.
*/
advanceIndexKey?: IDBValidKey;
/**
@@ -109,7 +86,31 @@ export interface RecordGetRequest {
resultLevel: ResultLevel;
}
-/** @public */
+export interface ObjectStoreGetQuery {
+ direction: IDBCursorDirection;
+ objectStoreName: string;
+ /**
+ * The range of keys to return.
+ * Refers to the object store keys.
+ */
+ range: BridgeIDBKeyRange | undefined | null;
+ /**
+ * Last position in terms of the object store key.
+ */
+ lastObjectStorePosition?: IDBValidKey;
+ /**
+ * If specified, the primary key of the results must be greater
+ * or equal to advancePrimaryKey.
+ */
+ advancePrimaryKey?: IDBValidKey;
+ /**
+ * Maximum number of results to return.
+ * If 0, return all available results
+ */
+ limit: number;
+ resultLevel: ResultLevel;
+}
+
export interface RecordGetResponse {
values: any[] | undefined;
indexKeys: IDBValidKey[] | undefined;
@@ -117,7 +118,6 @@ export interface RecordGetResponse {
count: number;
}
-/** @public */
export interface RecordStoreRequest {
objectStoreName: string;
value: any;
@@ -125,7 +125,6 @@ export interface RecordStoreRequest {
storeLevel: StoreLevel;
}
-/** @public */
export interface RecordStoreResponse {
/**
* Key that the record was stored under in the object store.
@@ -133,38 +132,79 @@ export interface RecordStoreResponse {
key: IDBValidKey;
}
-/** @public */
+export interface ObjectStoreMeta {
+ indexSet: string[];
+ keyPath: string | string[] | null;
+ autoIncrement: boolean;
+}
+
+export interface IndexMeta {
+ keyPath: string | string[];
+ multiEntry: boolean;
+ unique: boolean;
+}
+
+// FIXME: Instead of referring to an object store by name,
+// maybe refer to it via some internal, numeric ID?
+// This would simplify renaming.
export interface Backend {
getDatabases(): Promise<BridgeIDBDatabaseInfo[]>;
- connectDatabase(name: string): Promise<DatabaseConnection>;
+ connectDatabase(name: string): Promise<ConnectResult>;
beginTransaction(
- conn: DatabaseConnection,
+ dbConn: DatabaseConnection,
objectStores: string[],
mode: IDBTransactionMode,
): Promise<DatabaseTransaction>;
enterVersionChange(
- conn: DatabaseConnection,
+ dbConn: DatabaseConnection,
newVersion: number,
): Promise<DatabaseTransaction>;
deleteDatabase(name: string): Promise<void>;
- close(db: DatabaseConnection): Promise<void>;
+ close(dbConn: DatabaseConnection): Promise<void>;
- getSchema(db: DatabaseConnection): Schema;
+ // FIXME: Use this for connection
+ // prepareConnect() - acquires a lock, maybe enters a version change transaction?
+ // finishConnect() - after possible versionchange is done, allow others to connect
- getCurrentTransactionSchema(btx: DatabaseTransaction): Schema;
+ /**
+ * Get metadata for an object store.
+ *
+ * When dbConn is running a version change transaction,
+ * the current schema (and not the initial schema) is returned.
+ *
+ * Caller may mutate the result, a new object
+ * is returned on each call.
+ */
+ getObjectStoreMeta(
+ dbConn: DatabaseConnection,
+ objectStoreName: string,
+ ): ObjectStoreMeta | undefined;
- getInitialTransactionSchema(btx: DatabaseTransaction): Schema;
+ /**
+ * Get metadata for an index.
+ *
+ * When dbConn is running a version change transaction,
+ * the current schema (and not the initial schema) is returned.
+ *
+ * Caller may mutate the result, a new object
+ * is returned on each call.
+ */
+ getIndexMeta(
+ dbConn: DatabaseConnection,
+ objectStoreName: string,
+ indexName: string,
+ ): IndexMeta | undefined;
renameIndex(
btx: DatabaseTransaction,
objectStoreName: string,
- oldName: string,
- newName: string,
+ oldIndexName: string,
+ newIndexName: string,
): void;
deleteIndex(
@@ -173,8 +213,9 @@ export interface Backend {
indexName: string,
): void;
- rollback(btx: DatabaseTransaction): Promise<void>;
+ rollback(btx: DatabaseTransaction): void;
+ // FIXME: Should probably not be async
commit(btx: DatabaseTransaction): Promise<void>;
deleteObjectStore(btx: DatabaseTransaction, name: string): void;
@@ -207,9 +248,14 @@ export interface Backend {
range: BridgeIDBKeyRange,
): Promise<void>;
- getRecords(
+ getObjectStoreRecords(
+ btx: DatabaseTransaction,
+ req: ObjectStoreGetQuery,
+ ): Promise<RecordGetResponse>;
+
+ getIndexRecords(
btx: DatabaseTransaction,
- req: RecordGetRequest,
+ req: IndexGetQuery,
): Promise<RecordGetResponse>;
storeRecord(
diff --git a/packages/idb-bridge/src/backends.test.ts b/packages/idb-bridge/src/backends.test.ts
new file mode 100644
index 000000000..684358eac
--- /dev/null
+++ b/packages/idb-bridge/src/backends.test.ts
@@ -0,0 +1,740 @@
+/*
+ Copyright 2019 Florian Dold
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ or implied. See the License for the specific language governing
+ permissions and limitations under the License.
+ */
+
+/**
+ * Tests that are backend-generic.
+ * See testingdb.ts for the backend selection in test runs.
+ */
+
+/**
+ * Imports.
+ */
+import test from "ava";
+import {
+ BridgeIDBCursorWithValue,
+ BridgeIDBDatabase,
+ BridgeIDBFactory,
+ BridgeIDBKeyRange,
+ BridgeIDBTransaction,
+} from "./bridge-idb.js";
+import {
+ IDBCursorDirection,
+ IDBCursorWithValue,
+ IDBDatabase,
+ IDBKeyRange,
+ IDBRequest,
+ IDBValidKey,
+} from "./idbtypes.js";
+import { initTestIndexedDB, useTestIndexedDb } from "./testingdb.js";
+import { MemoryBackend } from "./MemoryBackend.js";
+import { promiseFromRequest, promiseFromTransaction } from "./idbpromutil.js";
+
+test.before("test DB initialization", initTestIndexedDB);
+
+test("Spec: Example 1 Part 1", async (t) => {
+ const idb = useTestIndexedDb();
+
+ const dbname = "library-" + new Date().getTime() + Math.random();
+
+ const request = idb.open(dbname);
+ request.onupgradeneeded = () => {
+ const db = request.result as BridgeIDBDatabase;
+ const store = db.createObjectStore("books", { keyPath: "isbn" });
+ const titleIndex = store.createIndex("by_title", "title", { unique: true });
+ const authorIndex = store.createIndex("by_author", "author");
+
+ // Populate with initial data.
+ store.put({ title: "Quarry Memories", author: "Fred", isbn: 123456 });
+ store.put({ title: "Water Buffaloes", author: "Fred", isbn: 234567 });
+ store.put({ title: "Bedrock Nights", author: "Barney", isbn: 345678 });
+ };
+
+ await promiseFromRequest(request);
+ t.pass();
+});
+
+test("Spec: Example 1 Part 2", async (t) => {
+ const idb = useTestIndexedDb();
+
+ const dbname = "library-" + new Date().getTime() + Math.random();
+
+ const request = idb.open(dbname);
+ request.onupgradeneeded = () => {
+ const db = request.result;
+ const store = db.createObjectStore("books", { keyPath: "isbn" });
+ const titleIndex = store.createIndex("by_title", "title", { unique: true });
+ const authorIndex = store.createIndex("by_author", "author");
+ };
+
+ const db: BridgeIDBDatabase = await promiseFromRequest(request);
+
+ t.is(db.name, dbname);
+
+ const tx = db.transaction("books", "readwrite");
+ tx.oncomplete = () => {
+ console.log("oncomplete called");
+ };
+
+ const store = tx.objectStore("books");
+
+ store.put({ title: "Quarry Memories", author: "Fred", isbn: 123456 });
+ store.put({ title: "Water Buffaloes", author: "Fred", isbn: 234567 });
+ store.put({ title: "Bedrock Nights", author: "Barney", isbn: 345678 });
+
+ await promiseFromTransaction(tx);
+
+ t.pass();
+});
+
+test("duplicate index insertion", async (t) => {
+ const idb = useTestIndexedDb();
+
+ const dbname = "library-" + new Date().getTime() + Math.random();
+
+ const request = idb.open(dbname);
+ request.onupgradeneeded = () => {
+ const db = request.result;
+ const store = db.createObjectStore("books", { keyPath: "isbn" });
+ const titleIndex = store.createIndex("by_title", "title", { unique: true });
+ const authorIndex = store.createIndex("by_author", "author");
+ };
+
+ const db: BridgeIDBDatabase = await promiseFromRequest(request);
+
+ t.is(db.name, dbname);
+
+ const tx = db.transaction("books", "readwrite");
+ tx.oncomplete = () => {
+ console.log("oncomplete called");
+ };
+
+ const store = tx.objectStore("books");
+
+ store.put({ title: "Quarry Memories", author: "Fred", isbn: 123456 });
+
+ // Change the index key, keep primary key (isbn) the same.
+ store.put({ title: "Water Buffaloes", author: "Bla", isbn: 234567 });
+ store.put({ title: "Water Buffaloes", author: "Fred", isbn: 234567 });
+
+ store.put({ title: "Bedrock Nights", author: "Barney", isbn: 345678 });
+
+ await promiseFromTransaction(tx);
+
+ const tx3 = db.transaction(["books"], "readonly");
+ const store3 = tx3.objectStore("books");
+ const index3 = store3.index("by_author");
+ const request3 = index3.openCursor();
+
+ const authorList: string[] = [];
+
+ await promiseFromRequest(request3);
+ while (request3.result != null) {
+ const cursor: IDBCursorWithValue = request3.result;
+ authorList.push(cursor.value.author);
+ cursor.continue();
+ await promiseFromRequest(request3);
+ }
+
+ t.deepEqual(authorList, ["Barney", "Fred", "Fred"]);
+
+ t.pass();
+});
+
+test("simple index iteration", async (t) => {
+ const idb = useTestIndexedDb();
+ const dbname = "library-" + new Date().getTime() + Math.random();
+ const request = idb.open(dbname);
+ request.onupgradeneeded = () => {
+ const db = request.result;
+ const store = db.createObjectStore("books", { keyPath: "isbn" });
+ const titleIndex = store.createIndex("by_title", "title", { unique: true });
+ const authorIndex = store.createIndex("by_author", "author");
+ };
+
+ const db: BridgeIDBDatabase = await promiseFromRequest(request);
+ const tx = db.transaction("books", "readwrite");
+ const store = tx.objectStore("books");
+
+ store.put({ title: "Bedrock Nights", author: "Barney", isbn: 345678 });
+ store.put({ title: "Quarry Memories", author: "Fred", isbn: 123456 });
+ store.put({ title: "Water Buffaloes", author: "Fred", isbn: 234567 });
+
+ await promiseFromTransaction(tx);
+
+ const tx3 = db.transaction(["books"], "readonly");
+ const store3 = tx3.objectStore("books");
+ const index3 = store3.index("by_author");
+ const request3 = index3.openCursor(BridgeIDBKeyRange.only("Fred"));
+
+ await promiseFromRequest(request3);
+
+ let cursor: BridgeIDBCursorWithValue | null;
+ cursor = request3.result as BridgeIDBCursorWithValue;
+ t.is(cursor.value.author, "Fred");
+ t.is(cursor.value.isbn, 123456);
+
+ cursor.continue();
+
+ await promiseFromRequest(request3);
+
+ t.is(cursor.value.author, "Fred");
+ t.is(cursor.value.isbn, 234567);
+
+ cursor.continue();
+
+ await promiseFromRequest(request3);
+
+ t.is(cursor.value, undefined);
+});
+
+test("Spec: Example 1 Part 3", async (t) => {
+ const idb = useTestIndexedDb();
+ const dbname = "library-" + new Date().getTime() + Math.random();
+ const request = idb.open(dbname);
+ request.onupgradeneeded = () => {
+ const db = request.result;
+ const store = db.createObjectStore("books", { keyPath: "isbn" });
+ const titleIndex = store.createIndex("by_title", "title", { unique: true });
+ const authorIndex = store.createIndex("by_author", "author");
+ };
+
+ const db: BridgeIDBDatabase = await promiseFromRequest(request);
+
+ t.is(db.name, dbname);
+
+ const tx = db.transaction("books", "readwrite");
+
+ const store = tx.objectStore("books");
+
+ store.put({ title: "Bedrock Nights", author: "Barney", isbn: 345678 });
+ store.put({ title: "Quarry Memories", author: "Fred", isbn: 123456 });
+ store.put({ title: "Water Buffaloes", author: "Fred", isbn: 234567 });
+
+ await promiseFromTransaction(tx);
+
+ const tx2 = db.transaction("books", "readonly");
+ const store2 = tx2.objectStore("books");
+ var index2 = store2.index("by_title");
+ const request2 = index2.get("Bedrock Nights");
+ const result2: any = await promiseFromRequest(request2);
+
+ t.is(result2.author, "Barney");
+
+ const tx3 = db.transaction(["books"], "readonly");
+ const store3 = tx3.objectStore("books");
+ const index3 = store3.index("by_author");
+ const request3 = index3.openCursor(BridgeIDBKeyRange.only("Fred"));
+
+ await promiseFromRequest(request3);
+
+ let cursor: BridgeIDBCursorWithValue | null;
+ cursor = request3.result as BridgeIDBCursorWithValue;
+ t.is(cursor.value.author, "Fred");
+ t.is(cursor.value.isbn, 123456);
+
+ cursor.continue();
+
+ await promiseFromRequest(request3);
+
+ cursor = request3.result as BridgeIDBCursorWithValue;
+ t.is(cursor.value.author, "Fred");
+ t.is(cursor.value.isbn, 234567);
+
+ await promiseFromTransaction(tx3);
+
+ const tx4 = db.transaction("books", "readonly");
+ const store4 = tx4.objectStore("books");
+ const request4 = store4.openCursor();
+
+ await promiseFromRequest(request4);
+
+ cursor = request4.result;
+ if (!cursor) {
+ throw new Error();
+ }
+ t.is(cursor.value.isbn, 123456);
+
+ cursor.continue();
+
+ await promiseFromRequest(request4);
+
+ cursor = request4.result;
+ if (!cursor) {
+ throw new Error();
+ }
+ t.is(cursor.value.isbn, 234567);
+
+ cursor.continue();
+
+ await promiseFromRequest(request4);
+
+ cursor = request4.result;
+ if (!cursor) {
+ throw new Error();
+ }
+ t.is(cursor.value.isbn, 345678);
+
+ cursor.continue();
+ await promiseFromRequest(request4);
+
+ cursor = request4.result;
+
+ t.is(cursor, null);
+
+ const tx5 = db.transaction("books", "readonly");
+ const store5 = tx5.objectStore("books");
+ const index5 = store5.index("by_author");
+
+ const request5 = index5.openCursor(null, "next");
+
+ await promiseFromRequest(request5);
+ cursor = request5.result;
+ if (!cursor) {
+ throw new Error();
+ }
+ t.is(cursor.value.author, "Barney");
+ cursor.continue();
+
+ await promiseFromRequest(request5);
+ cursor = request5.result;
+ if (!cursor) {
+ throw new Error();
+ }
+ t.is(cursor.value.author, "Fred");
+ cursor.continue();
+
+ await promiseFromRequest(request5);
+ cursor = request5.result;
+ if (!cursor) {
+ throw new Error();
+ }
+ t.is(cursor.value.author, "Fred");
+ cursor.continue();
+
+ await promiseFromRequest(request5);
+ cursor = request5.result;
+ t.is(cursor, null);
+
+ const request6 = index5.openCursor(null, "nextunique");
+
+ await promiseFromRequest(request6);
+ cursor = request6.result;
+ if (!cursor) {
+ throw new Error();
+ }
+ t.is(cursor.value.author, "Barney");
+ cursor.continue();
+
+ await promiseFromRequest(request6);
+ cursor = request6.result;
+ if (!cursor) {
+ throw new Error();
+ }
+ t.is(cursor.value.author, "Fred");
+ t.is(cursor.value.isbn, 123456);
+ cursor.continue();
+
+ await promiseFromRequest(request6);
+ cursor = request6.result;
+ t.is(cursor, null);
+
+ console.log("---------------------------");
+
+ const request7 = index5.openCursor(null, "prevunique");
+ await promiseFromRequest(request7);
+ cursor = request7.result;
+ if (!cursor) {
+ throw new Error();
+ }
+ t.is(cursor.value.author, "Fred");
+ t.is(cursor.value.isbn, 123456);
+ cursor.continue();
+
+ await promiseFromRequest(request7);
+ cursor = request7.result;
+ if (!cursor) {
+ throw new Error();
+ }
+ t.is(cursor.value.author, "Barney");
+ cursor.continue();
+
+ await promiseFromRequest(request7);
+ cursor = request7.result;
+ t.is(cursor, null);
+
+ db.close();
+
+ t.pass();
+});
+
+test("simple deletion", async (t) => {
+ const idb = useTestIndexedDb();
+ const dbname = "library-" + new Date().getTime() + Math.random();
+ const request = idb.open(dbname);
+ request.onupgradeneeded = () => {
+ const db = request.result;
+ const store = db.createObjectStore("books", { keyPath: "isbn" });
+ const titleIndex = store.createIndex("by_title", "title", { unique: true });
+ const authorIndex = store.createIndex("by_author", "author");
+ };
+
+ const db: BridgeIDBDatabase = await promiseFromRequest(request);
+ const tx = db.transaction("books", "readwrite");
+ tx.oncomplete = () => {
+ console.log("oncomplete called");
+ };
+
+ const store = tx.objectStore("books");
+
+ store.put({ title: "Quarry Memories", author: "Fred", isbn: 123456 });
+ store.put({ title: "Water Buffaloes", author: "Fred", isbn: 234567 });
+ store.put({ title: "Bedrock Nights", author: "Barney", isbn: 345678 });
+
+ await promiseFromTransaction(tx);
+
+ const tx2 = db.transaction("books", "readwrite");
+
+ const store2 = tx2.objectStore("books");
+
+ const req1 = store2.get(234567);
+ await promiseFromRequest(req1);
+ t.is(req1.readyState, "done");
+ t.is(req1.result.author, "Fred");
+
+ store2.delete(123456);
+
+ const req2 = store2.get(123456);
+ await promiseFromRequest(req2);
+ t.is(req2.readyState, "done");
+ t.is(req2.result, undefined);
+
+ const req3 = store2.get(234567);
+ await promiseFromRequest(req3);
+ t.is(req3.readyState, "done");
+ t.is(req3.result.author, "Fred");
+
+ await promiseFromTransaction(tx2);
+
+ t.pass();
+});
+
+test("export", async (t) => {
+ const backend = new MemoryBackend();
+ const idb = new BridgeIDBFactory(backend);
+ const dbname = "library-" + new Date().getTime() + Math.random();
+ const request = idb.open(dbname, 42);
+ request.onupgradeneeded = () => {
+ const db = request.result;
+ const store = db.createObjectStore("books", { keyPath: "isbn" });
+ const titleIndex = store.createIndex("by_title", "title", { unique: true });
+ const authorIndex = store.createIndex("by_author", "author");
+ };
+
+ const db: BridgeIDBDatabase = await promiseFromRequest(request);
+
+ const tx = db.transaction("books", "readwrite");
+ tx.oncomplete = () => {
+ console.log("oncomplete called");
+ };
+
+ const store = tx.objectStore("books");
+
+ store.put({ title: "Quarry Memories", author: "Fred", isbn: 123456 });
+ store.put({ title: "Water Buffaloes", author: "Fred", isbn: 234567 });
+ store.put({ title: "Bedrock Nights", author: "Barney", isbn: 345678 });
+
+ await promiseFromTransaction(tx);
+
+ const exportedData = backend.exportDump();
+ const backend2 = new MemoryBackend();
+ backend2.importDump(exportedData);
+ const exportedData2 = backend2.exportDump();
+
+ t.assert(
+ exportedData.databases[dbname].objectStores["books"].records.length ===
+ 3,
+ );
+ t.deepEqual(exportedData, exportedData2);
+
+ t.is(exportedData.databases[dbname].schema.databaseVersion, 42);
+ t.is(exportedData2.databases[dbname].schema.databaseVersion, 42);
+ t.pass();
+});
+
+test("update with non-existent index values", async (t) => {
+ const idb = useTestIndexedDb();
+ const dbname = "mydb-" + new Date().getTime() + Math.random();
+ const request = idb.open(dbname);
+ request.onupgradeneeded = () => {
+ const db = request.result;
+ const store = db.createObjectStore("bla", { keyPath: "x" });
+ store.createIndex("by_y", "y");
+ store.createIndex("by_z", "z");
+ };
+
+ const db: BridgeIDBDatabase = await promiseFromRequest(request);
+
+ t.is(db.name, dbname);
+
+ {
+ const tx = db.transaction("bla", "readwrite");
+ const store = tx.objectStore("bla");
+ store.put({ x: 0, y: "a", z: 42 });
+ const index = store.index("by_z");
+ const indRes = await promiseFromRequest(index.get(42));
+ t.is(indRes.x, 0);
+ const res = await promiseFromRequest(store.get(0));
+ t.is(res.z, 42);
+ await promiseFromTransaction(tx);
+ }
+
+ {
+ const tx = db.transaction("bla", "readwrite");
+ const store = tx.objectStore("bla");
+ store.put({ x: 0, y: "a" });
+ const res = await promiseFromRequest(store.get(0));
+ t.is(res.z, undefined);
+ await promiseFromTransaction(tx);
+ }
+
+ {
+ const tx = db.transaction("bla", "readwrite");
+ const store = tx.objectStore("bla");
+ const index = store.index("by_z");
+ {
+ const indRes = await promiseFromRequest(index.get(42));
+ t.is(indRes, undefined);
+ }
+ const res = await promiseFromRequest(store.get(0));
+ t.is(res.z, undefined);
+ await promiseFromTransaction(tx);
+ }
+
+ t.pass();
+});
+
+test("delete from unique index", async (t) => {
+ const idb = useTestIndexedDb();
+ const dbname = "mydb-" + new Date().getTime() + Math.random();
+ const request = idb.open(dbname);
+ request.onupgradeneeded = () => {
+ const db = request.result as IDBDatabase;
+ const store = db.createObjectStore("bla", { keyPath: "x" });
+ store.createIndex("by_yz", ["y", "z"], {
+ unique: true,
+ });
+ };
+
+ const db: BridgeIDBDatabase = await promiseFromRequest(request);
+
+ t.is(db.name, dbname);
+
+ {
+ const tx = db.transaction("bla", "readwrite");
+ const store = tx.objectStore("bla");
+ store.put({ x: 0, y: "a", z: 42 });
+ const index = store.index("by_yz");
+ const indRes = await promiseFromRequest(index.get(["a", 42]));
+ t.is(indRes.x, 0);
+ const res = await promiseFromRequest(store.get(0));
+ t.is(res.z, 42);
+ await promiseFromTransaction(tx);
+ }
+
+ {
+ const tx = db.transaction("bla", "readwrite");
+ const store = tx.objectStore("bla");
+ store.put({ x: 0, y: "a", z: 42, extra: 123 });
+ await promiseFromTransaction(tx);
+ }
+
+ t.pass();
+});
+
+test("range queries", async (t) => {
+ const idb = useTestIndexedDb();
+ const dbname = "mydb-" + new Date().getTime() + Math.random();
+ const request = idb.open(dbname);
+ request.onupgradeneeded = () => {
+ const db = request.result;
+ const store = db.createObjectStore("bla", { keyPath: "x" });
+ store.createIndex("by_y", "y");
+ store.createIndex("by_z", "z");
+ };
+
+ const db: BridgeIDBDatabase = await promiseFromRequest(request);
+ const tx = db.transaction("bla", "readwrite");
+ const store = tx.objectStore("bla");
+
+ store.put({ x: 0, y: "a" });
+ store.put({ x: 2, y: "a" });
+ store.put({ x: 4, y: "b" });
+ store.put({ x: 8, y: "b" });
+ store.put({ x: 10, y: "c" });
+ store.put({ x: 12, y: "c" });
+
+ await promiseFromTransaction(tx);
+
+ async function doCursorStoreQuery(
+ range: IDBKeyRange | IDBValidKey | undefined,
+ direction: IDBCursorDirection | undefined,
+ expected: any[],
+ ): Promise<void> {
+ const tx = db.transaction("bla", "readwrite");
+ const store = tx.objectStore("bla");
+ const vals: any[] = [];
+
+ const req = store.openCursor(range, direction);
+ while (1) {
+ await promiseFromRequest(req);
+ const cursor: IDBCursorWithValue = req.result;
+ if (!cursor) {
+ break;
+ }
+ cursor.continue();
+ vals.push(cursor.value);
+ }
+
+ await promiseFromTransaction(tx);
+
+ t.deepEqual(vals, expected);
+ }
+
+ async function doCursorIndexQuery(
+ range: IDBKeyRange | IDBValidKey | undefined,
+ direction: IDBCursorDirection | undefined,
+ expected: any[],
+ ): Promise<void> {
+ const tx = db.transaction("bla", "readwrite");
+ const store = tx.objectStore("bla");
+ const index = store.index("by_y");
+ const vals: any[] = [];
+
+ const req = index.openCursor(range, direction);
+ while (1) {
+ await promiseFromRequest(req);
+ const cursor: IDBCursorWithValue = req.result;
+ if (!cursor) {
+ break;
+ }
+ cursor.continue();
+ vals.push(cursor.value);
+ }
+
+ await promiseFromTransaction(tx);
+
+ t.deepEqual(vals, expected);
+ }
+
+ await doCursorStoreQuery(undefined, undefined, [
+ {
+ x: 0,
+ y: "a",
+ },
+ {
+ x: 2,
+ y: "a",
+ },
+ {
+ x: 4,
+ y: "b",
+ },
+ {
+ x: 8,
+ y: "b",
+ },
+ {
+ x: 10,
+ y: "c",
+ },
+ {
+ x: 12,
+ y: "c",
+ },
+ ]);
+
+ await doCursorStoreQuery(
+ BridgeIDBKeyRange.bound(0, 12, true, true),
+ undefined,
+ [
+ {
+ x: 2,
+ y: "a",
+ },
+ {
+ x: 4,
+ y: "b",
+ },
+ {
+ x: 8,
+ y: "b",
+ },
+ {
+ x: 10,
+ y: "c",
+ },
+ ],
+ );
+
+ await doCursorIndexQuery(
+ BridgeIDBKeyRange.bound("a", "c", true, true),
+ undefined,
+ [
+ {
+ x: 4,
+ y: "b",
+ },
+ {
+ x: 8,
+ y: "b",
+ },
+ ],
+ );
+
+ await doCursorIndexQuery(undefined, "nextunique", [
+ {
+ x: 0,
+ y: "a",
+ },
+ {
+ x: 4,
+ y: "b",
+ },
+ {
+ x: 10,
+ y: "c",
+ },
+ ]);
+
+ await doCursorIndexQuery(undefined, "prevunique", [
+ {
+ x: 10,
+ y: "c",
+ },
+ {
+ x: 4,
+ y: "b",
+ },
+ {
+ x: 0,
+ y: "a",
+ },
+ ]);
+
+ db.close();
+
+ t.pass();
+});
diff --git a/packages/idb-bridge/src/bench.ts b/packages/idb-bridge/src/bench.ts
new file mode 100644
index 000000000..d196bacb1
--- /dev/null
+++ b/packages/idb-bridge/src/bench.ts
@@ -0,0 +1,110 @@
+/*
+ Copyright 2024 Taler Systems S.A.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ or implied. See the License for the specific language governing
+ permissions and limitations under the License.
+ */
+
+import * as fs from "node:fs";
+import {
+ BridgeIDBDatabase,
+ BridgeIDBFactory,
+ BridgeIDBRequest,
+ BridgeIDBTransaction,
+ createSqliteBackend,
+} from "./index.js";
+import { createNodeSqlite3Impl } from "./node-sqlite3-impl.js";
+
+function openDb(idbFactory: BridgeIDBFactory): Promise<BridgeIDBDatabase> {
+ return new Promise((resolve, reject) => {
+ const openReq = idbFactory.open("mydb", 1);
+ openReq.addEventListener("success", () => {
+ const database = openReq.result;
+ resolve(database);
+ });
+ openReq.addEventListener("upgradeneeded", (event: any) => {
+ const database: BridgeIDBDatabase = event.target.result;
+ const transaction: BridgeIDBTransaction = event.target.transaction;
+ database.createObjectStore("books", {
+ keyPath: "isbn",
+ });
+ });
+ });
+}
+
+function requestToPromise(req: BridgeIDBRequest): Promise<any> {
+ //const stack = Error("Failed request was started here.");
+ return new Promise((resolve, reject) => {
+ req.onsuccess = () => {
+ resolve(req.result);
+ };
+ req.onerror = () => {
+ console.error("error in DB request", req.error);
+ reject(req.error);
+ //console.error("Request failed:", stack);
+ };
+ });
+}
+
+function transactionToPromise(tx: BridgeIDBTransaction): Promise<void> {
+ //const stack = Error("Failed request was started here.");
+ return new Promise((resolve, reject) => {
+ tx.addEventListener("complete", () => {
+ resolve();
+ });
+ tx.onerror = () => {
+ console.error("error in DB txn", tx.error);
+ reject(tx.error);
+ //console.error("Request failed:", stack);
+ };
+ });
+}
+
+const nTx = Number(process.argv[2]);
+const nInsert = Number(process.argv[3]);
+
+async function main() {
+ const filename = "mytestdb.sqlite3";
+ try {
+ fs.unlinkSync(filename);
+ } catch (e) {
+ // Do nothing.
+ }
+
+ console.log(`doing ${nTx} iterations of ${nInsert} items`);
+
+ const sqlite3Impl = await createNodeSqlite3Impl();
+ const backend = await createSqliteBackend(sqlite3Impl, {
+ filename,
+ });
+ backend.enableTracing = false;
+ const idbFactory = new BridgeIDBFactory(backend);
+ const db = await openDb(idbFactory);
+
+ for (let i = 0; i < nTx; i++) {
+ const tx = db.transaction(["books"], "readwrite");
+ const txProm = transactionToPromise(tx);
+ const books = tx.objectStore("books");
+ for (let j = 0; j < nInsert; j++) {
+ const addReq = books.add({
+ isbn: `${i}-${j}`,
+ name: `book-${i}-${j}`,
+ });
+ await requestToPromise(addReq);
+ }
+ await txProm;
+ }
+
+ console.log("done");
+}
+
+main();
diff --git a/packages/idb-bridge/src/bridge-idb.ts b/packages/idb-bridge/src/bridge-idb.ts
index 128a6900d..afb3f4224 100644
--- a/packages/idb-bridge/src/bridge-idb.ts
+++ b/packages/idb-bridge/src/bridge-idb.ts
@@ -17,12 +17,16 @@
import {
Backend,
+ ConnectResult,
DatabaseConnection,
DatabaseTransaction,
- RecordGetRequest,
+ IndexGetQuery,
+ IndexMeta,
+ ObjectStoreGetQuery,
+ ObjectStoreMeta,
+ RecordGetResponse,
RecordStoreRequest,
ResultLevel,
- Schema,
StoreLevel,
} from "./backend-interface.js";
import {
@@ -57,10 +61,7 @@ import {
TransactionInactiveError,
VersionError,
} from "./util/errors.js";
-import {
- FakeDOMStringList,
- fakeDOMStringList,
-} from "./util/fakeDOMStringList.js";
+import { fakeDOMStringList } from "./util/fakeDOMStringList.js";
import FakeEvent from "./util/FakeEvent.js";
import FakeEventTarget from "./util/FakeEventTarget.js";
import { makeStoreKeyValue } from "./util/makeStoreKeyValue.js";
@@ -71,17 +72,14 @@ import { checkStructuredCloneOrThrow } from "./util/structuredClone.js";
import { validateKeyPath } from "./util/validateKeyPath.js";
import { valueToKey } from "./util/valueToKey.js";
-/** @public */
export type CursorSource = BridgeIDBIndex | BridgeIDBObjectStore;
-/** @public */
export interface RequestObj {
operation: () => Promise<any>;
request?: BridgeIDBRequest | undefined;
source?: any;
}
-/** @public */
export interface BridgeIDBDatabaseInfo {
name: string;
version: number;
@@ -101,8 +99,6 @@ function simplifyRange(
/**
* http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#cursor
- *
- * @public
*/
export class BridgeIDBCursor implements IDBCursor {
_request: BridgeIDBRequest | undefined;
@@ -207,29 +203,56 @@ export class BridgeIDBCursor implements IDBCursor {
);
BridgeIDBFactory.enableTracing &&
console.log("cursor type ", this.toString());
- const isIndex = this._indexName !== undefined;
- const recordGetRequest: RecordGetRequest = {
- direction: this.direction,
- indexName: this._indexName,
- lastIndexPosition: this._indexPosition,
- lastObjectStorePosition: this._objectStorePosition,
- limit: 1,
- range: simplifyRange(this._range),
- objectStoreName: this._objectStoreName,
- advanceIndexKey: isIndex ? key : undefined,
- advancePrimaryKey: isIndex ? primaryKey : key,
- resultLevel: this._keyOnly ? ResultLevel.OnlyKeys : ResultLevel.Full,
- };
+ const indexName = this._indexName;
const { btx } = this.source._confirmStartedBackendTransaction();
- let response = await this._backend.getRecords(btx, recordGetRequest);
+ let response: RecordGetResponse;
+
+ if (indexName != null) {
+ const indexRecordGetRequest: IndexGetQuery = {
+ direction: this.direction,
+ indexName: indexName,
+ lastIndexPosition: this._indexPosition,
+ lastObjectStorePosition: this._objectStorePosition,
+ limit: 1,
+ range: simplifyRange(this._range),
+ objectStoreName: this._objectStoreName,
+ advanceIndexKey: key,
+ advancePrimaryKey: primaryKey,
+ resultLevel: this._keyOnly ? ResultLevel.OnlyKeys : ResultLevel.Full,
+ };
+ response = await this._backend.getIndexRecords(
+ btx,
+ indexRecordGetRequest,
+ );
+ } else {
+ if (primaryKey != null) {
+ // Only allowed for index cursors
+ throw new InvalidAccessError();
+ }
+ const objStoreGetRequest: ObjectStoreGetQuery = {
+ direction: this.direction,
+ lastObjectStorePosition: this._objectStorePosition,
+ limit: 1,
+ range: simplifyRange(this._range),
+ objectStoreName: this._objectStoreName,
+ advancePrimaryKey: key,
+ resultLevel: this._keyOnly ? ResultLevel.OnlyKeys : ResultLevel.Full,
+ };
+ response = await this._backend.getObjectStoreRecords(
+ btx,
+ objStoreGetRequest,
+ );
+ }
if (response.count === 0) {
if (BridgeIDBFactory.enableTracing) {
console.log("cursor is returning empty result");
}
this._gotValue = false;
+ this._key = undefined;
+ this._value = undefined;
return null;
}
@@ -237,11 +260,6 @@ export class BridgeIDBCursor implements IDBCursor {
throw Error("invariant failed");
}
- if (BridgeIDBFactory.enableTracing) {
- console.log("request is:", JSON.stringify(recordGetRequest));
- console.log("get response is:", JSON.stringify(response));
- }
-
if (this._indexName !== undefined) {
this._key = response.indexKeys![0];
} else {
@@ -550,7 +568,6 @@ const confirmActiveVersionchangeTransaction = (database: BridgeIDBDatabase) => {
};
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#database-interface
-/** @public */
export class BridgeIDBDatabase extends FakeEventTarget implements IDBDatabase {
_closePending = false;
_closed = false;
@@ -561,7 +578,16 @@ export class BridgeIDBDatabase extends FakeEventTarget implements IDBDatabase {
_backendConnection: DatabaseConnection;
_backend: Backend;
- _schema: Schema;
+ _name: string;
+
+ _initialVersion: number;
+
+ _version: number;
+
+ // "object store set" from the spec
+ _objectStoreSet: string[];
+
+ // _schema: Schema;
/**
* Name that can be set to identify the object store in logs.
@@ -569,17 +595,15 @@ export class BridgeIDBDatabase extends FakeEventTarget implements IDBDatabase {
_debugName: string | undefined = undefined;
get name(): string {
- return this._schema.databaseName;
+ return this._name;
}
get version(): number {
- return this._schema.databaseVersion;
+ return this._version;
}
get objectStoreNames(): DOMStringList {
- return fakeDOMStringList(
- Object.keys(this._schema.objectStores),
- ).sort() as DOMStringList;
+ return fakeDOMStringList([...this._objectStoreSet]).sort() as DOMStringList;
}
/**
@@ -606,13 +630,13 @@ export class BridgeIDBDatabase extends FakeEventTarget implements IDBDatabase {
}
}
- constructor(backend: Backend, backendConnection: DatabaseConnection) {
+ constructor(name: string, backend: Backend, connResult: ConnectResult) {
super();
-
- this._schema = backend.getSchema(backendConnection);
-
+ this._name = name;
+ this._version = this._initialVersion = connResult.version;
this._backend = backend;
- this._backendConnection = backendConnection;
+ this._backendConnection = connResult.conn;
+ this._objectStoreSet = connResult.objectStores;
}
// http://w3c.github.io/IndexedDB/#dom-idbdatabase-createobjectstore
@@ -645,7 +669,8 @@ export class BridgeIDBDatabase extends FakeEventTarget implements IDBDatabase {
validateKeyPath(keyPath);
}
- if (Object.keys(this._schema.objectStores).includes(name)) {
+ if (this._objectStoreSet.includes(name)) {
+ // Already exists
throw new ConstraintError();
}
@@ -660,7 +685,9 @@ export class BridgeIDBDatabase extends FakeEventTarget implements IDBDatabase {
autoIncrement,
);
- this._schema = this._backend.getCurrentTransactionSchema(backendTx);
+ transaction._scope.add(name);
+ this._objectStoreSet.push(name);
+ this._objectStoreSet.sort();
const newObjectStore = transaction.objectStore(name);
newObjectStore._justCreated = true;
@@ -682,6 +709,10 @@ export class BridgeIDBDatabase extends FakeEventTarget implements IDBDatabase {
os._deleted = true;
transaction._objectStoresCache.delete(name);
}
+ transaction._cachedObjectStoreNames = undefined;
+ transaction._scope.delete(name);
+ const nameIdx = this._objectStoreSet.indexOf(name);
+ this._objectStoreSet.splice(nameIdx, 1);
}
public _internalTransaction(
@@ -704,7 +735,9 @@ export class BridgeIDBDatabase extends FakeEventTarget implements IDBDatabase {
}
if (this._closePending) {
- throw new InvalidStateError();
+ throw new InvalidStateError(
+ `tried to start transaction on ${this._name}, but a close is pending`,
+ );
}
if (!Array.isArray(storeNames)) {
@@ -766,10 +799,8 @@ export class BridgeIDBDatabase extends FakeEventTarget implements IDBDatabase {
}
}
-/** @public */
export type DatabaseList = Array<{ name: string; version: number }>;
-/** @public */
export class BridgeIDBFactory {
public cmp = compareKeys;
private backend: Backend;
@@ -810,8 +841,10 @@ export class BridgeIDBFactory {
});
request.dispatchEvent(event2);
} catch (err: any) {
- request.error = new Error();
- request.error.name = err.name;
+ const myErr = new Error();
+ myErr.name = err.name;
+ myErr.message = err.message;
+ request.error = myErr;
request.readyState = "done";
const event = new FakeEvent("error", {
@@ -841,27 +874,26 @@ export class BridgeIDBFactory {
const request = new BridgeIDBOpenDBRequest();
queueTask(async () => {
- let dbconn: DatabaseConnection;
+ let dbConnRes: ConnectResult;
try {
if (BridgeIDBFactory.enableTracing) {
console.log("TRACE: connecting to database");
}
- dbconn = await this.backend.connectDatabase(name);
+ dbConnRes = await this.backend.connectDatabase(name);
if (BridgeIDBFactory.enableTracing) {
console.log("TRACE: connected!");
}
} catch (err: any) {
if (BridgeIDBFactory.enableTracing) {
console.log(
- "TRACE: caught exception while trying to connect with backend",
+ "TRACE: caught exception while trying to connect with backend:",
+ err,
);
}
request._finishWithError(err);
return;
}
-
- const schema = this.backend.getSchema(dbconn);
- const existingVersion = schema.databaseVersion;
+ const existingVersion = dbConnRes.version;
if (version === undefined) {
version = existingVersion !== 0 ? existingVersion : 1;
@@ -879,7 +911,7 @@ export class BridgeIDBFactory {
return;
}
- const db = new BridgeIDBDatabase(this.backend, dbconn);
+ const db = new BridgeIDBDatabase(name, this.backend, dbConnRes);
if (existingVersion == requestedVersion) {
request.result = db;
@@ -900,6 +932,9 @@ export class BridgeIDBFactory {
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-steps-for-running-a-versionchange-transaction
for (const otherConn of this.connections) {
+ if (otherConn._name != db._name) {
+ continue;
+ }
if (otherConn._closePending) {
continue;
}
@@ -929,16 +964,14 @@ export class BridgeIDBFactory {
}
const backendTransaction = await this.backend.enterVersionChange(
- dbconn,
+ dbConnRes.conn,
requestedVersion,
);
// We need to expose the new version number to the upgrade transaction.
- db._schema =
- this.backend.getCurrentTransactionSchema(backendTransaction);
-
+ db._version = version;
const transaction = db._internalTransaction(
- [],
+ dbConnRes.objectStores,
"versionchange",
backendTransaction,
request,
@@ -964,7 +997,7 @@ export class BridgeIDBFactory {
await transaction._waitDone();
- // We re-use the same transaction (as per spec) here.
+ // We reuse the same transaction (as per spec) here.
transaction._active = true;
if (db._closed || db._closePending) {
@@ -1030,37 +1063,48 @@ export class BridgeIDBFactory {
}
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#idl-def-IDBIndex
-/** @public */
export class BridgeIDBIndex implements IDBIndex {
_objectStore: BridgeIDBObjectStore;
+ _indexMeta: IndexMeta;
+ _originalName: string | undefined = undefined;
+ _deleted: boolean = false;
+ _name: string;
+
+ /**
+ * Was this index newly created in the current transaction?
+ */
+ _justCreated: boolean = false;
get objectStore(): IDBObjectStore {
return this._objectStore;
}
- get _schema(): Schema {
- return this._objectStore._transaction._db._schema;
- }
-
get keyPath(): IDBKeyPath | IDBKeyPath[] {
- return this._schema.objectStores[this._objectStore.name].indexes[this._name]
- .keyPath;
+ return this._indexMeta.keyPath;
}
get multiEntry(): boolean {
- return this._schema.objectStores[this._objectStore.name].indexes[this._name]
- .multiEntry;
+ return this._indexMeta.multiEntry;
}
get unique(): boolean {
- return this._schema.objectStores[this._objectStore.name].indexes[this._name]
- .unique;
+ return this._indexMeta.multiEntry;
}
get _backend(): Backend {
return this._objectStore._backend;
}
+ constructor(
+ objectStore: BridgeIDBObjectStore,
+ name: string,
+ indexMeta: IndexMeta,
+ ) {
+ this._name = name;
+ this._objectStore = objectStore;
+ this._indexMeta = indexMeta;
+ }
+
_confirmStartedBackendTransaction(): { btx: DatabaseTransaction } {
return this._objectStore._confirmStartedBackendTransaction();
}
@@ -1069,20 +1113,6 @@ export class BridgeIDBIndex implements IDBIndex {
this._objectStore._confirmActiveTransaction();
}
- private _name: string;
-
- public _deleted: boolean = false;
-
- /**
- * Was this index newly created in the current transaction?
- */
- _justCreated: boolean = false;
-
- constructor(objectStore: BridgeIDBObjectStore, name: string) {
- this._name = name;
- this._objectStore = objectStore;
- }
-
get name() {
return this._name;
}
@@ -1107,18 +1137,39 @@ export class BridgeIDBIndex implements IDBIndex {
if (newName === oldName) {
return;
}
-
+ if (this._originalName != null) {
+ this._originalName = oldName;
+ }
this._backend.renameIndex(btx, this._objectStore.name, oldName, newName);
+ this._applyNameChange(oldName, newName);
+ if (this._objectStore._objectStoreMeta.indexSet.indexOf(name) >= 0) {
+ throw new Error("internal invariant violated");
+ }
+ }
- this._objectStore._transaction._db._schema =
- this._backend.getCurrentTransactionSchema(btx);
-
- this._objectStore._indexesCache.delete(oldName);
- this._objectStore._indexesCache.set(newName, this);
+ _applyNameChange(oldName: string, newName: string) {
+ this._objectStore._indexHandlesCache.delete(oldName);
+ this._objectStore._indexHandlesCache.set(newName, this);
+ const indexSet = this._objectStore._objectStoreMeta.indexSet;
+ const indexIdx = indexSet.indexOf(oldName);
+ indexSet[indexIdx] = newName;
+ indexSet.sort();
this._name = newName;
+ }
- if (this._objectStore._indexNames.indexOf(name) >= 0) {
- throw new Error("internal invariant violated");
+ _applyDelete() {
+ this._objectStore._indexHandlesCache.delete(this._name);
+ const indexSet = this._objectStore._objectStoreMeta.indexSet;
+ const indexIdx = indexSet.indexOf(this._name);
+ indexSet.splice(indexIdx, 1);
+ }
+
+ _abort() {
+ if (this._originalName != null) {
+ this._applyNameChange(this._name, this._originalName);
+ }
+ if (this._justCreated) {
+ this._deleted = true;
}
}
@@ -1199,34 +1250,23 @@ export class BridgeIDBIndex implements IDBIndex {
}
private _confirmIndexExists() {
- const storeSchema = this._schema.objectStores[this._objectStore._name];
- if (!storeSchema) {
- throw new InvalidStateError(
- `no schema for object store '${this._objectStore._name}'`,
- );
- }
- if (!storeSchema.indexes[this._name]) {
- throw new InvalidStateError(
- `no schema for index '${this._name}' of object store '${this._objectStore._name}'`,
- );
- }
- }
-
- get(key: BridgeIDBKeyRange | IDBValidKey) {
if (this._deleted) {
throw new InvalidStateError();
}
if (this._objectStore._deleted) {
throw new InvalidStateError();
}
- this._confirmActiveTransaction();
+ }
+
+ get(key: BridgeIDBKeyRange | IDBValidKey) {
this._confirmIndexExists();
+ this._confirmActiveTransaction();
if (!(key instanceof BridgeIDBKeyRange)) {
key = BridgeIDBKeyRange._valueToKeyRange(key);
}
- const getReq: RecordGetRequest = {
+ const getReq: IndexGetQuery = {
direction: "next",
indexName: this._name,
limit: 1,
@@ -1237,7 +1277,7 @@ export class BridgeIDBIndex implements IDBIndex {
const operation = async () => {
const { btx } = this._confirmStartedBackendTransaction();
- const result = await this._backend.getRecords(btx, getReq);
+ const result = await this._backend.getIndexRecords(btx, getReq);
if (result.count == 0) {
return undefined;
}
@@ -1273,7 +1313,7 @@ export class BridgeIDBIndex implements IDBIndex {
count = -1;
}
- const getReq: RecordGetRequest = {
+ const getReq: IndexGetQuery = {
direction: "next",
indexName: this._name,
limit: count,
@@ -1284,7 +1324,7 @@ export class BridgeIDBIndex implements IDBIndex {
const operation = async () => {
const { btx } = this._confirmStartedBackendTransaction();
- const result = await this._backend.getRecords(btx, getReq);
+ const result = await this._backend.getIndexRecords(btx, getReq);
const values = result.values;
if (!values) {
throw Error("invariant violated");
@@ -1307,7 +1347,7 @@ export class BridgeIDBIndex implements IDBIndex {
key = BridgeIDBKeyRange._valueToKeyRange(key);
}
- const getReq: RecordGetRequest = {
+ const getReq: IndexGetQuery = {
direction: "next",
indexName: this._name,
limit: 1,
@@ -1318,7 +1358,7 @@ export class BridgeIDBIndex implements IDBIndex {
const operation = async () => {
const { btx } = this._confirmStartedBackendTransaction();
- const result = await this._backend.getRecords(btx, getReq);
+ const result = await this._backend.getIndexRecords(btx, getReq);
if (result.count == 0) {
return undefined;
}
@@ -1351,7 +1391,7 @@ export class BridgeIDBIndex implements IDBIndex {
count = -1;
}
- const getReq: RecordGetRequest = {
+ const getReq: IndexGetQuery = {
direction: "next",
indexName: this._name,
limit: count,
@@ -1362,7 +1402,7 @@ export class BridgeIDBIndex implements IDBIndex {
const operation = async () => {
const { btx } = this._confirmStartedBackendTransaction();
- const result = await this._backend.getRecords(btx, getReq);
+ const result = await this._backend.getIndexRecords(btx, getReq);
const primaryKeys = result.primaryKeys;
if (!primaryKeys) {
throw Error("invariant violated");
@@ -1388,7 +1428,7 @@ export class BridgeIDBIndex implements IDBIndex {
key = BridgeIDBKeyRange.only(valueToKey(key));
}
- const getReq: RecordGetRequest = {
+ const getReq: IndexGetQuery = {
direction: "next",
indexName: this._name,
limit: 1,
@@ -1399,7 +1439,7 @@ export class BridgeIDBIndex implements IDBIndex {
const operation = async () => {
const { btx } = this._confirmStartedBackendTransaction();
- const result = await this._backend.getRecords(btx, getReq);
+ const result = await this._backend.getIndexRecords(btx, getReq);
return result.count;
};
@@ -1415,7 +1455,6 @@ export class BridgeIDBIndex implements IDBIndex {
}
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#range-concept
-/** @public */
export class BridgeIDBKeyRange {
public static only(value: IDBValidKey) {
if (arguments.length === 0) {
@@ -1525,10 +1564,8 @@ export class BridgeIDBKeyRange {
}
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#object-store
-/** @public */
export class BridgeIDBObjectStore implements IDBObjectStore {
- _indexesCache: Map<string, BridgeIDBIndex> = new Map();
-
+ _indexHandlesCache: Map<string, BridgeIDBIndex> = new Map();
_transaction: BridgeIDBTransaction;
/**
@@ -1536,41 +1573,43 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
*/
_debugName: string | undefined = undefined;
+ // Was the object store (not the handle, but the underlying store)
+ // created in this upgrade transaction?
_justCreated: boolean = false;
+ _originalName: string | undefined = undefined;
+ _objectStoreMeta: ObjectStoreMeta;
+
get transaction(): IDBTransaction {
return this._transaction;
}
get autoIncrement(): boolean {
- return this._schema.objectStores[this._name].autoIncrement;
- }
-
- get _indexNames(): FakeDOMStringList {
- return fakeDOMStringList(
- Object.keys(this._schema.objectStores[this._name].indexes),
- ).sort();
+ return this._objectStoreMeta.autoIncrement;
}
get indexNames(): DOMStringList {
- return this._indexNames as DOMStringList;
+ return fakeDOMStringList([...this._objectStoreMeta.indexSet]);
}
get keyPath(): IDBKeyPath | IDBKeyPath[] {
- return this._schema.objectStores[this._name].keyPath!;
+ // Bug in th official type declarations. The spec
+ // allows returning null here.
+ return this._objectStoreMeta.keyPath!;
}
_name: string;
- get _schema(): Schema {
- return this._transaction._db._schema;
- }
-
_deleted: boolean = false;
- constructor(transaction: BridgeIDBTransaction, name: string) {
+ constructor(
+ transaction: BridgeIDBTransaction,
+ name: string,
+ objectStoreMeta: ObjectStoreMeta,
+ ) {
this._name = name;
this._transaction = transaction;
+ this._objectStoreMeta = objectStoreMeta;
}
get name() {
@@ -1620,26 +1659,56 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
let { btx } = this._confirmStartedBackendTransaction();
newName = String(newName);
-
const oldName = this._name;
-
if (newName === oldName) {
return;
}
-
+ if (this._originalName == null) {
+ this._originalName = this._name;
+ }
this._backend.renameObjectStore(btx, oldName, newName);
- this._transaction._db._schema =
- this._backend.getCurrentTransactionSchema(btx);
+ this._applyNameChange(oldName, newName);
+ }
+ _applyNameChange(oldName: string, newName: string) {
+ this._transaction._scope.delete(oldName);
+ this._transaction._scope.add(newName);
// We don't modify scope, as the scope of the transaction
// doesn't matter if we're in an upgrade transaction.
this._transaction._objectStoresCache.delete(oldName);
this._transaction._objectStoresCache.set(newName, this);
this._transaction._cachedObjectStoreNames = undefined;
-
+ const objectStoreSet = this._transaction._db._objectStoreSet;
+ const oldIdx = objectStoreSet.indexOf(oldName);
+ objectStoreSet[oldIdx] = newName;
+ objectStoreSet.sort();
this._name = newName;
}
+ _applyDelete() {
+ this._deleted = true;
+ this._transaction._objectStoresCache.delete(this._name);
+ this._transaction._cachedObjectStoreNames = undefined;
+ const objectStoreSet = this._transaction._db._objectStoreSet;
+ const oldIdx = objectStoreSet.indexOf(this._name);
+ objectStoreSet.splice(oldIdx, 1);
+ }
+
+ /**
+ * Roll back changes to the handle after an abort.
+ */
+ _abort() {
+ if (this._originalName != null) {
+ this._applyNameChange(this._name, this._originalName);
+ }
+ if (this._justCreated) {
+ this._applyDelete();
+ }
+ }
+
+ /**
+ * "To add or put with handle, value, key, and no-overwrite flag, run these steps:"
+ */
public _store(value: any, key: IDBValidKey | undefined, overwrite: boolean) {
if (BridgeIDBFactory.enableTracing) {
console.log(
@@ -1647,6 +1716,12 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
);
}
+ if (this._deleted) {
+ throw new InvalidStateError(
+ "tried to call 'put' on a deleted object store",
+ );
+ }
+
if (!this._transaction._active) {
throw new TransactionInactiveError();
}
@@ -1655,14 +1730,21 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
throw new ReadOnlyError();
}
- const { keyPath, autoIncrement } = this._schema.objectStores[this._name];
+ const { keyPath, autoIncrement } = this._objectStoreMeta;
if (key !== null && key !== undefined) {
valueToKey(key);
}
// We only call this to synchronously verify the request.
- makeStoreKeyValue(value, key, 1, autoIncrement, keyPath);
+ // FIXME: The backend should do that!
+ makeStoreKeyValue({
+ value: value,
+ key: key,
+ currentKeyGenerator: 1,
+ autoIncrement,
+ keyPath,
+ });
const operation = async () => {
const { btx } = this._confirmStartedBackendTransaction();
@@ -1684,11 +1766,6 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
if (arguments.length === 0) {
throw new TypeError();
}
- if (this._deleted) {
- throw new InvalidStateError(
- "tried to call 'put' on a deleted object store",
- );
- }
return this._store(value, key, true);
}
@@ -1696,9 +1773,6 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
if (arguments.length === 0) {
throw new TypeError();
}
- if (!this._schema.objectStores[this._name]) {
- throw new InvalidStateError("object store does not exist");
- }
return this._store(value, key, false);
}
@@ -1767,10 +1841,8 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
}
}
- const recordRequest: RecordGetRequest = {
+ const recordRequest: ObjectStoreGetQuery = {
objectStoreName: this._name,
- indexName: undefined,
- lastIndexPosition: undefined,
lastObjectStorePosition: undefined,
direction: "next",
limit: 1,
@@ -1783,7 +1855,10 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
console.log("running get operation:", recordRequest);
}
const { btx } = this._confirmStartedBackendTransaction();
- const result = await this._backend.getRecords(btx, recordRequest);
+ const result = await this._backend.getObjectStoreRecords(
+ btx,
+ recordRequest,
+ );
if (BridgeIDBFactory.enableTracing) {
console.log("get operation result count:", result.count);
@@ -1833,10 +1908,8 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
let keyRange: BridgeIDBKeyRange | null = simplifyRange(query);
- const recordRequest: RecordGetRequest = {
+ const recordRequest: ObjectStoreGetQuery = {
objectStoreName: this._name,
- indexName: undefined,
- lastIndexPosition: undefined,
lastObjectStorePosition: undefined,
direction: "next",
limit: count,
@@ -1849,7 +1922,10 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
console.log("running getAll operation:", recordRequest);
}
const { btx } = this._confirmStartedBackendTransaction();
- const result = await this._backend.getRecords(btx, recordRequest);
+ const result = await this._backend.getObjectStoreRecords(
+ btx,
+ recordRequest,
+ );
if (BridgeIDBFactory.enableTracing) {
console.log("get operation result count:", result.count);
@@ -1887,10 +1963,8 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
let keyRange: BridgeIDBKeyRange | null = simplifyRange(query);
- const recordRequest: RecordGetRequest = {
+ const recordRequest: ObjectStoreGetQuery = {
objectStoreName: this._name,
- indexName: undefined,
- lastIndexPosition: undefined,
lastObjectStorePosition: undefined,
direction: "next",
limit: 1,
@@ -1903,7 +1977,10 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
console.log("running getKey operation:", recordRequest);
}
const { btx } = this._confirmStartedBackendTransaction();
- const result = await this._backend.getRecords(btx, recordRequest);
+ const result = await this._backend.getObjectStoreRecords(
+ btx,
+ recordRequest,
+ );
if (BridgeIDBFactory.enableTracing) {
console.log("getKey operation result count:", result.count);
@@ -1965,10 +2042,8 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
}
}
- const recordRequest: RecordGetRequest = {
+ const recordRequest: ObjectStoreGetQuery = {
objectStoreName: this._name,
- indexName: undefined,
- lastIndexPosition: undefined,
lastObjectStorePosition: undefined,
direction: "next",
limit: count,
@@ -1978,7 +2053,10 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
const operation = async () => {
const { btx } = this._confirmStartedBackendTransaction();
- const result = await this._backend.getRecords(btx, recordRequest);
+ const result = await this._backend.getObjectStoreRecords(
+ btx,
+ recordRequest,
+ );
const primaryKeys = result.primaryKeys;
if (!primaryKeys) {
@@ -2121,7 +2199,7 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
throw new InvalidStateError();
}
- if (this._indexNames.indexOf(indexName) >= 0) {
+ if (this._objectStoreMeta.indexSet.indexOf(indexName) >= 0) {
throw new ConstraintError();
}
@@ -2140,6 +2218,9 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
unique,
);
+ this._objectStoreMeta.indexSet.push(indexName);
+ this._objectStoreMeta.indexSet.sort();
+
const idx = this.index(indexName);
idx._justCreated = true;
return idx;
@@ -2154,13 +2235,20 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
if (this._transaction._finished) {
throw new InvalidStateError();
}
-
- const index = this._indexesCache.get(name);
+ const index = this._indexHandlesCache.get(name);
if (index !== undefined) {
return index;
}
- const newIndex = new BridgeIDBIndex(this, name);
- this._indexesCache.set(name, newIndex);
+ const indexMeta = this._backend.getIndexMeta(
+ this._backendConnection,
+ this._name,
+ name,
+ );
+ if (!indexMeta) {
+ throw new NotFoundError();
+ }
+ const newIndex = new BridgeIDBIndex(this, name, indexMeta);
+ this._indexHandlesCache.set(name, newIndex);
this._transaction._usedIndexes.push(newIndex);
return newIndex;
}
@@ -2180,12 +2268,15 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
const { btx } = this._confirmStartedBackendTransaction();
- const index = this._indexesCache.get(indexName);
+ const index = this._indexHandlesCache.get(indexName);
if (index !== undefined) {
index._deleted = true;
- this._indexesCache.delete(indexName);
+ this._indexHandlesCache.delete(indexName);
}
+ const indexIdx = this._objectStoreMeta.indexSet.indexOf(indexName);
+ this._objectStoreMeta.indexSet.splice(indexIdx, 1);
+
this._backend.deleteIndex(btx, this._name, indexName);
}
@@ -2198,11 +2289,9 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
key = BridgeIDBKeyRange.only(valueToKey(key));
}
- const recordGetRequest: RecordGetRequest = {
+ const recordGetRequest: ObjectStoreGetQuery = {
direction: "next",
- indexName: undefined,
- lastIndexPosition: undefined,
- limit: -1,
+ limit: 0,
objectStoreName: this._name,
lastObjectStorePosition: undefined,
range: key,
@@ -2211,7 +2300,10 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
const operation = async () => {
const { btx } = this._confirmStartedBackendTransaction();
- const result = await this._backend.getRecords(btx, recordGetRequest);
+ const result = await this._backend.getObjectStoreRecords(
+ btx,
+ recordGetRequest,
+ );
return result.count;
};
@@ -2223,7 +2315,6 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
}
}
-/** @public */
export class BridgeIDBRequest extends FakeEventTarget implements IDBRequest {
_result: any = null;
_error: Error | null | undefined = null;
@@ -2294,7 +2385,6 @@ export class BridgeIDBRequest extends FakeEventTarget implements IDBRequest {
}
}
-/** @public */
export class BridgeIDBOpenDBRequest
extends BridgeIDBRequest
implements IDBOpenDBRequest
@@ -2343,7 +2433,6 @@ function waitMacroQueue(): Promise<void> {
}
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#transaction
-/** @public */
export class BridgeIDBTransaction
extends FakeEventTarget
implements IDBTransaction
@@ -2382,6 +2471,8 @@ export class BridgeIDBTransaction
return this._committed || this._aborted;
}
+ _counter = 0;
+
_openRequest: BridgeIDBOpenDBRequest | null = null;
_backendTransaction?: DatabaseTransaction;
@@ -2390,13 +2481,9 @@ export class BridgeIDBTransaction
get objectStoreNames(): DOMStringList {
if (!this._cachedObjectStoreNames) {
- if (this._openRequest) {
- this._cachedObjectStoreNames = this._db.objectStoreNames;
- } else {
- this._cachedObjectStoreNames = fakeDOMStringList(
- Array.from(this._scope).sort(),
- );
- }
+ this._cachedObjectStoreNames = fakeDOMStringList(
+ Array.from(this._scope).sort(),
+ );
}
return this._cachedObjectStoreNames;
}
@@ -2496,41 +2583,34 @@ export class BridgeIDBTransaction
}
}
+ // All steps before happened synchronously. Now
+ // we asynchronously roll back the backend transaction,
+ // if necessary/possible.
+
+ const maybeBtx = this._backendTransaction;
+ if (maybeBtx) {
+ this._backend.rollback(maybeBtx);
+ }
+
// "Any object stores and indexes which were created during the
// transaction are now considered deleted for the purposes of other
// algorithms."
if (this._db._upgradeTransaction) {
for (const os of this._usedObjectStores) {
- if (os._justCreated) {
- os._deleted = true;
- }
+ os._abort();
}
for (const ind of this._usedIndexes) {
- if (ind._justCreated) {
- ind._deleted = true;
- }
+ ind._abort();
}
}
+ this._db._version = this._db._initialVersion;
+
// ("abort a transaction", step 5.1)
if (this._openRequest) {
this._db._upgradeTransaction = null;
}
- // All steps before happened synchronously. Now
- // we asynchronously roll back the backend transaction,
- // if necessary/possible.
-
- const maybeBtx = this._backendTransaction;
- if (maybeBtx) {
- this._db._schema = this._backend.getInitialTransactionSchema(maybeBtx);
- // Only roll back if we actually executed the scheduled operations.
- await this._backend.rollback(maybeBtx);
- this._backendTransaction = undefined;
- } else {
- this._db._schema = this._backend.getSchema(this._db._backendConnection);
- }
-
queueTask(() => {
const event = new FakeEvent("abort", {
bubbles: true,
@@ -2560,22 +2640,29 @@ export class BridgeIDBTransaction
throw new TransactionInactiveError();
}
- if (!this._db._schema.objectStores[name]) {
+ if (!this._scope.has(name)) {
throw new NotFoundError();
}
- if (!this._db._upgradeTransaction) {
- if (!this._scope.has(name)) {
- throw new NotFoundError();
- }
- }
-
const objectStore = this._objectStoresCache.get(name);
if (objectStore !== undefined) {
return objectStore;
}
- const newObjectStore = new BridgeIDBObjectStore(this, name);
+ const objectStoreMeta = this._backend.getObjectStoreMeta(
+ this._db._backendConnection,
+ name,
+ );
+
+ if (!objectStoreMeta) {
+ throw new NotFoundError();
+ }
+
+ const newObjectStore = new BridgeIDBObjectStore(
+ this,
+ name,
+ objectStoreMeta,
+ );
this._objectStoresCache.set(name, newObjectStore);
this._usedObjectStores.push(newObjectStore);
return newObjectStore;
@@ -2660,7 +2747,14 @@ export class BridgeIDBTransaction
// with error handling already built into operation
await operation();
} else {
- await waitMacroQueue();
+ this._counter++;
+ if (this._counter > 100) {
+ this._counter = 0;
+ // Give a chance for macro tasks to do something
+ // If we don't do this at all, we break WPT tests.
+ // If we always wait, performance is bad.
+ await waitMacroQueue();
+ }
let event;
try {
diff --git a/packages/idb-bridge/src/idb-wpt-ported/abort-in-initial-upgradeneeded.test.ts b/packages/idb-bridge/src/idb-wpt-ported/abort-in-initial-upgradeneeded.test.ts
index bbbcf9b94..14d4f7d6e 100644
--- a/packages/idb-bridge/src/idb-wpt-ported/abort-in-initial-upgradeneeded.test.ts
+++ b/packages/idb-bridge/src/idb-wpt-ported/abort-in-initial-upgradeneeded.test.ts
@@ -1,5 +1,7 @@
import test from "ava";
-import { createdb } from "./wptsupport.js";
+import { createdb, initTestIndexedDB } from "./wptsupport.js";
+
+test.before("test DB initialization", initTestIndexedDB);
test("WPT test abort-in-initial-upgradeneeded.htm", async (t) => {
await new Promise<void>((resolve, reject) => {
diff --git a/packages/idb-bridge/src/idb-wpt-ported/close-in-upgradeneeded.test.ts b/packages/idb-bridge/src/idb-wpt-ported/close-in-upgradeneeded.test.ts
index 723a0abb5..1a730df0b 100644
--- a/packages/idb-bridge/src/idb-wpt-ported/close-in-upgradeneeded.test.ts
+++ b/packages/idb-bridge/src/idb-wpt-ported/close-in-upgradeneeded.test.ts
@@ -1,5 +1,7 @@
import test from "ava";
-import { createdb } from "./wptsupport.js";
+import { createdb, initTestIndexedDB } from "./wptsupport.js";
+
+test.before("test DB initialization", initTestIndexedDB);
// When db.close is called in upgradeneeded, the db is cleaned up on refresh
test("WPT test close-in-upgradeneeded.htm", (t) => {
diff --git a/packages/idb-bridge/src/idb-wpt-ported/cursor-overloads.test.ts b/packages/idb-bridge/src/idb-wpt-ported/cursor-overloads.test.ts
index db2cdbca8..795d515ed 100644
--- a/packages/idb-bridge/src/idb-wpt-ported/cursor-overloads.test.ts
+++ b/packages/idb-bridge/src/idb-wpt-ported/cursor-overloads.test.ts
@@ -1,7 +1,9 @@
import test from "ava";
import { BridgeIDBKeyRange } from "../bridge-idb.js";
import { IDBRequest } from "../idbtypes.js";
-import { createdb } from "./wptsupport.js";
+import { createdb, initTestIndexedDB } from "./wptsupport.js";
+
+test.before("test DB initialization", initTestIndexedDB);
const IDBKeyRange = BridgeIDBKeyRange;
diff --git a/packages/idb-bridge/src/idb-wpt-ported/event-dispatch-active-flag.test.ts b/packages/idb-bridge/src/idb-wpt-ported/event-dispatch-active-flag.test.ts
index acc2a7578..1d895c712 100644
--- a/packages/idb-bridge/src/idb-wpt-ported/event-dispatch-active-flag.test.ts
+++ b/packages/idb-bridge/src/idb-wpt-ported/event-dispatch-active-flag.test.ts
@@ -2,11 +2,14 @@ import test from "ava";
import { BridgeIDBRequest } from "../bridge-idb.js";
import {
indexeddb_test,
+ initTestIndexedDB,
is_transaction_active,
keep_alive,
} from "./wptsupport.js";
-test("WPT test abort-in-initial-upgradeneeded.htm (subtest 1)", async (t) => {
+test.before("test DB initialization", initTestIndexedDB);
+
+test("WPT test event-dispatch-active-flag.html (subtest 1)", async (t) => {
// Transactions are active during success handlers
await indexeddb_test(
t,
@@ -54,7 +57,7 @@ test("WPT test abort-in-initial-upgradeneeded.htm (subtest 1)", async (t) => {
);
});
-test("WPT test abort-in-initial-upgradeneeded.htm (subtest 2)", async (t) => {
+test("WPT test event-dispatch-active-flag.html (subtest 2)", async (t) => {
// Transactions are active during success listeners
await indexeddb_test(
t,
@@ -100,7 +103,7 @@ test("WPT test abort-in-initial-upgradeneeded.htm (subtest 2)", async (t) => {
);
});
-test("WPT test abort-in-initial-upgradeneeded.htm (subtest 3)", async (t) => {
+test("WPT test event-dispatch-active-flag.html (subtest 3)", async (t) => {
// Transactions are active during error handlers
await indexeddb_test(
t,
@@ -149,7 +152,7 @@ test("WPT test abort-in-initial-upgradeneeded.htm (subtest 3)", async (t) => {
);
});
-test("WPT test abort-in-initial-upgradeneeded.htm (subtest 4)", async (t) => {
+test("WPT test event-dispatch-active-flag.html (subtest 4)", async (t) => {
// Transactions are active during error listeners
await indexeddb_test(
t,
diff --git a/packages/idb-bridge/src/idb-wpt-ported/idbcursor-advance-index.test.ts b/packages/idb-bridge/src/idb-wpt-ported/idbcursor-advance-index.test.ts
index 108e7c91c..1bf5ca697 100644
--- a/packages/idb-bridge/src/idb-wpt-ported/idbcursor-advance-index.test.ts
+++ b/packages/idb-bridge/src/idb-wpt-ported/idbcursor-advance-index.test.ts
@@ -1,6 +1,8 @@
import test from "ava";
import { BridgeIDBCursor,BridgeIDBRequest } from "../bridge-idb.js";
-import { createdb } from "./wptsupport.js";
+import { createdb, initTestIndexedDB } from "./wptsupport.js";
+
+test.before("test DB initialization", initTestIndexedDB);
test("WPT test idbcursor_advance_index.htm", async (t) => {
await new Promise<void>((resolve, reject) => {
diff --git a/packages/idb-bridge/src/idb-wpt-ported/idbcursor-continue-index.test.ts b/packages/idb-bridge/src/idb-wpt-ported/idbcursor-continue-index.test.ts
index f8b3a0f01..3cea3e86d 100644
--- a/packages/idb-bridge/src/idb-wpt-ported/idbcursor-continue-index.test.ts
+++ b/packages/idb-bridge/src/idb-wpt-ported/idbcursor-continue-index.test.ts
@@ -1,6 +1,9 @@
import test from "ava";
import { BridgeIDBCursor, BridgeIDBCursorWithValue } from "../bridge-idb.js";
-import { createdb } from "./wptsupport.js";
+import { createdb, initTestIndexedDB } from "./wptsupport.js";
+import { IDBDatabase } from "../idbtypes.js";
+
+test.before("test DB initialization", initTestIndexedDB);
test("WPT test idbcursor_continue_index.htm", (t) => {
return new Promise((resolve, reject) => {
@@ -209,7 +212,7 @@ test("WPT idbcursor-continue-index4.htm", (t) => {
// IDBCursor.continue() - index - iterate using 'prevunique'
test("WPT idbcursor-continue-index5.htm", (t) => {
return new Promise((resolve, reject) => {
- var db: any;
+ var db: IDBDatabase;
const records = [
{ pKey: "primaryKey_0", iKey: "indexKey_0" },
{ pKey: "primaryKey_1", iKey: "indexKey_1" },
diff --git a/packages/idb-bridge/src/idb-wpt-ported/idbcursor-continue-objectstore.test.ts b/packages/idb-bridge/src/idb-wpt-ported/idbcursor-continue-objectstore.test.ts
index e3169195f..d8b6f2b31 100644
--- a/packages/idb-bridge/src/idb-wpt-ported/idbcursor-continue-objectstore.test.ts
+++ b/packages/idb-bridge/src/idb-wpt-ported/idbcursor-continue-objectstore.test.ts
@@ -1,7 +1,9 @@
import test from "ava";
import { BridgeIDBCursor } from "../bridge-idb.js";
import { IDBDatabase } from "../idbtypes.js";
-import { createdb } from "./wptsupport.js";
+import { createdb, initTestIndexedDB } from "./wptsupport.js";
+
+test.before("test DB initialization", initTestIndexedDB);
// IDBCursor.continue() - object store - iterate to the next record
test("WPT test idbcursor_continue_objectstore.htm", (t) => {
diff --git a/packages/idb-bridge/src/idb-wpt-ported/idbcursor-delete-exception-order.test.ts b/packages/idb-bridge/src/idb-wpt-ported/idbcursor-delete-exception-order.test.ts
index f771d19a2..e159129da 100644
--- a/packages/idb-bridge/src/idb-wpt-ported/idbcursor-delete-exception-order.test.ts
+++ b/packages/idb-bridge/src/idb-wpt-ported/idbcursor-delete-exception-order.test.ts
@@ -1,5 +1,7 @@
import test from "ava";
-import { indexeddb_test } from "./wptsupport.js";
+import { indexeddb_test, initTestIndexedDB } from "./wptsupport.js";
+
+test.before("test DB initialization", initTestIndexedDB);
test("WPT idbcursor-delete-exception-order.htm", async (t) => {
// 'IDBCursor.delete exception order: TransactionInactiveError vs. ReadOnlyError'
diff --git a/packages/idb-bridge/src/idb-wpt-ported/idbcursor-delete-index.test.ts b/packages/idb-bridge/src/idb-wpt-ported/idbcursor-delete-index.test.ts
index 0232cf247..d34c9c3f9 100644
--- a/packages/idb-bridge/src/idb-wpt-ported/idbcursor-delete-index.test.ts
+++ b/packages/idb-bridge/src/idb-wpt-ported/idbcursor-delete-index.test.ts
@@ -1,7 +1,9 @@
import test from "ava";
import { BridgeIDBCursor } from "../bridge-idb.js";
import { IDBCursor } from "../idbtypes.js";
-import { createdb } from "./wptsupport.js";
+import { createdb, initTestIndexedDB } from "./wptsupport.js";
+
+test.before("test DB initialization", initTestIndexedDB);
// IDBCursor.delete() - index - remove a record from the object store
test("WPT idbcursor-delete-index.htm", (t) => {
diff --git a/packages/idb-bridge/src/idb-wpt-ported/idbcursor-delete-objectstore.test.ts b/packages/idb-bridge/src/idb-wpt-ported/idbcursor-delete-objectstore.test.ts
index 9410ca79e..2b9993b19 100644
--- a/packages/idb-bridge/src/idb-wpt-ported/idbcursor-delete-objectstore.test.ts
+++ b/packages/idb-bridge/src/idb-wpt-ported/idbcursor-delete-objectstore.test.ts
@@ -1,6 +1,8 @@
import test from "ava";
import { BridgeIDBCursor } from "../bridge-idb.js";
-import { createdb } from "./wptsupport.js";
+import { createdb, initTestIndexedDB } from "./wptsupport.js";
+
+test.before("test DB initialization", initTestIndexedDB);
// IDBCursor.delete() - object store - remove a record from the object store
test("WPT idbcursor-delete-objectstore.htm", (t) => {
diff --git a/packages/idb-bridge/src/idb-wpt-ported/idbcursor-reused.test.ts b/packages/idb-bridge/src/idb-wpt-ported/idbcursor-reused.test.ts
index 54745802e..e0e6c2bf8 100644
--- a/packages/idb-bridge/src/idb-wpt-ported/idbcursor-reused.test.ts
+++ b/packages/idb-bridge/src/idb-wpt-ported/idbcursor-reused.test.ts
@@ -1,5 +1,7 @@
import test from "ava";
-import { createdb } from "./wptsupport.js";
+import { createdb, initTestIndexedDB } from "./wptsupport.js";
+
+test.before("test DB initialization", initTestIndexedDB);
test("WPT idbcursor-reused.htm", async (t) => {
await new Promise<void>((resolve, reject) => {
@@ -24,7 +26,7 @@ test("WPT idbcursor-reused.htm", async (t) => {
case 0:
cursor = e.target.result;
- t.deepEqual(cursor.value, "data", "prequisite cursor.value");
+ t.deepEqual(cursor.value, "data", "prerequisite cursor.value");
cursor.custom_cursor_value = 1;
e.target.custom_request_value = 2;
@@ -32,7 +34,7 @@ test("WPT idbcursor-reused.htm", async (t) => {
break;
case 1:
- t.deepEqual(cursor.value, "data2", "prequisite cursor.value");
+ t.deepEqual(cursor.value, "data2", "prerequisite cursor.value");
t.deepEqual(cursor.custom_cursor_value, 1, "custom cursor value");
t.deepEqual(
e.target.custom_request_value,
diff --git a/packages/idb-bridge/src/idb-wpt-ported/idbcursor-update-index.test.ts b/packages/idb-bridge/src/idb-wpt-ported/idbcursor-update-index.test.ts
index 81a7cd753..8a878b35a 100644
--- a/packages/idb-bridge/src/idb-wpt-ported/idbcursor-update-index.test.ts
+++ b/packages/idb-bridge/src/idb-wpt-ported/idbcursor-update-index.test.ts
@@ -3,10 +3,13 @@ import { BridgeIDBCursor, BridgeIDBKeyRange } from "../bridge-idb.js";
import {
createDatabase,
createdb,
+ initTestIndexedDB,
promiseForRequest,
promiseForTransaction,
} from "./wptsupport.js";
+test.before("test DB initialization", initTestIndexedDB);
+
// IDBCursor.update() - index - modify a record in the object store
test("WPT test idbcursor_update_index.htm", (t) => {
return new Promise((resolve, reject) => {
diff --git a/packages/idb-bridge/src/idb-wpt-ported/idbfactory-cmp.test.ts b/packages/idb-bridge/src/idb-wpt-ported/idbfactory-cmp.test.ts
index a6cb97612..450bec7be 100644
--- a/packages/idb-bridge/src/idb-wpt-ported/idbfactory-cmp.test.ts
+++ b/packages/idb-bridge/src/idb-wpt-ported/idbfactory-cmp.test.ts
@@ -1,8 +1,10 @@
import test from "ava";
-import { idbFactory } from "./wptsupport.js";
+import { initTestIndexedDB, useTestIndexedDb } from "./wptsupport.js";
+
+test.before("test DB initialization", initTestIndexedDB);
test("WPT idbfactory-cmp*.html", async (t) => {
- const indexedDB = idbFactory;
+ const indexedDB = useTestIndexedDb();
var greater = indexedDB.cmp(2, 1);
var equal = indexedDB.cmp(2, 2);
var less = indexedDB.cmp(1, 2);
diff --git a/packages/idb-bridge/src/idb-wpt-ported/idbfactory-open.test.ts b/packages/idb-bridge/src/idb-wpt-ported/idbfactory-open.test.ts
index 02618f171..b8046fc1b 100644
--- a/packages/idb-bridge/src/idb-wpt-ported/idbfactory-open.test.ts
+++ b/packages/idb-bridge/src/idb-wpt-ported/idbfactory-open.test.ts
@@ -1,7 +1,10 @@
import test from "ava";
import { BridgeIDBVersionChangeEvent } from "../bridge-idb.js";
import FakeEvent from "../util/FakeEvent.js";
-import { createdb, format_value, idbFactory } from "./wptsupport.js";
+import { createdb, format_value, initTestIndexedDB, useTestIndexedDb } from "./wptsupport.js";
+import { IDBDatabase } from "../idbtypes.js";
+
+test.before("test DB initialization", initTestIndexedDB);
// IDBFactory.open() - request has no source
test("WPT idbfactory-open.htm", async (t) => {
@@ -36,7 +39,7 @@ test("WPT idbfactory-open2.htm", async (t) => {
// IDBFactory.open() - no version opens current database
test("WPT idbfactory-open3.htm", async (t) => {
- const indexedDB = idbFactory;
+ const indexedDB = useTestIndexedDb();
await new Promise<void>((resolve, reject) => {
var open_rq = createdb(t, undefined, 13);
var did_upgrade = false;
@@ -61,7 +64,6 @@ test("WPT idbfactory-open3.htm", async (t) => {
// IDBFactory.open() - new database has default version
test("WPT idbfactory-open4.htm", async (t) => {
- const indexedDB = idbFactory;
await new Promise<void>((resolve, reject) => {
var open_rq = createdb(t, t.title + "-database_name");
@@ -78,7 +80,6 @@ test("WPT idbfactory-open4.htm", async (t) => {
// IDBFactory.open() - new database is empty
test("WPT idbfactory-open5.htm", async (t) => {
- const indexedDB = idbFactory;
await new Promise<void>((resolve, reject) => {
var open_rq = createdb(t, t.title + "-database_name");
@@ -97,7 +98,7 @@ test("WPT idbfactory-open5.htm", async (t) => {
// IDBFactory.open() - open database with a lower version than current
test("WPT idbfactory-open6.htm", async (t) => {
- const indexedDB = idbFactory;
+ const indexedDB = useTestIndexedDb();
await new Promise<void>((resolve, reject) => {
var open_rq = createdb(t, undefined, 13);
var open_rq2: any;
@@ -131,7 +132,7 @@ test("WPT idbfactory-open6.htm", async (t) => {
// IDBFactory.open() - open database with a higher version than current
test("WPT idbfactory-open7.htm", async (t) => {
- const indexedDB = idbFactory;
+ const indexedDB = useTestIndexedDb();
await new Promise<void>((resolve, reject) => {
var open_rq = createdb(t, undefined, 13);
var did_upgrade = false;
@@ -169,7 +170,7 @@ test("WPT idbfactory-open7.htm", async (t) => {
// IDBFactory.open() - error in version change transaction aborts open
test("WPT idbfactory-open8.htm", async (t) => {
- const indexedDB = idbFactory;
+ const indexedDB = useTestIndexedDb();
await new Promise<void>((resolve, reject) => {
var open_rq = createdb(t, undefined, 13);
var did_upgrade = false;
@@ -193,7 +194,7 @@ test("WPT idbfactory-open8.htm", async (t) => {
// IDBFactory.open() - errors in version argument
test("WPT idbfactory-open9.htm", async (t) => {
- const indexedDB = idbFactory;
+ const indexedDB = useTestIndexedDb();
function should_throw(val: any, name?: string) {
if (!name) {
name = typeof val == "object" && val ? "object" : format_value(val);
@@ -281,9 +282,9 @@ test("WPT idbfactory-open9.htm", async (t) => {
// IDBFactory.open() - error in version change transaction aborts open
test("WPT idbfactory-open10.htm", async (t) => {
- const indexedDB = idbFactory;
+ const indexedDB = useTestIndexedDb();
await new Promise<void>((resolve, reject) => {
- var db: any, db2: any;
+ var db: IDBDatabase, db2: IDBDatabase;
var open_rq = createdb(t, undefined, 9);
open_rq.onupgradeneeded = function (e: any) {
@@ -350,7 +351,7 @@ test("WPT idbfactory-open10.htm", async (t) => {
var open_rq3 = indexedDB.open(db.name);
open_rq3.onsuccess = function (e: any) {
- var db3 = e.target.result;
+ var db3: IDBDatabase = e.target.result;
t.true(
db3.objectStoreNames.contains("store"),
@@ -407,7 +408,7 @@ test("WPT idbfactory-open10.htm", async (t) => {
// IDBFactory.open() - second open's transaction is available to get objectStores
test("WPT idbfactory-open11.htm", async (t) => {
- const indexedDB = idbFactory;
+ const indexedDB = useTestIndexedDb();
await new Promise<void>((resolve, reject) => {
var db: any;
var count_done = 0;
@@ -472,8 +473,6 @@ test("WPT idbfactory-open11.htm", async (t) => {
// IDBFactory.open() - upgradeneeded gets VersionChangeEvent
test("WPT idbfactory-open12.htm", async (t) => {
- const indexedDB = idbFactory;
-
var db: any;
var open_rq = createdb(t, undefined, 9);
diff --git a/packages/idb-bridge/src/idb-wpt-ported/idbindex-get.test.ts b/packages/idb-bridge/src/idb-wpt-ported/idbindex-get.test.ts
index d3b6e844e..ad8a57305 100644
--- a/packages/idb-bridge/src/idb-wpt-ported/idbindex-get.test.ts
+++ b/packages/idb-bridge/src/idb-wpt-ported/idbindex-get.test.ts
@@ -1,7 +1,9 @@
import test from "ava";
import { BridgeIDBKeyRange } from "../bridge-idb.js";
import { IDBDatabase } from "../idbtypes.js";
-import { createdb } from "./wptsupport.js";
+import { createdb, initTestIndexedDB } from "./wptsupport.js";
+
+test.before("test DB initialization", initTestIndexedDB);
// IDBIndex.get() - returns the record
test("WPT idbindex_get.htm", async (t) => {
@@ -93,7 +95,7 @@ test("WPT idbindex_get3.htm", async (t) => {
// IDBIndex.get() - returns the record with the first key in the range
test("WPT idbindex_get4.htm", async (t) => {
await new Promise<void>((resolve, reject) => {
- var db: any;
+ var db: IDBDatabase;
var open_rq = createdb(t);
diff --git a/packages/idb-bridge/src/idb-wpt-ported/idbindex-openCursor.test.ts b/packages/idb-bridge/src/idb-wpt-ported/idbindex-openCursor.test.ts
index 765bcf06a..5d61e68e5 100644
--- a/packages/idb-bridge/src/idb-wpt-ported/idbindex-openCursor.test.ts
+++ b/packages/idb-bridge/src/idb-wpt-ported/idbindex-openCursor.test.ts
@@ -1,5 +1,7 @@
import test from "ava";
-import { createdb } from "./wptsupport.js";
+import { createdb, initTestIndexedDB } from "./wptsupport.js";
+
+test.before("test DB initialization", initTestIndexedDB);
// IDBIndex.openCursor() - throw InvalidStateError when the index is deleted
test("WPT test idbindex-openCursor.htm", (t) => {
diff --git a/packages/idb-bridge/src/idb-wpt-ported/idbobjectstore-add-put-exception-order.test.ts b/packages/idb-bridge/src/idb-wpt-ported/idbobjectstore-add-put-exception-order.test.ts
index 901eda89c..60bf0cfb2 100644
--- a/packages/idb-bridge/src/idb-wpt-ported/idbobjectstore-add-put-exception-order.test.ts
+++ b/packages/idb-bridge/src/idb-wpt-ported/idbobjectstore-add-put-exception-order.test.ts
@@ -1,5 +1,7 @@
import test, { ExecutionContext } from "ava";
-import { indexeddb_test } from "./wptsupport.js";
+import { indexeddb_test, initTestIndexedDB } from "./wptsupport.js";
+
+test.before("test DB initialization", initTestIndexedDB);
async function t1(t: ExecutionContext, method: string): Promise<void> {
await indexeddb_test(
@@ -55,8 +57,6 @@ async function t2(t: ExecutionContext, method: string): Promise<void> {
done();
}, 0);
-
- console.log(`queued task for ${method}`);
},
"t2",
);
diff --git a/packages/idb-bridge/src/idb-wpt-ported/idbobjectstore-add.test.ts b/packages/idb-bridge/src/idb-wpt-ported/idbobjectstore-add.test.ts
index e8bc17471..4941c43d6 100644
--- a/packages/idb-bridge/src/idb-wpt-ported/idbobjectstore-add.test.ts
+++ b/packages/idb-bridge/src/idb-wpt-ported/idbobjectstore-add.test.ts
@@ -1,7 +1,9 @@
import test from "ava";
import { BridgeIDBRequest } from "../bridge-idb.js";
import { IDBDatabase } from "../idbtypes.js";
-import { createdb } from "./wptsupport.js";
+import { createdb, initTestIndexedDB } from "./wptsupport.js";
+
+test.before("test DB initialization", initTestIndexedDB);
// IDBObjectStore.add() - add with an inline key
test("WPT idbobjectstore_add.htm", async (t) => {
diff --git a/packages/idb-bridge/src/idb-wpt-ported/idbobjectstore-get.test.ts b/packages/idb-bridge/src/idb-wpt-ported/idbobjectstore-get.test.ts
index 79064d19d..922c2bcf4 100644
--- a/packages/idb-bridge/src/idb-wpt-ported/idbobjectstore-get.test.ts
+++ b/packages/idb-bridge/src/idb-wpt-ported/idbobjectstore-get.test.ts
@@ -1,6 +1,8 @@
import test from "ava";
import { BridgeIDBKeyRange } from "../bridge-idb.js";
-import { createdb } from "./wptsupport.js";
+import { createdb, initTestIndexedDB } from "./wptsupport.js";
+
+test.before("test DB initialization", initTestIndexedDB);
// IDBObjectStore.get() - key is a number
test("WPT idbobjectstore_get.htm", (t) => {
diff --git a/packages/idb-bridge/src/idb-wpt-ported/idbobjectstore-put.test.ts b/packages/idb-bridge/src/idb-wpt-ported/idbobjectstore-put.test.ts
index 152e3a9c1..f051c57b6 100644
--- a/packages/idb-bridge/src/idb-wpt-ported/idbobjectstore-put.test.ts
+++ b/packages/idb-bridge/src/idb-wpt-ported/idbobjectstore-put.test.ts
@@ -1,6 +1,8 @@
import test from "ava";
import { BridgeIDBRequest } from "../bridge-idb.js";
-import { createdb } from "./wptsupport.js";
+import { createdb, initTestIndexedDB } from "./wptsupport.js";
+
+test.before("test DB initialization", initTestIndexedDB);
// IDBObjectStore.put() - put with an inline key
test("WPT idbobjectstore_put.htm", (t) => {
diff --git a/packages/idb-bridge/src/idb-wpt-ported/idbobjectstore-rename-store.test.ts b/packages/idb-bridge/src/idb-wpt-ported/idbobjectstore-rename-store.test.ts
index a8aab828a..6f04552fa 100644
--- a/packages/idb-bridge/src/idb-wpt-ported/idbobjectstore-rename-store.test.ts
+++ b/packages/idb-bridge/src/idb-wpt-ported/idbobjectstore-rename-store.test.ts
@@ -6,9 +6,12 @@ import {
createBooksStore,
createDatabase,
createNotBooksStore,
+ initTestIndexedDB,
migrateDatabase,
} from "./wptsupport.js";
+test.before("test DB initialization", initTestIndexedDB);
+
// IndexedDB: object store renaming support
// IndexedDB object store rename in new transaction
test("WPT idbobjectstore-rename-store.html (subtest 1)", async (t) => {
diff --git a/packages/idb-bridge/src/idb-wpt-ported/idbtransaction-oncomplete.test.ts b/packages/idb-bridge/src/idb-wpt-ported/idbtransaction-oncomplete.test.ts
index a501ff2c9..f728cd487 100644
--- a/packages/idb-bridge/src/idb-wpt-ported/idbtransaction-oncomplete.test.ts
+++ b/packages/idb-bridge/src/idb-wpt-ported/idbtransaction-oncomplete.test.ts
@@ -1,5 +1,7 @@
import test from "ava";
-import { createdb } from "./wptsupport.js";
+import { createdb, initTestIndexedDB } from "./wptsupport.js";
+
+test.before("test DB initialization", initTestIndexedDB);
// IDBTransaction - complete event
test("WPT idbtransaction-oncomplete.htm", async (t) => {
diff --git a/packages/idb-bridge/src/idb-wpt-ported/keypath.test.ts b/packages/idb-bridge/src/idb-wpt-ported/keypath.test.ts
index 7ef1301f7..f15f93873 100644
--- a/packages/idb-bridge/src/idb-wpt-ported/keypath.test.ts
+++ b/packages/idb-bridge/src/idb-wpt-ported/keypath.test.ts
@@ -1,5 +1,7 @@
import test from "ava";
-import { assert_key_equals, createdb } from "./wptsupport.js";
+import { assert_key_equals, createdb, initTestIndexedDB } from "./wptsupport.js";
+
+test.before("test DB initialization", initTestIndexedDB);
test("WPT test keypath.htm", async (t) => {
function keypath(
@@ -9,8 +11,6 @@ test("WPT test keypath.htm", async (t) => {
desc?: string,
) {
return new Promise<void>((resolve, reject) => {
- console.log("key path", keypath);
- console.log("checking", desc);
let db: any;
const store_name = "store-" + Date.now() + Math.random();
diff --git a/packages/idb-bridge/src/idb-wpt-ported/request-bubble-and-capture.test.ts b/packages/idb-bridge/src/idb-wpt-ported/request-bubble-and-capture.test.ts
index 526c06784..14c8f3be5 100644
--- a/packages/idb-bridge/src/idb-wpt-ported/request-bubble-and-capture.test.ts
+++ b/packages/idb-bridge/src/idb-wpt-ported/request-bubble-and-capture.test.ts
@@ -1,6 +1,8 @@
import test from "ava";
import { EventTarget } from "../idbtypes.js";
-import { createdb } from "./wptsupport.js";
+import { createdb, initTestIndexedDB } from "./wptsupport.js";
+
+test.before("test DB initialization", initTestIndexedDB);
// Bubbling and capturing of request events
test("WPT request_bubble-and-capture.htm", async (t) => {
diff --git a/packages/idb-bridge/src/idb-wpt-ported/transaction-requestqueue.test.ts b/packages/idb-bridge/src/idb-wpt-ported/transaction-requestqueue.test.ts
index 9d76e79f2..971330e3d 100644
--- a/packages/idb-bridge/src/idb-wpt-ported/transaction-requestqueue.test.ts
+++ b/packages/idb-bridge/src/idb-wpt-ported/transaction-requestqueue.test.ts
@@ -1,5 +1,7 @@
import test from "ava";
-import { createdb } from "./wptsupport.js";
+import { createdb, initTestIndexedDB } from "./wptsupport.js";
+
+test.before("test DB initialization", initTestIndexedDB);
// Transactions have a request queue
test("transaction-requestqueue.htm", async (t) => {
diff --git a/packages/idb-bridge/src/idb-wpt-ported/value.test.ts b/packages/idb-bridge/src/idb-wpt-ported/value.test.ts
index a80ec2b5a..95712e152 100644
--- a/packages/idb-bridge/src/idb-wpt-ported/value.test.ts
+++ b/packages/idb-bridge/src/idb-wpt-ported/value.test.ts
@@ -1,6 +1,8 @@
import test from "ava";
import { IDBVersionChangeEvent } from "../idbtypes.js";
-import { createdb } from "./wptsupport.js";
+import { createdb, initTestIndexedDB } from "./wptsupport.js";
+
+test.before("test DB initialization", initTestIndexedDB);
test("WPT test value.htm, array", (t) => {
return new Promise((resolve, reject) => {
@@ -12,7 +14,6 @@ test("WPT test value.htm, array", (t) => {
createdb(t).onupgradeneeded = function (e: IDBVersionChangeEvent) {
(e.target as any).result.createObjectStore("store").add(value, 1);
(e.target as any).onsuccess = (e: any) => {
- console.log("in first onsuccess");
e.target.result
.transaction("store")
.objectStore("store")
@@ -35,13 +36,10 @@ test("WPT test value.htm, date", (t) => {
createdb(t).onupgradeneeded = function (e: IDBVersionChangeEvent) {
(e.target as any).result.createObjectStore("store").add(value, 1);
(e.target as any).onsuccess = (e: any) => {
- console.log("in first onsuccess");
e.target.result
.transaction("store")
.objectStore("store")
.get(1).onsuccess = (e: any) => {
- console.log("target", e.target);
- console.log("result", e.target.result);
t.assert(e.target.result instanceof _instanceof, "instanceof");
resolve();
};
diff --git a/packages/idb-bridge/src/idb-wpt-ported/wptsupport.ts b/packages/idb-bridge/src/idb-wpt-ported/wptsupport.ts
index 7f68a53e8..c648bf53f 100644
--- a/packages/idb-bridge/src/idb-wpt-ported/wptsupport.ts
+++ b/packages/idb-bridge/src/idb-wpt-ported/wptsupport.ts
@@ -1,5 +1,5 @@
import { ExecutionContext } from "ava";
-import { BridgeIDBFactory, BridgeIDBRequest } from "../bridge-idb.js";
+import { BridgeIDBRequest } from "../bridge-idb.js";
import {
IDBDatabase,
IDBIndex,
@@ -8,17 +8,10 @@ import {
IDBRequest,
IDBTransaction,
} from "../idbtypes.js";
-import { MemoryBackend } from "../MemoryBackend.js";
+import { initTestIndexedDB , useTestIndexedDb } from "../testingdb.js";
import { compareKeys } from "../util/cmp.js";
-BridgeIDBFactory.enableTracing = true;
-const backend = new MemoryBackend();
-backend.enableTracing = true;
-export const idbFactory = new BridgeIDBFactory(backend);
-
-const self = {
- indexedDB: idbFactory,
-};
+export { initTestIndexedDB, useTestIndexedDb } from "../testingdb.js"
export function createdb(
t: ExecutionContext<unknown>,
@@ -27,8 +20,8 @@ export function createdb(
): IDBOpenDBRequest {
var rq_open: IDBOpenDBRequest;
dbname = dbname ? dbname : "testdb-" + new Date().getTime() + Math.random();
- if (version) rq_open = self.indexedDB.open(dbname, version);
- else rq_open = self.indexedDB.open(dbname);
+ if (version) rq_open = useTestIndexedDb().open(dbname, version);
+ else rq_open = useTestIndexedDb().open(dbname);
return rq_open;
}
@@ -111,7 +104,7 @@ export async function migrateNamedDatabase(
migrationCallback: MigrationCallback,
): Promise<IDBDatabase> {
return new Promise<IDBDatabase>((resolve, reject) => {
- const request = self.indexedDB.open(databaseName, newVersion);
+ const request = useTestIndexedDb().open(databaseName, newVersion);
request.onupgradeneeded = (event: any) => {
const database = event.target.result;
const transaction = event.target.transaction;
@@ -175,7 +168,7 @@ export async function createDatabase(
setupCallback: MigrationCallback,
): Promise<IDBDatabase> {
const databaseName = makeDatabaseName(t.title);
- const request = self.indexedDB.deleteDatabase(databaseName);
+ const request = useTestIndexedDb().deleteDatabase(databaseName);
return migrateNamedDatabase(t, databaseName, 1, setupCallback);
}
@@ -463,9 +456,9 @@ export function indexeddb_test(
options = Object.assign({ upgrade_will_abort: false }, options);
const dbname =
"testdb-" + new Date().getTime() + Math.random() + (dbsuffix ?? "");
- var del = self.indexedDB.deleteDatabase(dbname);
+ var del = useTestIndexedDb().deleteDatabase(dbname);
del.onerror = () => t.fail("deleteDatabase should succeed");
- var open = self.indexedDB.open(dbname, 1);
+ var open = useTestIndexedDb().open(dbname, 1);
open.onupgradeneeded = function () {
var db = open.result;
t.teardown(function () {
@@ -474,7 +467,7 @@ export function indexeddb_test(
e.preventDefault();
};
db.close();
- self.indexedDB.deleteDatabase(db.name);
+ useTestIndexedDb().deleteDatabase(db.name);
});
var tx = open.transaction!;
upgrade_func(resolve, db, tx, open);
diff --git a/packages/idb-bridge/src/idbpromutil.ts b/packages/idb-bridge/src/idbpromutil.ts
new file mode 100644
index 000000000..e711db027
--- /dev/null
+++ b/packages/idb-bridge/src/idbpromutil.ts
@@ -0,0 +1,26 @@
+import { BridgeIDBTransaction } from "./bridge-idb.js";
+import { IDBRequest } from "./idbtypes.js";
+
+export function promiseFromRequest(request: IDBRequest): Promise<any> {
+ return new Promise((resolve, reject) => {
+ request.onsuccess = () => {
+ resolve(request.result);
+ };
+ request.onerror = () => {
+ reject(request.error);
+ };
+ });
+}
+
+export function promiseFromTransaction(
+ transaction: BridgeIDBTransaction,
+): Promise<void> {
+ return new Promise<void>((resolve, reject) => {
+ transaction.oncomplete = () => {
+ resolve();
+ };
+ transaction.onerror = () => {
+ reject();
+ };
+ });
+} \ No newline at end of file
diff --git a/packages/idb-bridge/src/idbtypes.ts b/packages/idb-bridge/src/idbtypes.ts
index a7878c38f..9ee93e050 100644
--- a/packages/idb-bridge/src/idbtypes.ts
+++ b/packages/idb-bridge/src/idbtypes.ts
@@ -19,48 +19,27 @@ and limitations under the License.
* Instead of ambient types, we export type declarations.
*/
-/**
- * @public
- */
export type IDBKeyPath = string;
-/**
- * @public
- */
export interface EventListener {
(evt: Event): void;
}
-/**
- * @public
- */
export interface EventListenerObject {
handleEvent(evt: Event): void;
}
-/**
- * @public
- */
export interface EventListenerOptions {
capture?: boolean;
}
-/**
- * @public
- */
export interface AddEventListenerOptions extends EventListenerOptions {
once?: boolean;
passive?: boolean;
}
-/**
- * @public
- */
export type IDBTransactionMode = "readonly" | "readwrite" | "versionchange";
-/**
- * @public
- */
export type EventListenerOrEventListenerObject =
| EventListener
| EventListenerObject;
@@ -68,8 +47,6 @@ export type EventListenerOrEventListenerObject =
/**
* EventTarget is a DOM interface implemented by objects that can receive
* events and may have listeners for them.
- *
- * @public
*/
export interface EventTarget {
/**
diff --git a/packages/idb-bridge/src/index.ts b/packages/idb-bridge/src/index.ts
index fc99b2ccd..47ff80119 100644
--- a/packages/idb-bridge/src/index.ts
+++ b/packages/idb-bridge/src/index.ts
@@ -2,14 +2,10 @@ import {
Backend,
DatabaseConnection,
DatabaseTransaction,
- IndexProperties,
- ObjectStoreProperties,
- RecordGetRequest,
RecordGetResponse,
RecordStoreRequest,
RecordStoreResponse,
ResultLevel,
- Schema,
StoreLevel,
} from "./backend-interface.js";
import {
@@ -36,6 +32,9 @@ import {
} from "./MemoryBackend.js";
import { Listener } from "./util/FakeEventTarget.js";
+export * from "./SqliteBackend.js";
+export * from "./sqlite3-interface.js";
+
export * from "./idbtypes.js";
export { MemoryBackend } from "./MemoryBackend.js";
export type { AccessStats } from "./MemoryBackend.js";
@@ -55,21 +54,17 @@ export {
};
export type {
DatabaseTransaction,
- RecordGetRequest,
RecordGetResponse,
- Schema,
Backend,
DatabaseList,
RecordStoreRequest,
RecordStoreResponse,
DatabaseConnection,
- ObjectStoreProperties,
RequestObj,
DatabaseDump,
ObjectStoreDump,
IndexRecord,
ObjectStoreRecord,
- IndexProperties,
MemoryBackendDump,
Event,
Listener,
diff --git a/packages/idb-bridge/src/node-sqlite3-impl.ts b/packages/idb-bridge/src/node-sqlite3-impl.ts
new file mode 100644
index 000000000..fa38d298f
--- /dev/null
+++ b/packages/idb-bridge/src/node-sqlite3-impl.ts
@@ -0,0 +1,84 @@
+/*
+ This file is part of GNU Taler
+ (C) 2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+// @ts-ignore: optional dependency
+import type Database from "better-sqlite3";
+import {
+ ResultRow,
+ Sqlite3Interface,
+ Sqlite3Statement,
+} from "./sqlite3-interface.js";
+
+export async function createNodeSqlite3Impl(): Promise<Sqlite3Interface> {
+ // @ts-ignore: optional dependency
+ const bsq = (await import("better-sqlite3")).default;
+
+ return {
+ open(filename: string) {
+ const internalDbHandle = bsq(filename);
+ return {
+ internalDbHandle,
+ close() {
+ internalDbHandle.close();
+ },
+ prepare(stmtStr): Sqlite3Statement {
+ const stmtHandle = internalDbHandle.prepare(stmtStr);
+ return {
+ internalStatement: stmtHandle,
+ getAll(params): ResultRow[] {
+ let res: ResultRow[];
+ if (params === undefined) {
+ res = stmtHandle.all() as ResultRow[];
+ } else {
+ res = stmtHandle.all(params) as ResultRow[];
+ }
+ return res;
+ },
+ getFirst(params): ResultRow | undefined {
+ let res: ResultRow | undefined;
+ if (params === undefined) {
+ res = stmtHandle.get() as ResultRow | undefined;
+ } else {
+ res = stmtHandle.get(params) as ResultRow | undefined;
+ }
+ return res;
+ },
+ run(params) {
+ const myParams = [];
+ if (params !== undefined) {
+ myParams.push(params);
+ }
+ // The better-sqlite3 library doesn't like it we pass
+ // undefined directly.
+ let res: Database.RunResult;
+ if (params !== undefined) {
+ res = stmtHandle.run(params);
+ } else {
+ res = stmtHandle.run();
+ }
+ return {
+ lastInsertRowid: res.lastInsertRowid,
+ };
+ },
+ };
+ },
+ exec(sqlStr): void {
+ internalDbHandle.exec(sqlStr);
+ },
+ };
+ },
+ };
+}
diff --git a/packages/idb-bridge/src/sqlite3-interface.ts b/packages/idb-bridge/src/sqlite3-interface.ts
new file mode 100644
index 000000000..8668ef844
--- /dev/null
+++ b/packages/idb-bridge/src/sqlite3-interface.ts
@@ -0,0 +1,34 @@
+export type Sqlite3Database = {
+ internalDbHandle: any;
+ exec(sqlStr: string): void;
+ prepare(stmtStr: string): Sqlite3Statement;
+ close(): void;
+};
+export type Sqlite3Statement = {
+ internalStatement: any;
+
+ run(params?: BindParams): RunResult;
+ getAll(params?: BindParams): ResultRow[];
+ getFirst(params?: BindParams): ResultRow | undefined;
+};
+
+export interface RunResult {
+ lastInsertRowid: number | bigint;
+}
+
+export type Sqlite3Value = string | Uint8Array | number | null | bigint;
+
+export type BindParams = Record<string, Sqlite3Value | undefined>;
+export type ResultRow = Record<string, Sqlite3Value>;
+
+/**
+ * Common interface that multiple sqlite3 bindings
+ * (such as better-sqlite3 or qtart's sqlite3 bindings)
+ * can adapt to.
+ *
+ * This does not expose full sqlite3 functionality, but just enough
+ * to be used by our IndexedDB sqlite3 backend.
+ */
+export interface Sqlite3Interface {
+ open(filename: string): Sqlite3Database;
+}
diff --git a/packages/idb-bridge/src/testingdb.ts b/packages/idb-bridge/src/testingdb.ts
new file mode 100644
index 000000000..6c13979ca
--- /dev/null
+++ b/packages/idb-bridge/src/testingdb.ts
@@ -0,0 +1,43 @@
+/*
+ Copyright 2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received 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 { createSqliteBackend } from "./SqliteBackend.js";
+import { BridgeIDBFactory } from "./bridge-idb.js";
+import { IDBFactory } from "./idbtypes.js";
+import { createNodeSqlite3Impl } from "./node-sqlite3-impl.js";
+
+let idbFactory: IDBFactory | undefined = undefined;
+
+export async function initTestIndexedDB(): Promise<void> {
+ // const backend = new MemoryBackend();
+ // backend.enableTracing = true;
+
+ const sqlite3Impl = await createNodeSqlite3Impl();
+
+ const backend = await createSqliteBackend(sqlite3Impl, {
+ filename: ":memory:",
+ });
+
+ idbFactory = new BridgeIDBFactory(backend);
+ backend.enableTracing = false;
+ BridgeIDBFactory.enableTracing = false;
+}
+
+export function useTestIndexedDb(): IDBFactory {
+ if (!idbFactory) {
+ throw Error("indexeddb factory not initialized");
+ }
+ return idbFactory;
+}
diff --git a/packages/idb-bridge/src/util/FakeDomEvent.ts b/packages/idb-bridge/src/util/FakeDomEvent.ts
new file mode 100644
index 000000000..b3ff298ec
--- /dev/null
+++ b/packages/idb-bridge/src/util/FakeDomEvent.ts
@@ -0,0 +1,103 @@
+/*
+ Copyright 2017 Jeremy Scheff
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ or implied. See the License for the specific language governing
+ permissions and limitations under the License.
+*/
+
+import FakeEventTarget from "./FakeEventTarget.js";
+import { Event, EventTarget } from "../idbtypes.js";
+
+/** @public */
+export type EventType =
+ | "abort"
+ | "blocked"
+ | "complete"
+ | "error"
+ | "success"
+ | "upgradeneeded"
+ | "versionchange";
+
+export class FakeDomEvent implements Event {
+ public eventPath: FakeEventTarget[] = [];
+ public type: EventType;
+
+ public readonly NONE = 0;
+ public readonly CAPTURING_PHASE = 1;
+ public readonly AT_TARGET = 2;
+ public readonly BUBBLING_PHASE = 3;
+
+ // Flags
+ public propagationStopped = false;
+ public immediatePropagationStopped = false;
+ public canceled = false;
+ public initialized = true;
+ public dispatched = false;
+
+ public target: FakeEventTarget | null = null;
+ public currentTarget: FakeEventTarget | null = null;
+
+ public eventPhase: 0 | 1 | 2 | 3 = 0;
+
+ public defaultPrevented = false;
+
+ public isTrusted = false;
+ public timeStamp = Date.now();
+
+ public bubbles: boolean;
+ public cancelable: boolean;
+
+ constructor(
+ type: EventType,
+ eventInitDict: { bubbles?: boolean; cancelable?: boolean } = {},
+ ) {
+ this.type = type;
+
+ this.bubbles =
+ eventInitDict.bubbles !== undefined ? eventInitDict.bubbles : false;
+ this.cancelable =
+ eventInitDict.cancelable !== undefined ? eventInitDict.cancelable : false;
+ }
+ cancelBubble: boolean = false;
+ composed: boolean = false;
+ returnValue: boolean = false;
+ get srcElement(): EventTarget | null {
+ return this.target;
+ }
+ composedPath(): EventTarget[] {
+ throw new Error("Method not implemented.");
+ }
+ initEvent(
+ type: string,
+ bubbles?: boolean | undefined,
+ cancelable?: boolean | undefined,
+ ): void {
+ throw new Error("Method not implemented.");
+ }
+
+ public preventDefault() {
+ if (this.cancelable) {
+ this.canceled = true;
+ }
+ }
+
+ public stopPropagation() {
+ this.propagationStopped = true;
+ }
+
+ public stopImmediatePropagation() {
+ this.propagationStopped = true;
+ this.immediatePropagationStopped = true;
+ }
+}
+
+export default FakeDomEvent;
diff --git a/packages/idb-bridge/src/util/FakeEventTarget.ts b/packages/idb-bridge/src/util/FakeEventTarget.ts
index 79f57cce3..839906a34 100644
--- a/packages/idb-bridge/src/util/FakeEventTarget.ts
+++ b/packages/idb-bridge/src/util/FakeEventTarget.ts
@@ -180,7 +180,7 @@ abstract class FakeEventTarget implements EventTarget {
fe.eventPath.reverse();
fe.eventPhase = event.BUBBLING_PHASE;
if (fe.eventPath.length === 0 && event.type === "error") {
- console.error("Unhandled error event: ", event.target);
+ console.error("Unhandled error event on target: ", event.target);
}
for (const obj of event.eventPath) {
if (!event.propagationStopped) {
diff --git a/packages/idb-bridge/src/util/extractKey.ts b/packages/idb-bridge/src/util/extractKey.ts
index 6a3d468ef..2a4ec45b9 100644
--- a/packages/idb-bridge/src/util/extractKey.ts
+++ b/packages/idb-bridge/src/util/extractKey.ts
@@ -19,7 +19,11 @@ import { IDBKeyPath, IDBValidKey } from "../idbtypes.js";
import { valueToKey } from "./valueToKey.js";
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-steps-for-extracting-a-key-from-a-value-using-a-key-path
+/**
+ * Algorithm to "extract a key from a value using a key path".
+ */
export const extractKey = (keyPath: IDBKeyPath | IDBKeyPath[], value: any) => {
+ //console.log(`extracting key ${JSON.stringify(keyPath)} from ${JSON.stringify(value)}`);
if (Array.isArray(keyPath)) {
const result: IDBValidKey[] = [];
diff --git a/packages/idb-bridge/src/util/fakeDOMStringList.ts b/packages/idb-bridge/src/util/fakeDOMStringList.ts
index 92785f9e1..24f5c96f4 100644
--- a/packages/idb-bridge/src/util/fakeDOMStringList.ts
+++ b/packages/idb-bridge/src/util/fakeDOMStringList.ts
@@ -21,7 +21,7 @@ export interface FakeDOMStringList extends Array<string> {
item: (i: number) => string | null;
}
-// Would be nicer to sublcass Array, but I'd have to sacrifice Node 4 support to do that.
+// Would be nicer to subclass Array, but I'd have to sacrifice Node 4 support to do that.
export const fakeDOMStringList = (arr: string[]): FakeDOMStringList => {
const arr2 = arr.slice();
diff --git a/packages/idb-bridge/src/util/key-storage.test.ts b/packages/idb-bridge/src/util/key-storage.test.ts
new file mode 100644
index 000000000..dc1e1827c
--- /dev/null
+++ b/packages/idb-bridge/src/util/key-storage.test.ts
@@ -0,0 +1,39 @@
+/*
+ This file is part of GNU Taler
+ (C) 2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import test, { ExecutionContext } from "ava";
+import { deserializeKey, serializeKey } from "./key-storage.js";
+import { IDBValidKey } from "../idbtypes.js";
+
+function checkKeySer(t: ExecutionContext, k: IDBValidKey): void {
+ const keyEnc = serializeKey(k);
+ const keyDec = deserializeKey(keyEnc);
+ t.deepEqual(k, keyDec);
+}
+
+test("basics", (t) => {
+ checkKeySer(t, "foo");
+ checkKeySer(t, "foo\0bar");
+ checkKeySer(t, "foo\u1000bar");
+ checkKeySer(t, "foo\u2000bar");
+ checkKeySer(t, "foo\u5000bar");
+ checkKeySer(t, "foo\uffffbar");
+ checkKeySer(t, 42);
+ checkKeySer(t, 255);
+ checkKeySer(t, 254);
+ checkKeySer(t, [1, 2, 3, 4]);
+ checkKeySer(t, [[[1], 3], [4]]);
+});
diff --git a/packages/idb-bridge/src/util/key-storage.ts b/packages/idb-bridge/src/util/key-storage.ts
new file mode 100644
index 000000000..b71548dd3
--- /dev/null
+++ b/packages/idb-bridge/src/util/key-storage.ts
@@ -0,0 +1,363 @@
+/*
+ This file is part of GNU Taler
+ (C) 2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/*
+Encoding rules (inspired by Firefox, but slightly simplified):
+
+Numbers: 0x10 n n n n n n n n
+Dates: 0x20 n n n n n n n n
+Strings: 0x30 s s s s ... 0
+Binaries: 0x40 s s s s ... 0
+Arrays: 0x50 i i i ... 0
+
+Numbers/dates are encoded as 64-bit IEEE 754 floats with the sign bit
+flipped, in order to make them sortable.
+*/
+
+/**
+ * Imports.
+ */
+import { IDBValidKey } from "../idbtypes.js";
+
+const tagNum = 0xa0;
+const tagDate = 0xb0;
+const tagString = 0xc0;
+const tagBinary = 0xc0;
+const tagArray = 0xe0;
+
+const oneByteOffset = 0x01;
+const twoByteOffset = 0x7f;
+const oneByteMax = 0x7e;
+const twoByteMax = 0x3fff + twoByteOffset;
+const twoByteMask = 0b1000_0000;
+const threeByteMask = 0b1100_0000;
+
+export function countEncSize(c: number): number {
+ if (c > twoByteMax) {
+ return 3;
+ }
+ if (c > oneByteMax) {
+ return 2;
+ }
+ return 1;
+}
+
+export function writeEnc(dv: DataView, offset: number, c: number): number {
+ if (c > twoByteMax) {
+ dv.setUint8(offset + 2, (c & 0xff) << 6);
+ dv.setUint8(offset + 1, (c >>> 2) & 0xff);
+ dv.setUint8(offset, threeByteMask | (c >>> 10));
+ return 3;
+ } else if (c > oneByteMax) {
+ c -= twoByteOffset;
+ dv.setUint8(offset + 1, c & 0xff);
+ dv.setUint8(offset, (c >>> 8) | twoByteMask);
+ return 2;
+ } else {
+ c += oneByteOffset;
+ dv.setUint8(offset, c);
+ return 1;
+ }
+}
+
+export function internalSerializeString(
+ dv: DataView,
+ offset: number,
+ key: string,
+): number {
+ dv.setUint8(offset, tagString);
+ let n = 1;
+ for (let i = 0; i < key.length; i++) {
+ let c = key.charCodeAt(i);
+ n += writeEnc(dv, offset + n, c);
+ }
+ // Null terminator
+ dv.setUint8(offset + n, 0);
+ n++;
+ return n;
+}
+
+export function countSerializeKey(key: IDBValidKey): number {
+ if (typeof key === "number") {
+ return 9;
+ }
+ if (key instanceof Date) {
+ return 9;
+ }
+ if (key instanceof ArrayBuffer) {
+ let len = 2;
+ const uv = new Uint8Array(key);
+ for (let i = 0; i < uv.length; i++) {
+ len += countEncSize(uv[i]);
+ }
+ return len;
+ }
+ if (ArrayBuffer.isView(key)) {
+ let len = 2;
+ const uv = new Uint8Array(key.buffer, key.byteOffset, key.byteLength);
+ for (let i = 0; i < uv.length; i++) {
+ len += countEncSize(uv[i]);
+ }
+ return len;
+ }
+ if (typeof key === "string") {
+ let len = 2;
+ for (let i = 0; i < key.length; i++) {
+ len += countEncSize(key.charCodeAt(i));
+ }
+ return len;
+ }
+ if (Array.isArray(key)) {
+ let len = 2;
+ for (let i = 0; i < key.length; i++) {
+ len += countSerializeKey(key[i]);
+ }
+ return len;
+ }
+ throw Error("unsupported type for key");
+}
+
+function internalSerializeNumeric(
+ dv: DataView,
+ offset: number,
+ tag: number,
+ val: number,
+): number {
+ dv.setUint8(offset, tagNum);
+ dv.setFloat64(offset + 1, val);
+ // Flip sign bit
+ let b = dv.getUint8(offset + 1);
+ b ^= 0x80;
+ dv.setUint8(offset + 1, b);
+ return 9;
+}
+
+function internalSerializeArray(
+ dv: DataView,
+ offset: number,
+ key: any[],
+): number {
+ dv.setUint8(offset, tagArray);
+ let n = 1;
+ for (let i = 0; i < key.length; i++) {
+ n += internalSerializeKey(key[i], dv, offset + n);
+ }
+ dv.setUint8(offset + n, 0);
+ n++;
+ return n;
+}
+
+function internalSerializeBinary(
+ dv: DataView,
+ offset: number,
+ key: Uint8Array,
+): number {
+ dv.setUint8(offset, tagBinary);
+ let n = 1;
+ for (let i = 0; i < key.length; i++) {
+ n += internalSerializeKey(key[i], dv, offset + n);
+ }
+ dv.setUint8(offset + n, 0);
+ n++;
+ return n;
+}
+
+function internalSerializeKey(
+ key: IDBValidKey,
+ dv: DataView,
+ offset: number,
+): number {
+ if (typeof key === "number") {
+ return internalSerializeNumeric(dv, offset, tagNum, key);
+ }
+ if (key instanceof Date) {
+ return internalSerializeNumeric(dv, offset, tagDate, key.getDate());
+ }
+ if (typeof key === "string") {
+ return internalSerializeString(dv, offset, key);
+ }
+ if (Array.isArray(key)) {
+ return internalSerializeArray(dv, offset, key);
+ }
+ if (key instanceof ArrayBuffer) {
+ return internalSerializeBinary(dv, offset, new Uint8Array(key));
+ }
+ if (ArrayBuffer.isView(key)) {
+ const uv = new Uint8Array(key.buffer, key.byteOffset, key.byteLength);
+ return internalSerializeBinary(dv, offset, uv);
+ }
+ throw Error("unsupported type for key");
+}
+
+export function serializeKey(key: IDBValidKey): Uint8Array {
+ const len = countSerializeKey(key);
+ let buf = new Uint8Array(len);
+ const outLen = internalSerializeKey(key, new DataView(buf.buffer), 0);
+ if (len != outLen) {
+ throw Error("internal invariant failed");
+ }
+ let numTrailingZeroes = 0;
+ for (let i = buf.length - 1; i >= 0 && buf[i] == 0; i--, numTrailingZeroes++);
+ if (numTrailingZeroes > 0) {
+ buf = buf.slice(0, buf.length - numTrailingZeroes);
+ }
+ return buf;
+}
+
+function internalReadString(dv: DataView, offset: number): [number, string] {
+ const chars: string[] = [];
+ while (offset < dv.byteLength) {
+ const v = dv.getUint8(offset);
+ if (v == 0) {
+ // Got end-of-string.
+ offset += 1;
+ break;
+ }
+ let c: number;
+ if ((v & threeByteMask) === threeByteMask) {
+ const b1 = v;
+ const b2 = dv.getUint8(offset + 1);
+ const b3 = dv.getUint8(offset + 2);
+ c = (b1 << 10) | (b2 << 2) | (b3 >> 6);
+ offset += 3;
+ } else if ((v & twoByteMask) === twoByteMask) {
+ const b1 = v & ~twoByteMask;
+ const b2 = dv.getUint8(offset + 1);
+ c = ((b1 << 8) | b2) + twoByteOffset;
+ offset += 2;
+ } else {
+ c = v - oneByteOffset;
+ offset += 1;
+ }
+ chars.push(String.fromCharCode(c));
+ }
+ return [offset, chars.join("")];
+}
+
+function internalReadBytes(dv: DataView, offset: number): [number, Uint8Array] {
+ let count = 0;
+ while (offset + count < dv.byteLength) {
+ const v = dv.getUint8(offset + count);
+ if (v === 0) {
+ break;
+ }
+ count++;
+ }
+ let writePos = 0;
+ const bytes = new Uint8Array(count);
+ while (offset < dv.byteLength) {
+ const v = dv.getUint8(offset);
+ if (v == 0) {
+ offset += 1;
+ break;
+ }
+ let c: number;
+ if ((v & threeByteMask) === threeByteMask) {
+ const b1 = v;
+ const b2 = dv.getUint8(offset + 1);
+ const b3 = dv.getUint8(offset + 2);
+ c = (b1 << 10) | (b2 << 2) | (b3 >> 6);
+ offset += 3;
+ } else if ((v & twoByteMask) === twoByteMask) {
+ const b1 = v & ~twoByteMask;
+ const b2 = dv.getUint8(offset + 1);
+ c = ((b1 << 8) | b2) + twoByteOffset;
+ offset += 2;
+ } else {
+ c = v - oneByteOffset;
+ offset += 1;
+ }
+ bytes[writePos] = c;
+ writePos++;
+ }
+ return [offset, bytes];
+}
+
+/**
+ * Same as DataView.getFloat64, but logically pad input
+ * with zeroes on the right if read offset would be out
+ * of bounds.
+ *
+ * This allows reading from buffers where zeros have been
+ * truncated.
+ */
+function getFloat64Trunc(dv: DataView, offset: number): number {
+ if (offset + 7 >= dv.byteLength) {
+ const buf = new Uint8Array(8);
+ for (let i = offset; i < dv.byteLength; i++) {
+ buf[i - offset] = dv.getUint8(i);
+ }
+ const dv2 = new DataView(buf.buffer);
+ return dv2.getFloat64(0);
+ } else {
+ return dv.getFloat64(offset);
+ }
+}
+
+function internalDeserializeKey(
+ dv: DataView,
+ offset: number,
+): [number, IDBValidKey] {
+ let tag = dv.getUint8(offset);
+ switch (tag) {
+ case tagNum: {
+ const num = -getFloat64Trunc(dv, offset + 1);
+ const newOffset = Math.min(offset + 9, dv.byteLength);
+ return [newOffset, num];
+ }
+ case tagDate: {
+ const num = -getFloat64Trunc(dv, offset + 1);
+ const newOffset = Math.min(offset + 9, dv.byteLength);
+ return [newOffset, new Date(num)];
+ }
+ case tagString: {
+ return internalReadString(dv, offset + 1);
+ }
+ case tagBinary: {
+ return internalReadBytes(dv, offset + 1);
+ }
+ case tagArray: {
+ const arr: any[] = [];
+ offset += 1;
+ while (offset < dv.byteLength) {
+ const innerTag = dv.getUint8(offset);
+ if (innerTag === 0) {
+ offset++;
+ break;
+ }
+ const [innerOff, innerVal] = internalDeserializeKey(dv, offset);
+ arr.push(innerVal);
+ offset = innerOff;
+ }
+ return [offset, arr];
+ }
+ default:
+ throw Error("invalid key (unrecognized tag)");
+ }
+}
+
+export function deserializeKey(encodedKey: Uint8Array): IDBValidKey {
+ const dv = new DataView(
+ encodedKey.buffer,
+ encodedKey.byteOffset,
+ encodedKey.byteLength,
+ );
+ let [off, res] = internalDeserializeKey(dv, 0);
+ if (off != encodedKey.byteLength) {
+ throw Error("internal invariant failed");
+ }
+ return res;
+}
diff --git a/packages/idb-bridge/src/util/makeStoreKeyValue.test.ts b/packages/idb-bridge/src/util/makeStoreKeyValue.test.ts
index 971697021..c1216fe97 100644
--- a/packages/idb-bridge/src/util/makeStoreKeyValue.test.ts
+++ b/packages/idb-bridge/src/util/makeStoreKeyValue.test.ts
@@ -20,55 +20,73 @@ import { makeStoreKeyValue } from "./makeStoreKeyValue.js";
test("basics", (t) => {
let result;
- result = makeStoreKeyValue({ name: "Florian" }, undefined, 42, true, "id");
+ result = makeStoreKeyValue({
+ value: { name: "Florian" },
+ key: undefined,
+ currentKeyGenerator: 42,
+ autoIncrement: true,
+ keyPath: "id",
+ });
t.is(result.updatedKeyGenerator, 43);
t.is(result.key, 42);
t.is(result.value.name, "Florian");
t.is(result.value.id, 42);
- result = makeStoreKeyValue(
- { name: "Florian", id: 10 },
- undefined,
- 5,
- true,
- "id",
- );
+ result = makeStoreKeyValue({
+ value: { name: "Florian", id: 10 },
+ key: undefined,
+ currentKeyGenerator: 5,
+ autoIncrement: true,
+ keyPath: "id",
+ });
t.is(result.updatedKeyGenerator, 11);
t.is(result.key, 10);
t.is(result.value.name, "Florian");
t.is(result.value.id, 10);
- result = makeStoreKeyValue(
- { name: "Florian", id: 5 },
- undefined,
- 10,
- true,
- "id",
- );
+ result = makeStoreKeyValue({
+ value: { name: "Florian", id: 5 },
+ key: undefined,
+ currentKeyGenerator: 10,
+ autoIncrement: true,
+ keyPath: "id",
+ });
t.is(result.updatedKeyGenerator, 10);
t.is(result.key, 5);
t.is(result.value.name, "Florian");
t.is(result.value.id, 5);
- result = makeStoreKeyValue(
- { name: "Florian", id: "foo" },
- undefined,
- 10,
- true,
- "id",
- );
+ result = makeStoreKeyValue({
+ value: { name: "Florian", id: "foo" },
+ key: undefined,
+ currentKeyGenerator: 10,
+ autoIncrement: true,
+ keyPath: "id",
+ });
t.is(result.updatedKeyGenerator, 10);
t.is(result.key, "foo");
t.is(result.value.name, "Florian");
t.is(result.value.id, "foo");
- result = makeStoreKeyValue({ name: "Florian" }, "foo", 10, true, null);
+ result = makeStoreKeyValue({
+ value: { name: "Florian" },
+ key: "foo",
+ currentKeyGenerator: 10,
+ autoIncrement: true,
+ keyPath: null,
+ });
t.is(result.updatedKeyGenerator, 10);
t.is(result.key, "foo");
t.is(result.value.name, "Florian");
t.is(result.value.id, undefined);
- result = makeStoreKeyValue({ name: "Florian" }, undefined, 10, true, null);
+ result = makeStoreKeyValue({
+ value: { name: "Florian" },
+ key: undefined,
+ currentKeyGenerator: 10,
+ autoIncrement: true,
+ keyPath: null,
+ });
t.is(result.updatedKeyGenerator, 11);
t.is(result.key, 10);
t.is(result.value.name, "Florian");
diff --git a/packages/idb-bridge/src/util/makeStoreKeyValue.ts b/packages/idb-bridge/src/util/makeStoreKeyValue.ts
index 4c7dab8d2..153cd9d81 100644
--- a/packages/idb-bridge/src/util/makeStoreKeyValue.ts
+++ b/packages/idb-bridge/src/util/makeStoreKeyValue.ts
@@ -75,19 +75,25 @@ function injectKey(
return newValue;
}
-export function makeStoreKeyValue(
- value: any,
- key: IDBValidKey | undefined,
- currentKeyGenerator: number,
- autoIncrement: boolean,
- keyPath: IDBKeyPath | IDBKeyPath[] | null,
-): StoreKeyResult {
+export interface MakeStoreKvRequest {
+ value: any;
+ key: IDBValidKey | undefined;
+ currentKeyGenerator: number;
+ autoIncrement: boolean;
+ keyPath: IDBKeyPath | IDBKeyPath[] | null;
+}
+
+export function makeStoreKeyValue(req: MakeStoreKvRequest): StoreKeyResult {
+ const { keyPath, currentKeyGenerator, autoIncrement } = req;
+ let { key, value } = req;
+
const haveKey = key !== null && key !== undefined;
const haveKeyPath = keyPath !== null && keyPath !== undefined;
// This models a decision table on (haveKey, haveKeyPath, autoIncrement)
try {
+ // FIXME: Perf: only do this if we need to inject something.
value = structuredClone(value);
} catch (e) {
throw new DataCloneError();
diff --git a/packages/idb-bridge/src/util/queueTask.ts b/packages/idb-bridge/src/util/queueTask.ts
index 297602c67..f8a6e799f 100644
--- a/packages/idb-bridge/src/util/queueTask.ts
+++ b/packages/idb-bridge/src/util/queueTask.ts
@@ -14,6 +14,11 @@
permissions and limitations under the License.
*/
+/**
+ * Queue a task to be executed *after* the microtask
+ * queue has been processed, but *before* subsequent setTimeout / setImmediate
+ * tasks.
+ */
export function queueTask(fn: () => void) {
let called = false;
const callFirst = () => {
diff --git a/packages/idb-bridge/src/util/structuredClone.test.ts b/packages/idb-bridge/src/util/structuredClone.test.ts
index 0c613e6cc..e13d4117f 100644
--- a/packages/idb-bridge/src/util/structuredClone.test.ts
+++ b/packages/idb-bridge/src/util/structuredClone.test.ts
@@ -15,7 +15,11 @@
*/
import test, { ExecutionContext } from "ava";
-import { structuredClone } from "./structuredClone.js";
+import {
+ structuredClone,
+ structuredEncapsulate,
+ structuredRevive,
+} from "./structuredClone.js";
function checkClone(t: ExecutionContext, x: any): void {
t.deepEqual(structuredClone(x), x);
@@ -59,3 +63,58 @@ test("structured clone (object cycles)", (t) => {
const obj1Clone = structuredClone(obj1);
t.is(obj1Clone, obj1Clone.c);
});
+
+test("encapsulate", (t) => {
+ t.deepEqual(structuredEncapsulate(42), 42);
+ t.deepEqual(structuredEncapsulate(true), true);
+ t.deepEqual(structuredEncapsulate(false), false);
+ t.deepEqual(structuredEncapsulate(null), null);
+
+ t.deepEqual(structuredEncapsulate(undefined), { $: "undef" });
+ t.deepEqual(structuredEncapsulate(42n), { $: "bigint", val: "42" });
+
+ t.deepEqual(structuredEncapsulate(new Date(42)), { $: "date", val: 42 });
+
+ t.deepEqual(structuredEncapsulate({ x: 42 }), { x: 42 });
+
+ t.deepEqual(structuredEncapsulate({ $: "bla", x: 42 }), {
+ $: "obj",
+ val: { $: "bla", x: 42 },
+ });
+
+ const x = { foo: 42, bar: {} } as any;
+ x.bar.baz = x;
+
+ t.deepEqual(structuredEncapsulate(x), {
+ foo: 42,
+ bar: {
+ baz: { $: "ref", d: 2, p: [] },
+ },
+ });
+});
+
+test("revive", (t) => {
+ t.deepEqual(structuredRevive(42), 42);
+ t.deepEqual(structuredRevive([1, 2, 3]), [1, 2, 3]);
+ t.deepEqual(structuredRevive(true), true);
+ t.deepEqual(structuredRevive(false), false);
+ t.deepEqual(structuredRevive(null), null);
+ t.deepEqual(structuredRevive({ $: "undef" }), undefined);
+ t.deepEqual(structuredRevive({ x: { $: "undef" } }), { x: undefined });
+
+ t.deepEqual(structuredRevive({ $: "date", val: 42}), new Date(42));
+
+ {
+ const x = { foo: 42, bar: {} } as any;
+ x.bar.baz = x;
+
+ const r = {
+ foo: 42,
+ bar: {
+ baz: { $: "ref", d: 2, p: [] },
+ },
+ };
+
+ t.deepEqual(structuredRevive(r), x);
+ }
+});
diff --git a/packages/idb-bridge/src/util/structuredClone.ts b/packages/idb-bridge/src/util/structuredClone.ts
index c33dc5e36..2f857c6c5 100644
--- a/packages/idb-bridge/src/util/structuredClone.ts
+++ b/packages/idb-bridge/src/util/structuredClone.ts
@@ -14,6 +14,28 @@
permissions and limitations under the License.
*/
+/**
+ * Encoding (new, compositional version):
+ *
+ * Encapsulate object that itself might contain a "$" field:
+ * { $: "obj", val: ... }
+ * (Outer level only:) Wrap other values into object
+ * { $: "lit", val: ... }
+ * Circular reference:
+ * { $: "ref" l: uplevel, p: path }
+ * Date:
+ * { $: "date", val: datestr }
+ * Bigint:
+ * { $: "bigint", val: bigintstr }
+ * Array with special (non-number) attributes:
+ * { $: "array", val: arrayobj }
+ * Undefined field
+ * { $: "undef" }
+ */
+
+/**
+ * Imports.
+ */
import { DataCloneError } from "./errors.js";
const { toString: toStr } = {};
@@ -73,10 +95,6 @@ function isUserObject(val: any): boolean {
return hasConstructorOf(val, Object) || isUserObject(proto);
}
-function isRegExp(val: any): boolean {
- return toStringTag(val) === "RegExp";
-}
-
function copyBuffer(cur: any) {
if (cur instanceof Buffer) {
return Buffer.from(cur);
@@ -242,22 +260,18 @@ export function mkDeepCloneCheckOnly() {
function internalEncapsulate(
val: any,
- outRoot: any,
path: string[],
memo: Map<any, string[]>,
- types: Array<[string[], string]>,
): any {
const memoPath = memo.get(val);
if (memoPath) {
- types.push([path, "ref"]);
- return memoPath;
+ return { $: "ref", d: path.length, p: memoPath };
}
if (val === null) {
return null;
}
if (val === undefined) {
- types.push([path, "undef"]);
- return 0;
+ return { $: "undef" };
}
if (Array.isArray(val)) {
memo.set(val, path);
@@ -270,31 +284,33 @@ function internalEncapsulate(
break;
}
}
- if (special) {
- types.push([path, "array"]);
- }
for (const x in val) {
const p = [...path, x];
- outArr[x] = internalEncapsulate(val[x], outRoot, p, memo, types);
+ outArr[x] = internalEncapsulate(val[x], p, memo);
+ }
+ if (special) {
+ return { $: "array", val: outArr };
+ } else {
+ return outArr;
}
- return outArr;
}
if (val instanceof Date) {
- types.push([path, "date"]);
- return val.getTime();
+ return { $: "date", val: val.getTime() };
}
if (isUserObject(val) || isPlainObject(val)) {
memo.set(val, path);
const outObj: any = {};
for (const x in val) {
const p = [...path, x];
- outObj[x] = internalEncapsulate(val[x], outRoot, p, memo, types);
+ outObj[x] = internalEncapsulate(val[x], p, memo);
+ }
+ if ("$" in outObj) {
+ return { $: "obj", val: outObj };
}
return outObj;
}
if (typeof val === "bigint") {
- types.push([path, "bigint"]);
- return val.toString();
+ return { $: "bigint", val: val.toString() };
}
if (typeof val === "boolean") {
return val;
@@ -308,123 +324,114 @@ function internalEncapsulate(
throw Error();
}
-/**
- * Encapsulate a cloneable value into a plain JSON object.
- */
-export function structuredEncapsulate(val: any): any {
- const outRoot = {};
- const types: Array<[string[], string]> = [];
- let res;
- res = internalEncapsulate(val, outRoot, [], new Map(), types);
- if (res === null) {
- return res;
- }
- // We need to further encapsulate the outer layer
- if (
- Array.isArray(res) ||
- typeof res !== "object" ||
- "$" in res ||
- "$types" in res
- ) {
- res = { $: res };
- }
- if (types.length > 0) {
- res["$types"] = types;
- }
- return res;
+function derefPath(
+ root: any,
+ p1: Array<string | number>,
+ n: number,
+ p2: Array<string | number>,
+): any {
+ let v = root;
+ for (let i = 0; i < n; i++) {
+ v = v[p1[i]];
+ }
+ for (let i = 0; i < p2.length; i++) {
+ v = v[p2[i]];
+ }
+ return v;
}
-export function internalStructuredRevive(val: any): any {
- val = JSON.parse(JSON.stringify(val));
- if (val === null) {
- return null;
+function internalReviveArray(sval: any, root: any, path: string[]): any {
+ const newArr: any[] = [];
+ if (root === undefined) {
+ root = newArr;
}
- if (typeof val === "number") {
- return val;
+ for (let i = 0; i < sval.length; i++) {
+ const p = [...path, String(i)];
+ newArr.push(internalStructuredRevive(sval[i], root, p));
}
- if (typeof val === "string") {
- return val;
+ return newArr;
+}
+
+function internalReviveObject(sval: any, root: any, path: string[]): any {
+ const newObj = {} as any;
+ if (root === undefined) {
+ root = newObj;
}
- if (typeof val === "boolean") {
- return val;
+ for (const key of Object.keys(sval)) {
+ const p = [...path, key];
+ newObj[key] = internalStructuredRevive(sval[key], root, p);
}
- if (!isPlainObject(val)) {
- throw Error();
- }
- let types = val.$types ?? [];
- delete val.$types;
- let outRoot: any;
- if ("$" in val) {
- outRoot = val.$;
- } else {
- outRoot = val;
- }
- function mutatePath(path: string[], f: (x: any) => any): void {
- if (path.length == 0) {
- outRoot = f(outRoot);
- return;
- }
- let obj = outRoot;
- for (let i = 0; i < path.length - 1; i++) {
- const n = path[i];
- if (!(n in obj)) {
- obj[n] = {};
- }
- obj = obj[n];
- }
- const last = path[path.length - 1];
- obj[last] = f(obj[last]);
+ return newObj;
+}
+
+function internalStructuredRevive(sval: any, root: any, path: string[]): any {
+ if (typeof sval === "string") {
+ return sval;
}
- function lookupPath(path: string[]): any {
- let obj = outRoot;
- for (const n of path) {
- obj = obj[n];
- }
- return obj;
+ if (typeof sval === "number") {
+ return sval;
}
- for (const [path, type] of types) {
- switch (type) {
- case "bigint": {
- mutatePath(path, (x) => BigInt(x));
- break;
- }
- case "array": {
- mutatePath(path, (x) => {
- const newArr: any = [];
- for (const k in x) {
- newArr[k] = x[k];
- }
- return newArr;
- });
- break;
- }
- case "date": {
- mutatePath(path, (x) => new Date(x));
- break;
- }
- case "undef": {
- mutatePath(path, (x) => undefined);
- break;
- }
- case "ref": {
- mutatePath(path, (x) => lookupPath(x));
- break;
+ if (typeof sval === "boolean") {
+ return sval;
+ }
+ if (sval === null) {
+ return null;
+ }
+ if (Array.isArray(sval)) {
+ return internalReviveArray(sval, root, path);
+ }
+
+ if (isUserObject(sval) || isPlainObject(sval)) {
+ if ("$" in sval) {
+ const dollar = sval.$;
+ switch (dollar) {
+ case "undef":
+ return undefined;
+ case "bigint":
+ return BigInt((sval as any).val);
+ case "date":
+ return new Date((sval as any).val);
+ case "obj": {
+ return internalReviveObject((sval as any).val, root, path);
+ }
+ case "array":
+ return internalReviveArray((sval as any).val, root, path);
+ case "ref": {
+ const level = (sval as any).l;
+ const p2 = (sval as any).p;
+ return derefPath(root, path, path.length - level, p2);
+ }
+ default:
+ throw Error();
}
- default:
- throw Error(`type '${type}' not implemented`);
+ } else {
+ return internalReviveObject(sval, root, path);
}
}
- return outRoot;
+
+ throw Error();
}
-export function structuredRevive(val: any): any {
- return internalStructuredRevive(val);
+/**
+ * Encapsulate a cloneable value into a plain JSON value.
+ */
+export function structuredEncapsulate(val: any): any {
+ return internalEncapsulate(val, [], new Map());
+}
+
+export function structuredRevive(sval: any): any {
+ return internalStructuredRevive(sval, undefined, []);
}
/**
* Structured clone for IndexedDB.
*/
export function structuredClone(val: any): any {
+ // @ts-ignore
+ if (globalThis._tart?.structuredClone) {
+ // @ts-ignore
+ return globalThis._tart?.structuredClone(val);
+ }
return mkDeepClone()(val);
}
@@ -432,5 +439,11 @@ export function structuredClone(val: any): any {
* Structured clone for IndexedDB.
*/
export function checkStructuredCloneOrThrow(val: any): void {
- return mkDeepCloneCheckOnly()(val);
+ // @ts-ignore
+ if (globalThis._tart?.structuredClone) {
+ // @ts-ignore
+ globalThis._tart?.structuredClone(val);
+ return;
+ }
+ mkDeepCloneCheckOnly()(val);
}
diff --git a/packages/idb-bridge/src/util/valueToKey.ts b/packages/idb-bridge/src/util/valueToKey.ts
index 6df82af81..0cd824689 100644
--- a/packages/idb-bridge/src/util/valueToKey.ts
+++ b/packages/idb-bridge/src/util/valueToKey.ts
@@ -17,7 +17,11 @@
import { IDBValidKey } from "../idbtypes.js";
import { DataError } from "./errors.js";
-// https://www.w3.org/TR/IndexedDB-2/#convert-a-value-to-a-key
+/**
+ * Algorithm to "convert a value to a key".
+ *
+ * https://www.w3.org/TR/IndexedDB/#convert-value-to-key
+ */
export function valueToKey(
input: any,
seen?: Set<object>,
diff --git a/packages/idb-bridge/tsconfig.json b/packages/idb-bridge/tsconfig.json
index b0a6808f4..44a27284b 100644
--- a/packages/idb-bridge/tsconfig.json
+++ b/packages/idb-bridge/tsconfig.json
@@ -1,10 +1,10 @@
{
"compilerOptions": {
"composite": true,
- "lib": ["es6"],
- "module": "ES2020",
+ "lib": ["ES2020"],
+ "module": "Node16",
"moduleResolution": "Node16",
- "target": "ES6",
+ "target": "ES2020",
"allowJs": true,
"noImplicitAny": true,
"outDir": "lib",
diff --git a/packages/merchant-backend-ui/.storybook/main.js b/packages/merchant-backend-ui/.storybook/main.js
deleted file mode 100644
index 5497a6510..000000000
--- a/packages/merchant-backend-ui/.storybook/main.js
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-
-module.exports = {
- "stories": [
- "../src/**/*.stories.mdx",
- "../src/**/*.stories.@(js|jsx|ts|tsx)"
- ],
- "addons": [
- "@storybook/preset-scss",
- "@storybook/addon-a11y",
- "@storybook/addon-essentials" //docs, control, actions, viewpot, toolbar, background
- ],
- // sb does not yet support new jsx transform by default
- // https://github.com/storybookjs/storybook/issues/12881
- // https://github.com/storybookjs/storybook/issues/12952
- babel: async (options) => ({
- ...options,
- presets: [
- ...options.presets,
- [
- '@babel/preset-react', {
- runtime: 'automatic',
- },
- 'preset-react-jsx-transform'
- ],
- "@linaria",
- ],
- }),
- webpackFinal: (config) => {
- // should be removed after storybook 6.3
- // https://github.com/storybookjs/storybook/issues/12853#issuecomment-821576113
- config.resolve.alias = {
- react: "preact/compat",
- "react-dom": "preact/compat",
- };
-
- // we need to add @linaria loader AFTER the babel-loader
- // https://github.com/callstack/linaria/blob/master/docs/BUNDLERS_INTEGRATION.md#webpack
- config.module.rules[0] = {
- ...(config.module.rules[0]),
- loader: undefined, // Disable the predefined babel-loader on the rule
- use: [
- {
- ...(config.module.rules[0].use[0]),
- loader: 'babel-loader',
- },
- {
- loader: '@linaria/webpack-loader',
- options: {
- sourceMap: true, //always true since this is dev
- babelOptions: {
- presets: config.module.rules[0].use[0].options.presets,
- }
- // Pass the current babel options to linaria's babel instance
- }
- }
- ]
- };
-
- return config;
- },
-} \ No newline at end of file
diff --git a/packages/merchant-backend-ui/.storybook/preview.js b/packages/merchant-backend-ui/.storybook/preview.js
deleted file mode 100644
index a9cc4c39a..000000000
--- a/packages/merchant-backend-ui/.storybook/preview.js
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- 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/>
- */
-
-import { ConfigContextProvider } from '../src/context/config'
-import { InstanceContextProvider } from '../src/context/instance'
-import { TranslationProvider } from '../src/context/translation'
-import { BackendContextProvider } from '../src/context/backend'
-import { h } from 'preact';
-
-const mockConfig = {
- backendURL: 'http://demo.taler.net',
- currency: 'TESTKUDOS'
-}
-
-const mockInstance = {
- id: 'instance-id',
- token: 'instance-token',
- admin: false,
-}
-
-const mockBackend = {
- url: 'http://merchant.url',
- token: 'default-token',
- triedToLog: false,
-}
-
-export const parameters = {
- controls: { expanded: true },
- // actions: { argTypesRegex: "^on.*" },
-}
-
-export const globalTypes = {
- locale: {
- name: 'Locale',
- description: 'Internationalization locale',
- defaultValue: 'en',
- toolbar: {
- icon: 'globe',
- items: [
- { value: 'en', right: '🇺🇸', title: 'English' },
- { value: 'es', right: '🇪🇸', title: 'Spanish' },
- ],
- },
- },
-};
-
-export const decorators = [
- (Story, { globals }) => <TranslationProvider initial='en' forceLang={globals.locale}>
- <Story />
- </TranslationProvider>,
- (Story) => <ConfigContextProvider value={mockConfig}>
- <Story />
- </ConfigContextProvider>,
- (Story) => <InstanceContextProvider value={mockInstance}>
- <Story />
- </InstanceContextProvider>,
- (Story) => <BackendContextProvider defaultUrl={mockBackend.url}>
- <Story />
- </BackendContextProvider>,
-];
diff --git a/packages/merchant-backend-ui/README.md b/packages/merchant-backend-ui/README.md
index 44a555ae0..7f9bcf5dc 100644
--- a/packages/merchant-backend-ui/README.md
+++ b/packages/merchant-backend-ui/README.md
@@ -4,11 +4,9 @@ Merchant Backend pages
This project generate 5 templates for the merchant backend:
- * DepletedTip
- * OfferRefund
- * OfferTip
- * RequestPayment
- * ShowOrderDetails
+- OfferRefund
+- RequestPayment
+- ShowOrderDetails
This pages are to be serve from the merchant-backend service and will be queried for browser that may or may not have JavaScript enabled, so we are going to do server side rendering.
The merchant-backend service is currently supporting mustache library for server side rendering.
diff --git a/packages/taler-wallet-webextension/babel.config-linaria.json b/packages/merchant-backend-ui/babel.config-linaria.json
index a5cf7ba9e..6192b62fe 100644
--- a/packages/taler-wallet-webextension/babel.config-linaria.json
+++ b/packages/merchant-backend-ui/babel.config-linaria.json
@@ -23,7 +23,5 @@
* This file should be used from @linaria/rollup plugin only
*/
{
- "presets": [
- "preact-cli/babel",
- ]
-} \ No newline at end of file
+ "plugins": ["./trim-extension.cjs"],
+}
diff --git a/packages/demobank-ui/build.mjs b/packages/merchant-backend-ui/build.mjs
index c93b4eb67..e72113dc5 100755
--- a/packages/demobank-ui/build.mjs
+++ b/packages/merchant-backend-ui/build.mjs
@@ -1,7 +1,7 @@
#!/usr/bin/env node
/*
This file is part of GNU Taler
- (C) 2022 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
@@ -18,7 +18,7 @@
import esbuild from "esbuild";
import path from "path";
import fs from "fs";
-import sass from "sass";
+import linaria from '@linaria/esbuild'
// eslint-disable-next-line no-undef
const BASE = process.cwd();
@@ -44,7 +44,8 @@ const preactCompatPlugin = {
},
};
-const entryPoints = ["src/index.tsx", "src/stories.tsx"];
+const pages = ["OfferRefund", "RequestPayment", "ShowOrderDetails"]
+const entryPoints = pages.map(p => `src/pages/${p}.tsx`);
let GIT_ROOT = BASE;
while (!fs.existsSync(path.join(GIT_ROOT, ".git")) && GIT_ROOT !== "/") {
@@ -73,45 +74,57 @@ function git_hash() {
return fs.readFileSync(path.join(GIT_ROOT, ".git", rev)).toString().trim();
}
}
+function toCamelCaseName(name) {
+ return name
+ .replace(/^[A-Z]/, letter => `${letter.toLowerCase()}`) //first letter lowercase
+ .replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`) //snake case
+ .concat(".en.html"); //extension
+}
-// FIXME: Put this into some helper library.
-function copyFilesPlugin(options) {
+function templatePlugin(options) {
return {
- name: "copy-files",
+ name: "template-backend",
setup(build) {
build.onEnd(() => {
- for (const fop of options) {
- fs.copyFileSync(fop.src, fop.dest);
+ for (const pageName of options.pages) {
+ const css = fs.readFileSync(path.join(build.initialOptions.outdir, `${pageName}.css`), "utf8").toString()
+ const js = fs.readFileSync(path.join(build.initialOptions.outdir, `${pageName}.js`), "utf8").toString()
+ const location = path.join(build.initialOptions.outdir, toCamelCaseName(pageName))
+ const render = new Function(`${js}; return page.buildTimeRendering();`)()
+ const html = `
+ <!doctype html>
+ <html>
+ <head>
+ ${render.head}
+ <style>${css}</style>
+ </head>
+ <script id="built_time_data">
+ </script>
+ <body>
+ ${render.body}
+ <script>${js}</script>
+ <script>page.mount()</script>
+ </body>
+ </html>`
+ fs.writeFileSync(location, html);
}
});
},
};
}
-const DEFAULT_SASS_FILTER = /\.(s[ac]ss|css)$/
-
-const buildSassPlugin = {
- name: "custom-build-sass",
- setup(build) {
-
- build.onLoad({ filter: DEFAULT_SASS_FILTER }, ({ path: file }) => {
- const resolveDir = path.dirname(file)
- const { css: contents } = sass.compile(file, { loadPaths: ["./"] })
-
- return {
- resolveDir,
- loader: 'css',
- contents
- }
- });
-
- },
-};
-
export const buildConfig = {
entryPoints: [...entryPoints],
bundle: true,
- outdir: "dist",
+ outdir: "dist/pages",
+ /*
+ * Doing a minified version will replace templatestring to common strings
+ * This app is building mustache template with placeholders that will be replaced
+ * with string in runtime by the merchant-backend
+ *
+ * To the date, merchant backend is replacing with multiline string so
+ * doing minified version will brake at runtime
+ * */
minify: false,
loader: {
".svg": "file",
@@ -122,10 +135,11 @@ export const buildConfig = {
'.woff2': 'file',
'.eot': 'file',
},
- target: ["es6"],
- format: "esm",
+ target: ["es2020"],
+ format: "iife",
platform: "browser",
- sourcemap: true,
+ sourcemap: false,
+ globalName: "page",
jsxFactory: "h",
jsxFragment: "Fragment",
define: {
@@ -133,15 +147,44 @@ export const buildConfig = {
__GIT_HASH__: `"${GIT_HASH}"`,
},
plugins: [
- preactCompatPlugin,
- copyFilesPlugin([
- {
- src: "./src/index.html",
- dest: "./dist/index.html",
+ linaria.default({
+ babelOptions: {
+ babelrc: false,
+ configFile: './babel.config-linaria.json',
},
- ]),
- buildSassPlugin
+ sourceMap: true,
+ }),
+ preactCompatPlugin,
+ templatePlugin({ pages })
],
};
await esbuild.build(buildConfig)
+
+export const testingConfig = {
+ entryPoints: ["src/render-examples.ts"],
+ bundle: true,
+ outdir: "dist/test",
+ minify: false,
+ loader: {
+ ".svg": "file",
+ ".png": "dataurl",
+ ".jpeg": "dataurl",
+ '.ttf': 'file',
+ '.woff': 'file',
+ '.woff2': 'file',
+ '.eot': 'file',
+ },
+ target: ["es2020"],
+ format: "iife",
+ platform: "node",
+ sourcemap: true,
+ define: {
+ __VERSION__: `"${_package.version}"`,
+ __GIT_HASH__: `"${GIT_HASH}"`,
+ },
+ plugins: [
+ ],
+};
+
+await esbuild.build(testingConfig)
diff --git a/packages/merchant-backend-ui/package.json b/packages/merchant-backend-ui/package.json
index c6ef3a413..bd16317f5 100644
--- a/packages/merchant-backend-ui/package.json
+++ b/packages/merchant-backend-ui/package.json
@@ -1,21 +1,16 @@
{
"private": true,
- "name": "merchant-backend",
- "version": "0.0.4",
- "license": "MIT",
+ "name": "@gnu-taler/merchant-backend-ui",
+ "version": "0.10.7",
+ "license": "AGPL-3.0-or-later",
"scripts": {
- "build": "rollup -c",
- "dev": "rollup -c -w",
- "render-examples": "ts-node -O '{\"module\": \"commonjs\"}' -T render-examples.ts dist/pages dist/examples",
+ "compile": "tsc && ./build.mjs",
+ "build": "pnpm compile",
+ "render-examples": "node dist/test/render-examples.js dist/pages dist/examples",
"lint-check": "eslint '{src,tests}/**/*.{js,jsx,ts,tsx}'",
"lint-fix": "eslint --fix '{src,tests}/**/*.{js,jsx,ts,tsx}'",
- "test": "jest ./tests",
- "dev-test": "jest ./tests --watch",
- "typedoc": "typedoc src",
- "clean": "rimraf build storybook-static docs single dist",
- "serve-dist": "sirv --port ${PORT:=8080} --cors --single dist",
- "build-storybook": "build-storybook",
- "storybook": "start-storybook -p 6006"
+ "clean": "rm -rf dist lib tsconfig.tsbuildinfo",
+ "serve-dist": "sirv --port ${PORT:=8080} --cors --single dist"
},
"engines": {
"node": ">=12",
@@ -41,104 +36,34 @@
]
},
"dependencies": {
- "@gnu-taler/taler-util": "workspace:*",
- "axios": "^0.21.1",
"date-fns": "^2.21.1",
- "history": "4.10.1",
- "jed": "^1.1.1",
- "preact": "^10.5.13",
- "preact-router": "^3.2.1",
- "qrcode-generator": "^1.4.4",
- "swr": "^0.5.5",
- "yup": "^0.32.9"
+ "preact": "10.11.3",
+ "qrcode-generator": "^1.4.4"
},
"devDependencies": {
"@babel/core": "7.18.9",
- "@babel/plugin-transform-react-jsx-source": "7.18.6",
- "@creativebulma/bulma-tooltip": "^1.2.0",
- "@gnu-taler/pogen": "^0.0.5",
+ "@gnu-taler/pogen": "workspace:*",
"@linaria/babel-preset": "3.0.0-beta.22",
"@linaria/core": "3.0.0-beta.22",
"@linaria/react": "3.0.0-beta.22",
- "@linaria/rollup": "3.0.0-beta.22",
"@linaria/shaker": "3.0.0-beta.22",
"@linaria/webpack-loader": "3.0.0-beta.22",
- "@rollup/plugin-alias": "^3.1.5",
- "@rollup/plugin-babel": "^5.3.0",
- "@rollup/plugin-commonjs": "^20.0.0",
- "@rollup/plugin-html": "^0.2.3",
- "@rollup/plugin-image": "^2.1.1",
- "@rollup/plugin-json": "^4.1.0",
- "@rollup/plugin-replace": "^3.0.0",
- "@rollup/plugin-typescript": "^8.2.5",
- "@storybook/addon-a11y": "^6.2.9",
- "@storybook/addon-actions": "^6.2.9",
- "@storybook/addon-essentials": "^6.2.9",
- "@storybook/addon-links": "^6.2.9",
- "@storybook/preact": "^6.2.9",
- "@storybook/preset-scss": "^1.0.3",
- "@testing-library/preact": "^2.0.1",
- "@testing-library/preact-hooks": "^1.1.0",
- "@types/enzyme": "^3.10.8",
- "@types/history": "^4.7.8",
- "@types/jest": "^26.0.23",
"@types/mocha": "^8.2.2",
"@types/mustache": "^4.1.2",
+ "@types/node": "^20.11.13",
"@typescript-eslint/eslint-plugin": "^4.22.0",
"@typescript-eslint/parser": "^4.22.0",
"babel-loader": "^8.2.2",
"base64-inline-loader": "^1.1.1",
- "bulma": "^0.9.2",
- "bulma-checkbox": "^1.1.1",
- "bulma-radio": "^1.1.1",
- "bulma-responsive-tables": "^1.2.3",
- "bulma-switch-control": "^1.1.1",
- "bulma-timeline": "^3.0.4",
- "bulma-upload-control": "^1.2.0",
- "dotenv": "^8.2.0",
- "enzyme": "^3.11.0",
- "enzyme-adapter-preact-pure": "^3.1.0",
"eslint": "^7.25.0",
"eslint-config-preact": "^1.1.4",
"eslint-plugin-header": "^3.1.1",
- "html-webpack-inline-chunk-plugin": "^1.1.1",
- "html-webpack-inline-source-plugin": "0.0.10",
- "html-webpack-skip-assets-plugin": "^1.0.1",
- "inline-chunk-html-plugin": "^1.1.1",
- "jest": "^26.6.3",
- "jest-preset-preact": "^4.0.2",
"mustache": "^4.2.0",
"po2json": "^0.4.5",
- "preact-cli": "^3.0.5",
- "preact-render-to-json": "^3.6.6",
"preact-render-to-string": "^5.1.19",
- "rimraf": "^3.0.2",
- "rollup": "^2.56.3",
- "rollup-plugin-bundle-html": "^0.2.2",
- "rollup-plugin-css-only": "^3.1.0",
- "sass": "^1.32.13",
- "sass-loader": "10.1.1",
- "script-ext-html-webpack-plugin": "^2.1.5",
"sirv-cli": "^1.0.11",
- "tslib": "^2.3.1",
- "typedoc": "^0.20.36",
- "typescript": "^4.2.4"
- },
- "jest": {
- "preset": "jest-preset-preact",
- "transformIgnorePatterns": [
- "node_modules/.pnpm/(?!(@gnu-taler\\+taler-util))",
- "\\.pnp\\.[^\\/]+$"
- ],
- "setupFiles": [
- "<rootDir>/tests/__mocks__/browserMocks.ts",
- "<rootDir>/tests/__mocks__/setupTests.ts"
- ],
- "moduleNameMapper": {
- "\\.(css|less)$": "identity-obj-proxy"
- },
- "transform": {
- "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga|po)$": "<rootDir>/tests/__mocks__/fileTransformer.js"
- }
+ "ts-node": "^10.9.1",
+ "tslib": "2.6.2",
+ "typescript": "5.3.3"
}
}
diff --git a/packages/merchant-backend-ui/render-examples.ts b/packages/merchant-backend-ui/render-examples.ts
deleted file mode 100644
index 47300ab8f..000000000
--- a/packages/merchant-backend-ui/render-examples.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import mustache from "mustache";
-import fs from "fs";
-import { format, formatDuration, intervalToDuration } from "date-fns";
-
-/**
- * This script will emulate what the merchant backend will do when being requested
- *
-*/
-
-const sourceDirectory = process.argv[2]
-const destDirectory = process.argv[3]
-
-if (!sourceDirectory || !destDirectory) {
- console.log('usage: render-mustache <source-directory> <dest-directory>')
- process.exit(1);
-}
-
-if (!fs.existsSync(destDirectory)) {
- fs.mkdirSync(destDirectory);
-}
-
-/**
- * Load all the html files
- */
-const files = fs.readdirSync(sourceDirectory).filter(f => /.html/.test(f))
-
-files.forEach(file => {
- const html = fs.readFileSync(`${sourceDirectory}/${file}`, 'utf8')
-
- const testName = file.replace('.html', '')
- if (testName !== 'ShowOrderDetails') return;
- // eslint-disable-next-line @typescript-eslint/no-var-requires
- const { exampleData } = require(`./src/pages/${testName}.examples`)
-
- Object.keys(exampleData).forEach(exampleName => {
- const example = exampleData[exampleName]
-
- //enhance the example with more information
- example.contract_terms_json = () => JSON.stringify(example.contract_terms);
- example.contract_terms.timestamp_str = () => example.contract_terms.timestamp && format(example.contract_terms.timestamp.t_s, 'dd MMM yyyy HH:mm:ss');
-
- example.contract_terms.hasProducts = () => example.contract_terms.products?.length > 0;
- example.contract_terms.hasAuditors = () => example.contract_terms.auditors?.length > 0;
- example.contract_terms.hasExchanges = () => example.contract_terms.exchanges?.length > 0;
-
- example.contract_terms.products.forEach(p => {
- p.delivery_date_str = () => p.delivery_date && format(p.delivery_date.t_s, 'dd MM yyyy HH:mm:ss')
- p.hasTaxes = () => p.taxes?.length > 0
- })
- example.contract_terms.has_delivery_info = () => example.contract_terms.delivery_date || example.contract_terms.delivery_location
-
- example.contract_terms.delivery_date_str = () => example.contract_terms.delivery_date && format(example.contract_terms.delivery_date.t_s, 'dd MM yyyy HH:mm:ss')
- example.contract_terms.pay_deadline_str = () => example.contract_terms.pay_deadline && format(example.contract_terms.pay_deadline.t_s, 'dd MM yyyy HH:mm:ss')
- example.contract_terms.wire_transfer_deadline_str = () => example.contract_terms.wire_transfer_deadline && format(example.contract_terms.wire_transfer_deadline.t_s, 'dd MM yyyy HH:mm:ss')
- example.contract_terms.refund_deadline_str = () => example.contract_terms.refund_deadline && format(example.contract_terms.refund_deadline.t_s, 'dd MM yyyy HH:mm:ss')
- example.contract_terms.auto_refund_str = () => example.contract_terms.auto_refund && formatDuration(intervalToDuration({ start: 0, end: example.contract_terms.auto_refund.d_us }))
-
- const output = mustache.render(html, example);
-
- fs.writeFileSync(`${destDirectory}/${testName}.${exampleName}.html`, output)
- })
-})
diff --git a/packages/merchant-backend-ui/rollup.config.js b/packages/merchant-backend-ui/rollup.config.js
deleted file mode 100644
index f5227ba74..000000000
--- a/packages/merchant-backend-ui/rollup.config.js
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- 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/>
- */
-
-// rollup.config.js
-import linaria from '@linaria/rollup';
-import nodeResolve from "@rollup/plugin-node-resolve";
-import alias from "@rollup/plugin-alias";
-import image from '@rollup/plugin-image';
-import json from "@rollup/plugin-json";
-import ts from "@rollup/plugin-typescript";
-import replace from "@rollup/plugin-replace";
-import css from 'rollup-plugin-css-only';
-import html from '@rollup/plugin-html';
-import commonjs from "@rollup/plugin-commonjs";
-
-
-
-const template = async ({
- files,
-}) => {
- const scripts = (files.js || []).map(({ code }) => `<script>${code}</script>`).join('\n');
- const css = (files.css || []).map(({ source }) => `<style>${source}</style>`).join('\n');
- const ssr = (files.js || []).map(({ code }) => code).join('\n');
- const page = new Function(`${ssr}; return page.buildTimeRendering();`)()
- return `
-<!doctype html>
-<html>
- <head>
- ${page.head}
- ${css}
- </head>
- <script id="built_time_data">
- </script>
- <body>
- ${page.body}
- ${scripts}
- <script>page.mount()</script>
- </body>
-</html>`;
-};
-
-const makePlugins = (name) => [
- alias({
- entries: [
- { find: 'react', replacement: 'preact/compat' },
- { find: 'react-dom', replacement: 'preact/compat' }
- ]
- }),
-
- replace({
- "process.env.NODE_ENV": JSON.stringify("production"),
- preventAssignment: true,
- }),
-
- commonjs({
- include: [/node_modules/, /dist/],
- extensions: [".js"],
- ignoreGlobal: true,
- sourceMap: true,
- }),
-
- nodeResolve({
- browser: true,
- preferBuiltins: true,
- }),
-
- json(),
- image(),
-
- linaria({
- sourceMap: process.env.NODE_ENV !== 'production',
- }),
- css(),
- ts({
- sourceMap: false,
- outputToFilesystem: false,
- }),
- html({ template, fileName: name }),
-];
-
-
-const pageDefinition = (name) => ({
- input: `src/pages/${name}.tsx`,
- output: {
- file: `dist/pages/${name}.js`,
- format: "iife",
- exports: 'named',
- name: 'page',
- },
- plugins: makePlugins(`${name}.html`),
-});
-
-export default [
- pageDefinition("OfferTip"),
- pageDefinition("OfferRefund"),
- pageDefinition("DepletedTip"),
- pageDefinition("RequestPayment"),
- pageDefinition("ShowOrderDetails"),
-]
diff --git a/packages/merchant-backend-ui/src/components/Footer.tsx b/packages/merchant-backend-ui/src/components/Footer.tsx
index 5f2957800..278e4a543 100644
--- a/packages/merchant-backend-ui/src/components/Footer.tsx
+++ b/packages/merchant-backend-ui/src/components/Footer.tsx
@@ -15,18 +15,21 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-import { h, VNode } from 'preact';
-import { FooterBar } from '../styled';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { h, VNode } from "preact";
+import { FooterBar } from "../styled/index.js";
export function Footer(): VNode {
- return <FooterBar>
- <p>
- <a href="https://taler.net/">Learn more about GNU Taler on our website.</a>
- <p>Copyright &copy; 2014&mdash;2021 Taler Systems SA</p>
- </p>
- </FooterBar>
+ return (
+ <FooterBar>
+ <p>
+ <a href="https://taler.net/">
+ Learn more about GNU Taler on our website.
+ </a>
+ <p>Copyright &copy; 2014&mdash;2021 Taler Systems SA</p>
+ </p>
+ </FooterBar>
+ );
}
-
diff --git a/packages/merchant-backend-ui/src/components/QR.tsx b/packages/merchant-backend-ui/src/components/QR.tsx
index 29c9920bf..425a94961 100644
--- a/packages/merchant-backend-ui/src/components/QR.tsx
+++ b/packages/merchant-backend-ui/src/components/QR.tsx
@@ -14,28 +14,41 @@
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 createSVG(text:string):string {
- const qr = qrcode(0, 'L');
+import { h, VNode } from "preact";
+import { useEffect, useRef } from "preact/hooks";
+import qrcode from "qrcode-generator";
+
+export function createSVG(text: string): string {
+ const qr = qrcode(0, "L");
qr.addData(text);
qr.make();
return qr.createSvgTag({
scalable: true,
- margin: 0
+ margin: 0,
});
}
- export function QR({ text }: { text: string; }):VNode {
- const divRef = useRef<HTMLDivElement>(null);
- useEffect(() => {
- divRef.current.innerHTML = createSVG(text)
- });
-
- return <div style={{ width: '100%', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
- <div style={{ width: '50%', minWidth: 200, maxWidth: 300 }} ref={divRef} />
- </div>;
- }
- \ No newline at end of file
+export function QR({ text }: { text: string }): VNode {
+ const divRef = useRef<HTMLDivElement>(null);
+ useEffect(() => {
+ if (divRef.current) {
+ divRef.current.innerHTML = createSVG(text);
+ }
+ });
+
+ 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/merchant-backend-ui/src/context/backend.ts b/packages/merchant-backend-ui/src/context/backend.ts
deleted file mode 100644
index a920d6ffc..000000000
--- a/packages/merchant-backend-ui/src/context/backend.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { createContext, h, VNode } from 'preact'
-import { useCallback, useContext, useState } from 'preact/hooks'
-import { useBackendDefaultToken, useBackendURL } from '../hooks';
-
-interface BackendContextType {
- url: string;
- token?: string;
- triedToLog: boolean;
- resetBackend: () => void;
- clearAllTokens: () => void;
- addTokenCleaner: (c: () => void) => void;
- updateLoginStatus: (url: string, token?: string) => void;
-}
-
-const BackendContext = createContext<BackendContextType>({
- url: '',
- token: undefined,
- triedToLog: false,
- resetBackend: () => null,
- clearAllTokens: () => null,
- addTokenCleaner: () => null,
- updateLoginStatus: () => null,
-})
-
-function useBackendContextState(defaultUrl?: string): BackendContextType {
- const [url, triedToLog, changeBackend, resetBackend] = useBackendURL(defaultUrl);
- const [token, _updateToken] = useBackendDefaultToken();
- const updateToken = (t?: string) => {
- _updateToken(t)
- }
-
- const tokenCleaner = useCallback(() => { updateToken(undefined) }, [])
- const [cleaners, setCleaners] = useState([tokenCleaner])
- const addTokenCleaner = (c: () => void) => setCleaners(cs => [...cs, c])
- const addTokenCleanerMemo = useCallback((c: () => void) => { addTokenCleaner(c) }, [tokenCleaner])
-
- const clearAllTokens = () => {
- cleaners.forEach(c => c())
- for (let i = 0; i < localStorage.length; i++) {
- const k = localStorage.key(i)
- if (k && /^backend-token/.test(k)) localStorage.removeItem(k)
- }
- resetBackend()
- }
-
- const updateLoginStatus = (url: string, token?: string) => {
- changeBackend(url);
- if (token) updateToken(token);
- };
-
-
- return { url, token, triedToLog, updateLoginStatus, resetBackend, clearAllTokens, addTokenCleaner: addTokenCleanerMemo }
-}
-
-export const BackendContextProvider = ({ children, defaultUrl }: { children: any, defaultUrl?: string }): VNode => {
- const value = useBackendContextState(defaultUrl)
-
- return h(BackendContext.Provider, { value, children });
-}
-
-export const useBackendContext = (): BackendContextType => useContext(BackendContext);
diff --git a/packages/merchant-backend-ui/src/context/fetch.ts b/packages/merchant-backend-ui/src/context/fetch.ts
deleted file mode 100644
index 52a4f9c8d..000000000
--- a/packages/merchant-backend-ui/src/context/fetch.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { h, createContext, VNode, ComponentChildren } from 'preact'
-import { useContext } from 'preact/hooks'
-import useSWR, { trigger, useSWRInfinite, cache, mutate } from 'swr';
-
-interface Type {
- useSWR: typeof useSWR,
- useSWRInfinite: typeof useSWRInfinite,
-}
-
-const Context = createContext<Type>({} as any)
-
-export const useFetchContext = (): Type => useContext(Context);
-export const FetchContextProvider = ({ children }: { children: ComponentChildren }): VNode => {
- return h(Context.Provider, { value: { useSWR, useSWRInfinite }, children });
-}
-
-export const FetchContextProviderTesting = ({ children, data }: { children: ComponentChildren, data:any }): VNode => {
- return h(Context.Provider, { value: { useSWR: () => data, useSWRInfinite }, children });
-}
diff --git a/packages/merchant-backend-ui/src/context/translation.ts b/packages/merchant-backend-ui/src/context/translation.ts
deleted file mode 100644
index 952a1e325..000000000
--- a/packages/merchant-backend-ui/src/context/translation.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { createContext, h, VNode } from 'preact'
-import { useContext, useEffect } from 'preact/hooks'
-import { useLang } from '../hooks'
-import * as jedLib from "jed";
-import { strings } from "../i18n/strings";
-
-interface Type {
- lang: string;
- handler: any;
- changeLanguage: (l: string) => void;
-}
-const initial = {
- lang: 'en',
- handler: null,
- changeLanguage: () => {
- // do not change anything
- }
-}
-const Context = createContext<Type>(initial)
-
-interface Props {
- initial?: string,
- children: any,
- forceLang?: string
-}
-
-export const TranslationProvider = ({ initial, children, forceLang }: Props): VNode => {
- const [lang, changeLanguage] = useLang(initial)
- useEffect(() => {
- if (forceLang) {
- changeLanguage(forceLang)
- }
- })
- const handler = new jedLib.Jed(strings[lang]);
- return h(Context.Provider, { value: { lang, handler, changeLanguage }, children });
-}
-
-export const useTranslationContext = (): Type => useContext(Context); \ No newline at end of file
diff --git a/packages/merchant-backend-ui/src/declaration.d.ts b/packages/merchant-backend-ui/src/declaration.d.ts
index 74b0a5011..bb71f61cd 100644
--- a/packages/merchant-backend-ui/src/declaration.d.ts
+++ b/packages/merchant-backend-ui/src/declaration.d.ts
@@ -50,7 +50,9 @@ interface WithId {
type Amount = string;
type UUID = string;
type Integer = number;
-
+type TalerProtocolTimestamp = {
+ t_s: number | "never"
+}
export namespace ExchangeBackend {
interface WireResponse {
@@ -1284,6 +1286,7 @@ export namespace MerchantBackend {
// will send back to the customer the same proposal. Clearly, this URL
// can be bookmarked and shared by users.
fulfillment_url?: string;
+ fulfillment_message?: string;
// Maximum total deposit fee accepted by the merchant for this contract
max_fee: Amount;
diff --git a/packages/merchant-backend-ui/src/hooks/async.ts b/packages/merchant-backend-ui/src/hooks/async.ts
deleted file mode 100644
index fd550043b..000000000
--- a/packages/merchant-backend-ui/src/hooks/async.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-import { useState } from "preact/hooks";
-import { cancelPendingRequest } from "./backend";
-
-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() {
- cancelPendingRequest()
- setLoading(false);
- setSlow(false)
- }
-
- return {
- request,
- cancel,
- data,
- isSlow,
- isLoading,
- error
- };
-}
diff --git a/packages/merchant-backend-ui/src/hooks/backend.ts b/packages/merchant-backend-ui/src/hooks/backend.ts
deleted file mode 100644
index 96b8f7139..000000000
--- a/packages/merchant-backend-ui/src/hooks/backend.ts
+++ /dev/null
@@ -1,262 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { mutate, cache } from 'swr';
-import axios, { AxiosError, AxiosResponse } from 'axios'
-import { MerchantBackend } from '../declaration';
-import { useBackendContext } from '../context/backend';
-import { useEffect, useState } from 'preact/hooks';
-import { DEFAULT_REQUEST_TIMEOUT } from '../utils/constants';
-
-export function mutateAll(re: RegExp, value?: unknown): Array<Promise<any>> {
- return cache.keys().filter(key => {
- return re.test(key)
- }).map(key => {
- return mutate(key, value)
- })
-}
-
-export type HttpResponse<T> = HttpResponseOk<T> | HttpResponseLoading<T> | HttpError;
-export type HttpResponsePaginated<T> = HttpResponseOkPaginated<T> | HttpResponseLoading<T> | HttpError;
-
-export interface RequestInfo {
- url: string;
- hasToken: boolean;
- params: unknown;
- data: unknown;
-}
-
-interface HttpResponseLoading<T> {
- ok?: false;
- loading: true;
- clientError?: false;
- serverError?: false;
-
- data?: T;
-}
-export interface HttpResponseOk<T> {
- ok: true;
- loading?: false;
- clientError?: false;
- serverError?: false;
-
- data: T;
- info?: RequestInfo;
-}
-
-export type HttpResponseOkPaginated<T> = HttpResponseOk<T> & WithPagination
-
-export interface WithPagination {
- loadMore: () => void;
- loadMorePrev: () => void;
- isReachingEnd?: boolean;
- isReachingStart?: boolean;
-}
-
-export type HttpError = HttpResponseClientError | HttpResponseServerError | HttpResponseUnexpectedError;
-export interface SwrError {
- info: unknown,
- status: number,
- message: string,
-}
-export interface HttpResponseServerError {
- ok?: false;
- loading?: false;
- clientError?: false;
- serverError: true;
-
- error?: MerchantBackend.ErrorDetail;
- status: number;
- message: string;
- info?: RequestInfo;
-}
-interface HttpResponseClientError {
- ok?: false;
- loading?: false;
- clientError: true;
- serverError?: false;
-
- info?: RequestInfo;
- isUnauthorized: boolean;
- isNotfound: boolean;
- status: number;
- error?: MerchantBackend.ErrorDetail;
- message: string;
-
-}
-
-interface HttpResponseUnexpectedError {
- ok?: false;
- loading?: false;
- clientError?: false;
- serverError?: false;
-
- info?: RequestInfo;
- status?: number;
- error: unknown;
- message: string;
-}
-
-type Methods = 'get' | 'post' | 'patch' | 'delete' | 'put';
-
-interface RequestOptions {
- method?: Methods;
- token?: string;
- data?: unknown;
- params?: unknown;
-}
-
-function buildRequestOk<T>(res: AxiosResponse<T>, url: string, hasToken: boolean): HttpResponseOk<T> {
- return {
- ok: true, data: res.data, info: {
- params: res.config.params,
- data: res.config.data,
- url,
- hasToken,
- }
- }
-}
-
-// function buildResponse<T>(data?: T, error?: MerchantBackend.ErrorDetail, isValidating?: boolean): HttpResponse<T> {
-// if (isValidating) return {loading: true}
-// if (error) return buildRequestFailed()
-// }
-
-function buildRequestFailed(ex: AxiosError<MerchantBackend.ErrorDetail>, url: string, hasToken: boolean): HttpResponseClientError | HttpResponseServerError | HttpResponseUnexpectedError {
- const status = ex.response?.status
-
- const info: RequestInfo = {
- data: ex.request?.data,
- params: ex.request?.params,
- url,
- hasToken,
- };
-
- if (status && status >= 400 && status < 500) {
- const error: HttpResponseClientError = {
- clientError: true,
- isNotfound: status === 404,
- isUnauthorized: status === 401,
- status,
- info,
- message: ex.response?.data?.hint || ex.message,
- error: ex.response?.data
- }
- return error
- }
- if (status && status >= 500 && status < 600) {
- const error: HttpResponseServerError = {
- serverError: true,
- status,
- info,
- message: `${ex.response?.data?.hint} (code ${ex.response?.data?.code})` || ex.message,
- error: ex.response?.data
- }
- return error;
- }
-
- const error: HttpResponseUnexpectedError = {
- info,
- status,
- error: ex,
- message: ex.message
- }
-
- return error
-}
-
-
-const CancelToken = axios.CancelToken;
-let source = CancelToken.source();
-
-export function cancelPendingRequest() {
- source.cancel('canceled by the user')
- source = CancelToken.source()
-}
-
-let removeAxiosCancelToken = false
-/**
- * Jest mocking seems to break when using the cancelToken property.
- * Using this workaround when testing while finding the correct solution
- */
-export function setAxiosRequestAsTestingEnvironment() {
- removeAxiosCancelToken = true
-}
-
-export async function request<T>(url: string, options: RequestOptions = {}): Promise<HttpResponseOk<T>> {
- const headers = options.token ? { Authorization: `Bearer ${options.token}` } : undefined
-
- try {
- const res = await axios({
- url,
- responseType: 'json',
- headers,
- cancelToken: !removeAxiosCancelToken? source.token : undefined,
- method: options.method || 'get',
- data: options.data,
- params: options.params,
- timeout: DEFAULT_REQUEST_TIMEOUT * 1000,
- })
- return buildRequestOk<T>(res, url, !!options.token)
- } catch (e) {
- const error = buildRequestFailed(e, url, !!options.token)
- throw error
- }
-
-}
-
-export function fetcher<T>(url: string, token: string, backend: string): Promise<HttpResponseOk<T>> {
- return request<T>(`${backend}${url}`, { token })
-}
-
-export function useBackendInstancesTestForAdmin(): HttpResponse<MerchantBackend.Instances.InstancesResponse> {
- const { url, token } = useBackendContext()
-
- type Type = MerchantBackend.Instances.InstancesResponse;
-
- const [result, setResult] = useState<HttpResponse<Type>>({ loading: true })
-
- useEffect(() => {
- request<Type>(`${url}/management/instances`, { token })
- .then(data => setResult(data))
- .catch(error => setResult(error))
- }, [url, token])
-
-
- return result
-}
-
-
-export function useBackendConfig(): HttpResponse<MerchantBackend.VersionResponse> {
- const { url, token } = useBackendContext()
-
- type Type = MerchantBackend.VersionResponse;
-
- const [result, setResult] = useState<HttpResponse<Type>>({ loading: true })
-
- useEffect(() => {
- request<Type>(`${url}/config`, { token })
- .then(data => setResult(data))
- .catch(error => setResult(error))
- }, [url, token])
-
- return result
-}
diff --git a/packages/merchant-backend-ui/src/hooks/index.ts b/packages/merchant-backend-ui/src/hooks/index.ts
deleted file mode 100644
index 19d672ad3..000000000
--- a/packages/merchant-backend-ui/src/hooks/index.ts
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { StateUpdater, useCallback, useState } from "preact/hooks";
-import { ValueOrFunction } from '../utils/types';
-
-
-const calculateRootPath = () => {
- const rootPath = typeof window !== undefined ? window.location.origin + window.location.pathname : '/'
- return rootPath
-}
-
-export function useBackendURL(url?: string): [string, boolean, StateUpdater<string>, () => void] {
- const [value, setter] = useNotNullLocalStorage('backend-url', url || calculateRootPath())
- const [triedToLog, setTriedToLog] = useLocalStorage('tried-login')
-
- const checkedSetter = (v: ValueOrFunction<string>) => {
- setTriedToLog('yes')
- return setter(p => (v instanceof Function ? v(p) : v).replace(/\/$/, ''))
- }
-
- const resetBackend = () => {
- setTriedToLog(undefined)
- }
- return [value, !!triedToLog, checkedSetter, resetBackend]
-}
-
-export function useBackendDefaultToken(): [string | undefined, StateUpdater<string | undefined>] {
- return useLocalStorage('backend-token')
-}
-
-export function useBackendInstanceToken(id: string): [string | undefined, StateUpdater<string | undefined>] {
- const [token, setToken] = useLocalStorage(`backend-token-${id}`)
- const [defaultToken, defaultSetToken] = useBackendDefaultToken()
-
- // instance named 'default' use the default token
- if (id === 'default') {
- return [defaultToken, defaultSetToken]
- }
-
- return [token, setToken]
-}
-
-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 useNotNullLocalStorage('lang-preference', defaultLang)
-}
-
-export function useLocalStorage(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];
-}
-
-export function useNotNullLocalStorage(key: string, initialValue: string): [string, StateUpdater<string>] {
- const [storedValue, setStoredValue] = useState<string>((): string => {
- return typeof window !== "undefined" ? window.localStorage.getItem(key) || initialValue : initialValue;
- });
-
- const setValue = (value: string | ((val: string) => string)) => {
- const valueToStore = value instanceof Function ? value(storedValue) : value;
- setStoredValue(valueToStore);
- if (typeof window !== "undefined") {
- if (!valueToStore) {
- window.localStorage.removeItem(key)
- } else {
- window.localStorage.setItem(key, valueToStore);
- }
- }
- };
-
- return [storedValue, setValue];
-}
-
-
diff --git a/packages/merchant-backend-ui/src/hooks/instance.ts b/packages/merchant-backend-ui/src/hooks/instance.ts
deleted file mode 100644
index 14ab8de9c..000000000
--- a/packages/merchant-backend-ui/src/hooks/instance.ts
+++ /dev/null
@@ -1,187 +0,0 @@
-/*
- 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/>
- */
-import { MerchantBackend } from '../declaration';
-import { useBackendContext } from '../context/backend';
-import { fetcher, HttpError, HttpResponse, HttpResponseOk, request, SwrError } from './backend';
-import useSWR, { mutate } from 'swr';
-import { useInstanceContext } from '../context/instance';
-
-
-interface InstanceAPI {
- updateInstance: (data: MerchantBackend.Instances.InstanceReconfigurationMessage) => Promise<void>;
- deleteInstance: () => Promise<void>;
- clearToken: () => Promise<void>;
- setNewToken: (token: string) => Promise<void>;
-}
-
-export function useManagementAPI(instanceId: string) : InstanceAPI {
- const { url, token } = useBackendContext()
-
- const updateInstance = async (instance: MerchantBackend.Instances.InstanceReconfigurationMessage): Promise<void> => {
- await request(`${url}/management/instances/${instanceId}`, {
- method: 'patch',
- token,
- data: instance
- })
-
- mutate([`/private/`, token, url], null)
- };
-
- const deleteInstance = async (): Promise<void> => {
- await request(`${url}/management/instances/${instanceId}`, {
- method: 'delete',
- token,
- })
-
- mutate([`/private/`, token, url], null)
- }
-
- const clearToken = async (): Promise<void> => {
- await request(`${url}/management/instances/${instanceId}/auth`, {
- method: 'post',
- token,
- data: { method: 'external' }
- })
-
- mutate([`/private/`, token, url], null)
- }
-
- const setNewToken = async (newToken: string): Promise<void> => {
- await request(`${url}/management/instances/${instanceId}/auth`, {
- method: 'post',
- token,
- data: { method: 'token', token: newToken }
- })
-
- mutate([`/private/`, token, url], null)
- }
-
- return { updateInstance, deleteInstance, setNewToken, clearToken }
-}
-
-export function useInstanceAPI(): InstanceAPI {
- const { url: baseUrl, token: adminToken } = useBackendContext()
- const { token: instanceToken, id, admin } = useInstanceContext()
-
- const { url, token } = !admin ? {
- url: baseUrl, token: adminToken
- } : {
- url: `${baseUrl}/instances/${id}`, token: instanceToken
- };
-
- const updateInstance = async (instance: MerchantBackend.Instances.InstanceReconfigurationMessage): Promise<void> => {
- await request(`${url}/private/`, {
- method: 'patch',
- token,
- data: instance
- })
-
- if (adminToken) mutate(['/private/instances', adminToken, baseUrl], null)
- mutate([`/private/`, token, url], null)
- };
-
- const deleteInstance = async (): Promise<void> => {
- await request(`${url}/private/`, {
- method: 'delete',
- token: adminToken,
- })
-
- if (adminToken) mutate(['/private/instances', adminToken, baseUrl], null)
- mutate([`/private/`, token, url], null)
- }
-
- const clearToken = async (): Promise<void> => {
- await request(`${url}/private/auth`, {
- method: 'post',
- token,
- data: { method: 'external' }
- })
-
- mutate([`/private/`, token, url], null)
- }
-
- const setNewToken = async (newToken: string): Promise<void> => {
- await request(`${url}/private/auth`, {
- method: 'post',
- token,
- data: { method: 'token', token: newToken }
- })
-
- mutate([`/private/`, token, url], null)
- }
-
- return { updateInstance, deleteInstance, setNewToken, clearToken }
-}
-
-
-export function useInstanceDetails(): HttpResponse<MerchantBackend.Instances.QueryInstancesResponse> {
- const { url: baseUrl, token: baseToken } = useBackendContext();
- const { token: instanceToken, id, admin } = useInstanceContext();
-
- const { url, token } = !admin ? {
- url: baseUrl, token: baseToken
- } : {
- url: `${baseUrl}/instances/${id}`, token: instanceToken
- }
-
- const { data, error, isValidating } = useSWR<HttpResponseOk<MerchantBackend.Instances.QueryInstancesResponse>, HttpError>([`/private/`, token, url], fetcher, {
- 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
- return {loading: true}
-}
-
-export function useManagedInstanceDetails(instanceId: string): HttpResponse<MerchantBackend.Instances.QueryInstancesResponse> {
- const { url, token } = useBackendContext();
-
- const { data, error, isValidating } = useSWR<HttpResponseOk<MerchantBackend.Instances.QueryInstancesResponse>, HttpError>([`/management/instances/${instanceId}`, token, url], fetcher, {
- 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
- return {loading: true}
-}
-
-export function useBackendInstances(): HttpResponse<MerchantBackend.Instances.InstancesResponse> {
- const { url } = useBackendContext()
- const { token } = useInstanceContext();
-
- const { data, error, isValidating } = useSWR<HttpResponseOk<MerchantBackend.Instances.InstancesResponse>, HttpError>(['/management/instances', token, url], fetcher)
-
- if (isValidating) return {loading:true, data: data?.data}
- if (data) return data
- if (error) return error
- return {loading: true}
-}
diff --git a/packages/merchant-backend-ui/src/hooks/notification.ts b/packages/merchant-backend-ui/src/hooks/notification.ts
deleted file mode 100644
index d1dfbff2c..000000000
--- a/packages/merchant-backend-ui/src/hooks/notification.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { useCallback, useState } from "preact/hooks";
-import { Notification } from '../utils/types';
-
-interface Result {
- notification?: Notification;
- pushNotification: (n: Notification) => void;
- removeNotification: () => void;
-}
-
-export function useNotification(): Result {
- const [notification, setNotifications] = useState<Notification|undefined>(undefined)
-
- const pushNotification = useCallback((n: Notification): void => {
- setNotifications(n)
- },[])
-
- const removeNotification = useCallback(() => {
- setNotifications(undefined)
- },[])
-
- return { notification, pushNotification, removeNotification }
-}
diff --git a/packages/merchant-backend-ui/src/hooks/order.ts b/packages/merchant-backend-ui/src/hooks/order.ts
deleted file mode 100644
index 4a17eac30..000000000
--- a/packages/merchant-backend-ui/src/hooks/order.ts
+++ /dev/null
@@ -1,217 +0,0 @@
-/*
- 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/>
- */
-import { useEffect, useState } from 'preact/hooks';
-import useSWR from 'swr';
-import { useBackendContext } from '../context/backend';
-import { useInstanceContext } from '../context/instance';
-import { MerchantBackend } from '../declaration';
-import { MAX_RESULT_SIZE, PAGE_SIZE } from '../utils/constants';
-import { fetcher, HttpError, HttpResponse, HttpResponseOk, HttpResponsePaginated, mutateAll, request } from './backend';
-
-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 orderFetcher<T>(url: string, token: string, backend: string, paid?: YesOrNo, refunded?: YesOrNo, wired?: YesOrNo, searchDate?: Date, delta?: number): Promise<HttpResponseOk<T>> {
- const date_ms = delta && delta < 0 && searchDate ? searchDate.getTime() + 1 : searchDate?.getTime()
- 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_ms !== undefined) params.date_ms = date_ms
- return request<T>(`${backend}${url}`, { token, params })
-}
-
-
-export function useOrderAPI(): OrderAPI {
- const { url: baseUrl, token: adminToken } = useBackendContext()
- const { token: instanceToken, id, admin } = useInstanceContext()
-
- const { url, token } = !admin ? {
- url: baseUrl, token: adminToken
- } : {
- url: `${baseUrl}/instances/${id}`, token: instanceToken
- }
-
- const createOrder = async (data: MerchantBackend.Orders.PostOrderRequest): Promise<HttpResponseOk<MerchantBackend.Orders.PostOrderResponse>> => {
- const res = await request<MerchantBackend.Orders.PostOrderResponse>(`${url}/private/orders`, {
- method: 'post',
- token,
- data
- })
- await mutateAll(/@"\/private\/orders"@/)
- return res
- }
- const refundOrder = async (orderId: string, data: MerchantBackend.Orders.RefundRequest): Promise<HttpResponseOk<MerchantBackend.Orders.MerchantRefundResponse>> => {
- mutateAll(/@"\/private\/orders"@/)
- return request<MerchantBackend.Orders.MerchantRefundResponse>(`${url}/private/orders/${orderId}/refund`, {
- method: 'post',
- token,
- data
- })
-
- // return res
- }
-
- const forgetOrder = async (orderId: string, data: MerchantBackend.Orders.ForgetRequest): Promise<HttpResponseOk<void>> => {
- mutateAll(/@"\/private\/orders"@/)
- return request(`${url}/private/orders/${orderId}/forget`, {
- method: 'patch',
- token,
- data
- })
-
- }
- const deleteOrder = async (orderId: string): Promise<HttpResponseOk<void>> => {
- mutateAll(/@"\/private\/orders"@/)
- return request(`${url}/private/orders/${orderId}`, {
- method: 'delete',
- token
- })
- }
-
- const getPaymentURL = async (orderId: string): Promise<HttpResponseOk<string>> => {
- return request<MerchantBackend.Orders.MerchantOrderStatusResponse>(`${url}/private/orders/${orderId}`, {
- method: 'get',
- token
- }).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> {
- const { url: baseUrl, token: baseToken } = useBackendContext();
- const { token: instanceToken, id, admin } = useInstanceContext();
-
- const { url, token } = !admin ? {
- url: baseUrl, token: baseToken
- } : {
- url: `${baseUrl}/instances/${id}`, token: instanceToken
- };
-
- const { data, error, isValidating } = useSWR<HttpResponseOk<MerchantBackend.Orders.MerchantOrderStatusResponse>, HttpError>([`/private/orders/${oderId}`, token, url], 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
- 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> {
- const { url: baseUrl, token: baseToken } = useBackendContext();
- const { token: instanceToken, id, admin } = useInstanceContext();
-
- const { url, token } = !admin ? {
- url: baseUrl, token: baseToken
- } : {
- url: `${baseUrl}/instances/${id}`, token: instanceToken
- }
-
- 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>, HttpError>(
- [`/private/orders`, token, url, args?.paid, args?.refunded, args?.wired, args?.date, totalBefore],
- orderFetcher,
- )
- const { data: afterData, error: afterError, isValidating: loadingAfter } = useSWR<HttpResponseOk<MerchantBackend.Orders.OrderHistory>, HttpError>(
- [`/private/orders`, token, url, args?.paid, args?.refunded, args?.wired, args?.date, -totalAfter],
- orderFetcher,
- )
-
- //this will save last result
- const [lastBefore, setLastBefore] = useState<HttpResponse<MerchantBackend.Orders.OrderHistory>>({ loading: true })
- const [lastAfter, setLastAfter] = useState<HttpResponse<MerchantBackend.Orders.OrderHistory>>({ loading: true })
- useEffect(() => {
- if (afterData) setLastAfter(afterData)
- if (beforeData) setLastBefore(beforeData)
- }, [afterData, beforeData])
-
- // this has problems when there are some ids missing
-
- if (beforeError) return beforeError
- if (afterError) return afterError
-
-
- const pagination = {
- isReachingEnd: afterData && afterData.data.orders.length < totalAfter,
- isReachingStart: (!args?.date) || (beforeData && beforeData.data.orders.length < totalBefore),
- loadMore: () => {
- if (!afterData) 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 && updateFilter) updateFilter(new Date(from))
- }
- },
- loadMorePrev: () => {
- if (!beforeData) 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 && updateFilter) updateFilter(new Date(from))
- }
- },
- }
-
- 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/merchant-backend-ui/src/hooks/product.ts b/packages/merchant-backend-ui/src/hooks/product.ts
deleted file mode 100644
index 4fc8bccb7..000000000
--- a/packages/merchant-backend-ui/src/hooks/product.ts
+++ /dev/null
@@ -1,223 +0,0 @@
-/*
- 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/>
- */
-import { useEffect } from "preact/hooks";
-import useSWR, { trigger, useSWRInfinite, cache, mutate } from "swr";
-import { useBackendContext } from "../context/backend";
-// import { useFetchContext } from '../context/fetch';
-import { useInstanceContext } from "../context/instance";
-import { MerchantBackend, WithId } from "../declaration";
-import {
- fetcher,
- HttpError,
- HttpResponse,
- HttpResponseOk,
- mutateAll,
- request,
-} from "./backend";
-
-export interface ProductAPI {
- 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 { url: baseUrl, token: adminToken } = useBackendContext();
- const { token: instanceToken, id, admin } = useInstanceContext();
-
- const { url, token } = !admin
- ? {
- url: baseUrl,
- token: adminToken,
- }
- : {
- url: `${baseUrl}/instances/${id}`,
- token: instanceToken,
- };
-
- const createProduct = async (
- data: MerchantBackend.Products.ProductAddDetail
- ): Promise<void> => {
- await request(`${url}/private/products`, {
- method: "post",
- token,
- data,
- });
-
- await mutateAll(/@"\/private\/products"@/, null);
- };
-
- const updateProduct = async (
- productId: string,
- data: MerchantBackend.Products.ProductPatchDetail
- ): Promise<void> => {
- const r = await request(`${url}/private/products/${productId}`, {
- method: "patch",
- token,
- data,
- });
-
- await mutateAll(/@"\/private\/products\/.*"@/);
- return Promise.resolve();
- };
-
- const deleteProduct = async (productId: string): Promise<void> => {
- await request(`${url}/private/products/${productId}`, {
- method: "delete",
- token,
- });
-
- await mutateAll(/@"\/private\/products"@/);
- };
-
- const lockProduct = async (
- productId: string,
- data: MerchantBackend.Products.LockRequest
- ): Promise<void> => {
- await request(`${url}/private/products/${productId}/lock`, {
- method: "post",
- token,
- data,
- });
-
- await mutateAll(/@"\/private\/products"@/);
- };
-
- return { createProduct, updateProduct, deleteProduct, lockProduct };
-}
-
-export function useInstanceProducts(): HttpResponse<
- (MerchantBackend.Products.ProductDetail & WithId)[]
-> {
- const { url: baseUrl, token: baseToken } = useBackendContext();
- const { token: instanceToken, id, admin } = useInstanceContext();
- // const { useSWR, useSWRInfinite } = useFetchContext();
-
- const { url, token } = !admin
- ? {
- url: baseUrl,
- token: baseToken,
- }
- : {
- url: `${baseUrl}/instances/${id}`,
- token: instanceToken,
- };
-
- const {
- data: list,
- error: listError,
- isValidating: listLoading,
- } = useSWR<
- HttpResponseOk<MerchantBackend.Products.InventorySummaryResponse>,
- HttpError
- >([`/private/products`, token, url], fetcher, {
- refreshInterval: 0,
- refreshWhenHidden: false,
- revalidateOnFocus: false,
- revalidateOnReconnect: false,
- refreshWhenOffline: false,
- });
-
- const {
- data: products,
- error: productError,
- setSize,
- size,
- } = useSWRInfinite<
- HttpResponseOk<MerchantBackend.Products.ProductDetail>,
- HttpError
- >(
- (pageIndex: number) => {
- if (!list?.data || !list.data.products.length || listError || listLoading)
- return null;
- return [
- `/private/products/${list.data.products[pageIndex].product_id}`,
- token,
- url,
- ];
- },
- fetcher,
- {
- revalidateAll: true,
- }
- );
-
- useEffect(() => {
- if (list?.data && list.data.products.length > 0) {
- setSize(list.data.products.length);
- }
- }, [list?.data.products.length, listLoading]);
-
- if (listLoading) return { loading: true, data: [] };
- if (listError) return listError;
- if (productError) return productError;
- if (list?.data && list.data.products.length === 0) {
- return { ok: true, data: [] };
- }
- 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> {
- const { url: baseUrl, token: baseToken } = useBackendContext();
- const { token: instanceToken, id, admin } = useInstanceContext();
-
- const { url, token } = !admin
- ? {
- url: baseUrl,
- token: baseToken,
- }
- : {
- url: `${baseUrl}/instances/${id}`,
- token: instanceToken,
- };
-
- const { data, error, isValidating } = useSWR<
- HttpResponseOk<MerchantBackend.Products.ProductDetail>,
- HttpError
- >([`/private/products/${productId}`, token, url], 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;
- return { loading: true };
-}
diff --git a/packages/merchant-backend-ui/src/hooks/tips.ts b/packages/merchant-backend-ui/src/hooks/tips.ts
deleted file mode 100644
index 345e1faa5..000000000
--- a/packages/merchant-backend-ui/src/hooks/tips.ts
+++ /dev/null
@@ -1,159 +0,0 @@
-/*
- 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/>
- */
-import useSWR from 'swr';
-import { useBackendContext } from '../context/backend';
-import { useInstanceContext } from '../context/instance';
-import { MerchantBackend } from '../declaration';
-import { fetcher, HttpError, HttpResponse, HttpResponseOk, mutateAll, request } from './backend';
-
-
-export function useReservesAPI(): ReserveMutateAPI {
- const { url: baseUrl, token: adminToken } = useBackendContext();
- const { token: instanceToken, id, admin } = useInstanceContext();
-
- const { url, token } = !admin ? {
- url: baseUrl, token: adminToken
- } : {
- url: `${baseUrl}/instances/${id}`, token: instanceToken
- };
-
- const createReserve = async (data: MerchantBackend.Tips.ReserveCreateRequest): Promise<HttpResponseOk<MerchantBackend.Tips.ReserveCreateConfirmation>> => {
- const res = await request<MerchantBackend.Tips.ReserveCreateConfirmation>(`${url}/private/reserves`, {
- method: 'post',
- token,
- data
- });
-
- await mutateAll(/@"\/private\/reserves"@/);
-
- return res
- };
-
- const authorizeTipReserve = async (pub: string, data: MerchantBackend.Tips.TipCreateRequest): Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>> => {
- const res = await request<MerchantBackend.Tips.TipCreateConfirmation>(`${url}/private/reserves/${pub}/authorize-tip`, {
- method: 'post',
- token,
- data
- });
- await mutateAll(/@"\/private\/reserves"@/);
-
- return res
- };
-
- const authorizeTip = async (data: MerchantBackend.Tips.TipCreateRequest): Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>> => {
- const res = await request<MerchantBackend.Tips.TipCreateConfirmation>(`${url}/private/tips`, {
- method: 'post',
- token,
- data
- });
-
- await mutateAll(/@"\/private\/reserves"@/);
-
- return res
- };
-
- const deleteReserve = async (pub: string): Promise<HttpResponse<void>> => {
- const res = await request<void>(`${url}/private/reserves/${pub}`, {
- method: 'delete',
- token,
- });
-
- await mutateAll(/@"\/private\/reserves"@/);
-
- return res
- };
-
-
- return { createReserve, authorizeTip, authorizeTipReserve, deleteReserve };
-}
-
-export interface ReserveMutateAPI {
- createReserve: (data: MerchantBackend.Tips.ReserveCreateRequest) => Promise<HttpResponseOk<MerchantBackend.Tips.ReserveCreateConfirmation>>;
- authorizeTipReserve: (id: string, data: MerchantBackend.Tips.TipCreateRequest) => Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>>;
- authorizeTip: (data: MerchantBackend.Tips.TipCreateRequest) => Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>>;
- deleteReserve: (id: string) => Promise<HttpResponse<void>>;
-}
-
-export function useInstanceTips(): HttpResponse<MerchantBackend.Tips.TippingReserveStatus> {
- const { url: baseUrl, token: baseToken } = useBackendContext();
- const { token: instanceToken, id, admin } = useInstanceContext();
-
- const { url, token } = !admin ? {
- url: baseUrl, token: baseToken
- } : {
- url: `${baseUrl}/instances/${id}`, token: instanceToken
- }
-
- const { data, error, isValidating } = useSWR<HttpResponseOk<MerchantBackend.Tips.TippingReserveStatus>, HttpError>([`/private/reserves`, token, url], fetcher)
-
- if (isValidating) return { loading: true, data: data?.data }
- if (data) return data
- if (error) return error
- return { loading: true }
-}
-
-
-export function useReserveDetails(reserveId: string): HttpResponse<MerchantBackend.Tips.ReserveDetail> {
- const { url: baseUrl } = useBackendContext();
- const { token, id: instanceId, admin } = useInstanceContext();
-
- const url = !admin ? baseUrl : `${baseUrl}/instances/${instanceId}`
-
- const { data, error, isValidating } = useSWR<HttpResponseOk<MerchantBackend.Tips.ReserveDetail>, HttpError>([`/private/reserves/${reserveId}`, token, url], 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
- return { loading: true }
-}
-
-export function useTipDetails(tipId: string): HttpResponse<MerchantBackend.Tips.TipDetails> {
- const { url: baseUrl } = useBackendContext();
- const { token, id: instanceId, admin } = useInstanceContext();
-
- const url = !admin ? baseUrl : `${baseUrl}/instances/${instanceId}`
-
- const { data, error, isValidating } = useSWR<HttpResponseOk<MerchantBackend.Tips.TipDetails>, HttpError>([`/private/tips/${tipId}`, token, url], tipsDetailFetcher, {
- 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
- return { loading: true }
-}
-
-export function reserveDetailFetcher<T>(url: string, token: string, backend: string): Promise<HttpResponseOk<T>> {
- return request<T>(`${backend}${url}`, { token, params: {
- tips: 'yes'
- } })
-}
-
-export function tipsDetailFetcher<T>(url: string, token: string, backend: string): Promise<HttpResponseOk<T>> {
- return request<T>(`${backend}${url}`, { token, params: {
- pickups: 'yes'
- } })
-}
diff --git a/packages/merchant-backend-ui/src/hooks/transfer.ts b/packages/merchant-backend-ui/src/hooks/transfer.ts
deleted file mode 100644
index 482f00dc5..000000000
--- a/packages/merchant-backend-ui/src/hooks/transfer.ts
+++ /dev/null
@@ -1,150 +0,0 @@
-/*
- 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/>
- */
-import { MerchantBackend } from '../declaration';
-import { useBackendContext } from '../context/backend';
-import { request, mutateAll, HttpResponse, HttpError, HttpResponseOk, HttpResponsePaginated } from './backend';
-import useSWR from 'swr';
-import { useInstanceContext } from '../context/instance';
-import { MAX_RESULT_SIZE, PAGE_SIZE } from '../utils/constants';
-import { useEffect, useState } from 'preact/hooks';
-
-async function transferFetcher<T>(url: string, token: string, backend: string, payto_uri?: string, verified?: string, position?: string, delta?: number): Promise<HttpResponseOk<T>> {
- const params: any = {}
- if (payto_uri !== undefined) params.payto_uri = payto_uri
- if (verified !== undefined) params.verified = verified
- if (delta !== undefined) {
- // if (delta > 0) {
- // params.after = searchDate?.getTime()
- // } else {
- // params.before = searchDate?.getTime()
- // }
- params.limit = delta
- }
- if (position !== undefined) params.offset = position
-
- return request<T>(`${backend}${url}`, { token, params })
-}
-
-export function useTransferAPI(): TransferAPI {
- const { url: baseUrl, token: adminToken } = useBackendContext();
- const { token: instanceToken, id, admin } = useInstanceContext();
-
- const { url, token } = !admin ? {
- url: baseUrl, token: adminToken
- } : {
- url: `${baseUrl}/instances/${id}`, token: instanceToken
- };
-
- const informTransfer = async (data: MerchantBackend.Transfers.TransferInformation): Promise<HttpResponseOk<MerchantBackend.Transfers.MerchantTrackTransferResponse>> => {
- mutateAll(/@"\/private\/transfers"@/);
-
- return request<MerchantBackend.Transfers.MerchantTrackTransferResponse>(`${url}/private/transfers`, {
- method: 'post',
- token,
- data
- });
- };
-
- return { informTransfer };
-}
-
-export interface TransferAPI {
- informTransfer: (data: MerchantBackend.Transfers.TransferInformation) => Promise<HttpResponseOk<MerchantBackend.Transfers.MerchantTrackTransferResponse>>;
-}
-
-export interface InstanceTransferFilter {
- payto_uri?: string;
- verified?: 'yes' | 'no';
- position?: string;
-}
-
-
-export function useInstanceTransfers(args?: InstanceTransferFilter, updatePosition?: (id: string) => void): HttpResponsePaginated<MerchantBackend.Transfers.TransferList> {
- const { url: baseUrl, token: baseToken } = useBackendContext();
- const { token: instanceToken, id, admin } = useInstanceContext();
-
- const { url, token } = !admin ? {
- url: baseUrl, token: baseToken
- } : {
- url: `${baseUrl}/instances/${id}`, token: instanceToken
- }
-
- 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>, HttpError>(
- [`/private/transfers`, token, url, args?.payto_uri, args?.verified, args?.position, totalBefore],
- transferFetcher,
- )
- const { data: afterData, error: afterError, isValidating: loadingAfter } = useSWR<HttpResponseOk<MerchantBackend.Transfers.TransferList>, HttpError>(
- [`/private/transfers`, token, url, args?.payto_uri, args?.verified, args?.position, -totalAfter],
- transferFetcher,
- )
-
- //this will save last result
- const [lastBefore, setLastBefore] = useState<HttpResponse<MerchantBackend.Transfers.TransferList>>({ loading: true })
- const [lastAfter, setLastAfter] = useState<HttpResponse<MerchantBackend.Transfers.TransferList>>({ loading: true })
- useEffect(() => {
- if (afterData) setLastAfter(afterData)
- if (beforeData) setLastBefore(beforeData)
- }, [afterData, beforeData])
-
- // this has problems when there are some ids missing
-
- if (beforeError) return beforeError
- if (afterError) return afterError
-
- const pagination = {
- isReachingEnd: afterData && afterData.data.transfers.length < totalAfter,
- isReachingStart: (!args?.position) || (beforeData && beforeData.data.transfers.length < totalBefore),
- loadMore: () => {
- if (!afterData) 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) 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/merchant-backend-ui/src/i18n/de.po b/packages/merchant-backend-ui/src/i18n/de.po
deleted file mode 100644
index 6b35bd0ce..000000000
--- a/packages/merchant-backend-ui/src/i18n/de.po
+++ /dev/null
@@ -1,1057 +0,0 @@
-# This file is part of TALER
-# (C) 2016 GNUnet e.V.
-#
-# TALER is free software; you can redistribute it and/or modify it under the
-# terms of the GNU General Public License as published by the Free Software
-# Foundation; either version 3, or (at your option) any later version.
-#
-# TALER is distributed in the hope that it will be useful, but WITHOUT ANY
-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
-# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
-#
-#, fuzzy
-msgid ""
-msgstr ""
-"Project-Id-Version: Taler Wallet\n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-11-23 00:00+0100\n"
-"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
-"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
-"Language-Team: LANGUAGE <LL@li.org>\n"
-"Language: \n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-
-#: src/ApplicationReadyRoutes.tsx:50 src/InstanceRoutes.tsx:118
-#: src/InstanceRoutes.tsx:299
-#, c-format
-msgid "Access denied"
-msgstr ""
-
-#: src/ApplicationReadyRoutes.tsx:51 src/InstanceRoutes.tsx:118
-#: src/InstanceRoutes.tsx:300
-#, c-format
-msgid "Check your token is valid"
-msgstr ""
-
-#: src/ApplicationReadyRoutes.tsx:72
-#, c-format
-msgid "Couldn't access the server."
-msgstr ""
-
-#: src/ApplicationReadyRoutes.tsx:73
-#, c-format
-msgid "Could not infer instance id from url %1$s"
-msgstr ""
-
-#: src/InstanceRoutes.tsx:109
-#, c-format
-msgid "HTTP status #%1$s: Server reported a problem"
-msgstr ""
-
-#: src/InstanceRoutes.tsx:110
-#, c-format
-msgid "Got message: \"%1$s\" from: %2$s"
-msgstr ""
-
-#: src/InstanceRoutes.tsx:127
-#, c-format
-msgid "No default instance"
-msgstr ""
-
-#: src/InstanceRoutes.tsx:128
-#, c-format
-msgid ""
-"in order to use merchant backoffice, you should create the default instance"
-msgstr ""
-
-#: src/InstanceRoutes.tsx:288
-#, c-format
-msgid "Server reported a problem: HTTP status #%1$s"
-msgstr ""
-
-#: src/InstanceRoutes.tsx:289
-#, c-format
-msgid "Got message: %1$s from: %2$s"
-msgstr ""
-
-#: src/components/exception/login.tsx:46
-#, c-format
-msgid "Login required"
-msgstr ""
-
-#: src/components/exception/login.tsx:49
-#, c-format
-msgid ""
-"Please enter your auth token. Token should have \"secret-token:\" and start "
-"with Bearer or ApiKey"
-msgstr ""
-
-#: src/components/exception/login.tsx:86 src/components/modal/index.tsx:53
-#: src/components/modal/index.tsx:75 src/paths/admin/create/CreatePage.tsx:115
-#: src/paths/instance/orders/create/CreatePage.tsx:325
-#: src/paths/instance/products/create/CreatePage.tsx:51
-#: src/paths/instance/products/list/Table.tsx:174
-#: src/paths/instance/products/list/Table.tsx:228
-#: src/paths/instance/products/update/UpdatePage.tsx:55
-#: src/paths/instance/transfers/create/CreatePage.tsx:89
-#: src/paths/instance/update/UpdatePage.tsx:134
-#, c-format
-msgid "Confirm"
-msgstr ""
-
-#: src/components/form/InputArray.tsx:72
-#, c-format
-msgid "The value %1$s is invalid for a payment url"
-msgstr ""
-
-#: src/components/form/InputDate.tsx:67
-#: src/paths/instance/orders/list/index.tsx:123
-#, c-format
-msgid "pick a date"
-msgstr ""
-
-#: src/components/form/InputDate.tsx:81
-#, c-format
-msgid "clear"
-msgstr ""
-
-#: src/components/form/InputDate.tsx:83
-#: src/paths/instance/transfers/list/Table.tsx:140
-#, c-format
-msgid "never"
-msgstr ""
-
-#: src/components/form/InputImage.tsx:80
-#, c-format
-msgid "Image should be smaller than 1 MB"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:28
-#, c-format
-msgid "Country"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:30
-#: src/paths/admin/create/CreatePage.tsx:99
-#: src/paths/instance/transfers/list/Table.tsx:124
-#: src/paths/instance/update/UpdatePage.tsx:118
-#, c-format
-msgid "Address"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:34
-#, c-format
-msgid "Building number"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:35
-#, c-format
-msgid "Building name"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:36
-#, c-format
-msgid "Street"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:37
-#, c-format
-msgid "Post code"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:38
-#, c-format
-msgid "Town location"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:39
-#, c-format
-msgid "Town"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:40
-#, c-format
-msgid "District"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:41
-#, c-format
-msgid "Country subdivision"
-msgstr ""
-
-#: src/components/form/InputSearchProduct.tsx:59
-#, c-format
-msgid "Product id"
-msgstr ""
-
-#: src/components/form/InputSearchProduct.tsx:60
-#: src/components/product/ProductForm.tsx:99
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:122
-#: src/paths/instance/orders/list/Table.tsx:227
-#: src/paths/instance/products/list/Table.tsx:86
-#, c-format
-msgid "Description"
-msgstr ""
-
-#: src/components/form/InputSearchProduct.tsx:73
-#: src/components/form/InputTaxes.tsx:81
-#: src/paths/admin/create/CreatePage.tsx:87 src/paths/admin/list/Table.tsx:110
-#: src/paths/instance/details/DetailPage.tsx:76
-#: src/paths/instance/update/UpdatePage.tsx:106
-#, c-format
-msgid "Name"
-msgstr ""
-
-#: src/components/form/InputSearchProduct.tsx:102
-#, c-format
-msgid "loading..."
-msgstr ""
-
-#: src/components/form/InputSearchProduct.tsx:108
-#, c-format
-msgid "no products found"
-msgstr ""
-
-#: src/components/form/InputSearchProduct.tsx:116
-#, c-format
-msgid "no results"
-msgstr ""
-
-#: src/components/form/InputSecured.tsx:33
-#, c-format
-msgid "Deleting"
-msgstr ""
-
-#: src/components/form/InputSecured.tsx:34
-#, c-format
-msgid "Changing"
-msgstr ""
-
-#: src/components/form/InputSecured.tsx:60
-#, c-format
-msgid "Manage token"
-msgstr ""
-
-#: src/components/form/InputSecured.tsx:83
-#, c-format
-msgid "Update"
-msgstr ""
-
-#: src/components/form/InputSecured.tsx:100
-#: src/paths/instance/orders/create/CreatePage.tsx:252
-#: src/paths/instance/orders/create/CreatePage.tsx:273
-#, c-format
-msgid "Remove"
-msgstr ""
-
-#: src/components/form/InputSecured.tsx:106 src/components/modal/index.tsx:52
-#: src/components/modal/index.tsx:73 src/paths/admin/create/CreatePage.tsx:114
-#: src/paths/instance/orders/create/CreatePage.tsx:324
-#: src/paths/instance/products/create/CreatePage.tsx:50
-#: src/paths/instance/products/list/Table.tsx:166
-#: src/paths/instance/products/list/Table.tsx:218
-#: src/paths/instance/products/update/UpdatePage.tsx:54
-#: src/paths/instance/transfers/create/CreatePage.tsx:88
-#: src/paths/instance/update/UpdatePage.tsx:133
-#, c-format
-msgid "Cancel"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:91
-#, c-format
-msgid "Manage stock"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:93
-#, c-format
-msgid "Infinite"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:105
-#, c-format
-msgid "lost cannot be greater that current + incoming (max %1$s)"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:111
-#, c-format
-msgid "current stock will change from %1$s to %2$s"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:112
-#, c-format
-msgid "current stock will stay at %1$s"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:129
-#: src/paths/instance/products/list/Table.tsx:204
-#, c-format
-msgid "Incoming"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:130
-#: src/paths/instance/products/list/Table.tsx:205
-#, c-format
-msgid "Lost"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:142
-#, c-format
-msgid "Current"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:145
-#, c-format
-msgid "without stock"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:150
-#, c-format
-msgid "Next restock"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:152
-#, c-format
-msgid "Delivery address"
-msgstr ""
-
-#: src/components/form/InputTaxes.tsx:73
-#, c-format
-msgid "this product has no taxes"
-msgstr ""
-
-#: src/components/form/InputTaxes.tsx:77
-#: src/paths/instance/orders/details/DetailPage.tsx:145
-#: src/paths/instance/orders/details/DetailPage.tsx:296
-#: src/paths/instance/orders/list/Table.tsx:116
-#: src/paths/instance/transfers/create/CreatePage.tsx:84
-#, c-format
-msgid "Amount"
-msgstr ""
-
-#: src/components/form/InputTaxes.tsx:78
-#, c-format
-msgid "currency and value separated with colon"
-msgstr ""
-
-#: src/components/form/InputTaxes.tsx:84
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:78
-#, c-format
-msgid "Add"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:53
-#, c-format
-msgid "Instance"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:59
-#, c-format
-msgid "Settings"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:65
-#: src/paths/instance/orders/list/Table.tsx:60
-#, c-format
-msgid "Orders"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:71
-#: src/paths/instance/orders/create/CreatePage.tsx:258
-#: src/paths/instance/products/list/Table.tsx:48
-#, c-format
-msgid "Products"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:77
-#: src/paths/instance/transfers/list/Table.tsx:65
-#, c-format
-msgid "Transfers"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:87
-#, c-format
-msgid "Connection"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:112 src/paths/admin/list/Table.tsx:57
-#, c-format
-msgid "Instances"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:116
-#, c-format
-msgid "New"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:122
-#, c-format
-msgid "List"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:129
-#, c-format
-msgid "Log out"
-msgstr ""
-
-#: src/components/modal/index.tsx:74
-#, c-format
-msgid "Clear"
-msgstr ""
-
-#: src/components/modal/index.tsx:110 src/components/modal/index.tsx:111
-#, c-format
-msgid "should be the same"
-msgstr ""
-
-#: src/components/modal/index.tsx:111
-#, c-format
-msgid "cannot be the same as before"
-msgstr ""
-
-#: src/components/modal/index.tsx:114
-#, c-format
-msgid ""
-"You are updating the authorization token from instance %1$s with id %2$s"
-msgstr ""
-
-#: src/components/modal/index.tsx:124
-#, c-format
-msgid "Old token"
-msgstr ""
-
-#: src/components/modal/index.tsx:125
-#, c-format
-msgid "New token"
-msgstr ""
-
-#: src/components/modal/index.tsx:127
-#, c-format
-msgid "Clearing the auth token will mean public access to the instance"
-msgstr ""
-
-#: src/components/product/ProductForm.tsx:96
-#: src/paths/admin/create/CreatePage.tsx:85 src/paths/admin/list/Table.tsx:109
-#: src/paths/instance/transfers/list/Table.tsx:122
-#, c-format
-msgid "ID"
-msgstr ""
-
-#: src/components/product/ProductForm.tsx:98
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:121
-#: src/paths/instance/products/list/Table.tsx:85
-#, c-format
-msgid "Image"
-msgstr ""
-
-#: src/components/product/ProductForm.tsx:100
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:123
-#, c-format
-msgid "Unit"
-msgstr ""
-
-#: src/components/product/ProductForm.tsx:101
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:124
-#: src/paths/instance/products/list/Table.tsx:162
-#: src/paths/instance/products/list/Table.tsx:214
-#, c-format
-msgid "Price"
-msgstr ""
-
-#: src/components/product/ProductForm.tsx:103
-#: src/paths/instance/products/list/Table.tsx:90
-#, c-format
-msgid "Stock"
-msgstr ""
-
-#: src/components/product/ProductForm.tsx:105
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:128
-#: src/paths/instance/products/list/Table.tsx:88
-#, c-format
-msgid "Taxes"
-msgstr ""
-
-#: src/index.tsx:75
-#, c-format
-msgid "Server not found"
-msgstr ""
-
-#: src/index.tsx:85
-#, c-format
-msgid "Couldn't access the server"
-msgstr ""
-
-#: src/index.tsx:87 src/index.tsx:99
-#, c-format
-msgid "Got message %1$s from %2$s"
-msgstr ""
-
-#: src/index.tsx:97
-#, c-format
-msgid "Unexpected Error"
-msgstr ""
-
-#: src/paths/admin/create/CreatePage.tsx:89
-#: src/paths/instance/update/UpdatePage.tsx:108
-#, c-format
-msgid "Auth token"
-msgstr ""
-
-#: src/paths/admin/create/CreatePage.tsx:91
-#: src/paths/instance/details/DetailPage.tsx:77
-#: src/paths/instance/update/UpdatePage.tsx:110
-#, c-format
-msgid "Account address"
-msgstr ""
-
-#: src/paths/admin/create/CreatePage.tsx:93
-#: src/paths/instance/update/UpdatePage.tsx:112
-#, c-format
-msgid "Default max deposit fee"
-msgstr ""
-
-#: src/paths/admin/create/CreatePage.tsx:95
-#: src/paths/instance/update/UpdatePage.tsx:114
-#, c-format
-msgid "Default max wire fee"
-msgstr ""
-
-#: src/paths/admin/create/CreatePage.tsx:97
-#: src/paths/instance/update/UpdatePage.tsx:116
-#, c-format
-msgid "Default wire fee amortization"
-msgstr ""
-
-#: src/paths/admin/create/CreatePage.tsx:103
-#: src/paths/instance/update/UpdatePage.tsx:122
-#, c-format
-msgid "Jurisdiction"
-msgstr ""
-
-#: src/paths/admin/create/CreatePage.tsx:107
-#: src/paths/instance/update/UpdatePage.tsx:126
-#, c-format
-msgid "Default pay delay"
-msgstr ""
-
-#: src/paths/admin/create/CreatePage.tsx:109
-#: src/paths/instance/update/UpdatePage.tsx:128
-#, c-format
-msgid "Default wire transfer delay"
-msgstr ""
-
-#: src/paths/admin/create/index.tsx:58
-#, c-format
-msgid "could not create instance"
-msgstr ""
-
-#: src/paths/admin/list/Table.tsx:63 src/paths/admin/list/Table.tsx:131
-#: src/paths/instance/transfers/list/Table.tsx:71
-#, c-format
-msgid "Delete"
-msgstr ""
-
-#: src/paths/admin/list/Table.tsx:128
-#, c-format
-msgid "Edit"
-msgstr ""
-
-#: src/paths/admin/list/Table.tsx:149
-#: src/paths/instance/products/list/Table.tsx:245
-#, c-format
-msgid "There is no instances yet, add more pressing the + sign"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:237
-#, c-format
-msgid "Inventory products"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:286
-#, c-format
-msgid "Total price"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:287
-#, c-format
-msgid "Total tax"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:289
-#: src/paths/instance/orders/create/CreatePage.tsx:297
-#, c-format
-msgid "Order price"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:295
-#, c-format
-msgid "Net"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:300
-#: src/paths/instance/orders/details/DetailPage.tsx:144
-#: src/paths/instance/orders/details/DetailPage.tsx:295
-#: src/paths/instance/orders/list/Table.tsx:117
-#, c-format
-msgid "Summary"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:302
-#, c-format
-msgid "Payments options"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:303
-#, c-format
-msgid "Auto refund deadline"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:304
-#, c-format
-msgid "Refund deadline"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:305
-#, c-format
-msgid "Pay deadline"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:307
-#, c-format
-msgid "Delivery date"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:308
-#, c-format
-msgid "Location"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:312
-#, c-format
-msgid "Max fee"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:313
-#, c-format
-msgid "Max wire fee"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:314
-#, c-format
-msgid "Wire fee amortization"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:315
-#, c-format
-msgid "Fullfilment url"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:318
-#, c-format
-msgid "Extra information"
-msgstr ""
-
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:44
-#, c-format
-msgid "select a product first"
-msgstr ""
-
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:51
-#, c-format
-msgid "should be greater than 0"
-msgstr ""
-
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:58
-#, c-format
-msgid ""
-"cannot be greater than current stock and quantity previously added. max: %1$s"
-msgstr ""
-
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:64
-#, c-format
-msgid "cannot be greater than current stock %1$s"
-msgstr ""
-
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:76
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:126
-#, c-format
-msgid "Quantity"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:92
-#: src/paths/instance/orders/details/DetailPage.tsx:235
-#: src/paths/instance/orders/details/DetailPage.tsx:333
-#, c-format
-msgid "Order"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:93
-#, c-format
-msgid "claimed"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:110
-#: src/paths/instance/orders/details/DetailPage.tsx:261
-#: src/paths/instance/orders/list/Table.tsx:136
-#, c-format
-msgid "copy url"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:126
-#: src/paths/instance/orders/details/DetailPage.tsx:349
-#, c-format
-msgid "pay at"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:127
-#: src/paths/instance/orders/details/DetailPage.tsx:350
-#, c-format
-msgid "created at"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:138
-#: src/paths/instance/orders/details/DetailPage.tsx:289
-#, c-format
-msgid "Timeline"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:142
-#: src/paths/instance/orders/details/DetailPage.tsx:293
-#, c-format
-msgid "Payment details"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:146
-#: src/paths/instance/orders/details/DetailPage.tsx:299
-#: src/paths/instance/orders/details/DetailPage.tsx:363
-#, c-format
-msgid "Order status"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:156
-#: src/paths/instance/orders/details/DetailPage.tsx:308
-#, c-format
-msgid "Product list"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:236
-#, c-format
-msgid "paid"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:238
-#, c-format
-msgid "wired"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:241
-#, c-format
-msgid "refunded"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:258
-#, c-format
-msgid "refund"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:297
-#, c-format
-msgid "Refunded amount"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:298
-#, c-format
-msgid "Deposit total"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:336
-#, c-format
-msgid "unpaid"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:364
-#, c-format
-msgid "Order status URL"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:365
-#, c-format
-msgid "Pay URI"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:383
-#, c-format
-msgid ""
-"Unknown order status. This is an error, please contact the administrator."
-msgstr ""
-
-#: src/paths/instance/orders/details/index.tsx:56
-#: src/paths/instance/orders/list/index.tsx:147
-#, c-format
-msgid "refund created successfully"
-msgstr ""
-
-#: src/paths/instance/orders/details/index.tsx:59
-#: src/paths/instance/orders/list/index.tsx:150
-#, c-format
-msgid "could not create the refund"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:111
-#, c-format
-msgid "load newer orders"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:115
-#, c-format
-msgid "Date"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:131
-#: src/paths/instance/orders/list/Table.tsx:223
-#, c-format
-msgid "Refund"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:145
-#, c-format
-msgid "load older orders"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:154
-#, c-format
-msgid "No orders has been found"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:202
-#, c-format
-msgid "date"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:203
-#, c-format
-msgid "amount"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:204
-#, c-format
-msgid "reason"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:224
-#, c-format
-msgid "Max refundable:"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:226
-#, c-format
-msgid "Reason"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:226
-#, c-format
-msgid "duplicated"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:226
-#, c-format
-msgid "requested by the customer"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:226
-#, c-format
-msgid "other"
-msgstr ""
-
-#: src/paths/instance/orders/list/index.tsx:91
-#, c-format
-msgid "go to order id"
-msgstr ""
-
-#: src/paths/instance/orders/list/index.tsx:107
-#, c-format
-msgid "Paid"
-msgstr ""
-
-#: src/paths/instance/orders/list/index.tsx:108
-#, c-format
-msgid "Refunded"
-msgstr ""
-
-#: src/paths/instance/orders/list/index.tsx:109
-#, c-format
-msgid "Not wired"
-msgstr ""
-
-#: src/paths/instance/orders/list/index.tsx:110
-#, c-format
-msgid "All"
-msgstr ""
-
-#: src/paths/instance/products/create/index.tsx:48
-#: src/paths/instance/products/update/index.tsx:64
-#, c-format
-msgid "could not create product"
-msgstr ""
-
-#: src/paths/instance/products/list/Table.tsx:87
-#, c-format
-msgid "Sell"
-msgstr ""
-
-#: src/paths/instance/products/list/Table.tsx:89
-#, c-format
-msgid "Profit"
-msgstr ""
-
-#: src/paths/instance/products/list/Table.tsx:91
-#, c-format
-msgid "Sold"
-msgstr ""
-
-#: src/paths/instance/products/list/index.tsx:59
-#, c-format
-msgid "product updated successfully"
-msgstr ""
-
-#: src/paths/instance/products/list/index.tsx:62
-#, c-format
-msgid "could not update the product"
-msgstr ""
-
-#: src/paths/instance/products/list/index.tsx:70
-#, c-format
-msgid "product delete successfully"
-msgstr ""
-
-#: src/paths/instance/products/list/index.tsx:73
-#, c-format
-msgid "could not delete the product"
-msgstr ""
-
-#: src/paths/instance/tips/list/Table.tsx:59
-#, c-format
-msgid "Tips"
-msgstr ""
-
-#: src/paths/instance/tips/list/Table.tsx:111
-#, c-format
-msgid "Committed amount"
-msgstr ""
-
-#: src/paths/instance/tips/list/Table.tsx:112
-#, c-format
-msgid "Exchange initial amount"
-msgstr ""
-
-#: src/paths/instance/tips/list/Table.tsx:113
-#, c-format
-msgid "Merchant initial amount"
-msgstr ""
-
-#: src/paths/instance/tips/list/Table.tsx:148
-#, c-format
-msgid "There is no tips yet, add more pressing the + sign"
-msgstr ""
-
-#: src/paths/instance/transfers/create/CreatePage.tsx:50
-#: src/paths/instance/transfers/create/CreatePage.tsx:54
-#: src/paths/instance/transfers/create/CreatePage.tsx:55
-#: src/paths/instance/transfers/create/CreatePage.tsx:56
-#, c-format
-msgid "cannot be empty"
-msgstr ""
-
-#: src/paths/instance/transfers/create/CreatePage.tsx:51
-#, c-format
-msgid "check the id, doest look valid"
-msgstr ""
-
-#: src/paths/instance/transfers/create/CreatePage.tsx:52
-#, c-format
-msgid "should have 52 characters, current %1$s"
-msgstr ""
-
-#: src/paths/instance/transfers/create/CreatePage.tsx:57
-#, c-format
-msgid "URL doesn't have the right format"
-msgstr ""
-
-#: src/paths/instance/transfers/create/CreatePage.tsx:74
-#, c-format
-msgid "Transfer ID"
-msgstr ""
-
-#: src/paths/instance/transfers/create/CreatePage.tsx:76
-#, c-format
-msgid "Account Address"
-msgstr ""
-
-#: src/paths/instance/transfers/create/CreatePage.tsx:82
-#: src/paths/instance/transfers/list/Table.tsx:125
-#, c-format
-msgid "Exchange URL"
-msgstr ""
-
-#: src/paths/instance/transfers/create/index.tsx:49
-#, c-format
-msgid "could not inform transfer"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:118
-#, c-format
-msgid "load newer transfers"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:123
-#, c-format
-msgid "Credit"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:126
-#, c-format
-msgid "Confirmed"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:127
-#: src/paths/instance/transfers/list/index.tsx:60
-#, c-format
-msgid "Verified"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:128
-#, c-format
-msgid "Executed at"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:138
-#: src/paths/instance/transfers/list/Table.tsx:139
-#, c-format
-msgid "yes"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:138
-#: src/paths/instance/transfers/list/Table.tsx:139
-#, c-format
-msgid "no"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:140
-#, c-format
-msgid "unknown"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:145
-#, c-format
-msgid "load older transfers"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:154
-#, c-format
-msgid "There is no transfer yet, add more pressing the + sign"
-msgstr ""
diff --git a/packages/merchant-backend-ui/src/i18n/en.po b/packages/merchant-backend-ui/src/i18n/en.po
deleted file mode 100644
index 6b35bd0ce..000000000
--- a/packages/merchant-backend-ui/src/i18n/en.po
+++ /dev/null
@@ -1,1057 +0,0 @@
-# This file is part of TALER
-# (C) 2016 GNUnet e.V.
-#
-# TALER is free software; you can redistribute it and/or modify it under the
-# terms of the GNU General Public License as published by the Free Software
-# Foundation; either version 3, or (at your option) any later version.
-#
-# TALER is distributed in the hope that it will be useful, but WITHOUT ANY
-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
-# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
-#
-#, fuzzy
-msgid ""
-msgstr ""
-"Project-Id-Version: Taler Wallet\n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-11-23 00:00+0100\n"
-"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
-"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
-"Language-Team: LANGUAGE <LL@li.org>\n"
-"Language: \n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-
-#: src/ApplicationReadyRoutes.tsx:50 src/InstanceRoutes.tsx:118
-#: src/InstanceRoutes.tsx:299
-#, c-format
-msgid "Access denied"
-msgstr ""
-
-#: src/ApplicationReadyRoutes.tsx:51 src/InstanceRoutes.tsx:118
-#: src/InstanceRoutes.tsx:300
-#, c-format
-msgid "Check your token is valid"
-msgstr ""
-
-#: src/ApplicationReadyRoutes.tsx:72
-#, c-format
-msgid "Couldn't access the server."
-msgstr ""
-
-#: src/ApplicationReadyRoutes.tsx:73
-#, c-format
-msgid "Could not infer instance id from url %1$s"
-msgstr ""
-
-#: src/InstanceRoutes.tsx:109
-#, c-format
-msgid "HTTP status #%1$s: Server reported a problem"
-msgstr ""
-
-#: src/InstanceRoutes.tsx:110
-#, c-format
-msgid "Got message: \"%1$s\" from: %2$s"
-msgstr ""
-
-#: src/InstanceRoutes.tsx:127
-#, c-format
-msgid "No default instance"
-msgstr ""
-
-#: src/InstanceRoutes.tsx:128
-#, c-format
-msgid ""
-"in order to use merchant backoffice, you should create the default instance"
-msgstr ""
-
-#: src/InstanceRoutes.tsx:288
-#, c-format
-msgid "Server reported a problem: HTTP status #%1$s"
-msgstr ""
-
-#: src/InstanceRoutes.tsx:289
-#, c-format
-msgid "Got message: %1$s from: %2$s"
-msgstr ""
-
-#: src/components/exception/login.tsx:46
-#, c-format
-msgid "Login required"
-msgstr ""
-
-#: src/components/exception/login.tsx:49
-#, c-format
-msgid ""
-"Please enter your auth token. Token should have \"secret-token:\" and start "
-"with Bearer or ApiKey"
-msgstr ""
-
-#: src/components/exception/login.tsx:86 src/components/modal/index.tsx:53
-#: src/components/modal/index.tsx:75 src/paths/admin/create/CreatePage.tsx:115
-#: src/paths/instance/orders/create/CreatePage.tsx:325
-#: src/paths/instance/products/create/CreatePage.tsx:51
-#: src/paths/instance/products/list/Table.tsx:174
-#: src/paths/instance/products/list/Table.tsx:228
-#: src/paths/instance/products/update/UpdatePage.tsx:55
-#: src/paths/instance/transfers/create/CreatePage.tsx:89
-#: src/paths/instance/update/UpdatePage.tsx:134
-#, c-format
-msgid "Confirm"
-msgstr ""
-
-#: src/components/form/InputArray.tsx:72
-#, c-format
-msgid "The value %1$s is invalid for a payment url"
-msgstr ""
-
-#: src/components/form/InputDate.tsx:67
-#: src/paths/instance/orders/list/index.tsx:123
-#, c-format
-msgid "pick a date"
-msgstr ""
-
-#: src/components/form/InputDate.tsx:81
-#, c-format
-msgid "clear"
-msgstr ""
-
-#: src/components/form/InputDate.tsx:83
-#: src/paths/instance/transfers/list/Table.tsx:140
-#, c-format
-msgid "never"
-msgstr ""
-
-#: src/components/form/InputImage.tsx:80
-#, c-format
-msgid "Image should be smaller than 1 MB"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:28
-#, c-format
-msgid "Country"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:30
-#: src/paths/admin/create/CreatePage.tsx:99
-#: src/paths/instance/transfers/list/Table.tsx:124
-#: src/paths/instance/update/UpdatePage.tsx:118
-#, c-format
-msgid "Address"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:34
-#, c-format
-msgid "Building number"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:35
-#, c-format
-msgid "Building name"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:36
-#, c-format
-msgid "Street"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:37
-#, c-format
-msgid "Post code"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:38
-#, c-format
-msgid "Town location"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:39
-#, c-format
-msgid "Town"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:40
-#, c-format
-msgid "District"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:41
-#, c-format
-msgid "Country subdivision"
-msgstr ""
-
-#: src/components/form/InputSearchProduct.tsx:59
-#, c-format
-msgid "Product id"
-msgstr ""
-
-#: src/components/form/InputSearchProduct.tsx:60
-#: src/components/product/ProductForm.tsx:99
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:122
-#: src/paths/instance/orders/list/Table.tsx:227
-#: src/paths/instance/products/list/Table.tsx:86
-#, c-format
-msgid "Description"
-msgstr ""
-
-#: src/components/form/InputSearchProduct.tsx:73
-#: src/components/form/InputTaxes.tsx:81
-#: src/paths/admin/create/CreatePage.tsx:87 src/paths/admin/list/Table.tsx:110
-#: src/paths/instance/details/DetailPage.tsx:76
-#: src/paths/instance/update/UpdatePage.tsx:106
-#, c-format
-msgid "Name"
-msgstr ""
-
-#: src/components/form/InputSearchProduct.tsx:102
-#, c-format
-msgid "loading..."
-msgstr ""
-
-#: src/components/form/InputSearchProduct.tsx:108
-#, c-format
-msgid "no products found"
-msgstr ""
-
-#: src/components/form/InputSearchProduct.tsx:116
-#, c-format
-msgid "no results"
-msgstr ""
-
-#: src/components/form/InputSecured.tsx:33
-#, c-format
-msgid "Deleting"
-msgstr ""
-
-#: src/components/form/InputSecured.tsx:34
-#, c-format
-msgid "Changing"
-msgstr ""
-
-#: src/components/form/InputSecured.tsx:60
-#, c-format
-msgid "Manage token"
-msgstr ""
-
-#: src/components/form/InputSecured.tsx:83
-#, c-format
-msgid "Update"
-msgstr ""
-
-#: src/components/form/InputSecured.tsx:100
-#: src/paths/instance/orders/create/CreatePage.tsx:252
-#: src/paths/instance/orders/create/CreatePage.tsx:273
-#, c-format
-msgid "Remove"
-msgstr ""
-
-#: src/components/form/InputSecured.tsx:106 src/components/modal/index.tsx:52
-#: src/components/modal/index.tsx:73 src/paths/admin/create/CreatePage.tsx:114
-#: src/paths/instance/orders/create/CreatePage.tsx:324
-#: src/paths/instance/products/create/CreatePage.tsx:50
-#: src/paths/instance/products/list/Table.tsx:166
-#: src/paths/instance/products/list/Table.tsx:218
-#: src/paths/instance/products/update/UpdatePage.tsx:54
-#: src/paths/instance/transfers/create/CreatePage.tsx:88
-#: src/paths/instance/update/UpdatePage.tsx:133
-#, c-format
-msgid "Cancel"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:91
-#, c-format
-msgid "Manage stock"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:93
-#, c-format
-msgid "Infinite"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:105
-#, c-format
-msgid "lost cannot be greater that current + incoming (max %1$s)"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:111
-#, c-format
-msgid "current stock will change from %1$s to %2$s"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:112
-#, c-format
-msgid "current stock will stay at %1$s"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:129
-#: src/paths/instance/products/list/Table.tsx:204
-#, c-format
-msgid "Incoming"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:130
-#: src/paths/instance/products/list/Table.tsx:205
-#, c-format
-msgid "Lost"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:142
-#, c-format
-msgid "Current"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:145
-#, c-format
-msgid "without stock"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:150
-#, c-format
-msgid "Next restock"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:152
-#, c-format
-msgid "Delivery address"
-msgstr ""
-
-#: src/components/form/InputTaxes.tsx:73
-#, c-format
-msgid "this product has no taxes"
-msgstr ""
-
-#: src/components/form/InputTaxes.tsx:77
-#: src/paths/instance/orders/details/DetailPage.tsx:145
-#: src/paths/instance/orders/details/DetailPage.tsx:296
-#: src/paths/instance/orders/list/Table.tsx:116
-#: src/paths/instance/transfers/create/CreatePage.tsx:84
-#, c-format
-msgid "Amount"
-msgstr ""
-
-#: src/components/form/InputTaxes.tsx:78
-#, c-format
-msgid "currency and value separated with colon"
-msgstr ""
-
-#: src/components/form/InputTaxes.tsx:84
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:78
-#, c-format
-msgid "Add"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:53
-#, c-format
-msgid "Instance"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:59
-#, c-format
-msgid "Settings"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:65
-#: src/paths/instance/orders/list/Table.tsx:60
-#, c-format
-msgid "Orders"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:71
-#: src/paths/instance/orders/create/CreatePage.tsx:258
-#: src/paths/instance/products/list/Table.tsx:48
-#, c-format
-msgid "Products"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:77
-#: src/paths/instance/transfers/list/Table.tsx:65
-#, c-format
-msgid "Transfers"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:87
-#, c-format
-msgid "Connection"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:112 src/paths/admin/list/Table.tsx:57
-#, c-format
-msgid "Instances"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:116
-#, c-format
-msgid "New"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:122
-#, c-format
-msgid "List"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:129
-#, c-format
-msgid "Log out"
-msgstr ""
-
-#: src/components/modal/index.tsx:74
-#, c-format
-msgid "Clear"
-msgstr ""
-
-#: src/components/modal/index.tsx:110 src/components/modal/index.tsx:111
-#, c-format
-msgid "should be the same"
-msgstr ""
-
-#: src/components/modal/index.tsx:111
-#, c-format
-msgid "cannot be the same as before"
-msgstr ""
-
-#: src/components/modal/index.tsx:114
-#, c-format
-msgid ""
-"You are updating the authorization token from instance %1$s with id %2$s"
-msgstr ""
-
-#: src/components/modal/index.tsx:124
-#, c-format
-msgid "Old token"
-msgstr ""
-
-#: src/components/modal/index.tsx:125
-#, c-format
-msgid "New token"
-msgstr ""
-
-#: src/components/modal/index.tsx:127
-#, c-format
-msgid "Clearing the auth token will mean public access to the instance"
-msgstr ""
-
-#: src/components/product/ProductForm.tsx:96
-#: src/paths/admin/create/CreatePage.tsx:85 src/paths/admin/list/Table.tsx:109
-#: src/paths/instance/transfers/list/Table.tsx:122
-#, c-format
-msgid "ID"
-msgstr ""
-
-#: src/components/product/ProductForm.tsx:98
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:121
-#: src/paths/instance/products/list/Table.tsx:85
-#, c-format
-msgid "Image"
-msgstr ""
-
-#: src/components/product/ProductForm.tsx:100
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:123
-#, c-format
-msgid "Unit"
-msgstr ""
-
-#: src/components/product/ProductForm.tsx:101
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:124
-#: src/paths/instance/products/list/Table.tsx:162
-#: src/paths/instance/products/list/Table.tsx:214
-#, c-format
-msgid "Price"
-msgstr ""
-
-#: src/components/product/ProductForm.tsx:103
-#: src/paths/instance/products/list/Table.tsx:90
-#, c-format
-msgid "Stock"
-msgstr ""
-
-#: src/components/product/ProductForm.tsx:105
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:128
-#: src/paths/instance/products/list/Table.tsx:88
-#, c-format
-msgid "Taxes"
-msgstr ""
-
-#: src/index.tsx:75
-#, c-format
-msgid "Server not found"
-msgstr ""
-
-#: src/index.tsx:85
-#, c-format
-msgid "Couldn't access the server"
-msgstr ""
-
-#: src/index.tsx:87 src/index.tsx:99
-#, c-format
-msgid "Got message %1$s from %2$s"
-msgstr ""
-
-#: src/index.tsx:97
-#, c-format
-msgid "Unexpected Error"
-msgstr ""
-
-#: src/paths/admin/create/CreatePage.tsx:89
-#: src/paths/instance/update/UpdatePage.tsx:108
-#, c-format
-msgid "Auth token"
-msgstr ""
-
-#: src/paths/admin/create/CreatePage.tsx:91
-#: src/paths/instance/details/DetailPage.tsx:77
-#: src/paths/instance/update/UpdatePage.tsx:110
-#, c-format
-msgid "Account address"
-msgstr ""
-
-#: src/paths/admin/create/CreatePage.tsx:93
-#: src/paths/instance/update/UpdatePage.tsx:112
-#, c-format
-msgid "Default max deposit fee"
-msgstr ""
-
-#: src/paths/admin/create/CreatePage.tsx:95
-#: src/paths/instance/update/UpdatePage.tsx:114
-#, c-format
-msgid "Default max wire fee"
-msgstr ""
-
-#: src/paths/admin/create/CreatePage.tsx:97
-#: src/paths/instance/update/UpdatePage.tsx:116
-#, c-format
-msgid "Default wire fee amortization"
-msgstr ""
-
-#: src/paths/admin/create/CreatePage.tsx:103
-#: src/paths/instance/update/UpdatePage.tsx:122
-#, c-format
-msgid "Jurisdiction"
-msgstr ""
-
-#: src/paths/admin/create/CreatePage.tsx:107
-#: src/paths/instance/update/UpdatePage.tsx:126
-#, c-format
-msgid "Default pay delay"
-msgstr ""
-
-#: src/paths/admin/create/CreatePage.tsx:109
-#: src/paths/instance/update/UpdatePage.tsx:128
-#, c-format
-msgid "Default wire transfer delay"
-msgstr ""
-
-#: src/paths/admin/create/index.tsx:58
-#, c-format
-msgid "could not create instance"
-msgstr ""
-
-#: src/paths/admin/list/Table.tsx:63 src/paths/admin/list/Table.tsx:131
-#: src/paths/instance/transfers/list/Table.tsx:71
-#, c-format
-msgid "Delete"
-msgstr ""
-
-#: src/paths/admin/list/Table.tsx:128
-#, c-format
-msgid "Edit"
-msgstr ""
-
-#: src/paths/admin/list/Table.tsx:149
-#: src/paths/instance/products/list/Table.tsx:245
-#, c-format
-msgid "There is no instances yet, add more pressing the + sign"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:237
-#, c-format
-msgid "Inventory products"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:286
-#, c-format
-msgid "Total price"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:287
-#, c-format
-msgid "Total tax"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:289
-#: src/paths/instance/orders/create/CreatePage.tsx:297
-#, c-format
-msgid "Order price"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:295
-#, c-format
-msgid "Net"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:300
-#: src/paths/instance/orders/details/DetailPage.tsx:144
-#: src/paths/instance/orders/details/DetailPage.tsx:295
-#: src/paths/instance/orders/list/Table.tsx:117
-#, c-format
-msgid "Summary"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:302
-#, c-format
-msgid "Payments options"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:303
-#, c-format
-msgid "Auto refund deadline"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:304
-#, c-format
-msgid "Refund deadline"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:305
-#, c-format
-msgid "Pay deadline"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:307
-#, c-format
-msgid "Delivery date"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:308
-#, c-format
-msgid "Location"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:312
-#, c-format
-msgid "Max fee"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:313
-#, c-format
-msgid "Max wire fee"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:314
-#, c-format
-msgid "Wire fee amortization"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:315
-#, c-format
-msgid "Fullfilment url"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:318
-#, c-format
-msgid "Extra information"
-msgstr ""
-
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:44
-#, c-format
-msgid "select a product first"
-msgstr ""
-
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:51
-#, c-format
-msgid "should be greater than 0"
-msgstr ""
-
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:58
-#, c-format
-msgid ""
-"cannot be greater than current stock and quantity previously added. max: %1$s"
-msgstr ""
-
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:64
-#, c-format
-msgid "cannot be greater than current stock %1$s"
-msgstr ""
-
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:76
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:126
-#, c-format
-msgid "Quantity"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:92
-#: src/paths/instance/orders/details/DetailPage.tsx:235
-#: src/paths/instance/orders/details/DetailPage.tsx:333
-#, c-format
-msgid "Order"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:93
-#, c-format
-msgid "claimed"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:110
-#: src/paths/instance/orders/details/DetailPage.tsx:261
-#: src/paths/instance/orders/list/Table.tsx:136
-#, c-format
-msgid "copy url"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:126
-#: src/paths/instance/orders/details/DetailPage.tsx:349
-#, c-format
-msgid "pay at"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:127
-#: src/paths/instance/orders/details/DetailPage.tsx:350
-#, c-format
-msgid "created at"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:138
-#: src/paths/instance/orders/details/DetailPage.tsx:289
-#, c-format
-msgid "Timeline"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:142
-#: src/paths/instance/orders/details/DetailPage.tsx:293
-#, c-format
-msgid "Payment details"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:146
-#: src/paths/instance/orders/details/DetailPage.tsx:299
-#: src/paths/instance/orders/details/DetailPage.tsx:363
-#, c-format
-msgid "Order status"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:156
-#: src/paths/instance/orders/details/DetailPage.tsx:308
-#, c-format
-msgid "Product list"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:236
-#, c-format
-msgid "paid"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:238
-#, c-format
-msgid "wired"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:241
-#, c-format
-msgid "refunded"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:258
-#, c-format
-msgid "refund"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:297
-#, c-format
-msgid "Refunded amount"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:298
-#, c-format
-msgid "Deposit total"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:336
-#, c-format
-msgid "unpaid"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:364
-#, c-format
-msgid "Order status URL"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:365
-#, c-format
-msgid "Pay URI"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:383
-#, c-format
-msgid ""
-"Unknown order status. This is an error, please contact the administrator."
-msgstr ""
-
-#: src/paths/instance/orders/details/index.tsx:56
-#: src/paths/instance/orders/list/index.tsx:147
-#, c-format
-msgid "refund created successfully"
-msgstr ""
-
-#: src/paths/instance/orders/details/index.tsx:59
-#: src/paths/instance/orders/list/index.tsx:150
-#, c-format
-msgid "could not create the refund"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:111
-#, c-format
-msgid "load newer orders"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:115
-#, c-format
-msgid "Date"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:131
-#: src/paths/instance/orders/list/Table.tsx:223
-#, c-format
-msgid "Refund"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:145
-#, c-format
-msgid "load older orders"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:154
-#, c-format
-msgid "No orders has been found"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:202
-#, c-format
-msgid "date"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:203
-#, c-format
-msgid "amount"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:204
-#, c-format
-msgid "reason"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:224
-#, c-format
-msgid "Max refundable:"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:226
-#, c-format
-msgid "Reason"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:226
-#, c-format
-msgid "duplicated"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:226
-#, c-format
-msgid "requested by the customer"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:226
-#, c-format
-msgid "other"
-msgstr ""
-
-#: src/paths/instance/orders/list/index.tsx:91
-#, c-format
-msgid "go to order id"
-msgstr ""
-
-#: src/paths/instance/orders/list/index.tsx:107
-#, c-format
-msgid "Paid"
-msgstr ""
-
-#: src/paths/instance/orders/list/index.tsx:108
-#, c-format
-msgid "Refunded"
-msgstr ""
-
-#: src/paths/instance/orders/list/index.tsx:109
-#, c-format
-msgid "Not wired"
-msgstr ""
-
-#: src/paths/instance/orders/list/index.tsx:110
-#, c-format
-msgid "All"
-msgstr ""
-
-#: src/paths/instance/products/create/index.tsx:48
-#: src/paths/instance/products/update/index.tsx:64
-#, c-format
-msgid "could not create product"
-msgstr ""
-
-#: src/paths/instance/products/list/Table.tsx:87
-#, c-format
-msgid "Sell"
-msgstr ""
-
-#: src/paths/instance/products/list/Table.tsx:89
-#, c-format
-msgid "Profit"
-msgstr ""
-
-#: src/paths/instance/products/list/Table.tsx:91
-#, c-format
-msgid "Sold"
-msgstr ""
-
-#: src/paths/instance/products/list/index.tsx:59
-#, c-format
-msgid "product updated successfully"
-msgstr ""
-
-#: src/paths/instance/products/list/index.tsx:62
-#, c-format
-msgid "could not update the product"
-msgstr ""
-
-#: src/paths/instance/products/list/index.tsx:70
-#, c-format
-msgid "product delete successfully"
-msgstr ""
-
-#: src/paths/instance/products/list/index.tsx:73
-#, c-format
-msgid "could not delete the product"
-msgstr ""
-
-#: src/paths/instance/tips/list/Table.tsx:59
-#, c-format
-msgid "Tips"
-msgstr ""
-
-#: src/paths/instance/tips/list/Table.tsx:111
-#, c-format
-msgid "Committed amount"
-msgstr ""
-
-#: src/paths/instance/tips/list/Table.tsx:112
-#, c-format
-msgid "Exchange initial amount"
-msgstr ""
-
-#: src/paths/instance/tips/list/Table.tsx:113
-#, c-format
-msgid "Merchant initial amount"
-msgstr ""
-
-#: src/paths/instance/tips/list/Table.tsx:148
-#, c-format
-msgid "There is no tips yet, add more pressing the + sign"
-msgstr ""
-
-#: src/paths/instance/transfers/create/CreatePage.tsx:50
-#: src/paths/instance/transfers/create/CreatePage.tsx:54
-#: src/paths/instance/transfers/create/CreatePage.tsx:55
-#: src/paths/instance/transfers/create/CreatePage.tsx:56
-#, c-format
-msgid "cannot be empty"
-msgstr ""
-
-#: src/paths/instance/transfers/create/CreatePage.tsx:51
-#, c-format
-msgid "check the id, doest look valid"
-msgstr ""
-
-#: src/paths/instance/transfers/create/CreatePage.tsx:52
-#, c-format
-msgid "should have 52 characters, current %1$s"
-msgstr ""
-
-#: src/paths/instance/transfers/create/CreatePage.tsx:57
-#, c-format
-msgid "URL doesn't have the right format"
-msgstr ""
-
-#: src/paths/instance/transfers/create/CreatePage.tsx:74
-#, c-format
-msgid "Transfer ID"
-msgstr ""
-
-#: src/paths/instance/transfers/create/CreatePage.tsx:76
-#, c-format
-msgid "Account Address"
-msgstr ""
-
-#: src/paths/instance/transfers/create/CreatePage.tsx:82
-#: src/paths/instance/transfers/list/Table.tsx:125
-#, c-format
-msgid "Exchange URL"
-msgstr ""
-
-#: src/paths/instance/transfers/create/index.tsx:49
-#, c-format
-msgid "could not inform transfer"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:118
-#, c-format
-msgid "load newer transfers"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:123
-#, c-format
-msgid "Credit"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:126
-#, c-format
-msgid "Confirmed"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:127
-#: src/paths/instance/transfers/list/index.tsx:60
-#, c-format
-msgid "Verified"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:128
-#, c-format
-msgid "Executed at"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:138
-#: src/paths/instance/transfers/list/Table.tsx:139
-#, c-format
-msgid "yes"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:138
-#: src/paths/instance/transfers/list/Table.tsx:139
-#, c-format
-msgid "no"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:140
-#, c-format
-msgid "unknown"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:145
-#, c-format
-msgid "load older transfers"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:154
-#, c-format
-msgid "There is no transfer yet, add more pressing the + sign"
-msgstr ""
diff --git a/packages/merchant-backend-ui/src/i18n/es.po b/packages/merchant-backend-ui/src/i18n/es.po
deleted file mode 100644
index 9075d4656..000000000
--- a/packages/merchant-backend-ui/src/i18n/es.po
+++ /dev/null
@@ -1,1065 +0,0 @@
-# This file is part of TALER
-# (C) 2016 GNUnet e.V.
-#
-# TALER is free software; you can redistribute it and/or modify it under the
-# terms of the GNU General Public License as published by the Free Software
-# Foundation; either version 3, or (at your option) any later version.
-#
-# TALER is distributed in the hope that it will be useful, but WITHOUT ANY
-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
-# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
-#
-#, fuzzy
-msgid ""
-msgstr ""
-"Project-Id-Version: Taler Wallet\n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-11-23 00:00+0100\n"
-"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
-"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
-"Language-Team: LANGUAGE <LL@li.org>\n"
-"Language: \n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-
-#: src/ApplicationReadyRoutes.tsx:50 src/InstanceRoutes.tsx:118
-#: src/InstanceRoutes.tsx:299
-#, c-format
-msgid "Access denied"
-msgstr "Acceso denegado"
-
-#: src/ApplicationReadyRoutes.tsx:51 src/InstanceRoutes.tsx:118
-#: src/InstanceRoutes.tsx:300
-#, c-format
-msgid "Check your token is valid"
-msgstr "Verifica que el token sea valido"
-
-#: src/ApplicationReadyRoutes.tsx:72
-#, c-format
-msgid "Couldn't access the server."
-msgstr "No se pudo acceder al servidor"
-
-#: src/ApplicationReadyRoutes.tsx:73
-#, c-format
-msgid "Could not infer instance id from url %1$s"
-msgstr "No se pudo inferir el id de la instancia con la url %1$s"
-
-#: src/InstanceRoutes.tsx:109
-#, c-format
-msgid "HTTP status #%1$s: Server reported a problem"
-msgstr "HTTP status #%1$s: Servidor reporto un problema"
-
-#: src/InstanceRoutes.tsx:110
-#, fuzzy, c-format
-msgid "Got message: \"%1$s\" from: %2$s"
-msgstr "Recivimos el mensaje %1$s desde %2$s"
-
-#: src/InstanceRoutes.tsx:127
-#, c-format
-msgid "No default instance"
-msgstr "Sin instancia default"
-
-#: src/InstanceRoutes.tsx:128
-#, c-format
-msgid ""
-"in order to use merchant backoffice, you should create the default instance"
-msgstr "para usar el merchant backoffice, debería crear la instancia default"
-
-#: src/InstanceRoutes.tsx:288
-#, c-format
-msgid "Server reported a problem: HTTP status #%1$s"
-msgstr "Servidir reporto un problema: HTTP status #%1$s"
-
-#: src/InstanceRoutes.tsx:289
-#, fuzzy, c-format
-msgid "Got message: %1$s from: %2$s"
-msgstr "Recivimos el mensaje %1$s desde %2$s"
-
-#: src/components/exception/login.tsx:46
-#, c-format
-msgid "Login required"
-msgstr "Login necesario"
-
-#: src/components/exception/login.tsx:49
-#, c-format
-msgid ""
-"Please enter your auth token. Token should have \"secret-token:\" and start "
-"with Bearer or ApiKey"
-msgstr ""
-"Por favor ingrese su token de autorización. El token debe tener \"secret-"
-"token\" y comenzar con Bearer o ApiKey"
-
-#: src/components/exception/login.tsx:86 src/components/modal/index.tsx:53
-#: src/components/modal/index.tsx:75 src/paths/admin/create/CreatePage.tsx:115
-#: src/paths/instance/orders/create/CreatePage.tsx:325
-#: src/paths/instance/products/create/CreatePage.tsx:51
-#: src/paths/instance/products/list/Table.tsx:174
-#: src/paths/instance/products/list/Table.tsx:228
-#: src/paths/instance/products/update/UpdatePage.tsx:55
-#: src/paths/instance/transfers/create/CreatePage.tsx:89
-#: src/paths/instance/update/UpdatePage.tsx:134
-#, c-format
-msgid "Confirm"
-msgstr "Confirmar"
-
-#: src/components/form/InputArray.tsx:72
-#, c-format
-msgid "The value %1$s is invalid for a payment url"
-msgstr "El valor %1$s es invalido para una URL de pago"
-
-#: src/components/form/InputDate.tsx:67
-#: src/paths/instance/orders/list/index.tsx:123
-#, c-format
-msgid "pick a date"
-msgstr "elegir una fecha"
-
-#: src/components/form/InputDate.tsx:81
-#, fuzzy, c-format
-msgid "clear"
-msgstr "Limpiar"
-
-#: src/components/form/InputDate.tsx:83
-#: src/paths/instance/transfers/list/Table.tsx:140
-#, c-format
-msgid "never"
-msgstr "nunca"
-
-#: src/components/form/InputImage.tsx:80
-#, c-format
-msgid "Image should be smaller than 1 MB"
-msgstr "La imagen debe ser mas chica que 1 MB"
-
-#: src/components/form/InputLocation.tsx:28
-#, c-format
-msgid "Country"
-msgstr "País"
-
-#: src/components/form/InputLocation.tsx:30
-#: src/paths/admin/create/CreatePage.tsx:99
-#: src/paths/instance/transfers/list/Table.tsx:124
-#: src/paths/instance/update/UpdatePage.tsx:118
-#, c-format
-msgid "Address"
-msgstr "Dirección"
-
-#: src/components/form/InputLocation.tsx:34
-#, c-format
-msgid "Building number"
-msgstr "Número de edificio"
-
-#: src/components/form/InputLocation.tsx:35
-#, c-format
-msgid "Building name"
-msgstr "Nombre de edificio"
-
-#: src/components/form/InputLocation.tsx:36
-#, c-format
-msgid "Street"
-msgstr "Calle"
-
-#: src/components/form/InputLocation.tsx:37
-#, c-format
-msgid "Post code"
-msgstr "Código postal"
-
-#: src/components/form/InputLocation.tsx:38
-#, fuzzy, c-format
-msgid "Town location"
-msgstr "Ubicación de ciudad"
-
-#: src/components/form/InputLocation.tsx:39
-#, c-format
-msgid "Town"
-msgstr "Ciudad"
-
-#: src/components/form/InputLocation.tsx:40
-#, c-format
-msgid "District"
-msgstr "Distrito"
-
-#: src/components/form/InputLocation.tsx:41
-#, c-format
-msgid "Country subdivision"
-msgstr "Provincia"
-
-#: src/components/form/InputSearchProduct.tsx:59
-#, fuzzy, c-format
-msgid "Product id"
-msgstr "Id de producto"
-
-#: src/components/form/InputSearchProduct.tsx:60
-#: src/components/product/ProductForm.tsx:99
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:122
-#: src/paths/instance/orders/list/Table.tsx:227
-#: src/paths/instance/products/list/Table.tsx:86
-#, c-format
-msgid "Description"
-msgstr "Descripcion"
-
-#: src/components/form/InputSearchProduct.tsx:73
-#: src/components/form/InputTaxes.tsx:81
-#: src/paths/admin/create/CreatePage.tsx:87 src/paths/admin/list/Table.tsx:110
-#: src/paths/instance/details/DetailPage.tsx:76
-#: src/paths/instance/update/UpdatePage.tsx:106
-#, c-format
-msgid "Name"
-msgstr "Nombre"
-
-#: src/components/form/InputSearchProduct.tsx:102
-#, c-format
-msgid "loading..."
-msgstr "Cargando..."
-
-#: src/components/form/InputSearchProduct.tsx:108
-#, c-format
-msgid "no products found"
-msgstr "No se encontraron productos"
-
-#: src/components/form/InputSearchProduct.tsx:116
-#, c-format
-msgid "no results"
-msgstr "Sin resultados"
-
-#: src/components/form/InputSecured.tsx:33
-#, c-format
-msgid "Deleting"
-msgstr "Borrando"
-
-#: src/components/form/InputSecured.tsx:34
-#, c-format
-msgid "Changing"
-msgstr "Cambiando"
-
-#: src/components/form/InputSecured.tsx:60
-#, c-format
-msgid "Manage token"
-msgstr "Administrar token"
-
-#: src/components/form/InputSecured.tsx:83
-#, c-format
-msgid "Update"
-msgstr "Actualizar"
-
-#: src/components/form/InputSecured.tsx:100
-#: src/paths/instance/orders/create/CreatePage.tsx:252
-#: src/paths/instance/orders/create/CreatePage.tsx:273
-#, c-format
-msgid "Remove"
-msgstr "Eliminar"
-
-#: src/components/form/InputSecured.tsx:106 src/components/modal/index.tsx:52
-#: src/components/modal/index.tsx:73 src/paths/admin/create/CreatePage.tsx:114
-#: src/paths/instance/orders/create/CreatePage.tsx:324
-#: src/paths/instance/products/create/CreatePage.tsx:50
-#: src/paths/instance/products/list/Table.tsx:166
-#: src/paths/instance/products/list/Table.tsx:218
-#: src/paths/instance/products/update/UpdatePage.tsx:54
-#: src/paths/instance/transfers/create/CreatePage.tsx:88
-#: src/paths/instance/update/UpdatePage.tsx:133
-#, c-format
-msgid "Cancel"
-msgstr "Cancelar"
-
-#: src/components/form/InputStock.tsx:91
-#, c-format
-msgid "Manage stock"
-msgstr "Administrar stock"
-
-#: src/components/form/InputStock.tsx:93
-#, c-format
-msgid "Infinite"
-msgstr "Inifinito"
-
-#: src/components/form/InputStock.tsx:105
-#, fuzzy, c-format
-msgid "lost cannot be greater that current + incoming (max %1$s)"
-msgstr "no puede ser mayor al stock actual %1$s"
-
-#: src/components/form/InputStock.tsx:111
-#, c-format
-msgid "current stock will change from %1$s to %2$s"
-msgstr "stock actual cambiará desde %1$s a %2$s"
-
-#: src/components/form/InputStock.tsx:112
-#, c-format
-msgid "current stock will stay at %1$s"
-msgstr "stock actual seguirá en %1$s"
-
-#: src/components/form/InputStock.tsx:129
-#: src/paths/instance/products/list/Table.tsx:204
-#, c-format
-msgid "Incoming"
-msgstr "Ingresando"
-
-#: src/components/form/InputStock.tsx:130
-#: src/paths/instance/products/list/Table.tsx:205
-#, c-format
-msgid "Lost"
-msgstr "Perdido"
-
-#: src/components/form/InputStock.tsx:142
-#, c-format
-msgid "Current"
-msgstr "Actual"
-
-#: src/components/form/InputStock.tsx:145
-#, c-format
-msgid "without stock"
-msgstr "sin stock"
-
-#: src/components/form/InputStock.tsx:150
-#, c-format
-msgid "Next restock"
-msgstr "Próximo reabastecimiento"
-
-#: src/components/form/InputStock.tsx:152
-#, c-format
-msgid "Delivery address"
-msgstr "Dirección de entrega"
-
-#: src/components/form/InputTaxes.tsx:73
-#, c-format
-msgid "this product has no taxes"
-msgstr "este producto no tiene impuestos"
-
-#: src/components/form/InputTaxes.tsx:77
-#: src/paths/instance/orders/details/DetailPage.tsx:145
-#: src/paths/instance/orders/details/DetailPage.tsx:296
-#: src/paths/instance/orders/list/Table.tsx:116
-#: src/paths/instance/transfers/create/CreatePage.tsx:84
-#, c-format
-msgid "Amount"
-msgstr "Monto"
-
-#: src/components/form/InputTaxes.tsx:78
-#, c-format
-msgid "currency and value separated with colon"
-msgstr "Moneda y valor separado por dos puntos"
-
-#: src/components/form/InputTaxes.tsx:84
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:78
-#, c-format
-msgid "Add"
-msgstr "Agregar"
-
-#: src/components/menu/SideBar.tsx:53
-#, c-format
-msgid "Instance"
-msgstr "Instancia"
-
-#: src/components/menu/SideBar.tsx:59
-#, c-format
-msgid "Settings"
-msgstr "Configuración"
-
-#: src/components/menu/SideBar.tsx:65
-#: src/paths/instance/orders/list/Table.tsx:60
-#, fuzzy, c-format
-msgid "Orders"
-msgstr "Ordenes"
-
-#: src/components/menu/SideBar.tsx:71
-#: src/paths/instance/orders/create/CreatePage.tsx:258
-#: src/paths/instance/products/list/Table.tsx:48
-#, c-format
-msgid "Products"
-msgstr "Productos"
-
-#: src/components/menu/SideBar.tsx:77
-#: src/paths/instance/transfers/list/Table.tsx:65
-#, c-format
-msgid "Transfers"
-msgstr "Transferencias"
-
-#: src/components/menu/SideBar.tsx:87
-#, fuzzy, c-format
-msgid "Connection"
-msgstr "Conexión"
-
-#: src/components/menu/SideBar.tsx:112 src/paths/admin/list/Table.tsx:57
-#, c-format
-msgid "Instances"
-msgstr "Instancias"
-
-#: src/components/menu/SideBar.tsx:116
-#, fuzzy, c-format
-msgid "New"
-msgstr "Nuevo"
-
-#: src/components/menu/SideBar.tsx:122
-#, c-format
-msgid "List"
-msgstr "Lista"
-
-#: src/components/menu/SideBar.tsx:129
-#, c-format
-msgid "Log out"
-msgstr "Salir"
-
-#: src/components/modal/index.tsx:74
-#, c-format
-msgid "Clear"
-msgstr "Limpiar"
-
-#: src/components/modal/index.tsx:110 src/components/modal/index.tsx:111
-#, c-format
-msgid "should be the same"
-msgstr "deberían ser iguales"
-
-#: src/components/modal/index.tsx:111
-#, c-format
-msgid "cannot be the same as before"
-msgstr "no puede ser igual al anterior"
-
-#: src/components/modal/index.tsx:114
-#, c-format
-msgid ""
-"You are updating the authorization token from instance %1$s with id %2$s"
-msgstr ""
-"Está actualizando el token de autorización para la instancia %1$s con id %2$s"
-
-#: src/components/modal/index.tsx:124
-#, c-format
-msgid "Old token"
-msgstr "Viejo token"
-
-#: src/components/modal/index.tsx:125
-#, c-format
-msgid "New token"
-msgstr "Nuevo token"
-
-#: src/components/modal/index.tsx:127
-#, c-format
-msgid "Clearing the auth token will mean public access to the instance"
-msgstr ""
-"Limpiar el token de autorización significa acceso publico a la instancia"
-
-#: src/components/product/ProductForm.tsx:96
-#: src/paths/admin/create/CreatePage.tsx:85 src/paths/admin/list/Table.tsx:109
-#: src/paths/instance/transfers/list/Table.tsx:122
-#, c-format
-msgid "ID"
-msgstr "ID"
-
-#: src/components/product/ProductForm.tsx:98
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:121
-#: src/paths/instance/products/list/Table.tsx:85
-#, c-format
-msgid "Image"
-msgstr "Imagen"
-
-#: src/components/product/ProductForm.tsx:100
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:123
-#, c-format
-msgid "Unit"
-msgstr "Unidad"
-
-#: src/components/product/ProductForm.tsx:101
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:124
-#: src/paths/instance/products/list/Table.tsx:162
-#: src/paths/instance/products/list/Table.tsx:214
-#, c-format
-msgid "Price"
-msgstr "Precio"
-
-#: src/components/product/ProductForm.tsx:103
-#: src/paths/instance/products/list/Table.tsx:90
-#, c-format
-msgid "Stock"
-msgstr "Stock"
-
-#: src/components/product/ProductForm.tsx:105
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:128
-#: src/paths/instance/products/list/Table.tsx:88
-#, c-format
-msgid "Taxes"
-msgstr "Impuesto"
-
-#: src/index.tsx:75
-#, c-format
-msgid "Server not found"
-msgstr "Servidor no encontrado"
-
-#: src/index.tsx:85
-#, c-format
-msgid "Couldn't access the server"
-msgstr "No se pudo aceder al servidor"
-
-#: src/index.tsx:87 src/index.tsx:99
-#, c-format
-msgid "Got message %1$s from %2$s"
-msgstr "Recivimos el mensaje %1$s desde %2$s"
-
-#: src/index.tsx:97
-#, c-format
-msgid "Unexpected Error"
-msgstr "Error inesperado"
-
-#: src/paths/admin/create/CreatePage.tsx:89
-#: src/paths/instance/update/UpdatePage.tsx:108
-#, c-format
-msgid "Auth token"
-msgstr "Token de autorización"
-
-#: src/paths/admin/create/CreatePage.tsx:91
-#: src/paths/instance/details/DetailPage.tsx:77
-#: src/paths/instance/update/UpdatePage.tsx:110
-#, c-format
-msgid "Account address"
-msgstr "Dirección de cuenta"
-
-#: src/paths/admin/create/CreatePage.tsx:93
-#: src/paths/instance/update/UpdatePage.tsx:112
-#, c-format
-msgid "Default max deposit fee"
-msgstr "Impuesto máximo de deposito por omisión"
-
-#: src/paths/admin/create/CreatePage.tsx:95
-#: src/paths/instance/update/UpdatePage.tsx:114
-#, c-format
-msgid "Default max wire fee"
-msgstr "Impuesto máximo de transferencia por omisión"
-
-#: src/paths/admin/create/CreatePage.tsx:97
-#: src/paths/instance/update/UpdatePage.tsx:116
-#, c-format
-msgid "Default wire fee amortization"
-msgstr "Amortización de impuesto de transferencia por omisión"
-
-#: src/paths/admin/create/CreatePage.tsx:103
-#: src/paths/instance/update/UpdatePage.tsx:122
-#, c-format
-msgid "Jurisdiction"
-msgstr "Jurisdicción"
-
-#: src/paths/admin/create/CreatePage.tsx:107
-#: src/paths/instance/update/UpdatePage.tsx:126
-#, c-format
-msgid "Default pay delay"
-msgstr "Retrazo de pago por omisión"
-
-#: src/paths/admin/create/CreatePage.tsx:109
-#: src/paths/instance/update/UpdatePage.tsx:128
-#, c-format
-msgid "Default wire transfer delay"
-msgstr "Retrazo de transferencia por omisión"
-
-#: src/paths/admin/create/index.tsx:58
-#, c-format
-msgid "could not create instance"
-msgstr "no se pudo crear la instancia"
-
-#: src/paths/admin/list/Table.tsx:63 src/paths/admin/list/Table.tsx:131
-#: src/paths/instance/transfers/list/Table.tsx:71
-#, fuzzy, c-format
-msgid "Delete"
-msgstr "Borrando"
-
-#: src/paths/admin/list/Table.tsx:128
-#, c-format
-msgid "Edit"
-msgstr ""
-
-#: src/paths/admin/list/Table.tsx:149
-#: src/paths/instance/products/list/Table.tsx:245
-#, c-format
-msgid "There is no instances yet, add more pressing the + sign"
-msgstr "No hay instancias todavían, agregue mas presionando el signo +"
-
-#: src/paths/instance/orders/create/CreatePage.tsx:237
-#, c-format
-msgid "Inventory products"
-msgstr "Productos de inventario"
-
-#: src/paths/instance/orders/create/CreatePage.tsx:286
-#, c-format
-msgid "Total price"
-msgstr "Precio total"
-
-#: src/paths/instance/orders/create/CreatePage.tsx:287
-#, c-format
-msgid "Total tax"
-msgstr "Impuesto total"
-
-#: src/paths/instance/orders/create/CreatePage.tsx:289
-#: src/paths/instance/orders/create/CreatePage.tsx:297
-#, c-format
-msgid "Order price"
-msgstr "Precio de la orden"
-
-#: src/paths/instance/orders/create/CreatePage.tsx:295
-#, fuzzy, c-format
-msgid "Net"
-msgstr "Neto"
-
-#: src/paths/instance/orders/create/CreatePage.tsx:300
-#: src/paths/instance/orders/details/DetailPage.tsx:144
-#: src/paths/instance/orders/details/DetailPage.tsx:295
-#: src/paths/instance/orders/list/Table.tsx:117
-#, c-format
-msgid "Summary"
-msgstr "Resumen"
-
-#: src/paths/instance/orders/create/CreatePage.tsx:302
-#, c-format
-msgid "Payments options"
-msgstr "Opciones de pago"
-
-#: src/paths/instance/orders/create/CreatePage.tsx:303
-#, c-format
-msgid "Auto refund deadline"
-msgstr "Plazo de reembolso automático"
-
-#: src/paths/instance/orders/create/CreatePage.tsx:304
-#, c-format
-msgid "Refund deadline"
-msgstr "Plazo de reembolso"
-
-#: src/paths/instance/orders/create/CreatePage.tsx:305
-#, c-format
-msgid "Pay deadline"
-msgstr "Plazo de pago"
-
-#: src/paths/instance/orders/create/CreatePage.tsx:307
-#, c-format
-msgid "Delivery date"
-msgstr "Fecha de entrega"
-
-#: src/paths/instance/orders/create/CreatePage.tsx:308
-#, fuzzy, c-format
-msgid "Location"
-msgstr "Ubicación"
-
-#: src/paths/instance/orders/create/CreatePage.tsx:312
-#, c-format
-msgid "Max fee"
-msgstr "Impuesto máximo"
-
-#: src/paths/instance/orders/create/CreatePage.tsx:313
-#, c-format
-msgid "Max wire fee"
-msgstr "Impuesto de transferencia máximo"
-
-#: src/paths/instance/orders/create/CreatePage.tsx:314
-#, c-format
-msgid "Wire fee amortization"
-msgstr "Amortización de impuesto de transferencia"
-
-#: src/paths/instance/orders/create/CreatePage.tsx:315
-#, c-format
-msgid "Fullfilment url"
-msgstr "URL de completitud"
-
-#: src/paths/instance/orders/create/CreatePage.tsx:318
-#, c-format
-msgid "Extra information"
-msgstr "Información extra"
-
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:44
-#, c-format
-msgid "select a product first"
-msgstr "seleccione un producto primero"
-
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:51
-#, fuzzy, c-format
-msgid "should be greater than 0"
-msgstr "La imagen debe ser mas chica que 1 MB"
-
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:58
-#, c-format
-msgid ""
-"cannot be greater than current stock and quantity previously added. max: %1$s"
-msgstr ""
-"no puede ser mayor al stock actual y la cantidad previamente agregada. "
-"máximo: %1$s"
-
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:64
-#, c-format
-msgid "cannot be greater than current stock %1$s"
-msgstr "no puede ser mayor al stock actual %1$s"
-
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:76
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:126
-#, c-format
-msgid "Quantity"
-msgstr "Cantidad"
-
-#: src/paths/instance/orders/details/DetailPage.tsx:92
-#: src/paths/instance/orders/details/DetailPage.tsx:235
-#: src/paths/instance/orders/details/DetailPage.tsx:333
-#, c-format
-msgid "Order"
-msgstr "Orden"
-
-#: src/paths/instance/orders/details/DetailPage.tsx:93
-#, c-format
-msgid "claimed"
-msgstr "reclamado"
-
-#: src/paths/instance/orders/details/DetailPage.tsx:110
-#: src/paths/instance/orders/details/DetailPage.tsx:261
-#: src/paths/instance/orders/list/Table.tsx:136
-#, c-format
-msgid "copy url"
-msgstr "copiar url"
-
-#: src/paths/instance/orders/details/DetailPage.tsx:126
-#: src/paths/instance/orders/details/DetailPage.tsx:349
-#, c-format
-msgid "pay at"
-msgstr "pagar en"
-
-#: src/paths/instance/orders/details/DetailPage.tsx:127
-#: src/paths/instance/orders/details/DetailPage.tsx:350
-#, c-format
-msgid "created at"
-msgstr "creado"
-
-#: src/paths/instance/orders/details/DetailPage.tsx:138
-#: src/paths/instance/orders/details/DetailPage.tsx:289
-#, c-format
-msgid "Timeline"
-msgstr "Cronología"
-
-#: src/paths/instance/orders/details/DetailPage.tsx:142
-#: src/paths/instance/orders/details/DetailPage.tsx:293
-#, c-format
-msgid "Payment details"
-msgstr "Detalles de pago"
-
-#: src/paths/instance/orders/details/DetailPage.tsx:146
-#: src/paths/instance/orders/details/DetailPage.tsx:299
-#: src/paths/instance/orders/details/DetailPage.tsx:363
-#, fuzzy, c-format
-msgid "Order status"
-msgstr "Estado de orden"
-
-#: src/paths/instance/orders/details/DetailPage.tsx:156
-#: src/paths/instance/orders/details/DetailPage.tsx:308
-#, fuzzy, c-format
-msgid "Product list"
-msgstr "Lista de producto"
-
-#: src/paths/instance/orders/details/DetailPage.tsx:236
-#, c-format
-msgid "paid"
-msgstr "pagados"
-
-#: src/paths/instance/orders/details/DetailPage.tsx:238
-#, c-format
-msgid "wired"
-msgstr "transferido"
-
-#: src/paths/instance/orders/details/DetailPage.tsx:241
-#, c-format
-msgid "refunded"
-msgstr "reembolzado"
-
-#: src/paths/instance/orders/details/DetailPage.tsx:258
-#, c-format
-msgid "refund"
-msgstr "reembolzar"
-
-#: src/paths/instance/orders/details/DetailPage.tsx:297
-#, c-format
-msgid "Refunded amount"
-msgstr "Monto reembolzado"
-
-#: src/paths/instance/orders/details/DetailPage.tsx:298
-#, c-format
-msgid "Deposit total"
-msgstr "Total depositado"
-
-#: src/paths/instance/orders/details/DetailPage.tsx:336
-#, c-format
-msgid "unpaid"
-msgstr "impago"
-
-#: src/paths/instance/orders/details/DetailPage.tsx:364
-#, c-format
-msgid "Order status URL"
-msgstr "URL de estado de orden"
-
-#: src/paths/instance/orders/details/DetailPage.tsx:365
-#, c-format
-msgid "Pay URI"
-msgstr "URI de pago"
-
-#: src/paths/instance/orders/details/DetailPage.tsx:383
-#, c-format
-msgid ""
-"Unknown order status. This is an error, please contact the administrator."
-msgstr ""
-"Estado de orden desconocido. Esto es un error, por favor contacte a su "
-"administrador"
-
-#: src/paths/instance/orders/details/index.tsx:56
-#: src/paths/instance/orders/list/index.tsx:147
-#, c-format
-msgid "refund created successfully"
-msgstr "reembolzo creado satisfactoriamente"
-
-#: src/paths/instance/orders/details/index.tsx:59
-#: src/paths/instance/orders/list/index.tsx:150
-#, fuzzy, c-format
-msgid "could not create the refund"
-msgstr "No se pudo aceder al servidor"
-
-#: src/paths/instance/orders/list/Table.tsx:111
-#, c-format
-msgid "load newer orders"
-msgstr "cargar nuevas ordenes"
-
-#: src/paths/instance/orders/list/Table.tsx:115
-#, c-format
-msgid "Date"
-msgstr "Fecha"
-
-#: src/paths/instance/orders/list/Table.tsx:131
-#: src/paths/instance/orders/list/Table.tsx:223
-#, c-format
-msgid "Refund"
-msgstr "Reembolzar"
-
-#: src/paths/instance/orders/list/Table.tsx:145
-#, c-format
-msgid "load older orders"
-msgstr "cargar viejas ordenes"
-
-#: src/paths/instance/orders/list/Table.tsx:154
-#, c-format
-msgid "No orders has been found"
-msgstr "No se enconraron ordenes"
-
-#: src/paths/instance/orders/list/Table.tsx:202
-#, c-format
-msgid "date"
-msgstr "fecha"
-
-#: src/paths/instance/orders/list/Table.tsx:203
-#, c-format
-msgid "amount"
-msgstr "monto"
-
-#: src/paths/instance/orders/list/Table.tsx:204
-#, c-format
-msgid "reason"
-msgstr "razón"
-
-#: src/paths/instance/orders/list/Table.tsx:224
-#, c-format
-msgid "Max refundable:"
-msgstr "Máximo reembolzable:"
-
-#: src/paths/instance/orders/list/Table.tsx:226
-#, c-format
-msgid "Reason"
-msgstr "Razón"
-
-#: src/paths/instance/orders/list/Table.tsx:226
-#, c-format
-msgid "duplicated"
-msgstr "duplicado"
-
-#: src/paths/instance/orders/list/Table.tsx:226
-#, c-format
-msgid "requested by the customer"
-msgstr "pedido por el consumidor"
-
-#: src/paths/instance/orders/list/Table.tsx:226
-#, c-format
-msgid "other"
-msgstr "otro"
-
-#: src/paths/instance/orders/list/index.tsx:91
-#, c-format
-msgid "go to order id"
-msgstr "ir a id de orden"
-
-#: src/paths/instance/orders/list/index.tsx:107
-#, c-format
-msgid "Paid"
-msgstr "Pagado"
-
-#: src/paths/instance/orders/list/index.tsx:108
-#, fuzzy, c-format
-msgid "Refunded"
-msgstr "Reembolzado"
-
-#: src/paths/instance/orders/list/index.tsx:109
-#, fuzzy, c-format
-msgid "Not wired"
-msgstr "No transferido"
-
-#: src/paths/instance/orders/list/index.tsx:110
-#, c-format
-msgid "All"
-msgstr "Todo"
-
-#: src/paths/instance/products/create/index.tsx:48
-#: src/paths/instance/products/update/index.tsx:64
-#, c-format
-msgid "could not create product"
-msgstr "no se pudo crear el producto"
-
-#: src/paths/instance/products/list/Table.tsx:87
-#, c-format
-msgid "Sell"
-msgstr "Venta"
-
-#: src/paths/instance/products/list/Table.tsx:89
-#, c-format
-msgid "Profit"
-msgstr "Ganancia"
-
-#: src/paths/instance/products/list/Table.tsx:91
-#, c-format
-msgid "Sold"
-msgstr "Vendido"
-
-#: src/paths/instance/products/list/index.tsx:59
-#, c-format
-msgid "product updated successfully"
-msgstr "producto actualizado correctamente"
-
-#: src/paths/instance/products/list/index.tsx:62
-#, c-format
-msgid "could not update the product"
-msgstr "no se pudo actualizar el producto"
-
-#: src/paths/instance/products/list/index.tsx:70
-#, c-format
-msgid "product delete successfully"
-msgstr "producto fue eliminado correctamente"
-
-#: src/paths/instance/products/list/index.tsx:73
-#, c-format
-msgid "could not delete the product"
-msgstr "no se pudo eliminar el producto"
-
-#: src/paths/instance/tips/list/Table.tsx:59
-#, c-format
-msgid "Tips"
-msgstr "Propinas"
-
-#: src/paths/instance/tips/list/Table.tsx:111
-#, c-format
-msgid "Committed amount"
-msgstr ""
-
-#: src/paths/instance/tips/list/Table.tsx:112
-#, c-format
-msgid "Exchange initial amount"
-msgstr ""
-
-#: src/paths/instance/tips/list/Table.tsx:113
-#, c-format
-msgid "Merchant initial amount"
-msgstr ""
-
-#: src/paths/instance/tips/list/Table.tsx:148
-#, c-format
-msgid "There is no tips yet, add more pressing the + sign"
-msgstr "No hay propinas todavía, agregar mas presionando el signo +"
-
-#: src/paths/instance/transfers/create/CreatePage.tsx:50
-#: src/paths/instance/transfers/create/CreatePage.tsx:54
-#: src/paths/instance/transfers/create/CreatePage.tsx:55
-#: src/paths/instance/transfers/create/CreatePage.tsx:56
-#, c-format
-msgid "cannot be empty"
-msgstr "no puede ser vacío"
-
-#: src/paths/instance/transfers/create/CreatePage.tsx:51
-#, c-format
-msgid "check the id, doest look valid"
-msgstr "verificar el id, no parece válido"
-
-#: src/paths/instance/transfers/create/CreatePage.tsx:52
-#, c-format
-msgid "should have 52 characters, current %1$s"
-msgstr "debería tener 52 caracteres, actualmente %1$s"
-
-#: src/paths/instance/transfers/create/CreatePage.tsx:57
-#, c-format
-msgid "URL doesn't have the right format"
-msgstr "La URL no tiene el formato correcto"
-
-#: src/paths/instance/transfers/create/CreatePage.tsx:74
-#, fuzzy, c-format
-msgid "Transfer ID"
-msgstr "Transferencias"
-
-#: src/paths/instance/transfers/create/CreatePage.tsx:76
-#, fuzzy, c-format
-msgid "Account Address"
-msgstr "Dirección de cuenta"
-
-#: src/paths/instance/transfers/create/CreatePage.tsx:82
-#: src/paths/instance/transfers/list/Table.tsx:125
-#, c-format
-msgid "Exchange URL"
-msgstr "URL del Exchange"
-
-#: src/paths/instance/transfers/create/index.tsx:49
-#, fuzzy, c-format
-msgid "could not inform transfer"
-msgstr "no se pudo crear la instancia"
-
-#: src/paths/instance/transfers/list/Table.tsx:118
-#, fuzzy, c-format
-msgid "load newer transfers"
-msgstr "cargar nuevas ordenes"
-
-#: src/paths/instance/transfers/list/Table.tsx:123
-#, c-format
-msgid "Credit"
-msgstr "Crédito"
-
-#: src/paths/instance/transfers/list/Table.tsx:126
-#, fuzzy, c-format
-msgid "Confirmed"
-msgstr "Confirmar"
-
-#: src/paths/instance/transfers/list/Table.tsx:127
-#: src/paths/instance/transfers/list/index.tsx:60
-#, c-format
-msgid "Verified"
-msgstr "Verificado"
-
-#: src/paths/instance/transfers/list/Table.tsx:128
-#, fuzzy, c-format
-msgid "Executed at"
-msgstr "creado"
-
-#: src/paths/instance/transfers/list/Table.tsx:138
-#: src/paths/instance/transfers/list/Table.tsx:139
-#, c-format
-msgid "yes"
-msgstr "si"
-
-#: src/paths/instance/transfers/list/Table.tsx:138
-#: src/paths/instance/transfers/list/Table.tsx:139
-#, c-format
-msgid "no"
-msgstr "no"
-
-#: src/paths/instance/transfers/list/Table.tsx:140
-#, c-format
-msgid "unknown"
-msgstr "desconocido"
-
-#: src/paths/instance/transfers/list/Table.tsx:145
-#, fuzzy, c-format
-msgid "load older transfers"
-msgstr "cargar viejas transferencias"
-
-#: src/paths/instance/transfers/list/Table.tsx:154
-#, c-format
-msgid "There is no transfer yet, add more pressing the + sign"
-msgstr "No hay transferencias todavía, agregar mas presionando el signo +"
diff --git a/packages/merchant-backend-ui/src/i18n/fr.po b/packages/merchant-backend-ui/src/i18n/fr.po
deleted file mode 100644
index 6b35bd0ce..000000000
--- a/packages/merchant-backend-ui/src/i18n/fr.po
+++ /dev/null
@@ -1,1057 +0,0 @@
-# This file is part of TALER
-# (C) 2016 GNUnet e.V.
-#
-# TALER is free software; you can redistribute it and/or modify it under the
-# terms of the GNU General Public License as published by the Free Software
-# Foundation; either version 3, or (at your option) any later version.
-#
-# TALER is distributed in the hope that it will be useful, but WITHOUT ANY
-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
-# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
-#
-#, fuzzy
-msgid ""
-msgstr ""
-"Project-Id-Version: Taler Wallet\n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-11-23 00:00+0100\n"
-"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
-"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
-"Language-Team: LANGUAGE <LL@li.org>\n"
-"Language: \n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-
-#: src/ApplicationReadyRoutes.tsx:50 src/InstanceRoutes.tsx:118
-#: src/InstanceRoutes.tsx:299
-#, c-format
-msgid "Access denied"
-msgstr ""
-
-#: src/ApplicationReadyRoutes.tsx:51 src/InstanceRoutes.tsx:118
-#: src/InstanceRoutes.tsx:300
-#, c-format
-msgid "Check your token is valid"
-msgstr ""
-
-#: src/ApplicationReadyRoutes.tsx:72
-#, c-format
-msgid "Couldn't access the server."
-msgstr ""
-
-#: src/ApplicationReadyRoutes.tsx:73
-#, c-format
-msgid "Could not infer instance id from url %1$s"
-msgstr ""
-
-#: src/InstanceRoutes.tsx:109
-#, c-format
-msgid "HTTP status #%1$s: Server reported a problem"
-msgstr ""
-
-#: src/InstanceRoutes.tsx:110
-#, c-format
-msgid "Got message: \"%1$s\" from: %2$s"
-msgstr ""
-
-#: src/InstanceRoutes.tsx:127
-#, c-format
-msgid "No default instance"
-msgstr ""
-
-#: src/InstanceRoutes.tsx:128
-#, c-format
-msgid ""
-"in order to use merchant backoffice, you should create the default instance"
-msgstr ""
-
-#: src/InstanceRoutes.tsx:288
-#, c-format
-msgid "Server reported a problem: HTTP status #%1$s"
-msgstr ""
-
-#: src/InstanceRoutes.tsx:289
-#, c-format
-msgid "Got message: %1$s from: %2$s"
-msgstr ""
-
-#: src/components/exception/login.tsx:46
-#, c-format
-msgid "Login required"
-msgstr ""
-
-#: src/components/exception/login.tsx:49
-#, c-format
-msgid ""
-"Please enter your auth token. Token should have \"secret-token:\" and start "
-"with Bearer or ApiKey"
-msgstr ""
-
-#: src/components/exception/login.tsx:86 src/components/modal/index.tsx:53
-#: src/components/modal/index.tsx:75 src/paths/admin/create/CreatePage.tsx:115
-#: src/paths/instance/orders/create/CreatePage.tsx:325
-#: src/paths/instance/products/create/CreatePage.tsx:51
-#: src/paths/instance/products/list/Table.tsx:174
-#: src/paths/instance/products/list/Table.tsx:228
-#: src/paths/instance/products/update/UpdatePage.tsx:55
-#: src/paths/instance/transfers/create/CreatePage.tsx:89
-#: src/paths/instance/update/UpdatePage.tsx:134
-#, c-format
-msgid "Confirm"
-msgstr ""
-
-#: src/components/form/InputArray.tsx:72
-#, c-format
-msgid "The value %1$s is invalid for a payment url"
-msgstr ""
-
-#: src/components/form/InputDate.tsx:67
-#: src/paths/instance/orders/list/index.tsx:123
-#, c-format
-msgid "pick a date"
-msgstr ""
-
-#: src/components/form/InputDate.tsx:81
-#, c-format
-msgid "clear"
-msgstr ""
-
-#: src/components/form/InputDate.tsx:83
-#: src/paths/instance/transfers/list/Table.tsx:140
-#, c-format
-msgid "never"
-msgstr ""
-
-#: src/components/form/InputImage.tsx:80
-#, c-format
-msgid "Image should be smaller than 1 MB"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:28
-#, c-format
-msgid "Country"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:30
-#: src/paths/admin/create/CreatePage.tsx:99
-#: src/paths/instance/transfers/list/Table.tsx:124
-#: src/paths/instance/update/UpdatePage.tsx:118
-#, c-format
-msgid "Address"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:34
-#, c-format
-msgid "Building number"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:35
-#, c-format
-msgid "Building name"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:36
-#, c-format
-msgid "Street"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:37
-#, c-format
-msgid "Post code"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:38
-#, c-format
-msgid "Town location"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:39
-#, c-format
-msgid "Town"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:40
-#, c-format
-msgid "District"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:41
-#, c-format
-msgid "Country subdivision"
-msgstr ""
-
-#: src/components/form/InputSearchProduct.tsx:59
-#, c-format
-msgid "Product id"
-msgstr ""
-
-#: src/components/form/InputSearchProduct.tsx:60
-#: src/components/product/ProductForm.tsx:99
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:122
-#: src/paths/instance/orders/list/Table.tsx:227
-#: src/paths/instance/products/list/Table.tsx:86
-#, c-format
-msgid "Description"
-msgstr ""
-
-#: src/components/form/InputSearchProduct.tsx:73
-#: src/components/form/InputTaxes.tsx:81
-#: src/paths/admin/create/CreatePage.tsx:87 src/paths/admin/list/Table.tsx:110
-#: src/paths/instance/details/DetailPage.tsx:76
-#: src/paths/instance/update/UpdatePage.tsx:106
-#, c-format
-msgid "Name"
-msgstr ""
-
-#: src/components/form/InputSearchProduct.tsx:102
-#, c-format
-msgid "loading..."
-msgstr ""
-
-#: src/components/form/InputSearchProduct.tsx:108
-#, c-format
-msgid "no products found"
-msgstr ""
-
-#: src/components/form/InputSearchProduct.tsx:116
-#, c-format
-msgid "no results"
-msgstr ""
-
-#: src/components/form/InputSecured.tsx:33
-#, c-format
-msgid "Deleting"
-msgstr ""
-
-#: src/components/form/InputSecured.tsx:34
-#, c-format
-msgid "Changing"
-msgstr ""
-
-#: src/components/form/InputSecured.tsx:60
-#, c-format
-msgid "Manage token"
-msgstr ""
-
-#: src/components/form/InputSecured.tsx:83
-#, c-format
-msgid "Update"
-msgstr ""
-
-#: src/components/form/InputSecured.tsx:100
-#: src/paths/instance/orders/create/CreatePage.tsx:252
-#: src/paths/instance/orders/create/CreatePage.tsx:273
-#, c-format
-msgid "Remove"
-msgstr ""
-
-#: src/components/form/InputSecured.tsx:106 src/components/modal/index.tsx:52
-#: src/components/modal/index.tsx:73 src/paths/admin/create/CreatePage.tsx:114
-#: src/paths/instance/orders/create/CreatePage.tsx:324
-#: src/paths/instance/products/create/CreatePage.tsx:50
-#: src/paths/instance/products/list/Table.tsx:166
-#: src/paths/instance/products/list/Table.tsx:218
-#: src/paths/instance/products/update/UpdatePage.tsx:54
-#: src/paths/instance/transfers/create/CreatePage.tsx:88
-#: src/paths/instance/update/UpdatePage.tsx:133
-#, c-format
-msgid "Cancel"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:91
-#, c-format
-msgid "Manage stock"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:93
-#, c-format
-msgid "Infinite"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:105
-#, c-format
-msgid "lost cannot be greater that current + incoming (max %1$s)"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:111
-#, c-format
-msgid "current stock will change from %1$s to %2$s"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:112
-#, c-format
-msgid "current stock will stay at %1$s"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:129
-#: src/paths/instance/products/list/Table.tsx:204
-#, c-format
-msgid "Incoming"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:130
-#: src/paths/instance/products/list/Table.tsx:205
-#, c-format
-msgid "Lost"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:142
-#, c-format
-msgid "Current"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:145
-#, c-format
-msgid "without stock"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:150
-#, c-format
-msgid "Next restock"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:152
-#, c-format
-msgid "Delivery address"
-msgstr ""
-
-#: src/components/form/InputTaxes.tsx:73
-#, c-format
-msgid "this product has no taxes"
-msgstr ""
-
-#: src/components/form/InputTaxes.tsx:77
-#: src/paths/instance/orders/details/DetailPage.tsx:145
-#: src/paths/instance/orders/details/DetailPage.tsx:296
-#: src/paths/instance/orders/list/Table.tsx:116
-#: src/paths/instance/transfers/create/CreatePage.tsx:84
-#, c-format
-msgid "Amount"
-msgstr ""
-
-#: src/components/form/InputTaxes.tsx:78
-#, c-format
-msgid "currency and value separated with colon"
-msgstr ""
-
-#: src/components/form/InputTaxes.tsx:84
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:78
-#, c-format
-msgid "Add"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:53
-#, c-format
-msgid "Instance"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:59
-#, c-format
-msgid "Settings"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:65
-#: src/paths/instance/orders/list/Table.tsx:60
-#, c-format
-msgid "Orders"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:71
-#: src/paths/instance/orders/create/CreatePage.tsx:258
-#: src/paths/instance/products/list/Table.tsx:48
-#, c-format
-msgid "Products"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:77
-#: src/paths/instance/transfers/list/Table.tsx:65
-#, c-format
-msgid "Transfers"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:87
-#, c-format
-msgid "Connection"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:112 src/paths/admin/list/Table.tsx:57
-#, c-format
-msgid "Instances"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:116
-#, c-format
-msgid "New"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:122
-#, c-format
-msgid "List"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:129
-#, c-format
-msgid "Log out"
-msgstr ""
-
-#: src/components/modal/index.tsx:74
-#, c-format
-msgid "Clear"
-msgstr ""
-
-#: src/components/modal/index.tsx:110 src/components/modal/index.tsx:111
-#, c-format
-msgid "should be the same"
-msgstr ""
-
-#: src/components/modal/index.tsx:111
-#, c-format
-msgid "cannot be the same as before"
-msgstr ""
-
-#: src/components/modal/index.tsx:114
-#, c-format
-msgid ""
-"You are updating the authorization token from instance %1$s with id %2$s"
-msgstr ""
-
-#: src/components/modal/index.tsx:124
-#, c-format
-msgid "Old token"
-msgstr ""
-
-#: src/components/modal/index.tsx:125
-#, c-format
-msgid "New token"
-msgstr ""
-
-#: src/components/modal/index.tsx:127
-#, c-format
-msgid "Clearing the auth token will mean public access to the instance"
-msgstr ""
-
-#: src/components/product/ProductForm.tsx:96
-#: src/paths/admin/create/CreatePage.tsx:85 src/paths/admin/list/Table.tsx:109
-#: src/paths/instance/transfers/list/Table.tsx:122
-#, c-format
-msgid "ID"
-msgstr ""
-
-#: src/components/product/ProductForm.tsx:98
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:121
-#: src/paths/instance/products/list/Table.tsx:85
-#, c-format
-msgid "Image"
-msgstr ""
-
-#: src/components/product/ProductForm.tsx:100
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:123
-#, c-format
-msgid "Unit"
-msgstr ""
-
-#: src/components/product/ProductForm.tsx:101
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:124
-#: src/paths/instance/products/list/Table.tsx:162
-#: src/paths/instance/products/list/Table.tsx:214
-#, c-format
-msgid "Price"
-msgstr ""
-
-#: src/components/product/ProductForm.tsx:103
-#: src/paths/instance/products/list/Table.tsx:90
-#, c-format
-msgid "Stock"
-msgstr ""
-
-#: src/components/product/ProductForm.tsx:105
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:128
-#: src/paths/instance/products/list/Table.tsx:88
-#, c-format
-msgid "Taxes"
-msgstr ""
-
-#: src/index.tsx:75
-#, c-format
-msgid "Server not found"
-msgstr ""
-
-#: src/index.tsx:85
-#, c-format
-msgid "Couldn't access the server"
-msgstr ""
-
-#: src/index.tsx:87 src/index.tsx:99
-#, c-format
-msgid "Got message %1$s from %2$s"
-msgstr ""
-
-#: src/index.tsx:97
-#, c-format
-msgid "Unexpected Error"
-msgstr ""
-
-#: src/paths/admin/create/CreatePage.tsx:89
-#: src/paths/instance/update/UpdatePage.tsx:108
-#, c-format
-msgid "Auth token"
-msgstr ""
-
-#: src/paths/admin/create/CreatePage.tsx:91
-#: src/paths/instance/details/DetailPage.tsx:77
-#: src/paths/instance/update/UpdatePage.tsx:110
-#, c-format
-msgid "Account address"
-msgstr ""
-
-#: src/paths/admin/create/CreatePage.tsx:93
-#: src/paths/instance/update/UpdatePage.tsx:112
-#, c-format
-msgid "Default max deposit fee"
-msgstr ""
-
-#: src/paths/admin/create/CreatePage.tsx:95
-#: src/paths/instance/update/UpdatePage.tsx:114
-#, c-format
-msgid "Default max wire fee"
-msgstr ""
-
-#: src/paths/admin/create/CreatePage.tsx:97
-#: src/paths/instance/update/UpdatePage.tsx:116
-#, c-format
-msgid "Default wire fee amortization"
-msgstr ""
-
-#: src/paths/admin/create/CreatePage.tsx:103
-#: src/paths/instance/update/UpdatePage.tsx:122
-#, c-format
-msgid "Jurisdiction"
-msgstr ""
-
-#: src/paths/admin/create/CreatePage.tsx:107
-#: src/paths/instance/update/UpdatePage.tsx:126
-#, c-format
-msgid "Default pay delay"
-msgstr ""
-
-#: src/paths/admin/create/CreatePage.tsx:109
-#: src/paths/instance/update/UpdatePage.tsx:128
-#, c-format
-msgid "Default wire transfer delay"
-msgstr ""
-
-#: src/paths/admin/create/index.tsx:58
-#, c-format
-msgid "could not create instance"
-msgstr ""
-
-#: src/paths/admin/list/Table.tsx:63 src/paths/admin/list/Table.tsx:131
-#: src/paths/instance/transfers/list/Table.tsx:71
-#, c-format
-msgid "Delete"
-msgstr ""
-
-#: src/paths/admin/list/Table.tsx:128
-#, c-format
-msgid "Edit"
-msgstr ""
-
-#: src/paths/admin/list/Table.tsx:149
-#: src/paths/instance/products/list/Table.tsx:245
-#, c-format
-msgid "There is no instances yet, add more pressing the + sign"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:237
-#, c-format
-msgid "Inventory products"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:286
-#, c-format
-msgid "Total price"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:287
-#, c-format
-msgid "Total tax"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:289
-#: src/paths/instance/orders/create/CreatePage.tsx:297
-#, c-format
-msgid "Order price"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:295
-#, c-format
-msgid "Net"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:300
-#: src/paths/instance/orders/details/DetailPage.tsx:144
-#: src/paths/instance/orders/details/DetailPage.tsx:295
-#: src/paths/instance/orders/list/Table.tsx:117
-#, c-format
-msgid "Summary"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:302
-#, c-format
-msgid "Payments options"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:303
-#, c-format
-msgid "Auto refund deadline"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:304
-#, c-format
-msgid "Refund deadline"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:305
-#, c-format
-msgid "Pay deadline"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:307
-#, c-format
-msgid "Delivery date"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:308
-#, c-format
-msgid "Location"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:312
-#, c-format
-msgid "Max fee"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:313
-#, c-format
-msgid "Max wire fee"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:314
-#, c-format
-msgid "Wire fee amortization"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:315
-#, c-format
-msgid "Fullfilment url"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:318
-#, c-format
-msgid "Extra information"
-msgstr ""
-
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:44
-#, c-format
-msgid "select a product first"
-msgstr ""
-
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:51
-#, c-format
-msgid "should be greater than 0"
-msgstr ""
-
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:58
-#, c-format
-msgid ""
-"cannot be greater than current stock and quantity previously added. max: %1$s"
-msgstr ""
-
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:64
-#, c-format
-msgid "cannot be greater than current stock %1$s"
-msgstr ""
-
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:76
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:126
-#, c-format
-msgid "Quantity"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:92
-#: src/paths/instance/orders/details/DetailPage.tsx:235
-#: src/paths/instance/orders/details/DetailPage.tsx:333
-#, c-format
-msgid "Order"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:93
-#, c-format
-msgid "claimed"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:110
-#: src/paths/instance/orders/details/DetailPage.tsx:261
-#: src/paths/instance/orders/list/Table.tsx:136
-#, c-format
-msgid "copy url"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:126
-#: src/paths/instance/orders/details/DetailPage.tsx:349
-#, c-format
-msgid "pay at"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:127
-#: src/paths/instance/orders/details/DetailPage.tsx:350
-#, c-format
-msgid "created at"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:138
-#: src/paths/instance/orders/details/DetailPage.tsx:289
-#, c-format
-msgid "Timeline"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:142
-#: src/paths/instance/orders/details/DetailPage.tsx:293
-#, c-format
-msgid "Payment details"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:146
-#: src/paths/instance/orders/details/DetailPage.tsx:299
-#: src/paths/instance/orders/details/DetailPage.tsx:363
-#, c-format
-msgid "Order status"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:156
-#: src/paths/instance/orders/details/DetailPage.tsx:308
-#, c-format
-msgid "Product list"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:236
-#, c-format
-msgid "paid"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:238
-#, c-format
-msgid "wired"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:241
-#, c-format
-msgid "refunded"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:258
-#, c-format
-msgid "refund"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:297
-#, c-format
-msgid "Refunded amount"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:298
-#, c-format
-msgid "Deposit total"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:336
-#, c-format
-msgid "unpaid"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:364
-#, c-format
-msgid "Order status URL"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:365
-#, c-format
-msgid "Pay URI"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:383
-#, c-format
-msgid ""
-"Unknown order status. This is an error, please contact the administrator."
-msgstr ""
-
-#: src/paths/instance/orders/details/index.tsx:56
-#: src/paths/instance/orders/list/index.tsx:147
-#, c-format
-msgid "refund created successfully"
-msgstr ""
-
-#: src/paths/instance/orders/details/index.tsx:59
-#: src/paths/instance/orders/list/index.tsx:150
-#, c-format
-msgid "could not create the refund"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:111
-#, c-format
-msgid "load newer orders"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:115
-#, c-format
-msgid "Date"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:131
-#: src/paths/instance/orders/list/Table.tsx:223
-#, c-format
-msgid "Refund"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:145
-#, c-format
-msgid "load older orders"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:154
-#, c-format
-msgid "No orders has been found"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:202
-#, c-format
-msgid "date"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:203
-#, c-format
-msgid "amount"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:204
-#, c-format
-msgid "reason"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:224
-#, c-format
-msgid "Max refundable:"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:226
-#, c-format
-msgid "Reason"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:226
-#, c-format
-msgid "duplicated"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:226
-#, c-format
-msgid "requested by the customer"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:226
-#, c-format
-msgid "other"
-msgstr ""
-
-#: src/paths/instance/orders/list/index.tsx:91
-#, c-format
-msgid "go to order id"
-msgstr ""
-
-#: src/paths/instance/orders/list/index.tsx:107
-#, c-format
-msgid "Paid"
-msgstr ""
-
-#: src/paths/instance/orders/list/index.tsx:108
-#, c-format
-msgid "Refunded"
-msgstr ""
-
-#: src/paths/instance/orders/list/index.tsx:109
-#, c-format
-msgid "Not wired"
-msgstr ""
-
-#: src/paths/instance/orders/list/index.tsx:110
-#, c-format
-msgid "All"
-msgstr ""
-
-#: src/paths/instance/products/create/index.tsx:48
-#: src/paths/instance/products/update/index.tsx:64
-#, c-format
-msgid "could not create product"
-msgstr ""
-
-#: src/paths/instance/products/list/Table.tsx:87
-#, c-format
-msgid "Sell"
-msgstr ""
-
-#: src/paths/instance/products/list/Table.tsx:89
-#, c-format
-msgid "Profit"
-msgstr ""
-
-#: src/paths/instance/products/list/Table.tsx:91
-#, c-format
-msgid "Sold"
-msgstr ""
-
-#: src/paths/instance/products/list/index.tsx:59
-#, c-format
-msgid "product updated successfully"
-msgstr ""
-
-#: src/paths/instance/products/list/index.tsx:62
-#, c-format
-msgid "could not update the product"
-msgstr ""
-
-#: src/paths/instance/products/list/index.tsx:70
-#, c-format
-msgid "product delete successfully"
-msgstr ""
-
-#: src/paths/instance/products/list/index.tsx:73
-#, c-format
-msgid "could not delete the product"
-msgstr ""
-
-#: src/paths/instance/tips/list/Table.tsx:59
-#, c-format
-msgid "Tips"
-msgstr ""
-
-#: src/paths/instance/tips/list/Table.tsx:111
-#, c-format
-msgid "Committed amount"
-msgstr ""
-
-#: src/paths/instance/tips/list/Table.tsx:112
-#, c-format
-msgid "Exchange initial amount"
-msgstr ""
-
-#: src/paths/instance/tips/list/Table.tsx:113
-#, c-format
-msgid "Merchant initial amount"
-msgstr ""
-
-#: src/paths/instance/tips/list/Table.tsx:148
-#, c-format
-msgid "There is no tips yet, add more pressing the + sign"
-msgstr ""
-
-#: src/paths/instance/transfers/create/CreatePage.tsx:50
-#: src/paths/instance/transfers/create/CreatePage.tsx:54
-#: src/paths/instance/transfers/create/CreatePage.tsx:55
-#: src/paths/instance/transfers/create/CreatePage.tsx:56
-#, c-format
-msgid "cannot be empty"
-msgstr ""
-
-#: src/paths/instance/transfers/create/CreatePage.tsx:51
-#, c-format
-msgid "check the id, doest look valid"
-msgstr ""
-
-#: src/paths/instance/transfers/create/CreatePage.tsx:52
-#, c-format
-msgid "should have 52 characters, current %1$s"
-msgstr ""
-
-#: src/paths/instance/transfers/create/CreatePage.tsx:57
-#, c-format
-msgid "URL doesn't have the right format"
-msgstr ""
-
-#: src/paths/instance/transfers/create/CreatePage.tsx:74
-#, c-format
-msgid "Transfer ID"
-msgstr ""
-
-#: src/paths/instance/transfers/create/CreatePage.tsx:76
-#, c-format
-msgid "Account Address"
-msgstr ""
-
-#: src/paths/instance/transfers/create/CreatePage.tsx:82
-#: src/paths/instance/transfers/list/Table.tsx:125
-#, c-format
-msgid "Exchange URL"
-msgstr ""
-
-#: src/paths/instance/transfers/create/index.tsx:49
-#, c-format
-msgid "could not inform transfer"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:118
-#, c-format
-msgid "load newer transfers"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:123
-#, c-format
-msgid "Credit"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:126
-#, c-format
-msgid "Confirmed"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:127
-#: src/paths/instance/transfers/list/index.tsx:60
-#, c-format
-msgid "Verified"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:128
-#, c-format
-msgid "Executed at"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:138
-#: src/paths/instance/transfers/list/Table.tsx:139
-#, c-format
-msgid "yes"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:138
-#: src/paths/instance/transfers/list/Table.tsx:139
-#, c-format
-msgid "no"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:140
-#, c-format
-msgid "unknown"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:145
-#, c-format
-msgid "load older transfers"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:154
-#, c-format
-msgid "There is no transfer yet, add more pressing the + sign"
-msgstr ""
diff --git a/packages/merchant-backend-ui/src/i18n/index.tsx b/packages/merchant-backend-ui/src/i18n/index.tsx
deleted file mode 100644
index 63c8e1934..000000000
--- a/packages/merchant-backend-ui/src/i18n/index.tsx
+++ /dev/null
@@ -1,203 +0,0 @@
-/*
- 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/>
- */
-
-/**
- * Translation helpers for React components and template literals.
- */
-
-/**
- * Imports
- */
-import { ComponentChild, ComponentChildren, h, Fragment, VNode } from "preact";
-
-import { useTranslationContext } from "../context/translation";
-
-export function useTranslator() {
- const ctx = useTranslationContext();
- const jed = ctx.handler
- return function str(stringSeq: TemplateStringsArray, ...values: any[]): string {
- const s = toI18nString(stringSeq);
- if (!s) return s
- const tr = jed
- .translate(s)
- .ifPlural(1, s)
- .fetch(...values);
- return tr;
- }
-}
-
-
-/**
- * Convert template strings to a msgid
- */
- function toI18nString(stringSeq: ReadonlyArray<string>): string {
- let s = "";
- for (let i = 0; i < stringSeq.length; i++) {
- s += stringSeq[i];
- if (i < stringSeq.length - 1) {
- s += `%${i + 1}$s`;
- }
- }
- return s;
-}
-
-
-interface TranslateSwitchProps {
- target: number;
- children: ComponentChildren;
-}
-
-function stringifyChildren(children: ComponentChildren): string {
- let n = 1;
- const ss = (children instanceof Array ? children : [children]).map((c) => {
- if (typeof c === "string") {
- return c;
- }
- return `%${n++}$s`;
- });
- const s = ss.join("").replace(/ +/g, " ").trim();
- return s;
-}
-
-interface TranslateProps {
- children: ComponentChildren;
- /**
- * Component that the translated element should be wrapped in.
- * Defaults to "div".
- */
- wrap?: any;
-
- /**
- * Props to give to the wrapped component.
- */
- wrapProps?: any;
-}
-
-function getTranslatedChildren(
- translation: string,
- children: ComponentChildren,
-): ComponentChild[] {
- const tr = translation.split(/%(\d+)\$s/);
- const childArray = children instanceof Array ? children : [children];
- // Merge consecutive string children.
- const placeholderChildren = Array<ComponentChild>();
- for (let i = 0; i < childArray.length; i++) {
- const x = childArray[i];
- if (x === undefined) {
- continue;
- } else if (typeof x === "string") {
- continue;
- } else {
- placeholderChildren.push(x);
- }
- }
- const result = Array<ComponentChild>();
- for (let i = 0; i < tr.length; i++) {
- if (i % 2 == 0) {
- // Text
- result.push(tr[i]);
- } else {
- const childIdx = Number.parseInt(tr[i],10) - 1;
- result.push(placeholderChildren[childIdx]);
- }
- }
- return result;
-}
-
-/**
- * Translate text node children of this component.
- * If a child component might produce a text node, it must be wrapped
- * in a another non-text element.
- *
- * Example:
- * ```
- * <Translate>
- * Hello. Your score is <span><PlayerScore player={player} /></span>
- * </Translate>
- * ```
- */
-export function Translate({ children }: TranslateProps): VNode {
- const s = stringifyChildren(children);
- const ctx = useTranslationContext()
- const translation: string = ctx.handler.ngettext(s, s, 1);
- const result = getTranslatedChildren(translation, children)
- return <Fragment>{result}</Fragment>;
-}
-
-/**
- * Switch translation based on singular or plural based on the target prop.
- * Should only contain TranslateSingular and TransplatePlural as children.
- *
- * Example:
- * ```
- * <TranslateSwitch target={n}>
- * <TranslateSingular>I have {n} apple.</TranslateSingular>
- * <TranslatePlural>I have {n} apples.</TranslatePlural>
- * </TranslateSwitch>
- * ```
- */
-export function TranslateSwitch({ children, target }: TranslateSwitchProps) {
- let singular: VNode<TranslationPluralProps> | undefined;
- let plural: VNode<TranslationPluralProps> | undefined;
- // const children = this.props.children;
- if (children) {
- (children instanceof Array ? children : [children]).forEach((child: any) => {
- if (child.type === TranslatePlural) {
- plural = child;
- }
- if (child.type === TranslateSingular) {
- singular = child;
- }
- });
- }
- if (!singular || !plural) {
- console.error("translation not found");
- return h("span", {}, ["translation not found"]);
- }
- singular.props.target = target;
- plural.props.target = target;
- // We're looking up the translation based on the
- // singular, even if we must use the plural form.
- return singular;
-}
-
-interface TranslationPluralProps {
- children: ComponentChildren;
- target: number;
-}
-
-/**
- * See [[TranslateSwitch]].
- */
-export function TranslatePlural({ children, target }: TranslationPluralProps): VNode {
- const s = stringifyChildren(children);
- const ctx = useTranslationContext()
- const translation = ctx.handler.ngettext(s, s, 1);
- const result = getTranslatedChildren(translation, children);
- return <Fragment>{result}</Fragment>;
-}
-
-/**
- * See [[TranslateSwitch]].
- */
-export function TranslateSingular({ children, target }: TranslationPluralProps): VNode {
- const s = stringifyChildren(children);
- const ctx = useTranslationContext()
- const translation = ctx.handler.ngettext(s, s, target);
- const result = getTranslatedChildren(translation, children);
- return <Fragment>{result}</Fragment>;
-
-}
diff --git a/packages/merchant-backend-ui/src/i18n/it.po b/packages/merchant-backend-ui/src/i18n/it.po
deleted file mode 100644
index 6b35bd0ce..000000000
--- a/packages/merchant-backend-ui/src/i18n/it.po
+++ /dev/null
@@ -1,1057 +0,0 @@
-# This file is part of TALER
-# (C) 2016 GNUnet e.V.
-#
-# TALER is free software; you can redistribute it and/or modify it under the
-# terms of the GNU General Public License as published by the Free Software
-# Foundation; either version 3, or (at your option) any later version.
-#
-# TALER is distributed in the hope that it will be useful, but WITHOUT ANY
-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
-# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
-#
-#, fuzzy
-msgid ""
-msgstr ""
-"Project-Id-Version: Taler Wallet\n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-11-23 00:00+0100\n"
-"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
-"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
-"Language-Team: LANGUAGE <LL@li.org>\n"
-"Language: \n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-
-#: src/ApplicationReadyRoutes.tsx:50 src/InstanceRoutes.tsx:118
-#: src/InstanceRoutes.tsx:299
-#, c-format
-msgid "Access denied"
-msgstr ""
-
-#: src/ApplicationReadyRoutes.tsx:51 src/InstanceRoutes.tsx:118
-#: src/InstanceRoutes.tsx:300
-#, c-format
-msgid "Check your token is valid"
-msgstr ""
-
-#: src/ApplicationReadyRoutes.tsx:72
-#, c-format
-msgid "Couldn't access the server."
-msgstr ""
-
-#: src/ApplicationReadyRoutes.tsx:73
-#, c-format
-msgid "Could not infer instance id from url %1$s"
-msgstr ""
-
-#: src/InstanceRoutes.tsx:109
-#, c-format
-msgid "HTTP status #%1$s: Server reported a problem"
-msgstr ""
-
-#: src/InstanceRoutes.tsx:110
-#, c-format
-msgid "Got message: \"%1$s\" from: %2$s"
-msgstr ""
-
-#: src/InstanceRoutes.tsx:127
-#, c-format
-msgid "No default instance"
-msgstr ""
-
-#: src/InstanceRoutes.tsx:128
-#, c-format
-msgid ""
-"in order to use merchant backoffice, you should create the default instance"
-msgstr ""
-
-#: src/InstanceRoutes.tsx:288
-#, c-format
-msgid "Server reported a problem: HTTP status #%1$s"
-msgstr ""
-
-#: src/InstanceRoutes.tsx:289
-#, c-format
-msgid "Got message: %1$s from: %2$s"
-msgstr ""
-
-#: src/components/exception/login.tsx:46
-#, c-format
-msgid "Login required"
-msgstr ""
-
-#: src/components/exception/login.tsx:49
-#, c-format
-msgid ""
-"Please enter your auth token. Token should have \"secret-token:\" and start "
-"with Bearer or ApiKey"
-msgstr ""
-
-#: src/components/exception/login.tsx:86 src/components/modal/index.tsx:53
-#: src/components/modal/index.tsx:75 src/paths/admin/create/CreatePage.tsx:115
-#: src/paths/instance/orders/create/CreatePage.tsx:325
-#: src/paths/instance/products/create/CreatePage.tsx:51
-#: src/paths/instance/products/list/Table.tsx:174
-#: src/paths/instance/products/list/Table.tsx:228
-#: src/paths/instance/products/update/UpdatePage.tsx:55
-#: src/paths/instance/transfers/create/CreatePage.tsx:89
-#: src/paths/instance/update/UpdatePage.tsx:134
-#, c-format
-msgid "Confirm"
-msgstr ""
-
-#: src/components/form/InputArray.tsx:72
-#, c-format
-msgid "The value %1$s is invalid for a payment url"
-msgstr ""
-
-#: src/components/form/InputDate.tsx:67
-#: src/paths/instance/orders/list/index.tsx:123
-#, c-format
-msgid "pick a date"
-msgstr ""
-
-#: src/components/form/InputDate.tsx:81
-#, c-format
-msgid "clear"
-msgstr ""
-
-#: src/components/form/InputDate.tsx:83
-#: src/paths/instance/transfers/list/Table.tsx:140
-#, c-format
-msgid "never"
-msgstr ""
-
-#: src/components/form/InputImage.tsx:80
-#, c-format
-msgid "Image should be smaller than 1 MB"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:28
-#, c-format
-msgid "Country"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:30
-#: src/paths/admin/create/CreatePage.tsx:99
-#: src/paths/instance/transfers/list/Table.tsx:124
-#: src/paths/instance/update/UpdatePage.tsx:118
-#, c-format
-msgid "Address"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:34
-#, c-format
-msgid "Building number"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:35
-#, c-format
-msgid "Building name"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:36
-#, c-format
-msgid "Street"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:37
-#, c-format
-msgid "Post code"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:38
-#, c-format
-msgid "Town location"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:39
-#, c-format
-msgid "Town"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:40
-#, c-format
-msgid "District"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:41
-#, c-format
-msgid "Country subdivision"
-msgstr ""
-
-#: src/components/form/InputSearchProduct.tsx:59
-#, c-format
-msgid "Product id"
-msgstr ""
-
-#: src/components/form/InputSearchProduct.tsx:60
-#: src/components/product/ProductForm.tsx:99
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:122
-#: src/paths/instance/orders/list/Table.tsx:227
-#: src/paths/instance/products/list/Table.tsx:86
-#, c-format
-msgid "Description"
-msgstr ""
-
-#: src/components/form/InputSearchProduct.tsx:73
-#: src/components/form/InputTaxes.tsx:81
-#: src/paths/admin/create/CreatePage.tsx:87 src/paths/admin/list/Table.tsx:110
-#: src/paths/instance/details/DetailPage.tsx:76
-#: src/paths/instance/update/UpdatePage.tsx:106
-#, c-format
-msgid "Name"
-msgstr ""
-
-#: src/components/form/InputSearchProduct.tsx:102
-#, c-format
-msgid "loading..."
-msgstr ""
-
-#: src/components/form/InputSearchProduct.tsx:108
-#, c-format
-msgid "no products found"
-msgstr ""
-
-#: src/components/form/InputSearchProduct.tsx:116
-#, c-format
-msgid "no results"
-msgstr ""
-
-#: src/components/form/InputSecured.tsx:33
-#, c-format
-msgid "Deleting"
-msgstr ""
-
-#: src/components/form/InputSecured.tsx:34
-#, c-format
-msgid "Changing"
-msgstr ""
-
-#: src/components/form/InputSecured.tsx:60
-#, c-format
-msgid "Manage token"
-msgstr ""
-
-#: src/components/form/InputSecured.tsx:83
-#, c-format
-msgid "Update"
-msgstr ""
-
-#: src/components/form/InputSecured.tsx:100
-#: src/paths/instance/orders/create/CreatePage.tsx:252
-#: src/paths/instance/orders/create/CreatePage.tsx:273
-#, c-format
-msgid "Remove"
-msgstr ""
-
-#: src/components/form/InputSecured.tsx:106 src/components/modal/index.tsx:52
-#: src/components/modal/index.tsx:73 src/paths/admin/create/CreatePage.tsx:114
-#: src/paths/instance/orders/create/CreatePage.tsx:324
-#: src/paths/instance/products/create/CreatePage.tsx:50
-#: src/paths/instance/products/list/Table.tsx:166
-#: src/paths/instance/products/list/Table.tsx:218
-#: src/paths/instance/products/update/UpdatePage.tsx:54
-#: src/paths/instance/transfers/create/CreatePage.tsx:88
-#: src/paths/instance/update/UpdatePage.tsx:133
-#, c-format
-msgid "Cancel"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:91
-#, c-format
-msgid "Manage stock"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:93
-#, c-format
-msgid "Infinite"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:105
-#, c-format
-msgid "lost cannot be greater that current + incoming (max %1$s)"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:111
-#, c-format
-msgid "current stock will change from %1$s to %2$s"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:112
-#, c-format
-msgid "current stock will stay at %1$s"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:129
-#: src/paths/instance/products/list/Table.tsx:204
-#, c-format
-msgid "Incoming"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:130
-#: src/paths/instance/products/list/Table.tsx:205
-#, c-format
-msgid "Lost"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:142
-#, c-format
-msgid "Current"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:145
-#, c-format
-msgid "without stock"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:150
-#, c-format
-msgid "Next restock"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:152
-#, c-format
-msgid "Delivery address"
-msgstr ""
-
-#: src/components/form/InputTaxes.tsx:73
-#, c-format
-msgid "this product has no taxes"
-msgstr ""
-
-#: src/components/form/InputTaxes.tsx:77
-#: src/paths/instance/orders/details/DetailPage.tsx:145
-#: src/paths/instance/orders/details/DetailPage.tsx:296
-#: src/paths/instance/orders/list/Table.tsx:116
-#: src/paths/instance/transfers/create/CreatePage.tsx:84
-#, c-format
-msgid "Amount"
-msgstr ""
-
-#: src/components/form/InputTaxes.tsx:78
-#, c-format
-msgid "currency and value separated with colon"
-msgstr ""
-
-#: src/components/form/InputTaxes.tsx:84
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:78
-#, c-format
-msgid "Add"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:53
-#, c-format
-msgid "Instance"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:59
-#, c-format
-msgid "Settings"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:65
-#: src/paths/instance/orders/list/Table.tsx:60
-#, c-format
-msgid "Orders"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:71
-#: src/paths/instance/orders/create/CreatePage.tsx:258
-#: src/paths/instance/products/list/Table.tsx:48
-#, c-format
-msgid "Products"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:77
-#: src/paths/instance/transfers/list/Table.tsx:65
-#, c-format
-msgid "Transfers"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:87
-#, c-format
-msgid "Connection"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:112 src/paths/admin/list/Table.tsx:57
-#, c-format
-msgid "Instances"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:116
-#, c-format
-msgid "New"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:122
-#, c-format
-msgid "List"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:129
-#, c-format
-msgid "Log out"
-msgstr ""
-
-#: src/components/modal/index.tsx:74
-#, c-format
-msgid "Clear"
-msgstr ""
-
-#: src/components/modal/index.tsx:110 src/components/modal/index.tsx:111
-#, c-format
-msgid "should be the same"
-msgstr ""
-
-#: src/components/modal/index.tsx:111
-#, c-format
-msgid "cannot be the same as before"
-msgstr ""
-
-#: src/components/modal/index.tsx:114
-#, c-format
-msgid ""
-"You are updating the authorization token from instance %1$s with id %2$s"
-msgstr ""
-
-#: src/components/modal/index.tsx:124
-#, c-format
-msgid "Old token"
-msgstr ""
-
-#: src/components/modal/index.tsx:125
-#, c-format
-msgid "New token"
-msgstr ""
-
-#: src/components/modal/index.tsx:127
-#, c-format
-msgid "Clearing the auth token will mean public access to the instance"
-msgstr ""
-
-#: src/components/product/ProductForm.tsx:96
-#: src/paths/admin/create/CreatePage.tsx:85 src/paths/admin/list/Table.tsx:109
-#: src/paths/instance/transfers/list/Table.tsx:122
-#, c-format
-msgid "ID"
-msgstr ""
-
-#: src/components/product/ProductForm.tsx:98
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:121
-#: src/paths/instance/products/list/Table.tsx:85
-#, c-format
-msgid "Image"
-msgstr ""
-
-#: src/components/product/ProductForm.tsx:100
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:123
-#, c-format
-msgid "Unit"
-msgstr ""
-
-#: src/components/product/ProductForm.tsx:101
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:124
-#: src/paths/instance/products/list/Table.tsx:162
-#: src/paths/instance/products/list/Table.tsx:214
-#, c-format
-msgid "Price"
-msgstr ""
-
-#: src/components/product/ProductForm.tsx:103
-#: src/paths/instance/products/list/Table.tsx:90
-#, c-format
-msgid "Stock"
-msgstr ""
-
-#: src/components/product/ProductForm.tsx:105
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:128
-#: src/paths/instance/products/list/Table.tsx:88
-#, c-format
-msgid "Taxes"
-msgstr ""
-
-#: src/index.tsx:75
-#, c-format
-msgid "Server not found"
-msgstr ""
-
-#: src/index.tsx:85
-#, c-format
-msgid "Couldn't access the server"
-msgstr ""
-
-#: src/index.tsx:87 src/index.tsx:99
-#, c-format
-msgid "Got message %1$s from %2$s"
-msgstr ""
-
-#: src/index.tsx:97
-#, c-format
-msgid "Unexpected Error"
-msgstr ""
-
-#: src/paths/admin/create/CreatePage.tsx:89
-#: src/paths/instance/update/UpdatePage.tsx:108
-#, c-format
-msgid "Auth token"
-msgstr ""
-
-#: src/paths/admin/create/CreatePage.tsx:91
-#: src/paths/instance/details/DetailPage.tsx:77
-#: src/paths/instance/update/UpdatePage.tsx:110
-#, c-format
-msgid "Account address"
-msgstr ""
-
-#: src/paths/admin/create/CreatePage.tsx:93
-#: src/paths/instance/update/UpdatePage.tsx:112
-#, c-format
-msgid "Default max deposit fee"
-msgstr ""
-
-#: src/paths/admin/create/CreatePage.tsx:95
-#: src/paths/instance/update/UpdatePage.tsx:114
-#, c-format
-msgid "Default max wire fee"
-msgstr ""
-
-#: src/paths/admin/create/CreatePage.tsx:97
-#: src/paths/instance/update/UpdatePage.tsx:116
-#, c-format
-msgid "Default wire fee amortization"
-msgstr ""
-
-#: src/paths/admin/create/CreatePage.tsx:103
-#: src/paths/instance/update/UpdatePage.tsx:122
-#, c-format
-msgid "Jurisdiction"
-msgstr ""
-
-#: src/paths/admin/create/CreatePage.tsx:107
-#: src/paths/instance/update/UpdatePage.tsx:126
-#, c-format
-msgid "Default pay delay"
-msgstr ""
-
-#: src/paths/admin/create/CreatePage.tsx:109
-#: src/paths/instance/update/UpdatePage.tsx:128
-#, c-format
-msgid "Default wire transfer delay"
-msgstr ""
-
-#: src/paths/admin/create/index.tsx:58
-#, c-format
-msgid "could not create instance"
-msgstr ""
-
-#: src/paths/admin/list/Table.tsx:63 src/paths/admin/list/Table.tsx:131
-#: src/paths/instance/transfers/list/Table.tsx:71
-#, c-format
-msgid "Delete"
-msgstr ""
-
-#: src/paths/admin/list/Table.tsx:128
-#, c-format
-msgid "Edit"
-msgstr ""
-
-#: src/paths/admin/list/Table.tsx:149
-#: src/paths/instance/products/list/Table.tsx:245
-#, c-format
-msgid "There is no instances yet, add more pressing the + sign"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:237
-#, c-format
-msgid "Inventory products"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:286
-#, c-format
-msgid "Total price"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:287
-#, c-format
-msgid "Total tax"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:289
-#: src/paths/instance/orders/create/CreatePage.tsx:297
-#, c-format
-msgid "Order price"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:295
-#, c-format
-msgid "Net"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:300
-#: src/paths/instance/orders/details/DetailPage.tsx:144
-#: src/paths/instance/orders/details/DetailPage.tsx:295
-#: src/paths/instance/orders/list/Table.tsx:117
-#, c-format
-msgid "Summary"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:302
-#, c-format
-msgid "Payments options"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:303
-#, c-format
-msgid "Auto refund deadline"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:304
-#, c-format
-msgid "Refund deadline"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:305
-#, c-format
-msgid "Pay deadline"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:307
-#, c-format
-msgid "Delivery date"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:308
-#, c-format
-msgid "Location"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:312
-#, c-format
-msgid "Max fee"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:313
-#, c-format
-msgid "Max wire fee"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:314
-#, c-format
-msgid "Wire fee amortization"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:315
-#, c-format
-msgid "Fullfilment url"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:318
-#, c-format
-msgid "Extra information"
-msgstr ""
-
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:44
-#, c-format
-msgid "select a product first"
-msgstr ""
-
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:51
-#, c-format
-msgid "should be greater than 0"
-msgstr ""
-
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:58
-#, c-format
-msgid ""
-"cannot be greater than current stock and quantity previously added. max: %1$s"
-msgstr ""
-
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:64
-#, c-format
-msgid "cannot be greater than current stock %1$s"
-msgstr ""
-
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:76
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:126
-#, c-format
-msgid "Quantity"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:92
-#: src/paths/instance/orders/details/DetailPage.tsx:235
-#: src/paths/instance/orders/details/DetailPage.tsx:333
-#, c-format
-msgid "Order"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:93
-#, c-format
-msgid "claimed"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:110
-#: src/paths/instance/orders/details/DetailPage.tsx:261
-#: src/paths/instance/orders/list/Table.tsx:136
-#, c-format
-msgid "copy url"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:126
-#: src/paths/instance/orders/details/DetailPage.tsx:349
-#, c-format
-msgid "pay at"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:127
-#: src/paths/instance/orders/details/DetailPage.tsx:350
-#, c-format
-msgid "created at"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:138
-#: src/paths/instance/orders/details/DetailPage.tsx:289
-#, c-format
-msgid "Timeline"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:142
-#: src/paths/instance/orders/details/DetailPage.tsx:293
-#, c-format
-msgid "Payment details"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:146
-#: src/paths/instance/orders/details/DetailPage.tsx:299
-#: src/paths/instance/orders/details/DetailPage.tsx:363
-#, c-format
-msgid "Order status"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:156
-#: src/paths/instance/orders/details/DetailPage.tsx:308
-#, c-format
-msgid "Product list"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:236
-#, c-format
-msgid "paid"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:238
-#, c-format
-msgid "wired"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:241
-#, c-format
-msgid "refunded"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:258
-#, c-format
-msgid "refund"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:297
-#, c-format
-msgid "Refunded amount"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:298
-#, c-format
-msgid "Deposit total"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:336
-#, c-format
-msgid "unpaid"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:364
-#, c-format
-msgid "Order status URL"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:365
-#, c-format
-msgid "Pay URI"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:383
-#, c-format
-msgid ""
-"Unknown order status. This is an error, please contact the administrator."
-msgstr ""
-
-#: src/paths/instance/orders/details/index.tsx:56
-#: src/paths/instance/orders/list/index.tsx:147
-#, c-format
-msgid "refund created successfully"
-msgstr ""
-
-#: src/paths/instance/orders/details/index.tsx:59
-#: src/paths/instance/orders/list/index.tsx:150
-#, c-format
-msgid "could not create the refund"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:111
-#, c-format
-msgid "load newer orders"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:115
-#, c-format
-msgid "Date"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:131
-#: src/paths/instance/orders/list/Table.tsx:223
-#, c-format
-msgid "Refund"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:145
-#, c-format
-msgid "load older orders"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:154
-#, c-format
-msgid "No orders has been found"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:202
-#, c-format
-msgid "date"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:203
-#, c-format
-msgid "amount"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:204
-#, c-format
-msgid "reason"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:224
-#, c-format
-msgid "Max refundable:"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:226
-#, c-format
-msgid "Reason"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:226
-#, c-format
-msgid "duplicated"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:226
-#, c-format
-msgid "requested by the customer"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:226
-#, c-format
-msgid "other"
-msgstr ""
-
-#: src/paths/instance/orders/list/index.tsx:91
-#, c-format
-msgid "go to order id"
-msgstr ""
-
-#: src/paths/instance/orders/list/index.tsx:107
-#, c-format
-msgid "Paid"
-msgstr ""
-
-#: src/paths/instance/orders/list/index.tsx:108
-#, c-format
-msgid "Refunded"
-msgstr ""
-
-#: src/paths/instance/orders/list/index.tsx:109
-#, c-format
-msgid "Not wired"
-msgstr ""
-
-#: src/paths/instance/orders/list/index.tsx:110
-#, c-format
-msgid "All"
-msgstr ""
-
-#: src/paths/instance/products/create/index.tsx:48
-#: src/paths/instance/products/update/index.tsx:64
-#, c-format
-msgid "could not create product"
-msgstr ""
-
-#: src/paths/instance/products/list/Table.tsx:87
-#, c-format
-msgid "Sell"
-msgstr ""
-
-#: src/paths/instance/products/list/Table.tsx:89
-#, c-format
-msgid "Profit"
-msgstr ""
-
-#: src/paths/instance/products/list/Table.tsx:91
-#, c-format
-msgid "Sold"
-msgstr ""
-
-#: src/paths/instance/products/list/index.tsx:59
-#, c-format
-msgid "product updated successfully"
-msgstr ""
-
-#: src/paths/instance/products/list/index.tsx:62
-#, c-format
-msgid "could not update the product"
-msgstr ""
-
-#: src/paths/instance/products/list/index.tsx:70
-#, c-format
-msgid "product delete successfully"
-msgstr ""
-
-#: src/paths/instance/products/list/index.tsx:73
-#, c-format
-msgid "could not delete the product"
-msgstr ""
-
-#: src/paths/instance/tips/list/Table.tsx:59
-#, c-format
-msgid "Tips"
-msgstr ""
-
-#: src/paths/instance/tips/list/Table.tsx:111
-#, c-format
-msgid "Committed amount"
-msgstr ""
-
-#: src/paths/instance/tips/list/Table.tsx:112
-#, c-format
-msgid "Exchange initial amount"
-msgstr ""
-
-#: src/paths/instance/tips/list/Table.tsx:113
-#, c-format
-msgid "Merchant initial amount"
-msgstr ""
-
-#: src/paths/instance/tips/list/Table.tsx:148
-#, c-format
-msgid "There is no tips yet, add more pressing the + sign"
-msgstr ""
-
-#: src/paths/instance/transfers/create/CreatePage.tsx:50
-#: src/paths/instance/transfers/create/CreatePage.tsx:54
-#: src/paths/instance/transfers/create/CreatePage.tsx:55
-#: src/paths/instance/transfers/create/CreatePage.tsx:56
-#, c-format
-msgid "cannot be empty"
-msgstr ""
-
-#: src/paths/instance/transfers/create/CreatePage.tsx:51
-#, c-format
-msgid "check the id, doest look valid"
-msgstr ""
-
-#: src/paths/instance/transfers/create/CreatePage.tsx:52
-#, c-format
-msgid "should have 52 characters, current %1$s"
-msgstr ""
-
-#: src/paths/instance/transfers/create/CreatePage.tsx:57
-#, c-format
-msgid "URL doesn't have the right format"
-msgstr ""
-
-#: src/paths/instance/transfers/create/CreatePage.tsx:74
-#, c-format
-msgid "Transfer ID"
-msgstr ""
-
-#: src/paths/instance/transfers/create/CreatePage.tsx:76
-#, c-format
-msgid "Account Address"
-msgstr ""
-
-#: src/paths/instance/transfers/create/CreatePage.tsx:82
-#: src/paths/instance/transfers/list/Table.tsx:125
-#, c-format
-msgid "Exchange URL"
-msgstr ""
-
-#: src/paths/instance/transfers/create/index.tsx:49
-#, c-format
-msgid "could not inform transfer"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:118
-#, c-format
-msgid "load newer transfers"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:123
-#, c-format
-msgid "Credit"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:126
-#, c-format
-msgid "Confirmed"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:127
-#: src/paths/instance/transfers/list/index.tsx:60
-#, c-format
-msgid "Verified"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:128
-#, c-format
-msgid "Executed at"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:138
-#: src/paths/instance/transfers/list/Table.tsx:139
-#, c-format
-msgid "yes"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:138
-#: src/paths/instance/transfers/list/Table.tsx:139
-#, c-format
-msgid "no"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:140
-#, c-format
-msgid "unknown"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:145
-#, c-format
-msgid "load older transfers"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:154
-#, c-format
-msgid "There is no transfer yet, add more pressing the + sign"
-msgstr ""
diff --git a/packages/merchant-backend-ui/src/i18n/strings.ts b/packages/merchant-backend-ui/src/i18n/strings.ts
deleted file mode 100644
index 63e96949a..000000000
--- a/packages/merchant-backend-ui/src/i18n/strings.ts
+++ /dev/null
@@ -1,3445 +0,0 @@
-/*
- 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/>
- */
-
-/*eslint quote-props: ["error", "consistent"]*/
-export const strings: {[s: string]: any} = {};
-
-strings['de'] = {
- "domain": "messages",
- "locale_data": {
- "messages": {
- "": {
- "domain": "messages",
- "plural_forms": "nplurals=2; plural=(n != 1);",
- "lang": ""
- },
- "Access denied": [
- ""
- ],
- "Check your token is valid": [
- ""
- ],
- "Couldn't access the server.": [
- ""
- ],
- "Could not infer instance id from url %1$s": [
- ""
- ],
- "HTTP status #%1$s: Server reported a problem": [
- ""
- ],
- "Got message: \"%1$s\" from: %2$s": [
- ""
- ],
- "No default instance": [
- ""
- ],
- "in order to use merchant backoffice, you should create the default instance": [
- ""
- ],
- "Server reported a problem: HTTP status #%1$s": [
- ""
- ],
- "Got message: %1$s from: %2$s": [
- ""
- ],
- "Login required": [
- ""
- ],
- "Please enter your auth token. Token should have \"secret-token:\" and start with Bearer or ApiKey": [
- ""
- ],
- "Confirm": [
- ""
- ],
- "The value %1$s is invalid for a payment url": [
- ""
- ],
- "pick a date": [
- ""
- ],
- "clear": [
- ""
- ],
- "never": [
- ""
- ],
- "Image should be smaller than 1 MB": [
- ""
- ],
- "Country": [
- ""
- ],
- "Address": [
- ""
- ],
- "Building number": [
- ""
- ],
- "Building name": [
- ""
- ],
- "Street": [
- ""
- ],
- "Post code": [
- ""
- ],
- "Town location": [
- ""
- ],
- "Town": [
- ""
- ],
- "District": [
- ""
- ],
- "Country subdivision": [
- ""
- ],
- "Product id": [
- ""
- ],
- "Description": [
- ""
- ],
- "Name": [
- ""
- ],
- "loading...": [
- ""
- ],
- "no products found": [
- ""
- ],
- "no results": [
- ""
- ],
- "Deleting": [
- ""
- ],
- "Changing": [
- ""
- ],
- "Manage token": [
- ""
- ],
- "Update": [
- ""
- ],
- "Remove": [
- ""
- ],
- "Cancel": [
- ""
- ],
- "Manage stock": [
- ""
- ],
- "Infinite": [
- ""
- ],
- "lost cannot be greater that current + incoming (max %1$s)": [
- ""
- ],
- "current stock will change from %1$s to %2$s": [
- ""
- ],
- "current stock will stay at %1$s": [
- ""
- ],
- "Incoming": [
- ""
- ],
- "Lost": [
- ""
- ],
- "Current": [
- ""
- ],
- "without stock": [
- ""
- ],
- "Next restock": [
- ""
- ],
- "Delivery address": [
- ""
- ],
- "this product has no taxes": [
- ""
- ],
- "Amount": [
- ""
- ],
- "currency and value separated with colon": [
- ""
- ],
- "Add": [
- ""
- ],
- "Instance": [
- ""
- ],
- "Settings": [
- ""
- ],
- "Orders": [
- ""
- ],
- "Products": [
- ""
- ],
- "Transfers": [
- ""
- ],
- "Connection": [
- ""
- ],
- "Instances": [
- ""
- ],
- "New": [
- ""
- ],
- "List": [
- ""
- ],
- "Log out": [
- ""
- ],
- "Clear": [
- ""
- ],
- "should be the same": [
- ""
- ],
- "cannot be the same as before": [
- ""
- ],
- "You are updating the authorization token from instance %1$s with id %2$s": [
- ""
- ],
- "Old token": [
- ""
- ],
- "New token": [
- ""
- ],
- "Clearing the auth token will mean public access to the instance": [
- ""
- ],
- "ID": [
- ""
- ],
- "Image": [
- ""
- ],
- "Unit": [
- ""
- ],
- "Price": [
- ""
- ],
- "Stock": [
- ""
- ],
- "Taxes": [
- ""
- ],
- "Server not found": [
- ""
- ],
- "Couldn't access the server": [
- ""
- ],
- "Got message %1$s from %2$s": [
- ""
- ],
- "Unexpected Error": [
- ""
- ],
- "Auth token": [
- ""
- ],
- "Account address": [
- ""
- ],
- "Default max deposit fee": [
- ""
- ],
- "Default max wire fee": [
- ""
- ],
- "Default wire fee amortization": [
- ""
- ],
- "Jurisdiction": [
- ""
- ],
- "Default pay delay": [
- ""
- ],
- "Default wire transfer delay": [
- ""
- ],
- "could not create instance": [
- ""
- ],
- "Delete": [
- ""
- ],
- "Edit": [
- ""
- ],
- "There is no instances yet, add more pressing the + sign": [
- ""
- ],
- "Inventory products": [
- ""
- ],
- "Total price": [
- ""
- ],
- "Total tax": [
- ""
- ],
- "Order price": [
- ""
- ],
- "Net": [
- ""
- ],
- "Summary": [
- ""
- ],
- "Payments options": [
- ""
- ],
- "Auto refund deadline": [
- ""
- ],
- "Refund deadline": [
- ""
- ],
- "Pay deadline": [
- ""
- ],
- "Delivery date": [
- ""
- ],
- "Location": [
- ""
- ],
- "Max fee": [
- ""
- ],
- "Max wire fee": [
- ""
- ],
- "Wire fee amortization": [
- ""
- ],
- "Fullfilment url": [
- ""
- ],
- "Extra information": [
- ""
- ],
- "select a product first": [
- ""
- ],
- "should be greater than 0": [
- ""
- ],
- "cannot be greater than current stock and quantity previously added. max: %1$s": [
- ""
- ],
- "cannot be greater than current stock %1$s": [
- ""
- ],
- "Quantity": [
- ""
- ],
- "Order": [
- ""
- ],
- "claimed": [
- ""
- ],
- "copy url": [
- ""
- ],
- "pay at": [
- ""
- ],
- "created at": [
- ""
- ],
- "Timeline": [
- ""
- ],
- "Payment details": [
- ""
- ],
- "Order status": [
- ""
- ],
- "Product list": [
- ""
- ],
- "paid": [
- ""
- ],
- "wired": [
- ""
- ],
- "refunded": [
- ""
- ],
- "refund": [
- ""
- ],
- "Refunded amount": [
- ""
- ],
- "Deposit total": [
- ""
- ],
- "unpaid": [
- ""
- ],
- "Order status URL": [
- ""
- ],
- "Pay URI": [
- ""
- ],
- "Unknown order status. This is an error, please contact the administrator.": [
- ""
- ],
- "refund created successfully": [
- ""
- ],
- "could not create the refund": [
- ""
- ],
- "load newer orders": [
- ""
- ],
- "Date": [
- ""
- ],
- "Refund": [
- ""
- ],
- "load older orders": [
- ""
- ],
- "No orders has been found": [
- ""
- ],
- "date": [
- ""
- ],
- "amount": [
- ""
- ],
- "reason": [
- ""
- ],
- "Max refundable:": [
- ""
- ],
- "Reason": [
- ""
- ],
- "duplicated": [
- ""
- ],
- "requested by the customer": [
- ""
- ],
- "other": [
- ""
- ],
- "go to order id": [
- ""
- ],
- "Paid": [
- ""
- ],
- "Refunded": [
- ""
- ],
- "Not wired": [
- ""
- ],
- "All": [
- ""
- ],
- "could not create product": [
- ""
- ],
- "Sell": [
- ""
- ],
- "Profit": [
- ""
- ],
- "Sold": [
- ""
- ],
- "product updated successfully": [
- ""
- ],
- "could not update the product": [
- ""
- ],
- "product delete successfully": [
- ""
- ],
- "could not delete the product": [
- ""
- ],
- "Tips": [
- ""
- ],
- "Committed amount": [
- ""
- ],
- "Exchange initial amount": [
- ""
- ],
- "Merchant initial amount": [
- ""
- ],
- "There is no tips yet, add more pressing the + sign": [
- ""
- ],
- "cannot be empty": [
- ""
- ],
- "check the id, doest look valid": [
- ""
- ],
- "should have 52 characters, current %1$s": [
- ""
- ],
- "URL doesn't have the right format": [
- ""
- ],
- "Transfer ID": [
- ""
- ],
- "Account Address": [
- ""
- ],
- "Exchange URL": [
- ""
- ],
- "could not inform transfer": [
- ""
- ],
- "load newer transfers": [
- ""
- ],
- "Credit": [
- ""
- ],
- "Confirmed": [
- ""
- ],
- "Verified": [
- ""
- ],
- "Executed at": [
- ""
- ],
- "yes": [
- ""
- ],
- "no": [
- ""
- ],
- "unknown": [
- ""
- ],
- "load older transfers": [
- ""
- ],
- "There is no transfer yet, add more pressing the + sign": [
- ""
- ]
- }
- }
-};
-
-strings['en'] = {
- "domain": "messages",
- "locale_data": {
- "messages": {
- "": {
- "domain": "messages",
- "plural_forms": "nplurals=2; plural=(n != 1);",
- "lang": ""
- },
- "Access denied": [
- ""
- ],
- "Check your token is valid": [
- ""
- ],
- "Couldn't access the server.": [
- ""
- ],
- "Could not infer instance id from url %1$s": [
- ""
- ],
- "HTTP status #%1$s: Server reported a problem": [
- ""
- ],
- "Got message: \"%1$s\" from: %2$s": [
- ""
- ],
- "No default instance": [
- ""
- ],
- "in order to use merchant backoffice, you should create the default instance": [
- ""
- ],
- "Server reported a problem: HTTP status #%1$s": [
- ""
- ],
- "Got message: %1$s from: %2$s": [
- ""
- ],
- "Login required": [
- ""
- ],
- "Please enter your auth token. Token should have \"secret-token:\" and start with Bearer or ApiKey": [
- ""
- ],
- "Confirm": [
- ""
- ],
- "The value %1$s is invalid for a payment url": [
- ""
- ],
- "pick a date": [
- ""
- ],
- "clear": [
- ""
- ],
- "never": [
- ""
- ],
- "Image should be smaller than 1 MB": [
- ""
- ],
- "Country": [
- ""
- ],
- "Address": [
- ""
- ],
- "Building number": [
- ""
- ],
- "Building name": [
- ""
- ],
- "Street": [
- ""
- ],
- "Post code": [
- ""
- ],
- "Town location": [
- ""
- ],
- "Town": [
- ""
- ],
- "District": [
- ""
- ],
- "Country subdivision": [
- ""
- ],
- "Product id": [
- ""
- ],
- "Description": [
- ""
- ],
- "Name": [
- ""
- ],
- "loading...": [
- ""
- ],
- "no products found": [
- ""
- ],
- "no results": [
- ""
- ],
- "Deleting": [
- ""
- ],
- "Changing": [
- ""
- ],
- "Manage token": [
- ""
- ],
- "Update": [
- ""
- ],
- "Remove": [
- ""
- ],
- "Cancel": [
- ""
- ],
- "Manage stock": [
- ""
- ],
- "Infinite": [
- ""
- ],
- "lost cannot be greater that current + incoming (max %1$s)": [
- ""
- ],
- "current stock will change from %1$s to %2$s": [
- ""
- ],
- "current stock will stay at %1$s": [
- ""
- ],
- "Incoming": [
- ""
- ],
- "Lost": [
- ""
- ],
- "Current": [
- ""
- ],
- "without stock": [
- ""
- ],
- "Next restock": [
- ""
- ],
- "Delivery address": [
- ""
- ],
- "this product has no taxes": [
- ""
- ],
- "Amount": [
- ""
- ],
- "currency and value separated with colon": [
- ""
- ],
- "Add": [
- ""
- ],
- "Instance": [
- ""
- ],
- "Settings": [
- ""
- ],
- "Orders": [
- ""
- ],
- "Products": [
- ""
- ],
- "Transfers": [
- ""
- ],
- "Connection": [
- ""
- ],
- "Instances": [
- ""
- ],
- "New": [
- ""
- ],
- "List": [
- ""
- ],
- "Log out": [
- ""
- ],
- "Clear": [
- ""
- ],
- "should be the same": [
- ""
- ],
- "cannot be the same as before": [
- ""
- ],
- "You are updating the authorization token from instance %1$s with id %2$s": [
- ""
- ],
- "Old token": [
- ""
- ],
- "New token": [
- ""
- ],
- "Clearing the auth token will mean public access to the instance": [
- ""
- ],
- "ID": [
- ""
- ],
- "Image": [
- ""
- ],
- "Unit": [
- ""
- ],
- "Price": [
- ""
- ],
- "Stock": [
- ""
- ],
- "Taxes": [
- ""
- ],
- "Server not found": [
- ""
- ],
- "Couldn't access the server": [
- ""
- ],
- "Got message %1$s from %2$s": [
- ""
- ],
- "Unexpected Error": [
- ""
- ],
- "Auth token": [
- ""
- ],
- "Account address": [
- ""
- ],
- "Default max deposit fee": [
- ""
- ],
- "Default max wire fee": [
- ""
- ],
- "Default wire fee amortization": [
- ""
- ],
- "Jurisdiction": [
- ""
- ],
- "Default pay delay": [
- ""
- ],
- "Default wire transfer delay": [
- ""
- ],
- "could not create instance": [
- ""
- ],
- "Delete": [
- ""
- ],
- "Edit": [
- ""
- ],
- "There is no instances yet, add more pressing the + sign": [
- ""
- ],
- "Inventory products": [
- ""
- ],
- "Total price": [
- ""
- ],
- "Total tax": [
- ""
- ],
- "Order price": [
- ""
- ],
- "Net": [
- ""
- ],
- "Summary": [
- ""
- ],
- "Payments options": [
- ""
- ],
- "Auto refund deadline": [
- ""
- ],
- "Refund deadline": [
- ""
- ],
- "Pay deadline": [
- ""
- ],
- "Delivery date": [
- ""
- ],
- "Location": [
- ""
- ],
- "Max fee": [
- ""
- ],
- "Max wire fee": [
- ""
- ],
- "Wire fee amortization": [
- ""
- ],
- "Fullfilment url": [
- ""
- ],
- "Extra information": [
- ""
- ],
- "select a product first": [
- ""
- ],
- "should be greater than 0": [
- ""
- ],
- "cannot be greater than current stock and quantity previously added. max: %1$s": [
- ""
- ],
- "cannot be greater than current stock %1$s": [
- ""
- ],
- "Quantity": [
- ""
- ],
- "Order": [
- ""
- ],
- "claimed": [
- ""
- ],
- "copy url": [
- ""
- ],
- "pay at": [
- ""
- ],
- "created at": [
- ""
- ],
- "Timeline": [
- ""
- ],
- "Payment details": [
- ""
- ],
- "Order status": [
- ""
- ],
- "Product list": [
- ""
- ],
- "paid": [
- ""
- ],
- "wired": [
- ""
- ],
- "refunded": [
- ""
- ],
- "refund": [
- ""
- ],
- "Refunded amount": [
- ""
- ],
- "Deposit total": [
- ""
- ],
- "unpaid": [
- ""
- ],
- "Order status URL": [
- ""
- ],
- "Pay URI": [
- ""
- ],
- "Unknown order status. This is an error, please contact the administrator.": [
- ""
- ],
- "refund created successfully": [
- ""
- ],
- "could not create the refund": [
- ""
- ],
- "load newer orders": [
- ""
- ],
- "Date": [
- ""
- ],
- "Refund": [
- ""
- ],
- "load older orders": [
- ""
- ],
- "No orders has been found": [
- ""
- ],
- "date": [
- ""
- ],
- "amount": [
- ""
- ],
- "reason": [
- ""
- ],
- "Max refundable:": [
- ""
- ],
- "Reason": [
- ""
- ],
- "duplicated": [
- ""
- ],
- "requested by the customer": [
- ""
- ],
- "other": [
- ""
- ],
- "go to order id": [
- ""
- ],
- "Paid": [
- ""
- ],
- "Refunded": [
- ""
- ],
- "Not wired": [
- ""
- ],
- "All": [
- ""
- ],
- "could not create product": [
- ""
- ],
- "Sell": [
- ""
- ],
- "Profit": [
- ""
- ],
- "Sold": [
- ""
- ],
- "product updated successfully": [
- ""
- ],
- "could not update the product": [
- ""
- ],
- "product delete successfully": [
- ""
- ],
- "could not delete the product": [
- ""
- ],
- "Tips": [
- ""
- ],
- "Committed amount": [
- ""
- ],
- "Exchange initial amount": [
- ""
- ],
- "Merchant initial amount": [
- ""
- ],
- "There is no tips yet, add more pressing the + sign": [
- ""
- ],
- "cannot be empty": [
- ""
- ],
- "check the id, doest look valid": [
- ""
- ],
- "should have 52 characters, current %1$s": [
- ""
- ],
- "URL doesn't have the right format": [
- ""
- ],
- "Transfer ID": [
- ""
- ],
- "Account Address": [
- ""
- ],
- "Exchange URL": [
- ""
- ],
- "could not inform transfer": [
- ""
- ],
- "load newer transfers": [
- ""
- ],
- "Credit": [
- ""
- ],
- "Confirmed": [
- ""
- ],
- "Verified": [
- ""
- ],
- "Executed at": [
- ""
- ],
- "yes": [
- ""
- ],
- "no": [
- ""
- ],
- "unknown": [
- ""
- ],
- "load older transfers": [
- ""
- ],
- "There is no transfer yet, add more pressing the + sign": [
- ""
- ]
- }
- }
-};
-
-strings['es'] = {
- "domain": "messages",
- "locale_data": {
- "messages": {
- "": {
- "domain": "messages",
- "plural_forms": "nplurals=2; plural=(n != 1);",
- "lang": ""
- },
- "Access denied": [
- "Acceso denegado"
- ],
- "Check your token is valid": [
- "Verifica que el token sea valido"
- ],
- "Couldn't access the server.": [
- "No se pudo acceder al servidor"
- ],
- "Could not infer instance id from url %1$s": [
- "No se pudo inferir el id de la instancia con la url %1$s"
- ],
- "HTTP status #%1$s: Server reported a problem": [
- "HTTP status #%1$s: Servidor reporto un problema"
- ],
- "Got message: \"%1$s\" from: %2$s": [
- "Recivimos el mensaje %1$s desde %2$s"
- ],
- "No default instance": [
- "Sin instancia default"
- ],
- "in order to use merchant backoffice, you should create the default instance": [
- "para usar el merchant backoffice, debería crear la instancia default"
- ],
- "Server reported a problem: HTTP status #%1$s": [
- "Servidir reporto un problema: HTTP status #%1$s"
- ],
- "Got message: %1$s from: %2$s": [
- "Recivimos el mensaje %1$s desde %2$s"
- ],
- "Login required": [
- "Login necesario"
- ],
- "Please enter your auth token. Token should have \"secret-token:\" and start with Bearer or ApiKey": [
- "Por favor ingrese su token de autorización. El token debe tener \"secret-token\" y comenzar con Bearer o ApiKey"
- ],
- "Confirm": [
- "Confirmar"
- ],
- "The value %1$s is invalid for a payment url": [
- "El valor %1$s es invalido para una URL de pago"
- ],
- "pick a date": [
- "elegir una fecha"
- ],
- "clear": [
- "Limpiar"
- ],
- "never": [
- "nunca"
- ],
- "Image should be smaller than 1 MB": [
- "La imagen debe ser mas chica que 1 MB"
- ],
- "Country": [
- "País"
- ],
- "Address": [
- "Dirección"
- ],
- "Building number": [
- "Número de edificio"
- ],
- "Building name": [
- "Nombre de edificio"
- ],
- "Street": [
- "Calle"
- ],
- "Post code": [
- "Código postal"
- ],
- "Town location": [
- "Ubicación de ciudad"
- ],
- "Town": [
- "Ciudad"
- ],
- "District": [
- "Distrito"
- ],
- "Country subdivision": [
- "Provincia"
- ],
- "Product id": [
- "Id de producto"
- ],
- "Description": [
- "Descripcion"
- ],
- "Name": [
- "Nombre"
- ],
- "loading...": [
- "Cargando..."
- ],
- "no products found": [
- "No se encontraron productos"
- ],
- "no results": [
- "Sin resultados"
- ],
- "Deleting": [
- "Borrando"
- ],
- "Changing": [
- "Cambiando"
- ],
- "Manage token": [
- "Administrar token"
- ],
- "Update": [
- "Actualizar"
- ],
- "Remove": [
- "Eliminar"
- ],
- "Cancel": [
- "Cancelar"
- ],
- "Manage stock": [
- "Administrar stock"
- ],
- "Infinite": [
- "Inifinito"
- ],
- "lost cannot be greater that current + incoming (max %1$s)": [
- "no puede ser mayor al stock actual %1$s"
- ],
- "current stock will change from %1$s to %2$s": [
- "stock actual cambiará desde %1$s a %2$s"
- ],
- "current stock will stay at %1$s": [
- "stock actual seguirá en %1$s"
- ],
- "Incoming": [
- "Ingresando"
- ],
- "Lost": [
- "Perdido"
- ],
- "Current": [
- "Actual"
- ],
- "without stock": [
- "sin stock"
- ],
- "Next restock": [
- "Próximo reabastecimiento"
- ],
- "Delivery address": [
- "Dirección de entrega"
- ],
- "this product has no taxes": [
- "este producto no tiene impuestos"
- ],
- "Amount": [
- "Monto"
- ],
- "currency and value separated with colon": [
- "Moneda y valor separado por dos puntos"
- ],
- "Add": [
- "Agregar"
- ],
- "Instance": [
- "Instancia"
- ],
- "Settings": [
- "Configuración"
- ],
- "Orders": [
- "Ordenes"
- ],
- "Products": [
- "Productos"
- ],
- "Transfers": [
- "Transferencias"
- ],
- "Connection": [
- "Conexión"
- ],
- "Instances": [
- "Instancias"
- ],
- "New": [
- "Nuevo"
- ],
- "List": [
- "Lista"
- ],
- "Log out": [
- "Salir"
- ],
- "Clear": [
- "Limpiar"
- ],
- "should be the same": [
- "deberían ser iguales"
- ],
- "cannot be the same as before": [
- "no puede ser igual al anterior"
- ],
- "You are updating the authorization token from instance %1$s with id %2$s": [
- "Está actualizando el token de autorización para la instancia %1$s con id %2$s"
- ],
- "Old token": [
- "Viejo token"
- ],
- "New token": [
- "Nuevo token"
- ],
- "Clearing the auth token will mean public access to the instance": [
- "Limpiar el token de autorización significa acceso publico a la instancia"
- ],
- "ID": [
- "ID"
- ],
- "Image": [
- "Imagen"
- ],
- "Unit": [
- "Unidad"
- ],
- "Price": [
- "Precio"
- ],
- "Stock": [
- "Stock"
- ],
- "Taxes": [
- "Impuesto"
- ],
- "Server not found": [
- "Servidor no encontrado"
- ],
- "Couldn't access the server": [
- "No se pudo aceder al servidor"
- ],
- "Got message %1$s from %2$s": [
- "Recivimos el mensaje %1$s desde %2$s"
- ],
- "Unexpected Error": [
- "Error inesperado"
- ],
- "Auth token": [
- "Token de autorización"
- ],
- "Account address": [
- "Dirección de cuenta"
- ],
- "Default max deposit fee": [
- "Impuesto máximo de deposito por omisión"
- ],
- "Default max wire fee": [
- "Impuesto máximo de transferencia por omisión"
- ],
- "Default wire fee amortization": [
- "Amortización de impuesto de transferencia por omisión"
- ],
- "Jurisdiction": [
- "Jurisdicción"
- ],
- "Default pay delay": [
- "Retrazo de pago por omisión"
- ],
- "Default wire transfer delay": [
- "Retrazo de transferencia por omisión"
- ],
- "could not create instance": [
- "no se pudo crear la instancia"
- ],
- "Delete": [
- "Borrando"
- ],
- "Edit": [
- ""
- ],
- "There is no instances yet, add more pressing the + sign": [
- "No hay instancias todavían, agregue mas presionando el signo +"
- ],
- "Inventory products": [
- "Productos de inventario"
- ],
- "Total price": [
- "Precio total"
- ],
- "Total tax": [
- "Impuesto total"
- ],
- "Order price": [
- "Precio de la orden"
- ],
- "Net": [
- "Neto"
- ],
- "Summary": [
- "Resumen"
- ],
- "Payments options": [
- "Opciones de pago"
- ],
- "Auto refund deadline": [
- "Plazo de reembolso automático"
- ],
- "Refund deadline": [
- "Plazo de reembolso"
- ],
- "Pay deadline": [
- "Plazo de pago"
- ],
- "Delivery date": [
- "Fecha de entrega"
- ],
- "Location": [
- "Ubicación"
- ],
- "Max fee": [
- "Impuesto máximo"
- ],
- "Max wire fee": [
- "Impuesto de transferencia máximo"
- ],
- "Wire fee amortization": [
- "Amortización de impuesto de transferencia"
- ],
- "Fullfilment url": [
- "URL de completitud"
- ],
- "Extra information": [
- "Información extra"
- ],
- "select a product first": [
- "seleccione un producto primero"
- ],
- "should be greater than 0": [
- "La imagen debe ser mas chica que 1 MB"
- ],
- "cannot be greater than current stock and quantity previously added. max: %1$s": [
- "no puede ser mayor al stock actual y la cantidad previamente agregada. máximo: %1$s"
- ],
- "cannot be greater than current stock %1$s": [
- "no puede ser mayor al stock actual %1$s"
- ],
- "Quantity": [
- "Cantidad"
- ],
- "Order": [
- "Orden"
- ],
- "claimed": [
- "reclamado"
- ],
- "copy url": [
- "copiar url"
- ],
- "pay at": [
- "pagar en"
- ],
- "created at": [
- "creado"
- ],
- "Timeline": [
- "Cronología"
- ],
- "Payment details": [
- "Detalles de pago"
- ],
- "Order status": [
- "Estado de orden"
- ],
- "Product list": [
- "Lista de producto"
- ],
- "paid": [
- "pagados"
- ],
- "wired": [
- "transferido"
- ],
- "refunded": [
- "reembolzado"
- ],
- "refund": [
- "reembolzar"
- ],
- "Refunded amount": [
- "Monto reembolzado"
- ],
- "Deposit total": [
- "Total depositado"
- ],
- "unpaid": [
- "impago"
- ],
- "Order status URL": [
- "URL de estado de orden"
- ],
- "Pay URI": [
- "URI de pago"
- ],
- "Unknown order status. This is an error, please contact the administrator.": [
- "Estado de orden desconocido. Esto es un error, por favor contacte a su administrador"
- ],
- "refund created successfully": [
- "reembolzo creado satisfactoriamente"
- ],
- "could not create the refund": [
- "No se pudo aceder al servidor"
- ],
- "load newer orders": [
- "cargar nuevas ordenes"
- ],
- "Date": [
- "Fecha"
- ],
- "Refund": [
- "Reembolzar"
- ],
- "load older orders": [
- "cargar viejas ordenes"
- ],
- "No orders has been found": [
- "No se enconraron ordenes"
- ],
- "date": [
- "fecha"
- ],
- "amount": [
- "monto"
- ],
- "reason": [
- "razón"
- ],
- "Max refundable:": [
- "Máximo reembolzable:"
- ],
- "Reason": [
- "Razón"
- ],
- "duplicated": [
- "duplicado"
- ],
- "requested by the customer": [
- "pedido por el consumidor"
- ],
- "other": [
- "otro"
- ],
- "go to order id": [
- "ir a id de orden"
- ],
- "Paid": [
- "Pagado"
- ],
- "Refunded": [
- "Reembolzado"
- ],
- "Not wired": [
- "No transferido"
- ],
- "All": [
- "Todo"
- ],
- "could not create product": [
- "no se pudo crear el producto"
- ],
- "Sell": [
- "Venta"
- ],
- "Profit": [
- "Ganancia"
- ],
- "Sold": [
- "Vendido"
- ],
- "product updated successfully": [
- "producto actualizado correctamente"
- ],
- "could not update the product": [
- "no se pudo actualizar el producto"
- ],
- "product delete successfully": [
- "producto fue eliminado correctamente"
- ],
- "could not delete the product": [
- "no se pudo eliminar el producto"
- ],
- "Tips": [
- "Propinas"
- ],
- "Committed amount": [
- ""
- ],
- "Exchange initial amount": [
- ""
- ],
- "Merchant initial amount": [
- ""
- ],
- "There is no tips yet, add more pressing the + sign": [
- "No hay propinas todavía, agregar mas presionando el signo +"
- ],
- "cannot be empty": [
- "no puede ser vacío"
- ],
- "check the id, doest look valid": [
- "verificar el id, no parece válido"
- ],
- "should have 52 characters, current %1$s": [
- "debería tener 52 caracteres, actualmente %1$s"
- ],
- "URL doesn't have the right format": [
- "La URL no tiene el formato correcto"
- ],
- "Transfer ID": [
- "Transferencias"
- ],
- "Account Address": [
- "Dirección de cuenta"
- ],
- "Exchange URL": [
- "URL del Exchange"
- ],
- "could not inform transfer": [
- "no se pudo crear la instancia"
- ],
- "load newer transfers": [
- "cargar nuevas ordenes"
- ],
- "Credit": [
- "Crédito"
- ],
- "Confirmed": [
- "Confirmar"
- ],
- "Verified": [
- "Verificado"
- ],
- "Executed at": [
- "creado"
- ],
- "yes": [
- "si"
- ],
- "no": [
- "no"
- ],
- "unknown": [
- "desconocido"
- ],
- "load older transfers": [
- "cargar viejas transferencias"
- ],
- "There is no transfer yet, add more pressing the + sign": [
- "No hay transferencias todavía, agregar mas presionando el signo +"
- ]
- }
- }
-};
-
-strings['fr'] = {
- "domain": "messages",
- "locale_data": {
- "messages": {
- "": {
- "domain": "messages",
- "plural_forms": "nplurals=2; plural=(n != 1);",
- "lang": ""
- },
- "Access denied": [
- ""
- ],
- "Check your token is valid": [
- ""
- ],
- "Couldn't access the server.": [
- ""
- ],
- "Could not infer instance id from url %1$s": [
- ""
- ],
- "HTTP status #%1$s: Server reported a problem": [
- ""
- ],
- "Got message: \"%1$s\" from: %2$s": [
- ""
- ],
- "No default instance": [
- ""
- ],
- "in order to use merchant backoffice, you should create the default instance": [
- ""
- ],
- "Server reported a problem: HTTP status #%1$s": [
- ""
- ],
- "Got message: %1$s from: %2$s": [
- ""
- ],
- "Login required": [
- ""
- ],
- "Please enter your auth token. Token should have \"secret-token:\" and start with Bearer or ApiKey": [
- ""
- ],
- "Confirm": [
- ""
- ],
- "The value %1$s is invalid for a payment url": [
- ""
- ],
- "pick a date": [
- ""
- ],
- "clear": [
- ""
- ],
- "never": [
- ""
- ],
- "Image should be smaller than 1 MB": [
- ""
- ],
- "Country": [
- ""
- ],
- "Address": [
- ""
- ],
- "Building number": [
- ""
- ],
- "Building name": [
- ""
- ],
- "Street": [
- ""
- ],
- "Post code": [
- ""
- ],
- "Town location": [
- ""
- ],
- "Town": [
- ""
- ],
- "District": [
- ""
- ],
- "Country subdivision": [
- ""
- ],
- "Product id": [
- ""
- ],
- "Description": [
- ""
- ],
- "Name": [
- ""
- ],
- "loading...": [
- ""
- ],
- "no products found": [
- ""
- ],
- "no results": [
- ""
- ],
- "Deleting": [
- ""
- ],
- "Changing": [
- ""
- ],
- "Manage token": [
- ""
- ],
- "Update": [
- ""
- ],
- "Remove": [
- ""
- ],
- "Cancel": [
- ""
- ],
- "Manage stock": [
- ""
- ],
- "Infinite": [
- ""
- ],
- "lost cannot be greater that current + incoming (max %1$s)": [
- ""
- ],
- "current stock will change from %1$s to %2$s": [
- ""
- ],
- "current stock will stay at %1$s": [
- ""
- ],
- "Incoming": [
- ""
- ],
- "Lost": [
- ""
- ],
- "Current": [
- ""
- ],
- "without stock": [
- ""
- ],
- "Next restock": [
- ""
- ],
- "Delivery address": [
- ""
- ],
- "this product has no taxes": [
- ""
- ],
- "Amount": [
- ""
- ],
- "currency and value separated with colon": [
- ""
- ],
- "Add": [
- ""
- ],
- "Instance": [
- ""
- ],
- "Settings": [
- ""
- ],
- "Orders": [
- ""
- ],
- "Products": [
- ""
- ],
- "Transfers": [
- ""
- ],
- "Connection": [
- ""
- ],
- "Instances": [
- ""
- ],
- "New": [
- ""
- ],
- "List": [
- ""
- ],
- "Log out": [
- ""
- ],
- "Clear": [
- ""
- ],
- "should be the same": [
- ""
- ],
- "cannot be the same as before": [
- ""
- ],
- "You are updating the authorization token from instance %1$s with id %2$s": [
- ""
- ],
- "Old token": [
- ""
- ],
- "New token": [
- ""
- ],
- "Clearing the auth token will mean public access to the instance": [
- ""
- ],
- "ID": [
- ""
- ],
- "Image": [
- ""
- ],
- "Unit": [
- ""
- ],
- "Price": [
- ""
- ],
- "Stock": [
- ""
- ],
- "Taxes": [
- ""
- ],
- "Server not found": [
- ""
- ],
- "Couldn't access the server": [
- ""
- ],
- "Got message %1$s from %2$s": [
- ""
- ],
- "Unexpected Error": [
- ""
- ],
- "Auth token": [
- ""
- ],
- "Account address": [
- ""
- ],
- "Default max deposit fee": [
- ""
- ],
- "Default max wire fee": [
- ""
- ],
- "Default wire fee amortization": [
- ""
- ],
- "Jurisdiction": [
- ""
- ],
- "Default pay delay": [
- ""
- ],
- "Default wire transfer delay": [
- ""
- ],
- "could not create instance": [
- ""
- ],
- "Delete": [
- ""
- ],
- "Edit": [
- ""
- ],
- "There is no instances yet, add more pressing the + sign": [
- ""
- ],
- "Inventory products": [
- ""
- ],
- "Total price": [
- ""
- ],
- "Total tax": [
- ""
- ],
- "Order price": [
- ""
- ],
- "Net": [
- ""
- ],
- "Summary": [
- ""
- ],
- "Payments options": [
- ""
- ],
- "Auto refund deadline": [
- ""
- ],
- "Refund deadline": [
- ""
- ],
- "Pay deadline": [
- ""
- ],
- "Delivery date": [
- ""
- ],
- "Location": [
- ""
- ],
- "Max fee": [
- ""
- ],
- "Max wire fee": [
- ""
- ],
- "Wire fee amortization": [
- ""
- ],
- "Fullfilment url": [
- ""
- ],
- "Extra information": [
- ""
- ],
- "select a product first": [
- ""
- ],
- "should be greater than 0": [
- ""
- ],
- "cannot be greater than current stock and quantity previously added. max: %1$s": [
- ""
- ],
- "cannot be greater than current stock %1$s": [
- ""
- ],
- "Quantity": [
- ""
- ],
- "Order": [
- ""
- ],
- "claimed": [
- ""
- ],
- "copy url": [
- ""
- ],
- "pay at": [
- ""
- ],
- "created at": [
- ""
- ],
- "Timeline": [
- ""
- ],
- "Payment details": [
- ""
- ],
- "Order status": [
- ""
- ],
- "Product list": [
- ""
- ],
- "paid": [
- ""
- ],
- "wired": [
- ""
- ],
- "refunded": [
- ""
- ],
- "refund": [
- ""
- ],
- "Refunded amount": [
- ""
- ],
- "Deposit total": [
- ""
- ],
- "unpaid": [
- ""
- ],
- "Order status URL": [
- ""
- ],
- "Pay URI": [
- ""
- ],
- "Unknown order status. This is an error, please contact the administrator.": [
- ""
- ],
- "refund created successfully": [
- ""
- ],
- "could not create the refund": [
- ""
- ],
- "load newer orders": [
- ""
- ],
- "Date": [
- ""
- ],
- "Refund": [
- ""
- ],
- "load older orders": [
- ""
- ],
- "No orders has been found": [
- ""
- ],
- "date": [
- ""
- ],
- "amount": [
- ""
- ],
- "reason": [
- ""
- ],
- "Max refundable:": [
- ""
- ],
- "Reason": [
- ""
- ],
- "duplicated": [
- ""
- ],
- "requested by the customer": [
- ""
- ],
- "other": [
- ""
- ],
- "go to order id": [
- ""
- ],
- "Paid": [
- ""
- ],
- "Refunded": [
- ""
- ],
- "Not wired": [
- ""
- ],
- "All": [
- ""
- ],
- "could not create product": [
- ""
- ],
- "Sell": [
- ""
- ],
- "Profit": [
- ""
- ],
- "Sold": [
- ""
- ],
- "product updated successfully": [
- ""
- ],
- "could not update the product": [
- ""
- ],
- "product delete successfully": [
- ""
- ],
- "could not delete the product": [
- ""
- ],
- "Tips": [
- ""
- ],
- "Committed amount": [
- ""
- ],
- "Exchange initial amount": [
- ""
- ],
- "Merchant initial amount": [
- ""
- ],
- "There is no tips yet, add more pressing the + sign": [
- ""
- ],
- "cannot be empty": [
- ""
- ],
- "check the id, doest look valid": [
- ""
- ],
- "should have 52 characters, current %1$s": [
- ""
- ],
- "URL doesn't have the right format": [
- ""
- ],
- "Transfer ID": [
- ""
- ],
- "Account Address": [
- ""
- ],
- "Exchange URL": [
- ""
- ],
- "could not inform transfer": [
- ""
- ],
- "load newer transfers": [
- ""
- ],
- "Credit": [
- ""
- ],
- "Confirmed": [
- ""
- ],
- "Verified": [
- ""
- ],
- "Executed at": [
- ""
- ],
- "yes": [
- ""
- ],
- "no": [
- ""
- ],
- "unknown": [
- ""
- ],
- "load older transfers": [
- ""
- ],
- "There is no transfer yet, add more pressing the + sign": [
- ""
- ]
- }
- }
-};
-
-strings['it'] = {
- "domain": "messages",
- "locale_data": {
- "messages": {
- "": {
- "domain": "messages",
- "plural_forms": "nplurals=2; plural=(n != 1);",
- "lang": ""
- },
- "Access denied": [
- ""
- ],
- "Check your token is valid": [
- ""
- ],
- "Couldn't access the server.": [
- ""
- ],
- "Could not infer instance id from url %1$s": [
- ""
- ],
- "HTTP status #%1$s: Server reported a problem": [
- ""
- ],
- "Got message: \"%1$s\" from: %2$s": [
- ""
- ],
- "No default instance": [
- ""
- ],
- "in order to use merchant backoffice, you should create the default instance": [
- ""
- ],
- "Server reported a problem: HTTP status #%1$s": [
- ""
- ],
- "Got message: %1$s from: %2$s": [
- ""
- ],
- "Login required": [
- ""
- ],
- "Please enter your auth token. Token should have \"secret-token:\" and start with Bearer or ApiKey": [
- ""
- ],
- "Confirm": [
- ""
- ],
- "The value %1$s is invalid for a payment url": [
- ""
- ],
- "pick a date": [
- ""
- ],
- "clear": [
- ""
- ],
- "never": [
- ""
- ],
- "Image should be smaller than 1 MB": [
- ""
- ],
- "Country": [
- ""
- ],
- "Address": [
- ""
- ],
- "Building number": [
- ""
- ],
- "Building name": [
- ""
- ],
- "Street": [
- ""
- ],
- "Post code": [
- ""
- ],
- "Town location": [
- ""
- ],
- "Town": [
- ""
- ],
- "District": [
- ""
- ],
- "Country subdivision": [
- ""
- ],
- "Product id": [
- ""
- ],
- "Description": [
- ""
- ],
- "Name": [
- ""
- ],
- "loading...": [
- ""
- ],
- "no products found": [
- ""
- ],
- "no results": [
- ""
- ],
- "Deleting": [
- ""
- ],
- "Changing": [
- ""
- ],
- "Manage token": [
- ""
- ],
- "Update": [
- ""
- ],
- "Remove": [
- ""
- ],
- "Cancel": [
- ""
- ],
- "Manage stock": [
- ""
- ],
- "Infinite": [
- ""
- ],
- "lost cannot be greater that current + incoming (max %1$s)": [
- ""
- ],
- "current stock will change from %1$s to %2$s": [
- ""
- ],
- "current stock will stay at %1$s": [
- ""
- ],
- "Incoming": [
- ""
- ],
- "Lost": [
- ""
- ],
- "Current": [
- ""
- ],
- "without stock": [
- ""
- ],
- "Next restock": [
- ""
- ],
- "Delivery address": [
- ""
- ],
- "this product has no taxes": [
- ""
- ],
- "Amount": [
- ""
- ],
- "currency and value separated with colon": [
- ""
- ],
- "Add": [
- ""
- ],
- "Instance": [
- ""
- ],
- "Settings": [
- ""
- ],
- "Orders": [
- ""
- ],
- "Products": [
- ""
- ],
- "Transfers": [
- ""
- ],
- "Connection": [
- ""
- ],
- "Instances": [
- ""
- ],
- "New": [
- ""
- ],
- "List": [
- ""
- ],
- "Log out": [
- ""
- ],
- "Clear": [
- ""
- ],
- "should be the same": [
- ""
- ],
- "cannot be the same as before": [
- ""
- ],
- "You are updating the authorization token from instance %1$s with id %2$s": [
- ""
- ],
- "Old token": [
- ""
- ],
- "New token": [
- ""
- ],
- "Clearing the auth token will mean public access to the instance": [
- ""
- ],
- "ID": [
- ""
- ],
- "Image": [
- ""
- ],
- "Unit": [
- ""
- ],
- "Price": [
- ""
- ],
- "Stock": [
- ""
- ],
- "Taxes": [
- ""
- ],
- "Server not found": [
- ""
- ],
- "Couldn't access the server": [
- ""
- ],
- "Got message %1$s from %2$s": [
- ""
- ],
- "Unexpected Error": [
- ""
- ],
- "Auth token": [
- ""
- ],
- "Account address": [
- ""
- ],
- "Default max deposit fee": [
- ""
- ],
- "Default max wire fee": [
- ""
- ],
- "Default wire fee amortization": [
- ""
- ],
- "Jurisdiction": [
- ""
- ],
- "Default pay delay": [
- ""
- ],
- "Default wire transfer delay": [
- ""
- ],
- "could not create instance": [
- ""
- ],
- "Delete": [
- ""
- ],
- "Edit": [
- ""
- ],
- "There is no instances yet, add more pressing the + sign": [
- ""
- ],
- "Inventory products": [
- ""
- ],
- "Total price": [
- ""
- ],
- "Total tax": [
- ""
- ],
- "Order price": [
- ""
- ],
- "Net": [
- ""
- ],
- "Summary": [
- ""
- ],
- "Payments options": [
- ""
- ],
- "Auto refund deadline": [
- ""
- ],
- "Refund deadline": [
- ""
- ],
- "Pay deadline": [
- ""
- ],
- "Delivery date": [
- ""
- ],
- "Location": [
- ""
- ],
- "Max fee": [
- ""
- ],
- "Max wire fee": [
- ""
- ],
- "Wire fee amortization": [
- ""
- ],
- "Fullfilment url": [
- ""
- ],
- "Extra information": [
- ""
- ],
- "select a product first": [
- ""
- ],
- "should be greater than 0": [
- ""
- ],
- "cannot be greater than current stock and quantity previously added. max: %1$s": [
- ""
- ],
- "cannot be greater than current stock %1$s": [
- ""
- ],
- "Quantity": [
- ""
- ],
- "Order": [
- ""
- ],
- "claimed": [
- ""
- ],
- "copy url": [
- ""
- ],
- "pay at": [
- ""
- ],
- "created at": [
- ""
- ],
- "Timeline": [
- ""
- ],
- "Payment details": [
- ""
- ],
- "Order status": [
- ""
- ],
- "Product list": [
- ""
- ],
- "paid": [
- ""
- ],
- "wired": [
- ""
- ],
- "refunded": [
- ""
- ],
- "refund": [
- ""
- ],
- "Refunded amount": [
- ""
- ],
- "Deposit total": [
- ""
- ],
- "unpaid": [
- ""
- ],
- "Order status URL": [
- ""
- ],
- "Pay URI": [
- ""
- ],
- "Unknown order status. This is an error, please contact the administrator.": [
- ""
- ],
- "refund created successfully": [
- ""
- ],
- "could not create the refund": [
- ""
- ],
- "load newer orders": [
- ""
- ],
- "Date": [
- ""
- ],
- "Refund": [
- ""
- ],
- "load older orders": [
- ""
- ],
- "No orders has been found": [
- ""
- ],
- "date": [
- ""
- ],
- "amount": [
- ""
- ],
- "reason": [
- ""
- ],
- "Max refundable:": [
- ""
- ],
- "Reason": [
- ""
- ],
- "duplicated": [
- ""
- ],
- "requested by the customer": [
- ""
- ],
- "other": [
- ""
- ],
- "go to order id": [
- ""
- ],
- "Paid": [
- ""
- ],
- "Refunded": [
- ""
- ],
- "Not wired": [
- ""
- ],
- "All": [
- ""
- ],
- "could not create product": [
- ""
- ],
- "Sell": [
- ""
- ],
- "Profit": [
- ""
- ],
- "Sold": [
- ""
- ],
- "product updated successfully": [
- ""
- ],
- "could not update the product": [
- ""
- ],
- "product delete successfully": [
- ""
- ],
- "could not delete the product": [
- ""
- ],
- "Tips": [
- ""
- ],
- "Committed amount": [
- ""
- ],
- "Exchange initial amount": [
- ""
- ],
- "Merchant initial amount": [
- ""
- ],
- "There is no tips yet, add more pressing the + sign": [
- ""
- ],
- "cannot be empty": [
- ""
- ],
- "check the id, doest look valid": [
- ""
- ],
- "should have 52 characters, current %1$s": [
- ""
- ],
- "URL doesn't have the right format": [
- ""
- ],
- "Transfer ID": [
- ""
- ],
- "Account Address": [
- ""
- ],
- "Exchange URL": [
- ""
- ],
- "could not inform transfer": [
- ""
- ],
- "load newer transfers": [
- ""
- ],
- "Credit": [
- ""
- ],
- "Confirmed": [
- ""
- ],
- "Verified": [
- ""
- ],
- "Executed at": [
- ""
- ],
- "yes": [
- ""
- ],
- "no": [
- ""
- ],
- "unknown": [
- ""
- ],
- "load older transfers": [
- ""
- ],
- "There is no transfer yet, add more pressing the + sign": [
- ""
- ]
- }
- }
-};
-
-strings['sv'] = {
- "domain": "messages",
- "locale_data": {
- "messages": {
- "": {
- "domain": "messages",
- "plural_forms": "nplurals=2; plural=(n != 1);",
- "lang": ""
- },
- "Access denied": [
- ""
- ],
- "Check your token is valid": [
- ""
- ],
- "Couldn't access the server.": [
- ""
- ],
- "Could not infer instance id from url %1$s": [
- ""
- ],
- "HTTP status #%1$s: Server reported a problem": [
- ""
- ],
- "Got message: \"%1$s\" from: %2$s": [
- ""
- ],
- "No default instance": [
- ""
- ],
- "in order to use merchant backoffice, you should create the default instance": [
- ""
- ],
- "Server reported a problem: HTTP status #%1$s": [
- ""
- ],
- "Got message: %1$s from: %2$s": [
- ""
- ],
- "Login required": [
- ""
- ],
- "Please enter your auth token. Token should have \"secret-token:\" and start with Bearer or ApiKey": [
- ""
- ],
- "Confirm": [
- ""
- ],
- "The value %1$s is invalid for a payment url": [
- ""
- ],
- "pick a date": [
- ""
- ],
- "clear": [
- ""
- ],
- "never": [
- ""
- ],
- "Image should be smaller than 1 MB": [
- ""
- ],
- "Country": [
- ""
- ],
- "Address": [
- ""
- ],
- "Building number": [
- ""
- ],
- "Building name": [
- ""
- ],
- "Street": [
- ""
- ],
- "Post code": [
- ""
- ],
- "Town location": [
- ""
- ],
- "Town": [
- ""
- ],
- "District": [
- ""
- ],
- "Country subdivision": [
- ""
- ],
- "Product id": [
- ""
- ],
- "Description": [
- ""
- ],
- "Name": [
- ""
- ],
- "loading...": [
- ""
- ],
- "no products found": [
- ""
- ],
- "no results": [
- ""
- ],
- "Deleting": [
- ""
- ],
- "Changing": [
- ""
- ],
- "Manage token": [
- ""
- ],
- "Update": [
- ""
- ],
- "Remove": [
- ""
- ],
- "Cancel": [
- ""
- ],
- "Manage stock": [
- ""
- ],
- "Infinite": [
- ""
- ],
- "lost cannot be greater that current + incoming (max %1$s)": [
- ""
- ],
- "current stock will change from %1$s to %2$s": [
- ""
- ],
- "current stock will stay at %1$s": [
- ""
- ],
- "Incoming": [
- ""
- ],
- "Lost": [
- ""
- ],
- "Current": [
- ""
- ],
- "without stock": [
- ""
- ],
- "Next restock": [
- ""
- ],
- "Delivery address": [
- ""
- ],
- "this product has no taxes": [
- ""
- ],
- "Amount": [
- ""
- ],
- "currency and value separated with colon": [
- ""
- ],
- "Add": [
- ""
- ],
- "Instance": [
- ""
- ],
- "Settings": [
- ""
- ],
- "Orders": [
- ""
- ],
- "Products": [
- ""
- ],
- "Transfers": [
- ""
- ],
- "Connection": [
- ""
- ],
- "Instances": [
- ""
- ],
- "New": [
- ""
- ],
- "List": [
- ""
- ],
- "Log out": [
- ""
- ],
- "Clear": [
- ""
- ],
- "should be the same": [
- ""
- ],
- "cannot be the same as before": [
- ""
- ],
- "You are updating the authorization token from instance %1$s with id %2$s": [
- ""
- ],
- "Old token": [
- ""
- ],
- "New token": [
- ""
- ],
- "Clearing the auth token will mean public access to the instance": [
- ""
- ],
- "ID": [
- ""
- ],
- "Image": [
- ""
- ],
- "Unit": [
- ""
- ],
- "Price": [
- ""
- ],
- "Stock": [
- ""
- ],
- "Taxes": [
- ""
- ],
- "Server not found": [
- ""
- ],
- "Couldn't access the server": [
- ""
- ],
- "Got message %1$s from %2$s": [
- ""
- ],
- "Unexpected Error": [
- ""
- ],
- "Auth token": [
- ""
- ],
- "Account address": [
- ""
- ],
- "Default max deposit fee": [
- ""
- ],
- "Default max wire fee": [
- ""
- ],
- "Default wire fee amortization": [
- ""
- ],
- "Jurisdiction": [
- ""
- ],
- "Default pay delay": [
- ""
- ],
- "Default wire transfer delay": [
- ""
- ],
- "could not create instance": [
- ""
- ],
- "Delete": [
- ""
- ],
- "Edit": [
- ""
- ],
- "There is no instances yet, add more pressing the + sign": [
- ""
- ],
- "Inventory products": [
- ""
- ],
- "Total price": [
- ""
- ],
- "Total tax": [
- ""
- ],
- "Order price": [
- ""
- ],
- "Net": [
- ""
- ],
- "Summary": [
- ""
- ],
- "Payments options": [
- ""
- ],
- "Auto refund deadline": [
- ""
- ],
- "Refund deadline": [
- ""
- ],
- "Pay deadline": [
- ""
- ],
- "Delivery date": [
- ""
- ],
- "Location": [
- ""
- ],
- "Max fee": [
- ""
- ],
- "Max wire fee": [
- ""
- ],
- "Wire fee amortization": [
- ""
- ],
- "Fullfilment url": [
- ""
- ],
- "Extra information": [
- ""
- ],
- "select a product first": [
- ""
- ],
- "should be greater than 0": [
- ""
- ],
- "cannot be greater than current stock and quantity previously added. max: %1$s": [
- ""
- ],
- "cannot be greater than current stock %1$s": [
- ""
- ],
- "Quantity": [
- ""
- ],
- "Order": [
- ""
- ],
- "claimed": [
- ""
- ],
- "copy url": [
- ""
- ],
- "pay at": [
- ""
- ],
- "created at": [
- ""
- ],
- "Timeline": [
- ""
- ],
- "Payment details": [
- ""
- ],
- "Order status": [
- ""
- ],
- "Product list": [
- ""
- ],
- "paid": [
- ""
- ],
- "wired": [
- ""
- ],
- "refunded": [
- ""
- ],
- "refund": [
- ""
- ],
- "Refunded amount": [
- ""
- ],
- "Deposit total": [
- ""
- ],
- "unpaid": [
- ""
- ],
- "Order status URL": [
- ""
- ],
- "Pay URI": [
- ""
- ],
- "Unknown order status. This is an error, please contact the administrator.": [
- ""
- ],
- "refund created successfully": [
- ""
- ],
- "could not create the refund": [
- ""
- ],
- "load newer orders": [
- ""
- ],
- "Date": [
- ""
- ],
- "Refund": [
- ""
- ],
- "load older orders": [
- ""
- ],
- "No orders has been found": [
- ""
- ],
- "date": [
- ""
- ],
- "amount": [
- ""
- ],
- "reason": [
- ""
- ],
- "Max refundable:": [
- ""
- ],
- "Reason": [
- ""
- ],
- "duplicated": [
- ""
- ],
- "requested by the customer": [
- ""
- ],
- "other": [
- ""
- ],
- "go to order id": [
- ""
- ],
- "Paid": [
- ""
- ],
- "Refunded": [
- ""
- ],
- "Not wired": [
- ""
- ],
- "All": [
- ""
- ],
- "could not create product": [
- ""
- ],
- "Sell": [
- ""
- ],
- "Profit": [
- ""
- ],
- "Sold": [
- ""
- ],
- "product updated successfully": [
- ""
- ],
- "could not update the product": [
- ""
- ],
- "product delete successfully": [
- ""
- ],
- "could not delete the product": [
- ""
- ],
- "Tips": [
- ""
- ],
- "Committed amount": [
- ""
- ],
- "Exchange initial amount": [
- ""
- ],
- "Merchant initial amount": [
- ""
- ],
- "There is no tips yet, add more pressing the + sign": [
- ""
- ],
- "cannot be empty": [
- ""
- ],
- "check the id, doest look valid": [
- ""
- ],
- "should have 52 characters, current %1$s": [
- ""
- ],
- "URL doesn't have the right format": [
- ""
- ],
- "Transfer ID": [
- ""
- ],
- "Account Address": [
- ""
- ],
- "Exchange URL": [
- ""
- ],
- "could not inform transfer": [
- ""
- ],
- "load newer transfers": [
- ""
- ],
- "Credit": [
- ""
- ],
- "Confirmed": [
- ""
- ],
- "Verified": [
- ""
- ],
- "Executed at": [
- ""
- ],
- "yes": [
- ""
- ],
- "no": [
- ""
- ],
- "unknown": [
- ""
- ],
- "load older transfers": [
- ""
- ],
- "There is no transfer yet, add more pressing the + sign": [
- ""
- ]
- }
- }
-};
-
diff --git a/packages/merchant-backend-ui/src/i18n/sv.po b/packages/merchant-backend-ui/src/i18n/sv.po
deleted file mode 100644
index 6b35bd0ce..000000000
--- a/packages/merchant-backend-ui/src/i18n/sv.po
+++ /dev/null
@@ -1,1057 +0,0 @@
-# This file is part of TALER
-# (C) 2016 GNUnet e.V.
-#
-# TALER is free software; you can redistribute it and/or modify it under the
-# terms of the GNU General Public License as published by the Free Software
-# Foundation; either version 3, or (at your option) any later version.
-#
-# TALER is distributed in the hope that it will be useful, but WITHOUT ANY
-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
-# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
-#
-#, fuzzy
-msgid ""
-msgstr ""
-"Project-Id-Version: Taler Wallet\n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-11-23 00:00+0100\n"
-"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
-"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
-"Language-Team: LANGUAGE <LL@li.org>\n"
-"Language: \n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-
-#: src/ApplicationReadyRoutes.tsx:50 src/InstanceRoutes.tsx:118
-#: src/InstanceRoutes.tsx:299
-#, c-format
-msgid "Access denied"
-msgstr ""
-
-#: src/ApplicationReadyRoutes.tsx:51 src/InstanceRoutes.tsx:118
-#: src/InstanceRoutes.tsx:300
-#, c-format
-msgid "Check your token is valid"
-msgstr ""
-
-#: src/ApplicationReadyRoutes.tsx:72
-#, c-format
-msgid "Couldn't access the server."
-msgstr ""
-
-#: src/ApplicationReadyRoutes.tsx:73
-#, c-format
-msgid "Could not infer instance id from url %1$s"
-msgstr ""
-
-#: src/InstanceRoutes.tsx:109
-#, c-format
-msgid "HTTP status #%1$s: Server reported a problem"
-msgstr ""
-
-#: src/InstanceRoutes.tsx:110
-#, c-format
-msgid "Got message: \"%1$s\" from: %2$s"
-msgstr ""
-
-#: src/InstanceRoutes.tsx:127
-#, c-format
-msgid "No default instance"
-msgstr ""
-
-#: src/InstanceRoutes.tsx:128
-#, c-format
-msgid ""
-"in order to use merchant backoffice, you should create the default instance"
-msgstr ""
-
-#: src/InstanceRoutes.tsx:288
-#, c-format
-msgid "Server reported a problem: HTTP status #%1$s"
-msgstr ""
-
-#: src/InstanceRoutes.tsx:289
-#, c-format
-msgid "Got message: %1$s from: %2$s"
-msgstr ""
-
-#: src/components/exception/login.tsx:46
-#, c-format
-msgid "Login required"
-msgstr ""
-
-#: src/components/exception/login.tsx:49
-#, c-format
-msgid ""
-"Please enter your auth token. Token should have \"secret-token:\" and start "
-"with Bearer or ApiKey"
-msgstr ""
-
-#: src/components/exception/login.tsx:86 src/components/modal/index.tsx:53
-#: src/components/modal/index.tsx:75 src/paths/admin/create/CreatePage.tsx:115
-#: src/paths/instance/orders/create/CreatePage.tsx:325
-#: src/paths/instance/products/create/CreatePage.tsx:51
-#: src/paths/instance/products/list/Table.tsx:174
-#: src/paths/instance/products/list/Table.tsx:228
-#: src/paths/instance/products/update/UpdatePage.tsx:55
-#: src/paths/instance/transfers/create/CreatePage.tsx:89
-#: src/paths/instance/update/UpdatePage.tsx:134
-#, c-format
-msgid "Confirm"
-msgstr ""
-
-#: src/components/form/InputArray.tsx:72
-#, c-format
-msgid "The value %1$s is invalid for a payment url"
-msgstr ""
-
-#: src/components/form/InputDate.tsx:67
-#: src/paths/instance/orders/list/index.tsx:123
-#, c-format
-msgid "pick a date"
-msgstr ""
-
-#: src/components/form/InputDate.tsx:81
-#, c-format
-msgid "clear"
-msgstr ""
-
-#: src/components/form/InputDate.tsx:83
-#: src/paths/instance/transfers/list/Table.tsx:140
-#, c-format
-msgid "never"
-msgstr ""
-
-#: src/components/form/InputImage.tsx:80
-#, c-format
-msgid "Image should be smaller than 1 MB"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:28
-#, c-format
-msgid "Country"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:30
-#: src/paths/admin/create/CreatePage.tsx:99
-#: src/paths/instance/transfers/list/Table.tsx:124
-#: src/paths/instance/update/UpdatePage.tsx:118
-#, c-format
-msgid "Address"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:34
-#, c-format
-msgid "Building number"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:35
-#, c-format
-msgid "Building name"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:36
-#, c-format
-msgid "Street"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:37
-#, c-format
-msgid "Post code"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:38
-#, c-format
-msgid "Town location"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:39
-#, c-format
-msgid "Town"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:40
-#, c-format
-msgid "District"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:41
-#, c-format
-msgid "Country subdivision"
-msgstr ""
-
-#: src/components/form/InputSearchProduct.tsx:59
-#, c-format
-msgid "Product id"
-msgstr ""
-
-#: src/components/form/InputSearchProduct.tsx:60
-#: src/components/product/ProductForm.tsx:99
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:122
-#: src/paths/instance/orders/list/Table.tsx:227
-#: src/paths/instance/products/list/Table.tsx:86
-#, c-format
-msgid "Description"
-msgstr ""
-
-#: src/components/form/InputSearchProduct.tsx:73
-#: src/components/form/InputTaxes.tsx:81
-#: src/paths/admin/create/CreatePage.tsx:87 src/paths/admin/list/Table.tsx:110
-#: src/paths/instance/details/DetailPage.tsx:76
-#: src/paths/instance/update/UpdatePage.tsx:106
-#, c-format
-msgid "Name"
-msgstr ""
-
-#: src/components/form/InputSearchProduct.tsx:102
-#, c-format
-msgid "loading..."
-msgstr ""
-
-#: src/components/form/InputSearchProduct.tsx:108
-#, c-format
-msgid "no products found"
-msgstr ""
-
-#: src/components/form/InputSearchProduct.tsx:116
-#, c-format
-msgid "no results"
-msgstr ""
-
-#: src/components/form/InputSecured.tsx:33
-#, c-format
-msgid "Deleting"
-msgstr ""
-
-#: src/components/form/InputSecured.tsx:34
-#, c-format
-msgid "Changing"
-msgstr ""
-
-#: src/components/form/InputSecured.tsx:60
-#, c-format
-msgid "Manage token"
-msgstr ""
-
-#: src/components/form/InputSecured.tsx:83
-#, c-format
-msgid "Update"
-msgstr ""
-
-#: src/components/form/InputSecured.tsx:100
-#: src/paths/instance/orders/create/CreatePage.tsx:252
-#: src/paths/instance/orders/create/CreatePage.tsx:273
-#, c-format
-msgid "Remove"
-msgstr ""
-
-#: src/components/form/InputSecured.tsx:106 src/components/modal/index.tsx:52
-#: src/components/modal/index.tsx:73 src/paths/admin/create/CreatePage.tsx:114
-#: src/paths/instance/orders/create/CreatePage.tsx:324
-#: src/paths/instance/products/create/CreatePage.tsx:50
-#: src/paths/instance/products/list/Table.tsx:166
-#: src/paths/instance/products/list/Table.tsx:218
-#: src/paths/instance/products/update/UpdatePage.tsx:54
-#: src/paths/instance/transfers/create/CreatePage.tsx:88
-#: src/paths/instance/update/UpdatePage.tsx:133
-#, c-format
-msgid "Cancel"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:91
-#, c-format
-msgid "Manage stock"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:93
-#, c-format
-msgid "Infinite"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:105
-#, c-format
-msgid "lost cannot be greater that current + incoming (max %1$s)"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:111
-#, c-format
-msgid "current stock will change from %1$s to %2$s"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:112
-#, c-format
-msgid "current stock will stay at %1$s"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:129
-#: src/paths/instance/products/list/Table.tsx:204
-#, c-format
-msgid "Incoming"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:130
-#: src/paths/instance/products/list/Table.tsx:205
-#, c-format
-msgid "Lost"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:142
-#, c-format
-msgid "Current"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:145
-#, c-format
-msgid "without stock"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:150
-#, c-format
-msgid "Next restock"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:152
-#, c-format
-msgid "Delivery address"
-msgstr ""
-
-#: src/components/form/InputTaxes.tsx:73
-#, c-format
-msgid "this product has no taxes"
-msgstr ""
-
-#: src/components/form/InputTaxes.tsx:77
-#: src/paths/instance/orders/details/DetailPage.tsx:145
-#: src/paths/instance/orders/details/DetailPage.tsx:296
-#: src/paths/instance/orders/list/Table.tsx:116
-#: src/paths/instance/transfers/create/CreatePage.tsx:84
-#, c-format
-msgid "Amount"
-msgstr ""
-
-#: src/components/form/InputTaxes.tsx:78
-#, c-format
-msgid "currency and value separated with colon"
-msgstr ""
-
-#: src/components/form/InputTaxes.tsx:84
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:78
-#, c-format
-msgid "Add"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:53
-#, c-format
-msgid "Instance"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:59
-#, c-format
-msgid "Settings"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:65
-#: src/paths/instance/orders/list/Table.tsx:60
-#, c-format
-msgid "Orders"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:71
-#: src/paths/instance/orders/create/CreatePage.tsx:258
-#: src/paths/instance/products/list/Table.tsx:48
-#, c-format
-msgid "Products"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:77
-#: src/paths/instance/transfers/list/Table.tsx:65
-#, c-format
-msgid "Transfers"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:87
-#, c-format
-msgid "Connection"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:112 src/paths/admin/list/Table.tsx:57
-#, c-format
-msgid "Instances"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:116
-#, c-format
-msgid "New"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:122
-#, c-format
-msgid "List"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:129
-#, c-format
-msgid "Log out"
-msgstr ""
-
-#: src/components/modal/index.tsx:74
-#, c-format
-msgid "Clear"
-msgstr ""
-
-#: src/components/modal/index.tsx:110 src/components/modal/index.tsx:111
-#, c-format
-msgid "should be the same"
-msgstr ""
-
-#: src/components/modal/index.tsx:111
-#, c-format
-msgid "cannot be the same as before"
-msgstr ""
-
-#: src/components/modal/index.tsx:114
-#, c-format
-msgid ""
-"You are updating the authorization token from instance %1$s with id %2$s"
-msgstr ""
-
-#: src/components/modal/index.tsx:124
-#, c-format
-msgid "Old token"
-msgstr ""
-
-#: src/components/modal/index.tsx:125
-#, c-format
-msgid "New token"
-msgstr ""
-
-#: src/components/modal/index.tsx:127
-#, c-format
-msgid "Clearing the auth token will mean public access to the instance"
-msgstr ""
-
-#: src/components/product/ProductForm.tsx:96
-#: src/paths/admin/create/CreatePage.tsx:85 src/paths/admin/list/Table.tsx:109
-#: src/paths/instance/transfers/list/Table.tsx:122
-#, c-format
-msgid "ID"
-msgstr ""
-
-#: src/components/product/ProductForm.tsx:98
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:121
-#: src/paths/instance/products/list/Table.tsx:85
-#, c-format
-msgid "Image"
-msgstr ""
-
-#: src/components/product/ProductForm.tsx:100
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:123
-#, c-format
-msgid "Unit"
-msgstr ""
-
-#: src/components/product/ProductForm.tsx:101
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:124
-#: src/paths/instance/products/list/Table.tsx:162
-#: src/paths/instance/products/list/Table.tsx:214
-#, c-format
-msgid "Price"
-msgstr ""
-
-#: src/components/product/ProductForm.tsx:103
-#: src/paths/instance/products/list/Table.tsx:90
-#, c-format
-msgid "Stock"
-msgstr ""
-
-#: src/components/product/ProductForm.tsx:105
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:128
-#: src/paths/instance/products/list/Table.tsx:88
-#, c-format
-msgid "Taxes"
-msgstr ""
-
-#: src/index.tsx:75
-#, c-format
-msgid "Server not found"
-msgstr ""
-
-#: src/index.tsx:85
-#, c-format
-msgid "Couldn't access the server"
-msgstr ""
-
-#: src/index.tsx:87 src/index.tsx:99
-#, c-format
-msgid "Got message %1$s from %2$s"
-msgstr ""
-
-#: src/index.tsx:97
-#, c-format
-msgid "Unexpected Error"
-msgstr ""
-
-#: src/paths/admin/create/CreatePage.tsx:89
-#: src/paths/instance/update/UpdatePage.tsx:108
-#, c-format
-msgid "Auth token"
-msgstr ""
-
-#: src/paths/admin/create/CreatePage.tsx:91
-#: src/paths/instance/details/DetailPage.tsx:77
-#: src/paths/instance/update/UpdatePage.tsx:110
-#, c-format
-msgid "Account address"
-msgstr ""
-
-#: src/paths/admin/create/CreatePage.tsx:93
-#: src/paths/instance/update/UpdatePage.tsx:112
-#, c-format
-msgid "Default max deposit fee"
-msgstr ""
-
-#: src/paths/admin/create/CreatePage.tsx:95
-#: src/paths/instance/update/UpdatePage.tsx:114
-#, c-format
-msgid "Default max wire fee"
-msgstr ""
-
-#: src/paths/admin/create/CreatePage.tsx:97
-#: src/paths/instance/update/UpdatePage.tsx:116
-#, c-format
-msgid "Default wire fee amortization"
-msgstr ""
-
-#: src/paths/admin/create/CreatePage.tsx:103
-#: src/paths/instance/update/UpdatePage.tsx:122
-#, c-format
-msgid "Jurisdiction"
-msgstr ""
-
-#: src/paths/admin/create/CreatePage.tsx:107
-#: src/paths/instance/update/UpdatePage.tsx:126
-#, c-format
-msgid "Default pay delay"
-msgstr ""
-
-#: src/paths/admin/create/CreatePage.tsx:109
-#: src/paths/instance/update/UpdatePage.tsx:128
-#, c-format
-msgid "Default wire transfer delay"
-msgstr ""
-
-#: src/paths/admin/create/index.tsx:58
-#, c-format
-msgid "could not create instance"
-msgstr ""
-
-#: src/paths/admin/list/Table.tsx:63 src/paths/admin/list/Table.tsx:131
-#: src/paths/instance/transfers/list/Table.tsx:71
-#, c-format
-msgid "Delete"
-msgstr ""
-
-#: src/paths/admin/list/Table.tsx:128
-#, c-format
-msgid "Edit"
-msgstr ""
-
-#: src/paths/admin/list/Table.tsx:149
-#: src/paths/instance/products/list/Table.tsx:245
-#, c-format
-msgid "There is no instances yet, add more pressing the + sign"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:237
-#, c-format
-msgid "Inventory products"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:286
-#, c-format
-msgid "Total price"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:287
-#, c-format
-msgid "Total tax"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:289
-#: src/paths/instance/orders/create/CreatePage.tsx:297
-#, c-format
-msgid "Order price"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:295
-#, c-format
-msgid "Net"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:300
-#: src/paths/instance/orders/details/DetailPage.tsx:144
-#: src/paths/instance/orders/details/DetailPage.tsx:295
-#: src/paths/instance/orders/list/Table.tsx:117
-#, c-format
-msgid "Summary"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:302
-#, c-format
-msgid "Payments options"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:303
-#, c-format
-msgid "Auto refund deadline"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:304
-#, c-format
-msgid "Refund deadline"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:305
-#, c-format
-msgid "Pay deadline"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:307
-#, c-format
-msgid "Delivery date"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:308
-#, c-format
-msgid "Location"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:312
-#, c-format
-msgid "Max fee"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:313
-#, c-format
-msgid "Max wire fee"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:314
-#, c-format
-msgid "Wire fee amortization"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:315
-#, c-format
-msgid "Fullfilment url"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:318
-#, c-format
-msgid "Extra information"
-msgstr ""
-
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:44
-#, c-format
-msgid "select a product first"
-msgstr ""
-
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:51
-#, c-format
-msgid "should be greater than 0"
-msgstr ""
-
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:58
-#, c-format
-msgid ""
-"cannot be greater than current stock and quantity previously added. max: %1$s"
-msgstr ""
-
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:64
-#, c-format
-msgid "cannot be greater than current stock %1$s"
-msgstr ""
-
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:76
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:126
-#, c-format
-msgid "Quantity"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:92
-#: src/paths/instance/orders/details/DetailPage.tsx:235
-#: src/paths/instance/orders/details/DetailPage.tsx:333
-#, c-format
-msgid "Order"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:93
-#, c-format
-msgid "claimed"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:110
-#: src/paths/instance/orders/details/DetailPage.tsx:261
-#: src/paths/instance/orders/list/Table.tsx:136
-#, c-format
-msgid "copy url"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:126
-#: src/paths/instance/orders/details/DetailPage.tsx:349
-#, c-format
-msgid "pay at"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:127
-#: src/paths/instance/orders/details/DetailPage.tsx:350
-#, c-format
-msgid "created at"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:138
-#: src/paths/instance/orders/details/DetailPage.tsx:289
-#, c-format
-msgid "Timeline"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:142
-#: src/paths/instance/orders/details/DetailPage.tsx:293
-#, c-format
-msgid "Payment details"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:146
-#: src/paths/instance/orders/details/DetailPage.tsx:299
-#: src/paths/instance/orders/details/DetailPage.tsx:363
-#, c-format
-msgid "Order status"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:156
-#: src/paths/instance/orders/details/DetailPage.tsx:308
-#, c-format
-msgid "Product list"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:236
-#, c-format
-msgid "paid"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:238
-#, c-format
-msgid "wired"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:241
-#, c-format
-msgid "refunded"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:258
-#, c-format
-msgid "refund"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:297
-#, c-format
-msgid "Refunded amount"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:298
-#, c-format
-msgid "Deposit total"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:336
-#, c-format
-msgid "unpaid"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:364
-#, c-format
-msgid "Order status URL"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:365
-#, c-format
-msgid "Pay URI"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:383
-#, c-format
-msgid ""
-"Unknown order status. This is an error, please contact the administrator."
-msgstr ""
-
-#: src/paths/instance/orders/details/index.tsx:56
-#: src/paths/instance/orders/list/index.tsx:147
-#, c-format
-msgid "refund created successfully"
-msgstr ""
-
-#: src/paths/instance/orders/details/index.tsx:59
-#: src/paths/instance/orders/list/index.tsx:150
-#, c-format
-msgid "could not create the refund"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:111
-#, c-format
-msgid "load newer orders"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:115
-#, c-format
-msgid "Date"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:131
-#: src/paths/instance/orders/list/Table.tsx:223
-#, c-format
-msgid "Refund"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:145
-#, c-format
-msgid "load older orders"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:154
-#, c-format
-msgid "No orders has been found"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:202
-#, c-format
-msgid "date"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:203
-#, c-format
-msgid "amount"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:204
-#, c-format
-msgid "reason"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:224
-#, c-format
-msgid "Max refundable:"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:226
-#, c-format
-msgid "Reason"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:226
-#, c-format
-msgid "duplicated"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:226
-#, c-format
-msgid "requested by the customer"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:226
-#, c-format
-msgid "other"
-msgstr ""
-
-#: src/paths/instance/orders/list/index.tsx:91
-#, c-format
-msgid "go to order id"
-msgstr ""
-
-#: src/paths/instance/orders/list/index.tsx:107
-#, c-format
-msgid "Paid"
-msgstr ""
-
-#: src/paths/instance/orders/list/index.tsx:108
-#, c-format
-msgid "Refunded"
-msgstr ""
-
-#: src/paths/instance/orders/list/index.tsx:109
-#, c-format
-msgid "Not wired"
-msgstr ""
-
-#: src/paths/instance/orders/list/index.tsx:110
-#, c-format
-msgid "All"
-msgstr ""
-
-#: src/paths/instance/products/create/index.tsx:48
-#: src/paths/instance/products/update/index.tsx:64
-#, c-format
-msgid "could not create product"
-msgstr ""
-
-#: src/paths/instance/products/list/Table.tsx:87
-#, c-format
-msgid "Sell"
-msgstr ""
-
-#: src/paths/instance/products/list/Table.tsx:89
-#, c-format
-msgid "Profit"
-msgstr ""
-
-#: src/paths/instance/products/list/Table.tsx:91
-#, c-format
-msgid "Sold"
-msgstr ""
-
-#: src/paths/instance/products/list/index.tsx:59
-#, c-format
-msgid "product updated successfully"
-msgstr ""
-
-#: src/paths/instance/products/list/index.tsx:62
-#, c-format
-msgid "could not update the product"
-msgstr ""
-
-#: src/paths/instance/products/list/index.tsx:70
-#, c-format
-msgid "product delete successfully"
-msgstr ""
-
-#: src/paths/instance/products/list/index.tsx:73
-#, c-format
-msgid "could not delete the product"
-msgstr ""
-
-#: src/paths/instance/tips/list/Table.tsx:59
-#, c-format
-msgid "Tips"
-msgstr ""
-
-#: src/paths/instance/tips/list/Table.tsx:111
-#, c-format
-msgid "Committed amount"
-msgstr ""
-
-#: src/paths/instance/tips/list/Table.tsx:112
-#, c-format
-msgid "Exchange initial amount"
-msgstr ""
-
-#: src/paths/instance/tips/list/Table.tsx:113
-#, c-format
-msgid "Merchant initial amount"
-msgstr ""
-
-#: src/paths/instance/tips/list/Table.tsx:148
-#, c-format
-msgid "There is no tips yet, add more pressing the + sign"
-msgstr ""
-
-#: src/paths/instance/transfers/create/CreatePage.tsx:50
-#: src/paths/instance/transfers/create/CreatePage.tsx:54
-#: src/paths/instance/transfers/create/CreatePage.tsx:55
-#: src/paths/instance/transfers/create/CreatePage.tsx:56
-#, c-format
-msgid "cannot be empty"
-msgstr ""
-
-#: src/paths/instance/transfers/create/CreatePage.tsx:51
-#, c-format
-msgid "check the id, doest look valid"
-msgstr ""
-
-#: src/paths/instance/transfers/create/CreatePage.tsx:52
-#, c-format
-msgid "should have 52 characters, current %1$s"
-msgstr ""
-
-#: src/paths/instance/transfers/create/CreatePage.tsx:57
-#, c-format
-msgid "URL doesn't have the right format"
-msgstr ""
-
-#: src/paths/instance/transfers/create/CreatePage.tsx:74
-#, c-format
-msgid "Transfer ID"
-msgstr ""
-
-#: src/paths/instance/transfers/create/CreatePage.tsx:76
-#, c-format
-msgid "Account Address"
-msgstr ""
-
-#: src/paths/instance/transfers/create/CreatePage.tsx:82
-#: src/paths/instance/transfers/list/Table.tsx:125
-#, c-format
-msgid "Exchange URL"
-msgstr ""
-
-#: src/paths/instance/transfers/create/index.tsx:49
-#, c-format
-msgid "could not inform transfer"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:118
-#, c-format
-msgid "load newer transfers"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:123
-#, c-format
-msgid "Credit"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:126
-#, c-format
-msgid "Confirmed"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:127
-#: src/paths/instance/transfers/list/index.tsx:60
-#, c-format
-msgid "Verified"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:128
-#, c-format
-msgid "Executed at"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:138
-#: src/paths/instance/transfers/list/Table.tsx:139
-#, c-format
-msgid "yes"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:138
-#: src/paths/instance/transfers/list/Table.tsx:139
-#, c-format
-msgid "no"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:140
-#, c-format
-msgid "unknown"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:145
-#, c-format
-msgid "load older transfers"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:154
-#, c-format
-msgid "There is no transfer yet, add more pressing the + sign"
-msgstr ""
diff --git a/packages/merchant-backend-ui/src/i18n/taler-merchant-backoffice.pot b/packages/merchant-backend-ui/src/i18n/taler-merchant-backoffice.pot
deleted file mode 100644
index 21fd863b0..000000000
--- a/packages/merchant-backend-ui/src/i18n/taler-merchant-backoffice.pot
+++ /dev/null
@@ -1,1054 +0,0 @@
-# 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/>
-#
-#, fuzzy
-msgid ""
-msgstr ""
-"Project-Id-Version: Taler Wallet\n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-11-23 00:00+0100\n"
-"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
-"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
-"Language-Team: LANGUAGE <LL@li.org>\n"
-"Language: \n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-
-#: src/ApplicationReadyRoutes.tsx:50 src/InstanceRoutes.tsx:118
-#: src/InstanceRoutes.tsx:299
-#, c-format
-msgid "Access denied"
-msgstr ""
-
-#: src/ApplicationReadyRoutes.tsx:51 src/InstanceRoutes.tsx:118
-#: src/InstanceRoutes.tsx:300
-#, c-format
-msgid "Check your token is valid"
-msgstr ""
-
-#: src/ApplicationReadyRoutes.tsx:72
-#, c-format
-msgid "Couldn't access the server."
-msgstr ""
-
-#: src/ApplicationReadyRoutes.tsx:73
-#, c-format
-msgid "Could not infer instance id from url %1$s"
-msgstr ""
-
-#: src/InstanceRoutes.tsx:109
-#, c-format
-msgid "HTTP status #%1$s: Server reported a problem"
-msgstr ""
-
-#: src/InstanceRoutes.tsx:110
-#, c-format
-msgid "Got message: \"%1$s\" from: %2$s"
-msgstr ""
-
-#: src/InstanceRoutes.tsx:127
-#, c-format
-msgid "No default instance"
-msgstr ""
-
-#: src/InstanceRoutes.tsx:128
-#, c-format
-msgid ""
-"in order to use merchant backoffice, you should create the default instance"
-msgstr ""
-
-#: src/InstanceRoutes.tsx:288
-#, c-format
-msgid "Server reported a problem: HTTP status #%1$s"
-msgstr ""
-
-#: src/InstanceRoutes.tsx:289
-#, c-format
-msgid "Got message: %1$s from: %2$s"
-msgstr ""
-
-#: src/components/exception/login.tsx:46
-#, c-format
-msgid "Login required"
-msgstr ""
-
-#: src/components/exception/login.tsx:49
-#, c-format
-msgid ""
-"Please enter your auth token. Token should have \"secret-token:\" and start "
-"with Bearer or ApiKey"
-msgstr ""
-
-#: src/components/exception/login.tsx:86 src/components/modal/index.tsx:53
-#: src/components/modal/index.tsx:75 src/paths/admin/create/CreatePage.tsx:115
-#: src/paths/instance/orders/create/CreatePage.tsx:325
-#: src/paths/instance/products/create/CreatePage.tsx:51
-#: src/paths/instance/products/list/Table.tsx:174
-#: src/paths/instance/products/list/Table.tsx:228
-#: src/paths/instance/products/update/UpdatePage.tsx:55
-#: src/paths/instance/transfers/create/CreatePage.tsx:89
-#: src/paths/instance/update/UpdatePage.tsx:134
-#, c-format
-msgid "Confirm"
-msgstr ""
-
-#: src/components/form/InputArray.tsx:72
-#, c-format
-msgid "The value %1$s is invalid for a payment url"
-msgstr ""
-
-#: src/components/form/InputDate.tsx:67
-#: src/paths/instance/orders/list/index.tsx:123
-#, c-format
-msgid "pick a date"
-msgstr ""
-
-#: src/components/form/InputDate.tsx:81
-#, c-format
-msgid "clear"
-msgstr ""
-
-#: src/components/form/InputDate.tsx:83
-#: src/paths/instance/transfers/list/Table.tsx:140
-#, c-format
-msgid "never"
-msgstr ""
-
-#: src/components/form/InputImage.tsx:80
-#, c-format
-msgid "Image should be smaller than 1 MB"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:28
-#, c-format
-msgid "Country"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:30
-#: src/paths/admin/create/CreatePage.tsx:99
-#: src/paths/instance/transfers/list/Table.tsx:124
-#: src/paths/instance/update/UpdatePage.tsx:118
-#, c-format
-msgid "Address"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:34
-#, c-format
-msgid "Building number"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:35
-#, c-format
-msgid "Building name"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:36
-#, c-format
-msgid "Street"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:37
-#, c-format
-msgid "Post code"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:38
-#, c-format
-msgid "Town location"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:39
-#, c-format
-msgid "Town"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:40
-#, c-format
-msgid "District"
-msgstr ""
-
-#: src/components/form/InputLocation.tsx:41
-#, c-format
-msgid "Country subdivision"
-msgstr ""
-
-#: src/components/form/InputSearchProduct.tsx:59
-#, c-format
-msgid "Product id"
-msgstr ""
-
-#: src/components/form/InputSearchProduct.tsx:60
-#: src/components/product/ProductForm.tsx:99
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:122
-#: src/paths/instance/orders/list/Table.tsx:227
-#: src/paths/instance/products/list/Table.tsx:86
-#, c-format
-msgid "Description"
-msgstr ""
-
-#: src/components/form/InputSearchProduct.tsx:73
-#: src/components/form/InputTaxes.tsx:81
-#: src/paths/admin/create/CreatePage.tsx:87 src/paths/admin/list/Table.tsx:110
-#: src/paths/instance/details/DetailPage.tsx:76
-#: src/paths/instance/update/UpdatePage.tsx:106
-#, c-format
-msgid "Name"
-msgstr ""
-
-#: src/components/form/InputSearchProduct.tsx:102
-#, c-format
-msgid "loading..."
-msgstr ""
-
-#: src/components/form/InputSearchProduct.tsx:108
-#, c-format
-msgid "no products found"
-msgstr ""
-
-#: src/components/form/InputSearchProduct.tsx:116
-#, c-format
-msgid "no results"
-msgstr ""
-
-#: src/components/form/InputSecured.tsx:33
-#, c-format
-msgid "Deleting"
-msgstr ""
-
-#: src/components/form/InputSecured.tsx:34
-#, c-format
-msgid "Changing"
-msgstr ""
-
-#: src/components/form/InputSecured.tsx:60
-#, c-format
-msgid "Manage token"
-msgstr ""
-
-#: src/components/form/InputSecured.tsx:83
-#, c-format
-msgid "Update"
-msgstr ""
-
-#: src/components/form/InputSecured.tsx:100
-#: src/paths/instance/orders/create/CreatePage.tsx:252
-#: src/paths/instance/orders/create/CreatePage.tsx:273
-#, c-format
-msgid "Remove"
-msgstr ""
-
-#: src/components/form/InputSecured.tsx:106 src/components/modal/index.tsx:52
-#: src/components/modal/index.tsx:73 src/paths/admin/create/CreatePage.tsx:114
-#: src/paths/instance/orders/create/CreatePage.tsx:324
-#: src/paths/instance/products/create/CreatePage.tsx:50
-#: src/paths/instance/products/list/Table.tsx:166
-#: src/paths/instance/products/list/Table.tsx:218
-#: src/paths/instance/products/update/UpdatePage.tsx:54
-#: src/paths/instance/transfers/create/CreatePage.tsx:88
-#: src/paths/instance/update/UpdatePage.tsx:133
-#, c-format
-msgid "Cancel"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:91
-#, c-format
-msgid "Manage stock"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:93
-#, c-format
-msgid "Infinite"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:105
-#, c-format
-msgid "lost cannot be greater that current + incoming (max %1$s)"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:111
-#, c-format
-msgid "current stock will change from %1$s to %2$s"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:112
-#, c-format
-msgid "current stock will stay at %1$s"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:129
-#: src/paths/instance/products/list/Table.tsx:204
-#, c-format
-msgid "Incoming"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:130
-#: src/paths/instance/products/list/Table.tsx:205
-#, c-format
-msgid "Lost"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:142
-#, c-format
-msgid "Current"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:145
-#, c-format
-msgid "without stock"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:150
-#, c-format
-msgid "Next restock"
-msgstr ""
-
-#: src/components/form/InputStock.tsx:152
-#, c-format
-msgid "Delivery address"
-msgstr ""
-
-#: src/components/form/InputTaxes.tsx:73
-#, c-format
-msgid "this product has no taxes"
-msgstr ""
-
-#: src/components/form/InputTaxes.tsx:77
-#: src/paths/instance/orders/details/DetailPage.tsx:145
-#: src/paths/instance/orders/details/DetailPage.tsx:296
-#: src/paths/instance/orders/list/Table.tsx:116
-#: src/paths/instance/transfers/create/CreatePage.tsx:84
-#, c-format
-msgid "Amount"
-msgstr ""
-
-#: src/components/form/InputTaxes.tsx:78
-#, c-format
-msgid "currency and value separated with colon"
-msgstr ""
-
-#: src/components/form/InputTaxes.tsx:84
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:78
-#, c-format
-msgid "Add"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:53
-#, c-format
-msgid "Instance"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:59
-#, c-format
-msgid "Settings"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:65
-#: src/paths/instance/orders/list/Table.tsx:60
-#, c-format
-msgid "Orders"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:71
-#: src/paths/instance/orders/create/CreatePage.tsx:258
-#: src/paths/instance/products/list/Table.tsx:48
-#, c-format
-msgid "Products"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:77
-#: src/paths/instance/transfers/list/Table.tsx:65
-#, c-format
-msgid "Transfers"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:87
-#, c-format
-msgid "Connection"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:112 src/paths/admin/list/Table.tsx:57
-#, c-format
-msgid "Instances"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:116
-#, c-format
-msgid "New"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:122
-#, c-format
-msgid "List"
-msgstr ""
-
-#: src/components/menu/SideBar.tsx:129
-#, c-format
-msgid "Log out"
-msgstr ""
-
-#: src/components/modal/index.tsx:74
-#, c-format
-msgid "Clear"
-msgstr ""
-
-#: src/components/modal/index.tsx:110 src/components/modal/index.tsx:111
-#, c-format
-msgid "should be the same"
-msgstr ""
-
-#: src/components/modal/index.tsx:111
-#, c-format
-msgid "cannot be the same as before"
-msgstr ""
-
-#: src/components/modal/index.tsx:114
-#, c-format
-msgid ""
-"You are updating the authorization token from instance %1$s with id %2$s"
-msgstr ""
-
-#: src/components/modal/index.tsx:124
-#, c-format
-msgid "Old token"
-msgstr ""
-
-#: src/components/modal/index.tsx:125
-#, c-format
-msgid "New token"
-msgstr ""
-
-#: src/components/modal/index.tsx:127
-#, c-format
-msgid "Clearing the auth token will mean public access to the instance"
-msgstr ""
-
-#: src/components/product/ProductForm.tsx:96
-#: src/paths/admin/create/CreatePage.tsx:85 src/paths/admin/list/Table.tsx:109
-#: src/paths/instance/transfers/list/Table.tsx:122
-#, c-format
-msgid "ID"
-msgstr ""
-
-#: src/components/product/ProductForm.tsx:98
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:121
-#: src/paths/instance/products/list/Table.tsx:85
-#, c-format
-msgid "Image"
-msgstr ""
-
-#: src/components/product/ProductForm.tsx:100
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:123
-#, c-format
-msgid "Unit"
-msgstr ""
-
-#: src/components/product/ProductForm.tsx:101
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:124
-#: src/paths/instance/products/list/Table.tsx:162
-#: src/paths/instance/products/list/Table.tsx:214
-#, c-format
-msgid "Price"
-msgstr ""
-
-#: src/components/product/ProductForm.tsx:103
-#: src/paths/instance/products/list/Table.tsx:90
-#, c-format
-msgid "Stock"
-msgstr ""
-
-#: src/components/product/ProductForm.tsx:105
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:128
-#: src/paths/instance/products/list/Table.tsx:88
-#, c-format
-msgid "Taxes"
-msgstr ""
-
-#: src/index.tsx:75
-#, c-format
-msgid "Server not found"
-msgstr ""
-
-#: src/index.tsx:85
-#, c-format
-msgid "Couldn't access the server"
-msgstr ""
-
-#: src/index.tsx:87 src/index.tsx:99
-#, c-format
-msgid "Got message %1$s from %2$s"
-msgstr ""
-
-#: src/index.tsx:97
-#, c-format
-msgid "Unexpected Error"
-msgstr ""
-
-#: src/paths/admin/create/CreatePage.tsx:89
-#: src/paths/instance/update/UpdatePage.tsx:108
-#, c-format
-msgid "Auth token"
-msgstr ""
-
-#: src/paths/admin/create/CreatePage.tsx:91
-#: src/paths/instance/details/DetailPage.tsx:77
-#: src/paths/instance/update/UpdatePage.tsx:110
-#, c-format
-msgid "Account address"
-msgstr ""
-
-#: src/paths/admin/create/CreatePage.tsx:93
-#: src/paths/instance/update/UpdatePage.tsx:112
-#, c-format
-msgid "Default max deposit fee"
-msgstr ""
-
-#: src/paths/admin/create/CreatePage.tsx:95
-#: src/paths/instance/update/UpdatePage.tsx:114
-#, c-format
-msgid "Default max wire fee"
-msgstr ""
-
-#: src/paths/admin/create/CreatePage.tsx:97
-#: src/paths/instance/update/UpdatePage.tsx:116
-#, c-format
-msgid "Default wire fee amortization"
-msgstr ""
-
-#: src/paths/admin/create/CreatePage.tsx:103
-#: src/paths/instance/update/UpdatePage.tsx:122
-#, c-format
-msgid "Jurisdiction"
-msgstr ""
-
-#: src/paths/admin/create/CreatePage.tsx:107
-#: src/paths/instance/update/UpdatePage.tsx:126
-#, c-format
-msgid "Default pay delay"
-msgstr ""
-
-#: src/paths/admin/create/CreatePage.tsx:109
-#: src/paths/instance/update/UpdatePage.tsx:128
-#, c-format
-msgid "Default wire transfer delay"
-msgstr ""
-
-#: src/paths/admin/create/index.tsx:58
-#, c-format
-msgid "could not create instance"
-msgstr ""
-
-#: src/paths/admin/list/Table.tsx:63 src/paths/admin/list/Table.tsx:131
-#: src/paths/instance/transfers/list/Table.tsx:71
-#, c-format
-msgid "Delete"
-msgstr ""
-
-#: src/paths/admin/list/Table.tsx:128
-#, c-format
-msgid "Edit"
-msgstr ""
-
-#: src/paths/admin/list/Table.tsx:149
-#: src/paths/instance/products/list/Table.tsx:245
-#, c-format
-msgid "There is no instances yet, add more pressing the + sign"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:237
-#, c-format
-msgid "Inventory products"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:286
-#, c-format
-msgid "Total price"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:287
-#, c-format
-msgid "Total tax"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:289
-#: src/paths/instance/orders/create/CreatePage.tsx:297
-#, c-format
-msgid "Order price"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:295
-#, c-format
-msgid "Net"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:300
-#: src/paths/instance/orders/details/DetailPage.tsx:144
-#: src/paths/instance/orders/details/DetailPage.tsx:295
-#: src/paths/instance/orders/list/Table.tsx:117
-#, c-format
-msgid "Summary"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:302
-#, c-format
-msgid "Payments options"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:303
-#, c-format
-msgid "Auto refund deadline"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:304
-#, c-format
-msgid "Refund deadline"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:305
-#, c-format
-msgid "Pay deadline"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:307
-#, c-format
-msgid "Delivery date"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:308
-#, c-format
-msgid "Location"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:312
-#, c-format
-msgid "Max fee"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:313
-#, c-format
-msgid "Max wire fee"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:314
-#, c-format
-msgid "Wire fee amortization"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:315
-#, c-format
-msgid "Fullfilment url"
-msgstr ""
-
-#: src/paths/instance/orders/create/CreatePage.tsx:318
-#, c-format
-msgid "Extra information"
-msgstr ""
-
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:44
-#, c-format
-msgid "select a product first"
-msgstr ""
-
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:51
-#, c-format
-msgid "should be greater than 0"
-msgstr ""
-
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:58
-#, c-format
-msgid ""
-"cannot be greater than current stock and quantity previously added. max: %1$s"
-msgstr ""
-
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:64
-#, c-format
-msgid "cannot be greater than current stock %1$s"
-msgstr ""
-
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:76
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:126
-#, c-format
-msgid "Quantity"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:92
-#: src/paths/instance/orders/details/DetailPage.tsx:235
-#: src/paths/instance/orders/details/DetailPage.tsx:333
-#, c-format
-msgid "Order"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:93
-#, c-format
-msgid "claimed"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:110
-#: src/paths/instance/orders/details/DetailPage.tsx:261
-#: src/paths/instance/orders/list/Table.tsx:136
-#, c-format
-msgid "copy url"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:126
-#: src/paths/instance/orders/details/DetailPage.tsx:349
-#, c-format
-msgid "pay at"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:127
-#: src/paths/instance/orders/details/DetailPage.tsx:350
-#, c-format
-msgid "created at"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:138
-#: src/paths/instance/orders/details/DetailPage.tsx:289
-#, c-format
-msgid "Timeline"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:142
-#: src/paths/instance/orders/details/DetailPage.tsx:293
-#, c-format
-msgid "Payment details"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:146
-#: src/paths/instance/orders/details/DetailPage.tsx:299
-#: src/paths/instance/orders/details/DetailPage.tsx:363
-#, c-format
-msgid "Order status"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:156
-#: src/paths/instance/orders/details/DetailPage.tsx:308
-#, c-format
-msgid "Product list"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:236
-#, c-format
-msgid "paid"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:238
-#, c-format
-msgid "wired"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:241
-#, c-format
-msgid "refunded"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:258
-#, c-format
-msgid "refund"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:297
-#, c-format
-msgid "Refunded amount"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:298
-#, c-format
-msgid "Deposit total"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:336
-#, c-format
-msgid "unpaid"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:364
-#, c-format
-msgid "Order status URL"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:365
-#, c-format
-msgid "Pay URI"
-msgstr ""
-
-#: src/paths/instance/orders/details/DetailPage.tsx:383
-#, c-format
-msgid ""
-"Unknown order status. This is an error, please contact the administrator."
-msgstr ""
-
-#: src/paths/instance/orders/details/index.tsx:56
-#: src/paths/instance/orders/list/index.tsx:147
-#, c-format
-msgid "refund created successfully"
-msgstr ""
-
-#: src/paths/instance/orders/details/index.tsx:59
-#: src/paths/instance/orders/list/index.tsx:150
-#, c-format
-msgid "could not create the refund"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:111
-#, c-format
-msgid "load newer orders"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:115
-#, c-format
-msgid "Date"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:131
-#: src/paths/instance/orders/list/Table.tsx:223
-#, c-format
-msgid "Refund"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:145
-#, c-format
-msgid "load older orders"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:154
-#, c-format
-msgid "No orders has been found"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:202
-#, c-format
-msgid "date"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:203
-#, c-format
-msgid "amount"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:204
-#, c-format
-msgid "reason"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:224
-#, c-format
-msgid "Max refundable:"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:226
-#, c-format
-msgid "Reason"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:226
-#, c-format
-msgid "duplicated"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:226
-#, c-format
-msgid "requested by the customer"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:226
-#, c-format
-msgid "other"
-msgstr ""
-
-#: src/paths/instance/orders/list/index.tsx:91
-#, c-format
-msgid "go to order id"
-msgstr ""
-
-#: src/paths/instance/orders/list/index.tsx:107
-#, c-format
-msgid "Paid"
-msgstr ""
-
-#: src/paths/instance/orders/list/index.tsx:108
-#, c-format
-msgid "Refunded"
-msgstr ""
-
-#: src/paths/instance/orders/list/index.tsx:109
-#, c-format
-msgid "Not wired"
-msgstr ""
-
-#: src/paths/instance/orders/list/index.tsx:110
-#, c-format
-msgid "All"
-msgstr ""
-
-#: src/paths/instance/products/create/index.tsx:48
-#: src/paths/instance/products/update/index.tsx:64
-#, c-format
-msgid "could not create product"
-msgstr ""
-
-#: src/paths/instance/products/list/Table.tsx:87
-#, c-format
-msgid "Sell"
-msgstr ""
-
-#: src/paths/instance/products/list/Table.tsx:89
-#, c-format
-msgid "Profit"
-msgstr ""
-
-#: src/paths/instance/products/list/Table.tsx:91
-#, c-format
-msgid "Sold"
-msgstr ""
-
-#: src/paths/instance/products/list/index.tsx:59
-#, c-format
-msgid "product updated successfully"
-msgstr ""
-
-#: src/paths/instance/products/list/index.tsx:62
-#, c-format
-msgid "could not update the product"
-msgstr ""
-
-#: src/paths/instance/products/list/index.tsx:70
-#, c-format
-msgid "product delete successfully"
-msgstr ""
-
-#: src/paths/instance/products/list/index.tsx:73
-#, c-format
-msgid "could not delete the product"
-msgstr ""
-
-#: src/paths/instance/tips/list/Table.tsx:59
-#, c-format
-msgid "Tips"
-msgstr ""
-
-#: src/paths/instance/tips/list/Table.tsx:111
-#, c-format
-msgid "Committed amount"
-msgstr ""
-
-#: src/paths/instance/tips/list/Table.tsx:112
-#, c-format
-msgid "Exchange initial amount"
-msgstr ""
-
-#: src/paths/instance/tips/list/Table.tsx:113
-#, c-format
-msgid "Merchant initial amount"
-msgstr ""
-
-#: src/paths/instance/tips/list/Table.tsx:148
-#, c-format
-msgid "There is no tips yet, add more pressing the + sign"
-msgstr ""
-
-#: src/paths/instance/transfers/create/CreatePage.tsx:50
-#: src/paths/instance/transfers/create/CreatePage.tsx:54
-#: src/paths/instance/transfers/create/CreatePage.tsx:55
-#: src/paths/instance/transfers/create/CreatePage.tsx:56
-#, c-format
-msgid "cannot be empty"
-msgstr ""
-
-#: src/paths/instance/transfers/create/CreatePage.tsx:51
-#, c-format
-msgid "check the id, doest look valid"
-msgstr ""
-
-#: src/paths/instance/transfers/create/CreatePage.tsx:52
-#, c-format
-msgid "should have 52 characters, current %1$s"
-msgstr ""
-
-#: src/paths/instance/transfers/create/CreatePage.tsx:57
-#, c-format
-msgid "URL doesn't have the right format"
-msgstr ""
-
-#: src/paths/instance/transfers/create/CreatePage.tsx:74
-#, c-format
-msgid "Transfer ID"
-msgstr ""
-
-#: src/paths/instance/transfers/create/CreatePage.tsx:76
-#, c-format
-msgid "Account Address"
-msgstr ""
-
-#: src/paths/instance/transfers/create/CreatePage.tsx:82
-#: src/paths/instance/transfers/list/Table.tsx:125
-#, c-format
-msgid "Exchange URL"
-msgstr ""
-
-#: src/paths/instance/transfers/create/index.tsx:49
-#, c-format
-msgid "could not inform transfer"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:118
-#, c-format
-msgid "load newer transfers"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:123
-#, c-format
-msgid "Credit"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:126
-#, c-format
-msgid "Confirmed"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:127
-#: src/paths/instance/transfers/list/index.tsx:60
-#, c-format
-msgid "Verified"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:128
-#, c-format
-msgid "Executed at"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:138
-#: src/paths/instance/transfers/list/Table.tsx:139
-#, c-format
-msgid "yes"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:138
-#: src/paths/instance/transfers/list/Table.tsx:139
-#, c-format
-msgid "no"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:140
-#, c-format
-msgid "unknown"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:145
-#, c-format
-msgid "load older transfers"
-msgstr ""
-
-#: src/paths/instance/transfers/list/Table.tsx:154
-#, c-format
-msgid "There is no transfer yet, add more pressing the + sign"
-msgstr ""
diff --git a/packages/merchant-backend-ui/src/index.tsx b/packages/merchant-backend-ui/src/index.tsx
deleted file mode 100644
index 275f63371..000000000
--- a/packages/merchant-backend-ui/src/index.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { h, VNode, Fragment } from 'preact';
-import { BackendContextProvider } from './context/backend';
-import { TranslationProvider } from './context/translation';
-// import { Page as RequestPayment } from './RequestPayment';
-import "./css/pure-min.css"
-import { Route, Router } from 'preact-router';
-import { Footer } from './components/Footer';
-// import OfferTip from './pages/OfferTip';
-// import {OfferRefund} from './pages/OfferRefund';
-// import DepletedTip from './pages/DepletedTip';
-// import RequestPayment from './pages/RequestPayment';
-// import ShowOrderDetails from './pages/ShowOrderDetails';
-
-export default function Application(): VNode {
- return (
- // <FetchContextProvider>
- <BackendContextProvider>
- <TranslationProvider>
- <ApplicationStatusRoutes />
- </TranslationProvider>
- </BackendContextProvider>
- // </FetchContextProvider>
- );
-}
-
-function ApplicationStatusRoutes(): VNode {
- return <Fragment>
- <Router>
- {/* <Route path="offer_tip" component={OfferTip} />
- <Route path="offer_refund" component={OfferRefund} />
- <Route path="depleted_tip" component={DepletedTip} />
- <Route path="request_payment" component={RequestPayment} />
- <Route path="show_order_details" component={ShowOrderDetails} /> */}
- <Route default component={() => <div>
- hello!
- </div>} />
- </Router>
- <Footer />
- </Fragment>
-}
diff --git a/packages/merchant-backend-ui/src/pages/DepletedTip.tsx b/packages/merchant-backend-ui/src/pages/DepletedTip.tsx
deleted file mode 100644
index 756b08d6a..000000000
--- a/packages/merchant-backend-ui/src/pages/DepletedTip.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-import { Fragment, h, render, VNode } from 'preact';
-import { render as renderToString } from 'preact-render-to-string';
-import { Footer } from '../components/Footer';
-import "../css/pure-min.css";
-import "../css/style.css";
-import { Page } from '../styled';
-
-function Head(): VNode {
- return <title>Status of your tip</title>
-}
-
-export function DepletedTip(): VNode {
- return <Page>
- <section>
- <h1>Tip already collected</h1>
- <div>
- You have already collected this tip.
- </div>
- </section>
- <Footer />
- </Page>
-}
-
-export function mount(): void {
- try {
- render(<DepletedTip />, document.body);
- } catch (e) {
- console.error("got error", e);
- if (e instanceof Error) {
- document.body.innerText = `Fatal error: "${e.message}". Please report this bug at https://bugs.gnunet.org/.`;
- }
- }
-}
-
-export function buildTimeRendering(): { head: string, body: string } {
- return {
- head: renderToString(<Head />),
- body: renderToString(<DepletedTip />)
- }
-}
diff --git a/packages/merchant-backend-ui/src/pages/OfferRefund.tsx b/packages/merchant-backend-ui/src/pages/OfferRefund.tsx
index 14c9372c2..b1cf63572 100644
--- a/packages/merchant-backend-ui/src/pages/OfferRefund.tsx
+++ b/packages/merchant-backend-ui/src/pages/OfferRefund.tsx
@@ -52,6 +52,8 @@ function Head({ order_summary }: { order_summary?: string }): VNode {
return <Fragment>
<meta charSet="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <meta name="taler-support" content="uri" />
+ <meta name="taler-uri" content="{{ taler_refund_uri }}"></meta>
<noscript>
<meta http-equiv="refresh" content="1" />
</noscript>
@@ -61,6 +63,8 @@ function Head({ order_summary }: { order_summary?: string }): VNode {
export function OfferRefund({ refundURI, qr_code, order_status_url }: Props): VNode {
useEffect(() => {
+ const longpollDelayMs = 60 * 1000;
+ const delayMs = 500;
let checkUrl: URL;
try {
checkUrl = new URL(order_status_url ? order_status_url : "{{& order_status_url }}");
@@ -68,7 +72,7 @@ export function OfferRefund({ refundURI, qr_code, order_status_url }: Props): VN
return;
}
checkUrl.searchParams.set("await_refund_obtained", "yes");
- const delayMs = 500;
+ checkUrl.searchParams.set("timeout_ms", longpollDelayMs.toString());
function check() {
let retried = false;
function retryOnce() {
@@ -111,7 +115,7 @@ export function OfferRefund({ refundURI, qr_code, order_status_url }: Props): VN
<QRPlaceholder dangerouslySetInnerHTML={{ __html: qr_code ? qr_code : `{{{ taler_refund_qrcode_svg }}}` }} />
<p>
<WalletLink href={refundURI ? refundURI : `{{ taler_refund_uri }}`}>
- Or open your Taller wallet
+ Or open your Taler wallet
</WalletLink>
</p>
<p>
diff --git a/packages/merchant-backend-ui/src/pages/OfferTip.stories.tsx b/packages/merchant-backend-ui/src/pages/OfferTip.stories.tsx
deleted file mode 100644
index dfbf71fff..000000000
--- a/packages/merchant-backend-ui/src/pages/OfferTip.stories.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { h, VNode, FunctionalComponent } from 'preact';
-import { createSVG } from '../components/QR';
-import { OfferTip as TestedComponent } from './OfferTip';
-
-
-export default {
- title: 'OfferTip',
- component: TestedComponent,
- argTypes: {
- },
-};
-
-function createExample<Props>(Component: FunctionalComponent<Props>, props: Partial<Props>) {
- const r = (args: any) => <Component {...args} />
- r.args = props
- return r
-}
-
-const TIP_URI_EXAMPLE = 'taler+http://tip/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0'
-
-export const Example = createExample(TestedComponent, {
- tipURI: TIP_URI_EXAMPLE,
- qr_code: createSVG(TIP_URI_EXAMPLE)
-});
diff --git a/packages/merchant-backend-ui/src/pages/OfferTip.tsx b/packages/merchant-backend-ui/src/pages/OfferTip.tsx
deleted file mode 100644
index ace1059ca..000000000
--- a/packages/merchant-backend-ui/src/pages/OfferTip.tsx
+++ /dev/null
@@ -1,141 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-import { Fragment, h, render, VNode } from 'preact';
-import { render as renderToString } from 'preact-render-to-string';
-import { useEffect } from 'preact/hooks';
-import { Footer } from '../components/Footer';
-import { QR } from '../components/QR';
-import "../css/pure-min.css";
-import "../css/style.css";
-import { Page, QRPlaceholder, WalletLink } from '../styled';
-import { ShowOrderDetails } from './ShowOrderDetails';
-
-
-/**
- * This page creates a tip offer QR code
- *
- * It will build into a mustache html template for server side rendering
- *
- * server side rendering params:
- * - tip_status_url
- * - taler_tip_qrcode_svg
- * - taler_tip_uri
- *
- * request params:
- * - tip_uri
- * - tip_status_url
- */
-
-interface Props {
- tipURI?: string,
- tip_status_url?: string,
- qr_code?: string,
-}
-
-export function Head(): VNode {
- return <Fragment>
- <meta charSet="UTF-8" />
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
- <noscript>
- <meta http-equiv="refresh" content="1" />
- </noscript>
- <title>Tip available</title>
- </Fragment>
-}
-
-export function OfferTip({ tipURI, qr_code, tip_status_url }: Props): VNode {
- useEffect(() => {
- let checkUrl: URL;
- try {
- checkUrl = new URL(tip_status_url ? tip_status_url : "{{& tip_status_url }}");
- } catch (e) {
- return;
- }
-
- const delayMs = 500;
- function check() {
- let retried = false;
- function retryOnce() {
- if (!retried) {
- retried = true;
- check();
- }
- }
- const req = new XMLHttpRequest();
- req.onreadystatechange = function () {
- if (req.readyState === XMLHttpRequest.DONE) {
- if (req.status === 410) {
- window.location.reload();
- }
- setTimeout(retryOnce, delayMs);
- }
- };
- req.onerror = function () {
- setTimeout(retryOnce, delayMs);
- }
- req.open("GET", checkUrl.href);
- req.send();
- }
-
- setTimeout(check, delayMs);
- })
- return <Page>
- <section>
- <h1 >Collect Taler tip</h1>
- <p>
- Scan this QR code with your Taler mobile wallet:
- </p>
- <QRPlaceholder dangerouslySetInnerHTML={{ __html: qr_code ? qr_code : `{{{ taler_tip_qrcode_svg }}}` }} />
- <p>
- <WalletLink href={tipURI ? tipURI : `{{ taler_tip_uri }}`}>
- Or open your Taller wallet
- </WalletLink>
- </p>
- <p>
- <a href="https://wallet.taler.net/">Don't have a Taler wallet yet? Install it!</a>
- </p>
- </section>
- <Footer />
- </Page>
-}
-
-export function mount(): void {
- try {
- const fromLocation = new URL(window.location.href).searchParams
-
- const uri = fromLocation.get('tip_uri') || undefined
- const tsu = fromLocation.get('tip_status_url') || undefined
-
- render(<OfferTip tipURI={uri} tip_status_url={tsu} />, document.body);
- } catch (e) {
- console.error("got error", e);
- if (e instanceof Error) {
- document.body.innerText = `Fatal error: "${e.message}". Please report this bug at https://bugs.gnunet.org/.`;
- }
- }
-}
-
-export function buildTimeRendering(): { head: string, body: string } {
- return {
- head: renderToString(<Head />),
- body: renderToString(<ShowOrderDetails />)
- }
-}
diff --git a/packages/merchant-backend-ui/src/pages/RequestPayment.tsx b/packages/merchant-backend-ui/src/pages/RequestPayment.tsx
index 050755dfb..513438ba2 100644
--- a/packages/merchant-backend-ui/src/pages/RequestPayment.tsx
+++ b/packages/merchant-backend-ui/src/pages/RequestPayment.tsx
@@ -55,6 +55,8 @@ function Head({ order_summary }: { order_summary?: string }): VNode {
<Fragment>
<meta charSet="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <meta name="taler-support" content="uri" />
+ <meta name="taler-uri" content="{{ taler_pay_uri }}"></meta>
<noscript>
<meta http-equiv="refresh" content="1" />
</noscript>
@@ -81,7 +83,8 @@ export function RequestPayment({
} catch (e) {
return;
}
- checkUrl.searchParams.set("timeout_s", longpollDelayMs.toString());
+ checkUrl.searchParams.set("timeout_ms", longpollDelayMs.toString());
+ const delayMs = 500;
function check() {
let retried = false;
function retryOnce() {
@@ -98,6 +101,8 @@ export function RequestPayment({
const resp = JSON.parse(req.responseText);
if (resp.fulfillment_url) {
window.location.replace(resp.fulfillment_url);
+ } else {
+ window.location.reload()
}
} catch (e) {
console.error("could not parse response:", e);
@@ -108,6 +113,8 @@ export function RequestPayment({
const resp = JSON.parse(req.responseText);
if (resp.fulfillment_url) {
window.location.replace(resp.fulfillment_url);
+ } else {
+ window.location.reload()
}
} catch (e) {
console.error("could not parse response:", e);
@@ -123,20 +130,20 @@ export function RequestPayment({
console.error("could not parse response:", e);
}
}
- setTimeout(retryOnce, 500);
+ setTimeout(retryOnce, delayMs);
}
};
req.onerror = function () {
- setTimeout(retryOnce, 500);
+ setTimeout(retryOnce, delayMs);
};
req.ontimeout = function () {
- setTimeout(retryOnce, 500);
+ setTimeout(retryOnce, delayMs);
};
req.timeout = longpollDelayMs;
req.open("GET", checkUrl.href);
req.send();
}
- setTimeout(check, 500);
+ setTimeout(check, delayMs);
});
return (
<Page>
@@ -150,7 +157,7 @@ export function RequestPayment({
/>
<p>
<WalletLink href={payURI ? payURI : `{{ taler_pay_uri }}`}>
- Or open your Taller wallet
+ Or open your Taler wallet
</WalletLink>
</p>
<p>
diff --git a/packages/merchant-backend-ui/src/pages/ShowOrderDetails.examples.ts b/packages/merchant-backend-ui/src/pages/ShowOrderDetails.examples.ts
index ba68397ee..86992c9e1 100644
--- a/packages/merchant-backend-ui/src/pages/ShowOrderDetails.examples.ts
+++ b/packages/merchant-backend-ui/src/pages/ShowOrderDetails.examples.ts
@@ -28,7 +28,7 @@ const defaultContractTerms: MerchantBackend.ContractTerms = {
amount: 'USD:10',
summary: 'this is a short summary',
pay_deadline: {
- t_s: new Date().getTime() + 6 * 24 * 60 * 60 * 1000
+ t_s: Math.round(new Date().getTime() / 1000) + 6 * 24 * 60 * 60
},
merchant: {
name: 'the merchant (inc)',
@@ -48,7 +48,7 @@ const defaultContractTerms: MerchantBackend.ContractTerms = {
wire_fee_amortization: 1,
products: [],
timestamp: {
- t_s: new Date().getTime()
+ t_s: Math.round(new Date().getTime() / 1000)
},
auditors: [],
exchanges: [],
@@ -57,18 +57,18 @@ const defaultContractTerms: MerchantBackend.ContractTerms = {
merchant_pub: 'QWEASDQWEASD',
nonce: 'NONCE',
refund_deadline: {
- t_s: new Date().getTime() + 6 * 24 * 60 * 60 * 1000
+ t_s: Math.round(new Date().getTime() / 1000) + 6 * 24 * 60 * 60
},
wire_method: 'x-taler-bank',
wire_transfer_deadline: {
- t_s: new Date().getTime() + 3 * 24 * 60 * 60 * 1000
+ t_s: Math.round(new Date().getTime() / 1000) + 3 * 24 * 60 * 60
},
};
-const inSixDays = new Date().getTime() + 6 * 24 * 60 * 60 * 1000
-const in10Minutes = new Date().getTime() + 10 * 60 * 1000
-const in15Minutes = new Date().getTime() + 15 * 60 * 1000
-const in20Minutes = new Date().getTime() + 20 * 60 * 1000
+const inSixDays = Math.round(new Date().getTime() / 1000) + 6 * 24 * 60 * 60
+const in10Minutes = Math.round(new Date().getTime() / 1000) + 10 * 60
+const in15Minutes = Math.round(new Date().getTime() / 1000) + 15 * 60
+const in20Minutes = Math.round(new Date().getTime() / 1000) + 20 * 60
export const exampleData: { [name: string]: Props } = {
Simplest: {
@@ -216,4 +216,38 @@ export const exampleData: { [name: string]: Props } = {
}
},
},
+ WithFulfillmentURL: {
+ order_summary: 'this is the order with fulfillmentURL',
+ contract_terms: {
+ ...defaultContractTerms,
+ fulfillment_url: "https://demo.taler.net",
+ fulfillment_message: "Congratulations! You just purchased an valuable item!"
+ },
+ },
+ WithFulfillmentMessage: {
+ order_summary: 'this is the order with fulfillment message',
+ contract_terms: {
+ ...defaultContractTerms,
+ fulfillment_message: "Congratulations! You just purchased an valuable item!"
+ },
+ },
+ WithoutWireTransferDeadline: {
+ order_summary: 'this is the order without transfer deadline',
+ contract_terms: {
+ ...defaultContractTerms,
+ // @ts-ignore
+ wire_transfer_deadline: undefined,
+ },
+ },
+ ZeroFee: {
+ order_summary: 'example with zero fee',
+ contract_terms: {
+ ...defaultContractTerms,
+ // @ts-ignore
+ max_fee: undefined,
+ // @ts-ignore
+ max_wire_fee: undefined,
+ fulfillment_message: "Congratulations! You just purchased an valuable item!"
+ },
+ },
}
diff --git a/packages/merchant-backend-ui/src/pages/ShowOrderDetails.tsx b/packages/merchant-backend-ui/src/pages/ShowOrderDetails.tsx
index aa62c2932..7d11eb21d 100644
--- a/packages/merchant-backend-ui/src/pages/ShowOrderDetails.tsx
+++ b/packages/merchant-backend-ui/src/pages/ShowOrderDetails.tsx
@@ -27,6 +27,7 @@ import "../css/pure-min.css";
import "../css/style.css";
import { MerchantBackend } from "../declaration";
import { Page, InfoBox, TableExpanded, TableSimple } from "../styled";
+import { TIME_DATE_FORMAT } from "../utils";
/**
* This page creates a payment request QR code
@@ -56,6 +57,7 @@ function Head({ order_summary }: { order_summary?: string }): VNode {
<Fragment>
<meta charSet="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <meta name="taler-support" content="uri" />
<noscript>
<meta http-equiv="refresh" content="1" />
</noscript>
@@ -79,7 +81,7 @@ function Location({
location: MerchantBackend.Location | undefined;
btr?: boolean;
}) {
- //FIXME: mustache strings show be constructed in a way that ends in the final output of the html but is not present in the
+ //FIXME: mustache strings will be constructed in a way that ends in the final output of the html but is not present in the
// javascript code, otherwise when mustache render engine run over the html it will also replace string in the javascript code
// that is made to run when the browser has javascript enable leading into undefined behavior.
// that's why in the next fields we are using concatenations to build the mustache placeholder.
@@ -171,10 +173,24 @@ export function ShowOrderDetails({
)}
{btr && `{{/refund_amount}}`}
+ {btr && `{{#contract_terms.fulfillment_message}}`}
+ {(btr || contract_terms?.fulfillment_message) && (
+ <section>
+ <InfoBox>
+ <b>{contract_terms?.fulfillment_message || `{{ contract_terms.fulfillment_message }}`}</b>.
+ </InfoBox>
+ </section>
+ )}
+ {btr && `{{/contract_terms.fulfillment_message}}`}
+
<section>
<TableExpanded>
<dt>Order summary:</dt>
<dd>{contract_terms?.summary || `{{ contract_terms.summary }}`}</dd>
+ {btr && `{{#contract_terms.fulfillment_url}}`}
+ <dt>Fulfillment URL:</dt>
+ <dd><a href={contract_terms?.fulfillment_url || `{{ contract_terms.fulfillment_url }}`}>{contract_terms?.fulfillment_url || `{{ contract_terms.fulfillment_url }}`}</a></dd>
+ {btr && `{{/contract_terms.fulfillment_url}}`}
<dt>Amount paid:</dt>
<dd>{contract_terms?.amount || `{{ contract_terms.amount }}`}</dd>
<dt>Order date:</dt>
@@ -182,9 +198,9 @@ export function ShowOrderDetails({
{contract_terms?.timestamp
? contract_terms?.timestamp.t_s != "never"
? format(
- contract_terms?.timestamp.t_s,
- "dd MMM yyyy HH:mm:ss"
- )
+ contract_terms?.timestamp.t_s * 1000,
+ TIME_DATE_FORMAT,
+ )
: "never"
: `{{ contract_terms.timestamp_str }}`}{" "}
</dd>
@@ -242,9 +258,9 @@ export function ShowOrderDetails({
{p.delivery_date
? p.delivery_date.t_s != "never"
? format(
- p.delivery_date.t_s,
- "dd MMM yyyy HH:mm:ss"
- )
+ p.delivery_date.t_s,
+ TIME_DATE_FORMAT,
+ )
: "never"
: `{{ delivery_date_str }}`}{" "}
</dd>
@@ -292,9 +308,9 @@ export function ShowOrderDetails({
{contract_terms?.delivery_date
? contract_terms?.delivery_date.t_s != "never"
? format(
- contract_terms?.delivery_date.t_s,
- "dd MMM yyyy HH:mm:ss"
- )
+ contract_terms?.delivery_date.t_s,
+ TIME_DATE_FORMAT,
+ )
: "never"
: `{{ contract_terms.delivery_date_str }}`}{" "}
</dd>
@@ -322,48 +338,47 @@ export function ShowOrderDetails({
<section>
<h2>Full payment information</h2>
<TableExpanded>
- <dt>Amount paid:</dt>
- <dd>{contract_terms?.amount || `{{ contract_terms.amount }}`}</dd>
- <dt>Wire transfer method:</dt>
- <dd>
- {contract_terms?.wire_method ||
- `{{ contract_terms.wire_method }}`}
- </dd>
- <dt>Payment deadline:</dt>
- <dd>
- {contract_terms?.pay_deadline
- ? contract_terms?.pay_deadline.t_s != "never"
- ? format(
- contract_terms?.pay_deadline.t_s,
- "dd MMM yyyy HH:mm:ss"
- )
- : "never"
- : `{{ contract_terms.pay_deadline_str }}`}{" "}
- </dd>
<dt>Exchange transfer deadline:</dt>
+ {btr && `{{` + `#contract_terms.wire_transfer_deadline_str}}`}
<dd>
{contract_terms?.wire_transfer_deadline
? contract_terms?.wire_transfer_deadline.t_s != "never"
? format(
- contract_terms?.wire_transfer_deadline.t_s,
- "dd MMM yyyy HH:mm:ss"
- )
+ contract_terms?.wire_transfer_deadline.t_s * 1000,
+ TIME_DATE_FORMAT,
+ )
: "never"
: `{{ contract_terms.wire_transfer_deadline_str }}`}{" "}
</dd>
+ {btr && `{{` + `/contract_terms.wire_transfer_deadline_str}}`}
+
+ {btr && `{{` + `^contract_terms.wire_transfer_deadline_str}}`}
+ <dd>
+ Wire transfer settled.
+ </dd>
+ {btr && `{{` + `/contract_terms.wire_transfer_deadline_str}}`}
+
+ {btr && `{{` + `#contract_terms.max_fee}}`}
<dt>Maximum deposit fee:</dt>
<dd>{contract_terms?.max_fee || `{{ contract_terms.max_fee }}`}</dd>
+ {btr && `{{` + `/contract_terms.max_fee}}`}
+
+ {btr && `{{` + `#contract_terms.max_wire_fee}}`}
<dt>Maximum wire fee:</dt>
<dd>
{contract_terms?.max_wire_fee ||
`{{ contract_terms.max_wire_fee }}`}
</dd>
+ {btr && `{{` + `/contract_terms.max_wire_fee}}`}
+
+ {btr && `{{` + `#contract_terms.wire_fee_amortization}}`}
<dt>Wire fee amortization:</dt>
<dd>
{contract_terms?.wire_fee_amortization ||
`{{ contract_terms.wire_fee_amortization }}`}{" "}
transactions
</dd>
+ {btr && `{{` + `/contract_terms.wire_fee_amortization}}`}
</TableExpanded>
</section>
@@ -375,9 +390,9 @@ export function ShowOrderDetails({
{contract_terms?.refund_deadline
? contract_terms?.refund_deadline.t_s != "never"
? format(
- contract_terms?.refund_deadline.t_s,
- "dd MMM yyyy HH:mm:ss"
- )
+ contract_terms?.refund_deadline.t_s * 1000,
+ TIME_DATE_FORMAT,
+ )
: "never"
: `{{ contract_terms.refund_deadline_str }}`}{" "}
</dd>
@@ -390,11 +405,11 @@ export function ShowOrderDetails({
{contract_terms?.auto_refund
? contract_terms?.auto_refund.d_us != "forever"
? formatDuration(
- intervalToDuration({
- start: 0,
- end: contract_terms?.auto_refund.d_us,
- })
- )
+ intervalToDuration({
+ start: 0,
+ end: contract_terms?.auto_refund.d_us,
+ }),
+ )
: "forever"
: `{{ contract_terms.auto_refund_str }}`}{" "}
</dd>
@@ -525,7 +540,7 @@ export function mount(): void {
let contractTerms: MerchantBackend.ContractTerms | undefined;
try {
contractTerms = JSON.parse((window as any).contractTermsStr);
- } catch {}
+ } catch { }
render(
<ShowOrderDetails
@@ -533,7 +548,7 @@ export function mount(): void {
order_summary={os}
refund_amount={ra}
/>,
- document.body
+ document.body,
);
} catch (e) {
console.error("got error", e);
diff --git a/packages/merchant-backend-ui/src/render-examples.ts b/packages/merchant-backend-ui/src/render-examples.ts
new file mode 100644
index 000000000..957e06a58
--- /dev/null
+++ b/packages/merchant-backend-ui/src/render-examples.ts
@@ -0,0 +1,112 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import fs from "fs";
+import mustache from "mustache";
+import { createDateToStringFunction, createDurationToStringFunction, createNonEmptyFunction } from "./utils.js"
+import { exampleData as ShowOrderDetailsExamples } from "./pages/ShowOrderDetails.examples.js";
+/**
+ * This script will emulate what the merchant backend will do when being requested
+ *
+ */
+
+const templateDirectory = process.argv[2];
+const destDirectory = process.argv[3];
+
+if (!templateDirectory || !destDirectory) {
+ console.log("usage: render-mustache <source-directory> <dest-directory>");
+ process.exit(1);
+}
+
+if (!fs.existsSync(destDirectory)) {
+ fs.mkdirSync(destDirectory);
+}
+
+function fromCamelCaseName(name: string) {
+ const result = name
+ .replace(/^[a-z]/, (letter) => `${letter.toUpperCase()}`) //first letter lowercase
+ .replace(/_[a-z]/g, (letter) => `${letter[1].toUpperCase()}`); //snake case
+ return result;
+}
+/**
+ * Load all the html files
+ */
+const templateFiles = fs.readdirSync(templateDirectory).filter((f) => /.html/.test(f));
+const exampleByTemplate: Record<string, any> = {
+ "show_order_details.en.html": ShowOrderDetailsExamples
+}
+
+templateFiles.forEach((templateFile) => {
+ const html = fs.readFileSync(`${templateDirectory}/${templateFile}`, "utf8");
+
+ const templateFileWithoutExt = templateFile.replace(".en.html", "");
+ // const exampleFileName = `src/pages/${fromCamelCaseName(testName)}.examples`;
+ // if (!fs.existsSync(`./${exampleFileName}.ts`)) {
+ // console.log(`- skipping ${testName}: no examples found`);
+ // return;
+ // }
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
+ // const pepe = `./${exampleFileName}.ts`
+ // const { exampleData } = require(pepe);
+
+ const exampleData = exampleByTemplate[templateFile]
+ if (!exampleData) {
+ console.log(`- skipping ${templateFile}: no examples found`);
+ return;
+ }
+ const exampleNames = Object.keys(exampleData)
+ console.log(`+ rendering ${templateFile}: ${exampleNames.length} examples`);
+ exampleNames.forEach((exampleName) => {
+ const example = exampleData[exampleName];
+
+ //enhance the example with more information
+ example.contract_terms_json = () => JSON.stringify(example.contract_terms);
+
+ example.contract_terms.timestamp_str = createDateToStringFunction(example.contract_terms.timestamp)
+
+ example.contract_terms.hasProducts = createNonEmptyFunction(example.contract_terms.products)
+ example.contract_terms.hasAuditors = createNonEmptyFunction(example.contract_terms.auditors)
+ example.contract_terms.hasExchanges = createNonEmptyFunction(example.contract_terms.exchanges)
+
+ example.contract_terms.products.forEach((p: any) => {
+ p.delivery_date_str = createDateToStringFunction(p.delivery_date)
+ p.hasTaxes = createNonEmptyFunction(p.taxes)
+ });
+
+ example.contract_terms.has_delivery_info = () =>
+ example.contract_terms.delivery_date ||
+ example.contract_terms.delivery_location;
+
+ example.contract_terms.delivery_date_str = createDateToStringFunction(example.contract_terms.delivery_date)
+ example.contract_terms.pay_deadline_str = createDateToStringFunction(example.contract_terms.pay_deadline)
+ example.contract_terms.wire_transfer_deadline_str = createDateToStringFunction(example.contract_terms.wire_transfer_deadline)
+
+ example.contract_terms.refund_deadline_str = createDateToStringFunction(example.contract_terms.refund_deadline)
+ example.contract_terms.auto_refund_str = createDurationToStringFunction(example.contract_terms.auto_refund)
+
+ const output = mustache.render(html, example);
+
+ fs.writeFileSync(
+ `${destDirectory}/${templateFileWithoutExt}.${exampleName}.html`,
+ output,
+ );
+ });
+});
diff --git a/packages/merchant-backend-ui/src/utils/table.ts b/packages/merchant-backend-ui/src/utils.ts
index 198c46543..0a420aa22 100644
--- a/packages/merchant-backend-ui/src/utils/table.ts
+++ b/packages/merchant-backend-ui/src/utils.ts
@@ -14,24 +14,28 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { WithId } from "../declaration";
+import { format, formatDuration, intervalToDuration } from "date-fns";
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+export const TIME_DATE_FORMAT = "dd MMM yyyy HH:mm:ss"
-export interface Actions<T extends WithId> {
- element: T;
- type: 'DELETE' | 'UPDATE';
+export function createDateToStringFunction(date: any) {
+ return () => {
+ if (!date) return "";
+ return format(date.t_s * 1000, TIME_DATE_FORMAT);
+ }
}
-function notEmpty<TValue>(value: TValue | null | undefined): value is TValue {
- return value !== null && value !== undefined;
+export function createDurationToStringFunction(duration: any) {
+ return () => {
+ if (!duration) return "";
+ return formatDuration(intervalToDuration({ start: 0, end: duration.d_us }));
+ }
}
-export function buildActions<T extends WithId>(instances: T[], selected: string[], action: 'DELETE'): Actions<T>[] {
- return selected.map(id => instances.find(i => i.id === id))
- .filter(notEmpty)
- .map(id => ({ element: id, type: action }))
+export function createNonEmptyFunction(list: any) {
+ return () => {
+ if (!list) return false;
+ return list.length > 0;
+ }
}
+
diff --git a/packages/merchant-backend-ui/src/utils/amount.ts b/packages/merchant-backend-ui/src/utils/amount.ts
deleted file mode 100644
index a54c52abe..000000000
--- a/packages/merchant-backend-ui/src/utils/amount.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- 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/>
- */
-import { amountFractionalBase, AmountJson, Amounts } from "@gnu-taler/taler-util";
-import { MerchantBackend } from "../declaration";
-
-/**
- * sums two prices,
- * @param one
- * @param two
- * @returns
- */
-const sumPrices = (one: string, two: string) => {
- const [currency, valueOne] = one.split(':')
- const [, valueTwo] = two.split(':')
- return `${currency}:${parseInt(valueOne, 10) + parseInt(valueTwo, 10)}`
-}
-
-/**
- * merge refund with the same description and a difference less than one minute
- * @param prev list of refunds that will hold the merged refunds
- * @param cur new refund to add to the list
- * @returns list with the new refund, may be merged with the last
- */
-export function mergeRefunds(prev: MerchantBackend.Orders.RefundDetails[], cur: MerchantBackend.Orders.RefundDetails) {
- let tail;
-
- if (prev.length === 0 || //empty list
- cur.timestamp.t_s === 'never' || //current does not have timestamp
- (tail = prev[prev.length - 1]).timestamp.t_s === 'never' || // last does not have timestamp
- cur.reason !== tail.reason || //different reason
- Math.abs(cur.timestamp.t_s - tail.timestamp.t_s) > 1000 * 60) {//more than 1 minute difference
-
- prev.push(cur)
- return prev
- }
-
- prev[prev.length - 1] = {
- ...tail,
- amount: sumPrices(tail.amount, cur.amount)
- }
-
- return prev
-}
-
-export const rate = (one: string, two: string) => {
- const a = Amounts.parseOrThrow(one)
- const b = Amounts.parseOrThrow(two)
- const af = toFloat(a)
- const bf = toFloat(b)
- if (bf === 0) return 0
- return af / bf
-}
-
-function toFloat(amount: AmountJson) {
- return amount.value + (amount.fraction / amountFractionalBase);
-}
diff --git a/packages/merchant-backend-ui/src/utils/constants.ts b/packages/merchant-backend-ui/src/utils/constants.ts
deleted file mode 100644
index 37c46e4c2..000000000
--- a/packages/merchant-backend-ui/src/utils/constants.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-//https://tools.ietf.org/html/rfc8905
-export const PAYTO_REGEX = /^payto:\/\/[a-zA-Z][a-zA-Z0-9-.]+(\/[a-zA-Z0-9\-\.\~\(\)@_%:!$&'*+,;=]*)*\??((amount|receiver-name|sender-name|instruction|message)=[a-zA-Z0-9\-\.\~\(\)@_%:!$'*+,;=]*&?)*$/
-export const PAYTO_WIRE_METHOD_LOOKUP = /payto:\/\/([a-zA-Z][a-zA-Z0-9-.]+)\/.*/
-
-export const AMOUNT_REGEX = /^[a-zA-Z][a-zA-Z]*:[0-9][0-9,]*\.?[0-9,]*$/
-
-export const INSTANCE_ID_LOOKUP = /^\/instances\/([^/]*)\/?$/
-
-export const AMOUNT_ZERO_REGEX = /^[a-zA-Z][a-zA-Z]*:0$/
-
-export const CROCKFORD_BASE32_REGEX = /^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]+[*~$=U]*$/
-
-export const URL_REGEX = /^((https?:)(\/\/\/?)([\w]*(?::[\w]*)?@)?([\d\w\.-]+)(?::(\d+))?)\/$/
-
-// how much rows we add every time user hit load more
-export const PAGE_SIZE = 20
-// how bigger can be the result set
-// after this threshold, load more with move the cursor
-export const MAX_RESULT_SIZE = PAGE_SIZE * 2 - 1;
-
-// how much we will wait for all request, in seconds
-export const DEFAULT_REQUEST_TIMEOUT = 10;
-
-export const MAX_IMAGE_SIZE = 1024 * 1024;
-
-export const INSTANCE_ID_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9_.@-]+$/
diff --git a/packages/merchant-backend-ui/tests/__mocks__/fileTransformer.js b/packages/merchant-backend-ui/tests/__mocks__/fileTransformer.js
deleted file mode 100644
index 51ebbfa62..000000000
--- a/packages/merchant-backend-ui/tests/__mocks__/fileTransformer.js
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-// fileTransformer.js
-
-// eslint-disable-next-line @typescript-eslint/no-var-requires
-const path = require('path');
-
-module.exports = {
- process(src, filename, config, options) {
- return `module.exports = ${ JSON.stringify(path.basename(filename)) };`;
- },
-};
-
diff --git a/packages/merchant-backend-ui/tests/funcitons/regex.test.ts b/packages/merchant-backend-ui/tests/funcitons/regex.test.ts
deleted file mode 100644
index fc8a6a42f..000000000
--- a/packages/merchant-backend-ui/tests/funcitons/regex.test.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
- /**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import { AMOUNT_REGEX, PAYTO_REGEX } from "../../src/utils/constants";
-
-describe('payto uri format', () => {
- const valids = [
- 'payto://iban/DE75512108001245126199?amount=EUR:200.0&message=hello',
- 'payto://ach/122000661/1234',
- 'payto://upi/alice@example.com?receiver-name=Alice&amount=INR:200',
- 'payto://void/?amount=EUR:10.5',
- 'payto://ilp/g.acme.bob'
- ]
-
- test('should be valid', () => {
- valids.forEach(v => expect(v).toMatch(PAYTO_REGEX))
- });
-
- const invalids = [
- // has two question marks
- 'payto://iban/DE75?512108001245126199?amount=EUR:200.0&message=hello',
- // has a space
- 'payto://ach /122000661/1234',
- // has a space
- 'payto://upi/alice@ example.com?receiver-name=Alice&amount=INR:200',
- // invalid field name (mount instead of amount)
- 'payto://void/?mount=EUR:10.5',
- // payto:// is incomplete
- 'payto: //ilp/g.acme.bob'
- ]
-
- test('should not be valid', () => {
- invalids.forEach(v => expect(v).not.toMatch(PAYTO_REGEX))
- });
-})
-
-describe('amount format', () => {
- const valids = [
- 'ARS:10',
- 'COL:10.2',
- 'UY:1,000.2',
- 'ARS:10.123,123',
- 'ARS:1,000,000',
- 'ARSCOL:10',
- 'THISISTHEMOTHERCOIN:1,000,000.123,123',
- ]
-
- test('should be valid', () => {
- valids.forEach(v => expect(v).toMatch(AMOUNT_REGEX))
- });
-
- const invalids = [
- //no currency name
- ':10',
- //use . instead of ,
- 'ARS:1.000.000',
- //currency name with numbers
- '1ARS:10',
- //currency name with numbers
- 'AR5:10',
- //missing value
- 'USD:',
- ]
-
- test('should not be valid', () => {
- invalids.forEach(v => expect(v).not.toMatch(AMOUNT_REGEX))
- });
-
-}) \ No newline at end of file
diff --git a/packages/merchant-backend-ui/tests/util.ts b/packages/merchant-backend-ui/tests/util.ts
deleted file mode 100644
index 14b82b51c..000000000
--- a/packages/merchant-backend-ui/tests/util.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-import * as axios from 'axios';
-
-type Query<Req, Res> = (GetQuery | PostQuery | DeleteQuery | PatchQuery) & RequestResponse<Req, Res>
-
-interface RequestResponse<Req, Res> {
- request?: Req,
- params?: any,
- response?: Res,
-}
-interface GetQuery { get: string }
-interface PostQuery { post: string }
-interface DeleteQuery { delete: string }
-interface PatchQuery { patch: string }
-
-export function simulateBackendResponse<R, T>(query: Query<R, T>): void {
- (axios.default as jest.MockedFunction<axios.AxiosStatic>).mockImplementationOnce(function (opt?: axios.AxiosRequestConfig): axios.AxiosPromise {
- // console.log(opt, JSON.stringify(query,undefined,2))
- expect(opt).toBeDefined();
- if (!opt)
- return Promise.reject();
-
- // expect(query.request).toStrictEqual(opt.data);
- // expect(query.params).toStrictEqual(opt.params);
- if ('get' in query) {
- expect(opt.method).toBe('get');
- expect(opt.url).toBe(query.get);
- }
- if ('post' in query) {
- expect(opt.method).toBe('post');
- expect(opt.url).toBe(query.post);
- }
- if ('delete' in query) {
- expect(opt.method).toBe('delete');
- expect(opt.url).toBe(query.delete);
- }
- if ('patch' in query) {
- expect(opt.method).toBe('patch');
- expect(opt.url).toBe(query.patch);
- }
- return ({ data: query.response, config: {} } as any);
- } as any)
-}
diff --git a/packages/merchant-backend-ui/trim-extension.cjs b/packages/merchant-backend-ui/trim-extension.cjs
new file mode 100644
index 000000000..00e8f9f01
--- /dev/null
+++ b/packages/merchant-backend-ui/trim-extension.cjs
@@ -0,0 +1,23 @@
+// Simple plugin to trim extensions from the filename of relative import statements.
+// Required to get linaria to work with `moduleResulution: "Node16"` imports.
+// @author Florian Dold
+module.exports = function({ types: t }) {
+ return {
+ name: "trim-extension",
+ visitor: {
+ ImportDeclaration: (x) => {
+ const src = x.node.source;
+ if (src.value.startsWith(".")) {
+ if (src.value.endsWith(".js")) {
+ const newVal = src.value.replace(/[.]js$/, "")
+ x.node.source = t.stringLiteral(newVal);
+ }
+ }
+ if (src.value.endsWith(".jsx")) {
+ const newVal = src.value.replace(/[.]jsx$/, "")
+ x.node.source = t.stringLiteral(newVal);
+ }
+ },
+ }
+ };
+}
diff --git a/packages/merchant-backend-ui/tsconfig.back.json b/packages/merchant-backend-ui/tsconfig.back.json
deleted file mode 100644
index 9ac5a3c25..000000000
--- a/packages/merchant-backend-ui/tsconfig.back.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
- "compilerOptions": {
- "composite": true,
- "lib": ["es6", "DOM"],
- "jsx": "react",
- "jsxFactory": "h",
- "jsxFragmentFactory": "Fragment",
- "moduleResolution": "Node",
- "module": "ESNext",
- "target": "ES6",
- "noImplicitAny": true,
- "noEmitOnError": true,
- "strict": true,
- "incremental": true,
- "sourceMap": true,
- "esModuleInterop": true,
- "importHelpers": true,
- "rootDir": "./src",
- "typeRoots": ["./node_modules/@types"]
- },
- "include": ["src/**/*"]
- }
- \ No newline at end of file
diff --git a/packages/merchant-backend-ui/tsconfig.json b/packages/merchant-backend-ui/tsconfig.json
index 7a4d70a17..d9cd57c4e 100644
--- a/packages/merchant-backend-ui/tsconfig.json
+++ b/packages/merchant-backend-ui/tsconfig.json
@@ -1,7 +1,7 @@
{
"compilerOptions": {
/* Basic Options */
- "target": "ES6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */
+ "target": "ES2020", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */
"module": "ESNext", /* Specify module code generation: 'none', commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
// "lib": [], /* Specify library files to be included in the compilation: */
"allowJs": true, /* Allow javascript files to be compiled. */
diff --git a/packages/merchant-backoffice-ui/.eslintrc.cjs b/packages/merchant-backoffice-ui/.eslintrc.cjs
new file mode 100644
index 000000000..05618b499
--- /dev/null
+++ b/packages/merchant-backoffice-ui/.eslintrc.cjs
@@ -0,0 +1,28 @@
+module.exports = {
+ extends: [
+ 'eslint:recommended',
+ 'plugin:@typescript-eslint/recommended',
+ 'plugin:react/recommended',
+ ],
+ parser: '@typescript-eslint/parser',
+ plugins: ['@typescript-eslint', 'header'],
+ root: true,
+ rules: {
+ "react/no-unknown-property": 0,
+ "react/no-unescaped-entities": 0,
+ "@typescript-eslint/no-namespace": 0,
+ "@typescript-eslint/no-unused-vars": [2,{argsIgnorePattern:"^_"}],
+ "header/header": [2,"copyleft-header.js"]
+ },
+ parserOptions: {
+ ecmaVersion: 6,
+ sourceType: 'module',
+ jsx: true,
+ },
+ settings: {
+ react: {
+ version: "18",
+ pragma: "h",
+ }
+ },
+};
diff --git a/packages/merchant-backoffice-ui/.storybook/main.js b/packages/merchant-backoffice-ui/.storybook/main.js
deleted file mode 100644
index f8e4bbcc7..000000000
--- a/packages/merchant-backoffice-ui/.storybook/main.js
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-
-module.exports = {
- "stories": [
- "../src/**/*.stories.mdx",
- "../src/**/*.stories.@(js|jsx|ts|tsx)"
- ],
- "addons": [
- "@storybook/preset-scss",
- "@storybook/addon-a11y",
- "@storybook/addon-essentials" //docs, control, actions, viewpot, toolbar, background
- ],
- // sb does not yet support new jsx transform by default
- // https://github.com/storybookjs/storybook/issues/12881
- // https://github.com/storybookjs/storybook/issues/12952
- babel: async (options) => ({
- ...options,
- presets: [
- ...options.presets,
- [
- '@babel/preset-react', {
- runtime: 'automatic',
- },
- 'preset-react-jsx-transform'
- ],
- ],
- }),
- webpackFinal: (config) => {
- // should be removed after storybook 6.3
- // https://github.com/storybookjs/storybook/issues/12853#issuecomment-821576113
- config.resolve.alias = {
- react: "preact/compat",
- "react-dom": "preact/compat",
- };
- return config;
- },
-} \ No newline at end of file
diff --git a/packages/merchant-backoffice-ui/.storybook/preview.js b/packages/merchant-backoffice-ui/.storybook/preview.js
deleted file mode 100644
index d13103ac9..000000000
--- a/packages/merchant-backoffice-ui/.storybook/preview.js
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- 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/>
- */
-
-import "../src/scss/main.scss"
-import { ConfigContextProvider } from '../src/context/config'
-import { InstanceContextProvider } from '../src/context/instance'
-import { TranslationProvider } from '../src/context/translation'
-import { BackendContextProvider } from '../src/context/backend'
-import { h } from 'preact';
-
-const mockConfig = {
- backendURL: 'http://demo.taler.net',
- currency: 'TESTKUDOS'
-}
-
-const mockInstance = {
- id: 'instance-id',
- token: 'instance-token',
- admin: false,
-}
-
-const mockBackend = {
- url: 'http://merchant.url',
- token: 'default-token',
- triedToLog: false,
-}
-
-export const parameters = {
- controls: { expanded: true },
- // actions: { argTypesRegex: "^on.*" },
-}
-
-export const globalTypes = {
- locale: {
- name: 'Locale',
- description: 'Internationalization locale',
- defaultValue: 'en',
- toolbar: {
- icon: 'globe',
- items: [
- { value: 'en', right: '🇺🇸', title: 'English' },
- { value: 'es', right: '🇪🇸', title: 'Spanish' },
- ],
- },
- },
-};
-
-export const decorators = [
- (Story, { globals }) => <TranslationProvider initial='en' forceLang={globals.locale}>
- <Story />
- </TranslationProvider>,
- (Story) => <ConfigContextProvider value={mockConfig}>
- <Story />
- </ConfigContextProvider>,
- (Story) => <InstanceContextProvider value={mockInstance}>
- <Story />
- </InstanceContextProvider>,
- (Story) => <BackendContextProvider defaultUrl={mockBackend.url}>
- <Story />
- </BackendContextProvider>,
-];
diff --git a/packages/merchant-backoffice-ui/DESIGN.md b/packages/merchant-backoffice-ui/DESIGN.md
new file mode 100644
index 000000000..d6252ccdc
--- /dev/null
+++ b/packages/merchant-backoffice-ui/DESIGN.md
@@ -0,0 +1,195 @@
+# Page internal routing
+
+* The SPA is loaded from the BACKOFFICE_URL
+
+* The view to be rendered is decided by the URL fragment
+
+* Query parameters that may affect routing
+
+ - instance: use from the default instance to mimic another instance management
+
+* The user must provide BACKEND_URL or BACKOFFICE_URL will use as default
+
+* Token for querying the backend will be saved in localStorage under
+ backend-token-${name}
+
+# HTTP queries to the backend
+
+HTTP queries will have 4 states:
+
+* loading: request did not end yet. data and error are undefined
+
+* ok: data has information, http response status == 200
+
+* clientError: http response status is between 400 and 499
+
+ - notfound: http status 404
+
+ - unauthorized: http status 401
+
+* serverError: http response status is grater than 500
+
+There are categories of queries:
+
+ * sync: getting information for the page rendering
+
+ * async: performing an CRUD operation
+
+## Loading the page information (sync)
+
+In this scenario, a failed request will make the app flow to break.
+
+When receiving an not found error a generic not found page will be shown. If the
+BACKEND_URL points to a default instance it should send the user to create the
+instance.
+
+When receiving an unauthorized error, the user should be prompted with a login form.
+
+When receiving an another error (400 < http status < 600), the login form should
+be shown with an error message using the hint from the backend.
+
+On other unexpected error (like network error), the login form should be shown
+with an error message.
+
+## CRUD operation (async)
+
+In this scenario, a failed request does not break the flow but a message will be
+prompted.
+
+# Forms
+
+All the input components should be placed in the folder `src/components/from`.
+
+The core concepts are:
+
+ * <FormProvider<T> /> places instead of <form /> it should be mapped to an
+ object of type T
+
+ * <Input /> an others: defines UI, create <input /> DOM controls and access the
+ form with useField()
+
+To use it you will need a state somewhere with the object holding all the form
+information.
+
+```
+const [state, setState] = useState({ name: '', age: 11 })
+```
+
+Optionally an error object an be built with the error messages
+
+```
+const errors = {
+ field1: undefined,
+ field2: 'should be greater than 18',
+}
+```
+
+These 3 elements are used to setup the FormProvider
+
+```
+<FormProvider errors={errors} object={state} valueHandler={setState}>
+...inputs
+</FormProvider>
+```
+
+Inputs should handle UI rendering and use `useField(name)` to get:
+
+ * error: the field has been modified and the value is not correct
+ * required: the field need to be corrected
+ * value: the current value of the object
+ * initial: original value before change
+ * onChange: function to update the current field
+
+Also, every input must be ready to receive these properties
+
+ * name: property of the form object being manipulated
+ * label: how the name of the property will be shown in the UI
+ * placeholder: optional, inplace text when there is no value yet
+ * readonly: default to false, will prevent change the value
+ * help: optional, example text below the input text to help the user
+ * tooltip: optional, will add a (i) with a popup to describe the field
+
+
+# Custom Hooks
+
+All the general purpose hooks should be placed in folder `src/hooks` and tests
+under `tests/hooks`. Starts with the `use` word.
+
+# Contexts
+
+All the contexts should be placed in the folder `src/context` as a function.
+Should expose provider as a component `<XxxContextProvider />` and consumer as a
+hook function `useXxxContext()` (where XXX is the name)
+
+# Components
+
+Type of components:
+
+ * main entry point: src/index.tsx, mostly initialization
+
+ * routing: in the `src` folder, deciding who is going to take the work. That's
+ when the page is loading but also create navigation handlers
+
+ * pages: in the `paths` folder, setup page information (like querying the
+ backend for the list of things), handlers for CRUD events, delegated routing
+ to parent and UI to children.
+
+Some other guidelines:
+
+ * Hooks over classes are preferred
+
+ * Components that are ready to be reused on any place should be in
+ `src/components` folder
+
+ * Since one of the build targets is a single bundle with all the pages, we are
+ avoiding route based code splitting
+ https://github.com/preactjs/preact-cli#route-based-code-splitting
+
+
+# Testing
+
+Every components should have examples using storybook (xxx.stories.tsx). There
+is an automated test that check that every example can be rendered so we make
+sure that we do not add a regression.
+
+Every hook should have examples under `tests/hooks` with common usage trying to
+follow this structure:
+
+ * (Given) set some context of the initial condition
+
+ * (When) some action to be tested. May be the initialization of a hook or an
+ action associated with it
+
+ * (Then) a particular set of observable consequences should be expected
+
+# Accessibility
+
+Pages and components should be built with accessibility in mind.
+
+https://github.com/nickcolley/jest-axe
+https://orkhanhuseyn.medium.com/accessibility-testing-in-react-with-jest-axe-e08c2a3f3289
+http://accesibilidadweb.dlsi.ua.es/?menu=jaws
+https://webaim.org/projects/screenreadersurvey8/#intro
+https://www.gov.uk/service-manual/technology/testing-with-assistive-technologies#how-to-test
+https://es.reactjs.org/docs/accessibility.html
+
+# Internationalization
+
+Every non translated message should be written in English and wrapped into:
+
+ * i18n function from useTranslator() hook
+ * <Translate /> component
+
+Makefile has a i18n that will parse source files and update the po template.
+When *.po are updated, running the i18n target will create the strings.ts that
+the application will use in runtime.
+
+# Documentation Conventions
+
+* labels
+ * begin w/ a capital letter
+ * acronyms (e.g., "URL") are upper case
+* tooltips
+ * begin w/ a lower case letter
+ * do not end w/ punctuation (period)
+ * avoid leading article ("a", "an", "the")
diff --git a/packages/merchant-backoffice-ui/Makefile b/packages/merchant-backoffice-ui/Makefile
index 2dfee7999..7175ef723 100644
--- a/packages/merchant-backoffice-ui/Makefile
+++ b/packages/merchant-backoffice-ui/Makefile
@@ -1,16 +1,35 @@
# This Makefile has been placed in the public domain
-# Settings from "./configure"
-include .config.mk
+ifeq ($(TOPLEVEL), yes)
+ $(info top-level build)
+ -include ../../.config.mk
+ override DESTDIR := $(TOP_DESTDIR)
+else
+ $(info package-level build)
+ -include ../../.config.mk
+ -include .config.mk
+endif
+$(info prefix is $(prefix))
+
+.PHONY: all
all:
@echo run \'make install\' to install
-spa_dir=$(prefix)/share/taler/merchant-backoffice
+spa_dir=$(DESTDIR)$(prefix)/share/taler/merchant-backoffice
-install:
+.PHONY: deps
+deps:
pnpm install --frozen-lockfile --filter @gnu-taler/merchant-backoffice...
pnpm run build
- install -d $(spa_dir)
- install ./dist/* $(spa_dir)
+
+.PHONY: install-nodeps
+install-nodeps:
+ (cd dist/prod && find . -type f -exec install -D "{}" "$(spa_dir)/{}" \;)
+
+
+.PHONY: install
+install:
+ $(MAKE) deps
+ $(MAKE) install-nodeps
diff --git a/packages/merchant-backoffice-ui/README.md b/packages/merchant-backoffice-ui/README.md
new file mode 100644
index 000000000..34bb98b67
--- /dev/null
+++ b/packages/merchant-backoffice-ui/README.md
@@ -0,0 +1,64 @@
+## Merchant Admin Frontend
+
+Merchant Admin Frontend is a Single Page Application (SPA) that connects with a running Merchant Backend and lets you manage instances, orders, products and tipping.
+
+## System requirements
+
+- Node: v16.15.0
+- pnpm: 7.14.2
+- make
+
+## Compiling from source
+
+Run `pnpm install --frozen-lockfile --filter @gnu-taler/merchant-backoffice...` to install all the nodejs dependencies.
+
+Then the command `pnpm build` create the distribution in the `dist` folder.
+
+By default the installation prefix will be `/usr/local/share/taler/merchant-backoffice/` but it can be overridden by `--prefix` in the configuration process:
+
+```shell
+./configure --prefix=/another/directory
+```
+
+To install run `make install`
+
+## Running develop
+
+To run a development server run:
+
+```shell
+./dev.mjs
+```
+
+This should start a watch process that will reload the server every time that a file is saved.
+
+The application need to connect to a merchant-backend properly configured to run.
+
+## Building for deploy
+
+To build and deploy the SPA in your local server run the install script:
+
+```shell
+make install
+```
+
+## Runtime dependencies
+
+* preact: Fast 3kB alternative to React with the same modern API
+
+* preact-router: URL component router for Preact
+
+* SWR: React Hooks library for data fetching (stale-while-revalidate)
+
+* Yup: schema builder for value parsing and validation (to be deprecated)
+
+* Date-fns: library for manipulating javascript date
+
+* qrcode-generator: simplest qr implementation based on JIS X 0510:1999
+
+* @gnu-taler/taler-util: types and tooling
+
+* history: manage the history stack, navigate, and persist state between sessions
+
+* jed: gettext like library for internationalization
+
diff --git a/packages/merchant-backoffice-ui/build.mjs b/packages/merchant-backoffice-ui/build.mjs
index c93b4eb67..b6d6e5127 100755
--- a/packages/merchant-backoffice-ui/build.mjs
+++ b/packages/merchant-backoffice-ui/build.mjs
@@ -1,7 +1,7 @@
#!/usr/bin/env node
/*
This file is part of GNU Taler
- (C) 2022 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
@@ -15,133 +15,14 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import esbuild from "esbuild";
-import path from "path";
-import fs from "fs";
-import sass from "sass";
+import { build } from "@gnu-taler/web-util/build";
-// eslint-disable-next-line no-undef
-const BASE = process.cwd();
-
-const preact = path.join(
- BASE,
- "node_modules",
- "preact",
- "compat",
- "dist",
- "compat.module.js",
-);
-
-const preactCompatPlugin = {
- name: "preact-compat",
- setup(build) {
- build.onResolve({ filter: /^(react-dom|react)$/ }, (args) => {
- //console.log("onresolve", JSON.stringify(args, undefined, 2));
- return {
- path: preact,
- };
- });
+await build({
+ type: "production",
+ source: {
+ js: ["src/index.tsx"],
+ assets: [{base:"src",files:["src/index.html"]}],
},
-};
-
-const entryPoints = ["src/index.tsx", "src/stories.tsx"];
-
-let GIT_ROOT = BASE;
-while (!fs.existsSync(path.join(GIT_ROOT, ".git")) && GIT_ROOT !== "/") {
- GIT_ROOT = path.join(GIT_ROOT, "../");
-}
-if (GIT_ROOT === "/") {
- // eslint-disable-next-line no-undef
- console.log("not found");
- // eslint-disable-next-line no-undef
- process.exit(1);
-}
-const GIT_HASH = GIT_ROOT === "/" ? undefined : git_hash();
-
-let _package = JSON.parse(fs.readFileSync(path.join(BASE, "package.json")));
-
-function git_hash() {
- const rev = fs
- .readFileSync(path.join(GIT_ROOT, ".git", "HEAD"))
- .toString()
- .trim()
- .split(/.*[: ]/)
- .slice(-1)[0];
- if (rev.indexOf("/") === -1) {
- return rev;
- } else {
- return fs.readFileSync(path.join(GIT_ROOT, ".git", rev)).toString().trim();
- }
-}
-
-// FIXME: Put this into some helper library.
-function copyFilesPlugin(options) {
- return {
- name: "copy-files",
- setup(build) {
- build.onEnd(() => {
- for (const fop of options) {
- fs.copyFileSync(fop.src, fop.dest);
- }
- });
- },
- };
-}
-
-const DEFAULT_SASS_FILTER = /\.(s[ac]ss|css)$/
-
-const buildSassPlugin = {
- name: "custom-build-sass",
- setup(build) {
-
- build.onLoad({ filter: DEFAULT_SASS_FILTER }, ({ path: file }) => {
- const resolveDir = path.dirname(file)
- const { css: contents } = sass.compile(file, { loadPaths: ["./"] })
-
- return {
- resolveDir,
- loader: 'css',
- contents
- }
- });
-
- },
-};
-
-export const buildConfig = {
- entryPoints: [...entryPoints],
- bundle: true,
- outdir: "dist",
- minify: false,
- loader: {
- ".svg": "file",
- ".png": "dataurl",
- ".jpeg": "dataurl",
- '.ttf': 'file',
- '.woff': 'file',
- '.woff2': 'file',
- '.eot': 'file',
- },
- target: ["es6"],
- format: "esm",
- platform: "browser",
- sourcemap: true,
- jsxFactory: "h",
- jsxFragment: "Fragment",
- define: {
- __VERSION__: `"${_package.version}"`,
- __GIT_HASH__: `"${GIT_HASH}"`,
- },
- plugins: [
- preactCompatPlugin,
- copyFilesPlugin([
- {
- src: "./src/index.html",
- dest: "./dist/index.html",
- },
- ]),
- buildSassPlugin
- ],
-};
-
-await esbuild.build(buildConfig)
+ destination: "./dist/prod",
+ css: "sass",
+});
diff --git a/packages/merchant-backoffice-ui/copyleft-header.js b/packages/merchant-backoffice-ui/copyleft-header.js
index 0794cb839..2589fdc92 100644
--- a/packages/merchant-backoffice-ui/copyleft-header.js
+++ b/packages/merchant-backoffice-ui/copyleft-header.js
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2021-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
diff --git a/packages/merchant-backoffice-ui/dev.mjs b/packages/merchant-backoffice-ui/dev.mjs
index 35a9fa16c..6cc99add0 100755
--- a/packages/merchant-backoffice-ui/dev.mjs
+++ b/packages/merchant-backoffice-ui/dev.mjs
@@ -1,7 +1,7 @@
#!/usr/bin/env node
/*
This file is part of GNU Taler
- (C) 2022 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
@@ -15,16 +15,26 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { serve } from "@gnu-taler/web-util/lib/index.node";
-import esbuild from "esbuild";
-import { buildConfig } from "./build.mjs";
+import { serve } from "@gnu-taler/web-util/node";
+import { initializeDev } from "@gnu-taler/web-util/build";
-buildConfig.inject = ['./node_modules/@gnu-taler/web-util/lib/live-reload.mjs']
+const devEntryPoints = ["src/stories.tsx", "src/index.tsx"];
+
+const build = initializeDev({
+ type: "development",
+ source: {
+ js: devEntryPoints,
+ assets: [{base:"src",files:["src/index.html"]}],
+ },
+ css: "sass",
+ destination: "./dist/dev",
+});
+
+await build();
serve({
- folder: './dist',
+ folder: "./dist/dev",
port: 8080,
- source: './src',
- development: true,
- onUpdate: async () => esbuild.build(buildConfig)
-})
+ source: "./src",
+ onSourceUpdate: build,
+});
diff --git a/packages/merchant-backoffice-ui/package.json b/packages/merchant-backoffice-ui/package.json
index 91c4c1857..e80604777 100644
--- a/packages/merchant-backoffice-ui/package.json
+++ b/packages/merchant-backoffice-ui/package.json
@@ -1,81 +1,48 @@
{
"private": true,
- "name": "@gnu-taler/merchant-backoffice",
- "version": "0.0.4",
- "license": "MIT",
+ "name": "@gnu-taler/merchant-backoffice-ui",
+ "version": "0.10.7",
+ "license": "AGPL-3.0-or-later",
+ "type": "module",
"scripts": {
- "build": "preact build --no-sw --no-esm",
- "compile": "tsc",
- "serve": "sirv build --port ${PORT:=8080} --cors --single",
+ "clean": "rm -rf dist tsconfig.tsbuildinfo",
+ "build": "./build.mjs",
+ "check": "tsc",
+ "compile": "tsc && ./build.mjs",
"dev": "preact watch --port ${PORT:=8080} --no-sw --no-esm",
- "lint-check": "eslint '{src,tests}/**/*.{js,jsx,ts,tsx}'",
- "lint-fix": "eslint --fix '{src,tests}/**/*.{js,jsx,ts,tsx}'",
- "test": "jest ./tests",
- "dev-test": "jest ./tests --watch",
- "typedoc": "typedoc src",
- "clean": "rimraf build storybook-static docs single",
- "build-single": "preact build --no-sw --no-esm -c preact.single-config.js --dest single && sh remove-link-stylesheet.sh",
- "serve-single": "sirv single --port ${PORT:=8080} --cors --single",
- "build-storybook": "build-storybook",
- "storybook": "start-storybook -p 6006"
- },
- "engines": {
- "node": ">=12",
- "pnpm": ">=5"
- },
- "eslintConfig": {
- "parser": "@typescript-eslint/parser",
- "extends": [
- "preact",
- "plugin:@typescript-eslint/recommended"
- ],
- "plugins": [
- "header"
- ],
- "rules": {
- "header/header": [
- 2,
- "copyleft-header.js"
- ]
- },
- "ignorePatterns": [
- "build/"
- ]
+ "test": "./test.mjs && mocha --require source-map-support/register 'dist/**/*.test.js' 'dist/**/test.js'",
+ "lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
+ "i18n:extract": "pogen extract",
+ "i18n:merge": "pogen merge",
+ "i18n:emit": "pogen emit",
+ "i18n": "pnpm i18n:extract && pnpm i18n:merge && pnpm i18n:emit",
+ "typedoc": "typedoc --out dist/typedoc ./src/",
+ "pretty": "prettier --write src"
},
"dependencies": {
"@gnu-taler/taler-util": "workspace:*",
"@gnu-taler/web-util": "workspace:*",
- "axios": "^0.21.1",
"date-fns": "2.29.3",
"history": "4.10.1",
"jed": "1.1.1",
- "preact": "10.6.5",
+ "preact": "10.11.3",
"preact-router": "3.2.1",
"qrcode-generator": "1.4.4",
- "swr": "1.3.0",
- "react": "npm:@preact/compat@^17.1.2",
+ "swr": "2.2.2",
"yup": "^0.32.9"
},
"devDependencies": {
- "@babel/core": "7.18.9",
- "@babel/plugin-transform-react-jsx-source": "7.18.6",
+ "eslint": "^8.56.0",
+ "@typescript-eslint/eslint-plugin": "^6.19.0",
+ "@typescript-eslint/parser": "^6.19.0",
+ "eslint-config-prettier": "^9.1.0",
+ "eslint-plugin-react": "^7.33.2",
"@creativebulma/bulma-tooltip": "^1.2.0",
- "@gnu-taler/pogen": "^0.0.5",
- "@storybook/addon-a11y": "^6.2.9",
- "@storybook/addon-actions": "^6.2.9",
- "@storybook/addon-essentials": "^6.2.9",
- "@storybook/addon-links": "^6.2.9",
- "@storybook/preact": "^6.2.9",
- "@storybook/preset-scss": "^1.0.3",
- "@testing-library/preact": "^2.0.1",
- "@testing-library/preact-hooks": "^1.1.0",
+ "@gnu-taler/pogen": "workspace:*",
+ "@types/chai": "^4.3.0",
"@types/history": "^4.7.8",
- "@types/jest": "^26.0.23",
- "@types/mocha": "^8.2.2",
- "@types/node": "^18.8.5",
- "@typescript-eslint/eslint-plugin": "^4.22.0",
- "@typescript-eslint/parser": "^4.22.0",
- "babel-loader": "^8.2.2",
+ "@types/mocha": "^8.2.3",
+ "@types/node": "^18.11.17",
"base64-inline-loader": "^1.1.1",
"bulma": "^0.9.2",
"bulma-checkbox": "^1.1.1",
@@ -84,43 +51,20 @@
"bulma-switch-control": "^1.1.1",
"bulma-timeline": "^3.0.4",
"bulma-upload-control": "^1.2.0",
+ "chai": "^4.3.6",
"dotenv": "^8.2.0",
- "eslint": "^7.25.0",
- "eslint-config-preact": "^1.1.4",
- "eslint-plugin-header": "^3.1.1",
"html-webpack-inline-chunk-plugin": "^1.1.1",
"html-webpack-inline-source-plugin": "0.0.10",
"html-webpack-skip-assets-plugin": "^1.0.1",
"inline-chunk-html-plugin": "^1.1.1",
- "jest": "^26.6.3",
- "jest-preset-preact": "^4.0.2",
- "po2json": "^0.4.5",
- "preact-cli": "^3.0.5",
- "preact-render-to-json": "^3.6.6",
- "preact-render-to-string": "^5.1.19",
- "rimraf": "^3.0.2",
- "sass": "^1.32.13",
- "sass-loader": "10.1.1",
- "script-ext-html-webpack-plugin": "^2.1.5",
- "sirv-cli": "^1.0.11",
- "typedoc": "^0.20.36",
- "typescript": "4.4.4"
+ "mocha": "^9.2.0",
+ "preact-render-to-string": "^5.2.6",
+ "sass": "1.56.1",
+ "source-map-support": "^0.5.21",
+ "typedoc": "^0.25.4",
+ "typescript": "5.3.3"
},
- "jest": {
- "preset": "jest-preset-preact",
- "transformIgnorePatterns": [
- "node_modules/.pnpm/(?!(@gnu-taler\\+taler-util))",
- "\\.pnp\\.[^\\/]+$"
- ],
- "setupFiles": [
- "<rootDir>/tests/__mocks__/browserMocks.ts",
- "<rootDir>/tests/__mocks__/setupTests.ts"
- ],
- "moduleNameMapper": {
- "\\.(css|less)$": "identity-obj-proxy"
- },
- "transform": {
- "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga|po)$": "<rootDir>/tests/__mocks__/fileTransformer.js"
- }
+ "pogen": {
+ "domain": "taler-merchant-backoffice"
}
-} \ No newline at end of file
+}
diff --git a/packages/merchant-backoffice-ui/preact.config.js b/packages/merchant-backoffice-ui/preact.config.js
index 8e640f3ff..b20017a0c 100644
--- a/packages/merchant-backoffice-ui/preact.config.js
+++ b/packages/merchant-backoffice-ui/preact.config.js
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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/merchant-backoffice-ui/preact.single-config.js b/packages/merchant-backoffice-ui/preact.single-config.js
index 61a79bb8a..d3640a5a6 100644
--- a/packages/merchant-backoffice-ui/preact.single-config.js
+++ b/packages/merchant-backoffice-ui/preact.single-config.js
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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/merchant-backoffice-ui/src/AdminRoutes.tsx b/packages/merchant-backoffice-ui/src/AdminRoutes.tsx
index 9caf8ea7a..b186f1408 100644
--- a/packages/merchant-backoffice-ui/src/AdminRoutes.tsx
+++ b/packages/merchant-backoffice-ui/src/AdminRoutes.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2021-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -14,45 +14,39 @@
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 { Router, route, Route } from "preact-router";
import InstanceCreatePage from "./paths/admin/create/index.js";
-import InstanceListPage from './paths/admin/list/index.js';
-
+import InstanceListPage from "./paths/admin/list/index.js";
+import { InstancePaths } from "./Routing.js";
export enum AdminPaths {
- list_instances = '/instances',
- new_instance = '/instance/new',
+ 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>
-} \ No newline at end of file
+ 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(InstancePaths.bank_list);
+ }}
+
+ />
+ </Router>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/Application.tsx b/packages/merchant-backoffice-ui/src/Application.tsx
new file mode 100644
index 000000000..097e98567
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/Application.tsx
@@ -0,0 +1,364 @@
+/*
+ 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 {
+ CacheEvictor,
+ TalerMerchantApi,
+ TalerMerchantInstanceCacheEviction,
+ TalerMerchantManagementCacheEviction,
+ assertUnreachable,
+ canonicalizeBaseUrl,
+} from "@gnu-taler/taler-util";
+import {
+ BrowserHashNavigationProvider,
+ ConfigResultFail,
+ MerchantApiProvider,
+ TalerWalletIntegrationBrowserProvider,
+ TranslationProvider,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
+import { useEffect, useState } from "preact/hooks";
+import { SWRConfig } from "swr";
+import { Routing } from "./Routing.js";
+import { Loading } from "./components/exception/loading.js";
+import { NotificationCard } from "./components/menu/index.js";
+import { SettingsProvider } from "./context/settings.js";
+import {
+ revalidateBankAccountDetails,
+ revalidateInstanceBankAccounts,
+} from "./hooks/bank.js";
+import {
+ revalidateBackendInstances,
+ revalidateInstanceDetails,
+ revalidateManagedInstanceDetails,
+} from "./hooks/instance.js";
+import {
+ revalidateInstanceOtpDevices,
+ revalidateOtpDeviceDetails,
+} from "./hooks/otp.js";
+import {
+ revalidateInstanceProducts,
+ revalidateProductDetails,
+} from "./hooks/product.js";
+import {
+ revalidateInstanceTemplates,
+ revalidateTemplateDetails,
+} from "./hooks/templates.js";
+import { revalidateInstanceTransfers } from "./hooks/transfer.js";
+import {
+ revalidateInstanceWebhooks,
+ revalidateWebhookDetails,
+} from "./hooks/webhooks.js";
+import { strings } from "./i18n/strings.js";
+import {
+ MerchantUiSettings,
+ buildDefaultBackendBaseURL,
+ fetchSettings,
+} from "./settings.js";
+import {
+ revalidateInstanceOrders,
+ revalidateOrderDetails,
+} from "./hooks/order.js";
+import { SessionContextProvider } from "./context/session.js";
+const WITH_LOCAL_STORAGE_CACHE = false;
+
+export function Application(): VNode {
+ const [settings, setSettings] = useState<MerchantUiSettings>();
+ useEffect(() => {
+ fetchSettings(setSettings);
+ }, []);
+ if (!settings) return <Loading />;
+
+ const baseUrl = getInitialBackendBaseURL(settings.backendBaseURL);
+ return (
+ <SettingsProvider value={settings}>
+ <TranslationProvider
+ source={strings}
+ completeness={{
+ es: strings["es"].completeness,
+ de: strings["de"].completeness,
+ }}
+ >
+ <MerchantApiProvider
+ baseUrl={new URL("./", baseUrl)}
+ frameOnError={OnConfigError}
+ evictors={{
+ management: swrCacheEvictor,
+ }}
+ >
+ <SessionContextProvider>
+ <SWRConfig
+ value={{
+ provider: WITH_LOCAL_STORAGE_CACHE
+ ? localStorageProvider
+ : undefined,
+ // normally, do not revalidate
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ revalidateIfStale: false,
+ revalidateOnMount: undefined,
+ focusThrottleInterval: undefined,
+
+ // normally, do not refresh
+ refreshInterval: undefined,
+ dedupingInterval: 2000,
+ refreshWhenHidden: false,
+ refreshWhenOffline: false,
+
+ // ignore errors
+ shouldRetryOnError: false,
+ errorRetryCount: 0,
+ errorRetryInterval: undefined,
+
+ // do not go to loading again if already has data
+ keepPreviousData: true,
+ }}
+ >
+ <TalerWalletIntegrationBrowserProvider>
+ <BrowserHashNavigationProvider>
+ <Routing />
+ </BrowserHashNavigationProvider>
+ </TalerWalletIntegrationBrowserProvider>
+ </SWRConfig>
+ </SessionContextProvider>
+ </MerchantApiProvider>
+ </TranslationProvider>
+ </SettingsProvider>
+ );
+}
+
+function getInitialBackendBaseURL(
+ backendFromSettings: string | undefined,
+): string {
+ /**
+ * For testing purpose
+ */
+ const overrideUrl =
+ typeof localStorage !== "undefined"
+ ? localStorage.getItem("merchant-base-url")
+ : undefined;
+ let result: string;
+
+ if (overrideUrl) {
+ // testing/development path
+ result = overrideUrl;
+ } else {
+ // normal path
+ if (!backendFromSettings) {
+ console.error(
+ "ERROR: backendBaseURL was overridden by a setting file and missing. Setting value to 'window.origin'",
+ );
+ result = buildDefaultBackendBaseURL();
+ } else {
+ result = backendFromSettings;
+ }
+ }
+ try {
+ return canonicalizeBaseUrl(result);
+ } catch (e) {
+ // fall back
+ return canonicalizeBaseUrl(window.origin);
+ }
+}
+
+function localStorageProvider(): Map<unknown, unknown> {
+ const map = new Map(JSON.parse(localStorage.getItem("app-cache") || "[]"));
+
+ window.addEventListener("beforeunload", () => {
+ const appCache = JSON.stringify(Array.from(map.entries()));
+ localStorage.setItem("app-cache", appCache);
+ });
+ return map;
+}
+
+function OnConfigError({
+ state,
+}: {
+ state: ConfigResultFail<TalerMerchantApi.VersionResponse> | undefined;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ if (!state) {
+ return (
+ <i18n.Translate>checking compatibility with server...</i18n.Translate>
+ );
+ }
+ switch (state.type) {
+ case "error": {
+ return (
+ <NotificationCard
+ notification={{
+ message: i18n.str`Contacting the server failed`,
+ description: state.error.message,
+ details: JSON.stringify(state.error.errorDetail, undefined, 2),
+ type: "ERROR",
+ }}
+ />
+ );
+ }
+ case "incompatible": {
+ return (
+ <NotificationCard
+ notification={{
+ message: i18n.str`The server version is not supported`,
+ description: i18n.str`Supported version "${state.supported}", server version "${state.result.version}".`,
+ type: "WARN",
+ }}
+ />
+ );
+ }
+ default:
+ assertUnreachable(state);
+ }
+}
+
+const swrCacheEvictor = new (class
+ implements
+ CacheEvictor<
+ TalerMerchantManagementCacheEviction | TalerMerchantInstanceCacheEviction
+ >
+{
+ async notifySuccess(
+ op:
+ | TalerMerchantManagementCacheEviction
+ | TalerMerchantInstanceCacheEviction,
+ ) {
+ switch (op) {
+ case TalerMerchantManagementCacheEviction.CREATE_INSTANCE: {
+ await Promise.all([revalidateBackendInstances()]);
+ return;
+ }
+ case TalerMerchantManagementCacheEviction.UPDATE_INSTANCE: {
+ await Promise.all([revalidateManagedInstanceDetails()]);
+ return;
+ }
+ case TalerMerchantManagementCacheEviction.DELETE_INSTANCE: {
+ await Promise.all([revalidateBackendInstances()]);
+ return;
+ }
+ case TalerMerchantInstanceCacheEviction.UPDATE_CURRENT_INSTANCE: {
+ await Promise.all([revalidateInstanceDetails()]);
+ return;
+ }
+ case TalerMerchantInstanceCacheEviction.DELETE_CURRENT_INSTANCE: {
+ await Promise.all([revalidateInstanceDetails()]);
+ return;
+ }
+ case TalerMerchantInstanceCacheEviction.CREATE_BANK_ACCOUNT: {
+ await Promise.all([revalidateInstanceBankAccounts()]);
+ return;
+ }
+ case TalerMerchantInstanceCacheEviction.UPDATE_BANK_ACCOUNT: {
+ await Promise.all([revalidateBankAccountDetails()]);
+ return;
+ }
+ case TalerMerchantInstanceCacheEviction.DELETE_BANK_ACCOUNT: {
+ await Promise.all([revalidateInstanceBankAccounts()]);
+ return;
+ }
+ case TalerMerchantInstanceCacheEviction.CREATE_PRODUCT: {
+ await Promise.all([revalidateInstanceProducts()]);
+ return;
+ }
+ case TalerMerchantInstanceCacheEviction.UPDATE_PRODUCT: {
+ await Promise.all([revalidateProductDetails()]);
+ return;
+ }
+ case TalerMerchantInstanceCacheEviction.DELETE_PRODUCT: {
+ await Promise.all([revalidateInstanceProducts()]);
+ return;
+ }
+ case TalerMerchantInstanceCacheEviction.CREATE_TRANSFER: {
+ await Promise.all([revalidateInstanceTransfers()]);
+ return;
+ }
+ case TalerMerchantInstanceCacheEviction.DELETE_TRANSFER: {
+ await Promise.all([revalidateInstanceTransfers()]);
+ return;
+ }
+ case TalerMerchantInstanceCacheEviction.CREATE_DEVICE: {
+ await Promise.all([revalidateInstanceOtpDevices()]);
+ return;
+ }
+ case TalerMerchantInstanceCacheEviction.UPDATE_DEVICE: {
+ await Promise.all([revalidateOtpDeviceDetails()]);
+ return;
+ }
+ case TalerMerchantInstanceCacheEviction.DELETE_DEVICE: {
+ await Promise.all([revalidateInstanceOtpDevices()]);
+ return;
+ }
+ case TalerMerchantInstanceCacheEviction.CREATE_TEMPLATE: {
+ await Promise.all([revalidateInstanceTemplates()]);
+ return;
+ }
+ case TalerMerchantInstanceCacheEviction.UPDATE_TEMPLATE: {
+ await Promise.all([revalidateTemplateDetails()]);
+ return;
+ }
+ case TalerMerchantInstanceCacheEviction.DELETE_TEMPLATE: {
+ await Promise.all([revalidateInstanceTemplates()]);
+ return;
+ }
+ case TalerMerchantInstanceCacheEviction.CREATE_WEBHOOK: {
+ await Promise.all([revalidateInstanceWebhooks()]);
+ return;
+ }
+ case TalerMerchantInstanceCacheEviction.UPDATE_WEBHOOK: {
+ await Promise.all([revalidateWebhookDetails()]);
+ return;
+ }
+ case TalerMerchantInstanceCacheEviction.DELETE_WEBHOOK: {
+ await Promise.all([revalidateInstanceWebhooks()]);
+ return;
+ }
+ case TalerMerchantInstanceCacheEviction.CREATE_ORDER: {
+ await Promise.all([revalidateInstanceOrders()]);
+ return;
+ }
+ case TalerMerchantInstanceCacheEviction.UPDATE_ORDER: {
+ await Promise.all([revalidateOrderDetails()]);
+ return;
+ }
+ case TalerMerchantInstanceCacheEviction.DELETE_ORDER: {
+ await Promise.all([revalidateInstanceOrders()]);
+ return;
+ }
+ case TalerMerchantInstanceCacheEviction.LAST:
+ // case TalerMerchantInstanceCacheEviction.CREATE_TOKENFAMILY:{
+ // await Promise.all([
+ // reva
+ // ])
+ // return
+ // }
+ // case TalerMerchantInstanceCacheEviction.UPDATE_TOKENFAMILY:{
+ // await Promise.all([
+ // ])
+ // return
+ // }
+ // case TalerMerchantInstanceCacheEviction.DELETE_TOKENFAMILY:{
+ // await Promise.all([
+ // ])
+ // return
+ // }
+ }
+ }
+})();
diff --git a/packages/merchant-backoffice-ui/src/ApplicationReadyRoutes.tsx b/packages/merchant-backoffice-ui/src/ApplicationReadyRoutes.tsx
deleted file mode 100644
index 27a1406a0..000000000
--- a/packages/merchant-backoffice-ui/src/ApplicationReadyRoutes.tsx
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-import { Fragment, h, VNode } from "preact";
-import Router, { Route, route } from "preact-router";
-import { useBackendContext } from "./context/backend.js";
-import { useBackendInstancesTestForAdmin } from "./hooks/backend.js";
-import { InstanceRoutes } from "./InstanceRoutes.js";
-import LoginPage from "./paths/login/index.js";
-import { INSTANCE_ID_LOOKUP } from "./utils/constants.js";
-import { NotYetReadyAppMenu, NotificationCard } from "./components/menu/index.js";
-import { useTranslator } from "./i18n/index.js";
-import { createHashHistory } from "history";
-import { useState } from "preact/hooks";
-
-export function ApplicationReadyRoutes(): VNode {
- const i18n = useTranslator();
- const {
- url: backendURL,
- updateLoginStatus,
- clearAllTokens,
- } = useBackendContext();
-
- const result = useBackendInstancesTestForAdmin();
-
- const clearTokenAndGoToRoot = () => {
- clearAllTokens();
- route("/");
- };
-
- if (result.clientError && result.isUnauthorized) {
- return (
- <Fragment>
- <NotYetReadyAppMenu title="Login" onLogout={clearTokenAndGoToRoot} />
- <NotificationCard
- notification={{
- message: i18n`Access denied`,
- description: i18n`Check your token is valid`,
- type: "ERROR",
- }}
- />
- <LoginPage onConfirm={updateLoginStatus} />
- </Fragment>
- );
- }
-
- if (result.loading) return <NotYetReadyAppMenu title="Loading..." />;
-
- let admin = true;
- let instanceNameByBackendURL;
-
- if (!result.ok) {
- 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 title="Error" onLogout={clearTokenAndGoToRoot} />
- <NotificationCard
- notification={{
- message: i18n`Couldn't access the server.`,
- description: i18n`Could not infer instance id from url ${backendURL}`,
- type: "ERROR",
- }}
- />
- <LoginPage onConfirm={updateLoginStatus} />
- </Fragment>
- );
- }
-
- admin = false;
- instanceNameByBackendURL = match[1];
- }
-
- const history = createHashHistory();
- return (
- <Router history={history}>
- <Route
- default
- component={DefaultMainRoute}
- admin={admin}
- instanceNameByBackendURL={instanceNameByBackendURL}
- />
- </Router>
- );
-}
-
-function DefaultMainRoute({ instance, admin, instanceNameByBackendURL }: any) {
- const [instanceName, setInstanceName] = useState(
- instanceNameByBackendURL || instance || "default"
- );
-
- return (
- <InstanceRoutes
- admin={admin}
- id={instanceName}
- setInstanceName={setInstanceName}
- />
- );
-}
diff --git a/packages/merchant-backoffice-ui/src/Routing.tsx b/packages/merchant-backoffice-ui/src/Routing.tsx
new file mode 100644
index 000000000..665137415
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/Routing.tsx
@@ -0,0 +1,754 @@
+/*
+ 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 {
+ AbsoluteTime,
+ TalerError,
+ TranslatedString,
+} from "@gnu-taler/taler-util";
+import { urlPattern, 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 { Loading } from "./components/exception/loading.js";
+import {
+ Menu,
+ NotConnectedAppMenu,
+ NotificationCard,
+} from "./components/menu/index.js";
+import { useSessionContext } from "./context/session.js";
+import { useInstanceBankAccounts } from "./hooks/bank.js";
+import { useInstanceKYCDetails } from "./hooks/instance.js";
+import { usePreference } from "./hooks/preference.js";
+import InstanceCreatePage from "./paths/admin/create/index.js";
+import InstanceListPage from "./paths/admin/list/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 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 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 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 TemplateCreatePage from "./paths/instance/templates/create/index.js";
+import TemplateListPage from "./paths/instance/templates/list/index.js";
+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 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 WebhookCreatePage from "./paths/instance/webhooks/create/index.js";
+import WebhookListPage from "./paths/instance/webhooks/list/index.js";
+import WebhookUpdatePage from "./paths/instance/webhooks/update/index.js";
+import { LoginPage } from "./paths/login/index.js";
+import { NotFoundPage } from "./paths/notfound/index.js";
+import { Settings } from "./paths/settings/index.js";
+import { Notification } from "./utils/types.js";
+
+export enum InstancePaths {
+ error = "/error",
+ settings = "/settings",
+ token = "/token",
+
+ bank_list = "/bank",
+ bank_update = "/bank/:bid/update",
+ bank_new = "/bank/new",
+
+ inventory_list = "/inventory",
+ inventory_update = "/inventory/:pid/update",
+ inventory_new = "/inventory/new",
+
+ order_list = "/orders",
+ 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",
+ transfers_new = "/transfer/new",
+
+ templates_list = "/templates",
+ templates_update = "/templates/:tid/update",
+ templates_new = "/templates/new",
+ templates_use = "/templates/:tid/use",
+ templates_qr = "/templates/:tid/qr",
+
+ webhooks_list = "/webhooks",
+ webhooks_update = "/webhooks/:tid/update",
+ webhooks_new = "/webhooks/new",
+
+ otp_devices_list = "/otp-devices",
+ otp_devices_update = "/otp-devices/:vid/update",
+ otp_devices_new = "/otp-devices/new",
+
+ interface = "/interface",
+}
+
+// eslint-disable-next-line @typescript-eslint/no-empty-function
+// const noop = () => { };
+
+export enum AdminPaths {
+ list_instances = "/instances",
+ new_instance = "/instance/new",
+ update_instance = "/instance/:id/update",
+}
+
+export interface Props {}
+
+export const privatePages = {
+ home: urlPattern(/\/home/, () => "#/home"),
+ go: urlPattern(/\/home/, () => "#/home"),
+};
+export const publicPages = {
+ home: urlPattern(/\/home/, () => "#/home"),
+ go: urlPattern(/\/home/, () => "#/home"),
+};
+
+const history = createHashHistory();
+export function Routing(_p: Props): VNode {
+ // const { i18n } = useTranslationContext();
+ const { state } = useSessionContext();
+
+ type GlobalNotifState =
+ | (Notification & { to: string | undefined })
+ | undefined;
+ const [globalNotification, setGlobalNotification] =
+ useState<GlobalNotifState>(undefined);
+
+ const [error] = useErrorBoundary();
+ const [preference] = usePreference();
+
+ const now = AbsoluteTime.now();
+
+ const instance = useInstanceBankAccounts();
+ const accounts =
+ !instance || instance instanceof TalerError || instance.type === "fail"
+ ? undefined
+ : instance.body;
+ const shouldWarnAboutMissingBankAccounts =
+ !state.isAdmin &&
+ accounts !== undefined &&
+ accounts.accounts.length < 1 &&
+ (AbsoluteTime.isNever(preference.hideMissingAccountUntil) ||
+ AbsoluteTime.cmp(now, preference.hideMissingAccountUntil) > 1);
+
+ 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>
+ <NotConnectedAppMenu title="Welcome!" />
+ <LoginPage />
+ </Fragment>
+ );
+ }
+
+ if (shouldWarnAboutMissingBankAccounts) {
+ return (
+ <Fragment>
+ <Menu />
+ <BankAccountBanner />
+ <BankAccountCreatePage
+ onConfirm={() => {
+ route(InstancePaths.bank_list);
+ }}
+ />
+ </Fragment>
+ );
+ }
+
+ return (
+ <Fragment>
+ <Menu />
+ <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>
+ ),
+ }}
+ />
+ )}
+
+ <Router
+ history={history}
+ onChange={(e) => {
+ const movingOutFromNotification =
+ globalNotification && e.url !== globalNotification.to;
+ if (movingOutFromNotification) {
+ setGlobalNotification(undefined);
+ }
+ }}
+ >
+ <Route path="/" component={Redirect} to={InstancePaths.order_list} />
+ {/**
+ * Admin pages
+ */}
+ {state.isAdmin && (
+ <Route
+ path={AdminPaths.list_instances}
+ component={InstanceListPage}
+ onCreate={() => {
+ route(AdminPaths.new_instance);
+ }}
+ onUpdate={(id: string): void => {
+ route(`/instance/${id}/update`);
+ }}
+ />
+ )}
+ {state.isAdmin && (
+ <Route
+ path={AdminPaths.new_instance}
+ component={InstanceCreatePage}
+ onBack={() => route(AdminPaths.list_instances)}
+ onConfirm={() => {
+ route(AdminPaths.list_instances);
+ }}
+ />
+ )}
+ {state.isAdmin && (
+ <Route
+ path={AdminPaths.update_instance}
+ component={AdminInstanceUpdatePage}
+ onBack={() => route(AdminPaths.list_instances)}
+ onConfirm={() => {
+ route(AdminPaths.list_instances);
+ }}
+ />
+ )}
+ {/**
+ * Update instance page
+ */}
+ <Route
+ path={InstancePaths.settings}
+ component={InstanceUpdatePage}
+ onBack={() => {
+ route(`/`);
+ }}
+ onConfirm={() => {
+ route(`/`);
+ }}
+ />
+ {/**
+ * Update instance page
+ */}
+ <Route
+ path={InstancePaths.token}
+ component={TokenPage}
+ onChange={() => {
+ route(`/`);
+ }}
+ onCancel={() => {
+ route(InstancePaths.order_list);
+ }}
+ />
+ {/**
+ * Inventory pages
+ */}
+ <Route
+ path={InstancePaths.inventory_list}
+ component={ProductListPage}
+ onCreate={() => {
+ route(InstancePaths.inventory_new);
+ }}
+ onSelect={(id: string) => {
+ route(InstancePaths.inventory_update.replace(":pid", id));
+ }}
+ />
+ <Route
+ path={InstancePaths.inventory_update}
+ component={ProductUpdatePage}
+ onConfirm={() => {
+ route(InstancePaths.inventory_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.inventory_list);
+ }}
+ />
+ <Route
+ path={InstancePaths.inventory_new}
+ component={ProductCreatePage}
+ onConfirm={() => {
+ route(InstancePaths.inventory_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.inventory_list);
+ }}
+ />
+ {/**
+ * Bank pages
+ */}
+ <Route
+ path={InstancePaths.bank_list}
+ component={BankAccountListPage}
+ onCreate={() => {
+ route(InstancePaths.bank_new);
+ }}
+ onSelect={(id: string) => {
+ route(InstancePaths.bank_update.replace(":bid", id));
+ }}
+ />
+ <Route
+ path={InstancePaths.bank_update}
+ component={BankAccountUpdatePage}
+ onConfirm={() => {
+ route(InstancePaths.bank_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.bank_list);
+ }}
+ />
+ <Route
+ path={InstancePaths.bank_new}
+ component={BankAccountCreatePage}
+ onConfirm={() => {
+ route(InstancePaths.bank_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.bank_list);
+ }}
+ />
+ {/**
+ * Order pages
+ */}
+ <Route
+ path={InstancePaths.order_list}
+ component={OrderListPage}
+ onCreate={() => {
+ route(InstancePaths.order_new);
+ }}
+ onSelect={(id: string) => {
+ route(InstancePaths.order_details.replace(":oid", id));
+ }}
+ />
+ <Route
+ path={InstancePaths.order_details}
+ component={OrderDetailsPage}
+ onBack={() => {
+ route(InstancePaths.order_list);
+ }}
+ />
+ <Route
+ path={InstancePaths.order_new}
+ component={OrderCreatePage}
+ onConfirm={(orderId: string) => {
+ route(InstancePaths.order_details.replace(":oid", orderId));
+ }}
+ onBack={() => {
+ route(InstancePaths.order_list);
+ }}
+ />
+ {/**
+ * Transfer pages
+ */}
+ <Route
+ path={InstancePaths.transfers_list}
+ component={TransferListPage}
+ onCreate={() => {
+ route(InstancePaths.transfers_new);
+ }}
+ />
+ <Route
+ path={InstancePaths.transfers_new}
+ component={TransferCreatePage}
+ onConfirm={() => {
+ route(InstancePaths.transfers_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.transfers_list);
+ }}
+ />
+ {/**
+ * Webhooks pages
+ */}
+ <Route
+ path={InstancePaths.webhooks_list}
+ component={WebhookListPage}
+ onCreate={() => {
+ route(InstancePaths.webhooks_new);
+ }}
+ onSelect={(id: string) => {
+ route(InstancePaths.webhooks_update.replace(":tid", id));
+ }}
+ />
+ <Route
+ path={InstancePaths.webhooks_update}
+ component={WebhookUpdatePage}
+ onConfirm={() => {
+ route(InstancePaths.webhooks_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.webhooks_list);
+ }}
+ />
+ <Route
+ path={InstancePaths.webhooks_new}
+ component={WebhookCreatePage}
+ onConfirm={() => {
+ route(InstancePaths.webhooks_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.webhooks_list);
+ }}
+ />
+ {/**
+ * Validator pages
+ */}
+ <Route
+ path={InstancePaths.otp_devices_list}
+ component={ValidatorListPage}
+ onCreate={() => {
+ route(InstancePaths.otp_devices_new);
+ }}
+ onSelect={(id: string) => {
+ route(InstancePaths.otp_devices_update.replace(":vid", id));
+ }}
+ />
+ <Route
+ path={InstancePaths.otp_devices_update}
+ component={ValidatorUpdatePage}
+ onConfirm={() => {
+ route(InstancePaths.otp_devices_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.otp_devices_list);
+ }}
+ />
+ <Route
+ path={InstancePaths.otp_devices_new}
+ component={ValidatorCreatePage}
+ onConfirm={() => {
+ route(InstancePaths.otp_devices_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.otp_devices_list);
+ }}
+ />
+ {/**
+ * Templates pages
+ */}
+ <Route
+ path={InstancePaths.templates_list}
+ component={TemplateListPage}
+ onCreate={() => {
+ route(InstancePaths.templates_new);
+ }}
+ onNewOrder={(id: string) => {
+ route(InstancePaths.templates_use.replace(":tid", id));
+ }}
+ onQR={(id: string) => {
+ route(InstancePaths.templates_qr.replace(":tid", id));
+ }}
+ onSelect={(id: string) => {
+ route(InstancePaths.templates_update.replace(":tid", id));
+ }}
+ />
+ <Route
+ path={InstancePaths.templates_update}
+ component={TemplateUpdatePage}
+ onConfirm={() => {
+ route(InstancePaths.templates_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.templates_list);
+ }}
+ />
+ <Route
+ path={InstancePaths.templates_new}
+ component={TemplateCreatePage}
+ onConfirm={() => {
+ route(InstancePaths.templates_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.templates_list);
+ }}
+ />
+ <Route
+ path={InstancePaths.templates_use}
+ component={TemplateUsePage}
+ onOrderCreated={(id: string) => {
+ route(InstancePaths.order_details.replace(":oid", id));
+ }}
+ onBack={() => {
+ route(InstancePaths.templates_list);
+ }}
+ />
+ <Route
+ path={InstancePaths.templates_qr}
+ component={TemplateQrPage}
+ onBack={() => {
+ route(InstancePaths.templates_list);
+ }}
+ />
+
+ <Route path={InstancePaths.kyc} component={ListKYCPage} />
+ <Route path={InstancePaths.interface} component={Settings} />
+ {/**
+ * Example pages
+ */}
+ <Route path="/loading" component={Loading} />
+ <Route default component={NotFoundPage} />
+ </Router>
+ </Fragment>
+ );
+}
+
+export function Redirect({ to }: { to: string }): null {
+ useEffect(() => {
+ route(to, true);
+ });
+ return null;
+}
+
+function AdminInstanceUpdatePage({
+ id,
+ ...rest
+}: { id: string } & InstanceUpdatePageProps): VNode {
+ // const { i18n } = useTranslationContext();
+
+ return (
+ <Fragment>
+ <InstanceAdminUpdatePage
+ {...rest}
+ instanceId={id}
+ // onLoadError={(error: HttpError<TalerErrorDetail>) => {
+ // const notif =
+ // error.type === ErrorType.TIMEOUT
+ // ? {
+ // message: i18n.str`The request to the backend take too long and was cancelled`,
+ // description: i18n.str`Diagnostic from ${error.info.url} is '${error.message}'`,
+ // type: "ERROR" as const,
+ // }
+ // : {
+ // message: i18n.str`The backend reported a problem: HTTP status #${error.status}`,
+ // description: i18n.str`Diagnostic from ${error.info.url} is '${error.message}'`,
+ // details:
+ // error.type === ErrorType.CLIENT ||
+ // error.type === ErrorType.SERVER
+ // ? error.payload.hint
+ // : undefined,
+ // type: "ERROR" as const,
+ // };
+ // return (
+ // <Fragment>
+ // <NotificationCard notification={notif} />
+ // <LoginPage />
+ // </Fragment>
+ // );
+ // }}
+ // onUnauthorized={() => {
+ // return (
+ // <Fragment>
+ // <NotificationCard
+ // notification={{
+ // message: i18n.str`Access denied`,
+ // description: i18n.str`The access token provided is invalid`,
+ // type: "ERROR",
+ // }}
+ // />
+ // <LoginPage />
+ // </Fragment>
+ // );
+ // }}
+ />
+ </Fragment>
+ );
+}
+
+function BankAccountBanner(): VNode {
+ const { i18n } = useTranslationContext();
+
+ const [, updatePref] = usePreference();
+ const now = AbsoluteTime.now();
+ const oneDay = { d_ms: 1000 * 60 * 60 * 24 };
+ const tomorrow = AbsoluteTime.addDuration(now, oneDay);
+
+ return (
+ <NotificationCard
+ notification={{
+ type: "INFO",
+ message: i18n.str`You need to associate a bank account to receive revenue.`,
+ description: (
+ <div>
+ <p>
+ <i18n.Translate>
+ Without this the merchant backend will refuse to create new
+ orders.
+ </i18n.Translate>
+ </p>
+ <div class="buttons is-right">
+ <button
+ class="button"
+ onClick={() => updatePref("hideMissingAccountUntil", tomorrow)}
+ >
+ <i18n.Translate>Hide for today</i18n.Translate>
+ </button>
+ </div>
+ </div>
+ ),
+ }}
+ />
+ );
+}
+
+function KycBanner(): VNode {
+ const kycStatus = useInstanceKYCDetails();
+ const { i18n } = useTranslationContext();
+ // const today = format(new Date(), dateFormatForSettings(settings));
+ const [prefs, updatePref] = usePreference();
+
+ const now = AbsoluteTime.now();
+
+ const needsToBeShown =
+ kycStatus !== undefined &&
+ !(kycStatus instanceof TalerError) &&
+ kycStatus.type === "ok" &&
+ !!kycStatus.body;
+
+ const hidden = AbsoluteTime.cmp(now, prefs.hideKycUntil) < 1;
+ if (hidden || !needsToBeShown) return <Fragment />;
+
+ const oneDay = { d_ms: 1000 * 60 * 60 * 24 };
+ const tomorrow = AbsoluteTime.addDuration(now, oneDay);
+
+ return (
+ <NotificationCard
+ notification={{
+ type: "WARN",
+ message: "KYC verification needed",
+ description: (
+ <div>
+ <p>
+ <i18n.Translate>
+ Some transfer are on hold until a KYC process is completed. Go
+ to the KYC section in the left panel for more information
+ </i18n.Translate>
+ </p>
+ <div class="buttons is-right">
+ <button
+ class="button"
+ onClick={() => updatePref("hideKycUntil", tomorrow)}
+ >
+ <i18n.Translate>Hide for today</i18n.Translate>
+ </button>
+ </div>
+ </div>
+ ),
+ }}
+ />
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/assets/logo-2021.svg b/packages/merchant-backoffice-ui/src/assets/logo-2021.svg
new file mode 100644
index 000000000..8c5ff3e5b
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/assets/logo-2021.svg
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 201 90">
+ <g fill="#0042b3" fill-rule="evenodd" stroke-width=".3">
+ <path d="M86.7 1.1c15.6 0 29 9.4 36 23.2h-5.9A35.1 35.1 0 0086.7 6.5C67 6.5 51 23.6 51 44.7c0 10.4 3.8 19.7 10 26.6a31.4 31.4 0 01-4.2 3A45.2 45.2 0 0146 44.7c0-24 18.2-43.6 40.7-43.6zm35.8 64.3a40.4 40.4 0 01-39 22.8c3-1.5 6-3.5 8.6-5.7a35.6 35.6 0 0024.6-17.1z" />
+ <path d="M64.2 1.1l3.1.1c-3 1.6-5.9 3.5-8.5 5.8a37.5 37.5 0 00-30.2 37.7c0 14.3 7.3 26.7 18 33.3a29.6 29.6 0 01-8.5.2c-9-8-14.6-20-14.6-33.5 0-24 18.2-43.6 40.7-43.6zm5.4 81.4a35.6 35.6 0 0024.6-17.1h5.9a40.4 40.4 0 01-39 22.8c3-1.5 5.9-3.5 8.5-5.7zm24.8-58.2a37 37 0 00-12.6-12.8 29.6 29.6 0 018.5-.2c4 3.6 7.4 8 9.9 13z" />
+ <path d="M41.8 1.1c1 0 2 0 3.1.2-3 1.5-5.9 3.4-8.5 5.6A37.5 37.5 0 006.1 44.7c0 21.1 16 38.3 35.7 38.3 12.6 0 23.6-7 30-17.6h5.8a40.4 40.4 0 01-35.8 23C19.3 88.4 1 68.8 1 44.7c0-24 18.2-43.6 40.7-43.6zm30.1 23.2a38.1 38.1 0 00-4.5-6.1c1.3-1.2 2.7-2.2 4.3-3 2.3 2.7 4.4 5.8 6 9.1z" />
+ </g>
+ <path d="M76.1 34.4h9.2v-5H61.9v5H71v26h5.1zM92.6 52.9h13.7l3 7.4h5.3l-12.7-31.2h-4.7L84.5 60.3h5.2zm11.8-4.9h-9.9l5-12.4zM123.8 29.4h-4.6v31h20.6v-5h-16zM166.5 29.4H145v31h21.6v-5H150v-8.3h14.5v-4.9h-14.5v-8h16.4zM191.2 39.5c0 1.6-.5 2.8-1.6 3.8s-2.6 1.4-4.4 1.4h-7.4V34.3h7.4c1.9 0 3.4.4 4.4 1.3 1 .9 1.6 2.2 1.6 3.9zm6 20.8l-7.7-11.7c1-.3 1.9-.7 2.7-1.3a8.8 8.8 0 003.6-4.6c.4-1 .5-2.2.5-3.5 0-1.5-.2-2.9-.7-4.1a8.4 8.4 0 00-2.1-3.1c-1-.8-2-1.5-3.4-2-1.3-.4-2.8-.6-4.5-.6h-12.9v31h5V49.4h6.5l7 10.8z" />
+</svg> \ No newline at end of file
diff --git a/packages/merchant-backoffice-ui/src/components/ErrorLoadingMerchant.tsx b/packages/merchant-backoffice-ui/src/components/ErrorLoadingMerchant.tsx
new file mode 100644
index 000000000..b1d1cac66
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/components/ErrorLoadingMerchant.tsx
@@ -0,0 +1,146 @@
+/*
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { TalerError, TalerErrorCode, assertUnreachable } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { NotificationCard } from "./menu/index.js";
+
+/**
+ * equivalent to ErrorLoading for merchant-backoffice which uses notification-card
+ * @param param0
+ * @returns
+ */
+export function ErrorLoadingMerchant({ error, showDetail }: { error: TalerError, showDetail?: boolean }): VNode {
+ const { i18n } = useTranslationContext()
+ switch (error.errorDetail.code) {
+ //////////////////
+ // Every error that can be produce in a Http Request
+ //////////////////
+ case TalerErrorCode.GENERIC_TIMEOUT: {
+ if (error.hasErrorCode(TalerErrorCode.GENERIC_TIMEOUT)) {
+ const { requestMethod, requestUrl, timeoutMs } = error.errorDetail
+ return <NotificationCard
+ notification={{
+ type: "ERROR",
+ message: i18n.str`The request reached a timeout, check your connection.`,
+ description: error.message,
+ details: JSON.stringify({ requestMethod, requestUrl, timeoutMs }, undefined, 2)
+ }} />
+ }
+ assertUnreachable(1 as never)
+ }
+ case TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR: {
+ if (error.hasErrorCode(TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR)) {
+ const { requestMethod, requestUrl, timeoutMs } = error.errorDetail
+ return <NotificationCard
+ notification={{
+ type: "ERROR",
+ message: i18n.str`The request was cancelled.`,
+ description: error.message,
+ details: JSON.stringify({ requestMethod, requestUrl, timeoutMs }, undefined, 2)
+ }} />
+ }
+ assertUnreachable(1 as never)
+ }
+ case TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT: {
+ if (error.hasErrorCode(TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT)) {
+ const { requestMethod, requestUrl, timeoutMs } = error.errorDetail
+ return <NotificationCard
+ notification={{
+ type: "ERROR",
+ message: i18n.str`The request reached a timeout, check your connection.`,
+ description: error.message,
+ details: JSON.stringify({ requestMethod, requestUrl, timeoutMs }, undefined, 2)
+ }} />
+ }
+ assertUnreachable(1 as never)
+ }
+ case TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED: {
+ if (error.hasErrorCode(TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED)) {
+ const { requestMethod, requestUrl, throttleStats } = error.errorDetail
+ return <NotificationCard
+ notification={{
+ type: "ERROR",
+ message: i18n.str`A lot of request were made to the same server and this action was throttled.`,
+ description: error.message,
+ details: JSON.stringify({ requestMethod, requestUrl, throttleStats }, undefined, 2)
+ }} />
+ }
+ assertUnreachable(1 as never)
+ }
+ case TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE: {
+ if (error.hasErrorCode(TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE)) {
+ const { requestMethod, requestUrl, httpStatusCode, validationError } = error.errorDetail
+ return <NotificationCard
+ notification={{
+ type: "ERROR",
+ message: i18n.str`The response of the request is malformed.`,
+ description: error.message,
+ details: JSON.stringify({ requestMethod, requestUrl, httpStatusCode, validationError }, undefined, 2)
+ }} />
+ }
+ assertUnreachable(1 as never)
+ }
+ case TalerErrorCode.WALLET_NETWORK_ERROR: {
+ if (error.hasErrorCode(TalerErrorCode.WALLET_NETWORK_ERROR)) {
+ const { requestMethod, requestUrl } = error.errorDetail
+ return <NotificationCard
+ notification={{
+ type: "ERROR",
+ message: i18n.str`Could not complete the request due to a network problem.`,
+ description: error.message,
+ details: JSON.stringify({ requestMethod, requestUrl }, undefined, 2)
+ }} />
+ }
+ assertUnreachable(1 as never)
+ }
+ case TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR: {
+ if (error.hasErrorCode(TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR)) {
+ const { requestMethod, requestUrl, httpStatusCode, errorResponse } = error.errorDetail
+ return <NotificationCard
+ notification={{
+ type: "ERROR",
+ message: i18n.str`Unexpected request error.`,
+ description: error.message,
+ details: JSON.stringify({ requestMethod, requestUrl, httpStatusCode, errorResponse }, undefined, 2)
+ }} />
+ }
+ assertUnreachable(1 as never)
+ }
+ //////////////////
+ // Every other error
+ //////////////////
+ // case TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR: {
+ // return <Attention type="danger" title={i18n.str``}>
+ // </Attention>
+ // }
+ //////////////////
+ // Default message for unhandled case
+ //////////////////
+ default: {
+ return <NotificationCard
+ notification={{
+ type: "ERROR",
+ message: i18n.str`Unexpected error.`,
+ description: error.message,
+ details: JSON.stringify(error.errorDetail, undefined, 2)
+ }} />
+ }
+ }
+}
+
diff --git a/packages/merchant-backoffice-ui/src/components/exception/AsyncButton.tsx b/packages/merchant-backoffice-ui/src/components/exception/AsyncButton.tsx
index 2060425fb..58c10e7d7 100644
--- a/packages/merchant-backoffice-ui/src/components/exception/AsyncButton.tsx
+++ b/packages/merchant-backoffice-ui/src/components/exception/AsyncButton.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,35 +15,41 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
import { ComponentChildren, h } from "preact";
import { LoadingModal } from "../modal/index.js";
import { useAsync } from "../../hooks/async.js";
-import { Translate } from "../../i18n/index.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
type Props = {
- children: ComponentChildren,
+ children: ComponentChildren;
disabled: boolean;
onClick?: () => Promise<void>;
- [rest:string]: any,
+ [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"><Translate>Loading...</Translate></button>;
+ 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>;
+ return (
+ <span {...rest}>
+ <button class="button is-success" onClick={request} disabled={disabled}>
+ {children}
+ </button>
+ </span>
+ );
}
diff --git a/packages/merchant-backoffice-ui/src/components/exception/QR.tsx b/packages/merchant-backoffice-ui/src/components/exception/QR.tsx
index bcb9964a5..246ce0229 100644
--- a/packages/merchant-backoffice-ui/src/components/exception/QR.tsx
+++ b/packages/merchant-backoffice-ui/src/components/exception/QR.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2021-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
diff --git a/packages/merchant-backoffice-ui/src/components/exception/loading.tsx b/packages/merchant-backoffice-ui/src/components/exception/loading.tsx
index f2139a17e..5c249f79d 100644
--- a/packages/merchant-backoffice-ui/src/components/exception/loading.tsx
+++ b/packages/merchant-backoffice-ui/src/components/exception/loading.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,18 +15,34 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
import { h, VNode } from "preact";
export function Loading(): VNode {
- return <div class="columns is-centered is-vcentered" style={{ height: 'calc(100% - 3rem)', position: 'absolute', width: '100%' }}>
- <Spinner />
- </div>
+ 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>
-} \ No newline at end of file
+ return (
+ <div class="lds-ring">
+ <div />
+ <div />
+ <div />
+ <div />
+ </div>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/components/exception/login.tsx b/packages/merchant-backoffice-ui/src/components/exception/login.tsx
deleted file mode 100644
index eefce2de7..000000000
--- a/packages/merchant-backoffice-ui/src/components/exception/login.tsx
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import { h, VNode } from "preact";
-import { useState } from "preact/hooks";
-import { useBackendContext } from "../../context/backend.js";
-import { useInstanceContext } from "../../context/instance.js";
-import { Translate, useTranslator } from "../../i18n/index.js";
-import { Notification } from "../../utils/types.js";
-
-interface Props {
- withMessage?: Notification;
- onConfirm: (backend: string, token?: string) => void;
-}
-
-function getTokenValuePart(t?: string): string | undefined {
- if (!t) return t;
- const match = /secret-token:(.*)/.exec(t);
- if (!match || !match[1]) return undefined;
- return match[1];
-}
-
-function normalizeToken(r: string | undefined): string | undefined {
- return r ? `secret-token:${encodeURIComponent(r)}` : undefined;
-}
-
-export function LoginModal({ onConfirm, withMessage }: Props): VNode {
- const { url: backendUrl, token: baseToken } = useBackendContext();
- const { admin, token: instanceToken } = useInstanceContext();
- const currentToken = getTokenValuePart(
- !admin ? baseToken : instanceToken || ""
- );
- const [token, setToken] = useState(currentToken);
-
- const [url, setURL] = useState(backendUrl);
- const i18n = useTranslator();
-
- return (
- <div class="columns is-centered">
- <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`Login required`}</p>
- </header>
- <section
- class="modal-card-body"
- style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }}
- >
- <Translate>Please enter your access token.</Translate>
- <div class="field is-horizontal">
- <div class="field-label is-normal">
- <label class="label">URL</label>
- </div>
- <div class="field-body">
- <div class="field">
- <p class="control is-expanded">
- <input
- class="input"
- type="text"
- placeholder="set new url"
- name="id"
- value={url}
- onKeyPress={(e) =>
- e.keyCode === 13
- ? onConfirm(url, normalizeToken(token))
- : null
- }
- onInput={(e): void => setURL(e?.currentTarget.value)}
- />
- </p>
- </div>
- </div>
- </div>
- <div class="field is-horizontal">
- <div class="field-label is-normal">
- <label class="label">
- <Translate>Access Token</Translate>
- </label>
- </div>
- <div class="field-body">
- <div class="field">
- <p class="control is-expanded">
- <input
- class="input"
- type="password"
- placeholder={"set new access token"}
- name="token"
- onKeyPress={(e) =>
- e.keyCode === 13
- ? onConfirm(url, normalizeToken(token))
- : null
- }
- value={token}
- onInput={(e): void => setToken(e?.currentTarget.value)}
- />
- </p>
- </div>
- </div>
- </div>
- </section>
- <footer
- class="modal-card-foot "
- style={{
- justifyContent: "flex-end",
- border: "1px solid",
- borderTop: 0,
- }}
- >
- <button
- class="button is-info"
- onClick={(): void => {
- onConfirm(url, normalizeToken(token));
- }}
- >
- <Translate>Confirm</Translate>
- </button>
- </footer>
- </div>
- </div>
- </div>
- );
-}
diff --git a/packages/merchant-backoffice-ui/src/components/form/FormProvider.tsx b/packages/merchant-backoffice-ui/src/components/form/FormProvider.tsx
index aef410ce7..a5f3c1d2f 100644
--- a/packages/merchant-backoffice-ui/src/components/form/FormProvider.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/FormProvider.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,37 +15,59 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
import { ComponentChildren, createContext, h, VNode } from "preact";
import { useContext, useMemo } from "preact/hooks";
-type Updater<S> = (value: ((prevState: S) => S) ) => void;
+type Updater<S> = (value: (prevState: S) => S) => void;
export interface Props<T> {
object?: Partial<T>;
errors?: FormErrors<T>;
name?: string;
valueHandler: Updater<Partial<T>> | null;
- children: ComponentChildren
+ children: ComponentChildren;
}
-const noUpdater: Updater<Partial<unknown>> = () => (s: unknown) => s
+const noUpdater: Updater<Partial<unknown>> = () => (s: unknown) => s;
-export function FormProvider<T>({ object = {}, errors = {}, name = '', valueHandler, children }: Props<T>): VNode {
+export function FormProvider<T>({
+ object = {},
+ errors = {},
+ name = "",
+ valueHandler,
+ children,
+}: Props<T>): VNode {
const initialObject = useMemo(() => object, []);
- const value = useMemo<FormType<T>>(() => ({ errors, object, initialObject, valueHandler: valueHandler ? valueHandler : noUpdater, name, toStr: {}, fromStr: {} }), [errors, object, valueHandler]);
-
- return <FormContext.Provider value={value}>
- <form class="field" onSubmit={(e) => {
- e.preventDefault();
- // if (valueHandler) valueHandler(object);
- }}>
- {children}
- </form>
- </FormContext.Provider>;
+ const value = useMemo<FormType<T>>(
+ () => ({
+ errors,
+ object,
+ initialObject,
+ valueHandler: valueHandler ? valueHandler : noUpdater,
+ name,
+ toStr: {},
+ fromStr: {},
+ }),
+ [errors, object, valueHandler],
+ );
+
+ return (
+ <FormContext.Provider value={value}>
+ <form
+ class="field"
+ onSubmit={(e) => {
+ e.preventDefault();
+ // if (valueHandler) valueHandler(object);
+ }}
+ >
+ {children}
+ </form>
+ </FormContext.Provider>
+ );
}
export interface FormType<T> {
@@ -58,24 +80,30 @@ export interface FormType<T> {
valueHandler: Updater<Partial<T>>;
}
-const FormContext = createContext<FormType<unknown>>(null!)
+const FormContext = createContext<FormType<unknown>>(null!);
+
+/**
+ * FIXME:
+ * USE MEMORY EVENTS INSTEAD OF CONTEXT
+ * @deprecated
+ */
export function useFormContext<T>() {
- return useContext<FormType<T>>(FormContext)
+ return useContext<FormType<T>>(FormContext);
}
export type FormErrors<T> = {
- [P in keyof T]?: string | FormErrors<T[P]>
-}
+ [P in keyof T]?: string | FormErrors<T[P]>;
+};
export type FormtoStr<T> = {
- [P in keyof T]?: ((f?: T[P]) => string)
-}
+ [P in keyof T]?: (f?: T[P]) => string;
+};
export type FormfromStr<T> = {
- [P in keyof T]?: ((f: string) => T[P])
-}
+ [P in keyof T]?: (f: string) => T[P];
+};
export type FormUpdater<T> = {
- [P in keyof T]?: (f: keyof T) => (v: T[P]) => void
-}
+ [P in keyof T]?: (f: keyof T) => (v: T[P]) => void;
+};
diff --git a/packages/merchant-backoffice-ui/src/components/form/Input.tsx b/packages/merchant-backoffice-ui/src/components/form/Input.tsx
index dc4e9ae1a..899061c35 100644
--- a/packages/merchant-backoffice-ui/src/components/form/Input.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/Input.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,102 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @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';
+ inputType?: "text" | "number" | "multiline" | "password";
expand?: boolean;
toStr?: (v?: any) => string;
fromStr?: (s: string) => any;
- inputExtra?: any,
+ inputExtra?: any;
side?: ComponentChildren;
children?: ComponentChildren;
}
-const defaultToString = (f?: any): string => f || ''
-const defaultFromString = (v: string): any => v as any
+const defaultToString = (f?: any): string => f || "";
+const defaultFromString = (v: string): any => v as any;
-const TextInput = ({ inputType, error, ...rest }: any) => inputType === 'multiline' ?
- <textarea {...rest} class={error ? "textarea is-danger" : "textarea"} rows="3" /> :
- <input {...rest} class={error ? "input is-danger" : "input"} type={inputType} />;
+const TextInput = ({ inputType, error, ...rest }: any) =>
+ inputType === "multiline" ? (
+ <textarea
+ {...rest}
+ class={error ? "textarea is-danger" : "textarea"}
+ rows="3"
+ />
+ ) : (
+ <input
+ {...rest}
+ class={error ? "input is-danger" : "input"}
+ type={inputType}
+ />
+ );
-export function Input<T>({ name, readonly, placeholder, tooltip, label, expand, help, children, inputType, inputExtra, side, fromStr = defaultFromString, toStr = defaultToString }: Props<keyof T>): VNode {
+export function Input<T>({
+ name,
+ readonly,
+ placeholder,
+ tooltip,
+ label,
+ expand,
+ help,
+ children,
+ inputType,
+ inputExtra,
+ side,
+ fromStr = defaultFromString,
+ toStr = defaultToString,
+}: Props<keyof T>): VNode {
const { error, value, onChange, required } = useField<T>(name);
- return <div class="field is-horizontal">
- <div class="field-label is-normal">
- <label class="label">
- {label}
- {tooltip && <span class="icon has-tooltip-right" data-tooltip={tooltip}>
- <i class="mdi mdi-information" />
- </span>}
- </label>
- </div>
- <div class="field-body is-flex-grow-3">
- <div class="field">
- <p class={expand ? "control is-expanded has-icons-right" : "control has-icons-right"}>
- <TextInput error={error} {...inputExtra}
- inputType={inputType}
- placeholder={placeholder} readonly={readonly}
- name={String(name)} value={toStr(value)}
- onChange={(e: h.JSX.TargetedEvent<HTMLInputElement>): void => onChange(fromStr(e.currentTarget.value))} />
- {help}
- {children}
- { required && <span class="icon has-text-danger is-right">
- <i class="mdi mdi-alert" />
- </span> }
- </p>
- {error && <p class="help is-danger">{error}</p>}
+ return (
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ {label}
+ {tooltip && (
+ <span class="icon has-tooltip-right" data-tooltip={tooltip}>
+ <i class="mdi mdi-information" />
+ </span>
+ )}
+ </label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p
+ class={
+ expand
+ ? "control is-expanded has-icons-right"
+ : "control has-icons-right"
+ }
+ >
+ <TextInput
+ error={error}
+ {...inputExtra}
+ inputType={inputType}
+ placeholder={placeholder}
+ readonly={readonly}
+ disabled={readonly}
+ name={String(name)}
+ value={toStr(value)}
+ onChange={(e: h.JSX.TargetedEvent<HTMLInputElement>): void =>
+ onChange(fromStr(e.currentTarget.value))
+ }
+ />
+ {help}
+ {children}
+ {required && (
+ <span class="icon has-text-danger is-right">
+ <i class="mdi mdi-alert" />
+ </span>
+ )}
+ </p>
+ {error && <p class="help is-danger">{error}</p>}
+ </div>
+ {side}
</div>
- {side}
</div>
- </div>;
+ );
}
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputArray.tsx b/packages/merchant-backoffice-ui/src/components/form/InputArray.tsx
index bcefc25d9..b0b9eaefc 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputArray.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputArray.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,12 +15,12 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
-import { Translate, useTranslator } from "../../i18n/index.js";
import { InputProps, useField } from "./useField.js";
export interface Props<T> extends InputProps<T> {
@@ -30,68 +30,110 @@ export interface Props<T> extends InputProps<T> {
fromStr?: (s: string) => any;
}
-const defaultToString = (f?: any): string => f || ''
-const defaultFromString = (v: string): any => v as 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 {
+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 [localError, setLocalError] = useState<string | null>(null);
- const error = localError || formError
+ const error = localError || formError;
const array: any[] = (value ? value! : []) as any;
- const [currentValue, setCurrentValue] = useState('');
- const i18n = useTranslator();
+ 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`The value ${v} is invalid for a payment url`)
- return;
- }
- setLocalError(null)
- onChange([v, ...array] as any);
- setCurrentValue('');
- }} data-tooltip={i18n`add element to the list`}><Translate>add</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));
- }} />
+ 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>
- </div>;
+ );
}
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputBoolean.tsx b/packages/merchant-backoffice-ui/src/components/form/InputBoolean.tsx
index c4ef9441c..bdb2feb6b 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputBoolean.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputBoolean.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,9 +15,9 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
import { h, VNode } from "preact";
import { InputProps, useField } from "./useField.js";
@@ -30,43 +30,62 @@ interface Props<T> extends InputProps<T> {
fromBoolean?: (s: boolean | undefined) => any;
}
-const defaultToBoolean = (f?: any): boolean | undefined => f || ''
-const defaultFromBoolean = (v: boolean | undefined): any => v as 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 {
+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))
- }
+ 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>}
+ 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>
- </div>;
+ );
}
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputCurrency.tsx b/packages/merchant-backoffice-ui/src/components/form/InputCurrency.tsx
index c4c6b8ce3..11396b88e 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputCurrency.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputCurrency.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,14 +15,15 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-import { ComponentChildren, h } from "preact";
-import { useConfigContext } from "../../context/config.js";
-import { Amount } from "../../declaration.js";
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { useMerchantApiContext } from "@gnu-taler/web-util/browser";
+import { ComponentChildren, h, VNode } from "preact";
import { InputWithAddon } from "./InputWithAddon.js";
import { InputProps } from "./useField.js";
+import { AmountString } from "@gnu-taler/taler-util";
+import { useSessionContext } from "../../context/session.js";
export interface Props<T> extends InputProps<T> {
expand?: boolean;
@@ -31,17 +32,37 @@ export interface Props<T> extends InputProps<T> {
side?: ComponentChildren;
}
-export function InputCurrency<T>({ name, readonly, label, placeholder, help, tooltip, expand, addonAfter, children, side }: Props<keyof T>) {
- const config = useConfigContext()
- return <InputWithAddon<T> name={name} readonly={readonly} addonBefore={config.currency}
- side={side}
- label={label} placeholder={placeholder} help={help} tooltip={tooltip}
- addonAfter={addonAfter}
- inputType='number' expand={expand}
- toStr={(v?: Amount) => v?.split(':')[1] || ''}
- fromStr={(v: string) => !v ? '' : `${config.currency}:${v}`}
- inputExtra={{ min: 0 }}
- children={children}
- />
+export function InputCurrency<T>({
+ name,
+ readonly,
+ label,
+ placeholder,
+ help,
+ tooltip,
+ expand,
+ addonAfter,
+ children,
+ side,
+}: Props<keyof T>): VNode {
+ const { config } = useSessionContext();
+ return (
+ <InputWithAddon<T>
+ name={name}
+ readonly={readonly}
+ addonBefore={config.currency}
+ side={side}
+ label={label}
+ placeholder={placeholder}
+ help={help}
+ tooltip={tooltip}
+ addonAfter={addonAfter}
+ inputType="number"
+ expand={expand}
+ toStr={(v?: AmountString) => v?.split(":")[1] || ""}
+ fromStr={(v: string) => (!v ? undefined : `${config.currency}:${v}`)}
+ inputExtra={{ min: 0 }}
+ >
+ {children}
+ </InputWithAddon>
+ );
}
-
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputDate.tsx b/packages/merchant-backoffice-ui/src/components/form/InputDate.tsx
index ce99a02fb..812505f6a 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputDate.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputDate.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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
@@ -18,18 +18,20 @@
*
* @author Sebastian Javier Marchano (sebasjm)
*/
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { format } from "date-fns";
-import { h, VNode } from "preact";
+import { ComponentChildren, h, VNode } from "preact";
import { useState } from "preact/hooks";
-import { Translate, useTranslator } from "../../i18n/index.js";
import { DatePicker } from "../picker/DatePicker.js";
import { InputProps, useField } from "./useField.js";
+import { dateFormatForSettings, usePreference } from "../../hooks/preference.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>({
@@ -41,9 +43,11 @@ export function InputDate<T>({
tooltip,
expand,
withTimestampSupport,
+ side,
}: Props<keyof T>): VNode {
const [opened, setOpened] = useState(false);
- const i18n = useTranslator();
+ const { i18n } = useTranslationContext();
+ const [settings] = usePreference()
const { error, required, value, onChange } = useField<T>(name);
@@ -51,14 +55,14 @@ export function InputDate<T>({
if (!value) {
strValue = withTimestampSupport ? "unknown" : "";
} else if (value instanceof Date) {
- strValue = format(value, "yyyy/MM/dd");
+ strValue = format(value, dateFormatForSettings(settings));
} else if (value.t_s) {
strValue =
value.t_s === "never"
? withTimestampSupport
? "never"
: ""
- : format(new Date(value.t_s * 1000), "yyyy/MM/dd");
+ : format(new Date(value.t_s * 1000), dateFormatForSettings(settings));
}
return (
@@ -120,28 +124,29 @@ export function InputDate<T>({
<span
data-tooltip={
withTimestampSupport
- ? i18n`change value to unknown date`
- : i18n`change value to empty`
+ ? i18n.str`change value to unknown date`
+ : i18n.str`change value to empty`
}
>
<button
class="button is-info mr-3"
onClick={() => onChange(undefined as any)}
>
- <Translate>clear</Translate>
+ <i18n.Translate>clear</i18n.Translate>
</button>
</span>
)}
{withTimestampSupport && (
- <span data-tooltip={i18n`change value to never`}>
+ <span data-tooltip={i18n.str`change value to never`}>
<button
class="button is-info"
onClick={() => onChange({ t_s: "never" } as any)}
>
- <Translate>never</Translate>
+ <i18n.Translate>never</i18n.Translate>
</button>
</span>
)}
+ {side}
</div>
<DatePicker
opened={opened}
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputDuration.tsx b/packages/merchant-backoffice-ui/src/components/form/InputDuration.tsx
index 6abe55be0..ad3cb0e32 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputDuration.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputDuration.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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
@@ -18,18 +18,21 @@
*
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { intervalToDuration, formatDuration } from "date-fns";
-import { h, VNode } from "preact";
+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 { Translate, useTranslator } from "../../i18n/index.js";
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>({
@@ -41,35 +44,41 @@ export function InputDuration<T>({
help,
readonly,
withForever,
+ withoutClear,
+ side,
}: Props<keyof T>): VNode {
const [opened, setOpened] = useState(false);
- const i18n = useTranslator();
+ const { i18n } = useTranslationContext();
- const { error, required, value, onChange } = useField<T>(name);
+ const { error, required, value: anyValue, onChange } = useField<T>(name);
let strValue = "";
+ const value: Duration = anyValue
if (!value) {
strValue = "";
- } else if (value.d_us === "forever") {
- strValue = i18n`forever`;
+ } 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)}'`)
+ }
strValue = formatDuration(
- intervalToDuration({ start: 0, end: value.d_us / 1000 }),
+ intervalToDuration({ start: 0, end: value.d_ms }),
{
locale: {
formatDistance: (name, value) => {
switch (name) {
case "xMonths":
- return i18n`${value}M`;
+ return i18n.str`${value}M`;
case "xYears":
- return i18n`${value}Y`;
+ return i18n.str`${value}Y`;
case "xDays":
- return i18n`${value}d`;
+ return i18n.str`${value}d`;
case "xHours":
- return i18n`${value}h`;
+ return i18n.str`${value}h`;
case "xMinutes":
- return i18n`${value}min`;
+ return i18n.str`${value}min`;
case "xSeconds":
- return i18n`${value}sec`;
+ return i18n.str`${value}sec`;
}
},
localize: {
@@ -81,13 +90,13 @@ export function InputDuration<T>({
era: () => "e",
},
},
- }
+ },
);
}
return (
<div class="field is-horizontal">
- <div class="field-label is-normal">
+ <div class="field-label is-normal is-flex-grow-3">
<label class="label">
{label}
{tooltip && (
@@ -97,72 +106,80 @@ export function InputDuration<T>({
)}
</label>
</div>
- <div class="field-body is-flex-grow-3">
- <div class="field">
- <div class="field has-addons">
- <p class={expand ? "control is-expanded " : "control "}>
- <input
- class="input"
- type="text"
- readonly
- value={strValue}
- placeholder={placeholder}
+
+ <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);
}}
- />
- {required && (
- <span class="icon has-text-danger is-right">
- <i class="mdi mdi-alert" />
- </span>
- )}
- {help}
- </p>
- <div
- class="control"
- onClick={() => {
- if (!readonly) setOpened(true);
- }}
- >
- <a class="button is-static">
- <span class="icon">
- <i class="mdi mdi-clock" />
- </span>
- </a>
+ >
+ <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>
- {error && <p class="help is-danger">{error}</p>}
+ {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>
- {withForever && (
- <span data-tooltip={i18n`change value to never`}>
- <button
- class="button is-info mr-3"
- onClick={() => onChange({ d_us: "forever" } as any)}
- >
- <Translate>forever</Translate>
- </button>
- </span>
- )}
- {!readonly && (
- <span data-tooltip={i18n`change value to empty`}>
- <button
- class="button is-info "
- onClick={() => onChange(undefined as any)}
- >
- <Translate>clear</Translate>
- </button>
- </span>
- )}
+ <span>
+ {help}
+ </span>
</div>
+
+
{opened && (
<SimpleModal onCancel={() => setOpened(false)}>
<DurationPicker
days
hours
minutes
- value={!value || value.d_us === "forever" ? 0 : value.d_us}
+ value={!value || value.d_ms === "forever" ? 0 : value.d_ms}
onChange={(v) => {
- onChange({ d_us: v } as any);
+ onChange({ d_ms: v } as any);
}}
/>
</SimpleModal>
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputGroup.tsx b/packages/merchant-backoffice-ui/src/components/form/InputGroup.tsx
index b73e5c5f7..92b9e8b16 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputGroup.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputGroup.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,9 +15,9 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
import { ComponentChildren, h, VNode } from "preact";
import { useState } from "preact/hooks";
import { useGroupField } from "./useGroupField.js";
@@ -32,35 +32,55 @@ export interface Props<T> {
initialActive?: boolean;
}
-export function InputGroup<T>({ name, label, children, tooltip, alternative, fixed, initialActive }: Props<keyof T>): VNode {
+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>;
+ 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/merchant-backoffice-ui/src/components/form/InputImage.tsx b/packages/merchant-backoffice-ui/src/components/form/InputImage.tsx
index 8e2586933..d284b476f 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputImage.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputImage.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,12 +15,12 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @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 { Translate } from "../../i18n/index.js";
import { MAX_IMAGE_SIZE as MAX_IMAGE_UPLOAD_SIZE } from "../../utils/constants.js";
import { InputProps, useField } from "./useField.js";
@@ -30,65 +30,93 @@ export interface Props<T> extends InputProps<T> {
children?: ComponentChildren;
}
-export function InputImage<T>({ name, readonly, placeholder, tooltip, label, help, children, expand }: Props<keyof T>): VNode {
+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 [sizeError, setSizeError] = useState(false)
+ 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 = 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">
- <Translate>Image should be smaller than 1 MB</Translate>
- </p>}
- {!value &&
- <button class="button" onClick={() => image.current?.click()} ><Translate>Add</Translate></button>
- }
- {value &&
- <button class="button" onClick={() => onChange(undefined!)} ><Translate>Remove</Translate></button>
- }
+ 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>
- </div>
+ );
}
-
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputLocation.tsx b/packages/merchant-backoffice-ui/src/components/form/InputLocation.tsx
index bc90cf128..d4b13d555 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputLocation.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputLocation.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,29 +15,39 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
import { Fragment, h } from "preact";
-import { useTranslator } from "../../i18n/index.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Input } from "./Input.js";
-export function InputLocation({name}:{name:string}) {
- const i18n = useTranslator()
- return <>
- <Input name={`${name}.country`} label={i18n`Country`} />
- <Input name={`${name}.address_lines`} inputType="multiline"
- label={i18n`Address`}
- toStr={(v: string[] | undefined) => !v ? '' : v.join('\n')}
- fromStr={(v: string) => v.split('\n')}
- />
- <Input name={`${name}.building_number`} label={i18n`Building number`} />
- <Input name={`${name}.building_name`} label={i18n`Building name`} />
- <Input name={`${name}.street`} label={i18n`Street`} />
- <Input name={`${name}.post_code`} label={i18n`Post code`} />
- <Input name={`${name}.town_location`} label={i18n`Town location`} />
- <Input name={`${name}.town`} label={i18n`Town`} />
- <Input name={`${name}.district`} label={i18n`District`} />
- <Input name={`${name}.country_subdivision`} label={i18n`Country subdivision`} />
- </>
-} \ No newline at end of file
+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/merchant-backoffice-ui/src/components/form/InputNumber.tsx b/packages/merchant-backoffice-ui/src/components/form/InputNumber.tsx
index 7b7499f2d..38444b85d 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputNumber.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputNumber.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,9 +15,9 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
import { ComponentChildren, h } from "preact";
import { InputWithAddon } from "./InputWithAddon.js";
import { InputProps } from "./useField.js";
@@ -29,14 +29,33 @@ export interface Props<T> extends InputProps<T> {
children?: ComponentChildren;
}
-export function InputNumber<T>({ name, readonly, placeholder, tooltip, label, help, expand, children, side }: Props<keyof T>) {
- return <InputWithAddon<T> name={name} readonly={readonly}
- fromStr={(v) => !v ? undefined : parseInt(v, 10) } toStr={(v) => `${v}`}
- inputType='number' expand={expand}
- label={label} placeholder={placeholder} help={help} tooltip={tooltip}
- inputExtra={{ min: 0 }}
- children={children}
- side={side}
- />
+export function InputNumber<T>({
+ name,
+ readonly,
+ placeholder,
+ tooltip,
+ label,
+ help,
+ expand,
+ children,
+ side,
+}: Props<keyof T>) {
+ return (
+ <InputWithAddon<T>
+ name={name}
+ readonly={readonly}
+ fromStr={(v) => (!v ? undefined : parseInt(v, 10))}
+ toStr={(v) => `${v}`}
+ inputType="number"
+ expand={expand}
+ label={label}
+ placeholder={placeholder}
+ help={help}
+ tooltip={tooltip}
+ inputExtra={{ min: 0 }}
+ side={side}
+ >
+ {children}
+ </InputWithAddon>
+ );
}
-
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputPayto.tsx b/packages/merchant-backoffice-ui/src/components/form/InputPayto.tsx
index 703792936..fcecd8932 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputPayto.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputPayto.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,9 +15,9 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
import { h, VNode } from "preact";
import { InputArray } from "./InputArray.js";
import { PAYTO_REGEX } from "../../utils/constants.js";
@@ -25,15 +25,28 @@ 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}` }
- />
+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/merchant-backoffice-ui/src/components/form/InputPaytoForm.stories.tsx b/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.stories.tsx
new file mode 100644
index 000000000..cc5326bbe
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.stories.tsx
@@ -0,0 +1,47 @@
+/*
+ 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 { 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/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx b/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx
index e8022ca15..a0c15c77c 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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
@@ -18,9 +18,13 @@
*
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { h, VNode, Fragment } from "preact";
-import { useCallback, useState } from "preact/hooks";
-import { Translate, Translator, useTranslator } from "../../i18n/index.js";
+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";
@@ -28,21 +32,23 @@ 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;
+ path1?: string;
// path2 if the second field to be used, optional
path2?: string;
- // options of the payto uri
- options: {
+ // params of the payto uri
+ params: {
"receiver-name"?: string;
sender?: string;
message?: string;
@@ -69,24 +75,53 @@ function checkAddressChecksum(address: string) {
return true;
}
-function validateBitcoin(addr: string, i18n: Translator): string | undefined {
+function validateBitcoin_path1(
+ 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`This is not a valid bitcoin address.`;
+ return i18n.str`This is not a valid bitcoin address.`;
}
-function validateEthereum(addr: string, i18n: Translator): string | undefined {
+function validateEthereum_path1(
+ 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`This is not a valid Ethereum address.`;
+ return i18n.str`This is not a valid Ethereum address.`;
+}
+
+/**
+ * validates
+ * bank.com/
+ * bank.com
+ * bank.com/path
+ * bank.com/path/subpath/
+ */
+const DOMAIN_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9](?:\.[a-zA-Z]{2,})+(\/[a-zA-Z0-9-.]+)*\/?$/
+
+function validateTalerBank_path1(
+ addr: string,
+ i18n: ReturnType<typeof useTranslationContext>["i18n"],
+): string | undefined {
+ console.log(addr, DOMAIN_REGEX.test(addr))
+ try {
+ const valid = DOMAIN_REGEX.test(addr);
+ if (valid) return undefined;
+ } catch (e) {
+ console.log(e);
+ }
+ return i18n.str`This is not a valid host.`;
}
/**
@@ -103,12 +138,15 @@ function validateEthereum(addr: string, i18n: Translator): string | undefined {
* If the remainder is 1, the check digit test is passed and the IBAN might be valid.
*
*/
-function validateIBAN(iban: string, i18n: Translator): string | undefined {
+function validateIBAN_path1(
+ iban: string,
+ i18n: ReturnType<typeof useTranslationContext>["i18n"],
+): string | undefined {
// Check total length
if (iban.length < 4)
- return i18n`IBAN numbers usually have more that 4 digits`;
+ return i18n.str`IBAN numbers usually have more that 4 digits`;
if (iban.length > 34)
- return i18n`IBAN numbers usually have less that 34 digits`;
+ return i18n.str`IBAN numbers usually have less that 34 digits`;
const A_code = "A".charCodeAt(0);
const Z_code = "Z".charCodeAt(0);
@@ -116,7 +154,7 @@ function validateIBAN(iban: string, i18n: Translator): string | undefined {
// check supported country
const code = IBAN.substr(0, 2);
const found = code in COUNTRY_TABLE;
- if (!found) return i18n`IBAN country code not found`;
+ 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);
@@ -140,7 +178,8 @@ function validateIBAN(iban: string, i18n: Translator): string | undefined {
}
const checksum = calculate_iban_checksum(step3);
- if (checksum !== 1) return i18n`IBAN number is not valid, checksum is wrong`;
+ if (checksum !== 1)
+ return i18n.str`IBAN number is not valid, checksum is wrong`;
return undefined;
}
@@ -153,7 +192,10 @@ const targets = [
"ethereum",
];
const noTargetValue = targets[0];
-const defaultTarget = { target: noTargetValue, options: {} };
+const defaultTarget: Entity = {
+ target: noTargetValue,
+ params: {},
+};
export function InputPaytoForm<T>({
name,
@@ -161,52 +203,47 @@ export function InputPaytoForm<T>({
label,
tooltip,
}: Props<keyof T>): VNode {
- const { value: paytos, onChange } = useField<T>(name);
+ const { value: initialValueStr, onChange } = useField<T>(name);
- const [value, valueHandler] = useState<Partial<Entity>>(defaultTarget);
+ 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);
- let payToPath;
- if (value.target === "iban" && value.path1) {
- payToPath = `/${value.path1.toUpperCase()}`;
- } else if (value.path1) {
- if (value.path2) {
- payToPath = `/${value.path1}/${value.path2}`;
- } else {
- payToPath = `/${value.path1}`;
- }
- }
- const i18n = useTranslator();
-
- const ops = value.options!;
- const url = tryUrl(`payto://${value.target}${payToPath}`);
- if (url) {
- Object.keys(ops).forEach((opt_key) => {
- const opt_value = ops[opt_key];
- if (opt_value) url.searchParams.set(opt_key, opt_value);
- });
- }
- const paytoURL = !url ? "" : url.toString();
+ const { i18n } = useTranslationContext();
const errors: FormErrors<Entity> = {
- target: value.target === noTargetValue ? i18n`required` : undefined,
+ target: value.target === noTargetValue ? i18n.str`required` : undefined,
path1: !value.path1
- ? i18n`required`
+ ? 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,
+ ? validateIBAN_path1(value.path1, i18n)
+ : value.target === "bitcoin"
+ ? validateBitcoin_path1(value.path1, i18n)
+ : value.target === "ethereum"
+ ? validateEthereum_path1(value.path1, i18n)
+ : value.target === "x-taler-bank"
+ ? validateTalerBank_path1(value.path1, i18n)
+ : undefined,
path2:
value.target === "x-taler-bank"
? !value.path2
- ? i18n`required`
+ ? i18n.str`required`
: undefined
: undefined,
- options: undefinedIfEmpty({
- "receiver-name": !value.options?.["receiver-name"]
- ? i18n`required`
+ params: undefinedIfEmpty({
+ "receiver-name": !value.params?.["receiver-name"]
+ ? i18n.str`required`
: undefined,
}),
};
@@ -215,14 +252,51 @@ export function InputPaytoForm<T>({
(k) => (errors as any)[k] !== undefined,
);
- const submit = useCallback((): void => {
- const alreadyExists =
- paytos.findIndex((x: string) => x === paytoURL) !== -1;
- if (!alreadyExists) {
- onChange([paytoURL, ...paytos] as any);
- }
- valueHandler(defaultTarget);
- }, [value]);
+ const path1WithSlash = value.path1 && !value.path1.endsWith("/") ? value.path1 + "/" : value.path1
+ const str =
+ hasErrors || !value.target
+ ? undefined
+ : stringifyPaytoUri({
+ targetType: value.target,
+ targetPath: value.path2
+ ? `${path1WithSlash}${value.path2}`
+ : value.path1 ?? "",
+ params: value.params ?? ({} as any),
+ isKnown: false,
+ });
+ useEffect(() => {
+ onChange(str as any);
+ }, [str]);
+
+ // const submit = useCallback((): void => {
+ // // const accounts: TalerMerchantApi.AccountAddDetails[] = paytos;
+ // // const alreadyExists =
+ // // accounts.findIndex((x) => x.payto_uri === paytoURL) !== -1;
+ // // if (!alreadyExists) {
+ // const newValue: TalerMerchantApi.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 (
@@ -231,27 +305,30 @@ export function InputPaytoForm<T>({
name="tax"
errors={errors}
object={value}
- valueHandler={valueHandler}
+ valueHandler={setValue}
>
<InputSelector<Entity>
name="target"
- label={i18n`Target type`}
- tooltip={i18n`Method to use for wire transfer`}
+ label={i18n.str`Account type`}
+ tooltip={i18n.str`Method to use for wire transfer`}
values={targets}
- toStr={(v) => (v === noTargetValue ? i18n`Choose one...` : v)}
+ readonly={readonly}
+ toStr={(v) => (v === noTargetValue ? i18n.str`Choose one...` : v)}
/>
{value.target === "ach" && (
<Fragment>
<Input<Entity>
name="path1"
- label={i18n`Routing`}
- tooltip={i18n`Routing number.`}
+ label={i18n.str`Routing`}
+ readonly={readonly}
+ tooltip={i18n.str`Routing number.`}
/>
<Input<Entity>
name="path2"
- label={i18n`Account`}
- tooltip={i18n`Account number.`}
+ label={i18n.str`Account`}
+ readonly={readonly}
+ tooltip={i18n.str`Account number.`}
/>
</Fragment>
)}
@@ -259,8 +336,9 @@ export function InputPaytoForm<T>({
<Fragment>
<Input<Entity>
name="path1"
- label={i18n`Code`}
- tooltip={i18n`Business Identifier Code.`}
+ label={i18n.str`Code`}
+ readonly={readonly}
+ tooltip={i18n.str`Business Identifier Code.`}
/>
</Fragment>
)}
@@ -268,8 +346,10 @@ export function InputPaytoForm<T>({
<Fragment>
<Input<Entity>
name="path1"
- label={i18n`Account`}
- tooltip={i18n`Bank Account Number.`}
+ label={i18n.str`IBAN`}
+ tooltip={i18n.str`International Bank Account Number.`}
+ readonly={readonly}
+ placeholder="DE1231231231"
inputExtra={{ style: { textTransform: "uppercase" } }}
/>
</Fragment>
@@ -278,8 +358,9 @@ export function InputPaytoForm<T>({
<Fragment>
<Input<Entity>
name="path1"
- label={i18n`Account`}
- tooltip={i18n`Unified Payment Interface.`}
+ readonly={readonly}
+ label={i18n.str`Account`}
+ tooltip={i18n.str`Unified Payment Interface.`}
/>
</Fragment>
)}
@@ -287,8 +368,9 @@ export function InputPaytoForm<T>({
<Fragment>
<Input<Entity>
name="path1"
- label={i18n`Address`}
- tooltip={i18n`Bitcoin protocol.`}
+ readonly={readonly}
+ label={i18n.str`Address`}
+ tooltip={i18n.str`Bitcoin protocol.`}
/>
</Fragment>
)}
@@ -296,8 +378,9 @@ export function InputPaytoForm<T>({
<Fragment>
<Input<Entity>
name="path1"
- label={i18n`Address`}
- tooltip={i18n`Ethereum protocol.`}
+ readonly={readonly}
+ label={i18n.str`Address`}
+ tooltip={i18n.str`Ethereum protocol.`}
/>
</Fragment>
)}
@@ -305,8 +388,9 @@ export function InputPaytoForm<T>({
<Fragment>
<Input<Entity>
name="path1"
- label={i18n`Address`}
- tooltip={i18n`Interledger protocol.`}
+ readonly={readonly}
+ label={i18n.str`Address`}
+ tooltip={i18n.str`Interledger protocol.`}
/>
</Fragment>
)}
@@ -315,73 +399,49 @@ export function InputPaytoForm<T>({
<Fragment>
<Input<Entity>
name="path1"
- label={i18n`Host`}
- tooltip={i18n`Bank host.`}
+ readonly={readonly}
+ label={i18n.str`Host`}
+ fromStr={(v) => {
+ if (v.startsWith("http")) {
+ try {
+ const url = new URL(v);
+ return url.host + url.pathname;
+ } catch {
+ return v;
+ }
+ }
+ return v;
+ }}
+ tooltip={i18n.str`Bank host.`}
+ help={<Fragment>
+ <div><i18n.Translate>Without scheme and may include subpath:</i18n.Translate></div>
+ <div>bank.com/</div>
+ <div>bank.com/path/subpath/</div>
+ </Fragment>}
/>
<Input<Entity>
name="path2"
- label={i18n`Account`}
- tooltip={i18n`Bank account.`}
+ readonly={readonly}
+ label={i18n.str`Account`}
+ tooltip={i18n.str`Bank account.`}
/>
</Fragment>
)}
+ {/**
+ * Show additional fields apart from the payto
+ */}
{value.target !== noTargetValue && (
- <Input
- name="options.receiver-name"
- label={i18n`Name`}
- tooltip={i18n`Bank account owner's name.`}
- />
- )}
-
- <div class="field is-horizontal">
- <div class="field-label is-normal" />
- <div class="field-body" style={{ display: "block" }}>
- {paytos.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%" }}
- >
- {v}
- </span>
- <a
- class="tag is-medium is-danger is-delete mb-0"
- onClick={() => {
- onChange(paytos.filter((f: any) => f !== v) as any);
- }}
- />
- </div>
- ))}
- {!paytos.length && i18n`No accounts yet.`}
- </div>
- </div>
-
- {value.target !== noTargetValue && (
- <div class="buttons is-right mt-5">
- <button
- class="button is-info"
- data-tooltip={i18n`add tax to the tax list`}
- disabled={hasErrors}
- onClick={submit}
- >
- <Translate>Add</Translate>
- </button>
- </div>
+ <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>
);
}
-
-function tryUrl(s: string): URL | undefined {
- try {
- return new URL(s);
- } catch (e) {
- return undefined;
- }
-}
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputSearchOnList.tsx b/packages/merchant-backoffice-ui/src/components/form/InputSearchOnList.tsx
new file mode 100644
index 000000000..9956a6427
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/components/form/InputSearchOnList.tsx
@@ -0,0 +1,204 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import 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/merchant-backoffice-ui/src/components/form/InputSearchProduct.tsx b/packages/merchant-backoffice-ui/src/components/form/InputSearchProduct.tsx
deleted file mode 100644
index 2b239d483..000000000
--- a/packages/merchant-backoffice-ui/src/components/form/InputSearchProduct.tsx
+++ /dev/null
@@ -1,138 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-import { h, VNode } from "preact";
-import { useState } from "preact/hooks";
-import emptyImage from "../../assets/empty.png";
-import { MerchantBackend, WithId } from "../../declaration.js";
-import { Translate, useTranslator } from "../../i18n/index.js";
-import { FormErrors, FormProvider } from "./FormProvider.js";
-import { InputWithAddon } from "./InputWithAddon.js";
-
-type Entity = MerchantBackend.Products.ProductDetail & WithId
-
-export interface Props {
- selected?: Entity;
- onChange: (p?: Entity) => void;
- products: (MerchantBackend.Products.ProductDetail & WithId)[],
-}
-
-interface ProductSearch {
- name: string;
-}
-
-export function InputSearchProduct({ selected, onChange, products }: Props): VNode {
- const [prodForm, setProdName] = useState<Partial<ProductSearch>>({ name: '' })
-
- const errors: FormErrors<ProductSearch> = {
- name: undefined
- }
- const i18n = useTranslator()
-
-
- if (selected) {
- return <article class="media">
- <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"><Translate>Product id</Translate>: <b>{selected.id}</b></p>
- <p><Translate>Description</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<ProductSearch> errors={errors} object={prodForm} valueHandler={setProdName} >
-
- <InputWithAddon<ProductSearch>
- name="name"
- label={i18n`Product`}
- tooltip={i18n`search products by it's description or id`}
- addonAfter={<span class="icon" ><i class="mdi mdi-magnify" /></span>}
- >
- <div>
- <ProductList
- name={prodForm.name}
- list={products}
- onSelect={(p) => {
- setProdName({ name: '' })
- onChange(p)
- }}
- />
- </div>
- </InputWithAddon>
-
- </FormProvider>
-
-}
-
-interface ProductListProps {
- name?: string;
- onSelect: (p: MerchantBackend.Products.ProductDetail & WithId) => void;
- list: (MerchantBackend.Products.ProductDetail & WithId)[]
-}
-
-function ProductList({ name, onSelect, list }: ProductListProps) {
- 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" >
- <Translate>no products found with that description</Translate>
- </div> :
- filtered.map(p => (
- <div key={p.id} class="dropdown-item" onClick={() => onSelect(p)} style={{ cursor: 'pointer' }}>
- <article class="media">
- <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> <small>{p.price}</small>
- <br />
- {p.description}
- </p>
- </div>
- </div>
- </article>
- </div>
- ))
- }
- </div>
- </div>
- </div>
-}
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputSecured.stories.tsx b/packages/merchant-backoffice-ui/src/components/form/InputSecured.stories.tsx
index 72355e242..4de84d984 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputSecured.stories.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputSecured.stories.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,41 +15,47 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { h, VNode } from 'preact';
-import { useState } from 'preact/hooks';
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
import { FormProvider } from "./FormProvider.js";
-import { InputSecured } from './InputSecured.js';
+import { InputSecured } from "./InputSecured.js";
export default {
- title: 'Components/Form/InputSecured',
+ title: "Components/Form/InputSecured",
component: InputSecured,
};
-type T = { auth_token: string | null }
+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>
-}
+ 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>
-}
+ 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>
-}
+ 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/merchant-backoffice-ui/src/components/form/InputSecured.tsx b/packages/merchant-backoffice-ui/src/components/form/InputSecured.tsx
index 17431fcfc..4a35ad96c 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputSecured.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputSecured.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,105 +15,172 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @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 { Translate, useTranslator } from "../../i18n/index.js";
import { InputProps, useField } from "./useField.js";
export type Props<T> = InputProps<T>;
const TokenStatus = ({ prev, post }: any) => {
- 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"><Translate>Deleting</Translate></span> :
- <span class="tag is-warning is-align-self-center ml-2"><Translate>Changing</Translate></span>
+ 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 {
+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 [newValue, setNuewValue] = useState("");
- const i18n = useTranslator()
+ 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><Translate>Manage access token</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><Translate>Update</Translate></span>
- </button>
- </div>
- </div>
- </Fragment>
- }
- {error ? <p class="help is-danger">{error}</p> : null}
- </div>
- </div>
- {active &&
+ 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="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><Translate>Remove</Translate></span>
+ {!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>
- <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><Translate>Cancel</Translate></span>
- </button>
+ </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>
- </div>
- }
- </Fragment >;
+ )}
+ </Fragment>
+ );
}
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputSelector.tsx b/packages/merchant-backoffice-ui/src/components/form/InputSelector.tsx
index f2574240b..f567f7247 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputSelector.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputSelector.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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
@@ -24,7 +24,7 @@ import { InputProps, useField } from "./useField.js";
interface Props<T> extends InputProps<T> {
readonly?: boolean;
expand?: boolean;
- values: string[];
+ values: any[];
toStr?: (v?: any) => string;
fromStr?: (s: string) => any;
}
@@ -41,10 +41,10 @@ export function InputSelector<T>({
label,
help,
values,
+ fromStr = defaultFromString,
toStr = defaultToString,
}: Props<keyof T>): VNode {
- const { error, value, onChange } = useField<T>(name);
-
+ const { error, value, onChange, required } = useField<T>(name);
return (
<div class="field is-horizontal">
<div class="field-label is-normal">
@@ -58,26 +58,34 @@ export function InputSelector<T>({
</label>
</div>
<div class="field-body is-flex-grow-3">
- <div class="field">
- <p class={expand ? "control is-expanded select" : "control select"}>
+ <div class="field has-icons-right">
+ <p class={expand ? "control is-expanded select" : "control select "}>
<select
class={error ? "select is-danger" : "select"}
name={String(name)}
disabled={readonly}
readonly={readonly}
onChange={(e) => {
- onChange(e.currentTarget.value as any);
+ onChange(fromStr(e.currentTarget.value));
}}
>
{placeholder && <option>{placeholder}</option>}
- {values.map((v, i) => (
- <option key={i} value={v} selected={value === v}>
- {toStr(v)}
- </option>
- ))}
+ {values.map((v, i) => {
+ return (
+ <option key={i} value={v} selected={value === v}>
+ {toStr(v)}
+ </option>
+ );
+ })}
</select>
+
{help}
</p>
+ {required && (
+ <span class="icon has-text-danger is-right" style={{height: "2.5em"}}>
+ <i class="mdi mdi-alert" />
+ </span>
+ )}
{error && <p class="help is-danger">{error}</p>}
</div>
</div>
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputStock.stories.tsx b/packages/merchant-backoffice-ui/src/components/form/InputStock.stories.tsx
index bfd607c4e..d7cf04553 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputStock.stories.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputStock.stories.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2021-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputStock.tsx b/packages/merchant-backoffice-ui/src/components/form/InputStock.tsx
index 8e9b56f62..8104d1f9f 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputStock.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputStock.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,34 +15,32 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { TalerMerchantApi, TalerProtocolTimestamp } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, h } from "preact";
-import { MerchantBackend, Timestamp } from "../../declaration.js";
-import { InputProps, useField } from "./useField.js";
-import { FormProvider, FormErrors } from "./FormProvider.js";
import { useLayoutEffect, useState } from "preact/hooks";
-import { Input } from "./Input.js";
-import { InputGroup } from "./InputGroup.js";
-import { InputNumber } from "./InputNumber.js";
+import { FormErrors, FormProvider } from "./FormProvider.js";
import { InputDate } from "./InputDate.js";
-import { Translate, useTranslator } from "../../i18n/index.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
+type Entity = Stock;
export interface Stock {
current: number;
lost: number;
sold: number;
- address?: MerchantBackend.Location;
- nextRestock?: Timestamp;
+ address?: TalerMerchantApi.Location;
+ nextRestock?: TalerProtocolTimestamp;
}
interface StockDelta {
@@ -50,93 +48,135 @@ interface StockDelta {
lost: number;
}
-
-export function InputStock<T>({ name, tooltip, label, alreadyExist }: Props<keyof T>) {
+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 = useTranslator()
+ 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)
+ onChange(undefined as any);
} else {
onChange({
...formValue,
current: (formValue?.current || 0) + addedStock.incoming,
- lost: (formValue?.lost || 0) + addedStock.lost
- } as any)
+ lost: (formValue?.lost || 0) + addedStock.lost,
+ } as any);
}
- }, [formValue, addedStock])
+ }, [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`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><Translate>Manage stock</Translate></span>
- </button> : <button class="button"
- data-tooltip={i18n`this product has been configured without stock control`}
- disabled >
- <span><Translate>Infinite</Translate></span>
- </button>
- }
+ 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>
- </div>
- </Fragment >
+ </Fragment>
+ );
}
- const currentStock = (formValue.current || 0) - (formValue.lost || 0) - (formValue.sold || 0)
+ const currentStock =
+ (formValue.current || 0) - (formValue.lost || 0) - (formValue.sold || 0);
const stockAddedErrors: FormErrors<typeof addedStock> = {
- lost: currentStock + addedStock.incoming < addedStock.lost ?
- i18n`lost cannot be greater than current and incoming (max ${currentStock + addedStock.incoming})`
- : undefined
- }
+ 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`current stock will change from ${currentStock} to ${currentStock + addedStock.incoming - addedStock.lost}` :
- // i18n`current stock will stay at ${currentStock}`
+ // 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`Incoming`} />
- <InputNumber name="lost" label={i18n`Lost`} />
- </FormProvider>
-
- {/* <div class="field is-horizontal">
+ 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">
@@ -144,28 +184,40 @@ export function InputStock<T>({ name, tooltip, label, alreadyExist }: Props<keyo
</div>
</div>
</div> */}
-
- </Fragment> : <InputNumber<Entity> name="current"
- label={i18n`Current`}
- side={
- <button class="button is-danger"
- data-tooltip={i18n`remove stock control for this product`}
- onClick={(): void => { valueHandler(undefined as any) }} >
- <span><Translate>without stock</Translate></span>
- </button>
- }
- />}
-
- <InputDate<Entity> name="nextRestock" label={i18n`Next restock`} withTimestampSupport />
-
- <InputGroup<Entity> name="address" label={i18n`Delivery address`}>
- <InputLocation name="address" />
- </InputGroup>
- </FormProvider>
+ </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>
- </div>
- </Fragment>
+ </Fragment>
+ );
}
- // (
-
-
+// (
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputTab.tsx b/packages/merchant-backoffice-ui/src/components/form/InputTab.tsx
new file mode 100644
index 000000000..1cd88d31a
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/components/form/InputTab.tsx
@@ -0,0 +1,90 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @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/merchant-backoffice-ui/src/components/form/InputTaxes.tsx b/packages/merchant-backoffice-ui/src/components/form/InputTaxes.tsx
index f7e983460..4392c7659 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputTaxes.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputTaxes.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,83 +15,133 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @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 { Translate, useTranslator } from "../../i18n/index.js";
+import * as yup from "yup";
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";
+import { TalerMerchantApi } from "@gnu-taler/taler-util";
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);
+type Entity = TalerMerchantApi.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 [value, valueHandler] = useState<Partial<Entity>>({});
// const [errors, setErrors] = useState<FormErrors<Entity>>({})
- let errors: FormErrors<Entity> = {}
+ let errors: FormErrors<Entity> = {};
try {
- schema.validateSync(value, { abortEarly: false })
+ 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 yupErrors = err.inner as yup.ValidationError[];
+ errors = yupErrors.reduce(
+ (prev, cur) =>
+ !cur.path ? prev : { ...prev, [cur.path]: cur.message },
+ {},
+ );
}
}
- const hasErrors = Object.keys(errors).some(k => (errors as any)[k] !== undefined)
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
const submit = useCallback((): void => {
- onChange([value as any, ...taxes] as any)
- valueHandler({})
- }, [value])
+ onChange([value as any, ...taxes] as any);
+ valueHandler({});
+ }, [value]);
- const i18n = useTranslator()
+ 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} >
-
+ <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`No taxes configured for this product.`}
+ <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`Amount`} tooltip={i18n`Taxes can be in currencies that differ from the main currency used by the merchant.`}>
- <Translate>Enter currency and value separated with a colon, e.g. "USD:2.3".</Translate>
+ <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`Description`} tooltip={i18n`Legal name of the tax, e.g. VAT or import duties.`} />
+ <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`add tax to the tax list`}
+ <button
+ class="button is-info"
+ data-tooltip={i18n.str`add tax to the tax list`}
disabled={hasErrors}
- onClick={submit}><Translate>Add</Translate></button>
+ onClick={submit}
+ >
+ <i18n.Translate>Add</i18n.Translate>
+ </button>
</div>
</FormProvider>
</InputGroup>
- )
+ );
}
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx b/packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx
new file mode 100644
index 000000000..8c935f33b
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx
@@ -0,0 +1,91 @@
+/*
+ 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 { h, VNode } from "preact";
+import { InputProps, useField } from "./useField.js";
+
+interface Props<T> extends InputProps<T> {
+ name: T;
+ readonly?: boolean;
+ expand?: boolean;
+ threeState?: boolean;
+ toBoolean?: (v?: any) => boolean | undefined;
+ fromBoolean?: (s: boolean | undefined) => any;
+}
+
+const defaultToBoolean = (f?: any): boolean | undefined => f || "";
+const defaultFromBoolean = (v: boolean | undefined): any => v as any;
+
+export function InputToggle<T>({
+ name,
+ readonly,
+ placeholder,
+ tooltip,
+ label,
+ help,
+ threeState,
+ expand,
+ fromBoolean = defaultFromBoolean,
+ toBoolean = defaultToBoolean,
+}: Props<keyof T>): VNode {
+ const { error, value, onChange } = useField<T>(name);
+
+ const onCheckboxClick = (): void => {
+ const c = toBoolean(value);
+ if (c === false && threeState) return onChange(undefined as any);
+ return onChange(fromBoolean(!c));
+ };
+
+ return (
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label" >
+ {label}
+ {tooltip && (
+ <span class="icon has-tooltip-right" data-tooltip={tooltip}>
+ <i class="mdi mdi-information" />
+ </span>
+ )}
+ </label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class={expand ? "control is-expanded" : "control"}>
+ <label class="toggle" style={{ marginLeft: 4, marginTop: 0 }}>
+ <input
+ type="checkbox"
+ class={toBoolean(value) === undefined ? "is-indeterminate" : "toggle-checkbox"}
+ checked={toBoolean(value)}
+ placeholder={placeholder}
+ readonly={readonly}
+ name={String(name)}
+ disabled={readonly}
+ onChange={onCheckboxClick}
+ />
+ <div class={`toggle-switch ${readonly ? "disabled" : ""}`} style={{ cursor: readonly ? "default" : undefined }}></div>
+ </label>
+ {help}
+ </p>
+ {error && <p class="help is-danger">{error}</p>}
+ </div>
+ </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 16fc5d611..b8cd4c2d2 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputWithAddon.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputWithAddon.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,63 +15,102 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
import { ComponentChildren, h, VNode } from "preact";
import { InputProps, useField } from "./useField.js";
export interface Props<T> extends InputProps<T> {
expand?: boolean;
- inputType?: 'text' | 'number';
+ inputType?: "text" | "number" | "password";
addonBefore?: ComponentChildren;
addonAfter?: ComponentChildren;
+ addonAfterAction?: () => void;
toStr?: (v?: any) => string;
fromStr?: (s: string) => any;
- inputExtra?: any,
- children?: ComponentChildren,
+ inputExtra?: any;
+ children?: ComponentChildren;
side?: ComponentChildren;
}
-const defaultToString = (f?: any): string => f || ''
-const defaultFromString = (v: string): any => v as any
+const defaultToString = (f?: any): string => f || "";
+const defaultFromString = (v: string): any => v as any;
-export function InputWithAddon<T>({ name, readonly, addonBefore, children, expand, label, placeholder, help, tooltip, inputType, inputExtra, side, addonAfter, toStr = defaultToString, fromStr = defaultFromString }: Props<keyof T>): VNode {
+export function InputWithAddon<T>({
+ name,
+ readonly,
+ addonBefore,
+ children,
+ expand,
+ label,
+ placeholder,
+ help,
+ tooltip,
+ inputType,
+ inputExtra,
+ side,
+ addonAfter,
+ addonAfterAction,
+ toStr = defaultToString,
+ fromStr = defaultFromString,
+}: Props<keyof T>): VNode {
const { error, value, onChange, required } = useField<T>(name);
- return <div class="field is-horizontal">
- <div class="field-label is-normal">
- <label class="label">
- {label}
- {tooltip && <span class="icon has-tooltip-right" data-tooltip={tooltip}>
- <i class="mdi mdi-information" />
- </span>}
- </label>
- </div>
- <div class="field-body is-flex-grow-3">
- <div class="field">
- <div class="field has-addons">
- {addonBefore && <div class="control">
- <a class="button is-static">{addonBefore}</a>
- </div>}
- <p class={`control${expand ? " is-expanded" :""}${required ? " has-icons-right" : ''}`}>
- <input {...(inputExtra || {})} class={error ? "input is-danger" : "input"} type={inputType}
- placeholder={placeholder} readonly={readonly}
- name={String(name)} value={toStr(value)}
- onChange={(e): void => onChange(fromStr(e.currentTarget.value))} />
- {required && <span class="icon has-text-danger is-right">
- <i class="mdi mdi-alert" />
- </span>}
- {help}
- {children}
- </p>
- {addonAfter && <div class="control">
- <a class="button is-static">{addonAfter}</a>
- </div>}
+ return (
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ {label}
+ {tooltip && (
+ <span class="icon has-tooltip-right" data-tooltip={tooltip}>
+ <i class="mdi mdi-information" />
+ </span>
+ )}
+ </label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <div class="field has-addons">
+ {addonBefore && (
+ <div class="control">
+ <a class="button is-static">{addonBefore}</a>
+ </div>
+ )}
+ <p
+ class={`control${expand ? " is-expanded" : ""}${required ? " has-icons-right" : ""
+ }`}
+ >
+ <input
+ {...(inputExtra || {})}
+ class={error ? "input is-danger" : "input"}
+ type={inputType}
+ placeholder={placeholder}
+ readonly={readonly}
+ disabled={readonly}
+ name={String(name)}
+ value={toStr(value)}
+ onChange={(e): void => onChange(fromStr(e.currentTarget.value))}
+ />
+ {required && (
+ <span class="icon has-text-danger is-right">
+ <i class="mdi mdi-alert" />
+ </span>
+ )}
+ {children}
+ </p>
+ {addonAfter && (
+ <div class="control" onClick={addonAfterAction} style={{ cursor: addonAfterAction ? "pointer" : undefined }}>
+ <a class="button is-static">{addonAfter}</a>
+ </div>
+ )}
+ </div>
+ {error && <p class="help is-danger">{error}</p>}
+ <span class="has-text-grey">{help}</span>
</div>
- {error && <p class="help is-danger">{error}</p>}
+ {expand ? <div>{side}</div> : side}
</div>
- {side}
+
</div>
- </div>;
+ );
}
diff --git a/packages/merchant-backoffice-ui/src/components/form/JumpToElementById.tsx b/packages/merchant-backoffice-ui/src/components/form/JumpToElementById.tsx
new file mode 100644
index 000000000..f5f9d5b4f
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/components/form/JumpToElementById.tsx
@@ -0,0 +1,63 @@
+import { TranslatedString } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+
+export function JumpToElementById({ testIfExist, onSelect, placeholder, description }: { placeholder: TranslatedString, description: TranslatedString, testIfExist: (id: string) => Promise<boolean>, onSelect: (id: string) => void }): VNode {
+ const { i18n } = useTranslationContext()
+
+ 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 {
+ const exi = await testIfExist(currentId);
+ if (exi) {
+ onSelect(currentId);
+ setError(undefined);
+ } else {
+ setError(i18n.str`not found`);
+ }
+ } 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/merchant-backoffice-ui/src/components/form/TextField.tsx b/packages/merchant-backoffice-ui/src/components/form/TextField.tsx
index 3dd157b79..8f897c2d8 100644
--- a/packages/merchant-backoffice-ui/src/components/form/TextField.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/TextField.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,39 +15,57 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @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';
+ 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 {
+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>}
+ 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>
- {side}
</div>
- </div>;
+ );
}
diff --git a/packages/merchant-backoffice-ui/src/components/form/useField.tsx b/packages/merchant-backoffice-ui/src/components/form/useField.tsx
index a6e5a01a8..49bba4984 100644
--- a/packages/merchant-backoffice-ui/src/components/form/useField.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/useField.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,11 +15,12 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
import { ComponentChildren, VNode } from "preact";
+import { useState } from "preact/hooks";
import { useFormContext } from "./FormProvider.js";
interface Use<V> {
@@ -29,26 +30,29 @@ interface Use<V> {
initial: any;
onChange: (v: V) => void;
toStr: (f: V | undefined) => string;
- fromStr: (v: string) => V
+ fromStr: (v: string) => V;
}
export function useField<T>(name: keyof T): Use<T[typeof name]> {
- const { errors, object, initialObject, toStr, fromStr, valueHandler } = useFormContext<T>()
- type P = typeof name
- type V = T[P]
-
- const updateField = (field: P) => (value: V): void => {
- return valueHandler((prev) => {
- return setValueDeeper(prev, String(field).split('.'), value)
- })
- }
+ const { errors, object, initialObject, toStr, fromStr, valueHandler } =
+ useFormContext<T>();
+ type P = typeof name;
+ type V = T[P];
+ const [isDirty, setDirty] = useState(false);
+ const updateField =
+ (field: P) =>
+ (value: V): void => {
+ setDirty(true);
+ return valueHandler((prev) => {
+ return setValueDeeper(prev, String(field).split("."), value);
+ });
+ };
- const defaultToString = ((f?: V): string => String(!f ? '' : f))
- const defaultFromString = ((v: string): V => v as any)
- const value = readField(object, String(name))
- const initial = readField(initialObject, String(name))
- const isDirty = value !== initial
- const hasError = readField(errors, String(name))
+ const defaultToString = (f?: V): string => String(!f ? "" : f);
+ const defaultFromString = (v: string): V => v as any;
+ const value = readField(object, String(name));
+ const initial = readField(initialObject, String(name));
+ const hasError = readField(errors, String(name));
return {
error: isDirty ? hasError : undefined,
required: !isDirty && hasError,
@@ -57,24 +61,26 @@ export function useField<T>(name: keyof T): Use<T[typeof name]> {
onChange: updateField(name) as any,
toStr: toStr[name] ? toStr[name]! : defaultToString,
fromStr: fromStr[name] ? fromStr[name]! : defaultFromString,
- }
+ };
}
/**
* read the field of an object an support accessing it using '.'
- *
- * @param object
- * @param name
- * @returns
+ *
+ * @param object
+ * @param name
+ * @returns
*/
const readField = (object: any, name: string) => {
- return name.split('.').reduce((prev, current) => prev && prev[current], object)
-}
+ return name
+ .split(".")
+ .reduce((prev, current) => prev && prev[current], object);
+};
const setValueDeeper = (object: any, names: string[], value: any): any => {
- if (names.length === 0) return value
- const [head, ...rest] = names
- return { ...object, [head]: setValueDeeper(object[head] || {}, rest, value) }
-}
+ if (names.length === 0) return value;
+ const [head, ...rest] = names;
+ return { ...object, [head]: setValueDeeper(object[head] || {}, rest, value) };
+};
export interface InputProps<T> {
name: T;
@@ -83,4 +89,4 @@ export interface InputProps<T> {
tooltip?: ComponentChildren;
readonly?: boolean;
help?: ComponentChildren;
-} \ No newline at end of file
+}
diff --git a/packages/merchant-backoffice-ui/src/components/form/useGroupField.tsx b/packages/merchant-backoffice-ui/src/components/form/useGroupField.tsx
index d2bb021cf..4fbfc4a75 100644
--- a/packages/merchant-backoffice-ui/src/components/form/useGroupField.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/useGroupField.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,9 +15,9 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
import { useFormContext } from "./FormProvider.js";
@@ -27,14 +27,15 @@ interface Use {
export function useGroupField<T>(name: keyof T): Use {
const f = useFormContext<T>();
- if (!f)
- return {};
+ if (!f) return {};
return {
- hasError: readField(f.errors, String(name))
+ hasError: readField(f.errors, String(name)),
};
}
const readField = (object: any, name: string) => {
- return name.split('.').reduce((prev, current) => prev && prev[current], object)
-}
+ return name
+ .split(".")
+ .reduce((prev, current) => prev && prev[current], object);
+};
diff --git a/packages/merchant-backoffice-ui/src/components/index.stories.ts b/packages/merchant-backoffice-ui/src/components/index.stories.ts
new file mode 100644
index 000000000..f96defc09
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/components/index.stories.ts
@@ -0,0 +1,17 @@
+/*
+ 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/>
+ */
+
+export * as payto from "./form/InputPaytoForm.stories.js";
diff --git a/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx b/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx
index a32eb9088..864d09f48 100644
--- a/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx
+++ b/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2021-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -19,18 +19,19 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { Fragment, h, VNode } from "preact";
-import { useBackendContext } from "../../context/backend.js";
-import { useTranslator } from "../../i18n/index.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useSessionContext } from "../../context/session.js";
import { Entity } from "../../paths/admin/create/CreatePage.js";
import { Input } from "../form/Input.js";
-import { InputCurrency } from "../form/InputCurrency.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 { InputPaytoForm } from "../form/InputPaytoForm.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,
@@ -39,95 +40,93 @@ export function DefaultInstanceFormFields({
readonlyId?: boolean;
showId: boolean;
}): VNode {
- const i18n = useTranslator();
- const backend = useBackendContext();
+ const { i18n } = useTranslationContext();
+ const { state } = useSessionContext();
return (
<Fragment>
{showId && (
<InputWithAddon<Entity>
name="id"
- addonBefore={`${backend.url}/instances/`}
+ addonBefore={new URL("instances/", state.backendUrl.href).href}
readonly={readonlyId}
- label={i18n`Identifier`}
- tooltip={i18n`Name of the instance in URLs. The 'default' instance is special in that it is used to administer other instances.`}
+ 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`Business name`}
- tooltip={i18n`Legal name of the business represented by this instance.`}
+ label={i18n.str`Business name`}
+ tooltip={i18n.str`Legal name of the business represented by this instance.`}
+ />
+
+ <TextField name="asdasd" label="">
+ <i18n.Translate>
+ Choose individual if you don't have or are not required to have legal business permission.
+ </i18n.Translate>
+ </TextField>
+
+ <InputSelector<Entity>
+ name="user_type"
+ label={i18n.str`Selling as`}
+ tooltip={i18n.str`Different type of account can have different rules and requirements.`}
+ values={["business", "individual"]}
+ toStr={(d: string) => {
+ return d.toUpperCase();
+ }}
/>
<Input<Entity>
name="email"
- label={i18n`Email`}
- tooltip={i18n`Contact email`}
+ label={i18n.str`Email`}
+ tooltip={i18n.str`Contact email`}
/>
<Input<Entity>
name="website"
- label={i18n`Website URL`}
- tooltip={i18n`URL.`}
+ label={i18n.str`Website URL`}
+ tooltip={i18n.str`URL.`}
/>
<InputImage<Entity>
name="logo"
- label={i18n`Logo`}
- tooltip={i18n`Logo image.`}
- />
-
- <InputPaytoForm<Entity>
- name="payto_uris"
- label={i18n`Bank account`}
- tooltip={i18n`URI specifying bank account for crediting revenue.`}
- />
-
- <InputCurrency<Entity>
- name="default_max_deposit_fee"
- label={i18n`Default max deposit fee`}
- tooltip={i18n`Maximum deposit fees this merchant is willing to pay per order by default.`}
- />
-
- <InputCurrency<Entity>
- name="default_max_wire_fee"
- label={i18n`Default max wire fee`}
- tooltip={i18n`Maximum wire fees this merchant is willing to pay per wire transfer by default.`}
- />
-
- <Input<Entity>
- name="default_wire_fee_amortization"
- label={i18n`Default wire fee amortization`}
- tooltip={i18n`Number of orders excess wire transfer fees will be divided by to compute per order surcharge.`}
+ label={i18n.str`Logo`}
+ tooltip={i18n.str`Logo image.`}
/>
<InputGroup
name="address"
- label={i18n`Address`}
- tooltip={i18n`Physical location of the merchant.`}
+ label={i18n.str`Address`}
+ tooltip={i18n.str`Physical location of the merchant.`}
>
<InputLocation name="address" />
</InputGroup>
<InputGroup
name="jurisdiction"
- label={i18n`Jurisdiction`}
- tooltip={i18n`Jurisdiction for legal disputes with the merchant.`}
+ 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.`}
+ />
+
<InputDuration<Entity>
name="default_pay_delay"
- label={i18n`Default payment delay`}
+ label={i18n.str`Default payment delay`}
withForever
- tooltip={i18n`Time customers have to pay an order before the offer expires by default.`}
+ 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`Default wire transfer delay`}
- tooltip={i18n`Maximum time an exchange is allowed to delay wiring funds to the merchant, enabling it to aggregate smaller payments into larger wire transfers and reducing wire fees.`}
+ 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/merchant-backoffice-ui/src/components/menu/LangSelector.tsx b/packages/merchant-backoffice-ui/src/components/menu/LangSelector.tsx
index 4f35e3f76..a6cd8014d 100644
--- a/packages/merchant-backoffice-ui/src/components/menu/LangSelector.tsx
+++ b/packages/merchant-backoffice-ui/src/components/menu/LangSelector.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,59 +15,78 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
-import langIcon from '../../assets/icons/languageicon.svg';
-import { useTranslationContext } from "../../context/translation.js";
-import { strings as messages } from '../../i18n/strings'
+import langIcon from "../../assets/icons/languageicon.svg";
+import { strings as messages } from "../../i18n/strings.js";
type LangsNames = {
- [P in keyof typeof messages]: string
-}
+ [P in keyof typeof messages]: string;
+};
const names: LangsNames = {
- es: 'Español [es]',
- en: 'English [en]',
- fr: 'Français [fr]',
- de: 'Deutsch [de]',
- sv: 'Svenska [sv]',
- it: 'Italiano [it]',
-}
+ es: "Español [es]",
+ en: "English [en]",
+ fr: "Français [fr]",
+ de: "Deutsch [de]",
+ sv: "Svenska [sv]",
+ it: "Italiano [it]",
+};
function getLangName(s: keyof LangsNames | string) {
- if (names[s]) return names[s]
- return s
+ if (names[s]) return names[s];
+ return s;
}
export function LangSelector(): VNode {
- const [updatingLang, setUpdatingLang] = useState(false)
- const { lang, changeLanguage } = useTranslationContext()
+ const [updatingLang, setUpdatingLang] = useState(false);
+ const { lang, changeLanguage } = useTranslationContext();
- return <div class="dropdown is-active ">
- <div class="dropdown-trigger">
- <button class="button has-tooltip-left"
- data-tooltip="change language selection"
- aria-haspopup="true"
- aria-controls="dropdown-menu" onClick={() => setUpdatingLang(!updatingLang)}>
- <div class="icon is-small is-left">
- <img src={langIcon} />
- </div>
- <span>{getLangName(lang)}</span>
- <div class="icon is-right">
- <i class="mdi mdi-chevron-down" />
+ return (
+ <div class="dropdown is-active ">
+ <div class="dropdown-trigger">
+ <button
+ class="button has-tooltip-left"
+ data-tooltip="change language selection"
+ aria-haspopup="true"
+ aria-controls="dropdown-menu"
+ onClick={() => setUpdatingLang(!updatingLang)}
+ >
+ <div class="icon is-small is-left">
+ <img src={langIcon} />
+ </div>
+ <span>{getLangName(lang)}</span>
+ <div class="icon is-right">
+ <i class="mdi mdi-chevron-down" />
+ </div>
+ </button>
+ </div>
+ {updatingLang && (
+ <div class="dropdown-menu" id="dropdown-menu" role="menu">
+ <div class="dropdown-content">
+ {Object.keys(messages)
+ .filter((l) => l !== lang)
+ .map((l) => (
+ <a
+ key={l}
+ class="dropdown-item"
+ value={l}
+ onClick={() => {
+ changeLanguage(l);
+ setUpdatingLang(false);
+ }}
+ >
+ {getLangName(l)}
+ </a>
+ ))}
+ </div>
</div>
- </button>
+ )}
</div>
- {updatingLang && <div class="dropdown-menu" id="dropdown-menu" role="menu">
- <div class="dropdown-content">
- {Object.keys(messages)
- .filter((l) => l !== lang)
- .map(l => <a key={l} class="dropdown-item" value={l} onClick={() => { changeLanguage(l); setUpdatingLang(false) }}>{getLangName(l)}</a>)}
- </div>
- </div>}
- </div>
-} \ No newline at end of file
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/components/menu/NavigationBar.tsx b/packages/merchant-backoffice-ui/src/components/menu/NavigationBar.tsx
index 7ef446bd1..d81410bdf 100644
--- a/packages/merchant-backoffice-ui/src/components/menu/NavigationBar.tsx
+++ b/packages/merchant-backoffice-ui/src/components/menu/NavigationBar.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,13 +15,12 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { h, VNode } from 'preact';
-import logo from '../../assets/logo.jpeg';
-import { LangSelector } from "./LangSelector.js";
+import { h, VNode } from "preact";
+import logo from "../../assets/logo-2021.svg";
interface Props {
onMobileMenu: () => void;
@@ -29,30 +28,45 @@ interface Props {
}
export function NavigationBar({ onMobileMenu, title }: Props): VNode {
- return (<nav class="navbar is-fixed-top" role="navigation" aria-label="main navigation">
- <div class="navbar-brand">
- <span class="navbar-item" style={{ fontSize: 24, fontWeight: 900 }}>{title}</span>
-
- <a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" onClick={(e) => {
- onMobileMenu()
- e.stopPropagation()
- }}>
- <span aria-hidden="true" />
- <span aria-hidden="true" />
- <span aria-hidden="true" />
- </a>
- </div>
-
- <div class="navbar-menu ">
- <a class="navbar-start is-justify-content-center is-flex-grow-1" href="https://taler.net">
- <img src={logo} style={{ height: 50, maxHeight: 50 }} />
- </a>
- <div class="navbar-end">
- <div class="navbar-item" style={{ paddingTop: 4, paddingBottom: 4 }}>
- <LangSelector />
+ return (
+ <nav
+ class="navbar is-fixed-top"
+ role="navigation"
+ aria-label="main navigation"
+ >
+ <div class="navbar-brand">
+ <span class="navbar-item" style={{ fontSize: 24, fontWeight: 900 }}>
+ {title}
+ </span>
+
+ <a
+ role="button"
+ class="navbar-burger"
+ aria-label="menu"
+ aria-expanded="false"
+ onClick={(e) => {
+ onMobileMenu();
+ e.stopPropagation();
+ }}
+ >
+ <span aria-hidden="true" />
+ <span aria-hidden="true" />
+ <span aria-hidden="true" />
+ </a>
+ </div>
+
+ <div class="navbar-menu ">
+ <a
+ class="navbar-start is-justify-content-center is-flex-grow-1"
+ href="https://taler.net"
+ >
+ <img src={logo} style={{ height: 35, margin: 10 }} />
+ </a>
+ <div class="navbar-end">
+ <div class="navbar-item" style={{ paddingTop: 4, paddingBottom: 4 }}>
+ </div>
</div>
</div>
- </div>
- </nav>
+ </nav>
);
-} \ No newline at end of file
+}
diff --git a/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx
index 11dec6e80..2090704d9 100644
--- a/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx
+++ b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2021-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -19,44 +19,38 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { Fragment, h, VNode } from "preact";
-import { useCallback } from "preact/hooks";
-import { useBackendContext } from "../../context/backend.js";
-import { useConfigContext } from "../../context/config.js";
-import { useInstanceContext } from "../../context/instance.js";
+import { TalerError } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useSessionContext } from "../../context/session.js";
import { useInstanceKYCDetails } from "../../hooks/instance.js";
-import { Translate } from "../../i18n/index.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;
mobile?: boolean;
- instance: string;
- admin?: boolean;
- mimic?: boolean;
}
-export function Sidebar({
- mobile,
- instance,
- onLogout,
- admin,
- mimic,
-}: Props): VNode {
- const config = useConfigContext();
- const backend = useBackendContext();
-
+export function Sidebar({ mobile }: Props): VNode {
+ const { i18n } = useTranslationContext();
+ const { state, logOut, config } = useSessionContext();
const kycStatus = useInstanceKYCDetails();
- const needKYC = kycStatus.ok && kycStatus.data.type === "redirect";
- // const withInstanceIdIfNeeded = useCallback(function (path: string) {
- // if (mimic) {
- // return path + '?instance=' + instance
- // }
- // return path
- // },[instance])
+ const needKYC =
+ kycStatus !== undefined &&
+ !(kycStatus instanceof TalerError) &&
+ kycStatus.type === "ok" &&
+ !!kycStatus.body;
+ const isLoggedIn = state.status === "loggedIn";
+ const hasToken = isLoggedIn && state.token !== undefined;
+
return (
- <aside class="aside is-placed-left is-expanded">
+ <aside
+ class="aside is-placed-left is-expanded"
+ style={{ overflowY: "scroll" }}
+ >
{mobile && (
<div
class="footer"
@@ -76,63 +70,52 @@ export function Sidebar({
class="is-size-7 has-text-right"
style={{ lineHeight: 0, marginTop: -10 }}
>
- {process.env.__VERSION__} ({config.version})
+ {VERSION} ({config.version})
</div>
</div>
</div>
<div class="menu is-menu-main">
- {instance ? (
+ {isLoggedIn ? (
<Fragment>
- <p class="menu-label">
- <Translate>Instance</Translate>
- </p>
<ul class="menu-list">
<li>
- <a href={"/update"} class="has-icon">
- <span class="icon">
- <i class="mdi mdi-square-edit-outline" />
- </span>
- <span class="menu-item-label">
- <Translate>Settings</Translate>
- </span>
- </a>
- </li>
- <li>
<a href={"/orders"} class="has-icon">
<span class="icon">
<i class="mdi mdi-cash-register" />
</span>
<span class="menu-item-label">
- <Translate>Orders</Translate>
+ <i18n.Translate>Orders</i18n.Translate>
</span>
</a>
</li>
<li>
- <a href={"/products"} class="has-icon">
+ <a href={"/inventory"} class="has-icon">
<span class="icon">
<i class="mdi mdi-shopping" />
</span>
<span class="menu-item-label">
- <Translate>Products</Translate>
+ <i18n.Translate>Inventory</i18n.Translate>
</span>
</a>
</li>
<li>
<a href={"/transfers"} class="has-icon">
<span class="icon">
- <i class="mdi mdi-bank" />
+ <i class="mdi mdi-arrow-left-right" />
</span>
<span class="menu-item-label">
- <Translate>Transfers</Translate>
+ <i18n.Translate>Transfers</i18n.Translate>
</span>
</a>
</li>
<li>
- <a href={"/reserves"} class="has-icon">
+ <a href={"/templates"} class="has-icon">
<span class="icon">
- <i class="mdi mdi-cash" />
+ <i class="mdi mdi-newspaper" />
+ </span>
+ <span class="menu-item-label">
+ <i18n.Translate>Templates</i18n.Translate>
</span>
- <span class="menu-item-label">Reserves</span>
</a>
</li>
{needKYC && (
@@ -146,28 +129,83 @@ export function Sidebar({
</li>
)}
</ul>
+ <p class="menu-label">
+ <i18n.Translate>Configuration</i18n.Translate>
+ </p>
+ <ul class="menu-list">
+ <li>
+ <a href={"/bank"} class="has-icon">
+ <span class="icon">
+ <i class="mdi mdi-bank" />
+ </span>
+ <span class="menu-item-label">
+ <i18n.Translate>Bank account</i18n.Translate>
+ </span>
+ </a>
+ </li>
+ <li>
+ <a href={"/otp-devices"} class="has-icon">
+ <span class="icon">
+ <i class="mdi mdi-lock" />
+ </span>
+ <span class="menu-item-label">
+ <i18n.Translate>OTP Devices</i18n.Translate>
+ </span>
+ </a>
+ </li>
+ <li>
+ <a href={"/webhooks"} class="has-icon">
+ <span class="icon">
+ <i class="mdi mdi-newspaper" />
+ </span>
+ <span class="menu-item-label">
+ <i18n.Translate>Webhooks</i18n.Translate>
+ </span>
+ </a>
+ </li>
+ <li>
+ <a href={"/settings"} class="has-icon">
+ <span class="icon">
+ <i class="mdi mdi-square-edit-outline" />
+ </span>
+ <span class="menu-item-label">
+ <i18n.Translate>Settings</i18n.Translate>
+ </span>
+ </a>
+ </li>
+ <li>
+ <a href={"/token"} class="has-icon">
+ <span class="icon">
+ <i class="mdi mdi-security" />
+ </span>
+ <span class="menu-item-label">
+ <i18n.Translate>Access token</i18n.Translate>
+ </span>
+ </a>
+ </li>
+ </ul>
</Fragment>
) : undefined}
<p class="menu-label">
- <Translate>Connection</Translate>
+ <i18n.Translate>Connection</i18n.Translate>
</p>
<ul class="menu-list">
<li>
- <div>
- <span style={{ width: "3rem" }} class="icon">
- <i class="mdi mdi-currency-eur" />
+ <a class="has-icon is-state-info is-hoverable" href="/interface">
+ <span class="icon">
+ <i class="mdi mdi-newspaper" />
</span>
- <span class="menu-item-label">{config.currency}</span>
- </div>
+ <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(backend.url).hostname}
- </span>
+ <span class="menu-item-label">{state.backendUrl.hostname}</span>
</div>
</li>
<li>
@@ -175,15 +213,13 @@ export function Sidebar({
<span style={{ width: "3rem" }} class="icon">
ID
</span>
- <span class="menu-item-label">
- {!instance ? "default" : instance}
- </span>
+ <span class="menu-item-label">{state.instance}</span>
</div>
</li>
- {admin && !mimic && (
+ {state.isAdmin && (
<Fragment>
<p class="menu-label">
- <Translate>Instances</Translate>
+ <i18n.Translate>Instances</i18n.Translate>
</p>
<li>
<a href={"/instance/new"} class="has-icon">
@@ -191,7 +227,7 @@ export function Sidebar({
<i class="mdi mdi-plus" />
</span>
<span class="menu-item-label">
- <Translate>New</Translate>
+ <i18n.Translate>New</i18n.Translate>
</span>
</a>
</li>
@@ -201,25 +237,30 @@ export function Sidebar({
<i class="mdi mdi-format-list-bulleted" />
</span>
<span class="menu-item-label">
- <Translate>List</Translate>
+ <i18n.Translate>List</i18n.Translate>
</span>
</a>
</li>
</Fragment>
)}
- <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">
- <Translate>Log out</Translate>
- </span>
- </a>
- </li>
+ {hasToken ? (
+ <li>
+ <a
+ class="has-icon is-state-info is-hoverable"
+ onClick={(e): void => {
+ logOut();
+ e.preventDefault();
+ }}
+ >
+ <span class="icon">
+ <i class="mdi mdi-logout default" />
+ </span>
+ <span class="menu-item-label">
+ <i18n.Translate>Log out</i18n.Translate>
+ </span>
+ </a>
+ </li>
+ ) : undefined}
</ul>
</div>
</aside>
diff --git a/packages/merchant-backoffice-ui/src/components/menu/index.tsx b/packages/merchant-backoffice-ui/src/components/menu/index.tsx
index 2a2e6f819..a35c07ace 100644
--- a/packages/merchant-backoffice-ui/src/components/menu/index.tsx
+++ b/packages/merchant-backoffice-ui/src/components/menu/index.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,27 +15,28 @@
*/
import { ComponentChildren, Fragment, h, VNode } from "preact";
-import Match from "preact-router/match";
import { useEffect, useState } from "preact/hooks";
import { AdminPaths } from "../../AdminRoutes.js";
-import { InstancePaths } from "../../InstanceRoutes.js";
+import { InstancePaths } from "../../Routing.js";
import { Notification } from "../../utils/types.js";
import { NavigationBar } from "./NavigationBar.js";
import { Sidebar } from "./SideBar.js";
+import { useSessionContext } from "../../context/session.js";
+import { useNavigationContext } from "@gnu-taler/web-util/browser";
function getInstanceTitle(path: string, id: string): string {
switch (path) {
- case InstancePaths.update:
+ case InstancePaths.settings:
return `${id}: Settings`;
case InstancePaths.order_list:
return `${id}: Orders`;
case InstancePaths.order_new:
return `${id}: New order`;
- case InstancePaths.product_list:
- return `${id}: Products`;
- case InstancePaths.product_new:
+ case InstancePaths.inventory_list:
+ return `${id}: Inventory`;
+ case InstancePaths.inventory_new:
return `${id}: New product`;
- case InstancePaths.product_update:
+ case InstancePaths.inventory_update:
return `${id}: Update product`;
case InstancePaths.reserves_new:
return `${id}: New reserve`;
@@ -45,6 +46,28 @@ function getInstanceTitle(path: string, id: string): string {
return `${id}: Transfers`;
case InstancePaths.transfers_new:
return `${id}: New transfer`;
+ case InstancePaths.webhooks_list:
+ return `${id}: Webhooks`;
+ case InstancePaths.webhooks_new:
+ return `${id}: New webhook`;
+ case InstancePaths.webhooks_update:
+ return `${id}: Update webhook`;
+ case InstancePaths.otp_devices_list:
+ return `${id}: otp devices`;
+ case InstancePaths.otp_devices_new:
+ return `${id}: New otp devices`;
+ case InstancePaths.otp_devices_update:
+ return `${id}: Update otp devices`;
+ case InstancePaths.templates_new:
+ return `${id}: New template`;
+ case InstancePaths.templates_update:
+ return `${id}: Update template`;
+ case InstancePaths.templates_list:
+ return `${id}: Templates`;
+ case InstancePaths.templates_use:
+ return `${id}: Use template`;
+ case InstancePaths.interface:
+ return `${id}: Interface`;
default:
return "";
}
@@ -56,13 +79,7 @@ function getAdminTitle(path: string, instance: string) {
return getInstanceTitle(path, instance);
}
-interface MenuProps {
- title?: string;
- instance: string;
- admin?: boolean;
- onLogout?: () => void;
- setInstanceName: (s: string) => void;
-}
+interface MenuProps {}
function WithTitle({
title,
@@ -77,74 +94,67 @@ function WithTitle({
return <Fragment>{children}</Fragment>;
}
-export function Menu({
- onLogout,
- title,
- instance,
- admin,
- setInstanceName,
-}: MenuProps): VNode {
+export function Menu(_p: MenuProps): VNode {
const [mobileOpen, setMobileOpen] = useState(false);
+ const { state, deImpersonate } = useSessionContext();
+ const { path } = useNavigationContext();
+
+ const titleWithSubtitle = !state.isAdmin
+ ? getInstanceTitle(path, state.instance)
+ : getAdminTitle(path, state.instance);
+
+ const isLoggedIn = state.status === "loggedIn";
+
return (
- <Match>
- {({ path }: any) => {
- 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
- onLogout={onLogout}
- admin={admin}
- mimic={mimic}
- instance={instance}
- mobile={mobileOpen}
- />
- )}
-
- {mimic && (
- <nav class="level">
- <div class="level-item has-text-centered has-background-warning">
- <p class="is-size-5">
- You are viewing the instance <b>"{instance}"</b>.{" "}
- <a
- href="#/instances"
- onClick={(e) => {
- setInstanceName("default");
- }}
- >
- go back
- </a>
- </p>
- </div>
- </nav>
- )}
+ <WithTitle title={titleWithSubtitle}>
+ <div
+ class={mobileOpen ? "has-aside-mobile-expanded" : ""}
+ onClick={() => setMobileOpen(false)}
+ >
+ <NavigationBar
+ onMobileMenu={() => setMobileOpen(!mobileOpen)}
+ title={titleWithSubtitle}
+ />
+
+ {isLoggedIn && <Sidebar mobile={mobileOpen} />}
+
+ {state.status !== "loggedOut" && state.impersonated && (
+ <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;{state.instance}&quot;</b>
+ .{" "}
+ <a
+ href="#/instances"
+ onClick={() => {
+ deImpersonate();
+ }}
+ >
+ go back
+ </a>
+ </p>
</div>
- </WithTitle>
- );
- }}
- </Match>
+ </nav>
+ )}
+ </div>
+ </WithTitle>
);
}
interface NotYetReadyAppMenuProps {
title: string;
+ onShowSettings: () => void;
onLogout?: () => void;
+ isPasswordOk: boolean;
}
interface NotifProps {
@@ -163,8 +173,8 @@ export function NotificationCard({
n.type === "ERROR"
? "message is-danger"
: n.type === "WARN"
- ? "message is-warning"
- : "message is-info"
+ ? "message is-warning"
+ : "message is-info"
}
>
<div class="message-header">
@@ -183,16 +193,41 @@ export function NotificationCard({
);
}
-export function NotYetReadyAppMenu({
- onLogout,
+interface NotConnectedAppMenuProps {
+ title: string;
+}
+export function NotConnectedAppMenu({
title,
-}: NotYetReadyAppMenuProps): VNode {
+}: 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({ title }: NotYetReadyAppMenuProps): VNode {
const [mobileOpen, setMobileOpen] = useState(false);
+ const { state } = useSessionContext();
useEffect(() => {
document.title = `Taler Backoffice: ${title}`;
}, [title]);
+ const isLoggedIn = state.status === "loggedIn";
+
return (
<div
class={mobileOpen ? "has-aside-mobile-expanded" : ""}
@@ -202,9 +237,7 @@ export function NotYetReadyAppMenu({
onMobileMenu={() => setMobileOpen(!mobileOpen)}
title={title}
/>
- {onLogout && (
- <Sidebar onLogout={onLogout} instance="" mobile={mobileOpen} />
- )}
+ {isLoggedIn && <Sidebar mobile={mobileOpen} />}
</div>
);
}
diff --git a/packages/merchant-backoffice-ui/src/components/modal/index.tsx b/packages/merchant-backoffice-ui/src/components/modal/index.tsx
index 15e80f470..1335d0f77 100644
--- a/packages/merchant-backoffice-ui/src/components/modal/index.tsx
+++ b/packages/merchant-backoffice-ui/src/components/modal/index.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,19 +15,18 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { ComponentChildren, h, VNode } from "preact";
+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 { Translate, useTranslator } from "../../i18n/index.js";
import { DEFAULT_REQUEST_TIMEOUT } from "../../utils/constants.js";
-import { Loading, Spinner } from "../exception/loading.js";
+import { Spinner } from "../exception/loading.js";
import { FormProvider } from "../form/FormProvider.js";
import { Input } from "../form/Input.js";
+import { useSessionContext } from "../../context/session.js";
interface Props {
active?: boolean;
@@ -40,104 +39,233 @@ interface Props {
disabled?: boolean;
}
-export function ConfirmModal({ active, description, onCancel, onConfirm, children, danger, disabled, label = 'Confirm' }: Props): VNode {
- return <div class={active ? "modal is-active" : "modal"}>
- <div class="modal-background " onClick={onCancel} />
- <div class="modal-card" style={{maxWidth: 700}}>
- <header class="modal-card-head">
- {!description ? null : <p class="modal-card-title"><b>{description}</b></p>}
- <button class="delete " aria-label="close" onClick={onCancel} />
- </header>
- <section class="modal-card-body">
- {children}
- </section>
- <footer class="modal-card-foot">
- <div class="buttons is-right" style={{ width: '100%' }}>
- <button class="button " onClick={onCancel} ><Translate>Cancel</Translate></button>
- <button class={danger ? "button is-danger " : "button is-info "} disabled={disabled} onClick={onConfirm} ><Translate>{label}</Translate></button>
- </div>
- </footer>
+export function ConfirmModal({
+ active,
+ description,
+ onCancel,
+ onConfirm,
+ children,
+ danger,
+ disabled,
+ label = "Confirm",
+}: Props): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class={active ? "modal is-active" : "modal"}>
+ <div class="modal-background " onClick={onCancel} />
+ <div class="modal-card" style={{ maxWidth: 700 }}>
+ <header class="modal-card-head">
+ {!description ? null : (
+ <p class="modal-card-title">
+ <b>{description}</b>
+ </p>
+ )}
+ <button class="delete " aria-label="close" onClick={onCancel} />
+ </header>
+ <section class="modal-card-body">{children}</section>
+ <footer class="modal-card-foot">
+ <div class="buttons is-right" style={{ width: "100%" }}>
+ {onConfirm ? (
+ <Fragment>
+ <button class="button " onClick={onCancel}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+
+ <button
+ class={danger ? "button is-danger " : "button is-info "}
+ disabled={disabled}
+ onClick={onConfirm}
+ >
+ <i18n.Translate>{label}</i18n.Translate>
+ </button>
+ </Fragment>
+ ) : (
+ <button class="button " onClick={onCancel}>
+ <i18n.Translate>Close</i18n.Translate>
+ </button>
+ )}
+ </div>
+ </footer>
+ </div>
+ <button
+ class="modal-close is-large "
+ aria-label="close"
+ onClick={onCancel}
+ />
</div>
- <button class="modal-close is-large " aria-label="close" onClick={onCancel} />
- </div>
+ );
}
-export function ContinueModal({ active, description, onCancel, onConfirm, children, disabled }: Props): VNode {
- return <div class={active ? "modal is-active" : "modal"}>
- <div class="modal-background " onClick={onCancel} />
- <div class="modal-card">
- <header class="modal-card-head has-background-success">
- {!description ? null : <p class="modal-card-title">{description}</p>}
- <button class="delete " aria-label="close" onClick={onCancel} />
- </header>
- <section class="modal-card-body">
- {children}
- </section>
- <footer class="modal-card-foot">
- <div class="buttons is-right" style={{ width: '100%' }}>
- <button class="button is-success " disabled={disabled} onClick={onConfirm} ><Translate>Continue</Translate></button>
- </div>
- </footer>
+export function ContinueModal({
+ active,
+ description,
+ onCancel,
+ onConfirm,
+ children,
+ disabled,
+}: Props): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class={active ? "modal is-active" : "modal"}>
+ <div class="modal-background " onClick={onCancel} />
+ <div class="modal-card">
+ <header class="modal-card-head has-background-success">
+ {!description ? null : <p class="modal-card-title">{description}</p>}
+ <button class="delete " aria-label="close" onClick={onCancel} />
+ </header>
+ <section class="modal-card-body">{children}</section>
+ <footer class="modal-card-foot">
+ <div class="buttons is-right" style={{ width: "100%" }}>
+ <button
+ class="button is-success "
+ disabled={disabled}
+ onClick={onConfirm}
+ >
+ <i18n.Translate>Continue</i18n.Translate>
+ </button>
+ </div>
+ </footer>
+ </div>
+ <button
+ class="modal-close is-large "
+ aria-label="close"
+ onClick={onCancel}
+ />
</div>
- <button class="modal-close is-large " aria-label="close" onClick={onCancel} />
- </div>
+ );
}
export function SimpleModal({ onCancel, children }: any): VNode {
- return <div class="modal is-active">
- <div class="modal-background " onClick={onCancel} />
- <div class="modal-card">
- <section class="modal-card-body is-main-section">
- {children}
- </section>
+ return (
+ <div class="modal is-active">
+ <div class="modal-background " onClick={onCancel} />
+ <div class="modal-card">
+ <section class="modal-card-body is-main-section">{children}</section>
+ </div>
+ <button
+ class="modal-close is-large "
+ aria-label="close"
+ onClick={onCancel}
+ />
</div>
- <button class="modal-close is-large " aria-label="close" onClick={onCancel} />
- </div>
+ );
}
-export function ClearConfirmModal({ description, onCancel, onClear, onConfirm, children }: Props & { onClear?: () => void }): VNode {
- return <div class="modal is-active">
- <div class="modal-background " onClick={onCancel} />
- <div class="modal-card">
- <header class="modal-card-head">
- {!description ? null : <p class="modal-card-title">{description}</p>}
- <button class="delete " aria-label="close" onClick={onCancel} />
- </header>
- <section class="modal-card-body is-main-section">
- {children}
- </section>
- <footer class="modal-card-foot">
- {onClear && <button class="button is-danger" onClick={onClear} disabled={onClear === undefined} ><Translate>Clear</Translate></button>}
- <div class="buttons is-right" style={{ width: '100%' }}>
- <button class="button " onClick={onCancel} ><Translate>Cancel</Translate></button>
- <button class="button is-info" onClick={onConfirm} disabled={onConfirm === undefined} ><Translate>Confirm</Translate></button>
- </div>
- </footer>
+export function ClearConfirmModal({
+ description,
+ onCancel,
+ onClear,
+ onConfirm,
+ children,
+}: Props & { onClear?: () => void }): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="modal is-active">
+ <div class="modal-background " onClick={onCancel} />
+ <div class="modal-card">
+ <header class="modal-card-head">
+ {!description ? null : <p class="modal-card-title">{description}</p>}
+ <button class="delete " aria-label="close" onClick={onCancel} />
+ </header>
+ <section class="modal-card-body is-main-section">{children}</section>
+ <footer class="modal-card-foot">
+ {onClear && (
+ <button
+ class="button is-danger"
+ onClick={onClear}
+ disabled={onClear === undefined}
+ >
+ <i18n.Translate>Clear</i18n.Translate>
+ </button>
+ )}
+ <div class="buttons is-right" style={{ width: "100%" }}>
+ <button class="button " onClick={onCancel}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ <button
+ class="button is-info"
+ onClick={onConfirm}
+ disabled={onConfirm === undefined}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </button>
+ </div>
+ </footer>
+ </div>
+ <button
+ class="modal-close is-large "
+ aria-label="close"
+ onClick={onCancel}
+ />
</div>
- <button class="modal-close is-large " aria-label="close" onClick={onCancel} />
- </div>
+ );
}
interface DeleteModalProps {
- element: { id: string, name: string };
+ element: { id: string; name: string };
onCancel: () => void;
onConfirm: (id: string) => void;
}
-export function DeleteModal({ element, onCancel, onConfirm }: DeleteModalProps): VNode {
- return <ConfirmModal label={`Delete instance`} description={`Delete the instance "${element.name}"`} danger active onCancel={onCancel} onConfirm={() => onConfirm(element.id)}>
- <p>If you delete the instance named <b>"{element.name}"</b> (ID: <b>{element.id}</b>), the merchant will no longer be able to process orders or refunds</p>
- <p>This action deletes the instance private key, but preserves all transaction data. You can still access that data after deleting the instance.</p>
- <p class="warning">Deleting an instance <b>cannot be undone</b>.</p>
- </ConfirmModal>
+export function DeleteModal({
+ element,
+ onCancel,
+ onConfirm,
+}: DeleteModalProps): VNode {
+ return (
+ <ConfirmModal
+ label={`Delete instance`}
+ description={`Delete the instance "${element.name}"`}
+ danger
+ active
+ onCancel={onCancel}
+ onConfirm={() => onConfirm(element.id)}
+ >
+ <p>
+ If you delete the instance named <b>&quot;{element.name}&quot;</b> (ID:{" "}
+ <b>{element.id}</b>), the merchant will no longer be able to process
+ orders or refunds
+ </p>
+ <p>
+ This action deletes the instance private key, but preserves all
+ transaction data. You can still access that data after deleting the
+ instance.
+ </p>
+ <p class="warning">
+ Deleting an instance <b>cannot be undone</b>.
+ </p>
+ </ConfirmModal>
+ );
}
-export function PurgeModal({ element, onCancel, onConfirm }: DeleteModalProps): VNode {
- return <ConfirmModal label={`Purge the instance`} description={`Purge the instance "${element.name}"`} danger active onCancel={onCancel} onConfirm={() => onConfirm(element.id)}>
- <p>If you purge the instance named <b>"{element.name}"</b> (ID: <b>{element.id}</b>), you will also delete all it's transaction data.</p>
- <p>The instance will disappear from your list, and you will no longer be able to access it's data.</p>
- <p class="warning">Purging an instance <b>cannot be undone</b>.</p>
- </ConfirmModal>
+export function PurgeModal({
+ element,
+ onCancel,
+ onConfirm,
+}: DeleteModalProps): VNode {
+ return (
+ <ConfirmModal
+ label={`Purge the instance`}
+ description={`Purge the instance "${element.name}"`}
+ danger
+ active
+ onCancel={onCancel}
+ onConfirm={() => onConfirm(element.id)}
+ >
+ <p>
+ If you purge the instance named <b>&quot;{element.name}&quot;</b> (ID:{" "}
+ <b>{element.id}</b>), you will also delete all it&apos;s transaction
+ data.
+ </p>
+ <p>
+ The instance will disappear from your list, and you will no longer be
+ able to access it&apos;s data.
+ </p>
+ <p class="warning">
+ Purging an instance <b>cannot be undone</b>.
+ </p>
+ </ConfirmModal>
+ );
}
interface UpdateTokenModalProps {
@@ -148,115 +276,221 @@ interface UpdateTokenModalProps {
}
//FIXME: merge UpdateTokenModal with SetTokenNewInstanceModal
-export function UpdateTokenModal({ onCancel, onClear, onConfirm, oldToken }: UpdateTokenModalProps): VNode {
- type State = { old_token: string, new_token: string, repeat_token: string }
+export function UpdateTokenModal({
+ onCancel,
+ onClear,
+ onConfirm,
+ oldToken,
+}: UpdateTokenModalProps): VNode {
+ type State = { old_token: string; new_token: string; repeat_token: string };
const [form, setValue] = useState<Partial<State>>({
- old_token: '', new_token: '', repeat_token: '',
- })
- const i18n = useTranslator()
+ old_token: "",
+ new_token: "",
+ repeat_token: "",
+ });
+ const { i18n } = useTranslationContext();
- const hasInputTheCorrectOldToken = oldToken && oldToken !== form.old_token
+ const hasInputTheCorrectOldToken = oldToken && oldToken !== form.old_token;
const errors = {
- old_token: hasInputTheCorrectOldToken ? i18n`is not the same as the current access token` : undefined,
- new_token: !form.new_token ? i18n`cannot be empty` : (form.new_token === form.old_token ? i18n`cannot be the same as the old token` : undefined),
- repeat_token: form.new_token !== form.repeat_token ? i18n`is not the same` : undefined
- }
+ old_token: hasInputTheCorrectOldToken
+ ? i18n.str`is not the same as the current access token`
+ : undefined,
+ new_token: !form.new_token
+ ? i18n.str`cannot be empty`
+ : form.new_token === form.old_token
+ ? i18n.str`cannot be the same as the old token`
+ : undefined,
+ repeat_token:
+ form.new_token !== form.repeat_token
+ ? i18n.str`is not the same`
+ : undefined,
+ };
- const hasErrors = Object.keys(errors).some(k => (errors as any)[k] !== undefined)
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
- const instance = useInstanceContext()
+ const { state } = useSessionContext();
- const text = i18n`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 ${state.instance}`;
- return <ClearConfirmModal description={text}
- onCancel={onCancel}
- onConfirm={!hasErrors ? () => onConfirm(form.new_token!) : undefined}
- onClear={!hasInputTheCorrectOldToken && oldToken ? onClear : undefined}
- >
- <div class="columns">
- <div class="column" />
- <div class="column is-four-fifths" >
- <FormProvider errors={errors} object={form} valueHandler={setValue}>
- {oldToken && <Input<State> name="old_token" label={i18n`Old access token`} tooltip={i18n`access token currently in use`} inputType="password" />}
- <Input<State> name="new_token" label={i18n`New access token`} tooltip={i18n`next access token to be used`} inputType="password" />
- <Input<State> name="repeat_token" label={i18n`Repeat access token`} tooltip={i18n`confirm the same access token`} inputType="password" />
- </FormProvider>
- <p><Translate>Clearing the access token will mean public access to the instance</Translate></p>
+ return (
+ <ClearConfirmModal
+ description={text}
+ onCancel={onCancel}
+ onConfirm={!hasErrors ? () => onConfirm(form.new_token!) : undefined}
+ onClear={!hasInputTheCorrectOldToken && oldToken ? onClear : undefined}
+ >
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <FormProvider errors={errors} object={form} valueHandler={setValue}>
+ {oldToken && (
+ <Input<State>
+ name="old_token"
+ label={i18n.str`Old access token`}
+ tooltip={i18n.str`access token currently in use`}
+ inputType="password"
+ />
+ )}
+ <Input<State>
+ name="new_token"
+ label={i18n.str`New access token`}
+ tooltip={i18n.str`next access token to be used`}
+ inputType="password"
+ />
+ <Input<State>
+ name="repeat_token"
+ label={i18n.str`Repeat access token`}
+ tooltip={i18n.str`confirm the same access token`}
+ inputType="password"
+ />
+ </FormProvider>
+ <p>
+ <i18n.Translate>
+ Clearing the access token will mean public access to the instance
+ </i18n.Translate>
+ </p>
+ </div>
+ <div class="column" />
</div>
- <div class="column" />
- </div>
- </ClearConfirmModal>
+ </ClearConfirmModal>
+ );
}
-export function SetTokenNewInstanceModal({ onCancel, onClear, onConfirm }: UpdateTokenModalProps): VNode {
- type State = { old_token: string, new_token: string, repeat_token: string }
+export function SetTokenNewInstanceModal({
+ onCancel,
+ onClear,
+ onConfirm,
+}: UpdateTokenModalProps): VNode {
+ type State = { old_token: string; new_token: string; repeat_token: string };
const [form, setValue] = useState<Partial<State>>({
- new_token: '', repeat_token: '',
- })
- const i18n = useTranslator()
+ new_token: "",
+ repeat_token: "",
+ });
+ const { i18n } = useTranslationContext();
const errors = {
- new_token: !form.new_token ? i18n`cannot be empty` : (form.new_token === form.old_token ? i18n`cannot be the same as the old access token` : undefined),
- repeat_token: form.new_token !== form.repeat_token ? i18n`is not the same` : undefined
- }
-
- const hasErrors = Object.keys(errors).some(k => (errors as any)[k] !== 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 access token`
+ : undefined,
+ repeat_token:
+ form.new_token !== form.repeat_token
+ ? i18n.str`is not the same`
+ : undefined,
+ };
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
- return <div class="modal is-active">
- <div class="modal-background " onClick={onCancel} />
- <div class="modal-card">
- <header class="modal-card-head">
- <p class="modal-card-title">{i18n`You are setting the access token for the new instance`}</p>
- <button class="delete " aria-label="close" onClick={onCancel} />
- </header>
- <section class="modal-card-body is-main-section">
- <div class="columns">
- <div class="column" />
- <div class="column is-four-fifths" >
- <FormProvider errors={errors} object={form} valueHandler={setValue}>
- <Input<State> name="new_token" label={i18n`New access token`} tooltip={i18n`next access token to be used`} inputType="password" />
- <Input<State> name="repeat_token" label={i18n`Repeat access token`} tooltip={i18n`confirm the same access token`} inputType="password" />
- </FormProvider>
- <p><Translate>With external authorization method no check will be done by the merchant backend</Translate></p>
+ return (
+ <div class="modal is-active">
+ <div class="modal-background " onClick={onCancel} />
+ <div class="modal-card">
+ <header class="modal-card-head">
+ <p class="modal-card-title">{i18n.str`You are setting the access token for the new instance`}</p>
+ <button class="delete " aria-label="close" onClick={onCancel} />
+ </header>
+ <section class="modal-card-body is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <FormProvider
+ errors={errors}
+ object={form}
+ valueHandler={setValue}
+ >
+ <Input<State>
+ name="new_token"
+ label={i18n.str`New access token`}
+ tooltip={i18n.str`next access token to be used`}
+ inputType="password"
+ />
+ <Input<State>
+ name="repeat_token"
+ label={i18n.str`Repeat access token`}
+ tooltip={i18n.str`confirm the same access token`}
+ inputType="password"
+ />
+ </FormProvider>
+ <p>
+ <i18n.Translate>
+ With external authorization method no check will be done by
+ the merchant backend
+ </i18n.Translate>
+ </p>
+ </div>
+ <div class="column" />
</div>
- <div class="column" />
- </div>
- </section>
- <footer class="modal-card-foot">
- {onClear && <button class="button is-danger" onClick={onClear} disabled={onClear === undefined} ><Translate>Set external authorization</Translate></button>}
- <div class="buttons is-right" style={{ width: '100%' }}>
- <button class="button " onClick={onCancel} ><Translate>Cancel</Translate></button>
- <button class="button is-info" onClick={() => onConfirm(form.new_token!)} disabled={hasErrors} ><Translate>Set access token</Translate></button>
- </div>
- </footer>
+ </section>
+ <footer class="modal-card-foot">
+ {onClear && (
+ <button
+ class="button is-danger"
+ onClick={onClear}
+ disabled={onClear === undefined}
+ >
+ <i18n.Translate>Set external authorization</i18n.Translate>
+ </button>
+ )}
+ <div class="buttons is-right" style={{ width: "100%" }}>
+ <button class="button " onClick={onCancel}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ <button
+ class="button is-info"
+ onClick={() => onConfirm(form.new_token!)}
+ disabled={hasErrors}
+ >
+ <i18n.Translate>Set access token</i18n.Translate>
+ </button>
+ </div>
+ </footer>
+ </div>
+ <button
+ class="modal-close is-large "
+ aria-label="close"
+ onClick={onCancel}
+ />
</div>
- <button class="modal-close is-large " aria-label="close" onClick={onCancel} />
- </div>
+ );
}
export function LoadingModal({ onCancel }: { onCancel: () => void }): VNode {
- const i18n = useTranslator()
- return <div class="modal is-active">
- <div class="modal-background " onClick={onCancel} />
- <div class="modal-card">
- <header class="modal-card-head">
- <p class="modal-card-title"><Translate>Operation in progress...</Translate></p>
- </header>
- <section class="modal-card-body">
- <div class="columns">
- <div class="column" />
- <Spinner />
- <div class="column" />
- </div>
- <p>{i18n`The operation will be automatically canceled after ${DEFAULT_REQUEST_TIMEOUT} seconds`}</p>
- </section>
- <footer class="modal-card-foot">
- <div class="buttons is-right" style={{ width: '100%' }}>
- <button class="button " onClick={onCancel} ><Translate>Cancel</Translate></button>
- </div>
- </footer>
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="modal is-active">
+ <div class="modal-background " onClick={onCancel} />
+ <div class="modal-card">
+ <header class="modal-card-head">
+ <p class="modal-card-title">
+ <i18n.Translate>Operation in progress...</i18n.Translate>
+ </p>
+ </header>
+ <section class="modal-card-body">
+ <div class="columns">
+ <div class="column" />
+ <Spinner />
+ <div class="column" />
+ </div>
+ <p>{i18n.str`The operation will be automatically canceled after ${DEFAULT_REQUEST_TIMEOUT} seconds`}</p>
+ </section>
+ <footer class="modal-card-foot">
+ <div class="buttons is-right" style={{ width: "100%" }}>
+ <button class="button " onClick={onCancel}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ </div>
+ </footer>
+ </div>
+ <button
+ class="modal-close is-large "
+ aria-label="close"
+ onClick={onCancel}
+ />
</div>
- <button class="modal-close is-large " aria-label="close" onClick={onCancel} />
- </div>
+ );
}
diff --git a/packages/merchant-backoffice-ui/src/components/notifications/CreatedSuccessfully.tsx b/packages/merchant-backoffice-ui/src/components/notifications/CreatedSuccessfully.tsx
index e0b355c2e..5cd8a237b 100644
--- a/packages/merchant-backoffice-ui/src/components/notifications/CreatedSuccessfully.tsx
+++ b/packages/merchant-backoffice-ui/src/components/notifications/CreatedSuccessfully.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2021-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -14,9 +14,9 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
import { ComponentChildren, h, VNode } from "preact";
interface Props {
@@ -25,25 +25,33 @@ interface Props {
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}
+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>
<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>
+ {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>
- <div class="column" />
- </div>
+ );
}
diff --git a/packages/merchant-backoffice-ui/src/components/notifications/Notifications.stories.tsx b/packages/merchant-backoffice-ui/src/components/notifications/Notifications.stories.tsx
index 15e00b790..d75c5ced2 100644
--- a/packages/merchant-backoffice-ui/src/components/notifications/Notifications.stories.tsx
+++ b/packages/merchant-backoffice-ui/src/components/notifications/Notifications.stories.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,43 +15,48 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { h } from 'preact';
+import { h } from "preact";
import { Notifications } from "./index.js";
-
export default {
- title: 'Components/Notification',
+ title: "Components/Notification",
component: Notifications,
argTypes: {
- removeNotification: { action: 'removeNotification' },
+ removeNotification: { action: "removeNotification" },
},
};
export const Info = (a: any) => <Notifications {...a} />;
Info.args = {
- notifications: [{
- message: 'Title',
- description: 'Some large description',
- type: 'INFO',
- }]
-}
+ 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',
- }]
-}
+ 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',
- }]
-}
+ notifications: [
+ {
+ message: "Title",
+ description: "Some large description",
+ type: "ERROR",
+ },
+ ],
+};
diff --git a/packages/merchant-backoffice-ui/src/components/notifications/index.tsx b/packages/merchant-backoffice-ui/src/components/notifications/index.tsx
index d4da7b35a..0c4e0d761 100644
--- a/packages/merchant-backoffice-ui/src/components/notifications/index.tsx
+++ b/packages/merchant-backoffice-ui/src/components/notifications/index.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,9 +15,9 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
import { h, VNode } from "preact";
import { MessageType, Notification } from "../../utils/types.js";
@@ -29,24 +29,37 @@ interface Props {
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"
+ 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>
-} \ No newline at end of file
+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/merchant-backoffice-ui/src/components/picker/DatePicker.tsx b/packages/merchant-backoffice-ui/src/components/picker/DatePicker.tsx
index 084b7b00a..6dc1fadd6 100644
--- a/packages/merchant-backoffice-ui/src/components/picker/DatePicker.tsx
+++ b/packages/merchant-backoffice-ui/src/components/picker/DatePicker.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,11 +15,11 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { h, Component } from "preact";
+import { Component, h } from "preact";
interface Props {
closeFunction?: () => void;
@@ -35,39 +35,36 @@ interface State {
// 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
- */
+ * 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
+ 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'));
+ const date = new Date(element.getAttribute("data-value"));
// update the state
this.setState({ currentDate: date });
- this.passDateToParent(date)
+ this.passDateToParent(date);
}
/**
- * returns days in month as array
- * @param {number} month the month to display
- * @param {number} year the year to display
- */
+ * 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 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
@@ -76,15 +73,17 @@ export class DatePicker extends Component<Props, State> {
// 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
+ 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
});
}
@@ -92,51 +91,48 @@ export class DatePicker extends Component<Props, State> {
}
/**
- * Display previous month by updating state
- */
+ * Display previous month by updating state
+ */
displayPrevMonth() {
if (this.state.displayedMonth <= 0) {
this.setState({
displayedMonth: 11,
- displayedYear: this.state.displayedYear - 1
+ displayedYear: this.state.displayedYear - 1,
});
- }
- else {
+ } else {
this.setState({
- displayedMonth: this.state.displayedMonth - 1
+ displayedMonth: this.state.displayedMonth - 1,
});
}
}
/**
- * Display next month by updating state
- */
+ * Display next month by updating state
+ */
displayNextMonth() {
if (this.state.displayedMonth >= 11) {
this.setState({
displayedMonth: 0,
- displayedYear: this.state.displayedYear + 1
+ displayedYear: this.state.displayedYear + 1,
});
- }
- else {
+ } else {
this.setState({
- displayedMonth: this.state.displayedMonth + 1
+ displayedMonth: this.state.displayedMonth + 1,
});
}
}
/**
- * Display the selected month (gets fired when clicking on the date string)
- */
+ * Display the selected month (gets fired when clicking on the date string)
+ */
displaySelectedMonth() {
if (this.state.selectYearMode) {
this.toggleYearSelector();
- }
- else {
+ } else {
if (!this.state.currentDate) return false;
this.setState({
displayedMonth: this.state.currentDate.getMonth(),
- displayedYear: this.state.currentDate.getFullYear()
+ displayedYear: this.state.currentDate.getFullYear(),
});
}
}
@@ -148,23 +144,27 @@ export class DatePicker extends Component<Props, State> {
changeDisplayedYear(e: any) {
const element = e.target;
this.toggleYearSelector();
- this.setState({ displayedYear: parseInt(element.innerHTML, 10), displayedMonth: 0 });
+ this.setState({
+ displayedYear: parseInt(element.innerHTML, 10),
+ displayedMonth: 0,
+ });
}
/**
- * Pass the selected date to parent when 'OK' is clicked
- */
+ * Pass the selected date to parent when 'OK' is clicked
+ */
passSavedDateDateToParent() {
- this.passDateToParent(this.state.currentDate)
+ this.passDateToParent(this.state.currentDate);
}
passDateToParent(date: Date) {
- if (typeof this.props.dateReceiver === 'function') this.props.dateReceiver(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
+ document.getElementsByClassName("selected")[0].scrollIntoView(); // works in every browser incl. IE, replace with scrollIntoViewIfNeeded when browsers support it
}
}
@@ -181,143 +181,168 @@ export class DatePicker extends Component<Props, State> {
this.toggleYearSelector = this.toggleYearSelector.bind(this);
this.displaySelectedMonth = this.displaySelectedMonth.bind(this);
-
this.state = {
currentDate: now,
displayedMonth: now.getMonth(),
displayedYear: now.getFullYear(),
- selectYearMode: false
- }
+ selectYearMode: false,
+ };
}
render() {
-
- const { currentDate, displayedMonth, displayedYear, selectYearMode } = this.state;
+ const { currentDate, displayedMonth, displayedYear, selectYearMode } =
+ this.state;
return (
<div>
- <div class={`datePicker ${ this.props.opened && "datePicker--opened"}`} >
-
+ <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()}
+ <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>}
+ {!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">
-
- {/*
+ {!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' : '')}
+ {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>)
- }
- )
- }
-
+ </span>
+ );
+ })}
+ </div>
</div>
-
- </div>}
-
- {selectYearMode && <div class="datePicker--selectYear">
-
- {yearArr.map(year => (
- <span key={year} class={(year === displayedYear) ? 'selected' : ''} onClick={this.changeDisplayedYear}>
- {year}
- </span>
- ))}
-
- </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
+ 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'
-]
+ "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[] = []
+ "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/merchant-backoffice-ui/src/components/picker/DurationPicker.stories.tsx b/packages/merchant-backoffice-ui/src/components/picker/DurationPicker.stories.tsx
index 81bca17ba..b95ab054c 100644
--- a/packages/merchant-backoffice-ui/src/components/picker/DurationPicker.stories.tsx
+++ b/packages/merchant-backoffice-ui/src/components/picker/DurationPicker.stories.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,36 +15,41 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { h, FunctionalComponent } from 'preact';
-import { useState } from 'preact/hooks';
+import { h, FunctionalComponent } from "preact";
+import { useState } from "preact/hooks";
import { DurationPicker as TestedComponent } from "./DurationPicker.js";
-
export default {
- title: 'Components/Picker/Duration',
+ title: "Components/Picker/Duration",
component: TestedComponent,
argTypes: {
- onCreate: { action: 'onCreate' },
- goBack: { action: 'goBack' },
- }
+ 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
+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
+ 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 />
-}
+ const [v, s] = useState<number>(1000000);
+ return <TestedComponent value={v} onChange={s} days minutes hours seconds />;
+};
diff --git a/packages/merchant-backoffice-ui/src/components/picker/DurationPicker.tsx b/packages/merchant-backoffice-ui/src/components/picker/DurationPicker.tsx
index 63ac22697..7c1c172ac 100644
--- a/packages/merchant-backoffice-ui/src/components/picker/DurationPicker.tsx
+++ b/packages/merchant-backoffice-ui/src/components/picker/DurationPicker.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2021-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -19,9 +19,9 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
-import { useTranslator } from "../../i18n/index.js";
import "../../scss/DurationPicker.scss";
export interface Props {
@@ -42,17 +42,17 @@ export function DurationPicker({
onChange,
value,
}: Props): VNode {
- const ss = 1000 * 1000;
+ const ss = 1000;
const ms = ss * 60;
const hs = ms * 60;
const ds = hs * 24;
- const i18n = useTranslator();
+ const { i18n } = useTranslationContext();
return (
<div class="rdp-picker">
{days && (
<DurationColumn
- unit={i18n`days`}
+ unit={i18n.str`days`}
max={99}
value={Math.floor(value / ds)}
onDecrease={value >= ds ? () => onChange(value - ds) : undefined}
@@ -62,7 +62,7 @@ export function DurationPicker({
)}
{hours && (
<DurationColumn
- unit={i18n`hours`}
+ unit={i18n.str`hours`}
max={23}
min={1}
value={Math.floor(value / hs) % 24}
@@ -73,7 +73,7 @@ export function DurationPicker({
)}
{minutes && (
<DurationColumn
- unit={i18n`minutes`}
+ unit={i18n.str`minutes`}
max={59}
min={1}
value={Math.floor(value / ms) % 60}
@@ -84,7 +84,7 @@ export function DurationPicker({
)}
{seconds && (
<DurationColumn
- unit={i18n`seconds`}
+ unit={i18n.str`seconds`}
max={59}
value={Math.floor(value / ss) % 60}
onDecrease={value >= ss ? () => onChange(value - ss) : undefined}
diff --git a/packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.stories.tsx b/packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.stories.tsx
index fd87283c4..fcc97f96a 100644
--- a/packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.stories.tsx
+++ b/packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.stories.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,44 +15,48 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { h, VNode, FunctionalComponent } from 'preact';
+import { h, VNode, FunctionalComponent } from "preact";
import { InventoryProductForm as TestedComponent } from "./InventoryProductForm.js";
-
export default {
- title: 'Components/Product/Add',
+ title: "Components/Product/Add",
component: TestedComponent,
argTypes: {
- onAddProduct: { action: 'onAddProduct' },
+ onAddProduct: { action: "onAddProduct" },
},
};
-function createExample<Props>(Component: FunctionalComponent<Props>, props: Partial<Props>) {
- const r = (args: any) => <Component {...args} />
- r.args = props
- return r
+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]
+ inventory: [
+ {
+ id: "this id",
+ description: "this is the description",
+ } as any,
+ ],
});
export const WithAProductSelected = createExample(TestedComponent, {
- inventory:[],
+ inventory: [],
currentProducts: {
thisid: {
quantity: 1,
product: {
- id: 'asd',
- description: 'asdsadsad',
- } as any
- }
- }
+ id: "asd",
+ description: "asdsadsad",
+ } as any,
+ },
+ },
});
diff --git a/packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.tsx b/packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.tsx
index 30cb06c12..52ac2a1fe 100644
--- a/packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.tsx
+++ b/packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,83 +13,115 @@
You should have received 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 { FormProvider, FormErrors } from "../form/FormProvider.js";
-import { InputNumber } from "../form/InputNumber.js";
-import { InputSearchProduct } from "../form/InputSearchProduct.js";
-import { MerchantBackend, WithId } from "../../declaration.js";
-import { Translate, useTranslator } from "../../i18n/index.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";
+import { TalerMerchantApi } from "@gnu-taler/taler-util";
type Form = {
- product: MerchantBackend.Products.ProductDetail & WithId,
+ product: TalerMerchantApi.ProductDetail & WithId;
quantity: number;
-}
+};
interface Props {
- currentProducts: ProductMap,
- onAddProduct: (product: MerchantBackend.Products.ProductDetail & WithId, quantity: number) => void,
- inventory: (MerchantBackend.Products.ProductDetail & WithId)[],
+ currentProducts: ProductMap;
+ onAddProduct: (
+ product: TalerMerchantApi.ProductDetail & WithId,
+ quantity: number,
+ ) => void;
+ inventory: (TalerMerchantApi.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>>({})
+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 = useTranslator()
+ const { i18n } = useTranslationContext();
- const productWithInfiniteStock = state.product && state.product.total_stock === -1
+ const productWithInfiniteStock =
+ state.product && state.product.total_stock === -1;
const submit = (): void => {
if (!state.product) {
- setErrors({ product: i18n`You must enter a valid product identifier.` });
+ setErrors({
+ product: i18n.str`You must enter a valid product identifier.`,
+ });
return;
}
if (productWithInfiniteStock) {
- onAddProduct(state.product, 1)
+ onAddProduct(state.product, 1);
} else {
if (!state.quantity || state.quantity <= 0) {
- setErrors({ quantity: i18n`Quantity must be greater than 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]
+ 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`This quantity exceeds remaining stock. Currently, only ${left} units remain unreserved in stock.` });
+ 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)
+ onAddProduct(state.product, state.quantity + p.quantity);
} else {
if (state.quantity > currentStock) {
const left = currentStock;
- setErrors({ quantity: i18n`This quantity exceeds remaining stock. Currently, only ${left} units remain unreserved in stock.` });
+ setErrors({
+ quantity: i18n.str`This quantity exceeds remaining stock. Currently, only ${left} units remain unreserved in stock.`,
+ });
return;
}
- onAddProduct(state.product, state.quantity)
+ onAddProduct(state.product, state.quantity);
}
}
- setState(initialState)
- }
+ setState(initialState);
+ };
- return <FormProvider<Form> errors={errors} object={state} valueHandler={setState}>
- <InputSearchProduct selected={state.product} onChange={(p) => setState(v => ({ ...v, product: p }))} products={inventory} />
- { state.product && <div class="columns mt-5">
- <div class="column is-two-thirds">
- {!productWithInfiniteStock &&
- <InputNumber<Form> name="quantity" label={i18n`Quantity`} tooltip={i18n`how many products will be added`} />
- }
- </div>
- <div class="column">
- <div class="buttons is-right">
- <button class="button is-success" onClick={submit}><Translate>Add from inventory</Translate></button>
+ 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>
- </div>
- </div> }
-
- </FormProvider>
+ )}
+ </FormProvider>
+ );
}
diff --git a/packages/merchant-backoffice-ui/src/components/product/NonInventoryProductForm.tsx b/packages/merchant-backoffice-ui/src/components/product/NonInventoryProductForm.tsx
index 1a864d0b3..a127999fc 100644
--- a/packages/merchant-backoffice-ui/src/components/product/NonInventoryProductForm.tsx
+++ b/packages/merchant-backoffice-ui/src/components/product/NonInventoryProductForm.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,79 +13,116 @@
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+import { AmountString, TalerMerchantApi } from "@gnu-taler/taler-util";
+import { 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 * as yup from "yup";
+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";
-import { MerchantBackend } from "../../declaration.js";
-import { useListener } from "../../hooks/listener.js";
-import { Translate, useTranslator } from "../../i18n/index.js";
-import {
- NonInventoryProductSchema as schema
-} from "../../schemas/index.js";
-
-type Entity = MerchantBackend.Product
+type Entity = TalerMerchantApi.Product;
interface Props {
onAddProduct: (p: Entity) => Promise<void>;
productToEdit?: Entity;
}
-export function NonInventoryProductFrom({ productToEdit, onAddProduct }: Props): VNode {
- const [showCreateProduct, setShowCreateProduct] = useState(false)
+export function NonInventoryProductFrom({
+ productToEdit,
+ onAddProduct,
+}: Props): VNode {
+ const [showCreateProduct, setShowCreateProduct] = useState(false);
- const isEditing = !!productToEdit
+ const isEditing = !!productToEdit;
useEffect(() => {
- setShowCreateProduct(isEditing)
- }, [isEditing])
+ setShowCreateProduct(isEditing);
+ }, [isEditing]);
- const [submitForm, addFormSubmitter] = useListener<Partial<MerchantBackend.Product> | undefined>((result) => {
+ const [submitForm, addFormSubmitter] = useListener<
+ Partial<TalerMerchantApi.Product> | undefined
+ >((result) => {
if (result) {
- setShowCreateProduct(false)
+ setShowCreateProduct(false);
return onAddProduct({
quantity: result.quantity || 0,
taxes: result.taxes || [],
- description: result.description || '',
- image: result.image || '',
- price: result.price || '',
- unit: result.unit || ''
- })
+ description: result.description || "",
+ image: result.image || "",
+ price: (result.price || "") as AmountString,
+ unit: result.unit || "",
+ });
}
- return Promise.resolve()
- })
-
- const i18n = useTranslator()
-
- return <Fragment>
- <div class="buttons">
- <button class="button is-success" data-tooltip={i18n`describe and add a product that is not in the inventory list`} onClick={() => setShowCreateProduct(true)} ><Translate>Add custom product</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`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)} ><Translate>Cancel</Translate></button>
- <button class="button is-info " disabled={!submitForm} onClick={submitForm} ><Translate>Confirm</Translate></button>
- </div>
- </footer>
+ 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>
- <button class="modal-close is-large " aria-label="close" onClick={() => setShowCreateProduct(false)} />
- </div>}
- </Fragment>
+ {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 {
@@ -99,48 +136,80 @@ interface NonInventoryProduct {
unit: string;
price: string;
image: string;
- taxes: MerchantBackend.Tax[];
+ taxes: TalerMerchantApi.Tax[];
}
export function ProductForm({ onSubscribe, initial }: ProductProps): VNode {
const [value, valueHandler] = useState<Partial<NonInventoryProduct>>({
taxes: [],
...initial,
- })
- let errors: FormErrors<Entity> = {}
+ });
+ let errors: FormErrors<NonInventoryProduct> = {};
try {
- schema.validateSync(value, { abortEarly: false })
+ 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 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])
+ return value as TalerMerchantApi.Product;
+ }, [value]);
- const hasErrors = Object.keys(errors).some(k => (errors as any)[k] !== undefined)
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
useEffect(() => {
- onSubscribe(hasErrors ? undefined : submit)
- }, [submit, hasErrors])
-
- const i18n = useTranslator()
-
- return <div>
- <FormProvider<NonInventoryProduct> name="product" errors={errors} object={value} valueHandler={valueHandler} >
-
- <InputImage<NonInventoryProduct> name="image" label={i18n`Image`} tooltip={i18n`photo of the product`} />
- <Input<NonInventoryProduct> name="description" inputType="multiline" label={i18n`Description`} tooltip={i18n`full product description`} />
- <Input<NonInventoryProduct> name="unit" label={i18n`Unit`} tooltip={i18n`name of the product unit`} />
- <InputCurrency<NonInventoryProduct> name="price" label={i18n`Price`} tooltip={i18n`amount in the current currency`} />
-
- <InputNumber<NonInventoryProduct> name="quantity" label={i18n`Quantity`} tooltip={i18n`how many products will be added`} />
-
- <InputTaxes<NonInventoryProduct> name="taxes" label={i18n`Taxes`} />
-
- </FormProvider>
- </div>
+ 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/merchant-backoffice-ui/src/components/product/ProductForm.tsx b/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx
index bf7489a94..c6d687b85 100644
--- a/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx
+++ b/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2021-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -19,17 +19,17 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
+import { AmountString, TalerMerchantApi } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { h } from "preact";
import { useCallback, useEffect, useState } from "preact/hooks";
import * as yup from "yup";
-import { useBackendContext } from "../../context/backend.js";
-import { MerchantBackend } from "../../declaration.js";
-import { useTranslator } from "../../i18n/index.js";
+import { useSessionContext } from "../../context/session.js";
import {
ProductCreateSchema as createSchema,
ProductUpdateSchema as updateSchema,
} from "../../schemas/index.js";
-import { FormProvider, FormErrors } from "../form/FormProvider.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";
@@ -38,7 +38,7 @@ 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 };
+type Entity = TalerMerchantApi.ProductDetail & { product_id: string };
interface Props {
onSubscribe: (c?: () => Entity | undefined) => void;
@@ -52,7 +52,7 @@ export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) {
description_i18n: {},
taxes: [],
next_restock: { t_s: "never" },
- price: ":0",
+ price: ":0" as AmountString,
...initial,
stock:
!initial || initial.total_stock === -1
@@ -77,16 +77,16 @@ export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) {
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
+ (k) => (errors as Record<string, unknown>)[k] !== undefined,
);
const submit = useCallback((): Entity | undefined => {
- const stock: Stock = (value as any).stock;
+ const stock = value.stock;
if (!stock) {
value.total_stock = -1;
@@ -99,13 +99,13 @@ export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) {
: stock.nextRestock;
value.address = stock.address;
}
- delete (value as any).stock;
+ delete value.stock;
if (typeof value.minimum_age !== "undefined" && value.minimum_age < 1) {
delete value.minimum_age;
}
- return value as MerchantBackend.Products.ProductDetail & {
+ return value as TalerMerchantApi.ProductDetail & {
product_id: string;
};
}, [value]);
@@ -114,9 +114,8 @@ export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) {
onSubscribe(hasErrors ? undefined : submit);
}, [submit, hasErrors]);
- const backend = useBackendContext();
- const i18n = useTranslator();
-
+ const { i18n } = useTranslationContext();
+ const { state } = useSessionContext();
return (
<div>
<FormProvider<Entity>
@@ -128,47 +127,49 @@ export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) {
{alreadyExist ? undefined : (
<InputWithAddon<Entity>
name="product_id"
- addonBefore={`${backend.url}/product/`}
- label={i18n`ID`}
- tooltip={i18n`product identification to use in URLs (for internal use only)`}
+ addonBefore={new URL("product/", state.backendUrl.href).href}
+ label={i18n.str`ID`}
+ tooltip={i18n.str`product identification to use in URLs (for internal use only)`}
/>
)}
<InputImage<Entity>
name="image"
- label={i18n`Image`}
- tooltip={i18n`illustration of the product for customers`}
+ label={i18n.str`Image`}
+ tooltip={i18n.str`illustration of the product for customers`}
/>
<Input<Entity>
name="description"
inputType="multiline"
- label={i18n`Description`}
- tooltip={i18n`product description for customers`}
+ label={i18n.str`Description`}
+ tooltip={i18n.str`product description for customers`}
/>
<InputNumber<Entity>
name="minimum_age"
- label={i18n`Age restricted`}
- tooltip={i18n`is this product restricted for customer below certain 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`Unit`}
- tooltip={i18n`unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 items, 5 meters) for customers`}
+ 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`Price`}
- tooltip={i18n`sale price for customers, including taxes, for above units of the product`}
+ 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`Stock`}
+ label={i18n.str`Stock`}
alreadyExist={alreadyExist}
- tooltip={i18n`product inventory for products with finite supply (for internal use only)`}
+ tooltip={i18n.str`inventory for products with finite supply (for internal use only)`}
/>
<InputTaxes<Entity>
name="taxes"
- label={i18n`Taxes`}
- tooltip={i18n`taxes included in the product price, exposed to customers`}
+ label={i18n.str`Taxes`}
+ tooltip={i18n.str`taxes included in the product price, exposed to customers`}
/>
</FormProvider>
</div>
diff --git a/packages/merchant-backoffice-ui/src/components/product/ProductList.tsx b/packages/merchant-backoffice-ui/src/components/product/ProductList.tsx
index 8b3d0fa20..4fff66fd7 100644
--- a/packages/merchant-backoffice-ui/src/components/product/ProductList.tsx
+++ b/packages/merchant-backoffice-ui/src/components/product/ProductList.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,40 +13,40 @@
You should have received 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 { Amounts, TalerMerchantApi } from "@gnu-taler/taler-util";
import { h, VNode } from "preact";
import emptyImage from "../../assets/empty.png";
-import { MerchantBackend } from "../../declaration.js";
-import { Translate } from "../../i18n/index.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
interface Props {
- list: MerchantBackend.Product[];
+ list: TalerMerchantApi.Product[];
actions?: {
name: string;
tooltip: string;
- handler: (d: MerchantBackend.Product, index: number) => void;
+ handler: (d: TalerMerchantApi.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>
- <Translate>image</Translate>
+ <i18n.Translate>image</i18n.Translate>
</th>
<th>
- <Translate>description</Translate>
+ <i18n.Translate>description</i18n.Translate>
</th>
<th>
- <Translate>quantity</Translate>
+ <i18n.Translate>quantity</i18n.Translate>
</th>
<th>
- <Translate>unit price</Translate>
+ <i18n.Translate>unit price</i18n.Translate>
</th>
<th>
- <Translate>total price</Translate>
+ <i18n.Translate>total price</i18n.Translate>
</th>
<th />
</tr>
@@ -59,8 +59,8 @@ export function ProductList({ list, actions = [] }: Props): VNode {
: Amounts.stringify(
Amounts.mult(
Amounts.parseOrThrow(entry.price),
- entry.quantity
- ).amount
+ entry.quantity ?? 0
+ ).amount,
);
return (
diff --git a/packages/merchant-backoffice-ui/src/context/backend.ts b/packages/merchant-backoffice-ui/src/context/backend.ts
deleted file mode 100644
index cbfebe2e4..000000000
--- a/packages/merchant-backoffice-ui/src/context/backend.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { createContext, h, VNode } from 'preact'
-import { useCallback, useContext, useState } from 'preact/hooks'
-import { useBackendDefaultToken, useBackendURL } from "../hooks/index.js";
-
-interface BackendContextType {
- url: string;
- token?: string;
- triedToLog: boolean;
- resetBackend: () => void;
- clearAllTokens: () => void;
- addTokenCleaner: (c: () => void) => void;
- updateLoginStatus: (url: string, token?: string) => void;
-}
-
-const BackendContext = createContext<BackendContextType>({
- url: '',
- token: undefined,
- triedToLog: false,
- resetBackend: () => null,
- clearAllTokens: () => null,
- addTokenCleaner: () => null,
- updateLoginStatus: () => null,
-})
-
-function useBackendContextState(defaultUrl?: string, initialToken?: string): BackendContextType {
- const [url, triedToLog, changeBackend, resetBackend] = useBackendURL(defaultUrl);
- const [token, _updateToken] = useBackendDefaultToken(initialToken);
- const updateToken = (t?: string) => {
- _updateToken(t)
- }
-
- const tokenCleaner = useCallback(() => { updateToken(undefined) }, [])
- const [cleaners, setCleaners] = useState([tokenCleaner])
- const addTokenCleaner = (c: () => void) => setCleaners(cs => [...cs, c])
- const addTokenCleanerMemo = useCallback((c: () => void) => { addTokenCleaner(c) }, [tokenCleaner])
-
- const clearAllTokens = () => {
- cleaners.forEach(c => c())
- for (let i = 0; i < localStorage.length; i++) {
- const k = localStorage.key(i)
- if (k && /^backend-token/.test(k)) localStorage.removeItem(k)
- }
- resetBackend()
- }
-
- const updateLoginStatus = (url: string, token?: string) => {
- changeBackend(url);
- if (token) updateToken(token);
- };
-
-
- return { url, token, triedToLog, updateLoginStatus, resetBackend, clearAllTokens, addTokenCleaner: addTokenCleanerMemo }
-}
-
-export const BackendContextProvider = ({ children, defaultUrl, initialToken }: { children: any, defaultUrl?: string, initialToken?: string }): VNode => {
- const value = useBackendContextState(defaultUrl, initialToken)
-
- return h(BackendContext.Provider, { value, children });
-}
-
-export const useBackendContext = (): BackendContextType => useContext(BackendContext);
diff --git a/packages/merchant-backoffice-ui/src/context/listener.ts b/packages/merchant-backoffice-ui/src/context/listener.ts
deleted file mode 100644
index 659db0a03..000000000
--- a/packages/merchant-backoffice-ui/src/context/listener.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { createContext } from 'preact'
-import { useContext } from 'preact/hooks'
-
-interface Type {
- id: string;
- token?: string;
- admin?: boolean;
- changeToken: (t?:string) => void;
-}
-
-const Context = createContext<Type>({} as any)
-
-export const ListenerContextProvider = Context.Provider
-export const useListenerContext = (): Type => useContext(Context);
diff --git a/packages/merchant-backoffice-ui/src/context/session.ts b/packages/merchant-backoffice-ui/src/context/session.ts
new file mode 100644
index 000000000..dbd188ccd
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/context/session.ts
@@ -0,0 +1,246 @@
+/*
+ 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 {
+ AccessToken,
+ Codec,
+ TalerMerchantApi,
+ buildCodecForObject,
+ codecForString,
+ codecForURL,
+ codecOptional,
+} from "@gnu-taler/taler-util";
+import {
+ buildStorageKey,
+ useLocalStorage,
+ useMerchantApiContext,
+} from "@gnu-taler/web-util/browser";
+import { ComponentChildren, VNode, createContext, h } from "preact";
+import { useContext, useEffect, useState } from "preact/hooks";
+import { mutate } from "swr";
+import { MerchantLib } from "../../../web-util/lib/context/activity.js";
+
+/**
+ * Has the information to reach and
+ * authenticate at the bank's backend.
+ */
+export type SessionState = LoggedIn | LoggedOut;
+
+interface LoggedIn {
+ status: "loggedIn";
+
+ // is this instance admin? usually "default" name
+ isAdmin: boolean;
+
+ // url where all the request will be made
+ // usually this is from where the SPA was loaded
+ // unless it's using impersonate feature
+ backendUrl: URL;
+
+ // instance name
+ instance: string;
+
+ // session is not the same from where it was loaded
+ impersonated: boolean;
+
+ //instance access token
+ token: AccessToken | undefined;
+}
+
+interface LoggedOut {
+ status: "loggedOut";
+ backendUrl: URL;
+ instance: string;
+ isAdmin: boolean;
+ token: AccessToken | undefined;
+}
+
+interface SavedSession {
+ backendUrl: URL;
+ token: AccessToken | undefined;
+ prevToken: AccessToken | undefined;
+}
+
+export const codecForSessionState = (): Codec<SavedSession> =>
+ buildCodecForObject<SavedSession>()
+ .property("backendUrl", codecForURL())
+ .property("token", codecOptional(codecForString() as Codec<AccessToken>))
+ .property(
+ "prevToken",
+ codecOptional(codecForString() as Codec<AccessToken>),
+ )
+ .build("SavedSession");
+
+function inferInstanceName(url: URL) {
+ const match = INSTANCE_ID_LOOKUP.exec(url.href);
+ return !match || !match[1] ? DEFAULT_ADMIN_USERNAME : match[1];
+}
+
+export const defaultState = (url: URL): SavedSession => {
+ return {
+ backendUrl: url,
+ token: undefined,
+ prevToken: undefined,
+ };
+};
+
+export interface SessionStateHandler {
+ lib: MerchantLib;
+ config: TalerMerchantApi.VersionResponse;
+
+ state: SessionState;
+ /**
+ * from every state to logout state
+ */
+ logOut(): void;
+ /**
+ * from impersonate to loggedIn
+ */
+ deImpersonate(): void;
+ /**
+ * from any to loggedIn
+ * @param info
+ */
+ logIn(token: AccessToken | undefined): void;
+ /**
+ * from loggedIn to impersonate
+ * @param info
+ */
+ impersonate(baseUrl: URL): void;
+}
+
+const SESSION_STATE_KEY = buildStorageKey(
+ "merchant-session",
+ codecForSessionState(),
+);
+
+export const DEFAULT_ADMIN_USERNAME = "default";
+
+export const INSTANCE_ID_LOOKUP = /\/instances\/([^/]*)\/?$/;
+
+export function cleanAllCache(): void {
+ mutate(() => true, undefined, { revalidate: false });
+}
+
+const Context = createContext<SessionStateHandler>(undefined!);
+
+export const useSessionContext = (): SessionStateHandler => useContext(Context);
+
+export const SessionContextProvider = ({
+ children,
+ // value,
+}: {
+ // value: MerchantUiSettings;
+ children: ComponentChildren;
+}): VNode => {
+ const {
+ lib: rootLib,
+ config: rootConfig,
+ url: merchantUrl,
+ } = useMerchantApiContext();
+ const [status, setStatus] = useState<"loggedIn" | "loggedOut">("loggedIn");
+ const [currentConfig, setCurrentConfig] =
+ useState<TalerMerchantApi.VersionResponse>();
+ const { value: state, update } = useLocalStorage(
+ SESSION_STATE_KEY,
+ defaultState(merchantUrl),
+ );
+
+ const currentInstance = inferInstanceName(state.backendUrl);
+
+ let lib: MerchantLib;
+ let config: TalerMerchantApi.VersionResponse;
+ const doingImpersonation = state.backendUrl.href !== merchantUrl.href;
+ if (doingImpersonation) {
+ /**
+ * FIXME: can't impersonate other than local instances
+ */
+ lib = rootLib.subInstanceApi(inferInstanceName(state.backendUrl));
+
+ config = currentConfig ?? rootConfig;
+ } else {
+ lib = rootLib;
+ config = rootConfig;
+ }
+
+ useEffect(() => {
+ // FIXME: handle what happen if the subinstance /config
+ // fails
+ if (!doingImpersonation) return;
+ lib.instance.getConfig().then((resp) => {
+ if (resp.type === "ok") {
+ setCurrentConfig(resp.body);
+ }
+ });
+ }, [state.backendUrl.href]);
+
+ const value: SessionStateHandler = {
+ state: {
+ backendUrl: state.backendUrl,
+ token: state.token,
+ impersonated: doingImpersonation,
+ instance: currentInstance,
+ isAdmin: currentInstance === DEFAULT_ADMIN_USERNAME,
+ status: status,
+ },
+ lib,
+ config,
+ logOut() {
+ setStatus("loggedOut");
+ update({
+ backendUrl: merchantUrl,
+ token: undefined,
+ prevToken: undefined,
+ });
+ cleanAllCache();
+ },
+ deImpersonate() {
+ cleanAllCache();
+ update({
+ backendUrl: merchantUrl,
+ token: state.prevToken,
+ prevToken: undefined,
+ });
+ setStatus("loggedIn");
+ },
+ impersonate(baseUrl) {
+ /**
+ * FIXME: can't impersonate other than local instances
+ */
+ update({
+ backendUrl: baseUrl,
+ token: undefined,
+ prevToken: state.token,
+ });
+ setStatus("loggedIn");
+ cleanAllCache();
+ },
+ logIn(token) {
+ cleanAllCache();
+ setStatus("loggedIn");
+ update({
+ backendUrl: state.backendUrl,
+ token: token,
+ prevToken: state.prevToken,
+ });
+ },
+ };
+
+ return h(Context.Provider, {
+ value,
+ children,
+ });
+};
diff --git a/packages/merchant-backend-ui/tests/__mocks__/browserMocks.ts b/packages/merchant-backoffice-ui/src/context/settings.ts
index ee6bba505..8bd1506d6 100644
--- a/packages/merchant-backend-ui/tests/__mocks__/browserMocks.ts
+++ b/packages/merchant-backoffice-ui/src/context/settings.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2021-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -14,29 +14,31 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- /**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
+import { ComponentChildren, createContext, h, VNode } from "preact";
+import { useContext } from "preact/hooks";
+import { MerchantUiSettings } from "../settings.js";
-// Mock Browser API's which are not supported by JSDOM, e.g. ServiceWorker, LocalStorage
/**
- * An example how to mock localStorage is given below 👇
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
*/
-/*
-// Mocks localStorage
-const localStorageMock = (function() {
- let store = {};
-
- return {
- getItem: (key) => store[key] || null,
- setItem: (key, value) => store[key] = value.toString(),
- clear: () => store = {}
- };
-
-})();
-
-Object.defineProperty(window, 'localStorage', {
- value: localStorageMock
-}); */
+export type Type = MerchantUiSettings;
+
+const initial: MerchantUiSettings = {};
+const Context = createContext<Type>(initial);
+
+export const useSettingsContext = (): Type => useContext(Context);
+
+export const SettingsProvider = ({
+ children,
+ value,
+}: {
+ value: MerchantUiSettings;
+ children: ComponentChildren;
+}): VNode => {
+ return h(Context.Provider, {
+ value,
+ children,
+ });
+};
diff --git a/packages/merchant-backoffice-ui/src/context/translation.ts b/packages/merchant-backoffice-ui/src/context/translation.ts
deleted file mode 100644
index 952a1e325..000000000
--- a/packages/merchant-backoffice-ui/src/context/translation.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { createContext, h, VNode } from 'preact'
-import { useContext, useEffect } from 'preact/hooks'
-import { useLang } from '../hooks'
-import * as jedLib from "jed";
-import { strings } from "../i18n/strings";
-
-interface Type {
- lang: string;
- handler: any;
- changeLanguage: (l: string) => void;
-}
-const initial = {
- lang: 'en',
- handler: null,
- changeLanguage: () => {
- // do not change anything
- }
-}
-const Context = createContext<Type>(initial)
-
-interface Props {
- initial?: string,
- children: any,
- forceLang?: string
-}
-
-export const TranslationProvider = ({ initial, children, forceLang }: Props): VNode => {
- const [lang, changeLanguage] = useLang(initial)
- useEffect(() => {
- if (forceLang) {
- changeLanguage(forceLang)
- }
- })
- const handler = new jedLib.Jed(strings[lang]);
- return h(Context.Provider, { value: { lang, handler, changeLanguage }, children });
-}
-
-export const useTranslationContext = (): Type => useContext(Context); \ No newline at end of file
diff --git a/packages/merchant-backoffice-ui/src/custom.d.ts b/packages/merchant-backoffice-ui/src/custom.d.ts
index d2705003b..34522a2dd 100644
--- a/packages/merchant-backoffice-ui/src/custom.d.ts
+++ b/packages/merchant-backoffice-ui/src/custom.d.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,11 +13,11 @@
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-declare module '*.po' {
+declare module "*.po" {
const content: any;
export default content;
}
-declare module 'jed' {
+declare module "jed" {
const x: any;
export = x;
}
@@ -29,12 +29,14 @@ declare module "*.png" {
const content: any;
export default content;
}
-declare module '*.svg' {
+declare module "*.svg" {
const content: any;
export default content;
}
-declare module '*.scss' {
+declare module "*.scss" {
const content: Record<string, string>;
export default content;
}
+declare const __VERSION__: string;
+declare const __GIT_HASH__: string;
diff --git a/packages/merchant-backoffice-ui/src/declaration.d.ts b/packages/merchant-backoffice-ui/src/declaration.d.ts
index f0d257d3c..1baf80ba6 100644
--- a/packages/merchant-backoffice-ui/src/declaration.d.ts
+++ b/packages/merchant-backoffice-ui/src/declaration.d.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,1429 +15,10 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-
-type HashCode = string;
-type EddsaPublicKey = string;
-type EddsaSignature = string;
-type WireTransferIdentifierRawP = string;
-type RelativeTime = Duration;
-type ImageDataUrl = string;
-
-export interface WithId {
- id: string
-}
-
-interface Timestamp {
- // Milliseconds since epoch, or the special
- // value "forever" to represent an event that will
- // never happen.
- t_s: number | "never";
-}
-interface Duration {
- d_us: number | "forever";
-}
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
interface WithId {
- id: string;
-}
-
-type Amount = string;
-type UUID = string;
-type Integer = number;
-
-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 WireAccount {
- // payto:// URI identifying the account and wire method
- payto_uri: string;
-
- // Signature using the exchange's offline key
- // with purpose TALER_SIGNATURE_MASTER_WIRE_DETAILS.
- master_sig: EddsaSignature;
- }
- 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 {
- // The URI where the wallet will send coins. A merchant may have
- // multiple accounts, thus this is an array. Note that by
- // removing URIs from this list the respective account is set to
- // inactive and thus unavailable for new contracts, but preserved
- // in the database as existing offers and contracts may still refer
- // to it.
- payto_uris: string[];
-
- // Name of the merchant instance to create (will become $INSTANCE).
- id: string;
-
- // Merchant name corresponding to this instance.
- name: string;
-
- email: string;
- website: string;
- // An optional base64-encoded logo image
- 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;
-
- // Maximum wire fee this instance is willing to pay.
- // Can be overridden by the frontend on a per-order basis.
- default_max_wire_fee: Amount;
-
- // Default factor for wire fee amortization calculations.
- // Can be overridden by the frontend on a per-order basis.
- default_wire_fee_amortization: Integer;
-
- // Maximum deposit fee (sum over all coins) this instance is willing to pay.
- // Can be overridden by the frontend on a per-order basis.
- default_max_deposit_fee: Amount;
-
- // 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 {
- // The URI where the wallet will send coins. A merchant may have
- // multiple accounts, thus this is an array. Note that by
- // removing URIs from this list
- payto_uris: string[];
-
- // Merchant name corresponding to this instance.
- name: string;
-
- // 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;
-
- // Maximum wire fee this instance is willing to pay.
- // Can be overridden by the frontend on a per-order basis.
- default_max_wire_fee: Amount;
-
- // Default factor for wire fee amortization calculations.
- // Can be overridden by the frontend on a per-order basis.
- default_wire_fee_amortization: Integer;
-
- // Maximum deposit fee (sum over all coins) this instance is willing to pay.
- // Can be overridden by the frontend on a per-order basis.
- default_max_deposit_fee: Amount;
-
- // 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;
-
- deleted?: boolean;
-
- // 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[];
-
- }
-
- //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).
- kyc_url: string;
-
- // Base URL of the exchange this is about.
- exchange_url: string;
-
- // 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;
-
- }
-
- //GET /private/instances/$INSTANCE
- interface QueryInstancesResponse {
- // The URI where the wallet will send coins. A merchant may have
- // multiple accounts, thus this is an array.
- accounts: MerchantAccount[];
-
- // Merchant name corresponding to this instance.
- name: string;
-
- // 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;
-
- // Maximum wire fee this instance is willing to pay.
- // Can be overridden by the frontend on a per-order basis.
- default_max_wire_fee: Amount;
-
- // Default factor for wire fee amortization calculations.
- // Can be overridden by the frontend on a per-order basis.
- default_wire_fee_amortization: Integer;
-
- // Maximum deposit fee (sum over all coins) this instance is willing to pay.
- // Can be overridden by the frontend on a per-order basis.
- default_max_deposit_fee: Amount;
-
- // 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";
- token?: string;
- };
- }
-
- interface MerchantAccount {
-
- // 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;
-
- // true if this account is active,
- // false if it is historic.
- active: boolean;
- }
-
- // DELETE /private/instances/$INSTANCE
-
-
- }
-
- 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;
-
- }
- 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 Tips {
-
- // GET /private/reserves
- interface TippingReserveStatus {
- // 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 tips 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 tipping
- 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 account of the exchange where to transfer the funds
- payto_uri: string;
- }
- interface TipCreateRequest {
- // Amount that the customer should be tipped
- amount: Amount;
-
- // Justification for giving the tip
- justification: string;
-
- // URL that the user should be directed to after tipping,
- // will be included in the tip_token.
- next_url: string;
- }
- interface TipCreateConfirmation {
- // Unique tip identifier for the tip that was created.
- tip_id: HashCode;
-
- // taler://tip URI for the tip
- taler_tip_uri: string;
-
- // URL that will directly trigger processing
- // the tip when the browser is redirected to it
- tip_status_url: string;
-
- // when does the tip expire
- tip_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 tips that exceeds the pickup_amount.
- committed_amount: Amount;
-
- // Array of all tips created by this reserves (possibly empty!).
- // Only present if asked for explicitly.
- tips?: TipStatusEntry[];
-
- // Is this reserve active (false if it was deleted but not purged)?
- active: boolean;
-
- // URI to use to fill the reserve, can be NULL
- // if the reserve is inactive or was already filled
- payto_uri: string;
-
- // URL of the exchange hosting the reserve,
- // NULL if the reserve is inactive
- exchange_url: string;
-
- }
-
- interface TipStatusEntry {
-
- // Unique identifier for the tip.
- tip_id: HashCode;
-
- // Total amount of the tip that can be withdrawn.
- total_amount: Amount;
-
- // Human-readable reason for why the tip was granted.
- reason: string;
- }
-
- interface TipDetails {
- // Amount that we authorized for this tip.
- total_authorized: Amount;
-
- // Amount that was picked up by the user already.
- total_picked_up: Amount;
-
- // Human-readable reason given when authorizing the tip.
- reason: string;
-
- // Timestamp indicating when the tip is set to expire (may be in the past).
- expiration: Timestamp;
-
- // Reserve public key from which the tip 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;
- }
- interface MerchantTrackTransferResponse {
- // Total amount transferred
- total: Amount;
-
- // Applicable wire fee that was charged
- wire_fee: Amount;
-
- // Time of the execution of the wire transfer by the exchange, according to the exchange
- execution_time: Timestamp;
-
- // details about the deposits
- deposits_sums: MerchantTrackTransferDetail[];
- }
- interface MerchantTrackTransferDetail {
- // Business activity associated with the wire transferred amount
- // deposit_value.
- order_id: string;
-
- // The total amount the exchange paid back for order_id.
- deposit_value: Amount;
-
- // applicable fees for the deposit
- deposit_fee: Amount;
- }
-
- type ExchangeConflictDetails = WireFeeConflictDetails | TrackTransferConflictDetails
- // Note: this is not the full 'proof' of missbehavior, as
- // the bogus message from the exchange with a signature
- // over the 'different' wire fee is missing.
- //
- // This information is NOT provided by the current implementation,
- // because this would be quite expensive to generate and is
- // hardly needed _here_. Once we add automated reports for
- // the Taler auditor, we need to generate this data anyway
- // and should probably return it here as well.
- interface WireFeeConflictDetails {
- // Numerical error code:
- code: "TALER_EC_MERCHANT_PRIVATE_POST_TRANSFERS_BAD_WIRE_FEE";
-
- // Text describing the issue for humans.
- hint: string;
-
-
- // Wire fee (wrongly) charged by the exchange, breaking the
- // contract affirmed by the exchange_sig.
- wire_fee: Amount;
-
- // Timestamp of the wire transfer
- execution_time: Timestamp;
-
- // The expected wire fee (as signed by the exchange)
- expected_wire_fee: Amount;
-
- // Expected closing fee (needed to verify signature)
- expected_closing_fee: Amount;
-
- // Start date of the expected fee structure
- start_date: Timestamp;
-
- // End date of the expected fee structure
- end_date: Timestamp;
-
- // Signature of the exchange affirming the expected fee structure
- master_sig: EddsaSignature;
-
- // Master public key of the exchange
- master_pub: EddsaPublicKey;
- }
- interface TrackTransferConflictDetails {
- // Numerical error code
- code: "TALER_EC_MERCHANT_PRIVATE_POST_TRANSFERS_CONFLICTING_REPORTS";
-
- // Text describing the issue for humans.
- hint: string;
-
- // Offset in the exchange_transfer where the
- // exchange's response fails to match the exchange_deposit_proof.
- conflict_offset: number;
-
- // The response from the exchange which tells us when the
- // coin was returned to us, except that it does not match
- // the expected value of the coin.
- //
- // This field is NOT provided by the current implementation,
- // because this would be quite expensive to generate and is
- // hardly needed _here_. Once we add automated reports for
- // the Taler auditor, we need to generate this data anyway
- // and should probably return it here as well.
- // exchange_transfer?: TrackTransferResponse;
-
- // Public key of the exchange used to sign the response to
- // our deposit request.
- deposit_exchange_pub: EddsaPublicKey;
-
- // Signature of the exchange signing the (conflicting) response.
- // Signs over a struct TALER_DepositConfirmationPS.
- deposit_exchange_sig: EddsaSignature;
-
- // Hash of the merchant's bank account the wire transfer went to
- h_wire: HashCode;
-
- // Hash of the contract terms with the conflicting deposit.
- h_contract_terms: HashCode;
-
- // At what time the exchange received the deposit. Needed
- // to verify the \exchange_sig\.
- deposit_timestamp: Timestamp;
-
- // At what time the refund possibility expired (needed to verify exchange_sig).
- refund_deadline: Timestamp;
-
- // Public key of the coin for which we have conflicting information.
- coin_pub: EddsaPublicKey;
-
- // Amount the exchange counted the coin for in the transfer.
- amount_with_fee: Amount;
-
- // Expected value of the coin.
- coin_value: Amount;
-
- // Expected deposit fee of the coin.
- coin_fee: Amount;
-
- // Expected deposit fee of the coin.
- deposit_fee: Amount;
-
- }
-
- // interface TrackTransferProof {
- // // signature from the exchange made with purpose
- // // TALER_SIGNATURE_EXCHANGE_CONFIRM_WIRE_DEPOSIT
- // exchange_sig: EddsaSignature;
-
- // // public EdDSA key of the exchange that was used to generate the signature.
- // // Should match one of the exchange's signing keys from /keys. Again given
- // // explicitly as the client might otherwise be confused by clock skew as to
- // // which signing key was used.
- // exchange_pub: EddsaSignature;
-
- // // hash of the wire details (identical for all deposits)
- // // Needed to check the exchange_sig
- // h_wire: HashCode;
- // }
-
- }
-
-
- 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;
-
- // Maximum wire fee accepted by the merchant (customer share to be
- // divided by the 'wire_fee_amortization' factor, and further reduced
- // if deposit fees are below 'max_fee'). Default if missing is zero.
- max_wire_fee: Amount;
-
- // Over how many customer transactions does the merchant expect to
- // amortize wire fees on average? If the exchange's wire fee is
- // above 'max_wire_fee', the difference is divided by this number
- // to compute the expected customer's contribution to the wire fee.
- // The customer's contribution may further be reduced by the difference
- // between the 'max_fee' and the sum of the actual deposit fees.
- // Optional, default value if missing is 1. 0 and negative values are
- // invalid and also interpreted as 1.
- wire_fee_amortization: number;
-
- // 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;
- }
-
+ id: string;
}
diff --git a/packages/merchant-backoffice-ui/src/hooks/async.ts b/packages/merchant-backoffice-ui/src/hooks/async.ts
index 69cb231a4..212ef2211 100644
--- a/packages/merchant-backoffice-ui/src/hooks/async.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/async.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,38 +15,40 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
import { useState } from "preact/hooks";
-import { cancelPendingRequest } from "./backend.js";
export interface Options {
- slowTolerance: number,
+ slowTolerance: number;
}
export interface AsyncOperationApi<T> {
- request: (...a: any) => void,
- cancel: () => void,
- data: T | undefined,
- isSlow: boolean,
- isLoading: boolean,
- error: string | undefined
+ 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> {
+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 [isSlow, setSlow] = useState(false);
const request = async (...args: any) => {
if (!fn) return;
setLoading(true);
const handler = setTimeout(() => {
- setSlow(true)
- }, tooLong)
+ setSlow(true);
+ }, tooLong);
try {
const result = await fn(...args);
@@ -55,14 +57,13 @@ export function useAsync<T>(fn?: (...args: any) => Promise<T>, { slowTolerance:
setError(error);
}
setLoading(false);
- setSlow(false)
- clearTimeout(handler)
+ setSlow(false);
+ clearTimeout(handler);
};
- function cancel() {
- cancelPendingRequest()
+ function cancel(): void {
setLoading(false);
- setSlow(false)
+ setSlow(false);
}
return {
@@ -71,6 +72,6 @@ export function useAsync<T>(fn?: (...args: any) => Promise<T>, { slowTolerance:
data,
isSlow,
isLoading,
- error
+ error,
};
}
diff --git a/packages/merchant-backoffice-ui/src/hooks/backend.ts b/packages/merchant-backoffice-ui/src/hooks/backend.ts
deleted file mode 100644
index 6262a0bca..000000000
--- a/packages/merchant-backoffice-ui/src/hooks/backend.ts
+++ /dev/null
@@ -1,319 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import { useSWRConfig } from "swr";
-import axios, { AxiosError, AxiosResponse } from "axios";
-import { MerchantBackend } from "../declaration.js";
-import { useBackendContext } from "../context/backend.js";
-import { useEffect, useState } from "preact/hooks";
-import { DEFAULT_REQUEST_TIMEOUT } from "../utils/constants.js";
-import { axiosHandler, removeAxiosCancelToken } from "../utils/switchableAxios.js";
-
-export function useMatchMutate(): (
- re: RegExp,
- value?: unknown
-) => Promise<any> {
- const { cache, mutate } = useSWRConfig();
-
- if (!(cache instanceof Map)) {
- throw new Error(
- "matchMutate requires the cache provider to be a Map instance"
- );
- }
-
- return function matchRegexMutate(re: RegExp, value?: unknown) {
- const allKeys = Array.from(cache.keys());
- // console.log(allKeys)
- const keys = allKeys.filter((key) => re.test(key));
- // console.log(allKeys.length, keys.length)
- const mutations = keys.map((key) => {
- // console.log(key)
- mutate(key, value, true);
- });
- return Promise.all(mutations);
- };
-}
-
-export type HttpResponse<T> =
- | HttpResponseOk<T>
- | HttpResponseLoading<T>
- | HttpError;
-export type HttpResponsePaginated<T> =
- | HttpResponseOkPaginated<T>
- | HttpResponseLoading<T>
- | HttpError;
-
-export interface RequestInfo {
- url: string;
- hasToken: boolean;
- params: unknown;
- data: unknown;
- status: number;
-}
-
-interface HttpResponseLoading<T> {
- ok?: false;
- loading: true;
- clientError?: false;
- serverError?: false;
-
- data?: T;
-}
-export interface HttpResponseOk<T> {
- ok: true;
- loading?: false;
- clientError?: false;
- serverError?: false;
-
- data: T;
- info?: RequestInfo;
-}
-
-export type HttpResponseOkPaginated<T> = HttpResponseOk<T> & WithPagination;
-
-export interface WithPagination {
- loadMore: () => void;
- loadMorePrev: () => void;
- isReachingEnd?: boolean;
- isReachingStart?: boolean;
-}
-
-export type HttpError =
- | HttpResponseClientError
- | HttpResponseServerError
- | HttpResponseUnexpectedError;
-export interface SwrError {
- info: unknown;
- status: number;
- message: string;
-}
-export interface HttpResponseServerError {
- ok?: false;
- loading?: false;
- clientError?: false;
- serverError: true;
-
- error?: MerchantBackend.ErrorDetail;
- status: number;
- message: string;
- info?: RequestInfo;
-}
-interface HttpResponseClientError {
- ok?: false;
- loading?: false;
- clientError: true;
- serverError?: false;
-
- info?: RequestInfo;
- isUnauthorized: boolean;
- isNotfound: boolean;
- status: number;
- error?: MerchantBackend.ErrorDetail;
- message: string;
-}
-
-interface HttpResponseUnexpectedError {
- ok?: false;
- loading?: false;
- clientError?: false;
- serverError?: false;
-
- info?: RequestInfo;
- status?: number;
- error: unknown;
- message: string;
-}
-
-type Methods = "get" | "post" | "patch" | "delete" | "put";
-
-interface RequestOptions {
- method?: Methods;
- token?: string;
- data?: unknown;
- params?: unknown;
-}
-
-function buildRequestOk<T>(
- res: AxiosResponse<T>,
- url: string,
- hasToken: boolean
-): HttpResponseOk<T> {
- return {
- ok: true,
- data: res.data,
- info: {
- params: res.config.params,
- data: res.config.data,
- url,
- hasToken,
- status: res.status,
- },
- };
-}
-
-// function buildResponse<T>(data?: T, error?: MerchantBackend.ErrorDetail, isValidating?: boolean): HttpResponse<T> {
-// if (isValidating) return {loading: true}
-// if (error) return buildRequestFailed()
-// }
-
-function buildRequestFailed(
- ex: AxiosError<MerchantBackend.ErrorDetail>,
- url: string,
- hasToken: boolean
-):
- | HttpResponseClientError
- | HttpResponseServerError
- | HttpResponseUnexpectedError {
- const status = ex.response?.status;
-
- const info: RequestInfo = {
- data: ex.request?.data,
- params: ex.request?.params,
- url,
- hasToken,
- status: status || 0,
- };
-
- if (status && status >= 400 && status < 500) {
- const error: HttpResponseClientError = {
- clientError: true,
- isNotfound: status === 404,
- isUnauthorized: status === 401,
- status,
- info,
- message: ex.response?.data?.hint || ex.message,
- error: ex.response?.data,
- };
- return error;
- }
- if (status && status >= 500 && status < 600) {
- const error: HttpResponseServerError = {
- serverError: true,
- status,
- info,
- message:
- `${ex.response?.data?.hint} (code ${ex.response?.data?.code})` ||
- ex.message,
- error: ex.response?.data,
- };
- return error;
- }
-
- const error: HttpResponseUnexpectedError = {
- info,
- status,
- error: ex,
- message: ex.message,
- };
-
- return error;
-}
-
-const CancelToken = axios.CancelToken;
-let source = CancelToken.source();
-
-export function cancelPendingRequest(): void {
- source.cancel("canceled by the user");
- source = CancelToken.source();
-}
-
-export function isAxiosError<T>(
- error: AxiosError | any
-): error is AxiosError<T> {
- return error && error.isAxiosError;
-}
-
-export async function request<T>(
- url: string,
- options: RequestOptions = {}
-): Promise<HttpResponseOk<T>> {
- const headers = options.token
- ? { Authorization: `Bearer ${options.token}` }
- : undefined;
-
- try {
- const res = await axiosHandler({
- url,
- responseType: "json",
- headers,
- cancelToken: !removeAxiosCancelToken ? source.token : undefined,
- method: options.method || "get",
- data: options.data,
- params: options.params,
- timeout: DEFAULT_REQUEST_TIMEOUT * 1000,
- });
- return buildRequestOk<T>(res, url, !!options.token);
- } catch (e) {
- if (isAxiosError<MerchantBackend.ErrorDetail>(e)) {
- const error = buildRequestFailed(e, url, !!options.token);
- throw error;
- }
- throw e;
- }
-}
-
-export function multiFetcher<T>(
- urls: string[],
- token: string,
- backend: string
-): Promise<HttpResponseOk<T>[]> {
- return Promise.all(urls.map((url) => fetcher<T>(url, token, backend)));
-}
-
-export function fetcher<T>(
- url: string,
- token: string,
- backend: string
-): Promise<HttpResponseOk<T>> {
- return request<T>(`${backend}${url}`, { token });
-}
-
-export function useBackendInstancesTestForAdmin(): HttpResponse<MerchantBackend.Instances.InstancesResponse> {
- const { url, token } = useBackendContext();
-
- type Type = MerchantBackend.Instances.InstancesResponse;
-
- const [result, setResult] = useState<HttpResponse<Type>>({ loading: true });
-
- useEffect(() => {
- request<Type>(`${url}/management/instances`, { token })
- .then((data) => setResult(data))
- .catch((error) => setResult(error));
- }, [url, token]);
-
- return result;
-}
-
-export function useBackendConfig(): HttpResponse<MerchantBackend.VersionResponse> {
- const { url, token } = useBackendContext();
-
- type Type = MerchantBackend.VersionResponse;
-
- const [result, setResult] = useState<HttpResponse<Type>>({ loading: true });
-
- useEffect(() => {
- request<Type>(`${url}/config`, { token })
- .then((data) => setResult(data))
- .catch((error) => setResult(error));
- }, [url, token]);
-
- return result;
-}
diff --git a/packages/merchant-backoffice-ui/src/hooks/bank.ts b/packages/merchant-backoffice-ui/src/hooks/bank.ts
new file mode 100644
index 000000000..8857ad839
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/hooks/bank.ts
@@ -0,0 +1,86 @@
+/*
+ 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 {
+ 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 _useSWR, { SWRHook, mutate } from "swr";
+import { useSessionContext } from "../context/session.js";
+const useSWR = _useSWR as unknown as SWRHook;
+
+export interface InstanceBankAccountFilter {
+}
+
+export function revalidateInstanceBankAccounts() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "listBankAccounts",
+ undefined,
+ { revalidate: true },
+ );
+}
+export function useInstanceBankAccounts() {
+ const { state: session } = useSessionContext();
+ const { lib: { instance } } = useSessionContext();
+
+ // const [offset, setOffset] = useState<string | undefined>();
+
+ async function fetcher([token, _bid]: [AccessToken, string]) {
+ return await instance.listBankAccounts(token, {
+ // limit: PAGINATED_LIST_REQUEST,
+ // offset: bid,
+ // order: "dec",
+ });
+ }
+
+ const { data, error } = useSWR<
+ TalerMerchantManagementResultByMethod<"listBankAccounts">,
+ TalerHttpError
+ >([session.token, "offset", "listBankAccounts"], fetcher);
+
+ if (error) return error;
+ if (data === undefined) return undefined;
+ if (data.type !== "ok") return data;
+
+ // return buildPaginatedResult(data.body.accounts, offset, setOffset, (d) => d.h_wire)
+ return data;
+}
+
+export function revalidateBankAccountDetails() {
+ return mutate(
+ (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();
+
+ async function fetcher([token, wireId]: [AccessToken, string]) {
+ return await instance.getBankAccountDetails(token, wireId);
+ }
+
+ const { data, error } = useSWR<
+ TalerMerchantManagementResultByMethod<"getBankAccountDetails">,
+ TalerHttpError
+ >([session.token, h_wire, "getBankAccountDetails"], fetcher);
+
+ if (data) return data;
+ if (error) return error;
+ return undefined;
+}
diff --git a/packages/merchant-backoffice-ui/src/hooks/index.ts b/packages/merchant-backoffice-ui/src/hooks/index.ts
deleted file mode 100644
index e61dc4c34..000000000
--- a/packages/merchant-backoffice-ui/src/hooks/index.ts
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { StateUpdater, useCallback, useState } from "preact/hooks";
-import { ValueOrFunction } from "../utils/types.js";
-
-
-const calculateRootPath = () => {
- const rootPath = typeof window !== undefined ? window.location.origin + window.location.pathname : '/'
- return rootPath
-}
-
-export function useBackendURL(url?: string): [string, boolean, StateUpdater<string>, () => void] {
- const [value, setter] = useNotNullLocalStorage('backend-url', url || calculateRootPath())
- const [triedToLog, setTriedToLog] = useLocalStorage('tried-login')
-
- const checkedSetter = (v: ValueOrFunction<string>) => {
- setTriedToLog('yes')
- return setter(p => (v instanceof Function ? v(p) : v).replace(/\/$/, ''))
- }
-
- const resetBackend = () => {
- setTriedToLog(undefined)
- }
- return [value, !!triedToLog, checkedSetter, resetBackend]
-}
-
-export function useBackendDefaultToken(initialValue?: string): [string | undefined, StateUpdater<string | undefined>] {
- return useLocalStorage('backend-token', initialValue)
-}
-
-export function useBackendInstanceToken(id: string): [string | undefined, StateUpdater<string | undefined>] {
- const [token, setToken] = useLocalStorage(`backend-token-${id}`)
- const [defaultToken, defaultSetToken] = useBackendDefaultToken()
-
- // instance named 'default' use the default token
- if (id === 'default') {
- return [defaultToken, defaultSetToken]
- }
-
- return [token, setToken]
-}
-
-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 useNotNullLocalStorage('lang-preference', defaultLang)
-}
-
-export function useLocalStorage(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];
-}
-
-export function useNotNullLocalStorage(key: string, initialValue: string): [string, StateUpdater<string>] {
- const [storedValue, setStoredValue] = useState<string>((): string => {
- return typeof window !== "undefined" ? window.localStorage.getItem(key) || initialValue : initialValue;
- });
-
- const setValue = (value: string | ((val: string) => string)) => {
- const valueToStore = value instanceof Function ? value(storedValue) : value;
- setStoredValue(valueToStore);
- if (typeof window !== "undefined") {
- if (!valueToStore) {
- window.localStorage.removeItem(key)
- } else {
- window.localStorage.setItem(key, valueToStore);
- }
- }
- };
-
- return [storedValue, setValue];
-}
-
-
diff --git a/packages/merchant-backoffice-ui/src/hooks/instance.test.ts b/packages/merchant-backoffice-ui/src/hooks/instance.test.ts
new file mode 100644
index 000000000..f409592b0
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/hooks/instance.test.ts
@@ -0,0 +1,741 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { TalerMerchantApi } from "@gnu-taler/taler-util";
+import { useMerchantApiContext } from "@gnu-taler/web-util/browser";
+import * as tests from "@gnu-taler/web-util/testing";
+import { expect } from "chai";
+import {
+ useBackendInstances,
+ useInstanceDetails,
+} from "./instance.js";
+import { ApiMockEnvironment } from "./testing.js";
+import {
+ API_CREATE_INSTANCE,
+ API_DELETE_INSTANCE,
+ API_GET_CURRENT_INSTANCE,
+ API_LIST_INSTANCES,
+ 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 TalerMerchantApi.QueryInstancesResponse,
+ });
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ // const api = useInstanceAPI();
+ const { lib: api } = useMerchantApiContext()
+ 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 TalerMerchantApi.InstanceReconfigurationMessage,
+ });
+ env.addRequestExpectation(API_GET_CURRENT_INSTANCE, {
+ response: {
+ name: "other_name",
+ } as TalerMerchantApi.QueryInstancesResponse,
+ });
+ api.instance.updateCurrentInstance(undefined, {
+ name: "other_name",
+ } as TalerMerchantApi.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 TalerMerchantApi.QueryInstancesResponse,
+ // });
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const { lib: api } = useMerchantApiContext()
+ 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 TalerMerchantApi.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 TalerMerchantApi.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 TalerMerchantApi.QueryInstancesResponse,
+ });
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const { lib: api } = useMerchantApiContext()
+ 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 TalerMerchantApi.InstanceAuthConfigurationMessage,
+ });
+ env.addRequestExpectation(API_GET_CURRENT_INSTANCE, {
+ response: {
+ name: "instance_name",
+ auth: {
+ method: "external",
+ },
+ } as TalerMerchantApi.QueryInstancesResponse,
+ });
+
+ api.instance.updateCurrentInstanceAuthentication(undefined, {
+ method: "external"
+ });
+ },
+ ({ 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 TalerMerchantApi.Instance,
+ ],
+ },
+ });
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const { lib: api } = useMerchantApiContext()
+ 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 TalerMerchantApi.InstanceConfigurationMessage,
+ });
+ env.addRequestExpectation(API_LIST_INSTANCES, {
+ response: {
+ instances: [
+ {
+ name: "instance_name",
+ } as TalerMerchantApi.Instance,
+ {
+ name: "other_name",
+ } as TalerMerchantApi.Instance,
+ ],
+ },
+ });
+
+ api.instance.createInstance(undefined, {
+ name: "other_name",
+ } as TalerMerchantApi.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 TalerMerchantApi.Instance,
+ {
+ id: "the_id",
+ name: "second_instance",
+ } as TalerMerchantApi.Instance,
+ ],
+ },
+ });
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const { lib: api } = useMerchantApiContext()
+ 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 TalerMerchantApi.Instance,
+ ],
+ },
+ });
+
+ api.instance.deleteInstance(undefined, "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 TalerMerchantApi.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 TalerMerchantApi.Instance,
+ {
+ id: "the_id",
+ name: "second_instance",
+ } as TalerMerchantApi.Instance,
+ ],
+ },
+ });
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const { lib: api } = useMerchantApiContext()
+ 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 TalerMerchantApi.Instance,
+ ],
+ },
+ });
+
+ api.instance.deleteInstance(undefined, "the_id", { purge: 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",
+ // },
+ // ],
+ // });
+ },
+ ],
+ 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 TalerMerchantApi.Instance,
+ ],
+ },
+ });
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const { lib: api } = useMerchantApiContext()
+ 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 TalerMerchantApi.InstanceReconfigurationMessage,
+ });
+ env.addRequestExpectation(API_LIST_INSTANCES, {
+ response: {
+ instances: [
+ {
+ id: "managed",
+ name: "other_name",
+ } as TalerMerchantApi.Instance,
+ ],
+ },
+ });
+
+ api.instance.updateCurrentInstance(undefined, {
+ name: "other_name",
+ } as TalerMerchantApi.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/merchant-backoffice-ui/src/hooks/instance.ts b/packages/merchant-backoffice-ui/src/hooks/instance.ts
index fd78aabf6..f5f8893cd 100644
--- a/packages/merchant-backoffice-ui/src/hooks/instance.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/instance.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,280 +13,112 @@
You should have received 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 useSWR, { useSWRConfig } from "swr";
-import { useBackendContext } from "../context/backend.js";
-import { useInstanceContext } from "../context/instance.js";
-import { MerchantBackend } from "../declaration.js";
-import {
- fetcher,
- HttpError,
- HttpResponse,
- HttpResponseOk,
- request,
- useMatchMutate,
-} from "./backend.js";
-interface InstanceAPI {
- updateInstance: (
- data: MerchantBackend.Instances.InstanceReconfigurationMessage
- ) => Promise<void>;
- deleteInstance: () => Promise<void>;
- clearToken: () => Promise<void>;
- setNewToken: (token: string) => Promise<void>;
-}
-
-export function useAdminAPI(): AdminAPI {
- const { url, token } = useBackendContext();
- const mutateAll = useMatchMutate();
-
- const createInstance = async (
- instance: MerchantBackend.Instances.InstanceConfigurationMessage
- ): Promise<void> => {
- await request(`${url}/management/instances`, {
- method: "post",
- token,
- data: instance,
- });
-
- mutateAll(/\/management\/instances/);
- };
-
- const deleteInstance = async (id: string): Promise<void> => {
- await request(`${url}/management/instances/${id}`, {
- method: "delete",
- token,
- });
-
- mutateAll(/\/management\/instances/);
- };
+// FIX default import https://github.com/microsoft/TypeScript/issues/49189
+import { AccessToken, TalerHttpError, TalerMerchantManagementResultByMethod } from "@gnu-taler/taler-util";
+import _useSWR, { SWRHook, mutate } from "swr";
+import { useSessionContext } from "../context/session.js";
+const useSWR = _useSWR as unknown as SWRHook;
- const purgeInstance = async (id: string): Promise<void> => {
- await request(`${url}/management/instances/${id}`, {
- method: "delete",
- token,
- 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 revalidateInstanceDetails() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "getCurrentInstanceDetails",
+ undefined,
+ { revalidate: true },
+ );
}
+export function useInstanceDetails() {
+ const { state: session } = useSessionContext();
+ const { lib: { instance } } = useSessionContext();
-export function useManagementAPI(instanceId: string): InstanceAPI {
- const mutateAll = useMatchMutate();
- const { url, token, updateLoginStatus } = useBackendContext();
-
- const updateInstance = async (
- instance: MerchantBackend.Instances.InstanceReconfigurationMessage
- ): Promise<void> => {
- await request(`${url}/management/instances/${instanceId}`, {
- method: "patch",
- token,
- data: instance,
- });
-
- mutateAll(/\/management\/instances/);
- };
-
- const deleteInstance = async (): Promise<void> => {
- await request(`${url}/management/instances/${instanceId}`, {
- method: "delete",
- token,
- });
-
- mutateAll(/\/management\/instances/);
- };
-
- const clearToken = async (): Promise<void> => {
- await request(`${url}/management/instances/${instanceId}/auth`, {
- method: "post",
- token,
- data: { method: "external" },
- });
-
- mutateAll(/\/management\/instances/);
- };
-
- const setNewToken = async (newToken: string): Promise<void> => {
- await request(`${url}/management/instances/${instanceId}/auth`, {
- method: "post",
- token,
- data: { method: "token", token: newToken },
- });
+ async function fetcher([token]: [AccessToken]) {
+ return await instance.getCurrentInstanceDetails(token);
+ }
- updateLoginStatus(url, newToken)
- mutateAll(/\/management\/instances/);
- };
+ const { data, error } = useSWR<
+ TalerMerchantManagementResultByMethod<"getCurrentInstanceDetails">,
+ TalerHttpError
+ >([session.token, "getCurrentInstanceDetails"], fetcher);
- return { updateInstance, deleteInstance, setNewToken, clearToken };
+ if (data) return data;
+ if (error) return error;
+ return undefined;
}
-export function useInstanceAPI(): InstanceAPI {
- const { mutate } = useSWRConfig();
- const { url: baseUrl, token: adminToken, updateLoginStatus } = useBackendContext();
- const { token: instanceToken, id, admin } = useInstanceContext();
-
- const { url, token } = !admin
- ? { url: baseUrl, token: adminToken }
- : { url: `${baseUrl}/instances/${id}`, token: instanceToken };
-
- const updateInstance = async (
- instance: MerchantBackend.Instances.InstanceReconfigurationMessage
- ): Promise<void> => {
- await request(`${url}/private/`, {
- method: "patch",
- token,
- data: instance,
- });
-
- if (adminToken) mutate(["/private/instances", adminToken, baseUrl], null);
- mutate([`/private/`, token, url], null);
- };
-
- const deleteInstance = async (): Promise<void> => {
- await request(`${url}/private/`, {
- method: "delete",
- token: adminToken,
- });
-
- if (adminToken) mutate(["/private/instances", adminToken, baseUrl], null);
- mutate([`/private/`, token, url], null);
- };
-
- const clearToken = async (): Promise<void> => {
- await request(`${url}/private/auth`, {
- method: "post",
- token,
- data: { method: "external" },
- });
-
- mutate([`/private/`, token, url], null);
- };
-
- const setNewToken = async (newToken: string): Promise<void> => {
- await request(`${url}/private/auth`, {
- method: "post",
- token,
- data: { method: "token", token: newToken },
- });
-
- updateLoginStatus(baseUrl, newToken)
- mutate([`/private/`, token, url], null);
- };
-
- return { updateInstance, deleteInstance, setNewToken, clearToken };
+export function revalidateInstanceKYCDetails() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "getCurrentIntanceKycStatus",
+ undefined,
+ { revalidate: true },
+ );
}
+export function useInstanceKYCDetails() {
+ const { state: session } = useSessionContext();
+ const { lib: { instance } } = useSessionContext();
-export function useInstanceDetails(): HttpResponse<MerchantBackend.Instances.QueryInstancesResponse> {
- const { url: baseUrl, token: baseToken } = useBackendContext();
- const { token: instanceToken, id, admin } = useInstanceContext();
-
- const { url, token } = !admin
- ? { url: baseUrl, token: baseToken }
- : { url: `${baseUrl}/instances/${id}`, token: instanceToken };
+ async function fetcher([token]: [AccessToken]) {
+ return await instance.getCurrentIntanceKycStatus(token, {});
+ }
- const { data, error, isValidating } = useSWR<
- HttpResponseOk<MerchantBackend.Instances.QueryInstancesResponse>,
- HttpError
- >([`/private/`, token, url], fetcher, {
- refreshInterval: 0,
- refreshWhenHidden: false,
- revalidateOnFocus: false,
- revalidateOnReconnect: false,
- refreshWhenOffline: false,
- errorRetryCount: 0,
- errorRetryInterval: 1,
- shouldRetryOnError: false,
- });
+ const { data, error } = useSWR<
+ TalerMerchantManagementResultByMethod<"getCurrentIntanceKycStatus">,
+ TalerHttpError
+ >([session.token, "getCurrentIntanceKycStatus"], fetcher);
- if (isValidating) return { loading: true, data: data?.data };
if (data) return data;
if (error) return error;
- return { loading: true };
-}
-
-type KYCStatus =
- | { type: "ok" }
- | { type: "redirect"; status: MerchantBackend.Instances.AccountKycRedirects };
+ return undefined;
-export function useInstanceKYCDetails(): HttpResponse<KYCStatus> {
- const { url: baseUrl, token: baseToken } = useBackendContext();
- const { token: instanceToken, id, admin } = useInstanceContext();
- const { url, token } = !admin
- ? { url: baseUrl, token: baseToken }
- : { url: `${baseUrl}/instances/${id}`, token: instanceToken };
-
- const { data, error } = useSWR<
- HttpResponseOk<MerchantBackend.Instances.AccountKycRedirects>,
- HttpError
- >([`/private/kyc`, token, url], fetcher, {
- refreshInterval: 5000,
- refreshWhenHidden: false,
- revalidateOnFocus: 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;
- return { loading: true };
+export function revalidateManagedInstanceDetails() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "getInstanceDetails",
+ undefined,
+ { revalidate: true },
+ );
}
+export function useManagedInstanceDetails(instanceId: string) {
+ const { state: session } = useSessionContext();
+ const { lib: { instance } } = useSessionContext();
-export function useManagedInstanceDetails(
- instanceId: string
-): HttpResponse<MerchantBackend.Instances.QueryInstancesResponse> {
- const { url, token } = useBackendContext();
+ async function fetcher([token, instanceId]: [AccessToken, string]) {
+ return await instance.getInstanceDetails(token, instanceId);
+ }
- const { data, error, isValidating } = useSWR<
- HttpResponseOk<MerchantBackend.Instances.QueryInstancesResponse>,
- HttpError
- >([`/management/instances/${instanceId}`, token, url], fetcher, {
- refreshInterval: 0,
- refreshWhenHidden: false,
- revalidateOnFocus: false,
- revalidateOnReconnect: false,
- refreshWhenOffline: false,
- errorRetryCount: 0,
- errorRetryInterval: 1,
- shouldRetryOnError: false,
- });
+ const { data, error } = useSWR<
+ TalerMerchantManagementResultByMethod<"getInstanceDetails">,
+ TalerHttpError
+ >([session.token, instanceId, "getInstanceDetails"], fetcher);
- if (isValidating) return { loading: true, data: data?.data };
if (data) return data;
if (error) return error;
- return { loading: true };
+ return undefined;
}
-export function useBackendInstances(): HttpResponse<MerchantBackend.Instances.InstancesResponse> {
- const { url } = useBackendContext();
- const { token } = useInstanceContext();
+export function revalidateBackendInstances() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "listInstances",
+ undefined,
+ { revalidate: true },
+ );
+}
+export function useBackendInstances() {
+ const { state: session } = useSessionContext();
+ const { lib: { instance } } = useSessionContext();
- const { data, error, isValidating } = useSWR<
- HttpResponseOk<MerchantBackend.Instances.InstancesResponse>,
- HttpError
- >(["/management/instances", token, url], fetcher);
+ async function fetcher([token]: [AccessToken]) {
+ return await instance.listInstances(token);
+ }
+
+ const { data, error } = useSWR<
+ TalerMerchantManagementResultByMethod<"listInstances">,
+ TalerHttpError
+ >([session.token, "listInstances"], fetcher);
- if (isValidating) return { loading: true, data: data?.data };
if (data) return data;
if (error) return error;
- return { loading: true };
+ return undefined;
}
diff --git a/packages/merchant-backoffice-ui/src/hooks/listener.ts b/packages/merchant-backoffice-ui/src/hooks/listener.ts
index e7e3327b7..f59794fd4 100644
--- a/packages/merchant-backoffice-ui/src/hooks/listener.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/listener.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,9 +15,9 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
import { useState } from "preact/hooks";
@@ -26,25 +26,27 @@ import { useState } from "preact/hooks";
* 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.
- *
+ *
+ * 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
+ * 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>; };
+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>({});
/**
@@ -58,24 +60,26 @@ export function useListener<T, R = any>(action: (r: T) => Promise<R>): [undefine
toBeRan: () => {
const whatWeGetFromTheListener = listener();
return action(whatWeGetFromTheListener);
- }
+ },
});
} else {
setState({
- toBeRan: undefined
- })
+ 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;
+ const activator = state.toBeRan
+ ? async () => {
+ if (state.toBeRan) {
+ return state.toBeRan();
+ }
+ return Promise.reject();
+ }
+ : undefined;
return [activator, subscriber];
}
diff --git a/packages/merchant-backoffice-ui/src/hooks/notifications.ts b/packages/merchant-backoffice-ui/src/hooks/notifications.ts
index 9c5e21c79..137ef5333 100644
--- a/packages/merchant-backoffice-ui/src/hooks/notifications.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/notifications.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,9 +15,9 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
import { useState } from "preact/hooks";
import { Notification } from "../utils/types.js";
@@ -28,21 +28,29 @@ interface Result {
removeNotification: (n: Notification) => void;
}
-type NotificationWithDate = Notification & { since: Date }
+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() })))
+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 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 }
+ setNotifications((ns: NotificationWithDate[]) =>
+ ns.filter((n) => n !== notif),
+ );
+ };
+ return { notifications, pushNotification, removeNotification };
}
diff --git a/packages/merchant-backoffice-ui/src/hooks/order.test.ts b/packages/merchant-backoffice-ui/src/hooks/order.test.ts
new file mode 100644
index 000000000..9c1eaccbb
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/hooks/order.test.ts
@@ -0,0 +1,581 @@
+/*
+ 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 { AbsoluteTime, AmountString, TalerMerchantApi } from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
+import { expect } from "chai";
+import { useInstanceOrders, 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";
+import { useMerchantApiContext } from "@gnu-taler/web-util/browser";
+
+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 TalerMerchantApi.OrderHistoryEntry],
+ },
+ });
+
+ const newDate = (_d: string | undefined) => {
+ //console.log("new date", d);
+ };
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const query = useInstanceOrders({ paid: true }, newDate);
+ const { lib: api } = useMerchantApiContext()
+ return { query, api };
+ },
+ {},
+ [
+ ({ query }) => {
+ expect(query).undefined;
+ },
+ ({ 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" as AmountString, summary: "pay me" },
+ lock_uuids: []
+ },
+ 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.instance.createOrder(undefined, {
+ order: { amount: "ARS:12" as AmountString, summary: "pay me" },
+ })
+ },
+ ({ 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 TalerMerchantApi.OrderHistoryEntry]
+ },
+ });
+
+ const newDate = (_d: string | undefined) => {
+ //console.log("new date", d);
+ };
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const query = useInstanceOrders({ paid: true }, newDate);
+ const { lib: api } = useMerchantApiContext()
+ 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.instance.addRefund(undefined, "1", {
+ reason: "double pay",
+ refund: "EUR:1" as AmountString,
+ })
+ },
+ ({ 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 TalerMerchantApi.OrderHistoryEntry],
+ },
+ });
+
+ const newDate = (_d: string | undefined) => {
+ //console.log("new date", d);
+ };
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const query = useInstanceOrders({ paid: true }, newDate);
+ const { lib: api } = useMerchantApiContext()
+ 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.instance.deleteOrder(undefined, "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 TalerMerchantApi.CheckPaymentPaidResponse,
+ });
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const query = useOrderDetails("1");
+ const { lib: api } = useMerchantApiContext()
+ 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 TalerMerchantApi.CheckPaymentPaidResponse,
+ });
+
+ api.instance.addRefund(undefined, "1", {
+ reason: "double pay",
+ refund: "EUR:1" as AmountString,
+ })
+ },
+ ({ 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 TalerMerchantApi.CheckPaymentPaidResponse,
+ });
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const query = useOrderDetails("1");
+ const { lib: api } = useMerchantApiContext()
+ 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 TalerMerchantApi.CheckPaymentPaidResponse,
+ });
+
+ api.instance.forgetOrder(undefined, "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: string | undefined) => {
+ //console.log("new date", d);
+ };
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const date = new Date(12000);
+ const query = useInstanceOrders({ wired: true, date: AbsoluteTime.fromMilliseconds(date.getTime()) }, newDate);
+ const { lib: api } = useMerchantApiContext()
+ 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;
+
+ },
+ ],
+ env.buildTestingContext(),
+ );
+
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+
+ it("should load more if result brings more that PAGINATED_LIST_REQUEST", 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),
+ }));
+
+ 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: string | undefined) => {
+ //console.log("new date", d);
+ };
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const date = new Date(12000);
+ const query = useInstanceOrders({ wired: true, date: AbsoluteTime.fromMilliseconds(date.getTime()) }, newDate);
+ const { lib: api } = useMerchantApiContext()
+ 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/merchant-backoffice-ui/src/hooks/order.ts b/packages/merchant-backoffice-ui/src/hooks/order.ts
index a25d18681..d0513dc40 100644
--- a/packages/merchant-backoffice-ui/src/hooks/order.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/order.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,311 +13,86 @@
You should have received 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 { useEffect, useState } from "preact/hooks";
-import useSWR, { useSWRConfig } from "swr";
-import { useBackendContext } from "../context/backend.js";
-import { useInstanceContext } from "../context/instance.js";
-import { MerchantBackend } from "../declaration.js";
-import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js";
-import {
- fetcher,
- HttpError,
- HttpResponse,
- HttpResponseOk,
- HttpResponsePaginated,
- request,
- useMatchMutate,
-} from "./backend.js";
+import { PAGINATED_LIST_REQUEST } from "../utils/constants.js";
-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 orderFetcher<T>(
- url: string,
- token: string,
- backend: string,
- paid?: YesOrNo,
- refunded?: YesOrNo,
- wired?: YesOrNo,
- searchDate?: Date,
- delta?: number
-): Promise<HttpResponseOk<T>> {
- const date_ms =
- delta && delta < 0 && searchDate
- ? searchDate.getTime() + 1
- : searchDate?.getTime();
- 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_ms !== undefined) params.date_ms = date_ms;
- return request<T>(`${backend}${url}`, { token, params });
-}
-
-export function useOrderAPI(): OrderAPI {
- const mutateAll = useMatchMutate();
- const { url: baseUrl, token: adminToken } = useBackendContext();
- const { token: instanceToken, id, admin } = useInstanceContext();
-
- const { url, token } = !admin
- ? {
- url: baseUrl,
- token: adminToken,
- }
- : {
- url: `${baseUrl}/instances/${id}`,
- token: instanceToken,
- };
-
- const createOrder = async (
- data: MerchantBackend.Orders.PostOrderRequest
- ): Promise<HttpResponseOk<MerchantBackend.Orders.PostOrderResponse>> => {
- const res = await request<MerchantBackend.Orders.PostOrderResponse>(
- `${url}/private/orders`,
- {
- method: "post",
- token,
- 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>(
- `${url}/private/orders/${orderId}/refund`,
- {
- method: "post",
- token,
- data,
- }
- );
+// FIX default import https://github.com/microsoft/TypeScript/issues/49189
+import { AbsoluteTime, AccessToken, TalerHttpError, TalerMerchantManagementResultByMethod } from "@gnu-taler/taler-util";
+import _useSWR, { SWRHook, mutate } from "swr";
+import { useSessionContext } from "../context/session.js";
+import { buildPaginatedResult } from "./webhooks.js";
+const useSWR = _useSWR as unknown as SWRHook;
- // 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>(`${url}/private/orders/${orderId}/forget`, {
- method: "patch",
- token,
- 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>(`${url}/private/orders/${orderId}`, {
- method: "delete",
- token,
- });
- await mutateAll(/.*private\/orders.*/);
- return res
- };
-
- const getPaymentURL = async (
- orderId: string
- ): Promise<HttpResponseOk<string>> => {
- return request<MerchantBackend.Orders.MerchantOrderStatusResponse>(
- `${url}/private/orders/${orderId}`,
- {
- method: "get",
- token,
- }
- ).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 revalidateOrderDetails() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "getOrderDetails",
+ undefined,
+ { revalidate: true },
+ );
}
+export function useOrderDetails(oderId: string) {
+ const { state: session } = useSessionContext();
+ const { lib: { instance } } = useSessionContext();
-export function useOrderDetails(
- oderId: string
-): HttpResponse<MerchantBackend.Orders.MerchantOrderStatusResponse> {
- const { url: baseUrl, token: baseToken } = useBackendContext();
- const { token: instanceToken, id, admin } = useInstanceContext();
-
- const { url, token } = !admin
- ? { url: baseUrl, token: baseToken, }
- : { url: `${baseUrl}/instances/${id}`, token: instanceToken, };
+ async function fetcher([dId, token]: [string, AccessToken]) {
+ return await instance.getOrderDetails(token, dId);
+ }
- const { data, error, isValidating } = useSWR<
- HttpResponseOk<MerchantBackend.Orders.MerchantOrderStatusResponse>,
- HttpError
- >([`/private/orders/${oderId}`, token, url], fetcher, {
- refreshInterval: 0,
- refreshWhenHidden: false,
- revalidateOnFocus: false,
- revalidateOnReconnect: false,
- refreshWhenOffline: false,
- });
+ const { data, error } = useSWR<
+ TalerMerchantManagementResultByMethod<"getOrderDetails">,
+ TalerHttpError
+ >([oderId, session.token, "getOrderDetails"], fetcher);
- if (isValidating) return { loading: true, data: data?.data };
if (data) return data;
if (error) return error;
- return { loading: true };
+ return undefined;
}
export interface InstanceOrderFilter {
- paid?: YesOrNo;
- refunded?: YesOrNo;
- wired?: YesOrNo;
- date?: Date;
+ paid?: boolean;
+ refunded?: boolean;
+ wired?: boolean;
+ date?: AbsoluteTime;
+ position?: string;
}
+export function revalidateInstanceOrders() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "listOrders",
+ undefined,
+ { revalidate: true },
+ );
+}
export function useInstanceOrders(
args?: InstanceOrderFilter,
- updateFilter?: (d: Date) => void
-): HttpResponsePaginated<MerchantBackend.Orders.OrderHistory> {
- const { url: baseUrl, token: baseToken } = useBackendContext();
- const { token: instanceToken, id, admin } = useInstanceContext();
-
- const { url, token } = !admin
- ? { url: baseUrl, token: baseToken, }
- : { url: `${baseUrl}/instances/${id}`, token: instanceToken, };
-
- 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>, HttpError>(
- [
- `/private/orders`,
- token,
- url,
- args?.paid,
- args?.refunded,
- args?.wired,
- args?.date,
- totalBefore,
- ],
- orderFetcher
- );
- const {
- data: afterData,
- error: afterError,
- isValidating: loadingAfter,
- } = useSWR<HttpResponseOk<MerchantBackend.Orders.OrderHistory>, HttpError>(
- [
- `/private/orders`,
- token,
- url,
- args?.paid,
- args?.refunded,
- args?.wired,
- args?.date,
- -totalAfter,
- ],
- orderFetcher
- );
-
- //this will save last result
- const [lastBefore, setLastBefore] = useState<
- HttpResponse<MerchantBackend.Orders.OrderHistory>
- >({ loading: true });
- const [lastAfter, setLastAfter] = useState<
- HttpResponse<MerchantBackend.Orders.OrderHistory>
- >({ loading: true });
- useEffect(() => {
- if (afterData) setLastAfter(afterData);
- if (beforeData) setLastBefore(beforeData);
- }, [afterData, beforeData]);
-
- if (beforeError) return beforeError;
- if (afterError) return afterError;
+ updatePosition: (d: string | undefined) => void = () => { },
+) {
+ const { state: session } = useSessionContext();
+ const { lib: { instance } } = useSessionContext();
+
+ // const [offset, setOffset] = useState<string | undefined>(args?.position);
+
+ async function fetcher([token, o, p, r, w, d]: [AccessToken, string, boolean, boolean, boolean, AbsoluteTime]) {
+ return await instance.listOrders(token, {
+ limit: PAGINATED_LIST_REQUEST,
+ offset: o,
+ order: "dec",
+ paid: p,
+ refunded: r,
+ wired: w,
+ date: d,
+ });
+ }
- // 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 { data, error } = useSWR<
+ TalerMerchantManagementResultByMethod<"listOrders">,
+ TalerHttpError
+ >([session.token, args?.position, args?.paid, args?.refunded, args?.wired, args?.date, "listOrders"], fetcher);
- 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));
- }
- },
- };
+ if (error) return error;
+ if (data === undefined) return undefined;
+ if (data.type !== "ok") return data;
- 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 };
+ return buildPaginatedResult(data.body.orders, args?.position, updatePosition, (d) => String(d.row_id))
}
diff --git a/packages/merchant-backoffice-ui/src/hooks/otp.ts b/packages/merchant-backoffice-ui/src/hooks/otp.ts
new file mode 100644
index 000000000..41ed89f70
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/hooks/otp.ts
@@ -0,0 +1,80 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+// FIX default import https://github.com/microsoft/TypeScript/issues/49189
+import { AccessToken, TalerHttpError, TalerMerchantManagementResultByMethod } from "@gnu-taler/taler-util";
+import _useSWR, { SWRHook, mutate } from "swr";
+import { useSessionContext } from "../context/session.js";
+const useSWR = _useSWR as unknown as SWRHook;
+
+export function revalidateInstanceOtpDevices() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "listOtpDevices",
+ undefined,
+ { revalidate: true },
+ );
+}
+export function useInstanceOtpDevices() {
+ const { state: session } = useSessionContext();
+ const { lib: { instance } } = useSessionContext();
+
+ // const [offset, setOffset] = useState<string | undefined>();
+
+ async function fetcher([token, _bid]: [AccessToken, string]) {
+ return await instance.listOtpDevices(token, {
+ // limit: PAGINATED_LIST_REQUEST,
+ // offset: bid,
+ // order: "dec",
+ });
+ }
+
+ const { data, error } = useSWR<
+ TalerMerchantManagementResultByMethod<"listOtpDevices">,
+ TalerHttpError
+ >([session.token, "offset", "listOtpDevices"], fetcher);
+
+ if (error) return error;
+ if (data === undefined) return undefined;
+ if (data.type !== "ok") return data;
+
+ // return buildPaginatedResult(data.body.otp_devices, offset, setOffset, (d) => d.otp_device_id)
+ return data;
+}
+
+export function revalidateOtpDeviceDetails() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "getOtpDeviceDetails",
+ undefined,
+ { revalidate: true },
+ );
+}
+export function useOtpDeviceDetails(deviceId: string) {
+ const { state: session } = useSessionContext();
+ const { lib: { instance } } = useSessionContext();
+
+ async function fetcher([dId, token]: [string, AccessToken]) {
+ return await instance.getOtpDeviceDetails(token, dId);
+ }
+
+ const { data, error } = useSWR<
+ TalerMerchantManagementResultByMethod<"getOtpDeviceDetails">,
+ TalerHttpError
+ >([deviceId, session.token, "getOtpDeviceDetails"], fetcher);
+
+ if (data) return data;
+ if (error) return error;
+ return undefined;
+}
diff --git a/packages/merchant-backoffice-ui/src/hooks/preference.ts b/packages/merchant-backoffice-ui/src/hooks/preference.ts
new file mode 100644
index 000000000..a21d2921c
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/hooks/preference.ts
@@ -0,0 +1,89 @@
+/*
+ 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 {
+ AbsoluteTime,
+ Codec,
+ buildCodecForObject,
+ codecForAbsoluteTime,
+ codecForBoolean,
+ codecForConstString,
+ codecForEither,
+} from "@gnu-taler/taler-util";
+import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser";
+
+export interface Preferences {
+ advanceOrderMode: boolean;
+ hideKycUntil: AbsoluteTime;
+ hideMissingAccountUntil: AbsoluteTime;
+ dateFormat: "ymd" | "dmy" | "mdy";
+}
+
+const defaultSettings: Preferences = {
+ advanceOrderMode: false,
+ hideKycUntil: AbsoluteTime.never(),
+ hideMissingAccountUntil: AbsoluteTime.never(),
+ dateFormat: "ymd",
+};
+
+export const codecForPreferences = (): Codec<Preferences> =>
+ buildCodecForObject<Preferences>()
+ .property("advanceOrderMode", codecForBoolean())
+ .property("hideKycUntil", codecForAbsoluteTime)
+ .property("hideMissingAccountUntil", codecForAbsoluteTime)
+ .property(
+ "dateFormat",
+ codecForEither(
+ codecForConstString("ymd"),
+ codecForConstString("dmy"),
+ codecForConstString("mdy"),
+ ),
+ )
+ .build("Preferences");
+
+const PREFERENCES_KEY = buildStorageKey(
+ "merchant-preferences",
+ codecForPreferences(),
+);
+
+export function usePreference(): [
+ Readonly<Preferences>,
+ <T extends keyof Preferences>(key: T, value: Preferences[T]) => void,
+ (s: Preferences) => void,
+] {
+ const { value, update } = useLocalStorage(PREFERENCES_KEY, defaultSettings);
+ function updateField<T extends keyof Preferences>(k: T, v: Preferences[T]) {
+ const newValue = { ...value, [k]: v };
+ update(newValue);
+ }
+
+ return [value, updateField, update];
+}
+
+export function dateFormatForSettings(s: Preferences): string {
+ switch (s.dateFormat) {
+ case "ymd":
+ return "yyyy/MM/dd";
+ case "dmy":
+ return "dd/MM/yyyy";
+ case "mdy":
+ return "MM/dd/yyyy";
+ }
+}
+
+export function datetimeFormatForSettings(s: Preferences): string {
+ return dateFormatForSettings(s) + " HH:mm:ss";
+}
diff --git a/packages/merchant-backoffice-ui/src/hooks/product.test.ts b/packages/merchant-backoffice-ui/src/hooks/product.test.ts
new file mode 100644
index 000000000..39281241c
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/hooks/product.test.ts
@@ -0,0 +1,362 @@
+/*
+ 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 * as tests from "@gnu-taler/web-util/testing";
+import { expect } from "chai";
+import {
+ useInstanceProducts,
+ 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";
+import { TalerMerchantApi } from "@gnu-taler/taler-util";
+import { useMerchantApiContext } from "@gnu-taler/web-util/browser";
+
+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", product_serial: 1 }],
+ },
+ });
+ env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), {
+ response: { price: "ARS:12" } as TalerMerchantApi.ProductDetail,
+ });
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const query = useInstanceProducts();
+ const { lib: api } = useMerchantApiContext();
+ 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 TalerMerchantApi.ProductAddDetail,
+ });
+
+ env.addRequestExpectation(API_LIST_PRODUCTS, {
+ response: {
+ products: [{ product_id: "1234", product_serial: 1 }, { product_id: "2345", product_serial: 2 }],
+ },
+ });
+ env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), {
+ response: {
+ price: "ARS:12",
+ } as TalerMerchantApi.ProductDetail,
+ });
+ env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), {
+ response: {
+ price: "ARS:12",
+ } as TalerMerchantApi.ProductDetail,
+ });
+ env.addRequestExpectation(API_GET_PRODUCT_BY_ID("2345"), {
+ response: {
+ price: "ARS:23",
+ } as TalerMerchantApi.ProductDetail,
+ });
+
+ api.instance.addProduct(undefined, {
+ 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", product_serial: 1 }],
+ },
+ });
+ env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), {
+ response: { price: "ARS:12" } as TalerMerchantApi.ProductDetail,
+ });
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const query = useInstanceProducts();
+ const { lib: api } = useMerchantApiContext();
+ 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 TalerMerchantApi.ProductPatchDetail,
+ });
+
+ env.addRequestExpectation(API_LIST_PRODUCTS, {
+ response: {
+ products: [{ product_id: "1234", product_serial: 1 }],
+ },
+ });
+ env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), {
+ response: {
+ price: "ARS:13",
+ } as TalerMerchantApi.ProductDetail,
+ });
+
+ api.instance.updateProduct(undefined, "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_serial: 1 }, { product_id: "2345", product_serial: 2 }],
+ },
+ });
+ env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), {
+ response: { price: "ARS:12" } as TalerMerchantApi.ProductDetail,
+ });
+ env.addRequestExpectation(API_GET_PRODUCT_BY_ID("2345"), {
+ response: { price: "ARS:23" } as TalerMerchantApi.ProductDetail,
+ });
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const query = useInstanceProducts();
+ const { lib: api } = useMerchantApiContext();
+ 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", product_serial: 1 }],
+ },
+ });
+
+ env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), {
+ response: {
+ price: "ARS:12",
+ } as TalerMerchantApi.ProductDetail,
+ });
+ api.instance.deleteProduct(undefined, "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 TalerMerchantApi.ProductDetail,
+ });
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const query = useProductDetails("12");
+ const { lib: api } = useMerchantApiContext();
+ 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 TalerMerchantApi.ProductPatchDetail,
+ });
+
+ env.addRequestExpectation(API_GET_PRODUCT_BY_ID("12"), {
+ response: {
+ description: "other description",
+ } as TalerMerchantApi.ProductDetail,
+ });
+
+ api.instance.updateProduct(undefined, "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/merchant-backoffice-ui/src/hooks/product.ts b/packages/merchant-backoffice-ui/src/hooks/product.ts
index 2b6332c51..defda5552 100644
--- a/packages/merchant-backoffice-ui/src/hooks/product.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/product.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,175 +13,91 @@
You should have received 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 useSWR, { useSWRConfig } from "swr";
-import { useBackendContext } from "../context/backend.js";
-import { useInstanceContext } from "../context/instance.js";
-import { MerchantBackend, WithId } from "../declaration.js";
-import {
- fetcher,
- HttpError,
- HttpResponse,
- HttpResponseOk,
- multiFetcher,
- request,
- useMatchMutate
-} from "./backend.js";
-
-export interface ProductAPI {
- 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 { url: baseUrl, token: adminToken } = useBackendContext();
- const { token: instanceToken, id, admin } = useInstanceContext();
-
- const { url, token } = !admin
- ? { url: baseUrl, token: adminToken, }
- : { url: `${baseUrl}/instances/${id}`, token: instanceToken, };
-
- const createProduct = async (
- data: MerchantBackend.Products.ProductAddDetail
- ): Promise<void> => {
- const res = await request(`${url}/private/products`, {
- method: "post",
- token,
- data,
- });
+// FIX default import https://github.com/microsoft/TypeScript/issues/49189
+import { AccessToken, OperationOk, TalerHttpError, TalerMerchantApi, TalerMerchantManagementErrorsByMethod, TalerMerchantManagementResultByMethod, opFixedSuccess } from "@gnu-taler/taler-util";
+import { useState } from "preact/hooks";
+import _useSWR, { SWRHook, mutate } from "swr";
+import { useSessionContext } from "../context/session.js";
+import { PAGINATED_LIST_REQUEST } from "../utils/constants.js";
+import { buildPaginatedResult } from "./webhooks.js";
+const useSWR = _useSWR as unknown as SWRHook;
+
+type ProductWithId = TalerMerchantApi.ProductDetail & { id: string, serial: number };
+function notUndefined(c: ProductWithId | undefined): c is ProductWithId {
+ return c !== undefined;
+}
- return await mutateAll(/.*"\/private\/products.*/);
- };
-
- const updateProduct = async (
- productId: string,
- data: MerchantBackend.Products.ProductPatchDetail
- ): Promise<void> => {
- const r = await request(`${url}/private/products/${productId}`, {
- method: "patch",
- token,
- data,
- });
+export function revalidateInstanceProducts() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "listProductsWithId",
+ undefined,
+ { revalidate: true },
+ );
+}
+export function useInstanceProducts() {
+ const { state: session } = useSessionContext();
+ const { lib: { instance } } = useSessionContext();
- return await mutateAll(/.*"\/private\/products.*/);
- };
+ const [offset, setOffset] = useState<number | undefined>();
- const deleteProduct = async (productId: string): Promise<void> => {
- await request(`${url}/private/products/${productId}`, {
- method: "delete",
- token,
- });
- await mutate([`/private/products`, token, url]);
- };
-
- const lockProduct = async (
- productId: string,
- data: MerchantBackend.Products.LockRequest
- ): Promise<void> => {
- await request(`${url}/private/products/${productId}/lock`, {
- method: "post",
- token,
- data,
+ async function fetcher([token, bid]: [AccessToken, number]) {
+ const list = await instance.listProducts(token, {
+ limit: PAGINATED_LIST_REQUEST,
+ offset: bid === undefined ? undefined: String(bid),
+ order: "dec",
});
+ if (list.type !== "ok") {
+ return list;
+ }
+ const all: Array<ProductWithId | undefined> = await Promise.all(
+ list.body.products.map(async (c) => {
+ const r = await instance.getProductDetails(token, c.product_id);
+ if (r.type === "fail") {
+ return undefined;
+ }
+ return { ...r.body, id: c.product_id, serial: c.product_serial };
+ }),
+ );
+ const products = all.filter(notUndefined);
+
+ return opFixedSuccess({ products });
+ }
+
+ const { data, error } = useSWR<
+ OperationOk<{ products: ProductWithId[] }> |
+ TalerMerchantManagementErrorsByMethod<"listProducts">,
+ TalerHttpError
+ >([session.token, offset, "listProductsWithId"], fetcher);
- return await mutateAll(/.*"\/private\/products.*/);
- };
+ if (error) return error;
+ if (data === undefined) return undefined;
+ if (data.type !== "ok") return data;
- return { createProduct, updateProduct, deleteProduct, lockProduct };
+ return buildPaginatedResult(data.body.products, offset, setOffset, (d) => d.serial)
}
-export function useInstanceProducts(): HttpResponse<
- (MerchantBackend.Products.ProductDetail & WithId)[]
-> {
- const { url: baseUrl, token: baseToken } = useBackendContext();
- const { token: instanceToken, id, admin } = useInstanceContext();
-
- const { url, token } = !admin
- ? { url: baseUrl, token: baseToken, }
- : { url: `${baseUrl}/instances/${id}`, token: instanceToken, };
-
- const { data: list, error: listError } = useSWR<
- HttpResponseOk<MerchantBackend.Products.InventorySummaryResponse>,
- HttpError
- >([`/private/products`, token, url], fetcher, {
- refreshInterval: 0,
- refreshWhenHidden: false,
- revalidateOnFocus: false,
- revalidateOnReconnect: false,
- refreshWhenOffline: false,
- });
-
- const paths = (list?.data.products || []).map(
- (p) => `/private/products/${p.product_id}`
+export function revalidateProductDetails() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "getProductDetails",
+ undefined,
+ { revalidate: true },
);
- const { data: products, error: productError } = useSWR<
- HttpResponseOk<MerchantBackend.Products.ProductDetail>[],
- HttpError
- >([paths, token, url], multiFetcher, {
- refreshInterval: 0,
- refreshWhenHidden: false,
- revalidateOnFocus: false,
- revalidateOnReconnect: false,
- refreshWhenOffline: false,
- });
-
-
- if (listError) return listError;
- if (productError) return productError;
-
- 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) {
+ const { state: session } = useSessionContext();
+ const { lib: { instance } } = useSessionContext();
+
+ async function fetcher([pid, token]: [string, AccessToken]) {
+ return await instance.getProductDetails(token, pid);
+ }
-export function useProductDetails(
- productId: string
-): HttpResponse<MerchantBackend.Products.ProductDetail> {
- const { url: baseUrl, token: baseToken } = useBackendContext();
- const { token: instanceToken, id, admin } = useInstanceContext();
+ const { data, error } = useSWR<
+ TalerMerchantManagementResultByMethod<"getProductDetails">,
+ TalerHttpError
+ >([productId, session.token, "getProductDetails"], fetcher);
- const { url, token } = !admin
- ? {
- url: baseUrl,
- token: baseToken,
- }
- : {
- url: `${baseUrl}/instances/${id}`,
- token: instanceToken,
- };
-
- const { data, error, isValidating } = useSWR<
- HttpResponseOk<MerchantBackend.Products.ProductDetail>,
- HttpError
- >([`/private/products/${productId}`, token, url], 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;
- return { loading: true };
+ return undefined;
}
diff --git a/packages/merchant-backoffice-ui/src/hooks/reserves.ts b/packages/merchant-backoffice-ui/src/hooks/reserves.ts
deleted file mode 100644
index d1dcb0b7a..000000000
--- a/packages/merchant-backoffice-ui/src/hooks/reserves.ts
+++ /dev/null
@@ -1,218 +0,0 @@
-/*
- 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/>
- */
-import useSWR, { useSWRConfig } from "swr";
-import { useBackendContext } from "../context/backend.js";
-import { useInstanceContext } from "../context/instance.js";
-import { MerchantBackend } from "../declaration.js";
-import {
- fetcher,
- HttpError,
- HttpResponse,
- HttpResponseOk,
- request,
- useMatchMutate,
-} from "./backend.js";
-
-export function useReservesAPI(): ReserveMutateAPI {
- const mutateAll = useMatchMutate();
- const { mutate } = useSWRConfig();
- const { url: baseUrl, token: adminToken } = useBackendContext();
- const { token: instanceToken, id, admin } = useInstanceContext();
-
- const { url, token } = !admin
- ? { url: baseUrl, token: adminToken, }
- : { url: `${baseUrl}/instances/${id}`, token: instanceToken, };
-
- const createReserve = async (
- data: MerchantBackend.Tips.ReserveCreateRequest
- ): Promise<
- HttpResponseOk<MerchantBackend.Tips.ReserveCreateConfirmation>
- > => {
- const res = await request<MerchantBackend.Tips.ReserveCreateConfirmation>(
- `${url}/private/reserves`,
- {
- method: "post",
- token,
- data,
- }
- );
-
- //evict reserve list query
- await mutateAll(/.*private\/reserves.*/);
-
- return res;
- };
-
- const authorizeTipReserve = async (
- pub: string,
- data: MerchantBackend.Tips.TipCreateRequest
- ): Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>> => {
- const res = await request<MerchantBackend.Tips.TipCreateConfirmation>(
- `${url}/private/reserves/${pub}/authorize-tip`,
- {
- method: "post",
- token,
- data,
- }
- );
-
- //evict reserve details query
- await mutate([`/private/reserves/${pub}`, token, url]);
-
- return res;
- };
-
- const authorizeTip = async (
- data: MerchantBackend.Tips.TipCreateRequest
- ): Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>> => {
- const res = await request<MerchantBackend.Tips.TipCreateConfirmation>(
- `${url}/private/tips`,
- {
- method: "post",
- token,
- data,
- }
- );
-
- //evict all details query
- await mutateAll(/.*private\/reserves\/.*/);
-
- return res;
- };
-
- const deleteReserve = async (pub: string): Promise<HttpResponse<void>> => {
- const res = await request<void>(`${url}/private/reserves/${pub}`, {
- method: "delete",
- token,
- });
-
- //evict reserve list query
- await mutateAll(/.*private\/reserves.*/);
-
- return res;
- };
-
- return { createReserve, authorizeTip, authorizeTipReserve, deleteReserve };
-}
-
-export interface ReserveMutateAPI {
- createReserve: (
- data: MerchantBackend.Tips.ReserveCreateRequest
- ) => Promise<HttpResponseOk<MerchantBackend.Tips.ReserveCreateConfirmation>>;
- authorizeTipReserve: (
- id: string,
- data: MerchantBackend.Tips.TipCreateRequest
- ) => Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>>;
- authorizeTip: (
- data: MerchantBackend.Tips.TipCreateRequest
- ) => Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>>;
- deleteReserve: (id: string) => Promise<HttpResponse<void>>;
-}
-
-export function useInstanceReserves(): HttpResponse<MerchantBackend.Tips.TippingReserveStatus> {
- const { url: baseUrl, token: baseToken } = useBackendContext();
- const { token: instanceToken, id, admin } = useInstanceContext();
-
- const { url, token } = !admin
- ? { url: baseUrl, token: baseToken, }
- : { url: `${baseUrl}/instances/${id}`, token: instanceToken, };
-
- const { data, error, isValidating } = useSWR<
- HttpResponseOk<MerchantBackend.Tips.TippingReserveStatus>,
- HttpError
- >([`/private/reserves`, token, url], fetcher);
-
- if (isValidating) return { loading: true, data: data?.data };
- if (data) return data;
- if (error) return error;
- return { loading: true };
-}
-
-export function useReserveDetails(
- reserveId: string
-): HttpResponse<MerchantBackend.Tips.ReserveDetail> {
- const { url: baseUrl } = useBackendContext();
- const { token, id: instanceId, admin } = useInstanceContext();
-
- const url = !admin ? baseUrl : `${baseUrl}/instances/${instanceId}`;
-
- const { data, error, isValidating } = useSWR<
- HttpResponseOk<MerchantBackend.Tips.ReserveDetail>,
- HttpError
- >([`/private/reserves/${reserveId}`, token, url], 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;
- return { loading: true };
-}
-
-export function useTipDetails(
- tipId: string
-): HttpResponse<MerchantBackend.Tips.TipDetails> {
- const { url: baseUrl } = useBackendContext();
- const { token, id: instanceId, admin } = useInstanceContext();
-
- const url = !admin ? baseUrl : `${baseUrl}/instances/${instanceId}`;
-
- const { data, error, isValidating } = useSWR<
- HttpResponseOk<MerchantBackend.Tips.TipDetails>,
- HttpError
- >([`/private/tips/${tipId}`, token, url], tipsDetailFetcher, {
- 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;
- return { loading: true };
-}
-
-function reserveDetailFetcher<T>(
- url: string,
- token: string,
- backend: string
-): Promise<HttpResponseOk<T>> {
- return request<T>(`${backend}${url}`, {
- token,
- params: {
- tips: "yes",
- },
- });
-}
-
-function tipsDetailFetcher<T>(
- url: string,
- token: string,
- backend: string
-): Promise<HttpResponseOk<T>> {
- return request<T>(`${backend}${url}`, {
- token,
- params: {
- pickups: "yes",
- },
- });
-}
diff --git a/packages/merchant-backoffice-ui/src/hooks/templates.ts b/packages/merchant-backoffice-ui/src/hooks/templates.ts
new file mode 100644
index 000000000..12d99f3fc
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/hooks/templates.ts
@@ -0,0 +1,87 @@
+/*
+ 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 { useState } from "preact/hooks";
+import { PAGINATED_LIST_REQUEST } from "../utils/constants.js";
+
+// FIX default import https://github.com/microsoft/TypeScript/issues/49189
+import { AccessToken, TalerHttpError, TalerMerchantManagementResultByMethod } from "@gnu-taler/taler-util";
+import _useSWR, { SWRHook, mutate } from "swr";
+import { useSessionContext } from "../context/session.js";
+import { buildPaginatedResult } from "./webhooks.js";
+const useSWR = _useSWR as unknown as SWRHook;
+
+
+export interface InstanceTemplateFilter {
+}
+
+export function revalidateInstanceTemplates() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "listTemplates",
+ undefined,
+ { revalidate: true },
+ );
+}
+export function useInstanceTemplates() {
+ const { state: session } = useSessionContext();
+ const { lib: { instance } } = useSessionContext();
+
+ const [offset, setOffset] = useState<string | undefined>();
+
+ async function fetcher([token, bid]: [AccessToken, string]) {
+ return await instance.listTemplates(token, {
+ limit: PAGINATED_LIST_REQUEST,
+ offset: bid,
+ order: "dec",
+ });
+ }
+
+ const { data, error } = useSWR<
+ TalerMerchantManagementResultByMethod<"listTemplates">,
+ TalerHttpError
+ >([session.token, offset, "listTemplates"], fetcher);
+
+ if (error) return error;
+ if (data === undefined) return undefined;
+ if (data.type !== "ok") return data;
+
+ return buildPaginatedResult(data.body.templates, offset, setOffset, (d) => d.template_id)
+
+}
+
+export function revalidateTemplateDetails() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "getTemplateDetails",
+ undefined,
+ { revalidate: true },
+ );
+}
+export function useTemplateDetails(templateId: string) {
+ const { state: session } = useSessionContext();
+ const { lib: { instance } } = useSessionContext();
+
+ async function fetcher([tid, token]: [string, AccessToken]) {
+ return await instance.getTemplateDetails(token, tid);
+ }
+
+ const { data, error } = useSWR<
+ TalerMerchantManagementResultByMethod<"getTemplateDetails">,
+ TalerHttpError
+ >([templateId, session.token, "getTemplateDetails"], fetcher);
+
+ if (data) return data;
+ if (error) return error;
+ return undefined;
+}
diff --git a/packages/merchant-backoffice-ui/src/hooks/testing.tsx b/packages/merchant-backoffice-ui/src/hooks/testing.tsx
new file mode 100644
index 000000000..fc78f6c58
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/hooks/testing.tsx
@@ -0,0 +1,192 @@
+/*
+ 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 { 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 { TalerBankIntegrationHttpClient, TalerCoreBankHttpClient, TalerRevenueHttpClient, TalerWireGatewayHttpClient } from "@gnu-taler/taler-util";
+
+interface RequestOptions {
+ method?: "GET" | "POST" | "HEAD",
+ params?: any,
+ token?: string | undefined,
+ data?: any,
+}
+interface HttpResponseOk<T> {
+ ok: true,
+ data: T,
+ loading: boolean,
+ clientError: boolean,
+ serverError: boolean,
+ info: any,
+}
+
+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 : undefined as any, 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/merchant-backoffice-ui/src/hooks/transfer.test.ts b/packages/merchant-backoffice-ui/src/hooks/transfer.test.ts
new file mode 100644
index 000000000..7daaf5049
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/hooks/transfer.test.ts
@@ -0,0 +1,260 @@
+/*
+ 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 {
+ AmountString,
+ PaytoString,
+ TalerMerchantApi,
+} from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
+import { expect } from "chai";
+import { ApiMockEnvironment } from "./testing.js";
+import { useInstanceTransfers } from "./transfer.js";
+import { API_INFORM_TRANSFERS, API_LIST_TRANSFERS } from "./urls.js";
+import { useMerchantApiContext } from "@gnu-taler/web-util/browser";
+
+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 TalerMerchantApi.TransferDetails],
+ },
+ });
+
+ const moveCursor = (d: string | undefined) => {
+ console.log("new position", d);
+ };
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const query = useInstanceTransfers({}, moveCursor);
+ const { lib: api } = useMerchantApiContext();
+ 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.instance.informWireTransfer(undefined, {
+ wtid: "3",
+ credit_amount: "EUR:1" as AmountString,
+ exchange_url: "exchange.url",
+ payto_uri: "payto://" as PaytoString,
+ });
+ },
+ ({ 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 | undefined) => {
+ console.log("new position", d);
+ };
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const query = useInstanceTransfers(
+ { payto_uri: "payto://" },
+ moveCursor,
+ );
+ return { query };
+ },
+ {},
+ [
+ (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;
+
+ },
+ ],
+ env.buildTestingContext(),
+ );
+
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ });
+
+ it("should load more if result brings more that PAGINATED_LIST_REQUEST", 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 | undefined) => {
+ console.log("new position", d);
+ };
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const query = useInstanceTransfers(
+ { payto_uri: "payto://", position: "1" },
+ moveCursor,
+ );
+ return { query };
+ },
+ {},
+ [
+ (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/merchant-backoffice-ui/src/hooks/transfer.ts b/packages/merchant-backoffice-ui/src/hooks/transfer.ts
index fddbc7cda..6f77369c2 100644
--- a/packages/merchant-backoffice-ui/src/hooks/transfer.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/transfer.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,205 +13,56 @@
You should have received 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 { useBackendContext } from "../context/backend.js";
-import {
- request,
- HttpResponse,
- HttpError,
- HttpResponseOk,
- HttpResponsePaginated,
- useMatchMutate,
-} from "./backend.js";
-import useSWR from "swr";
-import { useInstanceContext } from "../context/instance.js";
-import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js";
-import { useEffect, useState } from "preact/hooks";
+import { PAGINATED_LIST_REQUEST } from "../utils/constants.js";
-async function transferFetcher<T>(
- url: string,
- token: string,
- backend: string,
- payto_uri?: string,
- verified?: string,
- position?: string,
- delta?: number
-): Promise<HttpResponseOk<T>> {
- const params: any = {};
- if (payto_uri !== undefined) params.payto_uri = payto_uri;
- if (verified !== undefined) params.verified = verified;
- if (delta !== undefined) {
- params.limit = delta;
- }
- if (position !== undefined) params.offset = position;
-
- return request<T>(`${backend}${url}`, { token, params });
-}
-
-export function useTransferAPI(): TransferAPI {
- const mutateAll = useMatchMutate();
- const { url: baseUrl, token: adminToken } = useBackendContext();
- const { token: instanceToken, id, admin } = useInstanceContext();
-
- const { url, token } = !admin
- ? {
- url: baseUrl,
- token: adminToken,
- }
- : {
- url: `${baseUrl}/instances/${id}`,
- token: instanceToken,
- };
-
- const informTransfer = async (
- data: MerchantBackend.Transfers.TransferInformation
- ): Promise<
- HttpResponseOk<MerchantBackend.Transfers.MerchantTrackTransferResponse>
- > => {
- const res = await request<MerchantBackend.Transfers.MerchantTrackTransferResponse>(
- `${url}/private/transfers`, {
- method: "post",
- token,
- data,
- });
-
- await mutateAll(/.*private\/transfers.*/);
- return res
- };
-
- return { informTransfer };
-}
-
-export interface TransferAPI {
- informTransfer: (
- data: MerchantBackend.Transfers.TransferInformation
- ) => Promise<
- HttpResponseOk<MerchantBackend.Transfers.MerchantTrackTransferResponse>
- >;
-}
+// FIX default import https://github.com/microsoft/TypeScript/issues/49189
+import { AccessToken, TalerHttpError, TalerMerchantManagementResultByMethod } from "@gnu-taler/taler-util";
+import _useSWR, { SWRHook, mutate } from "swr";
+import { useSessionContext } from "../context/session.js";
+import { buildPaginatedResult } from "./webhooks.js";
+const useSWR = _useSWR as unknown as SWRHook;
export interface InstanceTransferFilter {
payto_uri?: string;
- verified?: "yes" | "no";
+ verified?: boolean;
position?: string;
}
+export function revalidateInstanceTransfers() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "listWireTransfers",
+ undefined,
+ { revalidate: true },
+ );
+}
export function useInstanceTransfers(
args?: InstanceTransferFilter,
- updatePosition?: (id: string) => void
-): HttpResponsePaginated<MerchantBackend.Transfers.TransferList> {
- const { url: baseUrl, token: baseToken } = useBackendContext();
- const { token: instanceToken, id, admin } = useInstanceContext();
-
- const { url, token } = !admin
- ? { url: baseUrl, token: baseToken }
- : { url: `${baseUrl}/instances/${id}`, token: instanceToken };
-
- 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>, HttpError>(
- [
- `/private/transfers`,
- token,
- url,
- args?.payto_uri,
- args?.verified,
- args?.position,
- totalBefore,
- ],
- transferFetcher
- );
- const {
- data: afterData,
- error: afterError,
- isValidating: loadingAfter,
- } = useSWR<HttpResponseOk<MerchantBackend.Transfers.TransferList>, HttpError>(
- [
- `/private/transfers`,
- token,
- url,
- args?.payto_uri,
- args?.verified,
- args?.position,
- -totalAfter,
- ],
- transferFetcher
- );
-
- //this will save last result
- const [lastBefore, setLastBefore] = useState<
- HttpResponse<MerchantBackend.Transfers.TransferList>
- >({ loading: true });
- const [lastAfter, setLastAfter] = useState<
- HttpResponse<MerchantBackend.Transfers.TransferList>
- >({ loading: true });
- useEffect(() => {
- if (afterData) setLastAfter(afterData);
- if (beforeData) setLastBefore(beforeData);
- }, [afterData, beforeData]);
+ updatePosition: (id: string | undefined) => void = (() => { }),
+) {
+ const { state: session } = useSessionContext();
+ const { lib: { instance } } = useSessionContext();
+
+ // const [offset, setOffset] = useState<string | undefined>(args?.position);
+
+ async function fetcher([token, o, p, v]: [AccessToken, string, string, boolean]) {
+ return await instance.listWireTransfers(token, {
+ paytoURI: p,
+ verified: v,
+ limit: PAGINATED_LIST_REQUEST,
+ offset: o,
+ order: "dec",
+ });
+ }
- if (beforeError) return beforeError;
- if (afterError) return afterError;
+ const { data, error } = useSWR<
+ TalerMerchantManagementResultByMethod<"listWireTransfers">,
+ TalerHttpError
+ >([session.token, args?.position, args?.payto_uri, args?.verified, "listWireTransfers"], fetcher);
- // 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);
+ if (error) return error;
+ if (data === undefined) return undefined;
+ if (data.type !== "ok") return data;
- 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);
- }
- },
- };
+ return buildPaginatedResult(data.body.transfers, args?.position, updatePosition, (d) => String(d.transfer_serial_id))
- 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/merchant-backoffice-ui/src/hooks/urls.ts b/packages/merchant-backoffice-ui/src/hooks/urls.ts
new file mode 100644
index 000000000..95e1c04f3
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/hooks/urls.ts
@@ -0,0 +1,235 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { TalerMerchantApi } from "@gnu-taler/taler-util";
+import { Query } from "@gnu-taler/web-util/testing";
+
+////////////////////
+// ORDER
+////////////////////
+
+export const API_CREATE_ORDER: Query<
+ TalerMerchantApi.PostOrderRequest,
+ TalerMerchantApi.PostOrderResponse
+> = {
+ method: "POST",
+ url: "http://backend/instances/default/private/orders",
+};
+
+export const API_GET_ORDER_BY_ID = (
+ id: string,
+): Query<unknown, TalerMerchantApi.MerchantOrderStatusResponse> => ({
+ method: "GET",
+ url: `http://backend/instances/default/private/orders/${id}`,
+});
+
+export const API_LIST_ORDERS: Query<
+ unknown,
+ TalerMerchantApi.OrderHistory
+> = {
+ method: "GET",
+ url: "http://backend/instances/default/private/orders",
+};
+
+export const API_REFUND_ORDER_BY_ID = (
+ id: string,
+): Query<
+ TalerMerchantApi.RefundRequest,
+ TalerMerchantApi.MerchantRefundResponse
+> => ({
+ method: "POST",
+ url: `http://backend/instances/default/private/orders/${id}/refund`,
+});
+
+export const API_FORGET_ORDER_BY_ID = (
+ id: string,
+): Query<TalerMerchantApi.ForgetRequest, unknown> => ({
+ method: "PATCH",
+ url: `http://backend/instances/default/private/orders/${id}/forget`,
+});
+
+export const API_DELETE_ORDER = (
+ id: string,
+): Query<TalerMerchantApi.ForgetRequest, unknown> => ({
+ method: "DELETE",
+ url: `http://backend/instances/default/private/orders/${id}`,
+});
+
+////////////////////
+// TRANSFER
+////////////////////
+
+export const API_LIST_TRANSFERS: Query<
+ unknown,
+ TalerMerchantApi.TransferList
+> = {
+ method: "GET",
+ url: "http://backend/instances/default/private/transfers",
+};
+
+export const API_INFORM_TRANSFERS: Query<
+ TalerMerchantApi.TransferInformation,
+ {}
+> = {
+ method: "POST",
+ url: "http://backend/instances/default/private/transfers",
+};
+
+////////////////////
+// PRODUCT
+////////////////////
+
+export const API_CREATE_PRODUCT: Query<
+ TalerMerchantApi.ProductAddDetail,
+ unknown
+> = {
+ method: "POST",
+ url: "http://backend/instances/default/private/products",
+};
+
+export const API_LIST_PRODUCTS: Query<
+ unknown,
+ TalerMerchantApi.InventorySummaryResponse
+> = {
+ method: "GET",
+ url: "http://backend/instances/default/private/products",
+};
+
+export const API_GET_PRODUCT_BY_ID = (
+ id: string,
+): Query<unknown, TalerMerchantApi.ProductDetail> => ({
+ method: "GET",
+ url: `http://backend/instances/default/private/products/${id}`,
+});
+
+export const API_UPDATE_PRODUCT_BY_ID = (
+ id: string,
+): Query<
+ TalerMerchantApi.ProductPatchDetail,
+ TalerMerchantApi.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}`,
+});
+
+////////////////////
+// INSTANCE ADMIN
+////////////////////
+
+export const API_CREATE_INSTANCE: Query<
+ TalerMerchantApi.InstanceConfigurationMessage,
+ unknown
+> = {
+ method: "POST",
+ url: "http://backend/management/instances",
+};
+
+export const API_GET_INSTANCE_BY_ID = (
+ id: string,
+): Query<unknown, TalerMerchantApi.QueryInstancesResponse> => ({
+ method: "GET",
+ url: `http://backend/management/instances/${id}`,
+});
+
+export const API_GET_INSTANCE_KYC_BY_ID = (
+ id: string,
+): Query<unknown, TalerMerchantApi.AccountKycRedirects> => ({
+ method: "GET",
+ url: `http://backend/management/instances/${id}/kyc`,
+});
+
+export const API_LIST_INSTANCES: Query<
+ unknown,
+ TalerMerchantApi.InstancesResponse
+> = {
+ method: "GET",
+ url: "http://backend/management/instances",
+};
+
+export const API_UPDATE_INSTANCE_BY_ID = (
+ id: string,
+): Query<
+ TalerMerchantApi.InstanceReconfigurationMessage,
+ unknown
+> => ({
+ method: "PATCH",
+ url: `http://backend/management/instances/${id}`,
+});
+
+export const API_UPDATE_INSTANCE_AUTH_BY_ID = (
+ id: string,
+): Query<
+ TalerMerchantApi.InstanceAuthConfigurationMessage,
+ unknown
+> => ({
+ 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}`,
+});
+
+////////////////////
+// INSTANCE
+////////////////////
+
+export const API_GET_CURRENT_INSTANCE: Query<
+ unknown,
+ TalerMerchantApi.QueryInstancesResponse
+> = {
+ method: "GET",
+ url: `http://backend/instances/default/private/`,
+};
+
+export const API_GET_CURRENT_INSTANCE_KYC: Query<
+ unknown,
+ TalerMerchantApi.AccountKycRedirects
+> = {
+ method: "GET",
+ url: `http://backend/instances/default/private/kyc`,
+};
+
+export const API_UPDATE_CURRENT_INSTANCE: Query<
+ TalerMerchantApi.InstanceReconfigurationMessage,
+ unknown
+> = {
+ method: "PATCH",
+ url: `http://backend/instances/default/private/`,
+};
+
+export const API_UPDATE_CURRENT_INSTANCE_AUTH: Query<
+ TalerMerchantApi.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/merchant-backoffice-ui/src/hooks/webhooks.ts b/packages/merchant-backoffice-ui/src/hooks/webhooks.ts
new file mode 100644
index 000000000..fe37162aa
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/hooks/webhooks.ts
@@ -0,0 +1,118 @@
+/*
+ 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 { PAGINATED_LIST_REQUEST } from "../utils/constants.js";
+
+// FIX default import https://github.com/microsoft/TypeScript/issues/49189
+import { AccessToken, OperationOk, 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 InstanceWebhookFilter {
+}
+
+export function revalidateInstanceWebhooks() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "listWebhooks",
+ undefined,
+ { revalidate: true },
+ );
+}
+export function useInstanceWebhooks() {
+ const { state: session } = useSessionContext();
+ const { lib: { instance } } = useSessionContext();
+
+ // const [offset, setOffset] = useState<string | undefined>();
+
+ async function fetcher([token, _bid]: [AccessToken, string]) {
+ return await instance.listWebhooks(token, {
+ // limit: PAGINATED_LIST_REQUEST,
+ // offset: bid,
+ // order: "dec",
+ });
+ }
+
+ const { data, error } = useSWR<
+ TalerMerchantManagementResultByMethod<"listWebhooks">,
+ TalerHttpError
+ >([session.token, "offset", "listWebhooks"], fetcher);
+
+ if (error) return error;
+ if (data === undefined) return undefined;
+ if (data.type !== "ok") return data;
+
+ // return buildPaginatedResult(data.body.webhooks, offset, setOffset, (d) => d.webhook_id)
+ return data;
+}
+
+type PaginatedResult<T> = OperationOk<T> & {
+ isLastPage: boolean;
+ isFirstPage: boolean;
+ loadNext(): void;
+ loadFirst(): void;
+}
+
+//TODO: consider sending this to web-util
+export function buildPaginatedResult<R, OffId>(data: R[], offset: OffId | undefined, setOffset: (o: OffId | undefined) => void, getId: (r: R) => OffId): PaginatedResult<R[]> {
+
+ const isLastPage = data.length < PAGINATED_LIST_REQUEST;
+ const isFirstPage = offset === undefined;
+
+ const result = structuredClone(data);
+ if (result.length == PAGINATED_LIST_REQUEST) {
+ result.pop();
+ }
+ return {
+ type: "ok",
+ body: result,
+ isLastPage,
+ isFirstPage,
+ loadNext: () => {
+ if (!result.length) return;
+ const id = getId(result[result.length - 1])
+ setOffset(id);
+ },
+ loadFirst: () => {
+ setOffset(undefined);
+ },
+ };
+}
+
+
+export function revalidateWebhookDetails() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "getWebhookDetails",
+ undefined,
+ { revalidate: true },
+ );
+}
+export function useWebhookDetails(webhookId: string) {
+ const { state: session } = useSessionContext();
+ const { lib: { instance } } = useSessionContext();
+
+ async function fetcher([hookId, token]: [string, AccessToken]) {
+ return await instance.getWebhookDetails(token, hookId);
+ }
+
+ const { data, error } = useSWR<
+ TalerMerchantManagementResultByMethod<"getWebhookDetails">,
+ TalerHttpError
+ >([webhookId, session.token, "getWebhookDetails"], fetcher);
+
+ if (data) return data;
+ if (error) return error;
+ return undefined;
+}
diff --git a/packages/merchant-backoffice-ui/src/i18n/de.po b/packages/merchant-backoffice-ui/src/i18n/de.po
index 6b35bd0ce..f34d5dd20 100644
--- a/packages/merchant-backoffice-ui/src/i18n/de.po
+++ b/packages/merchant-backoffice-ui/src/i18n/de.po
@@ -12,1046 +12,2731 @@
# You should have received a copy of the GNU General Public License along with
# TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
#
-#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: Taler Wallet\n"
-"Report-Msgid-Bugs-To: \n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
"POT-Creation-Date: 2016-11-23 00:00+0100\n"
-"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
-"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
-"Language-Team: LANGUAGE <LL@li.org>\n"
-"Language: \n"
+"PO-Revision-Date: 2024-03-21 21:39+0000\n"
+"Last-Translator: Stefan Kügel <skuegel@web.de>\n"
+"Language-Team: German <https://weblate.taler.net/projects/gnu-taler/"
+"merchant-backoffice/de/>\n"
+"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 5.2.1\n"
-#: src/ApplicationReadyRoutes.tsx:50 src/InstanceRoutes.tsx:118
-#: src/InstanceRoutes.tsx:299
+#: src/components/modal/index.tsx:71
#, c-format
-msgid "Access denied"
+msgid "Cancel"
msgstr ""
-#: src/ApplicationReadyRoutes.tsx:51 src/InstanceRoutes.tsx:118
-#: src/InstanceRoutes.tsx:300
+#: src/components/modal/index.tsx:79
#, c-format
-msgid "Check your token is valid"
+msgid "%1$s"
msgstr ""
-#: src/ApplicationReadyRoutes.tsx:72
+#: src/components/modal/index.tsx:84
#, c-format
-msgid "Couldn't access the server."
+msgid "Close"
+msgstr ""
+
+#: src/components/modal/index.tsx:124
+#, c-format
+msgid "Continue"
msgstr ""
-#: src/ApplicationReadyRoutes.tsx:73
+#: src/components/modal/index.tsx:178
#, c-format
-msgid "Could not infer instance id from url %1$s"
+msgid "Clear"
msgstr ""
-#: src/InstanceRoutes.tsx:109
+#: src/components/modal/index.tsx:190
#, c-format
-msgid "HTTP status #%1$s: Server reported a problem"
+msgid "Confirm"
+msgstr "Bestätigen"
+
+#: src/components/modal/index.tsx:296
+#, c-format
+msgid "is not the same as the current access token"
msgstr ""
-#: src/InstanceRoutes.tsx:110
+#: src/components/modal/index.tsx:299
#, c-format
-msgid "Got message: \"%1$s\" from: %2$s"
+msgid "cannot be empty"
msgstr ""
-#: src/InstanceRoutes.tsx:127
+#: src/components/modal/index.tsx:301
#, c-format
-msgid "No default instance"
+msgid "cannot be the same as the old token"
msgstr ""
-#: src/InstanceRoutes.tsx:128
+#: src/components/modal/index.tsx:305
#, c-format
-msgid ""
-"in order to use merchant backoffice, you should create the default instance"
+msgid "is not the same"
msgstr ""
-#: src/InstanceRoutes.tsx:288
+#: src/components/modal/index.tsx:315
#, c-format
-msgid "Server reported a problem: HTTP status #%1$s"
+msgid "You are updating the access token from instance with id %1$s"
msgstr ""
-#: src/InstanceRoutes.tsx:289
+#: src/components/modal/index.tsx:331
#, c-format
-msgid "Got message: %1$s from: %2$s"
+msgid "Old access token"
msgstr ""
-#: src/components/exception/login.tsx:46
+#: src/components/modal/index.tsx:332
#, c-format
-msgid "Login required"
+msgid "access token currently in use"
msgstr ""
-#: src/components/exception/login.tsx:49
+#: src/components/modal/index.tsx:338
+#, c-format
+msgid "New access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:339
+#, c-format
+msgid "next access token to be used"
+msgstr ""
+
+#: src/components/modal/index.tsx:344
+#, c-format
+msgid "Repeat access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:345
+#, c-format
+msgid "confirm the same access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:350
+#, c-format
+msgid "Clearing the access token will mean public access to the instance"
+msgstr ""
+
+#: src/components/modal/index.tsx:377
+#, c-format
+msgid "cannot be the same as the old access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:394
+#, c-format
+msgid "You are setting the access token for the new instance"
+msgstr ""
+
+#: src/components/modal/index.tsx:420
#, c-format
msgid ""
-"Please enter your auth token. Token should have \"secret-token:\" and start "
-"with Bearer or ApiKey"
+"With external authorization method no check will be done by the merchant "
+"backend"
msgstr ""
-#: src/components/exception/login.tsx:86 src/components/modal/index.tsx:53
-#: src/components/modal/index.tsx:75 src/paths/admin/create/CreatePage.tsx:115
-#: src/paths/instance/orders/create/CreatePage.tsx:325
-#: src/paths/instance/products/create/CreatePage.tsx:51
-#: src/paths/instance/products/list/Table.tsx:174
-#: src/paths/instance/products/list/Table.tsx:228
-#: src/paths/instance/products/update/UpdatePage.tsx:55
-#: src/paths/instance/transfers/create/CreatePage.tsx:89
-#: src/paths/instance/update/UpdatePage.tsx:134
+#: src/components/modal/index.tsx:436
#, c-format
-msgid "Confirm"
+msgid "Set external authorization"
msgstr ""
-#: src/components/form/InputArray.tsx:72
+#: src/components/modal/index.tsx:448
#, c-format
-msgid "The value %1$s is invalid for a payment url"
+msgid "Set access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:470
+#, c-format
+msgid "Operation in progress..."
+msgstr ""
+
+#: src/components/modal/index.tsx:479
+#, c-format
+msgid "The operation will be automatically canceled after %1$s seconds"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:80
+#, c-format
+msgid "Instances"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:93
+#, c-format
+msgid "Delete"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:99
+#, c-format
+msgid "add new instance"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:178
+#, c-format
+msgid "ID"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:181
+#, c-format
+msgid "Name"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:220
+#, c-format
+msgid "Edit"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:237
+#, c-format
+msgid "Purge"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:261
+#, c-format
+msgid "There is no instances yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:68
+#, c-format
+msgid "Only show active instances"
msgstr ""
-#: src/components/form/InputDate.tsx:67
-#: src/paths/instance/orders/list/index.tsx:123
+#: src/paths/admin/list/View.tsx:71
#, c-format
-msgid "pick a date"
+msgid "Active"
msgstr ""
-#: src/components/form/InputDate.tsx:81
+#: src/paths/admin/list/View.tsx:78
+#, c-format
+msgid "Only show deleted instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:81
+#, c-format
+msgid "Deleted"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:88
+#, c-format
+msgid "Show all instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:91
+#, c-format
+msgid "All"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:101
+#, c-format
+msgid "Instance \"%1$s\" (ID: %2$s) has been deleted"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:106
+#, c-format
+msgid "Failed to delete instance"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:124
+#, c-format
+msgid "Instance '%1$s' (ID: %2$s) has been disabled"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:129
+#, c-format
+msgid "Failed to purge instance"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:41
+#, c-format
+msgid "Pending KYC verification"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:66
+#, c-format
+msgid "Timed out"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:103
+#, c-format
+msgid "Exchange"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:106
+#, c-format
+msgid "Target account"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:109
+#, c-format
+msgid "KYC URL"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:144
+#, c-format
+msgid "Code"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:147
+#, c-format
+msgid "Http Status"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:177
+#, c-format
+msgid "No pending kyc verification!"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:123
+#, c-format
+msgid "change value to unknown date"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:124
+#, c-format
+msgid "change value to empty"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:131
#, c-format
msgid "clear"
msgstr ""
-#: src/components/form/InputDate.tsx:83
-#: src/paths/instance/transfers/list/Table.tsx:140
+#: src/components/form/InputDate.tsx:136
#, c-format
-msgid "never"
+msgid "change value to never"
msgstr ""
-#: src/components/form/InputImage.tsx:80
+#: src/components/form/InputDate.tsx:141
#, c-format
-msgid "Image should be smaller than 1 MB"
+msgid "never"
msgstr ""
-#: src/components/form/InputLocation.tsx:28
+#: src/components/form/InputLocation.tsx:29
#, c-format
msgid "Country"
msgstr ""
-#: src/components/form/InputLocation.tsx:30
-#: src/paths/admin/create/CreatePage.tsx:99
-#: src/paths/instance/transfers/list/Table.tsx:124
-#: src/paths/instance/update/UpdatePage.tsx:118
+#: src/components/form/InputLocation.tsx:33
#, c-format
msgid "Address"
msgstr ""
-#: src/components/form/InputLocation.tsx:34
+#: src/components/form/InputLocation.tsx:39
#, c-format
msgid "Building number"
msgstr ""
-#: src/components/form/InputLocation.tsx:35
+#: src/components/form/InputLocation.tsx:41
#, c-format
msgid "Building name"
msgstr ""
-#: src/components/form/InputLocation.tsx:36
+#: src/components/form/InputLocation.tsx:42
#, c-format
msgid "Street"
msgstr ""
-#: src/components/form/InputLocation.tsx:37
+#: src/components/form/InputLocation.tsx:43
#, c-format
msgid "Post code"
msgstr ""
-#: src/components/form/InputLocation.tsx:38
+#: src/components/form/InputLocation.tsx:44
#, c-format
msgid "Town location"
msgstr ""
-#: src/components/form/InputLocation.tsx:39
+#: src/components/form/InputLocation.tsx:45
#, c-format
msgid "Town"
msgstr ""
-#: src/components/form/InputLocation.tsx:40
+#: src/components/form/InputLocation.tsx:46
#, c-format
msgid "District"
msgstr ""
-#: src/components/form/InputLocation.tsx:41
+#: src/components/form/InputLocation.tsx:49
#, c-format
msgid "Country subdivision"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:59
+#: src/components/form/InputSearchProduct.tsx:66
#, c-format
msgid "Product id"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:60
-#: src/components/product/ProductForm.tsx:99
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:122
-#: src/paths/instance/orders/list/Table.tsx:227
-#: src/paths/instance/products/list/Table.tsx:86
+#: src/components/form/InputSearchProduct.tsx:69
#, c-format
msgid "Description"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:73
-#: src/components/form/InputTaxes.tsx:81
-#: src/paths/admin/create/CreatePage.tsx:87 src/paths/admin/list/Table.tsx:110
-#: src/paths/instance/details/DetailPage.tsx:76
-#: src/paths/instance/update/UpdatePage.tsx:106
+#: src/components/form/InputSearchProduct.tsx:94
#, c-format
-msgid "Name"
+msgid "Product"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:102
+#: src/components/form/InputSearchProduct.tsx:95
#, c-format
-msgid "loading..."
+msgid "search products by it's description or id"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:108
+#: src/components/form/InputSearchProduct.tsx:151
#, c-format
-msgid "no products found"
+msgid "no products found with that description"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:116
+#: src/components/product/InventoryProductForm.tsx:56
#, c-format
-msgid "no results"
+msgid "You must enter a valid product identifier."
msgstr ""
-#: src/components/form/InputSecured.tsx:33
+#: src/components/product/InventoryProductForm.tsx:64
#, c-format
-msgid "Deleting"
+msgid "Quantity must be greater than 0!"
msgstr ""
-#: src/components/form/InputSecured.tsx:34
+#: src/components/product/InventoryProductForm.tsx:76
#, c-format
-msgid "Changing"
+msgid ""
+"This quantity exceeds remaining stock. Currently, only %1$s units remain "
+"unreserved in stock."
msgstr ""
-#: src/components/form/InputSecured.tsx:60
+#: src/components/product/InventoryProductForm.tsx:109
#, c-format
-msgid "Manage token"
+msgid "Quantity"
msgstr ""
-#: src/components/form/InputSecured.tsx:83
+#: src/components/product/InventoryProductForm.tsx:110
#, c-format
-msgid "Update"
+msgid "how many products will be added"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:117
+#, c-format
+msgid "Add from inventory"
+msgstr ""
+
+#: src/components/form/InputImage.tsx:105
+#, c-format
+msgid "Image should be smaller than 1 MB"
+msgstr ""
+
+#: src/components/form/InputImage.tsx:110
+#, c-format
+msgid "Add"
msgstr ""
-#: src/components/form/InputSecured.tsx:100
-#: src/paths/instance/orders/create/CreatePage.tsx:252
-#: src/paths/instance/orders/create/CreatePage.tsx:273
+#: src/components/form/InputImage.tsx:115
#, c-format
msgid "Remove"
msgstr ""
-#: src/components/form/InputSecured.tsx:106 src/components/modal/index.tsx:52
-#: src/components/modal/index.tsx:73 src/paths/admin/create/CreatePage.tsx:114
-#: src/paths/instance/orders/create/CreatePage.tsx:324
-#: src/paths/instance/products/create/CreatePage.tsx:50
-#: src/paths/instance/products/list/Table.tsx:166
-#: src/paths/instance/products/list/Table.tsx:218
-#: src/paths/instance/products/update/UpdatePage.tsx:54
-#: src/paths/instance/transfers/create/CreatePage.tsx:88
-#: src/paths/instance/update/UpdatePage.tsx:133
+#: src/components/form/InputTaxes.tsx:113
#, c-format
-msgid "Cancel"
+msgid "No taxes configured for this product."
msgstr ""
-#: src/components/form/InputStock.tsx:91
+#: src/components/form/InputTaxes.tsx:119
#, c-format
-msgid "Manage stock"
+msgid "Amount"
+msgstr "Betrag"
+
+#: src/components/form/InputTaxes.tsx:120
+#, c-format
+msgid ""
+"Taxes can be in currencies that differ from the main currency used by the "
+"merchant."
msgstr ""
-#: src/components/form/InputStock.tsx:93
+#: src/components/form/InputTaxes.tsx:122
#, c-format
-msgid "Infinite"
+msgid ""
+"Enter currency and value separated with a colon, e.g. &quot;USD:2.3&quot;."
msgstr ""
-#: src/components/form/InputStock.tsx:105
+#: src/components/form/InputTaxes.tsx:131
#, c-format
-msgid "lost cannot be greater that current + incoming (max %1$s)"
+msgid "Legal name of the tax, e.g. VAT or import duties."
msgstr ""
-#: src/components/form/InputStock.tsx:111
+#: src/components/form/InputTaxes.tsx:137
#, c-format
-msgid "current stock will change from %1$s to %2$s"
+msgid "add tax to the tax list"
msgstr ""
-#: src/components/form/InputStock.tsx:112
+#: src/components/product/NonInventoryProductForm.tsx:72
#, c-format
-msgid "current stock will stay at %1$s"
+msgid "describe and add a product that is not in the inventory list"
msgstr ""
-#: src/components/form/InputStock.tsx:129
-#: src/paths/instance/products/list/Table.tsx:204
+#: src/components/product/NonInventoryProductForm.tsx:75
#, c-format
-msgid "Incoming"
+msgid "Add custom product"
msgstr ""
-#: src/components/form/InputStock.tsx:130
-#: src/paths/instance/products/list/Table.tsx:205
+#: src/components/product/NonInventoryProductForm.tsx:86
#, c-format
-msgid "Lost"
+msgid "Complete information of the product"
msgstr ""
-#: src/components/form/InputStock.tsx:142
+#: src/components/product/NonInventoryProductForm.tsx:185
#, c-format
-msgid "Current"
+msgid "Image"
msgstr ""
-#: src/components/form/InputStock.tsx:145
+#: src/components/product/NonInventoryProductForm.tsx:186
#, c-format
-msgid "without stock"
+msgid "photo of the product"
msgstr ""
-#: src/components/form/InputStock.tsx:150
+#: src/components/product/NonInventoryProductForm.tsx:192
#, c-format
-msgid "Next restock"
+msgid "full product description"
msgstr ""
-#: src/components/form/InputStock.tsx:152
+#: src/components/product/NonInventoryProductForm.tsx:196
#, c-format
-msgid "Delivery address"
+msgid "Unit"
msgstr ""
-#: src/components/form/InputTaxes.tsx:73
+#: src/components/product/NonInventoryProductForm.tsx:197
#, c-format
-msgid "this product has no taxes"
+msgid "name of the product unit"
msgstr ""
-#: src/components/form/InputTaxes.tsx:77
-#: src/paths/instance/orders/details/DetailPage.tsx:145
-#: src/paths/instance/orders/details/DetailPage.tsx:296
-#: src/paths/instance/orders/list/Table.tsx:116
-#: src/paths/instance/transfers/create/CreatePage.tsx:84
+#: src/components/product/NonInventoryProductForm.tsx:201
#, c-format
-msgid "Amount"
+msgid "Price"
msgstr ""
-#: src/components/form/InputTaxes.tsx:78
+#: src/components/product/NonInventoryProductForm.tsx:202
#, c-format
-msgid "currency and value separated with colon"
+msgid "amount in the current currency"
msgstr ""
-#: src/components/form/InputTaxes.tsx:84
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:78
+#: src/components/product/NonInventoryProductForm.tsx:211
#, c-format
-msgid "Add"
+msgid "Taxes"
msgstr ""
-#: src/components/menu/SideBar.tsx:53
+#: src/components/product/ProductList.tsx:38
#, c-format
-msgid "Instance"
+msgid "image"
msgstr ""
-#: src/components/menu/SideBar.tsx:59
+#: src/components/product/ProductList.tsx:41
#, c-format
-msgid "Settings"
+msgid "description"
msgstr ""
-#: src/components/menu/SideBar.tsx:65
-#: src/paths/instance/orders/list/Table.tsx:60
+#: src/components/product/ProductList.tsx:44
#, c-format
-msgid "Orders"
+msgid "quantity"
msgstr ""
-#: src/components/menu/SideBar.tsx:71
-#: src/paths/instance/orders/create/CreatePage.tsx:258
-#: src/paths/instance/products/list/Table.tsx:48
+#: src/components/product/ProductList.tsx:47
#, c-format
-msgid "Products"
+msgid "unit price"
msgstr ""
-#: src/components/menu/SideBar.tsx:77
-#: src/paths/instance/transfers/list/Table.tsx:65
+#: src/components/product/ProductList.tsx:50
#, c-format
-msgid "Transfers"
+msgid "total price"
msgstr ""
-#: src/components/menu/SideBar.tsx:87
+#: src/paths/instance/orders/create/CreatePage.tsx:153
#, c-format
-msgid "Connection"
+msgid "required"
msgstr ""
-#: src/components/menu/SideBar.tsx:112 src/paths/admin/list/Table.tsx:57
+#: src/paths/instance/orders/create/CreatePage.tsx:157
#, c-format
-msgid "Instances"
+msgid "not valid"
msgstr ""
-#: src/components/menu/SideBar.tsx:116
+#: src/paths/instance/orders/create/CreatePage.tsx:159
#, c-format
-msgid "New"
+msgid "must be greater than 0"
msgstr ""
-#: src/components/menu/SideBar.tsx:122
+#: src/paths/instance/orders/create/CreatePage.tsx:164
#, c-format
-msgid "List"
+msgid "not a valid json"
msgstr ""
-#: src/components/menu/SideBar.tsx:129
+#: src/paths/instance/orders/create/CreatePage.tsx:170
#, c-format
-msgid "Log out"
+msgid "should be in the future"
msgstr ""
-#: src/components/modal/index.tsx:74
+#: src/paths/instance/orders/create/CreatePage.tsx:173
#, c-format
-msgid "Clear"
+msgid "refund deadline cannot be before pay deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:179
+#, c-format
+msgid "wire transfer deadline cannot be before refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:190
+#, c-format
+msgid "wire transfer deadline cannot be before pay deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:197
+#, c-format
+msgid "should have a refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:202
+#, c-format
+msgid "auto refund cannot be after refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:360
+#, c-format
+msgid "Manage products in order"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:369
+#, c-format
+msgid "Manage list of products in the order."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:391
+#, c-format
+msgid "Remove this product from the order."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:415
+#, c-format
+msgid "Total price"
msgstr ""
-#: src/components/modal/index.tsx:110 src/components/modal/index.tsx:111
+#: src/paths/instance/orders/create/CreatePage.tsx:417
#, c-format
-msgid "should be the same"
+msgid "total product price added up"
msgstr ""
-#: src/components/modal/index.tsx:111
+#: src/paths/instance/orders/create/CreatePage.tsx:430
#, c-format
-msgid "cannot be the same as before"
+msgid "Amount to be paid by the customer"
msgstr ""
-#: src/components/modal/index.tsx:114
+#: src/paths/instance/orders/create/CreatePage.tsx:436
+#, c-format
+msgid "Order price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:437
+#, c-format
+msgid "final order price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:444
+#, c-format
+msgid "Summary"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:445
+#, c-format
+msgid "Title of the order to be shown to the customer"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:450
+#, c-format
+msgid "Shipping and Fulfillment"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:455
+#, c-format
+msgid "Delivery date"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:456
+#, c-format
+msgid "Deadline for physical delivery assured by the merchant."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:461
+#, c-format
+msgid "Location"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:462
+#, c-format
+msgid "address where the products will be delivered"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:469
+#, c-format
+msgid "Fulfillment URL"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:470
+#, c-format
+msgid "URL to which the user will be redirected after successful payment."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:476
+#, c-format
+msgid "Taler payment options"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:477
+#, c-format
+msgid "Override default Taler payment settings for this order"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:481
+#, c-format
+msgid "Payment deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:482
#, c-format
msgid ""
-"You are updating the authorization token from instance %1$s with id %2$s"
+"Deadline for the customer to pay for the offer before it expires. Inventory "
+"products will be reserved until this deadline."
msgstr ""
-#: src/components/modal/index.tsx:124
+#: src/paths/instance/orders/create/CreatePage.tsx:486
#, c-format
-msgid "Old token"
+msgid "Refund deadline"
msgstr ""
-#: src/components/modal/index.tsx:125
+#: src/paths/instance/orders/create/CreatePage.tsx:487
#, c-format
-msgid "New token"
+msgid "Time until which the order can be refunded by the merchant."
msgstr ""
-#: src/components/modal/index.tsx:127
+#: src/paths/instance/orders/create/CreatePage.tsx:491
#, c-format
-msgid "Clearing the auth token will mean public access to the instance"
+msgid "Wire transfer deadline"
msgstr ""
-#: src/components/product/ProductForm.tsx:96
-#: src/paths/admin/create/CreatePage.tsx:85 src/paths/admin/list/Table.tsx:109
-#: src/paths/instance/transfers/list/Table.tsx:122
+#: src/paths/instance/orders/create/CreatePage.tsx:492
#, c-format
-msgid "ID"
+msgid "Deadline for the exchange to make the wire transfer."
msgstr ""
-#: src/components/product/ProductForm.tsx:98
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:121
-#: src/paths/instance/products/list/Table.tsx:85
+#: src/paths/instance/orders/create/CreatePage.tsx:496
#, c-format
-msgid "Image"
+msgid "Auto-refund deadline"
msgstr ""
-#: src/components/product/ProductForm.tsx:100
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:123
+#: src/paths/instance/orders/create/CreatePage.tsx:497
#, c-format
-msgid "Unit"
+msgid ""
+"Time until which the wallet will automatically check for refunds without "
+"user interaction."
msgstr ""
-#: src/components/product/ProductForm.tsx:101
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:124
-#: src/paths/instance/products/list/Table.tsx:162
-#: src/paths/instance/products/list/Table.tsx:214
+#: src/paths/instance/orders/create/CreatePage.tsx:502
#, c-format
-msgid "Price"
+msgid "Maximum deposit fee"
msgstr ""
-#: src/components/product/ProductForm.tsx:103
-#: src/paths/instance/products/list/Table.tsx:90
+#: src/paths/instance/orders/create/CreatePage.tsx:503
#, c-format
-msgid "Stock"
+msgid ""
+"Maximum deposit fees the merchant is willing to cover for this order. Higher "
+"deposit fees must be covered in full by the consumer."
msgstr ""
-#: src/components/product/ProductForm.tsx:105
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:128
-#: src/paths/instance/products/list/Table.tsx:88
+#: src/paths/instance/orders/create/CreatePage.tsx:507
#, c-format
-msgid "Taxes"
+msgid "Maximum wire fee"
msgstr ""
-#: src/index.tsx:75
+#: src/paths/instance/orders/create/CreatePage.tsx:508
#, c-format
-msgid "Server not found"
+msgid ""
+"Maximum aggregate wire fees the merchant is willing to cover for this order. "
+"Wire fees exceeding this amount are to be covered by the customers."
msgstr ""
-#: src/index.tsx:85
+#: src/paths/instance/orders/create/CreatePage.tsx:512
#, c-format
-msgid "Couldn't access the server"
+msgid "Wire fee amortization"
msgstr ""
-#: src/index.tsx:87 src/index.tsx:99
+#: src/paths/instance/orders/create/CreatePage.tsx:513
#, c-format
-msgid "Got message %1$s from %2$s"
+msgid ""
+"Factor by which wire fees exceeding the above threshold are divided to "
+"determine the share of excess wire fees to be paid explicitly by the "
+"consumer."
msgstr ""
-#: src/index.tsx:97
+#: src/paths/instance/orders/create/CreatePage.tsx:517
#, c-format
-msgid "Unexpected Error"
+msgid "Create token"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:89
-#: src/paths/instance/update/UpdatePage.tsx:108
+#: src/paths/instance/orders/create/CreatePage.tsx:518
#, c-format
-msgid "Auth token"
+msgid ""
+"Uncheck this option if the merchant backend generated an order ID with "
+"enough entropy to prevent adversarial claims."
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:91
-#: src/paths/instance/details/DetailPage.tsx:77
-#: src/paths/instance/update/UpdatePage.tsx:110
+#: src/paths/instance/orders/create/CreatePage.tsx:522
#, c-format
-msgid "Account address"
+msgid "Minimum age required"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:93
-#: src/paths/instance/update/UpdatePage.tsx:112
+#: src/paths/instance/orders/create/CreatePage.tsx:523
#, c-format
-msgid "Default max deposit fee"
+msgid ""
+"Any value greater than 0 will limit the coins able be used to pay this "
+"contract. If empty the age restriction will be defined by the products"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:95
-#: src/paths/instance/update/UpdatePage.tsx:114
+#: src/paths/instance/orders/create/CreatePage.tsx:526
#, c-format
-msgid "Default max wire fee"
+msgid "Min age defined by the producs is %1$s"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:97
-#: src/paths/instance/update/UpdatePage.tsx:116
+#: src/paths/instance/orders/create/CreatePage.tsx:534
#, c-format
-msgid "Default wire fee amortization"
+msgid "Additional information"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:103
-#: src/paths/instance/update/UpdatePage.tsx:122
+#: src/paths/instance/orders/create/CreatePage.tsx:535
#, c-format
-msgid "Jurisdiction"
+msgid "Custom information to be included in the contract for this order."
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:107
-#: src/paths/instance/update/UpdatePage.tsx:126
+#: src/paths/instance/orders/create/CreatePage.tsx:541
#, c-format
-msgid "Default pay delay"
+msgid "You must enter a value in JavaScript Object Notation (JSON)."
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:109
-#: src/paths/instance/update/UpdatePage.tsx:128
+#: src/components/picker/DurationPicker.tsx:55
#, c-format
-msgid "Default wire transfer delay"
+msgid "days"
msgstr ""
-#: src/paths/admin/create/index.tsx:58
+#: src/components/picker/DurationPicker.tsx:65
#, c-format
-msgid "could not create instance"
+msgid "hours"
msgstr ""
-#: src/paths/admin/list/Table.tsx:63 src/paths/admin/list/Table.tsx:131
-#: src/paths/instance/transfers/list/Table.tsx:71
+#: src/components/picker/DurationPicker.tsx:76
#, c-format
-msgid "Delete"
+msgid "minutes"
msgstr ""
-#: src/paths/admin/list/Table.tsx:128
+#: src/components/picker/DurationPicker.tsx:87
#, c-format
-msgid "Edit"
+msgid "seconds"
msgstr ""
-#: src/paths/admin/list/Table.tsx:149
-#: src/paths/instance/products/list/Table.tsx:245
+#: src/components/form/InputDuration.tsx:53
#, c-format
-msgid "There is no instances yet, add more pressing the + sign"
+msgid "forever"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:237
+#: src/components/form/InputDuration.tsx:62
#, c-format
-msgid "Inventory products"
+msgid "%1$sM"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:286
+#: src/components/form/InputDuration.tsx:64
#, c-format
-msgid "Total price"
+msgid "%1$sY"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:287
+#: src/components/form/InputDuration.tsx:66
#, c-format
-msgid "Total tax"
+msgid "%1$sd"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:289
-#: src/paths/instance/orders/create/CreatePage.tsx:297
+#: src/components/form/InputDuration.tsx:68
#, c-format
-msgid "Order price"
+msgid "%1$sh"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:295
+#: src/components/form/InputDuration.tsx:70
#, c-format
-msgid "Net"
+msgid "%1$smin"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:300
-#: src/paths/instance/orders/details/DetailPage.tsx:144
-#: src/paths/instance/orders/details/DetailPage.tsx:295
-#: src/paths/instance/orders/list/Table.tsx:117
+#: src/components/form/InputDuration.tsx:72
#, c-format
-msgid "Summary"
+msgid "%1$ssec"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:302
+#: src/paths/instance/orders/list/Table.tsx:75
#, c-format
-msgid "Payments options"
+msgid "Orders"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:303
+#: src/paths/instance/orders/list/Table.tsx:81
#, c-format
-msgid "Auto refund deadline"
+msgid "create order"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:304
+#: src/paths/instance/orders/list/Table.tsx:147
#, c-format
-msgid "Refund deadline"
+msgid "load newer orders"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:305
+#: src/paths/instance/orders/list/Table.tsx:154
+#, c-format
+msgid "Date"
+msgstr "Datum"
+
+#: src/paths/instance/orders/list/Table.tsx:200
#, c-format
-msgid "Pay deadline"
+msgid "Refund"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:307
+#: src/paths/instance/orders/list/Table.tsx:209
#, c-format
-msgid "Delivery date"
+msgid "copy url"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:308
+#: src/paths/instance/orders/list/Table.tsx:225
#, c-format
-msgid "Location"
+msgid "load older orders"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:312
+#: src/paths/instance/orders/list/Table.tsx:242
+#, c-format
+msgid "No orders have been found matching your query!"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:288
+#, c-format
+msgid "duplicated"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:299
+#, c-format
+msgid "invalid format"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:301
+#, c-format
+msgid "this value exceed the refundable amount"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:346
+#, c-format
+msgid "date"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:349
+#, c-format
+msgid "amount"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:352
+#, c-format
+msgid "reason"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:389
+#, c-format
+msgid "amount to be refunded"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:391
+#, c-format
+msgid "Max refundable:"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:396
+#, c-format
+msgid "Reason"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:397
+#, c-format
+msgid "Choose one..."
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:399
+#, c-format
+msgid "requested by the customer"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:400
+#, c-format
+msgid "other"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:403
+#, c-format
+msgid "why this order is being refunded"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:409
+#, c-format
+msgid "more information to give context"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:62
+#, c-format
+msgid "Contract Terms"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:68
+#, c-format
+msgid "human-readable description of the whole purchase"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:74
+#, c-format
+msgid "total price for the transaction"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:81
+#, c-format
+msgid "URL for this purchase"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:87
#, c-format
msgid "Max fee"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:313
+#: src/paths/instance/orders/details/DetailPage.tsx:88
+#, c-format
+msgid "maximum total deposit fee accepted by the merchant for this contract"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:93
#, c-format
msgid "Max wire fee"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:314
+#: src/paths/instance/orders/details/DetailPage.tsx:94
#, c-format
-msgid "Wire fee amortization"
+msgid "maximum wire fee accepted by the merchant"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:315
+#: src/paths/instance/orders/details/DetailPage.tsx:100
#, c-format
-msgid "Fullfilment url"
+msgid ""
+"over how many customer transactions does the merchant expect to amortize "
+"wire fees on average"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:318
+#: src/paths/instance/orders/details/DetailPage.tsx:105
#, c-format
-msgid "Extra information"
+msgid "Created at"
msgstr ""
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:44
+#: src/paths/instance/orders/details/DetailPage.tsx:106
#, c-format
-msgid "select a product first"
+msgid "time when this contract was generated"
msgstr ""
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:51
+#: src/paths/instance/orders/details/DetailPage.tsx:112
#, c-format
-msgid "should be greater than 0"
+msgid "after this deadline has passed no refunds will be accepted"
msgstr ""
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:58
+#: src/paths/instance/orders/details/DetailPage.tsx:118
#, c-format
msgid ""
-"cannot be greater than current stock and quantity previously added. max: %1$s"
+"after this deadline, the merchant won't accept payments for the contract"
msgstr ""
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:64
+#: src/paths/instance/orders/details/DetailPage.tsx:124
#, c-format
-msgid "cannot be greater than current stock %1$s"
+msgid "transfer deadline for the exchange"
msgstr ""
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:76
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:126
+#: src/paths/instance/orders/details/DetailPage.tsx:130
#, c-format
-msgid "Quantity"
+msgid "time indicating when the order should be delivered"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:92
-#: src/paths/instance/orders/details/DetailPage.tsx:235
-#: src/paths/instance/orders/details/DetailPage.tsx:333
+#: src/paths/instance/orders/details/DetailPage.tsx:136
#, c-format
-msgid "Order"
+msgid "where the order will be delivered"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:93
+#: src/paths/instance/orders/details/DetailPage.tsx:144
#, c-format
-msgid "claimed"
+msgid "Auto-refund delay"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:110
-#: src/paths/instance/orders/details/DetailPage.tsx:261
-#: src/paths/instance/orders/list/Table.tsx:136
+#: src/paths/instance/orders/details/DetailPage.tsx:145
#, c-format
-msgid "copy url"
+msgid ""
+"how long the wallet should try to get an automatic refund for the purchase"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:126
-#: src/paths/instance/orders/details/DetailPage.tsx:349
+#: src/paths/instance/orders/details/DetailPage.tsx:150
#, c-format
-msgid "pay at"
+msgid "Extra info"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:127
-#: src/paths/instance/orders/details/DetailPage.tsx:350
+#: src/paths/instance/orders/details/DetailPage.tsx:151
#, c-format
-msgid "created at"
+msgid "extra data that is only interpreted by the merchant frontend"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:138
-#: src/paths/instance/orders/details/DetailPage.tsx:289
+#: src/paths/instance/orders/details/DetailPage.tsx:219
+#, c-format
+msgid "Order"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:221
+#, c-format
+msgid "claimed"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:247
+#, c-format
+msgid "claimed at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:265
#, c-format
msgid "Timeline"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:142
-#: src/paths/instance/orders/details/DetailPage.tsx:293
+#: src/paths/instance/orders/details/DetailPage.tsx:271
#, c-format
msgid "Payment details"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:146
-#: src/paths/instance/orders/details/DetailPage.tsx:299
-#: src/paths/instance/orders/details/DetailPage.tsx:363
+#: src/paths/instance/orders/details/DetailPage.tsx:291
#, c-format
msgid "Order status"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:156
-#: src/paths/instance/orders/details/DetailPage.tsx:308
+#: src/paths/instance/orders/details/DetailPage.tsx:301
#, c-format
msgid "Product list"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:236
+#: src/paths/instance/orders/details/DetailPage.tsx:451
#, c-format
msgid "paid"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:238
+#: src/paths/instance/orders/details/DetailPage.tsx:455
#, c-format
msgid "wired"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:241
+#: src/paths/instance/orders/details/DetailPage.tsx:460
#, c-format
msgid "refunded"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:258
+#: src/paths/instance/orders/details/DetailPage.tsx:480
+#, c-format
+msgid "refund order"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:481
+#, c-format
+msgid "not refundable"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:489
#, c-format
msgid "refund"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:297
+#: src/paths/instance/orders/details/DetailPage.tsx:553
#, c-format
msgid "Refunded amount"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:298
+#: src/paths/instance/orders/details/DetailPage.tsx:560
+#, c-format
+msgid "Refund taken"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:570
+#, c-format
+msgid "Status URL"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:583
#, c-format
-msgid "Deposit total"
+msgid "Refund URI"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:336
+#: src/paths/instance/orders/details/DetailPage.tsx:636
#, c-format
msgid "unpaid"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:364
+#: src/paths/instance/orders/details/DetailPage.tsx:654
+#, c-format
+msgid "pay at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:666
+#, c-format
+msgid "created at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:707
#, c-format
msgid "Order status URL"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:365
+#: src/paths/instance/orders/details/DetailPage.tsx:711
#, c-format
-msgid "Pay URI"
+msgid "Payment URI"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:383
+#: src/paths/instance/orders/details/DetailPage.tsx:740
#, c-format
msgid ""
"Unknown order status. This is an error, please contact the administrator."
msgstr ""
-#: src/paths/instance/orders/details/index.tsx:56
-#: src/paths/instance/orders/list/index.tsx:147
+#: src/paths/instance/orders/details/DetailPage.tsx:767
+#, c-format
+msgid "Back"
+msgstr "Zurück"
+
+#: src/paths/instance/orders/details/index.tsx:79
#, c-format
msgid "refund created successfully"
msgstr ""
-#: src/paths/instance/orders/details/index.tsx:59
-#: src/paths/instance/orders/list/index.tsx:150
+#: src/paths/instance/orders/details/index.tsx:85
#, c-format
msgid "could not create the refund"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:111
+#: src/paths/instance/orders/list/ListPage.tsx:78
#, c-format
-msgid "load newer orders"
+msgid "select date to show nearby orders"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:115
+#: src/paths/instance/orders/list/ListPage.tsx:94
#, c-format
-msgid "Date"
+msgid "order id"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:131
-#: src/paths/instance/orders/list/Table.tsx:223
+#: src/paths/instance/orders/list/ListPage.tsx:100
#, c-format
-msgid "Refund"
+msgid "jump to order with the given order ID"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:145
+#: src/paths/instance/orders/list/ListPage.tsx:122
#, c-format
-msgid "load older orders"
+msgid "remove all filters"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:154
+#: src/paths/instance/orders/list/ListPage.tsx:132
#, c-format
-msgid "No orders has been found"
+msgid "only show paid orders"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:202
+#: src/paths/instance/orders/list/ListPage.tsx:135
#, c-format
-msgid "date"
+msgid "Paid"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:203
+#: src/paths/instance/orders/list/ListPage.tsx:142
#, c-format
-msgid "amount"
+msgid "only show orders with refunds"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:204
+#: src/paths/instance/orders/list/ListPage.tsx:145
#, c-format
-msgid "reason"
+msgid "Refunded"
+msgstr "Rückerstattet"
+
+#: src/paths/instance/orders/list/ListPage.tsx:152
+#, c-format
+msgid ""
+"only show orders where customers paid, but wire payments from payment "
+"provider are still pending"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:224
+#: src/paths/instance/orders/list/ListPage.tsx:155
#, c-format
-msgid "Max refundable:"
+msgid "Not wired"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:226
+#: src/paths/instance/orders/list/ListPage.tsx:170
#, c-format
-msgid "Reason"
+msgid "clear date filter"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:226
+#: src/paths/instance/orders/list/ListPage.tsx:184
#, c-format
-msgid "duplicated"
+msgid "date (YYYY/MM/DD)"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:226
+#: src/paths/instance/orders/list/index.tsx:103
#, c-format
-msgid "requested by the customer"
+msgid "Enter an order id"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:226
+#: src/paths/instance/orders/list/index.tsx:111
#, c-format
-msgid "other"
+msgid "order not found"
msgstr ""
-#: src/paths/instance/orders/list/index.tsx:91
+#: src/paths/instance/orders/list/index.tsx:178
#, c-format
-msgid "go to order id"
+msgid "could not get the order to refund"
msgstr ""
-#: src/paths/instance/orders/list/index.tsx:107
+#: src/components/exception/AsyncButton.tsx:43
#, c-format
-msgid "Paid"
+msgid "Loading..."
msgstr ""
-#: src/paths/instance/orders/list/index.tsx:108
+#: src/components/form/InputStock.tsx:99
#, c-format
-msgid "Refunded"
+msgid ""
+"click here to configure the stock of the product, leave it as is and the "
+"backend will not control stock"
msgstr ""
-#: src/paths/instance/orders/list/index.tsx:109
+#: src/components/form/InputStock.tsx:109
#, c-format
-msgid "Not wired"
+msgid "Manage stock"
msgstr ""
-#: src/paths/instance/orders/list/index.tsx:110
+#: src/components/form/InputStock.tsx:115
#, c-format
-msgid "All"
+msgid "this product has been configured without stock control"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:119
+#, c-format
+msgid "Infinite"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:136
+#, c-format
+msgid "lost cannot be greater than current and incoming (max %1$s)"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:176
+#, c-format
+msgid "Incoming"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:177
+#, c-format
+msgid "Lost"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:192
+#, c-format
+msgid "Current"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:196
+#, c-format
+msgid "remove stock control for this product"
msgstr ""
-#: src/paths/instance/products/create/index.tsx:48
-#: src/paths/instance/products/update/index.tsx:64
+#: src/components/form/InputStock.tsx:202
+#, c-format
+msgid "without stock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:211
+#, c-format
+msgid "Next restock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:217
+#, c-format
+msgid "Delivery address"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:133
+#, c-format
+msgid "product identification to use in URLs (for internal use only)"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:139
+#, c-format
+msgid "illustration of the product for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:145
+#, c-format
+msgid "product description for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:149
+#, c-format
+msgid "Age restricted"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:150
+#, c-format
+msgid "is this product restricted for customer below certain age?"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:155
+#, c-format
+msgid ""
+"unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 "
+"items, 5 meters) for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:160
+#, c-format
+msgid ""
+"sale price for customers, including taxes, for above units of the product"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:164
+#, c-format
+msgid "Stock"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:166
+#, c-format
+msgid ""
+"product inventory for products with finite supply (for internal use only)"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:171
+#, c-format
+msgid "taxes included in the product price, exposed to customers"
+msgstr ""
+
+#: src/paths/instance/products/create/CreatePage.tsx:66
+#, c-format
+msgid "Need to complete marked fields"
+msgstr ""
+
+#: src/paths/instance/products/create/index.tsx:51
#, c-format
msgid "could not create product"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:87
+#: src/paths/instance/products/list/Table.tsx:68
+#, c-format
+msgid "Products"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:73
+#, c-format
+msgid "add product to inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:137
#, c-format
msgid "Sell"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:89
+#: src/paths/instance/products/list/Table.tsx:143
#, c-format
msgid "Profit"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:91
+#: src/paths/instance/products/list/Table.tsx:149
#, c-format
msgid "Sold"
msgstr ""
-#: src/paths/instance/products/list/index.tsx:59
+#: src/paths/instance/products/list/Table.tsx:210
+#, c-format
+msgid "free"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:248
+#, c-format
+msgid "go to product update page"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:255
+#, c-format
+msgid "Update"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:260
+#, c-format
+msgid "remove this product from the database"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:331
+#, c-format
+msgid "update the product with new price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:341
+#, c-format
+msgid "update product with new price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:399
+#, c-format
+msgid "add more elements to the inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:404
+#, c-format
+msgid "report elements lost in the inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:409
+#, c-format
+msgid "new price for the product"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:421
+#, c-format
+msgid "the are value with errors"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:422
+#, c-format
+msgid "update product with new stock and price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:463
+#, c-format
+msgid "There is no products yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:86
#, c-format
msgid "product updated successfully"
msgstr ""
-#: src/paths/instance/products/list/index.tsx:62
+#: src/paths/instance/products/list/index.tsx:92
#, c-format
msgid "could not update the product"
msgstr ""
-#: src/paths/instance/products/list/index.tsx:70
+#: src/paths/instance/products/list/index.tsx:103
#, c-format
msgid "product delete successfully"
msgstr ""
-#: src/paths/instance/products/list/index.tsx:73
+#: src/paths/instance/products/list/index.tsx:109
#, c-format
msgid "could not delete the product"
msgstr ""
-#: src/paths/instance/tips/list/Table.tsx:59
+#: src/paths/instance/products/update/UpdatePage.tsx:56
+#, c-format
+msgid "Product id:"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:95
+#, c-format
+msgid ""
+"To complete the setup of the reserve, you must now initiate a wire transfer "
+"using the given wire transfer subject and crediting the specified amount to "
+"the indicated account of the exchange."
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:102
+#, c-format
+msgid "If your system supports RFC 8905, you can do this by opening this URI:"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:83
+#, c-format
+msgid "it should be greater than 0"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:88
+#, c-format
+msgid "must be a valid URL"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:107
+#, c-format
+msgid "Initial balance"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:108
+#, c-format
+msgid "balance prior to deposit"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:112
+#, c-format
+msgid "Exchange URL"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:113
+#, c-format
+msgid "URL of exchange"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:148
+#, c-format
+msgid "Next"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:186
+#, c-format
+msgid "Wire method"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:187
+#, c-format
+msgid "method to use for wire transfer"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:189
+#, c-format
+msgid "Select one wire method"
+msgstr ""
+
+#: src/paths/instance/reserves/create/index.tsx:62
+#, c-format
+msgid "could not create reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:77
+#, c-format
+msgid "Valid until"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:82
+#, c-format
+msgid "Created balance"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:99
+#, c-format
+msgid "Exchange balance"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:104
+#, c-format
+msgid "Picked up"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:109
+#, c-format
+msgid "Committed"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:116
+#, c-format
+msgid "Account address"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:119
+#, c-format
+msgid "Subject"
+msgstr "Verwendungszweck"
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:130
#, c-format
msgid "Tips"
msgstr ""
-#: src/paths/instance/tips/list/Table.tsx:111
+#: src/paths/instance/reserves/details/DetailPage.tsx:193
#, c-format
-msgid "Committed amount"
+msgid "No tips has been authorized from this reserve"
msgstr ""
-#: src/paths/instance/tips/list/Table.tsx:112
+#: src/paths/instance/reserves/details/DetailPage.tsx:213
#, c-format
-msgid "Exchange initial amount"
+msgid "Authorized"
msgstr ""
-#: src/paths/instance/tips/list/Table.tsx:113
+#: src/paths/instance/reserves/details/DetailPage.tsx:222
#, c-format
-msgid "Merchant initial amount"
+msgid "Expiration"
msgstr ""
-#: src/paths/instance/tips/list/Table.tsx:148
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:108
#, c-format
-msgid "There is no tips yet, add more pressing the + sign"
+msgid "amount of tip"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:50
-#: src/paths/instance/transfers/create/CreatePage.tsx:54
-#: src/paths/instance/transfers/create/CreatePage.tsx:55
-#: src/paths/instance/transfers/create/CreatePage.tsx:56
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:112
#, c-format
-msgid "cannot be empty"
+msgid "Justification"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:114
+#, c-format
+msgid "reason for the tip"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:118
+#, c-format
+msgid "URL after tip"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:119
+#, c-format
+msgid "URL to visit after tip payment"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:65
+#, c-format
+msgid "Reserves not yet funded"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:89
+#, c-format
+msgid "Reserves ready"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:95
+#, c-format
+msgid "add new reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:143
+#, c-format
+msgid "Expires at"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:146
+#, c-format
+msgid "Initial"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:202
+#, c-format
+msgid "delete selected reserve from the database"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:210
+#, c-format
+msgid "authorize new tip from selected reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:237
+#, c-format
+msgid ""
+"There is no ready reserves yet, add more pressing the + sign or fund them"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:264
+#, c-format
+msgid "Expected Balance"
+msgstr ""
+
+#: src/paths/instance/reserves/list/index.tsx:110
+#, c-format
+msgid "could not create the tip"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:77
+#, c-format
+msgid "should not be empty"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:93
+#, c-format
+msgid "should be greater that 0"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:96
+#, c-format
+msgid "can't be empty"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:100
+#, c-format
+msgid "to short"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:108
+#, c-format
+msgid "just letters and numbers from 2 to 7"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:110
+#, c-format
+msgid "size of the key should be 32"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:137
+#, c-format
+msgid "Identifier"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:138
+#, c-format
+msgid "Name of the template in URLs."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:144
+#, c-format
+msgid "Describe what this template stands for"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:149
+#, c-format
+msgid "Fixed summary"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:150
+#, c-format
+msgid "If specified, this template will create order with the same summary"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:154
+#, c-format
+msgid "Fixed price"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:155
+#, c-format
+msgid "If specified, this template will create order with the same price"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:159
+#, c-format
+msgid "Minimum age"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:161
+#, c-format
+msgid "Is this contract restricted to some age?"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:165
+#, c-format
+msgid "Payment timeout"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:167
+#, c-format
+msgid ""
+"How much time has the customer to complete the payment once the order was "
+"created."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:171
+#, c-format
+msgid "Verification algorithm"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:172
+#, c-format
+msgid "Algorithm to use to verify transaction in offline mode"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:180
+#, c-format
+msgid "Point-of-sale key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:182
+#, c-format
+msgid "Useful to validate the purchase"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:196
+#, c-format
+msgid "generate random secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:203
+#, c-format
+msgid "random"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:208
+#, c-format
+msgid "show secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:209
+#, c-format
+msgid "hide secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:216
+#, c-format
+msgid "hide"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:218
+#, c-format
+msgid "show"
+msgstr ""
+
+#: src/paths/instance/templates/create/index.tsx:52
+#, c-format
+msgid "could not inform template"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:54
+#, c-format
+msgid "Amount is required"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:58
+#, c-format
+msgid "Order summary is required"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:86
+#, c-format
+msgid "New order for template"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:108
+#, c-format
+msgid "Amount of the order"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:113
+#, c-format
+msgid "Order summary"
+msgstr ""
+
+#: src/paths/instance/templates/use/index.tsx:92
+#, c-format
+msgid "could not create order from template"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:131
+#, c-format
+msgid ""
+"Here you can specify a default value for fields that are not fixed. Default "
+"values can be edited by the customer before the payment."
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:148
+#, c-format
+msgid "Fixed amount"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:51
+#: src/paths/instance/templates/qr/QrPage.tsx:149
#, c-format
-msgid "check the id, doest look valid"
+msgid "Default amount"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:52
+#: src/paths/instance/templates/qr/QrPage.tsx:161
+#, c-format
+msgid "Default summary"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:177
+#, c-format
+msgid "Print"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:184
+#, c-format
+msgid "Setup TOTP"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:65
+#, c-format
+msgid "Templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:70
+#, c-format
+msgid "add new templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:142
+#, c-format
+msgid "load more templates before the first one"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:146
+#, c-format
+msgid "load newer templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:181
+#, c-format
+msgid "delete selected templates from the database"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:188
+#, c-format
+msgid "use template to create new order"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:195
+#, c-format
+msgid "create qr code for the template"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:210
+#, c-format
+msgid "load more templates after the last one"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:214
+#, c-format
+msgid "load older templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:231
+#, c-format
+msgid "There is no templates yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/templates/list/index.tsx:104
+#, c-format
+msgid "template delete successfully"
+msgstr ""
+
+#: src/paths/instance/templates/list/index.tsx:110
+#, c-format
+msgid "could not delete the template"
+msgstr ""
+
+#: src/paths/instance/templates/update/index.tsx:90
+#, c-format
+msgid "could not update template"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:57
+#, c-format
+msgid "should be one of '%1$s'"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:85
+#, c-format
+msgid "Webhook ID to use"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:89
+#, c-format
+msgid "Event"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:90
+#, c-format
+msgid "The event of the webhook: why the webhook is used"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:94
+#, c-format
+msgid "Method"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:95
+#, c-format
+msgid "Method used by the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:99
+#, c-format
+msgid "URL"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:100
+#, c-format
+msgid "URL of the webhook where the customer will be redirected"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:104
+#, c-format
+msgid "Header"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:106
+#, c-format
+msgid "Header template of the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:111
+#, c-format
+msgid "Body"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:112
+#, c-format
+msgid "Body template by the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:61
+#, c-format
+msgid "Webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:66
+#, c-format
+msgid "add new webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:137
+#, c-format
+msgid "load more webhooks before the first one"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:141
+#, c-format
+msgid "load newer webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:151
+#, c-format
+msgid "Event type"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:176
+#, c-format
+msgid "delete selected webhook from the database"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:198
+#, c-format
+msgid "load more webhooks after the last one"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:202
+#, c-format
+msgid "load older webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:219
+#, c-format
+msgid "There is no webhooks yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/index.tsx:94
+#, c-format
+msgid "webhook delete successfully"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/index.tsx:100
+#, c-format
+msgid "could not delete the webhook"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:63
+#, c-format
+msgid "check the id, does not look valid"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:65
#, c-format
msgid "should have 52 characters, current %1$s"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:57
+#: src/paths/instance/transfers/create/CreatePage.tsx:72
#, c-format
msgid "URL doesn't have the right format"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:74
+#: src/paths/instance/transfers/create/CreatePage.tsx:98
#, c-format
-msgid "Transfer ID"
+msgid "Credited bank account"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:76
+#: src/paths/instance/transfers/create/CreatePage.tsx:100
#, c-format
-msgid "Account Address"
+msgid "Select one account"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:82
-#: src/paths/instance/transfers/list/Table.tsx:125
+#: src/paths/instance/transfers/create/CreatePage.tsx:101
#, c-format
-msgid "Exchange URL"
+msgid "Bank account of the merchant where the payment was received"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:105
+#, c-format
+msgid "Wire transfer ID"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:107
+#, c-format
+msgid ""
+"unique identifier of the wire transfer used by the exchange, must be 52 "
+"characters long"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:112
+#, c-format
+msgid ""
+"Base URL of the exchange that made the transfer, should have been in the "
+"wire transfer subject"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:117
+#, c-format
+msgid "Amount credited"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:118
+#, c-format
+msgid "Actual amount that was wired to the merchant's bank account"
msgstr ""
-#: src/paths/instance/transfers/create/index.tsx:49
+#: src/paths/instance/transfers/create/index.tsx:58
#, c-format
msgid "could not inform transfer"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:118
+#: src/paths/instance/transfers/list/Table.tsx:61
+#, c-format
+msgid "Transfers"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:66
+#, c-format
+msgid "add new transfer"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:129
+#, c-format
+msgid "load more transfers before the first one"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:133
#, c-format
msgid "load newer transfers"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:123
+#: src/paths/instance/transfers/list/Table.tsx:143
#, c-format
msgid "Credit"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:126
+#: src/paths/instance/transfers/list/Table.tsx:152
#, c-format
msgid "Confirmed"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:127
-#: src/paths/instance/transfers/list/index.tsx:60
+#: src/paths/instance/transfers/list/Table.tsx:155
#, c-format
msgid "Verified"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:128
+#: src/paths/instance/transfers/list/Table.tsx:158
#, c-format
msgid "Executed at"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:138
-#: src/paths/instance/transfers/list/Table.tsx:139
+#: src/paths/instance/transfers/list/Table.tsx:171
#, c-format
msgid "yes"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:138
-#: src/paths/instance/transfers/list/Table.tsx:139
+#: src/paths/instance/transfers/list/Table.tsx:171
#, c-format
msgid "no"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:140
+#: src/paths/instance/transfers/list/Table.tsx:181
#, c-format
msgid "unknown"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:145
+#: src/paths/instance/transfers/list/Table.tsx:187
+#, c-format
+msgid "delete selected transfer from the database"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:202
+#, c-format
+msgid "load more transfer after the last one"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:206
#, c-format
msgid "load older transfers"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:154
+#: src/paths/instance/transfers/list/Table.tsx:223
#, c-format
msgid "There is no transfer yet, add more pressing the + sign"
msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:79
+#, c-format
+msgid "filter by account address"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:100
+#, c-format
+msgid "only show wire transfers confirmed by the merchant"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:110
+#, c-format
+msgid "only show wire transfers claimed by the exchange"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:113
+#, c-format
+msgid "Unverified"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:69
+#, c-format
+msgid "is not valid"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:94
+#, c-format
+msgid "is not a number"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:96
+#, c-format
+msgid "must be 1 or greater"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:107
+#, c-format
+msgid "max 7 lines"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:178
+#, c-format
+msgid "change authorization configuration"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:217
+#, c-format
+msgid "Need to complete marked fields and choose authorization method"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:82
+#, c-format
+msgid "This is not a valid bitcoin address."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:95
+#, c-format
+msgid "This is not a valid Ethereum address."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:118
+#, c-format
+msgid "IBAN numbers usually have more that 4 digits"
+msgstr "IBAN-Nummern haben normalerweise mehr als 4 Ziffern"
+
+#: src/components/form/InputPaytoForm.tsx:120
+#, c-format
+msgid "IBAN numbers usually have less that 34 digits"
+msgstr "IBAN-Nummern haben normalerweise weniger als 34 Ziffern"
+
+#: src/components/form/InputPaytoForm.tsx:128
+#, c-format
+msgid "IBAN country code not found"
+msgstr "IBAN-Ländercode wurde nicht gefunden"
+
+#: src/components/form/InputPaytoForm.tsx:153
+#, c-format
+msgid "IBAN number is not valid, checksum is wrong"
+msgstr "IBAN-Nummer ist ungültig, die Prüfsumme ist falsch"
+
+#: src/components/form/InputPaytoForm.tsx:248
+#, c-format
+msgid "Target type"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:249
+#, c-format
+msgid "Method to use for wire transfer"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:258
+#, c-format
+msgid "Routing"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:259
+#, c-format
+msgid "Routing number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:263
+#, c-format
+msgid "Account"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:264
+#, c-format
+msgid "Account number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:273
+#, c-format
+msgid "Business Identifier Code."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:282
+#, c-format
+msgid "Bank Account Number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:292
+#, c-format
+msgid "Unified Payment Interface."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:301
+#, c-format
+msgid "Bitcoin protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:310
+#, c-format
+msgid "Ethereum protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:319
+#, c-format
+msgid "Interledger protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:328
+#, c-format
+msgid "Host"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:329
+#, c-format
+msgid "Bank host."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:334
+#, c-format
+msgid "Bank account."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:343
+#, c-format
+msgid "Bank account owner's name."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:370
+#, c-format
+msgid "No accounts yet."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:52
+#, c-format
+msgid ""
+"Name of the instance in URLs. The 'default' instance is special in that it "
+"is used to administer other instances."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:58
+#, c-format
+msgid "Business name"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:59
+#, c-format
+msgid "Legal name of the business represented by this instance."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:64
+#, c-format
+msgid "Email"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:65
+#, c-format
+msgid "Contact email"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:70
+#, c-format
+msgid "Website URL"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:71
+#, c-format
+msgid "URL."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:76
+#, c-format
+msgid "Logo"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:77
+#, c-format
+msgid "Logo image."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:82
+#, c-format
+msgid "Bank account"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:83
+#, c-format
+msgid "URI specifying bank account for crediting revenue."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:88
+#, c-format
+msgid "Default max deposit fee"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:89
+#, c-format
+msgid ""
+"Maximum deposit fees this merchant is willing to pay per order by default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:94
+#, c-format
+msgid "Default max wire fee"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:95
+#, c-format
+msgid ""
+"Maximum wire fees this merchant is willing to pay per wire transfer by "
+"default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:100
+#, c-format
+msgid "Default wire fee amortization"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:101
+#, c-format
+msgid ""
+"Number of orders excess wire transfer fees will be divided by to compute per "
+"order surcharge."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:107
+#, c-format
+msgid "Physical location of the merchant."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:114
+#, c-format
+msgid "Jurisdiction"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:115
+#, c-format
+msgid "Jurisdiction for legal disputes with the merchant."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:122
+#, c-format
+msgid "Default payment delay"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:124
+#, c-format
+msgid ""
+"Time customers have to pay an order before the offer expires by default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:129
+#, c-format
+msgid "Default wire transfer delay"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:130
+#, c-format
+msgid ""
+"Maximum time an exchange is allowed to delay wiring funds to the merchant, "
+"enabling it to aggregate smaller payments into larger wire transfers and "
+"reducing wire fees."
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:164
+#, c-format
+msgid "Instance id"
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:173
+#, c-format
+msgid "Change the authorization method use for this instance."
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:182
+#, c-format
+msgid "Manage access token"
+msgstr ""
+
+#: src/paths/instance/update/index.tsx:112
+#, c-format
+msgid "Failed to create instance"
+msgstr ""
+
+#: src/components/exception/login.tsx:74
+#, c-format
+msgid "Login required"
+msgstr ""
+
+#: src/components/exception/login.tsx:80
+#, c-format
+msgid "Please enter your access token."
+msgstr ""
+
+#: src/components/exception/login.tsx:108
+#, c-format
+msgid "Access Token"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:171
+#, c-format
+msgid "The request to the backend take too long and was cancelled"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:172
+#, c-format
+msgid "Diagnostic from %1$s is \"%2$s\""
+msgstr ""
+
+#: src/InstanceRoutes.tsx:178
+#, c-format
+msgid "The backend reported a problem: HTTP status #%1$s"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:179
+#, c-format
+msgid "Diagnostic from %1$s is '%2$s'"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:196
+#, c-format
+msgid "Access denied"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:197
+#, c-format
+msgid "The access token provided is invalid."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:212
+#, c-format
+msgid "No 'default' instance configured yet."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:213
+#, c-format
+msgid "Create a 'default' instance to begin using the merchant backoffice."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:630
+#, c-format
+msgid "The access token provided is invalid"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:664
+#, c-format
+msgid "Hide for today"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:82
+#, c-format
+msgid "Instance"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:91
+#, c-format
+msgid "Settings"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:167
+#, c-format
+msgid "Connection"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:209
+#, c-format
+msgid "New"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:219
+#, c-format
+msgid "List"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:234
+#, c-format
+msgid "Log out"
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:71
+#, c-format
+msgid "Check your token is valid"
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:90
+#, c-format
+msgid "Couldn't access the server."
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:91
+#, c-format
+msgid "Could not infer instance id from url %1$s"
+msgstr ""
+
+#: src/Application.tsx:104
+#, c-format
+msgid "Server not found"
+msgstr ""
+
+#: src/Application.tsx:118
+#, c-format
+msgid "Server response with an error code"
+msgstr ""
+
+#: src/Application.tsx:120
+#, c-format
+msgid "Got message %1$s from %2$s"
+msgstr ""
+
+#: src/Application.tsx:131
+#, c-format
+msgid "Response from server is unreadable, http status: %1$s"
+msgstr ""
+
+#: src/Application.tsx:144
+#, c-format
+msgid "Unexpected Error"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:101
+#, c-format
+msgid "The value %1$s is invalid for a payment url"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:110
+#, c-format
+msgid "add element to the list"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:112
+#, c-format
+msgid "add"
+msgstr ""
+
+#: src/components/form/InputSecured.tsx:37
+#, c-format
+msgid "Deleting"
+msgstr ""
+
+#: src/components/form/InputSecured.tsx:41
+#, c-format
+msgid "Changing"
+msgstr ""
+
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:87
+#, c-format
+msgid "Order ID"
+msgstr ""
+
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:101
+#, c-format
+msgid "Payment URL"
+msgstr ""
diff --git a/packages/merchant-backoffice-ui/src/i18n/en.po b/packages/merchant-backoffice-ui/src/i18n/en.po
index 6b35bd0ce..d8d0bae29 100644
--- a/packages/merchant-backoffice-ui/src/i18n/en.po
+++ b/packages/merchant-backoffice-ui/src/i18n/en.po
@@ -16,7 +16,7 @@
msgid ""
msgstr ""
"Project-Id-Version: Taler Wallet\n"
-"Report-Msgid-Bugs-To: \n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
"POT-Creation-Date: 2016-11-23 00:00+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
@@ -27,1031 +27,2715 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-#: src/ApplicationReadyRoutes.tsx:50 src/InstanceRoutes.tsx:118
-#: src/InstanceRoutes.tsx:299
+#: src/components/modal/index.tsx:71
#, c-format
-msgid "Access denied"
+msgid "Cancel"
msgstr ""
-#: src/ApplicationReadyRoutes.tsx:51 src/InstanceRoutes.tsx:118
-#: src/InstanceRoutes.tsx:300
+#: src/components/modal/index.tsx:79
#, c-format
-msgid "Check your token is valid"
+msgid "%1$s"
msgstr ""
-#: src/ApplicationReadyRoutes.tsx:72
+#: src/components/modal/index.tsx:84
#, c-format
-msgid "Couldn't access the server."
+msgid "Close"
msgstr ""
-#: src/ApplicationReadyRoutes.tsx:73
+#: src/components/modal/index.tsx:124
#, c-format
-msgid "Could not infer instance id from url %1$s"
+msgid "Continue"
msgstr ""
-#: src/InstanceRoutes.tsx:109
+#: src/components/modal/index.tsx:178
#, c-format
-msgid "HTTP status #%1$s: Server reported a problem"
+msgid "Clear"
msgstr ""
-#: src/InstanceRoutes.tsx:110
+#: src/components/modal/index.tsx:190
#, c-format
-msgid "Got message: \"%1$s\" from: %2$s"
+msgid "Confirm"
msgstr ""
-#: src/InstanceRoutes.tsx:127
+#: src/components/modal/index.tsx:296
#, c-format
-msgid "No default instance"
+msgid "is not the same as the current access token"
msgstr ""
-#: src/InstanceRoutes.tsx:128
+#: src/components/modal/index.tsx:299
#, c-format
-msgid ""
-"in order to use merchant backoffice, you should create the default instance"
+msgid "cannot be empty"
msgstr ""
-#: src/InstanceRoutes.tsx:288
+#: src/components/modal/index.tsx:301
#, c-format
-msgid "Server reported a problem: HTTP status #%1$s"
+msgid "cannot be the same as the old token"
msgstr ""
-#: src/InstanceRoutes.tsx:289
+#: src/components/modal/index.tsx:305
#, c-format
-msgid "Got message: %1$s from: %2$s"
+msgid "is not the same"
msgstr ""
-#: src/components/exception/login.tsx:46
+#: src/components/modal/index.tsx:315
#, c-format
-msgid "Login required"
+msgid "You are updating the access token from instance with id %1$s"
+msgstr ""
+
+#: src/components/modal/index.tsx:331
+#, c-format
+msgid "Old access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:332
+#, c-format
+msgid "access token currently in use"
+msgstr ""
+
+#: src/components/modal/index.tsx:338
+#, c-format
+msgid "New access token"
msgstr ""
-#: src/components/exception/login.tsx:49
+#: src/components/modal/index.tsx:339
+#, c-format
+msgid "next access token to be used"
+msgstr ""
+
+#: src/components/modal/index.tsx:344
+#, c-format
+msgid "Repeat access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:345
+#, c-format
+msgid "confirm the same access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:350
+#, c-format
+msgid "Clearing the access token will mean public access to the instance"
+msgstr ""
+
+#: src/components/modal/index.tsx:377
+#, c-format
+msgid "cannot be the same as the old access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:394
+#, c-format
+msgid "You are setting the access token for the new instance"
+msgstr ""
+
+#: src/components/modal/index.tsx:420
#, c-format
msgid ""
-"Please enter your auth token. Token should have \"secret-token:\" and start "
-"with Bearer or ApiKey"
+"With external authorization method no check will be done by the merchant "
+"backend"
msgstr ""
-#: src/components/exception/login.tsx:86 src/components/modal/index.tsx:53
-#: src/components/modal/index.tsx:75 src/paths/admin/create/CreatePage.tsx:115
-#: src/paths/instance/orders/create/CreatePage.tsx:325
-#: src/paths/instance/products/create/CreatePage.tsx:51
-#: src/paths/instance/products/list/Table.tsx:174
-#: src/paths/instance/products/list/Table.tsx:228
-#: src/paths/instance/products/update/UpdatePage.tsx:55
-#: src/paths/instance/transfers/create/CreatePage.tsx:89
-#: src/paths/instance/update/UpdatePage.tsx:134
+#: src/components/modal/index.tsx:436
#, c-format
-msgid "Confirm"
+msgid "Set external authorization"
msgstr ""
-#: src/components/form/InputArray.tsx:72
+#: src/components/modal/index.tsx:448
#, c-format
-msgid "The value %1$s is invalid for a payment url"
+msgid "Set access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:470
+#, c-format
+msgid "Operation in progress..."
+msgstr ""
+
+#: src/components/modal/index.tsx:479
+#, c-format
+msgid "The operation will be automatically canceled after %1$s seconds"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:80
+#, c-format
+msgid "Instances"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:93
+#, c-format
+msgid "Delete"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:99
+#, c-format
+msgid "add new instance"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:178
+#, c-format
+msgid "ID"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:181
+#, c-format
+msgid "Name"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:220
+#, c-format
+msgid "Edit"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:237
+#, c-format
+msgid "Purge"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:261
+#, c-format
+msgid "There is no instances yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:68
+#, c-format
+msgid "Only show active instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:71
+#, c-format
+msgid "Active"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:78
+#, c-format
+msgid "Only show deleted instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:81
+#, c-format
+msgid "Deleted"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:88
+#, c-format
+msgid "Show all instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:91
+#, c-format
+msgid "All"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:101
+#, c-format
+msgid "Instance \"%1$s\" (ID: %2$s) has been deleted"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:106
+#, c-format
+msgid "Failed to delete instance"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:124
+#, c-format
+msgid "Instance '%1$s' (ID: %2$s) has been disabled"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:129
+#, c-format
+msgid "Failed to purge instance"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:41
+#, c-format
+msgid "Pending KYC verification"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:66
+#, c-format
+msgid "Timed out"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:103
+#, c-format
+msgid "Exchange"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:106
+#, c-format
+msgid "Target account"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:109
+#, c-format
+msgid "KYC URL"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:144
+#, c-format
+msgid "Code"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:147
+#, c-format
+msgid "Http Status"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:177
+#, c-format
+msgid "No pending kyc verification!"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:123
+#, c-format
+msgid "change value to unknown date"
msgstr ""
-#: src/components/form/InputDate.tsx:67
-#: src/paths/instance/orders/list/index.tsx:123
+#: src/components/form/InputDate.tsx:124
#, c-format
-msgid "pick a date"
+msgid "change value to empty"
msgstr ""
-#: src/components/form/InputDate.tsx:81
+#: src/components/form/InputDate.tsx:131
#, c-format
msgid "clear"
msgstr ""
-#: src/components/form/InputDate.tsx:83
-#: src/paths/instance/transfers/list/Table.tsx:140
+#: src/components/form/InputDate.tsx:136
#, c-format
-msgid "never"
+msgid "change value to never"
msgstr ""
-#: src/components/form/InputImage.tsx:80
+#: src/components/form/InputDate.tsx:141
#, c-format
-msgid "Image should be smaller than 1 MB"
+msgid "never"
msgstr ""
-#: src/components/form/InputLocation.tsx:28
+#: src/components/form/InputLocation.tsx:29
#, c-format
msgid "Country"
msgstr ""
-#: src/components/form/InputLocation.tsx:30
-#: src/paths/admin/create/CreatePage.tsx:99
-#: src/paths/instance/transfers/list/Table.tsx:124
-#: src/paths/instance/update/UpdatePage.tsx:118
+#: src/components/form/InputLocation.tsx:33
#, c-format
msgid "Address"
msgstr ""
-#: src/components/form/InputLocation.tsx:34
+#: src/components/form/InputLocation.tsx:39
#, c-format
msgid "Building number"
msgstr ""
-#: src/components/form/InputLocation.tsx:35
+#: src/components/form/InputLocation.tsx:41
#, c-format
msgid "Building name"
msgstr ""
-#: src/components/form/InputLocation.tsx:36
+#: src/components/form/InputLocation.tsx:42
#, c-format
msgid "Street"
msgstr ""
-#: src/components/form/InputLocation.tsx:37
+#: src/components/form/InputLocation.tsx:43
#, c-format
msgid "Post code"
msgstr ""
-#: src/components/form/InputLocation.tsx:38
+#: src/components/form/InputLocation.tsx:44
#, c-format
msgid "Town location"
msgstr ""
-#: src/components/form/InputLocation.tsx:39
+#: src/components/form/InputLocation.tsx:45
#, c-format
msgid "Town"
msgstr ""
-#: src/components/form/InputLocation.tsx:40
+#: src/components/form/InputLocation.tsx:46
#, c-format
msgid "District"
msgstr ""
-#: src/components/form/InputLocation.tsx:41
+#: src/components/form/InputLocation.tsx:49
#, c-format
msgid "Country subdivision"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:59
+#: src/components/form/InputSearchProduct.tsx:66
#, c-format
msgid "Product id"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:60
-#: src/components/product/ProductForm.tsx:99
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:122
-#: src/paths/instance/orders/list/Table.tsx:227
-#: src/paths/instance/products/list/Table.tsx:86
+#: src/components/form/InputSearchProduct.tsx:69
#, c-format
msgid "Description"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:73
-#: src/components/form/InputTaxes.tsx:81
-#: src/paths/admin/create/CreatePage.tsx:87 src/paths/admin/list/Table.tsx:110
-#: src/paths/instance/details/DetailPage.tsx:76
-#: src/paths/instance/update/UpdatePage.tsx:106
+#: src/components/form/InputSearchProduct.tsx:94
#, c-format
-msgid "Name"
+msgid "Product"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:102
+#: src/components/form/InputSearchProduct.tsx:95
#, c-format
-msgid "loading..."
+msgid "search products by it's description or id"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:108
+#: src/components/form/InputSearchProduct.tsx:151
#, c-format
-msgid "no products found"
+msgid "no products found with that description"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:116
+#: src/components/product/InventoryProductForm.tsx:56
#, c-format
-msgid "no results"
+msgid "You must enter a valid product identifier."
msgstr ""
-#: src/components/form/InputSecured.tsx:33
+#: src/components/product/InventoryProductForm.tsx:64
#, c-format
-msgid "Deleting"
+msgid "Quantity must be greater than 0!"
msgstr ""
-#: src/components/form/InputSecured.tsx:34
+#: src/components/product/InventoryProductForm.tsx:76
#, c-format
-msgid "Changing"
+msgid ""
+"This quantity exceeds remaining stock. Currently, only %1$s units remain "
+"unreserved in stock."
msgstr ""
-#: src/components/form/InputSecured.tsx:60
+#: src/components/product/InventoryProductForm.tsx:109
#, c-format
-msgid "Manage token"
+msgid "Quantity"
msgstr ""
-#: src/components/form/InputSecured.tsx:83
+#: src/components/product/InventoryProductForm.tsx:110
#, c-format
-msgid "Update"
+msgid "how many products will be added"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:117
+#, c-format
+msgid "Add from inventory"
+msgstr ""
+
+#: src/components/form/InputImage.tsx:105
+#, c-format
+msgid "Image should be smaller than 1 MB"
+msgstr ""
+
+#: src/components/form/InputImage.tsx:110
+#, c-format
+msgid "Add"
msgstr ""
-#: src/components/form/InputSecured.tsx:100
-#: src/paths/instance/orders/create/CreatePage.tsx:252
-#: src/paths/instance/orders/create/CreatePage.tsx:273
+#: src/components/form/InputImage.tsx:115
#, c-format
msgid "Remove"
msgstr ""
-#: src/components/form/InputSecured.tsx:106 src/components/modal/index.tsx:52
-#: src/components/modal/index.tsx:73 src/paths/admin/create/CreatePage.tsx:114
-#: src/paths/instance/orders/create/CreatePage.tsx:324
-#: src/paths/instance/products/create/CreatePage.tsx:50
-#: src/paths/instance/products/list/Table.tsx:166
-#: src/paths/instance/products/list/Table.tsx:218
-#: src/paths/instance/products/update/UpdatePage.tsx:54
-#: src/paths/instance/transfers/create/CreatePage.tsx:88
-#: src/paths/instance/update/UpdatePage.tsx:133
+#: src/components/form/InputTaxes.tsx:113
#, c-format
-msgid "Cancel"
+msgid "No taxes configured for this product."
msgstr ""
-#: src/components/form/InputStock.tsx:91
+#: src/components/form/InputTaxes.tsx:119
#, c-format
-msgid "Manage stock"
+msgid "Amount"
msgstr ""
-#: src/components/form/InputStock.tsx:93
+#: src/components/form/InputTaxes.tsx:120
#, c-format
-msgid "Infinite"
+msgid ""
+"Taxes can be in currencies that differ from the main currency used by the "
+"merchant."
msgstr ""
-#: src/components/form/InputStock.tsx:105
+#: src/components/form/InputTaxes.tsx:122
#, c-format
-msgid "lost cannot be greater that current + incoming (max %1$s)"
+msgid ""
+"Enter currency and value separated with a colon, e.g. &quot;USD:2.3&quot;."
msgstr ""
-#: src/components/form/InputStock.tsx:111
+#: src/components/form/InputTaxes.tsx:131
#, c-format
-msgid "current stock will change from %1$s to %2$s"
+msgid "Legal name of the tax, e.g. VAT or import duties."
msgstr ""
-#: src/components/form/InputStock.tsx:112
+#: src/components/form/InputTaxes.tsx:137
#, c-format
-msgid "current stock will stay at %1$s"
+msgid "add tax to the tax list"
msgstr ""
-#: src/components/form/InputStock.tsx:129
-#: src/paths/instance/products/list/Table.tsx:204
+#: src/components/product/NonInventoryProductForm.tsx:72
#, c-format
-msgid "Incoming"
+msgid "describe and add a product that is not in the inventory list"
msgstr ""
-#: src/components/form/InputStock.tsx:130
-#: src/paths/instance/products/list/Table.tsx:205
+#: src/components/product/NonInventoryProductForm.tsx:75
#, c-format
-msgid "Lost"
+msgid "Add custom product"
msgstr ""
-#: src/components/form/InputStock.tsx:142
+#: src/components/product/NonInventoryProductForm.tsx:86
#, c-format
-msgid "Current"
+msgid "Complete information of the product"
msgstr ""
-#: src/components/form/InputStock.tsx:145
+#: src/components/product/NonInventoryProductForm.tsx:185
#, c-format
-msgid "without stock"
+msgid "Image"
msgstr ""
-#: src/components/form/InputStock.tsx:150
+#: src/components/product/NonInventoryProductForm.tsx:186
#, c-format
-msgid "Next restock"
+msgid "photo of the product"
msgstr ""
-#: src/components/form/InputStock.tsx:152
+#: src/components/product/NonInventoryProductForm.tsx:192
#, c-format
-msgid "Delivery address"
+msgid "full product description"
msgstr ""
-#: src/components/form/InputTaxes.tsx:73
+#: src/components/product/NonInventoryProductForm.tsx:196
#, c-format
-msgid "this product has no taxes"
+msgid "Unit"
msgstr ""
-#: src/components/form/InputTaxes.tsx:77
-#: src/paths/instance/orders/details/DetailPage.tsx:145
-#: src/paths/instance/orders/details/DetailPage.tsx:296
-#: src/paths/instance/orders/list/Table.tsx:116
-#: src/paths/instance/transfers/create/CreatePage.tsx:84
+#: src/components/product/NonInventoryProductForm.tsx:197
#, c-format
-msgid "Amount"
+msgid "name of the product unit"
msgstr ""
-#: src/components/form/InputTaxes.tsx:78
+#: src/components/product/NonInventoryProductForm.tsx:201
#, c-format
-msgid "currency and value separated with colon"
+msgid "Price"
msgstr ""
-#: src/components/form/InputTaxes.tsx:84
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:78
+#: src/components/product/NonInventoryProductForm.tsx:202
#, c-format
-msgid "Add"
+msgid "amount in the current currency"
msgstr ""
-#: src/components/menu/SideBar.tsx:53
+#: src/components/product/NonInventoryProductForm.tsx:211
#, c-format
-msgid "Instance"
+msgid "Taxes"
msgstr ""
-#: src/components/menu/SideBar.tsx:59
+#: src/components/product/ProductList.tsx:38
#, c-format
-msgid "Settings"
+msgid "image"
msgstr ""
-#: src/components/menu/SideBar.tsx:65
-#: src/paths/instance/orders/list/Table.tsx:60
+#: src/components/product/ProductList.tsx:41
#, c-format
-msgid "Orders"
+msgid "description"
msgstr ""
-#: src/components/menu/SideBar.tsx:71
-#: src/paths/instance/orders/create/CreatePage.tsx:258
-#: src/paths/instance/products/list/Table.tsx:48
+#: src/components/product/ProductList.tsx:44
#, c-format
-msgid "Products"
+msgid "quantity"
msgstr ""
-#: src/components/menu/SideBar.tsx:77
-#: src/paths/instance/transfers/list/Table.tsx:65
+#: src/components/product/ProductList.tsx:47
#, c-format
-msgid "Transfers"
+msgid "unit price"
msgstr ""
-#: src/components/menu/SideBar.tsx:87
+#: src/components/product/ProductList.tsx:50
#, c-format
-msgid "Connection"
+msgid "total price"
msgstr ""
-#: src/components/menu/SideBar.tsx:112 src/paths/admin/list/Table.tsx:57
+#: src/paths/instance/orders/create/CreatePage.tsx:153
#, c-format
-msgid "Instances"
+msgid "required"
msgstr ""
-#: src/components/menu/SideBar.tsx:116
+#: src/paths/instance/orders/create/CreatePage.tsx:157
#, c-format
-msgid "New"
+msgid "not valid"
msgstr ""
-#: src/components/menu/SideBar.tsx:122
+#: src/paths/instance/orders/create/CreatePage.tsx:159
#, c-format
-msgid "List"
+msgid "must be greater than 0"
msgstr ""
-#: src/components/menu/SideBar.tsx:129
+#: src/paths/instance/orders/create/CreatePage.tsx:164
#, c-format
-msgid "Log out"
+msgid "not a valid json"
msgstr ""
-#: src/components/modal/index.tsx:74
+#: src/paths/instance/orders/create/CreatePage.tsx:170
#, c-format
-msgid "Clear"
+msgid "should be in the future"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:173
+#, c-format
+msgid "refund deadline cannot be before pay deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:179
+#, c-format
+msgid "wire transfer deadline cannot be before refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:190
+#, c-format
+msgid "wire transfer deadline cannot be before pay deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:197
+#, c-format
+msgid "should have a refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:202
+#, c-format
+msgid "auto refund cannot be after refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:360
+#, c-format
+msgid "Manage products in order"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:369
+#, c-format
+msgid "Manage list of products in the order."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:391
+#, c-format
+msgid "Remove this product from the order."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:415
+#, c-format
+msgid "Total price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:417
+#, c-format
+msgid "total product price added up"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:430
+#, c-format
+msgid "Amount to be paid by the customer"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:436
+#, c-format
+msgid "Order price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:437
+#, c-format
+msgid "final order price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:444
+#, c-format
+msgid "Summary"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:445
+#, c-format
+msgid "Title of the order to be shown to the customer"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:450
+#, c-format
+msgid "Shipping and Fulfillment"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:455
+#, c-format
+msgid "Delivery date"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:456
+#, c-format
+msgid "Deadline for physical delivery assured by the merchant."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:461
+#, c-format
+msgid "Location"
msgstr ""
-#: src/components/modal/index.tsx:110 src/components/modal/index.tsx:111
+#: src/paths/instance/orders/create/CreatePage.tsx:462
#, c-format
-msgid "should be the same"
+msgid "address where the products will be delivered"
msgstr ""
-#: src/components/modal/index.tsx:111
+#: src/paths/instance/orders/create/CreatePage.tsx:469
#, c-format
-msgid "cannot be the same as before"
+msgid "Fulfillment URL"
msgstr ""
-#: src/components/modal/index.tsx:114
+#: src/paths/instance/orders/create/CreatePage.tsx:470
+#, c-format
+msgid "URL to which the user will be redirected after successful payment."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:476
+#, c-format
+msgid "Taler payment options"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:477
+#, c-format
+msgid "Override default Taler payment settings for this order"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:481
+#, c-format
+msgid "Payment deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:482
#, c-format
msgid ""
-"You are updating the authorization token from instance %1$s with id %2$s"
+"Deadline for the customer to pay for the offer before it expires. Inventory "
+"products will be reserved until this deadline."
msgstr ""
-#: src/components/modal/index.tsx:124
+#: src/paths/instance/orders/create/CreatePage.tsx:486
#, c-format
-msgid "Old token"
+msgid "Refund deadline"
msgstr ""
-#: src/components/modal/index.tsx:125
+#: src/paths/instance/orders/create/CreatePage.tsx:487
#, c-format
-msgid "New token"
+msgid "Time until which the order can be refunded by the merchant."
msgstr ""
-#: src/components/modal/index.tsx:127
+#: src/paths/instance/orders/create/CreatePage.tsx:491
#, c-format
-msgid "Clearing the auth token will mean public access to the instance"
+msgid "Wire transfer deadline"
msgstr ""
-#: src/components/product/ProductForm.tsx:96
-#: src/paths/admin/create/CreatePage.tsx:85 src/paths/admin/list/Table.tsx:109
-#: src/paths/instance/transfers/list/Table.tsx:122
+#: src/paths/instance/orders/create/CreatePage.tsx:492
#, c-format
-msgid "ID"
+msgid "Deadline for the exchange to make the wire transfer."
msgstr ""
-#: src/components/product/ProductForm.tsx:98
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:121
-#: src/paths/instance/products/list/Table.tsx:85
+#: src/paths/instance/orders/create/CreatePage.tsx:496
#, c-format
-msgid "Image"
+msgid "Auto-refund deadline"
msgstr ""
-#: src/components/product/ProductForm.tsx:100
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:123
+#: src/paths/instance/orders/create/CreatePage.tsx:497
#, c-format
-msgid "Unit"
+msgid ""
+"Time until which the wallet will automatically check for refunds without "
+"user interaction."
msgstr ""
-#: src/components/product/ProductForm.tsx:101
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:124
-#: src/paths/instance/products/list/Table.tsx:162
-#: src/paths/instance/products/list/Table.tsx:214
+#: src/paths/instance/orders/create/CreatePage.tsx:502
#, c-format
-msgid "Price"
+msgid "Maximum deposit fee"
msgstr ""
-#: src/components/product/ProductForm.tsx:103
-#: src/paths/instance/products/list/Table.tsx:90
+#: src/paths/instance/orders/create/CreatePage.tsx:503
#, c-format
-msgid "Stock"
+msgid ""
+"Maximum deposit fees the merchant is willing to cover for this order. Higher "
+"deposit fees must be covered in full by the consumer."
msgstr ""
-#: src/components/product/ProductForm.tsx:105
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:128
-#: src/paths/instance/products/list/Table.tsx:88
+#: src/paths/instance/orders/create/CreatePage.tsx:507
#, c-format
-msgid "Taxes"
+msgid "Maximum wire fee"
msgstr ""
-#: src/index.tsx:75
+#: src/paths/instance/orders/create/CreatePage.tsx:508
#, c-format
-msgid "Server not found"
+msgid ""
+"Maximum aggregate wire fees the merchant is willing to cover for this order. "
+"Wire fees exceeding this amount are to be covered by the customers."
msgstr ""
-#: src/index.tsx:85
+#: src/paths/instance/orders/create/CreatePage.tsx:512
#, c-format
-msgid "Couldn't access the server"
+msgid "Wire fee amortization"
msgstr ""
-#: src/index.tsx:87 src/index.tsx:99
+#: src/paths/instance/orders/create/CreatePage.tsx:513
#, c-format
-msgid "Got message %1$s from %2$s"
+msgid ""
+"Factor by which wire fees exceeding the above threshold are divided to "
+"determine the share of excess wire fees to be paid explicitly by the "
+"consumer."
msgstr ""
-#: src/index.tsx:97
+#: src/paths/instance/orders/create/CreatePage.tsx:517
#, c-format
-msgid "Unexpected Error"
+msgid "Create token"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:89
-#: src/paths/instance/update/UpdatePage.tsx:108
+#: src/paths/instance/orders/create/CreatePage.tsx:518
#, c-format
-msgid "Auth token"
+msgid ""
+"Uncheck this option if the merchant backend generated an order ID with "
+"enough entropy to prevent adversarial claims."
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:91
-#: src/paths/instance/details/DetailPage.tsx:77
-#: src/paths/instance/update/UpdatePage.tsx:110
+#: src/paths/instance/orders/create/CreatePage.tsx:522
#, c-format
-msgid "Account address"
+msgid "Minimum age required"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:93
-#: src/paths/instance/update/UpdatePage.tsx:112
+#: src/paths/instance/orders/create/CreatePage.tsx:523
#, c-format
-msgid "Default max deposit fee"
+msgid ""
+"Any value greater than 0 will limit the coins able be used to pay this "
+"contract. If empty the age restriction will be defined by the products"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:95
-#: src/paths/instance/update/UpdatePage.tsx:114
+#: src/paths/instance/orders/create/CreatePage.tsx:526
#, c-format
-msgid "Default max wire fee"
+msgid "Min age defined by the producs is %1$s"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:97
-#: src/paths/instance/update/UpdatePage.tsx:116
+#: src/paths/instance/orders/create/CreatePage.tsx:534
#, c-format
-msgid "Default wire fee amortization"
+msgid "Additional information"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:103
-#: src/paths/instance/update/UpdatePage.tsx:122
+#: src/paths/instance/orders/create/CreatePage.tsx:535
#, c-format
-msgid "Jurisdiction"
+msgid "Custom information to be included in the contract for this order."
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:107
-#: src/paths/instance/update/UpdatePage.tsx:126
+#: src/paths/instance/orders/create/CreatePage.tsx:541
#, c-format
-msgid "Default pay delay"
+msgid "You must enter a value in JavaScript Object Notation (JSON)."
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:109
-#: src/paths/instance/update/UpdatePage.tsx:128
+#: src/components/picker/DurationPicker.tsx:55
#, c-format
-msgid "Default wire transfer delay"
+msgid "days"
msgstr ""
-#: src/paths/admin/create/index.tsx:58
+#: src/components/picker/DurationPicker.tsx:65
#, c-format
-msgid "could not create instance"
+msgid "hours"
msgstr ""
-#: src/paths/admin/list/Table.tsx:63 src/paths/admin/list/Table.tsx:131
-#: src/paths/instance/transfers/list/Table.tsx:71
+#: src/components/picker/DurationPicker.tsx:76
#, c-format
-msgid "Delete"
+msgid "minutes"
msgstr ""
-#: src/paths/admin/list/Table.tsx:128
+#: src/components/picker/DurationPicker.tsx:87
#, c-format
-msgid "Edit"
+msgid "seconds"
msgstr ""
-#: src/paths/admin/list/Table.tsx:149
-#: src/paths/instance/products/list/Table.tsx:245
+#: src/components/form/InputDuration.tsx:53
#, c-format
-msgid "There is no instances yet, add more pressing the + sign"
+msgid "forever"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:237
+#: src/components/form/InputDuration.tsx:62
#, c-format
-msgid "Inventory products"
+msgid "%1$sM"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:286
+#: src/components/form/InputDuration.tsx:64
#, c-format
-msgid "Total price"
+msgid "%1$sY"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:287
+#: src/components/form/InputDuration.tsx:66
#, c-format
-msgid "Total tax"
+msgid "%1$sd"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:289
-#: src/paths/instance/orders/create/CreatePage.tsx:297
+#: src/components/form/InputDuration.tsx:68
#, c-format
-msgid "Order price"
+msgid "%1$sh"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:295
+#: src/components/form/InputDuration.tsx:70
#, c-format
-msgid "Net"
+msgid "%1$smin"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:300
-#: src/paths/instance/orders/details/DetailPage.tsx:144
-#: src/paths/instance/orders/details/DetailPage.tsx:295
-#: src/paths/instance/orders/list/Table.tsx:117
+#: src/components/form/InputDuration.tsx:72
#, c-format
-msgid "Summary"
+msgid "%1$ssec"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:302
+#: src/paths/instance/orders/list/Table.tsx:75
#, c-format
-msgid "Payments options"
+msgid "Orders"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:303
+#: src/paths/instance/orders/list/Table.tsx:81
#, c-format
-msgid "Auto refund deadline"
+msgid "create order"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:304
+#: src/paths/instance/orders/list/Table.tsx:147
#, c-format
-msgid "Refund deadline"
+msgid "load newer orders"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:305
+#: src/paths/instance/orders/list/Table.tsx:154
#, c-format
-msgid "Pay deadline"
+msgid "Date"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:307
+#: src/paths/instance/orders/list/Table.tsx:200
#, c-format
-msgid "Delivery date"
+msgid "Refund"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:308
+#: src/paths/instance/orders/list/Table.tsx:209
#, c-format
-msgid "Location"
+msgid "copy url"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:225
+#, c-format
+msgid "load older orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:242
+#, c-format
+msgid "No orders have been found matching your query!"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:288
+#, c-format
+msgid "duplicated"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:299
+#, c-format
+msgid "invalid format"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:301
+#, c-format
+msgid "this value exceed the refundable amount"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:346
+#, c-format
+msgid "date"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:349
+#, c-format
+msgid "amount"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:352
+#, c-format
+msgid "reason"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:389
+#, c-format
+msgid "amount to be refunded"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:391
+#, c-format
+msgid "Max refundable:"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:396
+#, c-format
+msgid "Reason"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:397
+#, c-format
+msgid "Choose one..."
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:399
+#, c-format
+msgid "requested by the customer"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:400
+#, c-format
+msgid "other"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:403
+#, c-format
+msgid "why this order is being refunded"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:409
+#, c-format
+msgid "more information to give context"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:62
+#, c-format
+msgid "Contract Terms"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:68
+#, c-format
+msgid "human-readable description of the whole purchase"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:312
+#: src/paths/instance/orders/details/DetailPage.tsx:74
+#, c-format
+msgid "total price for the transaction"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:81
+#, c-format
+msgid "URL for this purchase"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:87
#, c-format
msgid "Max fee"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:313
+#: src/paths/instance/orders/details/DetailPage.tsx:88
+#, c-format
+msgid "maximum total deposit fee accepted by the merchant for this contract"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:93
#, c-format
msgid "Max wire fee"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:314
+#: src/paths/instance/orders/details/DetailPage.tsx:94
#, c-format
-msgid "Wire fee amortization"
+msgid "maximum wire fee accepted by the merchant"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:315
+#: src/paths/instance/orders/details/DetailPage.tsx:100
#, c-format
-msgid "Fullfilment url"
+msgid ""
+"over how many customer transactions does the merchant expect to amortize "
+"wire fees on average"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:318
+#: src/paths/instance/orders/details/DetailPage.tsx:105
#, c-format
-msgid "Extra information"
+msgid "Created at"
msgstr ""
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:44
+#: src/paths/instance/orders/details/DetailPage.tsx:106
#, c-format
-msgid "select a product first"
+msgid "time when this contract was generated"
msgstr ""
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:51
+#: src/paths/instance/orders/details/DetailPage.tsx:112
#, c-format
-msgid "should be greater than 0"
+msgid "after this deadline has passed no refunds will be accepted"
msgstr ""
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:58
+#: src/paths/instance/orders/details/DetailPage.tsx:118
#, c-format
msgid ""
-"cannot be greater than current stock and quantity previously added. max: %1$s"
+"after this deadline, the merchant won't accept payments for the contract"
msgstr ""
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:64
+#: src/paths/instance/orders/details/DetailPage.tsx:124
#, c-format
-msgid "cannot be greater than current stock %1$s"
+msgid "transfer deadline for the exchange"
msgstr ""
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:76
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:126
+#: src/paths/instance/orders/details/DetailPage.tsx:130
#, c-format
-msgid "Quantity"
+msgid "time indicating when the order should be delivered"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:92
-#: src/paths/instance/orders/details/DetailPage.tsx:235
-#: src/paths/instance/orders/details/DetailPage.tsx:333
+#: src/paths/instance/orders/details/DetailPage.tsx:136
#, c-format
-msgid "Order"
+msgid "where the order will be delivered"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:93
+#: src/paths/instance/orders/details/DetailPage.tsx:144
#, c-format
-msgid "claimed"
+msgid "Auto-refund delay"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:110
-#: src/paths/instance/orders/details/DetailPage.tsx:261
-#: src/paths/instance/orders/list/Table.tsx:136
+#: src/paths/instance/orders/details/DetailPage.tsx:145
#, c-format
-msgid "copy url"
+msgid ""
+"how long the wallet should try to get an automatic refund for the purchase"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:126
-#: src/paths/instance/orders/details/DetailPage.tsx:349
+#: src/paths/instance/orders/details/DetailPage.tsx:150
#, c-format
-msgid "pay at"
+msgid "Extra info"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:127
-#: src/paths/instance/orders/details/DetailPage.tsx:350
+#: src/paths/instance/orders/details/DetailPage.tsx:151
#, c-format
-msgid "created at"
+msgid "extra data that is only interpreted by the merchant frontend"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:219
+#, c-format
+msgid "Order"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:221
+#, c-format
+msgid "claimed"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:138
-#: src/paths/instance/orders/details/DetailPage.tsx:289
+#: src/paths/instance/orders/details/DetailPage.tsx:247
+#, c-format
+msgid "claimed at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:265
#, c-format
msgid "Timeline"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:142
-#: src/paths/instance/orders/details/DetailPage.tsx:293
+#: src/paths/instance/orders/details/DetailPage.tsx:271
#, c-format
msgid "Payment details"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:146
-#: src/paths/instance/orders/details/DetailPage.tsx:299
-#: src/paths/instance/orders/details/DetailPage.tsx:363
+#: src/paths/instance/orders/details/DetailPage.tsx:291
#, c-format
msgid "Order status"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:156
-#: src/paths/instance/orders/details/DetailPage.tsx:308
+#: src/paths/instance/orders/details/DetailPage.tsx:301
#, c-format
msgid "Product list"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:236
+#: src/paths/instance/orders/details/DetailPage.tsx:451
#, c-format
msgid "paid"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:238
+#: src/paths/instance/orders/details/DetailPage.tsx:455
#, c-format
msgid "wired"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:241
+#: src/paths/instance/orders/details/DetailPage.tsx:460
#, c-format
msgid "refunded"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:258
+#: src/paths/instance/orders/details/DetailPage.tsx:480
+#, c-format
+msgid "refund order"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:481
+#, c-format
+msgid "not refundable"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:489
#, c-format
msgid "refund"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:297
+#: src/paths/instance/orders/details/DetailPage.tsx:553
#, c-format
msgid "Refunded amount"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:298
+#: src/paths/instance/orders/details/DetailPage.tsx:560
#, c-format
-msgid "Deposit total"
+msgid "Refund taken"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:336
+#: src/paths/instance/orders/details/DetailPage.tsx:570
+#, c-format
+msgid "Status URL"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:583
+#, c-format
+msgid "Refund URI"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:636
#, c-format
msgid "unpaid"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:364
+#: src/paths/instance/orders/details/DetailPage.tsx:654
+#, c-format
+msgid "pay at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:666
+#, c-format
+msgid "created at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:707
#, c-format
msgid "Order status URL"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:365
+#: src/paths/instance/orders/details/DetailPage.tsx:711
#, c-format
-msgid "Pay URI"
+msgid "Payment URI"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:383
+#: src/paths/instance/orders/details/DetailPage.tsx:740
#, c-format
msgid ""
"Unknown order status. This is an error, please contact the administrator."
msgstr ""
-#: src/paths/instance/orders/details/index.tsx:56
-#: src/paths/instance/orders/list/index.tsx:147
+#: src/paths/instance/orders/details/DetailPage.tsx:767
+#, c-format
+msgid "Back"
+msgstr ""
+
+#: src/paths/instance/orders/details/index.tsx:79
#, c-format
msgid "refund created successfully"
msgstr ""
-#: src/paths/instance/orders/details/index.tsx:59
-#: src/paths/instance/orders/list/index.tsx:150
+#: src/paths/instance/orders/details/index.tsx:85
#, c-format
msgid "could not create the refund"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:111
+#: src/paths/instance/orders/list/ListPage.tsx:78
#, c-format
-msgid "load newer orders"
+msgid "select date to show nearby orders"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:115
+#: src/paths/instance/orders/list/ListPage.tsx:94
#, c-format
-msgid "Date"
+msgid "order id"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:131
-#: src/paths/instance/orders/list/Table.tsx:223
+#: src/paths/instance/orders/list/ListPage.tsx:100
#, c-format
-msgid "Refund"
+msgid "jump to order with the given order ID"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:145
+#: src/paths/instance/orders/list/ListPage.tsx:122
#, c-format
-msgid "load older orders"
+msgid "remove all filters"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:154
+#: src/paths/instance/orders/list/ListPage.tsx:132
#, c-format
-msgid "No orders has been found"
+msgid "only show paid orders"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:202
+#: src/paths/instance/orders/list/ListPage.tsx:135
#, c-format
-msgid "date"
+msgid "Paid"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:203
+#: src/paths/instance/orders/list/ListPage.tsx:142
#, c-format
-msgid "amount"
+msgid "only show orders with refunds"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:204
+#: src/paths/instance/orders/list/ListPage.tsx:145
#, c-format
-msgid "reason"
+msgid "Refunded"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:224
+#: src/paths/instance/orders/list/ListPage.tsx:152
#, c-format
-msgid "Max refundable:"
+msgid ""
+"only show orders where customers paid, but wire payments from payment "
+"provider are still pending"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:226
+#: src/paths/instance/orders/list/ListPage.tsx:155
#, c-format
-msgid "Reason"
+msgid "Not wired"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:226
+#: src/paths/instance/orders/list/ListPage.tsx:170
#, c-format
-msgid "duplicated"
+msgid "clear date filter"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:226
+#: src/paths/instance/orders/list/ListPage.tsx:184
#, c-format
-msgid "requested by the customer"
+msgid "date (YYYY/MM/DD)"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:226
+#: src/paths/instance/orders/list/index.tsx:103
#, c-format
-msgid "other"
+msgid "Enter an order id"
msgstr ""
-#: src/paths/instance/orders/list/index.tsx:91
+#: src/paths/instance/orders/list/index.tsx:111
#, c-format
-msgid "go to order id"
+msgid "order not found"
msgstr ""
-#: src/paths/instance/orders/list/index.tsx:107
+#: src/paths/instance/orders/list/index.tsx:178
#, c-format
-msgid "Paid"
+msgid "could not get the order to refund"
msgstr ""
-#: src/paths/instance/orders/list/index.tsx:108
+#: src/components/exception/AsyncButton.tsx:43
#, c-format
-msgid "Refunded"
+msgid "Loading..."
msgstr ""
-#: src/paths/instance/orders/list/index.tsx:109
+#: src/components/form/InputStock.tsx:99
#, c-format
-msgid "Not wired"
+msgid ""
+"click here to configure the stock of the product, leave it as is and the "
+"backend will not control stock"
msgstr ""
-#: src/paths/instance/orders/list/index.tsx:110
+#: src/components/form/InputStock.tsx:109
#, c-format
-msgid "All"
+msgid "Manage stock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:115
+#, c-format
+msgid "this product has been configured without stock control"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:119
+#, c-format
+msgid "Infinite"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:136
+#, c-format
+msgid "lost cannot be greater than current and incoming (max %1$s)"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:176
+#, c-format
+msgid "Incoming"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:177
+#, c-format
+msgid "Lost"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:192
+#, c-format
+msgid "Current"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:196
+#, c-format
+msgid "remove stock control for this product"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:202
+#, c-format
+msgid "without stock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:211
+#, c-format
+msgid "Next restock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:217
+#, c-format
+msgid "Delivery address"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:133
+#, c-format
+msgid "product identification to use in URLs (for internal use only)"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:139
+#, c-format
+msgid "illustration of the product for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:145
+#, c-format
+msgid "product description for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:149
+#, c-format
+msgid "Age restricted"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:150
+#, c-format
+msgid "is this product restricted for customer below certain age?"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:155
+#, c-format
+msgid ""
+"unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 "
+"items, 5 meters) for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:160
+#, c-format
+msgid ""
+"sale price for customers, including taxes, for above units of the product"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:164
+#, c-format
+msgid "Stock"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:166
+#, c-format
+msgid ""
+"product inventory for products with finite supply (for internal use only)"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:171
+#, c-format
+msgid "taxes included in the product price, exposed to customers"
+msgstr ""
+
+#: src/paths/instance/products/create/CreatePage.tsx:66
+#, c-format
+msgid "Need to complete marked fields"
msgstr ""
-#: src/paths/instance/products/create/index.tsx:48
-#: src/paths/instance/products/update/index.tsx:64
+#: src/paths/instance/products/create/index.tsx:51
#, c-format
msgid "could not create product"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:87
+#: src/paths/instance/products/list/Table.tsx:68
+#, c-format
+msgid "Products"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:73
+#, c-format
+msgid "add product to inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:137
#, c-format
msgid "Sell"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:89
+#: src/paths/instance/products/list/Table.tsx:143
#, c-format
msgid "Profit"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:91
+#: src/paths/instance/products/list/Table.tsx:149
#, c-format
msgid "Sold"
msgstr ""
-#: src/paths/instance/products/list/index.tsx:59
+#: src/paths/instance/products/list/Table.tsx:210
+#, c-format
+msgid "free"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:248
+#, c-format
+msgid "go to product update page"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:255
+#, c-format
+msgid "Update"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:260
+#, c-format
+msgid "remove this product from the database"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:331
+#, c-format
+msgid "update the product with new price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:341
+#, c-format
+msgid "update product with new price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:399
+#, c-format
+msgid "add more elements to the inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:404
+#, c-format
+msgid "report elements lost in the inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:409
+#, c-format
+msgid "new price for the product"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:421
+#, c-format
+msgid "the are value with errors"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:422
+#, c-format
+msgid "update product with new stock and price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:463
+#, c-format
+msgid "There is no products yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:86
#, c-format
msgid "product updated successfully"
msgstr ""
-#: src/paths/instance/products/list/index.tsx:62
+#: src/paths/instance/products/list/index.tsx:92
#, c-format
msgid "could not update the product"
msgstr ""
-#: src/paths/instance/products/list/index.tsx:70
+#: src/paths/instance/products/list/index.tsx:103
#, c-format
msgid "product delete successfully"
msgstr ""
-#: src/paths/instance/products/list/index.tsx:73
+#: src/paths/instance/products/list/index.tsx:109
#, c-format
msgid "could not delete the product"
msgstr ""
-#: src/paths/instance/tips/list/Table.tsx:59
+#: src/paths/instance/products/update/UpdatePage.tsx:56
+#, c-format
+msgid "Product id:"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:95
+#, c-format
+msgid ""
+"To complete the setup of the reserve, you must now initiate a wire transfer "
+"using the given wire transfer subject and crediting the specified amount to "
+"the indicated account of the exchange."
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:102
+#, c-format
+msgid "If your system supports RFC 8905, you can do this by opening this URI:"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:83
+#, c-format
+msgid "it should be greater than 0"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:88
+#, c-format
+msgid "must be a valid URL"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:107
+#, c-format
+msgid "Initial balance"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:108
+#, c-format
+msgid "balance prior to deposit"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:112
+#, c-format
+msgid "Exchange URL"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:113
+#, c-format
+msgid "URL of exchange"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:148
+#, c-format
+msgid "Next"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:186
+#, c-format
+msgid "Wire method"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:187
+#, c-format
+msgid "method to use for wire transfer"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:189
+#, c-format
+msgid "Select one wire method"
+msgstr ""
+
+#: src/paths/instance/reserves/create/index.tsx:62
+#, c-format
+msgid "could not create reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:77
+#, c-format
+msgid "Valid until"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:82
+#, c-format
+msgid "Created balance"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:99
+#, c-format
+msgid "Exchange balance"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:104
+#, c-format
+msgid "Picked up"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:109
+#, c-format
+msgid "Committed"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:116
+#, c-format
+msgid "Account address"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:119
+#, c-format
+msgid "Subject"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:130
#, c-format
msgid "Tips"
msgstr ""
-#: src/paths/instance/tips/list/Table.tsx:111
+#: src/paths/instance/reserves/details/DetailPage.tsx:193
#, c-format
-msgid "Committed amount"
+msgid "No tips has been authorized from this reserve"
msgstr ""
-#: src/paths/instance/tips/list/Table.tsx:112
+#: src/paths/instance/reserves/details/DetailPage.tsx:213
#, c-format
-msgid "Exchange initial amount"
+msgid "Authorized"
msgstr ""
-#: src/paths/instance/tips/list/Table.tsx:113
+#: src/paths/instance/reserves/details/DetailPage.tsx:222
#, c-format
-msgid "Merchant initial amount"
+msgid "Expiration"
msgstr ""
-#: src/paths/instance/tips/list/Table.tsx:148
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:108
#, c-format
-msgid "There is no tips yet, add more pressing the + sign"
+msgid "amount of tip"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:50
-#: src/paths/instance/transfers/create/CreatePage.tsx:54
-#: src/paths/instance/transfers/create/CreatePage.tsx:55
-#: src/paths/instance/transfers/create/CreatePage.tsx:56
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:112
#, c-format
-msgid "cannot be empty"
+msgid "Justification"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:114
+#, c-format
+msgid "reason for the tip"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:118
+#, c-format
+msgid "URL after tip"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:119
+#, c-format
+msgid "URL to visit after tip payment"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:65
+#, c-format
+msgid "Reserves not yet funded"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:89
+#, c-format
+msgid "Reserves ready"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:95
+#, c-format
+msgid "add new reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:143
+#, c-format
+msgid "Expires at"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:146
+#, c-format
+msgid "Initial"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:202
+#, c-format
+msgid "delete selected reserve from the database"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:210
+#, c-format
+msgid "authorize new tip from selected reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:237
+#, c-format
+msgid ""
+"There is no ready reserves yet, add more pressing the + sign or fund them"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:264
+#, c-format
+msgid "Expected Balance"
+msgstr ""
+
+#: src/paths/instance/reserves/list/index.tsx:110
+#, c-format
+msgid "could not create the tip"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:77
+#, c-format
+msgid "should not be empty"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:93
+#, c-format
+msgid "should be greater that 0"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:96
+#, c-format
+msgid "can't be empty"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:100
+#, c-format
+msgid "to short"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:108
+#, c-format
+msgid "just letters and numbers from 2 to 7"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:110
+#, c-format
+msgid "size of the key should be 32"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:137
+#, c-format
+msgid "Identifier"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:138
+#, c-format
+msgid "Name of the template in URLs."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:144
+#, c-format
+msgid "Describe what this template stands for"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:149
+#, c-format
+msgid "Fixed summary"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:150
+#, c-format
+msgid "If specified, this template will create order with the same summary"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:154
+#, c-format
+msgid "Fixed price"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:155
+#, c-format
+msgid "If specified, this template will create order with the same price"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:159
+#, c-format
+msgid "Minimum age"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:161
+#, c-format
+msgid "Is this contract restricted to some age?"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:165
+#, c-format
+msgid "Payment timeout"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:167
+#, c-format
+msgid ""
+"How much time has the customer to complete the payment once the order was "
+"created."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:171
+#, c-format
+msgid "Verification algorithm"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:172
+#, c-format
+msgid "Algorithm to use to verify transaction in offline mode"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:180
+#, c-format
+msgid "Point-of-sale key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:182
+#, c-format
+msgid "Useful to validate the purchase"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:196
+#, c-format
+msgid "generate random secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:203
+#, c-format
+msgid "random"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:208
+#, c-format
+msgid "show secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:209
+#, c-format
+msgid "hide secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:216
+#, c-format
+msgid "hide"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:218
+#, c-format
+msgid "show"
+msgstr ""
+
+#: src/paths/instance/templates/create/index.tsx:52
+#, c-format
+msgid "could not inform template"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:54
+#, c-format
+msgid "Amount is required"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:58
+#, c-format
+msgid "Order summary is required"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:86
+#, c-format
+msgid "New order for template"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:108
+#, c-format
+msgid "Amount of the order"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:113
+#, c-format
+msgid "Order summary"
+msgstr ""
+
+#: src/paths/instance/templates/use/index.tsx:92
+#, c-format
+msgid "could not create order from template"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:131
+#, c-format
+msgid ""
+"Here you can specify a default value for fields that are not fixed. Default "
+"values can be edited by the customer before the payment."
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:148
+#, c-format
+msgid "Fixed amount"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:51
+#: src/paths/instance/templates/qr/QrPage.tsx:149
#, c-format
-msgid "check the id, doest look valid"
+msgid "Default amount"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:52
+#: src/paths/instance/templates/qr/QrPage.tsx:161
+#, c-format
+msgid "Default summary"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:177
+#, c-format
+msgid "Print"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:184
+#, c-format
+msgid "Setup TOTP"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:65
+#, c-format
+msgid "Templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:70
+#, c-format
+msgid "add new templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:142
+#, c-format
+msgid "load more templates before the first one"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:146
+#, c-format
+msgid "load newer templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:181
+#, c-format
+msgid "delete selected templates from the database"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:188
+#, c-format
+msgid "use template to create new order"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:195
+#, c-format
+msgid "create qr code for the template"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:210
+#, c-format
+msgid "load more templates after the last one"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:214
+#, c-format
+msgid "load older templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:231
+#, c-format
+msgid "There is no templates yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/templates/list/index.tsx:104
+#, c-format
+msgid "template delete successfully"
+msgstr ""
+
+#: src/paths/instance/templates/list/index.tsx:110
+#, c-format
+msgid "could not delete the template"
+msgstr ""
+
+#: src/paths/instance/templates/update/index.tsx:90
+#, c-format
+msgid "could not update template"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:57
+#, c-format
+msgid "should be one of '%1$s'"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:85
+#, c-format
+msgid "Webhook ID to use"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:89
+#, c-format
+msgid "Event"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:90
+#, c-format
+msgid "The event of the webhook: why the webhook is used"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:94
+#, c-format
+msgid "Method"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:95
+#, c-format
+msgid "Method used by the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:99
+#, c-format
+msgid "URL"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:100
+#, c-format
+msgid "URL of the webhook where the customer will be redirected"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:104
+#, c-format
+msgid "Header"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:106
+#, c-format
+msgid "Header template of the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:111
+#, c-format
+msgid "Body"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:112
+#, c-format
+msgid "Body template by the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:61
+#, c-format
+msgid "Webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:66
+#, c-format
+msgid "add new webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:137
+#, c-format
+msgid "load more webhooks before the first one"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:141
+#, c-format
+msgid "load newer webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:151
+#, c-format
+msgid "Event type"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:176
+#, c-format
+msgid "delete selected webhook from the database"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:198
+#, c-format
+msgid "load more webhooks after the last one"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:202
+#, c-format
+msgid "load older webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:219
+#, c-format
+msgid "There is no webhooks yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/index.tsx:94
+#, c-format
+msgid "webhook delete successfully"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/index.tsx:100
+#, c-format
+msgid "could not delete the webhook"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:63
+#, c-format
+msgid "check the id, does not look valid"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:65
#, c-format
msgid "should have 52 characters, current %1$s"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:57
+#: src/paths/instance/transfers/create/CreatePage.tsx:72
#, c-format
msgid "URL doesn't have the right format"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:74
+#: src/paths/instance/transfers/create/CreatePage.tsx:98
#, c-format
-msgid "Transfer ID"
+msgid "Credited bank account"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:76
+#: src/paths/instance/transfers/create/CreatePage.tsx:100
#, c-format
-msgid "Account Address"
+msgid "Select one account"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:82
-#: src/paths/instance/transfers/list/Table.tsx:125
+#: src/paths/instance/transfers/create/CreatePage.tsx:101
#, c-format
-msgid "Exchange URL"
+msgid "Bank account of the merchant where the payment was received"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:105
+#, c-format
+msgid "Wire transfer ID"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:107
+#, c-format
+msgid ""
+"unique identifier of the wire transfer used by the exchange, must be 52 "
+"characters long"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:112
+#, c-format
+msgid ""
+"Base URL of the exchange that made the transfer, should have been in the "
+"wire transfer subject"
msgstr ""
-#: src/paths/instance/transfers/create/index.tsx:49
+#: src/paths/instance/transfers/create/CreatePage.tsx:117
+#, c-format
+msgid "Amount credited"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:118
+#, c-format
+msgid "Actual amount that was wired to the merchant's bank account"
+msgstr ""
+
+#: src/paths/instance/transfers/create/index.tsx:58
#, c-format
msgid "could not inform transfer"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:118
+#: src/paths/instance/transfers/list/Table.tsx:61
+#, c-format
+msgid "Transfers"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:66
+#, c-format
+msgid "add new transfer"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:129
+#, c-format
+msgid "load more transfers before the first one"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:133
#, c-format
msgid "load newer transfers"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:123
+#: src/paths/instance/transfers/list/Table.tsx:143
#, c-format
msgid "Credit"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:126
+#: src/paths/instance/transfers/list/Table.tsx:152
#, c-format
msgid "Confirmed"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:127
-#: src/paths/instance/transfers/list/index.tsx:60
+#: src/paths/instance/transfers/list/Table.tsx:155
#, c-format
msgid "Verified"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:128
+#: src/paths/instance/transfers/list/Table.tsx:158
#, c-format
msgid "Executed at"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:138
-#: src/paths/instance/transfers/list/Table.tsx:139
+#: src/paths/instance/transfers/list/Table.tsx:171
#, c-format
msgid "yes"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:138
-#: src/paths/instance/transfers/list/Table.tsx:139
+#: src/paths/instance/transfers/list/Table.tsx:171
#, c-format
msgid "no"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:140
+#: src/paths/instance/transfers/list/Table.tsx:181
#, c-format
msgid "unknown"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:145
+#: src/paths/instance/transfers/list/Table.tsx:187
+#, c-format
+msgid "delete selected transfer from the database"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:202
+#, c-format
+msgid "load more transfer after the last one"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:206
#, c-format
msgid "load older transfers"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:154
+#: src/paths/instance/transfers/list/Table.tsx:223
#, c-format
msgid "There is no transfer yet, add more pressing the + sign"
msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:79
+#, c-format
+msgid "filter by account address"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:100
+#, c-format
+msgid "only show wire transfers confirmed by the merchant"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:110
+#, c-format
+msgid "only show wire transfers claimed by the exchange"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:113
+#, c-format
+msgid "Unverified"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:69
+#, c-format
+msgid "is not valid"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:94
+#, c-format
+msgid "is not a number"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:96
+#, c-format
+msgid "must be 1 or greater"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:107
+#, c-format
+msgid "max 7 lines"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:178
+#, c-format
+msgid "change authorization configuration"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:217
+#, c-format
+msgid "Need to complete marked fields and choose authorization method"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:82
+#, c-format
+msgid "This is not a valid bitcoin address."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:95
+#, c-format
+msgid "This is not a valid Ethereum address."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:118
+#, c-format
+msgid "IBAN numbers usually have more that 4 digits"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:120
+#, c-format
+msgid "IBAN numbers usually have less that 34 digits"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:128
+#, c-format
+msgid "IBAN country code not found"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:153
+#, c-format
+msgid "IBAN number is not valid, checksum is wrong"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:248
+#, c-format
+msgid "Target type"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:249
+#, c-format
+msgid "Method to use for wire transfer"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:258
+#, c-format
+msgid "Routing"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:259
+#, c-format
+msgid "Routing number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:263
+#, c-format
+msgid "Account"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:264
+#, c-format
+msgid "Account number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:273
+#, c-format
+msgid "Business Identifier Code."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:282
+#, c-format
+msgid "Bank Account Number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:292
+#, c-format
+msgid "Unified Payment Interface."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:301
+#, c-format
+msgid "Bitcoin protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:310
+#, c-format
+msgid "Ethereum protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:319
+#, c-format
+msgid "Interledger protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:328
+#, c-format
+msgid "Host"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:329
+#, c-format
+msgid "Bank host."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:334
+#, c-format
+msgid "Bank account."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:343
+#, c-format
+msgid "Bank account owner's name."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:370
+#, c-format
+msgid "No accounts yet."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:52
+#, c-format
+msgid ""
+"Name of the instance in URLs. The 'default' instance is special in that it "
+"is used to administer other instances."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:58
+#, c-format
+msgid "Business name"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:59
+#, c-format
+msgid "Legal name of the business represented by this instance."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:64
+#, c-format
+msgid "Email"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:65
+#, c-format
+msgid "Contact email"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:70
+#, c-format
+msgid "Website URL"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:71
+#, c-format
+msgid "URL."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:76
+#, c-format
+msgid "Logo"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:77
+#, c-format
+msgid "Logo image."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:82
+#, c-format
+msgid "Bank account"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:83
+#, c-format
+msgid "URI specifying bank account for crediting revenue."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:88
+#, c-format
+msgid "Default max deposit fee"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:89
+#, c-format
+msgid ""
+"Maximum deposit fees this merchant is willing to pay per order by default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:94
+#, c-format
+msgid "Default max wire fee"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:95
+#, c-format
+msgid ""
+"Maximum wire fees this merchant is willing to pay per wire transfer by "
+"default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:100
+#, c-format
+msgid "Default wire fee amortization"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:101
+#, c-format
+msgid ""
+"Number of orders excess wire transfer fees will be divided by to compute per "
+"order surcharge."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:107
+#, c-format
+msgid "Physical location of the merchant."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:114
+#, c-format
+msgid "Jurisdiction"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:115
+#, c-format
+msgid "Jurisdiction for legal disputes with the merchant."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:122
+#, c-format
+msgid "Default payment delay"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:124
+#, c-format
+msgid ""
+"Time customers have to pay an order before the offer expires by default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:129
+#, c-format
+msgid "Default wire transfer delay"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:130
+#, c-format
+msgid ""
+"Maximum time an exchange is allowed to delay wiring funds to the merchant, "
+"enabling it to aggregate smaller payments into larger wire transfers and "
+"reducing wire fees."
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:164
+#, c-format
+msgid "Instance id"
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:173
+#, c-format
+msgid "Change the authorization method use for this instance."
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:182
+#, c-format
+msgid "Manage access token"
+msgstr ""
+
+#: src/paths/instance/update/index.tsx:112
+#, c-format
+msgid "Failed to create instance"
+msgstr ""
+
+#: src/components/exception/login.tsx:74
+#, c-format
+msgid "Login required"
+msgstr ""
+
+#: src/components/exception/login.tsx:80
+#, c-format
+msgid "Please enter your access token."
+msgstr ""
+
+#: src/components/exception/login.tsx:108
+#, c-format
+msgid "Access Token"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:171
+#, c-format
+msgid "The request to the backend take too long and was cancelled"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:172
+#, c-format
+msgid "Diagnostic from %1$s is \"%2$s\""
+msgstr ""
+
+#: src/InstanceRoutes.tsx:178
+#, c-format
+msgid "The backend reported a problem: HTTP status #%1$s"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:179
+#, c-format
+msgid "Diagnostic from %1$s is '%2$s'"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:196
+#, c-format
+msgid "Access denied"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:197
+#, c-format
+msgid "The access token provided is invalid."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:212
+#, c-format
+msgid "No 'default' instance configured yet."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:213
+#, c-format
+msgid "Create a 'default' instance to begin using the merchant backoffice."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:630
+#, c-format
+msgid "The access token provided is invalid"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:664
+#, c-format
+msgid "Hide for today"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:82
+#, c-format
+msgid "Instance"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:91
+#, c-format
+msgid "Settings"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:167
+#, c-format
+msgid "Connection"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:209
+#, c-format
+msgid "New"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:219
+#, c-format
+msgid "List"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:234
+#, c-format
+msgid "Log out"
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:71
+#, c-format
+msgid "Check your token is valid"
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:90
+#, c-format
+msgid "Couldn't access the server."
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:91
+#, c-format
+msgid "Could not infer instance id from url %1$s"
+msgstr ""
+
+#: src/Application.tsx:104
+#, c-format
+msgid "Server not found"
+msgstr ""
+
+#: src/Application.tsx:118
+#, c-format
+msgid "Server response with an error code"
+msgstr ""
+
+#: src/Application.tsx:120
+#, c-format
+msgid "Got message %1$s from %2$s"
+msgstr ""
+
+#: src/Application.tsx:131
+#, c-format
+msgid "Response from server is unreadable, http status: %1$s"
+msgstr ""
+
+#: src/Application.tsx:144
+#, c-format
+msgid "Unexpected Error"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:101
+#, c-format
+msgid "The value %1$s is invalid for a payment url"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:110
+#, c-format
+msgid "add element to the list"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:112
+#, c-format
+msgid "add"
+msgstr ""
+
+#: src/components/form/InputSecured.tsx:37
+#, c-format
+msgid "Deleting"
+msgstr ""
+
+#: src/components/form/InputSecured.tsx:41
+#, c-format
+msgid "Changing"
+msgstr ""
+
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:87
+#, c-format
+msgid "Order ID"
+msgstr ""
+
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:101
+#, c-format
+msgid "Payment URL"
+msgstr ""
diff --git a/packages/merchant-backoffice-ui/src/i18n/es.po b/packages/merchant-backoffice-ui/src/i18n/es.po
index 9075d4656..2c4bc64a7 100644
--- a/packages/merchant-backoffice-ui/src/i18n/es.po
+++ b/packages/merchant-backoffice-ui/src/i18n/es.po
@@ -12,1054 +12,2843 @@
# You should have received a copy of the GNU General Public License along with
# TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
#
-#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: Taler Wallet\n"
-"Report-Msgid-Bugs-To: \n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
"POT-Creation-Date: 2016-11-23 00:00+0100\n"
-"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
-"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
-"Language-Team: LANGUAGE <LL@li.org>\n"
-"Language: \n"
+"PO-Revision-Date: 2024-02-13 14:40+0000\n"
+"Last-Translator: Stefan Kügel <skuegel@web.de>\n"
+"Language-Team: Spanish <https://weblate.taler.net/projects/gnu-taler/"
+"merchant-backoffice/es/>\n"
+"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 5.2.1\n"
-#: src/ApplicationReadyRoutes.tsx:50 src/InstanceRoutes.tsx:118
-#: src/InstanceRoutes.tsx:299
+#: src/components/modal/index.tsx:71
#, c-format
-msgid "Access denied"
-msgstr "Acceso denegado"
+msgid "Cancel"
+msgstr "Cancelar"
-#: src/ApplicationReadyRoutes.tsx:51 src/InstanceRoutes.tsx:118
-#: src/InstanceRoutes.tsx:300
+#: src/components/modal/index.tsx:79
#, c-format
-msgid "Check your token is valid"
-msgstr "Verifica que el token sea valido"
+msgid "%1$s"
+msgstr "%1$s"
-#: src/ApplicationReadyRoutes.tsx:72
+#: src/components/modal/index.tsx:84
#, c-format
-msgid "Couldn't access the server."
-msgstr "No se pudo acceder al servidor"
+msgid "Close"
+msgstr "Cerrar"
-#: src/ApplicationReadyRoutes.tsx:73
+#: src/components/modal/index.tsx:124
#, c-format
-msgid "Could not infer instance id from url %1$s"
-msgstr "No se pudo inferir el id de la instancia con la url %1$s"
+msgid "Continue"
+msgstr "Continuar"
-#: src/InstanceRoutes.tsx:109
+#: src/components/modal/index.tsx:178
#, c-format
-msgid "HTTP status #%1$s: Server reported a problem"
-msgstr "HTTP status #%1$s: Servidor reporto un problema"
+msgid "Clear"
+msgstr "Limpiar"
-#: src/InstanceRoutes.tsx:110
-#, fuzzy, c-format
-msgid "Got message: \"%1$s\" from: %2$s"
-msgstr "Recivimos el mensaje %1$s desde %2$s"
+#: src/components/modal/index.tsx:190
+#, c-format
+msgid "Confirm"
+msgstr "Confirmar"
-#: src/InstanceRoutes.tsx:127
+#: src/components/modal/index.tsx:296
#, c-format
-msgid "No default instance"
-msgstr "Sin instancia default"
+msgid "is not the same as the current access token"
+msgstr "no es el mismo que el token de acceso actual"
-#: src/InstanceRoutes.tsx:128
+#: src/components/modal/index.tsx:299
#, c-format
-msgid ""
-"in order to use merchant backoffice, you should create the default instance"
-msgstr "para usar el merchant backoffice, debería crear la instancia default"
+msgid "cannot be empty"
+msgstr "no puede ser vacío"
-#: src/InstanceRoutes.tsx:288
+#: src/components/modal/index.tsx:301
#, c-format
-msgid "Server reported a problem: HTTP status #%1$s"
-msgstr "Servidir reporto un problema: HTTP status #%1$s"
+msgid "cannot be the same as the old token"
+msgstr "no puede ser igual al viejo token"
-#: src/InstanceRoutes.tsx:289
-#, fuzzy, c-format
-msgid "Got message: %1$s from: %2$s"
-msgstr "Recivimos el mensaje %1$s desde %2$s"
+#: src/components/modal/index.tsx:305
+#, c-format
+msgid "is not the same"
+msgstr "no son iguales"
-#: src/components/exception/login.tsx:46
+#: src/components/modal/index.tsx:315
#, c-format
-msgid "Login required"
-msgstr "Login necesario"
+msgid "You are updating the access token from instance with id %1$s"
+msgstr "Está actualizando el token de acceso para la instancia con id %1$s"
+
+#: src/components/modal/index.tsx:331
+#, c-format
+msgid "Old access token"
+msgstr "Viejo token de acceso"
+
+#: src/components/modal/index.tsx:332
+#, c-format
+msgid "access token currently in use"
+msgstr "acceder al token en uso actualmente"
+
+#: src/components/modal/index.tsx:338
+#, c-format
+msgid "New access token"
+msgstr "Nuevo token de acceso"
+
+#: src/components/modal/index.tsx:339
+#, c-format
+msgid "next access token to be used"
+msgstr "siguiente token de acceso a usar"
-#: src/components/exception/login.tsx:49
+#: src/components/modal/index.tsx:344
+#, c-format
+msgid "Repeat access token"
+msgstr "Repetir token de acceso"
+
+#: src/components/modal/index.tsx:345
+#, c-format
+msgid "confirm the same access token"
+msgstr "confirmar el mismo token de acceso"
+
+#: src/components/modal/index.tsx:350
+#, c-format
+msgid "Clearing the access token will mean public access to the instance"
+msgstr "Limpiar el token de acceso significa acceso público a la instancia"
+
+#: src/components/modal/index.tsx:377
+#, c-format
+msgid "cannot be the same as the old access token"
+msgstr "no puede ser igual al anterior token de acceso"
+
+#: src/components/modal/index.tsx:394
+#, c-format
+msgid "You are setting the access token for the new instance"
+msgstr "Está estableciendo el token de acceso para la nueva instancia"
+
+#: src/components/modal/index.tsx:420
#, c-format
msgid ""
-"Please enter your auth token. Token should have \"secret-token:\" and start "
-"with Bearer or ApiKey"
-msgstr ""
-"Por favor ingrese su token de autorización. El token debe tener \"secret-"
-"token\" y comenzar con Bearer o ApiKey"
-
-#: src/components/exception/login.tsx:86 src/components/modal/index.tsx:53
-#: src/components/modal/index.tsx:75 src/paths/admin/create/CreatePage.tsx:115
-#: src/paths/instance/orders/create/CreatePage.tsx:325
-#: src/paths/instance/products/create/CreatePage.tsx:51
-#: src/paths/instance/products/list/Table.tsx:174
-#: src/paths/instance/products/list/Table.tsx:228
-#: src/paths/instance/products/update/UpdatePage.tsx:55
-#: src/paths/instance/transfers/create/CreatePage.tsx:89
-#: src/paths/instance/update/UpdatePage.tsx:134
+"With external authorization method no check will be done by the merchant "
+"backend"
+msgstr ""
+"Con el método de autorización externa no se hará ninguna revisión por el "
+"backend del comerciante"
+
+#: src/components/modal/index.tsx:436
#, c-format
-msgid "Confirm"
-msgstr "Confirmar"
+msgid "Set external authorization"
+msgstr "Establecer autorización externa"
-#: src/components/form/InputArray.tsx:72
+#: src/components/modal/index.tsx:448
#, c-format
-msgid "The value %1$s is invalid for a payment url"
-msgstr "El valor %1$s es invalido para una URL de pago"
+msgid "Set access token"
+msgstr "Establecer token de acceso"
+
+#: src/components/modal/index.tsx:470
+#, c-format
+msgid "Operation in progress..."
+msgstr "Operación en progreso..."
+
+#: src/components/modal/index.tsx:479
+#, c-format
+msgid "The operation will be automatically canceled after %1$s seconds"
+msgstr "La operación será automáticamente cancelada luego de %1$s segundos"
+
+#: src/paths/admin/list/TableActive.tsx:80
+#, c-format
+msgid "Instances"
+msgstr "Instancias"
+
+#: src/paths/admin/list/TableActive.tsx:93
+#, c-format
+msgid "Delete"
+msgstr "Borrar"
-#: src/components/form/InputDate.tsx:67
-#: src/paths/instance/orders/list/index.tsx:123
+#: src/paths/admin/list/TableActive.tsx:99
#, c-format
-msgid "pick a date"
-msgstr "elegir una fecha"
+msgid "add new instance"
+msgstr "agregar nueva instancia"
-#: src/components/form/InputDate.tsx:81
+#: src/paths/admin/list/TableActive.tsx:178
+#, c-format
+msgid "ID"
+msgstr "ID"
+
+#: src/paths/admin/list/TableActive.tsx:181
+#, c-format
+msgid "Name"
+msgstr "Nombre"
+
+#: src/paths/admin/list/TableActive.tsx:220
+#, c-format
+msgid "Edit"
+msgstr "Editar"
+
+#: src/paths/admin/list/TableActive.tsx:237
+#, c-format
+msgid "Purge"
+msgstr "Purgar"
+
+#: src/paths/admin/list/TableActive.tsx:261
+#, c-format
+msgid "There is no instances yet, add more pressing the + sign"
+msgstr "Todavía no hay instancias, agregue más presionando el signo +"
+
+#: src/paths/admin/list/View.tsx:68
+#, c-format
+msgid "Only show active instances"
+msgstr "Solo mostrar instancias activas"
+
+#: src/paths/admin/list/View.tsx:71
+#, c-format
+msgid "Active"
+msgstr "Activo"
+
+#: src/paths/admin/list/View.tsx:78
+#, c-format
+msgid "Only show deleted instances"
+msgstr "Mostrar solo instancias eliminadas"
+
+#: src/paths/admin/list/View.tsx:81
+#, c-format
+msgid "Deleted"
+msgstr "Eliminado"
+
+#: src/paths/admin/list/View.tsx:88
+#, c-format
+msgid "Show all instances"
+msgstr "Mostrar todas las instancias"
+
+#: src/paths/admin/list/View.tsx:91
+#, c-format
+msgid "All"
+msgstr "Todo"
+
+#: src/paths/admin/list/index.tsx:101
#, fuzzy, c-format
+msgid "Instance \"%1$s\" (ID: %2$s) has been deleted"
+msgstr "La instancia '%1$s' (ID: %2$s) fue eliminada"
+
+#: src/paths/admin/list/index.tsx:106
+#, c-format
+msgid "Failed to delete instance"
+msgstr "Fallo al eliminar instancia"
+
+#: src/paths/admin/list/index.tsx:124
+#, c-format
+msgid "Instance '%1$s' (ID: %2$s) has been disabled"
+msgstr "Instance '%1$s' (ID: %2$s) ha sido deshabilitada"
+
+#: src/paths/admin/list/index.tsx:129
+#, c-format
+msgid "Failed to purge instance"
+msgstr "Fallo al purgar la instancia"
+
+#: src/paths/instance/kyc/list/ListPage.tsx:41
+#, c-format
+msgid "Pending KYC verification"
+msgstr "Verificación KYC pendiente"
+
+#: src/paths/instance/kyc/list/ListPage.tsx:66
+#, c-format
+msgid "Timed out"
+msgstr "Expirado"
+
+#: src/paths/instance/kyc/list/ListPage.tsx:103
+#, c-format
+msgid "Exchange"
+msgstr "Exchange"
+
+#: src/paths/instance/kyc/list/ListPage.tsx:106
+#, c-format
+msgid "Target account"
+msgstr "Cuenta objetivo"
+
+#: src/paths/instance/kyc/list/ListPage.tsx:109
+#, c-format
+msgid "KYC URL"
+msgstr "URL de KYC"
+
+#: src/paths/instance/kyc/list/ListPage.tsx:144
+#, c-format
+msgid "Code"
+msgstr "Código"
+
+#: src/paths/instance/kyc/list/ListPage.tsx:147
+#, c-format
+msgid "Http Status"
+msgstr "Estado http"
+
+#: src/paths/instance/kyc/list/ListPage.tsx:177
+#, c-format
+msgid "No pending kyc verification!"
+msgstr "¡No hay verificación kyc pendiente!"
+
+#: src/components/form/InputDate.tsx:123
+#, c-format
+msgid "change value to unknown date"
+msgstr "cambiar valor a fecha desconocida"
+
+#: src/components/form/InputDate.tsx:124
+#, c-format
+msgid "change value to empty"
+msgstr "cambiar valor a vacío"
+
+#: src/components/form/InputDate.tsx:131
+#, c-format
msgid "clear"
-msgstr "Limpiar"
+msgstr "limpiar"
-#: src/components/form/InputDate.tsx:83
-#: src/paths/instance/transfers/list/Table.tsx:140
+#: src/components/form/InputDate.tsx:136
#, c-format
-msgid "never"
-msgstr "nunca"
+msgid "change value to never"
+msgstr "cambiar valor a nunca"
-#: src/components/form/InputImage.tsx:80
+#: src/components/form/InputDate.tsx:141
#, c-format
-msgid "Image should be smaller than 1 MB"
-msgstr "La imagen debe ser mas chica que 1 MB"
+msgid "never"
+msgstr "nunca"
-#: src/components/form/InputLocation.tsx:28
+#: src/components/form/InputLocation.tsx:29
#, c-format
msgid "Country"
msgstr "País"
-#: src/components/form/InputLocation.tsx:30
-#: src/paths/admin/create/CreatePage.tsx:99
-#: src/paths/instance/transfers/list/Table.tsx:124
-#: src/paths/instance/update/UpdatePage.tsx:118
+#: src/components/form/InputLocation.tsx:33
#, c-format
msgid "Address"
msgstr "Dirección"
-#: src/components/form/InputLocation.tsx:34
+#: src/components/form/InputLocation.tsx:39
#, c-format
msgid "Building number"
msgstr "Número de edificio"
-#: src/components/form/InputLocation.tsx:35
+#: src/components/form/InputLocation.tsx:41
#, c-format
msgid "Building name"
msgstr "Nombre de edificio"
-#: src/components/form/InputLocation.tsx:36
+#: src/components/form/InputLocation.tsx:42
#, c-format
msgid "Street"
msgstr "Calle"
-#: src/components/form/InputLocation.tsx:37
+#: src/components/form/InputLocation.tsx:43
#, c-format
msgid "Post code"
msgstr "Código postal"
-#: src/components/form/InputLocation.tsx:38
-#, fuzzy, c-format
+#: src/components/form/InputLocation.tsx:44
+#, c-format
msgid "Town location"
msgstr "Ubicación de ciudad"
-#: src/components/form/InputLocation.tsx:39
+#: src/components/form/InputLocation.tsx:45
#, c-format
msgid "Town"
msgstr "Ciudad"
-#: src/components/form/InputLocation.tsx:40
+#: src/components/form/InputLocation.tsx:46
#, c-format
msgid "District"
msgstr "Distrito"
-#: src/components/form/InputLocation.tsx:41
+#: src/components/form/InputLocation.tsx:49
#, c-format
msgid "Country subdivision"
-msgstr "Provincia"
+msgstr "Subdivisión de país"
-#: src/components/form/InputSearchProduct.tsx:59
-#, fuzzy, c-format
+#: src/components/form/InputSearchProduct.tsx:66
+#, c-format
msgid "Product id"
msgstr "Id de producto"
-#: src/components/form/InputSearchProduct.tsx:60
-#: src/components/product/ProductForm.tsx:99
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:122
-#: src/paths/instance/orders/list/Table.tsx:227
-#: src/paths/instance/products/list/Table.tsx:86
+#: src/components/form/InputSearchProduct.tsx:69
#, c-format
msgid "Description"
msgstr "Descripcion"
-#: src/components/form/InputSearchProduct.tsx:73
-#: src/components/form/InputTaxes.tsx:81
-#: src/paths/admin/create/CreatePage.tsx:87 src/paths/admin/list/Table.tsx:110
-#: src/paths/instance/details/DetailPage.tsx:76
-#: src/paths/instance/update/UpdatePage.tsx:106
+#: src/components/form/InputSearchProduct.tsx:94
+#, fuzzy, c-format
+msgid "Product"
+msgstr "Productos"
+
+#: src/components/form/InputSearchProduct.tsx:95
#, c-format
-msgid "Name"
-msgstr "Nombre"
+msgid "search products by it's description or id"
+msgstr "buscar productos por su descripción o ID"
-#: src/components/form/InputSearchProduct.tsx:102
+#: src/components/form/InputSearchProduct.tsx:151
#, c-format
-msgid "loading..."
-msgstr "Cargando..."
+msgid "no products found with that description"
+msgstr "no se encontraron productos con esa descripción"
-#: src/components/form/InputSearchProduct.tsx:108
+#: src/components/product/InventoryProductForm.tsx:56
#, c-format
-msgid "no products found"
-msgstr "No se encontraron productos"
+msgid "You must enter a valid product identifier."
+msgstr "Debe ingresar un identificador de producto válido."
-#: src/components/form/InputSearchProduct.tsx:116
+#: src/components/product/InventoryProductForm.tsx:64
#, c-format
-msgid "no results"
-msgstr "Sin resultados"
+msgid "Quantity must be greater than 0!"
+msgstr "¡Cantidad debe ser mayor que 0!"
+
+#: src/components/product/InventoryProductForm.tsx:76
+#, fuzzy, c-format
+msgid ""
+"This quantity exceeds remaining stock. Currently, only %1$s units remain "
+"unreserved in stock."
+msgstr ""
+"Esta cantidad excede las existencias restantes. Actualmente, solo quedan "
+"%1$s unidades sin reservar en las existencias."
-#: src/components/form/InputSecured.tsx:33
+#: src/components/product/InventoryProductForm.tsx:109
#, c-format
-msgid "Deleting"
-msgstr "Borrando"
+msgid "Quantity"
+msgstr "Cantidad"
-#: src/components/form/InputSecured.tsx:34
+#: src/components/product/InventoryProductForm.tsx:110
#, c-format
-msgid "Changing"
-msgstr "Cambiando"
+msgid "how many products will be added"
+msgstr "cuántos productos serán agregados"
-#: src/components/form/InputSecured.tsx:60
+#: src/components/product/InventoryProductForm.tsx:117
#, c-format
-msgid "Manage token"
-msgstr "Administrar token"
+msgid "Add from inventory"
+msgstr "Agregar del inventario"
-#: src/components/form/InputSecured.tsx:83
+#: src/components/form/InputImage.tsx:105
#, c-format
-msgid "Update"
-msgstr "Actualizar"
+msgid "Image should be smaller than 1 MB"
+msgstr "La imagen debe ser mas chica que 1 MB"
+
+#: src/components/form/InputImage.tsx:110
+#, c-format
+msgid "Add"
+msgstr "Agregar"
-#: src/components/form/InputSecured.tsx:100
-#: src/paths/instance/orders/create/CreatePage.tsx:252
-#: src/paths/instance/orders/create/CreatePage.tsx:273
+#: src/components/form/InputImage.tsx:115
#, c-format
msgid "Remove"
msgstr "Eliminar"
-#: src/components/form/InputSecured.tsx:106 src/components/modal/index.tsx:52
-#: src/components/modal/index.tsx:73 src/paths/admin/create/CreatePage.tsx:114
-#: src/paths/instance/orders/create/CreatePage.tsx:324
-#: src/paths/instance/products/create/CreatePage.tsx:50
-#: src/paths/instance/products/list/Table.tsx:166
-#: src/paths/instance/products/list/Table.tsx:218
-#: src/paths/instance/products/update/UpdatePage.tsx:54
-#: src/paths/instance/transfers/create/CreatePage.tsx:88
-#: src/paths/instance/update/UpdatePage.tsx:133
+#: src/components/form/InputTaxes.tsx:113
#, c-format
-msgid "Cancel"
-msgstr "Cancelar"
+msgid "No taxes configured for this product."
+msgstr "Ningun impuesto configurado para este producto."
-#: src/components/form/InputStock.tsx:91
+#: src/components/form/InputTaxes.tsx:119
#, c-format
-msgid "Manage stock"
-msgstr "Administrar stock"
+msgid "Amount"
+msgstr "Monto"
-#: src/components/form/InputStock.tsx:93
+#: src/components/form/InputTaxes.tsx:120
#, c-format
-msgid "Infinite"
-msgstr "Inifinito"
+msgid ""
+"Taxes can be in currencies that differ from the main currency used by the "
+"merchant."
+msgstr ""
+"Impuestos pueden estar en divisas que difieren de la principal divisa usada "
+"por el comerciante."
-#: src/components/form/InputStock.tsx:105
-#, fuzzy, c-format
-msgid "lost cannot be greater that current + incoming (max %1$s)"
-msgstr "no puede ser mayor al stock actual %1$s"
+#: src/components/form/InputTaxes.tsx:122
+#, c-format
+msgid ""
+"Enter currency and value separated with a colon, e.g. &quot;USD:2.3&quot;."
+msgstr ""
+"Ingrese divisa y valor separado por dos puntos, e.g. &quot;USD:2.3&quot;."
-#: src/components/form/InputStock.tsx:111
+#: src/components/form/InputTaxes.tsx:131
#, c-format
-msgid "current stock will change from %1$s to %2$s"
-msgstr "stock actual cambiará desde %1$s a %2$s"
+msgid "Legal name of the tax, e.g. VAT or import duties."
+msgstr "Nombre legal del impuesto, e.g. IVA o arancel."
-#: src/components/form/InputStock.tsx:112
+#: src/components/form/InputTaxes.tsx:137
#, c-format
-msgid "current stock will stay at %1$s"
-msgstr "stock actual seguirá en %1$s"
+msgid "add tax to the tax list"
+msgstr "agregar impuesto a la lista de impuestos"
-#: src/components/form/InputStock.tsx:129
-#: src/paths/instance/products/list/Table.tsx:204
+#: src/components/product/NonInventoryProductForm.tsx:72
#, c-format
-msgid "Incoming"
-msgstr "Ingresando"
+msgid "describe and add a product that is not in the inventory list"
+msgstr "describa y agregue un producto que no está en la lista de inventarios"
-#: src/components/form/InputStock.tsx:130
-#: src/paths/instance/products/list/Table.tsx:205
+#: src/components/product/NonInventoryProductForm.tsx:75
#, c-format
-msgid "Lost"
-msgstr "Perdido"
+msgid "Add custom product"
+msgstr "Agregue un producto personalizado"
-#: src/components/form/InputStock.tsx:142
+#: src/components/product/NonInventoryProductForm.tsx:86
#, c-format
-msgid "Current"
-msgstr "Actual"
+msgid "Complete information of the product"
+msgstr "Complete información del producto"
-#: src/components/form/InputStock.tsx:145
+#: src/components/product/NonInventoryProductForm.tsx:185
#, c-format
-msgid "without stock"
-msgstr "sin stock"
+msgid "Image"
+msgstr "Imagen"
-#: src/components/form/InputStock.tsx:150
+#: src/components/product/NonInventoryProductForm.tsx:186
#, c-format
-msgid "Next restock"
-msgstr "Próximo reabastecimiento"
+msgid "photo of the product"
+msgstr "foto del producto"
-#: src/components/form/InputStock.tsx:152
+#: src/components/product/NonInventoryProductForm.tsx:192
#, c-format
-msgid "Delivery address"
-msgstr "Dirección de entrega"
+msgid "full product description"
+msgstr "descripción completa del producto"
-#: src/components/form/InputTaxes.tsx:73
+#: src/components/product/NonInventoryProductForm.tsx:196
#, c-format
-msgid "this product has no taxes"
-msgstr "este producto no tiene impuestos"
+msgid "Unit"
+msgstr "Unidad"
-#: src/components/form/InputTaxes.tsx:77
-#: src/paths/instance/orders/details/DetailPage.tsx:145
-#: src/paths/instance/orders/details/DetailPage.tsx:296
-#: src/paths/instance/orders/list/Table.tsx:116
-#: src/paths/instance/transfers/create/CreatePage.tsx:84
+#: src/components/product/NonInventoryProductForm.tsx:197
#, c-format
-msgid "Amount"
-msgstr "Monto"
+msgid "name of the product unit"
+msgstr "nombre de la unidad del producto"
-#: src/components/form/InputTaxes.tsx:78
+#: src/components/product/NonInventoryProductForm.tsx:201
#, c-format
-msgid "currency and value separated with colon"
-msgstr "Moneda y valor separado por dos puntos"
+msgid "Price"
+msgstr "Precio"
-#: src/components/form/InputTaxes.tsx:84
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:78
+#: src/components/product/NonInventoryProductForm.tsx:202
#, c-format
-msgid "Add"
-msgstr "Agregar"
+msgid "amount in the current currency"
+msgstr "monto de la divisa actual"
-#: src/components/menu/SideBar.tsx:53
+#: src/components/product/NonInventoryProductForm.tsx:211
#, c-format
-msgid "Instance"
-msgstr "Instancia"
+msgid "Taxes"
+msgstr "Impuestos"
-#: src/components/menu/SideBar.tsx:59
+#: src/components/product/ProductList.tsx:38
#, c-format
-msgid "Settings"
-msgstr "Configuración"
+msgid "image"
+msgstr "imagen"
-#: src/components/menu/SideBar.tsx:65
-#: src/paths/instance/orders/list/Table.tsx:60
-#, fuzzy, c-format
-msgid "Orders"
-msgstr "Ordenes"
+#: src/components/product/ProductList.tsx:41
+#, c-format
+msgid "description"
+msgstr "descripción"
-#: src/components/menu/SideBar.tsx:71
-#: src/paths/instance/orders/create/CreatePage.tsx:258
-#: src/paths/instance/products/list/Table.tsx:48
+#: src/components/product/ProductList.tsx:44
#, c-format
-msgid "Products"
-msgstr "Productos"
+msgid "quantity"
+msgstr "cantidad"
-#: src/components/menu/SideBar.tsx:77
-#: src/paths/instance/transfers/list/Table.tsx:65
+#: src/components/product/ProductList.tsx:47
#, c-format
-msgid "Transfers"
-msgstr "Transferencias"
+msgid "unit price"
+msgstr "precio unitario"
-#: src/components/menu/SideBar.tsx:87
-#, fuzzy, c-format
-msgid "Connection"
-msgstr "Conexión"
+#: src/components/product/ProductList.tsx:50
+#, c-format
+msgid "total price"
+msgstr "precio total"
-#: src/components/menu/SideBar.tsx:112 src/paths/admin/list/Table.tsx:57
+#: src/paths/instance/orders/create/CreatePage.tsx:153
#, c-format
-msgid "Instances"
-msgstr "Instancias"
+msgid "required"
+msgstr "requerido"
-#: src/components/menu/SideBar.tsx:116
-#, fuzzy, c-format
-msgid "New"
-msgstr "Nuevo"
+#: src/paths/instance/orders/create/CreatePage.tsx:157
+#, c-format
+msgid "not valid"
+msgstr "no válido"
-#: src/components/menu/SideBar.tsx:122
+#: src/paths/instance/orders/create/CreatePage.tsx:159
#, c-format
-msgid "List"
-msgstr "Lista"
+msgid "must be greater than 0"
+msgstr "debe ser mayor que 0"
-#: src/components/menu/SideBar.tsx:129
+#: src/paths/instance/orders/create/CreatePage.tsx:164
#, c-format
-msgid "Log out"
-msgstr "Salir"
+msgid "not a valid json"
+msgstr "no es un json válido"
-#: src/components/modal/index.tsx:74
+#: src/paths/instance/orders/create/CreatePage.tsx:170
#, c-format
-msgid "Clear"
-msgstr "Limpiar"
+msgid "should be in the future"
+msgstr "deberían ser en el futuro"
-#: src/components/modal/index.tsx:110 src/components/modal/index.tsx:111
+#: src/paths/instance/orders/create/CreatePage.tsx:173
#, c-format
-msgid "should be the same"
-msgstr "deberían ser iguales"
+msgid "refund deadline cannot be before pay deadline"
+msgstr "plazo de reembolso no puede ser antes que el plazo de pago"
-#: src/components/modal/index.tsx:111
+#: src/paths/instance/orders/create/CreatePage.tsx:179
#, c-format
-msgid "cannot be the same as before"
-msgstr "no puede ser igual al anterior"
+msgid "wire transfer deadline cannot be before refund deadline"
+msgstr ""
+"el plazo de la transferencia bancaria no puede ser antes que el plazo de "
+"reembolso"
-#: src/components/modal/index.tsx:114
+#: src/paths/instance/orders/create/CreatePage.tsx:190
#, c-format
-msgid ""
-"You are updating the authorization token from instance %1$s with id %2$s"
+msgid "wire transfer deadline cannot be before pay deadline"
msgstr ""
-"Está actualizando el token de autorización para la instancia %1$s con id %2$s"
+"el plazo de la transferencia bancaria no puede ser antes que el plazo de pago"
-#: src/components/modal/index.tsx:124
+#: src/paths/instance/orders/create/CreatePage.tsx:197
#, c-format
-msgid "Old token"
-msgstr "Viejo token"
+msgid "should have a refund deadline"
+msgstr "debería tener un plazo de reembolso"
-#: src/components/modal/index.tsx:125
+#: src/paths/instance/orders/create/CreatePage.tsx:202
#, c-format
-msgid "New token"
-msgstr "Nuevo token"
+msgid "auto refund cannot be after refund deadline"
+msgstr "reembolso automático no puede ser después qu el plazo de reembolso"
-#: src/components/modal/index.tsx:127
+#: src/paths/instance/orders/create/CreatePage.tsx:360
#, c-format
-msgid "Clearing the auth token will mean public access to the instance"
-msgstr ""
-"Limpiar el token de autorización significa acceso publico a la instancia"
+msgid "Manage products in order"
+msgstr "Manejar productos en orden"
-#: src/components/product/ProductForm.tsx:96
-#: src/paths/admin/create/CreatePage.tsx:85 src/paths/admin/list/Table.tsx:109
-#: src/paths/instance/transfers/list/Table.tsx:122
+#: src/paths/instance/orders/create/CreatePage.tsx:369
#, c-format
-msgid "ID"
-msgstr "ID"
+msgid "Manage list of products in the order."
+msgstr "Manejar lista de productos en la orden."
-#: src/components/product/ProductForm.tsx:98
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:121
-#: src/paths/instance/products/list/Table.tsx:85
+#: src/paths/instance/orders/create/CreatePage.tsx:391
#, c-format
-msgid "Image"
-msgstr "Imagen"
+msgid "Remove this product from the order."
+msgstr "Remover este producto de la orden."
-#: src/components/product/ProductForm.tsx:100
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:123
+#: src/paths/instance/orders/create/CreatePage.tsx:415
#, c-format
-msgid "Unit"
-msgstr "Unidad"
+msgid "Total price"
+msgstr "Precio total"
-#: src/components/product/ProductForm.tsx:101
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:124
-#: src/paths/instance/products/list/Table.tsx:162
-#: src/paths/instance/products/list/Table.tsx:214
+#: src/paths/instance/orders/create/CreatePage.tsx:417
#, c-format
-msgid "Price"
-msgstr "Precio"
+msgid "total product price added up"
+msgstr "precio total de producto agregado"
-#: src/components/product/ProductForm.tsx:103
-#: src/paths/instance/products/list/Table.tsx:90
+#: src/paths/instance/orders/create/CreatePage.tsx:430
#, c-format
-msgid "Stock"
-msgstr "Stock"
+msgid "Amount to be paid by the customer"
+msgstr "Monto a ser pagado por el cliente"
-#: src/components/product/ProductForm.tsx:105
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:128
-#: src/paths/instance/products/list/Table.tsx:88
+#: src/paths/instance/orders/create/CreatePage.tsx:436
#, c-format
-msgid "Taxes"
-msgstr "Impuesto"
+msgid "Order price"
+msgstr "Precio de la orden"
-#: src/index.tsx:75
+#: src/paths/instance/orders/create/CreatePage.tsx:437
#, c-format
-msgid "Server not found"
-msgstr "Servidor no encontrado"
+msgid "final order price"
+msgstr "Precio final de la orden"
-#: src/index.tsx:85
+#: src/paths/instance/orders/create/CreatePage.tsx:444
#, c-format
-msgid "Couldn't access the server"
-msgstr "No se pudo aceder al servidor"
+msgid "Summary"
+msgstr "Resumen"
-#: src/index.tsx:87 src/index.tsx:99
+#: src/paths/instance/orders/create/CreatePage.tsx:445
#, c-format
-msgid "Got message %1$s from %2$s"
-msgstr "Recivimos el mensaje %1$s desde %2$s"
+msgid "Title of the order to be shown to the customer"
+msgstr "Título de la orden a ser mostrado al cliente"
-#: src/index.tsx:97
+#: src/paths/instance/orders/create/CreatePage.tsx:450
#, c-format
-msgid "Unexpected Error"
-msgstr "Error inesperado"
+msgid "Shipping and Fulfillment"
+msgstr "Envío y cumplimiento"
-#: src/paths/admin/create/CreatePage.tsx:89
-#: src/paths/instance/update/UpdatePage.tsx:108
+#: src/paths/instance/orders/create/CreatePage.tsx:455
#, c-format
-msgid "Auth token"
-msgstr "Token de autorización"
+msgid "Delivery date"
+msgstr "Fecha de entrega"
-#: src/paths/admin/create/CreatePage.tsx:91
-#: src/paths/instance/details/DetailPage.tsx:77
-#: src/paths/instance/update/UpdatePage.tsx:110
+#: src/paths/instance/orders/create/CreatePage.tsx:456
#, c-format
-msgid "Account address"
-msgstr "Dirección de cuenta"
+msgid "Deadline for physical delivery assured by the merchant."
+msgstr "Plazo para la entrega física asegurado por el comerciante."
-#: src/paths/admin/create/CreatePage.tsx:93
-#: src/paths/instance/update/UpdatePage.tsx:112
+#: src/paths/instance/orders/create/CreatePage.tsx:461
#, c-format
-msgid "Default max deposit fee"
-msgstr "Impuesto máximo de deposito por omisión"
+msgid "Location"
+msgstr "Ubicación"
-#: src/paths/admin/create/CreatePage.tsx:95
-#: src/paths/instance/update/UpdatePage.tsx:114
+#: src/paths/instance/orders/create/CreatePage.tsx:462
#, c-format
-msgid "Default max wire fee"
-msgstr "Impuesto máximo de transferencia por omisión"
+msgid "address where the products will be delivered"
+msgstr "dirección a donde los productos serán entregados"
-#: src/paths/admin/create/CreatePage.tsx:97
-#: src/paths/instance/update/UpdatePage.tsx:116
+#: src/paths/instance/orders/create/CreatePage.tsx:469
#, c-format
-msgid "Default wire fee amortization"
-msgstr "Amortización de impuesto de transferencia por omisión"
+msgid "Fulfillment URL"
+msgstr "URL de cumplimiento"
-#: src/paths/admin/create/CreatePage.tsx:103
-#: src/paths/instance/update/UpdatePage.tsx:122
+#: src/paths/instance/orders/create/CreatePage.tsx:470
#, c-format
-msgid "Jurisdiction"
-msgstr "Jurisdicción"
+msgid "URL to which the user will be redirected after successful payment."
+msgstr "URL al cual el usuario será redirigido luego de pago exitoso."
-#: src/paths/admin/create/CreatePage.tsx:107
-#: src/paths/instance/update/UpdatePage.tsx:126
+#: src/paths/instance/orders/create/CreatePage.tsx:476
#, c-format
-msgid "Default pay delay"
-msgstr "Retrazo de pago por omisión"
+msgid "Taler payment options"
+msgstr "Opciones de pago de Taler"
-#: src/paths/admin/create/CreatePage.tsx:109
-#: src/paths/instance/update/UpdatePage.tsx:128
+#: src/paths/instance/orders/create/CreatePage.tsx:477
#, c-format
-msgid "Default wire transfer delay"
-msgstr "Retrazo de transferencia por omisión"
+msgid "Override default Taler payment settings for this order"
+msgstr "Sobreescribir pagos por omisión de Taler para esta orden"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:481
+#, fuzzy, c-format
+msgid "Payment deadline"
+msgstr "Plazo de pago"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:482
+#, c-format
+msgid ""
+"Deadline for the customer to pay for the offer before it expires. Inventory "
+"products will be reserved until this deadline."
+msgstr ""
+"Plazo límite para que el cliente pague por la oferta antes de que expire. "
+"Productos del inventario serán reservados hasta este plazo límite."
+
+#: src/paths/instance/orders/create/CreatePage.tsx:486
+#, c-format
+msgid "Refund deadline"
+msgstr "Plazo de reembolso"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:487
+#, c-format
+msgid "Time until which the order can be refunded by the merchant."
+msgstr ""
+"Tiempo hasta el cual la orden puede ser reembolsada por el comerciante."
-#: src/paths/admin/create/index.tsx:58
+#: src/paths/instance/orders/create/CreatePage.tsx:491
#, c-format
-msgid "could not create instance"
-msgstr "no se pudo crear la instancia"
+msgid "Wire transfer deadline"
+msgstr "Plazo de la transferencia"
-#: src/paths/admin/list/Table.tsx:63 src/paths/admin/list/Table.tsx:131
-#: src/paths/instance/transfers/list/Table.tsx:71
+#: src/paths/instance/orders/create/CreatePage.tsx:492
+#, c-format
+msgid "Deadline for the exchange to make the wire transfer."
+msgstr "Plazo para que el exchange haga la transferencia."
+
+#: src/paths/instance/orders/create/CreatePage.tsx:496
#, fuzzy, c-format
-msgid "Delete"
-msgstr "Borrando"
+msgid "Auto-refund deadline"
+msgstr "Plazo de reembolso automático"
-#: src/paths/admin/list/Table.tsx:128
+#: src/paths/instance/orders/create/CreatePage.tsx:497
#, c-format
-msgid "Edit"
+msgid ""
+"Time until which the wallet will automatically check for refunds without "
+"user interaction."
msgstr ""
+"Tiempo hasta el cual la billetera será automáticamente revisada por "
+"reembolsos win interación por parte del usuario."
-#: src/paths/admin/list/Table.tsx:149
-#: src/paths/instance/products/list/Table.tsx:245
+#: src/paths/instance/orders/create/CreatePage.tsx:502
#, c-format
-msgid "There is no instances yet, add more pressing the + sign"
-msgstr "No hay instancias todavían, agregue mas presionando el signo +"
+msgid "Maximum deposit fee"
+msgstr "Máxima tarifa de depósito"
-#: src/paths/instance/orders/create/CreatePage.tsx:237
+#: src/paths/instance/orders/create/CreatePage.tsx:503
#, c-format
-msgid "Inventory products"
-msgstr "Productos de inventario"
+msgid ""
+"Maximum deposit fees the merchant is willing to cover for this order. Higher "
+"deposit fees must be covered in full by the consumer."
+msgstr ""
+"Máxima tarifa de depósito que el comerciante esta dispuesto a cubir para "
+"esta orden. Mayores tarifas de depósito deben ser cubiertas completamente "
+"por el consumidor."
-#: src/paths/instance/orders/create/CreatePage.tsx:286
+#: src/paths/instance/orders/create/CreatePage.tsx:507
#, c-format
-msgid "Total price"
-msgstr "Precio total"
+msgid "Maximum wire fee"
+msgstr "Máxima tarifa de transferencia"
-#: src/paths/instance/orders/create/CreatePage.tsx:287
+#: src/paths/instance/orders/create/CreatePage.tsx:508
#, c-format
-msgid "Total tax"
-msgstr "Impuesto total"
+msgid ""
+"Maximum aggregate wire fees the merchant is willing to cover for this order. "
+"Wire fees exceeding this amount are to be covered by the customers."
+msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:289
-#: src/paths/instance/orders/create/CreatePage.tsx:297
+#: src/paths/instance/orders/create/CreatePage.tsx:512
#, c-format
-msgid "Order price"
-msgstr "Precio de la orden"
+msgid "Wire fee amortization"
+msgstr "Amortización de comisión de transferencia"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:513
+#, c-format
+msgid ""
+"Factor by which wire fees exceeding the above threshold are divided to "
+"determine the share of excess wire fees to be paid explicitly by the "
+"consumer."
+msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:295
+#: src/paths/instance/orders/create/CreatePage.tsx:517
#, fuzzy, c-format
-msgid "Net"
-msgstr "Neto"
+msgid "Create token"
+msgstr "Administrar token"
-#: src/paths/instance/orders/create/CreatePage.tsx:300
-#: src/paths/instance/orders/details/DetailPage.tsx:144
-#: src/paths/instance/orders/details/DetailPage.tsx:295
-#: src/paths/instance/orders/list/Table.tsx:117
+#: src/paths/instance/orders/create/CreatePage.tsx:518
#, c-format
-msgid "Summary"
-msgstr "Resumen"
+msgid ""
+"Uncheck this option if the merchant backend generated an order ID with "
+"enough entropy to prevent adversarial claims."
+msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:302
+#: src/paths/instance/orders/create/CreatePage.tsx:522
+#, fuzzy, c-format
+msgid "Minimum age required"
+msgstr "Login necesario"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:523
#, c-format
-msgid "Payments options"
-msgstr "Opciones de pago"
+msgid ""
+"Any value greater than 0 will limit the coins able be used to pay this "
+"contract. If empty the age restriction will be defined by the products"
+msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:303
+#: src/paths/instance/orders/create/CreatePage.tsx:526
#, c-format
-msgid "Auto refund deadline"
-msgstr "Plazo de reembolso automático"
+msgid "Min age defined by the producs is %1$s"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:534
+#, fuzzy, c-format
+msgid "Additional information"
+msgstr "Información extra"
-#: src/paths/instance/orders/create/CreatePage.tsx:304
+#: src/paths/instance/orders/create/CreatePage.tsx:535
#, c-format
-msgid "Refund deadline"
-msgstr "Plazo de reembolso"
+msgid "Custom information to be included in the contract for this order."
+msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:305
+#: src/paths/instance/orders/create/CreatePage.tsx:541
#, c-format
-msgid "Pay deadline"
-msgstr "Plazo de pago"
+msgid "You must enter a value in JavaScript Object Notation (JSON)."
+msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:307
+#: src/components/picker/DurationPicker.tsx:55
#, c-format
-msgid "Delivery date"
-msgstr "Fecha de entrega"
+msgid "days"
+msgstr "días"
+
+#: src/components/picker/DurationPicker.tsx:65
+#, c-format
+msgid "hours"
+msgstr "horas"
+
+#: src/components/picker/DurationPicker.tsx:76
+#, c-format
+msgid "minutes"
+msgstr "minutos"
-#: src/paths/instance/orders/create/CreatePage.tsx:308
+#: src/components/picker/DurationPicker.tsx:87
+#, c-format
+msgid "seconds"
+msgstr "segundos"
+
+#: src/components/form/InputDuration.tsx:53
#, fuzzy, c-format
-msgid "Location"
-msgstr "Ubicación"
+msgid "forever"
+msgstr "nunca"
+
+#: src/components/form/InputDuration.tsx:62
+#, c-format
+msgid "%1$sM"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:64
+#, c-format
+msgid "%1$sY"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:66
+#, c-format
+msgid "%1$sd"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:68
+#, c-format
+msgid "%1$sh"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:70
+#, c-format
+msgid "%1$smin"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:72
+#, c-format
+msgid "%1$ssec"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:75
+#, c-format
+msgid "Orders"
+msgstr "Órdenes"
+
+#: src/paths/instance/orders/list/Table.tsx:81
+#, fuzzy, c-format
+msgid "create order"
+msgstr "creado"
+
+#: src/paths/instance/orders/list/Table.tsx:147
+#, c-format
+msgid "load newer orders"
+msgstr "cargar nuevas ordenes"
+
+#: src/paths/instance/orders/list/Table.tsx:154
+#, c-format
+msgid "Date"
+msgstr "Fecha"
+
+#: src/paths/instance/orders/list/Table.tsx:200
+#, c-format
+msgid "Refund"
+msgstr "Devolución"
+
+#: src/paths/instance/orders/list/Table.tsx:209
+#, c-format
+msgid "copy url"
+msgstr "copiar url"
+
+#: src/paths/instance/orders/list/Table.tsx:225
+#, c-format
+msgid "load older orders"
+msgstr "cargar viejas ordenes"
+
+#: src/paths/instance/orders/list/Table.tsx:242
+#, c-format
+msgid "No orders have been found matching your query!"
+msgstr "¡No se encontraron órdenes que emparejen su búsqueda!"
+
+#: src/paths/instance/orders/list/Table.tsx:288
+#, c-format
+msgid "duplicated"
+msgstr "duplicado"
+
+#: src/paths/instance/orders/list/Table.tsx:299
+#, c-format
+msgid "invalid format"
+msgstr "formato inválido"
+
+#: src/paths/instance/orders/list/Table.tsx:301
+#, c-format
+msgid "this value exceed the refundable amount"
+msgstr "este monto excede el monto reembolsable"
-#: src/paths/instance/orders/create/CreatePage.tsx:312
+#: src/paths/instance/orders/list/Table.tsx:346
+#, c-format
+msgid "date"
+msgstr "fecha"
+
+#: src/paths/instance/orders/list/Table.tsx:349
+#, c-format
+msgid "amount"
+msgstr "monto"
+
+#: src/paths/instance/orders/list/Table.tsx:352
+#, c-format
+msgid "reason"
+msgstr "razón"
+
+#: src/paths/instance/orders/list/Table.tsx:389
+#, c-format
+msgid "amount to be refunded"
+msgstr "monto a ser reembolsado"
+
+#: src/paths/instance/orders/list/Table.tsx:391
+#, c-format
+msgid "Max refundable:"
+msgstr "Máximo reembolzable:"
+
+#: src/paths/instance/orders/list/Table.tsx:396
+#, c-format
+msgid "Reason"
+msgstr "Razón"
+
+#: src/paths/instance/orders/list/Table.tsx:397
+#, c-format
+msgid "Choose one..."
+msgstr "Elija uno..."
+
+#: src/paths/instance/orders/list/Table.tsx:399
+#, c-format
+msgid "requested by the customer"
+msgstr "pedido por el consumidor"
+
+#: src/paths/instance/orders/list/Table.tsx:400
+#, c-format
+msgid "other"
+msgstr "otro"
+
+#: src/paths/instance/orders/list/Table.tsx:403
+#, c-format
+msgid "why this order is being refunded"
+msgstr "por qué esta orden está siendo reembolsada"
+
+#: src/paths/instance/orders/list/Table.tsx:409
+#, c-format
+msgid "more information to give context"
+msgstr "más información para dar contexto"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:62
+#, c-format
+msgid "Contract Terms"
+msgstr "Términos de contrato"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:68
+#, c-format
+msgid "human-readable description of the whole purchase"
+msgstr "descripción legible de toda la compra"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:74
+#, c-format
+msgid "total price for the transaction"
+msgstr "precio total de la transacción"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:81
+#, c-format
+msgid "URL for this purchase"
+msgstr "URL para esta compra"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:87
#, c-format
msgid "Max fee"
-msgstr "Impuesto máximo"
+msgstr "Máxima comisión"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:88
+#, c-format
+msgid "maximum total deposit fee accepted by the merchant for this contract"
+msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:313
+#: src/paths/instance/orders/details/DetailPage.tsx:93
#, c-format
msgid "Max wire fee"
msgstr "Impuesto de transferencia máximo"
-#: src/paths/instance/orders/create/CreatePage.tsx:314
+#: src/paths/instance/orders/details/DetailPage.tsx:94
#, c-format
-msgid "Wire fee amortization"
-msgstr "Amortización de impuesto de transferencia"
+msgid "maximum wire fee accepted by the merchant"
+msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:315
+#: src/paths/instance/orders/details/DetailPage.tsx:100
#, c-format
-msgid "Fullfilment url"
-msgstr "URL de completitud"
+msgid ""
+"over how many customer transactions does the merchant expect to amortize "
+"wire fees on average"
+msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:318
+#: src/paths/instance/orders/details/DetailPage.tsx:105
#, c-format
-msgid "Extra information"
-msgstr "Información extra"
+msgid "Created at"
+msgstr "Creado en"
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:44
+#: src/paths/instance/orders/details/DetailPage.tsx:106
#, c-format
-msgid "select a product first"
-msgstr "seleccione un producto primero"
+msgid "time when this contract was generated"
+msgstr ""
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:51
-#, fuzzy, c-format
-msgid "should be greater than 0"
-msgstr "La imagen debe ser mas chica que 1 MB"
+#: src/paths/instance/orders/details/DetailPage.tsx:112
+#, c-format
+msgid "after this deadline has passed no refunds will be accepted"
+msgstr ""
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:58
+#: src/paths/instance/orders/details/DetailPage.tsx:118
#, c-format
msgid ""
-"cannot be greater than current stock and quantity previously added. max: %1$s"
+"after this deadline, the merchant won't accept payments for the contract"
msgstr ""
-"no puede ser mayor al stock actual y la cantidad previamente agregada. "
-"máximo: %1$s"
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:64
+#: src/paths/instance/orders/details/DetailPage.tsx:124
#, c-format
-msgid "cannot be greater than current stock %1$s"
-msgstr "no puede ser mayor al stock actual %1$s"
+msgid "transfer deadline for the exchange"
+msgstr ""
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:76
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:126
+#: src/paths/instance/orders/details/DetailPage.tsx:130
#, c-format
-msgid "Quantity"
-msgstr "Cantidad"
+msgid "time indicating when the order should be delivered"
+msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:92
-#: src/paths/instance/orders/details/DetailPage.tsx:235
-#: src/paths/instance/orders/details/DetailPage.tsx:333
+#: src/paths/instance/orders/details/DetailPage.tsx:136
#, c-format
-msgid "Order"
-msgstr "Orden"
+msgid "where the order will be delivered"
+msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:93
+#: src/paths/instance/orders/details/DetailPage.tsx:144
+#, fuzzy, c-format
+msgid "Auto-refund delay"
+msgstr "Plazo de reembolso automático"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:145
#, c-format
-msgid "claimed"
-msgstr "reclamado"
+msgid ""
+"how long the wallet should try to get an automatic refund for the purchase"
+msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:110
-#: src/paths/instance/orders/details/DetailPage.tsx:261
-#: src/paths/instance/orders/list/Table.tsx:136
+#: src/paths/instance/orders/details/DetailPage.tsx:150
+#, fuzzy, c-format
+msgid "Extra info"
+msgstr "Información extra"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:151
#, c-format
-msgid "copy url"
-msgstr "copiar url"
+msgid "extra data that is only interpreted by the merchant frontend"
+msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:126
-#: src/paths/instance/orders/details/DetailPage.tsx:349
+#: src/paths/instance/orders/details/DetailPage.tsx:219
#, c-format
-msgid "pay at"
-msgstr "pagar en"
+msgid "Order"
+msgstr "Orden"
-#: src/paths/instance/orders/details/DetailPage.tsx:127
-#: src/paths/instance/orders/details/DetailPage.tsx:350
+#: src/paths/instance/orders/details/DetailPage.tsx:221
#, c-format
-msgid "created at"
-msgstr "creado"
+msgid "claimed"
+msgstr "reclamado"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:247
+#, fuzzy, c-format
+msgid "claimed at"
+msgstr "reclamado"
-#: src/paths/instance/orders/details/DetailPage.tsx:138
-#: src/paths/instance/orders/details/DetailPage.tsx:289
+#: src/paths/instance/orders/details/DetailPage.tsx:265
#, c-format
msgid "Timeline"
msgstr "Cronología"
-#: src/paths/instance/orders/details/DetailPage.tsx:142
-#: src/paths/instance/orders/details/DetailPage.tsx:293
+#: src/paths/instance/orders/details/DetailPage.tsx:271
#, c-format
msgid "Payment details"
msgstr "Detalles de pago"
-#: src/paths/instance/orders/details/DetailPage.tsx:146
-#: src/paths/instance/orders/details/DetailPage.tsx:299
-#: src/paths/instance/orders/details/DetailPage.tsx:363
-#, fuzzy, c-format
+#: src/paths/instance/orders/details/DetailPage.tsx:291
+#, c-format
msgid "Order status"
msgstr "Estado de orden"
-#: src/paths/instance/orders/details/DetailPage.tsx:156
-#: src/paths/instance/orders/details/DetailPage.tsx:308
-#, fuzzy, c-format
+#: src/paths/instance/orders/details/DetailPage.tsx:301
+#, c-format
msgid "Product list"
msgstr "Lista de producto"
-#: src/paths/instance/orders/details/DetailPage.tsx:236
+#: src/paths/instance/orders/details/DetailPage.tsx:451
#, c-format
msgid "paid"
msgstr "pagados"
-#: src/paths/instance/orders/details/DetailPage.tsx:238
+#: src/paths/instance/orders/details/DetailPage.tsx:455
#, c-format
msgid "wired"
msgstr "transferido"
-#: src/paths/instance/orders/details/DetailPage.tsx:241
+#: src/paths/instance/orders/details/DetailPage.tsx:460
#, c-format
msgid "refunded"
msgstr "reembolzado"
-#: src/paths/instance/orders/details/DetailPage.tsx:258
+#: src/paths/instance/orders/details/DetailPage.tsx:480
+#, fuzzy, c-format
+msgid "refund order"
+msgstr "reembolzado"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:481
+#, fuzzy, c-format
+msgid "not refundable"
+msgstr "Máximo reembolzable:"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:489
#, c-format
msgid "refund"
msgstr "reembolzar"
-#: src/paths/instance/orders/details/DetailPage.tsx:297
+#: src/paths/instance/orders/details/DetailPage.tsx:553
#, c-format
msgid "Refunded amount"
msgstr "Monto reembolzado"
-#: src/paths/instance/orders/details/DetailPage.tsx:298
-#, c-format
-msgid "Deposit total"
-msgstr "Total depositado"
+#: src/paths/instance/orders/details/DetailPage.tsx:560
+#, fuzzy, c-format
+msgid "Refund taken"
+msgstr "Reembolzado"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:570
+#, fuzzy, c-format
+msgid "Status URL"
+msgstr "URL de estado de orden"
-#: src/paths/instance/orders/details/DetailPage.tsx:336
+#: src/paths/instance/orders/details/DetailPage.tsx:583
+#, fuzzy, c-format
+msgid "Refund URI"
+msgstr "Devolución"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:636
#, c-format
msgid "unpaid"
msgstr "impago"
-#: src/paths/instance/orders/details/DetailPage.tsx:364
+#: src/paths/instance/orders/details/DetailPage.tsx:654
+#, c-format
+msgid "pay at"
+msgstr "pagar en"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:666
+#, c-format
+msgid "created at"
+msgstr "creado"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:707
#, c-format
msgid "Order status URL"
msgstr "URL de estado de orden"
-#: src/paths/instance/orders/details/DetailPage.tsx:365
-#, c-format
-msgid "Pay URI"
+#: src/paths/instance/orders/details/DetailPage.tsx:711
+#, fuzzy, c-format
+msgid "Payment URI"
msgstr "URI de pago"
-#: src/paths/instance/orders/details/DetailPage.tsx:383
+#: src/paths/instance/orders/details/DetailPage.tsx:740
#, c-format
msgid ""
"Unknown order status. This is an error, please contact the administrator."
msgstr ""
"Estado de orden desconocido. Esto es un error, por favor contacte a su "
-"administrador"
+"administrador."
-#: src/paths/instance/orders/details/index.tsx:56
-#: src/paths/instance/orders/list/index.tsx:147
+#: src/paths/instance/orders/details/DetailPage.tsx:767
+#, c-format
+msgid "Back"
+msgstr ""
+
+#: src/paths/instance/orders/details/index.tsx:79
#, c-format
msgid "refund created successfully"
msgstr "reembolzo creado satisfactoriamente"
-#: src/paths/instance/orders/details/index.tsx:59
-#: src/paths/instance/orders/list/index.tsx:150
-#, fuzzy, c-format
+#: src/paths/instance/orders/details/index.tsx:85
+#, c-format
msgid "could not create the refund"
-msgstr "No se pudo aceder al servidor"
+msgstr "No se pudo create el reembolso"
-#: src/paths/instance/orders/list/Table.tsx:111
+#: src/paths/instance/orders/list/ListPage.tsx:78
#, c-format
-msgid "load newer orders"
-msgstr "cargar nuevas ordenes"
+msgid "select date to show nearby orders"
+msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:115
-#, c-format
-msgid "Date"
-msgstr "Fecha"
+#: src/paths/instance/orders/list/ListPage.tsx:94
+#, fuzzy, c-format
+msgid "order id"
+msgstr "ir a id de orden"
-#: src/paths/instance/orders/list/Table.tsx:131
-#: src/paths/instance/orders/list/Table.tsx:223
+#: src/paths/instance/orders/list/ListPage.tsx:100
#, c-format
-msgid "Refund"
-msgstr "Reembolzar"
+msgid "jump to order with the given order ID"
+msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:145
+#: src/paths/instance/orders/list/ListPage.tsx:122
#, c-format
-msgid "load older orders"
-msgstr "cargar viejas ordenes"
+msgid "remove all filters"
+msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:154
+#: src/paths/instance/orders/list/ListPage.tsx:132
#, c-format
-msgid "No orders has been found"
-msgstr "No se enconraron ordenes"
+msgid "only show paid orders"
+msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:202
+#: src/paths/instance/orders/list/ListPage.tsx:135
#, c-format
-msgid "date"
-msgstr "fecha"
+msgid "Paid"
+msgstr "Pagado"
+
+#: src/paths/instance/orders/list/ListPage.tsx:142
+#, fuzzy, c-format
+msgid "only show orders with refunds"
+msgstr "No se pudo create el reembolso"
-#: src/paths/instance/orders/list/Table.tsx:203
+#: src/paths/instance/orders/list/ListPage.tsx:145
#, c-format
-msgid "amount"
-msgstr "monto"
+msgid "Refunded"
+msgstr "Reembolsado"
-#: src/paths/instance/orders/list/Table.tsx:204
+#: src/paths/instance/orders/list/ListPage.tsx:152
#, c-format
-msgid "reason"
-msgstr "razón"
+msgid ""
+"only show orders where customers paid, but wire payments from payment "
+"provider are still pending"
+msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:224
+#: src/paths/instance/orders/list/ListPage.tsx:155
#, c-format
-msgid "Max refundable:"
-msgstr "Máximo reembolzable:"
+msgid "Not wired"
+msgstr "No transferido"
-#: src/paths/instance/orders/list/Table.tsx:226
+#: src/paths/instance/orders/list/ListPage.tsx:170
#, c-format
-msgid "Reason"
-msgstr "Razón"
+msgid "clear date filter"
+msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:226
+#: src/paths/instance/orders/list/ListPage.tsx:184
#, c-format
-msgid "duplicated"
-msgstr "duplicado"
+msgid "date (YYYY/MM/DD)"
+msgstr ""
+
+#: src/paths/instance/orders/list/index.tsx:103
+#, fuzzy, c-format
+msgid "Enter an order id"
+msgstr "ir a id de orden"
-#: src/paths/instance/orders/list/Table.tsx:226
+#: src/paths/instance/orders/list/index.tsx:111
+#, fuzzy, c-format
+msgid "order not found"
+msgstr "Servidor no encontrado"
+
+#: src/paths/instance/orders/list/index.tsx:178
+#, fuzzy, c-format
+msgid "could not get the order to refund"
+msgstr "No se pudo create el reembolso"
+
+#: src/components/exception/AsyncButton.tsx:43
+#, fuzzy, c-format
+msgid "Loading..."
+msgstr "Cargando..."
+
+#: src/components/form/InputStock.tsx:99
#, c-format
-msgid "requested by the customer"
-msgstr "pedido por el consumidor"
+msgid ""
+"click here to configure the stock of the product, leave it as is and the "
+"backend will not control stock"
+msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:226
+#: src/components/form/InputStock.tsx:109
#, c-format
-msgid "other"
-msgstr "otro"
+msgid "Manage stock"
+msgstr "Administrar stock"
-#: src/paths/instance/orders/list/index.tsx:91
+#: src/components/form/InputStock.tsx:115
#, c-format
-msgid "go to order id"
-msgstr "ir a id de orden"
+msgid "this product has been configured without stock control"
+msgstr ""
-#: src/paths/instance/orders/list/index.tsx:107
+#: src/components/form/InputStock.tsx:119
#, c-format
-msgid "Paid"
-msgstr "Pagado"
+msgid "Infinite"
+msgstr "Inifinito"
-#: src/paths/instance/orders/list/index.tsx:108
+#: src/components/form/InputStock.tsx:136
#, fuzzy, c-format
-msgid "Refunded"
-msgstr "Reembolzado"
+msgid "lost cannot be greater than current and incoming (max %1$s)"
+msgstr "la pérdida no puede ser mayor al stock actual + entrante (max %1$s )"
-#: src/paths/instance/orders/list/index.tsx:109
-#, fuzzy, c-format
-msgid "Not wired"
-msgstr "No transferido"
+#: src/components/form/InputStock.tsx:176
+#, c-format
+msgid "Incoming"
+msgstr "Ingresando"
-#: src/paths/instance/orders/list/index.tsx:110
+#: src/components/form/InputStock.tsx:177
#, c-format
-msgid "All"
-msgstr "Todo"
+msgid "Lost"
+msgstr "Perdido"
-#: src/paths/instance/products/create/index.tsx:48
-#: src/paths/instance/products/update/index.tsx:64
+#: src/components/form/InputStock.tsx:192
+#, c-format
+msgid "Current"
+msgstr "Actual"
+
+#: src/components/form/InputStock.tsx:196
+#, c-format
+msgid "remove stock control for this product"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:202
+#, c-format
+msgid "without stock"
+msgstr "sin stock"
+
+#: src/components/form/InputStock.tsx:211
+#, c-format
+msgid "Next restock"
+msgstr "Próximo reabastecimiento"
+
+#: src/components/form/InputStock.tsx:217
+#, c-format
+msgid "Delivery address"
+msgstr "Dirección de entrega"
+
+#: src/components/product/ProductForm.tsx:133
+#, c-format
+msgid "product identification to use in URLs (for internal use only)"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:139
+#, c-format
+msgid "illustration of the product for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:145
+#, c-format
+msgid "product description for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:149
+#, c-format
+msgid "Age restricted"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:150
+#, c-format
+msgid "is this product restricted for customer below certain age?"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:155
+#, c-format
+msgid ""
+"unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 "
+"items, 5 meters) for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:160
+#, c-format
+msgid ""
+"sale price for customers, including taxes, for above units of the product"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:164
+#, c-format
+msgid "Stock"
+msgstr "Existencias"
+
+#: src/components/product/ProductForm.tsx:166
+#, c-format
+msgid ""
+"product inventory for products with finite supply (for internal use only)"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:171
+#, c-format
+msgid "taxes included in the product price, exposed to customers"
+msgstr ""
+
+#: src/paths/instance/products/create/CreatePage.tsx:66
+#, c-format
+msgid "Need to complete marked fields"
+msgstr ""
+
+#: src/paths/instance/products/create/index.tsx:51
#, c-format
msgid "could not create product"
msgstr "no se pudo crear el producto"
-#: src/paths/instance/products/list/Table.tsx:87
+#: src/paths/instance/products/list/Table.tsx:68
+#, c-format
+msgid "Products"
+msgstr "Productos"
+
+#: src/paths/instance/products/list/Table.tsx:73
+#, c-format
+msgid "add product to inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:137
#, c-format
msgid "Sell"
msgstr "Venta"
-#: src/paths/instance/products/list/Table.tsx:89
+#: src/paths/instance/products/list/Table.tsx:143
#, c-format
msgid "Profit"
msgstr "Ganancia"
-#: src/paths/instance/products/list/Table.tsx:91
+#: src/paths/instance/products/list/Table.tsx:149
#, c-format
msgid "Sold"
msgstr "Vendido"
-#: src/paths/instance/products/list/index.tsx:59
+#: src/paths/instance/products/list/Table.tsx:210
+#, c-format
+msgid "free"
+msgstr "Gratis"
+
+#: src/paths/instance/products/list/Table.tsx:248
+#, fuzzy, c-format
+msgid "go to product update page"
+msgstr "producto actualizado correctamente"
+
+#: src/paths/instance/products/list/Table.tsx:255
+#, c-format
+msgid "Update"
+msgstr "Actualizar"
+
+#: src/paths/instance/products/list/Table.tsx:260
+#, c-format
+msgid "remove this product from the database"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:331
+#, c-format
+msgid "update the product with new price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:341
+#, c-format
+msgid "update product with new price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:399
+#, c-format
+msgid "add more elements to the inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:404
+#, c-format
+msgid "report elements lost in the inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:409
+#, fuzzy, c-format
+msgid "new price for the product"
+msgstr "no se pudo actualizar el producto"
+
+#: src/paths/instance/products/list/Table.tsx:421
+#, c-format
+msgid "the are value with errors"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:422
+#, c-format
+msgid "update product with new stock and price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:463
+#, fuzzy, c-format
+msgid "There is no products yet, add more pressing the + sign"
+msgstr "No hay propinas todavía, agregar mas presionando el signo +"
+
+#: src/paths/instance/products/list/index.tsx:86
#, c-format
msgid "product updated successfully"
msgstr "producto actualizado correctamente"
-#: src/paths/instance/products/list/index.tsx:62
+#: src/paths/instance/products/list/index.tsx:92
#, c-format
msgid "could not update the product"
msgstr "no se pudo actualizar el producto"
-#: src/paths/instance/products/list/index.tsx:70
+#: src/paths/instance/products/list/index.tsx:103
#, c-format
msgid "product delete successfully"
msgstr "producto fue eliminado correctamente"
-#: src/paths/instance/products/list/index.tsx:73
+#: src/paths/instance/products/list/index.tsx:109
#, c-format
msgid "could not delete the product"
msgstr "no se pudo eliminar el producto"
-#: src/paths/instance/tips/list/Table.tsx:59
+#: src/paths/instance/products/update/UpdatePage.tsx:56
+#, fuzzy, c-format
+msgid "Product id:"
+msgstr "Id de producto"
+
+#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:95
+#, c-format
+msgid ""
+"To complete the setup of the reserve, you must now initiate a wire transfer "
+"using the given wire transfer subject and crediting the specified amount to "
+"the indicated account of the exchange."
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:102
+#, c-format
+msgid "If your system supports RFC 8905, you can do this by opening this URI:"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:83
+#, fuzzy, c-format
+msgid "it should be greater than 0"
+msgstr "Debe ser mayor a 0"
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:88
+#, c-format
+msgid "must be a valid URL"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:107
+#, fuzzy, c-format
+msgid "Initial balance"
+msgstr "Instancia"
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:108
+#, c-format
+msgid "balance prior to deposit"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:112
+#, c-format
+msgid "Exchange URL"
+msgstr "URL del Exchange"
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:113
+#, c-format
+msgid "URL of exchange"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:148
+#, c-format
+msgid "Next"
+msgstr "Siguiente"
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:186
+#, c-format
+msgid "Wire method"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:187
+#, fuzzy, c-format
+msgid "method to use for wire transfer"
+msgstr "no se pudo informar la transferencia"
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:189
+#, c-format
+msgid "Select one wire method"
+msgstr ""
+
+#: src/paths/instance/reserves/create/index.tsx:62
+#, fuzzy, c-format
+msgid "could not create reserve"
+msgstr "No se pudo create el reembolso"
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:77
+#, c-format
+msgid "Valid until"
+msgstr "Válido hasta"
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:82
+#, fuzzy, c-format
+msgid "Created balance"
+msgstr "creado"
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:99
+#, fuzzy, c-format
+msgid "Exchange balance"
+msgstr "Monto inicial"
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:104
+#, c-format
+msgid "Picked up"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:109
+#, fuzzy, c-format
+msgid "Committed"
+msgstr "Monto confirmado"
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:116
+#, c-format
+msgid "Account address"
+msgstr "Dirección de cuenta"
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:119
+#, c-format
+msgid "Subject"
+msgstr "Asunto"
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:130
#, c-format
msgid "Tips"
msgstr "Propinas"
-#: src/paths/instance/tips/list/Table.tsx:111
+#: src/paths/instance/reserves/details/DetailPage.tsx:193
#, c-format
-msgid "Committed amount"
+msgid "No tips has been authorized from this reserve"
msgstr ""
-#: src/paths/instance/tips/list/Table.tsx:112
+#: src/paths/instance/reserves/details/DetailPage.tsx:213
+#, fuzzy, c-format
+msgid "Authorized"
+msgstr "Token de autorización"
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:222
+#, fuzzy, c-format
+msgid "Expiration"
+msgstr "Información extra"
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:108
+#, fuzzy, c-format
+msgid "amount of tip"
+msgstr "monto"
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:112
+#, fuzzy, c-format
+msgid "Justification"
+msgstr "Jurisdicción"
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:114
#, c-format
-msgid "Exchange initial amount"
+msgid "reason for the tip"
msgstr ""
-#: src/paths/instance/tips/list/Table.tsx:113
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:118
#, c-format
-msgid "Merchant initial amount"
+msgid "URL after tip"
msgstr ""
-#: src/paths/instance/tips/list/Table.tsx:148
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:119
#, c-format
-msgid "There is no tips yet, add more pressing the + sign"
-msgstr "No hay propinas todavía, agregar mas presionando el signo +"
+msgid "URL to visit after tip payment"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:65
+#, fuzzy, c-format
+msgid "Reserves not yet funded"
+msgstr "Servidor no encontrado"
-#: src/paths/instance/transfers/create/CreatePage.tsx:50
-#: src/paths/instance/transfers/create/CreatePage.tsx:54
-#: src/paths/instance/transfers/create/CreatePage.tsx:55
-#: src/paths/instance/transfers/create/CreatePage.tsx:56
+#: src/paths/instance/reserves/list/Table.tsx:89
#, c-format
-msgid "cannot be empty"
+msgid "Reserves ready"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:95
+#, fuzzy, c-format
+msgid "add new reserve"
+msgstr "cargar nuevas transferencias"
+
+#: src/paths/instance/reserves/list/Table.tsx:143
+#, c-format
+msgid "Expires at"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:146
+#, c-format
+msgid "Initial"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:202
+#, c-format
+msgid "delete selected reserve from the database"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:210
+#, c-format
+msgid "authorize new tip from selected reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:237
+#, fuzzy, c-format
+msgid ""
+"There is no ready reserves yet, add more pressing the + sign or fund them"
+msgstr "No hay transferencias todavía, agregar mas presionando el signo +"
+
+#: src/paths/instance/reserves/list/Table.tsx:264
+#, fuzzy, c-format
+msgid "Expected Balance"
+msgstr "Ejecutado en"
+
+#: src/paths/instance/reserves/list/index.tsx:110
+#, fuzzy, c-format
+msgid "could not create the tip"
+msgstr "No se pudo create el reembolso"
+
+#: src/paths/instance/templates/create/CreatePage.tsx:77
+#, fuzzy, c-format
+msgid "should not be empty"
+msgstr "no puede ser vacío"
+
+#: src/paths/instance/templates/create/CreatePage.tsx:93
+#, fuzzy, c-format
+msgid "should be greater that 0"
+msgstr "Debe ser mayor a 0"
+
+#: src/paths/instance/templates/create/CreatePage.tsx:96
+#, fuzzy, c-format
+msgid "can't be empty"
msgstr "no puede ser vacío"
-#: src/paths/instance/transfers/create/CreatePage.tsx:51
+#: src/paths/instance/templates/create/CreatePage.tsx:100
+#, c-format
+msgid "to short"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:108
+#, c-format
+msgid "just letters and numbers from 2 to 7"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:110
+#, c-format
+msgid "size of the key should be 32"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:137
+#, c-format
+msgid "Identifier"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:138
+#, c-format
+msgid "Name of the template in URLs."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:144
+#, c-format
+msgid "Describe what this template stands for"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:149
+#, fuzzy, c-format
+msgid "Fixed summary"
+msgstr "Estado de orden"
+
+#: src/paths/instance/templates/create/CreatePage.tsx:150
+#, c-format
+msgid "If specified, this template will create order with the same summary"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:154
+#, fuzzy, c-format
+msgid "Fixed price"
+msgstr "precio unitario"
+
+#: src/paths/instance/templates/create/CreatePage.tsx:155
+#, c-format
+msgid "If specified, this template will create order with the same price"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:159
+#, c-format
+msgid "Minimum age"
+msgstr "Edad mínima"
+
+#: src/paths/instance/templates/create/CreatePage.tsx:161
+#, c-format
+msgid "Is this contract restricted to some age?"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:165
+#, fuzzy, c-format
+msgid "Payment timeout"
+msgstr "Opciones de pago"
+
+#: src/paths/instance/templates/create/CreatePage.tsx:167
+#, c-format
+msgid ""
+"How much time has the customer to complete the payment once the order was "
+"created."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:171
+#, c-format
+msgid "Verification algorithm"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:172
+#, c-format
+msgid "Algorithm to use to verify transaction in offline mode"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:180
+#, c-format
+msgid "Point-of-sale key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:182
+#, c-format
+msgid "Useful to validate the purchase"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:196
+#, c-format
+msgid "generate random secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:203
+#, c-format
+msgid "random"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:208
+#, c-format
+msgid "show secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:209
+#, c-format
+msgid "hide secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:216
+#, c-format
+msgid "hide"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:218
+#, c-format
+msgid "show"
+msgstr ""
+
+#: src/paths/instance/templates/create/index.tsx:52
+#, fuzzy, c-format
+msgid "could not inform template"
+msgstr "no se pudo informar la transferencia"
+
+#: src/paths/instance/templates/use/UsePage.tsx:54
+#, fuzzy, c-format
+msgid "Amount is required"
+msgstr "Login necesario"
+
+#: src/paths/instance/templates/use/UsePage.tsx:58
+#, c-format
+msgid "Order summary is required"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:86
+#, fuzzy, c-format
+msgid "New order for template"
+msgstr "cargar viejas transferencias"
+
+#: src/paths/instance/templates/use/UsePage.tsx:108
+#, c-format
+msgid "Amount of the order"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:113
+#, fuzzy, c-format
+msgid "Order summary"
+msgstr "Estado de orden"
+
+#: src/paths/instance/templates/use/index.tsx:92
+#, fuzzy, c-format
+msgid "could not create order from template"
+msgstr "No se pudo create el reembolso"
+
+#: src/paths/instance/templates/qr/QrPage.tsx:131
+#, c-format
+msgid ""
+"Here you can specify a default value for fields that are not fixed. Default "
+"values can be edited by the customer before the payment."
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:148
+#, fuzzy, c-format
+msgid "Fixed amount"
+msgstr "Monto reembolzado"
+
+#: src/paths/instance/templates/qr/QrPage.tsx:149
+#, fuzzy, c-format
+msgid "Default amount"
+msgstr "Monto reembolzado"
+
+#: src/paths/instance/templates/qr/QrPage.tsx:161
+#, fuzzy, c-format
+msgid "Default summary"
+msgstr "Estado de orden"
+
+#: src/paths/instance/templates/qr/QrPage.tsx:177
+#, c-format
+msgid "Print"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:184
+#, c-format
+msgid "Setup TOTP"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:65
+#, c-format
+msgid "Templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:70
+#, c-format
+msgid "add new templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:142
+#, c-format
+msgid "load more templates before the first one"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:146
+#, fuzzy, c-format
+msgid "load newer templates"
+msgstr "cargar nuevas transferencias"
+
+#: src/paths/instance/templates/list/Table.tsx:181
+#, c-format
+msgid "delete selected templates from the database"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:188
+#, c-format
+msgid "use template to create new order"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:195
+#, fuzzy, c-format
+msgid "create qr code for the template"
+msgstr "No se pudo create el reembolso"
+
+#: src/paths/instance/templates/list/Table.tsx:210
+#, c-format
+msgid "load more templates after the last one"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:214
+#, fuzzy, c-format
+msgid "load older templates"
+msgstr "cargar viejas transferencias"
+
+#: src/paths/instance/templates/list/Table.tsx:231
+#, fuzzy, c-format
+msgid "There is no templates yet, add more pressing the + sign"
+msgstr "No hay propinas todavía, agregar mas presionando el signo +"
+
+#: src/paths/instance/templates/list/index.tsx:104
+#, fuzzy, c-format
+msgid "template delete successfully"
+msgstr "producto fue eliminado correctamente"
+
+#: src/paths/instance/templates/list/index.tsx:110
+#, fuzzy, c-format
+msgid "could not delete the template"
+msgstr "no se pudo eliminar el producto"
+
+#: src/paths/instance/templates/update/index.tsx:90
+#, fuzzy, c-format
+msgid "could not update template"
+msgstr "no se pudo actualizar el producto"
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:57
+#, fuzzy, c-format
+msgid "should be one of '%1$s'"
+msgstr "deberían ser iguales"
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:85
+#, c-format
+msgid "Webhook ID to use"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:89
+#, c-format
+msgid "Event"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:90
+#, c-format
+msgid "The event of the webhook: why the webhook is used"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:94
#, c-format
-msgid "check the id, doest look valid"
+msgid "Method"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:95
+#, c-format
+msgid "Method used by the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:99
+#, c-format
+msgid "URL"
+msgstr "URL"
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:100
+#, c-format
+msgid "URL of the webhook where the customer will be redirected"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:104
+#, c-format
+msgid "Header"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:106
+#, c-format
+msgid "Header template of the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:111
+#, c-format
+msgid "Body"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:112
+#, c-format
+msgid "Body template by the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:61
+#, c-format
+msgid "Webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:66
+#, c-format
+msgid "add new webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:137
+#, c-format
+msgid "load more webhooks before the first one"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:141
+#, fuzzy, c-format
+msgid "load newer webhooks"
+msgstr "cargar nuevas ordenes"
+
+#: src/paths/instance/webhooks/list/Table.tsx:151
+#, c-format
+msgid "Event type"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:176
+#, c-format
+msgid "delete selected webhook from the database"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:198
+#, c-format
+msgid "load more webhooks after the last one"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:202
+#, fuzzy, c-format
+msgid "load older webhooks"
+msgstr "cargar viejas ordenes"
+
+#: src/paths/instance/webhooks/list/Table.tsx:219
+#, fuzzy, c-format
+msgid "There is no webhooks yet, add more pressing the + sign"
+msgstr "No hay propinas todavía, agregar mas presionando el signo +"
+
+#: src/paths/instance/webhooks/list/index.tsx:94
+#, fuzzy, c-format
+msgid "webhook delete successfully"
+msgstr "producto fue eliminado correctamente"
+
+#: src/paths/instance/webhooks/list/index.tsx:100
+#, fuzzy, c-format
+msgid "could not delete the webhook"
+msgstr "no se pudo eliminar el producto"
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:63
+#, fuzzy, c-format
+msgid "check the id, does not look valid"
msgstr "verificar el id, no parece válido"
-#: src/paths/instance/transfers/create/CreatePage.tsx:52
+#: src/paths/instance/transfers/create/CreatePage.tsx:65
#, c-format
msgid "should have 52 characters, current %1$s"
msgstr "debería tener 52 caracteres, actualmente %1$s"
-#: src/paths/instance/transfers/create/CreatePage.tsx:57
+#: src/paths/instance/transfers/create/CreatePage.tsx:72
#, c-format
msgid "URL doesn't have the right format"
msgstr "La URL no tiene el formato correcto"
-#: src/paths/instance/transfers/create/CreatePage.tsx:74
-#, fuzzy, c-format
-msgid "Transfer ID"
-msgstr "Transferencias"
+#: src/paths/instance/transfers/create/CreatePage.tsx:98
+#, c-format
+msgid "Credited bank account"
+msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:76
-#, fuzzy, c-format
-msgid "Account Address"
-msgstr "Dirección de cuenta"
+#: src/paths/instance/transfers/create/CreatePage.tsx:100
+#, c-format
+msgid "Select one account"
+msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:82
-#: src/paths/instance/transfers/list/Table.tsx:125
+#: src/paths/instance/transfers/create/CreatePage.tsx:101
#, c-format
-msgid "Exchange URL"
-msgstr "URL del Exchange"
+msgid "Bank account of the merchant where the payment was received"
+msgstr ""
-#: src/paths/instance/transfers/create/index.tsx:49
+#: src/paths/instance/transfers/create/CreatePage.tsx:105
#, fuzzy, c-format
+msgid "Wire transfer ID"
+msgstr "Id de transferencia"
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:107
+#, c-format
+msgid ""
+"unique identifier of the wire transfer used by the exchange, must be 52 "
+"characters long"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:112
+#, c-format
+msgid ""
+"Base URL of the exchange that made the transfer, should have been in the "
+"wire transfer subject"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:117
+#, c-format
+msgid "Amount credited"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:118
+#, c-format
+msgid "Actual amount that was wired to the merchant's bank account"
+msgstr ""
+
+#: src/paths/instance/transfers/create/index.tsx:58
+#, c-format
msgid "could not inform transfer"
-msgstr "no se pudo crear la instancia"
+msgstr "no se pudo informar la transferencia"
-#: src/paths/instance/transfers/list/Table.tsx:118
+#: src/paths/instance/transfers/list/Table.tsx:61
+#, c-format
+msgid "Transfers"
+msgstr "Transferencias"
+
+#: src/paths/instance/transfers/list/Table.tsx:66
#, fuzzy, c-format
+msgid "add new transfer"
+msgstr "cargar nuevas transferencias"
+
+#: src/paths/instance/transfers/list/Table.tsx:129
+#, c-format
+msgid "load more transfers before the first one"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:133
+#, c-format
msgid "load newer transfers"
-msgstr "cargar nuevas ordenes"
+msgstr "cargar nuevas transferencias"
-#: src/paths/instance/transfers/list/Table.tsx:123
+#: src/paths/instance/transfers/list/Table.tsx:143
#, c-format
msgid "Credit"
msgstr "Crédito"
-#: src/paths/instance/transfers/list/Table.tsx:126
-#, fuzzy, c-format
+#: src/paths/instance/transfers/list/Table.tsx:152
+#, c-format
msgid "Confirmed"
-msgstr "Confirmar"
+msgstr "Confirmado"
-#: src/paths/instance/transfers/list/Table.tsx:127
-#: src/paths/instance/transfers/list/index.tsx:60
+#: src/paths/instance/transfers/list/Table.tsx:155
#, c-format
msgid "Verified"
msgstr "Verificado"
-#: src/paths/instance/transfers/list/Table.tsx:128
-#, fuzzy, c-format
+#: src/paths/instance/transfers/list/Table.tsx:158
+#, c-format
msgid "Executed at"
-msgstr "creado"
+msgstr "Ejecutado en"
-#: src/paths/instance/transfers/list/Table.tsx:138
-#: src/paths/instance/transfers/list/Table.tsx:139
+#: src/paths/instance/transfers/list/Table.tsx:171
#, c-format
msgid "yes"
msgstr "si"
-#: src/paths/instance/transfers/list/Table.tsx:138
-#: src/paths/instance/transfers/list/Table.tsx:139
+#: src/paths/instance/transfers/list/Table.tsx:171
#, c-format
msgid "no"
msgstr "no"
-#: src/paths/instance/transfers/list/Table.tsx:140
+#: src/paths/instance/transfers/list/Table.tsx:181
#, c-format
msgid "unknown"
msgstr "desconocido"
-#: src/paths/instance/transfers/list/Table.tsx:145
-#, fuzzy, c-format
+#: src/paths/instance/transfers/list/Table.tsx:187
+#, c-format
+msgid "delete selected transfer from the database"
+msgstr "eliminar transferencia seleccionada de la base de datos"
+
+#: src/paths/instance/transfers/list/Table.tsx:202
+#, c-format
+msgid "load more transfer after the last one"
+msgstr "cargue más transferencia luego de la última"
+
+#: src/paths/instance/transfers/list/Table.tsx:206
+#, c-format
msgid "load older transfers"
msgstr "cargar viejas transferencias"
-#: src/paths/instance/transfers/list/Table.tsx:154
+#: src/paths/instance/transfers/list/Table.tsx:223
#, c-format
msgid "There is no transfer yet, add more pressing the + sign"
msgstr "No hay transferencias todavía, agregar mas presionando el signo +"
+
+#: src/paths/instance/transfers/list/ListPage.tsx:79
+#, fuzzy, c-format
+msgid "filter by account address"
+msgstr "Dirección de cuenta"
+
+#: src/paths/instance/transfers/list/ListPage.tsx:100
+#, c-format
+msgid "only show wire transfers confirmed by the merchant"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:110
+#, c-format
+msgid "only show wire transfers claimed by the exchange"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:113
+#, fuzzy, c-format
+msgid "Unverified"
+msgstr "Verificado"
+
+#: src/paths/admin/create/CreatePage.tsx:69
+#, c-format
+msgid "is not valid"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:94
+#, fuzzy, c-format
+msgid "is not a number"
+msgstr "Número de edificio"
+
+#: src/paths/admin/create/CreatePage.tsx:96
+#, c-format
+msgid "must be 1 or greater"
+msgstr "debe ser 1 o mayor"
+
+#: src/paths/admin/create/CreatePage.tsx:107
+#, c-format
+msgid "max 7 lines"
+msgstr "máximo 7 líneas"
+
+#: src/paths/admin/create/CreatePage.tsx:178
+#, c-format
+msgid "change authorization configuration"
+msgstr "cambiar configuración de autorización"
+
+#: src/paths/admin/create/CreatePage.tsx:217
+#, c-format
+msgid "Need to complete marked fields and choose authorization method"
+msgstr "Necesita completar campos marcados y escoger un método de autorización"
+
+#: src/components/form/InputPaytoForm.tsx:82
+#, c-format
+msgid "This is not a valid bitcoin address."
+msgstr "Esta no es una dirección de bitcoin válida."
+
+#: src/components/form/InputPaytoForm.tsx:95
+#, c-format
+msgid "This is not a valid Ethereum address."
+msgstr "Esta no es una dirección de Ethereum válida."
+
+#: src/components/form/InputPaytoForm.tsx:118
+#, c-format
+msgid "IBAN numbers usually have more that 4 digits"
+msgstr "Los números IBAN usualmente tienen mas de 4 digitos"
+
+#: src/components/form/InputPaytoForm.tsx:120
+#, c-format
+msgid "IBAN numbers usually have less that 34 digits"
+msgstr "Los números IBAN usualmente tienen menos de 34 digitos"
+
+#: src/components/form/InputPaytoForm.tsx:128
+#, c-format
+msgid "IBAN country code not found"
+msgstr "Código de pais de IBAN no encontrado"
+
+#: src/components/form/InputPaytoForm.tsx:153
+#, c-format
+msgid "IBAN number is not valid, checksum is wrong"
+msgstr "El número IBAN no es válido, falló la verificación"
+
+#: src/components/form/InputPaytoForm.tsx:248
+#, c-format
+msgid "Target type"
+msgstr "Tipo objetivo"
+
+#: src/components/form/InputPaytoForm.tsx:249
+#, c-format
+msgid "Method to use for wire transfer"
+msgstr "Método a usar para la transferencia"
+
+#: src/components/form/InputPaytoForm.tsx:258
+#, c-format
+msgid "Routing"
+msgstr "Enrutamiento"
+
+#: src/components/form/InputPaytoForm.tsx:259
+#, c-format
+msgid "Routing number."
+msgstr "Número de enrutamiento."
+
+#: src/components/form/InputPaytoForm.tsx:263
+#, c-format
+msgid "Account"
+msgstr "Cuenta"
+
+#: src/components/form/InputPaytoForm.tsx:264
+#, fuzzy, c-format
+msgid "Account number."
+msgstr "Dirección de cuenta"
+
+#: src/components/form/InputPaytoForm.tsx:273
+#, c-format
+msgid "Business Identifier Code."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:282
+#, c-format
+msgid "Bank Account Number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:292
+#, c-format
+msgid "Unified Payment Interface."
+msgstr "Interfaz de pago unificado."
+
+#: src/components/form/InputPaytoForm.tsx:301
+#, c-format
+msgid "Bitcoin protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:310
+#, c-format
+msgid "Ethereum protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:319
+#, c-format
+msgid "Interledger protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:328
+#, c-format
+msgid "Host"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:329
+#, c-format
+msgid "Bank host."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:334
+#, c-format
+msgid "Bank account."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:343
+#, c-format
+msgid "Bank account owner's name."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:370
+#, c-format
+msgid "No accounts yet."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:52
+#, c-format
+msgid ""
+"Name of the instance in URLs. The 'default' instance is special in that it "
+"is used to administer other instances."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:58
+#, fuzzy, c-format
+msgid "Business name"
+msgstr "Nombre de edificio"
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:59
+#, c-format
+msgid "Legal name of the business represented by this instance."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:64
+#, c-format
+msgid "Email"
+msgstr "Correo eletrónico"
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:65
+#, c-format
+msgid "Contact email"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:70
+#, c-format
+msgid "Website URL"
+msgstr "URL de sitio web"
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:71
+#, c-format
+msgid "URL."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:76
+#, c-format
+msgid "Logo"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:77
+#, c-format
+msgid "Logo image."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:82
+#, c-format
+msgid "Bank account"
+msgstr "Cuenta bancaria"
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:83
+#, c-format
+msgid "URI specifying bank account for crediting revenue."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:88
+#, c-format
+msgid "Default max deposit fee"
+msgstr "Impuesto máximo de deposito por omisión"
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:89
+#, c-format
+msgid ""
+"Maximum deposit fees this merchant is willing to pay per order by default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:94
+#, c-format
+msgid "Default max wire fee"
+msgstr "Impuesto máximo de transferencia por omisión"
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:95
+#, c-format
+msgid ""
+"Maximum wire fees this merchant is willing to pay per wire transfer by "
+"default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:100
+#, c-format
+msgid "Default wire fee amortization"
+msgstr "Amortización de impuesto de transferencia por omisión"
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:101
+#, c-format
+msgid ""
+"Number of orders excess wire transfer fees will be divided by to compute per "
+"order surcharge."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:107
+#, c-format
+msgid "Physical location of the merchant."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:114
+#, c-format
+msgid "Jurisdiction"
+msgstr "Jurisdicción"
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:115
+#, c-format
+msgid "Jurisdiction for legal disputes with the merchant."
+msgstr "Jurisdicción para disputas legales con el comerciante."
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:122
+#, fuzzy, c-format
+msgid "Default payment delay"
+msgstr "Retrazo de pago por omisión"
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:124
+#, c-format
+msgid ""
+"Time customers have to pay an order before the offer expires by default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:129
+#, c-format
+msgid "Default wire transfer delay"
+msgstr "Retrazo de transferencia por omisión"
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:130
+#, c-format
+msgid ""
+"Maximum time an exchange is allowed to delay wiring funds to the merchant, "
+"enabling it to aggregate smaller payments into larger wire transfers and "
+"reducing wire fees."
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:164
+#, c-format
+msgid "Instance id"
+msgstr "ID de instancia"
+
+#: src/paths/instance/update/UpdatePage.tsx:173
+#, fuzzy, c-format
+msgid "Change the authorization method use for this instance."
+msgstr ""
+"Limpiar el token de autorización significa acceso público a la instancia"
+
+#: src/paths/instance/update/UpdatePage.tsx:182
+#, c-format
+msgid "Manage access token"
+msgstr "Administrar token de acceso"
+
+#: src/paths/instance/update/index.tsx:112
+#, c-format
+msgid "Failed to create instance"
+msgstr "Fallo al crear la instancia"
+
+#: src/components/exception/login.tsx:74
+#, c-format
+msgid "Login required"
+msgstr "Login necesario"
+
+#: src/components/exception/login.tsx:80
+#, c-format
+msgid "Please enter your access token."
+msgstr ""
+
+#: src/components/exception/login.tsx:108
+#, fuzzy, c-format
+msgid "Access Token"
+msgstr "Acceso denegado"
+
+#: src/InstanceRoutes.tsx:171
+#, c-format
+msgid "The request to the backend take too long and was cancelled"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:172
+#, c-format
+msgid "Diagnostic from %1$s is \"%2$s\""
+msgstr ""
+
+#: src/InstanceRoutes.tsx:178
+#, fuzzy, c-format
+msgid "The backend reported a problem: HTTP status #%1$s"
+msgstr "Servidir reporto un problema: HTTP status #%1$s"
+
+#: src/InstanceRoutes.tsx:179
+#, c-format
+msgid "Diagnostic from %1$s is '%2$s'"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:196
+#, c-format
+msgid "Access denied"
+msgstr "Acceso denegado"
+
+#: src/InstanceRoutes.tsx:197
+#, c-format
+msgid "The access token provided is invalid."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:212
+#, fuzzy, c-format
+msgid "No 'default' instance configured yet."
+msgstr "Sin instancia default"
+
+#: src/InstanceRoutes.tsx:213
+#, c-format
+msgid "Create a 'default' instance to begin using the merchant backoffice."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:630
+#, c-format
+msgid "The access token provided is invalid"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:664
+#, c-format
+msgid "Hide for today"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:82
+#, c-format
+msgid "Instance"
+msgstr "Instancia"
+
+#: src/components/menu/SideBar.tsx:91
+#, c-format
+msgid "Settings"
+msgstr "Configuración"
+
+#: src/components/menu/SideBar.tsx:167
+#, c-format
+msgid "Connection"
+msgstr "Conexión"
+
+#: src/components/menu/SideBar.tsx:209
+#, c-format
+msgid "New"
+msgstr "Nuevo"
+
+#: src/components/menu/SideBar.tsx:219
+#, c-format
+msgid "List"
+msgstr "Lista"
+
+#: src/components/menu/SideBar.tsx:234
+#, c-format
+msgid "Log out"
+msgstr "Salir"
+
+#: src/ApplicationReadyRoutes.tsx:71
+#, c-format
+msgid "Check your token is valid"
+msgstr "Verifica que el token sea valido"
+
+#: src/ApplicationReadyRoutes.tsx:90
+#, c-format
+msgid "Couldn't access the server."
+msgstr "No se pudo acceder al servidor."
+
+#: src/ApplicationReadyRoutes.tsx:91
+#, c-format
+msgid "Could not infer instance id from url %1$s"
+msgstr "No se pudo inferir el id de la instancia con la url %1$s"
+
+#: src/Application.tsx:104
+#, c-format
+msgid "Server not found"
+msgstr "Servidor no encontrado"
+
+#: src/Application.tsx:118
+#, c-format
+msgid "Server response with an error code"
+msgstr ""
+
+#: src/Application.tsx:120
+#, c-format
+msgid "Got message %1$s from %2$s"
+msgstr "Recibimos el mensaje %1$s desde %2$s"
+
+#: src/Application.tsx:131
+#, c-format
+msgid "Response from server is unreadable, http status: %1$s"
+msgstr ""
+
+#: src/Application.tsx:144
+#, c-format
+msgid "Unexpected Error"
+msgstr "Error inesperado"
+
+#: src/components/form/InputArray.tsx:101
+#, c-format
+msgid "The value %1$s is invalid for a payment url"
+msgstr "El valor %1$s es invalido para una URL de pago"
+
+#: src/components/form/InputArray.tsx:110
+#, c-format
+msgid "add element to the list"
+msgstr "agregar elemento a la lista"
+
+#: src/components/form/InputArray.tsx:112
+#, c-format
+msgid "add"
+msgstr "Agregar"
+
+#: src/components/form/InputSecured.tsx:37
+#, c-format
+msgid "Deleting"
+msgstr "Borrando"
+
+#: src/components/form/InputSecured.tsx:41
+#, c-format
+msgid "Changing"
+msgstr "Cambiando"
+
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:87
+#, c-format
+msgid "Order ID"
+msgstr "ID de pedido"
+
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:101
+#, c-format
+msgid "Payment URL"
+msgstr "URL de pago"
+
+#, c-format
+#~ msgid "Couldn't access the server"
+#~ msgstr "No se pudo aceder al servidor"
+
+#, c-format
+#~ msgid "HTTP status #%1$s: Server reported a problem"
+#~ msgstr "HTTP status #%1$s: Servidor reporto un problema"
+
+#, c-format
+#~ msgid "Got message: \"%1$s\" from: %2$s"
+#~ msgstr "Recibimos el mensaje: %1$s desde %2$s"
+
+#, c-format
+#~ msgid ""
+#~ "in order to use merchant backoffice, you should create the default "
+#~ "instance"
+#~ msgstr ""
+#~ "para usar el merchant backoffice, debería crear la instancia default"
+
+#, c-format
+#~ msgid "Got message: %1$s from: %2$s"
+#~ msgstr "Recibimos el mensaje %1$s desde %2$s"
+
+#, c-format
+#~ msgid ""
+#~ "Please enter your auth token. Token should have \"secret-token:\" and "
+#~ "start with Bearer or ApiKey"
+#~ msgstr ""
+#~ "Por favor ingrese su token de autorización. El token debe tener \"secret-"
+#~ "token\" y comenzar con Bearer o ApiKey"
+
+#, c-format
+#~ msgid "pick a date"
+#~ msgstr "elegir una fecha"
+
+#, c-format
+#~ msgid "no results"
+#~ msgstr "Sin resultados"
+
+#, c-format
+#~ msgid "current stock will change from %1$s to %2$s"
+#~ msgstr "stock actual cambiará desde %1$s a %2$s"
+
+#, c-format
+#~ msgid "current stock will stay at %1$s"
+#~ msgstr "stock actual seguirá en %1$s"
+
+#, c-format
+#~ msgid "this product has no taxes"
+#~ msgstr "este producto no tiene impuestos"
+
+#, c-format
+#~ msgid "Inventory products"
+#~ msgstr "Productos de inventario"
+
+#, c-format
+#~ msgid "Total tax"
+#~ msgstr "Impuesto total"
+
+#, c-format
+#~ msgid "Net"
+#~ msgstr "Neto"
+
+#, c-format
+#~ msgid "select a product first"
+#~ msgstr "seleccione un producto primero"
+
+#, c-format
+#~ msgid ""
+#~ "cannot be greater than current stock and quantity previously added. max: "
+#~ "%1$s"
+#~ msgstr ""
+#~ "no puede ser mayor al stock actual y la cantidad previamente agregada. "
+#~ "máximo: %1$s"
+
+#, c-format
+#~ msgid "cannot be greater than current stock %1$s"
+#~ msgstr "no puede ser mayor al stock actual %1$s"
+
+#, c-format
+#~ msgid "Deposit total"
+#~ msgstr "Total depositado"
+
+#, c-format
+#~ msgid "Merchant initial amount"
+#~ msgstr "Monto inicial"
+
+#, c-format
+#~ msgid "Account Address"
+#~ msgstr "Dirección de cuenta"
diff --git a/packages/merchant-backoffice-ui/src/i18n/fr.po b/packages/merchant-backoffice-ui/src/i18n/fr.po
index 6b35bd0ce..4da5c5b59 100644
--- a/packages/merchant-backoffice-ui/src/i18n/fr.po
+++ b/packages/merchant-backoffice-ui/src/i18n/fr.po
@@ -12,1046 +12,2731 @@
# You should have received a copy of the GNU General Public License along with
# TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
#
-#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: Taler Wallet\n"
-"Report-Msgid-Bugs-To: \n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
"POT-Creation-Date: 2016-11-23 00:00+0100\n"
-"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
-"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
-"Language-Team: LANGUAGE <LL@li.org>\n"
-"Language: \n"
+"PO-Revision-Date: 2024-02-28 08:07+0000\n"
+"Last-Translator: d0p1 <contact@d0p1.eu>\n"
+"Language-Team: French <https://weblate.taler.net/projects/gnu-taler/"
+"merchant-backoffice/fr/>\n"
+"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"Plural-Forms: nplurals=2; plural=(n!=1);\n"
+"X-Generator: Weblate 5.2.1\n"
-#: src/ApplicationReadyRoutes.tsx:50 src/InstanceRoutes.tsx:118
-#: src/InstanceRoutes.tsx:299
+#: src/components/modal/index.tsx:71
#, c-format
-msgid "Access denied"
+msgid "Cancel"
+msgstr "Annuler"
+
+#: src/components/modal/index.tsx:79
+#, c-format
+msgid "%1$s"
msgstr ""
-#: src/ApplicationReadyRoutes.tsx:51 src/InstanceRoutes.tsx:118
-#: src/InstanceRoutes.tsx:300
+#: src/components/modal/index.tsx:84
#, c-format
-msgid "Check your token is valid"
+msgid "Close"
+msgstr "Fermer"
+
+#: src/components/modal/index.tsx:124
+#, c-format
+msgid "Continue"
+msgstr "Continuer"
+
+#: src/components/modal/index.tsx:178
+#, c-format
+msgid "Clear"
msgstr ""
-#: src/ApplicationReadyRoutes.tsx:72
+#: src/components/modal/index.tsx:190
#, c-format
-msgid "Couldn't access the server."
+msgid "Confirm"
+msgstr "Confirmer"
+
+#: src/components/modal/index.tsx:296
+#, c-format
+msgid "is not the same as the current access token"
msgstr ""
-#: src/ApplicationReadyRoutes.tsx:73
+#: src/components/modal/index.tsx:299
#, c-format
-msgid "Could not infer instance id from url %1$s"
+msgid "cannot be empty"
+msgstr "ne peux pas être vide"
+
+#: src/components/modal/index.tsx:301
+#, c-format
+msgid "cannot be the same as the old token"
msgstr ""
-#: src/InstanceRoutes.tsx:109
+#: src/components/modal/index.tsx:305
#, c-format
-msgid "HTTP status #%1$s: Server reported a problem"
+msgid "is not the same"
msgstr ""
-#: src/InstanceRoutes.tsx:110
+#: src/components/modal/index.tsx:315
#, c-format
-msgid "Got message: \"%1$s\" from: %2$s"
+msgid "You are updating the access token from instance with id %1$s"
msgstr ""
-#: src/InstanceRoutes.tsx:127
+#: src/components/modal/index.tsx:331
#, c-format
-msgid "No default instance"
+msgid "Old access token"
msgstr ""
-#: src/InstanceRoutes.tsx:128
+#: src/components/modal/index.tsx:332
#, c-format
-msgid ""
-"in order to use merchant backoffice, you should create the default instance"
+msgid "access token currently in use"
msgstr ""
-#: src/InstanceRoutes.tsx:288
+#: src/components/modal/index.tsx:338
#, c-format
-msgid "Server reported a problem: HTTP status #%1$s"
+msgid "New access token"
msgstr ""
-#: src/InstanceRoutes.tsx:289
+#: src/components/modal/index.tsx:339
#, c-format
-msgid "Got message: %1$s from: %2$s"
+msgid "next access token to be used"
msgstr ""
-#: src/components/exception/login.tsx:46
+#: src/components/modal/index.tsx:344
#, c-format
-msgid "Login required"
+msgid "Repeat access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:345
+#, c-format
+msgid "confirm the same access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:350
+#, c-format
+msgid "Clearing the access token will mean public access to the instance"
+msgstr ""
+
+#: src/components/modal/index.tsx:377
+#, c-format
+msgid "cannot be the same as the old access token"
msgstr ""
-#: src/components/exception/login.tsx:49
+#: src/components/modal/index.tsx:394
+#, c-format
+msgid "You are setting the access token for the new instance"
+msgstr ""
+
+#: src/components/modal/index.tsx:420
#, c-format
msgid ""
-"Please enter your auth token. Token should have \"secret-token:\" and start "
-"with Bearer or ApiKey"
+"With external authorization method no check will be done by the merchant "
+"backend"
msgstr ""
-#: src/components/exception/login.tsx:86 src/components/modal/index.tsx:53
-#: src/components/modal/index.tsx:75 src/paths/admin/create/CreatePage.tsx:115
-#: src/paths/instance/orders/create/CreatePage.tsx:325
-#: src/paths/instance/products/create/CreatePage.tsx:51
-#: src/paths/instance/products/list/Table.tsx:174
-#: src/paths/instance/products/list/Table.tsx:228
-#: src/paths/instance/products/update/UpdatePage.tsx:55
-#: src/paths/instance/transfers/create/CreatePage.tsx:89
-#: src/paths/instance/update/UpdatePage.tsx:134
+#: src/components/modal/index.tsx:436
#, c-format
-msgid "Confirm"
+msgid "Set external authorization"
msgstr ""
-#: src/components/form/InputArray.tsx:72
+#: src/components/modal/index.tsx:448
#, c-format
-msgid "The value %1$s is invalid for a payment url"
+msgid "Set access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:470
+#, c-format
+msgid "Operation in progress..."
+msgstr ""
+
+#: src/components/modal/index.tsx:479
+#, c-format
+msgid "The operation will be automatically canceled after %1$s seconds"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:80
+#, c-format
+msgid "Instances"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:93
+#, c-format
+msgid "Delete"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:99
+#, c-format
+msgid "add new instance"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:178
+#, c-format
+msgid "ID"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:181
+#, c-format
+msgid "Name"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:220
+#, c-format
+msgid "Edit"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:237
+#, c-format
+msgid "Purge"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:261
+#, c-format
+msgid "There is no instances yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:68
+#, c-format
+msgid "Only show active instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:71
+#, c-format
+msgid "Active"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:78
+#, c-format
+msgid "Only show deleted instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:81
+#, c-format
+msgid "Deleted"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:88
+#, c-format
+msgid "Show all instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:91
+#, c-format
+msgid "All"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:101
+#, c-format
+msgid "Instance \"%1$s\" (ID: %2$s) has been deleted"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:106
+#, c-format
+msgid "Failed to delete instance"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:124
+#, c-format
+msgid "Instance '%1$s' (ID: %2$s) has been disabled"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:129
+#, c-format
+msgid "Failed to purge instance"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:41
+#, c-format
+msgid "Pending KYC verification"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:66
+#, c-format
+msgid "Timed out"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:103
+#, c-format
+msgid "Exchange"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:106
+#, c-format
+msgid "Target account"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:109
+#, c-format
+msgid "KYC URL"
msgstr ""
-#: src/components/form/InputDate.tsx:67
-#: src/paths/instance/orders/list/index.tsx:123
+#: src/paths/instance/kyc/list/ListPage.tsx:144
#, c-format
-msgid "pick a date"
+msgid "Code"
msgstr ""
-#: src/components/form/InputDate.tsx:81
+#: src/paths/instance/kyc/list/ListPage.tsx:147
+#, c-format
+msgid "Http Status"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:177
+#, c-format
+msgid "No pending kyc verification!"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:123
+#, c-format
+msgid "change value to unknown date"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:124
+#, c-format
+msgid "change value to empty"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:131
#, c-format
msgid "clear"
msgstr ""
-#: src/components/form/InputDate.tsx:83
-#: src/paths/instance/transfers/list/Table.tsx:140
+#: src/components/form/InputDate.tsx:136
#, c-format
-msgid "never"
+msgid "change value to never"
msgstr ""
-#: src/components/form/InputImage.tsx:80
+#: src/components/form/InputDate.tsx:141
#, c-format
-msgid "Image should be smaller than 1 MB"
+msgid "never"
msgstr ""
-#: src/components/form/InputLocation.tsx:28
+#: src/components/form/InputLocation.tsx:29
#, c-format
msgid "Country"
msgstr ""
-#: src/components/form/InputLocation.tsx:30
-#: src/paths/admin/create/CreatePage.tsx:99
-#: src/paths/instance/transfers/list/Table.tsx:124
-#: src/paths/instance/update/UpdatePage.tsx:118
+#: src/components/form/InputLocation.tsx:33
#, c-format
msgid "Address"
msgstr ""
-#: src/components/form/InputLocation.tsx:34
+#: src/components/form/InputLocation.tsx:39
#, c-format
msgid "Building number"
msgstr ""
-#: src/components/form/InputLocation.tsx:35
+#: src/components/form/InputLocation.tsx:41
#, c-format
msgid "Building name"
msgstr ""
-#: src/components/form/InputLocation.tsx:36
+#: src/components/form/InputLocation.tsx:42
#, c-format
msgid "Street"
msgstr ""
-#: src/components/form/InputLocation.tsx:37
+#: src/components/form/InputLocation.tsx:43
#, c-format
msgid "Post code"
msgstr ""
-#: src/components/form/InputLocation.tsx:38
+#: src/components/form/InputLocation.tsx:44
#, c-format
msgid "Town location"
msgstr ""
-#: src/components/form/InputLocation.tsx:39
+#: src/components/form/InputLocation.tsx:45
#, c-format
msgid "Town"
msgstr ""
-#: src/components/form/InputLocation.tsx:40
+#: src/components/form/InputLocation.tsx:46
#, c-format
msgid "District"
msgstr ""
-#: src/components/form/InputLocation.tsx:41
+#: src/components/form/InputLocation.tsx:49
#, c-format
msgid "Country subdivision"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:59
+#: src/components/form/InputSearchProduct.tsx:66
#, c-format
msgid "Product id"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:60
-#: src/components/product/ProductForm.tsx:99
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:122
-#: src/paths/instance/orders/list/Table.tsx:227
-#: src/paths/instance/products/list/Table.tsx:86
+#: src/components/form/InputSearchProduct.tsx:69
#, c-format
msgid "Description"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:73
-#: src/components/form/InputTaxes.tsx:81
-#: src/paths/admin/create/CreatePage.tsx:87 src/paths/admin/list/Table.tsx:110
-#: src/paths/instance/details/DetailPage.tsx:76
-#: src/paths/instance/update/UpdatePage.tsx:106
+#: src/components/form/InputSearchProduct.tsx:94
#, c-format
-msgid "Name"
+msgid "Product"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:102
+#: src/components/form/InputSearchProduct.tsx:95
#, c-format
-msgid "loading..."
+msgid "search products by it's description or id"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:108
+#: src/components/form/InputSearchProduct.tsx:151
#, c-format
-msgid "no products found"
+msgid "no products found with that description"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:116
+#: src/components/product/InventoryProductForm.tsx:56
#, c-format
-msgid "no results"
+msgid "You must enter a valid product identifier."
msgstr ""
-#: src/components/form/InputSecured.tsx:33
+#: src/components/product/InventoryProductForm.tsx:64
#, c-format
-msgid "Deleting"
+msgid "Quantity must be greater than 0!"
msgstr ""
-#: src/components/form/InputSecured.tsx:34
+#: src/components/product/InventoryProductForm.tsx:76
#, c-format
-msgid "Changing"
+msgid ""
+"This quantity exceeds remaining stock. Currently, only %1$s units remain "
+"unreserved in stock."
msgstr ""
-#: src/components/form/InputSecured.tsx:60
+#: src/components/product/InventoryProductForm.tsx:109
#, c-format
-msgid "Manage token"
+msgid "Quantity"
msgstr ""
-#: src/components/form/InputSecured.tsx:83
+#: src/components/product/InventoryProductForm.tsx:110
#, c-format
-msgid "Update"
+msgid "how many products will be added"
msgstr ""
-#: src/components/form/InputSecured.tsx:100
-#: src/paths/instance/orders/create/CreatePage.tsx:252
-#: src/paths/instance/orders/create/CreatePage.tsx:273
+#: src/components/product/InventoryProductForm.tsx:117
+#, c-format
+msgid "Add from inventory"
+msgstr ""
+
+#: src/components/form/InputImage.tsx:105
+#, c-format
+msgid "Image should be smaller than 1 MB"
+msgstr ""
+
+#: src/components/form/InputImage.tsx:110
+#, c-format
+msgid "Add"
+msgstr ""
+
+#: src/components/form/InputImage.tsx:115
#, c-format
msgid "Remove"
msgstr ""
-#: src/components/form/InputSecured.tsx:106 src/components/modal/index.tsx:52
-#: src/components/modal/index.tsx:73 src/paths/admin/create/CreatePage.tsx:114
-#: src/paths/instance/orders/create/CreatePage.tsx:324
-#: src/paths/instance/products/create/CreatePage.tsx:50
-#: src/paths/instance/products/list/Table.tsx:166
-#: src/paths/instance/products/list/Table.tsx:218
-#: src/paths/instance/products/update/UpdatePage.tsx:54
-#: src/paths/instance/transfers/create/CreatePage.tsx:88
-#: src/paths/instance/update/UpdatePage.tsx:133
+#: src/components/form/InputTaxes.tsx:113
#, c-format
-msgid "Cancel"
+msgid "No taxes configured for this product."
msgstr ""
-#: src/components/form/InputStock.tsx:91
+#: src/components/form/InputTaxes.tsx:119
#, c-format
-msgid "Manage stock"
+msgid "Amount"
msgstr ""
-#: src/components/form/InputStock.tsx:93
+#: src/components/form/InputTaxes.tsx:120
#, c-format
-msgid "Infinite"
+msgid ""
+"Taxes can be in currencies that differ from the main currency used by the "
+"merchant."
msgstr ""
-#: src/components/form/InputStock.tsx:105
+#: src/components/form/InputTaxes.tsx:122
#, c-format
-msgid "lost cannot be greater that current + incoming (max %1$s)"
+msgid ""
+"Enter currency and value separated with a colon, e.g. &quot;USD:2.3&quot;."
msgstr ""
-#: src/components/form/InputStock.tsx:111
+#: src/components/form/InputTaxes.tsx:131
#, c-format
-msgid "current stock will change from %1$s to %2$s"
+msgid "Legal name of the tax, e.g. VAT or import duties."
msgstr ""
-#: src/components/form/InputStock.tsx:112
+#: src/components/form/InputTaxes.tsx:137
#, c-format
-msgid "current stock will stay at %1$s"
+msgid "add tax to the tax list"
msgstr ""
-#: src/components/form/InputStock.tsx:129
-#: src/paths/instance/products/list/Table.tsx:204
+#: src/components/product/NonInventoryProductForm.tsx:72
#, c-format
-msgid "Incoming"
+msgid "describe and add a product that is not in the inventory list"
msgstr ""
-#: src/components/form/InputStock.tsx:130
-#: src/paths/instance/products/list/Table.tsx:205
+#: src/components/product/NonInventoryProductForm.tsx:75
#, c-format
-msgid "Lost"
+msgid "Add custom product"
msgstr ""
-#: src/components/form/InputStock.tsx:142
+#: src/components/product/NonInventoryProductForm.tsx:86
#, c-format
-msgid "Current"
+msgid "Complete information of the product"
msgstr ""
-#: src/components/form/InputStock.tsx:145
+#: src/components/product/NonInventoryProductForm.tsx:185
#, c-format
-msgid "without stock"
+msgid "Image"
msgstr ""
-#: src/components/form/InputStock.tsx:150
+#: src/components/product/NonInventoryProductForm.tsx:186
#, c-format
-msgid "Next restock"
+msgid "photo of the product"
msgstr ""
-#: src/components/form/InputStock.tsx:152
+#: src/components/product/NonInventoryProductForm.tsx:192
#, c-format
-msgid "Delivery address"
+msgid "full product description"
msgstr ""
-#: src/components/form/InputTaxes.tsx:73
+#: src/components/product/NonInventoryProductForm.tsx:196
#, c-format
-msgid "this product has no taxes"
+msgid "Unit"
msgstr ""
-#: src/components/form/InputTaxes.tsx:77
-#: src/paths/instance/orders/details/DetailPage.tsx:145
-#: src/paths/instance/orders/details/DetailPage.tsx:296
-#: src/paths/instance/orders/list/Table.tsx:116
-#: src/paths/instance/transfers/create/CreatePage.tsx:84
+#: src/components/product/NonInventoryProductForm.tsx:197
#, c-format
-msgid "Amount"
+msgid "name of the product unit"
msgstr ""
-#: src/components/form/InputTaxes.tsx:78
+#: src/components/product/NonInventoryProductForm.tsx:201
#, c-format
-msgid "currency and value separated with colon"
+msgid "Price"
msgstr ""
-#: src/components/form/InputTaxes.tsx:84
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:78
+#: src/components/product/NonInventoryProductForm.tsx:202
#, c-format
-msgid "Add"
+msgid "amount in the current currency"
msgstr ""
-#: src/components/menu/SideBar.tsx:53
+#: src/components/product/NonInventoryProductForm.tsx:211
#, c-format
-msgid "Instance"
+msgid "Taxes"
msgstr ""
-#: src/components/menu/SideBar.tsx:59
+#: src/components/product/ProductList.tsx:38
#, c-format
-msgid "Settings"
+msgid "image"
msgstr ""
-#: src/components/menu/SideBar.tsx:65
-#: src/paths/instance/orders/list/Table.tsx:60
+#: src/components/product/ProductList.tsx:41
#, c-format
-msgid "Orders"
+msgid "description"
msgstr ""
-#: src/components/menu/SideBar.tsx:71
-#: src/paths/instance/orders/create/CreatePage.tsx:258
-#: src/paths/instance/products/list/Table.tsx:48
+#: src/components/product/ProductList.tsx:44
#, c-format
-msgid "Products"
+msgid "quantity"
msgstr ""
-#: src/components/menu/SideBar.tsx:77
-#: src/paths/instance/transfers/list/Table.tsx:65
+#: src/components/product/ProductList.tsx:47
#, c-format
-msgid "Transfers"
+msgid "unit price"
msgstr ""
-#: src/components/menu/SideBar.tsx:87
+#: src/components/product/ProductList.tsx:50
#, c-format
-msgid "Connection"
+msgid "total price"
msgstr ""
-#: src/components/menu/SideBar.tsx:112 src/paths/admin/list/Table.tsx:57
+#: src/paths/instance/orders/create/CreatePage.tsx:153
#, c-format
-msgid "Instances"
+msgid "required"
msgstr ""
-#: src/components/menu/SideBar.tsx:116
+#: src/paths/instance/orders/create/CreatePage.tsx:157
#, c-format
-msgid "New"
+msgid "not valid"
msgstr ""
-#: src/components/menu/SideBar.tsx:122
+#: src/paths/instance/orders/create/CreatePage.tsx:159
#, c-format
-msgid "List"
+msgid "must be greater than 0"
msgstr ""
-#: src/components/menu/SideBar.tsx:129
+#: src/paths/instance/orders/create/CreatePage.tsx:164
#, c-format
-msgid "Log out"
+msgid "not a valid json"
msgstr ""
-#: src/components/modal/index.tsx:74
+#: src/paths/instance/orders/create/CreatePage.tsx:170
#, c-format
-msgid "Clear"
+msgid "should be in the future"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:173
+#, c-format
+msgid "refund deadline cannot be before pay deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:179
+#, c-format
+msgid "wire transfer deadline cannot be before refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:190
+#, c-format
+msgid "wire transfer deadline cannot be before pay deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:197
+#, c-format
+msgid "should have a refund deadline"
msgstr ""
-#: src/components/modal/index.tsx:110 src/components/modal/index.tsx:111
+#: src/paths/instance/orders/create/CreatePage.tsx:202
#, c-format
-msgid "should be the same"
+msgid "auto refund cannot be after refund deadline"
msgstr ""
-#: src/components/modal/index.tsx:111
+#: src/paths/instance/orders/create/CreatePage.tsx:360
#, c-format
-msgid "cannot be the same as before"
+msgid "Manage products in order"
msgstr ""
-#: src/components/modal/index.tsx:114
+#: src/paths/instance/orders/create/CreatePage.tsx:369
+#, c-format
+msgid "Manage list of products in the order."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:391
+#, c-format
+msgid "Remove this product from the order."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:415
+#, c-format
+msgid "Total price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:417
+#, c-format
+msgid "total product price added up"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:430
+#, c-format
+msgid "Amount to be paid by the customer"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:436
+#, c-format
+msgid "Order price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:437
+#, c-format
+msgid "final order price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:444
+#, c-format
+msgid "Summary"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:445
+#, c-format
+msgid "Title of the order to be shown to the customer"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:450
+#, c-format
+msgid "Shipping and Fulfillment"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:455
+#, c-format
+msgid "Delivery date"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:456
+#, c-format
+msgid "Deadline for physical delivery assured by the merchant."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:461
+#, c-format
+msgid "Location"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:462
+#, c-format
+msgid "address where the products will be delivered"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:469
+#, c-format
+msgid "Fulfillment URL"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:470
+#, c-format
+msgid "URL to which the user will be redirected after successful payment."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:476
+#, c-format
+msgid "Taler payment options"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:477
+#, c-format
+msgid "Override default Taler payment settings for this order"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:481
+#, c-format
+msgid "Payment deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:482
#, c-format
msgid ""
-"You are updating the authorization token from instance %1$s with id %2$s"
+"Deadline for the customer to pay for the offer before it expires. Inventory "
+"products will be reserved until this deadline."
msgstr ""
-#: src/components/modal/index.tsx:124
+#: src/paths/instance/orders/create/CreatePage.tsx:486
#, c-format
-msgid "Old token"
+msgid "Refund deadline"
msgstr ""
-#: src/components/modal/index.tsx:125
+#: src/paths/instance/orders/create/CreatePage.tsx:487
#, c-format
-msgid "New token"
+msgid "Time until which the order can be refunded by the merchant."
msgstr ""
-#: src/components/modal/index.tsx:127
+#: src/paths/instance/orders/create/CreatePage.tsx:491
#, c-format
-msgid "Clearing the auth token will mean public access to the instance"
+msgid "Wire transfer deadline"
msgstr ""
-#: src/components/product/ProductForm.tsx:96
-#: src/paths/admin/create/CreatePage.tsx:85 src/paths/admin/list/Table.tsx:109
-#: src/paths/instance/transfers/list/Table.tsx:122
+#: src/paths/instance/orders/create/CreatePage.tsx:492
#, c-format
-msgid "ID"
+msgid "Deadline for the exchange to make the wire transfer."
msgstr ""
-#: src/components/product/ProductForm.tsx:98
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:121
-#: src/paths/instance/products/list/Table.tsx:85
+#: src/paths/instance/orders/create/CreatePage.tsx:496
#, c-format
-msgid "Image"
+msgid "Auto-refund deadline"
msgstr ""
-#: src/components/product/ProductForm.tsx:100
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:123
+#: src/paths/instance/orders/create/CreatePage.tsx:497
#, c-format
-msgid "Unit"
+msgid ""
+"Time until which the wallet will automatically check for refunds without "
+"user interaction."
msgstr ""
-#: src/components/product/ProductForm.tsx:101
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:124
-#: src/paths/instance/products/list/Table.tsx:162
-#: src/paths/instance/products/list/Table.tsx:214
+#: src/paths/instance/orders/create/CreatePage.tsx:502
#, c-format
-msgid "Price"
+msgid "Maximum deposit fee"
msgstr ""
-#: src/components/product/ProductForm.tsx:103
-#: src/paths/instance/products/list/Table.tsx:90
+#: src/paths/instance/orders/create/CreatePage.tsx:503
#, c-format
-msgid "Stock"
+msgid ""
+"Maximum deposit fees the merchant is willing to cover for this order. Higher "
+"deposit fees must be covered in full by the consumer."
msgstr ""
-#: src/components/product/ProductForm.tsx:105
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:128
-#: src/paths/instance/products/list/Table.tsx:88
+#: src/paths/instance/orders/create/CreatePage.tsx:507
#, c-format
-msgid "Taxes"
+msgid "Maximum wire fee"
msgstr ""
-#: src/index.tsx:75
+#: src/paths/instance/orders/create/CreatePage.tsx:508
#, c-format
-msgid "Server not found"
+msgid ""
+"Maximum aggregate wire fees the merchant is willing to cover for this order. "
+"Wire fees exceeding this amount are to be covered by the customers."
msgstr ""
-#: src/index.tsx:85
+#: src/paths/instance/orders/create/CreatePage.tsx:512
#, c-format
-msgid "Couldn't access the server"
+msgid "Wire fee amortization"
msgstr ""
-#: src/index.tsx:87 src/index.tsx:99
+#: src/paths/instance/orders/create/CreatePage.tsx:513
#, c-format
-msgid "Got message %1$s from %2$s"
+msgid ""
+"Factor by which wire fees exceeding the above threshold are divided to "
+"determine the share of excess wire fees to be paid explicitly by the "
+"consumer."
msgstr ""
-#: src/index.tsx:97
+#: src/paths/instance/orders/create/CreatePage.tsx:517
#, c-format
-msgid "Unexpected Error"
+msgid "Create token"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:89
-#: src/paths/instance/update/UpdatePage.tsx:108
+#: src/paths/instance/orders/create/CreatePage.tsx:518
#, c-format
-msgid "Auth token"
+msgid ""
+"Uncheck this option if the merchant backend generated an order ID with "
+"enough entropy to prevent adversarial claims."
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:91
-#: src/paths/instance/details/DetailPage.tsx:77
-#: src/paths/instance/update/UpdatePage.tsx:110
+#: src/paths/instance/orders/create/CreatePage.tsx:522
#, c-format
-msgid "Account address"
+msgid "Minimum age required"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:93
-#: src/paths/instance/update/UpdatePage.tsx:112
+#: src/paths/instance/orders/create/CreatePage.tsx:523
#, c-format
-msgid "Default max deposit fee"
+msgid ""
+"Any value greater than 0 will limit the coins able be used to pay this "
+"contract. If empty the age restriction will be defined by the products"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:95
-#: src/paths/instance/update/UpdatePage.tsx:114
+#: src/paths/instance/orders/create/CreatePage.tsx:526
#, c-format
-msgid "Default max wire fee"
+msgid "Min age defined by the producs is %1$s"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:97
-#: src/paths/instance/update/UpdatePage.tsx:116
+#: src/paths/instance/orders/create/CreatePage.tsx:534
#, c-format
-msgid "Default wire fee amortization"
+msgid "Additional information"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:103
-#: src/paths/instance/update/UpdatePage.tsx:122
+#: src/paths/instance/orders/create/CreatePage.tsx:535
#, c-format
-msgid "Jurisdiction"
+msgid "Custom information to be included in the contract for this order."
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:107
-#: src/paths/instance/update/UpdatePage.tsx:126
+#: src/paths/instance/orders/create/CreatePage.tsx:541
#, c-format
-msgid "Default pay delay"
+msgid "You must enter a value in JavaScript Object Notation (JSON)."
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:109
-#: src/paths/instance/update/UpdatePage.tsx:128
+#: src/components/picker/DurationPicker.tsx:55
#, c-format
-msgid "Default wire transfer delay"
+msgid "days"
msgstr ""
-#: src/paths/admin/create/index.tsx:58
+#: src/components/picker/DurationPicker.tsx:65
#, c-format
-msgid "could not create instance"
+msgid "hours"
msgstr ""
-#: src/paths/admin/list/Table.tsx:63 src/paths/admin/list/Table.tsx:131
-#: src/paths/instance/transfers/list/Table.tsx:71
+#: src/components/picker/DurationPicker.tsx:76
#, c-format
-msgid "Delete"
+msgid "minutes"
msgstr ""
-#: src/paths/admin/list/Table.tsx:128
+#: src/components/picker/DurationPicker.tsx:87
#, c-format
-msgid "Edit"
+msgid "seconds"
msgstr ""
-#: src/paths/admin/list/Table.tsx:149
-#: src/paths/instance/products/list/Table.tsx:245
+#: src/components/form/InputDuration.tsx:53
#, c-format
-msgid "There is no instances yet, add more pressing the + sign"
+msgid "forever"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:237
+#: src/components/form/InputDuration.tsx:62
#, c-format
-msgid "Inventory products"
+msgid "%1$sM"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:286
+#: src/components/form/InputDuration.tsx:64
#, c-format
-msgid "Total price"
+msgid "%1$sY"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:287
+#: src/components/form/InputDuration.tsx:66
#, c-format
-msgid "Total tax"
+msgid "%1$sd"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:289
-#: src/paths/instance/orders/create/CreatePage.tsx:297
+#: src/components/form/InputDuration.tsx:68
#, c-format
-msgid "Order price"
+msgid "%1$sh"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:295
+#: src/components/form/InputDuration.tsx:70
#, c-format
-msgid "Net"
+msgid "%1$smin"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:300
-#: src/paths/instance/orders/details/DetailPage.tsx:144
-#: src/paths/instance/orders/details/DetailPage.tsx:295
-#: src/paths/instance/orders/list/Table.tsx:117
+#: src/components/form/InputDuration.tsx:72
#, c-format
-msgid "Summary"
+msgid "%1$ssec"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:302
+#: src/paths/instance/orders/list/Table.tsx:75
#, c-format
-msgid "Payments options"
+msgid "Orders"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:303
+#: src/paths/instance/orders/list/Table.tsx:81
#, c-format
-msgid "Auto refund deadline"
+msgid "create order"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:304
+#: src/paths/instance/orders/list/Table.tsx:147
#, c-format
-msgid "Refund deadline"
+msgid "load newer orders"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:305
+#: src/paths/instance/orders/list/Table.tsx:154
#, c-format
-msgid "Pay deadline"
+msgid "Date"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:307
+#: src/paths/instance/orders/list/Table.tsx:200
#, c-format
-msgid "Delivery date"
+msgid "Refund"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:308
+#: src/paths/instance/orders/list/Table.tsx:209
#, c-format
-msgid "Location"
+msgid "copy url"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:225
+#, c-format
+msgid "load older orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:242
+#, c-format
+msgid "No orders have been found matching your query!"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:288
+#, c-format
+msgid "duplicated"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:299
+#, c-format
+msgid "invalid format"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:301
+#, c-format
+msgid "this value exceed the refundable amount"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:346
+#, c-format
+msgid "date"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:349
+#, c-format
+msgid "amount"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:352
+#, c-format
+msgid "reason"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:389
+#, c-format
+msgid "amount to be refunded"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:391
+#, c-format
+msgid "Max refundable:"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:396
+#, c-format
+msgid "Reason"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:397
+#, c-format
+msgid "Choose one..."
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:399
+#, c-format
+msgid "requested by the customer"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:400
+#, c-format
+msgid "other"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:403
+#, c-format
+msgid "why this order is being refunded"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:409
+#, c-format
+msgid "more information to give context"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:62
+#, c-format
+msgid "Contract Terms"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:68
+#, c-format
+msgid "human-readable description of the whole purchase"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:74
+#, c-format
+msgid "total price for the transaction"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:81
+#, c-format
+msgid "URL for this purchase"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:312
+#: src/paths/instance/orders/details/DetailPage.tsx:87
#, c-format
msgid "Max fee"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:313
+#: src/paths/instance/orders/details/DetailPage.tsx:88
+#, c-format
+msgid "maximum total deposit fee accepted by the merchant for this contract"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:93
#, c-format
msgid "Max wire fee"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:314
+#: src/paths/instance/orders/details/DetailPage.tsx:94
#, c-format
-msgid "Wire fee amortization"
+msgid "maximum wire fee accepted by the merchant"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:315
+#: src/paths/instance/orders/details/DetailPage.tsx:100
#, c-format
-msgid "Fullfilment url"
+msgid ""
+"over how many customer transactions does the merchant expect to amortize "
+"wire fees on average"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:318
+#: src/paths/instance/orders/details/DetailPage.tsx:105
#, c-format
-msgid "Extra information"
+msgid "Created at"
msgstr ""
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:44
+#: src/paths/instance/orders/details/DetailPage.tsx:106
#, c-format
-msgid "select a product first"
+msgid "time when this contract was generated"
msgstr ""
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:51
+#: src/paths/instance/orders/details/DetailPage.tsx:112
#, c-format
-msgid "should be greater than 0"
+msgid "after this deadline has passed no refunds will be accepted"
msgstr ""
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:58
+#: src/paths/instance/orders/details/DetailPage.tsx:118
#, c-format
msgid ""
-"cannot be greater than current stock and quantity previously added. max: %1$s"
+"after this deadline, the merchant won't accept payments for the contract"
msgstr ""
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:64
+#: src/paths/instance/orders/details/DetailPage.tsx:124
#, c-format
-msgid "cannot be greater than current stock %1$s"
+msgid "transfer deadline for the exchange"
msgstr ""
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:76
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:126
+#: src/paths/instance/orders/details/DetailPage.tsx:130
#, c-format
-msgid "Quantity"
+msgid "time indicating when the order should be delivered"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:92
-#: src/paths/instance/orders/details/DetailPage.tsx:235
-#: src/paths/instance/orders/details/DetailPage.tsx:333
+#: src/paths/instance/orders/details/DetailPage.tsx:136
#, c-format
-msgid "Order"
+msgid "where the order will be delivered"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:93
+#: src/paths/instance/orders/details/DetailPage.tsx:144
#, c-format
-msgid "claimed"
+msgid "Auto-refund delay"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:110
-#: src/paths/instance/orders/details/DetailPage.tsx:261
-#: src/paths/instance/orders/list/Table.tsx:136
+#: src/paths/instance/orders/details/DetailPage.tsx:145
#, c-format
-msgid "copy url"
+msgid ""
+"how long the wallet should try to get an automatic refund for the purchase"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:126
-#: src/paths/instance/orders/details/DetailPage.tsx:349
+#: src/paths/instance/orders/details/DetailPage.tsx:150
#, c-format
-msgid "pay at"
+msgid "Extra info"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:127
-#: src/paths/instance/orders/details/DetailPage.tsx:350
+#: src/paths/instance/orders/details/DetailPage.tsx:151
#, c-format
-msgid "created at"
+msgid "extra data that is only interpreted by the merchant frontend"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:219
+#, c-format
+msgid "Order"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:221
+#, c-format
+msgid "claimed"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:247
+#, c-format
+msgid "claimed at"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:138
-#: src/paths/instance/orders/details/DetailPage.tsx:289
+#: src/paths/instance/orders/details/DetailPage.tsx:265
#, c-format
msgid "Timeline"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:142
-#: src/paths/instance/orders/details/DetailPage.tsx:293
+#: src/paths/instance/orders/details/DetailPage.tsx:271
#, c-format
msgid "Payment details"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:146
-#: src/paths/instance/orders/details/DetailPage.tsx:299
-#: src/paths/instance/orders/details/DetailPage.tsx:363
+#: src/paths/instance/orders/details/DetailPage.tsx:291
#, c-format
msgid "Order status"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:156
-#: src/paths/instance/orders/details/DetailPage.tsx:308
+#: src/paths/instance/orders/details/DetailPage.tsx:301
#, c-format
msgid "Product list"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:236
+#: src/paths/instance/orders/details/DetailPage.tsx:451
#, c-format
msgid "paid"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:238
+#: src/paths/instance/orders/details/DetailPage.tsx:455
#, c-format
msgid "wired"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:241
+#: src/paths/instance/orders/details/DetailPage.tsx:460
#, c-format
msgid "refunded"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:258
+#: src/paths/instance/orders/details/DetailPage.tsx:480
+#, c-format
+msgid "refund order"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:481
+#, c-format
+msgid "not refundable"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:489
#, c-format
msgid "refund"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:297
+#: src/paths/instance/orders/details/DetailPage.tsx:553
#, c-format
msgid "Refunded amount"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:298
+#: src/paths/instance/orders/details/DetailPage.tsx:560
+#, c-format
+msgid "Refund taken"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:570
+#, c-format
+msgid "Status URL"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:583
#, c-format
-msgid "Deposit total"
+msgid "Refund URI"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:336
+#: src/paths/instance/orders/details/DetailPage.tsx:636
#, c-format
msgid "unpaid"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:364
+#: src/paths/instance/orders/details/DetailPage.tsx:654
+#, c-format
+msgid "pay at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:666
+#, c-format
+msgid "created at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:707
#, c-format
msgid "Order status URL"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:365
+#: src/paths/instance/orders/details/DetailPage.tsx:711
#, c-format
-msgid "Pay URI"
+msgid "Payment URI"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:383
+#: src/paths/instance/orders/details/DetailPage.tsx:740
#, c-format
msgid ""
"Unknown order status. This is an error, please contact the administrator."
msgstr ""
-#: src/paths/instance/orders/details/index.tsx:56
-#: src/paths/instance/orders/list/index.tsx:147
+#: src/paths/instance/orders/details/DetailPage.tsx:767
+#, c-format
+msgid "Back"
+msgstr ""
+
+#: src/paths/instance/orders/details/index.tsx:79
#, c-format
msgid "refund created successfully"
msgstr ""
-#: src/paths/instance/orders/details/index.tsx:59
-#: src/paths/instance/orders/list/index.tsx:150
+#: src/paths/instance/orders/details/index.tsx:85
#, c-format
msgid "could not create the refund"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:111
+#: src/paths/instance/orders/list/ListPage.tsx:78
#, c-format
-msgid "load newer orders"
+msgid "select date to show nearby orders"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:115
+#: src/paths/instance/orders/list/ListPage.tsx:94
#, c-format
-msgid "Date"
+msgid "order id"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:131
-#: src/paths/instance/orders/list/Table.tsx:223
+#: src/paths/instance/orders/list/ListPage.tsx:100
#, c-format
-msgid "Refund"
+msgid "jump to order with the given order ID"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:145
+#: src/paths/instance/orders/list/ListPage.tsx:122
#, c-format
-msgid "load older orders"
+msgid "remove all filters"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:154
+#: src/paths/instance/orders/list/ListPage.tsx:132
#, c-format
-msgid "No orders has been found"
+msgid "only show paid orders"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:202
+#: src/paths/instance/orders/list/ListPage.tsx:135
#, c-format
-msgid "date"
+msgid "Paid"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:203
+#: src/paths/instance/orders/list/ListPage.tsx:142
#, c-format
-msgid "amount"
+msgid "only show orders with refunds"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:204
+#: src/paths/instance/orders/list/ListPage.tsx:145
#, c-format
-msgid "reason"
+msgid "Refunded"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:224
+#: src/paths/instance/orders/list/ListPage.tsx:152
#, c-format
-msgid "Max refundable:"
+msgid ""
+"only show orders where customers paid, but wire payments from payment "
+"provider are still pending"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:226
+#: src/paths/instance/orders/list/ListPage.tsx:155
#, c-format
-msgid "Reason"
+msgid "Not wired"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:226
+#: src/paths/instance/orders/list/ListPage.tsx:170
#, c-format
-msgid "duplicated"
+msgid "clear date filter"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:226
+#: src/paths/instance/orders/list/ListPage.tsx:184
#, c-format
-msgid "requested by the customer"
+msgid "date (YYYY/MM/DD)"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:226
+#: src/paths/instance/orders/list/index.tsx:103
#, c-format
-msgid "other"
+msgid "Enter an order id"
msgstr ""
-#: src/paths/instance/orders/list/index.tsx:91
+#: src/paths/instance/orders/list/index.tsx:111
#, c-format
-msgid "go to order id"
+msgid "order not found"
msgstr ""
-#: src/paths/instance/orders/list/index.tsx:107
+#: src/paths/instance/orders/list/index.tsx:178
#, c-format
-msgid "Paid"
+msgid "could not get the order to refund"
msgstr ""
-#: src/paths/instance/orders/list/index.tsx:108
+#: src/components/exception/AsyncButton.tsx:43
#, c-format
-msgid "Refunded"
+msgid "Loading..."
msgstr ""
-#: src/paths/instance/orders/list/index.tsx:109
+#: src/components/form/InputStock.tsx:99
#, c-format
-msgid "Not wired"
+msgid ""
+"click here to configure the stock of the product, leave it as is and the "
+"backend will not control stock"
msgstr ""
-#: src/paths/instance/orders/list/index.tsx:110
+#: src/components/form/InputStock.tsx:109
#, c-format
-msgid "All"
+msgid "Manage stock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:115
+#, c-format
+msgid "this product has been configured without stock control"
msgstr ""
-#: src/paths/instance/products/create/index.tsx:48
-#: src/paths/instance/products/update/index.tsx:64
+#: src/components/form/InputStock.tsx:119
+#, c-format
+msgid "Infinite"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:136
+#, c-format
+msgid "lost cannot be greater than current and incoming (max %1$s)"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:176
+#, c-format
+msgid "Incoming"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:177
+#, c-format
+msgid "Lost"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:192
+#, c-format
+msgid "Current"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:196
+#, c-format
+msgid "remove stock control for this product"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:202
+#, c-format
+msgid "without stock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:211
+#, c-format
+msgid "Next restock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:217
+#, c-format
+msgid "Delivery address"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:133
+#, c-format
+msgid "product identification to use in URLs (for internal use only)"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:139
+#, c-format
+msgid "illustration of the product for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:145
+#, c-format
+msgid "product description for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:149
+#, c-format
+msgid "Age restricted"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:150
+#, c-format
+msgid "is this product restricted for customer below certain age?"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:155
+#, c-format
+msgid ""
+"unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 "
+"items, 5 meters) for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:160
+#, c-format
+msgid ""
+"sale price for customers, including taxes, for above units of the product"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:164
+#, c-format
+msgid "Stock"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:166
+#, c-format
+msgid ""
+"product inventory for products with finite supply (for internal use only)"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:171
+#, c-format
+msgid "taxes included in the product price, exposed to customers"
+msgstr ""
+
+#: src/paths/instance/products/create/CreatePage.tsx:66
+#, c-format
+msgid "Need to complete marked fields"
+msgstr ""
+
+#: src/paths/instance/products/create/index.tsx:51
#, c-format
msgid "could not create product"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:87
+#: src/paths/instance/products/list/Table.tsx:68
+#, c-format
+msgid "Products"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:73
+#, c-format
+msgid "add product to inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:137
#, c-format
msgid "Sell"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:89
+#: src/paths/instance/products/list/Table.tsx:143
#, c-format
msgid "Profit"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:91
+#: src/paths/instance/products/list/Table.tsx:149
#, c-format
msgid "Sold"
msgstr ""
-#: src/paths/instance/products/list/index.tsx:59
+#: src/paths/instance/products/list/Table.tsx:210
+#, c-format
+msgid "free"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:248
+#, c-format
+msgid "go to product update page"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:255
+#, c-format
+msgid "Update"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:260
+#, c-format
+msgid "remove this product from the database"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:331
+#, c-format
+msgid "update the product with new price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:341
+#, c-format
+msgid "update product with new price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:399
+#, c-format
+msgid "add more elements to the inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:404
+#, c-format
+msgid "report elements lost in the inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:409
+#, c-format
+msgid "new price for the product"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:421
+#, c-format
+msgid "the are value with errors"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:422
+#, c-format
+msgid "update product with new stock and price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:463
+#, c-format
+msgid "There is no products yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:86
#, c-format
msgid "product updated successfully"
msgstr ""
-#: src/paths/instance/products/list/index.tsx:62
+#: src/paths/instance/products/list/index.tsx:92
#, c-format
msgid "could not update the product"
msgstr ""
-#: src/paths/instance/products/list/index.tsx:70
+#: src/paths/instance/products/list/index.tsx:103
#, c-format
msgid "product delete successfully"
msgstr ""
-#: src/paths/instance/products/list/index.tsx:73
+#: src/paths/instance/products/list/index.tsx:109
#, c-format
msgid "could not delete the product"
msgstr ""
-#: src/paths/instance/tips/list/Table.tsx:59
+#: src/paths/instance/products/update/UpdatePage.tsx:56
+#, c-format
+msgid "Product id:"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:95
+#, c-format
+msgid ""
+"To complete the setup of the reserve, you must now initiate a wire transfer "
+"using the given wire transfer subject and crediting the specified amount to "
+"the indicated account of the exchange."
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:102
+#, c-format
+msgid "If your system supports RFC 8905, you can do this by opening this URI:"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:83
+#, c-format
+msgid "it should be greater than 0"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:88
+#, c-format
+msgid "must be a valid URL"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:107
+#, c-format
+msgid "Initial balance"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:108
+#, c-format
+msgid "balance prior to deposit"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:112
+#, c-format
+msgid "Exchange URL"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:113
+#, c-format
+msgid "URL of exchange"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:148
+#, c-format
+msgid "Next"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:186
+#, c-format
+msgid "Wire method"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:187
+#, c-format
+msgid "method to use for wire transfer"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:189
+#, c-format
+msgid "Select one wire method"
+msgstr ""
+
+#: src/paths/instance/reserves/create/index.tsx:62
+#, c-format
+msgid "could not create reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:77
+#, c-format
+msgid "Valid until"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:82
+#, c-format
+msgid "Created balance"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:99
+#, c-format
+msgid "Exchange balance"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:104
+#, c-format
+msgid "Picked up"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:109
+#, c-format
+msgid "Committed"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:116
+#, c-format
+msgid "Account address"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:119
+#, c-format
+msgid "Subject"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:130
#, c-format
msgid "Tips"
msgstr ""
-#: src/paths/instance/tips/list/Table.tsx:111
+#: src/paths/instance/reserves/details/DetailPage.tsx:193
#, c-format
-msgid "Committed amount"
+msgid "No tips has been authorized from this reserve"
msgstr ""
-#: src/paths/instance/tips/list/Table.tsx:112
+#: src/paths/instance/reserves/details/DetailPage.tsx:213
#, c-format
-msgid "Exchange initial amount"
+msgid "Authorized"
msgstr ""
-#: src/paths/instance/tips/list/Table.tsx:113
+#: src/paths/instance/reserves/details/DetailPage.tsx:222
#, c-format
-msgid "Merchant initial amount"
+msgid "Expiration"
msgstr ""
-#: src/paths/instance/tips/list/Table.tsx:148
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:108
#, c-format
-msgid "There is no tips yet, add more pressing the + sign"
+msgid "amount of tip"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:50
-#: src/paths/instance/transfers/create/CreatePage.tsx:54
-#: src/paths/instance/transfers/create/CreatePage.tsx:55
-#: src/paths/instance/transfers/create/CreatePage.tsx:56
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:112
#, c-format
-msgid "cannot be empty"
+msgid "Justification"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:114
+#, c-format
+msgid "reason for the tip"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:118
+#, c-format
+msgid "URL after tip"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:119
+#, c-format
+msgid "URL to visit after tip payment"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:65
+#, c-format
+msgid "Reserves not yet funded"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:89
+#, c-format
+msgid "Reserves ready"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:95
+#, c-format
+msgid "add new reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:143
+#, c-format
+msgid "Expires at"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:146
+#, c-format
+msgid "Initial"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:202
+#, c-format
+msgid "delete selected reserve from the database"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:210
+#, c-format
+msgid "authorize new tip from selected reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:237
+#, c-format
+msgid ""
+"There is no ready reserves yet, add more pressing the + sign or fund them"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:264
+#, c-format
+msgid "Expected Balance"
+msgstr ""
+
+#: src/paths/instance/reserves/list/index.tsx:110
+#, c-format
+msgid "could not create the tip"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:77
+#, c-format
+msgid "should not be empty"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:93
+#, c-format
+msgid "should be greater that 0"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:96
+#, c-format
+msgid "can't be empty"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:100
+#, c-format
+msgid "to short"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:108
+#, c-format
+msgid "just letters and numbers from 2 to 7"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:110
+#, c-format
+msgid "size of the key should be 32"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:137
+#, c-format
+msgid "Identifier"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:138
+#, c-format
+msgid "Name of the template in URLs."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:144
+#, c-format
+msgid "Describe what this template stands for"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:149
+#, c-format
+msgid "Fixed summary"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:150
+#, c-format
+msgid "If specified, this template will create order with the same summary"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:154
+#, c-format
+msgid "Fixed price"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:155
+#, c-format
+msgid "If specified, this template will create order with the same price"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:159
+#, c-format
+msgid "Minimum age"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:161
+#, c-format
+msgid "Is this contract restricted to some age?"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:165
+#, c-format
+msgid "Payment timeout"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:167
+#, c-format
+msgid ""
+"How much time has the customer to complete the payment once the order was "
+"created."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:171
+#, c-format
+msgid "Verification algorithm"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:172
+#, c-format
+msgid "Algorithm to use to verify transaction in offline mode"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:180
+#, c-format
+msgid "Point-of-sale key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:182
+#, c-format
+msgid "Useful to validate the purchase"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:196
+#, c-format
+msgid "generate random secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:203
+#, c-format
+msgid "random"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:208
+#, c-format
+msgid "show secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:209
+#, c-format
+msgid "hide secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:216
+#, c-format
+msgid "hide"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:218
+#, c-format
+msgid "show"
+msgstr ""
+
+#: src/paths/instance/templates/create/index.tsx:52
+#, c-format
+msgid "could not inform template"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:54
+#, c-format
+msgid "Amount is required"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:58
+#, c-format
+msgid "Order summary is required"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:86
+#, c-format
+msgid "New order for template"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:108
+#, c-format
+msgid "Amount of the order"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:113
+#, c-format
+msgid "Order summary"
+msgstr ""
+
+#: src/paths/instance/templates/use/index.tsx:92
+#, c-format
+msgid "could not create order from template"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:131
+#, c-format
+msgid ""
+"Here you can specify a default value for fields that are not fixed. Default "
+"values can be edited by the customer before the payment."
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:148
+#, c-format
+msgid "Fixed amount"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:149
+#, c-format
+msgid "Default amount"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:51
+#: src/paths/instance/templates/qr/QrPage.tsx:161
#, c-format
-msgid "check the id, doest look valid"
+msgid "Default summary"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:52
+#: src/paths/instance/templates/qr/QrPage.tsx:177
+#, c-format
+msgid "Print"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:184
+#, c-format
+msgid "Setup TOTP"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:65
+#, c-format
+msgid "Templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:70
+#, c-format
+msgid "add new templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:142
+#, c-format
+msgid "load more templates before the first one"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:146
+#, c-format
+msgid "load newer templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:181
+#, c-format
+msgid "delete selected templates from the database"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:188
+#, c-format
+msgid "use template to create new order"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:195
+#, c-format
+msgid "create qr code for the template"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:210
+#, c-format
+msgid "load more templates after the last one"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:214
+#, c-format
+msgid "load older templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:231
+#, c-format
+msgid "There is no templates yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/templates/list/index.tsx:104
+#, c-format
+msgid "template delete successfully"
+msgstr ""
+
+#: src/paths/instance/templates/list/index.tsx:110
+#, c-format
+msgid "could not delete the template"
+msgstr ""
+
+#: src/paths/instance/templates/update/index.tsx:90
+#, c-format
+msgid "could not update template"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:57
+#, c-format
+msgid "should be one of '%1$s'"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:85
+#, c-format
+msgid "Webhook ID to use"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:89
+#, c-format
+msgid "Event"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:90
+#, c-format
+msgid "The event of the webhook: why the webhook is used"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:94
+#, c-format
+msgid "Method"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:95
+#, c-format
+msgid "Method used by the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:99
+#, c-format
+msgid "URL"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:100
+#, c-format
+msgid "URL of the webhook where the customer will be redirected"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:104
+#, c-format
+msgid "Header"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:106
+#, c-format
+msgid "Header template of the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:111
+#, c-format
+msgid "Body"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:112
+#, c-format
+msgid "Body template by the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:61
+#, c-format
+msgid "Webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:66
+#, c-format
+msgid "add new webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:137
+#, c-format
+msgid "load more webhooks before the first one"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:141
+#, c-format
+msgid "load newer webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:151
+#, c-format
+msgid "Event type"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:176
+#, c-format
+msgid "delete selected webhook from the database"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:198
+#, c-format
+msgid "load more webhooks after the last one"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:202
+#, c-format
+msgid "load older webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:219
+#, c-format
+msgid "There is no webhooks yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/index.tsx:94
+#, c-format
+msgid "webhook delete successfully"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/index.tsx:100
+#, c-format
+msgid "could not delete the webhook"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:63
+#, c-format
+msgid "check the id, does not look valid"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:65
#, c-format
msgid "should have 52 characters, current %1$s"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:57
+#: src/paths/instance/transfers/create/CreatePage.tsx:72
#, c-format
msgid "URL doesn't have the right format"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:74
+#: src/paths/instance/transfers/create/CreatePage.tsx:98
#, c-format
-msgid "Transfer ID"
+msgid "Credited bank account"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:76
+#: src/paths/instance/transfers/create/CreatePage.tsx:100
#, c-format
-msgid "Account Address"
+msgid "Select one account"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:82
-#: src/paths/instance/transfers/list/Table.tsx:125
+#: src/paths/instance/transfers/create/CreatePage.tsx:101
#, c-format
-msgid "Exchange URL"
+msgid "Bank account of the merchant where the payment was received"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:105
+#, c-format
+msgid "Wire transfer ID"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:107
+#, c-format
+msgid ""
+"unique identifier of the wire transfer used by the exchange, must be 52 "
+"characters long"
msgstr ""
-#: src/paths/instance/transfers/create/index.tsx:49
+#: src/paths/instance/transfers/create/CreatePage.tsx:112
+#, c-format
+msgid ""
+"Base URL of the exchange that made the transfer, should have been in the "
+"wire transfer subject"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:117
+#, c-format
+msgid "Amount credited"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:118
+#, c-format
+msgid "Actual amount that was wired to the merchant's bank account"
+msgstr ""
+
+#: src/paths/instance/transfers/create/index.tsx:58
#, c-format
msgid "could not inform transfer"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:118
+#: src/paths/instance/transfers/list/Table.tsx:61
+#, c-format
+msgid "Transfers"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:66
+#, c-format
+msgid "add new transfer"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:129
+#, c-format
+msgid "load more transfers before the first one"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:133
#, c-format
msgid "load newer transfers"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:123
+#: src/paths/instance/transfers/list/Table.tsx:143
#, c-format
msgid "Credit"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:126
+#: src/paths/instance/transfers/list/Table.tsx:152
#, c-format
msgid "Confirmed"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:127
-#: src/paths/instance/transfers/list/index.tsx:60
+#: src/paths/instance/transfers/list/Table.tsx:155
#, c-format
msgid "Verified"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:128
+#: src/paths/instance/transfers/list/Table.tsx:158
#, c-format
msgid "Executed at"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:138
-#: src/paths/instance/transfers/list/Table.tsx:139
+#: src/paths/instance/transfers/list/Table.tsx:171
#, c-format
msgid "yes"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:138
-#: src/paths/instance/transfers/list/Table.tsx:139
+#: src/paths/instance/transfers/list/Table.tsx:171
#, c-format
msgid "no"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:140
+#: src/paths/instance/transfers/list/Table.tsx:181
#, c-format
msgid "unknown"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:145
+#: src/paths/instance/transfers/list/Table.tsx:187
+#, c-format
+msgid "delete selected transfer from the database"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:202
+#, c-format
+msgid "load more transfer after the last one"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:206
#, c-format
msgid "load older transfers"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:154
+#: src/paths/instance/transfers/list/Table.tsx:223
#, c-format
msgid "There is no transfer yet, add more pressing the + sign"
msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:79
+#, c-format
+msgid "filter by account address"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:100
+#, c-format
+msgid "only show wire transfers confirmed by the merchant"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:110
+#, c-format
+msgid "only show wire transfers claimed by the exchange"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:113
+#, c-format
+msgid "Unverified"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:69
+#, c-format
+msgid "is not valid"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:94
+#, c-format
+msgid "is not a number"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:96
+#, c-format
+msgid "must be 1 or greater"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:107
+#, c-format
+msgid "max 7 lines"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:178
+#, c-format
+msgid "change authorization configuration"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:217
+#, c-format
+msgid "Need to complete marked fields and choose authorization method"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:82
+#, c-format
+msgid "This is not a valid bitcoin address."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:95
+#, c-format
+msgid "This is not a valid Ethereum address."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:118
+#, c-format
+msgid "IBAN numbers usually have more that 4 digits"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:120
+#, c-format
+msgid "IBAN numbers usually have less that 34 digits"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:128
+#, c-format
+msgid "IBAN country code not found"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:153
+#, c-format
+msgid "IBAN number is not valid, checksum is wrong"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:248
+#, c-format
+msgid "Target type"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:249
+#, c-format
+msgid "Method to use for wire transfer"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:258
+#, c-format
+msgid "Routing"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:259
+#, c-format
+msgid "Routing number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:263
+#, c-format
+msgid "Account"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:264
+#, c-format
+msgid "Account number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:273
+#, c-format
+msgid "Business Identifier Code."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:282
+#, c-format
+msgid "Bank Account Number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:292
+#, c-format
+msgid "Unified Payment Interface."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:301
+#, c-format
+msgid "Bitcoin protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:310
+#, c-format
+msgid "Ethereum protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:319
+#, c-format
+msgid "Interledger protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:328
+#, c-format
+msgid "Host"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:329
+#, c-format
+msgid "Bank host."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:334
+#, c-format
+msgid "Bank account."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:343
+#, c-format
+msgid "Bank account owner's name."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:370
+#, c-format
+msgid "No accounts yet."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:52
+#, c-format
+msgid ""
+"Name of the instance in URLs. The 'default' instance is special in that it "
+"is used to administer other instances."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:58
+#, c-format
+msgid "Business name"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:59
+#, c-format
+msgid "Legal name of the business represented by this instance."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:64
+#, c-format
+msgid "Email"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:65
+#, c-format
+msgid "Contact email"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:70
+#, c-format
+msgid "Website URL"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:71
+#, c-format
+msgid "URL."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:76
+#, c-format
+msgid "Logo"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:77
+#, c-format
+msgid "Logo image."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:82
+#, c-format
+msgid "Bank account"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:83
+#, c-format
+msgid "URI specifying bank account for crediting revenue."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:88
+#, c-format
+msgid "Default max deposit fee"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:89
+#, c-format
+msgid ""
+"Maximum deposit fees this merchant is willing to pay per order by default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:94
+#, c-format
+msgid "Default max wire fee"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:95
+#, c-format
+msgid ""
+"Maximum wire fees this merchant is willing to pay per wire transfer by "
+"default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:100
+#, c-format
+msgid "Default wire fee amortization"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:101
+#, c-format
+msgid ""
+"Number of orders excess wire transfer fees will be divided by to compute per "
+"order surcharge."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:107
+#, c-format
+msgid "Physical location of the merchant."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:114
+#, c-format
+msgid "Jurisdiction"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:115
+#, c-format
+msgid "Jurisdiction for legal disputes with the merchant."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:122
+#, c-format
+msgid "Default payment delay"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:124
+#, c-format
+msgid ""
+"Time customers have to pay an order before the offer expires by default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:129
+#, c-format
+msgid "Default wire transfer delay"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:130
+#, c-format
+msgid ""
+"Maximum time an exchange is allowed to delay wiring funds to the merchant, "
+"enabling it to aggregate smaller payments into larger wire transfers and "
+"reducing wire fees."
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:164
+#, c-format
+msgid "Instance id"
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:173
+#, c-format
+msgid "Change the authorization method use for this instance."
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:182
+#, c-format
+msgid "Manage access token"
+msgstr ""
+
+#: src/paths/instance/update/index.tsx:112
+#, c-format
+msgid "Failed to create instance"
+msgstr ""
+
+#: src/components/exception/login.tsx:74
+#, c-format
+msgid "Login required"
+msgstr ""
+
+#: src/components/exception/login.tsx:80
+#, c-format
+msgid "Please enter your access token."
+msgstr ""
+
+#: src/components/exception/login.tsx:108
+#, c-format
+msgid "Access Token"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:171
+#, c-format
+msgid "The request to the backend take too long and was cancelled"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:172
+#, c-format
+msgid "Diagnostic from %1$s is \"%2$s\""
+msgstr ""
+
+#: src/InstanceRoutes.tsx:178
+#, c-format
+msgid "The backend reported a problem: HTTP status #%1$s"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:179
+#, c-format
+msgid "Diagnostic from %1$s is '%2$s'"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:196
+#, c-format
+msgid "Access denied"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:197
+#, c-format
+msgid "The access token provided is invalid."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:212
+#, c-format
+msgid "No 'default' instance configured yet."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:213
+#, c-format
+msgid "Create a 'default' instance to begin using the merchant backoffice."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:630
+#, c-format
+msgid "The access token provided is invalid"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:664
+#, c-format
+msgid "Hide for today"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:82
+#, c-format
+msgid "Instance"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:91
+#, c-format
+msgid "Settings"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:167
+#, c-format
+msgid "Connection"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:209
+#, c-format
+msgid "New"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:219
+#, c-format
+msgid "List"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:234
+#, c-format
+msgid "Log out"
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:71
+#, c-format
+msgid "Check your token is valid"
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:90
+#, c-format
+msgid "Couldn't access the server."
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:91
+#, c-format
+msgid "Could not infer instance id from url %1$s"
+msgstr ""
+
+#: src/Application.tsx:104
+#, c-format
+msgid "Server not found"
+msgstr ""
+
+#: src/Application.tsx:118
+#, c-format
+msgid "Server response with an error code"
+msgstr ""
+
+#: src/Application.tsx:120
+#, c-format
+msgid "Got message %1$s from %2$s"
+msgstr ""
+
+#: src/Application.tsx:131
+#, c-format
+msgid "Response from server is unreadable, http status: %1$s"
+msgstr ""
+
+#: src/Application.tsx:144
+#, c-format
+msgid "Unexpected Error"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:101
+#, c-format
+msgid "The value %1$s is invalid for a payment url"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:110
+#, c-format
+msgid "add element to the list"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:112
+#, c-format
+msgid "add"
+msgstr ""
+
+#: src/components/form/InputSecured.tsx:37
+#, c-format
+msgid "Deleting"
+msgstr ""
+
+#: src/components/form/InputSecured.tsx:41
+#, c-format
+msgid "Changing"
+msgstr ""
+
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:87
+#, c-format
+msgid "Order ID"
+msgstr ""
+
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:101
+#, c-format
+msgid "Payment URL"
+msgstr ""
diff --git a/packages/merchant-backoffice-ui/src/i18n/index.tsx b/packages/merchant-backoffice-ui/src/i18n/index.tsx
deleted file mode 100644
index f2638fc05..000000000
--- a/packages/merchant-backoffice-ui/src/i18n/index.tsx
+++ /dev/null
@@ -1,215 +0,0 @@
-/*
- 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/>
- */
-
-/**
- * Translation helpers for React components and template literals.
- */
-
-/**
- * Imports
- */
-import { ComponentChild, ComponentChildren, h, Fragment, VNode } from "preact";
-
-import { useTranslationContext } from "../context/translation.js";
-
-export type Translator = (
- stringSeq: TemplateStringsArray,
- ...values: any[]
-) => string;
-export function useTranslator(): Translator {
- const ctx = useTranslationContext();
- const jed = ctx.handler;
- return function str(
- stringSeq: TemplateStringsArray,
- ...values: any[]
- ): string {
- const s = toI18nString(stringSeq);
- if (!s) return s;
- const tr = jed
- .translate(s)
- .ifPlural(1, s)
- .fetch(...values);
- return tr;
- };
-}
-
-/**
- * Convert template strings to a msgid
- */
-function toI18nString(stringSeq: ReadonlyArray<string>): string {
- let s = "";
- for (let i = 0; i < stringSeq.length; i++) {
- s += stringSeq[i];
- if (i < stringSeq.length - 1) {
- s += `%${i + 1}$s`;
- }
- }
- return s;
-}
-
-interface TranslateSwitchProps {
- target: number;
- children: ComponentChildren;
-}
-
-function stringifyChildren(children: ComponentChildren): string {
- let n = 1;
- const ss = (children instanceof Array ? children : [children]).map((c) => {
- if (typeof c === "string") {
- return c;
- }
- return `%${n++}$s`;
- });
- const s = ss.join("").replace(/ +/g, " ").trim();
- return s;
-}
-
-interface TranslateProps {
- children: ComponentChildren;
- /**
- * Component that the translated element should be wrapped in.
- * Defaults to "div".
- */
- wrap?: any;
-
- /**
- * Props to give to the wrapped component.
- */
- wrapProps?: any;
-}
-
-function getTranslatedChildren(
- translation: string,
- children: ComponentChildren
-): ComponentChild[] {
- const tr = translation.split(/%(\d+)\$s/);
- const childArray = children instanceof Array ? children : [children];
- // Merge consecutive string children.
- const placeholderChildren = Array<ComponentChild>();
- for (let i = 0; i < childArray.length; i++) {
- const x = childArray[i];
- if (x === undefined) {
- continue;
- } else if (typeof x === "string") {
- continue;
- } else {
- placeholderChildren.push(x);
- }
- }
- const result = Array<ComponentChild>();
- for (let i = 0; i < tr.length; i++) {
- if (i % 2 == 0) {
- // Text
- result.push(tr[i]);
- } else {
- const childIdx = Number.parseInt(tr[i], 10) - 1;
- result.push(placeholderChildren[childIdx]);
- }
- }
- return result;
-}
-
-/**
- * Translate text node children of this component.
- * If a child component might produce a text node, it must be wrapped
- * in a another non-text element.
- *
- * Example:
- * ```
- * <Translate>
- * Hello. Your score is <span><PlayerScore player={player} /></span>
- * </Translate>
- * ```
- */
-export function Translate({ children }: TranslateProps): VNode {
- const s = stringifyChildren(children);
- const ctx = useTranslationContext();
- const translation: string = ctx.handler.ngettext(s, s, 1);
- const result = getTranslatedChildren(translation, children);
- return <Fragment>{result}</Fragment>;
-}
-
-/**
- * Switch translation based on singular or plural based on the target prop.
- * Should only contain TranslateSingular and TransplatePlural as children.
- *
- * Example:
- * ```
- * <TranslateSwitch target={n}>
- * <TranslateSingular>I have {n} apple.</TranslateSingular>
- * <TranslatePlural>I have {n} apples.</TranslatePlural>
- * </TranslateSwitch>
- * ```
- */
-export function TranslateSwitch({ children, target }: TranslateSwitchProps) {
- let singular: VNode<TranslationPluralProps> | undefined;
- let plural: VNode<TranslationPluralProps> | undefined;
- // const children = this.props.children;
- if (children) {
- (children instanceof Array ? children : [children]).forEach(
- (child: any) => {
- if (child.type === TranslatePlural) {
- plural = child;
- }
- if (child.type === TranslateSingular) {
- singular = child;
- }
- }
- );
- }
- if (!singular || !plural) {
- console.error("translation not found");
- return h("span", {}, ["translation not found"]);
- }
- singular.props.target = target;
- plural.props.target = target;
- // We're looking up the translation based on the
- // singular, even if we must use the plural form.
- return singular;
-}
-
-interface TranslationPluralProps {
- children: ComponentChildren;
- target: number;
-}
-
-/**
- * See [[TranslateSwitch]].
- */
-export function TranslatePlural({
- children,
- target,
-}: TranslationPluralProps): VNode {
- const s = stringifyChildren(children);
- const ctx = useTranslationContext();
- const translation = ctx.handler.ngettext(s, s, 1);
- const result = getTranslatedChildren(translation, children);
- return <Fragment>{result}</Fragment>;
-}
-
-/**
- * See [[TranslateSwitch]].
- */
-export function TranslateSingular({
- children,
- target,
-}: TranslationPluralProps): VNode {
- const s = stringifyChildren(children);
- const ctx = useTranslationContext();
- const translation = ctx.handler.ngettext(s, s, target);
- const result = getTranslatedChildren(translation, children);
- return <Fragment>{result}</Fragment>;
-}
diff --git a/packages/merchant-backoffice-ui/src/i18n/it.po b/packages/merchant-backoffice-ui/src/i18n/it.po
index 6b35bd0ce..4055af10e 100644
--- a/packages/merchant-backoffice-ui/src/i18n/it.po
+++ b/packages/merchant-backoffice-ui/src/i18n/it.po
@@ -12,1046 +12,2731 @@
# You should have received a copy of the GNU General Public License along with
# TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
#
-#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: Taler Wallet\n"
-"Report-Msgid-Bugs-To: \n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
"POT-Creation-Date: 2016-11-23 00:00+0100\n"
-"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
-"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
-"Language-Team: LANGUAGE <LL@li.org>\n"
-"Language: \n"
+"PO-Revision-Date: 2023-08-16 12:43+0000\n"
+"Last-Translator: Krystian Baran <kiszkot@murena.io>\n"
+"Language-Team: Italian <https://weblate.taler.net/projects/gnu-taler/"
+"merchant-backoffice/it/>\n"
+"Language: it\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 4.13.1\n"
-#: src/ApplicationReadyRoutes.tsx:50 src/InstanceRoutes.tsx:118
-#: src/InstanceRoutes.tsx:299
+#: src/components/modal/index.tsx:71
#, c-format
-msgid "Access denied"
+msgid "Cancel"
msgstr ""
-#: src/ApplicationReadyRoutes.tsx:51 src/InstanceRoutes.tsx:118
-#: src/InstanceRoutes.tsx:300
+#: src/components/modal/index.tsx:79
#, c-format
-msgid "Check your token is valid"
+msgid "%1$s"
msgstr ""
-#: src/ApplicationReadyRoutes.tsx:72
+#: src/components/modal/index.tsx:84
#, c-format
-msgid "Couldn't access the server."
+msgid "Close"
msgstr ""
-#: src/ApplicationReadyRoutes.tsx:73
+#: src/components/modal/index.tsx:124
#, c-format
-msgid "Could not infer instance id from url %1$s"
+msgid "Continue"
msgstr ""
-#: src/InstanceRoutes.tsx:109
+#: src/components/modal/index.tsx:178
#, c-format
-msgid "HTTP status #%1$s: Server reported a problem"
+msgid "Clear"
msgstr ""
-#: src/InstanceRoutes.tsx:110
+#: src/components/modal/index.tsx:190
#, c-format
-msgid "Got message: \"%1$s\" from: %2$s"
+msgid "Confirm"
msgstr ""
-#: src/InstanceRoutes.tsx:127
+#: src/components/modal/index.tsx:296
#, c-format
-msgid "No default instance"
+msgid "is not the same as the current access token"
msgstr ""
-#: src/InstanceRoutes.tsx:128
+#: src/components/modal/index.tsx:299
#, c-format
-msgid ""
-"in order to use merchant backoffice, you should create the default instance"
+msgid "cannot be empty"
msgstr ""
-#: src/InstanceRoutes.tsx:288
+#: src/components/modal/index.tsx:301
#, c-format
-msgid "Server reported a problem: HTTP status #%1$s"
+msgid "cannot be the same as the old token"
msgstr ""
-#: src/InstanceRoutes.tsx:289
+#: src/components/modal/index.tsx:305
#, c-format
-msgid "Got message: %1$s from: %2$s"
+msgid "is not the same"
msgstr ""
-#: src/components/exception/login.tsx:46
+#: src/components/modal/index.tsx:315
#, c-format
-msgid "Login required"
+msgid "You are updating the access token from instance with id %1$s"
+msgstr ""
+
+#: src/components/modal/index.tsx:331
+#, c-format
+msgid "Old access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:332
+#, c-format
+msgid "access token currently in use"
+msgstr ""
+
+#: src/components/modal/index.tsx:338
+#, c-format
+msgid "New access token"
msgstr ""
-#: src/components/exception/login.tsx:49
+#: src/components/modal/index.tsx:339
+#, c-format
+msgid "next access token to be used"
+msgstr ""
+
+#: src/components/modal/index.tsx:344
+#, c-format
+msgid "Repeat access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:345
+#, c-format
+msgid "confirm the same access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:350
+#, c-format
+msgid "Clearing the access token will mean public access to the instance"
+msgstr ""
+
+#: src/components/modal/index.tsx:377
+#, c-format
+msgid "cannot be the same as the old access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:394
+#, c-format
+msgid "You are setting the access token for the new instance"
+msgstr ""
+
+#: src/components/modal/index.tsx:420
#, c-format
msgid ""
-"Please enter your auth token. Token should have \"secret-token:\" and start "
-"with Bearer or ApiKey"
+"With external authorization method no check will be done by the merchant "
+"backend"
msgstr ""
-#: src/components/exception/login.tsx:86 src/components/modal/index.tsx:53
-#: src/components/modal/index.tsx:75 src/paths/admin/create/CreatePage.tsx:115
-#: src/paths/instance/orders/create/CreatePage.tsx:325
-#: src/paths/instance/products/create/CreatePage.tsx:51
-#: src/paths/instance/products/list/Table.tsx:174
-#: src/paths/instance/products/list/Table.tsx:228
-#: src/paths/instance/products/update/UpdatePage.tsx:55
-#: src/paths/instance/transfers/create/CreatePage.tsx:89
-#: src/paths/instance/update/UpdatePage.tsx:134
+#: src/components/modal/index.tsx:436
#, c-format
-msgid "Confirm"
+msgid "Set external authorization"
msgstr ""
-#: src/components/form/InputArray.tsx:72
+#: src/components/modal/index.tsx:448
#, c-format
-msgid "The value %1$s is invalid for a payment url"
+msgid "Set access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:470
+#, c-format
+msgid "Operation in progress..."
+msgstr ""
+
+#: src/components/modal/index.tsx:479
+#, c-format
+msgid "The operation will be automatically canceled after %1$s seconds"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:80
+#, c-format
+msgid "Instances"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:93
+#, c-format
+msgid "Delete"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:99
+#, c-format
+msgid "add new instance"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:178
+#, c-format
+msgid "ID"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:181
+#, c-format
+msgid "Name"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:220
+#, c-format
+msgid "Edit"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:237
+#, c-format
+msgid "Purge"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:261
+#, c-format
+msgid "There is no instances yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:68
+#, c-format
+msgid "Only show active instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:71
+#, c-format
+msgid "Active"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:78
+#, c-format
+msgid "Only show deleted instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:81
+#, c-format
+msgid "Deleted"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:88
+#, c-format
+msgid "Show all instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:91
+#, c-format
+msgid "All"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:101
+#, c-format
+msgid "Instance \"%1$s\" (ID: %2$s) has been deleted"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:106
+#, c-format
+msgid "Failed to delete instance"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:124
+#, c-format
+msgid "Instance '%1$s' (ID: %2$s) has been disabled"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:129
+#, c-format
+msgid "Failed to purge instance"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:41
+#, c-format
+msgid "Pending KYC verification"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:66
+#, c-format
+msgid "Timed out"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:103
+#, c-format
+msgid "Exchange"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:106
+#, c-format
+msgid "Target account"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:109
+#, c-format
+msgid "KYC URL"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:144
+#, c-format
+msgid "Code"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:147
+#, c-format
+msgid "Http Status"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:177
+#, c-format
+msgid "No pending kyc verification!"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:123
+#, c-format
+msgid "change value to unknown date"
msgstr ""
-#: src/components/form/InputDate.tsx:67
-#: src/paths/instance/orders/list/index.tsx:123
+#: src/components/form/InputDate.tsx:124
#, c-format
-msgid "pick a date"
+msgid "change value to empty"
msgstr ""
-#: src/components/form/InputDate.tsx:81
+#: src/components/form/InputDate.tsx:131
#, c-format
msgid "clear"
msgstr ""
-#: src/components/form/InputDate.tsx:83
-#: src/paths/instance/transfers/list/Table.tsx:140
+#: src/components/form/InputDate.tsx:136
#, c-format
-msgid "never"
+msgid "change value to never"
msgstr ""
-#: src/components/form/InputImage.tsx:80
+#: src/components/form/InputDate.tsx:141
#, c-format
-msgid "Image should be smaller than 1 MB"
+msgid "never"
msgstr ""
-#: src/components/form/InputLocation.tsx:28
+#: src/components/form/InputLocation.tsx:29
#, c-format
msgid "Country"
msgstr ""
-#: src/components/form/InputLocation.tsx:30
-#: src/paths/admin/create/CreatePage.tsx:99
-#: src/paths/instance/transfers/list/Table.tsx:124
-#: src/paths/instance/update/UpdatePage.tsx:118
+#: src/components/form/InputLocation.tsx:33
#, c-format
msgid "Address"
msgstr ""
-#: src/components/form/InputLocation.tsx:34
+#: src/components/form/InputLocation.tsx:39
#, c-format
msgid "Building number"
msgstr ""
-#: src/components/form/InputLocation.tsx:35
+#: src/components/form/InputLocation.tsx:41
#, c-format
msgid "Building name"
msgstr ""
-#: src/components/form/InputLocation.tsx:36
+#: src/components/form/InputLocation.tsx:42
#, c-format
msgid "Street"
msgstr ""
-#: src/components/form/InputLocation.tsx:37
+#: src/components/form/InputLocation.tsx:43
#, c-format
msgid "Post code"
msgstr ""
-#: src/components/form/InputLocation.tsx:38
+#: src/components/form/InputLocation.tsx:44
#, c-format
msgid "Town location"
msgstr ""
-#: src/components/form/InputLocation.tsx:39
+#: src/components/form/InputLocation.tsx:45
#, c-format
msgid "Town"
msgstr ""
-#: src/components/form/InputLocation.tsx:40
+#: src/components/form/InputLocation.tsx:46
#, c-format
msgid "District"
msgstr ""
-#: src/components/form/InputLocation.tsx:41
+#: src/components/form/InputLocation.tsx:49
#, c-format
msgid "Country subdivision"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:59
+#: src/components/form/InputSearchProduct.tsx:66
#, c-format
msgid "Product id"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:60
-#: src/components/product/ProductForm.tsx:99
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:122
-#: src/paths/instance/orders/list/Table.tsx:227
-#: src/paths/instance/products/list/Table.tsx:86
+#: src/components/form/InputSearchProduct.tsx:69
#, c-format
msgid "Description"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:73
-#: src/components/form/InputTaxes.tsx:81
-#: src/paths/admin/create/CreatePage.tsx:87 src/paths/admin/list/Table.tsx:110
-#: src/paths/instance/details/DetailPage.tsx:76
-#: src/paths/instance/update/UpdatePage.tsx:106
+#: src/components/form/InputSearchProduct.tsx:94
#, c-format
-msgid "Name"
+msgid "Product"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:102
+#: src/components/form/InputSearchProduct.tsx:95
#, c-format
-msgid "loading..."
+msgid "search products by it's description or id"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:108
+#: src/components/form/InputSearchProduct.tsx:151
#, c-format
-msgid "no products found"
+msgid "no products found with that description"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:116
+#: src/components/product/InventoryProductForm.tsx:56
#, c-format
-msgid "no results"
+msgid "You must enter a valid product identifier."
msgstr ""
-#: src/components/form/InputSecured.tsx:33
+#: src/components/product/InventoryProductForm.tsx:64
#, c-format
-msgid "Deleting"
+msgid "Quantity must be greater than 0!"
msgstr ""
-#: src/components/form/InputSecured.tsx:34
+#: src/components/product/InventoryProductForm.tsx:76
#, c-format
-msgid "Changing"
+msgid ""
+"This quantity exceeds remaining stock. Currently, only %1$s units remain "
+"unreserved in stock."
msgstr ""
-#: src/components/form/InputSecured.tsx:60
+#: src/components/product/InventoryProductForm.tsx:109
#, c-format
-msgid "Manage token"
+msgid "Quantity"
msgstr ""
-#: src/components/form/InputSecured.tsx:83
+#: src/components/product/InventoryProductForm.tsx:110
#, c-format
-msgid "Update"
+msgid "how many products will be added"
msgstr ""
-#: src/components/form/InputSecured.tsx:100
-#: src/paths/instance/orders/create/CreatePage.tsx:252
-#: src/paths/instance/orders/create/CreatePage.tsx:273
+#: src/components/product/InventoryProductForm.tsx:117
+#, c-format
+msgid "Add from inventory"
+msgstr ""
+
+#: src/components/form/InputImage.tsx:105
+#, c-format
+msgid "Image should be smaller than 1 MB"
+msgstr ""
+
+#: src/components/form/InputImage.tsx:110
+#, c-format
+msgid "Add"
+msgstr ""
+
+#: src/components/form/InputImage.tsx:115
#, c-format
msgid "Remove"
msgstr ""
-#: src/components/form/InputSecured.tsx:106 src/components/modal/index.tsx:52
-#: src/components/modal/index.tsx:73 src/paths/admin/create/CreatePage.tsx:114
-#: src/paths/instance/orders/create/CreatePage.tsx:324
-#: src/paths/instance/products/create/CreatePage.tsx:50
-#: src/paths/instance/products/list/Table.tsx:166
-#: src/paths/instance/products/list/Table.tsx:218
-#: src/paths/instance/products/update/UpdatePage.tsx:54
-#: src/paths/instance/transfers/create/CreatePage.tsx:88
-#: src/paths/instance/update/UpdatePage.tsx:133
+#: src/components/form/InputTaxes.tsx:113
#, c-format
-msgid "Cancel"
+msgid "No taxes configured for this product."
msgstr ""
-#: src/components/form/InputStock.tsx:91
+#: src/components/form/InputTaxes.tsx:119
#, c-format
-msgid "Manage stock"
+msgid "Amount"
+msgstr "Importo"
+
+#: src/components/form/InputTaxes.tsx:120
+#, c-format
+msgid ""
+"Taxes can be in currencies that differ from the main currency used by the "
+"merchant."
msgstr ""
-#: src/components/form/InputStock.tsx:93
+#: src/components/form/InputTaxes.tsx:122
#, c-format
-msgid "Infinite"
+msgid ""
+"Enter currency and value separated with a colon, e.g. &quot;USD:2.3&quot;."
msgstr ""
-#: src/components/form/InputStock.tsx:105
+#: src/components/form/InputTaxes.tsx:131
#, c-format
-msgid "lost cannot be greater that current + incoming (max %1$s)"
+msgid "Legal name of the tax, e.g. VAT or import duties."
msgstr ""
-#: src/components/form/InputStock.tsx:111
+#: src/components/form/InputTaxes.tsx:137
#, c-format
-msgid "current stock will change from %1$s to %2$s"
+msgid "add tax to the tax list"
msgstr ""
-#: src/components/form/InputStock.tsx:112
+#: src/components/product/NonInventoryProductForm.tsx:72
#, c-format
-msgid "current stock will stay at %1$s"
+msgid "describe and add a product that is not in the inventory list"
msgstr ""
-#: src/components/form/InputStock.tsx:129
-#: src/paths/instance/products/list/Table.tsx:204
+#: src/components/product/NonInventoryProductForm.tsx:75
#, c-format
-msgid "Incoming"
+msgid "Add custom product"
msgstr ""
-#: src/components/form/InputStock.tsx:130
-#: src/paths/instance/products/list/Table.tsx:205
+#: src/components/product/NonInventoryProductForm.tsx:86
#, c-format
-msgid "Lost"
+msgid "Complete information of the product"
msgstr ""
-#: src/components/form/InputStock.tsx:142
+#: src/components/product/NonInventoryProductForm.tsx:185
#, c-format
-msgid "Current"
+msgid "Image"
msgstr ""
-#: src/components/form/InputStock.tsx:145
+#: src/components/product/NonInventoryProductForm.tsx:186
#, c-format
-msgid "without stock"
+msgid "photo of the product"
msgstr ""
-#: src/components/form/InputStock.tsx:150
+#: src/components/product/NonInventoryProductForm.tsx:192
#, c-format
-msgid "Next restock"
+msgid "full product description"
msgstr ""
-#: src/components/form/InputStock.tsx:152
+#: src/components/product/NonInventoryProductForm.tsx:196
#, c-format
-msgid "Delivery address"
+msgid "Unit"
msgstr ""
-#: src/components/form/InputTaxes.tsx:73
+#: src/components/product/NonInventoryProductForm.tsx:197
#, c-format
-msgid "this product has no taxes"
+msgid "name of the product unit"
msgstr ""
-#: src/components/form/InputTaxes.tsx:77
-#: src/paths/instance/orders/details/DetailPage.tsx:145
-#: src/paths/instance/orders/details/DetailPage.tsx:296
-#: src/paths/instance/orders/list/Table.tsx:116
-#: src/paths/instance/transfers/create/CreatePage.tsx:84
+#: src/components/product/NonInventoryProductForm.tsx:201
#, c-format
-msgid "Amount"
+msgid "Price"
msgstr ""
-#: src/components/form/InputTaxes.tsx:78
+#: src/components/product/NonInventoryProductForm.tsx:202
#, c-format
-msgid "currency and value separated with colon"
+msgid "amount in the current currency"
msgstr ""
-#: src/components/form/InputTaxes.tsx:84
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:78
+#: src/components/product/NonInventoryProductForm.tsx:211
#, c-format
-msgid "Add"
+msgid "Taxes"
msgstr ""
-#: src/components/menu/SideBar.tsx:53
+#: src/components/product/ProductList.tsx:38
#, c-format
-msgid "Instance"
+msgid "image"
msgstr ""
-#: src/components/menu/SideBar.tsx:59
+#: src/components/product/ProductList.tsx:41
#, c-format
-msgid "Settings"
+msgid "description"
msgstr ""
-#: src/components/menu/SideBar.tsx:65
-#: src/paths/instance/orders/list/Table.tsx:60
+#: src/components/product/ProductList.tsx:44
#, c-format
-msgid "Orders"
+msgid "quantity"
msgstr ""
-#: src/components/menu/SideBar.tsx:71
-#: src/paths/instance/orders/create/CreatePage.tsx:258
-#: src/paths/instance/products/list/Table.tsx:48
+#: src/components/product/ProductList.tsx:47
#, c-format
-msgid "Products"
+msgid "unit price"
msgstr ""
-#: src/components/menu/SideBar.tsx:77
-#: src/paths/instance/transfers/list/Table.tsx:65
+#: src/components/product/ProductList.tsx:50
#, c-format
-msgid "Transfers"
+msgid "total price"
msgstr ""
-#: src/components/menu/SideBar.tsx:87
+#: src/paths/instance/orders/create/CreatePage.tsx:153
#, c-format
-msgid "Connection"
+msgid "required"
msgstr ""
-#: src/components/menu/SideBar.tsx:112 src/paths/admin/list/Table.tsx:57
+#: src/paths/instance/orders/create/CreatePage.tsx:157
#, c-format
-msgid "Instances"
+msgid "not valid"
msgstr ""
-#: src/components/menu/SideBar.tsx:116
+#: src/paths/instance/orders/create/CreatePage.tsx:159
#, c-format
-msgid "New"
+msgid "must be greater than 0"
msgstr ""
-#: src/components/menu/SideBar.tsx:122
+#: src/paths/instance/orders/create/CreatePage.tsx:164
#, c-format
-msgid "List"
+msgid "not a valid json"
msgstr ""
-#: src/components/menu/SideBar.tsx:129
+#: src/paths/instance/orders/create/CreatePage.tsx:170
#, c-format
-msgid "Log out"
+msgid "should be in the future"
msgstr ""
-#: src/components/modal/index.tsx:74
+#: src/paths/instance/orders/create/CreatePage.tsx:173
#, c-format
-msgid "Clear"
+msgid "refund deadline cannot be before pay deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:179
+#, c-format
+msgid "wire transfer deadline cannot be before refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:190
+#, c-format
+msgid "wire transfer deadline cannot be before pay deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:197
+#, c-format
+msgid "should have a refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:202
+#, c-format
+msgid "auto refund cannot be after refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:360
+#, c-format
+msgid "Manage products in order"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:369
+#, c-format
+msgid "Manage list of products in the order."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:391
+#, c-format
+msgid "Remove this product from the order."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:415
+#, c-format
+msgid "Total price"
msgstr ""
-#: src/components/modal/index.tsx:110 src/components/modal/index.tsx:111
+#: src/paths/instance/orders/create/CreatePage.tsx:417
#, c-format
-msgid "should be the same"
+msgid "total product price added up"
msgstr ""
-#: src/components/modal/index.tsx:111
+#: src/paths/instance/orders/create/CreatePage.tsx:430
#, c-format
-msgid "cannot be the same as before"
+msgid "Amount to be paid by the customer"
msgstr ""
-#: src/components/modal/index.tsx:114
+#: src/paths/instance/orders/create/CreatePage.tsx:436
+#, c-format
+msgid "Order price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:437
+#, c-format
+msgid "final order price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:444
+#, c-format
+msgid "Summary"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:445
+#, c-format
+msgid "Title of the order to be shown to the customer"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:450
+#, c-format
+msgid "Shipping and Fulfillment"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:455
+#, c-format
+msgid "Delivery date"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:456
+#, c-format
+msgid "Deadline for physical delivery assured by the merchant."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:461
+#, c-format
+msgid "Location"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:462
+#, c-format
+msgid "address where the products will be delivered"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:469
+#, c-format
+msgid "Fulfillment URL"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:470
+#, c-format
+msgid "URL to which the user will be redirected after successful payment."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:476
+#, c-format
+msgid "Taler payment options"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:477
+#, c-format
+msgid "Override default Taler payment settings for this order"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:481
+#, c-format
+msgid "Payment deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:482
#, c-format
msgid ""
-"You are updating the authorization token from instance %1$s with id %2$s"
+"Deadline for the customer to pay for the offer before it expires. Inventory "
+"products will be reserved until this deadline."
msgstr ""
-#: src/components/modal/index.tsx:124
+#: src/paths/instance/orders/create/CreatePage.tsx:486
#, c-format
-msgid "Old token"
+msgid "Refund deadline"
msgstr ""
-#: src/components/modal/index.tsx:125
+#: src/paths/instance/orders/create/CreatePage.tsx:487
#, c-format
-msgid "New token"
+msgid "Time until which the order can be refunded by the merchant."
msgstr ""
-#: src/components/modal/index.tsx:127
+#: src/paths/instance/orders/create/CreatePage.tsx:491
#, c-format
-msgid "Clearing the auth token will mean public access to the instance"
+msgid "Wire transfer deadline"
msgstr ""
-#: src/components/product/ProductForm.tsx:96
-#: src/paths/admin/create/CreatePage.tsx:85 src/paths/admin/list/Table.tsx:109
-#: src/paths/instance/transfers/list/Table.tsx:122
+#: src/paths/instance/orders/create/CreatePage.tsx:492
#, c-format
-msgid "ID"
+msgid "Deadline for the exchange to make the wire transfer."
msgstr ""
-#: src/components/product/ProductForm.tsx:98
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:121
-#: src/paths/instance/products/list/Table.tsx:85
+#: src/paths/instance/orders/create/CreatePage.tsx:496
#, c-format
-msgid "Image"
+msgid "Auto-refund deadline"
msgstr ""
-#: src/components/product/ProductForm.tsx:100
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:123
+#: src/paths/instance/orders/create/CreatePage.tsx:497
#, c-format
-msgid "Unit"
+msgid ""
+"Time until which the wallet will automatically check for refunds without "
+"user interaction."
msgstr ""
-#: src/components/product/ProductForm.tsx:101
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:124
-#: src/paths/instance/products/list/Table.tsx:162
-#: src/paths/instance/products/list/Table.tsx:214
+#: src/paths/instance/orders/create/CreatePage.tsx:502
#, c-format
-msgid "Price"
+msgid "Maximum deposit fee"
msgstr ""
-#: src/components/product/ProductForm.tsx:103
-#: src/paths/instance/products/list/Table.tsx:90
+#: src/paths/instance/orders/create/CreatePage.tsx:503
#, c-format
-msgid "Stock"
+msgid ""
+"Maximum deposit fees the merchant is willing to cover for this order. Higher "
+"deposit fees must be covered in full by the consumer."
msgstr ""
-#: src/components/product/ProductForm.tsx:105
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:128
-#: src/paths/instance/products/list/Table.tsx:88
+#: src/paths/instance/orders/create/CreatePage.tsx:507
#, c-format
-msgid "Taxes"
+msgid "Maximum wire fee"
msgstr ""
-#: src/index.tsx:75
+#: src/paths/instance/orders/create/CreatePage.tsx:508
#, c-format
-msgid "Server not found"
+msgid ""
+"Maximum aggregate wire fees the merchant is willing to cover for this order. "
+"Wire fees exceeding this amount are to be covered by the customers."
msgstr ""
-#: src/index.tsx:85
+#: src/paths/instance/orders/create/CreatePage.tsx:512
#, c-format
-msgid "Couldn't access the server"
+msgid "Wire fee amortization"
msgstr ""
-#: src/index.tsx:87 src/index.tsx:99
+#: src/paths/instance/orders/create/CreatePage.tsx:513
#, c-format
-msgid "Got message %1$s from %2$s"
+msgid ""
+"Factor by which wire fees exceeding the above threshold are divided to "
+"determine the share of excess wire fees to be paid explicitly by the "
+"consumer."
msgstr ""
-#: src/index.tsx:97
+#: src/paths/instance/orders/create/CreatePage.tsx:517
#, c-format
-msgid "Unexpected Error"
+msgid "Create token"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:89
-#: src/paths/instance/update/UpdatePage.tsx:108
+#: src/paths/instance/orders/create/CreatePage.tsx:518
#, c-format
-msgid "Auth token"
+msgid ""
+"Uncheck this option if the merchant backend generated an order ID with "
+"enough entropy to prevent adversarial claims."
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:91
-#: src/paths/instance/details/DetailPage.tsx:77
-#: src/paths/instance/update/UpdatePage.tsx:110
+#: src/paths/instance/orders/create/CreatePage.tsx:522
#, c-format
-msgid "Account address"
+msgid "Minimum age required"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:93
-#: src/paths/instance/update/UpdatePage.tsx:112
+#: src/paths/instance/orders/create/CreatePage.tsx:523
#, c-format
-msgid "Default max deposit fee"
+msgid ""
+"Any value greater than 0 will limit the coins able be used to pay this "
+"contract. If empty the age restriction will be defined by the products"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:95
-#: src/paths/instance/update/UpdatePage.tsx:114
+#: src/paths/instance/orders/create/CreatePage.tsx:526
#, c-format
-msgid "Default max wire fee"
+msgid "Min age defined by the producs is %1$s"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:97
-#: src/paths/instance/update/UpdatePage.tsx:116
+#: src/paths/instance/orders/create/CreatePage.tsx:534
#, c-format
-msgid "Default wire fee amortization"
+msgid "Additional information"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:103
-#: src/paths/instance/update/UpdatePage.tsx:122
+#: src/paths/instance/orders/create/CreatePage.tsx:535
#, c-format
-msgid "Jurisdiction"
+msgid "Custom information to be included in the contract for this order."
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:107
-#: src/paths/instance/update/UpdatePage.tsx:126
+#: src/paths/instance/orders/create/CreatePage.tsx:541
#, c-format
-msgid "Default pay delay"
+msgid "You must enter a value in JavaScript Object Notation (JSON)."
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:109
-#: src/paths/instance/update/UpdatePage.tsx:128
+#: src/components/picker/DurationPicker.tsx:55
#, c-format
-msgid "Default wire transfer delay"
+msgid "days"
msgstr ""
-#: src/paths/admin/create/index.tsx:58
+#: src/components/picker/DurationPicker.tsx:65
#, c-format
-msgid "could not create instance"
+msgid "hours"
msgstr ""
-#: src/paths/admin/list/Table.tsx:63 src/paths/admin/list/Table.tsx:131
-#: src/paths/instance/transfers/list/Table.tsx:71
+#: src/components/picker/DurationPicker.tsx:76
#, c-format
-msgid "Delete"
+msgid "minutes"
msgstr ""
-#: src/paths/admin/list/Table.tsx:128
+#: src/components/picker/DurationPicker.tsx:87
#, c-format
-msgid "Edit"
+msgid "seconds"
msgstr ""
-#: src/paths/admin/list/Table.tsx:149
-#: src/paths/instance/products/list/Table.tsx:245
+#: src/components/form/InputDuration.tsx:53
#, c-format
-msgid "There is no instances yet, add more pressing the + sign"
+msgid "forever"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:237
+#: src/components/form/InputDuration.tsx:62
#, c-format
-msgid "Inventory products"
+msgid "%1$sM"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:286
+#: src/components/form/InputDuration.tsx:64
#, c-format
-msgid "Total price"
+msgid "%1$sY"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:287
+#: src/components/form/InputDuration.tsx:66
#, c-format
-msgid "Total tax"
+msgid "%1$sd"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:289
-#: src/paths/instance/orders/create/CreatePage.tsx:297
+#: src/components/form/InputDuration.tsx:68
#, c-format
-msgid "Order price"
+msgid "%1$sh"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:295
+#: src/components/form/InputDuration.tsx:70
#, c-format
-msgid "Net"
+msgid "%1$smin"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:300
-#: src/paths/instance/orders/details/DetailPage.tsx:144
-#: src/paths/instance/orders/details/DetailPage.tsx:295
-#: src/paths/instance/orders/list/Table.tsx:117
+#: src/components/form/InputDuration.tsx:72
#, c-format
-msgid "Summary"
+msgid "%1$ssec"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:302
+#: src/paths/instance/orders/list/Table.tsx:75
#, c-format
-msgid "Payments options"
+msgid "Orders"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:303
+#: src/paths/instance/orders/list/Table.tsx:81
#, c-format
-msgid "Auto refund deadline"
+msgid "create order"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:304
+#: src/paths/instance/orders/list/Table.tsx:147
#, c-format
-msgid "Refund deadline"
+msgid "load newer orders"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:305
+#: src/paths/instance/orders/list/Table.tsx:154
+#, c-format
+msgid "Date"
+msgstr "Data"
+
+#: src/paths/instance/orders/list/Table.tsx:200
#, c-format
-msgid "Pay deadline"
+msgid "Refund"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:307
+#: src/paths/instance/orders/list/Table.tsx:209
#, c-format
-msgid "Delivery date"
+msgid "copy url"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:308
+#: src/paths/instance/orders/list/Table.tsx:225
#, c-format
-msgid "Location"
+msgid "load older orders"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:312
+#: src/paths/instance/orders/list/Table.tsx:242
+#, c-format
+msgid "No orders have been found matching your query!"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:288
+#, c-format
+msgid "duplicated"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:299
+#, c-format
+msgid "invalid format"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:301
+#, c-format
+msgid "this value exceed the refundable amount"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:346
+#, c-format
+msgid "date"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:349
+#, c-format
+msgid "amount"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:352
+#, c-format
+msgid "reason"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:389
+#, c-format
+msgid "amount to be refunded"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:391
+#, c-format
+msgid "Max refundable:"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:396
+#, c-format
+msgid "Reason"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:397
+#, c-format
+msgid "Choose one..."
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:399
+#, c-format
+msgid "requested by the customer"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:400
+#, c-format
+msgid "other"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:403
+#, c-format
+msgid "why this order is being refunded"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:409
+#, c-format
+msgid "more information to give context"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:62
+#, c-format
+msgid "Contract Terms"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:68
+#, c-format
+msgid "human-readable description of the whole purchase"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:74
+#, c-format
+msgid "total price for the transaction"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:81
+#, c-format
+msgid "URL for this purchase"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:87
#, c-format
msgid "Max fee"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:313
+#: src/paths/instance/orders/details/DetailPage.tsx:88
+#, c-format
+msgid "maximum total deposit fee accepted by the merchant for this contract"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:93
#, c-format
msgid "Max wire fee"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:314
+#: src/paths/instance/orders/details/DetailPage.tsx:94
#, c-format
-msgid "Wire fee amortization"
+msgid "maximum wire fee accepted by the merchant"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:315
+#: src/paths/instance/orders/details/DetailPage.tsx:100
#, c-format
-msgid "Fullfilment url"
+msgid ""
+"over how many customer transactions does the merchant expect to amortize "
+"wire fees on average"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:318
+#: src/paths/instance/orders/details/DetailPage.tsx:105
#, c-format
-msgid "Extra information"
+msgid "Created at"
msgstr ""
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:44
+#: src/paths/instance/orders/details/DetailPage.tsx:106
#, c-format
-msgid "select a product first"
+msgid "time when this contract was generated"
msgstr ""
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:51
+#: src/paths/instance/orders/details/DetailPage.tsx:112
#, c-format
-msgid "should be greater than 0"
+msgid "after this deadline has passed no refunds will be accepted"
msgstr ""
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:58
+#: src/paths/instance/orders/details/DetailPage.tsx:118
#, c-format
msgid ""
-"cannot be greater than current stock and quantity previously added. max: %1$s"
+"after this deadline, the merchant won't accept payments for the contract"
msgstr ""
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:64
+#: src/paths/instance/orders/details/DetailPage.tsx:124
#, c-format
-msgid "cannot be greater than current stock %1$s"
+msgid "transfer deadline for the exchange"
msgstr ""
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:76
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:126
+#: src/paths/instance/orders/details/DetailPage.tsx:130
#, c-format
-msgid "Quantity"
+msgid "time indicating when the order should be delivered"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:92
-#: src/paths/instance/orders/details/DetailPage.tsx:235
-#: src/paths/instance/orders/details/DetailPage.tsx:333
+#: src/paths/instance/orders/details/DetailPage.tsx:136
#, c-format
-msgid "Order"
+msgid "where the order will be delivered"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:93
+#: src/paths/instance/orders/details/DetailPage.tsx:144
#, c-format
-msgid "claimed"
+msgid "Auto-refund delay"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:110
-#: src/paths/instance/orders/details/DetailPage.tsx:261
-#: src/paths/instance/orders/list/Table.tsx:136
+#: src/paths/instance/orders/details/DetailPage.tsx:145
#, c-format
-msgid "copy url"
+msgid ""
+"how long the wallet should try to get an automatic refund for the purchase"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:126
-#: src/paths/instance/orders/details/DetailPage.tsx:349
+#: src/paths/instance/orders/details/DetailPage.tsx:150
#, c-format
-msgid "pay at"
+msgid "Extra info"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:127
-#: src/paths/instance/orders/details/DetailPage.tsx:350
+#: src/paths/instance/orders/details/DetailPage.tsx:151
#, c-format
-msgid "created at"
+msgid "extra data that is only interpreted by the merchant frontend"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:138
-#: src/paths/instance/orders/details/DetailPage.tsx:289
+#: src/paths/instance/orders/details/DetailPage.tsx:219
+#, c-format
+msgid "Order"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:221
+#, c-format
+msgid "claimed"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:247
+#, c-format
+msgid "claimed at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:265
#, c-format
msgid "Timeline"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:142
-#: src/paths/instance/orders/details/DetailPage.tsx:293
+#: src/paths/instance/orders/details/DetailPage.tsx:271
#, c-format
msgid "Payment details"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:146
-#: src/paths/instance/orders/details/DetailPage.tsx:299
-#: src/paths/instance/orders/details/DetailPage.tsx:363
+#: src/paths/instance/orders/details/DetailPage.tsx:291
#, c-format
msgid "Order status"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:156
-#: src/paths/instance/orders/details/DetailPage.tsx:308
+#: src/paths/instance/orders/details/DetailPage.tsx:301
#, c-format
msgid "Product list"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:236
+#: src/paths/instance/orders/details/DetailPage.tsx:451
#, c-format
msgid "paid"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:238
+#: src/paths/instance/orders/details/DetailPage.tsx:455
#, c-format
msgid "wired"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:241
+#: src/paths/instance/orders/details/DetailPage.tsx:460
#, c-format
msgid "refunded"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:258
+#: src/paths/instance/orders/details/DetailPage.tsx:480
+#, c-format
+msgid "refund order"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:481
+#, c-format
+msgid "not refundable"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:489
#, c-format
msgid "refund"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:297
+#: src/paths/instance/orders/details/DetailPage.tsx:553
#, c-format
msgid "Refunded amount"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:298
+#: src/paths/instance/orders/details/DetailPage.tsx:560
+#, c-format
+msgid "Refund taken"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:570
+#, c-format
+msgid "Status URL"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:583
#, c-format
-msgid "Deposit total"
+msgid "Refund URI"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:336
+#: src/paths/instance/orders/details/DetailPage.tsx:636
#, c-format
msgid "unpaid"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:364
+#: src/paths/instance/orders/details/DetailPage.tsx:654
+#, c-format
+msgid "pay at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:666
+#, c-format
+msgid "created at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:707
#, c-format
msgid "Order status URL"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:365
+#: src/paths/instance/orders/details/DetailPage.tsx:711
#, c-format
-msgid "Pay URI"
+msgid "Payment URI"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:383
+#: src/paths/instance/orders/details/DetailPage.tsx:740
#, c-format
msgid ""
"Unknown order status. This is an error, please contact the administrator."
msgstr ""
-#: src/paths/instance/orders/details/index.tsx:56
-#: src/paths/instance/orders/list/index.tsx:147
+#: src/paths/instance/orders/details/DetailPage.tsx:767
+#, c-format
+msgid "Back"
+msgstr "Indietro"
+
+#: src/paths/instance/orders/details/index.tsx:79
#, c-format
msgid "refund created successfully"
msgstr ""
-#: src/paths/instance/orders/details/index.tsx:59
-#: src/paths/instance/orders/list/index.tsx:150
+#: src/paths/instance/orders/details/index.tsx:85
#, c-format
msgid "could not create the refund"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:111
+#: src/paths/instance/orders/list/ListPage.tsx:78
#, c-format
-msgid "load newer orders"
+msgid "select date to show nearby orders"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:115
+#: src/paths/instance/orders/list/ListPage.tsx:94
#, c-format
-msgid "Date"
+msgid "order id"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:131
-#: src/paths/instance/orders/list/Table.tsx:223
+#: src/paths/instance/orders/list/ListPage.tsx:100
#, c-format
-msgid "Refund"
+msgid "jump to order with the given order ID"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:145
+#: src/paths/instance/orders/list/ListPage.tsx:122
#, c-format
-msgid "load older orders"
+msgid "remove all filters"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:154
+#: src/paths/instance/orders/list/ListPage.tsx:132
#, c-format
-msgid "No orders has been found"
+msgid "only show paid orders"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:202
+#: src/paths/instance/orders/list/ListPage.tsx:135
#, c-format
-msgid "date"
+msgid "Paid"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:203
+#: src/paths/instance/orders/list/ListPage.tsx:142
#, c-format
-msgid "amount"
+msgid "only show orders with refunds"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:204
+#: src/paths/instance/orders/list/ListPage.tsx:145
#, c-format
-msgid "reason"
+msgid "Refunded"
+msgstr "Rimborsato"
+
+#: src/paths/instance/orders/list/ListPage.tsx:152
+#, c-format
+msgid ""
+"only show orders where customers paid, but wire payments from payment "
+"provider are still pending"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:224
+#: src/paths/instance/orders/list/ListPage.tsx:155
#, c-format
-msgid "Max refundable:"
+msgid "Not wired"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:226
+#: src/paths/instance/orders/list/ListPage.tsx:170
#, c-format
-msgid "Reason"
+msgid "clear date filter"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:226
+#: src/paths/instance/orders/list/ListPage.tsx:184
#, c-format
-msgid "duplicated"
+msgid "date (YYYY/MM/DD)"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:226
+#: src/paths/instance/orders/list/index.tsx:103
#, c-format
-msgid "requested by the customer"
+msgid "Enter an order id"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:226
+#: src/paths/instance/orders/list/index.tsx:111
#, c-format
-msgid "other"
+msgid "order not found"
msgstr ""
-#: src/paths/instance/orders/list/index.tsx:91
+#: src/paths/instance/orders/list/index.tsx:178
#, c-format
-msgid "go to order id"
+msgid "could not get the order to refund"
msgstr ""
-#: src/paths/instance/orders/list/index.tsx:107
+#: src/components/exception/AsyncButton.tsx:43
#, c-format
-msgid "Paid"
+msgid "Loading..."
msgstr ""
-#: src/paths/instance/orders/list/index.tsx:108
+#: src/components/form/InputStock.tsx:99
#, c-format
-msgid "Refunded"
+msgid ""
+"click here to configure the stock of the product, leave it as is and the "
+"backend will not control stock"
msgstr ""
-#: src/paths/instance/orders/list/index.tsx:109
+#: src/components/form/InputStock.tsx:109
#, c-format
-msgid "Not wired"
+msgid "Manage stock"
msgstr ""
-#: src/paths/instance/orders/list/index.tsx:110
+#: src/components/form/InputStock.tsx:115
#, c-format
-msgid "All"
+msgid "this product has been configured without stock control"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:119
+#, c-format
+msgid "Infinite"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:136
+#, c-format
+msgid "lost cannot be greater than current and incoming (max %1$s)"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:176
+#, c-format
+msgid "Incoming"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:177
+#, c-format
+msgid "Lost"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:192
+#, c-format
+msgid "Current"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:196
+#, c-format
+msgid "remove stock control for this product"
msgstr ""
-#: src/paths/instance/products/create/index.tsx:48
-#: src/paths/instance/products/update/index.tsx:64
+#: src/components/form/InputStock.tsx:202
+#, c-format
+msgid "without stock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:211
+#, c-format
+msgid "Next restock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:217
+#, c-format
+msgid "Delivery address"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:133
+#, c-format
+msgid "product identification to use in URLs (for internal use only)"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:139
+#, c-format
+msgid "illustration of the product for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:145
+#, c-format
+msgid "product description for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:149
+#, c-format
+msgid "Age restricted"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:150
+#, c-format
+msgid "is this product restricted for customer below certain age?"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:155
+#, c-format
+msgid ""
+"unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 "
+"items, 5 meters) for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:160
+#, c-format
+msgid ""
+"sale price for customers, including taxes, for above units of the product"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:164
+#, c-format
+msgid "Stock"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:166
+#, c-format
+msgid ""
+"product inventory for products with finite supply (for internal use only)"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:171
+#, c-format
+msgid "taxes included in the product price, exposed to customers"
+msgstr ""
+
+#: src/paths/instance/products/create/CreatePage.tsx:66
+#, c-format
+msgid "Need to complete marked fields"
+msgstr ""
+
+#: src/paths/instance/products/create/index.tsx:51
#, c-format
msgid "could not create product"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:87
+#: src/paths/instance/products/list/Table.tsx:68
+#, c-format
+msgid "Products"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:73
+#, c-format
+msgid "add product to inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:137
#, c-format
msgid "Sell"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:89
+#: src/paths/instance/products/list/Table.tsx:143
#, c-format
msgid "Profit"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:91
+#: src/paths/instance/products/list/Table.tsx:149
#, c-format
msgid "Sold"
msgstr ""
-#: src/paths/instance/products/list/index.tsx:59
+#: src/paths/instance/products/list/Table.tsx:210
+#, c-format
+msgid "free"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:248
+#, c-format
+msgid "go to product update page"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:255
+#, c-format
+msgid "Update"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:260
+#, c-format
+msgid "remove this product from the database"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:331
+#, c-format
+msgid "update the product with new price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:341
+#, c-format
+msgid "update product with new price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:399
+#, c-format
+msgid "add more elements to the inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:404
+#, c-format
+msgid "report elements lost in the inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:409
+#, c-format
+msgid "new price for the product"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:421
+#, c-format
+msgid "the are value with errors"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:422
+#, c-format
+msgid "update product with new stock and price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:463
+#, c-format
+msgid "There is no products yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:86
#, c-format
msgid "product updated successfully"
msgstr ""
-#: src/paths/instance/products/list/index.tsx:62
+#: src/paths/instance/products/list/index.tsx:92
#, c-format
msgid "could not update the product"
msgstr ""
-#: src/paths/instance/products/list/index.tsx:70
+#: src/paths/instance/products/list/index.tsx:103
#, c-format
msgid "product delete successfully"
msgstr ""
-#: src/paths/instance/products/list/index.tsx:73
+#: src/paths/instance/products/list/index.tsx:109
#, c-format
msgid "could not delete the product"
msgstr ""
-#: src/paths/instance/tips/list/Table.tsx:59
+#: src/paths/instance/products/update/UpdatePage.tsx:56
+#, c-format
+msgid "Product id:"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:95
+#, c-format
+msgid ""
+"To complete the setup of the reserve, you must now initiate a wire transfer "
+"using the given wire transfer subject and crediting the specified amount to "
+"the indicated account of the exchange."
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:102
+#, c-format
+msgid "If your system supports RFC 8905, you can do this by opening this URI:"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:83
+#, c-format
+msgid "it should be greater than 0"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:88
+#, c-format
+msgid "must be a valid URL"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:107
+#, c-format
+msgid "Initial balance"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:108
+#, c-format
+msgid "balance prior to deposit"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:112
+#, c-format
+msgid "Exchange URL"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:113
+#, c-format
+msgid "URL of exchange"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:148
+#, c-format
+msgid "Next"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:186
+#, c-format
+msgid "Wire method"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:187
+#, c-format
+msgid "method to use for wire transfer"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:189
+#, c-format
+msgid "Select one wire method"
+msgstr ""
+
+#: src/paths/instance/reserves/create/index.tsx:62
+#, c-format
+msgid "could not create reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:77
+#, c-format
+msgid "Valid until"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:82
+#, c-format
+msgid "Created balance"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:99
+#, c-format
+msgid "Exchange balance"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:104
+#, c-format
+msgid "Picked up"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:109
+#, c-format
+msgid "Committed"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:116
+#, c-format
+msgid "Account address"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:119
+#, c-format
+msgid "Subject"
+msgstr "Soggetto"
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:130
#, c-format
msgid "Tips"
msgstr ""
-#: src/paths/instance/tips/list/Table.tsx:111
+#: src/paths/instance/reserves/details/DetailPage.tsx:193
#, c-format
-msgid "Committed amount"
+msgid "No tips has been authorized from this reserve"
msgstr ""
-#: src/paths/instance/tips/list/Table.tsx:112
+#: src/paths/instance/reserves/details/DetailPage.tsx:213
#, c-format
-msgid "Exchange initial amount"
+msgid "Authorized"
msgstr ""
-#: src/paths/instance/tips/list/Table.tsx:113
+#: src/paths/instance/reserves/details/DetailPage.tsx:222
#, c-format
-msgid "Merchant initial amount"
+msgid "Expiration"
msgstr ""
-#: src/paths/instance/tips/list/Table.tsx:148
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:108
#, c-format
-msgid "There is no tips yet, add more pressing the + sign"
+msgid "amount of tip"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:50
-#: src/paths/instance/transfers/create/CreatePage.tsx:54
-#: src/paths/instance/transfers/create/CreatePage.tsx:55
-#: src/paths/instance/transfers/create/CreatePage.tsx:56
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:112
#, c-format
-msgid "cannot be empty"
+msgid "Justification"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:114
+#, c-format
+msgid "reason for the tip"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:118
+#, c-format
+msgid "URL after tip"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:119
+#, c-format
+msgid "URL to visit after tip payment"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:65
+#, c-format
+msgid "Reserves not yet funded"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:89
+#, c-format
+msgid "Reserves ready"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:95
+#, c-format
+msgid "add new reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:143
+#, c-format
+msgid "Expires at"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:146
+#, c-format
+msgid "Initial"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:202
+#, c-format
+msgid "delete selected reserve from the database"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:210
+#, c-format
+msgid "authorize new tip from selected reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:237
+#, c-format
+msgid ""
+"There is no ready reserves yet, add more pressing the + sign or fund them"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:264
+#, c-format
+msgid "Expected Balance"
+msgstr ""
+
+#: src/paths/instance/reserves/list/index.tsx:110
+#, c-format
+msgid "could not create the tip"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:77
+#, c-format
+msgid "should not be empty"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:93
+#, c-format
+msgid "should be greater that 0"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:96
+#, c-format
+msgid "can't be empty"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:100
+#, c-format
+msgid "to short"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:108
+#, c-format
+msgid "just letters and numbers from 2 to 7"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:110
+#, c-format
+msgid "size of the key should be 32"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:137
+#, c-format
+msgid "Identifier"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:138
+#, c-format
+msgid "Name of the template in URLs."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:144
+#, c-format
+msgid "Describe what this template stands for"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:149
+#, c-format
+msgid "Fixed summary"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:150
+#, c-format
+msgid "If specified, this template will create order with the same summary"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:154
+#, c-format
+msgid "Fixed price"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:155
+#, c-format
+msgid "If specified, this template will create order with the same price"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:159
+#, c-format
+msgid "Minimum age"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:161
+#, c-format
+msgid "Is this contract restricted to some age?"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:165
+#, c-format
+msgid "Payment timeout"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:167
+#, c-format
+msgid ""
+"How much time has the customer to complete the payment once the order was "
+"created."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:171
+#, c-format
+msgid "Verification algorithm"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:172
+#, c-format
+msgid "Algorithm to use to verify transaction in offline mode"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:180
+#, c-format
+msgid "Point-of-sale key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:182
+#, c-format
+msgid "Useful to validate the purchase"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:196
+#, c-format
+msgid "generate random secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:203
+#, c-format
+msgid "random"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:208
+#, c-format
+msgid "show secret key"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:51
+#: src/paths/instance/templates/create/CreatePage.tsx:209
#, c-format
-msgid "check the id, doest look valid"
+msgid "hide secret key"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:52
+#: src/paths/instance/templates/create/CreatePage.tsx:216
+#, c-format
+msgid "hide"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:218
+#, c-format
+msgid "show"
+msgstr ""
+
+#: src/paths/instance/templates/create/index.tsx:52
+#, c-format
+msgid "could not inform template"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:54
+#, c-format
+msgid "Amount is required"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:58
+#, c-format
+msgid "Order summary is required"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:86
+#, c-format
+msgid "New order for template"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:108
+#, c-format
+msgid "Amount of the order"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:113
+#, c-format
+msgid "Order summary"
+msgstr ""
+
+#: src/paths/instance/templates/use/index.tsx:92
+#, c-format
+msgid "could not create order from template"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:131
+#, c-format
+msgid ""
+"Here you can specify a default value for fields that are not fixed. Default "
+"values can be edited by the customer before the payment."
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:148
+#, c-format
+msgid "Fixed amount"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:149
+#, c-format
+msgid "Default amount"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:161
+#, c-format
+msgid "Default summary"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:177
+#, c-format
+msgid "Print"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:184
+#, c-format
+msgid "Setup TOTP"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:65
+#, c-format
+msgid "Templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:70
+#, c-format
+msgid "add new templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:142
+#, c-format
+msgid "load more templates before the first one"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:146
+#, c-format
+msgid "load newer templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:181
+#, c-format
+msgid "delete selected templates from the database"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:188
+#, c-format
+msgid "use template to create new order"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:195
+#, c-format
+msgid "create qr code for the template"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:210
+#, c-format
+msgid "load more templates after the last one"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:214
+#, c-format
+msgid "load older templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:231
+#, c-format
+msgid "There is no templates yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/templates/list/index.tsx:104
+#, c-format
+msgid "template delete successfully"
+msgstr ""
+
+#: src/paths/instance/templates/list/index.tsx:110
+#, c-format
+msgid "could not delete the template"
+msgstr ""
+
+#: src/paths/instance/templates/update/index.tsx:90
+#, c-format
+msgid "could not update template"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:57
+#, c-format
+msgid "should be one of '%1$s'"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:85
+#, c-format
+msgid "Webhook ID to use"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:89
+#, c-format
+msgid "Event"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:90
+#, c-format
+msgid "The event of the webhook: why the webhook is used"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:94
+#, c-format
+msgid "Method"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:95
+#, c-format
+msgid "Method used by the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:99
+#, c-format
+msgid "URL"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:100
+#, c-format
+msgid "URL of the webhook where the customer will be redirected"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:104
+#, c-format
+msgid "Header"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:106
+#, c-format
+msgid "Header template of the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:111
+#, c-format
+msgid "Body"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:112
+#, c-format
+msgid "Body template by the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:61
+#, c-format
+msgid "Webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:66
+#, c-format
+msgid "add new webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:137
+#, c-format
+msgid "load more webhooks before the first one"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:141
+#, c-format
+msgid "load newer webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:151
+#, c-format
+msgid "Event type"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:176
+#, c-format
+msgid "delete selected webhook from the database"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:198
+#, c-format
+msgid "load more webhooks after the last one"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:202
+#, c-format
+msgid "load older webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:219
+#, c-format
+msgid "There is no webhooks yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/index.tsx:94
+#, c-format
+msgid "webhook delete successfully"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/index.tsx:100
+#, c-format
+msgid "could not delete the webhook"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:63
+#, c-format
+msgid "check the id, does not look valid"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:65
#, c-format
msgid "should have 52 characters, current %1$s"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:57
+#: src/paths/instance/transfers/create/CreatePage.tsx:72
#, c-format
msgid "URL doesn't have the right format"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:74
+#: src/paths/instance/transfers/create/CreatePage.tsx:98
#, c-format
-msgid "Transfer ID"
+msgid "Credited bank account"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:76
+#: src/paths/instance/transfers/create/CreatePage.tsx:100
#, c-format
-msgid "Account Address"
+msgid "Select one account"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:82
-#: src/paths/instance/transfers/list/Table.tsx:125
+#: src/paths/instance/transfers/create/CreatePage.tsx:101
#, c-format
-msgid "Exchange URL"
+msgid "Bank account of the merchant where the payment was received"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:105
+#, c-format
+msgid "Wire transfer ID"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:107
+#, c-format
+msgid ""
+"unique identifier of the wire transfer used by the exchange, must be 52 "
+"characters long"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:112
+#, c-format
+msgid ""
+"Base URL of the exchange that made the transfer, should have been in the "
+"wire transfer subject"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:117
+#, c-format
+msgid "Amount credited"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:118
+#, c-format
+msgid "Actual amount that was wired to the merchant's bank account"
msgstr ""
-#: src/paths/instance/transfers/create/index.tsx:49
+#: src/paths/instance/transfers/create/index.tsx:58
#, c-format
msgid "could not inform transfer"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:118
+#: src/paths/instance/transfers/list/Table.tsx:61
+#, c-format
+msgid "Transfers"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:66
+#, c-format
+msgid "add new transfer"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:129
+#, c-format
+msgid "load more transfers before the first one"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:133
#, c-format
msgid "load newer transfers"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:123
+#: src/paths/instance/transfers/list/Table.tsx:143
#, c-format
msgid "Credit"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:126
+#: src/paths/instance/transfers/list/Table.tsx:152
#, c-format
msgid "Confirmed"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:127
-#: src/paths/instance/transfers/list/index.tsx:60
+#: src/paths/instance/transfers/list/Table.tsx:155
#, c-format
msgid "Verified"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:128
+#: src/paths/instance/transfers/list/Table.tsx:158
#, c-format
msgid "Executed at"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:138
-#: src/paths/instance/transfers/list/Table.tsx:139
+#: src/paths/instance/transfers/list/Table.tsx:171
#, c-format
msgid "yes"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:138
-#: src/paths/instance/transfers/list/Table.tsx:139
+#: src/paths/instance/transfers/list/Table.tsx:171
#, c-format
msgid "no"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:140
+#: src/paths/instance/transfers/list/Table.tsx:181
#, c-format
msgid "unknown"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:145
+#: src/paths/instance/transfers/list/Table.tsx:187
+#, c-format
+msgid "delete selected transfer from the database"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:202
+#, c-format
+msgid "load more transfer after the last one"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:206
#, c-format
msgid "load older transfers"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:154
+#: src/paths/instance/transfers/list/Table.tsx:223
#, c-format
msgid "There is no transfer yet, add more pressing the + sign"
msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:79
+#, c-format
+msgid "filter by account address"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:100
+#, c-format
+msgid "only show wire transfers confirmed by the merchant"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:110
+#, c-format
+msgid "only show wire transfers claimed by the exchange"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:113
+#, c-format
+msgid "Unverified"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:69
+#, c-format
+msgid "is not valid"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:94
+#, c-format
+msgid "is not a number"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:96
+#, c-format
+msgid "must be 1 or greater"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:107
+#, c-format
+msgid "max 7 lines"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:178
+#, c-format
+msgid "change authorization configuration"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:217
+#, c-format
+msgid "Need to complete marked fields and choose authorization method"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:82
+#, c-format
+msgid "This is not a valid bitcoin address."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:95
+#, c-format
+msgid "This is not a valid Ethereum address."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:118
+#, c-format
+msgid "IBAN numbers usually have more that 4 digits"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:120
+#, c-format
+msgid "IBAN numbers usually have less that 34 digits"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:128
+#, c-format
+msgid "IBAN country code not found"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:153
+#, c-format
+msgid "IBAN number is not valid, checksum is wrong"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:248
+#, c-format
+msgid "Target type"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:249
+#, c-format
+msgid "Method to use for wire transfer"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:258
+#, c-format
+msgid "Routing"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:259
+#, c-format
+msgid "Routing number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:263
+#, c-format
+msgid "Account"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:264
+#, c-format
+msgid "Account number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:273
+#, c-format
+msgid "Business Identifier Code."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:282
+#, c-format
+msgid "Bank Account Number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:292
+#, c-format
+msgid "Unified Payment Interface."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:301
+#, c-format
+msgid "Bitcoin protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:310
+#, c-format
+msgid "Ethereum protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:319
+#, c-format
+msgid "Interledger protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:328
+#, c-format
+msgid "Host"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:329
+#, c-format
+msgid "Bank host."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:334
+#, c-format
+msgid "Bank account."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:343
+#, c-format
+msgid "Bank account owner's name."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:370
+#, c-format
+msgid "No accounts yet."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:52
+#, c-format
+msgid ""
+"Name of the instance in URLs. The 'default' instance is special in that it "
+"is used to administer other instances."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:58
+#, c-format
+msgid "Business name"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:59
+#, c-format
+msgid "Legal name of the business represented by this instance."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:64
+#, c-format
+msgid "Email"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:65
+#, c-format
+msgid "Contact email"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:70
+#, c-format
+msgid "Website URL"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:71
+#, c-format
+msgid "URL."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:76
+#, c-format
+msgid "Logo"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:77
+#, c-format
+msgid "Logo image."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:82
+#, c-format
+msgid "Bank account"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:83
+#, c-format
+msgid "URI specifying bank account for crediting revenue."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:88
+#, c-format
+msgid "Default max deposit fee"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:89
+#, c-format
+msgid ""
+"Maximum deposit fees this merchant is willing to pay per order by default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:94
+#, c-format
+msgid "Default max wire fee"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:95
+#, c-format
+msgid ""
+"Maximum wire fees this merchant is willing to pay per wire transfer by "
+"default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:100
+#, c-format
+msgid "Default wire fee amortization"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:101
+#, c-format
+msgid ""
+"Number of orders excess wire transfer fees will be divided by to compute per "
+"order surcharge."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:107
+#, c-format
+msgid "Physical location of the merchant."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:114
+#, c-format
+msgid "Jurisdiction"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:115
+#, c-format
+msgid "Jurisdiction for legal disputes with the merchant."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:122
+#, c-format
+msgid "Default payment delay"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:124
+#, c-format
+msgid ""
+"Time customers have to pay an order before the offer expires by default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:129
+#, c-format
+msgid "Default wire transfer delay"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:130
+#, c-format
+msgid ""
+"Maximum time an exchange is allowed to delay wiring funds to the merchant, "
+"enabling it to aggregate smaller payments into larger wire transfers and "
+"reducing wire fees."
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:164
+#, c-format
+msgid "Instance id"
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:173
+#, c-format
+msgid "Change the authorization method use for this instance."
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:182
+#, c-format
+msgid "Manage access token"
+msgstr ""
+
+#: src/paths/instance/update/index.tsx:112
+#, c-format
+msgid "Failed to create instance"
+msgstr ""
+
+#: src/components/exception/login.tsx:74
+#, c-format
+msgid "Login required"
+msgstr ""
+
+#: src/components/exception/login.tsx:80
+#, c-format
+msgid "Please enter your access token."
+msgstr ""
+
+#: src/components/exception/login.tsx:108
+#, c-format
+msgid "Access Token"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:171
+#, c-format
+msgid "The request to the backend take too long and was cancelled"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:172
+#, c-format
+msgid "Diagnostic from %1$s is \"%2$s\""
+msgstr ""
+
+#: src/InstanceRoutes.tsx:178
+#, c-format
+msgid "The backend reported a problem: HTTP status #%1$s"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:179
+#, c-format
+msgid "Diagnostic from %1$s is '%2$s'"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:196
+#, c-format
+msgid "Access denied"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:197
+#, c-format
+msgid "The access token provided is invalid."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:212
+#, c-format
+msgid "No 'default' instance configured yet."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:213
+#, c-format
+msgid "Create a 'default' instance to begin using the merchant backoffice."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:630
+#, c-format
+msgid "The access token provided is invalid"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:664
+#, c-format
+msgid "Hide for today"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:82
+#, c-format
+msgid "Instance"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:91
+#, c-format
+msgid "Settings"
+msgstr "Impostazioni"
+
+#: src/components/menu/SideBar.tsx:167
+#, c-format
+msgid "Connection"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:209
+#, c-format
+msgid "New"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:219
+#, c-format
+msgid "List"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:234
+#, c-format
+msgid "Log out"
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:71
+#, c-format
+msgid "Check your token is valid"
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:90
+#, c-format
+msgid "Couldn't access the server."
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:91
+#, c-format
+msgid "Could not infer instance id from url %1$s"
+msgstr ""
+
+#: src/Application.tsx:104
+#, c-format
+msgid "Server not found"
+msgstr ""
+
+#: src/Application.tsx:118
+#, c-format
+msgid "Server response with an error code"
+msgstr ""
+
+#: src/Application.tsx:120
+#, c-format
+msgid "Got message %1$s from %2$s"
+msgstr ""
+
+#: src/Application.tsx:131
+#, c-format
+msgid "Response from server is unreadable, http status: %1$s"
+msgstr ""
+
+#: src/Application.tsx:144
+#, c-format
+msgid "Unexpected Error"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:101
+#, c-format
+msgid "The value %1$s is invalid for a payment url"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:110
+#, c-format
+msgid "add element to the list"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:112
+#, c-format
+msgid "add"
+msgstr ""
+
+#: src/components/form/InputSecured.tsx:37
+#, c-format
+msgid "Deleting"
+msgstr ""
+
+#: src/components/form/InputSecured.tsx:41
+#, c-format
+msgid "Changing"
+msgstr ""
+
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:87
+#, c-format
+msgid "Order ID"
+msgstr ""
+
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:101
+#, c-format
+msgid "Payment URL"
+msgstr ""
diff --git a/packages/merchant-backoffice-ui/src/i18n/poheader b/packages/merchant-backoffice-ui/src/i18n/poheader
index ee3fcd7be..7ddcf49b8 100644
--- a/packages/merchant-backoffice-ui/src/i18n/poheader
+++ b/packages/merchant-backoffice-ui/src/i18n/poheader
@@ -1,5 +1,5 @@
# This file is part of GNU Taler
-# (C) 2021 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/merchant-backoffice-ui/src/i18n/strings-prelude b/packages/merchant-backoffice-ui/src/i18n/strings-prelude
index cca13afad..6c68662de 100644
--- a/packages/merchant-backoffice-ui/src/i18n/strings-prelude
+++ b/packages/merchant-backoffice-ui/src/i18n/strings-prelude
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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/merchant-backoffice-ui/src/i18n/strings.ts b/packages/merchant-backoffice-ui/src/i18n/strings.ts
index 63e96949a..65dc41358 100644
--- a/packages/merchant-backoffice-ui/src/i18n/strings.ts
+++ b/packages/merchant-backoffice-ui/src/i18n/strings.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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
@@ -26,58 +26,172 @@ strings['de'] = {
"plural_forms": "nplurals=2; plural=(n != 1);",
"lang": ""
},
- "Access denied": [
+ "Cancel": [
""
],
- "Check your token is valid": [
+ "%1$s": [
""
],
- "Couldn't access the server.": [
+ "Close": [
""
],
- "Could not infer instance id from url %1$s": [
+ "Continue": [
""
],
- "HTTP status #%1$s: Server reported a problem": [
+ "Clear": [
""
],
- "Got message: \"%1$s\" from: %2$s": [
+ "Confirm": [
""
],
- "No default instance": [
+ "is not the same as the current access token": [
""
],
- "in order to use merchant backoffice, you should create the default instance": [
+ "cannot be empty": [
""
],
- "Server reported a problem: HTTP status #%1$s": [
+ "cannot be the same as the old token": [
""
],
- "Got message: %1$s from: %2$s": [
+ "is not the same": [
""
],
- "Login required": [
+ "You are updating the access token from instance with id %1$s": [
""
],
- "Please enter your auth token. Token should have \"secret-token:\" and start with Bearer or ApiKey": [
+ "Old access token": [
""
],
- "Confirm": [
+ "access token currently in use": [
""
],
- "The value %1$s is invalid for a payment url": [
+ "New access token": [
+ ""
+ ],
+ "next access token to be used": [
+ ""
+ ],
+ "Repeat access token": [
+ ""
+ ],
+ "confirm the same access token": [
+ ""
+ ],
+ "Clearing the access token will mean public access to the instance": [
+ ""
+ ],
+ "cannot be the same as the old access token": [
+ ""
+ ],
+ "You are setting the access token for the new instance": [
+ ""
+ ],
+ "With external authorization method no check will be done by the merchant backend": [
+ ""
+ ],
+ "Set external authorization": [
+ ""
+ ],
+ "Set access token": [
+ ""
+ ],
+ "Operation in progress...": [
+ ""
+ ],
+ "The operation will be automatically canceled after %1$s seconds": [
+ ""
+ ],
+ "Instances": [
+ ""
+ ],
+ "Delete": [
+ ""
+ ],
+ "add new instance": [
+ ""
+ ],
+ "ID": [
+ ""
+ ],
+ "Name": [
+ ""
+ ],
+ "Edit": [
+ ""
+ ],
+ "Purge": [
+ ""
+ ],
+ "There is no instances yet, add more pressing the + sign": [
+ ""
+ ],
+ "Only show active instances": [
+ ""
+ ],
+ "Active": [
+ ""
+ ],
+ "Only show deleted instances": [
+ ""
+ ],
+ "Deleted": [
+ ""
+ ],
+ "Show all instances": [
+ ""
+ ],
+ "All": [
+ ""
+ ],
+ "Instance \"%1$s\" (ID: %2$s) has been deleted": [
+ ""
+ ],
+ "Failed to delete instance": [
+ ""
+ ],
+ "Instance '%1$s' (ID: %2$s) has been disabled": [
+ ""
+ ],
+ "Failed to purge instance": [
+ ""
+ ],
+ "Pending KYC verification": [
+ ""
+ ],
+ "Timed out": [
+ ""
+ ],
+ "Exchange": [
""
],
- "pick a date": [
+ "Target account": [
+ ""
+ ],
+ "KYC URL": [
+ ""
+ ],
+ "Code": [
+ ""
+ ],
+ "Http Status": [
+ ""
+ ],
+ "No pending kyc verification!": [
+ ""
+ ],
+ "change value to unknown date": [
+ ""
+ ],
+ "change value to empty": [
""
],
"clear": [
""
],
- "never": [
+ "change value to never": [
""
],
- "Image should be smaller than 1 MB": [
+ "never": [
""
],
"Country": [
@@ -116,277 +230,427 @@ strings['de'] = {
"Description": [
""
],
- "Name": [
+ "Product": [
""
],
- "loading...": [
+ "search products by it's description or id": [
""
],
- "no products found": [
+ "no products found with that description": [
""
],
- "no results": [
+ "You must enter a valid product identifier.": [
""
],
- "Deleting": [
+ "Quantity must be greater than 0!": [
""
],
- "Changing": [
+ "This quantity exceeds remaining stock. Currently, only %1$s units remain unreserved in stock.": [
""
],
- "Manage token": [
+ "Quantity": [
""
],
- "Update": [
+ "how many products will be added": [
+ ""
+ ],
+ "Add from inventory": [
+ ""
+ ],
+ "Image should be smaller than 1 MB": [
+ ""
+ ],
+ "Add": [
""
],
"Remove": [
""
],
- "Cancel": [
+ "No taxes configured for this product.": [
""
],
- "Manage stock": [
+ "Amount": [
""
],
- "Infinite": [
+ "Taxes can be in currencies that differ from the main currency used by the merchant.": [
""
],
- "lost cannot be greater that current + incoming (max %1$s)": [
+ "Enter currency and value separated with a colon, e.g. &quot;USD:2.3&quot;.": [
""
],
- "current stock will change from %1$s to %2$s": [
+ "Legal name of the tax, e.g. VAT or import duties.": [
""
],
- "current stock will stay at %1$s": [
+ "add tax to the tax list": [
""
],
- "Incoming": [
+ "describe and add a product that is not in the inventory list": [
""
],
- "Lost": [
+ "Add custom product": [
""
],
- "Current": [
+ "Complete information of the product": [
""
],
- "without stock": [
+ "Image": [
""
],
- "Next restock": [
+ "photo of the product": [
""
],
- "Delivery address": [
+ "full product description": [
""
],
- "this product has no taxes": [
+ "Unit": [
""
],
- "Amount": [
+ "name of the product unit": [
""
],
- "currency and value separated with colon": [
+ "Price": [
""
],
- "Add": [
+ "amount in the current currency": [
""
],
- "Instance": [
+ "Taxes": [
""
],
- "Settings": [
+ "image": [
""
],
- "Orders": [
+ "description": [
""
],
- "Products": [
+ "quantity": [
""
],
- "Transfers": [
+ "unit price": [
""
],
- "Connection": [
+ "total price": [
""
],
- "Instances": [
+ "required": [
""
],
- "New": [
+ "not valid": [
""
],
- "List": [
+ "must be greater than 0": [
""
],
- "Log out": [
+ "not a valid json": [
""
],
- "Clear": [
+ "should be in the future": [
""
],
- "should be the same": [
+ "refund deadline cannot be before pay deadline": [
""
],
- "cannot be the same as before": [
+ "wire transfer deadline cannot be before refund deadline": [
""
],
- "You are updating the authorization token from instance %1$s with id %2$s": [
+ "wire transfer deadline cannot be before pay deadline": [
""
],
- "Old token": [
+ "should have a refund deadline": [
""
],
- "New token": [
+ "auto refund cannot be after refund deadline": [
""
],
- "Clearing the auth token will mean public access to the instance": [
+ "Manage products in order": [
""
],
- "ID": [
+ "Manage list of products in the order.": [
""
],
- "Image": [
+ "Remove this product from the order.": [
""
],
- "Unit": [
+ "Total price": [
""
],
- "Price": [
+ "total product price added up": [
""
],
- "Stock": [
+ "Amount to be paid by the customer": [
""
],
- "Taxes": [
+ "Order price": [
""
],
- "Server not found": [
+ "final order price": [
""
],
- "Couldn't access the server": [
+ "Summary": [
""
],
- "Got message %1$s from %2$s": [
+ "Title of the order to be shown to the customer": [
""
],
- "Unexpected Error": [
+ "Shipping and Fulfillment": [
""
],
- "Auth token": [
+ "Delivery date": [
""
],
- "Account address": [
+ "Deadline for physical delivery assured by the merchant.": [
""
],
- "Default max deposit fee": [
+ "Location": [
""
],
- "Default max wire fee": [
+ "address where the products will be delivered": [
""
],
- "Default wire fee amortization": [
+ "Fulfillment URL": [
""
],
- "Jurisdiction": [
+ "URL to which the user will be redirected after successful payment.": [
""
],
- "Default pay delay": [
+ "Taler payment options": [
""
],
- "Default wire transfer delay": [
+ "Override default Taler payment settings for this order": [
""
],
- "could not create instance": [
+ "Payment deadline": [
""
],
- "Delete": [
+ "Deadline for the customer to pay for the offer before it expires. Inventory products will be reserved until this deadline.": [
""
],
- "Edit": [
+ "Refund deadline": [
""
],
- "There is no instances yet, add more pressing the + sign": [
+ "Time until which the order can be refunded by the merchant.": [
""
],
- "Inventory products": [
+ "Wire transfer deadline": [
""
],
- "Total price": [
+ "Deadline for the exchange to make the wire transfer.": [
""
],
- "Total tax": [
+ "Auto-refund deadline": [
""
],
- "Order price": [
+ "Time until which the wallet will automatically check for refunds without user interaction.": [
""
],
- "Net": [
+ "Maximum deposit fee": [
""
],
- "Summary": [
+ "Maximum deposit fees the merchant is willing to cover for this order. Higher deposit fees must be covered in full by the consumer.": [
""
],
- "Payments options": [
+ "Maximum wire fee": [
""
],
- "Auto refund deadline": [
+ "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.": [
""
],
- "Refund deadline": [
+ "Wire fee amortization": [
""
],
- "Pay deadline": [
+ "Factor by which wire fees exceeding the above threshold are divided to determine the share of excess wire fees to be paid explicitly by the consumer.": [
""
],
- "Delivery date": [
+ "Create token": [
""
],
- "Location": [
+ "Uncheck this option if the merchant backend generated an order ID with enough entropy to prevent adversarial claims.": [
+ ""
+ ],
+ "Minimum age required": [
+ ""
+ ],
+ "Any value greater than 0 will limit the coins able be used to pay this contract. If empty the age restriction will be defined by the products": [
+ ""
+ ],
+ "Min age defined by the producs is %1$s": [
+ ""
+ ],
+ "Additional information": [
+ ""
+ ],
+ "Custom information to be included in the contract for this order.": [
+ ""
+ ],
+ "You must enter a value in JavaScript Object Notation (JSON).": [
+ ""
+ ],
+ "days": [
+ ""
+ ],
+ "hours": [
+ ""
+ ],
+ "minutes": [
+ ""
+ ],
+ "seconds": [
+ ""
+ ],
+ "forever": [
+ ""
+ ],
+ "%1$sM": [
+ ""
+ ],
+ "%1$sY": [
+ ""
+ ],
+ "%1$sd": [
+ ""
+ ],
+ "%1$sh": [
+ ""
+ ],
+ "%1$smin": [
+ ""
+ ],
+ "%1$ssec": [
+ ""
+ ],
+ "Orders": [
+ ""
+ ],
+ "create order": [
+ ""
+ ],
+ "load newer orders": [
+ ""
+ ],
+ "Date": [
+ ""
+ ],
+ "Refund": [
+ ""
+ ],
+ "copy url": [
+ ""
+ ],
+ "load older orders": [
+ ""
+ ],
+ "No orders have been found matching your query!": [
+ ""
+ ],
+ "duplicated": [
+ ""
+ ],
+ "invalid format": [
+ ""
+ ],
+ "this value exceed the refundable amount": [
+ ""
+ ],
+ "date": [
+ ""
+ ],
+ "amount": [
+ ""
+ ],
+ "reason": [
+ ""
+ ],
+ "amount to be refunded": [
+ ""
+ ],
+ "Max refundable:": [
+ ""
+ ],
+ "Reason": [
+ ""
+ ],
+ "Choose one...": [
+ ""
+ ],
+ "requested by the customer": [
+ ""
+ ],
+ "other": [
+ ""
+ ],
+ "why this order is being refunded": [
+ ""
+ ],
+ "more information to give context": [
+ ""
+ ],
+ "Contract Terms": [
+ ""
+ ],
+ "human-readable description of the whole purchase": [
+ ""
+ ],
+ "total price for the transaction": [
+ ""
+ ],
+ "URL for this purchase": [
""
],
"Max fee": [
""
],
+ "maximum total deposit fee accepted by the merchant for this contract": [
+ ""
+ ],
"Max wire fee": [
""
],
- "Wire fee amortization": [
+ "maximum wire fee accepted by the merchant": [
""
],
- "Fullfilment url": [
+ "over how many customer transactions does the merchant expect to amortize wire fees on average": [
""
],
- "Extra information": [
+ "Created at": [
""
],
- "select a product first": [
+ "time when this contract was generated": [
""
],
- "should be greater than 0": [
+ "after this deadline has passed no refunds will be accepted": [
""
],
- "cannot be greater than current stock and quantity previously added. max: %1$s": [
+ "after this deadline, the merchant won't accept payments for the contract": [
""
],
- "cannot be greater than current stock %1$s": [
+ "transfer deadline for the exchange": [
""
],
- "Quantity": [
+ "time indicating when the order should be delivered": [
""
],
- "Order": [
+ "where the order will be delivered": [
""
],
- "claimed": [
+ "Auto-refund delay": [
""
],
- "copy url": [
+ "how long the wallet should try to get an automatic refund for the purchase": [
""
],
- "pay at": [
+ "Extra info": [
""
],
- "created at": [
+ "extra data that is only interpreted by the merchant frontend": [
+ ""
+ ],
+ "Order": [
+ ""
+ ],
+ "claimed": [
+ ""
+ ],
+ "claimed at": [
""
],
"Timeline": [
@@ -410,90 +674,180 @@ strings['de'] = {
"refunded": [
""
],
+ "refund order": [
+ ""
+ ],
+ "not refundable": [
+ ""
+ ],
"refund": [
""
],
"Refunded amount": [
""
],
- "Deposit total": [
+ "Refund taken": [
+ ""
+ ],
+ "Status URL": [
+ ""
+ ],
+ "Refund URI": [
""
],
"unpaid": [
""
],
+ "pay at": [
+ ""
+ ],
+ "created at": [
+ ""
+ ],
"Order status URL": [
""
],
- "Pay URI": [
+ "Payment URI": [
""
],
"Unknown order status. This is an error, please contact the administrator.": [
""
],
+ "Back": [
+ ""
+ ],
"refund created successfully": [
""
],
"could not create the refund": [
""
],
- "load newer orders": [
+ "select date to show nearby orders": [
""
],
- "Date": [
+ "order id": [
""
],
- "Refund": [
+ "jump to order with the given order ID": [
""
],
- "load older orders": [
+ "remove all filters": [
""
],
- "No orders has been found": [
+ "only show paid orders": [
""
],
- "date": [
+ "Paid": [
""
],
- "amount": [
+ "only show orders with refunds": [
""
],
- "reason": [
+ "Refunded": [
""
],
- "Max refundable:": [
+ "only show orders where customers paid, but wire payments from payment provider are still pending": [
""
],
- "Reason": [
+ "Not wired": [
""
],
- "duplicated": [
+ "clear date filter": [
""
],
- "requested by the customer": [
+ "date (YYYY/MM/DD)": [
""
],
- "other": [
+ "Enter an order id": [
""
],
- "go to order id": [
+ "order not found": [
""
],
- "Paid": [
+ "could not get the order to refund": [
""
],
- "Refunded": [
+ "Loading...": [
""
],
- "Not wired": [
+ "click here to configure the stock of the product, leave it as is and the backend will not control stock": [
""
],
- "All": [
+ "Manage stock": [
+ ""
+ ],
+ "this product has been configured without stock control": [
+ ""
+ ],
+ "Infinite": [
+ ""
+ ],
+ "lost cannot be greater than current and incoming (max %1$s)": [
+ ""
+ ],
+ "Incoming": [
+ ""
+ ],
+ "Lost": [
+ ""
+ ],
+ "Current": [
+ ""
+ ],
+ "remove stock control for this product": [
+ ""
+ ],
+ "without stock": [
+ ""
+ ],
+ "Next restock": [
+ ""
+ ],
+ "Delivery address": [
+ ""
+ ],
+ "product identification to use in URLs (for internal use only)": [
+ ""
+ ],
+ "illustration of the product for customers": [
+ ""
+ ],
+ "product description for customers": [
+ ""
+ ],
+ "Age restricted": [
+ ""
+ ],
+ "is this product restricted for customer below certain age?": [
+ ""
+ ],
+ "unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 items, 5 meters) for customers": [
+ ""
+ ],
+ "sale price for customers, including taxes, for above units of the product": [
+ ""
+ ],
+ "Stock": [
+ ""
+ ],
+ "product inventory for products with finite supply (for internal use only)": [
+ ""
+ ],
+ "taxes included in the product price, exposed to customers": [
+ ""
+ ],
+ "Need to complete marked fields": [
""
],
"could not create product": [
""
],
+ "Products": [
+ ""
+ ],
+ "add product to inventory": [
+ ""
+ ],
"Sell": [
""
],
@@ -503,6 +857,42 @@ strings['de'] = {
"Sold": [
""
],
+ "free": [
+ ""
+ ],
+ "go to product update page": [
+ ""
+ ],
+ "Update": [
+ ""
+ ],
+ "remove this product from the database": [
+ ""
+ ],
+ "update the product with new price": [
+ ""
+ ],
+ "update product with new price": [
+ ""
+ ],
+ "add more elements to the inventory": [
+ ""
+ ],
+ "report elements lost in the inventory": [
+ ""
+ ],
+ "new price for the product": [
+ ""
+ ],
+ "the are value with errors": [
+ ""
+ ],
+ "update product with new stock and price": [
+ ""
+ ],
+ "There is no products yet, add more pressing the + sign": [
+ ""
+ ],
"product updated successfully": [
""
],
@@ -515,25 +905,355 @@ strings['de'] = {
"could not delete the product": [
""
],
+ "Product id:": [
+ ""
+ ],
+ "To complete the setup of the reserve, you must now initiate a wire transfer using the given wire transfer subject and crediting the specified amount to the indicated account of the exchange.": [
+ ""
+ ],
+ "If your system supports RFC 8905, you can do this by opening this URI:": [
+ ""
+ ],
+ "it should be greater than 0": [
+ ""
+ ],
+ "must be a valid URL": [
+ ""
+ ],
+ "Initial balance": [
+ ""
+ ],
+ "balance prior to deposit": [
+ ""
+ ],
+ "Exchange URL": [
+ ""
+ ],
+ "URL of exchange": [
+ ""
+ ],
+ "Next": [
+ ""
+ ],
+ "Wire method": [
+ ""
+ ],
+ "method to use for wire transfer": [
+ ""
+ ],
+ "Select one wire method": [
+ ""
+ ],
+ "could not create reserve": [
+ ""
+ ],
+ "Valid until": [
+ ""
+ ],
+ "Created balance": [
+ ""
+ ],
+ "Exchange balance": [
+ ""
+ ],
+ "Picked up": [
+ ""
+ ],
+ "Committed": [
+ ""
+ ],
+ "Account address": [
+ ""
+ ],
+ "Subject": [
+ ""
+ ],
"Tips": [
""
],
- "Committed amount": [
+ "No tips has been authorized from this reserve": [
""
],
- "Exchange initial amount": [
+ "Authorized": [
""
],
- "Merchant initial amount": [
+ "Expiration": [
""
],
- "There is no tips yet, add more pressing the + sign": [
+ "amount of tip": [
""
],
- "cannot be empty": [
+ "Justification": [
+ ""
+ ],
+ "reason for the tip": [
+ ""
+ ],
+ "URL after tip": [
+ ""
+ ],
+ "URL to visit after tip payment": [
+ ""
+ ],
+ "Reserves not yet funded": [
+ ""
+ ],
+ "Reserves ready": [
+ ""
+ ],
+ "add new reserve": [
+ ""
+ ],
+ "Expires at": [
+ ""
+ ],
+ "Initial": [
+ ""
+ ],
+ "delete selected reserve from the database": [
+ ""
+ ],
+ "authorize new tip from selected reserve": [
+ ""
+ ],
+ "There is no ready reserves yet, add more pressing the + sign or fund them": [
+ ""
+ ],
+ "Expected Balance": [
+ ""
+ ],
+ "could not create the tip": [
+ ""
+ ],
+ "should not be empty": [
+ ""
+ ],
+ "should be greater that 0": [
+ ""
+ ],
+ "can't be empty": [
+ ""
+ ],
+ "to short": [
+ ""
+ ],
+ "just letters and numbers from 2 to 7": [
+ ""
+ ],
+ "size of the key should be 32": [
+ ""
+ ],
+ "Identifier": [
+ ""
+ ],
+ "Name of the template in URLs.": [
+ ""
+ ],
+ "Describe what this template stands for": [
+ ""
+ ],
+ "Fixed summary": [
+ ""
+ ],
+ "If specified, this template will create order with the same summary": [
+ ""
+ ],
+ "Fixed price": [
+ ""
+ ],
+ "If specified, this template will create order with the same price": [
+ ""
+ ],
+ "Minimum age": [
+ ""
+ ],
+ "Is this contract restricted to some age?": [
+ ""
+ ],
+ "Payment timeout": [
+ ""
+ ],
+ "How much time has the customer to complete the payment once the order was created.": [
+ ""
+ ],
+ "Verification algorithm": [
+ ""
+ ],
+ "Algorithm to use to verify transaction in offline mode": [
+ ""
+ ],
+ "Point-of-sale key": [
+ ""
+ ],
+ "Useful to validate the purchase": [
+ ""
+ ],
+ "generate random secret key": [
+ ""
+ ],
+ "random": [
+ ""
+ ],
+ "show secret key": [
+ ""
+ ],
+ "hide secret key": [
+ ""
+ ],
+ "hide": [
+ ""
+ ],
+ "show": [
+ ""
+ ],
+ "could not inform template": [
+ ""
+ ],
+ "Amount is required": [
+ ""
+ ],
+ "Order summary is required": [
+ ""
+ ],
+ "New order for template": [
+ ""
+ ],
+ "Amount of the order": [
+ ""
+ ],
+ "Order summary": [
+ ""
+ ],
+ "could not create order from template": [
+ ""
+ ],
+ "Here you can specify a default value for fields that are not fixed. Default values can be edited by the customer before the payment.": [
+ ""
+ ],
+ "Fixed amount": [
+ ""
+ ],
+ "Default amount": [
+ ""
+ ],
+ "Default summary": [
+ ""
+ ],
+ "Print": [
+ ""
+ ],
+ "Setup TOTP": [
+ ""
+ ],
+ "Templates": [
+ ""
+ ],
+ "add new templates": [
+ ""
+ ],
+ "load more templates before the first one": [
+ ""
+ ],
+ "load newer templates": [
+ ""
+ ],
+ "delete selected templates from the database": [
+ ""
+ ],
+ "use template to create new order": [
+ ""
+ ],
+ "create qr code for the template": [
+ ""
+ ],
+ "load more templates after the last one": [
""
],
- "check the id, doest look valid": [
+ "load older templates": [
+ ""
+ ],
+ "There is no templates yet, add more pressing the + sign": [
+ ""
+ ],
+ "template delete successfully": [
+ ""
+ ],
+ "could not delete the template": [
+ ""
+ ],
+ "could not update template": [
+ ""
+ ],
+ "should be one of '%1$s'": [
+ ""
+ ],
+ "Webhook ID to use": [
+ ""
+ ],
+ "Event": [
+ ""
+ ],
+ "The event of the webhook: why the webhook is used": [
+ ""
+ ],
+ "Method": [
+ ""
+ ],
+ "Method used by the webhook": [
+ ""
+ ],
+ "URL": [
+ ""
+ ],
+ "URL of the webhook where the customer will be redirected": [
+ ""
+ ],
+ "Header": [
+ ""
+ ],
+ "Header template of the webhook": [
+ ""
+ ],
+ "Body": [
+ ""
+ ],
+ "Body template by the webhook": [
+ ""
+ ],
+ "Webhooks": [
+ ""
+ ],
+ "add new webhooks": [
+ ""
+ ],
+ "load more webhooks before the first one": [
+ ""
+ ],
+ "load newer webhooks": [
+ ""
+ ],
+ "Event type": [
+ ""
+ ],
+ "delete selected webhook from the database": [
+ ""
+ ],
+ "load more webhooks after the last one": [
+ ""
+ ],
+ "load older webhooks": [
+ ""
+ ],
+ "There is no webhooks yet, add more pressing the + sign": [
+ ""
+ ],
+ "webhook delete successfully": [
+ ""
+ ],
+ "could not delete the webhook": [
+ ""
+ ],
+ "check the id, does not look valid": [
""
],
"should have 52 characters, current %1$s": [
@@ -542,18 +1262,42 @@ strings['de'] = {
"URL doesn't have the right format": [
""
],
- "Transfer ID": [
+ "Credited bank account": [
""
],
- "Account Address": [
+ "Select one account": [
""
],
- "Exchange URL": [
+ "Bank account of the merchant where the payment was received": [
+ ""
+ ],
+ "Wire transfer ID": [
+ ""
+ ],
+ "unique identifier of the wire transfer used by the exchange, must be 52 characters long": [
+ ""
+ ],
+ "Base URL of the exchange that made the transfer, should have been in the wire transfer subject": [
+ ""
+ ],
+ "Amount credited": [
+ ""
+ ],
+ "Actual amount that was wired to the merchant's bank account": [
""
],
"could not inform transfer": [
""
],
+ "Transfers": [
+ ""
+ ],
+ "add new transfer": [
+ ""
+ ],
+ "load more transfers before the first one": [
+ ""
+ ],
"load newer transfers": [
""
],
@@ -578,11 +1322,302 @@ strings['de'] = {
"unknown": [
""
],
+ "delete selected transfer from the database": [
+ ""
+ ],
+ "load more transfer after the last one": [
+ ""
+ ],
"load older transfers": [
""
],
"There is no transfer yet, add more pressing the + sign": [
""
+ ],
+ "filter by account address": [
+ ""
+ ],
+ "only show wire transfers confirmed by the merchant": [
+ ""
+ ],
+ "only show wire transfers claimed by the exchange": [
+ ""
+ ],
+ "Unverified": [
+ ""
+ ],
+ "is not valid": [
+ ""
+ ],
+ "is not a number": [
+ ""
+ ],
+ "must be 1 or greater": [
+ ""
+ ],
+ "max 7 lines": [
+ ""
+ ],
+ "change authorization configuration": [
+ ""
+ ],
+ "Need to complete marked fields and choose authorization method": [
+ ""
+ ],
+ "This is not a valid bitcoin address.": [
+ ""
+ ],
+ "This is not a valid Ethereum address.": [
+ ""
+ ],
+ "IBAN numbers usually have more that 4 digits": [
+ ""
+ ],
+ "IBAN numbers usually have less that 34 digits": [
+ ""
+ ],
+ "IBAN country code not found": [
+ ""
+ ],
+ "IBAN number is not valid, checksum is wrong": [
+ ""
+ ],
+ "Target type": [
+ ""
+ ],
+ "Method to use for wire transfer": [
+ ""
+ ],
+ "Routing": [
+ ""
+ ],
+ "Routing number.": [
+ ""
+ ],
+ "Account": [
+ ""
+ ],
+ "Account number.": [
+ ""
+ ],
+ "Business Identifier Code.": [
+ ""
+ ],
+ "Bank Account Number.": [
+ ""
+ ],
+ "Unified Payment Interface.": [
+ ""
+ ],
+ "Bitcoin protocol.": [
+ ""
+ ],
+ "Ethereum protocol.": [
+ ""
+ ],
+ "Interledger protocol.": [
+ ""
+ ],
+ "Host": [
+ ""
+ ],
+ "Bank host.": [
+ ""
+ ],
+ "Bank account.": [
+ ""
+ ],
+ "Bank account owner's name.": [
+ ""
+ ],
+ "No accounts yet.": [
+ ""
+ ],
+ "Name of the instance in URLs. The 'default' instance is special in that it is used to administer other instances.": [
+ ""
+ ],
+ "Business name": [
+ ""
+ ],
+ "Legal name of the business represented by this instance.": [
+ ""
+ ],
+ "Email": [
+ ""
+ ],
+ "Contact email": [
+ ""
+ ],
+ "Website URL": [
+ ""
+ ],
+ "URL.": [
+ ""
+ ],
+ "Logo": [
+ ""
+ ],
+ "Logo image.": [
+ ""
+ ],
+ "Bank account": [
+ ""
+ ],
+ "URI specifying bank account for crediting revenue.": [
+ ""
+ ],
+ "Default max deposit fee": [
+ ""
+ ],
+ "Maximum deposit fees this merchant is willing to pay per order by default.": [
+ ""
+ ],
+ "Default max wire fee": [
+ ""
+ ],
+ "Maximum wire fees this merchant is willing to pay per wire transfer by default.": [
+ ""
+ ],
+ "Default wire fee amortization": [
+ ""
+ ],
+ "Number of orders excess wire transfer fees will be divided by to compute per order surcharge.": [
+ ""
+ ],
+ "Physical location of the merchant.": [
+ ""
+ ],
+ "Jurisdiction": [
+ ""
+ ],
+ "Jurisdiction for legal disputes with the merchant.": [
+ ""
+ ],
+ "Default payment delay": [
+ ""
+ ],
+ "Time customers have to pay an order before the offer expires by default.": [
+ ""
+ ],
+ "Default wire transfer delay": [
+ ""
+ ],
+ "Maximum time an exchange is allowed to delay wiring funds to the merchant, enabling it to aggregate smaller payments into larger wire transfers and reducing wire fees.": [
+ ""
+ ],
+ "Instance id": [
+ ""
+ ],
+ "Change the authorization method use for this instance.": [
+ ""
+ ],
+ "Manage access token": [
+ ""
+ ],
+ "Failed to create instance": [
+ ""
+ ],
+ "Login required": [
+ ""
+ ],
+ "Please enter your access token.": [
+ ""
+ ],
+ "Access Token": [
+ ""
+ ],
+ "The request to the backend take too long and was cancelled": [
+ ""
+ ],
+ "Diagnostic from %1$s is \"%2$s\"": [
+ ""
+ ],
+ "The backend reported a problem: HTTP status #%1$s": [
+ ""
+ ],
+ "Diagnostic from %1$s is '%2$s'": [
+ ""
+ ],
+ "Access denied": [
+ ""
+ ],
+ "The access token provided is invalid.": [
+ ""
+ ],
+ "No 'default' instance configured yet.": [
+ ""
+ ],
+ "Create a 'default' instance to begin using the merchant backoffice.": [
+ ""
+ ],
+ "The access token provided is invalid": [
+ ""
+ ],
+ "Hide for today": [
+ ""
+ ],
+ "Instance": [
+ ""
+ ],
+ "Settings": [
+ ""
+ ],
+ "Connection": [
+ ""
+ ],
+ "New": [
+ ""
+ ],
+ "List": [
+ ""
+ ],
+ "Log out": [
+ ""
+ ],
+ "Check your token is valid": [
+ ""
+ ],
+ "Couldn't access the server.": [
+ ""
+ ],
+ "Could not infer instance id from url %1$s": [
+ ""
+ ],
+ "Server not found": [
+ ""
+ ],
+ "Server response with an error code": [
+ ""
+ ],
+ "Got message %1$s from %2$s": [
+ ""
+ ],
+ "Response from server is unreadable, http status: %1$s": [
+ ""
+ ],
+ "Unexpected Error": [
+ ""
+ ],
+ "The value %1$s is invalid for a payment url": [
+ ""
+ ],
+ "add element to the list": [
+ ""
+ ],
+ "add": [
+ ""
+ ],
+ "Deleting": [
+ ""
+ ],
+ "Changing": [
+ ""
+ ],
+ "Order ID": [
+ ""
+ ],
+ "Payment URL": [
+ ""
]
}
}
@@ -597,58 +1632,172 @@ strings['en'] = {
"plural_forms": "nplurals=2; plural=(n != 1);",
"lang": ""
},
- "Access denied": [
+ "Cancel": [
""
],
- "Check your token is valid": [
+ "%1$s": [
""
],
- "Couldn't access the server.": [
+ "Close": [
""
],
- "Could not infer instance id from url %1$s": [
+ "Continue": [
""
],
- "HTTP status #%1$s: Server reported a problem": [
+ "Clear": [
""
],
- "Got message: \"%1$s\" from: %2$s": [
+ "Confirm": [
""
],
- "No default instance": [
+ "is not the same as the current access token": [
""
],
- "in order to use merchant backoffice, you should create the default instance": [
+ "cannot be empty": [
""
],
- "Server reported a problem: HTTP status #%1$s": [
+ "cannot be the same as the old token": [
""
],
- "Got message: %1$s from: %2$s": [
+ "is not the same": [
""
],
- "Login required": [
+ "You are updating the access token from instance with id %1$s": [
""
],
- "Please enter your auth token. Token should have \"secret-token:\" and start with Bearer or ApiKey": [
+ "Old access token": [
""
],
- "Confirm": [
+ "access token currently in use": [
""
],
- "The value %1$s is invalid for a payment url": [
+ "New access token": [
+ ""
+ ],
+ "next access token to be used": [
+ ""
+ ],
+ "Repeat access token": [
+ ""
+ ],
+ "confirm the same access token": [
+ ""
+ ],
+ "Clearing the access token will mean public access to the instance": [
+ ""
+ ],
+ "cannot be the same as the old access token": [
+ ""
+ ],
+ "You are setting the access token for the new instance": [
+ ""
+ ],
+ "With external authorization method no check will be done by the merchant backend": [
+ ""
+ ],
+ "Set external authorization": [
+ ""
+ ],
+ "Set access token": [
+ ""
+ ],
+ "Operation in progress...": [
+ ""
+ ],
+ "The operation will be automatically canceled after %1$s seconds": [
+ ""
+ ],
+ "Instances": [
+ ""
+ ],
+ "Delete": [
+ ""
+ ],
+ "add new instance": [
+ ""
+ ],
+ "ID": [
+ ""
+ ],
+ "Name": [
+ ""
+ ],
+ "Edit": [
+ ""
+ ],
+ "Purge": [
+ ""
+ ],
+ "There is no instances yet, add more pressing the + sign": [
""
],
- "pick a date": [
+ "Only show active instances": [
+ ""
+ ],
+ "Active": [
+ ""
+ ],
+ "Only show deleted instances": [
+ ""
+ ],
+ "Deleted": [
+ ""
+ ],
+ "Show all instances": [
+ ""
+ ],
+ "All": [
+ ""
+ ],
+ "Instance \"%1$s\" (ID: %2$s) has been deleted": [
+ ""
+ ],
+ "Failed to delete instance": [
+ ""
+ ],
+ "Instance '%1$s' (ID: %2$s) has been disabled": [
+ ""
+ ],
+ "Failed to purge instance": [
+ ""
+ ],
+ "Pending KYC verification": [
+ ""
+ ],
+ "Timed out": [
+ ""
+ ],
+ "Exchange": [
+ ""
+ ],
+ "Target account": [
+ ""
+ ],
+ "KYC URL": [
+ ""
+ ],
+ "Code": [
+ ""
+ ],
+ "Http Status": [
+ ""
+ ],
+ "No pending kyc verification!": [
+ ""
+ ],
+ "change value to unknown date": [
+ ""
+ ],
+ "change value to empty": [
""
],
"clear": [
""
],
- "never": [
+ "change value to never": [
""
],
- "Image should be smaller than 1 MB": [
+ "never": [
""
],
"Country": [
@@ -687,277 +1836,427 @@ strings['en'] = {
"Description": [
""
],
- "Name": [
+ "Product": [
""
],
- "loading...": [
+ "search products by it's description or id": [
""
],
- "no products found": [
+ "no products found with that description": [
""
],
- "no results": [
+ "You must enter a valid product identifier.": [
""
],
- "Deleting": [
+ "Quantity must be greater than 0!": [
""
],
- "Changing": [
+ "This quantity exceeds remaining stock. Currently, only %1$s units remain unreserved in stock.": [
""
],
- "Manage token": [
+ "Quantity": [
""
],
- "Update": [
+ "how many products will be added": [
+ ""
+ ],
+ "Add from inventory": [
+ ""
+ ],
+ "Image should be smaller than 1 MB": [
+ ""
+ ],
+ "Add": [
""
],
"Remove": [
""
],
- "Cancel": [
+ "No taxes configured for this product.": [
""
],
- "Manage stock": [
+ "Amount": [
""
],
- "Infinite": [
+ "Taxes can be in currencies that differ from the main currency used by the merchant.": [
""
],
- "lost cannot be greater that current + incoming (max %1$s)": [
+ "Enter currency and value separated with a colon, e.g. &quot;USD:2.3&quot;.": [
""
],
- "current stock will change from %1$s to %2$s": [
+ "Legal name of the tax, e.g. VAT or import duties.": [
""
],
- "current stock will stay at %1$s": [
+ "add tax to the tax list": [
""
],
- "Incoming": [
+ "describe and add a product that is not in the inventory list": [
""
],
- "Lost": [
+ "Add custom product": [
""
],
- "Current": [
+ "Complete information of the product": [
""
],
- "without stock": [
+ "Image": [
""
],
- "Next restock": [
+ "photo of the product": [
""
],
- "Delivery address": [
+ "full product description": [
""
],
- "this product has no taxes": [
+ "Unit": [
""
],
- "Amount": [
+ "name of the product unit": [
""
],
- "currency and value separated with colon": [
+ "Price": [
""
],
- "Add": [
+ "amount in the current currency": [
""
],
- "Instance": [
+ "Taxes": [
""
],
- "Settings": [
+ "image": [
""
],
- "Orders": [
+ "description": [
""
],
- "Products": [
+ "quantity": [
""
],
- "Transfers": [
+ "unit price": [
""
],
- "Connection": [
+ "total price": [
""
],
- "Instances": [
+ "required": [
""
],
- "New": [
+ "not valid": [
""
],
- "List": [
+ "must be greater than 0": [
""
],
- "Log out": [
+ "not a valid json": [
""
],
- "Clear": [
+ "should be in the future": [
""
],
- "should be the same": [
+ "refund deadline cannot be before pay deadline": [
""
],
- "cannot be the same as before": [
+ "wire transfer deadline cannot be before refund deadline": [
""
],
- "You are updating the authorization token from instance %1$s with id %2$s": [
+ "wire transfer deadline cannot be before pay deadline": [
""
],
- "Old token": [
+ "should have a refund deadline": [
""
],
- "New token": [
+ "auto refund cannot be after refund deadline": [
""
],
- "Clearing the auth token will mean public access to the instance": [
+ "Manage products in order": [
""
],
- "ID": [
+ "Manage list of products in the order.": [
""
],
- "Image": [
+ "Remove this product from the order.": [
""
],
- "Unit": [
+ "Total price": [
""
],
- "Price": [
+ "total product price added up": [
""
],
- "Stock": [
+ "Amount to be paid by the customer": [
""
],
- "Taxes": [
+ "Order price": [
""
],
- "Server not found": [
+ "final order price": [
""
],
- "Couldn't access the server": [
+ "Summary": [
""
],
- "Got message %1$s from %2$s": [
+ "Title of the order to be shown to the customer": [
""
],
- "Unexpected Error": [
+ "Shipping and Fulfillment": [
""
],
- "Auth token": [
+ "Delivery date": [
""
],
- "Account address": [
+ "Deadline for physical delivery assured by the merchant.": [
""
],
- "Default max deposit fee": [
+ "Location": [
""
],
- "Default max wire fee": [
+ "address where the products will be delivered": [
""
],
- "Default wire fee amortization": [
+ "Fulfillment URL": [
""
],
- "Jurisdiction": [
+ "URL to which the user will be redirected after successful payment.": [
""
],
- "Default pay delay": [
+ "Taler payment options": [
""
],
- "Default wire transfer delay": [
+ "Override default Taler payment settings for this order": [
""
],
- "could not create instance": [
+ "Payment deadline": [
""
],
- "Delete": [
+ "Deadline for the customer to pay for the offer before it expires. Inventory products will be reserved until this deadline.": [
""
],
- "Edit": [
+ "Refund deadline": [
""
],
- "There is no instances yet, add more pressing the + sign": [
+ "Time until which the order can be refunded by the merchant.": [
""
],
- "Inventory products": [
+ "Wire transfer deadline": [
""
],
- "Total price": [
+ "Deadline for the exchange to make the wire transfer.": [
""
],
- "Total tax": [
+ "Auto-refund deadline": [
""
],
- "Order price": [
+ "Time until which the wallet will automatically check for refunds without user interaction.": [
""
],
- "Net": [
+ "Maximum deposit fee": [
""
],
- "Summary": [
+ "Maximum deposit fees the merchant is willing to cover for this order. Higher deposit fees must be covered in full by the consumer.": [
""
],
- "Payments options": [
+ "Maximum wire fee": [
""
],
- "Auto refund deadline": [
+ "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.": [
""
],
- "Refund deadline": [
+ "Wire fee amortization": [
""
],
- "Pay deadline": [
+ "Factor by which wire fees exceeding the above threshold are divided to determine the share of excess wire fees to be paid explicitly by the consumer.": [
""
],
- "Delivery date": [
+ "Create token": [
""
],
- "Location": [
+ "Uncheck this option if the merchant backend generated an order ID with enough entropy to prevent adversarial claims.": [
+ ""
+ ],
+ "Minimum age required": [
+ ""
+ ],
+ "Any value greater than 0 will limit the coins able be used to pay this contract. If empty the age restriction will be defined by the products": [
+ ""
+ ],
+ "Min age defined by the producs is %1$s": [
+ ""
+ ],
+ "Additional information": [
+ ""
+ ],
+ "Custom information to be included in the contract for this order.": [
+ ""
+ ],
+ "You must enter a value in JavaScript Object Notation (JSON).": [
+ ""
+ ],
+ "days": [
+ ""
+ ],
+ "hours": [
+ ""
+ ],
+ "minutes": [
+ ""
+ ],
+ "seconds": [
+ ""
+ ],
+ "forever": [
+ ""
+ ],
+ "%1$sM": [
+ ""
+ ],
+ "%1$sY": [
+ ""
+ ],
+ "%1$sd": [
+ ""
+ ],
+ "%1$sh": [
+ ""
+ ],
+ "%1$smin": [
+ ""
+ ],
+ "%1$ssec": [
+ ""
+ ],
+ "Orders": [
+ ""
+ ],
+ "create order": [
+ ""
+ ],
+ "load newer orders": [
+ ""
+ ],
+ "Date": [
+ ""
+ ],
+ "Refund": [
+ ""
+ ],
+ "copy url": [
+ ""
+ ],
+ "load older orders": [
+ ""
+ ],
+ "No orders have been found matching your query!": [
+ ""
+ ],
+ "duplicated": [
+ ""
+ ],
+ "invalid format": [
+ ""
+ ],
+ "this value exceed the refundable amount": [
+ ""
+ ],
+ "date": [
+ ""
+ ],
+ "amount": [
+ ""
+ ],
+ "reason": [
+ ""
+ ],
+ "amount to be refunded": [
+ ""
+ ],
+ "Max refundable:": [
+ ""
+ ],
+ "Reason": [
+ ""
+ ],
+ "Choose one...": [
+ ""
+ ],
+ "requested by the customer": [
+ ""
+ ],
+ "other": [
+ ""
+ ],
+ "why this order is being refunded": [
+ ""
+ ],
+ "more information to give context": [
+ ""
+ ],
+ "Contract Terms": [
+ ""
+ ],
+ "human-readable description of the whole purchase": [
+ ""
+ ],
+ "total price for the transaction": [
+ ""
+ ],
+ "URL for this purchase": [
""
],
"Max fee": [
""
],
+ "maximum total deposit fee accepted by the merchant for this contract": [
+ ""
+ ],
"Max wire fee": [
""
],
- "Wire fee amortization": [
+ "maximum wire fee accepted by the merchant": [
""
],
- "Fullfilment url": [
+ "over how many customer transactions does the merchant expect to amortize wire fees on average": [
""
],
- "Extra information": [
+ "Created at": [
""
],
- "select a product first": [
+ "time when this contract was generated": [
""
],
- "should be greater than 0": [
+ "after this deadline has passed no refunds will be accepted": [
""
],
- "cannot be greater than current stock and quantity previously added. max: %1$s": [
+ "after this deadline, the merchant won't accept payments for the contract": [
""
],
- "cannot be greater than current stock %1$s": [
+ "transfer deadline for the exchange": [
""
],
- "Quantity": [
+ "time indicating when the order should be delivered": [
""
],
- "Order": [
+ "where the order will be delivered": [
""
],
- "claimed": [
+ "Auto-refund delay": [
""
],
- "copy url": [
+ "how long the wallet should try to get an automatic refund for the purchase": [
""
],
- "pay at": [
+ "Extra info": [
""
],
- "created at": [
+ "extra data that is only interpreted by the merchant frontend": [
+ ""
+ ],
+ "Order": [
+ ""
+ ],
+ "claimed": [
+ ""
+ ],
+ "claimed at": [
""
],
"Timeline": [
@@ -981,90 +2280,180 @@ strings['en'] = {
"refunded": [
""
],
+ "refund order": [
+ ""
+ ],
+ "not refundable": [
+ ""
+ ],
"refund": [
""
],
"Refunded amount": [
""
],
- "Deposit total": [
+ "Refund taken": [
+ ""
+ ],
+ "Status URL": [
+ ""
+ ],
+ "Refund URI": [
""
],
"unpaid": [
""
],
+ "pay at": [
+ ""
+ ],
+ "created at": [
+ ""
+ ],
"Order status URL": [
""
],
- "Pay URI": [
+ "Payment URI": [
""
],
"Unknown order status. This is an error, please contact the administrator.": [
""
],
+ "Back": [
+ ""
+ ],
"refund created successfully": [
""
],
"could not create the refund": [
""
],
- "load newer orders": [
+ "select date to show nearby orders": [
""
],
- "Date": [
+ "order id": [
""
],
- "Refund": [
+ "jump to order with the given order ID": [
""
],
- "load older orders": [
+ "remove all filters": [
""
],
- "No orders has been found": [
+ "only show paid orders": [
""
],
- "date": [
+ "Paid": [
""
],
- "amount": [
+ "only show orders with refunds": [
""
],
- "reason": [
+ "Refunded": [
""
],
- "Max refundable:": [
+ "only show orders where customers paid, but wire payments from payment provider are still pending": [
""
],
- "Reason": [
+ "Not wired": [
""
],
- "duplicated": [
+ "clear date filter": [
""
],
- "requested by the customer": [
+ "date (YYYY/MM/DD)": [
""
],
- "other": [
+ "Enter an order id": [
""
],
- "go to order id": [
+ "order not found": [
""
],
- "Paid": [
+ "could not get the order to refund": [
""
],
- "Refunded": [
+ "Loading...": [
""
],
- "Not wired": [
+ "click here to configure the stock of the product, leave it as is and the backend will not control stock": [
""
],
- "All": [
+ "Manage stock": [
+ ""
+ ],
+ "this product has been configured without stock control": [
+ ""
+ ],
+ "Infinite": [
+ ""
+ ],
+ "lost cannot be greater than current and incoming (max %1$s)": [
+ ""
+ ],
+ "Incoming": [
+ ""
+ ],
+ "Lost": [
+ ""
+ ],
+ "Current": [
+ ""
+ ],
+ "remove stock control for this product": [
+ ""
+ ],
+ "without stock": [
+ ""
+ ],
+ "Next restock": [
+ ""
+ ],
+ "Delivery address": [
+ ""
+ ],
+ "product identification to use in URLs (for internal use only)": [
+ ""
+ ],
+ "illustration of the product for customers": [
+ ""
+ ],
+ "product description for customers": [
+ ""
+ ],
+ "Age restricted": [
+ ""
+ ],
+ "is this product restricted for customer below certain age?": [
+ ""
+ ],
+ "unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 items, 5 meters) for customers": [
+ ""
+ ],
+ "sale price for customers, including taxes, for above units of the product": [
+ ""
+ ],
+ "Stock": [
+ ""
+ ],
+ "product inventory for products with finite supply (for internal use only)": [
+ ""
+ ],
+ "taxes included in the product price, exposed to customers": [
+ ""
+ ],
+ "Need to complete marked fields": [
""
],
"could not create product": [
""
],
+ "Products": [
+ ""
+ ],
+ "add product to inventory": [
+ ""
+ ],
"Sell": [
""
],
@@ -1074,6 +2463,42 @@ strings['en'] = {
"Sold": [
""
],
+ "free": [
+ ""
+ ],
+ "go to product update page": [
+ ""
+ ],
+ "Update": [
+ ""
+ ],
+ "remove this product from the database": [
+ ""
+ ],
+ "update the product with new price": [
+ ""
+ ],
+ "update product with new price": [
+ ""
+ ],
+ "add more elements to the inventory": [
+ ""
+ ],
+ "report elements lost in the inventory": [
+ ""
+ ],
+ "new price for the product": [
+ ""
+ ],
+ "the are value with errors": [
+ ""
+ ],
+ "update product with new stock and price": [
+ ""
+ ],
+ "There is no products yet, add more pressing the + sign": [
+ ""
+ ],
"product updated successfully": [
""
],
@@ -1086,25 +2511,355 @@ strings['en'] = {
"could not delete the product": [
""
],
+ "Product id:": [
+ ""
+ ],
+ "To complete the setup of the reserve, you must now initiate a wire transfer using the given wire transfer subject and crediting the specified amount to the indicated account of the exchange.": [
+ ""
+ ],
+ "If your system supports RFC 8905, you can do this by opening this URI:": [
+ ""
+ ],
+ "it should be greater than 0": [
+ ""
+ ],
+ "must be a valid URL": [
+ ""
+ ],
+ "Initial balance": [
+ ""
+ ],
+ "balance prior to deposit": [
+ ""
+ ],
+ "Exchange URL": [
+ ""
+ ],
+ "URL of exchange": [
+ ""
+ ],
+ "Next": [
+ ""
+ ],
+ "Wire method": [
+ ""
+ ],
+ "method to use for wire transfer": [
+ ""
+ ],
+ "Select one wire method": [
+ ""
+ ],
+ "could not create reserve": [
+ ""
+ ],
+ "Valid until": [
+ ""
+ ],
+ "Created balance": [
+ ""
+ ],
+ "Exchange balance": [
+ ""
+ ],
+ "Picked up": [
+ ""
+ ],
+ "Committed": [
+ ""
+ ],
+ "Account address": [
+ ""
+ ],
+ "Subject": [
+ ""
+ ],
"Tips": [
""
],
- "Committed amount": [
+ "No tips has been authorized from this reserve": [
""
],
- "Exchange initial amount": [
+ "Authorized": [
""
],
- "Merchant initial amount": [
+ "Expiration": [
""
],
- "There is no tips yet, add more pressing the + sign": [
+ "amount of tip": [
""
],
- "cannot be empty": [
+ "Justification": [
+ ""
+ ],
+ "reason for the tip": [
+ ""
+ ],
+ "URL after tip": [
+ ""
+ ],
+ "URL to visit after tip payment": [
+ ""
+ ],
+ "Reserves not yet funded": [
+ ""
+ ],
+ "Reserves ready": [
+ ""
+ ],
+ "add new reserve": [
+ ""
+ ],
+ "Expires at": [
+ ""
+ ],
+ "Initial": [
+ ""
+ ],
+ "delete selected reserve from the database": [
+ ""
+ ],
+ "authorize new tip from selected reserve": [
+ ""
+ ],
+ "There is no ready reserves yet, add more pressing the + sign or fund them": [
+ ""
+ ],
+ "Expected Balance": [
+ ""
+ ],
+ "could not create the tip": [
+ ""
+ ],
+ "should not be empty": [
+ ""
+ ],
+ "should be greater that 0": [
+ ""
+ ],
+ "can't be empty": [
+ ""
+ ],
+ "to short": [
+ ""
+ ],
+ "just letters and numbers from 2 to 7": [
+ ""
+ ],
+ "size of the key should be 32": [
+ ""
+ ],
+ "Identifier": [
+ ""
+ ],
+ "Name of the template in URLs.": [
+ ""
+ ],
+ "Describe what this template stands for": [
+ ""
+ ],
+ "Fixed summary": [
+ ""
+ ],
+ "If specified, this template will create order with the same summary": [
+ ""
+ ],
+ "Fixed price": [
+ ""
+ ],
+ "If specified, this template will create order with the same price": [
+ ""
+ ],
+ "Minimum age": [
+ ""
+ ],
+ "Is this contract restricted to some age?": [
+ ""
+ ],
+ "Payment timeout": [
+ ""
+ ],
+ "How much time has the customer to complete the payment once the order was created.": [
+ ""
+ ],
+ "Verification algorithm": [
+ ""
+ ],
+ "Algorithm to use to verify transaction in offline mode": [
+ ""
+ ],
+ "Point-of-sale key": [
+ ""
+ ],
+ "Useful to validate the purchase": [
+ ""
+ ],
+ "generate random secret key": [
+ ""
+ ],
+ "random": [
+ ""
+ ],
+ "show secret key": [
+ ""
+ ],
+ "hide secret key": [
+ ""
+ ],
+ "hide": [
+ ""
+ ],
+ "show": [
+ ""
+ ],
+ "could not inform template": [
+ ""
+ ],
+ "Amount is required": [
+ ""
+ ],
+ "Order summary is required": [
+ ""
+ ],
+ "New order for template": [
+ ""
+ ],
+ "Amount of the order": [
+ ""
+ ],
+ "Order summary": [
+ ""
+ ],
+ "could not create order from template": [
+ ""
+ ],
+ "Here you can specify a default value for fields that are not fixed. Default values can be edited by the customer before the payment.": [
+ ""
+ ],
+ "Fixed amount": [
+ ""
+ ],
+ "Default amount": [
+ ""
+ ],
+ "Default summary": [
+ ""
+ ],
+ "Print": [
+ ""
+ ],
+ "Setup TOTP": [
+ ""
+ ],
+ "Templates": [
+ ""
+ ],
+ "add new templates": [
+ ""
+ ],
+ "load more templates before the first one": [
+ ""
+ ],
+ "load newer templates": [
+ ""
+ ],
+ "delete selected templates from the database": [
+ ""
+ ],
+ "use template to create new order": [
+ ""
+ ],
+ "create qr code for the template": [
+ ""
+ ],
+ "load more templates after the last one": [
+ ""
+ ],
+ "load older templates": [
+ ""
+ ],
+ "There is no templates yet, add more pressing the + sign": [
+ ""
+ ],
+ "template delete successfully": [
+ ""
+ ],
+ "could not delete the template": [
+ ""
+ ],
+ "could not update template": [
+ ""
+ ],
+ "should be one of '%1$s'": [
+ ""
+ ],
+ "Webhook ID to use": [
+ ""
+ ],
+ "Event": [
+ ""
+ ],
+ "The event of the webhook: why the webhook is used": [
+ ""
+ ],
+ "Method": [
+ ""
+ ],
+ "Method used by the webhook": [
+ ""
+ ],
+ "URL": [
+ ""
+ ],
+ "URL of the webhook where the customer will be redirected": [
+ ""
+ ],
+ "Header": [
+ ""
+ ],
+ "Header template of the webhook": [
+ ""
+ ],
+ "Body": [
+ ""
+ ],
+ "Body template by the webhook": [
""
],
- "check the id, doest look valid": [
+ "Webhooks": [
+ ""
+ ],
+ "add new webhooks": [
+ ""
+ ],
+ "load more webhooks before the first one": [
+ ""
+ ],
+ "load newer webhooks": [
+ ""
+ ],
+ "Event type": [
+ ""
+ ],
+ "delete selected webhook from the database": [
+ ""
+ ],
+ "load more webhooks after the last one": [
+ ""
+ ],
+ "load older webhooks": [
+ ""
+ ],
+ "There is no webhooks yet, add more pressing the + sign": [
+ ""
+ ],
+ "webhook delete successfully": [
+ ""
+ ],
+ "could not delete the webhook": [
+ ""
+ ],
+ "check the id, does not look valid": [
""
],
"should have 52 characters, current %1$s": [
@@ -1113,18 +2868,42 @@ strings['en'] = {
"URL doesn't have the right format": [
""
],
- "Transfer ID": [
+ "Credited bank account": [
""
],
- "Account Address": [
+ "Select one account": [
""
],
- "Exchange URL": [
+ "Bank account of the merchant where the payment was received": [
+ ""
+ ],
+ "Wire transfer ID": [
+ ""
+ ],
+ "unique identifier of the wire transfer used by the exchange, must be 52 characters long": [
+ ""
+ ],
+ "Base URL of the exchange that made the transfer, should have been in the wire transfer subject": [
+ ""
+ ],
+ "Amount credited": [
+ ""
+ ],
+ "Actual amount that was wired to the merchant's bank account": [
""
],
"could not inform transfer": [
""
],
+ "Transfers": [
+ ""
+ ],
+ "add new transfer": [
+ ""
+ ],
+ "load more transfers before the first one": [
+ ""
+ ],
"load newer transfers": [
""
],
@@ -1149,11 +2928,302 @@ strings['en'] = {
"unknown": [
""
],
+ "delete selected transfer from the database": [
+ ""
+ ],
+ "load more transfer after the last one": [
+ ""
+ ],
"load older transfers": [
""
],
"There is no transfer yet, add more pressing the + sign": [
""
+ ],
+ "filter by account address": [
+ ""
+ ],
+ "only show wire transfers confirmed by the merchant": [
+ ""
+ ],
+ "only show wire transfers claimed by the exchange": [
+ ""
+ ],
+ "Unverified": [
+ ""
+ ],
+ "is not valid": [
+ ""
+ ],
+ "is not a number": [
+ ""
+ ],
+ "must be 1 or greater": [
+ ""
+ ],
+ "max 7 lines": [
+ ""
+ ],
+ "change authorization configuration": [
+ ""
+ ],
+ "Need to complete marked fields and choose authorization method": [
+ ""
+ ],
+ "This is not a valid bitcoin address.": [
+ ""
+ ],
+ "This is not a valid Ethereum address.": [
+ ""
+ ],
+ "IBAN numbers usually have more that 4 digits": [
+ ""
+ ],
+ "IBAN numbers usually have less that 34 digits": [
+ ""
+ ],
+ "IBAN country code not found": [
+ ""
+ ],
+ "IBAN number is not valid, checksum is wrong": [
+ ""
+ ],
+ "Target type": [
+ ""
+ ],
+ "Method to use for wire transfer": [
+ ""
+ ],
+ "Routing": [
+ ""
+ ],
+ "Routing number.": [
+ ""
+ ],
+ "Account": [
+ ""
+ ],
+ "Account number.": [
+ ""
+ ],
+ "Business Identifier Code.": [
+ ""
+ ],
+ "Bank Account Number.": [
+ ""
+ ],
+ "Unified Payment Interface.": [
+ ""
+ ],
+ "Bitcoin protocol.": [
+ ""
+ ],
+ "Ethereum protocol.": [
+ ""
+ ],
+ "Interledger protocol.": [
+ ""
+ ],
+ "Host": [
+ ""
+ ],
+ "Bank host.": [
+ ""
+ ],
+ "Bank account.": [
+ ""
+ ],
+ "Bank account owner's name.": [
+ ""
+ ],
+ "No accounts yet.": [
+ ""
+ ],
+ "Name of the instance in URLs. The 'default' instance is special in that it is used to administer other instances.": [
+ ""
+ ],
+ "Business name": [
+ ""
+ ],
+ "Legal name of the business represented by this instance.": [
+ ""
+ ],
+ "Email": [
+ ""
+ ],
+ "Contact email": [
+ ""
+ ],
+ "Website URL": [
+ ""
+ ],
+ "URL.": [
+ ""
+ ],
+ "Logo": [
+ ""
+ ],
+ "Logo image.": [
+ ""
+ ],
+ "Bank account": [
+ ""
+ ],
+ "URI specifying bank account for crediting revenue.": [
+ ""
+ ],
+ "Default max deposit fee": [
+ ""
+ ],
+ "Maximum deposit fees this merchant is willing to pay per order by default.": [
+ ""
+ ],
+ "Default max wire fee": [
+ ""
+ ],
+ "Maximum wire fees this merchant is willing to pay per wire transfer by default.": [
+ ""
+ ],
+ "Default wire fee amortization": [
+ ""
+ ],
+ "Number of orders excess wire transfer fees will be divided by to compute per order surcharge.": [
+ ""
+ ],
+ "Physical location of the merchant.": [
+ ""
+ ],
+ "Jurisdiction": [
+ ""
+ ],
+ "Jurisdiction for legal disputes with the merchant.": [
+ ""
+ ],
+ "Default payment delay": [
+ ""
+ ],
+ "Time customers have to pay an order before the offer expires by default.": [
+ ""
+ ],
+ "Default wire transfer delay": [
+ ""
+ ],
+ "Maximum time an exchange is allowed to delay wiring funds to the merchant, enabling it to aggregate smaller payments into larger wire transfers and reducing wire fees.": [
+ ""
+ ],
+ "Instance id": [
+ ""
+ ],
+ "Change the authorization method use for this instance.": [
+ ""
+ ],
+ "Manage access token": [
+ ""
+ ],
+ "Failed to create instance": [
+ ""
+ ],
+ "Login required": [
+ ""
+ ],
+ "Please enter your access token.": [
+ ""
+ ],
+ "Access Token": [
+ ""
+ ],
+ "The request to the backend take too long and was cancelled": [
+ ""
+ ],
+ "Diagnostic from %1$s is \"%2$s\"": [
+ ""
+ ],
+ "The backend reported a problem: HTTP status #%1$s": [
+ ""
+ ],
+ "Diagnostic from %1$s is '%2$s'": [
+ ""
+ ],
+ "Access denied": [
+ ""
+ ],
+ "The access token provided is invalid.": [
+ ""
+ ],
+ "No 'default' instance configured yet.": [
+ ""
+ ],
+ "Create a 'default' instance to begin using the merchant backoffice.": [
+ ""
+ ],
+ "The access token provided is invalid": [
+ ""
+ ],
+ "Hide for today": [
+ ""
+ ],
+ "Instance": [
+ ""
+ ],
+ "Settings": [
+ ""
+ ],
+ "Connection": [
+ ""
+ ],
+ "New": [
+ ""
+ ],
+ "List": [
+ ""
+ ],
+ "Log out": [
+ ""
+ ],
+ "Check your token is valid": [
+ ""
+ ],
+ "Couldn't access the server.": [
+ ""
+ ],
+ "Could not infer instance id from url %1$s": [
+ ""
+ ],
+ "Server not found": [
+ ""
+ ],
+ "Server response with an error code": [
+ ""
+ ],
+ "Got message %1$s from %2$s": [
+ ""
+ ],
+ "Response from server is unreadable, http status: %1$s": [
+ ""
+ ],
+ "Unexpected Error": [
+ ""
+ ],
+ "The value %1$s is invalid for a payment url": [
+ ""
+ ],
+ "add element to the list": [
+ ""
+ ],
+ "add": [
+ ""
+ ],
+ "Deleting": [
+ ""
+ ],
+ "Changing": [
+ ""
+ ],
+ "Order ID": [
+ ""
+ ],
+ "Payment URL": [
+ ""
]
}
}
@@ -1165,63 +3235,177 @@ strings['es'] = {
"messages": {
"": {
"domain": "messages",
- "plural_forms": "nplurals=2; plural=(n != 1);",
- "lang": ""
+ "plural_forms": "nplurals=2; plural=n != 1;",
+ "lang": "es"
},
- "Access denied": [
- "Acceso denegado"
+ "Cancel": [
+ "Cancelar"
],
- "Check your token is valid": [
- "Verifica que el token sea valido"
+ "%1$s": [
+ "%1$s"
],
- "Couldn't access the server.": [
- "No se pudo acceder al servidor"
+ "Close": [
+ ""
],
- "Could not infer instance id from url %1$s": [
- "No se pudo inferir el id de la instancia con la url %1$s"
+ "Continue": [
+ "Continuar"
+ ],
+ "Clear": [
+ "Limpiar"
],
- "HTTP status #%1$s: Server reported a problem": [
- "HTTP status #%1$s: Servidor reporto un problema"
+ "Confirm": [
+ "Confirmar"
],
- "Got message: \"%1$s\" from: %2$s": [
- "Recivimos el mensaje %1$s desde %2$s"
+ "is not the same as the current access token": [
+ "no es el mismo que el token de acceso actual"
],
- "No default instance": [
- "Sin instancia default"
+ "cannot be empty": [
+ "no puede ser vacío"
],
- "in order to use merchant backoffice, you should create the default instance": [
- "para usar el merchant backoffice, debería crear la instancia default"
+ "cannot be the same as the old token": [
+ "no puede ser igual al viejo token"
],
- "Server reported a problem: HTTP status #%1$s": [
- "Servidir reporto un problema: HTTP status #%1$s"
+ "is not the same": [
+ "no son iguales"
],
- "Got message: %1$s from: %2$s": [
- "Recivimos el mensaje %1$s desde %2$s"
+ "You are updating the access token from instance with id %1$s": [
+ "Está actualizando el token de acceso para la instancia con id %1$s"
],
- "Login required": [
- "Login necesario"
+ "Old access token": [
+ "Viejo token de acceso"
],
- "Please enter your auth token. Token should have \"secret-token:\" and start with Bearer or ApiKey": [
- "Por favor ingrese su token de autorización. El token debe tener \"secret-token\" y comenzar con Bearer o ApiKey"
+ "access token currently in use": [
+ "acceder al token en uso actualmente"
],
- "Confirm": [
- "Confirmar"
+ "New access token": [
+ "Nuevo token de acceso"
],
- "The value %1$s is invalid for a payment url": [
- "El valor %1$s es invalido para una URL de pago"
+ "next access token to be used": [
+ "siguiente token de acceso a usar"
+ ],
+ "Repeat access token": [
+ "Repetir token de acceso"
+ ],
+ "confirm the same access token": [
+ "confirmar el mismo token de acceso"
+ ],
+ "Clearing the access token will mean public access to the instance": [
+ "Limpiar el token de acceso significa acceso público a la instancia"
+ ],
+ "cannot be the same as the old access token": [
+ "no puede ser igual al anterior token de acceso"
+ ],
+ "You are setting the access token for the new instance": [
+ "Está estableciendo el token de acceso para la nueva instancia"
+ ],
+ "With external authorization method no check will be done by the merchant backend": [
+ "Con el método de autorización externa no se hará ninguna revisión por el backend del comerciante"
+ ],
+ "Set external authorization": [
+ "Establecer autorización externa"
+ ],
+ "Set access token": [
+ "Establecer token de acceso"
+ ],
+ "Operation in progress...": [
+ "Operación en progreso..."
+ ],
+ "The operation will be automatically canceled after %1$s seconds": [
+ "La operación será automáticamente cancelada luego de %1$s segundos"
+ ],
+ "Instances": [
+ "Instancias"
+ ],
+ "Delete": [
+ "Eliminar"
+ ],
+ "add new instance": [
+ "agregar nueva instancia"
+ ],
+ "ID": [
+ "ID"
+ ],
+ "Name": [
+ "Nombre"
+ ],
+ "Edit": [
+ "Editar"
+ ],
+ "Purge": [
+ "Purgar"
+ ],
+ "There is no instances yet, add more pressing the + sign": [
+ "Todavía no hay instancias, agregue más presionando el signo +"
+ ],
+ "Only show active instances": [
+ "Solo mostrar instancias activas"
+ ],
+ "Active": [
+ "Activo"
+ ],
+ "Only show deleted instances": [
+ "Mostrar solo instancias eliminadas"
+ ],
+ "Deleted": [
+ "Eliminado"
+ ],
+ "Show all instances": [
+ "Mostrar todas las instancias"
+ ],
+ "All": [
+ "Todo"
+ ],
+ "Instance \"%1$s\" (ID: %2$s) has been deleted": [
+ "La instancia '%1$s' (ID: %2$s) fue eliminada"
+ ],
+ "Failed to delete instance": [
+ "Fallo al eliminar instancia"
+ ],
+ "Instance '%1$s' (ID: %2$s) has been disabled": [
+ "Instance '%1$s' (ID: %2$s) ha sido deshabilitada"
+ ],
+ "Failed to purge instance": [
+ "Fallo al purgar la instancia"
+ ],
+ "Pending KYC verification": [
+ "Verificación KYC pendiente"
],
- "pick a date": [
- "elegir una fecha"
+ "Timed out": [
+ "Expirado"
+ ],
+ "Exchange": [
+ "Exchange"
+ ],
+ "Target account": [
+ "Cuenta objetivo"
+ ],
+ "KYC URL": [
+ "URL de KYC"
+ ],
+ "Code": [
+ "Código"
+ ],
+ "Http Status": [
+ "Estado http"
+ ],
+ "No pending kyc verification!": [
+ "¡No hay verificación kyc pendiente!"
+ ],
+ "change value to unknown date": [
+ "cambiar valor a fecha desconocida"
+ ],
+ "change value to empty": [
+ "cambiar valor a vacío"
],
"clear": [
- "Limpiar"
+ "limpiar"
+ ],
+ "change value to never": [
+ "cambiar valor a nunca"
],
"never": [
"nunca"
],
- "Image should be smaller than 1 MB": [
- "La imagen debe ser mas chica que 1 MB"
- ],
"Country": [
"País"
],
@@ -1250,7 +3434,7 @@ strings['es'] = {
"Distrito"
],
"Country subdivision": [
- "Provincia"
+ "Subdivisión de país"
],
"Product id": [
"Id de producto"
@@ -1258,263 +3442,419 @@ strings['es'] = {
"Description": [
"Descripcion"
],
- "Name": [
- "Nombre"
+ "Product": [
+ "Productos"
],
- "loading...": [
- "Cargando..."
+ "search products by it's description or id": [
+ "buscar productos por su descripción o ID"
],
- "no products found": [
- "No se encontraron productos"
+ "no products found with that description": [
+ "no se encontraron productos con esa descripción"
],
- "no results": [
- "Sin resultados"
+ "You must enter a valid product identifier.": [
+ "Debe ingresar un identificador de producto válido."
],
- "Deleting": [
- "Borrando"
+ "Quantity must be greater than 0!": [
+ "¡Cantidad debe ser mayor que 0!"
],
- "Changing": [
- "Cambiando"
+ "This quantity exceeds remaining stock. Currently, only %1$s units remain unreserved in stock.": [
+ "Esta cantidad excede las existencias restantes. Actualmente, solo quedan %1$s unidades sin reservar en las existencias."
],
- "Manage token": [
- "Administrar token"
+ "Quantity": [
+ "Cantidad"
],
- "Update": [
- "Actualizar"
+ "how many products will be added": [
+ "cuántos productos serán agregados"
+ ],
+ "Add from inventory": [
+ "Agregar del inventario"
+ ],
+ "Image should be smaller than 1 MB": [
+ "La imagen debe ser mas chica que 1 MB"
+ ],
+ "Add": [
+ "Agregar"
],
"Remove": [
"Eliminar"
],
- "Cancel": [
- "Cancelar"
+ "No taxes configured for this product.": [
+ "Ningun impuesto configurado para este producto."
],
- "Manage stock": [
- "Administrar stock"
+ "Amount": [
+ "Monto"
],
- "Infinite": [
- "Inifinito"
+ "Taxes can be in currencies that differ from the main currency used by the merchant.": [
+ "Impuestos pueden estar en divisas que difieren de la principal divisa usada por el comerciante."
],
- "lost cannot be greater that current + incoming (max %1$s)": [
- "no puede ser mayor al stock actual %1$s"
+ "Enter currency and value separated with a colon, e.g. &quot;USD:2.3&quot;.": [
+ "Ingrese divisa y valor separado por dos puntos, e.g. &quot;USD:2.3&quot;."
],
- "current stock will change from %1$s to %2$s": [
- "stock actual cambiará desde %1$s a %2$s"
+ "Legal name of the tax, e.g. VAT or import duties.": [
+ "Nombre legal del impuesto, e.g. IVA o arancel."
],
- "current stock will stay at %1$s": [
- "stock actual seguirá en %1$s"
+ "add tax to the tax list": [
+ "agregar impuesto a la lista de impuestos"
],
- "Incoming": [
- "Ingresando"
+ "describe and add a product that is not in the inventory list": [
+ "describa y agregue un producto que no está en la lista de inventarios"
],
- "Lost": [
- "Perdido"
+ "Add custom product": [
+ "Agregue un producto personalizado"
],
- "Current": [
- "Actual"
+ "Complete information of the product": [
+ "Complete información del producto"
],
- "without stock": [
- "sin stock"
+ "Image": [
+ "Imagen"
],
- "Next restock": [
- "Próximo reabastecimiento"
+ "photo of the product": [
+ "foto del producto"
],
- "Delivery address": [
- "Dirección de entrega"
+ "full product description": [
+ "descripción completa del producto"
],
- "this product has no taxes": [
- "este producto no tiene impuestos"
+ "Unit": [
+ "Unidad"
],
- "Amount": [
- "Monto"
+ "name of the product unit": [
+ "nombre de la unidad del producto"
],
- "currency and value separated with colon": [
- "Moneda y valor separado por dos puntos"
+ "Price": [
+ "Precio"
],
- "Add": [
- "Agregar"
+ "amount in the current currency": [
+ "monto de la divisa actual"
],
- "Instance": [
- "Instancia"
+ "Taxes": [
+ "Impuestos"
],
- "Settings": [
- "Configuración"
+ "image": [
+ "imagen"
],
- "Orders": [
- "Ordenes"
+ "description": [
+ "descripción"
],
- "Products": [
- "Productos"
+ "quantity": [
+ "cantidad"
],
- "Transfers": [
- "Transferencias"
+ "unit price": [
+ "precio unitario"
],
- "Connection": [
- "Conexión"
+ "total price": [
+ "precio total"
],
- "Instances": [
- "Instancias"
+ "required": [
+ "requerido"
],
- "New": [
- "Nuevo"
+ "not valid": [
+ "no es un json válido"
],
- "List": [
- "Lista"
+ "must be greater than 0": [
+ "debe ser mayor que 0"
],
- "Log out": [
- "Salir"
+ "not a valid json": [
+ "no es un json válido"
],
- "Clear": [
- "Limpiar"
+ "should be in the future": [
+ "deberían ser en el futuro"
],
- "should be the same": [
- "deberían ser iguales"
+ "refund deadline cannot be before pay deadline": [
+ "plazo de reembolso no puede ser antes que el plazo de pago"
],
- "cannot be the same as before": [
- "no puede ser igual al anterior"
+ "wire transfer deadline cannot be before refund deadline": [
+ "el plazo de la transferencia bancaria no puede ser antes que el plazo de reembolso"
],
- "You are updating the authorization token from instance %1$s with id %2$s": [
- "Está actualizando el token de autorización para la instancia %1$s con id %2$s"
+ "wire transfer deadline cannot be before pay deadline": [
+ "el plazo de la transferencia bancaria no puede ser antes que el plazo de pago"
],
- "Old token": [
- "Viejo token"
+ "should have a refund deadline": [
+ "debería tener un plazo de reembolso"
],
- "New token": [
- "Nuevo token"
+ "auto refund cannot be after refund deadline": [
+ "reembolso automático no puede ser después qu el plazo de reembolso"
],
- "Clearing the auth token will mean public access to the instance": [
- "Limpiar el token de autorización significa acceso publico a la instancia"
+ "Manage products in order": [
+ "Manejar productos en orden"
],
- "ID": [
- "ID"
+ "Manage list of products in the order.": [
+ "Manejar lista de productos en la orden."
],
- "Image": [
- "Imagen"
+ "Remove this product from the order.": [
+ "Remover este producto de la orden."
],
- "Unit": [
- "Unidad"
+ "Total price": [
+ "Precio total"
],
- "Price": [
- "Precio"
+ "total product price added up": [
+ "precio total de producto agregado"
],
- "Stock": [
- "Stock"
+ "Amount to be paid by the customer": [
+ "Monto a ser pagado por el cliente"
],
- "Taxes": [
- "Impuesto"
+ "Order price": [
+ "Precio de la orden"
],
- "Server not found": [
- "Servidor no encontrado"
+ "final order price": [
+ "Precio final de la orden"
],
- "Couldn't access the server": [
- "No se pudo aceder al servidor"
+ "Summary": [
+ "Resumen"
],
- "Got message %1$s from %2$s": [
- "Recivimos el mensaje %1$s desde %2$s"
+ "Title of the order to be shown to the customer": [
+ "Título de la orden a ser mostrado al cliente"
],
- "Unexpected Error": [
- "Error inesperado"
+ "Shipping and Fulfillment": [
+ "Envío y cumplimiento"
],
- "Auth token": [
- "Token de autorización"
+ "Delivery date": [
+ "Fecha de entrega"
],
- "Account address": [
- "Dirección de cuenta"
+ "Deadline for physical delivery assured by the merchant.": [
+ "Plazo para la entrega física asegurado por el comerciante."
],
- "Default max deposit fee": [
- "Impuesto máximo de deposito por omisión"
+ "Location": [
+ "Ubicación"
],
- "Default max wire fee": [
- "Impuesto máximo de transferencia por omisión"
+ "address where the products will be delivered": [
+ "dirección a donde los productos serán entregados"
],
- "Default wire fee amortization": [
- "Amortización de impuesto de transferencia por omisión"
+ "Fulfillment URL": [
+ "URL de cumplimiento"
],
- "Jurisdiction": [
- "Jurisdicción"
+ "URL to which the user will be redirected after successful payment.": [
+ "URL al cual el usuario será redirigido luego de pago exitoso."
],
- "Default pay delay": [
- "Retrazo de pago por omisión"
+ "Taler payment options": [
+ "Opciones de pago de Taler"
],
- "Default wire transfer delay": [
- "Retrazo de transferencia por omisión"
+ "Override default Taler payment settings for this order": [
+ "Sobreescribir pagos por omisión de Taler para esta orden"
],
- "could not create instance": [
- "no se pudo crear la instancia"
+ "Payment deadline": [
+ "Plazo de pago"
],
- "Delete": [
- "Borrando"
+ "Deadline for the customer to pay for the offer before it expires. Inventory products will be reserved until this deadline.": [
+ "Plazo límite para que el cliente pague por la oferta antes de que expire. Productos del inventario serán reservados hasta este plazo límite."
],
- "Edit": [
+ "Refund deadline": [
+ "Plazo de reembolso"
+ ],
+ "Time until which the order can be refunded by the merchant.": [
+ "Tiempo hasta el cual la orden puede ser reembolsada por el comerciante."
+ ],
+ "Wire transfer deadline": [
+ "Plazo de la transferencia"
+ ],
+ "Deadline for the exchange to make the wire transfer.": [
+ "Plazo para que el exchange haga la transferencia."
+ ],
+ "Auto-refund deadline": [
+ "Plazo de reembolso automático"
+ ],
+ "Time until which the wallet will automatically check for refunds without user interaction.": [
+ "Tiempo hasta el cual la billetera será automáticamente revisada por reembolsos win interación por parte del usuario."
+ ],
+ "Maximum deposit fee": [
+ "Máxima tarifa de depósito"
+ ],
+ "Maximum deposit fees the merchant is willing to cover for this order. Higher deposit fees must be covered in full by the consumer.": [
+ "Máxima tarifa de depósito que el comerciante esta dispuesto a cubir para esta orden. Mayores tarifas de depósito deben ser cubiertas completamente por el consumidor."
+ ],
+ "Maximum wire fee": [
+ "Máxima tarifa de transferencia"
+ ],
+ "Maximum aggregate wire fees the merchant is willing to cover for this order. Wire fees exceeding this amount are to be covered by the customers.": [
""
],
- "There is no instances yet, add more pressing the + sign": [
- "No hay instancias todavían, agregue mas presionando el signo +"
+ "Wire fee amortization": [
+ "Amortización de comisión de transferencia"
],
- "Inventory products": [
- "Productos de inventario"
+ "Factor by which wire fees exceeding the above threshold are divided to determine the share of excess wire fees to be paid explicitly by the consumer.": [
+ ""
],
- "Total price": [
- "Precio total"
+ "Create token": [
+ "Administrar token"
],
- "Total tax": [
- "Impuesto total"
+ "Uncheck this option if the merchant backend generated an order ID with enough entropy to prevent adversarial claims.": [
+ ""
],
- "Order price": [
- "Precio de la orden"
+ "Minimum age required": [
+ "Login necesario"
],
- "Net": [
- "Neto"
+ "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": [
+ ""
],
- "Summary": [
- "Resumen"
+ "Min age defined by the producs is %1$s": [
+ ""
],
- "Payments options": [
- "Opciones de pago"
+ "Additional information": [
+ "Información extra"
],
- "Auto refund deadline": [
- "Plazo de reembolso automático"
+ "Custom information to be included in the contract for this order.": [
+ ""
],
- "Refund deadline": [
- "Plazo de reembolso"
+ "You must enter a value in JavaScript Object Notation (JSON).": [
+ ""
],
- "Pay deadline": [
- "Plazo de pago"
+ "days": [
+ "días"
],
- "Delivery date": [
- "Fecha de entrega"
+ "hours": [
+ "horas"
],
- "Location": [
- "Ubicación"
+ "minutes": [
+ "minutos"
+ ],
+ "seconds": [
+ "segundos"
+ ],
+ "forever": [
+ "nunca"
+ ],
+ "%1$sM": [
+ ""
+ ],
+ "%1$sY": [
+ ""
+ ],
+ "%1$sd": [
+ ""
+ ],
+ "%1$sh": [
+ ""
+ ],
+ "%1$smin": [
+ ""
+ ],
+ "%1$ssec": [
+ ""
+ ],
+ "Orders": [
+ "Órdenes"
+ ],
+ "create order": [
+ "creado"
+ ],
+ "load newer orders": [
+ "cargar nuevas ordenes"
+ ],
+ "Date": [
+ "Fecha"
+ ],
+ "Refund": [
+ "Devolución"
+ ],
+ "copy url": [
+ "copiar url"
+ ],
+ "load older orders": [
+ "cargar viejas ordenes"
+ ],
+ "No orders have been found matching your query!": [
+ "¡No se encontraron órdenes que emparejen su búsqueda!"
+ ],
+ "duplicated": [
+ "duplicado"
+ ],
+ "invalid format": [
+ "formato inválido"
+ ],
+ "this value exceed the refundable amount": [
+ "este monto excede el monto reembolsable"
+ ],
+ "date": [
+ "fecha"
+ ],
+ "amount": [
+ "monto"
+ ],
+ "reason": [
+ "razón"
+ ],
+ "amount to be refunded": [
+ "monto a ser reembolsado"
+ ],
+ "Max refundable:": [
+ "Máximo reembolzable:"
+ ],
+ "Reason": [
+ "Razón"
+ ],
+ "Choose one...": [
+ "Elija uno..."
+ ],
+ "requested by the customer": [
+ "pedido por el consumidor"
+ ],
+ "other": [
+ "otro"
+ ],
+ "why this order is being refunded": [
+ "por qué esta orden está siendo reembolsada"
+ ],
+ "more information to give context": [
+ "más información para dar contexto"
+ ],
+ "Contract Terms": [
+ "Términos de contrato"
+ ],
+ "human-readable description of the whole purchase": [
+ "descripción legible de toda la compra"
+ ],
+ "total price for the transaction": [
+ "precio total de la transacción"
+ ],
+ "URL for this purchase": [
+ "URL para esta compra"
],
"Max fee": [
- "Impuesto máximo"
+ "Máxima comisión"
+ ],
+ "maximum total deposit fee accepted by the merchant for this contract": [
+ ""
],
"Max wire fee": [
"Impuesto de transferencia máximo"
],
- "Wire fee amortization": [
- "Amortización de impuesto de transferencia"
+ "maximum wire fee accepted by the merchant": [
+ ""
],
- "Fullfilment url": [
- "URL de completitud"
+ "over how many customer transactions does the merchant expect to amortize wire fees on average": [
+ ""
],
- "Extra information": [
- "Información extra"
+ "Created at": [
+ "Creado en"
],
- "select a product first": [
- "seleccione un producto primero"
+ "time when this contract was generated": [
+ ""
],
- "should be greater than 0": [
- "La imagen debe ser mas chica que 1 MB"
+ "after this deadline has passed no refunds will be accepted": [
+ ""
],
- "cannot be greater than current stock and quantity previously added. max: %1$s": [
- "no puede ser mayor al stock actual y la cantidad previamente agregada. máximo: %1$s"
+ "after this deadline, the merchant won't accept payments for the contract": [
+ ""
],
- "cannot be greater than current stock %1$s": [
- "no puede ser mayor al stock actual %1$s"
+ "transfer deadline for the exchange": [
+ ""
],
- "Quantity": [
- "Cantidad"
+ "time indicating when the order should be delivered": [
+ ""
+ ],
+ "where the order will be delivered": [
+ ""
+ ],
+ "Auto-refund delay": [
+ "Plazo de reembolso automático"
+ ],
+ "how long the wallet should try to get an automatic refund for the purchase": [
+ ""
+ ],
+ "Extra info": [
+ "Información extra"
+ ],
+ "extra data that is only interpreted by the merchant frontend": [
+ ""
],
"Order": [
"Orden"
@@ -1522,14 +3862,8 @@ strings['es'] = {
"claimed": [
"reclamado"
],
- "copy url": [
- "copiar url"
- ],
- "pay at": [
- "pagar en"
- ],
- "created at": [
- "creado"
+ "claimed at": [
+ "reclamado"
],
"Timeline": [
"Cronología"
@@ -1552,90 +3886,180 @@ strings['es'] = {
"refunded": [
"reembolzado"
],
+ "refund order": [
+ "reembolzado"
+ ],
+ "not refundable": [
+ "Máximo reembolzable:"
+ ],
"refund": [
"reembolzar"
],
"Refunded amount": [
"Monto reembolzado"
],
- "Deposit total": [
- "Total depositado"
+ "Refund taken": [
+ "Reembolzado"
+ ],
+ "Status URL": [
+ "URL de estado de orden"
+ ],
+ "Refund URI": [
+ "Devolución"
],
"unpaid": [
"impago"
],
+ "pay at": [
+ "pagar en"
+ ],
+ "created at": [
+ "creado"
+ ],
"Order status URL": [
"URL de estado de orden"
],
- "Pay URI": [
+ "Payment URI": [
"URI de pago"
],
"Unknown order status. This is an error, please contact the administrator.": [
- "Estado de orden desconocido. Esto es un error, por favor contacte a su administrador"
+ "Estado de orden desconocido. Esto es un error, por favor contacte a su administrador."
+ ],
+ "Back": [
+ ""
],
"refund created successfully": [
"reembolzo creado satisfactoriamente"
],
"could not create the refund": [
- "No se pudo aceder al servidor"
+ "No se pudo create el reembolso"
],
- "load newer orders": [
- "cargar nuevas ordenes"
- ],
- "Date": [
- "Fecha"
+ "select date to show nearby orders": [
+ ""
],
- "Refund": [
- "Reembolzar"
+ "order id": [
+ "ir a id de orden"
],
- "load older orders": [
- "cargar viejas ordenes"
+ "jump to order with the given order ID": [
+ ""
],
- "No orders has been found": [
- "No se enconraron ordenes"
+ "remove all filters": [
+ ""
],
- "date": [
- "fecha"
+ "only show paid orders": [
+ ""
],
- "amount": [
- "monto"
+ "Paid": [
+ "Pagado"
],
- "reason": [
- "razón"
+ "only show orders with refunds": [
+ "No se pudo create el reembolso"
],
- "Max refundable:": [
- "Máximo reembolzable:"
+ "Refunded": [
+ "Reembolzado"
],
- "Reason": [
- "Razón"
+ "only show orders where customers paid, but wire payments from payment provider are still pending": [
+ ""
],
- "duplicated": [
- "duplicado"
+ "Not wired": [
+ "No transferido"
],
- "requested by the customer": [
- "pedido por el consumidor"
+ "clear date filter": [
+ ""
],
- "other": [
- "otro"
+ "date (YYYY/MM/DD)": [
+ ""
],
- "go to order id": [
+ "Enter an order id": [
"ir a id de orden"
],
- "Paid": [
- "Pagado"
+ "order not found": [
+ "Servidor no encontrado"
],
- "Refunded": [
- "Reembolzado"
+ "could not get the order to refund": [
+ "No se pudo create el reembolso"
],
- "Not wired": [
- "No transferido"
+ "Loading...": [
+ "Cargando..."
],
- "All": [
- "Todo"
+ "click here to configure the stock of the product, leave it as is and the backend will not control stock": [
+ ""
+ ],
+ "Manage stock": [
+ "Administrar stock"
+ ],
+ "this product has been configured without stock control": [
+ ""
+ ],
+ "Infinite": [
+ "Inifinito"
+ ],
+ "lost cannot be greater than current and incoming (max %1$s)": [
+ "la pérdida no puede ser mayor al stock actual + entrante (max %1$s )"
+ ],
+ "Incoming": [
+ "Ingresando"
+ ],
+ "Lost": [
+ "Perdido"
+ ],
+ "Current": [
+ "Actual"
+ ],
+ "remove stock control for this product": [
+ ""
+ ],
+ "without stock": [
+ "sin stock"
+ ],
+ "Next restock": [
+ "Próximo reabastecimiento"
+ ],
+ "Delivery address": [
+ "Dirección de entrega"
+ ],
+ "product identification to use in URLs (for internal use only)": [
+ ""
+ ],
+ "illustration of the product for customers": [
+ ""
+ ],
+ "product description for customers": [
+ ""
+ ],
+ "Age restricted": [
+ ""
+ ],
+ "is this product restricted for customer below certain age?": [
+ ""
+ ],
+ "unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 items, 5 meters) for customers": [
+ ""
+ ],
+ "sale price for customers, including taxes, for above units of the product": [
+ ""
+ ],
+ "Stock": [
+ "Existencias"
+ ],
+ "product inventory for products with finite supply (for internal use only)": [
+ ""
+ ],
+ "taxes included in the product price, exposed to customers": [
+ ""
+ ],
+ "Need to complete marked fields": [
+ ""
],
"could not create product": [
"no se pudo crear el producto"
],
+ "Products": [
+ "Productos"
+ ],
+ "add product to inventory": [
+ ""
+ ],
"Sell": [
"Venta"
],
@@ -1645,6 +4069,42 @@ strings['es'] = {
"Sold": [
"Vendido"
],
+ "free": [
+ "Gratis"
+ ],
+ "go to product update page": [
+ "producto actualizado correctamente"
+ ],
+ "Update": [
+ "Actualizar"
+ ],
+ "remove this product from the database": [
+ ""
+ ],
+ "update the product with new price": [
+ ""
+ ],
+ "update product with new price": [
+ ""
+ ],
+ "add more elements to the inventory": [
+ ""
+ ],
+ "report elements lost in the inventory": [
+ ""
+ ],
+ "new price for the product": [
+ "no se pudo actualizar el producto"
+ ],
+ "the are value with errors": [
+ ""
+ ],
+ "update product with new stock and price": [
+ ""
+ ],
+ "There is no products yet, add more pressing the + sign": [
+ "No hay propinas todavía, agregar mas presionando el signo +"
+ ],
"product updated successfully": [
"producto actualizado correctamente"
],
@@ -1657,25 +4117,355 @@ strings['es'] = {
"could not delete the product": [
"no se pudo eliminar el producto"
],
+ "Product id:": [
+ "Id de producto"
+ ],
+ "To complete the setup of the reserve, you must now initiate a wire transfer using the given wire transfer subject and crediting the specified amount to the indicated account of the exchange.": [
+ ""
+ ],
+ "If your system supports RFC 8905, you can do this by opening this URI:": [
+ ""
+ ],
+ "it should be greater than 0": [
+ "Debe ser mayor a 0"
+ ],
+ "must be a valid URL": [
+ ""
+ ],
+ "Initial balance": [
+ "Instancia"
+ ],
+ "balance prior to deposit": [
+ ""
+ ],
+ "Exchange URL": [
+ "URL del Exchange"
+ ],
+ "URL of exchange": [
+ ""
+ ],
+ "Next": [
+ "Siguiente"
+ ],
+ "Wire method": [
+ ""
+ ],
+ "method to use for wire transfer": [
+ "no se pudo informar la transferencia"
+ ],
+ "Select one wire method": [
+ ""
+ ],
+ "could not create reserve": [
+ "No se pudo create el reembolso"
+ ],
+ "Valid until": [
+ "Válido hasta"
+ ],
+ "Created balance": [
+ "creado"
+ ],
+ "Exchange balance": [
+ "Monto inicial"
+ ],
+ "Picked up": [
+ ""
+ ],
+ "Committed": [
+ "Monto confirmado"
+ ],
+ "Account address": [
+ "Dirección de cuenta"
+ ],
+ "Subject": [
+ "Asunto"
+ ],
"Tips": [
"Propinas"
],
- "Committed amount": [
+ "No tips has been authorized from this reserve": [
+ ""
+ ],
+ "Authorized": [
+ "Token de autorización"
+ ],
+ "Expiration": [
+ "Información extra"
+ ],
+ "amount of tip": [
+ "monto"
+ ],
+ "Justification": [
+ "Jurisdicción"
+ ],
+ "reason for the tip": [
""
],
- "Exchange initial amount": [
+ "URL after tip": [
""
],
- "Merchant initial amount": [
+ "URL to visit after tip payment": [
""
],
- "There is no tips yet, add more pressing the + sign": [
- "No hay propinas todavía, agregar mas presionando el signo +"
+ "Reserves not yet funded": [
+ "Servidor no encontrado"
],
- "cannot be empty": [
+ "Reserves ready": [
+ ""
+ ],
+ "add new reserve": [
+ "cargar nuevas transferencias"
+ ],
+ "Expires at": [
+ ""
+ ],
+ "Initial": [
+ ""
+ ],
+ "delete selected reserve from the database": [
+ ""
+ ],
+ "authorize new tip from selected reserve": [
+ ""
+ ],
+ "There is no ready reserves yet, add more pressing the + sign or fund them": [
+ "No hay transferencias todavía, agregar mas presionando el signo +"
+ ],
+ "Expected Balance": [
+ "Ejecutado en"
+ ],
+ "could not create the tip": [
+ "No se pudo create el reembolso"
+ ],
+ "should not be empty": [
"no puede ser vacío"
],
- "check the id, doest look valid": [
+ "should be greater that 0": [
+ "Debe ser mayor a 0"
+ ],
+ "can't be empty": [
+ "no puede ser vacío"
+ ],
+ "to short": [
+ ""
+ ],
+ "just letters and numbers from 2 to 7": [
+ ""
+ ],
+ "size of the key should be 32": [
+ ""
+ ],
+ "Identifier": [
+ ""
+ ],
+ "Name of the template in URLs.": [
+ ""
+ ],
+ "Describe what this template stands for": [
+ ""
+ ],
+ "Fixed summary": [
+ "Estado de orden"
+ ],
+ "If specified, this template will create order with the same summary": [
+ ""
+ ],
+ "Fixed price": [
+ "precio unitario"
+ ],
+ "If specified, this template will create order with the same price": [
+ ""
+ ],
+ "Minimum age": [
+ "Edad mínima"
+ ],
+ "Is this contract restricted to some age?": [
+ ""
+ ],
+ "Payment timeout": [
+ "Opciones de pago"
+ ],
+ "How much time has the customer to complete the payment once the order was created.": [
+ ""
+ ],
+ "Verification algorithm": [
+ ""
+ ],
+ "Algorithm to use to verify transaction in offline mode": [
+ ""
+ ],
+ "Point-of-sale key": [
+ ""
+ ],
+ "Useful to validate the purchase": [
+ ""
+ ],
+ "generate random secret key": [
+ ""
+ ],
+ "random": [
+ ""
+ ],
+ "show secret key": [
+ ""
+ ],
+ "hide secret key": [
+ ""
+ ],
+ "hide": [
+ ""
+ ],
+ "show": [
+ ""
+ ],
+ "could not inform template": [
+ "no se pudo informar la transferencia"
+ ],
+ "Amount is required": [
+ "Login necesario"
+ ],
+ "Order summary is required": [
+ ""
+ ],
+ "New order for template": [
+ "cargar viejas transferencias"
+ ],
+ "Amount of the order": [
+ ""
+ ],
+ "Order summary": [
+ "Estado de orden"
+ ],
+ "could not create order from template": [
+ "No se pudo create el reembolso"
+ ],
+ "Here you can specify a default value for fields that are not fixed. Default values can be edited by the customer before the payment.": [
+ ""
+ ],
+ "Fixed amount": [
+ "Monto reembolzado"
+ ],
+ "Default amount": [
+ "Monto reembolzado"
+ ],
+ "Default summary": [
+ "Estado de orden"
+ ],
+ "Print": [
+ ""
+ ],
+ "Setup TOTP": [
+ ""
+ ],
+ "Templates": [
+ ""
+ ],
+ "add new templates": [
+ ""
+ ],
+ "load more templates before the first one": [
+ ""
+ ],
+ "load newer templates": [
+ "cargar nuevas transferencias"
+ ],
+ "delete selected templates from the database": [
+ ""
+ ],
+ "use template to create new order": [
+ ""
+ ],
+ "create qr code for the template": [
+ "No se pudo create el reembolso"
+ ],
+ "load more templates after the last one": [
+ ""
+ ],
+ "load older templates": [
+ "cargar viejas transferencias"
+ ],
+ "There is no templates yet, add more pressing the + sign": [
+ "No hay propinas todavía, agregar mas presionando el signo +"
+ ],
+ "template delete successfully": [
+ "producto fue eliminado correctamente"
+ ],
+ "could not delete the template": [
+ "no se pudo eliminar el producto"
+ ],
+ "could not update template": [
+ "no se pudo actualizar el producto"
+ ],
+ "should be one of '%1$s'": [
+ "deberían ser iguales"
+ ],
+ "Webhook ID to use": [
+ ""
+ ],
+ "Event": [
+ ""
+ ],
+ "The event of the webhook: why the webhook is used": [
+ ""
+ ],
+ "Method": [
+ ""
+ ],
+ "Method used by the webhook": [
+ ""
+ ],
+ "URL": [
+ "URL"
+ ],
+ "URL of the webhook where the customer will be redirected": [
+ ""
+ ],
+ "Header": [
+ ""
+ ],
+ "Header template of the webhook": [
+ ""
+ ],
+ "Body": [
+ ""
+ ],
+ "Body template by the webhook": [
+ ""
+ ],
+ "Webhooks": [
+ ""
+ ],
+ "add new webhooks": [
+ ""
+ ],
+ "load more webhooks before the first one": [
+ ""
+ ],
+ "load newer webhooks": [
+ "cargar nuevas ordenes"
+ ],
+ "Event type": [
+ ""
+ ],
+ "delete selected webhook from the database": [
+ ""
+ ],
+ "load more webhooks after the last one": [
+ ""
+ ],
+ "load older webhooks": [
+ "cargar viejas ordenes"
+ ],
+ "There is no webhooks yet, add more pressing the + sign": [
+ "No hay propinas todavía, agregar mas presionando el signo +"
+ ],
+ "webhook delete successfully": [
+ "producto fue eliminado correctamente"
+ ],
+ "could not delete the webhook": [
+ "no se pudo eliminar el producto"
+ ],
+ "check the id, does not look valid": [
"verificar el id, no parece válido"
],
"should have 52 characters, current %1$s": [
@@ -1684,32 +4474,56 @@ strings['es'] = {
"URL doesn't have the right format": [
"La URL no tiene el formato correcto"
],
- "Transfer ID": [
- "Transferencias"
+ "Credited bank account": [
+ ""
],
- "Account Address": [
- "Dirección de cuenta"
+ "Select one account": [
+ ""
],
- "Exchange URL": [
- "URL del Exchange"
+ "Bank account of the merchant where the payment was received": [
+ ""
+ ],
+ "Wire transfer ID": [
+ "Id de transferencia"
+ ],
+ "unique identifier of the wire transfer used by the exchange, must be 52 characters long": [
+ ""
+ ],
+ "Base URL of the exchange that made the transfer, should have been in the wire transfer subject": [
+ ""
+ ],
+ "Amount credited": [
+ ""
+ ],
+ "Actual amount that was wired to the merchant's bank account": [
+ ""
],
"could not inform transfer": [
- "no se pudo crear la instancia"
+ "no se pudo informar la transferencia"
+ ],
+ "Transfers": [
+ "Transferencias"
+ ],
+ "add new transfer": [
+ "cargar nuevas transferencias"
+ ],
+ "load more transfers before the first one": [
+ ""
],
"load newer transfers": [
- "cargar nuevas ordenes"
+ "cargar nuevas transferencias"
],
"Credit": [
"Crédito"
],
"Confirmed": [
- "Confirmar"
+ "Confirmado"
],
"Verified": [
"Verificado"
],
"Executed at": [
- "creado"
+ "Ejecutado en"
],
"yes": [
"si"
@@ -1720,11 +4534,302 @@ strings['es'] = {
"unknown": [
"desconocido"
],
+ "delete selected transfer from the database": [
+ "eliminar transferencia seleccionada de la base de datos"
+ ],
+ "load more transfer after the last one": [
+ "cargue más transferencia luego de la última"
+ ],
"load older transfers": [
"cargar viejas transferencias"
],
"There is no transfer yet, add more pressing the + sign": [
"No hay transferencias todavía, agregar mas presionando el signo +"
+ ],
+ "filter by account address": [
+ "Dirección de cuenta"
+ ],
+ "only show wire transfers confirmed by the merchant": [
+ ""
+ ],
+ "only show wire transfers claimed by the exchange": [
+ ""
+ ],
+ "Unverified": [
+ "Verificado"
+ ],
+ "is not valid": [
+ ""
+ ],
+ "is not a number": [
+ "Número de edificio"
+ ],
+ "must be 1 or greater": [
+ "debe ser 1 o mayor"
+ ],
+ "max 7 lines": [
+ "máximo 7 líneas"
+ ],
+ "change authorization configuration": [
+ "cambiar configuración de autorización"
+ ],
+ "Need to complete marked fields and choose authorization method": [
+ "Necesita completar campos marcados y escoger un método de autorización"
+ ],
+ "This is not a valid bitcoin address.": [
+ "Esta no es una dirección de bitcoin válida."
+ ],
+ "This is not a valid Ethereum address.": [
+ "Esta no es una dirección de Ethereum válida."
+ ],
+ "IBAN numbers usually have more that 4 digits": [
+ "Números IBAN usualmente tienen más de 4 dígitos"
+ ],
+ "IBAN numbers usually have less that 34 digits": [
+ "Número IBAN usualmente tienen menos de 34 dígitos"
+ ],
+ "IBAN country code not found": [
+ "Código IBAN de país no encontrado"
+ ],
+ "IBAN number is not valid, checksum is wrong": [
+ "Número IBAN no es válido, la suma de verificación es incorrecta"
+ ],
+ "Target type": [
+ "Tipo objetivo"
+ ],
+ "Method to use for wire transfer": [
+ "Método a usar para la transferencia"
+ ],
+ "Routing": [
+ "Enrutamiento"
+ ],
+ "Routing number.": [
+ "Número de enrutamiento."
+ ],
+ "Account": [
+ "Cuenta"
+ ],
+ "Account number.": [
+ "Dirección de cuenta"
+ ],
+ "Business Identifier Code.": [
+ ""
+ ],
+ "Bank Account Number.": [
+ ""
+ ],
+ "Unified Payment Interface.": [
+ "Interfaz de pago unificado."
+ ],
+ "Bitcoin protocol.": [
+ ""
+ ],
+ "Ethereum protocol.": [
+ ""
+ ],
+ "Interledger protocol.": [
+ ""
+ ],
+ "Host": [
+ ""
+ ],
+ "Bank host.": [
+ ""
+ ],
+ "Bank account.": [
+ ""
+ ],
+ "Bank account owner's name.": [
+ ""
+ ],
+ "No accounts yet.": [
+ ""
+ ],
+ "Name of the instance in URLs. The 'default' instance is special in that it is used to administer other instances.": [
+ ""
+ ],
+ "Business name": [
+ "Nombre de edificio"
+ ],
+ "Legal name of the business represented by this instance.": [
+ ""
+ ],
+ "Email": [
+ ""
+ ],
+ "Contact email": [
+ ""
+ ],
+ "Website URL": [
+ "URL de sitio web"
+ ],
+ "URL.": [
+ ""
+ ],
+ "Logo": [
+ ""
+ ],
+ "Logo image.": [
+ ""
+ ],
+ "Bank account": [
+ "Cuenta bancaria"
+ ],
+ "URI specifying bank account for crediting revenue.": [
+ ""
+ ],
+ "Default max deposit fee": [
+ "Impuesto máximo de deposito por omisión"
+ ],
+ "Maximum deposit fees this merchant is willing to pay per order by default.": [
+ ""
+ ],
+ "Default max wire fee": [
+ "Impuesto máximo de transferencia por omisión"
+ ],
+ "Maximum wire fees this merchant is willing to pay per wire transfer by default.": [
+ ""
+ ],
+ "Default wire fee amortization": [
+ "Amortización de impuesto de transferencia por omisión"
+ ],
+ "Number of orders excess wire transfer fees will be divided by to compute per order surcharge.": [
+ ""
+ ],
+ "Physical location of the merchant.": [
+ ""
+ ],
+ "Jurisdiction": [
+ "Jurisdicción"
+ ],
+ "Jurisdiction for legal disputes with the merchant.": [
+ "Jurisdicción para disputas legales con el comerciante."
+ ],
+ "Default payment delay": [
+ "Retrazo de pago por omisión"
+ ],
+ "Time customers have to pay an order before the offer expires by default.": [
+ ""
+ ],
+ "Default wire transfer delay": [
+ "Retrazo de transferencia por omisión"
+ ],
+ "Maximum time an exchange is allowed to delay wiring funds to the merchant, enabling it to aggregate smaller payments into larger wire transfers and reducing wire fees.": [
+ ""
+ ],
+ "Instance id": [
+ "ID de instancia"
+ ],
+ "Change the authorization method use for this instance.": [
+ "Limpiar el token de autorización significa acceso público a la instancia"
+ ],
+ "Manage access token": [
+ "Administrar token de acceso"
+ ],
+ "Failed to create instance": [
+ "Fallo al crear la instancia"
+ ],
+ "Login required": [
+ "Login necesario"
+ ],
+ "Please enter your access token.": [
+ ""
+ ],
+ "Access Token": [
+ "Acceso denegado"
+ ],
+ "The request to the backend take too long and was cancelled": [
+ ""
+ ],
+ "Diagnostic from %1$s is \"%2$s\"": [
+ ""
+ ],
+ "The backend reported a problem: HTTP status #%1$s": [
+ "Servidir reporto un problema: HTTP status #%1$s"
+ ],
+ "Diagnostic from %1$s is '%2$s'": [
+ ""
+ ],
+ "Access denied": [
+ "Acceso denegado"
+ ],
+ "The access token provided is invalid.": [
+ ""
+ ],
+ "No 'default' instance configured yet.": [
+ "Sin instancia default"
+ ],
+ "Create a 'default' instance to begin using the merchant backoffice.": [
+ ""
+ ],
+ "The access token provided is invalid": [
+ ""
+ ],
+ "Hide for today": [
+ ""
+ ],
+ "Instance": [
+ "Instancia"
+ ],
+ "Settings": [
+ "Configuración"
+ ],
+ "Connection": [
+ "Conexión"
+ ],
+ "New": [
+ "Nuevo"
+ ],
+ "List": [
+ "Lista"
+ ],
+ "Log out": [
+ "Salir"
+ ],
+ "Check your token is valid": [
+ "Verifica que el token sea valido"
+ ],
+ "Couldn't access the server.": [
+ "No se pudo acceder al servidor."
+ ],
+ "Could not infer instance id from url %1$s": [
+ "No se pudo inferir el id de la instancia con la url %1$s"
+ ],
+ "Server not found": [
+ "Servidor no encontrado"
+ ],
+ "Server response with an error code": [
+ ""
+ ],
+ "Got message %1$s from %2$s": [
+ "Recibimos el mensaje %1$s desde %2$s"
+ ],
+ "Response from server is unreadable, http status: %1$s": [
+ ""
+ ],
+ "Unexpected Error": [
+ "Error inesperado"
+ ],
+ "The value %1$s is invalid for a payment url": [
+ "El valor %1$s es invalido para una URL de pago"
+ ],
+ "add element to the list": [
+ "agregar elemento a la lista"
+ ],
+ "add": [
+ "Agregar"
+ ],
+ "Deleting": [
+ "Borrando"
+ ],
+ "Changing": [
+ "Cambiando"
+ ],
+ "Order ID": [
+ "ID de pedido"
+ ],
+ "Payment URL": [
+ "URL de pago"
]
}
}
@@ -1739,58 +4844,172 @@ strings['fr'] = {
"plural_forms": "nplurals=2; plural=(n != 1);",
"lang": ""
},
- "Access denied": [
+ "Cancel": [
""
],
- "Check your token is valid": [
+ "%1$s": [
""
],
- "Couldn't access the server.": [
+ "Close": [
""
],
- "Could not infer instance id from url %1$s": [
+ "Continue": [
""
],
- "HTTP status #%1$s: Server reported a problem": [
+ "Clear": [
""
],
- "Got message: \"%1$s\" from: %2$s": [
+ "Confirm": [
""
],
- "No default instance": [
+ "is not the same as the current access token": [
""
],
- "in order to use merchant backoffice, you should create the default instance": [
+ "cannot be empty": [
""
],
- "Server reported a problem: HTTP status #%1$s": [
+ "cannot be the same as the old token": [
""
],
- "Got message: %1$s from: %2$s": [
+ "is not the same": [
""
],
- "Login required": [
+ "You are updating the access token from instance with id %1$s": [
""
],
- "Please enter your auth token. Token should have \"secret-token:\" and start with Bearer or ApiKey": [
+ "Old access token": [
""
],
- "Confirm": [
+ "access token currently in use": [
""
],
- "The value %1$s is invalid for a payment url": [
+ "New access token": [
+ ""
+ ],
+ "next access token to be used": [
+ ""
+ ],
+ "Repeat access token": [
+ ""
+ ],
+ "confirm the same access token": [
+ ""
+ ],
+ "Clearing the access token will mean public access to the instance": [
+ ""
+ ],
+ "cannot be the same as the old access token": [
""
],
- "pick a date": [
+ "You are setting the access token for the new instance": [
+ ""
+ ],
+ "With external authorization method no check will be done by the merchant backend": [
+ ""
+ ],
+ "Set external authorization": [
+ ""
+ ],
+ "Set access token": [
+ ""
+ ],
+ "Operation in progress...": [
+ ""
+ ],
+ "The operation will be automatically canceled after %1$s seconds": [
+ ""
+ ],
+ "Instances": [
+ ""
+ ],
+ "Delete": [
+ ""
+ ],
+ "add new instance": [
+ ""
+ ],
+ "ID": [
+ ""
+ ],
+ "Name": [
+ ""
+ ],
+ "Edit": [
+ ""
+ ],
+ "Purge": [
+ ""
+ ],
+ "There is no instances yet, add more pressing the + sign": [
+ ""
+ ],
+ "Only show active instances": [
+ ""
+ ],
+ "Active": [
+ ""
+ ],
+ "Only show deleted instances": [
+ ""
+ ],
+ "Deleted": [
+ ""
+ ],
+ "Show all instances": [
+ ""
+ ],
+ "All": [
+ ""
+ ],
+ "Instance \"%1$s\" (ID: %2$s) has been deleted": [
+ ""
+ ],
+ "Failed to delete instance": [
+ ""
+ ],
+ "Instance '%1$s' (ID: %2$s) has been disabled": [
+ ""
+ ],
+ "Failed to purge instance": [
+ ""
+ ],
+ "Pending KYC verification": [
+ ""
+ ],
+ "Timed out": [
+ ""
+ ],
+ "Exchange": [
+ ""
+ ],
+ "Target account": [
+ ""
+ ],
+ "KYC URL": [
+ ""
+ ],
+ "Code": [
+ ""
+ ],
+ "Http Status": [
+ ""
+ ],
+ "No pending kyc verification!": [
+ ""
+ ],
+ "change value to unknown date": [
+ ""
+ ],
+ "change value to empty": [
""
],
"clear": [
""
],
- "never": [
+ "change value to never": [
""
],
- "Image should be smaller than 1 MB": [
+ "never": [
""
],
"Country": [
@@ -1829,277 +5048,427 @@ strings['fr'] = {
"Description": [
""
],
- "Name": [
+ "Product": [
""
],
- "loading...": [
+ "search products by it's description or id": [
""
],
- "no products found": [
+ "no products found with that description": [
""
],
- "no results": [
+ "You must enter a valid product identifier.": [
""
],
- "Deleting": [
+ "Quantity must be greater than 0!": [
""
],
- "Changing": [
+ "This quantity exceeds remaining stock. Currently, only %1$s units remain unreserved in stock.": [
""
],
- "Manage token": [
+ "Quantity": [
""
],
- "Update": [
+ "how many products will be added": [
+ ""
+ ],
+ "Add from inventory": [
+ ""
+ ],
+ "Image should be smaller than 1 MB": [
+ ""
+ ],
+ "Add": [
""
],
"Remove": [
""
],
- "Cancel": [
+ "No taxes configured for this product.": [
""
],
- "Manage stock": [
+ "Amount": [
""
],
- "Infinite": [
+ "Taxes can be in currencies that differ from the main currency used by the merchant.": [
""
],
- "lost cannot be greater that current + incoming (max %1$s)": [
+ "Enter currency and value separated with a colon, e.g. &quot;USD:2.3&quot;.": [
""
],
- "current stock will change from %1$s to %2$s": [
+ "Legal name of the tax, e.g. VAT or import duties.": [
""
],
- "current stock will stay at %1$s": [
+ "add tax to the tax list": [
""
],
- "Incoming": [
+ "describe and add a product that is not in the inventory list": [
""
],
- "Lost": [
+ "Add custom product": [
""
],
- "Current": [
+ "Complete information of the product": [
""
],
- "without stock": [
+ "Image": [
""
],
- "Next restock": [
+ "photo of the product": [
""
],
- "Delivery address": [
+ "full product description": [
""
],
- "this product has no taxes": [
+ "Unit": [
""
],
- "Amount": [
+ "name of the product unit": [
""
],
- "currency and value separated with colon": [
+ "Price": [
""
],
- "Add": [
+ "amount in the current currency": [
""
],
- "Instance": [
+ "Taxes": [
""
],
- "Settings": [
+ "image": [
""
],
- "Orders": [
+ "description": [
""
],
- "Products": [
+ "quantity": [
""
],
- "Transfers": [
+ "unit price": [
""
],
- "Connection": [
+ "total price": [
""
],
- "Instances": [
+ "required": [
""
],
- "New": [
+ "not valid": [
""
],
- "List": [
+ "must be greater than 0": [
""
],
- "Log out": [
+ "not a valid json": [
""
],
- "Clear": [
+ "should be in the future": [
""
],
- "should be the same": [
+ "refund deadline cannot be before pay deadline": [
""
],
- "cannot be the same as before": [
+ "wire transfer deadline cannot be before refund deadline": [
""
],
- "You are updating the authorization token from instance %1$s with id %2$s": [
+ "wire transfer deadline cannot be before pay deadline": [
""
],
- "Old token": [
+ "should have a refund deadline": [
""
],
- "New token": [
+ "auto refund cannot be after refund deadline": [
""
],
- "Clearing the auth token will mean public access to the instance": [
+ "Manage products in order": [
""
],
- "ID": [
+ "Manage list of products in the order.": [
""
],
- "Image": [
+ "Remove this product from the order.": [
""
],
- "Unit": [
+ "Total price": [
""
],
- "Price": [
+ "total product price added up": [
""
],
- "Stock": [
+ "Amount to be paid by the customer": [
""
],
- "Taxes": [
+ "Order price": [
""
],
- "Server not found": [
+ "final order price": [
""
],
- "Couldn't access the server": [
+ "Summary": [
""
],
- "Got message %1$s from %2$s": [
+ "Title of the order to be shown to the customer": [
""
],
- "Unexpected Error": [
+ "Shipping and Fulfillment": [
""
],
- "Auth token": [
+ "Delivery date": [
""
],
- "Account address": [
+ "Deadline for physical delivery assured by the merchant.": [
""
],
- "Default max deposit fee": [
+ "Location": [
""
],
- "Default max wire fee": [
+ "address where the products will be delivered": [
""
],
- "Default wire fee amortization": [
+ "Fulfillment URL": [
""
],
- "Jurisdiction": [
+ "URL to which the user will be redirected after successful payment.": [
""
],
- "Default pay delay": [
+ "Taler payment options": [
""
],
- "Default wire transfer delay": [
+ "Override default Taler payment settings for this order": [
""
],
- "could not create instance": [
+ "Payment deadline": [
""
],
- "Delete": [
+ "Deadline for the customer to pay for the offer before it expires. Inventory products will be reserved until this deadline.": [
""
],
- "Edit": [
+ "Refund deadline": [
""
],
- "There is no instances yet, add more pressing the + sign": [
+ "Time until which the order can be refunded by the merchant.": [
""
],
- "Inventory products": [
+ "Wire transfer deadline": [
""
],
- "Total price": [
+ "Deadline for the exchange to make the wire transfer.": [
""
],
- "Total tax": [
+ "Auto-refund deadline": [
""
],
- "Order price": [
+ "Time until which the wallet will automatically check for refunds without user interaction.": [
""
],
- "Net": [
+ "Maximum deposit fee": [
""
],
- "Summary": [
+ "Maximum deposit fees the merchant is willing to cover for this order. Higher deposit fees must be covered in full by the consumer.": [
""
],
- "Payments options": [
+ "Maximum wire fee": [
""
],
- "Auto refund deadline": [
+ "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.": [
""
],
- "Refund deadline": [
+ "Wire fee amortization": [
""
],
- "Pay deadline": [
+ "Factor by which wire fees exceeding the above threshold are divided to determine the share of excess wire fees to be paid explicitly by the consumer.": [
""
],
- "Delivery date": [
+ "Create token": [
""
],
- "Location": [
+ "Uncheck this option if the merchant backend generated an order ID with enough entropy to prevent adversarial claims.": [
+ ""
+ ],
+ "Minimum age required": [
+ ""
+ ],
+ "Any value greater than 0 will limit the coins able be used to pay this contract. If empty the age restriction will be defined by the products": [
+ ""
+ ],
+ "Min age defined by the producs is %1$s": [
+ ""
+ ],
+ "Additional information": [
+ ""
+ ],
+ "Custom information to be included in the contract for this order.": [
+ ""
+ ],
+ "You must enter a value in JavaScript Object Notation (JSON).": [
+ ""
+ ],
+ "days": [
+ ""
+ ],
+ "hours": [
+ ""
+ ],
+ "minutes": [
+ ""
+ ],
+ "seconds": [
+ ""
+ ],
+ "forever": [
+ ""
+ ],
+ "%1$sM": [
+ ""
+ ],
+ "%1$sY": [
+ ""
+ ],
+ "%1$sd": [
+ ""
+ ],
+ "%1$sh": [
+ ""
+ ],
+ "%1$smin": [
+ ""
+ ],
+ "%1$ssec": [
+ ""
+ ],
+ "Orders": [
+ ""
+ ],
+ "create order": [
+ ""
+ ],
+ "load newer orders": [
+ ""
+ ],
+ "Date": [
+ ""
+ ],
+ "Refund": [
+ ""
+ ],
+ "copy url": [
+ ""
+ ],
+ "load older orders": [
+ ""
+ ],
+ "No orders have been found matching your query!": [
+ ""
+ ],
+ "duplicated": [
+ ""
+ ],
+ "invalid format": [
+ ""
+ ],
+ "this value exceed the refundable amount": [
+ ""
+ ],
+ "date": [
+ ""
+ ],
+ "amount": [
+ ""
+ ],
+ "reason": [
+ ""
+ ],
+ "amount to be refunded": [
+ ""
+ ],
+ "Max refundable:": [
+ ""
+ ],
+ "Reason": [
+ ""
+ ],
+ "Choose one...": [
+ ""
+ ],
+ "requested by the customer": [
+ ""
+ ],
+ "other": [
+ ""
+ ],
+ "why this order is being refunded": [
+ ""
+ ],
+ "more information to give context": [
+ ""
+ ],
+ "Contract Terms": [
+ ""
+ ],
+ "human-readable description of the whole purchase": [
+ ""
+ ],
+ "total price for the transaction": [
+ ""
+ ],
+ "URL for this purchase": [
""
],
"Max fee": [
""
],
+ "maximum total deposit fee accepted by the merchant for this contract": [
+ ""
+ ],
"Max wire fee": [
""
],
- "Wire fee amortization": [
+ "maximum wire fee accepted by the merchant": [
""
],
- "Fullfilment url": [
+ "over how many customer transactions does the merchant expect to amortize wire fees on average": [
""
],
- "Extra information": [
+ "Created at": [
""
],
- "select a product first": [
+ "time when this contract was generated": [
""
],
- "should be greater than 0": [
+ "after this deadline has passed no refunds will be accepted": [
""
],
- "cannot be greater than current stock and quantity previously added. max: %1$s": [
+ "after this deadline, the merchant won't accept payments for the contract": [
""
],
- "cannot be greater than current stock %1$s": [
+ "transfer deadline for the exchange": [
""
],
- "Quantity": [
+ "time indicating when the order should be delivered": [
""
],
- "Order": [
+ "where the order will be delivered": [
""
],
- "claimed": [
+ "Auto-refund delay": [
""
],
- "copy url": [
+ "how long the wallet should try to get an automatic refund for the purchase": [
""
],
- "pay at": [
+ "Extra info": [
""
],
- "created at": [
+ "extra data that is only interpreted by the merchant frontend": [
+ ""
+ ],
+ "Order": [
+ ""
+ ],
+ "claimed": [
+ ""
+ ],
+ "claimed at": [
""
],
"Timeline": [
@@ -2123,90 +5492,180 @@ strings['fr'] = {
"refunded": [
""
],
+ "refund order": [
+ ""
+ ],
+ "not refundable": [
+ ""
+ ],
"refund": [
""
],
"Refunded amount": [
""
],
- "Deposit total": [
+ "Refund taken": [
+ ""
+ ],
+ "Status URL": [
+ ""
+ ],
+ "Refund URI": [
""
],
"unpaid": [
""
],
+ "pay at": [
+ ""
+ ],
+ "created at": [
+ ""
+ ],
"Order status URL": [
""
],
- "Pay URI": [
+ "Payment URI": [
""
],
"Unknown order status. This is an error, please contact the administrator.": [
""
],
+ "Back": [
+ ""
+ ],
"refund created successfully": [
""
],
"could not create the refund": [
""
],
- "load newer orders": [
+ "select date to show nearby orders": [
""
],
- "Date": [
+ "order id": [
""
],
- "Refund": [
+ "jump to order with the given order ID": [
""
],
- "load older orders": [
+ "remove all filters": [
""
],
- "No orders has been found": [
+ "only show paid orders": [
""
],
- "date": [
+ "Paid": [
""
],
- "amount": [
+ "only show orders with refunds": [
""
],
- "reason": [
+ "Refunded": [
""
],
- "Max refundable:": [
+ "only show orders where customers paid, but wire payments from payment provider are still pending": [
""
],
- "Reason": [
+ "Not wired": [
""
],
- "duplicated": [
+ "clear date filter": [
""
],
- "requested by the customer": [
+ "date (YYYY/MM/DD)": [
""
],
- "other": [
+ "Enter an order id": [
""
],
- "go to order id": [
+ "order not found": [
""
],
- "Paid": [
+ "could not get the order to refund": [
""
],
- "Refunded": [
+ "Loading...": [
""
],
- "Not wired": [
+ "click here to configure the stock of the product, leave it as is and the backend will not control stock": [
""
],
- "All": [
+ "Manage stock": [
+ ""
+ ],
+ "this product has been configured without stock control": [
+ ""
+ ],
+ "Infinite": [
+ ""
+ ],
+ "lost cannot be greater than current and incoming (max %1$s)": [
+ ""
+ ],
+ "Incoming": [
+ ""
+ ],
+ "Lost": [
+ ""
+ ],
+ "Current": [
+ ""
+ ],
+ "remove stock control for this product": [
+ ""
+ ],
+ "without stock": [
+ ""
+ ],
+ "Next restock": [
+ ""
+ ],
+ "Delivery address": [
+ ""
+ ],
+ "product identification to use in URLs (for internal use only)": [
+ ""
+ ],
+ "illustration of the product for customers": [
+ ""
+ ],
+ "product description for customers": [
+ ""
+ ],
+ "Age restricted": [
+ ""
+ ],
+ "is this product restricted for customer below certain age?": [
+ ""
+ ],
+ "unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 items, 5 meters) for customers": [
+ ""
+ ],
+ "sale price for customers, including taxes, for above units of the product": [
+ ""
+ ],
+ "Stock": [
+ ""
+ ],
+ "product inventory for products with finite supply (for internal use only)": [
+ ""
+ ],
+ "taxes included in the product price, exposed to customers": [
+ ""
+ ],
+ "Need to complete marked fields": [
""
],
"could not create product": [
""
],
+ "Products": [
+ ""
+ ],
+ "add product to inventory": [
+ ""
+ ],
"Sell": [
""
],
@@ -2216,6 +5675,42 @@ strings['fr'] = {
"Sold": [
""
],
+ "free": [
+ ""
+ ],
+ "go to product update page": [
+ ""
+ ],
+ "Update": [
+ ""
+ ],
+ "remove this product from the database": [
+ ""
+ ],
+ "update the product with new price": [
+ ""
+ ],
+ "update product with new price": [
+ ""
+ ],
+ "add more elements to the inventory": [
+ ""
+ ],
+ "report elements lost in the inventory": [
+ ""
+ ],
+ "new price for the product": [
+ ""
+ ],
+ "the are value with errors": [
+ ""
+ ],
+ "update product with new stock and price": [
+ ""
+ ],
+ "There is no products yet, add more pressing the + sign": [
+ ""
+ ],
"product updated successfully": [
""
],
@@ -2228,25 +5723,355 @@ strings['fr'] = {
"could not delete the product": [
""
],
+ "Product id:": [
+ ""
+ ],
+ "To complete the setup of the reserve, you must now initiate a wire transfer using the given wire transfer subject and crediting the specified amount to the indicated account of the exchange.": [
+ ""
+ ],
+ "If your system supports RFC 8905, you can do this by opening this URI:": [
+ ""
+ ],
+ "it should be greater than 0": [
+ ""
+ ],
+ "must be a valid URL": [
+ ""
+ ],
+ "Initial balance": [
+ ""
+ ],
+ "balance prior to deposit": [
+ ""
+ ],
+ "Exchange URL": [
+ ""
+ ],
+ "URL of exchange": [
+ ""
+ ],
+ "Next": [
+ ""
+ ],
+ "Wire method": [
+ ""
+ ],
+ "method to use for wire transfer": [
+ ""
+ ],
+ "Select one wire method": [
+ ""
+ ],
+ "could not create reserve": [
+ ""
+ ],
+ "Valid until": [
+ ""
+ ],
+ "Created balance": [
+ ""
+ ],
+ "Exchange balance": [
+ ""
+ ],
+ "Picked up": [
+ ""
+ ],
+ "Committed": [
+ ""
+ ],
+ "Account address": [
+ ""
+ ],
+ "Subject": [
+ ""
+ ],
"Tips": [
""
],
- "Committed amount": [
+ "No tips has been authorized from this reserve": [
""
],
- "Exchange initial amount": [
+ "Authorized": [
""
],
- "Merchant initial amount": [
+ "Expiration": [
""
],
- "There is no tips yet, add more pressing the + sign": [
+ "amount of tip": [
""
],
- "cannot be empty": [
+ "Justification": [
+ ""
+ ],
+ "reason for the tip": [
+ ""
+ ],
+ "URL after tip": [
+ ""
+ ],
+ "URL to visit after tip payment": [
+ ""
+ ],
+ "Reserves not yet funded": [
+ ""
+ ],
+ "Reserves ready": [
+ ""
+ ],
+ "add new reserve": [
+ ""
+ ],
+ "Expires at": [
+ ""
+ ],
+ "Initial": [
+ ""
+ ],
+ "delete selected reserve from the database": [
+ ""
+ ],
+ "authorize new tip from selected reserve": [
+ ""
+ ],
+ "There is no ready reserves yet, add more pressing the + sign or fund them": [
+ ""
+ ],
+ "Expected Balance": [
+ ""
+ ],
+ "could not create the tip": [
+ ""
+ ],
+ "should not be empty": [
+ ""
+ ],
+ "should be greater that 0": [
+ ""
+ ],
+ "can't be empty": [
+ ""
+ ],
+ "to short": [
+ ""
+ ],
+ "just letters and numbers from 2 to 7": [
+ ""
+ ],
+ "size of the key should be 32": [
+ ""
+ ],
+ "Identifier": [
+ ""
+ ],
+ "Name of the template in URLs.": [
+ ""
+ ],
+ "Describe what this template stands for": [
+ ""
+ ],
+ "Fixed summary": [
+ ""
+ ],
+ "If specified, this template will create order with the same summary": [
+ ""
+ ],
+ "Fixed price": [
+ ""
+ ],
+ "If specified, this template will create order with the same price": [
+ ""
+ ],
+ "Minimum age": [
+ ""
+ ],
+ "Is this contract restricted to some age?": [
+ ""
+ ],
+ "Payment timeout": [
+ ""
+ ],
+ "How much time has the customer to complete the payment once the order was created.": [
+ ""
+ ],
+ "Verification algorithm": [
+ ""
+ ],
+ "Algorithm to use to verify transaction in offline mode": [
+ ""
+ ],
+ "Point-of-sale key": [
+ ""
+ ],
+ "Useful to validate the purchase": [
+ ""
+ ],
+ "generate random secret key": [
+ ""
+ ],
+ "random": [
+ ""
+ ],
+ "show secret key": [
+ ""
+ ],
+ "hide secret key": [
+ ""
+ ],
+ "hide": [
+ ""
+ ],
+ "show": [
+ ""
+ ],
+ "could not inform template": [
+ ""
+ ],
+ "Amount is required": [
+ ""
+ ],
+ "Order summary is required": [
+ ""
+ ],
+ "New order for template": [
+ ""
+ ],
+ "Amount of the order": [
+ ""
+ ],
+ "Order summary": [
+ ""
+ ],
+ "could not create order from template": [
+ ""
+ ],
+ "Here you can specify a default value for fields that are not fixed. Default values can be edited by the customer before the payment.": [
+ ""
+ ],
+ "Fixed amount": [
+ ""
+ ],
+ "Default amount": [
+ ""
+ ],
+ "Default summary": [
""
],
- "check the id, doest look valid": [
+ "Print": [
+ ""
+ ],
+ "Setup TOTP": [
+ ""
+ ],
+ "Templates": [
+ ""
+ ],
+ "add new templates": [
+ ""
+ ],
+ "load more templates before the first one": [
+ ""
+ ],
+ "load newer templates": [
+ ""
+ ],
+ "delete selected templates from the database": [
+ ""
+ ],
+ "use template to create new order": [
+ ""
+ ],
+ "create qr code for the template": [
+ ""
+ ],
+ "load more templates after the last one": [
+ ""
+ ],
+ "load older templates": [
+ ""
+ ],
+ "There is no templates yet, add more pressing the + sign": [
+ ""
+ ],
+ "template delete successfully": [
+ ""
+ ],
+ "could not delete the template": [
+ ""
+ ],
+ "could not update template": [
+ ""
+ ],
+ "should be one of '%1$s'": [
+ ""
+ ],
+ "Webhook ID to use": [
+ ""
+ ],
+ "Event": [
+ ""
+ ],
+ "The event of the webhook: why the webhook is used": [
+ ""
+ ],
+ "Method": [
+ ""
+ ],
+ "Method used by the webhook": [
+ ""
+ ],
+ "URL": [
+ ""
+ ],
+ "URL of the webhook where the customer will be redirected": [
+ ""
+ ],
+ "Header": [
+ ""
+ ],
+ "Header template of the webhook": [
+ ""
+ ],
+ "Body": [
+ ""
+ ],
+ "Body template by the webhook": [
+ ""
+ ],
+ "Webhooks": [
+ ""
+ ],
+ "add new webhooks": [
+ ""
+ ],
+ "load more webhooks before the first one": [
+ ""
+ ],
+ "load newer webhooks": [
+ ""
+ ],
+ "Event type": [
+ ""
+ ],
+ "delete selected webhook from the database": [
+ ""
+ ],
+ "load more webhooks after the last one": [
+ ""
+ ],
+ "load older webhooks": [
+ ""
+ ],
+ "There is no webhooks yet, add more pressing the + sign": [
+ ""
+ ],
+ "webhook delete successfully": [
+ ""
+ ],
+ "could not delete the webhook": [
+ ""
+ ],
+ "check the id, does not look valid": [
""
],
"should have 52 characters, current %1$s": [
@@ -2255,18 +6080,42 @@ strings['fr'] = {
"URL doesn't have the right format": [
""
],
- "Transfer ID": [
+ "Credited bank account": [
""
],
- "Account Address": [
+ "Select one account": [
""
],
- "Exchange URL": [
+ "Bank account of the merchant where the payment was received": [
+ ""
+ ],
+ "Wire transfer ID": [
+ ""
+ ],
+ "unique identifier of the wire transfer used by the exchange, must be 52 characters long": [
+ ""
+ ],
+ "Base URL of the exchange that made the transfer, should have been in the wire transfer subject": [
+ ""
+ ],
+ "Amount credited": [
+ ""
+ ],
+ "Actual amount that was wired to the merchant's bank account": [
""
],
"could not inform transfer": [
""
],
+ "Transfers": [
+ ""
+ ],
+ "add new transfer": [
+ ""
+ ],
+ "load more transfers before the first one": [
+ ""
+ ],
"load newer transfers": [
""
],
@@ -2291,11 +6140,302 @@ strings['fr'] = {
"unknown": [
""
],
+ "delete selected transfer from the database": [
+ ""
+ ],
+ "load more transfer after the last one": [
+ ""
+ ],
"load older transfers": [
""
],
"There is no transfer yet, add more pressing the + sign": [
""
+ ],
+ "filter by account address": [
+ ""
+ ],
+ "only show wire transfers confirmed by the merchant": [
+ ""
+ ],
+ "only show wire transfers claimed by the exchange": [
+ ""
+ ],
+ "Unverified": [
+ ""
+ ],
+ "is not valid": [
+ ""
+ ],
+ "is not a number": [
+ ""
+ ],
+ "must be 1 or greater": [
+ ""
+ ],
+ "max 7 lines": [
+ ""
+ ],
+ "change authorization configuration": [
+ ""
+ ],
+ "Need to complete marked fields and choose authorization method": [
+ ""
+ ],
+ "This is not a valid bitcoin address.": [
+ ""
+ ],
+ "This is not a valid Ethereum address.": [
+ ""
+ ],
+ "IBAN numbers usually have more that 4 digits": [
+ ""
+ ],
+ "IBAN numbers usually have less that 34 digits": [
+ ""
+ ],
+ "IBAN country code not found": [
+ ""
+ ],
+ "IBAN number is not valid, checksum is wrong": [
+ ""
+ ],
+ "Target type": [
+ ""
+ ],
+ "Method to use for wire transfer": [
+ ""
+ ],
+ "Routing": [
+ ""
+ ],
+ "Routing number.": [
+ ""
+ ],
+ "Account": [
+ ""
+ ],
+ "Account number.": [
+ ""
+ ],
+ "Business Identifier Code.": [
+ ""
+ ],
+ "Bank Account Number.": [
+ ""
+ ],
+ "Unified Payment Interface.": [
+ ""
+ ],
+ "Bitcoin protocol.": [
+ ""
+ ],
+ "Ethereum protocol.": [
+ ""
+ ],
+ "Interledger protocol.": [
+ ""
+ ],
+ "Host": [
+ ""
+ ],
+ "Bank host.": [
+ ""
+ ],
+ "Bank account.": [
+ ""
+ ],
+ "Bank account owner's name.": [
+ ""
+ ],
+ "No accounts yet.": [
+ ""
+ ],
+ "Name of the instance in URLs. The 'default' instance is special in that it is used to administer other instances.": [
+ ""
+ ],
+ "Business name": [
+ ""
+ ],
+ "Legal name of the business represented by this instance.": [
+ ""
+ ],
+ "Email": [
+ ""
+ ],
+ "Contact email": [
+ ""
+ ],
+ "Website URL": [
+ ""
+ ],
+ "URL.": [
+ ""
+ ],
+ "Logo": [
+ ""
+ ],
+ "Logo image.": [
+ ""
+ ],
+ "Bank account": [
+ ""
+ ],
+ "URI specifying bank account for crediting revenue.": [
+ ""
+ ],
+ "Default max deposit fee": [
+ ""
+ ],
+ "Maximum deposit fees this merchant is willing to pay per order by default.": [
+ ""
+ ],
+ "Default max wire fee": [
+ ""
+ ],
+ "Maximum wire fees this merchant is willing to pay per wire transfer by default.": [
+ ""
+ ],
+ "Default wire fee amortization": [
+ ""
+ ],
+ "Number of orders excess wire transfer fees will be divided by to compute per order surcharge.": [
+ ""
+ ],
+ "Physical location of the merchant.": [
+ ""
+ ],
+ "Jurisdiction": [
+ ""
+ ],
+ "Jurisdiction for legal disputes with the merchant.": [
+ ""
+ ],
+ "Default payment delay": [
+ ""
+ ],
+ "Time customers have to pay an order before the offer expires by default.": [
+ ""
+ ],
+ "Default wire transfer delay": [
+ ""
+ ],
+ "Maximum time an exchange is allowed to delay wiring funds to the merchant, enabling it to aggregate smaller payments into larger wire transfers and reducing wire fees.": [
+ ""
+ ],
+ "Instance id": [
+ ""
+ ],
+ "Change the authorization method use for this instance.": [
+ ""
+ ],
+ "Manage access token": [
+ ""
+ ],
+ "Failed to create instance": [
+ ""
+ ],
+ "Login required": [
+ ""
+ ],
+ "Please enter your access token.": [
+ ""
+ ],
+ "Access Token": [
+ ""
+ ],
+ "The request to the backend take too long and was cancelled": [
+ ""
+ ],
+ "Diagnostic from %1$s is \"%2$s\"": [
+ ""
+ ],
+ "The backend reported a problem: HTTP status #%1$s": [
+ ""
+ ],
+ "Diagnostic from %1$s is '%2$s'": [
+ ""
+ ],
+ "Access denied": [
+ ""
+ ],
+ "The access token provided is invalid.": [
+ ""
+ ],
+ "No 'default' instance configured yet.": [
+ ""
+ ],
+ "Create a 'default' instance to begin using the merchant backoffice.": [
+ ""
+ ],
+ "The access token provided is invalid": [
+ ""
+ ],
+ "Hide for today": [
+ ""
+ ],
+ "Instance": [
+ ""
+ ],
+ "Settings": [
+ ""
+ ],
+ "Connection": [
+ ""
+ ],
+ "New": [
+ ""
+ ],
+ "List": [
+ ""
+ ],
+ "Log out": [
+ ""
+ ],
+ "Check your token is valid": [
+ ""
+ ],
+ "Couldn't access the server.": [
+ ""
+ ],
+ "Could not infer instance id from url %1$s": [
+ ""
+ ],
+ "Server not found": [
+ ""
+ ],
+ "Server response with an error code": [
+ ""
+ ],
+ "Got message %1$s from %2$s": [
+ ""
+ ],
+ "Response from server is unreadable, http status: %1$s": [
+ ""
+ ],
+ "Unexpected Error": [
+ ""
+ ],
+ "The value %1$s is invalid for a payment url": [
+ ""
+ ],
+ "add element to the list": [
+ ""
+ ],
+ "add": [
+ ""
+ ],
+ "Deleting": [
+ ""
+ ],
+ "Changing": [
+ ""
+ ],
+ "Order ID": [
+ ""
+ ],
+ "Payment URL": [
+ ""
]
}
}
@@ -2310,58 +6450,172 @@ strings['it'] = {
"plural_forms": "nplurals=2; plural=(n != 1);",
"lang": ""
},
- "Access denied": [
+ "Cancel": [
""
],
- "Check your token is valid": [
+ "%1$s": [
""
],
- "Couldn't access the server.": [
+ "Close": [
""
],
- "Could not infer instance id from url %1$s": [
+ "Continue": [
""
],
- "HTTP status #%1$s: Server reported a problem": [
+ "Clear": [
""
],
- "Got message: \"%1$s\" from: %2$s": [
+ "Confirm": [
""
],
- "No default instance": [
+ "is not the same as the current access token": [
""
],
- "in order to use merchant backoffice, you should create the default instance": [
+ "cannot be empty": [
""
],
- "Server reported a problem: HTTP status #%1$s": [
+ "cannot be the same as the old token": [
""
],
- "Got message: %1$s from: %2$s": [
+ "is not the same": [
""
],
- "Login required": [
+ "You are updating the access token from instance with id %1$s": [
""
],
- "Please enter your auth token. Token should have \"secret-token:\" and start with Bearer or ApiKey": [
+ "Old access token": [
""
],
- "Confirm": [
+ "access token currently in use": [
""
],
- "The value %1$s is invalid for a payment url": [
+ "New access token": [
+ ""
+ ],
+ "next access token to be used": [
+ ""
+ ],
+ "Repeat access token": [
+ ""
+ ],
+ "confirm the same access token": [
+ ""
+ ],
+ "Clearing the access token will mean public access to the instance": [
+ ""
+ ],
+ "cannot be the same as the old access token": [
+ ""
+ ],
+ "You are setting the access token for the new instance": [
+ ""
+ ],
+ "With external authorization method no check will be done by the merchant backend": [
+ ""
+ ],
+ "Set external authorization": [
+ ""
+ ],
+ "Set access token": [
+ ""
+ ],
+ "Operation in progress...": [
+ ""
+ ],
+ "The operation will be automatically canceled after %1$s seconds": [
+ ""
+ ],
+ "Instances": [
+ ""
+ ],
+ "Delete": [
+ ""
+ ],
+ "add new instance": [
+ ""
+ ],
+ "ID": [
+ ""
+ ],
+ "Name": [
+ ""
+ ],
+ "Edit": [
+ ""
+ ],
+ "Purge": [
+ ""
+ ],
+ "There is no instances yet, add more pressing the + sign": [
""
],
- "pick a date": [
+ "Only show active instances": [
+ ""
+ ],
+ "Active": [
+ ""
+ ],
+ "Only show deleted instances": [
+ ""
+ ],
+ "Deleted": [
+ ""
+ ],
+ "Show all instances": [
+ ""
+ ],
+ "All": [
+ ""
+ ],
+ "Instance \"%1$s\" (ID: %2$s) has been deleted": [
+ ""
+ ],
+ "Failed to delete instance": [
+ ""
+ ],
+ "Instance '%1$s' (ID: %2$s) has been disabled": [
+ ""
+ ],
+ "Failed to purge instance": [
+ ""
+ ],
+ "Pending KYC verification": [
+ ""
+ ],
+ "Timed out": [
+ ""
+ ],
+ "Exchange": [
+ ""
+ ],
+ "Target account": [
+ ""
+ ],
+ "KYC URL": [
+ ""
+ ],
+ "Code": [
+ ""
+ ],
+ "Http Status": [
+ ""
+ ],
+ "No pending kyc verification!": [
+ ""
+ ],
+ "change value to unknown date": [
+ ""
+ ],
+ "change value to empty": [
""
],
"clear": [
""
],
- "never": [
+ "change value to never": [
""
],
- "Image should be smaller than 1 MB": [
+ "never": [
""
],
"Country": [
@@ -2400,277 +6654,427 @@ strings['it'] = {
"Description": [
""
],
- "Name": [
+ "Product": [
""
],
- "loading...": [
+ "search products by it's description or id": [
""
],
- "no products found": [
+ "no products found with that description": [
""
],
- "no results": [
+ "You must enter a valid product identifier.": [
""
],
- "Deleting": [
+ "Quantity must be greater than 0!": [
""
],
- "Changing": [
+ "This quantity exceeds remaining stock. Currently, only %1$s units remain unreserved in stock.": [
""
],
- "Manage token": [
+ "Quantity": [
""
],
- "Update": [
+ "how many products will be added": [
+ ""
+ ],
+ "Add from inventory": [
+ ""
+ ],
+ "Image should be smaller than 1 MB": [
+ ""
+ ],
+ "Add": [
""
],
"Remove": [
""
],
- "Cancel": [
+ "No taxes configured for this product.": [
""
],
- "Manage stock": [
+ "Amount": [
""
],
- "Infinite": [
+ "Taxes can be in currencies that differ from the main currency used by the merchant.": [
""
],
- "lost cannot be greater that current + incoming (max %1$s)": [
+ "Enter currency and value separated with a colon, e.g. &quot;USD:2.3&quot;.": [
""
],
- "current stock will change from %1$s to %2$s": [
+ "Legal name of the tax, e.g. VAT or import duties.": [
""
],
- "current stock will stay at %1$s": [
+ "add tax to the tax list": [
""
],
- "Incoming": [
+ "describe and add a product that is not in the inventory list": [
""
],
- "Lost": [
+ "Add custom product": [
""
],
- "Current": [
+ "Complete information of the product": [
""
],
- "without stock": [
+ "Image": [
""
],
- "Next restock": [
+ "photo of the product": [
""
],
- "Delivery address": [
+ "full product description": [
""
],
- "this product has no taxes": [
+ "Unit": [
""
],
- "Amount": [
+ "name of the product unit": [
""
],
- "currency and value separated with colon": [
+ "Price": [
""
],
- "Add": [
+ "amount in the current currency": [
""
],
- "Instance": [
+ "Taxes": [
""
],
- "Settings": [
+ "image": [
""
],
- "Orders": [
+ "description": [
""
],
- "Products": [
+ "quantity": [
""
],
- "Transfers": [
+ "unit price": [
""
],
- "Connection": [
+ "total price": [
""
],
- "Instances": [
+ "required": [
""
],
- "New": [
+ "not valid": [
""
],
- "List": [
+ "must be greater than 0": [
""
],
- "Log out": [
+ "not a valid json": [
""
],
- "Clear": [
+ "should be in the future": [
""
],
- "should be the same": [
+ "refund deadline cannot be before pay deadline": [
""
],
- "cannot be the same as before": [
+ "wire transfer deadline cannot be before refund deadline": [
""
],
- "You are updating the authorization token from instance %1$s with id %2$s": [
+ "wire transfer deadline cannot be before pay deadline": [
""
],
- "Old token": [
+ "should have a refund deadline": [
""
],
- "New token": [
+ "auto refund cannot be after refund deadline": [
""
],
- "Clearing the auth token will mean public access to the instance": [
+ "Manage products in order": [
""
],
- "ID": [
+ "Manage list of products in the order.": [
""
],
- "Image": [
+ "Remove this product from the order.": [
""
],
- "Unit": [
+ "Total price": [
""
],
- "Price": [
+ "total product price added up": [
""
],
- "Stock": [
+ "Amount to be paid by the customer": [
""
],
- "Taxes": [
+ "Order price": [
""
],
- "Server not found": [
+ "final order price": [
""
],
- "Couldn't access the server": [
+ "Summary": [
""
],
- "Got message %1$s from %2$s": [
+ "Title of the order to be shown to the customer": [
""
],
- "Unexpected Error": [
+ "Shipping and Fulfillment": [
""
],
- "Auth token": [
+ "Delivery date": [
""
],
- "Account address": [
+ "Deadline for physical delivery assured by the merchant.": [
""
],
- "Default max deposit fee": [
+ "Location": [
""
],
- "Default max wire fee": [
+ "address where the products will be delivered": [
""
],
- "Default wire fee amortization": [
+ "Fulfillment URL": [
""
],
- "Jurisdiction": [
+ "URL to which the user will be redirected after successful payment.": [
""
],
- "Default pay delay": [
+ "Taler payment options": [
""
],
- "Default wire transfer delay": [
+ "Override default Taler payment settings for this order": [
""
],
- "could not create instance": [
+ "Payment deadline": [
""
],
- "Delete": [
+ "Deadline for the customer to pay for the offer before it expires. Inventory products will be reserved until this deadline.": [
""
],
- "Edit": [
+ "Refund deadline": [
""
],
- "There is no instances yet, add more pressing the + sign": [
+ "Time until which the order can be refunded by the merchant.": [
""
],
- "Inventory products": [
+ "Wire transfer deadline": [
""
],
- "Total price": [
+ "Deadline for the exchange to make the wire transfer.": [
""
],
- "Total tax": [
+ "Auto-refund deadline": [
""
],
- "Order price": [
+ "Time until which the wallet will automatically check for refunds without user interaction.": [
""
],
- "Net": [
+ "Maximum deposit fee": [
""
],
- "Summary": [
+ "Maximum deposit fees the merchant is willing to cover for this order. Higher deposit fees must be covered in full by the consumer.": [
""
],
- "Payments options": [
+ "Maximum wire fee": [
""
],
- "Auto refund deadline": [
+ "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.": [
""
],
- "Refund deadline": [
+ "Wire fee amortization": [
""
],
- "Pay deadline": [
+ "Factor by which wire fees exceeding the above threshold are divided to determine the share of excess wire fees to be paid explicitly by the consumer.": [
""
],
- "Delivery date": [
+ "Create token": [
""
],
- "Location": [
+ "Uncheck this option if the merchant backend generated an order ID with enough entropy to prevent adversarial claims.": [
+ ""
+ ],
+ "Minimum age required": [
+ ""
+ ],
+ "Any value greater than 0 will limit the coins able be used to pay this contract. If empty the age restriction will be defined by the products": [
+ ""
+ ],
+ "Min age defined by the producs is %1$s": [
+ ""
+ ],
+ "Additional information": [
+ ""
+ ],
+ "Custom information to be included in the contract for this order.": [
+ ""
+ ],
+ "You must enter a value in JavaScript Object Notation (JSON).": [
+ ""
+ ],
+ "days": [
+ ""
+ ],
+ "hours": [
+ ""
+ ],
+ "minutes": [
+ ""
+ ],
+ "seconds": [
+ ""
+ ],
+ "forever": [
+ ""
+ ],
+ "%1$sM": [
+ ""
+ ],
+ "%1$sY": [
+ ""
+ ],
+ "%1$sd": [
+ ""
+ ],
+ "%1$sh": [
+ ""
+ ],
+ "%1$smin": [
+ ""
+ ],
+ "%1$ssec": [
+ ""
+ ],
+ "Orders": [
+ ""
+ ],
+ "create order": [
+ ""
+ ],
+ "load newer orders": [
+ ""
+ ],
+ "Date": [
+ ""
+ ],
+ "Refund": [
+ ""
+ ],
+ "copy url": [
+ ""
+ ],
+ "load older orders": [
+ ""
+ ],
+ "No orders have been found matching your query!": [
+ ""
+ ],
+ "duplicated": [
+ ""
+ ],
+ "invalid format": [
+ ""
+ ],
+ "this value exceed the refundable amount": [
+ ""
+ ],
+ "date": [
+ ""
+ ],
+ "amount": [
+ ""
+ ],
+ "reason": [
+ ""
+ ],
+ "amount to be refunded": [
+ ""
+ ],
+ "Max refundable:": [
+ ""
+ ],
+ "Reason": [
+ ""
+ ],
+ "Choose one...": [
+ ""
+ ],
+ "requested by the customer": [
+ ""
+ ],
+ "other": [
+ ""
+ ],
+ "why this order is being refunded": [
+ ""
+ ],
+ "more information to give context": [
+ ""
+ ],
+ "Contract Terms": [
+ ""
+ ],
+ "human-readable description of the whole purchase": [
+ ""
+ ],
+ "total price for the transaction": [
+ ""
+ ],
+ "URL for this purchase": [
""
],
"Max fee": [
""
],
+ "maximum total deposit fee accepted by the merchant for this contract": [
+ ""
+ ],
"Max wire fee": [
""
],
- "Wire fee amortization": [
+ "maximum wire fee accepted by the merchant": [
""
],
- "Fullfilment url": [
+ "over how many customer transactions does the merchant expect to amortize wire fees on average": [
""
],
- "Extra information": [
+ "Created at": [
""
],
- "select a product first": [
+ "time when this contract was generated": [
""
],
- "should be greater than 0": [
+ "after this deadline has passed no refunds will be accepted": [
""
],
- "cannot be greater than current stock and quantity previously added. max: %1$s": [
+ "after this deadline, the merchant won't accept payments for the contract": [
""
],
- "cannot be greater than current stock %1$s": [
+ "transfer deadline for the exchange": [
""
],
- "Quantity": [
+ "time indicating when the order should be delivered": [
""
],
- "Order": [
+ "where the order will be delivered": [
""
],
- "claimed": [
+ "Auto-refund delay": [
""
],
- "copy url": [
+ "how long the wallet should try to get an automatic refund for the purchase": [
""
],
- "pay at": [
+ "Extra info": [
""
],
- "created at": [
+ "extra data that is only interpreted by the merchant frontend": [
+ ""
+ ],
+ "Order": [
+ ""
+ ],
+ "claimed": [
+ ""
+ ],
+ "claimed at": [
""
],
"Timeline": [
@@ -2694,90 +7098,180 @@ strings['it'] = {
"refunded": [
""
],
+ "refund order": [
+ ""
+ ],
+ "not refundable": [
+ ""
+ ],
"refund": [
""
],
"Refunded amount": [
""
],
- "Deposit total": [
+ "Refund taken": [
+ ""
+ ],
+ "Status URL": [
+ ""
+ ],
+ "Refund URI": [
""
],
"unpaid": [
""
],
+ "pay at": [
+ ""
+ ],
+ "created at": [
+ ""
+ ],
"Order status URL": [
""
],
- "Pay URI": [
+ "Payment URI": [
""
],
"Unknown order status. This is an error, please contact the administrator.": [
""
],
+ "Back": [
+ ""
+ ],
"refund created successfully": [
""
],
"could not create the refund": [
""
],
- "load newer orders": [
+ "select date to show nearby orders": [
""
],
- "Date": [
+ "order id": [
""
],
- "Refund": [
+ "jump to order with the given order ID": [
""
],
- "load older orders": [
+ "remove all filters": [
""
],
- "No orders has been found": [
+ "only show paid orders": [
""
],
- "date": [
+ "Paid": [
""
],
- "amount": [
+ "only show orders with refunds": [
""
],
- "reason": [
+ "Refunded": [
""
],
- "Max refundable:": [
+ "only show orders where customers paid, but wire payments from payment provider are still pending": [
""
],
- "Reason": [
+ "Not wired": [
""
],
- "duplicated": [
+ "clear date filter": [
""
],
- "requested by the customer": [
+ "date (YYYY/MM/DD)": [
""
],
- "other": [
+ "Enter an order id": [
""
],
- "go to order id": [
+ "order not found": [
""
],
- "Paid": [
+ "could not get the order to refund": [
""
],
- "Refunded": [
+ "Loading...": [
""
],
- "Not wired": [
+ "click here to configure the stock of the product, leave it as is and the backend will not control stock": [
""
],
- "All": [
+ "Manage stock": [
+ ""
+ ],
+ "this product has been configured without stock control": [
+ ""
+ ],
+ "Infinite": [
+ ""
+ ],
+ "lost cannot be greater than current and incoming (max %1$s)": [
+ ""
+ ],
+ "Incoming": [
+ ""
+ ],
+ "Lost": [
+ ""
+ ],
+ "Current": [
+ ""
+ ],
+ "remove stock control for this product": [
+ ""
+ ],
+ "without stock": [
+ ""
+ ],
+ "Next restock": [
+ ""
+ ],
+ "Delivery address": [
+ ""
+ ],
+ "product identification to use in URLs (for internal use only)": [
+ ""
+ ],
+ "illustration of the product for customers": [
+ ""
+ ],
+ "product description for customers": [
+ ""
+ ],
+ "Age restricted": [
+ ""
+ ],
+ "is this product restricted for customer below certain age?": [
+ ""
+ ],
+ "unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 items, 5 meters) for customers": [
+ ""
+ ],
+ "sale price for customers, including taxes, for above units of the product": [
+ ""
+ ],
+ "Stock": [
+ ""
+ ],
+ "product inventory for products with finite supply (for internal use only)": [
+ ""
+ ],
+ "taxes included in the product price, exposed to customers": [
+ ""
+ ],
+ "Need to complete marked fields": [
""
],
"could not create product": [
""
],
+ "Products": [
+ ""
+ ],
+ "add product to inventory": [
+ ""
+ ],
"Sell": [
""
],
@@ -2787,6 +7281,42 @@ strings['it'] = {
"Sold": [
""
],
+ "free": [
+ ""
+ ],
+ "go to product update page": [
+ ""
+ ],
+ "Update": [
+ ""
+ ],
+ "remove this product from the database": [
+ ""
+ ],
+ "update the product with new price": [
+ ""
+ ],
+ "update product with new price": [
+ ""
+ ],
+ "add more elements to the inventory": [
+ ""
+ ],
+ "report elements lost in the inventory": [
+ ""
+ ],
+ "new price for the product": [
+ ""
+ ],
+ "the are value with errors": [
+ ""
+ ],
+ "update product with new stock and price": [
+ ""
+ ],
+ "There is no products yet, add more pressing the + sign": [
+ ""
+ ],
"product updated successfully": [
""
],
@@ -2799,25 +7329,355 @@ strings['it'] = {
"could not delete the product": [
""
],
+ "Product id:": [
+ ""
+ ],
+ "To complete the setup of the reserve, you must now initiate a wire transfer using the given wire transfer subject and crediting the specified amount to the indicated account of the exchange.": [
+ ""
+ ],
+ "If your system supports RFC 8905, you can do this by opening this URI:": [
+ ""
+ ],
+ "it should be greater than 0": [
+ ""
+ ],
+ "must be a valid URL": [
+ ""
+ ],
+ "Initial balance": [
+ ""
+ ],
+ "balance prior to deposit": [
+ ""
+ ],
+ "Exchange URL": [
+ ""
+ ],
+ "URL of exchange": [
+ ""
+ ],
+ "Next": [
+ ""
+ ],
+ "Wire method": [
+ ""
+ ],
+ "method to use for wire transfer": [
+ ""
+ ],
+ "Select one wire method": [
+ ""
+ ],
+ "could not create reserve": [
+ ""
+ ],
+ "Valid until": [
+ ""
+ ],
+ "Created balance": [
+ ""
+ ],
+ "Exchange balance": [
+ ""
+ ],
+ "Picked up": [
+ ""
+ ],
+ "Committed": [
+ ""
+ ],
+ "Account address": [
+ ""
+ ],
+ "Subject": [
+ ""
+ ],
"Tips": [
""
],
- "Committed amount": [
+ "No tips has been authorized from this reserve": [
""
],
- "Exchange initial amount": [
+ "Authorized": [
""
],
- "Merchant initial amount": [
+ "Expiration": [
""
],
- "There is no tips yet, add more pressing the + sign": [
+ "amount of tip": [
""
],
- "cannot be empty": [
+ "Justification": [
+ ""
+ ],
+ "reason for the tip": [
+ ""
+ ],
+ "URL after tip": [
+ ""
+ ],
+ "URL to visit after tip payment": [
+ ""
+ ],
+ "Reserves not yet funded": [
+ ""
+ ],
+ "Reserves ready": [
+ ""
+ ],
+ "add new reserve": [
+ ""
+ ],
+ "Expires at": [
+ ""
+ ],
+ "Initial": [
+ ""
+ ],
+ "delete selected reserve from the database": [
+ ""
+ ],
+ "authorize new tip from selected reserve": [
+ ""
+ ],
+ "There is no ready reserves yet, add more pressing the + sign or fund them": [
+ ""
+ ],
+ "Expected Balance": [
+ ""
+ ],
+ "could not create the tip": [
+ ""
+ ],
+ "should not be empty": [
+ ""
+ ],
+ "should be greater that 0": [
+ ""
+ ],
+ "can't be empty": [
+ ""
+ ],
+ "to short": [
+ ""
+ ],
+ "just letters and numbers from 2 to 7": [
+ ""
+ ],
+ "size of the key should be 32": [
+ ""
+ ],
+ "Identifier": [
+ ""
+ ],
+ "Name of the template in URLs.": [
+ ""
+ ],
+ "Describe what this template stands for": [
+ ""
+ ],
+ "Fixed summary": [
+ ""
+ ],
+ "If specified, this template will create order with the same summary": [
+ ""
+ ],
+ "Fixed price": [
+ ""
+ ],
+ "If specified, this template will create order with the same price": [
+ ""
+ ],
+ "Minimum age": [
+ ""
+ ],
+ "Is this contract restricted to some age?": [
+ ""
+ ],
+ "Payment timeout": [
+ ""
+ ],
+ "How much time has the customer to complete the payment once the order was created.": [
+ ""
+ ],
+ "Verification algorithm": [
+ ""
+ ],
+ "Algorithm to use to verify transaction in offline mode": [
+ ""
+ ],
+ "Point-of-sale key": [
+ ""
+ ],
+ "Useful to validate the purchase": [
+ ""
+ ],
+ "generate random secret key": [
+ ""
+ ],
+ "random": [
+ ""
+ ],
+ "show secret key": [
+ ""
+ ],
+ "hide secret key": [
+ ""
+ ],
+ "hide": [
+ ""
+ ],
+ "show": [
+ ""
+ ],
+ "could not inform template": [
+ ""
+ ],
+ "Amount is required": [
+ ""
+ ],
+ "Order summary is required": [
+ ""
+ ],
+ "New order for template": [
+ ""
+ ],
+ "Amount of the order": [
+ ""
+ ],
+ "Order summary": [
+ ""
+ ],
+ "could not create order from template": [
+ ""
+ ],
+ "Here you can specify a default value for fields that are not fixed. Default values can be edited by the customer before the payment.": [
+ ""
+ ],
+ "Fixed amount": [
+ ""
+ ],
+ "Default amount": [
+ ""
+ ],
+ "Default summary": [
+ ""
+ ],
+ "Print": [
+ ""
+ ],
+ "Setup TOTP": [
+ ""
+ ],
+ "Templates": [
+ ""
+ ],
+ "add new templates": [
+ ""
+ ],
+ "load more templates before the first one": [
+ ""
+ ],
+ "load newer templates": [
+ ""
+ ],
+ "delete selected templates from the database": [
+ ""
+ ],
+ "use template to create new order": [
+ ""
+ ],
+ "create qr code for the template": [
+ ""
+ ],
+ "load more templates after the last one": [
+ ""
+ ],
+ "load older templates": [
+ ""
+ ],
+ "There is no templates yet, add more pressing the + sign": [
+ ""
+ ],
+ "template delete successfully": [
+ ""
+ ],
+ "could not delete the template": [
+ ""
+ ],
+ "could not update template": [
+ ""
+ ],
+ "should be one of '%1$s'": [
+ ""
+ ],
+ "Webhook ID to use": [
""
],
- "check the id, doest look valid": [
+ "Event": [
+ ""
+ ],
+ "The event of the webhook: why the webhook is used": [
+ ""
+ ],
+ "Method": [
+ ""
+ ],
+ "Method used by the webhook": [
+ ""
+ ],
+ "URL": [
+ ""
+ ],
+ "URL of the webhook where the customer will be redirected": [
+ ""
+ ],
+ "Header": [
+ ""
+ ],
+ "Header template of the webhook": [
+ ""
+ ],
+ "Body": [
+ ""
+ ],
+ "Body template by the webhook": [
+ ""
+ ],
+ "Webhooks": [
+ ""
+ ],
+ "add new webhooks": [
+ ""
+ ],
+ "load more webhooks before the first one": [
+ ""
+ ],
+ "load newer webhooks": [
+ ""
+ ],
+ "Event type": [
+ ""
+ ],
+ "delete selected webhook from the database": [
+ ""
+ ],
+ "load more webhooks after the last one": [
+ ""
+ ],
+ "load older webhooks": [
+ ""
+ ],
+ "There is no webhooks yet, add more pressing the + sign": [
+ ""
+ ],
+ "webhook delete successfully": [
+ ""
+ ],
+ "could not delete the webhook": [
+ ""
+ ],
+ "check the id, does not look valid": [
""
],
"should have 52 characters, current %1$s": [
@@ -2826,18 +7686,42 @@ strings['it'] = {
"URL doesn't have the right format": [
""
],
- "Transfer ID": [
+ "Credited bank account": [
""
],
- "Account Address": [
+ "Select one account": [
""
],
- "Exchange URL": [
+ "Bank account of the merchant where the payment was received": [
+ ""
+ ],
+ "Wire transfer ID": [
+ ""
+ ],
+ "unique identifier of the wire transfer used by the exchange, must be 52 characters long": [
+ ""
+ ],
+ "Base URL of the exchange that made the transfer, should have been in the wire transfer subject": [
+ ""
+ ],
+ "Amount credited": [
+ ""
+ ],
+ "Actual amount that was wired to the merchant's bank account": [
""
],
"could not inform transfer": [
""
],
+ "Transfers": [
+ ""
+ ],
+ "add new transfer": [
+ ""
+ ],
+ "load more transfers before the first one": [
+ ""
+ ],
"load newer transfers": [
""
],
@@ -2862,11 +7746,302 @@ strings['it'] = {
"unknown": [
""
],
+ "delete selected transfer from the database": [
+ ""
+ ],
+ "load more transfer after the last one": [
+ ""
+ ],
"load older transfers": [
""
],
"There is no transfer yet, add more pressing the + sign": [
""
+ ],
+ "filter by account address": [
+ ""
+ ],
+ "only show wire transfers confirmed by the merchant": [
+ ""
+ ],
+ "only show wire transfers claimed by the exchange": [
+ ""
+ ],
+ "Unverified": [
+ ""
+ ],
+ "is not valid": [
+ ""
+ ],
+ "is not a number": [
+ ""
+ ],
+ "must be 1 or greater": [
+ ""
+ ],
+ "max 7 lines": [
+ ""
+ ],
+ "change authorization configuration": [
+ ""
+ ],
+ "Need to complete marked fields and choose authorization method": [
+ ""
+ ],
+ "This is not a valid bitcoin address.": [
+ ""
+ ],
+ "This is not a valid Ethereum address.": [
+ ""
+ ],
+ "IBAN numbers usually have more that 4 digits": [
+ ""
+ ],
+ "IBAN numbers usually have less that 34 digits": [
+ ""
+ ],
+ "IBAN country code not found": [
+ ""
+ ],
+ "IBAN number is not valid, checksum is wrong": [
+ ""
+ ],
+ "Target type": [
+ ""
+ ],
+ "Method to use for wire transfer": [
+ ""
+ ],
+ "Routing": [
+ ""
+ ],
+ "Routing number.": [
+ ""
+ ],
+ "Account": [
+ ""
+ ],
+ "Account number.": [
+ ""
+ ],
+ "Business Identifier Code.": [
+ ""
+ ],
+ "Bank Account Number.": [
+ ""
+ ],
+ "Unified Payment Interface.": [
+ ""
+ ],
+ "Bitcoin protocol.": [
+ ""
+ ],
+ "Ethereum protocol.": [
+ ""
+ ],
+ "Interledger protocol.": [
+ ""
+ ],
+ "Host": [
+ ""
+ ],
+ "Bank host.": [
+ ""
+ ],
+ "Bank account.": [
+ ""
+ ],
+ "Bank account owner's name.": [
+ ""
+ ],
+ "No accounts yet.": [
+ ""
+ ],
+ "Name of the instance in URLs. The 'default' instance is special in that it is used to administer other instances.": [
+ ""
+ ],
+ "Business name": [
+ ""
+ ],
+ "Legal name of the business represented by this instance.": [
+ ""
+ ],
+ "Email": [
+ ""
+ ],
+ "Contact email": [
+ ""
+ ],
+ "Website URL": [
+ ""
+ ],
+ "URL.": [
+ ""
+ ],
+ "Logo": [
+ ""
+ ],
+ "Logo image.": [
+ ""
+ ],
+ "Bank account": [
+ ""
+ ],
+ "URI specifying bank account for crediting revenue.": [
+ ""
+ ],
+ "Default max deposit fee": [
+ ""
+ ],
+ "Maximum deposit fees this merchant is willing to pay per order by default.": [
+ ""
+ ],
+ "Default max wire fee": [
+ ""
+ ],
+ "Maximum wire fees this merchant is willing to pay per wire transfer by default.": [
+ ""
+ ],
+ "Default wire fee amortization": [
+ ""
+ ],
+ "Number of orders excess wire transfer fees will be divided by to compute per order surcharge.": [
+ ""
+ ],
+ "Physical location of the merchant.": [
+ ""
+ ],
+ "Jurisdiction": [
+ ""
+ ],
+ "Jurisdiction for legal disputes with the merchant.": [
+ ""
+ ],
+ "Default payment delay": [
+ ""
+ ],
+ "Time customers have to pay an order before the offer expires by default.": [
+ ""
+ ],
+ "Default wire transfer delay": [
+ ""
+ ],
+ "Maximum time an exchange is allowed to delay wiring funds to the merchant, enabling it to aggregate smaller payments into larger wire transfers and reducing wire fees.": [
+ ""
+ ],
+ "Instance id": [
+ ""
+ ],
+ "Change the authorization method use for this instance.": [
+ ""
+ ],
+ "Manage access token": [
+ ""
+ ],
+ "Failed to create instance": [
+ ""
+ ],
+ "Login required": [
+ ""
+ ],
+ "Please enter your access token.": [
+ ""
+ ],
+ "Access Token": [
+ ""
+ ],
+ "The request to the backend take too long and was cancelled": [
+ ""
+ ],
+ "Diagnostic from %1$s is \"%2$s\"": [
+ ""
+ ],
+ "The backend reported a problem: HTTP status #%1$s": [
+ ""
+ ],
+ "Diagnostic from %1$s is '%2$s'": [
+ ""
+ ],
+ "Access denied": [
+ ""
+ ],
+ "The access token provided is invalid.": [
+ ""
+ ],
+ "No 'default' instance configured yet.": [
+ ""
+ ],
+ "Create a 'default' instance to begin using the merchant backoffice.": [
+ ""
+ ],
+ "The access token provided is invalid": [
+ ""
+ ],
+ "Hide for today": [
+ ""
+ ],
+ "Instance": [
+ ""
+ ],
+ "Settings": [
+ ""
+ ],
+ "Connection": [
+ ""
+ ],
+ "New": [
+ ""
+ ],
+ "List": [
+ ""
+ ],
+ "Log out": [
+ ""
+ ],
+ "Check your token is valid": [
+ ""
+ ],
+ "Couldn't access the server.": [
+ ""
+ ],
+ "Could not infer instance id from url %1$s": [
+ ""
+ ],
+ "Server not found": [
+ ""
+ ],
+ "Server response with an error code": [
+ ""
+ ],
+ "Got message %1$s from %2$s": [
+ ""
+ ],
+ "Response from server is unreadable, http status: %1$s": [
+ ""
+ ],
+ "Unexpected Error": [
+ ""
+ ],
+ "The value %1$s is invalid for a payment url": [
+ ""
+ ],
+ "add element to the list": [
+ ""
+ ],
+ "add": [
+ ""
+ ],
+ "Deleting": [
+ ""
+ ],
+ "Changing": [
+ ""
+ ],
+ "Order ID": [
+ ""
+ ],
+ "Payment URL": [
+ ""
]
}
}
@@ -2881,58 +8056,172 @@ strings['sv'] = {
"plural_forms": "nplurals=2; plural=(n != 1);",
"lang": ""
},
- "Access denied": [
+ "Cancel": [
""
],
- "Check your token is valid": [
+ "%1$s": [
""
],
- "Couldn't access the server.": [
+ "Close": [
""
],
- "Could not infer instance id from url %1$s": [
+ "Continue": [
""
],
- "HTTP status #%1$s: Server reported a problem": [
+ "Clear": [
""
],
- "Got message: \"%1$s\" from: %2$s": [
+ "Confirm": [
""
],
- "No default instance": [
+ "is not the same as the current access token": [
""
],
- "in order to use merchant backoffice, you should create the default instance": [
+ "cannot be empty": [
""
],
- "Server reported a problem: HTTP status #%1$s": [
+ "cannot be the same as the old token": [
""
],
- "Got message: %1$s from: %2$s": [
+ "is not the same": [
""
],
- "Login required": [
+ "You are updating the access token from instance with id %1$s": [
""
],
- "Please enter your auth token. Token should have \"secret-token:\" and start with Bearer or ApiKey": [
+ "Old access token": [
""
],
- "Confirm": [
+ "access token currently in use": [
""
],
- "The value %1$s is invalid for a payment url": [
+ "New access token": [
+ ""
+ ],
+ "next access token to be used": [
+ ""
+ ],
+ "Repeat access token": [
+ ""
+ ],
+ "confirm the same access token": [
+ ""
+ ],
+ "Clearing the access token will mean public access to the instance": [
+ ""
+ ],
+ "cannot be the same as the old access token": [
+ ""
+ ],
+ "You are setting the access token for the new instance": [
+ ""
+ ],
+ "With external authorization method no check will be done by the merchant backend": [
+ ""
+ ],
+ "Set external authorization": [
+ ""
+ ],
+ "Set access token": [
+ ""
+ ],
+ "Operation in progress...": [
+ ""
+ ],
+ "The operation will be automatically canceled after %1$s seconds": [
+ ""
+ ],
+ "Instances": [
+ ""
+ ],
+ "Delete": [
+ ""
+ ],
+ "add new instance": [
+ ""
+ ],
+ "ID": [
+ ""
+ ],
+ "Name": [
+ ""
+ ],
+ "Edit": [
+ ""
+ ],
+ "Purge": [
+ ""
+ ],
+ "There is no instances yet, add more pressing the + sign": [
+ ""
+ ],
+ "Only show active instances": [
+ ""
+ ],
+ "Active": [
""
],
- "pick a date": [
+ "Only show deleted instances": [
+ ""
+ ],
+ "Deleted": [
+ ""
+ ],
+ "Show all instances": [
+ ""
+ ],
+ "All": [
+ ""
+ ],
+ "Instance \"%1$s\" (ID: %2$s) has been deleted": [
+ ""
+ ],
+ "Failed to delete instance": [
+ ""
+ ],
+ "Instance '%1$s' (ID: %2$s) has been disabled": [
+ ""
+ ],
+ "Failed to purge instance": [
+ ""
+ ],
+ "Pending KYC verification": [
+ ""
+ ],
+ "Timed out": [
+ ""
+ ],
+ "Exchange": [
+ ""
+ ],
+ "Target account": [
+ ""
+ ],
+ "KYC URL": [
+ ""
+ ],
+ "Code": [
+ ""
+ ],
+ "Http Status": [
+ ""
+ ],
+ "No pending kyc verification!": [
+ ""
+ ],
+ "change value to unknown date": [
+ ""
+ ],
+ "change value to empty": [
""
],
"clear": [
""
],
- "never": [
+ "change value to never": [
""
],
- "Image should be smaller than 1 MB": [
+ "never": [
""
],
"Country": [
@@ -2971,277 +8260,427 @@ strings['sv'] = {
"Description": [
""
],
- "Name": [
+ "Product": [
""
],
- "loading...": [
+ "search products by it's description or id": [
""
],
- "no products found": [
+ "no products found with that description": [
""
],
- "no results": [
+ "You must enter a valid product identifier.": [
""
],
- "Deleting": [
+ "Quantity must be greater than 0!": [
""
],
- "Changing": [
+ "This quantity exceeds remaining stock. Currently, only %1$s units remain unreserved in stock.": [
""
],
- "Manage token": [
+ "Quantity": [
""
],
- "Update": [
+ "how many products will be added": [
+ ""
+ ],
+ "Add from inventory": [
+ ""
+ ],
+ "Image should be smaller than 1 MB": [
+ ""
+ ],
+ "Add": [
""
],
"Remove": [
""
],
- "Cancel": [
+ "No taxes configured for this product.": [
""
],
- "Manage stock": [
+ "Amount": [
""
],
- "Infinite": [
+ "Taxes can be in currencies that differ from the main currency used by the merchant.": [
""
],
- "lost cannot be greater that current + incoming (max %1$s)": [
+ "Enter currency and value separated with a colon, e.g. &quot;USD:2.3&quot;.": [
""
],
- "current stock will change from %1$s to %2$s": [
+ "Legal name of the tax, e.g. VAT or import duties.": [
""
],
- "current stock will stay at %1$s": [
+ "add tax to the tax list": [
""
],
- "Incoming": [
+ "describe and add a product that is not in the inventory list": [
""
],
- "Lost": [
+ "Add custom product": [
""
],
- "Current": [
+ "Complete information of the product": [
""
],
- "without stock": [
+ "Image": [
""
],
- "Next restock": [
+ "photo of the product": [
""
],
- "Delivery address": [
+ "full product description": [
""
],
- "this product has no taxes": [
+ "Unit": [
""
],
- "Amount": [
+ "name of the product unit": [
""
],
- "currency and value separated with colon": [
+ "Price": [
""
],
- "Add": [
+ "amount in the current currency": [
""
],
- "Instance": [
+ "Taxes": [
""
],
- "Settings": [
+ "image": [
""
],
- "Orders": [
+ "description": [
""
],
- "Products": [
+ "quantity": [
""
],
- "Transfers": [
+ "unit price": [
""
],
- "Connection": [
+ "total price": [
""
],
- "Instances": [
+ "required": [
""
],
- "New": [
+ "not valid": [
""
],
- "List": [
+ "must be greater than 0": [
""
],
- "Log out": [
+ "not a valid json": [
""
],
- "Clear": [
+ "should be in the future": [
""
],
- "should be the same": [
+ "refund deadline cannot be before pay deadline": [
""
],
- "cannot be the same as before": [
+ "wire transfer deadline cannot be before refund deadline": [
""
],
- "You are updating the authorization token from instance %1$s with id %2$s": [
+ "wire transfer deadline cannot be before pay deadline": [
""
],
- "Old token": [
+ "should have a refund deadline": [
""
],
- "New token": [
+ "auto refund cannot be after refund deadline": [
""
],
- "Clearing the auth token will mean public access to the instance": [
+ "Manage products in order": [
""
],
- "ID": [
+ "Manage list of products in the order.": [
""
],
- "Image": [
+ "Remove this product from the order.": [
""
],
- "Unit": [
+ "Total price": [
""
],
- "Price": [
+ "total product price added up": [
""
],
- "Stock": [
+ "Amount to be paid by the customer": [
""
],
- "Taxes": [
+ "Order price": [
""
],
- "Server not found": [
+ "final order price": [
""
],
- "Couldn't access the server": [
+ "Summary": [
""
],
- "Got message %1$s from %2$s": [
+ "Title of the order to be shown to the customer": [
""
],
- "Unexpected Error": [
+ "Shipping and Fulfillment": [
""
],
- "Auth token": [
+ "Delivery date": [
""
],
- "Account address": [
+ "Deadline for physical delivery assured by the merchant.": [
""
],
- "Default max deposit fee": [
+ "Location": [
""
],
- "Default max wire fee": [
+ "address where the products will be delivered": [
""
],
- "Default wire fee amortization": [
+ "Fulfillment URL": [
""
],
- "Jurisdiction": [
+ "URL to which the user will be redirected after successful payment.": [
""
],
- "Default pay delay": [
+ "Taler payment options": [
""
],
- "Default wire transfer delay": [
+ "Override default Taler payment settings for this order": [
""
],
- "could not create instance": [
+ "Payment deadline": [
""
],
- "Delete": [
+ "Deadline for the customer to pay for the offer before it expires. Inventory products will be reserved until this deadline.": [
""
],
- "Edit": [
+ "Refund deadline": [
""
],
- "There is no instances yet, add more pressing the + sign": [
+ "Time until which the order can be refunded by the merchant.": [
""
],
- "Inventory products": [
+ "Wire transfer deadline": [
""
],
- "Total price": [
+ "Deadline for the exchange to make the wire transfer.": [
""
],
- "Total tax": [
+ "Auto-refund deadline": [
""
],
- "Order price": [
+ "Time until which the wallet will automatically check for refunds without user interaction.": [
""
],
- "Net": [
+ "Maximum deposit fee": [
""
],
- "Summary": [
+ "Maximum deposit fees the merchant is willing to cover for this order. Higher deposit fees must be covered in full by the consumer.": [
""
],
- "Payments options": [
+ "Maximum wire fee": [
""
],
- "Auto refund deadline": [
+ "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.": [
""
],
- "Refund deadline": [
+ "Wire fee amortization": [
""
],
- "Pay deadline": [
+ "Factor by which wire fees exceeding the above threshold are divided to determine the share of excess wire fees to be paid explicitly by the consumer.": [
""
],
- "Delivery date": [
+ "Create token": [
""
],
- "Location": [
+ "Uncheck this option if the merchant backend generated an order ID with enough entropy to prevent adversarial claims.": [
+ ""
+ ],
+ "Minimum age required": [
+ ""
+ ],
+ "Any value greater than 0 will limit the coins able be used to pay this contract. If empty the age restriction will be defined by the products": [
+ ""
+ ],
+ "Min age defined by the producs is %1$s": [
+ ""
+ ],
+ "Additional information": [
+ ""
+ ],
+ "Custom information to be included in the contract for this order.": [
+ ""
+ ],
+ "You must enter a value in JavaScript Object Notation (JSON).": [
+ ""
+ ],
+ "days": [
+ ""
+ ],
+ "hours": [
+ ""
+ ],
+ "minutes": [
+ ""
+ ],
+ "seconds": [
+ ""
+ ],
+ "forever": [
+ ""
+ ],
+ "%1$sM": [
+ ""
+ ],
+ "%1$sY": [
+ ""
+ ],
+ "%1$sd": [
+ ""
+ ],
+ "%1$sh": [
+ ""
+ ],
+ "%1$smin": [
+ ""
+ ],
+ "%1$ssec": [
+ ""
+ ],
+ "Orders": [
+ ""
+ ],
+ "create order": [
+ ""
+ ],
+ "load newer orders": [
+ ""
+ ],
+ "Date": [
+ ""
+ ],
+ "Refund": [
+ ""
+ ],
+ "copy url": [
+ ""
+ ],
+ "load older orders": [
+ ""
+ ],
+ "No orders have been found matching your query!": [
+ ""
+ ],
+ "duplicated": [
+ ""
+ ],
+ "invalid format": [
+ ""
+ ],
+ "this value exceed the refundable amount": [
+ ""
+ ],
+ "date": [
+ ""
+ ],
+ "amount": [
+ ""
+ ],
+ "reason": [
+ ""
+ ],
+ "amount to be refunded": [
+ ""
+ ],
+ "Max refundable:": [
+ ""
+ ],
+ "Reason": [
+ ""
+ ],
+ "Choose one...": [
+ ""
+ ],
+ "requested by the customer": [
+ ""
+ ],
+ "other": [
+ ""
+ ],
+ "why this order is being refunded": [
+ ""
+ ],
+ "more information to give context": [
+ ""
+ ],
+ "Contract Terms": [
+ ""
+ ],
+ "human-readable description of the whole purchase": [
+ ""
+ ],
+ "total price for the transaction": [
+ ""
+ ],
+ "URL for this purchase": [
""
],
"Max fee": [
""
],
+ "maximum total deposit fee accepted by the merchant for this contract": [
+ ""
+ ],
"Max wire fee": [
""
],
- "Wire fee amortization": [
+ "maximum wire fee accepted by the merchant": [
""
],
- "Fullfilment url": [
+ "over how many customer transactions does the merchant expect to amortize wire fees on average": [
""
],
- "Extra information": [
+ "Created at": [
""
],
- "select a product first": [
+ "time when this contract was generated": [
""
],
- "should be greater than 0": [
+ "after this deadline has passed no refunds will be accepted": [
""
],
- "cannot be greater than current stock and quantity previously added. max: %1$s": [
+ "after this deadline, the merchant won't accept payments for the contract": [
""
],
- "cannot be greater than current stock %1$s": [
+ "transfer deadline for the exchange": [
""
],
- "Quantity": [
+ "time indicating when the order should be delivered": [
""
],
- "Order": [
+ "where the order will be delivered": [
""
],
- "claimed": [
+ "Auto-refund delay": [
""
],
- "copy url": [
+ "how long the wallet should try to get an automatic refund for the purchase": [
""
],
- "pay at": [
+ "Extra info": [
""
],
- "created at": [
+ "extra data that is only interpreted by the merchant frontend": [
+ ""
+ ],
+ "Order": [
+ ""
+ ],
+ "claimed": [
+ ""
+ ],
+ "claimed at": [
""
],
"Timeline": [
@@ -3265,90 +8704,180 @@ strings['sv'] = {
"refunded": [
""
],
+ "refund order": [
+ ""
+ ],
+ "not refundable": [
+ ""
+ ],
"refund": [
""
],
"Refunded amount": [
""
],
- "Deposit total": [
+ "Refund taken": [
+ ""
+ ],
+ "Status URL": [
+ ""
+ ],
+ "Refund URI": [
""
],
"unpaid": [
""
],
+ "pay at": [
+ ""
+ ],
+ "created at": [
+ ""
+ ],
"Order status URL": [
""
],
- "Pay URI": [
+ "Payment URI": [
""
],
"Unknown order status. This is an error, please contact the administrator.": [
""
],
+ "Back": [
+ ""
+ ],
"refund created successfully": [
""
],
"could not create the refund": [
""
],
- "load newer orders": [
+ "select date to show nearby orders": [
""
],
- "Date": [
+ "order id": [
""
],
- "Refund": [
+ "jump to order with the given order ID": [
""
],
- "load older orders": [
+ "remove all filters": [
""
],
- "No orders has been found": [
+ "only show paid orders": [
""
],
- "date": [
+ "Paid": [
""
],
- "amount": [
+ "only show orders with refunds": [
""
],
- "reason": [
+ "Refunded": [
""
],
- "Max refundable:": [
+ "only show orders where customers paid, but wire payments from payment provider are still pending": [
""
],
- "Reason": [
+ "Not wired": [
""
],
- "duplicated": [
+ "clear date filter": [
""
],
- "requested by the customer": [
+ "date (YYYY/MM/DD)": [
""
],
- "other": [
+ "Enter an order id": [
""
],
- "go to order id": [
+ "order not found": [
""
],
- "Paid": [
+ "could not get the order to refund": [
""
],
- "Refunded": [
+ "Loading...": [
""
],
- "Not wired": [
+ "click here to configure the stock of the product, leave it as is and the backend will not control stock": [
""
],
- "All": [
+ "Manage stock": [
+ ""
+ ],
+ "this product has been configured without stock control": [
+ ""
+ ],
+ "Infinite": [
+ ""
+ ],
+ "lost cannot be greater than current and incoming (max %1$s)": [
+ ""
+ ],
+ "Incoming": [
+ ""
+ ],
+ "Lost": [
+ ""
+ ],
+ "Current": [
+ ""
+ ],
+ "remove stock control for this product": [
+ ""
+ ],
+ "without stock": [
+ ""
+ ],
+ "Next restock": [
+ ""
+ ],
+ "Delivery address": [
+ ""
+ ],
+ "product identification to use in URLs (for internal use only)": [
+ ""
+ ],
+ "illustration of the product for customers": [
+ ""
+ ],
+ "product description for customers": [
+ ""
+ ],
+ "Age restricted": [
+ ""
+ ],
+ "is this product restricted for customer below certain age?": [
+ ""
+ ],
+ "unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 items, 5 meters) for customers": [
+ ""
+ ],
+ "sale price for customers, including taxes, for above units of the product": [
+ ""
+ ],
+ "Stock": [
+ ""
+ ],
+ "product inventory for products with finite supply (for internal use only)": [
+ ""
+ ],
+ "taxes included in the product price, exposed to customers": [
+ ""
+ ],
+ "Need to complete marked fields": [
""
],
"could not create product": [
""
],
+ "Products": [
+ ""
+ ],
+ "add product to inventory": [
+ ""
+ ],
"Sell": [
""
],
@@ -3358,6 +8887,42 @@ strings['sv'] = {
"Sold": [
""
],
+ "free": [
+ ""
+ ],
+ "go to product update page": [
+ ""
+ ],
+ "Update": [
+ ""
+ ],
+ "remove this product from the database": [
+ ""
+ ],
+ "update the product with new price": [
+ ""
+ ],
+ "update product with new price": [
+ ""
+ ],
+ "add more elements to the inventory": [
+ ""
+ ],
+ "report elements lost in the inventory": [
+ ""
+ ],
+ "new price for the product": [
+ ""
+ ],
+ "the are value with errors": [
+ ""
+ ],
+ "update product with new stock and price": [
+ ""
+ ],
+ "There is no products yet, add more pressing the + sign": [
+ ""
+ ],
"product updated successfully": [
""
],
@@ -3370,25 +8935,355 @@ strings['sv'] = {
"could not delete the product": [
""
],
+ "Product id:": [
+ ""
+ ],
+ "To complete the setup of the reserve, you must now initiate a wire transfer using the given wire transfer subject and crediting the specified amount to the indicated account of the exchange.": [
+ ""
+ ],
+ "If your system supports RFC 8905, you can do this by opening this URI:": [
+ ""
+ ],
+ "it should be greater than 0": [
+ ""
+ ],
+ "must be a valid URL": [
+ ""
+ ],
+ "Initial balance": [
+ ""
+ ],
+ "balance prior to deposit": [
+ ""
+ ],
+ "Exchange URL": [
+ ""
+ ],
+ "URL of exchange": [
+ ""
+ ],
+ "Next": [
+ ""
+ ],
+ "Wire method": [
+ ""
+ ],
+ "method to use for wire transfer": [
+ ""
+ ],
+ "Select one wire method": [
+ ""
+ ],
+ "could not create reserve": [
+ ""
+ ],
+ "Valid until": [
+ ""
+ ],
+ "Created balance": [
+ ""
+ ],
+ "Exchange balance": [
+ ""
+ ],
+ "Picked up": [
+ ""
+ ],
+ "Committed": [
+ ""
+ ],
+ "Account address": [
+ ""
+ ],
+ "Subject": [
+ ""
+ ],
"Tips": [
""
],
- "Committed amount": [
+ "No tips has been authorized from this reserve": [
""
],
- "Exchange initial amount": [
+ "Authorized": [
""
],
- "Merchant initial amount": [
+ "Expiration": [
""
],
- "There is no tips yet, add more pressing the + sign": [
+ "amount of tip": [
""
],
- "cannot be empty": [
+ "Justification": [
+ ""
+ ],
+ "reason for the tip": [
+ ""
+ ],
+ "URL after tip": [
+ ""
+ ],
+ "URL to visit after tip payment": [
+ ""
+ ],
+ "Reserves not yet funded": [
+ ""
+ ],
+ "Reserves ready": [
+ ""
+ ],
+ "add new reserve": [
+ ""
+ ],
+ "Expires at": [
+ ""
+ ],
+ "Initial": [
+ ""
+ ],
+ "delete selected reserve from the database": [
+ ""
+ ],
+ "authorize new tip from selected reserve": [
+ ""
+ ],
+ "There is no ready reserves yet, add more pressing the + sign or fund them": [
+ ""
+ ],
+ "Expected Balance": [
+ ""
+ ],
+ "could not create the tip": [
+ ""
+ ],
+ "should not be empty": [
+ ""
+ ],
+ "should be greater that 0": [
+ ""
+ ],
+ "can't be empty": [
+ ""
+ ],
+ "to short": [
+ ""
+ ],
+ "just letters and numbers from 2 to 7": [
+ ""
+ ],
+ "size of the key should be 32": [
+ ""
+ ],
+ "Identifier": [
+ ""
+ ],
+ "Name of the template in URLs.": [
+ ""
+ ],
+ "Describe what this template stands for": [
+ ""
+ ],
+ "Fixed summary": [
+ ""
+ ],
+ "If specified, this template will create order with the same summary": [
+ ""
+ ],
+ "Fixed price": [
+ ""
+ ],
+ "If specified, this template will create order with the same price": [
+ ""
+ ],
+ "Minimum age": [
+ ""
+ ],
+ "Is this contract restricted to some age?": [
+ ""
+ ],
+ "Payment timeout": [
+ ""
+ ],
+ "How much time has the customer to complete the payment once the order was created.": [
+ ""
+ ],
+ "Verification algorithm": [
+ ""
+ ],
+ "Algorithm to use to verify transaction in offline mode": [
+ ""
+ ],
+ "Point-of-sale key": [
+ ""
+ ],
+ "Useful to validate the purchase": [
+ ""
+ ],
+ "generate random secret key": [
+ ""
+ ],
+ "random": [
+ ""
+ ],
+ "show secret key": [
+ ""
+ ],
+ "hide secret key": [
+ ""
+ ],
+ "hide": [
+ ""
+ ],
+ "show": [
+ ""
+ ],
+ "could not inform template": [
+ ""
+ ],
+ "Amount is required": [
+ ""
+ ],
+ "Order summary is required": [
+ ""
+ ],
+ "New order for template": [
""
],
- "check the id, doest look valid": [
+ "Amount of the order": [
+ ""
+ ],
+ "Order summary": [
+ ""
+ ],
+ "could not create order from template": [
+ ""
+ ],
+ "Here you can specify a default value for fields that are not fixed. Default values can be edited by the customer before the payment.": [
+ ""
+ ],
+ "Fixed amount": [
+ ""
+ ],
+ "Default amount": [
+ ""
+ ],
+ "Default summary": [
+ ""
+ ],
+ "Print": [
+ ""
+ ],
+ "Setup TOTP": [
+ ""
+ ],
+ "Templates": [
+ ""
+ ],
+ "add new templates": [
+ ""
+ ],
+ "load more templates before the first one": [
+ ""
+ ],
+ "load newer templates": [
+ ""
+ ],
+ "delete selected templates from the database": [
+ ""
+ ],
+ "use template to create new order": [
+ ""
+ ],
+ "create qr code for the template": [
+ ""
+ ],
+ "load more templates after the last one": [
+ ""
+ ],
+ "load older templates": [
+ ""
+ ],
+ "There is no templates yet, add more pressing the + sign": [
+ ""
+ ],
+ "template delete successfully": [
+ ""
+ ],
+ "could not delete the template": [
+ ""
+ ],
+ "could not update template": [
+ ""
+ ],
+ "should be one of '%1$s'": [
+ ""
+ ],
+ "Webhook ID to use": [
+ ""
+ ],
+ "Event": [
+ ""
+ ],
+ "The event of the webhook: why the webhook is used": [
+ ""
+ ],
+ "Method": [
+ ""
+ ],
+ "Method used by the webhook": [
+ ""
+ ],
+ "URL": [
+ ""
+ ],
+ "URL of the webhook where the customer will be redirected": [
+ ""
+ ],
+ "Header": [
+ ""
+ ],
+ "Header template of the webhook": [
+ ""
+ ],
+ "Body": [
+ ""
+ ],
+ "Body template by the webhook": [
+ ""
+ ],
+ "Webhooks": [
+ ""
+ ],
+ "add new webhooks": [
+ ""
+ ],
+ "load more webhooks before the first one": [
+ ""
+ ],
+ "load newer webhooks": [
+ ""
+ ],
+ "Event type": [
+ ""
+ ],
+ "delete selected webhook from the database": [
+ ""
+ ],
+ "load more webhooks after the last one": [
+ ""
+ ],
+ "load older webhooks": [
+ ""
+ ],
+ "There is no webhooks yet, add more pressing the + sign": [
+ ""
+ ],
+ "webhook delete successfully": [
+ ""
+ ],
+ "could not delete the webhook": [
+ ""
+ ],
+ "check the id, does not look valid": [
""
],
"should have 52 characters, current %1$s": [
@@ -3397,18 +9292,42 @@ strings['sv'] = {
"URL doesn't have the right format": [
""
],
- "Transfer ID": [
+ "Credited bank account": [
""
],
- "Account Address": [
+ "Select one account": [
""
],
- "Exchange URL": [
+ "Bank account of the merchant where the payment was received": [
+ ""
+ ],
+ "Wire transfer ID": [
+ ""
+ ],
+ "unique identifier of the wire transfer used by the exchange, must be 52 characters long": [
+ ""
+ ],
+ "Base URL of the exchange that made the transfer, should have been in the wire transfer subject": [
+ ""
+ ],
+ "Amount credited": [
+ ""
+ ],
+ "Actual amount that was wired to the merchant's bank account": [
""
],
"could not inform transfer": [
""
],
+ "Transfers": [
+ ""
+ ],
+ "add new transfer": [
+ ""
+ ],
+ "load more transfers before the first one": [
+ ""
+ ],
"load newer transfers": [
""
],
@@ -3433,11 +9352,302 @@ strings['sv'] = {
"unknown": [
""
],
+ "delete selected transfer from the database": [
+ ""
+ ],
+ "load more transfer after the last one": [
+ ""
+ ],
"load older transfers": [
""
],
"There is no transfer yet, add more pressing the + sign": [
""
+ ],
+ "filter by account address": [
+ ""
+ ],
+ "only show wire transfers confirmed by the merchant": [
+ ""
+ ],
+ "only show wire transfers claimed by the exchange": [
+ ""
+ ],
+ "Unverified": [
+ ""
+ ],
+ "is not valid": [
+ ""
+ ],
+ "is not a number": [
+ ""
+ ],
+ "must be 1 or greater": [
+ ""
+ ],
+ "max 7 lines": [
+ ""
+ ],
+ "change authorization configuration": [
+ ""
+ ],
+ "Need to complete marked fields and choose authorization method": [
+ ""
+ ],
+ "This is not a valid bitcoin address.": [
+ ""
+ ],
+ "This is not a valid Ethereum address.": [
+ ""
+ ],
+ "IBAN numbers usually have more that 4 digits": [
+ ""
+ ],
+ "IBAN numbers usually have less that 34 digits": [
+ ""
+ ],
+ "IBAN country code not found": [
+ ""
+ ],
+ "IBAN number is not valid, checksum is wrong": [
+ ""
+ ],
+ "Target type": [
+ ""
+ ],
+ "Method to use for wire transfer": [
+ ""
+ ],
+ "Routing": [
+ ""
+ ],
+ "Routing number.": [
+ ""
+ ],
+ "Account": [
+ ""
+ ],
+ "Account number.": [
+ ""
+ ],
+ "Business Identifier Code.": [
+ ""
+ ],
+ "Bank Account Number.": [
+ ""
+ ],
+ "Unified Payment Interface.": [
+ ""
+ ],
+ "Bitcoin protocol.": [
+ ""
+ ],
+ "Ethereum protocol.": [
+ ""
+ ],
+ "Interledger protocol.": [
+ ""
+ ],
+ "Host": [
+ ""
+ ],
+ "Bank host.": [
+ ""
+ ],
+ "Bank account.": [
+ ""
+ ],
+ "Bank account owner's name.": [
+ ""
+ ],
+ "No accounts yet.": [
+ ""
+ ],
+ "Name of the instance in URLs. The 'default' instance is special in that it is used to administer other instances.": [
+ ""
+ ],
+ "Business name": [
+ ""
+ ],
+ "Legal name of the business represented by this instance.": [
+ ""
+ ],
+ "Email": [
+ ""
+ ],
+ "Contact email": [
+ ""
+ ],
+ "Website URL": [
+ ""
+ ],
+ "URL.": [
+ ""
+ ],
+ "Logo": [
+ ""
+ ],
+ "Logo image.": [
+ ""
+ ],
+ "Bank account": [
+ ""
+ ],
+ "URI specifying bank account for crediting revenue.": [
+ ""
+ ],
+ "Default max deposit fee": [
+ ""
+ ],
+ "Maximum deposit fees this merchant is willing to pay per order by default.": [
+ ""
+ ],
+ "Default max wire fee": [
+ ""
+ ],
+ "Maximum wire fees this merchant is willing to pay per wire transfer by default.": [
+ ""
+ ],
+ "Default wire fee amortization": [
+ ""
+ ],
+ "Number of orders excess wire transfer fees will be divided by to compute per order surcharge.": [
+ ""
+ ],
+ "Physical location of the merchant.": [
+ ""
+ ],
+ "Jurisdiction": [
+ ""
+ ],
+ "Jurisdiction for legal disputes with the merchant.": [
+ ""
+ ],
+ "Default payment delay": [
+ ""
+ ],
+ "Time customers have to pay an order before the offer expires by default.": [
+ ""
+ ],
+ "Default wire transfer delay": [
+ ""
+ ],
+ "Maximum time an exchange is allowed to delay wiring funds to the merchant, enabling it to aggregate smaller payments into larger wire transfers and reducing wire fees.": [
+ ""
+ ],
+ "Instance id": [
+ ""
+ ],
+ "Change the authorization method use for this instance.": [
+ ""
+ ],
+ "Manage access token": [
+ ""
+ ],
+ "Failed to create instance": [
+ ""
+ ],
+ "Login required": [
+ ""
+ ],
+ "Please enter your access token.": [
+ ""
+ ],
+ "Access Token": [
+ ""
+ ],
+ "The request to the backend take too long and was cancelled": [
+ ""
+ ],
+ "Diagnostic from %1$s is \"%2$s\"": [
+ ""
+ ],
+ "The backend reported a problem: HTTP status #%1$s": [
+ ""
+ ],
+ "Diagnostic from %1$s is '%2$s'": [
+ ""
+ ],
+ "Access denied": [
+ ""
+ ],
+ "The access token provided is invalid.": [
+ ""
+ ],
+ "No 'default' instance configured yet.": [
+ ""
+ ],
+ "Create a 'default' instance to begin using the merchant backoffice.": [
+ ""
+ ],
+ "The access token provided is invalid": [
+ ""
+ ],
+ "Hide for today": [
+ ""
+ ],
+ "Instance": [
+ ""
+ ],
+ "Settings": [
+ ""
+ ],
+ "Connection": [
+ ""
+ ],
+ "New": [
+ ""
+ ],
+ "List": [
+ ""
+ ],
+ "Log out": [
+ ""
+ ],
+ "Check your token is valid": [
+ ""
+ ],
+ "Couldn't access the server.": [
+ ""
+ ],
+ "Could not infer instance id from url %1$s": [
+ ""
+ ],
+ "Server not found": [
+ ""
+ ],
+ "Server response with an error code": [
+ ""
+ ],
+ "Got message %1$s from %2$s": [
+ ""
+ ],
+ "Response from server is unreadable, http status: %1$s": [
+ ""
+ ],
+ "Unexpected Error": [
+ ""
+ ],
+ "The value %1$s is invalid for a payment url": [
+ ""
+ ],
+ "add element to the list": [
+ ""
+ ],
+ "add": [
+ ""
+ ],
+ "Deleting": [
+ ""
+ ],
+ "Changing": [
+ ""
+ ],
+ "Order ID": [
+ ""
+ ],
+ "Payment URL": [
+ ""
]
}
}
diff --git a/packages/merchant-backoffice-ui/src/i18n/sv.po b/packages/merchant-backoffice-ui/src/i18n/sv.po
index 6b35bd0ce..d8d0bae29 100644
--- a/packages/merchant-backoffice-ui/src/i18n/sv.po
+++ b/packages/merchant-backoffice-ui/src/i18n/sv.po
@@ -16,7 +16,7 @@
msgid ""
msgstr ""
"Project-Id-Version: Taler Wallet\n"
-"Report-Msgid-Bugs-To: \n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
"POT-Creation-Date: 2016-11-23 00:00+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
@@ -27,1031 +27,2715 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-#: src/ApplicationReadyRoutes.tsx:50 src/InstanceRoutes.tsx:118
-#: src/InstanceRoutes.tsx:299
+#: src/components/modal/index.tsx:71
#, c-format
-msgid "Access denied"
+msgid "Cancel"
msgstr ""
-#: src/ApplicationReadyRoutes.tsx:51 src/InstanceRoutes.tsx:118
-#: src/InstanceRoutes.tsx:300
+#: src/components/modal/index.tsx:79
#, c-format
-msgid "Check your token is valid"
+msgid "%1$s"
msgstr ""
-#: src/ApplicationReadyRoutes.tsx:72
+#: src/components/modal/index.tsx:84
#, c-format
-msgid "Couldn't access the server."
+msgid "Close"
msgstr ""
-#: src/ApplicationReadyRoutes.tsx:73
+#: src/components/modal/index.tsx:124
#, c-format
-msgid "Could not infer instance id from url %1$s"
+msgid "Continue"
msgstr ""
-#: src/InstanceRoutes.tsx:109
+#: src/components/modal/index.tsx:178
#, c-format
-msgid "HTTP status #%1$s: Server reported a problem"
+msgid "Clear"
msgstr ""
-#: src/InstanceRoutes.tsx:110
+#: src/components/modal/index.tsx:190
#, c-format
-msgid "Got message: \"%1$s\" from: %2$s"
+msgid "Confirm"
msgstr ""
-#: src/InstanceRoutes.tsx:127
+#: src/components/modal/index.tsx:296
#, c-format
-msgid "No default instance"
+msgid "is not the same as the current access token"
msgstr ""
-#: src/InstanceRoutes.tsx:128
+#: src/components/modal/index.tsx:299
#, c-format
-msgid ""
-"in order to use merchant backoffice, you should create the default instance"
+msgid "cannot be empty"
msgstr ""
-#: src/InstanceRoutes.tsx:288
+#: src/components/modal/index.tsx:301
#, c-format
-msgid "Server reported a problem: HTTP status #%1$s"
+msgid "cannot be the same as the old token"
msgstr ""
-#: src/InstanceRoutes.tsx:289
+#: src/components/modal/index.tsx:305
#, c-format
-msgid "Got message: %1$s from: %2$s"
+msgid "is not the same"
msgstr ""
-#: src/components/exception/login.tsx:46
+#: src/components/modal/index.tsx:315
#, c-format
-msgid "Login required"
+msgid "You are updating the access token from instance with id %1$s"
+msgstr ""
+
+#: src/components/modal/index.tsx:331
+#, c-format
+msgid "Old access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:332
+#, c-format
+msgid "access token currently in use"
+msgstr ""
+
+#: src/components/modal/index.tsx:338
+#, c-format
+msgid "New access token"
msgstr ""
-#: src/components/exception/login.tsx:49
+#: src/components/modal/index.tsx:339
+#, c-format
+msgid "next access token to be used"
+msgstr ""
+
+#: src/components/modal/index.tsx:344
+#, c-format
+msgid "Repeat access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:345
+#, c-format
+msgid "confirm the same access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:350
+#, c-format
+msgid "Clearing the access token will mean public access to the instance"
+msgstr ""
+
+#: src/components/modal/index.tsx:377
+#, c-format
+msgid "cannot be the same as the old access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:394
+#, c-format
+msgid "You are setting the access token for the new instance"
+msgstr ""
+
+#: src/components/modal/index.tsx:420
#, c-format
msgid ""
-"Please enter your auth token. Token should have \"secret-token:\" and start "
-"with Bearer or ApiKey"
+"With external authorization method no check will be done by the merchant "
+"backend"
msgstr ""
-#: src/components/exception/login.tsx:86 src/components/modal/index.tsx:53
-#: src/components/modal/index.tsx:75 src/paths/admin/create/CreatePage.tsx:115
-#: src/paths/instance/orders/create/CreatePage.tsx:325
-#: src/paths/instance/products/create/CreatePage.tsx:51
-#: src/paths/instance/products/list/Table.tsx:174
-#: src/paths/instance/products/list/Table.tsx:228
-#: src/paths/instance/products/update/UpdatePage.tsx:55
-#: src/paths/instance/transfers/create/CreatePage.tsx:89
-#: src/paths/instance/update/UpdatePage.tsx:134
+#: src/components/modal/index.tsx:436
#, c-format
-msgid "Confirm"
+msgid "Set external authorization"
msgstr ""
-#: src/components/form/InputArray.tsx:72
+#: src/components/modal/index.tsx:448
#, c-format
-msgid "The value %1$s is invalid for a payment url"
+msgid "Set access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:470
+#, c-format
+msgid "Operation in progress..."
+msgstr ""
+
+#: src/components/modal/index.tsx:479
+#, c-format
+msgid "The operation will be automatically canceled after %1$s seconds"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:80
+#, c-format
+msgid "Instances"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:93
+#, c-format
+msgid "Delete"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:99
+#, c-format
+msgid "add new instance"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:178
+#, c-format
+msgid "ID"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:181
+#, c-format
+msgid "Name"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:220
+#, c-format
+msgid "Edit"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:237
+#, c-format
+msgid "Purge"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:261
+#, c-format
+msgid "There is no instances yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:68
+#, c-format
+msgid "Only show active instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:71
+#, c-format
+msgid "Active"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:78
+#, c-format
+msgid "Only show deleted instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:81
+#, c-format
+msgid "Deleted"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:88
+#, c-format
+msgid "Show all instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:91
+#, c-format
+msgid "All"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:101
+#, c-format
+msgid "Instance \"%1$s\" (ID: %2$s) has been deleted"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:106
+#, c-format
+msgid "Failed to delete instance"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:124
+#, c-format
+msgid "Instance '%1$s' (ID: %2$s) has been disabled"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:129
+#, c-format
+msgid "Failed to purge instance"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:41
+#, c-format
+msgid "Pending KYC verification"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:66
+#, c-format
+msgid "Timed out"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:103
+#, c-format
+msgid "Exchange"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:106
+#, c-format
+msgid "Target account"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:109
+#, c-format
+msgid "KYC URL"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:144
+#, c-format
+msgid "Code"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:147
+#, c-format
+msgid "Http Status"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:177
+#, c-format
+msgid "No pending kyc verification!"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:123
+#, c-format
+msgid "change value to unknown date"
msgstr ""
-#: src/components/form/InputDate.tsx:67
-#: src/paths/instance/orders/list/index.tsx:123
+#: src/components/form/InputDate.tsx:124
#, c-format
-msgid "pick a date"
+msgid "change value to empty"
msgstr ""
-#: src/components/form/InputDate.tsx:81
+#: src/components/form/InputDate.tsx:131
#, c-format
msgid "clear"
msgstr ""
-#: src/components/form/InputDate.tsx:83
-#: src/paths/instance/transfers/list/Table.tsx:140
+#: src/components/form/InputDate.tsx:136
#, c-format
-msgid "never"
+msgid "change value to never"
msgstr ""
-#: src/components/form/InputImage.tsx:80
+#: src/components/form/InputDate.tsx:141
#, c-format
-msgid "Image should be smaller than 1 MB"
+msgid "never"
msgstr ""
-#: src/components/form/InputLocation.tsx:28
+#: src/components/form/InputLocation.tsx:29
#, c-format
msgid "Country"
msgstr ""
-#: src/components/form/InputLocation.tsx:30
-#: src/paths/admin/create/CreatePage.tsx:99
-#: src/paths/instance/transfers/list/Table.tsx:124
-#: src/paths/instance/update/UpdatePage.tsx:118
+#: src/components/form/InputLocation.tsx:33
#, c-format
msgid "Address"
msgstr ""
-#: src/components/form/InputLocation.tsx:34
+#: src/components/form/InputLocation.tsx:39
#, c-format
msgid "Building number"
msgstr ""
-#: src/components/form/InputLocation.tsx:35
+#: src/components/form/InputLocation.tsx:41
#, c-format
msgid "Building name"
msgstr ""
-#: src/components/form/InputLocation.tsx:36
+#: src/components/form/InputLocation.tsx:42
#, c-format
msgid "Street"
msgstr ""
-#: src/components/form/InputLocation.tsx:37
+#: src/components/form/InputLocation.tsx:43
#, c-format
msgid "Post code"
msgstr ""
-#: src/components/form/InputLocation.tsx:38
+#: src/components/form/InputLocation.tsx:44
#, c-format
msgid "Town location"
msgstr ""
-#: src/components/form/InputLocation.tsx:39
+#: src/components/form/InputLocation.tsx:45
#, c-format
msgid "Town"
msgstr ""
-#: src/components/form/InputLocation.tsx:40
+#: src/components/form/InputLocation.tsx:46
#, c-format
msgid "District"
msgstr ""
-#: src/components/form/InputLocation.tsx:41
+#: src/components/form/InputLocation.tsx:49
#, c-format
msgid "Country subdivision"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:59
+#: src/components/form/InputSearchProduct.tsx:66
#, c-format
msgid "Product id"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:60
-#: src/components/product/ProductForm.tsx:99
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:122
-#: src/paths/instance/orders/list/Table.tsx:227
-#: src/paths/instance/products/list/Table.tsx:86
+#: src/components/form/InputSearchProduct.tsx:69
#, c-format
msgid "Description"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:73
-#: src/components/form/InputTaxes.tsx:81
-#: src/paths/admin/create/CreatePage.tsx:87 src/paths/admin/list/Table.tsx:110
-#: src/paths/instance/details/DetailPage.tsx:76
-#: src/paths/instance/update/UpdatePage.tsx:106
+#: src/components/form/InputSearchProduct.tsx:94
#, c-format
-msgid "Name"
+msgid "Product"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:102
+#: src/components/form/InputSearchProduct.tsx:95
#, c-format
-msgid "loading..."
+msgid "search products by it's description or id"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:108
+#: src/components/form/InputSearchProduct.tsx:151
#, c-format
-msgid "no products found"
+msgid "no products found with that description"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:116
+#: src/components/product/InventoryProductForm.tsx:56
#, c-format
-msgid "no results"
+msgid "You must enter a valid product identifier."
msgstr ""
-#: src/components/form/InputSecured.tsx:33
+#: src/components/product/InventoryProductForm.tsx:64
#, c-format
-msgid "Deleting"
+msgid "Quantity must be greater than 0!"
msgstr ""
-#: src/components/form/InputSecured.tsx:34
+#: src/components/product/InventoryProductForm.tsx:76
#, c-format
-msgid "Changing"
+msgid ""
+"This quantity exceeds remaining stock. Currently, only %1$s units remain "
+"unreserved in stock."
msgstr ""
-#: src/components/form/InputSecured.tsx:60
+#: src/components/product/InventoryProductForm.tsx:109
#, c-format
-msgid "Manage token"
+msgid "Quantity"
msgstr ""
-#: src/components/form/InputSecured.tsx:83
+#: src/components/product/InventoryProductForm.tsx:110
#, c-format
-msgid "Update"
+msgid "how many products will be added"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:117
+#, c-format
+msgid "Add from inventory"
+msgstr ""
+
+#: src/components/form/InputImage.tsx:105
+#, c-format
+msgid "Image should be smaller than 1 MB"
+msgstr ""
+
+#: src/components/form/InputImage.tsx:110
+#, c-format
+msgid "Add"
msgstr ""
-#: src/components/form/InputSecured.tsx:100
-#: src/paths/instance/orders/create/CreatePage.tsx:252
-#: src/paths/instance/orders/create/CreatePage.tsx:273
+#: src/components/form/InputImage.tsx:115
#, c-format
msgid "Remove"
msgstr ""
-#: src/components/form/InputSecured.tsx:106 src/components/modal/index.tsx:52
-#: src/components/modal/index.tsx:73 src/paths/admin/create/CreatePage.tsx:114
-#: src/paths/instance/orders/create/CreatePage.tsx:324
-#: src/paths/instance/products/create/CreatePage.tsx:50
-#: src/paths/instance/products/list/Table.tsx:166
-#: src/paths/instance/products/list/Table.tsx:218
-#: src/paths/instance/products/update/UpdatePage.tsx:54
-#: src/paths/instance/transfers/create/CreatePage.tsx:88
-#: src/paths/instance/update/UpdatePage.tsx:133
+#: src/components/form/InputTaxes.tsx:113
#, c-format
-msgid "Cancel"
+msgid "No taxes configured for this product."
msgstr ""
-#: src/components/form/InputStock.tsx:91
+#: src/components/form/InputTaxes.tsx:119
#, c-format
-msgid "Manage stock"
+msgid "Amount"
msgstr ""
-#: src/components/form/InputStock.tsx:93
+#: src/components/form/InputTaxes.tsx:120
#, c-format
-msgid "Infinite"
+msgid ""
+"Taxes can be in currencies that differ from the main currency used by the "
+"merchant."
msgstr ""
-#: src/components/form/InputStock.tsx:105
+#: src/components/form/InputTaxes.tsx:122
#, c-format
-msgid "lost cannot be greater that current + incoming (max %1$s)"
+msgid ""
+"Enter currency and value separated with a colon, e.g. &quot;USD:2.3&quot;."
msgstr ""
-#: src/components/form/InputStock.tsx:111
+#: src/components/form/InputTaxes.tsx:131
#, c-format
-msgid "current stock will change from %1$s to %2$s"
+msgid "Legal name of the tax, e.g. VAT or import duties."
msgstr ""
-#: src/components/form/InputStock.tsx:112
+#: src/components/form/InputTaxes.tsx:137
#, c-format
-msgid "current stock will stay at %1$s"
+msgid "add tax to the tax list"
msgstr ""
-#: src/components/form/InputStock.tsx:129
-#: src/paths/instance/products/list/Table.tsx:204
+#: src/components/product/NonInventoryProductForm.tsx:72
#, c-format
-msgid "Incoming"
+msgid "describe and add a product that is not in the inventory list"
msgstr ""
-#: src/components/form/InputStock.tsx:130
-#: src/paths/instance/products/list/Table.tsx:205
+#: src/components/product/NonInventoryProductForm.tsx:75
#, c-format
-msgid "Lost"
+msgid "Add custom product"
msgstr ""
-#: src/components/form/InputStock.tsx:142
+#: src/components/product/NonInventoryProductForm.tsx:86
#, c-format
-msgid "Current"
+msgid "Complete information of the product"
msgstr ""
-#: src/components/form/InputStock.tsx:145
+#: src/components/product/NonInventoryProductForm.tsx:185
#, c-format
-msgid "without stock"
+msgid "Image"
msgstr ""
-#: src/components/form/InputStock.tsx:150
+#: src/components/product/NonInventoryProductForm.tsx:186
#, c-format
-msgid "Next restock"
+msgid "photo of the product"
msgstr ""
-#: src/components/form/InputStock.tsx:152
+#: src/components/product/NonInventoryProductForm.tsx:192
#, c-format
-msgid "Delivery address"
+msgid "full product description"
msgstr ""
-#: src/components/form/InputTaxes.tsx:73
+#: src/components/product/NonInventoryProductForm.tsx:196
#, c-format
-msgid "this product has no taxes"
+msgid "Unit"
msgstr ""
-#: src/components/form/InputTaxes.tsx:77
-#: src/paths/instance/orders/details/DetailPage.tsx:145
-#: src/paths/instance/orders/details/DetailPage.tsx:296
-#: src/paths/instance/orders/list/Table.tsx:116
-#: src/paths/instance/transfers/create/CreatePage.tsx:84
+#: src/components/product/NonInventoryProductForm.tsx:197
#, c-format
-msgid "Amount"
+msgid "name of the product unit"
msgstr ""
-#: src/components/form/InputTaxes.tsx:78
+#: src/components/product/NonInventoryProductForm.tsx:201
#, c-format
-msgid "currency and value separated with colon"
+msgid "Price"
msgstr ""
-#: src/components/form/InputTaxes.tsx:84
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:78
+#: src/components/product/NonInventoryProductForm.tsx:202
#, c-format
-msgid "Add"
+msgid "amount in the current currency"
msgstr ""
-#: src/components/menu/SideBar.tsx:53
+#: src/components/product/NonInventoryProductForm.tsx:211
#, c-format
-msgid "Instance"
+msgid "Taxes"
msgstr ""
-#: src/components/menu/SideBar.tsx:59
+#: src/components/product/ProductList.tsx:38
#, c-format
-msgid "Settings"
+msgid "image"
msgstr ""
-#: src/components/menu/SideBar.tsx:65
-#: src/paths/instance/orders/list/Table.tsx:60
+#: src/components/product/ProductList.tsx:41
#, c-format
-msgid "Orders"
+msgid "description"
msgstr ""
-#: src/components/menu/SideBar.tsx:71
-#: src/paths/instance/orders/create/CreatePage.tsx:258
-#: src/paths/instance/products/list/Table.tsx:48
+#: src/components/product/ProductList.tsx:44
#, c-format
-msgid "Products"
+msgid "quantity"
msgstr ""
-#: src/components/menu/SideBar.tsx:77
-#: src/paths/instance/transfers/list/Table.tsx:65
+#: src/components/product/ProductList.tsx:47
#, c-format
-msgid "Transfers"
+msgid "unit price"
msgstr ""
-#: src/components/menu/SideBar.tsx:87
+#: src/components/product/ProductList.tsx:50
#, c-format
-msgid "Connection"
+msgid "total price"
msgstr ""
-#: src/components/menu/SideBar.tsx:112 src/paths/admin/list/Table.tsx:57
+#: src/paths/instance/orders/create/CreatePage.tsx:153
#, c-format
-msgid "Instances"
+msgid "required"
msgstr ""
-#: src/components/menu/SideBar.tsx:116
+#: src/paths/instance/orders/create/CreatePage.tsx:157
#, c-format
-msgid "New"
+msgid "not valid"
msgstr ""
-#: src/components/menu/SideBar.tsx:122
+#: src/paths/instance/orders/create/CreatePage.tsx:159
#, c-format
-msgid "List"
+msgid "must be greater than 0"
msgstr ""
-#: src/components/menu/SideBar.tsx:129
+#: src/paths/instance/orders/create/CreatePage.tsx:164
#, c-format
-msgid "Log out"
+msgid "not a valid json"
msgstr ""
-#: src/components/modal/index.tsx:74
+#: src/paths/instance/orders/create/CreatePage.tsx:170
#, c-format
-msgid "Clear"
+msgid "should be in the future"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:173
+#, c-format
+msgid "refund deadline cannot be before pay deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:179
+#, c-format
+msgid "wire transfer deadline cannot be before refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:190
+#, c-format
+msgid "wire transfer deadline cannot be before pay deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:197
+#, c-format
+msgid "should have a refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:202
+#, c-format
+msgid "auto refund cannot be after refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:360
+#, c-format
+msgid "Manage products in order"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:369
+#, c-format
+msgid "Manage list of products in the order."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:391
+#, c-format
+msgid "Remove this product from the order."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:415
+#, c-format
+msgid "Total price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:417
+#, c-format
+msgid "total product price added up"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:430
+#, c-format
+msgid "Amount to be paid by the customer"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:436
+#, c-format
+msgid "Order price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:437
+#, c-format
+msgid "final order price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:444
+#, c-format
+msgid "Summary"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:445
+#, c-format
+msgid "Title of the order to be shown to the customer"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:450
+#, c-format
+msgid "Shipping and Fulfillment"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:455
+#, c-format
+msgid "Delivery date"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:456
+#, c-format
+msgid "Deadline for physical delivery assured by the merchant."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:461
+#, c-format
+msgid "Location"
msgstr ""
-#: src/components/modal/index.tsx:110 src/components/modal/index.tsx:111
+#: src/paths/instance/orders/create/CreatePage.tsx:462
#, c-format
-msgid "should be the same"
+msgid "address where the products will be delivered"
msgstr ""
-#: src/components/modal/index.tsx:111
+#: src/paths/instance/orders/create/CreatePage.tsx:469
#, c-format
-msgid "cannot be the same as before"
+msgid "Fulfillment URL"
msgstr ""
-#: src/components/modal/index.tsx:114
+#: src/paths/instance/orders/create/CreatePage.tsx:470
+#, c-format
+msgid "URL to which the user will be redirected after successful payment."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:476
+#, c-format
+msgid "Taler payment options"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:477
+#, c-format
+msgid "Override default Taler payment settings for this order"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:481
+#, c-format
+msgid "Payment deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:482
#, c-format
msgid ""
-"You are updating the authorization token from instance %1$s with id %2$s"
+"Deadline for the customer to pay for the offer before it expires. Inventory "
+"products will be reserved until this deadline."
msgstr ""
-#: src/components/modal/index.tsx:124
+#: src/paths/instance/orders/create/CreatePage.tsx:486
#, c-format
-msgid "Old token"
+msgid "Refund deadline"
msgstr ""
-#: src/components/modal/index.tsx:125
+#: src/paths/instance/orders/create/CreatePage.tsx:487
#, c-format
-msgid "New token"
+msgid "Time until which the order can be refunded by the merchant."
msgstr ""
-#: src/components/modal/index.tsx:127
+#: src/paths/instance/orders/create/CreatePage.tsx:491
#, c-format
-msgid "Clearing the auth token will mean public access to the instance"
+msgid "Wire transfer deadline"
msgstr ""
-#: src/components/product/ProductForm.tsx:96
-#: src/paths/admin/create/CreatePage.tsx:85 src/paths/admin/list/Table.tsx:109
-#: src/paths/instance/transfers/list/Table.tsx:122
+#: src/paths/instance/orders/create/CreatePage.tsx:492
#, c-format
-msgid "ID"
+msgid "Deadline for the exchange to make the wire transfer."
msgstr ""
-#: src/components/product/ProductForm.tsx:98
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:121
-#: src/paths/instance/products/list/Table.tsx:85
+#: src/paths/instance/orders/create/CreatePage.tsx:496
#, c-format
-msgid "Image"
+msgid "Auto-refund deadline"
msgstr ""
-#: src/components/product/ProductForm.tsx:100
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:123
+#: src/paths/instance/orders/create/CreatePage.tsx:497
#, c-format
-msgid "Unit"
+msgid ""
+"Time until which the wallet will automatically check for refunds without "
+"user interaction."
msgstr ""
-#: src/components/product/ProductForm.tsx:101
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:124
-#: src/paths/instance/products/list/Table.tsx:162
-#: src/paths/instance/products/list/Table.tsx:214
+#: src/paths/instance/orders/create/CreatePage.tsx:502
#, c-format
-msgid "Price"
+msgid "Maximum deposit fee"
msgstr ""
-#: src/components/product/ProductForm.tsx:103
-#: src/paths/instance/products/list/Table.tsx:90
+#: src/paths/instance/orders/create/CreatePage.tsx:503
#, c-format
-msgid "Stock"
+msgid ""
+"Maximum deposit fees the merchant is willing to cover for this order. Higher "
+"deposit fees must be covered in full by the consumer."
msgstr ""
-#: src/components/product/ProductForm.tsx:105
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:128
-#: src/paths/instance/products/list/Table.tsx:88
+#: src/paths/instance/orders/create/CreatePage.tsx:507
#, c-format
-msgid "Taxes"
+msgid "Maximum wire fee"
msgstr ""
-#: src/index.tsx:75
+#: src/paths/instance/orders/create/CreatePage.tsx:508
#, c-format
-msgid "Server not found"
+msgid ""
+"Maximum aggregate wire fees the merchant is willing to cover for this order. "
+"Wire fees exceeding this amount are to be covered by the customers."
msgstr ""
-#: src/index.tsx:85
+#: src/paths/instance/orders/create/CreatePage.tsx:512
#, c-format
-msgid "Couldn't access the server"
+msgid "Wire fee amortization"
msgstr ""
-#: src/index.tsx:87 src/index.tsx:99
+#: src/paths/instance/orders/create/CreatePage.tsx:513
#, c-format
-msgid "Got message %1$s from %2$s"
+msgid ""
+"Factor by which wire fees exceeding the above threshold are divided to "
+"determine the share of excess wire fees to be paid explicitly by the "
+"consumer."
msgstr ""
-#: src/index.tsx:97
+#: src/paths/instance/orders/create/CreatePage.tsx:517
#, c-format
-msgid "Unexpected Error"
+msgid "Create token"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:89
-#: src/paths/instance/update/UpdatePage.tsx:108
+#: src/paths/instance/orders/create/CreatePage.tsx:518
#, c-format
-msgid "Auth token"
+msgid ""
+"Uncheck this option if the merchant backend generated an order ID with "
+"enough entropy to prevent adversarial claims."
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:91
-#: src/paths/instance/details/DetailPage.tsx:77
-#: src/paths/instance/update/UpdatePage.tsx:110
+#: src/paths/instance/orders/create/CreatePage.tsx:522
#, c-format
-msgid "Account address"
+msgid "Minimum age required"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:93
-#: src/paths/instance/update/UpdatePage.tsx:112
+#: src/paths/instance/orders/create/CreatePage.tsx:523
#, c-format
-msgid "Default max deposit fee"
+msgid ""
+"Any value greater than 0 will limit the coins able be used to pay this "
+"contract. If empty the age restriction will be defined by the products"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:95
-#: src/paths/instance/update/UpdatePage.tsx:114
+#: src/paths/instance/orders/create/CreatePage.tsx:526
#, c-format
-msgid "Default max wire fee"
+msgid "Min age defined by the producs is %1$s"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:97
-#: src/paths/instance/update/UpdatePage.tsx:116
+#: src/paths/instance/orders/create/CreatePage.tsx:534
#, c-format
-msgid "Default wire fee amortization"
+msgid "Additional information"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:103
-#: src/paths/instance/update/UpdatePage.tsx:122
+#: src/paths/instance/orders/create/CreatePage.tsx:535
#, c-format
-msgid "Jurisdiction"
+msgid "Custom information to be included in the contract for this order."
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:107
-#: src/paths/instance/update/UpdatePage.tsx:126
+#: src/paths/instance/orders/create/CreatePage.tsx:541
#, c-format
-msgid "Default pay delay"
+msgid "You must enter a value in JavaScript Object Notation (JSON)."
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:109
-#: src/paths/instance/update/UpdatePage.tsx:128
+#: src/components/picker/DurationPicker.tsx:55
#, c-format
-msgid "Default wire transfer delay"
+msgid "days"
msgstr ""
-#: src/paths/admin/create/index.tsx:58
+#: src/components/picker/DurationPicker.tsx:65
#, c-format
-msgid "could not create instance"
+msgid "hours"
msgstr ""
-#: src/paths/admin/list/Table.tsx:63 src/paths/admin/list/Table.tsx:131
-#: src/paths/instance/transfers/list/Table.tsx:71
+#: src/components/picker/DurationPicker.tsx:76
#, c-format
-msgid "Delete"
+msgid "minutes"
msgstr ""
-#: src/paths/admin/list/Table.tsx:128
+#: src/components/picker/DurationPicker.tsx:87
#, c-format
-msgid "Edit"
+msgid "seconds"
msgstr ""
-#: src/paths/admin/list/Table.tsx:149
-#: src/paths/instance/products/list/Table.tsx:245
+#: src/components/form/InputDuration.tsx:53
#, c-format
-msgid "There is no instances yet, add more pressing the + sign"
+msgid "forever"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:237
+#: src/components/form/InputDuration.tsx:62
#, c-format
-msgid "Inventory products"
+msgid "%1$sM"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:286
+#: src/components/form/InputDuration.tsx:64
#, c-format
-msgid "Total price"
+msgid "%1$sY"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:287
+#: src/components/form/InputDuration.tsx:66
#, c-format
-msgid "Total tax"
+msgid "%1$sd"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:289
-#: src/paths/instance/orders/create/CreatePage.tsx:297
+#: src/components/form/InputDuration.tsx:68
#, c-format
-msgid "Order price"
+msgid "%1$sh"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:295
+#: src/components/form/InputDuration.tsx:70
#, c-format
-msgid "Net"
+msgid "%1$smin"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:300
-#: src/paths/instance/orders/details/DetailPage.tsx:144
-#: src/paths/instance/orders/details/DetailPage.tsx:295
-#: src/paths/instance/orders/list/Table.tsx:117
+#: src/components/form/InputDuration.tsx:72
#, c-format
-msgid "Summary"
+msgid "%1$ssec"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:302
+#: src/paths/instance/orders/list/Table.tsx:75
#, c-format
-msgid "Payments options"
+msgid "Orders"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:303
+#: src/paths/instance/orders/list/Table.tsx:81
#, c-format
-msgid "Auto refund deadline"
+msgid "create order"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:304
+#: src/paths/instance/orders/list/Table.tsx:147
#, c-format
-msgid "Refund deadline"
+msgid "load newer orders"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:305
+#: src/paths/instance/orders/list/Table.tsx:154
#, c-format
-msgid "Pay deadline"
+msgid "Date"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:307
+#: src/paths/instance/orders/list/Table.tsx:200
#, c-format
-msgid "Delivery date"
+msgid "Refund"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:308
+#: src/paths/instance/orders/list/Table.tsx:209
#, c-format
-msgid "Location"
+msgid "copy url"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:225
+#, c-format
+msgid "load older orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:242
+#, c-format
+msgid "No orders have been found matching your query!"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:288
+#, c-format
+msgid "duplicated"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:299
+#, c-format
+msgid "invalid format"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:301
+#, c-format
+msgid "this value exceed the refundable amount"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:346
+#, c-format
+msgid "date"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:349
+#, c-format
+msgid "amount"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:352
+#, c-format
+msgid "reason"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:389
+#, c-format
+msgid "amount to be refunded"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:391
+#, c-format
+msgid "Max refundable:"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:396
+#, c-format
+msgid "Reason"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:397
+#, c-format
+msgid "Choose one..."
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:399
+#, c-format
+msgid "requested by the customer"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:400
+#, c-format
+msgid "other"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:403
+#, c-format
+msgid "why this order is being refunded"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:409
+#, c-format
+msgid "more information to give context"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:62
+#, c-format
+msgid "Contract Terms"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:68
+#, c-format
+msgid "human-readable description of the whole purchase"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:312
+#: src/paths/instance/orders/details/DetailPage.tsx:74
+#, c-format
+msgid "total price for the transaction"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:81
+#, c-format
+msgid "URL for this purchase"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:87
#, c-format
msgid "Max fee"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:313
+#: src/paths/instance/orders/details/DetailPage.tsx:88
+#, c-format
+msgid "maximum total deposit fee accepted by the merchant for this contract"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:93
#, c-format
msgid "Max wire fee"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:314
+#: src/paths/instance/orders/details/DetailPage.tsx:94
#, c-format
-msgid "Wire fee amortization"
+msgid "maximum wire fee accepted by the merchant"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:315
+#: src/paths/instance/orders/details/DetailPage.tsx:100
#, c-format
-msgid "Fullfilment url"
+msgid ""
+"over how many customer transactions does the merchant expect to amortize "
+"wire fees on average"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:318
+#: src/paths/instance/orders/details/DetailPage.tsx:105
#, c-format
-msgid "Extra information"
+msgid "Created at"
msgstr ""
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:44
+#: src/paths/instance/orders/details/DetailPage.tsx:106
#, c-format
-msgid "select a product first"
+msgid "time when this contract was generated"
msgstr ""
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:51
+#: src/paths/instance/orders/details/DetailPage.tsx:112
#, c-format
-msgid "should be greater than 0"
+msgid "after this deadline has passed no refunds will be accepted"
msgstr ""
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:58
+#: src/paths/instance/orders/details/DetailPage.tsx:118
#, c-format
msgid ""
-"cannot be greater than current stock and quantity previously added. max: %1$s"
+"after this deadline, the merchant won't accept payments for the contract"
msgstr ""
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:64
+#: src/paths/instance/orders/details/DetailPage.tsx:124
#, c-format
-msgid "cannot be greater than current stock %1$s"
+msgid "transfer deadline for the exchange"
msgstr ""
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:76
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:126
+#: src/paths/instance/orders/details/DetailPage.tsx:130
#, c-format
-msgid "Quantity"
+msgid "time indicating when the order should be delivered"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:92
-#: src/paths/instance/orders/details/DetailPage.tsx:235
-#: src/paths/instance/orders/details/DetailPage.tsx:333
+#: src/paths/instance/orders/details/DetailPage.tsx:136
#, c-format
-msgid "Order"
+msgid "where the order will be delivered"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:93
+#: src/paths/instance/orders/details/DetailPage.tsx:144
#, c-format
-msgid "claimed"
+msgid "Auto-refund delay"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:110
-#: src/paths/instance/orders/details/DetailPage.tsx:261
-#: src/paths/instance/orders/list/Table.tsx:136
+#: src/paths/instance/orders/details/DetailPage.tsx:145
#, c-format
-msgid "copy url"
+msgid ""
+"how long the wallet should try to get an automatic refund for the purchase"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:126
-#: src/paths/instance/orders/details/DetailPage.tsx:349
+#: src/paths/instance/orders/details/DetailPage.tsx:150
#, c-format
-msgid "pay at"
+msgid "Extra info"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:127
-#: src/paths/instance/orders/details/DetailPage.tsx:350
+#: src/paths/instance/orders/details/DetailPage.tsx:151
#, c-format
-msgid "created at"
+msgid "extra data that is only interpreted by the merchant frontend"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:219
+#, c-format
+msgid "Order"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:221
+#, c-format
+msgid "claimed"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:138
-#: src/paths/instance/orders/details/DetailPage.tsx:289
+#: src/paths/instance/orders/details/DetailPage.tsx:247
+#, c-format
+msgid "claimed at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:265
#, c-format
msgid "Timeline"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:142
-#: src/paths/instance/orders/details/DetailPage.tsx:293
+#: src/paths/instance/orders/details/DetailPage.tsx:271
#, c-format
msgid "Payment details"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:146
-#: src/paths/instance/orders/details/DetailPage.tsx:299
-#: src/paths/instance/orders/details/DetailPage.tsx:363
+#: src/paths/instance/orders/details/DetailPage.tsx:291
#, c-format
msgid "Order status"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:156
-#: src/paths/instance/orders/details/DetailPage.tsx:308
+#: src/paths/instance/orders/details/DetailPage.tsx:301
#, c-format
msgid "Product list"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:236
+#: src/paths/instance/orders/details/DetailPage.tsx:451
#, c-format
msgid "paid"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:238
+#: src/paths/instance/orders/details/DetailPage.tsx:455
#, c-format
msgid "wired"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:241
+#: src/paths/instance/orders/details/DetailPage.tsx:460
#, c-format
msgid "refunded"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:258
+#: src/paths/instance/orders/details/DetailPage.tsx:480
+#, c-format
+msgid "refund order"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:481
+#, c-format
+msgid "not refundable"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:489
#, c-format
msgid "refund"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:297
+#: src/paths/instance/orders/details/DetailPage.tsx:553
#, c-format
msgid "Refunded amount"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:298
+#: src/paths/instance/orders/details/DetailPage.tsx:560
#, c-format
-msgid "Deposit total"
+msgid "Refund taken"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:336
+#: src/paths/instance/orders/details/DetailPage.tsx:570
+#, c-format
+msgid "Status URL"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:583
+#, c-format
+msgid "Refund URI"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:636
#, c-format
msgid "unpaid"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:364
+#: src/paths/instance/orders/details/DetailPage.tsx:654
+#, c-format
+msgid "pay at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:666
+#, c-format
+msgid "created at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:707
#, c-format
msgid "Order status URL"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:365
+#: src/paths/instance/orders/details/DetailPage.tsx:711
#, c-format
-msgid "Pay URI"
+msgid "Payment URI"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:383
+#: src/paths/instance/orders/details/DetailPage.tsx:740
#, c-format
msgid ""
"Unknown order status. This is an error, please contact the administrator."
msgstr ""
-#: src/paths/instance/orders/details/index.tsx:56
-#: src/paths/instance/orders/list/index.tsx:147
+#: src/paths/instance/orders/details/DetailPage.tsx:767
+#, c-format
+msgid "Back"
+msgstr ""
+
+#: src/paths/instance/orders/details/index.tsx:79
#, c-format
msgid "refund created successfully"
msgstr ""
-#: src/paths/instance/orders/details/index.tsx:59
-#: src/paths/instance/orders/list/index.tsx:150
+#: src/paths/instance/orders/details/index.tsx:85
#, c-format
msgid "could not create the refund"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:111
+#: src/paths/instance/orders/list/ListPage.tsx:78
#, c-format
-msgid "load newer orders"
+msgid "select date to show nearby orders"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:115
+#: src/paths/instance/orders/list/ListPage.tsx:94
#, c-format
-msgid "Date"
+msgid "order id"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:131
-#: src/paths/instance/orders/list/Table.tsx:223
+#: src/paths/instance/orders/list/ListPage.tsx:100
#, c-format
-msgid "Refund"
+msgid "jump to order with the given order ID"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:145
+#: src/paths/instance/orders/list/ListPage.tsx:122
#, c-format
-msgid "load older orders"
+msgid "remove all filters"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:154
+#: src/paths/instance/orders/list/ListPage.tsx:132
#, c-format
-msgid "No orders has been found"
+msgid "only show paid orders"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:202
+#: src/paths/instance/orders/list/ListPage.tsx:135
#, c-format
-msgid "date"
+msgid "Paid"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:203
+#: src/paths/instance/orders/list/ListPage.tsx:142
#, c-format
-msgid "amount"
+msgid "only show orders with refunds"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:204
+#: src/paths/instance/orders/list/ListPage.tsx:145
#, c-format
-msgid "reason"
+msgid "Refunded"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:224
+#: src/paths/instance/orders/list/ListPage.tsx:152
#, c-format
-msgid "Max refundable:"
+msgid ""
+"only show orders where customers paid, but wire payments from payment "
+"provider are still pending"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:226
+#: src/paths/instance/orders/list/ListPage.tsx:155
#, c-format
-msgid "Reason"
+msgid "Not wired"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:226
+#: src/paths/instance/orders/list/ListPage.tsx:170
#, c-format
-msgid "duplicated"
+msgid "clear date filter"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:226
+#: src/paths/instance/orders/list/ListPage.tsx:184
#, c-format
-msgid "requested by the customer"
+msgid "date (YYYY/MM/DD)"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:226
+#: src/paths/instance/orders/list/index.tsx:103
#, c-format
-msgid "other"
+msgid "Enter an order id"
msgstr ""
-#: src/paths/instance/orders/list/index.tsx:91
+#: src/paths/instance/orders/list/index.tsx:111
#, c-format
-msgid "go to order id"
+msgid "order not found"
msgstr ""
-#: src/paths/instance/orders/list/index.tsx:107
+#: src/paths/instance/orders/list/index.tsx:178
#, c-format
-msgid "Paid"
+msgid "could not get the order to refund"
msgstr ""
-#: src/paths/instance/orders/list/index.tsx:108
+#: src/components/exception/AsyncButton.tsx:43
#, c-format
-msgid "Refunded"
+msgid "Loading..."
msgstr ""
-#: src/paths/instance/orders/list/index.tsx:109
+#: src/components/form/InputStock.tsx:99
#, c-format
-msgid "Not wired"
+msgid ""
+"click here to configure the stock of the product, leave it as is and the "
+"backend will not control stock"
msgstr ""
-#: src/paths/instance/orders/list/index.tsx:110
+#: src/components/form/InputStock.tsx:109
#, c-format
-msgid "All"
+msgid "Manage stock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:115
+#, c-format
+msgid "this product has been configured without stock control"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:119
+#, c-format
+msgid "Infinite"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:136
+#, c-format
+msgid "lost cannot be greater than current and incoming (max %1$s)"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:176
+#, c-format
+msgid "Incoming"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:177
+#, c-format
+msgid "Lost"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:192
+#, c-format
+msgid "Current"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:196
+#, c-format
+msgid "remove stock control for this product"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:202
+#, c-format
+msgid "without stock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:211
+#, c-format
+msgid "Next restock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:217
+#, c-format
+msgid "Delivery address"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:133
+#, c-format
+msgid "product identification to use in URLs (for internal use only)"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:139
+#, c-format
+msgid "illustration of the product for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:145
+#, c-format
+msgid "product description for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:149
+#, c-format
+msgid "Age restricted"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:150
+#, c-format
+msgid "is this product restricted for customer below certain age?"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:155
+#, c-format
+msgid ""
+"unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 "
+"items, 5 meters) for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:160
+#, c-format
+msgid ""
+"sale price for customers, including taxes, for above units of the product"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:164
+#, c-format
+msgid "Stock"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:166
+#, c-format
+msgid ""
+"product inventory for products with finite supply (for internal use only)"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:171
+#, c-format
+msgid "taxes included in the product price, exposed to customers"
+msgstr ""
+
+#: src/paths/instance/products/create/CreatePage.tsx:66
+#, c-format
+msgid "Need to complete marked fields"
msgstr ""
-#: src/paths/instance/products/create/index.tsx:48
-#: src/paths/instance/products/update/index.tsx:64
+#: src/paths/instance/products/create/index.tsx:51
#, c-format
msgid "could not create product"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:87
+#: src/paths/instance/products/list/Table.tsx:68
+#, c-format
+msgid "Products"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:73
+#, c-format
+msgid "add product to inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:137
#, c-format
msgid "Sell"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:89
+#: src/paths/instance/products/list/Table.tsx:143
#, c-format
msgid "Profit"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:91
+#: src/paths/instance/products/list/Table.tsx:149
#, c-format
msgid "Sold"
msgstr ""
-#: src/paths/instance/products/list/index.tsx:59
+#: src/paths/instance/products/list/Table.tsx:210
+#, c-format
+msgid "free"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:248
+#, c-format
+msgid "go to product update page"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:255
+#, c-format
+msgid "Update"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:260
+#, c-format
+msgid "remove this product from the database"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:331
+#, c-format
+msgid "update the product with new price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:341
+#, c-format
+msgid "update product with new price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:399
+#, c-format
+msgid "add more elements to the inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:404
+#, c-format
+msgid "report elements lost in the inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:409
+#, c-format
+msgid "new price for the product"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:421
+#, c-format
+msgid "the are value with errors"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:422
+#, c-format
+msgid "update product with new stock and price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:463
+#, c-format
+msgid "There is no products yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:86
#, c-format
msgid "product updated successfully"
msgstr ""
-#: src/paths/instance/products/list/index.tsx:62
+#: src/paths/instance/products/list/index.tsx:92
#, c-format
msgid "could not update the product"
msgstr ""
-#: src/paths/instance/products/list/index.tsx:70
+#: src/paths/instance/products/list/index.tsx:103
#, c-format
msgid "product delete successfully"
msgstr ""
-#: src/paths/instance/products/list/index.tsx:73
+#: src/paths/instance/products/list/index.tsx:109
#, c-format
msgid "could not delete the product"
msgstr ""
-#: src/paths/instance/tips/list/Table.tsx:59
+#: src/paths/instance/products/update/UpdatePage.tsx:56
+#, c-format
+msgid "Product id:"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:95
+#, c-format
+msgid ""
+"To complete the setup of the reserve, you must now initiate a wire transfer "
+"using the given wire transfer subject and crediting the specified amount to "
+"the indicated account of the exchange."
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:102
+#, c-format
+msgid "If your system supports RFC 8905, you can do this by opening this URI:"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:83
+#, c-format
+msgid "it should be greater than 0"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:88
+#, c-format
+msgid "must be a valid URL"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:107
+#, c-format
+msgid "Initial balance"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:108
+#, c-format
+msgid "balance prior to deposit"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:112
+#, c-format
+msgid "Exchange URL"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:113
+#, c-format
+msgid "URL of exchange"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:148
+#, c-format
+msgid "Next"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:186
+#, c-format
+msgid "Wire method"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:187
+#, c-format
+msgid "method to use for wire transfer"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:189
+#, c-format
+msgid "Select one wire method"
+msgstr ""
+
+#: src/paths/instance/reserves/create/index.tsx:62
+#, c-format
+msgid "could not create reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:77
+#, c-format
+msgid "Valid until"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:82
+#, c-format
+msgid "Created balance"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:99
+#, c-format
+msgid "Exchange balance"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:104
+#, c-format
+msgid "Picked up"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:109
+#, c-format
+msgid "Committed"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:116
+#, c-format
+msgid "Account address"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:119
+#, c-format
+msgid "Subject"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:130
#, c-format
msgid "Tips"
msgstr ""
-#: src/paths/instance/tips/list/Table.tsx:111
+#: src/paths/instance/reserves/details/DetailPage.tsx:193
#, c-format
-msgid "Committed amount"
+msgid "No tips has been authorized from this reserve"
msgstr ""
-#: src/paths/instance/tips/list/Table.tsx:112
+#: src/paths/instance/reserves/details/DetailPage.tsx:213
#, c-format
-msgid "Exchange initial amount"
+msgid "Authorized"
msgstr ""
-#: src/paths/instance/tips/list/Table.tsx:113
+#: src/paths/instance/reserves/details/DetailPage.tsx:222
#, c-format
-msgid "Merchant initial amount"
+msgid "Expiration"
msgstr ""
-#: src/paths/instance/tips/list/Table.tsx:148
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:108
#, c-format
-msgid "There is no tips yet, add more pressing the + sign"
+msgid "amount of tip"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:50
-#: src/paths/instance/transfers/create/CreatePage.tsx:54
-#: src/paths/instance/transfers/create/CreatePage.tsx:55
-#: src/paths/instance/transfers/create/CreatePage.tsx:56
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:112
#, c-format
-msgid "cannot be empty"
+msgid "Justification"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:114
+#, c-format
+msgid "reason for the tip"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:118
+#, c-format
+msgid "URL after tip"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:119
+#, c-format
+msgid "URL to visit after tip payment"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:65
+#, c-format
+msgid "Reserves not yet funded"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:89
+#, c-format
+msgid "Reserves ready"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:95
+#, c-format
+msgid "add new reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:143
+#, c-format
+msgid "Expires at"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:146
+#, c-format
+msgid "Initial"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:202
+#, c-format
+msgid "delete selected reserve from the database"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:210
+#, c-format
+msgid "authorize new tip from selected reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:237
+#, c-format
+msgid ""
+"There is no ready reserves yet, add more pressing the + sign or fund them"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:264
+#, c-format
+msgid "Expected Balance"
+msgstr ""
+
+#: src/paths/instance/reserves/list/index.tsx:110
+#, c-format
+msgid "could not create the tip"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:77
+#, c-format
+msgid "should not be empty"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:93
+#, c-format
+msgid "should be greater that 0"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:96
+#, c-format
+msgid "can't be empty"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:100
+#, c-format
+msgid "to short"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:108
+#, c-format
+msgid "just letters and numbers from 2 to 7"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:110
+#, c-format
+msgid "size of the key should be 32"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:137
+#, c-format
+msgid "Identifier"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:138
+#, c-format
+msgid "Name of the template in URLs."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:144
+#, c-format
+msgid "Describe what this template stands for"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:149
+#, c-format
+msgid "Fixed summary"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:150
+#, c-format
+msgid "If specified, this template will create order with the same summary"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:154
+#, c-format
+msgid "Fixed price"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:155
+#, c-format
+msgid "If specified, this template will create order with the same price"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:159
+#, c-format
+msgid "Minimum age"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:161
+#, c-format
+msgid "Is this contract restricted to some age?"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:165
+#, c-format
+msgid "Payment timeout"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:167
+#, c-format
+msgid ""
+"How much time has the customer to complete the payment once the order was "
+"created."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:171
+#, c-format
+msgid "Verification algorithm"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:172
+#, c-format
+msgid "Algorithm to use to verify transaction in offline mode"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:180
+#, c-format
+msgid "Point-of-sale key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:182
+#, c-format
+msgid "Useful to validate the purchase"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:196
+#, c-format
+msgid "generate random secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:203
+#, c-format
+msgid "random"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:208
+#, c-format
+msgid "show secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:209
+#, c-format
+msgid "hide secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:216
+#, c-format
+msgid "hide"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:218
+#, c-format
+msgid "show"
+msgstr ""
+
+#: src/paths/instance/templates/create/index.tsx:52
+#, c-format
+msgid "could not inform template"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:54
+#, c-format
+msgid "Amount is required"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:58
+#, c-format
+msgid "Order summary is required"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:86
+#, c-format
+msgid "New order for template"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:108
+#, c-format
+msgid "Amount of the order"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:113
+#, c-format
+msgid "Order summary"
+msgstr ""
+
+#: src/paths/instance/templates/use/index.tsx:92
+#, c-format
+msgid "could not create order from template"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:131
+#, c-format
+msgid ""
+"Here you can specify a default value for fields that are not fixed. Default "
+"values can be edited by the customer before the payment."
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:148
+#, c-format
+msgid "Fixed amount"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:51
+#: src/paths/instance/templates/qr/QrPage.tsx:149
#, c-format
-msgid "check the id, doest look valid"
+msgid "Default amount"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:52
+#: src/paths/instance/templates/qr/QrPage.tsx:161
+#, c-format
+msgid "Default summary"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:177
+#, c-format
+msgid "Print"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:184
+#, c-format
+msgid "Setup TOTP"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:65
+#, c-format
+msgid "Templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:70
+#, c-format
+msgid "add new templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:142
+#, c-format
+msgid "load more templates before the first one"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:146
+#, c-format
+msgid "load newer templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:181
+#, c-format
+msgid "delete selected templates from the database"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:188
+#, c-format
+msgid "use template to create new order"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:195
+#, c-format
+msgid "create qr code for the template"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:210
+#, c-format
+msgid "load more templates after the last one"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:214
+#, c-format
+msgid "load older templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:231
+#, c-format
+msgid "There is no templates yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/templates/list/index.tsx:104
+#, c-format
+msgid "template delete successfully"
+msgstr ""
+
+#: src/paths/instance/templates/list/index.tsx:110
+#, c-format
+msgid "could not delete the template"
+msgstr ""
+
+#: src/paths/instance/templates/update/index.tsx:90
+#, c-format
+msgid "could not update template"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:57
+#, c-format
+msgid "should be one of '%1$s'"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:85
+#, c-format
+msgid "Webhook ID to use"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:89
+#, c-format
+msgid "Event"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:90
+#, c-format
+msgid "The event of the webhook: why the webhook is used"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:94
+#, c-format
+msgid "Method"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:95
+#, c-format
+msgid "Method used by the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:99
+#, c-format
+msgid "URL"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:100
+#, c-format
+msgid "URL of the webhook where the customer will be redirected"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:104
+#, c-format
+msgid "Header"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:106
+#, c-format
+msgid "Header template of the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:111
+#, c-format
+msgid "Body"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:112
+#, c-format
+msgid "Body template by the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:61
+#, c-format
+msgid "Webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:66
+#, c-format
+msgid "add new webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:137
+#, c-format
+msgid "load more webhooks before the first one"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:141
+#, c-format
+msgid "load newer webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:151
+#, c-format
+msgid "Event type"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:176
+#, c-format
+msgid "delete selected webhook from the database"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:198
+#, c-format
+msgid "load more webhooks after the last one"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:202
+#, c-format
+msgid "load older webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:219
+#, c-format
+msgid "There is no webhooks yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/index.tsx:94
+#, c-format
+msgid "webhook delete successfully"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/index.tsx:100
+#, c-format
+msgid "could not delete the webhook"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:63
+#, c-format
+msgid "check the id, does not look valid"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:65
#, c-format
msgid "should have 52 characters, current %1$s"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:57
+#: src/paths/instance/transfers/create/CreatePage.tsx:72
#, c-format
msgid "URL doesn't have the right format"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:74
+#: src/paths/instance/transfers/create/CreatePage.tsx:98
#, c-format
-msgid "Transfer ID"
+msgid "Credited bank account"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:76
+#: src/paths/instance/transfers/create/CreatePage.tsx:100
#, c-format
-msgid "Account Address"
+msgid "Select one account"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:82
-#: src/paths/instance/transfers/list/Table.tsx:125
+#: src/paths/instance/transfers/create/CreatePage.tsx:101
#, c-format
-msgid "Exchange URL"
+msgid "Bank account of the merchant where the payment was received"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:105
+#, c-format
+msgid "Wire transfer ID"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:107
+#, c-format
+msgid ""
+"unique identifier of the wire transfer used by the exchange, must be 52 "
+"characters long"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:112
+#, c-format
+msgid ""
+"Base URL of the exchange that made the transfer, should have been in the "
+"wire transfer subject"
msgstr ""
-#: src/paths/instance/transfers/create/index.tsx:49
+#: src/paths/instance/transfers/create/CreatePage.tsx:117
+#, c-format
+msgid "Amount credited"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:118
+#, c-format
+msgid "Actual amount that was wired to the merchant's bank account"
+msgstr ""
+
+#: src/paths/instance/transfers/create/index.tsx:58
#, c-format
msgid "could not inform transfer"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:118
+#: src/paths/instance/transfers/list/Table.tsx:61
+#, c-format
+msgid "Transfers"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:66
+#, c-format
+msgid "add new transfer"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:129
+#, c-format
+msgid "load more transfers before the first one"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:133
#, c-format
msgid "load newer transfers"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:123
+#: src/paths/instance/transfers/list/Table.tsx:143
#, c-format
msgid "Credit"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:126
+#: src/paths/instance/transfers/list/Table.tsx:152
#, c-format
msgid "Confirmed"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:127
-#: src/paths/instance/transfers/list/index.tsx:60
+#: src/paths/instance/transfers/list/Table.tsx:155
#, c-format
msgid "Verified"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:128
+#: src/paths/instance/transfers/list/Table.tsx:158
#, c-format
msgid "Executed at"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:138
-#: src/paths/instance/transfers/list/Table.tsx:139
+#: src/paths/instance/transfers/list/Table.tsx:171
#, c-format
msgid "yes"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:138
-#: src/paths/instance/transfers/list/Table.tsx:139
+#: src/paths/instance/transfers/list/Table.tsx:171
#, c-format
msgid "no"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:140
+#: src/paths/instance/transfers/list/Table.tsx:181
#, c-format
msgid "unknown"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:145
+#: src/paths/instance/transfers/list/Table.tsx:187
+#, c-format
+msgid "delete selected transfer from the database"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:202
+#, c-format
+msgid "load more transfer after the last one"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:206
#, c-format
msgid "load older transfers"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:154
+#: src/paths/instance/transfers/list/Table.tsx:223
#, c-format
msgid "There is no transfer yet, add more pressing the + sign"
msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:79
+#, c-format
+msgid "filter by account address"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:100
+#, c-format
+msgid "only show wire transfers confirmed by the merchant"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:110
+#, c-format
+msgid "only show wire transfers claimed by the exchange"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:113
+#, c-format
+msgid "Unverified"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:69
+#, c-format
+msgid "is not valid"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:94
+#, c-format
+msgid "is not a number"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:96
+#, c-format
+msgid "must be 1 or greater"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:107
+#, c-format
+msgid "max 7 lines"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:178
+#, c-format
+msgid "change authorization configuration"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:217
+#, c-format
+msgid "Need to complete marked fields and choose authorization method"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:82
+#, c-format
+msgid "This is not a valid bitcoin address."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:95
+#, c-format
+msgid "This is not a valid Ethereum address."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:118
+#, c-format
+msgid "IBAN numbers usually have more that 4 digits"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:120
+#, c-format
+msgid "IBAN numbers usually have less that 34 digits"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:128
+#, c-format
+msgid "IBAN country code not found"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:153
+#, c-format
+msgid "IBAN number is not valid, checksum is wrong"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:248
+#, c-format
+msgid "Target type"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:249
+#, c-format
+msgid "Method to use for wire transfer"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:258
+#, c-format
+msgid "Routing"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:259
+#, c-format
+msgid "Routing number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:263
+#, c-format
+msgid "Account"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:264
+#, c-format
+msgid "Account number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:273
+#, c-format
+msgid "Business Identifier Code."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:282
+#, c-format
+msgid "Bank Account Number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:292
+#, c-format
+msgid "Unified Payment Interface."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:301
+#, c-format
+msgid "Bitcoin protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:310
+#, c-format
+msgid "Ethereum protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:319
+#, c-format
+msgid "Interledger protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:328
+#, c-format
+msgid "Host"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:329
+#, c-format
+msgid "Bank host."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:334
+#, c-format
+msgid "Bank account."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:343
+#, c-format
+msgid "Bank account owner's name."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:370
+#, c-format
+msgid "No accounts yet."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:52
+#, c-format
+msgid ""
+"Name of the instance in URLs. The 'default' instance is special in that it "
+"is used to administer other instances."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:58
+#, c-format
+msgid "Business name"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:59
+#, c-format
+msgid "Legal name of the business represented by this instance."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:64
+#, c-format
+msgid "Email"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:65
+#, c-format
+msgid "Contact email"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:70
+#, c-format
+msgid "Website URL"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:71
+#, c-format
+msgid "URL."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:76
+#, c-format
+msgid "Logo"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:77
+#, c-format
+msgid "Logo image."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:82
+#, c-format
+msgid "Bank account"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:83
+#, c-format
+msgid "URI specifying bank account for crediting revenue."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:88
+#, c-format
+msgid "Default max deposit fee"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:89
+#, c-format
+msgid ""
+"Maximum deposit fees this merchant is willing to pay per order by default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:94
+#, c-format
+msgid "Default max wire fee"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:95
+#, c-format
+msgid ""
+"Maximum wire fees this merchant is willing to pay per wire transfer by "
+"default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:100
+#, c-format
+msgid "Default wire fee amortization"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:101
+#, c-format
+msgid ""
+"Number of orders excess wire transfer fees will be divided by to compute per "
+"order surcharge."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:107
+#, c-format
+msgid "Physical location of the merchant."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:114
+#, c-format
+msgid "Jurisdiction"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:115
+#, c-format
+msgid "Jurisdiction for legal disputes with the merchant."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:122
+#, c-format
+msgid "Default payment delay"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:124
+#, c-format
+msgid ""
+"Time customers have to pay an order before the offer expires by default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:129
+#, c-format
+msgid "Default wire transfer delay"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:130
+#, c-format
+msgid ""
+"Maximum time an exchange is allowed to delay wiring funds to the merchant, "
+"enabling it to aggregate smaller payments into larger wire transfers and "
+"reducing wire fees."
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:164
+#, c-format
+msgid "Instance id"
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:173
+#, c-format
+msgid "Change the authorization method use for this instance."
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:182
+#, c-format
+msgid "Manage access token"
+msgstr ""
+
+#: src/paths/instance/update/index.tsx:112
+#, c-format
+msgid "Failed to create instance"
+msgstr ""
+
+#: src/components/exception/login.tsx:74
+#, c-format
+msgid "Login required"
+msgstr ""
+
+#: src/components/exception/login.tsx:80
+#, c-format
+msgid "Please enter your access token."
+msgstr ""
+
+#: src/components/exception/login.tsx:108
+#, c-format
+msgid "Access Token"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:171
+#, c-format
+msgid "The request to the backend take too long and was cancelled"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:172
+#, c-format
+msgid "Diagnostic from %1$s is \"%2$s\""
+msgstr ""
+
+#: src/InstanceRoutes.tsx:178
+#, c-format
+msgid "The backend reported a problem: HTTP status #%1$s"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:179
+#, c-format
+msgid "Diagnostic from %1$s is '%2$s'"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:196
+#, c-format
+msgid "Access denied"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:197
+#, c-format
+msgid "The access token provided is invalid."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:212
+#, c-format
+msgid "No 'default' instance configured yet."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:213
+#, c-format
+msgid "Create a 'default' instance to begin using the merchant backoffice."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:630
+#, c-format
+msgid "The access token provided is invalid"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:664
+#, c-format
+msgid "Hide for today"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:82
+#, c-format
+msgid "Instance"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:91
+#, c-format
+msgid "Settings"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:167
+#, c-format
+msgid "Connection"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:209
+#, c-format
+msgid "New"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:219
+#, c-format
+msgid "List"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:234
+#, c-format
+msgid "Log out"
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:71
+#, c-format
+msgid "Check your token is valid"
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:90
+#, c-format
+msgid "Couldn't access the server."
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:91
+#, c-format
+msgid "Could not infer instance id from url %1$s"
+msgstr ""
+
+#: src/Application.tsx:104
+#, c-format
+msgid "Server not found"
+msgstr ""
+
+#: src/Application.tsx:118
+#, c-format
+msgid "Server response with an error code"
+msgstr ""
+
+#: src/Application.tsx:120
+#, c-format
+msgid "Got message %1$s from %2$s"
+msgstr ""
+
+#: src/Application.tsx:131
+#, c-format
+msgid "Response from server is unreadable, http status: %1$s"
+msgstr ""
+
+#: src/Application.tsx:144
+#, c-format
+msgid "Unexpected Error"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:101
+#, c-format
+msgid "The value %1$s is invalid for a payment url"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:110
+#, c-format
+msgid "add element to the list"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:112
+#, c-format
+msgid "add"
+msgstr ""
+
+#: src/components/form/InputSecured.tsx:37
+#, c-format
+msgid "Deleting"
+msgstr ""
+
+#: src/components/form/InputSecured.tsx:41
+#, c-format
+msgid "Changing"
+msgstr ""
+
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:87
+#, c-format
+msgid "Order ID"
+msgstr ""
+
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:101
+#, c-format
+msgid "Payment URL"
+msgstr ""
diff --git a/packages/merchant-backoffice-ui/src/i18n/taler-merchant-backoffice.pot b/packages/merchant-backoffice-ui/src/i18n/taler-merchant-backoffice.pot
index 21fd863b0..5ef56ca05 100644
--- a/packages/merchant-backoffice-ui/src/i18n/taler-merchant-backoffice.pot
+++ b/packages/merchant-backoffice-ui/src/i18n/taler-merchant-backoffice.pot
@@ -1,1054 +1,2726 @@
# This file is part of GNU Taler
-# (C) 2021 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
# Foundation; either version 3, or (at your option) any later version.
+
# GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
# You should have received a copy of the GNU General Public License along with
# GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: Taler Wallet\n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-11-23 00:00+0100\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
-"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+#: src/components/modal/index.tsx:71
+#, c-format
+msgid "Cancel"
+msgstr ""
-#: src/ApplicationReadyRoutes.tsx:50 src/InstanceRoutes.tsx:118
-#: src/InstanceRoutes.tsx:299
+#: src/components/modal/index.tsx:79
#, c-format
-msgid "Access denied"
+msgid "%1$s"
msgstr ""
-#: src/ApplicationReadyRoutes.tsx:51 src/InstanceRoutes.tsx:118
-#: src/InstanceRoutes.tsx:300
+#: src/components/modal/index.tsx:84
#, c-format
-msgid "Check your token is valid"
+msgid "Close"
msgstr ""
-#: src/ApplicationReadyRoutes.tsx:72
+#: src/components/modal/index.tsx:124
#, c-format
-msgid "Couldn't access the server."
+msgid "Continue"
msgstr ""
-#: src/ApplicationReadyRoutes.tsx:73
+#: src/components/modal/index.tsx:178
#, c-format
-msgid "Could not infer instance id from url %1$s"
+msgid "Clear"
msgstr ""
-#: src/InstanceRoutes.tsx:109
+#: src/components/modal/index.tsx:190
#, c-format
-msgid "HTTP status #%1$s: Server reported a problem"
+msgid "Confirm"
msgstr ""
-#: src/InstanceRoutes.tsx:110
+#: src/components/modal/index.tsx:296
#, c-format
-msgid "Got message: \"%1$s\" from: %2$s"
+msgid "is not the same as the current access token"
msgstr ""
-#: src/InstanceRoutes.tsx:127
+#: src/components/modal/index.tsx:299
#, c-format
-msgid "No default instance"
+msgid "cannot be empty"
msgstr ""
-#: src/InstanceRoutes.tsx:128
+#: src/components/modal/index.tsx:301
#, c-format
-msgid ""
-"in order to use merchant backoffice, you should create the default instance"
+msgid "cannot be the same as the old token"
msgstr ""
-#: src/InstanceRoutes.tsx:288
+#: src/components/modal/index.tsx:305
#, c-format
-msgid "Server reported a problem: HTTP status #%1$s"
+msgid "is not the same"
msgstr ""
-#: src/InstanceRoutes.tsx:289
+#: src/components/modal/index.tsx:315
#, c-format
-msgid "Got message: %1$s from: %2$s"
+msgid "You are updating the access token from instance with id %1$s"
msgstr ""
-#: src/components/exception/login.tsx:46
+#: src/components/modal/index.tsx:331
#, c-format
-msgid "Login required"
+msgid "Old access token"
msgstr ""
-#: src/components/exception/login.tsx:49
+#: src/components/modal/index.tsx:332
#, c-format
-msgid ""
-"Please enter your auth token. Token should have \"secret-token:\" and start "
-"with Bearer or ApiKey"
+msgid "access token currently in use"
msgstr ""
-#: src/components/exception/login.tsx:86 src/components/modal/index.tsx:53
-#: src/components/modal/index.tsx:75 src/paths/admin/create/CreatePage.tsx:115
-#: src/paths/instance/orders/create/CreatePage.tsx:325
-#: src/paths/instance/products/create/CreatePage.tsx:51
-#: src/paths/instance/products/list/Table.tsx:174
-#: src/paths/instance/products/list/Table.tsx:228
-#: src/paths/instance/products/update/UpdatePage.tsx:55
-#: src/paths/instance/transfers/create/CreatePage.tsx:89
-#: src/paths/instance/update/UpdatePage.tsx:134
+#: src/components/modal/index.tsx:338
#, c-format
-msgid "Confirm"
+msgid "New access token"
msgstr ""
-#: src/components/form/InputArray.tsx:72
+#: src/components/modal/index.tsx:339
#, c-format
-msgid "The value %1$s is invalid for a payment url"
+msgid "next access token to be used"
+msgstr ""
+
+#: src/components/modal/index.tsx:344
+#, c-format
+msgid "Repeat access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:345
+#, c-format
+msgid "confirm the same access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:350
+#, c-format
+msgid "Clearing the access token will mean public access to the instance"
+msgstr ""
+
+#: src/components/modal/index.tsx:377
+#, c-format
+msgid "cannot be the same as the old access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:394
+#, c-format
+msgid "You are setting the access token for the new instance"
+msgstr ""
+
+#: src/components/modal/index.tsx:420
+#, c-format
+msgid "With external authorization method no check will be done by the merchant backend"
+msgstr ""
+
+#: src/components/modal/index.tsx:436
+#, c-format
+msgid "Set external authorization"
+msgstr ""
+
+#: src/components/modal/index.tsx:448
+#, c-format
+msgid "Set access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:470
+#, c-format
+msgid "Operation in progress..."
+msgstr ""
+
+#: src/components/modal/index.tsx:479
+#, c-format
+msgid "The operation will be automatically canceled after %1$s seconds"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:80
+#, c-format
+msgid "Instances"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:93
+#, c-format
+msgid "Delete"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:99
+#, c-format
+msgid "add new instance"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:178
+#, c-format
+msgid "ID"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:181
+#, c-format
+msgid "Name"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:220
+#, c-format
+msgid "Edit"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:237
+#, c-format
+msgid "Purge"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:261
+#, c-format
+msgid "There is no instances yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:68
+#, c-format
+msgid "Only show active instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:71
+#, c-format
+msgid "Active"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:78
+#, c-format
+msgid "Only show deleted instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:81
+#, c-format
+msgid "Deleted"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:88
+#, c-format
+msgid "Show all instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:91
+#, c-format
+msgid "All"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:101
+#, c-format
+msgid "Instance \"%1$s\" (ID: %2$s) has been deleted"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:106
+#, c-format
+msgid "Failed to delete instance"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:124
+#, c-format
+msgid "Instance '%1$s' (ID: %2$s) has been disabled"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:129
+#, c-format
+msgid "Failed to purge instance"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:41
+#, c-format
+msgid "Pending KYC verification"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:66
+#, c-format
+msgid "Timed out"
msgstr ""
-#: src/components/form/InputDate.tsx:67
-#: src/paths/instance/orders/list/index.tsx:123
+#: src/paths/instance/kyc/list/ListPage.tsx:103
#, c-format
-msgid "pick a date"
+msgid "Exchange"
msgstr ""
-#: src/components/form/InputDate.tsx:81
+#: src/paths/instance/kyc/list/ListPage.tsx:106
+#, c-format
+msgid "Target account"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:109
+#, c-format
+msgid "KYC URL"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:144
+#, c-format
+msgid "Code"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:147
+#, c-format
+msgid "Http Status"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:177
+#, c-format
+msgid "No pending kyc verification!"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:123
+#, c-format
+msgid "change value to unknown date"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:124
+#, c-format
+msgid "change value to empty"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:131
#, c-format
msgid "clear"
msgstr ""
-#: src/components/form/InputDate.tsx:83
-#: src/paths/instance/transfers/list/Table.tsx:140
+#: src/components/form/InputDate.tsx:136
#, c-format
-msgid "never"
+msgid "change value to never"
msgstr ""
-#: src/components/form/InputImage.tsx:80
+#: src/components/form/InputDate.tsx:141
#, c-format
-msgid "Image should be smaller than 1 MB"
+msgid "never"
msgstr ""
-#: src/components/form/InputLocation.tsx:28
+#: src/components/form/InputLocation.tsx:29
#, c-format
msgid "Country"
msgstr ""
-#: src/components/form/InputLocation.tsx:30
-#: src/paths/admin/create/CreatePage.tsx:99
-#: src/paths/instance/transfers/list/Table.tsx:124
-#: src/paths/instance/update/UpdatePage.tsx:118
+#: src/components/form/InputLocation.tsx:33
#, c-format
msgid "Address"
msgstr ""
-#: src/components/form/InputLocation.tsx:34
+#: src/components/form/InputLocation.tsx:39
#, c-format
msgid "Building number"
msgstr ""
-#: src/components/form/InputLocation.tsx:35
+#: src/components/form/InputLocation.tsx:41
#, c-format
msgid "Building name"
msgstr ""
-#: src/components/form/InputLocation.tsx:36
+#: src/components/form/InputLocation.tsx:42
#, c-format
msgid "Street"
msgstr ""
-#: src/components/form/InputLocation.tsx:37
+#: src/components/form/InputLocation.tsx:43
#, c-format
msgid "Post code"
msgstr ""
-#: src/components/form/InputLocation.tsx:38
+#: src/components/form/InputLocation.tsx:44
#, c-format
msgid "Town location"
msgstr ""
-#: src/components/form/InputLocation.tsx:39
+#: src/components/form/InputLocation.tsx:45
#, c-format
msgid "Town"
msgstr ""
-#: src/components/form/InputLocation.tsx:40
+#: src/components/form/InputLocation.tsx:46
#, c-format
msgid "District"
msgstr ""
-#: src/components/form/InputLocation.tsx:41
+#: src/components/form/InputLocation.tsx:49
#, c-format
msgid "Country subdivision"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:59
+#: src/components/form/InputSearchProduct.tsx:66
#, c-format
msgid "Product id"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:60
-#: src/components/product/ProductForm.tsx:99
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:122
-#: src/paths/instance/orders/list/Table.tsx:227
-#: src/paths/instance/products/list/Table.tsx:86
+#: src/components/form/InputSearchProduct.tsx:69
#, c-format
msgid "Description"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:73
-#: src/components/form/InputTaxes.tsx:81
-#: src/paths/admin/create/CreatePage.tsx:87 src/paths/admin/list/Table.tsx:110
-#: src/paths/instance/details/DetailPage.tsx:76
-#: src/paths/instance/update/UpdatePage.tsx:106
+#: src/components/form/InputSearchProduct.tsx:94
#, c-format
-msgid "Name"
+msgid "Product"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:102
+#: src/components/form/InputSearchProduct.tsx:95
#, c-format
-msgid "loading..."
+msgid "search products by it's description or id"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:108
+#: src/components/form/InputSearchProduct.tsx:151
#, c-format
-msgid "no products found"
+msgid "no products found with that description"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:116
+#: src/components/product/InventoryProductForm.tsx:56
#, c-format
-msgid "no results"
+msgid "You must enter a valid product identifier."
msgstr ""
-#: src/components/form/InputSecured.tsx:33
+#: src/components/product/InventoryProductForm.tsx:64
#, c-format
-msgid "Deleting"
+msgid "Quantity must be greater than 0!"
msgstr ""
-#: src/components/form/InputSecured.tsx:34
+#: src/components/product/InventoryProductForm.tsx:76
#, c-format
-msgid "Changing"
+msgid ""
+"This quantity exceeds remaining stock. Currently, only %1$s units remain "
+"unreserved in stock."
msgstr ""
-#: src/components/form/InputSecured.tsx:60
+#: src/components/product/InventoryProductForm.tsx:109
#, c-format
-msgid "Manage token"
+msgid "Quantity"
msgstr ""
-#: src/components/form/InputSecured.tsx:83
+#: src/components/product/InventoryProductForm.tsx:110
#, c-format
-msgid "Update"
+msgid "how many products will be added"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:117
+#, c-format
+msgid "Add from inventory"
+msgstr ""
+
+#: src/components/form/InputImage.tsx:105
+#, c-format
+msgid "Image should be smaller than 1 MB"
+msgstr ""
+
+#: src/components/form/InputImage.tsx:110
+#, c-format
+msgid "Add"
msgstr ""
-#: src/components/form/InputSecured.tsx:100
-#: src/paths/instance/orders/create/CreatePage.tsx:252
-#: src/paths/instance/orders/create/CreatePage.tsx:273
+#: src/components/form/InputImage.tsx:115
#, c-format
msgid "Remove"
msgstr ""
-#: src/components/form/InputSecured.tsx:106 src/components/modal/index.tsx:52
-#: src/components/modal/index.tsx:73 src/paths/admin/create/CreatePage.tsx:114
-#: src/paths/instance/orders/create/CreatePage.tsx:324
-#: src/paths/instance/products/create/CreatePage.tsx:50
-#: src/paths/instance/products/list/Table.tsx:166
-#: src/paths/instance/products/list/Table.tsx:218
-#: src/paths/instance/products/update/UpdatePage.tsx:54
-#: src/paths/instance/transfers/create/CreatePage.tsx:88
-#: src/paths/instance/update/UpdatePage.tsx:133
+#: src/components/form/InputTaxes.tsx:113
#, c-format
-msgid "Cancel"
+msgid "No taxes configured for this product."
msgstr ""
-#: src/components/form/InputStock.tsx:91
+#: src/components/form/InputTaxes.tsx:119
#, c-format
-msgid "Manage stock"
+msgid "Amount"
msgstr ""
-#: src/components/form/InputStock.tsx:93
+#: src/components/form/InputTaxes.tsx:120
#, c-format
-msgid "Infinite"
+msgid ""
+"Taxes can be in currencies that differ from the main currency used by the "
+"merchant."
msgstr ""
-#: src/components/form/InputStock.tsx:105
+#: src/components/form/InputTaxes.tsx:122
#, c-format
-msgid "lost cannot be greater that current + incoming (max %1$s)"
+msgid "Enter currency and value separated with a colon, e.g. &quot;USD:2.3&quot;."
msgstr ""
-#: src/components/form/InputStock.tsx:111
+#: src/components/form/InputTaxes.tsx:131
#, c-format
-msgid "current stock will change from %1$s to %2$s"
+msgid "Legal name of the tax, e.g. VAT or import duties."
msgstr ""
-#: src/components/form/InputStock.tsx:112
+#: src/components/form/InputTaxes.tsx:137
#, c-format
-msgid "current stock will stay at %1$s"
+msgid "add tax to the tax list"
msgstr ""
-#: src/components/form/InputStock.tsx:129
-#: src/paths/instance/products/list/Table.tsx:204
+#: src/components/product/NonInventoryProductForm.tsx:72
#, c-format
-msgid "Incoming"
+msgid "describe and add a product that is not in the inventory list"
msgstr ""
-#: src/components/form/InputStock.tsx:130
-#: src/paths/instance/products/list/Table.tsx:205
+#: src/components/product/NonInventoryProductForm.tsx:75
#, c-format
-msgid "Lost"
+msgid "Add custom product"
msgstr ""
-#: src/components/form/InputStock.tsx:142
+#: src/components/product/NonInventoryProductForm.tsx:86
#, c-format
-msgid "Current"
+msgid "Complete information of the product"
msgstr ""
-#: src/components/form/InputStock.tsx:145
+#: src/components/product/NonInventoryProductForm.tsx:185
#, c-format
-msgid "without stock"
+msgid "Image"
msgstr ""
-#: src/components/form/InputStock.tsx:150
+#: src/components/product/NonInventoryProductForm.tsx:186
#, c-format
-msgid "Next restock"
+msgid "photo of the product"
msgstr ""
-#: src/components/form/InputStock.tsx:152
+#: src/components/product/NonInventoryProductForm.tsx:192
#, c-format
-msgid "Delivery address"
+msgid "full product description"
msgstr ""
-#: src/components/form/InputTaxes.tsx:73
+#: src/components/product/NonInventoryProductForm.tsx:196
#, c-format
-msgid "this product has no taxes"
+msgid "Unit"
msgstr ""
-#: src/components/form/InputTaxes.tsx:77
-#: src/paths/instance/orders/details/DetailPage.tsx:145
-#: src/paths/instance/orders/details/DetailPage.tsx:296
-#: src/paths/instance/orders/list/Table.tsx:116
-#: src/paths/instance/transfers/create/CreatePage.tsx:84
+#: src/components/product/NonInventoryProductForm.tsx:197
#, c-format
-msgid "Amount"
+msgid "name of the product unit"
msgstr ""
-#: src/components/form/InputTaxes.tsx:78
+#: src/components/product/NonInventoryProductForm.tsx:201
#, c-format
-msgid "currency and value separated with colon"
+msgid "Price"
msgstr ""
-#: src/components/form/InputTaxes.tsx:84
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:78
+#: src/components/product/NonInventoryProductForm.tsx:202
#, c-format
-msgid "Add"
+msgid "amount in the current currency"
msgstr ""
-#: src/components/menu/SideBar.tsx:53
+#: src/components/product/NonInventoryProductForm.tsx:211
#, c-format
-msgid "Instance"
+msgid "Taxes"
msgstr ""
-#: src/components/menu/SideBar.tsx:59
+#: src/components/product/ProductList.tsx:38
#, c-format
-msgid "Settings"
+msgid "image"
msgstr ""
-#: src/components/menu/SideBar.tsx:65
-#: src/paths/instance/orders/list/Table.tsx:60
+#: src/components/product/ProductList.tsx:41
#, c-format
-msgid "Orders"
+msgid "description"
msgstr ""
-#: src/components/menu/SideBar.tsx:71
-#: src/paths/instance/orders/create/CreatePage.tsx:258
-#: src/paths/instance/products/list/Table.tsx:48
+#: src/components/product/ProductList.tsx:44
#, c-format
-msgid "Products"
+msgid "quantity"
msgstr ""
-#: src/components/menu/SideBar.tsx:77
-#: src/paths/instance/transfers/list/Table.tsx:65
+#: src/components/product/ProductList.tsx:47
#, c-format
-msgid "Transfers"
+msgid "unit price"
msgstr ""
-#: src/components/menu/SideBar.tsx:87
+#: src/components/product/ProductList.tsx:50
#, c-format
-msgid "Connection"
+msgid "total price"
msgstr ""
-#: src/components/menu/SideBar.tsx:112 src/paths/admin/list/Table.tsx:57
+#: src/paths/instance/orders/create/CreatePage.tsx:153
#, c-format
-msgid "Instances"
+msgid "required"
msgstr ""
-#: src/components/menu/SideBar.tsx:116
+#: src/paths/instance/orders/create/CreatePage.tsx:157
#, c-format
-msgid "New"
+msgid "not valid"
msgstr ""
-#: src/components/menu/SideBar.tsx:122
+#: src/paths/instance/orders/create/CreatePage.tsx:159
#, c-format
-msgid "List"
+msgid "must be greater than 0"
msgstr ""
-#: src/components/menu/SideBar.tsx:129
+#: src/paths/instance/orders/create/CreatePage.tsx:164
#, c-format
-msgid "Log out"
+msgid "not a valid json"
msgstr ""
-#: src/components/modal/index.tsx:74
+#: src/paths/instance/orders/create/CreatePage.tsx:170
#, c-format
-msgid "Clear"
+msgid "should be in the future"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:173
+#, c-format
+msgid "refund deadline cannot be before pay deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:179
+#, c-format
+msgid "wire transfer deadline cannot be before refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:190
+#, c-format
+msgid "wire transfer deadline cannot be before pay deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:197
+#, c-format
+msgid "should have a refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:202
+#, c-format
+msgid "auto refund cannot be after refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:360
+#, c-format
+msgid "Manage products in order"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:369
+#, c-format
+msgid "Manage list of products in the order."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:391
+#, c-format
+msgid "Remove this product from the order."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:415
+#, c-format
+msgid "Total price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:417
+#, c-format
+msgid "total product price added up"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:430
+#, c-format
+msgid "Amount to be paid by the customer"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:436
+#, c-format
+msgid "Order price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:437
+#, c-format
+msgid "final order price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:444
+#, c-format
+msgid "Summary"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:445
+#, c-format
+msgid "Title of the order to be shown to the customer"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:450
+#, c-format
+msgid "Shipping and Fulfillment"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:455
+#, c-format
+msgid "Delivery date"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:456
+#, c-format
+msgid "Deadline for physical delivery assured by the merchant."
msgstr ""
-#: src/components/modal/index.tsx:110 src/components/modal/index.tsx:111
+#: src/paths/instance/orders/create/CreatePage.tsx:461
#, c-format
-msgid "should be the same"
+msgid "Location"
msgstr ""
-#: src/components/modal/index.tsx:111
+#: src/paths/instance/orders/create/CreatePage.tsx:462
#, c-format
-msgid "cannot be the same as before"
+msgid "address where the products will be delivered"
msgstr ""
-#: src/components/modal/index.tsx:114
+#: src/paths/instance/orders/create/CreatePage.tsx:469
+#, c-format
+msgid "Fulfillment URL"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:470
+#, c-format
+msgid "URL to which the user will be redirected after successful payment."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:476
+#, c-format
+msgid "Taler payment options"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:477
+#, c-format
+msgid "Override default Taler payment settings for this order"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:481
+#, c-format
+msgid "Payment deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:482
#, c-format
msgid ""
-"You are updating the authorization token from instance %1$s with id %2$s"
+"Deadline for the customer to pay for the offer before it expires. Inventory "
+"products will be reserved until this deadline."
msgstr ""
-#: src/components/modal/index.tsx:124
+#: src/paths/instance/orders/create/CreatePage.tsx:486
#, c-format
-msgid "Old token"
+msgid "Refund deadline"
msgstr ""
-#: src/components/modal/index.tsx:125
+#: src/paths/instance/orders/create/CreatePage.tsx:487
#, c-format
-msgid "New token"
+msgid "Time until which the order can be refunded by the merchant."
msgstr ""
-#: src/components/modal/index.tsx:127
+#: src/paths/instance/orders/create/CreatePage.tsx:491
#, c-format
-msgid "Clearing the auth token will mean public access to the instance"
+msgid "Wire transfer deadline"
msgstr ""
-#: src/components/product/ProductForm.tsx:96
-#: src/paths/admin/create/CreatePage.tsx:85 src/paths/admin/list/Table.tsx:109
-#: src/paths/instance/transfers/list/Table.tsx:122
+#: src/paths/instance/orders/create/CreatePage.tsx:492
#, c-format
-msgid "ID"
+msgid "Deadline for the exchange to make the wire transfer."
msgstr ""
-#: src/components/product/ProductForm.tsx:98
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:121
-#: src/paths/instance/products/list/Table.tsx:85
+#: src/paths/instance/orders/create/CreatePage.tsx:496
#, c-format
-msgid "Image"
+msgid "Auto-refund deadline"
msgstr ""
-#: src/components/product/ProductForm.tsx:100
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:123
+#: src/paths/instance/orders/create/CreatePage.tsx:497
#, c-format
-msgid "Unit"
+msgid ""
+"Time until which the wallet will automatically check for refunds without user "
+"interaction."
msgstr ""
-#: src/components/product/ProductForm.tsx:101
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:124
-#: src/paths/instance/products/list/Table.tsx:162
-#: src/paths/instance/products/list/Table.tsx:214
+#: src/paths/instance/orders/create/CreatePage.tsx:502
#, c-format
-msgid "Price"
+msgid "Maximum deposit fee"
msgstr ""
-#: src/components/product/ProductForm.tsx:103
-#: src/paths/instance/products/list/Table.tsx:90
+#: src/paths/instance/orders/create/CreatePage.tsx:503
#, c-format
-msgid "Stock"
+msgid ""
+"Maximum deposit fees the merchant is willing to cover for this order. Higher "
+"deposit fees must be covered in full by the consumer."
msgstr ""
-#: src/components/product/ProductForm.tsx:105
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:128
-#: src/paths/instance/products/list/Table.tsx:88
+#: src/paths/instance/orders/create/CreatePage.tsx:507
#, c-format
-msgid "Taxes"
+msgid "Maximum wire fee"
msgstr ""
-#: src/index.tsx:75
+#: src/paths/instance/orders/create/CreatePage.tsx:508
#, c-format
-msgid "Server not found"
+msgid ""
+"Maximum aggregate wire fees the merchant is willing to cover for this order. "
+"Wire fees exceeding this amount are to be covered by the customers."
msgstr ""
-#: src/index.tsx:85
+#: src/paths/instance/orders/create/CreatePage.tsx:512
#, c-format
-msgid "Couldn't access the server"
+msgid "Wire fee amortization"
msgstr ""
-#: src/index.tsx:87 src/index.tsx:99
+#: src/paths/instance/orders/create/CreatePage.tsx:513
#, c-format
-msgid "Got message %1$s from %2$s"
+msgid ""
+"Factor by which wire fees exceeding the above threshold are divided to determine "
+"the share of excess wire fees to be paid explicitly by the consumer."
msgstr ""
-#: src/index.tsx:97
+#: src/paths/instance/orders/create/CreatePage.tsx:517
#, c-format
-msgid "Unexpected Error"
+msgid "Create token"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:89
-#: src/paths/instance/update/UpdatePage.tsx:108
+#: src/paths/instance/orders/create/CreatePage.tsx:518
#, c-format
-msgid "Auth token"
+msgid ""
+"Uncheck this option if the merchant backend generated an order ID with enough "
+"entropy to prevent adversarial claims."
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:91
-#: src/paths/instance/details/DetailPage.tsx:77
-#: src/paths/instance/update/UpdatePage.tsx:110
+#: src/paths/instance/orders/create/CreatePage.tsx:522
#, c-format
-msgid "Account address"
+msgid "Minimum age required"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:93
-#: src/paths/instance/update/UpdatePage.tsx:112
+#: src/paths/instance/orders/create/CreatePage.tsx:523
#, c-format
-msgid "Default max deposit fee"
+msgid ""
+"Any value greater than 0 will limit the coins able be used to pay this contract. "
+"If empty the age restriction will be defined by the products"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:95
-#: src/paths/instance/update/UpdatePage.tsx:114
+#: src/paths/instance/orders/create/CreatePage.tsx:526
#, c-format
-msgid "Default max wire fee"
+msgid "Min age defined by the producs is %1$s"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:97
-#: src/paths/instance/update/UpdatePage.tsx:116
+#: src/paths/instance/orders/create/CreatePage.tsx:534
#, c-format
-msgid "Default wire fee amortization"
+msgid "Additional information"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:103
-#: src/paths/instance/update/UpdatePage.tsx:122
+#: src/paths/instance/orders/create/CreatePage.tsx:535
#, c-format
-msgid "Jurisdiction"
+msgid "Custom information to be included in the contract for this order."
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:107
-#: src/paths/instance/update/UpdatePage.tsx:126
+#: src/paths/instance/orders/create/CreatePage.tsx:541
#, c-format
-msgid "Default pay delay"
+msgid "You must enter a value in JavaScript Object Notation (JSON)."
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:109
-#: src/paths/instance/update/UpdatePage.tsx:128
+#: src/components/picker/DurationPicker.tsx:55
#, c-format
-msgid "Default wire transfer delay"
+msgid "days"
msgstr ""
-#: src/paths/admin/create/index.tsx:58
+#: src/components/picker/DurationPicker.tsx:65
#, c-format
-msgid "could not create instance"
+msgid "hours"
msgstr ""
-#: src/paths/admin/list/Table.tsx:63 src/paths/admin/list/Table.tsx:131
-#: src/paths/instance/transfers/list/Table.tsx:71
+#: src/components/picker/DurationPicker.tsx:76
#, c-format
-msgid "Delete"
+msgid "minutes"
msgstr ""
-#: src/paths/admin/list/Table.tsx:128
+#: src/components/picker/DurationPicker.tsx:87
#, c-format
-msgid "Edit"
+msgid "seconds"
msgstr ""
-#: src/paths/admin/list/Table.tsx:149
-#: src/paths/instance/products/list/Table.tsx:245
+#: src/components/form/InputDuration.tsx:53
#, c-format
-msgid "There is no instances yet, add more pressing the + sign"
+msgid "forever"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:237
+#: src/components/form/InputDuration.tsx:62
#, c-format
-msgid "Inventory products"
+msgid "%1$sM"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:286
+#: src/components/form/InputDuration.tsx:64
#, c-format
-msgid "Total price"
+msgid "%1$sY"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:287
+#: src/components/form/InputDuration.tsx:66
#, c-format
-msgid "Total tax"
+msgid "%1$sd"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:289
-#: src/paths/instance/orders/create/CreatePage.tsx:297
+#: src/components/form/InputDuration.tsx:68
#, c-format
-msgid "Order price"
+msgid "%1$sh"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:295
+#: src/components/form/InputDuration.tsx:70
#, c-format
-msgid "Net"
+msgid "%1$smin"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:300
-#: src/paths/instance/orders/details/DetailPage.tsx:144
-#: src/paths/instance/orders/details/DetailPage.tsx:295
-#: src/paths/instance/orders/list/Table.tsx:117
+#: src/components/form/InputDuration.tsx:72
#, c-format
-msgid "Summary"
+msgid "%1$ssec"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:302
+#: src/paths/instance/orders/list/Table.tsx:75
#, c-format
-msgid "Payments options"
+msgid "Orders"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:303
+#: src/paths/instance/orders/list/Table.tsx:81
#, c-format
-msgid "Auto refund deadline"
+msgid "create order"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:304
+#: src/paths/instance/orders/list/Table.tsx:147
#, c-format
-msgid "Refund deadline"
+msgid "load newer orders"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:305
+#: src/paths/instance/orders/list/Table.tsx:154
#, c-format
-msgid "Pay deadline"
+msgid "Date"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:307
+#: src/paths/instance/orders/list/Table.tsx:200
#, c-format
-msgid "Delivery date"
+msgid "Refund"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:308
+#: src/paths/instance/orders/list/Table.tsx:209
#, c-format
-msgid "Location"
+msgid "copy url"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:225
+#, c-format
+msgid "load older orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:242
+#, c-format
+msgid "No orders have been found matching your query!"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:288
+#, c-format
+msgid "duplicated"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:299
+#, c-format
+msgid "invalid format"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:301
+#, c-format
+msgid "this value exceed the refundable amount"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:346
+#, c-format
+msgid "date"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:349
+#, c-format
+msgid "amount"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:352
+#, c-format
+msgid "reason"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:389
+#, c-format
+msgid "amount to be refunded"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:391
+#, c-format
+msgid "Max refundable:"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:396
+#, c-format
+msgid "Reason"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:397
+#, c-format
+msgid "Choose one..."
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:399
+#, c-format
+msgid "requested by the customer"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:400
+#, c-format
+msgid "other"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:403
+#, c-format
+msgid "why this order is being refunded"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:409
+#, c-format
+msgid "more information to give context"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:62
+#, c-format
+msgid "Contract Terms"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:312
+#: src/paths/instance/orders/details/DetailPage.tsx:68
+#, c-format
+msgid "human-readable description of the whole purchase"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:74
+#, c-format
+msgid "total price for the transaction"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:81
+#, c-format
+msgid "URL for this purchase"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:87
#, c-format
msgid "Max fee"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:313
+#: src/paths/instance/orders/details/DetailPage.tsx:88
+#, c-format
+msgid "maximum total deposit fee accepted by the merchant for this contract"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:93
#, c-format
msgid "Max wire fee"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:314
+#: src/paths/instance/orders/details/DetailPage.tsx:94
#, c-format
-msgid "Wire fee amortization"
+msgid "maximum wire fee accepted by the merchant"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:315
+#: src/paths/instance/orders/details/DetailPage.tsx:100
#, c-format
-msgid "Fullfilment url"
+msgid ""
+"over how many customer transactions does the merchant expect to amortize wire "
+"fees on average"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:318
+#: src/paths/instance/orders/details/DetailPage.tsx:105
#, c-format
-msgid "Extra information"
+msgid "Created at"
msgstr ""
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:44
+#: src/paths/instance/orders/details/DetailPage.tsx:106
#, c-format
-msgid "select a product first"
+msgid "time when this contract was generated"
msgstr ""
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:51
+#: src/paths/instance/orders/details/DetailPage.tsx:112
#, c-format
-msgid "should be greater than 0"
+msgid "after this deadline has passed no refunds will be accepted"
msgstr ""
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:58
+#: src/paths/instance/orders/details/DetailPage.tsx:118
#, c-format
-msgid ""
-"cannot be greater than current stock and quantity previously added. max: %1$s"
+msgid "after this deadline, the merchant won't accept payments for the contract"
msgstr ""
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:64
+#: src/paths/instance/orders/details/DetailPage.tsx:124
#, c-format
-msgid "cannot be greater than current stock %1$s"
+msgid "transfer deadline for the exchange"
msgstr ""
-#: src/paths/instance/orders/create/InventoryProductForm.tsx:76
-#: src/paths/instance/orders/create/NonInventoryProductForm.tsx:126
+#: src/paths/instance/orders/details/DetailPage.tsx:130
#, c-format
-msgid "Quantity"
+msgid "time indicating when the order should be delivered"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:92
-#: src/paths/instance/orders/details/DetailPage.tsx:235
-#: src/paths/instance/orders/details/DetailPage.tsx:333
+#: src/paths/instance/orders/details/DetailPage.tsx:136
#, c-format
-msgid "Order"
+msgid "where the order will be delivered"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:93
+#: src/paths/instance/orders/details/DetailPage.tsx:144
#, c-format
-msgid "claimed"
+msgid "Auto-refund delay"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:110
-#: src/paths/instance/orders/details/DetailPage.tsx:261
-#: src/paths/instance/orders/list/Table.tsx:136
+#: src/paths/instance/orders/details/DetailPage.tsx:145
#, c-format
-msgid "copy url"
+msgid "how long the wallet should try to get an automatic refund for the purchase"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:126
-#: src/paths/instance/orders/details/DetailPage.tsx:349
+#: src/paths/instance/orders/details/DetailPage.tsx:150
#, c-format
-msgid "pay at"
+msgid "Extra info"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:127
-#: src/paths/instance/orders/details/DetailPage.tsx:350
+#: src/paths/instance/orders/details/DetailPage.tsx:151
#, c-format
-msgid "created at"
+msgid "extra data that is only interpreted by the merchant frontend"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:219
+#, c-format
+msgid "Order"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:221
+#, c-format
+msgid "claimed"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:247
+#, c-format
+msgid "claimed at"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:138
-#: src/paths/instance/orders/details/DetailPage.tsx:289
+#: src/paths/instance/orders/details/DetailPage.tsx:265
#, c-format
msgid "Timeline"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:142
-#: src/paths/instance/orders/details/DetailPage.tsx:293
+#: src/paths/instance/orders/details/DetailPage.tsx:271
#, c-format
msgid "Payment details"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:146
-#: src/paths/instance/orders/details/DetailPage.tsx:299
-#: src/paths/instance/orders/details/DetailPage.tsx:363
+#: src/paths/instance/orders/details/DetailPage.tsx:291
#, c-format
msgid "Order status"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:156
-#: src/paths/instance/orders/details/DetailPage.tsx:308
+#: src/paths/instance/orders/details/DetailPage.tsx:301
#, c-format
msgid "Product list"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:236
+#: src/paths/instance/orders/details/DetailPage.tsx:451
#, c-format
msgid "paid"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:238
+#: src/paths/instance/orders/details/DetailPage.tsx:455
#, c-format
msgid "wired"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:241
+#: src/paths/instance/orders/details/DetailPage.tsx:460
#, c-format
msgid "refunded"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:258
+#: src/paths/instance/orders/details/DetailPage.tsx:480
+#, c-format
+msgid "refund order"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:481
+#, c-format
+msgid "not refundable"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:489
#, c-format
msgid "refund"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:297
+#: src/paths/instance/orders/details/DetailPage.tsx:553
#, c-format
msgid "Refunded amount"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:298
+#: src/paths/instance/orders/details/DetailPage.tsx:560
#, c-format
-msgid "Deposit total"
+msgid "Refund taken"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:336
+#: src/paths/instance/orders/details/DetailPage.tsx:570
+#, c-format
+msgid "Status URL"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:583
+#, c-format
+msgid "Refund URI"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:636
#, c-format
msgid "unpaid"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:364
+#: src/paths/instance/orders/details/DetailPage.tsx:654
+#, c-format
+msgid "pay at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:666
+#, c-format
+msgid "created at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:707
#, c-format
msgid "Order status URL"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:365
+#: src/paths/instance/orders/details/DetailPage.tsx:711
#, c-format
-msgid "Pay URI"
+msgid "Payment URI"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:383
+#: src/paths/instance/orders/details/DetailPage.tsx:740
#, c-format
-msgid ""
-"Unknown order status. This is an error, please contact the administrator."
+msgid "Unknown order status. This is an error, please contact the administrator."
msgstr ""
-#: src/paths/instance/orders/details/index.tsx:56
-#: src/paths/instance/orders/list/index.tsx:147
+#: src/paths/instance/orders/details/DetailPage.tsx:767
+#, c-format
+msgid "Back"
+msgstr ""
+
+#: src/paths/instance/orders/details/index.tsx:79
#, c-format
msgid "refund created successfully"
msgstr ""
-#: src/paths/instance/orders/details/index.tsx:59
-#: src/paths/instance/orders/list/index.tsx:150
+#: src/paths/instance/orders/details/index.tsx:85
#, c-format
msgid "could not create the refund"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:111
+#: src/paths/instance/orders/list/ListPage.tsx:78
#, c-format
-msgid "load newer orders"
+msgid "select date to show nearby orders"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:115
+#: src/paths/instance/orders/list/ListPage.tsx:94
#, c-format
-msgid "Date"
+msgid "order id"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:131
-#: src/paths/instance/orders/list/Table.tsx:223
+#: src/paths/instance/orders/list/ListPage.tsx:100
#, c-format
-msgid "Refund"
+msgid "jump to order with the given order ID"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:145
+#: src/paths/instance/orders/list/ListPage.tsx:122
#, c-format
-msgid "load older orders"
+msgid "remove all filters"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:154
+#: src/paths/instance/orders/list/ListPage.tsx:132
#, c-format
-msgid "No orders has been found"
+msgid "only show paid orders"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:202
+#: src/paths/instance/orders/list/ListPage.tsx:135
#, c-format
-msgid "date"
+msgid "Paid"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:203
+#: src/paths/instance/orders/list/ListPage.tsx:142
#, c-format
-msgid "amount"
+msgid "only show orders with refunds"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:204
+#: src/paths/instance/orders/list/ListPage.tsx:145
#, c-format
-msgid "reason"
+msgid "Refunded"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:224
+#: src/paths/instance/orders/list/ListPage.tsx:152
#, c-format
-msgid "Max refundable:"
+msgid ""
+"only show orders where customers paid, but wire payments from payment provider "
+"are still pending"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:226
+#: src/paths/instance/orders/list/ListPage.tsx:155
#, c-format
-msgid "Reason"
+msgid "Not wired"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:226
+#: src/paths/instance/orders/list/ListPage.tsx:170
#, c-format
-msgid "duplicated"
+msgid "clear date filter"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:226
+#: src/paths/instance/orders/list/ListPage.tsx:184
#, c-format
-msgid "requested by the customer"
+msgid "date (YYYY/MM/DD)"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:226
+#: src/paths/instance/orders/list/index.tsx:103
#, c-format
-msgid "other"
+msgid "Enter an order id"
msgstr ""
-#: src/paths/instance/orders/list/index.tsx:91
+#: src/paths/instance/orders/list/index.tsx:111
#, c-format
-msgid "go to order id"
+msgid "order not found"
msgstr ""
-#: src/paths/instance/orders/list/index.tsx:107
+#: src/paths/instance/orders/list/index.tsx:178
#, c-format
-msgid "Paid"
+msgid "could not get the order to refund"
msgstr ""
-#: src/paths/instance/orders/list/index.tsx:108
+#: src/components/exception/AsyncButton.tsx:43
#, c-format
-msgid "Refunded"
+msgid "Loading..."
msgstr ""
-#: src/paths/instance/orders/list/index.tsx:109
+#: src/components/form/InputStock.tsx:99
#, c-format
-msgid "Not wired"
+msgid ""
+"click here to configure the stock of the product, leave it as is and the backend "
+"will not control stock"
msgstr ""
-#: src/paths/instance/orders/list/index.tsx:110
+#: src/components/form/InputStock.tsx:109
#, c-format
-msgid "All"
+msgid "Manage stock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:115
+#, c-format
+msgid "this product has been configured without stock control"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:119
+#, c-format
+msgid "Infinite"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:136
+#, c-format
+msgid "lost cannot be greater than current and incoming (max %1$s)"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:176
+#, c-format
+msgid "Incoming"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:177
+#, c-format
+msgid "Lost"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:192
+#, c-format
+msgid "Current"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:196
+#, c-format
+msgid "remove stock control for this product"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:202
+#, c-format
+msgid "without stock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:211
+#, c-format
+msgid "Next restock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:217
+#, c-format
+msgid "Delivery address"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:133
+#, c-format
+msgid "product identification to use in URLs (for internal use only)"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:139
+#, c-format
+msgid "illustration of the product for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:145
+#, c-format
+msgid "product description for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:149
+#, c-format
+msgid "Age restricted"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:150
+#, c-format
+msgid "is this product restricted for customer below certain age?"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:155
+#, c-format
+msgid ""
+"unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 items, 5 "
+"meters) for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:160
+#, c-format
+msgid "sale price for customers, including taxes, for above units of the product"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:164
+#, c-format
+msgid "Stock"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:166
+#, c-format
+msgid "product inventory for products with finite supply (for internal use only)"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:171
+#, c-format
+msgid "taxes included in the product price, exposed to customers"
msgstr ""
-#: src/paths/instance/products/create/index.tsx:48
-#: src/paths/instance/products/update/index.tsx:64
+#: src/paths/instance/products/create/CreatePage.tsx:66
+#, c-format
+msgid "Need to complete marked fields"
+msgstr ""
+
+#: src/paths/instance/products/create/index.tsx:51
#, c-format
msgid "could not create product"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:87
+#: src/paths/instance/products/list/Table.tsx:68
+#, c-format
+msgid "Products"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:73
+#, c-format
+msgid "add product to inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:137
#, c-format
msgid "Sell"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:89
+#: src/paths/instance/products/list/Table.tsx:143
#, c-format
msgid "Profit"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:91
+#: src/paths/instance/products/list/Table.tsx:149
#, c-format
msgid "Sold"
msgstr ""
-#: src/paths/instance/products/list/index.tsx:59
+#: src/paths/instance/products/list/Table.tsx:210
+#, c-format
+msgid "free"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:248
+#, c-format
+msgid "go to product update page"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:255
+#, c-format
+msgid "Update"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:260
+#, c-format
+msgid "remove this product from the database"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:331
+#, c-format
+msgid "update the product with new price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:341
+#, c-format
+msgid "update product with new price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:399
+#, c-format
+msgid "add more elements to the inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:404
+#, c-format
+msgid "report elements lost in the inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:409
+#, c-format
+msgid "new price for the product"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:421
+#, c-format
+msgid "the are value with errors"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:422
+#, c-format
+msgid "update product with new stock and price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:463
+#, c-format
+msgid "There is no products yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:86
#, c-format
msgid "product updated successfully"
msgstr ""
-#: src/paths/instance/products/list/index.tsx:62
+#: src/paths/instance/products/list/index.tsx:92
#, c-format
msgid "could not update the product"
msgstr ""
-#: src/paths/instance/products/list/index.tsx:70
+#: src/paths/instance/products/list/index.tsx:103
#, c-format
msgid "product delete successfully"
msgstr ""
-#: src/paths/instance/products/list/index.tsx:73
+#: src/paths/instance/products/list/index.tsx:109
#, c-format
msgid "could not delete the product"
msgstr ""
-#: src/paths/instance/tips/list/Table.tsx:59
+#: src/paths/instance/products/update/UpdatePage.tsx:56
+#, c-format
+msgid "Product id:"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:95
+#, c-format
+msgid ""
+"To complete the setup of the reserve, you must now initiate a wire transfer "
+"using the given wire transfer subject and crediting the specified amount to the "
+"indicated account of the exchange."
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:102
+#, c-format
+msgid "If your system supports RFC 8905, you can do this by opening this URI:"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:83
+#, c-format
+msgid "it should be greater than 0"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:88
+#, c-format
+msgid "must be a valid URL"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:107
+#, c-format
+msgid "Initial balance"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:108
+#, c-format
+msgid "balance prior to deposit"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:112
+#, c-format
+msgid "Exchange URL"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:113
+#, c-format
+msgid "URL of exchange"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:148
+#, c-format
+msgid "Next"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:186
+#, c-format
+msgid "Wire method"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:187
+#, c-format
+msgid "method to use for wire transfer"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:189
+#, c-format
+msgid "Select one wire method"
+msgstr ""
+
+#: src/paths/instance/reserves/create/index.tsx:62
+#, c-format
+msgid "could not create reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:77
+#, c-format
+msgid "Valid until"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:82
+#, c-format
+msgid "Created balance"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:99
+#, c-format
+msgid "Exchange balance"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:104
+#, c-format
+msgid "Picked up"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:109
+#, c-format
+msgid "Committed"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:116
+#, c-format
+msgid "Account address"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:119
+#, c-format
+msgid "Subject"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:130
#, c-format
msgid "Tips"
msgstr ""
-#: src/paths/instance/tips/list/Table.tsx:111
+#: src/paths/instance/reserves/details/DetailPage.tsx:193
#, c-format
-msgid "Committed amount"
+msgid "No tips has been authorized from this reserve"
msgstr ""
-#: src/paths/instance/tips/list/Table.tsx:112
+#: src/paths/instance/reserves/details/DetailPage.tsx:213
#, c-format
-msgid "Exchange initial amount"
+msgid "Authorized"
msgstr ""
-#: src/paths/instance/tips/list/Table.tsx:113
+#: src/paths/instance/reserves/details/DetailPage.tsx:222
#, c-format
-msgid "Merchant initial amount"
+msgid "Expiration"
msgstr ""
-#: src/paths/instance/tips/list/Table.tsx:148
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:108
#, c-format
-msgid "There is no tips yet, add more pressing the + sign"
+msgid "amount of tip"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:50
-#: src/paths/instance/transfers/create/CreatePage.tsx:54
-#: src/paths/instance/transfers/create/CreatePage.tsx:55
-#: src/paths/instance/transfers/create/CreatePage.tsx:56
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:112
#, c-format
-msgid "cannot be empty"
+msgid "Justification"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:114
+#, c-format
+msgid "reason for the tip"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:118
+#, c-format
+msgid "URL after tip"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:119
+#, c-format
+msgid "URL to visit after tip payment"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:65
+#, c-format
+msgid "Reserves not yet funded"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:89
+#, c-format
+msgid "Reserves ready"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:95
+#, c-format
+msgid "add new reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:143
+#, c-format
+msgid "Expires at"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:146
+#, c-format
+msgid "Initial"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:202
+#, c-format
+msgid "delete selected reserve from the database"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:210
+#, c-format
+msgid "authorize new tip from selected reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:237
+#, c-format
+msgid "There is no ready reserves yet, add more pressing the + sign or fund them"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:264
+#, c-format
+msgid "Expected Balance"
+msgstr ""
+
+#: src/paths/instance/reserves/list/index.tsx:110
+#, c-format
+msgid "could not create the tip"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:77
+#, c-format
+msgid "should not be empty"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:93
+#, c-format
+msgid "should be greater that 0"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:96
+#, c-format
+msgid "can't be empty"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:100
+#, c-format
+msgid "to short"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:108
+#, c-format
+msgid "just letters and numbers from 2 to 7"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:110
+#, c-format
+msgid "size of the key should be 32"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:137
+#, c-format
+msgid "Identifier"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:138
+#, c-format
+msgid "Name of the template in URLs."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:144
+#, c-format
+msgid "Describe what this template stands for"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:149
+#, c-format
+msgid "Fixed summary"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:150
+#, c-format
+msgid "If specified, this template will create order with the same summary"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:154
+#, c-format
+msgid "Fixed price"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:155
+#, c-format
+msgid "If specified, this template will create order with the same price"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:159
+#, c-format
+msgid "Minimum age"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:161
+#, c-format
+msgid "Is this contract restricted to some age?"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:165
+#, c-format
+msgid "Payment timeout"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:167
+#, c-format
+msgid ""
+"How much time has the customer to complete the payment once the order was "
+"created."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:171
+#, c-format
+msgid "Verification algorithm"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:172
+#, c-format
+msgid "Algorithm to use to verify transaction in offline mode"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:180
+#, c-format
+msgid "Point-of-sale key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:182
+#, c-format
+msgid "Useful to validate the purchase"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:196
+#, c-format
+msgid "generate random secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:203
+#, c-format
+msgid "random"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:208
+#, c-format
+msgid "show secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:209
+#, c-format
+msgid "hide secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:216
+#, c-format
+msgid "hide"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:218
+#, c-format
+msgid "show"
+msgstr ""
+
+#: src/paths/instance/templates/create/index.tsx:52
+#, c-format
+msgid "could not inform template"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:54
+#, c-format
+msgid "Amount is required"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:58
+#, c-format
+msgid "Order summary is required"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:86
+#, c-format
+msgid "New order for template"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:108
+#, c-format
+msgid "Amount of the order"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:113
+#, c-format
+msgid "Order summary"
+msgstr ""
+
+#: src/paths/instance/templates/use/index.tsx:92
+#, c-format
+msgid "could not create order from template"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:131
+#, c-format
+msgid ""
+"Here you can specify a default value for fields that are not fixed. Default "
+"values can be edited by the customer before the payment."
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:148
+#, c-format
+msgid "Fixed amount"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:149
+#, c-format
+msgid "Default amount"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:161
+#, c-format
+msgid "Default summary"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:177
+#, c-format
+msgid "Print"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:184
+#, c-format
+msgid "Setup TOTP"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:65
+#, c-format
+msgid "Templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:70
+#, c-format
+msgid "add new templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:142
+#, c-format
+msgid "load more templates before the first one"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:146
+#, c-format
+msgid "load newer templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:181
+#, c-format
+msgid "delete selected templates from the database"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:188
+#, c-format
+msgid "use template to create new order"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:195
+#, c-format
+msgid "create qr code for the template"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:210
+#, c-format
+msgid "load more templates after the last one"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:214
+#, c-format
+msgid "load older templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:231
+#, c-format
+msgid "There is no templates yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/templates/list/index.tsx:104
+#, c-format
+msgid "template delete successfully"
+msgstr ""
+
+#: src/paths/instance/templates/list/index.tsx:110
+#, c-format
+msgid "could not delete the template"
+msgstr ""
+
+#: src/paths/instance/templates/update/index.tsx:90
+#, c-format
+msgid "could not update template"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:57
+#, c-format
+msgid "should be one of '%1$s'"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:85
+#, c-format
+msgid "Webhook ID to use"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:89
+#, c-format
+msgid "Event"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:90
+#, c-format
+msgid "The event of the webhook: why the webhook is used"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:94
+#, c-format
+msgid "Method"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:95
+#, c-format
+msgid "Method used by the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:99
+#, c-format
+msgid "URL"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:100
+#, c-format
+msgid "URL of the webhook where the customer will be redirected"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:104
+#, c-format
+msgid "Header"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:106
+#, c-format
+msgid "Header template of the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:111
+#, c-format
+msgid "Body"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:112
+#, c-format
+msgid "Body template by the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:61
+#, c-format
+msgid "Webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:66
+#, c-format
+msgid "add new webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:137
+#, c-format
+msgid "load more webhooks before the first one"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:141
+#, c-format
+msgid "load newer webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:151
+#, c-format
+msgid "Event type"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:176
+#, c-format
+msgid "delete selected webhook from the database"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:198
+#, c-format
+msgid "load more webhooks after the last one"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:202
+#, c-format
+msgid "load older webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:219
+#, c-format
+msgid "There is no webhooks yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/index.tsx:94
+#, c-format
+msgid "webhook delete successfully"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:51
+#: src/paths/instance/webhooks/list/index.tsx:100
#, c-format
-msgid "check the id, doest look valid"
+msgid "could not delete the webhook"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:52
+#: src/paths/instance/transfers/create/CreatePage.tsx:63
+#, c-format
+msgid "check the id, does not look valid"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:65
#, c-format
msgid "should have 52 characters, current %1$s"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:57
+#: src/paths/instance/transfers/create/CreatePage.tsx:72
#, c-format
msgid "URL doesn't have the right format"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:74
+#: src/paths/instance/transfers/create/CreatePage.tsx:98
#, c-format
-msgid "Transfer ID"
+msgid "Credited bank account"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:76
+#: src/paths/instance/transfers/create/CreatePage.tsx:100
#, c-format
-msgid "Account Address"
+msgid "Select one account"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:82
-#: src/paths/instance/transfers/list/Table.tsx:125
+#: src/paths/instance/transfers/create/CreatePage.tsx:101
#, c-format
-msgid "Exchange URL"
+msgid "Bank account of the merchant where the payment was received"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:105
+#, c-format
+msgid "Wire transfer ID"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:107
+#, c-format
+msgid ""
+"unique identifier of the wire transfer used by the exchange, must be 52 "
+"characters long"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:112
+#, c-format
+msgid ""
+"Base URL of the exchange that made the transfer, should have been in the wire "
+"transfer subject"
msgstr ""
-#: src/paths/instance/transfers/create/index.tsx:49
+#: src/paths/instance/transfers/create/CreatePage.tsx:117
+#, c-format
+msgid "Amount credited"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:118
+#, c-format
+msgid "Actual amount that was wired to the merchant's bank account"
+msgstr ""
+
+#: src/paths/instance/transfers/create/index.tsx:58
#, c-format
msgid "could not inform transfer"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:118
+#: src/paths/instance/transfers/list/Table.tsx:61
+#, c-format
+msgid "Transfers"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:66
+#, c-format
+msgid "add new transfer"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:129
+#, c-format
+msgid "load more transfers before the first one"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:133
#, c-format
msgid "load newer transfers"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:123
+#: src/paths/instance/transfers/list/Table.tsx:143
#, c-format
msgid "Credit"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:126
+#: src/paths/instance/transfers/list/Table.tsx:152
#, c-format
msgid "Confirmed"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:127
-#: src/paths/instance/transfers/list/index.tsx:60
+#: src/paths/instance/transfers/list/Table.tsx:155
#, c-format
msgid "Verified"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:128
+#: src/paths/instance/transfers/list/Table.tsx:158
#, c-format
msgid "Executed at"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:138
-#: src/paths/instance/transfers/list/Table.tsx:139
+#: src/paths/instance/transfers/list/Table.tsx:171
#, c-format
msgid "yes"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:138
-#: src/paths/instance/transfers/list/Table.tsx:139
+#: src/paths/instance/transfers/list/Table.tsx:171
#, c-format
msgid "no"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:140
+#: src/paths/instance/transfers/list/Table.tsx:181
#, c-format
msgid "unknown"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:145
+#: src/paths/instance/transfers/list/Table.tsx:187
+#, c-format
+msgid "delete selected transfer from the database"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:202
+#, c-format
+msgid "load more transfer after the last one"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:206
#, c-format
msgid "load older transfers"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:154
+#: src/paths/instance/transfers/list/Table.tsx:223
#, c-format
msgid "There is no transfer yet, add more pressing the + sign"
msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:79
+#, c-format
+msgid "filter by account address"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:100
+#, c-format
+msgid "only show wire transfers confirmed by the merchant"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:110
+#, c-format
+msgid "only show wire transfers claimed by the exchange"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:113
+#, c-format
+msgid "Unverified"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:69
+#, c-format
+msgid "is not valid"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:94
+#, c-format
+msgid "is not a number"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:96
+#, c-format
+msgid "must be 1 or greater"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:107
+#, c-format
+msgid "max 7 lines"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:178
+#, c-format
+msgid "change authorization configuration"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:217
+#, c-format
+msgid "Need to complete marked fields and choose authorization method"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:82
+#, c-format
+msgid "This is not a valid bitcoin address."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:95
+#, c-format
+msgid "This is not a valid Ethereum address."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:118
+#, c-format
+msgid "IBAN numbers usually have more that 4 digits"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:120
+#, c-format
+msgid "IBAN numbers usually have less that 34 digits"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:128
+#, c-format
+msgid "IBAN country code not found"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:153
+#, c-format
+msgid "IBAN number is not valid, checksum is wrong"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:248
+#, c-format
+msgid "Target type"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:249
+#, c-format
+msgid "Method to use for wire transfer"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:258
+#, c-format
+msgid "Routing"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:259
+#, c-format
+msgid "Routing number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:263
+#, c-format
+msgid "Account"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:264
+#, c-format
+msgid "Account number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:273
+#, c-format
+msgid "Business Identifier Code."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:282
+#, c-format
+msgid "Bank Account Number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:292
+#, c-format
+msgid "Unified Payment Interface."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:301
+#, c-format
+msgid "Bitcoin protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:310
+#, c-format
+msgid "Ethereum protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:319
+#, c-format
+msgid "Interledger protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:328
+#, c-format
+msgid "Host"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:329
+#, c-format
+msgid "Bank host."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:334
+#, c-format
+msgid "Bank account."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:343
+#, c-format
+msgid "Bank account owner's name."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:370
+#, c-format
+msgid "No accounts yet."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:52
+#, c-format
+msgid ""
+"Name of the instance in URLs. The 'default' instance is special in that it is "
+"used to administer other instances."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:58
+#, c-format
+msgid "Business name"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:59
+#, c-format
+msgid "Legal name of the business represented by this instance."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:64
+#, c-format
+msgid "Email"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:65
+#, c-format
+msgid "Contact email"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:70
+#, c-format
+msgid "Website URL"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:71
+#, c-format
+msgid "URL."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:76
+#, c-format
+msgid "Logo"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:77
+#, c-format
+msgid "Logo image."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:82
+#, c-format
+msgid "Bank account"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:83
+#, c-format
+msgid "URI specifying bank account for crediting revenue."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:88
+#, c-format
+msgid "Default max deposit fee"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:89
+#, c-format
+msgid "Maximum deposit fees this merchant is willing to pay per order by default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:94
+#, c-format
+msgid "Default max wire fee"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:95
+#, c-format
+msgid "Maximum wire fees this merchant is willing to pay per wire transfer by default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:100
+#, c-format
+msgid "Default wire fee amortization"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:101
+#, c-format
+msgid ""
+"Number of orders excess wire transfer fees will be divided by to compute per "
+"order surcharge."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:107
+#, c-format
+msgid "Physical location of the merchant."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:114
+#, c-format
+msgid "Jurisdiction"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:115
+#, c-format
+msgid "Jurisdiction for legal disputes with the merchant."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:122
+#, c-format
+msgid "Default payment delay"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:124
+#, c-format
+msgid "Time customers have to pay an order before the offer expires by default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:129
+#, c-format
+msgid "Default wire transfer delay"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:130
+#, c-format
+msgid ""
+"Maximum time an exchange is allowed to delay wiring funds to the merchant, "
+"enabling it to aggregate smaller payments into larger wire transfers and "
+"reducing wire fees."
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:164
+#, c-format
+msgid "Instance id"
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:173
+#, c-format
+msgid "Change the authorization method use for this instance."
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:182
+#, c-format
+msgid "Manage access token"
+msgstr ""
+
+#: src/paths/instance/update/index.tsx:112
+#, c-format
+msgid "Failed to create instance"
+msgstr ""
+
+#: src/components/exception/login.tsx:74
+#, c-format
+msgid "Login required"
+msgstr ""
+
+#: src/components/exception/login.tsx:80
+#, c-format
+msgid "Please enter your access token."
+msgstr ""
+
+#: src/components/exception/login.tsx:108
+#, c-format
+msgid "Access Token"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:171
+#, c-format
+msgid "The request to the backend take too long and was cancelled"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:172
+#, c-format
+msgid "Diagnostic from %1$s is \"%2$s\""
+msgstr ""
+
+#: src/InstanceRoutes.tsx:178
+#, c-format
+msgid "The backend reported a problem: HTTP status #%1$s"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:179
+#, c-format
+msgid "Diagnostic from %1$s is '%2$s'"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:196
+#, c-format
+msgid "Access denied"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:197
+#, c-format
+msgid "The access token provided is invalid."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:212
+#, c-format
+msgid "No 'default' instance configured yet."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:213
+#, c-format
+msgid "Create a 'default' instance to begin using the merchant backoffice."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:630
+#, c-format
+msgid "The access token provided is invalid"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:664
+#, c-format
+msgid "Hide for today"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:82
+#, c-format
+msgid "Instance"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:91
+#, c-format
+msgid "Settings"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:167
+#, c-format
+msgid "Connection"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:209
+#, c-format
+msgid "New"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:219
+#, c-format
+msgid "List"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:234
+#, c-format
+msgid "Log out"
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:71
+#, c-format
+msgid "Check your token is valid"
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:90
+#, c-format
+msgid "Couldn't access the server."
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:91
+#, c-format
+msgid "Could not infer instance id from url %1$s"
+msgstr ""
+
+#: src/Application.tsx:104
+#, c-format
+msgid "Server not found"
+msgstr ""
+
+#: src/Application.tsx:118
+#, c-format
+msgid "Server response with an error code"
+msgstr ""
+
+#: src/Application.tsx:120
+#, c-format
+msgid "Got message %1$s from %2$s"
+msgstr ""
+
+#: src/Application.tsx:131
+#, c-format
+msgid "Response from server is unreadable, http status: %1$s"
+msgstr ""
+
+#: src/Application.tsx:144
+#, c-format
+msgid "Unexpected Error"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:101
+#, c-format
+msgid "The value %1$s is invalid for a payment url"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:110
+#, c-format
+msgid "add element to the list"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:112
+#, c-format
+msgid "add"
+msgstr ""
+
+#: src/components/form/InputSecured.tsx:37
+#, c-format
+msgid "Deleting"
+msgstr ""
+
+#: src/components/form/InputSecured.tsx:41
+#, c-format
+msgid "Changing"
+msgstr ""
+
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:87
+#, c-format
+msgid "Order ID"
+msgstr ""
+
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:101
+#, c-format
+msgid "Payment URL"
+msgstr ""
+
diff --git a/packages/merchant-backoffice-ui/src/index.html b/packages/merchant-backoffice-ui/src/index.html
new file mode 100644
index 000000000..b005f967d
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/index.html
@@ -0,0 +1,45 @@
+<!--
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+
+ @author Sebastian Javier Marchano
+-->
+<!DOCTYPE html>
+<html
+ lang="en"
+ class="has-aside-left has-aside-mobile-transition has-navbar-fixed-top has-aside-expanded"
+>
+ <head>
+ <meta charset="utf-8" />
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
+ <meta name="mobile-web-app-capable" content="yes" />
+ <meta name="apple-mobile-web-app-capable" content="yes" />
+
+ <link
+ rel="icon"
+ href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/Qn3//3ryn/795Tf/WrpP/2LCW/8B6T//w4Nb///////Pn4P+/d0v/9u3n/+7d0v/EhV7//v///+HDr//fxLD/zph2/+TJt//8/Pv/woBX//Lm3f/y5dz/v3hN//bu6f/JjGn/4sW0///////Df1j/8OLZ//v6+P+/elH/+vj1//jy7f+/elL//////+zYzP/Eg13//////967p//MlHT/wn5X///////v4Nb/yY1s///////jw7H/06KG////////////z5t9/+fNvf//////x4pn//Pp4v/8+vn/w39X/8WEX///////5s/A/9CbfP//////27Oc/9y2n////////////9itlf/gu6f//////86Vdf/r2Mz//////8SCXP/Df1j//////+7d0v/KkG7//////+HBrf/VpYr////////////RnoH/5sq6///////Ii2n/8ubf//39/P/Cf1j/xohk/+bNvv//////wn5W//Tq4//58/D/wHxV//7+/f/59fH/v3xU//39/P/w4Nf/xIFb///////hw7H/yo9t/+/f1f/AeU3/+/n2/+nSxP/FhmD//////9qzm//Upon/4MSx/96+qf//////xINc/+3bz//48e3/v3hN//Pn3///////6M+//752S//gw6//06aK/8J+VP/kzLr/zZd1/8OCWv/q18r/17KZ/9Ooi//fv6r/v3dK/+vWyP///////v39///////27un/1aeK/9Opjv/m1cf/1KCC/9a0nP/n08T/0Jx8/82YdP/QnHz/16yR//jx7P///////v39///////+/f3///7+///////+//7//v7+///////+/v7//v/+/////////////////////////v7//v79///////////////////+/v/+/Pv//v39///+/v/+/Pv///7+//7+/f/+/Pv//v39//79/P/+/Pv///7+////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
+ />
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
+ <title>Merchant Backoffice</title>
+ <!-- Optional customization script. -->
+ <script src="merchant-backoffice-ui-settings.js"></script>
+ <!-- Entry point for the SPA. -->
+ <script type="module" src="index.js"></script>
+ <link rel="stylesheet" href="index.css" />
+ </head>
+ <body>
+ <div id="app"></div>
+ </body>
+</html>
diff --git a/packages/merchant-backoffice-ui/src/index.tsx b/packages/merchant-backoffice-ui/src/index.tsx
index 8834ada53..46a3223bb 100644
--- a/packages/merchant-backoffice-ui/src/index.tsx
+++ b/packages/merchant-backoffice-ui/src/index.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2021-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -14,97 +14,15 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+import { Application } from "./Application.js";
-import { h, VNode } from 'preact';
-import { route } from 'preact-router';
-import { useMemo } from "preact/hooks";
-import { ApplicationReadyRoutes } from "./ApplicationReadyRoutes.js";
-import { Loading } from "./components/exception/loading.js";
-import { NotificationCard, NotYetReadyAppMenu } from "./components/menu/index.js";
-import { BackendContextProvider, useBackendContext } from './context/backend.js';
-import { ConfigContextProvider } from './context/config.js';
-import { TranslationProvider } from './context/translation.js';
-import { useBackendConfig } from "./hooks/backend.js";
-import { useTranslator } from './i18n/index.js';
-import LoginPage from './paths/login/index.js';
+import { h, render } from "preact";
import "./scss/main.scss";
-export default function Application(): VNode {
- return (
- // <FetchContextProvider>
- <BackendContextProvider>
- <TranslationProvider>
- <ApplicationStatusRoutes />
- </TranslationProvider>
- </BackendContextProvider>
- // </FetchContextProvider>
- );
-}
-
-function ApplicationStatusRoutes(): VNode {
- const { updateLoginStatus, triedToLog } = useBackendContext()
- const result = useBackendConfig();
- const i18n = useTranslator()
-
- const updateLoginInfoAndGoToRoot = (url: string, token?: string) => {
- updateLoginStatus(url, token)
- route('/')
- }
-
- const { currency, version } = result.ok ? result.data : { currency: 'unknown', version: 'unknown' }
- const ctx = useMemo(() => ({ currency, version }), [currency, version])
-
- if (!triedToLog) {
- return <div id="app">
- <NotYetReadyAppMenu title="Welcome!" />
- <LoginPage onConfirm={updateLoginInfoAndGoToRoot} />
- </div>
- }
-
- if (result.clientError && result.isUnauthorized) return <div id="app">
- <NotYetReadyAppMenu title="Login" />
- <LoginPage onConfirm={updateLoginInfoAndGoToRoot} />
- </div>
-
- if (result.clientError && result.isNotfound) return <div id="app">
- <NotYetReadyAppMenu title="Error" />
- <NotificationCard notification={{
- message: i18n`Server not found`,
- type: 'ERROR',
- description: `Check your url`,
- }} />
- <LoginPage onConfirm={updateLoginInfoAndGoToRoot} />
- </div>
-
- if (result.serverError) return <div id="app">
- <NotYetReadyAppMenu title="Error" />
- <NotificationCard notification={{
- message: i18n`Couldn't access the server`,
- type: 'ERROR',
- description: i18n`Got message ${result.message} from ${result.info?.url}`,
- }} />
- <LoginPage onConfirm={updateLoginInfoAndGoToRoot} />
- </div>
-
- if (result.loading) return <Loading />
-
- if (!result.ok) return <div id="app">
- <NotYetReadyAppMenu title="Error" />
- <NotificationCard notification={{
- message: i18n`Unexpected Error`,
- type: 'ERROR',
- description: i18n`Got message ${result.message} from ${result.info?.url}`,
- }} />
- <LoginPage onConfirm={updateLoginInfoAndGoToRoot} />
- </div>
+const app = document.getElementById("app");
- return <div id="app" class="has-navbar-fixed-top">
- <ConfigContextProvider value={ctx}>
- <ApplicationReadyRoutes />
- </ConfigContextProvider>
- </div>
+if (app) {
+ render(<Application />, app);
+} else {
+ console.error("HTML element with id 'app' not found.");
}
diff --git a/packages/merchant-backoffice-ui/src/manifest.json b/packages/merchant-backoffice-ui/src/manifest.json
deleted file mode 100644
index d0d3ea463..000000000
--- a/packages/merchant-backoffice-ui/src/manifest.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "name": "backoffice-preact",
- "short_name": "backoffice-preact",
- "start_url": "/",
- "display": "standalone",
- "orientation": "portrait",
- "background_color": "#fff",
- "theme_color": "#673ab8",
- "icons": [
- {
- "src": "/assets/icons/android-chrome-192x192.png",
- "type": "image/png",
- "sizes": "192x192"
- },
- {
- "src": "/assets/icons/android-chrome-512x512.png",
- "type": "image/png",
- "sizes": "512x512"
- }
- ]
-} \ No newline at end of file
diff --git a/packages/merchant-backoffice-ui/src/paths/admin/create/Create.stories.tsx b/packages/merchant-backoffice-ui/src/paths/admin/create/Create.stories.tsx
index 7411586a1..54d947e14 100644
--- a/packages/merchant-backoffice-ui/src/paths/admin/create/Create.stories.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/admin/create/Create.stories.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,31 +15,61 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { h, VNode, FunctionalComponent } from 'preact';
+import { MerchantApiProviderTesting } from "@gnu-taler/web-util/browser";
+import { FunctionalComponent, h } from "preact";
import { CreatePage as TestedComponent } from "./CreatePage.js";
-
export default {
- title: 'Pages/Instance/Create',
+ title: "Pages/Instance/Create",
component: TestedComponent,
argTypes: {
- onCreate: { action: 'onCreate' },
- goBack: { action: 'goBack' },
- }
+ 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
+function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => (
+ <MerchantApiProviderTesting
+ value={{
+ cancelRequest: () => {},
+ changeBackend: () => {},
+ config: {
+ currency: "ARS",
+ version: "1",
+ currencies: {
+ "ASD": {
+ name: "testkudos",
+ alt_unit_names: {},
+ num_fractional_input_digits: 1,
+ num_fractional_normal_digits: 1,
+ num_fractional_trailing_zero_digits: 1,
+ }
+ },
+ exchanges: [],
+ name: "taler-merchant"
+ },
+ hints: [],
+ lib: {} as any,
+ onActivity: (() => {}) as any,
+ url: new URL("asdasd"),
+ }}
+ >
+ <Component {...args} />
+ </MerchantApiProviderTesting>
+ );
+ r.args = props;
+ return r;
}
-export const Example = createExample(TestedComponent, {
-});
+export const Example = createExample(TestedComponent, {});
// export const Example = (a: any): VNode => <CreatePage {...a} />;
// Example.args = {
// isLoading: false
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 0d7681e1d..4a5ab440b 100644
--- a/packages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2021-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -19,28 +19,35 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
+import {
+ Duration,
+ TalerMerchantApi,
+ createAccessToken,
+} from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { h, VNode } 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 { SetTokenNewInstanceModal } from "../../../components/modal/index.js";
-import { MerchantBackend } from "../../../declaration.js";
-import { Translate, useTranslator } from "../../../i18n/index.js";
import { DefaultInstanceFormFields } from "../../../components/instance/DefaultInstanceFormFields.js";
-import { INSTANCE_ID_REGEX, PAYTO_REGEX } from "../../../utils/constants.js";
-import { Amounts } from "@gnu-taler/taler-util";
+import { SetTokenNewInstanceModal } from "../../../components/modal/index.js";
+import { INSTANCE_ID_REGEX } from "../../../utils/constants.js";
import { undefinedIfEmpty } from "../../../utils/table.js";
-export type Entity = MerchantBackend.Instances.InstanceConfigurationMessage & {
+export type Entity = Omit<
+ Omit<TalerMerchantApi.InstanceConfigurationMessage, "default_pay_delay">,
+ "default_wire_transfer_delay"
+> & {
auth_token?: string;
+ default_pay_delay: Duration;
+ default_wire_transfer_delay: Duration;
};
interface Props {
- onCreate: (d: Entity) => Promise<void>;
+ onCreate: (d: TalerMerchantApi.InstanceConfigurationMessage) => Promise<void>;
onBack?: () => void;
forceId?: string;
}
@@ -48,10 +55,11 @@ interface Props {
function with_defaults(id?: string): Partial<Entity> {
return {
id,
- payto_uris: [],
- default_pay_delay: { d_us: 2 * 1000 * 60 * 60 * 1000 }, // two hours
- default_wire_fee_amortization: 1,
- default_wire_transfer_delay: { d_us: 1000 * 2 * 60 * 60 * 24 * 1000 }, // two days
+ // 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
};
}
@@ -61,76 +69,86 @@ export function CreatePage({ onCreate, onBack, forceId }: Props): VNode {
const [isTokenDialogActive, updateIsTokenDialogActive] =
useState<boolean>(false);
- const i18n = useTranslator();
+ const { i18n } = useTranslationContext();
const errors: FormErrors<Entity> = {
id: !value.id
- ? i18n`required`
+ ? i18n.str`required`
: !INSTANCE_ID_REGEX.test(value.id)
- ? i18n`is not valid`
- : undefined,
- name: !value.name ? i18n`required` : undefined,
- payto_uris:
- !value.payto_uris || !value.payto_uris.length
- ? i18n`required`
- : undefinedIfEmpty(
- value.payto_uris.map((p) => {
- return !PAYTO_REGEX.test(p) ? i18n`is not valid` : undefined;
- })
- ),
- default_max_deposit_fee: !value.default_max_deposit_fee
- ? i18n`required`
- : !Amounts.parse(value.default_max_deposit_fee)
- ? i18n`invalid format`
- : undefined,
- default_max_wire_fee: !value.default_max_wire_fee
- ? i18n`required`
- : !Amounts.parse(value.default_max_wire_fee)
- ? i18n`invalid format`
- : undefined,
- default_wire_fee_amortization:
- value.default_wire_fee_amortization === undefined
- ? i18n`required`
- : isNaN(value.default_wire_fee_amortization)
- ? i18n`is not a number`
- : value.default_wire_fee_amortization < 1
- ? i18n`must be 1 or greater`
+ ? 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_pay_delay: !value.default_pay_delay ? i18n`required` : undefined,
default_wire_transfer_delay: !value.default_wire_transfer_delay
- ? i18n`required`
+ ? i18n.str`required`
: undefined,
address: undefinedIfEmpty({
address_lines:
value.address?.address_lines && value.address?.address_lines.length > 7
- ? i18n`max 7 lines`
+ ? i18n.str`max 7 lines`
: undefined,
}),
jurisdiction: undefinedIfEmpty({
address_lines:
value.address?.address_lines && value.address?.address_lines.length > 7
- ? i18n`max 7 lines`
+ ? i18n.str`max 7 lines`
: undefined,
}),
};
const hasErrors = Object.keys(errors).some(
- (k) => (errors as any)[k] !== undefined
+ (k) => (errors as Record<string, unknown>)[k] !== undefined,
);
const submit = (): Promise<void> => {
// use conversion instead of this
- const newToken = value.auth_token;
- value.auth_token = undefined;
- value.auth =
+ const newValue = structuredClone(value);
+
+ const newToken = newValue.auth_token;
+ newValue.auth_token = undefined;
+ newValue.auth =
newToken === null || newToken === undefined
? { method: "external" }
- : { method: "token", token: `secret-token:${newToken}` };
- if (!value.address) value.address = {};
- if (!value.jurisdiction) value.jurisdiction = {};
+ : { method: "token", token: createAccessToken(newToken) };
+ if (!newValue.address) newValue.address = {};
+ if (!newValue.jurisdiction) newValue.jurisdiction = {};
// remove above use conversion
// schema.validateSync(value, { abortEarly: false })
- return onCreate(value as Entity);
+ newValue.default_pay_delay = Duration.toTalerProtocolDuration(
+ newValue.default_pay_delay!,
+ ) as any;
+ newValue.default_wire_transfer_delay = Duration.toTalerProtocolDuration(
+ newValue.default_wire_transfer_delay!,
+ ) as any;
+ // delete value.default_pay_delay;
+ // delete value.default_wire_transfer_delay;
+
+ return onCreate(
+ newValue as any as TalerMerchantApi.InstanceConfigurationMessage,
+ );
};
function updateToken(token: string | null) {
@@ -167,29 +185,6 @@ export function CreatePage({ onCreate, onBack, forceId }: Props): VNode {
<div class="column" />
</div>
- <section class="hero is-hero-bar">
- <div class="hero-body">
- <div class="level">
- <div class="level-item has-text-centered">
- <h1 class="title">
- <button
- class="button is-danger has-tooltip-bottom"
- data-tooltip={i18n`change authorization configuration`}
- onClick={() => updateIsTokenDialogActive(true)}
- >
- <div class="icon is-centered">
- <i class="mdi mdi-lock-reset" />
- </div>
- <span>
- <Translate>Set access token</Translate>
- </span>
- </button>
- </h1>
- </div>
- </div>
- </div>
- </section>
-
<section class="section is-main-section">
<div class="columns">
<div class="column" />
@@ -202,22 +197,71 @@ export function CreatePage({ onCreate, onBack, forceId }: Props): VNode {
<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}>
- <Translate>Cancel</Translate>
+ <i18n.Translate>Cancel</i18n.Translate>
</button>
)}
<AsyncButton
onClick={submit}
- disabled={!isTokenSet || hasErrors}
+ disabled={hasErrors || !isTokenSet}
data-tooltip={
hasErrors
- ? i18n`Need to complete marked fields and choose authorization method`
+ ? i18n.str`Need to complete marked fields and choose authorization method`
: "confirm operation"
}
>
- <Translate>Confirm</Translate>
+ <i18n.Translate>Confirm</i18n.Translate>
</AsyncButton>
</div>
</div>
diff --git a/packages/merchant-backoffice-ui/src/paths/admin/create/InstanceCreatedSuccessfully.tsx b/packages/merchant-backoffice-ui/src/paths/admin/create/InstanceCreatedSuccessfully.tsx
index 37aa7e7d6..939f9b06a 100644
--- a/packages/merchant-backoffice-ui/src/paths/admin/create/InstanceCreatedSuccessfully.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/admin/create/InstanceCreatedSuccessfully.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2021-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -14,52 +14,61 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @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>
+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>
- </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 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>
- <div class="field is-horizontal">
- <div class="field-label is-normal">
- <label class="label">Access token</label>
+ <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-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 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>
- </div>
- </CreatedSuccessfully>;
+ </CreatedSuccessfully>
+ );
}
diff --git a/packages/merchant-backoffice-ui/src/paths/admin/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/admin/create/index.tsx
index dd124eab8..b00cfbe7d 100644
--- a/packages/merchant-backoffice-ui/src/paths/admin/create/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/admin/create/index.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,34 +17,29 @@
*
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { Fragment, h, VNode } from "preact";
+import { 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 { NotificationCard } from "../../../components/menu/index.js";
-import { MerchantBackend } from "../../../declaration.js";
-import { useAdminAPI } from "../../../hooks/instance.js";
-import { useTranslator } from "../../../i18n/index.js";
+import { useSessionContext } from "../../../context/session.js";
import { Notification } from "../../../utils/types.js";
import { CreatePage } from "./CreatePage.js";
-import { InstanceCreatedSuccessfully } from "./InstanceCreatedSuccessfully.js";
interface Props {
onBack?: () => void;
onConfirm: () => void;
forceId?: string;
}
-export type Entity = MerchantBackend.Instances.InstanceConfigurationMessage;
+export type Entity = TalerMerchantApi.InstanceConfigurationMessage;
export default function Create({ onBack, onConfirm, forceId }: Props): VNode {
- const { createInstance } = useAdminAPI();
const [notif, setNotif] = useState<Notification | undefined>(undefined);
- const [createdOk, setCreatedOk] = useState<Entity | undefined>(undefined);
- const i18n = useTranslator();
-
- if (createdOk) {
- return (
- <InstanceCreatedSuccessfully entity={createdOk} onConfirm={onConfirm} />
- );
- }
+ const { i18n } = useTranslationContext();
+ const { lib } = useSessionContext();
+ const { state, logIn } = useSessionContext();
return (
<Fragment>
@@ -53,20 +48,41 @@ export default function Create({ onBack, onConfirm, forceId }: Props): VNode {
<CreatePage
onBack={onBack}
forceId={forceId}
- onCreate={(
- d: MerchantBackend.Instances.InstanceConfigurationMessage
+ onCreate={async (
+ d: TalerMerchantApi.InstanceConfigurationMessage,
) => {
- return createInstance(d)
- .then(() => {
- setCreatedOk(d);
- })
- .catch((error) => {
+ if (state.status !== "loggedIn") return;
+ try {
+ await lib.instance.createInstance(state.token, d);
+ if (d.auth.token) {
+ //if auth has been updated, request a new access token
+ const result = await lib.authenticate.createAccessTokenBearer(
+ d.auth.token,
+ {
+ scope: "write",
+ duration: {
+ d_us: "forever",
+ },
+ refreshable: true,
+ },
+ );
+ if (result.type === "ok") {
+ const { token } = result.body;
+ logIn(token);
+ }
+ }
+ onConfirm();
+ } catch (ex) {
+ if (ex instanceof Error) {
setNotif({
- message: i18n`Failed to create instance`,
+ message: i18n.str`Failed to create instance`,
type: "ERROR",
- description: error.message,
+ description: ex.message,
});
- });
+ } else {
+ console.error(ex);
+ }
+ }
}}
/>
</Fragment>
diff --git a/packages/merchant-backoffice-ui/src/paths/admin/create/stories.tsx b/packages/merchant-backoffice-ui/src/paths/admin/create/stories.tsx
new file mode 100644
index 000000000..d4258058b
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/admin/create/stories.tsx
@@ -0,0 +1,71 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { FunctionalComponent, h } from "preact";
+import { CreatePage as TestedComponent } from "./CreatePage.js";
+import { MerchantApiProviderTesting } from "@gnu-taler/web-util/browser";
+
+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) => (
+ <MerchantApiProviderTesting
+ value={{
+ cancelRequest: () => {},
+ changeBackend: () => {},
+ config: {
+ currency: "ARS",
+ version: "1",
+ currencies: {
+ "ASD": {
+ name: "testkudos",
+ alt_unit_names: {},
+ num_fractional_input_digits: 1,
+ num_fractional_normal_digits: 1,
+ num_fractional_trailing_zero_digits: 1,
+ }
+ },
+ exchanges: [],
+ name: "taler-merchant"
+ },
+ hints: [],
+ lib: {} as any,
+ onActivity: (() => {}) as any,
+ url: new URL("asdasd"),
+ }}
+ >
+ <Internal {...(props as any)} />
+ </MerchantApiProviderTesting>
+ );
+ return { component, props };
+}
+
+export const Example = createExample(TestedComponent, {});
diff --git a/packages/merchant-backoffice-ui/src/paths/admin/index.stories.ts b/packages/merchant-backoffice-ui/src/paths/admin/index.stories.ts
new file mode 100644
index 000000000..3c16c0e57
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/admin/index.stories.ts
@@ -0,0 +1,18 @@
+/*
+ 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/>
+ */
+
+// export * as list from "./list/stories.js";
+export * as create from "./create/stories.js";
diff --git a/packages/merchant-backoffice-ui/src/paths/admin/list/TableActive.tsx b/packages/merchant-backoffice-ui/src/paths/admin/list/TableActive.tsx
index 71e90889a..cff3c5a02 100644
--- a/packages/merchant-backoffice-ui/src/paths/admin/list/TableActive.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/admin/list/TableActive.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,92 +15,144 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { h, VNode } from "preact";
+import { TalerMerchantApi } from "@gnu-taler/taler-util";
+import {
+ useTranslationContext
+} from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
import { StateUpdater, useEffect, useState } from "preact/hooks";
-import { MerchantBackend } from "../../../declaration.js";
-import { Translate, useTranslator } from "../../../i18n/index.js";
+import { useSessionContext } from "../../../context/session.js";
interface Props {
- instances: MerchantBackend.Instances.Instance[];
+ instances: TalerMerchantApi.Instance[];
onUpdate: (id: string) => void;
- onDelete: (id: MerchantBackend.Instances.Instance) => void;
- onPurge: (id: MerchantBackend.Instances.Instance) => void;
+ onDelete: (id: TalerMerchantApi.Instance) => void;
+ onPurge: (id: TalerMerchantApi.Instance) => void;
onCreate: () => void;
selected?: boolean;
- setInstanceName: (s: string) => void;
}
-export function CardTable({ instances, onCreate, onUpdate, onPurge, setInstanceName, onDelete, selected }: Props): VNode {
+export function CardTable({
+ instances,
+ onCreate,
+ onUpdate,
+ onPurge,
+ onDelete,
+ selected,
+}: Props): VNode {
const [actionQueue, actionQueueHandler] = useState<Actions[]>([]);
- const [rowSelection, rowSelectionHandler] = useState<string[]>([])
+ const [rowSelection, rowSelectionHandler] = useState<string[]>([]);
useEffect(() => {
- if (actionQueue.length > 0 && !selected && actionQueue[0].type == 'DELETE') {
- onDelete(actionQueue[0].element)
- actionQueueHandler(actionQueue.slice(1))
+ if (
+ actionQueue.length > 0 &&
+ !selected &&
+ actionQueue[0].type == "DELETE"
+ ) {
+ onDelete(actionQueue[0].element);
+ actionQueueHandler(actionQueue.slice(1));
}
- }, [actionQueue, selected, onDelete])
+ }, [actionQueue, selected, onDelete]);
useEffect(() => {
- if (actionQueue.length > 0 && !selected && actionQueue[0].type == 'UPDATE') {
- onUpdate(actionQueue[0].element.id)
- actionQueueHandler(actionQueue.slice(1))
+ if (
+ actionQueue.length > 0 &&
+ !selected &&
+ actionQueue[0].type == "UPDATE"
+ ) {
+ onUpdate(actionQueue[0].element.id);
+ actionQueueHandler(actionQueue.slice(1));
}
- }, [actionQueue, selected, onUpdate])
-
- const i18n = useTranslator()
+ }, [actionQueue, selected, onUpdate]);
- 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><Translate>Instances</Translate></p>
+ const { i18n } = useTranslationContext();
- <div class="card-header-icon" aria-label="more options">
+ 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>
- <button class={rowSelection.length > 0 ? "button is-danger" : "is-hidden"}
- type="button" onClick={(): void => actionQueueHandler(buildActions(instances, rowSelection, 'DELETE'))} >
- <Translate>Delete</Translate>
- </button>
- </div>
- <div class="card-header-icon" aria-label="more options">
- <span class="has-tooltip-left" data-tooltip={i18n`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>
+ <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>
- </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 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}
+ onDelete={onDelete}
+ rowSelection={rowSelection}
+ rowSelectionHandler={rowSelectionHandler}
+ />
+ ) : (
+ <EmptyTable />
+ )}
+ </div>
</div>
</div>
</div>
- </div>
+ );
}
interface TableProps {
rowSelection: string[];
- instances: MerchantBackend.Instances.Instance[];
+ instances: TalerMerchantApi.Instance[];
onUpdate: (id: string) => void;
- onDelete: (id: MerchantBackend.Instances.Instance) => void;
- onPurge: (id: MerchantBackend.Instances.Instance) => void;
+ onDelete: (id: TalerMerchantApi.Instance) => void;
+ onPurge: (id: TalerMerchantApi.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)
+ 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 {
+function Table({
+ rowSelection,
+ rowSelectionHandler,
+ instances,
+ onUpdate,
+ onDelete,
+ onPurge,
+}: TableProps): VNode {
+ const { i18n } = useTranslationContext();
+ const { lib } = useSessionContext();
+ const { impersonate } = useSessionContext();
return (
<div class="table-container">
<table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
@@ -108,75 +160,131 @@ function Table({ rowSelection, rowSelectionHandler, setInstanceName, instances,
<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))} />
+ <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><Translate>ID</Translate></th>
- <th><Translate>Name</Translate></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)}>
- <Translate>Edit</Translate>
- </button>
- {!i.deleted &&
- <button class="button is-small is-danger jb-modal is-outlined" type="button" onClick={(): void => onDelete(i)}>
- <Translate>Delete</Translate>
- </button>
- }
- {i.deleted &&
- <button class="button is-small is-danger jb-modal" type="button" onClick={(): void => onPurge(i)}>
- <Translate>Purge</Translate>
- </button>
- }
- </div>
- </td>
- </tr>
+ {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`}
+ onClick={async (_e) => {
+ // e.preventDefault();
+ const newInstanceApi = lib.subInstanceApi(i.id);
+ //not checking /config since this comes from instance list
+ impersonate(new URL(newInstanceApi.instance.baseUrl));
+ }}
+ >
+ {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 {
- 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><Translate>There is no instances yet, add more pressing the + sign</Translate></p>
- </div>
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="content has-text-grey has-text-centered">
+ <p>
+ <span class="icon is-large">
+ <i class="mdi mdi-magnify 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';
+ element: TalerMerchantApi.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))
+function buildActions(
+ instances: TalerMerchantApi.Instance[],
+ selected: string[],
+ action: "DELETE",
+): Actions[] {
+ return selected
+ .map((id) => instances.find((i) => i.id === id))
.filter(notEmpty)
- .map(id => ({ element: id, type: action }))
+ .map((id) => ({ element: id, type: action }));
}
diff --git a/packages/merchant-backoffice-ui/src/paths/admin/list/View.stories.tsx b/packages/merchant-backoffice-ui/src/paths/admin/list/View.stories.tsx
index f0b04aabe..c4c0996f6 100644
--- a/packages/merchant-backoffice-ui/src/paths/admin/list/View.stories.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/admin/list/View.stories.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,68 +15,76 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { h } from 'preact';
+import { h } from "preact";
import { View } from "./View.js";
-
export default {
- title: 'Pages/Instance/List',
+ title: "Pages/Instance/List",
component: View,
argTypes: {
- onSelect: { action: 'onSelect' },
+ onSelect: { action: "onSelect" },
},
};
export const Empty = (a: any) => <View {...a} />;
Empty.args = {
- instances: []
-}
+ instances: [],
+};
export const WithDefaultInstance = (a: any) => <View {...a} />;
WithDefaultInstance.args = {
- instances: [{
- id: 'default',
- name: 'the default instance',
- merchant_pub: 'abcdef',
- payment_targets: []
- }]
-}
+ 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']
- }]
-}
+ 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/merchant-backoffice-ui/src/paths/admin/list/View.tsx b/packages/merchant-backoffice-ui/src/paths/admin/list/View.tsx
index 4197a6679..940d14334 100644
--- a/packages/merchant-backoffice-ui/src/paths/admin/list/View.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/admin/list/View.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,66 +15,93 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @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 { MerchantBackend } from "../../../declaration.js";
+import { useState } from "preact/hooks";
import { CardTable as CardTableActive } from "./TableActive.js";
-import { useState } from 'preact/hooks';
-import { Translate, useTranslator } from "../../../i18n/index.js";
interface Props {
- instances: MerchantBackend.Instances.Instance[];
+ instances: TalerMerchantApi.Instance[];
onCreate: () => void;
onUpdate: (id: string) => void;
- onDelete: (id: MerchantBackend.Instances.Instance) => void;
- onPurge: (id: MerchantBackend.Instances.Instance) => void;
+ onDelete: (id: TalerMerchantApi.Instance) => void;
+ onPurge: (id: TalerMerchantApi.Instance) => void;
selected?: boolean;
- setInstanceName: (s: string) => void;
}
-export function View({ instances, onCreate, onDelete, onPurge, onUpdate, setInstanceName, selected }: Props): VNode {
+export function View({
+ instances,
+ onCreate,
+ onDelete,
+ onPurge,
+ onUpdate,
+ 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 = useTranslator()
+ 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 <div id="app">
+ 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' }}>
+ <div class="tabs" style={{ overflow: "inherit" }}>
<ul>
<li class={showIsActive}>
- <div class="has-tooltip-right" data-tooltip={i18n`Only show active instances`}>
- <a onClick={() => setShow("active")}><Translate>Active</Translate></a>
+ <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`Only show deleted instances`}>
- <a onClick={() => setShow("deleted")}><Translate>Deleted</Translate></a>
+ <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`Show all instances`}>
- <a onClick={() => setShow(null)}><Translate>All</Translate></a>
+ <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} />
+ <CardTableActive
+ instances={showingInstances}
+ onDelete={onDelete}
+ onPurge={onPurge}
+ onUpdate={onUpdate}
+ selected={selected}
+ onCreate={onCreate}
+ />
</section>
-
- </div >
+ );
}
diff --git a/packages/merchant-backoffice-ui/src/paths/admin/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/admin/list/index.tsx
index f20f0e921..5b492e45c 100644
--- a/packages/merchant-backoffice-ui/src/paths/admin/list/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/admin/list/index.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2021-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -19,60 +19,66 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { Fragment, h, VNode } from "preact";
+import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util";
+import {
+ useTranslationContext
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
+import { ErrorLoadingMerchant } from "../../../components/ErrorLoadingMerchant.js";
import { Loading } from "../../../components/exception/loading.js";
import { NotificationCard } from "../../../components/menu/index.js";
import { DeleteModal, PurgeModal } from "../../../components/modal/index.js";
-import { MerchantBackend } from "../../../declaration.js";
-import { HttpError } from "../../../hooks/backend.js";
-import { useAdminAPI, useBackendInstances } from "../../../hooks/instance.js";
-import { useTranslator } from "../../../i18n/index.js";
+import { useSessionContext } from "../../../context/session.js";
+import { useBackendInstances } from "../../../hooks/instance.js";
import { Notification } from "../../../utils/types.js";
+import { LoginPage } from "../../login/index.js";
import { View } from "./View.js";
interface Props {
onCreate: () => void;
onUpdate: (id: string) => void;
- instances: MerchantBackend.Instances.Instance[];
- onUnauthorized: () => VNode;
- onNotFound: () => VNode;
- onLoadError: (error: HttpError) => VNode;
- setInstanceName: (s: string) => void;
+ instances: TalerMerchantApi.Instance[];
}
export default function Instances({
- onUnauthorized,
- onLoadError,
- onNotFound,
onCreate,
onUpdate,
- setInstanceName,
}: Props): VNode {
const result = useBackendInstances();
const [deleting, setDeleting] =
- useState<MerchantBackend.Instances.Instance | null>(null);
+ useState<TalerMerchantApi.Instance | null>(null);
const [purging, setPurging] =
- useState<MerchantBackend.Instances.Instance | null>(null);
- const { deleteInstance, purgeInstance } = useAdminAPI();
+ useState<TalerMerchantApi.Instance | null>(null);
const [notif, setNotif] = useState<Notification | undefined>(undefined);
- const i18n = useTranslator();
+ const { i18n } = useTranslationContext();
+ const { lib } = useSessionContext();
+ const { state } = useSessionContext();
- if (result.clientError && result.isUnauthorized) return onUnauthorized();
- if (result.clientError && result.isNotfound) return onNotFound();
- if (result.loading) return <Loading />;
- if (!result.ok) return onLoadError(result);
+ if (!result) return <Loading />
+ if (result instanceof TalerError) {
+ return <ErrorLoadingMerchant error={result} />
+ }
+ if (result.type === "fail") {
+ switch(result.case) {
+ case HttpStatusCode.Unauthorized: {
+ return <LoginPage />
+ }
+ default: {
+ assertUnreachable(result.case)
+ }
+ }
+ }
return (
<Fragment>
<NotificationCard notification={notif} />
<View
- instances={result.data.instances}
+ instances={result.body.instances}
onDelete={setDeleting}
onCreate={onCreate}
onPurge={setPurging}
onUpdate={onUpdate}
- setInstanceName={setInstanceName}
selected={!!deleting}
/>
{deleting && (
@@ -80,20 +86,23 @@ export default function Instances({
element={deleting}
onCancel={() => setDeleting(null)}
onConfirm={async (): Promise<void> => {
+ if (state.status !== "loggedIn") {
+ return;
+ }
try {
- await deleteInstance(deleting.id);
- // pushNotification({ message: 'delete_success', type: 'SUCCESS' })
+ await lib.instance.deleteInstance(state.token, deleting.id);
+ // pushNotification({message: 'delete_success', type: 'SUCCESS' })
setNotif({
- message: i18n`Instance "${deleting.name}" (ID: ${deleting.id}) has been deleted`,
+ message: i18n.str`Instance "${deleting.name}" (ID: ${deleting.id}) has been deleted`,
type: "SUCCESS",
});
} catch (error) {
setNotif({
- message: i18n`Failed to delete instance`,
+ message: i18n.str`Failed to delete instance`,
type: "ERROR",
description: error instanceof Error ? error.message : undefined,
});
- // pushNotification({ message: 'delete_error', type: 'ERROR' })
+ // pushNotification({message: 'delete_error', type: 'ERROR' })
}
setDeleting(null);
}}
@@ -104,15 +113,18 @@ export default function Instances({
element={purging}
onCancel={() => setPurging(null)}
onConfirm={async (): Promise<void> => {
+ if (state.status !== "loggedIn") {
+ return;
+ }
try {
- await purgeInstance(purging.id);
+ await lib.instance.deleteInstance(state.token, purging.id, { purge: true });
setNotif({
- message: i18n`Instance "${purging.name}" (ID: ${purging.id}) has been disabled`,
+ message: i18n.str`Instance '${purging.name}' (ID: ${purging.id}) has been disabled`,
type: "SUCCESS",
});
} catch (error) {
setNotif({
- message: i18n`Failed to purge instance`,
+ message: i18n.str`Failed to purge instance`,
type: "ERROR",
description: error instanceof Error ? error.message : undefined,
});
diff --git a/packages/merchant-backoffice-ui/src/paths/index.stories.ts b/packages/merchant-backoffice-ui/src/paths/index.stories.ts
deleted file mode 100644
index b3811fd4f..000000000
--- a/packages/merchant-backoffice-ui/src/paths/index.stories.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export * as a1 from "./admin/create/Create.stories.js";
-export * as a2 from "./instance/details/Details.stories.js"; \ No newline at end of file
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/Create.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/Create.stories.tsx
new file mode 100644
index 000000000..50cd801d8
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/Create.stories.tsx
@@ -0,0 +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)
+ */
+
+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/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx
new file mode 100644
index 000000000..d05375b6c
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx
@@ -0,0 +1,197 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { TalerMerchantApi } from "@gnu-taler/taler-util";
+import { 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 { undefinedIfEmpty } from "../../../../utils/table.js";
+import { safeConvertURL } from "../update/UpdatePage.js";
+
+type Entity = TalerMerchantApi.AccountAddDetails & { repeatPassword: string };
+
+interface Props {
+ onCreate: (d: TalerMerchantApi.AccountAddDetails) => Promise<void>;
+ onBack?: () => void;
+}
+
+const accountAuthType = ["none", "basic"];
+
+export function CreatePage({ onCreate, onBack }: Props): VNode {
+ const { i18n } = useTranslationContext();
+
+ const [state, setState] = useState<Partial<Entity>>({});
+ const facadeURL = safeConvertURL(state.credit_facade_url);
+ 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
+ : !facadeURL
+ ? i18n.str`Invalid url`
+ : !facadeURL.href.endsWith("/")
+ ? i18n.str`URL should end with a '/'`
+ : facadeURL.searchParams.size > 0
+ ? i18n.str`URL should not contain params`
+ : 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(
+ (k) => (errors as Record<string, unknown>)[k] !== undefined,
+ );
+
+ const submitForm = () => {
+ if (hasErrors) return Promise.reject();
+ const credit_facade_url = !state.credit_facade_url
+ ? undefined
+ : facadeURL?.href;
+ const credit_facade_credentials:
+ | TalerMerchantApi.FacadeCredentials
+ | undefined =
+ credit_facade_url == 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 onCreate({
+ payto_uri: state.payto_uri!,
+ credit_facade_credentials,
+ credit_facade_url,
+ });
+ };
+
+ 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/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx
new file mode 100644
index 000000000..9bab33f6f
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx
@@ -0,0 +1,236 @@
+/*
+ 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 {
+ FacadeCredentials,
+ HttpStatusCode,
+ OperationFail,
+ OperationOk,
+ TalerError,
+ TalerMerchantApi,
+ TalerRevenueHttpClient,
+ assertUnreachable,
+ opFixedSuccess,
+} from "@gnu-taler/taler-util";
+import {
+ BrowserFetchHttpLib,
+ useTranslationContext
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { useSessionContext } from "../../../../context/session.js";
+import { Notification } from "../../../../utils/types.js";
+import { CreatePage } from "./CreatePage.js";
+
+export type Entity = TalerMerchantApi.AccountAddDetails;
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+}
+
+export default function CreateValidator({ onConfirm, onBack }: Props): VNode {
+ const { lib: api } = useSessionContext();
+ const { state } = useSessionContext();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+
+ return (
+ <>
+ <NotificationCard notification={notif} />
+ <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,
+ );
+ if (resp.type === "fail") {
+ switch (resp.case) {
+ case TestRevenueErrorType.NO_CONFIG: {
+ 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.CLIENT_BAD_REQUEST: {
+ setNotif({
+ message: i18n.str`Could not create account`,
+ type: "ERROR",
+ description: i18n.str`Server replied with "bad request".`,
+ });
+ return;
+ }
+ case TestRevenueErrorType.UNAUTHORIZED: {
+ setNotif({
+ message: i18n.str`Could not create account`,
+ type: "ERROR",
+ description: i18n.str`Unauthorized, try with another credentials.`,
+ });
+ return;
+ }
+ case TestRevenueErrorType.NOT_FOUND: {
+ setNotif({
+ message: i18n.str`Could not create account`,
+ type: "ERROR",
+ description: i18n.str`Check facade URL, server replied with "not found".`,
+ });
+ return;
+ }
+ case TestRevenueErrorType.GENERIC_ERROR: {
+ setNotif({
+ message: i18n.str`Could not create account`,
+ type: "ERROR",
+ description: resp.detail.hint,
+ });
+ return;
+ }
+ default: {
+ assertUnreachable(resp.case);
+ }
+ }
+ }
+ }
+
+ return api.instance
+ .addBankAccount(state.token, request)
+ .then(() => {
+ onConfirm();
+ })
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not create account`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </>
+ );
+}
+
+export enum TestRevenueErrorType {
+ NO_CONFIG,
+ CLIENT_BAD_REQUEST,
+ UNAUTHORIZED,
+ NOT_FOUND,
+ GENERIC_ERROR,
+}
+
+export async function testRevenueAPI(
+ revenueAPI: URL,
+ creds: FacadeCredentials | undefined,
+): Promise<OperationOk<void> | OperationFail<TestRevenueErrorType>> {
+ const api = new TalerRevenueHttpClient(
+ revenueAPI.href,
+ new BrowserFetchHttpLib(),
+ );
+ const auth =
+ creds === undefined
+ ? undefined
+ : creds.type === "none"
+ ? undefined
+ : creds.type === "basic"
+ ? {
+ username: creds.username,
+ password: creds.password,
+ }
+ : undefined;
+
+ try {
+ const config = await api.getConfig(auth);
+
+ if (config.type === "fail") {
+ switch (config.case) {
+ case HttpStatusCode.Unauthorized: {
+ return {
+ type: "fail",
+ case: TestRevenueErrorType.UNAUTHORIZED,
+ detail: {
+ code: 1,
+ },
+ };
+ }
+ case HttpStatusCode.NotFound: {
+ return {
+ type: "fail",
+ case: TestRevenueErrorType.NO_CONFIG,
+ detail: {
+ code: 1,
+ },
+ };
+ }
+ }
+ }
+
+ const history = await api.getHistory(auth);
+
+ if (history.type === "fail") {
+ switch (history.case) {
+ case HttpStatusCode.BadRequest: {
+ return {
+ type: "fail",
+ case: TestRevenueErrorType.CLIENT_BAD_REQUEST,
+ detail: {
+ code: 1,
+ },
+ };
+ }
+ case HttpStatusCode.Unauthorized: {
+ return {
+ type: "fail",
+ case: TestRevenueErrorType.UNAUTHORIZED,
+ detail: {
+ code: 1,
+ },
+ };
+ }
+ case HttpStatusCode.NotFound: {
+ return {
+ type: "fail",
+ case: TestRevenueErrorType.NOT_FOUND,
+ detail: {
+ code: 1,
+ },
+ };
+ }
+ }
+ }
+ } catch (err) {
+ if (err instanceof TalerError) {
+ return {
+ type: "fail",
+ case: TestRevenueErrorType.GENERIC_ERROR,
+ detail: err.errorDetail,
+ };
+ }
+ }
+
+ return opFixedSuccess(undefined);
+}
diff --git a/packages/merchant-backend-ui/tests/__mocks__/fileMocks.ts b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/List.stories.tsx
index 0c045e9d1..18e762642 100644
--- a/packages/merchant-backend-ui/tests/__mocks__/fileMocks.ts
+++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/List.stories.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2021-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -14,11 +14,15 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- /**
+/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
-// This fixed an error related to the CSS and loading gif breaking my Jest test
-// See https://facebook.github.io/jest/docs/en/webpack.html#handling-static-assets
-export default 'test-file-stub';
+import { FunctionalComponent, h } from "preact";
+import { ListPage as TestedComponent } from "./ListPage.js";
+
+export default {
+ title: "Pages/Accounts/List",
+ component: TestedComponent,
+};
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
new file mode 100644
index 000000000..4ee68cd80
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/ListPage.tsx
@@ -0,0 +1,61 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { TalerMerchantApi } from "@gnu-taler/taler-util";
+import { h, VNode } from "preact";
+import { CardTable } from "./Table.js";
+
+export interface Props {
+ devices: TalerMerchantApi.BankAccountSummaryEntry[];
+ // onLoadMoreBefore?: () => void;
+ // onLoadMoreAfter?: () => void;
+ onCreate: () => void;
+ onDelete: (e: TalerMerchantApi.BankAccountSummaryEntry) => void;
+ onSelect: (e: TalerMerchantApi.BankAccountSummaryEntry) => void;
+}
+
+export function ListPage({
+ devices,
+ onCreate,
+ onDelete,
+ onSelect,
+ // onLoadMoreBefore,
+ // onLoadMoreAfter,
+}: Props): VNode {
+
+ return (
+ <section class="section is-main-section">
+ <CardTable
+ accounts={devices.map((o) => ({
+ ...o,
+ id: String(o.h_wire),
+ }))}
+ onCreate={onCreate}
+ onDelete={onDelete}
+ onSelect={onSelect}
+ // onLoadMoreBefore={onLoadMoreBefore}
+ // hasMoreBefore={!onLoadMoreBefore}
+ // onLoadMoreAfter={onLoadMoreAfter}
+ // hasMoreAfter={!onLoadMoreAfter}
+ />
+ </section>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/Table.tsx
new file mode 100644
index 000000000..efe484402
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/Table.tsx
@@ -0,0 +1,359 @@
+/*
+ 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 { 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;
+
+interface Props {
+ accounts: Entity[];
+ onDelete: (e: Entity) => void;
+ onSelect: (e: Entity) => void;
+ onCreate: () => void;
+}
+
+export function CardTable({
+ accounts,
+ onCreate,
+ onDelete,
+ onSelect,
+}: 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}
+ />
+ ) : (
+ <EmptyTable />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+}
+interface TableProps {
+ rowSelection: string[];
+ accounts: Entity[];
+ onDelete: (e: Entity) => void;
+ onSelect: (e: Entity) => void;
+ rowSelectionHandler: StateUpdater<string[]>;
+}
+
+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 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-magnify 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/merchant-backoffice-ui/src/paths/instance/accounts/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/index.tsx
new file mode 100644
index 000000000..1eda7382d
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/index.tsx
@@ -0,0 +1,111 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util";
+import {
+ useTranslationContext
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { useSessionContext } from "../../../../context/session.js";
+import { useInstanceBankAccounts } from "../../../../hooks/bank.js";
+import { Notification } from "../../../../utils/types.js";
+import { LoginPage } from "../../../login/index.js";
+import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
+import { ListPage } from "./ListPage.js";
+
+interface Props {
+ onCreate: () => void;
+ onSelect: (id: string) => void;
+}
+
+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 instanceof TalerError) {
+ return <ErrorLoadingMerchant error={result} />
+ }
+ if (result.type === "fail") {
+ switch(result.case) {
+ case HttpStatusCode.NotFound: {
+ return <NotFoundPageOrAdminCreate />
+ }
+ case HttpStatusCode.Unauthorized: {
+ return <LoginPage />
+ }
+ default: {
+ assertUnreachable(result)
+ }
+ }
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+ {result.body.accounts.length < 1 &&
+ <NotificationCard notification={{
+ type: "WARN",
+ message: i18n.str`You need to associate a bank account to receive revenue.`,
+ description: i18n.str`Without this the merchant backend will refuse to create new orders.`
+ }} />
+ }
+ <ListPage
+ devices={result.body.accounts}
+ // onLoadMoreBefore={
+ // result.isFirstPage ? undefined: result.loadFirst
+ // }
+ // onLoadMoreAfter={result.isLastPage ? undefined : result.loadNext}
+ onCreate={onCreate}
+ onSelect={(e) => {
+ onSelect(e.h_wire);
+ }}
+ onDelete={(e: TalerMerchantApi.BankAccountSummaryEntry) => {
+ return api.instance.deleteBankAccount(state.token, e.h_wire)
+ .then(() =>
+ setNotif({
+ message: i18n.str`bank account delete successfully`,
+ type: "SUCCESS",
+ }),
+ )
+ .catch((error) =>
+ setNotif({
+ message: i18n.str`could not delete the bank account`,
+ type: "ERROR",
+ description: error.message,
+ }),
+ )
+ }
+ }
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/Update.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/Update.stories.tsx
new file mode 100644
index 000000000..06ea9d07a
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/Update.stories.tsx
@@ -0,0 +1,32 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { 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/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx
new file mode 100644
index 000000000..1a8e9bdc1
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx
@@ -0,0 +1,232 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { TalerMerchantApi } from "@gnu-taler/taler-util";
+import { 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 { undefinedIfEmpty } from "../../../../utils/table.js";
+
+type Entity = TalerMerchantApi.BankAccountEntry & WithId;
+
+const accountAuthType = ["unedit", "none", "basic"];
+interface Props {
+ onUpdate: (d: TalerMerchantApi.AccountPatchDetails) => Promise<void>;
+ onBack?: () => void;
+ account: Entity;
+}
+
+export function UpdatePage({ account, onUpdate, onBack }: Props): VNode {
+ const { i18n } = useTranslationContext();
+
+ const [state, setState] =
+ useState<Partial<TalerMerchantApi.AccountPatchDetails>>(account);
+
+ // @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 facadeURL = safeConvertURL(state.credit_facade_url);
+
+ const errors: FormErrors<TalerMerchantApi.AccountPatchDetails> = {
+ credit_facade_url: !state.credit_facade_url
+ ? undefined
+ : !facadeURL
+ ? i18n.str`Invalid url`
+ : !facadeURL.href.endsWith("/")
+ ? i18n.str`URL should end with a '/'`
+ : facadeURL.searchParams.size > 0
+ ? i18n.str`URL should not contain params`
+ : facadeURL.hash
+ ? i18n.str`URL should not hash param`
+ : 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 credit_facade_url = !state.credit_facade_url
+ ? undefined
+ : facadeURL?.href;
+
+ const credit_facade_credentials:
+ | TalerMerchantApi.FacadeCredentials
+ | undefined =
+ 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 });
+ };
+
+ 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>
+ );
+}
+
+//TODO: move to utils
+export function safeConvertURL(s?: string): URL | undefined {
+ if (!s) return undefined;
+ try {
+ return new URL(s);
+ } catch (e) {
+ return undefined;
+ }
+}
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
new file mode 100644
index 000000000..70942fd55
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx
@@ -0,0 +1,153 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util";
+import {
+ useTranslationContext
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { useSessionContext } from "../../../../context/session.js";
+import { 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";
+
+export type Entity = TalerMerchantApi.AccountPatchDetails & WithId;
+
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+ bid: string;
+}
+export default function UpdateValidator({
+ bid,
+ onConfirm,
+ onBack,
+}: Props): VNode {
+ const { lib: api } = useSessionContext();
+ const { state } = useSessionContext();
+ const result = useBankAccountDetails(bid);
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+
+ const { i18n } = useTranslationContext();
+
+ if (!result) return <Loading />;
+ if (result instanceof TalerError) {
+ return <ErrorLoadingMerchant error={result} />;
+ }
+ if (result.type === "fail") {
+ switch (result.case) {
+ case HttpStatusCode.NotFound: {
+ return <NotFoundPageOrAdminCreate />;
+ }
+ case HttpStatusCode.Unauthorized: {
+ return <LoginPage />
+ }
+ default: {
+ assertUnreachable(result);
+ }
+ }
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+ <UpdatePage
+ 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,
+ );
+ if (resp.type === "fail") {
+ switch (resp.case) {
+ case TestRevenueErrorType.NO_CONFIG: {
+ 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.CLIENT_BAD_REQUEST: {
+ setNotif({
+ message: i18n.str`Could not create account`,
+ type: "ERROR",
+ description: i18n.str`Server replied with "bad request".`,
+ });
+ return;
+ }
+ case TestRevenueErrorType.UNAUTHORIZED: {
+ setNotif({
+ message: i18n.str`Could not create account`,
+ type: "ERROR",
+ description: i18n.str`Unauthorized, try with another credentials.`,
+ });
+ return;
+ }
+ case TestRevenueErrorType.NOT_FOUND: {
+ setNotif({
+ message: i18n.str`Could not create account`,
+ type: "ERROR",
+ description: i18n.str`Check facade URL, server replied with "not found".`,
+ });
+ return;
+ }
+ case TestRevenueErrorType.GENERIC_ERROR: {
+ setNotif({
+ message: i18n.str`Could not create account`,
+ type: "ERROR",
+ description: resp.detail.hint,
+ });
+ return;
+ }
+ default: {
+ assertUnreachable(resp.case)
+ }
+ }
+ }
+ }
+ return api.instance.updateBankAccount(state.token, bid, request)
+ .then(onConfirm)
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not update account`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/details/DetailPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/details/DetailPage.tsx
index d4717f251..3168c7cc4 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/details/DetailPage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/details/DetailPage.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,73 +15,69 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @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";
-import { useTranslator } from "../../../i18n/index.js";
+import { TalerMerchantApi } from "@gnu-taler/taler-util";
-type Entity = MerchantBackend.Instances.InstanceReconfigurationMessage;
+type Entity = TalerMerchantApi.InstanceReconfigurationMessage;
interface Props {
onUpdate: () => void;
onDelete: () => void;
- selected: MerchantBackend.Instances.QueryInstancesResponse;
+ selected: TalerMerchantApi.QueryInstancesResponse;
}
-function convert(from: MerchantBackend.Instances.QueryInstancesResponse): Entity {
- const { accounts, ...rest } = from
- const payto_uris = accounts.filter(a => a.active).map(a => a.payto_uri)
+function convert(
+ from: TalerMerchantApi.QueryInstancesResponse,
+): Entity {
const defaults = {
default_wire_fee_amortization: 1,
- default_pay_delay: { d_us: 1000 * 60 * 60 }, //one hour
- default_wire_transfer_delay: { d_us: 1000 * 60 * 60 * 2 }, //two hours
- }
- return { ...defaults, ...rest, payto_uris };
+ 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 [value, valueHandler] = useState<Partial<Entity>>(convert(selected));
+
+ const { i18n } = useTranslationContext();
- const i18n = useTranslator()
-
- 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>
+ 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 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} >
+ </section>
- <Input<Entity> name="name" readonly label={i18n`Name`} />
- <Input<Entity> name="payto_uris" readonly label={i18n`Account address`} />
-
- </FormProvider>
+ <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>
- <div class="column" />
- </div>
- </section>
-
- </div>
-
-} \ No newline at end of file
+ </section>
+ </div>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/details/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/details/index.tsx
index 2ef0f6d1d..e1a7f87f0 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/details/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/details/index.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,53 +13,78 @@
You should have received 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 { Fragment, h, VNode } from "preact";
+import { HttpStatusCode, TalerError, assertUnreachable } from "@gnu-taler/taler-util";
+import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
+import { ErrorLoadingMerchant } from "../../../components/ErrorLoadingMerchant.js";
import { Loading } from "../../../components/exception/loading.js";
import { DeleteModal } from "../../../components/modal/index.js";
-import { useInstanceContext } from "../../../context/instance.js";
-import { HttpError } from "../../../hooks/backend.js";
-import { useInstanceAPI, useInstanceDetails } from "../../../hooks/instance.js";
+import { useSessionContext } from "../../../context/session.js";
+import { useInstanceDetails } from "../../../hooks/instance.js";
+import { LoginPage } from "../../login/index.js";
+import { NotFoundPageOrAdminCreate } from "../../notfound/index.js";
import { DetailPage } from "./DetailPage.js";
interface Props {
- onUnauthorized: () => VNode;
- onLoadError: (error: HttpError) => 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)
+export default function Detail({
+ onUpdate,
+ onDelete,
+}: Props): VNode {
+ const { state } = useSessionContext();
+ const result = useInstanceDetails();
+ const [deleting, setDeleting] = useState<boolean>(false);
- const { deleteInstance } = useInstanceAPI()
+ // const { deleteInstance } = useInstanceAPI();
+ const { lib } = useSessionContext();
- if (result.clientError && result.isUnauthorized) return onUnauthorized()
- if (result.clientError && result.isNotfound) return onNotFound()
- if (result.loading) return <Loading />
- if (!result.ok) return onLoadError(result)
+ if (!result) return <Loading />
+ if (result instanceof TalerError) {
+ return <ErrorLoadingMerchant error={result} />
+ }
+ if (result.type === "fail") {
+ switch(result.case) {
+ case HttpStatusCode.Unauthorized: {
+ return <LoginPage />
+ }
+ case HttpStatusCode.NotFound: {
+ return <NotFoundPageOrAdminCreate />;
+ }
+ default: {
+ assertUnreachable(result)
+ }
+ }
+ }
- return <Fragment>
- <DetailPage
- selected={result.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) {
- }
- setDeleting(false)
- }}
- />}
- </Fragment>
-} \ No newline at end of file
+ return (
+ <Fragment>
+ <DetailPage
+ selected={result.body}
+ onUpdate={onUpdate}
+ onDelete={() => setDeleting(true)}
+ />
+ {deleting && (
+ <DeleteModal
+ element={{ name: result.body.name, id: state.instance }}
+ onCancel={() => setDeleting(false)}
+ onConfirm={async (): Promise<void> => {
+ if (state.status !== "loggedIn") {
+ return
+ }
+ try {
+ await lib.instance.deleteCurrentInstance(state.token);
+ onDelete();
+ } catch (error) {
+ //FIXME: show message error
+ }
+ setDeleting(false);
+ }}
+ />
+ )}
+ </Fragment>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/details/stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/details/stories.tsx
new file mode 100644
index 000000000..42cb1cb02
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/details/stories.tsx
@@ -0,0 +1,87 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { MerchantApiProviderTesting } from "@gnu-taler/web-util/browser";
+import { FunctionalComponent, h } from "preact";
+import { DetailPage as TestedComponent } from "./DetailPage.js";
+
+export default {
+ title: "Pages/Instance/Detail",
+ component: TestedComponent,
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+function createExample<Props>(
+ Internal: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const component = (args: any) => (
+ <MerchantApiProviderTesting
+ value={{
+ cancelRequest: () => { },
+ changeBackend: () => { },
+ config: {
+ currency: "ARS",
+ version: "1",
+ currencies: {
+ "ASD": {
+ name: "testkudos",
+ alt_unit_names: {},
+ num_fractional_input_digits: 1,
+ num_fractional_normal_digits: 1,
+ num_fractional_trailing_zero_digits: 1,
+ }
+ },
+ exchanges: [],
+ name: "taler-merchant"
+ },
+ hints: [],
+ lib: {} as any,
+ onActivity: (() => { }) as any,
+ url: new URL("asdasd"),
+ }}
+ >
+ <Internal {...(props as any)} />
+ </MerchantApiProviderTesting>
+ );
+ return { component, props };
+}
+
+export const Example = createExample(TestedComponent, {
+ selected: {
+ name: "name",
+ auth: { method: "external" },
+ address: {},
+ user_type: "business",
+ jurisdiction: {},
+ use_stefan: true,
+ default_pay_delay: {
+ d_us: 1000 * 1000, //one second
+ },
+ default_wire_transfer_delay: {
+ d_us: 1000 * 1000, //one second
+ },
+ merchant_pub: "ASDWQEKASJDKSADJ",
+ },
+});
diff --git a/packages/demobank-ui/.storybook/.babelrc b/packages/merchant-backoffice-ui/src/paths/instance/index.stories.ts
index 610b6f339..8f06937df 100644
--- a/packages/demobank-ui/.storybook/.babelrc
+++ b/packages/merchant-backoffice-ui/src/paths/instance/index.stories.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2021-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -14,12 +14,5 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- /**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-{
- "presets": [
- "preact-cli/babel"
- ]
-} \ No newline at end of file
+export * as details from "./details/stories.js";
+export * as kycList from "./kyc/list/ListPage.stories.js";
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.stories.tsx
new file mode 100644
index 000000000..046636b4b
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.stories.tsx
@@ -0,0 +1,57 @@
+/*
+ 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 { PaytoString } from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
+import { ListPage as TestedComponent } from "./ListPage.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" as PaytoString,
+ kyc_url: "http://exchange.taler/kyc",
+ },
+ {
+ aml_status: 1,
+ exchange_url: "http://exchange.taler",
+ payto_uri: "payto://iban/de123123123" as PaytoString,
+ },
+ {
+ aml_status: 2,
+ exchange_url: "http://exchange.taler",
+ payto_uri: "payto://iban/de123123123" as PaytoString,
+ },
+ ],
+ },
+});
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx
index 6b9a50660..3eeed1d7b 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2021-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -19,16 +19,16 @@
* @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 { MerchantBackend } from "../../../../declaration.js";
-import { Translate, useTranslator } from "../../../../i18n/index.js";
export interface Props {
- status: MerchantBackend.Instances.AccountKycRedirects;
+ status: TalerMerchantApi.AccountKycRedirects;
}
export function ListPage({ status }: Props): VNode {
- const i18n = useTranslator();
+ const { i18n } = useTranslationContext();
return (
<section class="section is-main-section">
@@ -38,7 +38,7 @@ export function ListPage({ status }: Props): VNode {
<span class="icon">
<i class="mdi mdi-clock" />
</span>
- <Translate>Pending KYC verification</Translate>
+ <i18n.Translate>Pending KYC verification</i18n.Translate>
</p>
<div class="card-header-icon" aria-label="more options" />
@@ -63,7 +63,7 @@ export function ListPage({ status }: Props): VNode {
<span class="icon">
<i class="mdi mdi-clock" />
</span>
- <Translate>Timed out</Translate>
+ <i18n.Translate>Timed out</i18n.Translate>
</p>
<div class="card-header-icon" aria-label="more options" />
@@ -85,43 +85,71 @@ export function ListPage({ status }: Props): VNode {
);
}
interface PendingTableProps {
- entries: MerchantBackend.Instances.MerchantAccountKycRedirect[];
+ entries: TalerMerchantApi.MerchantAccountKycRedirect[];
}
interface TimedOutTableProps {
- entries: MerchantBackend.Instances.ExchangeKycTimeout[];
+ entries: TalerMerchantApi.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>
- <Translate>Exchange</Translate>
+ <i18n.Translate>Exchange</i18n.Translate>
</th>
<th>
- <Translate>Target account</Translate>
+ <i18n.Translate>Target account</i18n.Translate>
</th>
<th>
- <Translate>KYC URL</Translate>
+ <i18n.Translate>Reason</i18n.Translate>
</th>
</tr>
</thead>
<tbody>
{entries.map((e, i) => {
- return (
- <tr key={i}>
- <td>{e.exchange_url}</td>
- <td>{e.payto_uri}</td>
- <td>
- <a href={e.kyc_url} target="_black" rel="noreferrer">
- {e.kyc_url}
- </a>
- </td>
- </tr>
- );
+ 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>
@@ -130,19 +158,20 @@ function PendingTable({ entries }: PendingTableProps): VNode {
}
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>
- <Translate>Exchange</Translate>
+ <i18n.Translate>Exchange</i18n.Translate>
</th>
<th>
- <Translate>Code</Translate>
+ <i18n.Translate>Code</i18n.Translate>
</th>
<th>
- <Translate>Http Status</Translate>
+ <i18n.Translate>Http Status</i18n.Translate>
</th>
</tr>
</thead>
@@ -163,6 +192,7 @@ function TimedOutTable({ entries }: TimedOutTableProps): VNode {
}
function EmptyTable(): VNode {
+ const { i18n } = useTranslationContext();
return (
<div class="content has-text-grey has-text-centered">
<p>
@@ -171,7 +201,7 @@ function EmptyTable(): VNode {
</span>
</p>
<p>
- <Translate>No pending kyc verification!</Translate>
+ <i18n.Translate>No pending kyc verification!</i18n.Translate>
</p>
</div>
);
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/index.tsx
index 3ef95cb34..ed0e1220f 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/index.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2021-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -19,30 +19,55 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { h, VNode } from "preact";
+import { HttpStatusCode, TalerError, assertUnreachable } from "@gnu-taler/taler-util";
+import { VNode, h } from "preact";
+import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
import { Loading } from "../../../../components/exception/loading.js";
-import { HttpError } from "../../../../hooks/backend.js";
import { useInstanceKYCDetails } from "../../../../hooks/instance.js";
import { ListPage } from "./ListPage.js";
interface Props {
- onUnauthorized: () => VNode;
- onLoadError: (error: HttpError) => VNode;
- onNotFound: () => VNode;
}
-export default function ListKYC({
- onUnauthorized,
- onLoadError,
- onNotFound,
-}: Props): VNode {
+export default function ListKYC(_p: Props): VNode {
const result = useInstanceKYCDetails();
- if (result.clientError && result.isUnauthorized) return onUnauthorized();
- if (result.clientError && result.isNotfound) return onNotFound();
- if (result.loading) return <Loading />;
- if (!result.ok) return onLoadError(result);
+ if (!result) return <Loading />
+ if (result instanceof TalerError) {
+ return <ErrorLoadingMerchant error={result} />
+ }
+ /**
+ * This component just render known kyc requirements.
+ * If query fail then is safe to hide errors.
+ */
+ if (result.type === "fail") {
+ switch (result.case) {
+ case HttpStatusCode.GatewayTimeout: {
+ return <div />
+ }
+ case HttpStatusCode.BadGateway: {
+ const status = result.body;
+
+ if (!status) {
+ return <div>no kyc required</div>;
+ }
+ return <ListPage status={status} />;
- const status = result.data.type === "ok" ? undefined : result.data.status;
+ }
+ case HttpStatusCode.ServiceUnavailable: {
+ return <div />
+ }
+ case HttpStatusCode.Unauthorized: {
+ return <div />
+ }
+ case HttpStatusCode.NotFound: {
+ return <div />;
+ }
+ default: {
+ assertUnreachable(result)
+ }
+ }
+ }
+ const status = result.body;
if (!status) {
return <div>no kyc required</div>;
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/Create.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/Create.stories.tsx
index e658dff8f..fc814b68f 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/Create.stories.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/Create.stories.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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
@@ -33,7 +33,7 @@ export default {
function createExample<Props>(
Component: FunctionalComponent<Props>,
- props: Partial<Props>
+ props: Partial<Props>,
) {
const r = (args: any) => <Component {...args} />;
r.args = props;
@@ -42,12 +42,13 @@ function createExample<Props>(
export const Example = createExample(TestedComponent, {
instanceConfig: {
- default_max_deposit_fee: "",
- default_max_wire_fee: "",
default_pay_delay: {
- d_us: 1000 * 60 * 60,
+ d_us: 1000 * 1000 * 60 * 60, //one hour
},
- default_wire_fee_amortization: 1,
+ default_wire_transfer_delay: {
+ d_us: 1000 * 1000 * 60 * 60, //one hour
+ },
+ use_stefan: true,
},
instanceInventory: [
{
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 56bb65b90..7be3d23f6 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
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2021-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -19,69 +19,81 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { add, isAfter, isBefore, isFuture } from "date-fns";
-import { Amounts } from "@gnu-taler/taler-util";
-import { Fragment, h, VNode } from "preact";
+import {
+ AbsoluteTime,
+ AmountString,
+ Amounts,
+ Duration,
+ TalerMerchantApi,
+ TalerProtocolDuration,
+} from "@gnu-taler/taler-util";
+import {
+ useTranslationContext
+} from "@gnu-taler/web-util/browser";
+import { format, isFuture } from "date-fns";
+import { Fragment, VNode, h } from "preact";
import { useEffect, useState } from "preact/hooks";
import {
- FormProvider,
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 { ProductList } from "../../../../components/product/ProductList.js";
-import { useConfigContext } from "../../../../context/config.js";
-import { Duration, MerchantBackend, WithId } from "../../../../declaration.js";
-import { Translate, useTranslator } from "../../../../i18n/index.js";
-import { OrderCreateSchema as schema } from "../../../../schemas/index.js";
-import { rate } from "../../../../utils/amount.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 { InputNumber } from "../../../../components/form/InputNumber.js";
-import { InputBoolean } from "../../../../components/form/InputBoolean.js";
+import { ProductList } from "../../../../components/product/ProductList.js";
+import { useSessionContext } from "../../../../context/session.js";
+import { usePreference } from "../../../../hooks/preference.js";
+import { rate } from "../../../../utils/amount.js";
import { undefinedIfEmpty } from "../../../../utils/table.js";
interface Props {
- onCreate: (d: MerchantBackend.Orders.PostOrderRequest) => void;
+ onCreate: (d: TalerMerchantApi.PostOrderRequest) => void;
onBack?: () => void;
instanceConfig: InstanceConfig;
- instanceInventory: (MerchantBackend.Products.ProductDetail & WithId)[];
+ instanceInventory: (TalerMerchantApi.ProductDetail & WithId)[];
}
interface InstanceConfig {
- default_max_wire_fee: string;
- default_max_deposit_fee: string;
- default_wire_fee_amortization: number;
- default_pay_delay: Duration;
+ use_stefan: boolean;
+ default_pay_delay: TalerProtocolDuration;
+ default_wire_transfer_delay: TalerProtocolDuration;
}
-function with_defaults(config: InstanceConfig): Partial<Entity> {
- const defaultPayDeadline =
- !config.default_pay_delay || config.default_pay_delay.d_us === "forever"
- ? undefined
- : add(new Date(), { seconds: config.default_pay_delay.d_us / 1000 });
+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_wire_fee: config.default_max_wire_fee,
- max_fee: config.default_max_deposit_fee,
- wire_fee_amortization: config.default_wire_fee_amortization,
+ max_fee: undefined,
+ createToken: true,
pay_deadline: defaultPayDeadline,
refund_deadline: defaultPayDeadline,
- createToken: true,
+ wire_transfer_deadline: defaultWireDeadline,
},
shipping: {},
- extra: "",
+ extra: {},
};
}
interface ProductAndQuantity {
- product: MerchantBackend.Products.ProductDetail & WithId;
+ product: TalerMerchantApi.ProductDetail & WithId;
quantity: number;
}
export interface ProductMap {
@@ -95,156 +107,144 @@ interface Pricing {
}
interface Shipping {
delivery_date?: Date;
- delivery_location?: MerchantBackend.Location;
+ delivery_location?: TalerMerchantApi.Location;
fullfilment_url?: string;
}
interface Payments {
- refund_deadline?: Date;
- pay_deadline?: Date;
- wire_transfer_deadline?: Date;
- auto_refund_deadline?: Date;
+ refund_deadline: Duration;
+ pay_deadline: Duration;
+ wire_transfer_deadline: Duration;
+ auto_refund_deadline: Duration;
max_fee?: string;
- max_wire_fee?: string;
- wire_fee_amortization?: number;
createToken: boolean;
minimum_age?: number;
}
interface Entity {
inventoryProducts: ProductMap;
- products: MerchantBackend.Product[];
+ products: TalerMerchantApi.Product[];
pricing: Partial<Pricing>;
payments: Partial<Payments>;
shipping: Partial<Shipping>;
- extra: string;
+ 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 [value, valueHandler] = useState(with_defaults(instanceConfig));
- const config = useConfigContext();
+ const { config } = useSessionContext();
+ 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 inventoryList = Object.values(value.inventoryProducts || {});
const productList = Object.values(value.products || {});
- const i18n = useTranslator();
+ 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`required` : undefined,
+ summary: !value.pricing?.summary ? i18n.str`required` : undefined,
order_price: !value.pricing?.order_price
- ? i18n`required`
- : Amounts.isZero(value.pricing.order_price)
- ? i18n`must be greater than 0`
- : undefined,
+ ? i18n.str`required`
+ : !parsedPrice
+ ? i18n.str`not valid`
+ : Amounts.isZero(parsedPrice)
+ ? i18n.str`must be greater than 0`
+ : undefined,
}),
- extra:
- value.extra && !stringIsValidJSON(value.extra)
- ? i18n`not a valid json`
- : undefined,
payments: undefinedIfEmpty({
refund_deadline: !value.payments?.refund_deadline
? undefined
- : !isFuture(value.payments.refund_deadline)
- ? i18n`should be in the future`
: value.payments.pay_deadline &&
- isBefore(value.payments.refund_deadline, value.payments.pay_deadline)
- ? i18n`refund deadline cannot be before pay deadline`
- : value.payments.wire_transfer_deadline &&
- isBefore(
- value.payments.wire_transfer_deadline,
- value.payments.refund_deadline
- )
- ? i18n`wire transfer deadline cannot be before refund deadline`
- : undefined,
+ 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
- ? undefined
- : !isFuture(value.payments.pay_deadline)
- ? i18n`should be in the future`
+ ? i18n.str`required`
: value.payments.wire_transfer_deadline &&
- isBefore(
- value.payments.wire_transfer_deadline,
- value.payments.pay_deadline
- )
- ? i18n`wire transfer deadline cannot be before pay 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
- : !isFuture(value.payments.auto_refund_deadline)
- ? i18n`should be in the future`
: !value.payments?.refund_deadline
- ? i18n`should have a refund deadline`
- : !isAfter(
- value.payments.refund_deadline,
- value.payments.auto_refund_deadline
- )
- ? i18n`auto refund cannot be after refund deadline`
- : undefined,
+ ? 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`should be in the future`
- : undefined,
+ ? i18n.str`should be in the future`
+ : undefined,
}),
};
const hasErrors = Object.keys(errors).some(
- (k) => (errors as any)[k] !== undefined
+ (k) => (errors as any)[k] !== undefined,
);
const submit = (): void => {
- const order = schema.cast(value);
+ const order = value as any; //schema.cast(value);
if (!value.payments) return;
if (!value.shipping) return;
- const request: MerchantBackend.Orders.PostOrderRequest = {
+ const request: TalerMerchantApi.PostOrderRequest = {
order: {
amount: order.pricing.order_price,
summary: order.pricing.summary,
products: productList,
- extra: value.extra,
- pay_deadline: value.payments.pay_deadline
- ? {
- t_s: Math.floor(value.payments.pay_deadline.getTime() / 1000),
- }
- : undefined,
- wire_transfer_deadline: value.payments.wire_transfer_deadline
- ? {
- t_s: Math.floor(
- value.payments.wire_transfer_deadline.getTime() / 1000
- ),
- }
- : undefined,
- refund_deadline: value.payments.refund_deadline
- ? {
- t_s: Math.floor(value.payments.refund_deadline.getTime() / 1000),
- }
- : undefined,
+ extra: undefinedIfEmpty(value.extra),
+ pay_deadline: AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ value.payments.pay_deadline!,
+ ),
+ ),
+ wire_transfer_deadline: AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ value.payments.wire_transfer_deadline!,
+ ),
+ ),
+ refund_deadline: AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ value.payments.refund_deadline!,
+ ),
+ ),
auto_refund: value.payments.auto_refund_deadline
- ? {
- d_us: Math.floor(
- value.payments.auto_refund_deadline.getTime() * 1000
- ),
- }
+ ? Duration.toTalerProtocolDuration(
+ value.payments.auto_refund_deadline,
+ )
: undefined,
- wire_fee_amortization: value.payments.wire_fee_amortization as number,
- max_fee: value.payments.max_fee as string,
- max_wire_fee: value.payments.max_wire_fee as string,
-
+ max_fee: value.payments.max_fee as AmountString,
delivery_date: value.shipping.delivery_date
? { t_s: value.shipping.delivery_date.getTime() / 1000 }
: undefined,
@@ -263,8 +263,8 @@ export function CreatePage({
};
const addProductToTheInventoryList = (
- product: MerchantBackend.Products.ProductDetail & WithId,
- quantity: number
+ product: TalerMerchantApi.ProductDetail & WithId,
+ quantity: number,
) => {
valueHandler((v) => {
const inventoryProducts = { ...v.inventoryProducts };
@@ -281,7 +281,7 @@ export function CreatePage({
});
};
- const addNewProduct = async (product: MerchantBackend.Product) => {
+ const addNewProduct = async (product: TalerMerchantApi.Product) => {
return valueHandler((v) => {
const products = v.products ? [...v.products, product] : [];
return { ...v, products };
@@ -297,7 +297,7 @@ export function CreatePage({
};
const [editingProduct, setEditingProduct] = useState<
- MerchantBackend.Product | undefined
+ TalerMerchantApi.Product | undefined
>(undefined);
const totalPriceInventory = inventoryList.reduce((prev, cur) => {
@@ -308,7 +308,7 @@ export function CreatePage({
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;
+ return Amounts.add(prev, Amounts.mult(p, cur.quantity ?? 0).amount).amount;
}, zero);
const hasProducts = inventoryList.length > 0 || productList.length > 0;
@@ -317,6 +317,8 @@ export function CreatePage({
const totalAsString = Amounts.stringify(totalPrice.amount);
const allProducts = productList.concat(inventoryList.map(asProduct));
+ const [newField, setNewField] = useState("");
+
useEffect(() => {
valueHandler((v) => {
return {
@@ -331,25 +333,65 @@ export function CreatePage({
}, [hasProducts, totalAsString]);
const discountOrRise = rate(
- value.pricing?.order_price || `${config.currency}:0`,
- totalAsString
+ parsedPrice ?? Amounts.zeroOfCurrency(config.currency),
+ totalPrice.amount,
);
- const minAgeByProducts = allProducts.reduce(
+ const minAgeByProducts = inventoryList.reduce(
(cur, prev) =>
- !prev.minimum_age || cur > prev.minimum_age ? cur : prev.minimum_age,
- 0
+ !prev.product.minimum_age || cur > prev.product.minimum_age ? cur : prev.product.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("advanceOrderMode", false);
+ }}
+ >
+ <a>
+ <span>
+ <i18n.Translate>Simple</i18n.Translate>
+ </span>
+ </a>
+ </li>
+ <li
+ class={settings.advanceOrderMode ? "is-active" : ""}
+ onClick={() => {
+ updateSettings("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`Manage products in order`}
+ label={i18n.str`Manage products in order`}
alternative={
allProducts.length > 0 && (
<p>
@@ -358,7 +400,7 @@ export function CreatePage({
</p>
)
}
- tooltip={i18n`Manage list of products in the order.`}
+ tooltip={i18n.str`Manage list of products in the order.`}
>
<InventoryProductForm
currentProducts={value.inventoryProducts || {}}
@@ -366,21 +408,23 @@ export function CreatePage({
inventory={instanceInventory}
/>
- <NonInventoryProductFrom
- productToEdit={editingProduct}
- onAddProduct={(p) => {
- setEditingProduct(undefined);
- return addNewProduct(p);
- }}
- />
+ {settings.advanceOrderMode && (
+ <NonInventoryProductFrom
+ productToEdit={editingProduct}
+ onAddProduct={(p) => {
+ setEditingProduct(undefined);
+ return addNewProduct(p);
+ }}
+ />
+ )}
{allProducts.length > 0 && (
<ProductList
list={allProducts}
actions={[
{
- name: i18n`Remove`,
- tooltip: i18n`Remove this product from the order.`,
+ name: i18n.str`Remove`,
+ tooltip: i18n.str`Remove this product from the order.`,
handler: (e, index) => {
if (e.product_id) {
removeProductFromTheInventoryList(e.product_id);
@@ -404,141 +448,299 @@ export function CreatePage({
<Fragment>
<InputCurrency
name="pricing.products_price"
- label={i18n`Total price`}
+ label={i18n.str`Total price`}
readonly
- tooltip={i18n`total product price added up`}
+ tooltip={i18n.str`total product price added up`}
/>
<InputCurrency
name="pricing.order_price"
- label={i18n`Total price`}
+ label={i18n.str`Total price`}
addonAfter={
discountOrRise > 0 &&
(discountOrRise < 1
? `discount of %${Math.round(
- (1 - discountOrRise) * 100
+ (1 - discountOrRise) * 100,
)}`
: `rise of %${Math.round((discountOrRise - 1) * 100)}`)
}
- tooltip={i18n`Amount to be paid by the customer`}
+ tooltip={i18n.str`Amount to be paid by the customer`}
/>
</Fragment>
) : (
<InputCurrency
name="pricing.order_price"
- label={i18n`Order price`}
- tooltip={i18n`final order price`}
+ label={i18n.str`Order price`}
+ tooltip={i18n.str`final order price`}
/>
)}
<Input
name="pricing.summary"
inputType="multiline"
- label={i18n`Summary`}
- tooltip={i18n`Title of the order to be shown to the customer`}
+ label={i18n.str`Summary`}
+ tooltip={i18n.str`Title of the order to be shown to the customer`}
/>
- <InputGroup
- name="shipping"
- label={i18n`Shipping and Fulfillment`}
- initialActive
- >
- <InputDate
- name="shipping.delivery_date"
- label={i18n`Delivery date`}
- tooltip={i18n`Deadline for physical delivery assured by the merchant.`}
- />
- {value.shipping?.delivery_date && (
- <InputGroup
- name="shipping.delivery_location"
- label={i18n`Location`}
- tooltip={i18n`address where the products will be delivered`}
- >
- <InputLocation name="shipping.delivery_location" />
- </InputGroup>
- )}
- <Input
- name="shipping.fullfilment_url"
- label={i18n`Fulfillment URL`}
- tooltip={i18n`URL to which the user will be redirected after successful payment.`}
- />
- </InputGroup>
-
- <InputGroup
- name="payments"
- label={i18n`Taler payment options`}
- tooltip={i18n`Override default Taler payment settings for this order`}
- >
- <InputDate
- name="payments.pay_deadline"
- label={i18n`Payment deadline`}
- tooltip={i18n`Deadline for the customer to pay for the offer before it expires. Inventory products will be reserved until this deadline.`}
- />
- <InputDate
- name="payments.refund_deadline"
- label={i18n`Refund deadline`}
- tooltip={i18n`Time until which the order can be refunded by the merchant.`}
- />
- <InputDate
- name="payments.wire_transfer_deadline"
- label={i18n`Wire transfer deadline`}
- tooltip={i18n`Deadline for the exchange to make the wire transfer.`}
- />
- <InputDate
- name="payments.auto_refund_deadline"
- label={i18n`Auto-refund deadline`}
- tooltip={i18n`Time until which the wallet will automatically check for refunds without user interaction.`}
- />
+ {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>
+ )}
- <InputCurrency
- name="payments.max_fee"
- label={i18n`Maximum deposit fee`}
- tooltip={i18n`Maximum deposit fees the merchant is willing to cover for this order. Higher deposit fees must be covered in full by the consumer.`}
- />
- <InputCurrency
- name="payments.max_wire_fee"
- label={i18n`Maximum wire fee`}
- tooltip={i18n`Maximum aggregate wire fees the merchant is willing to cover for this order. Wire fees exceeding this amount are to be covered by the customers.`}
- />
- <InputNumber
- name="payments.wire_fee_amortization"
- label={i18n`Wire fee amortization`}
- tooltip={i18n`Factor by which wire fees exceeding the above threshold are divided to determine the share of excess wire fees to be paid explicitly by the consumer.`}
- />
- <InputBoolean
- name="payments.createToken"
- label={i18n`Create token`}
- tooltip={i18n`Uncheck this option if the merchant backend generated an order ID with enough entropy to prevent adversarial claims.`}
- />
- <InputNumber
- name="payments.minimum_age"
- label={i18n`Minimum age required`}
- tooltip={i18n`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`Min age defined by the producs is ${minAgeByProducts}`
- : undefined
- }
- />
- </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>
+ )}
- <InputGroup
- name="extra"
- label={i18n`Additional information`}
- tooltip={i18n`Custom information to be included in the contract for this order.`}
- >
- <Input
+ {settings.advanceOrderMode && (
+ <InputGroup
name="extra"
- inputType="multiline"
- label={`Value`}
- tooltip={i18n`You must enter a value in JavaScript Object Notation (JSON).`}
- />
- </InputGroup>
+ 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, idx) => {
+ return (
+ <Input
+ name={`extra.${key}`}
+ key={String(idx)}
+ 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
+ ) {
+ delete value.extra[key];
+ }
+ valueHandler({
+ ...value,
+ });
+ e.preventDefault();
+ }}
+ >
+ 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]: "",
+ },
+ });
+ e.preventDefault();
+ }}
+ >
+ add
+ </button>
+ </div>
+ </InputGroup>
+ )}
</FormProvider>
<div class="buttons is-right mt-5">
{onBack && (
<button class="button" onClick={onBack}>
- <Translate>Cancel</Translate>
+ <i18n.Translate>Cancel</i18n.Translate>
</button>
)}
<button
@@ -546,7 +748,7 @@ export function CreatePage({
onClick={submit}
disabled={hasErrors}
>
- <Translate>Confirm</Translate>
+ <i18n.Translate>Confirm</i18n.Translate>
</button>
</div>
</div>
@@ -557,7 +759,7 @@ export function CreatePage({
);
}
-function asProduct(p: ProductAndQuantity): MerchantBackend.Product {
+function asProduct(p: ProductAndQuantity): TalerMerchantApi.Product {
return {
product_id: p.product.id,
image: p.product.image,
@@ -566,6 +768,27 @@ function asProduct(p: ProductAndQuantity): MerchantBackend.Product {
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/merchant-backoffice-ui/src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx
index d94013da3..32f3f05c7 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,12 +13,15 @@
You should have received 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, useState } from "preact/hooks";
+import { HttpStatusCode, TalerError, assertUnreachable } from "@gnu-taler/taler-util";
+import { Loading, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
+import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
import { CreatedSuccessfully } from "../../../../components/notifications/CreatedSuccessfully.js";
-import { useOrderAPI } from "../../../../hooks/order.js";
-import { Translate } from "../../../../i18n/index.js";
+import { useOrderDetails } from "../../../../hooks/order.js";
+import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
import { Entity } from "./index.js";
+import { LoginPage } from "../../../login/index.js";
interface Props {
entity: Entity;
@@ -26,64 +29,113 @@ interface Props {
onCreateAnother?: () => void;
}
-export function OrderCreatedSuccessfully({ entity, onConfirm, onCreateAnother }: Props): VNode {
- const { getPaymentURL } = useOrderAPI()
- const [url, setURL] = useState<string | undefined>(undefined)
+export function OrderCreatedSuccessfully({
+ entity,
+ onConfirm,
+ onCreateAnother,
+}: Props): VNode {
+ const result = useOrderDetails(entity.response.order_id)
+ const { i18n } = useTranslationContext();
- useEffect(() => {
- getPaymentURL(entity.response.order_id).then(response => {
- setURL(response.data)
- })
- }, [getPaymentURL, entity.response.order_id])
+ 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.BadGateway: {
+ return <div>Failed to obtain a response from the exchange</div>;
+ }
+ case HttpStatusCode.GatewayTimeout: {
+ return (
+ <div>The merchant's interaction with the exchange took too long</div>
+ );
+ }
+ case HttpStatusCode.Unauthorized: {
+ return <LoginPage />
+ }
+ default: {
+ assertUnreachable(result)
+ }
+ }
+ }
- return <CreatedSuccessfully onConfirm={onConfirm} onCreateAnother={onCreateAnother}>
- <div class="field is-horizontal">
- <div class="field-label is-normal">
- <label class="label"><Translate>Amount</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>
+ const url = result.body.order_status === "unpaid" ?
+ result.body.taler_pay_uri :
+ result.body.contract_terms.fulfillment_url
+
+ 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>
- </div>
- <div class="field is-horizontal">
- <div class="field-label is-normal">
- <label class="label"><Translate>Summary</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 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>
- <div class="field is-horizontal">
- <div class="field-label is-normal">
- <label class="label"><Translate>Order ID</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 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>
- <div class="field is-horizontal">
- <div class="field-label is-normal">
- <label class="label"><Translate>Payment URL</Translate></label>
+ <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-body is-flex-grow-3">
- <div class="field">
- <p class="control">
- <input class="input" readonly value={url} />
- </p>
+ <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>
- </div>
- </CreatedSuccessfully>;
+ </CreatedSuccessfully>
+ );
}
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 b58a6507e..861114014 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
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,68 +15,106 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { Fragment, h, VNode } from 'preact';
-import { useState } from 'preact/hooks';
+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";
import { Loading } from "../../../../components/exception/loading.js";
import { NotificationCard } from "../../../../components/menu/index.js";
-import { MerchantBackend } from "../../../../declaration.js";
-import { HttpError } from "../../../../hooks/backend.js";
+import { useSessionContext } from "../../../../context/session.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 { LoginPage } from "../../../login/index.js";
+import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
import { CreatePage } from "./CreatePage.js";
-import { OrderCreatedSuccessfully } from "./OrderCreatedSuccessfully.js";
export type Entity = {
- request: MerchantBackend.Orders.PostOrderRequest,
- response: MerchantBackend.Orders.PostOrderResponse
-}
+ request: TalerMerchantApi.PostOrderRequest;
+ response: TalerMerchantApi.PostOrderResponse;
+};
interface Props {
onBack?: () => void;
- onConfirm: () => void;
- onUnauthorized: () => VNode;
- onNotFound: () => VNode;
- onLoadError: (error: HttpError) => VNode;
+ onConfirm: (id: string) => void;
}
-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.clientError && detailsResult.isUnauthorized) return onUnauthorized()
- if (detailsResult.clientError && detailsResult.isNotfound) return onNotFound()
- if (detailsResult.loading) return <Loading />
- if (!detailsResult.ok) return onLoadError(detailsResult)
+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 (inventoryResult.clientError && inventoryResult.isUnauthorized) return onUnauthorized()
- if (inventoryResult.clientError && inventoryResult.isNotfound) return onNotFound()
- if (inventoryResult.loading) return <Loading />
- if (!inventoryResult.ok) return onLoadError(inventoryResult)
+ if (!detailsResult) return <Loading />
+ if (detailsResult instanceof TalerError) {
+ return <ErrorLoadingMerchant error={detailsResult} />
+ }
+ if (detailsResult.type === "fail") {
+ switch (detailsResult.case) {
+ case HttpStatusCode.Unauthorized: {
+ return <LoginPage />
+ }
+ case HttpStatusCode.NotFound: {
+ return <NotFoundPageOrAdminCreate />;
+ }
+ default: {
+ assertUnreachable(detailsResult);
+ }
+ }
+ }
+ if (!inventoryResult) return <Loading />
+ if (inventoryResult instanceof TalerError) {
+ return <ErrorLoadingMerchant error={inventoryResult} />
+ }
+ if (inventoryResult.type === "fail") {
+ switch (inventoryResult.case) {
+ case HttpStatusCode.NotFound: {
+ return <NotFoundPageOrAdminCreate />;
+ }
+ case HttpStatusCode.Unauthorized: {
+ return <LoginPage />
+ }
+ default: {
+ assertUnreachable(inventoryResult);
+ }
+ }
+ }
- return <Fragment>
-
- <NotificationCard notification={notif} />
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
- <CreatePage
- onBack={onBack}
- onCreate={(request: MerchantBackend.Orders.PostOrderRequest) => {
- createOrder(request).then(onConfirm).catch((error) => {
- setNotif({
- message: 'could not create order',
- type: "ERROR",
- description: error.message
- })
- })
- }}
- instanceConfig={detailsResult.data}
- instanceInventory={inventoryResult.data}
+ <CreatePage
+ onBack={onBack}
+ onCreate={(request: TalerMerchantApi.PostOrderRequest) => {
+ lib.instance.createOrder(state.token, request)
+ .then((r) => {
+ if (r.type === "ok") {
+ return onConfirm(r.body.order_id)
+ } else {
+ setNotif({
+ message: "could not create order",
+ type: "ERROR",
+ });
+ }
+ })
+ .catch((error) => {
+ setNotif({
+ message: "could not create order",
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ instanceConfig={detailsResult.body}
+ instanceInventory={inventoryResult.body}
/>
- </Fragment>
+ </Fragment>
+ );
}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/Detail.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/Detail.stories.tsx
index 8c21665e6..7d4877db9 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/Detail.stories.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/Detail.stories.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2021-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -19,9 +19,9 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
+import { AmountString, TalerMerchantApi } from "@gnu-taler/taler-util";
import { addDays } from "date-fns";
-import { h, VNode, FunctionalComponent } from "preact";
-import { MerchantBackend } from "../../../../declaration.js";
+import { FunctionalComponent, h } from "preact";
import { DetailPage as TestedComponent } from "./DetailPage.js";
export default {
@@ -35,28 +35,25 @@ export default {
function createExample<Props>(
Component: FunctionalComponent<Props>,
- props: Partial<Props>
+ props: Partial<Props>,
) {
const r = (args: any) => <Component {...args} />;
r.args = props;
return r;
}
-const defaultContractTerm = {
- amount: "TESTKUDOS:10",
+const defaultContractTerm: TalerMerchantApi.ContractTerms = {
+ amount: "TESTKUDOS:10" as AmountString,
timestamp: {
t_s: new Date().getTime() / 1000,
},
- auditors: [],
exchanges: [],
- max_fee: "TESTKUDOS:1",
- max_wire_fee: "TESTKUDOS:1",
+ max_fee: "TESTKUDOS:1" as AmountString,
merchant: {} as any,
merchant_base_url: "http://merchant.url/",
order_id: "2021.165-03GDFC26Y1NNG",
products: [],
summary: "text summary",
- wire_fee_amortization: 1,
wire_transfer_deadline: {
t_s: "never",
},
@@ -68,7 +65,7 @@ const defaultContractTerm = {
},
wire_method: "x-taler-bank",
h_wire: "asd",
-} as MerchantBackend.ContractTerms;
+};
// contract_terms: defaultContracTerm,
export const Claimed = createExample(TestedComponent, {
@@ -85,16 +82,16 @@ export const PaidNotRefundable = createExample(TestedComponent, {
order_status: "paid",
contract_terms: defaultContractTerm,
refunded: false,
- deposit_total: "TESTKUDOS:10",
- exchange_ec: 0,
+ deposit_total: "TESTKUDOS:10" as AmountString,
+ exchange_code: 0,
order_status_url: "http://merchant.backend/status",
- exchange_hc: 0,
- refund_amount: "TESTKUDOS:0",
+ exchange_http_status: 0,
+ refund_amount: "TESTKUDOS:0" as AmountString,
refund_details: [],
refund_pending: false,
wire_details: [],
- wire_reports: [],
wired: false,
+ wire_reports: [],
},
});
@@ -109,15 +106,15 @@ export const PaidRefundable = createExample(TestedComponent, {
},
},
refunded: false,
- deposit_total: "TESTKUDOS:10",
- exchange_ec: 0,
+ deposit_total: "TESTKUDOS:10" as AmountString,
+ exchange_code: 0,
order_status_url: "http://merchant.backend/status",
- exchange_hc: 0,
- refund_amount: "TESTKUDOS:0",
+ exchange_http_status: 0,
+ refund_amount: "TESTKUDOS:0" as AmountString,
refund_details: [],
+ wire_reports: [],
refund_pending: false,
wire_details: [],
- wire_reports: [],
wired: false,
},
});
@@ -132,6 +129,6 @@ export const Unpaid = createExample(TestedComponent, {
},
summary: "text summary",
taler_pay_uri: "pay uri",
- total_amount: "TESTKUDOS:10",
+ total_amount: "TESTKUDOS:10" as AmountString,
},
});
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx
index 3445a86df..498ea83e3 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2021-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -19,9 +19,17 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { AmountJson, Amounts } from "@gnu-taler/taler-util";
-import { format } from "date-fns";
-import { Fragment, h, VNode } from "preact";
+import {
+ AmountJson,
+ Amounts,
+ TalerMerchantApi,
+ 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";
@@ -32,108 +40,98 @@ 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 { Translate, useTranslator } from "../../../../i18n/index.js";
+import { useSessionContext } from "../../../../context/session.js";
+import {
+ datetimeFormatForSettings,
+ usePreference,
+} from "../../../../hooks/preference.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;
+type Entity = TalerMerchantApi.MerchantOrderStatusResponse;
+type CT = TalerMerchantApi.ContractTerms;
interface Props {
onBack: () => void;
selected: Entity;
id: string;
- onRefund: (id: string, value: MerchantBackend.Orders.RefundRequest) => void;
+ onRefund: (id: string, value: TalerMerchantApi.RefundRequest) => void;
}
-type Paid = MerchantBackend.Orders.CheckPaymentPaidResponse & {
+type Paid = TalerMerchantApi.CheckPaymentPaidResponse & {
refund_taken: string;
};
-type Unpaid = MerchantBackend.Orders.CheckPaymentUnpaidResponse;
-type Claimed = MerchantBackend.Orders.CheckPaymentClaimedResponse;
+type Unpaid = TalerMerchantApi.CheckPaymentUnpaidResponse;
+type Claimed = TalerMerchantApi.CheckPaymentClaimedResponse;
function ContractTerms({ value }: { value: CT }) {
- const i18n = useTranslator();
+ const { i18n } = useTranslationContext();
return (
- <InputGroup name="contract_terms" label={i18n`Contract Terms`}>
+ <InputGroup name="contract_terms" label={i18n.str`Contract Terms`}>
<FormProvider<CT> object={value} valueHandler={null}>
<Input<CT>
readonly
name="summary"
- label={i18n`Summary`}
- tooltip={i18n`human-readable description of the whole purchase`}
+ label={i18n.str`Summary`}
+ tooltip={i18n.str`human-readable description of the whole purchase`}
/>
<InputCurrency<CT>
readonly
name="amount"
- label={i18n`Amount`}
- tooltip={i18n`total price for the transaction`}
+ label={i18n.str`Amount`}
+ tooltip={i18n.str`total price for the transaction`}
/>
{value.fulfillment_url && (
<Input<CT>
readonly
name="fulfillment_url"
- label={i18n`Fulfillment URL`}
- tooltip={i18n`URL for this purchase`}
+ label={i18n.str`Fulfillment URL`}
+ tooltip={i18n.str`URL for this purchase`}
/>
)}
<Input<CT>
readonly
name="max_fee"
- label={i18n`Max fee`}
- tooltip={i18n`maximum total deposit fee accepted by the merchant for this contract`}
- />
- <Input<CT>
- readonly
- name="max_wire_fee"
- label={i18n`Max wire fee`}
- tooltip={i18n`maximum wire fee accepted by the merchant`}
- />
- <Input<CT>
- readonly
- name="wire_fee_amortization"
- label={i18n`Wire fee amortization`}
- tooltip={i18n`over how many customer transactions does the merchant expect to amortize wire fees on average`}
+ 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`Created at`}
- tooltip={i18n`time when this contract was generated`}
+ label={i18n.str`Created at`}
+ tooltip={i18n.str`time when this contract was generated`}
/>
<InputDate<CT>
readonly
name="refund_deadline"
- label={i18n`Refund deadline`}
- tooltip={i18n`after this deadline has passed no refunds will be accepted`}
+ 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`Payment deadline`}
- tooltip={i18n`after this deadline, the merchant won't accept payments for the contract`}
+ 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`Wire transfer deadline`}
- tooltip={i18n`transfer deadline for the exchange`}
+ label={i18n.str`Wire transfer deadline`}
+ tooltip={i18n.str`transfer deadline for the exchange`}
/>
<InputDate<CT>
readonly
name="delivery_date"
- label={i18n`Delivery date`}
- tooltip={i18n`time indicating when the order should be delivered`}
+ 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`Location`}
- tooltip={i18n`where the order will be delivered`}
+ label={i18n.str`Location`}
+ tooltip={i18n.str`where the order will be delivered`}
>
<InputLocation name="payments.delivery_location" />
</InputGroup>
@@ -141,14 +139,14 @@ function ContractTerms({ value }: { value: CT }) {
<InputDuration<CT>
readonly
name="auto_refund"
- label={i18n`Auto-refund delay`}
- tooltip={i18n`how long the wallet should try to get an automatic refund for the purchase`}
+ 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`Extra info`}
- tooltip={i18n`extra data that is only interpreted by the merchant frontend`}
+ label={i18n.str`Extra info`}
+ tooltip={i18n.str`extra data that is only interpreted by the merchant frontend`}
/>
</FormProvider>
</InputGroup>
@@ -160,8 +158,12 @@ function ClaimedPage({
order,
}: {
id: string;
- order: MerchantBackend.Orders.CheckPaymentClaimedResponse;
+ order: TalerMerchantApi.CheckPaymentClaimedResponse;
}) {
+ const now = new Date();
+ const refundable =
+ order.contract_terms.refund_deadline.t_s !== "never" &&
+ now.getTime() < order.contract_terms.refund_deadline.t_s * 1000;
const events: Event[] = [];
if (order.contract_terms.timestamp.t_s !== "never") {
events.push({
@@ -177,20 +179,20 @@ function ClaimedPage({
type: "deadline",
});
}
- if (order.contract_terms.refund_deadline.t_s !== "never") {
+ if (order.contract_terms.refund_deadline.t_s !== "never" && refundable) {
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.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"
@@ -203,7 +205,8 @@ function ClaimedPage({
}
const [value, valueHandler] = useState<Partial<Claimed>>(order);
- const i18n = useTranslator();
+ const { i18n } = useTranslationContext();
+ const [settings] = usePreference();
return (
<div>
@@ -216,13 +219,14 @@ function ClaimedPage({
<div class="level">
<div class="level-left">
<div class="level-item">
- <Translate>Order</Translate> #{id}
+ <i18n.Translate>Order</i18n.Translate> #{id}
<div class="tag is-info ml-4">
- <Translate>claimed</Translate>
+ <i18n.Translate>claimed</i18n.Translate>
</div>
</div>
</div>
</div>
+
<div class="level">
<div class="level-left">
<div class="level-item">
@@ -244,12 +248,16 @@ function ClaimedPage({
>
<p>
<b>
- <Translate>claimed at</Translate>:
+ <i18n.Translate>claimed at</i18n.Translate>:
</b>{" "}
- {format(
- new Date(order.contract_terms.timestamp.t_s * 1000),
- "yyyy-MM-dd HH:mm:ss"
- )}
+ {order.contract_terms.timestamp.t_s === "never"
+ ? "never"
+ : format(
+ new Date(
+ order.contract_terms.timestamp.t_s * 1000,
+ ),
+ datetimeFormatForSettings(settings),
+ )}
</p>
</div>
</div>
@@ -262,13 +270,13 @@ function ClaimedPage({
<div class="columns">
<div class="column is-4">
<div class="title">
- <Translate>Timeline</Translate>
+ <i18n.Translate>Timeline</i18n.Translate>
</div>
<Timeline events={events} />
</div>
<div class="column is-8">
<div class="title">
- <Translate>Payment details</Translate>
+ <i18n.Translate>Payment details</i18n.Translate>
</div>
<FormProvider<Claimed>
object={value}
@@ -278,17 +286,17 @@ function ClaimedPage({
name="contract_terms.summary"
readonly
inputType="multiline"
- label={i18n`Summary`}
+ label={i18n.str`Summary`}
/>
<InputCurrency
name="contract_terms.amount"
readonly
- label={i18n`Amount`}
+ label={i18n.str`Amount`}
/>
<Input<Claimed>
name="order_status"
readonly
- label={i18n`Order status`}
+ label={i18n.str`Order status`}
/>
</FormProvider>
</div>
@@ -298,7 +306,7 @@ function ClaimedPage({
{order.contract_terms.products.length ? (
<Fragment>
<div class="title">
- <Translate>Product list</Translate>
+ <i18n.Translate>Product list</i18n.Translate>
</div>
<ProductList list={order.contract_terms.products} />
</Fragment>
@@ -320,25 +328,16 @@ function PaidPage({
onRefund,
}: {
id: string;
- order: MerchantBackend.Orders.CheckPaymentPaidResponse;
+ order: TalerMerchantApi.CheckPaymentPaidResponse;
onRefund: (id: string) => void;
}) {
+ const now = new Date();
+ const refundable =
+ order.contract_terms.refund_deadline.t_s !== "never" &&
+ now.getTime() < order.contract_terms.refund_deadline.t_s * 1000;
+
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") {
+ if (order.contract_terms.refund_deadline.t_s !== "never" && refundable) {
events.push({
when: new Date(order.contract_terms.refund_deadline.t_s * 1000),
description: "refund deadline",
@@ -372,61 +371,72 @@ function PaidPage({
});
}
});
- 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",
+ const ra = !order.refunded ? undefined : Amounts.parse(order.refund_amount);
+ const am = Amounts.parseOrThrow(order.contract_terms.amount);
+ if (ra && Amounts.cmp(ra, am) === 1) {
+ if (order.wire_details && order.wire_details.length) {
+ if (order.wire_details.length > 1) {
+ let last: TalerMerchantApi.TransactionWireTransfer | null = null;
+ let first: TalerMerchantApi.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;
});
- }
- } else {
- order.wire_details.forEach((e) => {
- if (e.execution_time.t_s !== "never") {
+ const last_time = last!.execution_time.t_s;
+ if (last_time !== "never") {
events.push({
- when: new Date(e.execution_time.t_s * 1000),
- description: `wired ${e.amount}`,
- type: "wired",
+ 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 nextEvent = events.find((e) => {
+ return e.when.getTime() > now.getTime();
+ });
+
const [value, valueHandler] = useState<Partial<Paid>>(order);
- const { url } = useBackendContext();
- const refundHost = url.replace(/.*:\/\//, ""); // remove protocol part
- const proto = url.startsWith("http://") ? "taler+http" : "taler";
- const refundurl = `${proto}://refund/${refundHost}/${order.contract_terms.order_id}/`;
- const refundable =
- new Date().getTime() < order.contract_terms.refund_deadline.t_s * 1000;
- const i18n = useTranslator();
+ const { state } = useSessionContext();
+
+ const refundurl = stringifyRefundUri({
+ merchantBaseUrl: state.backendUrl.href,
+ orderId: order.contract_terms.order_id,
+ });
+ const { i18n } = useTranslationContext();
const amount = Amounts.parseOrThrow(order.contract_terms.amount);
const refund_taken = order.refund_details.reduce((prev, cur) => {
@@ -446,18 +456,18 @@ function PaidPage({
<div class="level">
<div class="level-left">
<div class="level-item">
- <Translate>Order</Translate> #{id}
+ <i18n.Translate>Order</i18n.Translate> #{id}
<div class="tag is-success ml-4">
- <Translate>paid</Translate>
+ <i18n.Translate>paid</i18n.Translate>
</div>
{order.wired ? (
<div class="tag is-success ml-4">
- <Translate>wired</Translate>
+ <i18n.Translate>wired</i18n.Translate>
</div>
) : null}
{order.refunded ? (
<div class="tag is-danger ml-4">
- <Translate>refunded</Translate>
+ <i18n.Translate>refunded</i18n.Translate>
</div>
) : null}
</div>
@@ -477,8 +487,8 @@ function PaidPage({
class="has-tooltip-left"
data-tooltip={
refundable
- ? i18n`refund order`
- : i18n`not refundable`
+ ? i18n.str`refund order`
+ : i18n.str`not refundable`
}
>
<button
@@ -486,7 +496,7 @@ function PaidPage({
disabled={!refundable}
onClick={() => onRefund(id)}
>
- <Translate>refund</Translate>
+ <i18n.Translate>refund</i18n.Translate>
</button>
</span>
</div>
@@ -504,24 +514,18 @@ function PaidPage({
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
- // maxWidth: '100%',
}}
>
- <p>
- <a
- href={order.contract_terms.fulfillment_url}
- rel="nofollow"
- target="new"
- >
- {order.contract_terms.fulfillment_url}
- </a>
- </p>
- <p>
- {format(
- new Date(order.contract_terms.timestamp.t_s * 1000),
- "yyyy/MM/dd HH:mm:ss"
- )}
- </p>
+ {nextEvent && (
+ <p>
+ <i18n.Translate>Next event in </i18n.Translate>{" "}
+ {formatDistance(
+ nextEvent.when,
+ new Date(),
+ // "yyyy/MM/dd HH:mm:ss",
+ )}
+ </p>
+ )}
</div>
</div>
</div>
@@ -533,41 +537,41 @@ function PaidPage({
<div class="columns">
<div class="column is-4">
<div class="title">
- <Translate>Timeline</Translate>
+ <i18n.Translate>Timeline</i18n.Translate>
</div>
<Timeline events={events} />
</div>
<div class="column is-8">
<div class="title">
- <Translate>Payment details</Translate>
+ <i18n.Translate>Payment details</i18n.Translate>
</div>
<FormProvider<Paid>
object={value}
valueHandler={valueHandler}
>
- {/* <InputCurrency<Paid> name="deposit_total" readonly label={i18n`Deposit total`} /> */}
+ {/* <InputCurrency<Paid> name="deposit_total" readonly label={i18n.str`Deposit total`} /> */}
{order.refunded && (
<InputCurrency<Paid>
name="refund_amount"
readonly
- label={i18n`Refunded amount`}
+ label={i18n.str`Refunded amount`}
/>
)}
{order.refunded && (
<InputCurrency<Paid>
name="refund_taken"
readonly
- label={i18n`Refund taken`}
+ label={i18n.str`Refund taken`}
/>
)}
<Input<Paid>
name="order_status"
readonly
- label={i18n`Order status`}
+ label={i18n.str`Order status`}
/>
<TextField<Paid>
name="order_status_url"
- label={i18n`Status URL`}
+ label={i18n.str`Status URL`}
>
<a
target="_blank"
@@ -580,7 +584,7 @@ function PaidPage({
{order.refunded && (
<TextField<Paid>
name="order_status_url"
- label={i18n`Refund URI`}
+ label={i18n.str`Refund URI`}
>
<a target="_blank" rel="noreferrer" href={refundurl}>
{refundurl}
@@ -595,7 +599,7 @@ function PaidPage({
{order.contract_terms.products.length ? (
<Fragment>
<div class="title">
- <Translate>Product list</Translate>
+ <i18n.Translate>Product list</i18n.Translate>
</div>
<ProductList list={order.contract_terms.products} />
</Fragment>
@@ -617,10 +621,11 @@ function UnpaidPage({
order,
}: {
id: string;
- order: MerchantBackend.Orders.CheckPaymentUnpaidResponse;
+ order: TalerMerchantApi.CheckPaymentUnpaidResponse;
}) {
const [value, valueHandler] = useState<Partial<Unpaid>>(order);
- const i18n = useTranslator();
+ const { i18n } = useTranslationContext();
+ const [settings] = usePreference();
return (
<div>
<section class="hero is-hero-bar">
@@ -629,11 +634,11 @@ function UnpaidPage({
<div class="level-left">
<div class="level-item">
<h1 class="title">
- <Translate>Order</Translate> #{id}
+ <i18n.Translate>Order</i18n.Translate> #{id}
</h1>
</div>
<div class="tag is-dark">
- <Translate>unpaid</Translate>
+ <i18n.Translate>unpaid</i18n.Translate>
</div>
</div>
</div>
@@ -651,7 +656,7 @@ function UnpaidPage({
>
<p>
<b>
- <Translate>pay at</Translate>:
+ <i18n.Translate>pay at</i18n.Translate>:
</b>{" "}
<a
href={order.order_status_url}
@@ -663,13 +668,13 @@ function UnpaidPage({
</p>
<p>
<b>
- <Translate>created at</Translate>:
+ <i18n.Translate>created at</i18n.Translate>:
</b>{" "}
{order.creation_time.t_s === "never"
? "never"
: format(
new Date(order.creation_time.t_s * 1000),
- "yyyy-MM-dd HH:mm:ss"
+ datetimeFormatForSettings(settings),
)}
</p>
</div>
@@ -687,26 +692,29 @@ function UnpaidPage({
<Input<Unpaid>
readonly
name="summary"
- label={i18n`Summary`}
- tooltip={i18n`human-readable description of the whole purchase`}
+ label={i18n.str`Summary`}
+ tooltip={i18n.str`human-readable description of the whole purchase`}
/>
<InputCurrency<Unpaid>
readonly
name="total_amount"
- label={i18n`Amount`}
- tooltip={i18n`total price for the transaction`}
+ label={i18n.str`Amount`}
+ tooltip={i18n.str`total price for the transaction`}
/>
<Input<Unpaid>
name="order_status"
readonly
- label={i18n`Order status`}
+ label={i18n.str`Order status`}
/>
<Input<Unpaid>
name="order_status_url"
readonly
- label={i18n`Order status URL`}
+ label={i18n.str`Order status URL`}
/>
- <TextField<Unpaid> name="taler_pay_uri" label={i18n`Payment URI`}>
+ <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>
@@ -722,7 +730,7 @@ function UnpaidPage({
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":
@@ -734,10 +742,10 @@ export function DetailPage({ id, selected, onRefund, onBack }: Props): VNode {
default:
return (
<div>
- <Translate>
+ <i18n.Translate>
Unknown order status. This is an error, please contact the
administrator.
- </Translate>
+ </i18n.Translate>
</div>
);
}
@@ -761,7 +769,7 @@ export function DetailPage({ id, selected, onRefund, onBack }: Props): VNode {
<div class="column is-four-fifths">
<div class="buttons is-right mt-5">
<button class="button" onClick={onBack}>
- <Translate>Back</Translate>
+ <i18n.Translate>Back</i18n.Translate>
</button>
</div>
</div>
@@ -770,7 +778,3 @@ export function DetailPage({ id, selected, onRefund, onBack }: Props): VNode {
</Fragment>
);
}
-
-async function copyToClipboard(text: string) {
- return navigator.clipboard.writeText(text);
-}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/Timeline.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/Timeline.tsx
index bea65607a..2d62e2252 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/Timeline.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/Timeline.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,6 +16,7 @@
import { format } from "date-fns";
import { h } from "preact";
import { useEffect, useState } from "preact/hooks";
+import { datetimeFormatForSettings, usePreference } from "../../../../hooks/preference.js";
interface Props {
events: Event[];
@@ -30,7 +31,7 @@ export function Timeline({ events: e }: Props) {
});
events.sort((a, b) => a.when.getTime() - b.when.getTime());
-
+ const [settings] = usePreference();
const [state, setState] = useState(events);
useEffect(() => {
const handle = setTimeout(() => {
@@ -67,7 +68,7 @@ export function Timeline({ events: e }: Props) {
);
case "start":
return (
- <div class="timeline-marker is-icon is-success">
+ <div class="timeline-marker is-icon">
<i class="mdi mdi-flag " />
</div>
);
@@ -104,7 +105,7 @@ export function Timeline({ events: e }: Props) {
}
})()}
<div class="timeline-content">
- <p class="heading">{format(e.when, "yyyy/MM/dd HH:mm:ss")}</p>
+ {e.description !== "now" && <p class="heading">{format(e.when, datetimeFormatForSettings(settings))}</p>}
<p>{e.description}</p>
</div>
</div>
@@ -117,12 +118,12 @@ export interface Event {
when: Date;
description: string;
type:
- | "start"
- | "refund"
- | "refund-taken"
- | "wired"
- | "wired-range"
- | "deadline"
- | "delivery"
- | "now";
+ | "start"
+ | "refund"
+ | "refund-taken"
+ | "wired"
+ | "wired-range"
+ | "deadline"
+ | "delivery"
+ | "now";
}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/index.tsx
index 0549ab8ed..b28e59b29 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/index.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,55 +13,94 @@
You should have received 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 { Fragment, h, VNode } from "preact";
+import {
+ HttpStatusCode,
+ TalerError,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
+import {
+ useTranslationContext
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
+import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
import { Loading } from "../../../../components/exception/loading.js";
import { NotificationCard } from "../../../../components/menu/index.js";
-import { HttpError } from "../../../../hooks/backend.js";
-import { useOrderDetails, useOrderAPI } from "../../../../hooks/order.js";
-import { useTranslator } from "../../../../i18n/index.js";
+import { useSessionContext } from "../../../../context/session.js";
+import { useOrderDetails } from "../../../../hooks/order.js";
import { Notification } from "../../../../utils/types.js";
+import { LoginPage } from "../../../login/index.js";
+import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
import { DetailPage } from "./DetailPage.js";
export interface Props {
oid: string;
-
onBack: () => void;
- onUnauthorized: () => VNode;
- onNotFound: () => VNode;
- onLoadError: (error: HttpError) => 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 = useTranslator()
+export default function Update({ oid, onBack }: Props): VNode {
+ const result = useOrderDetails(oid);
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { lib: api } = useSessionContext();
+ const { state } = useSessionContext();
- if (result.clientError && result.isUnauthorized) return onUnauthorized()
- if (result.clientError && result.isNotfound) return onNotFound()
- if (result.loading) return <Loading />
- if (!result.ok) return onLoadError(result)
+ const { i18n } = useTranslationContext();
- return <Fragment>
+ 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.BadGateway: {
+ return <div>Failed to obtain a response from the exchange</div>;
+ }
+ case HttpStatusCode.GatewayTimeout: {
+ return (
+ <div>The merchant's interaction with the exchange took too long</div>
+ );
+ }
+ case HttpStatusCode.Unauthorized: {
+ return <LoginPage />
+ }
+ default: {
+ assertUnreachable(result);
+ }
+ }
+ }
- <NotificationCard notification={notif} />
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
- <DetailPage
- onBack={onBack}
- id={oid}
- onRefund={(id, value) => refundOrder(id, value)
- .then(() => setNotif({
- message: i18n`refund created successfully`,
- type: "SUCCESS"
- })).catch((error) => setNotif({
- message: i18n`could not create the refund`,
- type: "ERROR",
- description: error.message
- }))
- }
- selected={result.data}
- />
- </Fragment>
-} \ No newline at end of file
+ <DetailPage
+ onBack={onBack}
+ id={oid}
+ onRefund={(id, value) => {
+ if (state.status !== "loggedIn") {
+ return;
+ }
+ api.instance
+ .addRefund(state.token, 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.body}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/List.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/List.stories.tsx
index f68c3fd2e..5c9969689 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/List.stories.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/List.stories.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2021-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -19,8 +19,9 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { h, VNode, FunctionalComponent } from "preact";
+import { FunctionalComponent, h } from "preact";
import { ListPage as TestedComponent } from "./ListPage.js";
+import { AmountString } from "@gnu-taler/taler-util";
export default {
title: "Pages/Order/List",
@@ -43,7 +44,7 @@ export default {
function createExample<Props>(
Component: FunctionalComponent<Props>,
- props: Partial<Props>
+ props: Partial<Props>,
) {
const r = (args: any) => <Component {...args} />;
r.args = props;
@@ -54,7 +55,7 @@ export const Example = createExample(TestedComponent, {
orders: [
{
id: "123",
- amount: "TESTKUDOS:10",
+ amount: "TESTKUDOS:10" as AmountString,
paid: false,
refundable: true,
row_id: 1,
@@ -66,7 +67,7 @@ export const Example = createExample(TestedComponent, {
},
{
id: "234",
- amount: "TESTKUDOS:12",
+ amount: "TESTKUDOS:12" as AmountString,
paid: true,
refundable: true,
row_id: 2,
@@ -79,7 +80,7 @@ export const Example = createExample(TestedComponent, {
},
{
id: "456",
- amount: "TESTKUDOS:1",
+ amount: "TESTKUDOS:1" as AmountString,
paid: false,
refundable: false,
row_id: 3,
@@ -92,7 +93,7 @@ export const Example = createExample(TestedComponent, {
},
{
id: "234",
- amount: "TESTKUDOS:12",
+ amount: "TESTKUDOS:12" as AmountString,
paid: false,
refundable: false,
row_id: 4,
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 69e60954f..408bc0c0a 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
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,132 +15,208 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { format } from 'date-fns';
-import { h, VNode } from 'preact';
-import { useState } from 'preact/hooks';
+import { AbsoluteTime, TalerMerchantApi } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { format } from "date-fns";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
import { DatePicker } from "../../../../components/picker/DatePicker.js";
-import { MerchantBackend, WithId } from "../../../../declaration.js";
-import { Translate, useTranslator } from '../../../../i18n/index.js';
+import { dateFormatForSettings, usePreference } from "../../../../hooks/preference.js";
import { CardTable } from "./Table.js";
export interface ListPageProps {
- errorOrderId: string | undefined,
-
- onShowAll: () => void,
- onShowPaid: () => void,
- onShowRefunded: () => void,
- onShowNotWired: () => void,
+ onShowAll: () => void;
+ onShowNotPaid: () => void;
+ onShowPaid: () => void;
+ onShowRefunded: () => void;
+ onShowNotWired: () => void;
+ onShowWired: () => void;
onCopyURL: (id: string) => void;
- isAllActive: string,
- isPaidActive: string,
- isRefundedActive: string,
- isNotWiredActive: string,
+ isAllActive: string;
+ isPaidActive: string;
+ isNotPaidActive: string;
+ isRefundedActive: string;
+ isNotWiredActive: string;
+ isWiredActive: string;
- jumpToDate?: Date,
- onSelectDate: (date?: Date) => void,
+ jumpToDate?: AbsoluteTime;
+ onSelectDate: (date?: AbsoluteTime) => void;
- orders: (MerchantBackend.Orders.OrderHistoryEntry & WithId)[];
+ orders: (TalerMerchantApi.OrderHistoryEntry & WithId)[];
onLoadMoreBefore?: () => void;
- hasMoreBefore?: boolean;
- hasMoreAfter?: boolean;
onLoadMoreAfter?: () => void;
- onSelectOrder: (o: MerchantBackend.Orders.OrderHistoryEntry & WithId) => void;
- onRefundOrder: (o: MerchantBackend.Orders.OrderHistoryEntry & WithId) => void;
- onSearchOrderById: (id: string) => void;
+ onSelectOrder: (o: TalerMerchantApi.OrderHistoryEntry & WithId) => void;
+ onRefundOrder: (o: TalerMerchantApi.OrderHistoryEntry & WithId) => void;
onCreate: () => void;
}
-export function ListPage({ orders, errorOrderId, isAllActive, onSelectOrder, onRefundOrder, onSearchOrderById, jumpToDate, onCopyURL, onShowAll, onShowPaid, onShowRefunded, onShowNotWired, onSelectDate, isPaidActive, isRefundedActive, isNotWiredActive, onCreate }: ListPageProps): VNode {
- const i18n = useTranslator();
- const dateTooltip = i18n`select date to show nearby orders`;
+export function ListPage({
+ 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 [orderId, setOrderId] = useState<string>('');
-
- return <section class="section is-main-section">
+ const [settings] = usePreference();
- <div class="level">
- <div class="level-left">
- <div class="level-item">
- <div class="field has-addons">
- <div class="control">
- <input class={errorOrderId ? "input is-danger" : "input"} type="text" value={orderId} onChange={e => setOrderId(e.currentTarget.value)} placeholder={i18n`order id`} />
- {errorOrderId && <p class="help is-danger">{errorOrderId}</p>}
- </div>
- <span class="has-tooltip-bottom" data-tooltip={i18n`jump to order with the given order ID`}>
- <button class="button" onClick={(e) => onSearchOrderById(orderId)}>
- <span class="icon"><i class="mdi mdi-arrow-right" /></span>
- </button>
- </span>
+ 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>
- </div>
- <div class="columns">
- <div class="column is-two-thirds">
- <div class="tabs" style={{overflow:'inherit'}}>
- <ul>
- <li class={isAllActive}>
- <div class="has-tooltip-right" data-tooltip={i18n`remove all filters`}>
- <a onClick={onShowAll}><Translate>All</Translate></a>
+ <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 || jumpToDate.t_ms === "never" ? "" : format(jumpToDate.t_ms, dateFormatForSettings(settings))}
+ placeholder={i18n.str`date (${dateFormatForSettings(settings)})`}
+ onClick={() => {
+ setPickDate(true);
+ }}
+ />
+ </span>
</div>
- </li>
- <li class={isPaidActive}>
- <div class="has-tooltip-right" data-tooltip={i18n`only show paid orders`}>
- <a onClick={onShowPaid}><Translate>Paid</Translate></a>
+ <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>
- </li>
- <li class={isRefundedActive}>
- <div class="has-tooltip-right" data-tooltip={i18n`only show orders with refunds`}>
- <a onClick={onShowRefunded}><Translate>Refunded</Translate></a>
- </div>
- </li>
- <li class={isNotWiredActive}>
- <div class="has-tooltip-left" data-tooltip={i18n`only show orders where customers paid, but wire payments from payment provider are still pending`}>
- <a onClick={onShowNotWired}><Translate>Not wired</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" onClick={() => onSelectDate(undefined)}>
- <span class="icon" data-tooltip={i18n`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, 'yyyy/MM/dd')} placeholder={i18n`date (YYYY/MM/DD)`} onClick={() => { setPickDate(true); }} />
- </span>
- </div>
- <div class="control">
- <span class="has-tooltip-left" data-tooltip={dateTooltip}>
- <a class="button" 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} />
+ <DatePicker
+ opened={pickDate}
+ closeFunction={() => setPickDate(false)}
+ dateReceiver={(d) => {
+ onSelectDate(AbsoluteTime.fromMilliseconds(d.getTime()))
+ }}
+ />
- <CardTable orders={orders}
- onCreate={onCreate}
- onCopyURL={onCopyURL}
- onSelect={onSelectOrder}
- onRefund={onRefundOrder} />
- </section>;
+ <CardTable
+ orders={orders}
+ onCreate={onCreate}
+ onCopyURL={onCopyURL}
+ onSelect={onSelectOrder}
+ onRefund={onRefundOrder}
+ onLoadMoreAfter={onLoadMoreAfter}
+ onLoadMoreBefore={onLoadMoreBefore}
+ />
+ </Fragment>
+ );
}
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 02d5b0bfc..5ece34409 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
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2021-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -19,9 +19,12 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { Amounts } from "@gnu-taler/taler-util";
+import { Amounts, TalerMerchantApi } from "@gnu-taler/taler-util";
+import {
+ useTranslationContext
+} from "@gnu-taler/web-util/browser";
import { format } from "date-fns";
-import { h, VNode } from "preact";
+import { VNode, h } from "preact";
import { StateUpdater, useState } from "preact/hooks";
import {
FormErrors,
@@ -32,12 +35,14 @@ 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 { Translate, useTranslator } from "../../../../i18n/index.js";
+import { useSessionContext } from "../../../../context/session.js";
+import {
+ datetimeFormatForSettings,
+ usePreference,
+} from "../../../../hooks/preference.js";
import { mergeRefunds } from "../../../../utils/amount.js";
-type Entity = MerchantBackend.Orders.OrderHistoryEntry & WithId;
+type Entity = TalerMerchantApi.OrderHistoryEntry & WithId;
interface Props {
orders: Entity[];
onRefund: (value: Entity) => void;
@@ -45,8 +50,6 @@ interface Props {
onCreate: () => void;
onSelect: (order: Entity) => void;
onLoadMoreBefore?: () => void;
- hasMoreBefore?: boolean;
- hasMoreAfter?: boolean;
onLoadMoreAfter?: () => void;
}
@@ -58,12 +61,10 @@ export function CardTable({
onSelect,
onLoadMoreAfter,
onLoadMoreBefore,
- hasMoreAfter,
- hasMoreBefore,
}: Props): VNode {
const [rowSelection, rowSelectionHandler] = useState<string[]>([]);
- const i18n = useTranslator();
+ const { i18n } = useTranslationContext();
return (
<div class="card has-table">
@@ -72,13 +73,13 @@ export function CardTable({
<span class="icon">
<i class="mdi mdi-cash-register" />
</span>
- <Translate>Orders</Translate>
+ <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`create order`}>
+ <span class="has-tooltip-left" data-tooltip={i18n.str`create order`}>
<button class="button is-info" type="button" onClick={onCreate}>
<span class="icon is-small">
<i class="mdi mdi-plus mdi-36px" />
@@ -100,8 +101,6 @@ export function CardTable({
rowSelectionHandler={rowSelectionHandler}
onLoadMoreAfter={onLoadMoreAfter}
onLoadMoreBefore={onLoadMoreBefore}
- hasMoreAfter={hasMoreAfter}
- hasMoreBefore={hasMoreBefore}
/>
) : (
<EmptyTable />
@@ -120,8 +119,6 @@ interface TableProps {
onSelect: (id: Entity) => void;
rowSelectionHandler: StateUpdater<string[]>;
onLoadMoreBefore?: () => void;
- hasMoreBefore?: boolean;
- hasMoreAfter?: boolean;
onLoadMoreAfter?: () => void;
}
@@ -132,31 +129,27 @@ function Table({
onCopyURL,
onLoadMoreAfter,
onLoadMoreBefore,
- hasMoreAfter,
- hasMoreBefore,
}: TableProps): VNode {
+ const { i18n } = useTranslationContext();
+ const [settings] = usePreference();
return (
<div class="table-container">
{onLoadMoreBefore && (
- <button
- class="button is-fullwidth"
- disabled={!hasMoreBefore}
- onClick={onLoadMoreBefore}
- >
- <Translate>load newer orders</Translate>
+ <button class="button is-fullwidth" onClick={onLoadMoreBefore}>
+ <i18n.Translate>load first page</i18n.Translate>
</button>
)}
<table class="table is-striped is-hoverable is-fullwidth">
<thead>
<tr>
<th style={{ minWidth: 100 }}>
- <Translate>Date</Translate>
+ <i18n.Translate>Date</i18n.Translate>
</th>
<th style={{ minWidth: 100 }}>
- <Translate>Amount</Translate>
+ <i18n.Translate>Amount</i18n.Translate>
</th>
<th style={{ minWidth: 400 }}>
- <Translate>Summary</Translate>
+ <i18n.Translate>Summary</i18n.Translate>
</th>
<th style={{ minWidth: 50 }} />
</tr>
@@ -173,7 +166,7 @@ function Table({
? "never"
: format(
new Date(i.timestamp.t_s * 1000),
- "yyyy/MM/dd HH:mm:ss"
+ datetimeFormatForSettings(settings),
)}
</td>
<td
@@ -196,7 +189,7 @@ function Table({
type="button"
onClick={(): void => onRefund(i)}
>
- <Translate>Refund</Translate>
+ <i18n.Translate>Refund</i18n.Translate>
</button>
)}
{!i.paid && (
@@ -205,7 +198,7 @@ function Table({
type="button"
onClick={(): void => onCopyURL(i)}
>
- <Translate>copy url</Translate>
+ <i18n.Translate>copy url</i18n.Translate>
</button>
)}
</div>
@@ -216,12 +209,10 @@ function Table({
</tbody>
</table>
{onLoadMoreAfter && (
- <button
- class="button is-fullwidth"
- disabled={!hasMoreAfter}
- onClick={onLoadMoreAfter}
- >
- <Translate>load older orders</Translate>
+ <button class="button is-fullwidth"
+ data-tooltip={i18n.str`load more orders after the last one`}
+ onClick={onLoadMoreAfter}>
+ <i18n.Translate>load next page</i18n.Translate>
</button>
)}
</div>
@@ -229,15 +220,18 @@ function Table({
}
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" />
+ <i class="mdi mdi-magnify mdi-48px" />
</span>
</p>
<p>
- <Translate>No orders have been found matching your query!</Translate>
+ <i18n.Translate>
+ No orders have been found matching your query!
+ </i18n.Translate>
</p>
</div>
);
@@ -245,8 +239,8 @@ function EmptyTable(): VNode {
interface RefundModalProps {
onCancel: () => void;
- onConfirm: (value: MerchantBackend.Orders.RefundRequest) => void;
- order: MerchantBackend.Orders.MerchantOrderStatusResponse;
+ onConfirm: (value: TalerMerchantApi.RefundRequest) => void;
+ order: TalerMerchantApi.MerchantOrderStatusResponse;
}
export function RefundModal({
@@ -256,19 +250,20 @@ export function RefundModal({
}: RefundModalProps): VNode {
type State = { mainReason?: string; description?: string; refund?: string };
const [form, setValue] = useState<State>({});
- const i18n = useTranslator();
+ const [settings] = usePreference();
+ const { i18n } = useTranslationContext();
// const [errors, setErrors] = useState<FormErrors<State>>({});
const refunds = (
order.order_status === "paid" ? order.refund_details : []
).reduce(mergeRefunds, []);
- const config = useConfigContext();
+ const { config } = useSessionContext();
const totalRefunded = refunds
.map((r) => r.amount)
.reduce(
(p, c) => Amounts.add(p, Amounts.parseOrThrow(c)).amount,
- Amounts.zeroOfCurrency(config.currency)
+ Amounts.zeroOfCurrency(config.currency),
);
const orderPrice =
order.order_status === "paid"
@@ -277,28 +272,28 @@ export function RefundModal({
const totalRefundable = !orderPrice
? Amounts.zeroOfCurrency(totalRefunded.currency)
: refunds.length
- ? Amounts.sub(orderPrice, totalRefunded).amount
- : orderPrice;
+ ? Amounts.sub(orderPrice, totalRefunded).amount
+ : orderPrice;
const isRefundable = Amounts.isNonZero(totalRefundable);
- const duplicatedText = i18n`duplicated`;
+ const duplicatedText = i18n.str`duplicated`;
const errors: FormErrors<State> = {
- mainReason: !form.mainReason ? i18n`required` : undefined,
+ mainReason: !form.mainReason ? i18n.str`required` : undefined,
description:
!form.description && form.mainReason !== duplicatedText
- ? i18n`required`
+ ? i18n.str`required`
: undefined,
refund: !form.refund
- ? i18n`required`
+ ? i18n.str`required`
: !Amounts.parse(form.refund)
- ? i18n`invalid format`
- : Amounts.cmp(totalRefundable, Amounts.parse(form.refund)!) === -1
- ? i18n`this value exceed the refundable amount`
- : undefined,
+ ? 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
+ (k) => (errors as Record<string, unknown>)[k] !== undefined,
);
const validateAndConfirm = () => {
@@ -306,7 +301,7 @@ export function RefundModal({
if (!form.refund) return;
onConfirm({
refund: Amounts.stringify(
- Amounts.add(Amounts.parse(form.refund)!, totalRefunded).amount
+ Amounts.add(Amounts.parse(form.refund)!, totalRefunded).amount,
),
reason:
form.description === undefined
@@ -339,13 +334,13 @@ export function RefundModal({
<thead>
<tr>
<th>
- <Translate>date</Translate>
+ <i18n.Translate>date</i18n.Translate>
</th>
<th>
- <Translate>amount</Translate>
+ <i18n.Translate>amount</i18n.Translate>
</th>
<th>
- <Translate>reason</Translate>
+ <i18n.Translate>reason</i18n.Translate>
</th>
</tr>
</thead>
@@ -358,7 +353,7 @@ export function RefundModal({
? "never"
: format(
new Date(r.timestamp.t_s * 1000),
- "yyyy-MM-dd HH:mm:ss"
+ datetimeFormatForSettings(settings),
)}
</td>
<td>{r.amount}</td>
@@ -377,32 +372,32 @@ export function RefundModal({
<FormProvider<State>
errors={errors}
object={form}
- valueHandler={(d) => setValue(d as any)}
+ valueHandler={(d) => setValue(d)}
>
<InputCurrency<State>
name="refund"
- label={i18n`Refund`}
- tooltip={i18n`amount to be refunded`}
+ label={i18n.str`Refund`}
+ tooltip={i18n.str`amount to be refunded`}
>
- <Translate>Max refundable:</Translate>{" "}
+ <i18n.Translate>Max refundable:</i18n.Translate>{" "}
{Amounts.stringify(totalRefundable)}
</InputCurrency>
<InputSelector
name="mainReason"
- label={i18n`Reason`}
+ label={i18n.str`Reason`}
values={[
- i18n`Choose one...`,
+ i18n.str`Choose one...`,
duplicatedText,
- i18n`requested by the customer`,
- i18n`other`,
+ i18n.str`requested by the customer`,
+ i18n.str`other`,
]}
- tooltip={i18n`why this order is being refunded`}
+ tooltip={i18n.str`why this order is being refunded`}
/>
{form.mainReason && form.mainReason !== duplicatedText ? (
<Input<State>
- label={i18n`Description`}
+ label={i18n.str`Description`}
name="description"
- tooltip={i18n`more information to give context`}
+ tooltip={i18n.str`more information to give context`}
/>
) : undefined}
</FormProvider>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx
index 233afde04..8a1f85b1c 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,157 +15,216 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { h, VNode, Fragment } from 'preact';
-import { useState } from 'preact/hooks';
+import {
+ AbsoluteTime,
+ HttpStatusCode,
+ TalerError,
+ TalerMerchantApi,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
+import {
+ useTranslationContext
+} from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
import { Loading } from "../../../../components/exception/loading.js";
+import { JumpToElementById } from "../../../../components/form/JumpToElementById.js";
import { NotificationCard } from "../../../../components/menu/index.js";
-import { MerchantBackend, WithId } from "../../../../declaration.js";
-import { HttpError } from "../../../../hooks/backend.js";
-import { InstanceOrderFilter, useInstanceOrders, useOrderAPI, useOrderDetails } from "../../../../hooks/order.js";
-import { useTranslator } from '../../../../i18n/index.js';
+import { useSessionContext } from "../../../../context/session.js";
+import {
+ InstanceOrderFilter,
+ useInstanceOrders,
+ useOrderDetails,
+} from "../../../../hooks/order.js";
import { Notification } from "../../../../utils/types.js";
-import { RefundModal } from "./Table.js";
+import { LoginPage } from "../../../login/index.js";
+import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
import { ListPage } from "./ListPage.js";
+import { RefundModal } from "./Table.js";
interface Props {
- onUnauthorized: () => VNode;
- onLoadError: (error: HttpError) => VNode;
- onNotFound: () => VNode;
onSelect: (id: string) => void;
onCreate: () => void;
}
-export default function ({ onUnauthorized, onLoadError, onCreate, onSelect, onNotFound }: Props): VNode {
- const [filter, setFilter] = useState<InstanceOrderFilter>({})
- const [orderToBeRefunded, setOrderToBeRefunded] = useState<MerchantBackend.Orders.OrderHistoryEntry | undefined>(undefined)
-
- const setNewDate = (date?: Date) => setFilter(prev => ({ ...prev, date }))
+export default function OrderList({ onCreate, onSelect }: Props): VNode {
+ const [filter, setFilter] = useState<InstanceOrderFilter>({ paid: false });
+ const [orderToBeRefunded, setOrderToBeRefunded] = useState<
+ TalerMerchantApi.OrderHistoryEntry | undefined
+ >(undefined);
- const result = useInstanceOrders(filter, setNewDate)
- const { refundOrder, getPaymentURL } = useOrderAPI()
+ const setNewDate = (date?: AbsoluteTime): void =>
+ setFilter((prev) => ({ ...prev, date }));
- const [notif, setNotif] = useState<Notification | undefined>(undefined)
+ const result = useInstanceOrders(filter, (d) =>
+ setFilter({ ...filter, position: d }),
+ );
+ const { lib } = useSessionContext();
- if (result.clientError && result.isUnauthorized) return onUnauthorized()
- if (result.clientError && result.isNotfound) return onNotFound()
- if (result.loading) return <Loading />
- if (!result.ok) return onLoadError(result)
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
- const isPaidActive = filter.paid === 'yes' ? "is-active" : ''
- const isRefundedActive = filter.refunded === 'yes' ? "is-active" : ''
- const isNotWiredActive = filter.wired === 'no' ? "is-active" : ''
- const isAllActive = filter.paid === undefined && filter.refunded === undefined && filter.wired === undefined ? 'is-active' : ''
+ const { i18n } = useTranslationContext();
+ const { state } = useSessionContext();
- const i18n = useTranslator()
- const [errorOrderId, setErrorOrderId] = useState<string | undefined>(undefined)
-
- async function testIfOrderExistAndSelect(orderId: string) {
- if (!orderId) {
- setErrorOrderId(i18n`Enter an order id`)
- return;
- }
- try {
- await getPaymentURL(orderId)
- onSelect(orderId)
- setErrorOrderId(undefined)
- } catch {
- setErrorOrderId(i18n`order not found`)
+ if (!result) return <Loading />;
+ if (result instanceof TalerError) {
+ return <ErrorLoadingMerchant error={result} />;
+ }
+ if (result.type === "fail") {
+ switch(result.case) {
+ case HttpStatusCode.NotFound: {
+ return <NotFoundPageOrAdminCreate />;
+ }
+ case HttpStatusCode.Unauthorized: {
+ return <LoginPage />
+ }
+ default: {
+ assertUnreachable(result)
+ }
}
}
- return <Fragment>
- <NotificationCard notification={notif} />
-
- <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)}
-
- errorOrderId={errorOrderId}
- isAllActive={isAllActive}
- isNotWiredActive={isNotWiredActive}
- isPaidActive={isPaidActive}
- isRefundedActive={isRefundedActive}
- jumpToDate={filter.date}
- onCopyURL={(id) => getPaymentURL(id).then((resp) => copyToClipboard(resp.data))}
-
- onCreate={onCreate}
- onSearchOrderById={testIfOrderExistAndSelect}
- onSelectDate={setNewDate}
- onShowAll={() => setFilter({})}
- onShowPaid={() => setFilter({ paid: 'yes' })}
- onShowRefunded={() => setFilter({ refunded: 'yes' })}
- onShowNotWired={() => setFilter({ wired: 'no' })}
-
- />
-
- {orderToBeRefunded && <RefundModalForTable
- id={orderToBeRefunded.order_id}
- onCancel={() => setOrderToBeRefunded(undefined)}
- onConfirm={(value) => refundOrder(orderToBeRefunded.order_id, value)
- .then(() => setNotif({
- message: i18n`refund created successfully`,
- type: "SUCCESS"
- }))
- .catch((error) => setNotif({
- message: i18n`could not create the refund`,
- type: "ERROR",
- description: error.message
- }))
- .then(() => setOrderToBeRefunded(undefined))}
- onLoadError={(error) => {
- setNotif({
- message: i18n`could not create the refund`,
- type: "ERROR",
- description: error.message
- });
- setOrderToBeRefunded(undefined);
- return <div />;
- }}
- onUnauthorized={onUnauthorized}
- onNotFound={() => {
- setNotif({
- message: i18n`could not get the order to refund`,
- type: "ERROR",
- // description: error.message
- });
- setOrderToBeRefunded(undefined);
- return <div />;
- }} />}
- </Fragment>
+ const isNotPaidActive = filter.paid === false ? "is-active" : "";
+ const isPaidActive =
+ filter.paid === true && filter.wired === undefined ? "is-active" : "";
+ const isRefundedActive = filter.refunded === true ? "is-active" : "";
+ const isNotWiredActive =
+ filter.wired === false && filter.paid === true ? "is-active" : "";
+ const isWiredActive = filter.wired === true ? "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={async (order) => {
+ const resp = await lib.instance.getOrderDetails(state.token, order);
+ return resp.type === "ok";
+ }}
+ onSelect={onSelect}
+ description={i18n.str`jump to order with the given product ID`}
+ placeholder={i18n.str`order id`}
+ />
+
+ <ListPage
+ orders={result.body.map((o) => ({ ...o, id: o.order_id }))}
+ onLoadMoreBefore={result.isFirstPage ? undefined : result.loadFirst}
+ onLoadMoreAfter={result.isLastPage ? undefined : result.loadNext}
+ onSelectOrder={(order) => onSelect(order.id)}
+ onRefundOrder={(value) => setOrderToBeRefunded(value)}
+ isAllActive={isAllActive}
+ isNotWiredActive={isNotWiredActive}
+ isWiredActive={isWiredActive}
+ isPaidActive={isPaidActive}
+ isNotPaidActive={isNotPaidActive}
+ isRefundedActive={isRefundedActive}
+ jumpToDate={filter.date}
+ onSelectDate={setNewDate}
+ onCopyURL={async (id) => {
+ const resp = await lib.instance.getOrderDetails(state.token, id);
+ if (resp.type === "ok") {
+ if (resp.body.order_status === "unpaid") {
+ copyToClipboard(resp.body.taler_pay_uri);
+ } else {
+ if (resp.body.contract_terms.fulfillment_url) {
+ copyToClipboard(resp.body.contract_terms.fulfillment_url);
+ }
+ }
+ copyToClipboard(resp.body.order_status);
+ }
+ }}
+ onCreate={onCreate}
+ onShowAll={() => setFilter({})}
+ onShowNotPaid={() => setFilter({ paid: false })}
+ onShowPaid={() => setFilter({ paid: true })}
+ onShowRefunded={() => setFilter({ refunded: true })}
+ onShowNotWired={() => setFilter({ wired: false, paid: true })}
+ onShowWired={() => setFilter({ wired: true })}
+ />
+
+ {orderToBeRefunded && (
+ <RefundModalForTable
+ id={orderToBeRefunded.order_id}
+ onCancel={() => setOrderToBeRefunded(undefined)}
+ onConfirm={(value) => {
+ lib.instance
+ .addRefund(state.token, 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));
+ }}
+ />
+ )}
+ </section>
+ );
}
interface RefundProps {
id: string;
- onUnauthorized: () => VNode;
- onLoadError: (error: HttpError) => VNode;
- onNotFound: () => VNode;
onCancel: () => void;
- onConfirm: (m: MerchantBackend.Orders.RefundRequest) => void;
+ onConfirm: (m: TalerMerchantApi.RefundRequest) => void;
}
-function RefundModalForTable({ id, onUnauthorized, onLoadError, onNotFound, onConfirm, onCancel }: RefundProps) {
+function RefundModalForTable({ id, onConfirm, onCancel }: RefundProps): VNode {
const result = useOrderDetails(id);
- if (result.clientError && result.isUnauthorized) return onUnauthorized()
- if (result.clientError && result.isNotfound) return onNotFound()
- if (result.loading) return <Loading />
- if (!result.ok) 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.BadGateway: {
+ return <div>Failed to obtain a response from the exchange</div>;
+ }
+ case HttpStatusCode.GatewayTimeout: {
+ return (
+ <div>The merchant's interaction with the exchange took too long</div>
+ );
+ }
+ case HttpStatusCode.Unauthorized: {
+ return <LoginPage />
+ }
+ default: {
+ assertUnreachable(result);
+ }
+ }
+ }
- return <RefundModal
- order={result.data}
- onCancel={onCancel}
- onConfirm={onConfirm}
- />
+ return (
+ <RefundModal
+ order={result.body}
+ onCancel={onCancel}
+ onConfirm={onConfirm}
+ />
+ );
}
-async function copyToClipboard(text: string) {
- return navigator.clipboard.writeText(text)
+async function copyToClipboard(text: string): Promise<void> {
+ return navigator.clipboard.writeText(text);
}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/Create.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/Create.stories.tsx
new file mode 100644
index 000000000..36b31ebe8
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/Create.stories.tsx
@@ -0,0 +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)
+ */
+
+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/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx
new file mode 100644
index 000000000..d5522c2d4
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx
@@ -0,0 +1,181 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import {
+ TalerMerchantApi,
+ isRfc3548Base32Charset,
+ randomRfc3548Base32Key,
+} 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 { InputSelector } from "../../../../components/form/InputSelector.js";
+import { InputWithAddon } from "../../../../components/form/InputWithAddon.js";
+
+type Entity = TalerMerchantApi.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 [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 ? (
+ <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(),
+ }));
+ e.preventDefault();
+ }}
+ >
+ <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/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatedSuccessfully.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatedSuccessfully.tsx
new file mode 100644
index 000000000..7723bec81
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatedSuccessfully.tsx
@@ -0,0 +1,98 @@
+/*
+ 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 { TalerMerchantApi } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
+import { QR } from "../../../../components/exception/QR.js";
+import { CreatedSuccessfully as Template } from "../../../../components/notifications/CreatedSuccessfully.js";
+import { useSessionContext } from "../../../../context/session.js";
+
+type Entity = TalerMerchantApi.OtpDeviceAddDetails;
+
+interface Props {
+ entity: Entity;
+ onConfirm: () => void;
+}
+
+export function CreatedSuccessfully({
+ entity,
+ onConfirm,
+}: Props): VNode {
+ const { i18n } = useTranslationContext();
+ const { state } = useSessionContext();
+ const issuer = state.backendUrl.href;
+ const qrText = `otpauth://totp/${state.instance}/${entity.otp_device_id}?issuer=${issuer}&algorithm=SHA1&digits=8&period=30&secret=${entity.otp_key}`;
+ const qrTextSafe = `otpauth://totp/${state.instance}/${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 save the key before continuing.
+ </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/merchant-backoffice-ui/src/paths/instance/otp_devices/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/index.tsx
new file mode 100644
index 000000000..8ab0e1f26
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/index.tsx
@@ -0,0 +1,70 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { 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 { NotificationCard } from "../../../../components/menu/index.js";
+import { useSessionContext } from "../../../../context/session.js";
+import { Notification } from "../../../../utils/types.js";
+import { CreatePage } from "./CreatePage.js";
+import { CreatedSuccessfully } from "./CreatedSuccessfully.js";
+
+export type Entity = TalerMerchantApi.OtpDeviceAddDetails;
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+}
+
+export default function CreateValidator({ onConfirm, onBack }: Props): VNode {
+ const { lib: api } = useSessionContext();
+ const { state } = useSessionContext();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+ const [created, setCreated] = useState<TalerMerchantApi.OtpDeviceAddDetails | null>(null)
+
+ if (created) {
+ return <CreatedSuccessfully entity={created} onConfirm={onConfirm} />
+ }
+
+ return (
+ <>
+ <NotificationCard notification={notif} />
+ <CreatePage
+ onBack={onBack}
+ onCreate={(request: Entity) => {
+ return api.instance.addOtpDevice(state.token, request)
+ .then((d) => {
+ setCreated(request)
+ })
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not create device`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/List.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/List.stories.tsx
new file mode 100644
index 000000000..49032c80e
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/List.stories.tsx
@@ -0,0 +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)
+ */
+
+import { FunctionalComponent, h } from "preact";
+import { ListPage as TestedComponent } from "./ListPage.js";
+
+export default {
+ title: "Pages/OtpDevices/List",
+ component: TestedComponent,
+};
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/ListPage.tsx
new file mode 100644
index 000000000..8ca0a9c58
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/ListPage.tsx
@@ -0,0 +1,59 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { TalerMerchantApi } from "@gnu-taler/taler-util";
+import { h, VNode } from "preact";
+import { CardTable } from "./Table.js";
+
+export interface Props {
+ devices: TalerMerchantApi.OtpDeviceEntry[];
+ onLoadMoreBefore?: () => void;
+ onLoadMoreAfter?: () => void;
+ onCreate: () => void;
+ onDelete: (e: TalerMerchantApi.OtpDeviceEntry) => void;
+ onSelect: (e: TalerMerchantApi.OtpDeviceEntry) => void;
+}
+
+export function ListPage({
+ devices,
+ onCreate,
+ onDelete,
+ onSelect,
+ onLoadMoreBefore,
+ onLoadMoreAfter,
+}: Props): VNode {
+
+ return (
+ <section class="section is-main-section">
+ <CardTable
+ devices={devices.map((o) => ({
+ ...o,
+ id: String(o.otp_device_id),
+ }))}
+ onCreate={onCreate}
+ onDelete={onDelete}
+ onSelect={onSelect}
+ onLoadMoreBefore={onLoadMoreBefore}
+ onLoadMoreAfter={onLoadMoreAfter}
+ />
+ </section>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx
new file mode 100644
index 000000000..afe3c98e2
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx
@@ -0,0 +1,196 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { TalerMerchantApi } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { StateUpdater, useState } from "preact/hooks";
+
+type Entity = TalerMerchantApi.OtpDeviceEntry;
+
+interface Props {
+ devices: Entity[];
+ onDelete: (e: Entity) => void;
+ onSelect: (e: Entity) => void;
+ onCreate: () => void;
+ onLoadMoreBefore?: () => void;
+ onLoadMoreAfter?: () => void;
+}
+
+export function CardTable({
+ devices,
+ onCreate,
+ onDelete,
+ onSelect,
+ onLoadMoreAfter,
+ onLoadMoreBefore,
+}: 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}
+ />
+ ) : (
+ <EmptyTable />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+}
+interface TableProps {
+ rowSelection: string[];
+ instances: Entity[];
+ onDelete: (e: Entity) => void;
+ onSelect: (e: Entity) => void;
+ rowSelectionHandler: StateUpdater<string[]>;
+ onLoadMoreBefore?: () => void;
+ onLoadMoreAfter?: () => void;
+}
+
+function Table({
+ instances,
+ onLoadMoreAfter,
+ onDelete,
+ onSelect,
+ onLoadMoreBefore,
+}: TableProps): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="table-container">
+ {onLoadMoreBefore && (
+ <button
+ class="button is-fullwidth"
+ data-tooltip={i18n.str`load more devices before the first one`}
+ onClick={onLoadMoreBefore}
+ >
+ <i18n.Translate>load first page</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.device_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 devices from the database`}
+ onClick={() => onDelete(i)}
+ >
+ Delete
+ </button>
+ </div>
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ {onLoadMoreAfter && (
+ <button
+ class="button is-fullwidth"
+ data-tooltip={i18n.str`load more devices after the last one`}
+ onClick={onLoadMoreAfter}
+ >
+ <i18n.Translate>load next page</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-magnify 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/merchant-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx
new file mode 100644
index 000000000..b6a077863
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx
@@ -0,0 +1,106 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import {
+ HttpStatusCode,
+ TalerError,
+ TalerMerchantApi,
+ assertUnreachable
+} from "@gnu-taler/taler-util";
+import {
+ useTranslationContext
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { useSessionContext } from "../../../../context/session.js";
+import { useInstanceOtpDevices } from "../../../../hooks/otp.js";
+import { Notification } from "../../../../utils/types.js";
+import { LoginPage } from "../../../login/index.js";
+import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
+import { ListPage } from "./ListPage.js";
+
+interface Props {
+ onCreate: () => void;
+ onSelect: (id: string) => void;
+}
+
+export default function ListOtpDevices({ onCreate, onSelect }: Props): VNode {
+ // const [position, setPosition] = useState<string | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { lib } = useSessionContext();
+ const { state } = useSessionContext();
+ const result = useInstanceOtpDevices();
+
+ if (!result) return <Loading />;
+ if (result instanceof TalerError) {
+ return <ErrorLoadingMerchant error={result} />;
+ }
+ if (result.type === "fail") {
+ switch (result.case) {
+ case HttpStatusCode.NotFound: {
+ return <NotFoundPageOrAdminCreate />;
+ }
+ case HttpStatusCode.Unauthorized: {
+ return <LoginPage />
+ }
+ default: {
+ assertUnreachable(result);
+ }
+ }
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+
+ <ListPage
+ devices={result.body.otp_devices}
+ onLoadMoreBefore={undefined} //result.isFirstPage ? undefined : result.loadFirst}
+ onLoadMoreAfter={undefined} //result.isLastPage ? undefined : result.loadNext}
+ onCreate={onCreate}
+ onSelect={(e) => {
+ onSelect(e.otp_device_id);
+ }}
+ onDelete={(e: TalerMerchantApi.OtpDeviceEntry) => {
+ return lib.instance
+ .deleteOtpDevice(state.token, e.otp_device_id)
+ .then(() =>
+ setNotif({
+ message: i18n.str`validator delete successfully`,
+ type: "SUCCESS",
+ }),
+ )
+ .catch((error) =>
+ setNotif({
+ message: i18n.str`could not delete the validator`,
+ type: "ERROR",
+ description: error.message,
+ }),
+ );
+ }}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/Update.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/Update.stories.tsx
new file mode 100644
index 000000000..06ea9d07a
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/Update.stories.tsx
@@ -0,0 +1,32 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { 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/merchant-backoffice-ui/src/paths/instance/otp_devices/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/UpdatePage.tsx
new file mode 100644
index 000000000..35d67cbc6
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/UpdatePage.tsx
@@ -0,0 +1,185 @@
+/*
+ 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 { randomRfc3548Base32Key, TalerMerchantApi } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { InputSelector } from "../../../../components/form/InputSelector.js";
+import { InputWithAddon } from "../../../../components/form/InputWithAddon.js";
+
+type Entity = TalerMerchantApi.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/merchant-backoffice-ui/src/paths/instance/otp_devices/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/index.tsx
new file mode 100644
index 000000000..99edb95c3
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/index.tsx
@@ -0,0 +1,147 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import {
+ HttpStatusCode,
+ TalerError,
+ TalerMerchantApi,
+ assertUnreachable
+} from "@gnu-taler/taler-util";
+import {
+ useTranslationContext
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { useSessionContext } from "../../../../context/session.js";
+import { useOtpDeviceDetails } from "../../../../hooks/otp.js";
+import { Notification } from "../../../../utils/types.js";
+import { LoginPage } from "../../../login/index.js";
+import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
+import { CreatedSuccessfully } from "../create/CreatedSuccessfully.js";
+import { UpdatePage } from "./UpdatePage.js";
+
+export type Entity = TalerMerchantApi.OtpDevicePatchDetails & WithId;
+
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+ vid: string;
+}
+export default function UpdateValidator({
+ vid,
+ onConfirm,
+ onBack,
+}: Props): VNode {
+ const result = useOtpDeviceDetails(vid);
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const [keyUpdated, setKeyUpdated] =
+ useState<TalerMerchantApi.OtpDeviceAddDetails | null>(null);
+ const { lib } = useSessionContext();
+ const { state } = useSessionContext();
+
+ const { i18n } = useTranslationContext();
+
+ if (!result) return <Loading />;
+ if (result instanceof TalerError) {
+ return <ErrorLoadingMerchant error={result} />;
+ }
+ if (result.type === "fail") {
+ switch (result.case) {
+ case HttpStatusCode.NotFound: {
+ return <NotFoundPageOrAdminCreate />;
+ }
+ case HttpStatusCode.Unauthorized: {
+ return <LoginPage />
+ }
+ default: {
+ assertUnreachable(result);
+ }
+ }
+ }
+
+ if (keyUpdated) {
+ return <CreatedSuccessfully entity={keyUpdated} onConfirm={onConfirm} />;
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+ <UpdatePage
+ device={{
+ id: vid,
+ otp_algorithm: result.body.otp_algorithm,
+ otp_device_description: result.body.device_description,
+ otp_key: "",
+ otp_ctr: result.body.otp_ctr,
+ }}
+ onBack={onBack}
+ onUpdate={async (newInfo) => {
+ return lib.instance
+ .updateOtpDevice(state.token, vid, newInfo)
+ .then((d) => {
+ if (d.type === "ok") {
+ if (newInfo.otp_key) {
+ setKeyUpdated({
+ otp_algorithm: newInfo.otp_algorithm,
+ otp_device_description: newInfo.otp_device_description,
+ otp_device_id: newInfo.id,
+ otp_key: newInfo.otp_key,
+ otp_ctr: newInfo.otp_ctr,
+ });
+ } else {
+ onConfirm();
+ }
+ } else {
+ switch(d.case) {
+ case HttpStatusCode.NotFound: {
+ setNotif({
+ message: i18n.str`Could not update template`,
+ type: "ERROR",
+ description: i18n.str`Template id is unknown`,
+ });
+ break;
+ }
+ case HttpStatusCode.Conflict: {
+ setNotif({
+ message: i18n.str`Could not update template`,
+ type: "ERROR",
+ description: i18n.str`The provided information is inconsistent with the current state of the template`,
+ });
+ break;
+ }
+ }
+ }
+ })
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not update template`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/create/Create.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/create/Create.stories.tsx
index b42b9f929..22bbfe28a 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/products/create/Create.stories.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/products/create/Create.stories.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,28 +15,29 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { h, VNode, FunctionalComponent } from 'preact';
+import { h, VNode, FunctionalComponent } from "preact";
import { CreatePage as TestedComponent } from "./CreatePage.js";
-
export default {
- title: 'Pages/Product/Create',
+ title: "Pages/Product/Create",
component: TestedComponent,
argTypes: {
- onCreate: { action: 'onCreate' },
- onBack: { action: 'onBack' },
+ 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
+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, {
-});
+export const Example = createExample(TestedComponent, {});
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/create/CreatePage.tsx
index 434a55aab..64b174f64 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/products/create/CreatePage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/products/create/CreatePage.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,51 +15,66 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @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 { 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 { Translate, useTranslator } from "../../../../i18n/index.js";
-type Entity = MerchantBackend.Products.ProductAddDetail & { product_id: string}
+type Entity = TalerMerchantApi.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 [submitForm, addFormSubmitter] = useListener<Entity | undefined>((result) => {
- if (result) return onCreate(result)
- return Promise.reject()
- })
-
- const i18n = useTranslator()
+ 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} />
+ 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} ><Translate>Cancel</Translate></button>}
- <AsyncButton onClick={submitForm} data-tooltip={
- !submitForm ? i18n`Need to complete marked fields` : 'confirm operation'
- } disabled={!submitForm}><Translate>Confirm</Translate></AsyncButton>
+ <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>
- <div class="column" />
- </div>
- </section>
- </div>
-} \ No newline at end of file
+ </section>
+ </div>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/create/CreatedSuccessfully.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/create/CreatedSuccessfully.tsx
index 9bbdc4070..2b6ebed45 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/products/create/CreatedSuccessfully.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/products/create/CreatedSuccessfully.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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
@@ -24,44 +24,49 @@ interface Props {
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>
+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>
- </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 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>
- <div class="field is-horizontal">
- <div class="field-label is-normal">
- <label class="label">Price</label>
+ <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-body is-flex-grow-3">
- <div class="field">
- <p class="control">
- <input class="input" readonly value={entity.price} />
- </p>
+ <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>
- </div>
- </Template>;
+ </Template>
+ );
}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/create/index.tsx
index 51dc63431..9de5cae78 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/products/create/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/products/create/index.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,41 +15,47 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { Fragment, h, VNode } from 'preact';
-import { useState } from 'preact/hooks';
+import { 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 { NotificationCard } from "../../../../components/menu/index.js";
-import { MerchantBackend } from "../../../../declaration.js";
-import { useProductAPI } from "../../../../hooks/product.js";
-import { useTranslator } from '../../../../i18n/index.js';
+import { useSessionContext } from "../../../../context/session.js";
import { Notification } from "../../../../utils/types.js";
import { CreatePage } from "./CreatePage.js";
-export type Entity = MerchantBackend.Products.ProductAddDetail
+export type Entity = TalerMerchantApi.ProductAddDetail;
interface Props {
onBack?: () => void;
onConfirm: () => void;
}
export default function CreateProduct({ onConfirm, onBack }: Props): VNode {
- const { createProduct } = useProductAPI()
- const [notif, setNotif] = useState<Notification | undefined>(undefined)
- const i18n = useTranslator()
-
- return <Fragment>
- <NotificationCard notification={notif} />
- <CreatePage
- onBack={onBack}
- onCreate={(request: MerchantBackend.Products.ProductAddDetail) => {
- return createProduct(request).then(() => onConfirm()).catch((error) => {
- setNotif({
- message: i18n`could not create product`,
- type: "ERROR",
- description: error.message
- })
- })
- }} />
- </Fragment>
-} \ No newline at end of file
+ const { lib } = useSessionContext();
+ const { state } = useSessionContext();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+ <CreatePage
+ onBack={onBack}
+ onCreate={(request: TalerMerchantApi.ProductAddDetail) => {
+ return lib.instance.addProduct(state.token, request)
+ .then(() => onConfirm())
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not create product`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/list/List.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/list/List.stories.tsx
index 53217d051..580a92cdc 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/products/list/List.stories.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/products/list/List.stories.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,44 +15,48 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { h, VNode, FunctionalComponent } from 'preact';
+import { AmountString } from "@gnu-taler/taler-util";
+import { FunctionalComponent, h } from "preact";
import { CardTable as TestedComponent } from "./Table.js";
-
export default {
- title: 'Pages/Product/List',
+ title: "Pages/Product/List",
component: TestedComponent,
argTypes: {
- onCreate: { action: 'onCreate' },
- onSelect: { action: 'onSelect' },
- onDelete: { action: 'onDelete' },
- onUpdate: { action: 'onUpdate' },
+ 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
+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: {}
- }]
+ instances: [
+ {
+ id: "orderid",
+ description: "description1",
+ description_i18n: {} as any,
+ image: "",
+ price: "TESTKUDOS:10" as AmountString,
+ taxes: [],
+ total_lost: 10,
+ total_sold: 5,
+ total_stock: 15,
+ unit: "bar",
+ address: {},
+ },
+ ],
});
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx
index 81acb9876..9d5701fa7 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
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2021-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -19,21 +19,21 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
+import { AmountString, Amounts, TalerMerchantApi } 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 { ComponentChildren, Fragment, VNode, h } from "preact";
import { StateUpdater, useState } from "preact/hooks";
+import emptyImage from "../../../../assets/empty.png";
import {
- FormProvider,
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 emptyImage from "../../../../assets/empty.png";
-import { Translate, useTranslator } from "../../../../i18n/index.js";
-import { Amounts } from "@gnu-taler/taler-util";
+import { dateFormatForSettings, usePreference } from "../../../../hooks/preference.js";
-type Entity = MerchantBackend.Products.ProductDetail & WithId;
+type Entity = TalerMerchantApi.ProductDetail & WithId;
interface Props {
instances: Entity[];
@@ -41,10 +41,12 @@ interface Props {
onSelect: (product: Entity) => void;
onUpdate: (
id: string,
- data: MerchantBackend.Products.ProductPatchDetail
+ data: TalerMerchantApi.ProductPatchDetail,
) => Promise<void>;
onCreate: () => void;
selected?: boolean;
+ onLoadMoreBefore?: () => void;
+ onLoadMoreAfter?: () => void;
}
export function CardTable({
@@ -53,11 +55,13 @@ export function CardTable({
onSelect,
onUpdate,
onDelete,
+ onLoadMoreAfter,
+ onLoadMoreBefore
}: Props): VNode {
const [rowSelection, rowSelectionHandler] = useState<string | undefined>(
- undefined
+ undefined,
);
- const i18n = useTranslator();
+ const { i18n } = useTranslationContext();
return (
<div class="card has-table">
<header class="card-header">
@@ -65,12 +69,12 @@ export function CardTable({
<span class="icon">
<i class="mdi mdi-shopping" />
</span>
- <Translate>Products</Translate>
+ <i18n.Translate>Inventory</i18n.Translate>
</p>
<div class="card-header-icon" aria-label="more options">
<span
class="has-tooltip-left"
- data-tooltip={i18n`add product to inventory`}
+ data-tooltip={i18n.str`add product to inventory`}
>
<button class="button is-info" type="button" onClick={onCreate}>
<span class="icon is-small">
@@ -89,6 +93,8 @@ export function CardTable({
onSelect={onSelect}
onDelete={onDelete}
onUpdate={onUpdate}
+ onLoadMoreAfter={onLoadMoreAfter}
+ onLoadMoreBefore={onLoadMoreBefore}
rowSelection={rowSelection}
rowSelectionHandler={rowSelectionHandler}
/>
@@ -107,10 +113,12 @@ interface TableProps {
onSelect: (id: Entity) => void;
onUpdate: (
id: string,
- data: MerchantBackend.Products.ProductPatchDetail
+ data: TalerMerchantApi.ProductPatchDetail,
) => Promise<void>;
onDelete: (id: Entity) => void;
rowSelectionHandler: StateUpdater<string | undefined>;
+ onLoadMoreBefore?: () => void;
+ onLoadMoreAfter?: () => void;
}
function Table({
@@ -120,33 +128,41 @@ function Table({
onSelect,
onUpdate,
onDelete,
+ onLoadMoreAfter,
+ onLoadMoreBefore
}: TableProps): VNode {
- const i18n = useTranslator();
+ const { i18n } = useTranslationContext();
+ const [settings] = usePreference();
return (
<div class="table-container">
+ {onLoadMoreBefore && (
+ <button class="button is-fullwidth" onClick={onLoadMoreBefore}>
+ <i18n.Translate>load first page</i18n.Translate>
+ </button>
+ )}
<table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
<thead>
<tr>
<th>
- <Translate>Image</Translate>
+ <i18n.Translate>Image</i18n.Translate>
</th>
<th>
- <Translate>Description</Translate>
+ <i18n.Translate>Description</i18n.Translate>
</th>
<th>
- <Translate>Sell</Translate>
+ <i18n.Translate>Price per unit</i18n.Translate>
</th>
<th>
- <Translate>Taxes</Translate>
+ <i18n.Translate>Taxes</i18n.Translate>
</th>
<th>
- <Translate>Profit</Translate>
+ <i18n.Translate>Sales</i18n.Translate>
</th>
<th>
- <Translate>Stock</Translate>
+ <i18n.Translate>Stock</i18n.Translate>
</th>
<th>
- <Translate>Sold</Translate>
+ <i18n.Translate>Sold</i18n.Translate>
</th>
<th />
</tr>
@@ -156,10 +172,10 @@ function Table({
const restStockInfo = !i.next_restock
? ""
: i.next_restock.t_s === "never"
- ? "never"
- : `restock at ${format(
+ ? "never"
+ : `restock at ${format(
new Date(i.next_restock.t_s * 1000),
- "yyyy/MM/dd"
+ dateFormatForSettings(settings),
)}`;
let stockInfo: ComponentChildren = "";
if (i.total_stock < 0) {
@@ -188,18 +204,21 @@ function Table({
src={i.image ? i.image : emptyImage}
style={{
border: "solid black 1px",
- width: 100,
- height: 100,
+ 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}
+ {i.description.length > 30 ? i.description.substring(0, 30) + "..." : i.description}
</td>
<td
onClick={() =>
@@ -207,7 +226,7 @@ function Table({
}
style={{ cursor: "pointer" }}
>
- {isFree ? i18n`free` : `${i.price} / ${i.unit}`}
+ {isFree ? i18n.str`free` : `${i.price} / ${i.unit}`}
</td>
<td
onClick={() =>
@@ -239,32 +258,35 @@ function Table({
}
style={{ cursor: "pointer" }}
>
- {i.total_sold} {i.unit}
+ <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`go to product update page`}
+ data-tooltip={i18n.str`go to product update page`}
>
<button
class="button is-small is-success "
type="button"
onClick={(): void => onSelect(i)}
>
- <Translate>Update</Translate>
+ <i18n.Translate>Update</i18n.Translate>
</button>
</span>
<span
class="has-tooltip-left"
- data-tooltip={i18n`remove this product from the database`}
+ data-tooltip={i18n.str`remove this product from the database`}
>
<button
class="button is-small is-danger"
type="button"
onClick={(): void => onDelete(i)}
>
- <Translate>Delete</Translate>
+ <i18n.Translate>Delete</i18n.Translate>
</button>
</span>
</div>
@@ -276,8 +298,8 @@ function Table({
<FastProductUpdateForm
product={i}
onUpdate={(prod) =>
- onUpdate(i.id, prod).then((r) =>
- rowSelectionHandler(undefined)
+ onUpdate(i.id, prod).then(() =>
+ rowSelectionHandler(undefined),
)
}
onCancel={() => rowSelectionHandler(undefined)}
@@ -290,6 +312,13 @@ function Table({
})}
</tbody>
</table>
+ {onLoadMoreAfter && (
+ <button class="button is-fullwidth"
+ data-tooltip={i18n.str`load more products after the last one`}
+ onClick={onLoadMoreAfter}>
+ <i18n.Translate>load next page</i18n.Translate>
+ </button>
+ )}
</div>
);
}
@@ -297,7 +326,7 @@ function Table({
interface FastProductUpdateFormProps {
product: Entity;
onUpdate: (
- data: MerchantBackend.Products.ProductPatchDetail
+ data: TalerMerchantApi.ProductPatchDetail,
) => Promise<void>;
onCancel: () => void;
}
@@ -316,7 +345,7 @@ function FastProductWithInfiniteStockUpdateForm({
onCancel,
}: FastProductUpdateFormProps) {
const [value, valueHandler] = useState<UpdatePrice>({ price: product.price });
- const i18n = useTranslator();
+ const { i18n } = useTranslationContext();
return (
<Fragment>
@@ -327,31 +356,34 @@ function FastProductWithInfiniteStockUpdateForm({
>
<InputCurrency<FastProductUpdate>
name="price"
- label={i18n`Price`}
- tooltip={i18n`update the product with new price`}
+ label={i18n.str`Price`}
+ tooltip={i18n.str`update the product with new price`}
/>
</FormProvider>
- <div class="buttons is-right mt-5">
- <button class="button" onClick={onCancel}>
- <Translate>Cancel</Translate>
- </button>
- <span
- class="has-tooltip-left"
- data-tooltip={i18n`update product with new price`}
- >
- <button
- class="button is-info"
- onClick={() =>
- onUpdate({
- ...product,
- price: value.price,
- })
- }
- >
- <Translate>Confirm</Translate>
+ <div class="buttons is-expanded">
+
+ <div class="buttons is-right mt-5">
+ <button class="button" onClick={onCancel}>
+ <i18n.Translate>Cancel</i18n.Translate>
</button>
- </span>
+ <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 as AmountString,
+ })
+ }
+ >
+ <i18n.Translate>Confirm update</i18n.Translate>
+ </button>
+ </span>
+ </div>
</div>
</Fragment>
);
@@ -374,16 +406,15 @@ function FastProductWithManagedStockUpdateForm({
const errors: FormErrors<FastProductUpdate> = {
lost:
currentStock + value.incoming < value.lost
- ? `lost cannot be greater that current + incoming (max ${
- currentStock + value.incoming
- })`
+ ? `lost cannot be greater that current + incoming (max ${currentStock + value.incoming
+ })`
: undefined,
};
const hasErrors = Object.keys(errors).some(
- (k) => (errors as any)[k] !== undefined
+ (k) => (errors as Record<string,unknown>)[k] !== undefined,
);
- const i18n = useTranslator();
+ const { i18n } = useTranslationContext();
return (
<Fragment>
@@ -395,31 +426,31 @@ function FastProductWithManagedStockUpdateForm({
>
<InputNumber<FastProductUpdate>
name="incoming"
- label={i18n`Incoming`}
- tooltip={i18n`add more elements to the inventory`}
+ label={i18n.str`Incoming`}
+ tooltip={i18n.str`add more elements to the inventory`}
/>
<InputNumber<FastProductUpdate>
name="lost"
- label={i18n`Lost`}
- tooltip={i18n`report elements lost in the inventory`}
+ label={i18n.str`Lost`}
+ tooltip={i18n.str`report elements lost in the inventory`}
/>
<InputCurrency<FastProductUpdate>
name="price"
- label={i18n`Price`}
- tooltip={i18n`new price for the product`}
+ 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}>
- <Translate>Cancel</Translate>
+ <i18n.Translate>Cancel</i18n.Translate>
</button>
<span
class="has-tooltip-left"
data-tooltip={
hasErrors
- ? i18n`the are value with errors`
- : i18n`update product with new stock and price`
+ ? i18n.str`the are value with errors`
+ : i18n.str`update product with new stock and price`
}
>
<button
@@ -430,11 +461,11 @@ function FastProductWithManagedStockUpdateForm({
...product,
total_stock: product.total_stock + value.incoming,
total_lost: product.total_lost + value.lost,
- price: value.price,
+ price: value.price as AmountString,
})
}
>
- <Translate>Confirm</Translate>
+ <i18n.Translate>Confirm</i18n.Translate>
</button>
</span>
</div>
@@ -451,17 +482,18 @@ function FastProductUpdateForm(props: FastProductUpdateFormProps) {
}
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" />
+ <i class="mdi mdi-magnify mdi-48px" />
</span>
</p>
<p>
- <Translate>
+ <i18n.Translate>
There is no products yet, add more pressing the + sign
- </Translate>
+ </i18n.Translate>
</p>
</div>
);
@@ -474,6 +506,6 @@ function difference(price: string, tax: number) {
ps[1] = `${p - tax}`;
return ps.join(":");
}
-function sum(taxes: MerchantBackend.Tax[]) {
+function sum(taxes: TalerMerchantApi.Tax[]) {
return taxes.reduce((p, c) => p + parseInt(c.tax.split(":")[1], 10), 0);
}
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 eb98d7871..6ad0d4598 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
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,66 +15,139 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { h, VNode } from 'preact';
-import { useState } from 'preact/hooks';
+import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util";
+import {
+ useTranslationContext
+} from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
import { Loading } from "../../../../components/exception/loading.js";
+import { JumpToElementById } from "../../../../components/form/JumpToElementById.js";
import { NotificationCard } from "../../../../components/menu/index.js";
-import { MerchantBackend, WithId } from "../../../../declaration.js";
-import { HttpError } from "../../../../hooks/backend.js";
-import { useInstanceProducts, useProductAPI } from "../../../../hooks/product.js";
-import { useTranslator } from '../../../../i18n/index.js';
+import { ConfirmModal } from "../../../../components/modal/index.js";
+import { useSessionContext } from "../../../../context/session.js";
+import {
+ useInstanceProducts
+} from "../../../../hooks/product.js";
import { Notification } from "../../../../utils/types.js";
+import { LoginPage } from "../../../login/index.js";
+import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
import { CardTable } from "./Table.js";
interface Props {
- onUnauthorized: () => VNode;
- onNotFound: () => VNode;
onCreate: () => void;
onSelect: (id: string) => void;
- onLoadError: (e: HttpError) => VNode;
}
-export default function ProductList({ onUnauthorized, onLoadError, onCreate, onSelect, onNotFound }: Props): VNode {
- const result = useInstanceProducts()
- const { deleteProduct, updateProduct } = useProductAPI()
- const [notif, setNotif] = useState<Notification | undefined>(undefined)
-
- const i18n = useTranslator()
-
- if (result.clientError && result.isUnauthorized) return onUnauthorized()
- if (result.clientError && result.isNotfound) return onNotFound()
- if (result.loading) return <Loading />
- if (!result.ok) return onLoadError(result)
+export default function ProductList({
+ onCreate,
+ onSelect,
+}: Props): VNode {
+ const result = useInstanceProducts();
+ const { lib } = useSessionContext();
+ const { state } = useSessionContext();
+ const [deleting, setDeleting] =
+ useState<TalerMerchantApi.ProductDetail & WithId | null>(null);
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
- return <section class="section is-main-section">
- <NotificationCard notification={notif} />
+ const { i18n } = useTranslationContext();
- <CardTable instances={result.data}
- onCreate={onCreate}
- onUpdate={(id, prod) => updateProduct(id, prod)
- .then(() => setNotif({
- message: i18n`product updated successfully`,
- type: "SUCCESS"
- })).catch((error) => setNotif({
- message: i18n`could not update the product`,
- type: "ERROR",
- description: error.message
- }))
+ if (!result) return <Loading />;
+ if (result instanceof TalerError) {
+ return <ErrorLoadingMerchant error={result} />;
+ }
+ if (result.type === "fail") {
+ switch (result.case) {
+ case HttpStatusCode.NotFound: {
+ return <NotFoundPageOrAdminCreate />;
}
- onSelect={(product) => onSelect(product.id)}
- onDelete={(prod: (MerchantBackend.Products.ProductDetail & WithId)) => deleteProduct(prod.id)
- .then(() => setNotif({
- message: i18n`product delete successfully`,
- type: "SUCCESS"
- })).catch((error) => setNotif({
- message: i18n`could not delete the product`,
- type: "ERROR",
- description: error.message
- }))
+ case HttpStatusCode.Unauthorized: {
+ return <LoginPage />
}
- />
- </section>
-} \ No newline at end of file
+ default: {
+ assertUnreachable(result);
+ }
+ }
+ }
+
+ return (
+ <section class="section is-main-section">
+ <NotificationCard notification={notif} />
+
+ <JumpToElementById
+ testIfExist={async (id) => {
+ const resp = await lib.instance.getProductDetails(state.token, id);
+ return resp.type === "ok";
+ }}
+ onSelect={onSelect}
+ description={i18n.str`jump to product with the given product ID`}
+ placeholder={i18n.str`product id`}
+ />
+
+ <CardTable
+ instances={result.body}
+ onLoadMoreBefore={result.isFirstPage ? undefined : result.loadFirst}
+ onLoadMoreAfter={result.isLastPage ? undefined : result.loadNext}
+ onCreate={onCreate}
+ onUpdate={async (id, prod) => {
+ try {
+ await lib.instance.updateProduct(state.token, id, prod);
+ setNotif({
+ message: i18n.str`product updated successfully`,
+ type: "SUCCESS",
+ });
+ } catch (error) {
+ setNotif({
+ message: i18n.str`could not update the product`,
+ type: "ERROR",
+ description: error instanceof Error ? error.message : undefined,
+ });
+ }
+ return
+ }}
+ onSelect={(product) => onSelect(product.id)}
+ onDelete={(prod: TalerMerchantApi.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 lib.instance.deleteProduct(state.token, 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/merchant-backoffice-ui/src/paths/instance/products/update/Update.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/update/Update.stories.tsx
index f97d6d367..7aa93b186 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/products/update/Update.stories.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/products/update/Update.stories.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,60 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { h, VNode, FunctionalComponent } from 'preact';
+import { AmountString } from "@gnu-taler/taler-util";
+import { FunctionalComponent, h } from "preact";
import { UpdatePage as TestedComponent } from "./UpdatePage.js";
-
export default {
- title: 'Pages/Product/Update',
+ title: "Pages/Product/Update",
component: TestedComponent,
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ 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
+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',
+ product_id: "20102-ASDAS-QWE",
+ description: "description1",
description_i18n: {} as any,
- image: '',
- price: 'TESTKUDOS:10',
+ image: "",
+ price: "TESTKUDOS:10" as AmountString,
taxes: [],
total_lost: 10,
total_sold: 5,
total_stock: 15,
- unit: 'bar',
- address: {}
- }
+ unit: "bar",
+ address: {},
+ },
});
export const WithInfiniteStock = createExample(TestedComponent, {
product: {
- product_id: '20102-ASDAS-QWE',
- description: 'description1',
+ product_id: "20102-ASDAS-QWE",
+ description: "description1",
description_i18n: {} as any,
- image: '',
- price: 'TESTKUDOS:10',
+ image: "",
+ price: "TESTKUDOS:10" as AmountString,
taxes: [],
total_lost: 10,
total_sold: 5,
total_stock: -1,
- unit: 'bar',
- address: {}
- }
+ unit: "bar",
+ address: {},
+ },
});
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/update/UpdatePage.tsx
index ff1b3979d..5395ae40f 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/products/update/UpdatePage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/products/update/UpdatePage.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,18 +15,18 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @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 { AsyncButton } from "../../../../components/exception/AsyncButton.js";
import { ProductForm } from "../../../../components/product/ProductForm.js";
-import { MerchantBackend, WithId } from "../../../../declaration.js";
import { useListener } from "../../../../hooks/listener.js";
-import { Translate, useTranslator } from "../../../../i18n/index.js";
-type Entity = MerchantBackend.Products.ProductDetail & { product_id: string }
+type Entity = TalerMerchantApi.ProductDetail & { product_id: string };
interface Props {
onUpdate: (d: Entity) => Promise<void>;
@@ -35,43 +35,65 @@ interface Props {
}
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 = useTranslator()
+ const [submitForm, addFormSubmitter] = useListener<Entity | undefined>(
+ (result) => {
+ if (result) return onUpdate(result);
+ return Promise.resolve();
+ },
+ );
- return <div>
- <section class="section">
- <section class="hero is-hero-bar">
- <div class="hero-body">
+ const { i18n } = useTranslationContext();
- <div class="level">
- <div class="level-left">
- <div class="level-item">
- <span class="is-size-4"><Translate>Product id:</Translate><b>{product.product_id}</b></span>
+ 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>
- </div>
- </section>
- <hr />
+ </section>
+ <hr />
- <div class="columns">
- <div class="column" />
- <div class="column is-four-fifths">
- <ProductForm initial={product} onSubscribe={addFormSubmitter} alreadyExist />
+ <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} ><Translate>Cancel</Translate></button>}
- <AsyncButton onClick={submitForm} data-tooltip={
- !submitForm ? i18n`Need to complete marked fields` : 'confirm operation'
- } disabled={!submitForm}><Translate>Confirm</Translate></AsyncButton>
+ <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>
- <div class="column" />
- </div>
- </section>
- </div>
-} \ No newline at end of file
+ </section>
+ </div>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/update/index.tsx
index 59cfec15a..5e3e58d80 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/products/update/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/products/update/index.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,80 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { Fragment, h, VNode } from 'preact';
-import { useState } from 'preact/hooks';
+import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util";
+import {
+ useTranslationContext
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
import { Loading } from "../../../../components/exception/loading.js";
import { NotificationCard } from "../../../../components/menu/index.js";
-import { MerchantBackend } from "../../../../declaration.js";
-import { HttpError } from "../../../../hooks/backend.js";
-import { useProductAPI, useProductDetails } from "../../../../hooks/product.js";
-import { useTranslator } from '../../../../i18n/index.js';
+import { useSessionContext } from "../../../../context/session.js";
+import { useProductDetails } from "../../../../hooks/product.js";
import { Notification } from "../../../../utils/types.js";
+import { LoginPage } from "../../../login/index.js";
+import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
import { UpdatePage } from "./UpdatePage.js";
-export type Entity = MerchantBackend.Products.ProductAddDetail
+export type Entity = TalerMerchantApi.ProductAddDetail;
interface Props {
onBack?: () => void;
onConfirm: () => void;
- onUnauthorized: () => VNode;
- onNotFound: () => VNode;
- onLoadError: (e: HttpError) => VNode;
pid: string;
}
-export default function UpdateProduct({ pid, onConfirm, onBack, onUnauthorized, onNotFound, onLoadError }: Props): VNode {
- const { updateProduct } = useProductAPI()
- const result = useProductDetails(pid)
- const [notif, setNotif] = useState<Notification | undefined>(undefined)
+export default function UpdateProduct({
+ pid,
+ onConfirm,
+ onBack,
+}: Props): VNode {
+ const result = useProductDetails(pid);
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { lib } = useSessionContext();
+ const { state } = useSessionContext();
- const i18n = useTranslator()
+ const { i18n } = useTranslationContext();
- if (result.clientError && result.isUnauthorized) return onUnauthorized()
- if (result.clientError && result.isNotfound) return onNotFound()
- if (result.loading) return <Loading />
- if (!result.ok) 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);
+ }
+ }
+ }
- return <Fragment>
- <NotificationCard notification={notif} />
- <UpdatePage
- product={{ ...result.data, product_id: pid }}
- onBack={onBack}
- onUpdate={(data) => {
- return updateProduct(pid, data)
- .then(onConfirm)
- .catch((error) => {
- setNotif({
- message: i18n`could not create product`,
- type: "ERROR",
- description: error.message
- })
- })
- }} />
- </Fragment>
-} \ No newline at end of file
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+ <UpdatePage
+ product={{ ...result.body, product_id: pid }}
+ onBack={onBack}
+ onUpdate={(data) => {
+ return lib.instance.updateProduct(state.token, pid, data)
+ .then(onConfirm)
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not create product`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/CreatePage.tsx
deleted file mode 100644
index 4d92e812f..000000000
--- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/CreatePage.tsx
+++ /dev/null
@@ -1,168 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { Fragment, h, VNode } from "preact";
-import { StateUpdater, 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 { ExchangeBackend, MerchantBackend } from "../../../../declaration.js";
-import { Translate, useTranslator } from "../../../../i18n/index.js";
-import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
-import { canonicalizeBaseUrl, ExchangeKeysJson } from "@gnu-taler/taler-util"
-import { PAYTO_WIRE_METHOD_LOOKUP, URL_REGEX } from "../../../../utils/constants.js";
-import { request } from "../../../../hooks/backend.js";
-import { InputSelector } from "../../../../components/form/InputSelector.js";
-
-type Entity = MerchantBackend.Tips.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 = useTranslator()
- 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`it should be greater than 0` : undefined,
- exchange_url: !reserve.exchange_url ? i18n`cannot be empty` : !URL_REGEX.test(reserve.exchange_url) ? i18n`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`Initial balance`} tooltip={i18n`balance prior to deposit`} />
- <Input<Entity> name="exchange_url" label={i18n`Exchange URL`} tooltip={i18n`URL of exchange`} />
- </FormProvider>
-
- <div class="buttons is-right mt-5">
- {onBack && <button class="button" onClick={onBack} ><Translate>Cancel</Translate></button>}
- <AsyncButton class="has-tooltip-left" onClick={() => {
- return request<ExchangeBackend.WireResponse>(`${reserve.exchange_url}wire`).then(r => {
- const wireMethods = r.data.accounts.map(a => {
- const match = PAYTO_WIRE_METHOD_LOOKUP.exec(a.payto_uri)
- return match && match[1] || ''
- })
- setWireMethods(wireMethods)
- setCurrentStep(Steps.WIRE_METHOD)
- return
- }).catch((r: any) => {
- setExchangeQueryError(r.message)
- })
- }} data-tooltip={
- hasErrors ? i18n`Need to complete marked fields` : 'confirm operation'
- } disabled={hasErrors} ><Translate>Next</Translate></AsyncButton>
- </div>
- </Fragment>
- }
-
- case Steps.WIRE_METHOD: {
- const errors: FormErrors<Entity> = {
- wire_method: !reserve.wire_method ? i18n`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`Initial balance`} tooltip={i18n`balance prior to deposit`} readonly />
- <Input<Entity> name="exchange_url" label={i18n`Exchange URL`} tooltip={i18n`URL of exchange`} readonly />
- <InputSelector<Entity> name="wire_method" label={i18n`Wire method`} tooltip={i18n`method to use for wire transfer`} values={wireMethods} placeholder={i18n`Select one wire method`} />
- </FormProvider>
- <div class="buttons is-right mt-5">
- {onBack && <button class="button" onClick={() => setCurrentStep(Steps.EXCHANGE)} ><Translate>Back</Translate></button>}
- <AsyncButton onClick={submitForm} data-tooltip={
- hasErrors ? i18n`Need to complete marked fields` : 'confirm operation'
- } disabled={hasErrors} ><Translate>Confirm</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/merchant-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.stories.tsx
deleted file mode 100644
index 0cea361fd..000000000
--- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.stories.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { h, VNode, FunctionalComponent } from 'preact';
-import { CreatedSuccessfully as TestedComponent } from "./CreatedSuccessfully.js";
-
-
-export default {
- title: 'Pages/Reserve/CreatedSuccessfully',
- 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, {
- entity: {
- request: {
- exchange_url: 'http://exchange.taler/',
- initial_balance: 'TESTKUDOS:1',
- wire_method: 'x-taler-bank',
- },
- response: {
- payto_uri: 'payto://x-taler-bank/bank.taler:8080/exchange_account',
- reserve_pub: 'WEQWDASDQWEASDADASDQWEQWEASDAS'
- }
- }
-});
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.tsx
deleted file mode 100644
index 68ddc70b5..000000000
--- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- 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/>
- */
-
-import { h, VNode } from "preact";
-import { CreatedSuccessfully as Template } from "../../../../components/notifications/CreatedSuccessfully.js";
-import { MerchantBackend } from "../../../../declaration.js";
-import { Translate } from "../../../../i18n/index.js";
-import { QR } from "../../../../components/exception/QR.js";
-
-type Entity = { request: MerchantBackend.Tips.ReserveCreateRequest, response: MerchantBackend.Tips.ReserveCreateConfirmation };
-
-interface Props {
- entity: Entity;
- onConfirm: () => void;
- onCreateAnother?: () => void;
-}
-
-export function CreatedSuccessfully({ entity, onConfirm, onCreateAnother }: Props): VNode {
- const link = `${entity.response.payto_uri}?message=${entity.response.reserve_pub}&amount=${entity.request.initial_balance}`
-
- return <Template onConfirm={onConfirm} onCreateAnother={onCreateAnother}>
- <div class="field is-horizontal">
- <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">Exchange bank account</label>
- </div>
- <div class="field-body is-flex-grow-3">
- <div class="field">
- <p class="control">
- <input readonly class="input" value={entity.response.payto_uri} />
- </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>
- <p class="is-size-5"><Translate>To complete the setup of the reserve, you must now initiate a wire transfer using the given wire transfer subject and crediting the specified amount to the indicated account of the exchange.</Translate></p>
- <p class="is-size-5"><Translate>If your system supports RFC 8905, you can do this by opening this URI:</Translate></p>
- <pre>
- <a target="_blank" rel="noreferrer" href={link}>{link}</a>
- </pre>
- <QR text={link} />
- </Template>;
-}
-
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/AutorizeTipModal.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/AutorizeTipModal.tsx
deleted file mode 100644
index e72bfa1f7..000000000
--- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/AutorizeTipModal.tsx
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { h, VNode } from "preact";
-import { useState } from "preact/hooks";
-import { 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 { useTranslator } from "../../../../i18n/index.js";
-import { AuthorizeTipSchema } from "../../../../schemas/index.js";
-import { CreatedSuccessfully } from "./CreatedSuccessfully.js";
-import * as yup from 'yup';
-
-interface AuthorizeTipModalProps {
- onCancel: () => void;
- onConfirm: (value: MerchantBackend.Tips.TipCreateRequest) => void;
- tipAuthorized?: {
- response: MerchantBackend.Tips.TipCreateConfirmation;
- request: MerchantBackend.Tips.TipCreateRequest;
- };
-}
-
-export function AuthorizeTipModal({ onCancel, onConfirm, tipAuthorized }: AuthorizeTipModalProps): VNode {
- // const result = useOrderDetails(id)
- type State = MerchantBackend.Tips.TipCreateRequest
- const [form, setValue] = useState<Partial<State>>({})
- const i18n = useTranslator();
-
- // const [errors, setErrors] = useState<FormErrors<State>>({})
- let errors: FormErrors<State> = {}
- try {
- AuthorizeTipSchema.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 (tipAuthorized) {
- return <ContinueModal description="tip" active onConfirm={onCancel}>
- <CreatedSuccessfully
- entity={tipAuthorized.response}
- request={tipAuthorized.request}
- onConfirm={onCancel}
- />
- </ContinueModal>
- }
-
- return <ConfirmModal description="tip" active onCancel={onCancel} disabled={hasErrors} onConfirm={validateAndConfirm}>
-
- <FormProvider<State> errors={errors} object={form} valueHandler={setValue} >
- <InputCurrency<State> name="amount" label={i18n`Amount`} tooltip={i18n`amount of tip`} />
- <Input<State> name="justification" label={i18n`Justification`} inputType="multiline" tooltip={i18n`reason for the tip`} />
- <Input<State> name="next_url" label={i18n`URL after tip`} tooltip={i18n`URL to visit after tip payment`} />
- </FormProvider>
-
- </ConfirmModal>
-}
-
-
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/index.tsx
deleted file mode 100644
index 9d0b4054d..000000000
--- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/index.tsx
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import { h, VNode } from "preact";
-import { useState } from "preact/hooks";
-import { Loading } from "../../../../components/exception/loading.js";
-import { NotificationCard } from "../../../../components/menu/index.js";
-import { MerchantBackend } from "../../../../declaration.js";
-import { HttpError } from "../../../../hooks/backend.js";
-import {
- useInstanceReserves,
- useReservesAPI,
-} from "../../../../hooks/reserves.js";
-import { useTranslator } from "../../../../i18n/index.js";
-import { Notification } from "../../../../utils/types.js";
-import { CardTable } from "./Table.js";
-import { AuthorizeTipModal } from "./AutorizeTipModal.js";
-
-interface Props {
- onUnauthorized: () => VNode;
- onLoadError: (e: HttpError) => VNode;
- onSelect: (id: string) => void;
- onNotFound: () => VNode;
- onCreate: () => void;
-}
-
-interface TipConfirmation {
- response: MerchantBackend.Tips.TipCreateConfirmation;
- request: MerchantBackend.Tips.TipCreateRequest;
-}
-
-export default function ListTips({
- onUnauthorized,
- onLoadError,
- onNotFound,
- onSelect,
- onCreate,
-}: Props): VNode {
- const result = useInstanceReserves();
- const { deleteReserve, authorizeTipReserve } = useReservesAPI();
- const [notif, setNotif] = useState<Notification | undefined>(undefined);
- const i18n = useTranslator();
- const [reserveForTip, setReserveForTip] = useState<string | undefined>(
- undefined
- );
- const [tipAuthorized, setTipAuthorized] = useState<
- TipConfirmation | undefined
- >(undefined);
-
- if (result.clientError && result.isUnauthorized) return onUnauthorized();
- if (result.clientError && result.isNotfound) return onNotFound();
- if (result.loading) return <Loading />;
- if (!result.ok) return onLoadError(result);
-
- return (
- <section class="section is-main-section">
- <NotificationCard notification={notif} />
-
- {reserveForTip && (
- <AuthorizeTipModal
- onCancel={() => {
- setReserveForTip(undefined);
- setTipAuthorized(undefined);
- }}
- tipAuthorized={tipAuthorized}
- onConfirm={async (request) => {
- try {
- const response = await authorizeTipReserve(
- reserveForTip,
- request
- );
- setTipAuthorized({
- request,
- response: response.data,
- });
- } catch (error) {
- setNotif({
- message: i18n`could not create the tip`,
- type: "ERROR",
- description: error instanceof Error ? error.message : undefined,
- });
- setReserveForTip(undefined);
- }
- }}
- />
- )}
-
- <CardTable
- instances={result.data.reserves
- .filter((r) => r.active)
- .map((o) => ({ ...o, id: o.reserve_pub }))}
- onCreate={onCreate}
- onDelete={(reserve) => deleteReserve(reserve.reserve_pub)}
- onSelect={(reserve) => onSelect(reserve.id)}
- onNewTip={(reserve) => setReserveForTip(reserve.id)}
- />
- </section>
- );
-}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/create/Create.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/Create.stories.tsx
new file mode 100644
index 000000000..53025f153
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/Create.stories.tsx
@@ -0,0 +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)
+ */
+
+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/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx
new file mode 100644
index 000000000..78d7c83ac
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx
@@ -0,0 +1,292 @@
+/*
+ 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 {
+ AmountString,
+ Amounts,
+ Duration,
+ TalerError,
+ TalerMerchantApi,
+ TranslatedString,
+} 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 { InputSelector } from "../../../../components/form/InputSelector.js";
+import { InputToggle } from "../../../../components/form/InputToggle.js";
+import { InputWithAddon } from "../../../../components/form/InputWithAddon.js";
+import { TextField } from "../../../../components/form/TextField.js";
+import { useSessionContext } from "../../../../context/session.js";
+import { useInstanceOtpDevices } from "../../../../hooks/otp.js";
+
+// type Entity = TalerMerchantApi.TemplateAddDetails & { type: Steps };
+type Entity = {
+ id?: string;
+ description?: string;
+ otpId?: string;
+ summary?: string;
+ amount?: AmountString;
+ minimum_age?: number;
+ pay_duration?: Duration;
+ summary_editable?: boolean;
+ amount_editable?: boolean;
+ currency_editable?: boolean;
+};
+
+interface Props {
+ onCreate: (d: TalerMerchantApi.TemplateAddDetails) => Promise<void>;
+ onBack?: () => void;
+}
+
+export function CreatePage({ onCreate, onBack }: Props): VNode {
+ const { i18n } = useTranslationContext();
+ const { config } = useSessionContext();
+ const {state:session} = useSessionContext();
+ const devices = useInstanceOtpDevices();
+
+ const [state, setState] = useState<Partial<Entity>>({
+ minimum_age: 0,
+ pay_duration: {
+ d_ms: 1000 * 60 * 30, //30 min
+ },
+ });
+
+ function updateState(up: (s: Partial<Entity>) => Partial<Entity>) {
+ setState((old) => {
+ const newState = up(old);
+ if (!newState.amount_editable) {
+ newState.currency_editable = false;
+ }
+ return newState;
+ });
+ }
+
+ const parsedPrice = !state.amount ? undefined : Amounts.parse(state.amount);
+
+ const errors: FormErrors<Entity> = {
+ id: !state.id
+ ? i18n.str`should not be empty`
+ : !/[a-zA-Z0-9]*/.test(state.id)
+ ? i18n.str`no valid. only characters and numbers`
+ : undefined,
+ description: !state.description ? i18n.str`should not be empty` : undefined,
+ amount: !state.amount
+ ? undefined
+ : !parsedPrice
+ ? i18n.str`not valid`
+ : Amounts.isZero(parsedPrice)
+ ? i18n.str`must be greater than 0`
+ : undefined,
+ minimum_age:
+ state.minimum_age && state.minimum_age < 0
+ ? i18n.str`should be greater that 0`
+ : undefined,
+ pay_duration: !state.pay_duration
+ ? i18n.str`can't be empty`
+ : state.pay_duration.d_ms === "forever"
+ ? undefined
+ : state.pay_duration.d_ms < 1000 //less than one second
+ ? i18n.str`to short`
+ : undefined,
+ };
+
+ const cList = Object.values(config.currencies).map((d) => d.name);
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as Record<string, unknown>)[k] !== undefined,
+ );
+
+ const submitForm = () => {
+ if (hasErrors) return Promise.reject();
+ return onCreate({
+ template_id: state.id!,
+ template_description: state.description!,
+ template_contract: {
+ minimum_age: state.minimum_age!,
+ pay_duration: Duration.toTalerProtocolDuration(state.pay_duration!),
+ amount: state.amount_editable ? undefined : state.amount,
+ summary: state.summary_editable ? undefined : state.summary,
+ currency:
+ cList.length > 1 && state.currency_editable
+ ? undefined
+ : config.currency,
+ },
+ editable_defaults: {
+ amount: !state.amount_editable ? undefined : state.amount,
+ summary: !state.summary_editable ? undefined : state.summary,
+ currency:
+ cList.length === 1 || !state.currency_editable
+ ? undefined
+ : config.currency,
+ },
+ otp_id: state.otpId!,
+ });
+ };
+ const deviceList =
+ !devices || devices instanceof TalerError || devices.type === "fail"
+ ? []
+ : devices.body.otp_devices;
+ const deviceMap = deviceList.reduce(
+ (prev, cur) => {
+ prev[cur.otp_device_id] = cur.device_description as TranslatedString;
+ return prev;
+ },
+ {} as Record<string, TranslatedString>,
+ );
+ return (
+ <div>
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <FormProvider
+ object={state}
+ valueHandler={updateState}
+ errors={errors}
+ >
+ <InputWithAddon<Entity>
+ name="id"
+ help={
+ new URL(`templates/${state.id ?? ""}`, session.backendUrl.href).href
+ }
+ label={i18n.str`Identifier`}
+ tooltip={i18n.str`Name of the template in URLs.`}
+ />
+ <Input<Entity>
+ name="description"
+ label={i18n.str`Description`}
+ help=""
+ tooltip={i18n.str`Describe what this template stands for`}
+ />
+
+ <Input<Entity>
+ name="summary"
+ inputType="multiline"
+ label={i18n.str`Summary`}
+ tooltip={i18n.str`If specified, this template will create order with the same summary`}
+ />
+ <InputToggle<Entity>
+ name="summary_editable"
+ label={i18n.str`Summary is editable`}
+ tooltip={i18n.str`Allow the user to change the summary.`}
+ />
+
+ <InputCurrency<Entity>
+ name="amount"
+ label={i18n.str`Amount`}
+ tooltip={i18n.str`If specified, this template will create order with the same price`}
+ />
+ <InputToggle<Entity>
+ name="amount_editable"
+ label={i18n.str`Amount is editable`}
+ tooltip={i18n.str`Allow the user to select the amount to pay.`}
+ />
+ {cList.length > 1 && (
+ <Fragment>
+ <InputToggle<Entity>
+ name="currency_editable"
+ readonly={!state.amount_editable}
+ label={i18n.str`Currency is editable`}
+ tooltip={i18n.str`Allow the user to change currency.`}
+ />
+ <TextField name="sc" label={i18n.str`Supported currencies`}>
+ <i18n.Translate>supported currencies: {cList.join(", ")}</i18n.Translate>
+ </TextField>
+ </Fragment>
+ )}
+ <InputNumber<Entity>
+ name="minimum_age"
+ label={i18n.str`Minimum age`}
+ help=""
+ tooltip={i18n.str`Is this contract restricted to some age?`}
+ />
+ <InputDuration<Entity>
+ name="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.`}
+ />
+ {!deviceList.length ? (
+ <TextField
+ name="otpId"
+ label={i18n.str`OTP device`}
+ tooltip={i18n.str`Use to verify transaction while offline.`}
+ >
+ <i18n.Translate>No OTP device.</i18n.Translate>&nbsp;
+ <a href="/otp-devices/new">
+ <i18n.Translate>Add one first</i18n.Translate>
+ </a>
+ </TextField>
+ ) : (
+ <InputSelector<Entity>
+ name="otpId"
+ label={i18n.str`OTP device`}
+ values={[
+ undefined,
+ ...deviceList.map((e) => e.otp_device_id),
+ ]}
+ toStr={(v?: string) => {
+ if (!v) {
+ return i18n.str`No device`;
+ }
+ return deviceMap[v];
+ }}
+ tooltip={i18n.str`Use to verify transaction in offline mode.`}
+ />
+ )}
+ </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/merchant-backoffice-ui/src/paths/instance/templates/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/index.tsx
new file mode 100644
index 000000000..499c7c859
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/index.tsx
@@ -0,0 +1,62 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { 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 { NotificationCard } from "../../../../components/menu/index.js";
+import { useSessionContext } from "../../../../context/session.js";
+import { Notification } from "../../../../utils/types.js";
+import { CreatePage } from "./CreatePage.js";
+
+export type Entity = TalerMerchantApi.TransferInformation;
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+}
+
+export default function CreateTransfer({ onConfirm, onBack }: Props): VNode {
+ const { lib } = useSessionContext();
+ const { state } = useSessionContext();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+
+ return (
+ <>
+ <NotificationCard notification={notif} />
+ <CreatePage
+ onBack={onBack}
+ onCreate={(request: TalerMerchantApi.TemplateAddDetails) => {
+ return lib.instance.addTemplate(state.token, request)
+ .then(() => onConfirm())
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not inform template`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/List.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/List.stories.tsx
new file mode 100644
index 000000000..707324d40
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/List.stories.tsx
@@ -0,0 +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)
+ */
+
+import { FunctionalComponent, h } from "preact";
+import { ListPage as TestedComponent } from "./ListPage.js";
+
+export default {
+ title: "Pages/Templates/List",
+ component: TestedComponent,
+};
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx
new file mode 100644
index 000000000..66d8a2f7e
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx
@@ -0,0 +1,63 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { TalerMerchantApi } from "@gnu-taler/taler-util";
+import { h, VNode } from "preact";
+import { CardTable } from "./Table.js";
+
+export interface Props {
+ templates: TalerMerchantApi.TemplateEntry[];
+ onLoadMoreBefore?: () => void;
+ onLoadMoreAfter?: () => void;
+ onCreate: () => void;
+ onDelete: (e: TalerMerchantApi.TemplateEntry) => void;
+ onSelect: (e: TalerMerchantApi.TemplateEntry) => void;
+ onNewOrder: (e: TalerMerchantApi.TemplateEntry) => void;
+ onQR: (e: TalerMerchantApi.TemplateEntry) => void;
+}
+
+export function ListPage({
+ templates,
+ onCreate,
+ onDelete,
+ onSelect,
+ onNewOrder,
+ onQR,
+ onLoadMoreBefore,
+ onLoadMoreAfter,
+}: Props): VNode {
+
+ return (
+ <CardTable
+ templates={templates.map((o) => ({
+ ...o,
+ id: String(o.template_id),
+ }))}
+ onQR={onQR}
+ onCreate={onCreate}
+ onDelete={onDelete}
+ onSelect={onSelect}
+ onNewOrder={onNewOrder}
+ onLoadMoreBefore={onLoadMoreBefore}
+ onLoadMoreAfter={onLoadMoreAfter}
+ />
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx
new file mode 100644
index 000000000..082e622e3
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx
@@ -0,0 +1,220 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { TalerMerchantApi } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { StateUpdater, useState } from "preact/hooks";
+
+type Entity = TalerMerchantApi.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;
+ onLoadMoreAfter?: () => void;
+}
+
+export function CardTable({
+ templates,
+ onCreate,
+ onDelete,
+ onSelect,
+ onQR,
+ onNewOrder,
+ onLoadMoreAfter,
+ onLoadMoreBefore,
+}: 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}
+ />
+ ) : (
+ <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;
+ onLoadMoreAfter?: () => void;
+}
+
+function Table({
+ instances,
+ onLoadMoreAfter,
+ onDelete,
+ onNewOrder,
+ onQR,
+ onSelect,
+ onLoadMoreBefore,
+}: TableProps): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="table-container">
+ {onLoadMoreBefore && (
+ <button
+ class="button is-fullwidth"
+ data-tooltip={i18n.str`load more templates before the first one`}
+ onClick={onLoadMoreBefore}
+ >
+ <i18n.Translate>load first page</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>
+ {onLoadMoreAfter && (
+ <button
+ class="button is-fullwidth"
+ data-tooltip={i18n.str`load more templates after the last one`}
+ onClick={onLoadMoreAfter}
+ >
+ <i18n.Translate>load next page</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-magnify 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/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx
new file mode 100644
index 000000000..9e59609c7
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx
@@ -0,0 +1,152 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util";
+import {
+ useTranslationContext
+} from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
+import { Loading } from "../../../../components/exception/loading.js";
+import { JumpToElementById } from "../../../../components/form/JumpToElementById.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { ConfirmModal } from "../../../../components/modal/index.js";
+import { useSessionContext } from "../../../../context/session.js";
+import {
+ useInstanceTemplates
+} from "../../../../hooks/templates.js";
+import { Notification } from "../../../../utils/types.js";
+import { LoginPage } from "../../../login/index.js";
+import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
+import { ListPage } from "./ListPage.js";
+
+interface Props {
+ onCreate: () => void;
+ onSelect: (id: string) => void;
+ onNewOrder: (id: string) => void;
+ onQR: (id: string) => void;
+}
+
+export default function ListTemplates({
+ onCreate,
+ onQR,
+ onSelect,
+ onNewOrder,
+}: Props): VNode {
+ const { i18n } = useTranslationContext();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { lib } = useSessionContext();
+ const result = useInstanceTemplates();
+ const [deleting, setDeleting] =
+ useState<TalerMerchantApi.TemplateEntry | null>(null);
+ const { state } = useSessionContext();
+
+ 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} />
+
+ <JumpToElementById
+ testIfExist={async (id) => {
+ const resp = await lib.instance.getTemplateDetails(state.token, id)
+ return resp.type === "ok"
+ }}
+ onSelect={onSelect}
+ description={i18n.str`jump to template with the given template ID`}
+ placeholder={i18n.str`template id`}
+ />
+
+ <ListPage
+ templates={result.body}
+ onLoadMoreBefore={
+ result.isFirstPage ? undefined: result.loadFirst
+ }
+ onLoadMoreAfter={result.isLastPage ? undefined : result.loadNext}
+ onCreate={onCreate}
+ onSelect={(e) => {
+ onSelect(e.template_id);
+ }}
+ onNewOrder={(e) => {
+ onNewOrder(e.template_id);
+ }}
+ onQR={(e) => {
+ onQR(e.template_id);
+ }}
+ onDelete={(e: TalerMerchantApi.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 lib.instance.deleteTemplate(state.token, deleting.template_id);
+ setNotif({
+ message: i18n.str`Template "${deleting.template_description}" (ID: ${deleting.template_id}) has been deleted`,
+ type: "SUCCESS",
+ });
+ } 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/merchant-backoffice-ui/src/paths/instance/templates/qr/Qr.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/Qr.stories.tsx
new file mode 100644
index 000000000..c0059c7bc
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/Qr.stories.tsx
@@ -0,0 +1,27 @@
+/*
+ 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 { QrPage as TestedComponent } from "./QrPage.js";
+
+export default {
+ title: "Pages/Templates/QR",
+ component: TestedComponent,
+};
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx
new file mode 100644
index 000000000..7322ca169
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx
@@ -0,0 +1,154 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import {
+ TalerMerchantApi,
+ stringifyPayTemplateUri
+} from "@gnu-taler/taler-util";
+import {
+ useTranslationContext
+} from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
+import { QR } from "../../../../components/exception/QR.js";
+import { useSessionContext } from "../../../../context/session.js";
+
+// type Entity = TalerMerchantApi.UsingTemplateDetails;
+
+interface Props {
+ contract: TalerMerchantApi.TemplateContractDetails;
+ id: string;
+ onBack?: () => void;
+}
+
+export function QrPage({ id: templateId, onBack }: Props): VNode {
+ const { i18n } = useTranslationContext();
+ const { state } = useSessionContext();
+
+ // const [state, setState] = useState<Partial<Entity>>({
+ // amount: contract.amount,
+ // summary: contract.summary,
+ // });
+
+ // const errors: FormErrors<Entity> = {};
+
+ // const fixedAmount = !!contract.amount;
+ // const fixedSummary = !!contract.summary;
+
+ // const templateParams: Record<string, string> = {};
+ // if (!fixedAmount) {
+ // if (state.amount) {
+ // templateParams.amount = state.amount;
+ // } else {
+ // templateParams.amount = config.currency;
+ // }
+ // }
+
+ // if (!fixedSummary) {
+ // templateParams.summary = state.summary ?? "";
+ // }
+
+ const merchantBaseUrl = state.backendUrl.href;
+
+ const payTemplateUri = stringifyPayTemplateUri({
+ merchantBaseUrl,
+ templateId,
+ templateParams: {},
+ });
+
+ return (
+ <div>
+ <section id="printThis">
+ <QR text={payTemplateUri} />
+ <pre style={{ textAlign: "center" }}>
+ <a href={payTemplateUri}>{payTemplateUri}</a>
+ </pre>
+ </section>
+
+ <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={i18n.str`Amount`}
+ readonly
+ tooltip={i18n.str`Amount of the order`}
+ />
+ <Input<Entity>
+ name="summary"
+ inputType="multiline"
+ readonly
+ label={i18n.str`Summary`}
+ tooltip={i18n.str`Title of the order to be shown to the customer`}
+ />
+ </FormProvider> */}
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <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>
+ </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/merchant-backoffice-ui/src/paths/instance/templates/qr/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/index.tsx
new file mode 100644
index 000000000..ed809c7b3
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/index.tsx
@@ -0,0 +1,66 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util";
+import { VNode, h } from "preact";
+import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
+import { Loading } from "../../../../components/exception/loading.js";
+import {
+ useTemplateDetails
+} from "../../../../hooks/templates.js";
+import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
+import { QrPage } from "./QrPage.js";
+import { LoginPage } from "../../../login/index.js";
+
+export type Entity = TalerMerchantApi.TransferInformation;
+interface Props {
+ onBack?: () => void;
+ tid: string;
+}
+
+export default function TemplateQrPage({
+ tid,
+ onBack,
+}: Props): VNode {
+ const result = useTemplateDetails(tid);
+ 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 (
+ <QrPage contract={result.body.template_contract} id={tid} onBack={onBack} />
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/update/Update.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/Update.stories.tsx
new file mode 100644
index 000000000..303d17b72
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/Update.stories.tsx
@@ -0,0 +1,32 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { 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/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx
new file mode 100644
index 000000000..eedb77f28
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx
@@ -0,0 +1,305 @@
+/*
+ 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 {
+ AmountString,
+ Amounts,
+ Duration,
+ TalerError,
+ TalerMerchantApi,
+ TranslatedString,
+} 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 { InputSelector } from "../../../../components/form/InputSelector.js";
+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";
+
+type Entity = {
+ description?: string;
+ otpId?: string | null;
+ summary?: string;
+ amount?: AmountString;
+ minimum_age?: number;
+ pay_duration?: Duration;
+ summary_editable?: boolean;
+ amount_editable?: boolean;
+ currency_editable?: boolean;
+};
+
+interface Props {
+ onUpdate: (d: TalerMerchantApi.TemplatePatchDetails) => Promise<void>;
+ onBack?: () => void;
+ template: TalerMerchantApi.TemplateDetails & WithId;
+}
+
+export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
+ const { i18n } = useTranslationContext();
+ const { config } = useSessionContext();
+ const {state:session} = useSessionContext();
+
+ const [state, setState] = useState<Partial<Entity>>({
+ description: template.template_description,
+ minimum_age: template.template_contract.minimum_age,
+ otpId: template.otp_id,
+ pay_duration: template.template_contract.pay_duration
+ ? Duration.fromTalerProtocolDuration(
+ template.template_contract.pay_duration,
+ )
+ : undefined,
+ summary:
+ template.editable_defaults?.summary ?? template.template_contract.summary,
+ amount:
+ template.editable_defaults?.amount ??
+ (template.template_contract.amount as AmountString | undefined),
+ currency_editable: !!template.editable_defaults?.currency,
+ summary_editable: !!template.editable_defaults?.summary,
+ amount_editable: !!template.editable_defaults?.amount,
+ });
+
+ function updateState(up: (s: Partial<Entity>) => Partial<Entity>) {
+ setState((old) => {
+ const newState = up(old);
+ if (!newState.amount_editable) {
+ newState.currency_editable = false;
+ }
+ return newState;
+ });
+ }
+
+ const devices = useInstanceOtpDevices();
+ const deviceList =
+ !devices || devices instanceof TalerError || devices.type === "fail"
+ ? []
+ : devices.body.otp_devices;
+ const deviceMap = deviceList.reduce(
+ (prev, cur) => {
+ prev[cur.otp_device_id] = cur.device_description as TranslatedString;
+ return prev;
+ },
+ {} as Record<string, TranslatedString>,
+ );
+
+ const parsedPrice = !state.amount ? undefined : Amounts.parse(state.amount);
+
+ const errors: FormErrors<Entity> = {
+ description: !state.description ? i18n.str`should not be empty` : undefined,
+ amount: !state.amount
+ ? undefined
+ : !parsedPrice
+ ? i18n.str`not valid`
+ : Amounts.isZero(parsedPrice)
+ ? i18n.str`must be greater than 0`
+ : undefined,
+ minimum_age:
+ state.minimum_age && state.minimum_age < 0
+ ? i18n.str`should be greater that 0`
+ : undefined,
+ pay_duration: !state.pay_duration
+ ? i18n.str`can't be empty`
+ : state.pay_duration.d_ms === "forever"
+ ? undefined
+ : state.pay_duration.d_ms < 1000 // less than one second
+ ? i18n.str`to short`
+ : undefined,
+ };
+
+ const cList = Object.values(config.currencies).map((d) => d.name);
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as Record<string, unknown>)[k] !== undefined,
+ );
+
+ const submitForm = () => {
+ if (hasErrors) return Promise.reject();
+ return onUpdate({
+ template_description: state.description!,
+ template_contract: {
+ minimum_age: state.minimum_age!,
+ pay_duration: Duration.toTalerProtocolDuration(state.pay_duration!),
+ amount: state.amount_editable ? undefined : state.amount,
+ summary: state.summary_editable ? undefined : state.summary,
+ currency:
+ cList.length > 1 && state.currency_editable
+ ? undefined
+ : config.currency,
+ },
+ editable_defaults: {
+ amount: !state.amount_editable ? undefined : state.amount,
+ summary: !state.summary_editable ? undefined : state.summary,
+ currency:
+ cList.length === 1 || !state.currency_editable
+ ? undefined
+ : config.currency,
+ },
+ otp_id: state.otpId!,
+ });
+ };
+
+ 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">
+ {new URL(`templates/${template.id}`, session.backendUrl.href).href}
+ </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={updateState}
+ errors={errors}
+ >
+ <Input<Entity>
+ name="description"
+ label={i18n.str`Description`}
+ help=""
+ tooltip={i18n.str`Describe what this template stands for`}
+ />
+ <Input<Entity>
+ name="summary"
+ inputType="multiline"
+ label={i18n.str`Summary`}
+ tooltip={i18n.str`If specified, this template will create order with the same summary`}
+ />
+ <InputToggle<Entity>
+ name="summary_editable"
+ label={i18n.str`Summary is editable`}
+ tooltip={i18n.str`Allow the user to change the summary.`}
+ />
+ <InputCurrency<Entity>
+ name="amount"
+ label={i18n.str`Amount`}
+ tooltip={i18n.str`If specified, this template will create order with the same price`}
+ />
+ <InputToggle<Entity>
+ name="amount_editable"
+ label={i18n.str`Amount is editable`}
+ tooltip={i18n.str`Allow the user to select the amount to pay.`}
+ />
+ {cList.length > 1 && (
+ <Fragment>
+ <InputToggle<Entity>
+ name="currency_editable"
+ readonly={!state.amount_editable}
+ label={i18n.str`Currency is editable`}
+ tooltip={i18n.str`Allow the user to change currency.`}
+ />
+ <TextField name="sc" label={i18n.str`Supported currencies`}>
+ <i18n.Translate>
+ supported currencies: {cList.join(", ")}
+ </i18n.Translate>
+ </TextField>
+ </Fragment>
+ )}
+ <InputNumber<Entity>
+ name="minimum_age"
+ label={i18n.str`Minimum age`}
+ help=""
+ tooltip={i18n.str`Is this contract restricted to some age?`}
+ />
+ <InputDuration<Entity>
+ name="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.`}
+ />
+ {!deviceList.length ? (
+ <TextField
+ name="otpId"
+ label={i18n.str`OTP device`}
+ tooltip={i18n.str`Use to verify transaction while offline.`}
+ >
+ <i18n.Translate>No OTP device.</i18n.Translate>&nbsp;
+ <a href="/otp-devices/new">
+ <i18n.Translate>Add one first</i18n.Translate>
+ </a>
+ </TextField>
+ ) : (
+ <InputSelector<Entity>
+ name="otpId"
+ label={i18n.str`OTP device`}
+ values={[
+ undefined,
+ ...deviceList.map((e) => e.otp_device_id),
+ ]}
+ toStr={(v?: string) => {
+ if (!v) {
+ return i18n.str`No device`;
+ }
+ return deviceMap[v];
+ }}
+ tooltip={i18n.str`Use to verify transaction in offline mode.`}
+ />
+ )}
+ </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/merchant-backoffice-ui/src/paths/instance/templates/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/index.tsx
new file mode 100644
index 000000000..6185bd2a9
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/index.tsx
@@ -0,0 +1,97 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util";
+import {
+ useTranslationContext
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { useSessionContext } from "../../../../context/session.js";
+import {
+ useTemplateDetails,
+} from "../../../../hooks/templates.js";
+import { Notification } from "../../../../utils/types.js";
+import { LoginPage } from "../../../login/index.js";
+import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
+import { UpdatePage } from "./UpdatePage.js";
+
+export type Entity = TalerMerchantApi.TemplatePatchDetails & WithId;
+
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+ tid: string;
+}
+export default function UpdateTemplate({
+ tid,
+ onConfirm,
+ onBack,
+}: Props): VNode {
+ const { lib } = useSessionContext();
+ const { state } = useSessionContext();
+ const result = useTemplateDetails(tid);
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+
+ const { i18n } = useTranslationContext();
+
+ if (!result) return <Loading />
+ if (result instanceof TalerError) {
+ return <ErrorLoadingMerchant error={result} />
+ }
+ if (result.type === "fail") {
+ switch(result.case) {
+ case HttpStatusCode.NotFound: {
+ return <NotFoundPageOrAdminCreate />
+ }
+ case HttpStatusCode.Unauthorized: {
+ return <LoginPage />
+ }
+ default: {
+ assertUnreachable(result)
+ }
+ }
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+ <UpdatePage
+ template={{...result.body, id: tid}}
+ onBack={onBack}
+ onUpdate={(data) => {
+ return lib.instance.updateTemplate(state.token, tid, data)
+ .then(onConfirm)
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not update template`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/use/Use.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/use/Use.stories.tsx
new file mode 100644
index 000000000..d91888b97
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/use/Use.stories.tsx
@@ -0,0 +1,27 @@
+/*
+ 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 { UsePage as TestedComponent } from "./UsePage.js";
+
+export default {
+ title: "Pages/Templates/Create",
+ component: TestedComponent,
+};
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx
new file mode 100644
index 000000000..360c9d373
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx
@@ -0,0 +1,144 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { TalerMerchantApi } from "@gnu-taler/taler-util";
+import { 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";
+
+type Entity = TalerMerchantApi.TemplateContractDetails;
+
+interface Props {
+ id: string;
+ template: TalerMerchantApi.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>>({
+ currency: template.editable_defaults?.currency ?? template.template_contract.currency,
+ amount: template.editable_defaults?.amount ?? template.template_contract.amount,
+ summary: template.editable_defaults?.summary ?? template.template_contract.summary,
+ });
+
+ const errors: FormErrors<Entity> = {
+ amount:
+ !state.amount
+ ? i18n.str`Amount is required`
+ : undefined,
+ 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/merchant-backoffice-ui/src/paths/instance/templates/use/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/use/index.tsx
new file mode 100644
index 000000000..00cb2b827
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/use/index.tsx
@@ -0,0 +1,108 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util";
+import {
+ useTranslationContext
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import {
+ useTemplateDetails
+} from "../../../../hooks/templates.js";
+import { Notification } from "../../../../utils/types.js";
+import { LoginPage } from "../../../login/index.js";
+import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
+import { UsePage } from "./UsePage.js";
+import { useSessionContext } from "../../../../context/session.js";
+
+export type Entity = TalerMerchantApi.TransferInformation;
+interface Props {
+ onBack?: () => void;
+ onOrderCreated: (id: string) => void;
+ tid: string;
+}
+
+export default function TemplateUsePage({
+ tid,
+ onOrderCreated,
+ onBack,
+}: Props): VNode {
+ const { lib } = useSessionContext();
+ const result = useTemplateDetails(tid);
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ 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 (
+ <>
+ <NotificationCard notification={notif} />
+ <UsePage
+ template={result.body}
+ id={tid}
+ onBack={onBack}
+ onCreateOrder={(
+ request: TalerMerchantApi.UsingTemplateDetails,
+ ) => {
+
+ return lib.instance.useTemplateCreateOrder(tid, request)
+ .then((res) => {
+ if (res.type === "ok") {
+ onOrderCreated(res.body.order_id)
+ } else {
+ setNotif({
+ message: i18n.str`could not create order from template`,
+ type: "ERROR",
+ });
+ }
+ })
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not create order from template`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx
new file mode 100644
index 000000000..f75ee89b8
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx
@@ -0,0 +1,188 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { 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 { NotificationCard } from "../../../components/menu/index.js";
+import { useSessionContext } from "../../../context/session.js";
+import { AccessToken, createAccessToken } from "@gnu-taler/taler-util";
+
+interface Props {
+ hasToken: boolean | undefined;
+ onClearToken: (c: AccessToken | undefined) => void;
+ onNewToken: (c: AccessToken | undefined, s: AccessToken) => void;
+ onBack?: () => void;
+}
+
+export function DetailPage({
+ 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 Record<string, unknown>)[k] !== undefined,
+ );
+
+ const { state } = useSessionContext();
+
+ const text = i18n.str`You are updating the access token from instance with id "${state.instance}"`;
+
+ async function submitForm() {
+ if (hasErrors) return;
+ const oldToken =
+ form.old_token !== undefined && hasToken
+ ? createAccessToken(form.old_token)
+ : undefined;
+ const newToken = createAccessToken(form.new_token!);
+ 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) {
+ onClearToken(form.old_token ? createAccessToken(form.old_token) : undefined);
+ } 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>
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <a class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </a>
+ )}
+ <AsyncButton
+ type="submit"
+ disabled={hasErrors}
+ data-tooltip={
+ hasErrors
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ onClick={submitForm}
+ >
+ <i18n.Translate>Confirm change</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </FormProvider>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx
new file mode 100644
index 000000000..c23e5be17
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx
@@ -0,0 +1,152 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ HttpStatusCode,
+ TalerError,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
+import {
+ useTranslationContext
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { ErrorLoadingMerchant } from "../../../components/ErrorLoadingMerchant.js";
+import { Loading } from "../../../components/exception/loading.js";
+import { NotificationCard } from "../../../components/menu/index.js";
+import { useSessionContext } from "../../../context/session.js";
+import { useInstanceDetails } from "../../../hooks/instance.js";
+import { Notification } from "../../../utils/types.js";
+import { LoginPage } from "../../login/index.js";
+import { NotFoundPageOrAdminCreate } from "../../notfound/index.js";
+import { DetailPage } from "./DetailPage.js";
+
+interface Props {
+ onChange: () => void;
+ onCancel: () => void;
+}
+
+export default function Token({ onChange, onCancel }: Props): VNode {
+ const { i18n } = useTranslationContext();
+ const { lib } = useSessionContext();
+ const { logIn } = useSessionContext();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const result = useInstanceDetails();
+
+ if (!result) return <Loading />;
+ if (result instanceof TalerError) {
+ return <ErrorLoadingMerchant error={result} />;
+ }
+ if (result.type === "fail") {
+ switch (result.case) {
+ case HttpStatusCode.Unauthorized: {
+ return <LoginPage />;
+ }
+ case HttpStatusCode.NotFound: {
+ return <NotFoundPageOrAdminCreate />;
+ }
+ default: {
+ assertUnreachable(result);
+ }
+ }
+ }
+
+ const hasToken = result.body.auth.method === "token";
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+ <DetailPage
+ onBack={onCancel}
+ hasToken={hasToken}
+ onClearToken={async (currentToken): Promise<void> => {
+ try {
+ const resp = await lib.instance.updateCurrentInstanceAuthentication(
+ currentToken,
+ {
+ method: "external",
+ },
+ );
+ if (resp.type === "ok") {
+ onChange();
+ } else {
+ return setNotif({
+ message: i18n.str`Failed to clear token`,
+ type: "ERROR",
+ description: resp.detail.hint,
+ });
+ }
+ } catch (error) {
+ if (error instanceof Error) {
+ return setNotif({
+ message: i18n.str`Failed to clear token`,
+ type: "ERROR",
+ description: error.message,
+ });
+ }
+ }
+ }}
+ onNewToken={async (currentToken, newToken): Promise<void> => {
+ try {
+ {
+ const resp =
+ await lib.instance.updateCurrentInstanceAuthentication(
+ currentToken,
+ {
+ token: newToken,
+ method: "token",
+ },
+ );
+ if (resp.type === "fail") {
+ return setNotif({
+ message: i18n.str`Failed to set new token`,
+ type: "ERROR",
+ description: resp.detail.hint,
+ });
+ }
+ }
+ const resp = await lib.authenticate.createAccessTokenBearer(
+ newToken,
+ {
+ scope: "write",
+ duration: {
+ d_us: "forever",
+ },
+ refreshable: true,
+ },
+ );
+ if (resp.type === "ok") {
+ logIn(resp.body.token);
+ return onChange();
+ } else {
+ return setNotif({
+ message: i18n.str`Failed to set new token`,
+ type: "ERROR",
+ });
+ }
+ } catch (error) {
+ if (error instanceof Error) {
+ return setNotif({
+ message: i18n.str`Failed to set new token`,
+ type: "ERROR",
+ description: error.message,
+ });
+ }
+ }
+ }}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/token/stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/token/stories.tsx
new file mode 100644
index 000000000..581828657
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/token/stories.tsx
@@ -0,0 +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)
+ */
+
+import { DetailPage as TestedComponent } from "./DetailPage.js";
+
+export default {
+ title: "Pages/Token",
+ component: TestedComponent,
+};
+
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/Create.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/Create.stories.tsx
index a21fe8e5b..ca38defc3 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/Create.stories.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/Create.stories.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,29 +15,31 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { h, VNode, FunctionalComponent } from 'preact';
+import { h, VNode, FunctionalComponent } from "preact";
import { CreatePage as TestedComponent } from "./CreatePage.js";
-
export default {
- title: 'Pages/Transfer/Create',
+ title: "Pages/Transfer/Create",
component: TestedComponent,
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ 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
+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']
+ accounts: ["payto://x-taler-bank/account1", "payto://x-taler-bank/account2"],
});
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx
index 15860381c..91aabe58e 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,90 +15,130 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { h, VNode } from "preact";
+import { AmountString, TalerMerchantApi } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { 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 {
+ 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 { Translate, useTranslator } from "../../../../i18n/index.js";
-import { CROCKFORD_BASE32_REGEX, URL_REGEX } from "../../../../utils/constants.js";
+import {
+ CROCKFORD_BASE32_REGEX,
+ URL_REGEX,
+} from "../../../../utils/constants.js";
-type Entity = MerchantBackend.Transfers.TransferInformation
+type Entity = TalerMerchantApi.TransferInformation;
interface Props {
onCreate: (d: Entity) => Promise<void>;
onBack?: () => void;
- accounts: string[],
+ accounts: string[];
}
export function CreatePage({ accounts, onCreate, onBack }: Props): VNode {
- const i18n = useTranslator()
- const { currency } = useConfigContext()
+ const { i18n } = useTranslationContext();
const [state, setState] = useState<Partial<Entity>>({
- wtid: '',
+ wtid: "",
// payto_uri: ,
// exchange_url: 'http://exchange.taler:8081/',
- credit_amount: ``,
+ credit_amount: `` as AmountString,
});
const errors: FormErrors<Entity> = {
- wtid: !state.wtid ? i18n`cannot be empty` :
- (!CROCKFORD_BASE32_REGEX.test(state.wtid) ? i18n`check the id, does not look valid` :
- (state.wtid.length !== 52 ? i18n`should have 52 characters, current ${state.wtid.length}` :
- undefined)),
- payto_uri: !state.payto_uri ? i18n`cannot be empty` : undefined,
- credit_amount: !state.credit_amount ? i18n`cannot be empty` : undefined,
- exchange_url: !state.exchange_url ? i18n`cannot be empty` :
- (!URL_REGEX.test(state.exchange_url) ? i18n`URL doesn't have the right format` : undefined),
- }
-
- const hasErrors = Object.keys(errors).some(k => (errors as any)[k] !== undefined)
+ 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`Credited bank account`}
- values={accounts}
- placeholder={i18n`Select one account`}
- tooltip={i18n`Bank account of the merchant where the payment was received`}
- />
- <Input<Entity> name="wtid" label={i18n`Wire transfer ID`} help="" tooltip={i18n`unique identifier of the wire transfer used by the exchange, must be 52 characters long`} />
- <Input<Entity> name="exchange_url"
- label={i18n`Exchange URL`}
- tooltip={i18n`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`Amount credited`} tooltip={i18n`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} ><Translate>Cancel</Translate></button>}
- <AsyncButton disabled={hasErrors} data-tooltip={
- hasErrors ? i18n`Need to complete marked fields` : 'confirm operation'
- } onClick={submitForm} ><Translate>Confirm</Translate></AsyncButton>
+ 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>
- <div class="column" />
- </div>
- </section>
- </div>
+ </section>
+ </div>
+ );
}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/index.tsx
index 0d3676096..428476337 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/index.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,46 +15,58 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { Fragment, h, VNode } from 'preact';
-import { useState } from 'preact/hooks';
+import { TalerError, 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 { NotificationCard } from "../../../../components/menu/index.js";
-import { MerchantBackend } from "../../../../declaration.js";
-import { useInstanceDetails } from "../../../../hooks/instance.js";
-import { useTransferAPI } from "../../../../hooks/transfer.js";
-import { useTranslator } from '../../../../i18n/index.js';
+import { useSessionContext } from "../../../../context/session.js";
+import { useInstanceBankAccounts } from "../../../../hooks/bank.js";
import { Notification } from "../../../../utils/types.js";
import { CreatePage } from "./CreatePage.js";
-export type Entity = MerchantBackend.Transfers.TransferInformation
+export type Entity = TalerMerchantApi.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 = useTranslator()
- const instance = useInstanceDetails()
- 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`could not inform transfer`,
- type: "ERROR",
- description: error.message
- })
- })
- }} />
- </>
-} \ No newline at end of file
+export default function CreateTransfer({ onConfirm, onBack }: Props): VNode {
+ const { lib } = useSessionContext();
+ const { state } = useSessionContext();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+ const instance = useInstanceBankAccounts();
+ const accounts =
+ !instance || instance instanceof TalerError || instance.type === "fail"
+ ? []
+ : instance.body.accounts.map((a) => a.payto_uri);
+
+ return (
+ <>
+ <NotificationCard notification={notif} />
+ <CreatePage
+ onBack={onBack}
+ accounts={accounts}
+ onCreate={(request: TalerMerchantApi.TransferInformation) => {
+ return lib.instance
+ .informWireTransfer(state.token, request)
+ .then(() => onConfirm())
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not inform transfer`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/List.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/List.stories.tsx
index 94d03a492..def03fe27 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/List.stories.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/List.stories.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2021-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -19,7 +19,8 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { h, VNode, FunctionalComponent } from "preact";
+import { AmountString, PaytoString } from "@gnu-taler/taler-util";
+import { FunctionalComponent, h } from "preact";
import { ListPage as TestedComponent } from "./ListPage.js";
export default {
@@ -39,7 +40,7 @@ export default {
function createExample<Props>(
Component: FunctionalComponent<Props>,
- props: Partial<Props>
+ props: Partial<Props>,
) {
const r = (args: any) => <Component {...args} />;
r.args = props;
@@ -50,8 +51,8 @@ export const Example = createExample(TestedComponent, {
transfers: [
{
exchange_url: "http://exchange.url/",
- credit_amount: "TESTKUDOS:10",
- payto_uri: "payto//x-taler-bank/bank:8080/account",
+ credit_amount: "TESTKUDOS:10" as AmountString,
+ payto_uri: "payto//x-taler-bank/bank:8080/account" as PaytoString,
transfer_serial_id: 123123123,
wtid: "!@KJELQKWEJ!L@K#!J@",
confirmed: true,
@@ -62,8 +63,8 @@ export const Example = createExample(TestedComponent, {
},
{
exchange_url: "http://exchange.url/",
- credit_amount: "TESTKUDOS:10",
- payto_uri: "payto//x-taler-bank/bank:8080/account",
+ credit_amount: "TESTKUDOS:10" as AmountString,
+ payto_uri: "payto//x-taler-bank/bank:8080/account" as PaytoString,
transfer_serial_id: 123123123,
wtid: "!@KJELQKWEJ!L@K#!J@",
confirmed: true,
@@ -74,8 +75,8 @@ export const Example = createExample(TestedComponent, {
},
{
exchange_url: "http://exchange.url/",
- credit_amount: "TESTKUDOS:10",
- payto_uri: "payto//x-taler-bank/bank:8080/account",
+ credit_amount: "TESTKUDOS:10" as AmountString,
+ payto_uri: "payto//x-taler-bank/bank:8080/account" as PaytoString,
transfer_serial_id: 123123123,
wtid: "!@KJELQKWEJ!L@K#!J@",
confirmed: true,
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 25f9ff95c..22ad0b8d8 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
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,19 +15,19 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { h, VNode } from 'preact';
+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 { Translate, useTranslator } from '../../../../i18n/index.js';
import { CardTable } from "./Table.js";
+import { TalerMerchantApi } from "@gnu-taler/taler-util";
export interface Props {
- transfers: MerchantBackend.Transfers.TransferDetails[];
+ transfers: TalerMerchantApi.TransferDetails[];
onLoadMoreBefore?: () => void;
onLoadMoreAfter?: () => void;
onShowAll: () => void;
@@ -43,47 +43,95 @@ export interface Props {
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 }
+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 = useTranslator();
- 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`Address`}
- values={accounts}
- placeholder={i18n`Select one account`}
- tooltip={i18n`filter by account address`} />
- </FormProvider>
+ 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}
+ fromStr={(d) => {
+ const idx = accounts.indexOf(d)
+ if (idx === -1) return undefined;
+ return d
+ }}
+ placeholder={i18n.str`Select one account`}
+ tooltip={i18n.str`filter by account address`}
+ />
+ </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>
- <div class="column" />
- </div>
- <div class="tabs">
- <ul>
- <li class={isAllTransfers ? 'is-active' : ''}>
- <div class="has-tooltip-right" data-tooltip={i18n`remove all filters`}>
- <a onClick={onShowAll}><Translate>All</Translate></a>
- </div>
- </li>
- <li class={isVerifiedTransfers ? 'is-active' : ''}>
- <div class="has-tooltip-right" data-tooltip={i18n`only show wire transfers confirmed by the merchant`}>
- <a onClick={onShowVerified}><Translate>Verified</Translate></a>
- </div>
- </li>
- <li class={isNonVerifiedTransfers ? 'is-active' : ''}>
- <div class="has-tooltip-right" data-tooltip={i18n`only show wire transfers claimed by the exchange`}>
- <a onClick={onShowUnverified}><Translate>Unverified</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>;
+ <CardTable
+ transfers={transfers.map((o) => ({
+ ...o,
+ id: String(o.transfer_serial_id),
+ }))}
+ accounts={accounts}
+ onCreate={onCreate}
+ onDelete={onDelete}
+ onLoadMoreBefore={onLoadMoreBefore}
+ onLoadMoreAfter={onLoadMoreAfter}
+ />
+ </section>
+ );
}
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 26cb1ff83..b9235c669 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
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2021-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -19,13 +19,14 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
+import { TalerMerchantApi } 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 { MerchantBackend, WithId } from "../../../../declaration.js";
-import { Translate, useTranslator } from "../../../../i18n/index.js";
+import { datetimeFormatForSettings, usePreference } from "../../../../hooks/preference.js";
-type Entity = MerchantBackend.Transfers.TransferDetails & WithId;
+type Entity = TalerMerchantApi.TransferDetails & WithId;
interface Props {
transfers: Entity[];
@@ -33,8 +34,6 @@ interface Props {
onCreate: () => void;
accounts: string[];
onLoadMoreBefore?: () => void;
- hasMoreBefore?: boolean;
- hasMoreAfter?: boolean;
onLoadMoreAfter?: () => void;
}
@@ -44,24 +43,25 @@ export function CardTable({
onDelete,
onLoadMoreAfter,
onLoadMoreBefore,
- hasMoreAfter,
- hasMoreBefore,
}: Props): VNode {
const [rowSelection, rowSelectionHandler] = useState<string[]>([]);
- const i18n = useTranslator();
+ 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-bank" />
+ <i class="mdi mdi-arrow-left-right" />
</span>
- <Translate>Transfers</Translate>
+ <i18n.Translate>Transfers</i18n.Translate>
</p>
<div class="card-header-icon" aria-label="more options">
- <span class="has-tooltip-left" data-tooltip={i18n`add new transfer`}>
+ <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" />
@@ -81,8 +81,6 @@ export function CardTable({
rowSelectionHandler={rowSelectionHandler}
onLoadMoreAfter={onLoadMoreAfter}
onLoadMoreBefore={onLoadMoreBefore}
- hasMoreAfter={hasMoreAfter}
- hasMoreBefore={hasMoreBefore}
/>
) : (
<EmptyTable />
@@ -99,60 +97,51 @@ interface TableProps {
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 = useTranslator();
+ const { i18n } = useTranslationContext();
+ const [settings] = usePreference();
return (
<div class="table-container">
{onLoadMoreBefore && (
<button
class="button is-fullwidth"
- data-tooltip={i18n`load more transfers before the first one`}
- disabled={!hasMoreBefore}
+ data-tooltip={i18n.str`load more transfers before the first one`}
onClick={onLoadMoreBefore}
>
- <Translate>load newer transfers</Translate>
+ <i18n.Translate>load first page</i18n.Translate>
</button>
)}
<table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
<thead>
<tr>
<th>
- <Translate>ID</Translate>
+ <i18n.Translate>ID</i18n.Translate>
</th>
<th>
- <Translate>Credit</Translate>
+ <i18n.Translate>Credit</i18n.Translate>
</th>
<th>
- <Translate>Address</Translate>
+ <i18n.Translate>Address</i18n.Translate>
</th>
<th>
- <Translate>Exchange URL</Translate>
+ <i18n.Translate>Exchange URL</i18n.Translate>
</th>
<th>
- <Translate>Confirmed</Translate>
+ <i18n.Translate>Confirmed</i18n.Translate>
</th>
<th>
- <Translate>Verified</Translate>
+ <i18n.Translate>Verified</i18n.Translate>
</th>
<th>
- <Translate>Executed at</Translate>
+ <i18n.Translate>Executed at</i18n.Translate>
</th>
<th />
</tr>
@@ -165,23 +154,23 @@ function Table({
<td>{i.credit_amount}</td>
<td>{i.payto_uri}</td>
<td>{i.exchange_url}</td>
- <td>{i.confirmed ? i18n`yes` : i18n`no`}</td>
- <td>{i.verified ? i18n`yes` : i18n`no`}</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`never`
+ ? i18n.str`never`
: format(
- i.execution_time.t_s * 1000,
- "yyyy/MM/dd HH:mm:ss"
- )
- : i18n`unknown`}
+ 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`delete selected transfer from the database`}
+ data-tooltip={i18n.str`delete selected transfer from the database`}
onClick={() => onDelete(i)}
>
Delete
@@ -196,11 +185,10 @@ function Table({
{onLoadMoreAfter && (
<button
class="button is-fullwidth"
- data-tooltip={i18n`load more transfer after the last one`}
- disabled={!hasMoreAfter}
+ data-tooltip={i18n.str`load more transfers after the last one`}
onClick={onLoadMoreAfter}
>
- <Translate>load older transfers</Translate>
+ <i18n.Translate>load next page</i18n.Translate>
</button>
)}
</div>
@@ -208,17 +196,18 @@ function Table({
}
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" />
+ <i class="mdi mdi-magnify mdi-48px" />
</span>
</p>
<p>
- <Translate>
+ <i18n.Translate>
There is no transfer yet, add more pressing the + sign
- </Translate>
+ </i18n.Translate>
</p>
</div>
);
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx
index c3079c136..8b4d1f3cb 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
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,71 +15,98 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { h, VNode } from 'preact';
-import { useState } from 'preact/hooks';
+import { HttpStatusCode, TalerError, assertUnreachable } from "@gnu-taler/taler-util";
+import { VNode, h } from "preact";
+import { useEffect, useState } from "preact/hooks";
+import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
import { Loading } from "../../../../components/exception/loading.js";
-import { MerchantBackend } from "../../../../declaration.js";
-import { HttpError } from "../../../../hooks/backend.js";
-import { useInstanceDetails } from "../../../../hooks/instance.js";
+import { useInstanceBankAccounts } from "../../../../hooks/bank.js";
import { useInstanceTransfers } from "../../../../hooks/transfer.js";
+import { LoginPage } from "../../../login/index.js";
import { ListPage } from "./ListPage.js";
+import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
interface Props {
- onUnauthorized: () => VNode;
- onLoadError: (error: HttpError) => VNode;
- onNotFound: () => VNode;
onCreate: () => void;
}
interface Form {
- verified?: 'yes' | 'no';
+ verified?: boolean;
payto_uri?: string;
}
-export default function ListTransfer({ onUnauthorized, onLoadError, onCreate, onNotFound }: Props): VNode {
- const [form, setForm] = useState<Form>({ payto_uri: '' })
- const setFilter = (s?: 'yes' | 'no') => setForm({ ...form, verified: s })
+export default function ListTransfer({
+ onCreate,
+}: Props): VNode {
+ const setFilter = (s?: boolean) => setForm({ ...form, verified: s });
- const [position, setPosition] = useState<string | undefined>(undefined)
+ const [position, setPosition] = useState<string | undefined>(undefined);
- const instance = useInstanceDetails()
- const accounts = !instance.ok ? [] : instance.data.accounts.map(a => a.payto_uri)
+ const instance = useInstanceBankAccounts();
+ 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 isVerifiedTransfers = form.verified === 'yes'
- const isNonVerifiedTransfers = form.verified === 'no'
- const isAllTransfers = form.verified === undefined
+ const shoulUseDefaultAccount = accounts.length === 1
+ useEffect(() => {
+ if (shoulUseDefaultAccount) {
+ setForm({...form, payto_uri: accounts[0]})
+ }
+ }, [shoulUseDefaultAccount])
- const result = useInstanceTransfers({
- position,
- payto_uri: form.payto_uri === '' ? undefined : form.payto_uri,
- verified: form.verified,
- }, (id) => setPosition(id))
+ const isVerifiedTransfers = form.verified === true;
+ const isNonVerifiedTransfers = form.verified === false;
+ const isAllTransfers = form.verified === undefined;
- if (result.clientError && result.isUnauthorized) return onUnauthorized()
- if (result.clientError && result.isNotfound) return onNotFound()
- if (result.loading) return <Loading />
- if (!result.ok) return onLoadError(result)
+ const result = useInstanceTransfers(
+ {
+ position,
+ payto_uri: form.payto_uri === "" ? undefined : form.payto_uri,
+ verified: form.verified,
+ },
+ (id) => setPosition(id),
+ );
- 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 }))}
- />
+ if (!result) return <Loading />;
+ if (result instanceof TalerError) {
+ return <ErrorLoadingMerchant error={result} />;
+ }
+ if (result.type === "fail") {
+ switch (result.case) {
+ case HttpStatusCode.Unauthorized: {
+ return <LoginPage />
+ }
+ case HttpStatusCode.NotFound: {
+ return <NotFoundPageOrAdminCreate />;
+ }
+ default: {
+ assertUnreachable(result);
+ }
+ }
+ }
+ return (
+ <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 }))}
+ />
+ );
}
-
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/update/index.tsx
index caa808693..719f99209 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/transfers/update/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/transfers/update/index.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,12 +15,12 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { h, VNode } from 'preact';
+import { h, VNode } from "preact";
-export default function UpdateTransfer():VNode {
- return <div>order transfer page</div>
-} \ No newline at end of file
+export default function UpdateTransfer(): VNode {
+ return <div>order transfer page</div>;
+}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/update/Update.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/update/Update.stories.tsx
index 8c4717275..5bd12e4e9 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/update/Update.stories.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/update/Update.stories.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2021-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -19,7 +19,7 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { h, VNode, FunctionalComponent } from "preact";
+import { FunctionalComponent, h } from "preact";
import { UpdatePage as TestedComponent } from "./UpdatePage.js";
export default {
@@ -33,7 +33,7 @@ export default {
function createExample<Props>(
Component: FunctionalComponent<Props>,
- props: Partial<Props>
+ props: Partial<Props>,
) {
const r = (args: any) => <Component {...args} />;
r.args = props;
@@ -42,19 +42,17 @@ function createExample<Props>(
export const Example = createExample(TestedComponent, {
selected: {
- accounts: [],
name: "name",
auth: { method: "external" },
address: {},
+ user_type: "business",
+ use_stefan: true,
jurisdiction: {},
- default_max_deposit_fee: "TESTKUDOS:2",
- default_max_wire_fee: "TESTKUDOS:1",
default_pay_delay: {
- d_us: 1000000,
+ d_us: 1000 * 1000, //one second
},
- default_wire_fee_amortization: 1,
default_wire_transfer_delay: {
- d_us: 100000,
+ d_us: 1000 * 1000, //one second
},
merchant_pub: "ASDWQEKASJDKSADJ",
},
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx
index b5328249a..cde58967f 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2021-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -19,137 +19,101 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { h, VNode } from "preact";
+import { Duration, TalerMerchantApi } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
import { useState } from "preact/hooks";
-import * as yup from "yup";
import { AsyncButton } from "../../../components/exception/AsyncButton.js";
import {
- FormProvider,
FormErrors,
+ FormProvider,
} from "../../../components/form/FormProvider.js";
-import { UpdateTokenModal } from "../../../components/modal/index.js";
-import { useInstanceContext } from "../../../context/instance.js";
-import { MerchantBackend } from "../../../declaration.js";
-import { Translate, useTranslator } from "../../../i18n/index.js";
import { DefaultInstanceFormFields } from "../../../components/instance/DefaultInstanceFormFields.js";
-import { PAYTO_REGEX } from "../../../utils/constants.js";
-import { Amounts } from "@gnu-taler/taler-util";
+import { useSessionContext } from "../../../context/session.js";
import { undefinedIfEmpty } from "../../../utils/table.js";
-type Entity = MerchantBackend.Instances.InstanceReconfigurationMessage & {
- auth_token?: string;
+export type Entity = Omit<Omit<TalerMerchantApi.InstanceReconfigurationMessage, "default_pay_delay">, "default_wire_transfer_delay"> & {
+ default_pay_delay: Duration,
+ default_wire_transfer_delay: Duration,
};
-//MerchantBackend.Instances.InstanceAuthConfigurationMessage
+//TalerMerchantApi.InstanceAuthConfigurationMessage
interface Props {
- onUpdate: (d: Entity) => void;
- onChangeAuth: (
- d: MerchantBackend.Instances.InstanceAuthConfigurationMessage
- ) => Promise<void>;
- selected: MerchantBackend.Instances.QueryInstancesResponse;
+ onUpdate: (d: TalerMerchantApi.InstanceReconfigurationMessage) => void;
+ selected: TalerMerchantApi.QueryInstancesResponse;
isLoading: boolean;
onBack: () => void;
}
function convert(
- from: MerchantBackend.Instances.QueryInstancesResponse
+ from: TalerMerchantApi.QueryInstancesResponse,
): Entity {
- const { accounts, ...rest } = from;
- const payto_uris = accounts.filter((a) => a.active).map((a) => a.payto_uri);
+ const { default_pay_delay, default_wire_transfer_delay, ...rest } = from;
+
const defaults = {
- default_wire_fee_amortization: 1,
- default_pay_delay: { d_us: 2 * 1000 * 1000 * 60 * 60 }, //two hours
- default_wire_transfer_delay: { d_us: 2 * 1000 * 1000 * 60 * 60 * 2 }, //two hours
+ use_stefan: false,
+ default_pay_delay: Duration.fromTalerProtocolDuration(default_pay_delay),
+ default_wire_transfer_delay: Duration.fromTalerProtocolDuration(default_wire_transfer_delay),
};
- return { ...defaults, ...rest, payto_uris };
-}
-
-function getTokenValuePart(t?: string): string | undefined {
- if (!t) return t;
- const match = /secret-token:(.*)/.exec(t);
- if (!match || !match[1]) return undefined;
- return match[1];
+ return { ...defaults, ...rest };
}
export function UpdatePage({
onUpdate,
- onChangeAuth,
selected,
onBack,
}: Props): VNode {
- const { id, token } = useInstanceContext();
- const currentTokenValue = getTokenValuePart(token);
-
- function updateToken(token: string | undefined | null) {
- const value =
- token && token.startsWith("secret-token:")
- ? token.substring("secret-token:".length)
- : token;
-
- if (!token) {
- onChangeAuth({ method: "external" });
- } else {
- onChangeAuth({ method: "token", token: `secret-token:${value}` });
- }
- }
+ const { state } = useSessionContext();
const [value, valueHandler] = useState<Partial<Entity>>(convert(selected));
- const i18n = useTranslator();
+ const { i18n } = useTranslationContext();
const errors: FormErrors<Entity> = {
- name: !value.name ? i18n`required` : undefined,
- payto_uris:
- !value.payto_uris || !value.payto_uris.length
- ? i18n`required`
- : undefinedIfEmpty(
- value.payto_uris.map((p) => {
- return !PAYTO_REGEX.test(p) ? i18n`is not valid` : undefined;
- })
- ),
- default_max_deposit_fee: !value.default_max_deposit_fee
- ? i18n`required`
- : !Amounts.parse(value.default_max_deposit_fee)
- ? i18n`invalid format`
- : undefined,
- default_max_wire_fee: !value.default_max_wire_fee
- ? i18n`required`
- : !Amounts.parse(value.default_max_wire_fee)
- ? i18n`invalid format`
- : undefined,
- default_wire_fee_amortization:
- value.default_wire_fee_amortization === undefined
- ? i18n`required`
- : isNaN(value.default_wire_fee_amortization)
- ? i18n`is not a number`
- : value.default_wire_fee_amortization < 1
- ? i18n`must be 1 or greater`
+ 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`required` : 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`required`
+ ? i18n.str`required`
: undefined,
address: undefinedIfEmpty({
address_lines:
value.address?.address_lines && value.address?.address_lines.length > 7
- ? i18n`max 7 lines`
+ ? i18n.str`max 7 lines`
: undefined,
}),
jurisdiction: undefinedIfEmpty({
address_lines:
value.address?.address_lines && value.address?.address_lines.length > 7
- ? i18n`max 7 lines`
+ ? i18n.str`max 7 lines`
: undefined,
}),
};
const hasErrors = Object.keys(errors).some(
- (k) => (errors as any)[k] !== undefined
+ (k) => (errors as any)[k] !== undefined,
);
+
const submit = async (): Promise<void> => {
- await onUpdate(value as Entity);
+ const { default_pay_delay, default_wire_transfer_delay, ...rest } = value as Required<Entity>;
+ const result: TalerMerchantApi.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);
+ // const [active, setActive] = useState(false);
return (
<div>
@@ -160,56 +124,14 @@ export function UpdatePage({
<div class="level-left">
<div class="level-item">
<span class="is-size-4">
- <Translate>Instance id</Translate>: <b>{id}</b>
+ <i18n.Translate>Instance id</i18n.Translate>: <b>{state.instance}</b>
</span>
</div>
</div>
- <div class="level-right">
- <div class="level-item">
- <h1 class="title">
- <button
- class="button is-danger"
- data-tooltip={i18n`Change the authorization method use for this instance.`}
- onClick={(): void => {
- setActive(!active);
- }}
- >
- <div class="icon is-left">
- <i class="mdi mdi-lock-reset" />
- </div>
- <span>
- <Translate>Manage access token</Translate>
- </span>
- </button>
- </h1>
- </div>
- </div>
</div>
</div>
</section>
- <div class="columns">
- <div class="column" />
- <div class="column is-four-fifths">
- {active && (
- <UpdateTokenModal
- oldToken={currentTokenValue}
- onCancel={() => {
- setActive(false);
- }}
- onClear={() => {
- updateToken(null);
- setActive(false);
- }}
- onConfirm={(newToken) => {
- updateToken(newToken);
- setActive(false);
- }}
- />
- )}
- </div>
- <div class="column" />
- </div>
<hr />
<div class="columns">
@@ -229,19 +151,19 @@ export function UpdatePage({
onClick={onBack}
data-tooltip="cancel operation"
>
- <Translate>Cancel</Translate>
+ <i18n.Translate>Cancel</i18n.Translate>
</button>
<AsyncButton
onClick={submit}
data-tooltip={
hasErrors
- ? i18n`Need to complete marked fields`
+ ? i18n.str`Need to complete marked fields`
: "confirm operation"
}
disabled={hasErrors}
>
- <Translate>Confirm</Translate>
+ <i18n.Translate>Confirm</i18n.Translate>
</AsyncButton>
</div>
</div>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx
index c0780fadf..9da7f7efb 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,69 +13,80 @@
You should have received 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 { Fragment, h, VNode } from "preact";
+import { HttpStatusCode, TalerError, TalerMerchantApi, TalerMerchantInstanceHttpClient, TalerMerchantManagementResultByMethod, assertUnreachable } from "@gnu-taler/taler-util";
+import {
+ useTranslationContext
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
+import { ErrorLoadingMerchant } from "../../../components/ErrorLoadingMerchant.js";
import { Loading } from "../../../components/exception/loading.js";
import { NotificationCard } from "../../../components/menu/index.js";
-import { useInstanceContext } from "../../../context/instance.js";
-import { MerchantBackend } from "../../../declaration.js";
-import { HttpError, HttpResponse } from "../../../hooks/backend.js";
+import { useSessionContext } from "../../../context/session.js";
import {
- useInstanceAPI,
useInstanceDetails,
useManagedInstanceDetails,
- useManagementAPI,
} from "../../../hooks/instance.js";
-import { useTranslator } from "../../../i18n/index.js";
import { Notification } from "../../../utils/types.js";
+import { LoginPage } from "../../login/index.js";
+import { NotFoundPageOrAdminCreate } from "../../notfound/index.js";
import { UpdatePage } from "./UpdatePage.js";
export interface Props {
onBack: () => void;
onConfirm: () => void;
- onUnauthorized: () => VNode;
- onNotFound: () => VNode;
- onLoadError: (e: HttpError) => VNode;
- onUpdateError: (e: HttpError) => void;
+ // onUnauthorized: () => VNode;
+ // onNotFound: () => VNode;
+ // onLoadError: (e: HttpError<TalerErrorDetail>) => VNode;
+ // onUpdateError: (e: HttpError<TalerErrorDetail>) => void;
}
export default function Update(props: Props): VNode {
- const { updateInstance, clearToken, setNewToken } = useInstanceAPI();
+ const { lib } = useSessionContext();
+ const updateInstance = lib.instance.updateCurrentInstance.bind(lib.instance)
const result = useInstanceDetails();
- return CommonUpdate(props, result, updateInstance, clearToken, setNewToken);
+ return CommonUpdate(props, result, updateInstance,);
}
export function AdminUpdate(props: Props & { instanceId: string }): VNode {
- const { updateInstance, clearToken, setNewToken } = useManagementAPI(
- props.instanceId
- );
+ const { lib } = useSessionContext();
+ const t = lib.subInstanceApi(props.instanceId).instance;
+ const updateInstance = t.updateCurrentInstance.bind(t)
const result = useManagedInstanceDetails(props.instanceId);
- return CommonUpdate(props, result, updateInstance, clearToken, setNewToken);
+ return CommonUpdate(props, result, updateInstance,);
}
+
function CommonUpdate(
{
onBack,
onConfirm,
- onLoadError,
- onNotFound,
- onUpdateError,
- onUnauthorized,
}: Props,
- result: HttpResponse<MerchantBackend.Instances.QueryInstancesResponse>,
- updateInstance: any,
- clearToken: any,
- setNewToken: any
+ result: TalerMerchantManagementResultByMethod<"getInstanceDetails"> | TalerError | undefined,
+ updateInstance: typeof TalerMerchantInstanceHttpClient.prototype.updateCurrentInstance,
): VNode {
- const { changeToken } = useInstanceContext();
const [notif, setNotif] = useState<Notification | undefined>(undefined);
- const i18n = useTranslator();
+ const { i18n } = useTranslationContext();
+ const { state } = useSessionContext();
- if (result.clientError && result.isUnauthorized) return onUnauthorized();
- if (result.clientError && result.isNotfound) return onNotFound();
- if (result.loading) return <Loading />;
- if (!result.ok) return onLoadError(result);
+ if (!result) return <Loading />
+ if (result instanceof TalerError) {
+ return <ErrorLoadingMerchant error={result} />
+ }
+ if (result.type === "fail") {
+ switch(result.case) {
+ case HttpStatusCode.Unauthorized: {
+ return <LoginPage />
+ }
+ case HttpStatusCode.NotFound: {
+ return <NotFoundPageOrAdminCreate />;
+ }
+ default: {
+ assertUnreachable(result)
+ }
+ }
+ }
return (
<Fragment>
@@ -83,30 +94,23 @@ function CommonUpdate(
<UpdatePage
onBack={onBack}
isLoading={false}
- selected={result.data}
+ selected={result.body}
onUpdate={(
- d: MerchantBackend.Instances.InstanceReconfigurationMessage
+ d: TalerMerchantApi.InstanceReconfigurationMessage,
): Promise<void> => {
- return updateInstance(d)
+ if (state.status !== "loggedIn") {
+ return Promise.resolve();
+ }
+ return updateInstance(state.token, d)
.then(onConfirm)
.catch((error: Error) =>
setNotif({
- message: i18n`Failed to create instance`,
+ message: i18n.str`Failed to update instance`,
type: "ERROR",
description: error.message,
- })
+ }),
);
}}
- onChangeAuth={(
- d: MerchantBackend.Instances.InstanceAuthConfigurationMessage
- ): Promise<void> => {
- const apiCall =
- d.method === "external" ? clearToken() : setNewToken(d.token!);
- return apiCall
- .then(() => changeToken(d.token))
- .then(onConfirm)
- .catch(onUpdateError);
- }}
/>
</Fragment>
);
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/Create.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/Create.stories.tsx
new file mode 100644
index 000000000..2e2a58b33
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/Create.stories.tsx
@@ -0,0 +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)
+ */
+
+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/merchant-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx
new file mode 100644
index 000000000..8792aabea
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx
@@ -0,0 +1,179 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { 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 { TalerMerchantApi } from "@gnu-taler/taler-util";
+
+type Entity = TalerMerchantApi.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/merchant-backoffice-ui/src/paths/instance/webhooks/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/index.tsx
new file mode 100644
index 000000000..70f246ff1
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/index.tsx
@@ -0,0 +1,62 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { 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 { NotificationCard } from "../../../../components/menu/index.js";
+import { useSessionContext } from "../../../../context/session.js";
+import { Notification } from "../../../../utils/types.js";
+import { CreatePage } from "./CreatePage.js";
+
+export type Entity = TalerMerchantApi.WebhookAddDetails;
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+}
+
+export default function CreateWebhook({ onConfirm, onBack }: Props): VNode {
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+ const { lib } = useSessionContext();
+ const { state } = useSessionContext();
+
+ return (
+ <>
+ <NotificationCard notification={notif} />
+ <CreatePage
+ onBack={onBack}
+ onCreate={(request: TalerMerchantApi.WebhookAddDetails) => {
+ return lib.instance.addWebhook(state.token, request)
+ .then(() => onConfirm())
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not inform template`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/List.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/List.stories.tsx
new file mode 100644
index 000000000..707324d40
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/List.stories.tsx
@@ -0,0 +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)
+ */
+
+import { FunctionalComponent, h } from "preact";
+import { ListPage as TestedComponent } from "./ListPage.js";
+
+export default {
+ title: "Pages/Templates/List",
+ component: TestedComponent,
+};
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/ListPage.tsx
new file mode 100644
index 000000000..3f1feb8e9
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/ListPage.tsx
@@ -0,0 +1,62 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode } from "preact";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { CardTable } from "./Table.js";
+import { TalerMerchantApi } from "@gnu-taler/taler-util";
+
+export interface Props {
+ webhooks: TalerMerchantApi.WebhookEntry[];
+ onLoadMoreBefore?: () => void;
+ onLoadMoreAfter?: () => void;
+ onCreate: () => void;
+ onDelete: (e: TalerMerchantApi.WebhookEntry) => void;
+ onSelect: (e: TalerMerchantApi.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}
+ onLoadMoreAfter={onLoadMoreAfter}
+ />
+ </section>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx
new file mode 100644
index 000000000..919285e78
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx
@@ -0,0 +1,203 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { TalerMerchantApi } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { StateUpdater, useState } from "preact/hooks";
+
+type Entity = TalerMerchantApi.WebhookEntry;
+
+interface Props {
+ webhooks: Entity[];
+ onDelete: (e: Entity) => void;
+ onSelect: (e: Entity) => void;
+ onCreate: () => void;
+ onLoadMoreBefore?: () => void;
+ onLoadMoreAfter?: () => void;
+}
+
+export function CardTable({
+ webhooks,
+ onCreate,
+ onDelete,
+ onSelect,
+ onLoadMoreAfter,
+ onLoadMoreBefore,
+}: 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}
+ />
+ ) : (
+ <EmptyTable />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+}
+interface TableProps {
+ rowSelection: string[];
+ instances: Entity[];
+ onDelete: (e: Entity) => void;
+ onSelect: (e: Entity) => void;
+ rowSelectionHandler: StateUpdater<string[]>;
+ onLoadMoreBefore?: () => void;
+ onLoadMoreAfter?: () => void;
+}
+
+function Table({
+ instances,
+ onLoadMoreAfter,
+ onDelete,
+ onSelect,
+ onLoadMoreBefore,
+}: TableProps): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="table-container">
+ {onLoadMoreBefore && (
+ <button
+ class="button is-fullwidth"
+ data-tooltip={i18n.str`load more webhooks before the first one`}
+ onClick={onLoadMoreBefore}
+ >
+ <i18n.Translate>load first page</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>
+ {onLoadMoreAfter && (
+ <button
+ class="button is-fullwidth"
+ data-tooltip={i18n.str`load more webhooks after the last one`}
+ onClick={onLoadMoreAfter}
+ >
+ <i18n.Translate>load next page</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-magnify 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/merchant-backoffice-ui/src/paths/instance/webhooks/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/index.tsx
new file mode 100644
index 000000000..789b8d73b
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/index.tsx
@@ -0,0 +1,105 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import {
+ HttpStatusCode,
+ TalerError,
+ TalerMerchantApi,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
+import {
+ useTranslationContext
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { useSessionContext } from "../../../../context/session.js";
+import { useInstanceWebhooks } from "../../../../hooks/webhooks.js";
+import { Notification } from "../../../../utils/types.js";
+import { LoginPage } from "../../../login/index.js";
+import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
+import { ListPage } from "./ListPage.js";
+
+interface Props {
+ onCreate: () => void;
+ onSelect: (id: string) => void;
+}
+
+export default function ListWebhooks({ onCreate, onSelect }: Props): VNode {
+ const { i18n } = useTranslationContext();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { lib } = useSessionContext();
+ const { state } = useSessionContext();
+ const result = useInstanceWebhooks();
+
+ if (!result) return <Loading />;
+ if (result instanceof TalerError) {
+ return <ErrorLoadingMerchant error={result} />;
+ }
+ if (result.type === "fail") {
+ switch (result.case) {
+ case HttpStatusCode.NotFound: {
+ return <NotFoundPageOrAdminCreate />;
+ }
+ case HttpStatusCode.Unauthorized: {
+ return <LoginPage />
+ }
+ default: {
+ assertUnreachable(result);
+ }
+ }
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+
+ <ListPage
+ webhooks={result.body.webhooks}
+ onLoadMoreBefore={undefined} //result.isFirstPage ? undefined : result.loadFirst}
+ onLoadMoreAfter={undefined} //result.isLastPage ? undefined : result.loadNext}
+ onCreate={onCreate}
+ onSelect={(e) => {
+ onSelect(e.webhook_id);
+ }}
+ onDelete={(e: TalerMerchantApi.WebhookEntry) => {
+ return lib.instance
+ .deleteWebhook(state.token, 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/merchant-backoffice-ui/src/paths/instance/webhooks/update/Update.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/Update.stories.tsx
new file mode 100644
index 000000000..303d17b72
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/Update.stories.tsx
@@ -0,0 +1,32 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { 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/merchant-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx
new file mode 100644
index 000000000..6aca62582
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx
@@ -0,0 +1,145 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { TalerMerchantApi } from "@gnu-taler/taler-util";
+
+type Entity = TalerMerchantApi.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/merchant-backoffice-ui/src/paths/instance/webhooks/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/index.tsx
new file mode 100644
index 000000000..5b2ba7bb9
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/index.tsx
@@ -0,0 +1,97 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util";
+import {
+ useTranslationContext
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { useSessionContext } from "../../../../context/session.js";
+import {
+ useWebhookDetails,
+} from "../../../../hooks/webhooks.js";
+import { Notification } from "../../../../utils/types.js";
+import { LoginPage } from "../../../login/index.js";
+import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
+import { UpdatePage } from "./UpdatePage.js";
+
+export type Entity = TalerMerchantApi.WebhookPatchDetails & WithId;
+
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+ tid: string;
+}
+export default function UpdateWebhook({
+ tid,
+ onConfirm,
+ onBack,
+}: Props): VNode {
+ const { lib } = useSessionContext();
+ const { state } = useSessionContext();
+ const result = useWebhookDetails(tid);
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+
+ const { i18n } = useTranslationContext();
+
+ if (!result) return <Loading />;
+ if (result instanceof TalerError) {
+ return <ErrorLoadingMerchant error={result} />;
+ }
+ if (result.type === "fail") {
+ switch (result.case) {
+ case HttpStatusCode.NotFound: {
+ return <NotFoundPageOrAdminCreate />;
+ }
+ case HttpStatusCode.Unauthorized: {
+ return <LoginPage />
+ }
+ default: {
+ assertUnreachable(result);
+ }
+ }
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+ <UpdatePage
+ webhook={{ ...result.body, id: tid }}
+ onBack={onBack}
+ onUpdate={(data) => {
+ return lib.instance.updateWebhook(state.token, tid, data)
+ .then(onConfirm)
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not update template`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/paths/login/index.tsx b/packages/merchant-backoffice-ui/src/paths/login/index.tsx
index ac75e094f..272c40b55 100644
--- a/packages/merchant-backoffice-ui/src/paths/login/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/login/index.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2021-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -14,16 +14,161 @@
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 { LoginModal } from "../../components/exception/login.js";
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { HttpStatusCode, createAccessToken } from "@gnu-taler/taler-util";
+import {
+ useTranslationContext
+} from "@gnu-taler/web-util/browser";
+import { ComponentChildren, Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { NotificationCard } from "../../components/menu/index.js";
+import { useSessionContext } from "../../context/session.js";
+import { Notification } from "../../utils/types.js";
+
+interface Props {}
+
+const tokenRequest = {
+ scope: "write",
+ duration: {
+ d_us: "forever" as const,
+ },
+ refreshable: true,
+};
+
+export function LoginPage(_p: Props): VNode {
+ const [token, setToken] = useState("");
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { state, logIn } = useSessionContext();
+ const { lib } = useSessionContext();
+
+ const { i18n } = useTranslationContext();
+
+ async function doLoginImpl() {
+ const result = await lib.authenticate.createAccessTokenBearer(
+ createAccessToken(token),
+ tokenRequest,
+ );
+ if (result.type === "ok") {
+ const { token } = result.body;
+ logIn(token);
+ return;
+ } else {
+ switch (result.case) {
+ case HttpStatusCode.Unauthorized: {
+ setNotif({
+ message: "Your password is incorrect",
+ type: "ERROR",
+ });
+ return;
+ }
+ case HttpStatusCode.NotFound: {
+ setNotif({
+ message: "Your instance not found",
+ type: "ERROR",
+ });
+ return;
+ }
+ }
+ }
+ }
+
+ 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 for <b>"{state.instance}"</b>.
+ </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 ? doLoginImpl() : null
+ }
+ value={token}
+ onInput={(e): void => setToken(e?.currentTarget.value)}
+ />
+ </p>
+ </div>
+ </div>
+ </div>
+ </section>
+ <footer
+ class="modal-card-foot "
+ style={{
+ justifyContent: "space-between",
+ border: "1px solid",
+ borderTop: 0,
+ }}
+ >
+ <div />
+ <AsyncButton type="is-info" onClick={doLoginImpl}>
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </footer>
+ </div>
+ </div>
+ </div>
+ </Fragment>
+ );
+}
-interface Props {
- onConfirm: (url: string, token?: string) => void;
+function AsyncButton({
+ onClick,
+ disabled,
+ type = "",
+ children,
+}: {
+ type?: string;
+ disabled?: boolean;
+ onClick: () => Promise<void>;
+ children: ComponentChildren;
+}): VNode {
+ const [running, setRunning] = useState(false);
+ return (
+ <button
+ class={"button " + type}
+ disabled={disabled || running}
+ onClick={() => {
+ setRunning(true);
+ onClick()
+ .then(() => {
+ setRunning(false);
+ })
+ .catch(() => {
+ setRunning(false);
+ });
+ }}
+ >
+ {children}
+ </button>
+ );
}
-export default function LoginPage({ onConfirm }: Props): VNode {
- return <LoginModal onConfirm={onConfirm} />
-} \ No newline at end of file
diff --git a/packages/merchant-backoffice-ui/src/paths/notfound/index.tsx b/packages/merchant-backoffice-ui/src/paths/notfound/index.tsx
index 10c3fac25..4d348c02b 100644
--- a/packages/merchant-backoffice-ui/src/paths/notfound/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/notfound/index.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2021-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -14,23 +14,59 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- /**
+/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { h, VNode } from 'preact';
-import { Link } from 'preact-router/match';
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { Link, route } from "preact-router";
+import { NotificationCard } from "../../components/menu/index.js";
+import {
+ cleanAllCache,
+ DEFAULT_ADMIN_USERNAME,
+ useSessionContext,
+} from "../../context/session.js";
+import InstanceCreatePage from "../../paths/admin/create/index.js";
+import { InstancePaths } from "../../Routing.js";
-export default function NotFoundPage(): VNode {
+export function NotFoundPage(): VNode {
+ return (
+ <div>
+ <p>That page doesn&apos;t exist.</p>
+ <Link href="/">
+ <h4>Back to Home</h4>
+ </Link>
+ </div>
+ );
+}
+
+export function NotFoundPageOrAdminCreate(): VNode {
+ const { state } = useSessionContext();
+ const { i18n } = useTranslationContext();
+ if (state.isAdmin && state.instance === DEFAULT_ADMIN_USERNAME) {
return (
- <div>
- <h1>Error 404</h1>
- <p>That page doesn&apos;t exist.</p>
- <Link href="/">
- <h4>Back to Home</h4>
- </Link>
- </div>
+ <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={() => {
+ // we need to clear everything since we take some
+ // 404 as "default instance don't exist"
+ cleanAllCache()
+ route(InstancePaths.bank_list);
+ }}
+ />
+ </Fragment>
);
-}
+ }
+ return <NotFoundPage />
+}
diff --git a/packages/merchant-backoffice-ui/src/paths/settings/index.tsx b/packages/merchant-backoffice-ui/src/paths/settings/index.tsx
new file mode 100644
index 000000000..0c4b9dd1a
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/settings/index.tsx
@@ -0,0 +1,141 @@
+/*
+ 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 { useLang, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../components/form/FormProvider.js";
+import { InputSelector } from "../../components/form/InputSelector.js";
+import { InputToggle } from "../../components/form/InputToggle.js";
+import { LangSelector } from "../../components/menu/LangSelector.js";
+import { Preferences, usePreference } from "../../hooks/preference.js";
+import { AbsoluteTime } from "@gnu-taler/taler-util";
+
+function getBrowserLang(): string | undefined {
+ if (typeof window === "undefined") return undefined;
+ if (window.navigator.languages) return window.navigator.languages[0];
+ if (window.navigator.language) return window.navigator.language;
+ return undefined;
+}
+
+export function Settings({ onClose }: { onClose?: () => void }): VNode {
+ const { i18n } = useTranslationContext();
+ const borwserLang = getBrowserLang();
+ const { update } = useLang(undefined, {});
+
+ const [value, , updateValue] = usePreference();
+ const errors: FormErrors<Preferences> = {};
+
+ function valueHandler(s: (d: Partial<Preferences>) => Partial<Preferences>): void {
+ const next = s(value);
+ const v: Preferences = {
+ advanceOrderMode: next.advanceOrderMode ?? false,
+ hideMissingAccountUntil: next.hideMissingAccountUntil ?? AbsoluteTime.never(),
+ hideKycUntil: next.hideKycUntil ?? AbsoluteTime.never(),
+ dateFormat: next.dateFormat ?? "ymd",
+ };
+ updateValue(v);
+ }
+
+ return (
+ <div>
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <div>
+ <FormProvider<Preferences>
+ name="settings"
+ errors={errors}
+ object={value}
+ valueHandler={valueHandler}
+ >
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ <i18n.Translate>Language</i18n.Translate>
+ <span
+ class="icon has-tooltip-right"
+ data-tooltip={
+ "Force language setting instance of taking the browser"
+ }
+ >
+ <i class="mdi mdi-information" />
+ </span>
+ </label>
+ </div>
+ <div class="field field-body has-addons is-flex-grow-3">
+ <LangSelector />
+ &nbsp;
+ {borwserLang !== undefined && (
+ <button
+ data-tooltip={i18n.str`generate random secret key`}
+ class="button is-info mr-2"
+ onClick={(e) => {
+ update(borwserLang.substring(0, 2));
+ e.preventDefault()
+ }}
+ >
+ <i18n.Translate>Set default</i18n.Translate>
+ </button>
+ )}
+ </div>
+ </div>
+ <InputToggle<Preferences>
+ label={i18n.str`Advance order creation`}
+ tooltip={i18n.str`Shows more options in the order creation form`}
+ name="advanceOrderMode"
+ />
+ <InputSelector<Preferences>
+ name="dateFormat"
+ label={i18n.str`Date format`}
+ expand={true}
+ help={
+ value.dateFormat === "dmy"
+ ? "31/12/2001"
+ : value.dateFormat === "mdy"
+ ? "12/31/2001"
+ : value.dateFormat === "ymd"
+ ? "2001/12/31"
+ : ""
+ }
+ toStr={(e) => {
+ if (e === "ymd") return "year month day";
+ if (e === "mdy") return "month day year";
+ if (e === "dmy") return "day month year";
+ return "choose one";
+ }}
+ values={["ymd", "mdy", "dmy"]}
+ tooltip={i18n.str`how the date is going to be displayed`}
+ />
+ </FormProvider>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ {onClose && (
+ <section class="section is-main-section">
+ <button class="button" onClick={onClose}>
+ <i18n.Translate>Close</i18n.Translate>
+ </button>
+ </section>
+ )}
+ </div>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/schemas/index.ts b/packages/merchant-backoffice-ui/src/schemas/index.ts
index 0f705c555..693894ae0 100644
--- a/packages/merchant-backoffice-ui/src/schemas/index.ts
+++ b/packages/merchant-backoffice-ui/src/schemas/index.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,188 +15,210 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { isAfter, isFuture } from 'date-fns';
-import * as yup from 'yup';
-import { AMOUNT_REGEX, PAYTO_REGEX } from "../utils/constants.js";
+import { Amounts } from "@gnu-taler/taler-util";
+import { isAfter, isFuture } from "date-fns";
+import * as yup from "yup";
+import { PAYTO_REGEX } from "../utils/constants.js";
yup.setLocale({
mixed: {
- default: 'field_invalid',
+ default: "field_invalid",
},
number: {
- min: ({ min }: any) => ({ key: 'field_too_short', values: { min } }),
- max: ({ max }: any) => ({ key: 'field_too_big', values: { max } }),
+ min: ({ min }: any) => ({ key: "field_too_short", values: { min } }),
+ max: ({ max }: any) => ({ key: "field_too_big", values: { max } }),
},
});
function listOfPayToUrisAreValid(values?: (string | undefined)[]): boolean {
- return !!values && values.every(v => v && PAYTO_REGEX.test(v));
+ return !!values && values.every((v) => v && PAYTO_REGEX.test(v));
}
function currencyWithAmountIsValid(value?: string): boolean {
- return !!value && AMOUNT_REGEX.test(value)
+ return !!value && Amounts.parse(value) !== undefined;
}
function currencyGreaterThan0(value?: string) {
if (value) {
try {
- const [, amount] = value.split(':')
- const intAmount = parseInt(amount, 10)
- return intAmount > 0
+ const [, amount] = value.split(":");
+ const intAmount = parseInt(amount, 10);
+ return intAmount > 0;
} catch {
- return false
+ return false;
}
}
- return true
+ return true;
}
export const InstanceSchema = yup.object().shape({
- id: yup.string().required().meta({ type: 'url' }),
+ id: yup.string().required().meta({ type: "url" }),
name: yup.string().required(),
auth: yup.object().shape({
method: yup.string().matches(/^(external|token)$/),
token: yup.string().optional().nullable(),
}),
- payto_uris: yup.array().of(yup.string())
+ payto_uris: yup
+ .array()
+ .of(yup.string())
.min(1)
- .meta({ type: 'array' })
- .test('payto', '{path} is not valid', listOfPayToUrisAreValid),
- default_max_deposit_fee: yup.string()
+ .meta({ type: "array" })
+ .test("payto", "{path} is not valid", listOfPayToUrisAreValid),
+ default_max_deposit_fee: yup
+ .string()
.required()
- .test('amount', 'the amount is not valid', currencyWithAmountIsValid)
- .meta({ type: 'amount' }),
- default_max_wire_fee: yup.string()
+ .test("amount", "the amount is not valid", currencyWithAmountIsValid)
+ .meta({ type: "amount" }),
+ default_max_wire_fee: yup
+ .string()
.required()
- .test('amount', '{path} is not valid', currencyWithAmountIsValid)
- .meta({ type: 'amount' }),
- default_wire_fee_amortization: yup.number()
- .required(),
- address: yup.object().shape({
- country: yup.string().optional(),
- address_lines: yup.array().of(yup.string()).max(7).optional(),
- building_number: yup.string().optional(),
- building_name: yup.string().optional(),
- street: yup.string().optional(),
- post_code: yup.string().optional(),
- town_location: yup.string().optional(),
- town: yup.string(),
- district: yup.string().optional(),
- country_subdivision: yup.string().optional(),
- }).meta({ type: 'group' }),
- jurisdiction: yup.object().shape({
- country: yup.string().optional(),
- address_lines: yup.array().of(yup.string()).max(7).optional(),
- building_number: yup.string().optional(),
- building_name: yup.string().optional(),
- street: yup.string().optional(),
- post_code: yup.string().optional(),
- town_location: yup.string().optional(),
- town: yup.string(),
- district: yup.string().optional(),
- country_subdivision: yup.string().optional(),
- }).meta({ type: 'group' }),
+ .test("amount", "{path} is not valid", currencyWithAmountIsValid)
+ .meta({ type: "amount" }),
+ default_wire_fee_amortization: yup.number().required(),
+ address: yup
+ .object()
+ .shape({
+ country: yup.string().optional(),
+ address_lines: yup.array().of(yup.string()).max(7).optional(),
+ building_number: yup.string().optional(),
+ building_name: yup.string().optional(),
+ street: yup.string().optional(),
+ post_code: yup.string().optional(),
+ town_location: yup.string().optional(),
+ town: yup.string(),
+ district: yup.string().optional(),
+ country_subdivision: yup.string().optional(),
+ })
+ .meta({ type: "group" }),
+ jurisdiction: yup
+ .object()
+ .shape({
+ country: yup.string().optional(),
+ address_lines: yup.array().of(yup.string()).max(7).optional(),
+ building_number: yup.string().optional(),
+ building_name: yup.string().optional(),
+ street: yup.string().optional(),
+ post_code: yup.string().optional(),
+ town_location: yup.string().optional(),
+ town: yup.string(),
+ district: yup.string().optional(),
+ country_subdivision: yup.string().optional(),
+ })
+ .meta({ type: "group" }),
// default_pay_delay: yup.object()
// .shape({ d_us: yup.number() })
// .required()
// .meta({ type: 'duration' }),
// .transform(numberToDuration),
- default_wire_transfer_delay: yup.object()
+ default_wire_transfer_delay: yup
+ .object()
.shape({ d_us: yup.number() })
.required()
- .meta({ type: 'duration' }),
+ .meta({ type: "duration" }),
// .transform(numberToDuration),
-})
+});
-export const InstanceUpdateSchema = InstanceSchema.clone().omit(['id']);
+export const InstanceUpdateSchema = InstanceSchema.clone().omit(["id"]);
export const InstanceCreateSchema = InstanceSchema.clone();
-export const AuthorizeTipSchema = yup.object().shape({
- justification: yup.string().required(),
- amount: yup.string()
- .required()
- .test('amount', 'the amount is not valid', currencyWithAmountIsValid)
- .test('amount_positive', 'the amount is not valid', currencyGreaterThan0),
- next_url: yup.string().required(),
-})
-
-const stringIsValidJSON = (value?: string) => {
- const p = value?.trim()
- if (!p) return true;
- try {
- JSON.parse(p)
- return true
- } catch {
- return false
- }
-}
-
export const OrderCreateSchema = yup.object().shape({
- pricing: yup.object().required().shape({
- summary: yup.string().ensure().required(),
- order_price: yup.string()
- .ensure()
- .required()
- .test('amount', 'the amount is not valid', currencyWithAmountIsValid)
- .test('amount_positive', 'the amount should be greater than 0', currencyGreaterThan0),
- }),
- extra: yup.string().test('extra', 'is not a JSON format', stringIsValidJSON),
- payments: yup.object().required().shape({
- refund_deadline: yup.date()
- .test('future', 'should be in the future', (d) => d ? isFuture(d) : true),
- pay_deadline: yup.date()
- .test('future', 'should be in the future', (d) => d ? isFuture(d) : true),
- auto_refund_deadline: yup.date()
- .test('future', 'should be in the future', (d) => d ? isFuture(d) : true),
- delivery_date: yup.date()
- .test('future', 'should be in the future', (d) => d ? isFuture(d) : true),
- }).test('payment', 'dates', (d) => {
- if (d.pay_deadline && d.refund_deadline && isAfter(d.refund_deadline, d.pay_deadline)) {
- return new yup.ValidationError('pay deadline should be greater than refund', 'asd', 'payments.pay_deadline')
- }
- return true
- })
-})
+ pricing: yup
+ .object()
+ .required()
+ .shape({
+ summary: yup.string().ensure().required(),
+ order_price: yup
+ .string()
+ .ensure()
+ .required()
+ .test("amount", "the amount is not valid", currencyWithAmountIsValid)
+ .test(
+ "amount_positive",
+ "the amount should be greater than 0",
+ currencyGreaterThan0,
+ ),
+ }),
+ // extra: yup.object().test("extra", "is not a JSON format", stringIsValidJSON),
+ payments: yup
+ .object()
+ .required()
+ .shape({
+ refund_deadline: yup
+ .date()
+ .test("future", "should be in the future", (d) =>
+ d ? isFuture(d) : true,
+ ),
+ pay_deadline: yup
+ .date()
+ .test("future", "should be in the future", (d) =>
+ d ? isFuture(d) : true,
+ ),
+ auto_refund_deadline: yup
+ .date()
+ .test("future", "should be in the future", (d) =>
+ d ? isFuture(d) : true,
+ ),
+ delivery_date: yup
+ .date()
+ .test("future", "should be in the future", (d) =>
+ d ? isFuture(d) : true,
+ ),
+ })
+ .test("payment", "dates", (d) => {
+ if (
+ d.pay_deadline &&
+ d.refund_deadline &&
+ isAfter(d.refund_deadline, d.pay_deadline)
+ ) {
+ return new yup.ValidationError(
+ "pay deadline should be greater than refund",
+ "asd",
+ "payments.pay_deadline",
+ );
+ }
+ return true;
+ }),
+});
export const ProductCreateSchema = yup.object().shape({
product_id: yup.string().ensure().required(),
description: yup.string().required(),
unit: yup.string().ensure().required(),
- price: yup.string()
+ price: yup
+ .string()
.required()
- .test('amount', 'the amount is not valid', currencyWithAmountIsValid),
- stock: yup.object({
-
- }).optional(),
+ .test("amount", "the amount is not valid", currencyWithAmountIsValid),
+ stock: yup.object({}).optional(),
minimum_age: yup.number().optional().min(0),
-})
+});
export const ProductUpdateSchema = yup.object().shape({
description: yup.string().required(),
- price: yup.string()
+ price: yup
+ .string()
.required()
- .test('amount', 'the amount is not valid', currencyWithAmountIsValid),
- stock: yup.object({
-
- }).optional(),
+ .test("amount", "the amount is not valid", currencyWithAmountIsValid),
+ stock: yup.object({}).optional(),
minimum_age: yup.number().optional().min(0),
-})
-
+});
export const TaxSchema = yup.object().shape({
name: yup.string().required().ensure(),
- tax: yup.string()
+ tax: yup
+ .string()
.required()
- .test('amount', 'the amount is not valid', currencyWithAmountIsValid),
-})
+ .test("amount", "the amount is not valid", currencyWithAmountIsValid),
+});
export const NonInventoryProductSchema = yup.object().shape({
quantity: yup.number().required().positive(),
description: yup.string().required(),
unit: yup.string().ensure().required(),
- price: yup.string()
+ price: yup
+ .string()
.required()
- .test('amount', 'the amount is not valid', currencyWithAmountIsValid),
-})
+ .test("amount", "the amount is not valid", currencyWithAmountIsValid),
+});
diff --git a/packages/merchant-backoffice-ui/src/scss/DurationPicker.scss b/packages/merchant-backoffice-ui/src/scss/DurationPicker.scss
index a35575324..aa75b9916 100644
--- a/packages/merchant-backoffice-ui/src/scss/DurationPicker.scss
+++ b/packages/merchant-backoffice-ui/src/scss/DurationPicker.scss
@@ -1,4 +1,3 @@
-
.rdp-picker {
display: flex;
height: 175px;
diff --git a/packages/merchant-backoffice-ui/src/scss/_aside.scss b/packages/merchant-backoffice-ui/src/scss/_aside.scss
index 22258acf8..b7b59516b 100644
--- a/packages/merchant-backoffice-ui/src/scss/_aside.scss
+++ b/packages/merchant-backoffice-ui/src/scss/_aside.scss
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2021-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- /**
+/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
@@ -23,7 +23,8 @@
html {
&.has-aside-left {
&.has-aside-expanded {
- nav.navbar, body {
+ nav.navbar,
+ body {
padding-left: $aside-width;
}
}
@@ -71,11 +72,11 @@ aside.aside {
color: $aside-tools-color;
line-height: $navbar-height;
height: $navbar-height;
- padding-left: $default-padding * .5;
+ padding-left: $default-padding * 0.5;
flex: 1;
.icon {
- margin-right: $default-padding * .5;
+ margin-right: $default-padding * 0.5;
}
}
@@ -88,7 +89,7 @@ aside.aside {
.dropdown-icon {
position: absolute;
- top: $size-base * .5;
+ top: $size-base * 0.5;
right: 0;
}
}
@@ -98,11 +99,12 @@ aside.aside {
border-left: 0;
background-color: darken($base-color, 2.5%);
padding-left: 0;
- margin: 0 0 $default-padding * .5;
+ margin: 0 0 $default-padding * 0.5;
li {
a {
- padding: $default-padding * .5 0 $default-padding * .5 $default-padding * .5;
+ padding: $default-padding * 0.5 0 $default-padding * 0.5
+ $default-padding * 0.5;
font-size: $aside-submenu-font-size;
&.has-icon {
@@ -120,15 +122,14 @@ aside.aside {
}
.menu-label {
- padding: 0 $default-padding * .5;
- margin-top: $default-padding * .5;
- margin-bottom: $default-padding * .5;
+ padding: 0 $default-padding * 0.5;
+ margin-top: $default-padding * 0.5;
+ margin-bottom: $default-padding * 0.5;
}
-
}
@include touch {
- nav.navbar {
+ nav.navbar {
@include transition(margin-left);
}
aside.aside {
@@ -138,7 +139,8 @@ aside.aside {
body {
overflow-x: hidden;
}
- body, nav.navbar {
+ body,
+ nav.navbar {
width: 100vw;
}
aside.aside {
@@ -148,7 +150,7 @@ aside.aside {
.image {
img {
- max-width: $aside-mobile-width * .33;
+ max-width: $aside-mobile-width * 0.33;
}
}
diff --git a/packages/merchant-backoffice-ui/src/scss/_card.scss b/packages/merchant-backoffice-ui/src/scss/_card.scss
index b2eec27a1..a4118400f 100644
--- a/packages/merchant-backoffice-ui/src/scss/_card.scss
+++ b/packages/merchant-backoffice-ui/src/scss/_card.scss
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2021-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- /**
+/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
@@ -39,7 +39,7 @@
&.is-card-widget {
.card-content {
- padding: $default-padding * .5;
+ padding: $default-padding * 0.5;
}
}
diff --git a/packages/merchant-backoffice-ui/src/scss/_custom-calendar.scss b/packages/merchant-backoffice-ui/src/scss/_custom-calendar.scss
index 9ac877ce0..62414a00a 100644
--- a/packages/merchant-backoffice-ui/src/scss/_custom-calendar.scss
+++ b/packages/merchant-backoffice-ui/src/scss/_custom-calendar.scss
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,31 +16,30 @@
:root {
--primary-color: #3298dc;
-
- --primary-text-color-dark: rgba(0,0,0,.87);
- --secondary-text-color-dark: rgba(0,0,0,.57);
- --disabled-text-color-dark: rgba(0,0,0,.13);
-
- --primary-text-color-light: rgba(255,255,255,.87);
- --secondary-text-color-light: rgba(255,255,255,.57);
- --disabled-text-color-light: rgba(255,255,255,.13);
-
- --font-stack: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif;
-
+
+ --primary-text-color-dark: rgba(0, 0, 0, 0.87);
+ --secondary-text-color-dark: rgba(0, 0, 0, 0.57);
+ --disabled-text-color-dark: rgba(0, 0, 0, 0.13);
+
+ --primary-text-color-light: rgba(255, 255, 255, 0.87);
+ --secondary-text-color-light: rgba(255, 255, 255, 0.57);
+ --disabled-text-color-light: rgba(255, 255, 255, 0.13);
+
+ --font-stack: "Roboto", "Helvetica Neue", Helvetica, Arial, sans-serif;
+
--primary-card-color: #fff;
--primary-background-color: #f2f2f2;
-
+
--box-shadow-lvl-1: 0 1px 3px rgba(0, 0, 0, 0.12),
- 0 1px 2px rgba(0, 0, 0, 0.24);
+ 0 1px 2px rgba(0, 0, 0, 0.24);
--box-shadow-lvl-2: 0 3px 6px rgba(0, 0, 0, 0.16),
- 0 3px 6px rgba(0, 0, 0, 0.23);
+ 0 3px 6px rgba(0, 0, 0, 0.23);
--box-shadow-lvl-3: 0 10px 20px rgba(0, 0, 0, 0.19),
- 0 6px 6px rgba(0, 0, 0, 0.23);
+ 0 6px 6px rgba(0, 0, 0, 0.23);
--box-shadow-lvl-4: 0 14px 28px rgba(0, 0, 0, 0.25),
- 0 10px 10px rgba(0, 0, 0, 0.22);
+ 0 10px 10px rgba(0, 0, 0, 0.22);
}
-
.datePicker {
text-align: left;
background: var(--primary-card-color);
@@ -52,7 +51,7 @@
width: 90vw;
max-width: 448px;
transform-origin: top left;
- transition: transform .22s ease-in-out, opacity .22s ease-in-out;
+ transition: transform 0.22s ease-in-out, opacity 0.22s ease-in-out;
top: 50%;
left: 50%;
opacity: 0;
@@ -63,7 +62,7 @@
opacity: 1;
transform: scale(1) translate(-50%, -50%);
}
-
+
.datePicker--titles {
border-top-left-radius: 3px;
border-top-right-radius: 3px;
@@ -71,7 +70,8 @@
height: 100px;
background: var(--primary-color);
- h2, h3 {
+ h2,
+ h3 {
cursor: pointer;
color: #fff;
line-height: 1;
@@ -81,7 +81,7 @@
}
h3 {
- color: rgba(255,255,255,.57);
+ color: rgba(255, 255, 255, 0.57);
font-size: 18px;
padding-bottom: 2px;
}
@@ -110,13 +110,13 @@
font-size: 26px;
user-select: none;
border-radius: 50%;
-
+
&:hover {
background: var(--disabled-text-color-dark);
}
}
}
-
+
.datePicker--scroll {
overflow-y: auto;
max-height: calc(90vh - 56px - 100px);
@@ -129,9 +129,11 @@
width: 100%;
display: grid;
text-align: center;
-
+
// there's probably a better way to do this, but wanted to try out CSS grid
- grid-template-columns: calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7);
+ grid-template-columns:
+ calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7)
+ calc(100% / 7) calc(100% / 7) calc(100% / 7);
span {
color: var(--secondary-text-color-dark);
@@ -145,14 +147,16 @@
width: 100%;
display: grid;
text-align: center;
- grid-template-columns: calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7);
+ grid-template-columns:
+ calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7)
+ calc(100% / 7) calc(100% / 7) calc(100% / 7);
span {
color: var(--primary-text-color-dark);
line-height: 42px;
font-size: 14px;
display: inline-grid;
- transition: color .22s;
+ transition: color 0.22s;
height: 42px;
position: relative;
cursor: pointer;
@@ -160,7 +164,7 @@
border-radius: 50%;
&::before {
- content: '';
+ content: "";
position: absolute;
z-index: -1;
height: 42px;
@@ -168,12 +172,12 @@
left: calc(50% - 21px);
background: var(--primary-color);
border-radius: 50%;
- transition: transform .22s, opacity .22s;
+ transition: transform 0.22s, opacity 0.22s;
transform: scale(0);
opacity: 0;
}
-
- &[disabled=true] {
+
+ &[disabled="true"] {
cursor: unset;
}
@@ -182,7 +186,7 @@
}
&.datePicker--selected {
- color: rgba(255,255,255,.87);
+ color: rgba(255, 255, 255, 0.87);
&:before {
transform: scale(1);
@@ -192,21 +196,21 @@
}
}
}
-
+
.datePicker--selectYear {
padding: 0 20px;
display: block;
width: 100%;
text-align: center;
max-height: 362px;
-
+
span {
display: block;
width: 100%;
font-size: 24px;
margin: 20px auto;
cursor: pointer;
-
+
&.selected {
font-size: 42px;
color: var(--primary-color);
@@ -232,9 +236,10 @@
appearance: none;
padding: 0 16px;
border-radius: 3px;
- transition: background-color .13s;
+ transition: background-color 0.13s;
- &:hover, &:focus {
+ &:hover,
+ &:focus {
outline: none;
background-color: var(--disabled-text-color-dark);
}
@@ -249,6 +254,6 @@
left: 0;
bottom: 0;
right: 0;
- background: rgba(0,0,0,.52);
- animation: fadeIn .22s forwards;
+ background: rgba(0, 0, 0, 0.52);
+ animation: fadeIn 0.22s forwards;
}
diff --git a/packages/merchant-backoffice-ui/src/scss/_footer.scss b/packages/merchant-backoffice-ui/src/scss/_footer.scss
index 027a5ca8b..7e90c40cc 100644
--- a/packages/merchant-backoffice-ui/src/scss/_footer.scss
+++ b/packages/merchant-backoffice-ui/src/scss/_footer.scss
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2021-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- /**
+/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
diff --git a/packages/merchant-backoffice-ui/src/scss/_form.scss b/packages/merchant-backoffice-ui/src/scss/_form.scss
index 71f0d4da4..126d3d0cc 100644
--- a/packages/merchant-backoffice-ui/src/scss/_form.scss
+++ b/packages/merchant-backoffice-ui/src/scss/_form.scss
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2021-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- /**
+/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
@@ -22,11 +22,12 @@
.field {
&.has-check {
.field-body {
- margin-top: $default-padding * .125;
+ margin-top: $default-padding * 0.125;
}
}
.control {
- .mdi-24px.mdi-set, .mdi-24px.mdi:before {
+ .mdi-24px.mdi-set,
+ .mdi-24px.mdi:before {
font-size: inherit;
}
}
@@ -37,28 +38,34 @@
}
}
-.input, .textarea, select {
+.input,
+.textarea,
+select {
box-shadow: none;
- &:focus, &:active {
- box-shadow: none!important;
+ &:focus,
+ &:active {
+ box-shadow: none !important;
}
}
-.switch input[type=checkbox]+.check:before {
+.switch input[type="checkbox"] + .check:before {
box-shadow: none;
}
-.switch, .b-checkbox.checkbox {
- input[type=checkbox] {
- &:focus + .check, &:focus:checked + .check {
- box-shadow: none!important;
+.switch,
+.b-checkbox.checkbox {
+ input[type="checkbox"] {
+ &:focus + .check,
+ &:focus:checked + .check {
+ box-shadow: none !important;
}
}
}
-.b-checkbox.checkbox input[type=checkbox], .b-radio.radio input[type=radio] {
- &+.check {
+.b-checkbox.checkbox input[type="checkbox"],
+.b-radio.radio input[type="radio"] {
+ & + .check {
border: $checkbox-border;
}
}
diff --git a/packages/merchant-backoffice-ui/src/scss/_hero-bar.scss b/packages/merchant-backoffice-ui/src/scss/_hero-bar.scss
index 90b67a2ed..cb3f438e9 100644
--- a/packages/merchant-backoffice-ui/src/scss/_hero-bar.scss
+++ b/packages/merchant-backoffice-ui/src/scss/_hero-bar.scss
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2021-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- /**
+/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
@@ -32,17 +32,17 @@ section.hero.is-hero-bar {
}
> div > .level {
- margin-bottom: $default-padding * .5;
+ margin-bottom: $default-padding * 0.5;
}
.subtitle + p {
- margin-top: $default-padding * .5;
+ margin-top: $default-padding * 0.5;
}
}
.button {
&.is-hero-button {
- background-color: rgba($white, .5);
+ background-color: rgba($white, 0.5);
font-weight: 300;
@include transition(background-color);
diff --git a/packages/merchant-backoffice-ui/src/scss/_loading.scss b/packages/merchant-backoffice-ui/src/scss/_loading.scss
index d25bf8048..32f64f276 100644
--- a/packages/merchant-backoffice-ui/src/scss/_loading.scss
+++ b/packages/merchant-backoffice-ui/src/scss/_loading.scss
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2021-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
diff --git a/packages/merchant-backoffice-ui/src/scss/_main-section.scss b/packages/merchant-backoffice-ui/src/scss/_main-section.scss
index 1a4fad81d..444af5235 100644
--- a/packages/merchant-backoffice-ui/src/scss/_main-section.scss
+++ b/packages/merchant-backoffice-ui/src/scss/_main-section.scss
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2021-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- /**
+/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
diff --git a/packages/merchant-backoffice-ui/src/scss/_misc.scss b/packages/merchant-backoffice-ui/src/scss/_misc.scss
index 65bd28dbd..a0dbc64fc 100644
--- a/packages/merchant-backoffice-ui/src/scss/_misc.scss
+++ b/packages/merchant-backoffice-ui/src/scss/_misc.scss
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2021-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
diff --git a/packages/merchant-backoffice-ui/src/scss/_mixins.scss b/packages/merchant-backoffice-ui/src/scss/_mixins.scss
index 0809033ed..f119ec68a 100644
--- a/packages/merchant-backoffice-ui/src/scss/_mixins.scss
+++ b/packages/merchant-backoffice-ui/src/scss/_mixins.scss
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- /**
+/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
@@ -23,12 +23,12 @@
transition: $t 250ms ease-in-out 50ms;
}
-@mixin icon-with-update-mark ($icon-base-width) {
+@mixin icon-with-update-mark($icon-base-width) {
.icon {
width: $icon-base-width;
&.has-update-mark:after {
- right: ($icon-base-width / 2) - .85;
+ right: calc($icon-base-width / 2) - 0.85;
}
}
}
diff --git a/packages/merchant-backoffice-ui/src/scss/_modal.scss b/packages/merchant-backoffice-ui/src/scss/_modal.scss
index 3edbb8d3a..d2565e7c7 100644
--- a/packages/merchant-backoffice-ui/src/scss/_modal.scss
+++ b/packages/merchant-backoffice-ui/src/scss/_modal.scss
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2021-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- /**
+/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
diff --git a/packages/merchant-backoffice-ui/src/scss/_nav-bar.scss b/packages/merchant-backoffice-ui/src/scss/_nav-bar.scss
index 09f1e2326..4c0e2f5cc 100644
--- a/packages/merchant-backoffice-ui/src/scss/_nav-bar.scss
+++ b/packages/merchant-backoffice-ui/src/scss/_nav-bar.scss
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2021-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- /**
+/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
@@ -25,7 +25,7 @@ nav.navbar {
.navbar-item {
&.has-user-avatar {
.is-user-avatar {
- margin-right: $default-padding * .5;
+ margin-right: $default-padding * 0.5;
display: inline-flex;
width: $navbar-avatar-size;
height: $navbar-avatar-size;
@@ -98,11 +98,11 @@ nav.navbar {
.navbar-item {
.icon:first-child {
- margin-right: $default-padding * .5;
+ margin-right: $default-padding * 0.5;
}
&.has-dropdown {
- >.navbar-link {
+ > .navbar-link {
background-color: $white-ter;
.icon:last-child {
display: none;
@@ -111,11 +111,11 @@ nav.navbar {
}
&.has-user-avatar {
- >.navbar-link {
+ > .navbar-link {
display: flex;
align-items: center;
- padding-top: $default-padding * .5;
- padding-bottom: $default-padding * .5;
+ padding-top: $default-padding * 0.5;
+ padding-bottom: $default-padding * 0.5;
}
}
}
@@ -131,7 +131,7 @@ nav.navbar {
&:not(.is-desktop-icon-only) {
.icon:first-child {
- margin-right: $default-padding * .5;
+ margin-right: $default-padding * 0.5;
}
}
&.is-desktop-icon-only {
diff --git a/packages/merchant-backoffice-ui/src/scss/_table.scss b/packages/merchant-backoffice-ui/src/scss/_table.scss
index 9cf6f4dcd..6c7765a74 100644
--- a/packages/merchant-backoffice-ui/src/scss/_table.scss
+++ b/packages/merchant-backoffice-ui/src/scss/_table.scss
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2021-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- /**
+/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
@@ -26,7 +26,8 @@ table.table {
}
}
- td, th {
+ td,
+ th {
&.checkbox-cell {
.b-checkbox.checkbox:not(.button) {
margin-right: 0;
@@ -83,7 +84,9 @@ table.table {
}
}
- .pagination-previous, .pagination-next, .pagination-link {
+ .pagination-previous,
+ .pagination-next,
+ .pagination-link {
border-color: $button-border-color;
color: $base-color;
@@ -108,24 +111,25 @@ table.table {
&.has-mobile-sort-spaced {
.b-table {
.field.table-mobile-sort {
- padding-top: $default-padding * .5;
+ padding-top: $default-padding * 0.5;
}
}
}
}
.b-table {
.field.table-mobile-sort {
- padding: 0 $default-padding * .5;
+ padding: 0 $default-padding * 0.5;
}
.table-wrapper.has-mobile-cards {
tr {
box-shadow: 0 2px 3px rgba(10, 10, 10, 0.1);
- margin-bottom: 3px!important;
+ margin-bottom: 3px !important;
}
td {
&.is-progress-col {
- span, progress {
+ span,
+ progress {
display: flex;
width: 45%;
align-items: center;
@@ -133,11 +137,13 @@ table.table {
}
}
- &.checkbox-cell, &.is-image-cell {
- border-bottom: 0!important;
+ &.checkbox-cell,
+ &.is-image-cell {
+ border-bottom: 0 !important;
}
- &.checkbox-cell, &.is-actions-cell {
+ &.checkbox-cell,
+ &.is-actions-cell {
&:before {
display: none;
}
@@ -163,7 +169,7 @@ table.table {
.image {
width: $table-avatar-size-mobile;
height: auto;
- margin: 0 auto $default-padding * .25;
+ margin: 0 auto $default-padding * 0.25;
}
}
}
diff --git a/packages/merchant-backoffice-ui/src/scss/_theme-default.scss b/packages/merchant-backoffice-ui/src/scss/_theme-default.scss
index 538dfd4da..f34497bde 100644
--- a/packages/merchant-backoffice-ui/src/scss/_theme-default.scss
+++ b/packages/merchant-backoffice-ui/src/scss/_theme-default.scss
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2021-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
diff --git a/packages/merchant-backoffice-ui/src/scss/_tiles.scss b/packages/merchant-backoffice-ui/src/scss/_tiles.scss
index 94fc04e70..75bc6b94e 100644
--- a/packages/merchant-backoffice-ui/src/scss/_tiles.scss
+++ b/packages/merchant-backoffice-ui/src/scss/_tiles.scss
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2021-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -14,12 +14,11 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- /**
+/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
-
.is-tiles-wrapper {
margin-bottom: $default-padding;
}
diff --git a/packages/merchant-backoffice-ui/src/scss/_title-bar.scss b/packages/merchant-backoffice-ui/src/scss/_title-bar.scss
index 736f26cbd..5de384a32 100644
--- a/packages/merchant-backoffice-ui/src/scss/_title-bar.scss
+++ b/packages/merchant-backoffice-ui/src/scss/_title-bar.scss
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2021-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- /**
+/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
@@ -26,14 +26,14 @@ section.section.is-title-bar {
ul {
li {
display: inline-block;
- padding: 0 $default-padding * .5 0 0;
+ padding: 0 $default-padding * 0.5 0 0;
font-size: $default-padding;
color: $title-bar-color;
&:after {
display: inline-block;
- content: '/';
- padding-left: $default-padding * .5;
+ content: "/";
+ padding-left: $default-padding * 0.5;
}
&:last-child {
diff --git a/packages/merchant-backoffice-ui/src/scss/fonts/nunito.css b/packages/merchant-backoffice-ui/src/scss/fonts/nunito.css
index ab30db36b..591fc3da2 100644
--- a/packages/merchant-backoffice-ui/src/scss/fonts/nunito.css
+++ b/packages/merchant-backoffice-ui/src/scss/fonts/nunito.css
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,8 +15,8 @@
*/
@font-face {
- font-family: 'Nunito';
+ font-family: "Nunito";
font-style: normal;
font-weight: 400;
- src: url(./XRXV3I6Li01BKofINeaE.ttf) format('truetype');
+ src: url(./XRXV3I6Li01BKofINeaE.ttf) format("truetype");
}
diff --git a/packages/merchant-backoffice-ui/src/scss/icons/materialdesignicons-4.9.95.min.css b/packages/merchant-backoffice-ui/src/scss/icons/materialdesignicons-4.9.95.min.css
index 24a89d639..2b8a2b244 100644
--- a/packages/merchant-backoffice-ui/src/scss/icons/materialdesignicons-4.9.95.min.css
+++ b/packages/merchant-backoffice-ui/src/scss/icons/materialdesignicons-4.9.95.min.css
@@ -1,3 +1,15109 @@
-@font-face{font-family:"Material Design Icons";src:url("./fonts/materialdesignicons-webfont-4.9.95.eot");src:url("./fonts/materialdesignicons-webfont-4.9.95.woff2") format("woff2"),url("./fonts/materialdesignicons-webfont-4.9.95.woff") format("woff"),url("./fonts/materialdesignicons-webfont-4.9.95.ttf") format("truetype");font-weight:normal;font-style:normal}.mdi:before,.mdi-set{display:inline-block;font:normal normal normal 24px/1 "Material Design Icons";font-size:inherit;text-rendering:auto;line-height:inherit;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.mdi-ab-testing::before{content:"\F001C"}.mdi-abjad-arabic::before{content:"\F0353"}.mdi-abjad-hebrew::before{content:"\F0354"}.mdi-abugida-devanagari::before{content:"\F0355"}.mdi-abugida-thai::before{content:"\F0356"}.mdi-access-point::before{content:"\F002"}.mdi-access-point-network::before{content:"\F003"}.mdi-access-point-network-off::before{content:"\FBBD"}.mdi-account::before{content:"\F004"}.mdi-account-alert::before{content:"\F005"}.mdi-account-alert-outline::before{content:"\FB2C"}.mdi-account-arrow-left::before{content:"\FB2D"}.mdi-account-arrow-left-outline::before{content:"\FB2E"}.mdi-account-arrow-right::before{content:"\FB2F"}.mdi-account-arrow-right-outline::before{content:"\FB30"}.mdi-account-badge::before{content:"\FD83"}.mdi-account-badge-alert::before{content:"\FD84"}.mdi-account-badge-alert-outline::before{content:"\FD85"}.mdi-account-badge-horizontal::before{content:"\FDF0"}.mdi-account-badge-horizontal-outline::before{content:"\FDF1"}.mdi-account-badge-outline::before{content:"\FD86"}.mdi-account-box::before{content:"\F006"}.mdi-account-box-multiple::before{content:"\F933"}.mdi-account-box-multiple-outline::before{content:"\F002C"}.mdi-account-box-outline::before{content:"\F007"}.mdi-account-cancel::before{content:"\F030A"}.mdi-account-cancel-outline::before{content:"\F030B"}.mdi-account-card-details::before{content:"\F5D2"}.mdi-account-card-details-outline::before{content:"\FD87"}.mdi-account-cash::before{content:"\F00C2"}.mdi-account-cash-outline::before{content:"\F00C3"}.mdi-account-check::before{content:"\F008"}.mdi-account-check-outline::before{content:"\FBBE"}.mdi-account-child::before{content:"\FA88"}.mdi-account-child-circle::before{content:"\FA89"}.mdi-account-child-outline::before{content:"\F00F3"}.mdi-account-circle::before{content:"\F009"}.mdi-account-circle-outline::before{content:"\FB31"}.mdi-account-clock::before{content:"\FB32"}.mdi-account-clock-outline::before{content:"\FB33"}.mdi-account-cog::before{content:"\F039B"}.mdi-account-cog-outline::before{content:"\F039C"}.mdi-account-convert::before{content:"\F00A"}.mdi-account-convert-outline::before{content:"\F032C"}.mdi-account-details::before{content:"\F631"}.mdi-account-details-outline::before{content:"\F039D"}.mdi-account-edit::before{content:"\F6BB"}.mdi-account-edit-outline::before{content:"\F001D"}.mdi-account-group::before{content:"\F848"}.mdi-account-group-outline::before{content:"\FB34"}.mdi-account-heart::before{content:"\F898"}.mdi-account-heart-outline::before{content:"\FBBF"}.mdi-account-key::before{content:"\F00B"}.mdi-account-key-outline::before{content:"\FBC0"}.mdi-account-lock::before{content:"\F0189"}.mdi-account-lock-outline::before{content:"\F018A"}.mdi-account-minus::before{content:"\F00D"}.mdi-account-minus-outline::before{content:"\FAEB"}.mdi-account-multiple::before{content:"\F00E"}.mdi-account-multiple-check::before{content:"\F8C4"}.mdi-account-multiple-check-outline::before{content:"\F0229"}.mdi-account-multiple-minus::before{content:"\F5D3"}.mdi-account-multiple-minus-outline::before{content:"\FBC1"}.mdi-account-multiple-outline::before{content:"\F00F"}.mdi-account-multiple-plus::before{content:"\F010"}.mdi-account-multiple-plus-outline::before{content:"\F7FF"}.mdi-account-multiple-remove::before{content:"\F0235"}.mdi-account-multiple-remove-outline::before{content:"\F0236"}.mdi-account-network::before{content:"\F011"}.mdi-account-network-outline::before{content:"\FBC2"}.mdi-account-off::before{content:"\F012"}.mdi-account-off-outline::before{content:"\FBC3"}.mdi-account-outline::before{content:"\F013"}.mdi-account-plus::before{content:"\F014"}.mdi-account-plus-outline::before{content:"\F800"}.mdi-account-question::before{content:"\FB35"}.mdi-account-question-outline::before{content:"\FB36"}.mdi-account-remove::before{content:"\F015"}.mdi-account-remove-outline::before{content:"\FAEC"}.mdi-account-search::before{content:"\F016"}.mdi-account-search-outline::before{content:"\F934"}.mdi-account-settings::before{content:"\F630"}.mdi-account-settings-outline::before{content:"\F00F4"}.mdi-account-star::before{content:"\F017"}.mdi-account-star-outline::before{content:"\FBC4"}.mdi-account-supervisor::before{content:"\FA8A"}.mdi-account-supervisor-circle::before{content:"\FA8B"}.mdi-account-supervisor-outline::before{content:"\F0158"}.mdi-account-switch::before{content:"\F019"}.mdi-account-tie::before{content:"\FCBF"}.mdi-account-tie-outline::before{content:"\F00F5"}.mdi-account-tie-voice::before{content:"\F0333"}.mdi-account-tie-voice-off::before{content:"\F0335"}.mdi-account-tie-voice-off-outline::before{content:"\F0336"}.mdi-account-tie-voice-outline::before{content:"\F0334"}.mdi-accusoft::before{content:"\F849"}.mdi-adjust::before{content:"\F01A"}.mdi-adobe::before{content:"\F935"}.mdi-adobe-acrobat::before{content:"\FFBD"}.mdi-air-conditioner::before{content:"\F01B"}.mdi-air-filter::before{content:"\FD1F"}.mdi-air-horn::before{content:"\FD88"}.mdi-air-humidifier::before{content:"\F00C4"}.mdi-air-purifier::before{content:"\FD20"}.mdi-airbag::before{content:"\FBC5"}.mdi-airballoon::before{content:"\F01C"}.mdi-airballoon-outline::before{content:"\F002D"}.mdi-airplane::before{content:"\F01D"}.mdi-airplane-landing::before{content:"\F5D4"}.mdi-airplane-off::before{content:"\F01E"}.mdi-airplane-takeoff::before{content:"\F5D5"}.mdi-airplay::before{content:"\F01F"}.mdi-airport::before{content:"\F84A"}.mdi-alarm::before{content:"\F020"}.mdi-alarm-bell::before{content:"\F78D"}.mdi-alarm-check::before{content:"\F021"}.mdi-alarm-light::before{content:"\F78E"}.mdi-alarm-light-outline::before{content:"\FBC6"}.mdi-alarm-multiple::before{content:"\F022"}.mdi-alarm-note::before{content:"\FE8E"}.mdi-alarm-note-off::before{content:"\FE8F"}.mdi-alarm-off::before{content:"\F023"}.mdi-alarm-plus::before{content:"\F024"}.mdi-alarm-snooze::before{content:"\F68D"}.mdi-album::before{content:"\F025"}.mdi-alert::before{content:"\F026"}.mdi-alert-box::before{content:"\F027"}.mdi-alert-box-outline::before{content:"\FCC0"}.mdi-alert-circle::before{content:"\F028"}.mdi-alert-circle-check::before{content:"\F0218"}.mdi-alert-circle-check-outline::before{content:"\F0219"}.mdi-alert-circle-outline::before{content:"\F5D6"}.mdi-alert-decagram::before{content:"\F6BC"}.mdi-alert-decagram-outline::before{content:"\FCC1"}.mdi-alert-octagon::before{content:"\F029"}.mdi-alert-octagon-outline::before{content:"\FCC2"}.mdi-alert-octagram::before{content:"\F766"}.mdi-alert-octagram-outline::before{content:"\FCC3"}.mdi-alert-outline::before{content:"\F02A"}.mdi-alert-rhombus::before{content:"\F01F9"}.mdi-alert-rhombus-outline::before{content:"\F01FA"}.mdi-alien::before{content:"\F899"}.mdi-alien-outline::before{content:"\F00F6"}.mdi-align-horizontal-center::before{content:"\F01EE"}.mdi-align-horizontal-left::before{content:"\F01ED"}.mdi-align-horizontal-right::before{content:"\F01EF"}.mdi-align-vertical-bottom::before{content:"\F01F0"}.mdi-align-vertical-center::before{content:"\F01F1"}.mdi-align-vertical-top::before{content:"\F01F2"}.mdi-all-inclusive::before{content:"\F6BD"}.mdi-allergy::before{content:"\F0283"}.mdi-alpha::before{content:"\F02B"}.mdi-alpha-a::before{content:"\41"}.mdi-alpha-a-box::before{content:"\FAED"}.mdi-alpha-a-box-outline::before{content:"\FBC7"}.mdi-alpha-a-circle::before{content:"\FBC8"}.mdi-alpha-a-circle-outline::before{content:"\FBC9"}.mdi-alpha-b::before{content:"\42"}.mdi-alpha-b-box::before{content:"\FAEE"}.mdi-alpha-b-box-outline::before{content:"\FBCA"}.mdi-alpha-b-circle::before{content:"\FBCB"}.mdi-alpha-b-circle-outline::before{content:"\FBCC"}.mdi-alpha-c::before{content:"\43"}.mdi-alpha-c-box::before{content:"\FAEF"}.mdi-alpha-c-box-outline::before{content:"\FBCD"}.mdi-alpha-c-circle::before{content:"\FBCE"}.mdi-alpha-c-circle-outline::before{content:"\FBCF"}.mdi-alpha-d::before{content:"\44"}.mdi-alpha-d-box::before{content:"\FAF0"}.mdi-alpha-d-box-outline::before{content:"\FBD0"}.mdi-alpha-d-circle::before{content:"\FBD1"}.mdi-alpha-d-circle-outline::before{content:"\FBD2"}.mdi-alpha-e::before{content:"\45"}.mdi-alpha-e-box::before{content:"\FAF1"}.mdi-alpha-e-box-outline::before{content:"\FBD3"}.mdi-alpha-e-circle::before{content:"\FBD4"}.mdi-alpha-e-circle-outline::before{content:"\FBD5"}.mdi-alpha-f::before{content:"\46"}.mdi-alpha-f-box::before{content:"\FAF2"}.mdi-alpha-f-box-outline::before{content:"\FBD6"}.mdi-alpha-f-circle::before{content:"\FBD7"}.mdi-alpha-f-circle-outline::before{content:"\FBD8"}.mdi-alpha-g::before{content:"\47"}.mdi-alpha-g-box::before{content:"\FAF3"}.mdi-alpha-g-box-outline::before{content:"\FBD9"}.mdi-alpha-g-circle::before{content:"\FBDA"}.mdi-alpha-g-circle-outline::before{content:"\FBDB"}.mdi-alpha-h::before{content:"\48"}.mdi-alpha-h-box::before{content:"\FAF4"}.mdi-alpha-h-box-outline::before{content:"\FBDC"}.mdi-alpha-h-circle::before{content:"\FBDD"}.mdi-alpha-h-circle-outline::before{content:"\FBDE"}.mdi-alpha-i::before{content:"\49"}.mdi-alpha-i-box::before{content:"\FAF5"}.mdi-alpha-i-box-outline::before{content:"\FBDF"}.mdi-alpha-i-circle::before{content:"\FBE0"}.mdi-alpha-i-circle-outline::before{content:"\FBE1"}.mdi-alpha-j::before{content:"\4A"}.mdi-alpha-j-box::before{content:"\FAF6"}.mdi-alpha-j-box-outline::before{content:"\FBE2"}.mdi-alpha-j-circle::before{content:"\FBE3"}.mdi-alpha-j-circle-outline::before{content:"\FBE4"}.mdi-alpha-k::before{content:"\4B"}.mdi-alpha-k-box::before{content:"\FAF7"}.mdi-alpha-k-box-outline::before{content:"\FBE5"}.mdi-alpha-k-circle::before{content:"\FBE6"}.mdi-alpha-k-circle-outline::before{content:"\FBE7"}.mdi-alpha-l::before{content:"\4C"}.mdi-alpha-l-box::before{content:"\FAF8"}.mdi-alpha-l-box-outline::before{content:"\FBE8"}.mdi-alpha-l-circle::before{content:"\FBE9"}.mdi-alpha-l-circle-outline::before{content:"\FBEA"}.mdi-alpha-m::before{content:"\4D"}.mdi-alpha-m-box::before{content:"\FAF9"}.mdi-alpha-m-box-outline::before{content:"\FBEB"}.mdi-alpha-m-circle::before{content:"\FBEC"}.mdi-alpha-m-circle-outline::before{content:"\FBED"}.mdi-alpha-n::before{content:"\4E"}.mdi-alpha-n-box::before{content:"\FAFA"}.mdi-alpha-n-box-outline::before{content:"\FBEE"}.mdi-alpha-n-circle::before{content:"\FBEF"}.mdi-alpha-n-circle-outline::before{content:"\FBF0"}.mdi-alpha-o::before{content:"\4F"}.mdi-alpha-o-box::before{content:"\FAFB"}.mdi-alpha-o-box-outline::before{content:"\FBF1"}.mdi-alpha-o-circle::before{content:"\FBF2"}.mdi-alpha-o-circle-outline::before{content:"\FBF3"}.mdi-alpha-p::before{content:"\50"}.mdi-alpha-p-box::before{content:"\FAFC"}.mdi-alpha-p-box-outline::before{content:"\FBF4"}.mdi-alpha-p-circle::before{content:"\FBF5"}.mdi-alpha-p-circle-outline::before{content:"\FBF6"}.mdi-alpha-q::before{content:"\51"}.mdi-alpha-q-box::before{content:"\FAFD"}.mdi-alpha-q-box-outline::before{content:"\FBF7"}.mdi-alpha-q-circle::before{content:"\FBF8"}.mdi-alpha-q-circle-outline::before{content:"\FBF9"}.mdi-alpha-r::before{content:"\52"}.mdi-alpha-r-box::before{content:"\FAFE"}.mdi-alpha-r-box-outline::before{content:"\FBFA"}.mdi-alpha-r-circle::before{content:"\FBFB"}.mdi-alpha-r-circle-outline::before{content:"\FBFC"}.mdi-alpha-s::before{content:"\53"}.mdi-alpha-s-box::before{content:"\FAFF"}.mdi-alpha-s-box-outline::before{content:"\FBFD"}.mdi-alpha-s-circle::before{content:"\FBFE"}.mdi-alpha-s-circle-outline::before{content:"\FBFF"}.mdi-alpha-t::before{content:"\54"}.mdi-alpha-t-box::before{content:"\FB00"}.mdi-alpha-t-box-outline::before{content:"\FC00"}.mdi-alpha-t-circle::before{content:"\FC01"}.mdi-alpha-t-circle-outline::before{content:"\FC02"}.mdi-alpha-u::before{content:"\55"}.mdi-alpha-u-box::before{content:"\FB01"}.mdi-alpha-u-box-outline::before{content:"\FC03"}.mdi-alpha-u-circle::before{content:"\FC04"}.mdi-alpha-u-circle-outline::before{content:"\FC05"}.mdi-alpha-v::before{content:"\56"}.mdi-alpha-v-box::before{content:"\FB02"}.mdi-alpha-v-box-outline::before{content:"\FC06"}.mdi-alpha-v-circle::before{content:"\FC07"}.mdi-alpha-v-circle-outline::before{content:"\FC08"}.mdi-alpha-w::before{content:"\57"}.mdi-alpha-w-box::before{content:"\FB03"}.mdi-alpha-w-box-outline::before{content:"\FC09"}.mdi-alpha-w-circle::before{content:"\FC0A"}.mdi-alpha-w-circle-outline::before{content:"\FC0B"}.mdi-alpha-x::before{content:"\58"}.mdi-alpha-x-box::before{content:"\FB04"}.mdi-alpha-x-box-outline::before{content:"\FC0C"}.mdi-alpha-x-circle::before{content:"\FC0D"}.mdi-alpha-x-circle-outline::before{content:"\FC0E"}.mdi-alpha-y::before{content:"\59"}.mdi-alpha-y-box::before{content:"\FB05"}.mdi-alpha-y-box-outline::before{content:"\FC0F"}.mdi-alpha-y-circle::before{content:"\FC10"}.mdi-alpha-y-circle-outline::before{content:"\FC11"}.mdi-alpha-z::before{content:"\5A"}.mdi-alpha-z-box::before{content:"\FB06"}.mdi-alpha-z-box-outline::before{content:"\FC12"}.mdi-alpha-z-circle::before{content:"\FC13"}.mdi-alpha-z-circle-outline::before{content:"\FC14"}.mdi-alphabet-aurebesh::before{content:"\F0357"}.mdi-alphabet-cyrillic::before{content:"\F0358"}.mdi-alphabet-greek::before{content:"\F0359"}.mdi-alphabet-latin::before{content:"\F035A"}.mdi-alphabet-piqad::before{content:"\F035B"}.mdi-alphabet-tengwar::before{content:"\F0362"}.mdi-alphabetical::before{content:"\F02C"}.mdi-alphabetical-off::before{content:"\F002E"}.mdi-alphabetical-variant::before{content:"\F002F"}.mdi-alphabetical-variant-off::before{content:"\F0030"}.mdi-altimeter::before{content:"\F5D7"}.mdi-amazon::before{content:"\F02D"}.mdi-amazon-alexa::before{content:"\F8C5"}.mdi-amazon-drive::before{content:"\F02E"}.mdi-ambulance::before{content:"\F02F"}.mdi-ammunition::before{content:"\FCC4"}.mdi-ampersand::before{content:"\FA8C"}.mdi-amplifier::before{content:"\F030"}.mdi-amplifier-off::before{content:"\F01E0"}.mdi-anchor::before{content:"\F031"}.mdi-android::before{content:"\F032"}.mdi-android-auto::before{content:"\FA8D"}.mdi-android-debug-bridge::before{content:"\F033"}.mdi-android-head::before{content:"\F78F"}.mdi-android-messages::before{content:"\FD21"}.mdi-android-studio::before{content:"\F034"}.mdi-angle-acute::before{content:"\F936"}.mdi-angle-obtuse::before{content:"\F937"}.mdi-angle-right::before{content:"\F938"}.mdi-angular::before{content:"\F6B1"}.mdi-angularjs::before{content:"\F6BE"}.mdi-animation::before{content:"\F5D8"}.mdi-animation-outline::before{content:"\FA8E"}.mdi-animation-play::before{content:"\F939"}.mdi-animation-play-outline::before{content:"\FA8F"}.mdi-ansible::before{content:"\F00C5"}.mdi-antenna::before{content:"\F0144"}.mdi-anvil::before{content:"\F89A"}.mdi-apache-kafka::before{content:"\F0031"}.mdi-api::before{content:"\F00C6"}.mdi-api-off::before{content:"\F0282"}.mdi-apple::before{content:"\F035"}.mdi-apple-finder::before{content:"\F036"}.mdi-apple-icloud::before{content:"\F038"}.mdi-apple-ios::before{content:"\F037"}.mdi-apple-keyboard-caps::before{content:"\F632"}.mdi-apple-keyboard-command::before{content:"\F633"}.mdi-apple-keyboard-control::before{content:"\F634"}.mdi-apple-keyboard-option::before{content:"\F635"}.mdi-apple-keyboard-shift::before{content:"\F636"}.mdi-apple-safari::before{content:"\F039"}.mdi-application::before{content:"\F614"}.mdi-application-export::before{content:"\FD89"}.mdi-application-import::before{content:"\FD8A"}.mdi-approximately-equal::before{content:"\FFBE"}.mdi-approximately-equal-box::before{content:"\FFBF"}.mdi-apps::before{content:"\F03B"}.mdi-apps-box::before{content:"\FD22"}.mdi-arch::before{content:"\F8C6"}.mdi-archive::before{content:"\F03C"}.mdi-archive-arrow-down::before{content:"\F0284"}.mdi-archive-arrow-down-outline::before{content:"\F0285"}.mdi-archive-arrow-up::before{content:"\F0286"}.mdi-archive-arrow-up-outline::before{content:"\F0287"}.mdi-archive-outline::before{content:"\F0239"}.mdi-arm-flex::before{content:"\F008F"}.mdi-arm-flex-outline::before{content:"\F0090"}.mdi-arrange-bring-forward::before{content:"\F03D"}.mdi-arrange-bring-to-front::before{content:"\F03E"}.mdi-arrange-send-backward::before{content:"\F03F"}.mdi-arrange-send-to-back::before{content:"\F040"}.mdi-arrow-all::before{content:"\F041"}.mdi-arrow-bottom-left::before{content:"\F042"}.mdi-arrow-bottom-left-bold-outline::before{content:"\F9B6"}.mdi-arrow-bottom-left-thick::before{content:"\F9B7"}.mdi-arrow-bottom-right::before{content:"\F043"}.mdi-arrow-bottom-right-bold-outline::before{content:"\F9B8"}.mdi-arrow-bottom-right-thick::before{content:"\F9B9"}.mdi-arrow-collapse::before{content:"\F615"}.mdi-arrow-collapse-all::before{content:"\F044"}.mdi-arrow-collapse-down::before{content:"\F791"}.mdi-arrow-collapse-horizontal::before{content:"\F84B"}.mdi-arrow-collapse-left::before{content:"\F792"}.mdi-arrow-collapse-right::before{content:"\F793"}.mdi-arrow-collapse-up::before{content:"\F794"}.mdi-arrow-collapse-vertical::before{content:"\F84C"}.mdi-arrow-decision::before{content:"\F9BA"}.mdi-arrow-decision-auto::before{content:"\F9BB"}.mdi-arrow-decision-auto-outline::before{content:"\F9BC"}.mdi-arrow-decision-outline::before{content:"\F9BD"}.mdi-arrow-down::before{content:"\F045"}.mdi-arrow-down-bold::before{content:"\F72D"}.mdi-arrow-down-bold-box::before{content:"\F72E"}.mdi-arrow-down-bold-box-outline::before{content:"\F72F"}.mdi-arrow-down-bold-circle::before{content:"\F047"}.mdi-arrow-down-bold-circle-outline::before{content:"\F048"}.mdi-arrow-down-bold-hexagon-outline::before{content:"\F049"}.mdi-arrow-down-bold-outline::before{content:"\F9BE"}.mdi-arrow-down-box::before{content:"\F6BF"}.mdi-arrow-down-circle::before{content:"\FCB7"}.mdi-arrow-down-circle-outline::before{content:"\FCB8"}.mdi-arrow-down-drop-circle::before{content:"\F04A"}.mdi-arrow-down-drop-circle-outline::before{content:"\F04B"}.mdi-arrow-down-thick::before{content:"\F046"}.mdi-arrow-expand::before{content:"\F616"}.mdi-arrow-expand-all::before{content:"\F04C"}.mdi-arrow-expand-down::before{content:"\F795"}.mdi-arrow-expand-horizontal::before{content:"\F84D"}.mdi-arrow-expand-left::before{content:"\F796"}.mdi-arrow-expand-right::before{content:"\F797"}.mdi-arrow-expand-up::before{content:"\F798"}.mdi-arrow-expand-vertical::before{content:"\F84E"}.mdi-arrow-horizontal-lock::before{content:"\F0186"}.mdi-arrow-left::before{content:"\F04D"}.mdi-arrow-left-bold::before{content:"\F730"}.mdi-arrow-left-bold-box::before{content:"\F731"}.mdi-arrow-left-bold-box-outline::before{content:"\F732"}.mdi-arrow-left-bold-circle::before{content:"\F04F"}.mdi-arrow-left-bold-circle-outline::before{content:"\F050"}.mdi-arrow-left-bold-hexagon-outline::before{content:"\F051"}.mdi-arrow-left-bold-outline::before{content:"\F9BF"}.mdi-arrow-left-box::before{content:"\F6C0"}.mdi-arrow-left-circle::before{content:"\FCB9"}.mdi-arrow-left-circle-outline::before{content:"\FCBA"}.mdi-arrow-left-drop-circle::before{content:"\F052"}.mdi-arrow-left-drop-circle-outline::before{content:"\F053"}.mdi-arrow-left-right::before{content:"\FE90"}.mdi-arrow-left-right-bold::before{content:"\FE91"}.mdi-arrow-left-right-bold-outline::before{content:"\F9C0"}.mdi-arrow-left-thick::before{content:"\F04E"}.mdi-arrow-right::before{content:"\F054"}.mdi-arrow-right-bold::before{content:"\F733"}.mdi-arrow-right-bold-box::before{content:"\F734"}.mdi-arrow-right-bold-box-outline::before{content:"\F735"}.mdi-arrow-right-bold-circle::before{content:"\F056"}.mdi-arrow-right-bold-circle-outline::before{content:"\F057"}.mdi-arrow-right-bold-hexagon-outline::before{content:"\F058"}.mdi-arrow-right-bold-outline::before{content:"\F9C1"}.mdi-arrow-right-box::before{content:"\F6C1"}.mdi-arrow-right-circle::before{content:"\FCBB"}.mdi-arrow-right-circle-outline::before{content:"\FCBC"}.mdi-arrow-right-drop-circle::before{content:"\F059"}.mdi-arrow-right-drop-circle-outline::before{content:"\F05A"}.mdi-arrow-right-thick::before{content:"\F055"}.mdi-arrow-split-horizontal::before{content:"\F93A"}.mdi-arrow-split-vertical::before{content:"\F93B"}.mdi-arrow-top-left::before{content:"\F05B"}.mdi-arrow-top-left-bold-outline::before{content:"\F9C2"}.mdi-arrow-top-left-bottom-right::before{content:"\FE92"}.mdi-arrow-top-left-bottom-right-bold::before{content:"\FE93"}.mdi-arrow-top-left-thick::before{content:"\F9C3"}.mdi-arrow-top-right::before{content:"\F05C"}.mdi-arrow-top-right-bold-outline::before{content:"\F9C4"}.mdi-arrow-top-right-bottom-left::before{content:"\FE94"}.mdi-arrow-top-right-bottom-left-bold::before{content:"\FE95"}.mdi-arrow-top-right-thick::before{content:"\F9C5"}.mdi-arrow-up::before{content:"\F05D"}.mdi-arrow-up-bold::before{content:"\F736"}.mdi-arrow-up-bold-box::before{content:"\F737"}.mdi-arrow-up-bold-box-outline::before{content:"\F738"}.mdi-arrow-up-bold-circle::before{content:"\F05F"}.mdi-arrow-up-bold-circle-outline::before{content:"\F060"}.mdi-arrow-up-bold-hexagon-outline::before{content:"\F061"}.mdi-arrow-up-bold-outline::before{content:"\F9C6"}.mdi-arrow-up-box::before{content:"\F6C2"}.mdi-arrow-up-circle::before{content:"\FCBD"}.mdi-arrow-up-circle-outline::before{content:"\FCBE"}.mdi-arrow-up-down::before{content:"\FE96"}.mdi-arrow-up-down-bold::before{content:"\FE97"}.mdi-arrow-up-down-bold-outline::before{content:"\F9C7"}.mdi-arrow-up-drop-circle::before{content:"\F062"}.mdi-arrow-up-drop-circle-outline::before{content:"\F063"}.mdi-arrow-up-thick::before{content:"\F05E"}.mdi-arrow-vertical-lock::before{content:"\F0187"}.mdi-artist::before{content:"\F802"}.mdi-artist-outline::before{content:"\FCC5"}.mdi-artstation::before{content:"\FB37"}.mdi-aspect-ratio::before{content:"\FA23"}.mdi-assistant::before{content:"\F064"}.mdi-asterisk::before{content:"\F6C3"}.mdi-at::before{content:"\F065"}.mdi-atlassian::before{content:"\F803"}.mdi-atm::before{content:"\FD23"}.mdi-atom::before{content:"\F767"}.mdi-atom-variant::before{content:"\FE98"}.mdi-attachment::before{content:"\F066"}.mdi-audio-video::before{content:"\F93C"}.mdi-audio-video-off::before{content:"\F01E1"}.mdi-audiobook::before{content:"\F067"}.mdi-augmented-reality::before{content:"\F84F"}.mdi-auto-download::before{content:"\F03A9"}.mdi-auto-fix::before{content:"\F068"}.mdi-auto-upload::before{content:"\F069"}.mdi-autorenew::before{content:"\F06A"}.mdi-av-timer::before{content:"\F06B"}.mdi-aws::before{content:"\FDF2"}.mdi-axe::before{content:"\F8C7"}.mdi-axis::before{content:"\FD24"}.mdi-axis-arrow::before{content:"\FD25"}.mdi-axis-arrow-lock::before{content:"\FD26"}.mdi-axis-lock::before{content:"\FD27"}.mdi-axis-x-arrow::before{content:"\FD28"}.mdi-axis-x-arrow-lock::before{content:"\FD29"}.mdi-axis-x-rotate-clockwise::before{content:"\FD2A"}.mdi-axis-x-rotate-counterclockwise::before{content:"\FD2B"}.mdi-axis-x-y-arrow-lock::before{content:"\FD2C"}.mdi-axis-y-arrow::before{content:"\FD2D"}.mdi-axis-y-arrow-lock::before{content:"\FD2E"}.mdi-axis-y-rotate-clockwise::before{content:"\FD2F"}.mdi-axis-y-rotate-counterclockwise::before{content:"\FD30"}.mdi-axis-z-arrow::before{content:"\FD31"}.mdi-axis-z-arrow-lock::before{content:"\FD32"}.mdi-axis-z-rotate-clockwise::before{content:"\FD33"}.mdi-axis-z-rotate-counterclockwise::before{content:"\FD34"}.mdi-azure::before{content:"\F804"}.mdi-azure-devops::before{content:"\F0091"}.mdi-babel::before{content:"\FA24"}.mdi-baby::before{content:"\F06C"}.mdi-baby-bottle::before{content:"\FF56"}.mdi-baby-bottle-outline::before{content:"\FF57"}.mdi-baby-carriage::before{content:"\F68E"}.mdi-baby-carriage-off::before{content:"\FFC0"}.mdi-baby-face::before{content:"\FE99"}.mdi-baby-face-outline::before{content:"\FE9A"}.mdi-backburger::before{content:"\F06D"}.mdi-backspace::before{content:"\F06E"}.mdi-backspace-outline::before{content:"\FB38"}.mdi-backspace-reverse::before{content:"\FE9B"}.mdi-backspace-reverse-outline::before{content:"\FE9C"}.mdi-backup-restore::before{content:"\F06F"}.mdi-bacteria::before{content:"\FEF2"}.mdi-bacteria-outline::before{content:"\FEF3"}.mdi-badminton::before{content:"\F850"}.mdi-bag-carry-on::before{content:"\FF58"}.mdi-bag-carry-on-check::before{content:"\FD41"}.mdi-bag-carry-on-off::before{content:"\FF59"}.mdi-bag-checked::before{content:"\FF5A"}.mdi-bag-personal::before{content:"\FDF3"}.mdi-bag-personal-off::before{content:"\FDF4"}.mdi-bag-personal-off-outline::before{content:"\FDF5"}.mdi-bag-personal-outline::before{content:"\FDF6"}.mdi-baguette::before{content:"\FF5B"}.mdi-balloon::before{content:"\FA25"}.mdi-ballot::before{content:"\F9C8"}.mdi-ballot-outline::before{content:"\F9C9"}.mdi-ballot-recount::before{content:"\FC15"}.mdi-ballot-recount-outline::before{content:"\FC16"}.mdi-bandage::before{content:"\FD8B"}.mdi-bandcamp::before{content:"\F674"}.mdi-bank::before{content:"\F070"}.mdi-bank-minus::before{content:"\FD8C"}.mdi-bank-outline::before{content:"\FE9D"}.mdi-bank-plus::before{content:"\FD8D"}.mdi-bank-remove::before{content:"\FD8E"}.mdi-bank-transfer::before{content:"\FA26"}.mdi-bank-transfer-in::before{content:"\FA27"}.mdi-bank-transfer-out::before{content:"\FA28"}.mdi-barcode::before{content:"\F071"}.mdi-barcode-off::before{content:"\F0261"}.mdi-barcode-scan::before{content:"\F072"}.mdi-barley::before{content:"\F073"}.mdi-barley-off::before{content:"\FB39"}.mdi-barn::before{content:"\FB3A"}.mdi-barrel::before{content:"\F074"}.mdi-baseball::before{content:"\F851"}.mdi-baseball-bat::before{content:"\F852"}.mdi-basecamp::before{content:"\F075"}.mdi-bash::before{content:"\F01AE"}.mdi-basket::before{content:"\F076"}.mdi-basket-fill::before{content:"\F077"}.mdi-basket-outline::before{content:"\F01AC"}.mdi-basket-unfill::before{content:"\F078"}.mdi-basketball::before{content:"\F805"}.mdi-basketball-hoop::before{content:"\FC17"}.mdi-basketball-hoop-outline::before{content:"\FC18"}.mdi-bat::before{content:"\FB3B"}.mdi-battery::before{content:"\F079"}.mdi-battery-10::before{content:"\F07A"}.mdi-battery-10-bluetooth::before{content:"\F93D"}.mdi-battery-20::before{content:"\F07B"}.mdi-battery-20-bluetooth::before{content:"\F93E"}.mdi-battery-30::before{content:"\F07C"}.mdi-battery-30-bluetooth::before{content:"\F93F"}.mdi-battery-40::before{content:"\F07D"}.mdi-battery-40-bluetooth::before{content:"\F940"}.mdi-battery-50::before{content:"\F07E"}.mdi-battery-50-bluetooth::before{content:"\F941"}.mdi-battery-60::before{content:"\F07F"}.mdi-battery-60-bluetooth::before{content:"\F942"}.mdi-battery-70::before{content:"\F080"}.mdi-battery-70-bluetooth::before{content:"\F943"}.mdi-battery-80::before{content:"\F081"}.mdi-battery-80-bluetooth::before{content:"\F944"}.mdi-battery-90::before{content:"\F082"}.mdi-battery-90-bluetooth::before{content:"\F945"}.mdi-battery-alert::before{content:"\F083"}.mdi-battery-alert-bluetooth::before{content:"\F946"}.mdi-battery-alert-variant::before{content:"\F00F7"}.mdi-battery-alert-variant-outline::before{content:"\F00F8"}.mdi-battery-bluetooth::before{content:"\F947"}.mdi-battery-bluetooth-variant::before{content:"\F948"}.mdi-battery-charging::before{content:"\F084"}.mdi-battery-charging-10::before{content:"\F89B"}.mdi-battery-charging-100::before{content:"\F085"}.mdi-battery-charging-20::before{content:"\F086"}.mdi-battery-charging-30::before{content:"\F087"}.mdi-battery-charging-40::before{content:"\F088"}.mdi-battery-charging-50::before{content:"\F89C"}.mdi-battery-charging-60::before{content:"\F089"}.mdi-battery-charging-70::before{content:"\F89D"}.mdi-battery-charging-80::before{content:"\F08A"}.mdi-battery-charging-90::before{content:"\F08B"}.mdi-battery-charging-high::before{content:"\F02D1"}.mdi-battery-charging-low::before{content:"\F02CF"}.mdi-battery-charging-medium::before{content:"\F02D0"}.mdi-battery-charging-outline::before{content:"\F89E"}.mdi-battery-charging-wireless::before{content:"\F806"}.mdi-battery-charging-wireless-10::before{content:"\F807"}.mdi-battery-charging-wireless-20::before{content:"\F808"}.mdi-battery-charging-wireless-30::before{content:"\F809"}.mdi-battery-charging-wireless-40::before{content:"\F80A"}.mdi-battery-charging-wireless-50::before{content:"\F80B"}.mdi-battery-charging-wireless-60::before{content:"\F80C"}.mdi-battery-charging-wireless-70::before{content:"\F80D"}.mdi-battery-charging-wireless-80::before{content:"\F80E"}.mdi-battery-charging-wireless-90::before{content:"\F80F"}.mdi-battery-charging-wireless-alert::before{content:"\F810"}.mdi-battery-charging-wireless-outline::before{content:"\F811"}.mdi-battery-heart::before{content:"\F023A"}.mdi-battery-heart-outline::before{content:"\F023B"}.mdi-battery-heart-variant::before{content:"\F023C"}.mdi-battery-high::before{content:"\F02CE"}.mdi-battery-low::before{content:"\F02CC"}.mdi-battery-medium::before{content:"\F02CD"}.mdi-battery-minus::before{content:"\F08C"}.mdi-battery-negative::before{content:"\F08D"}.mdi-battery-off::before{content:"\F0288"}.mdi-battery-off-outline::before{content:"\F0289"}.mdi-battery-outline::before{content:"\F08E"}.mdi-battery-plus::before{content:"\F08F"}.mdi-battery-positive::before{content:"\F090"}.mdi-battery-unknown::before{content:"\F091"}.mdi-battery-unknown-bluetooth::before{content:"\F949"}.mdi-battlenet::before{content:"\FB3C"}.mdi-beach::before{content:"\F092"}.mdi-beaker::before{content:"\FCC6"}.mdi-beaker-alert::before{content:"\F0254"}.mdi-beaker-alert-outline::before{content:"\F0255"}.mdi-beaker-check::before{content:"\F0256"}.mdi-beaker-check-outline::before{content:"\F0257"}.mdi-beaker-minus::before{content:"\F0258"}.mdi-beaker-minus-outline::before{content:"\F0259"}.mdi-beaker-outline::before{content:"\F68F"}.mdi-beaker-plus::before{content:"\F025A"}.mdi-beaker-plus-outline::before{content:"\F025B"}.mdi-beaker-question::before{content:"\F025C"}.mdi-beaker-question-outline::before{content:"\F025D"}.mdi-beaker-remove::before{content:"\F025E"}.mdi-beaker-remove-outline::before{content:"\F025F"}.mdi-beats::before{content:"\F097"}.mdi-bed-double::before{content:"\F0092"}.mdi-bed-double-outline::before{content:"\F0093"}.mdi-bed-empty::before{content:"\F89F"}.mdi-bed-king::before{content:"\F0094"}.mdi-bed-king-outline::before{content:"\F0095"}.mdi-bed-queen::before{content:"\F0096"}.mdi-bed-queen-outline::before{content:"\F0097"}.mdi-bed-single::before{content:"\F0098"}.mdi-bed-single-outline::before{content:"\F0099"}.mdi-bee::before{content:"\FFC1"}.mdi-bee-flower::before{content:"\FFC2"}.mdi-beehive-outline::before{content:"\F00F9"}.mdi-beer::before{content:"\F098"}.mdi-beer-outline::before{content:"\F0337"}.mdi-behance::before{content:"\F099"}.mdi-bell::before{content:"\F09A"}.mdi-bell-alert::before{content:"\FD35"}.mdi-bell-alert-outline::before{content:"\FE9E"}.mdi-bell-check::before{content:"\F0210"}.mdi-bell-check-outline::before{content:"\F0211"}.mdi-bell-circle::before{content:"\FD36"}.mdi-bell-circle-outline::before{content:"\FD37"}.mdi-bell-off::before{content:"\F09B"}.mdi-bell-off-outline::before{content:"\FA90"}.mdi-bell-outline::before{content:"\F09C"}.mdi-bell-plus::before{content:"\F09D"}.mdi-bell-plus-outline::before{content:"\FA91"}.mdi-bell-ring::before{content:"\F09E"}.mdi-bell-ring-outline::before{content:"\F09F"}.mdi-bell-sleep::before{content:"\F0A0"}.mdi-bell-sleep-outline::before{content:"\FA92"}.mdi-beta::before{content:"\F0A1"}.mdi-betamax::before{content:"\F9CA"}.mdi-biathlon::before{content:"\FDF7"}.mdi-bible::before{content:"\F0A2"}.mdi-bicycle::before{content:"\F00C7"}.mdi-bicycle-basket::before{content:"\F0260"}.mdi-bike::before{content:"\F0A3"}.mdi-bike-fast::before{content:"\F014A"}.mdi-billboard::before{content:"\F0032"}.mdi-billiards::before{content:"\FB3D"}.mdi-billiards-rack::before{content:"\FB3E"}.mdi-bing::before{content:"\F0A4"}.mdi-binoculars::before{content:"\F0A5"}.mdi-bio::before{content:"\F0A6"}.mdi-biohazard::before{content:"\F0A7"}.mdi-bitbucket::before{content:"\F0A8"}.mdi-bitcoin::before{content:"\F812"}.mdi-black-mesa::before{content:"\F0A9"}.mdi-blackberry::before{content:"\F0AA"}.mdi-blender::before{content:"\FCC7"}.mdi-blender-software::before{content:"\F0AB"}.mdi-blinds::before{content:"\F0AC"}.mdi-blinds-open::before{content:"\F0033"}.mdi-block-helper::before{content:"\F0AD"}.mdi-blogger::before{content:"\F0AE"}.mdi-blood-bag::before{content:"\FCC8"}.mdi-bluetooth::before{content:"\F0AF"}.mdi-bluetooth-audio::before{content:"\F0B0"}.mdi-bluetooth-connect::before{content:"\F0B1"}.mdi-bluetooth-off::before{content:"\F0B2"}.mdi-bluetooth-settings::before{content:"\F0B3"}.mdi-bluetooth-transfer::before{content:"\F0B4"}.mdi-blur::before{content:"\F0B5"}.mdi-blur-linear::before{content:"\F0B6"}.mdi-blur-off::before{content:"\F0B7"}.mdi-blur-radial::before{content:"\F0B8"}.mdi-bolnisi-cross::before{content:"\FCC9"}.mdi-bolt::before{content:"\FD8F"}.mdi-bomb::before{content:"\F690"}.mdi-bomb-off::before{content:"\F6C4"}.mdi-bone::before{content:"\F0B9"}.mdi-book::before{content:"\F0BA"}.mdi-book-information-variant::before{content:"\F009A"}.mdi-book-lock::before{content:"\F799"}.mdi-book-lock-open::before{content:"\F79A"}.mdi-book-minus::before{content:"\F5D9"}.mdi-book-minus-multiple::before{content:"\FA93"}.mdi-book-multiple::before{content:"\F0BB"}.mdi-book-open::before{content:"\F0BD"}.mdi-book-open-outline::before{content:"\FB3F"}.mdi-book-open-page-variant::before{content:"\F5DA"}.mdi-book-open-variant::before{content:"\F0BE"}.mdi-book-outline::before{content:"\FB40"}.mdi-book-play::before{content:"\FE9F"}.mdi-book-play-outline::before{content:"\FEA0"}.mdi-book-plus::before{content:"\F5DB"}.mdi-book-plus-multiple::before{content:"\FA94"}.mdi-book-remove::before{content:"\FA96"}.mdi-book-remove-multiple::before{content:"\FA95"}.mdi-book-search::before{content:"\FEA1"}.mdi-book-search-outline::before{content:"\FEA2"}.mdi-book-variant::before{content:"\F0BF"}.mdi-book-variant-multiple::before{content:"\F0BC"}.mdi-bookmark::before{content:"\F0C0"}.mdi-bookmark-check::before{content:"\F0C1"}.mdi-bookmark-check-outline::before{content:"\F03A6"}.mdi-bookmark-minus::before{content:"\F9CB"}.mdi-bookmark-minus-outline::before{content:"\F9CC"}.mdi-bookmark-multiple::before{content:"\FDF8"}.mdi-bookmark-multiple-outline::before{content:"\FDF9"}.mdi-bookmark-music::before{content:"\F0C2"}.mdi-bookmark-music-outline::before{content:"\F03A4"}.mdi-bookmark-off::before{content:"\F9CD"}.mdi-bookmark-off-outline::before{content:"\F9CE"}.mdi-bookmark-outline::before{content:"\F0C3"}.mdi-bookmark-plus::before{content:"\F0C5"}.mdi-bookmark-plus-outline::before{content:"\F0C4"}.mdi-bookmark-remove::before{content:"\F0C6"}.mdi-bookmark-remove-outline::before{content:"\F03A5"}.mdi-bookshelf::before{content:"\F028A"}.mdi-boom-gate::before{content:"\FEA3"}.mdi-boom-gate-alert::before{content:"\FEA4"}.mdi-boom-gate-alert-outline::before{content:"\FEA5"}.mdi-boom-gate-down::before{content:"\FEA6"}.mdi-boom-gate-down-outline::before{content:"\FEA7"}.mdi-boom-gate-outline::before{content:"\FEA8"}.mdi-boom-gate-up::before{content:"\FEA9"}.mdi-boom-gate-up-outline::before{content:"\FEAA"}.mdi-boombox::before{content:"\F5DC"}.mdi-boomerang::before{content:"\F00FA"}.mdi-bootstrap::before{content:"\F6C5"}.mdi-border-all::before{content:"\F0C7"}.mdi-border-all-variant::before{content:"\F8A0"}.mdi-border-bottom::before{content:"\F0C8"}.mdi-border-bottom-variant::before{content:"\F8A1"}.mdi-border-color::before{content:"\F0C9"}.mdi-border-horizontal::before{content:"\F0CA"}.mdi-border-inside::before{content:"\F0CB"}.mdi-border-left::before{content:"\F0CC"}.mdi-border-left-variant::before{content:"\F8A2"}.mdi-border-none::before{content:"\F0CD"}.mdi-border-none-variant::before{content:"\F8A3"}.mdi-border-outside::before{content:"\F0CE"}.mdi-border-right::before{content:"\F0CF"}.mdi-border-right-variant::before{content:"\F8A4"}.mdi-border-style::before{content:"\F0D0"}.mdi-border-top::before{content:"\F0D1"}.mdi-border-top-variant::before{content:"\F8A5"}.mdi-border-vertical::before{content:"\F0D2"}.mdi-bottle-soda::before{content:"\F009B"}.mdi-bottle-soda-classic::before{content:"\F009C"}.mdi-bottle-soda-classic-outline::before{content:"\F038E"}.mdi-bottle-soda-outline::before{content:"\F009D"}.mdi-bottle-tonic::before{content:"\F0159"}.mdi-bottle-tonic-outline::before{content:"\F015A"}.mdi-bottle-tonic-plus::before{content:"\F015B"}.mdi-bottle-tonic-plus-outline::before{content:"\F015C"}.mdi-bottle-tonic-skull::before{content:"\F015D"}.mdi-bottle-tonic-skull-outline::before{content:"\F015E"}.mdi-bottle-wine::before{content:"\F853"}.mdi-bottle-wine-outline::before{content:"\F033B"}.mdi-bow-tie::before{content:"\F677"}.mdi-bowl::before{content:"\F617"}.mdi-bowling::before{content:"\F0D3"}.mdi-box::before{content:"\F0D4"}.mdi-box-cutter::before{content:"\F0D5"}.mdi-box-shadow::before{content:"\F637"}.mdi-boxing-glove::before{content:"\FB41"}.mdi-braille::before{content:"\F9CF"}.mdi-brain::before{content:"\F9D0"}.mdi-bread-slice::before{content:"\FCCA"}.mdi-bread-slice-outline::before{content:"\FCCB"}.mdi-bridge::before{content:"\F618"}.mdi-briefcase::before{content:"\F0D6"}.mdi-briefcase-account::before{content:"\FCCC"}.mdi-briefcase-account-outline::before{content:"\FCCD"}.mdi-briefcase-check::before{content:"\F0D7"}.mdi-briefcase-check-outline::before{content:"\F0349"}.mdi-briefcase-clock::before{content:"\F00FB"}.mdi-briefcase-clock-outline::before{content:"\F00FC"}.mdi-briefcase-download::before{content:"\F0D8"}.mdi-briefcase-download-outline::before{content:"\FC19"}.mdi-briefcase-edit::before{content:"\FA97"}.mdi-briefcase-edit-outline::before{content:"\FC1A"}.mdi-briefcase-minus::before{content:"\FA29"}.mdi-briefcase-minus-outline::before{content:"\FC1B"}.mdi-briefcase-outline::before{content:"\F813"}.mdi-briefcase-plus::before{content:"\FA2A"}.mdi-briefcase-plus-outline::before{content:"\FC1C"}.mdi-briefcase-remove::before{content:"\FA2B"}.mdi-briefcase-remove-outline::before{content:"\FC1D"}.mdi-briefcase-search::before{content:"\FA2C"}.mdi-briefcase-search-outline::before{content:"\FC1E"}.mdi-briefcase-upload::before{content:"\F0D9"}.mdi-briefcase-upload-outline::before{content:"\FC1F"}.mdi-brightness-1::before{content:"\F0DA"}.mdi-brightness-2::before{content:"\F0DB"}.mdi-brightness-3::before{content:"\F0DC"}.mdi-brightness-4::before{content:"\F0DD"}.mdi-brightness-5::before{content:"\F0DE"}.mdi-brightness-6::before{content:"\F0DF"}.mdi-brightness-7::before{content:"\F0E0"}.mdi-brightness-auto::before{content:"\F0E1"}.mdi-brightness-percent::before{content:"\FCCE"}.mdi-broom::before{content:"\F0E2"}.mdi-brush::before{content:"\F0E3"}.mdi-buddhism::before{content:"\F94A"}.mdi-buffer::before{content:"\F619"}.mdi-bug::before{content:"\F0E4"}.mdi-bug-check::before{content:"\FA2D"}.mdi-bug-check-outline::before{content:"\FA2E"}.mdi-bug-outline::before{content:"\FA2F"}.mdi-bugle::before{content:"\FD90"}.mdi-bulldozer::before{content:"\FB07"}.mdi-bullet::before{content:"\FCCF"}.mdi-bulletin-board::before{content:"\F0E5"}.mdi-bullhorn::before{content:"\F0E6"}.mdi-bullhorn-outline::before{content:"\FB08"}.mdi-bullseye::before{content:"\F5DD"}.mdi-bullseye-arrow::before{content:"\F8C8"}.mdi-bulma::before{content:"\F0312"}.mdi-bunk-bed::before{content:"\F032D"}.mdi-bus::before{content:"\F0E7"}.mdi-bus-alert::before{content:"\FA98"}.mdi-bus-articulated-end::before{content:"\F79B"}.mdi-bus-articulated-front::before{content:"\F79C"}.mdi-bus-clock::before{content:"\F8C9"}.mdi-bus-double-decker::before{content:"\F79D"}.mdi-bus-marker::before{content:"\F023D"}.mdi-bus-multiple::before{content:"\FF5C"}.mdi-bus-school::before{content:"\F79E"}.mdi-bus-side::before{content:"\F79F"}.mdi-bus-stop::before{content:"\F0034"}.mdi-bus-stop-covered::before{content:"\F0035"}.mdi-bus-stop-uncovered::before{content:"\F0036"}.mdi-cached::before{content:"\F0E8"}.mdi-cactus::before{content:"\FD91"}.mdi-cake::before{content:"\F0E9"}.mdi-cake-layered::before{content:"\F0EA"}.mdi-cake-variant::before{content:"\F0EB"}.mdi-calculator::before{content:"\F0EC"}.mdi-calculator-variant::before{content:"\FA99"}.mdi-calendar::before{content:"\F0ED"}.mdi-calendar-account::before{content:"\FEF4"}.mdi-calendar-account-outline::before{content:"\FEF5"}.mdi-calendar-alert::before{content:"\FA30"}.mdi-calendar-arrow-left::before{content:"\F015F"}.mdi-calendar-arrow-right::before{content:"\F0160"}.mdi-calendar-blank::before{content:"\F0EE"}.mdi-calendar-blank-multiple::before{content:"\F009E"}.mdi-calendar-blank-outline::before{content:"\FB42"}.mdi-calendar-check::before{content:"\F0EF"}.mdi-calendar-check-outline::before{content:"\FC20"}.mdi-calendar-clock::before{content:"\F0F0"}.mdi-calendar-edit::before{content:"\F8A6"}.mdi-calendar-export::before{content:"\FB09"}.mdi-calendar-heart::before{content:"\F9D1"}.mdi-calendar-import::before{content:"\FB0A"}.mdi-calendar-minus::before{content:"\FD38"}.mdi-calendar-month::before{content:"\FDFA"}.mdi-calendar-month-outline::before{content:"\FDFB"}.mdi-calendar-multiple::before{content:"\F0F1"}.mdi-calendar-multiple-check::before{content:"\F0F2"}.mdi-calendar-multiselect::before{content:"\FA31"}.mdi-calendar-outline::before{content:"\FB43"}.mdi-calendar-plus::before{content:"\F0F3"}.mdi-calendar-question::before{content:"\F691"}.mdi-calendar-range::before{content:"\F678"}.mdi-calendar-range-outline::before{content:"\FB44"}.mdi-calendar-remove::before{content:"\F0F4"}.mdi-calendar-remove-outline::before{content:"\FC21"}.mdi-calendar-repeat::before{content:"\FEAB"}.mdi-calendar-repeat-outline::before{content:"\FEAC"}.mdi-calendar-search::before{content:"\F94B"}.mdi-calendar-star::before{content:"\F9D2"}.mdi-calendar-text::before{content:"\F0F5"}.mdi-calendar-text-outline::before{content:"\FC22"}.mdi-calendar-today::before{content:"\F0F6"}.mdi-calendar-week::before{content:"\FA32"}.mdi-calendar-week-begin::before{content:"\FA33"}.mdi-calendar-weekend::before{content:"\FEF6"}.mdi-calendar-weekend-outline::before{content:"\FEF7"}.mdi-call-made::before{content:"\F0F7"}.mdi-call-merge::before{content:"\F0F8"}.mdi-call-missed::before{content:"\F0F9"}.mdi-call-received::before{content:"\F0FA"}.mdi-call-split::before{content:"\F0FB"}.mdi-camcorder::before{content:"\F0FC"}.mdi-camcorder-box::before{content:"\F0FD"}.mdi-camcorder-box-off::before{content:"\F0FE"}.mdi-camcorder-off::before{content:"\F0FF"}.mdi-camera::before{content:"\F100"}.mdi-camera-account::before{content:"\F8CA"}.mdi-camera-burst::before{content:"\F692"}.mdi-camera-control::before{content:"\FB45"}.mdi-camera-enhance::before{content:"\F101"}.mdi-camera-enhance-outline::before{content:"\FB46"}.mdi-camera-front::before{content:"\F102"}.mdi-camera-front-variant::before{content:"\F103"}.mdi-camera-gopro::before{content:"\F7A0"}.mdi-camera-image::before{content:"\F8CB"}.mdi-camera-iris::before{content:"\F104"}.mdi-camera-metering-center::before{content:"\F7A1"}.mdi-camera-metering-matrix::before{content:"\F7A2"}.mdi-camera-metering-partial::before{content:"\F7A3"}.mdi-camera-metering-spot::before{content:"\F7A4"}.mdi-camera-off::before{content:"\F5DF"}.mdi-camera-outline::before{content:"\FD39"}.mdi-camera-party-mode::before{content:"\F105"}.mdi-camera-plus::before{content:"\FEF8"}.mdi-camera-plus-outline::before{content:"\FEF9"}.mdi-camera-rear::before{content:"\F106"}.mdi-camera-rear-variant::before{content:"\F107"}.mdi-camera-retake::before{content:"\FDFC"}.mdi-camera-retake-outline::before{content:"\FDFD"}.mdi-camera-switch::before{content:"\F108"}.mdi-camera-timer::before{content:"\F109"}.mdi-camera-wireless::before{content:"\FD92"}.mdi-camera-wireless-outline::before{content:"\FD93"}.mdi-campfire::before{content:"\FEFA"}.mdi-cancel::before{content:"\F739"}.mdi-candle::before{content:"\F5E2"}.mdi-candycane::before{content:"\F10A"}.mdi-cannabis::before{content:"\F7A5"}.mdi-caps-lock::before{content:"\FA9A"}.mdi-car::before{content:"\F10B"}.mdi-car-2-plus::before{content:"\F0037"}.mdi-car-3-plus::before{content:"\F0038"}.mdi-car-back::before{content:"\FDFE"}.mdi-car-battery::before{content:"\F10C"}.mdi-car-brake-abs::before{content:"\FC23"}.mdi-car-brake-alert::before{content:"\FC24"}.mdi-car-brake-hold::before{content:"\FD3A"}.mdi-car-brake-parking::before{content:"\FD3B"}.mdi-car-brake-retarder::before{content:"\F0039"}.mdi-car-child-seat::before{content:"\FFC3"}.mdi-car-clutch::before{content:"\F003A"}.mdi-car-connected::before{content:"\F10D"}.mdi-car-convertible::before{content:"\F7A6"}.mdi-car-coolant-level::before{content:"\F003B"}.mdi-car-cruise-control::before{content:"\FD3C"}.mdi-car-defrost-front::before{content:"\FD3D"}.mdi-car-defrost-rear::before{content:"\FD3E"}.mdi-car-door::before{content:"\FB47"}.mdi-car-door-lock::before{content:"\F00C8"}.mdi-car-electric::before{content:"\FB48"}.mdi-car-esp::before{content:"\FC25"}.mdi-car-estate::before{content:"\F7A7"}.mdi-car-hatchback::before{content:"\F7A8"}.mdi-car-info::before{content:"\F01E9"}.mdi-car-key::before{content:"\FB49"}.mdi-car-light-dimmed::before{content:"\FC26"}.mdi-car-light-fog::before{content:"\FC27"}.mdi-car-light-high::before{content:"\FC28"}.mdi-car-limousine::before{content:"\F8CC"}.mdi-car-multiple::before{content:"\FB4A"}.mdi-car-off::before{content:"\FDFF"}.mdi-car-parking-lights::before{content:"\FD3F"}.mdi-car-pickup::before{content:"\F7A9"}.mdi-car-seat::before{content:"\FFC4"}.mdi-car-seat-cooler::before{content:"\FFC5"}.mdi-car-seat-heater::before{content:"\FFC6"}.mdi-car-shift-pattern::before{content:"\FF5D"}.mdi-car-side::before{content:"\F7AA"}.mdi-car-sports::before{content:"\F7AB"}.mdi-car-tire-alert::before{content:"\FC29"}.mdi-car-traction-control::before{content:"\FD40"}.mdi-car-turbocharger::before{content:"\F003C"}.mdi-car-wash::before{content:"\F10E"}.mdi-car-windshield::before{content:"\F003D"}.mdi-car-windshield-outline::before{content:"\F003E"}.mdi-caravan::before{content:"\F7AC"}.mdi-card::before{content:"\FB4B"}.mdi-card-bulleted::before{content:"\FB4C"}.mdi-card-bulleted-off::before{content:"\FB4D"}.mdi-card-bulleted-off-outline::before{content:"\FB4E"}.mdi-card-bulleted-outline::before{content:"\FB4F"}.mdi-card-bulleted-settings::before{content:"\FB50"}.mdi-card-bulleted-settings-outline::before{content:"\FB51"}.mdi-card-outline::before{content:"\FB52"}.mdi-card-plus::before{content:"\F022A"}.mdi-card-plus-outline::before{content:"\F022B"}.mdi-card-search::before{content:"\F009F"}.mdi-card-search-outline::before{content:"\F00A0"}.mdi-card-text::before{content:"\FB53"}.mdi-card-text-outline::before{content:"\FB54"}.mdi-cards::before{content:"\F638"}.mdi-cards-club::before{content:"\F8CD"}.mdi-cards-diamond::before{content:"\F8CE"}.mdi-cards-diamond-outline::before{content:"\F003F"}.mdi-cards-heart::before{content:"\F8CF"}.mdi-cards-outline::before{content:"\F639"}.mdi-cards-playing-outline::before{content:"\F63A"}.mdi-cards-spade::before{content:"\F8D0"}.mdi-cards-variant::before{content:"\F6C6"}.mdi-carrot::before{content:"\F10F"}.mdi-cart::before{content:"\F110"}.mdi-cart-arrow-down::before{content:"\FD42"}.mdi-cart-arrow-right::before{content:"\FC2A"}.mdi-cart-arrow-up::before{content:"\FD43"}.mdi-cart-minus::before{content:"\FD44"}.mdi-cart-off::before{content:"\F66B"}.mdi-cart-outline::before{content:"\F111"}.mdi-cart-plus::before{content:"\F112"}.mdi-cart-remove::before{content:"\FD45"}.mdi-case-sensitive-alt::before{content:"\F113"}.mdi-cash::before{content:"\F114"}.mdi-cash-100::before{content:"\F115"}.mdi-cash-marker::before{content:"\FD94"}.mdi-cash-minus::before{content:"\F028B"}.mdi-cash-multiple::before{content:"\F116"}.mdi-cash-plus::before{content:"\F028C"}.mdi-cash-refund::before{content:"\FA9B"}.mdi-cash-register::before{content:"\FCD0"}.mdi-cash-remove::before{content:"\F028D"}.mdi-cash-usd::before{content:"\F01A1"}.mdi-cash-usd-outline::before{content:"\F117"}.mdi-cassette::before{content:"\F9D3"}.mdi-cast::before{content:"\F118"}.mdi-cast-audio::before{content:"\F0040"}.mdi-cast-connected::before{content:"\F119"}.mdi-cast-education::before{content:"\FE6D"}.mdi-cast-off::before{content:"\F789"}.mdi-castle::before{content:"\F11A"}.mdi-cat::before{content:"\F11B"}.mdi-cctv::before{content:"\F7AD"}.mdi-ceiling-light::before{content:"\F768"}.mdi-cellphone::before{content:"\F11C"}.mdi-cellphone-android::before{content:"\F11D"}.mdi-cellphone-arrow-down::before{content:"\F9D4"}.mdi-cellphone-basic::before{content:"\F11E"}.mdi-cellphone-dock::before{content:"\F11F"}.mdi-cellphone-erase::before{content:"\F94C"}.mdi-cellphone-information::before{content:"\FF5E"}.mdi-cellphone-iphone::before{content:"\F120"}.mdi-cellphone-key::before{content:"\F94D"}.mdi-cellphone-link::before{content:"\F121"}.mdi-cellphone-link-off::before{content:"\F122"}.mdi-cellphone-lock::before{content:"\F94E"}.mdi-cellphone-message::before{content:"\F8D2"}.mdi-cellphone-message-off::before{content:"\F00FD"}.mdi-cellphone-nfc::before{content:"\FEAD"}.mdi-cellphone-nfc-off::before{content:"\F0303"}.mdi-cellphone-off::before{content:"\F94F"}.mdi-cellphone-play::before{content:"\F0041"}.mdi-cellphone-screenshot::before{content:"\FA34"}.mdi-cellphone-settings::before{content:"\F123"}.mdi-cellphone-settings-variant::before{content:"\F950"}.mdi-cellphone-sound::before{content:"\F951"}.mdi-cellphone-text::before{content:"\F8D1"}.mdi-cellphone-wireless::before{content:"\F814"}.mdi-celtic-cross::before{content:"\FCD1"}.mdi-centos::before{content:"\F0145"}.mdi-certificate::before{content:"\F124"}.mdi-certificate-outline::before{content:"\F01B3"}.mdi-chair-rolling::before{content:"\FFBA"}.mdi-chair-school::before{content:"\F125"}.mdi-charity::before{content:"\FC2B"}.mdi-chart-arc::before{content:"\F126"}.mdi-chart-areaspline::before{content:"\F127"}.mdi-chart-areaspline-variant::before{content:"\FEAE"}.mdi-chart-bar::before{content:"\F128"}.mdi-chart-bar-stacked::before{content:"\F769"}.mdi-chart-bell-curve::before{content:"\FC2C"}.mdi-chart-bell-curve-cumulative::before{content:"\FFC7"}.mdi-chart-bubble::before{content:"\F5E3"}.mdi-chart-donut::before{content:"\F7AE"}.mdi-chart-donut-variant::before{content:"\F7AF"}.mdi-chart-gantt::before{content:"\F66C"}.mdi-chart-histogram::before{content:"\F129"}.mdi-chart-line::before{content:"\F12A"}.mdi-chart-line-stacked::before{content:"\F76A"}.mdi-chart-line-variant::before{content:"\F7B0"}.mdi-chart-multiline::before{content:"\F8D3"}.mdi-chart-multiple::before{content:"\F023E"}.mdi-chart-pie::before{content:"\F12B"}.mdi-chart-ppf::before{content:"\F03AB"}.mdi-chart-scatter-plot::before{content:"\FEAF"}.mdi-chart-scatter-plot-hexbin::before{content:"\F66D"}.mdi-chart-snakey::before{content:"\F020A"}.mdi-chart-snakey-variant::before{content:"\F020B"}.mdi-chart-timeline::before{content:"\F66E"}.mdi-chart-timeline-variant::before{content:"\FEB0"}.mdi-chart-tree::before{content:"\FEB1"}.mdi-chat::before{content:"\FB55"}.mdi-chat-alert::before{content:"\FB56"}.mdi-chat-alert-outline::before{content:"\F02F4"}.mdi-chat-outline::before{content:"\FEFB"}.mdi-chat-processing::before{content:"\FB57"}.mdi-chat-processing-outline::before{content:"\F02F5"}.mdi-chat-sleep::before{content:"\F02FC"}.mdi-chat-sleep-outline::before{content:"\F02FD"}.mdi-check::before{content:"\F12C"}.mdi-check-all::before{content:"\F12D"}.mdi-check-bold::before{content:"\FE6E"}.mdi-check-box-multiple-outline::before{content:"\FC2D"}.mdi-check-box-outline::before{content:"\FC2E"}.mdi-check-circle::before{content:"\F5E0"}.mdi-check-circle-outline::before{content:"\F5E1"}.mdi-check-decagram::before{content:"\F790"}.mdi-check-network::before{content:"\FC2F"}.mdi-check-network-outline::before{content:"\FC30"}.mdi-check-outline::before{content:"\F854"}.mdi-check-underline::before{content:"\FE70"}.mdi-check-underline-circle::before{content:"\FE71"}.mdi-check-underline-circle-outline::before{content:"\FE72"}.mdi-checkbook::before{content:"\FA9C"}.mdi-checkbox-blank::before{content:"\F12E"}.mdi-checkbox-blank-circle::before{content:"\F12F"}.mdi-checkbox-blank-circle-outline::before{content:"\F130"}.mdi-checkbox-blank-off::before{content:"\F0317"}.mdi-checkbox-blank-off-outline::before{content:"\F0318"}.mdi-checkbox-blank-outline::before{content:"\F131"}.mdi-checkbox-intermediate::before{content:"\F855"}.mdi-checkbox-marked::before{content:"\F132"}.mdi-checkbox-marked-circle::before{content:"\F133"}.mdi-checkbox-marked-circle-outline::before{content:"\F134"}.mdi-checkbox-marked-outline::before{content:"\F135"}.mdi-checkbox-multiple-blank::before{content:"\F136"}.mdi-checkbox-multiple-blank-circle::before{content:"\F63B"}.mdi-checkbox-multiple-blank-circle-outline::before{content:"\F63C"}.mdi-checkbox-multiple-blank-outline::before{content:"\F137"}.mdi-checkbox-multiple-marked::before{content:"\F138"}.mdi-checkbox-multiple-marked-circle::before{content:"\F63D"}.mdi-checkbox-multiple-marked-circle-outline::before{content:"\F63E"}.mdi-checkbox-multiple-marked-outline::before{content:"\F139"}.mdi-checkerboard::before{content:"\F13A"}.mdi-checkerboard-minus::before{content:"\F022D"}.mdi-checkerboard-plus::before{content:"\F022C"}.mdi-checkerboard-remove::before{content:"\F022E"}.mdi-cheese::before{content:"\F02E4"}.mdi-chef-hat::before{content:"\FB58"}.mdi-chemical-weapon::before{content:"\F13B"}.mdi-chess-bishop::before{content:"\F85B"}.mdi-chess-king::before{content:"\F856"}.mdi-chess-knight::before{content:"\F857"}.mdi-chess-pawn::before{content:"\F858"}.mdi-chess-queen::before{content:"\F859"}.mdi-chess-rook::before{content:"\F85A"}.mdi-chevron-double-down::before{content:"\F13C"}.mdi-chevron-double-left::before{content:"\F13D"}.mdi-chevron-double-right::before{content:"\F13E"}.mdi-chevron-double-up::before{content:"\F13F"}.mdi-chevron-down::before{content:"\F140"}.mdi-chevron-down-box::before{content:"\F9D5"}.mdi-chevron-down-box-outline::before{content:"\F9D6"}.mdi-chevron-down-circle::before{content:"\FB0B"}.mdi-chevron-down-circle-outline::before{content:"\FB0C"}.mdi-chevron-left::before{content:"\F141"}.mdi-chevron-left-box::before{content:"\F9D7"}.mdi-chevron-left-box-outline::before{content:"\F9D8"}.mdi-chevron-left-circle::before{content:"\FB0D"}.mdi-chevron-left-circle-outline::before{content:"\FB0E"}.mdi-chevron-right::before{content:"\F142"}.mdi-chevron-right-box::before{content:"\F9D9"}.mdi-chevron-right-box-outline::before{content:"\F9DA"}.mdi-chevron-right-circle::before{content:"\FB0F"}.mdi-chevron-right-circle-outline::before{content:"\FB10"}.mdi-chevron-triple-down::before{content:"\FD95"}.mdi-chevron-triple-left::before{content:"\FD96"}.mdi-chevron-triple-right::before{content:"\FD97"}.mdi-chevron-triple-up::before{content:"\FD98"}.mdi-chevron-up::before{content:"\F143"}.mdi-chevron-up-box::before{content:"\F9DB"}.mdi-chevron-up-box-outline::before{content:"\F9DC"}.mdi-chevron-up-circle::before{content:"\FB11"}.mdi-chevron-up-circle-outline::before{content:"\FB12"}.mdi-chili-hot::before{content:"\F7B1"}.mdi-chili-medium::before{content:"\F7B2"}.mdi-chili-mild::before{content:"\F7B3"}.mdi-chip::before{content:"\F61A"}.mdi-christianity::before{content:"\F952"}.mdi-christianity-outline::before{content:"\FCD2"}.mdi-church::before{content:"\F144"}.mdi-cigar::before{content:"\F01B4"}.mdi-circle::before{content:"\F764"}.mdi-circle-double::before{content:"\FEB2"}.mdi-circle-edit-outline::before{content:"\F8D4"}.mdi-circle-expand::before{content:"\FEB3"}.mdi-circle-medium::before{content:"\F9DD"}.mdi-circle-off-outline::before{content:"\F00FE"}.mdi-circle-outline::before{content:"\F765"}.mdi-circle-slice-1::before{content:"\FA9D"}.mdi-circle-slice-2::before{content:"\FA9E"}.mdi-circle-slice-3::before{content:"\FA9F"}.mdi-circle-slice-4::before{content:"\FAA0"}.mdi-circle-slice-5::before{content:"\FAA1"}.mdi-circle-slice-6::before{content:"\FAA2"}.mdi-circle-slice-7::before{content:"\FAA3"}.mdi-circle-slice-8::before{content:"\FAA4"}.mdi-circle-small::before{content:"\F9DE"}.mdi-circular-saw::before{content:"\FE73"}.mdi-cisco-webex::before{content:"\F145"}.mdi-city::before{content:"\F146"}.mdi-city-variant::before{content:"\FA35"}.mdi-city-variant-outline::before{content:"\FA36"}.mdi-clipboard::before{content:"\F147"}.mdi-clipboard-account::before{content:"\F148"}.mdi-clipboard-account-outline::before{content:"\FC31"}.mdi-clipboard-alert::before{content:"\F149"}.mdi-clipboard-alert-outline::before{content:"\FCD3"}.mdi-clipboard-arrow-down::before{content:"\F14A"}.mdi-clipboard-arrow-down-outline::before{content:"\FC32"}.mdi-clipboard-arrow-left::before{content:"\F14B"}.mdi-clipboard-arrow-left-outline::before{content:"\FCD4"}.mdi-clipboard-arrow-right::before{content:"\FCD5"}.mdi-clipboard-arrow-right-outline::before{content:"\FCD6"}.mdi-clipboard-arrow-up::before{content:"\FC33"}.mdi-clipboard-arrow-up-outline::before{content:"\FC34"}.mdi-clipboard-check::before{content:"\F14C"}.mdi-clipboard-check-multiple::before{content:"\F028E"}.mdi-clipboard-check-multiple-outline::before{content:"\F028F"}.mdi-clipboard-check-outline::before{content:"\F8A7"}.mdi-clipboard-file::before{content:"\F0290"}.mdi-clipboard-file-outline::before{content:"\F0291"}.mdi-clipboard-flow::before{content:"\F6C7"}.mdi-clipboard-flow-outline::before{content:"\F0142"}.mdi-clipboard-list::before{content:"\F00FF"}.mdi-clipboard-list-outline::before{content:"\F0100"}.mdi-clipboard-multiple::before{content:"\F0292"}.mdi-clipboard-multiple-outline::before{content:"\F0293"}.mdi-clipboard-outline::before{content:"\F14D"}.mdi-clipboard-play::before{content:"\FC35"}.mdi-clipboard-play-multiple::before{content:"\F0294"}.mdi-clipboard-play-multiple-outline::before{content:"\F0295"}.mdi-clipboard-play-outline::before{content:"\FC36"}.mdi-clipboard-plus::before{content:"\F750"}.mdi-clipboard-plus-outline::before{content:"\F034A"}.mdi-clipboard-pulse::before{content:"\F85C"}.mdi-clipboard-pulse-outline::before{content:"\F85D"}.mdi-clipboard-text::before{content:"\F14E"}.mdi-clipboard-text-multiple::before{content:"\F0296"}.mdi-clipboard-text-multiple-outline::before{content:"\F0297"}.mdi-clipboard-text-outline::before{content:"\FA37"}.mdi-clipboard-text-play::before{content:"\FC37"}.mdi-clipboard-text-play-outline::before{content:"\FC38"}.mdi-clippy::before{content:"\F14F"}.mdi-clock::before{content:"\F953"}.mdi-clock-alert::before{content:"\F954"}.mdi-clock-alert-outline::before{content:"\F5CE"}.mdi-clock-check::before{content:"\FFC8"}.mdi-clock-check-outline::before{content:"\FFC9"}.mdi-clock-digital::before{content:"\FEB4"}.mdi-clock-end::before{content:"\F151"}.mdi-clock-fast::before{content:"\F152"}.mdi-clock-in::before{content:"\F153"}.mdi-clock-out::before{content:"\F154"}.mdi-clock-outline::before{content:"\F150"}.mdi-clock-start::before{content:"\F155"}.mdi-close::before{content:"\F156"}.mdi-close-box::before{content:"\F157"}.mdi-close-box-multiple::before{content:"\FC39"}.mdi-close-box-multiple-outline::before{content:"\FC3A"}.mdi-close-box-outline::before{content:"\F158"}.mdi-close-circle::before{content:"\F159"}.mdi-close-circle-outline::before{content:"\F15A"}.mdi-close-network::before{content:"\F15B"}.mdi-close-network-outline::before{content:"\FC3B"}.mdi-close-octagon::before{content:"\F15C"}.mdi-close-octagon-outline::before{content:"\F15D"}.mdi-close-outline::before{content:"\F6C8"}.mdi-closed-caption::before{content:"\F15E"}.mdi-closed-caption-outline::before{content:"\FD99"}.mdi-cloud::before{content:"\F15F"}.mdi-cloud-alert::before{content:"\F9DF"}.mdi-cloud-braces::before{content:"\F7B4"}.mdi-cloud-check::before{content:"\F160"}.mdi-cloud-check-outline::before{content:"\F02F7"}.mdi-cloud-circle::before{content:"\F161"}.mdi-cloud-download::before{content:"\F162"}.mdi-cloud-download-outline::before{content:"\FB59"}.mdi-cloud-lock::before{content:"\F021C"}.mdi-cloud-lock-outline::before{content:"\F021D"}.mdi-cloud-off-outline::before{content:"\F164"}.mdi-cloud-outline::before{content:"\F163"}.mdi-cloud-print::before{content:"\F165"}.mdi-cloud-print-outline::before{content:"\F166"}.mdi-cloud-question::before{content:"\FA38"}.mdi-cloud-search::before{content:"\F955"}.mdi-cloud-search-outline::before{content:"\F956"}.mdi-cloud-sync::before{content:"\F63F"}.mdi-cloud-sync-outline::before{content:"\F0301"}.mdi-cloud-tags::before{content:"\F7B5"}.mdi-cloud-upload::before{content:"\F167"}.mdi-cloud-upload-outline::before{content:"\FB5A"}.mdi-clover::before{content:"\F815"}.mdi-coach-lamp::before{content:"\F0042"}.mdi-coat-rack::before{content:"\F00C9"}.mdi-code-array::before{content:"\F168"}.mdi-code-braces::before{content:"\F169"}.mdi-code-braces-box::before{content:"\F0101"}.mdi-code-brackets::before{content:"\F16A"}.mdi-code-equal::before{content:"\F16B"}.mdi-code-greater-than::before{content:"\F16C"}.mdi-code-greater-than-or-equal::before{content:"\F16D"}.mdi-code-less-than::before{content:"\F16E"}.mdi-code-less-than-or-equal::before{content:"\F16F"}.mdi-code-not-equal::before{content:"\F170"}.mdi-code-not-equal-variant::before{content:"\F171"}.mdi-code-parentheses::before{content:"\F172"}.mdi-code-parentheses-box::before{content:"\F0102"}.mdi-code-string::before{content:"\F173"}.mdi-code-tags::before{content:"\F174"}.mdi-code-tags-check::before{content:"\F693"}.mdi-codepen::before{content:"\F175"}.mdi-coffee::before{content:"\F176"}.mdi-coffee-maker::before{content:"\F00CA"}.mdi-coffee-off::before{content:"\FFCA"}.mdi-coffee-off-outline::before{content:"\FFCB"}.mdi-coffee-outline::before{content:"\F6C9"}.mdi-coffee-to-go::before{content:"\F177"}.mdi-coffee-to-go-outline::before{content:"\F0339"}.mdi-coffin::before{content:"\FB5B"}.mdi-cog-clockwise::before{content:"\F0208"}.mdi-cog-counterclockwise::before{content:"\F0209"}.mdi-cogs::before{content:"\F8D5"}.mdi-coin::before{content:"\F0196"}.mdi-coin-outline::before{content:"\F178"}.mdi-coins::before{content:"\F694"}.mdi-collage::before{content:"\F640"}.mdi-collapse-all::before{content:"\FAA5"}.mdi-collapse-all-outline::before{content:"\FAA6"}.mdi-color-helper::before{content:"\F179"}.mdi-comma::before{content:"\FE74"}.mdi-comma-box::before{content:"\FE75"}.mdi-comma-box-outline::before{content:"\FE76"}.mdi-comma-circle::before{content:"\FE77"}.mdi-comma-circle-outline::before{content:"\FE78"}.mdi-comment::before{content:"\F17A"}.mdi-comment-account::before{content:"\F17B"}.mdi-comment-account-outline::before{content:"\F17C"}.mdi-comment-alert::before{content:"\F17D"}.mdi-comment-alert-outline::before{content:"\F17E"}.mdi-comment-arrow-left::before{content:"\F9E0"}.mdi-comment-arrow-left-outline::before{content:"\F9E1"}.mdi-comment-arrow-right::before{content:"\F9E2"}.mdi-comment-arrow-right-outline::before{content:"\F9E3"}.mdi-comment-check::before{content:"\F17F"}.mdi-comment-check-outline::before{content:"\F180"}.mdi-comment-edit::before{content:"\F01EA"}.mdi-comment-edit-outline::before{content:"\F02EF"}.mdi-comment-eye::before{content:"\FA39"}.mdi-comment-eye-outline::before{content:"\FA3A"}.mdi-comment-multiple::before{content:"\F85E"}.mdi-comment-multiple-outline::before{content:"\F181"}.mdi-comment-outline::before{content:"\F182"}.mdi-comment-plus::before{content:"\F9E4"}.mdi-comment-plus-outline::before{content:"\F183"}.mdi-comment-processing::before{content:"\F184"}.mdi-comment-processing-outline::before{content:"\F185"}.mdi-comment-question::before{content:"\F816"}.mdi-comment-question-outline::before{content:"\F186"}.mdi-comment-quote::before{content:"\F0043"}.mdi-comment-quote-outline::before{content:"\F0044"}.mdi-comment-remove::before{content:"\F5DE"}.mdi-comment-remove-outline::before{content:"\F187"}.mdi-comment-search::before{content:"\FA3B"}.mdi-comment-search-outline::before{content:"\FA3C"}.mdi-comment-text::before{content:"\F188"}.mdi-comment-text-multiple::before{content:"\F85F"}.mdi-comment-text-multiple-outline::before{content:"\F860"}.mdi-comment-text-outline::before{content:"\F189"}.mdi-compare::before{content:"\F18A"}.mdi-compass::before{content:"\F18B"}.mdi-compass-off::before{content:"\FB5C"}.mdi-compass-off-outline::before{content:"\FB5D"}.mdi-compass-outline::before{content:"\F18C"}.mdi-compass-rose::before{content:"\F03AD"}.mdi-concourse-ci::before{content:"\F00CB"}.mdi-console::before{content:"\F18D"}.mdi-console-line::before{content:"\F7B6"}.mdi-console-network::before{content:"\F8A8"}.mdi-console-network-outline::before{content:"\FC3C"}.mdi-consolidate::before{content:"\F0103"}.mdi-contact-mail::before{content:"\F18E"}.mdi-contact-mail-outline::before{content:"\FEB5"}.mdi-contact-phone::before{content:"\FEB6"}.mdi-contact-phone-outline::before{content:"\FEB7"}.mdi-contactless-payment::before{content:"\FD46"}.mdi-contacts::before{content:"\F6CA"}.mdi-contain::before{content:"\FA3D"}.mdi-contain-end::before{content:"\FA3E"}.mdi-contain-start::before{content:"\FA3F"}.mdi-content-copy::before{content:"\F18F"}.mdi-content-cut::before{content:"\F190"}.mdi-content-duplicate::before{content:"\F191"}.mdi-content-paste::before{content:"\F192"}.mdi-content-save::before{content:"\F193"}.mdi-content-save-alert::before{content:"\FF5F"}.mdi-content-save-alert-outline::before{content:"\FF60"}.mdi-content-save-all::before{content:"\F194"}.mdi-content-save-all-outline::before{content:"\FF61"}.mdi-content-save-edit::before{content:"\FCD7"}.mdi-content-save-edit-outline::before{content:"\FCD8"}.mdi-content-save-move::before{content:"\FE79"}.mdi-content-save-move-outline::before{content:"\FE7A"}.mdi-content-save-outline::before{content:"\F817"}.mdi-content-save-settings::before{content:"\F61B"}.mdi-content-save-settings-outline::before{content:"\FB13"}.mdi-contrast::before{content:"\F195"}.mdi-contrast-box::before{content:"\F196"}.mdi-contrast-circle::before{content:"\F197"}.mdi-controller-classic::before{content:"\FB5E"}.mdi-controller-classic-outline::before{content:"\FB5F"}.mdi-cookie::before{content:"\F198"}.mdi-coolant-temperature::before{content:"\F3C8"}.mdi-copyright::before{content:"\F5E6"}.mdi-cordova::before{content:"\F957"}.mdi-corn::before{content:"\F7B7"}.mdi-counter::before{content:"\F199"}.mdi-cow::before{content:"\F19A"}.mdi-cowboy::before{content:"\FEB8"}.mdi-cpu-32-bit::before{content:"\FEFC"}.mdi-cpu-64-bit::before{content:"\FEFD"}.mdi-crane::before{content:"\F861"}.mdi-creation::before{content:"\F1C9"}.mdi-creative-commons::before{content:"\FD47"}.mdi-credit-card::before{content:"\F0010"}.mdi-credit-card-clock::before{content:"\FEFE"}.mdi-credit-card-clock-outline::before{content:"\FFBC"}.mdi-credit-card-marker::before{content:"\F6A7"}.mdi-credit-card-marker-outline::before{content:"\FD9A"}.mdi-credit-card-minus::before{content:"\FFCC"}.mdi-credit-card-minus-outline::before{content:"\FFCD"}.mdi-credit-card-multiple::before{content:"\F0011"}.mdi-credit-card-multiple-outline::before{content:"\F19C"}.mdi-credit-card-off::before{content:"\F0012"}.mdi-credit-card-off-outline::before{content:"\F5E4"}.mdi-credit-card-outline::before{content:"\F19B"}.mdi-credit-card-plus::before{content:"\F0013"}.mdi-credit-card-plus-outline::before{content:"\F675"}.mdi-credit-card-refund::before{content:"\F0014"}.mdi-credit-card-refund-outline::before{content:"\FAA7"}.mdi-credit-card-remove::before{content:"\FFCE"}.mdi-credit-card-remove-outline::before{content:"\FFCF"}.mdi-credit-card-scan::before{content:"\F0015"}.mdi-credit-card-scan-outline::before{content:"\F19D"}.mdi-credit-card-settings::before{content:"\F0016"}.mdi-credit-card-settings-outline::before{content:"\F8D6"}.mdi-credit-card-wireless::before{content:"\F801"}.mdi-credit-card-wireless-outline::before{content:"\FD48"}.mdi-cricket::before{content:"\FD49"}.mdi-crop::before{content:"\F19E"}.mdi-crop-free::before{content:"\F19F"}.mdi-crop-landscape::before{content:"\F1A0"}.mdi-crop-portrait::before{content:"\F1A1"}.mdi-crop-rotate::before{content:"\F695"}.mdi-crop-square::before{content:"\F1A2"}.mdi-crosshairs::before{content:"\F1A3"}.mdi-crosshairs-gps::before{content:"\F1A4"}.mdi-crosshairs-off::before{content:"\FF62"}.mdi-crosshairs-question::before{content:"\F0161"}.mdi-crown::before{content:"\F1A5"}.mdi-crown-outline::before{content:"\F01FB"}.mdi-cryengine::before{content:"\F958"}.mdi-crystal-ball::before{content:"\FB14"}.mdi-cube::before{content:"\F1A6"}.mdi-cube-outline::before{content:"\F1A7"}.mdi-cube-scan::before{content:"\FB60"}.mdi-cube-send::before{content:"\F1A8"}.mdi-cube-unfolded::before{content:"\F1A9"}.mdi-cup::before{content:"\F1AA"}.mdi-cup-off::before{content:"\F5E5"}.mdi-cup-off-outline::before{content:"\F03A8"}.mdi-cup-outline::before{content:"\F033A"}.mdi-cup-water::before{content:"\F1AB"}.mdi-cupboard::before{content:"\FF63"}.mdi-cupboard-outline::before{content:"\FF64"}.mdi-cupcake::before{content:"\F959"}.mdi-curling::before{content:"\F862"}.mdi-currency-bdt::before{content:"\F863"}.mdi-currency-brl::before{content:"\FB61"}.mdi-currency-btc::before{content:"\F1AC"}.mdi-currency-cny::before{content:"\F7B9"}.mdi-currency-eth::before{content:"\F7BA"}.mdi-currency-eur::before{content:"\F1AD"}.mdi-currency-eur-off::before{content:"\F0340"}.mdi-currency-gbp::before{content:"\F1AE"}.mdi-currency-ils::before{content:"\FC3D"}.mdi-currency-inr::before{content:"\F1AF"}.mdi-currency-jpy::before{content:"\F7BB"}.mdi-currency-krw::before{content:"\F7BC"}.mdi-currency-kzt::before{content:"\F864"}.mdi-currency-ngn::before{content:"\F1B0"}.mdi-currency-php::before{content:"\F9E5"}.mdi-currency-rial::before{content:"\FEB9"}.mdi-currency-rub::before{content:"\F1B1"}.mdi-currency-sign::before{content:"\F7BD"}.mdi-currency-try::before{content:"\F1B2"}.mdi-currency-twd::before{content:"\F7BE"}.mdi-currency-usd::before{content:"\F1B3"}.mdi-currency-usd-off::before{content:"\F679"}.mdi-current-ac::before{content:"\F95A"}.mdi-current-dc::before{content:"\F95B"}.mdi-cursor-default::before{content:"\F1B4"}.mdi-cursor-default-click::before{content:"\FCD9"}.mdi-cursor-default-click-outline::before{content:"\FCDA"}.mdi-cursor-default-gesture::before{content:"\F0152"}.mdi-cursor-default-gesture-outline::before{content:"\F0153"}.mdi-cursor-default-outline::before{content:"\F1B5"}.mdi-cursor-move::before{content:"\F1B6"}.mdi-cursor-pointer::before{content:"\F1B7"}.mdi-cursor-text::before{content:"\F5E7"}.mdi-database::before{content:"\F1B8"}.mdi-database-check::before{content:"\FAA8"}.mdi-database-edit::before{content:"\FB62"}.mdi-database-export::before{content:"\F95D"}.mdi-database-import::before{content:"\F95C"}.mdi-database-lock::before{content:"\FAA9"}.mdi-database-marker::before{content:"\F0321"}.mdi-database-minus::before{content:"\F1B9"}.mdi-database-plus::before{content:"\F1BA"}.mdi-database-refresh::before{content:"\FCDB"}.mdi-database-remove::before{content:"\FCDC"}.mdi-database-search::before{content:"\F865"}.mdi-database-settings::before{content:"\FCDD"}.mdi-death-star::before{content:"\F8D7"}.mdi-death-star-variant::before{content:"\F8D8"}.mdi-deathly-hallows::before{content:"\FB63"}.mdi-debian::before{content:"\F8D9"}.mdi-debug-step-into::before{content:"\F1BB"}.mdi-debug-step-out::before{content:"\F1BC"}.mdi-debug-step-over::before{content:"\F1BD"}.mdi-decagram::before{content:"\F76B"}.mdi-decagram-outline::before{content:"\F76C"}.mdi-decimal::before{content:"\F00CC"}.mdi-decimal-comma::before{content:"\F00CD"}.mdi-decimal-comma-decrease::before{content:"\F00CE"}.mdi-decimal-comma-increase::before{content:"\F00CF"}.mdi-decimal-decrease::before{content:"\F1BE"}.mdi-decimal-increase::before{content:"\F1BF"}.mdi-delete::before{content:"\F1C0"}.mdi-delete-alert::before{content:"\F00D0"}.mdi-delete-alert-outline::before{content:"\F00D1"}.mdi-delete-circle::before{content:"\F682"}.mdi-delete-circle-outline::before{content:"\FB64"}.mdi-delete-empty::before{content:"\F6CB"}.mdi-delete-empty-outline::before{content:"\FEBA"}.mdi-delete-forever::before{content:"\F5E8"}.mdi-delete-forever-outline::before{content:"\FB65"}.mdi-delete-off::before{content:"\F00D2"}.mdi-delete-off-outline::before{content:"\F00D3"}.mdi-delete-outline::before{content:"\F9E6"}.mdi-delete-restore::before{content:"\F818"}.mdi-delete-sweep::before{content:"\F5E9"}.mdi-delete-sweep-outline::before{content:"\FC3E"}.mdi-delete-variant::before{content:"\F1C1"}.mdi-delta::before{content:"\F1C2"}.mdi-desk::before{content:"\F0264"}.mdi-desk-lamp::before{content:"\F95E"}.mdi-deskphone::before{content:"\F1C3"}.mdi-desktop-classic::before{content:"\F7BF"}.mdi-desktop-mac::before{content:"\F1C4"}.mdi-desktop-mac-dashboard::before{content:"\F9E7"}.mdi-desktop-tower::before{content:"\F1C5"}.mdi-desktop-tower-monitor::before{content:"\FAAA"}.mdi-details::before{content:"\F1C6"}.mdi-dev-to::before{content:"\FD4A"}.mdi-developer-board::before{content:"\F696"}.mdi-deviantart::before{content:"\F1C7"}.mdi-devices::before{content:"\FFD0"}.mdi-diabetes::before{content:"\F0151"}.mdi-dialpad::before{content:"\F61C"}.mdi-diameter::before{content:"\FC3F"}.mdi-diameter-outline::before{content:"\FC40"}.mdi-diameter-variant::before{content:"\FC41"}.mdi-diamond::before{content:"\FB66"}.mdi-diamond-outline::before{content:"\FB67"}.mdi-diamond-stone::before{content:"\F1C8"}.mdi-dice-1::before{content:"\F1CA"}.mdi-dice-1-outline::before{content:"\F0175"}.mdi-dice-2::before{content:"\F1CB"}.mdi-dice-2-outline::before{content:"\F0176"}.mdi-dice-3::before{content:"\F1CC"}.mdi-dice-3-outline::before{content:"\F0177"}.mdi-dice-4::before{content:"\F1CD"}.mdi-dice-4-outline::before{content:"\F0178"}.mdi-dice-5::before{content:"\F1CE"}.mdi-dice-5-outline::before{content:"\F0179"}.mdi-dice-6::before{content:"\F1CF"}.mdi-dice-6-outline::before{content:"\F017A"}.mdi-dice-d10::before{content:"\F017E"}.mdi-dice-d10-outline::before{content:"\F76E"}.mdi-dice-d12::before{content:"\F017F"}.mdi-dice-d12-outline::before{content:"\F866"}.mdi-dice-d20::before{content:"\F0180"}.mdi-dice-d20-outline::before{content:"\F5EA"}.mdi-dice-d4::before{content:"\F017B"}.mdi-dice-d4-outline::before{content:"\F5EB"}.mdi-dice-d6::before{content:"\F017C"}.mdi-dice-d6-outline::before{content:"\F5EC"}.mdi-dice-d8::before{content:"\F017D"}.mdi-dice-d8-outline::before{content:"\F5ED"}.mdi-dice-multiple::before{content:"\F76D"}.mdi-dice-multiple-outline::before{content:"\F0181"}.mdi-dictionary::before{content:"\F61D"}.mdi-digital-ocean::before{content:"\F0262"}.mdi-dip-switch::before{content:"\F7C0"}.mdi-directions::before{content:"\F1D0"}.mdi-directions-fork::before{content:"\F641"}.mdi-disc::before{content:"\F5EE"}.mdi-disc-alert::before{content:"\F1D1"}.mdi-disc-player::before{content:"\F95F"}.mdi-discord::before{content:"\F66F"}.mdi-dishwasher::before{content:"\FAAB"}.mdi-dishwasher-alert::before{content:"\F01E3"}.mdi-dishwasher-off::before{content:"\F01E4"}.mdi-disqus::before{content:"\F1D2"}.mdi-disqus-outline::before{content:"\F1D3"}.mdi-distribute-horizontal-center::before{content:"\F01F4"}.mdi-distribute-horizontal-left::before{content:"\F01F3"}.mdi-distribute-horizontal-right::before{content:"\F01F5"}.mdi-distribute-vertical-bottom::before{content:"\F01F6"}.mdi-distribute-vertical-center::before{content:"\F01F7"}.mdi-distribute-vertical-top::before{content:"\F01F8"}.mdi-diving-flippers::before{content:"\FD9B"}.mdi-diving-helmet::before{content:"\FD9C"}.mdi-diving-scuba::before{content:"\FD9D"}.mdi-diving-scuba-flag::before{content:"\FD9E"}.mdi-diving-scuba-tank::before{content:"\FD9F"}.mdi-diving-scuba-tank-multiple::before{content:"\FDA0"}.mdi-diving-snorkel::before{content:"\FDA1"}.mdi-division::before{content:"\F1D4"}.mdi-division-box::before{content:"\F1D5"}.mdi-dlna::before{content:"\FA40"}.mdi-dna::before{content:"\F683"}.mdi-dns::before{content:"\F1D6"}.mdi-dns-outline::before{content:"\FB68"}.mdi-do-not-disturb::before{content:"\F697"}.mdi-do-not-disturb-off::before{content:"\F698"}.mdi-dock-bottom::before{content:"\F00D4"}.mdi-dock-left::before{content:"\F00D5"}.mdi-dock-right::before{content:"\F00D6"}.mdi-dock-window::before{content:"\F00D7"}.mdi-docker::before{content:"\F867"}.mdi-doctor::before{content:"\FA41"}.mdi-dog::before{content:"\FA42"}.mdi-dog-service::before{content:"\FAAC"}.mdi-dog-side::before{content:"\FA43"}.mdi-dolby::before{content:"\F6B2"}.mdi-dolly::before{content:"\FEBB"}.mdi-domain::before{content:"\F1D7"}.mdi-domain-off::before{content:"\FD4B"}.mdi-domain-plus::before{content:"\F00D8"}.mdi-domain-remove::before{content:"\F00D9"}.mdi-domino-mask::before{content:"\F0045"}.mdi-donkey::before{content:"\F7C1"}.mdi-door::before{content:"\F819"}.mdi-door-closed::before{content:"\F81A"}.mdi-door-closed-lock::before{content:"\F00DA"}.mdi-door-open::before{content:"\F81B"}.mdi-doorbell::before{content:"\F0311"}.mdi-doorbell-video::before{content:"\F868"}.mdi-dot-net::before{content:"\FAAD"}.mdi-dots-horizontal::before{content:"\F1D8"}.mdi-dots-horizontal-circle::before{content:"\F7C2"}.mdi-dots-horizontal-circle-outline::before{content:"\FB69"}.mdi-dots-vertical::before{content:"\F1D9"}.mdi-dots-vertical-circle::before{content:"\F7C3"}.mdi-dots-vertical-circle-outline::before{content:"\FB6A"}.mdi-douban::before{content:"\F699"}.mdi-download::before{content:"\F1DA"}.mdi-download-lock::before{content:"\F034B"}.mdi-download-lock-outline::before{content:"\F034C"}.mdi-download-multiple::before{content:"\F9E8"}.mdi-download-network::before{content:"\F6F3"}.mdi-download-network-outline::before{content:"\FC42"}.mdi-download-off::before{content:"\F00DB"}.mdi-download-off-outline::before{content:"\F00DC"}.mdi-download-outline::before{content:"\FB6B"}.mdi-drag::before{content:"\F1DB"}.mdi-drag-horizontal::before{content:"\F1DC"}.mdi-drag-horizontal-variant::before{content:"\F031B"}.mdi-drag-variant::before{content:"\FB6C"}.mdi-drag-vertical::before{content:"\F1DD"}.mdi-drag-vertical-variant::before{content:"\F031C"}.mdi-drama-masks::before{content:"\FCDE"}.mdi-draw::before{content:"\FF66"}.mdi-drawing::before{content:"\F1DE"}.mdi-drawing-box::before{content:"\F1DF"}.mdi-dresser::before{content:"\FF67"}.mdi-dresser-outline::before{content:"\FF68"}.mdi-dribbble::before{content:"\F1E0"}.mdi-dribbble-box::before{content:"\F1E1"}.mdi-drone::before{content:"\F1E2"}.mdi-dropbox::before{content:"\F1E3"}.mdi-drupal::before{content:"\F1E4"}.mdi-duck::before{content:"\F1E5"}.mdi-dumbbell::before{content:"\F1E6"}.mdi-dump-truck::before{content:"\FC43"}.mdi-ear-hearing::before{content:"\F7C4"}.mdi-ear-hearing-off::before{content:"\FA44"}.mdi-earth::before{content:"\F1E7"}.mdi-earth-arrow-right::before{content:"\F033C"}.mdi-earth-box::before{content:"\F6CC"}.mdi-earth-box-off::before{content:"\F6CD"}.mdi-earth-off::before{content:"\F1E8"}.mdi-edge::before{content:"\F1E9"}.mdi-edge-legacy::before{content:"\F027B"}.mdi-egg::before{content:"\FAAE"}.mdi-egg-easter::before{content:"\FAAF"}.mdi-eight-track::before{content:"\F9E9"}.mdi-eject::before{content:"\F1EA"}.mdi-eject-outline::before{content:"\FB6D"}.mdi-electric-switch::before{content:"\FEBC"}.mdi-electric-switch-closed::before{content:"\F0104"}.mdi-electron-framework::before{content:"\F0046"}.mdi-elephant::before{content:"\F7C5"}.mdi-elevation-decline::before{content:"\F1EB"}.mdi-elevation-rise::before{content:"\F1EC"}.mdi-elevator::before{content:"\F1ED"}.mdi-elevator-down::before{content:"\F02ED"}.mdi-elevator-passenger::before{content:"\F03AC"}.mdi-elevator-up::before{content:"\F02EC"}.mdi-ellipse::before{content:"\FEBD"}.mdi-ellipse-outline::before{content:"\FEBE"}.mdi-email::before{content:"\F1EE"}.mdi-email-alert::before{content:"\F6CE"}.mdi-email-alert-outline::before{content:"\FD1E"}.mdi-email-box::before{content:"\FCDF"}.mdi-email-check::before{content:"\FAB0"}.mdi-email-check-outline::before{content:"\FAB1"}.mdi-email-edit::before{content:"\FF00"}.mdi-email-edit-outline::before{content:"\FF01"}.mdi-email-lock::before{content:"\F1F1"}.mdi-email-mark-as-unread::before{content:"\FB6E"}.mdi-email-minus::before{content:"\FF02"}.mdi-email-minus-outline::before{content:"\FF03"}.mdi-email-multiple::before{content:"\FF04"}.mdi-email-multiple-outline::before{content:"\FF05"}.mdi-email-newsletter::before{content:"\FFD1"}.mdi-email-open::before{content:"\F1EF"}.mdi-email-open-multiple::before{content:"\FF06"}.mdi-email-open-multiple-outline::before{content:"\FF07"}.mdi-email-open-outline::before{content:"\F5EF"}.mdi-email-outline::before{content:"\F1F0"}.mdi-email-plus::before{content:"\F9EA"}.mdi-email-plus-outline::before{content:"\F9EB"}.mdi-email-receive::before{content:"\F0105"}.mdi-email-receive-outline::before{content:"\F0106"}.mdi-email-search::before{content:"\F960"}.mdi-email-search-outline::before{content:"\F961"}.mdi-email-send::before{content:"\F0107"}.mdi-email-send-outline::before{content:"\F0108"}.mdi-email-sync::before{content:"\F02F2"}.mdi-email-sync-outline::before{content:"\F02F3"}.mdi-email-variant::before{content:"\F5F0"}.mdi-ember::before{content:"\FB15"}.mdi-emby::before{content:"\F6B3"}.mdi-emoticon::before{content:"\FC44"}.mdi-emoticon-angry::before{content:"\FC45"}.mdi-emoticon-angry-outline::before{content:"\FC46"}.mdi-emoticon-confused::before{content:"\F0109"}.mdi-emoticon-confused-outline::before{content:"\F010A"}.mdi-emoticon-cool::before{content:"\FC47"}.mdi-emoticon-cool-outline::before{content:"\F1F3"}.mdi-emoticon-cry::before{content:"\FC48"}.mdi-emoticon-cry-outline::before{content:"\FC49"}.mdi-emoticon-dead::before{content:"\FC4A"}.mdi-emoticon-dead-outline::before{content:"\F69A"}.mdi-emoticon-devil::before{content:"\FC4B"}.mdi-emoticon-devil-outline::before{content:"\F1F4"}.mdi-emoticon-excited::before{content:"\FC4C"}.mdi-emoticon-excited-outline::before{content:"\F69B"}.mdi-emoticon-frown::before{content:"\FF69"}.mdi-emoticon-frown-outline::before{content:"\FF6A"}.mdi-emoticon-happy::before{content:"\FC4D"}.mdi-emoticon-happy-outline::before{content:"\F1F5"}.mdi-emoticon-kiss::before{content:"\FC4E"}.mdi-emoticon-kiss-outline::before{content:"\FC4F"}.mdi-emoticon-lol::before{content:"\F023F"}.mdi-emoticon-lol-outline::before{content:"\F0240"}.mdi-emoticon-neutral::before{content:"\FC50"}.mdi-emoticon-neutral-outline::before{content:"\F1F6"}.mdi-emoticon-outline::before{content:"\F1F2"}.mdi-emoticon-poop::before{content:"\F1F7"}.mdi-emoticon-poop-outline::before{content:"\FC51"}.mdi-emoticon-sad::before{content:"\FC52"}.mdi-emoticon-sad-outline::before{content:"\F1F8"}.mdi-emoticon-tongue::before{content:"\F1F9"}.mdi-emoticon-tongue-outline::before{content:"\FC53"}.mdi-emoticon-wink::before{content:"\FC54"}.mdi-emoticon-wink-outline::before{content:"\FC55"}.mdi-engine::before{content:"\F1FA"}.mdi-engine-off::before{content:"\FA45"}.mdi-engine-off-outline::before{content:"\FA46"}.mdi-engine-outline::before{content:"\F1FB"}.mdi-epsilon::before{content:"\F010B"}.mdi-equal::before{content:"\F1FC"}.mdi-equal-box::before{content:"\F1FD"}.mdi-equalizer::before{content:"\FEBF"}.mdi-equalizer-outline::before{content:"\FEC0"}.mdi-eraser::before{content:"\F1FE"}.mdi-eraser-variant::before{content:"\F642"}.mdi-escalator::before{content:"\F1FF"}.mdi-escalator-down::before{content:"\F02EB"}.mdi-escalator-up::before{content:"\F02EA"}.mdi-eslint::before{content:"\FC56"}.mdi-et::before{content:"\FAB2"}.mdi-ethereum::before{content:"\F869"}.mdi-ethernet::before{content:"\F200"}.mdi-ethernet-cable::before{content:"\F201"}.mdi-ethernet-cable-off::before{content:"\F202"}.mdi-etsy::before{content:"\F203"}.mdi-ev-station::before{content:"\F5F1"}.mdi-eventbrite::before{content:"\F7C6"}.mdi-evernote::before{content:"\F204"}.mdi-excavator::before{content:"\F0047"}.mdi-exclamation::before{content:"\F205"}.mdi-exclamation-thick::before{content:"\F0263"}.mdi-exit-run::before{content:"\FA47"}.mdi-exit-to-app::before{content:"\F206"}.mdi-expand-all::before{content:"\FAB3"}.mdi-expand-all-outline::before{content:"\FAB4"}.mdi-expansion-card::before{content:"\F8AD"}.mdi-expansion-card-variant::before{content:"\FFD2"}.mdi-exponent::before{content:"\F962"}.mdi-exponent-box::before{content:"\F963"}.mdi-export::before{content:"\F207"}.mdi-export-variant::before{content:"\FB6F"}.mdi-eye::before{content:"\F208"}.mdi-eye-check::before{content:"\FCE0"}.mdi-eye-check-outline::before{content:"\FCE1"}.mdi-eye-circle::before{content:"\FB70"}.mdi-eye-circle-outline::before{content:"\FB71"}.mdi-eye-minus::before{content:"\F0048"}.mdi-eye-minus-outline::before{content:"\F0049"}.mdi-eye-off::before{content:"\F209"}.mdi-eye-off-outline::before{content:"\F6D0"}.mdi-eye-outline::before{content:"\F6CF"}.mdi-eye-plus::before{content:"\F86A"}.mdi-eye-plus-outline::before{content:"\F86B"}.mdi-eye-settings::before{content:"\F86C"}.mdi-eye-settings-outline::before{content:"\F86D"}.mdi-eyedropper::before{content:"\F20A"}.mdi-eyedropper-variant::before{content:"\F20B"}.mdi-face::before{content:"\F643"}.mdi-face-agent::before{content:"\FD4C"}.mdi-face-outline::before{content:"\FB72"}.mdi-face-profile::before{content:"\F644"}.mdi-face-profile-woman::before{content:"\F00A1"}.mdi-face-recognition::before{content:"\FC57"}.mdi-face-woman::before{content:"\F00A2"}.mdi-face-woman-outline::before{content:"\F00A3"}.mdi-facebook::before{content:"\F20C"}.mdi-facebook-box::before{content:"\F20D"}.mdi-facebook-messenger::before{content:"\F20E"}.mdi-facebook-workplace::before{content:"\FB16"}.mdi-factory::before{content:"\F20F"}.mdi-fan::before{content:"\F210"}.mdi-fan-off::before{content:"\F81C"}.mdi-fast-forward::before{content:"\F211"}.mdi-fast-forward-10::before{content:"\FD4D"}.mdi-fast-forward-30::before{content:"\FCE2"}.mdi-fast-forward-5::before{content:"\F0223"}.mdi-fast-forward-outline::before{content:"\F6D1"}.mdi-fax::before{content:"\F212"}.mdi-feather::before{content:"\F6D2"}.mdi-feature-search::before{content:"\FA48"}.mdi-feature-search-outline::before{content:"\FA49"}.mdi-fedora::before{content:"\F8DA"}.mdi-ferris-wheel::before{content:"\FEC1"}.mdi-ferry::before{content:"\F213"}.mdi-file::before{content:"\F214"}.mdi-file-account::before{content:"\F73A"}.mdi-file-account-outline::before{content:"\F004A"}.mdi-file-alert::before{content:"\FA4A"}.mdi-file-alert-outline::before{content:"\FA4B"}.mdi-file-cabinet::before{content:"\FAB5"}.mdi-file-cad::before{content:"\FF08"}.mdi-file-cad-box::before{content:"\FF09"}.mdi-file-cancel::before{content:"\FDA2"}.mdi-file-cancel-outline::before{content:"\FDA3"}.mdi-file-certificate::before{content:"\F01B1"}.mdi-file-certificate-outline::before{content:"\F01B2"}.mdi-file-chart::before{content:"\F215"}.mdi-file-chart-outline::before{content:"\F004B"}.mdi-file-check::before{content:"\F216"}.mdi-file-check-outline::before{content:"\FE7B"}.mdi-file-clock::before{content:"\F030C"}.mdi-file-clock-outline::before{content:"\F030D"}.mdi-file-cloud::before{content:"\F217"}.mdi-file-cloud-outline::before{content:"\F004C"}.mdi-file-code::before{content:"\F22E"}.mdi-file-code-outline::before{content:"\F004D"}.mdi-file-compare::before{content:"\F8A9"}.mdi-file-delimited::before{content:"\F218"}.mdi-file-delimited-outline::before{content:"\FEC2"}.mdi-file-document::before{content:"\F219"}.mdi-file-document-box::before{content:"\F21A"}.mdi-file-document-box-check::before{content:"\FEC3"}.mdi-file-document-box-check-outline::before{content:"\FEC4"}.mdi-file-document-box-minus::before{content:"\FEC5"}.mdi-file-document-box-minus-outline::before{content:"\FEC6"}.mdi-file-document-box-multiple::before{content:"\FAB6"}.mdi-file-document-box-multiple-outline::before{content:"\FAB7"}.mdi-file-document-box-outline::before{content:"\F9EC"}.mdi-file-document-box-plus::before{content:"\FEC7"}.mdi-file-document-box-plus-outline::before{content:"\FEC8"}.mdi-file-document-box-remove::before{content:"\FEC9"}.mdi-file-document-box-remove-outline::before{content:"\FECA"}.mdi-file-document-box-search::before{content:"\FECB"}.mdi-file-document-box-search-outline::before{content:"\FECC"}.mdi-file-document-edit::before{content:"\FDA4"}.mdi-file-document-edit-outline::before{content:"\FDA5"}.mdi-file-document-outline::before{content:"\F9ED"}.mdi-file-download::before{content:"\F964"}.mdi-file-download-outline::before{content:"\F965"}.mdi-file-edit::before{content:"\F0212"}.mdi-file-edit-outline::before{content:"\F0213"}.mdi-file-excel::before{content:"\F21B"}.mdi-file-excel-box::before{content:"\F21C"}.mdi-file-excel-box-outline::before{content:"\F004E"}.mdi-file-excel-outline::before{content:"\F004F"}.mdi-file-export::before{content:"\F21D"}.mdi-file-export-outline::before{content:"\F0050"}.mdi-file-eye::before{content:"\FDA6"}.mdi-file-eye-outline::before{content:"\FDA7"}.mdi-file-find::before{content:"\F21E"}.mdi-file-find-outline::before{content:"\FB73"}.mdi-file-hidden::before{content:"\F613"}.mdi-file-image::before{content:"\F21F"}.mdi-file-image-outline::before{content:"\FECD"}.mdi-file-import::before{content:"\F220"}.mdi-file-import-outline::before{content:"\F0051"}.mdi-file-key::before{content:"\F01AF"}.mdi-file-key-outline::before{content:"\F01B0"}.mdi-file-link::before{content:"\F01A2"}.mdi-file-link-outline::before{content:"\F01A3"}.mdi-file-lock::before{content:"\F221"}.mdi-file-lock-outline::before{content:"\F0052"}.mdi-file-move::before{content:"\FAB8"}.mdi-file-move-outline::before{content:"\F0053"}.mdi-file-multiple::before{content:"\F222"}.mdi-file-multiple-outline::before{content:"\F0054"}.mdi-file-music::before{content:"\F223"}.mdi-file-music-outline::before{content:"\FE7C"}.mdi-file-outline::before{content:"\F224"}.mdi-file-pdf::before{content:"\F225"}.mdi-file-pdf-box::before{content:"\F226"}.mdi-file-pdf-box-outline::before{content:"\FFD3"}.mdi-file-pdf-outline::before{content:"\FE7D"}.mdi-file-percent::before{content:"\F81D"}.mdi-file-percent-outline::before{content:"\F0055"}.mdi-file-phone::before{content:"\F01A4"}.mdi-file-phone-outline::before{content:"\F01A5"}.mdi-file-plus::before{content:"\F751"}.mdi-file-plus-outline::before{content:"\FF0A"}.mdi-file-powerpoint::before{content:"\F227"}.mdi-file-powerpoint-box::before{content:"\F228"}.mdi-file-powerpoint-box-outline::before{content:"\F0056"}.mdi-file-powerpoint-outline::before{content:"\F0057"}.mdi-file-presentation-box::before{content:"\F229"}.mdi-file-question::before{content:"\F86E"}.mdi-file-question-outline::before{content:"\F0058"}.mdi-file-remove::before{content:"\FB74"}.mdi-file-remove-outline::before{content:"\F0059"}.mdi-file-replace::before{content:"\FB17"}.mdi-file-replace-outline::before{content:"\FB18"}.mdi-file-restore::before{content:"\F670"}.mdi-file-restore-outline::before{content:"\F005A"}.mdi-file-search::before{content:"\FC58"}.mdi-file-search-outline::before{content:"\FC59"}.mdi-file-send::before{content:"\F22A"}.mdi-file-send-outline::before{content:"\F005B"}.mdi-file-settings::before{content:"\F00A4"}.mdi-file-settings-outline::before{content:"\F00A5"}.mdi-file-settings-variant::before{content:"\F00A6"}.mdi-file-settings-variant-outline::before{content:"\F00A7"}.mdi-file-star::before{content:"\F005C"}.mdi-file-star-outline::before{content:"\F005D"}.mdi-file-swap::before{content:"\FFD4"}.mdi-file-swap-outline::before{content:"\FFD5"}.mdi-file-sync::before{content:"\F0241"}.mdi-file-sync-outline::before{content:"\F0242"}.mdi-file-table::before{content:"\FC5A"}.mdi-file-table-box::before{content:"\F010C"}.mdi-file-table-box-multiple::before{content:"\F010D"}.mdi-file-table-box-multiple-outline::before{content:"\F010E"}.mdi-file-table-box-outline::before{content:"\F010F"}.mdi-file-table-outline::before{content:"\FC5B"}.mdi-file-tree::before{content:"\F645"}.mdi-file-undo::before{content:"\F8DB"}.mdi-file-undo-outline::before{content:"\F005E"}.mdi-file-upload::before{content:"\FA4C"}.mdi-file-upload-outline::before{content:"\FA4D"}.mdi-file-video::before{content:"\F22B"}.mdi-file-video-outline::before{content:"\FE10"}.mdi-file-word::before{content:"\F22C"}.mdi-file-word-box::before{content:"\F22D"}.mdi-file-word-box-outline::before{content:"\F005F"}.mdi-file-word-outline::before{content:"\F0060"}.mdi-film::before{content:"\F22F"}.mdi-filmstrip::before{content:"\F230"}.mdi-filmstrip-off::before{content:"\F231"}.mdi-filter::before{content:"\F232"}.mdi-filter-menu::before{content:"\F0110"}.mdi-filter-menu-outline::before{content:"\F0111"}.mdi-filter-minus::before{content:"\FF0B"}.mdi-filter-minus-outline::before{content:"\FF0C"}.mdi-filter-outline::before{content:"\F233"}.mdi-filter-plus::before{content:"\FF0D"}.mdi-filter-plus-outline::before{content:"\FF0E"}.mdi-filter-remove::before{content:"\F234"}.mdi-filter-remove-outline::before{content:"\F235"}.mdi-filter-variant::before{content:"\F236"}.mdi-filter-variant-minus::before{content:"\F013D"}.mdi-filter-variant-plus::before{content:"\F013E"}.mdi-filter-variant-remove::before{content:"\F0061"}.mdi-finance::before{content:"\F81E"}.mdi-find-replace::before{content:"\F6D3"}.mdi-fingerprint::before{content:"\F237"}.mdi-fingerprint-off::before{content:"\FECE"}.mdi-fire::before{content:"\F238"}.mdi-fire-extinguisher::before{content:"\FF0F"}.mdi-fire-hydrant::before{content:"\F0162"}.mdi-fire-hydrant-alert::before{content:"\F0163"}.mdi-fire-hydrant-off::before{content:"\F0164"}.mdi-fire-truck::before{content:"\F8AA"}.mdi-firebase::before{content:"\F966"}.mdi-firefox::before{content:"\F239"}.mdi-fireplace::before{content:"\FE11"}.mdi-fireplace-off::before{content:"\FE12"}.mdi-firework::before{content:"\FE13"}.mdi-fish::before{content:"\F23A"}.mdi-fishbowl::before{content:"\FF10"}.mdi-fishbowl-outline::before{content:"\FF11"}.mdi-fit-to-page::before{content:"\FF12"}.mdi-fit-to-page-outline::before{content:"\FF13"}.mdi-flag::before{content:"\F23B"}.mdi-flag-checkered::before{content:"\F23C"}.mdi-flag-minus::before{content:"\FB75"}.mdi-flag-minus-outline::before{content:"\F00DD"}.mdi-flag-outline::before{content:"\F23D"}.mdi-flag-plus::before{content:"\FB76"}.mdi-flag-plus-outline::before{content:"\F00DE"}.mdi-flag-remove::before{content:"\FB77"}.mdi-flag-remove-outline::before{content:"\F00DF"}.mdi-flag-triangle::before{content:"\F23F"}.mdi-flag-variant::before{content:"\F240"}.mdi-flag-variant-outline::before{content:"\F23E"}.mdi-flare::before{content:"\FD4E"}.mdi-flash::before{content:"\F241"}.mdi-flash-alert::before{content:"\FF14"}.mdi-flash-alert-outline::before{content:"\FF15"}.mdi-flash-auto::before{content:"\F242"}.mdi-flash-circle::before{content:"\F81F"}.mdi-flash-off::before{content:"\F243"}.mdi-flash-outline::before{content:"\F6D4"}.mdi-flash-red-eye::before{content:"\F67A"}.mdi-flashlight::before{content:"\F244"}.mdi-flashlight-off::before{content:"\F245"}.mdi-flask::before{content:"\F093"}.mdi-flask-empty::before{content:"\F094"}.mdi-flask-empty-minus::before{content:"\F0265"}.mdi-flask-empty-minus-outline::before{content:"\F0266"}.mdi-flask-empty-outline::before{content:"\F095"}.mdi-flask-empty-plus::before{content:"\F0267"}.mdi-flask-empty-plus-outline::before{content:"\F0268"}.mdi-flask-empty-remove::before{content:"\F0269"}.mdi-flask-empty-remove-outline::before{content:"\F026A"}.mdi-flask-minus::before{content:"\F026B"}.mdi-flask-minus-outline::before{content:"\F026C"}.mdi-flask-outline::before{content:"\F096"}.mdi-flask-plus::before{content:"\F026D"}.mdi-flask-plus-outline::before{content:"\F026E"}.mdi-flask-remove::before{content:"\F026F"}.mdi-flask-remove-outline::before{content:"\F0270"}.mdi-flask-round-bottom::before{content:"\F0276"}.mdi-flask-round-bottom-empty::before{content:"\F0277"}.mdi-flask-round-bottom-empty-outline::before{content:"\F0278"}.mdi-flask-round-bottom-outline::before{content:"\F0279"}.mdi-flattr::before{content:"\F246"}.mdi-fleur-de-lis::before{content:"\F032E"}.mdi-flickr::before{content:"\FCE3"}.mdi-flip-horizontal::before{content:"\F0112"}.mdi-flip-to-back::before{content:"\F247"}.mdi-flip-to-front::before{content:"\F248"}.mdi-flip-vertical::before{content:"\F0113"}.mdi-floor-lamp::before{content:"\F8DC"}.mdi-floor-lamp-dual::before{content:"\F0062"}.mdi-floor-lamp-variant::before{content:"\F0063"}.mdi-floor-plan::before{content:"\F820"}.mdi-floppy::before{content:"\F249"}.mdi-floppy-variant::before{content:"\F9EE"}.mdi-flower::before{content:"\F24A"}.mdi-flower-outline::before{content:"\F9EF"}.mdi-flower-poppy::before{content:"\FCE4"}.mdi-flower-tulip::before{content:"\F9F0"}.mdi-flower-tulip-outline::before{content:"\F9F1"}.mdi-focus-auto::before{content:"\FF6B"}.mdi-focus-field::before{content:"\FF6C"}.mdi-focus-field-horizontal::before{content:"\FF6D"}.mdi-focus-field-vertical::before{content:"\FF6E"}.mdi-folder::before{content:"\F24B"}.mdi-folder-account::before{content:"\F24C"}.mdi-folder-account-outline::before{content:"\FB78"}.mdi-folder-alert::before{content:"\FDA8"}.mdi-folder-alert-outline::before{content:"\FDA9"}.mdi-folder-clock::before{content:"\FAB9"}.mdi-folder-clock-outline::before{content:"\FABA"}.mdi-folder-download::before{content:"\F24D"}.mdi-folder-download-outline::before{content:"\F0114"}.mdi-folder-edit::before{content:"\F8DD"}.mdi-folder-edit-outline::before{content:"\FDAA"}.mdi-folder-google-drive::before{content:"\F24E"}.mdi-folder-heart::before{content:"\F0115"}.mdi-folder-heart-outline::before{content:"\F0116"}.mdi-folder-home::before{content:"\F00E0"}.mdi-folder-home-outline::before{content:"\F00E1"}.mdi-folder-image::before{content:"\F24F"}.mdi-folder-information::before{content:"\F00E2"}.mdi-folder-information-outline::before{content:"\F00E3"}.mdi-folder-key::before{content:"\F8AB"}.mdi-folder-key-network::before{content:"\F8AC"}.mdi-folder-key-network-outline::before{content:"\FC5C"}.mdi-folder-key-outline::before{content:"\F0117"}.mdi-folder-lock::before{content:"\F250"}.mdi-folder-lock-open::before{content:"\F251"}.mdi-folder-marker::before{content:"\F0298"}.mdi-folder-marker-outline::before{content:"\F0299"}.mdi-folder-move::before{content:"\F252"}.mdi-folder-move-outline::before{content:"\F0271"}.mdi-folder-multiple::before{content:"\F253"}.mdi-folder-multiple-image::before{content:"\F254"}.mdi-folder-multiple-outline::before{content:"\F255"}.mdi-folder-music::before{content:"\F0384"}.mdi-folder-music-outline::before{content:"\F0385"}.mdi-folder-network::before{content:"\F86F"}.mdi-folder-network-outline::before{content:"\FC5D"}.mdi-folder-open::before{content:"\F76F"}.mdi-folder-open-outline::before{content:"\FDAB"}.mdi-folder-outline::before{content:"\F256"}.mdi-folder-plus::before{content:"\F257"}.mdi-folder-plus-outline::before{content:"\FB79"}.mdi-folder-pound::before{content:"\FCE5"}.mdi-folder-pound-outline::before{content:"\FCE6"}.mdi-folder-remove::before{content:"\F258"}.mdi-folder-remove-outline::before{content:"\FB7A"}.mdi-folder-search::before{content:"\F967"}.mdi-folder-search-outline::before{content:"\F968"}.mdi-folder-settings::before{content:"\F00A8"}.mdi-folder-settings-outline::before{content:"\F00A9"}.mdi-folder-settings-variant::before{content:"\F00AA"}.mdi-folder-settings-variant-outline::before{content:"\F00AB"}.mdi-folder-star::before{content:"\F69C"}.mdi-folder-star-outline::before{content:"\FB7B"}.mdi-folder-swap::before{content:"\FFD6"}.mdi-folder-swap-outline::before{content:"\FFD7"}.mdi-folder-sync::before{content:"\FCE7"}.mdi-folder-sync-outline::before{content:"\FCE8"}.mdi-folder-table::before{content:"\F030E"}.mdi-folder-table-outline::before{content:"\F030F"}.mdi-folder-text::before{content:"\FC5E"}.mdi-folder-text-outline::before{content:"\FC5F"}.mdi-folder-upload::before{content:"\F259"}.mdi-folder-upload-outline::before{content:"\F0118"}.mdi-folder-zip::before{content:"\F6EA"}.mdi-folder-zip-outline::before{content:"\F7B8"}.mdi-font-awesome::before{content:"\F03A"}.mdi-food::before{content:"\F25A"}.mdi-food-apple::before{content:"\F25B"}.mdi-food-apple-outline::before{content:"\FC60"}.mdi-food-croissant::before{content:"\F7C7"}.mdi-food-fork-drink::before{content:"\F5F2"}.mdi-food-off::before{content:"\F5F3"}.mdi-food-variant::before{content:"\F25C"}.mdi-foot-print::before{content:"\FF6F"}.mdi-football::before{content:"\F25D"}.mdi-football-australian::before{content:"\F25E"}.mdi-football-helmet::before{content:"\F25F"}.mdi-forklift::before{content:"\F7C8"}.mdi-format-align-bottom::before{content:"\F752"}.mdi-format-align-center::before{content:"\F260"}.mdi-format-align-justify::before{content:"\F261"}.mdi-format-align-left::before{content:"\F262"}.mdi-format-align-middle::before{content:"\F753"}.mdi-format-align-right::before{content:"\F263"}.mdi-format-align-top::before{content:"\F754"}.mdi-format-annotation-minus::before{content:"\FABB"}.mdi-format-annotation-plus::before{content:"\F646"}.mdi-format-bold::before{content:"\F264"}.mdi-format-clear::before{content:"\F265"}.mdi-format-color-fill::before{content:"\F266"}.mdi-format-color-highlight::before{content:"\FE14"}.mdi-format-color-marker-cancel::before{content:"\F033E"}.mdi-format-color-text::before{content:"\F69D"}.mdi-format-columns::before{content:"\F8DE"}.mdi-format-float-center::before{content:"\F267"}.mdi-format-float-left::before{content:"\F268"}.mdi-format-float-none::before{content:"\F269"}.mdi-format-float-right::before{content:"\F26A"}.mdi-format-font::before{content:"\F6D5"}.mdi-format-font-size-decrease::before{content:"\F9F2"}.mdi-format-font-size-increase::before{content:"\F9F3"}.mdi-format-header-1::before{content:"\F26B"}.mdi-format-header-2::before{content:"\F26C"}.mdi-format-header-3::before{content:"\F26D"}.mdi-format-header-4::before{content:"\F26E"}.mdi-format-header-5::before{content:"\F26F"}.mdi-format-header-6::before{content:"\F270"}.mdi-format-header-decrease::before{content:"\F271"}.mdi-format-header-equal::before{content:"\F272"}.mdi-format-header-increase::before{content:"\F273"}.mdi-format-header-pound::before{content:"\F274"}.mdi-format-horizontal-align-center::before{content:"\F61E"}.mdi-format-horizontal-align-left::before{content:"\F61F"}.mdi-format-horizontal-align-right::before{content:"\F620"}.mdi-format-indent-decrease::before{content:"\F275"}.mdi-format-indent-increase::before{content:"\F276"}.mdi-format-italic::before{content:"\F277"}.mdi-format-letter-case::before{content:"\FB19"}.mdi-format-letter-case-lower::before{content:"\FB1A"}.mdi-format-letter-case-upper::before{content:"\FB1B"}.mdi-format-letter-ends-with::before{content:"\FFD8"}.mdi-format-letter-matches::before{content:"\FFD9"}.mdi-format-letter-starts-with::before{content:"\FFDA"}.mdi-format-line-spacing::before{content:"\F278"}.mdi-format-line-style::before{content:"\F5C8"}.mdi-format-line-weight::before{content:"\F5C9"}.mdi-format-list-bulleted::before{content:"\F279"}.mdi-format-list-bulleted-square::before{content:"\FDAC"}.mdi-format-list-bulleted-triangle::before{content:"\FECF"}.mdi-format-list-bulleted-type::before{content:"\F27A"}.mdi-format-list-checkbox::before{content:"\F969"}.mdi-format-list-checks::before{content:"\F755"}.mdi-format-list-numbered::before{content:"\F27B"}.mdi-format-list-numbered-rtl::before{content:"\FCE9"}.mdi-format-list-text::before{content:"\F029A"}.mdi-format-overline::before{content:"\FED0"}.mdi-format-page-break::before{content:"\F6D6"}.mdi-format-paint::before{content:"\F27C"}.mdi-format-paragraph::before{content:"\F27D"}.mdi-format-pilcrow::before{content:"\F6D7"}.mdi-format-quote-close::before{content:"\F27E"}.mdi-format-quote-close-outline::before{content:"\F01D3"}.mdi-format-quote-open::before{content:"\F756"}.mdi-format-quote-open-outline::before{content:"\F01D2"}.mdi-format-rotate-90::before{content:"\F6A9"}.mdi-format-section::before{content:"\F69E"}.mdi-format-size::before{content:"\F27F"}.mdi-format-strikethrough::before{content:"\F280"}.mdi-format-strikethrough-variant::before{content:"\F281"}.mdi-format-subscript::before{content:"\F282"}.mdi-format-superscript::before{content:"\F283"}.mdi-format-text::before{content:"\F284"}.mdi-format-text-rotation-angle-down::before{content:"\FFDB"}.mdi-format-text-rotation-angle-up::before{content:"\FFDC"}.mdi-format-text-rotation-down::before{content:"\FD4F"}.mdi-format-text-rotation-down-vertical::before{content:"\FFDD"}.mdi-format-text-rotation-none::before{content:"\FD50"}.mdi-format-text-rotation-up::before{content:"\FFDE"}.mdi-format-text-rotation-vertical::before{content:"\FFDF"}.mdi-format-text-variant::before{content:"\FE15"}.mdi-format-text-wrapping-clip::before{content:"\FCEA"}.mdi-format-text-wrapping-overflow::before{content:"\FCEB"}.mdi-format-text-wrapping-wrap::before{content:"\FCEC"}.mdi-format-textbox::before{content:"\FCED"}.mdi-format-textdirection-l-to-r::before{content:"\F285"}.mdi-format-textdirection-r-to-l::before{content:"\F286"}.mdi-format-title::before{content:"\F5F4"}.mdi-format-underline::before{content:"\F287"}.mdi-format-vertical-align-bottom::before{content:"\F621"}.mdi-format-vertical-align-center::before{content:"\F622"}.mdi-format-vertical-align-top::before{content:"\F623"}.mdi-format-wrap-inline::before{content:"\F288"}.mdi-format-wrap-square::before{content:"\F289"}.mdi-format-wrap-tight::before{content:"\F28A"}.mdi-format-wrap-top-bottom::before{content:"\F28B"}.mdi-forum::before{content:"\F28C"}.mdi-forum-outline::before{content:"\F821"}.mdi-forward::before{content:"\F28D"}.mdi-forwardburger::before{content:"\FD51"}.mdi-fountain::before{content:"\F96A"}.mdi-fountain-pen::before{content:"\FCEE"}.mdi-fountain-pen-tip::before{content:"\FCEF"}.mdi-foursquare::before{content:"\F28E"}.mdi-freebsd::before{content:"\F8DF"}.mdi-frequently-asked-questions::before{content:"\FED1"}.mdi-fridge::before{content:"\F290"}.mdi-fridge-alert::before{content:"\F01DC"}.mdi-fridge-alert-outline::before{content:"\F01DD"}.mdi-fridge-bottom::before{content:"\F292"}.mdi-fridge-off::before{content:"\F01DA"}.mdi-fridge-off-outline::before{content:"\F01DB"}.mdi-fridge-outline::before{content:"\F28F"}.mdi-fridge-top::before{content:"\F291"}.mdi-fruit-cherries::before{content:"\F0064"}.mdi-fruit-citrus::before{content:"\F0065"}.mdi-fruit-grapes::before{content:"\F0066"}.mdi-fruit-grapes-outline::before{content:"\F0067"}.mdi-fruit-pineapple::before{content:"\F0068"}.mdi-fruit-watermelon::before{content:"\F0069"}.mdi-fuel::before{content:"\F7C9"}.mdi-fullscreen::before{content:"\F293"}.mdi-fullscreen-exit::before{content:"\F294"}.mdi-function::before{content:"\F295"}.mdi-function-variant::before{content:"\F870"}.mdi-furigana-horizontal::before{content:"\F00AC"}.mdi-furigana-vertical::before{content:"\F00AD"}.mdi-fuse::before{content:"\FC61"}.mdi-fuse-blade::before{content:"\FC62"}.mdi-gamepad::before{content:"\F296"}.mdi-gamepad-circle::before{content:"\FE16"}.mdi-gamepad-circle-down::before{content:"\FE17"}.mdi-gamepad-circle-left::before{content:"\FE18"}.mdi-gamepad-circle-outline::before{content:"\FE19"}.mdi-gamepad-circle-right::before{content:"\FE1A"}.mdi-gamepad-circle-up::before{content:"\FE1B"}.mdi-gamepad-down::before{content:"\FE1C"}.mdi-gamepad-left::before{content:"\FE1D"}.mdi-gamepad-right::before{content:"\FE1E"}.mdi-gamepad-round::before{content:"\FE1F"}.mdi-gamepad-round-down::before{content:"\FE7E"}.mdi-gamepad-round-left::before{content:"\FE7F"}.mdi-gamepad-round-outline::before{content:"\FE80"}.mdi-gamepad-round-right::before{content:"\FE81"}.mdi-gamepad-round-up::before{content:"\FE82"}.mdi-gamepad-square::before{content:"\FED2"}.mdi-gamepad-square-outline::before{content:"\FED3"}.mdi-gamepad-up::before{content:"\FE83"}.mdi-gamepad-variant::before{content:"\F297"}.mdi-gamepad-variant-outline::before{content:"\FED4"}.mdi-gamma::before{content:"\F0119"}.mdi-gantry-crane::before{content:"\FDAD"}.mdi-garage::before{content:"\F6D8"}.mdi-garage-alert::before{content:"\F871"}.mdi-garage-alert-variant::before{content:"\F0300"}.mdi-garage-open::before{content:"\F6D9"}.mdi-garage-open-variant::before{content:"\F02FF"}.mdi-garage-variant::before{content:"\F02FE"}.mdi-gas-cylinder::before{content:"\F647"}.mdi-gas-station::before{content:"\F298"}.mdi-gas-station-outline::before{content:"\FED5"}.mdi-gate::before{content:"\F299"}.mdi-gate-and::before{content:"\F8E0"}.mdi-gate-arrow-right::before{content:"\F0194"}.mdi-gate-nand::before{content:"\F8E1"}.mdi-gate-nor::before{content:"\F8E2"}.mdi-gate-not::before{content:"\F8E3"}.mdi-gate-open::before{content:"\F0195"}.mdi-gate-or::before{content:"\F8E4"}.mdi-gate-xnor::before{content:"\F8E5"}.mdi-gate-xor::before{content:"\F8E6"}.mdi-gatsby::before{content:"\FE84"}.mdi-gauge::before{content:"\F29A"}.mdi-gauge-empty::before{content:"\F872"}.mdi-gauge-full::before{content:"\F873"}.mdi-gauge-low::before{content:"\F874"}.mdi-gavel::before{content:"\F29B"}.mdi-gender-female::before{content:"\F29C"}.mdi-gender-male::before{content:"\F29D"}.mdi-gender-male-female::before{content:"\F29E"}.mdi-gender-male-female-variant::before{content:"\F016A"}.mdi-gender-non-binary::before{content:"\F016B"}.mdi-gender-transgender::before{content:"\F29F"}.mdi-gentoo::before{content:"\F8E7"}.mdi-gesture::before{content:"\F7CA"}.mdi-gesture-double-tap::before{content:"\F73B"}.mdi-gesture-pinch::before{content:"\FABC"}.mdi-gesture-spread::before{content:"\FABD"}.mdi-gesture-swipe::before{content:"\FD52"}.mdi-gesture-swipe-down::before{content:"\F73C"}.mdi-gesture-swipe-horizontal::before{content:"\FABE"}.mdi-gesture-swipe-left::before{content:"\F73D"}.mdi-gesture-swipe-right::before{content:"\F73E"}.mdi-gesture-swipe-up::before{content:"\F73F"}.mdi-gesture-swipe-vertical::before{content:"\FABF"}.mdi-gesture-tap::before{content:"\F740"}.mdi-gesture-tap-box::before{content:"\F02D4"}.mdi-gesture-tap-button::before{content:"\F02D3"}.mdi-gesture-tap-hold::before{content:"\FD53"}.mdi-gesture-two-double-tap::before{content:"\F741"}.mdi-gesture-two-tap::before{content:"\F742"}.mdi-ghost::before{content:"\F2A0"}.mdi-ghost-off::before{content:"\F9F4"}.mdi-gif::before{content:"\FD54"}.mdi-gift::before{content:"\FE85"}.mdi-gift-outline::before{content:"\F2A1"}.mdi-git::before{content:"\F2A2"}.mdi-github-box::before{content:"\F2A3"}.mdi-github-circle::before{content:"\F2A4"}.mdi-github-face::before{content:"\F6DA"}.mdi-gitlab::before{content:"\FB7C"}.mdi-glass-cocktail::before{content:"\F356"}.mdi-glass-flute::before{content:"\F2A5"}.mdi-glass-mug::before{content:"\F2A6"}.mdi-glass-mug-variant::before{content:"\F0141"}.mdi-glass-pint-outline::before{content:"\F0338"}.mdi-glass-stange::before{content:"\F2A7"}.mdi-glass-tulip::before{content:"\F2A8"}.mdi-glass-wine::before{content:"\F875"}.mdi-glassdoor::before{content:"\F2A9"}.mdi-glasses::before{content:"\F2AA"}.mdi-globe-light::before{content:"\F0302"}.mdi-globe-model::before{content:"\F8E8"}.mdi-gmail::before{content:"\F2AB"}.mdi-gnome::before{content:"\F2AC"}.mdi-go-kart::before{content:"\FD55"}.mdi-go-kart-track::before{content:"\FD56"}.mdi-gog::before{content:"\FB7D"}.mdi-gold::before{content:"\F027A"}.mdi-golf::before{content:"\F822"}.mdi-golf-cart::before{content:"\F01CF"}.mdi-golf-tee::before{content:"\F00AE"}.mdi-gondola::before{content:"\F685"}.mdi-goodreads::before{content:"\FD57"}.mdi-google::before{content:"\F2AD"}.mdi-google-adwords::before{content:"\FC63"}.mdi-google-analytics::before{content:"\F7CB"}.mdi-google-assistant::before{content:"\F7CC"}.mdi-google-cardboard::before{content:"\F2AE"}.mdi-google-chrome::before{content:"\F2AF"}.mdi-google-circles::before{content:"\F2B0"}.mdi-google-circles-communities::before{content:"\F2B1"}.mdi-google-circles-extended::before{content:"\F2B2"}.mdi-google-circles-group::before{content:"\F2B3"}.mdi-google-classroom::before{content:"\F2C0"}.mdi-google-cloud::before{content:"\F0221"}.mdi-google-controller::before{content:"\F2B4"}.mdi-google-controller-off::before{content:"\F2B5"}.mdi-google-downasaur::before{content:"\F038D"}.mdi-google-drive::before{content:"\F2B6"}.mdi-google-earth::before{content:"\F2B7"}.mdi-google-fit::before{content:"\F96B"}.mdi-google-glass::before{content:"\F2B8"}.mdi-google-hangouts::before{content:"\F2C9"}.mdi-google-home::before{content:"\F823"}.mdi-google-keep::before{content:"\F6DB"}.mdi-google-lens::before{content:"\F9F5"}.mdi-google-maps::before{content:"\F5F5"}.mdi-google-my-business::before{content:"\F006A"}.mdi-google-nearby::before{content:"\F2B9"}.mdi-google-pages::before{content:"\F2BA"}.mdi-google-photos::before{content:"\F6DC"}.mdi-google-physical-web::before{content:"\F2BB"}.mdi-google-play::before{content:"\F2BC"}.mdi-google-plus::before{content:"\F2BD"}.mdi-google-plus-box::before{content:"\F2BE"}.mdi-google-podcast::before{content:"\FED6"}.mdi-google-spreadsheet::before{content:"\F9F6"}.mdi-google-street-view::before{content:"\FC64"}.mdi-google-translate::before{content:"\F2BF"}.mdi-gradient::before{content:"\F69F"}.mdi-grain::before{content:"\FD58"}.mdi-graph::before{content:"\F006B"}.mdi-graph-outline::before{content:"\F006C"}.mdi-graphql::before{content:"\F876"}.mdi-grave-stone::before{content:"\FB7E"}.mdi-grease-pencil::before{content:"\F648"}.mdi-greater-than::before{content:"\F96C"}.mdi-greater-than-or-equal::before{content:"\F96D"}.mdi-grid::before{content:"\F2C1"}.mdi-grid-large::before{content:"\F757"}.mdi-grid-off::before{content:"\F2C2"}.mdi-grill::before{content:"\FE86"}.mdi-grill-outline::before{content:"\F01B5"}.mdi-group::before{content:"\F2C3"}.mdi-guitar-acoustic::before{content:"\F770"}.mdi-guitar-electric::before{content:"\F2C4"}.mdi-guitar-pick::before{content:"\F2C5"}.mdi-guitar-pick-outline::before{content:"\F2C6"}.mdi-guy-fawkes-mask::before{content:"\F824"}.mdi-hackernews::before{content:"\F624"}.mdi-hail::before{content:"\FAC0"}.mdi-hair-dryer::before{content:"\F011A"}.mdi-hair-dryer-outline::before{content:"\F011B"}.mdi-halloween::before{content:"\FB7F"}.mdi-hamburger::before{content:"\F684"}.mdi-hammer::before{content:"\F8E9"}.mdi-hammer-screwdriver::before{content:"\F034D"}.mdi-hammer-wrench::before{content:"\F034E"}.mdi-hand::before{content:"\FA4E"}.mdi-hand-heart::before{content:"\F011C"}.mdi-hand-left::before{content:"\FE87"}.mdi-hand-okay::before{content:"\FA4F"}.mdi-hand-peace::before{content:"\FA50"}.mdi-hand-peace-variant::before{content:"\FA51"}.mdi-hand-pointing-down::before{content:"\FA52"}.mdi-hand-pointing-left::before{content:"\FA53"}.mdi-hand-pointing-right::before{content:"\F2C7"}.mdi-hand-pointing-up::before{content:"\FA54"}.mdi-hand-right::before{content:"\FE88"}.mdi-hand-saw::before{content:"\FE89"}.mdi-handball::before{content:"\FF70"}.mdi-handcuffs::before{content:"\F0169"}.mdi-handshake::before{content:"\F0243"}.mdi-hanger::before{content:"\F2C8"}.mdi-hard-hat::before{content:"\F96E"}.mdi-harddisk::before{content:"\F2CA"}.mdi-harddisk-plus::before{content:"\F006D"}.mdi-harddisk-remove::before{content:"\F006E"}.mdi-hat-fedora::before{content:"\FB80"}.mdi-hazard-lights::before{content:"\FC65"}.mdi-hdr::before{content:"\FD59"}.mdi-hdr-off::before{content:"\FD5A"}.mdi-head::before{content:"\F0389"}.mdi-head-alert::before{content:"\F0363"}.mdi-head-alert-outline::before{content:"\F0364"}.mdi-head-check::before{content:"\F0365"}.mdi-head-check-outline::before{content:"\F0366"}.mdi-head-cog::before{content:"\F0367"}.mdi-head-cog-outline::before{content:"\F0368"}.mdi-head-dots-horizontal::before{content:"\F0369"}.mdi-head-dots-horizontal-outline::before{content:"\F036A"}.mdi-head-flash::before{content:"\F036B"}.mdi-head-flash-outline::before{content:"\F036C"}.mdi-head-heart::before{content:"\F036D"}.mdi-head-heart-outline::before{content:"\F036E"}.mdi-head-lightbulb::before{content:"\F036F"}.mdi-head-lightbulb-outline::before{content:"\F0370"}.mdi-head-minus::before{content:"\F0371"}.mdi-head-minus-outline::before{content:"\F0372"}.mdi-head-outline::before{content:"\F038A"}.mdi-head-plus::before{content:"\F0373"}.mdi-head-plus-outline::before{content:"\F0374"}.mdi-head-question::before{content:"\F0375"}.mdi-head-question-outline::before{content:"\F0376"}.mdi-head-remove::before{content:"\F0377"}.mdi-head-remove-outline::before{content:"\F0378"}.mdi-head-snowflake::before{content:"\F0379"}.mdi-head-snowflake-outline::before{content:"\F037A"}.mdi-head-sync::before{content:"\F037B"}.mdi-head-sync-outline::before{content:"\F037C"}.mdi-headphones::before{content:"\F2CB"}.mdi-headphones-bluetooth::before{content:"\F96F"}.mdi-headphones-box::before{content:"\F2CC"}.mdi-headphones-off::before{content:"\F7CD"}.mdi-headphones-settings::before{content:"\F2CD"}.mdi-headset::before{content:"\F2CE"}.mdi-headset-dock::before{content:"\F2CF"}.mdi-headset-off::before{content:"\F2D0"}.mdi-heart::before{content:"\F2D1"}.mdi-heart-box::before{content:"\F2D2"}.mdi-heart-box-outline::before{content:"\F2D3"}.mdi-heart-broken::before{content:"\F2D4"}.mdi-heart-broken-outline::before{content:"\FCF0"}.mdi-heart-circle::before{content:"\F970"}.mdi-heart-circle-outline::before{content:"\F971"}.mdi-heart-flash::before{content:"\FF16"}.mdi-heart-half::before{content:"\F6DE"}.mdi-heart-half-full::before{content:"\F6DD"}.mdi-heart-half-outline::before{content:"\F6DF"}.mdi-heart-multiple::before{content:"\FA55"}.mdi-heart-multiple-outline::before{content:"\FA56"}.mdi-heart-off::before{content:"\F758"}.mdi-heart-outline::before{content:"\F2D5"}.mdi-heart-pulse::before{content:"\F5F6"}.mdi-helicopter::before{content:"\FAC1"}.mdi-help::before{content:"\F2D6"}.mdi-help-box::before{content:"\F78A"}.mdi-help-circle::before{content:"\F2D7"}.mdi-help-circle-outline::before{content:"\F625"}.mdi-help-network::before{content:"\F6F4"}.mdi-help-network-outline::before{content:"\FC66"}.mdi-help-rhombus::before{content:"\FB81"}.mdi-help-rhombus-outline::before{content:"\FB82"}.mdi-hexadecimal::before{content:"\F02D2"}.mdi-hexagon::before{content:"\F2D8"}.mdi-hexagon-multiple::before{content:"\F6E0"}.mdi-hexagon-multiple-outline::before{content:"\F011D"}.mdi-hexagon-outline::before{content:"\F2D9"}.mdi-hexagon-slice-1::before{content:"\FAC2"}.mdi-hexagon-slice-2::before{content:"\FAC3"}.mdi-hexagon-slice-3::before{content:"\FAC4"}.mdi-hexagon-slice-4::before{content:"\FAC5"}.mdi-hexagon-slice-5::before{content:"\FAC6"}.mdi-hexagon-slice-6::before{content:"\FAC7"}.mdi-hexagram::before{content:"\FAC8"}.mdi-hexagram-outline::before{content:"\FAC9"}.mdi-high-definition::before{content:"\F7CE"}.mdi-high-definition-box::before{content:"\F877"}.mdi-highway::before{content:"\F5F7"}.mdi-hiking::before{content:"\FD5B"}.mdi-hinduism::before{content:"\F972"}.mdi-history::before{content:"\F2DA"}.mdi-hockey-puck::before{content:"\F878"}.mdi-hockey-sticks::before{content:"\F879"}.mdi-hololens::before{content:"\F2DB"}.mdi-home::before{content:"\F2DC"}.mdi-home-account::before{content:"\F825"}.mdi-home-alert::before{content:"\F87A"}.mdi-home-analytics::before{content:"\FED7"}.mdi-home-assistant::before{content:"\F7CF"}.mdi-home-automation::before{content:"\F7D0"}.mdi-home-circle::before{content:"\F7D1"}.mdi-home-circle-outline::before{content:"\F006F"}.mdi-home-city::before{content:"\FCF1"}.mdi-home-city-outline::before{content:"\FCF2"}.mdi-home-currency-usd::before{content:"\F8AE"}.mdi-home-edit::before{content:"\F0184"}.mdi-home-edit-outline::before{content:"\F0185"}.mdi-home-export-outline::before{content:"\FFB8"}.mdi-home-flood::before{content:"\FF17"}.mdi-home-floor-0::before{content:"\FDAE"}.mdi-home-floor-1::before{content:"\FD5C"}.mdi-home-floor-2::before{content:"\FD5D"}.mdi-home-floor-3::before{content:"\FD5E"}.mdi-home-floor-a::before{content:"\FD5F"}.mdi-home-floor-b::before{content:"\FD60"}.mdi-home-floor-g::before{content:"\FD61"}.mdi-home-floor-l::before{content:"\FD62"}.mdi-home-floor-negative-1::before{content:"\FDAF"}.mdi-home-group::before{content:"\FDB0"}.mdi-home-heart::before{content:"\F826"}.mdi-home-import-outline::before{content:"\FFB9"}.mdi-home-lightbulb::before{content:"\F027C"}.mdi-home-lightbulb-outline::before{content:"\F027D"}.mdi-home-lock::before{content:"\F8EA"}.mdi-home-lock-open::before{content:"\F8EB"}.mdi-home-map-marker::before{content:"\F5F8"}.mdi-home-minus::before{content:"\F973"}.mdi-home-modern::before{content:"\F2DD"}.mdi-home-outline::before{content:"\F6A0"}.mdi-home-plus::before{content:"\F974"}.mdi-home-remove::before{content:"\F0272"}.mdi-home-roof::before{content:"\F0156"}.mdi-home-thermometer::before{content:"\FF71"}.mdi-home-thermometer-outline::before{content:"\FF72"}.mdi-home-variant::before{content:"\F2DE"}.mdi-home-variant-outline::before{content:"\FB83"}.mdi-hook::before{content:"\F6E1"}.mdi-hook-off::before{content:"\F6E2"}.mdi-hops::before{content:"\F2DF"}.mdi-horizontal-rotate-clockwise::before{content:"\F011E"}.mdi-horizontal-rotate-counterclockwise::before{content:"\F011F"}.mdi-horseshoe::before{content:"\FA57"}.mdi-hospital::before{content:"\F0017"}.mdi-hospital-box::before{content:"\F2E0"}.mdi-hospital-box-outline::before{content:"\F0018"}.mdi-hospital-building::before{content:"\F2E1"}.mdi-hospital-marker::before{content:"\F2E2"}.mdi-hot-tub::before{content:"\F827"}.mdi-hotel::before{content:"\F2E3"}.mdi-houzz::before{content:"\F2E4"}.mdi-houzz-box::before{content:"\F2E5"}.mdi-hubspot::before{content:"\FCF3"}.mdi-hulu::before{content:"\F828"}.mdi-human::before{content:"\F2E6"}.mdi-human-child::before{content:"\F2E7"}.mdi-human-female::before{content:"\F649"}.mdi-human-female-boy::before{content:"\FA58"}.mdi-human-female-female::before{content:"\FA59"}.mdi-human-female-girl::before{content:"\FA5A"}.mdi-human-greeting::before{content:"\F64A"}.mdi-human-handsdown::before{content:"\F64B"}.mdi-human-handsup::before{content:"\F64C"}.mdi-human-male::before{content:"\F64D"}.mdi-human-male-boy::before{content:"\FA5B"}.mdi-human-male-female::before{content:"\F2E8"}.mdi-human-male-girl::before{content:"\FA5C"}.mdi-human-male-height::before{content:"\FF18"}.mdi-human-male-height-variant::before{content:"\FF19"}.mdi-human-male-male::before{content:"\FA5D"}.mdi-human-pregnant::before{content:"\F5CF"}.mdi-humble-bundle::before{content:"\F743"}.mdi-hvac::before{content:"\F037D"}.mdi-hydraulic-oil-level::before{content:"\F034F"}.mdi-hydraulic-oil-temperature::before{content:"\F0350"}.mdi-hydro-power::before{content:"\F0310"}.mdi-ice-cream::before{content:"\F829"}.mdi-ice-pop::before{content:"\FF1A"}.mdi-id-card::before{content:"\FFE0"}.mdi-identifier::before{content:"\FF1B"}.mdi-ideogram-cjk::before{content:"\F035C"}.mdi-ideogram-cjk-variant::before{content:"\F035D"}.mdi-iframe::before{content:"\FC67"}.mdi-iframe-array::before{content:"\F0120"}.mdi-iframe-array-outline::before{content:"\F0121"}.mdi-iframe-braces::before{content:"\F0122"}.mdi-iframe-braces-outline::before{content:"\F0123"}.mdi-iframe-outline::before{content:"\FC68"}.mdi-iframe-parentheses::before{content:"\F0124"}.mdi-iframe-parentheses-outline::before{content:"\F0125"}.mdi-iframe-variable::before{content:"\F0126"}.mdi-iframe-variable-outline::before{content:"\F0127"}.mdi-image::before{content:"\F2E9"}.mdi-image-album::before{content:"\F2EA"}.mdi-image-area::before{content:"\F2EB"}.mdi-image-area-close::before{content:"\F2EC"}.mdi-image-auto-adjust::before{content:"\FFE1"}.mdi-image-broken::before{content:"\F2ED"}.mdi-image-broken-variant::before{content:"\F2EE"}.mdi-image-edit::before{content:"\F020E"}.mdi-image-edit-outline::before{content:"\F020F"}.mdi-image-filter::before{content:"\F2EF"}.mdi-image-filter-black-white::before{content:"\F2F0"}.mdi-image-filter-center-focus::before{content:"\F2F1"}.mdi-image-filter-center-focus-strong::before{content:"\FF1C"}.mdi-image-filter-center-focus-strong-outline::before{content:"\FF1D"}.mdi-image-filter-center-focus-weak::before{content:"\F2F2"}.mdi-image-filter-drama::before{content:"\F2F3"}.mdi-image-filter-frames::before{content:"\F2F4"}.mdi-image-filter-hdr::before{content:"\F2F5"}.mdi-image-filter-none::before{content:"\F2F6"}.mdi-image-filter-tilt-shift::before{content:"\F2F7"}.mdi-image-filter-vintage::before{content:"\F2F8"}.mdi-image-frame::before{content:"\FE8A"}.mdi-image-move::before{content:"\F9F7"}.mdi-image-multiple::before{content:"\F2F9"}.mdi-image-off::before{content:"\F82A"}.mdi-image-off-outline::before{content:"\F01FC"}.mdi-image-outline::before{content:"\F975"}.mdi-image-plus::before{content:"\F87B"}.mdi-image-search::before{content:"\F976"}.mdi-image-search-outline::before{content:"\F977"}.mdi-image-size-select-actual::before{content:"\FC69"}.mdi-image-size-select-large::before{content:"\FC6A"}.mdi-image-size-select-small::before{content:"\FC6B"}.mdi-import::before{content:"\F2FA"}.mdi-inbox::before{content:"\F686"}.mdi-inbox-arrow-down::before{content:"\F2FB"}.mdi-inbox-arrow-down-outline::before{content:"\F029B"}.mdi-inbox-arrow-up::before{content:"\F3D1"}.mdi-inbox-arrow-up-outline::before{content:"\F029C"}.mdi-inbox-full::before{content:"\F029D"}.mdi-inbox-full-outline::before{content:"\F029E"}.mdi-inbox-multiple::before{content:"\F8AF"}.mdi-inbox-multiple-outline::before{content:"\FB84"}.mdi-inbox-outline::before{content:"\F029F"}.mdi-incognito::before{content:"\F5F9"}.mdi-infinity::before{content:"\F6E3"}.mdi-information::before{content:"\F2FC"}.mdi-information-outline::before{content:"\F2FD"}.mdi-information-variant::before{content:"\F64E"}.mdi-instagram::before{content:"\F2FE"}.mdi-instapaper::before{content:"\F2FF"}.mdi-instrument-triangle::before{content:"\F0070"}.mdi-internet-explorer::before{content:"\F300"}.mdi-invert-colors::before{content:"\F301"}.mdi-invert-colors-off::before{content:"\FE8B"}.mdi-iobroker::before{content:"\F0313"}.mdi-ip::before{content:"\FA5E"}.mdi-ip-network::before{content:"\FA5F"}.mdi-ip-network-outline::before{content:"\FC6C"}.mdi-ipod::before{content:"\FC6D"}.mdi-islam::before{content:"\F978"}.mdi-island::before{content:"\F0071"}.mdi-itunes::before{content:"\F676"}.mdi-iv-bag::before{content:"\F00E4"}.mdi-jabber::before{content:"\FDB1"}.mdi-jeepney::before{content:"\F302"}.mdi-jellyfish::before{content:"\FF1E"}.mdi-jellyfish-outline::before{content:"\FF1F"}.mdi-jira::before{content:"\F303"}.mdi-jquery::before{content:"\F87C"}.mdi-jsfiddle::before{content:"\F304"}.mdi-json::before{content:"\F626"}.mdi-judaism::before{content:"\F979"}.mdi-jump-rope::before{content:"\F032A"}.mdi-kabaddi::before{content:"\FD63"}.mdi-karate::before{content:"\F82B"}.mdi-keg::before{content:"\F305"}.mdi-kettle::before{content:"\F5FA"}.mdi-kettle-alert::before{content:"\F0342"}.mdi-kettle-alert-outline::before{content:"\F0343"}.mdi-kettle-off::before{content:"\F0346"}.mdi-kettle-off-outline::before{content:"\F0347"}.mdi-kettle-outline::before{content:"\FF73"}.mdi-kettle-steam::before{content:"\F0344"}.mdi-kettle-steam-outline::before{content:"\F0345"}.mdi-kettlebell::before{content:"\F032B"}.mdi-key::before{content:"\F306"}.mdi-key-arrow-right::before{content:"\F033D"}.mdi-key-change::before{content:"\F307"}.mdi-key-link::before{content:"\F01CA"}.mdi-key-minus::before{content:"\F308"}.mdi-key-outline::before{content:"\FDB2"}.mdi-key-plus::before{content:"\F309"}.mdi-key-remove::before{content:"\F30A"}.mdi-key-star::before{content:"\F01C9"}.mdi-key-variant::before{content:"\F30B"}.mdi-key-wireless::before{content:"\FFE2"}.mdi-keyboard::before{content:"\F30C"}.mdi-keyboard-backspace::before{content:"\F30D"}.mdi-keyboard-caps::before{content:"\F30E"}.mdi-keyboard-close::before{content:"\F30F"}.mdi-keyboard-esc::before{content:"\F02E2"}.mdi-keyboard-f1::before{content:"\F02D6"}.mdi-keyboard-f10::before{content:"\F02DF"}.mdi-keyboard-f11::before{content:"\F02E0"}.mdi-keyboard-f12::before{content:"\F02E1"}.mdi-keyboard-f2::before{content:"\F02D7"}.mdi-keyboard-f3::before{content:"\F02D8"}.mdi-keyboard-f4::before{content:"\F02D9"}.mdi-keyboard-f5::before{content:"\F02DA"}.mdi-keyboard-f6::before{content:"\F02DB"}.mdi-keyboard-f7::before{content:"\F02DC"}.mdi-keyboard-f8::before{content:"\F02DD"}.mdi-keyboard-f9::before{content:"\F02DE"}.mdi-keyboard-off::before{content:"\F310"}.mdi-keyboard-off-outline::before{content:"\FE8C"}.mdi-keyboard-outline::before{content:"\F97A"}.mdi-keyboard-return::before{content:"\F311"}.mdi-keyboard-settings::before{content:"\F9F8"}.mdi-keyboard-settings-outline::before{content:"\F9F9"}.mdi-keyboard-space::before{content:"\F0072"}.mdi-keyboard-tab::before{content:"\F312"}.mdi-keyboard-variant::before{content:"\F313"}.mdi-khanda::before{content:"\F0128"}.mdi-kickstarter::before{content:"\F744"}.mdi-klingon::before{content:"\F0386"}.mdi-knife::before{content:"\F9FA"}.mdi-knife-military::before{content:"\F9FB"}.mdi-kodi::before{content:"\F314"}.mdi-kotlin::before{content:"\F0244"}.mdi-kubernetes::before{content:"\F0129"}.mdi-label::before{content:"\F315"}.mdi-label-multiple::before{content:"\F03A0"}.mdi-label-multiple-outline::before{content:"\F03A1"}.mdi-label-off::before{content:"\FACA"}.mdi-label-off-outline::before{content:"\FACB"}.mdi-label-outline::before{content:"\F316"}.mdi-label-percent::before{content:"\F0315"}.mdi-label-percent-outline::before{content:"\F0316"}.mdi-label-variant::before{content:"\FACC"}.mdi-label-variant-outline::before{content:"\FACD"}.mdi-ladybug::before{content:"\F82C"}.mdi-lambda::before{content:"\F627"}.mdi-lamp::before{content:"\F6B4"}.mdi-lan::before{content:"\F317"}.mdi-lan-check::before{content:"\F02D5"}.mdi-lan-connect::before{content:"\F318"}.mdi-lan-disconnect::before{content:"\F319"}.mdi-lan-pending::before{content:"\F31A"}.mdi-language-c::before{content:"\F671"}.mdi-language-cpp::before{content:"\F672"}.mdi-language-csharp::before{content:"\F31B"}.mdi-language-css3::before{content:"\F31C"}.mdi-language-fortran::before{content:"\F0245"}.mdi-language-go::before{content:"\F7D2"}.mdi-language-haskell::before{content:"\FC6E"}.mdi-language-html5::before{content:"\F31D"}.mdi-language-java::before{content:"\FB1C"}.mdi-language-javascript::before{content:"\F31E"}.mdi-language-lua::before{content:"\F8B0"}.mdi-language-php::before{content:"\F31F"}.mdi-language-python::before{content:"\F320"}.mdi-language-python-text::before{content:"\F321"}.mdi-language-r::before{content:"\F7D3"}.mdi-language-ruby-on-rails::before{content:"\FACE"}.mdi-language-swift::before{content:"\F6E4"}.mdi-language-typescript::before{content:"\F6E5"}.mdi-laptop::before{content:"\F322"}.mdi-laptop-chromebook::before{content:"\F323"}.mdi-laptop-mac::before{content:"\F324"}.mdi-laptop-off::before{content:"\F6E6"}.mdi-laptop-windows::before{content:"\F325"}.mdi-laravel::before{content:"\FACF"}.mdi-lasso::before{content:"\FF20"}.mdi-lastfm::before{content:"\F326"}.mdi-lastpass::before{content:"\F446"}.mdi-latitude::before{content:"\FF74"}.mdi-launch::before{content:"\F327"}.mdi-lava-lamp::before{content:"\F7D4"}.mdi-layers::before{content:"\F328"}.mdi-layers-minus::before{content:"\FE8D"}.mdi-layers-off::before{content:"\F329"}.mdi-layers-off-outline::before{content:"\F9FC"}.mdi-layers-outline::before{content:"\F9FD"}.mdi-layers-plus::before{content:"\FE30"}.mdi-layers-remove::before{content:"\FE31"}.mdi-layers-search::before{content:"\F0231"}.mdi-layers-search-outline::before{content:"\F0232"}.mdi-layers-triple::before{content:"\FF75"}.mdi-layers-triple-outline::before{content:"\FF76"}.mdi-lead-pencil::before{content:"\F64F"}.mdi-leaf::before{content:"\F32A"}.mdi-leaf-maple::before{content:"\FC6F"}.mdi-leaf-maple-off::before{content:"\F0305"}.mdi-leaf-off::before{content:"\F0304"}.mdi-leak::before{content:"\FDB3"}.mdi-leak-off::before{content:"\FDB4"}.mdi-led-off::before{content:"\F32B"}.mdi-led-on::before{content:"\F32C"}.mdi-led-outline::before{content:"\F32D"}.mdi-led-strip::before{content:"\F7D5"}.mdi-led-strip-variant::before{content:"\F0073"}.mdi-led-variant-off::before{content:"\F32E"}.mdi-led-variant-on::before{content:"\F32F"}.mdi-led-variant-outline::before{content:"\F330"}.mdi-leek::before{content:"\F01A8"}.mdi-less-than::before{content:"\F97B"}.mdi-less-than-or-equal::before{content:"\F97C"}.mdi-library::before{content:"\F331"}.mdi-library-books::before{content:"\F332"}.mdi-library-movie::before{content:"\FCF4"}.mdi-library-music::before{content:"\F333"}.mdi-library-music-outline::before{content:"\FF21"}.mdi-library-shelves::before{content:"\FB85"}.mdi-library-video::before{content:"\FCF5"}.mdi-license::before{content:"\FFE3"}.mdi-lifebuoy::before{content:"\F87D"}.mdi-light-switch::before{content:"\F97D"}.mdi-lightbulb::before{content:"\F335"}.mdi-lightbulb-cfl::before{content:"\F0233"}.mdi-lightbulb-cfl-off::before{content:"\F0234"}.mdi-lightbulb-cfl-spiral::before{content:"\F02A0"}.mdi-lightbulb-cfl-spiral-off::before{content:"\F02EE"}.mdi-lightbulb-group::before{content:"\F027E"}.mdi-lightbulb-group-off::before{content:"\F02F8"}.mdi-lightbulb-group-off-outline::before{content:"\F02F9"}.mdi-lightbulb-group-outline::before{content:"\F027F"}.mdi-lightbulb-multiple::before{content:"\F0280"}.mdi-lightbulb-multiple-off::before{content:"\F02FA"}.mdi-lightbulb-multiple-off-outline::before{content:"\F02FB"}.mdi-lightbulb-multiple-outline::before{content:"\F0281"}.mdi-lightbulb-off::before{content:"\FE32"}.mdi-lightbulb-off-outline::before{content:"\FE33"}.mdi-lightbulb-on::before{content:"\F6E7"}.mdi-lightbulb-on-outline::before{content:"\F6E8"}.mdi-lightbulb-outline::before{content:"\F336"}.mdi-lighthouse::before{content:"\F9FE"}.mdi-lighthouse-on::before{content:"\F9FF"}.mdi-link::before{content:"\F337"}.mdi-link-box::before{content:"\FCF6"}.mdi-link-box-outline::before{content:"\FCF7"}.mdi-link-box-variant::before{content:"\FCF8"}.mdi-link-box-variant-outline::before{content:"\FCF9"}.mdi-link-lock::before{content:"\F00E5"}.mdi-link-off::before{content:"\F338"}.mdi-link-plus::before{content:"\FC70"}.mdi-link-variant::before{content:"\F339"}.mdi-link-variant-minus::before{content:"\F012A"}.mdi-link-variant-off::before{content:"\F33A"}.mdi-link-variant-plus::before{content:"\F012B"}.mdi-link-variant-remove::before{content:"\F012C"}.mdi-linkedin::before{content:"\F33B"}.mdi-linkedin-box::before{content:"\F33C"}.mdi-linux::before{content:"\F33D"}.mdi-linux-mint::before{content:"\F8EC"}.mdi-litecoin::before{content:"\FA60"}.mdi-loading::before{content:"\F771"}.mdi-location-enter::before{content:"\FFE4"}.mdi-location-exit::before{content:"\FFE5"}.mdi-lock::before{content:"\F33E"}.mdi-lock-alert::before{content:"\F8ED"}.mdi-lock-clock::before{content:"\F97E"}.mdi-lock-open::before{content:"\F33F"}.mdi-lock-open-outline::before{content:"\F340"}.mdi-lock-open-variant::before{content:"\FFE6"}.mdi-lock-open-variant-outline::before{content:"\FFE7"}.mdi-lock-outline::before{content:"\F341"}.mdi-lock-pattern::before{content:"\F6E9"}.mdi-lock-plus::before{content:"\F5FB"}.mdi-lock-question::before{content:"\F8EE"}.mdi-lock-reset::before{content:"\F772"}.mdi-lock-smart::before{content:"\F8B1"}.mdi-locker::before{content:"\F7D6"}.mdi-locker-multiple::before{content:"\F7D7"}.mdi-login::before{content:"\F342"}.mdi-login-variant::before{content:"\F5FC"}.mdi-logout::before{content:"\F343"}.mdi-logout-variant::before{content:"\F5FD"}.mdi-longitude::before{content:"\FF77"}.mdi-looks::before{content:"\F344"}.mdi-loupe::before{content:"\F345"}.mdi-lumx::before{content:"\F346"}.mdi-lungs::before{content:"\F00AF"}.mdi-lyft::before{content:"\FB1D"}.mdi-magnet::before{content:"\F347"}.mdi-magnet-on::before{content:"\F348"}.mdi-magnify::before{content:"\F349"}.mdi-magnify-close::before{content:"\F97F"}.mdi-magnify-minus::before{content:"\F34A"}.mdi-magnify-minus-cursor::before{content:"\FA61"}.mdi-magnify-minus-outline::before{content:"\F6EB"}.mdi-magnify-plus::before{content:"\F34B"}.mdi-magnify-plus-cursor::before{content:"\FA62"}.mdi-magnify-plus-outline::before{content:"\F6EC"}.mdi-magnify-remove-cursor::before{content:"\F0237"}.mdi-magnify-remove-outline::before{content:"\F0238"}.mdi-magnify-scan::before{content:"\F02A1"}.mdi-mail::before{content:"\FED8"}.mdi-mail-ru::before{content:"\F34C"}.mdi-mailbox::before{content:"\F6ED"}.mdi-mailbox-open::before{content:"\FD64"}.mdi-mailbox-open-outline::before{content:"\FD65"}.mdi-mailbox-open-up::before{content:"\FD66"}.mdi-mailbox-open-up-outline::before{content:"\FD67"}.mdi-mailbox-outline::before{content:"\FD68"}.mdi-mailbox-up::before{content:"\FD69"}.mdi-mailbox-up-outline::before{content:"\FD6A"}.mdi-map::before{content:"\F34D"}.mdi-map-check::before{content:"\FED9"}.mdi-map-check-outline::before{content:"\FEDA"}.mdi-map-clock::before{content:"\FCFA"}.mdi-map-clock-outline::before{content:"\FCFB"}.mdi-map-legend::before{content:"\FA00"}.mdi-map-marker::before{content:"\F34E"}.mdi-map-marker-alert::before{content:"\FF22"}.mdi-map-marker-alert-outline::before{content:"\FF23"}.mdi-map-marker-check::before{content:"\FC71"}.mdi-map-marker-check-outline::before{content:"\F0326"}.mdi-map-marker-circle::before{content:"\F34F"}.mdi-map-marker-distance::before{content:"\F8EF"}.mdi-map-marker-down::before{content:"\F012D"}.mdi-map-marker-left::before{content:"\F0306"}.mdi-map-marker-left-outline::before{content:"\F0308"}.mdi-map-marker-minus::before{content:"\F650"}.mdi-map-marker-minus-outline::before{content:"\F0324"}.mdi-map-marker-multiple::before{content:"\F350"}.mdi-map-marker-multiple-outline::before{content:"\F02A2"}.mdi-map-marker-off::before{content:"\F351"}.mdi-map-marker-off-outline::before{content:"\F0328"}.mdi-map-marker-outline::before{content:"\F7D8"}.mdi-map-marker-path::before{content:"\FCFC"}.mdi-map-marker-plus::before{content:"\F651"}.mdi-map-marker-plus-outline::before{content:"\F0323"}.mdi-map-marker-question::before{content:"\FF24"}.mdi-map-marker-question-outline::before{content:"\FF25"}.mdi-map-marker-radius::before{content:"\F352"}.mdi-map-marker-radius-outline::before{content:"\F0327"}.mdi-map-marker-remove::before{content:"\FF26"}.mdi-map-marker-remove-outline::before{content:"\F0325"}.mdi-map-marker-remove-variant::before{content:"\FF27"}.mdi-map-marker-right::before{content:"\F0307"}.mdi-map-marker-right-outline::before{content:"\F0309"}.mdi-map-marker-up::before{content:"\F012E"}.mdi-map-minus::before{content:"\F980"}.mdi-map-outline::before{content:"\F981"}.mdi-map-plus::before{content:"\F982"}.mdi-map-search::before{content:"\F983"}.mdi-map-search-outline::before{content:"\F984"}.mdi-mapbox::before{content:"\FB86"}.mdi-margin::before{content:"\F353"}.mdi-markdown::before{content:"\F354"}.mdi-markdown-outline::before{content:"\FF78"}.mdi-marker::before{content:"\F652"}.mdi-marker-cancel::before{content:"\FDB5"}.mdi-marker-check::before{content:"\F355"}.mdi-mastodon::before{content:"\FAD0"}.mdi-mastodon-variant::before{content:"\FAD1"}.mdi-material-design::before{content:"\F985"}.mdi-material-ui::before{content:"\F357"}.mdi-math-compass::before{content:"\F358"}.mdi-math-cos::before{content:"\FC72"}.mdi-math-integral::before{content:"\FFE8"}.mdi-math-integral-box::before{content:"\FFE9"}.mdi-math-log::before{content:"\F00B0"}.mdi-math-norm::before{content:"\FFEA"}.mdi-math-norm-box::before{content:"\FFEB"}.mdi-math-sin::before{content:"\FC73"}.mdi-math-tan::before{content:"\FC74"}.mdi-matrix::before{content:"\F628"}.mdi-medal::before{content:"\F986"}.mdi-medal-outline::before{content:"\F0351"}.mdi-medical-bag::before{content:"\F6EE"}.mdi-meditation::before{content:"\F01A6"}.mdi-medium::before{content:"\F35A"}.mdi-meetup::before{content:"\FAD2"}.mdi-memory::before{content:"\F35B"}.mdi-menu::before{content:"\F35C"}.mdi-menu-down::before{content:"\F35D"}.mdi-menu-down-outline::before{content:"\F6B5"}.mdi-menu-left::before{content:"\F35E"}.mdi-menu-left-outline::before{content:"\FA01"}.mdi-menu-open::before{content:"\FB87"}.mdi-menu-right::before{content:"\F35F"}.mdi-menu-right-outline::before{content:"\FA02"}.mdi-menu-swap::before{content:"\FA63"}.mdi-menu-swap-outline::before{content:"\FA64"}.mdi-menu-up::before{content:"\F360"}.mdi-menu-up-outline::before{content:"\F6B6"}.mdi-merge::before{content:"\FF79"}.mdi-message::before{content:"\F361"}.mdi-message-alert::before{content:"\F362"}.mdi-message-alert-outline::before{content:"\FA03"}.mdi-message-arrow-left::before{content:"\F031D"}.mdi-message-arrow-left-outline::before{content:"\F031E"}.mdi-message-arrow-right::before{content:"\F031F"}.mdi-message-arrow-right-outline::before{content:"\F0320"}.mdi-message-bulleted::before{content:"\F6A1"}.mdi-message-bulleted-off::before{content:"\F6A2"}.mdi-message-draw::before{content:"\F363"}.mdi-message-image::before{content:"\F364"}.mdi-message-image-outline::before{content:"\F0197"}.mdi-message-lock::before{content:"\FFEC"}.mdi-message-lock-outline::before{content:"\F0198"}.mdi-message-minus::before{content:"\F0199"}.mdi-message-minus-outline::before{content:"\F019A"}.mdi-message-outline::before{content:"\F365"}.mdi-message-plus::before{content:"\F653"}.mdi-message-plus-outline::before{content:"\F00E6"}.mdi-message-processing::before{content:"\F366"}.mdi-message-processing-outline::before{content:"\F019B"}.mdi-message-reply::before{content:"\F367"}.mdi-message-reply-text::before{content:"\F368"}.mdi-message-settings::before{content:"\F6EF"}.mdi-message-settings-outline::before{content:"\F019C"}.mdi-message-settings-variant::before{content:"\F6F0"}.mdi-message-settings-variant-outline::before{content:"\F019D"}.mdi-message-text::before{content:"\F369"}.mdi-message-text-clock::before{content:"\F019E"}.mdi-message-text-clock-outline::before{content:"\F019F"}.mdi-message-text-lock::before{content:"\FFED"}.mdi-message-text-lock-outline::before{content:"\F01A0"}.mdi-message-text-outline::before{content:"\F36A"}.mdi-message-video::before{content:"\F36B"}.mdi-meteor::before{content:"\F629"}.mdi-metronome::before{content:"\F7D9"}.mdi-metronome-tick::before{content:"\F7DA"}.mdi-micro-sd::before{content:"\F7DB"}.mdi-microphone::before{content:"\F36C"}.mdi-microphone-minus::before{content:"\F8B2"}.mdi-microphone-off::before{content:"\F36D"}.mdi-microphone-outline::before{content:"\F36E"}.mdi-microphone-plus::before{content:"\F8B3"}.mdi-microphone-settings::before{content:"\F36F"}.mdi-microphone-variant::before{content:"\F370"}.mdi-microphone-variant-off::before{content:"\F371"}.mdi-microscope::before{content:"\F654"}.mdi-microsoft::before{content:"\F372"}.mdi-microsoft-dynamics::before{content:"\F987"}.mdi-microwave::before{content:"\FC75"}.mdi-middleware::before{content:"\FF7A"}.mdi-middleware-outline::before{content:"\FF7B"}.mdi-midi::before{content:"\F8F0"}.mdi-midi-port::before{content:"\F8F1"}.mdi-mine::before{content:"\FDB6"}.mdi-minecraft::before{content:"\F373"}.mdi-mini-sd::before{content:"\FA04"}.mdi-minidisc::before{content:"\FA05"}.mdi-minus::before{content:"\F374"}.mdi-minus-box::before{content:"\F375"}.mdi-minus-box-multiple::before{content:"\F016C"}.mdi-minus-box-multiple-outline::before{content:"\F016D"}.mdi-minus-box-outline::before{content:"\F6F1"}.mdi-minus-circle::before{content:"\F376"}.mdi-minus-circle-outline::before{content:"\F377"}.mdi-minus-network::before{content:"\F378"}.mdi-minus-network-outline::before{content:"\FC76"}.mdi-mirror::before{content:"\F0228"}.mdi-mixcloud::before{content:"\F62A"}.mdi-mixed-martial-arts::before{content:"\FD6B"}.mdi-mixed-reality::before{content:"\F87E"}.mdi-mixer::before{content:"\F7DC"}.mdi-molecule::before{content:"\FB88"}.mdi-monitor::before{content:"\F379"}.mdi-monitor-cellphone::before{content:"\F988"}.mdi-monitor-cellphone-star::before{content:"\F989"}.mdi-monitor-clean::before{content:"\F012F"}.mdi-monitor-dashboard::before{content:"\FA06"}.mdi-monitor-edit::before{content:"\F02F1"}.mdi-monitor-lock::before{content:"\FDB7"}.mdi-monitor-multiple::before{content:"\F37A"}.mdi-monitor-off::before{content:"\FD6C"}.mdi-monitor-screenshot::before{content:"\FE34"}.mdi-monitor-speaker::before{content:"\FF7C"}.mdi-monitor-speaker-off::before{content:"\FF7D"}.mdi-monitor-star::before{content:"\FDB8"}.mdi-moon-first-quarter::before{content:"\FF7E"}.mdi-moon-full::before{content:"\FF7F"}.mdi-moon-last-quarter::before{content:"\FF80"}.mdi-moon-new::before{content:"\FF81"}.mdi-moon-waning-crescent::before{content:"\FF82"}.mdi-moon-waning-gibbous::before{content:"\FF83"}.mdi-moon-waxing-crescent::before{content:"\FF84"}.mdi-moon-waxing-gibbous::before{content:"\FF85"}.mdi-moped::before{content:"\F00B1"}.mdi-more::before{content:"\F37B"}.mdi-mother-heart::before{content:"\F033F"}.mdi-mother-nurse::before{content:"\FCFD"}.mdi-motion-sensor::before{content:"\FD6D"}.mdi-motorbike::before{content:"\F37C"}.mdi-mouse::before{content:"\F37D"}.mdi-mouse-bluetooth::before{content:"\F98A"}.mdi-mouse-off::before{content:"\F37E"}.mdi-mouse-variant::before{content:"\F37F"}.mdi-mouse-variant-off::before{content:"\F380"}.mdi-move-resize::before{content:"\F655"}.mdi-move-resize-variant::before{content:"\F656"}.mdi-movie::before{content:"\F381"}.mdi-movie-edit::before{content:"\F014D"}.mdi-movie-edit-outline::before{content:"\F014E"}.mdi-movie-filter::before{content:"\F014F"}.mdi-movie-filter-outline::before{content:"\F0150"}.mdi-movie-open::before{content:"\FFEE"}.mdi-movie-open-outline::before{content:"\FFEF"}.mdi-movie-outline::before{content:"\FDB9"}.mdi-movie-roll::before{content:"\F7DD"}.mdi-movie-search::before{content:"\F01FD"}.mdi-movie-search-outline::before{content:"\F01FE"}.mdi-muffin::before{content:"\F98B"}.mdi-multiplication::before{content:"\F382"}.mdi-multiplication-box::before{content:"\F383"}.mdi-mushroom::before{content:"\F7DE"}.mdi-mushroom-outline::before{content:"\F7DF"}.mdi-music::before{content:"\F759"}.mdi-music-accidental-double-flat::before{content:"\FF86"}.mdi-music-accidental-double-sharp::before{content:"\FF87"}.mdi-music-accidental-flat::before{content:"\FF88"}.mdi-music-accidental-natural::before{content:"\FF89"}.mdi-music-accidental-sharp::before{content:"\FF8A"}.mdi-music-box::before{content:"\F384"}.mdi-music-box-outline::before{content:"\F385"}.mdi-music-circle::before{content:"\F386"}.mdi-music-circle-outline::before{content:"\FAD3"}.mdi-music-clef-alto::before{content:"\FF8B"}.mdi-music-clef-bass::before{content:"\FF8C"}.mdi-music-clef-treble::before{content:"\FF8D"}.mdi-music-note::before{content:"\F387"}.mdi-music-note-bluetooth::before{content:"\F5FE"}.mdi-music-note-bluetooth-off::before{content:"\F5FF"}.mdi-music-note-eighth::before{content:"\F388"}.mdi-music-note-eighth-dotted::before{content:"\FF8E"}.mdi-music-note-half::before{content:"\F389"}.mdi-music-note-half-dotted::before{content:"\FF8F"}.mdi-music-note-off::before{content:"\F38A"}.mdi-music-note-off-outline::before{content:"\FF90"}.mdi-music-note-outline::before{content:"\FF91"}.mdi-music-note-plus::before{content:"\FDBA"}.mdi-music-note-quarter::before{content:"\F38B"}.mdi-music-note-quarter-dotted::before{content:"\FF92"}.mdi-music-note-sixteenth::before{content:"\F38C"}.mdi-music-note-sixteenth-dotted::before{content:"\FF93"}.mdi-music-note-whole::before{content:"\F38D"}.mdi-music-note-whole-dotted::before{content:"\FF94"}.mdi-music-off::before{content:"\F75A"}.mdi-music-rest-eighth::before{content:"\FF95"}.mdi-music-rest-half::before{content:"\FF96"}.mdi-music-rest-quarter::before{content:"\FF97"}.mdi-music-rest-sixteenth::before{content:"\FF98"}.mdi-music-rest-whole::before{content:"\FF99"}.mdi-nail::before{content:"\FDBB"}.mdi-nas::before{content:"\F8F2"}.mdi-nativescript::before{content:"\F87F"}.mdi-nature::before{content:"\F38E"}.mdi-nature-people::before{content:"\F38F"}.mdi-navigation::before{content:"\F390"}.mdi-near-me::before{content:"\F5CD"}.mdi-necklace::before{content:"\FF28"}.mdi-needle::before{content:"\F391"}.mdi-netflix::before{content:"\F745"}.mdi-network::before{content:"\F6F2"}.mdi-network-off::before{content:"\FC77"}.mdi-network-off-outline::before{content:"\FC78"}.mdi-network-outline::before{content:"\FC79"}.mdi-network-router::before{content:"\F00B2"}.mdi-network-strength-1::before{content:"\F8F3"}.mdi-network-strength-1-alert::before{content:"\F8F4"}.mdi-network-strength-2::before{content:"\F8F5"}.mdi-network-strength-2-alert::before{content:"\F8F6"}.mdi-network-strength-3::before{content:"\F8F7"}.mdi-network-strength-3-alert::before{content:"\F8F8"}.mdi-network-strength-4::before{content:"\F8F9"}.mdi-network-strength-4-alert::before{content:"\F8FA"}.mdi-network-strength-off::before{content:"\F8FB"}.mdi-network-strength-off-outline::before{content:"\F8FC"}.mdi-network-strength-outline::before{content:"\F8FD"}.mdi-new-box::before{content:"\F394"}.mdi-newspaper::before{content:"\F395"}.mdi-newspaper-minus::before{content:"\FF29"}.mdi-newspaper-plus::before{content:"\FF2A"}.mdi-newspaper-variant::before{content:"\F0023"}.mdi-newspaper-variant-multiple::before{content:"\F0024"}.mdi-newspaper-variant-multiple-outline::before{content:"\F0025"}.mdi-newspaper-variant-outline::before{content:"\F0026"}.mdi-nfc::before{content:"\F396"}.mdi-nfc-off::before{content:"\FE35"}.mdi-nfc-search-variant::before{content:"\FE36"}.mdi-nfc-tap::before{content:"\F397"}.mdi-nfc-variant::before{content:"\F398"}.mdi-nfc-variant-off::before{content:"\FE37"}.mdi-ninja::before{content:"\F773"}.mdi-nintendo-switch::before{content:"\F7E0"}.mdi-nix::before{content:"\F0130"}.mdi-nodejs::before{content:"\F399"}.mdi-noodles::before{content:"\F01A9"}.mdi-not-equal::before{content:"\F98C"}.mdi-not-equal-variant::before{content:"\F98D"}.mdi-note::before{content:"\F39A"}.mdi-note-multiple::before{content:"\F6B7"}.mdi-note-multiple-outline::before{content:"\F6B8"}.mdi-note-outline::before{content:"\F39B"}.mdi-note-plus::before{content:"\F39C"}.mdi-note-plus-outline::before{content:"\F39D"}.mdi-note-text::before{content:"\F39E"}.mdi-note-text-outline::before{content:"\F0202"}.mdi-notebook::before{content:"\F82D"}.mdi-notebook-multiple::before{content:"\FE38"}.mdi-notebook-outline::before{content:"\FEDC"}.mdi-notification-clear-all::before{content:"\F39F"}.mdi-npm::before{content:"\F6F6"}.mdi-npm-variant::before{content:"\F98E"}.mdi-npm-variant-outline::before{content:"\F98F"}.mdi-nuke::before{content:"\F6A3"}.mdi-null::before{content:"\F7E1"}.mdi-numeric::before{content:"\F3A0"}.mdi-numeric-0::before{content:"\30"}.mdi-numeric-0-box::before{content:"\F3A1"}.mdi-numeric-0-box-multiple::before{content:"\FF2B"}.mdi-numeric-0-box-multiple-outline::before{content:"\F3A2"}.mdi-numeric-0-box-outline::before{content:"\F3A3"}.mdi-numeric-0-circle::before{content:"\FC7A"}.mdi-numeric-0-circle-outline::before{content:"\FC7B"}.mdi-numeric-1::before{content:"\31"}.mdi-numeric-1-box::before{content:"\F3A4"}.mdi-numeric-1-box-multiple::before{content:"\FF2C"}.mdi-numeric-1-box-multiple-outline::before{content:"\F3A5"}.mdi-numeric-1-box-outline::before{content:"\F3A6"}.mdi-numeric-1-circle::before{content:"\FC7C"}.mdi-numeric-1-circle-outline::before{content:"\FC7D"}.mdi-numeric-10::before{content:"\F000A"}.mdi-numeric-10-box::before{content:"\FF9A"}.mdi-numeric-10-box-multiple::before{content:"\F000B"}.mdi-numeric-10-box-multiple-outline::before{content:"\F000C"}.mdi-numeric-10-box-outline::before{content:"\FF9B"}.mdi-numeric-10-circle::before{content:"\F000D"}.mdi-numeric-10-circle-outline::before{content:"\F000E"}.mdi-numeric-2::before{content:"\32"}.mdi-numeric-2-box::before{content:"\F3A7"}.mdi-numeric-2-box-multiple::before{content:"\FF2D"}.mdi-numeric-2-box-multiple-outline::before{content:"\F3A8"}.mdi-numeric-2-box-outline::before{content:"\F3A9"}.mdi-numeric-2-circle::before{content:"\FC7E"}.mdi-numeric-2-circle-outline::before{content:"\FC7F"}.mdi-numeric-3::before{content:"\33"}.mdi-numeric-3-box::before{content:"\F3AA"}.mdi-numeric-3-box-multiple::before{content:"\FF2E"}.mdi-numeric-3-box-multiple-outline::before{content:"\F3AB"}.mdi-numeric-3-box-outline::before{content:"\F3AC"}.mdi-numeric-3-circle::before{content:"\FC80"}.mdi-numeric-3-circle-outline::before{content:"\FC81"}.mdi-numeric-4::before{content:"\34"}.mdi-numeric-4-box::before{content:"\F3AD"}.mdi-numeric-4-box-multiple::before{content:"\FF2F"}.mdi-numeric-4-box-multiple-outline::before{content:"\F3AE"}.mdi-numeric-4-box-outline::before{content:"\F3AF"}.mdi-numeric-4-circle::before{content:"\FC82"}.mdi-numeric-4-circle-outline::before{content:"\FC83"}.mdi-numeric-5::before{content:"\35"}.mdi-numeric-5-box::before{content:"\F3B0"}.mdi-numeric-5-box-multiple::before{content:"\FF30"}.mdi-numeric-5-box-multiple-outline::before{content:"\F3B1"}.mdi-numeric-5-box-outline::before{content:"\F3B2"}.mdi-numeric-5-circle::before{content:"\FC84"}.mdi-numeric-5-circle-outline::before{content:"\FC85"}.mdi-numeric-6::before{content:"\36"}.mdi-numeric-6-box::before{content:"\F3B3"}.mdi-numeric-6-box-multiple::before{content:"\FF31"}.mdi-numeric-6-box-multiple-outline::before{content:"\F3B4"}.mdi-numeric-6-box-outline::before{content:"\F3B5"}.mdi-numeric-6-circle::before{content:"\FC86"}.mdi-numeric-6-circle-outline::before{content:"\FC87"}.mdi-numeric-7::before{content:"\37"}.mdi-numeric-7-box::before{content:"\F3B6"}.mdi-numeric-7-box-multiple::before{content:"\FF32"}.mdi-numeric-7-box-multiple-outline::before{content:"\F3B7"}.mdi-numeric-7-box-outline::before{content:"\F3B8"}.mdi-numeric-7-circle::before{content:"\FC88"}.mdi-numeric-7-circle-outline::before{content:"\FC89"}.mdi-numeric-8::before{content:"\38"}.mdi-numeric-8-box::before{content:"\F3B9"}.mdi-numeric-8-box-multiple::before{content:"\FF33"}.mdi-numeric-8-box-multiple-outline::before{content:"\F3BA"}.mdi-numeric-8-box-outline::before{content:"\F3BB"}.mdi-numeric-8-circle::before{content:"\FC8A"}.mdi-numeric-8-circle-outline::before{content:"\FC8B"}.mdi-numeric-9::before{content:"\39"}.mdi-numeric-9-box::before{content:"\F3BC"}.mdi-numeric-9-box-multiple::before{content:"\FF34"}.mdi-numeric-9-box-multiple-outline::before{content:"\F3BD"}.mdi-numeric-9-box-outline::before{content:"\F3BE"}.mdi-numeric-9-circle::before{content:"\FC8C"}.mdi-numeric-9-circle-outline::before{content:"\FC8D"}.mdi-numeric-9-plus::before{content:"\F000F"}.mdi-numeric-9-plus-box::before{content:"\F3BF"}.mdi-numeric-9-plus-box-multiple::before{content:"\FF35"}.mdi-numeric-9-plus-box-multiple-outline::before{content:"\F3C0"}.mdi-numeric-9-plus-box-outline::before{content:"\F3C1"}.mdi-numeric-9-plus-circle::before{content:"\FC8E"}.mdi-numeric-9-plus-circle-outline::before{content:"\FC8F"}.mdi-numeric-negative-1::before{content:"\F0074"}.mdi-nut::before{content:"\F6F7"}.mdi-nutrition::before{content:"\F3C2"}.mdi-nuxt::before{content:"\F0131"}.mdi-oar::before{content:"\F67B"}.mdi-ocarina::before{content:"\FDBC"}.mdi-oci::before{content:"\F0314"}.mdi-ocr::before{content:"\F0165"}.mdi-octagon::before{content:"\F3C3"}.mdi-octagon-outline::before{content:"\F3C4"}.mdi-octagram::before{content:"\F6F8"}.mdi-octagram-outline::before{content:"\F774"}.mdi-odnoklassniki::before{content:"\F3C5"}.mdi-offer::before{content:"\F0246"}.mdi-office::before{content:"\F3C6"}.mdi-office-building::before{content:"\F990"}.mdi-oil::before{content:"\F3C7"}.mdi-oil-lamp::before{content:"\FF36"}.mdi-oil-level::before{content:"\F0075"}.mdi-oil-temperature::before{content:"\F0019"}.mdi-omega::before{content:"\F3C9"}.mdi-one-up::before{content:"\FB89"}.mdi-onedrive::before{content:"\F3CA"}.mdi-onenote::before{content:"\F746"}.mdi-onepassword::before{content:"\F880"}.mdi-opacity::before{content:"\F5CC"}.mdi-open-in-app::before{content:"\F3CB"}.mdi-open-in-new::before{content:"\F3CC"}.mdi-open-source-initiative::before{content:"\FB8A"}.mdi-openid::before{content:"\F3CD"}.mdi-opera::before{content:"\F3CE"}.mdi-orbit::before{content:"\F018"}.mdi-origin::before{content:"\FB2B"}.mdi-ornament::before{content:"\F3CF"}.mdi-ornament-variant::before{content:"\F3D0"}.mdi-outdoor-lamp::before{content:"\F0076"}.mdi-outlook::before{content:"\FCFE"}.mdi-overscan::before{content:"\F0027"}.mdi-owl::before{content:"\F3D2"}.mdi-pac-man::before{content:"\FB8B"}.mdi-package::before{content:"\F3D3"}.mdi-package-down::before{content:"\F3D4"}.mdi-package-up::before{content:"\F3D5"}.mdi-package-variant::before{content:"\F3D6"}.mdi-package-variant-closed::before{content:"\F3D7"}.mdi-page-first::before{content:"\F600"}.mdi-page-last::before{content:"\F601"}.mdi-page-layout-body::before{content:"\F6F9"}.mdi-page-layout-footer::before{content:"\F6FA"}.mdi-page-layout-header::before{content:"\F6FB"}.mdi-page-layout-header-footer::before{content:"\FF9C"}.mdi-page-layout-sidebar-left::before{content:"\F6FC"}.mdi-page-layout-sidebar-right::before{content:"\F6FD"}.mdi-page-next::before{content:"\FB8C"}.mdi-page-next-outline::before{content:"\FB8D"}.mdi-page-previous::before{content:"\FB8E"}.mdi-page-previous-outline::before{content:"\FB8F"}.mdi-palette::before{content:"\F3D8"}.mdi-palette-advanced::before{content:"\F3D9"}.mdi-palette-outline::before{content:"\FE6C"}.mdi-palette-swatch::before{content:"\F8B4"}.mdi-palette-swatch-outline::before{content:"\F0387"}.mdi-palm-tree::before{content:"\F0077"}.mdi-pan::before{content:"\FB90"}.mdi-pan-bottom-left::before{content:"\FB91"}.mdi-pan-bottom-right::before{content:"\FB92"}.mdi-pan-down::before{content:"\FB93"}.mdi-pan-horizontal::before{content:"\FB94"}.mdi-pan-left::before{content:"\FB95"}.mdi-pan-right::before{content:"\FB96"}.mdi-pan-top-left::before{content:"\FB97"}.mdi-pan-top-right::before{content:"\FB98"}.mdi-pan-up::before{content:"\FB99"}.mdi-pan-vertical::before{content:"\FB9A"}.mdi-panda::before{content:"\F3DA"}.mdi-pandora::before{content:"\F3DB"}.mdi-panorama::before{content:"\F3DC"}.mdi-panorama-fisheye::before{content:"\F3DD"}.mdi-panorama-horizontal::before{content:"\F3DE"}.mdi-panorama-vertical::before{content:"\F3DF"}.mdi-panorama-wide-angle::before{content:"\F3E0"}.mdi-paper-cut-vertical::before{content:"\F3E1"}.mdi-paper-roll::before{content:"\F0182"}.mdi-paper-roll-outline::before{content:"\F0183"}.mdi-paperclip::before{content:"\F3E2"}.mdi-parachute::before{content:"\FC90"}.mdi-parachute-outline::before{content:"\FC91"}.mdi-parking::before{content:"\F3E3"}.mdi-party-popper::before{content:"\F0078"}.mdi-passport::before{content:"\F7E2"}.mdi-passport-biometric::before{content:"\FDBD"}.mdi-pasta::before{content:"\F018B"}.mdi-patio-heater::before{content:"\FF9D"}.mdi-patreon::before{content:"\F881"}.mdi-pause::before{content:"\F3E4"}.mdi-pause-circle::before{content:"\F3E5"}.mdi-pause-circle-outline::before{content:"\F3E6"}.mdi-pause-octagon::before{content:"\F3E7"}.mdi-pause-octagon-outline::before{content:"\F3E8"}.mdi-paw::before{content:"\F3E9"}.mdi-paw-off::before{content:"\F657"}.mdi-paypal::before{content:"\F882"}.mdi-pdf-box::before{content:"\FE39"}.mdi-peace::before{content:"\F883"}.mdi-peanut::before{content:"\F001E"}.mdi-peanut-off::before{content:"\F001F"}.mdi-peanut-off-outline::before{content:"\F0021"}.mdi-peanut-outline::before{content:"\F0020"}.mdi-pen::before{content:"\F3EA"}.mdi-pen-lock::before{content:"\FDBE"}.mdi-pen-minus::before{content:"\FDBF"}.mdi-pen-off::before{content:"\FDC0"}.mdi-pen-plus::before{content:"\FDC1"}.mdi-pen-remove::before{content:"\FDC2"}.mdi-pencil::before{content:"\F3EB"}.mdi-pencil-box::before{content:"\F3EC"}.mdi-pencil-box-multiple::before{content:"\F016F"}.mdi-pencil-box-multiple-outline::before{content:"\F0170"}.mdi-pencil-box-outline::before{content:"\F3ED"}.mdi-pencil-circle::before{content:"\F6FE"}.mdi-pencil-circle-outline::before{content:"\F775"}.mdi-pencil-lock::before{content:"\F3EE"}.mdi-pencil-lock-outline::before{content:"\FDC3"}.mdi-pencil-minus::before{content:"\FDC4"}.mdi-pencil-minus-outline::before{content:"\FDC5"}.mdi-pencil-off::before{content:"\F3EF"}.mdi-pencil-off-outline::before{content:"\FDC6"}.mdi-pencil-outline::before{content:"\FC92"}.mdi-pencil-plus::before{content:"\FDC7"}.mdi-pencil-plus-outline::before{content:"\FDC8"}.mdi-pencil-remove::before{content:"\FDC9"}.mdi-pencil-remove-outline::before{content:"\FDCA"}.mdi-pencil-ruler::before{content:"\F037E"}.mdi-penguin::before{content:"\FEDD"}.mdi-pentagon::before{content:"\F6FF"}.mdi-pentagon-outline::before{content:"\F700"}.mdi-percent::before{content:"\F3F0"}.mdi-percent-outline::before{content:"\F02A3"}.mdi-periodic-table::before{content:"\F8B5"}.mdi-periodic-table-co::before{content:"\F0329"}.mdi-periodic-table-co2::before{content:"\F7E3"}.mdi-periscope::before{content:"\F747"}.mdi-perspective-less::before{content:"\FCFF"}.mdi-perspective-more::before{content:"\FD00"}.mdi-pharmacy::before{content:"\F3F1"}.mdi-phone::before{content:"\F3F2"}.mdi-phone-alert::before{content:"\FF37"}.mdi-phone-alert-outline::before{content:"\F01B9"}.mdi-phone-bluetooth::before{content:"\F3F3"}.mdi-phone-bluetooth-outline::before{content:"\F01BA"}.mdi-phone-cancel::before{content:"\F00E7"}.mdi-phone-cancel-outline::before{content:"\F01BB"}.mdi-phone-check::before{content:"\F01D4"}.mdi-phone-check-outline::before{content:"\F01D5"}.mdi-phone-classic::before{content:"\F602"}.mdi-phone-classic-off::before{content:"\F02A4"}.mdi-phone-forward::before{content:"\F3F4"}.mdi-phone-forward-outline::before{content:"\F01BC"}.mdi-phone-hangup::before{content:"\F3F5"}.mdi-phone-hangup-outline::before{content:"\F01BD"}.mdi-phone-in-talk::before{content:"\F3F6"}.mdi-phone-in-talk-outline::before{content:"\F01AD"}.mdi-phone-incoming::before{content:"\F3F7"}.mdi-phone-incoming-outline::before{content:"\F01BE"}.mdi-phone-lock::before{content:"\F3F8"}.mdi-phone-lock-outline::before{content:"\F01BF"}.mdi-phone-log::before{content:"\F3F9"}.mdi-phone-log-outline::before{content:"\F01C0"}.mdi-phone-message::before{content:"\F01C1"}.mdi-phone-message-outline::before{content:"\F01C2"}.mdi-phone-minus::before{content:"\F658"}.mdi-phone-minus-outline::before{content:"\F01C3"}.mdi-phone-missed::before{content:"\F3FA"}.mdi-phone-missed-outline::before{content:"\F01D0"}.mdi-phone-off::before{content:"\FDCB"}.mdi-phone-off-outline::before{content:"\F01D1"}.mdi-phone-outgoing::before{content:"\F3FB"}.mdi-phone-outgoing-outline::before{content:"\F01C4"}.mdi-phone-outline::before{content:"\FDCC"}.mdi-phone-paused::before{content:"\F3FC"}.mdi-phone-paused-outline::before{content:"\F01C5"}.mdi-phone-plus::before{content:"\F659"}.mdi-phone-plus-outline::before{content:"\F01C6"}.mdi-phone-return::before{content:"\F82E"}.mdi-phone-return-outline::before{content:"\F01C7"}.mdi-phone-ring::before{content:"\F01D6"}.mdi-phone-ring-outline::before{content:"\F01D7"}.mdi-phone-rotate-landscape::before{content:"\F884"}.mdi-phone-rotate-portrait::before{content:"\F885"}.mdi-phone-settings::before{content:"\F3FD"}.mdi-phone-settings-outline::before{content:"\F01C8"}.mdi-phone-voip::before{content:"\F3FE"}.mdi-pi::before{content:"\F3FF"}.mdi-pi-box::before{content:"\F400"}.mdi-pi-hole::before{content:"\FDCD"}.mdi-piano::before{content:"\F67C"}.mdi-pickaxe::before{content:"\F8B6"}.mdi-picture-in-picture-bottom-right::before{content:"\FE3A"}.mdi-picture-in-picture-bottom-right-outline::before{content:"\FE3B"}.mdi-picture-in-picture-top-right::before{content:"\FE3C"}.mdi-picture-in-picture-top-right-outline::before{content:"\FE3D"}.mdi-pier::before{content:"\F886"}.mdi-pier-crane::before{content:"\F887"}.mdi-pig::before{content:"\F401"}.mdi-pig-variant::before{content:"\F0028"}.mdi-piggy-bank::before{content:"\F0029"}.mdi-pill::before{content:"\F402"}.mdi-pillar::before{content:"\F701"}.mdi-pin::before{content:"\F403"}.mdi-pin-off::before{content:"\F404"}.mdi-pin-off-outline::before{content:"\F92F"}.mdi-pin-outline::before{content:"\F930"}.mdi-pine-tree::before{content:"\F405"}.mdi-pine-tree-box::before{content:"\F406"}.mdi-pinterest::before{content:"\F407"}.mdi-pinterest-box::before{content:"\F408"}.mdi-pinwheel::before{content:"\FAD4"}.mdi-pinwheel-outline::before{content:"\FAD5"}.mdi-pipe::before{content:"\F7E4"}.mdi-pipe-disconnected::before{content:"\F7E5"}.mdi-pipe-leak::before{content:"\F888"}.mdi-pipe-wrench::before{content:"\F037F"}.mdi-pirate::before{content:"\FA07"}.mdi-pistol::before{content:"\F702"}.mdi-piston::before{content:"\F889"}.mdi-pizza::before{content:"\F409"}.mdi-play::before{content:"\F40A"}.mdi-play-box::before{content:"\F02A5"}.mdi-play-box-outline::before{content:"\F40B"}.mdi-play-circle::before{content:"\F40C"}.mdi-play-circle-outline::before{content:"\F40D"}.mdi-play-network::before{content:"\F88A"}.mdi-play-network-outline::before{content:"\FC93"}.mdi-play-outline::before{content:"\FF38"}.mdi-play-pause::before{content:"\F40E"}.mdi-play-protected-content::before{content:"\F40F"}.mdi-play-speed::before{content:"\F8FE"}.mdi-playlist-check::before{content:"\F5C7"}.mdi-playlist-edit::before{content:"\F8FF"}.mdi-playlist-minus::before{content:"\F410"}.mdi-playlist-music::before{content:"\FC94"}.mdi-playlist-music-outline::before{content:"\FC95"}.mdi-playlist-play::before{content:"\F411"}.mdi-playlist-plus::before{content:"\F412"}.mdi-playlist-remove::before{content:"\F413"}.mdi-playlist-star::before{content:"\FDCE"}.mdi-playstation::before{content:"\F414"}.mdi-plex::before{content:"\F6B9"}.mdi-plus::before{content:"\F415"}.mdi-plus-box::before{content:"\F416"}.mdi-plus-box-multiple::before{content:"\F334"}.mdi-plus-box-multiple-outline::before{content:"\F016E"}.mdi-plus-box-outline::before{content:"\F703"}.mdi-plus-circle::before{content:"\F417"}.mdi-plus-circle-multiple-outline::before{content:"\F418"}.mdi-plus-circle-outline::before{content:"\F419"}.mdi-plus-minus::before{content:"\F991"}.mdi-plus-minus-box::before{content:"\F992"}.mdi-plus-network::before{content:"\F41A"}.mdi-plus-network-outline::before{content:"\FC96"}.mdi-plus-one::before{content:"\F41B"}.mdi-plus-outline::before{content:"\F704"}.mdi-plus-thick::before{content:"\F0217"}.mdi-pocket::before{content:"\F41C"}.mdi-podcast::before{content:"\F993"}.mdi-podium::before{content:"\FD01"}.mdi-podium-bronze::before{content:"\FD02"}.mdi-podium-gold::before{content:"\FD03"}.mdi-podium-silver::before{content:"\FD04"}.mdi-point-of-sale::before{content:"\FD6E"}.mdi-pokeball::before{content:"\F41D"}.mdi-pokemon-go::before{content:"\FA08"}.mdi-poker-chip::before{content:"\F82F"}.mdi-polaroid::before{content:"\F41E"}.mdi-police-badge::before{content:"\F0192"}.mdi-police-badge-outline::before{content:"\F0193"}.mdi-poll::before{content:"\F41F"}.mdi-poll-box::before{content:"\F420"}.mdi-poll-box-outline::before{content:"\F02A6"}.mdi-polymer::before{content:"\F421"}.mdi-pool::before{content:"\F606"}.mdi-popcorn::before{content:"\F422"}.mdi-post::before{content:"\F002A"}.mdi-post-outline::before{content:"\F002B"}.mdi-postage-stamp::before{content:"\FC97"}.mdi-pot::before{content:"\F65A"}.mdi-pot-mix::before{content:"\F65B"}.mdi-pound::before{content:"\F423"}.mdi-pound-box::before{content:"\F424"}.mdi-pound-box-outline::before{content:"\F01AA"}.mdi-power::before{content:"\F425"}.mdi-power-cycle::before{content:"\F900"}.mdi-power-off::before{content:"\F901"}.mdi-power-on::before{content:"\F902"}.mdi-power-plug::before{content:"\F6A4"}.mdi-power-plug-off::before{content:"\F6A5"}.mdi-power-settings::before{content:"\F426"}.mdi-power-sleep::before{content:"\F903"}.mdi-power-socket::before{content:"\F427"}.mdi-power-socket-au::before{content:"\F904"}.mdi-power-socket-de::before{content:"\F0132"}.mdi-power-socket-eu::before{content:"\F7E6"}.mdi-power-socket-fr::before{content:"\F0133"}.mdi-power-socket-jp::before{content:"\F0134"}.mdi-power-socket-uk::before{content:"\F7E7"}.mdi-power-socket-us::before{content:"\F7E8"}.mdi-power-standby::before{content:"\F905"}.mdi-powershell::before{content:"\FA09"}.mdi-prescription::before{content:"\F705"}.mdi-presentation::before{content:"\F428"}.mdi-presentation-play::before{content:"\F429"}.mdi-printer::before{content:"\F42A"}.mdi-printer-3d::before{content:"\F42B"}.mdi-printer-3d-nozzle::before{content:"\FE3E"}.mdi-printer-3d-nozzle-alert::before{content:"\F01EB"}.mdi-printer-3d-nozzle-alert-outline::before{content:"\F01EC"}.mdi-printer-3d-nozzle-outline::before{content:"\FE3F"}.mdi-printer-alert::before{content:"\F42C"}.mdi-printer-check::before{content:"\F0171"}.mdi-printer-off::before{content:"\FE40"}.mdi-printer-pos::before{content:"\F0079"}.mdi-printer-settings::before{content:"\F706"}.mdi-printer-wireless::before{content:"\FA0A"}.mdi-priority-high::before{content:"\F603"}.mdi-priority-low::before{content:"\F604"}.mdi-professional-hexagon::before{content:"\F42D"}.mdi-progress-alert::before{content:"\FC98"}.mdi-progress-check::before{content:"\F994"}.mdi-progress-clock::before{content:"\F995"}.mdi-progress-close::before{content:"\F0135"}.mdi-progress-download::before{content:"\F996"}.mdi-progress-upload::before{content:"\F997"}.mdi-progress-wrench::before{content:"\FC99"}.mdi-projector::before{content:"\F42E"}.mdi-projector-screen::before{content:"\F42F"}.mdi-propane-tank::before{content:"\F0382"}.mdi-propane-tank-outline::before{content:"\F0383"}.mdi-protocol::before{content:"\FFF9"}.mdi-publish::before{content:"\F6A6"}.mdi-pulse::before{content:"\F430"}.mdi-pumpkin::before{content:"\FB9B"}.mdi-purse::before{content:"\FF39"}.mdi-purse-outline::before{content:"\FF3A"}.mdi-puzzle::before{content:"\F431"}.mdi-puzzle-outline::before{content:"\FA65"}.mdi-qi::before{content:"\F998"}.mdi-qqchat::before{content:"\F605"}.mdi-qrcode::before{content:"\F432"}.mdi-qrcode-edit::before{content:"\F8B7"}.mdi-qrcode-minus::before{content:"\F01B7"}.mdi-qrcode-plus::before{content:"\F01B6"}.mdi-qrcode-remove::before{content:"\F01B8"}.mdi-qrcode-scan::before{content:"\F433"}.mdi-quadcopter::before{content:"\F434"}.mdi-quality-high::before{content:"\F435"}.mdi-quality-low::before{content:"\FA0B"}.mdi-quality-medium::before{content:"\FA0C"}.mdi-quicktime::before{content:"\F436"}.mdi-quora::before{content:"\FD05"}.mdi-rabbit::before{content:"\F906"}.mdi-racing-helmet::before{content:"\FD6F"}.mdi-racquetball::before{content:"\FD70"}.mdi-radar::before{content:"\F437"}.mdi-radiator::before{content:"\F438"}.mdi-radiator-disabled::before{content:"\FAD6"}.mdi-radiator-off::before{content:"\FAD7"}.mdi-radio::before{content:"\F439"}.mdi-radio-am::before{content:"\FC9A"}.mdi-radio-fm::before{content:"\FC9B"}.mdi-radio-handheld::before{content:"\F43A"}.mdi-radio-off::before{content:"\F0247"}.mdi-radio-tower::before{content:"\F43B"}.mdi-radioactive::before{content:"\F43C"}.mdi-radioactive-off::before{content:"\FEDE"}.mdi-radiobox-blank::before{content:"\F43D"}.mdi-radiobox-marked::before{content:"\F43E"}.mdi-radius::before{content:"\FC9C"}.mdi-radius-outline::before{content:"\FC9D"}.mdi-railroad-light::before{content:"\FF3B"}.mdi-raspberry-pi::before{content:"\F43F"}.mdi-ray-end::before{content:"\F440"}.mdi-ray-end-arrow::before{content:"\F441"}.mdi-ray-start::before{content:"\F442"}.mdi-ray-start-arrow::before{content:"\F443"}.mdi-ray-start-end::before{content:"\F444"}.mdi-ray-vertex::before{content:"\F445"}.mdi-react::before{content:"\F707"}.mdi-read::before{content:"\F447"}.mdi-receipt::before{content:"\F449"}.mdi-record::before{content:"\F44A"}.mdi-record-circle::before{content:"\FEDF"}.mdi-record-circle-outline::before{content:"\FEE0"}.mdi-record-player::before{content:"\F999"}.mdi-record-rec::before{content:"\F44B"}.mdi-rectangle::before{content:"\FE41"}.mdi-rectangle-outline::before{content:"\FE42"}.mdi-recycle::before{content:"\F44C"}.mdi-reddit::before{content:"\F44D"}.mdi-redhat::before{content:"\F0146"}.mdi-redo::before{content:"\F44E"}.mdi-redo-variant::before{content:"\F44F"}.mdi-reflect-horizontal::before{content:"\FA0D"}.mdi-reflect-vertical::before{content:"\FA0E"}.mdi-refresh::before{content:"\F450"}.mdi-refresh-circle::before{content:"\F03A2"}.mdi-regex::before{content:"\F451"}.mdi-registered-trademark::before{content:"\FA66"}.mdi-relative-scale::before{content:"\F452"}.mdi-reload::before{content:"\F453"}.mdi-reload-alert::before{content:"\F0136"}.mdi-reminder::before{content:"\F88B"}.mdi-remote::before{content:"\F454"}.mdi-remote-desktop::before{content:"\F8B8"}.mdi-remote-off::before{content:"\FEE1"}.mdi-remote-tv::before{content:"\FEE2"}.mdi-remote-tv-off::before{content:"\FEE3"}.mdi-rename-box::before{content:"\F455"}.mdi-reorder-horizontal::before{content:"\F687"}.mdi-reorder-vertical::before{content:"\F688"}.mdi-repeat::before{content:"\F456"}.mdi-repeat-off::before{content:"\F457"}.mdi-repeat-once::before{content:"\F458"}.mdi-replay::before{content:"\F459"}.mdi-reply::before{content:"\F45A"}.mdi-reply-all::before{content:"\F45B"}.mdi-reply-all-outline::before{content:"\FF3C"}.mdi-reply-circle::before{content:"\F01D9"}.mdi-reply-outline::before{content:"\FF3D"}.mdi-reproduction::before{content:"\F45C"}.mdi-resistor::before{content:"\FB1F"}.mdi-resistor-nodes::before{content:"\FB20"}.mdi-resize::before{content:"\FA67"}.mdi-resize-bottom-right::before{content:"\F45D"}.mdi-responsive::before{content:"\F45E"}.mdi-restart::before{content:"\F708"}.mdi-restart-alert::before{content:"\F0137"}.mdi-restart-off::before{content:"\FD71"}.mdi-restore::before{content:"\F99A"}.mdi-restore-alert::before{content:"\F0138"}.mdi-rewind::before{content:"\F45F"}.mdi-rewind-10::before{content:"\FD06"}.mdi-rewind-30::before{content:"\FD72"}.mdi-rewind-5::before{content:"\F0224"}.mdi-rewind-outline::before{content:"\F709"}.mdi-rhombus::before{content:"\F70A"}.mdi-rhombus-medium::before{content:"\FA0F"}.mdi-rhombus-outline::before{content:"\F70B"}.mdi-rhombus-split::before{content:"\FA10"}.mdi-ribbon::before{content:"\F460"}.mdi-rice::before{content:"\F7E9"}.mdi-ring::before{content:"\F7EA"}.mdi-rivet::before{content:"\FE43"}.mdi-road::before{content:"\F461"}.mdi-road-variant::before{content:"\F462"}.mdi-robber::before{content:"\F007A"}.mdi-robot::before{content:"\F6A8"}.mdi-robot-industrial::before{content:"\FB21"}.mdi-robot-mower::before{content:"\F0222"}.mdi-robot-mower-outline::before{content:"\F021E"}.mdi-robot-vacuum::before{content:"\F70C"}.mdi-robot-vacuum-variant::before{content:"\F907"}.mdi-rocket::before{content:"\F463"}.mdi-rodent::before{content:"\F0352"}.mdi-roller-skate::before{content:"\FD07"}.mdi-rollerblade::before{content:"\FD08"}.mdi-rollupjs::before{content:"\FB9C"}.mdi-roman-numeral-1::before{content:"\F00B3"}.mdi-roman-numeral-10::before{content:"\F00BC"}.mdi-roman-numeral-2::before{content:"\F00B4"}.mdi-roman-numeral-3::before{content:"\F00B5"}.mdi-roman-numeral-4::before{content:"\F00B6"}.mdi-roman-numeral-5::before{content:"\F00B7"}.mdi-roman-numeral-6::before{content:"\F00B8"}.mdi-roman-numeral-7::before{content:"\F00B9"}.mdi-roman-numeral-8::before{content:"\F00BA"}.mdi-roman-numeral-9::before{content:"\F00BB"}.mdi-room-service::before{content:"\F88C"}.mdi-room-service-outline::before{content:"\FD73"}.mdi-rotate-3d::before{content:"\FEE4"}.mdi-rotate-3d-variant::before{content:"\F464"}.mdi-rotate-left::before{content:"\F465"}.mdi-rotate-left-variant::before{content:"\F466"}.mdi-rotate-orbit::before{content:"\FD74"}.mdi-rotate-right::before{content:"\F467"}.mdi-rotate-right-variant::before{content:"\F468"}.mdi-rounded-corner::before{content:"\F607"}.mdi-router::before{content:"\F020D"}.mdi-router-wireless::before{content:"\F469"}.mdi-router-wireless-settings::before{content:"\FA68"}.mdi-routes::before{content:"\F46A"}.mdi-routes-clock::before{content:"\F007B"}.mdi-rowing::before{content:"\F608"}.mdi-rss::before{content:"\F46B"}.mdi-rss-box::before{content:"\F46C"}.mdi-rss-off::before{content:"\FF3E"}.mdi-ruby::before{content:"\FD09"}.mdi-rugby::before{content:"\FD75"}.mdi-ruler::before{content:"\F46D"}.mdi-ruler-square::before{content:"\FC9E"}.mdi-ruler-square-compass::before{content:"\FEDB"}.mdi-run::before{content:"\F70D"}.mdi-run-fast::before{content:"\F46E"}.mdi-rv-truck::before{content:"\F01FF"}.mdi-sack::before{content:"\FD0A"}.mdi-sack-percent::before{content:"\FD0B"}.mdi-safe::before{content:"\FA69"}.mdi-safe-square::before{content:"\F02A7"}.mdi-safe-square-outline::before{content:"\F02A8"}.mdi-safety-goggles::before{content:"\FD0C"}.mdi-sailing::before{content:"\FEE5"}.mdi-sale::before{content:"\F46F"}.mdi-salesforce::before{content:"\F88D"}.mdi-sass::before{content:"\F7EB"}.mdi-satellite::before{content:"\F470"}.mdi-satellite-uplink::before{content:"\F908"}.mdi-satellite-variant::before{content:"\F471"}.mdi-sausage::before{content:"\F8B9"}.mdi-saw-blade::before{content:"\FE44"}.mdi-saxophone::before{content:"\F609"}.mdi-scale::before{content:"\F472"}.mdi-scale-balance::before{content:"\F5D1"}.mdi-scale-bathroom::before{content:"\F473"}.mdi-scale-off::before{content:"\F007C"}.mdi-scanner::before{content:"\F6AA"}.mdi-scanner-off::before{content:"\F909"}.mdi-scatter-plot::before{content:"\FEE6"}.mdi-scatter-plot-outline::before{content:"\FEE7"}.mdi-school::before{content:"\F474"}.mdi-school-outline::before{content:"\F01AB"}.mdi-scissors-cutting::before{content:"\FA6A"}.mdi-scooter::before{content:"\F0214"}.mdi-scoreboard::before{content:"\F02A9"}.mdi-scoreboard-outline::before{content:"\F02AA"}.mdi-screen-rotation::before{content:"\F475"}.mdi-screen-rotation-lock::before{content:"\F476"}.mdi-screw-flat-top::before{content:"\FDCF"}.mdi-screw-lag::before{content:"\FE54"}.mdi-screw-machine-flat-top::before{content:"\FE55"}.mdi-screw-machine-round-top::before{content:"\FE56"}.mdi-screw-round-top::before{content:"\FE57"}.mdi-screwdriver::before{content:"\F477"}.mdi-script::before{content:"\FB9D"}.mdi-script-outline::before{content:"\F478"}.mdi-script-text::before{content:"\FB9E"}.mdi-script-text-outline::before{content:"\FB9F"}.mdi-sd::before{content:"\F479"}.mdi-seal::before{content:"\F47A"}.mdi-seal-variant::before{content:"\FFFA"}.mdi-search-web::before{content:"\F70E"}.mdi-seat::before{content:"\FC9F"}.mdi-seat-flat::before{content:"\F47B"}.mdi-seat-flat-angled::before{content:"\F47C"}.mdi-seat-individual-suite::before{content:"\F47D"}.mdi-seat-legroom-extra::before{content:"\F47E"}.mdi-seat-legroom-normal::before{content:"\F47F"}.mdi-seat-legroom-reduced::before{content:"\F480"}.mdi-seat-outline::before{content:"\FCA0"}.mdi-seat-passenger::before{content:"\F0274"}.mdi-seat-recline-extra::before{content:"\F481"}.mdi-seat-recline-normal::before{content:"\F482"}.mdi-seatbelt::before{content:"\FCA1"}.mdi-security::before{content:"\F483"}.mdi-security-network::before{content:"\F484"}.mdi-seed::before{content:"\FE45"}.mdi-seed-outline::before{content:"\FE46"}.mdi-segment::before{content:"\FEE8"}.mdi-select::before{content:"\F485"}.mdi-select-all::before{content:"\F486"}.mdi-select-color::before{content:"\FD0D"}.mdi-select-compare::before{content:"\FAD8"}.mdi-select-drag::before{content:"\FA6B"}.mdi-select-group::before{content:"\FF9F"}.mdi-select-inverse::before{content:"\F487"}.mdi-select-marker::before{content:"\F02AB"}.mdi-select-multiple::before{content:"\F02AC"}.mdi-select-multiple-marker::before{content:"\F02AD"}.mdi-select-off::before{content:"\F488"}.mdi-select-place::before{content:"\FFFB"}.mdi-select-search::before{content:"\F022F"}.mdi-selection::before{content:"\F489"}.mdi-selection-drag::before{content:"\FA6C"}.mdi-selection-ellipse::before{content:"\FD0E"}.mdi-selection-ellipse-arrow-inside::before{content:"\FF3F"}.mdi-selection-marker::before{content:"\F02AE"}.mdi-selection-multiple-marker::before{content:"\F02AF"}.mdi-selection-mutliple::before{content:"\F02B0"}.mdi-selection-off::before{content:"\F776"}.mdi-selection-search::before{content:"\F0230"}.mdi-semantic-web::before{content:"\F0341"}.mdi-send::before{content:"\F48A"}.mdi-send-check::before{content:"\F018C"}.mdi-send-check-outline::before{content:"\F018D"}.mdi-send-circle::before{content:"\FE58"}.mdi-send-circle-outline::before{content:"\FE59"}.mdi-send-clock::before{content:"\F018E"}.mdi-send-clock-outline::before{content:"\F018F"}.mdi-send-lock::before{content:"\F7EC"}.mdi-send-lock-outline::before{content:"\F0191"}.mdi-send-outline::before{content:"\F0190"}.mdi-serial-port::before{content:"\F65C"}.mdi-server::before{content:"\F48B"}.mdi-server-minus::before{content:"\F48C"}.mdi-server-network::before{content:"\F48D"}.mdi-server-network-off::before{content:"\F48E"}.mdi-server-off::before{content:"\F48F"}.mdi-server-plus::before{content:"\F490"}.mdi-server-remove::before{content:"\F491"}.mdi-server-security::before{content:"\F492"}.mdi-set-all::before{content:"\F777"}.mdi-set-center::before{content:"\F778"}.mdi-set-center-right::before{content:"\F779"}.mdi-set-left::before{content:"\F77A"}.mdi-set-left-center::before{content:"\F77B"}.mdi-set-left-right::before{content:"\F77C"}.mdi-set-none::before{content:"\F77D"}.mdi-set-right::before{content:"\F77E"}.mdi-set-top-box::before{content:"\F99E"}.mdi-settings::before{content:"\F493"}.mdi-settings-box::before{content:"\F494"}.mdi-settings-helper::before{content:"\FA6D"}.mdi-settings-outline::before{content:"\F8BA"}.mdi-settings-transfer::before{content:"\F007D"}.mdi-settings-transfer-outline::before{content:"\F007E"}.mdi-shaker::before{content:"\F0139"}.mdi-shaker-outline::before{content:"\F013A"}.mdi-shape::before{content:"\F830"}.mdi-shape-circle-plus::before{content:"\F65D"}.mdi-shape-outline::before{content:"\F831"}.mdi-shape-oval-plus::before{content:"\F0225"}.mdi-shape-plus::before{content:"\F495"}.mdi-shape-polygon-plus::before{content:"\F65E"}.mdi-shape-rectangle-plus::before{content:"\F65F"}.mdi-shape-square-plus::before{content:"\F660"}.mdi-share::before{content:"\F496"}.mdi-share-all::before{content:"\F021F"}.mdi-share-all-outline::before{content:"\F0220"}.mdi-share-circle::before{content:"\F01D8"}.mdi-share-off::before{content:"\FF40"}.mdi-share-off-outline::before{content:"\FF41"}.mdi-share-outline::before{content:"\F931"}.mdi-share-variant::before{content:"\F497"}.mdi-sheep::before{content:"\FCA2"}.mdi-shield::before{content:"\F498"}.mdi-shield-account::before{content:"\F88E"}.mdi-shield-account-outline::before{content:"\FA11"}.mdi-shield-airplane::before{content:"\F6BA"}.mdi-shield-airplane-outline::before{content:"\FCA3"}.mdi-shield-alert::before{content:"\FEE9"}.mdi-shield-alert-outline::before{content:"\FEEA"}.mdi-shield-car::before{content:"\FFA0"}.mdi-shield-check::before{content:"\F565"}.mdi-shield-check-outline::before{content:"\FCA4"}.mdi-shield-cross::before{content:"\FCA5"}.mdi-shield-cross-outline::before{content:"\FCA6"}.mdi-shield-edit::before{content:"\F01CB"}.mdi-shield-edit-outline::before{content:"\F01CC"}.mdi-shield-half::before{content:"\F038B"}.mdi-shield-half-full::before{content:"\F77F"}.mdi-shield-home::before{content:"\F689"}.mdi-shield-home-outline::before{content:"\FCA7"}.mdi-shield-key::before{content:"\FBA0"}.mdi-shield-key-outline::before{content:"\FBA1"}.mdi-shield-link-variant::before{content:"\FD0F"}.mdi-shield-link-variant-outline::before{content:"\FD10"}.mdi-shield-lock::before{content:"\F99C"}.mdi-shield-lock-outline::before{content:"\FCA8"}.mdi-shield-off::before{content:"\F99D"}.mdi-shield-off-outline::before{content:"\F99B"}.mdi-shield-outline::before{content:"\F499"}.mdi-shield-plus::before{content:"\FAD9"}.mdi-shield-plus-outline::before{content:"\FADA"}.mdi-shield-refresh::before{content:"\F01CD"}.mdi-shield-refresh-outline::before{content:"\F01CE"}.mdi-shield-remove::before{content:"\FADB"}.mdi-shield-remove-outline::before{content:"\FADC"}.mdi-shield-search::before{content:"\FD76"}.mdi-shield-star::before{content:"\F0166"}.mdi-shield-star-outline::before{content:"\F0167"}.mdi-shield-sun::before{content:"\F007F"}.mdi-shield-sun-outline::before{content:"\F0080"}.mdi-ship-wheel::before{content:"\F832"}.mdi-shoe-formal::before{content:"\FB22"}.mdi-shoe-heel::before{content:"\FB23"}.mdi-shoe-print::before{content:"\FE5A"}.mdi-shopify::before{content:"\FADD"}.mdi-shopping::before{content:"\F49A"}.mdi-shopping-music::before{content:"\F49B"}.mdi-shopping-outline::before{content:"\F0200"}.mdi-shopping-search::before{content:"\FFA1"}.mdi-shovel::before{content:"\F70F"}.mdi-shovel-off::before{content:"\F710"}.mdi-shower::before{content:"\F99F"}.mdi-shower-head::before{content:"\F9A0"}.mdi-shredder::before{content:"\F49C"}.mdi-shuffle::before{content:"\F49D"}.mdi-shuffle-disabled::before{content:"\F49E"}.mdi-shuffle-variant::before{content:"\F49F"}.mdi-shuriken::before{content:"\F03AA"}.mdi-sigma::before{content:"\F4A0"}.mdi-sigma-lower::before{content:"\F62B"}.mdi-sign-caution::before{content:"\F4A1"}.mdi-sign-direction::before{content:"\F780"}.mdi-sign-direction-minus::before{content:"\F0022"}.mdi-sign-direction-plus::before{content:"\FFFD"}.mdi-sign-direction-remove::before{content:"\FFFE"}.mdi-sign-real-estate::before{content:"\F0143"}.mdi-sign-text::before{content:"\F781"}.mdi-signal::before{content:"\F4A2"}.mdi-signal-2g::before{content:"\F711"}.mdi-signal-3g::before{content:"\F712"}.mdi-signal-4g::before{content:"\F713"}.mdi-signal-5g::before{content:"\FA6E"}.mdi-signal-cellular-1::before{content:"\F8BB"}.mdi-signal-cellular-2::before{content:"\F8BC"}.mdi-signal-cellular-3::before{content:"\F8BD"}.mdi-signal-cellular-outline::before{content:"\F8BE"}.mdi-signal-distance-variant::before{content:"\FE47"}.mdi-signal-hspa::before{content:"\F714"}.mdi-signal-hspa-plus::before{content:"\F715"}.mdi-signal-off::before{content:"\F782"}.mdi-signal-variant::before{content:"\F60A"}.mdi-signature::before{content:"\FE5B"}.mdi-signature-freehand::before{content:"\FE5C"}.mdi-signature-image::before{content:"\FE5D"}.mdi-signature-text::before{content:"\FE5E"}.mdi-silo::before{content:"\FB24"}.mdi-silverware::before{content:"\F4A3"}.mdi-silverware-clean::before{content:"\FFFF"}.mdi-silverware-fork::before{content:"\F4A4"}.mdi-silverware-fork-knife::before{content:"\FA6F"}.mdi-silverware-spoon::before{content:"\F4A5"}.mdi-silverware-variant::before{content:"\F4A6"}.mdi-sim::before{content:"\F4A7"}.mdi-sim-alert::before{content:"\F4A8"}.mdi-sim-off::before{content:"\F4A9"}.mdi-simple-icons::before{content:"\F0348"}.mdi-sina-weibo::before{content:"\FADE"}.mdi-sitemap::before{content:"\F4AA"}.mdi-skate::before{content:"\FD11"}.mdi-skew-less::before{content:"\FD12"}.mdi-skew-more::before{content:"\FD13"}.mdi-ski::before{content:"\F032F"}.mdi-ski-cross-country::before{content:"\F0330"}.mdi-ski-water::before{content:"\F0331"}.mdi-skip-backward::before{content:"\F4AB"}.mdi-skip-backward-outline::before{content:"\FF42"}.mdi-skip-forward::before{content:"\F4AC"}.mdi-skip-forward-outline::before{content:"\FF43"}.mdi-skip-next::before{content:"\F4AD"}.mdi-skip-next-circle::before{content:"\F661"}.mdi-skip-next-circle-outline::before{content:"\F662"}.mdi-skip-next-outline::before{content:"\FF44"}.mdi-skip-previous::before{content:"\F4AE"}.mdi-skip-previous-circle::before{content:"\F663"}.mdi-skip-previous-circle-outline::before{content:"\F664"}.mdi-skip-previous-outline::before{content:"\FF45"}.mdi-skull::before{content:"\F68B"}.mdi-skull-crossbones::before{content:"\FBA2"}.mdi-skull-crossbones-outline::before{content:"\FBA3"}.mdi-skull-outline::before{content:"\FBA4"}.mdi-skype::before{content:"\F4AF"}.mdi-skype-business::before{content:"\F4B0"}.mdi-slack::before{content:"\F4B1"}.mdi-slackware::before{content:"\F90A"}.mdi-slash-forward::before{content:"\F0000"}.mdi-slash-forward-box::before{content:"\F0001"}.mdi-sleep::before{content:"\F4B2"}.mdi-sleep-off::before{content:"\F4B3"}.mdi-slope-downhill::before{content:"\FE5F"}.mdi-slope-uphill::before{content:"\FE60"}.mdi-slot-machine::before{content:"\F013F"}.mdi-slot-machine-outline::before{content:"\F0140"}.mdi-smart-card::before{content:"\F00E8"}.mdi-smart-card-outline::before{content:"\F00E9"}.mdi-smart-card-reader::before{content:"\F00EA"}.mdi-smart-card-reader-outline::before{content:"\F00EB"}.mdi-smog::before{content:"\FA70"}.mdi-smoke-detector::before{content:"\F392"}.mdi-smoking::before{content:"\F4B4"}.mdi-smoking-off::before{content:"\F4B5"}.mdi-snapchat::before{content:"\F4B6"}.mdi-snowboard::before{content:"\F0332"}.mdi-snowflake::before{content:"\F716"}.mdi-snowflake-alert::before{content:"\FF46"}.mdi-snowflake-melt::before{content:"\F02F6"}.mdi-snowflake-variant::before{content:"\FF47"}.mdi-snowman::before{content:"\F4B7"}.mdi-soccer::before{content:"\F4B8"}.mdi-soccer-field::before{content:"\F833"}.mdi-sofa::before{content:"\F4B9"}.mdi-solar-panel::before{content:"\FD77"}.mdi-solar-panel-large::before{content:"\FD78"}.mdi-solar-power::before{content:"\FA71"}.mdi-soldering-iron::before{content:"\F00BD"}.mdi-solid::before{content:"\F68C"}.mdi-sort::before{content:"\F4BA"}.mdi-sort-alphabetical::before{content:"\F4BB"}.mdi-sort-alphabetical-ascending::before{content:"\F0173"}.mdi-sort-alphabetical-descending::before{content:"\F0174"}.mdi-sort-ascending::before{content:"\F4BC"}.mdi-sort-descending::before{content:"\F4BD"}.mdi-sort-numeric::before{content:"\F4BE"}.mdi-sort-variant::before{content:"\F4BF"}.mdi-sort-variant-lock::before{content:"\FCA9"}.mdi-sort-variant-lock-open::before{content:"\FCAA"}.mdi-sort-variant-remove::before{content:"\F0172"}.mdi-soundcloud::before{content:"\F4C0"}.mdi-source-branch::before{content:"\F62C"}.mdi-source-commit::before{content:"\F717"}.mdi-source-commit-end::before{content:"\F718"}.mdi-source-commit-end-local::before{content:"\F719"}.mdi-source-commit-local::before{content:"\F71A"}.mdi-source-commit-next-local::before{content:"\F71B"}.mdi-source-commit-start::before{content:"\F71C"}.mdi-source-commit-start-next-local::before{content:"\F71D"}.mdi-source-fork::before{content:"\F4C1"}.mdi-source-merge::before{content:"\F62D"}.mdi-source-pull::before{content:"\F4C2"}.mdi-source-repository::before{content:"\FCAB"}.mdi-source-repository-multiple::before{content:"\FCAC"}.mdi-soy-sauce::before{content:"\F7ED"}.mdi-spa::before{content:"\FCAD"}.mdi-spa-outline::before{content:"\FCAE"}.mdi-space-invaders::before{content:"\FBA5"}.mdi-space-station::before{content:"\F03AE"}.mdi-spade::before{content:"\FE48"}.mdi-speaker::before{content:"\F4C3"}.mdi-speaker-bluetooth::before{content:"\F9A1"}.mdi-speaker-multiple::before{content:"\FD14"}.mdi-speaker-off::before{content:"\F4C4"}.mdi-speaker-wireless::before{content:"\F71E"}.mdi-speedometer::before{content:"\F4C5"}.mdi-speedometer-medium::before{content:"\FFA2"}.mdi-speedometer-slow::before{content:"\FFA3"}.mdi-spellcheck::before{content:"\F4C6"}.mdi-spider::before{content:"\F0215"}.mdi-spider-thread::before{content:"\F0216"}.mdi-spider-web::before{content:"\FBA6"}.mdi-spotify::before{content:"\F4C7"}.mdi-spotlight::before{content:"\F4C8"}.mdi-spotlight-beam::before{content:"\F4C9"}.mdi-spray::before{content:"\F665"}.mdi-spray-bottle::before{content:"\FADF"}.mdi-sprinkler::before{content:"\F0081"}.mdi-sprinkler-variant::before{content:"\F0082"}.mdi-sprout::before{content:"\FE49"}.mdi-sprout-outline::before{content:"\FE4A"}.mdi-square::before{content:"\F763"}.mdi-square-edit-outline::before{content:"\F90B"}.mdi-square-inc::before{content:"\F4CA"}.mdi-square-inc-cash::before{content:"\F4CB"}.mdi-square-medium::before{content:"\FA12"}.mdi-square-medium-outline::before{content:"\FA13"}.mdi-square-off::before{content:"\F0319"}.mdi-square-off-outline::before{content:"\F031A"}.mdi-square-outline::before{content:"\F762"}.mdi-square-root::before{content:"\F783"}.mdi-square-root-box::before{content:"\F9A2"}.mdi-square-small::before{content:"\FA14"}.mdi-squeegee::before{content:"\FAE0"}.mdi-ssh::before{content:"\F8BF"}.mdi-stack-exchange::before{content:"\F60B"}.mdi-stack-overflow::before{content:"\F4CC"}.mdi-stackpath::before{content:"\F359"}.mdi-stadium::before{content:"\F001A"}.mdi-stadium-variant::before{content:"\F71F"}.mdi-stairs::before{content:"\F4CD"}.mdi-stairs-down::before{content:"\F02E9"}.mdi-stairs-up::before{content:"\F02E8"}.mdi-stamper::before{content:"\FD15"}.mdi-standard-definition::before{content:"\F7EE"}.mdi-star::before{content:"\F4CE"}.mdi-star-box::before{content:"\FA72"}.mdi-star-box-multiple::before{content:"\F02B1"}.mdi-star-box-multiple-outline::before{content:"\F02B2"}.mdi-star-box-outline::before{content:"\FA73"}.mdi-star-circle::before{content:"\F4CF"}.mdi-star-circle-outline::before{content:"\F9A3"}.mdi-star-face::before{content:"\F9A4"}.mdi-star-four-points::before{content:"\FAE1"}.mdi-star-four-points-outline::before{content:"\FAE2"}.mdi-star-half::before{content:"\F4D0"}.mdi-star-off::before{content:"\F4D1"}.mdi-star-outline::before{content:"\F4D2"}.mdi-star-three-points::before{content:"\FAE3"}.mdi-star-three-points-outline::before{content:"\FAE4"}.mdi-state-machine::before{content:"\F021A"}.mdi-steam::before{content:"\F4D3"}.mdi-steam-box::before{content:"\F90C"}.mdi-steering::before{content:"\F4D4"}.mdi-steering-off::before{content:"\F90D"}.mdi-step-backward::before{content:"\F4D5"}.mdi-step-backward-2::before{content:"\F4D6"}.mdi-step-forward::before{content:"\F4D7"}.mdi-step-forward-2::before{content:"\F4D8"}.mdi-stethoscope::before{content:"\F4D9"}.mdi-sticker::before{content:"\F038F"}.mdi-sticker-alert::before{content:"\F0390"}.mdi-sticker-alert-outline::before{content:"\F0391"}.mdi-sticker-check::before{content:"\F0392"}.mdi-sticker-check-outline::before{content:"\F0393"}.mdi-sticker-circle-outline::before{content:"\F5D0"}.mdi-sticker-emoji::before{content:"\F784"}.mdi-sticker-minus::before{content:"\F0394"}.mdi-sticker-minus-outline::before{content:"\F0395"}.mdi-sticker-outline::before{content:"\F0396"}.mdi-sticker-plus::before{content:"\F0397"}.mdi-sticker-plus-outline::before{content:"\F0398"}.mdi-sticker-remove::before{content:"\F0399"}.mdi-sticker-remove-outline::before{content:"\F039A"}.mdi-stocking::before{content:"\F4DA"}.mdi-stomach::before{content:"\F00BE"}.mdi-stop::before{content:"\F4DB"}.mdi-stop-circle::before{content:"\F666"}.mdi-stop-circle-outline::before{content:"\F667"}.mdi-store::before{content:"\F4DC"}.mdi-store-24-hour::before{content:"\F4DD"}.mdi-store-outline::before{content:"\F038C"}.mdi-storefront::before{content:"\F00EC"}.mdi-stove::before{content:"\F4DE"}.mdi-strategy::before{content:"\F0201"}.mdi-strava::before{content:"\FB25"}.mdi-stretch-to-page::before{content:"\FF48"}.mdi-stretch-to-page-outline::before{content:"\FF49"}.mdi-string-lights::before{content:"\F02E5"}.mdi-string-lights-off::before{content:"\F02E6"}.mdi-subdirectory-arrow-left::before{content:"\F60C"}.mdi-subdirectory-arrow-right::before{content:"\F60D"}.mdi-subtitles::before{content:"\FA15"}.mdi-subtitles-outline::before{content:"\FA16"}.mdi-subway::before{content:"\F6AB"}.mdi-subway-alert-variant::before{content:"\FD79"}.mdi-subway-variant::before{content:"\F4DF"}.mdi-summit::before{content:"\F785"}.mdi-sunglasses::before{content:"\F4E0"}.mdi-surround-sound::before{content:"\F5C5"}.mdi-surround-sound-2-0::before{content:"\F7EF"}.mdi-surround-sound-3-1::before{content:"\F7F0"}.mdi-surround-sound-5-1::before{content:"\F7F1"}.mdi-surround-sound-7-1::before{content:"\F7F2"}.mdi-svg::before{content:"\F720"}.mdi-swap-horizontal::before{content:"\F4E1"}.mdi-swap-horizontal-bold::before{content:"\FBA9"}.mdi-swap-horizontal-circle::before{content:"\F0002"}.mdi-swap-horizontal-circle-outline::before{content:"\F0003"}.mdi-swap-horizontal-variant::before{content:"\F8C0"}.mdi-swap-vertical::before{content:"\F4E2"}.mdi-swap-vertical-bold::before{content:"\FBAA"}.mdi-swap-vertical-circle::before{content:"\F0004"}.mdi-swap-vertical-circle-outline::before{content:"\F0005"}.mdi-swap-vertical-variant::before{content:"\F8C1"}.mdi-swim::before{content:"\F4E3"}.mdi-switch::before{content:"\F4E4"}.mdi-sword::before{content:"\F4E5"}.mdi-sword-cross::before{content:"\F786"}.mdi-syllabary-hangul::before{content:"\F035E"}.mdi-syllabary-hiragana::before{content:"\F035F"}.mdi-syllabary-katakana::before{content:"\F0360"}.mdi-syllabary-katakana-half-width::before{content:"\F0361"}.mdi-symfony::before{content:"\FAE5"}.mdi-sync::before{content:"\F4E6"}.mdi-sync-alert::before{content:"\F4E7"}.mdi-sync-circle::before{content:"\F03A3"}.mdi-sync-off::before{content:"\F4E8"}.mdi-tab::before{content:"\F4E9"}.mdi-tab-minus::before{content:"\FB26"}.mdi-tab-plus::before{content:"\F75B"}.mdi-tab-remove::before{content:"\FB27"}.mdi-tab-unselected::before{content:"\F4EA"}.mdi-table::before{content:"\F4EB"}.mdi-table-border::before{content:"\FA17"}.mdi-table-chair::before{content:"\F0083"}.mdi-table-column::before{content:"\F834"}.mdi-table-column-plus-after::before{content:"\F4EC"}.mdi-table-column-plus-before::before{content:"\F4ED"}.mdi-table-column-remove::before{content:"\F4EE"}.mdi-table-column-width::before{content:"\F4EF"}.mdi-table-edit::before{content:"\F4F0"}.mdi-table-eye::before{content:"\F00BF"}.mdi-table-headers-eye::before{content:"\F0248"}.mdi-table-headers-eye-off::before{content:"\F0249"}.mdi-table-large::before{content:"\F4F1"}.mdi-table-large-plus::before{content:"\FFA4"}.mdi-table-large-remove::before{content:"\FFA5"}.mdi-table-merge-cells::before{content:"\F9A5"}.mdi-table-of-contents::before{content:"\F835"}.mdi-table-plus::before{content:"\FA74"}.mdi-table-remove::before{content:"\FA75"}.mdi-table-row::before{content:"\F836"}.mdi-table-row-height::before{content:"\F4F2"}.mdi-table-row-plus-after::before{content:"\F4F3"}.mdi-table-row-plus-before::before{content:"\F4F4"}.mdi-table-row-remove::before{content:"\F4F5"}.mdi-table-search::before{content:"\F90E"}.mdi-table-settings::before{content:"\F837"}.mdi-table-tennis::before{content:"\FE4B"}.mdi-tablet::before{content:"\F4F6"}.mdi-tablet-android::before{content:"\F4F7"}.mdi-tablet-cellphone::before{content:"\F9A6"}.mdi-tablet-dashboard::before{content:"\FEEB"}.mdi-tablet-ipad::before{content:"\F4F8"}.mdi-taco::before{content:"\F761"}.mdi-tag::before{content:"\F4F9"}.mdi-tag-faces::before{content:"\F4FA"}.mdi-tag-heart::before{content:"\F68A"}.mdi-tag-heart-outline::before{content:"\FBAB"}.mdi-tag-minus::before{content:"\F90F"}.mdi-tag-minus-outline::before{content:"\F024A"}.mdi-tag-multiple::before{content:"\F4FB"}.mdi-tag-multiple-outline::before{content:"\F0322"}.mdi-tag-off::before{content:"\F024B"}.mdi-tag-off-outline::before{content:"\F024C"}.mdi-tag-outline::before{content:"\F4FC"}.mdi-tag-plus::before{content:"\F721"}.mdi-tag-plus-outline::before{content:"\F024D"}.mdi-tag-remove::before{content:"\F722"}.mdi-tag-remove-outline::before{content:"\F024E"}.mdi-tag-text::before{content:"\F024F"}.mdi-tag-text-outline::before{content:"\F4FD"}.mdi-tank::before{content:"\FD16"}.mdi-tanker-truck::before{content:"\F0006"}.mdi-tape-measure::before{content:"\FB28"}.mdi-target::before{content:"\F4FE"}.mdi-target-account::before{content:"\FBAC"}.mdi-target-variant::before{content:"\FA76"}.mdi-taxi::before{content:"\F4FF"}.mdi-tea::before{content:"\FD7A"}.mdi-tea-outline::before{content:"\FD7B"}.mdi-teach::before{content:"\F88F"}.mdi-teamviewer::before{content:"\F500"}.mdi-telegram::before{content:"\F501"}.mdi-telescope::before{content:"\FB29"}.mdi-television::before{content:"\F502"}.mdi-television-ambient-light::before{content:"\F0381"}.mdi-television-box::before{content:"\F838"}.mdi-television-classic::before{content:"\F7F3"}.mdi-television-classic-off::before{content:"\F839"}.mdi-television-clean::before{content:"\F013B"}.mdi-television-guide::before{content:"\F503"}.mdi-television-off::before{content:"\F83A"}.mdi-television-pause::before{content:"\FFA6"}.mdi-television-play::before{content:"\FEEC"}.mdi-television-stop::before{content:"\FFA7"}.mdi-temperature-celsius::before{content:"\F504"}.mdi-temperature-fahrenheit::before{content:"\F505"}.mdi-temperature-kelvin::before{content:"\F506"}.mdi-tennis::before{content:"\FD7C"}.mdi-tennis-ball::before{content:"\F507"}.mdi-tent::before{content:"\F508"}.mdi-terraform::before{content:"\F0084"}.mdi-terrain::before{content:"\F509"}.mdi-test-tube::before{content:"\F668"}.mdi-test-tube-empty::before{content:"\F910"}.mdi-test-tube-off::before{content:"\F911"}.mdi-text::before{content:"\F9A7"}.mdi-text-recognition::before{content:"\F0168"}.mdi-text-shadow::before{content:"\F669"}.mdi-text-short::before{content:"\F9A8"}.mdi-text-subject::before{content:"\F9A9"}.mdi-text-to-speech::before{content:"\F50A"}.mdi-text-to-speech-off::before{content:"\F50B"}.mdi-textarea::before{content:"\F00C0"}.mdi-textbox::before{content:"\F60E"}.mdi-textbox-lock::before{content:"\F0388"}.mdi-textbox-password::before{content:"\F7F4"}.mdi-texture::before{content:"\F50C"}.mdi-texture-box::before{content:"\F0007"}.mdi-theater::before{content:"\F50D"}.mdi-theme-light-dark::before{content:"\F50E"}.mdi-thermometer::before{content:"\F50F"}.mdi-thermometer-alert::before{content:"\FE61"}.mdi-thermometer-chevron-down::before{content:"\FE62"}.mdi-thermometer-chevron-up::before{content:"\FE63"}.mdi-thermometer-high::before{content:"\F00ED"}.mdi-thermometer-lines::before{content:"\F510"}.mdi-thermometer-low::before{content:"\F00EE"}.mdi-thermometer-minus::before{content:"\FE64"}.mdi-thermometer-plus::before{content:"\FE65"}.mdi-thermostat::before{content:"\F393"}.mdi-thermostat-box::before{content:"\F890"}.mdi-thought-bubble::before{content:"\F7F5"}.mdi-thought-bubble-outline::before{content:"\F7F6"}.mdi-thumb-down::before{content:"\F511"}.mdi-thumb-down-outline::before{content:"\F512"}.mdi-thumb-up::before{content:"\F513"}.mdi-thumb-up-outline::before{content:"\F514"}.mdi-thumbs-up-down::before{content:"\F515"}.mdi-ticket::before{content:"\F516"}.mdi-ticket-account::before{content:"\F517"}.mdi-ticket-confirmation::before{content:"\F518"}.mdi-ticket-outline::before{content:"\F912"}.mdi-ticket-percent::before{content:"\F723"}.mdi-tie::before{content:"\F519"}.mdi-tilde::before{content:"\F724"}.mdi-timelapse::before{content:"\F51A"}.mdi-timeline::before{content:"\FBAD"}.mdi-timeline-alert::before{content:"\FFB2"}.mdi-timeline-alert-outline::before{content:"\FFB5"}.mdi-timeline-clock::before{content:"\F0226"}.mdi-timeline-clock-outline::before{content:"\F0227"}.mdi-timeline-help::before{content:"\FFB6"}.mdi-timeline-help-outline::before{content:"\FFB7"}.mdi-timeline-outline::before{content:"\FBAE"}.mdi-timeline-plus::before{content:"\FFB3"}.mdi-timeline-plus-outline::before{content:"\FFB4"}.mdi-timeline-text::before{content:"\FBAF"}.mdi-timeline-text-outline::before{content:"\FBB0"}.mdi-timer::before{content:"\F51B"}.mdi-timer-10::before{content:"\F51C"}.mdi-timer-3::before{content:"\F51D"}.mdi-timer-off::before{content:"\F51E"}.mdi-timer-sand::before{content:"\F51F"}.mdi-timer-sand-empty::before{content:"\F6AC"}.mdi-timer-sand-full::before{content:"\F78B"}.mdi-timetable::before{content:"\F520"}.mdi-toaster::before{content:"\F0085"}.mdi-toaster-off::before{content:"\F01E2"}.mdi-toaster-oven::before{content:"\FCAF"}.mdi-toggle-switch::before{content:"\F521"}.mdi-toggle-switch-off::before{content:"\F522"}.mdi-toggle-switch-off-outline::before{content:"\FA18"}.mdi-toggle-switch-outline::before{content:"\FA19"}.mdi-toilet::before{content:"\F9AA"}.mdi-toolbox::before{content:"\F9AB"}.mdi-toolbox-outline::before{content:"\F9AC"}.mdi-tools::before{content:"\F0086"}.mdi-tooltip::before{content:"\F523"}.mdi-tooltip-account::before{content:"\F00C"}.mdi-tooltip-edit::before{content:"\F524"}.mdi-tooltip-edit-outline::before{content:"\F02F0"}.mdi-tooltip-image::before{content:"\F525"}.mdi-tooltip-image-outline::before{content:"\FBB1"}.mdi-tooltip-outline::before{content:"\F526"}.mdi-tooltip-plus::before{content:"\FBB2"}.mdi-tooltip-plus-outline::before{content:"\F527"}.mdi-tooltip-text::before{content:"\F528"}.mdi-tooltip-text-outline::before{content:"\FBB3"}.mdi-tooth::before{content:"\F8C2"}.mdi-tooth-outline::before{content:"\F529"}.mdi-toothbrush::before{content:"\F0154"}.mdi-toothbrush-electric::before{content:"\F0157"}.mdi-toothbrush-paste::before{content:"\F0155"}.mdi-tor::before{content:"\F52A"}.mdi-tortoise::before{content:"\FD17"}.mdi-toslink::before{content:"\F02E3"}.mdi-tournament::before{content:"\F9AD"}.mdi-tower-beach::before{content:"\F680"}.mdi-tower-fire::before{content:"\F681"}.mdi-towing::before{content:"\F83B"}.mdi-toy-brick::before{content:"\F02B3"}.mdi-toy-brick-marker::before{content:"\F02B4"}.mdi-toy-brick-marker-outline::before{content:"\F02B5"}.mdi-toy-brick-minus::before{content:"\F02B6"}.mdi-toy-brick-minus-outline::before{content:"\F02B7"}.mdi-toy-brick-outline::before{content:"\F02B8"}.mdi-toy-brick-plus::before{content:"\F02B9"}.mdi-toy-brick-plus-outline::before{content:"\F02BA"}.mdi-toy-brick-remove::before{content:"\F02BB"}.mdi-toy-brick-remove-outline::before{content:"\F02BC"}.mdi-toy-brick-search::before{content:"\F02BD"}.mdi-toy-brick-search-outline::before{content:"\F02BE"}.mdi-track-light::before{content:"\F913"}.mdi-trackpad::before{content:"\F7F7"}.mdi-trackpad-lock::before{content:"\F932"}.mdi-tractor::before{content:"\F891"}.mdi-trademark::before{content:"\FA77"}.mdi-traffic-cone::before{content:"\F03A7"}.mdi-traffic-light::before{content:"\F52B"}.mdi-train::before{content:"\F52C"}.mdi-train-car::before{content:"\FBB4"}.mdi-train-variant::before{content:"\F8C3"}.mdi-tram::before{content:"\F52D"}.mdi-tram-side::before{content:"\F0008"}.mdi-transcribe::before{content:"\F52E"}.mdi-transcribe-close::before{content:"\F52F"}.mdi-transfer::before{content:"\F0087"}.mdi-transfer-down::before{content:"\FD7D"}.mdi-transfer-left::before{content:"\FD7E"}.mdi-transfer-right::before{content:"\F530"}.mdi-transfer-up::before{content:"\FD7F"}.mdi-transit-connection::before{content:"\FD18"}.mdi-transit-connection-variant::before{content:"\FD19"}.mdi-transit-detour::before{content:"\FFA8"}.mdi-transit-transfer::before{content:"\F6AD"}.mdi-transition::before{content:"\F914"}.mdi-transition-masked::before{content:"\F915"}.mdi-translate::before{content:"\F5CA"}.mdi-translate-off::before{content:"\FE66"}.mdi-transmission-tower::before{content:"\FD1A"}.mdi-trash-can::before{content:"\FA78"}.mdi-trash-can-outline::before{content:"\FA79"}.mdi-tray::before{content:"\F02BF"}.mdi-tray-alert::before{content:"\F02C0"}.mdi-tray-full::before{content:"\F02C1"}.mdi-tray-minus::before{content:"\F02C2"}.mdi-tray-plus::before{content:"\F02C3"}.mdi-tray-remove::before{content:"\F02C4"}.mdi-treasure-chest::before{content:"\F725"}.mdi-tree::before{content:"\F531"}.mdi-tree-outline::before{content:"\FE4C"}.mdi-trello::before{content:"\F532"}.mdi-trending-down::before{content:"\F533"}.mdi-trending-neutral::before{content:"\F534"}.mdi-trending-up::before{content:"\F535"}.mdi-triangle::before{content:"\F536"}.mdi-triangle-outline::before{content:"\F537"}.mdi-triforce::before{content:"\FBB5"}.mdi-trophy::before{content:"\F538"}.mdi-trophy-award::before{content:"\F539"}.mdi-trophy-broken::before{content:"\FD80"}.mdi-trophy-outline::before{content:"\F53A"}.mdi-trophy-variant::before{content:"\F53B"}.mdi-trophy-variant-outline::before{content:"\F53C"}.mdi-truck::before{content:"\F53D"}.mdi-truck-check::before{content:"\FCB0"}.mdi-truck-check-outline::before{content:"\F02C5"}.mdi-truck-delivery::before{content:"\F53E"}.mdi-truck-delivery-outline::before{content:"\F02C6"}.mdi-truck-fast::before{content:"\F787"}.mdi-truck-fast-outline::before{content:"\F02C7"}.mdi-truck-outline::before{content:"\F02C8"}.mdi-truck-trailer::before{content:"\F726"}.mdi-trumpet::before{content:"\F00C1"}.mdi-tshirt-crew::before{content:"\FA7A"}.mdi-tshirt-crew-outline::before{content:"\F53F"}.mdi-tshirt-v::before{content:"\FA7B"}.mdi-tshirt-v-outline::before{content:"\F540"}.mdi-tumble-dryer::before{content:"\F916"}.mdi-tumble-dryer-alert::before{content:"\F01E5"}.mdi-tumble-dryer-off::before{content:"\F01E6"}.mdi-tumblr::before{content:"\F541"}.mdi-tumblr-box::before{content:"\F917"}.mdi-tumblr-reblog::before{content:"\F542"}.mdi-tune::before{content:"\F62E"}.mdi-tune-vertical::before{content:"\F66A"}.mdi-turnstile::before{content:"\FCB1"}.mdi-turnstile-outline::before{content:"\FCB2"}.mdi-turtle::before{content:"\FCB3"}.mdi-twitch::before{content:"\F543"}.mdi-twitter::before{content:"\F544"}.mdi-twitter-box::before{content:"\F545"}.mdi-twitter-circle::before{content:"\F546"}.mdi-twitter-retweet::before{content:"\F547"}.mdi-two-factor-authentication::before{content:"\F9AE"}.mdi-typewriter::before{content:"\FF4A"}.mdi-uber::before{content:"\F748"}.mdi-ubisoft::before{content:"\FBB6"}.mdi-ubuntu::before{content:"\F548"}.mdi-ufo::before{content:"\F00EF"}.mdi-ufo-outline::before{content:"\F00F0"}.mdi-ultra-high-definition::before{content:"\F7F8"}.mdi-umbraco::before{content:"\F549"}.mdi-umbrella::before{content:"\F54A"}.mdi-umbrella-closed::before{content:"\F9AF"}.mdi-umbrella-outline::before{content:"\F54B"}.mdi-undo::before{content:"\F54C"}.mdi-undo-variant::before{content:"\F54D"}.mdi-unfold-less-horizontal::before{content:"\F54E"}.mdi-unfold-less-vertical::before{content:"\F75F"}.mdi-unfold-more-horizontal::before{content:"\F54F"}.mdi-unfold-more-vertical::before{content:"\F760"}.mdi-ungroup::before{content:"\F550"}.mdi-unicode::before{content:"\FEED"}.mdi-unity::before{content:"\F6AE"}.mdi-unreal::before{content:"\F9B0"}.mdi-untappd::before{content:"\F551"}.mdi-update::before{content:"\F6AF"}.mdi-upload::before{content:"\F552"}.mdi-upload-lock::before{content:"\F039E"}.mdi-upload-lock-outline::before{content:"\F039F"}.mdi-upload-multiple::before{content:"\F83C"}.mdi-upload-network::before{content:"\F6F5"}.mdi-upload-network-outline::before{content:"\FCB4"}.mdi-upload-off::before{content:"\F00F1"}.mdi-upload-off-outline::before{content:"\F00F2"}.mdi-upload-outline::before{content:"\FE67"}.mdi-usb::before{content:"\F553"}.mdi-usb-flash-drive::before{content:"\F02C9"}.mdi-usb-flash-drive-outline::before{content:"\F02CA"}.mdi-usb-port::before{content:"\F021B"}.mdi-valve::before{content:"\F0088"}.mdi-valve-closed::before{content:"\F0089"}.mdi-valve-open::before{content:"\F008A"}.mdi-van-passenger::before{content:"\F7F9"}.mdi-van-utility::before{content:"\F7FA"}.mdi-vanish::before{content:"\F7FB"}.mdi-vanity-light::before{content:"\F020C"}.mdi-variable::before{content:"\FAE6"}.mdi-variable-box::before{content:"\F013C"}.mdi-vector-arrange-above::before{content:"\F554"}.mdi-vector-arrange-below::before{content:"\F555"}.mdi-vector-bezier::before{content:"\FAE7"}.mdi-vector-circle::before{content:"\F556"}.mdi-vector-circle-variant::before{content:"\F557"}.mdi-vector-combine::before{content:"\F558"}.mdi-vector-curve::before{content:"\F559"}.mdi-vector-difference::before{content:"\F55A"}.mdi-vector-difference-ab::before{content:"\F55B"}.mdi-vector-difference-ba::before{content:"\F55C"}.mdi-vector-ellipse::before{content:"\F892"}.mdi-vector-intersection::before{content:"\F55D"}.mdi-vector-line::before{content:"\F55E"}.mdi-vector-link::before{content:"\F0009"}.mdi-vector-point::before{content:"\F55F"}.mdi-vector-polygon::before{content:"\F560"}.mdi-vector-polyline::before{content:"\F561"}.mdi-vector-polyline-edit::before{content:"\F0250"}.mdi-vector-polyline-minus::before{content:"\F0251"}.mdi-vector-polyline-plus::before{content:"\F0252"}.mdi-vector-polyline-remove::before{content:"\F0253"}.mdi-vector-radius::before{content:"\F749"}.mdi-vector-rectangle::before{content:"\F5C6"}.mdi-vector-selection::before{content:"\F562"}.mdi-vector-square::before{content:"\F001"}.mdi-vector-triangle::before{content:"\F563"}.mdi-vector-union::before{content:"\F564"}.mdi-venmo::before{content:"\F578"}.mdi-vhs::before{content:"\FA1A"}.mdi-vibrate::before{content:"\F566"}.mdi-vibrate-off::before{content:"\FCB5"}.mdi-video::before{content:"\F567"}.mdi-video-3d::before{content:"\F7FC"}.mdi-video-3d-variant::before{content:"\FEEE"}.mdi-video-4k-box::before{content:"\F83D"}.mdi-video-account::before{content:"\F918"}.mdi-video-check::before{content:"\F008B"}.mdi-video-check-outline::before{content:"\F008C"}.mdi-video-image::before{content:"\F919"}.mdi-video-input-antenna::before{content:"\F83E"}.mdi-video-input-component::before{content:"\F83F"}.mdi-video-input-hdmi::before{content:"\F840"}.mdi-video-input-scart::before{content:"\FFA9"}.mdi-video-input-svideo::before{content:"\F841"}.mdi-video-minus::before{content:"\F9B1"}.mdi-video-off::before{content:"\F568"}.mdi-video-off-outline::before{content:"\FBB7"}.mdi-video-outline::before{content:"\FBB8"}.mdi-video-plus::before{content:"\F9B2"}.mdi-video-stabilization::before{content:"\F91A"}.mdi-video-switch::before{content:"\F569"}.mdi-video-vintage::before{content:"\FA1B"}.mdi-video-wireless::before{content:"\FEEF"}.mdi-video-wireless-outline::before{content:"\FEF0"}.mdi-view-agenda::before{content:"\F56A"}.mdi-view-agenda-outline::before{content:"\F0203"}.mdi-view-array::before{content:"\F56B"}.mdi-view-carousel::before{content:"\F56C"}.mdi-view-column::before{content:"\F56D"}.mdi-view-comfy::before{content:"\FE4D"}.mdi-view-compact::before{content:"\FE4E"}.mdi-view-compact-outline::before{content:"\FE4F"}.mdi-view-dashboard::before{content:"\F56E"}.mdi-view-dashboard-outline::before{content:"\FA1C"}.mdi-view-dashboard-variant::before{content:"\F842"}.mdi-view-day::before{content:"\F56F"}.mdi-view-grid::before{content:"\F570"}.mdi-view-grid-outline::before{content:"\F0204"}.mdi-view-grid-plus::before{content:"\FFAA"}.mdi-view-grid-plus-outline::before{content:"\F0205"}.mdi-view-headline::before{content:"\F571"}.mdi-view-list::before{content:"\F572"}.mdi-view-module::before{content:"\F573"}.mdi-view-parallel::before{content:"\F727"}.mdi-view-quilt::before{content:"\F574"}.mdi-view-sequential::before{content:"\F728"}.mdi-view-split-horizontal::before{content:"\FBA7"}.mdi-view-split-vertical::before{content:"\FBA8"}.mdi-view-stream::before{content:"\F575"}.mdi-view-week::before{content:"\F576"}.mdi-vimeo::before{content:"\F577"}.mdi-violin::before{content:"\F60F"}.mdi-virtual-reality::before{content:"\F893"}.mdi-visual-studio::before{content:"\F610"}.mdi-visual-studio-code::before{content:"\FA1D"}.mdi-vk::before{content:"\F579"}.mdi-vk-box::before{content:"\F57A"}.mdi-vk-circle::before{content:"\F57B"}.mdi-vlc::before{content:"\F57C"}.mdi-voice::before{content:"\F5CB"}.mdi-voice-off::before{content:"\FEF1"}.mdi-voicemail::before{content:"\F57D"}.mdi-volleyball::before{content:"\F9B3"}.mdi-volume-high::before{content:"\F57E"}.mdi-volume-low::before{content:"\F57F"}.mdi-volume-medium::before{content:"\F580"}.mdi-volume-minus::before{content:"\F75D"}.mdi-volume-mute::before{content:"\F75E"}.mdi-volume-off::before{content:"\F581"}.mdi-volume-plus::before{content:"\F75C"}.mdi-volume-source::before{content:"\F014B"}.mdi-volume-variant-off::before{content:"\FE68"}.mdi-volume-vibrate::before{content:"\F014C"}.mdi-vote::before{content:"\FA1E"}.mdi-vote-outline::before{content:"\FA1F"}.mdi-vpn::before{content:"\F582"}.mdi-vuejs::before{content:"\F843"}.mdi-vuetify::before{content:"\FE50"}.mdi-walk::before{content:"\F583"}.mdi-wall::before{content:"\F7FD"}.mdi-wall-sconce::before{content:"\F91B"}.mdi-wall-sconce-flat::before{content:"\F91C"}.mdi-wall-sconce-variant::before{content:"\F91D"}.mdi-wallet::before{content:"\F584"}.mdi-wallet-giftcard::before{content:"\F585"}.mdi-wallet-membership::before{content:"\F586"}.mdi-wallet-outline::before{content:"\FBB9"}.mdi-wallet-plus::before{content:"\FFAB"}.mdi-wallet-plus-outline::before{content:"\FFAC"}.mdi-wallet-travel::before{content:"\F587"}.mdi-wallpaper::before{content:"\FE69"}.mdi-wan::before{content:"\F588"}.mdi-wardrobe::before{content:"\FFAD"}.mdi-wardrobe-outline::before{content:"\FFAE"}.mdi-warehouse::before{content:"\FFBB"}.mdi-washing-machine::before{content:"\F729"}.mdi-washing-machine-alert::before{content:"\F01E7"}.mdi-washing-machine-off::before{content:"\F01E8"}.mdi-watch::before{content:"\F589"}.mdi-watch-export::before{content:"\F58A"}.mdi-watch-export-variant::before{content:"\F894"}.mdi-watch-import::before{content:"\F58B"}.mdi-watch-import-variant::before{content:"\F895"}.mdi-watch-variant::before{content:"\F896"}.mdi-watch-vibrate::before{content:"\F6B0"}.mdi-watch-vibrate-off::before{content:"\FCB6"}.mdi-water::before{content:"\F58C"}.mdi-water-boiler::before{content:"\FFAF"}.mdi-water-boiler-alert::before{content:"\F01DE"}.mdi-water-boiler-off::before{content:"\F01DF"}.mdi-water-off::before{content:"\F58D"}.mdi-water-outline::before{content:"\FE6A"}.mdi-water-percent::before{content:"\F58E"}.mdi-water-polo::before{content:"\F02CB"}.mdi-water-pump::before{content:"\F58F"}.mdi-water-pump-off::before{content:"\FFB0"}.mdi-water-well::before{content:"\F008D"}.mdi-water-well-outline::before{content:"\F008E"}.mdi-watermark::before{content:"\F612"}.mdi-wave::before{content:"\FF4B"}.mdi-waves::before{content:"\F78C"}.mdi-waze::before{content:"\FBBA"}.mdi-weather-cloudy::before{content:"\F590"}.mdi-weather-cloudy-alert::before{content:"\FF4C"}.mdi-weather-cloudy-arrow-right::before{content:"\FE51"}.mdi-weather-fog::before{content:"\F591"}.mdi-weather-hail::before{content:"\F592"}.mdi-weather-hazy::before{content:"\FF4D"}.mdi-weather-hurricane::before{content:"\F897"}.mdi-weather-lightning::before{content:"\F593"}.mdi-weather-lightning-rainy::before{content:"\F67D"}.mdi-weather-night::before{content:"\F594"}.mdi-weather-night-partly-cloudy::before{content:"\FF4E"}.mdi-weather-partly-cloudy::before{content:"\F595"}.mdi-weather-partly-lightning::before{content:"\FF4F"}.mdi-weather-partly-rainy::before{content:"\FF50"}.mdi-weather-partly-snowy::before{content:"\FF51"}.mdi-weather-partly-snowy-rainy::before{content:"\FF52"}.mdi-weather-pouring::before{content:"\F596"}.mdi-weather-rainy::before{content:"\F597"}.mdi-weather-snowy::before{content:"\F598"}.mdi-weather-snowy-heavy::before{content:"\FF53"}.mdi-weather-snowy-rainy::before{content:"\F67E"}.mdi-weather-sunny::before{content:"\F599"}.mdi-weather-sunny-alert::before{content:"\FF54"}.mdi-weather-sunset::before{content:"\F59A"}.mdi-weather-sunset-down::before{content:"\F59B"}.mdi-weather-sunset-up::before{content:"\F59C"}.mdi-weather-tornado::before{content:"\FF55"}.mdi-weather-windy::before{content:"\F59D"}.mdi-weather-windy-variant::before{content:"\F59E"}.mdi-web::before{content:"\F59F"}.mdi-web-box::before{content:"\FFB1"}.mdi-web-clock::before{content:"\F0275"}.mdi-webcam::before{content:"\F5A0"}.mdi-webhook::before{content:"\F62F"}.mdi-webpack::before{content:"\F72A"}.mdi-webrtc::before{content:"\F0273"}.mdi-wechat::before{content:"\F611"}.mdi-weight::before{content:"\F5A1"}.mdi-weight-gram::before{content:"\FD1B"}.mdi-weight-kilogram::before{content:"\F5A2"}.mdi-weight-lifter::before{content:"\F0188"}.mdi-weight-pound::before{content:"\F9B4"}.mdi-whatsapp::before{content:"\F5A3"}.mdi-wheelchair-accessibility::before{content:"\F5A4"}.mdi-whistle::before{content:"\F9B5"}.mdi-whistle-outline::before{content:"\F02E7"}.mdi-white-balance-auto::before{content:"\F5A5"}.mdi-white-balance-incandescent::before{content:"\F5A6"}.mdi-white-balance-iridescent::before{content:"\F5A7"}.mdi-white-balance-sunny::before{content:"\F5A8"}.mdi-widgets::before{content:"\F72B"}.mdi-widgets-outline::before{content:"\F0380"}.mdi-wifi::before{content:"\F5A9"}.mdi-wifi-off::before{content:"\F5AA"}.mdi-wifi-star::before{content:"\FE6B"}.mdi-wifi-strength-1::before{content:"\F91E"}.mdi-wifi-strength-1-alert::before{content:"\F91F"}.mdi-wifi-strength-1-lock::before{content:"\F920"}.mdi-wifi-strength-2::before{content:"\F921"}.mdi-wifi-strength-2-alert::before{content:"\F922"}.mdi-wifi-strength-2-lock::before{content:"\F923"}.mdi-wifi-strength-3::before{content:"\F924"}.mdi-wifi-strength-3-alert::before{content:"\F925"}.mdi-wifi-strength-3-lock::before{content:"\F926"}.mdi-wifi-strength-4::before{content:"\F927"}.mdi-wifi-strength-4-alert::before{content:"\F928"}.mdi-wifi-strength-4-lock::before{content:"\F929"}.mdi-wifi-strength-alert-outline::before{content:"\F92A"}.mdi-wifi-strength-lock-outline::before{content:"\F92B"}.mdi-wifi-strength-off::before{content:"\F92C"}.mdi-wifi-strength-off-outline::before{content:"\F92D"}.mdi-wifi-strength-outline::before{content:"\F92E"}.mdi-wii::before{content:"\F5AB"}.mdi-wiiu::before{content:"\F72C"}.mdi-wikipedia::before{content:"\F5AC"}.mdi-wind-turbine::before{content:"\FD81"}.mdi-window-close::before{content:"\F5AD"}.mdi-window-closed::before{content:"\F5AE"}.mdi-window-closed-variant::before{content:"\F0206"}.mdi-window-maximize::before{content:"\F5AF"}.mdi-window-minimize::before{content:"\F5B0"}.mdi-window-open::before{content:"\F5B1"}.mdi-window-open-variant::before{content:"\F0207"}.mdi-window-restore::before{content:"\F5B2"}.mdi-window-shutter::before{content:"\F0147"}.mdi-window-shutter-alert::before{content:"\F0148"}.mdi-window-shutter-open::before{content:"\F0149"}.mdi-windows::before{content:"\F5B3"}.mdi-windows-classic::before{content:"\FA20"}.mdi-wiper::before{content:"\FAE8"}.mdi-wiper-wash::before{content:"\FD82"}.mdi-wordpress::before{content:"\F5B4"}.mdi-worker::before{content:"\F5B5"}.mdi-wrap::before{content:"\F5B6"}.mdi-wrap-disabled::before{content:"\FBBB"}.mdi-wrench::before{content:"\F5B7"}.mdi-wrench-outline::before{content:"\FBBC"}.mdi-wunderlist::before{content:"\F5B8"}.mdi-xamarin::before{content:"\F844"}.mdi-xamarin-outline::before{content:"\F845"}.mdi-xaml::before{content:"\F673"}.mdi-xbox::before{content:"\F5B9"}.mdi-xbox-controller::before{content:"\F5BA"}.mdi-xbox-controller-battery-alert::before{content:"\F74A"}.mdi-xbox-controller-battery-charging::before{content:"\FA21"}.mdi-xbox-controller-battery-empty::before{content:"\F74B"}.mdi-xbox-controller-battery-full::before{content:"\F74C"}.mdi-xbox-controller-battery-low::before{content:"\F74D"}.mdi-xbox-controller-battery-medium::before{content:"\F74E"}.mdi-xbox-controller-battery-unknown::before{content:"\F74F"}.mdi-xbox-controller-menu::before{content:"\FE52"}.mdi-xbox-controller-off::before{content:"\F5BB"}.mdi-xbox-controller-view::before{content:"\FE53"}.mdi-xda::before{content:"\F5BC"}.mdi-xing::before{content:"\F5BD"}.mdi-xing-box::before{content:"\F5BE"}.mdi-xing-circle::before{content:"\F5BF"}.mdi-xml::before{content:"\F5C0"}.mdi-xmpp::before{content:"\F7FE"}.mdi-yahoo::before{content:"\FB2A"}.mdi-yammer::before{content:"\F788"}.mdi-yeast::before{content:"\F5C1"}.mdi-yelp::before{content:"\F5C2"}.mdi-yin-yang::before{content:"\F67F"}.mdi-yoga::before{content:"\F01A7"}.mdi-youtube::before{content:"\F5C3"}.mdi-youtube-creator-studio::before{content:"\F846"}.mdi-youtube-gaming::before{content:"\F847"}.mdi-youtube-subscription::before{content:"\FD1C"}.mdi-youtube-tv::before{content:"\F448"}.mdi-z-wave::before{content:"\FAE9"}.mdi-zend::before{content:"\FAEA"}.mdi-zigbee::before{content:"\FD1D"}.mdi-zip-box::before{content:"\F5C4"}.mdi-zip-box-outline::before{content:"\F001B"}.mdi-zip-disk::before{content:"\FA22"}.mdi-zodiac-aquarius::before{content:"\FA7C"}.mdi-zodiac-aries::before{content:"\FA7D"}.mdi-zodiac-cancer::before{content:"\FA7E"}.mdi-zodiac-capricorn::before{content:"\FA7F"}.mdi-zodiac-gemini::before{content:"\FA80"}.mdi-zodiac-leo::before{content:"\FA81"}.mdi-zodiac-libra::before{content:"\FA82"}.mdi-zodiac-pisces::before{content:"\FA83"}.mdi-zodiac-sagittarius::before{content:"\FA84"}.mdi-zodiac-scorpio::before{content:"\FA85"}.mdi-zodiac-taurus::before{content:"\FA86"}.mdi-zodiac-virgo::before{content:"\FA87"}.mdi-blank::before{content:"\F68C";visibility:hidden}.mdi-18px.mdi-set,.mdi-18px.mdi:before{font-size:18px}.mdi-24px.mdi-set,.mdi-24px.mdi:before{font-size:24px}.mdi-36px.mdi-set,.mdi-36px.mdi:before{font-size:36px}.mdi-48px.mdi-set,.mdi-48px.mdi:before{font-size:48px}.mdi-dark:before{color:rgba(0,0,0,0.54)}.mdi-dark.mdi-inactive:before{color:rgba(0,0,0,0.26)}.mdi-light:before{color:#fff}.mdi-light.mdi-inactive:before{color:rgba(255,255,255,0.3)}.mdi-rotate-45:before{-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg)}.mdi-rotate-90:before{-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.mdi-rotate-135:before{-webkit-transform:rotate(135deg);-ms-transform:rotate(135deg);transform:rotate(135deg)}.mdi-rotate-180:before{-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.mdi-rotate-225:before{-webkit-transform:rotate(225deg);-ms-transform:rotate(225deg);transform:rotate(225deg)}.mdi-rotate-270:before{-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.mdi-rotate-315:before{-webkit-transform:rotate(315deg);-ms-transform:rotate(315deg);transform:rotate(315deg)}.mdi-flip-h:before{-webkit-transform:scaleX(-1);transform:scaleX(-1);filter:FlipH;-ms-filter:"FlipH"}.mdi-flip-v:before{-webkit-transform:scaleY(-1);transform:scaleY(-1);filter:FlipV;-ms-filter:"FlipV"}.mdi-spin:before{-webkit-animation:mdi-spin 2s infinite linear;animation:mdi-spin 2s infinite linear}@-webkit-keyframes mdi-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes mdi-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}
+@font-face {
+ font-family: "Material Design Icons";
+ src: url("./fonts/materialdesignicons-webfont-4.9.95.eot");
+ src: url("./fonts/materialdesignicons-webfont-4.9.95.woff2") format("woff2"),
+ url("./fonts/materialdesignicons-webfont-4.9.95.woff") format("woff"),
+ url("./fonts/materialdesignicons-webfont-4.9.95.ttf") format("truetype");
+ font-weight: normal;
+ font-style: normal;
+}
+.mdi:before,
+.mdi-set {
+ display: inline-block;
+ font: normal normal normal 24px/1 "Material Design Icons";
+ font-size: inherit;
+ text-rendering: auto;
+ line-height: inherit;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+.mdi-ab-testing::before {
+ content: "\F001C";
+}
+.mdi-abjad-arabic::before {
+ content: "\F0353";
+}
+.mdi-abjad-hebrew::before {
+ content: "\F0354";
+}
+.mdi-abugida-devanagari::before {
+ content: "\F0355";
+}
+.mdi-abugida-thai::before {
+ content: "\F0356";
+}
+.mdi-access-point::before {
+ content: "\F002";
+}
+.mdi-access-point-network::before {
+ content: "\F003";
+}
+.mdi-access-point-network-off::before {
+ content: "\FBBD";
+}
+.mdi-account::before {
+ content: "\F004";
+}
+.mdi-account-alert::before {
+ content: "\F005";
+}
+.mdi-account-alert-outline::before {
+ content: "\FB2C";
+}
+.mdi-account-arrow-left::before {
+ content: "\FB2D";
+}
+.mdi-account-arrow-left-outline::before {
+ content: "\FB2E";
+}
+.mdi-account-arrow-right::before {
+ content: "\FB2F";
+}
+.mdi-account-arrow-right-outline::before {
+ content: "\FB30";
+}
+.mdi-account-badge::before {
+ content: "\FD83";
+}
+.mdi-account-badge-alert::before {
+ content: "\FD84";
+}
+.mdi-account-badge-alert-outline::before {
+ content: "\FD85";
+}
+.mdi-account-badge-horizontal::before {
+ content: "\FDF0";
+}
+.mdi-account-badge-horizontal-outline::before {
+ content: "\FDF1";
+}
+.mdi-account-badge-outline::before {
+ content: "\FD86";
+}
+.mdi-account-box::before {
+ content: "\F006";
+}
+.mdi-account-box-multiple::before {
+ content: "\F933";
+}
+.mdi-account-box-multiple-outline::before {
+ content: "\F002C";
+}
+.mdi-account-box-outline::before {
+ content: "\F007";
+}
+.mdi-account-cancel::before {
+ content: "\F030A";
+}
+.mdi-account-cancel-outline::before {
+ content: "\F030B";
+}
+.mdi-account-card-details::before {
+ content: "\F5D2";
+}
+.mdi-account-card-details-outline::before {
+ content: "\FD87";
+}
+.mdi-account-cash::before {
+ content: "\F00C2";
+}
+.mdi-account-cash-outline::before {
+ content: "\F00C3";
+}
+.mdi-account-check::before {
+ content: "\F008";
+}
+.mdi-account-check-outline::before {
+ content: "\FBBE";
+}
+.mdi-account-child::before {
+ content: "\FA88";
+}
+.mdi-account-child-circle::before {
+ content: "\FA89";
+}
+.mdi-account-child-outline::before {
+ content: "\F00F3";
+}
+.mdi-account-circle::before {
+ content: "\F009";
+}
+.mdi-account-circle-outline::before {
+ content: "\FB31";
+}
+.mdi-account-clock::before {
+ content: "\FB32";
+}
+.mdi-account-clock-outline::before {
+ content: "\FB33";
+}
+.mdi-account-cog::before {
+ content: "\F039B";
+}
+.mdi-account-cog-outline::before {
+ content: "\F039C";
+}
+.mdi-account-convert::before {
+ content: "\F00A";
+}
+.mdi-account-convert-outline::before {
+ content: "\F032C";
+}
+.mdi-account-details::before {
+ content: "\F631";
+}
+.mdi-account-details-outline::before {
+ content: "\F039D";
+}
+.mdi-account-edit::before {
+ content: "\F6BB";
+}
+.mdi-account-edit-outline::before {
+ content: "\F001D";
+}
+.mdi-account-group::before {
+ content: "\F848";
+}
+.mdi-account-group-outline::before {
+ content: "\FB34";
+}
+.mdi-account-heart::before {
+ content: "\F898";
+}
+.mdi-account-heart-outline::before {
+ content: "\FBBF";
+}
+.mdi-account-key::before {
+ content: "\F00B";
+}
+.mdi-account-key-outline::before {
+ content: "\FBC0";
+}
+.mdi-account-lock::before {
+ content: "\F0189";
+}
+.mdi-account-lock-outline::before {
+ content: "\F018A";
+}
+.mdi-account-minus::before {
+ content: "\F00D";
+}
+.mdi-account-minus-outline::before {
+ content: "\FAEB";
+}
+.mdi-account-multiple::before {
+ content: "\F00E";
+}
+.mdi-account-multiple-check::before {
+ content: "\F8C4";
+}
+.mdi-account-multiple-check-outline::before {
+ content: "\F0229";
+}
+.mdi-account-multiple-minus::before {
+ content: "\F5D3";
+}
+.mdi-account-multiple-minus-outline::before {
+ content: "\FBC1";
+}
+.mdi-account-multiple-outline::before {
+ content: "\F00F";
+}
+.mdi-account-multiple-plus::before {
+ content: "\F010";
+}
+.mdi-account-multiple-plus-outline::before {
+ content: "\F7FF";
+}
+.mdi-account-multiple-remove::before {
+ content: "\F0235";
+}
+.mdi-account-multiple-remove-outline::before {
+ content: "\F0236";
+}
+.mdi-account-network::before {
+ content: "\F011";
+}
+.mdi-account-network-outline::before {
+ content: "\FBC2";
+}
+.mdi-account-off::before {
+ content: "\F012";
+}
+.mdi-account-off-outline::before {
+ content: "\FBC3";
+}
+.mdi-account-outline::before {
+ content: "\F013";
+}
+.mdi-account-plus::before {
+ content: "\F014";
+}
+.mdi-account-plus-outline::before {
+ content: "\F800";
+}
+.mdi-account-question::before {
+ content: "\FB35";
+}
+.mdi-account-question-outline::before {
+ content: "\FB36";
+}
+.mdi-account-remove::before {
+ content: "\F015";
+}
+.mdi-account-remove-outline::before {
+ content: "\FAEC";
+}
+.mdi-account-search::before {
+ content: "\F016";
+}
+.mdi-account-search-outline::before {
+ content: "\F934";
+}
+.mdi-account-settings::before {
+ content: "\F630";
+}
+.mdi-account-settings-outline::before {
+ content: "\F00F4";
+}
+.mdi-account-star::before {
+ content: "\F017";
+}
+.mdi-account-star-outline::before {
+ content: "\FBC4";
+}
+.mdi-account-supervisor::before {
+ content: "\FA8A";
+}
+.mdi-account-supervisor-circle::before {
+ content: "\FA8B";
+}
+.mdi-account-supervisor-outline::before {
+ content: "\F0158";
+}
+.mdi-account-switch::before {
+ content: "\F019";
+}
+.mdi-account-tie::before {
+ content: "\FCBF";
+}
+.mdi-account-tie-outline::before {
+ content: "\F00F5";
+}
+.mdi-account-tie-voice::before {
+ content: "\F0333";
+}
+.mdi-account-tie-voice-off::before {
+ content: "\F0335";
+}
+.mdi-account-tie-voice-off-outline::before {
+ content: "\F0336";
+}
+.mdi-account-tie-voice-outline::before {
+ content: "\F0334";
+}
+.mdi-accusoft::before {
+ content: "\F849";
+}
+.mdi-adjust::before {
+ content: "\F01A";
+}
+.mdi-adobe::before {
+ content: "\F935";
+}
+.mdi-adobe-acrobat::before {
+ content: "\FFBD";
+}
+.mdi-air-conditioner::before {
+ content: "\F01B";
+}
+.mdi-air-filter::before {
+ content: "\FD1F";
+}
+.mdi-air-horn::before {
+ content: "\FD88";
+}
+.mdi-air-humidifier::before {
+ content: "\F00C4";
+}
+.mdi-air-purifier::before {
+ content: "\FD20";
+}
+.mdi-airbag::before {
+ content: "\FBC5";
+}
+.mdi-airballoon::before {
+ content: "\F01C";
+}
+.mdi-airballoon-outline::before {
+ content: "\F002D";
+}
+.mdi-airplane::before {
+ content: "\F01D";
+}
+.mdi-airplane-landing::before {
+ content: "\F5D4";
+}
+.mdi-airplane-off::before {
+ content: "\F01E";
+}
+.mdi-airplane-takeoff::before {
+ content: "\F5D5";
+}
+.mdi-airplay::before {
+ content: "\F01F";
+}
+.mdi-airport::before {
+ content: "\F84A";
+}
+.mdi-alarm::before {
+ content: "\F020";
+}
+.mdi-alarm-bell::before {
+ content: "\F78D";
+}
+.mdi-alarm-check::before {
+ content: "\F021";
+}
+.mdi-alarm-light::before {
+ content: "\F78E";
+}
+.mdi-alarm-light-outline::before {
+ content: "\FBC6";
+}
+.mdi-alarm-multiple::before {
+ content: "\F022";
+}
+.mdi-alarm-note::before {
+ content: "\FE8E";
+}
+.mdi-alarm-note-off::before {
+ content: "\FE8F";
+}
+.mdi-alarm-off::before {
+ content: "\F023";
+}
+.mdi-alarm-plus::before {
+ content: "\F024";
+}
+.mdi-alarm-snooze::before {
+ content: "\F68D";
+}
+.mdi-album::before {
+ content: "\F025";
+}
+.mdi-alert::before {
+ content: "\F026";
+}
+.mdi-alert-box::before {
+ content: "\F027";
+}
+.mdi-alert-box-outline::before {
+ content: "\FCC0";
+}
+.mdi-alert-circle::before {
+ content: "\F028";
+}
+.mdi-alert-circle-check::before {
+ content: "\F0218";
+}
+.mdi-alert-circle-check-outline::before {
+ content: "\F0219";
+}
+.mdi-alert-circle-outline::before {
+ content: "\F5D6";
+}
+.mdi-alert-decagram::before {
+ content: "\F6BC";
+}
+.mdi-alert-decagram-outline::before {
+ content: "\FCC1";
+}
+.mdi-alert-octagon::before {
+ content: "\F029";
+}
+.mdi-alert-octagon-outline::before {
+ content: "\FCC2";
+}
+.mdi-alert-octagram::before {
+ content: "\F766";
+}
+.mdi-alert-octagram-outline::before {
+ content: "\FCC3";
+}
+.mdi-alert-outline::before {
+ content: "\F02A";
+}
+.mdi-alert-rhombus::before {
+ content: "\F01F9";
+}
+.mdi-alert-rhombus-outline::before {
+ content: "\F01FA";
+}
+.mdi-alien::before {
+ content: "\F899";
+}
+.mdi-alien-outline::before {
+ content: "\F00F6";
+}
+.mdi-align-horizontal-center::before {
+ content: "\F01EE";
+}
+.mdi-align-horizontal-left::before {
+ content: "\F01ED";
+}
+.mdi-align-horizontal-right::before {
+ content: "\F01EF";
+}
+.mdi-align-vertical-bottom::before {
+ content: "\F01F0";
+}
+.mdi-align-vertical-center::before {
+ content: "\F01F1";
+}
+.mdi-align-vertical-top::before {
+ content: "\F01F2";
+}
+.mdi-all-inclusive::before {
+ content: "\F6BD";
+}
+.mdi-allergy::before {
+ content: "\F0283";
+}
+.mdi-alpha::before {
+ content: "\F02B";
+}
+.mdi-alpha-a::before {
+ content: "\41";
+}
+.mdi-alpha-a-box::before {
+ content: "\FAED";
+}
+.mdi-alpha-a-box-outline::before {
+ content: "\FBC7";
+}
+.mdi-alpha-a-circle::before {
+ content: "\FBC8";
+}
+.mdi-alpha-a-circle-outline::before {
+ content: "\FBC9";
+}
+.mdi-alpha-b::before {
+ content: "\42";
+}
+.mdi-alpha-b-box::before {
+ content: "\FAEE";
+}
+.mdi-alpha-b-box-outline::before {
+ content: "\FBCA";
+}
+.mdi-alpha-b-circle::before {
+ content: "\FBCB";
+}
+.mdi-alpha-b-circle-outline::before {
+ content: "\FBCC";
+}
+.mdi-alpha-c::before {
+ content: "\43";
+}
+.mdi-alpha-c-box::before {
+ content: "\FAEF";
+}
+.mdi-alpha-c-box-outline::before {
+ content: "\FBCD";
+}
+.mdi-alpha-c-circle::before {
+ content: "\FBCE";
+}
+.mdi-alpha-c-circle-outline::before {
+ content: "\FBCF";
+}
+.mdi-alpha-d::before {
+ content: "\44";
+}
+.mdi-alpha-d-box::before {
+ content: "\FAF0";
+}
+.mdi-alpha-d-box-outline::before {
+ content: "\FBD0";
+}
+.mdi-alpha-d-circle::before {
+ content: "\FBD1";
+}
+.mdi-alpha-d-circle-outline::before {
+ content: "\FBD2";
+}
+.mdi-alpha-e::before {
+ content: "\45";
+}
+.mdi-alpha-e-box::before {
+ content: "\FAF1";
+}
+.mdi-alpha-e-box-outline::before {
+ content: "\FBD3";
+}
+.mdi-alpha-e-circle::before {
+ content: "\FBD4";
+}
+.mdi-alpha-e-circle-outline::before {
+ content: "\FBD5";
+}
+.mdi-alpha-f::before {
+ content: "\46";
+}
+.mdi-alpha-f-box::before {
+ content: "\FAF2";
+}
+.mdi-alpha-f-box-outline::before {
+ content: "\FBD6";
+}
+.mdi-alpha-f-circle::before {
+ content: "\FBD7";
+}
+.mdi-alpha-f-circle-outline::before {
+ content: "\FBD8";
+}
+.mdi-alpha-g::before {
+ content: "\47";
+}
+.mdi-alpha-g-box::before {
+ content: "\FAF3";
+}
+.mdi-alpha-g-box-outline::before {
+ content: "\FBD9";
+}
+.mdi-alpha-g-circle::before {
+ content: "\FBDA";
+}
+.mdi-alpha-g-circle-outline::before {
+ content: "\FBDB";
+}
+.mdi-alpha-h::before {
+ content: "\48";
+}
+.mdi-alpha-h-box::before {
+ content: "\FAF4";
+}
+.mdi-alpha-h-box-outline::before {
+ content: "\FBDC";
+}
+.mdi-alpha-h-circle::before {
+ content: "\FBDD";
+}
+.mdi-alpha-h-circle-outline::before {
+ content: "\FBDE";
+}
+.mdi-alpha-i::before {
+ content: "\49";
+}
+.mdi-alpha-i-box::before {
+ content: "\FAF5";
+}
+.mdi-alpha-i-box-outline::before {
+ content: "\FBDF";
+}
+.mdi-alpha-i-circle::before {
+ content: "\FBE0";
+}
+.mdi-alpha-i-circle-outline::before {
+ content: "\FBE1";
+}
+.mdi-alpha-j::before {
+ content: "\4A";
+}
+.mdi-alpha-j-box::before {
+ content: "\FAF6";
+}
+.mdi-alpha-j-box-outline::before {
+ content: "\FBE2";
+}
+.mdi-alpha-j-circle::before {
+ content: "\FBE3";
+}
+.mdi-alpha-j-circle-outline::before {
+ content: "\FBE4";
+}
+.mdi-alpha-k::before {
+ content: "\4B";
+}
+.mdi-alpha-k-box::before {
+ content: "\FAF7";
+}
+.mdi-alpha-k-box-outline::before {
+ content: "\FBE5";
+}
+.mdi-alpha-k-circle::before {
+ content: "\FBE6";
+}
+.mdi-alpha-k-circle-outline::before {
+ content: "\FBE7";
+}
+.mdi-alpha-l::before {
+ content: "\4C";
+}
+.mdi-alpha-l-box::before {
+ content: "\FAF8";
+}
+.mdi-alpha-l-box-outline::before {
+ content: "\FBE8";
+}
+.mdi-alpha-l-circle::before {
+ content: "\FBE9";
+}
+.mdi-alpha-l-circle-outline::before {
+ content: "\FBEA";
+}
+.mdi-alpha-m::before {
+ content: "\4D";
+}
+.mdi-alpha-m-box::before {
+ content: "\FAF9";
+}
+.mdi-alpha-m-box-outline::before {
+ content: "\FBEB";
+}
+.mdi-alpha-m-circle::before {
+ content: "\FBEC";
+}
+.mdi-alpha-m-circle-outline::before {
+ content: "\FBED";
+}
+.mdi-alpha-n::before {
+ content: "\4E";
+}
+.mdi-alpha-n-box::before {
+ content: "\FAFA";
+}
+.mdi-alpha-n-box-outline::before {
+ content: "\FBEE";
+}
+.mdi-alpha-n-circle::before {
+ content: "\FBEF";
+}
+.mdi-alpha-n-circle-outline::before {
+ content: "\FBF0";
+}
+.mdi-alpha-o::before {
+ content: "\4F";
+}
+.mdi-alpha-o-box::before {
+ content: "\FAFB";
+}
+.mdi-alpha-o-box-outline::before {
+ content: "\FBF1";
+}
+.mdi-alpha-o-circle::before {
+ content: "\FBF2";
+}
+.mdi-alpha-o-circle-outline::before {
+ content: "\FBF3";
+}
+.mdi-alpha-p::before {
+ content: "\50";
+}
+.mdi-alpha-p-box::before {
+ content: "\FAFC";
+}
+.mdi-alpha-p-box-outline::before {
+ content: "\FBF4";
+}
+.mdi-alpha-p-circle::before {
+ content: "\FBF5";
+}
+.mdi-alpha-p-circle-outline::before {
+ content: "\FBF6";
+}
+.mdi-alpha-q::before {
+ content: "\51";
+}
+.mdi-alpha-q-box::before {
+ content: "\FAFD";
+}
+.mdi-alpha-q-box-outline::before {
+ content: "\FBF7";
+}
+.mdi-alpha-q-circle::before {
+ content: "\FBF8";
+}
+.mdi-alpha-q-circle-outline::before {
+ content: "\FBF9";
+}
+.mdi-alpha-r::before {
+ content: "\52";
+}
+.mdi-alpha-r-box::before {
+ content: "\FAFE";
+}
+.mdi-alpha-r-box-outline::before {
+ content: "\FBFA";
+}
+.mdi-alpha-r-circle::before {
+ content: "\FBFB";
+}
+.mdi-alpha-r-circle-outline::before {
+ content: "\FBFC";
+}
+.mdi-alpha-s::before {
+ content: "\53";
+}
+.mdi-alpha-s-box::before {
+ content: "\FAFF";
+}
+.mdi-alpha-s-box-outline::before {
+ content: "\FBFD";
+}
+.mdi-alpha-s-circle::before {
+ content: "\FBFE";
+}
+.mdi-alpha-s-circle-outline::before {
+ content: "\FBFF";
+}
+.mdi-alpha-t::before {
+ content: "\54";
+}
+.mdi-alpha-t-box::before {
+ content: "\FB00";
+}
+.mdi-alpha-t-box-outline::before {
+ content: "\FC00";
+}
+.mdi-alpha-t-circle::before {
+ content: "\FC01";
+}
+.mdi-alpha-t-circle-outline::before {
+ content: "\FC02";
+}
+.mdi-alpha-u::before {
+ content: "\55";
+}
+.mdi-alpha-u-box::before {
+ content: "\FB01";
+}
+.mdi-alpha-u-box-outline::before {
+ content: "\FC03";
+}
+.mdi-alpha-u-circle::before {
+ content: "\FC04";
+}
+.mdi-alpha-u-circle-outline::before {
+ content: "\FC05";
+}
+.mdi-alpha-v::before {
+ content: "\56";
+}
+.mdi-alpha-v-box::before {
+ content: "\FB02";
+}
+.mdi-alpha-v-box-outline::before {
+ content: "\FC06";
+}
+.mdi-alpha-v-circle::before {
+ content: "\FC07";
+}
+.mdi-alpha-v-circle-outline::before {
+ content: "\FC08";
+}
+.mdi-alpha-w::before {
+ content: "\57";
+}
+.mdi-alpha-w-box::before {
+ content: "\FB03";
+}
+.mdi-alpha-w-box-outline::before {
+ content: "\FC09";
+}
+.mdi-alpha-w-circle::before {
+ content: "\FC0A";
+}
+.mdi-alpha-w-circle-outline::before {
+ content: "\FC0B";
+}
+.mdi-alpha-x::before {
+ content: "\58";
+}
+.mdi-alpha-x-box::before {
+ content: "\FB04";
+}
+.mdi-alpha-x-box-outline::before {
+ content: "\FC0C";
+}
+.mdi-alpha-x-circle::before {
+ content: "\FC0D";
+}
+.mdi-alpha-x-circle-outline::before {
+ content: "\FC0E";
+}
+.mdi-alpha-y::before {
+ content: "\59";
+}
+.mdi-alpha-y-box::before {
+ content: "\FB05";
+}
+.mdi-alpha-y-box-outline::before {
+ content: "\FC0F";
+}
+.mdi-alpha-y-circle::before {
+ content: "\FC10";
+}
+.mdi-alpha-y-circle-outline::before {
+ content: "\FC11";
+}
+.mdi-alpha-z::before {
+ content: "\5A";
+}
+.mdi-alpha-z-box::before {
+ content: "\FB06";
+}
+.mdi-alpha-z-box-outline::before {
+ content: "\FC12";
+}
+.mdi-alpha-z-circle::before {
+ content: "\FC13";
+}
+.mdi-alpha-z-circle-outline::before {
+ content: "\FC14";
+}
+.mdi-alphabet-aurebesh::before {
+ content: "\F0357";
+}
+.mdi-alphabet-cyrillic::before {
+ content: "\F0358";
+}
+.mdi-alphabet-greek::before {
+ content: "\F0359";
+}
+.mdi-alphabet-latin::before {
+ content: "\F035A";
+}
+.mdi-alphabet-piqad::before {
+ content: "\F035B";
+}
+.mdi-alphabet-tengwar::before {
+ content: "\F0362";
+}
+.mdi-alphabetical::before {
+ content: "\F02C";
+}
+.mdi-alphabetical-off::before {
+ content: "\F002E";
+}
+.mdi-alphabetical-variant::before {
+ content: "\F002F";
+}
+.mdi-alphabetical-variant-off::before {
+ content: "\F0030";
+}
+.mdi-altimeter::before {
+ content: "\F5D7";
+}
+.mdi-amazon::before {
+ content: "\F02D";
+}
+.mdi-amazon-alexa::before {
+ content: "\F8C5";
+}
+.mdi-amazon-drive::before {
+ content: "\F02E";
+}
+.mdi-ambulance::before {
+ content: "\F02F";
+}
+.mdi-ammunition::before {
+ content: "\FCC4";
+}
+.mdi-ampersand::before {
+ content: "\FA8C";
+}
+.mdi-amplifier::before {
+ content: "\F030";
+}
+.mdi-amplifier-off::before {
+ content: "\F01E0";
+}
+.mdi-anchor::before {
+ content: "\F031";
+}
+.mdi-android::before {
+ content: "\F032";
+}
+.mdi-android-auto::before {
+ content: "\FA8D";
+}
+.mdi-android-debug-bridge::before {
+ content: "\F033";
+}
+.mdi-android-head::before {
+ content: "\F78F";
+}
+.mdi-android-messages::before {
+ content: "\FD21";
+}
+.mdi-android-studio::before {
+ content: "\F034";
+}
+.mdi-angle-acute::before {
+ content: "\F936";
+}
+.mdi-angle-obtuse::before {
+ content: "\F937";
+}
+.mdi-angle-right::before {
+ content: "\F938";
+}
+.mdi-angular::before {
+ content: "\F6B1";
+}
+.mdi-angularjs::before {
+ content: "\F6BE";
+}
+.mdi-animation::before {
+ content: "\F5D8";
+}
+.mdi-animation-outline::before {
+ content: "\FA8E";
+}
+.mdi-animation-play::before {
+ content: "\F939";
+}
+.mdi-animation-play-outline::before {
+ content: "\FA8F";
+}
+.mdi-ansible::before {
+ content: "\F00C5";
+}
+.mdi-antenna::before {
+ content: "\F0144";
+}
+.mdi-anvil::before {
+ content: "\F89A";
+}
+.mdi-apache-kafka::before {
+ content: "\F0031";
+}
+.mdi-api::before {
+ content: "\F00C6";
+}
+.mdi-api-off::before {
+ content: "\F0282";
+}
+.mdi-apple::before {
+ content: "\F035";
+}
+.mdi-apple-finder::before {
+ content: "\F036";
+}
+.mdi-apple-icloud::before {
+ content: "\F038";
+}
+.mdi-apple-ios::before {
+ content: "\F037";
+}
+.mdi-apple-keyboard-caps::before {
+ content: "\F632";
+}
+.mdi-apple-keyboard-command::before {
+ content: "\F633";
+}
+.mdi-apple-keyboard-control::before {
+ content: "\F634";
+}
+.mdi-apple-keyboard-option::before {
+ content: "\F635";
+}
+.mdi-apple-keyboard-shift::before {
+ content: "\F636";
+}
+.mdi-apple-safari::before {
+ content: "\F039";
+}
+.mdi-application::before {
+ content: "\F614";
+}
+.mdi-application-export::before {
+ content: "\FD89";
+}
+.mdi-application-import::before {
+ content: "\FD8A";
+}
+.mdi-approximately-equal::before {
+ content: "\FFBE";
+}
+.mdi-approximately-equal-box::before {
+ content: "\FFBF";
+}
+.mdi-apps::before {
+ content: "\F03B";
+}
+.mdi-apps-box::before {
+ content: "\FD22";
+}
+.mdi-arch::before {
+ content: "\F8C6";
+}
+.mdi-archive::before {
+ content: "\F03C";
+}
+.mdi-archive-arrow-down::before {
+ content: "\F0284";
+}
+.mdi-archive-arrow-down-outline::before {
+ content: "\F0285";
+}
+.mdi-archive-arrow-up::before {
+ content: "\F0286";
+}
+.mdi-archive-arrow-up-outline::before {
+ content: "\F0287";
+}
+.mdi-archive-outline::before {
+ content: "\F0239";
+}
+.mdi-arm-flex::before {
+ content: "\F008F";
+}
+.mdi-arm-flex-outline::before {
+ content: "\F0090";
+}
+.mdi-arrange-bring-forward::before {
+ content: "\F03D";
+}
+.mdi-arrange-bring-to-front::before {
+ content: "\F03E";
+}
+.mdi-arrange-send-backward::before {
+ content: "\F03F";
+}
+.mdi-arrange-send-to-back::before {
+ content: "\F040";
+}
+.mdi-arrow-all::before {
+ content: "\F041";
+}
+.mdi-arrow-bottom-left::before {
+ content: "\F042";
+}
+.mdi-arrow-bottom-left-bold-outline::before {
+ content: "\F9B6";
+}
+.mdi-arrow-bottom-left-thick::before {
+ content: "\F9B7";
+}
+.mdi-arrow-bottom-right::before {
+ content: "\F043";
+}
+.mdi-arrow-bottom-right-bold-outline::before {
+ content: "\F9B8";
+}
+.mdi-arrow-bottom-right-thick::before {
+ content: "\F9B9";
+}
+.mdi-arrow-collapse::before {
+ content: "\F615";
+}
+.mdi-arrow-collapse-all::before {
+ content: "\F044";
+}
+.mdi-arrow-collapse-down::before {
+ content: "\F791";
+}
+.mdi-arrow-collapse-horizontal::before {
+ content: "\F84B";
+}
+.mdi-arrow-collapse-left::before {
+ content: "\F792";
+}
+.mdi-arrow-collapse-right::before {
+ content: "\F793";
+}
+.mdi-arrow-collapse-up::before {
+ content: "\F794";
+}
+.mdi-arrow-collapse-vertical::before {
+ content: "\F84C";
+}
+.mdi-arrow-decision::before {
+ content: "\F9BA";
+}
+.mdi-arrow-decision-auto::before {
+ content: "\F9BB";
+}
+.mdi-arrow-decision-auto-outline::before {
+ content: "\F9BC";
+}
+.mdi-arrow-decision-outline::before {
+ content: "\F9BD";
+}
+.mdi-arrow-down::before {
+ content: "\F045";
+}
+.mdi-arrow-down-bold::before {
+ content: "\F72D";
+}
+.mdi-arrow-down-bold-box::before {
+ content: "\F72E";
+}
+.mdi-arrow-down-bold-box-outline::before {
+ content: "\F72F";
+}
+.mdi-arrow-down-bold-circle::before {
+ content: "\F047";
+}
+.mdi-arrow-down-bold-circle-outline::before {
+ content: "\F048";
+}
+.mdi-arrow-down-bold-hexagon-outline::before {
+ content: "\F049";
+}
+.mdi-arrow-down-bold-outline::before {
+ content: "\F9BE";
+}
+.mdi-arrow-down-box::before {
+ content: "\F6BF";
+}
+.mdi-arrow-down-circle::before {
+ content: "\FCB7";
+}
+.mdi-arrow-down-circle-outline::before {
+ content: "\FCB8";
+}
+.mdi-arrow-down-drop-circle::before {
+ content: "\F04A";
+}
+.mdi-arrow-down-drop-circle-outline::before {
+ content: "\F04B";
+}
+.mdi-arrow-down-thick::before {
+ content: "\F046";
+}
+.mdi-arrow-expand::before {
+ content: "\F616";
+}
+.mdi-arrow-expand-all::before {
+ content: "\F04C";
+}
+.mdi-arrow-expand-down::before {
+ content: "\F795";
+}
+.mdi-arrow-expand-horizontal::before {
+ content: "\F84D";
+}
+.mdi-arrow-expand-left::before {
+ content: "\F796";
+}
+.mdi-arrow-expand-right::before {
+ content: "\F797";
+}
+.mdi-arrow-expand-up::before {
+ content: "\F798";
+}
+.mdi-arrow-expand-vertical::before {
+ content: "\F84E";
+}
+.mdi-arrow-horizontal-lock::before {
+ content: "\F0186";
+}
+.mdi-arrow-left::before {
+ content: "\F04D";
+}
+.mdi-arrow-left-bold::before {
+ content: "\F730";
+}
+.mdi-arrow-left-bold-box::before {
+ content: "\F731";
+}
+.mdi-arrow-left-bold-box-outline::before {
+ content: "\F732";
+}
+.mdi-arrow-left-bold-circle::before {
+ content: "\F04F";
+}
+.mdi-arrow-left-bold-circle-outline::before {
+ content: "\F050";
+}
+.mdi-arrow-left-bold-hexagon-outline::before {
+ content: "\F051";
+}
+.mdi-arrow-left-bold-outline::before {
+ content: "\F9BF";
+}
+.mdi-arrow-left-box::before {
+ content: "\F6C0";
+}
+.mdi-arrow-left-circle::before {
+ content: "\FCB9";
+}
+.mdi-arrow-left-circle-outline::before {
+ content: "\FCBA";
+}
+.mdi-arrow-left-drop-circle::before {
+ content: "\F052";
+}
+.mdi-arrow-left-drop-circle-outline::before {
+ content: "\F053";
+}
+.mdi-arrow-left-right::before {
+ content: "\FE90";
+}
+.mdi-arrow-left-right-bold::before {
+ content: "\FE91";
+}
+.mdi-arrow-left-right-bold-outline::before {
+ content: "\F9C0";
+}
+.mdi-arrow-left-thick::before {
+ content: "\F04E";
+}
+.mdi-arrow-right::before {
+ content: "\F054";
+}
+.mdi-arrow-right-bold::before {
+ content: "\F733";
+}
+.mdi-arrow-right-bold-box::before {
+ content: "\F734";
+}
+.mdi-arrow-right-bold-box-outline::before {
+ content: "\F735";
+}
+.mdi-arrow-right-bold-circle::before {
+ content: "\F056";
+}
+.mdi-arrow-right-bold-circle-outline::before {
+ content: "\F057";
+}
+.mdi-arrow-right-bold-hexagon-outline::before {
+ content: "\F058";
+}
+.mdi-arrow-right-bold-outline::before {
+ content: "\F9C1";
+}
+.mdi-arrow-right-box::before {
+ content: "\F6C1";
+}
+.mdi-arrow-right-circle::before {
+ content: "\FCBB";
+}
+.mdi-arrow-right-circle-outline::before {
+ content: "\FCBC";
+}
+.mdi-arrow-right-drop-circle::before {
+ content: "\F059";
+}
+.mdi-arrow-right-drop-circle-outline::before {
+ content: "\F05A";
+}
+.mdi-arrow-right-thick::before {
+ content: "\F055";
+}
+.mdi-arrow-split-horizontal::before {
+ content: "\F93A";
+}
+.mdi-arrow-split-vertical::before {
+ content: "\F93B";
+}
+.mdi-arrow-top-left::before {
+ content: "\F05B";
+}
+.mdi-arrow-top-left-bold-outline::before {
+ content: "\F9C2";
+}
+.mdi-arrow-top-left-bottom-right::before {
+ content: "\FE92";
+}
+.mdi-arrow-top-left-bottom-right-bold::before {
+ content: "\FE93";
+}
+.mdi-arrow-top-left-thick::before {
+ content: "\F9C3";
+}
+.mdi-arrow-top-right::before {
+ content: "\F05C";
+}
+.mdi-arrow-top-right-bold-outline::before {
+ content: "\F9C4";
+}
+.mdi-arrow-top-right-bottom-left::before {
+ content: "\FE94";
+}
+.mdi-arrow-top-right-bottom-left-bold::before {
+ content: "\FE95";
+}
+.mdi-arrow-top-right-thick::before {
+ content: "\F9C5";
+}
+.mdi-arrow-up::before {
+ content: "\F05D";
+}
+.mdi-arrow-up-bold::before {
+ content: "\F736";
+}
+.mdi-arrow-up-bold-box::before {
+ content: "\F737";
+}
+.mdi-arrow-up-bold-box-outline::before {
+ content: "\F738";
+}
+.mdi-arrow-up-bold-circle::before {
+ content: "\F05F";
+}
+.mdi-arrow-up-bold-circle-outline::before {
+ content: "\F060";
+}
+.mdi-arrow-up-bold-hexagon-outline::before {
+ content: "\F061";
+}
+.mdi-arrow-up-bold-outline::before {
+ content: "\F9C6";
+}
+.mdi-arrow-up-box::before {
+ content: "\F6C2";
+}
+.mdi-arrow-up-circle::before {
+ content: "\FCBD";
+}
+.mdi-arrow-up-circle-outline::before {
+ content: "\FCBE";
+}
+.mdi-arrow-up-down::before {
+ content: "\FE96";
+}
+.mdi-arrow-up-down-bold::before {
+ content: "\FE97";
+}
+.mdi-arrow-up-down-bold-outline::before {
+ content: "\F9C7";
+}
+.mdi-arrow-up-drop-circle::before {
+ content: "\F062";
+}
+.mdi-arrow-up-drop-circle-outline::before {
+ content: "\F063";
+}
+.mdi-arrow-up-thick::before {
+ content: "\F05E";
+}
+.mdi-arrow-vertical-lock::before {
+ content: "\F0187";
+}
+.mdi-artist::before {
+ content: "\F802";
+}
+.mdi-artist-outline::before {
+ content: "\FCC5";
+}
+.mdi-artstation::before {
+ content: "\FB37";
+}
+.mdi-aspect-ratio::before {
+ content: "\FA23";
+}
+.mdi-assistant::before {
+ content: "\F064";
+}
+.mdi-asterisk::before {
+ content: "\F6C3";
+}
+.mdi-at::before {
+ content: "\F065";
+}
+.mdi-atlassian::before {
+ content: "\F803";
+}
+.mdi-atm::before {
+ content: "\FD23";
+}
+.mdi-atom::before {
+ content: "\F767";
+}
+.mdi-atom-variant::before {
+ content: "\FE98";
+}
+.mdi-attachment::before {
+ content: "\F066";
+}
+.mdi-audio-video::before {
+ content: "\F93C";
+}
+.mdi-audio-video-off::before {
+ content: "\F01E1";
+}
+.mdi-audiobook::before {
+ content: "\F067";
+}
+.mdi-augmented-reality::before {
+ content: "\F84F";
+}
+.mdi-auto-download::before {
+ content: "\F03A9";
+}
+.mdi-auto-fix::before {
+ content: "\F068";
+}
+.mdi-auto-upload::before {
+ content: "\F069";
+}
+.mdi-autorenew::before {
+ content: "\F06A";
+}
+.mdi-av-timer::before {
+ content: "\F06B";
+}
+.mdi-aws::before {
+ content: "\FDF2";
+}
+.mdi-axe::before {
+ content: "\F8C7";
+}
+.mdi-axis::before {
+ content: "\FD24";
+}
+.mdi-axis-arrow::before {
+ content: "\FD25";
+}
+.mdi-axis-arrow-lock::before {
+ content: "\FD26";
+}
+.mdi-axis-lock::before {
+ content: "\FD27";
+}
+.mdi-axis-x-arrow::before {
+ content: "\FD28";
+}
+.mdi-axis-x-arrow-lock::before {
+ content: "\FD29";
+}
+.mdi-axis-x-rotate-clockwise::before {
+ content: "\FD2A";
+}
+.mdi-axis-x-rotate-counterclockwise::before {
+ content: "\FD2B";
+}
+.mdi-axis-x-y-arrow-lock::before {
+ content: "\FD2C";
+}
+.mdi-axis-y-arrow::before {
+ content: "\FD2D";
+}
+.mdi-axis-y-arrow-lock::before {
+ content: "\FD2E";
+}
+.mdi-axis-y-rotate-clockwise::before {
+ content: "\FD2F";
+}
+.mdi-axis-y-rotate-counterclockwise::before {
+ content: "\FD30";
+}
+.mdi-axis-z-arrow::before {
+ content: "\FD31";
+}
+.mdi-axis-z-arrow-lock::before {
+ content: "\FD32";
+}
+.mdi-axis-z-rotate-clockwise::before {
+ content: "\FD33";
+}
+.mdi-axis-z-rotate-counterclockwise::before {
+ content: "\FD34";
+}
+.mdi-azure::before {
+ content: "\F804";
+}
+.mdi-azure-devops::before {
+ content: "\F0091";
+}
+.mdi-babel::before {
+ content: "\FA24";
+}
+.mdi-baby::before {
+ content: "\F06C";
+}
+.mdi-baby-bottle::before {
+ content: "\FF56";
+}
+.mdi-baby-bottle-outline::before {
+ content: "\FF57";
+}
+.mdi-baby-carriage::before {
+ content: "\F68E";
+}
+.mdi-baby-carriage-off::before {
+ content: "\FFC0";
+}
+.mdi-baby-face::before {
+ content: "\FE99";
+}
+.mdi-baby-face-outline::before {
+ content: "\FE9A";
+}
+.mdi-backburger::before {
+ content: "\F06D";
+}
+.mdi-backspace::before {
+ content: "\F06E";
+}
+.mdi-backspace-outline::before {
+ content: "\FB38";
+}
+.mdi-backspace-reverse::before {
+ content: "\FE9B";
+}
+.mdi-backspace-reverse-outline::before {
+ content: "\FE9C";
+}
+.mdi-backup-restore::before {
+ content: "\F06F";
+}
+.mdi-bacteria::before {
+ content: "\FEF2";
+}
+.mdi-bacteria-outline::before {
+ content: "\FEF3";
+}
+.mdi-badminton::before {
+ content: "\F850";
+}
+.mdi-bag-carry-on::before {
+ content: "\FF58";
+}
+.mdi-bag-carry-on-check::before {
+ content: "\FD41";
+}
+.mdi-bag-carry-on-off::before {
+ content: "\FF59";
+}
+.mdi-bag-checked::before {
+ content: "\FF5A";
+}
+.mdi-bag-personal::before {
+ content: "\FDF3";
+}
+.mdi-bag-personal-off::before {
+ content: "\FDF4";
+}
+.mdi-bag-personal-off-outline::before {
+ content: "\FDF5";
+}
+.mdi-bag-personal-outline::before {
+ content: "\FDF6";
+}
+.mdi-baguette::before {
+ content: "\FF5B";
+}
+.mdi-balloon::before {
+ content: "\FA25";
+}
+.mdi-ballot::before {
+ content: "\F9C8";
+}
+.mdi-ballot-outline::before {
+ content: "\F9C9";
+}
+.mdi-ballot-recount::before {
+ content: "\FC15";
+}
+.mdi-ballot-recount-outline::before {
+ content: "\FC16";
+}
+.mdi-bandage::before {
+ content: "\FD8B";
+}
+.mdi-bandcamp::before {
+ content: "\F674";
+}
+.mdi-bank::before {
+ content: "\F070";
+}
+.mdi-bank-minus::before {
+ content: "\FD8C";
+}
+.mdi-bank-outline::before {
+ content: "\FE9D";
+}
+.mdi-bank-plus::before {
+ content: "\FD8D";
+}
+.mdi-bank-remove::before {
+ content: "\FD8E";
+}
+.mdi-bank-transfer::before {
+ content: "\FA26";
+}
+.mdi-bank-transfer-in::before {
+ content: "\FA27";
+}
+.mdi-bank-transfer-out::before {
+ content: "\FA28";
+}
+.mdi-barcode::before {
+ content: "\F071";
+}
+.mdi-barcode-off::before {
+ content: "\F0261";
+}
+.mdi-barcode-scan::before {
+ content: "\F072";
+}
+.mdi-barley::before {
+ content: "\F073";
+}
+.mdi-barley-off::before {
+ content: "\FB39";
+}
+.mdi-barn::before {
+ content: "\FB3A";
+}
+.mdi-barrel::before {
+ content: "\F074";
+}
+.mdi-baseball::before {
+ content: "\F851";
+}
+.mdi-baseball-bat::before {
+ content: "\F852";
+}
+.mdi-basecamp::before {
+ content: "\F075";
+}
+.mdi-bash::before {
+ content: "\F01AE";
+}
+.mdi-basket::before {
+ content: "\F076";
+}
+.mdi-basket-fill::before {
+ content: "\F077";
+}
+.mdi-basket-outline::before {
+ content: "\F01AC";
+}
+.mdi-basket-unfill::before {
+ content: "\F078";
+}
+.mdi-basketball::before {
+ content: "\F805";
+}
+.mdi-basketball-hoop::before {
+ content: "\FC17";
+}
+.mdi-basketball-hoop-outline::before {
+ content: "\FC18";
+}
+.mdi-bat::before {
+ content: "\FB3B";
+}
+.mdi-battery::before {
+ content: "\F079";
+}
+.mdi-battery-10::before {
+ content: "\F07A";
+}
+.mdi-battery-10-bluetooth::before {
+ content: "\F93D";
+}
+.mdi-battery-20::before {
+ content: "\F07B";
+}
+.mdi-battery-20-bluetooth::before {
+ content: "\F93E";
+}
+.mdi-battery-30::before {
+ content: "\F07C";
+}
+.mdi-battery-30-bluetooth::before {
+ content: "\F93F";
+}
+.mdi-battery-40::before {
+ content: "\F07D";
+}
+.mdi-battery-40-bluetooth::before {
+ content: "\F940";
+}
+.mdi-battery-50::before {
+ content: "\F07E";
+}
+.mdi-battery-50-bluetooth::before {
+ content: "\F941";
+}
+.mdi-battery-60::before {
+ content: "\F07F";
+}
+.mdi-battery-60-bluetooth::before {
+ content: "\F942";
+}
+.mdi-battery-70::before {
+ content: "\F080";
+}
+.mdi-battery-70-bluetooth::before {
+ content: "\F943";
+}
+.mdi-battery-80::before {
+ content: "\F081";
+}
+.mdi-battery-80-bluetooth::before {
+ content: "\F944";
+}
+.mdi-battery-90::before {
+ content: "\F082";
+}
+.mdi-battery-90-bluetooth::before {
+ content: "\F945";
+}
+.mdi-battery-alert::before {
+ content: "\F083";
+}
+.mdi-battery-alert-bluetooth::before {
+ content: "\F946";
+}
+.mdi-battery-alert-variant::before {
+ content: "\F00F7";
+}
+.mdi-battery-alert-variant-outline::before {
+ content: "\F00F8";
+}
+.mdi-battery-bluetooth::before {
+ content: "\F947";
+}
+.mdi-battery-bluetooth-variant::before {
+ content: "\F948";
+}
+.mdi-battery-charging::before {
+ content: "\F084";
+}
+.mdi-battery-charging-10::before {
+ content: "\F89B";
+}
+.mdi-battery-charging-100::before {
+ content: "\F085";
+}
+.mdi-battery-charging-20::before {
+ content: "\F086";
+}
+.mdi-battery-charging-30::before {
+ content: "\F087";
+}
+.mdi-battery-charging-40::before {
+ content: "\F088";
+}
+.mdi-battery-charging-50::before {
+ content: "\F89C";
+}
+.mdi-battery-charging-60::before {
+ content: "\F089";
+}
+.mdi-battery-charging-70::before {
+ content: "\F89D";
+}
+.mdi-battery-charging-80::before {
+ content: "\F08A";
+}
+.mdi-battery-charging-90::before {
+ content: "\F08B";
+}
+.mdi-battery-charging-high::before {
+ content: "\F02D1";
+}
+.mdi-battery-charging-low::before {
+ content: "\F02CF";
+}
+.mdi-battery-charging-medium::before {
+ content: "\F02D0";
+}
+.mdi-battery-charging-outline::before {
+ content: "\F89E";
+}
+.mdi-battery-charging-wireless::before {
+ content: "\F806";
+}
+.mdi-battery-charging-wireless-10::before {
+ content: "\F807";
+}
+.mdi-battery-charging-wireless-20::before {
+ content: "\F808";
+}
+.mdi-battery-charging-wireless-30::before {
+ content: "\F809";
+}
+.mdi-battery-charging-wireless-40::before {
+ content: "\F80A";
+}
+.mdi-battery-charging-wireless-50::before {
+ content: "\F80B";
+}
+.mdi-battery-charging-wireless-60::before {
+ content: "\F80C";
+}
+.mdi-battery-charging-wireless-70::before {
+ content: "\F80D";
+}
+.mdi-battery-charging-wireless-80::before {
+ content: "\F80E";
+}
+.mdi-battery-charging-wireless-90::before {
+ content: "\F80F";
+}
+.mdi-battery-charging-wireless-alert::before {
+ content: "\F810";
+}
+.mdi-battery-charging-wireless-outline::before {
+ content: "\F811";
+}
+.mdi-battery-heart::before {
+ content: "\F023A";
+}
+.mdi-battery-heart-outline::before {
+ content: "\F023B";
+}
+.mdi-battery-heart-variant::before {
+ content: "\F023C";
+}
+.mdi-battery-high::before {
+ content: "\F02CE";
+}
+.mdi-battery-low::before {
+ content: "\F02CC";
+}
+.mdi-battery-medium::before {
+ content: "\F02CD";
+}
+.mdi-battery-minus::before {
+ content: "\F08C";
+}
+.mdi-battery-negative::before {
+ content: "\F08D";
+}
+.mdi-battery-off::before {
+ content: "\F0288";
+}
+.mdi-battery-off-outline::before {
+ content: "\F0289";
+}
+.mdi-battery-outline::before {
+ content: "\F08E";
+}
+.mdi-battery-plus::before {
+ content: "\F08F";
+}
+.mdi-battery-positive::before {
+ content: "\F090";
+}
+.mdi-battery-unknown::before {
+ content: "\F091";
+}
+.mdi-battery-unknown-bluetooth::before {
+ content: "\F949";
+}
+.mdi-battlenet::before {
+ content: "\FB3C";
+}
+.mdi-beach::before {
+ content: "\F092";
+}
+.mdi-beaker::before {
+ content: "\FCC6";
+}
+.mdi-beaker-alert::before {
+ content: "\F0254";
+}
+.mdi-beaker-alert-outline::before {
+ content: "\F0255";
+}
+.mdi-beaker-check::before {
+ content: "\F0256";
+}
+.mdi-beaker-check-outline::before {
+ content: "\F0257";
+}
+.mdi-beaker-minus::before {
+ content: "\F0258";
+}
+.mdi-beaker-minus-outline::before {
+ content: "\F0259";
+}
+.mdi-beaker-outline::before {
+ content: "\F68F";
+}
+.mdi-beaker-plus::before {
+ content: "\F025A";
+}
+.mdi-beaker-plus-outline::before {
+ content: "\F025B";
+}
+.mdi-beaker-question::before {
+ content: "\F025C";
+}
+.mdi-beaker-question-outline::before {
+ content: "\F025D";
+}
+.mdi-beaker-remove::before {
+ content: "\F025E";
+}
+.mdi-beaker-remove-outline::before {
+ content: "\F025F";
+}
+.mdi-beats::before {
+ content: "\F097";
+}
+.mdi-bed-double::before {
+ content: "\F0092";
+}
+.mdi-bed-double-outline::before {
+ content: "\F0093";
+}
+.mdi-bed-empty::before {
+ content: "\F89F";
+}
+.mdi-bed-king::before {
+ content: "\F0094";
+}
+.mdi-bed-king-outline::before {
+ content: "\F0095";
+}
+.mdi-bed-queen::before {
+ content: "\F0096";
+}
+.mdi-bed-queen-outline::before {
+ content: "\F0097";
+}
+.mdi-bed-single::before {
+ content: "\F0098";
+}
+.mdi-bed-single-outline::before {
+ content: "\F0099";
+}
+.mdi-bee::before {
+ content: "\FFC1";
+}
+.mdi-bee-flower::before {
+ content: "\FFC2";
+}
+.mdi-beehive-outline::before {
+ content: "\F00F9";
+}
+.mdi-beer::before {
+ content: "\F098";
+}
+.mdi-beer-outline::before {
+ content: "\F0337";
+}
+.mdi-behance::before {
+ content: "\F099";
+}
+.mdi-bell::before {
+ content: "\F09A";
+}
+.mdi-bell-alert::before {
+ content: "\FD35";
+}
+.mdi-bell-alert-outline::before {
+ content: "\FE9E";
+}
+.mdi-bell-check::before {
+ content: "\F0210";
+}
+.mdi-bell-check-outline::before {
+ content: "\F0211";
+}
+.mdi-bell-circle::before {
+ content: "\FD36";
+}
+.mdi-bell-circle-outline::before {
+ content: "\FD37";
+}
+.mdi-bell-off::before {
+ content: "\F09B";
+}
+.mdi-bell-off-outline::before {
+ content: "\FA90";
+}
+.mdi-bell-outline::before {
+ content: "\F09C";
+}
+.mdi-bell-plus::before {
+ content: "\F09D";
+}
+.mdi-bell-plus-outline::before {
+ content: "\FA91";
+}
+.mdi-bell-ring::before {
+ content: "\F09E";
+}
+.mdi-bell-ring-outline::before {
+ content: "\F09F";
+}
+.mdi-bell-sleep::before {
+ content: "\F0A0";
+}
+.mdi-bell-sleep-outline::before {
+ content: "\FA92";
+}
+.mdi-beta::before {
+ content: "\F0A1";
+}
+.mdi-betamax::before {
+ content: "\F9CA";
+}
+.mdi-biathlon::before {
+ content: "\FDF7";
+}
+.mdi-bible::before {
+ content: "\F0A2";
+}
+.mdi-bicycle::before {
+ content: "\F00C7";
+}
+.mdi-bicycle-basket::before {
+ content: "\F0260";
+}
+.mdi-bike::before {
+ content: "\F0A3";
+}
+.mdi-bike-fast::before {
+ content: "\F014A";
+}
+.mdi-billboard::before {
+ content: "\F0032";
+}
+.mdi-billiards::before {
+ content: "\FB3D";
+}
+.mdi-billiards-rack::before {
+ content: "\FB3E";
+}
+.mdi-bing::before {
+ content: "\F0A4";
+}
+.mdi-binoculars::before {
+ content: "\F0A5";
+}
+.mdi-bio::before {
+ content: "\F0A6";
+}
+.mdi-biohazard::before {
+ content: "\F0A7";
+}
+.mdi-bitbucket::before {
+ content: "\F0A8";
+}
+.mdi-bitcoin::before {
+ content: "\F812";
+}
+.mdi-black-mesa::before {
+ content: "\F0A9";
+}
+.mdi-blackberry::before {
+ content: "\F0AA";
+}
+.mdi-blender::before {
+ content: "\FCC7";
+}
+.mdi-blender-software::before {
+ content: "\F0AB";
+}
+.mdi-blinds::before {
+ content: "\F0AC";
+}
+.mdi-blinds-open::before {
+ content: "\F0033";
+}
+.mdi-block-helper::before {
+ content: "\F0AD";
+}
+.mdi-blogger::before {
+ content: "\F0AE";
+}
+.mdi-blood-bag::before {
+ content: "\FCC8";
+}
+.mdi-bluetooth::before {
+ content: "\F0AF";
+}
+.mdi-bluetooth-audio::before {
+ content: "\F0B0";
+}
+.mdi-bluetooth-connect::before {
+ content: "\F0B1";
+}
+.mdi-bluetooth-off::before {
+ content: "\F0B2";
+}
+.mdi-bluetooth-settings::before {
+ content: "\F0B3";
+}
+.mdi-bluetooth-transfer::before {
+ content: "\F0B4";
+}
+.mdi-blur::before {
+ content: "\F0B5";
+}
+.mdi-blur-linear::before {
+ content: "\F0B6";
+}
+.mdi-blur-off::before {
+ content: "\F0B7";
+}
+.mdi-blur-radial::before {
+ content: "\F0B8";
+}
+.mdi-bolnisi-cross::before {
+ content: "\FCC9";
+}
+.mdi-bolt::before {
+ content: "\FD8F";
+}
+.mdi-bomb::before {
+ content: "\F690";
+}
+.mdi-bomb-off::before {
+ content: "\F6C4";
+}
+.mdi-bone::before {
+ content: "\F0B9";
+}
+.mdi-book::before {
+ content: "\F0BA";
+}
+.mdi-book-information-variant::before {
+ content: "\F009A";
+}
+.mdi-book-lock::before {
+ content: "\F799";
+}
+.mdi-book-lock-open::before {
+ content: "\F79A";
+}
+.mdi-book-minus::before {
+ content: "\F5D9";
+}
+.mdi-book-minus-multiple::before {
+ content: "\FA93";
+}
+.mdi-book-multiple::before {
+ content: "\F0BB";
+}
+.mdi-book-open::before {
+ content: "\F0BD";
+}
+.mdi-book-open-outline::before {
+ content: "\FB3F";
+}
+.mdi-book-open-page-variant::before {
+ content: "\F5DA";
+}
+.mdi-book-open-variant::before {
+ content: "\F0BE";
+}
+.mdi-book-outline::before {
+ content: "\FB40";
+}
+.mdi-book-play::before {
+ content: "\FE9F";
+}
+.mdi-book-play-outline::before {
+ content: "\FEA0";
+}
+.mdi-book-plus::before {
+ content: "\F5DB";
+}
+.mdi-book-plus-multiple::before {
+ content: "\FA94";
+}
+.mdi-book-remove::before {
+ content: "\FA96";
+}
+.mdi-book-remove-multiple::before {
+ content: "\FA95";
+}
+.mdi-book-search::before {
+ content: "\FEA1";
+}
+.mdi-book-search-outline::before {
+ content: "\FEA2";
+}
+.mdi-book-variant::before {
+ content: "\F0BF";
+}
+.mdi-book-variant-multiple::before {
+ content: "\F0BC";
+}
+.mdi-bookmark::before {
+ content: "\F0C0";
+}
+.mdi-bookmark-check::before {
+ content: "\F0C1";
+}
+.mdi-bookmark-check-outline::before {
+ content: "\F03A6";
+}
+.mdi-bookmark-minus::before {
+ content: "\F9CB";
+}
+.mdi-bookmark-minus-outline::before {
+ content: "\F9CC";
+}
+.mdi-bookmark-multiple::before {
+ content: "\FDF8";
+}
+.mdi-bookmark-multiple-outline::before {
+ content: "\FDF9";
+}
+.mdi-bookmark-music::before {
+ content: "\F0C2";
+}
+.mdi-bookmark-music-outline::before {
+ content: "\F03A4";
+}
+.mdi-bookmark-off::before {
+ content: "\F9CD";
+}
+.mdi-bookmark-off-outline::before {
+ content: "\F9CE";
+}
+.mdi-bookmark-outline::before {
+ content: "\F0C3";
+}
+.mdi-bookmark-plus::before {
+ content: "\F0C5";
+}
+.mdi-bookmark-plus-outline::before {
+ content: "\F0C4";
+}
+.mdi-bookmark-remove::before {
+ content: "\F0C6";
+}
+.mdi-bookmark-remove-outline::before {
+ content: "\F03A5";
+}
+.mdi-bookshelf::before {
+ content: "\F028A";
+}
+.mdi-boom-gate::before {
+ content: "\FEA3";
+}
+.mdi-boom-gate-alert::before {
+ content: "\FEA4";
+}
+.mdi-boom-gate-alert-outline::before {
+ content: "\FEA5";
+}
+.mdi-boom-gate-down::before {
+ content: "\FEA6";
+}
+.mdi-boom-gate-down-outline::before {
+ content: "\FEA7";
+}
+.mdi-boom-gate-outline::before {
+ content: "\FEA8";
+}
+.mdi-boom-gate-up::before {
+ content: "\FEA9";
+}
+.mdi-boom-gate-up-outline::before {
+ content: "\FEAA";
+}
+.mdi-boombox::before {
+ content: "\F5DC";
+}
+.mdi-boomerang::before {
+ content: "\F00FA";
+}
+.mdi-bootstrap::before {
+ content: "\F6C5";
+}
+.mdi-border-all::before {
+ content: "\F0C7";
+}
+.mdi-border-all-variant::before {
+ content: "\F8A0";
+}
+.mdi-border-bottom::before {
+ content: "\F0C8";
+}
+.mdi-border-bottom-variant::before {
+ content: "\F8A1";
+}
+.mdi-border-color::before {
+ content: "\F0C9";
+}
+.mdi-border-horizontal::before {
+ content: "\F0CA";
+}
+.mdi-border-inside::before {
+ content: "\F0CB";
+}
+.mdi-border-left::before {
+ content: "\F0CC";
+}
+.mdi-border-left-variant::before {
+ content: "\F8A2";
+}
+.mdi-border-none::before {
+ content: "\F0CD";
+}
+.mdi-border-none-variant::before {
+ content: "\F8A3";
+}
+.mdi-border-outside::before {
+ content: "\F0CE";
+}
+.mdi-border-right::before {
+ content: "\F0CF";
+}
+.mdi-border-right-variant::before {
+ content: "\F8A4";
+}
+.mdi-border-style::before {
+ content: "\F0D0";
+}
+.mdi-border-top::before {
+ content: "\F0D1";
+}
+.mdi-border-top-variant::before {
+ content: "\F8A5";
+}
+.mdi-border-vertical::before {
+ content: "\F0D2";
+}
+.mdi-bottle-soda::before {
+ content: "\F009B";
+}
+.mdi-bottle-soda-classic::before {
+ content: "\F009C";
+}
+.mdi-bottle-soda-classic-outline::before {
+ content: "\F038E";
+}
+.mdi-bottle-soda-outline::before {
+ content: "\F009D";
+}
+.mdi-bottle-tonic::before {
+ content: "\F0159";
+}
+.mdi-bottle-tonic-outline::before {
+ content: "\F015A";
+}
+.mdi-bottle-tonic-plus::before {
+ content: "\F015B";
+}
+.mdi-bottle-tonic-plus-outline::before {
+ content: "\F015C";
+}
+.mdi-bottle-tonic-skull::before {
+ content: "\F015D";
+}
+.mdi-bottle-tonic-skull-outline::before {
+ content: "\F015E";
+}
+.mdi-bottle-wine::before {
+ content: "\F853";
+}
+.mdi-bottle-wine-outline::before {
+ content: "\F033B";
+}
+.mdi-bow-tie::before {
+ content: "\F677";
+}
+.mdi-bowl::before {
+ content: "\F617";
+}
+.mdi-bowling::before {
+ content: "\F0D3";
+}
+.mdi-box::before {
+ content: "\F0D4";
+}
+.mdi-box-cutter::before {
+ content: "\F0D5";
+}
+.mdi-box-shadow::before {
+ content: "\F637";
+}
+.mdi-boxing-glove::before {
+ content: "\FB41";
+}
+.mdi-braille::before {
+ content: "\F9CF";
+}
+.mdi-brain::before {
+ content: "\F9D0";
+}
+.mdi-bread-slice::before {
+ content: "\FCCA";
+}
+.mdi-bread-slice-outline::before {
+ content: "\FCCB";
+}
+.mdi-bridge::before {
+ content: "\F618";
+}
+.mdi-briefcase::before {
+ content: "\F0D6";
+}
+.mdi-briefcase-account::before {
+ content: "\FCCC";
+}
+.mdi-briefcase-account-outline::before {
+ content: "\FCCD";
+}
+.mdi-briefcase-check::before {
+ content: "\F0D7";
+}
+.mdi-briefcase-check-outline::before {
+ content: "\F0349";
+}
+.mdi-briefcase-clock::before {
+ content: "\F00FB";
+}
+.mdi-briefcase-clock-outline::before {
+ content: "\F00FC";
+}
+.mdi-briefcase-download::before {
+ content: "\F0D8";
+}
+.mdi-briefcase-download-outline::before {
+ content: "\FC19";
+}
+.mdi-briefcase-edit::before {
+ content: "\FA97";
+}
+.mdi-briefcase-edit-outline::before {
+ content: "\FC1A";
+}
+.mdi-briefcase-minus::before {
+ content: "\FA29";
+}
+.mdi-briefcase-minus-outline::before {
+ content: "\FC1B";
+}
+.mdi-briefcase-outline::before {
+ content: "\F813";
+}
+.mdi-briefcase-plus::before {
+ content: "\FA2A";
+}
+.mdi-briefcase-plus-outline::before {
+ content: "\FC1C";
+}
+.mdi-briefcase-remove::before {
+ content: "\FA2B";
+}
+.mdi-briefcase-remove-outline::before {
+ content: "\FC1D";
+}
+.mdi-briefcase-search::before {
+ content: "\FA2C";
+}
+.mdi-briefcase-search-outline::before {
+ content: "\FC1E";
+}
+.mdi-briefcase-upload::before {
+ content: "\F0D9";
+}
+.mdi-briefcase-upload-outline::before {
+ content: "\FC1F";
+}
+.mdi-brightness-1::before {
+ content: "\F0DA";
+}
+.mdi-brightness-2::before {
+ content: "\F0DB";
+}
+.mdi-brightness-3::before {
+ content: "\F0DC";
+}
+.mdi-brightness-4::before {
+ content: "\F0DD";
+}
+.mdi-brightness-5::before {
+ content: "\F0DE";
+}
+.mdi-brightness-6::before {
+ content: "\F0DF";
+}
+.mdi-brightness-7::before {
+ content: "\F0E0";
+}
+.mdi-brightness-auto::before {
+ content: "\F0E1";
+}
+.mdi-brightness-percent::before {
+ content: "\FCCE";
+}
+.mdi-broom::before {
+ content: "\F0E2";
+}
+.mdi-brush::before {
+ content: "\F0E3";
+}
+.mdi-buddhism::before {
+ content: "\F94A";
+}
+.mdi-buffer::before {
+ content: "\F619";
+}
+.mdi-bug::before {
+ content: "\F0E4";
+}
+.mdi-bug-check::before {
+ content: "\FA2D";
+}
+.mdi-bug-check-outline::before {
+ content: "\FA2E";
+}
+.mdi-bug-outline::before {
+ content: "\FA2F";
+}
+.mdi-bugle::before {
+ content: "\FD90";
+}
+.mdi-bulldozer::before {
+ content: "\FB07";
+}
+.mdi-bullet::before {
+ content: "\FCCF";
+}
+.mdi-bulletin-board::before {
+ content: "\F0E5";
+}
+.mdi-bullhorn::before {
+ content: "\F0E6";
+}
+.mdi-bullhorn-outline::before {
+ content: "\FB08";
+}
+.mdi-bullseye::before {
+ content: "\F5DD";
+}
+.mdi-bullseye-arrow::before {
+ content: "\F8C8";
+}
+.mdi-bulma::before {
+ content: "\F0312";
+}
+.mdi-bunk-bed::before {
+ content: "\F032D";
+}
+.mdi-bus::before {
+ content: "\F0E7";
+}
+.mdi-bus-alert::before {
+ content: "\FA98";
+}
+.mdi-bus-articulated-end::before {
+ content: "\F79B";
+}
+.mdi-bus-articulated-front::before {
+ content: "\F79C";
+}
+.mdi-bus-clock::before {
+ content: "\F8C9";
+}
+.mdi-bus-double-decker::before {
+ content: "\F79D";
+}
+.mdi-bus-marker::before {
+ content: "\F023D";
+}
+.mdi-bus-multiple::before {
+ content: "\FF5C";
+}
+.mdi-bus-school::before {
+ content: "\F79E";
+}
+.mdi-bus-side::before {
+ content: "\F79F";
+}
+.mdi-bus-stop::before {
+ content: "\F0034";
+}
+.mdi-bus-stop-covered::before {
+ content: "\F0035";
+}
+.mdi-bus-stop-uncovered::before {
+ content: "\F0036";
+}
+.mdi-cached::before {
+ content: "\F0E8";
+}
+.mdi-cactus::before {
+ content: "\FD91";
+}
+.mdi-cake::before {
+ content: "\F0E9";
+}
+.mdi-cake-layered::before {
+ content: "\F0EA";
+}
+.mdi-cake-variant::before {
+ content: "\F0EB";
+}
+.mdi-calculator::before {
+ content: "\F0EC";
+}
+.mdi-calculator-variant::before {
+ content: "\FA99";
+}
+.mdi-calendar::before {
+ content: "\F0ED";
+}
+.mdi-calendar-account::before {
+ content: "\FEF4";
+}
+.mdi-calendar-account-outline::before {
+ content: "\FEF5";
+}
+.mdi-calendar-alert::before {
+ content: "\FA30";
+}
+.mdi-calendar-arrow-left::before {
+ content: "\F015F";
+}
+.mdi-calendar-arrow-right::before {
+ content: "\F0160";
+}
+.mdi-calendar-blank::before {
+ content: "\F0EE";
+}
+.mdi-calendar-blank-multiple::before {
+ content: "\F009E";
+}
+.mdi-calendar-blank-outline::before {
+ content: "\FB42";
+}
+.mdi-calendar-check::before {
+ content: "\F0EF";
+}
+.mdi-calendar-check-outline::before {
+ content: "\FC20";
+}
+.mdi-calendar-clock::before {
+ content: "\F0F0";
+}
+.mdi-calendar-edit::before {
+ content: "\F8A6";
+}
+.mdi-calendar-export::before {
+ content: "\FB09";
+}
+.mdi-calendar-heart::before {
+ content: "\F9D1";
+}
+.mdi-calendar-import::before {
+ content: "\FB0A";
+}
+.mdi-calendar-minus::before {
+ content: "\FD38";
+}
+.mdi-calendar-month::before {
+ content: "\FDFA";
+}
+.mdi-calendar-month-outline::before {
+ content: "\FDFB";
+}
+.mdi-calendar-multiple::before {
+ content: "\F0F1";
+}
+.mdi-calendar-multiple-check::before {
+ content: "\F0F2";
+}
+.mdi-calendar-multiselect::before {
+ content: "\FA31";
+}
+.mdi-calendar-outline::before {
+ content: "\FB43";
+}
+.mdi-calendar-plus::before {
+ content: "\F0F3";
+}
+.mdi-calendar-question::before {
+ content: "\F691";
+}
+.mdi-calendar-range::before {
+ content: "\F678";
+}
+.mdi-calendar-range-outline::before {
+ content: "\FB44";
+}
+.mdi-calendar-remove::before {
+ content: "\F0F4";
+}
+.mdi-calendar-remove-outline::before {
+ content: "\FC21";
+}
+.mdi-calendar-repeat::before {
+ content: "\FEAB";
+}
+.mdi-calendar-repeat-outline::before {
+ content: "\FEAC";
+}
+.mdi-calendar-search::before {
+ content: "\F94B";
+}
+.mdi-calendar-star::before {
+ content: "\F9D2";
+}
+.mdi-calendar-text::before {
+ content: "\F0F5";
+}
+.mdi-calendar-text-outline::before {
+ content: "\FC22";
+}
+.mdi-calendar-today::before {
+ content: "\F0F6";
+}
+.mdi-calendar-week::before {
+ content: "\FA32";
+}
+.mdi-calendar-week-begin::before {
+ content: "\FA33";
+}
+.mdi-calendar-weekend::before {
+ content: "\FEF6";
+}
+.mdi-calendar-weekend-outline::before {
+ content: "\FEF7";
+}
+.mdi-call-made::before {
+ content: "\F0F7";
+}
+.mdi-call-merge::before {
+ content: "\F0F8";
+}
+.mdi-call-missed::before {
+ content: "\F0F9";
+}
+.mdi-call-received::before {
+ content: "\F0FA";
+}
+.mdi-call-split::before {
+ content: "\F0FB";
+}
+.mdi-camcorder::before {
+ content: "\F0FC";
+}
+.mdi-camcorder-box::before {
+ content: "\F0FD";
+}
+.mdi-camcorder-box-off::before {
+ content: "\F0FE";
+}
+.mdi-camcorder-off::before {
+ content: "\F0FF";
+}
+.mdi-camera::before {
+ content: "\F100";
+}
+.mdi-camera-account::before {
+ content: "\F8CA";
+}
+.mdi-camera-burst::before {
+ content: "\F692";
+}
+.mdi-camera-control::before {
+ content: "\FB45";
+}
+.mdi-camera-enhance::before {
+ content: "\F101";
+}
+.mdi-camera-enhance-outline::before {
+ content: "\FB46";
+}
+.mdi-camera-front::before {
+ content: "\F102";
+}
+.mdi-camera-front-variant::before {
+ content: "\F103";
+}
+.mdi-camera-gopro::before {
+ content: "\F7A0";
+}
+.mdi-camera-image::before {
+ content: "\F8CB";
+}
+.mdi-camera-iris::before {
+ content: "\F104";
+}
+.mdi-camera-metering-center::before {
+ content: "\F7A1";
+}
+.mdi-camera-metering-matrix::before {
+ content: "\F7A2";
+}
+.mdi-camera-metering-partial::before {
+ content: "\F7A3";
+}
+.mdi-camera-metering-spot::before {
+ content: "\F7A4";
+}
+.mdi-camera-off::before {
+ content: "\F5DF";
+}
+.mdi-camera-outline::before {
+ content: "\FD39";
+}
+.mdi-camera-party-mode::before {
+ content: "\F105";
+}
+.mdi-camera-plus::before {
+ content: "\FEF8";
+}
+.mdi-camera-plus-outline::before {
+ content: "\FEF9";
+}
+.mdi-camera-rear::before {
+ content: "\F106";
+}
+.mdi-camera-rear-variant::before {
+ content: "\F107";
+}
+.mdi-camera-retake::before {
+ content: "\FDFC";
+}
+.mdi-camera-retake-outline::before {
+ content: "\FDFD";
+}
+.mdi-camera-switch::before {
+ content: "\F108";
+}
+.mdi-camera-timer::before {
+ content: "\F109";
+}
+.mdi-camera-wireless::before {
+ content: "\FD92";
+}
+.mdi-camera-wireless-outline::before {
+ content: "\FD93";
+}
+.mdi-campfire::before {
+ content: "\FEFA";
+}
+.mdi-cancel::before {
+ content: "\F739";
+}
+.mdi-candle::before {
+ content: "\F5E2";
+}
+.mdi-candycane::before {
+ content: "\F10A";
+}
+.mdi-cannabis::before {
+ content: "\F7A5";
+}
+.mdi-caps-lock::before {
+ content: "\FA9A";
+}
+.mdi-car::before {
+ content: "\F10B";
+}
+.mdi-car-2-plus::before {
+ content: "\F0037";
+}
+.mdi-car-3-plus::before {
+ content: "\F0038";
+}
+.mdi-car-back::before {
+ content: "\FDFE";
+}
+.mdi-car-battery::before {
+ content: "\F10C";
+}
+.mdi-car-brake-abs::before {
+ content: "\FC23";
+}
+.mdi-car-brake-alert::before {
+ content: "\FC24";
+}
+.mdi-car-brake-hold::before {
+ content: "\FD3A";
+}
+.mdi-car-brake-parking::before {
+ content: "\FD3B";
+}
+.mdi-car-brake-retarder::before {
+ content: "\F0039";
+}
+.mdi-car-child-seat::before {
+ content: "\FFC3";
+}
+.mdi-car-clutch::before {
+ content: "\F003A";
+}
+.mdi-car-connected::before {
+ content: "\F10D";
+}
+.mdi-car-convertible::before {
+ content: "\F7A6";
+}
+.mdi-car-coolant-level::before {
+ content: "\F003B";
+}
+.mdi-car-cruise-control::before {
+ content: "\FD3C";
+}
+.mdi-car-defrost-front::before {
+ content: "\FD3D";
+}
+.mdi-car-defrost-rear::before {
+ content: "\FD3E";
+}
+.mdi-car-door::before {
+ content: "\FB47";
+}
+.mdi-car-door-lock::before {
+ content: "\F00C8";
+}
+.mdi-car-electric::before {
+ content: "\FB48";
+}
+.mdi-car-esp::before {
+ content: "\FC25";
+}
+.mdi-car-estate::before {
+ content: "\F7A7";
+}
+.mdi-car-hatchback::before {
+ content: "\F7A8";
+}
+.mdi-car-info::before {
+ content: "\F01E9";
+}
+.mdi-car-key::before {
+ content: "\FB49";
+}
+.mdi-car-light-dimmed::before {
+ content: "\FC26";
+}
+.mdi-car-light-fog::before {
+ content: "\FC27";
+}
+.mdi-car-light-high::before {
+ content: "\FC28";
+}
+.mdi-car-limousine::before {
+ content: "\F8CC";
+}
+.mdi-car-multiple::before {
+ content: "\FB4A";
+}
+.mdi-car-off::before {
+ content: "\FDFF";
+}
+.mdi-car-parking-lights::before {
+ content: "\FD3F";
+}
+.mdi-car-pickup::before {
+ content: "\F7A9";
+}
+.mdi-car-seat::before {
+ content: "\FFC4";
+}
+.mdi-car-seat-cooler::before {
+ content: "\FFC5";
+}
+.mdi-car-seat-heater::before {
+ content: "\FFC6";
+}
+.mdi-car-shift-pattern::before {
+ content: "\FF5D";
+}
+.mdi-car-side::before {
+ content: "\F7AA";
+}
+.mdi-car-sports::before {
+ content: "\F7AB";
+}
+.mdi-car-tire-alert::before {
+ content: "\FC29";
+}
+.mdi-car-traction-control::before {
+ content: "\FD40";
+}
+.mdi-car-turbocharger::before {
+ content: "\F003C";
+}
+.mdi-car-wash::before {
+ content: "\F10E";
+}
+.mdi-car-windshield::before {
+ content: "\F003D";
+}
+.mdi-car-windshield-outline::before {
+ content: "\F003E";
+}
+.mdi-caravan::before {
+ content: "\F7AC";
+}
+.mdi-card::before {
+ content: "\FB4B";
+}
+.mdi-card-bulleted::before {
+ content: "\FB4C";
+}
+.mdi-card-bulleted-off::before {
+ content: "\FB4D";
+}
+.mdi-card-bulleted-off-outline::before {
+ content: "\FB4E";
+}
+.mdi-card-bulleted-outline::before {
+ content: "\FB4F";
+}
+.mdi-card-bulleted-settings::before {
+ content: "\FB50";
+}
+.mdi-card-bulleted-settings-outline::before {
+ content: "\FB51";
+}
+.mdi-card-outline::before {
+ content: "\FB52";
+}
+.mdi-card-plus::before {
+ content: "\F022A";
+}
+.mdi-card-plus-outline::before {
+ content: "\F022B";
+}
+.mdi-card-search::before {
+ content: "\F009F";
+}
+.mdi-card-search-outline::before {
+ content: "\F00A0";
+}
+.mdi-card-text::before {
+ content: "\FB53";
+}
+.mdi-card-text-outline::before {
+ content: "\FB54";
+}
+.mdi-cards::before {
+ content: "\F638";
+}
+.mdi-cards-club::before {
+ content: "\F8CD";
+}
+.mdi-cards-diamond::before {
+ content: "\F8CE";
+}
+.mdi-cards-diamond-outline::before {
+ content: "\F003F";
+}
+.mdi-cards-heart::before {
+ content: "\F8CF";
+}
+.mdi-cards-outline::before {
+ content: "\F639";
+}
+.mdi-cards-playing-outline::before {
+ content: "\F63A";
+}
+.mdi-cards-spade::before {
+ content: "\F8D0";
+}
+.mdi-cards-variant::before {
+ content: "\F6C6";
+}
+.mdi-carrot::before {
+ content: "\F10F";
+}
+.mdi-cart::before {
+ content: "\F110";
+}
+.mdi-cart-arrow-down::before {
+ content: "\FD42";
+}
+.mdi-cart-arrow-right::before {
+ content: "\FC2A";
+}
+.mdi-cart-arrow-up::before {
+ content: "\FD43";
+}
+.mdi-cart-minus::before {
+ content: "\FD44";
+}
+.mdi-cart-off::before {
+ content: "\F66B";
+}
+.mdi-cart-outline::before {
+ content: "\F111";
+}
+.mdi-cart-plus::before {
+ content: "\F112";
+}
+.mdi-cart-remove::before {
+ content: "\FD45";
+}
+.mdi-case-sensitive-alt::before {
+ content: "\F113";
+}
+.mdi-cash::before {
+ content: "\F114";
+}
+.mdi-cash-100::before {
+ content: "\F115";
+}
+.mdi-cash-marker::before {
+ content: "\FD94";
+}
+.mdi-cash-minus::before {
+ content: "\F028B";
+}
+.mdi-cash-multiple::before {
+ content: "\F116";
+}
+.mdi-cash-plus::before {
+ content: "\F028C";
+}
+.mdi-cash-refund::before {
+ content: "\FA9B";
+}
+.mdi-cash-register::before {
+ content: "\FCD0";
+}
+.mdi-cash-remove::before {
+ content: "\F028D";
+}
+.mdi-cash-usd::before {
+ content: "\F01A1";
+}
+.mdi-cash-usd-outline::before {
+ content: "\F117";
+}
+.mdi-cassette::before {
+ content: "\F9D3";
+}
+.mdi-cast::before {
+ content: "\F118";
+}
+.mdi-cast-audio::before {
+ content: "\F0040";
+}
+.mdi-cast-connected::before {
+ content: "\F119";
+}
+.mdi-cast-education::before {
+ content: "\FE6D";
+}
+.mdi-cast-off::before {
+ content: "\F789";
+}
+.mdi-castle::before {
+ content: "\F11A";
+}
+.mdi-cat::before {
+ content: "\F11B";
+}
+.mdi-cctv::before {
+ content: "\F7AD";
+}
+.mdi-ceiling-light::before {
+ content: "\F768";
+}
+.mdi-cellphone::before {
+ content: "\F11C";
+}
+.mdi-cellphone-android::before {
+ content: "\F11D";
+}
+.mdi-cellphone-arrow-down::before {
+ content: "\F9D4";
+}
+.mdi-cellphone-basic::before {
+ content: "\F11E";
+}
+.mdi-cellphone-dock::before {
+ content: "\F11F";
+}
+.mdi-cellphone-erase::before {
+ content: "\F94C";
+}
+.mdi-cellphone-information::before {
+ content: "\FF5E";
+}
+.mdi-cellphone-iphone::before {
+ content: "\F120";
+}
+.mdi-cellphone-key::before {
+ content: "\F94D";
+}
+.mdi-cellphone-link::before {
+ content: "\F121";
+}
+.mdi-cellphone-link-off::before {
+ content: "\F122";
+}
+.mdi-cellphone-lock::before {
+ content: "\F94E";
+}
+.mdi-cellphone-message::before {
+ content: "\F8D2";
+}
+.mdi-cellphone-message-off::before {
+ content: "\F00FD";
+}
+.mdi-cellphone-nfc::before {
+ content: "\FEAD";
+}
+.mdi-cellphone-nfc-off::before {
+ content: "\F0303";
+}
+.mdi-cellphone-off::before {
+ content: "\F94F";
+}
+.mdi-cellphone-play::before {
+ content: "\F0041";
+}
+.mdi-cellphone-screenshot::before {
+ content: "\FA34";
+}
+.mdi-cellphone-settings::before {
+ content: "\F123";
+}
+.mdi-cellphone-settings-variant::before {
+ content: "\F950";
+}
+.mdi-cellphone-sound::before {
+ content: "\F951";
+}
+.mdi-cellphone-text::before {
+ content: "\F8D1";
+}
+.mdi-cellphone-wireless::before {
+ content: "\F814";
+}
+.mdi-celtic-cross::before {
+ content: "\FCD1";
+}
+.mdi-centos::before {
+ content: "\F0145";
+}
+.mdi-certificate::before {
+ content: "\F124";
+}
+.mdi-certificate-outline::before {
+ content: "\F01B3";
+}
+.mdi-chair-rolling::before {
+ content: "\FFBA";
+}
+.mdi-chair-school::before {
+ content: "\F125";
+}
+.mdi-charity::before {
+ content: "\FC2B";
+}
+.mdi-chart-arc::before {
+ content: "\F126";
+}
+.mdi-chart-areaspline::before {
+ content: "\F127";
+}
+.mdi-chart-areaspline-variant::before {
+ content: "\FEAE";
+}
+.mdi-chart-bar::before {
+ content: "\F128";
+}
+.mdi-chart-bar-stacked::before {
+ content: "\F769";
+}
+.mdi-chart-bell-curve::before {
+ content: "\FC2C";
+}
+.mdi-chart-bell-curve-cumulative::before {
+ content: "\FFC7";
+}
+.mdi-chart-bubble::before {
+ content: "\F5E3";
+}
+.mdi-chart-donut::before {
+ content: "\F7AE";
+}
+.mdi-chart-donut-variant::before {
+ content: "\F7AF";
+}
+.mdi-chart-gantt::before {
+ content: "\F66C";
+}
+.mdi-chart-histogram::before {
+ content: "\F129";
+}
+.mdi-chart-line::before {
+ content: "\F12A";
+}
+.mdi-chart-line-stacked::before {
+ content: "\F76A";
+}
+.mdi-chart-line-variant::before {
+ content: "\F7B0";
+}
+.mdi-chart-multiline::before {
+ content: "\F8D3";
+}
+.mdi-chart-multiple::before {
+ content: "\F023E";
+}
+.mdi-chart-pie::before {
+ content: "\F12B";
+}
+.mdi-chart-ppf::before {
+ content: "\F03AB";
+}
+.mdi-chart-scatter-plot::before {
+ content: "\FEAF";
+}
+.mdi-chart-scatter-plot-hexbin::before {
+ content: "\F66D";
+}
+.mdi-chart-snakey::before {
+ content: "\F020A";
+}
+.mdi-chart-snakey-variant::before {
+ content: "\F020B";
+}
+.mdi-chart-timeline::before {
+ content: "\F66E";
+}
+.mdi-chart-timeline-variant::before {
+ content: "\FEB0";
+}
+.mdi-chart-tree::before {
+ content: "\FEB1";
+}
+.mdi-chat::before {
+ content: "\FB55";
+}
+.mdi-chat-alert::before {
+ content: "\FB56";
+}
+.mdi-chat-alert-outline::before {
+ content: "\F02F4";
+}
+.mdi-chat-outline::before {
+ content: "\FEFB";
+}
+.mdi-chat-processing::before {
+ content: "\FB57";
+}
+.mdi-chat-processing-outline::before {
+ content: "\F02F5";
+}
+.mdi-chat-sleep::before {
+ content: "\F02FC";
+}
+.mdi-chat-sleep-outline::before {
+ content: "\F02FD";
+}
+.mdi-check::before {
+ content: "\F12C";
+}
+.mdi-check-all::before {
+ content: "\F12D";
+}
+.mdi-check-bold::before {
+ content: "\FE6E";
+}
+.mdi-check-box-multiple-outline::before {
+ content: "\FC2D";
+}
+.mdi-check-box-outline::before {
+ content: "\FC2E";
+}
+.mdi-check-circle::before {
+ content: "\F5E0";
+}
+.mdi-check-circle-outline::before {
+ content: "\F5E1";
+}
+.mdi-check-decagram::before {
+ content: "\F790";
+}
+.mdi-check-network::before {
+ content: "\FC2F";
+}
+.mdi-check-network-outline::before {
+ content: "\FC30";
+}
+.mdi-check-outline::before {
+ content: "\F854";
+}
+.mdi-check-underline::before {
+ content: "\FE70";
+}
+.mdi-check-underline-circle::before {
+ content: "\FE71";
+}
+.mdi-check-underline-circle-outline::before {
+ content: "\FE72";
+}
+.mdi-checkbook::before {
+ content: "\FA9C";
+}
+.mdi-checkbox-blank::before {
+ content: "\F12E";
+}
+.mdi-checkbox-blank-circle::before {
+ content: "\F12F";
+}
+.mdi-checkbox-blank-circle-outline::before {
+ content: "\F130";
+}
+.mdi-checkbox-blank-off::before {
+ content: "\F0317";
+}
+.mdi-checkbox-blank-off-outline::before {
+ content: "\F0318";
+}
+.mdi-checkbox-blank-outline::before {
+ content: "\F131";
+}
+.mdi-checkbox-intermediate::before {
+ content: "\F855";
+}
+.mdi-checkbox-marked::before {
+ content: "\F132";
+}
+.mdi-checkbox-marked-circle::before {
+ content: "\F133";
+}
+.mdi-checkbox-marked-circle-outline::before {
+ content: "\F134";
+}
+.mdi-checkbox-marked-outline::before {
+ content: "\F135";
+}
+.mdi-checkbox-multiple-blank::before {
+ content: "\F136";
+}
+.mdi-checkbox-multiple-blank-circle::before {
+ content: "\F63B";
+}
+.mdi-checkbox-multiple-blank-circle-outline::before {
+ content: "\F63C";
+}
+.mdi-checkbox-multiple-blank-outline::before {
+ content: "\F137";
+}
+.mdi-checkbox-multiple-marked::before {
+ content: "\F138";
+}
+.mdi-checkbox-multiple-marked-circle::before {
+ content: "\F63D";
+}
+.mdi-checkbox-multiple-marked-circle-outline::before {
+ content: "\F63E";
+}
+.mdi-checkbox-multiple-marked-outline::before {
+ content: "\F139";
+}
+.mdi-checkerboard::before {
+ content: "\F13A";
+}
+.mdi-checkerboard-minus::before {
+ content: "\F022D";
+}
+.mdi-checkerboard-plus::before {
+ content: "\F022C";
+}
+.mdi-checkerboard-remove::before {
+ content: "\F022E";
+}
+.mdi-cheese::before {
+ content: "\F02E4";
+}
+.mdi-chef-hat::before {
+ content: "\FB58";
+}
+.mdi-chemical-weapon::before {
+ content: "\F13B";
+}
+.mdi-chess-bishop::before {
+ content: "\F85B";
+}
+.mdi-chess-king::before {
+ content: "\F856";
+}
+.mdi-chess-knight::before {
+ content: "\F857";
+}
+.mdi-chess-pawn::before {
+ content: "\F858";
+}
+.mdi-chess-queen::before {
+ content: "\F859";
+}
+.mdi-chess-rook::before {
+ content: "\F85A";
+}
+.mdi-chevron-double-down::before {
+ content: "\F13C";
+}
+.mdi-chevron-double-left::before {
+ content: "\F13D";
+}
+.mdi-chevron-double-right::before {
+ content: "\F13E";
+}
+.mdi-chevron-double-up::before {
+ content: "\F13F";
+}
+.mdi-chevron-down::before {
+ content: "\F140";
+}
+.mdi-chevron-down-box::before {
+ content: "\F9D5";
+}
+.mdi-chevron-down-box-outline::before {
+ content: "\F9D6";
+}
+.mdi-chevron-down-circle::before {
+ content: "\FB0B";
+}
+.mdi-chevron-down-circle-outline::before {
+ content: "\FB0C";
+}
+.mdi-chevron-left::before {
+ content: "\F141";
+}
+.mdi-chevron-left-box::before {
+ content: "\F9D7";
+}
+.mdi-chevron-left-box-outline::before {
+ content: "\F9D8";
+}
+.mdi-chevron-left-circle::before {
+ content: "\FB0D";
+}
+.mdi-chevron-left-circle-outline::before {
+ content: "\FB0E";
+}
+.mdi-chevron-right::before {
+ content: "\F142";
+}
+.mdi-chevron-right-box::before {
+ content: "\F9D9";
+}
+.mdi-chevron-right-box-outline::before {
+ content: "\F9DA";
+}
+.mdi-chevron-right-circle::before {
+ content: "\FB0F";
+}
+.mdi-chevron-right-circle-outline::before {
+ content: "\FB10";
+}
+.mdi-chevron-triple-down::before {
+ content: "\FD95";
+}
+.mdi-chevron-triple-left::before {
+ content: "\FD96";
+}
+.mdi-chevron-triple-right::before {
+ content: "\FD97";
+}
+.mdi-chevron-triple-up::before {
+ content: "\FD98";
+}
+.mdi-chevron-up::before {
+ content: "\F143";
+}
+.mdi-chevron-up-box::before {
+ content: "\F9DB";
+}
+.mdi-chevron-up-box-outline::before {
+ content: "\F9DC";
+}
+.mdi-chevron-up-circle::before {
+ content: "\FB11";
+}
+.mdi-chevron-up-circle-outline::before {
+ content: "\FB12";
+}
+.mdi-chili-hot::before {
+ content: "\F7B1";
+}
+.mdi-chili-medium::before {
+ content: "\F7B2";
+}
+.mdi-chili-mild::before {
+ content: "\F7B3";
+}
+.mdi-chip::before {
+ content: "\F61A";
+}
+.mdi-christianity::before {
+ content: "\F952";
+}
+.mdi-christianity-outline::before {
+ content: "\FCD2";
+}
+.mdi-church::before {
+ content: "\F144";
+}
+.mdi-cigar::before {
+ content: "\F01B4";
+}
+.mdi-circle::before {
+ content: "\F764";
+}
+.mdi-circle-double::before {
+ content: "\FEB2";
+}
+.mdi-circle-edit-outline::before {
+ content: "\F8D4";
+}
+.mdi-circle-expand::before {
+ content: "\FEB3";
+}
+.mdi-circle-medium::before {
+ content: "\F9DD";
+}
+.mdi-circle-off-outline::before {
+ content: "\F00FE";
+}
+.mdi-circle-outline::before {
+ content: "\F765";
+}
+.mdi-circle-slice-1::before {
+ content: "\FA9D";
+}
+.mdi-circle-slice-2::before {
+ content: "\FA9E";
+}
+.mdi-circle-slice-3::before {
+ content: "\FA9F";
+}
+.mdi-circle-slice-4::before {
+ content: "\FAA0";
+}
+.mdi-circle-slice-5::before {
+ content: "\FAA1";
+}
+.mdi-circle-slice-6::before {
+ content: "\FAA2";
+}
+.mdi-circle-slice-7::before {
+ content: "\FAA3";
+}
+.mdi-circle-slice-8::before {
+ content: "\FAA4";
+}
+.mdi-circle-small::before {
+ content: "\F9DE";
+}
+.mdi-circular-saw::before {
+ content: "\FE73";
+}
+.mdi-cisco-webex::before {
+ content: "\F145";
+}
+.mdi-city::before {
+ content: "\F146";
+}
+.mdi-city-variant::before {
+ content: "\FA35";
+}
+.mdi-city-variant-outline::before {
+ content: "\FA36";
+}
+.mdi-clipboard::before {
+ content: "\F147";
+}
+.mdi-clipboard-account::before {
+ content: "\F148";
+}
+.mdi-clipboard-account-outline::before {
+ content: "\FC31";
+}
+.mdi-clipboard-alert::before {
+ content: "\F149";
+}
+.mdi-clipboard-alert-outline::before {
+ content: "\FCD3";
+}
+.mdi-clipboard-arrow-down::before {
+ content: "\F14A";
+}
+.mdi-clipboard-arrow-down-outline::before {
+ content: "\FC32";
+}
+.mdi-clipboard-arrow-left::before {
+ content: "\F14B";
+}
+.mdi-clipboard-arrow-left-outline::before {
+ content: "\FCD4";
+}
+.mdi-clipboard-arrow-right::before {
+ content: "\FCD5";
+}
+.mdi-clipboard-arrow-right-outline::before {
+ content: "\FCD6";
+}
+.mdi-clipboard-arrow-up::before {
+ content: "\FC33";
+}
+.mdi-clipboard-arrow-up-outline::before {
+ content: "\FC34";
+}
+.mdi-clipboard-check::before {
+ content: "\F14C";
+}
+.mdi-clipboard-check-multiple::before {
+ content: "\F028E";
+}
+.mdi-clipboard-check-multiple-outline::before {
+ content: "\F028F";
+}
+.mdi-clipboard-check-outline::before {
+ content: "\F8A7";
+}
+.mdi-clipboard-file::before {
+ content: "\F0290";
+}
+.mdi-clipboard-file-outline::before {
+ content: "\F0291";
+}
+.mdi-clipboard-flow::before {
+ content: "\F6C7";
+}
+.mdi-clipboard-flow-outline::before {
+ content: "\F0142";
+}
+.mdi-clipboard-list::before {
+ content: "\F00FF";
+}
+.mdi-clipboard-list-outline::before {
+ content: "\F0100";
+}
+.mdi-clipboard-multiple::before {
+ content: "\F0292";
+}
+.mdi-clipboard-multiple-outline::before {
+ content: "\F0293";
+}
+.mdi-clipboard-outline::before {
+ content: "\F14D";
+}
+.mdi-clipboard-play::before {
+ content: "\FC35";
+}
+.mdi-clipboard-play-multiple::before {
+ content: "\F0294";
+}
+.mdi-clipboard-play-multiple-outline::before {
+ content: "\F0295";
+}
+.mdi-clipboard-play-outline::before {
+ content: "\FC36";
+}
+.mdi-clipboard-plus::before {
+ content: "\F750";
+}
+.mdi-clipboard-plus-outline::before {
+ content: "\F034A";
+}
+.mdi-clipboard-pulse::before {
+ content: "\F85C";
+}
+.mdi-clipboard-pulse-outline::before {
+ content: "\F85D";
+}
+.mdi-clipboard-text::before {
+ content: "\F14E";
+}
+.mdi-clipboard-text-multiple::before {
+ content: "\F0296";
+}
+.mdi-clipboard-text-multiple-outline::before {
+ content: "\F0297";
+}
+.mdi-clipboard-text-outline::before {
+ content: "\FA37";
+}
+.mdi-clipboard-text-play::before {
+ content: "\FC37";
+}
+.mdi-clipboard-text-play-outline::before {
+ content: "\FC38";
+}
+.mdi-clippy::before {
+ content: "\F14F";
+}
+.mdi-clock::before {
+ content: "\F953";
+}
+.mdi-clock-alert::before {
+ content: "\F954";
+}
+.mdi-clock-alert-outline::before {
+ content: "\F5CE";
+}
+.mdi-clock-check::before {
+ content: "\FFC8";
+}
+.mdi-clock-check-outline::before {
+ content: "\FFC9";
+}
+.mdi-clock-digital::before {
+ content: "\FEB4";
+}
+.mdi-clock-end::before {
+ content: "\F151";
+}
+.mdi-clock-fast::before {
+ content: "\F152";
+}
+.mdi-clock-in::before {
+ content: "\F153";
+}
+.mdi-clock-out::before {
+ content: "\F154";
+}
+.mdi-clock-outline::before {
+ content: "\F150";
+}
+.mdi-clock-start::before {
+ content: "\F155";
+}
+.mdi-close::before {
+ content: "\F156";
+}
+.mdi-close-box::before {
+ content: "\F157";
+}
+.mdi-close-box-multiple::before {
+ content: "\FC39";
+}
+.mdi-close-box-multiple-outline::before {
+ content: "\FC3A";
+}
+.mdi-close-box-outline::before {
+ content: "\F158";
+}
+.mdi-close-circle::before {
+ content: "\F159";
+}
+.mdi-close-circle-outline::before {
+ content: "\F15A";
+}
+.mdi-close-network::before {
+ content: "\F15B";
+}
+.mdi-close-network-outline::before {
+ content: "\FC3B";
+}
+.mdi-close-octagon::before {
+ content: "\F15C";
+}
+.mdi-close-octagon-outline::before {
+ content: "\F15D";
+}
+.mdi-close-outline::before {
+ content: "\F6C8";
+}
+.mdi-closed-caption::before {
+ content: "\F15E";
+}
+.mdi-closed-caption-outline::before {
+ content: "\FD99";
+}
+.mdi-cloud::before {
+ content: "\F15F";
+}
+.mdi-cloud-alert::before {
+ content: "\F9DF";
+}
+.mdi-cloud-braces::before {
+ content: "\F7B4";
+}
+.mdi-cloud-check::before {
+ content: "\F160";
+}
+.mdi-cloud-check-outline::before {
+ content: "\F02F7";
+}
+.mdi-cloud-circle::before {
+ content: "\F161";
+}
+.mdi-cloud-download::before {
+ content: "\F162";
+}
+.mdi-cloud-download-outline::before {
+ content: "\FB59";
+}
+.mdi-cloud-lock::before {
+ content: "\F021C";
+}
+.mdi-cloud-lock-outline::before {
+ content: "\F021D";
+}
+.mdi-cloud-off-outline::before {
+ content: "\F164";
+}
+.mdi-cloud-outline::before {
+ content: "\F163";
+}
+.mdi-cloud-print::before {
+ content: "\F165";
+}
+.mdi-cloud-print-outline::before {
+ content: "\F166";
+}
+.mdi-cloud-question::before {
+ content: "\FA38";
+}
+.mdi-cloud-search::before {
+ content: "\F955";
+}
+.mdi-cloud-search-outline::before {
+ content: "\F956";
+}
+.mdi-cloud-sync::before {
+ content: "\F63F";
+}
+.mdi-cloud-sync-outline::before {
+ content: "\F0301";
+}
+.mdi-cloud-tags::before {
+ content: "\F7B5";
+}
+.mdi-cloud-upload::before {
+ content: "\F167";
+}
+.mdi-cloud-upload-outline::before {
+ content: "\FB5A";
+}
+.mdi-clover::before {
+ content: "\F815";
+}
+.mdi-coach-lamp::before {
+ content: "\F0042";
+}
+.mdi-coat-rack::before {
+ content: "\F00C9";
+}
+.mdi-code-array::before {
+ content: "\F168";
+}
+.mdi-code-braces::before {
+ content: "\F169";
+}
+.mdi-code-braces-box::before {
+ content: "\F0101";
+}
+.mdi-code-brackets::before {
+ content: "\F16A";
+}
+.mdi-code-equal::before {
+ content: "\F16B";
+}
+.mdi-code-greater-than::before {
+ content: "\F16C";
+}
+.mdi-code-greater-than-or-equal::before {
+ content: "\F16D";
+}
+.mdi-code-less-than::before {
+ content: "\F16E";
+}
+.mdi-code-less-than-or-equal::before {
+ content: "\F16F";
+}
+.mdi-code-not-equal::before {
+ content: "\F170";
+}
+.mdi-code-not-equal-variant::before {
+ content: "\F171";
+}
+.mdi-code-parentheses::before {
+ content: "\F172";
+}
+.mdi-code-parentheses-box::before {
+ content: "\F0102";
+}
+.mdi-code-string::before {
+ content: "\F173";
+}
+.mdi-code-tags::before {
+ content: "\F174";
+}
+.mdi-code-tags-check::before {
+ content: "\F693";
+}
+.mdi-codepen::before {
+ content: "\F175";
+}
+.mdi-coffee::before {
+ content: "\F176";
+}
+.mdi-coffee-maker::before {
+ content: "\F00CA";
+}
+.mdi-coffee-off::before {
+ content: "\FFCA";
+}
+.mdi-coffee-off-outline::before {
+ content: "\FFCB";
+}
+.mdi-coffee-outline::before {
+ content: "\F6C9";
+}
+.mdi-coffee-to-go::before {
+ content: "\F177";
+}
+.mdi-coffee-to-go-outline::before {
+ content: "\F0339";
+}
+.mdi-coffin::before {
+ content: "\FB5B";
+}
+.mdi-cog-clockwise::before {
+ content: "\F0208";
+}
+.mdi-cog-counterclockwise::before {
+ content: "\F0209";
+}
+.mdi-cogs::before {
+ content: "\F8D5";
+}
+.mdi-coin::before {
+ content: "\F0196";
+}
+.mdi-coin-outline::before {
+ content: "\F178";
+}
+.mdi-coins::before {
+ content: "\F694";
+}
+.mdi-collage::before {
+ content: "\F640";
+}
+.mdi-collapse-all::before {
+ content: "\FAA5";
+}
+.mdi-collapse-all-outline::before {
+ content: "\FAA6";
+}
+.mdi-color-helper::before {
+ content: "\F179";
+}
+.mdi-comma::before {
+ content: "\FE74";
+}
+.mdi-comma-box::before {
+ content: "\FE75";
+}
+.mdi-comma-box-outline::before {
+ content: "\FE76";
+}
+.mdi-comma-circle::before {
+ content: "\FE77";
+}
+.mdi-comma-circle-outline::before {
+ content: "\FE78";
+}
+.mdi-comment::before {
+ content: "\F17A";
+}
+.mdi-comment-account::before {
+ content: "\F17B";
+}
+.mdi-comment-account-outline::before {
+ content: "\F17C";
+}
+.mdi-comment-alert::before {
+ content: "\F17D";
+}
+.mdi-comment-alert-outline::before {
+ content: "\F17E";
+}
+.mdi-comment-arrow-left::before {
+ content: "\F9E0";
+}
+.mdi-comment-arrow-left-outline::before {
+ content: "\F9E1";
+}
+.mdi-comment-arrow-right::before {
+ content: "\F9E2";
+}
+.mdi-comment-arrow-right-outline::before {
+ content: "\F9E3";
+}
+.mdi-comment-check::before {
+ content: "\F17F";
+}
+.mdi-comment-check-outline::before {
+ content: "\F180";
+}
+.mdi-comment-edit::before {
+ content: "\F01EA";
+}
+.mdi-comment-edit-outline::before {
+ content: "\F02EF";
+}
+.mdi-comment-eye::before {
+ content: "\FA39";
+}
+.mdi-comment-eye-outline::before {
+ content: "\FA3A";
+}
+.mdi-comment-multiple::before {
+ content: "\F85E";
+}
+.mdi-comment-multiple-outline::before {
+ content: "\F181";
+}
+.mdi-comment-outline::before {
+ content: "\F182";
+}
+.mdi-comment-plus::before {
+ content: "\F9E4";
+}
+.mdi-comment-plus-outline::before {
+ content: "\F183";
+}
+.mdi-comment-processing::before {
+ content: "\F184";
+}
+.mdi-comment-processing-outline::before {
+ content: "\F185";
+}
+.mdi-comment-question::before {
+ content: "\F816";
+}
+.mdi-comment-question-outline::before {
+ content: "\F186";
+}
+.mdi-comment-quote::before {
+ content: "\F0043";
+}
+.mdi-comment-quote-outline::before {
+ content: "\F0044";
+}
+.mdi-comment-remove::before {
+ content: "\F5DE";
+}
+.mdi-comment-remove-outline::before {
+ content: "\F187";
+}
+.mdi-comment-search::before {
+ content: "\FA3B";
+}
+.mdi-comment-search-outline::before {
+ content: "\FA3C";
+}
+.mdi-comment-text::before {
+ content: "\F188";
+}
+.mdi-comment-text-multiple::before {
+ content: "\F85F";
+}
+.mdi-comment-text-multiple-outline::before {
+ content: "\F860";
+}
+.mdi-comment-text-outline::before {
+ content: "\F189";
+}
+.mdi-compare::before {
+ content: "\F18A";
+}
+.mdi-compass::before {
+ content: "\F18B";
+}
+.mdi-compass-off::before {
+ content: "\FB5C";
+}
+.mdi-compass-off-outline::before {
+ content: "\FB5D";
+}
+.mdi-compass-outline::before {
+ content: "\F18C";
+}
+.mdi-compass-rose::before {
+ content: "\F03AD";
+}
+.mdi-concourse-ci::before {
+ content: "\F00CB";
+}
+.mdi-console::before {
+ content: "\F18D";
+}
+.mdi-console-line::before {
+ content: "\F7B6";
+}
+.mdi-console-network::before {
+ content: "\F8A8";
+}
+.mdi-console-network-outline::before {
+ content: "\FC3C";
+}
+.mdi-consolidate::before {
+ content: "\F0103";
+}
+.mdi-contact-mail::before {
+ content: "\F18E";
+}
+.mdi-contact-mail-outline::before {
+ content: "\FEB5";
+}
+.mdi-contact-phone::before {
+ content: "\FEB6";
+}
+.mdi-contact-phone-outline::before {
+ content: "\FEB7";
+}
+.mdi-contactless-payment::before {
+ content: "\FD46";
+}
+.mdi-contacts::before {
+ content: "\F6CA";
+}
+.mdi-contain::before {
+ content: "\FA3D";
+}
+.mdi-contain-end::before {
+ content: "\FA3E";
+}
+.mdi-contain-start::before {
+ content: "\FA3F";
+}
+.mdi-content-copy::before {
+ content: "\F18F";
+}
+.mdi-content-cut::before {
+ content: "\F190";
+}
+.mdi-content-duplicate::before {
+ content: "\F191";
+}
+.mdi-content-paste::before {
+ content: "\F192";
+}
+.mdi-content-save::before {
+ content: "\F193";
+}
+.mdi-content-save-alert::before {
+ content: "\FF5F";
+}
+.mdi-content-save-alert-outline::before {
+ content: "\FF60";
+}
+.mdi-content-save-all::before {
+ content: "\F194";
+}
+.mdi-content-save-all-outline::before {
+ content: "\FF61";
+}
+.mdi-content-save-edit::before {
+ content: "\FCD7";
+}
+.mdi-content-save-edit-outline::before {
+ content: "\FCD8";
+}
+.mdi-content-save-move::before {
+ content: "\FE79";
+}
+.mdi-content-save-move-outline::before {
+ content: "\FE7A";
+}
+.mdi-content-save-outline::before {
+ content: "\F817";
+}
+.mdi-content-save-settings::before {
+ content: "\F61B";
+}
+.mdi-content-save-settings-outline::before {
+ content: "\FB13";
+}
+.mdi-contrast::before {
+ content: "\F195";
+}
+.mdi-contrast-box::before {
+ content: "\F196";
+}
+.mdi-contrast-circle::before {
+ content: "\F197";
+}
+.mdi-controller-classic::before {
+ content: "\FB5E";
+}
+.mdi-controller-classic-outline::before {
+ content: "\FB5F";
+}
+.mdi-cookie::before {
+ content: "\F198";
+}
+.mdi-coolant-temperature::before {
+ content: "\F3C8";
+}
+.mdi-copyright::before {
+ content: "\F5E6";
+}
+.mdi-cordova::before {
+ content: "\F957";
+}
+.mdi-corn::before {
+ content: "\F7B7";
+}
+.mdi-counter::before {
+ content: "\F199";
+}
+.mdi-cow::before {
+ content: "\F19A";
+}
+.mdi-cowboy::before {
+ content: "\FEB8";
+}
+.mdi-cpu-32-bit::before {
+ content: "\FEFC";
+}
+.mdi-cpu-64-bit::before {
+ content: "\FEFD";
+}
+.mdi-crane::before {
+ content: "\F861";
+}
+.mdi-creation::before {
+ content: "\F1C9";
+}
+.mdi-creative-commons::before {
+ content: "\FD47";
+}
+.mdi-credit-card::before {
+ content: "\F0010";
+}
+.mdi-credit-card-clock::before {
+ content: "\FEFE";
+}
+.mdi-credit-card-clock-outline::before {
+ content: "\FFBC";
+}
+.mdi-credit-card-marker::before {
+ content: "\F6A7";
+}
+.mdi-credit-card-marker-outline::before {
+ content: "\FD9A";
+}
+.mdi-credit-card-minus::before {
+ content: "\FFCC";
+}
+.mdi-credit-card-minus-outline::before {
+ content: "\FFCD";
+}
+.mdi-credit-card-multiple::before {
+ content: "\F0011";
+}
+.mdi-credit-card-multiple-outline::before {
+ content: "\F19C";
+}
+.mdi-credit-card-off::before {
+ content: "\F0012";
+}
+.mdi-credit-card-off-outline::before {
+ content: "\F5E4";
+}
+.mdi-credit-card-outline::before {
+ content: "\F19B";
+}
+.mdi-credit-card-plus::before {
+ content: "\F0013";
+}
+.mdi-credit-card-plus-outline::before {
+ content: "\F675";
+}
+.mdi-credit-card-refund::before {
+ content: "\F0014";
+}
+.mdi-credit-card-refund-outline::before {
+ content: "\FAA7";
+}
+.mdi-credit-card-remove::before {
+ content: "\FFCE";
+}
+.mdi-credit-card-remove-outline::before {
+ content: "\FFCF";
+}
+.mdi-credit-card-scan::before {
+ content: "\F0015";
+}
+.mdi-credit-card-scan-outline::before {
+ content: "\F19D";
+}
+.mdi-credit-card-settings::before {
+ content: "\F0016";
+}
+.mdi-credit-card-settings-outline::before {
+ content: "\F8D6";
+}
+.mdi-credit-card-wireless::before {
+ content: "\F801";
+}
+.mdi-credit-card-wireless-outline::before {
+ content: "\FD48";
+}
+.mdi-cricket::before {
+ content: "\FD49";
+}
+.mdi-crop::before {
+ content: "\F19E";
+}
+.mdi-crop-free::before {
+ content: "\F19F";
+}
+.mdi-crop-landscape::before {
+ content: "\F1A0";
+}
+.mdi-crop-portrait::before {
+ content: "\F1A1";
+}
+.mdi-crop-rotate::before {
+ content: "\F695";
+}
+.mdi-crop-square::before {
+ content: "\F1A2";
+}
+.mdi-crosshairs::before {
+ content: "\F1A3";
+}
+.mdi-crosshairs-gps::before {
+ content: "\F1A4";
+}
+.mdi-crosshairs-off::before {
+ content: "\FF62";
+}
+.mdi-crosshairs-question::before {
+ content: "\F0161";
+}
+.mdi-crown::before {
+ content: "\F1A5";
+}
+.mdi-crown-outline::before {
+ content: "\F01FB";
+}
+.mdi-cryengine::before {
+ content: "\F958";
+}
+.mdi-crystal-ball::before {
+ content: "\FB14";
+}
+.mdi-cube::before {
+ content: "\F1A6";
+}
+.mdi-cube-outline::before {
+ content: "\F1A7";
+}
+.mdi-cube-scan::before {
+ content: "\FB60";
+}
+.mdi-cube-send::before {
+ content: "\F1A8";
+}
+.mdi-cube-unfolded::before {
+ content: "\F1A9";
+}
+.mdi-cup::before {
+ content: "\F1AA";
+}
+.mdi-cup-off::before {
+ content: "\F5E5";
+}
+.mdi-cup-off-outline::before {
+ content: "\F03A8";
+}
+.mdi-cup-outline::before {
+ content: "\F033A";
+}
+.mdi-cup-water::before {
+ content: "\F1AB";
+}
+.mdi-cupboard::before {
+ content: "\FF63";
+}
+.mdi-cupboard-outline::before {
+ content: "\FF64";
+}
+.mdi-cupcake::before {
+ content: "\F959";
+}
+.mdi-curling::before {
+ content: "\F862";
+}
+.mdi-currency-bdt::before {
+ content: "\F863";
+}
+.mdi-currency-brl::before {
+ content: "\FB61";
+}
+.mdi-currency-btc::before {
+ content: "\F1AC";
+}
+.mdi-currency-cny::before {
+ content: "\F7B9";
+}
+.mdi-currency-eth::before {
+ content: "\F7BA";
+}
+.mdi-currency-eur::before {
+ content: "\F1AD";
+}
+.mdi-currency-eur-off::before {
+ content: "\F0340";
+}
+.mdi-currency-gbp::before {
+ content: "\F1AE";
+}
+.mdi-currency-ils::before {
+ content: "\FC3D";
+}
+.mdi-currency-inr::before {
+ content: "\F1AF";
+}
+.mdi-currency-jpy::before {
+ content: "\F7BB";
+}
+.mdi-currency-krw::before {
+ content: "\F7BC";
+}
+.mdi-currency-kzt::before {
+ content: "\F864";
+}
+.mdi-currency-ngn::before {
+ content: "\F1B0";
+}
+.mdi-currency-php::before {
+ content: "\F9E5";
+}
+.mdi-currency-rial::before {
+ content: "\FEB9";
+}
+.mdi-currency-rub::before {
+ content: "\F1B1";
+}
+.mdi-currency-sign::before {
+ content: "\F7BD";
+}
+.mdi-currency-try::before {
+ content: "\F1B2";
+}
+.mdi-currency-twd::before {
+ content: "\F7BE";
+}
+.mdi-currency-usd::before {
+ content: "\F1B3";
+}
+.mdi-currency-usd-off::before {
+ content: "\F679";
+}
+.mdi-current-ac::before {
+ content: "\F95A";
+}
+.mdi-current-dc::before {
+ content: "\F95B";
+}
+.mdi-cursor-default::before {
+ content: "\F1B4";
+}
+.mdi-cursor-default-click::before {
+ content: "\FCD9";
+}
+.mdi-cursor-default-click-outline::before {
+ content: "\FCDA";
+}
+.mdi-cursor-default-gesture::before {
+ content: "\F0152";
+}
+.mdi-cursor-default-gesture-outline::before {
+ content: "\F0153";
+}
+.mdi-cursor-default-outline::before {
+ content: "\F1B5";
+}
+.mdi-cursor-move::before {
+ content: "\F1B6";
+}
+.mdi-cursor-pointer::before {
+ content: "\F1B7";
+}
+.mdi-cursor-text::before {
+ content: "\F5E7";
+}
+.mdi-database::before {
+ content: "\F1B8";
+}
+.mdi-database-check::before {
+ content: "\FAA8";
+}
+.mdi-database-edit::before {
+ content: "\FB62";
+}
+.mdi-database-export::before {
+ content: "\F95D";
+}
+.mdi-database-import::before {
+ content: "\F95C";
+}
+.mdi-database-lock::before {
+ content: "\FAA9";
+}
+.mdi-database-marker::before {
+ content: "\F0321";
+}
+.mdi-database-minus::before {
+ content: "\F1B9";
+}
+.mdi-database-plus::before {
+ content: "\F1BA";
+}
+.mdi-database-refresh::before {
+ content: "\FCDB";
+}
+.mdi-database-remove::before {
+ content: "\FCDC";
+}
+.mdi-database-search::before {
+ content: "\F865";
+}
+.mdi-database-settings::before {
+ content: "\FCDD";
+}
+.mdi-death-star::before {
+ content: "\F8D7";
+}
+.mdi-death-star-variant::before {
+ content: "\F8D8";
+}
+.mdi-deathly-hallows::before {
+ content: "\FB63";
+}
+.mdi-debian::before {
+ content: "\F8D9";
+}
+.mdi-debug-step-into::before {
+ content: "\F1BB";
+}
+.mdi-debug-step-out::before {
+ content: "\F1BC";
+}
+.mdi-debug-step-over::before {
+ content: "\F1BD";
+}
+.mdi-decagram::before {
+ content: "\F76B";
+}
+.mdi-decagram-outline::before {
+ content: "\F76C";
+}
+.mdi-decimal::before {
+ content: "\F00CC";
+}
+.mdi-decimal-comma::before {
+ content: "\F00CD";
+}
+.mdi-decimal-comma-decrease::before {
+ content: "\F00CE";
+}
+.mdi-decimal-comma-increase::before {
+ content: "\F00CF";
+}
+.mdi-decimal-decrease::before {
+ content: "\F1BE";
+}
+.mdi-decimal-increase::before {
+ content: "\F1BF";
+}
+.mdi-delete::before {
+ content: "\F1C0";
+}
+.mdi-delete-alert::before {
+ content: "\F00D0";
+}
+.mdi-delete-alert-outline::before {
+ content: "\F00D1";
+}
+.mdi-delete-circle::before {
+ content: "\F682";
+}
+.mdi-delete-circle-outline::before {
+ content: "\FB64";
+}
+.mdi-delete-empty::before {
+ content: "\F6CB";
+}
+.mdi-delete-empty-outline::before {
+ content: "\FEBA";
+}
+.mdi-delete-forever::before {
+ content: "\F5E8";
+}
+.mdi-delete-forever-outline::before {
+ content: "\FB65";
+}
+.mdi-delete-off::before {
+ content: "\F00D2";
+}
+.mdi-delete-off-outline::before {
+ content: "\F00D3";
+}
+.mdi-delete-outline::before {
+ content: "\F9E6";
+}
+.mdi-delete-restore::before {
+ content: "\F818";
+}
+.mdi-delete-sweep::before {
+ content: "\F5E9";
+}
+.mdi-delete-sweep-outline::before {
+ content: "\FC3E";
+}
+.mdi-delete-variant::before {
+ content: "\F1C1";
+}
+.mdi-delta::before {
+ content: "\F1C2";
+}
+.mdi-desk::before {
+ content: "\F0264";
+}
+.mdi-desk-lamp::before {
+ content: "\F95E";
+}
+.mdi-deskphone::before {
+ content: "\F1C3";
+}
+.mdi-desktop-classic::before {
+ content: "\F7BF";
+}
+.mdi-desktop-mac::before {
+ content: "\F1C4";
+}
+.mdi-desktop-mac-dashboard::before {
+ content: "\F9E7";
+}
+.mdi-desktop-tower::before {
+ content: "\F1C5";
+}
+.mdi-desktop-tower-monitor::before {
+ content: "\FAAA";
+}
+.mdi-details::before {
+ content: "\F1C6";
+}
+.mdi-dev-to::before {
+ content: "\FD4A";
+}
+.mdi-developer-board::before {
+ content: "\F696";
+}
+.mdi-deviantart::before {
+ content: "\F1C7";
+}
+.mdi-devices::before {
+ content: "\FFD0";
+}
+.mdi-diabetes::before {
+ content: "\F0151";
+}
+.mdi-dialpad::before {
+ content: "\F61C";
+}
+.mdi-diameter::before {
+ content: "\FC3F";
+}
+.mdi-diameter-outline::before {
+ content: "\FC40";
+}
+.mdi-diameter-variant::before {
+ content: "\FC41";
+}
+.mdi-diamond::before {
+ content: "\FB66";
+}
+.mdi-diamond-outline::before {
+ content: "\FB67";
+}
+.mdi-diamond-stone::before {
+ content: "\F1C8";
+}
+.mdi-dice-1::before {
+ content: "\F1CA";
+}
+.mdi-dice-1-outline::before {
+ content: "\F0175";
+}
+.mdi-dice-2::before {
+ content: "\F1CB";
+}
+.mdi-dice-2-outline::before {
+ content: "\F0176";
+}
+.mdi-dice-3::before {
+ content: "\F1CC";
+}
+.mdi-dice-3-outline::before {
+ content: "\F0177";
+}
+.mdi-dice-4::before {
+ content: "\F1CD";
+}
+.mdi-dice-4-outline::before {
+ content: "\F0178";
+}
+.mdi-dice-5::before {
+ content: "\F1CE";
+}
+.mdi-dice-5-outline::before {
+ content: "\F0179";
+}
+.mdi-dice-6::before {
+ content: "\F1CF";
+}
+.mdi-dice-6-outline::before {
+ content: "\F017A";
+}
+.mdi-dice-d10::before {
+ content: "\F017E";
+}
+.mdi-dice-d10-outline::before {
+ content: "\F76E";
+}
+.mdi-dice-d12::before {
+ content: "\F017F";
+}
+.mdi-dice-d12-outline::before {
+ content: "\F866";
+}
+.mdi-dice-d20::before {
+ content: "\F0180";
+}
+.mdi-dice-d20-outline::before {
+ content: "\F5EA";
+}
+.mdi-dice-d4::before {
+ content: "\F017B";
+}
+.mdi-dice-d4-outline::before {
+ content: "\F5EB";
+}
+.mdi-dice-d6::before {
+ content: "\F017C";
+}
+.mdi-dice-d6-outline::before {
+ content: "\F5EC";
+}
+.mdi-dice-d8::before {
+ content: "\F017D";
+}
+.mdi-dice-d8-outline::before {
+ content: "\F5ED";
+}
+.mdi-dice-multiple::before {
+ content: "\F76D";
+}
+.mdi-dice-multiple-outline::before {
+ content: "\F0181";
+}
+.mdi-dictionary::before {
+ content: "\F61D";
+}
+.mdi-digital-ocean::before {
+ content: "\F0262";
+}
+.mdi-dip-switch::before {
+ content: "\F7C0";
+}
+.mdi-directions::before {
+ content: "\F1D0";
+}
+.mdi-directions-fork::before {
+ content: "\F641";
+}
+.mdi-disc::before {
+ content: "\F5EE";
+}
+.mdi-disc-alert::before {
+ content: "\F1D1";
+}
+.mdi-disc-player::before {
+ content: "\F95F";
+}
+.mdi-discord::before {
+ content: "\F66F";
+}
+.mdi-dishwasher::before {
+ content: "\FAAB";
+}
+.mdi-dishwasher-alert::before {
+ content: "\F01E3";
+}
+.mdi-dishwasher-off::before {
+ content: "\F01E4";
+}
+.mdi-disqus::before {
+ content: "\F1D2";
+}
+.mdi-disqus-outline::before {
+ content: "\F1D3";
+}
+.mdi-distribute-horizontal-center::before {
+ content: "\F01F4";
+}
+.mdi-distribute-horizontal-left::before {
+ content: "\F01F3";
+}
+.mdi-distribute-horizontal-right::before {
+ content: "\F01F5";
+}
+.mdi-distribute-vertical-bottom::before {
+ content: "\F01F6";
+}
+.mdi-distribute-vertical-center::before {
+ content: "\F01F7";
+}
+.mdi-distribute-vertical-top::before {
+ content: "\F01F8";
+}
+.mdi-diving-flippers::before {
+ content: "\FD9B";
+}
+.mdi-diving-helmet::before {
+ content: "\FD9C";
+}
+.mdi-diving-scuba::before {
+ content: "\FD9D";
+}
+.mdi-diving-scuba-flag::before {
+ content: "\FD9E";
+}
+.mdi-diving-scuba-tank::before {
+ content: "\FD9F";
+}
+.mdi-diving-scuba-tank-multiple::before {
+ content: "\FDA0";
+}
+.mdi-diving-snorkel::before {
+ content: "\FDA1";
+}
+.mdi-division::before {
+ content: "\F1D4";
+}
+.mdi-division-box::before {
+ content: "\F1D5";
+}
+.mdi-dlna::before {
+ content: "\FA40";
+}
+.mdi-dna::before {
+ content: "\F683";
+}
+.mdi-dns::before {
+ content: "\F1D6";
+}
+.mdi-dns-outline::before {
+ content: "\FB68";
+}
+.mdi-do-not-disturb::before {
+ content: "\F697";
+}
+.mdi-do-not-disturb-off::before {
+ content: "\F698";
+}
+.mdi-dock-bottom::before {
+ content: "\F00D4";
+}
+.mdi-dock-left::before {
+ content: "\F00D5";
+}
+.mdi-dock-right::before {
+ content: "\F00D6";
+}
+.mdi-dock-window::before {
+ content: "\F00D7";
+}
+.mdi-docker::before {
+ content: "\F867";
+}
+.mdi-doctor::before {
+ content: "\FA41";
+}
+.mdi-dog::before {
+ content: "\FA42";
+}
+.mdi-dog-service::before {
+ content: "\FAAC";
+}
+.mdi-dog-side::before {
+ content: "\FA43";
+}
+.mdi-dolby::before {
+ content: "\F6B2";
+}
+.mdi-dolly::before {
+ content: "\FEBB";
+}
+.mdi-domain::before {
+ content: "\F1D7";
+}
+.mdi-domain-off::before {
+ content: "\FD4B";
+}
+.mdi-domain-plus::before {
+ content: "\F00D8";
+}
+.mdi-domain-remove::before {
+ content: "\F00D9";
+}
+.mdi-domino-mask::before {
+ content: "\F0045";
+}
+.mdi-donkey::before {
+ content: "\F7C1";
+}
+.mdi-door::before {
+ content: "\F819";
+}
+.mdi-door-closed::before {
+ content: "\F81A";
+}
+.mdi-door-closed-lock::before {
+ content: "\F00DA";
+}
+.mdi-door-open::before {
+ content: "\F81B";
+}
+.mdi-doorbell::before {
+ content: "\F0311";
+}
+.mdi-doorbell-video::before {
+ content: "\F868";
+}
+.mdi-dot-net::before {
+ content: "\FAAD";
+}
+.mdi-dots-horizontal::before {
+ content: "\F1D8";
+}
+.mdi-dots-horizontal-circle::before {
+ content: "\F7C2";
+}
+.mdi-dots-horizontal-circle-outline::before {
+ content: "\FB69";
+}
+.mdi-dots-vertical::before {
+ content: "\F1D9";
+}
+.mdi-dots-vertical-circle::before {
+ content: "\F7C3";
+}
+.mdi-dots-vertical-circle-outline::before {
+ content: "\FB6A";
+}
+.mdi-douban::before {
+ content: "\F699";
+}
+.mdi-download::before {
+ content: "\F1DA";
+}
+.mdi-download-lock::before {
+ content: "\F034B";
+}
+.mdi-download-lock-outline::before {
+ content: "\F034C";
+}
+.mdi-download-multiple::before {
+ content: "\F9E8";
+}
+.mdi-download-network::before {
+ content: "\F6F3";
+}
+.mdi-download-network-outline::before {
+ content: "\FC42";
+}
+.mdi-download-off::before {
+ content: "\F00DB";
+}
+.mdi-download-off-outline::before {
+ content: "\F00DC";
+}
+.mdi-download-outline::before {
+ content: "\FB6B";
+}
+.mdi-drag::before {
+ content: "\F1DB";
+}
+.mdi-drag-horizontal::before {
+ content: "\F1DC";
+}
+.mdi-drag-horizontal-variant::before {
+ content: "\F031B";
+}
+.mdi-drag-variant::before {
+ content: "\FB6C";
+}
+.mdi-drag-vertical::before {
+ content: "\F1DD";
+}
+.mdi-drag-vertical-variant::before {
+ content: "\F031C";
+}
+.mdi-drama-masks::before {
+ content: "\FCDE";
+}
+.mdi-draw::before {
+ content: "\FF66";
+}
+.mdi-drawing::before {
+ content: "\F1DE";
+}
+.mdi-drawing-box::before {
+ content: "\F1DF";
+}
+.mdi-dresser::before {
+ content: "\FF67";
+}
+.mdi-dresser-outline::before {
+ content: "\FF68";
+}
+.mdi-dribbble::before {
+ content: "\F1E0";
+}
+.mdi-dribbble-box::before {
+ content: "\F1E1";
+}
+.mdi-drone::before {
+ content: "\F1E2";
+}
+.mdi-dropbox::before {
+ content: "\F1E3";
+}
+.mdi-drupal::before {
+ content: "\F1E4";
+}
+.mdi-duck::before {
+ content: "\F1E5";
+}
+.mdi-dumbbell::before {
+ content: "\F1E6";
+}
+.mdi-dump-truck::before {
+ content: "\FC43";
+}
+.mdi-ear-hearing::before {
+ content: "\F7C4";
+}
+.mdi-ear-hearing-off::before {
+ content: "\FA44";
+}
+.mdi-earth::before {
+ content: "\F1E7";
+}
+.mdi-earth-arrow-right::before {
+ content: "\F033C";
+}
+.mdi-earth-box::before {
+ content: "\F6CC";
+}
+.mdi-earth-box-off::before {
+ content: "\F6CD";
+}
+.mdi-earth-off::before {
+ content: "\F1E8";
+}
+.mdi-edge::before {
+ content: "\F1E9";
+}
+.mdi-edge-legacy::before {
+ content: "\F027B";
+}
+.mdi-egg::before {
+ content: "\FAAE";
+}
+.mdi-egg-easter::before {
+ content: "\FAAF";
+}
+.mdi-eight-track::before {
+ content: "\F9E9";
+}
+.mdi-eject::before {
+ content: "\F1EA";
+}
+.mdi-eject-outline::before {
+ content: "\FB6D";
+}
+.mdi-electric-switch::before {
+ content: "\FEBC";
+}
+.mdi-electric-switch-closed::before {
+ content: "\F0104";
+}
+.mdi-electron-framework::before {
+ content: "\F0046";
+}
+.mdi-elephant::before {
+ content: "\F7C5";
+}
+.mdi-elevation-decline::before {
+ content: "\F1EB";
+}
+.mdi-elevation-rise::before {
+ content: "\F1EC";
+}
+.mdi-elevator::before {
+ content: "\F1ED";
+}
+.mdi-elevator-down::before {
+ content: "\F02ED";
+}
+.mdi-elevator-passenger::before {
+ content: "\F03AC";
+}
+.mdi-elevator-up::before {
+ content: "\F02EC";
+}
+.mdi-ellipse::before {
+ content: "\FEBD";
+}
+.mdi-ellipse-outline::before {
+ content: "\FEBE";
+}
+.mdi-email::before {
+ content: "\F1EE";
+}
+.mdi-email-alert::before {
+ content: "\F6CE";
+}
+.mdi-email-alert-outline::before {
+ content: "\FD1E";
+}
+.mdi-email-box::before {
+ content: "\FCDF";
+}
+.mdi-email-check::before {
+ content: "\FAB0";
+}
+.mdi-email-check-outline::before {
+ content: "\FAB1";
+}
+.mdi-email-edit::before {
+ content: "\FF00";
+}
+.mdi-email-edit-outline::before {
+ content: "\FF01";
+}
+.mdi-email-lock::before {
+ content: "\F1F1";
+}
+.mdi-email-mark-as-unread::before {
+ content: "\FB6E";
+}
+.mdi-email-minus::before {
+ content: "\FF02";
+}
+.mdi-email-minus-outline::before {
+ content: "\FF03";
+}
+.mdi-email-multiple::before {
+ content: "\FF04";
+}
+.mdi-email-multiple-outline::before {
+ content: "\FF05";
+}
+.mdi-email-newsletter::before {
+ content: "\FFD1";
+}
+.mdi-email-open::before {
+ content: "\F1EF";
+}
+.mdi-email-open-multiple::before {
+ content: "\FF06";
+}
+.mdi-email-open-multiple-outline::before {
+ content: "\FF07";
+}
+.mdi-email-open-outline::before {
+ content: "\F5EF";
+}
+.mdi-email-outline::before {
+ content: "\F1F0";
+}
+.mdi-email-plus::before {
+ content: "\F9EA";
+}
+.mdi-email-plus-outline::before {
+ content: "\F9EB";
+}
+.mdi-email-receive::before {
+ content: "\F0105";
+}
+.mdi-email-receive-outline::before {
+ content: "\F0106";
+}
+.mdi-email-search::before {
+ content: "\F960";
+}
+.mdi-email-search-outline::before {
+ content: "\F961";
+}
+.mdi-email-send::before {
+ content: "\F0107";
+}
+.mdi-email-send-outline::before {
+ content: "\F0108";
+}
+.mdi-email-sync::before {
+ content: "\F02F2";
+}
+.mdi-email-sync-outline::before {
+ content: "\F02F3";
+}
+.mdi-email-variant::before {
+ content: "\F5F0";
+}
+.mdi-ember::before {
+ content: "\FB15";
+}
+.mdi-emby::before {
+ content: "\F6B3";
+}
+.mdi-emoticon::before {
+ content: "\FC44";
+}
+.mdi-emoticon-angry::before {
+ content: "\FC45";
+}
+.mdi-emoticon-angry-outline::before {
+ content: "\FC46";
+}
+.mdi-emoticon-confused::before {
+ content: "\F0109";
+}
+.mdi-emoticon-confused-outline::before {
+ content: "\F010A";
+}
+.mdi-emoticon-cool::before {
+ content: "\FC47";
+}
+.mdi-emoticon-cool-outline::before {
+ content: "\F1F3";
+}
+.mdi-emoticon-cry::before {
+ content: "\FC48";
+}
+.mdi-emoticon-cry-outline::before {
+ content: "\FC49";
+}
+.mdi-emoticon-dead::before {
+ content: "\FC4A";
+}
+.mdi-emoticon-dead-outline::before {
+ content: "\F69A";
+}
+.mdi-emoticon-devil::before {
+ content: "\FC4B";
+}
+.mdi-emoticon-devil-outline::before {
+ content: "\F1F4";
+}
+.mdi-emoticon-excited::before {
+ content: "\FC4C";
+}
+.mdi-emoticon-excited-outline::before {
+ content: "\F69B";
+}
+.mdi-emoticon-frown::before {
+ content: "\FF69";
+}
+.mdi-emoticon-frown-outline::before {
+ content: "\FF6A";
+}
+.mdi-emoticon-happy::before {
+ content: "\FC4D";
+}
+.mdi-emoticon-happy-outline::before {
+ content: "\F1F5";
+}
+.mdi-emoticon-kiss::before {
+ content: "\FC4E";
+}
+.mdi-emoticon-kiss-outline::before {
+ content: "\FC4F";
+}
+.mdi-emoticon-lol::before {
+ content: "\F023F";
+}
+.mdi-emoticon-lol-outline::before {
+ content: "\F0240";
+}
+.mdi-emoticon-neutral::before {
+ content: "\FC50";
+}
+.mdi-emoticon-neutral-outline::before {
+ content: "\F1F6";
+}
+.mdi-emoticon-outline::before {
+ content: "\F1F2";
+}
+.mdi-emoticon-poop::before {
+ content: "\F1F7";
+}
+.mdi-emoticon-poop-outline::before {
+ content: "\FC51";
+}
+.mdi-emoticon-sad::before {
+ content: "\FC52";
+}
+.mdi-emoticon-sad-outline::before {
+ content: "\F1F8";
+}
+.mdi-emoticon-tongue::before {
+ content: "\F1F9";
+}
+.mdi-emoticon-tongue-outline::before {
+ content: "\FC53";
+}
+.mdi-emoticon-wink::before {
+ content: "\FC54";
+}
+.mdi-emoticon-wink-outline::before {
+ content: "\FC55";
+}
+.mdi-engine::before {
+ content: "\F1FA";
+}
+.mdi-engine-off::before {
+ content: "\FA45";
+}
+.mdi-engine-off-outline::before {
+ content: "\FA46";
+}
+.mdi-engine-outline::before {
+ content: "\F1FB";
+}
+.mdi-epsilon::before {
+ content: "\F010B";
+}
+.mdi-equal::before {
+ content: "\F1FC";
+}
+.mdi-equal-box::before {
+ content: "\F1FD";
+}
+.mdi-equalizer::before {
+ content: "\FEBF";
+}
+.mdi-equalizer-outline::before {
+ content: "\FEC0";
+}
+.mdi-eraser::before {
+ content: "\F1FE";
+}
+.mdi-eraser-variant::before {
+ content: "\F642";
+}
+.mdi-escalator::before {
+ content: "\F1FF";
+}
+.mdi-escalator-down::before {
+ content: "\F02EB";
+}
+.mdi-escalator-up::before {
+ content: "\F02EA";
+}
+.mdi-eslint::before {
+ content: "\FC56";
+}
+.mdi-et::before {
+ content: "\FAB2";
+}
+.mdi-ethereum::before {
+ content: "\F869";
+}
+.mdi-ethernet::before {
+ content: "\F200";
+}
+.mdi-ethernet-cable::before {
+ content: "\F201";
+}
+.mdi-ethernet-cable-off::before {
+ content: "\F202";
+}
+.mdi-etsy::before {
+ content: "\F203";
+}
+.mdi-ev-station::before {
+ content: "\F5F1";
+}
+.mdi-eventbrite::before {
+ content: "\F7C6";
+}
+.mdi-evernote::before {
+ content: "\F204";
+}
+.mdi-excavator::before {
+ content: "\F0047";
+}
+.mdi-exclamation::before {
+ content: "\F205";
+}
+.mdi-exclamation-thick::before {
+ content: "\F0263";
+}
+.mdi-exit-run::before {
+ content: "\FA47";
+}
+.mdi-exit-to-app::before {
+ content: "\F206";
+}
+.mdi-expand-all::before {
+ content: "\FAB3";
+}
+.mdi-expand-all-outline::before {
+ content: "\FAB4";
+}
+.mdi-expansion-card::before {
+ content: "\F8AD";
+}
+.mdi-expansion-card-variant::before {
+ content: "\FFD2";
+}
+.mdi-exponent::before {
+ content: "\F962";
+}
+.mdi-exponent-box::before {
+ content: "\F963";
+}
+.mdi-export::before {
+ content: "\F207";
+}
+.mdi-export-variant::before {
+ content: "\FB6F";
+}
+.mdi-eye::before {
+ content: "\F208";
+}
+.mdi-eye-check::before {
+ content: "\FCE0";
+}
+.mdi-eye-check-outline::before {
+ content: "\FCE1";
+}
+.mdi-eye-circle::before {
+ content: "\FB70";
+}
+.mdi-eye-circle-outline::before {
+ content: "\FB71";
+}
+.mdi-eye-minus::before {
+ content: "\F0048";
+}
+.mdi-eye-minus-outline::before {
+ content: "\F0049";
+}
+.mdi-eye-off::before {
+ content: "\F209";
+}
+.mdi-eye-off-outline::before {
+ content: "\F6D0";
+}
+.mdi-eye-outline::before {
+ content: "\F6CF";
+}
+.mdi-eye-plus::before {
+ content: "\F86A";
+}
+.mdi-eye-plus-outline::before {
+ content: "\F86B";
+}
+.mdi-eye-settings::before {
+ content: "\F86C";
+}
+.mdi-eye-settings-outline::before {
+ content: "\F86D";
+}
+.mdi-eyedropper::before {
+ content: "\F20A";
+}
+.mdi-eyedropper-variant::before {
+ content: "\F20B";
+}
+.mdi-face::before {
+ content: "\F643";
+}
+.mdi-face-agent::before {
+ content: "\FD4C";
+}
+.mdi-face-outline::before {
+ content: "\FB72";
+}
+.mdi-face-profile::before {
+ content: "\F644";
+}
+.mdi-face-profile-woman::before {
+ content: "\F00A1";
+}
+.mdi-face-recognition::before {
+ content: "\FC57";
+}
+.mdi-face-woman::before {
+ content: "\F00A2";
+}
+.mdi-face-woman-outline::before {
+ content: "\F00A3";
+}
+.mdi-facebook::before {
+ content: "\F20C";
+}
+.mdi-facebook-box::before {
+ content: "\F20D";
+}
+.mdi-facebook-messenger::before {
+ content: "\F20E";
+}
+.mdi-facebook-workplace::before {
+ content: "\FB16";
+}
+.mdi-factory::before {
+ content: "\F20F";
+}
+.mdi-fan::before {
+ content: "\F210";
+}
+.mdi-fan-off::before {
+ content: "\F81C";
+}
+.mdi-fast-forward::before {
+ content: "\F211";
+}
+.mdi-fast-forward-10::before {
+ content: "\FD4D";
+}
+.mdi-fast-forward-30::before {
+ content: "\FCE2";
+}
+.mdi-fast-forward-5::before {
+ content: "\F0223";
+}
+.mdi-fast-forward-outline::before {
+ content: "\F6D1";
+}
+.mdi-fax::before {
+ content: "\F212";
+}
+.mdi-feather::before {
+ content: "\F6D2";
+}
+.mdi-feature-search::before {
+ content: "\FA48";
+}
+.mdi-feature-search-outline::before {
+ content: "\FA49";
+}
+.mdi-fedora::before {
+ content: "\F8DA";
+}
+.mdi-ferris-wheel::before {
+ content: "\FEC1";
+}
+.mdi-ferry::before {
+ content: "\F213";
+}
+.mdi-file::before {
+ content: "\F214";
+}
+.mdi-file-account::before {
+ content: "\F73A";
+}
+.mdi-file-account-outline::before {
+ content: "\F004A";
+}
+.mdi-file-alert::before {
+ content: "\FA4A";
+}
+.mdi-file-alert-outline::before {
+ content: "\FA4B";
+}
+.mdi-file-cabinet::before {
+ content: "\FAB5";
+}
+.mdi-file-cad::before {
+ content: "\FF08";
+}
+.mdi-file-cad-box::before {
+ content: "\FF09";
+}
+.mdi-file-cancel::before {
+ content: "\FDA2";
+}
+.mdi-file-cancel-outline::before {
+ content: "\FDA3";
+}
+.mdi-file-certificate::before {
+ content: "\F01B1";
+}
+.mdi-file-certificate-outline::before {
+ content: "\F01B2";
+}
+.mdi-file-chart::before {
+ content: "\F215";
+}
+.mdi-file-chart-outline::before {
+ content: "\F004B";
+}
+.mdi-file-check::before {
+ content: "\F216";
+}
+.mdi-file-check-outline::before {
+ content: "\FE7B";
+}
+.mdi-file-clock::before {
+ content: "\F030C";
+}
+.mdi-file-clock-outline::before {
+ content: "\F030D";
+}
+.mdi-file-cloud::before {
+ content: "\F217";
+}
+.mdi-file-cloud-outline::before {
+ content: "\F004C";
+}
+.mdi-file-code::before {
+ content: "\F22E";
+}
+.mdi-file-code-outline::before {
+ content: "\F004D";
+}
+.mdi-file-compare::before {
+ content: "\F8A9";
+}
+.mdi-file-delimited::before {
+ content: "\F218";
+}
+.mdi-file-delimited-outline::before {
+ content: "\FEC2";
+}
+.mdi-file-document::before {
+ content: "\F219";
+}
+.mdi-file-document-box::before {
+ content: "\F21A";
+}
+.mdi-file-document-box-check::before {
+ content: "\FEC3";
+}
+.mdi-file-document-box-check-outline::before {
+ content: "\FEC4";
+}
+.mdi-file-document-box-minus::before {
+ content: "\FEC5";
+}
+.mdi-file-document-box-minus-outline::before {
+ content: "\FEC6";
+}
+.mdi-file-document-box-multiple::before {
+ content: "\FAB6";
+}
+.mdi-file-document-box-multiple-outline::before {
+ content: "\FAB7";
+}
+.mdi-file-document-box-outline::before {
+ content: "\F9EC";
+}
+.mdi-file-document-box-plus::before {
+ content: "\FEC7";
+}
+.mdi-file-document-box-plus-outline::before {
+ content: "\FEC8";
+}
+.mdi-file-document-box-remove::before {
+ content: "\FEC9";
+}
+.mdi-file-document-box-remove-outline::before {
+ content: "\FECA";
+}
+.mdi-file-document-box-search::before {
+ content: "\FECB";
+}
+.mdi-file-document-box-search-outline::before {
+ content: "\FECC";
+}
+.mdi-file-document-edit::before {
+ content: "\FDA4";
+}
+.mdi-file-document-edit-outline::before {
+ content: "\FDA5";
+}
+.mdi-file-document-outline::before {
+ content: "\F9ED";
+}
+.mdi-file-download::before {
+ content: "\F964";
+}
+.mdi-file-download-outline::before {
+ content: "\F965";
+}
+.mdi-file-edit::before {
+ content: "\F0212";
+}
+.mdi-file-edit-outline::before {
+ content: "\F0213";
+}
+.mdi-file-excel::before {
+ content: "\F21B";
+}
+.mdi-file-excel-box::before {
+ content: "\F21C";
+}
+.mdi-file-excel-box-outline::before {
+ content: "\F004E";
+}
+.mdi-file-excel-outline::before {
+ content: "\F004F";
+}
+.mdi-file-export::before {
+ content: "\F21D";
+}
+.mdi-file-export-outline::before {
+ content: "\F0050";
+}
+.mdi-file-eye::before {
+ content: "\FDA6";
+}
+.mdi-file-eye-outline::before {
+ content: "\FDA7";
+}
+.mdi-file-find::before {
+ content: "\F21E";
+}
+.mdi-file-find-outline::before {
+ content: "\FB73";
+}
+.mdi-file-hidden::before {
+ content: "\F613";
+}
+.mdi-file-image::before {
+ content: "\F21F";
+}
+.mdi-file-image-outline::before {
+ content: "\FECD";
+}
+.mdi-file-import::before {
+ content: "\F220";
+}
+.mdi-file-import-outline::before {
+ content: "\F0051";
+}
+.mdi-file-key::before {
+ content: "\F01AF";
+}
+.mdi-file-key-outline::before {
+ content: "\F01B0";
+}
+.mdi-file-link::before {
+ content: "\F01A2";
+}
+.mdi-file-link-outline::before {
+ content: "\F01A3";
+}
+.mdi-file-lock::before {
+ content: "\F221";
+}
+.mdi-file-lock-outline::before {
+ content: "\F0052";
+}
+.mdi-file-move::before {
+ content: "\FAB8";
+}
+.mdi-file-move-outline::before {
+ content: "\F0053";
+}
+.mdi-file-multiple::before {
+ content: "\F222";
+}
+.mdi-file-multiple-outline::before {
+ content: "\F0054";
+}
+.mdi-file-music::before {
+ content: "\F223";
+}
+.mdi-file-music-outline::before {
+ content: "\FE7C";
+}
+.mdi-file-outline::before {
+ content: "\F224";
+}
+.mdi-file-pdf::before {
+ content: "\F225";
+}
+.mdi-file-pdf-box::before {
+ content: "\F226";
+}
+.mdi-file-pdf-box-outline::before {
+ content: "\FFD3";
+}
+.mdi-file-pdf-outline::before {
+ content: "\FE7D";
+}
+.mdi-file-percent::before {
+ content: "\F81D";
+}
+.mdi-file-percent-outline::before {
+ content: "\F0055";
+}
+.mdi-file-phone::before {
+ content: "\F01A4";
+}
+.mdi-file-phone-outline::before {
+ content: "\F01A5";
+}
+.mdi-file-plus::before {
+ content: "\F751";
+}
+.mdi-file-plus-outline::before {
+ content: "\FF0A";
+}
+.mdi-file-powerpoint::before {
+ content: "\F227";
+}
+.mdi-file-powerpoint-box::before {
+ content: "\F228";
+}
+.mdi-file-powerpoint-box-outline::before {
+ content: "\F0056";
+}
+.mdi-file-powerpoint-outline::before {
+ content: "\F0057";
+}
+.mdi-file-presentation-box::before {
+ content: "\F229";
+}
+.mdi-file-question::before {
+ content: "\F86E";
+}
+.mdi-file-question-outline::before {
+ content: "\F0058";
+}
+.mdi-file-remove::before {
+ content: "\FB74";
+}
+.mdi-file-remove-outline::before {
+ content: "\F0059";
+}
+.mdi-file-replace::before {
+ content: "\FB17";
+}
+.mdi-file-replace-outline::before {
+ content: "\FB18";
+}
+.mdi-file-restore::before {
+ content: "\F670";
+}
+.mdi-file-restore-outline::before {
+ content: "\F005A";
+}
+.mdi-file-search::before {
+ content: "\FC58";
+}
+.mdi-file-search-outline::before {
+ content: "\FC59";
+}
+.mdi-file-send::before {
+ content: "\F22A";
+}
+.mdi-file-send-outline::before {
+ content: "\F005B";
+}
+.mdi-file-settings::before {
+ content: "\F00A4";
+}
+.mdi-file-settings-outline::before {
+ content: "\F00A5";
+}
+.mdi-file-settings-variant::before {
+ content: "\F00A6";
+}
+.mdi-file-settings-variant-outline::before {
+ content: "\F00A7";
+}
+.mdi-file-star::before {
+ content: "\F005C";
+}
+.mdi-file-star-outline::before {
+ content: "\F005D";
+}
+.mdi-file-swap::before {
+ content: "\FFD4";
+}
+.mdi-file-swap-outline::before {
+ content: "\FFD5";
+}
+.mdi-file-sync::before {
+ content: "\F0241";
+}
+.mdi-file-sync-outline::before {
+ content: "\F0242";
+}
+.mdi-file-table::before {
+ content: "\FC5A";
+}
+.mdi-file-table-box::before {
+ content: "\F010C";
+}
+.mdi-file-table-box-multiple::before {
+ content: "\F010D";
+}
+.mdi-file-table-box-multiple-outline::before {
+ content: "\F010E";
+}
+.mdi-file-table-box-outline::before {
+ content: "\F010F";
+}
+.mdi-file-table-outline::before {
+ content: "\FC5B";
+}
+.mdi-file-tree::before {
+ content: "\F645";
+}
+.mdi-file-undo::before {
+ content: "\F8DB";
+}
+.mdi-file-undo-outline::before {
+ content: "\F005E";
+}
+.mdi-file-upload::before {
+ content: "\FA4C";
+}
+.mdi-file-upload-outline::before {
+ content: "\FA4D";
+}
+.mdi-file-video::before {
+ content: "\F22B";
+}
+.mdi-file-video-outline::before {
+ content: "\FE10";
+}
+.mdi-file-word::before {
+ content: "\F22C";
+}
+.mdi-file-word-box::before {
+ content: "\F22D";
+}
+.mdi-file-word-box-outline::before {
+ content: "\F005F";
+}
+.mdi-file-word-outline::before {
+ content: "\F0060";
+}
+.mdi-film::before {
+ content: "\F22F";
+}
+.mdi-filmstrip::before {
+ content: "\F230";
+}
+.mdi-filmstrip-off::before {
+ content: "\F231";
+}
+.mdi-filter::before {
+ content: "\F232";
+}
+.mdi-filter-menu::before {
+ content: "\F0110";
+}
+.mdi-filter-menu-outline::before {
+ content: "\F0111";
+}
+.mdi-filter-minus::before {
+ content: "\FF0B";
+}
+.mdi-filter-minus-outline::before {
+ content: "\FF0C";
+}
+.mdi-filter-outline::before {
+ content: "\F233";
+}
+.mdi-filter-plus::before {
+ content: "\FF0D";
+}
+.mdi-filter-plus-outline::before {
+ content: "\FF0E";
+}
+.mdi-filter-remove::before {
+ content: "\F234";
+}
+.mdi-filter-remove-outline::before {
+ content: "\F235";
+}
+.mdi-filter-variant::before {
+ content: "\F236";
+}
+.mdi-filter-variant-minus::before {
+ content: "\F013D";
+}
+.mdi-filter-variant-plus::before {
+ content: "\F013E";
+}
+.mdi-filter-variant-remove::before {
+ content: "\F0061";
+}
+.mdi-finance::before {
+ content: "\F81E";
+}
+.mdi-find-replace::before {
+ content: "\F6D3";
+}
+.mdi-fingerprint::before {
+ content: "\F237";
+}
+.mdi-fingerprint-off::before {
+ content: "\FECE";
+}
+.mdi-fire::before {
+ content: "\F238";
+}
+.mdi-fire-extinguisher::before {
+ content: "\FF0F";
+}
+.mdi-fire-hydrant::before {
+ content: "\F0162";
+}
+.mdi-fire-hydrant-alert::before {
+ content: "\F0163";
+}
+.mdi-fire-hydrant-off::before {
+ content: "\F0164";
+}
+.mdi-fire-truck::before {
+ content: "\F8AA";
+}
+.mdi-firebase::before {
+ content: "\F966";
+}
+.mdi-firefox::before {
+ content: "\F239";
+}
+.mdi-fireplace::before {
+ content: "\FE11";
+}
+.mdi-fireplace-off::before {
+ content: "\FE12";
+}
+.mdi-firework::before {
+ content: "\FE13";
+}
+.mdi-fish::before {
+ content: "\F23A";
+}
+.mdi-fishbowl::before {
+ content: "\FF10";
+}
+.mdi-fishbowl-outline::before {
+ content: "\FF11";
+}
+.mdi-fit-to-page::before {
+ content: "\FF12";
+}
+.mdi-fit-to-page-outline::before {
+ content: "\FF13";
+}
+.mdi-flag::before {
+ content: "\F23B";
+}
+.mdi-flag-checkered::before {
+ content: "\F23C";
+}
+.mdi-flag-minus::before {
+ content: "\FB75";
+}
+.mdi-flag-minus-outline::before {
+ content: "\F00DD";
+}
+.mdi-flag-outline::before {
+ content: "\F23D";
+}
+.mdi-flag-plus::before {
+ content: "\FB76";
+}
+.mdi-flag-plus-outline::before {
+ content: "\F00DE";
+}
+.mdi-flag-remove::before {
+ content: "\FB77";
+}
+.mdi-flag-remove-outline::before {
+ content: "\F00DF";
+}
+.mdi-flag-triangle::before {
+ content: "\F23F";
+}
+.mdi-flag-variant::before {
+ content: "\F240";
+}
+.mdi-flag-variant-outline::before {
+ content: "\F23E";
+}
+.mdi-flare::before {
+ content: "\FD4E";
+}
+.mdi-flash::before {
+ content: "\F241";
+}
+.mdi-flash-alert::before {
+ content: "\FF14";
+}
+.mdi-flash-alert-outline::before {
+ content: "\FF15";
+}
+.mdi-flash-auto::before {
+ content: "\F242";
+}
+.mdi-flash-circle::before {
+ content: "\F81F";
+}
+.mdi-flash-off::before {
+ content: "\F243";
+}
+.mdi-flash-outline::before {
+ content: "\F6D4";
+}
+.mdi-flash-red-eye::before {
+ content: "\F67A";
+}
+.mdi-flashlight::before {
+ content: "\F244";
+}
+.mdi-flashlight-off::before {
+ content: "\F245";
+}
+.mdi-flask::before {
+ content: "\F093";
+}
+.mdi-flask-empty::before {
+ content: "\F094";
+}
+.mdi-flask-empty-minus::before {
+ content: "\F0265";
+}
+.mdi-flask-empty-minus-outline::before {
+ content: "\F0266";
+}
+.mdi-flask-empty-outline::before {
+ content: "\F095";
+}
+.mdi-flask-empty-plus::before {
+ content: "\F0267";
+}
+.mdi-flask-empty-plus-outline::before {
+ content: "\F0268";
+}
+.mdi-flask-empty-remove::before {
+ content: "\F0269";
+}
+.mdi-flask-empty-remove-outline::before {
+ content: "\F026A";
+}
+.mdi-flask-minus::before {
+ content: "\F026B";
+}
+.mdi-flask-minus-outline::before {
+ content: "\F026C";
+}
+.mdi-flask-outline::before {
+ content: "\F096";
+}
+.mdi-flask-plus::before {
+ content: "\F026D";
+}
+.mdi-flask-plus-outline::before {
+ content: "\F026E";
+}
+.mdi-flask-remove::before {
+ content: "\F026F";
+}
+.mdi-flask-remove-outline::before {
+ content: "\F0270";
+}
+.mdi-flask-round-bottom::before {
+ content: "\F0276";
+}
+.mdi-flask-round-bottom-empty::before {
+ content: "\F0277";
+}
+.mdi-flask-round-bottom-empty-outline::before {
+ content: "\F0278";
+}
+.mdi-flask-round-bottom-outline::before {
+ content: "\F0279";
+}
+.mdi-flattr::before {
+ content: "\F246";
+}
+.mdi-fleur-de-lis::before {
+ content: "\F032E";
+}
+.mdi-flickr::before {
+ content: "\FCE3";
+}
+.mdi-flip-horizontal::before {
+ content: "\F0112";
+}
+.mdi-flip-to-back::before {
+ content: "\F247";
+}
+.mdi-flip-to-front::before {
+ content: "\F248";
+}
+.mdi-flip-vertical::before {
+ content: "\F0113";
+}
+.mdi-floor-lamp::before {
+ content: "\F8DC";
+}
+.mdi-floor-lamp-dual::before {
+ content: "\F0062";
+}
+.mdi-floor-lamp-variant::before {
+ content: "\F0063";
+}
+.mdi-floor-plan::before {
+ content: "\F820";
+}
+.mdi-floppy::before {
+ content: "\F249";
+}
+.mdi-floppy-variant::before {
+ content: "\F9EE";
+}
+.mdi-flower::before {
+ content: "\F24A";
+}
+.mdi-flower-outline::before {
+ content: "\F9EF";
+}
+.mdi-flower-poppy::before {
+ content: "\FCE4";
+}
+.mdi-flower-tulip::before {
+ content: "\F9F0";
+}
+.mdi-flower-tulip-outline::before {
+ content: "\F9F1";
+}
+.mdi-focus-auto::before {
+ content: "\FF6B";
+}
+.mdi-focus-field::before {
+ content: "\FF6C";
+}
+.mdi-focus-field-horizontal::before {
+ content: "\FF6D";
+}
+.mdi-focus-field-vertical::before {
+ content: "\FF6E";
+}
+.mdi-folder::before {
+ content: "\F24B";
+}
+.mdi-folder-account::before {
+ content: "\F24C";
+}
+.mdi-folder-account-outline::before {
+ content: "\FB78";
+}
+.mdi-folder-alert::before {
+ content: "\FDA8";
+}
+.mdi-folder-alert-outline::before {
+ content: "\FDA9";
+}
+.mdi-folder-clock::before {
+ content: "\FAB9";
+}
+.mdi-folder-clock-outline::before {
+ content: "\FABA";
+}
+.mdi-folder-download::before {
+ content: "\F24D";
+}
+.mdi-folder-download-outline::before {
+ content: "\F0114";
+}
+.mdi-folder-edit::before {
+ content: "\F8DD";
+}
+.mdi-folder-edit-outline::before {
+ content: "\FDAA";
+}
+.mdi-folder-google-drive::before {
+ content: "\F24E";
+}
+.mdi-folder-heart::before {
+ content: "\F0115";
+}
+.mdi-folder-heart-outline::before {
+ content: "\F0116";
+}
+.mdi-folder-home::before {
+ content: "\F00E0";
+}
+.mdi-folder-home-outline::before {
+ content: "\F00E1";
+}
+.mdi-folder-image::before {
+ content: "\F24F";
+}
+.mdi-folder-information::before {
+ content: "\F00E2";
+}
+.mdi-folder-information-outline::before {
+ content: "\F00E3";
+}
+.mdi-folder-key::before {
+ content: "\F8AB";
+}
+.mdi-folder-key-network::before {
+ content: "\F8AC";
+}
+.mdi-folder-key-network-outline::before {
+ content: "\FC5C";
+}
+.mdi-folder-key-outline::before {
+ content: "\F0117";
+}
+.mdi-folder-lock::before {
+ content: "\F250";
+}
+.mdi-folder-lock-open::before {
+ content: "\F251";
+}
+.mdi-folder-marker::before {
+ content: "\F0298";
+}
+.mdi-folder-marker-outline::before {
+ content: "\F0299";
+}
+.mdi-folder-move::before {
+ content: "\F252";
+}
+.mdi-folder-move-outline::before {
+ content: "\F0271";
+}
+.mdi-folder-multiple::before {
+ content: "\F253";
+}
+.mdi-folder-multiple-image::before {
+ content: "\F254";
+}
+.mdi-folder-multiple-outline::before {
+ content: "\F255";
+}
+.mdi-folder-music::before {
+ content: "\F0384";
+}
+.mdi-folder-music-outline::before {
+ content: "\F0385";
+}
+.mdi-folder-network::before {
+ content: "\F86F";
+}
+.mdi-folder-network-outline::before {
+ content: "\FC5D";
+}
+.mdi-folder-open::before {
+ content: "\F76F";
+}
+.mdi-folder-open-outline::before {
+ content: "\FDAB";
+}
+.mdi-folder-outline::before {
+ content: "\F256";
+}
+.mdi-folder-plus::before {
+ content: "\F257";
+}
+.mdi-folder-plus-outline::before {
+ content: "\FB79";
+}
+.mdi-folder-pound::before {
+ content: "\FCE5";
+}
+.mdi-folder-pound-outline::before {
+ content: "\FCE6";
+}
+.mdi-folder-remove::before {
+ content: "\F258";
+}
+.mdi-folder-remove-outline::before {
+ content: "\FB7A";
+}
+.mdi-folder-search::before {
+ content: "\F967";
+}
+.mdi-folder-search-outline::before {
+ content: "\F968";
+}
+.mdi-folder-settings::before {
+ content: "\F00A8";
+}
+.mdi-folder-settings-outline::before {
+ content: "\F00A9";
+}
+.mdi-folder-settings-variant::before {
+ content: "\F00AA";
+}
+.mdi-folder-settings-variant-outline::before {
+ content: "\F00AB";
+}
+.mdi-folder-star::before {
+ content: "\F69C";
+}
+.mdi-folder-star-outline::before {
+ content: "\FB7B";
+}
+.mdi-folder-swap::before {
+ content: "\FFD6";
+}
+.mdi-folder-swap-outline::before {
+ content: "\FFD7";
+}
+.mdi-folder-sync::before {
+ content: "\FCE7";
+}
+.mdi-folder-sync-outline::before {
+ content: "\FCE8";
+}
+.mdi-folder-table::before {
+ content: "\F030E";
+}
+.mdi-folder-table-outline::before {
+ content: "\F030F";
+}
+.mdi-folder-text::before {
+ content: "\FC5E";
+}
+.mdi-folder-text-outline::before {
+ content: "\FC5F";
+}
+.mdi-folder-upload::before {
+ content: "\F259";
+}
+.mdi-folder-upload-outline::before {
+ content: "\F0118";
+}
+.mdi-folder-zip::before {
+ content: "\F6EA";
+}
+.mdi-folder-zip-outline::before {
+ content: "\F7B8";
+}
+.mdi-font-awesome::before {
+ content: "\F03A";
+}
+.mdi-food::before {
+ content: "\F25A";
+}
+.mdi-food-apple::before {
+ content: "\F25B";
+}
+.mdi-food-apple-outline::before {
+ content: "\FC60";
+}
+.mdi-food-croissant::before {
+ content: "\F7C7";
+}
+.mdi-food-fork-drink::before {
+ content: "\F5F2";
+}
+.mdi-food-off::before {
+ content: "\F5F3";
+}
+.mdi-food-variant::before {
+ content: "\F25C";
+}
+.mdi-foot-print::before {
+ content: "\FF6F";
+}
+.mdi-football::before {
+ content: "\F25D";
+}
+.mdi-football-australian::before {
+ content: "\F25E";
+}
+.mdi-football-helmet::before {
+ content: "\F25F";
+}
+.mdi-forklift::before {
+ content: "\F7C8";
+}
+.mdi-format-align-bottom::before {
+ content: "\F752";
+}
+.mdi-format-align-center::before {
+ content: "\F260";
+}
+.mdi-format-align-justify::before {
+ content: "\F261";
+}
+.mdi-format-align-left::before {
+ content: "\F262";
+}
+.mdi-format-align-middle::before {
+ content: "\F753";
+}
+.mdi-format-align-right::before {
+ content: "\F263";
+}
+.mdi-format-align-top::before {
+ content: "\F754";
+}
+.mdi-format-annotation-minus::before {
+ content: "\FABB";
+}
+.mdi-format-annotation-plus::before {
+ content: "\F646";
+}
+.mdi-format-bold::before {
+ content: "\F264";
+}
+.mdi-format-clear::before {
+ content: "\F265";
+}
+.mdi-format-color-fill::before {
+ content: "\F266";
+}
+.mdi-format-color-highlight::before {
+ content: "\FE14";
+}
+.mdi-format-color-marker-cancel::before {
+ content: "\F033E";
+}
+.mdi-format-color-text::before {
+ content: "\F69D";
+}
+.mdi-format-columns::before {
+ content: "\F8DE";
+}
+.mdi-format-float-center::before {
+ content: "\F267";
+}
+.mdi-format-float-left::before {
+ content: "\F268";
+}
+.mdi-format-float-none::before {
+ content: "\F269";
+}
+.mdi-format-float-right::before {
+ content: "\F26A";
+}
+.mdi-format-font::before {
+ content: "\F6D5";
+}
+.mdi-format-font-size-decrease::before {
+ content: "\F9F2";
+}
+.mdi-format-font-size-increase::before {
+ content: "\F9F3";
+}
+.mdi-format-header-1::before {
+ content: "\F26B";
+}
+.mdi-format-header-2::before {
+ content: "\F26C";
+}
+.mdi-format-header-3::before {
+ content: "\F26D";
+}
+.mdi-format-header-4::before {
+ content: "\F26E";
+}
+.mdi-format-header-5::before {
+ content: "\F26F";
+}
+.mdi-format-header-6::before {
+ content: "\F270";
+}
+.mdi-format-header-decrease::before {
+ content: "\F271";
+}
+.mdi-format-header-equal::before {
+ content: "\F272";
+}
+.mdi-format-header-increase::before {
+ content: "\F273";
+}
+.mdi-format-header-pound::before {
+ content: "\F274";
+}
+.mdi-format-horizontal-align-center::before {
+ content: "\F61E";
+}
+.mdi-format-horizontal-align-left::before {
+ content: "\F61F";
+}
+.mdi-format-horizontal-align-right::before {
+ content: "\F620";
+}
+.mdi-format-indent-decrease::before {
+ content: "\F275";
+}
+.mdi-format-indent-increase::before {
+ content: "\F276";
+}
+.mdi-format-italic::before {
+ content: "\F277";
+}
+.mdi-format-letter-case::before {
+ content: "\FB19";
+}
+.mdi-format-letter-case-lower::before {
+ content: "\FB1A";
+}
+.mdi-format-letter-case-upper::before {
+ content: "\FB1B";
+}
+.mdi-format-letter-ends-with::before {
+ content: "\FFD8";
+}
+.mdi-format-letter-matches::before {
+ content: "\FFD9";
+}
+.mdi-format-letter-starts-with::before {
+ content: "\FFDA";
+}
+.mdi-format-line-spacing::before {
+ content: "\F278";
+}
+.mdi-format-line-style::before {
+ content: "\F5C8";
+}
+.mdi-format-line-weight::before {
+ content: "\F5C9";
+}
+.mdi-format-list-bulleted::before {
+ content: "\F279";
+}
+.mdi-format-list-bulleted-square::before {
+ content: "\FDAC";
+}
+.mdi-format-list-bulleted-triangle::before {
+ content: "\FECF";
+}
+.mdi-format-list-bulleted-type::before {
+ content: "\F27A";
+}
+.mdi-format-list-checkbox::before {
+ content: "\F969";
+}
+.mdi-format-list-checks::before {
+ content: "\F755";
+}
+.mdi-format-list-numbered::before {
+ content: "\F27B";
+}
+.mdi-format-list-numbered-rtl::before {
+ content: "\FCE9";
+}
+.mdi-format-list-text::before {
+ content: "\F029A";
+}
+.mdi-format-overline::before {
+ content: "\FED0";
+}
+.mdi-format-page-break::before {
+ content: "\F6D6";
+}
+.mdi-format-paint::before {
+ content: "\F27C";
+}
+.mdi-format-paragraph::before {
+ content: "\F27D";
+}
+.mdi-format-pilcrow::before {
+ content: "\F6D7";
+}
+.mdi-format-quote-close::before {
+ content: "\F27E";
+}
+.mdi-format-quote-close-outline::before {
+ content: "\F01D3";
+}
+.mdi-format-quote-open::before {
+ content: "\F756";
+}
+.mdi-format-quote-open-outline::before {
+ content: "\F01D2";
+}
+.mdi-format-rotate-90::before {
+ content: "\F6A9";
+}
+.mdi-format-section::before {
+ content: "\F69E";
+}
+.mdi-format-size::before {
+ content: "\F27F";
+}
+.mdi-format-strikethrough::before {
+ content: "\F280";
+}
+.mdi-format-strikethrough-variant::before {
+ content: "\F281";
+}
+.mdi-format-subscript::before {
+ content: "\F282";
+}
+.mdi-format-superscript::before {
+ content: "\F283";
+}
+.mdi-format-text::before {
+ content: "\F284";
+}
+.mdi-format-text-rotation-angle-down::before {
+ content: "\FFDB";
+}
+.mdi-format-text-rotation-angle-up::before {
+ content: "\FFDC";
+}
+.mdi-format-text-rotation-down::before {
+ content: "\FD4F";
+}
+.mdi-format-text-rotation-down-vertical::before {
+ content: "\FFDD";
+}
+.mdi-format-text-rotation-none::before {
+ content: "\FD50";
+}
+.mdi-format-text-rotation-up::before {
+ content: "\FFDE";
+}
+.mdi-format-text-rotation-vertical::before {
+ content: "\FFDF";
+}
+.mdi-format-text-variant::before {
+ content: "\FE15";
+}
+.mdi-format-text-wrapping-clip::before {
+ content: "\FCEA";
+}
+.mdi-format-text-wrapping-overflow::before {
+ content: "\FCEB";
+}
+.mdi-format-text-wrapping-wrap::before {
+ content: "\FCEC";
+}
+.mdi-format-textbox::before {
+ content: "\FCED";
+}
+.mdi-format-textdirection-l-to-r::before {
+ content: "\F285";
+}
+.mdi-format-textdirection-r-to-l::before {
+ content: "\F286";
+}
+.mdi-format-title::before {
+ content: "\F5F4";
+}
+.mdi-format-underline::before {
+ content: "\F287";
+}
+.mdi-format-vertical-align-bottom::before {
+ content: "\F621";
+}
+.mdi-format-vertical-align-center::before {
+ content: "\F622";
+}
+.mdi-format-vertical-align-top::before {
+ content: "\F623";
+}
+.mdi-format-wrap-inline::before {
+ content: "\F288";
+}
+.mdi-format-wrap-square::before {
+ content: "\F289";
+}
+.mdi-format-wrap-tight::before {
+ content: "\F28A";
+}
+.mdi-format-wrap-top-bottom::before {
+ content: "\F28B";
+}
+.mdi-forum::before {
+ content: "\F28C";
+}
+.mdi-forum-outline::before {
+ content: "\F821";
+}
+.mdi-forward::before {
+ content: "\F28D";
+}
+.mdi-forwardburger::before {
+ content: "\FD51";
+}
+.mdi-fountain::before {
+ content: "\F96A";
+}
+.mdi-fountain-pen::before {
+ content: "\FCEE";
+}
+.mdi-fountain-pen-tip::before {
+ content: "\FCEF";
+}
+.mdi-foursquare::before {
+ content: "\F28E";
+}
+.mdi-freebsd::before {
+ content: "\F8DF";
+}
+.mdi-frequently-asked-questions::before {
+ content: "\FED1";
+}
+.mdi-fridge::before {
+ content: "\F290";
+}
+.mdi-fridge-alert::before {
+ content: "\F01DC";
+}
+.mdi-fridge-alert-outline::before {
+ content: "\F01DD";
+}
+.mdi-fridge-bottom::before {
+ content: "\F292";
+}
+.mdi-fridge-off::before {
+ content: "\F01DA";
+}
+.mdi-fridge-off-outline::before {
+ content: "\F01DB";
+}
+.mdi-fridge-outline::before {
+ content: "\F28F";
+}
+.mdi-fridge-top::before {
+ content: "\F291";
+}
+.mdi-fruit-cherries::before {
+ content: "\F0064";
+}
+.mdi-fruit-citrus::before {
+ content: "\F0065";
+}
+.mdi-fruit-grapes::before {
+ content: "\F0066";
+}
+.mdi-fruit-grapes-outline::before {
+ content: "\F0067";
+}
+.mdi-fruit-pineapple::before {
+ content: "\F0068";
+}
+.mdi-fruit-watermelon::before {
+ content: "\F0069";
+}
+.mdi-fuel::before {
+ content: "\F7C9";
+}
+.mdi-fullscreen::before {
+ content: "\F293";
+}
+.mdi-fullscreen-exit::before {
+ content: "\F294";
+}
+.mdi-function::before {
+ content: "\F295";
+}
+.mdi-function-variant::before {
+ content: "\F870";
+}
+.mdi-furigana-horizontal::before {
+ content: "\F00AC";
+}
+.mdi-furigana-vertical::before {
+ content: "\F00AD";
+}
+.mdi-fuse::before {
+ content: "\FC61";
+}
+.mdi-fuse-blade::before {
+ content: "\FC62";
+}
+.mdi-gamepad::before {
+ content: "\F296";
+}
+.mdi-gamepad-circle::before {
+ content: "\FE16";
+}
+.mdi-gamepad-circle-down::before {
+ content: "\FE17";
+}
+.mdi-gamepad-circle-left::before {
+ content: "\FE18";
+}
+.mdi-gamepad-circle-outline::before {
+ content: "\FE19";
+}
+.mdi-gamepad-circle-right::before {
+ content: "\FE1A";
+}
+.mdi-gamepad-circle-up::before {
+ content: "\FE1B";
+}
+.mdi-gamepad-down::before {
+ content: "\FE1C";
+}
+.mdi-gamepad-left::before {
+ content: "\FE1D";
+}
+.mdi-gamepad-right::before {
+ content: "\FE1E";
+}
+.mdi-gamepad-round::before {
+ content: "\FE1F";
+}
+.mdi-gamepad-round-down::before {
+ content: "\FE7E";
+}
+.mdi-gamepad-round-left::before {
+ content: "\FE7F";
+}
+.mdi-gamepad-round-outline::before {
+ content: "\FE80";
+}
+.mdi-gamepad-round-right::before {
+ content: "\FE81";
+}
+.mdi-gamepad-round-up::before {
+ content: "\FE82";
+}
+.mdi-gamepad-square::before {
+ content: "\FED2";
+}
+.mdi-gamepad-square-outline::before {
+ content: "\FED3";
+}
+.mdi-gamepad-up::before {
+ content: "\FE83";
+}
+.mdi-gamepad-variant::before {
+ content: "\F297";
+}
+.mdi-gamepad-variant-outline::before {
+ content: "\FED4";
+}
+.mdi-gamma::before {
+ content: "\F0119";
+}
+.mdi-gantry-crane::before {
+ content: "\FDAD";
+}
+.mdi-garage::before {
+ content: "\F6D8";
+}
+.mdi-garage-alert::before {
+ content: "\F871";
+}
+.mdi-garage-alert-variant::before {
+ content: "\F0300";
+}
+.mdi-garage-open::before {
+ content: "\F6D9";
+}
+.mdi-garage-open-variant::before {
+ content: "\F02FF";
+}
+.mdi-garage-variant::before {
+ content: "\F02FE";
+}
+.mdi-gas-cylinder::before {
+ content: "\F647";
+}
+.mdi-gas-station::before {
+ content: "\F298";
+}
+.mdi-gas-station-outline::before {
+ content: "\FED5";
+}
+.mdi-gate::before {
+ content: "\F299";
+}
+.mdi-gate-and::before {
+ content: "\F8E0";
+}
+.mdi-gate-arrow-right::before {
+ content: "\F0194";
+}
+.mdi-gate-nand::before {
+ content: "\F8E1";
+}
+.mdi-gate-nor::before {
+ content: "\F8E2";
+}
+.mdi-gate-not::before {
+ content: "\F8E3";
+}
+.mdi-gate-open::before {
+ content: "\F0195";
+}
+.mdi-gate-or::before {
+ content: "\F8E4";
+}
+.mdi-gate-xnor::before {
+ content: "\F8E5";
+}
+.mdi-gate-xor::before {
+ content: "\F8E6";
+}
+.mdi-gatsby::before {
+ content: "\FE84";
+}
+.mdi-gauge::before {
+ content: "\F29A";
+}
+.mdi-gauge-empty::before {
+ content: "\F872";
+}
+.mdi-gauge-full::before {
+ content: "\F873";
+}
+.mdi-gauge-low::before {
+ content: "\F874";
+}
+.mdi-gavel::before {
+ content: "\F29B";
+}
+.mdi-gender-female::before {
+ content: "\F29C";
+}
+.mdi-gender-male::before {
+ content: "\F29D";
+}
+.mdi-gender-male-female::before {
+ content: "\F29E";
+}
+.mdi-gender-male-female-variant::before {
+ content: "\F016A";
+}
+.mdi-gender-non-binary::before {
+ content: "\F016B";
+}
+.mdi-gender-transgender::before {
+ content: "\F29F";
+}
+.mdi-gentoo::before {
+ content: "\F8E7";
+}
+.mdi-gesture::before {
+ content: "\F7CA";
+}
+.mdi-gesture-double-tap::before {
+ content: "\F73B";
+}
+.mdi-gesture-pinch::before {
+ content: "\FABC";
+}
+.mdi-gesture-spread::before {
+ content: "\FABD";
+}
+.mdi-gesture-swipe::before {
+ content: "\FD52";
+}
+.mdi-gesture-swipe-down::before {
+ content: "\F73C";
+}
+.mdi-gesture-swipe-horizontal::before {
+ content: "\FABE";
+}
+.mdi-gesture-swipe-left::before {
+ content: "\F73D";
+}
+.mdi-gesture-swipe-right::before {
+ content: "\F73E";
+}
+.mdi-gesture-swipe-up::before {
+ content: "\F73F";
+}
+.mdi-gesture-swipe-vertical::before {
+ content: "\FABF";
+}
+.mdi-gesture-tap::before {
+ content: "\F740";
+}
+.mdi-gesture-tap-box::before {
+ content: "\F02D4";
+}
+.mdi-gesture-tap-button::before {
+ content: "\F02D3";
+}
+.mdi-gesture-tap-hold::before {
+ content: "\FD53";
+}
+.mdi-gesture-two-double-tap::before {
+ content: "\F741";
+}
+.mdi-gesture-two-tap::before {
+ content: "\F742";
+}
+.mdi-ghost::before {
+ content: "\F2A0";
+}
+.mdi-ghost-off::before {
+ content: "\F9F4";
+}
+.mdi-gif::before {
+ content: "\FD54";
+}
+.mdi-gift::before {
+ content: "\FE85";
+}
+.mdi-gift-outline::before {
+ content: "\F2A1";
+}
+.mdi-git::before {
+ content: "\F2A2";
+}
+.mdi-github-box::before {
+ content: "\F2A3";
+}
+.mdi-github-circle::before {
+ content: "\F2A4";
+}
+.mdi-github-face::before {
+ content: "\F6DA";
+}
+.mdi-gitlab::before {
+ content: "\FB7C";
+}
+.mdi-glass-cocktail::before {
+ content: "\F356";
+}
+.mdi-glass-flute::before {
+ content: "\F2A5";
+}
+.mdi-glass-mug::before {
+ content: "\F2A6";
+}
+.mdi-glass-mug-variant::before {
+ content: "\F0141";
+}
+.mdi-glass-pint-outline::before {
+ content: "\F0338";
+}
+.mdi-glass-stange::before {
+ content: "\F2A7";
+}
+.mdi-glass-tulip::before {
+ content: "\F2A8";
+}
+.mdi-glass-wine::before {
+ content: "\F875";
+}
+.mdi-glassdoor::before {
+ content: "\F2A9";
+}
+.mdi-glasses::before {
+ content: "\F2AA";
+}
+.mdi-globe-light::before {
+ content: "\F0302";
+}
+.mdi-globe-model::before {
+ content: "\F8E8";
+}
+.mdi-gmail::before {
+ content: "\F2AB";
+}
+.mdi-gnome::before {
+ content: "\F2AC";
+}
+.mdi-go-kart::before {
+ content: "\FD55";
+}
+.mdi-go-kart-track::before {
+ content: "\FD56";
+}
+.mdi-gog::before {
+ content: "\FB7D";
+}
+.mdi-gold::before {
+ content: "\F027A";
+}
+.mdi-golf::before {
+ content: "\F822";
+}
+.mdi-golf-cart::before {
+ content: "\F01CF";
+}
+.mdi-golf-tee::before {
+ content: "\F00AE";
+}
+.mdi-gondola::before {
+ content: "\F685";
+}
+.mdi-goodreads::before {
+ content: "\FD57";
+}
+.mdi-google::before {
+ content: "\F2AD";
+}
+.mdi-google-adwords::before {
+ content: "\FC63";
+}
+.mdi-google-analytics::before {
+ content: "\F7CB";
+}
+.mdi-google-assistant::before {
+ content: "\F7CC";
+}
+.mdi-google-cardboard::before {
+ content: "\F2AE";
+}
+.mdi-google-chrome::before {
+ content: "\F2AF";
+}
+.mdi-google-circles::before {
+ content: "\F2B0";
+}
+.mdi-google-circles-communities::before {
+ content: "\F2B1";
+}
+.mdi-google-circles-extended::before {
+ content: "\F2B2";
+}
+.mdi-google-circles-group::before {
+ content: "\F2B3";
+}
+.mdi-google-classroom::before {
+ content: "\F2C0";
+}
+.mdi-google-cloud::before {
+ content: "\F0221";
+}
+.mdi-google-controller::before {
+ content: "\F2B4";
+}
+.mdi-google-controller-off::before {
+ content: "\F2B5";
+}
+.mdi-google-downasaur::before {
+ content: "\F038D";
+}
+.mdi-google-drive::before {
+ content: "\F2B6";
+}
+.mdi-google-earth::before {
+ content: "\F2B7";
+}
+.mdi-google-fit::before {
+ content: "\F96B";
+}
+.mdi-google-glass::before {
+ content: "\F2B8";
+}
+.mdi-google-hangouts::before {
+ content: "\F2C9";
+}
+.mdi-google-home::before {
+ content: "\F823";
+}
+.mdi-google-keep::before {
+ content: "\F6DB";
+}
+.mdi-google-lens::before {
+ content: "\F9F5";
+}
+.mdi-google-maps::before {
+ content: "\F5F5";
+}
+.mdi-google-my-business::before {
+ content: "\F006A";
+}
+.mdi-google-nearby::before {
+ content: "\F2B9";
+}
+.mdi-google-pages::before {
+ content: "\F2BA";
+}
+.mdi-google-photos::before {
+ content: "\F6DC";
+}
+.mdi-google-physical-web::before {
+ content: "\F2BB";
+}
+.mdi-google-play::before {
+ content: "\F2BC";
+}
+.mdi-google-plus::before {
+ content: "\F2BD";
+}
+.mdi-google-plus-box::before {
+ content: "\F2BE";
+}
+.mdi-google-podcast::before {
+ content: "\FED6";
+}
+.mdi-google-spreadsheet::before {
+ content: "\F9F6";
+}
+.mdi-google-street-view::before {
+ content: "\FC64";
+}
+.mdi-google-translate::before {
+ content: "\F2BF";
+}
+.mdi-gradient::before {
+ content: "\F69F";
+}
+.mdi-grain::before {
+ content: "\FD58";
+}
+.mdi-graph::before {
+ content: "\F006B";
+}
+.mdi-graph-outline::before {
+ content: "\F006C";
+}
+.mdi-graphql::before {
+ content: "\F876";
+}
+.mdi-grave-stone::before {
+ content: "\FB7E";
+}
+.mdi-grease-pencil::before {
+ content: "\F648";
+}
+.mdi-greater-than::before {
+ content: "\F96C";
+}
+.mdi-greater-than-or-equal::before {
+ content: "\F96D";
+}
+.mdi-grid::before {
+ content: "\F2C1";
+}
+.mdi-grid-large::before {
+ content: "\F757";
+}
+.mdi-grid-off::before {
+ content: "\F2C2";
+}
+.mdi-grill::before {
+ content: "\FE86";
+}
+.mdi-grill-outline::before {
+ content: "\F01B5";
+}
+.mdi-group::before {
+ content: "\F2C3";
+}
+.mdi-guitar-acoustic::before {
+ content: "\F770";
+}
+.mdi-guitar-electric::before {
+ content: "\F2C4";
+}
+.mdi-guitar-pick::before {
+ content: "\F2C5";
+}
+.mdi-guitar-pick-outline::before {
+ content: "\F2C6";
+}
+.mdi-guy-fawkes-mask::before {
+ content: "\F824";
+}
+.mdi-hackernews::before {
+ content: "\F624";
+}
+.mdi-hail::before {
+ content: "\FAC0";
+}
+.mdi-hair-dryer::before {
+ content: "\F011A";
+}
+.mdi-hair-dryer-outline::before {
+ content: "\F011B";
+}
+.mdi-halloween::before {
+ content: "\FB7F";
+}
+.mdi-hamburger::before {
+ content: "\F684";
+}
+.mdi-hammer::before {
+ content: "\F8E9";
+}
+.mdi-hammer-screwdriver::before {
+ content: "\F034D";
+}
+.mdi-hammer-wrench::before {
+ content: "\F034E";
+}
+.mdi-hand::before {
+ content: "\FA4E";
+}
+.mdi-hand-heart::before {
+ content: "\F011C";
+}
+.mdi-hand-left::before {
+ content: "\FE87";
+}
+.mdi-hand-okay::before {
+ content: "\FA4F";
+}
+.mdi-hand-peace::before {
+ content: "\FA50";
+}
+.mdi-hand-peace-variant::before {
+ content: "\FA51";
+}
+.mdi-hand-pointing-down::before {
+ content: "\FA52";
+}
+.mdi-hand-pointing-left::before {
+ content: "\FA53";
+}
+.mdi-hand-pointing-right::before {
+ content: "\F2C7";
+}
+.mdi-hand-pointing-up::before {
+ content: "\FA54";
+}
+.mdi-hand-right::before {
+ content: "\FE88";
+}
+.mdi-hand-saw::before {
+ content: "\FE89";
+}
+.mdi-handball::before {
+ content: "\FF70";
+}
+.mdi-handcuffs::before {
+ content: "\F0169";
+}
+.mdi-handshake::before {
+ content: "\F0243";
+}
+.mdi-hanger::before {
+ content: "\F2C8";
+}
+.mdi-hard-hat::before {
+ content: "\F96E";
+}
+.mdi-harddisk::before {
+ content: "\F2CA";
+}
+.mdi-harddisk-plus::before {
+ content: "\F006D";
+}
+.mdi-harddisk-remove::before {
+ content: "\F006E";
+}
+.mdi-hat-fedora::before {
+ content: "\FB80";
+}
+.mdi-hazard-lights::before {
+ content: "\FC65";
+}
+.mdi-hdr::before {
+ content: "\FD59";
+}
+.mdi-hdr-off::before {
+ content: "\FD5A";
+}
+.mdi-head::before {
+ content: "\F0389";
+}
+.mdi-head-alert::before {
+ content: "\F0363";
+}
+.mdi-head-alert-outline::before {
+ content: "\F0364";
+}
+.mdi-head-check::before {
+ content: "\F0365";
+}
+.mdi-head-check-outline::before {
+ content: "\F0366";
+}
+.mdi-head-cog::before {
+ content: "\F0367";
+}
+.mdi-head-cog-outline::before {
+ content: "\F0368";
+}
+.mdi-head-dots-horizontal::before {
+ content: "\F0369";
+}
+.mdi-head-dots-horizontal-outline::before {
+ content: "\F036A";
+}
+.mdi-head-flash::before {
+ content: "\F036B";
+}
+.mdi-head-flash-outline::before {
+ content: "\F036C";
+}
+.mdi-head-heart::before {
+ content: "\F036D";
+}
+.mdi-head-heart-outline::before {
+ content: "\F036E";
+}
+.mdi-head-lightbulb::before {
+ content: "\F036F";
+}
+.mdi-head-lightbulb-outline::before {
+ content: "\F0370";
+}
+.mdi-head-minus::before {
+ content: "\F0371";
+}
+.mdi-head-minus-outline::before {
+ content: "\F0372";
+}
+.mdi-head-outline::before {
+ content: "\F038A";
+}
+.mdi-head-plus::before {
+ content: "\F0373";
+}
+.mdi-head-plus-outline::before {
+ content: "\F0374";
+}
+.mdi-head-question::before {
+ content: "\F0375";
+}
+.mdi-head-question-outline::before {
+ content: "\F0376";
+}
+.mdi-head-remove::before {
+ content: "\F0377";
+}
+.mdi-head-remove-outline::before {
+ content: "\F0378";
+}
+.mdi-head-snowflake::before {
+ content: "\F0379";
+}
+.mdi-head-snowflake-outline::before {
+ content: "\F037A";
+}
+.mdi-head-sync::before {
+ content: "\F037B";
+}
+.mdi-head-sync-outline::before {
+ content: "\F037C";
+}
+.mdi-headphones::before {
+ content: "\F2CB";
+}
+.mdi-headphones-bluetooth::before {
+ content: "\F96F";
+}
+.mdi-headphones-box::before {
+ content: "\F2CC";
+}
+.mdi-headphones-off::before {
+ content: "\F7CD";
+}
+.mdi-headphones-settings::before {
+ content: "\F2CD";
+}
+.mdi-headset::before {
+ content: "\F2CE";
+}
+.mdi-headset-dock::before {
+ content: "\F2CF";
+}
+.mdi-headset-off::before {
+ content: "\F2D0";
+}
+.mdi-heart::before {
+ content: "\F2D1";
+}
+.mdi-heart-box::before {
+ content: "\F2D2";
+}
+.mdi-heart-box-outline::before {
+ content: "\F2D3";
+}
+.mdi-heart-broken::before {
+ content: "\F2D4";
+}
+.mdi-heart-broken-outline::before {
+ content: "\FCF0";
+}
+.mdi-heart-circle::before {
+ content: "\F970";
+}
+.mdi-heart-circle-outline::before {
+ content: "\F971";
+}
+.mdi-heart-flash::before {
+ content: "\FF16";
+}
+.mdi-heart-half::before {
+ content: "\F6DE";
+}
+.mdi-heart-half-full::before {
+ content: "\F6DD";
+}
+.mdi-heart-half-outline::before {
+ content: "\F6DF";
+}
+.mdi-heart-multiple::before {
+ content: "\FA55";
+}
+.mdi-heart-multiple-outline::before {
+ content: "\FA56";
+}
+.mdi-heart-off::before {
+ content: "\F758";
+}
+.mdi-heart-outline::before {
+ content: "\F2D5";
+}
+.mdi-heart-pulse::before {
+ content: "\F5F6";
+}
+.mdi-helicopter::before {
+ content: "\FAC1";
+}
+.mdi-help::before {
+ content: "\F2D6";
+}
+.mdi-help-box::before {
+ content: "\F78A";
+}
+.mdi-help-circle::before {
+ content: "\F2D7";
+}
+.mdi-help-circle-outline::before {
+ content: "\F625";
+}
+.mdi-help-network::before {
+ content: "\F6F4";
+}
+.mdi-help-network-outline::before {
+ content: "\FC66";
+}
+.mdi-help-rhombus::before {
+ content: "\FB81";
+}
+.mdi-help-rhombus-outline::before {
+ content: "\FB82";
+}
+.mdi-hexadecimal::before {
+ content: "\F02D2";
+}
+.mdi-hexagon::before {
+ content: "\F2D8";
+}
+.mdi-hexagon-multiple::before {
+ content: "\F6E0";
+}
+.mdi-hexagon-multiple-outline::before {
+ content: "\F011D";
+}
+.mdi-hexagon-outline::before {
+ content: "\F2D9";
+}
+.mdi-hexagon-slice-1::before {
+ content: "\FAC2";
+}
+.mdi-hexagon-slice-2::before {
+ content: "\FAC3";
+}
+.mdi-hexagon-slice-3::before {
+ content: "\FAC4";
+}
+.mdi-hexagon-slice-4::before {
+ content: "\FAC5";
+}
+.mdi-hexagon-slice-5::before {
+ content: "\FAC6";
+}
+.mdi-hexagon-slice-6::before {
+ content: "\FAC7";
+}
+.mdi-hexagram::before {
+ content: "\FAC8";
+}
+.mdi-hexagram-outline::before {
+ content: "\FAC9";
+}
+.mdi-high-definition::before {
+ content: "\F7CE";
+}
+.mdi-high-definition-box::before {
+ content: "\F877";
+}
+.mdi-highway::before {
+ content: "\F5F7";
+}
+.mdi-hiking::before {
+ content: "\FD5B";
+}
+.mdi-hinduism::before {
+ content: "\F972";
+}
+.mdi-history::before {
+ content: "\F2DA";
+}
+.mdi-hockey-puck::before {
+ content: "\F878";
+}
+.mdi-hockey-sticks::before {
+ content: "\F879";
+}
+.mdi-hololens::before {
+ content: "\F2DB";
+}
+.mdi-home::before {
+ content: "\F2DC";
+}
+.mdi-home-account::before {
+ content: "\F825";
+}
+.mdi-home-alert::before {
+ content: "\F87A";
+}
+.mdi-home-analytics::before {
+ content: "\FED7";
+}
+.mdi-home-assistant::before {
+ content: "\F7CF";
+}
+.mdi-home-automation::before {
+ content: "\F7D0";
+}
+.mdi-home-circle::before {
+ content: "\F7D1";
+}
+.mdi-home-circle-outline::before {
+ content: "\F006F";
+}
+.mdi-home-city::before {
+ content: "\FCF1";
+}
+.mdi-home-city-outline::before {
+ content: "\FCF2";
+}
+.mdi-home-currency-usd::before {
+ content: "\F8AE";
+}
+.mdi-home-edit::before {
+ content: "\F0184";
+}
+.mdi-home-edit-outline::before {
+ content: "\F0185";
+}
+.mdi-home-export-outline::before {
+ content: "\FFB8";
+}
+.mdi-home-flood::before {
+ content: "\FF17";
+}
+.mdi-home-floor-0::before {
+ content: "\FDAE";
+}
+.mdi-home-floor-1::before {
+ content: "\FD5C";
+}
+.mdi-home-floor-2::before {
+ content: "\FD5D";
+}
+.mdi-home-floor-3::before {
+ content: "\FD5E";
+}
+.mdi-home-floor-a::before {
+ content: "\FD5F";
+}
+.mdi-home-floor-b::before {
+ content: "\FD60";
+}
+.mdi-home-floor-g::before {
+ content: "\FD61";
+}
+.mdi-home-floor-l::before {
+ content: "\FD62";
+}
+.mdi-home-floor-negative-1::before {
+ content: "\FDAF";
+}
+.mdi-home-group::before {
+ content: "\FDB0";
+}
+.mdi-home-heart::before {
+ content: "\F826";
+}
+.mdi-home-import-outline::before {
+ content: "\FFB9";
+}
+.mdi-home-lightbulb::before {
+ content: "\F027C";
+}
+.mdi-home-lightbulb-outline::before {
+ content: "\F027D";
+}
+.mdi-home-lock::before {
+ content: "\F8EA";
+}
+.mdi-home-lock-open::before {
+ content: "\F8EB";
+}
+.mdi-home-map-marker::before {
+ content: "\F5F8";
+}
+.mdi-home-minus::before {
+ content: "\F973";
+}
+.mdi-home-modern::before {
+ content: "\F2DD";
+}
+.mdi-home-outline::before {
+ content: "\F6A0";
+}
+.mdi-home-plus::before {
+ content: "\F974";
+}
+.mdi-home-remove::before {
+ content: "\F0272";
+}
+.mdi-home-roof::before {
+ content: "\F0156";
+}
+.mdi-home-thermometer::before {
+ content: "\FF71";
+}
+.mdi-home-thermometer-outline::before {
+ content: "\FF72";
+}
+.mdi-home-variant::before {
+ content: "\F2DE";
+}
+.mdi-home-variant-outline::before {
+ content: "\FB83";
+}
+.mdi-hook::before {
+ content: "\F6E1";
+}
+.mdi-hook-off::before {
+ content: "\F6E2";
+}
+.mdi-hops::before {
+ content: "\F2DF";
+}
+.mdi-horizontal-rotate-clockwise::before {
+ content: "\F011E";
+}
+.mdi-horizontal-rotate-counterclockwise::before {
+ content: "\F011F";
+}
+.mdi-horseshoe::before {
+ content: "\FA57";
+}
+.mdi-hospital::before {
+ content: "\F0017";
+}
+.mdi-hospital-box::before {
+ content: "\F2E0";
+}
+.mdi-hospital-box-outline::before {
+ content: "\F0018";
+}
+.mdi-hospital-building::before {
+ content: "\F2E1";
+}
+.mdi-hospital-marker::before {
+ content: "\F2E2";
+}
+.mdi-hot-tub::before {
+ content: "\F827";
+}
+.mdi-hotel::before {
+ content: "\F2E3";
+}
+.mdi-houzz::before {
+ content: "\F2E4";
+}
+.mdi-houzz-box::before {
+ content: "\F2E5";
+}
+.mdi-hubspot::before {
+ content: "\FCF3";
+}
+.mdi-hulu::before {
+ content: "\F828";
+}
+.mdi-human::before {
+ content: "\F2E6";
+}
+.mdi-human-child::before {
+ content: "\F2E7";
+}
+.mdi-human-female::before {
+ content: "\F649";
+}
+.mdi-human-female-boy::before {
+ content: "\FA58";
+}
+.mdi-human-female-female::before {
+ content: "\FA59";
+}
+.mdi-human-female-girl::before {
+ content: "\FA5A";
+}
+.mdi-human-greeting::before {
+ content: "\F64A";
+}
+.mdi-human-handsdown::before {
+ content: "\F64B";
+}
+.mdi-human-handsup::before {
+ content: "\F64C";
+}
+.mdi-human-male::before {
+ content: "\F64D";
+}
+.mdi-human-male-boy::before {
+ content: "\FA5B";
+}
+.mdi-human-male-female::before {
+ content: "\F2E8";
+}
+.mdi-human-male-girl::before {
+ content: "\FA5C";
+}
+.mdi-human-male-height::before {
+ content: "\FF18";
+}
+.mdi-human-male-height-variant::before {
+ content: "\FF19";
+}
+.mdi-human-male-male::before {
+ content: "\FA5D";
+}
+.mdi-human-pregnant::before {
+ content: "\F5CF";
+}
+.mdi-humble-bundle::before {
+ content: "\F743";
+}
+.mdi-hvac::before {
+ content: "\F037D";
+}
+.mdi-hydraulic-oil-level::before {
+ content: "\F034F";
+}
+.mdi-hydraulic-oil-temperature::before {
+ content: "\F0350";
+}
+.mdi-hydro-power::before {
+ content: "\F0310";
+}
+.mdi-ice-cream::before {
+ content: "\F829";
+}
+.mdi-ice-pop::before {
+ content: "\FF1A";
+}
+.mdi-id-card::before {
+ content: "\FFE0";
+}
+.mdi-identifier::before {
+ content: "\FF1B";
+}
+.mdi-ideogram-cjk::before {
+ content: "\F035C";
+}
+.mdi-ideogram-cjk-variant::before {
+ content: "\F035D";
+}
+.mdi-iframe::before {
+ content: "\FC67";
+}
+.mdi-iframe-array::before {
+ content: "\F0120";
+}
+.mdi-iframe-array-outline::before {
+ content: "\F0121";
+}
+.mdi-iframe-braces::before {
+ content: "\F0122";
+}
+.mdi-iframe-braces-outline::before {
+ content: "\F0123";
+}
+.mdi-iframe-outline::before {
+ content: "\FC68";
+}
+.mdi-iframe-parentheses::before {
+ content: "\F0124";
+}
+.mdi-iframe-parentheses-outline::before {
+ content: "\F0125";
+}
+.mdi-iframe-variable::before {
+ content: "\F0126";
+}
+.mdi-iframe-variable-outline::before {
+ content: "\F0127";
+}
+.mdi-image::before {
+ content: "\F2E9";
+}
+.mdi-image-album::before {
+ content: "\F2EA";
+}
+.mdi-image-area::before {
+ content: "\F2EB";
+}
+.mdi-image-area-close::before {
+ content: "\F2EC";
+}
+.mdi-image-auto-adjust::before {
+ content: "\FFE1";
+}
+.mdi-image-broken::before {
+ content: "\F2ED";
+}
+.mdi-image-broken-variant::before {
+ content: "\F2EE";
+}
+.mdi-image-edit::before {
+ content: "\F020E";
+}
+.mdi-image-edit-outline::before {
+ content: "\F020F";
+}
+.mdi-image-filter::before {
+ content: "\F2EF";
+}
+.mdi-image-filter-black-white::before {
+ content: "\F2F0";
+}
+.mdi-image-filter-center-focus::before {
+ content: "\F2F1";
+}
+.mdi-image-filter-center-focus-strong::before {
+ content: "\FF1C";
+}
+.mdi-image-filter-center-focus-strong-outline::before {
+ content: "\FF1D";
+}
+.mdi-image-filter-center-focus-weak::before {
+ content: "\F2F2";
+}
+.mdi-image-filter-drama::before {
+ content: "\F2F3";
+}
+.mdi-image-filter-frames::before {
+ content: "\F2F4";
+}
+.mdi-image-filter-hdr::before {
+ content: "\F2F5";
+}
+.mdi-image-filter-none::before {
+ content: "\F2F6";
+}
+.mdi-image-filter-tilt-shift::before {
+ content: "\F2F7";
+}
+.mdi-image-filter-vintage::before {
+ content: "\F2F8";
+}
+.mdi-image-frame::before {
+ content: "\FE8A";
+}
+.mdi-image-move::before {
+ content: "\F9F7";
+}
+.mdi-image-multiple::before {
+ content: "\F2F9";
+}
+.mdi-image-off::before {
+ content: "\F82A";
+}
+.mdi-image-off-outline::before {
+ content: "\F01FC";
+}
+.mdi-image-outline::before {
+ content: "\F975";
+}
+.mdi-image-plus::before {
+ content: "\F87B";
+}
+.mdi-image-search::before {
+ content: "\F976";
+}
+.mdi-image-search-outline::before {
+ content: "\F977";
+}
+.mdi-image-size-select-actual::before {
+ content: "\FC69";
+}
+.mdi-image-size-select-large::before {
+ content: "\FC6A";
+}
+.mdi-image-size-select-small::before {
+ content: "\FC6B";
+}
+.mdi-import::before {
+ content: "\F2FA";
+}
+.mdi-inbox::before {
+ content: "\F686";
+}
+.mdi-inbox-arrow-down::before {
+ content: "\F2FB";
+}
+.mdi-inbox-arrow-down-outline::before {
+ content: "\F029B";
+}
+.mdi-inbox-arrow-up::before {
+ content: "\F3D1";
+}
+.mdi-inbox-arrow-up-outline::before {
+ content: "\F029C";
+}
+.mdi-inbox-full::before {
+ content: "\F029D";
+}
+.mdi-inbox-full-outline::before {
+ content: "\F029E";
+}
+.mdi-inbox-multiple::before {
+ content: "\F8AF";
+}
+.mdi-inbox-multiple-outline::before {
+ content: "\FB84";
+}
+.mdi-inbox-outline::before {
+ content: "\F029F";
+}
+.mdi-incognito::before {
+ content: "\F5F9";
+}
+.mdi-infinity::before {
+ content: "\F6E3";
+}
+.mdi-information::before {
+ content: "\F2FC";
+}
+.mdi-information-outline::before {
+ content: "\F2FD";
+}
+.mdi-information-variant::before {
+ content: "\F64E";
+}
+.mdi-instagram::before {
+ content: "\F2FE";
+}
+.mdi-instapaper::before {
+ content: "\F2FF";
+}
+.mdi-instrument-triangle::before {
+ content: "\F0070";
+}
+.mdi-internet-explorer::before {
+ content: "\F300";
+}
+.mdi-invert-colors::before {
+ content: "\F301";
+}
+.mdi-invert-colors-off::before {
+ content: "\FE8B";
+}
+.mdi-iobroker::before {
+ content: "\F0313";
+}
+.mdi-ip::before {
+ content: "\FA5E";
+}
+.mdi-ip-network::before {
+ content: "\FA5F";
+}
+.mdi-ip-network-outline::before {
+ content: "\FC6C";
+}
+.mdi-ipod::before {
+ content: "\FC6D";
+}
+.mdi-islam::before {
+ content: "\F978";
+}
+.mdi-island::before {
+ content: "\F0071";
+}
+.mdi-itunes::before {
+ content: "\F676";
+}
+.mdi-iv-bag::before {
+ content: "\F00E4";
+}
+.mdi-jabber::before {
+ content: "\FDB1";
+}
+.mdi-jeepney::before {
+ content: "\F302";
+}
+.mdi-jellyfish::before {
+ content: "\FF1E";
+}
+.mdi-jellyfish-outline::before {
+ content: "\FF1F";
+}
+.mdi-jira::before {
+ content: "\F303";
+}
+.mdi-jquery::before {
+ content: "\F87C";
+}
+.mdi-jsfiddle::before {
+ content: "\F304";
+}
+.mdi-json::before {
+ content: "\F626";
+}
+.mdi-judaism::before {
+ content: "\F979";
+}
+.mdi-jump-rope::before {
+ content: "\F032A";
+}
+.mdi-kabaddi::before {
+ content: "\FD63";
+}
+.mdi-karate::before {
+ content: "\F82B";
+}
+.mdi-keg::before {
+ content: "\F305";
+}
+.mdi-kettle::before {
+ content: "\F5FA";
+}
+.mdi-kettle-alert::before {
+ content: "\F0342";
+}
+.mdi-kettle-alert-outline::before {
+ content: "\F0343";
+}
+.mdi-kettle-off::before {
+ content: "\F0346";
+}
+.mdi-kettle-off-outline::before {
+ content: "\F0347";
+}
+.mdi-kettle-outline::before {
+ content: "\FF73";
+}
+.mdi-kettle-steam::before {
+ content: "\F0344";
+}
+.mdi-kettle-steam-outline::before {
+ content: "\F0345";
+}
+.mdi-kettlebell::before {
+ content: "\F032B";
+}
+.mdi-key::before {
+ content: "\F306";
+}
+.mdi-key-arrow-right::before {
+ content: "\F033D";
+}
+.mdi-key-change::before {
+ content: "\F307";
+}
+.mdi-key-link::before {
+ content: "\F01CA";
+}
+.mdi-key-minus::before {
+ content: "\F308";
+}
+.mdi-key-outline::before {
+ content: "\FDB2";
+}
+.mdi-key-plus::before {
+ content: "\F309";
+}
+.mdi-key-remove::before {
+ content: "\F30A";
+}
+.mdi-key-star::before {
+ content: "\F01C9";
+}
+.mdi-key-variant::before {
+ content: "\F30B";
+}
+.mdi-key-wireless::before {
+ content: "\FFE2";
+}
+.mdi-keyboard::before {
+ content: "\F30C";
+}
+.mdi-keyboard-backspace::before {
+ content: "\F30D";
+}
+.mdi-keyboard-caps::before {
+ content: "\F30E";
+}
+.mdi-keyboard-close::before {
+ content: "\F30F";
+}
+.mdi-keyboard-esc::before {
+ content: "\F02E2";
+}
+.mdi-keyboard-f1::before {
+ content: "\F02D6";
+}
+.mdi-keyboard-f10::before {
+ content: "\F02DF";
+}
+.mdi-keyboard-f11::before {
+ content: "\F02E0";
+}
+.mdi-keyboard-f12::before {
+ content: "\F02E1";
+}
+.mdi-keyboard-f2::before {
+ content: "\F02D7";
+}
+.mdi-keyboard-f3::before {
+ content: "\F02D8";
+}
+.mdi-keyboard-f4::before {
+ content: "\F02D9";
+}
+.mdi-keyboard-f5::before {
+ content: "\F02DA";
+}
+.mdi-keyboard-f6::before {
+ content: "\F02DB";
+}
+.mdi-keyboard-f7::before {
+ content: "\F02DC";
+}
+.mdi-keyboard-f8::before {
+ content: "\F02DD";
+}
+.mdi-keyboard-f9::before {
+ content: "\F02DE";
+}
+.mdi-keyboard-off::before {
+ content: "\F310";
+}
+.mdi-keyboard-off-outline::before {
+ content: "\FE8C";
+}
+.mdi-keyboard-outline::before {
+ content: "\F97A";
+}
+.mdi-keyboard-return::before {
+ content: "\F311";
+}
+.mdi-keyboard-settings::before {
+ content: "\F9F8";
+}
+.mdi-keyboard-settings-outline::before {
+ content: "\F9F9";
+}
+.mdi-keyboard-space::before {
+ content: "\F0072";
+}
+.mdi-keyboard-tab::before {
+ content: "\F312";
+}
+.mdi-keyboard-variant::before {
+ content: "\F313";
+}
+.mdi-khanda::before {
+ content: "\F0128";
+}
+.mdi-kickstarter::before {
+ content: "\F744";
+}
+.mdi-klingon::before {
+ content: "\F0386";
+}
+.mdi-knife::before {
+ content: "\F9FA";
+}
+.mdi-knife-military::before {
+ content: "\F9FB";
+}
+.mdi-kodi::before {
+ content: "\F314";
+}
+.mdi-kotlin::before {
+ content: "\F0244";
+}
+.mdi-kubernetes::before {
+ content: "\F0129";
+}
+.mdi-label::before {
+ content: "\F315";
+}
+.mdi-label-multiple::before {
+ content: "\F03A0";
+}
+.mdi-label-multiple-outline::before {
+ content: "\F03A1";
+}
+.mdi-label-off::before {
+ content: "\FACA";
+}
+.mdi-label-off-outline::before {
+ content: "\FACB";
+}
+.mdi-label-outline::before {
+ content: "\F316";
+}
+.mdi-label-percent::before {
+ content: "\F0315";
+}
+.mdi-label-percent-outline::before {
+ content: "\F0316";
+}
+.mdi-label-variant::before {
+ content: "\FACC";
+}
+.mdi-label-variant-outline::before {
+ content: "\FACD";
+}
+.mdi-ladybug::before {
+ content: "\F82C";
+}
+.mdi-lambda::before {
+ content: "\F627";
+}
+.mdi-lamp::before {
+ content: "\F6B4";
+}
+.mdi-lan::before {
+ content: "\F317";
+}
+.mdi-lan-check::before {
+ content: "\F02D5";
+}
+.mdi-lan-connect::before {
+ content: "\F318";
+}
+.mdi-lan-disconnect::before {
+ content: "\F319";
+}
+.mdi-lan-pending::before {
+ content: "\F31A";
+}
+.mdi-language-c::before {
+ content: "\F671";
+}
+.mdi-language-cpp::before {
+ content: "\F672";
+}
+.mdi-language-csharp::before {
+ content: "\F31B";
+}
+.mdi-language-css3::before {
+ content: "\F31C";
+}
+.mdi-language-fortran::before {
+ content: "\F0245";
+}
+.mdi-language-go::before {
+ content: "\F7D2";
+}
+.mdi-language-haskell::before {
+ content: "\FC6E";
+}
+.mdi-language-html5::before {
+ content: "\F31D";
+}
+.mdi-language-java::before {
+ content: "\FB1C";
+}
+.mdi-language-javascript::before {
+ content: "\F31E";
+}
+.mdi-language-lua::before {
+ content: "\F8B0";
+}
+.mdi-language-php::before {
+ content: "\F31F";
+}
+.mdi-language-python::before {
+ content: "\F320";
+}
+.mdi-language-python-text::before {
+ content: "\F321";
+}
+.mdi-language-r::before {
+ content: "\F7D3";
+}
+.mdi-language-ruby-on-rails::before {
+ content: "\FACE";
+}
+.mdi-language-swift::before {
+ content: "\F6E4";
+}
+.mdi-language-typescript::before {
+ content: "\F6E5";
+}
+.mdi-laptop::before {
+ content: "\F322";
+}
+.mdi-laptop-chromebook::before {
+ content: "\F323";
+}
+.mdi-laptop-mac::before {
+ content: "\F324";
+}
+.mdi-laptop-off::before {
+ content: "\F6E6";
+}
+.mdi-laptop-windows::before {
+ content: "\F325";
+}
+.mdi-laravel::before {
+ content: "\FACF";
+}
+.mdi-lasso::before {
+ content: "\FF20";
+}
+.mdi-lastfm::before {
+ content: "\F326";
+}
+.mdi-lastpass::before {
+ content: "\F446";
+}
+.mdi-latitude::before {
+ content: "\FF74";
+}
+.mdi-launch::before {
+ content: "\F327";
+}
+.mdi-lava-lamp::before {
+ content: "\F7D4";
+}
+.mdi-layers::before {
+ content: "\F328";
+}
+.mdi-layers-minus::before {
+ content: "\FE8D";
+}
+.mdi-layers-off::before {
+ content: "\F329";
+}
+.mdi-layers-off-outline::before {
+ content: "\F9FC";
+}
+.mdi-layers-outline::before {
+ content: "\F9FD";
+}
+.mdi-layers-plus::before {
+ content: "\FE30";
+}
+.mdi-layers-remove::before {
+ content: "\FE31";
+}
+.mdi-layers-search::before {
+ content: "\F0231";
+}
+.mdi-layers-search-outline::before {
+ content: "\F0232";
+}
+.mdi-layers-triple::before {
+ content: "\FF75";
+}
+.mdi-layers-triple-outline::before {
+ content: "\FF76";
+}
+.mdi-lead-pencil::before {
+ content: "\F64F";
+}
+.mdi-leaf::before {
+ content: "\F32A";
+}
+.mdi-leaf-maple::before {
+ content: "\FC6F";
+}
+.mdi-leaf-maple-off::before {
+ content: "\F0305";
+}
+.mdi-leaf-off::before {
+ content: "\F0304";
+}
+.mdi-leak::before {
+ content: "\FDB3";
+}
+.mdi-leak-off::before {
+ content: "\FDB4";
+}
+.mdi-led-off::before {
+ content: "\F32B";
+}
+.mdi-led-on::before {
+ content: "\F32C";
+}
+.mdi-led-outline::before {
+ content: "\F32D";
+}
+.mdi-led-strip::before {
+ content: "\F7D5";
+}
+.mdi-led-strip-variant::before {
+ content: "\F0073";
+}
+.mdi-led-variant-off::before {
+ content: "\F32E";
+}
+.mdi-led-variant-on::before {
+ content: "\F32F";
+}
+.mdi-led-variant-outline::before {
+ content: "\F330";
+}
+.mdi-leek::before {
+ content: "\F01A8";
+}
+.mdi-less-than::before {
+ content: "\F97B";
+}
+.mdi-less-than-or-equal::before {
+ content: "\F97C";
+}
+.mdi-library::before {
+ content: "\F331";
+}
+.mdi-library-books::before {
+ content: "\F332";
+}
+.mdi-library-movie::before {
+ content: "\FCF4";
+}
+.mdi-library-music::before {
+ content: "\F333";
+}
+.mdi-library-music-outline::before {
+ content: "\FF21";
+}
+.mdi-library-shelves::before {
+ content: "\FB85";
+}
+.mdi-library-video::before {
+ content: "\FCF5";
+}
+.mdi-license::before {
+ content: "\FFE3";
+}
+.mdi-lifebuoy::before {
+ content: "\F87D";
+}
+.mdi-light-switch::before {
+ content: "\F97D";
+}
+.mdi-lightbulb::before {
+ content: "\F335";
+}
+.mdi-lightbulb-cfl::before {
+ content: "\F0233";
+}
+.mdi-lightbulb-cfl-off::before {
+ content: "\F0234";
+}
+.mdi-lightbulb-cfl-spiral::before {
+ content: "\F02A0";
+}
+.mdi-lightbulb-cfl-spiral-off::before {
+ content: "\F02EE";
+}
+.mdi-lightbulb-group::before {
+ content: "\F027E";
+}
+.mdi-lightbulb-group-off::before {
+ content: "\F02F8";
+}
+.mdi-lightbulb-group-off-outline::before {
+ content: "\F02F9";
+}
+.mdi-lightbulb-group-outline::before {
+ content: "\F027F";
+}
+.mdi-lightbulb-multiple::before {
+ content: "\F0280";
+}
+.mdi-lightbulb-multiple-off::before {
+ content: "\F02FA";
+}
+.mdi-lightbulb-multiple-off-outline::before {
+ content: "\F02FB";
+}
+.mdi-lightbulb-multiple-outline::before {
+ content: "\F0281";
+}
+.mdi-lightbulb-off::before {
+ content: "\FE32";
+}
+.mdi-lightbulb-off-outline::before {
+ content: "\FE33";
+}
+.mdi-lightbulb-on::before {
+ content: "\F6E7";
+}
+.mdi-lightbulb-on-outline::before {
+ content: "\F6E8";
+}
+.mdi-lightbulb-outline::before {
+ content: "\F336";
+}
+.mdi-lighthouse::before {
+ content: "\F9FE";
+}
+.mdi-lighthouse-on::before {
+ content: "\F9FF";
+}
+.mdi-link::before {
+ content: "\F337";
+}
+.mdi-link-box::before {
+ content: "\FCF6";
+}
+.mdi-link-box-outline::before {
+ content: "\FCF7";
+}
+.mdi-link-box-variant::before {
+ content: "\FCF8";
+}
+.mdi-link-box-variant-outline::before {
+ content: "\FCF9";
+}
+.mdi-link-lock::before {
+ content: "\F00E5";
+}
+.mdi-link-off::before {
+ content: "\F338";
+}
+.mdi-link-plus::before {
+ content: "\FC70";
+}
+.mdi-link-variant::before {
+ content: "\F339";
+}
+.mdi-link-variant-minus::before {
+ content: "\F012A";
+}
+.mdi-link-variant-off::before {
+ content: "\F33A";
+}
+.mdi-link-variant-plus::before {
+ content: "\F012B";
+}
+.mdi-link-variant-remove::before {
+ content: "\F012C";
+}
+.mdi-linkedin::before {
+ content: "\F33B";
+}
+.mdi-linkedin-box::before {
+ content: "\F33C";
+}
+.mdi-linux::before {
+ content: "\F33D";
+}
+.mdi-linux-mint::before {
+ content: "\F8EC";
+}
+.mdi-litecoin::before {
+ content: "\FA60";
+}
+.mdi-loading::before {
+ content: "\F771";
+}
+.mdi-location-enter::before {
+ content: "\FFE4";
+}
+.mdi-location-exit::before {
+ content: "\FFE5";
+}
+.mdi-lock::before {
+ content: "\F33E";
+}
+.mdi-lock-alert::before {
+ content: "\F8ED";
+}
+.mdi-lock-clock::before {
+ content: "\F97E";
+}
+.mdi-lock-open::before {
+ content: "\F33F";
+}
+.mdi-lock-open-outline::before {
+ content: "\F340";
+}
+.mdi-lock-open-variant::before {
+ content: "\FFE6";
+}
+.mdi-lock-open-variant-outline::before {
+ content: "\FFE7";
+}
+.mdi-lock-outline::before {
+ content: "\F341";
+}
+.mdi-lock-pattern::before {
+ content: "\F6E9";
+}
+.mdi-lock-plus::before {
+ content: "\F5FB";
+}
+.mdi-lock-question::before {
+ content: "\F8EE";
+}
+.mdi-lock-reset::before {
+ content: "\F772";
+}
+.mdi-lock-smart::before {
+ content: "\F8B1";
+}
+.mdi-locker::before {
+ content: "\F7D6";
+}
+.mdi-locker-multiple::before {
+ content: "\F7D7";
+}
+.mdi-login::before {
+ content: "\F342";
+}
+.mdi-login-variant::before {
+ content: "\F5FC";
+}
+.mdi-logout::before {
+ content: "\F343";
+}
+.mdi-logout-variant::before {
+ content: "\F5FD";
+}
+.mdi-longitude::before {
+ content: "\FF77";
+}
+.mdi-looks::before {
+ content: "\F344";
+}
+.mdi-loupe::before {
+ content: "\F345";
+}
+.mdi-lumx::before {
+ content: "\F346";
+}
+.mdi-lungs::before {
+ content: "\F00AF";
+}
+.mdi-lyft::before {
+ content: "\FB1D";
+}
+.mdi-magnet::before {
+ content: "\F347";
+}
+.mdi-magnet-on::before {
+ content: "\F348";
+}
+.mdi-magnify::before {
+ content: "\F349";
+}
+.mdi-magnify-close::before {
+ content: "\F97F";
+}
+.mdi-magnify-minus::before {
+ content: "\F34A";
+}
+.mdi-magnify-minus-cursor::before {
+ content: "\FA61";
+}
+.mdi-magnify-minus-outline::before {
+ content: "\F6EB";
+}
+.mdi-magnify-plus::before {
+ content: "\F34B";
+}
+.mdi-magnify-plus-cursor::before {
+ content: "\FA62";
+}
+.mdi-magnify-plus-outline::before {
+ content: "\F6EC";
+}
+.mdi-magnify-remove-cursor::before {
+ content: "\F0237";
+}
+.mdi-magnify-remove-outline::before {
+ content: "\F0238";
+}
+.mdi-magnify-scan::before {
+ content: "\F02A1";
+}
+.mdi-mail::before {
+ content: "\FED8";
+}
+.mdi-mail-ru::before {
+ content: "\F34C";
+}
+.mdi-mailbox::before {
+ content: "\F6ED";
+}
+.mdi-mailbox-open::before {
+ content: "\FD64";
+}
+.mdi-mailbox-open-outline::before {
+ content: "\FD65";
+}
+.mdi-mailbox-open-up::before {
+ content: "\FD66";
+}
+.mdi-mailbox-open-up-outline::before {
+ content: "\FD67";
+}
+.mdi-mailbox-outline::before {
+ content: "\FD68";
+}
+.mdi-mailbox-up::before {
+ content: "\FD69";
+}
+.mdi-mailbox-up-outline::before {
+ content: "\FD6A";
+}
+.mdi-map::before {
+ content: "\F34D";
+}
+.mdi-map-check::before {
+ content: "\FED9";
+}
+.mdi-map-check-outline::before {
+ content: "\FEDA";
+}
+.mdi-map-clock::before {
+ content: "\FCFA";
+}
+.mdi-map-clock-outline::before {
+ content: "\FCFB";
+}
+.mdi-map-legend::before {
+ content: "\FA00";
+}
+.mdi-map-marker::before {
+ content: "\F34E";
+}
+.mdi-map-marker-alert::before {
+ content: "\FF22";
+}
+.mdi-map-marker-alert-outline::before {
+ content: "\FF23";
+}
+.mdi-map-marker-check::before {
+ content: "\FC71";
+}
+.mdi-map-marker-check-outline::before {
+ content: "\F0326";
+}
+.mdi-map-marker-circle::before {
+ content: "\F34F";
+}
+.mdi-map-marker-distance::before {
+ content: "\F8EF";
+}
+.mdi-map-marker-down::before {
+ content: "\F012D";
+}
+.mdi-map-marker-left::before {
+ content: "\F0306";
+}
+.mdi-map-marker-left-outline::before {
+ content: "\F0308";
+}
+.mdi-map-marker-minus::before {
+ content: "\F650";
+}
+.mdi-map-marker-minus-outline::before {
+ content: "\F0324";
+}
+.mdi-map-marker-multiple::before {
+ content: "\F350";
+}
+.mdi-map-marker-multiple-outline::before {
+ content: "\F02A2";
+}
+.mdi-map-marker-off::before {
+ content: "\F351";
+}
+.mdi-map-marker-off-outline::before {
+ content: "\F0328";
+}
+.mdi-map-marker-outline::before {
+ content: "\F7D8";
+}
+.mdi-map-marker-path::before {
+ content: "\FCFC";
+}
+.mdi-map-marker-plus::before {
+ content: "\F651";
+}
+.mdi-map-marker-plus-outline::before {
+ content: "\F0323";
+}
+.mdi-map-marker-question::before {
+ content: "\FF24";
+}
+.mdi-map-marker-question-outline::before {
+ content: "\FF25";
+}
+.mdi-map-marker-radius::before {
+ content: "\F352";
+}
+.mdi-map-marker-radius-outline::before {
+ content: "\F0327";
+}
+.mdi-map-marker-remove::before {
+ content: "\FF26";
+}
+.mdi-map-marker-remove-outline::before {
+ content: "\F0325";
+}
+.mdi-map-marker-remove-variant::before {
+ content: "\FF27";
+}
+.mdi-map-marker-right::before {
+ content: "\F0307";
+}
+.mdi-map-marker-right-outline::before {
+ content: "\F0309";
+}
+.mdi-map-marker-up::before {
+ content: "\F012E";
+}
+.mdi-map-minus::before {
+ content: "\F980";
+}
+.mdi-map-outline::before {
+ content: "\F981";
+}
+.mdi-map-plus::before {
+ content: "\F982";
+}
+.mdi-map-search::before {
+ content: "\F983";
+}
+.mdi-map-search-outline::before {
+ content: "\F984";
+}
+.mdi-mapbox::before {
+ content: "\FB86";
+}
+.mdi-margin::before {
+ content: "\F353";
+}
+.mdi-markdown::before {
+ content: "\F354";
+}
+.mdi-markdown-outline::before {
+ content: "\FF78";
+}
+.mdi-marker::before {
+ content: "\F652";
+}
+.mdi-marker-cancel::before {
+ content: "\FDB5";
+}
+.mdi-marker-check::before {
+ content: "\F355";
+}
+.mdi-mastodon::before {
+ content: "\FAD0";
+}
+.mdi-mastodon-variant::before {
+ content: "\FAD1";
+}
+.mdi-material-design::before {
+ content: "\F985";
+}
+.mdi-material-ui::before {
+ content: "\F357";
+}
+.mdi-math-compass::before {
+ content: "\F358";
+}
+.mdi-math-cos::before {
+ content: "\FC72";
+}
+.mdi-math-integral::before {
+ content: "\FFE8";
+}
+.mdi-math-integral-box::before {
+ content: "\FFE9";
+}
+.mdi-math-log::before {
+ content: "\F00B0";
+}
+.mdi-math-norm::before {
+ content: "\FFEA";
+}
+.mdi-math-norm-box::before {
+ content: "\FFEB";
+}
+.mdi-math-sin::before {
+ content: "\FC73";
+}
+.mdi-math-tan::before {
+ content: "\FC74";
+}
+.mdi-matrix::before {
+ content: "\F628";
+}
+.mdi-medal::before {
+ content: "\F986";
+}
+.mdi-medal-outline::before {
+ content: "\F0351";
+}
+.mdi-medical-bag::before {
+ content: "\F6EE";
+}
+.mdi-meditation::before {
+ content: "\F01A6";
+}
+.mdi-medium::before {
+ content: "\F35A";
+}
+.mdi-meetup::before {
+ content: "\FAD2";
+}
+.mdi-memory::before {
+ content: "\F35B";
+}
+.mdi-menu::before {
+ content: "\F35C";
+}
+.mdi-menu-down::before {
+ content: "\F35D";
+}
+.mdi-menu-down-outline::before {
+ content: "\F6B5";
+}
+.mdi-menu-left::before {
+ content: "\F35E";
+}
+.mdi-menu-left-outline::before {
+ content: "\FA01";
+}
+.mdi-menu-open::before {
+ content: "\FB87";
+}
+.mdi-menu-right::before {
+ content: "\F35F";
+}
+.mdi-menu-right-outline::before {
+ content: "\FA02";
+}
+.mdi-menu-swap::before {
+ content: "\FA63";
+}
+.mdi-menu-swap-outline::before {
+ content: "\FA64";
+}
+.mdi-menu-up::before {
+ content: "\F360";
+}
+.mdi-menu-up-outline::before {
+ content: "\F6B6";
+}
+.mdi-merge::before {
+ content: "\FF79";
+}
+.mdi-message::before {
+ content: "\F361";
+}
+.mdi-message-alert::before {
+ content: "\F362";
+}
+.mdi-message-alert-outline::before {
+ content: "\FA03";
+}
+.mdi-message-arrow-left::before {
+ content: "\F031D";
+}
+.mdi-message-arrow-left-outline::before {
+ content: "\F031E";
+}
+.mdi-message-arrow-right::before {
+ content: "\F031F";
+}
+.mdi-message-arrow-right-outline::before {
+ content: "\F0320";
+}
+.mdi-message-bulleted::before {
+ content: "\F6A1";
+}
+.mdi-message-bulleted-off::before {
+ content: "\F6A2";
+}
+.mdi-message-draw::before {
+ content: "\F363";
+}
+.mdi-message-image::before {
+ content: "\F364";
+}
+.mdi-message-image-outline::before {
+ content: "\F0197";
+}
+.mdi-message-lock::before {
+ content: "\FFEC";
+}
+.mdi-message-lock-outline::before {
+ content: "\F0198";
+}
+.mdi-message-minus::before {
+ content: "\F0199";
+}
+.mdi-message-minus-outline::before {
+ content: "\F019A";
+}
+.mdi-message-outline::before {
+ content: "\F365";
+}
+.mdi-message-plus::before {
+ content: "\F653";
+}
+.mdi-message-plus-outline::before {
+ content: "\F00E6";
+}
+.mdi-message-processing::before {
+ content: "\F366";
+}
+.mdi-message-processing-outline::before {
+ content: "\F019B";
+}
+.mdi-message-reply::before {
+ content: "\F367";
+}
+.mdi-message-reply-text::before {
+ content: "\F368";
+}
+.mdi-message-settings::before {
+ content: "\F6EF";
+}
+.mdi-message-settings-outline::before {
+ content: "\F019C";
+}
+.mdi-message-settings-variant::before {
+ content: "\F6F0";
+}
+.mdi-message-settings-variant-outline::before {
+ content: "\F019D";
+}
+.mdi-message-text::before {
+ content: "\F369";
+}
+.mdi-message-text-clock::before {
+ content: "\F019E";
+}
+.mdi-message-text-clock-outline::before {
+ content: "\F019F";
+}
+.mdi-message-text-lock::before {
+ content: "\FFED";
+}
+.mdi-message-text-lock-outline::before {
+ content: "\F01A0";
+}
+.mdi-message-text-outline::before {
+ content: "\F36A";
+}
+.mdi-message-video::before {
+ content: "\F36B";
+}
+.mdi-meteor::before {
+ content: "\F629";
+}
+.mdi-metronome::before {
+ content: "\F7D9";
+}
+.mdi-metronome-tick::before {
+ content: "\F7DA";
+}
+.mdi-micro-sd::before {
+ content: "\F7DB";
+}
+.mdi-microphone::before {
+ content: "\F36C";
+}
+.mdi-microphone-minus::before {
+ content: "\F8B2";
+}
+.mdi-microphone-off::before {
+ content: "\F36D";
+}
+.mdi-microphone-outline::before {
+ content: "\F36E";
+}
+.mdi-microphone-plus::before {
+ content: "\F8B3";
+}
+.mdi-microphone-settings::before {
+ content: "\F36F";
+}
+.mdi-microphone-variant::before {
+ content: "\F370";
+}
+.mdi-microphone-variant-off::before {
+ content: "\F371";
+}
+.mdi-microscope::before {
+ content: "\F654";
+}
+.mdi-microsoft::before {
+ content: "\F372";
+}
+.mdi-microsoft-dynamics::before {
+ content: "\F987";
+}
+.mdi-microwave::before {
+ content: "\FC75";
+}
+.mdi-middleware::before {
+ content: "\FF7A";
+}
+.mdi-middleware-outline::before {
+ content: "\FF7B";
+}
+.mdi-midi::before {
+ content: "\F8F0";
+}
+.mdi-midi-port::before {
+ content: "\F8F1";
+}
+.mdi-mine::before {
+ content: "\FDB6";
+}
+.mdi-minecraft::before {
+ content: "\F373";
+}
+.mdi-mini-sd::before {
+ content: "\FA04";
+}
+.mdi-minidisc::before {
+ content: "\FA05";
+}
+.mdi-minus::before {
+ content: "\F374";
+}
+.mdi-minus-box::before {
+ content: "\F375";
+}
+.mdi-minus-box-multiple::before {
+ content: "\F016C";
+}
+.mdi-minus-box-multiple-outline::before {
+ content: "\F016D";
+}
+.mdi-minus-box-outline::before {
+ content: "\F6F1";
+}
+.mdi-minus-circle::before {
+ content: "\F376";
+}
+.mdi-minus-circle-outline::before {
+ content: "\F377";
+}
+.mdi-minus-network::before {
+ content: "\F378";
+}
+.mdi-minus-network-outline::before {
+ content: "\FC76";
+}
+.mdi-mirror::before {
+ content: "\F0228";
+}
+.mdi-mixcloud::before {
+ content: "\F62A";
+}
+.mdi-mixed-martial-arts::before {
+ content: "\FD6B";
+}
+.mdi-mixed-reality::before {
+ content: "\F87E";
+}
+.mdi-mixer::before {
+ content: "\F7DC";
+}
+.mdi-molecule::before {
+ content: "\FB88";
+}
+.mdi-monitor::before {
+ content: "\F379";
+}
+.mdi-monitor-cellphone::before {
+ content: "\F988";
+}
+.mdi-monitor-cellphone-star::before {
+ content: "\F989";
+}
+.mdi-monitor-clean::before {
+ content: "\F012F";
+}
+.mdi-monitor-dashboard::before {
+ content: "\FA06";
+}
+.mdi-monitor-edit::before {
+ content: "\F02F1";
+}
+.mdi-monitor-lock::before {
+ content: "\FDB7";
+}
+.mdi-monitor-multiple::before {
+ content: "\F37A";
+}
+.mdi-monitor-off::before {
+ content: "\FD6C";
+}
+.mdi-monitor-screenshot::before {
+ content: "\FE34";
+}
+.mdi-monitor-speaker::before {
+ content: "\FF7C";
+}
+.mdi-monitor-speaker-off::before {
+ content: "\FF7D";
+}
+.mdi-monitor-star::before {
+ content: "\FDB8";
+}
+.mdi-moon-first-quarter::before {
+ content: "\FF7E";
+}
+.mdi-moon-full::before {
+ content: "\FF7F";
+}
+.mdi-moon-last-quarter::before {
+ content: "\FF80";
+}
+.mdi-moon-new::before {
+ content: "\FF81";
+}
+.mdi-moon-waning-crescent::before {
+ content: "\FF82";
+}
+.mdi-moon-waning-gibbous::before {
+ content: "\FF83";
+}
+.mdi-moon-waxing-crescent::before {
+ content: "\FF84";
+}
+.mdi-moon-waxing-gibbous::before {
+ content: "\FF85";
+}
+.mdi-moped::before {
+ content: "\F00B1";
+}
+.mdi-more::before {
+ content: "\F37B";
+}
+.mdi-mother-heart::before {
+ content: "\F033F";
+}
+.mdi-mother-nurse::before {
+ content: "\FCFD";
+}
+.mdi-motion-sensor::before {
+ content: "\FD6D";
+}
+.mdi-motorbike::before {
+ content: "\F37C";
+}
+.mdi-mouse::before {
+ content: "\F37D";
+}
+.mdi-mouse-bluetooth::before {
+ content: "\F98A";
+}
+.mdi-mouse-off::before {
+ content: "\F37E";
+}
+.mdi-mouse-variant::before {
+ content: "\F37F";
+}
+.mdi-mouse-variant-off::before {
+ content: "\F380";
+}
+.mdi-move-resize::before {
+ content: "\F655";
+}
+.mdi-move-resize-variant::before {
+ content: "\F656";
+}
+.mdi-movie::before {
+ content: "\F381";
+}
+.mdi-movie-edit::before {
+ content: "\F014D";
+}
+.mdi-movie-edit-outline::before {
+ content: "\F014E";
+}
+.mdi-movie-filter::before {
+ content: "\F014F";
+}
+.mdi-movie-filter-outline::before {
+ content: "\F0150";
+}
+.mdi-movie-open::before {
+ content: "\FFEE";
+}
+.mdi-movie-open-outline::before {
+ content: "\FFEF";
+}
+.mdi-movie-outline::before {
+ content: "\FDB9";
+}
+.mdi-movie-roll::before {
+ content: "\F7DD";
+}
+.mdi-movie-search::before {
+ content: "\F01FD";
+}
+.mdi-movie-search-outline::before {
+ content: "\F01FE";
+}
+.mdi-muffin::before {
+ content: "\F98B";
+}
+.mdi-multiplication::before {
+ content: "\F382";
+}
+.mdi-multiplication-box::before {
+ content: "\F383";
+}
+.mdi-mushroom::before {
+ content: "\F7DE";
+}
+.mdi-mushroom-outline::before {
+ content: "\F7DF";
+}
+.mdi-music::before {
+ content: "\F759";
+}
+.mdi-music-accidental-double-flat::before {
+ content: "\FF86";
+}
+.mdi-music-accidental-double-sharp::before {
+ content: "\FF87";
+}
+.mdi-music-accidental-flat::before {
+ content: "\FF88";
+}
+.mdi-music-accidental-natural::before {
+ content: "\FF89";
+}
+.mdi-music-accidental-sharp::before {
+ content: "\FF8A";
+}
+.mdi-music-box::before {
+ content: "\F384";
+}
+.mdi-music-box-outline::before {
+ content: "\F385";
+}
+.mdi-music-circle::before {
+ content: "\F386";
+}
+.mdi-music-circle-outline::before {
+ content: "\FAD3";
+}
+.mdi-music-clef-alto::before {
+ content: "\FF8B";
+}
+.mdi-music-clef-bass::before {
+ content: "\FF8C";
+}
+.mdi-music-clef-treble::before {
+ content: "\FF8D";
+}
+.mdi-music-note::before {
+ content: "\F387";
+}
+.mdi-music-note-bluetooth::before {
+ content: "\F5FE";
+}
+.mdi-music-note-bluetooth-off::before {
+ content: "\F5FF";
+}
+.mdi-music-note-eighth::before {
+ content: "\F388";
+}
+.mdi-music-note-eighth-dotted::before {
+ content: "\FF8E";
+}
+.mdi-music-note-half::before {
+ content: "\F389";
+}
+.mdi-music-note-half-dotted::before {
+ content: "\FF8F";
+}
+.mdi-music-note-off::before {
+ content: "\F38A";
+}
+.mdi-music-note-off-outline::before {
+ content: "\FF90";
+}
+.mdi-music-note-outline::before {
+ content: "\FF91";
+}
+.mdi-music-note-plus::before {
+ content: "\FDBA";
+}
+.mdi-music-note-quarter::before {
+ content: "\F38B";
+}
+.mdi-music-note-quarter-dotted::before {
+ content: "\FF92";
+}
+.mdi-music-note-sixteenth::before {
+ content: "\F38C";
+}
+.mdi-music-note-sixteenth-dotted::before {
+ content: "\FF93";
+}
+.mdi-music-note-whole::before {
+ content: "\F38D";
+}
+.mdi-music-note-whole-dotted::before {
+ content: "\FF94";
+}
+.mdi-music-off::before {
+ content: "\F75A";
+}
+.mdi-music-rest-eighth::before {
+ content: "\FF95";
+}
+.mdi-music-rest-half::before {
+ content: "\FF96";
+}
+.mdi-music-rest-quarter::before {
+ content: "\FF97";
+}
+.mdi-music-rest-sixteenth::before {
+ content: "\FF98";
+}
+.mdi-music-rest-whole::before {
+ content: "\FF99";
+}
+.mdi-nail::before {
+ content: "\FDBB";
+}
+.mdi-nas::before {
+ content: "\F8F2";
+}
+.mdi-nativescript::before {
+ content: "\F87F";
+}
+.mdi-nature::before {
+ content: "\F38E";
+}
+.mdi-nature-people::before {
+ content: "\F38F";
+}
+.mdi-navigation::before {
+ content: "\F390";
+}
+.mdi-near-me::before {
+ content: "\F5CD";
+}
+.mdi-necklace::before {
+ content: "\FF28";
+}
+.mdi-needle::before {
+ content: "\F391";
+}
+.mdi-netflix::before {
+ content: "\F745";
+}
+.mdi-network::before {
+ content: "\F6F2";
+}
+.mdi-network-off::before {
+ content: "\FC77";
+}
+.mdi-network-off-outline::before {
+ content: "\FC78";
+}
+.mdi-network-outline::before {
+ content: "\FC79";
+}
+.mdi-network-router::before {
+ content: "\F00B2";
+}
+.mdi-network-strength-1::before {
+ content: "\F8F3";
+}
+.mdi-network-strength-1-alert::before {
+ content: "\F8F4";
+}
+.mdi-network-strength-2::before {
+ content: "\F8F5";
+}
+.mdi-network-strength-2-alert::before {
+ content: "\F8F6";
+}
+.mdi-network-strength-3::before {
+ content: "\F8F7";
+}
+.mdi-network-strength-3-alert::before {
+ content: "\F8F8";
+}
+.mdi-network-strength-4::before {
+ content: "\F8F9";
+}
+.mdi-network-strength-4-alert::before {
+ content: "\F8FA";
+}
+.mdi-network-strength-off::before {
+ content: "\F8FB";
+}
+.mdi-network-strength-off-outline::before {
+ content: "\F8FC";
+}
+.mdi-network-strength-outline::before {
+ content: "\F8FD";
+}
+.mdi-new-box::before {
+ content: "\F394";
+}
+.mdi-newspaper::before {
+ content: "\F395";
+}
+.mdi-newspaper-minus::before {
+ content: "\FF29";
+}
+.mdi-newspaper-plus::before {
+ content: "\FF2A";
+}
+.mdi-newspaper-variant::before {
+ content: "\F0023";
+}
+.mdi-newspaper-variant-multiple::before {
+ content: "\F0024";
+}
+.mdi-newspaper-variant-multiple-outline::before {
+ content: "\F0025";
+}
+.mdi-newspaper-variant-outline::before {
+ content: "\F0026";
+}
+.mdi-nfc::before {
+ content: "\F396";
+}
+.mdi-nfc-off::before {
+ content: "\FE35";
+}
+.mdi-nfc-search-variant::before {
+ content: "\FE36";
+}
+.mdi-nfc-tap::before {
+ content: "\F397";
+}
+.mdi-nfc-variant::before {
+ content: "\F398";
+}
+.mdi-nfc-variant-off::before {
+ content: "\FE37";
+}
+.mdi-ninja::before {
+ content: "\F773";
+}
+.mdi-nintendo-switch::before {
+ content: "\F7E0";
+}
+.mdi-nix::before {
+ content: "\F0130";
+}
+.mdi-nodejs::before {
+ content: "\F399";
+}
+.mdi-noodles::before {
+ content: "\F01A9";
+}
+.mdi-not-equal::before {
+ content: "\F98C";
+}
+.mdi-not-equal-variant::before {
+ content: "\F98D";
+}
+.mdi-note::before {
+ content: "\F39A";
+}
+.mdi-note-multiple::before {
+ content: "\F6B7";
+}
+.mdi-note-multiple-outline::before {
+ content: "\F6B8";
+}
+.mdi-note-outline::before {
+ content: "\F39B";
+}
+.mdi-note-plus::before {
+ content: "\F39C";
+}
+.mdi-note-plus-outline::before {
+ content: "\F39D";
+}
+.mdi-note-text::before {
+ content: "\F39E";
+}
+.mdi-note-text-outline::before {
+ content: "\F0202";
+}
+.mdi-notebook::before {
+ content: "\F82D";
+}
+.mdi-notebook-multiple::before {
+ content: "\FE38";
+}
+.mdi-notebook-outline::before {
+ content: "\FEDC";
+}
+.mdi-notification-clear-all::before {
+ content: "\F39F";
+}
+.mdi-npm::before {
+ content: "\F6F6";
+}
+.mdi-npm-variant::before {
+ content: "\F98E";
+}
+.mdi-npm-variant-outline::before {
+ content: "\F98F";
+}
+.mdi-nuke::before {
+ content: "\F6A3";
+}
+.mdi-null::before {
+ content: "\F7E1";
+}
+.mdi-numeric::before {
+ content: "\F3A0";
+}
+.mdi-numeric-0::before {
+ content: "\30";
+}
+.mdi-numeric-0-box::before {
+ content: "\F3A1";
+}
+.mdi-numeric-0-box-multiple::before {
+ content: "\FF2B";
+}
+.mdi-numeric-0-box-multiple-outline::before {
+ content: "\F3A2";
+}
+.mdi-numeric-0-box-outline::before {
+ content: "\F3A3";
+}
+.mdi-numeric-0-circle::before {
+ content: "\FC7A";
+}
+.mdi-numeric-0-circle-outline::before {
+ content: "\FC7B";
+}
+.mdi-numeric-1::before {
+ content: "\31";
+}
+.mdi-numeric-1-box::before {
+ content: "\F3A4";
+}
+.mdi-numeric-1-box-multiple::before {
+ content: "\FF2C";
+}
+.mdi-numeric-1-box-multiple-outline::before {
+ content: "\F3A5";
+}
+.mdi-numeric-1-box-outline::before {
+ content: "\F3A6";
+}
+.mdi-numeric-1-circle::before {
+ content: "\FC7C";
+}
+.mdi-numeric-1-circle-outline::before {
+ content: "\FC7D";
+}
+.mdi-numeric-10::before {
+ content: "\F000A";
+}
+.mdi-numeric-10-box::before {
+ content: "\FF9A";
+}
+.mdi-numeric-10-box-multiple::before {
+ content: "\F000B";
+}
+.mdi-numeric-10-box-multiple-outline::before {
+ content: "\F000C";
+}
+.mdi-numeric-10-box-outline::before {
+ content: "\FF9B";
+}
+.mdi-numeric-10-circle::before {
+ content: "\F000D";
+}
+.mdi-numeric-10-circle-outline::before {
+ content: "\F000E";
+}
+.mdi-numeric-2::before {
+ content: "\32";
+}
+.mdi-numeric-2-box::before {
+ content: "\F3A7";
+}
+.mdi-numeric-2-box-multiple::before {
+ content: "\FF2D";
+}
+.mdi-numeric-2-box-multiple-outline::before {
+ content: "\F3A8";
+}
+.mdi-numeric-2-box-outline::before {
+ content: "\F3A9";
+}
+.mdi-numeric-2-circle::before {
+ content: "\FC7E";
+}
+.mdi-numeric-2-circle-outline::before {
+ content: "\FC7F";
+}
+.mdi-numeric-3::before {
+ content: "\33";
+}
+.mdi-numeric-3-box::before {
+ content: "\F3AA";
+}
+.mdi-numeric-3-box-multiple::before {
+ content: "\FF2E";
+}
+.mdi-numeric-3-box-multiple-outline::before {
+ content: "\F3AB";
+}
+.mdi-numeric-3-box-outline::before {
+ content: "\F3AC";
+}
+.mdi-numeric-3-circle::before {
+ content: "\FC80";
+}
+.mdi-numeric-3-circle-outline::before {
+ content: "\FC81";
+}
+.mdi-numeric-4::before {
+ content: "\34";
+}
+.mdi-numeric-4-box::before {
+ content: "\F3AD";
+}
+.mdi-numeric-4-box-multiple::before {
+ content: "\FF2F";
+}
+.mdi-numeric-4-box-multiple-outline::before {
+ content: "\F3AE";
+}
+.mdi-numeric-4-box-outline::before {
+ content: "\F3AF";
+}
+.mdi-numeric-4-circle::before {
+ content: "\FC82";
+}
+.mdi-numeric-4-circle-outline::before {
+ content: "\FC83";
+}
+.mdi-numeric-5::before {
+ content: "\35";
+}
+.mdi-numeric-5-box::before {
+ content: "\F3B0";
+}
+.mdi-numeric-5-box-multiple::before {
+ content: "\FF30";
+}
+.mdi-numeric-5-box-multiple-outline::before {
+ content: "\F3B1";
+}
+.mdi-numeric-5-box-outline::before {
+ content: "\F3B2";
+}
+.mdi-numeric-5-circle::before {
+ content: "\FC84";
+}
+.mdi-numeric-5-circle-outline::before {
+ content: "\FC85";
+}
+.mdi-numeric-6::before {
+ content: "\36";
+}
+.mdi-numeric-6-box::before {
+ content: "\F3B3";
+}
+.mdi-numeric-6-box-multiple::before {
+ content: "\FF31";
+}
+.mdi-numeric-6-box-multiple-outline::before {
+ content: "\F3B4";
+}
+.mdi-numeric-6-box-outline::before {
+ content: "\F3B5";
+}
+.mdi-numeric-6-circle::before {
+ content: "\FC86";
+}
+.mdi-numeric-6-circle-outline::before {
+ content: "\FC87";
+}
+.mdi-numeric-7::before {
+ content: "\37";
+}
+.mdi-numeric-7-box::before {
+ content: "\F3B6";
+}
+.mdi-numeric-7-box-multiple::before {
+ content: "\FF32";
+}
+.mdi-numeric-7-box-multiple-outline::before {
+ content: "\F3B7";
+}
+.mdi-numeric-7-box-outline::before {
+ content: "\F3B8";
+}
+.mdi-numeric-7-circle::before {
+ content: "\FC88";
+}
+.mdi-numeric-7-circle-outline::before {
+ content: "\FC89";
+}
+.mdi-numeric-8::before {
+ content: "\38";
+}
+.mdi-numeric-8-box::before {
+ content: "\F3B9";
+}
+.mdi-numeric-8-box-multiple::before {
+ content: "\FF33";
+}
+.mdi-numeric-8-box-multiple-outline::before {
+ content: "\F3BA";
+}
+.mdi-numeric-8-box-outline::before {
+ content: "\F3BB";
+}
+.mdi-numeric-8-circle::before {
+ content: "\FC8A";
+}
+.mdi-numeric-8-circle-outline::before {
+ content: "\FC8B";
+}
+.mdi-numeric-9::before {
+ content: "\39";
+}
+.mdi-numeric-9-box::before {
+ content: "\F3BC";
+}
+.mdi-numeric-9-box-multiple::before {
+ content: "\FF34";
+}
+.mdi-numeric-9-box-multiple-outline::before {
+ content: "\F3BD";
+}
+.mdi-numeric-9-box-outline::before {
+ content: "\F3BE";
+}
+.mdi-numeric-9-circle::before {
+ content: "\FC8C";
+}
+.mdi-numeric-9-circle-outline::before {
+ content: "\FC8D";
+}
+.mdi-numeric-9-plus::before {
+ content: "\F000F";
+}
+.mdi-numeric-9-plus-box::before {
+ content: "\F3BF";
+}
+.mdi-numeric-9-plus-box-multiple::before {
+ content: "\FF35";
+}
+.mdi-numeric-9-plus-box-multiple-outline::before {
+ content: "\F3C0";
+}
+.mdi-numeric-9-plus-box-outline::before {
+ content: "\F3C1";
+}
+.mdi-numeric-9-plus-circle::before {
+ content: "\FC8E";
+}
+.mdi-numeric-9-plus-circle-outline::before {
+ content: "\FC8F";
+}
+.mdi-numeric-negative-1::before {
+ content: "\F0074";
+}
+.mdi-nut::before {
+ content: "\F6F7";
+}
+.mdi-nutrition::before {
+ content: "\F3C2";
+}
+.mdi-nuxt::before {
+ content: "\F0131";
+}
+.mdi-oar::before {
+ content: "\F67B";
+}
+.mdi-ocarina::before {
+ content: "\FDBC";
+}
+.mdi-oci::before {
+ content: "\F0314";
+}
+.mdi-ocr::before {
+ content: "\F0165";
+}
+.mdi-octagon::before {
+ content: "\F3C3";
+}
+.mdi-octagon-outline::before {
+ content: "\F3C4";
+}
+.mdi-octagram::before {
+ content: "\F6F8";
+}
+.mdi-octagram-outline::before {
+ content: "\F774";
+}
+.mdi-odnoklassniki::before {
+ content: "\F3C5";
+}
+.mdi-offer::before {
+ content: "\F0246";
+}
+.mdi-office::before {
+ content: "\F3C6";
+}
+.mdi-office-building::before {
+ content: "\F990";
+}
+.mdi-oil::before {
+ content: "\F3C7";
+}
+.mdi-oil-lamp::before {
+ content: "\FF36";
+}
+.mdi-oil-level::before {
+ content: "\F0075";
+}
+.mdi-oil-temperature::before {
+ content: "\F0019";
+}
+.mdi-omega::before {
+ content: "\F3C9";
+}
+.mdi-one-up::before {
+ content: "\FB89";
+}
+.mdi-onedrive::before {
+ content: "\F3CA";
+}
+.mdi-onenote::before {
+ content: "\F746";
+}
+.mdi-onepassword::before {
+ content: "\F880";
+}
+.mdi-opacity::before {
+ content: "\F5CC";
+}
+.mdi-open-in-app::before {
+ content: "\F3CB";
+}
+.mdi-open-in-new::before {
+ content: "\F3CC";
+}
+.mdi-open-source-initiative::before {
+ content: "\FB8A";
+}
+.mdi-openid::before {
+ content: "\F3CD";
+}
+.mdi-opera::before {
+ content: "\F3CE";
+}
+.mdi-orbit::before {
+ content: "\F018";
+}
+.mdi-origin::before {
+ content: "\FB2B";
+}
+.mdi-ornament::before {
+ content: "\F3CF";
+}
+.mdi-ornament-variant::before {
+ content: "\F3D0";
+}
+.mdi-outdoor-lamp::before {
+ content: "\F0076";
+}
+.mdi-outlook::before {
+ content: "\FCFE";
+}
+.mdi-overscan::before {
+ content: "\F0027";
+}
+.mdi-owl::before {
+ content: "\F3D2";
+}
+.mdi-pac-man::before {
+ content: "\FB8B";
+}
+.mdi-package::before {
+ content: "\F3D3";
+}
+.mdi-package-down::before {
+ content: "\F3D4";
+}
+.mdi-package-up::before {
+ content: "\F3D5";
+}
+.mdi-package-variant::before {
+ content: "\F3D6";
+}
+.mdi-package-variant-closed::before {
+ content: "\F3D7";
+}
+.mdi-page-first::before {
+ content: "\F600";
+}
+.mdi-page-last::before {
+ content: "\F601";
+}
+.mdi-page-layout-body::before {
+ content: "\F6F9";
+}
+.mdi-page-layout-footer::before {
+ content: "\F6FA";
+}
+.mdi-page-layout-header::before {
+ content: "\F6FB";
+}
+.mdi-page-layout-header-footer::before {
+ content: "\FF9C";
+}
+.mdi-page-layout-sidebar-left::before {
+ content: "\F6FC";
+}
+.mdi-page-layout-sidebar-right::before {
+ content: "\F6FD";
+}
+.mdi-page-next::before {
+ content: "\FB8C";
+}
+.mdi-page-next-outline::before {
+ content: "\FB8D";
+}
+.mdi-page-previous::before {
+ content: "\FB8E";
+}
+.mdi-page-previous-outline::before {
+ content: "\FB8F";
+}
+.mdi-palette::before {
+ content: "\F3D8";
+}
+.mdi-palette-advanced::before {
+ content: "\F3D9";
+}
+.mdi-palette-outline::before {
+ content: "\FE6C";
+}
+.mdi-palette-swatch::before {
+ content: "\F8B4";
+}
+.mdi-palette-swatch-outline::before {
+ content: "\F0387";
+}
+.mdi-palm-tree::before {
+ content: "\F0077";
+}
+.mdi-pan::before {
+ content: "\FB90";
+}
+.mdi-pan-bottom-left::before {
+ content: "\FB91";
+}
+.mdi-pan-bottom-right::before {
+ content: "\FB92";
+}
+.mdi-pan-down::before {
+ content: "\FB93";
+}
+.mdi-pan-horizontal::before {
+ content: "\FB94";
+}
+.mdi-pan-left::before {
+ content: "\FB95";
+}
+.mdi-pan-right::before {
+ content: "\FB96";
+}
+.mdi-pan-top-left::before {
+ content: "\FB97";
+}
+.mdi-pan-top-right::before {
+ content: "\FB98";
+}
+.mdi-pan-up::before {
+ content: "\FB99";
+}
+.mdi-pan-vertical::before {
+ content: "\FB9A";
+}
+.mdi-panda::before {
+ content: "\F3DA";
+}
+.mdi-pandora::before {
+ content: "\F3DB";
+}
+.mdi-panorama::before {
+ content: "\F3DC";
+}
+.mdi-panorama-fisheye::before {
+ content: "\F3DD";
+}
+.mdi-panorama-horizontal::before {
+ content: "\F3DE";
+}
+.mdi-panorama-vertical::before {
+ content: "\F3DF";
+}
+.mdi-panorama-wide-angle::before {
+ content: "\F3E0";
+}
+.mdi-paper-cut-vertical::before {
+ content: "\F3E1";
+}
+.mdi-paper-roll::before {
+ content: "\F0182";
+}
+.mdi-paper-roll-outline::before {
+ content: "\F0183";
+}
+.mdi-paperclip::before {
+ content: "\F3E2";
+}
+.mdi-parachute::before {
+ content: "\FC90";
+}
+.mdi-parachute-outline::before {
+ content: "\FC91";
+}
+.mdi-parking::before {
+ content: "\F3E3";
+}
+.mdi-party-popper::before {
+ content: "\F0078";
+}
+.mdi-passport::before {
+ content: "\F7E2";
+}
+.mdi-passport-biometric::before {
+ content: "\FDBD";
+}
+.mdi-pasta::before {
+ content: "\F018B";
+}
+.mdi-patio-heater::before {
+ content: "\FF9D";
+}
+.mdi-patreon::before {
+ content: "\F881";
+}
+.mdi-pause::before {
+ content: "\F3E4";
+}
+.mdi-pause-circle::before {
+ content: "\F3E5";
+}
+.mdi-pause-circle-outline::before {
+ content: "\F3E6";
+}
+.mdi-pause-octagon::before {
+ content: "\F3E7";
+}
+.mdi-pause-octagon-outline::before {
+ content: "\F3E8";
+}
+.mdi-paw::before {
+ content: "\F3E9";
+}
+.mdi-paw-off::before {
+ content: "\F657";
+}
+.mdi-paypal::before {
+ content: "\F882";
+}
+.mdi-pdf-box::before {
+ content: "\FE39";
+}
+.mdi-peace::before {
+ content: "\F883";
+}
+.mdi-peanut::before {
+ content: "\F001E";
+}
+.mdi-peanut-off::before {
+ content: "\F001F";
+}
+.mdi-peanut-off-outline::before {
+ content: "\F0021";
+}
+.mdi-peanut-outline::before {
+ content: "\F0020";
+}
+.mdi-pen::before {
+ content: "\F3EA";
+}
+.mdi-pen-lock::before {
+ content: "\FDBE";
+}
+.mdi-pen-minus::before {
+ content: "\FDBF";
+}
+.mdi-pen-off::before {
+ content: "\FDC0";
+}
+.mdi-pen-plus::before {
+ content: "\FDC1";
+}
+.mdi-pen-remove::before {
+ content: "\FDC2";
+}
+.mdi-pencil::before {
+ content: "\F3EB";
+}
+.mdi-pencil-box::before {
+ content: "\F3EC";
+}
+.mdi-pencil-box-multiple::before {
+ content: "\F016F";
+}
+.mdi-pencil-box-multiple-outline::before {
+ content: "\F0170";
+}
+.mdi-pencil-box-outline::before {
+ content: "\F3ED";
+}
+.mdi-pencil-circle::before {
+ content: "\F6FE";
+}
+.mdi-pencil-circle-outline::before {
+ content: "\F775";
+}
+.mdi-pencil-lock::before {
+ content: "\F3EE";
+}
+.mdi-pencil-lock-outline::before {
+ content: "\FDC3";
+}
+.mdi-pencil-minus::before {
+ content: "\FDC4";
+}
+.mdi-pencil-minus-outline::before {
+ content: "\FDC5";
+}
+.mdi-pencil-off::before {
+ content: "\F3EF";
+}
+.mdi-pencil-off-outline::before {
+ content: "\FDC6";
+}
+.mdi-pencil-outline::before {
+ content: "\FC92";
+}
+.mdi-pencil-plus::before {
+ content: "\FDC7";
+}
+.mdi-pencil-plus-outline::before {
+ content: "\FDC8";
+}
+.mdi-pencil-remove::before {
+ content: "\FDC9";
+}
+.mdi-pencil-remove-outline::before {
+ content: "\FDCA";
+}
+.mdi-pencil-ruler::before {
+ content: "\F037E";
+}
+.mdi-penguin::before {
+ content: "\FEDD";
+}
+.mdi-pentagon::before {
+ content: "\F6FF";
+}
+.mdi-pentagon-outline::before {
+ content: "\F700";
+}
+.mdi-percent::before {
+ content: "\F3F0";
+}
+.mdi-percent-outline::before {
+ content: "\F02A3";
+}
+.mdi-periodic-table::before {
+ content: "\F8B5";
+}
+.mdi-periodic-table-co::before {
+ content: "\F0329";
+}
+.mdi-periodic-table-co2::before {
+ content: "\F7E3";
+}
+.mdi-periscope::before {
+ content: "\F747";
+}
+.mdi-perspective-less::before {
+ content: "\FCFF";
+}
+.mdi-perspective-more::before {
+ content: "\FD00";
+}
+.mdi-pharmacy::before {
+ content: "\F3F1";
+}
+.mdi-phone::before {
+ content: "\F3F2";
+}
+.mdi-phone-alert::before {
+ content: "\FF37";
+}
+.mdi-phone-alert-outline::before {
+ content: "\F01B9";
+}
+.mdi-phone-bluetooth::before {
+ content: "\F3F3";
+}
+.mdi-phone-bluetooth-outline::before {
+ content: "\F01BA";
+}
+.mdi-phone-cancel::before {
+ content: "\F00E7";
+}
+.mdi-phone-cancel-outline::before {
+ content: "\F01BB";
+}
+.mdi-phone-check::before {
+ content: "\F01D4";
+}
+.mdi-phone-check-outline::before {
+ content: "\F01D5";
+}
+.mdi-phone-classic::before {
+ content: "\F602";
+}
+.mdi-phone-classic-off::before {
+ content: "\F02A4";
+}
+.mdi-phone-forward::before {
+ content: "\F3F4";
+}
+.mdi-phone-forward-outline::before {
+ content: "\F01BC";
+}
+.mdi-phone-hangup::before {
+ content: "\F3F5";
+}
+.mdi-phone-hangup-outline::before {
+ content: "\F01BD";
+}
+.mdi-phone-in-talk::before {
+ content: "\F3F6";
+}
+.mdi-phone-in-talk-outline::before {
+ content: "\F01AD";
+}
+.mdi-phone-incoming::before {
+ content: "\F3F7";
+}
+.mdi-phone-incoming-outline::before {
+ content: "\F01BE";
+}
+.mdi-phone-lock::before {
+ content: "\F3F8";
+}
+.mdi-phone-lock-outline::before {
+ content: "\F01BF";
+}
+.mdi-phone-log::before {
+ content: "\F3F9";
+}
+.mdi-phone-log-outline::before {
+ content: "\F01C0";
+}
+.mdi-phone-message::before {
+ content: "\F01C1";
+}
+.mdi-phone-message-outline::before {
+ content: "\F01C2";
+}
+.mdi-phone-minus::before {
+ content: "\F658";
+}
+.mdi-phone-minus-outline::before {
+ content: "\F01C3";
+}
+.mdi-phone-missed::before {
+ content: "\F3FA";
+}
+.mdi-phone-missed-outline::before {
+ content: "\F01D0";
+}
+.mdi-phone-off::before {
+ content: "\FDCB";
+}
+.mdi-phone-off-outline::before {
+ content: "\F01D1";
+}
+.mdi-phone-outgoing::before {
+ content: "\F3FB";
+}
+.mdi-phone-outgoing-outline::before {
+ content: "\F01C4";
+}
+.mdi-phone-outline::before {
+ content: "\FDCC";
+}
+.mdi-phone-paused::before {
+ content: "\F3FC";
+}
+.mdi-phone-paused-outline::before {
+ content: "\F01C5";
+}
+.mdi-phone-plus::before {
+ content: "\F659";
+}
+.mdi-phone-plus-outline::before {
+ content: "\F01C6";
+}
+.mdi-phone-return::before {
+ content: "\F82E";
+}
+.mdi-phone-return-outline::before {
+ content: "\F01C7";
+}
+.mdi-phone-ring::before {
+ content: "\F01D6";
+}
+.mdi-phone-ring-outline::before {
+ content: "\F01D7";
+}
+.mdi-phone-rotate-landscape::before {
+ content: "\F884";
+}
+.mdi-phone-rotate-portrait::before {
+ content: "\F885";
+}
+.mdi-phone-settings::before {
+ content: "\F3FD";
+}
+.mdi-phone-settings-outline::before {
+ content: "\F01C8";
+}
+.mdi-phone-voip::before {
+ content: "\F3FE";
+}
+.mdi-pi::before {
+ content: "\F3FF";
+}
+.mdi-pi-box::before {
+ content: "\F400";
+}
+.mdi-pi-hole::before {
+ content: "\FDCD";
+}
+.mdi-piano::before {
+ content: "\F67C";
+}
+.mdi-pickaxe::before {
+ content: "\F8B6";
+}
+.mdi-picture-in-picture-bottom-right::before {
+ content: "\FE3A";
+}
+.mdi-picture-in-picture-bottom-right-outline::before {
+ content: "\FE3B";
+}
+.mdi-picture-in-picture-top-right::before {
+ content: "\FE3C";
+}
+.mdi-picture-in-picture-top-right-outline::before {
+ content: "\FE3D";
+}
+.mdi-pier::before {
+ content: "\F886";
+}
+.mdi-pier-crane::before {
+ content: "\F887";
+}
+.mdi-pig::before {
+ content: "\F401";
+}
+.mdi-pig-variant::before {
+ content: "\F0028";
+}
+.mdi-piggy-bank::before {
+ content: "\F0029";
+}
+.mdi-pill::before {
+ content: "\F402";
+}
+.mdi-pillar::before {
+ content: "\F701";
+}
+.mdi-pin::before {
+ content: "\F403";
+}
+.mdi-pin-off::before {
+ content: "\F404";
+}
+.mdi-pin-off-outline::before {
+ content: "\F92F";
+}
+.mdi-pin-outline::before {
+ content: "\F930";
+}
+.mdi-pine-tree::before {
+ content: "\F405";
+}
+.mdi-pine-tree-box::before {
+ content: "\F406";
+}
+.mdi-pinterest::before {
+ content: "\F407";
+}
+.mdi-pinterest-box::before {
+ content: "\F408";
+}
+.mdi-pinwheel::before {
+ content: "\FAD4";
+}
+.mdi-pinwheel-outline::before {
+ content: "\FAD5";
+}
+.mdi-pipe::before {
+ content: "\F7E4";
+}
+.mdi-pipe-disconnected::before {
+ content: "\F7E5";
+}
+.mdi-pipe-leak::before {
+ content: "\F888";
+}
+.mdi-pipe-wrench::before {
+ content: "\F037F";
+}
+.mdi-pirate::before {
+ content: "\FA07";
+}
+.mdi-pistol::before {
+ content: "\F702";
+}
+.mdi-piston::before {
+ content: "\F889";
+}
+.mdi-pizza::before {
+ content: "\F409";
+}
+.mdi-play::before {
+ content: "\F40A";
+}
+.mdi-play-box::before {
+ content: "\F02A5";
+}
+.mdi-play-box-outline::before {
+ content: "\F40B";
+}
+.mdi-play-circle::before {
+ content: "\F40C";
+}
+.mdi-play-circle-outline::before {
+ content: "\F40D";
+}
+.mdi-play-network::before {
+ content: "\F88A";
+}
+.mdi-play-network-outline::before {
+ content: "\FC93";
+}
+.mdi-play-outline::before {
+ content: "\FF38";
+}
+.mdi-play-pause::before {
+ content: "\F40E";
+}
+.mdi-play-protected-content::before {
+ content: "\F40F";
+}
+.mdi-play-speed::before {
+ content: "\F8FE";
+}
+.mdi-playlist-check::before {
+ content: "\F5C7";
+}
+.mdi-playlist-edit::before {
+ content: "\F8FF";
+}
+.mdi-playlist-minus::before {
+ content: "\F410";
+}
+.mdi-playlist-music::before {
+ content: "\FC94";
+}
+.mdi-playlist-music-outline::before {
+ content: "\FC95";
+}
+.mdi-playlist-play::before {
+ content: "\F411";
+}
+.mdi-playlist-plus::before {
+ content: "\F412";
+}
+.mdi-playlist-remove::before {
+ content: "\F413";
+}
+.mdi-playlist-star::before {
+ content: "\FDCE";
+}
+.mdi-playstation::before {
+ content: "\F414";
+}
+.mdi-plex::before {
+ content: "\F6B9";
+}
+.mdi-plus::before {
+ content: "\F415";
+}
+.mdi-plus-box::before {
+ content: "\F416";
+}
+.mdi-plus-box-multiple::before {
+ content: "\F334";
+}
+.mdi-plus-box-multiple-outline::before {
+ content: "\F016E";
+}
+.mdi-plus-box-outline::before {
+ content: "\F703";
+}
+.mdi-plus-circle::before {
+ content: "\F417";
+}
+.mdi-plus-circle-multiple-outline::before {
+ content: "\F418";
+}
+.mdi-plus-circle-outline::before {
+ content: "\F419";
+}
+.mdi-plus-minus::before {
+ content: "\F991";
+}
+.mdi-plus-minus-box::before {
+ content: "\F992";
+}
+.mdi-plus-network::before {
+ content: "\F41A";
+}
+.mdi-plus-network-outline::before {
+ content: "\FC96";
+}
+.mdi-plus-one::before {
+ content: "\F41B";
+}
+.mdi-plus-outline::before {
+ content: "\F704";
+}
+.mdi-plus-thick::before {
+ content: "\F0217";
+}
+.mdi-pocket::before {
+ content: "\F41C";
+}
+.mdi-podcast::before {
+ content: "\F993";
+}
+.mdi-podium::before {
+ content: "\FD01";
+}
+.mdi-podium-bronze::before {
+ content: "\FD02";
+}
+.mdi-podium-gold::before {
+ content: "\FD03";
+}
+.mdi-podium-silver::before {
+ content: "\FD04";
+}
+.mdi-point-of-sale::before {
+ content: "\FD6E";
+}
+.mdi-pokeball::before {
+ content: "\F41D";
+}
+.mdi-pokemon-go::before {
+ content: "\FA08";
+}
+.mdi-poker-chip::before {
+ content: "\F82F";
+}
+.mdi-polaroid::before {
+ content: "\F41E";
+}
+.mdi-police-badge::before {
+ content: "\F0192";
+}
+.mdi-police-badge-outline::before {
+ content: "\F0193";
+}
+.mdi-poll::before {
+ content: "\F41F";
+}
+.mdi-poll-box::before {
+ content: "\F420";
+}
+.mdi-poll-box-outline::before {
+ content: "\F02A6";
+}
+.mdi-polymer::before {
+ content: "\F421";
+}
+.mdi-pool::before {
+ content: "\F606";
+}
+.mdi-popcorn::before {
+ content: "\F422";
+}
+.mdi-post::before {
+ content: "\F002A";
+}
+.mdi-post-outline::before {
+ content: "\F002B";
+}
+.mdi-postage-stamp::before {
+ content: "\FC97";
+}
+.mdi-pot::before {
+ content: "\F65A";
+}
+.mdi-pot-mix::before {
+ content: "\F65B";
+}
+.mdi-pound::before {
+ content: "\F423";
+}
+.mdi-pound-box::before {
+ content: "\F424";
+}
+.mdi-pound-box-outline::before {
+ content: "\F01AA";
+}
+.mdi-power::before {
+ content: "\F425";
+}
+.mdi-power-cycle::before {
+ content: "\F900";
+}
+.mdi-power-off::before {
+ content: "\F901";
+}
+.mdi-power-on::before {
+ content: "\F902";
+}
+.mdi-power-plug::before {
+ content: "\F6A4";
+}
+.mdi-power-plug-off::before {
+ content: "\F6A5";
+}
+.mdi-power-settings::before {
+ content: "\F426";
+}
+.mdi-power-sleep::before {
+ content: "\F903";
+}
+.mdi-power-socket::before {
+ content: "\F427";
+}
+.mdi-power-socket-au::before {
+ content: "\F904";
+}
+.mdi-power-socket-de::before {
+ content: "\F0132";
+}
+.mdi-power-socket-eu::before {
+ content: "\F7E6";
+}
+.mdi-power-socket-fr::before {
+ content: "\F0133";
+}
+.mdi-power-socket-jp::before {
+ content: "\F0134";
+}
+.mdi-power-socket-uk::before {
+ content: "\F7E7";
+}
+.mdi-power-socket-us::before {
+ content: "\F7E8";
+}
+.mdi-power-standby::before {
+ content: "\F905";
+}
+.mdi-powershell::before {
+ content: "\FA09";
+}
+.mdi-prescription::before {
+ content: "\F705";
+}
+.mdi-presentation::before {
+ content: "\F428";
+}
+.mdi-presentation-play::before {
+ content: "\F429";
+}
+.mdi-printer::before {
+ content: "\F42A";
+}
+.mdi-printer-3d::before {
+ content: "\F42B";
+}
+.mdi-printer-3d-nozzle::before {
+ content: "\FE3E";
+}
+.mdi-printer-3d-nozzle-alert::before {
+ content: "\F01EB";
+}
+.mdi-printer-3d-nozzle-alert-outline::before {
+ content: "\F01EC";
+}
+.mdi-printer-3d-nozzle-outline::before {
+ content: "\FE3F";
+}
+.mdi-printer-alert::before {
+ content: "\F42C";
+}
+.mdi-printer-check::before {
+ content: "\F0171";
+}
+.mdi-printer-off::before {
+ content: "\FE40";
+}
+.mdi-printer-pos::before {
+ content: "\F0079";
+}
+.mdi-printer-settings::before {
+ content: "\F706";
+}
+.mdi-printer-wireless::before {
+ content: "\FA0A";
+}
+.mdi-priority-high::before {
+ content: "\F603";
+}
+.mdi-priority-low::before {
+ content: "\F604";
+}
+.mdi-professional-hexagon::before {
+ content: "\F42D";
+}
+.mdi-progress-alert::before {
+ content: "\FC98";
+}
+.mdi-progress-check::before {
+ content: "\F994";
+}
+.mdi-progress-clock::before {
+ content: "\F995";
+}
+.mdi-progress-close::before {
+ content: "\F0135";
+}
+.mdi-progress-download::before {
+ content: "\F996";
+}
+.mdi-progress-upload::before {
+ content: "\F997";
+}
+.mdi-progress-wrench::before {
+ content: "\FC99";
+}
+.mdi-projector::before {
+ content: "\F42E";
+}
+.mdi-projector-screen::before {
+ content: "\F42F";
+}
+.mdi-propane-tank::before {
+ content: "\F0382";
+}
+.mdi-propane-tank-outline::before {
+ content: "\F0383";
+}
+.mdi-protocol::before {
+ content: "\FFF9";
+}
+.mdi-publish::before {
+ content: "\F6A6";
+}
+.mdi-pulse::before {
+ content: "\F430";
+}
+.mdi-pumpkin::before {
+ content: "\FB9B";
+}
+.mdi-purse::before {
+ content: "\FF39";
+}
+.mdi-purse-outline::before {
+ content: "\FF3A";
+}
+.mdi-puzzle::before {
+ content: "\F431";
+}
+.mdi-puzzle-outline::before {
+ content: "\FA65";
+}
+.mdi-qi::before {
+ content: "\F998";
+}
+.mdi-qqchat::before {
+ content: "\F605";
+}
+.mdi-qrcode::before {
+ content: "\F432";
+}
+.mdi-qrcode-edit::before {
+ content: "\F8B7";
+}
+.mdi-qrcode-minus::before {
+ content: "\F01B7";
+}
+.mdi-qrcode-plus::before {
+ content: "\F01B6";
+}
+.mdi-qrcode-remove::before {
+ content: "\F01B8";
+}
+.mdi-qrcode-scan::before {
+ content: "\F433";
+}
+.mdi-quadcopter::before {
+ content: "\F434";
+}
+.mdi-quality-high::before {
+ content: "\F435";
+}
+.mdi-quality-low::before {
+ content: "\FA0B";
+}
+.mdi-quality-medium::before {
+ content: "\FA0C";
+}
+.mdi-quicktime::before {
+ content: "\F436";
+}
+.mdi-quora::before {
+ content: "\FD05";
+}
+.mdi-rabbit::before {
+ content: "\F906";
+}
+.mdi-racing-helmet::before {
+ content: "\FD6F";
+}
+.mdi-racquetball::before {
+ content: "\FD70";
+}
+.mdi-radar::before {
+ content: "\F437";
+}
+.mdi-radiator::before {
+ content: "\F438";
+}
+.mdi-radiator-disabled::before {
+ content: "\FAD6";
+}
+.mdi-radiator-off::before {
+ content: "\FAD7";
+}
+.mdi-radio::before {
+ content: "\F439";
+}
+.mdi-radio-am::before {
+ content: "\FC9A";
+}
+.mdi-radio-fm::before {
+ content: "\FC9B";
+}
+.mdi-radio-handheld::before {
+ content: "\F43A";
+}
+.mdi-radio-off::before {
+ content: "\F0247";
+}
+.mdi-radio-tower::before {
+ content: "\F43B";
+}
+.mdi-radioactive::before {
+ content: "\F43C";
+}
+.mdi-radioactive-off::before {
+ content: "\FEDE";
+}
+.mdi-radiobox-blank::before {
+ content: "\F43D";
+}
+.mdi-radiobox-marked::before {
+ content: "\F43E";
+}
+.mdi-radius::before {
+ content: "\FC9C";
+}
+.mdi-radius-outline::before {
+ content: "\FC9D";
+}
+.mdi-railroad-light::before {
+ content: "\FF3B";
+}
+.mdi-raspberry-pi::before {
+ content: "\F43F";
+}
+.mdi-ray-end::before {
+ content: "\F440";
+}
+.mdi-ray-end-arrow::before {
+ content: "\F441";
+}
+.mdi-ray-start::before {
+ content: "\F442";
+}
+.mdi-ray-start-arrow::before {
+ content: "\F443";
+}
+.mdi-ray-start-end::before {
+ content: "\F444";
+}
+.mdi-ray-vertex::before {
+ content: "\F445";
+}
+.mdi-react::before {
+ content: "\F707";
+}
+.mdi-read::before {
+ content: "\F447";
+}
+.mdi-receipt::before {
+ content: "\F449";
+}
+.mdi-record::before {
+ content: "\F44A";
+}
+.mdi-record-circle::before {
+ content: "\FEDF";
+}
+.mdi-record-circle-outline::before {
+ content: "\FEE0";
+}
+.mdi-record-player::before {
+ content: "\F999";
+}
+.mdi-record-rec::before {
+ content: "\F44B";
+}
+.mdi-rectangle::before {
+ content: "\FE41";
+}
+.mdi-rectangle-outline::before {
+ content: "\FE42";
+}
+.mdi-recycle::before {
+ content: "\F44C";
+}
+.mdi-reddit::before {
+ content: "\F44D";
+}
+.mdi-redhat::before {
+ content: "\F0146";
+}
+.mdi-redo::before {
+ content: "\F44E";
+}
+.mdi-redo-variant::before {
+ content: "\F44F";
+}
+.mdi-reflect-horizontal::before {
+ content: "\FA0D";
+}
+.mdi-reflect-vertical::before {
+ content: "\FA0E";
+}
+.mdi-refresh::before {
+ content: "\F450";
+}
+.mdi-refresh-circle::before {
+ content: "\F03A2";
+}
+.mdi-regex::before {
+ content: "\F451";
+}
+.mdi-registered-trademark::before {
+ content: "\FA66";
+}
+.mdi-relative-scale::before {
+ content: "\F452";
+}
+.mdi-reload::before {
+ content: "\F453";
+}
+.mdi-reload-alert::before {
+ content: "\F0136";
+}
+.mdi-reminder::before {
+ content: "\F88B";
+}
+.mdi-remote::before {
+ content: "\F454";
+}
+.mdi-remote-desktop::before {
+ content: "\F8B8";
+}
+.mdi-remote-off::before {
+ content: "\FEE1";
+}
+.mdi-remote-tv::before {
+ content: "\FEE2";
+}
+.mdi-remote-tv-off::before {
+ content: "\FEE3";
+}
+.mdi-rename-box::before {
+ content: "\F455";
+}
+.mdi-reorder-horizontal::before {
+ content: "\F687";
+}
+.mdi-reorder-vertical::before {
+ content: "\F688";
+}
+.mdi-repeat::before {
+ content: "\F456";
+}
+.mdi-repeat-off::before {
+ content: "\F457";
+}
+.mdi-repeat-once::before {
+ content: "\F458";
+}
+.mdi-replay::before {
+ content: "\F459";
+}
+.mdi-reply::before {
+ content: "\F45A";
+}
+.mdi-reply-all::before {
+ content: "\F45B";
+}
+.mdi-reply-all-outline::before {
+ content: "\FF3C";
+}
+.mdi-reply-circle::before {
+ content: "\F01D9";
+}
+.mdi-reply-outline::before {
+ content: "\FF3D";
+}
+.mdi-reproduction::before {
+ content: "\F45C";
+}
+.mdi-resistor::before {
+ content: "\FB1F";
+}
+.mdi-resistor-nodes::before {
+ content: "\FB20";
+}
+.mdi-resize::before {
+ content: "\FA67";
+}
+.mdi-resize-bottom-right::before {
+ content: "\F45D";
+}
+.mdi-responsive::before {
+ content: "\F45E";
+}
+.mdi-restart::before {
+ content: "\F708";
+}
+.mdi-restart-alert::before {
+ content: "\F0137";
+}
+.mdi-restart-off::before {
+ content: "\FD71";
+}
+.mdi-restore::before {
+ content: "\F99A";
+}
+.mdi-restore-alert::before {
+ content: "\F0138";
+}
+.mdi-rewind::before {
+ content: "\F45F";
+}
+.mdi-rewind-10::before {
+ content: "\FD06";
+}
+.mdi-rewind-30::before {
+ content: "\FD72";
+}
+.mdi-rewind-5::before {
+ content: "\F0224";
+}
+.mdi-rewind-outline::before {
+ content: "\F709";
+}
+.mdi-rhombus::before {
+ content: "\F70A";
+}
+.mdi-rhombus-medium::before {
+ content: "\FA0F";
+}
+.mdi-rhombus-outline::before {
+ content: "\F70B";
+}
+.mdi-rhombus-split::before {
+ content: "\FA10";
+}
+.mdi-ribbon::before {
+ content: "\F460";
+}
+.mdi-rice::before {
+ content: "\F7E9";
+}
+.mdi-ring::before {
+ content: "\F7EA";
+}
+.mdi-rivet::before {
+ content: "\FE43";
+}
+.mdi-road::before {
+ content: "\F461";
+}
+.mdi-road-variant::before {
+ content: "\F462";
+}
+.mdi-robber::before {
+ content: "\F007A";
+}
+.mdi-robot::before {
+ content: "\F6A8";
+}
+.mdi-robot-industrial::before {
+ content: "\FB21";
+}
+.mdi-robot-mower::before {
+ content: "\F0222";
+}
+.mdi-robot-mower-outline::before {
+ content: "\F021E";
+}
+.mdi-robot-vacuum::before {
+ content: "\F70C";
+}
+.mdi-robot-vacuum-variant::before {
+ content: "\F907";
+}
+.mdi-rocket::before {
+ content: "\F463";
+}
+.mdi-rodent::before {
+ content: "\F0352";
+}
+.mdi-roller-skate::before {
+ content: "\FD07";
+}
+.mdi-rollerblade::before {
+ content: "\FD08";
+}
+.mdi-rollupjs::before {
+ content: "\FB9C";
+}
+.mdi-roman-numeral-1::before {
+ content: "\F00B3";
+}
+.mdi-roman-numeral-10::before {
+ content: "\F00BC";
+}
+.mdi-roman-numeral-2::before {
+ content: "\F00B4";
+}
+.mdi-roman-numeral-3::before {
+ content: "\F00B5";
+}
+.mdi-roman-numeral-4::before {
+ content: "\F00B6";
+}
+.mdi-roman-numeral-5::before {
+ content: "\F00B7";
+}
+.mdi-roman-numeral-6::before {
+ content: "\F00B8";
+}
+.mdi-roman-numeral-7::before {
+ content: "\F00B9";
+}
+.mdi-roman-numeral-8::before {
+ content: "\F00BA";
+}
+.mdi-roman-numeral-9::before {
+ content: "\F00BB";
+}
+.mdi-room-service::before {
+ content: "\F88C";
+}
+.mdi-room-service-outline::before {
+ content: "\FD73";
+}
+.mdi-rotate-3d::before {
+ content: "\FEE4";
+}
+.mdi-rotate-3d-variant::before {
+ content: "\F464";
+}
+.mdi-rotate-left::before {
+ content: "\F465";
+}
+.mdi-rotate-left-variant::before {
+ content: "\F466";
+}
+.mdi-rotate-orbit::before {
+ content: "\FD74";
+}
+.mdi-rotate-right::before {
+ content: "\F467";
+}
+.mdi-rotate-right-variant::before {
+ content: "\F468";
+}
+.mdi-rounded-corner::before {
+ content: "\F607";
+}
+.mdi-router::before {
+ content: "\F020D";
+}
+.mdi-router-wireless::before {
+ content: "\F469";
+}
+.mdi-router-wireless-settings::before {
+ content: "\FA68";
+}
+.mdi-routes::before {
+ content: "\F46A";
+}
+.mdi-routes-clock::before {
+ content: "\F007B";
+}
+.mdi-rowing::before {
+ content: "\F608";
+}
+.mdi-rss::before {
+ content: "\F46B";
+}
+.mdi-rss-box::before {
+ content: "\F46C";
+}
+.mdi-rss-off::before {
+ content: "\FF3E";
+}
+.mdi-ruby::before {
+ content: "\FD09";
+}
+.mdi-rugby::before {
+ content: "\FD75";
+}
+.mdi-ruler::before {
+ content: "\F46D";
+}
+.mdi-ruler-square::before {
+ content: "\FC9E";
+}
+.mdi-ruler-square-compass::before {
+ content: "\FEDB";
+}
+.mdi-run::before {
+ content: "\F70D";
+}
+.mdi-run-fast::before {
+ content: "\F46E";
+}
+.mdi-rv-truck::before {
+ content: "\F01FF";
+}
+.mdi-sack::before {
+ content: "\FD0A";
+}
+.mdi-sack-percent::before {
+ content: "\FD0B";
+}
+.mdi-safe::before {
+ content: "\FA69";
+}
+.mdi-safe-square::before {
+ content: "\F02A7";
+}
+.mdi-safe-square-outline::before {
+ content: "\F02A8";
+}
+.mdi-safety-goggles::before {
+ content: "\FD0C";
+}
+.mdi-sailing::before {
+ content: "\FEE5";
+}
+.mdi-sale::before {
+ content: "\F46F";
+}
+.mdi-salesforce::before {
+ content: "\F88D";
+}
+.mdi-sass::before {
+ content: "\F7EB";
+}
+.mdi-satellite::before {
+ content: "\F470";
+}
+.mdi-satellite-uplink::before {
+ content: "\F908";
+}
+.mdi-satellite-variant::before {
+ content: "\F471";
+}
+.mdi-sausage::before {
+ content: "\F8B9";
+}
+.mdi-saw-blade::before {
+ content: "\FE44";
+}
+.mdi-saxophone::before {
+ content: "\F609";
+}
+.mdi-scale::before {
+ content: "\F472";
+}
+.mdi-scale-balance::before {
+ content: "\F5D1";
+}
+.mdi-scale-bathroom::before {
+ content: "\F473";
+}
+.mdi-scale-off::before {
+ content: "\F007C";
+}
+.mdi-scanner::before {
+ content: "\F6AA";
+}
+.mdi-scanner-off::before {
+ content: "\F909";
+}
+.mdi-scatter-plot::before {
+ content: "\FEE6";
+}
+.mdi-scatter-plot-outline::before {
+ content: "\FEE7";
+}
+.mdi-school::before {
+ content: "\F474";
+}
+.mdi-school-outline::before {
+ content: "\F01AB";
+}
+.mdi-scissors-cutting::before {
+ content: "\FA6A";
+}
+.mdi-scooter::before {
+ content: "\F0214";
+}
+.mdi-scoreboard::before {
+ content: "\F02A9";
+}
+.mdi-scoreboard-outline::before {
+ content: "\F02AA";
+}
+.mdi-screen-rotation::before {
+ content: "\F475";
+}
+.mdi-screen-rotation-lock::before {
+ content: "\F476";
+}
+.mdi-screw-flat-top::before {
+ content: "\FDCF";
+}
+.mdi-screw-lag::before {
+ content: "\FE54";
+}
+.mdi-screw-machine-flat-top::before {
+ content: "\FE55";
+}
+.mdi-screw-machine-round-top::before {
+ content: "\FE56";
+}
+.mdi-screw-round-top::before {
+ content: "\FE57";
+}
+.mdi-screwdriver::before {
+ content: "\F477";
+}
+.mdi-script::before {
+ content: "\FB9D";
+}
+.mdi-script-outline::before {
+ content: "\F478";
+}
+.mdi-script-text::before {
+ content: "\FB9E";
+}
+.mdi-script-text-outline::before {
+ content: "\FB9F";
+}
+.mdi-sd::before {
+ content: "\F479";
+}
+.mdi-seal::before {
+ content: "\F47A";
+}
+.mdi-seal-variant::before {
+ content: "\FFFA";
+}
+.mdi-search-web::before {
+ content: "\F70E";
+}
+.mdi-seat::before {
+ content: "\FC9F";
+}
+.mdi-seat-flat::before {
+ content: "\F47B";
+}
+.mdi-seat-flat-angled::before {
+ content: "\F47C";
+}
+.mdi-seat-individual-suite::before {
+ content: "\F47D";
+}
+.mdi-seat-legroom-extra::before {
+ content: "\F47E";
+}
+.mdi-seat-legroom-normal::before {
+ content: "\F47F";
+}
+.mdi-seat-legroom-reduced::before {
+ content: "\F480";
+}
+.mdi-seat-outline::before {
+ content: "\FCA0";
+}
+.mdi-seat-passenger::before {
+ content: "\F0274";
+}
+.mdi-seat-recline-extra::before {
+ content: "\F481";
+}
+.mdi-seat-recline-normal::before {
+ content: "\F482";
+}
+.mdi-seatbelt::before {
+ content: "\FCA1";
+}
+.mdi-security::before {
+ content: "\F483";
+}
+.mdi-security-network::before {
+ content: "\F484";
+}
+.mdi-seed::before {
+ content: "\FE45";
+}
+.mdi-seed-outline::before {
+ content: "\FE46";
+}
+.mdi-segment::before {
+ content: "\FEE8";
+}
+.mdi-select::before {
+ content: "\F485";
+}
+.mdi-select-all::before {
+ content: "\F486";
+}
+.mdi-select-color::before {
+ content: "\FD0D";
+}
+.mdi-select-compare::before {
+ content: "\FAD8";
+}
+.mdi-select-drag::before {
+ content: "\FA6B";
+}
+.mdi-select-group::before {
+ content: "\FF9F";
+}
+.mdi-select-inverse::before {
+ content: "\F487";
+}
+.mdi-select-marker::before {
+ content: "\F02AB";
+}
+.mdi-select-multiple::before {
+ content: "\F02AC";
+}
+.mdi-select-multiple-marker::before {
+ content: "\F02AD";
+}
+.mdi-select-off::before {
+ content: "\F488";
+}
+.mdi-select-place::before {
+ content: "\FFFB";
+}
+.mdi-select-search::before {
+ content: "\F022F";
+}
+.mdi-selection::before {
+ content: "\F489";
+}
+.mdi-selection-drag::before {
+ content: "\FA6C";
+}
+.mdi-selection-ellipse::before {
+ content: "\FD0E";
+}
+.mdi-selection-ellipse-arrow-inside::before {
+ content: "\FF3F";
+}
+.mdi-selection-marker::before {
+ content: "\F02AE";
+}
+.mdi-selection-multiple-marker::before {
+ content: "\F02AF";
+}
+.mdi-selection-mutliple::before {
+ content: "\F02B0";
+}
+.mdi-selection-off::before {
+ content: "\F776";
+}
+.mdi-selection-search::before {
+ content: "\F0230";
+}
+.mdi-semantic-web::before {
+ content: "\F0341";
+}
+.mdi-send::before {
+ content: "\F48A";
+}
+.mdi-send-check::before {
+ content: "\F018C";
+}
+.mdi-send-check-outline::before {
+ content: "\F018D";
+}
+.mdi-send-circle::before {
+ content: "\FE58";
+}
+.mdi-send-circle-outline::before {
+ content: "\FE59";
+}
+.mdi-send-clock::before {
+ content: "\F018E";
+}
+.mdi-send-clock-outline::before {
+ content: "\F018F";
+}
+.mdi-send-lock::before {
+ content: "\F7EC";
+}
+.mdi-send-lock-outline::before {
+ content: "\F0191";
+}
+.mdi-send-outline::before {
+ content: "\F0190";
+}
+.mdi-serial-port::before {
+ content: "\F65C";
+}
+.mdi-server::before {
+ content: "\F48B";
+}
+.mdi-server-minus::before {
+ content: "\F48C";
+}
+.mdi-server-network::before {
+ content: "\F48D";
+}
+.mdi-server-network-off::before {
+ content: "\F48E";
+}
+.mdi-server-off::before {
+ content: "\F48F";
+}
+.mdi-server-plus::before {
+ content: "\F490";
+}
+.mdi-server-remove::before {
+ content: "\F491";
+}
+.mdi-server-security::before {
+ content: "\F492";
+}
+.mdi-set-all::before {
+ content: "\F777";
+}
+.mdi-set-center::before {
+ content: "\F778";
+}
+.mdi-set-center-right::before {
+ content: "\F779";
+}
+.mdi-set-left::before {
+ content: "\F77A";
+}
+.mdi-set-left-center::before {
+ content: "\F77B";
+}
+.mdi-set-left-right::before {
+ content: "\F77C";
+}
+.mdi-set-none::before {
+ content: "\F77D";
+}
+.mdi-set-right::before {
+ content: "\F77E";
+}
+.mdi-set-top-box::before {
+ content: "\F99E";
+}
+.mdi-settings::before {
+ content: "\F493";
+}
+.mdi-settings-box::before {
+ content: "\F494";
+}
+.mdi-settings-helper::before {
+ content: "\FA6D";
+}
+.mdi-settings-outline::before {
+ content: "\F8BA";
+}
+.mdi-settings-transfer::before {
+ content: "\F007D";
+}
+.mdi-settings-transfer-outline::before {
+ content: "\F007E";
+}
+.mdi-shaker::before {
+ content: "\F0139";
+}
+.mdi-shaker-outline::before {
+ content: "\F013A";
+}
+.mdi-shape::before {
+ content: "\F830";
+}
+.mdi-shape-circle-plus::before {
+ content: "\F65D";
+}
+.mdi-shape-outline::before {
+ content: "\F831";
+}
+.mdi-shape-oval-plus::before {
+ content: "\F0225";
+}
+.mdi-shape-plus::before {
+ content: "\F495";
+}
+.mdi-shape-polygon-plus::before {
+ content: "\F65E";
+}
+.mdi-shape-rectangle-plus::before {
+ content: "\F65F";
+}
+.mdi-shape-square-plus::before {
+ content: "\F660";
+}
+.mdi-share::before {
+ content: "\F496";
+}
+.mdi-share-all::before {
+ content: "\F021F";
+}
+.mdi-share-all-outline::before {
+ content: "\F0220";
+}
+.mdi-share-circle::before {
+ content: "\F01D8";
+}
+.mdi-share-off::before {
+ content: "\FF40";
+}
+.mdi-share-off-outline::before {
+ content: "\FF41";
+}
+.mdi-share-outline::before {
+ content: "\F931";
+}
+.mdi-share-variant::before {
+ content: "\F497";
+}
+.mdi-sheep::before {
+ content: "\FCA2";
+}
+.mdi-shield::before {
+ content: "\F498";
+}
+.mdi-shield-account::before {
+ content: "\F88E";
+}
+.mdi-shield-account-outline::before {
+ content: "\FA11";
+}
+.mdi-shield-airplane::before {
+ content: "\F6BA";
+}
+.mdi-shield-airplane-outline::before {
+ content: "\FCA3";
+}
+.mdi-shield-alert::before {
+ content: "\FEE9";
+}
+.mdi-shield-alert-outline::before {
+ content: "\FEEA";
+}
+.mdi-shield-car::before {
+ content: "\FFA0";
+}
+.mdi-shield-check::before {
+ content: "\F565";
+}
+.mdi-shield-check-outline::before {
+ content: "\FCA4";
+}
+.mdi-shield-cross::before {
+ content: "\FCA5";
+}
+.mdi-shield-cross-outline::before {
+ content: "\FCA6";
+}
+.mdi-shield-edit::before {
+ content: "\F01CB";
+}
+.mdi-shield-edit-outline::before {
+ content: "\F01CC";
+}
+.mdi-shield-half::before {
+ content: "\F038B";
+}
+.mdi-shield-half-full::before {
+ content: "\F77F";
+}
+.mdi-shield-home::before {
+ content: "\F689";
+}
+.mdi-shield-home-outline::before {
+ content: "\FCA7";
+}
+.mdi-shield-key::before {
+ content: "\FBA0";
+}
+.mdi-shield-key-outline::before {
+ content: "\FBA1";
+}
+.mdi-shield-link-variant::before {
+ content: "\FD0F";
+}
+.mdi-shield-link-variant-outline::before {
+ content: "\FD10";
+}
+.mdi-shield-lock::before {
+ content: "\F99C";
+}
+.mdi-shield-lock-outline::before {
+ content: "\FCA8";
+}
+.mdi-shield-off::before {
+ content: "\F99D";
+}
+.mdi-shield-off-outline::before {
+ content: "\F99B";
+}
+.mdi-shield-outline::before {
+ content: "\F499";
+}
+.mdi-shield-plus::before {
+ content: "\FAD9";
+}
+.mdi-shield-plus-outline::before {
+ content: "\FADA";
+}
+.mdi-shield-refresh::before {
+ content: "\F01CD";
+}
+.mdi-shield-refresh-outline::before {
+ content: "\F01CE";
+}
+.mdi-shield-remove::before {
+ content: "\FADB";
+}
+.mdi-shield-remove-outline::before {
+ content: "\FADC";
+}
+.mdi-shield-search::before {
+ content: "\FD76";
+}
+.mdi-shield-star::before {
+ content: "\F0166";
+}
+.mdi-shield-star-outline::before {
+ content: "\F0167";
+}
+.mdi-shield-sun::before {
+ content: "\F007F";
+}
+.mdi-shield-sun-outline::before {
+ content: "\F0080";
+}
+.mdi-ship-wheel::before {
+ content: "\F832";
+}
+.mdi-shoe-formal::before {
+ content: "\FB22";
+}
+.mdi-shoe-heel::before {
+ content: "\FB23";
+}
+.mdi-shoe-print::before {
+ content: "\FE5A";
+}
+.mdi-shopify::before {
+ content: "\FADD";
+}
+.mdi-shopping::before {
+ content: "\F49A";
+}
+.mdi-shopping-music::before {
+ content: "\F49B";
+}
+.mdi-shopping-outline::before {
+ content: "\F0200";
+}
+.mdi-shopping-search::before {
+ content: "\FFA1";
+}
+.mdi-shovel::before {
+ content: "\F70F";
+}
+.mdi-shovel-off::before {
+ content: "\F710";
+}
+.mdi-shower::before {
+ content: "\F99F";
+}
+.mdi-shower-head::before {
+ content: "\F9A0";
+}
+.mdi-shredder::before {
+ content: "\F49C";
+}
+.mdi-shuffle::before {
+ content: "\F49D";
+}
+.mdi-shuffle-disabled::before {
+ content: "\F49E";
+}
+.mdi-shuffle-variant::before {
+ content: "\F49F";
+}
+.mdi-shuriken::before {
+ content: "\F03AA";
+}
+.mdi-sigma::before {
+ content: "\F4A0";
+}
+.mdi-sigma-lower::before {
+ content: "\F62B";
+}
+.mdi-sign-caution::before {
+ content: "\F4A1";
+}
+.mdi-sign-direction::before {
+ content: "\F780";
+}
+.mdi-sign-direction-minus::before {
+ content: "\F0022";
+}
+.mdi-sign-direction-plus::before {
+ content: "\FFFD";
+}
+.mdi-sign-direction-remove::before {
+ content: "\FFFE";
+}
+.mdi-sign-real-estate::before {
+ content: "\F0143";
+}
+.mdi-sign-text::before {
+ content: "\F781";
+}
+.mdi-signal::before {
+ content: "\F4A2";
+}
+.mdi-signal-2g::before {
+ content: "\F711";
+}
+.mdi-signal-3g::before {
+ content: "\F712";
+}
+.mdi-signal-4g::before {
+ content: "\F713";
+}
+.mdi-signal-5g::before {
+ content: "\FA6E";
+}
+.mdi-signal-cellular-1::before {
+ content: "\F8BB";
+}
+.mdi-signal-cellular-2::before {
+ content: "\F8BC";
+}
+.mdi-signal-cellular-3::before {
+ content: "\F8BD";
+}
+.mdi-signal-cellular-outline::before {
+ content: "\F8BE";
+}
+.mdi-signal-distance-variant::before {
+ content: "\FE47";
+}
+.mdi-signal-hspa::before {
+ content: "\F714";
+}
+.mdi-signal-hspa-plus::before {
+ content: "\F715";
+}
+.mdi-signal-off::before {
+ content: "\F782";
+}
+.mdi-signal-variant::before {
+ content: "\F60A";
+}
+.mdi-signature::before {
+ content: "\FE5B";
+}
+.mdi-signature-freehand::before {
+ content: "\FE5C";
+}
+.mdi-signature-image::before {
+ content: "\FE5D";
+}
+.mdi-signature-text::before {
+ content: "\FE5E";
+}
+.mdi-silo::before {
+ content: "\FB24";
+}
+.mdi-silverware::before {
+ content: "\F4A3";
+}
+.mdi-silverware-clean::before {
+ content: "\FFFF";
+}
+.mdi-silverware-fork::before {
+ content: "\F4A4";
+}
+.mdi-silverware-fork-knife::before {
+ content: "\FA6F";
+}
+.mdi-silverware-spoon::before {
+ content: "\F4A5";
+}
+.mdi-silverware-variant::before {
+ content: "\F4A6";
+}
+.mdi-sim::before {
+ content: "\F4A7";
+}
+.mdi-sim-alert::before {
+ content: "\F4A8";
+}
+.mdi-sim-off::before {
+ content: "\F4A9";
+}
+.mdi-simple-icons::before {
+ content: "\F0348";
+}
+.mdi-sina-weibo::before {
+ content: "\FADE";
+}
+.mdi-sitemap::before {
+ content: "\F4AA";
+}
+.mdi-skate::before {
+ content: "\FD11";
+}
+.mdi-skew-less::before {
+ content: "\FD12";
+}
+.mdi-skew-more::before {
+ content: "\FD13";
+}
+.mdi-ski::before {
+ content: "\F032F";
+}
+.mdi-ski-cross-country::before {
+ content: "\F0330";
+}
+.mdi-ski-water::before {
+ content: "\F0331";
+}
+.mdi-skip-backward::before {
+ content: "\F4AB";
+}
+.mdi-skip-backward-outline::before {
+ content: "\FF42";
+}
+.mdi-skip-forward::before {
+ content: "\F4AC";
+}
+.mdi-skip-forward-outline::before {
+ content: "\FF43";
+}
+.mdi-skip-next::before {
+ content: "\F4AD";
+}
+.mdi-skip-next-circle::before {
+ content: "\F661";
+}
+.mdi-skip-next-circle-outline::before {
+ content: "\F662";
+}
+.mdi-skip-next-outline::before {
+ content: "\FF44";
+}
+.mdi-skip-previous::before {
+ content: "\F4AE";
+}
+.mdi-skip-previous-circle::before {
+ content: "\F663";
+}
+.mdi-skip-previous-circle-outline::before {
+ content: "\F664";
+}
+.mdi-skip-previous-outline::before {
+ content: "\FF45";
+}
+.mdi-skull::before {
+ content: "\F68B";
+}
+.mdi-skull-crossbones::before {
+ content: "\FBA2";
+}
+.mdi-skull-crossbones-outline::before {
+ content: "\FBA3";
+}
+.mdi-skull-outline::before {
+ content: "\FBA4";
+}
+.mdi-skype::before {
+ content: "\F4AF";
+}
+.mdi-skype-business::before {
+ content: "\F4B0";
+}
+.mdi-slack::before {
+ content: "\F4B1";
+}
+.mdi-slackware::before {
+ content: "\F90A";
+}
+.mdi-slash-forward::before {
+ content: "\F0000";
+}
+.mdi-slash-forward-box::before {
+ content: "\F0001";
+}
+.mdi-sleep::before {
+ content: "\F4B2";
+}
+.mdi-sleep-off::before {
+ content: "\F4B3";
+}
+.mdi-slope-downhill::before {
+ content: "\FE5F";
+}
+.mdi-slope-uphill::before {
+ content: "\FE60";
+}
+.mdi-slot-machine::before {
+ content: "\F013F";
+}
+.mdi-slot-machine-outline::before {
+ content: "\F0140";
+}
+.mdi-smart-card::before {
+ content: "\F00E8";
+}
+.mdi-smart-card-outline::before {
+ content: "\F00E9";
+}
+.mdi-smart-card-reader::before {
+ content: "\F00EA";
+}
+.mdi-smart-card-reader-outline::before {
+ content: "\F00EB";
+}
+.mdi-smog::before {
+ content: "\FA70";
+}
+.mdi-smoke-detector::before {
+ content: "\F392";
+}
+.mdi-smoking::before {
+ content: "\F4B4";
+}
+.mdi-smoking-off::before {
+ content: "\F4B5";
+}
+.mdi-snapchat::before {
+ content: "\F4B6";
+}
+.mdi-snowboard::before {
+ content: "\F0332";
+}
+.mdi-snowflake::before {
+ content: "\F716";
+}
+.mdi-snowflake-alert::before {
+ content: "\FF46";
+}
+.mdi-snowflake-melt::before {
+ content: "\F02F6";
+}
+.mdi-snowflake-variant::before {
+ content: "\FF47";
+}
+.mdi-snowman::before {
+ content: "\F4B7";
+}
+.mdi-soccer::before {
+ content: "\F4B8";
+}
+.mdi-soccer-field::before {
+ content: "\F833";
+}
+.mdi-sofa::before {
+ content: "\F4B9";
+}
+.mdi-solar-panel::before {
+ content: "\FD77";
+}
+.mdi-solar-panel-large::before {
+ content: "\FD78";
+}
+.mdi-solar-power::before {
+ content: "\FA71";
+}
+.mdi-soldering-iron::before {
+ content: "\F00BD";
+}
+.mdi-solid::before {
+ content: "\F68C";
+}
+.mdi-sort::before {
+ content: "\F4BA";
+}
+.mdi-sort-alphabetical::before {
+ content: "\F4BB";
+}
+.mdi-sort-alphabetical-ascending::before {
+ content: "\F0173";
+}
+.mdi-sort-alphabetical-descending::before {
+ content: "\F0174";
+}
+.mdi-sort-ascending::before {
+ content: "\F4BC";
+}
+.mdi-sort-descending::before {
+ content: "\F4BD";
+}
+.mdi-sort-numeric::before {
+ content: "\F4BE";
+}
+.mdi-sort-variant::before {
+ content: "\F4BF";
+}
+.mdi-sort-variant-lock::before {
+ content: "\FCA9";
+}
+.mdi-sort-variant-lock-open::before {
+ content: "\FCAA";
+}
+.mdi-sort-variant-remove::before {
+ content: "\F0172";
+}
+.mdi-soundcloud::before {
+ content: "\F4C0";
+}
+.mdi-source-branch::before {
+ content: "\F62C";
+}
+.mdi-source-commit::before {
+ content: "\F717";
+}
+.mdi-source-commit-end::before {
+ content: "\F718";
+}
+.mdi-source-commit-end-local::before {
+ content: "\F719";
+}
+.mdi-source-commit-local::before {
+ content: "\F71A";
+}
+.mdi-source-commit-next-local::before {
+ content: "\F71B";
+}
+.mdi-source-commit-start::before {
+ content: "\F71C";
+}
+.mdi-source-commit-start-next-local::before {
+ content: "\F71D";
+}
+.mdi-source-fork::before {
+ content: "\F4C1";
+}
+.mdi-source-merge::before {
+ content: "\F62D";
+}
+.mdi-source-pull::before {
+ content: "\F4C2";
+}
+.mdi-source-repository::before {
+ content: "\FCAB";
+}
+.mdi-source-repository-multiple::before {
+ content: "\FCAC";
+}
+.mdi-soy-sauce::before {
+ content: "\F7ED";
+}
+.mdi-spa::before {
+ content: "\FCAD";
+}
+.mdi-spa-outline::before {
+ content: "\FCAE";
+}
+.mdi-space-invaders::before {
+ content: "\FBA5";
+}
+.mdi-space-station::before {
+ content: "\F03AE";
+}
+.mdi-spade::before {
+ content: "\FE48";
+}
+.mdi-speaker::before {
+ content: "\F4C3";
+}
+.mdi-speaker-bluetooth::before {
+ content: "\F9A1";
+}
+.mdi-speaker-multiple::before {
+ content: "\FD14";
+}
+.mdi-speaker-off::before {
+ content: "\F4C4";
+}
+.mdi-speaker-wireless::before {
+ content: "\F71E";
+}
+.mdi-speedometer::before {
+ content: "\F4C5";
+}
+.mdi-speedometer-medium::before {
+ content: "\FFA2";
+}
+.mdi-speedometer-slow::before {
+ content: "\FFA3";
+}
+.mdi-spellcheck::before {
+ content: "\F4C6";
+}
+.mdi-spider::before {
+ content: "\F0215";
+}
+.mdi-spider-thread::before {
+ content: "\F0216";
+}
+.mdi-spider-web::before {
+ content: "\FBA6";
+}
+.mdi-spotify::before {
+ content: "\F4C7";
+}
+.mdi-spotlight::before {
+ content: "\F4C8";
+}
+.mdi-spotlight-beam::before {
+ content: "\F4C9";
+}
+.mdi-spray::before {
+ content: "\F665";
+}
+.mdi-spray-bottle::before {
+ content: "\FADF";
+}
+.mdi-sprinkler::before {
+ content: "\F0081";
+}
+.mdi-sprinkler-variant::before {
+ content: "\F0082";
+}
+.mdi-sprout::before {
+ content: "\FE49";
+}
+.mdi-sprout-outline::before {
+ content: "\FE4A";
+}
+.mdi-square::before {
+ content: "\F763";
+}
+.mdi-square-edit-outline::before {
+ content: "\F90B";
+}
+.mdi-square-inc::before {
+ content: "\F4CA";
+}
+.mdi-square-inc-cash::before {
+ content: "\F4CB";
+}
+.mdi-square-medium::before {
+ content: "\FA12";
+}
+.mdi-square-medium-outline::before {
+ content: "\FA13";
+}
+.mdi-square-off::before {
+ content: "\F0319";
+}
+.mdi-square-off-outline::before {
+ content: "\F031A";
+}
+.mdi-square-outline::before {
+ content: "\F762";
+}
+.mdi-square-root::before {
+ content: "\F783";
+}
+.mdi-square-root-box::before {
+ content: "\F9A2";
+}
+.mdi-square-small::before {
+ content: "\FA14";
+}
+.mdi-squeegee::before {
+ content: "\FAE0";
+}
+.mdi-ssh::before {
+ content: "\F8BF";
+}
+.mdi-stack-exchange::before {
+ content: "\F60B";
+}
+.mdi-stack-overflow::before {
+ content: "\F4CC";
+}
+.mdi-stackpath::before {
+ content: "\F359";
+}
+.mdi-stadium::before {
+ content: "\F001A";
+}
+.mdi-stadium-variant::before {
+ content: "\F71F";
+}
+.mdi-stairs::before {
+ content: "\F4CD";
+}
+.mdi-stairs-down::before {
+ content: "\F02E9";
+}
+.mdi-stairs-up::before {
+ content: "\F02E8";
+}
+.mdi-stamper::before {
+ content: "\FD15";
+}
+.mdi-standard-definition::before {
+ content: "\F7EE";
+}
+.mdi-star::before {
+ content: "\F4CE";
+}
+.mdi-star-box::before {
+ content: "\FA72";
+}
+.mdi-star-box-multiple::before {
+ content: "\F02B1";
+}
+.mdi-star-box-multiple-outline::before {
+ content: "\F02B2";
+}
+.mdi-star-box-outline::before {
+ content: "\FA73";
+}
+.mdi-star-circle::before {
+ content: "\F4CF";
+}
+.mdi-star-circle-outline::before {
+ content: "\F9A3";
+}
+.mdi-star-face::before {
+ content: "\F9A4";
+}
+.mdi-star-four-points::before {
+ content: "\FAE1";
+}
+.mdi-star-four-points-outline::before {
+ content: "\FAE2";
+}
+.mdi-star-half::before {
+ content: "\F4D0";
+}
+.mdi-star-off::before {
+ content: "\F4D1";
+}
+.mdi-star-outline::before {
+ content: "\F4D2";
+}
+.mdi-star-three-points::before {
+ content: "\FAE3";
+}
+.mdi-star-three-points-outline::before {
+ content: "\FAE4";
+}
+.mdi-state-machine::before {
+ content: "\F021A";
+}
+.mdi-steam::before {
+ content: "\F4D3";
+}
+.mdi-steam-box::before {
+ content: "\F90C";
+}
+.mdi-steering::before {
+ content: "\F4D4";
+}
+.mdi-steering-off::before {
+ content: "\F90D";
+}
+.mdi-step-backward::before {
+ content: "\F4D5";
+}
+.mdi-step-backward-2::before {
+ content: "\F4D6";
+}
+.mdi-step-forward::before {
+ content: "\F4D7";
+}
+.mdi-step-forward-2::before {
+ content: "\F4D8";
+}
+.mdi-stethoscope::before {
+ content: "\F4D9";
+}
+.mdi-sticker::before {
+ content: "\F038F";
+}
+.mdi-sticker-alert::before {
+ content: "\F0390";
+}
+.mdi-sticker-alert-outline::before {
+ content: "\F0391";
+}
+.mdi-sticker-check::before {
+ content: "\F0392";
+}
+.mdi-sticker-check-outline::before {
+ content: "\F0393";
+}
+.mdi-sticker-circle-outline::before {
+ content: "\F5D0";
+}
+.mdi-sticker-emoji::before {
+ content: "\F784";
+}
+.mdi-sticker-minus::before {
+ content: "\F0394";
+}
+.mdi-sticker-minus-outline::before {
+ content: "\F0395";
+}
+.mdi-sticker-outline::before {
+ content: "\F0396";
+}
+.mdi-sticker-plus::before {
+ content: "\F0397";
+}
+.mdi-sticker-plus-outline::before {
+ content: "\F0398";
+}
+.mdi-sticker-remove::before {
+ content: "\F0399";
+}
+.mdi-sticker-remove-outline::before {
+ content: "\F039A";
+}
+.mdi-stocking::before {
+ content: "\F4DA";
+}
+.mdi-stomach::before {
+ content: "\F00BE";
+}
+.mdi-stop::before {
+ content: "\F4DB";
+}
+.mdi-stop-circle::before {
+ content: "\F666";
+}
+.mdi-stop-circle-outline::before {
+ content: "\F667";
+}
+.mdi-store::before {
+ content: "\F4DC";
+}
+.mdi-store-24-hour::before {
+ content: "\F4DD";
+}
+.mdi-store-outline::before {
+ content: "\F038C";
+}
+.mdi-storefront::before {
+ content: "\F00EC";
+}
+.mdi-stove::before {
+ content: "\F4DE";
+}
+.mdi-strategy::before {
+ content: "\F0201";
+}
+.mdi-strava::before {
+ content: "\FB25";
+}
+.mdi-stretch-to-page::before {
+ content: "\FF48";
+}
+.mdi-stretch-to-page-outline::before {
+ content: "\FF49";
+}
+.mdi-string-lights::before {
+ content: "\F02E5";
+}
+.mdi-string-lights-off::before {
+ content: "\F02E6";
+}
+.mdi-subdirectory-arrow-left::before {
+ content: "\F60C";
+}
+.mdi-subdirectory-arrow-right::before {
+ content: "\F60D";
+}
+.mdi-subtitles::before {
+ content: "\FA15";
+}
+.mdi-subtitles-outline::before {
+ content: "\FA16";
+}
+.mdi-subway::before {
+ content: "\F6AB";
+}
+.mdi-subway-alert-variant::before {
+ content: "\FD79";
+}
+.mdi-subway-variant::before {
+ content: "\F4DF";
+}
+.mdi-summit::before {
+ content: "\F785";
+}
+.mdi-sunglasses::before {
+ content: "\F4E0";
+}
+.mdi-surround-sound::before {
+ content: "\F5C5";
+}
+.mdi-surround-sound-2-0::before {
+ content: "\F7EF";
+}
+.mdi-surround-sound-3-1::before {
+ content: "\F7F0";
+}
+.mdi-surround-sound-5-1::before {
+ content: "\F7F1";
+}
+.mdi-surround-sound-7-1::before {
+ content: "\F7F2";
+}
+.mdi-svg::before {
+ content: "\F720";
+}
+.mdi-swap-horizontal::before {
+ content: "\F4E1";
+}
+.mdi-swap-horizontal-bold::before {
+ content: "\FBA9";
+}
+.mdi-swap-horizontal-circle::before {
+ content: "\F0002";
+}
+.mdi-swap-horizontal-circle-outline::before {
+ content: "\F0003";
+}
+.mdi-swap-horizontal-variant::before {
+ content: "\F8C0";
+}
+.mdi-swap-vertical::before {
+ content: "\F4E2";
+}
+.mdi-swap-vertical-bold::before {
+ content: "\FBAA";
+}
+.mdi-swap-vertical-circle::before {
+ content: "\F0004";
+}
+.mdi-swap-vertical-circle-outline::before {
+ content: "\F0005";
+}
+.mdi-swap-vertical-variant::before {
+ content: "\F8C1";
+}
+.mdi-swim::before {
+ content: "\F4E3";
+}
+.mdi-switch::before {
+ content: "\F4E4";
+}
+.mdi-sword::before {
+ content: "\F4E5";
+}
+.mdi-sword-cross::before {
+ content: "\F786";
+}
+.mdi-syllabary-hangul::before {
+ content: "\F035E";
+}
+.mdi-syllabary-hiragana::before {
+ content: "\F035F";
+}
+.mdi-syllabary-katakana::before {
+ content: "\F0360";
+}
+.mdi-syllabary-katakana-half-width::before {
+ content: "\F0361";
+}
+.mdi-symfony::before {
+ content: "\FAE5";
+}
+.mdi-sync::before {
+ content: "\F4E6";
+}
+.mdi-sync-alert::before {
+ content: "\F4E7";
+}
+.mdi-sync-circle::before {
+ content: "\F03A3";
+}
+.mdi-sync-off::before {
+ content: "\F4E8";
+}
+.mdi-tab::before {
+ content: "\F4E9";
+}
+.mdi-tab-minus::before {
+ content: "\FB26";
+}
+.mdi-tab-plus::before {
+ content: "\F75B";
+}
+.mdi-tab-remove::before {
+ content: "\FB27";
+}
+.mdi-tab-unselected::before {
+ content: "\F4EA";
+}
+.mdi-table::before {
+ content: "\F4EB";
+}
+.mdi-table-border::before {
+ content: "\FA17";
+}
+.mdi-table-chair::before {
+ content: "\F0083";
+}
+.mdi-table-column::before {
+ content: "\F834";
+}
+.mdi-table-column-plus-after::before {
+ content: "\F4EC";
+}
+.mdi-table-column-plus-before::before {
+ content: "\F4ED";
+}
+.mdi-table-column-remove::before {
+ content: "\F4EE";
+}
+.mdi-table-column-width::before {
+ content: "\F4EF";
+}
+.mdi-table-edit::before {
+ content: "\F4F0";
+}
+.mdi-table-eye::before {
+ content: "\F00BF";
+}
+.mdi-table-headers-eye::before {
+ content: "\F0248";
+}
+.mdi-table-headers-eye-off::before {
+ content: "\F0249";
+}
+.mdi-table-large::before {
+ content: "\F4F1";
+}
+.mdi-table-large-plus::before {
+ content: "\FFA4";
+}
+.mdi-table-large-remove::before {
+ content: "\FFA5";
+}
+.mdi-table-merge-cells::before {
+ content: "\F9A5";
+}
+.mdi-table-of-contents::before {
+ content: "\F835";
+}
+.mdi-table-plus::before {
+ content: "\FA74";
+}
+.mdi-table-remove::before {
+ content: "\FA75";
+}
+.mdi-table-row::before {
+ content: "\F836";
+}
+.mdi-table-row-height::before {
+ content: "\F4F2";
+}
+.mdi-table-row-plus-after::before {
+ content: "\F4F3";
+}
+.mdi-table-row-plus-before::before {
+ content: "\F4F4";
+}
+.mdi-table-row-remove::before {
+ content: "\F4F5";
+}
+.mdi-table-search::before {
+ content: "\F90E";
+}
+.mdi-table-settings::before {
+ content: "\F837";
+}
+.mdi-table-tennis::before {
+ content: "\FE4B";
+}
+.mdi-tablet::before {
+ content: "\F4F6";
+}
+.mdi-tablet-android::before {
+ content: "\F4F7";
+}
+.mdi-tablet-cellphone::before {
+ content: "\F9A6";
+}
+.mdi-tablet-dashboard::before {
+ content: "\FEEB";
+}
+.mdi-tablet-ipad::before {
+ content: "\F4F8";
+}
+.mdi-taco::before {
+ content: "\F761";
+}
+.mdi-tag::before {
+ content: "\F4F9";
+}
+.mdi-tag-faces::before {
+ content: "\F4FA";
+}
+.mdi-tag-heart::before {
+ content: "\F68A";
+}
+.mdi-tag-heart-outline::before {
+ content: "\FBAB";
+}
+.mdi-tag-minus::before {
+ content: "\F90F";
+}
+.mdi-tag-minus-outline::before {
+ content: "\F024A";
+}
+.mdi-tag-multiple::before {
+ content: "\F4FB";
+}
+.mdi-tag-multiple-outline::before {
+ content: "\F0322";
+}
+.mdi-tag-off::before {
+ content: "\F024B";
+}
+.mdi-tag-off-outline::before {
+ content: "\F024C";
+}
+.mdi-tag-outline::before {
+ content: "\F4FC";
+}
+.mdi-tag-plus::before {
+ content: "\F721";
+}
+.mdi-tag-plus-outline::before {
+ content: "\F024D";
+}
+.mdi-tag-remove::before {
+ content: "\F722";
+}
+.mdi-tag-remove-outline::before {
+ content: "\F024E";
+}
+.mdi-tag-text::before {
+ content: "\F024F";
+}
+.mdi-tag-text-outline::before {
+ content: "\F4FD";
+}
+.mdi-tank::before {
+ content: "\FD16";
+}
+.mdi-tanker-truck::before {
+ content: "\F0006";
+}
+.mdi-tape-measure::before {
+ content: "\FB28";
+}
+.mdi-target::before {
+ content: "\F4FE";
+}
+.mdi-target-account::before {
+ content: "\FBAC";
+}
+.mdi-target-variant::before {
+ content: "\FA76";
+}
+.mdi-taxi::before {
+ content: "\F4FF";
+}
+.mdi-tea::before {
+ content: "\FD7A";
+}
+.mdi-tea-outline::before {
+ content: "\FD7B";
+}
+.mdi-teach::before {
+ content: "\F88F";
+}
+.mdi-teamviewer::before {
+ content: "\F500";
+}
+.mdi-telegram::before {
+ content: "\F501";
+}
+.mdi-telescope::before {
+ content: "\FB29";
+}
+.mdi-television::before {
+ content: "\F502";
+}
+.mdi-television-ambient-light::before {
+ content: "\F0381";
+}
+.mdi-television-box::before {
+ content: "\F838";
+}
+.mdi-television-classic::before {
+ content: "\F7F3";
+}
+.mdi-television-classic-off::before {
+ content: "\F839";
+}
+.mdi-television-clean::before {
+ content: "\F013B";
+}
+.mdi-television-guide::before {
+ content: "\F503";
+}
+.mdi-television-off::before {
+ content: "\F83A";
+}
+.mdi-television-pause::before {
+ content: "\FFA6";
+}
+.mdi-television-play::before {
+ content: "\FEEC";
+}
+.mdi-television-stop::before {
+ content: "\FFA7";
+}
+.mdi-temperature-celsius::before {
+ content: "\F504";
+}
+.mdi-temperature-fahrenheit::before {
+ content: "\F505";
+}
+.mdi-temperature-kelvin::before {
+ content: "\F506";
+}
+.mdi-tennis::before {
+ content: "\FD7C";
+}
+.mdi-tennis-ball::before {
+ content: "\F507";
+}
+.mdi-tent::before {
+ content: "\F508";
+}
+.mdi-terraform::before {
+ content: "\F0084";
+}
+.mdi-terrain::before {
+ content: "\F509";
+}
+.mdi-test-tube::before {
+ content: "\F668";
+}
+.mdi-test-tube-empty::before {
+ content: "\F910";
+}
+.mdi-test-tube-off::before {
+ content: "\F911";
+}
+.mdi-text::before {
+ content: "\F9A7";
+}
+.mdi-text-recognition::before {
+ content: "\F0168";
+}
+.mdi-text-shadow::before {
+ content: "\F669";
+}
+.mdi-text-short::before {
+ content: "\F9A8";
+}
+.mdi-text-subject::before {
+ content: "\F9A9";
+}
+.mdi-text-to-speech::before {
+ content: "\F50A";
+}
+.mdi-text-to-speech-off::before {
+ content: "\F50B";
+}
+.mdi-textarea::before {
+ content: "\F00C0";
+}
+.mdi-textbox::before {
+ content: "\F60E";
+}
+.mdi-textbox-lock::before {
+ content: "\F0388";
+}
+.mdi-textbox-password::before {
+ content: "\F7F4";
+}
+.mdi-texture::before {
+ content: "\F50C";
+}
+.mdi-texture-box::before {
+ content: "\F0007";
+}
+.mdi-theater::before {
+ content: "\F50D";
+}
+.mdi-theme-light-dark::before {
+ content: "\F50E";
+}
+.mdi-thermometer::before {
+ content: "\F50F";
+}
+.mdi-thermometer-alert::before {
+ content: "\FE61";
+}
+.mdi-thermometer-chevron-down::before {
+ content: "\FE62";
+}
+.mdi-thermometer-chevron-up::before {
+ content: "\FE63";
+}
+.mdi-thermometer-high::before {
+ content: "\F00ED";
+}
+.mdi-thermometer-lines::before {
+ content: "\F510";
+}
+.mdi-thermometer-low::before {
+ content: "\F00EE";
+}
+.mdi-thermometer-minus::before {
+ content: "\FE64";
+}
+.mdi-thermometer-plus::before {
+ content: "\FE65";
+}
+.mdi-thermostat::before {
+ content: "\F393";
+}
+.mdi-thermostat-box::before {
+ content: "\F890";
+}
+.mdi-thought-bubble::before {
+ content: "\F7F5";
+}
+.mdi-thought-bubble-outline::before {
+ content: "\F7F6";
+}
+.mdi-thumb-down::before {
+ content: "\F511";
+}
+.mdi-thumb-down-outline::before {
+ content: "\F512";
+}
+.mdi-thumb-up::before {
+ content: "\F513";
+}
+.mdi-thumb-up-outline::before {
+ content: "\F514";
+}
+.mdi-thumbs-up-down::before {
+ content: "\F515";
+}
+.mdi-ticket::before {
+ content: "\F516";
+}
+.mdi-ticket-account::before {
+ content: "\F517";
+}
+.mdi-ticket-confirmation::before {
+ content: "\F518";
+}
+.mdi-ticket-outline::before {
+ content: "\F912";
+}
+.mdi-ticket-percent::before {
+ content: "\F723";
+}
+.mdi-tie::before {
+ content: "\F519";
+}
+.mdi-tilde::before {
+ content: "\F724";
+}
+.mdi-timelapse::before {
+ content: "\F51A";
+}
+.mdi-timeline::before {
+ content: "\FBAD";
+}
+.mdi-timeline-alert::before {
+ content: "\FFB2";
+}
+.mdi-timeline-alert-outline::before {
+ content: "\FFB5";
+}
+.mdi-timeline-clock::before {
+ content: "\F0226";
+}
+.mdi-timeline-clock-outline::before {
+ content: "\F0227";
+}
+.mdi-timeline-help::before {
+ content: "\FFB6";
+}
+.mdi-timeline-help-outline::before {
+ content: "\FFB7";
+}
+.mdi-timeline-outline::before {
+ content: "\FBAE";
+}
+.mdi-timeline-plus::before {
+ content: "\FFB3";
+}
+.mdi-timeline-plus-outline::before {
+ content: "\FFB4";
+}
+.mdi-timeline-text::before {
+ content: "\FBAF";
+}
+.mdi-timeline-text-outline::before {
+ content: "\FBB0";
+}
+.mdi-timer::before {
+ content: "\F51B";
+}
+.mdi-timer-10::before {
+ content: "\F51C";
+}
+.mdi-timer-3::before {
+ content: "\F51D";
+}
+.mdi-timer-off::before {
+ content: "\F51E";
+}
+.mdi-timer-sand::before {
+ content: "\F51F";
+}
+.mdi-timer-sand-empty::before {
+ content: "\F6AC";
+}
+.mdi-timer-sand-full::before {
+ content: "\F78B";
+}
+.mdi-timetable::before {
+ content: "\F520";
+}
+.mdi-toaster::before {
+ content: "\F0085";
+}
+.mdi-toaster-off::before {
+ content: "\F01E2";
+}
+.mdi-toaster-oven::before {
+ content: "\FCAF";
+}
+.mdi-toggle-switch::before {
+ content: "\F521";
+}
+.mdi-toggle-switch-off::before {
+ content: "\F522";
+}
+.mdi-toggle-switch-off-outline::before {
+ content: "\FA18";
+}
+.mdi-toggle-switch-outline::before {
+ content: "\FA19";
+}
+.mdi-toilet::before {
+ content: "\F9AA";
+}
+.mdi-toolbox::before {
+ content: "\F9AB";
+}
+.mdi-toolbox-outline::before {
+ content: "\F9AC";
+}
+.mdi-tools::before {
+ content: "\F0086";
+}
+.mdi-tooltip::before {
+ content: "\F523";
+}
+.mdi-tooltip-account::before {
+ content: "\F00C";
+}
+.mdi-tooltip-edit::before {
+ content: "\F524";
+}
+.mdi-tooltip-edit-outline::before {
+ content: "\F02F0";
+}
+.mdi-tooltip-image::before {
+ content: "\F525";
+}
+.mdi-tooltip-image-outline::before {
+ content: "\FBB1";
+}
+.mdi-tooltip-outline::before {
+ content: "\F526";
+}
+.mdi-tooltip-plus::before {
+ content: "\FBB2";
+}
+.mdi-tooltip-plus-outline::before {
+ content: "\F527";
+}
+.mdi-tooltip-text::before {
+ content: "\F528";
+}
+.mdi-tooltip-text-outline::before {
+ content: "\FBB3";
+}
+.mdi-tooth::before {
+ content: "\F8C2";
+}
+.mdi-tooth-outline::before {
+ content: "\F529";
+}
+.mdi-toothbrush::before {
+ content: "\F0154";
+}
+.mdi-toothbrush-electric::before {
+ content: "\F0157";
+}
+.mdi-toothbrush-paste::before {
+ content: "\F0155";
+}
+.mdi-tor::before {
+ content: "\F52A";
+}
+.mdi-tortoise::before {
+ content: "\FD17";
+}
+.mdi-toslink::before {
+ content: "\F02E3";
+}
+.mdi-tournament::before {
+ content: "\F9AD";
+}
+.mdi-tower-beach::before {
+ content: "\F680";
+}
+.mdi-tower-fire::before {
+ content: "\F681";
+}
+.mdi-towing::before {
+ content: "\F83B";
+}
+.mdi-toy-brick::before {
+ content: "\F02B3";
+}
+.mdi-toy-brick-marker::before {
+ content: "\F02B4";
+}
+.mdi-toy-brick-marker-outline::before {
+ content: "\F02B5";
+}
+.mdi-toy-brick-minus::before {
+ content: "\F02B6";
+}
+.mdi-toy-brick-minus-outline::before {
+ content: "\F02B7";
+}
+.mdi-toy-brick-outline::before {
+ content: "\F02B8";
+}
+.mdi-toy-brick-plus::before {
+ content: "\F02B9";
+}
+.mdi-toy-brick-plus-outline::before {
+ content: "\F02BA";
+}
+.mdi-toy-brick-remove::before {
+ content: "\F02BB";
+}
+.mdi-toy-brick-remove-outline::before {
+ content: "\F02BC";
+}
+.mdi-toy-brick-search::before {
+ content: "\F02BD";
+}
+.mdi-toy-brick-search-outline::before {
+ content: "\F02BE";
+}
+.mdi-track-light::before {
+ content: "\F913";
+}
+.mdi-trackpad::before {
+ content: "\F7F7";
+}
+.mdi-trackpad-lock::before {
+ content: "\F932";
+}
+.mdi-tractor::before {
+ content: "\F891";
+}
+.mdi-trademark::before {
+ content: "\FA77";
+}
+.mdi-traffic-cone::before {
+ content: "\F03A7";
+}
+.mdi-traffic-light::before {
+ content: "\F52B";
+}
+.mdi-train::before {
+ content: "\F52C";
+}
+.mdi-train-car::before {
+ content: "\FBB4";
+}
+.mdi-train-variant::before {
+ content: "\F8C3";
+}
+.mdi-tram::before {
+ content: "\F52D";
+}
+.mdi-tram-side::before {
+ content: "\F0008";
+}
+.mdi-transcribe::before {
+ content: "\F52E";
+}
+.mdi-transcribe-close::before {
+ content: "\F52F";
+}
+.mdi-transfer::before {
+ content: "\F0087";
+}
+.mdi-transfer-down::before {
+ content: "\FD7D";
+}
+.mdi-transfer-left::before {
+ content: "\FD7E";
+}
+.mdi-transfer-right::before {
+ content: "\F530";
+}
+.mdi-transfer-up::before {
+ content: "\FD7F";
+}
+.mdi-transit-connection::before {
+ content: "\FD18";
+}
+.mdi-transit-connection-variant::before {
+ content: "\FD19";
+}
+.mdi-transit-detour::before {
+ content: "\FFA8";
+}
+.mdi-transit-transfer::before {
+ content: "\F6AD";
+}
+.mdi-transition::before {
+ content: "\F914";
+}
+.mdi-transition-masked::before {
+ content: "\F915";
+}
+.mdi-translate::before {
+ content: "\F5CA";
+}
+.mdi-translate-off::before {
+ content: "\FE66";
+}
+.mdi-transmission-tower::before {
+ content: "\FD1A";
+}
+.mdi-trash-can::before {
+ content: "\FA78";
+}
+.mdi-trash-can-outline::before {
+ content: "\FA79";
+}
+.mdi-tray::before {
+ content: "\F02BF";
+}
+.mdi-tray-alert::before {
+ content: "\F02C0";
+}
+.mdi-tray-full::before {
+ content: "\F02C1";
+}
+.mdi-tray-minus::before {
+ content: "\F02C2";
+}
+.mdi-tray-plus::before {
+ content: "\F02C3";
+}
+.mdi-tray-remove::before {
+ content: "\F02C4";
+}
+.mdi-treasure-chest::before {
+ content: "\F725";
+}
+.mdi-tree::before {
+ content: "\F531";
+}
+.mdi-tree-outline::before {
+ content: "\FE4C";
+}
+.mdi-trello::before {
+ content: "\F532";
+}
+.mdi-trending-down::before {
+ content: "\F533";
+}
+.mdi-trending-neutral::before {
+ content: "\F534";
+}
+.mdi-trending-up::before {
+ content: "\F535";
+}
+.mdi-triangle::before {
+ content: "\F536";
+}
+.mdi-triangle-outline::before {
+ content: "\F537";
+}
+.mdi-triforce::before {
+ content: "\FBB5";
+}
+.mdi-trophy::before {
+ content: "\F538";
+}
+.mdi-trophy-award::before {
+ content: "\F539";
+}
+.mdi-trophy-broken::before {
+ content: "\FD80";
+}
+.mdi-trophy-outline::before {
+ content: "\F53A";
+}
+.mdi-trophy-variant::before {
+ content: "\F53B";
+}
+.mdi-trophy-variant-outline::before {
+ content: "\F53C";
+}
+.mdi-truck::before {
+ content: "\F53D";
+}
+.mdi-truck-check::before {
+ content: "\FCB0";
+}
+.mdi-truck-check-outline::before {
+ content: "\F02C5";
+}
+.mdi-truck-delivery::before {
+ content: "\F53E";
+}
+.mdi-truck-delivery-outline::before {
+ content: "\F02C6";
+}
+.mdi-truck-fast::before {
+ content: "\F787";
+}
+.mdi-truck-fast-outline::before {
+ content: "\F02C7";
+}
+.mdi-truck-outline::before {
+ content: "\F02C8";
+}
+.mdi-truck-trailer::before {
+ content: "\F726";
+}
+.mdi-trumpet::before {
+ content: "\F00C1";
+}
+.mdi-tshirt-crew::before {
+ content: "\FA7A";
+}
+.mdi-tshirt-crew-outline::before {
+ content: "\F53F";
+}
+.mdi-tshirt-v::before {
+ content: "\FA7B";
+}
+.mdi-tshirt-v-outline::before {
+ content: "\F540";
+}
+.mdi-tumble-dryer::before {
+ content: "\F916";
+}
+.mdi-tumble-dryer-alert::before {
+ content: "\F01E5";
+}
+.mdi-tumble-dryer-off::before {
+ content: "\F01E6";
+}
+.mdi-tumblr::before {
+ content: "\F541";
+}
+.mdi-tumblr-box::before {
+ content: "\F917";
+}
+.mdi-tumblr-reblog::before {
+ content: "\F542";
+}
+.mdi-tune::before {
+ content: "\F62E";
+}
+.mdi-tune-vertical::before {
+ content: "\F66A";
+}
+.mdi-turnstile::before {
+ content: "\FCB1";
+}
+.mdi-turnstile-outline::before {
+ content: "\FCB2";
+}
+.mdi-turtle::before {
+ content: "\FCB3";
+}
+.mdi-twitch::before {
+ content: "\F543";
+}
+.mdi-twitter::before {
+ content: "\F544";
+}
+.mdi-twitter-box::before {
+ content: "\F545";
+}
+.mdi-twitter-circle::before {
+ content: "\F546";
+}
+.mdi-twitter-retweet::before {
+ content: "\F547";
+}
+.mdi-two-factor-authentication::before {
+ content: "\F9AE";
+}
+.mdi-typewriter::before {
+ content: "\FF4A";
+}
+.mdi-uber::before {
+ content: "\F748";
+}
+.mdi-ubisoft::before {
+ content: "\FBB6";
+}
+.mdi-ubuntu::before {
+ content: "\F548";
+}
+.mdi-ufo::before {
+ content: "\F00EF";
+}
+.mdi-ufo-outline::before {
+ content: "\F00F0";
+}
+.mdi-ultra-high-definition::before {
+ content: "\F7F8";
+}
+.mdi-umbraco::before {
+ content: "\F549";
+}
+.mdi-umbrella::before {
+ content: "\F54A";
+}
+.mdi-umbrella-closed::before {
+ content: "\F9AF";
+}
+.mdi-umbrella-outline::before {
+ content: "\F54B";
+}
+.mdi-undo::before {
+ content: "\F54C";
+}
+.mdi-undo-variant::before {
+ content: "\F54D";
+}
+.mdi-unfold-less-horizontal::before {
+ content: "\F54E";
+}
+.mdi-unfold-less-vertical::before {
+ content: "\F75F";
+}
+.mdi-unfold-more-horizontal::before {
+ content: "\F54F";
+}
+.mdi-unfold-more-vertical::before {
+ content: "\F760";
+}
+.mdi-ungroup::before {
+ content: "\F550";
+}
+.mdi-unicode::before {
+ content: "\FEED";
+}
+.mdi-unity::before {
+ content: "\F6AE";
+}
+.mdi-unreal::before {
+ content: "\F9B0";
+}
+.mdi-untappd::before {
+ content: "\F551";
+}
+.mdi-update::before {
+ content: "\F6AF";
+}
+.mdi-upload::before {
+ content: "\F552";
+}
+.mdi-upload-lock::before {
+ content: "\F039E";
+}
+.mdi-upload-lock-outline::before {
+ content: "\F039F";
+}
+.mdi-upload-multiple::before {
+ content: "\F83C";
+}
+.mdi-upload-network::before {
+ content: "\F6F5";
+}
+.mdi-upload-network-outline::before {
+ content: "\FCB4";
+}
+.mdi-upload-off::before {
+ content: "\F00F1";
+}
+.mdi-upload-off-outline::before {
+ content: "\F00F2";
+}
+.mdi-upload-outline::before {
+ content: "\FE67";
+}
+.mdi-usb::before {
+ content: "\F553";
+}
+.mdi-usb-flash-drive::before {
+ content: "\F02C9";
+}
+.mdi-usb-flash-drive-outline::before {
+ content: "\F02CA";
+}
+.mdi-usb-port::before {
+ content: "\F021B";
+}
+.mdi-valve::before {
+ content: "\F0088";
+}
+.mdi-valve-closed::before {
+ content: "\F0089";
+}
+.mdi-valve-open::before {
+ content: "\F008A";
+}
+.mdi-van-passenger::before {
+ content: "\F7F9";
+}
+.mdi-van-utility::before {
+ content: "\F7FA";
+}
+.mdi-vanish::before {
+ content: "\F7FB";
+}
+.mdi-vanity-light::before {
+ content: "\F020C";
+}
+.mdi-variable::before {
+ content: "\FAE6";
+}
+.mdi-variable-box::before {
+ content: "\F013C";
+}
+.mdi-vector-arrange-above::before {
+ content: "\F554";
+}
+.mdi-vector-arrange-below::before {
+ content: "\F555";
+}
+.mdi-vector-bezier::before {
+ content: "\FAE7";
+}
+.mdi-vector-circle::before {
+ content: "\F556";
+}
+.mdi-vector-circle-variant::before {
+ content: "\F557";
+}
+.mdi-vector-combine::before {
+ content: "\F558";
+}
+.mdi-vector-curve::before {
+ content: "\F559";
+}
+.mdi-vector-difference::before {
+ content: "\F55A";
+}
+.mdi-vector-difference-ab::before {
+ content: "\F55B";
+}
+.mdi-vector-difference-ba::before {
+ content: "\F55C";
+}
+.mdi-vector-ellipse::before {
+ content: "\F892";
+}
+.mdi-vector-intersection::before {
+ content: "\F55D";
+}
+.mdi-vector-line::before {
+ content: "\F55E";
+}
+.mdi-vector-link::before {
+ content: "\F0009";
+}
+.mdi-vector-point::before {
+ content: "\F55F";
+}
+.mdi-vector-polygon::before {
+ content: "\F560";
+}
+.mdi-vector-polyline::before {
+ content: "\F561";
+}
+.mdi-vector-polyline-edit::before {
+ content: "\F0250";
+}
+.mdi-vector-polyline-minus::before {
+ content: "\F0251";
+}
+.mdi-vector-polyline-plus::before {
+ content: "\F0252";
+}
+.mdi-vector-polyline-remove::before {
+ content: "\F0253";
+}
+.mdi-vector-radius::before {
+ content: "\F749";
+}
+.mdi-vector-rectangle::before {
+ content: "\F5C6";
+}
+.mdi-vector-selection::before {
+ content: "\F562";
+}
+.mdi-vector-square::before {
+ content: "\F001";
+}
+.mdi-vector-triangle::before {
+ content: "\F563";
+}
+.mdi-vector-union::before {
+ content: "\F564";
+}
+.mdi-venmo::before {
+ content: "\F578";
+}
+.mdi-vhs::before {
+ content: "\FA1A";
+}
+.mdi-vibrate::before {
+ content: "\F566";
+}
+.mdi-vibrate-off::before {
+ content: "\FCB5";
+}
+.mdi-video::before {
+ content: "\F567";
+}
+.mdi-video-3d::before {
+ content: "\F7FC";
+}
+.mdi-video-3d-variant::before {
+ content: "\FEEE";
+}
+.mdi-video-4k-box::before {
+ content: "\F83D";
+}
+.mdi-video-account::before {
+ content: "\F918";
+}
+.mdi-video-check::before {
+ content: "\F008B";
+}
+.mdi-video-check-outline::before {
+ content: "\F008C";
+}
+.mdi-video-image::before {
+ content: "\F919";
+}
+.mdi-video-input-antenna::before {
+ content: "\F83E";
+}
+.mdi-video-input-component::before {
+ content: "\F83F";
+}
+.mdi-video-input-hdmi::before {
+ content: "\F840";
+}
+.mdi-video-input-scart::before {
+ content: "\FFA9";
+}
+.mdi-video-input-svideo::before {
+ content: "\F841";
+}
+.mdi-video-minus::before {
+ content: "\F9B1";
+}
+.mdi-video-off::before {
+ content: "\F568";
+}
+.mdi-video-off-outline::before {
+ content: "\FBB7";
+}
+.mdi-video-outline::before {
+ content: "\FBB8";
+}
+.mdi-video-plus::before {
+ content: "\F9B2";
+}
+.mdi-video-stabilization::before {
+ content: "\F91A";
+}
+.mdi-video-switch::before {
+ content: "\F569";
+}
+.mdi-video-vintage::before {
+ content: "\FA1B";
+}
+.mdi-video-wireless::before {
+ content: "\FEEF";
+}
+.mdi-video-wireless-outline::before {
+ content: "\FEF0";
+}
+.mdi-view-agenda::before {
+ content: "\F56A";
+}
+.mdi-view-agenda-outline::before {
+ content: "\F0203";
+}
+.mdi-view-array::before {
+ content: "\F56B";
+}
+.mdi-view-carousel::before {
+ content: "\F56C";
+}
+.mdi-view-column::before {
+ content: "\F56D";
+}
+.mdi-view-comfy::before {
+ content: "\FE4D";
+}
+.mdi-view-compact::before {
+ content: "\FE4E";
+}
+.mdi-view-compact-outline::before {
+ content: "\FE4F";
+}
+.mdi-view-dashboard::before {
+ content: "\F56E";
+}
+.mdi-view-dashboard-outline::before {
+ content: "\FA1C";
+}
+.mdi-view-dashboard-variant::before {
+ content: "\F842";
+}
+.mdi-view-day::before {
+ content: "\F56F";
+}
+.mdi-view-grid::before {
+ content: "\F570";
+}
+.mdi-view-grid-outline::before {
+ content: "\F0204";
+}
+.mdi-view-grid-plus::before {
+ content: "\FFAA";
+}
+.mdi-view-grid-plus-outline::before {
+ content: "\F0205";
+}
+.mdi-view-headline::before {
+ content: "\F571";
+}
+.mdi-view-list::before {
+ content: "\F572";
+}
+.mdi-view-module::before {
+ content: "\F573";
+}
+.mdi-view-parallel::before {
+ content: "\F727";
+}
+.mdi-view-quilt::before {
+ content: "\F574";
+}
+.mdi-view-sequential::before {
+ content: "\F728";
+}
+.mdi-view-split-horizontal::before {
+ content: "\FBA7";
+}
+.mdi-view-split-vertical::before {
+ content: "\FBA8";
+}
+.mdi-view-stream::before {
+ content: "\F575";
+}
+.mdi-view-week::before {
+ content: "\F576";
+}
+.mdi-vimeo::before {
+ content: "\F577";
+}
+.mdi-violin::before {
+ content: "\F60F";
+}
+.mdi-virtual-reality::before {
+ content: "\F893";
+}
+.mdi-visual-studio::before {
+ content: "\F610";
+}
+.mdi-visual-studio-code::before {
+ content: "\FA1D";
+}
+.mdi-vk::before {
+ content: "\F579";
+}
+.mdi-vk-box::before {
+ content: "\F57A";
+}
+.mdi-vk-circle::before {
+ content: "\F57B";
+}
+.mdi-vlc::before {
+ content: "\F57C";
+}
+.mdi-voice::before {
+ content: "\F5CB";
+}
+.mdi-voice-off::before {
+ content: "\FEF1";
+}
+.mdi-voicemail::before {
+ content: "\F57D";
+}
+.mdi-volleyball::before {
+ content: "\F9B3";
+}
+.mdi-volume-high::before {
+ content: "\F57E";
+}
+.mdi-volume-low::before {
+ content: "\F57F";
+}
+.mdi-volume-medium::before {
+ content: "\F580";
+}
+.mdi-volume-minus::before {
+ content: "\F75D";
+}
+.mdi-volume-mute::before {
+ content: "\F75E";
+}
+.mdi-volume-off::before {
+ content: "\F581";
+}
+.mdi-volume-plus::before {
+ content: "\F75C";
+}
+.mdi-volume-source::before {
+ content: "\F014B";
+}
+.mdi-volume-variant-off::before {
+ content: "\FE68";
+}
+.mdi-volume-vibrate::before {
+ content: "\F014C";
+}
+.mdi-vote::before {
+ content: "\FA1E";
+}
+.mdi-vote-outline::before {
+ content: "\FA1F";
+}
+.mdi-vpn::before {
+ content: "\F582";
+}
+.mdi-vuejs::before {
+ content: "\F843";
+}
+.mdi-vuetify::before {
+ content: "\FE50";
+}
+.mdi-walk::before {
+ content: "\F583";
+}
+.mdi-wall::before {
+ content: "\F7FD";
+}
+.mdi-wall-sconce::before {
+ content: "\F91B";
+}
+.mdi-wall-sconce-flat::before {
+ content: "\F91C";
+}
+.mdi-wall-sconce-variant::before {
+ content: "\F91D";
+}
+.mdi-wallet::before {
+ content: "\F584";
+}
+.mdi-wallet-giftcard::before {
+ content: "\F585";
+}
+.mdi-wallet-membership::before {
+ content: "\F586";
+}
+.mdi-wallet-outline::before {
+ content: "\FBB9";
+}
+.mdi-wallet-plus::before {
+ content: "\FFAB";
+}
+.mdi-wallet-plus-outline::before {
+ content: "\FFAC";
+}
+.mdi-wallet-travel::before {
+ content: "\F587";
+}
+.mdi-wallpaper::before {
+ content: "\FE69";
+}
+.mdi-wan::before {
+ content: "\F588";
+}
+.mdi-wardrobe::before {
+ content: "\FFAD";
+}
+.mdi-wardrobe-outline::before {
+ content: "\FFAE";
+}
+.mdi-warehouse::before {
+ content: "\FFBB";
+}
+.mdi-washing-machine::before {
+ content: "\F729";
+}
+.mdi-washing-machine-alert::before {
+ content: "\F01E7";
+}
+.mdi-washing-machine-off::before {
+ content: "\F01E8";
+}
+.mdi-watch::before {
+ content: "\F589";
+}
+.mdi-watch-export::before {
+ content: "\F58A";
+}
+.mdi-watch-export-variant::before {
+ content: "\F894";
+}
+.mdi-watch-import::before {
+ content: "\F58B";
+}
+.mdi-watch-import-variant::before {
+ content: "\F895";
+}
+.mdi-watch-variant::before {
+ content: "\F896";
+}
+.mdi-watch-vibrate::before {
+ content: "\F6B0";
+}
+.mdi-watch-vibrate-off::before {
+ content: "\FCB6";
+}
+.mdi-water::before {
+ content: "\F58C";
+}
+.mdi-water-boiler::before {
+ content: "\FFAF";
+}
+.mdi-water-boiler-alert::before {
+ content: "\F01DE";
+}
+.mdi-water-boiler-off::before {
+ content: "\F01DF";
+}
+.mdi-water-off::before {
+ content: "\F58D";
+}
+.mdi-water-outline::before {
+ content: "\FE6A";
+}
+.mdi-water-percent::before {
+ content: "\F58E";
+}
+.mdi-water-polo::before {
+ content: "\F02CB";
+}
+.mdi-water-pump::before {
+ content: "\F58F";
+}
+.mdi-water-pump-off::before {
+ content: "\FFB0";
+}
+.mdi-water-well::before {
+ content: "\F008D";
+}
+.mdi-water-well-outline::before {
+ content: "\F008E";
+}
+.mdi-watermark::before {
+ content: "\F612";
+}
+.mdi-wave::before {
+ content: "\FF4B";
+}
+.mdi-waves::before {
+ content: "\F78C";
+}
+.mdi-waze::before {
+ content: "\FBBA";
+}
+.mdi-weather-cloudy::before {
+ content: "\F590";
+}
+.mdi-weather-cloudy-alert::before {
+ content: "\FF4C";
+}
+.mdi-weather-cloudy-arrow-right::before {
+ content: "\FE51";
+}
+.mdi-weather-fog::before {
+ content: "\F591";
+}
+.mdi-weather-hail::before {
+ content: "\F592";
+}
+.mdi-weather-hazy::before {
+ content: "\FF4D";
+}
+.mdi-weather-hurricane::before {
+ content: "\F897";
+}
+.mdi-weather-lightning::before {
+ content: "\F593";
+}
+.mdi-weather-lightning-rainy::before {
+ content: "\F67D";
+}
+.mdi-weather-night::before {
+ content: "\F594";
+}
+.mdi-weather-night-partly-cloudy::before {
+ content: "\FF4E";
+}
+.mdi-weather-partly-cloudy::before {
+ content: "\F595";
+}
+.mdi-weather-partly-lightning::before {
+ content: "\FF4F";
+}
+.mdi-weather-partly-rainy::before {
+ content: "\FF50";
+}
+.mdi-weather-partly-snowy::before {
+ content: "\FF51";
+}
+.mdi-weather-partly-snowy-rainy::before {
+ content: "\FF52";
+}
+.mdi-weather-pouring::before {
+ content: "\F596";
+}
+.mdi-weather-rainy::before {
+ content: "\F597";
+}
+.mdi-weather-snowy::before {
+ content: "\F598";
+}
+.mdi-weather-snowy-heavy::before {
+ content: "\FF53";
+}
+.mdi-weather-snowy-rainy::before {
+ content: "\F67E";
+}
+.mdi-weather-sunny::before {
+ content: "\F599";
+}
+.mdi-weather-sunny-alert::before {
+ content: "\FF54";
+}
+.mdi-weather-sunset::before {
+ content: "\F59A";
+}
+.mdi-weather-sunset-down::before {
+ content: "\F59B";
+}
+.mdi-weather-sunset-up::before {
+ content: "\F59C";
+}
+.mdi-weather-tornado::before {
+ content: "\FF55";
+}
+.mdi-weather-windy::before {
+ content: "\F59D";
+}
+.mdi-weather-windy-variant::before {
+ content: "\F59E";
+}
+.mdi-web::before {
+ content: "\F59F";
+}
+.mdi-web-box::before {
+ content: "\FFB1";
+}
+.mdi-web-clock::before {
+ content: "\F0275";
+}
+.mdi-webcam::before {
+ content: "\F5A0";
+}
+.mdi-webhook::before {
+ content: "\F62F";
+}
+.mdi-webpack::before {
+ content: "\F72A";
+}
+.mdi-webrtc::before {
+ content: "\F0273";
+}
+.mdi-wechat::before {
+ content: "\F611";
+}
+.mdi-weight::before {
+ content: "\F5A1";
+}
+.mdi-weight-gram::before {
+ content: "\FD1B";
+}
+.mdi-weight-kilogram::before {
+ content: "\F5A2";
+}
+.mdi-weight-lifter::before {
+ content: "\F0188";
+}
+.mdi-weight-pound::before {
+ content: "\F9B4";
+}
+.mdi-whatsapp::before {
+ content: "\F5A3";
+}
+.mdi-wheelchair-accessibility::before {
+ content: "\F5A4";
+}
+.mdi-whistle::before {
+ content: "\F9B5";
+}
+.mdi-whistle-outline::before {
+ content: "\F02E7";
+}
+.mdi-white-balance-auto::before {
+ content: "\F5A5";
+}
+.mdi-white-balance-incandescent::before {
+ content: "\F5A6";
+}
+.mdi-white-balance-iridescent::before {
+ content: "\F5A7";
+}
+.mdi-white-balance-sunny::before {
+ content: "\F5A8";
+}
+.mdi-widgets::before {
+ content: "\F72B";
+}
+.mdi-widgets-outline::before {
+ content: "\F0380";
+}
+.mdi-wifi::before {
+ content: "\F5A9";
+}
+.mdi-wifi-off::before {
+ content: "\F5AA";
+}
+.mdi-wifi-star::before {
+ content: "\FE6B";
+}
+.mdi-wifi-strength-1::before {
+ content: "\F91E";
+}
+.mdi-wifi-strength-1-alert::before {
+ content: "\F91F";
+}
+.mdi-wifi-strength-1-lock::before {
+ content: "\F920";
+}
+.mdi-wifi-strength-2::before {
+ content: "\F921";
+}
+.mdi-wifi-strength-2-alert::before {
+ content: "\F922";
+}
+.mdi-wifi-strength-2-lock::before {
+ content: "\F923";
+}
+.mdi-wifi-strength-3::before {
+ content: "\F924";
+}
+.mdi-wifi-strength-3-alert::before {
+ content: "\F925";
+}
+.mdi-wifi-strength-3-lock::before {
+ content: "\F926";
+}
+.mdi-wifi-strength-4::before {
+ content: "\F927";
+}
+.mdi-wifi-strength-4-alert::before {
+ content: "\F928";
+}
+.mdi-wifi-strength-4-lock::before {
+ content: "\F929";
+}
+.mdi-wifi-strength-alert-outline::before {
+ content: "\F92A";
+}
+.mdi-wifi-strength-lock-outline::before {
+ content: "\F92B";
+}
+.mdi-wifi-strength-off::before {
+ content: "\F92C";
+}
+.mdi-wifi-strength-off-outline::before {
+ content: "\F92D";
+}
+.mdi-wifi-strength-outline::before {
+ content: "\F92E";
+}
+.mdi-wii::before {
+ content: "\F5AB";
+}
+.mdi-wiiu::before {
+ content: "\F72C";
+}
+.mdi-wikipedia::before {
+ content: "\F5AC";
+}
+.mdi-wind-turbine::before {
+ content: "\FD81";
+}
+.mdi-window-close::before {
+ content: "\F5AD";
+}
+.mdi-window-closed::before {
+ content: "\F5AE";
+}
+.mdi-window-closed-variant::before {
+ content: "\F0206";
+}
+.mdi-window-maximize::before {
+ content: "\F5AF";
+}
+.mdi-window-minimize::before {
+ content: "\F5B0";
+}
+.mdi-window-open::before {
+ content: "\F5B1";
+}
+.mdi-window-open-variant::before {
+ content: "\F0207";
+}
+.mdi-window-restore::before {
+ content: "\F5B2";
+}
+.mdi-window-shutter::before {
+ content: "\F0147";
+}
+.mdi-window-shutter-alert::before {
+ content: "\F0148";
+}
+.mdi-window-shutter-open::before {
+ content: "\F0149";
+}
+.mdi-windows::before {
+ content: "\F5B3";
+}
+.mdi-windows-classic::before {
+ content: "\FA20";
+}
+.mdi-wiper::before {
+ content: "\FAE8";
+}
+.mdi-wiper-wash::before {
+ content: "\FD82";
+}
+.mdi-wordpress::before {
+ content: "\F5B4";
+}
+.mdi-worker::before {
+ content: "\F5B5";
+}
+.mdi-wrap::before {
+ content: "\F5B6";
+}
+.mdi-wrap-disabled::before {
+ content: "\FBBB";
+}
+.mdi-wrench::before {
+ content: "\F5B7";
+}
+.mdi-wrench-outline::before {
+ content: "\FBBC";
+}
+.mdi-wunderlist::before {
+ content: "\F5B8";
+}
+.mdi-xamarin::before {
+ content: "\F844";
+}
+.mdi-xamarin-outline::before {
+ content: "\F845";
+}
+.mdi-xaml::before {
+ content: "\F673";
+}
+.mdi-xbox::before {
+ content: "\F5B9";
+}
+.mdi-xbox-controller::before {
+ content: "\F5BA";
+}
+.mdi-xbox-controller-battery-alert::before {
+ content: "\F74A";
+}
+.mdi-xbox-controller-battery-charging::before {
+ content: "\FA21";
+}
+.mdi-xbox-controller-battery-empty::before {
+ content: "\F74B";
+}
+.mdi-xbox-controller-battery-full::before {
+ content: "\F74C";
+}
+.mdi-xbox-controller-battery-low::before {
+ content: "\F74D";
+}
+.mdi-xbox-controller-battery-medium::before {
+ content: "\F74E";
+}
+.mdi-xbox-controller-battery-unknown::before {
+ content: "\F74F";
+}
+.mdi-xbox-controller-menu::before {
+ content: "\FE52";
+}
+.mdi-xbox-controller-off::before {
+ content: "\F5BB";
+}
+.mdi-xbox-controller-view::before {
+ content: "\FE53";
+}
+.mdi-xda::before {
+ content: "\F5BC";
+}
+.mdi-xing::before {
+ content: "\F5BD";
+}
+.mdi-xing-box::before {
+ content: "\F5BE";
+}
+.mdi-xing-circle::before {
+ content: "\F5BF";
+}
+.mdi-xml::before {
+ content: "\F5C0";
+}
+.mdi-xmpp::before {
+ content: "\F7FE";
+}
+.mdi-yahoo::before {
+ content: "\FB2A";
+}
+.mdi-yammer::before {
+ content: "\F788";
+}
+.mdi-yeast::before {
+ content: "\F5C1";
+}
+.mdi-yelp::before {
+ content: "\F5C2";
+}
+.mdi-yin-yang::before {
+ content: "\F67F";
+}
+.mdi-yoga::before {
+ content: "\F01A7";
+}
+.mdi-youtube::before {
+ content: "\F5C3";
+}
+.mdi-youtube-creator-studio::before {
+ content: "\F846";
+}
+.mdi-youtube-gaming::before {
+ content: "\F847";
+}
+.mdi-youtube-subscription::before {
+ content: "\FD1C";
+}
+.mdi-youtube-tv::before {
+ content: "\F448";
+}
+.mdi-z-wave::before {
+ content: "\FAE9";
+}
+.mdi-zend::before {
+ content: "\FAEA";
+}
+.mdi-zigbee::before {
+ content: "\FD1D";
+}
+.mdi-zip-box::before {
+ content: "\F5C4";
+}
+.mdi-zip-box-outline::before {
+ content: "\F001B";
+}
+.mdi-zip-disk::before {
+ content: "\FA22";
+}
+.mdi-zodiac-aquarius::before {
+ content: "\FA7C";
+}
+.mdi-zodiac-aries::before {
+ content: "\FA7D";
+}
+.mdi-zodiac-cancer::before {
+ content: "\FA7E";
+}
+.mdi-zodiac-capricorn::before {
+ content: "\FA7F";
+}
+.mdi-zodiac-gemini::before {
+ content: "\FA80";
+}
+.mdi-zodiac-leo::before {
+ content: "\FA81";
+}
+.mdi-zodiac-libra::before {
+ content: "\FA82";
+}
+.mdi-zodiac-pisces::before {
+ content: "\FA83";
+}
+.mdi-zodiac-sagittarius::before {
+ content: "\FA84";
+}
+.mdi-zodiac-scorpio::before {
+ content: "\FA85";
+}
+.mdi-zodiac-taurus::before {
+ content: "\FA86";
+}
+.mdi-zodiac-virgo::before {
+ content: "\FA87";
+}
+.mdi-blank::before {
+ content: "\F68C";
+ visibility: hidden;
+}
+.mdi-18px.mdi-set,
+.mdi-18px.mdi:before {
+ font-size: 18px;
+}
+.mdi-24px.mdi-set,
+.mdi-24px.mdi:before {
+ font-size: 24px;
+}
+.mdi-36px.mdi-set,
+.mdi-36px.mdi:before {
+ font-size: 36px;
+}
+.mdi-48px.mdi-set,
+.mdi-48px.mdi:before {
+ font-size: 48px;
+}
+.mdi-dark:before {
+ color: rgba(0, 0, 0, 0.54);
+}
+.mdi-dark.mdi-inactive:before {
+ color: rgba(0, 0, 0, 0.26);
+}
+.mdi-light:before {
+ color: #fff;
+}
+.mdi-light.mdi-inactive:before {
+ color: rgba(255, 255, 255, 0.3);
+}
+.mdi-rotate-45:before {
+ -webkit-transform: rotate(45deg);
+ -ms-transform: rotate(45deg);
+ transform: rotate(45deg);
+}
+.mdi-rotate-90:before {
+ -webkit-transform: rotate(90deg);
+ -ms-transform: rotate(90deg);
+ transform: rotate(90deg);
+}
+.mdi-rotate-135:before {
+ -webkit-transform: rotate(135deg);
+ -ms-transform: rotate(135deg);
+ transform: rotate(135deg);
+}
+.mdi-rotate-180:before {
+ -webkit-transform: rotate(180deg);
+ -ms-transform: rotate(180deg);
+ transform: rotate(180deg);
+}
+.mdi-rotate-225:before {
+ -webkit-transform: rotate(225deg);
+ -ms-transform: rotate(225deg);
+ transform: rotate(225deg);
+}
+.mdi-rotate-270:before {
+ -webkit-transform: rotate(270deg);
+ -ms-transform: rotate(270deg);
+ transform: rotate(270deg);
+}
+.mdi-rotate-315:before {
+ -webkit-transform: rotate(315deg);
+ -ms-transform: rotate(315deg);
+ transform: rotate(315deg);
+}
+.mdi-flip-h:before {
+ -webkit-transform: scaleX(-1);
+ transform: scaleX(-1);
+ filter: FlipH;
+ -ms-filter: "FlipH";
+}
+.mdi-flip-v:before {
+ -webkit-transform: scaleY(-1);
+ transform: scaleY(-1);
+ filter: FlipV;
+ -ms-filter: "FlipV";
+}
+.mdi-spin:before {
+ -webkit-animation: mdi-spin 2s infinite linear;
+ animation: mdi-spin 2s infinite linear;
+}
+@-webkit-keyframes mdi-spin {
+ 0% {
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+ 100% {
+ -webkit-transform: rotate(359deg);
+ transform: rotate(359deg);
+ }
+}
+@keyframes mdi-spin {
+ 0% {
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+ 100% {
+ -webkit-transform: rotate(359deg);
+ transform: rotate(359deg);
+ }
+}
/*# sourceMappingURL=materialdesignicons.css.map */
diff --git a/packages/merchant-backoffice-ui/src/scss/libs/_all.scss b/packages/merchant-backoffice-ui/src/scss/libs/_all.scss
index 313eb52f9..ab8030a13 100644
--- a/packages/merchant-backoffice-ui/src/scss/libs/_all.scss
+++ b/packages/merchant-backoffice-ui/src/scss/libs/_all.scss
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2021-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- /**
+/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
diff --git a/packages/merchant-backoffice-ui/src/scss/main.scss b/packages/merchant-backoffice-ui/src/scss/main.scss
index b523566c1..4a46472f9 100644
--- a/packages/merchant-backoffice-ui/src/scss/main.scss
+++ b/packages/merchant-backoffice-ui/src/scss/main.scss
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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
@@ -52,6 +52,8 @@ $tooltip-color: red;
@import "../../node_modules/@creativebulma/bulma-tooltip/dist/bulma-tooltip.min.css";
@import "../../node_modules/bulma-timeline/dist/css/bulma-timeline.min.css";
+@import "toggle";
+
.notification {
background-color: transparent;
}
@@ -82,7 +84,7 @@ $tooltip-color: red;
pointer-events: none;
}
-.toast > .message {
+.toast>.message {
white-space: pre-wrap;
opacity: 80%;
}
@@ -92,6 +94,7 @@ div {
position: relative;
pointer-events: none;
opacity: 0.5;
+
&:after {
// @include loader;
position: absolute;
@@ -104,7 +107,7 @@ div {
}
}
-input[type="checkbox"]:indeterminate + .check {
+input[type="checkbox"]:indeterminate+.check {
background: red !important;
}
@@ -125,6 +128,7 @@ input[type="checkbox"]:indeterminate + .check {
tr:hover .right-sticky {
background-color: hsl(0, 0%, 80%);
}
+
.table.is-striped tbody tr:nth-child(even):hover .right-sticky {
background-color: hsl(0, 0%, 95%);
}
@@ -181,11 +185,11 @@ div[data-tooltip]::before {
position: absolute;
}
-.modal-card-body > p {
+.modal-card-body>p {
padding: 1em;
}
-.modal-card-body > p.warning {
+.modal-card-body>p.warning {
background-color: #fffbdd;
border: solid 1px #f2e9bf;
-}
+} \ No newline at end of file
diff --git a/packages/merchant-backoffice-ui/src/scss/toggle.scss b/packages/merchant-backoffice-ui/src/scss/toggle.scss
new file mode 100644
index 000000000..6c7346eb3
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/scss/toggle.scss
@@ -0,0 +1,67 @@
+$green: #56c080;
+
+.toggle {
+ cursor: pointer;
+ display: inline-block;
+}
+
+.toggle-switch {
+ display: inline-block;
+ background: #ccc;
+ border-radius: 16px;
+ width: 58px;
+ height: 32px;
+ position: relative;
+ vertical-align: middle;
+ transition: background 0.25s;
+
+ &:before,
+ &:after {
+ content: "";
+ }
+
+ &:before {
+ display: block;
+ background: linear-gradient(to bottom, #fff 0%, #eee 100%);
+ border-radius: 50%;
+ box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25);
+ width: 24px;
+ height: 24px;
+ position: absolute;
+ top: 4px;
+ left: 4px;
+ transition: left 0.25s;
+ }
+
+ .toggle:hover &:before {
+ background: linear-gradient(to bottom, #fff 0%, #fff 100%);
+ box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.5);
+ }
+
+ &.disabled:before {
+ background: linear-gradient(to bottom, #ccc 0%, #bbb 100%);
+ }
+
+ .toggle:hover &.disabled:before {
+ background: linear-gradient(to bottom, #ccc 0%, #bbb 100%);
+ }
+
+ .toggle-checkbox:checked+& {
+ background: $green;
+
+ &:before {
+ left: 30px;
+ }
+ }
+}
+
+.toggle-checkbox {
+ position: absolute;
+ visibility: hidden;
+}
+
+.toggle-label {
+ margin-left: 5px;
+ position: relative;
+ top: 2px;
+} \ No newline at end of file
diff --git a/packages/merchant-backoffice-ui/src/settings.ts b/packages/merchant-backoffice-ui/src/settings.ts
new file mode 100644
index 000000000..0e377bec2
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/settings.ts
@@ -0,0 +1,84 @@
+/*
+ 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 {
+ Codec,
+ buildCodecForObject,
+ canonicalizeBaseUrl,
+ codecForString,
+ codecOptional
+} from "@gnu-taler/taler-util";
+
+export interface MerchantUiSettings {
+ // Where merchant backend is localted
+ // default: window.origin without "webui/"
+ backendBaseURL?: string;
+}
+
+/**
+ * Global settings for the bank UI.
+ */
+const defaultSettings: MerchantUiSettings = {
+ backendBaseURL: buildDefaultBackendBaseURL(),
+};
+
+const codecForBankUISettings = (): Codec<MerchantUiSettings> =>
+ buildCodecForObject<MerchantUiSettings>()
+ .property("backendBaseURL", codecOptional(codecForString()))
+ .build("MerchantUiSettings");
+
+function removeUndefineField<T extends object>(obj: T): T {
+ const keys = Object.keys(obj) as Array<keyof T>;
+ return keys.reduce((prev, cur) => {
+ if (typeof prev[cur] === "undefined") {
+ delete prev[cur];
+ }
+ return prev;
+ }, obj);
+}
+
+export function fetchSettings(listener: (s: MerchantUiSettings) => void): void {
+ fetch("./settings.json")
+ .then((resp) => resp.json())
+ .then((json) => codecForBankUISettings().decode(json))
+ .then((result) =>
+ listener({
+ ...defaultSettings,
+ ...removeUndefineField(result),
+ }),
+ )
+ .catch((e) => {
+ console.log("failed to fetch settings", e);
+ listener(defaultSettings);
+ });
+}
+
+export function buildDefaultBackendBaseURL(): string {
+ if (typeof window !== "undefined") {
+ const currentLocation = new URL(
+ window.location.pathname,
+ window.location.origin,
+ ).href;
+ /**
+ * 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 canonicalizeBaseUrl(currentLocation.replace("/webui", ""));
+ }
+ throw Error("No default URL")
+}
diff --git a/packages/merchant-backoffice-ui/src/stories.test.ts b/packages/merchant-backoffice-ui/src/stories.test.ts
new file mode 100644
index 000000000..6ce88b916
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/stories.test.ts
@@ -0,0 +1,44 @@
+/*
+ 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 { 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/merchant-backoffice-ui/src/stories.tsx b/packages/merchant-backoffice-ui/src/stories.tsx
index b7136d185..8bb06b8cb 100644
--- a/packages/merchant-backoffice-ui/src/stories.tsx
+++ b/packages/merchant-backoffice-ui/src/stories.tsx
@@ -1,17 +1,17 @@
/*
- This file is part of GNU Anastasis
- (C) 2021-2022 Anastasis SARL
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
- GNU Anastasis is free software; you can redistribute it and/or modify it under the
- terms of the GNU Affero General Public License as published by the Free Software
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
- GNU Anastasis is distributed in the hope that it will be useful, but WITHOUT ANY
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- You should have received a copy of the GNU Affero General Public License along with
- GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
@@ -20,9 +20,11 @@
*/
import { strings } from "./i18n/strings.js";
-import * as pages from "./paths/index.stories.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/lib/index.browser";
+import { renderStories } from "@gnu-taler/web-util/browser";
import "./scss/main.scss";
@@ -32,7 +34,7 @@ function SortStories(a: any, b: any): number {
function main(): void {
renderStories(
- { pages },
+ { admin, instance, components },
{
strings,
},
diff --git a/packages/merchant-backoffice-ui/src/sw.js b/packages/merchant-backoffice-ui/src/sw.js
index 5fcde8281..bf52db6fa 100644
--- a/packages/merchant-backoffice-ui/src/sw.js
+++ b/packages/merchant-backoffice-ui/src/sw.js
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2021-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- /**
+/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
diff --git a/packages/merchant-backoffice-ui/src/template.html b/packages/merchant-backoffice-ui/src/template.html
deleted file mode 100644
index ccccab167..000000000
--- a/packages/merchant-backoffice-ui/src/template.html
+++ /dev/null
@@ -1,52 +0,0 @@
-<!--
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
-
- @author Sebastian Javier Marchano
--->
-<!DOCTYPE html>
-<html lang="en" class="has-aside-left has-aside-mobile-transition has-navbar-fixed-top has-aside-expanded">
- <head>
- <meta charset="utf-8">
- <title><%= htmlWebpackPlugin.options.title %></title>
- <meta name="viewport" content="width=device-width,initial-scale=1">
- <meta name="mobile-web-app-capable" content="yes">
- <meta name="apple-mobile-web-app-capable" content="yes">
-
- <link rel="icon" href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/Qn3//3ryn/795Tf/WrpP/2LCW/8B6T//w4Nb///////Pn4P+/d0v/9u3n/+7d0v/EhV7//v///+HDr//fxLD/zph2/+TJt//8/Pv/woBX//Lm3f/y5dz/v3hN//bu6f/JjGn/4sW0///////Df1j/8OLZ//v6+P+/elH/+vj1//jy7f+/elL//////+zYzP/Eg13//////967p//MlHT/wn5X///////v4Nb/yY1s///////jw7H/06KG////////////z5t9/+fNvf//////x4pn//Pp4v/8+vn/w39X/8WEX///////5s/A/9CbfP//////27Oc/9y2n////////////9itlf/gu6f//////86Vdf/r2Mz//////8SCXP/Df1j//////+7d0v/KkG7//////+HBrf/VpYr////////////RnoH/5sq6///////Ii2n/8ubf//39/P/Cf1j/xohk/+bNvv//////wn5W//Tq4//58/D/wHxV//7+/f/59fH/v3xU//39/P/w4Nf/xIFb///////hw7H/yo9t/+/f1f/AeU3/+/n2/+nSxP/FhmD//////9qzm//Upon/4MSx/96+qf//////xINc/+3bz//48e3/v3hN//Pn3///////6M+//752S//gw6//06aK/8J+VP/kzLr/zZd1/8OCWv/q18r/17KZ/9Ooi//fv6r/v3dK/+vWyP///////v39///////27un/1aeK/9Opjv/m1cf/1KCC/9a0nP/n08T/0Jx8/82YdP/QnHz/16yR//jx7P///////v39///////+/f3///7+///////+//7//v7+///////+/v7//v/+/////////////////////////v7//v79///////////////////+/v/+/Pv//v39///+/v/+/Pv///7+//7+/f/+/Pv//v39//79/P/+/Pv///7+////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" />
- <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
-
- <% if (htmlWebpackPlugin.options.manifest.theme_color) { %>
- <meta name="theme-color" content="<%= htmlWebpackPlugin.options.manifest.theme_color %>">
- <% } %>
-
- <% for (const index in htmlWebpackPlugin.files.css) { %>
- <% const file = htmlWebpackPlugin.files.css[index] %>
- <style data-href='<%= file %>' >
- <%= compilation.assets[file.substr(htmlWebpackPlugin.files.publicPath.length)].source() %>
- </style>
- <% } %>
-
- </head>
- <body>
-
- <script>
- <%= compilation.assets[htmlWebpackPlugin.files.chunks["polyfills"].entry.substr(htmlWebpackPlugin.files.publicPath.length)].source() %>
- </script>
- <script>
- <%= compilation.assets[htmlWebpackPlugin.files.chunks["bundle"].entry.substr(htmlWebpackPlugin.files.publicPath.length)].source() %>
- </script>
-
- </body>
-</html>
diff --git a/packages/merchant-backoffice-ui/src/utils/amount.ts b/packages/merchant-backoffice-ui/src/utils/amount.ts
index 575d70d87..c94101b4b 100644
--- a/packages/merchant-backoffice-ui/src/utils/amount.ts
+++ b/packages/merchant-backoffice-ui/src/utils/amount.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,20 +13,12 @@
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { amountFractionalBase, AmountJson, Amounts } from "@gnu-taler/taler-util";
-import { MerchantBackend } from "../declaration.js";
-
-/**
- * sums two prices,
- * @param one
- * @param two
- * @returns
- */
-const sumPrices = (one: string, two: string) => {
- const [currency, valueOne] = one.split(':')
- const [, valueTwo] = two.split(':')
- return `${currency}:${parseInt(valueOne, 10) + parseInt(valueTwo, 10)}`
-}
+import {
+ amountFractionalBase,
+ AmountJson,
+ Amounts,
+ TalerMerchantApi,
+} from "@gnu-taler/taler-util";
/**
* merge refund with the same description and a difference less than one minute
@@ -34,37 +26,46 @@ const sumPrices = (one: string, two: string) => {
* @param cur new refund to add to the list
* @returns list with the new refund, may be merged with the last
*/
-export function mergeRefunds(prev: MerchantBackend.Orders.RefundDetails[], cur: MerchantBackend.Orders.RefundDetails) {
+export function mergeRefunds(
+ prev: TalerMerchantApi.RefundDetails[],
+ cur: TalerMerchantApi.RefundDetails,
+): TalerMerchantApi.RefundDetails[] {
let tail;
- if (prev.length === 0 || //empty list
- cur.timestamp.t_s === 'never' || //current does not have timestamp
- (tail = prev[prev.length - 1]).timestamp.t_s === 'never' || // last does not have timestamp
+ if (
+ prev.length === 0 || //empty list
+ cur.timestamp.t_s === "never" || //current does not have timestamp
+ (tail = prev[prev.length - 1]).timestamp.t_s === "never" || // last does not have timestamp
cur.reason !== tail.reason || //different reason
cur.pending !== tail.pending || //different pending state
- Math.abs(cur.timestamp.t_s - tail.timestamp.t_s) > 1000 * 60) {//more than 1 minute difference
+ Math.abs(cur.timestamp.t_s - tail.timestamp.t_s) > 1000 * 60
+ ) {
+ //more than 1 minute difference
- prev.push(cur)
- return prev
+ //can't merge refunds, they are different or to distant in time
+ prev.push(cur);
+ return prev;
}
+ const a = Amounts.parseOrThrow(tail.amount);
+ const b = Amounts.parseOrThrow(cur.amount);
+ const r = Amounts.add(a, b).amount;
+
prev[prev.length - 1] = {
...tail,
- amount: sumPrices(tail.amount, cur.amount)
- }
+ amount: Amounts.stringify(r),
+ };
- return prev
+ return prev;
}
-export const rate = (one: string, two: string) => {
- const a = Amounts.parseOrThrow(one)
- const b = Amounts.parseOrThrow(two)
- const af = toFloat(a)
- const bf = toFloat(b)
- if (bf === 0) return 0
- return af / bf
+export function rate(a: AmountJson, b: AmountJson): number {
+ const af = toFloat(a);
+ const bf = toFloat(b);
+ if (bf === 0) return 0;
+ return af / bf;
}
-function toFloat(amount: AmountJson) {
- return amount.value + (amount.fraction / amountFractionalBase);
+function toFloat(amount: AmountJson): number {
+ return amount.value + amount.fraction / amountFractionalBase;
}
diff --git a/packages/merchant-backoffice-ui/src/utils/constants.ts b/packages/merchant-backoffice-ui/src/utils/constants.ts
index 5356a1a49..6b4d8eade 100644
--- a/packages/merchant-backoffice-ui/src/utils/constants.ts
+++ b/packages/merchant-backoffice-ui/src/utils/constants.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,36 +15,37 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
//https://tools.ietf.org/html/rfc8905
-export const PAYTO_REGEX = /^payto:\/\/[a-zA-Z][a-zA-Z0-9-.]+(\/[a-zA-Z0-9\-\.\~\(\)@_%:!$&'*+,;=]*)*\??((amount|receiver-name|sender-name|instruction|message)=[a-zA-Z0-9\-\.\~\(\)@_%:!$'*+,;=]*&?)*$/
-export const PAYTO_WIRE_METHOD_LOOKUP = /payto:\/\/([a-zA-Z][a-zA-Z0-9-.]+)\/.*/
-
-export const AMOUNT_REGEX = /^[a-zA-Z][a-zA-Z]*:[0-9][0-9,]*\.?[0-9,]*$/
+export const PAYTO_REGEX =
+ /^payto:\/\/[a-zA-Z][a-zA-Z0-9-.]+(\/[a-zA-Z0-9\-\.\~\(\)@_%:!$&'*+,;=]*)*\??((amount|receiver-name|sender-name|instruction|message)=[a-zA-Z0-9\-\.\~\(\)@_%:!$'*+,;=]*&?)*$/;
+export const PAYTO_WIRE_METHOD_LOOKUP =
+ /payto:\/\/([a-zA-Z][a-zA-Z0-9-.]+)\/.*/;
-export const INSTANCE_ID_LOOKUP = /\/instances\/([^/]*)\/?$/
+export const AMOUNT_REGEX = /^[a-zA-Z][a-zA-Z]{1,11}:[0-9][0-9,]*\.?[0-9,]*$/;
-export const AMOUNT_ZERO_REGEX = /^[a-zA-Z][a-zA-Z]*:0$/
+export const AMOUNT_ZERO_REGEX = /^[a-zA-Z][a-zA-Z]*:0$/;
-export const CROCKFORD_BASE32_REGEX = /^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]+[*~$=U]*$/
+export const CROCKFORD_BASE32_REGEX =
+ /^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]+[*~$=U]*$/;
-export const URL_REGEX = /^((https?:)(\/\/\/?)([\w]*(?::[\w]*)?@)?([\d\w\.-]+)(?::(\d+))?)\/$/
+export const URL_REGEX =
+ /^((https?:)(\/\/\/?)([\w]*(?::[\w]*)?@)?([\d\w\.-]+)(?::(\d+))?)\/$/;
-// how much rows we add every time user hit load more
-export const PAGE_SIZE = 20
-// how bigger can be the result set
-// after this threshold, load more with move the cursor
-export const MAX_RESULT_SIZE = PAGE_SIZE * 2 - 1;
+export const PAGINATED_LIST_SIZE = 5;
+// when doing paginated request, ask for one more
+// and use it to know if there are more to request
+export const PAGINATED_LIST_REQUEST = PAGINATED_LIST_SIZE + 1;
// how much we will wait for all request, in seconds
export const DEFAULT_REQUEST_TIMEOUT = 10;
export const MAX_IMAGE_SIZE = 1024 * 1024;
-export const INSTANCE_ID_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9_.@-]+$/
+export const INSTANCE_ID_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9_.@-]+$/;
export const COUNTRY_TABLE = {
AE: "U.A.E.",
@@ -189,6 +190,5 @@ export const COUNTRY_TABLE = {
VN: "Viet Nam",
YE: "Yemen",
ZA: "South Africa",
- ZW: "Zimbabwe"
-}
-
+ ZW: "Zimbabwe",
+};
diff --git a/packages/merchant-backoffice-ui/src/utils/regex.test.ts b/packages/merchant-backoffice-ui/src/utils/regex.test.ts
new file mode 100644
index 000000000..78f2ef5ae
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/utils/regex.test.ts
@@ -0,0 +1,88 @@
+/*
+ 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 { expect } from "chai";
+import { AMOUNT_REGEX, PAYTO_REGEX } from "./constants.js";
+
+describe("payto uri format", () => {
+ const valids = [
+ "payto://iban/DE75512108001245126199?amount=EUR:200.0&message=hello",
+ "payto://ach/122000661/1234",
+ "payto://upi/alice@example.com?receiver-name=Alice&amount=INR:200",
+ "payto://void/?amount=EUR:10.5",
+ "payto://ilp/g.acme.bob",
+ ];
+
+ it("should be valid", () => {
+ valids.forEach((v) => expect(v).match(PAYTO_REGEX));
+ });
+
+ const invalids = [
+ // has two question marks
+ "payto://iban/DE75?512108001245126199?amount=EUR:200.0&message=hello",
+ // has a space
+ "payto://ach /122000661/1234",
+ // has a space
+ "payto://upi/alice@ example.com?receiver-name=Alice&amount=INR:200",
+ // invalid field name (mount instead of amount)
+ "payto://void/?mount=EUR:10.5",
+ // payto:// is incomplete
+ "payto: //ilp/g.acme.bob",
+ ];
+
+ it("should not be valid", () => {
+ invalids.forEach((v) => expect(v).not.match(PAYTO_REGEX));
+ });
+});
+
+describe("amount format", () => {
+ const valids = [
+ "ARS:10",
+ "COL:10.2",
+ "UY:1,000.2",
+ "ARS:10.123,123",
+ "ARS:1,000,000",
+ "ARSCOL:10",
+ "LONGESTCURR:1,000,000.123,123",
+ ];
+
+
+ it("should be valid", () => {
+ valids.forEach((v) => expect(v).match(AMOUNT_REGEX));
+ });
+
+ const invalids = [
+ //no currency name
+ ":10",
+ //use . instead of ,
+ "ARS:1.000.000",
+ //currency name with numbers
+ "1ARS:10",
+ //currency name with numbers
+ "AR5:10",
+ //missing value
+ "USD:",
+ ];
+
+ it("should not be valid", () => {
+ invalids.forEach((v) => expect(v).not.match(AMOUNT_REGEX));
+ });
+});
diff --git a/packages/merchant-backoffice-ui/src/utils/switchableAxios.ts b/packages/merchant-backoffice-ui/src/utils/switchableAxios.ts
deleted file mode 100644
index be7eedd48..000000000
--- a/packages/merchant-backoffice-ui/src/utils/switchableAxios.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- 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/>
- */
-
-import axios, { AxiosPromise, AxiosRequestConfig } from "axios";
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-export let removeAxiosCancelToken = false;
-
-export let axiosHandler = function doAxiosRequest(config: AxiosRequestConfig): AxiosPromise<any> {
- return axios(config)
-}
-
-/**
- * Set this backend library to testing mode.
- * Instead of calling the axios library the @handler will be called
- *
- * @param handler callback that will mock axios
- */
-export function setAxiosRequestAsTestingEnvironment(handler: AxiosHandler): void {
- removeAxiosCancelToken = true;
- axiosHandler = function defaultTestingHandler(config) {
- const currentHanlder = listOfHandlersToUseOnce.shift()
- if (!currentHanlder) {
- return handler(config)
- }
-
- return currentHanlder(config)
- }
-}
-
-type AxiosHandler = (config: AxiosRequestConfig) => AxiosPromise<any>;
-type AxiosArguments = { args: AxiosRequestConfig | undefined }
-
-
-const listOfHandlersToUseOnce = new Array<AxiosHandler>()
-
-/**
- *
- * @param handler mock function
- * @returns savedArgs
- */
-export function mockAxiosOnce(handler: AxiosHandler): { args: AxiosRequestConfig | undefined } {
- const savedArgs: AxiosArguments = { args: undefined }
- listOfHandlersToUseOnce.push((config: AxiosRequestConfig): AxiosPromise<any> => {
- savedArgs.args = config;
- return handler(config)
- })
- return savedArgs;
-}
diff --git a/packages/merchant-backoffice-ui/src/utils/table.ts b/packages/merchant-backoffice-ui/src/utils/table.ts
index 199e5fda5..982b68e5e 100644
--- a/packages/merchant-backoffice-ui/src/utils/table.ts
+++ b/packages/merchant-backoffice-ui/src/utils/table.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2021-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -14,40 +14,43 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { WithId } from "../declaration.js";
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
export interface Actions<T extends WithId> {
element: T;
- type: 'DELETE' | 'UPDATE';
+ type: "DELETE" | "UPDATE";
}
function notEmpty<TValue>(value: TValue | null | undefined): value is TValue {
return value !== null && value !== undefined;
}
-export function buildActions<T extends WithId>(instances: T[], selected: string[], action: 'DELETE'): Actions<T>[] {
- return selected.map(id => instances.find(i => i.id === id))
+export function buildActions<T extends WithId>(
+ instances: T[],
+ selected: string[],
+ action: "DELETE",
+): Actions<T>[] {
+ return selected
+ .map((id) => instances.find((i) => i.id === id))
.filter(notEmpty)
- .map(id => ({ element: id, type: action }))
+ .map((id) => ({ element: id, type: action }));
}
/**
* For any object or array, return the same object if is not empty.
- * not empty:
+ * not empty:
* - for arrays: at least one element not undefined
* - for objects: at least one property not undefined
- * @param obj
- * @returns
+ * @param obj
+ * @returns
*/
-export function undefinedIfEmpty<T extends Record<string, unknown>|Array<unknown>>(obj: T): T | undefined {
- if (obj === undefined) return undefined
- return Object.values(obj).some((v) => v !== undefined)
- ? obj
- : undefined;
+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/merchant-backoffice-ui/src/utils/types.ts b/packages/merchant-backoffice-ui/src/utils/types.ts
index a3f23ac10..9ce6da4d1 100644
--- a/packages/merchant-backoffice-ui/src/utils/types.ts
+++ b/packages/merchant-backoffice-ui/src/utils/types.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 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,7 +23,7 @@ export interface KeyValue {
export interface Notification {
message: string;
description?: string | VNode;
- details?: string | VNode;
+ details?: string | VNode | string;
type: MessageType;
}
diff --git a/packages/merchant-backoffice-ui/test.mjs b/packages/merchant-backoffice-ui/test.mjs
new file mode 100755
index 000000000..be76348e5
--- /dev/null
+++ b/packages/merchant-backoffice-ui/test.mjs
@@ -0,0 +1,31 @@
+#!/usr/bin/env node
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { build } from "@gnu-taler/web-util/build";
+import { getFilesInDirectory } from "@gnu-taler/web-util/build";
+
+const allTestFiles = getFilesInDirectory("src", /.test.tsx?$/);
+
+await build({
+ type: "test",
+ source: {
+ js: allTestFiles.files,
+ assets: [{base:"src",files:["src/index.html"]}],
+ },
+ destination: "./dist/test",
+ css: "sass",
+});
diff --git a/packages/merchant-backoffice-ui/tests/__mocks__/fileMocks.ts b/packages/merchant-backoffice-ui/tests/__mocks__/fileMocks.ts
deleted file mode 100644
index 0c045e9d1..000000000
--- a/packages/merchant-backoffice-ui/tests/__mocks__/fileMocks.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
- /**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-// This fixed an error related to the CSS and loading gif breaking my Jest test
-// See https://facebook.github.io/jest/docs/en/webpack.html#handling-static-assets
-export default 'test-file-stub';
diff --git a/packages/merchant-backoffice-ui/tests/__mocks__/fileTransformer.js b/packages/merchant-backoffice-ui/tests/__mocks__/fileTransformer.js
deleted file mode 100644
index e6193f8fd..000000000
--- a/packages/merchant-backoffice-ui/tests/__mocks__/fileTransformer.js
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-// fileTransformer.js
-
-// eslint-disable-next-line @typescript-eslint/no-var-requires
-const path = require('path');
-
-module.exports = {
- process(src, filename, config, options) {
- return 'module.exports = ' + JSON.stringify(path.basename(filename)) + ';';
- },
-};
-
diff --git a/packages/merchant-backoffice-ui/tests/axiosMock.ts b/packages/merchant-backoffice-ui/tests/axiosMock.ts
deleted file mode 100644
index 5bb8694c9..000000000
--- a/packages/merchant-backoffice-ui/tests/axiosMock.ts
+++ /dev/null
@@ -1,445 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-import * as axios from 'axios';
-import { MerchantBackend } from "../src/declaration.js";
-import { mockAxiosOnce, setAxiosRequestAsTestingEnvironment } from "../src/utils/switchableAxios.js";
-// import { mockAxiosOnce, setAxiosRequestAsTestingEnvironment } from "../src/hooks/backend.js";
-
-export type Query<Req, Res> = (GetQuery | PostQuery | DeleteQuery | PatchQuery) & RequestResponse<Req, Res>
-
-interface RequestResponse<Req, Res> {
- code?: number,
-}
-interface GetQuery { get: string }
-interface PostQuery { post: string }
-interface DeleteQuery { delete: string }
-interface PatchQuery { patch: string }
-
-
-const JEST_DEBUG_LOG = process.env['JEST_DEBUG_LOG'] !== undefined
-
-type ExpectationValues = { query: Query<any, any>; params?: { auth?: string, request?: any, qparam?: any, response?: any } }
-
-type TestValues = [axios.AxiosRequestConfig | undefined, ExpectationValues | undefined]
-
-const defaultCallback = (actualQuery?: axios.AxiosRequestConfig): axios.AxiosPromise<any> => {
- if (JEST_DEBUG_LOG) {
- console.log('UNEXPECTED QUERY', actualQuery)
- }
- throw Error('Default Axios mock callback is called, this mean that the test did a tried to use axios but there was no expectation in place, try using JEST_DEBUG_LOG env')
-}
-
-setAxiosRequestAsTestingEnvironment(
- defaultCallback
-);
-
-export class AxiosMockEnvironment {
- expectations: Array<{
- query: Query<any, any>,
- auth?: string,
- params?: { request?: any, qparam?: any, response?: any },
- result: { args: axios.AxiosRequestConfig | undefined }
- } | undefined> = []
- // axiosMock: jest.MockedFunction<axios.AxiosStatic>
-
- addRequestExpectation<RequestType, ResponseType>(expectedQuery: Query<RequestType, ResponseType>, params: { auth?: string, request?: RequestType, qparam?: any, response?: ResponseType }): void {
- const result = mockAxiosOnce(function (actualQuery?: axios.AxiosRequestConfig): axios.AxiosPromise {
-
- if (JEST_DEBUG_LOG) {
- console.log('query to the backend is made', actualQuery)
- }
- if (!expectedQuery) {
- return Promise.reject("a query was made but it was not expected")
- }
- if (JEST_DEBUG_LOG) {
- console.log('expected query:', params?.request)
- console.log('expected qparams:', params?.qparam)
- console.log('sending response:', params?.response)
- }
-
- const responseCode = expectedQuery.code || 200
-
- //This response is what buildRequestOk is expecting in file hook/backend.ts
- if (responseCode >= 200 && responseCode < 300) {
- return Promise.resolve({
- data: params?.response, config: {
- data: params?.response,
- params: actualQuery?.params || {},
- }, request: { params: actualQuery?.params || {} }
- } as any);
- }
- //This response is what buildRequestFailed is expecting in file hook/backend.ts
- return Promise.reject({
- response: {
- status: responseCode
- },
- request: {
- data: params?.response,
- params: actualQuery?.params || {},
- }
- })
-
- } as any)
-
- this.expectations.push(expectedQuery ? { query: expectedQuery, params, result } : undefined)
- }
-
- getLastTestValues(): TestValues {
- const expectedQuery = this.expectations.shift()
-
- return [
- expectedQuery?.result.args, expectedQuery
- ]
- }
-
-}
-
-export function assertJustExpectedRequestWereMade(env: AxiosMockEnvironment): void {
- let size = env.expectations.length
- while (size-- > 0) {
- assertNextRequest(env)
- }
- assertNoMoreRequestWereMade(env)
-}
-
-export function assertNoMoreRequestWereMade(env: AxiosMockEnvironment): void {
- const [actualQuery, expectedQuery] = env.getLastTestValues()
-
- expect(actualQuery).toBeUndefined();
- expect(expectedQuery).toBeUndefined();
-}
-
-export function assertNextRequest(env: AxiosMockEnvironment): void {
- const [actualQuery, expectedQuery] = env.getLastTestValues()
-
- if (!actualQuery) {
- //expected one query but the tested component didn't execute one
- expect(actualQuery).toBe(expectedQuery);
- return
- }
-
- if (!expectedQuery) {
- const errorMessage = 'a query was made to the backend but the test explicitly expected no query';
- if (JEST_DEBUG_LOG) {
- console.log(errorMessage, actualQuery)
- }
- throw Error(errorMessage)
- }
- if ('get' in expectedQuery.query) {
- expect(actualQuery.method).toBe('get');
- expect(actualQuery.url).toBe(expectedQuery.query.get);
- }
- if ('post' in expectedQuery.query) {
- expect(actualQuery.method).toBe('post');
- expect(actualQuery.url).toBe(expectedQuery.query.post);
- }
- if ('delete' in expectedQuery.query) {
- expect(actualQuery.method).toBe('delete');
- expect(actualQuery.url).toBe(expectedQuery.query.delete);
- }
- if ('patch' in expectedQuery.query) {
- expect(actualQuery.method).toBe('patch');
- expect(actualQuery.url).toBe(expectedQuery.query.patch);
- }
-
- if (expectedQuery.params?.request) {
- expect(actualQuery.data).toMatchObject(expectedQuery.params.request)
- }
- if (expectedQuery.params?.qparam) {
- expect(actualQuery.params).toMatchObject(expectedQuery.params.qparam)
- }
-
- if (expectedQuery.params?.auth) {
- expect(actualQuery.headers.Authorization).toBe(expectedQuery.params?.auth)
- }
-
-}
-
-////////////////////
-// ORDER
-////////////////////
-
-export const API_CREATE_ORDER: Query<
- MerchantBackend.Orders.PostOrderRequest,
- MerchantBackend.Orders.PostOrderResponse
-> = {
- post: "http://backend/instances/default/private/orders",
-};
-
-export const API_GET_ORDER_BY_ID = (
- id: string
-): Query<
- unknown,
- MerchantBackend.Orders.MerchantOrderStatusResponse
-> => ({
- get: `http://backend/instances/default/private/orders/${id}`,
-});
-
-export const API_LIST_ORDERS: Query<
- unknown,
- MerchantBackend.Orders.OrderHistory
-> = {
- get: "http://backend/instances/default/private/orders",
-};
-
-export const API_REFUND_ORDER_BY_ID = (
- id: string
-): Query<
- MerchantBackend.Orders.RefundRequest,
- MerchantBackend.Orders.MerchantRefundResponse
-> => ({
- post: `http://backend/instances/default/private/orders/${id}/refund`,
-});
-
-export const API_FORGET_ORDER_BY_ID = (
- id: string
-): Query<
- MerchantBackend.Orders.ForgetRequest,
- unknown
-> => ({
- patch: `http://backend/instances/default/private/orders/${id}/forget`,
-});
-
-export const API_DELETE_ORDER = (
- id: string
-): Query<
- MerchantBackend.Orders.ForgetRequest,
- unknown
-> => ({
- delete: `http://backend/instances/default/private/orders/${id}`,
-});
-
-////////////////////
-// TRANSFER
-////////////////////
-
-export const API_LIST_TRANSFERS: Query<
- unknown,
- MerchantBackend.Transfers.TransferList
-> = {
- get: "http://backend/instances/default/private/transfers",
-};
-
-export const API_INFORM_TRANSFERS: Query<
- MerchantBackend.Transfers.TransferInformation,
- MerchantBackend.Transfers.MerchantTrackTransferResponse
-> = {
- post: "http://backend/instances/default/private/transfers",
-};
-
-////////////////////
-// PRODUCT
-////////////////////
-
-export const API_CREATE_PRODUCT: Query<
- MerchantBackend.Products.ProductAddDetail,
- unknown
-> = {
- post: "http://backend/instances/default/private/products",
-};
-
-export const API_LIST_PRODUCTS: Query<
- unknown,
- MerchantBackend.Products.InventorySummaryResponse
-> = {
- get: "http://backend/instances/default/private/products",
-};
-
-export const API_GET_PRODUCT_BY_ID = (
- id: string
-): Query<unknown, MerchantBackend.Products.ProductDetail> => ({
- get: `http://backend/instances/default/private/products/${id}`,
-});
-
-export const API_UPDATE_PRODUCT_BY_ID = (
- id: string
-): Query<
- MerchantBackend.Products.ProductPatchDetail,
- MerchantBackend.Products.InventorySummaryResponse
-> => ({
- patch: `http://backend/instances/default/private/products/${id}`,
-});
-
-export const API_DELETE_PRODUCT = (
- id: string
-): Query<
- unknown, unknown
-> => ({
- delete: `http://backend/instances/default/private/products/${id}`,
-});
-
-////////////////////
-// RESERVES
-////////////////////
-
-export const API_CREATE_RESERVE: Query<
- MerchantBackend.Tips.ReserveCreateRequest,
- MerchantBackend.Tips.ReserveCreateConfirmation
-> = {
- post: "http://backend/instances/default/private/reserves",
-};
-export const API_LIST_RESERVES: Query<
- unknown,
- MerchantBackend.Tips.TippingReserveStatus
-> = {
- get: "http://backend/instances/default/private/reserves",
-};
-
-export const API_GET_RESERVE_BY_ID = (
- pub: string
-): Query<unknown, MerchantBackend.Tips.ReserveDetail> => ({
- get: `http://backend/instances/default/private/reserves/${pub}`,
-});
-
-export const API_GET_TIP_BY_ID = (
- pub: string
-): Query<
- unknown,
- MerchantBackend.Tips.TipDetails
-> => ({
- get: `http://backend/instances/default/private/tips/${pub}`,
-});
-
-export const API_AUTHORIZE_TIP_FOR_RESERVE = (
- pub: string
-): Query<
- MerchantBackend.Tips.TipCreateRequest,
- MerchantBackend.Tips.TipCreateConfirmation
-> => ({
- post: `http://backend/instances/default/private/reserves/${pub}/authorize-tip`,
-});
-
-export const API_AUTHORIZE_TIP: Query<
- MerchantBackend.Tips.TipCreateRequest,
- MerchantBackend.Tips.TipCreateConfirmation
-> = ({
- post: `http://backend/instances/default/private/tips`,
-});
-
-
-export const API_DELETE_RESERVE = (
- id: string
-): Query<unknown, unknown> => ({
- delete: `http://backend/instances/default/private/reserves/${id}`,
-});
-
-
-////////////////////
-// INSTANCE ADMIN
-////////////////////
-
-export const API_CREATE_INSTANCE: Query<
- MerchantBackend.Instances.InstanceConfigurationMessage,
- unknown
-> = {
- post: "http://backend/management/instances",
-};
-
-export const API_GET_INSTANCE_BY_ID = (
- id: string
-): Query<
- unknown,
- MerchantBackend.Instances.QueryInstancesResponse
-> => ({
- get: `http://backend/management/instances/${id}`,
-});
-
-export const API_GET_INSTANCE_KYC_BY_ID = (
- id: string
-): Query<
- unknown,
- MerchantBackend.Instances.AccountKycRedirects
-> => ({
- get: `http://backend/management/instances/${id}/kyc`,
-});
-
-export const API_LIST_INSTANCES: Query<
- unknown,
- MerchantBackend.Instances.InstancesResponse
-> = {
- get: "http://backend/management/instances",
-};
-
-export const API_UPDATE_INSTANCE_BY_ID = (
- id: string
-): Query<
- MerchantBackend.Instances.InstanceReconfigurationMessage,
- unknown
-> => ({
- patch: `http://backend/management/instances/${id}`,
-});
-
-export const API_UPDATE_INSTANCE_AUTH_BY_ID = (
- id: string
-): Query<
- MerchantBackend.Instances.InstanceAuthConfigurationMessage,
- unknown
-> => ({
- post: `http://backend/management/instances/${id}/auth`,
-});
-
-export const API_DELETE_INSTANCE = (
- id: string
-): Query<unknown, unknown> => ({
- delete: `http://backend/management/instances/${id}`,
-});
-
-////////////////////
-// INSTANCE
-////////////////////
-
-export const API_GET_CURRENT_INSTANCE: Query<
- unknown,
- MerchantBackend.Instances.QueryInstancesResponse
-> = ({
- get: `http://backend/instances/default/private/`,
-});
-
-export const API_GET_CURRENT_INSTANCE_KYC: Query<
- unknown,
- MerchantBackend.Instances.AccountKycRedirects
-> =
- ({
- get: `http://backend/instances/default/private/kyc`,
- });
-
-export const API_UPDATE_CURRENT_INSTANCE: Query<
- MerchantBackend.Instances.InstanceReconfigurationMessage,
- unknown
-> = {
- patch: `http://backend/instances/default/private/`,
-};
-
-export const API_UPDATE_CURRENT_INSTANCE_AUTH: Query<
- MerchantBackend.Instances.InstanceAuthConfigurationMessage,
- unknown
-> = {
- post: `http://backend/instances/default/private/auth`,
-};
-
-export const API_DELETE_CURRENT_INSTANCE: Query<
- unknown,
- unknown
-> = ({
- delete: `http://backend/instances/default/private`,
-});
-
-
diff --git a/packages/merchant-backoffice-ui/tests/context/backend.test.tsx b/packages/merchant-backoffice-ui/tests/context/backend.test.tsx
deleted file mode 100644
index 5e80d4adb..000000000
--- a/packages/merchant-backoffice-ui/tests/context/backend.test.tsx
+++ /dev/null
@@ -1,172 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import { renderHook } from "@testing-library/preact-hooks";
-import { ComponentChildren, h, VNode } from "preact";
-import { act } from "preact/test-utils";
-import { BackendContextProvider } from "../../src/context/backend.js";
-import { InstanceContextProvider } from "../../src/context/instance.js";
-import { MerchantBackend } from "../../src/declaration.js";
-import {
- useAdminAPI,
- useInstanceAPI,
- useManagementAPI,
-} from "../../src/hooks/instance.js";
-import {
- API_CREATE_INSTANCE,
- API_GET_CURRENT_INSTANCE,
- API_UPDATE_CURRENT_INSTANCE_AUTH,
- API_UPDATE_INSTANCE_AUTH_BY_ID,
- assertJustExpectedRequestWereMade,
- AxiosMockEnvironment,
-} from "../axiosMock.js";
-
-interface TestingContextProps {
- children?: ComponentChildren;
-}
-
-function TestingContext({ children }: TestingContextProps): VNode {
- return (
- <BackendContextProvider defaultUrl="http://backend" initialToken="token">
- {children}
- </BackendContextProvider>
- );
-}
-function AdminTestingContext({ children }: TestingContextProps): VNode {
- return (
- <BackendContextProvider defaultUrl="http://backend" initialToken="token">
- <InstanceContextProvider
- value={{
- token: "token",
- id: "default",
- admin: true,
- changeToken: () => null,
- }}
- >
- {children}
- </InstanceContextProvider>
- </BackendContextProvider>
- );
-}
-
-describe("backend context api ", () => {
- it("should use new token after updating the instance token in the settings as user", async () => {
- const env = new AxiosMockEnvironment();
-
- const { result, waitForNextUpdate } = renderHook(
- () => {
- const instance = useInstanceAPI();
- const management = useManagementAPI("default");
- const admin = useAdminAPI();
-
- return { instance, management, admin };
- },
- { wrapper: TestingContext }
- );
-
- if (!result.current) {
- expect(result.current).toBeDefined();
- return;
- }
-
- env.addRequestExpectation(API_UPDATE_INSTANCE_AUTH_BY_ID("default"), {
- request: {
- method: "token",
- token: "another_token",
- },
- response: {
- name: "instance_name",
- } as MerchantBackend.Instances.QueryInstancesResponse,
- });
-
- await act(async () => {
- await result.current?.management.setNewToken("another_token");
- });
-
- // await waitForNextUpdate({ timeout: 1 });
-
- assertJustExpectedRequestWereMade(env);
-
- env.addRequestExpectation(API_CREATE_INSTANCE, {
- auth: "Bearer another_token",
- request: {
- id: "new_instance_id",
- } as MerchantBackend.Instances.InstanceConfigurationMessage,
- });
-
- result.current.admin.createInstance({
- id: "new_instance_id",
- } as MerchantBackend.Instances.InstanceConfigurationMessage);
-
- assertJustExpectedRequestWereMade(env);
- });
-
- it("should use new token after updating the instance token in the settings as admin", async () => {
- const env = new AxiosMockEnvironment();
-
- const { result, waitForNextUpdate } = renderHook(
- () => {
- const instance = useInstanceAPI();
- const management = useManagementAPI("default");
- const admin = useAdminAPI();
-
- return { instance, management, admin };
- },
- { wrapper: AdminTestingContext }
- );
-
- if (!result.current) {
- expect(result.current).toBeDefined();
- return;
- }
-
- env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE_AUTH, {
- request: {
- method: "token",
- token: "another_token",
- },
- response: {
- name: "instance_name",
- } as MerchantBackend.Instances.QueryInstancesResponse,
- });
-
- await act(async () => {
- await result.current?.instance.setNewToken("another_token");
- });
-
- // await waitForNextUpdate({ timeout: 1 });
-
- assertJustExpectedRequestWereMade(env);
-
- env.addRequestExpectation(API_CREATE_INSTANCE, {
- auth: "Bearer another_token",
- request: {
- id: "new_instance_id",
- } as MerchantBackend.Instances.InstanceConfigurationMessage,
- });
-
- result.current.admin.createInstance({
- id: "new_instance_id",
- } as MerchantBackend.Instances.InstanceConfigurationMessage);
-
- assertJustExpectedRequestWereMade(env);
- });
-});
diff --git a/packages/merchant-backoffice-ui/tests/functions/regex.test.ts b/packages/merchant-backoffice-ui/tests/functions/regex.test.ts
deleted file mode 100644
index ed06fb655..000000000
--- a/packages/merchant-backoffice-ui/tests/functions/regex.test.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
- /**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import { AMOUNT_REGEX, PAYTO_REGEX } from "../../src/utils/constants.js";
-
-describe('payto uri format', () => {
- const valids = [
- 'payto://iban/DE75512108001245126199?amount=EUR:200.0&message=hello',
- 'payto://ach/122000661/1234',
- 'payto://upi/alice@example.com?receiver-name=Alice&amount=INR:200',
- 'payto://void/?amount=EUR:10.5',
- 'payto://ilp/g.acme.bob'
- ]
-
- test('should be valid', () => {
- valids.forEach(v => expect(v).toMatch(PAYTO_REGEX))
- });
-
- const invalids = [
- // has two question marks
- 'payto://iban/DE75?512108001245126199?amount=EUR:200.0&message=hello',
- // has a space
- 'payto://ach /122000661/1234',
- // has a space
- 'payto://upi/alice@ example.com?receiver-name=Alice&amount=INR:200',
- // invalid field name (mount instead of amount)
- 'payto://void/?mount=EUR:10.5',
- // payto:// is incomplete
- 'payto: //ilp/g.acme.bob'
- ]
-
- test('should not be valid', () => {
- invalids.forEach(v => expect(v).not.toMatch(PAYTO_REGEX))
- });
-})
-
-describe('amount format', () => {
- const valids = [
- 'ARS:10',
- 'COL:10.2',
- 'UY:1,000.2',
- 'ARS:10.123,123',
- 'ARS:1,000,000',
- 'ARSCOL:10',
- 'THISISTHEMOTHERCOIN:1,000,000.123,123',
- ]
-
- test('should be valid', () => {
- valids.forEach(v => expect(v).toMatch(AMOUNT_REGEX))
- });
-
- const invalids = [
- //no currency name
- ':10',
- //use . instead of ,
- 'ARS:1.000.000',
- //currency name with numbers
- '1ARS:10',
- //currency name with numbers
- 'AR5:10',
- //missing value
- 'USD:',
- ]
-
- test('should not be valid', () => {
- invalids.forEach(v => expect(v).not.toMatch(AMOUNT_REGEX))
- });
-
-}) \ No newline at end of file
diff --git a/packages/merchant-backoffice-ui/tests/header.test.tsx b/packages/merchant-backoffice-ui/tests/header.test.tsx
deleted file mode 100644
index d855346de..000000000
--- a/packages/merchant-backoffice-ui/tests/header.test.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import { h } from "preact";
-import { ProductList } from "../src/components/product/ProductList.js";
-// See: https://github.com/preactjs/enzyme-adapter-preact-pure
-// import { shallow } from 'enzyme';
-import * as backend from "../src/context/config.js";
-import { render, findAllByText } from "@testing-library/preact";
-import * as i18n from "../src/context/translation.js";
-
-import * as jedLib from "jed";
-const handler = new jedLib.Jed("en");
-
-describe("Initial Test of the Sidebar", () => {
- beforeEach(() => {
- jest
- .spyOn(backend, "useConfigContext")
- .mockImplementation(() => ({ version: "", currency: "" }));
- jest.spyOn(i18n, "useTranslationContext").mockImplementation(() => ({
- changeLanguage: () => null,
- handler,
- lang: "en",
- }));
- });
- test("Product list renders a table", () => {
- const context = render(
- <ProductList
- list={[
- {
- description: "description of the product",
- image: "asdasda",
- price: "USD:10",
- quantity: 1,
- taxes: [{ name: "VAT", tax: "EUR:1" }],
- unit: "book",
- },
- ]}
- />,
- );
-
- expect(context.findAllByText("description of the product")).toBeDefined();
- // expect(context.find('table tr td img').map(img => img.prop('src'))).toEqual('');
- });
-});
diff --git a/packages/merchant-backoffice-ui/tests/hooks/async.test.ts b/packages/merchant-backoffice-ui/tests/hooks/async.test.ts
deleted file mode 100644
index ebdfc9bb9..000000000
--- a/packages/merchant-backoffice-ui/tests/hooks/async.test.ts
+++ /dev/null
@@ -1,158 +0,0 @@
-/*
- 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/>
- */
-
-import { renderHook } from "@testing-library/preact-hooks"
-import { useAsync } from "../../src/hooks/async.js"
-
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-test("async function is called", async () => {
- jest.useFakeTimers()
-
- const timeout = 500
-
- const asyncFunction = jest.fn(() => new Promise((res) => {
- setTimeout(() => {
- res({ the_answer: 'yes' })
- }, timeout);
- }))
-
- const { result, waitForNextUpdate } = renderHook(() => {
- return useAsync(asyncFunction)
- })
-
- expect(result.current?.isLoading).toBeFalsy()
-
- result.current?.request()
- expect(asyncFunction).toBeCalled()
- await waitForNextUpdate({ timeout: 1 })
- expect(result.current?.isLoading).toBeTruthy()
-
- jest.advanceTimersByTime(timeout + 1)
- await waitForNextUpdate({ timeout: 1 })
- expect(result.current?.isLoading).toBeFalsy()
- expect(result.current?.data).toMatchObject({ the_answer: 'yes' })
- expect(result.current?.error).toBeUndefined()
- expect(result.current?.isSlow).toBeFalsy()
-})
-
-test("async function return error if rejected", async () => {
- jest.useFakeTimers()
-
- const timeout = 500
-
- const asyncFunction = jest.fn(() => new Promise((_, rej) => {
- setTimeout(() => {
- rej({ the_error: 'yes' })
- }, timeout);
- }))
-
- const { result, waitForNextUpdate } = renderHook(() => {
- return useAsync(asyncFunction)
- })
-
- expect(result.current?.isLoading).toBeFalsy()
-
- result.current?.request()
- expect(asyncFunction).toBeCalled()
- await waitForNextUpdate({ timeout: 1 })
- expect(result.current?.isLoading).toBeTruthy()
-
- jest.advanceTimersByTime(timeout + 1)
- await waitForNextUpdate({ timeout: 1 })
- expect(result.current?.isLoading).toBeFalsy()
- expect(result.current?.error).toMatchObject({ the_error: 'yes' })
- expect(result.current?.data).toBeUndefined()
- expect(result.current?.isSlow).toBeFalsy()
-})
-
-test("async function is slow", async () => {
- jest.useFakeTimers()
-
- const timeout = 2200
-
- const asyncFunction = jest.fn(() => new Promise((res) => {
- setTimeout(() => {
- res({ the_answer: 'yes' })
- }, timeout);
- }))
-
- const { result, waitForNextUpdate } = renderHook(() => {
- return useAsync(asyncFunction)
- })
-
- expect(result.current?.isLoading).toBeFalsy()
-
- result.current?.request()
- expect(asyncFunction).toBeCalled()
- await waitForNextUpdate({ timeout: 1 })
- expect(result.current?.isLoading).toBeTruthy()
-
- jest.advanceTimersByTime(timeout / 2)
- await waitForNextUpdate({ timeout: 1 })
- expect(result.current?.isLoading).toBeTruthy()
- expect(result.current?.isSlow).toBeTruthy()
- expect(result.current?.data).toBeUndefined()
- expect(result.current?.error).toBeUndefined()
-
- jest.advanceTimersByTime(timeout / 2)
- await waitForNextUpdate({ timeout: 1 })
- expect(result.current?.isLoading).toBeFalsy()
- expect(result.current?.data).toMatchObject({ the_answer: 'yes' })
- expect(result.current?.error).toBeUndefined()
- expect(result.current?.isSlow).toBeFalsy()
-
-})
-
-test("async function is cancellable", async () => {
- jest.useFakeTimers()
-
- const timeout = 2200
-
- const asyncFunction = jest.fn(() => new Promise((res) => {
- setTimeout(() => {
- res({ the_answer: 'yes' })
- }, timeout);
- }))
-
- const { result, waitForNextUpdate } = renderHook(() => {
- return useAsync(asyncFunction)
- })
-
- expect(result.current?.isLoading).toBeFalsy()
-
- result.current?.request()
- expect(asyncFunction).toBeCalled()
- await waitForNextUpdate({ timeout: 1 })
- expect(result.current?.isLoading).toBeTruthy()
-
- jest.advanceTimersByTime(timeout / 2)
- await waitForNextUpdate({ timeout: 1 })
- expect(result.current?.isLoading).toBeTruthy()
- expect(result.current?.isSlow).toBeTruthy()
- expect(result.current?.data).toBeUndefined()
- expect(result.current?.error).toBeUndefined()
-
- result.current?.cancel()
- await waitForNextUpdate({ timeout: 1 })
- expect(result.current?.isLoading).toBeFalsy()
- expect(result.current?.data).toBeUndefined()
- expect(result.current?.error).toBeUndefined()
- expect(result.current?.isSlow).toBeFalsy()
-
-})
diff --git a/packages/merchant-backoffice-ui/tests/hooks/listener.test.ts b/packages/merchant-backoffice-ui/tests/hooks/listener.test.ts
deleted file mode 100644
index 597243f76..000000000
--- a/packages/merchant-backoffice-ui/tests/hooks/listener.test.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { renderHook, act } from '@testing-library/preact-hooks';
-import { useListener } from "../../src/hooks/listener.js";
-
-// jest.useFakeTimers()
-
-test('listener', async () => {
-
-
- function createSomeString() {
- return "hello"
- }
- async function addWorldToTheEnd(resultFromComponentB: string) {
- return `${resultFromComponentB} world`
- }
- const expectedResult = "hello world"
-
- const { result } = renderHook(() => useListener(addWorldToTheEnd))
-
- expect(result.current).toBeDefined()
- if (!result.current) {
- return;
- }
-
- {
- const [activator, subscriber] = result.current
- expect(activator).toBeUndefined()
-
- act(() => {
- subscriber(createSomeString)
- })
-
- }
-
- const [activator] = result.current
- expect(activator).toBeDefined()
- if (!activator) return;
-
- const response = await activator()
- expect(response).toBe(expectedResult)
-
-});
diff --git a/packages/merchant-backoffice-ui/tests/hooks/notification.test.ts b/packages/merchant-backoffice-ui/tests/hooks/notification.test.ts
deleted file mode 100644
index 75cfcd015..000000000
--- a/packages/merchant-backoffice-ui/tests/hooks/notification.test.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
- /**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import { renderHook, act} from '@testing-library/preact-hooks';
-import { useNotifications } from "../../src/hooks/notifications.js";
-
-jest.useFakeTimers()
-
-test('notification should disappear after timeout', () => {
- jest.spyOn(global, 'setTimeout');
-
- const timeout = 1000
- const { result, rerender } = renderHook(() => useNotifications(undefined, timeout));
-
- expect(result.current?.notifications.length).toBe(0);
-
- act(() => {
- result.current?.pushNotification({
- message: 'some_id',
- type: 'INFO'
- });
- });
- expect(result.current?.notifications.length).toBe(1);
-
- jest.advanceTimersByTime(timeout/2);
- rerender()
- expect(result.current?.notifications.length).toBe(1);
-
- jest.advanceTimersByTime(timeout);
- rerender()
- expect(result.current?.notifications.length).toBe(0);
-
-});
diff --git a/packages/merchant-backoffice-ui/tests/hooks/swr/index.tsx b/packages/merchant-backoffice-ui/tests/hooks/swr/index.tsx
deleted file mode 100644
index 655081711..000000000
--- a/packages/merchant-backoffice-ui/tests/hooks/swr/index.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import { ComponentChildren, h, VNode } from "preact";
-import { SWRConfig } from "swr";
-import { BackendContextProvider } from "../../../src/context/backend.js";
-import { InstanceContextProvider } from "../../../src/context/instance.js";
-
-interface TestingContextProps {
- children?: ComponentChildren;
-}
-export function TestingContext({ children }: TestingContextProps): VNode {
- const SC: any = SWRConfig
- return (
- <BackendContextProvider defaultUrl="http://backend" initialToken="token">
- <InstanceContextProvider
- value={{
- token: "token",
- id: "default",
- admin: true,
- changeToken: () => null,
- }}
- >
- <SC value={{ provider: () => new Map() }}>{children}</SC>
- </InstanceContextProvider>
- </BackendContextProvider>
- );
-}
diff --git a/packages/merchant-backoffice-ui/tests/hooks/swr/instance.test.ts b/packages/merchant-backoffice-ui/tests/hooks/swr/instance.test.ts
deleted file mode 100644
index 2a1fd76ee..000000000
--- a/packages/merchant-backoffice-ui/tests/hooks/swr/instance.test.ts
+++ /dev/null
@@ -1,636 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import { renderHook } from "@testing-library/preact-hooks";
-import { act } from "preact/test-utils";
-import { MerchantBackend } from "../../../src/declaration.js";
-import { useAdminAPI, useBackendInstances, useInstanceAPI, useInstanceDetails, useManagementAPI } from "../../../src/hooks/instance.js";
-import {
- API_CREATE_INSTANCE,
- API_DELETE_INSTANCE,
- API_GET_CURRENT_INSTANCE,
- API_LIST_INSTANCES,
- API_UPDATE_CURRENT_INSTANCE,
- API_UPDATE_CURRENT_INSTANCE_AUTH,
- API_UPDATE_INSTANCE_AUTH_BY_ID,
- API_UPDATE_INSTANCE_BY_ID,
- assertJustExpectedRequestWereMade,
- AxiosMockEnvironment
-} from "../../axiosMock.js";
-import { TestingContext } from "./index.js";
-
-describe("instance api interaction with details", () => {
-
- it("should evict cache when updating an instance", async () => {
-
- const env = new AxiosMockEnvironment();
-
- env.addRequestExpectation(API_GET_CURRENT_INSTANCE, {
- response: {
- name: 'instance_name'
- } as MerchantBackend.Instances.QueryInstancesResponse,
- });
-
- const { result, waitForNextUpdate } = renderHook(
- () => {
- const api = useInstanceAPI();
- const query = useInstanceDetails();
-
- return { query, api };
- },
- { wrapper: TestingContext }
- );
-
- expect(result.current).toBeDefined();
- if (!result.current) {
- return;
- }
- expect(result.current.query.loading).toBeTruthy();
-
- await waitForNextUpdate({ timeout: 1 });
-
- assertJustExpectedRequestWereMade(env);
-
- expect(result.current.query.loading).toBeFalsy();
-
- expect(result.current?.query.ok).toBeTruthy();
- if (!result.current?.query.ok) return;
-
- expect(result.current.query.data).toEqual({
- name: 'instance_name'
- });
-
- env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE, {
- request: {
- name: 'other_name'
- } as MerchantBackend.Instances.InstanceReconfigurationMessage,
- });
-
- act(async () => {
- await result.current?.api.updateInstance({
- name: 'other_name'
- } as MerchantBackend.Instances.InstanceReconfigurationMessage);
- });
-
- assertJustExpectedRequestWereMade(env);
-
- env.addRequestExpectation(API_GET_CURRENT_INSTANCE, {
- response: {
- name: 'other_name'
- } as MerchantBackend.Instances.QueryInstancesResponse,
- });
-
- expect(result.current.query.loading).toBeFalsy();
-
- await waitForNextUpdate({ timeout: 1 });
-
- assertJustExpectedRequestWereMade(env);
-
- expect(result.current.query.loading).toBeFalsy();
- expect(result.current.query.ok).toBeTruthy();
-
- expect(result.current.query.data).toEqual({
- name: 'other_name'
- });
- });
-
- it("should evict cache when setting the instance's token", async () => {
- const env = new AxiosMockEnvironment();
-
- env.addRequestExpectation(API_GET_CURRENT_INSTANCE, {
- response: {
- name: 'instance_name',
- auth: {
- method: 'token',
- token: 'not-secret',
- }
- } as MerchantBackend.Instances.QueryInstancesResponse,
- });
-
- const { result, waitForNextUpdate } = renderHook(
- () => {
- const api = useInstanceAPI();
- const query = useInstanceDetails();
-
- return { query, api };
- },
- { wrapper: TestingContext }
- );
-
- expect(result.current).toBeDefined();
- if (!result.current) {
- return;
- }
- expect(result.current.query.loading).toBeTruthy();
-
- await waitForNextUpdate({ timeout: 1 });
-
- assertJustExpectedRequestWereMade(env);
-
- expect(result.current.query.loading).toBeFalsy();
-
- expect(result.current?.query.ok).toBeTruthy();
- if (!result.current?.query.ok) return;
-
- expect(result.current.query.data).toEqual({
- name: 'instance_name',
- auth: {
- method: 'token',
- token: 'not-secret',
- }
- });
-
- env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE_AUTH, {
- request: {
- method: 'token',
- token: 'secret'
- } as MerchantBackend.Instances.InstanceAuthConfigurationMessage,
- });
-
- act(async () => {
- await result.current?.api.setNewToken('secret');
- });
-
- assertJustExpectedRequestWereMade(env);
-
- env.addRequestExpectation(API_GET_CURRENT_INSTANCE, {
- response: {
- name: 'instance_name',
- auth: {
- method: 'token',
- token: 'secret',
- }
- } as MerchantBackend.Instances.QueryInstancesResponse,
- });
-
- expect(result.current.query.loading).toBeFalsy();
-
- await waitForNextUpdate({ timeout: 1 });
-
- assertJustExpectedRequestWereMade(env);
-
- expect(result.current.query.loading).toBeFalsy();
- expect(result.current.query.ok).toBeTruthy();
-
- expect(result.current.query.data).toEqual({
- name: 'instance_name',
- auth: {
- method: 'token',
- token: 'secret',
- }
- });
- });
-
- it("should evict cache when clearing the instance's token", async () => {
- const env = new AxiosMockEnvironment();
-
- env.addRequestExpectation(API_GET_CURRENT_INSTANCE, {
- response: {
- name: 'instance_name',
- auth: {
- method: 'token',
- token: 'not-secret',
- }
- } as MerchantBackend.Instances.QueryInstancesResponse,
- });
-
- const { result, waitForNextUpdate } = renderHook(
- () => {
- const api = useInstanceAPI();
- const query = useInstanceDetails();
-
- return { query, api };
- },
- { wrapper: TestingContext }
- );
-
- expect(result.current).toBeDefined();
- if (!result.current) {
- return;
- }
- expect(result.current.query.loading).toBeTruthy();
-
- await waitForNextUpdate({ timeout: 1 });
-
- assertJustExpectedRequestWereMade(env);
-
- expect(result.current.query.loading).toBeFalsy();
-
- expect(result.current?.query.ok).toBeTruthy();
- if (!result.current?.query.ok) return;
-
- expect(result.current.query.data).toEqual({
- name: 'instance_name',
- auth: {
- method: 'token',
- token: 'not-secret',
- }
- });
-
- env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE_AUTH, {
- request: {
- method: 'external',
- } as MerchantBackend.Instances.InstanceAuthConfigurationMessage,
- });
-
- act(async () => {
- await result.current?.api.clearToken();
- });
-
- assertJustExpectedRequestWereMade(env);
-
- env.addRequestExpectation(API_GET_CURRENT_INSTANCE, {
- response: {
- name: 'instance_name',
- auth: {
- method: 'external',
- }
- } as MerchantBackend.Instances.QueryInstancesResponse,
- });
-
- expect(result.current.query.loading).toBeFalsy();
-
- await waitForNextUpdate({ timeout: 1 });
-
- assertJustExpectedRequestWereMade(env);
-
- expect(result.current.query.loading).toBeFalsy();
- expect(result.current.query.ok).toBeTruthy();
-
- expect(result.current.query.data).toEqual({
- 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 AxiosMockEnvironment();
-
- env.addRequestExpectation(API_LIST_INSTANCES, {
- response: {
- instances: [{
- name: 'instance_name'
- } as MerchantBackend.Instances.Instance]
- },
- });
-
- const { result, waitForNextUpdate } = renderHook(
- () => {
- const api = useAdminAPI();
- const query = useBackendInstances();
-
- return { query, api };
- },
- { wrapper: TestingContext }
- );
-
- expect(result.current).toBeDefined();
- if (!result.current) {
- return;
- }
- expect(result.current.query.loading).toBeTruthy();
-
- await waitForNextUpdate({ timeout: 1 });
-
- assertJustExpectedRequestWereMade(env);
-
- expect(result.current.query.loading).toBeFalsy();
-
- expect(result.current?.query.ok).toBeTruthy();
- if (!result.current?.query.ok) return;
-
- expect(result.current.query.data).toEqual({
- instances: [{
- name: 'instance_name'
- }]
- });
-
- env.addRequestExpectation(API_CREATE_INSTANCE, {
- request: {
- name: 'other_name'
- } as MerchantBackend.Instances.InstanceConfigurationMessage,
- });
-
- act(async () => {
- await result.current?.api.createInstance({
- name: 'other_name'
- } as MerchantBackend.Instances.InstanceConfigurationMessage);
- });
-
- assertJustExpectedRequestWereMade(env);
-
- env.addRequestExpectation(API_LIST_INSTANCES, {
- response: {
- instances: [{
- name: 'instance_name'
- } as MerchantBackend.Instances.Instance,
- {
- name: 'other_name'
- } as MerchantBackend.Instances.Instance]
- },
- });
-
- expect(result.current.query.loading).toBeFalsy();
-
- await waitForNextUpdate({ timeout: 1 });
-
- assertJustExpectedRequestWereMade(env);
-
- expect(result.current.query.loading).toBeFalsy();
- expect(result.current.query.ok).toBeTruthy();
-
- expect(result.current.query.data).toEqual({
- instances: [{
- name: 'instance_name'
- }, {
- name: 'other_name'
- }]
- });
- });
-
- it("should evict cache when deleting an instance", async () => {
- const env = new AxiosMockEnvironment();
-
- 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 { result, waitForNextUpdate } = renderHook(
- () => {
- const api = useAdminAPI();
- const query = useBackendInstances();
-
- return { query, api };
- },
- { wrapper: TestingContext }
- );
-
- expect(result.current).toBeDefined();
- if (!result.current) {
- return;
- }
- expect(result.current.query.loading).toBeTruthy();
-
- await waitForNextUpdate({ timeout: 1 });
-
- assertJustExpectedRequestWereMade(env);
-
- expect(result.current.query.loading).toBeFalsy();
-
- expect(result.current?.query.ok).toBeTruthy();
- if (!result.current?.query.ok) return;
-
- expect(result.current.query.data).toEqual({
- 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');
- });
-
- assertJustExpectedRequestWereMade(env);
-
- env.addRequestExpectation(API_LIST_INSTANCES, {
- response: {
- instances: [{
- id: 'default',
- name: 'instance_name'
- } as MerchantBackend.Instances.Instance]
- },
- });
-
- expect(result.current.query.loading).toBeFalsy();
-
- await waitForNextUpdate({ timeout: 1 });
-
- assertJustExpectedRequestWereMade(env);
-
- expect(result.current.query.loading).toBeFalsy();
- expect(result.current.query.ok).toBeTruthy();
-
- expect(result.current.query.data).toEqual({
- instances: [{
- id: 'default',
- name: 'instance_name'
- }]
- });
- });
- it("should evict cache when deleting (purge) an instance", async () => {
- const env = new AxiosMockEnvironment();
-
- 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 { result, waitForNextUpdate } = renderHook(
- () => {
- const api = useAdminAPI();
- const query = useBackendInstances();
-
- return { query, api };
- },
- { wrapper: TestingContext }
- );
-
- expect(result.current).toBeDefined();
- if (!result.current) {
- return;
- }
- expect(result.current.query.loading).toBeTruthy();
-
- await waitForNextUpdate({ timeout: 1 });
-
- assertJustExpectedRequestWereMade(env);
-
- expect(result.current.query.loading).toBeFalsy();
-
- expect(result.current?.query.ok).toBeTruthy();
- if (!result.current?.query.ok) return;
-
- expect(result.current.query.data).toEqual({
- instances: [{
- id: 'default',
- name: 'instance_name'
- }, {
- id: 'the_id',
- name: 'second_instance'
- }]
- });
-
- env.addRequestExpectation(API_DELETE_INSTANCE('the_id'), {
- qparam: {
- purge: 'YES'
- }
- });
-
- act(async () => {
- await result.current?.api.purgeInstance('the_id');
- });
-
- assertJustExpectedRequestWereMade(env);
-
- env.addRequestExpectation(API_LIST_INSTANCES, {
- response: {
- instances: [{
- id: 'default',
- name: 'instance_name'
- } as MerchantBackend.Instances.Instance]
- },
- });
-
- expect(result.current.query.loading).toBeFalsy();
-
- await waitForNextUpdate({ timeout: 1 });
-
- assertJustExpectedRequestWereMade(env);
-
- expect(result.current.query.loading).toBeFalsy();
- expect(result.current.query.ok).toBeTruthy();
-
- expect(result.current.query.data).toEqual({
- instances: [{
- id: 'default',
- name: 'instance_name'
- }]
- });
- });
-});
-
-describe("instance management api interaction with listing", () => {
-
- it("should evict cache when updating an instance", async () => {
- const env = new AxiosMockEnvironment();
-
- env.addRequestExpectation(API_LIST_INSTANCES, {
- response: {
- instances: [{
- id: 'managed',
- name: 'instance_name'
- } as MerchantBackend.Instances.Instance]
- },
- });
-
- const { result, waitForNextUpdate } = renderHook(
- () => {
- const api = useManagementAPI('managed');
- const query = useBackendInstances();
-
- return { query, api };
- },
- { wrapper: TestingContext }
- );
-
- expect(result.current).toBeDefined();
- if (!result.current) {
- return;
- }
- expect(result.current.query.loading).toBeTruthy();
-
- await waitForNextUpdate({ timeout: 1 });
-
- assertJustExpectedRequestWereMade(env);
-
- expect(result.current.query.loading).toBeFalsy();
-
- expect(result.current?.query.ok).toBeTruthy();
- if (!result.current?.query.ok) return;
-
- expect(result.current.query.data).toEqual({
- instances: [{
- id: 'managed',
- name: 'instance_name'
- }]
- });
-
- env.addRequestExpectation(API_UPDATE_INSTANCE_BY_ID('managed'), {
- request: {
- name: 'other_name'
- } as MerchantBackend.Instances.InstanceReconfigurationMessage,
- });
-
- act(async () => {
- await result.current?.api.updateInstance({
- name: 'other_name'
- } as MerchantBackend.Instances.InstanceConfigurationMessage);
- });
-
- assertJustExpectedRequestWereMade(env);
-
- env.addRequestExpectation(API_LIST_INSTANCES, {
- response: {
- instances: [
- {
- id: 'managed',
- name: 'other_name'
- } as MerchantBackend.Instances.Instance]
- },
- });
-
- expect(result.current.query.loading).toBeFalsy();
-
- await waitForNextUpdate({ timeout: 1 });
-
- assertJustExpectedRequestWereMade(env);
-
- expect(result.current.query.loading).toBeFalsy();
- expect(result.current.query.ok).toBeTruthy();
-
- expect(result.current.query.data).toEqual({
- instances: [{
- id: 'managed',
- name: 'other_name'
- }]
- });
- });
-
-});
-
diff --git a/packages/merchant-backoffice-ui/tests/hooks/swr/order.test.ts b/packages/merchant-backoffice-ui/tests/hooks/swr/order.test.ts
deleted file mode 100644
index 31444d942..000000000
--- a/packages/merchant-backoffice-ui/tests/hooks/swr/order.test.ts
+++ /dev/null
@@ -1,567 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import { renderHook } from "@testing-library/preact-hooks";
-import { act } from "preact/test-utils";
-import { TestingContext } from ".";
-import { MerchantBackend } from "../../../src/declaration.js";
-import { useInstanceOrders, useOrderAPI, useOrderDetails } from "../../../src/hooks/order.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, assertJustExpectedRequestWereMade, assertNextRequest, assertNoMoreRequestWereMade, AxiosMockEnvironment
-} from "../../axiosMock.js";
-
-describe("order api interaction with listing", () => {
-
- it("should evict cache when creating an order", async () => {
- const env = new AxiosMockEnvironment();
-
- env.addRequestExpectation(API_LIST_ORDERS, {
- qparam: { delta: 0, paid: "yes" },
- response: {
- orders: [{ order_id: "1" } as MerchantBackend.Orders.OrderHistoryEntry],
- },
- });
-
- env.addRequestExpectation(API_LIST_ORDERS, {
- qparam: { delta: -20, paid: "yes" },
- response: {
- orders: [{ order_id: "2" } as MerchantBackend.Orders.OrderHistoryEntry],
- },
- });
-
-
- const { result, waitForNextUpdate } = renderHook(() => {
- const newDate = (d: Date) => {
- console.log("new date", d);
- };
- const query = useInstanceOrders({ paid: "yes" }, newDate);
- const api = useOrderAPI();
-
- return { query, api };
- }, { wrapper: TestingContext });
-
- expect(result.current).toBeDefined();
- if (!result.current) {
- return;
- }
-
- expect(result.current.query.loading).toBeTruthy();
- await waitForNextUpdate({ timeout: 1 });
- assertJustExpectedRequestWereMade(env);
-
- expect(result.current.query.loading).toBeFalsy();
- expect(result.current?.query.ok).toBeTruthy();
- if (!result.current?.query.ok) return;
-
- expect(result.current.query.data).toEqual({
- 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: 0, paid: "yes" },
- response: {
- orders: [{ order_id: "1" } as any],
- },
- });
-
- env.addRequestExpectation(API_LIST_ORDERS, {
- qparam: { delta: -20, paid: "yes" },
- response: {
- orders: [{ order_id: "2" } as any, { order_id: "3" } as any],
- },
- });
-
- act(async () => {
- await result.current?.api.createOrder({
- order: { amount: "ARS:12", summary: "pay me" },
- } as any);
- });
-
- await waitForNextUpdate({ timeout: 1 });
- assertJustExpectedRequestWereMade(env);
-
- expect(result.current.query.loading).toBeFalsy();
- expect(result.current?.query.ok).toBeTruthy();
- if (!result.current?.query.ok) return;
-
- expect(result.current.query.data).toEqual({
- orders: [{ order_id: "1" }, { order_id: "2" }, { order_id: "3" }],
- });
- });
- it("should evict cache when doing a refund", async () => {
- const env = new AxiosMockEnvironment();
-
- env.addRequestExpectation(API_LIST_ORDERS, {
- qparam: { delta: 0, paid: "yes" },
- response: {
- orders: [{ order_id: "1", amount: 'EUR:12', refundable: true } as MerchantBackend.Orders.OrderHistoryEntry],
- },
- });
-
- env.addRequestExpectation(API_LIST_ORDERS, {
- qparam: { delta: -20, paid: "yes" },
- response: { orders: [], },
- });
-
-
- const { result, waitForNextUpdate } = renderHook(() => {
- const newDate = (d: Date) => {
- console.log("new date", d);
- };
- const query = useInstanceOrders({ paid: "yes" }, newDate);
- const api = useOrderAPI();
-
- return { query, api };
- }, { wrapper: TestingContext });
-
- expect(result.current).toBeDefined();
- if (!result.current) {
- return;
- }
-
- expect(result.current.query.loading).toBeTruthy();
- await waitForNextUpdate({ timeout: 1 });
- assertJustExpectedRequestWereMade(env);
-
-
- expect(result.current.query.loading).toBeFalsy();
- expect(result.current?.query.ok).toBeTruthy();
- if (!result.current?.query.ok) return;
-
- expect(result.current.query.data).toEqual({
- 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: 0, paid: "yes" },
- response: {
- orders: [{ order_id: "1", amount: 'EUR:12', refundable: false } as any],
- },
- });
-
- env.addRequestExpectation(API_LIST_ORDERS, {
- qparam: { delta: -20, paid: "yes" },
- response: { orders: [], },
- });
-
- act(async () => {
- await result.current?.api.refundOrder('1', {
- reason: 'double pay',
- refund: 'EUR:1'
- });
- });
-
- await waitForNextUpdate({ timeout: 1 });
- assertJustExpectedRequestWereMade(env);
-
- expect(result.current.query.loading).toBeFalsy();
- expect(result.current?.query.ok).toBeTruthy();
- if (!result.current?.query.ok) return;
-
- expect(result.current.query.data).toEqual({
- orders: [{
- order_id: "1",
- amount: 'EUR:12',
- refundable: false,
- }],
- });
- });
- it("should evict cache when deleting an order", async () => {
- const env = new AxiosMockEnvironment();
-
- env.addRequestExpectation(API_LIST_ORDERS, {
- qparam: { delta: 0, paid: "yes" },
- response: {
- orders: [{ order_id: "1" } as MerchantBackend.Orders.OrderHistoryEntry],
- },
- });
-
- env.addRequestExpectation(API_LIST_ORDERS, {
- qparam: { delta: -20, paid: "yes" },
- response: {
- orders: [{ order_id: "2" } as MerchantBackend.Orders.OrderHistoryEntry],
- },
- });
-
-
- const { result, waitForNextUpdate } = renderHook(() => {
- const newDate = (d: Date) => {
- console.log("new date", d);
- };
- const query = useInstanceOrders({ paid: "yes" }, newDate);
- const api = useOrderAPI();
-
- return { query, api };
- }, { wrapper: TestingContext });
-
- expect(result.current).toBeDefined();
- if (!result.current) {
- return;
- }
-
- expect(result.current.query.loading).toBeTruthy();
- await waitForNextUpdate({ timeout: 1 });
- assertJustExpectedRequestWereMade(env);
-
- expect(result.current.query.loading).toBeFalsy();
- expect(result.current?.query.ok).toBeTruthy();
- if (!result.current?.query.ok) return;
-
- expect(result.current.query.data).toEqual({
- orders: [{ order_id: "1" }, { order_id: "2" }],
- });
-
- env.addRequestExpectation(API_DELETE_ORDER('1'), {});
-
- env.addRequestExpectation(API_LIST_ORDERS, {
- qparam: { delta: 0, paid: "yes" },
- response: {
- orders: [],
- },
- });
-
- env.addRequestExpectation(API_LIST_ORDERS, {
- qparam: { delta: -20, paid: "yes" },
- response: {
- orders: [{ order_id: "2" } as any],
- },
- });
-
- act(async () => {
- await result.current?.api.deleteOrder('1');
- });
-
- await waitForNextUpdate({ timeout: 1 });
- assertJustExpectedRequestWereMade(env);
-
- expect(result.current.query.loading).toBeFalsy();
- expect(result.current?.query.ok).toBeTruthy();
- if (!result.current?.query.ok) return;
-
- expect(result.current.query.data).toEqual({
- orders: [{ order_id: "2" }],
- });
- });
-
-});
-
-describe("order api interaction with details", () => {
-
- it("should evict cache when doing a refund", async () => {
- const env = new AxiosMockEnvironment();
-
- 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 { result, waitForNextUpdate } = renderHook(() => {
- const query = useOrderDetails('1')
- const api = useOrderAPI();
-
- return { query, api };
- }, { wrapper: TestingContext });
-
- expect(result.current).toBeDefined();
- if (!result.current) {
- return;
- }
-
- expect(result.current.query.loading).toBeTruthy();
- await waitForNextUpdate({ timeout: 1 });
- assertJustExpectedRequestWereMade(env);
-
- expect(result.current.query.loading).toBeFalsy();
- expect(result.current?.query.ok).toBeTruthy();
- if (!result.current?.query.ok) return;
-
- expect(result.current.query.data).toEqual({
- 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,
- });
-
- act(async () => {
- await result.current?.api.refundOrder('1', {
- reason: 'double pay',
- refund: 'EUR:1'
- });
- });
-
- await waitForNextUpdate({ timeout: 1 });
- assertJustExpectedRequestWereMade(env);
-
- expect(result.current.query.loading).toBeFalsy();
- expect(result.current?.query.ok).toBeTruthy();
- if (!result.current?.query.ok) return;
-
- expect(result.current.query.data).toEqual({
- summary: 'description',
- refund_amount: 'EUR:1',
- });
- })
- it("should evict cache when doing a forget", async () => {
- const env = new AxiosMockEnvironment();
-
- 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 { result, waitForNextUpdate } = renderHook(() => {
- const query = useOrderDetails('1')
- const api = useOrderAPI();
-
- return { query, api };
- }, { wrapper: TestingContext });
-
- expect(result.current).toBeDefined();
- if (!result.current) {
- return;
- }
-
- expect(result.current.query.loading).toBeTruthy();
- await waitForNextUpdate({ timeout: 1 });
- assertJustExpectedRequestWereMade(env);
-
- expect(result.current.query.loading).toBeFalsy();
- expect(result.current?.query.ok).toBeTruthy();
- if (!result.current?.query.ok) return;
-
- expect(result.current.query.data).toEqual({
- 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,
- });
-
- act(async () => {
- await result.current?.api.forgetOrder('1', {
- fields: ['$.summary']
- });
- });
-
- await waitForNextUpdate({ timeout: 1 });
- assertJustExpectedRequestWereMade(env);
-
- expect(result.current.query.loading).toBeFalsy();
- expect(result.current?.query.ok).toBeTruthy();
- if (!result.current?.query.ok) return;
-
- expect(result.current.query.data).toEqual({
- summary: undefined,
- });
- })
-})
-
-describe("order listing pagination", () => {
-
- it("should not load more if has reach the end", async () => {
- const env = new AxiosMockEnvironment();
- env.addRequestExpectation(API_LIST_ORDERS, {
- qparam: { delta: 20, wired: "yes", date_ms: 12 },
- response: {
- orders: [{ order_id: "1" } as any],
- },
- });
-
- env.addRequestExpectation(API_LIST_ORDERS, {
- qparam: { delta: -20, wired: "yes", date_ms: 13 },
- response: {
- orders: [{ order_id: "2" } as any],
- },
- });
-
-
- const { result, waitForNextUpdate } = renderHook(() => {
- const newDate = (d: Date) => {
- console.log("new date", d);
- };
- const date = new Date(12);
- const query = useInstanceOrders({ wired: "yes", date }, newDate)
- return { query }
- }, { wrapper: TestingContext });
-
- assertJustExpectedRequestWereMade(env);
-
- await waitForNextUpdate();
-
- expect(result.current?.query.ok).toBeTruthy();
- if (!result.current?.query.ok) return;
-
- expect(result.current.query.data).toEqual({
- orders: [{ order_id: "1" }, { order_id: "2" }],
- });
-
- expect(result.current.query.isReachingEnd).toBeTruthy()
- expect(result.current.query.isReachingStart).toBeTruthy()
-
- await act(() => {
- if (!result.current?.query.ok) throw Error("not ok");
- result.current.query.loadMore();
- });
- assertNoMoreRequestWereMade(env);
-
- await act(() => {
- if (!result.current?.query.ok) throw Error("not ok");
- result.current.query.loadMorePrev();
- });
- assertNoMoreRequestWereMade(env);
-
- expect(result.current.query.data).toEqual({
- orders: [
- { order_id: "1" },
- { order_id: "2" },
- ],
- });
- });
-
- it("should load more if result brings more that PAGE_SIZE", async () => {
- const env = new AxiosMockEnvironment();
-
- 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_ms: 12 },
- response: {
- orders: ordersFrom0to20,
- },
- });
-
- env.addRequestExpectation(API_LIST_ORDERS, {
- qparam: { delta: -20, wired: "yes", date_ms: 13 },
- response: {
- orders: ordersFrom20to40,
- },
- });
-
- const { result, waitForNextUpdate } = renderHook(() => {
- const newDate = (d: Date) => {
- console.log("new date", d);
- };
- const date = new Date(12);
- const query = useInstanceOrders({ wired: "yes", date }, newDate)
- return { query }
- }, { wrapper: TestingContext });
-
- assertJustExpectedRequestWereMade(env);
-
- await waitForNextUpdate({ timeout: 1 });
-
- expect(result.current?.query.ok).toBeTruthy();
- if (!result.current?.query.ok) return;
-
- expect(result.current.query.data).toEqual({
- orders: [...ordersFrom20to0, ...ordersFrom20to40],
- });
-
- expect(result.current.query.isReachingEnd).toBeFalsy()
- expect(result.current.query.isReachingStart).toBeFalsy()
-
- env.addRequestExpectation(API_LIST_ORDERS, {
- qparam: { delta: -40, wired: "yes", date_ms: 13 },
- response: {
- orders: [...ordersFrom20to40, { order_id: '41' }],
- },
- });
-
- await act(() => {
- if (!result.current?.query.ok) throw Error("not ok");
- result.current.query.loadMore();
- });
- await waitForNextUpdate({ timeout: 1 });
-
- assertJustExpectedRequestWereMade(env);
-
- env.addRequestExpectation(API_LIST_ORDERS, {
- qparam: { delta: 40, wired: "yes", date_ms: 12 },
- response: {
- orders: [...ordersFrom0to20, { order_id: '-1' }],
- },
- });
-
- await act(() => {
- if (!result.current?.query.ok) throw Error("not ok");
- result.current.query.loadMorePrev();
- });
- await waitForNextUpdate({ timeout: 1 });
- assertJustExpectedRequestWereMade(env);
-
- expect(result.current.query.data).toEqual({
- orders: [{ order_id: '-1' }, ...ordersFrom20to0, ...ordersFrom20to40, { order_id: '41' }],
- });
- });
-
-
-});
diff --git a/packages/merchant-backoffice-ui/tests/hooks/swr/product.test.ts b/packages/merchant-backoffice-ui/tests/hooks/swr/product.test.ts
deleted file mode 100644
index 3ea03e3c7..000000000
--- a/packages/merchant-backoffice-ui/tests/hooks/swr/product.test.ts
+++ /dev/null
@@ -1,338 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import { renderHook } from "@testing-library/preact-hooks";
-import { act } from "preact/test-utils";
-import { TestingContext } from ".";
-import { MerchantBackend } from "../../../src/declaration.js";
-import { useInstanceProducts, useProductAPI, useProductDetails } from "../../../src/hooks/product.js";
-import {
- API_CREATE_PRODUCT,
- API_DELETE_PRODUCT, API_GET_PRODUCT_BY_ID,
- API_LIST_PRODUCTS,
- API_UPDATE_PRODUCT_BY_ID,
- assertJustExpectedRequestWereMade,
- assertNextRequest,
- AxiosMockEnvironment
-} from "../../axiosMock.js";
-
-describe("product api interaction with listing", () => {
- it("should evict cache when creating a product", async () => {
- const env = new AxiosMockEnvironment();
-
- 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 { result, waitForNextUpdate } = renderHook(
- () => {
- const query = useInstanceProducts();
- const api = useProductAPI();
- return { api, query };
- },
- { wrapper: TestingContext }
- ); // get products -> loading
-
- expect(result.current).toBeDefined();
- if (!result.current) {
- return;
- }
- expect(result.current.query.loading).toBeTruthy();
- await waitForNextUpdate({ timeout: 1 });
-
- await waitForNextUpdate({ timeout: 1 });
- assertJustExpectedRequestWereMade(env);
- expect(result.current.query.loading).toBeFalsy();
- expect(result.current.query.ok).toBeTruthy();
- if (!result.current?.query.ok) return;
-
- expect(result.current.query.data).toEqual([
- { 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,
- });
-
- act(async () => {
- await result.current?.api.createProduct({
- price: "ARS:23",
- } as any);
- });
-
- assertNextRequest(env);
- await waitForNextUpdate({ timeout: 1 });
- await waitForNextUpdate({ timeout: 1 });
-
- assertJustExpectedRequestWereMade(env);
-
- expect(result.current.query.loading).toBeFalsy();
- expect(result.current?.query.ok).toBeTruthy();
- if (!result.current?.query.ok) return;
-
- expect(result.current.query.data).toEqual([
- {
- id: "1234",
- price: "ARS:12",
- },
- {
- id: "2345",
- price: "ARS:23",
- },
- ]);
- });
-
- it("should evict cache when updating a product", async () => {
- const env = new AxiosMockEnvironment();
-
- 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 { result, waitForNextUpdate } = renderHook(
- () => {
- const query = useInstanceProducts();
- const api = useProductAPI();
- return { api, query };
- },
- { wrapper: TestingContext }
- ); // get products -> loading
-
- expect(result.current).toBeDefined();
- if (!result.current) {
- return;
- }
- expect(result.current.query.loading).toBeTruthy();
- await waitForNextUpdate({ timeout: 1 });
-
- await waitForNextUpdate({ timeout: 1 });
- assertJustExpectedRequestWereMade(env);
- expect(result.current.query.loading).toBeFalsy();
- expect(result.current.query.ok).toBeTruthy();
- if (!result.current?.query.ok) return;
-
- expect(result.current.query.data).toEqual([
- { 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,
- });
-
- act(async () => {
- await result.current?.api.updateProduct("1234", {
- price: "ARS:13",
- } as any);
- });
-
- assertNextRequest(env);
- await waitForNextUpdate({ timeout: 1 });
-
- assertJustExpectedRequestWereMade(env);
-
- expect(result.current.query.loading).toBeFalsy();
- expect(result.current?.query.ok).toBeTruthy();
- if (!result.current?.query.ok) return;
-
- expect(result.current.query.data).toEqual([
- {
- id: "1234",
- price: "ARS:13",
- },
- ]);
- });
-
- it("should evict cache when deleting a product", async () => {
- const env = new AxiosMockEnvironment();
-
- 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 { result, waitForNextUpdate } = renderHook(
- () => {
- const query = useInstanceProducts();
- const api = useProductAPI();
- return { api, query };
- },
- { wrapper: TestingContext }
- ); // get products -> loading
-
- expect(result.current).toBeDefined();
- if (!result.current) {
- return;
- }
- expect(result.current.query.loading).toBeTruthy();
- await waitForNextUpdate({ timeout: 1 });
-
- await waitForNextUpdate({ timeout: 1 });
- // await waitForNextUpdate({ timeout: 1 });
- assertJustExpectedRequestWereMade(env);
-
- expect(result.current.query.loading).toBeFalsy();
- expect(result.current.query.ok).toBeTruthy();
- if (!result.current?.query.ok) return;
-
- expect(result.current.query.data).toEqual([
- { 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:13" } as MerchantBackend.Products.ProductDetail,
- });
-
- act(async () => {
- await result.current?.api.deleteProduct("2345");
- });
-
- assertNextRequest(env);
- await waitForNextUpdate({ timeout: 1 });
- await waitForNextUpdate({ timeout: 1 });
-
- assertJustExpectedRequestWereMade(env);
-
- expect(result.current.query.loading).toBeFalsy();
- expect(result.current?.query.ok).toBeTruthy();
- if (!result.current?.query.ok) return;
-
- expect(result.current.query.data).toEqual([
- {
- id: "1234",
- price: "ARS:13",
- },
- ]);
- });
-
-});
-
-describe("product api interaction with details", () => {
- it("should evict cache when updating a product", async () => {
- const env = new AxiosMockEnvironment();
-
- env.addRequestExpectation(API_GET_PRODUCT_BY_ID("12"), {
- response: {
- description: "this is a description",
- } as MerchantBackend.Products.ProductDetail,
- });
-
- const { result, waitForNextUpdate } = renderHook(() => {
- const query = useProductDetails("12");
- const api = useProductAPI();
- return { query, api };
- }, { wrapper: TestingContext });
-
- expect(result.current).toBeDefined();
- if (!result.current) {
- return;
- }
- expect(result.current.query.loading).toBeTruthy();
- await waitForNextUpdate();
-
- assertJustExpectedRequestWereMade(env);
-
- expect(result.current.query.loading).toBeFalsy();
- expect(result.current?.query.ok).toBeTruthy();
- if (!result.current?.query.ok) return;
-
- expect(result.current.query.data).toEqual({
- 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,
- });
-
- act(async () => {
- return await result.current?.api.updateProduct("12", {
- description: "other description",
- } as any);
- });
-
- assertNextRequest(env);
- await waitForNextUpdate();
-
- assertJustExpectedRequestWereMade(env);
-
- expect(result.current.query.loading).toBeFalsy();
- expect(result.current?.query.ok).toBeTruthy();
- if (!result.current?.query.ok) return;
-
- expect(result.current.query.data).toEqual({
- description: "other description",
- });
- })
-}) \ No newline at end of file
diff --git a/packages/merchant-backoffice-ui/tests/hooks/swr/reserve.test.ts b/packages/merchant-backoffice-ui/tests/hooks/swr/reserve.test.ts
deleted file mode 100644
index deae9d389..000000000
--- a/packages/merchant-backoffice-ui/tests/hooks/swr/reserve.test.ts
+++ /dev/null
@@ -1,470 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import { renderHook } from "@testing-library/preact-hooks";
-import { act } from "preact/test-utils";
-import { MerchantBackend } from "../../../src/declaration.js";
-import {
- useInstanceReserves,
- useReserveDetails,
- useReservesAPI,
- useTipDetails,
-} from "../../../src/hooks/reserves.js";
-import {
- API_AUTHORIZE_TIP,
- API_AUTHORIZE_TIP_FOR_RESERVE,
- API_CREATE_RESERVE,
- API_DELETE_RESERVE,
- API_GET_RESERVE_BY_ID,
- API_GET_TIP_BY_ID,
- API_LIST_RESERVES,
- assertJustExpectedRequestWereMade,
- AxiosMockEnvironment,
-} from "../../axiosMock.js";
-import { TestingContext } from "./index.js";
-
-describe("reserve api interaction with listing", () => {
- it("should evict cache when creating a reserve", async () => {
- const env = new AxiosMockEnvironment();
-
- env.addRequestExpectation(API_LIST_RESERVES, {
- response: {
- reserves: [
- {
- reserve_pub: "11",
- } as MerchantBackend.Tips.ReserveStatusEntry,
- ],
- },
- });
-
- const { result, waitForNextUpdate } = renderHook(
- () => {
- const api = useReservesAPI();
- const query = useInstanceReserves();
-
- return { query, api };
- },
- { wrapper: TestingContext }
- );
-
- expect(result.current).toBeDefined();
- if (!result.current) {
- return;
- }
- expect(result.current.query.loading).toBeTruthy();
-
- await waitForNextUpdate({ timeout: 1 });
-
- assertJustExpectedRequestWereMade(env);
-
- expect(result.current.query.loading).toBeFalsy();
- expect(result.current?.query.ok).toBeTruthy();
- if (!result.current?.query.ok) return;
-
- expect(result.current.query.data).toEqual({
- 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",
- payto_uri: "payto",
- },
- });
-
- act(async () => {
- await result.current?.api.createReserve({
- initial_balance: "ARS:3333",
- exchange_url: "http://url",
- wire_method: "iban",
- });
- return;
- });
-
- assertJustExpectedRequestWereMade(env);
-
- env.addRequestExpectation(API_LIST_RESERVES, {
- response: {
- reserves: [
- {
- reserve_pub: "11",
- } as MerchantBackend.Tips.ReserveStatusEntry,
- {
- reserve_pub: "22",
- } as MerchantBackend.Tips.ReserveStatusEntry,
- ],
- },
- });
-
- expect(result.current.query.loading).toBeFalsy();
-
- await waitForNextUpdate({ timeout: 1 });
-
- assertJustExpectedRequestWereMade(env);
-
- expect(result.current.query.loading).toBeFalsy();
- expect(result.current.query.ok).toBeTruthy();
-
- expect(result.current.query.data).toEqual({
- reserves: [
- {
- reserve_pub: "11",
- } as MerchantBackend.Tips.ReserveStatusEntry,
- {
- reserve_pub: "22",
- } as MerchantBackend.Tips.ReserveStatusEntry,
- ],
- });
- });
-
- it("should evict cache when deleting a reserve", async () => {
- const env = new AxiosMockEnvironment();
-
- env.addRequestExpectation(API_LIST_RESERVES, {
- response: {
- reserves: [
- {
- reserve_pub: "11",
- } as MerchantBackend.Tips.ReserveStatusEntry,
- {
- reserve_pub: "22",
- } as MerchantBackend.Tips.ReserveStatusEntry,
- {
- reserve_pub: "33",
- } as MerchantBackend.Tips.ReserveStatusEntry,
- ],
- },
- });
-
- const { result, waitForNextUpdate } = renderHook(
- () => {
- const api = useReservesAPI();
- const query = useInstanceReserves();
-
- return { query, api };
- },
- {
- wrapper: TestingContext,
- }
- );
-
- expect(result.current).toBeDefined();
- if (!result.current) {
- return;
- }
- expect(result.current.query.loading).toBeTruthy();
-
- await waitForNextUpdate({ timeout: 1 });
-
- assertJustExpectedRequestWereMade(env);
-
- expect(result.current.query.loading).toBeFalsy();
- expect(result.current?.query.ok).toBeTruthy();
- if (!result.current?.query.ok) return;
-
- expect(result.current.query.data).toEqual({
- reserves: [
- { reserve_pub: "11" },
- { reserve_pub: "22" },
- { reserve_pub: "33" },
- ],
- });
-
- env.addRequestExpectation(API_DELETE_RESERVE("11"), {});
-
- act(async () => {
- await result.current?.api.deleteReserve("11");
- return;
- });
-
- assertJustExpectedRequestWereMade(env);
-
- env.addRequestExpectation(API_LIST_RESERVES, {
- response: {
- reserves: [
- {
- reserve_pub: "22",
- } as MerchantBackend.Tips.ReserveStatusEntry,
- {
- reserve_pub: "33",
- } as MerchantBackend.Tips.ReserveStatusEntry,
- ],
- },
- });
-
- expect(result.current.query.loading).toBeFalsy();
-
- await waitForNextUpdate({ timeout: 1 });
-
- assertJustExpectedRequestWereMade(env);
-
- expect(result.current.query.loading).toBeFalsy();
- expect(result.current.query.ok).toBeTruthy();
-
- expect(result.current.query.data).toEqual({
- reserves: [
- {
- reserve_pub: "22",
- } as MerchantBackend.Tips.ReserveStatusEntry,
- {
- reserve_pub: "33",
- } as MerchantBackend.Tips.ReserveStatusEntry,
- ],
- });
- });
-});
-
-describe("reserve api interaction with details", () => {
- it("should evict cache when adding a tip for a specific reserve", async () => {
- const env = new AxiosMockEnvironment();
-
- env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), {
- response: {
- payto_uri: "payto://here",
- tips: [{ reason: "why?", tip_id: "id1", total_amount: "USD:10" }],
- } as MerchantBackend.Tips.ReserveDetail,
- });
-
- const { result, waitForNextUpdate } = renderHook(
- () => {
- const api = useReservesAPI();
- const query = useReserveDetails("11");
-
- return { query, api };
- },
- {
- wrapper: TestingContext,
- }
- );
-
- expect(result.current).toBeDefined();
- if (!result.current) {
- return;
- }
- expect(result.current.query.loading).toBeTruthy();
-
- await waitForNextUpdate({ timeout: 1 });
-
- assertJustExpectedRequestWereMade(env);
-
- expect(result.current.query.loading).toBeFalsy();
- expect(result.current?.query.ok).toBeTruthy();
- if (!result.current?.query.ok) return;
-
- expect(result.current.query.data).toEqual({
- payto_uri: "payto://here",
- tips: [{ reason: "why?", tip_id: "id1", total_amount: "USD:10" }],
- });
-
- env.addRequestExpectation(API_AUTHORIZE_TIP_FOR_RESERVE("11"), {
- request: {
- amount: "USD:12",
- justification: "not",
- next_url: "http://taler.net",
- },
- response: {
- tip_id: "id2",
- taler_tip_uri: "uri",
- tip_expiration: { t_s: 1 },
- tip_status_url: "url",
- },
- });
-
- act(async () => {
- await result.current?.api.authorizeTipReserve("11", {
- amount: "USD:12",
- justification: "not",
- next_url: "http://taler.net",
- });
- });
-
- assertJustExpectedRequestWereMade(env);
-
- expect(result.current.query.loading).toBeFalsy();
-
- env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), {
- response: {
- payto_uri: "payto://here",
- tips: [
- { reason: "why?", tip_id: "id1", total_amount: "USD:10" },
- { reason: "not", tip_id: "id2", total_amount: "USD:12" },
- ],
- } as MerchantBackend.Tips.ReserveDetail,
- });
-
- await waitForNextUpdate({ timeout: 1 });
-
- assertJustExpectedRequestWereMade(env);
-
- expect(result.current.query.loading).toBeFalsy();
- expect(result.current.query.ok).toBeTruthy();
-
- expect(result.current.query.data).toEqual({
- payto_uri: "payto://here",
- tips: [
- { reason: "why?", tip_id: "id1", total_amount: "USD:10" },
- { reason: "not", tip_id: "id2", total_amount: "USD:12" },
- ],
- });
- });
-
- it("should evict cache when adding a tip for a random reserve", async () => {
- const env = new AxiosMockEnvironment();
-
- env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), {
- response: {
- payto_uri: "payto://here",
- tips: [{ reason: "why?", tip_id: "id1", total_amount: "USD:10" }],
- } as MerchantBackend.Tips.ReserveDetail,
- });
-
- const { result, waitForNextUpdate } = renderHook(
- () => {
- const api = useReservesAPI();
- const query = useReserveDetails("11");
-
- return { query, api };
- },
- {
- wrapper: TestingContext,
- }
- );
-
- expect(result.current).toBeDefined();
- if (!result.current) {
- return;
- }
- expect(result.current.query.loading).toBeTruthy();
-
- await waitForNextUpdate({ timeout: 1 });
-
- assertJustExpectedRequestWereMade(env);
-
- expect(result.current.query.loading).toBeFalsy();
- expect(result.current?.query.ok).toBeTruthy();
- if (!result.current?.query.ok) return;
-
- expect(result.current.query.data).toEqual({
- payto_uri: "payto://here",
- tips: [{ reason: "why?", tip_id: "id1", total_amount: "USD:10" }],
- });
-
- env.addRequestExpectation(API_AUTHORIZE_TIP, {
- request: {
- amount: "USD:12",
- justification: "not",
- next_url: "http://taler.net",
- },
- response: {
- tip_id: "id2",
- taler_tip_uri: "uri",
- tip_expiration: { t_s: 1 },
- tip_status_url: "url",
- },
- });
-
- act(async () => {
- await result.current?.api.authorizeTip({
- amount: "USD:12",
- justification: "not",
- next_url: "http://taler.net",
- });
- });
-
- assertJustExpectedRequestWereMade(env);
-
- expect(result.current.query.loading).toBeFalsy();
-
- env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), {
- response: {
- payto_uri: "payto://here",
- tips: [
- { reason: "why?", tip_id: "id1", total_amount: "USD:10" },
- { reason: "not", tip_id: "id2", total_amount: "USD:12" },
- ],
- } as MerchantBackend.Tips.ReserveDetail,
- });
-
- await waitForNextUpdate({ timeout: 1 });
-
- assertJustExpectedRequestWereMade(env);
-
- expect(result.current.query.loading).toBeFalsy();
- expect(result.current.query.ok).toBeTruthy();
-
- expect(result.current.query.data).toEqual({
- payto_uri: "payto://here",
- tips: [
- { reason: "why?", tip_id: "id1", total_amount: "USD:10" },
- { reason: "not", tip_id: "id2", total_amount: "USD:12" },
- ],
- });
- });
-});
-
-describe("reserve api interaction with tip details", () => {
- it("should list tips", async () => {
- const env = new AxiosMockEnvironment();
-
- env.addRequestExpectation(API_GET_TIP_BY_ID("11"), {
- response: {
- total_picked_up: "USD:12",
- reason: "not",
- } as MerchantBackend.Tips.TipDetails,
- });
-
- const { result, waitForNextUpdate } = renderHook(
- () => {
- // const api = useReservesAPI();
- const query = useTipDetails("11");
-
- return { query };
- },
- {
- wrapper: TestingContext,
- }
- );
-
- expect(result.current).toBeDefined();
- if (!result.current) {
- return;
- }
- expect(result.current.query.loading).toBeTruthy();
-
- await waitForNextUpdate({ timeout: 1 });
-
- assertJustExpectedRequestWereMade(env);
-
- expect(result.current.query.loading).toBeFalsy();
- expect(result.current?.query.ok).toBeTruthy();
- if (!result.current?.query.ok) return;
-
- expect(result.current.query.data).toEqual({
- total_picked_up: "USD:12",
- reason: "not",
- });
- });
-});
diff --git a/packages/merchant-backoffice-ui/tests/hooks/swr/transfer.test.ts b/packages/merchant-backoffice-ui/tests/hooks/swr/transfer.test.ts
deleted file mode 100644
index 45efea04c..000000000
--- a/packages/merchant-backoffice-ui/tests/hooks/swr/transfer.test.ts
+++ /dev/null
@@ -1,268 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import { act, renderHook } from "@testing-library/preact-hooks";
-import { TestingContext } from "./index.js";
-import { useInstanceTransfers, useTransferAPI } from "../../../src/hooks/transfer.js";
-import {
- API_INFORM_TRANSFERS,
- API_LIST_TRANSFERS,
- assertJustExpectedRequestWereMade,
- assertNoMoreRequestWereMade,
- AxiosMockEnvironment,
-} from "../../axiosMock.js";
-import { MerchantBackend } from "../../../src/declaration.js";
-
-describe("transfer api interaction with listing", () => {
-
- it("should evict cache when informing a transfer", async () => {
- const env = new AxiosMockEnvironment();
-
- env.addRequestExpectation(API_LIST_TRANSFERS, {
- qparam: { limit: 0 },
- response: {
- transfers: [{ wtid: "2" } as MerchantBackend.Transfers.TransferDetails],
- },
- });
- // FIXME: is this query really needed? if the hook is rendered without
- // position argument then then backend is returning the newest and no need
- // to this second query
- env.addRequestExpectation(API_LIST_TRANSFERS, {
- qparam: { limit: -20 },
- response: {
- transfers: [],
- },
- });
-
- const { result, waitForNextUpdate } = renderHook(() => {
- const moveCursor = (d: string) => {
- console.log("new position", d);
- };
- const query = useInstanceTransfers({}, moveCursor);
- const api = useTransferAPI();
-
- return { query, api };
- }, { wrapper: TestingContext });
-
- expect(result.current).toBeDefined();
- if (!result.current) {
- return;
- }
-
- expect(result.current.query.loading).toBeTruthy();
- await waitForNextUpdate({ timeout: 1 });
- assertJustExpectedRequestWereMade(env);
-
- expect(result.current.query.loading).toBeFalsy();
- expect(result.current.query.ok).toBeTruthy();
- if (!result.current.query.ok) return;
-
- expect(result.current.query.data).toEqual({
- 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: 0 },
- response: {
- transfers: [{ wtid: "2" } as any, { wtid: "3" } as any],
- },
- });
-
- env.addRequestExpectation(API_LIST_TRANSFERS, {
- qparam: { limit: -20 },
- response: {
- transfers: [],
- },
- });
-
- act(async () => {
- await result.current?.api.informTransfer({
- wtid: '3',
- credit_amount: 'EUR:1',
- exchange_url: 'exchange.url',
- payto_uri: 'payto://'
- });
- });
-
- await waitForNextUpdate({ timeout: 1 });
- assertJustExpectedRequestWereMade(env);
-
- expect(result.current.query.loading).toBeFalsy();
- expect(result.current.query.ok).toBeTruthy();
- if (!result.current?.query.ok) return;
-
- expect(result.current.query.data).toEqual({
- transfers: [{ wtid: "3" }, { wtid: "2" }],
- });
- });
-
-});
-
-describe("transfer listing pagination", () => {
-
- it("should not load more if has reach the end", async () => {
- const env = new AxiosMockEnvironment();
- env.addRequestExpectation(API_LIST_TRANSFERS, {
- qparam: { limit: 0, payto_uri: 'payto://' },
- response: {
- transfers: [{ wtid: "2" } as any],
- },
- });
-
- env.addRequestExpectation(API_LIST_TRANSFERS, {
- qparam: { limit: -20, payto_uri: 'payto://' },
- response: {
- transfers: [{ wtid: "1" } as any],
- },
- });
-
-
- const { result, waitForNextUpdate } = renderHook(() => {
- const moveCursor = (d: string) => {
- console.log("new position", d);
- };
- const query = useInstanceTransfers({ payto_uri: 'payto://' }, moveCursor)
- return { query }
- }, { wrapper: TestingContext });
-
- assertJustExpectedRequestWereMade(env);
-
- await waitForNextUpdate();
-
- expect(result.current?.query.ok).toBeTruthy();
- if (!result.current?.query.ok) return;
-
- expect(result.current.query.data).toEqual({
- transfers: [{ wtid: "2" }, { wtid: "1" }],
- });
-
- expect(result.current.query.isReachingEnd).toBeTruthy()
- expect(result.current.query.isReachingStart).toBeTruthy()
-
- await act(() => {
- if (!result.current?.query.ok) throw Error("not ok");
- result.current.query.loadMore();
- });
- assertNoMoreRequestWereMade(env);
-
- await act(() => {
- if (!result.current?.query.ok) throw Error("not ok");
- result.current.query.loadMorePrev();
- });
- assertNoMoreRequestWereMade(env);
-
- expect(result.current.query.data).toEqual({
- transfers: [
- { wtid: "2" },
- { wtid: "1" },
- ],
- });
- });
-
- it("should load more if result brings more that PAGE_SIZE", async () => {
- const env = new AxiosMockEnvironment();
-
- 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://' },
- response: {
- transfers: transfersFrom0to20,
- },
- });
-
- env.addRequestExpectation(API_LIST_TRANSFERS, {
- qparam: { limit: -20, payto_uri: 'payto://' },
- response: {
- transfers: transfersFrom20to40,
- },
- });
-
- const { result, waitForNextUpdate } = renderHook(() => {
- const moveCursor = (d: string) => {
- console.log("new position", d);
- };
- const query = useInstanceTransfers({ payto_uri: 'payto://', position: '1' }, moveCursor)
- return { query }
- }, { wrapper: TestingContext });
-
- assertJustExpectedRequestWereMade(env);
-
- await waitForNextUpdate({ timeout: 1 });
-
- expect(result.current?.query.ok).toBeTruthy();
- if (!result.current?.query.ok) return;
-
- expect(result.current.query.data).toEqual({
- transfers: [...transfersFrom20to0, ...transfersFrom20to40],
- });
-
- expect(result.current.query.isReachingEnd).toBeFalsy()
- expect(result.current.query.isReachingStart).toBeFalsy()
-
- env.addRequestExpectation(API_LIST_TRANSFERS, {
- qparam: { limit: -40, payto_uri: 'payto://', offset: "1" },
- response: {
- transfers: [...transfersFrom20to40, { wtid: '41' }],
- },
- });
-
- await act(() => {
- if (!result.current?.query.ok) throw Error("not ok");
- result.current.query.loadMore();
- });
- await waitForNextUpdate({ timeout: 1 });
-
- assertJustExpectedRequestWereMade(env);
-
- env.addRequestExpectation(API_LIST_TRANSFERS, {
- qparam: { limit: 40, payto_uri: 'payto://', offset: "1" },
- response: {
- transfers: [...transfersFrom0to20, { wtid: '-1' }],
- },
- });
-
- await act(() => {
- if (!result.current?.query.ok) throw Error("not ok");
- result.current.query.loadMorePrev();
- });
- await waitForNextUpdate({ timeout: 1 });
- assertJustExpectedRequestWereMade(env);
-
- expect(result.current.query.data).toEqual({
- transfers: [{ wtid: '-1' }, ...transfersFrom20to0, ...transfersFrom20to40, { wtid: '41' }],
- });
- });
-
-
-});
diff --git a/packages/merchant-backoffice-ui/tests/stories.test.tsx b/packages/merchant-backoffice-ui/tests/stories.test.tsx
deleted file mode 100644
index b53b703e9..000000000
--- a/packages/merchant-backoffice-ui/tests/stories.test.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-import { h, VNode } from "preact";
-import * as config from "../src/context/config.js";
-import * as i18n from "../src/context/translation.js";
-import { cleanup, render as originalRender } from "@testing-library/preact";
-import { SWRConfig } from "swr";
-
-import fs from "fs";
-
-function getFiles(dir: string, files_: string[] = []) {
- const files = fs.readdirSync(dir);
- for (const i in files) {
- const name = dir + "/" + files[i];
- if (fs.statSync(name).isDirectory()) {
- getFiles(name, files_);
- } else {
- files_.push(name);
- }
- }
- return files_;
-}
-
-const STORIES_NAME_REGEX = RegExp(".*.stories.tsx");
-
-function render(vnode: VNode) {
- const SC: any = SWRConfig
- return originalRender(
- <SC value={{ provider: () => new Map() }}>
- {vnode}
- </SC>,
- );
-}
-
-import * as jedLib from "jed";
-const handler = new jedLib.Jed("en");
-
-describe("storybook testing", () => {
- it("render every story", () => {
- jest
- .spyOn(config, "useConfigContext")
- .mockImplementation(() => ({ version: "1.0.0", currency: "EUR" }));
- jest.spyOn(i18n, "useTranslationContext").mockImplementation(() => ({
- changeLanguage: () => null,
- handler,
- lang: "en",
- }));
-
- getFiles("./src")
- .filter((f) => STORIES_NAME_REGEX.test(f))
- .map((f) => {
- // const f = "./src/paths/instance/transfers/list/List.stories.tsx";
- // eslint-disable-next-line @typescript-eslint/no-var-requires
- const s = require(`../${f}`);
-
- delete s.default;
- Object.keys(s).forEach((k) => {
- const Component = s[k];
- const vdom = <Component {...Component.args} />;
- expect(() => {
- const { unmount } = render(vdom);
- unmount();
- }).not.toThrow(); //`problem rendering ${f} example ${k}`
- cleanup();
- });
- });
- });
-});
diff --git a/packages/merchant-backoffice-ui/tsconfig.json b/packages/merchant-backoffice-ui/tsconfig.json
index a0f25cba2..396f1e9e7 100644
--- a/packages/merchant-backoffice-ui/tsconfig.json
+++ b/packages/merchant-backoffice-ui/tsconfig.json
@@ -1,61 +1,58 @@
{
- "compilerOptions": {
- /* Basic Options */
- "target": "ES6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */
- "module": "ESNext", /* Specify module code generation: 'none', commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
- "lib": ["es2021","dom"], /* Specify library files to be included in the compilation: */
- // "allowJs": true, /* Allow javascript files to be compiled. */
- // "checkJs": true, /* Report errors in .js files. */
- "jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
- "jsxFactory": "h", /* Specify the JSX factory function to use when targeting react JSX emit, e.g. React.createElement or h. */
- "jsxFragmentFactory": "Fragment", // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-0.html#custom-jsx-factories
- // "declaration": true, /* Generates corresponding '.d.ts' file. */
- // "sourceMap": true, /* Generates corresponding '.map' file. */
- // "outFile": "./", /* Concatenate and emit output to single file. */
- // "outDir": "./", /* Redirect output structure to the directory. */
- // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
- // "removeComments": true, /* Do not emit comments to output. */
- "noEmit": true, /* Do not emit outputs. */
- // "importHelpers": true, /* Import emit helpers from 'tslib'. */
- // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
- // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
-
- /* Strict Type-Checking Options */
- "strict": true, /* Enable all strict type-checking options. */
- // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
- // "strictNullChecks": true, /* Enable strict null checks. */
- // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
- // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
-
- /* Additional Checks */
- // "noUnusedLocals": true, /* Report errors on unused locals. */
- // "noUnusedParameters": true, /* Report errors on unused parameters. */
- // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
- // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
-
- /* Module Resolution Options */
- "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
- "esModuleInterop": true, /* */
- // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
- // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
- // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
- // "typeRoots": [], /* List of folders to include type definitions from. */
- // "types": [], /* Type declaration files to be included in compilation. */
- // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
- // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
-
- /* Source Map Options */
- // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
- // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */
- // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
- // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
-
- /* Experimental Options */
- // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
- // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
-
- /* Advanced Options */
- "skipLibCheck": true /* Skip type checking of declaration files. */
- },
- "include": ["src/**/*", "tests/**/*"]
+ "compilerOptions": {
+ /* Basic Options */
+ "target": "ES2020" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */,
+ "module": "Node16" /* Specify module code generation: 'none', commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
+ "lib": [
+ "es2020",
+ "dom"
+ ] /* Specify library files to be included in the compilation: */,
+ // "allowJs": true, /* Allow javascript files to be compiled. */
+ // "checkJs": true, /* Report errors in .js files. */
+ "jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */,
+ "jsxFactory": "h" /* Specify the JSX factory function to use when targeting react JSX emit, e.g. React.createElement or h. */,
+ "jsxFragmentFactory": "Fragment", // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-0.html#custom-jsx-factories
+ // "declaration": true, /* Generates corresponding '.d.ts' file. */
+ // "sourceMap": true, /* Generates corresponding '.map' file. */
+ // "outFile": "./", /* Concatenate and emit output to single file. */
+ // "outDir": "./", /* Redirect output structure to the directory. */
+ // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
+ // "removeComments": true, /* Do not emit comments to output. */
+ "noEmit": true /* Do not emit outputs. */,
+ // "importHelpers": true, /* Import emit helpers from 'tslib'. */
+ // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
+ // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
+ /* Strict Type-Checking Options */
+ "strict": true /* Enable all strict type-checking options. */,
+ // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
+ // "strictNullChecks": true, /* Enable strict null checks. */
+ // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
+ // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
+ /* Additional Checks */
+ // "noUnusedLocals": true, /* Report errors on unused locals. */
+ // "noUnusedParameters": true, /* Report errors on unused parameters. */
+ // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
+ // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
+ /* Module Resolution Options */
+ "moduleResolution": "node16" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
+ "esModuleInterop": true /* */,
+ // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
+ // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
+ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
+ // "typeRoots": [], /* List of folders to include type definitions from. */
+ // "types": [], /* Type declaration files to be included in compilation. */
+ // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
+ // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
+ /* Source Map Options */
+ // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
+ // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */
+ // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
+ // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
+ /* Experimental Options */
+ // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
+ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
+ /* Advanced Options */
+ "skipLibCheck": true /* Skip type checking of declaration files. */
+ },
+ "include": ["src/**/*", "tests/**/*"]
}
diff --git a/packages/pogen/bin/pogen b/packages/pogen/bin/pogen
index a7ef879d8..3b17d1df7 100755
--- a/packages/pogen/bin/pogen
+++ b/packages/pogen/bin/pogen
@@ -1,2 +1,2 @@
-#!/usr/bin/env node
+#!/usr/bin/env -S node --trace-deprecation
require('../lib/pogen.js').main();
diff --git a/packages/pogen/example/proj1/package.json b/packages/pogen/example/proj1/package.json
index 954139ecf..97adf0f3a 100644
--- a/packages/pogen/example/proj1/package.json
+++ b/packages/pogen/example/proj1/package.json
@@ -7,5 +7,8 @@
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
- "license": "ISC"
+ "license": "ISC",
+ "pogen": {
+ "domain": "test"
+ }
}
diff --git a/packages/pogen/example/messages.po b/packages/pogen/example/proj1/src/i18n/test.pot
index 1addae3f2..8bc5ef236 100644
--- a/packages/pogen/example/messages.po
+++ b/packages/pogen/example/proj1/src/i18n/test.pot
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-01-27 01:51+0100\n"
+"POT-Creation-Date: 2016-11-23 00:00+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -17,68 +17,78 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-#: example/test.ts:3
-#, csharp-format
+#: src/test.ts:4
+#, c-format
msgid "Hello1, World"
msgstr ""
-#: example/test.ts:4
-#, csharp-format
-msgid "Hello2, World"
+#: src/test.ts:8
+#, c-format
+msgid "Hello, quoted \"world\""
msgstr ""
-#: example/test.ts:5
-#, csharp-format
-msgid "Hello3, World"
-msgstr ""
-
-#. This is a comment and should be included
-#: example/test.ts:9
-#, csharp-format
-msgid "Hello4, World"
-msgstr ""
-
-#: example/test.ts:12
-#, csharp-format
+#: src/test.ts:15
+#, c-format
msgid "Hello5, World"
msgstr ""
-#: example/test.ts:13
-#, csharp-format
-msgid "Hello6,{0} World"
+#: src/test.ts:16
+#, c-format
+msgid "Hello6,%1$s World"
msgstr ""
#. This one has a multi line comment.
#. It has multiple lines, and a trailing empty line.
#.
-#: example/test.ts:20
-#, csharp-format
-msgid "Hello7,{0} World{1}"
+#: src/test.ts:23
+#, c-format
+msgid "Hello7,%1$s World%2$s"
msgstr ""
-#: example/test.ts:21
-#, csharp-format
-msgid "{0}Hello8,{1} World{2}"
+#: src/test.ts:23
+#, c-format
+msgid "one %1$s"
+msgid_plural "many %1$s"
+msgstr[0] ""
+msgstr[1] ""
+
+#: src/test.ts:26
+#, c-format
+msgid "one bla %1$s"
+msgid_plural "many bla %1$s"
+msgstr[0] ""
+msgstr[1] ""
+
+#: src/test.ts:31
+#, c-format
+msgid "I have %1$s apple"
+msgid_plural "I have %1$s apples"
+msgstr[0] ""
+msgstr[1] ""
+
+#: src/test.ts:35
+#, c-format
+msgid "%1$sHello8,%2$s World%3$s"
msgstr ""
#.
#. This one has a multi line comment.
#. It has multiple lines, and a leading empty line.
-#: example/test.ts:28
-#, csharp-format
+#: src/test.ts:42
+#, c-format
msgid "Hello9,\" '\" World"
msgstr ""
-#: example/test.ts:32
-#, csharp-format
+#: src/test.ts:46
+#, c-format
msgid ""
"Hello10\n"
" ,\" '\" Wo\n"
" rld"
msgstr ""
-#: example/test.ts:37
-#, csharp-format
+#: src/test.ts:51
+#, c-format
msgid ""
"Hello11 this is a long long string\n"
"it will go over multiple lines and in the pofile\n"
@@ -86,8 +96,8 @@ msgid ""
msgstr ""
#. This is a single line comment
-#: example/test.ts:42
-#, csharp-format
+#: src/test.ts:56
+#, c-format
msgid ""
"Hello12 this is a long long string it will go over multiple lines and in the "
"pofile it should be wrapped and stuff. asdf asdf asdf asdf asdf asdf asdf asdf "
@@ -95,13 +105,26 @@ msgid ""
"asdf"
msgstr ""
-#: example/test.ts:42
-#, csharp-format
+#. First occurrence
+#: src/test.ts:65
+#, c-format
msgid "This message appears twice"
msgstr ""
-#: example/test.ts:45
-#, csharp-format
-msgid "This message appears twice"
+#: src/test2.tsx:1
+#, c-format
+msgid "foo %1$s foo baz"
+msgstr ""
+
+#: src/test2.tsx:5
+#, c-format
+msgid "singular form second line"
+msgid_plural "plural form"
+msgstr[0] ""
+msgstr[1] ""
+
+#: src/test2.tsx:17
+#, c-format
+msgid "\"foo\""
msgstr ""
diff --git a/packages/pogen/example/test.ts b/packages/pogen/example/proj1/src/test.ts
index 6577fa314..2e9b4cbdd 100644
--- a/packages/pogen/example/test.ts
+++ b/packages/pogen/example/proj1/src/test.ts
@@ -1,9 +1,12 @@
declare var i18n: any;
+
console.log(i18n`Hello1, World`);
console.log(i18n.foo()`Hello2, World`);
console.log(i18n.foo()`Hello3, World`);
+console.log(i18n`Hello, quoted "world"`);
+
/* This is a comment and should be included */
console.log(i18n().foo()`Hello4, World`);
diff --git a/packages/pogen/example/test2.tsx b/packages/pogen/example/proj1/src/test2.tsx
index 4133f86fb..1c1ab49f2 100644
--- a/packages/pogen/example/test2.tsx
+++ b/packages/pogen/example/proj1/src/test2.tsx
@@ -13,3 +13,5 @@ let y = (
</i18n.TranslatePlural>
</i18n.TranslateSwitch>
);
+
+let z = <i18n.Translate>"foo"</i18n.Translate>;
diff --git a/packages/pogen/example/proj1/tsconfig.json b/packages/pogen/example/proj1/tsconfig.json
index 30cb65e1d..36ef053db 100644
--- a/packages/pogen/example/proj1/tsconfig.json
+++ b/packages/pogen/example/proj1/tsconfig.json
@@ -4,7 +4,7 @@
"composite": true,
"declaration": true,
"declarationMap": false,
- "target": "ES6",
+ "target": "ES2020",
"module": "ESNext",
"moduleResolution": "node",
"sourceMap": true,
diff --git a/packages/pogen/package.json b/packages/pogen/package.json
index 9b6daad13..24edc348b 100644
--- a/packages/pogen/package.json
+++ b/packages/pogen/package.json
@@ -1,21 +1,21 @@
{
"name": "@gnu-taler/pogen",
- "version": "0.0.5",
+ "version": "0.10.7",
"bin": {
"pogen": "bin/pogen"
},
"author": "Florian Dold",
"license": "GPL-2.0+",
"scripts": {
- "prepare": "tsc",
+ "clean": "rm -rf lib",
"compile": "tsc"
},
"devDependencies": {
"po2json": "^0.4.5",
- "typescript": "^4.8.4"
+ "typescript": "^5.3.3"
},
"dependencies": {
- "@types/node": "^18.8.5",
- "glob": "^7.2.0"
+ "@types/node": "^18.11.17",
+ "glob": "^10.3.10"
}
}
diff --git a/packages/pogen/src/po2ts.ts b/packages/pogen/src/po2ts.ts
index 7e831e6f8..0b5b1384d 100644
--- a/packages/pogen/src/po2ts.ts
+++ b/packages/pogen/src/po2ts.ts
@@ -19,11 +19,54 @@
*/
// @ts-ignore
-import * as po2json from "po2json";
+import * as po2jsonLib from "po2json";
import * as fs from "fs";
-import * as path from "path";
import glob = require("glob");
+//types defined by the po2json library
+type Header = {
+ domain: string;
+ lang: string;
+ 'plural_forms': string;
+};
+
+type MessagesType = Record<string, undefined | Array<string>> & { "": Header }
+interface pojsonType {
+ // X-Domain or 'messages'
+ domain: string;
+ locale_data: {
+ messages: MessagesType
+ }
+}
+// ----------- end pf po2json
+
+interface StringsType {
+ // X-Domain or 'messages'
+ domain: string;
+ lang: string;
+ completeness: number,
+ 'plural_forms': string;
+ locale_data: {
+ messages: Record<string, undefined | Array<string>>
+ }
+}
+
+// This prelude match the types above
+const TYPES_FOR_STRING_PRELUDE = `
+export interface StringsType {
+ domain: string;
+ lang: string;
+ completeness: number;
+ 'plural_forms': string;
+ locale_data: {
+ messages: Record<string, unknown>;
+ };
+};
+`;
+
+const DEFAULT_STRING_PRELUDE = `${TYPES_FOR_STRING_PRELUDE}export const strings: Record<string,StringsType> = {};\n\n`
+
+
export function po2ts(): void {
const files = glob.sync("src/i18n/*.po");
@@ -34,9 +77,14 @@ export function po2ts(): void {
console.log(files);
- const chunks: string[] = [
- "export const strings: any = {};\n\n"
- ];
+ let prelude: string;
+ try {
+ prelude = fs.readFileSync("src/i18n/strings-prelude", "utf-8")
+ } catch (e) {
+ prelude = DEFAULT_STRING_PRELUDE
+ }
+
+ const chunks = [prelude];
for (const filename of files) {
const m = filename.match(/([a-zA-Z0-9-_]+).po/);
@@ -47,16 +95,26 @@ export function po2ts(): void {
}
const lang = m[1];
- const pojson = po2json.parseFileSync(filename, {
+ const poAsJson: pojsonType = po2jsonLib.parseFileSync(filename, {
format: "jed1.x",
fuzzy: true,
});
- const s =
- "strings['" +
- lang +
- "'] = " +
- JSON.stringify(pojson, null, " ") +
- ";\n\n";
+ const header = poAsJson.locale_data.messages[""]
+ const total = calculateTotalTranslations(poAsJson.locale_data.messages)
+ const completeness =
+ header.lang === "en"
+ ? 100 // 'en' is always complete
+ : Math.floor(total.translations * 100 / total.keys);
+
+ const strings: StringsType = {
+ locale_data: poAsJson.locale_data,
+ domain: poAsJson.domain,
+ plural_forms: header.plural_forms,
+ lang: header.lang,
+ completeness,
+ }
+ const value = JSON.stringify(strings, undefined, 2)
+ const s = `strings['${lang}'] = ${value};\n\n`
chunks.push(s);
}
@@ -64,3 +122,25 @@ export function po2ts(): void {
fs.writeFileSync("src/i18n/strings.ts", tsContents);
}
+
+function calculateTotalTranslations(msgs: MessagesType): { keys: number, translations: number } {
+ const kv = Object.entries(msgs)
+ const [keys, translations] = kv.reduce(([total, withTranslation], translation) => {
+ if (!translation || translation.length !== 2 || !translation[1]) {
+ //current key is empty
+ return [total, withTranslation]
+ }
+ const v = translation[1]
+ if (!Array.isArray(v)) {
+ // this is not a translation
+ return [total, withTranslation]
+ }
+ if (!v.length || !v[0].length) {
+ //translation is missing
+ return [total + 1, withTranslation]
+ }
+ //current key has a translation
+ return [total + 1, withTranslation + 1]
+ }, [0, 0])
+ return { keys, translations }
+} \ No newline at end of file
diff --git a/packages/pogen/src/potextract.ts b/packages/pogen/src/potextract.ts
index 8961c8da0..243d44c6f 100644
--- a/packages/pogen/src/potextract.ts
+++ b/packages/pogen/src/potextract.ts
@@ -21,6 +21,26 @@ import * as ts from "typescript";
import * as fs from "fs";
import * as path from "path"
+const DEFAULT_PO_HEADER = `# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\\n"
+"Report-Msgid-Bugs-To: \\n"
+"POT-Creation-Date: 2016-11-23 00:00+0100\\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n"
+"Language-Team: LANGUAGE <LL@li.org>\\n"
+"Language: \\n"
+"MIME-Version: 1.0\\n"
+"Content-Type: text/plain; charset=UTF-8\\n"
+"Content-Transfer-Encoding: 8bit\\n"\n\n`
+
+
function wordwrap(str: string, width: number = 80): string[] {
var regex = ".{1," + width + "}(\\s|$)|\\S+(\\s|$)";
return str.match(RegExp(regex, "g"));
@@ -134,7 +154,7 @@ function processFile(
path,
line: lc.line,
comment: getComment(tte),
- template: getTemplate(tte.template).replace(/"/g, '\\"'),
+ template: getTemplate(tte.template),
};
return res;
}
@@ -151,10 +171,12 @@ function processFile(
}
function formatMsgLine(head: string, msg: string) {
+ const m = msg.match(/(.*\n|.+$)/g)
+ if (!m) return;
// Do escaping, wrap break at newlines
- let parts = msg
- .match(/(.*\n|.+$)/g)
- .map((x) => x.replace(/\n/g, "\\n").replace(/"/g, '\\"'))
+ console.log("head", JSON.stringify(head));
+ console.log("msg", JSON.stringify(msg));
+ let parts = m.map((x) => x.replace(/\n/g, "\\n").replace(/"/g, '\\"'))
.map((p) => wordwrap(p))
.reduce((a, b) => a.concat(b));
if (parts.length == 1) {
@@ -417,28 +439,14 @@ export function potextract() {
!prog.isSourceFileDefaultLibrary(x),
);
- // console.log(ownFiles.map((x) => x.fileName));
-
- const chunks = [];
+ let header: string
+ try {
+ header = fs.readFileSync("src/i18n/poheader", "utf-8")
+ } catch (e) {
+ header = DEFAULT_PO_HEADER
+ }
- chunks.push(`# SOME DESCRIPTIVE TITLE.
-# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
-# This file is distributed under the same license as the PACKAGE package.
-# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
-#
-#, fuzzy
-msgid ""
-msgstr ""
-"Project-Id-Version: PACKAGE VERSION\\n"
-"Report-Msgid-Bugs-To: \\n"
-"POT-Creation-Date: 2016-11-23 00:00+0100\\n"
-"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n"
-"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n"
-"Language-Team: LANGUAGE <LL@li.org>\\n"
-"Language: \\n"
-"MIME-Version: 1.0\\n"
-"Content-Type: text/plain; charset=UTF-8\\n"
-"Content-Transfer-Encoding: 8bit\\n"\n\n`);
+ const chunks = [header];
const knownMessageIds = new Set<string>();
diff --git a/packages/pogen/tsconfig.json b/packages/pogen/tsconfig.json
index 68225832d..482ce6fe8 100644
--- a/packages/pogen/tsconfig.json
+++ b/packages/pogen/tsconfig.json
@@ -1,13 +1,13 @@
{
"compilerOptions": {
"module": "commonjs",
- "target": "es5",
+ "target": "ES2020",
"noImplicitAny": false,
"outDir": "lib",
"incremental": true,
"moduleResolution": "node",
"sourceMap": true,
- "lib": ["es6"],
+ "lib": ["ES2020"],
"types": ["node"]
},
"include": ["src/**/*.ts"]
diff --git a/packages/taler-harness/Makefile b/packages/taler-harness/Makefile
new file mode 100644
index 000000000..e9663aa61
--- /dev/null
+++ b/packages/taler-harness/Makefile
@@ -0,0 +1,47 @@
+# This Makefile has been placed in the public domain.
+
+ifeq ($(TOPLEVEL), yes)
+ $(info top-level build)
+ -include ../../.config.mk
+ override DESTDIR := $(TOP_DESTDIR)
+else
+ $(info package-level build)
+ -include ../../.config.mk
+ -include .config.mk
+endif
+
+$(info prefix is $(prefix))
+
+all:
+ @echo use 'make install' to build and install taler-harness
+
+ifndef prefix
+.PHONY: warn-noprefix install
+warn-noprefix:
+ @echo "no prefix configured, did you run ./configure?"
+install: warn-noprefix
+else
+BINDIR = $(prefix)/bin
+LIBDIR = $(prefix)/lib/taler-harness
+NODEDIR = $(LIBDIR)/node_modules/taler-harness
+.PHONY: install deps install-nodeps
+install-nodeps:
+ ./build.mjs
+ install -d $(DESTDIR)$(BINDIR)
+ install -d $(DESTDIR)$(NODEDIR)
+ install -d $(DESTDIR)$(NODEDIR)/bin
+ install -d $(DESTDIR)$(NODEDIR)/dist
+ install ./dist/taler-harness-bundled.cjs $(DESTDIR)$(NODEDIR)/dist/
+ install ./dist/taler-harness-bundled.cjs.map $(DESTDIR)$(NODEDIR)/dist/
+ install ./bin/taler-harness.mjs $(DESTDIR)$(NODEDIR)/bin/
+ ln -sf ../lib/taler-harness/node_modules/taler-harness/bin/taler-harness.mjs $(DESTDIR)$(BINDIR)/taler-harness
+deps:
+ pnpm install --frozen-lockfile --filter @gnu-taler/taler-harness...
+install:
+ $(MAKE) deps
+ $(MAKE) install-nodeps
+endif
+
+.PHONY: deb
+deb:
+ dpkg-buildpackage -rfakeroot -b -uc -us
diff --git a/packages/taler-harness/README.md b/packages/taler-harness/README.md
new file mode 100644
index 000000000..f88024297
--- /dev/null
+++ b/packages/taler-harness/README.md
@@ -0,0 +1,13 @@
+# taler-harness
+
+This package implements the `taler-harness` CLI tool. It contains integration
+tests for GNU Taler and GNU anastasis, as well as various helpers for managing
+deployments of GNU Taler.
+
+## Debugging
+
+To get more actionable stack traces, enable source maps for node:
+
+```
+export NODE_OPTIONS=--enable-source-maps
+```
diff --git a/packages/taler-harness/bin/taler-harness.mjs b/packages/taler-harness/bin/taler-harness.mjs
new file mode 100755
index 000000000..f8deebedb
--- /dev/null
+++ b/packages/taler-harness/bin/taler-harness.mjs
@@ -0,0 +1,19 @@
+#!/usr/bin/env node
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { main } from '../dist/taler-harness-bundled.cjs';
+main();
diff --git a/packages/taler-harness/build.mjs b/packages/taler-harness/build.mjs
new file mode 100755
index 000000000..ef2a2b111
--- /dev/null
+++ b/packages/taler-harness/build.mjs
@@ -0,0 +1,76 @@
+#!/usr/bin/env node
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import esbuild from "esbuild";
+import fs from "fs";
+import path from "path";
+
+const BASE = process.cwd();
+
+let GIT_ROOT = BASE;
+while (!fs.existsSync(path.join(GIT_ROOT, ".git")) && GIT_ROOT !== "/") {
+ GIT_ROOT = path.join(GIT_ROOT, "../");
+}
+if (GIT_ROOT === "/") {
+ console.log("not found");
+ process.exit(1);
+}
+const GIT_HASH = GIT_ROOT === "/" ? undefined : git_hash();
+
+let _package = JSON.parse(fs.readFileSync(path.join(BASE, "package.json")));
+
+function git_hash() {
+ const rev = fs
+ .readFileSync(path.join(GIT_ROOT, ".git", "HEAD"))
+ .toString()
+ .trim()
+ .split(/.*[: ]/)
+ .slice(-1)[0];
+ if (rev.indexOf("/") === -1) {
+ return rev;
+ } else {
+ return fs
+ .readFileSync(path.join(GIT_ROOT, ".git", rev))
+ .toString()
+ .trim();
+ }
+}
+
+// Still commonjs, because axios doesn't work properly under mjs
+export const buildConfig = {
+ entryPoints: ["src/index.ts"],
+ outfile: "dist/taler-harness-bundled.cjs",
+ bundle: true,
+ minify: false,
+ target: ["es2020"],
+ format: "cjs",
+ platform: "node",
+ sourcemap: true,
+ inject: ["src/import-meta-url.js"],
+ define: {
+ __VERSION__: `"${_package.version}"`,
+ __GIT_HASH__: `"${GIT_HASH}"`,
+ "walletCoreBuildInfo.implementationSemver": `"${_package.version}"`,
+ "walletCoreBuildInfo.implementationGitHash": `"${GIT_HASH}"`,
+ "import.meta.url": "import_meta_url",
+ },
+};
+
+esbuild.build(buildConfig).catch((e) => {
+ console.log(e);
+ process.exit(1);
+});
diff --git a/packages/taler-harness/debian/README b/packages/taler-harness/debian/README
new file mode 100644
index 000000000..fb451a71d
--- /dev/null
+++ b/packages/taler-harness/debian/README
@@ -0,0 +1,8 @@
+For the moment, building the Debian package needs
+a preliminary manual step to install the taler-harness
+Node.JS package. In the future, this will either be invoked
+by DH, or added as packaging instructions to Debian.
+
+$ ./configure --prefix=/usr
+$ make install
+$ dpkg-buildpackage -rfakeroot -b -uc -us
diff --git a/packages/taler-harness/debian/changelog b/packages/taler-harness/debian/changelog
new file mode 100644
index 000000000..269c6b99d
--- /dev/null
+++ b/packages/taler-harness/debian/changelog
@@ -0,0 +1,57 @@
+taler-harness (0.10.7) unstable; urgency=low
+
+ * Release 0.10.7
+
+ -- Florian Dold <dold@taler.net> Mon, 22 Apr 2024 20:16:39 +0200
+
+taler-harness (0.10.6) unstable; urgency=low
+
+ * Release 0.10.6
+
+ -- Florian Dold <dold@taler.net> Wed, 10 Apr 2024 15:19:33 +0200
+
+taler-harness (0.10.5) unstable; urgency=low
+
+ * Release 0.10.5
+
+ -- Florian Dold <dold@taler.net> Tue, 09 Apr 2024 16:13:58 +0200
+
+taler-harness (0.10.4) unstable; urgency=low
+
+ * Release 0.10.4
+
+ -- Florian Dold <dold@taler.net> Tue, 09 Apr 2024 14:39:44 +0200
+
+taler-harness (0.9.4a) unstable; urgency=low
+
+ * Release v0.9.4a.
+
+ -- Florian Dold <dold@taler.net> Thu, 07 Mar 2024 23:43:05 +0100
+
+taler-harness (0.9.3-1) unstable; urgency=low
+
+ * Misc bugfixes.
+
+ -- Christian Grothoff <grothoff@gnu.org> Tue, 13 Dec 2023 18:53:15 -0700
+
+taler-harness (0.9.3) unstable; urgency=low
+
+ * First release for GNU Taler v0.9.3.
+
+ -- Christian Grothoff <grothoff@gnu.org> Wed, 29 Nov 2023 08:53:15 -0800
+
+taler-harness (0.9.2-2) unstable; urgency=low
+
+ * Release with various Debian package fixes.
+
+ -- Christian Grothoff <grothoff@gnu.org> Sat, 3 Mar 2023 23:47:15 -0300
+
+taler-harness (0.9.2) unstable; urgency=low
+
+ * Official 0.9.2 release.
+
+ -- Christian Grothoff <grothoff@gnu.org> Sat, 25 Feb 2023 12:47:15 -0300
+
+Local variables:
+mode: debian-changelog
+End:
diff --git a/packages/taler-harness/debian/control b/packages/taler-harness/debian/control
new file mode 100644
index 000000000..c57b01202
--- /dev/null
+++ b/packages/taler-harness/debian/control
@@ -0,0 +1,16 @@
+Source: taler-harness
+Section: networking
+Priority: optional
+Maintainer: Taler Systems SA <deb@taler.net>
+Uploaders: Christian Grothoff <grothoff@gnu.org>, Florian Dold <dold@taler.net>
+Build-Depends: debhelper-compat (= 12),
+Standards-Version: 4.1.0
+Vcs-Git: https://git.taler.net/wallet-core.git
+Homepage: https://taler.net/
+
+Package: taler-harness
+Architecture: all
+Depends: nodejs,
+ ${misc:Depends}
+Recommends:
+Description: Software package to test Taler installations.
diff --git a/packages/taler-harness/debian/rules b/packages/taler-harness/debian/rules
new file mode 100755
index 000000000..0a8d6408c
--- /dev/null
+++ b/packages/taler-harness/debian/rules
@@ -0,0 +1,19 @@
+#!/usr/bin/make -f
+
+%:
+ dh ${@}
+
+# Override because our configure doesn't like extra arguments.
+override_dh_auto_configure:
+ ./configure --prefix=/usr
+
+override_dh_builddeb:
+ dh_builddeb -- -Zgzip
+
+# Override this step because it's very slow and likely
+# unnecessary for us.
+override_dh_strip_nondeterminism:
+ true
+
+get-orig-source:
+ uscan --force-download --rename
diff --git a/packages/taler-harness/package.json b/packages/taler-harness/package.json
new file mode 100644
index 000000000..38d640f51
--- /dev/null
+++ b/packages/taler-harness/package.json
@@ -0,0 +1,45 @@
+{
+ "name": "@gnu-taler/taler-harness",
+ "version": "0.10.7",
+ "description": "",
+ "engines": {
+ "node": ">=0.12.0"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git://git.taler.net/wallet-core.git"
+ },
+ "author": "Florian Dold",
+ "license": "GPL-3.0",
+ "bin": {
+ "taler-harness": "./bin/taler-harness.mjs"
+ },
+ "type": "module",
+ "scripts": {
+ "compile": "tsc && ./build.mjs",
+ "check": "tsc",
+ "test": "tsc",
+ "typedoc": "typedoc --out dist/typedoc ./src/",
+ "clean": "rm -rf lib dist tsconfig.tsbuildinfo",
+ "pretty": "prettier --write src"
+ },
+ "files": [
+ "AUTHORS",
+ "README",
+ "COPYING",
+ "bin/",
+ "dist/node",
+ "src/"
+ ],
+ "devDependencies": {
+ "@types/node": "^18.11.17",
+ "esbuild": "^0.19.9",
+ "prettier": "^3.1.1",
+ "typescript": "^5.3.3"
+ },
+ "dependencies": {
+ "@gnu-taler/taler-util": "workspace:*",
+ "@gnu-taler/taler-wallet-core": "workspace:*",
+ "tslib": "^2.6.2"
+ }
+}
diff --git a/packages/taler-wallet-cli/src/bench1.ts b/packages/taler-harness/src/bench1.ts
index 84786d25a..216760260 100644
--- a/packages/taler-wallet-cli/src/bench1.ts
+++ b/packages/taler-harness/src/bench1.ts
@@ -18,21 +18,22 @@
* Imports.
*/
import {
+ AmountString,
buildCodecForObject,
+ codecForBoolean,
codecForNumber,
codecForString,
- codecForBoolean,
codecOptional,
j2s,
Logger,
} from "@gnu-taler/taler-util";
import {
- getDefaultNodeWallet2,
- NodeHttpLib,
- WalletApiOperation,
- Wallet,
AccessStats,
+ createNativeWalletHost2,
+ Wallet,
+ WalletApiOperation,
} from "@gnu-taler/taler-wallet-core";
+import { harnessHttpLib } from "./harness/harness.js";
/**
* Entry point for the benchmark.
@@ -46,9 +47,6 @@ export async function runBench1(configJson: any): Promise<void> {
// Validate the configuration file for this benchmark.
const b1conf = codecForBench1Config().decode(configJson);
- const myHttpLib = new NodeHttpLib();
- myHttpLib.setThrottling(false);
-
const numIter = b1conf.iterations ?? 1;
const numDeposits = b1conf.deposits ?? 5;
const restartWallet = b1conf.restartAfter ?? 20;
@@ -66,7 +64,6 @@ export async function runBench1(configJson: any): Promise<void> {
} else {
logger.info("not trusting exchange (validating signatures)");
}
- const batchWithdrawal = !!process.env["TALER_WALLET_BATCH_WITHDRAWAL"];
let wallet = {} as Wallet;
let getDbStats: () => AccessStats;
@@ -81,32 +78,33 @@ export async function runBench1(configJson: any): Promise<void> {
console.log("wallet DB stats", j2s(getDbStats!()));
}
- const res = await getDefaultNodeWallet2({
+ const res = await createNativeWalletHost2({
// No persistent DB storage.
persistentStoragePath: undefined,
- httpLib: myHttpLib,
+ httpLib: harnessHttpLib,
});
wallet = res.wallet;
getDbStats = res.getDbStats;
- if (trustExchange) {
- wallet.setInsecureTrustExchange();
- }
- wallet.setBatchWithdrawal(batchWithdrawal);
- await wallet.client.call(WalletApiOperation.InitWallet, {});
+ await wallet.client.call(WalletApiOperation.InitWallet, {
+ config: {
+ testing: {
+ insecureTrustExchange: trustExchange,
+ },
+ features: {},
+ },
+ });
}
logger.trace(`Starting withdrawal amount=${withdrawAmount}`);
let start = Date.now();
- await wallet.client.call(WalletApiOperation.WithdrawFakebank, {
- amount: b1conf.currency + ":" + withdrawAmount,
- bank: b1conf.bank,
- exchange: b1conf.exchange,
+ await wallet.client.call(WalletApiOperation.WithdrawTestBalance, {
+ amount: (b1conf.currency + ":" + withdrawAmount) as AmountString,
+ corebankApiBaseUrl: b1conf.bank,
+ exchangeBaseUrl: b1conf.exchange,
});
- await wallet.runTaskLoop({
- stopWhenDone: true,
- });
+ await wallet.client.call(WalletApiOperation.TestingWaitTasksDone, {});
logger.info(
`Finished withdrawal amount=${withdrawAmount} time=${Date.now() - start}`,
@@ -118,13 +116,11 @@ export async function runBench1(configJson: any): Promise<void> {
start = Date.now();
await wallet.client.call(WalletApiOperation.CreateDepositGroup, {
- amount: b1conf.currency + ":10",
+ amount: (b1conf.currency + ":10") as AmountString,
depositPaytoUri: b1conf.payto,
});
- await wallet.runTaskLoop({
- stopWhenDone: true,
- });
+ await wallet.client.call(WalletApiOperation.TestingWaitTasksDone, {});
logger.info(`Finished deposit amount=10 time=${Date.now() - start}`);
}
diff --git a/packages/taler-wallet-cli/src/bench2.ts b/packages/taler-harness/src/bench2.ts
index 196737436..90924caec 100644
--- a/packages/taler-wallet-cli/src/bench2.ts
+++ b/packages/taler-harness/src/bench2.ts
@@ -18,24 +18,29 @@
* Imports.
*/
import {
+ AmountString,
buildCodecForObject,
codecForNumber,
codecForString,
codecOptional,
Logger,
} from "@gnu-taler/taler-util";
+import { createPlatformHttpLib } from "@gnu-taler/taler-util/http";
import {
- checkReserve,
- createFakebankReserve,
+ applyRunConfigDefaults,
CryptoDispatcher,
+ SynchronousCryptoWorkerFactoryPlain,
+ Wallet,
+} from "@gnu-taler/taler-wallet-core";
+import {
+ checkReserve,
+ createTestingReserve,
depositCoin,
downloadExchangeInfo,
findDenomOrThrow,
- NodeHttpLib,
refreshCoin,
- SynchronousCryptoWorkerFactoryNode,
withdrawCoin,
-} from "@gnu-taler/taler-wallet-core";
+} from "@gnu-taler/taler-wallet-core/dbless";
/**
* Entry point for the benchmark.
@@ -49,17 +54,22 @@ export async function runBench2(configJson: any): Promise<void> {
// Validate the configuration file for this benchmark.
const benchConf = codecForBench2Config().decode(configJson);
const curr = benchConf.currency;
- const cryptoDisp = new CryptoDispatcher(new SynchronousCryptoWorkerFactoryNode());
+ const cryptoDisp = new CryptoDispatcher(
+ new SynchronousCryptoWorkerFactoryPlain(),
+ );
const cryptoApi = cryptoDisp.cryptoApi;
- const http = new NodeHttpLib();
- http.setThrottling(false);
+ const http = createPlatformHttpLib({
+ enableThrottling: false,
+ });
const numIter = benchConf.iterations ?? 1;
const numDeposits = benchConf.deposits ?? 5;
const reserveAmount = (numDeposits + 1) * 10;
+ const defaultConfig = applyRunConfigDefaults();
+
for (let i = 0; i < numIter; i++) {
const exchangeInfo = await downloadExchangeInfo(benchConf.exchange, http);
@@ -67,10 +77,10 @@ export async function runBench2(configJson: any): Promise<void> {
console.log("creating fakebank reserve");
- await createFakebankReserve({
+ await createTestingReserve({
amount: `${curr}:${reserveAmount}`,
exchangeInfo,
- fakebankBaseUrl: benchConf.bank,
+ corebankApiBaseUrl: benchConf.bank,
http,
reservePub: reserveKeyPair.pub,
});
@@ -81,7 +91,9 @@ export async function runBench2(configJson: any): Promise<void> {
console.log("reserve found");
- const d1 = findDenomOrThrow(exchangeInfo, `${curr}:8`);
+ const d1 = findDenomOrThrow(exchangeInfo, `${curr}:8` as AmountString, {
+ denomselAllowLate: defaultConfig.testing.denomselAllowLate,
+ });
for (let j = 0; j < numDeposits; j++) {
console.log("withdrawing coin");
@@ -99,7 +111,7 @@ export async function runBench2(configJson: any): Promise<void> {
console.log("depositing coin");
await depositCoin({
- amount: `${curr}:4`,
+ amount: `${curr}:4` as AmountString,
coin: coin,
cryptoApi,
exchangeBaseUrl: benchConf.exchange,
@@ -108,8 +120,12 @@ export async function runBench2(configJson: any): Promise<void> {
});
const refreshDenoms = [
- findDenomOrThrow(exchangeInfo, `${curr}:1`),
- findDenomOrThrow(exchangeInfo, `${curr}:1`),
+ findDenomOrThrow(exchangeInfo, `${curr}:1` as AmountString, {
+ denomselAllowLate: defaultConfig.testing.denomselAllowLate,
+ }),
+ findDenomOrThrow(exchangeInfo, `${curr}:1` as AmountString, {
+ denomselAllowLate: defaultConfig.testing.denomselAllowLate,
+ }),
];
console.log("refreshing coin");
diff --git a/packages/taler-wallet-cli/src/bench3.ts b/packages/taler-harness/src/bench3.ts
index 6041c525c..a5bc094df 100644
--- a/packages/taler-wallet-cli/src/bench3.ts
+++ b/packages/taler-harness/src/bench3.ts
@@ -18,6 +18,7 @@
* Imports.
*/
import {
+ AmountString,
buildCodecForObject,
codecForNumber,
codecForString,
@@ -25,12 +26,12 @@ import {
j2s,
Logger,
} from "@gnu-taler/taler-util";
+import { createPlatformHttpLib } from "@gnu-taler/taler-util/http";
import {
- getDefaultNodeWallet2,
- NodeHttpLib,
- WalletApiOperation,
- Wallet,
AccessStats,
+ createNativeWalletHost2,
+ Wallet,
+ WalletApiOperation,
} from "@gnu-taler/taler-wallet-core";
import benchMerchantIDGenerator from "./benchMerchantIDGenerator.js";
@@ -50,8 +51,9 @@ export async function runBench3(configJson: any): Promise<void> {
throw new Error("Payto template url must contain '${id}' placeholder");
}
- const myHttpLib = new NodeHttpLib();
- myHttpLib.setThrottling(false);
+ const myHttpLib = createPlatformHttpLib({
+ enableThrottling: false,
+ });
const numIter = b3conf.iterations ?? 1;
const numDeposits = b3conf.deposits ?? 5;
@@ -59,7 +61,10 @@ export async function runBench3(configJson: any): Promise<void> {
const withdrawAmount = (numDeposits + 1) * 10;
- const IDGenerator = benchMerchantIDGenerator(b3conf.randomAlg, b3conf.numMerchants ?? 100);
+ const IDGenerator = benchMerchantIDGenerator(
+ b3conf.randomAlg,
+ b3conf.numMerchants ?? 100,
+ );
logger.info(
`Starting Benchmark iterations=${numIter} deposits=${numDeposits} with ${b3conf.randomAlg} merchant selection`,
@@ -71,8 +76,6 @@ export async function runBench3(configJson: any): Promise<void> {
} else {
logger.info("not trusting exchange (validating signatures)");
}
- const batchWithdrawal = !!process.env["TALER_WALLET_BATCH_WITHDRAWAL"];
-
let wallet = {} as Wallet;
let getDbStats: () => AccessStats;
@@ -86,32 +89,33 @@ export async function runBench3(configJson: any): Promise<void> {
console.log("wallet DB stats", j2s(getDbStats!()));
}
- const res = await getDefaultNodeWallet2({
+ const res = await createNativeWalletHost2({
// No persistent DB storage.
persistentStoragePath: undefined,
httpLib: myHttpLib,
});
wallet = res.wallet;
getDbStats = res.getDbStats;
- if (trustExchange) {
- wallet.setInsecureTrustExchange();
- }
- wallet.setBatchWithdrawal(batchWithdrawal);
- await wallet.client.call(WalletApiOperation.InitWallet, {});
+ await wallet.client.call(WalletApiOperation.InitWallet, {
+ config: {
+ features: {},
+ testing: {
+ insecureTrustExchange: trustExchange,
+ },
+ },
+ });
}
logger.trace(`Starting withdrawal amount=${withdrawAmount}`);
let start = Date.now();
- await wallet.client.call(WalletApiOperation.WithdrawFakebank, {
- amount: b3conf.currency + ":" + withdrawAmount,
- bank: b3conf.bank,
- exchange: b3conf.exchange,
+ await wallet.client.call(WalletApiOperation.WithdrawTestBalance, {
+ amount: (b3conf.currency + ":" + withdrawAmount) as AmountString,
+ corebankApiBaseUrl: b3conf.bank,
+ exchangeBaseUrl: b3conf.exchange,
});
- await wallet.runTaskLoop({
- stopWhenDone: true,
- });
+ await wallet.client.call(WalletApiOperation.TestingWaitTasksDone, {});
logger.info(
`Finished withdrawal amount=${withdrawAmount} time=${Date.now() - start}`,
@@ -125,13 +129,11 @@ export async function runBench3(configJson: any): Promise<void> {
let payto = b3conf.paytoTemplate.replace("${id}", merchID.toString());
await wallet.client.call(WalletApiOperation.CreateDepositGroup, {
- amount: b3conf.currency + ":10",
+ amount: (b3conf.currency + ":10") as AmountString,
depositPaytoUri: payto,
});
- await wallet.runTaskLoop({
- stopWhenDone: true,
- });
+ await wallet.client.call(WalletApiOperation.TestingWaitTasksDone, {});
logger.info(`Finished deposit amount=10 time=${Date.now() - start}`);
}
diff --git a/packages/taler-wallet-cli/src/benchMerchantIDGenerator.ts b/packages/taler-harness/src/benchMerchantIDGenerator.ts
index b83c12bb8..89b26dc81 100644
--- a/packages/taler-wallet-cli/src/benchMerchantIDGenerator.ts
+++ b/packages/taler-harness/src/benchMerchantIDGenerator.ts
@@ -16,16 +16,15 @@
@author: Boss Marco
*/
-const getRandomInt = function(max: number) {
+const getRandomInt = function (max: number) {
return Math.floor(Math.random() * max);
-}
+};
abstract class BenchMerchantIDGenerator {
- abstract getRandomMerchantID(): number
+ abstract getRandomMerchantID(): number;
}
class ZipfGenerator extends BenchMerchantIDGenerator {
-
weights: number[];
total_weight: number;
@@ -33,10 +32,10 @@ class ZipfGenerator extends BenchMerchantIDGenerator {
super();
this.weights = new Array<number>(numMerchants);
for (var i = 0; i < this.weights.length; i++) {
- /* we use integers (floor), make sure we have big enough values
+ /* we use integers (floor), make sure we have big enough values
* by multiplying with
* numMerchants again */
- this.weights[i] = Math.floor((numMerchants/(i+1)) * numMerchants);
+ this.weights[i] = Math.floor((numMerchants / (i + 1)) * numMerchants);
}
this.total_weight = this.weights.reduce((p, n) => p + n);
}
@@ -48,7 +47,7 @@ class ZipfGenerator extends BenchMerchantIDGenerator {
for (var i = 0; i < this.weights.length; i++) {
current += this.weights[i];
if (random <= current) {
- return i+1;
+ return i + 1;
}
}
@@ -58,12 +57,11 @@ class ZipfGenerator extends BenchMerchantIDGenerator {
}
class RandomGenerator extends BenchMerchantIDGenerator {
-
- max: number
+ max: number;
constructor(numMerchants: number) {
super();
- this.max = numMerchants
+ this.max = numMerchants;
}
getRandomMerchantID() {
@@ -71,7 +69,10 @@ class RandomGenerator extends BenchMerchantIDGenerator {
}
}
-export default function(type: string, maxID: number): BenchMerchantIDGenerator {
+export default function (
+ type: string,
+ maxID: number,
+): BenchMerchantIDGenerator {
switch (type) {
case "zipf":
return new ZipfGenerator(maxID);
diff --git a/packages/taler-harness/src/env-full.ts b/packages/taler-harness/src/env-full.ts
new file mode 100644
index 000000000..bb2cb8c47
--- /dev/null
+++ b/packages/taler-harness/src/env-full.ts
@@ -0,0 +1,101 @@
+/*
+ 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 { Duration, j2s, URL } from "@gnu-taler/taler-util";
+import { CoinConfig, defaultCoinConfig } from "./harness/denomStructures.js";
+import {
+ GlobalTestState,
+ setupDb,
+ ExchangeService,
+ FakebankService,
+ MerchantService,
+ generateRandomPayto,
+} from "./harness/harness.js";
+
+/**
+ * Entry point for the full Taler test environment.
+ */
+export async function runEnvFull(t: GlobalTestState): Promise<void> {
+ 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",
+ );
+ console.log("exchange bank account", j2s(exchangeBankAccount));
+ exchange.addBankAccount("1", exchangeBankAccount);
+
+ bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri);
+
+ await bank.start();
+
+ await bank.pingUntilAvailable();
+
+ const coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS"));
+ 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")],
+ defaultWireTransferDelay: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ minutes: 1 }),
+ ),
+ });
+
+ await merchant.addInstanceWithWireAccount({
+ id: "minst1",
+ name: "minst1",
+ paytoUris: [generateRandomPayto("minst1")],
+ defaultWireTransferDelay: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ minutes: 1 }),
+ ),
+ });
+
+ console.log("setup done!");
+}
diff --git a/packages/taler-wallet-cli/src/env1.ts b/packages/taler-harness/src/env1.ts
index aec0b7b8f..aec0b7b8f 100644
--- a/packages/taler-wallet-cli/src/env1.ts
+++ b/packages/taler-harness/src/env1.ts
diff --git a/packages/taler-wallet-cli/src/harness/denomStructures.ts b/packages/taler-harness/src/harness/denomStructures.ts
index b12857c7e..2d5e719b0 100644
--- a/packages/taler-wallet-cli/src/harness/denomStructures.ts
+++ b/packages/taler-harness/src/harness/denomStructures.ts
@@ -136,7 +136,7 @@ export function makeNoFeeCoinConfig(curr: string): CoinConfig[] {
const ct = 2 ** i;
const unit = Math.floor(ct / 100);
- const cent = ct % 100;
+ const cent = `${ct % 100}`.padStart(2, "0");
cc.push({
cipher: "RSA",
diff --git a/packages/taler-wallet-cli/src/harness/faultInjection.ts b/packages/taler-harness/src/harness/faultInjection.ts
index 4c3d0c123..f4d7fc4b9 100644
--- a/packages/taler-wallet-cli/src/harness/faultInjection.ts
+++ b/packages/taler-harness/src/harness/faultInjection.ts
@@ -47,6 +47,10 @@ export interface FaultInjectionRequestContext {
requestHeaders: Record<string, string | string[] | undefined>;
requestBody?: Buffer;
dropRequest: boolean;
+ // These are only used when the request is dropped
+ substituteResponseBody?: Buffer;
+ substituteResponseStatusCode?: number;
+ substituteResponseHeaders?: Record<string, string | string[] | undefined>;
}
export interface FaultInjectionResponseContext {
@@ -101,7 +105,18 @@ export class FaultProxy {
}
if (faultReqContext.dropRequest) {
- res.destroy();
+ if (faultReqContext.substituteResponseStatusCode) {
+ const statusCode = faultReqContext.substituteResponseStatusCode;
+ res.writeHead(
+ statusCode,
+ http.STATUS_CODES[statusCode],
+ faultReqContext.substituteResponseHeaders,
+ );
+ res.write(faultReqContext.substituteResponseBody);
+ res.end();
+ } else {
+ res.destroy();
+ }
return;
}
diff --git a/packages/taler-wallet-cli/src/harness/harness.ts b/packages/taler-harness/src/harness/harness.ts
index 6f722dc8d..68c0744fc 100644
--- a/packages/taler-wallet-cli/src/harness/harness.ts
+++ b/packages/taler-harness/src/harness/harness.ts
@@ -21,66 +21,61 @@
* @author Florian Dold <dold@taler.net>
*/
-const logger = new Logger("harness.ts");
-
/**
* Imports
*/
import {
+ AccountAddDetails,
+ AccountRestriction,
AmountJson,
Amounts,
- AmountString,
Configuration,
CoreApiResponse,
- createEddsaKeyPair,
Duration,
- eddsaGetPublic,
EddsaKeyPair,
+ Logger,
+ MerchantInstanceConfig,
+ PartialMerchantInstanceConfig,
+ TalerCorebankApiClient,
+ TalerError,
+ WalletNotification,
+ createEddsaKeyPair,
+ eddsaGetPublic,
encodeCrock,
hash,
j2s,
- Logger,
+ openPromise,
parsePaytoUri,
stringToBytes,
- TalerProtocolDuration,
} from "@gnu-taler/taler-util";
import {
- BankAccessApi,
- BankApi,
- BankServiceHandle,
- HarnessExchangeBankAccount,
- NodeHttpLib,
- openPromise,
- TalerError,
+ HttpRequestLibrary,
+ createPlatformHttpLib,
+ expectSuccessResponseOrThrow,
+} from "@gnu-taler/taler-util/http";
+import {
WalletCoreApiClient,
+ WalletCoreRequestType,
+ WalletCoreResponseType,
+ WalletOperations,
} from "@gnu-taler/taler-wallet-core";
+import {
+ RemoteWallet,
+ WalletNotificationWaiter,
+ createRemoteWallet,
+ getClientFromRemoteWallet,
+ makeNotificationWaiter,
+} from "@gnu-taler/taler-wallet-core/remote";
import { deepStrictEqual } from "assert";
-import axiosImp, { AxiosError } from "axios";
import { ChildProcess, spawn } from "child_process";
-import * as child_process from "child_process";
import * as fs from "fs";
import * as http from "http";
+import * as net from "node:net";
import * as path from "path";
import * as readline from "readline";
-import { URL } from "url";
-import * as util from "util";
import { CoinConfig } from "./denomStructures.js";
-import { LibeufinNexusApi, LibeufinSandboxApi } from "./libeufin-apis.js";
-import {
- codecForMerchantOrderPrivateStatusResponse,
- codecForPostOrderResponse,
- MerchantInstancesResponse,
- MerchantOrderPrivateStatusResponse,
- PostOrderRequest,
- PostOrderResponse,
- TipCreateConfirmation,
- TipCreateRequest,
- TippingReserveStatus,
-} from "./merchantApiTypes.js";
-
-const exec = util.promisify(child_process.exec);
-const axios = axiosImp.default;
+const logger = new Logger("harness.ts");
export async function delayMs(ms: number): Promise<void> {
return new Promise((resolve, reject) => {
@@ -97,6 +92,21 @@ interface WaitResult {
signal: NodeJS.Signals | null;
}
+class CommandError extends Error {
+ constructor(
+ public message: string,
+ public logName: string,
+ public command: string,
+ public args: string[],
+ public env: Env,
+ public code: number | null,
+ ) {
+ super(message);
+ }
+}
+interface Env {
+ [index: string]: string | undefined;
+}
/**
* Run a shell command, return stdout.
*/
@@ -104,15 +114,15 @@ export async function sh(
t: GlobalTestState,
logName: string,
command: string,
- env: { [index: string]: string | undefined } = process.env,
+ env: Env = process.env,
): Promise<string> {
- logger.info(`running command ${command}`);
+ logger.trace(`running command ${command}`);
return new Promise((resolve, reject) => {
const stdoutChunks: Buffer[] = [];
const proc = spawn(command, {
stdio: ["inherit", "pipe", "pipe"],
shell: true,
- env: env,
+ env,
});
proc.stdout.on("data", (x) => {
if (x instanceof Buffer) {
@@ -127,16 +137,34 @@ export async function sh(
});
proc.stderr.pipe(stderrLog);
proc.on("exit", (code, signal) => {
- logger.info(`child process exited (${code} / ${signal})`);
+ logger.info(`child process ${logName} exited (${code} / ${signal})`);
if (code != 0) {
- reject(Error(`Unexpected exit code ${code} for '${command}'`));
+ reject(
+ new CommandError(
+ `Unexpected exit code ${code}`,
+ logName,
+ command,
+ [],
+ env,
+ code,
+ ),
+ );
return;
}
const b = Buffer.concat(stdoutChunks).toString("utf-8");
resolve(b);
});
- proc.on("error", () => {
- reject(Error("Child process had error"));
+ proc.on("error", (err) => {
+ reject(
+ new CommandError(
+ "Child process had error:" + err.message,
+ logName,
+ command,
+ [],
+ env,
+ null,
+ ),
+ );
});
});
}
@@ -165,6 +193,7 @@ export async function runCommand(
env: { [index: string]: string | undefined } = process.env,
): Promise<string> {
logger.info(`running command ${shellescape([command, ...args])}`);
+
return new Promise((resolve, reject) => {
const stdoutChunks: Buffer[] = [];
const proc = spawn(command, args, {
@@ -185,16 +214,34 @@ export async function runCommand(
});
proc.stderr.pipe(stderrLog);
proc.on("exit", (code, signal) => {
- logger.info(`child process exited (${code} / ${signal})`);
+ logger.trace(`child process exited (${code} / ${signal})`);
if (code != 0) {
- reject(Error(`Unexpected exit code ${code} for '${command}'`));
+ reject(
+ new CommandError(
+ `Unexpected exit code ${code}`,
+ logName,
+ command,
+ [],
+ env,
+ code,
+ ),
+ );
return;
}
const b = Buffer.concat(stdoutChunks).toString("utf-8");
resolve(b);
});
- proc.on("error", () => {
- reject(Error("Child process had error"));
+ proc.on("error", (err) => {
+ reject(
+ new CommandError(
+ "Child process had error:" + err.message,
+ logName,
+ command,
+ [],
+ env,
+ null,
+ ),
+ );
});
});
}
@@ -259,12 +306,6 @@ export class GlobalTestState {
);
}
- assertAxiosError(e: any): asserts e is AxiosError {
- if (!e.isAxiosError) {
- throw Error("expected axios error");
- }
- }
-
assertTrue(b: boolean): asserts b {
if (!b) {
throw Error("test assertion failed");
@@ -323,12 +364,16 @@ export class GlobalTestState {
stdio: ["inherit", "pipe", "pipe"],
env: env,
});
- logger.info(`spawned process (${logName}) with pid ${proc.pid}`);
+ logger.trace(`spawned process (${logName}) with pid ${proc.pid}`);
proc.on("error", (err) => {
logger.warn(`could not start process (${command})`, err);
});
proc.on("exit", (code, signal) => {
- logger.warn(`process ${logName} exited ${j2s({ code, signal })}`);
+ if (code == 0 && signal == null) {
+ logger.trace(`process ${logName} exited with success`);
+ } else {
+ logger.warn(`process ${logName} exited ${j2s({ code, signal })}`);
+ }
});
const stderrLogFileName = this.testDir + `/${logName}-stderr.log`;
const stderrLog = fs.createWriteStream(stderrLogFileName, {
@@ -350,23 +395,34 @@ export class GlobalTestState {
return;
}
if (shouldLingerInTest()) {
- logger.info("refusing to shut down, lingering was requested");
+ logger.trace("refusing to shut down, lingering was requested");
return;
}
this.inShutdown = true;
- logger.info("shutting down");
+ logger.trace("shutting down");
for (const s of this.servers) {
s.close();
s.removeAllListeners();
}
for (const p of this.procs) {
if (p.proc.exitCode == null) {
- logger.info(`killing process ${p.proc.pid}`);
+ logger.trace(`killing process ${p.proc.pid}`);
p.proc.kill("SIGTERM");
await p.wait();
}
}
}
+
+ /**
+ * Log that the test arrived a certain step.
+ *
+ * The step name should be unique across the whole
+ */
+ logStep(stepName: string): void {
+ // Now we just log, later we may report the steps that were done
+ // to easily see where the test hangs.
+ console.info(`STEP: ${stepName}`);
+ }
}
export function shouldLingerInTest(): boolean {
@@ -390,10 +446,42 @@ export interface DbInfo {
dbname: string;
}
-export async function setupDb(gc: GlobalTestState): Promise<DbInfo> {
- const dbname = "taler-integrationtest";
- await exec(`dropdb "${dbname}" || true`);
- await exec(`createdb "${dbname}"`);
+export interface SetupDbOpts {
+ nameSuffix?: string;
+}
+
+export async function setupDb(
+ t: GlobalTestState,
+ opts: SetupDbOpts = {},
+): Promise<DbInfo> {
+ let dbname: string;
+ if (!opts.nameSuffix) {
+ dbname = "taler-integrationtest";
+ } else {
+ dbname = `taler-integrationtest-${opts.nameSuffix}`;
+ }
+ try {
+ await runCommand(t, "dropdb", "dropdb", [dbname]);
+ } catch (e: any) {
+ logger.warn(`dropdb failed: ${e.toString()}`);
+ }
+ await runCommand(t, "createdb", "createdb", [dbname]);
+ return {
+ connStr: `postgres:///${dbname}`,
+ dbname,
+ };
+}
+
+/**
+ * Make sure that the taler-integrationtest-shared database exists.
+ * Don't delete it if it already exists.
+ */
+export async function setupSharedDb(t: GlobalTestState): Promise<DbInfo> {
+ const dbname = "taler-integrationtest-shared";
+ const databases = await runCommand(t, "list-dbs", "psql", ["-Aqtl"]);
+ if (databases.indexOf("taler-integrationtest-shared") < 0) {
+ await runCommand(t, "createdb", "createdb", [dbname]);
+ }
return {
connStr: `postgres:///${dbname}`,
dbname,
@@ -406,6 +494,7 @@ export interface BankConfig {
database: string;
allowRegistrations: boolean;
maxDebt?: string;
+ overrideTestDir?: string;
}
export interface FakeBankConfig {
@@ -413,11 +502,15 @@ export interface FakeBankConfig {
httpPort: number;
}
-function setTalerPaths(config: Configuration, home: string) {
+/**
+ * @param name additional component name, needed when launching multiple instances of the same component
+ */
+function setTalerPaths(config: Configuration, home: string, name?: string) {
config.setString("paths", "taler_home", home);
// We need to make sure that the path of taler_runtime_dir isn't too long,
// as it contains unix domain sockets (108 character limit).
- const runDir = fs.mkdtempSync("/tmp/taler-test-");
+ const extraName = name != null ? `${name}-` : "";
+ const runDir = fs.mkdtempSync(`/tmp/taler-test-${extraName}`);
config.setString("paths", "taler_runtime_dir", runDir);
config.setString(
"paths",
@@ -451,6 +544,14 @@ function setCoin(config: Configuration, c: CoinConfig) {
}
}
+function backoffStart(): number {
+ return 10;
+}
+
+function backoffIncrement(n: number): number {
+ return Math.min(Math.floor(n * 1.5), 1000);
+}
+
/**
* Send an HTTP request until it succeeds or the process dies.
*/
@@ -462,16 +563,21 @@ export async function pingProc(
if (!proc || proc.proc.exitCode !== null) {
throw Error(`service process ${serviceName} not started, can't ping`);
}
+ let nextDelay = backoffStart();
while (true) {
try {
- logger.info(`pinging ${serviceName} at ${url}`);
- const resp = await axios.get(url);
- logger.info(`service ${serviceName} available`);
+ logger.trace(`pinging ${serviceName} at ${url}`);
+ const resp = await harnessHttpLib.fetch(url);
+ if (resp.status !== 200) {
+ throw Error("non-200 status code");
+ }
+ logger.trace(`service ${serviceName} available`);
return;
} catch (e: any) {
- logger.info(`service ${serviceName} not ready:`, e.toString());
- //console.log(e);
- await delayMs(1000);
+ logger.warn(`service ${serviceName} not ready:`, e.toString());
+ logger.info(`waiting ${nextDelay}ms on ${serviceName}`);
+ await delayMs(nextDelay);
+ nextDelay = backoffIncrement(nextDelay);
}
if (!proc || proc.proc.exitCode != null || proc.proc.signalCode != null) {
throw Error(`service process ${serviceName} stopped unexpectedly`);
@@ -489,290 +595,21 @@ class BankServiceBase {
) {}
}
-/**
- * Work in progress. The key point is that both Sandbox and Nexus
- * will be configured and started by this class.
- */
-class LibEuFinBankService extends BankServiceBase implements BankServiceHandle {
- sandboxProc: ProcessWrapper | undefined;
- nexusProc: ProcessWrapper | undefined;
+export interface HarnessExchangeBankAccount {
+ accountName: string;
+ accountPassword: string;
+ accountPaytoUri: string;
+ wireGatewayApiBaseUrl: string;
- http = new NodeHttpLib();
+ conversionUrl?: string;
- static async create(
- gc: GlobalTestState,
- bc: BankConfig,
- ): Promise<LibEuFinBankService> {
- return new LibEuFinBankService(gc, bc, "foo");
- }
-
- get port() {
- return this.bankConfig.httpPort;
- }
- get nexusPort() {
- return this.bankConfig.httpPort + 1000;
- }
-
- get nexusDbConn(): string {
- return `jdbc:sqlite:${this.globalTestState.testDir}/libeufin-nexus.sqlite3`;
- }
-
- get sandboxDbConn(): string {
- return `jdbc:sqlite:${this.globalTestState.testDir}/libeufin-sandbox.sqlite3`;
- }
-
- get nexusBaseUrl(): string {
- return `http://localhost:${this.nexusPort}`;
- }
-
- get baseUrlDemobank(): string {
- let url = new URL("demobanks/default/", this.baseUrlNetloc);
- return url.href;
- }
-
- // FIXME: Duplicate? Where is this needed?
- get baseUrlAccessApi(): string {
- let url = new URL("access-api/", this.baseUrlDemobank);
- return url.href;
- }
-
- get bankAccessApiBaseUrl(): string {
- let url = new URL("access-api/", this.baseUrlDemobank);
- return url.href;
- }
-
- get baseUrlNetloc(): string {
- return `http://localhost:${this.bankConfig.httpPort}/`;
- }
-
- get baseUrl(): string {
- return this.baseUrlAccessApi;
- }
-
- async setSuggestedExchange(
- e: ExchangeServiceInterface,
- exchangePayto: string,
- ) {
- await sh(
- this.globalTestState,
- "libeufin-sandbox-set-default-exchange",
- `libeufin-sandbox default-exchange ${e.baseUrl} ${exchangePayto}`,
- {
- ...process.env,
- LIBEUFIN_SANDBOX_DB_CONNECTION: this.sandboxDbConn,
- },
- );
- }
-
- // Create one at both sides: Sandbox and Nexus.
- async createExchangeAccount(
- accountName: string,
- password: string,
- ): Promise<HarnessExchangeBankAccount> {
- logger.info("Create Exchange account(s)!");
- /**
- * Many test cases try to create a Exchange account before
- * starting the bank; that's because the Pybank did it entirely
- * via the configuration file.
- */
- await this.start();
- await this.pingUntilAvailable();
- await LibeufinSandboxApi.createDemobankAccount(accountName, password, {
- baseUrl: this.baseUrlAccessApi,
- });
- let bankAccountLabel = accountName;
- await LibeufinSandboxApi.createDemobankEbicsSubscriber(
- {
- hostID: "talertestEbicsHost",
- userID: "exchangeEbicsUser",
- partnerID: "exchangeEbicsPartner",
- },
- bankAccountLabel,
- { baseUrl: this.baseUrlDemobank },
- );
-
- await LibeufinNexusApi.createUser(
- { baseUrl: this.nexusBaseUrl },
- {
- username: accountName,
- password: password,
- },
- );
- await LibeufinNexusApi.createEbicsBankConnection(
- { baseUrl: this.nexusBaseUrl },
- {
- name: "ebics-connection", // connection name.
- ebicsURL: new URL("ebicsweb", this.baseUrlNetloc).href,
- hostID: "talertestEbicsHost",
- userID: "exchangeEbicsUser",
- partnerID: "exchangeEbicsPartner",
- },
- );
- await LibeufinNexusApi.connectBankConnection(
- { baseUrl: this.nexusBaseUrl },
- "ebics-connection",
- );
- await LibeufinNexusApi.fetchAccounts(
- { baseUrl: this.nexusBaseUrl },
- "ebics-connection",
- );
- await LibeufinNexusApi.importConnectionAccount(
- { baseUrl: this.nexusBaseUrl },
- "ebics-connection", // connection name
- accountName, // offered account label
- `${accountName}-nexus-label`, // bank account label at Nexus
- );
- await LibeufinNexusApi.createTwgFacade(
- { baseUrl: this.nexusBaseUrl },
- {
- name: "exchange-facade",
- connectionName: "ebics-connection",
- accountName: `${accountName}-nexus-label`,
- currency: "EUR",
- reserveTransferLevel: "report",
- },
- );
- await LibeufinNexusApi.postPermission(
- { baseUrl: this.nexusBaseUrl },
- {
- action: "grant",
- permission: {
- subjectId: accountName,
- subjectType: "user",
- resourceType: "facade",
- resourceId: "exchange-facade", // facade name
- permissionName: "facade.talerWireGateway.transfer",
- },
- },
- );
- await LibeufinNexusApi.postPermission(
- { baseUrl: this.nexusBaseUrl },
- {
- action: "grant",
- permission: {
- subjectId: accountName,
- subjectType: "user",
- resourceType: "facade",
- resourceId: "exchange-facade", // facade name
- permissionName: "facade.talerWireGateway.history",
- },
- },
- );
- // Set fetch task.
- await LibeufinNexusApi.postTask(
- { baseUrl: this.nexusBaseUrl },
- `${accountName}-nexus-label`,
- {
- name: "wirewatch-task",
- cronspec: "* * *",
- type: "fetch",
- params: {
- level: "all",
- rangeType: "all",
- },
- },
- );
- await LibeufinNexusApi.postTask(
- { baseUrl: this.nexusBaseUrl },
- `${accountName}-nexus-label`,
- {
- name: "aggregator-task",
- cronspec: "* * *",
- type: "submit",
- params: {},
- },
- );
- let facadesResp = await LibeufinNexusApi.getAllFacades({
- baseUrl: this.nexusBaseUrl,
- });
- let accountInfoResp = await LibeufinSandboxApi.demobankAccountInfo(
- "admin",
- "secret",
- { baseUrl: this.baseUrlAccessApi },
- accountName, // bank account label.
- );
- return {
- accountName: accountName,
- accountPassword: password,
- accountPaytoUri: accountInfoResp.data.paytoUri,
- wireGatewayApiBaseUrl: facadesResp.data.facades[0].baseUrl,
- };
- }
-
- async start(): Promise<void> {
- /**
- * Because many test cases try to create a Exchange bank
- * account _before_ starting the bank (Pybank did it only via
- * the config), it is possible that at this point Sandbox and
- * Nexus are already running. Hence, this method only launches
- * them if they weren't launched earlier.
- */
-
- // Only go ahead if BOTH aren't running.
- if (this.sandboxProc || this.nexusProc) {
- logger.info("Nexus or Sandbox already running, not taking any action.");
- return;
- }
- await sh(
- this.globalTestState,
- "libeufin-sandbox-config-demobank",
- `libeufin-sandbox config --currency=${this.bankConfig.currency} default`,
- {
- ...process.env,
- LIBEUFIN_SANDBOX_DB_CONNECTION: this.sandboxDbConn,
- LIBEUFIN_SANDBOX_ADMIN_PASSWORD: "secret",
- },
- );
- this.sandboxProc = this.globalTestState.spawnService(
- "libeufin-sandbox",
- ["serve", "--port", `${this.port}`],
- "libeufin-sandbox",
- {
- ...process.env,
- LIBEUFIN_SANDBOX_DB_CONNECTION: this.sandboxDbConn,
- LIBEUFIN_SANDBOX_ADMIN_PASSWORD: "secret",
- },
- );
- await runCommand(
- this.globalTestState,
- "libeufin-nexus-superuser",
- "libeufin-nexus",
- ["superuser", "admin", "--password", "test"],
- {
- ...process.env,
- LIBEUFIN_NEXUS_DB_CONNECTION: this.nexusDbConn,
- },
- );
- this.nexusProc = this.globalTestState.spawnService(
- "libeufin-nexus",
- ["serve", "--port", `${this.nexusPort}`],
- "libeufin-nexus",
- {
- ...process.env,
- LIBEUFIN_NEXUS_DB_CONNECTION: this.nexusDbConn,
- },
- );
- // need to wait here, because at this point
- // a Ebics host needs to be created (RESTfully)
- await this.pingUntilAvailable();
- LibeufinSandboxApi.createEbicsHost(
- { baseUrl: this.baseUrlNetloc },
- "talertestEbicsHost",
- );
- }
+ debitRestrictions?: AccountRestriction[];
+ creditRestrictions?: AccountRestriction[];
- async pingUntilAvailable(): Promise<void> {
- await pingProc(
- this.sandboxProc,
- `http://localhost:${this.bankConfig.httpPort}`,
- "libeufin-sandbox",
- );
- await pingProc(
- this.nexusProc,
- `${this.nexusBaseUrl}/config`,
- "libeufin-nexus",
- );
- }
+ /**
+ * If set, the harness will not automatically configure the wire fee for this account.
+ */
+ skipWireFeeCreation?: boolean;
}
/**
@@ -784,7 +621,7 @@ export class FakebankService
{
proc: ProcessWrapper | undefined;
- http = new NodeHttpLib();
+ http = createPlatformHttpLib({ enableThrottling: false });
// We store "created" accounts during setup and
// register them after startup.
@@ -793,42 +630,70 @@ export class FakebankService
accountPassword: string;
}[] = [];
+ /**
+ * Create a new fakebank service handle.
+ *
+ * First generates the configuration for the fakebank and
+ * then creates a fakebank handle, but doesn't start the fakebank
+ * service yet.
+ */
static async create(
gc: GlobalTestState,
bc: BankConfig,
): Promise<FakebankService> {
const config = new Configuration();
- setTalerPaths(config, gc.testDir + "/talerhome");
+ const testDir = bc.overrideTestDir ?? gc.testDir;
+ setTalerPaths(config, testDir + "/talerhome");
config.setString("taler", "currency", bc.currency);
config.setString("bank", "http_port", `${bc.httpPort}`);
config.setString("bank", "serve", "http");
config.setString("bank", "max_debt_bank", `${bc.currency}:999999`);
config.setString("bank", "max_debt", bc.maxDebt ?? `${bc.currency}:100`);
config.setString("bank", "ram_limit", `${1024}`);
- const cfgFilename = gc.testDir + "/bank.conf";
- config.write(cfgFilename);
+ const cfgFilename = testDir + "/bank.conf";
+ config.write(cfgFilename, { excludeDefaults: true });
return new FakebankService(gc, bc, cfgFilename);
}
+ static fromExistingConfig(
+ gc: GlobalTestState,
+ opts: { overridePath?: string },
+ ): FakebankService {
+ const testDir = opts.overridePath ?? gc.testDir;
+ const cfgFilename = testDir + `/bank.conf`;
+ const config = Configuration.load(cfgFilename);
+ const bc: BankConfig = {
+ allowRegistrations:
+ config.getYesNo("bank", "allow_registrations").orUndefined() ?? true,
+ currency: config.getString("taler", "currency").required(),
+ database: "none",
+ httpPort: config.getNumber("bank", "http_port").required(),
+ maxDebt: config.getString("bank", "max_debt").required(),
+ };
+ return new FakebankService(gc, bc, cfgFilename);
+ }
+
setSuggestedExchange(e: ExchangeServiceInterface, exchangePayto: string) {
if (!!this.proc) {
throw Error("Can't set suggested exchange while bank is running.");
}
const config = Configuration.load(this.configFile);
config.setString("bank", "suggested_exchange", e.baseUrl);
- config.write(this.configFile);
+ config.write(this.configFile, { excludeDefaults: true });
}
get baseUrl(): string {
return `http://localhost:${this.bankConfig.httpPort}/`;
}
- get bankAccessApiBaseUrl(): string {
- let url = new URL("taler-bank-access/", this.baseUrl);
- return url.href;
+ get corebankApiBaseUrl(): string {
+ return this.baseUrl;
}
+ // FIXME: Why do we have this function at all?
+ // We now have a unified corebank API, we should just use that
+ // to create bank accounts, also for the exchange.
async createExchangeAccount(
accountName: string,
password: string,
@@ -840,8 +705,8 @@ export class FakebankService
return {
accountName: accountName,
accountPassword: password,
- accountPaytoUri: getPayto(accountName),
- wireGatewayApiBaseUrl: `http://localhost:${this.bankConfig.httpPort}/taler-wire-gateway/${accountName}/`,
+ accountPaytoUri: generateRandomPayto(accountName),
+ wireGatewayApiBaseUrl: `http://localhost:${this.bankConfig.httpPort}/accounts/${accountName}/taler-wire-gateway/`,
};
}
@@ -866,20 +731,169 @@ export class FakebankService
"bank",
);
await this.pingUntilAvailable();
+ const bankClient = new TalerCorebankApiClient(this.corebankApiBaseUrl);
for (const acc of this.accounts) {
- await BankApi.registerAccount(this, acc.accountName, acc.accountPassword);
+ await bankClient.registerAccount(acc.accountName, acc.accountPassword);
}
}
async pingUntilAvailable(): Promise<void> {
- const url = `http://localhost:${this.bankConfig.httpPort}/taler-bank-integration/config`;
+ const url = `http://localhost:${this.bankConfig.httpPort}/config`;
await pingProc(this.proc, url, "bank");
}
}
+/**
+ * Implementation of the bank service using the libeufin-bank implementation.
+ */
+export class LibeufinBankService
+ extends BankServiceBase
+ implements BankServiceHandle
+{
+ proc: ProcessWrapper | undefined;
+
+ http = createPlatformHttpLib({ enableThrottling: false });
+
+ // We store "created" accounts during setup and
+ // register them after startup.
+ private accounts: {
+ accountName: string;
+ accountPassword: string;
+ }[] = [];
+
+ /**
+ * Create a new fakebank service handle.
+ *
+ * First generates the configuration for the fakebank and
+ * then creates a fakebank handle, but doesn't start the fakebank
+ * service yet.
+ */
+ static async create(
+ gc: GlobalTestState,
+ bc: BankConfig,
+ ): Promise<LibeufinBankService> {
+ const config = new Configuration();
+ const testDir = bc.overrideTestDir ?? gc.testDir;
+ setTalerPaths(config, testDir + "/talerhome");
+ config.setString("libeufin-bankdb-postgres", "config", bc.database);
+ config.setString("libeufin-bank", "currency", bc.currency);
+ config.setString("libeufin-bank", "port", `${bc.httpPort}`);
+ config.setString("libeufin-bank", "serve", "tcp");
+ config.setString(
+ "libeufin-bank",
+ "DEFAULT_DEBT_LIMIT",
+ `${bc.currency}:100`,
+ );
+ config.setString(
+ "libeufin-bank",
+ "registration_bonus",
+ `${bc.currency}:100`,
+ );
+ const cfgFilename = testDir + "/bank.conf";
+ config.write(cfgFilename, { excludeDefaults: true });
+
+ return new LibeufinBankService(gc, bc, cfgFilename);
+ }
+
+ static fromExistingConfig(
+ gc: GlobalTestState,
+ opts: { overridePath?: string },
+ ): FakebankService {
+ const testDir = opts.overridePath ?? gc.testDir;
+ const cfgFilename = testDir + `/bank.conf`;
+ const config = Configuration.load(cfgFilename);
+ const bc: BankConfig = {
+ allowRegistrations:
+ config.getYesNo("libeufin-bank", "allow_registrations").orUndefined() ??
+ true,
+ currency: config.getString("libeufin-bank", "currency").required(),
+ database: config
+ .getString("libeufin-bankdb-postgres", "config")
+ .required(),
+ httpPort: config.getNumber("libeufin-bank", "port").required(),
+ maxDebt: config
+ .getString("libeufin-bank", "DEFAULT_DEBT_LIMIT")
+ .required(),
+ };
+ return new FakebankService(gc, bc, cfgFilename);
+ }
+
+ setSuggestedExchange(e: ExchangeServiceInterface) {
+ if (!!this.proc) {
+ throw Error("Can't set suggested exchange while bank is running.");
+ }
+ const config = Configuration.load(this.configFile);
+ config.setString(
+ "libeufin-bank",
+ "suggested_withdrawal_exchange",
+ e.baseUrl,
+ );
+ config.write(this.configFile, { excludeDefaults: true });
+ }
+
+ get baseUrl(): string {
+ return `http://localhost:${this.bankConfig.httpPort}/`;
+ }
+
+ get corebankApiBaseUrl(): string {
+ return this.baseUrl;
+ }
+
+ get port() {
+ return this.bankConfig.httpPort;
+ }
+
+ async start(): Promise<void> {
+ logger.info("starting libeufin-bank");
+ if (this.proc) {
+ logger.info("libeufin-bank already running, not starting again");
+ return;
+ }
+
+ await sh(
+ this.globalTestState,
+ "libeufin-bank-dbinit",
+ `libeufin-bank dbinit -r -c "${this.configFile}"`,
+ );
+
+ await sh(
+ this.globalTestState,
+ "libeufin-bank-passwd",
+ `libeufin-bank passwd -c "${this.configFile}" admin adminpw`,
+ );
+
+ await sh(
+ this.globalTestState,
+ "libeufin-bank-edit-account",
+ `libeufin-bank edit-account -c "${this.configFile}" admin --debit_threshold=${this.bankConfig.currency}:1000`,
+ );
+
+ this.proc = this.globalTestState.spawnService(
+ "libeufin-bank",
+ ["serve", "-c", this.configFile],
+ "libeufin-bank-httpd",
+ );
+ await this.pingUntilAvailable();
+ const bankClient = new TalerCorebankApiClient(this.corebankApiBaseUrl);
+ for (const acc of this.accounts) {
+ await bankClient.registerAccount(acc.accountName, acc.accountPassword);
+ }
+ }
+
+ async pingUntilAvailable(): Promise<void> {
+ const url = `http://localhost:${this.bankConfig.httpPort}/config`;
+ await pingProc(this.proc, url, "libeufin-bank");
+ }
+}
+
// Use libeufin bank instead of pybank.
const useLibeufinBank = false;
+export interface BankServiceHandle {
+ readonly corebankApiBaseUrl: string;
+ readonly http: HttpRequestLibrary;
+}
+
export type BankService = BankServiceHandle;
export const BankService = FakebankService;
@@ -889,6 +903,8 @@ export interface ExchangeConfig {
roundUnit?: string;
httpPort: number;
database: string;
+ overrideTestDir?: string;
+ overrideWireFee?: string;
}
export interface ExchangeServiceInterface {
@@ -899,8 +915,13 @@ export interface ExchangeServiceInterface {
}
export class ExchangeService implements ExchangeServiceInterface {
- static fromExistingConfig(gc: GlobalTestState, exchangeName: string) {
- const cfgFilename = gc.testDir + `/exchange-${exchangeName}.conf`;
+ static fromExistingConfig(
+ gc: GlobalTestState,
+ exchangeName: string,
+ opts: { overridePath?: string },
+ ) {
+ const testDir = opts.overridePath ?? gc.testDir;
+ const cfgFilename = testDir + `/exchange-${exchangeName}.conf`;
const config = Configuration.load(cfgFilename);
const ec: ExchangeConfig = {
currency: config.getString("taler", "currency").required(),
@@ -909,7 +930,9 @@ export class ExchangeService implements ExchangeServiceInterface {
name: exchangeName,
roundUnit: config.getString("taler", "currency_round_unit").required(),
};
- const privFile = config.getPath("exchange", "master_priv_file").required();
+ const privFile = config
+ .getPath("exchange-offline", "master_priv_file")
+ .required();
const eddsaPriv = fs.readFileSync(privFile);
const keyPair: EddsaKeyPair = {
eddsaPriv,
@@ -918,19 +941,21 @@ export class ExchangeService implements ExchangeServiceInterface {
return new ExchangeService(gc, ec, cfgFilename, keyPair);
}
- private currentTimetravel: Duration | undefined;
+ private currentTimetravelOffsetMs: number | undefined;
- setTimetravel(t: Duration | undefined): void {
+ private exchangeBankAccounts: HarnessExchangeBankAccount[] = [];
+
+ setTimetravel(tMs: number | undefined): void {
if (this.isRunning()) {
throw Error("can't set time travel while the exchange is running");
}
- this.currentTimetravel = t;
+ this.currentTimetravelOffsetMs = tMs;
}
private get timetravelArg(): string | undefined {
- if (this.currentTimetravel && this.currentTimetravel.d_ms !== "forever") {
+ if (this.currentTimetravelOffsetMs != null) {
// Convert to microseconds
- return `--timetravel=+${this.currentTimetravel.d_ms * 1000}`;
+ return `--timetravel=+${this.currentTimetravelOffsetMs * 1000}`;
}
return undefined;
}
@@ -950,7 +975,7 @@ export class ExchangeService implements ExchangeServiceInterface {
async runWirewatchOnce() {
if (useLibeufinBank) {
- // Not even 2 secods showed to be enough!
+ // Not even 2 seconds showed to be enough!
await waitMs(4000);
}
await runCommand(
@@ -961,25 +986,33 @@ export class ExchangeService implements ExchangeServiceInterface {
);
}
+ async runAggregatorOnceWithTimetravel(opts: {
+ timetravelMicroseconds: number;
+ }) {
+ let timetravelArgArr = [];
+ timetravelArgArr.push(`--timetravel=${opts.timetravelMicroseconds}`);
+ await runCommand(
+ this.globalState,
+ `exchange-${this.name}-aggregator-once`,
+ "taler-exchange-aggregator",
+ [...timetravelArgArr, "-c", this.configFilename, "-t", "-y", "-LINFO"],
+ );
+ }
+
async runAggregatorOnce() {
- try {
- await runCommand(
- this.globalState,
- `exchange-${this.name}-aggregator-once`,
- "taler-exchange-aggregator",
- [...this.timetravelArgArr, "-c", this.configFilename, "-t", "-y"],
- );
- } catch (e) {
- logger.info(
- "running aggregator with KYC off didn't work, might be old version, running again",
- );
- await runCommand(
- this.globalState,
- `exchange-${this.name}-aggregator-once`,
- "taler-exchange-aggregator",
- [...this.timetravelArgArr, "-c", this.configFilename, "-t"],
- );
- }
+ await runCommand(
+ this.globalState,
+ `exchange-${this.name}-aggregator-once`,
+ "taler-exchange-aggregator",
+ [
+ ...this.timetravelArgArr,
+ "-c",
+ this.configFilename,
+ "-t",
+ "-y",
+ "-LINFO",
+ ],
+ );
}
async runTransferOnce() {
@@ -991,21 +1024,53 @@ export class ExchangeService implements ExchangeServiceInterface {
);
}
+ async runTransferOnceWithTimetravel(opts: {
+ timetravelMicroseconds: number;
+ }) {
+ let timetravelArgArr = [];
+ timetravelArgArr.push(`--timetravel=${opts.timetravelMicroseconds}`);
+ await runCommand(
+ this.globalState,
+ `exchange-${this.name}-transfer-once`,
+ "taler-exchange-transfer",
+ [...timetravelArgArr, "-c", this.configFilename, "-t"],
+ );
+ }
+
+ /**
+ * Run the taler-exchange-expire command once in test mode.
+ */
+ async runExpireOnce() {
+ await runCommand(
+ this.globalState,
+ `exchange-${this.name}-expire-once`,
+ "taler-exchange-expire",
+ [...this.timetravelArgArr, "-c", this.configFilename, "-t"],
+ );
+ }
+
changeConfig(f: (config: Configuration) => void) {
const config = Configuration.load(this.configFilename);
f(config);
- config.write(this.configFilename);
+ config.write(this.configFilename, { excludeDefaults: true });
}
static create(gc: GlobalTestState, e: ExchangeConfig) {
+ const testDir = e.overrideTestDir ?? gc.testDir;
const config = new Configuration();
+ setTalerPaths(config, `${testDir}/talerhome-exchange-${e.name}`, e.name);
config.setString("taler", "currency", e.currency);
+ // Required by the exchange but not really used yet.
+ config.setString("exchange", "aml_threshold", `${e.currency}:1000000`);
config.setString(
"taler",
"currency_round_unit",
e.roundUnit ?? `${e.currency}:0.01`,
);
- setTalerPaths(config, gc.testDir + "/talerhome");
+ // Set to a high value to not break existing test cases where the merchant
+ // would cover all fees.
+ config.setString("exchange", "STEFAN_ABS", `${e.currency}:1`);
+ config.setString("exchange", "STEFAN_LOG", `${e.currency}:1`);
config.setString(
"exchange",
"revocation_dir",
@@ -1018,6 +1083,7 @@ export class ExchangeService implements ExchangeServiceInterface {
"master_priv_file",
"${TALER_DATA_HOME}/exchange/offline-keys/master.priv",
);
+ config.setString("exchange", "base_url", `http://localhost:${e.httpPort}/`);
config.setString("exchange", "serve", "tcp");
config.setString("exchange", "port", `${e.httpPort}`);
@@ -1026,6 +1092,9 @@ export class ExchangeService implements ExchangeServiceInterface {
config.setString("taler-exchange-secmod-eddsa", "lookahead_sign", "20 s");
config.setString("taler-exchange-secmod-rsa", "lookahead_sign", "20 s");
+ // FIXME: Remove once the exchange default config properly ships this.
+ config.setString("exchange", "EXPIRE_IDLE_SLEEP_INTERVAL", "1 s");
+
const exchangeMasterKey = createEddsaKeyPair();
config.setString(
@@ -1040,10 +1109,16 @@ export class ExchangeService implements ExchangeServiceInterface {
fs.mkdirSync(path.dirname(masterPrivFile), { recursive: true });
+ if (fs.existsSync(masterPrivFile)) {
+ throw new Error(
+ "master priv file already exists, can't create new exchange config",
+ );
+ }
+
fs.writeFileSync(masterPrivFile, Buffer.from(exchangeMasterKey.eddsaPriv));
- const cfgFilename = gc.testDir + `/exchange-${e.name}.conf`;
- config.write(cfgFilename);
+ const cfgFilename = testDir + `/exchange-${e.name}.conf`;
+ config.write(cfgFilename, { excludeDefaults: true });
return new ExchangeService(gc, e, cfgFilename, exchangeMasterKey);
}
@@ -1052,13 +1127,13 @@ export class ExchangeService implements ExchangeServiceInterface {
offeredCoins.forEach((cc) =>
setCoin(config, cc(this.exchangeConfig.currency)),
);
- config.write(this.configFilename);
+ config.write(this.configFilename, { excludeDefaults: true });
}
addCoinConfigList(ccs: CoinConfig[]) {
const config = Configuration.load(this.configFilename);
ccs.forEach((cc) => setCoin(config, cc));
- config.write(this.configFilename);
+ config.write(this.configFilename, { excludeDefaults: true });
}
enableAgeRestrictions(maskStr: string) {
@@ -1069,7 +1144,7 @@ export class ExchangeService implements ExchangeServiceInterface {
"age_groups",
maskStr,
);
- config.write(this.configFilename);
+ config.write(this.configFilename, { excludeDefaults: true });
}
get masterPub() {
@@ -1080,10 +1155,24 @@ export class ExchangeService implements ExchangeServiceInterface {
return this.exchangeConfig.httpPort;
}
+ /**
+ * Run a function that modifies the existing exchange configuration.
+ * The modified exchange configuration will then be written to the
+ * file system.
+ */
+ async modifyConfig(
+ f: (config: Configuration) => Promise<void>,
+ ): Promise<void> {
+ const config = Configuration.load(this.configFilename);
+ await f(config);
+ config.write(this.configFilename, { excludeDefaults: true });
+ }
+
async addBankAccount(
localName: string,
exchangeBankAccount: HarnessExchangeBankAccount,
): Promise<void> {
+ this.exchangeBankAccounts.push(exchangeBankAccount);
const config = Configuration.load(this.configFilename);
config.setString(
`exchange-account-${localName}`,
@@ -1117,12 +1206,15 @@ export class ExchangeService implements ExchangeServiceInterface {
"password",
exchangeBankAccount.accountPassword,
);
- config.write(this.configFilename);
+ config.write(this.configFilename, { excludeDefaults: true });
}
exchangeHttpProc: ProcessWrapper | undefined;
exchangeWirewatchProc: ProcessWrapper | undefined;
+ exchangeTransferProc: ProcessWrapper | undefined;
+ exchangeAggregatorProc: ProcessWrapper | undefined;
+
helperCryptoRsaProc: ProcessWrapper | undefined;
helperCryptoEddsaProc: ProcessWrapper | undefined;
helperCryptoCsProc: ProcessWrapper | undefined;
@@ -1146,6 +1238,38 @@ export class ExchangeService implements ExchangeServiceInterface {
return !!this.exchangeWirewatchProc || !!this.exchangeHttpProc;
}
+ /**
+ * Stop the wirewatch service (which runs by default).
+ *
+ * Useful for some tests.
+ */
+ async stopWirewatch(): Promise<void> {
+ const wirewatch = this.exchangeWirewatchProc;
+ if (wirewatch) {
+ wirewatch.proc.kill("SIGTERM");
+ await wirewatch.wait();
+ this.exchangeWirewatchProc = undefined;
+ }
+ }
+
+ async stopAggregator(): Promise<void> {
+ const agg = this.exchangeAggregatorProc;
+ if (agg) {
+ agg.proc.kill("SIGTERM");
+ await agg.wait();
+ this.exchangeAggregatorProc = undefined;
+ }
+ }
+
+ async startWirewatch(): Promise<void> {
+ const wirewatch = this.exchangeWirewatchProc;
+ if (wirewatch) {
+ logger.warn("wirewatch already running");
+ } else {
+ this.internalCreateWirewatchProc();
+ }
+ }
+
async stop(): Promise<void> {
const wirewatch = this.exchangeWirewatchProc;
if (wirewatch) {
@@ -1153,6 +1277,18 @@ export class ExchangeService implements ExchangeServiceInterface {
await wirewatch.wait();
this.exchangeWirewatchProc = undefined;
}
+ const aggregatorProc = this.exchangeAggregatorProc;
+ if (aggregatorProc) {
+ aggregatorProc.proc.kill("SIGTERM");
+ await aggregatorProc.wait();
+ this.exchangeAggregatorProc = undefined;
+ }
+ const transferProc = this.exchangeTransferProc;
+ if (transferProc) {
+ transferProc.proc.kill("SIGTERM");
+ await transferProc.wait();
+ this.exchangeTransferProc = undefined;
+ }
const httpd = this.exchangeHttpProc;
if (httpd) {
httpd.proc.kill("SIGTERM");
@@ -1191,55 +1327,64 @@ export class ExchangeService implements ExchangeServiceInterface {
["-c", this.configFilename, "download", "sign", "upload"],
);
- const accounts: string[] = [];
const accountTargetTypes: Set<string> = new Set();
- const config = Configuration.load(this.configFilename);
- for (const sectionName of config.getSectionNames()) {
- if (sectionName.startsWith("EXCHANGE-ACCOUNT-")) {
- const paytoUri = config.getString(sectionName, "payto_uri").required();
- const p = parsePaytoUri(paytoUri);
- if (!p) {
- throw Error(`invalid payto uri in exchange config: ${paytoUri}`);
- }
- accountTargetTypes.add(p?.targetType);
- accounts.push(paytoUri);
+ for (const acct of this.exchangeBankAccounts) {
+ const paytoUri = acct.accountPaytoUri;
+ const p = parsePaytoUri(paytoUri);
+ if (!p) {
+ throw Error(`invalid payto uri in exchange config: ${paytoUri}`);
+ }
+ const optArgs: string[] = [];
+ if (acct.conversionUrl != null) {
+ optArgs.push("conversion-url", acct.conversionUrl);
}
- }
-
- logger.info("configuring bank accounts", accounts);
- for (const acc of accounts) {
await runCommand(
this.globalState,
"exchange-offline",
"taler-exchange-offline",
- ["-c", this.configFilename, "enable-account", acc, "upload"],
+ [
+ "-c",
+ this.configFilename,
+ "enable-account",
+ paytoUri,
+ ...optArgs,
+ "upload",
+ ],
);
- }
- const year = new Date().getFullYear();
- for (const accTargetType of accountTargetTypes.values()) {
- for (let i = year; i < year + 5; i++) {
- await runCommand(
- this.globalState,
- "exchange-offline",
- "taler-exchange-offline",
- [
- "-c",
- this.configFilename,
- "wire-fee",
- // Year
- `${i}`,
- // Wire method
- accTargetType,
- // Wire fee
- `${this.exchangeConfig.currency}:0.01`,
- // Closing fee
- `${this.exchangeConfig.currency}:0.01`,
- "upload",
- ],
- );
+ const accTargetType = p.targetType;
+
+ const covered = accountTargetTypes.has(p.targetType);
+ if (!covered && !acct.skipWireFeeCreation) {
+ const year = new Date().getFullYear();
+
+ for (let i = year; i < year + 5; i++) {
+ const wireFee =
+ this.exchangeConfig.overrideWireFee ??
+ `${this.exchangeConfig.currency}:0.01`;
+ await runCommand(
+ this.globalState,
+ "exchange-offline",
+ "taler-exchange-offline",
+ [
+ "-c",
+ this.configFilename,
+ "wire-fee",
+ // Year
+ `${i}`,
+ // Wire method
+ accTargetType,
+ // Wire fee
+ wireFee,
+ // Closing fee
+ `${this.exchangeConfig.currency}:0.01`,
+ "upload",
+ ],
+ );
+ accountTargetTypes.add(accTargetType);
+ }
}
}
@@ -1309,15 +1454,55 @@ export class ExchangeService implements ExchangeServiceInterface {
);
}
- async start(): Promise<void> {
- if (this.isRunning()) {
- throw Error("exchange is already running");
- }
+ private internalCreateWirewatchProc() {
+ this.exchangeWirewatchProc = this.globalState.spawnService(
+ "taler-exchange-wirewatch",
+ [
+ "-c",
+ this.configFilename,
+ "--longpoll-timeout=5s",
+ ...this.timetravelArgArr,
+ ],
+ `exchange-wirewatch-${this.name}`,
+ );
+ }
+
+ private internalCreateAggregatorProc() {
+ this.exchangeAggregatorProc = this.globalState.spawnService(
+ "taler-exchange-aggregator",
+ ["-c", this.configFilename, ...this.timetravelArgArr],
+ `exchange-aggregator-${this.name}`,
+ );
+ }
+
+ private internalCreateTransferProc() {
+ this.exchangeTransferProc = this.globalState.spawnService(
+ "taler-exchange-transfer",
+ ["-c", this.configFilename, ...this.timetravelArgArr],
+ `exchange-transfer-${this.name}`,
+ );
+ }
+
+ async dbinit() {
await sh(
this.globalState,
"exchange-dbinit",
`taler-exchange-dbinit -c "${this.configFilename}"`,
);
+ }
+
+ async start(
+ opts: { skipDbinit?: boolean; skipKeyup?: boolean } = {},
+ ): Promise<void> {
+ if (this.isRunning()) {
+ throw Error("exchange is already running");
+ }
+
+ const skipDbinit = opts.skipDbinit ?? false;
+
+ if (!skipDbinit) {
+ await this.dbinit();
+ }
this.helperCryptoEddsaProc = this.globalState.spawnService(
"taler-exchange-secmod-eddsa",
@@ -1337,11 +1522,9 @@ export class ExchangeService implements ExchangeServiceInterface {
`exchange-crypto-rsa-${this.name}`,
);
- this.exchangeWirewatchProc = this.globalState.spawnService(
- "taler-exchange-wirewatch",
- ["-c", this.configFilename, ...this.timetravelArgArr],
- `exchange-wirewatch-${this.name}`,
- );
+ this.internalCreateWirewatchProc();
+ this.internalCreateTransferProc();
+ this.internalCreateAggregatorProc();
this.exchangeHttpProc = this.globalState.spawnService(
"taler-exchange-httpd",
@@ -1350,7 +1533,14 @@ export class ExchangeService implements ExchangeServiceInterface {
);
await this.pingUntilAvailable();
- await this.keyup();
+
+ const skipKeyup = opts.skipKeyup ?? false;
+
+ if (!skipKeyup) {
+ await this.keyup();
+ } else {
+ logger.info("skipping keyup");
+ }
}
async pingUntilAvailable(): Promise<void> {
@@ -1366,12 +1556,7 @@ export interface MerchantConfig {
currency: string;
httpPort: number;
database: string;
-}
-
-export interface PrivateOrderStatusQuery {
- instance?: string;
- orderId: string;
- sessionId?: string;
+ overrideTestDir?: string;
}
export interface MerchantServiceInterface {
@@ -1380,187 +1565,21 @@ export interface MerchantServiceInterface {
readonly name: string;
}
-export class MerchantApiClient {
- constructor(
- private baseUrl: string,
- public readonly auth: MerchantAuthConfiguration,
- ) {}
-
- async changeAuth(auth: MerchantAuthConfiguration): Promise<void> {
- const url = new URL("private/auth", this.baseUrl);
- await axios.post(url.href, auth, {
- headers: this.makeAuthHeader(),
- });
- }
-
- async deleteInstance(instanceId: string) {
- const url = new URL(`management/instances/${instanceId}`, this.baseUrl);
- await axios.delete(url.href, {
- headers: this.makeAuthHeader(),
- });
- }
-
- async createInstance(req: MerchantInstanceConfig): Promise<void> {
- const url = new URL("management/instances", this.baseUrl);
- await axios.post(url.href, req, {
- headers: this.makeAuthHeader(),
- });
- }
-
- async getInstances(): Promise<MerchantInstancesResponse> {
- const url = new URL("management/instances", this.baseUrl);
- const resp = await axios.get(url.href, {
- headers: this.makeAuthHeader(),
- });
- return resp.data;
- }
-
- async getInstanceFullDetails(instanceId: string): Promise<any> {
- const url = new URL(`management/instances/${instanceId}`, this.baseUrl);
- try {
- const resp = await axios.get(url.href, {
- headers: this.makeAuthHeader(),
- });
- return resp.data;
- } catch (e) {
- throw e;
- }
- }
-
- makeAuthHeader(): Record<string, string> {
- switch (this.auth.method) {
- case "external":
- return {};
- case "token":
- return {
- Authorization: `Bearer ${this.auth.token}`,
- };
- }
- }
-}
-
/**
- * FIXME: This should be deprecated in favor of MerchantApiClient
+ * Default HTTP client handle for the integration test harness.
*/
-export namespace MerchantPrivateApi {
- export async function createOrder(
- merchantService: MerchantServiceInterface,
- instanceName: string,
- req: PostOrderRequest,
- withAuthorization: WithAuthorization = {},
- ): Promise<PostOrderResponse> {
- const baseUrl = merchantService.makeInstanceBaseUrl(instanceName);
- let url = new URL("private/orders", baseUrl);
- const resp = await axios.post(url.href, req, {
- headers: withAuthorization as Record<string, string>,
- });
- return codecForPostOrderResponse().decode(resp.data);
- }
-
- export async function queryPrivateOrderStatus(
- merchantService: MerchantServiceInterface,
- query: PrivateOrderStatusQuery,
- withAuthorization: WithAuthorization = {},
- ): Promise<MerchantOrderPrivateStatusResponse> {
- const reqUrl = new URL(
- `private/orders/${query.orderId}`,
- merchantService.makeInstanceBaseUrl(query.instance),
- );
- if (query.sessionId) {
- reqUrl.searchParams.set("session_id", query.sessionId);
- }
- const resp = await axios.get(reqUrl.href, {
- headers: withAuthorization as Record<string, string>,
- });
- return codecForMerchantOrderPrivateStatusResponse().decode(resp.data);
- }
-
- export async function giveRefund(
- merchantService: MerchantServiceInterface,
- r: {
- instance: string;
- orderId: string;
- amount: string;
- justification: string;
- },
- ): Promise<{ talerRefundUri: string }> {
- const reqUrl = new URL(
- `private/orders/${r.orderId}/refund`,
- merchantService.makeInstanceBaseUrl(r.instance),
- );
- const resp = await axios.post(reqUrl.href, {
- refund: r.amount,
- reason: r.justification,
- });
- return {
- talerRefundUri: resp.data.taler_refund_uri,
- };
- }
-
- export async function createTippingReserve(
- merchantService: MerchantServiceInterface,
- instance: string,
- req: CreateMerchantTippingReserveRequest,
- ): Promise<CreateMerchantTippingReserveConfirmation> {
- const reqUrl = new URL(
- `private/reserves`,
- merchantService.makeInstanceBaseUrl(instance),
- );
- const resp = await axios.post(reqUrl.href, req);
- // FIXME: validate
- return resp.data;
- }
-
- export async function queryTippingReserves(
- merchantService: MerchantServiceInterface,
- instance: string,
- ): Promise<TippingReserveStatus> {
- const reqUrl = new URL(
- `private/reserves`,
- merchantService.makeInstanceBaseUrl(instance),
- );
- const resp = await axios.get(reqUrl.href);
- // FIXME: validate
- return resp.data;
- }
-
- export async function giveTip(
- merchantService: MerchantServiceInterface,
- instance: string,
- req: TipCreateRequest,
- ): Promise<TipCreateConfirmation> {
- const reqUrl = new URL(
- `private/tips`,
- merchantService.makeInstanceBaseUrl(instance),
- );
- const resp = await axios.post(reqUrl.href, req);
- // FIXME: validate
- return resp.data;
- }
-}
-
-export interface CreateMerchantTippingReserveRequest {
- // Amount that the merchant promises to put into the reserve
- initial_balance: AmountString;
-
- // Exchange the merchant intends to use for tipping
- exchange_url: string;
-
- // Desired wire method, for example "iban" or "x-taler-bank"
- wire_method: string;
-}
-
-export interface CreateMerchantTippingReserveConfirmation {
- // Public key identifying the reserve
- reserve_pub: string;
-
- // Wire account of the exchange where to transfer the funds
- payto_uri: string;
-}
+export const harnessHttpLib = createPlatformHttpLib({
+ enableThrottling: false,
+});
export class MerchantService implements MerchantServiceInterface {
- static fromExistingConfig(gc: GlobalTestState, name: string) {
- const cfgFilename = gc.testDir + `/merchant-${name}.conf`;
+ static fromExistingConfig(
+ gc: GlobalTestState,
+ name: string,
+ opts: { overridePath?: string },
+ ) {
+ const testDir = opts.overridePath ?? gc.testDir;
+ const cfgFilename = testDir + `/merchant-${name}.conf`;
const config = Configuration.load(cfgFilename);
const mc: MerchantConfig = {
currency: config.getString("taler", "currency").required(),
@@ -1579,23 +1598,23 @@ export class MerchantService implements MerchantServiceInterface {
private configFilename: string,
) {}
- private currentTimetravel: Duration | undefined;
+ private currentTimetravelOffsetMs: number | undefined;
private isRunning(): boolean {
return !!this.proc;
}
- setTimetravel(t: Duration | undefined): void {
+ setTimetravel(t: number | undefined): void {
if (this.isRunning()) {
throw Error("can't set time travel while the exchange is running");
}
- this.currentTimetravel = t;
+ this.currentTimetravelOffsetMs = t;
}
private get timetravelArg(): string | undefined {
- if (this.currentTimetravel && this.currentTimetravel.d_ms !== "forever") {
+ if (this.currentTimetravelOffsetMs != null) {
// Convert to microseconds
- return `--timetravel=+${this.currentTimetravel.d_ms * 1000}`;
+ return `--timetravel=+${this.currentTimetravelOffsetMs * 1000}`;
}
return undefined;
}
@@ -1630,8 +1649,24 @@ export class MerchantService implements MerchantServiceInterface {
}
}
- async start(): Promise<void> {
- await exec(`taler-merchant-dbinit -c "${this.configFilename}"`);
+ async dbinit() {
+ await runCommand(
+ this.globalState,
+ "merchant-dbinit",
+ "taler-merchant-dbinit",
+ ["-c", this.configFilename],
+ );
+ }
+
+ /**
+ * Start the merchant,
+ */
+ async start(opts: { skipDbinit?: boolean } = {}): Promise<void> {
+ const skipSetup = opts.skipDbinit ?? false;
+
+ if (!skipSetup) {
+ await this.dbinit();
+ }
this.proc = this.globalState.spawnService(
"taler-merchant-httpd",
@@ -1650,11 +1685,12 @@ export class MerchantService implements MerchantServiceInterface {
gc: GlobalTestState,
mc: MerchantConfig,
): Promise<MerchantService> {
+ const testDir = mc.overrideTestDir ?? gc.testDir;
const config = new Configuration();
config.setString("taler", "currency", mc.currency);
- const cfgFilename = gc.testDir + `/merchant-${mc.name}.conf`;
- setTalerPaths(config, gc.testDir + "/talerhome");
+ const cfgFilename = testDir + `/merchant-${mc.name}.conf`;
+ setTalerPaths(config, testDir + "/talerhome");
config.setString("merchant", "serve", "tcp");
config.setString("merchant", "port", `${mc.httpPort}`);
config.setString(
@@ -1663,7 +1699,9 @@ export class MerchantService implements MerchantServiceInterface {
"${TALER_DATA_HOME}/merchant/merchant.priv",
);
config.setString("merchantdb-postgres", "config", mc.database);
- config.write(cfgFilename);
+ // Do not contact demo.taler.net exchange in tests
+ config.setString("merchant-exchange-kudos", "disabled", "yes");
+ config.write(cfgFilename, { excludeDefaults: true });
return new MerchantService(gc, mc, cfgFilename);
}
@@ -1681,45 +1719,41 @@ export class MerchantService implements MerchantServiceInterface {
this.merchantConfig.currency,
);
config.setString(`merchant-exchange-${e.name}`, "master_key", e.masterPub);
- config.write(this.configFilename);
+ config.write(this.configFilename, { excludeDefaults: true });
}
async addDefaultInstance(): Promise<void> {
- return await this.addInstance({
+ return await this.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [getPayto("merchant-default")],
+ paytoUris: [generateRandomPayto("merchant-default")],
auth: {
method: "external",
},
});
}
- async addInstance(
+ /**
+ * Add an instance together with a wire account.
+ */
+ async addInstanceWithWireAccount(
instanceConfig: PartialMerchantInstanceConfig,
): Promise<void> {
if (!this.proc) {
throw Error("merchant must be running to add instance");
}
- logger.info("adding instance");
+ logger.info(`adding instance '${instanceConfig.id}'`);
const url = `http://localhost:${this.merchantConfig.httpPort}/management/instances`;
const auth = instanceConfig.auth ?? { method: "external" };
const body: MerchantInstanceConfig = {
auth,
- payto_uris: instanceConfig.paytoUris,
id: instanceConfig.id,
name: instanceConfig.name,
address: instanceConfig.address ?? {},
jurisdiction: instanceConfig.jurisdiction ?? {},
- default_max_wire_fee:
- instanceConfig.defaultMaxWireFee ??
- `${this.merchantConfig.currency}:1.0`,
- default_wire_fee_amortization:
- instanceConfig.defaultWireFeeAmortization ?? 3,
- default_max_deposit_fee:
- instanceConfig.defaultMaxDepositFee ??
- `${this.merchantConfig.currency}:1.0`,
+ // FIXME: In some tests, we might want to make this configurable
+ use_stefan: true,
default_wire_transfer_delay:
instanceConfig.defaultWireTransferDelay ??
Duration.toTalerProtocolDuration(
@@ -1731,7 +1765,20 @@ export class MerchantService implements MerchantServiceInterface {
instanceConfig.defaultPayDelay ??
Duration.toTalerProtocolDuration(Duration.getForever()),
};
- await axios.post(url, body);
+ const resp = await harnessHttpLib.fetch(url, { method: "POST", body });
+ await expectSuccessResponseOrThrow(resp);
+
+ const accountCreateUrl = `http://localhost:${this.merchantConfig.httpPort}/instances/${instanceConfig.id}/private/accounts`;
+ for (const paytoUri of instanceConfig.paytoUris) {
+ const accountReq: AccountAddDetails = {
+ payto_uri: paytoUri,
+ };
+ const acctResp = await harnessHttpLib.fetch(accountCreateUrl, {
+ method: "POST",
+ body: accountReq,
+ });
+ await expectSuccessResponseOrThrow(acctResp);
+ }
}
makeInstanceBaseUrl(instanceName?: string): string {
@@ -1748,39 +1795,6 @@ export class MerchantService implements MerchantServiceInterface {
}
}
-export interface MerchantAuthConfiguration {
- method: "external" | "token";
- token?: string;
-}
-
-export interface PartialMerchantInstanceConfig {
- auth?: MerchantAuthConfiguration;
- id: string;
- name: string;
- paytoUris: string[];
- address?: unknown;
- jurisdiction?: unknown;
- defaultMaxWireFee?: string;
- defaultMaxDepositFee?: string;
- defaultWireFeeAmortization?: number;
- defaultWireTransferDelay?: TalerProtocolDuration;
- defaultPayDelay?: TalerProtocolDuration;
-}
-
-export interface MerchantInstanceConfig {
- auth: MerchantAuthConfiguration;
- id: string;
- name: string;
- payto_uris: string[];
- address: unknown;
- jurisdiction: unknown;
- default_max_wire_fee: string;
- default_max_deposit_fee: string;
- default_wire_fee_amortization: number;
- default_wire_transfer_delay: TalerProtocolDuration;
- default_pay_delay: TalerProtocolDuration;
-}
-
type TestStatus = "pass" | "fail" | "skip";
export interface TestRunResult {
@@ -1812,7 +1826,7 @@ export async function runTestWithState(
const handleSignal = (s: string) => {
logger.warn(
- `**** received fatal process event, terminating test ${testName}`,
+ `**** received fatal process event (${s}), terminating test ${testName}`,
);
gc.shutdownSync();
process.exit(1);
@@ -1820,12 +1834,28 @@ export async function runTestWithState(
process.on("SIGINT", handleSignal);
process.on("SIGTERM", handleSignal);
- process.on("unhandledRejection", handleSignal);
- process.on("uncaughtException", handleSignal);
+
+ process.on("unhandledRejection", (reason: unknown, promise: any) => {
+ logger.warn(
+ `**** received unhandled rejection (${reason}), terminating test ${testName}`,
+ );
+ logger.warn(`reason type: ${typeof reason}`);
+ gc.shutdownSync();
+ process.exit(1);
+ });
+ process.on("uncaughtException", (error, origin) => {
+ logger.warn(
+ `**** received uncaught exception (${error}), terminating test ${testName}`,
+ );
+ console.warn("stack", error.stack);
+ gc.shutdownSync();
+ process.exit(1);
+ });
try {
logger.info("running test in directory", gc.testDir);
await Promise.race([testMain(gc), p.promise]);
+ logger.info("completed test in directory", gc.testDir);
status = "pass";
if (linger) {
const rl = readline.createInterface({
@@ -1842,9 +1872,23 @@ export async function runTestWithState(
rl.close();
}
} catch (e) {
- console.error("FATAL: test failed with exception", e);
- if (e instanceof TalerError) {
- console.error(`error detail: ${j2s(e.errorDetail)}`);
+ if (e instanceof CommandError) {
+ console.error("FATAL: test failed for", e.logName);
+ const errorLog = fs.readFileSync(
+ path.join(gc.testDir, `${e.logName}-stderr.log`),
+ );
+ console.error(`${e.message}: "${e.command}"`);
+ console.error(errorLog.toString());
+ console.error(e);
+ } else if (e instanceof TalerError) {
+ console.error(
+ "FATAL: test failed",
+ e.message,
+ `error detail: ${j2s(e.errorDetail)}`,
+ );
+ console.error(e.stack);
+ } else {
+ console.error("FATAL: test failed with exception", e);
}
status = "fail";
} finally {
@@ -1866,6 +1910,189 @@ export interface WalletCliOpts {
cryptoWorkerType?: "sync" | "node-worker-thread";
}
+function tryUnixConnect(socketPath: string): Promise<void> {
+ return new Promise((resolve, reject) => {
+ const client = net.createConnection(socketPath);
+ client.on("error", (e) => {
+ reject(e);
+ });
+ client.on("connect", () => {
+ client.end();
+ resolve();
+ });
+ });
+}
+
+export interface WalletServiceOptions {
+ useInMemoryDb?: boolean;
+ /**
+ * Use a particular DB path instead of the default one in the
+ * test environment.
+ */
+ overrideDbPath?: string;
+ name: string;
+}
+
+/**
+ * A wallet service that listens on a unix domain socket for commands.
+ */
+export class WalletService {
+ walletProc: ProcessWrapper | undefined;
+
+ private internalDbPath: string;
+
+ constructor(
+ private globalState: GlobalTestState,
+ private opts: WalletServiceOptions,
+ ) {
+ if (this.opts.overrideDbPath) {
+ this.internalDbPath = this.opts.overrideDbPath;
+ } else {
+ if (this.opts.useInMemoryDb) {
+ this.internalDbPath = ":memory:";
+ } else {
+ this.internalDbPath = path.join(
+ this.globalState.testDir,
+ `walletdb-${this.opts.name}.sqlite3`,
+ );
+ }
+ }
+ }
+
+ get socketPath() {
+ const unixPath = path.join(
+ this.globalState.testDir,
+ `${this.opts.name}.sock`,
+ );
+ return unixPath;
+ }
+
+ get dbPath() {
+ return this.internalDbPath;
+ }
+
+ async stop(): Promise<void> {
+ if (this.walletProc) {
+ this.walletProc.proc.kill("SIGTERM");
+ await this.walletProc.wait();
+ }
+ }
+
+ async start(): Promise<void> {
+ const unixPath = this.socketPath;
+ this.walletProc = this.globalState.spawnService(
+ "taler-wallet-cli",
+ [
+ "--wallet-db",
+ this.dbPath,
+ "-LTRACE", // FIXME: Make this configurable?
+ "--no-throttle", // FIXME: Optionally do throttling for some tests?
+ "advanced",
+ "serve",
+ "--unix-path",
+ unixPath,
+ "--no-init",
+ ],
+ `wallet-${this.opts.name}`,
+ );
+ logger.info(
+ `hint: connect to wallet using taler-wallet-cli --wallet-connection=${unixPath}`,
+ );
+ }
+
+ async pingUntilAvailable(): Promise<void> {
+ let nextDelay = backoffStart();
+ while (1) {
+ try {
+ await tryUnixConnect(this.socketPath);
+ } catch (e) {
+ logger.info(`wallet connection attempt failed: ${e}`);
+ logger.info(`waiting on wallet for ${nextDelay}ms`);
+ await delayMs(nextDelay);
+ nextDelay = backoffIncrement(nextDelay);
+ continue;
+ }
+ logger.info("connection to wallet-core succeeded");
+ break;
+ }
+ }
+}
+
+export interface WalletClientArgs {
+ name?: string;
+ unixPath: string;
+ onNotification?(n: WalletNotification): void;
+}
+
+export type CancelFn = () => void;
+export type NotificationHandler = (n: WalletNotification) => void;
+
+/**
+ * Convenience wrapper around a (remote) wallet handle.
+ */
+export class WalletClient {
+ remoteWallet: RemoteWallet | undefined = undefined;
+ private waiter: WalletNotificationWaiter = makeNotificationWaiter();
+ notificationHandlers: NotificationHandler[] = [];
+
+ addNotificationListener(f: NotificationHandler): CancelFn {
+ this.notificationHandlers.push(f);
+ return () => {
+ const idx = this.notificationHandlers.indexOf(f);
+ if (idx >= 0) {
+ this.notificationHandlers.splice(idx, 1);
+ }
+ };
+ }
+
+ async call<Op extends keyof WalletOperations>(
+ operation: Op,
+ payload: WalletCoreRequestType<Op>,
+ ): Promise<WalletCoreResponseType<Op>> {
+ if (!this.remoteWallet) {
+ throw Error("wallet not connected");
+ }
+ const client = getClientFromRemoteWallet(this.remoteWallet);
+ return client.call(operation, payload);
+ }
+
+ constructor(private args: WalletClientArgs) {}
+
+ async connect(): Promise<void> {
+ const waiter = this.waiter;
+ const walletClient = this;
+ const w = await createRemoteWallet({
+ name: this.args.name,
+ socketFilename: this.args.unixPath,
+ notificationHandler(n) {
+ if (walletClient.args.onNotification) {
+ walletClient.args.onNotification(n);
+ }
+ waiter.notify(n);
+ for (const h of walletClient.notificationHandlers) {
+ h(n);
+ }
+ },
+ });
+ this.remoteWallet = w;
+
+ this.waiter.waitForNotificationCond;
+ }
+
+ get client() {
+ if (!this.remoteWallet) {
+ throw Error("wallet not connected");
+ }
+ return getClientFromRemoteWallet(this.remoteWallet);
+ }
+
+ waitForNotificationCond<T>(
+ cond: (n: WalletNotification) => T | undefined | false,
+ ): Promise<T> {
+ return this.waiter.waitForNotificationCond(cond);
+ }
+}
+
export class WalletCli {
private currentTimetravel: Duration | undefined;
private _client: WalletCoreApiClient;
@@ -1896,23 +2123,28 @@ export class WalletCli {
const cryptoWorkerArg = cliOpts.cryptoWorkerType
? `--crypto-worker=${cliOpts.cryptoWorkerType}`
: "";
- const resp = await sh(
- self.globalTestState,
- `wallet-${self.name}`,
- `taler-wallet-cli ${
- self.timetravelArg ?? ""
- } ${cryptoWorkerArg} --no-throttle -LTRACE --skip-defaults --wallet-db '${
- self.dbfile
- }' api '${op}' ${shellWrap(JSON.stringify(payload))}`,
- );
+ const logName = `wallet-${self.name}`;
+ const command = `taler-wallet-cli ${
+ self.timetravelArg ?? ""
+ } ${cryptoWorkerArg} --no-throttle -LTRACE --skip-defaults --wallet-db '${
+ self.dbfile
+ }' api '${op}' ${shellWrap(JSON.stringify(payload))}`;
+ const resp = await sh(self.globalTestState, logName, command);
logger.info("--- wallet core response ---");
logger.info(resp);
logger.info("--- end of response ---");
- let ar: any;
+ let ar: CoreApiResponse;
try {
- ar = JSON.parse(resp) as CoreApiResponse;
+ ar = JSON.parse(resp);
} catch (e) {
- throw new Error("wallet CLI did not return a proper JSON response");
+ throw new CommandError(
+ "wallet CLI did not return a proper JSON response",
+ logName,
+ command,
+ [],
+ {},
+ null,
+ );
}
if (ar.type === "error") {
throw TalerError.fromUncheckedDetail(ar.error);
@@ -1942,7 +2174,7 @@ export class WalletCli {
return this._client;
}
- async runUntilDone(args: { maxRetries?: number } = {}): Promise<void> {
+ async runUntilDone(args: {} = {}): Promise<void> {
await runCommand(
this.globalTestState,
`wallet-${this.name}`,
@@ -1955,7 +2187,6 @@ export class WalletCli {
"--wallet-db",
this.dbfile,
"run-until-done",
- ...(args.maxRetries ? ["--max-retries", `${args.maxRetries}`] : []),
],
);
}
@@ -1979,7 +2210,7 @@ export class WalletCli {
}
}
-export function getRandomIban(salt: string | null = null): string {
+export function generateRandomTestIban(salt: string | null = null): string {
function getBban(salt: string | null): string {
if (!salt) return Math.random().toString().substring(2, 6);
let hashed = hash(stringToBytes(salt));
@@ -2011,12 +2242,12 @@ export function getWireMethodForTest(): string {
* Generate a payto address, whose authority depends
* on whether the banking is served by euFin or Pybank.
*/
-export function getPayto(label: string): string {
+export function generateRandomPayto(label: string): string {
if (useLibeufinBank)
- return `payto://iban/SANDBOXX/${getRandomIban(
+ return `payto://iban/SANDBOXX/${generateRandomTestIban(
label,
)}?receiver-name=${label}`;
- return `payto://x-taler-bank/localhost/${label}`;
+ return `payto://x-taler-bank/localhost/${label}?receiver-name=${label}`;
}
function waitMs(ms: number): Promise<void> {
diff --git a/packages/taler-harness/src/harness/helpers.ts b/packages/taler-harness/src/harness/helpers.ts
new file mode 100644
index 000000000..ea9047d0b
--- /dev/null
+++ b/packages/taler-harness/src/harness/helpers.ts
@@ -0,0 +1,711 @@
+/*
+ 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/>
+ */
+
+/**
+ * Helpers to create typical test environments.
+ *
+ * @author Florian Dold <dold@taler.net>
+ */
+
+/**
+ * Imports
+ */
+import {
+ AmountString,
+ ConfirmPayResultType,
+ Duration,
+ Logger,
+ MerchantApiClient,
+ MerchantContractTerms,
+ NotificationType,
+ PartialWalletRunConfig,
+ PreparePayResultType,
+ TalerCorebankApiClient,
+ TransactionMajorState,
+ WalletNotification,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { CoinConfig, defaultCoinConfig } from "./denomStructures.js";
+import {
+ FaultInjectedExchangeService,
+ FaultInjectedMerchantService,
+} from "./faultInjection.js";
+import {
+ BankService,
+ DbInfo,
+ ExchangeService,
+ ExchangeServiceInterface,
+ FakebankService,
+ GlobalTestState,
+ HarnessExchangeBankAccount,
+ MerchantService,
+ MerchantServiceInterface,
+ WalletCli,
+ WalletClient,
+ WalletService,
+ WithAuthorization,
+ generateRandomPayto,
+ setupDb,
+ setupSharedDb,
+} from "./harness.js";
+
+import * as fs from "fs";
+
+const logger = new Logger("helpers.ts");
+
+/**
+ * @deprecated
+ */
+export interface SimpleTestEnvironment {
+ commonDb: DbInfo;
+ bank: BankService;
+ exchange: ExchangeService;
+ exchangeBankAccount: HarnessExchangeBankAccount;
+ merchant: MerchantService;
+ wallet: WalletCli;
+}
+
+/**
+ * Improved version of the simple test environment,
+ * with the daemonized wallet.
+ */
+export interface SimpleTestEnvironmentNg {
+ commonDb: DbInfo;
+ bank: BankService;
+ exchange: ExchangeService;
+ exchangeBankAccount: HarnessExchangeBankAccount;
+ merchant: MerchantService;
+ walletClient: WalletClient;
+ walletService: WalletService;
+}
+
+export interface EnvOptions {
+ /**
+ * If provided, enable age restrictions with the specified age mask string.
+ */
+ ageMaskSpec?: string;
+
+ mixedAgeRestriction?: boolean;
+
+ additionalExchangeConfig?(e: ExchangeService): void;
+ additionalMerchantConfig?(m: MerchantService): void;
+ additionalBankConfig?(b: BankService): void;
+}
+
+export function getSharedTestDir(): string {
+ return `/tmp/taler-harness@${process.env.USER}`;
+}
+
+export async function useSharedTestkudosEnvironment(t: GlobalTestState) {
+ const coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS"));
+
+ const sharedDir = getSharedTestDir();
+
+ fs.mkdirSync(sharedDir, { recursive: true });
+
+ const db = await setupSharedDb(t);
+
+ let bank: FakebankService;
+
+ const prevSetupDone = fs.existsSync(sharedDir + "/setup-done");
+
+ logger.info(`previous setup done: ${prevSetupDone}`);
+
+ // Wallet has longer startup-time and no dependencies,
+ // so we start it rather early.
+ const walletStartProm = createWalletDaemonWithClient(t, { name: "wallet" });
+
+ if (fs.existsSync(sharedDir + "/bank.conf")) {
+ logger.info("reusing existing bank");
+ bank = BankService.fromExistingConfig(t, {
+ overridePath: sharedDir,
+ });
+ } else {
+ logger.info("creating new bank config");
+ bank = await BankService.create(t, {
+ allowRegistrations: true,
+ currency: "TESTKUDOS",
+ database: db.connStr,
+ httpPort: 8082,
+ overrideTestDir: sharedDir,
+ });
+ }
+
+ logger.info("setting up exchange");
+
+ const exchangeName = "testexchange-1";
+ const exchangeConfigFilename = sharedDir + `/exchange-${exchangeName}.conf`;
+
+ logger.info(`exchange config filename: ${exchangeConfigFilename}`);
+
+ let exchange: ExchangeService;
+
+ if (fs.existsSync(exchangeConfigFilename)) {
+ logger.info("reusing existing exchange config");
+ exchange = ExchangeService.fromExistingConfig(t, exchangeName, {
+ overridePath: sharedDir,
+ });
+ } else {
+ logger.info("creating new exchange config");
+ exchange = ExchangeService.create(t, {
+ name: "testexchange-1",
+ currency: "TESTKUDOS",
+ httpPort: 8081,
+ database: db.connStr,
+ overrideTestDir: sharedDir,
+ });
+ }
+
+ logger.info("setting up merchant");
+
+ let merchant: MerchantService;
+ const merchantName = "testmerchant-1";
+ const merchantConfigFilename = sharedDir + `/merchant-${merchantName}}`;
+
+ if (fs.existsSync(merchantConfigFilename)) {
+ merchant = MerchantService.fromExistingConfig(t, merchantName, {
+ overridePath: sharedDir,
+ });
+ } else {
+ merchant = await MerchantService.create(t, {
+ name: "testmerchant-1",
+ currency: "TESTKUDOS",
+ httpPort: 8083,
+ database: db.connStr,
+ overrideTestDir: sharedDir,
+ });
+ }
+
+ logger.info("creating bank account for exchange");
+
+ const exchangeBankAccount = await bank.createExchangeAccount(
+ "myexchange",
+ "x",
+ );
+
+ logger.info("creating exchange bank account");
+ await exchange.addBankAccount("1", exchangeBankAccount);
+
+ bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri);
+
+ exchange.addCoinConfigList(coinConfig);
+
+ merchant.addExchange(exchange);
+
+ logger.info("basic setup done, starting services");
+
+ if (!prevSetupDone) {
+ // Must be done sequentially, due to a concurrency
+ // issue in the *-dbinit tools.
+ await exchange.dbinit();
+ await merchant.dbinit();
+ }
+
+ const bankStart = async () => {
+ await bank.start();
+ await bank.pingUntilAvailable();
+ };
+
+ const exchangeStart = async () => {
+ await exchange.start({
+ skipDbinit: true,
+ skipKeyup: prevSetupDone,
+ });
+ await exchange.pingUntilAvailable();
+ };
+
+ const merchStart = async () => {
+ await merchant.start({
+ skipDbinit: true,
+ });
+ await merchant.pingUntilAvailable();
+
+ if (!prevSetupDone) {
+ await merchant.addInstanceWithWireAccount({
+ id: "default",
+ name: "Default Instance",
+ paytoUris: [generateRandomPayto("merchant-default")],
+ defaultWireTransferDelay: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ minutes: 1 }),
+ ),
+ });
+
+ await merchant.addInstanceWithWireAccount({
+ id: "minst1",
+ name: "minst1",
+ paytoUris: [generateRandomPayto("minst1")],
+ defaultWireTransferDelay: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ minutes: 1 }),
+ ),
+ });
+ }
+ };
+
+ await bankStart();
+
+ const res = await Promise.all([
+ exchangeStart(),
+ merchStart(),
+ undefined,
+ walletStartProm,
+ ]);
+
+ const walletClient = res[3].walletClient;
+ const walletService = res[3].walletService;
+
+ fs.writeFileSync(sharedDir + "/setup-done", "OK");
+
+ logger.info("setup done!");
+
+ return {
+ commonDb: db,
+ exchange,
+ merchant,
+ walletClient,
+ walletService,
+ bank,
+ exchangeBankAccount,
+ };
+}
+
+/**
+ * Run a test case with a simple TESTKUDOS Taler environment, consisting
+ * of one exchange, one bank and one merchant.
+ *
+ * V2 uses a daemonized wallet instead of the CLI wallet.
+ */
+export async function createSimpleTestkudosEnvironmentV2(
+ t: GlobalTestState,
+ coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")),
+ opts: EnvOptions = {},
+): Promise<SimpleTestEnvironmentNg> {
+ const db = await setupDb(t);
+
+ const bank = await BankService.create(t, {
+ allowRegistrations: true,
+ currency: "TESTKUDOS",
+ database: db.connStr,
+ httpPort: 8082,
+ });
+
+ const exchange = ExchangeService.create(t, {
+ name: "testexchange-1",
+ currency: "TESTKUDOS",
+ httpPort: 8081,
+ database: db.connStr,
+ });
+
+ const merchant = await MerchantService.create(t, {
+ name: "testmerchant-1",
+ currency: "TESTKUDOS",
+ httpPort: 8083,
+ database: db.connStr,
+ });
+
+ const exchangeBankAccount = await bank.createExchangeAccount(
+ "myexchange",
+ "x",
+ );
+ await exchange.addBankAccount("1", exchangeBankAccount);
+
+ bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri);
+
+ if (opts.additionalBankConfig) {
+ opts.additionalBankConfig(bank);
+ }
+ await bank.start();
+
+ await bank.pingUntilAvailable();
+
+ const ageMaskSpec = opts.ageMaskSpec;
+
+ if (ageMaskSpec) {
+ exchange.enableAgeRestrictions(ageMaskSpec);
+ // Enable age restriction for all coins.
+ exchange.addCoinConfigList(
+ coinConfig.map((x) => ({
+ ...x,
+ name: `${x.name}-age`,
+ ageRestricted: true,
+ })),
+ );
+ // For mixed age restrictions, we also offer coins without age restrictions
+ if (opts.mixedAgeRestriction) {
+ exchange.addCoinConfigList(
+ coinConfig.map((x) => ({ ...x, ageRestricted: false })),
+ );
+ }
+ } else {
+ exchange.addCoinConfigList(coinConfig);
+ }
+
+ if (opts.additionalExchangeConfig) {
+ opts.additionalExchangeConfig(exchange);
+ }
+ await exchange.start();
+ await exchange.pingUntilAvailable();
+
+ merchant.addExchange(exchange);
+
+ if (opts.additionalMerchantConfig) {
+ opts.additionalMerchantConfig(merchant);
+ }
+ await merchant.start();
+ await merchant.pingUntilAvailable();
+
+ await merchant.addInstanceWithWireAccount({
+ id: "default",
+ name: "Default Instance",
+ paytoUris: [generateRandomPayto("merchant-default")],
+ defaultWireTransferDelay: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ minutes: 1 }),
+ ),
+ });
+
+ await merchant.addInstanceWithWireAccount({
+ id: "minst1",
+ name: "minst1",
+ paytoUris: [generateRandomPayto("minst1")],
+ defaultWireTransferDelay: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ minutes: 1 }),
+ ),
+ });
+
+ const { walletClient, walletService } = await createWalletDaemonWithClient(
+ t,
+ { name: "wallet", persistent: true },
+ );
+
+ console.log("setup done!");
+
+ return {
+ commonDb: db,
+ exchange,
+ merchant,
+ walletClient,
+ walletService,
+ bank,
+ exchangeBankAccount,
+ };
+}
+
+export interface CreateWalletArgs {
+ handleNotification?(wn: WalletNotification): void;
+ name: string;
+ persistent?: boolean;
+ overrideDbPath?: string;
+ config?: PartialWalletRunConfig;
+}
+
+export async function createWalletDaemonWithClient(
+ t: GlobalTestState,
+ args: CreateWalletArgs,
+): Promise<{ walletClient: WalletClient; walletService: WalletService }> {
+ const walletService = new WalletService(t, {
+ name: args.name,
+ useInMemoryDb: !args.persistent,
+ overrideDbPath: args.overrideDbPath,
+ });
+ await walletService.start();
+ await walletService.pingUntilAvailable();
+
+ const observabilityEventFile = t.testDir + `/wallet-${args.name}-notifs.log`;
+
+ const onNotif = (notif: WalletNotification) => {
+ if (observabilityEventFile) {
+ fs.appendFileSync(
+ observabilityEventFile,
+ new Date().toISOString() + " " + JSON.stringify(notif) + "\n",
+ );
+ }
+ if (args.handleNotification) {
+ args.handleNotification(notif);
+ }
+ };
+
+ const walletClient = new WalletClient({
+ name: args.name,
+ unixPath: walletService.socketPath,
+ onNotification: onNotif,
+ });
+ await walletClient.connect();
+ const defaultRunConfig = {
+ testing: {
+ skipDefaults: true,
+ emitObservabilityEvents: !!process.env["TALER_TEST_OBSERVABILITY"],
+ },
+ } satisfies PartialWalletRunConfig;
+ await walletClient.client.call(WalletApiOperation.InitWallet, {
+ config: args.config ?? defaultRunConfig,
+ });
+
+ return { walletClient, walletService };
+}
+
+export interface FaultyMerchantTestEnvironment {
+ commonDb: DbInfo;
+ bank: BankService;
+ exchange: ExchangeService;
+ faultyExchange: FaultInjectedExchangeService;
+ exchangeBankAccount: HarnessExchangeBankAccount;
+ merchant: MerchantService;
+ faultyMerchant: FaultInjectedMerchantService;
+ walletClient: WalletClient;
+}
+
+/**
+ * Run a test case with a simple TESTKUDOS Taler environment, consisting
+ * of one exchange, one bank and one merchant.
+ */
+export async function createFaultInjectedMerchantTestkudosEnvironment(
+ t: GlobalTestState,
+): Promise<FaultyMerchantTestEnvironment> {
+ const db = await setupDb(t);
+
+ const bank = await BankService.create(t, {
+ allowRegistrations: true,
+ currency: "TESTKUDOS",
+ database: db.connStr,
+ httpPort: 8082,
+ });
+
+ const exchange = ExchangeService.create(t, {
+ name: "testexchange-1",
+ currency: "TESTKUDOS",
+ httpPort: 8081,
+ database: db.connStr,
+ });
+
+ const merchant = await MerchantService.create(t, {
+ name: "testmerchant-1",
+ currency: "TESTKUDOS",
+ httpPort: 8083,
+ database: db.connStr,
+ });
+
+ const faultyMerchant = new FaultInjectedMerchantService(t, merchant, 9083);
+ const faultyExchange = new FaultInjectedExchangeService(t, exchange, 9081);
+
+ // Base URL must contain port that the proxy is listening on.
+ await exchange.modifyConfig(async (config) => {
+ config.setString("exchange", "base_url", "http://localhost:9081/");
+ });
+
+ const exchangeBankAccount = await bank.createExchangeAccount(
+ "myexchange",
+ "x",
+ );
+ exchange.addBankAccount("1", exchangeBankAccount);
+
+ bank.setSuggestedExchange(
+ faultyExchange,
+ exchangeBankAccount.accountPaytoUri,
+ );
+
+ await bank.start();
+
+ await bank.pingUntilAvailable();
+
+ exchange.addOfferedCoins(defaultCoinConfig);
+
+ await exchange.start();
+ await exchange.pingUntilAvailable();
+
+ merchant.addExchange(faultyExchange);
+
+ await merchant.start();
+ await merchant.pingUntilAvailable();
+
+ await merchant.addInstanceWithWireAccount({
+ id: "default",
+ name: "Default Instance",
+ paytoUris: [generateRandomPayto("merchant-default")],
+ });
+
+ await merchant.addInstanceWithWireAccount({
+ id: "minst1",
+ name: "minst1",
+ paytoUris: [generateRandomPayto("minst1")],
+ });
+
+ console.log("setup done!");
+
+ const { walletClient } = await createWalletDaemonWithClient(t, {
+ name: "default",
+ });
+
+ return {
+ commonDb: db,
+ exchange,
+ merchant,
+ walletClient,
+ bank,
+ exchangeBankAccount,
+ faultyMerchant,
+ faultyExchange,
+ };
+}
+
+export interface WithdrawViaBankResult {
+ withdrawalFinishedCond: Promise<true>;
+}
+
+/**
+ * Withdraw via a bank with the testing API enabled.
+ * Uses the new notification-based mechanism to wait for the
+ * operation to finish.
+ */
+export async function withdrawViaBankV2(
+ t: GlobalTestState,
+ p: {
+ walletClient: WalletClient;
+ bank: BankService;
+ exchange: ExchangeServiceInterface;
+ amount: AmountString | string;
+ restrictAge?: number;
+ },
+): Promise<WithdrawViaBankResult> {
+ const { walletClient: wallet, bank, exchange, amount } = p;
+
+ const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl);
+
+ const user = await bankClient.createRandomBankUser();
+ const wop = await bankClient.createWithdrawalOperation(user.username, amount);
+
+ // Hand it to the wallet
+
+ await wallet.client.call(WalletApiOperation.GetWithdrawalDetailsForUri, {
+ talerWithdrawUri: wop.taler_withdraw_uri,
+ restrictAge: p.restrictAge,
+ });
+
+ // Withdraw (AKA select)
+
+ const acceptRes = await wallet.client.call(
+ WalletApiOperation.AcceptBankIntegratedWithdrawal,
+ {
+ exchangeBaseUrl: exchange.baseUrl,
+ talerWithdrawUri: wop.taler_withdraw_uri,
+ restrictAge: p.restrictAge,
+ },
+ );
+
+ const withdrawalFinishedCond = wallet.waitForNotificationCond(
+ (x) =>
+ x.type === NotificationType.TransactionStateTransition &&
+ x.newTxState.major === TransactionMajorState.Done &&
+ x.transactionId === acceptRes.transactionId,
+ );
+
+ // Confirm it
+
+ await bankClient.confirmWithdrawalOperation(user.username, {
+ withdrawalOperationId: wop.withdrawal_id,
+ });
+
+ return {
+ withdrawalFinishedCond,
+ };
+}
+
+export async function applyTimeTravelV2(
+ timetravelOffsetMs: number,
+ s: {
+ exchange?: ExchangeService;
+ merchant?: MerchantService;
+ walletClient?: WalletClient;
+ },
+): Promise<void> {
+ if (s.exchange) {
+ await s.exchange.stop();
+ s.exchange.setTimetravel(timetravelOffsetMs);
+ await s.exchange.start();
+ await s.exchange.pingUntilAvailable();
+ }
+
+ if (s.merchant) {
+ await s.merchant.stop();
+ s.merchant.setTimetravel(timetravelOffsetMs);
+ await s.merchant.start();
+ await s.merchant.pingUntilAvailable();
+ }
+
+ if (s.walletClient) {
+ await s.walletClient.call(WalletApiOperation.TestingSetTimetravel, {
+ offsetMs: timetravelOffsetMs,
+ });
+ }
+}
+
+/**
+ * Make a simple payment and check that it succeeded.
+ */
+export async function makeTestPaymentV2(
+ t: GlobalTestState,
+ args: {
+ merchant: MerchantServiceInterface;
+ walletClient: WalletClient;
+ order: Partial<MerchantContractTerms>;
+ instance?: string;
+ },
+ auth: WithAuthorization = {},
+): Promise<void> {
+ // Set up order.
+
+ const { walletClient, merchant, instance } = args;
+
+ const merchantClient = new MerchantApiClient(
+ merchant.makeInstanceBaseUrl(instance),
+ );
+
+ const orderResp = await merchantClient.createOrder({
+ order: args.order,
+ });
+
+ let orderStatus = await merchantClient.queryPrivateOrderStatus({
+ orderId: orderResp.order_id,
+ });
+
+ t.assertTrue(orderStatus.order_status === "unpaid");
+
+ // Make wallet pay for the order
+
+ const preparePayResult = await walletClient.call(
+ WalletApiOperation.PreparePayForUri,
+ {
+ talerPayUri: orderStatus.taler_pay_uri,
+ },
+ );
+
+ t.assertTrue(
+ preparePayResult.status === PreparePayResultType.PaymentPossible,
+ );
+
+ const r2 = await walletClient.call(WalletApiOperation.ConfirmPay, {
+ transactionId: preparePayResult.transactionId,
+ });
+
+ t.assertTrue(r2.type === ConfirmPayResultType.Done);
+
+ // Check if payment was successful.
+
+ orderStatus = await merchantClient.queryPrivateOrderStatus({
+ orderId: orderResp.order_id,
+ instance,
+ });
+
+ t.assertTrue(orderStatus.order_status === "paid");
+}
diff --git a/packages/taler-wallet-cli/src/harness/sync.ts b/packages/taler-harness/src/harness/sync.ts
index a9e8de412..64c9acaef 100644
--- a/packages/taler-wallet-cli/src/harness/sync.ts
+++ b/packages/taler-harness/src/harness/sync.ts
@@ -115,5 +115,5 @@ export class SyncService {
private globalState: GlobalTestState,
private syncConfig: SyncConfig,
private configFilename: string,
- ) { }
+ ) {}
}
diff --git a/packages/taler-harness/src/import-meta-url.js b/packages/taler-harness/src/import-meta-url.js
new file mode 100644
index 000000000..c0e657160
--- /dev/null
+++ b/packages/taler-harness/src/import-meta-url.js
@@ -0,0 +1,2 @@
+// Helper to make 'import.meta.url' available in esbuild-bundled code as well.
+export const import_meta_url = require("url").pathToFileURL(__filename);
diff --git a/packages/taler-harness/src/index.ts b/packages/taler-harness/src/index.ts
new file mode 100644
index 000000000..0f282e123
--- /dev/null
+++ b/packages/taler-harness/src/index.ts
@@ -0,0 +1,1288 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received 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 {
+ AccessToken,
+ AmountString,
+ Amounts,
+ BalancesResponse,
+ Configuration,
+ Duration,
+ HttpStatusCode,
+ Logger,
+ PaytoString,
+ TalerAuthenticationHttpClient,
+ TalerBankConversionHttpClient,
+ TalerCoreBankHttpClient,
+ TalerErrorCode,
+ TalerMerchantInstanceHttpClient,
+ TalerMerchantManagementHttpClient,
+ TransactionsResponse,
+ createAccessToken,
+ decodeCrock,
+ encodeCrock,
+ generateIban,
+ j2s,
+ randomBytes,
+ rsaBlind,
+ setGlobalLogLevelFromString,
+ setPrintHttpRequestAsCurl,
+ stringifyPayTemplateUri,
+} from "@gnu-taler/taler-util";
+import { clk } from "@gnu-taler/taler-util/clk";
+import {
+ HttpResponse,
+ createPlatformHttpLib,
+} from "@gnu-taler/taler-util/http";
+import {
+ CryptoDispatcher,
+ SynchronousCryptoWorkerFactoryPlain,
+ WalletApiOperation,
+} from "@gnu-taler/taler-wallet-core";
+import {
+ downloadExchangeInfo,
+ topupReserveWithBank,
+} from "@gnu-taler/taler-wallet-core/dbless";
+import { deepStrictEqual } from "assert";
+import fs from "fs";
+import os from "os";
+import path from "path";
+import { runBench1 } from "./bench1.js";
+import { runBench2 } from "./bench2.js";
+import { runBench3 } from "./bench3.js";
+import { runEnvFull } from "./env-full.js";
+import { runEnv1 } from "./env1.js";
+import {
+ GlobalTestState,
+ WalletClient,
+ delayMs,
+ runTestWithState,
+} from "./harness/harness.js";
+import {
+ createSimpleTestkudosEnvironmentV2,
+ createWalletDaemonWithClient,
+} from "./harness/helpers.js";
+import { getTestInfo, runTests } from "./integrationtests/testrunner.js";
+import { lintExchangeDeployment } from "./lint.js";
+import { randomUUID } from "crypto";
+
+const logger = new Logger("taler-harness:index.ts");
+
+process.on("unhandledRejection", (error: any) => {
+ logger.error("unhandledRejection", error.message);
+ logger.error("stack", error.stack);
+ process.exit(2);
+});
+
+declare const __VERSION__: string;
+declare const __GIT_HASH__: string;
+function printVersion(): void {
+ console.log(`${__VERSION__} ${__GIT_HASH__}`);
+ process.exit(0);
+}
+
+export const testingCli = clk
+ .program("testing", {
+ help: "Command line interface for the GNU Taler test/deployment harness.",
+ })
+ .maybeOption("log", ["-L", "--log"], clk.STRING, {
+ help: "configure log level (NONE, ..., TRACE)",
+ onPresentHandler: (x) => {
+ setGlobalLogLevelFromString(x);
+ },
+ })
+ .flag("version", ["-v", "--version"], {
+ onPresentHandler: printVersion,
+ })
+ .flag("verbose", ["-V", "--verbose"], {
+ help: "Enable verbose output.",
+ });
+
+const advancedCli = testingCli.subcommand("advancedArgs", "advanced", {
+ help: "Subcommands for advanced operations (only use if you know what you're doing!).",
+});
+
+advancedCli
+ .subcommand("decode", "decode", {
+ help: "Decode base32-crockford.",
+ })
+ .action((args) => {
+ const enc = fs.readFileSync(0, "utf8");
+ console.log(decodeCrock(enc.trim()));
+ });
+
+advancedCli
+ .subcommand("bench1", "bench1", {
+ help: "Run the 'bench1' benchmark",
+ })
+ .requiredOption("configJson", ["--config-json"], clk.STRING)
+ .action(async (args) => {
+ let config: any;
+ try {
+ config = JSON.parse(args.bench1.configJson);
+ } catch (e) {
+ console.log("Could not parse config JSON");
+ }
+ await runBench1(config);
+ });
+
+advancedCli
+ .subcommand("bench2", "bench2", {
+ help: "Run the 'bench2' benchmark",
+ })
+ .requiredOption("configJson", ["--config-json"], clk.STRING)
+ .action(async (args) => {
+ let config: any;
+ try {
+ config = JSON.parse(args.bench2.configJson);
+ } catch (e) {
+ console.log("Could not parse config JSON");
+ }
+ await runBench2(config);
+ });
+
+advancedCli
+ .subcommand("bench3", "bench3", {
+ help: "Run the 'bench3' benchmark",
+ })
+ .requiredOption("configJson", ["--config-json"], clk.STRING)
+ .action(async (args) => {
+ let config: any;
+ try {
+ config = JSON.parse(args.bench3.configJson);
+ } catch (e) {
+ console.log("Could not parse config JSON");
+ }
+ await runBench3(config);
+ });
+
+advancedCli
+ .subcommand("envFull", "env-full", {
+ help: "Run a test environment for bench1",
+ })
+ .action(async (args) => {
+ const testDir = fs.mkdtempSync(path.join(os.tmpdir(), "taler-env-full-"));
+ const testState = new GlobalTestState({
+ testDir,
+ });
+ await runTestWithState(testState, runEnvFull, "env-full", true);
+ });
+
+advancedCli
+ .subcommand("env1", "env1", {
+ help: "Run a test environment for bench1",
+ })
+ .action(async (args) => {
+ const testDir = fs.mkdtempSync(path.join(os.tmpdir(), "taler-env1-"));
+ const testState = new GlobalTestState({
+ testDir,
+ });
+ await runTestWithState(testState, runEnv1, "env1", true);
+ });
+
+async function doDbChecks(
+ t: GlobalTestState,
+ walletClient: WalletClient,
+ indir: string,
+): Promise<void> {
+ // Check that balance didn't break
+ const balPath = `${indir}/wallet-balances.json`;
+ const expectedBal: BalancesResponse = JSON.parse(
+ fs.readFileSync(balPath, { encoding: "utf8" }),
+ ) as BalancesResponse;
+ const actualBal = await walletClient.call(WalletApiOperation.GetBalances, {});
+ t.assertDeepEqual(actualBal.balances.length, expectedBal.balances.length);
+
+ // Check that transactions didn't break
+ const txnPath = `${indir}/wallet-transactions.json`;
+ const expectedTxn: TransactionsResponse = JSON.parse(
+ fs.readFileSync(txnPath, { encoding: "utf8" }),
+ ) as TransactionsResponse;
+ const actualTxn = await walletClient.call(
+ WalletApiOperation.GetTransactions,
+ { includeRefreshes: true },
+ );
+ t.assertDeepEqual(
+ actualTxn.transactions.length,
+ expectedTxn.transactions.length,
+ );
+}
+
+advancedCli
+ .subcommand("walletDbcheck", "wallet-dbcheck", {
+ help: "Check a wallet database (used for migration testing).",
+ })
+ .requiredArgument("indir", clk.STRING)
+ .action(async (args) => {
+ const indir = args.walletDbcheck.indir;
+ if (!fs.existsSync(indir)) {
+ throw Error("directory to be checked does not exist");
+ }
+
+ const testRootDir = fs.mkdtempSync(path.join(os.tmpdir(), "taler-dbchk-"));
+ const t: GlobalTestState = new GlobalTestState({
+ testDir: testRootDir,
+ });
+ const origWalletDbPath = `${indir}/wallet-db.sqlite3`;
+ const testWalletDbPath = `${testRootDir}/wallet-testdb.sqlite3`;
+ fs.cpSync(origWalletDbPath, testWalletDbPath);
+ if (!fs.existsSync(origWalletDbPath)) {
+ throw new Error("wallet db to be checked does not exist");
+ }
+ const { walletClient, walletService } = await createWalletDaemonWithClient(
+ t,
+ { name: "wallet-loaded", overrideDbPath: testWalletDbPath },
+ );
+
+ await walletService.pingUntilAvailable();
+
+ // Do DB checks with the DB we loaded.
+ await doDbChecks(t, walletClient, indir);
+
+ const {
+ walletClient: freshWalletClient,
+ walletService: freshWalletService,
+ } = await createWalletDaemonWithClient(t, {
+ name: "wallet-fresh",
+ persistent: false,
+ });
+
+ await freshWalletService.pingUntilAvailable();
+
+ // Check that we can still import the backup JSON.
+
+ const backupPath = `${indir}/wallet-backup.json`;
+ const backupData = JSON.parse(
+ fs.readFileSync(backupPath, { encoding: "utf8" }),
+ );
+ await freshWalletClient.call(WalletApiOperation.ImportDb, {
+ dump: backupData,
+ });
+
+ // Repeat same checks with wallet that we restored from backup
+ // instead of from the DB file.
+ await doDbChecks(t, freshWalletClient, indir);
+
+ await t.shutdown();
+ });
+
+advancedCli
+ .subcommand("walletDbgen", "wallet-dbgen", {
+ help: "Generate a wallet test database (to be used for migration testing).",
+ })
+ .requiredArgument("outdir", clk.STRING)
+ .action(async (args) => {
+ const outdir = args.walletDbgen.outdir;
+ if (fs.existsSync(outdir)) {
+ throw new Error("outdir already exists, please delete first");
+ }
+ fs.mkdirSync(outdir, {
+ recursive: true,
+ });
+
+ const testRootDir = fs.mkdtempSync(path.join(os.tmpdir(), "taler-dbgen-"));
+ console.log(`generating data in ${testRootDir}`);
+ const t = new GlobalTestState({
+ testDir: testRootDir,
+ });
+ 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(),
+ });
+ await walletClient.call(
+ WalletApiOperation.TestingWaitTransactionsFinal,
+ {},
+ );
+
+ const transactionsJson = await walletClient.call(
+ WalletApiOperation.GetTransactions,
+ {
+ includeRefreshes: true,
+ },
+ );
+
+ const balancesJson = await walletClient.call(
+ WalletApiOperation.GetBalances,
+ {},
+ );
+
+ const backupJson = await walletClient.call(WalletApiOperation.ExportDb, {});
+
+ const versionJson = await walletClient.call(
+ WalletApiOperation.GetVersion,
+ {},
+ );
+
+ await walletService.stop();
+
+ await t.shutdown();
+
+ console.log(`generated data in ${testRootDir}`);
+
+ fs.copyFileSync(walletService.dbPath, `${outdir}/wallet-db.sqlite3`);
+ fs.writeFileSync(
+ `${outdir}/wallet-transactions.json`,
+ j2s(transactionsJson),
+ );
+ fs.writeFileSync(`${outdir}/wallet-balances.json`, j2s(balancesJson));
+ fs.writeFileSync(`${outdir}/wallet-backup.json`, j2s(backupJson));
+ fs.writeFileSync(`${outdir}/wallet-version.json`, j2s(versionJson));
+ fs.writeFileSync(
+ `${outdir}/meta.json`,
+ j2s({
+ timestamp: new Date(),
+ }),
+ );
+ });
+
+const configCli = testingCli.subcommand("configArgs", "config", {
+ help: "Subcommands for handling the Taler configuration.",
+});
+
+configCli.subcommand("show", "show").action(async (args) => {
+ const config = Configuration.load();
+ const cfgStr = config.stringify({
+ diagnostics: true,
+ });
+ console.log(cfgStr);
+});
+
+configCli
+ .subcommand("get", "get")
+ .requiredArgument("section", clk.STRING)
+ .requiredArgument("option", clk.STRING)
+ .flag("file", ["-f"])
+ .action(async (args) => {
+ const config = Configuration.load();
+ let res;
+ if (args.get.file) {
+ res = config.getPath(args.get.section, args.get.option);
+ } else {
+ res = config.getString(args.get.section, args.get.option);
+ }
+ if (res.isDefined()) {
+ console.log(res.required());
+ } else {
+ console.warn("not found");
+ process.exit(1);
+ }
+ });
+
+const deploymentCli = testingCli.subcommand("deploymentArgs", "deployment", {
+ help: "Subcommands for handling GNU Taler deployments.",
+});
+
+deploymentCli
+ .subcommand("testTalerdotnetDemo", "test-demodottalerdotnet")
+ .action(async (args) => {
+ const http = createPlatformHttpLib();
+ const cryptiDisp = new CryptoDispatcher(
+ new SynchronousCryptoWorkerFactoryPlain(),
+ );
+ const cryptoApi = cryptiDisp.cryptoApi;
+ const reserveKeyPair = await cryptoApi.createEddsaKeypair({});
+ const exchangeBaseUrl = "https://exchange.demo.taler.net/";
+ const exchangeInfo = await downloadExchangeInfo(exchangeBaseUrl, http);
+ await topupReserveWithBank({
+ amount: "KUDOS:10" as AmountString,
+ corebankApiBaseUrl: "https://bank.demo.taler.net/",
+ exchangeInfo,
+ http,
+ reservePub: reserveKeyPair.pub,
+ });
+ let reserveUrl = new URL(`reserves/${reserveKeyPair.pub}`, exchangeBaseUrl);
+ reserveUrl.searchParams.set("timeout_ms", "30000");
+ console.log("requesting", reserveUrl.href);
+ const longpollReq = http.fetch(reserveUrl.href, {
+ method: "GET",
+ });
+ const reserveStatusResp = await longpollReq;
+ console.log("reserve status", reserveStatusResp.status);
+ });
+
+deploymentCli
+ .subcommand("testDemoTestdotdalerdotnet", "test-testdottalerdotnet")
+ .action(async (args) => {
+ const http = createPlatformHttpLib();
+ const cryptiDisp = new CryptoDispatcher(
+ new SynchronousCryptoWorkerFactoryPlain(),
+ );
+ const cryptoApi = cryptiDisp.cryptoApi;
+ const reserveKeyPair = await cryptoApi.createEddsaKeypair({});
+ const exchangeBaseUrl = "https://exchange.test.taler.net/";
+ const exchangeInfo = await downloadExchangeInfo(exchangeBaseUrl, http);
+ await topupReserveWithBank({
+ amount: "TESTKUDOS:10" as AmountString,
+ corebankApiBaseUrl: "https://bank.test.taler.net/",
+ exchangeInfo,
+ http,
+ reservePub: reserveKeyPair.pub,
+ });
+ let reserveUrl = new URL(`reserves/${reserveKeyPair.pub}`, exchangeBaseUrl);
+ reserveUrl.searchParams.set("timeout_ms", "30000");
+ console.log("requesting", reserveUrl.href);
+ const longpollReq = http.fetch(reserveUrl.href, {
+ method: "GET",
+ });
+ const reserveStatusResp = await longpollReq;
+ console.log("reserve status", reserveStatusResp.status);
+ });
+
+deploymentCli
+ .subcommand("testLocalhostDemo", "test-demo-localhost")
+ .action(async (args) => {
+ // Run checks against the "env-full" demo deployment on localhost
+ const http = createPlatformHttpLib();
+ const cryptiDisp = new CryptoDispatcher(
+ new SynchronousCryptoWorkerFactoryPlain(),
+ );
+ const cryptoApi = cryptiDisp.cryptoApi;
+ const reserveKeyPair = await cryptoApi.createEddsaKeypair({});
+ const exchangeBaseUrl = "http://localhost:8081/";
+ const exchangeInfo = await downloadExchangeInfo(exchangeBaseUrl, http);
+ await topupReserveWithBank({
+ amount: "TESTKUDOS:10" as AmountString,
+ corebankApiBaseUrl: "http://localhost:8082/taler-bank-access/",
+ exchangeInfo,
+ http,
+ reservePub: reserveKeyPair.pub,
+ });
+ let reserveUrl = new URL(`reserves/${reserveKeyPair.pub}`, exchangeBaseUrl);
+ reserveUrl.searchParams.set("timeout_ms", "30000");
+ console.log("requesting", reserveUrl.href);
+ const longpollReq = http.fetch(reserveUrl.href, {
+ method: "GET",
+ });
+ const reserveStatusResp = await longpollReq;
+ console.log("reserve status", reserveStatusResp.status);
+ });
+
+deploymentCli
+ .subcommand("lintExchange", "lint-exchange", {
+ help: "Run checks on the exchange deployment.",
+ })
+ .flag("cont", ["--continue"], {
+ help: "Continue after errors if possible",
+ })
+ .flag("debug", ["--debug"], {
+ help: "Output extra debug info",
+ })
+ .action(async (args) => {
+ await lintExchangeDeployment(
+ args.lintExchange.debug,
+ args.lintExchange.cont,
+ );
+ });
+
+deploymentCli
+ .subcommand("waitService", "wait-taler-service", {
+ help: "Wait for the config endpoint of a Taler-style service to be available",
+ })
+ .requiredArgument("serviceName", clk.STRING)
+ .requiredArgument("serviceConfigUrl", clk.STRING)
+ .action(async (args) => {
+ const serviceName = args.waitService.serviceName;
+ const serviceUrl = args.waitService.serviceConfigUrl;
+ console.log(
+ `Waiting for service ${serviceName} to be ready at ${serviceUrl}`,
+ );
+ const httpLib = createPlatformHttpLib();
+ while (1) {
+ console.log(`Fetching ${serviceUrl}`);
+ let resp: HttpResponse;
+ try {
+ resp = await httpLib.fetch(serviceUrl);
+ } catch (e) {
+ console.log(
+ `Got network error for service ${serviceName} at ${serviceUrl}`,
+ );
+ await delayMs(1000);
+ continue;
+ }
+ if (resp.status != 200) {
+ console.log(
+ `Got unexpected status ${resp.status} for service at ${serviceUrl}`,
+ );
+ await delayMs(1000);
+ continue;
+ }
+ let respJson: any;
+ try {
+ respJson = await resp.json();
+ } catch (e) {
+ console.log(
+ `Got json error for service ${serviceName} at ${serviceUrl}`,
+ );
+ await delayMs(1000);
+ continue;
+ }
+ const recServiceName = respJson.name;
+ console.log(`Got name ${recServiceName}`);
+ if (recServiceName != serviceName) {
+ console.log(`A different service is still running at ${serviceUrl}`);
+ await delayMs(1000);
+ continue;
+ }
+ console.log(`service ${serviceName} at ${serviceUrl} is now available`);
+ return;
+ }
+ });
+
+deploymentCli
+ .subcommand("waitEndpoint", "wait-endpoint", {
+ help: "Wait for an endpoint to return an HTTP 200 Ok status with JSON body",
+ })
+ .requiredArgument("serviceEndpoint", clk.STRING)
+ .action(async (args) => {
+ const serviceUrl = args.waitEndpoint.serviceEndpoint;
+ console.log(`Waiting for endpoint ${serviceUrl} to be ready`);
+ const httpLib = createPlatformHttpLib();
+ while (1) {
+ console.log(`Fetching ${serviceUrl}`);
+ let resp: HttpResponse;
+ try {
+ resp = await httpLib.fetch(serviceUrl);
+ } catch (e) {
+ console.log(`Got network error for service at ${serviceUrl}`);
+ await delayMs(1000);
+ continue;
+ }
+ if (resp.status != 200) {
+ console.log(
+ `Got unexpected status ${resp.status} for service at ${serviceUrl}`,
+ );
+ await delayMs(1000);
+ continue;
+ }
+ let respJson: any;
+ try {
+ respJson = await resp.json();
+ } catch (e) {
+ console.log(`Got json error for service at ${serviceUrl}`);
+ await delayMs(1000);
+ continue;
+ }
+ return;
+ }
+ });
+
+deploymentCli
+ .subcommand("genIban", "gen-iban", {
+ help: "Generate a random IBAN.",
+ })
+ .requiredArgument("countryCode", clk.STRING)
+ .requiredArgument("length", clk.INT)
+ .action(async (args) => {
+ console.log(generateIban(args.genIban.countryCode, args.genIban.length));
+ });
+
+deploymentCli
+ .subcommand("provisionBankMerchant", "provision-bank-and-merchant", {
+ help: "Provision a bank account, merchant instance and link them together.",
+ })
+ .requiredArgument("merchantApiBaseUrl", clk.STRING, {
+ help: "URL location of the merchant backend",
+ })
+ .requiredArgument("corebankApiBaseUrl", clk.STRING, {
+ help: "URL location of the libeufin bank backend",
+ })
+ .requiredOption(
+ "merchantToken",
+ ["--merchant-management-token"],
+ clk.STRING,
+ {
+ help: "access token of the default instance in the merchant backend",
+ },
+ )
+ .maybeOption("bankToken", ["--bank-admin-token"], clk.STRING, {
+ help: "libeufin bank admin's token if the account creation is restricted",
+ })
+ .maybeOption("bankPassword", ["--bank-admin-password"], clk.STRING, {
+ help: "libeufin bank admin's password if the account creation is restricted, it will override --bank-admin-token",
+ })
+ .requiredOption("name", ["--legal-name"], clk.STRING, {
+ help: "legal name of the merchant",
+ })
+ .maybeOption("email", ["--email"], clk.STRING, {
+ help: "email contact of the merchant",
+ })
+ .maybeOption("phone", ["--phone"], clk.STRING, {
+ help: "phone contact of the merchant",
+ })
+ .requiredOption("id", ["--id"], clk.STRING, {
+ help: "login id for the bank account and instance id of the merchant backend",
+ })
+ .flag("template", ["--create-template"], {
+ help: "use this flag to create a default template for the merchant with fixed summary",
+ })
+ .requiredOption("password", ["--password"], clk.STRING, {
+ help: "password of the accounts in libeufin bank and merchant backend",
+ })
+ .flag("randomPassword", ["--set-random-password"], {
+ help: "if everything worked ok, change the password of the accounts at the end",
+ })
+ .action(async (args) => {
+ const managementToken = createAccessToken(
+ args.provisionBankMerchant.merchantToken,
+ );
+ const bankAdminPassword = args.provisionBankMerchant.bankPassword;
+ const bankAdminTokenArg = args.provisionBankMerchant.bankToken
+ ? createAccessToken(args.provisionBankMerchant.bankToken)
+ : undefined;
+ const id = args.provisionBankMerchant.id;
+ const name = args.provisionBankMerchant.name;
+ const email = args.provisionBankMerchant.email;
+ const phone = args.provisionBankMerchant.phone;
+ const password = args.provisionBankMerchant.password;
+
+ const httpLib = createPlatformHttpLib({});
+ const merchantManager = new TalerMerchantManagementHttpClient(
+ args.provisionBankMerchant.merchantApiBaseUrl,
+ httpLib,
+ );
+ const bank = new TalerCoreBankHttpClient(
+ args.provisionBankMerchant.corebankApiBaseUrl,
+ httpLib,
+ );
+ const instanceURL = merchantManager.getSubInstanceAPI(id).href;
+ const merchantInstance = new TalerMerchantInstanceHttpClient(
+ instanceURL,
+ httpLib,
+ );
+ const conv = new TalerBankConversionHttpClient(
+ bank.getConversionInfoAPI().href,
+ httpLib,
+ );
+ const bankAuth = new TalerAuthenticationHttpClient(
+ bank.getAuthenticationAPI(id).href,
+ httpLib,
+ );
+
+ const bc = await bank.getConfig();
+ if (bc.type === "fail") {
+ logger.error(`couldn't get bank config. ${bc.detail.hint}`);
+ return;
+ }
+ if (!bank.isCompatible(bc.body.version)) {
+ logger.error(
+ `bank server version is not compatible: ${bc.body.version}, client version: ${bank.PROTOCOL_VERSION}`,
+ );
+ return;
+ }
+ const mc = await merchantManager.getConfig();
+ if (mc.type === "fail") {
+ logger.error(`couldn't get merchant config. ${mc.detail.hint}`);
+ return;
+ }
+ if (!merchantManager.isCompatible(mc.body.version)) {
+ logger.error(
+ `merchant server version is not compatible: ${mc.body.version}, client version: ${merchantManager.PROTOCOL_VERSION}`,
+ );
+ return;
+ }
+
+ let bankAdminToken: AccessToken | undefined;
+ if (bankAdminPassword) {
+ const adminAuth = new TalerAuthenticationHttpClient(
+ bank.getAuthenticationAPI("admin").href,
+ httpLib,
+ );
+
+ const resp = await adminAuth.createAccessTokenBasic(
+ "admin",
+ bankAdminPassword,
+ {
+ scope: "write",
+ duration: {
+ d_us: 1000 * 1000 * 10, //10 secs
+ },
+ refreshable: false,
+ },
+ );
+ if (resp.type === "fail") {
+ logger.error(`could not get bank admin token from password.`);
+ return;
+ }
+ bankAdminToken = resp.body.access_token;
+ } else {
+ bankAdminToken = bankAdminTokenArg;
+ }
+
+ /**
+ * create bank account
+ */
+ let accountPayto: PaytoString;
+ {
+ const resp = await bank.createAccount(bankAdminToken, {
+ name: name,
+ password: password,
+ username: id,
+ contact_data:
+ email || phone
+ ? {
+ email: email,
+ phone: phone,
+ }
+ : undefined,
+ });
+
+ if (resp.type === "fail") {
+ logger.error(
+ `unable to provision bank account, HTTP response status ${resp.case}`,
+ );
+ process.exit(2);
+ }
+ logger.info(`account ${id} successfully provisioned`);
+ accountPayto = resp.body.internal_payto_uri;
+ }
+
+ /**
+ * create merchant account
+ */
+ {
+ const resp = await merchantManager.createInstance(managementToken, {
+ address: {},
+ auth: {
+ method: "token",
+ token: createAccessToken(password),
+ },
+ default_pay_delay: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ hours: 1 }),
+ ),
+ default_wire_transfer_delay: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ hours: 1 }),
+ ),
+ id: id,
+ jurisdiction: {},
+ name: name,
+ use_stefan: true,
+ });
+
+ if (resp.type === "ok") {
+ logger.info(`instance ${id} created successfully`);
+ } else if (resp.case === HttpStatusCode.Conflict) {
+ logger.info(`instance ${id} already exists`);
+ } else {
+ logger.error(
+ `unable to create instance ${id}, HTTP status ${resp.case}`,
+ );
+ process.exit(2);
+ }
+ }
+
+ let wireAccount: string;
+ /**
+ * link bank account and merchant
+ */
+ {
+ const resp = await merchantInstance.addBankAccount(
+ createAccessToken(password),
+ {
+ payto_uri: accountPayto,
+ credit_facade_url: bank.getRevenueAPI(id).href,
+ credit_facade_credentials: {
+ type: "basic",
+ username: id,
+ password: password,
+ },
+ },
+ );
+ if (resp.type === "fail") {
+ console.error(
+ `unable to configure bank account for instance ${id}, status ${resp.case}`,
+ );
+ console.error(j2s(resp.detail));
+ process.exit(2);
+ }
+ wireAccount = resp.body.h_wire;
+ }
+
+ logger.info(`successfully configured bank account for ${id}`);
+
+ let templateURI;
+ /**
+ * create template
+ */
+ if (args.provisionBankMerchant.template) {
+ let currency = bc.body.currency;
+ if (bc.body.allow_conversion) {
+ const cc = await conv.getConfig();
+ if (cc.type === "ok") {
+ currency = cc.body.fiat_currency;
+ } else {
+ console.error(`could not get fiat currency status ${cc.case}`);
+ console.error(j2s(cc.detail));
+ }
+ } else {
+ console.log(`conversion is disabled, using bank currency`);
+ }
+
+ {
+ const resp = await merchantInstance.addTemplate(
+ createAccessToken(password),
+ {
+ template_id: "default",
+ template_description: "First template",
+ template_contract: {
+ pay_duration: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ hours: 1 }),
+ ),
+ minimum_age: 0,
+ currency,
+ summary: "Pay me!",
+ },
+ },
+ );
+ if (resp.type === "fail") {
+ console.error(
+ `unable to create template for insntaince ${id}, status ${resp.case}`,
+ );
+ console.error(j2s(resp.detail));
+ process.exit(2);
+ }
+ }
+
+ logger.info(`template default successfully created`);
+ templateURI = stringifyPayTemplateUri({
+ merchantBaseUrl: instanceURL,
+ templateId: "default",
+ templateParams: {
+ amount: currency,
+ },
+ });
+ }
+
+ let finalPassword = password;
+ if (args.provisionBankMerchant.randomPassword) {
+ const prevPassword = password;
+ const randomPassword = encodeCrock(randomBytes(16));
+ logger.info("random password: ", randomPassword);
+ let token: AccessToken;
+ {
+ const resp = await bankAuth.createAccessTokenBasic(id, prevPassword, {
+ scope: "readwrite",
+ duration: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ minutes: 1 }),
+ ),
+ refreshable: false,
+ });
+ if (resp.type === "fail") {
+ console.error(
+ `unable to login into bank accountfor user ${id}, status ${resp.case}`,
+ );
+ console.error(j2s(resp.detail));
+ process.exit(2);
+ }
+ token = resp.body.access_token;
+ }
+
+ {
+ const resp = await bank.updatePassword(
+ { username: id, token },
+ {
+ old_password: prevPassword,
+ new_password: randomPassword,
+ },
+ );
+ if (resp.type === "fail") {
+ console.error(
+ `unable to change bank password for user ${id}, status ${resp.case}`,
+ );
+ if (resp.case !== HttpStatusCode.Accepted) {
+ console.error(j2s(resp.detail));
+ } else {
+ console.error("2FA required");
+ }
+ process.exit(2);
+ }
+ }
+
+ {
+ const resp = await merchantInstance.updateCurrentInstanceAuthentication(
+ createAccessToken(prevPassword),
+ {
+ method: "token",
+ token: createAccessToken(randomPassword),
+ },
+ );
+ if (resp.type === "fail") {
+ console.error(
+ `unable to change merchant password for instance ${id}, status ${resp.case}`,
+ );
+ console.error(j2s(resp.detail));
+ process.exit(2);
+ }
+ }
+
+ {
+ const resp = await merchantInstance.updateBankAccount(
+ createAccessToken(randomPassword),
+ wireAccount,
+ {
+ credit_facade_url: bank.getRevenueAPI(id).href,
+ credit_facade_credentials: {
+ type: "basic",
+ username: id,
+ password: randomPassword,
+ },
+ },
+ );
+ if (resp.type != "ok") {
+ console.error(
+ `unable to update bank account for instance ${id}, status ${resp.case}`,
+ );
+ console.error(j2s(resp.detail));
+ process.exit(2);
+ }
+ }
+ finalPassword = randomPassword;
+ }
+ logger.info(`successfully configured bank account for ${id}`);
+
+ /**
+ * show result
+ */
+ console.log(
+ JSON.stringify(
+ {
+ bankUser: id,
+ bankURL: args.provisionBankMerchant.corebankApiBaseUrl,
+ merchantURL: instanceURL,
+ templateURI,
+ password: finalPassword,
+ },
+ undefined,
+ 2,
+ ),
+ );
+ });
+
+deploymentCli
+ .subcommand("provisionMerchantInstance", "provision-merchant-instance", {
+ help: "Provision a merchant backend instance.",
+ })
+ .requiredArgument("merchantApiBaseUrl", clk.STRING)
+ .requiredOption("managementToken", ["--management-token"], clk.STRING)
+ .requiredOption("instanceToken", ["--instance-token"], clk.STRING)
+ .requiredOption("name", ["--name"], clk.STRING)
+ .requiredOption("id", ["--id"], clk.STRING)
+ .requiredOption("payto", ["--payto"], clk.STRING)
+ .maybeOption("bankURL", ["--bankURL"], clk.STRING)
+ .maybeOption("bankUser", ["--bankUser"], clk.STRING)
+ .maybeOption("bankPassword", ["--bankPassword"], clk.STRING)
+ .action(async (args) => {
+ const httpLib = createPlatformHttpLib({});
+ const baseUrl = args.provisionMerchantInstance.merchantApiBaseUrl;
+ const api = new TalerMerchantManagementHttpClient(baseUrl, httpLib);
+ const managementToken = createAccessToken(
+ args.provisionMerchantInstance.managementToken,
+ );
+ const instanceToken = createAccessToken(
+ args.provisionMerchantInstance.instanceToken,
+ );
+ const instanceId = args.provisionMerchantInstance.id;
+ const instancceName = args.provisionMerchantInstance.name;
+ const bankURL = args.provisionMerchantInstance.bankURL;
+ const bankUser = args.provisionMerchantInstance.bankUser;
+ const bankPassword = args.provisionMerchantInstance.bankPassword;
+ const accountPayto = args.provisionMerchantInstance.payto as PaytoString;
+
+ const createResp = await api.createInstance(managementToken, {
+ address: {},
+ auth: {
+ method: "token",
+ token: instanceToken,
+ },
+ default_pay_delay: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ hours: 1 }),
+ ),
+ default_wire_transfer_delay: { d_us: 1 },
+ id: instanceId,
+ jurisdiction: {},
+ name: instancceName,
+ use_stefan: true,
+ });
+
+ if (createResp.type === "ok") {
+ logger.info(`instance ${instanceId} created successfully`);
+ } else if (createResp.case === HttpStatusCode.Conflict) {
+ logger.info(`instance ${instanceId} already exists`);
+ } else {
+ logger.error(
+ `unable to create instance ${instanceId}, HTTP status ${createResp.case}`,
+ );
+ process.exit(2);
+ }
+
+ const createAccountResp = await api.addBankAccount(instanceToken, {
+ 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}`,
+ );
+ console.error(j2s(createAccountResp.detail));
+ process.exit(2);
+ }
+ logger.info(`successfully configured bank account for ${instanceId}`);
+ });
+
+deploymentCli
+ .subcommand("provisionBankAccount", "provision-bank-account", {
+ help: "Provision a corebank account.",
+ })
+ .requiredArgument("corebankApiBaseUrl", clk.STRING)
+ .flag("exchange", ["--exchange"])
+ .flag("public", ["--public"])
+ .requiredOption("login", ["--login"], clk.STRING)
+ .requiredOption("name", ["--name"], clk.STRING)
+ .requiredOption("password", ["--password"], clk.STRING)
+ .maybeOption("internalPayto", ["--payto"], clk.STRING)
+ .action(async (args) => {
+ const httpLib = createPlatformHttpLib();
+ const baseUrl = args.provisionBankAccount.corebankApiBaseUrl;
+ const api = new TalerCoreBankHttpClient(baseUrl, httpLib);
+
+ const accountLogin = args.provisionBankAccount.login;
+ const resp = await api.createAccount(undefined, {
+ name: args.provisionBankAccount.name,
+ password: args.provisionBankAccount.password,
+ username: accountLogin,
+ is_public: !!args.provisionBankAccount.public,
+ is_taler_exchange: !!args.provisionBankAccount.exchange,
+ payto_uri: args.provisionBankAccount.internalPayto as PaytoString,
+ });
+
+ if (resp.type === "ok") {
+ logger.info(`account ${accountLogin} successfully provisioned`);
+ return;
+ }
+ logger.error(
+ `unable to provision bank account, HTTP response status ${resp.case}`,
+ );
+ process.exit(2);
+ });
+
+deploymentCli
+ .subcommand("coincfg", "gen-coin-config", {
+ help: "Generate a coin/denomination configuration for the exchange.",
+ })
+ .requiredOption("minAmount", ["--min-amount"], clk.STRING, {
+ help: "Smallest denomination",
+ })
+ .requiredOption("maxAmount", ["--max-amount"], clk.STRING, {
+ help: "Largest denomination",
+ })
+ .flag("noFees", ["--no-fees"])
+ .action(async (args) => {
+ let out = "";
+
+ const stamp = Math.floor(new Date().getTime() / 1000);
+
+ const min = Amounts.parseOrThrow(args.coincfg.minAmount);
+ const max = Amounts.parseOrThrow(args.coincfg.maxAmount);
+ if (min.currency != max.currency) {
+ console.error("currency mismatch");
+ process.exit(1);
+ }
+ const currency = min.currency;
+ let x = min;
+ let n = 1;
+
+ out += "# Coin configuration for the exchange.\n";
+ out += '# Should be placed in "/etc/taler/conf.d/exchange-coins.conf".\n';
+ out += "\n";
+
+ while (Amounts.cmp(x, max) < 0) {
+ out += `[COIN-${currency}-n${n}-t${stamp}]\n`;
+ out += `VALUE = ${Amounts.stringify(x)}\n`;
+ out += `DURATION_WITHDRAW = 7 days\n`;
+ out += `DURATION_SPEND = 2 years\n`;
+ out += `DURATION_LEGAL = 6 years\n`;
+ out += `FEE_WITHDRAW = ${currency}:0\n`;
+ if (args.coincfg.noFees) {
+ out += `FEE_DEPOSIT = ${currency}:0\n`;
+ } else {
+ out += `FEE_DEPOSIT = ${Amounts.stringify(min)}\n`;
+ }
+ out += `FEE_REFRESH = ${currency}:0\n`;
+ out += `FEE_REFUND = ${currency}:0\n`;
+ out += `RSA_KEYSIZE = 2048\n`;
+ out += `CIPHER = RSA\n`;
+ out += "\n";
+ x = Amounts.add(x, x).amount;
+ n++;
+ }
+
+ console.log(out);
+ });
+
+const deploymentConfigCli = deploymentCli.subcommand("configArgs", "config", {
+ help: "Subcommands the Taler configuration.",
+});
+
+deploymentConfigCli
+ .subcommand("show", "show")
+ .flag("diagnostics", ["-d", "--diagnostics"])
+ .maybeArgument("cfgfile", clk.STRING, {})
+ .action(async (args) => {
+ const cfg = Configuration.load(args.show.cfgfile);
+ console.log(
+ cfg.stringify({
+ diagnostics: args.show.diagnostics,
+ }),
+ );
+ });
+
+testingCli.subcommand("logtest", "logtest").action(async (args) => {
+ logger.trace("This is a trace message.");
+ logger.info("This is an info message.");
+ logger.warn("This is an warning message.");
+ logger.error("This is an error message.");
+});
+
+testingCli
+ .subcommand("listIntegrationtests", "list-integrationtests")
+ .action(async (args) => {
+ for (const t of getTestInfo()) {
+ let s = t.name;
+ if (t.suites.length > 0) {
+ s += ` (suites: ${t.suites.join(",")})`;
+ }
+ if (t.experimental) {
+ s += ` [experimental]`;
+ }
+ console.log(s);
+ }
+ });
+
+testingCli
+ .subcommand("runIntegrationtests", "run-integrationtests")
+ .maybeArgument("pattern", clk.STRING, {
+ help: "Glob pattern to select which tests to run",
+ })
+ .maybeOption("suites", ["--suites"], clk.STRING, {
+ help: "Only run selected suites (comma-separated list)",
+ })
+ .flag("dryRun", ["--dry"], {
+ help: "Only print tests that will be selected to run.",
+ })
+ .flag("experimental", ["--experimental"], {
+ help: "Include tests marked as experimental",
+ })
+ .flag("failFast", ["--fail-fast"], {
+ help: "Exit after the first error",
+ })
+ .flag("waitOnFail", ["--wait-on-fail"], {
+ help: "Exit after the first error",
+ })
+ .flag("quiet", ["--quiet"], {
+ help: "Produce less output.",
+ })
+ .flag("noTimeout", ["--no-timeout"], {
+ help: "Do not time out tests.",
+ })
+ .action(async (args) => {
+ await runTests({
+ includePattern: args.runIntegrationtests.pattern,
+ failFast: args.runIntegrationtests.failFast,
+ waitOnFail: args.runIntegrationtests.waitOnFail,
+ suiteSpec: args.runIntegrationtests.suites,
+ dryRun: args.runIntegrationtests.dryRun,
+ verbosity: args.runIntegrationtests.quiet ? 0 : 1,
+ includeExperimental: args.runIntegrationtests.experimental ?? false,
+ noTimeout: args.runIntegrationtests.noTimeout,
+ });
+ });
+
+async function read(stream: NodeJS.ReadStream) {
+ const chunks = [];
+ for await (const chunk of stream) chunks.push(chunk);
+ return Buffer.concat(chunks).toString("utf8");
+}
+
+testingCli.subcommand("tvgcheck", "tvgcheck").action(async (args) => {
+ const data = await read(process.stdin);
+
+ const lines = data.match(/[^\r\n]+/g);
+
+ if (!lines) {
+ throw Error("can't split lines");
+ }
+
+ const vals: Record<string, string> = {};
+
+ let inBlindSigningSection = false;
+
+ for (const line of lines) {
+ if (line === "blind signing:") {
+ inBlindSigningSection = true;
+ continue;
+ }
+ if (line[0] !== " ") {
+ inBlindSigningSection = false;
+ continue;
+ }
+ if (inBlindSigningSection) {
+ const m = line.match(/ (\w+) (\w+)/);
+ if (!m) {
+ console.log("bad format");
+ process.exit(2);
+ }
+ vals[m[1]] = m[2];
+ }
+ }
+
+ console.log(vals);
+
+ const req = (k: string) => {
+ if (!vals[k]) {
+ throw Error(`no value for ${k}`);
+ }
+ return decodeCrock(vals[k]);
+ };
+
+ const myBm = rsaBlind(
+ req("message_hash"),
+ req("blinding_key_secret"),
+ req("rsa_public_key"),
+ );
+
+ deepStrictEqual(req("blinded_message"), myBm);
+
+ console.log("check passed!");
+});
+
+export function main() {
+ testingCli.run();
+}
diff --git a/packages/taler-harness/src/integrationtests/test-age-restrictions-deposit.ts b/packages/taler-harness/src/integrationtests/test-age-restrictions-deposit.ts
new file mode 100644
index 000000000..d36ba0e61
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-age-restrictions-deposit.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 {
+ AmountString,
+ NotificationType,
+ TransactionMajorState,
+ TransactionMinorState,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState, generateRandomPayto } from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironmentV2,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
+import { defaultCoinConfig } from "../harness/denomStructures.js";
+
+/**
+ * Run test for basic, bank-integrated withdrawal and payment.
+ */
+export async function runAgeRestrictionsDepositTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { walletClient, bank, exchange } =
+ await createSimpleTestkudosEnvironmentV2(
+ t,
+ defaultCoinConfig.map((x) => x("TESTKUDOS")),
+ {
+ ageMaskSpec: "8:10:12:14:16:18:21",
+ },
+ );
+
+ // Withdraw digital cash into the wallet.
+
+ const withdrawalResult = await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ });
+
+ await withdrawalResult.withdrawalFinishedCond;
+
+ const dgIdResp = await walletClient.client.call(
+ WalletApiOperation.GenerateDepositGroupTxId,
+ {},
+ );
+
+ const depositTxId = dgIdResp.transactionId;
+
+ const depositTrack = walletClient.waitForNotificationCond(
+ (n) =>
+ n.type == NotificationType.TransactionStateTransition &&
+ n.transactionId == depositTxId &&
+ n.newTxState.major == TransactionMajorState.Pending &&
+ n.newTxState.minor == TransactionMinorState.Track,
+ );
+
+ const depositDone = walletClient.waitForNotificationCond(
+ (n) =>
+ n.type == NotificationType.TransactionStateTransition &&
+ n.transactionId == depositTxId &&
+ n.newTxState.major == TransactionMajorState.Done,
+ );
+
+ const depositGroupResult = await walletClient.client.call(
+ WalletApiOperation.CreateDepositGroup,
+ {
+ amount: "TESTKUDOS:10" as AmountString,
+ depositPaytoUri: generateRandomPayto("foo"),
+ transactionId: depositTxId,
+ },
+ );
+
+ t.assertDeepEqual(depositGroupResult.transactionId, depositTxId);
+
+ await depositTrack;
+
+ await exchange.runAggregatorOnceWithTimetravel({
+ timetravelMicroseconds: 1000 * 1000 * 60 * 60 * 3,
+ });
+
+ await depositDone;
+
+ const transactions = await walletClient.client.call(
+ WalletApiOperation.GetTransactions,
+ {},
+ );
+
+ console.log("transactions", JSON.stringify(transactions, undefined, 2));
+ t.assertDeepEqual(transactions.transactions[0].type, "withdrawal");
+ t.assertDeepEqual(transactions.transactions[1].type, "deposit");
+ // The raw amount is what ends up on the bank account, which includes
+ // deposit and wire fees.
+ t.assertDeepEqual(transactions.transactions[1].amountRaw, "TESTKUDOS:9.79");
+}
+
+runAgeRestrictionsDepositTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-age-restrictions-merchant.ts b/packages/taler-harness/src/integrationtests/test-age-restrictions-merchant.ts
new file mode 100644
index 000000000..bba571328
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-age-restrictions-merchant.ts
@@ -0,0 +1,174 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received 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, MerchantApiClient } from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { defaultCoinConfig } from "../harness/denomStructures.js";
+import { GlobalTestState } from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironmentV2,
+ createWalletDaemonWithClient,
+ makeTestPaymentV2,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
+
+/**
+ * Run test for basic, bank-integrated withdrawal and payment.
+ */
+export async function runAgeRestrictionsMerchantTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const {
+ walletClient: walletClientOne,
+ bank,
+ exchange,
+ merchant,
+ exchangeBankAccount,
+ } = await createSimpleTestkudosEnvironmentV2(
+ t,
+ defaultCoinConfig.map((x) => x("TESTKUDOS")),
+ {
+ ageMaskSpec: "8:10:12:14:16:18:21",
+ },
+ );
+
+ const { walletClient: walletClientTwo } = await createWalletDaemonWithClient(
+ t,
+ {
+ name: "w2",
+ },
+ );
+
+ const { walletClient: walletClientThree } =
+ await createWalletDaemonWithClient(t, {
+ name: "w3",
+ });
+
+ {
+ const { walletClient: walletClientZero } =
+ await createWalletDaemonWithClient(t, {
+ name: "w0",
+ });
+
+ const wres = await withdrawViaBankV2(t, {
+ walletClient: walletClientZero,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20" as AmountString,
+ restrictAge: 13,
+ });
+ await wres.withdrawalFinishedCond;
+
+ const order = {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "taler://fulfillment-success/thx",
+ minimum_age: 9,
+ };
+
+ await makeTestPaymentV2(t, {
+ walletClient: walletClientZero,
+ merchant,
+ order,
+ });
+ await walletClientZero.call(
+ WalletApiOperation.TestingWaitTransactionsFinal,
+ {},
+ );
+ }
+
+ {
+ const walletClient = walletClientOne;
+
+ const wres = await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20" as AmountString,
+ restrictAge: 13,
+ });
+ await wres.withdrawalFinishedCond;
+
+ const order = {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "taler://fulfillment-success/thx",
+ minimum_age: 9,
+ };
+
+ await makeTestPaymentV2(t, { walletClient, merchant, order });
+ await walletClient.call(
+ WalletApiOperation.TestingWaitTransactionsFinal,
+ {},
+ );
+ }
+
+ {
+ const walletClient = walletClientTwo;
+
+ const wres = await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20" as AmountString,
+ restrictAge: 13,
+ });
+ await wres.withdrawalFinishedCond;
+
+ const order = {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "taler://fulfillment-success/thx",
+ };
+
+ await makeTestPaymentV2(t, { walletClient, merchant, order });
+ await walletClient.call(
+ WalletApiOperation.TestingWaitTransactionsFinal,
+ {},
+ );
+ }
+
+ {
+ const walletClient = walletClientThree;
+
+ const wres = await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20" as AmountString,
+ });
+ await wres.withdrawalFinishedCond;
+
+ const order = {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "taler://fulfillment-success/thx",
+ minimum_age: 9,
+ };
+
+ await makeTestPaymentV2(t, { walletClient, merchant, order });
+ await walletClient.call(
+ WalletApiOperation.TestingWaitTransactionsFinal,
+ {},
+ );
+ }
+}
+
+runAgeRestrictionsMerchantTest.suites = ["wallet"];
+runAgeRestrictionsMerchantTest.timeoutMs = 120 * 1000;
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-age-restrictions-mixed-merchant.ts b/packages/taler-harness/src/integrationtests/test-age-restrictions-mixed-merchant.ts
index 8bf71b63d..244de1972 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-age-restrictions-mixed-merchant.ts
+++ b/packages/taler-harness/src/integrationtests/test-age-restrictions-mixed-merchant.ts
@@ -17,13 +17,16 @@
/**
* Imports.
*/
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { defaultCoinConfig } from "../harness/denomStructures.js";
-import { GlobalTestState, WalletCli } from "../harness/harness.js";
+import { GlobalTestState } from "../harness/harness.js";
import {
- createSimpleTestkudosEnvironment,
- withdrawViaBank,
- makeTestPayment,
+ createSimpleTestkudosEnvironmentV2,
+ createWalletDaemonWithClient,
+ makeTestPaymentV2,
+ withdrawViaBankV2,
} from "../harness/helpers.js";
+import { AmountString } from "@gnu-taler/taler-util";
/**
* Run test for basic, bank-integrated withdrawal and payment.
@@ -32,11 +35,11 @@ export async function runAgeRestrictionsMixedMerchantTest(t: GlobalTestState) {
// Set up test environment
const {
- wallet: walletOne,
+ walletClient: walletOne,
bank,
exchange,
merchant,
- } = await createSimpleTestkudosEnvironment(
+ } = await createSimpleTestkudosEnvironmentV2(
t,
defaultCoinConfig.map((x) => x("TESTKUDOS")),
{
@@ -45,62 +48,74 @@ export async function runAgeRestrictionsMixedMerchantTest(t: GlobalTestState) {
},
);
- const walletTwo = new WalletCli(t, "walletTwo");
- const walletThree = new WalletCli(t, "walletThree");
+ const { walletClient: walletTwo } = await createWalletDaemonWithClient(t, {
+ name: "w2",
+ });
+
+ const { walletClient: walletThree } = await createWalletDaemonWithClient(t, {
+ name: "w3",
+ });
{
- const wallet = walletOne;
+ const walletClient = walletOne;
- await withdrawViaBank(t, {
- wallet,
+ const wres = await withdrawViaBankV2(t, {
+ walletClient,
bank,
exchange,
- amount: "TESTKUDOS:20",
+ amount: "TESTKUDOS:20" as AmountString,
restrictAge: 13,
});
+ await wres.withdrawalFinishedCond;
+
const order = {
summary: "Buy me!",
- amount: "TESTKUDOS:5",
+ amount: "TESTKUDOS:5" as AmountString,
fulfillment_url: "taler://fulfillment-success/thx",
minimum_age: 9,
};
- await makeTestPayment(t, { wallet, merchant, order });
- await wallet.runUntilDone();
+ await makeTestPaymentV2(t, { walletClient, merchant, order });
+ await walletClient.call(
+ WalletApiOperation.TestingWaitTransactionsFinal,
+ {},
+ );
}
{
- const wallet = walletTwo;
-
- await withdrawViaBank(t, {
- wallet,
+ const wres = await withdrawViaBankV2(t, {
+ walletClient: walletTwo,
bank,
exchange,
- amount: "TESTKUDOS:20",
+ amount: "TESTKUDOS:20" as AmountString,
restrictAge: 13,
});
+
+ await wres.withdrawalFinishedCond;
+
const order = {
summary: "Buy me!",
- amount: "TESTKUDOS:5",
+ amount: "TESTKUDOS:5" as AmountString,
fulfillment_url: "taler://fulfillment-success/thx",
};
- await makeTestPayment(t, { wallet, merchant, order });
- await wallet.runUntilDone();
+ await makeTestPaymentV2(t, { walletClient: walletTwo, merchant, order });
+ await walletTwo.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
}
{
- const wallet = walletThree;
-
- await withdrawViaBank(t, {
- wallet,
+ const wres = await withdrawViaBankV2(t, {
+ walletClient: walletThree,
bank,
exchange,
- amount: "TESTKUDOS:20",
+ amount: "TESTKUDOS:20" as AmountString,
});
+
+ await wres.withdrawalFinishedCond;
+
const order = {
summary: "Buy me!",
amount: "TESTKUDOS:5",
@@ -108,8 +123,8 @@ export async function runAgeRestrictionsMixedMerchantTest(t: GlobalTestState) {
minimum_age: 9,
};
- await makeTestPayment(t, { wallet, merchant, order });
- await wallet.runUntilDone();
+ await makeTestPaymentV2(t, { walletClient: walletThree, merchant, order });
+ await walletThree.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
}
}
diff --git a/packages/taler-harness/src/integrationtests/test-age-restrictions-peer.ts b/packages/taler-harness/src/integrationtests/test-age-restrictions-peer.ts
new file mode 100644
index 000000000..aea59b706
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-age-restrictions-peer.ts
@@ -0,0 +1,135 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ AbsoluteTime,
+ AmountString,
+ Duration,
+ NotificationType,
+ TalerUriAction,
+ TransactionMajorState,
+ TransactionMinorState,
+ TransactionType,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { defaultCoinConfig } from "../harness/denomStructures.js";
+import { GlobalTestState } from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironmentV2,
+ createWalletDaemonWithClient,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
+
+/**
+ * Run test for basic, bank-integrated withdrawal and payment.
+ */
+export async function runAgeRestrictionsPeerTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { bank, exchange } = await createSimpleTestkudosEnvironmentV2(
+ t,
+ defaultCoinConfig.map((x) => x("TESTKUDOS")),
+ {
+ ageMaskSpec: "8:10:12:14:16:18:21",
+ },
+ );
+
+ const w1 = await createWalletDaemonWithClient(t, {
+ name: "w1",
+ persistent: true,
+ });
+ const w2 = await createWalletDaemonWithClient(t, {
+ name: "w2",
+ persistent: true,
+ });
+
+ const wallet1 = w1.walletClient;
+ const wallet2 = w2.walletClient;
+
+ {
+ const withdrawalRes = await withdrawViaBankV2(t, {
+ walletClient: wallet1,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ restrictAge: 13,
+ });
+
+ await withdrawalRes.withdrawalFinishedCond;
+
+ const purse_expiration = AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ days: 2 }),
+ ),
+ );
+
+ const initResp = await wallet1.client.call(
+ WalletApiOperation.InitiatePeerPushDebit,
+ {
+ partialContractTerms: {
+ summary: "Hello, World",
+ amount: "TESTKUDOS:1" as AmountString,
+ purse_expiration,
+ },
+ },
+ );
+
+ const peerPushReadyCond = wallet1.waitForNotificationCond(
+ (x) =>
+ x.type === NotificationType.TransactionStateTransition &&
+ x.newTxState.major === TransactionMajorState.Pending &&
+ x.newTxState.minor === TransactionMinorState.Ready &&
+ x.transactionId === initResp.transactionId,
+ );
+
+ await peerPushReadyCond;
+
+ const txDetails = await wallet1.call(
+ WalletApiOperation.GetTransactionById,
+ {
+ transactionId: initResp.transactionId,
+ },
+ );
+ t.assertDeepEqual(txDetails.type, TransactionType.PeerPushDebit);
+ t.assertTrue(!!txDetails.talerUri);
+
+ const checkResp = await wallet2.call(
+ WalletApiOperation.PreparePeerPushCredit,
+ {
+ talerUri: txDetails.talerUri,
+ },
+ );
+
+ await wallet2.call(WalletApiOperation.ConfirmPeerPushCredit, {
+ transactionId: checkResp.transactionId,
+ });
+
+ const peerPullCreditDoneCond = wallet2.waitForNotificationCond(
+ (x) =>
+ x.type === NotificationType.TransactionStateTransition &&
+ x.newTxState.major === TransactionMajorState.Done &&
+ x.transactionId === checkResp.transactionId,
+ );
+
+ await peerPullCreditDoneCond;
+ }
+}
+
+runAgeRestrictionsPeerTest.suites = ["wallet"];
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-bank-api.ts b/packages/taler-harness/src/integrationtests/test-bank-api.ts
index c7a23d3ce..9c5b06397 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-bank-api.ts
+++ b/packages/taler-harness/src/integrationtests/test-bank-api.ts
@@ -18,21 +18,21 @@
* Imports.
*/
import {
- GlobalTestState,
- WalletCli,
- ExchangeService,
- setupDb,
+ TalerCorebankApiClient,
+ CreditDebitIndicator,
+ WireGatewayApiClient,
+ createEddsaKeyPair,
+ encodeCrock,
+} from "@gnu-taler/taler-util";
+import { defaultCoinConfig } from "../harness/denomStructures.js";
+import {
BankService,
+ ExchangeService,
+ GlobalTestState,
MerchantService,
- getPayto,
+ generateRandomPayto,
+ setupDb,
} from "../harness/harness.js";
-import { createEddsaKeyPair, encodeCrock } from "@gnu-taler/taler-util";
-import { defaultCoinConfig } from "../harness/denomStructures.js";
-import {
- BankApi,
- BankAccessApi,
- CreditDebitIndicator,
-} from "@gnu-taler/taler-wallet-core";
/**
* Run test for basic, bank-integrated withdrawal.
@@ -84,32 +84,34 @@ export async function runBankApiTest(t: GlobalTestState) {
await merchant.start();
await merchant.pingUntilAvailable();
- await merchant.addDefaultInstance();
- await merchant.addInstance({
- id: "minst1",
- name: "minst1",
- paytoUris: [getPayto("minst1")],
- });
- await merchant.addInstance({
+ await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [getPayto("merchant-default")],
+ paytoUris: [generateRandomPayto("merchant-default")],
+ });
+
+ await merchant.addInstanceWithWireAccount({
+ id: "minst1",
+ name: "minst1",
+ paytoUris: [generateRandomPayto("minst1")],
});
console.log("setup done!");
- const bankUser = await BankApi.registerAccount(bank, "user1", "pw1");
+ const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl);
+
+ const bankUser = await bankClient.registerAccount("user1", "pw1");
// Make sure that registering twice results in a 409 Conflict
{
const e = await t.assertThrowsTalerErrorAsync(async () => {
- await BankApi.registerAccount(bank, "user1", "pw2");
+ await bankClient.registerAccount("user1", "pw2");
});
t.assertTrue(e.errorDetail.httpStatusCode === 409);
}
- let balResp = await BankAccessApi.getAccountBalance(bank, bankUser);
+ let balResp = await bankClient.getAccountBalance(bankUser.username);
console.log(balResp);
@@ -121,16 +123,27 @@ export async function runBankApiTest(t: GlobalTestState) {
const res = createEddsaKeyPair();
- await BankApi.adminAddIncoming(bank, {
+ const wireGatewayApiClient = new WireGatewayApiClient(
+ exchangeBankAccount.wireGatewayApiBaseUrl,
+ {
+ auth: {
+ username: exchangeBankAccount.accountName,
+ password: exchangeBankAccount.accountPassword,
+ },
+ },
+ );
+
+ await wireGatewayApiClient.adminAddIncoming({
amount: "TESTKUDOS:115",
debitAccountPayto: bankUser.accountPaytoUri,
- exchangeBankAccount: exchangeBankAccount,
reservePub: encodeCrock(res.eddsaPub),
});
- balResp = await BankAccessApi.getAccountBalance(bank, bankUser);
+ balResp = await bankClient.getAccountBalance(bankUser.username);
t.assertAmountEquals(balResp.balance.amount, "TESTKUDOS:15");
t.assertTrue(
balResp.balance.credit_debit_indicator === CreditDebitIndicator.Debit,
);
}
+
+runBankApiTest.suites = ["fakebank"] \ No newline at end of file
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-claim-loop.ts b/packages/taler-harness/src/integrationtests/test-claim-loop.ts
index a509e3b19..a424e0101 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-claim-loop.ts
+++ b/packages/taler-harness/src/integrationtests/test-claim-loop.ts
@@ -17,10 +17,14 @@
/**
* Imports.
*/
-import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js";
-import { createSimpleTestkudosEnvironment, withdrawViaBank } from "../harness/helpers.js";
-import { URL } from "url";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { URL } from "url";
+import { GlobalTestState } from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironmentV2,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
+import { MerchantApiClient } from "@gnu-taler/taler-util";
/**
* Run test for the merchant's order lifecycle.
@@ -31,17 +35,20 @@ import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
export async function runClaimLoopTest(t: GlobalTestState) {
// Set up test environment
- const {
- wallet,
+ const { walletClient, bank, exchange, merchant } =
+ await createSimpleTestkudosEnvironmentV2(t);
+
+ await withdrawViaBankV2(t, {
+ walletClient,
bank,
exchange,
- merchant,
- } = await createSimpleTestkudosEnvironment(t);
+ amount: "TESTKUDOS:20",
+ });
- await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
+ const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl());
// Set up order.
- const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
+ const orderResp = await merchantClient.createOrder({
order: {
summary: "Buy me!",
amount: "TESTKUDOS:5",
@@ -50,30 +57,26 @@ export async function runClaimLoopTest(t: GlobalTestState) {
});
// Query private order status before claiming it.
- let orderStatusBefore = await MerchantPrivateApi.queryPrivateOrderStatus(
- merchant,
- {
- orderId: orderResp.order_id,
- },
- );
+ let orderStatusBefore = await merchantClient.queryPrivateOrderStatus({
+ orderId: orderResp.order_id,
+ });
t.assertTrue(orderStatusBefore.order_status === "unpaid");
let statusUrlBefore = new URL(orderStatusBefore.order_status_url);
// Make wallet claim the unpaid order.
t.assertTrue(orderStatusBefore.order_status === "unpaid");
const talerPayUri = orderStatusBefore.taler_pay_uri;
- await wallet.client.call(WalletApiOperation.PreparePayForUri, {
+ await walletClient.call(WalletApiOperation.PreparePayForUri, {
talerPayUri,
});
// Query private order status after claiming it.
- let orderStatusAfter = await MerchantPrivateApi.queryPrivateOrderStatus(
- merchant,
- {
- orderId: orderResp.order_id,
- },
- );
+ let orderStatusAfter = await merchantClient.queryPrivateOrderStatus({
+ orderId: orderResp.order_id,
+ });
t.assertTrue(orderStatusAfter.order_status === "claimed");
await t.shutdown();
}
+
+runClaimLoopTest.suites = ["merchant"]; \ No newline at end of file
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-clause-schnorr.ts b/packages/taler-harness/src/integrationtests/test-clause-schnorr.ts
index bf42dc4c6..a5ad382a7 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-clause-schnorr.ts
+++ b/packages/taler-harness/src/integrationtests/test-clause-schnorr.ts
@@ -17,12 +17,13 @@
/**
* Imports.
*/
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js";
import { GlobalTestState } from "../harness/harness.js";
import {
- createSimpleTestkudosEnvironment,
- withdrawViaBank,
- makeTestPayment,
+ createSimpleTestkudosEnvironmentV2,
+ makeTestPaymentV2,
+ withdrawViaBankV2,
} from "../harness/helpers.js";
/**
@@ -53,12 +54,18 @@ export async function runClauseSchnorrTest(t: GlobalTestState) {
name: "rsa_dummy",
});
- const { wallet, bank, exchange, merchant } =
- await createSimpleTestkudosEnvironment(t, coinConfig);
+ const { walletClient, bank, exchange, merchant } =
+ await createSimpleTestkudosEnvironmentV2(t, coinConfig);
// Withdraw digital cash into the wallet.
- await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
+ const wres = await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ });
+ await wres.withdrawalFinishedCond;
const order = {
summary: "Buy me!",
@@ -66,8 +73,8 @@ export async function runClauseSchnorrTest(t: GlobalTestState) {
fulfillment_url: "taler://fulfillment-success/thx",
};
- await makeTestPayment(t, { wallet, merchant, order });
- await wallet.runUntilDone();
+ await makeTestPaymentV2(t, { walletClient, merchant, order });
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
// Test JSON normalization of contract terms: Does the wallet
// agree with the merchant?
@@ -77,8 +84,8 @@ export async function runClauseSchnorrTest(t: GlobalTestState) {
fulfillment_url: "taler://fulfillment-success/thx",
};
- await makeTestPayment(t, { wallet, merchant, order: order2 });
- await wallet.runUntilDone();
+ await makeTestPaymentV2(t, { walletClient, merchant, order: order2 });
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
// Test JSON normalization of contract terms: Does the wallet
// agree with the merchant?
@@ -88,10 +95,9 @@ export async function runClauseSchnorrTest(t: GlobalTestState) {
fulfillment_url: "taler://fulfillment-success/thx",
};
- await makeTestPayment(t, { wallet, merchant, order: order3 });
-
- await wallet.runUntilDone();
+ await makeTestPaymentV2(t, { walletClient, merchant, order: order3 });
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
}
runClauseSchnorrTest.suites = ["experimental-wallet"];
-runClauseSchnorrTest.excludeByDefault = true;
+runClauseSchnorrTest.experimental = true;
diff --git a/packages/taler-harness/src/integrationtests/test-currency-scope.ts b/packages/taler-harness/src/integrationtests/test-currency-scope.ts
new file mode 100644
index 000000000..e07a8f47b
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-currency-scope.ts
@@ -0,0 +1,192 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import { Duration, j2s } from "@gnu-taler/taler-util";
+import { Wallet, WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { defaultCoinConfig } from "../harness/denomStructures.js";
+import {
+ BankService,
+ ExchangeService,
+ GlobalTestState,
+ MerchantService,
+ generateRandomPayto,
+ setupDb,
+} from "../harness/harness.js";
+import {
+ createWalletDaemonWithClient,
+ makeTestPaymentV2,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
+
+/**
+ * Run test for basic, bank-integrated withdrawal and payment.
+ */
+export async function runCurrencyScopeTest(t: GlobalTestState) {
+ // Set up test environment
+ const dbDefault = await setupDb(t);
+
+ const dbExchangeTwo = await setupDb(t, {
+ nameSuffix: "exchange2",
+ });
+
+ const bank = await BankService.create(t, {
+ allowRegistrations: true,
+ currency: "TESTKUDOS",
+ database: dbDefault.connStr,
+ httpPort: 8082,
+ });
+
+ const exchangeOne = ExchangeService.create(t, {
+ name: "testexchange-1",
+ currency: "TESTKUDOS",
+ httpPort: 8081,
+ database: dbDefault.connStr,
+ });
+
+ const exchangeTwo = ExchangeService.create(t, {
+ name: "testexchange-2",
+ currency: "TESTKUDOS",
+ httpPort: 8281,
+ database: dbExchangeTwo.connStr,
+ });
+
+ const merchant = await MerchantService.create(t, {
+ name: "testmerchant-1",
+ currency: "TESTKUDOS",
+ httpPort: 8083,
+ database: dbDefault.connStr,
+ });
+
+ const exchangeOneBankAccount = await bank.createExchangeAccount(
+ "myexchange",
+ "x",
+ );
+ await exchangeOne.addBankAccount("1", exchangeOneBankAccount);
+
+ const exchangeTwoBankAccount = await bank.createExchangeAccount(
+ "myexchange2",
+ "x",
+ );
+ await exchangeTwo.addBankAccount("1", exchangeTwoBankAccount);
+
+ bank.setSuggestedExchange(
+ exchangeOne,
+ exchangeOneBankAccount.accountPaytoUri,
+ );
+
+ await bank.start();
+
+ await bank.pingUntilAvailable();
+
+ // Set up the first exchange
+
+ exchangeOne.addOfferedCoins(defaultCoinConfig);
+ await exchangeOne.start();
+ await exchangeOne.pingUntilAvailable();
+
+ // Set up the second exchange
+
+ exchangeTwo.addOfferedCoins(defaultCoinConfig);
+ await exchangeTwo.start();
+ await exchangeTwo.pingUntilAvailable();
+
+ // Start and configure merchant
+
+ merchant.addExchange(exchangeOne);
+ merchant.addExchange(exchangeTwo);
+
+ await merchant.start();
+ await merchant.pingUntilAvailable();
+
+ await merchant.addInstanceWithWireAccount({
+ id: "default",
+ name: "Default Instance",
+ paytoUris: [generateRandomPayto("merchant-default")],
+ defaultWireTransferDelay: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ minutes: 1 }),
+ ),
+ });
+
+ await merchant.addInstanceWithWireAccount({
+ id: "minst1",
+ name: "minst1",
+ paytoUris: [generateRandomPayto("minst1")],
+ defaultWireTransferDelay: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ minutes: 1 }),
+ ),
+ });
+
+ const { walletClient } = await createWalletDaemonWithClient(t, {
+ name: "wallet",
+ });
+
+ console.log("setup done!");
+
+ // Withdraw digital cash into the wallet.
+
+ const w1 = await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange: exchangeOne,
+ amount: "TESTKUDOS:6",
+ });
+
+ const w2 = await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange: exchangeTwo,
+ amount: "TESTKUDOS:6",
+ });
+
+ await w1.withdrawalFinishedCond;
+ await w2.withdrawalFinishedCond;
+
+ const bal = await walletClient.call(WalletApiOperation.GetBalances, {});
+ console.log(j2s(bal));
+
+ // Separate balances, exchange-scope.
+ t.assertDeepEqual(bal.balances.length, 2);
+
+ await walletClient.call(WalletApiOperation.AddGlobalCurrencyExchange, {
+ currency: "TESTKUDOS",
+ exchangeBaseUrl: exchangeOne.baseUrl,
+ exchangeMasterPub: exchangeOne.masterPub,
+ });
+
+ await walletClient.call(WalletApiOperation.AddGlobalCurrencyExchange, {
+ currency: "TESTKUDOS",
+ exchangeBaseUrl: exchangeTwo.baseUrl,
+ exchangeMasterPub: exchangeTwo.masterPub,
+ });
+
+ const ex = walletClient.call(
+ WalletApiOperation.ListGlobalCurrencyExchanges,
+ {},
+ );
+ console.log("global currency exchanges:");
+ console.log(j2s(ex));
+
+ const bal2 = await walletClient.call(WalletApiOperation.GetBalances, {});
+ console.log(j2s(bal2));
+
+ // Global currencies are merged
+ t.assertDeepEqual(bal2.balances.length, 1);
+}
+
+runCurrencyScopeTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-denom-lost.ts b/packages/taler-harness/src/integrationtests/test-denom-lost.ts
new file mode 100644
index 000000000..307ae352a
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-denom-lost.ts
@@ -0,0 +1,81 @@
+/*
+ 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 { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState } from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironmentV2,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
+
+/**
+ * Run test for refreshe after a payment.
+ */
+export async function runDenomLostTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { walletClient, bank, exchange, merchant } =
+ await createSimpleTestkudosEnvironmentV2(t);
+
+ // Withdraw digital cash into the wallet.
+
+ const wres = await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ });
+
+ await wres.withdrawalFinishedCond;
+
+ const dsBefore = await walletClient.call(
+ WalletApiOperation.TestingGetDenomStats,
+ {
+ exchangeBaseUrl: exchange.baseUrl,
+ },
+ );
+
+ t.assertDeepEqual(dsBefore.numLost, 0);
+ t.assertDeepEqual(dsBefore.numOffered, dsBefore.numKnown);
+
+ await exchange.stop();
+
+ await exchange.purgeSecmodKeys();
+
+ await exchange.start();
+
+ await walletClient.call(WalletApiOperation.UpdateExchangeEntry, {
+ exchangeBaseUrl: exchange.baseUrl,
+ force: true,
+ });
+
+ const dsAfter = await walletClient.call(
+ WalletApiOperation.TestingGetDenomStats,
+ {
+ exchangeBaseUrl: exchange.baseUrl,
+ },
+ );
+
+ // All previous denominations were lost
+ t.assertDeepEqual(dsBefore.numOffered, dsAfter.numLost);
+ // But we have new ones!
+ t.assertTrue(dsAfter.numKnown > dsBefore.numKnown);
+}
+
+runDenomLostTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-denom-unoffered.ts b/packages/taler-harness/src/integrationtests/test-denom-unoffered.ts
new file mode 100644
index 000000000..6a82d586c
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-denom-unoffered.ts
@@ -0,0 +1,163 @@
+/*
+ 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 {
+ MerchantApiClient,
+ PreparePayResultType,
+ TalerErrorCode,
+ TransactionType,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState } from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironmentV2,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
+
+export async function runDenomUnofferedTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { walletClient, bank, exchange, merchant } =
+ await createSimpleTestkudosEnvironmentV2(t);
+
+ // Withdraw digital cash into the wallet.
+
+ const wres = await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ });
+
+ await wres.withdrawalFinishedCond;
+
+ // Make the exchange forget the denomination.
+ // Effectively we completely reset the exchange,
+ // but keep the exchange master public key.
+
+ await merchant.stop();
+
+ await exchange.stop();
+ await exchange.purgeDatabase();
+ await exchange.purgeSecmodKeys();
+ await exchange.start();
+ await exchange.pingUntilAvailable();
+
+ await merchant.start();
+ await merchant.pingUntilAvailable();
+
+ const order = {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "taler://fulfillment-success/thx",
+ };
+
+ const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl());
+
+ const orderResp = await merchantClient.createOrder({
+ order: order,
+ });
+
+ let orderStatus = await merchantClient.queryPrivateOrderStatus({
+ orderId: orderResp.order_id,
+ });
+
+ t.assertTrue(orderStatus.order_status === "unpaid");
+
+ // Make wallet pay for the order
+
+ const preparePayResult = await walletClient.call(
+ WalletApiOperation.PreparePayForUri,
+ {
+ talerPayUri: orderStatus.taler_pay_uri,
+ },
+ );
+
+ t.assertTrue(
+ preparePayResult.status === PreparePayResultType.PaymentPossible,
+ );
+
+ const confirmResp = await walletClient.call(WalletApiOperation.ConfirmPay, {
+ transactionId: preparePayResult.transactionId,
+ });
+
+ const tx = await walletClient.call(WalletApiOperation.GetTransactionById, {
+ transactionId: confirmResp.transactionId,
+ });
+
+ t.assertTrue(tx.error != null);
+ t.assertTrue(
+ tx.error.code === TalerErrorCode.WALLET_PAY_MERCHANT_SERVER_ERROR,
+ );
+
+ const merchantErrorCode = (tx.error as any).requestError.errorResponse.code;
+
+ t.assertDeepEqual(
+ merchantErrorCode,
+ TalerErrorCode.MERCHANT_GENERIC_EXCHANGE_UNEXPECTED_STATUS,
+ );
+
+ const exchangeErrorCode = (tx.error as any).requestError.errorResponse
+ .exchange_ec;
+
+ t.assertDeepEqual(
+ exchangeErrorCode,
+ TalerErrorCode.EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN,
+ );
+
+ // Depending on whether the merchant has seen the new denominations or not,
+ // the error code might be different here.
+ // t.assertDeepEqual(
+ // merchantErrorCode,
+ // TalerErrorCode.MERCHANT_POST_ORDERS_ID_PAY_DENOMINATION_KEY_NOT_FOUND,
+ // );
+
+ // Force updating the exchange entry so that the wallet knows about the new denominations.
+ await walletClient.call(WalletApiOperation.UpdateExchangeEntry, {
+ exchangeBaseUrl: exchange.baseUrl,
+ force: true,
+ });
+
+ await walletClient.call(WalletApiOperation.DeleteTransaction, {
+ transactionId: confirmResp.transactionId,
+ });
+
+ // Now withdrawal should work again.
+ await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ });
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+
+ const txs = await walletClient.call(WalletApiOperation.GetTransactions, {
+ sort: "stable-ascending",
+ includeRefreshes: true,
+ });
+ console.log(JSON.stringify(txs, undefined, 2));
+
+ t.assertDeepEqual(txs.transactions[0].type, TransactionType.Withdrawal);
+ t.assertDeepEqual(txs.transactions[1].type, TransactionType.Refresh);
+ t.assertDeepEqual(txs.transactions[2].type, TransactionType.DenomLoss);
+ t.assertDeepEqual(txs.transactions[3].type, TransactionType.Withdrawal);
+}
+
+runDenomUnofferedTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-deposit.ts b/packages/taler-harness/src/integrationtests/test-deposit.ts
new file mode 100644
index 000000000..74b318226
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-deposit.ts
@@ -0,0 +1,122 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ AmountString,
+ NotificationType,
+ TransactionMajorState,
+ TransactionMinorState,
+ j2s,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState, generateRandomPayto } from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironmentV2,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
+
+/**
+ * Run test for basic, bank-integrated withdrawal and payment.
+ */
+export async function runDepositTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { walletClient, bank, exchange } =
+ await createSimpleTestkudosEnvironmentV2(t);
+
+ // Withdraw digital cash into the wallet.
+
+ const withdrawalResult = await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ });
+
+ await withdrawalResult.withdrawalFinishedCond;
+
+ const dgIdResp = await walletClient.client.call(
+ WalletApiOperation.GenerateDepositGroupTxId,
+ {},
+ );
+
+ const depositTxId = dgIdResp.transactionId;
+
+ const depositTrack = walletClient.waitForNotificationCond(
+ (n) =>
+ n.type == NotificationType.TransactionStateTransition &&
+ n.transactionId == depositTxId &&
+ n.newTxState.major == TransactionMajorState.Pending &&
+ n.newTxState.minor == TransactionMinorState.Track,
+ );
+
+ const depositDone = walletClient.waitForNotificationCond(
+ (n) =>
+ n.type == NotificationType.TransactionStateTransition &&
+ n.transactionId == depositTxId &&
+ n.newTxState.major == TransactionMajorState.Done,
+ );
+
+ const depositGroupResult = await walletClient.client.call(
+ WalletApiOperation.CreateDepositGroup,
+ {
+ amount: "TESTKUDOS:10" as AmountString,
+ depositPaytoUri: generateRandomPayto("foo"),
+ transactionId: depositTxId,
+ },
+ );
+
+ t.assertDeepEqual(depositGroupResult.transactionId, depositTxId);
+
+ const balDuring = await walletClient.call(WalletApiOperation.GetBalances, {});
+ console.log(`balances during deposit: ${j2s(balDuring)}`);
+ t.assertAmountEquals(balDuring.balances[0].pendingOutgoing, "TESTKUDOS:10");
+
+ await depositTrack;
+
+ t.logStep("before-aggregator");
+
+ await exchange.runAggregatorOnceWithTimetravel({
+ timetravelMicroseconds: 1000 * 1000 * 60 * 60 * 3,
+ });
+
+ await exchange.runTransferOnceWithTimetravel({
+ timetravelMicroseconds: 1000 * 1000 * 60 * 60 * 3,
+ });
+
+ await depositDone;
+
+ const transactions = await walletClient.client.call(
+ WalletApiOperation.GetTransactions,
+ {},
+ );
+
+ console.log("transactions", JSON.stringify(transactions, undefined, 2));
+ t.assertDeepEqual(transactions.transactions[0].type, "withdrawal");
+ t.assertDeepEqual(transactions.transactions[1].type, "deposit");
+ // The raw amount is what ends up on the bank account, which includes
+ // deposit and wire fees.
+ t.assertDeepEqual(transactions.transactions[1].amountRaw, "TESTKUDOS:9.79");
+
+ const balAfter = await walletClient.call(WalletApiOperation.GetBalances, {});
+ console.log(`balances after deposit: ${j2s(balAfter)}`);
+ t.assertAmountEquals(balAfter.balances[0].pendingOutgoing, "TESTKUDOS:0");
+}
+
+runDepositTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-exchange-deposit.ts b/packages/taler-harness/src/integrationtests/test-exchange-deposit.ts
new file mode 100644
index 000000000..47a17a1f2
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-exchange-deposit.ts
@@ -0,0 +1,159 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ AmountString,
+ encodeCrock,
+ getRandomBytes,
+ j2s,
+ TalerError,
+} from "@gnu-taler/taler-util";
+import { createPlatformHttpLib } from "@gnu-taler/taler-util/http";
+import {
+ CryptoDispatcher,
+ SynchronousCryptoWorkerFactoryPlain,
+} from "@gnu-taler/taler-wallet-core";
+import {
+ checkReserve,
+ depositCoin,
+ downloadExchangeInfo,
+ findDenomOrThrow,
+ topupReserveWithBank,
+ withdrawCoin,
+} from "@gnu-taler/taler-wallet-core/dbless";
+import { GlobalTestState } from "../harness/harness.js";
+import { createSimpleTestkudosEnvironmentV2 } from "../harness/helpers.js";
+
+/**
+ * Run test for basic, bank-integrated withdrawal and payment.
+ */
+export async function runExchangeDepositTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { bank, exchange } = await createSimpleTestkudosEnvironmentV2(t);
+
+ const http = createPlatformHttpLib({
+ enableThrottling: false,
+ });
+ const cryptiDisp = new CryptoDispatcher(
+ new SynchronousCryptoWorkerFactoryPlain(),
+ );
+ const cryptoApi = cryptiDisp.cryptoApi;
+
+ try {
+ // Withdraw digital cash into the wallet.
+
+ const exchangeInfo = await downloadExchangeInfo(exchange.baseUrl, http);
+
+ const reserveKeyPair = await cryptoApi.createEddsaKeypair({});
+
+ await topupReserveWithBank({
+ http,
+ amount: "TESTKUDOS:10" as AmountString,
+ corebankApiBaseUrl: bank.corebankApiBaseUrl,
+ exchangeInfo,
+ reservePub: reserveKeyPair.pub,
+ });
+
+ await exchange.runWirewatchOnce();
+
+ await checkReserve(http, exchange.baseUrl, reserveKeyPair.pub);
+
+ const d1 = findDenomOrThrow(
+ exchangeInfo,
+ "TESTKUDOS:8" as AmountString,
+ {},
+ );
+
+ const coin = await withdrawCoin({
+ http,
+ cryptoApi,
+ reserveKeyPair: {
+ reservePriv: reserveKeyPair.priv,
+ reservePub: reserveKeyPair.pub,
+ },
+ denom: d1,
+ exchangeBaseUrl: exchange.baseUrl,
+ });
+
+ const wireSalt = encodeCrock(getRandomBytes(16));
+ const merchantPub = encodeCrock(getRandomBytes(32));
+ const contractTermsHash = encodeCrock(getRandomBytes(64));
+
+ await depositCoin({
+ contractTermsHash,
+ merchantPub,
+ wireSalt,
+ amount: "TESTKUDOS:4" as AmountString,
+ coin: coin,
+ cryptoApi,
+ exchangeBaseUrl: exchange.baseUrl,
+ http,
+ });
+
+ // Idempotency
+ await depositCoin({
+ contractTermsHash,
+ merchantPub,
+ wireSalt,
+ amount: "TESTKUDOS:4" as AmountString,
+ coin: coin,
+ cryptoApi,
+ exchangeBaseUrl: exchange.baseUrl,
+ http,
+ });
+
+ try {
+ // Non-idempotent request with different amount
+ await depositCoin({
+ contractTermsHash,
+ merchantPub,
+ wireSalt,
+ amount: "TESTKUDOS:3.5" as AmountString,
+ coin: coin,
+ cryptoApi,
+ exchangeBaseUrl: exchange.baseUrl,
+ http,
+ });
+ } catch (e) {
+ if (e instanceof TalerError && e.errorDetail.code === 7005) {
+ if (e.errorDetail.httpStatusCode === 409) {
+ console.log("got expected error response from exchange");
+ console.log(e);
+ console.log(j2s(e.errorDetail));
+ } else {
+ console.log("did not expect deposit error from exchange");
+ throw e;
+ }
+ } else {
+ throw e;
+ }
+ }
+ } catch (e) {
+ if (e instanceof TalerError) {
+ console.log(e);
+ console.log(j2s(e.errorDetail));
+ } else {
+ console.log(e);
+ }
+ throw e;
+ }
+}
+
+runExchangeDepositTest.suites = ["exchange"];
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-exchange-management.ts b/packages/taler-harness/src/integrationtests/test-exchange-management-fault.ts
index 6b63c3741..d3bd022ae 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-exchange-management.ts
+++ b/packages/taler-harness/src/integrationtests/test-exchange-management-fault.ts
@@ -18,35 +18,32 @@
* Imports.
*/
import {
- GlobalTestState,
- WalletCli,
- setupDb,
- BankService,
- ExchangeService,
- MerchantService,
- getPayto,
-} from "../harness/harness.js";
-import {
- WalletApiOperation,
- BankApi,
- BankAccessApi,
-} from "@gnu-taler/taler-wallet-core";
-import {
ExchangesListResponse,
- URL,
+ TalerCorebankApiClient,
TalerErrorCode,
+ URL,
j2s,
} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { defaultCoinConfig } from "../harness/denomStructures.js";
import {
FaultInjectedExchangeService,
FaultInjectionResponseContext,
} from "../harness/faultInjection.js";
-import { defaultCoinConfig } from "../harness/denomStructures.js";
+import {
+ BankService,
+ ExchangeService,
+ GlobalTestState,
+ MerchantService,
+ WalletCli,
+ generateRandomPayto,
+ setupDb,
+} from "../harness/harness.js";
/**
- * Test if the wallet handles outdated exchange versions correct.y
+ * Test if the wallet handles outdated exchange versions correctly.
*/
-export async function runExchangeManagementTest(
+export async function runExchangeManagementFaultTest(
t: GlobalTestState,
): Promise<void> {
// Set up test environment
@@ -81,6 +78,10 @@ export async function runExchangeManagementTest(
exchange.addBankAccount("1", exchangeBankAccount);
const faultyExchange = new FaultInjectedExchangeService(t, exchange, 8091);
+ // Base URL must contain port that the proxy is listening on.
+ await exchange.modifyConfig(async (config) => {
+ config.setString("exchange", "base_url", "http://localhost:8091/");
+ });
bank.setSuggestedExchange(
faultyExchange,
@@ -101,16 +102,16 @@ export async function runExchangeManagementTest(
await merchant.start();
await merchant.pingUntilAvailable();
- await merchant.addInstance({
+ await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [getPayto("merchant-default")],
+ paytoUris: [generateRandomPayto("merchant-default")],
});
- await merchant.addInstance({
+ await merchant.addInstanceWithWireAccount({
id: "minst1",
name: "minst1",
- paytoUris: [getPayto("minst1")],
+ paytoUris: [generateRandomPayto("minst1")],
});
console.log("setup done!");
@@ -189,11 +190,14 @@ export async function runExchangeManagementTest(
});
});
+ console.log("got error", err1);
+
// Response is malformed, since it didn't even contain a version code
// in a format the wallet can understand.
t.assertTrue(
- err1.errorDetail.code === TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ err1.errorDetail.code === TalerErrorCode.WALLET_EXCHANGE_UNAVAILABLE,
);
+
exchangesList = await wallet.client.call(
WalletApiOperation.ListExchanges,
{},
@@ -234,11 +238,7 @@ export async function runExchangeManagementTest(
});
});
- t.assertTrue(
- err2.hasErrorCode(
- TalerErrorCode.WALLET_EXCHANGE_PROTOCOL_VERSION_INCOMPATIBLE,
- ),
- );
+ t.assertTrue(err2.hasErrorCode(TalerErrorCode.WALLET_EXCHANGE_UNAVAILABLE));
exchangesList = await wallet.client.call(
WalletApiOperation.ListExchanges,
@@ -262,10 +262,11 @@ export async function runExchangeManagementTest(
// Create withdrawal operation
- const user = await BankApi.createRandomBankUser(bank);
- const wop = await BankAccessApi.createWithdrawalOperation(
- bank,
- user,
+ const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl);
+
+ const user = await bankClient.createRandomBankUser();
+ const wop = await bankClient.createWithdrawalOperation(
+ user.username,
"TESTKUDOS:10",
);
@@ -282,4 +283,4 @@ export async function runExchangeManagementTest(
t.assertTrue(wd.possibleExchanges.length === 0);
}
-runExchangeManagementTest.suites = ["wallet", "exchange"];
+runExchangeManagementFaultTest.suites = ["wallet", "exchange"];
diff --git a/packages/taler-harness/src/integrationtests/test-exchange-management.ts b/packages/taler-harness/src/integrationtests/test-exchange-management.ts
new file mode 100644
index 000000000..9d3c1d867
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-exchange-management.ts
@@ -0,0 +1,82 @@
+/*
+ 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 { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState } from "../harness/harness.js";
+import { createSimpleTestkudosEnvironmentV2 } from "../harness/helpers.js";
+
+/**
+ * Test if the wallet handles outdated exchange versions correctly.
+ */
+export async function runExchangeManagementTest(
+ t: GlobalTestState,
+): Promise<void> {
+ // Set up test environment
+
+ const { walletClient, exchange } =
+ await createSimpleTestkudosEnvironmentV2(t);
+
+ // Since the default exchanges can change, we start the wallet in tests
+ // with no built-in defaults. Thus the list of exchanges is empty here.
+ const exchangesListResult = await walletClient.call(
+ WalletApiOperation.ListExchanges,
+ {},
+ );
+
+ t.assertDeepEqual(exchangesListResult.exchanges.length, 0);
+
+ await walletClient.call(WalletApiOperation.UpdateExchangeEntry, {
+ exchangeBaseUrl: exchange.baseUrl,
+ force: true,
+ });
+
+ const exchangesListResult2 = await walletClient.call(
+ WalletApiOperation.ListExchanges,
+ {},
+ );
+
+ t.assertDeepEqual(exchangesListResult2.exchanges.length, 1);
+
+ await walletClient.call(WalletApiOperation.DeleteExchange, {
+ exchangeBaseUrl: exchange.baseUrl,
+ });
+
+ const exchangesListResult3 = await walletClient.call(
+ WalletApiOperation.ListExchanges,
+ {},
+ );
+
+ t.assertDeepEqual(exchangesListResult3.exchanges.length, 0);
+
+ // Check for regression: Can we re-add a deleted exchange?
+
+ await walletClient.call(WalletApiOperation.UpdateExchangeEntry, {
+ exchangeBaseUrl: exchange.baseUrl,
+ force: true,
+ });
+
+ const exchangesListResult4 = await walletClient.call(
+ WalletApiOperation.ListExchanges,
+ {},
+ );
+
+ t.assertDeepEqual(exchangesListResult4.exchanges.length, 1);
+}
+
+runExchangeManagementTest.suites = ["wallet", "exchange"];
diff --git a/packages/taler-harness/src/integrationtests/test-exchange-purse.ts b/packages/taler-harness/src/integrationtests/test-exchange-purse.ts
new file mode 100644
index 000000000..6666e2d0b
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-exchange-purse.ts
@@ -0,0 +1,224 @@
+/*
+ This file is part of GNU Taler
+ (C) 2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ AbsoluteTime,
+ AmountString,
+ ContractTermsUtil,
+ decodeCrock,
+ Duration,
+ encodeCrock,
+ getRandomBytes,
+ hash,
+ j2s,
+ PeerContractTerms,
+ TalerError,
+} from "@gnu-taler/taler-util";
+import {
+ CryptoDispatcher,
+ EncryptContractRequest,
+ SpendCoinDetails,
+ SynchronousCryptoWorkerFactoryPlain,
+} from "@gnu-taler/taler-wallet-core";
+import {
+ checkReserve,
+ downloadExchangeInfo,
+ findDenomOrThrow,
+ topupReserveWithBank,
+ withdrawCoin,
+} from "@gnu-taler/taler-wallet-core/dbless";
+import { GlobalTestState, harnessHttpLib } from "../harness/harness.js";
+import { createSimpleTestkudosEnvironmentV2 } from "../harness/helpers.js";
+
+/**
+ * Test the exchange's purse API.
+ */
+export async function runExchangePurseTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { bank, exchange } = await createSimpleTestkudosEnvironmentV2(t);
+
+ const http = harnessHttpLib;
+ const cryptoDisp = new CryptoDispatcher(
+ new SynchronousCryptoWorkerFactoryPlain(),
+ );
+ const cryptoApi = cryptoDisp.cryptoApi;
+
+ try {
+ // Withdraw digital cash into the wallet.
+
+ const exchangeInfo = await downloadExchangeInfo(exchange.baseUrl, http);
+
+ const reserveKeyPair = await cryptoApi.createEddsaKeypair({});
+
+ let reserveUrl = new URL(
+ `reserves/${reserveKeyPair.pub}`,
+ exchange.baseUrl,
+ );
+ reserveUrl.searchParams.set("timeout_ms", "30000");
+ const longpollReq = http.fetch(reserveUrl.href, {
+ method: "GET",
+ });
+
+ await topupReserveWithBank({
+ amount: "TESTKUDOS:10" as AmountString,
+ http,
+ reservePub: reserveKeyPair.pub,
+ corebankApiBaseUrl: bank.corebankApiBaseUrl,
+ exchangeInfo,
+ });
+
+ console.log("waiting for longpoll request");
+ const resp = await longpollReq;
+ console.log(`got response, status ${resp.status}`);
+
+ console.log(exchangeInfo);
+
+ await checkReserve(http, exchange.baseUrl, reserveKeyPair.pub);
+
+ const d1 = findDenomOrThrow(
+ exchangeInfo,
+ "TESTKUDOS:8" as AmountString,
+ {},
+ );
+
+ const coin = await withdrawCoin({
+ http,
+ cryptoApi,
+ reserveKeyPair: {
+ reservePriv: reserveKeyPair.priv,
+ reservePub: reserveKeyPair.pub,
+ },
+ denom: d1,
+ exchangeBaseUrl: exchange.baseUrl,
+ });
+
+ const amount = "TESTKUDOS:5" as AmountString;
+
+ const contractTerms: PeerContractTerms = {
+ amount,
+ summary: "Hello",
+ purse_expiration: AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ minutes: 1 }),
+ ),
+ ),
+ };
+
+ const mergeReservePair = await cryptoApi.createEddsaKeypair({});
+ const pursePair = await cryptoApi.createEddsaKeypair({});
+ const mergePair = await cryptoApi.createEddsaKeypair({});
+ const contractPair = await cryptoApi.createEddsaKeypair({});
+ const contractEncNonce = encodeCrock(getRandomBytes(24));
+
+ const pursePub = pursePair.pub;
+
+ const hContractTerms = ContractTermsUtil.hashContractTerms(contractTerms);
+
+ const purseSigResp = await cryptoApi.signPurseCreation({
+ hContractTerms,
+ mergePub: mergePair.pub,
+ minAge: 0,
+ purseAmount: amount,
+ purseExpiration: contractTerms.purse_expiration,
+ pursePriv: pursePair.priv,
+ });
+
+ const coinSpend: SpendCoinDetails = {
+ ageCommitmentProof: undefined,
+ coinPriv: coin.coinPriv,
+ coinPub: coin.coinPub,
+ contribution: amount,
+ denomPubHash: coin.denomPubHash,
+ denomSig: coin.denomSig,
+ };
+
+ const depositSigsResp = await cryptoApi.signPurseDeposits({
+ exchangeBaseUrl: exchange.baseUrl,
+ pursePub: pursePair.pub,
+ coins: [coinSpend],
+ });
+
+ const encryptContractRequest: EncryptContractRequest = {
+ contractTerms: contractTerms,
+ mergePriv: mergePair.priv,
+ pursePriv: pursePair.priv,
+ pursePub: pursePair.pub,
+ contractPriv: contractPair.priv,
+ contractPub: contractPair.pub,
+ nonce: contractEncNonce,
+ };
+
+ const econtractResp = await cryptoApi.encryptContractForMerge(
+ encryptContractRequest,
+ );
+
+ const econtractHash = encodeCrock(
+ hash(decodeCrock(econtractResp.econtract.econtract)),
+ );
+
+ const createPurseUrl = new URL(
+ `purses/${pursePair.pub}/create`,
+ exchange.baseUrl,
+ );
+
+ const reqBody = {
+ amount: amount,
+ merge_pub: mergePair.pub,
+ purse_sig: purseSigResp.sig,
+ h_contract_terms: hContractTerms,
+ purse_expiration: contractTerms.purse_expiration,
+ deposits: depositSigsResp.deposits,
+ min_age: 0,
+ econtract: econtractResp.econtract,
+ };
+
+ const httpResp = await http.fetch(createPurseUrl.href, {
+ method: "POST",
+ body: reqBody,
+ });
+
+ const respBody = await httpResp.json();
+
+ console.log("status", httpResp.status);
+
+ console.log(j2s(respBody));
+
+ const mergeUrl = new URL(`purses/${pursePub}/merge`, exchange.baseUrl);
+ mergeUrl.searchParams.set("timeout_ms", "300");
+ const statusResp = await http.fetch(mergeUrl.href, {});
+
+ const statusRespBody = await statusResp.json();
+
+ console.log(j2s(statusRespBody));
+
+ t.assertTrue(statusRespBody.merge_timestamp === undefined);
+ } catch (e) {
+ if (e instanceof TalerError) {
+ console.log(e);
+ console.log(j2s(e.errorDetail));
+ } else {
+ console.log(e);
+ }
+ throw e;
+ }
+}
+
+runExchangePurseTest.suites = ["wallet"];
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-exchange-timetravel.ts b/packages/taler-harness/src/integrationtests/test-exchange-timetravel.ts
index 074126e9f..714a7f879 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-exchange-timetravel.ts
+++ b/packages/taler-harness/src/integrationtests/test-exchange-timetravel.ts
@@ -21,55 +21,79 @@ import {
AbsoluteTime,
codecForExchangeKeysJson,
DenominationPubKey,
+ DenomKeyType,
Duration,
- durationFromSpec,
+ ExchangeKeysJson,
+ Logger,
} from "@gnu-taler/taler-util";
import {
- NodeHttpLib,
+ createPlatformHttpLib,
readSuccessResponseJsonOrThrow,
-} from "@gnu-taler/taler-wallet-core";
+} from "@gnu-taler/taler-util/http";
import { makeNoFeeCoinConfig } from "../harness/denomStructures.js";
import {
BankService,
ExchangeService,
+ generateRandomPayto,
GlobalTestState,
- MerchantPrivateApi,
MerchantService,
setupDb,
- WalletCli,
- getPayto,
} from "../harness/harness.js";
-import { startWithdrawViaBank, withdrawViaBank } from "../harness/helpers.js";
-
-async function applyTimeTravel(
- timetravelDuration: Duration,
- s: {
- exchange?: ExchangeService;
- merchant?: MerchantService;
- wallet?: WalletCli;
- },
-): Promise<void> {
- if (s.exchange) {
- await s.exchange.stop();
- s.exchange.setTimetravel(timetravelDuration);
- await s.exchange.start();
- await s.exchange.pingUntilAvailable();
- }
+import {
+ applyTimeTravelV2,
+ createWalletDaemonWithClient,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
- if (s.merchant) {
- await s.merchant.stop();
- s.merchant.setTimetravel(timetravelDuration);
- await s.merchant.start();
- await s.merchant.pingUntilAvailable();
- }
+const logger = new Logger("test-exchange-timetravel.ts");
+
+interface DenomInfo {
+ denomPub: DenominationPubKey;
+ expireDeposit: string;
+}
- if (s.wallet) {
- console.log("setting wallet time travel to", timetravelDuration);
- s.wallet.setTimetravel(timetravelDuration);
+function getDenomInfoFromKeys(ek: ExchangeKeysJson): DenomInfo[] {
+ const denomInfos: DenomInfo[] = [];
+ for (const denomGroup of ek.denominations) {
+ switch (denomGroup.cipher) {
+ case "RSA":
+ case "RSA+age_restricted": {
+ let ageMask = 0;
+ if (denomGroup.cipher === "RSA+age_restricted") {
+ ageMask = denomGroup.age_mask;
+ }
+ for (const denomIn of denomGroup.denoms) {
+ const denomPub: DenominationPubKey = {
+ age_mask: ageMask,
+ cipher: DenomKeyType.Rsa,
+ rsa_public_key: denomIn.rsa_pub,
+ };
+ denomInfos.push({
+ denomPub,
+ expireDeposit: AbsoluteTime.stringify(
+ AbsoluteTime.fromProtocolTimestamp(denomIn.stamp_expire_deposit),
+ ),
+ });
+ }
+ break;
+ }
+ case "CS+age_restricted":
+ case "CS":
+ logger.warn("Clause-Schnorr denominations not supported");
+ continue;
+ default:
+ logger.warn(
+ `denomination type ${(denomGroup as any).cipher} not supported`,
+ );
+ continue;
+ }
}
+ return denomInfos;
}
-const http = new NodeHttpLib();
+const http = createPlatformHttpLib({
+ enableThrottling: false,
+});
/**
* Basic time travel test.
@@ -122,27 +146,35 @@ export async function runExchangeTimetravelTest(t: GlobalTestState) {
await merchant.start();
await merchant.pingUntilAvailable();
- await merchant.addInstance({
+ await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [getPayto("merchant-default")],
+ paytoUris: [generateRandomPayto("merchant-default")],
});
- await merchant.addInstance({
+ await merchant.addInstanceWithWireAccount({
id: "minst1",
name: "minst1",
- paytoUris: [getPayto("minst1")],
+ paytoUris: [generateRandomPayto("minst1")],
});
console.log("setup done!");
- const wallet = new WalletCli(t);
+ const { walletClient } = await createWalletDaemonWithClient(t, {
+ name: "default",
+ });
// Withdraw digital cash into the wallet.
- await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:15" });
+ const wres = await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:15",
+ });
+ await wres.withdrawalFinishedCond;
- const keysResp1 = await http.get(exchange.baseUrl + "keys");
+ const keysResp1 = await http.fetch(exchange.baseUrl + "keys");
const keys1 = await readSuccessResponseJsonOrThrow(
keysResp1,
codecForExchangeKeysJson(),
@@ -155,13 +187,16 @@ export async function runExchangeTimetravelTest(t: GlobalTestState) {
// Travel into the future, the deposit expiration is two years
// into the future.
console.log("applying first time travel");
- await applyTimeTravel(durationFromSpec({ days: 400 }), {
- wallet,
- exchange,
- merchant,
- });
+ await applyTimeTravelV2(
+ Duration.toMilliseconds(Duration.fromSpec({ days: 400 })),
+ {
+ walletClient,
+ exchange,
+ merchant,
+ },
+ );
- const keysResp2 = await http.get(exchange.baseUrl + "keys");
+ const keysResp2 = await http.fetch(exchange.baseUrl + "keys");
const keys2 = await readSuccessResponseJsonOrThrow(
keysResp2,
codecForExchangeKeysJson(),
@@ -171,41 +206,31 @@ export async function runExchangeTimetravelTest(t: GlobalTestState) {
JSON.stringify(keys2, undefined, 2),
);
- const denomPubs1 = keys1.denoms.map((x) => {
- return {
- denomPub: x.denom_pub,
- expireDeposit: AbsoluteTime.stringify(
- AbsoluteTime.fromTimestamp(x.stamp_expire_deposit),
- ),
- };
- });
+ const denomPubs1 = getDenomInfoFromKeys(keys1);
+ const denomPubs2 = getDenomInfoFromKeys(keys2);
- const denomPubs2 = keys2.denoms.map((x) => {
- return {
- denomPub: x.denom_pub,
- expireDeposit: AbsoluteTime.stringify(
- AbsoluteTime.fromTimestamp(x.stamp_expire_deposit),
- ),
- };
- });
const dps2 = new Set(denomPubs2.map((x) => x.denomPub));
console.log("=== KEYS RESPONSE 1 ===");
console.log(
"list issue date",
- AbsoluteTime.stringify(AbsoluteTime.fromTimestamp(keys1.list_issue_date)),
+ AbsoluteTime.stringify(
+ AbsoluteTime.fromProtocolTimestamp(keys1.list_issue_date),
+ ),
);
- console.log("num denoms", keys1.denoms.length);
+ console.log("num denoms", denomPubs1.length);
console.log("denoms", JSON.stringify(denomPubs1, undefined, 2));
console.log("=== KEYS RESPONSE 2 ===");
console.log(
"list issue date",
- AbsoluteTime.stringify(AbsoluteTime.fromTimestamp(keys2.list_issue_date)),
+ AbsoluteTime.stringify(
+ AbsoluteTime.fromProtocolTimestamp(keys2.list_issue_date),
+ ),
);
- console.log("num denoms", keys2.denoms.length);
+ console.log("num denoms", denomPubs2.length);
console.log("denoms", JSON.stringify(denomPubs2, undefined, 2));
for (const da of denomPubs1) {
@@ -225,7 +250,7 @@ export async function runExchangeTimetravelTest(t: GlobalTestState) {
);
console.log(
`the new /keys response was issued ${AbsoluteTime.stringify(
- AbsoluteTime.fromTimestamp(keys2.list_issue_date),
+ AbsoluteTime.fromProtocolTimestamp(keys2.list_issue_date),
)}`,
);
console.log(
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-fee-regression.ts b/packages/taler-harness/src/integrationtests/test-fee-regression.ts
index 8c5a5bea4..f164606c4 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-fee-regression.ts
+++ b/packages/taler-harness/src/integrationtests/test-fee-regression.ts
@@ -19,18 +19,18 @@
*/
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import {
- GlobalTestState,
BankService,
ExchangeService,
+ GlobalTestState,
MerchantService,
+ generateRandomPayto,
setupDb,
- WalletCli,
- getPayto,
} from "../harness/harness.js";
import {
- withdrawViaBank,
- makeTestPayment,
- SimpleTestEnvironment,
+ SimpleTestEnvironmentNg,
+ createWalletDaemonWithClient,
+ makeTestPaymentV2,
+ withdrawViaBankV2,
} from "../harness/helpers.js";
/**
@@ -39,7 +39,7 @@ import {
*/
export async function createMyTestkudosEnvironment(
t: GlobalTestState,
-): Promise<SimpleTestEnvironment> {
+): Promise<SimpleTestEnvironmentNg> {
const db = await setupDb(t);
const bank = await BankService.create(t, {
@@ -139,21 +139,27 @@ export async function createMyTestkudosEnvironment(
await merchant.pingUntilAvailable();
await merchant.addDefaultInstance();
- await merchant.addInstance({
+ await merchant.addInstanceWithWireAccount({
id: "minst1",
name: "minst1",
- paytoUris: [getPayto("minst1")],
+ paytoUris: [generateRandomPayto("minst1")],
});
console.log("setup done!");
- const wallet = new WalletCli(t);
+ const { walletClient, walletService } = await createWalletDaemonWithClient(
+ t,
+ {
+ name: "w1",
+ },
+ );
return {
commonDb: db,
exchange,
merchant,
- wallet,
+ walletClient,
+ walletService,
bank,
exchangeBankAccount,
};
@@ -165,19 +171,21 @@ export async function createMyTestkudosEnvironment(
export async function runFeeRegressionTest(t: GlobalTestState) {
// Set up test environment
- const { wallet, bank, exchange, merchant } =
+ const { walletClient, bank, exchange, merchant } =
await createMyTestkudosEnvironment(t);
// Withdraw digital cash into the wallet.
- await withdrawViaBank(t, {
- wallet,
+ const wres = await withdrawViaBankV2(t, {
+ walletClient,
bank,
exchange,
amount: "TESTKUDOS:1.92",
});
- const coins = await wallet.client.call(WalletApiOperation.DumpCoins, {});
+ await wres.withdrawalFinishedCond;
+
+ const coins = await walletClient.call(WalletApiOperation.DumpCoins, {});
// Make sure we really withdraw one 0.64 and one 1.28 coin.
t.assertTrue(coins.coins.length === 2);
@@ -188,11 +196,11 @@ export async function runFeeRegressionTest(t: GlobalTestState) {
fulfillment_url: "taler://fulfillment-success/thx",
};
- await makeTestPayment(t, { wallet, merchant, order });
+ await makeTestPaymentV2(t, { walletClient, merchant, order });
- await wallet.runUntilDone();
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
- const txs = await wallet.client.call(WalletApiOperation.GetTransactions, {});
+ const txs = await walletClient.call(WalletApiOperation.GetTransactions, {});
t.assertAmountEquals(txs.transactions[1].amountEffective, "TESTKUDOS:1.30");
console.log(txs);
}
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-forced-selection.ts b/packages/taler-harness/src/integrationtests/test-forced-selection.ts
index 91be11a82..839ddd927 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-forced-selection.ts
+++ b/packages/taler-harness/src/integrationtests/test-forced-selection.ts
@@ -17,10 +17,10 @@
/**
* Imports.
*/
-import { j2s } from "@gnu-taler/taler-util";
+import { AmountString, j2s } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { GlobalTestState } from "../harness/harness.js";
-import { createSimpleTestkudosEnvironment } from "../harness/helpers.js";
+import { createSimpleTestkudosEnvironmentV2 } from "../harness/helpers.js";
/**
* Run test for forced denom/coin selection.
@@ -28,51 +28,50 @@ import { createSimpleTestkudosEnvironment } from "../harness/helpers.js";
export async function runForcedSelectionTest(t: GlobalTestState) {
// Set up test environment
- const { wallet, bank, exchange, merchant } =
- await createSimpleTestkudosEnvironment(t);
+ const { walletClient, bank, exchange, merchant } =
+ await createSimpleTestkudosEnvironmentV2(t);
- await wallet.client.call(WalletApiOperation.AddExchange, {
+ await walletClient.call(WalletApiOperation.AddExchange, {
exchangeBaseUrl: exchange.baseUrl,
});
- await wallet.client.call(WalletApiOperation.WithdrawTestBalance, {
+ await walletClient.call(WalletApiOperation.WithdrawTestBalance, {
exchangeBaseUrl: exchange.baseUrl,
- amount: "TESTKUDOS:10",
- bankBaseUrl: bank.baseUrl,
- bankAccessApiBaseUrl: bank.bankAccessApiBaseUrl,
+ amount: "TESTKUDOS:10" as AmountString,
+ corebankApiBaseUrl: bank.corebankApiBaseUrl,
forcedDenomSel: {
denoms: [
{
- value: "TESTKUDOS:2",
+ value: "TESTKUDOS:2" as AmountString,
count: 3,
},
],
},
});
- await wallet.runUntilDone();
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
- const coinDump = await wallet.client.call(WalletApiOperation.DumpCoins, {});
+ const coinDump = await walletClient.call(WalletApiOperation.DumpCoins, {});
console.log(coinDump);
t.assertDeepEqual(coinDump.coins.length, 3);
- const payResp = await wallet.client.call(WalletApiOperation.TestPay, {
- amount: "TESTKUDOS:3",
+ const payResp = await walletClient.call(WalletApiOperation.TestPay, {
+ amount: "TESTKUDOS:3" as AmountString,
merchantBaseUrl: merchant.makeInstanceBaseUrl(),
summary: "bla",
forcedCoinSel: {
coins: [
{
- value: "TESTKUDOS:2",
- contribution: "TESTKUDOS:1",
+ value: "TESTKUDOS:2" as AmountString,
+ contribution: "TESTKUDOS:1" as AmountString,
},
{
- value: "TESTKUDOS:2",
- contribution: "TESTKUDOS:1",
+ value: "TESTKUDOS:2" as AmountString,
+ contribution: "TESTKUDOS:1" as AmountString,
},
{
- value: "TESTKUDOS:2",
- contribution: "TESTKUDOS:1",
+ value: "TESTKUDOS:2" as AmountString,
+ contribution: "TESTKUDOS:1" as AmountString,
},
],
},
@@ -81,7 +80,7 @@ export async function runForcedSelectionTest(t: GlobalTestState) {
console.log(j2s(payResp));
// Without forced selection, we would only use 2 coins.
- t.assertDeepEqual(payResp.payCoinSelection.coinContributions.length, 3);
+ t.assertDeepEqual(payResp.numCoins, 3);
}
runForcedSelectionTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-kyc.ts b/packages/taler-harness/src/integrationtests/test-kyc.ts
new file mode 100644
index 000000000..a9ef654fd
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-kyc.ts
@@ -0,0 +1,422 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ Duration,
+ Logger,
+ NotificationType,
+ TalerCorebankApiClient,
+ TransactionMajorState,
+ TransactionMinorState,
+ TransactionType,
+ j2s,
+} from "@gnu-taler/taler-util";
+import { createPlatformHttpLib } from "@gnu-taler/taler-util/http";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import * as http from "node:http";
+import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js";
+import {
+ BankService,
+ ExchangeService,
+ GlobalTestState,
+ MerchantService,
+ WalletClient,
+ WalletService,
+ generateRandomPayto,
+ setupDb,
+} from "../harness/harness.js";
+import { EnvOptions, SimpleTestEnvironmentNg } from "../harness/helpers.js";
+
+const logger = new Logger("test-kyc.ts");
+
+export async function createKycTestkudosEnvironment(
+ t: GlobalTestState,
+ coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")),
+ opts: EnvOptions = {},
+): Promise<SimpleTestEnvironmentNg> {
+ const db = await setupDb(t);
+
+ const bank = await BankService.create(t, {
+ allowRegistrations: true,
+ currency: "TESTKUDOS",
+ database: db.connStr,
+ httpPort: 8082,
+ });
+
+ const exchange = ExchangeService.create(t, {
+ name: "testexchange-1",
+ currency: "TESTKUDOS",
+ httpPort: 8081,
+ database: db.connStr,
+ });
+
+ 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 ageMaskSpec = opts.ageMaskSpec;
+
+ if (ageMaskSpec) {
+ exchange.enableAgeRestrictions(ageMaskSpec);
+ // Enable age restriction for all coins.
+ exchange.addCoinConfigList(
+ coinConfig.map((x) => ({
+ ...x,
+ name: `${x.name}-age`,
+ ageRestricted: true,
+ })),
+ );
+ // For mixed age restrictions, we also offer coins without age restrictions
+ if (opts.mixedAgeRestriction) {
+ exchange.addCoinConfigList(
+ coinConfig.map((x) => ({ ...x, ageRestricted: false })),
+ );
+ }
+ } else {
+ exchange.addCoinConfigList(coinConfig);
+ }
+
+ await exchange.modifyConfig(async (config) => {
+ const myprov = "kyc-provider-myprov";
+ config.setString(myprov, "cost", "0");
+ config.setString(myprov, "logic", "oauth2");
+ config.setString(myprov, "provided_checks", "dummy1");
+ config.setString(myprov, "user_type", "individual");
+ config.setString(myprov, "kyc_oauth2_validity", "forever");
+ config.setString(
+ myprov,
+ "kyc_oauth2_token_url",
+ "http://localhost:6666/oauth/v2/token",
+ );
+ config.setString(
+ myprov,
+ "kyc_oauth2_authorize_url",
+ "http://localhost:6666/oauth/v2/login",
+ );
+ config.setString(
+ myprov,
+ "kyc_oauth2_info_url",
+ "http://localhost:6666/oauth/v2/info",
+ );
+ config.setString(
+ myprov,
+ "kyc_oauth2_converter_helper",
+ "taler-exchange-kyc-oauth2-test-converter.sh",
+ );
+ config.setString(myprov, "kyc_oauth2_client_id", "taler-exchange");
+ config.setString(myprov, "kyc_oauth2_client_secret", "exchange-secret");
+ config.setString(myprov, "kyc_oauth2_post_url", "https://taler.net");
+
+ config.setString(
+ "kyc-legitimization-withdraw1",
+ "operation_type",
+ "withdraw",
+ );
+ config.setString(
+ "kyc-legitimization-withdraw1",
+ "required_checks",
+ "dummy1",
+ );
+ config.setString("kyc-legitimization-withdraw1", "timeframe", "1d");
+ config.setString(
+ "kyc-legitimization-withdraw1",
+ "threshold",
+ "TESTKUDOS:5",
+ );
+ });
+
+ await exchange.start();
+ await exchange.pingUntilAvailable();
+
+ merchant.addExchange(exchange);
+
+ await merchant.start();
+ await merchant.pingUntilAvailable();
+
+ await merchant.addInstanceWithWireAccount({
+ id: "default",
+ name: "Default Instance",
+ paytoUris: [generateRandomPayto("merchant-default")],
+ defaultWireTransferDelay: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ minutes: 1 }),
+ ),
+ });
+
+ await merchant.addInstanceWithWireAccount({
+ id: "minst1",
+ name: "minst1",
+ paytoUris: [generateRandomPayto("minst1")],
+ defaultWireTransferDelay: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ minutes: 1 }),
+ ),
+ });
+
+ const walletService = new WalletService(t, {
+ name: "wallet",
+ useInMemoryDb: true,
+ });
+ await walletService.start();
+ await walletService.pingUntilAvailable();
+
+ const walletClient = new WalletClient({
+ name: "wallet",
+ unixPath: walletService.socketPath,
+ onNotification(n) {
+ console.log("got notification", n);
+ },
+ });
+ await walletClient.connect();
+ await walletClient.client.call(WalletApiOperation.InitWallet, {
+ config: {
+ testing: {
+ skipDefaults: true,
+ },
+ },
+ });
+
+ console.log("setup done!");
+
+ return {
+ commonDb: db,
+ exchange,
+ merchant,
+ walletClient,
+ walletService,
+ bank,
+ exchangeBankAccount,
+ };
+}
+
+interface TestfakeKycService {
+ stop: () => void;
+}
+
+function splitInTwoAt(s: string, separator: string): [string, string] {
+ const idx = s.indexOf(separator);
+ if (idx === -1) {
+ return [s, ""];
+ }
+ return [s.slice(0, idx), s.slice(idx + 1)];
+}
+
+/**
+ * Testfake for the kyc service that the exchange talks to.
+ */
+async function runTestfakeKycService(): Promise<TestfakeKycService> {
+ const server = http.createServer((req, res) => {
+ const requestUrl = req.url!;
+ logger.info(`kyc: got ${req.method} request, ${requestUrl}`);
+
+ const [path, query] = splitInTwoAt(requestUrl, "?");
+
+ const qp = new URLSearchParams(query);
+
+ if (path === "/oauth/v2/login") {
+ // Usually this would render some HTML page for the user to log in,
+ // but we return JSON here.
+ const redirUriUnparsed = qp.get("redirect_uri");
+ if (!redirUriUnparsed) {
+ throw Error("missing redirect_url");
+ }
+ const state = qp.get("state");
+ if (!state) {
+ throw Error("missing state");
+ }
+ const redirUri = new URL(redirUriUnparsed);
+ redirUri.searchParams.set("code", "code_is_ok");
+ redirUri.searchParams.set("state", state);
+ res.writeHead(200, { "Content-Type": "application/json" });
+ res.end(
+ JSON.stringify({
+ redirect_uri: redirUri.href,
+ }),
+ );
+ } else if (path === "/oauth/v2/token") {
+ let reqBody = "";
+ req.on("data", (x) => {
+ reqBody += x;
+ });
+
+ req.on("end", () => {
+ logger.info("login request body:", reqBody);
+
+ res.writeHead(200, { "Content-Type": "application/json" });
+ // Normally, the access_token would also include which user we're trying
+ // to get info about, but we (for now) skip it in this test.
+ res.end(
+ JSON.stringify({
+ access_token: "exchange_access_token",
+ token_type: "Bearer",
+ }),
+ );
+ });
+ } else if (path === "/oauth/v2/info") {
+ logger.info("authorization header:", req.headers.authorization);
+ res.writeHead(200, { "Content-Type": "application/json" });
+ res.end(
+ JSON.stringify({
+ status: "success",
+ data: {
+ id: "foobar",
+ },
+ }),
+ );
+ } else {
+ res.writeHead(400, { "Content-Type": "application/json" });
+ res.end(JSON.stringify({ code: 1, message: "bad request" }));
+ }
+ });
+ await new Promise<void>((resolve, reject) => {
+ server.listen(6666, () => resolve());
+ });
+ return {
+ stop() {
+ server.close();
+ },
+ };
+}
+
+export async function runKycTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { walletClient, bank, exchange, merchant } =
+ await createKycTestkudosEnvironment(t);
+
+ const kycServer = await runTestfakeKycService();
+
+ // Withdraw digital cash into the wallet.
+
+ const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl);
+
+ const amount = "TESTKUDOS:20";
+ const user = await bankClient.createRandomBankUser();
+ const wop = await bankClient.createWithdrawalOperation(user.username, amount);
+
+ // Hand it to the wallet
+
+ await walletClient.client.call(
+ WalletApiOperation.GetWithdrawalDetailsForUri,
+ {
+ talerWithdrawUri: wop.taler_withdraw_uri,
+ },
+ );
+
+ // Withdraw
+
+ const acceptResp = await walletClient.client.call(
+ WalletApiOperation.AcceptBankIntegratedWithdrawal,
+ {
+ exchangeBaseUrl: exchange.baseUrl,
+ talerWithdrawUri: wop.taler_withdraw_uri,
+ },
+ );
+
+ const withdrawalTxId = acceptResp.transactionId;
+
+ // Confirm it
+
+ await bankClient.confirmWithdrawalOperation(user.username, {
+ withdrawalOperationId: wop.withdrawal_id,
+ });
+
+ const kycNotificationCond = walletClient.waitForNotificationCond((x) => {
+ if (
+ x.type === NotificationType.TransactionStateTransition &&
+ x.transactionId === withdrawalTxId &&
+ x.newTxState.major === TransactionMajorState.Pending &&
+ x.newTxState.minor === TransactionMinorState.KycRequired
+ ) {
+ return x;
+ }
+ return false;
+ });
+
+ const withdrawalDoneCond = walletClient.waitForNotificationCond(
+ (x) =>
+ x.type === NotificationType.TransactionStateTransition &&
+ x.transactionId === withdrawalTxId &&
+ x.newTxState.major === TransactionMajorState.Done,
+ );
+
+ const kycNotif = await kycNotificationCond;
+
+ logger.info("got kyc notification:", j2s(kycNotif));
+
+ const txState = await walletClient.client.call(
+ WalletApiOperation.GetTransactionById,
+ {
+ transactionId: withdrawalTxId,
+ },
+ );
+
+ t.assertDeepEqual(txState.type, TransactionType.Withdrawal);
+
+ const kycUrl = txState.kycUrl;
+
+ t.assertTrue(!!kycUrl);
+
+ logger.info(`kyc URL is ${kycUrl}`);
+
+ // We now simulate the user interacting with the KYC service,
+ // which would usually done in the browser.
+
+ const httpLib = createPlatformHttpLib({
+ enableThrottling: false,
+ });
+ const kycServerResp = await httpLib.fetch(kycUrl);
+ const kycLoginResp = await kycServerResp.json();
+ logger.info(`kyc server resp: ${j2s(kycLoginResp)}`);
+ const kycProofUrl = kycLoginResp.redirect_uri;
+ // We need to "visit" the KYC proof URL at least once to trigger the exchange
+ // asking for the KYC status.
+ const proofHttpResp = await httpLib.fetch(kycProofUrl);
+ logger.info(`proof resp status ${proofHttpResp.status}`);
+ logger.info(`resp headers ${j2s(proofHttpResp.headers.toJSON())}`);
+ if (
+ !(proofHttpResp.status >= 200 && proofHttpResp.status <= 299) &&
+ proofHttpResp.status !== 303
+ ) {
+ logger.error("kyc proof failed");
+ logger.info(await proofHttpResp.text());
+ t.assertTrue(false);
+ }
+
+ // Now that KYC is done, withdrawal should finally succeed.
+
+ await withdrawalDoneCond;
+
+ kycServer.stop();
+}
+
+runKycTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-bank.ts b/packages/taler-harness/src/integrationtests/test-libeufin-bank.ts
new file mode 100644
index 000000000..3900779e2
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-libeufin-bank.ts
@@ -0,0 +1,231 @@
+/*
+ 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 {
+ CreditDebitIndicator,
+ Logger,
+ TalerCorebankApiClient,
+ TransactionMajorState,
+ TransactionMinorState,
+ WireGatewayApiClient,
+ createEddsaKeyPair,
+ encodeCrock,
+ j2s,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { defaultCoinConfig } from "../harness/denomStructures.js";
+import {
+ ExchangeService,
+ GlobalTestState,
+ LibeufinBankService,
+ MerchantService,
+ generateRandomPayto,
+ generateRandomTestIban,
+ setupDb,
+} from "../harness/harness.js";
+import { createWalletDaemonWithClient } from "../harness/helpers.js";
+
+const logger = new Logger("test-libeufin-bank.ts");
+
+/**
+ * Run test for the basic functionality of libeufin-bank.
+ */
+export async function runLibeufinBankTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const db = await setupDb(t);
+
+ const bank = await LibeufinBankService.create(t, {
+ currency: "TESTKUDOS",
+ httpPort: 8082,
+ database: db.connStr,
+ allowRegistrations: true,
+ });
+
+ 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 exchangeIban = generateRandomTestIban();
+ const exchangeBankUsername = "exchange";
+ const exchangeBankPw = "mypw";
+ const exchangePlainPayto = `payto://iban/${exchangeIban}`;
+ const exchangeExtendedPayto = `payto://iban/${exchangeIban}?receiver-name=Exchange`;
+ const wireGatewayApiBaseUrl = new URL(
+ "accounts/exchange/taler-wire-gateway/",
+ bank.baseUrl,
+ ).href;
+
+ logger.info("creating bank account for the exchange");
+
+ exchange.addBankAccount("1", {
+ wireGatewayApiBaseUrl,
+ accountName: exchangeBankUsername,
+ accountPassword: exchangeBankPw,
+ accountPaytoUri: exchangeExtendedPayto,
+ });
+
+ bank.setSuggestedExchange(exchange);
+
+ await bank.start();
+
+ await bank.pingUntilAvailable();
+
+ exchange.addOfferedCoins(defaultCoinConfig);
+
+ 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")],
+ });
+
+ await merchant.addInstanceWithWireAccount({
+ id: "minst1",
+ name: "minst1",
+ paytoUris: [generateRandomPayto("minst1")],
+ });
+
+ const { walletClient } = await createWalletDaemonWithClient(t, {
+ name: "wallet",
+ });
+
+ console.log("setup done!");
+
+ const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl, {
+ auth: {
+ username: "admin",
+ password: "adminpw",
+ },
+ });
+
+ // register exchange bank account
+ await bankClient.registerAccountExtended({
+ name: "Exchange",
+ password: exchangeBankPw,
+ username: exchangeBankUsername,
+ is_taler_exchange: true,
+ payto_uri: exchangePlainPayto,
+ });
+
+ const bankUser = await bankClient.registerAccount("user1", "pw1");
+ bankClient.setAuth({
+ username: "user1",
+ password: "pw1",
+ });
+
+ // Make sure that registering twice results in a 409 Conflict
+ // {
+ // const e = await t.assertThrowsTalerErrorAsync(async () => {
+ // await bankClient.registerAccount("user1", "pw2");
+ // });
+ // t.assertTrue(e.errorDetail.httpStatusCode === 409);
+ // }
+
+ let balResp = await bankClient.getAccountBalance(bankUser.username);
+
+ console.log(balResp);
+
+ // Check that we got the sign-up bonus.
+ t.assertAmountEquals(balResp.balance.amount, "TESTKUDOS:100");
+ t.assertTrue(
+ balResp.balance.credit_debit_indicator === CreditDebitIndicator.Credit,
+ );
+
+ const res = createEddsaKeyPair();
+
+ // Not a normal client, but one with admin credentials,
+ // as /add-incoming is testing functionality only allowed by the admin.
+ const wireGatewayApiAdminClient = new WireGatewayApiClient(
+ wireGatewayApiBaseUrl,
+ {
+ auth: {
+ username: "admin",
+ password: "adminpw",
+ },
+ },
+ );
+
+ await wireGatewayApiAdminClient.adminAddIncoming({
+ amount: "TESTKUDOS:115",
+ debitAccountPayto: bankUser.accountPaytoUri,
+ reservePub: encodeCrock(res.eddsaPub),
+ });
+
+ balResp = await bankClient.getAccountBalance(bankUser.username);
+ t.assertAmountEquals(balResp.balance.amount, "TESTKUDOS:15");
+ t.assertTrue(
+ balResp.balance.credit_debit_indicator === CreditDebitIndicator.Debit,
+ );
+
+ const wop = await bankClient.createWithdrawalOperation(
+ bankUser.username,
+ "TESTKUDOS:10",
+ );
+
+ const r1 = await walletClient.client.call(
+ WalletApiOperation.GetWithdrawalDetailsForUri,
+ {
+ talerWithdrawUri: wop.taler_withdraw_uri,
+ },
+ );
+
+ console.log(j2s(r1));
+
+ const r2 = await walletClient.client.call(
+ WalletApiOperation.AcceptBankIntegratedWithdrawal,
+ {
+ exchangeBaseUrl: exchange.baseUrl,
+ talerWithdrawUri: wop.taler_withdraw_uri,
+ },
+ );
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionState, {
+ transactionId: r2.transactionId,
+ txState: {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.BankConfirmTransfer,
+ },
+ });
+
+ await bankClient.confirmWithdrawalOperation(bankUser.username, {
+ withdrawalOperationId: wop.withdrawal_id,
+ });
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+}
+
+runLibeufinBankTest.suites = ["fakebank"];
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-merchant-exchange-confusion.ts b/packages/taler-harness/src/integrationtests/test-merchant-exchange-confusion.ts
index 30ab1cd4b..35e3267b1 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-merchant-exchange-confusion.ts
+++ b/packages/taler-harness/src/integrationtests/test-merchant-exchange-confusion.ts
@@ -20,11 +20,10 @@
import {
codecForMerchantOrderStatusUnpaid,
ConfirmPayResultType,
+ MerchantApiClient,
PreparePayResultType,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-import axiosImp from "axios";
-const axios = axiosImp.default;
import { URL } from "url";
import { defaultCoinConfig } from "../harness/denomStructures.js";
import {
@@ -34,16 +33,16 @@ import {
import {
BankService,
ExchangeService,
- getPayto,
+ generateRandomPayto,
GlobalTestState,
- MerchantPrivateApi,
+ harnessHttpLib,
MerchantService,
setupDb,
- WalletCli,
} from "../harness/harness.js";
import {
+ createWalletDaemonWithClient,
FaultyMerchantTestEnvironment,
- withdrawViaBank,
+ withdrawViaBankV2,
} from "../harness/helpers.js";
/**
@@ -79,6 +78,11 @@ export async function createConfusedMerchantTestkudosEnvironment(
const faultyMerchant = new FaultInjectedMerchantService(t, merchant, 9083);
const faultyExchange = new FaultInjectedExchangeService(t, exchange, 9081);
+ // Base URL must contain port that the proxy is listening on.
+ await exchange.modifyConfig(async (config) => {
+ config.setString("exchange", "base_url", "http://localhost:9081/");
+ });
+
const exchangeBankAccount = await bank.createExchangeAccount(
"myexchange",
"x",
@@ -105,27 +109,29 @@ export async function createConfusedMerchantTestkudosEnvironment(
await merchant.start();
await merchant.pingUntilAvailable();
- await merchant.addInstance({
+ await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [getPayto("merchant-default")],
+ paytoUris: [generateRandomPayto("merchant-default")],
});
- await merchant.addInstance({
+ await merchant.addInstanceWithWireAccount({
id: "minst1",
name: "minst1",
- paytoUris: [getPayto("minst1")],
+ paytoUris: [generateRandomPayto("minst1")],
});
console.log("setup done!");
- const wallet = new WalletCli(t);
+ const { walletClient } = await createWalletDaemonWithClient(t, {
+ name: "default",
+ });
return {
commonDb: db,
exchange,
merchant,
- wallet,
+ walletClient,
bank,
exchangeBankAccount,
faultyMerchant,
@@ -140,18 +146,20 @@ export async function createConfusedMerchantTestkudosEnvironment(
export async function runMerchantExchangeConfusionTest(t: GlobalTestState) {
// Set up test environment
- const { wallet, bank, faultyExchange, faultyMerchant } =
+ const { walletClient, bank, faultyExchange, faultyMerchant } =
await createConfusedMerchantTestkudosEnvironment(t);
// Withdraw digital cash into the wallet.
- await withdrawViaBank(t, {
- wallet,
+ const wres = await withdrawViaBankV2(t, {
+ walletClient,
bank,
exchange: faultyExchange,
amount: "TESTKUDOS:20",
});
+ await wres.withdrawalFinishedCond;
+
/**
* =========================================================================
* Create an order and let the wallet pay under a session ID
@@ -163,7 +171,9 @@ export async function runMerchantExchangeConfusionTest(t: GlobalTestState) {
const merchant = faultyMerchant;
- let orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
+ const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl());
+
+ let orderResp = await merchantClient.createOrder({
order: {
summary: "Buy me!",
amount: "TESTKUDOS:5",
@@ -171,7 +181,7 @@ export async function runMerchantExchangeConfusionTest(t: GlobalTestState) {
},
});
- let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ let orderStatus = await merchantClient.queryPrivateOrderStatus({
orderId: orderResp.order_id,
sessionId: "mysession-one",
});
@@ -181,9 +191,7 @@ export async function runMerchantExchangeConfusionTest(t: GlobalTestState) {
t.assertTrue(orderStatus.already_paid_order_id === undefined);
let publicOrderStatusUrl = orderStatus.order_status_url;
- let publicOrderStatusResp = await axios.get(publicOrderStatusUrl, {
- validateStatus: () => true,
- });
+ let publicOrderStatusResp = await harnessHttpLib.fetch(publicOrderStatusUrl);
if (publicOrderStatusResp.status != 402) {
throw Error(
@@ -192,12 +200,12 @@ export async function runMerchantExchangeConfusionTest(t: GlobalTestState) {
}
let pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode(
- publicOrderStatusResp.data,
+ await publicOrderStatusResp.json(),
);
console.log(pubUnpaidStatus);
- let preparePayResp = await wallet.client.call(
+ let preparePayResp = await walletClient.call(
WalletApiOperation.PreparePayForUri,
{
talerPayUri: pubUnpaidStatus.taler_pay_uri,
@@ -216,9 +224,7 @@ export async function runMerchantExchangeConfusionTest(t: GlobalTestState) {
console.log("requesting", orderUrlWithHash.href);
- publicOrderStatusResp = await axios.get(orderUrlWithHash.href, {
- validateStatus: () => true,
- });
+ publicOrderStatusResp = await harnessHttpLib.fetch(orderUrlWithHash.href);
if (publicOrderStatusResp.status != 402) {
throw Error(
@@ -227,15 +233,12 @@ export async function runMerchantExchangeConfusionTest(t: GlobalTestState) {
}
pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode(
- publicOrderStatusResp.data,
+ await publicOrderStatusResp.json(),
);
- const confirmPayRes = await wallet.client.call(
- WalletApiOperation.ConfirmPay,
- {
- proposalId: proposalId,
- },
- );
+ const confirmPayRes = await walletClient.call(WalletApiOperation.ConfirmPay, {
+ proposalId: proposalId,
+ });
t.assertTrue(confirmPayRes.type === ConfirmPayResultType.Done);
}
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-merchant-instances-delete.ts b/packages/taler-harness/src/integrationtests/test-merchant-instances-delete.ts
index 09231cdd8..c0c9353e4 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-merchant-instances-delete.ts
+++ b/packages/taler-harness/src/integrationtests/test-merchant-instances-delete.ts
@@ -17,16 +17,14 @@
/**
* Imports.
*/
-import { URL } from "@gnu-taler/taler-util";
-import axiosImp from "axios";
-const axios = axiosImp.default;
+import { MerchantApiClient, TalerError, URL } from "@gnu-taler/taler-util";
import {
ExchangeService,
GlobalTestState,
- MerchantApiClient,
MerchantService,
+ generateRandomPayto,
+ harnessHttpLib,
setupDb,
- getPayto,
} from "../harness/harness.js";
/**
@@ -61,39 +59,45 @@ export async function runMerchantInstancesDeleteTest(t: GlobalTestState) {
const baseUrl = merchant.makeInstanceBaseUrl();
{
- const r = await axios.get(new URL("config", baseUrl).href);
- console.log(r.data);
- t.assertDeepEqual(r.data.currency, "TESTKUDOS");
+ 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 axios.get(new URL("management/instances", baseUrl).href);
- t.assertDeepEqual(r.data.instances, []);
+ 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.addInstance({
+ await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [getPayto("merchant-default")],
+ paytoUris: [generateRandomPayto("merchant-default")],
auth: {
method: "external",
},
});
// Add an instance, no auth!
- await merchant.addInstance({
+ await merchant.addInstanceWithWireAccount({
id: "myinst",
name: "Second Instance",
- paytoUris: [getPayto("merchant-default")],
+ paytoUris: [generateRandomPayto("merchant-default")],
auth: {
method: "external",
},
});
let merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl(), {
- method: "external",
+ auth: {
+ method: "external",
+ },
});
await merchantClient.changeAuth({
@@ -102,8 +106,10 @@ export async function runMerchantInstancesDeleteTest(t: GlobalTestState) {
});
merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl(), {
- method: "token",
- token: "secret-token:foobar",
+ auth: {
+ method: "token",
+ token: "secret-token:foobar",
+ },
});
// Check that deleting an instance checks the auth
@@ -112,8 +118,10 @@ export async function runMerchantInstancesDeleteTest(t: GlobalTestState) {
const unauthMerchantClient = new MerchantApiClient(
merchant.makeInstanceBaseUrl(),
{
- method: "token",
- token: "secret-token:invalid",
+ auth: {
+ method: "token",
+ token: "secret-token:invalid",
+ },
},
);
@@ -121,8 +129,8 @@ export async function runMerchantInstancesDeleteTest(t: GlobalTestState) {
await unauthMerchantClient.deleteInstance("myinst");
});
console.log("Got expected exception", exc);
- t.assertAxiosError(exc);
- t.assertDeepEqual(exc.response?.status, 401);
+ t.assertTrue(exc instanceof TalerError);
+ t.assertDeepEqual(exc.errorDetail.httpStatusCode, 401);
}
}
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-merchant-instances-urls.ts b/packages/taler-harness/src/integrationtests/test-merchant-instances-urls.ts
index a4e44c7f3..b631ea1a4 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-merchant-instances-urls.ts
+++ b/packages/taler-harness/src/integrationtests/test-merchant-instances-urls.ts
@@ -17,24 +17,19 @@
/**
* Imports.
*/
-import { Duration } from "@gnu-taler/taler-util";
-import axiosImp from "axios";
-const axios = axiosImp.default;
+import { Duration, MerchantApiClient } from "@gnu-taler/taler-util";
import {
ExchangeService,
GlobalTestState,
- MerchantApiClient,
MerchantService,
+ harnessHttpLib,
setupDb,
- getPayto,
} from "../harness/harness.js";
/**
* Do basic checks on instance management and authentication.
*/
export async function runMerchantInstancesUrlsTest(t: GlobalTestState) {
- // Set up test environment
-
const db = await setupDb(t);
const exchange = ExchangeService.create(t, {
@@ -59,26 +54,25 @@ export async function runMerchantInstancesUrlsTest(t: GlobalTestState) {
const clientForDefault = new MerchantApiClient(
merchant.makeInstanceBaseUrl(),
{
- method: "token",
- token: "secret-token:i-am-default",
+ auth: {
+ method: "token",
+ token: "secret-token:i-am-default",
+ },
},
);
await clientForDefault.createInstance({
id: "default",
address: {},
- default_max_deposit_fee: "TESTKUDOS:1",
- default_max_wire_fee: "TESTKUDOS:1",
+ use_stefan: true,
default_pay_delay: Duration.toTalerProtocolDuration(
Duration.fromSpec({ seconds: 60 }),
),
- default_wire_fee_amortization: 1,
default_wire_transfer_delay: Duration.toTalerProtocolDuration(
Duration.fromSpec({ seconds: 60 }),
),
jurisdiction: {},
name: "My Default Instance",
- payto_uris: [getPayto("bar")],
auth: {
method: "token",
token: "secret-token:i-am-default",
@@ -88,18 +82,15 @@ export async function runMerchantInstancesUrlsTest(t: GlobalTestState) {
await clientForDefault.createInstance({
id: "myinst",
address: {},
- default_max_deposit_fee: "TESTKUDOS:1",
- default_max_wire_fee: "TESTKUDOS:1",
default_pay_delay: Duration.toTalerProtocolDuration(
Duration.fromSpec({ seconds: 60 }),
),
- default_wire_fee_amortization: 1,
+ use_stefan: true,
default_wire_transfer_delay: Duration.toTalerProtocolDuration(
Duration.fromSpec({ seconds: 60 }),
),
jurisdiction: {},
name: "My Second Instance",
- payto_uris: [getPayto("bar")],
auth: {
method: "token",
token: "secret-token:i-am-myinst",
@@ -107,11 +98,10 @@ export async function runMerchantInstancesUrlsTest(t: GlobalTestState) {
});
async function check(url: string, token: string, expectedStatus: number) {
- const resp = await axios.get(url, {
+ const resp = await harnessHttpLib.fetch(url, {
headers: {
Authorization: `Bearer ${token}`,
},
- validateStatus: () => true,
});
console.log(
`checking ${url}, expected ${expectedStatus}, got ${resp.status}`,
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-merchant-instances.ts b/packages/taler-harness/src/integrationtests/test-merchant-instances.ts
index 3efe83241..188451e15 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-merchant-instances.ts
+++ b/packages/taler-harness/src/integrationtests/test-merchant-instances.ts
@@ -17,16 +17,14 @@
/**
* Imports.
*/
-import { URL } from "@gnu-taler/taler-util";
-import axiosImp from "axios";
-const axios = axiosImp.default;
+import { MerchantApiClient, URL } from "@gnu-taler/taler-util";
import {
ExchangeService,
GlobalTestState,
- MerchantApiClient,
MerchantService,
+ generateRandomPayto,
+ harnessHttpLib,
setupDb,
- getPayto
} from "../harness/harness.js";
/**
@@ -61,39 +59,55 @@ export async function runMerchantInstancesTest(t: GlobalTestState) {
const baseUrl = merchant.makeInstanceBaseUrl();
{
- const r = await axios.get(new URL("config", baseUrl).href);
- console.log(r.data);
- t.assertDeepEqual(r.data.currency, "TESTKUDOS");
+ 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 axios.get(new URL("management/instances", baseUrl).href);
- t.assertDeepEqual(r.data.instances, []);
+ 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.addInstance({
+ await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [getPayto("merchant-default")],
+ paytoUris: [generateRandomPayto("merchant-default")],
+ auth: {
+ method: "external",
+ },
+ });
+
+ // Add it again, should be idempotent
+ await merchant.addInstanceWithWireAccount({
+ id: "default",
+ name: "Default Instance",
+ paytoUris: [generateRandomPayto("merchant-default")],
auth: {
method: "external",
},
});
// Add an instance, no auth!
- await merchant.addInstance({
+ await merchant.addInstanceWithWireAccount({
id: "myinst",
name: "Second Instance",
- paytoUris: [getPayto("merchant-default")],
+ paytoUris: [generateRandomPayto("merchant-default")],
auth: {
method: "external",
},
});
let merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl(), {
- method: "external",
+ auth: {
+ method: "external",
+ },
});
{
@@ -104,11 +118,14 @@ export async function runMerchantInstancesTest(t: GlobalTestState) {
// Check that a "malformed" bearer Authorization header gets ignored
{
const url = merchant.makeInstanceBaseUrl();
- const resp = await axios.get(new URL("management/instances", url).href, {
- headers: {
- Authorization: "foo bar-baz",
+ const resp = await harnessHttpLib.fetch(
+ new URL("management/instances", url).href,
+ {
+ headers: {
+ Authorization: "foo bar-baz",
+ },
},
- });
+ );
t.assertDeepEqual(resp.status, 200);
}
@@ -130,13 +147,13 @@ export async function runMerchantInstancesTest(t: GlobalTestState) {
});
console.log(exc);
-
- t.assertAxiosError(exc);
- t.assertTrue(exc.response?.status === 401);
+ t.assertTrue(exc.errorDetail.httpStatusCode === 401);
merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl(), {
- method: "token",
- token: "secret-token:foobar",
+ auth: {
+ method: "token",
+ token: "secret-token:foobar",
+ },
});
// With the new client auth settings, request should work again.
@@ -145,12 +162,15 @@ export async function runMerchantInstancesTest(t: GlobalTestState) {
// Now, try some variations.
{
const url = merchant.makeInstanceBaseUrl();
- const resp = await axios.get(new URL("management/instances", url).href, {
- headers: {
- // Note the spaces
- Authorization: "Bearer secret-token:foobar",
+ const resp = await harnessHttpLib.fetch(
+ new URL("management/instances", url).href,
+ {
+ headers: {
+ // Note the spaces
+ Authorization: "Bearer secret-token:foobar",
+ },
},
- });
+ );
t.assertDeepEqual(resp.status, 200);
}
@@ -168,7 +188,9 @@ export async function runMerchantInstancesTest(t: GlobalTestState) {
const unauthMerchantClient = new MerchantApiClient(
merchant.makeInstanceBaseUrl(),
{
- method: "external",
+ auth: {
+ method: "external",
+ },
},
);
@@ -176,8 +198,7 @@ export async function runMerchantInstancesTest(t: GlobalTestState) {
await unauthMerchantClient.deleteInstance("myinst");
});
console.log(exc);
- t.assertAxiosError(exc);
- t.assertDeepEqual(exc.response?.status, 401);
+ t.assertTrue(exc.errorDetail.httpStatusCode === 401);
}
}
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-merchant-longpolling.ts b/packages/taler-harness/src/integrationtests/test-merchant-longpolling.ts
index 4b9f53f05..bd63a8445 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-merchant-longpolling.ts
+++ b/packages/taler-harness/src/integrationtests/test-merchant-longpolling.ts
@@ -17,34 +17,40 @@
/**
* Imports.
*/
-import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js";
-import { createSimpleTestkudosEnvironment, withdrawViaBank } from "../harness/helpers.js";
import {
- PreparePayResultType,
- codecForMerchantOrderStatusUnpaid,
ConfirmPayResultType,
+ MerchantApiClient,
+ PreparePayResultType,
URL,
+ codecForMerchantOrderStatusUnpaid,
} from "@gnu-taler/taler-util";
-import axiosImp from "axios";
-const axios = axiosImp.default;
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState, harnessHttpLib } from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironmentV2,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
/**
* Run test for basic, bank-integrated withdrawal.
*/
export async function runMerchantLongpollingTest(t: GlobalTestState) {
// Set up test environment
+ const { walletClient, bank, exchange, merchant } =
+ await createSimpleTestkudosEnvironmentV2(t);
- const {
- wallet,
+ // Withdraw digital cash into the wallet.
+
+ const wres = await withdrawViaBankV2(t, {
+ walletClient,
bank,
exchange,
- merchant,
- } = await createSimpleTestkudosEnvironment(t);
+ amount: "TESTKUDOS:20",
+ });
- // Withdraw digital cash into the wallet.
+ await wres.withdrawalFinishedCond;
- await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
+ const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl());
/**
* =========================================================================
@@ -55,7 +61,7 @@ export async function runMerchantLongpollingTest(t: GlobalTestState) {
* =========================================================================
*/
- let orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
+ let orderResp = await merchantClient.createOrder({
order: {
summary: "Buy me!",
amount: "TESTKUDOS:5",
@@ -64,7 +70,7 @@ export async function runMerchantLongpollingTest(t: GlobalTestState) {
create_token: false,
});
- let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ let orderStatus = await merchantClient.queryPrivateOrderStatus({
orderId: orderResp.order_id,
sessionId: "mysession-one",
});
@@ -77,9 +83,9 @@ export async function runMerchantLongpollingTest(t: GlobalTestState) {
// First, request order status without longpolling
{
console.log("requesting", publicOrderStatusUrl.href);
- let publicOrderStatusResp = await axios.get(publicOrderStatusUrl.href, {
- validateStatus: () => true,
- });
+ let publicOrderStatusResp = await harnessHttpLib.fetch(
+ publicOrderStatusUrl.href,
+ );
if (publicOrderStatusResp.status != 402) {
throw Error(
@@ -92,9 +98,9 @@ export async function runMerchantLongpollingTest(t: GlobalTestState) {
publicOrderStatusUrl.searchParams.set("timeout_ms", "500");
console.log("requesting", publicOrderStatusUrl.href);
- let publicOrderStatusResp = await axios.get(publicOrderStatusUrl.href, {
- validateStatus: () => true,
- });
+ let publicOrderStatusResp = await harnessHttpLib.fetch(
+ publicOrderStatusUrl.href,
+ );
if (publicOrderStatusResp.status != 402) {
throw Error(
@@ -103,7 +109,7 @@ export async function runMerchantLongpollingTest(t: GlobalTestState) {
}
let pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode(
- publicOrderStatusResp.data,
+ await publicOrderStatusResp.json(),
);
console.log(pubUnpaidStatus);
@@ -114,7 +120,7 @@ export async function runMerchantLongpollingTest(t: GlobalTestState) {
* =========================================================================
*/
- let preparePayResp = await wallet.client.call(
+ let preparePayResp = await walletClient.call(
WalletApiOperation.PreparePayForUri,
{
talerPayUri: pubUnpaidStatus.taler_pay_uri,
@@ -129,9 +135,9 @@ export async function runMerchantLongpollingTest(t: GlobalTestState) {
preparePayResp.contractTermsHash,
);
- let publicOrderStatusPromise = axios.get(publicOrderStatusUrl.href, {
- validateStatus: () => true,
- });
+ let publicOrderStatusPromise = harnessHttpLib.fetch(
+ publicOrderStatusUrl.href,
+ );
t.assertTrue(preparePayResp.status === PreparePayResultType.PaymentPossible);
@@ -146,15 +152,12 @@ export async function runMerchantLongpollingTest(t: GlobalTestState) {
}
pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode(
- publicOrderStatusResp.data,
+ await publicOrderStatusResp.json(),
);
- const confirmPayRes = await wallet.client.call(
- WalletApiOperation.ConfirmPay,
- {
- proposalId: proposalId,
- },
- );
+ const confirmPayRes = await walletClient.call(WalletApiOperation.ConfirmPay, {
+ proposalId: proposalId,
+ });
t.assertTrue(confirmPayRes.type === ConfirmPayResultType.Done);
}
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-merchant-refund-api.ts b/packages/taler-harness/src/integrationtests/test-merchant-refund-api.ts
index 5d9b23fa7..7ee4c977b 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-merchant-refund-api.ts
+++ b/packages/taler-harness/src/integrationtests/test-merchant-refund-api.ts
@@ -18,53 +18,51 @@
* Imports.
*/
import {
+ Duration,
+ MerchantApiClient,
+ PreparePayResultType,
+ URL,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import {
+ BankServiceHandle,
+ ExchangeServiceInterface,
GlobalTestState,
- MerchantPrivateApi,
MerchantServiceInterface,
- WalletCli,
- ExchangeServiceInterface,
+ WalletClient,
+ harnessHttpLib,
} from "../harness/harness.js";
import {
- createSimpleTestkudosEnvironment,
- withdrawViaBank,
+ createSimpleTestkudosEnvironmentV2,
+ withdrawViaBankV2,
} from "../harness/helpers.js";
-import {
- URL,
- durationFromSpec,
- PreparePayResultType,
- Duration,
-} from "@gnu-taler/taler-util";
-import axiosImp from "axios";
-const axios = axiosImp.default;
-import {
- WalletApiOperation,
- BankServiceHandle,
-} from "@gnu-taler/taler-wallet-core";
async function testRefundApiWithFulfillmentUrl(
t: GlobalTestState,
env: {
merchant: MerchantServiceInterface;
bank: BankServiceHandle;
- wallet: WalletCli;
+ walletClient: WalletClient;
exchange: ExchangeServiceInterface;
},
): Promise<void> {
- const { wallet, bank, exchange, merchant } = env;
+ const { walletClient, merchant } = env;
+
+ const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl());
// Set up order.
- const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
+ const orderResp = await merchantClient.createOrder({
order: {
summary: "Buy me!",
amount: "TESTKUDOS:5",
fulfillment_url: "https://example.com/fulfillment",
},
refund_delay: Duration.toTalerProtocolDuration(
- durationFromSpec({ minutes: 5 }),
+ Duration.fromSpec({ minutes: 5 }),
),
});
- let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ let orderStatus = await merchantClient.queryPrivateOrderStatus({
orderId: orderResp.order_id,
});
@@ -75,7 +73,7 @@ async function testRefundApiWithFulfillmentUrl(
// Make wallet pay for the order
- let preparePayResult = await wallet.client.call(
+ let preparePayResult = await walletClient.call(
WalletApiOperation.PreparePayForUri,
{
talerPayUri,
@@ -86,19 +84,19 @@ async function testRefundApiWithFulfillmentUrl(
preparePayResult.status === PreparePayResultType.PaymentPossible,
);
- await wallet.client.call(WalletApiOperation.ConfirmPay, {
+ await walletClient.call(WalletApiOperation.ConfirmPay, {
proposalId: preparePayResult.proposalId,
});
// Check if payment was successful.
- orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderStatus = await merchantClient.queryPrivateOrderStatus({
orderId: orderResp.order_id,
});
t.assertTrue(orderStatus.order_status === "paid");
- preparePayResult = await wallet.client.call(
+ preparePayResult = await walletClient.call(
WalletApiOperation.PreparePayForUri,
{
talerPayUri,
@@ -109,14 +107,14 @@ async function testRefundApiWithFulfillmentUrl(
preparePayResult.status === PreparePayResultType.AlreadyConfirmed,
);
- await MerchantPrivateApi.giveRefund(merchant, {
+ await merchantClient.giveRefund({
amount: "TESTKUDOS:5",
instance: "default",
justification: "foo",
orderId: orderResp.order_id,
});
- orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderStatus = await merchantClient.queryPrivateOrderStatus({
orderId: orderResp.order_id,
});
@@ -136,23 +134,21 @@ async function testRefundApiWithFulfillmentUrl(
preparePayResult.contractTermsHash,
);
- let publicOrderStatusResp = await axios.get(publicOrderStatusUrl.href, {
- validateStatus: () => true,
- });
- console.log(publicOrderStatusResp.data);
+ let publicOrderStatusResp = await harnessHttpLib.fetch(
+ publicOrderStatusUrl.href,
+ );
+ const respData = await publicOrderStatusResp.json();
t.assertTrue(publicOrderStatusResp.status === 200);
- t.assertAmountEquals(publicOrderStatusResp.data.refund_amount, "TESTKUDOS:5");
+ t.assertAmountEquals(respData.refund_amount, "TESTKUDOS:5");
publicOrderStatusUrl = new URL(
`orders/${orderId}`,
merchant.makeInstanceBaseUrl(),
);
console.log(`requesting order status via '${publicOrderStatusUrl.href}'`);
- publicOrderStatusResp = await axios.get(publicOrderStatusUrl.href, {
- validateStatus: () => true,
- });
+ publicOrderStatusResp = await harnessHttpLib.fetch(publicOrderStatusUrl.href);
console.log(publicOrderStatusResp.status);
- console.log(publicOrderStatusResp.data);
+ console.log(await publicOrderStatusResp.json());
// We didn't give any authentication, so we should get a fulfillment URL back
t.assertTrue(publicOrderStatusResp.status === 403);
}
@@ -162,25 +158,27 @@ async function testRefundApiWithFulfillmentMessage(
env: {
merchant: MerchantServiceInterface;
bank: BankServiceHandle;
- wallet: WalletCli;
+ walletClient: WalletClient;
exchange: ExchangeServiceInterface;
},
): Promise<void> {
- const { wallet, bank, exchange, merchant } = env;
+ const { walletClient, merchant } = env;
+
+ const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl());
// Set up order.
- const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
+ const orderResp = await merchantClient.createOrder({
order: {
summary: "Buy me!",
amount: "TESTKUDOS:5",
fulfillment_message: "Thank you for buying foobar",
},
refund_delay: Duration.toTalerProtocolDuration(
- durationFromSpec({ minutes: 5 }),
+ Duration.fromSpec({ minutes: 5 }),
),
});
- let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ let orderStatus = await merchantClient.queryPrivateOrderStatus({
orderId: orderResp.order_id,
});
@@ -191,7 +189,7 @@ async function testRefundApiWithFulfillmentMessage(
// Make wallet pay for the order
- let preparePayResult = await wallet.client.call(
+ let preparePayResult = await walletClient.call(
WalletApiOperation.PreparePayForUri,
{
talerPayUri,
@@ -202,19 +200,19 @@ async function testRefundApiWithFulfillmentMessage(
preparePayResult.status === PreparePayResultType.PaymentPossible,
);
- await wallet.client.call(WalletApiOperation.ConfirmPay, {
+ await walletClient.call(WalletApiOperation.ConfirmPay, {
proposalId: preparePayResult.proposalId,
});
// Check if payment was successful.
- orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderStatus = await merchantClient.queryPrivateOrderStatus({
orderId: orderResp.order_id,
});
t.assertTrue(orderStatus.order_status === "paid");
- preparePayResult = await wallet.client.call(
+ preparePayResult = await walletClient.call(
WalletApiOperation.PreparePayForUri,
{
talerPayUri,
@@ -225,14 +223,14 @@ async function testRefundApiWithFulfillmentMessage(
preparePayResult.status === PreparePayResultType.AlreadyConfirmed,
);
- await MerchantPrivateApi.giveRefund(merchant, {
+ await merchantClient.giveRefund({
amount: "TESTKUDOS:5",
instance: "default",
justification: "foo",
orderId: orderResp.order_id,
});
- orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderStatus = await merchantClient.queryPrivateOrderStatus({
orderId: orderResp.order_id,
});
@@ -252,22 +250,22 @@ async function testRefundApiWithFulfillmentMessage(
preparePayResult.contractTermsHash,
);
- let publicOrderStatusResp = await axios.get(publicOrderStatusUrl.href, {
- validateStatus: () => true,
- });
- console.log(publicOrderStatusResp.data);
+ let publicOrderStatusResp = await harnessHttpLib.fetch(
+ publicOrderStatusUrl.href,
+ );
+ let respData = await publicOrderStatusResp.json();
+ console.log(respData);
t.assertTrue(publicOrderStatusResp.status === 200);
- t.assertAmountEquals(publicOrderStatusResp.data.refund_amount, "TESTKUDOS:5");
+ t.assertAmountEquals(respData.refund_amount, "TESTKUDOS:5");
publicOrderStatusUrl = new URL(
`orders/${orderId}`,
merchant.makeInstanceBaseUrl(),
);
- publicOrderStatusResp = await axios.get(publicOrderStatusUrl.href, {
- validateStatus: () => true,
- });
- console.log(publicOrderStatusResp.data);
+ publicOrderStatusResp = await harnessHttpLib.fetch(publicOrderStatusUrl.href);
+ respData = await publicOrderStatusResp.json();
+ console.log(respData);
// We didn't give any authentication, so we should get a fulfillment URL back
t.assertTrue(publicOrderStatusResp.status === 403);
}
@@ -278,22 +276,28 @@ async function testRefundApiWithFulfillmentMessage(
export async function runMerchantRefundApiTest(t: GlobalTestState) {
// Set up test environment
- const { wallet, bank, exchange, merchant } =
- await createSimpleTestkudosEnvironment(t);
+ const { walletClient, bank, exchange, merchant } =
+ await createSimpleTestkudosEnvironmentV2(t);
// Withdraw digital cash into the wallet.
- await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
+ const wres = await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ });
+ await wres.withdrawalFinishedCond;
await testRefundApiWithFulfillmentUrl(t, {
- wallet,
+ walletClient,
bank,
exchange,
merchant,
});
await testRefundApiWithFulfillmentMessage(t, {
- wallet,
+ walletClient,
bank,
exchange,
merchant,
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-merchant-spec-public-orders.ts b/packages/taler-harness/src/integrationtests/test-merchant-spec-public-orders.ts
index 70edaaf0c..8e664dfa9 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-merchant-spec-public-orders.ts
+++ b/packages/taler-harness/src/integrationtests/test-merchant-spec-public-orders.ts
@@ -19,27 +19,26 @@
*/
import {
ConfirmPayResultType,
+ MerchantApiClient,
PreparePayResultType,
URL,
encodeCrock,
getRandomBytes,
} from "@gnu-taler/taler-util";
-import { NodeHttpLib, WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import {
BankService,
ExchangeService,
GlobalTestState,
- MerchantPrivateApi,
MerchantService,
- WalletCli,
+ harnessHttpLib,
} from "../harness/harness.js";
import {
- createSimpleTestkudosEnvironment,
- withdrawViaBank,
+ createSimpleTestkudosEnvironmentV2,
+ createWalletDaemonWithClient,
+ withdrawViaBankV2,
} from "../harness/helpers.js";
-const httpLib = new NodeHttpLib();
-
interface Context {
merchant: MerchantService;
merchantBaseUrl: string;
@@ -47,16 +46,27 @@ interface Context {
exchange: ExchangeService;
}
+const httpLib = harnessHttpLib;
+
async function testWithClaimToken(
t: GlobalTestState,
c: Context,
): Promise<void> {
- const wallet = new WalletCli(t, "withclaimtoken");
+ const { walletClient } = await createWalletDaemonWithClient(t, {
+ name: "wct",
+ });
const { bank, exchange } = c;
const { merchant, merchantBaseUrl } = c;
- await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
+ const wres = await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ });
+ await wres.withdrawalFinishedCond;
const sessionId = "mysession";
- const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
+ const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl());
+ const orderResp = await merchantClient.createOrder({
order: {
summary: "Buy me!",
amount: "TESTKUDOS:5",
@@ -71,7 +81,7 @@ async function testWithClaimToken(
let talerPayUri: string;
{
- const httpResp = await httpLib.get(
+ const httpResp = await httpLib.fetch(
new URL(`orders/${orderId}`, merchantBaseUrl).href,
);
const r = await httpResp.json();
@@ -82,7 +92,7 @@ async function testWithClaimToken(
{
const url = new URL(`orders/${orderId}`, merchantBaseUrl);
url.searchParams.set("token", claimToken);
- const httpResp = await httpLib.get(url.href);
+ const httpResp = await httpLib.fetch(url.href);
const r = await httpResp.json();
t.assertDeepEqual(httpResp.status, 402);
console.log(r);
@@ -93,7 +103,7 @@ async function testWithClaimToken(
{
const url = new URL(`orders/${orderId}`, merchantBaseUrl);
url.searchParams.set("token", claimToken);
- const httpResp = await httpLib.get(url.href, {
+ const httpResp = await httpLib.fetch(url.href, {
headers: {
Accept: "text/html",
},
@@ -103,7 +113,7 @@ async function testWithClaimToken(
console.log(r);
}
- const preparePayResp = await wallet.client.call(
+ const preparePayResp = await walletClient.call(
WalletApiOperation.PreparePayForUri,
{
talerPayUri,
@@ -119,7 +129,7 @@ async function testWithClaimToken(
const url = new URL(`orders/${orderId}`, merchantBaseUrl);
const hcWrong = encodeCrock(getRandomBytes(64));
url.searchParams.set("h_contract", hcWrong);
- const httpResp = await httpLib.get(url.href);
+ const httpResp = await httpLib.fetch(url.href);
const r = await httpResp.json();
console.log(r);
t.assertDeepEqual(httpResp.status, 403);
@@ -130,7 +140,7 @@ async function testWithClaimToken(
const url = new URL(`orders/${orderId}`, merchantBaseUrl);
const ctWrong = encodeCrock(getRandomBytes(16));
url.searchParams.set("token", ctWrong);
- const httpResp = await httpLib.get(url.href);
+ const httpResp = await httpLib.fetch(url.href);
const r = await httpResp.json();
console.log(r);
t.assertDeepEqual(httpResp.status, 403);
@@ -140,7 +150,7 @@ async function testWithClaimToken(
{
const url = new URL(`orders/${orderId}`, merchantBaseUrl);
url.searchParams.set("token", claimToken);
- const httpResp = await httpLib.get(url.href);
+ const httpResp = await httpLib.fetch(url.href);
const r = await httpResp.json();
console.log(r);
t.assertDeepEqual(httpResp.status, 402);
@@ -150,7 +160,7 @@ async function testWithClaimToken(
{
const url = new URL(`orders/${orderId}`, merchantBaseUrl);
url.searchParams.set("h_contract", contractTermsHash);
- const httpResp = await httpLib.get(url.href);
+ const httpResp = await httpLib.fetch(url.href);
const r = await httpResp.json();
console.log(r);
t.assertDeepEqual(httpResp.status, 402);
@@ -159,25 +169,22 @@ async function testWithClaimToken(
// claimed, unpaid, access without credentials
{
const url = new URL(`orders/${orderId}`, merchantBaseUrl);
- const httpResp = await httpLib.get(url.href);
+ const httpResp = await httpLib.fetch(url.href);
const r = await httpResp.json();
console.log(r);
t.assertDeepEqual(httpResp.status, 202);
}
- const confirmPayRes = await wallet.client.call(
- WalletApiOperation.ConfirmPay,
- {
- proposalId: proposalId,
- },
- );
+ const confirmPayRes = await walletClient.call(WalletApiOperation.ConfirmPay, {
+ proposalId: proposalId,
+ });
t.assertTrue(confirmPayRes.type === ConfirmPayResultType.Done);
// paid, access without credentials
{
const url = new URL(`orders/${orderId}`, merchantBaseUrl);
- const httpResp = await httpLib.get(url.href);
+ const httpResp = await httpLib.fetch(url.href);
const r = await httpResp.json();
console.log(r);
t.assertDeepEqual(httpResp.status, 202);
@@ -188,7 +195,7 @@ async function testWithClaimToken(
const url = new URL(`orders/${orderId}`, merchantBaseUrl);
const hcWrong = encodeCrock(getRandomBytes(64));
url.searchParams.set("h_contract", hcWrong);
- const httpResp = await httpLib.get(url.href);
+ const httpResp = await httpLib.fetch(url.href);
const r = await httpResp.json();
console.log(r);
t.assertDeepEqual(httpResp.status, 403);
@@ -199,7 +206,7 @@ async function testWithClaimToken(
const url = new URL(`orders/${orderId}`, merchantBaseUrl);
const ctWrong = encodeCrock(getRandomBytes(16));
url.searchParams.set("token", ctWrong);
- const httpResp = await httpLib.get(url.href);
+ const httpResp = await httpLib.fetch(url.href);
const r = await httpResp.json();
console.log(r);
t.assertDeepEqual(httpResp.status, 403);
@@ -209,7 +216,7 @@ async function testWithClaimToken(
{
const url = new URL(`orders/${orderId}`, merchantBaseUrl);
url.searchParams.set("h_contract", contractTermsHash);
- const httpResp = await httpLib.get(url.href);
+ const httpResp = await httpLib.fetch(url.href);
const r = await httpResp.json();
console.log(r);
t.assertDeepEqual(httpResp.status, 200);
@@ -219,7 +226,7 @@ async function testWithClaimToken(
{
const url = new URL(`orders/${orderId}`, merchantBaseUrl);
url.searchParams.set("token", claimToken);
- const httpResp = await httpLib.get(url.href);
+ const httpResp = await httpLib.fetch(url.href);
const r = await httpResp.json();
console.log(r);
t.assertDeepEqual(httpResp.status, 200);
@@ -231,13 +238,13 @@ async function testWithClaimToken(
{
const url = new URL(`orders/${orderId}`, merchantBaseUrl);
url.searchParams.set("token", claimToken);
- const httpResp = await httpLib.get(url.href, {
+ const httpResp = await httpLib.fetch(url.href, {
headers: { Accept: "text/html" },
});
t.assertDeepEqual(httpResp.status, 200);
}
- const confirmPayRes2 = await wallet.client.call(
+ const confirmPayRes2 = await walletClient.call(
WalletApiOperation.ConfirmPay,
{
proposalId: proposalId,
@@ -248,18 +255,14 @@ async function testWithClaimToken(
t.assertTrue(confirmPayRes2.type === ConfirmPayResultType.Done);
// Create another order with identical fulfillment URL to test the "already paid" flow
- const alreadyPaidOrderResp = await MerchantPrivateApi.createOrder(
- merchant,
- "default",
- {
- order: {
- summary: "Buy me!",
- amount: "TESTKUDOS:5",
- fulfillment_url: "https://example.com/article42",
- public_reorder_url: "https://example.com/article42-share",
- },
+ const alreadyPaidOrderResp = await merchantClient.createOrder({
+ order: {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "https://example.com/article42",
+ public_reorder_url: "https://example.com/article42-share",
},
- );
+ });
const apOrderId = alreadyPaidOrderResp.order_id;
const apToken = alreadyPaidOrderResp.token;
@@ -268,7 +271,7 @@ async function testWithClaimToken(
{
const url = new URL(`orders/${apOrderId}`, merchantBaseUrl);
url.searchParams.set("token", apToken);
- const httpResp = await httpLib.get(url.href);
+ const httpResp = await httpLib.fetch(url.href);
const r = await httpResp.json();
console.log(r);
t.assertDeepEqual(httpResp.status, 402);
@@ -279,7 +282,7 @@ async function testWithClaimToken(
const url = new URL(`orders/${apOrderId}`, merchantBaseUrl);
url.searchParams.set("token", apToken);
url.searchParams.set("session_id", sessionId);
- const httpResp = await httpLib.get(url.href);
+ const httpResp = await httpLib.fetch(url.href);
const r = await httpResp.json();
console.log(r);
t.assertDeepEqual(httpResp.status, 402);
@@ -292,9 +295,13 @@ async function testWithClaimToken(
const url = new URL(`orders/${apOrderId}`, merchantBaseUrl);
url.searchParams.set("token", apToken);
url.searchParams.set("session_id", sessionId);
- const httpResp = await httpLib.get(url.href, {
+ const httpResp = await httpLib.fetch(url.href, {
headers: { Accept: "text/html" },
+ redirect: "manual",
});
+ console.log(
+ `requesting GET ${url.href}, expected 302 got ${httpResp.status}`,
+ );
t.assertDeepEqual(httpResp.status, 302);
const location = httpResp.headers.get("Location");
console.log("location header:", location);
@@ -306,12 +313,21 @@ async function testWithoutClaimToken(
t: GlobalTestState,
c: Context,
): Promise<void> {
- const wallet = new WalletCli(t, "withoutct");
+ const { walletClient } = await createWalletDaemonWithClient(t, {
+ name: "wnoct",
+ });
const sessionId = "mysession2";
const { bank, exchange } = c;
const { merchant, merchantBaseUrl } = c;
- await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
- const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
+ const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl());
+ const wres = await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ });
+ await wres.withdrawalFinishedCond;
+ const orderResp = await merchantClient.createOrder({
order: {
summary: "Buy me!",
amount: "TESTKUDOS:5",
@@ -325,7 +341,7 @@ async function testWithoutClaimToken(
let talerPayUri: string;
{
- const httpResp = await httpLib.get(
+ const httpResp = await httpLib.fetch(
new URL(`orders/${orderId}`, merchantBaseUrl).href,
);
const r = await httpResp.json();
@@ -335,7 +351,7 @@ async function testWithoutClaimToken(
{
const url = new URL(`orders/${orderId}`, merchantBaseUrl);
- const httpResp = await httpLib.get(url.href);
+ const httpResp = await httpLib.fetch(url.href);
const r = await httpResp.json();
t.assertDeepEqual(httpResp.status, 402);
console.log(r);
@@ -345,7 +361,7 @@ async function testWithoutClaimToken(
{
const url = new URL(`orders/${orderId}`, merchantBaseUrl);
- const httpResp = await httpLib.get(url.href, {
+ const httpResp = await httpLib.fetch(url.href, {
headers: {
Accept: "text/html",
},
@@ -355,7 +371,7 @@ async function testWithoutClaimToken(
console.log(r);
}
- const preparePayResp = await wallet.client.call(
+ const preparePayResp = await walletClient.call(
WalletApiOperation.PreparePayForUri,
{
talerPayUri,
@@ -373,7 +389,7 @@ async function testWithoutClaimToken(
const url = new URL(`orders/${orderId}`, merchantBaseUrl);
const hcWrong = encodeCrock(getRandomBytes(64));
url.searchParams.set("h_contract", hcWrong);
- const httpResp = await httpLib.get(url.href);
+ const httpResp = await httpLib.fetch(url.href);
const r = await httpResp.json();
console.log(r);
t.assertDeepEqual(httpResp.status, 403);
@@ -384,7 +400,7 @@ async function testWithoutClaimToken(
const url = new URL(`orders/${orderId}`, merchantBaseUrl);
const ctWrong = encodeCrock(getRandomBytes(16));
url.searchParams.set("token", ctWrong);
- const httpResp = await httpLib.get(url.href);
+ const httpResp = await httpLib.fetch(url.href);
const r = await httpResp.json();
console.log(r);
t.assertDeepEqual(httpResp.status, 403);
@@ -393,7 +409,7 @@ async function testWithoutClaimToken(
// claimed, unpaid, no claim token
{
const url = new URL(`orders/${orderId}`, merchantBaseUrl);
- const httpResp = await httpLib.get(url.href);
+ const httpResp = await httpLib.fetch(url.href);
const r = await httpResp.json();
console.log(r);
t.assertDeepEqual(httpResp.status, 402);
@@ -403,7 +419,7 @@ async function testWithoutClaimToken(
{
const url = new URL(`orders/${orderId}`, merchantBaseUrl);
url.searchParams.set("h_contract", contractTermsHash);
- const httpResp = await httpLib.get(url.href);
+ const httpResp = await httpLib.fetch(url.href);
const r = await httpResp.json();
console.log(r);
t.assertDeepEqual(httpResp.status, 402);
@@ -412,7 +428,7 @@ async function testWithoutClaimToken(
// claimed, unpaid, access without credentials
{
const url = new URL(`orders/${orderId}`, merchantBaseUrl);
- const httpResp = await httpLib.get(url.href);
+ const httpResp = await httpLib.fetch(url.href);
const r = await httpResp.json();
console.log(r);
// No credentials, but the order doesn't require a claim token.
@@ -421,19 +437,16 @@ async function testWithoutClaimToken(
t.assertDeepEqual(httpResp.status, 402);
}
- const confirmPayRes = await wallet.client.call(
- WalletApiOperation.ConfirmPay,
- {
- proposalId: proposalId,
- },
- );
+ const confirmPayRes = await walletClient.call(WalletApiOperation.ConfirmPay, {
+ proposalId: proposalId,
+ });
t.assertTrue(confirmPayRes.type === ConfirmPayResultType.Done);
// paid, access without credentials
{
const url = new URL(`orders/${orderId}`, merchantBaseUrl);
- const httpResp = await httpLib.get(url.href);
+ const httpResp = await httpLib.fetch(url.href);
const r = await httpResp.json();
console.log(r);
t.assertDeepEqual(httpResp.status, 200);
@@ -444,7 +457,7 @@ async function testWithoutClaimToken(
const url = new URL(`orders/${orderId}`, merchantBaseUrl);
const hcWrong = encodeCrock(getRandomBytes(64));
url.searchParams.set("h_contract", hcWrong);
- const httpResp = await httpLib.get(url.href);
+ const httpResp = await httpLib.fetch(url.href);
const r = await httpResp.json();
console.log(r);
t.assertDeepEqual(httpResp.status, 403);
@@ -455,7 +468,7 @@ async function testWithoutClaimToken(
const url = new URL(`orders/${orderId}`, merchantBaseUrl);
const ctWrong = encodeCrock(getRandomBytes(16));
url.searchParams.set("token", ctWrong);
- const httpResp = await httpLib.get(url.href);
+ const httpResp = await httpLib.fetch(url.href);
const r = await httpResp.json();
console.log(r);
t.assertDeepEqual(httpResp.status, 403);
@@ -465,7 +478,7 @@ async function testWithoutClaimToken(
{
const url = new URL(`orders/${orderId}`, merchantBaseUrl);
url.searchParams.set("h_contract", contractTermsHash);
- const httpResp = await httpLib.get(url.href);
+ const httpResp = await httpLib.fetch(url.href);
const r = await httpResp.json();
console.log(r);
t.assertDeepEqual(httpResp.status, 200);
@@ -474,7 +487,7 @@ async function testWithoutClaimToken(
// paid, JSON
{
const url = new URL(`orders/${orderId}`, merchantBaseUrl);
- const httpResp = await httpLib.get(url.href);
+ const httpResp = await httpLib.fetch(url.href);
const r = await httpResp.json();
console.log(r);
t.assertDeepEqual(httpResp.status, 200);
@@ -485,13 +498,13 @@ async function testWithoutClaimToken(
// paid, HTML
{
const url = new URL(`orders/${orderId}`, merchantBaseUrl);
- const httpResp = await httpLib.get(url.href, {
+ const httpResp = await httpLib.fetch(url.href, {
headers: { Accept: "text/html" },
});
t.assertDeepEqual(httpResp.status, 200);
}
- const confirmPayRes2 = await wallet.client.call(
+ const confirmPayRes2 = await walletClient.call(
WalletApiOperation.ConfirmPay,
{
proposalId: proposalId,
@@ -502,18 +515,14 @@ async function testWithoutClaimToken(
t.assertTrue(confirmPayRes2.type === ConfirmPayResultType.Done);
// Create another order with identical fulfillment URL to test the "already paid" flow
- const alreadyPaidOrderResp = await MerchantPrivateApi.createOrder(
- merchant,
- "default",
- {
- order: {
- summary: "Buy me!",
- amount: "TESTKUDOS:5",
- fulfillment_url: "https://example.com/article42",
- public_reorder_url: "https://example.com/article42-share",
- },
+ const alreadyPaidOrderResp = await merchantClient.createOrder({
+ order: {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "https://example.com/article42",
+ public_reorder_url: "https://example.com/article42-share",
},
- );
+ });
const apOrderId = alreadyPaidOrderResp.order_id;
const apToken = alreadyPaidOrderResp.token;
@@ -522,7 +531,7 @@ async function testWithoutClaimToken(
{
const url = new URL(`orders/${apOrderId}`, merchantBaseUrl);
url.searchParams.set("token", apToken);
- const httpResp = await httpLib.get(url.href);
+ const httpResp = await httpLib.fetch(url.href);
const r = await httpResp.json();
console.log(r);
t.assertDeepEqual(httpResp.status, 402);
@@ -533,7 +542,7 @@ async function testWithoutClaimToken(
const url = new URL(`orders/${apOrderId}`, merchantBaseUrl);
url.searchParams.set("token", apToken);
url.searchParams.set("session_id", sessionId);
- const httpResp = await httpLib.get(url.href);
+ const httpResp = await httpLib.fetch(url.href);
const r = await httpResp.json();
console.log(r);
t.assertDeepEqual(httpResp.status, 402);
@@ -546,8 +555,9 @@ async function testWithoutClaimToken(
const url = new URL(`orders/${apOrderId}`, merchantBaseUrl);
url.searchParams.set("token", apToken);
url.searchParams.set("session_id", sessionId);
- const httpResp = await httpLib.get(url.href, {
+ const httpResp = await httpLib.fetch(url.href, {
headers: { Accept: "text/html" },
+ redirect: "manual",
});
t.assertDeepEqual(httpResp.status, 302);
const location = httpResp.headers.get("Location");
@@ -563,22 +573,23 @@ async function testWithoutClaimToken(
* specification of the endpoint.
*/
export async function runMerchantSpecPublicOrdersTest(t: GlobalTestState) {
- const { bank, exchange, merchant } = await createSimpleTestkudosEnvironment(
- t,
- );
+ const { bank, exchange, merchant } =
+ await createSimpleTestkudosEnvironmentV2(t);
// Base URL for the default instance.
const merchantBaseUrl = merchant.makeInstanceBaseUrl();
{
- const httpResp = await httpLib.get(new URL("config", merchantBaseUrl).href);
+ const httpResp = await httpLib.fetch(
+ new URL("config", merchantBaseUrl).href,
+ );
const r = await httpResp.json();
console.log(r);
t.assertDeepEqual(r.currency, "TESTKUDOS");
}
{
- const httpResp = await httpLib.get(
+ const httpResp = await httpLib.fetch(
new URL("orders/foo", merchantBaseUrl).href,
);
const r = await httpResp.json();
@@ -588,7 +599,7 @@ export async function runMerchantSpecPublicOrdersTest(t: GlobalTestState) {
}
{
- const httpResp = await httpLib.get(
+ const httpResp = await httpLib.fetch(
new URL("orders/foo", merchantBaseUrl).href,
{
headers: {
diff --git a/packages/taler-harness/src/integrationtests/test-multiexchange.ts b/packages/taler-harness/src/integrationtests/test-multiexchange.ts
new file mode 100644
index 000000000..e27bccc46
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-multiexchange.ts
@@ -0,0 +1,172 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import { Duration } from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { defaultCoinConfig } from "../harness/denomStructures.js";
+import {
+ BankService,
+ ExchangeService,
+ GlobalTestState,
+ MerchantService,
+ generateRandomPayto,
+ setupDb,
+} from "../harness/harness.js";
+import {
+ createWalletDaemonWithClient,
+ makeTestPaymentV2,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
+
+/**
+ * Run test for basic, bank-integrated withdrawal and payment.
+ */
+export async function runMultiExchangeTest(t: GlobalTestState) {
+ // Set up test environment
+ const dbDefault = await setupDb(t);
+
+ const dbExchangeTwo = await setupDb(t, {
+ nameSuffix: "exchange2",
+ });
+
+ const bank = await BankService.create(t, {
+ allowRegistrations: true,
+ currency: "TESTKUDOS",
+ database: dbDefault.connStr,
+ httpPort: 8082,
+ });
+
+ const exchangeOne = ExchangeService.create(t, {
+ name: "testexchange-1",
+ currency: "TESTKUDOS",
+ httpPort: 8081,
+ database: dbDefault.connStr,
+ });
+
+ const exchangeTwo = ExchangeService.create(t, {
+ name: "testexchange-2",
+ currency: "TESTKUDOS",
+ httpPort: 8281,
+ database: dbExchangeTwo.connStr,
+ });
+
+ const merchant = await MerchantService.create(t, {
+ name: "testmerchant-1",
+ currency: "TESTKUDOS",
+ httpPort: 8083,
+ database: dbDefault.connStr,
+ });
+
+ const exchangeOneBankAccount = await bank.createExchangeAccount(
+ "myexchange",
+ "x",
+ );
+ await exchangeOne.addBankAccount("1", exchangeOneBankAccount);
+
+ const exchangeTwoBankAccount = await bank.createExchangeAccount(
+ "myexchange2",
+ "x",
+ );
+ await exchangeTwo.addBankAccount("1", exchangeTwoBankAccount);
+
+ bank.setSuggestedExchange(
+ exchangeOne,
+ exchangeOneBankAccount.accountPaytoUri,
+ );
+
+ await bank.start();
+
+ await bank.pingUntilAvailable();
+
+ // Set up the first exchange
+
+ exchangeOne.addOfferedCoins(defaultCoinConfig);
+ await exchangeOne.start();
+ await exchangeOne.pingUntilAvailable();
+
+ // Set up the second exchange
+
+ exchangeTwo.addOfferedCoins(defaultCoinConfig);
+ await exchangeTwo.start();
+ await exchangeTwo.pingUntilAvailable();
+
+ // Start and configure merchant
+
+ merchant.addExchange(exchangeOne);
+ merchant.addExchange(exchangeTwo);
+
+ await merchant.start();
+ await merchant.pingUntilAvailable();
+
+ await merchant.addInstanceWithWireAccount({
+ id: "default",
+ name: "Default Instance",
+ paytoUris: [generateRandomPayto("merchant-default")],
+ defaultWireTransferDelay: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ minutes: 1 }),
+ ),
+ });
+
+ await merchant.addInstanceWithWireAccount({
+ id: "minst1",
+ name: "minst1",
+ paytoUris: [generateRandomPayto("minst1")],
+ defaultWireTransferDelay: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ minutes: 1 }),
+ ),
+ });
+
+ const { walletClient, walletService } = await createWalletDaemonWithClient(
+ t,
+ { name: "wallet" },
+ );
+
+ console.log("setup done!");
+
+ // Withdraw digital cash into the wallet.
+
+ await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange: exchangeOne,
+ amount: "TESTKUDOS:6",
+ });
+
+ await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange: exchangeTwo,
+ amount: "TESTKUDOS:6",
+ });
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+
+ const order = {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:10",
+ fulfillment_url: "taler://fulfillment-success/thx",
+ };
+
+ console.log("making test payment");
+
+ await makeTestPaymentV2(t, { walletClient, merchant, order });
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+}
+
+runMultiExchangeTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-otp.ts b/packages/taler-harness/src/integrationtests/test-otp.ts
new file mode 100644
index 000000000..d0aeba095
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-otp.ts
@@ -0,0 +1,119 @@
+/*
+ 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 {
+ ConfirmPayResultType,
+ Duration,
+ MerchantApiClient,
+ PreparePayResultType,
+ TransactionType,
+ j2s,
+ narrowOpSuccessOrThrow,
+ randomRfc3548Base32Key,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState } from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironmentV2,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
+
+/**
+ * Run test for basic, bank-integrated withdrawal and payment.
+ */
+export async function runOtpTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { walletClient, bank, exchange, merchant } =
+ await createSimpleTestkudosEnvironmentV2(t);
+
+ const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl());
+ const createOtpRes = await merchantClient.createOtpDevice({
+ otp_algorithm: 1,
+ otp_device_description: "Hello",
+ otp_device_id: "mydevice",
+ otp_key: randomRfc3548Base32Key(),
+ });
+ narrowOpSuccessOrThrow("createOtpDevice", createOtpRes);
+
+ const createTemplateRes = await merchantClient.createTemplate({
+ template_description: "my template",
+ template_id: "tpl1",
+ otp_id: "mydevice",
+ template_contract: {
+ summary: "test",
+ amount: "TESTKUDOS:1",
+ minimum_age: 0,
+ pay_duration: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ hours: 1 }),
+ ),
+ },
+ });
+ narrowOpSuccessOrThrow("createTemplate", createTemplateRes);
+
+ const getTemplateResp = await merchantClient.getTemplate("tpl1");
+ narrowOpSuccessOrThrow("getTemplate", getTemplateResp);
+
+ console.log(`template: ${j2s(getTemplateResp.body)}`);
+
+ const wres = await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ });
+ await wres.withdrawalFinishedCond;
+
+ const preparePayResult = await walletClient.call(
+ WalletApiOperation.PreparePayForTemplate,
+ {
+ talerPayTemplateUri: `taler+http://pay-template/localhost:${merchant.port}/tpl1`,
+ templateParams: {},
+ },
+ );
+
+ console.log(preparePayResult);
+
+ t.assertTrue(
+ preparePayResult.status === PreparePayResultType.PaymentPossible,
+ );
+
+ // Pay for it
+
+ const r2 = await walletClient.call(WalletApiOperation.ConfirmPay, {
+ transactionId: preparePayResult.transactionId,
+ });
+
+ t.assertTrue(r2.type === ConfirmPayResultType.Done);
+
+ const transaction = await walletClient.call(
+ WalletApiOperation.GetTransactionById,
+ {
+ transactionId: preparePayResult.transactionId,
+ },
+ );
+
+ console.log(j2s(transaction));
+
+ t.assertTrue(transaction.type === TransactionType.Payment);
+ t.assertTrue(transaction.posConfirmation != null);
+ t.assertTrue(transaction.posConfirmation.length > 10);
+}
+
+runOtpTest.suites = ["wallet"];
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-pay-paid.ts b/packages/taler-harness/src/integrationtests/test-pay-paid.ts
index 2ef91e4a8..3d93f6e29 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-pay-paid.ts
+++ b/packages/taler-harness/src/integrationtests/test-pay-paid.ts
@@ -17,21 +17,20 @@
/**
* Imports.
*/
-import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js";
import {
- withdrawViaBank,
- createFaultInjectedMerchantTestkudosEnvironment,
-} from "../harness/helpers.js";
-import {
- PreparePayResultType,
- codecForMerchantOrderStatusUnpaid,
ConfirmPayResultType,
+ MerchantApiClient,
+ PreparePayResultType,
URL,
+ codecForMerchantOrderStatusUnpaid,
} from "@gnu-taler/taler-util";
-import axiosImp from "axios";
-const axios = axiosImp.default;
-import { FaultInjectionRequestContext } from "../harness/faultInjection.js";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { FaultInjectionRequestContext } from "../harness/faultInjection.js";
+import { GlobalTestState, harnessHttpLib } from "../harness/harness.js";
+import {
+ createFaultInjectedMerchantTestkudosEnvironment,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
/**
* Run test for the wallets repurchase detection mechanism
@@ -44,18 +43,20 @@ import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
export async function runPayPaidTest(t: GlobalTestState) {
// Set up test environment
- const { wallet, bank, faultyExchange, faultyMerchant } =
+ const { walletClient, bank, faultyExchange, faultyMerchant } =
await createFaultInjectedMerchantTestkudosEnvironment(t);
// Withdraw digital cash into the wallet.
- await withdrawViaBank(t, {
- wallet,
+ const wres = await withdrawViaBankV2(t, {
+ walletClient,
bank,
exchange: faultyExchange,
amount: "TESTKUDOS:20",
});
+ await wres.withdrawalFinishedCond;
+
/**
* =========================================================================
* Create an order and let the wallet pay under a session ID
@@ -67,7 +68,9 @@ export async function runPayPaidTest(t: GlobalTestState) {
const merchant = faultyMerchant;
- let orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
+ const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl());
+
+ let orderResp = await merchantClient.createOrder({
order: {
summary: "Buy me!",
amount: "TESTKUDOS:5",
@@ -76,7 +79,7 @@ export async function runPayPaidTest(t: GlobalTestState) {
},
});
- let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ let orderStatus = await merchantClient.queryPrivateOrderStatus({
orderId: orderResp.order_id,
sessionId: "mysession-one",
});
@@ -86,9 +89,7 @@ export async function runPayPaidTest(t: GlobalTestState) {
t.assertTrue(orderStatus.already_paid_order_id === undefined);
let publicOrderStatusUrl = orderStatus.order_status_url;
- let publicOrderStatusResp = await axios.get(publicOrderStatusUrl, {
- validateStatus: () => true,
- });
+ let publicOrderStatusResp = await harnessHttpLib.fetch(publicOrderStatusUrl);
if (publicOrderStatusResp.status != 402) {
throw Error(
@@ -97,12 +98,12 @@ export async function runPayPaidTest(t: GlobalTestState) {
}
let pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode(
- publicOrderStatusResp.data,
+ publicOrderStatusResp.json(),
);
console.log(pubUnpaidStatus);
- let preparePayResp = await wallet.client.call(
+ let preparePayResp = await walletClient.call(
WalletApiOperation.PreparePayForUri,
{
talerPayUri: pubUnpaidStatus.taler_pay_uri,
@@ -113,9 +114,7 @@ export async function runPayPaidTest(t: GlobalTestState) {
const proposalId = preparePayResp.proposalId;
- publicOrderStatusResp = await axios.get(publicOrderStatusUrl, {
- validateStatus: () => true,
- });
+ publicOrderStatusResp = await harnessHttpLib.fetch(publicOrderStatusUrl);
if (publicOrderStatusResp.status != 402) {
throw Error(
@@ -124,26 +123,21 @@ export async function runPayPaidTest(t: GlobalTestState) {
}
pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode(
- publicOrderStatusResp.data,
+ publicOrderStatusResp.json(),
);
- const confirmPayRes = await wallet.client.call(
- WalletApiOperation.ConfirmPay,
- {
- proposalId: proposalId,
- },
- );
+ const confirmPayRes = await walletClient.call(WalletApiOperation.ConfirmPay, {
+ proposalId: proposalId,
+ });
t.assertTrue(confirmPayRes.type === ConfirmPayResultType.Done);
- publicOrderStatusResp = await axios.get(publicOrderStatusUrl, {
- validateStatus: () => true,
- });
+ publicOrderStatusResp = await harnessHttpLib.fetch(publicOrderStatusUrl);
- console.log(publicOrderStatusResp.data);
+ console.log(publicOrderStatusResp.json());
if (publicOrderStatusResp.status != 200) {
- console.log(publicOrderStatusResp.data);
+ console.log(publicOrderStatusResp.json());
throw Error(
`expected status 200 (after paying), but got ${publicOrderStatusResp.status}`,
);
@@ -155,7 +149,7 @@ export async function runPayPaidTest(t: GlobalTestState) {
* =========================================================================
*/
- orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderStatus = await merchantClient.queryPrivateOrderStatus({
orderId: orderResp.order_id,
sessionId: "mysession-two",
});
@@ -182,7 +176,7 @@ export async function runPayPaidTest(t: GlobalTestState) {
},
});
- let orderRespTwo = await MerchantPrivateApi.createOrder(merchant, "default", {
+ let orderRespTwo = await merchantClient.createOrder({
order: {
summary: "Buy me!",
amount: "TESTKUDOS:5",
@@ -191,20 +185,17 @@ export async function runPayPaidTest(t: GlobalTestState) {
},
});
- let orderStatusTwo = await MerchantPrivateApi.queryPrivateOrderStatus(
- merchant,
- {
- orderId: orderRespTwo.order_id,
- sessionId: "mysession-two",
- },
- );
+ let orderStatusTwo = await merchantClient.queryPrivateOrderStatus({
+ orderId: orderRespTwo.order_id,
+ sessionId: "mysession-two",
+ });
t.assertTrue(orderStatusTwo.order_status === "unpaid");
// Pay with new taler://pay URI, which should
// have the new session ID!
// Wallet should now automatically re-play payment.
- preparePayResp = await wallet.client.call(
+ preparePayResp = await walletClient.call(
WalletApiOperation.PreparePayForUri,
{
talerPayUri: orderStatusTwo.taler_pay_uri,
diff --git a/packages/taler-harness/src/integrationtests/test-payment-abort.ts b/packages/taler-harness/src/integrationtests/test-payment-abort.ts
new file mode 100644
index 000000000..ca8384411
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-payment-abort.ts
@@ -0,0 +1,164 @@
+/*
+ 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 {
+ ConfirmPayResultType,
+ MerchantApiClient,
+ PreparePayResultType,
+ TalerErrorCode,
+ TalerErrorDetail,
+ URL,
+ codecForMerchantOrderStatusUnpaid,
+ j2s,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { FaultInjectionRequestContext } from "../harness/faultInjection.js";
+import { GlobalTestState, harnessHttpLib } from "../harness/harness.js";
+import {
+ createFaultInjectedMerchantTestkudosEnvironment,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
+
+export async function runPaymentAbortTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { walletClient, bank, exchange, faultyMerchant, faultyExchange } =
+ await createFaultInjectedMerchantTestkudosEnvironment(t);
+
+ // Withdraw digital cash into the wallet.
+
+ const wres = await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange: faultyExchange,
+ amount: "TESTKUDOS:20",
+ });
+
+ await wres.withdrawalFinishedCond;
+
+ const merchantClient = new MerchantApiClient(
+ faultyMerchant.makeInstanceBaseUrl(),
+ );
+
+ let orderResp = await merchantClient.createOrder({
+ order: {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "https://example.com/article42",
+ public_reorder_url: "https://example.com/article42-share",
+ },
+ });
+
+ let orderStatus = await merchantClient.queryPrivateOrderStatus({
+ orderId: orderResp.order_id,
+ sessionId: "mysession-one",
+ });
+
+ t.assertTrue(orderStatus.order_status === "unpaid");
+
+ t.assertTrue(orderStatus.already_paid_order_id === undefined);
+ let publicOrderStatusUrl = orderStatus.order_status_url;
+
+ let publicOrderStatusResp = await harnessHttpLib.fetch(publicOrderStatusUrl);
+
+ if (publicOrderStatusResp.status != 402) {
+ throw Error(
+ `expected status 402 (before claiming), but got ${publicOrderStatusResp.status}`,
+ );
+ }
+
+ let pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode(
+ publicOrderStatusResp.json(),
+ );
+
+ console.log(pubUnpaidStatus);
+
+ let preparePayResp = await walletClient.call(
+ WalletApiOperation.PreparePayForUri,
+ {
+ talerPayUri: pubUnpaidStatus.taler_pay_uri,
+ },
+ );
+
+ t.assertTrue(preparePayResp.status === PreparePayResultType.PaymentPossible);
+
+ publicOrderStatusResp = await harnessHttpLib.fetch(publicOrderStatusUrl);
+
+ if (publicOrderStatusResp.status != 402) {
+ throw Error(
+ `expected status 402 (after claiming), but got ${publicOrderStatusResp.status}`,
+ );
+ }
+
+ pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode(
+ publicOrderStatusResp.json(),
+ );
+
+ faultyMerchant.faultProxy.addFault({
+ async modifyRequest(ctx: FaultInjectionRequestContext) {
+ const url = new URL(ctx.requestUrl);
+ if (!url.pathname.endsWith("/pay")) {
+ return;
+ }
+ ctx.dropRequest = true;
+ const err: TalerErrorDetail = {
+ code: TalerErrorCode.GENERIC_CONFIGURATION_INVALID,
+ hint: "something went wrong",
+ };
+ ctx.substituteResponseStatusCode = 404;
+ ctx.substituteResponseBody = Buffer.from(JSON.stringify(err));
+ console.log("injecting pay fault");
+ },
+ });
+
+ const confirmPayResp = await walletClient.call(
+ WalletApiOperation.ConfirmPay,
+ {
+ transactionId: preparePayResp.transactionId,
+ },
+ );
+
+ // Can't have succeeded yet, but network error results in "pending" state.
+ t.assertDeepEqual(confirmPayResp.type, ConfirmPayResultType.Pending);
+
+ const txns = await walletClient.call(WalletApiOperation.GetTransactions, {
+ sort: "stable-ascending",
+ });
+ console.log(j2s(txns));
+
+ await walletClient.call(WalletApiOperation.AbortTransaction, {
+ transactionId: txns.transactions[1].transactionId,
+ });
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+
+ const txns2 = await walletClient.call(WalletApiOperation.GetTransactions, {
+ sort: "stable-ascending",
+ });
+ console.log(j2s(txns2));
+
+ const txTypes = txns2.transactions.map((x) => x.type);
+ console.log(txTypes);
+ t.assertDeepEqual(txTypes, ["withdrawal", "payment", "refund"]);
+
+ // FIXME: also check extended transaction list for refresh.
+ // FIXME: also check balance
+}
+
+runPaymentAbortTest.suites = ["wallet"];
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-payment-claim.ts b/packages/taler-harness/src/integrationtests/test-payment-claim.ts
index e93d2c44c..3595a1750 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-payment-claim.ts
+++ b/packages/taler-harness/src/integrationtests/test-payment-claim.ts
@@ -18,36 +18,45 @@
* Imports.
*/
import {
- GlobalTestState,
- MerchantPrivateApi,
- WalletCli,
-} from "../harness/harness.js";
+ MerchantApiClient,
+ PreparePayResultType,
+ TalerErrorCode,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState } from "../harness/harness.js";
import {
- createSimpleTestkudosEnvironment,
- withdrawViaBank,
+ createSimpleTestkudosEnvironmentV2,
+ createWalletDaemonWithClient,
+ withdrawViaBankV2,
} from "../harness/helpers.js";
-import { PreparePayResultType } from "@gnu-taler/taler-util";
-import { TalerErrorCode } from "@gnu-taler/taler-util";
-import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
/**
- * Run test for basic, bank-integrated withdrawal.
+ * Run test where a wallet tries to claim an already claimed order.
*/
export async function runPaymentClaimTest(t: GlobalTestState) {
// Set up test environment
- const { wallet, bank, exchange, merchant } =
- await createSimpleTestkudosEnvironment(t);
+ const { walletClient, bank, exchange, merchant } =
+ await createSimpleTestkudosEnvironmentV2(t);
+
+ const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl());
- const walletTwo = new WalletCli(t, "two");
+ const w2 = await createWalletDaemonWithClient(t, { name: "w2" });
// Withdraw digital cash into the wallet.
- await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
+ const wres = await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ });
+
+ await wres.withdrawalFinishedCond;
// Set up order.
- const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
+ const orderResp = await merchantClient.createOrder({
order: {
summary: "Buy me!",
amount: "TESTKUDOS:5",
@@ -55,7 +64,7 @@ export async function runPaymentClaimTest(t: GlobalTestState) {
},
});
- let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ let orderStatus = await merchantClient.queryPrivateOrderStatus({
orderId: orderResp.order_id,
});
@@ -65,7 +74,7 @@ export async function runPaymentClaimTest(t: GlobalTestState) {
// Make wallet pay for the order
- const preparePayResult = await wallet.client.call(
+ const preparePayResult = await walletClient.call(
WalletApiOperation.PreparePayForUri,
{
talerPayUri,
@@ -76,28 +85,30 @@ export async function runPaymentClaimTest(t: GlobalTestState) {
preparePayResult.status === PreparePayResultType.PaymentPossible,
);
- t.assertThrowsTalerErrorAsync(async () => {
- await walletTwo.client.call(WalletApiOperation.PreparePayForUri, {
+ const errOne = t.assertThrowsTalerErrorAsync(async () => {
+ await w2.walletClient.call(WalletApiOperation.PreparePayForUri, {
talerPayUri,
});
});
- await wallet.client.call(WalletApiOperation.ConfirmPay, {
- proposalId: preparePayResult.proposalId,
+ console.log(errOne);
+
+ await walletClient.call(WalletApiOperation.ConfirmPay, {
+ transactionId: preparePayResult.transactionId,
});
// Check if payment was successful.
- orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderStatus = await merchantClient.queryPrivateOrderStatus({
orderId: orderResp.order_id,
});
t.assertTrue(orderStatus.order_status === "paid");
- walletTwo.deleteDatabase();
+ await w2.walletClient.call(WalletApiOperation.ClearDb, {});
const err = await t.assertThrowsTalerErrorAsync(async () => {
- await walletTwo.client.call(WalletApiOperation.PreparePayForUri, {
+ await w2.walletClient.call(WalletApiOperation.PreparePayForUri, {
talerPayUri,
});
});
diff --git a/packages/taler-harness/src/integrationtests/test-payment-deleted.ts b/packages/taler-harness/src/integrationtests/test-payment-deleted.ts
new file mode 100644
index 000000000..1d25848fd
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-payment-deleted.ts
@@ -0,0 +1,104 @@
+/*
+ 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 {
+ ConfirmPayResultType,
+ MerchantApiClient,
+ PreparePayResultType,
+ j2s,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState } from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironmentV2,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
+
+/**
+ * Test behavior when an order is deleted while the wallet is paying for it.
+ */
+export async function runPaymentDeletedTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { walletClient, bank, exchange, merchant } =
+ await createSimpleTestkudosEnvironmentV2(t);
+
+ // First, make a "free" payment when we don't even have
+ // any money in the
+
+ // Withdraw digital cash into the wallet.
+ await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ });
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+
+ const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl());
+
+ const orderResp = await merchantClient.createOrder({
+ order: {
+ summary: "Hello",
+ amount: "TESTKUDOS:2",
+ },
+ });
+
+ let orderStatus = await merchantClient.queryPrivateOrderStatus({
+ orderId: orderResp.order_id,
+ });
+
+ t.assertTrue(orderStatus.order_status === "unpaid");
+
+ // Make wallet pay for the order
+
+ const preparePayResult = await walletClient.call(
+ WalletApiOperation.PreparePayForUri,
+ {
+ talerPayUri: orderStatus.taler_pay_uri,
+ },
+ );
+
+ t.assertTrue(
+ preparePayResult.status === PreparePayResultType.PaymentPossible,
+ );
+
+ await merchantClient.deleteOrder({
+ orderId: orderResp.order_id,
+ force: true,
+ });
+
+ const r2 = await walletClient.call(WalletApiOperation.ConfirmPay, {
+ transactionId: preparePayResult.transactionId,
+ });
+
+ t.assertTrue(r2.type === ConfirmPayResultType.Pending);
+
+ await walletClient.call(WalletApiOperation.AbortTransaction, {
+ transactionId: preparePayResult.transactionId,
+ });
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+
+ const bal = await walletClient.call(WalletApiOperation.GetBalances, {});
+ console.log(j2s(bal));
+}
+
+runPaymentDeletedTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-payment-expired.ts b/packages/taler-harness/src/integrationtests/test-payment-expired.ts
new file mode 100644
index 000000000..a837b18fa
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-payment-expired.ts
@@ -0,0 +1,132 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ AbsoluteTime,
+ ConfirmPayResultType,
+ Duration,
+ MerchantApiClient,
+ MerchantContractTerms,
+ PreparePayResultType,
+ j2s,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState } from "../harness/harness.js";
+import {
+ applyTimeTravelV2,
+ createSimpleTestkudosEnvironmentV2,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
+
+/**
+ * Run a test for the following scenario:
+ *
+ * - Wallet claims an order
+ * - Merchant goes down
+ * - Wallet tried to pay, but it fails as the merchant is unavailable
+ * - The order expires
+ * - The merchant goes back up again
+ * - Instead of trying to get an abort-refund, the wallet notices that
+ * the order is expired, puts the transaction into "failed",
+ * refreshes allocated coins and thus raises the balance again.
+ */
+export async function runPaymentExpiredTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { walletClient, bank, exchange, merchant } =
+ await createSimpleTestkudosEnvironmentV2(t);
+
+ // Withdraw digital cash into the wallet.
+
+ await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ });
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+
+ // Order that can only be paid within five minutes.
+ const order: Partial<MerchantContractTerms> = {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "taler://fulfillment-success/thx",
+ pay_deadline: AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ minutes: 5 }),
+ ),
+ ),
+ };
+
+ const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl());
+
+ const orderResp = await merchantClient.createOrder({
+ order,
+ });
+
+ let orderStatus = await merchantClient.queryPrivateOrderStatus({
+ orderId: orderResp.order_id,
+ });
+
+ t.assertTrue(orderStatus.order_status === "unpaid");
+
+ const preparePayResult = await walletClient.call(
+ WalletApiOperation.PreparePayForUri,
+ {
+ talerPayUri: orderStatus.taler_pay_uri,
+ },
+ );
+
+ t.assertDeepEqual(
+ preparePayResult.status,
+ PreparePayResultType.PaymentPossible,
+ );
+
+ await applyTimeTravelV2(
+ Duration.toMilliseconds(Duration.fromSpec({ hours: 1 })),
+ { walletClient, exchange, merchant },
+ );
+
+ const confirmPayResult = await walletClient.call(
+ WalletApiOperation.ConfirmPay,
+ { transactionId: preparePayResult.transactionId },
+ );
+ console.log("confirm pay result:");
+ console.log(j2s(confirmPayResult));
+ t.assertDeepEqual(confirmPayResult.type, ConfirmPayResultType.Pending);
+ await walletClient.call(WalletApiOperation.AbortTransaction, {
+ transactionId: preparePayResult.transactionId,
+ });
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+
+ const txns = await walletClient.call(WalletApiOperation.GetTransactions, {
+ sort: "stable-ascending",
+ includeRefreshes: true,
+ });
+ console.log(j2s(txns));
+
+ const bal = await walletClient.call(WalletApiOperation.GetBalances, {});
+ console.log(j2s(bal));
+
+ t.assertAmountEquals(bal.balances[0].available, "TESTKUDOS:18.93");
+}
+
+runPaymentExpiredTest.suites = ["wallet"];
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-payment-fault.ts b/packages/taler-harness/src/integrationtests/test-payment-fault.ts
index dea538e35..cadcc9056 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-payment-fault.ts
+++ b/packages/taler-harness/src/integrationtests/test-payment-fault.ts
@@ -21,28 +21,26 @@
/**
* Imports.
*/
-import {
- GlobalTestState,
- MerchantService,
- ExchangeService,
- setupDb,
- BankService,
- WalletCli,
- MerchantPrivateApi,
- getPayto,
-} from "../harness/harness.js";
+import { ConfirmPayResultType, MerchantApiClient } from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { defaultCoinConfig } from "../harness/denomStructures.js";
import {
FaultInjectedExchangeService,
FaultInjectionRequestContext,
FaultInjectionResponseContext,
} from "../harness/faultInjection.js";
-import { CoreApiResponse } from "@gnu-taler/taler-util";
-import { defaultCoinConfig } from "../harness/denomStructures.js";
import {
- WalletApiOperation,
- BankApi,
- BankAccessApi,
-} from "@gnu-taler/taler-wallet-core";
+ BankService,
+ ExchangeService,
+ GlobalTestState,
+ MerchantService,
+ generateRandomPayto,
+ setupDb,
+} from "../harness/harness.js";
+import {
+ createWalletDaemonWithClient,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
/**
* Run test for basic, bank-integrated withdrawal.
@@ -71,7 +69,16 @@ export async function runPaymentFaultTest(t: GlobalTestState) {
"x",
);
- bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri);
+ const faultyExchange = new FaultInjectedExchangeService(t, exchange, 8091);
+ // Base URL must contain port that the proxy is listening on.
+ await exchange.modifyConfig(async (config) => {
+ config.setString("exchange", "base_url", "http://localhost:8091/");
+ });
+
+ bank.setSuggestedExchange(
+ faultyExchange,
+ exchangeBankAccount.accountPaytoUri,
+ );
await bank.start();
@@ -83,8 +90,6 @@ export async function runPaymentFaultTest(t: GlobalTestState) {
await exchange.start();
await exchange.pingUntilAvailable();
- const faultyExchange = new FaultInjectedExchangeService(t, exchange, 8091);
-
// Print all requests to the exchange
faultyExchange.faultProxy.addFault({
async modifyRequest(ctx: FaultInjectionRequestContext) {
@@ -107,54 +112,34 @@ export async function runPaymentFaultTest(t: GlobalTestState) {
await merchant.start();
await merchant.pingUntilAvailable();
- await merchant.addInstance({
+ await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [getPayto("merchant-default")],
+ paytoUris: [generateRandomPayto("merchant-default")],
});
- console.log("setup done!");
-
- const wallet = new WalletCli(t);
-
- // Create withdrawal operation
-
- const user = await BankApi.createRandomBankUser(bank);
- const wop = await BankAccessApi.createWithdrawalOperation(
- bank,
- user,
- "TESTKUDOS:20",
- );
+ const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl());
- // Hand it to the wallet
+ console.log("setup done!");
- await wallet.client.call(WalletApiOperation.GetWithdrawalDetailsForUri, {
- talerWithdrawUri: wop.taler_withdraw_uri,
+ const { walletClient } = await createWalletDaemonWithClient(t, {
+ name: "default",
});
- await wallet.runPending();
-
- // Withdraw
+ await walletClient.call(WalletApiOperation.GetBalances, {});
- await wallet.client.call(WalletApiOperation.AcceptBankIntegratedWithdrawal, {
- exchangeBaseUrl: faultyExchange.baseUrl,
- talerWithdrawUri: wop.taler_withdraw_uri,
+ const wres = await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange: faultyExchange,
+ amount: "TESTKUDOS:20",
});
- await wallet.runPending();
-
- // Confirm it
-
- await BankApi.confirmWithdrawalOperation(bank, user, wop);
- await wallet.runUntilDone();
-
- // Check balance
-
- await wallet.client.call(WalletApiOperation.GetBalances, {});
+ await wres.withdrawalFinishedCond;
// Set up order.
- const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
+ const orderResp = await merchantClient.createOrder({
order: {
summary: "Buy me!",
amount: "TESTKUDOS:5",
@@ -162,7 +147,7 @@ export async function runPaymentFaultTest(t: GlobalTestState) {
},
});
- let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ let orderStatus = await merchantClient.queryPrivateOrderStatus({
orderId: orderResp.order_id,
});
@@ -170,24 +155,22 @@ export async function runPaymentFaultTest(t: GlobalTestState) {
// Make wallet pay for the order
- let apiResp: CoreApiResponse;
-
- const prepResp = await wallet.client.call(
+ const prepResp = await walletClient.call(
WalletApiOperation.PreparePayForUri,
{
talerPayUri: orderStatus.taler_pay_uri,
},
);
- const proposalId = prepResp.proposalId;
-
- await wallet.runPending();
-
// Drop 3 responses from the exchange.
let faultCount = 0;
faultyExchange.faultProxy.addFault({
async modifyResponse(ctx: FaultInjectionResponseContext) {
- if (!ctx.request.requestUrl.endsWith("/deposit")) {
+ console.log(`in modifyResponse for ${ctx.request.requestUrl}`);
+ if (
+ !ctx.request.requestUrl.endsWith("/deposit") &&
+ !ctx.request.requestUrl.endsWith("/batch-deposit")
+ ) {
return;
}
if (faultCount < 3) {
@@ -202,16 +185,20 @@ export async function runPaymentFaultTest(t: GlobalTestState) {
// confirmPay won't work, as the exchange is unreachable
- await wallet.client.call(WalletApiOperation.ConfirmPay, {
- // FIXME: should be validated, don't cast!
- proposalId: proposalId,
- });
+ const confirmPayResp = await walletClient.call(
+ WalletApiOperation.ConfirmPay,
+ {
+ transactionId: prepResp.transactionId,
+ },
+ );
+
+ t.assertDeepEqual(confirmPayResp.type, ConfirmPayResultType.Pending);
- await wallet.runUntilDone();
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
// Check if payment was successful.
- orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderStatus = await merchantClient.queryPrivateOrderStatus({
orderId: orderResp.order_id,
});
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-payment-forgettable.ts b/packages/taler-harness/src/integrationtests/test-payment-forgettable.ts
index 3bdd6bef3..21d76397d 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-payment-forgettable.ts
+++ b/packages/taler-harness/src/integrationtests/test-payment-forgettable.ts
@@ -17,11 +17,12 @@
/**
* Imports.
*/
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { GlobalTestState } from "../harness/harness.js";
import {
- createSimpleTestkudosEnvironment,
- withdrawViaBank,
- makeTestPayment,
+ createSimpleTestkudosEnvironmentV2,
+ makeTestPaymentV2,
+ withdrawViaBankV2,
} from "../harness/helpers.js";
/**
@@ -30,16 +31,19 @@ import {
export async function runPaymentForgettableTest(t: GlobalTestState) {
// Set up test environment
- const {
- wallet,
- bank,
- exchange,
- merchant,
- } = await createSimpleTestkudosEnvironment(t);
+ const { walletClient, bank, exchange, merchant } =
+ await createSimpleTestkudosEnvironmentV2(t);
// Withdraw digital cash into the wallet.
- await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
+ const wres = await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ });
+
+ await wres.withdrawalFinishedCond;
{
const order = {
@@ -54,7 +58,7 @@ export async function runPaymentForgettableTest(t: GlobalTestState) {
},
};
- await makeTestPayment(t, { wallet, merchant, order });
+ await makeTestPaymentV2(t, { walletClient, merchant, order });
}
console.log("testing with forgettable field without hash");
@@ -72,10 +76,10 @@ export async function runPaymentForgettableTest(t: GlobalTestState) {
},
};
- await makeTestPayment(t, { wallet, merchant, order });
+ await makeTestPaymentV2(t, { walletClient, merchant, order });
}
- await wallet.runUntilDone();
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
}
runPaymentForgettableTest.suites = ["wallet", "merchant"];
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-payment-idempotency.ts b/packages/taler-harness/src/integrationtests/test-payment-idempotency.ts
index 1099a8188..65fd3a562 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-payment-idempotency.ts
+++ b/packages/taler-harness/src/integrationtests/test-payment-idempotency.ts
@@ -17,13 +17,13 @@
/**
* Imports.
*/
-import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js";
+import { MerchantApiClient, PreparePayResultType } from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState } from "../harness/harness.js";
import {
- createSimpleTestkudosEnvironment,
- withdrawViaBank,
+ createSimpleTestkudosEnvironmentV2,
+ withdrawViaBankV2,
} from "../harness/helpers.js";
-import { PreparePayResultType } from "@gnu-taler/taler-util";
-import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
/**
* Test the wallet-core payment API, especially that repeated operations
@@ -32,16 +32,25 @@ import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
export async function runPaymentIdempotencyTest(t: GlobalTestState) {
// Set up test environment
- const { wallet, bank, exchange, merchant } =
- await createSimpleTestkudosEnvironment(t);
+ const { walletClient, bank, exchange, merchant } =
+ await createSimpleTestkudosEnvironmentV2(t);
// Withdraw digital cash into the wallet.
- await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
+ const wres = await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ });
+
+ await wres.withdrawalFinishedCond;
+
+ const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl());
// Set up order.
- const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
+ const orderResp = await merchantClient.createOrder({
order: {
summary: "Buy me!",
amount: "TESTKUDOS:5",
@@ -49,7 +58,7 @@ export async function runPaymentIdempotencyTest(t: GlobalTestState) {
},
});
- let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ let orderStatus = await merchantClient.queryPrivateOrderStatus({
orderId: orderResp.order_id,
});
@@ -59,14 +68,14 @@ export async function runPaymentIdempotencyTest(t: GlobalTestState) {
// Make wallet pay for the order
- const preparePayResult = await wallet.client.call(
+ const preparePayResult = await walletClient.call(
WalletApiOperation.PreparePayForUri,
{
talerPayUri: orderStatus.taler_pay_uri,
},
);
- const preparePayResultRep = await wallet.client.call(
+ const preparePayResultRep = await walletClient.call(
WalletApiOperation.PreparePayForUri,
{
talerPayUri: orderStatus.taler_pay_uri,
@@ -82,7 +91,7 @@ export async function runPaymentIdempotencyTest(t: GlobalTestState) {
const proposalId = preparePayResult.proposalId;
- const confirmPayResult = await wallet.client.call(
+ const confirmPayResult = await walletClient.call(
WalletApiOperation.ConfirmPay,
{
proposalId: proposalId,
@@ -91,17 +100,17 @@ export async function runPaymentIdempotencyTest(t: GlobalTestState) {
console.log("confirm pay result", confirmPayResult);
- await wallet.runUntilDone();
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
// Check if payment was successful.
- orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderStatus = await merchantClient.queryPrivateOrderStatus({
orderId: orderResp.order_id,
});
t.assertTrue(orderStatus.order_status === "paid");
- const preparePayResultAfter = await wallet.client.call(
+ const preparePayResultAfter = await walletClient.call(
WalletApiOperation.PreparePayForUri,
{
talerPayUri,
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-payment-multiple.ts b/packages/taler-harness/src/integrationtests/test-payment-multiple.ts
index 46325c05f..0caa3c3e7 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-payment-multiple.ts
+++ b/packages/taler-harness/src/integrationtests/test-payment-multiple.ts
@@ -17,23 +17,23 @@
/**
* Imports.
*/
+import { MerchantApiClient } from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { coin_ct10, coin_u1 } from "../harness/denomStructures.js";
import {
- GlobalTestState,
- setupDb,
BankService,
ExchangeService,
+ GlobalTestState,
MerchantService,
- WalletCli,
- MerchantPrivateApi,
- getPayto
+ generateRandomPayto,
+ setupDb,
} from "../harness/harness.js";
-import { withdrawViaBank } from "../harness/helpers.js";
-import { coin_ct10, coin_u1 } from "../harness/denomStructures.js";
-import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import {
+ createWalletDaemonWithClient,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
-async function setupTest(
- t: GlobalTestState,
-): Promise<{
+async function setupTest(t: GlobalTestState): Promise<{
merchant: MerchantService;
exchange: ExchangeService;
bank: BankService;
@@ -84,16 +84,16 @@ async function setupTest(
await merchant.start();
await merchant.pingUntilAvailable();
- await merchant.addInstance({
+ await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [getPayto("merchant-default")],
+ paytoUris: [generateRandomPayto("merchant-default")],
});
- await merchant.addInstance({
+ await merchant.addInstanceWithWireAccount({
id: "minst1",
name: "minst1",
- paytoUris: [getPayto("minst1")],
+ paytoUris: [generateRandomPayto("minst1")],
});
console.log("setup done!");
@@ -115,15 +115,26 @@ export async function runPaymentMultipleTest(t: GlobalTestState) {
const { merchant, bank, exchange } = await setupTest(t);
- const wallet = new WalletCli(t);
+ const { walletClient } = await createWalletDaemonWithClient(t, {
+ name: "default",
+ });
+
+ const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl());
// Withdraw digital cash into the wallet.
- await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:100" });
+ const wres = await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:100",
+ });
+
+ await wres.withdrawalFinishedCond;
// Set up order.
- const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
+ const orderResp = await merchantClient.createOrder({
order: {
summary: "Buy me!",
amount: "TESTKUDOS:80",
@@ -131,7 +142,7 @@ export async function runPaymentMultipleTest(t: GlobalTestState) {
},
});
- let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ let orderStatus = await merchantClient.queryPrivateOrderStatus({
orderId: orderResp.order_id,
});
@@ -139,18 +150,17 @@ export async function runPaymentMultipleTest(t: GlobalTestState) {
// Make wallet pay for the order
- const r1 = await wallet.client.call(WalletApiOperation.PreparePayForUri, {
+ const r1 = await walletClient.call(WalletApiOperation.PreparePayForUri, {
talerPayUri: orderStatus.taler_pay_uri,
});
- await wallet.client.call(WalletApiOperation.ConfirmPay, {
- // FIXME: should be validated, don't cast!
- proposalId: r1.proposalId,
+ await walletClient.call(WalletApiOperation.ConfirmPay, {
+ transactionId: r1.transactionId,
});
// Check if payment was successful.
- orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderStatus = await merchantClient.queryPrivateOrderStatus({
orderId: orderResp.order_id,
});
diff --git a/packages/taler-harness/src/integrationtests/test-payment-share.ts b/packages/taler-harness/src/integrationtests/test-payment-share.ts
new file mode 100644
index 000000000..141faa81e
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-payment-share.ts
@@ -0,0 +1,308 @@
+/*
+ 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 {
+ ConfirmPayResultType,
+ MerchantApiClient,
+ NotificationType,
+ PreparePayResultType,
+ j2s,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState } from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironmentV2,
+ createWalletDaemonWithClient,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
+
+/**
+ * Run test for basic, bank-integrated withdrawal and payment.
+ */
+export async function runPaymentShareTest(t: GlobalTestState) {
+ // Set up test environment
+ const {
+ walletClient: firstWallet,
+ bank,
+ exchange,
+ merchant,
+ } = await createSimpleTestkudosEnvironmentV2(t);
+
+ const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl());
+
+ // Withdraw digital cash into the wallet.
+ await withdrawViaBankV2(t, {
+ walletClient: firstWallet,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ });
+ await firstWallet.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+
+ const { walletClient: secondWallet } = await createWalletDaemonWithClient(t, {
+ name: "wallet2",
+ });
+
+ await withdrawViaBankV2(t, {
+ walletClient: secondWallet,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ });
+ await secondWallet.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+
+ {
+ const first = await firstWallet.call(WalletApiOperation.GetBalances, {});
+ const second = await secondWallet.call(WalletApiOperation.GetBalances, {});
+ t.assertAmountEquals(first.balances[0].available, "TESTKUDOS:19.53");
+ t.assertAmountEquals(second.balances[0].available, "TESTKUDOS:19.53");
+ }
+
+ t.logStep("setup-done");
+
+ // create two orders to pay
+ async function createOrder(amount: string) {
+ const order = {
+ summary: "Buy me!",
+ amount,
+ fulfillment_url: "taler://fulfillment-success/thx",
+ };
+
+ const args = { order };
+
+ const orderResp = await merchantClient.createOrder({
+ order: args.order,
+ });
+
+ const orderStatus = await merchantClient.queryPrivateOrderStatus({
+ orderId: orderResp.order_id,
+ });
+
+ t.assertTrue(orderStatus.order_status === "unpaid");
+ return { id: orderResp.order_id, uri: orderStatus.taler_pay_uri };
+ }
+
+ t.logStep("orders-created");
+
+ /**
+ * Case 1:
+ * - Claim with first wallet and pay in the second wallet.
+ * - First wallet should be notified.
+ */
+ {
+ const order = await createOrder("TESTKUDOS:5");
+ // Claim the order with the first wallet
+ const claimFirstWallet = await firstWallet.call(
+ WalletApiOperation.PreparePayForUri,
+ { talerPayUri: order.uri },
+ );
+
+ t.assertTrue(
+ claimFirstWallet.status === PreparePayResultType.PaymentPossible,
+ );
+
+ t.logStep("w1-payment-possible");
+
+ // share order from the first wallet
+ const { privatePayUri } = await firstWallet.call(
+ WalletApiOperation.SharePayment,
+ {
+ merchantBaseUrl: merchant.makeInstanceBaseUrl(),
+ orderId: order.id,
+ },
+ );
+
+ t.logStep("w1-payment-shared");
+
+ // claim from the second wallet
+ const claimSecondWallet = await secondWallet.call(
+ WalletApiOperation.PreparePayForUri,
+ { talerPayUri: privatePayUri },
+ );
+
+ t.assertTrue(
+ claimSecondWallet.status === PreparePayResultType.PaymentPossible,
+ );
+
+ t.logStep("w2-claimed");
+
+ // pay from the second wallet
+ const r2 = await secondWallet.call(WalletApiOperation.ConfirmPay, {
+ transactionId: claimSecondWallet.transactionId,
+ });
+
+ t.assertTrue(r2.type === ConfirmPayResultType.Done);
+
+ t.logStep("w2-confirmed");
+
+ // Wait for refresh to settle before we do checks
+ await secondWallet.call(
+ WalletApiOperation.TestingWaitTransactionsFinal,
+ {},
+ );
+
+ t.logStep("w2-refresh-settled");
+
+ {
+ const first = await firstWallet.call(WalletApiOperation.GetBalances, {});
+ const second = await secondWallet.call(
+ WalletApiOperation.GetBalances,
+ {},
+ );
+ t.assertAmountEquals(first.balances[0].available, "TESTKUDOS:19.53");
+ t.assertAmountEquals(second.balances[0].available, "TESTKUDOS:14.23");
+ }
+
+ t.logStep("wait-for-payment");
+ // firstWallet.waitForNotificationCond(n =>
+ // n.type === NotificationType.TransactionStateTransition &&
+ // n.transactionId === claimFirstWallet.transactionId
+ // )
+ // Claim the order with the first wallet
+ const claimFirstWalletAgain = await firstWallet.call(
+ WalletApiOperation.PreparePayForUri,
+ { talerPayUri: order.uri },
+ );
+
+ t.assertTrue(
+ claimFirstWalletAgain.status === PreparePayResultType.AlreadyConfirmed,
+ );
+ t.assertTrue( claimFirstWalletAgain.paid );
+
+ t.logStep("w1-prepared-again");
+
+ const r1 = await firstWallet.call(WalletApiOperation.ConfirmPay, {
+ transactionId: claimFirstWallet.transactionId,
+ });
+
+ //t.assertTrue(r1.type === ConfirmPayResultType.Pending);
+
+ t.logStep("w1-confirmed-shared");
+
+ await firstWallet.call(
+ WalletApiOperation.TestingWaitTransactionsFinal,
+ {},
+ );
+
+ await secondWallet.call(
+ WalletApiOperation.TestingWaitTransactionsFinal,
+ {},
+ );
+
+ /**
+ * only the second wallet balance was affected
+ */
+ {
+ const first = await firstWallet.call(WalletApiOperation.GetBalances, {});
+ const second = await secondWallet.call(
+ WalletApiOperation.GetBalances,
+ {},
+ );
+ t.assertAmountEquals(first.balances[0].available, "TESTKUDOS:19.53");
+ t.assertAmountEquals(second.balances[0].available, "TESTKUDOS:14.23");
+ }
+ }
+
+ t.logStep("first-case-done");
+
+ /**
+ * Case 2:
+ * - Claim with first wallet and share with the second wallet
+ * - Pay with the first wallet, second wallet should be notified
+ */
+ {
+ const order = await createOrder("TESTKUDOS:3");
+ // Claim the order with the first wallet
+ const claimFirstWallet = await firstWallet.call(
+ WalletApiOperation.PreparePayForUri,
+ { talerPayUri: order.uri },
+ );
+
+ t.assertTrue(
+ claimFirstWallet.status === PreparePayResultType.PaymentPossible,
+ );
+
+ t.logStep("case2-w1-claimed");
+
+ // share order from the first wallet
+ const { privatePayUri } = await firstWallet.call(
+ WalletApiOperation.SharePayment,
+ {
+ merchantBaseUrl: merchant.makeInstanceBaseUrl(),
+ orderId: order.id,
+ },
+ );
+
+ t.logStep("case2-w1-shared");
+
+ // claim from the second wallet
+ const claimSecondWallet = await secondWallet.call(
+ WalletApiOperation.PreparePayForUri,
+ { talerPayUri: privatePayUri },
+ );
+
+ t.logStep("case2-w2-prepared");
+
+ t.assertTrue(
+ claimSecondWallet.status === PreparePayResultType.PaymentPossible,
+ );
+
+ // pay from the first wallet
+ const r2 = await firstWallet.call(WalletApiOperation.ConfirmPay, {
+ transactionId: claimFirstWallet.transactionId,
+ });
+
+ t.assertTrue(r2.type === ConfirmPayResultType.Done);
+
+ // Wait for refreshes to settle before doing checks
+ await firstWallet.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+
+ /**
+ * only the first wallet balance was affected
+ */
+ const bal1 = await firstWallet.call(WalletApiOperation.GetBalances, {});
+ const bal2 = await secondWallet.call(WalletApiOperation.GetBalances, {});
+ t.assertAmountEquals(bal1.balances[0].available, "TESTKUDOS:16.18");
+ t.assertAmountEquals(bal2.balances[0].available, "TESTKUDOS:14.23");
+
+ t.logStep("wait-for-payment");
+ // secondWallet.waitForNotificationCond(n =>
+ // n.type === NotificationType.TransactionStateTransition &&
+ // n.transactionId === claimSecondWallet.transactionId
+ // )
+
+ // Claim the order with the first wallet
+ const claimSecondWalletAgain = await secondWallet.call(
+ WalletApiOperation.PreparePayForUri,
+ { talerPayUri: order.uri },
+ );
+
+ t.assertTrue(
+ claimSecondWalletAgain.status === PreparePayResultType.AlreadyConfirmed,
+ );
+ t.assertTrue(
+ claimSecondWalletAgain.paid,
+ );
+
+ }
+
+ t.logStep("second-case-done");
+}
+
+runPaymentShareTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-payment-template.ts b/packages/taler-harness/src/integrationtests/test-payment-template.ts
new file mode 100644
index 000000000..c3ab5ffc8
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-payment-template.ts
@@ -0,0 +1,105 @@
+/*
+ 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 {
+ ConfirmPayResultType,
+ Duration,
+ MerchantApiClient,
+ PreparePayResultType,
+ narrowOpSuccessOrThrow,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState } from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironmentV2,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
+
+/**
+ * Test for taler://payment-template/ URIs
+ */
+export async function runPaymentTemplateTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { walletClient, bank, exchange, merchant } =
+ await createSimpleTestkudosEnvironmentV2(t);
+
+ const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl());
+
+ const createTemplateRes = await merchantClient.createTemplate({
+ template_id: "template1",
+ template_description: "my test template",
+ template_contract: {
+ minimum_age: 0,
+ pay_duration: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({
+ minutes: 2,
+ }),
+ ),
+ summary: "hello, I'm a summary",
+ },
+ });
+ narrowOpSuccessOrThrow("createTemplate", createTemplateRes);
+
+ // Withdraw digital cash into the wallet.
+
+ const wres = await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ });
+ await wres.withdrawalFinishedCond;
+
+ // Request a template payment
+
+ const preparePayResult = await walletClient.call(
+ WalletApiOperation.PreparePayForTemplate,
+ {
+ talerPayTemplateUri: `taler+http://pay-template/localhost:${merchant.port}/template1?amount=TESTKUDOS:1`,
+ templateParams: {},
+ },
+ );
+
+ console.log(preparePayResult);
+
+ t.assertTrue(
+ preparePayResult.status === PreparePayResultType.PaymentPossible,
+ );
+
+ // Pay for it
+
+ const r2 = await walletClient.call(WalletApiOperation.ConfirmPay, {
+ transactionId: preparePayResult.transactionId,
+ });
+
+ t.assertTrue(r2.type === ConfirmPayResultType.Done);
+
+ // Check if payment was successful.
+
+ const orderStatus = await merchantClient.queryPrivateOrderStatus({
+ orderId: preparePayResult.contractTerms.order_id,
+ instance: "default",
+ });
+
+ t.assertTrue(orderStatus.order_status === "paid");
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+}
+
+runPaymentTemplateTest.suites = ["wallet"];
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-payment-transient.ts b/packages/taler-harness/src/integrationtests/test-payment-transient.ts
index b57b355c6..1911b5e92 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-payment-transient.ts
+++ b/packages/taler-harness/src/integrationtests/test-payment-transient.ts
@@ -17,25 +17,22 @@
/**
* Imports.
*/
-import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js";
import {
- withdrawViaBank,
- createFaultInjectedMerchantTestkudosEnvironment,
-} from "../harness/helpers.js";
-import {
- FaultInjectionResponseContext,
-} from "../harness/faultInjection.js";
-import {
- codecForMerchantOrderStatusUnpaid,
ConfirmPayResultType,
+ MerchantApiClient,
PreparePayResultType,
TalerErrorCode,
TalerErrorDetail,
URL,
+ codecForMerchantOrderStatusUnpaid,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-import axiosImp from "axios";
-const axios = axiosImp.default;
+import { FaultInjectionResponseContext } from "../harness/faultInjection.js";
+import { GlobalTestState, harnessHttpLib } from "../harness/harness.js";
+import {
+ createFaultInjectedMerchantTestkudosEnvironment,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
/**
* Run test for a payment where the merchant has a transient
@@ -44,20 +41,25 @@ const axios = axiosImp.default;
export async function runPaymentTransientTest(t: GlobalTestState) {
// Set up test environment
- const {
- wallet,
- bank,
- exchange,
- faultyMerchant,
- } = await createFaultInjectedMerchantTestkudosEnvironment(t);
+ const { walletClient, bank, faultyMerchant, faultyExchange } =
+ await createFaultInjectedMerchantTestkudosEnvironment(t);
+
+ const merchantClient = new MerchantApiClient(
+ faultyMerchant.makeInstanceBaseUrl(),
+ );
// Withdraw digital cash into the wallet.
- await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
+ const wres = await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange: faultyExchange,
+ amount: "TESTKUDOS:20",
+ });
- const merchant = faultyMerchant;
+ await wres.withdrawalFinishedCond;
- let orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
+ let orderResp = await merchantClient.createOrder({
order: {
summary: "Buy me!",
amount: "TESTKUDOS:5",
@@ -66,7 +68,7 @@ export async function runPaymentTransientTest(t: GlobalTestState) {
},
});
- let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ let orderStatus = await merchantClient.queryPrivateOrderStatus({
orderId: orderResp.order_id,
sessionId: "mysession-one",
});
@@ -76,25 +78,21 @@ export async function runPaymentTransientTest(t: GlobalTestState) {
t.assertTrue(orderStatus.already_paid_order_id === undefined);
let publicOrderStatusUrl = orderStatus.order_status_url;
- let publicOrderStatusResp = await axios.get(publicOrderStatusUrl, {
- validateStatus: () => true,
- });
+ let publicOrderStatusResp = await harnessHttpLib.fetch(publicOrderStatusUrl);
if (publicOrderStatusResp.status != 402) {
-
-
throw Error(
`expected status 402 (before claiming), but got ${publicOrderStatusResp.status}`,
);
}
let pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode(
- publicOrderStatusResp.data,
+ publicOrderStatusResp.json(),
);
console.log(pubUnpaidStatus);
- let preparePayResp = await wallet.client.call(
+ let preparePayResp = await walletClient.call(
WalletApiOperation.PreparePayForUri,
{
talerPayUri: pubUnpaidStatus.taler_pay_uri,
@@ -105,9 +103,7 @@ export async function runPaymentTransientTest(t: GlobalTestState) {
const proposalId = preparePayResp.proposalId;
- publicOrderStatusResp = await axios.get(publicOrderStatusUrl, {
- validateStatus: () => true,
- });
+ publicOrderStatusResp = await harnessHttpLib.fetch(publicOrderStatusUrl);
if (publicOrderStatusResp.status != 402) {
throw Error(
@@ -116,7 +112,7 @@ export async function runPaymentTransientTest(t: GlobalTestState) {
}
pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode(
- publicOrderStatusResp.data,
+ publicOrderStatusResp.json(),
);
let faultInjected = false;
@@ -144,7 +140,7 @@ export async function runPaymentTransientTest(t: GlobalTestState) {
},
});
- const confirmPayResp = await wallet.client.call(
+ const confirmPayResp = await walletClient.call(
WalletApiOperation.ConfirmPay,
{
proposalId,
@@ -156,7 +152,7 @@ export async function runPaymentTransientTest(t: GlobalTestState) {
t.assertTrue(confirmPayResp.type === ConfirmPayResultType.Pending);
t.assertTrue(faultInjected);
- const confirmPayRespTwo = await wallet.client.call(
+ const confirmPayRespTwo = await walletClient.call(
WalletApiOperation.ConfirmPay,
{
proposalId,
@@ -168,14 +164,12 @@ export async function runPaymentTransientTest(t: GlobalTestState) {
// Now ask the merchant if paid
console.log("requesting", publicOrderStatusUrl);
- publicOrderStatusResp = await axios.get(publicOrderStatusUrl, {
- validateStatus: () => true,
- });
+ publicOrderStatusResp = await harnessHttpLib.fetch(publicOrderStatusUrl);
- console.log(publicOrderStatusResp.data);
+ console.log(publicOrderStatusResp.json());
if (publicOrderStatusResp.status != 200) {
- console.log(publicOrderStatusResp.data);
+ console.log(publicOrderStatusResp.json());
throw Error(
`expected status 200 (after paying), but got ${publicOrderStatusResp.status}`,
);
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-payment-zero.ts b/packages/taler-harness/src/integrationtests/test-payment-zero.ts
index c38b8b382..7423751a5 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-payment-zero.ts
+++ b/packages/taler-harness/src/integrationtests/test-payment-zero.ts
@@ -20,10 +20,11 @@
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { GlobalTestState } from "../harness/harness.js";
import {
- createSimpleTestkudosEnvironment,
- withdrawViaBank,
- makeTestPayment,
+ createSimpleTestkudosEnvironmentV2,
+ withdrawViaBankV2,
+ makeTestPaymentV2,
} from "../harness/helpers.js";
+import { TransactionMajorState } from "@gnu-taler/taler-util";
/**
* Run test for a payment for a "free" order with
@@ -32,23 +33,19 @@ import {
export async function runPaymentZeroTest(t: GlobalTestState) {
// Set up test environment
- const {
- wallet,
- bank,
- exchange,
- merchant,
- } = await createSimpleTestkudosEnvironment(t);
+ const { walletClient, bank, exchange, merchant } =
+ await createSimpleTestkudosEnvironmentV2(t);
// First, make a "free" payment when we don't even have
// any money in the
// Withdraw digital cash into the wallet.
- await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
+ await withdrawViaBankV2(t, { walletClient, bank, exchange, amount: "TESTKUDOS:20" });
- await wallet.runUntilDone();
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
- await makeTestPayment(t, {
- wallet,
+ await makeTestPaymentV2(t, {
+ walletClient,
merchant,
order: {
summary: "I am free!",
@@ -57,15 +54,15 @@ export async function runPaymentZeroTest(t: GlobalTestState) {
},
});
- await wallet.runUntilDone();
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
- const transactions = await wallet.client.call(
+ const transactions = await walletClient.call(
WalletApiOperation.GetTransactions,
{},
);
for (const tr of transactions.transactions) {
- t.assertDeepEqual(tr.pending, false);
+ t.assertDeepEqual(tr.txState.major, TransactionMajorState.Done);
}
}
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-payment.ts b/packages/taler-harness/src/integrationtests/test-payment.ts
index 66d10f996..9d1ce0e22 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-payment.ts
+++ b/packages/taler-harness/src/integrationtests/test-payment.ts
@@ -17,12 +17,14 @@
/**
* Imports.
*/
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { GlobalTestState } from "../harness/harness.js";
import {
- createSimpleTestkudosEnvironment,
- withdrawViaBank,
- makeTestPayment,
+ createSimpleTestkudosEnvironmentV2,
+ withdrawViaBankV2,
+ makeTestPaymentV2,
} from "../harness/helpers.js";
+import { j2s } from "@gnu-taler/taler-util";
/**
* Run test for basic, bank-integrated withdrawal and payment.
@@ -30,16 +32,14 @@ import {
export async function runPaymentTest(t: GlobalTestState) {
// Set up test environment
- const {
- wallet,
- bank,
- exchange,
- merchant,
- } = await createSimpleTestkudosEnvironment(t);
+ const { walletClient, bank, exchange, merchant } =
+ await createSimpleTestkudosEnvironmentV2(t);
// Withdraw digital cash into the wallet.
- await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
+ await withdrawViaBankV2(t, { walletClient, bank, exchange, amount: "TESTKUDOS:20" });
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
const order = {
summary: "Buy me!",
@@ -47,19 +47,19 @@ export async function runPaymentTest(t: GlobalTestState) {
fulfillment_url: "taler://fulfillment-success/thx",
};
- await makeTestPayment(t, { wallet, merchant, order });
- await wallet.runUntilDone();
+ await makeTestPaymentV2(t, { walletClient, merchant, order });
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
// Test JSON normalization of contract terms: Does the wallet
// agree with the merchant?
const order2 = {
- summary: "Testing “unicode” characters",
+ summary: "Testing “unicode” characters: 😁😱😇🥺🫦",
amount: "TESTKUDOS:5",
fulfillment_url: "taler://fulfillment-success/thx",
};
- await makeTestPayment(t, { wallet, merchant, order: order2 });
- await wallet.runUntilDone();
+ await makeTestPaymentV2(t, { walletClient, merchant, order: order2 });
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
// Test JSON normalization of contract terms: Does the wallet
// agree with the merchant?
@@ -69,9 +69,14 @@ export async function runPaymentTest(t: GlobalTestState) {
fulfillment_url: "taler://fulfillment-success/thx",
};
- await makeTestPayment(t, { wallet, merchant, order: order3 });
+ await makeTestPaymentV2(t, { walletClient, merchant, order: order3 });
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
- await wallet.runUntilDone();
+ const bal = await walletClient.call(WalletApiOperation.GetBalances, {});
+ console.log(`balance after 3 payments: ${j2s(bal)}`);
+ t.assertAmountEquals(bal.balances[0].available, "TESTKUDOS:3.8");
+ t.assertAmountEquals(bal.balances[0].pendingIncoming, "TESTKUDOS:0");
+ t.assertAmountEquals(bal.balances[0].pendingOutgoing, "TESTKUDOS:0");
}
runPaymentTest.suites = ["wallet"];
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-paywall-flow.ts b/packages/taler-harness/src/integrationtests/test-paywall-flow.ts
index a9601c625..247ec9cad 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-paywall-flow.ts
+++ b/packages/taler-harness/src/integrationtests/test-paywall-flow.ts
@@ -17,20 +17,19 @@
/**
* Imports.
*/
-import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js";
import {
- createSimpleTestkudosEnvironment,
- withdrawViaBank,
-} from "../harness/helpers.js";
-import {
- PreparePayResultType,
- codecForMerchantOrderStatusUnpaid,
ConfirmPayResultType,
+ MerchantApiClient,
+ PreparePayResultType,
URL,
+ codecForMerchantOrderStatusUnpaid,
} from "@gnu-taler/taler-util";
-import axiosImp from "axios";
-const axios = axiosImp.default;
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState, harnessHttpLib } from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironmentV2,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
/**
* Run test for basic, bank-integrated withdrawal.
@@ -38,12 +37,21 @@ import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
export async function runPaywallFlowTest(t: GlobalTestState) {
// Set up test environment
- const { wallet, bank, exchange, merchant } =
- await createSimpleTestkudosEnvironment(t);
+ const { walletClient, bank, exchange, merchant } =
+ await createSimpleTestkudosEnvironmentV2(t);
+
+ const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl());
// Withdraw digital cash into the wallet.
- await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
+ const wres = await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ });
+
+ await wres.withdrawalFinishedCond;
/**
* =========================================================================
@@ -54,7 +62,7 @@ export async function runPaywallFlowTest(t: GlobalTestState) {
* =========================================================================
*/
- let orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
+ let orderResp = await merchantClient.createOrder({
order: {
summary: "Buy me!",
amount: "TESTKUDOS:5",
@@ -65,7 +73,7 @@ export async function runPaywallFlowTest(t: GlobalTestState) {
const firstOrderId = orderResp.order_id;
- let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ let orderStatus = await merchantClient.queryPrivateOrderStatus({
orderId: orderResp.order_id,
sessionId: "mysession-one",
});
@@ -77,9 +85,9 @@ export async function runPaywallFlowTest(t: GlobalTestState) {
t.assertTrue(orderStatus.already_paid_order_id === undefined);
let publicOrderStatusUrl = new URL(orderStatus.order_status_url);
- let publicOrderStatusResp = await axios.get(publicOrderStatusUrl.href, {
- validateStatus: () => true,
- });
+ let publicOrderStatusResp = await harnessHttpLib.fetch(
+ publicOrderStatusUrl.href,
+ );
if (publicOrderStatusResp.status != 402) {
throw Error(
@@ -88,12 +96,12 @@ export async function runPaywallFlowTest(t: GlobalTestState) {
}
let pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode(
- publicOrderStatusResp.data,
+ publicOrderStatusResp.json(),
);
console.log(pubUnpaidStatus);
- let preparePayResp = await wallet.client.call(
+ let preparePayResp = await walletClient.call(
WalletApiOperation.PreparePayForUri,
{
talerPayUri: pubUnpaidStatus.taler_pay_uri,
@@ -105,10 +113,8 @@ export async function runPaywallFlowTest(t: GlobalTestState) {
const proposalId = preparePayResp.proposalId;
console.log("requesting", publicOrderStatusUrl.href);
- publicOrderStatusResp = await axios.get(publicOrderStatusUrl.href, {
- validateStatus: () => true,
- });
- console.log("response body", publicOrderStatusResp.data);
+ publicOrderStatusResp = await harnessHttpLib.fetch(publicOrderStatusUrl.href);
+ console.log("response body", publicOrderStatusResp.json());
if (publicOrderStatusResp.status != 402) {
throw Error(
`expected status 402 (after claiming), but got ${publicOrderStatusResp.status}`,
@@ -116,26 +122,20 @@ export async function runPaywallFlowTest(t: GlobalTestState) {
}
pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode(
- publicOrderStatusResp.data,
+ publicOrderStatusResp.json(),
);
- const confirmPayRes = await wallet.client.call(
- WalletApiOperation.ConfirmPay,
- {
- proposalId: proposalId,
- },
- );
+ const confirmPayRes = await walletClient.call(WalletApiOperation.ConfirmPay, {
+ proposalId: proposalId,
+ });
t.assertTrue(confirmPayRes.type === ConfirmPayResultType.Done);
+ publicOrderStatusResp = await harnessHttpLib.fetch(publicOrderStatusUrl.href);
- publicOrderStatusResp = await axios.get(publicOrderStatusUrl.href, {
- validateStatus: () => true,
- });
-
- console.log(publicOrderStatusResp.data);
+ console.log(publicOrderStatusResp.json());
if (publicOrderStatusResp.status != 200) {
- console.log(publicOrderStatusResp.data);
+ console.log(publicOrderStatusResp.json());
throw Error(
`expected status 200 (after paying), but got ${publicOrderStatusResp.status}`,
);
@@ -147,7 +147,7 @@ export async function runPaywallFlowTest(t: GlobalTestState) {
* =========================================================================
*/
- orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderStatus = await merchantClient.queryPrivateOrderStatus({
orderId: orderResp.order_id,
sessionId: "mysession-two",
});
@@ -158,7 +158,7 @@ export async function runPaywallFlowTest(t: GlobalTestState) {
// Pay with new taler://pay URI, which should
// have the new session ID!
// Wallet should now automatically re-play payment.
- preparePayResp = await wallet.client.call(
+ preparePayResp = await walletClient.call(
WalletApiOperation.PreparePayForUri,
{
talerPayUri: talerPayUriOne,
@@ -174,7 +174,7 @@ export async function runPaywallFlowTest(t: GlobalTestState) {
* =========================================================================
*/
- orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
+ orderResp = await merchantClient.createOrder({
order: {
summary: "Buy me!",
amount: "TESTKUDOS:5",
@@ -186,7 +186,7 @@ export async function runPaywallFlowTest(t: GlobalTestState) {
const secondOrderId = orderResp.order_id;
- orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderStatus = await merchantClient.queryPrivateOrderStatus({
orderId: secondOrderId,
sessionId: "mysession-three",
});
@@ -199,7 +199,7 @@ export async function runPaywallFlowTest(t: GlobalTestState) {
// Here the re-purchase detection should kick in,
// and the wallet should re-pay for the old order
// under the new session ID (mysession-three).
- preparePayResp = await wallet.client.call(
+ preparePayResp = await walletClient.call(
WalletApiOperation.PreparePayForUri,
{
talerPayUri: orderStatus.taler_pay_uri,
@@ -211,7 +211,7 @@ export async function runPaywallFlowTest(t: GlobalTestState) {
// The first order should now be paid under "mysession-three",
// as the wallet did re-purchase detection
- orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderStatus = await merchantClient.queryPrivateOrderStatus({
orderId: firstOrderId,
sessionId: "mysession-three",
});
@@ -220,7 +220,7 @@ export async function runPaywallFlowTest(t: GlobalTestState) {
// Check that with a completely new session ID, the status would NOT
// be paid.
- orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderStatus = await merchantClient.queryPrivateOrderStatus({
orderId: firstOrderId,
sessionId: "mysession-four",
});
@@ -232,19 +232,17 @@ export async function runPaywallFlowTest(t: GlobalTestState) {
console.log("requesting public status", publicOrderStatusUrl);
// Ask the order status of the claimed-but-unpaid order
- publicOrderStatusResp = await axios.get(publicOrderStatusUrl.href, {
- validateStatus: () => true,
- });
+ publicOrderStatusResp = await harnessHttpLib.fetch(publicOrderStatusUrl.href);
if (publicOrderStatusResp.status != 402) {
throw Error(`expected status 402, but got ${publicOrderStatusResp.status}`);
}
pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode(
- publicOrderStatusResp.data,
+ publicOrderStatusResp.json(),
);
- console.log(publicOrderStatusResp.data);
+ console.log(publicOrderStatusResp.json());
t.assertTrue(pubUnpaidStatus.already_paid_order_id === firstOrderId);
}
diff --git a/packages/taler-harness/src/integrationtests/test-peer-pull-large.ts b/packages/taler-harness/src/integrationtests/test-peer-pull-large.ts
new file mode 100644
index 000000000..6de3c2e33
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-peer-pull-large.ts
@@ -0,0 +1,194 @@
+/*
+ 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 {
+ AbsoluteTime,
+ AmountString,
+ Duration,
+ j2s,
+ NotificationType,
+ TransactionMajorState,
+ TransactionMinorState,
+ TransactionType,
+ WalletNotification,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { CoinConfig } from "../harness/denomStructures.js";
+import {
+ BankServiceHandle,
+ ExchangeService,
+ GlobalTestState,
+ WalletClient,
+} from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironmentV2,
+ createWalletDaemonWithClient,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
+
+const coinCommon = {
+ cipher: "RSA" as const,
+ durationLegal: "3 years",
+ durationSpend: "2 years",
+ durationWithdraw: "7 days",
+ feeDeposit: "TESTKUDOS:0",
+ feeRefresh: "TESTKUDOS:0",
+ feeRefund: "TESTKUDOS:0",
+ feeWithdraw: "TESTKUDOS:0",
+ rsaKeySize: 1024,
+};
+
+const coinConfigList: CoinConfig[] = [
+ {
+ ...coinCommon,
+ name: "n1",
+ value: "TESTKUDOS:1",
+ },
+];
+
+export async function runPeerPullLargeTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { bank, exchange } = await createSimpleTestkudosEnvironmentV2(
+ t,
+ coinConfigList,
+ );
+
+ let allW1Notifications: WalletNotification[] = [];
+ let allW2Notifications: WalletNotification[] = [];
+
+ const w1 = await createWalletDaemonWithClient(t, {
+ name: "w1",
+ persistent: true,
+ handleNotification(wn) {
+ allW1Notifications.push(wn);
+ },
+ });
+ const w2 = await createWalletDaemonWithClient(t, {
+ name: "w2",
+ persistent: true,
+ handleNotification(wn) {
+ allW2Notifications.push(wn);
+ },
+ });
+
+ // Withdraw digital cash into the wallet.
+ const wallet1 = w1.walletClient;
+ const wallet2 = w2.walletClient;
+
+ await checkNormalPeerPull(t, bank, exchange, wallet1, wallet2);
+}
+
+async function checkNormalPeerPull(
+ t: GlobalTestState,
+ bank: BankServiceHandle,
+ exchange: ExchangeService,
+ wallet1: WalletClient,
+ wallet2: WalletClient,
+): Promise<void> {
+ const withdrawRes = await withdrawViaBankV2(t, {
+ walletClient: wallet2,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:500",
+ });
+
+ await withdrawRes.withdrawalFinishedCond;
+
+ const purseExpiration = AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ days: 2 }),
+ ),
+ );
+
+ const resp = await wallet1.client.call(
+ WalletApiOperation.InitiatePeerPullCredit,
+ {
+ exchangeBaseUrl: exchange.baseUrl,
+ partialContractTerms: {
+ summary: "Hello World",
+ amount: "TESTKUDOS:200" as AmountString,
+ purse_expiration: purseExpiration,
+ },
+ },
+ );
+
+ const peerPullCreditReadyCond = wallet1.waitForNotificationCond(
+ (x) =>
+ x.type === NotificationType.TransactionStateTransition &&
+ x.transactionId === resp.transactionId &&
+ x.newTxState.major === TransactionMajorState.Pending &&
+ x.newTxState.minor === TransactionMinorState.Ready,
+ );
+
+ await peerPullCreditReadyCond;
+
+ const creditTx = await wallet1.call(WalletApiOperation.GetTransactionById, {
+ transactionId: resp.transactionId,
+ });
+
+ t.assertDeepEqual(creditTx.type, TransactionType.PeerPullCredit);
+ t.assertTrue(!!creditTx.talerUri);
+
+ const checkResp = await wallet2.client.call(
+ WalletApiOperation.PreparePeerPullDebit,
+ {
+ talerUri: creditTx.talerUri,
+ },
+ );
+
+ console.log(`checkResp: ${j2s(checkResp)}`);
+
+ const peerPullCreditDoneCond = wallet1.waitForNotificationCond(
+ (x) =>
+ x.type === NotificationType.TransactionStateTransition &&
+ x.transactionId === resp.transactionId &&
+ x.newTxState.major === TransactionMajorState.Done,
+ );
+
+ const peerPullDebitDoneCond = wallet2.waitForNotificationCond(
+ (x) =>
+ x.type === NotificationType.TransactionStateTransition &&
+ x.transactionId === checkResp.transactionId &&
+ x.newTxState.major === TransactionMajorState.Done,
+ );
+
+ await wallet2.client.call(WalletApiOperation.ConfirmPeerPullDebit, {
+ transactionId: checkResp.transactionId,
+ });
+
+ await peerPullCreditDoneCond;
+ await peerPullDebitDoneCond;
+
+ const txn1 = await wallet1.client.call(
+ WalletApiOperation.GetTransactions,
+ {},
+ );
+
+ const txn2 = await wallet2.client.call(
+ WalletApiOperation.GetTransactions,
+ {},
+ );
+
+ console.log(`txn1: ${j2s(txn1)}`);
+ console.log(`txn2: ${j2s(txn2)}`);
+}
+
+runPeerPullLargeTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-peer-push-large.ts b/packages/taler-harness/src/integrationtests/test-peer-push-large.ts
new file mode 100644
index 000000000..b7fbe9f6e
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-peer-push-large.ts
@@ -0,0 +1,177 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ AbsoluteTime,
+ AmountString,
+ Duration,
+ NotificationType,
+ TransactionMajorState,
+ TransactionMinorState,
+ TransactionType,
+ WalletNotification,
+ j2s,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState } from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironmentV2,
+ createWalletDaemonWithClient,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
+import { CoinConfig } from "../harness/denomStructures.js";
+
+const coinCommon = {
+ cipher: "RSA" as const,
+ durationLegal: "3 years",
+ durationSpend: "2 years",
+ durationWithdraw: "7 days",
+ feeDeposit: "TESTKUDOS:0",
+ feeRefresh: "TESTKUDOS:0",
+ feeRefund: "TESTKUDOS:0",
+ feeWithdraw: "TESTKUDOS:0",
+ rsaKeySize: 1024,
+};
+
+const coinConfigList: CoinConfig[] = [
+ {
+ ...coinCommon,
+ name: "n1",
+ value: "TESTKUDOS:1",
+ },
+];
+
+/**
+ * Run a test for a multi-batch peer push payment.
+ */
+export async function runPeerPushLargeTest(t: GlobalTestState) {
+ const { bank, exchange } = await createSimpleTestkudosEnvironmentV2(t, coinConfigList);
+
+ let allW1Notifications: WalletNotification[] = [];
+ let allW2Notifications: WalletNotification[] = [];
+
+ const w1 = await createWalletDaemonWithClient(t, {
+ name: "w1",
+ handleNotification(wn) {
+ allW1Notifications.push(wn);
+ },
+ });
+ const w2 = await createWalletDaemonWithClient(t, {
+ name: "w2",
+ handleNotification(wn) {
+ allW2Notifications.push(wn);
+ },
+ });
+
+ // Withdraw digital cash into the wallet.
+
+ const withdrawRes = await withdrawViaBankV2(t, {
+ walletClient: w1.walletClient,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:300",
+ });
+
+ await withdrawRes.withdrawalFinishedCond;
+
+ const purse_expiration = AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ days: 2 }),
+ ),
+ );
+
+ const checkResp0 = await w1.walletClient.call(
+ WalletApiOperation.CheckPeerPushDebit,
+ {
+ amount: "TESTKUDOS:200" as AmountString,
+ },
+ );
+
+ t.assertAmountEquals(checkResp0.amountEffective, "TESTKUDOS:200");
+
+ const resp = await w1.walletClient.call(
+ WalletApiOperation.InitiatePeerPushDebit,
+ {
+ partialContractTerms: {
+ summary: "Hello World 🥺",
+ amount: "TESTKUDOS:200" as AmountString,
+ purse_expiration,
+ },
+ },
+ );
+
+ console.log(resp);
+
+ const peerPushReadyCond = w1.walletClient.waitForNotificationCond(
+ (x) =>
+ x.type === NotificationType.TransactionStateTransition &&
+ x.newTxState.major === TransactionMajorState.Pending &&
+ x.newTxState.minor === TransactionMinorState.Ready &&
+ x.transactionId === resp.transactionId,
+ );
+
+ await peerPushReadyCond;
+
+ const txDetails = await w1.walletClient.call(
+ WalletApiOperation.GetTransactionById,
+ {
+ transactionId: resp.transactionId,
+ },
+ );
+ t.assertDeepEqual(txDetails.type, TransactionType.PeerPushDebit);
+ t.assertTrue(!!txDetails.talerUri);
+
+ const checkResp = await w2.walletClient.call(
+ WalletApiOperation.PreparePeerPushCredit,
+ {
+ talerUri: txDetails.talerUri,
+ },
+ );
+
+ console.log(checkResp);
+
+ const acceptResp = await w2.walletClient.call(
+ WalletApiOperation.ConfirmPeerPushCredit,
+ {
+ transactionId: checkResp.transactionId,
+ },
+ );
+
+ console.log(acceptResp);
+
+ await w2.walletClient.call(
+ WalletApiOperation.TestingWaitTransactionsFinal,
+ {},
+ );
+
+ const txn1 = await w1.walletClient.call(
+ WalletApiOperation.GetTransactions,
+ {},
+ );
+ const txn2 = await w2.walletClient.call(
+ WalletApiOperation.GetTransactions,
+ {},
+ );
+
+ console.log(`txn1: ${j2s(txn1)}`);
+ console.log(`txn2: ${j2s(txn2)}`);
+}
+
+runPeerPushLargeTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-peer-repair.ts b/packages/taler-harness/src/integrationtests/test-peer-repair.ts
new file mode 100644
index 000000000..8bc7ed0a1
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-peer-repair.ts
@@ -0,0 +1,213 @@
+/*
+ This file is part of GNU Taler
+ (C) 2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ AbsoluteTime,
+ AmountString,
+ Duration,
+ NotificationType,
+ TransactionMajorState,
+ TransactionMinorState,
+ TransactionType,
+ WalletNotification,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import * as fs from "node:fs";
+import { GlobalTestState } from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironmentV2,
+ createWalletDaemonWithClient,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
+
+export async function runPeerRepairTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { bank, exchange } = await createSimpleTestkudosEnvironmentV2(t);
+
+ let allW1Notifications: WalletNotification[] = [];
+ let allW2Notifications: WalletNotification[] = [];
+
+ let w1 = await createWalletDaemonWithClient(t, {
+ name: "w1",
+ persistent: true,
+ handleNotification(wn) {
+ allW1Notifications.push(wn);
+ },
+ });
+ const w2 = await createWalletDaemonWithClient(t, {
+ name: "w2",
+ handleNotification(wn) {
+ allW2Notifications.push(wn);
+ },
+ });
+
+ // Withdraw digital cash into the wallet.
+ let wallet1 = w1.walletClient;
+ const wallet2 = w2.walletClient;
+
+ const withdrawalDoneCond = wallet1.waitForNotificationCond(
+ (x) =>
+ x.type === NotificationType.TransactionStateTransition &&
+ x.newTxState.major === TransactionMajorState.Done &&
+ x.transactionId.startsWith("txn:withdrawal:"),
+ );
+
+ await withdrawViaBankV2(t, {
+ walletClient: wallet1,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:5",
+ });
+
+ await withdrawalDoneCond;
+ const w1DbPath = w1.walletService.dbPath;
+ const w1DbCopyPath = w1.walletService.dbPath + ".copy";
+ fs.copyFileSync(w1DbPath, w1DbCopyPath);
+
+ const purse_expiration = AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ days: 2 }),
+ ),
+ );
+
+ const resp1 = await wallet1.client.call(
+ WalletApiOperation.InitiatePeerPushDebit,
+ {
+ exchangeBaseUrl: exchange.baseUrl,
+ partialContractTerms: {
+ summary: "Hello World",
+ amount: "TESTKUDOS:3" as AmountString,
+ purse_expiration,
+ },
+ },
+ );
+
+ const peerPushDebitReady1Cond = wallet1.waitForNotificationCond(
+ (x) =>
+ x.type === NotificationType.TransactionStateTransition &&
+ x.transactionId === resp1.transactionId &&
+ x.newTxState.major === TransactionMajorState.Pending &&
+ x.newTxState.minor === TransactionMinorState.Ready,
+ );
+
+ await peerPushDebitReady1Cond;
+
+ const txDetails = await wallet1.call(WalletApiOperation.GetTransactionById, {
+ transactionId: resp1.transactionId,
+ });
+ t.assertDeepEqual(txDetails.type, TransactionType.PeerPushDebit);
+ t.assertTrue(!!txDetails.talerUri);
+
+ const resp2 = await wallet2.client.call(
+ WalletApiOperation.PreparePeerPushCredit,
+ {
+ talerUri: txDetails.talerUri,
+ },
+ );
+
+ const peerPushCreditDone1Cond = wallet2.waitForNotificationCond(
+ (x) =>
+ x.type === NotificationType.TransactionStateTransition &&
+ x.transactionId === resp2.transactionId &&
+ x.newTxState.major === TransactionMajorState.Done,
+ );
+
+ const peerPushDebitDone1Cond = wallet1.waitForNotificationCond(
+ (x) =>
+ x.type === NotificationType.TransactionStateTransition &&
+ x.transactionId === resp1.transactionId &&
+ x.newTxState.major === TransactionMajorState.Done,
+ );
+
+ await wallet2.client.call(WalletApiOperation.ConfirmPeerPushCredit, {
+ transactionId: resp2.transactionId,
+ });
+
+ await peerPushCreditDone1Cond;
+ await peerPushDebitDone1Cond;
+
+ w1.walletClient.remoteWallet?.close();
+ await w1.walletService.stop();
+
+ fs.copyFileSync(w1DbCopyPath, w1DbPath);
+
+ console.log(`copied back to ${w1DbPath}`);
+
+ w1 = await createWalletDaemonWithClient(t, {
+ name: "w1",
+ persistent: true,
+ handleNotification(wn) {
+ allW1Notifications.push(wn);
+ },
+ });
+ wallet1 = w1.walletClient;
+
+ console.log("attempting peer-push-debit, should fail.");
+
+ const initResp2 = await wallet1.client.call(
+ WalletApiOperation.InitiatePeerPushDebit,
+ {
+ exchangeBaseUrl: exchange.baseUrl,
+ partialContractTerms: {
+ summary: "Hello World",
+ amount: "TESTKUDOS:3" as AmountString,
+ purse_expiration,
+ },
+ },
+ );
+
+ const peerPushDebitFailingCond = wallet1.waitForNotificationCond(
+ (x) =>
+ x.type === NotificationType.TransactionStateTransition &&
+ x.transactionId === initResp2.transactionId &&
+ x.newTxState.major === TransactionMajorState.Pending &&
+ x.errorInfo != null,
+ );
+
+ console.log(`waiting for error on ${initResp2.transactionId}`);
+
+ await peerPushDebitFailingCond;
+
+ console.log("reached error");
+
+ // Now withdraw so we have enough coins to re-select
+
+ const withdraw2Res = await withdrawViaBankV2(t, {
+ walletClient: wallet1,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:5",
+ });
+
+ await withdraw2Res.withdrawalFinishedCond;
+
+ const peerPushDebitReady2Cond = wallet1.waitForNotificationCond(
+ (x) =>
+ x.type === NotificationType.TransactionStateTransition &&
+ x.transactionId === initResp2.transactionId &&
+ x.newTxState.major === TransactionMajorState.Pending &&
+ x.newTxState.minor === TransactionMinorState.Ready,
+ );
+
+ await peerPushDebitReady2Cond;
+}
+
+runPeerRepairTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-peer-to-peer-pull.ts b/packages/taler-harness/src/integrationtests/test-peer-to-peer-pull.ts
new file mode 100644
index 000000000..e1565f295
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-peer-to-peer-pull.ts
@@ -0,0 +1,285 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ AbsoluteTime,
+ AmountString,
+ Duration,
+ j2s,
+ NotificationType,
+ TransactionMajorState,
+ TransactionMinorState,
+ TransactionType,
+ WalletNotification,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import {
+ BankServiceHandle,
+ ExchangeService,
+ GlobalTestState,
+ WalletClient,
+} from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironmentV2,
+ createWalletDaemonWithClient,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
+
+/**
+ * Run test for basic, bank-integrated withdrawal and payment.
+ */
+export async function runPeerToPeerPullTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { bank, exchange } = await createSimpleTestkudosEnvironmentV2(t);
+
+ let allW1Notifications: WalletNotification[] = [];
+ let allW2Notifications: WalletNotification[] = [];
+
+ const w1 = await createWalletDaemonWithClient(t, {
+ name: "w1",
+ persistent: true,
+ handleNotification(wn) {
+ allW1Notifications.push(wn);
+ },
+ });
+ const w2 = await createWalletDaemonWithClient(t, {
+ name: "w2",
+ persistent: true,
+ handleNotification(wn) {
+ allW2Notifications.push(wn);
+ },
+ });
+
+ // Withdraw digital cash into the wallet.
+ const wallet1 = w1.walletClient;
+ const wallet2 = w2.walletClient;
+
+ await checkNormalPeerPull(t, bank, exchange, wallet1, wallet2);
+
+ console.log(`w1 notifications: ${j2s(allW1Notifications)}`);
+
+ // Check that we don't have an excessive number of notifications.
+ t.assertTrue(allW1Notifications.length <= 60);
+
+ await checkAbortedPeerPull(t, bank, exchange, wallet1, wallet2);
+}
+
+async function checkNormalPeerPull(
+ t: GlobalTestState,
+ bank: BankServiceHandle,
+ exchange: ExchangeService,
+ wallet1: WalletClient,
+ wallet2: WalletClient,
+): Promise<void> {
+ const withdrawRes = await withdrawViaBankV2(t, {
+ walletClient: wallet2,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ });
+
+ await withdrawRes.withdrawalFinishedCond;
+
+ const purseExpiration = AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ days: 2 }),
+ ),
+ );
+
+ const resp = await wallet1.client.call(
+ WalletApiOperation.InitiatePeerPullCredit,
+ {
+ exchangeBaseUrl: exchange.baseUrl,
+ partialContractTerms: {
+ summary: "Hello World",
+ amount: "TESTKUDOS:5" as AmountString,
+ purse_expiration: purseExpiration,
+ },
+ },
+ );
+
+ const peerPullCreditReadyCond = wallet1.waitForNotificationCond(
+ (x) =>
+ x.type === NotificationType.TransactionStateTransition &&
+ x.transactionId === resp.transactionId &&
+ x.newTxState.major === TransactionMajorState.Pending &&
+ x.newTxState.minor === TransactionMinorState.Ready,
+ );
+
+ await peerPullCreditReadyCond;
+
+ const creditTx = await wallet1.call(WalletApiOperation.GetTransactionById, {
+ transactionId: resp.transactionId,
+ });
+
+ t.assertDeepEqual(creditTx.type, TransactionType.PeerPullCredit);
+ t.assertTrue(!!creditTx.talerUri);
+
+ const checkResp = await wallet2.client.call(
+ WalletApiOperation.PreparePeerPullDebit,
+ {
+ talerUri: creditTx.talerUri,
+ },
+ );
+
+ console.log(`checkResp: ${j2s(checkResp)}`);
+
+ const peerPullCreditDoneCond = wallet1.waitForNotificationCond(
+ (x) =>
+ x.type === NotificationType.TransactionStateTransition &&
+ x.transactionId === resp.transactionId &&
+ x.newTxState.major === TransactionMajorState.Done,
+ );
+
+ const peerPullDebitDoneCond = wallet2.waitForNotificationCond(
+ (x) =>
+ x.type === NotificationType.TransactionStateTransition &&
+ x.transactionId === checkResp.transactionId &&
+ x.newTxState.major === TransactionMajorState.Done,
+ );
+
+ await wallet2.client.call(WalletApiOperation.ConfirmPeerPullDebit, {
+ transactionId: checkResp.transactionId,
+ });
+
+ await peerPullCreditDoneCond;
+ await peerPullDebitDoneCond;
+
+ const txn1 = await wallet1.client.call(
+ WalletApiOperation.GetTransactions,
+ {},
+ );
+
+ const txn2 = await wallet2.client.call(
+ WalletApiOperation.GetTransactions,
+ {},
+ );
+
+ console.log(`txn1: ${j2s(txn1)}`);
+ console.log(`txn2: ${j2s(txn2)}`);
+}
+
+async function checkAbortedPeerPull(
+ t: GlobalTestState,
+ bank: BankServiceHandle,
+ exchange: ExchangeService,
+ wallet1: WalletClient,
+ wallet2: WalletClient,
+): Promise<void> {
+ const withdrawRes = await withdrawViaBankV2(t, {
+ walletClient: wallet2,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ });
+
+ await withdrawRes.withdrawalFinishedCond;
+
+ const purseExpiration = AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ days: 2 }),
+ ),
+ );
+
+ const resp = await wallet1.client.call(
+ WalletApiOperation.InitiatePeerPullCredit,
+ {
+ exchangeBaseUrl: exchange.baseUrl,
+ partialContractTerms: {
+ summary: "Hello World",
+ amount: "TESTKUDOS:5" as AmountString,
+ purse_expiration: purseExpiration,
+ },
+ },
+ );
+
+ const peerPullCreditReadyCond = wallet1.waitForNotificationCond(
+ (x) =>
+ x.type === NotificationType.TransactionStateTransition &&
+ x.transactionId === resp.transactionId &&
+ x.newTxState.major === TransactionMajorState.Pending &&
+ x.newTxState.minor === TransactionMinorState.Ready,
+ );
+
+ await peerPullCreditReadyCond;
+
+ const creditTx = await wallet1.call(WalletApiOperation.GetTransactionById, {
+ transactionId: resp.transactionId,
+ });
+
+ t.assertDeepEqual(creditTx.type, TransactionType.PeerPullCredit);
+ t.assertTrue(!!creditTx.talerUri);
+
+ const checkResp = await wallet2.client.call(
+ WalletApiOperation.PreparePeerPullDebit,
+ {
+ talerUri: creditTx.talerUri,
+ },
+ );
+
+ console.log(`checkResp: ${j2s(checkResp)}`);
+
+ const peerPullCreditAbortedCond = wallet1.waitForNotificationCond(
+ (x) =>
+ x.type === NotificationType.TransactionStateTransition &&
+ x.transactionId === resp.transactionId &&
+ x.newTxState.major === TransactionMajorState.Aborted,
+ );
+
+ const peerPullDebitAbortedCond = wallet2.waitForNotificationCond(
+ (x) =>
+ x.type === NotificationType.TransactionStateTransition &&
+ x.transactionId === checkResp.transactionId &&
+ x.newTxState.major === TransactionMajorState.Aborted,
+ );
+
+ await wallet1.call(WalletApiOperation.AbortTransaction, {
+ transactionId: resp.transactionId,
+ });
+
+ await wallet2.client.call(WalletApiOperation.ConfirmPeerPullDebit, {
+ transactionId: checkResp.transactionId,
+ });
+
+ console.log(`waiting for ${resp.transactionId} to go to state aborted`);
+ console.log("checkpoint: before-aborted-wait");
+ await peerPullCreditAbortedCond;
+ console.log("checkpoint: after-credit-aborted-wait");
+ await peerPullDebitAbortedCond;
+ console.log("checkpoint: after-debit-aborted-wait");
+ console.log("checkpoint: after-aborted-wait");
+
+ const txn1 = await wallet1.client.call(
+ WalletApiOperation.GetTransactions,
+ {},
+ );
+
+ const txn2 = await wallet2.client.call(
+ WalletApiOperation.GetTransactions,
+ {},
+ );
+
+ console.log(`txn1: ${j2s(txn1)}`);
+ console.log(`txn2: ${j2s(txn2)}`);
+}
+
+runPeerToPeerPullTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-peer-to-peer-push.ts b/packages/taler-harness/src/integrationtests/test-peer-to-peer-push.ts
new file mode 100644
index 000000000..21e0d384a
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-peer-to-peer-push.ts
@@ -0,0 +1,264 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ AbsoluteTime,
+ AmountString,
+ Duration,
+ NotificationType,
+ TransactionMajorState,
+ TransactionMinorState,
+ TransactionType,
+ WalletNotification,
+ j2s,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState } from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironmentV2,
+ createWalletDaemonWithClient,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
+
+/**
+ * Run a test for basic peer-push payments.
+ */
+export async function runPeerToPeerPushTest(t: GlobalTestState) {
+ const { bank, exchange } = await createSimpleTestkudosEnvironmentV2(t);
+
+ let allW1Notifications: WalletNotification[] = [];
+ let allW2Notifications: WalletNotification[] = [];
+
+ const w1 = await createWalletDaemonWithClient(t, {
+ name: "w1",
+ handleNotification(wn) {
+ allW1Notifications.push(wn);
+ },
+ });
+ const w2 = await createWalletDaemonWithClient(t, {
+ name: "w2",
+ handleNotification(wn) {
+ allW2Notifications.push(wn);
+ },
+ });
+
+ // Withdraw digital cash into the wallet.
+
+ const withdrawRes = await withdrawViaBankV2(t, {
+ walletClient: w1.walletClient,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ });
+
+ await withdrawRes.withdrawalFinishedCond;
+
+ const purse_expiration = AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ days: 2 }),
+ ),
+ );
+
+ const checkResp0 = await w1.walletClient.call(
+ WalletApiOperation.CheckPeerPushDebit,
+ {
+ amount: "TESTKUDOS:5" as AmountString,
+ },
+ );
+
+ t.assertAmountEquals(checkResp0.amountEffective, "TESTKUDOS:5.49");
+
+ {
+ const resp = await w1.walletClient.call(
+ WalletApiOperation.InitiatePeerPushDebit,
+ {
+ partialContractTerms: {
+ summary: "Hello World 😁😇",
+ amount: "TESTKUDOS:5" as AmountString,
+ purse_expiration,
+ },
+ },
+ );
+
+ console.log(resp);
+ }
+
+ {
+ const bal = await w1.walletClient.call(WalletApiOperation.GetBalances, {});
+ t.assertAmountEquals(bal.balances[0].pendingOutgoing, "TESTKUDOS:5.49");
+ }
+
+ await w1.walletClient.call(WalletApiOperation.TestingWaitRefreshesFinal, {});
+
+ const resp = await w1.walletClient.call(
+ WalletApiOperation.InitiatePeerPushDebit,
+ {
+ partialContractTerms: {
+ summary: "Hello World 🥺",
+ amount: "TESTKUDOS:5" as AmountString,
+ purse_expiration,
+ },
+ },
+ );
+
+ console.log(resp);
+
+ const peerPushReadyCond = w1.walletClient.waitForNotificationCond(
+ (x) =>
+ x.type === NotificationType.TransactionStateTransition &&
+ x.newTxState.major === TransactionMajorState.Pending &&
+ x.newTxState.minor === TransactionMinorState.Ready &&
+ x.transactionId === resp.transactionId,
+ );
+
+ await peerPushReadyCond;
+
+ const txDetails = await w1.walletClient.call(
+ WalletApiOperation.GetTransactionById,
+ {
+ transactionId: resp.transactionId,
+ },
+ );
+ t.assertDeepEqual(txDetails.type, TransactionType.PeerPushDebit);
+ t.assertTrue(!!txDetails.talerUri);
+
+ const checkResp = await w2.walletClient.call(
+ WalletApiOperation.PreparePeerPushCredit,
+ {
+ talerUri: txDetails.talerUri,
+ },
+ );
+
+ console.log(checkResp);
+
+ const acceptResp = await w2.walletClient.call(
+ WalletApiOperation.ConfirmPeerPushCredit,
+ {
+ transactionId: checkResp.transactionId,
+ },
+ );
+
+ console.log(acceptResp);
+
+ const txn1 = await w1.walletClient.call(
+ WalletApiOperation.GetTransactions,
+ {},
+ );
+ const txn2 = await w2.walletClient.call(
+ WalletApiOperation.GetTransactions,
+ {},
+ );
+
+ console.log(`txn1: ${j2s(txn1)}`);
+ console.log(`txn2: ${j2s(txn2)}`);
+
+ // We expect insufficient balance here!
+ const ex1 = await t.assertThrowsTalerErrorAsync(async () => {
+ await w1.walletClient.call(WalletApiOperation.InitiatePeerPushDebit, {
+ partialContractTerms: {
+ summary: "(this will fail)",
+ amount: "TESTKUDOS:15" as AmountString,
+ purse_expiration,
+ },
+ });
+ });
+
+ console.log("got expected exception detail", j2s(ex1.errorDetail));
+
+ const initiateResp2 = await w1.walletClient.call(
+ WalletApiOperation.InitiatePeerPushDebit,
+ {
+ partialContractTerms: {
+ summary: "second tx, will expire",
+ amount: "TESTKUDOS:5" as AmountString,
+ purse_expiration,
+ },
+ },
+ );
+
+ const peerPushReadyCond2 = w1.walletClient.waitForNotificationCond(
+ (x) =>
+ x.type === NotificationType.TransactionStateTransition &&
+ x.newTxState.major === TransactionMajorState.Pending &&
+ x.newTxState.minor === TransactionMinorState.Ready &&
+ x.transactionId === initiateResp2.transactionId,
+ );
+
+ await peerPushReadyCond2;
+
+ const txDetails3 = await w1.walletClient.call(
+ WalletApiOperation.GetTransactionById,
+ {
+ transactionId: initiateResp2.transactionId,
+ },
+ );
+ t.assertDeepEqual(txDetails3.type, TransactionType.PeerPushDebit);
+ t.assertTrue(!!txDetails3.talerUri);
+
+ await w2.walletClient.call(WalletApiOperation.PreparePeerPushCredit, {
+ talerUri: txDetails3.talerUri,
+ });
+
+ const timetravelOffsetMs = Duration.toMilliseconds(
+ Duration.fromSpec({ days: 5 }),
+ );
+
+ console.log("stopping exchange to apply time-travel");
+
+ await exchange.stop();
+ exchange.setTimetravel(timetravelOffsetMs);
+ await exchange.start();
+ await exchange.pingUntilAvailable();
+
+ console.log("running expire");
+ await exchange.runExpireOnce();
+ console.log("done running expire");
+
+ console.log("purse should now be expired");
+
+ await w1.walletClient.call(WalletApiOperation.TestingSetTimetravel, {
+ offsetMs: timetravelOffsetMs,
+ });
+
+ await w2.walletClient.call(WalletApiOperation.TestingSetTimetravel, {
+ offsetMs: timetravelOffsetMs,
+ });
+
+ await w1.walletClient.call(
+ WalletApiOperation.TestingWaitTransactionsFinal,
+ {},
+ );
+
+ await w2.walletClient.call(
+ WalletApiOperation.TestingWaitTransactionsFinal,
+ {},
+ );
+
+ const txDetails2 = await w1.walletClient.call(
+ WalletApiOperation.GetTransactionById,
+ {
+ transactionId: initiateResp2.transactionId,
+ },
+ );
+
+ console.log(`tx details 2: ${j2s(txDetails2)}`);
+}
+
+runPeerToPeerPushTest.suites = ["wallet"];
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-refund-auto.ts b/packages/taler-harness/src/integrationtests/test-refund-auto.ts
index 4c2a2f94a..2a2e26ea4 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-refund-auto.ts
+++ b/packages/taler-harness/src/integrationtests/test-refund-auto.ts
@@ -17,13 +17,13 @@
/**
* Imports.
*/
-import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js";
+import { Duration, MerchantApiClient } from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState } from "../harness/harness.js";
import {
- createSimpleTestkudosEnvironment,
- withdrawViaBank,
+ createSimpleTestkudosEnvironmentV2,
+ withdrawViaBankV2,
} from "../harness/helpers.js";
-import { Duration, durationFromSpec } from "@gnu-taler/taler-util";
-import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
/**
* Run test for basic, bank-integrated withdrawal.
@@ -31,15 +31,24 @@ import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
export async function runRefundAutoTest(t: GlobalTestState) {
// Set up test environment
- const { wallet, bank, exchange, merchant } =
- await createSimpleTestkudosEnvironment(t);
+ const { walletClient, bank, exchange, merchant } =
+ await createSimpleTestkudosEnvironmentV2(t);
+
+ const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl());
// Withdraw digital cash into the wallet.
- await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
+ const wres = await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ });
+
+ await wres.withdrawalFinishedCond;
// Set up order.
- const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
+ const orderResp = await merchantClient.createOrder({
order: {
summary: "Buy me!",
amount: "TESTKUDOS:5",
@@ -49,11 +58,11 @@ export async function runRefundAutoTest(t: GlobalTestState) {
},
},
refund_delay: Duration.toTalerProtocolDuration(
- durationFromSpec({ minutes: 5 }),
+ Duration.fromSpec({ minutes: 5 }),
),
});
- let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ let orderStatus = await merchantClient.queryPrivateOrderStatus({
orderId: orderResp.order_id,
});
@@ -61,24 +70,23 @@ export async function runRefundAutoTest(t: GlobalTestState) {
// Make wallet pay for the order
- const r1 = await wallet.client.call(WalletApiOperation.PreparePayForUri, {
+ const r1 = await walletClient.call(WalletApiOperation.PreparePayForUri, {
talerPayUri: orderStatus.taler_pay_uri,
});
- await wallet.client.call(WalletApiOperation.ConfirmPay, {
- // FIXME: should be validated, don't cast!
- proposalId: r1.proposalId,
+ await walletClient.call(WalletApiOperation.ConfirmPay, {
+ transactionId: r1.transactionId,
});
// Check if payment was successful.
- orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderStatus = await merchantClient.queryPrivateOrderStatus({
orderId: orderResp.order_id,
});
t.assertTrue(orderStatus.order_status === "paid");
- const ref = await MerchantPrivateApi.giveRefund(merchant, {
+ const ref = await merchantClient.giveRefund({
amount: "TESTKUDOS:5",
instance: "default",
justification: "foo",
@@ -88,9 +96,9 @@ export async function runRefundAutoTest(t: GlobalTestState) {
console.log(ref);
// The wallet should now automatically pick up the refund.
- await wallet.runUntilDone();
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
- const transactions = await wallet.client.call(
+ const transactions = await walletClient.call(
WalletApiOperation.GetTransactions,
{},
);
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-refund-gone.ts b/packages/taler-harness/src/integrationtests/test-refund-gone.ts
index b6cefda86..8a661868f 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-refund-gone.ts
+++ b/packages/taler-harness/src/integrationtests/test-refund-gone.ts
@@ -17,54 +17,66 @@
/**
* Imports.
*/
-import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js";
-import {
- createSimpleTestkudosEnvironment,
- withdrawViaBank,
- applyTimeTravel,
-} from "../harness/helpers.js";
import {
AbsoluteTime,
Duration,
- durationFromSpec,
+ MerchantApiClient,
+ TransactionMajorState,
+ TransactionType,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState } from "../harness/harness.js";
+import {
+ applyTimeTravelV2,
+ createSimpleTestkudosEnvironmentV2,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
/**
- * Run test for basic, bank-integrated withdrawal.
+ * Test wallet behavior when a refund expires before the wallet
+ * can claim it.
*/
export async function runRefundGoneTest(t: GlobalTestState) {
// Set up test environment
- const { wallet, bank, exchange, merchant } =
- await createSimpleTestkudosEnvironment(t);
+ const { walletClient, bank, exchange, merchant } =
+ await createSimpleTestkudosEnvironmentV2(t);
+
+ const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl());
// Withdraw digital cash into the wallet.
- await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
+ const wres = await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ });
+
+ await wres.withdrawalFinishedCond;
// Set up order.
- const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
+ const orderResp = await merchantClient.createOrder({
order: {
summary: "Buy me!",
amount: "TESTKUDOS:5",
fulfillment_url: "taler://fulfillment-success/thx",
- pay_deadline: AbsoluteTime.toTimestamp(
+ pay_deadline: AbsoluteTime.toProtocolTimestamp(
AbsoluteTime.addDuration(
AbsoluteTime.now(),
- durationFromSpec({
+ Duration.fromSpec({
minutes: 10,
}),
),
),
},
refund_delay: Duration.toTalerProtocolDuration(
- durationFromSpec({ minutes: 1 }),
+ Duration.fromSpec({ minutes: 1 }),
),
});
- let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ let orderStatus = await merchantClient.queryPrivateOrderStatus({
orderId: orderResp.order_id,
});
@@ -72,17 +84,17 @@ export async function runRefundGoneTest(t: GlobalTestState) {
// Make wallet pay for the order
- const r1 = await wallet.client.call(WalletApiOperation.PreparePayForUri, {
+ const r1 = await walletClient.call(WalletApiOperation.PreparePayForUri, {
talerPayUri: orderStatus.taler_pay_uri,
});
- const r2 = await wallet.client.call(WalletApiOperation.ConfirmPay, {
- proposalId: r1.proposalId,
+ const r2 = await walletClient.call(WalletApiOperation.ConfirmPay, {
+ transactionId: r1.transactionId,
});
// Check if payment was successful.
- orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderStatus = await merchantClient.queryPrivateOrderStatus({
orderId: orderResp.order_id,
});
@@ -90,11 +102,14 @@ export async function runRefundGoneTest(t: GlobalTestState) {
console.log(orderStatus);
- await applyTimeTravel(durationFromSpec({ hours: 1 }), { exchange, wallet });
-
+ await applyTimeTravelV2(
+ Duration.toMilliseconds(Duration.fromSpec({ hours: 1 })),
+ { exchange, merchant, walletClient: walletClient },
+ );
+ await exchange.stopAggregator();
await exchange.runAggregatorOnce();
- const ref = await MerchantPrivateApi.giveRefund(merchant, {
+ const ref = await merchantClient.giveRefund({
amount: "TESTKUDOS:5",
instance: "default",
justification: "foo",
@@ -103,22 +118,22 @@ export async function runRefundGoneTest(t: GlobalTestState) {
console.log(ref);
- let rr = await wallet.client.call(WalletApiOperation.ApplyRefund, {
- talerRefundUri: ref.talerRefundUri,
+ await walletClient.call(WalletApiOperation.StartRefundQuery, {
+ transactionId: r1.transactionId,
});
- console.log("refund response:", rr);
- t.assertAmountEquals(rr.amountRefundGone, "TESTKUDOS:5");
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
- await wallet.runUntilDone();
-
- let r = await wallet.client.call(WalletApiOperation.GetBalances, {});
+ let r = await walletClient.call(WalletApiOperation.GetBalances, {});
console.log(JSON.stringify(r, undefined, 2));
- const r3 = await wallet.client.call(WalletApiOperation.GetTransactions, {});
+ const r3 = await walletClient.call(WalletApiOperation.GetTransactions, {});
console.log(JSON.stringify(r3, undefined, 2));
- await t.shutdown();
+ const refundTx = r3.transactions[2];
+
+ t.assertDeepEqual(refundTx.type, TransactionType.Refund);
+ t.assertDeepEqual(refundTx.txState.major, TransactionMajorState.Failed);
}
runRefundGoneTest.suites = ["wallet"];
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-refund-incremental.ts b/packages/taler-harness/src/integrationtests/test-refund-incremental.ts
index 8d1f6e873..8a5d23315 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-refund-incremental.ts
+++ b/packages/taler-harness/src/integrationtests/test-refund-incremental.ts
@@ -18,21 +18,17 @@
* Imports.
*/
import {
- GlobalTestState,
- delayMs,
- MerchantPrivateApi,
-} from "../harness/harness.js";
-import {
- createSimpleTestkudosEnvironment,
- withdrawViaBank,
-} from "../harness/helpers.js";
-import {
- TransactionType,
Amounts,
- durationFromSpec,
Duration,
+ MerchantApiClient,
+ TransactionType,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState, delayMs } from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironmentV2,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
/**
* Run test for basic, bank-integrated withdrawal.
@@ -40,27 +36,36 @@ import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
export async function runRefundIncrementalTest(t: GlobalTestState) {
// Set up test environment
- const { wallet, bank, exchange, merchant } =
- await createSimpleTestkudosEnvironment(t);
+ const { walletClient, bank, exchange, merchant } =
+ await createSimpleTestkudosEnvironmentV2(t);
+
+ const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl());
// Withdraw digital cash into the wallet.
- await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
+ const wres = await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ });
+
+ await wres.withdrawalFinishedCond;
// Set up order.
- const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
+ const orderResp = await merchantClient.createOrder({
order: {
summary: "Buy me!",
amount: "TESTKUDOS:10",
fulfillment_url: "taler://fulfillment-success/thx",
},
refund_delay: Duration.toTalerProtocolDuration(
- durationFromSpec({ minutes: 5 }),
+ Duration.fromSpec({ minutes: 5 }),
),
});
- let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ let orderStatus = await merchantClient.queryPrivateOrderStatus({
orderId: orderResp.order_id,
});
@@ -68,23 +73,23 @@ export async function runRefundIncrementalTest(t: GlobalTestState) {
// Make wallet pay for the order
- const r1 = await wallet.client.call(WalletApiOperation.PreparePayForUri, {
+ const r1 = await walletClient.call(WalletApiOperation.PreparePayForUri, {
talerPayUri: orderStatus.taler_pay_uri,
});
- await wallet.client.call(WalletApiOperation.ConfirmPay, {
+ await walletClient.call(WalletApiOperation.ConfirmPay, {
proposalId: r1.proposalId,
});
// Check if payment was successful.
- orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderStatus = await merchantClient.queryPrivateOrderStatus({
orderId: orderResp.order_id,
});
t.assertTrue(orderStatus.order_status === "paid");
- let ref = await MerchantPrivateApi.giveRefund(merchant, {
+ let ref = await merchantClient.giveRefund({
amount: "TESTKUDOS:2.5",
instance: "default",
justification: "foo",
@@ -94,14 +99,15 @@ export async function runRefundIncrementalTest(t: GlobalTestState) {
console.log("first refund increase response", ref);
{
- let wr = await wallet.client.call(WalletApiOperation.ApplyRefund, {
- talerRefundUri: ref.talerRefundUri,
+ let wr = await walletClient.call(WalletApiOperation.StartRefundQuery, {
+ transactionId: r1.transactionId,
});
- console.log(wr);
- const txs = await wallet.client.call(
- WalletApiOperation.GetTransactions,
+ await walletClient.call(
+ WalletApiOperation.TestingWaitTransactionsFinal,
{},
);
+ console.log(wr);
+ const txs = await walletClient.call(WalletApiOperation.GetTransactions, {});
console.log(
"transactions after applying first refund:",
JSON.stringify(txs, undefined, 2),
@@ -112,7 +118,7 @@ export async function runRefundIncrementalTest(t: GlobalTestState) {
// refund will be grouped with the previous one.
await delayMs(1200);
- ref = await MerchantPrivateApi.giveRefund(merchant, {
+ ref = await merchantClient.giveRefund({
amount: "TESTKUDOS:5",
instance: "default",
justification: "bar",
@@ -125,7 +131,7 @@ export async function runRefundIncrementalTest(t: GlobalTestState) {
// refund will be grouped with the previous one.
await delayMs(1200);
- ref = await MerchantPrivateApi.giveRefund(merchant, {
+ ref = await merchantClient.giveRefund({
amount: "TESTKUDOS:10",
instance: "default",
justification: "bar",
@@ -135,13 +141,17 @@ export async function runRefundIncrementalTest(t: GlobalTestState) {
console.log("third refund increase response", ref);
{
- let wr = await wallet.client.call(WalletApiOperation.ApplyRefund, {
- talerRefundUri: ref.talerRefundUri,
+ let wr = await walletClient.call(WalletApiOperation.StartRefundQuery, {
+ transactionId: r1.transactionId,
});
+ await walletClient.call(
+ WalletApiOperation.TestingWaitTransactionsFinal,
+ {},
+ );
console.log(wr);
}
- orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderStatus = await merchantClient.queryPrivateOrderStatus({
orderId: orderResp.order_id,
});
@@ -151,26 +161,17 @@ export async function runRefundIncrementalTest(t: GlobalTestState) {
console.log(JSON.stringify(orderStatus, undefined, 2));
- await wallet.runUntilDone();
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
- const bal = await wallet.client.call(WalletApiOperation.GetBalances, {});
+ const bal = await walletClient.call(WalletApiOperation.GetBalances, {});
console.log(JSON.stringify(bal, undefined, 2));
{
- const txs = await wallet.client.call(
- WalletApiOperation.GetTransactions,
- {},
- );
+ const txs = await walletClient.call(WalletApiOperation.GetTransactions, {});
console.log(JSON.stringify(txs, undefined, 2));
const txTypes = txs.transactions.map((x) => x.type);
- t.assertDeepEqual(txTypes, [
- "withdrawal",
- "payment",
- "refund",
- "refund",
- "refund",
- ]);
+ t.assertDeepEqual(txTypes, ["withdrawal", "payment", "refund", "refund"]);
for (const tx of txs.transactions) {
if (tx.type !== TransactionType.Refund) {
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-refund.ts b/packages/taler-harness/src/integrationtests/test-refund.ts
index b63dad590..999a9b621 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-refund.ts
+++ b/packages/taler-harness/src/integrationtests/test-refund.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2020 Taler Systems S.A.
+ (C) 2020-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,41 +17,57 @@
/**
* Imports.
*/
-import { Duration, durationFromSpec } from "@gnu-taler/taler-util";
+import {
+ Duration,
+ j2s,
+ MerchantApiClient,
+ NotificationType,
+ TransactionMajorState,
+ TransactionType,
+} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js";
+import { GlobalTestState } from "../harness/harness.js";
import {
- createSimpleTestkudosEnvironment,
- withdrawViaBank,
+ createSimpleTestkudosEnvironmentV2,
+ withdrawViaBankV2,
} from "../harness/helpers.js";
-/**
- * Run test for basic, bank-integrated withdrawal.
- */
export async function runRefundTest(t: GlobalTestState) {
// Set up test environment
- const { wallet, bank, exchange, merchant } =
- await createSimpleTestkudosEnvironment(t);
+ const {
+ walletClient: wallet,
+ bank,
+ exchange,
+ merchant,
+ } = await createSimpleTestkudosEnvironmentV2(t);
+
+ const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl());
// Withdraw digital cash into the wallet.
- await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
+ const withdrawalRes = await withdrawViaBankV2(t, {
+ walletClient: wallet,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ });
+
+ await withdrawalRes.withdrawalFinishedCond;
// Set up order.
-
- const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
+ const orderResp = await merchantClient.createOrder({
order: {
summary: "Buy me!",
amount: "TESTKUDOS:5",
fulfillment_url: "taler://fulfillment-success/thx",
},
refund_delay: Duration.toTalerProtocolDuration(
- durationFromSpec({ minutes: 5 }),
+ Duration.fromSpec({ minutes: 5 }),
),
});
- let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ let orderStatus = await merchantClient.queryPrivateOrderStatus({
orderId: orderResp.order_id,
});
@@ -64,18 +80,28 @@ export async function runRefundTest(t: GlobalTestState) {
});
await wallet.client.call(WalletApiOperation.ConfirmPay, {
- proposalId: r1.proposalId,
+ transactionId: r1.transactionId,
});
// Check if payment was successful.
- orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderStatus = await merchantClient.queryPrivateOrderStatus({
orderId: orderResp.order_id,
});
t.assertTrue(orderStatus.order_status === "paid");
- const ref = await MerchantPrivateApi.giveRefund(merchant, {
+ {
+ const tx = await wallet.client.call(WalletApiOperation.GetTransactionById, {
+ transactionId: r1.transactionId,
+ });
+
+ t.assertTrue(
+ tx.type === TransactionType.Payment && tx.refundPending === undefined,
+ );
+ }
+
+ const ref = await merchantClient.giveRefund({
amount: "TESTKUDOS:5",
instance: "default",
justification: "foo",
@@ -84,23 +110,40 @@ export async function runRefundTest(t: GlobalTestState) {
console.log(ref);
- let r = await wallet.client.call(WalletApiOperation.ApplyRefund, {
- talerRefundUri: ref.talerRefundUri,
- });
- console.log(r);
-
- await wallet.runUntilDone();
+ {
+ const refundFinishedCond = wallet.waitForNotificationCond(
+ (x) =>
+ x.type === NotificationType.TransactionStateTransition &&
+ x.transactionId === r1.transactionId &&
+ x.newTxState.major === TransactionMajorState.Done,
+ );
+ await wallet.client.call(WalletApiOperation.StartRefundQuery, {
+ transactionId: r1.transactionId,
+ });
+ await refundFinishedCond;
+ }
{
const r2 = await wallet.client.call(WalletApiOperation.GetBalances, {});
console.log(JSON.stringify(r2, undefined, 2));
}
+
{
const r2 = await wallet.client.call(WalletApiOperation.GetTransactions, {});
console.log(JSON.stringify(r2, undefined, 2));
}
- await t.shutdown();
+ {
+ const tx = await wallet.client.call(WalletApiOperation.GetTransactionById, {
+ transactionId: r1.transactionId,
+ });
+
+ console.log(j2s(tx));
+
+ t.assertTrue(
+ tx.type === TransactionType.Payment && tx.refundPending === undefined,
+ );
+ }
}
runRefundTest.suites = ["wallet"];
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-revocation.ts b/packages/taler-harness/src/integrationtests/test-revocation.ts
index 0fbb4960e..ac118e4eb 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-revocation.ts
+++ b/packages/taler-harness/src/integrationtests/test-revocation.ts
@@ -20,28 +20,30 @@
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { CoinConfig } from "../harness/denomStructures.js";
import {
- GlobalTestState,
+ BankService,
ExchangeService,
+ GlobalTestState,
MerchantService,
WalletCli,
- setupDb,
- BankService,
+ WalletClient,
delayMs,
- getPayto,
+ generateRandomPayto,
+ setupDb,
} from "../harness/harness.js";
import {
- withdrawViaBank,
- makeTestPayment,
- SimpleTestEnvironment,
+ SimpleTestEnvironmentNg,
+ createWalletDaemonWithClient,
+ makeTestPaymentV2,
+ withdrawViaBankV2,
} from "../harness/helpers.js";
async function revokeAllWalletCoins(req: {
- wallet: WalletCli;
+ walletClient: WalletClient;
exchange: ExchangeService;
merchant: MerchantService;
}): Promise<void> {
- const { wallet, exchange, merchant } = req;
- const coinDump = await wallet.client.call(WalletApiOperation.DumpCoins, {});
+ const { walletClient, exchange, merchant } = req;
+ const coinDump = await walletClient.call(WalletApiOperation.DumpCoins, {});
console.log(coinDump);
const usedDenomHashes = new Set<string>();
for (const coin of coinDump.coins) {
@@ -60,7 +62,7 @@ async function revokeAllWalletCoins(req: {
async function createTestEnvironment(
t: GlobalTestState,
-): Promise<SimpleTestEnvironment> {
+): Promise<SimpleTestEnvironmentNg> {
const db = await setupDb(t);
const bank = await BankService.create(t, {
@@ -120,27 +122,35 @@ async function createTestEnvironment(
await merchant.start();
await merchant.pingUntilAvailable();
- await merchant.addInstance({
+ await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [getPayto("merchant-default")],
+ paytoUris: [generateRandomPayto("merchant-default")],
});
- await merchant.addInstance({
+ await merchant.addInstanceWithWireAccount({
id: "minst1",
name: "minst1",
- paytoUris: [getPayto("minst1")],
+ paytoUris: [generateRandomPayto("minst1")],
});
console.log("setup done!");
const wallet = new WalletCli(t);
+ const { walletService, walletClient } = await createWalletDaemonWithClient(
+ t,
+ {
+ name: "default",
+ },
+ );
+
return {
commonDb: db,
exchange,
merchant,
- wallet,
+ walletClient,
+ walletService,
bank,
exchangeBankAccount,
};
@@ -152,24 +162,29 @@ async function createTestEnvironment(
export async function runRevocationTest(t: GlobalTestState) {
// Set up test environment
- const { wallet, bank, exchange, merchant } = await createTestEnvironment(t);
+ const { walletClient, bank, exchange, merchant } =
+ await createTestEnvironment(t);
// Withdraw digital cash into the wallet.
- await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:15" });
+ const wres = await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:15",
+ });
+ await wres.withdrawalFinishedCond;
console.log("revoking first time");
- await revokeAllWalletCoins({ wallet, exchange, merchant });
+ await revokeAllWalletCoins({ walletClient, exchange, merchant });
// FIXME: this shouldn't be necessary once https://bugs.taler.net/n/6565
// is implemented.
- await wallet.client.call(WalletApiOperation.AddExchange, {
+ await walletClient.call(WalletApiOperation.UpdateExchangeEntry, {
exchangeBaseUrl: exchange.baseUrl,
- forceUpdate: true,
});
- await wallet.runUntilDone();
- await wallet.runUntilDone();
- const bal = await wallet.client.call(WalletApiOperation.GetBalances, {});
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+ const bal = await walletClient.call(WalletApiOperation.GetBalances, {});
console.log("wallet balance", bal);
const order = {
@@ -178,38 +193,42 @@ export async function runRevocationTest(t: GlobalTestState) {
fulfillment_url: "taler://fulfillment-success/thx",
};
- await makeTestPayment(t, { wallet, merchant, order });
+ await makeTestPaymentV2(t, { walletClient, merchant, order });
- wallet.deleteDatabase();
+ await walletClient.call(WalletApiOperation.ClearDb, {});
- await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:15" });
+ await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:15",
+ });
- const coinDump = await wallet.client.call(WalletApiOperation.DumpCoins, {});
+ const coinDump = await walletClient.call(WalletApiOperation.DumpCoins, {});
console.log(coinDump);
const coinPubList = coinDump.coins.map((x) => x.coin_pub);
- await wallet.client.call(WalletApiOperation.ForceRefresh, {
- coinPubList,
+ await walletClient.call(WalletApiOperation.ForceRefresh, {
+ refreshCoinSpecs: coinPubList.map((x) => ({ coinPub: x })),
});
- await wallet.runUntilDone();
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
console.log("revoking second time");
- await revokeAllWalletCoins({ wallet, exchange, merchant });
+ await revokeAllWalletCoins({ walletClient, exchange, merchant });
// FIXME: this shouldn't be necessary once https://bugs.taler.net/n/6565
// is implemented.
- await wallet.client.call(WalletApiOperation.AddExchange, {
+ await walletClient.call(WalletApiOperation.UpdateExchangeEntry, {
exchangeBaseUrl: exchange.baseUrl,
- forceUpdate: true,
});
- await wallet.runUntilDone();
- await wallet.runUntilDone();
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
{
- const bal = await wallet.client.call(WalletApiOperation.GetBalances, {});
+ const bal = await walletClient.call(WalletApiOperation.GetBalances, {});
console.log("wallet balance", bal);
}
- await makeTestPayment(t, { wallet, merchant, order });
+ await makeTestPaymentV2(t, { walletClient, merchant, order });
}
runRevocationTest.timeoutMs = 120000;
runRevocationTest.suites = ["wallet"];
+runRevocationTest.experimental = true;
diff --git a/packages/taler-harness/src/integrationtests/test-simple-payment.ts b/packages/taler-harness/src/integrationtests/test-simple-payment.ts
new file mode 100644
index 000000000..58ab61435
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-simple-payment.ts
@@ -0,0 +1,58 @@
+/*
+ This file is part of GNU Taler
+ (C) 2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState } from "../harness/harness.js";
+import {
+ withdrawViaBankV2,
+ makeTestPaymentV2,
+ useSharedTestkudosEnvironment,
+} from "../harness/helpers.js";
+
+/**
+ * Run test for basic, bank-integrated withdrawal and payment.
+ */
+export async function runSimplePaymentTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { walletClient, bank, exchange, merchant } =
+ await useSharedTestkudosEnvironment(t);
+
+ // Withdraw digital cash into the wallet.
+
+ await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ });
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+
+ const order = {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "taler://fulfillment-success/thx",
+ };
+
+ await makeTestPaymentV2(t, { walletClient, merchant, order });
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+}
+
+runSimplePaymentTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-stored-backups.ts b/packages/taler-harness/src/integrationtests/test-stored-backups.ts
new file mode 100644
index 000000000..a3a5e6ca3
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-stored-backups.ts
@@ -0,0 +1,112 @@
+/*
+ This file is part of GNU Taler
+ (C) 2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState } from "../harness/harness.js";
+import {
+ withdrawViaBankV2,
+ makeTestPaymentV2,
+ useSharedTestkudosEnvironment,
+} from "../harness/helpers.js";
+
+/**
+ * Test stored backup wallet-core API.
+ */
+export async function runStoredBackupsTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { walletClient, bank, exchange, merchant } =
+ await useSharedTestkudosEnvironment(t);
+
+ // Withdraw digital cash into the wallet.
+
+ const wres = await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ });
+
+ await wres;
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+
+ const sb1Resp = await walletClient.call(
+ WalletApiOperation.CreateStoredBackup,
+ {},
+ );
+ const sbList = await walletClient.call(
+ WalletApiOperation.ListStoredBackups,
+ {},
+ );
+ t.assertTrue(sbList.storedBackups.length === 1);
+ t.assertTrue(sbList.storedBackups[0].name === sb1Resp.name);
+
+ const order = {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "taler://fulfillment-success/thx",
+ };
+
+ await makeTestPaymentV2(t, { walletClient, merchant, order });
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+
+ const txn1 = await walletClient.call(WalletApiOperation.GetTransactions, {});
+ t.assertDeepEqual(txn1.transactions.length, 2);
+
+ // Recover from the stored backup now.
+
+ const sb2Resp = await walletClient.call(
+ WalletApiOperation.CreateStoredBackup,
+ {},
+ );
+
+ console.log("recovering backup");
+
+ await walletClient.call(WalletApiOperation.RecoverStoredBackup, {
+ name: sb1Resp.name,
+ });
+
+ console.log("first recovery done");
+
+ // Recovery went well, now we can delete the backup
+ // of the old database we stored before importing.
+ {
+ const sbl1 = await walletClient.call(
+ WalletApiOperation.ListStoredBackups,
+ {},
+ );
+ t.assertTrue(sbl1.storedBackups.length === 2);
+
+ await walletClient.call(WalletApiOperation.DeleteStoredBackup, {
+ name: sb1Resp.name,
+ });
+ const sbl2 = await walletClient.call(
+ WalletApiOperation.ListStoredBackups,
+ {},
+ );
+ t.assertTrue(sbl2.storedBackups.length === 1);
+ }
+
+ const txn2 = await walletClient.call(WalletApiOperation.GetTransactions, {});
+ // We only have the withdrawal after restoring
+ t.assertDeepEqual(txn2.transactions.length, 1);
+}
+
+runStoredBackupsTest.suites = ["wallet"];
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-timetravel-autorefresh.ts b/packages/taler-harness/src/integrationtests/test-timetravel-autorefresh.ts
index 54b66e0b2..e144683cb 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-timetravel-autorefresh.ts
+++ b/packages/taler-harness/src/integrationtests/test-timetravel-autorefresh.ts
@@ -20,53 +20,25 @@
import {
ConfirmPayResultType,
Duration,
- durationFromSpec,
+ MerchantApiClient,
+ NotificationType,
PreparePayResultType,
} from "@gnu-taler/taler-util";
-import {
- PendingOperationsResponse,
- WalletApiOperation,
-} from "@gnu-taler/taler-wallet-core";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { makeNoFeeCoinConfig } from "../harness/denomStructures.js";
import {
BankService,
ExchangeService,
GlobalTestState,
- MerchantPrivateApi,
MerchantService,
+ generateRandomPayto,
setupDb,
- WalletCli,
- getPayto
} from "../harness/harness.js";
-import { startWithdrawViaBank, withdrawViaBank } from "../harness/helpers.js";
-
-async function applyTimeTravel(
- timetravelDuration: Duration,
- s: {
- exchange?: ExchangeService;
- merchant?: MerchantService;
- wallet?: WalletCli;
- },
-): Promise<void> {
- if (s.exchange) {
- await s.exchange.stop();
- s.exchange.setTimetravel(timetravelDuration);
- await s.exchange.start();
- await s.exchange.pingUntilAvailable();
- }
-
- if (s.merchant) {
- await s.merchant.stop();
- s.merchant.setTimetravel(timetravelDuration);
- await s.merchant.start();
- await s.merchant.pingUntilAvailable();
- }
-
- if (s.wallet) {
- console.log("setting wallet time travel to", timetravelDuration);
- s.wallet.setTimetravel(timetravelDuration);
- }
-}
+import {
+ applyTimeTravelV2,
+ createWalletDaemonWithClient,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
/**
* Basic time travel test.
@@ -119,68 +91,95 @@ export async function runTimetravelAutorefreshTest(t: GlobalTestState) {
await merchant.start();
await merchant.pingUntilAvailable();
- await merchant.addInstance({
+ await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [getPayto("merchant-default")],
+ paytoUris: [generateRandomPayto("merchant-default")],
});
- await merchant.addInstance({
+ await merchant.addInstanceWithWireAccount({
id: "minst1",
name: "minst1",
- paytoUris: [getPayto("minst1")],
+ paytoUris: [generateRandomPayto("minst1")],
});
console.log("setup done!");
- const wallet = new WalletCli(t);
+ const { walletClient } = await createWalletDaemonWithClient(t, {
+ name: "w1",
+ });
+
+ const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl());
// Withdraw digital cash into the wallet.
- await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:15" });
-
- // Travel into the future, the deposit expiration is two years
- // into the future.
- console.log("applying first time travel");
- await applyTimeTravel(durationFromSpec({ days: 400 }), {
- wallet,
+ const wres = await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
exchange,
- merchant,
+ amount: "TESTKUDOS:15",
});
+ await wres.withdrawalFinishedCond;
- await wallet.runUntilDone();
+ const exchangeUpdated1Cond = walletClient.waitForNotificationCond(
+ (x) =>
+ x.type === NotificationType.ExchangeStateTransition &&
+ x.exchangeBaseUrl === exchange.baseUrl,
+ );
- let p: PendingOperationsResponse;
- p = await wallet.client.call(WalletApiOperation.GetPendingOperations, {});
+ // Travel into the future, the deposit expiration is two years
+ // into the future.
+ console.log("applying first time travel");
+ await applyTimeTravelV2(
+ Duration.toMilliseconds(Duration.fromSpec({ days: 400 })),
+ {
+ walletClient,
+ exchange,
+ merchant,
+ },
+ );
- console.log("pending operations after first time travel");
- console.log(JSON.stringify(p, undefined, 2));
+ // The time travel should cause exchanges to update.
+ await exchangeUpdated1Cond;
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
- await startWithdrawViaBank(t, {
- wallet,
+ const wres2 = await withdrawViaBankV2(t, {
+ walletClient,
bank,
exchange,
amount: "TESTKUDOS:20",
});
+ await wres2.withdrawalFinishedCond;
- await wallet.runUntilDone();
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+
+ const exchangeUpdated2Cond = walletClient.waitForNotificationCond(
+ (x) =>
+ x.type === NotificationType.ExchangeStateTransition &&
+ x.exchangeBaseUrl === exchange.baseUrl,
+ );
// Travel into the future, the deposit expiration is two years
// into the future.
console.log("applying second time travel");
- await applyTimeTravel(durationFromSpec({ years: 2, months: 6 }), {
- wallet,
- exchange,
- merchant,
- });
+ await applyTimeTravelV2(
+ Duration.toMilliseconds(Duration.fromSpec({ years: 2, months: 6 })),
+ {
+ walletClient,
+ exchange,
+ merchant,
+ },
+ );
+
+ // The time travel should cause exchanges to update.
+ await exchangeUpdated2Cond;
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
// At this point, the original coins should've been refreshed.
// It would be too late to refresh them now, as we're past
// the two year deposit expiration.
- await wallet.runUntilDone();
-
- const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
+ const orderResp = await merchantClient.createOrder({
order: {
fulfillment_url: "http://example.com",
summary: "foo",
@@ -188,17 +187,14 @@ export async function runTimetravelAutorefreshTest(t: GlobalTestState) {
},
});
- const orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(
- merchant,
- {
- orderId: orderResp.order_id,
- instance: "default",
- },
- );
+ const orderStatus = await merchantClient.queryPrivateOrderStatus({
+ orderId: orderResp.order_id,
+ instance: "default",
+ });
t.assertTrue(orderStatus.order_status === "unpaid");
- const r = await wallet.client.call(WalletApiOperation.PreparePayForUri, {
+ const r = await walletClient.call(WalletApiOperation.PreparePayForUri, {
talerPayUri: orderStatus.taler_pay_uri,
});
@@ -206,8 +202,8 @@ export async function runTimetravelAutorefreshTest(t: GlobalTestState) {
t.assertTrue(r.status === PreparePayResultType.PaymentPossible);
- const cpr = await wallet.client.call(WalletApiOperation.ConfirmPay, {
- proposalId: r.proposalId,
+ const cpr = await walletClient.call(WalletApiOperation.ConfirmPay, {
+ transactionId: r.transactionId,
});
t.assertTrue(cpr.type === ConfirmPayResultType.Done);
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-timetravel-withdraw.ts b/packages/taler-harness/src/integrationtests/test-timetravel-withdraw.ts
index 9335af9f5..9cd0beb42 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-timetravel-withdraw.ts
+++ b/packages/taler-harness/src/integrationtests/test-timetravel-withdraw.ts
@@ -17,13 +17,17 @@
/**
* Imports.
*/
-import { Duration, TransactionType } from "@gnu-taler/taler-util";
+import {
+ Duration,
+ TransactionMajorState,
+ TransactionType,
+ j2s,
+} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { GlobalTestState } from "../harness/harness.js";
import {
- createSimpleTestkudosEnvironment,
- startWithdrawViaBank,
- withdrawViaBank,
+ createSimpleTestkudosEnvironmentV2,
+ withdrawViaBankV2,
} from "../harness/helpers.js";
/**
@@ -32,12 +36,18 @@ import {
export async function runTimetravelWithdrawTest(t: GlobalTestState) {
// Set up test environment
- const { wallet, bank, exchange, merchant } =
- await createSimpleTestkudosEnvironment(t);
+ const { walletClient, bank, exchange, merchant } =
+ await createSimpleTestkudosEnvironmentV2(t);
// Withdraw digital cash into the wallet.
- await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:15" });
+ const wres1 = await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:15",
+ });
+ await wres1.withdrawalFinishedCond;
// Travel 400 days into the future,
// as the deposit expiration is two years
@@ -47,21 +57,21 @@ export async function runTimetravelWithdrawTest(t: GlobalTestState) {
};
await exchange.stop();
- exchange.setTimetravel(timetravelDuration);
+ exchange.setTimetravel(Duration.toMilliseconds(timetravelDuration));
await exchange.start();
await exchange.pingUntilAvailable();
await exchange.keyup();
await merchant.stop();
- merchant.setTimetravel(timetravelDuration);
+ merchant.setTimetravel(Duration.toMilliseconds(timetravelDuration));
await merchant.start();
await merchant.pingUntilAvailable();
console.log("starting withdrawal via bank");
// This should fail, as the wallet didn't time travel yet.
- await startWithdrawViaBank(t, {
- wallet,
+ const wres2 = await withdrawViaBankV2(t, {
+ walletClient,
bank,
exchange,
amount: "TESTKUDOS:20",
@@ -71,28 +81,29 @@ export async function runTimetravelWithdrawTest(t: GlobalTestState) {
// Check that transactions are correct for the failed withdrawal
{
- console.log("running until done (should run into maxRetries limit)");
- await wallet.runUntilDone({ maxRetries: 5 });
- console.log("wallet done running");
- const transactions = await wallet.client.call(
+ const transactions = await walletClient.call(
WalletApiOperation.GetTransactions,
- {},
+ {
+ sort: "stable-ascending",
+ },
);
- console.log(transactions);
+ console.log(j2s(transactions));
const types = transactions.transactions.map((x) => x.type);
t.assertDeepEqual(types, ["withdrawal", "withdrawal"]);
const wtrans = transactions.transactions[1];
t.assertTrue(wtrans.type === TransactionType.Withdrawal);
- t.assertTrue(wtrans.pending);
+ t.assertTrue(wtrans.txState.major === TransactionMajorState.Pending);
}
// Now we also let the wallet time travel
- wallet.setTimetravel(timetravelDuration);
+ walletClient.call(WalletApiOperation.TestingSetTimetravel, {
+ offsetMs: Duration.toMilliseconds(timetravelDuration),
+ });
- // This doesn't work yet, see https://bugs.taler.net/n/6585
+ // The wallet should do denomination re-selection and succeed
- // await wallet.runUntilDone({ maxRetries: 5 });
+ await wres2.withdrawalFinishedCond;
}
runTimetravelWithdrawTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-tos-format.ts b/packages/taler-harness/src/integrationtests/test-tos-format.ts
new file mode 100644
index 000000000..e6087af9d
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-tos-format.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 { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState } from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironmentV2,
+} from "../harness/helpers.js";
+import * as fs from "fs";
+
+/**
+ * Run test for basic, bank-integrated withdrawal and payment.
+ */
+export async function runTermOfServiceFormatTest(t: GlobalTestState) {
+ // Set up test environment
+ const tosDir = t.testDir + `/tos/`;
+ const langs = ["es", "en", "de"]
+
+ langs.forEach(l => {
+ const langDir = tosDir + l + "/"
+ fs.mkdirSync(langDir, { recursive: true });
+ fs.writeFileSync(langDir + "v1.txt", "text content");
+ fs.writeFileSync(langDir + "v1.md", "markdown content");
+ fs.writeFileSync(langDir + "v1.html", "html content");
+ });
+
+ const { walletClient, exchange, } =
+ await createSimpleTestkudosEnvironmentV2(t, undefined, {
+ additionalExchangeConfig: (ex) => {
+ ex.changeConfig((cfg) => {
+ cfg.setString("exchange", "terms_etag", "v1")
+ cfg.setString("exchange", "terms_dir", tosDir)
+ })
+ }
+ });
+
+
+ {
+ const tos = await walletClient.client.call(WalletApiOperation.GetExchangeTos, {
+ exchangeBaseUrl: exchange.baseUrl,
+ })
+
+ t.assertDeepEqual(tos.content, "text content");
+ }
+
+ {
+ const tos = await walletClient.client.call(WalletApiOperation.GetExchangeTos, {
+ exchangeBaseUrl: exchange.baseUrl,
+ acceptedFormat: ["text/html"]
+ })
+
+ t.assertDeepEqual(tos.content, "html content");
+ }
+
+ {
+ const tos = await walletClient.client.call(WalletApiOperation.GetExchangeTos, {
+ exchangeBaseUrl: exchange.baseUrl,
+ acceptedFormat: ["text/markdown"]
+ })
+
+ t.assertDeepEqual(tos.content, "markdown content");
+ }
+
+ {
+ const tos = await walletClient.client.call(WalletApiOperation.GetExchangeTos, {
+ exchangeBaseUrl: exchange.baseUrl,
+ acceptedFormat: ["text/markdown", "text/html"]
+ })
+
+ // prefer markdown since its the first one in the list
+ t.assertDeepEqual(tos.content, "markdown content");
+ }
+
+ {
+ const tos = await walletClient.client.call(WalletApiOperation.GetExchangeTos, {
+ exchangeBaseUrl: exchange.baseUrl,
+ acceptedFormat: ["text/pdf", "text/html"]
+ })
+
+ // there is no pdf so fallback in html
+ t.assertDeepEqual(tos.content, "html content");
+ }
+}
+
+runTermOfServiceFormatTest.suites = ["wallet"];
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-basic.ts b/packages/taler-harness/src/integrationtests/test-wallet-backup-basic.ts
index fc2f3335d..cb4a50a2b 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-basic.ts
+++ b/packages/taler-harness/src/integrationtests/test-wallet-backup-basic.ts
@@ -19,10 +19,11 @@
*/
import { j2s } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-import { GlobalTestState, WalletCli } from "../harness/harness.js";
+import { GlobalTestState } from "../harness/harness.js";
import {
- createSimpleTestkudosEnvironment,
- withdrawViaBank,
+ createSimpleTestkudosEnvironmentV2,
+ createWalletDaemonWithClient,
+ withdrawViaBankV2,
} from "../harness/helpers.js";
import { SyncService } from "../harness/sync.js";
@@ -32,8 +33,8 @@ import { SyncService } from "../harness/sync.js";
export async function runWalletBackupBasicTest(t: GlobalTestState) {
// Set up test environment
- const { commonDb, merchant, wallet, bank, exchange } =
- await createSimpleTestkudosEnvironment(t);
+ const { commonDb, merchant, walletClient, bank, exchange } =
+ await createSimpleTestkudosEnvironmentV2(t);
const sync = await SyncService.create(t, {
currency: "TESTKUDOS",
@@ -49,32 +50,32 @@ export async function runWalletBackupBasicTest(t: GlobalTestState) {
await sync.start();
await sync.pingUntilAvailable();
- await wallet.client.call(WalletApiOperation.AddBackupProvider, {
+ await walletClient.call(WalletApiOperation.AddBackupProvider, {
backupProviderBaseUrl: sync.baseUrl,
activate: false,
name: sync.baseUrl,
});
{
- const bi = await wallet.client.call(WalletApiOperation.GetBackupInfo, {});
+ const bi = await walletClient.call(WalletApiOperation.GetBackupInfo, {});
t.assertDeepEqual(bi.providers[0].active, false);
}
- await wallet.client.call(WalletApiOperation.AddBackupProvider, {
+ await walletClient.call(WalletApiOperation.AddBackupProvider, {
backupProviderBaseUrl: sync.baseUrl,
activate: true,
name: sync.baseUrl,
});
{
- const bi = await wallet.client.call(WalletApiOperation.GetBackupInfo, {});
+ const bi = await walletClient.call(WalletApiOperation.GetBackupInfo, {});
t.assertDeepEqual(bi.providers[0].active, true);
}
- await wallet.client.call(WalletApiOperation.RunBackupCycle, {});
+ await walletClient.call(WalletApiOperation.RunBackupCycle, {});
{
- const bi = await wallet.client.call(WalletApiOperation.GetBackupInfo, {});
+ const bi = await walletClient.call(WalletApiOperation.GetBackupInfo, {});
console.log(bi);
t.assertDeepEqual(
bi.providers[0].paymentStatus.type,
@@ -82,68 +83,83 @@ export async function runWalletBackupBasicTest(t: GlobalTestState) {
);
}
- await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:10" });
+ await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:10",
+ });
- await wallet.runUntilDone();
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
- await wallet.client.call(WalletApiOperation.RunBackupCycle, {});
+ await walletClient.call(WalletApiOperation.RunBackupCycle, {});
{
- const bi = await wallet.client.call(WalletApiOperation.GetBackupInfo, {});
+ const bi = await walletClient.call(WalletApiOperation.GetBackupInfo, {});
console.log(bi);
}
- await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:5" });
+ await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:5",
+ });
- await wallet.client.call(WalletApiOperation.RunBackupCycle, {});
+ await walletClient.call(WalletApiOperation.RunBackupCycle, {});
{
- const bi = await wallet.client.call(WalletApiOperation.GetBackupInfo, {});
+ const bi = await walletClient.call(WalletApiOperation.GetBackupInfo, {});
console.log(bi);
}
- const backupRecovery = await wallet.client.call(
+ const backupRecovery = await walletClient.call(
WalletApiOperation.ExportBackupRecovery,
{},
);
- const txs = await wallet.client.call(WalletApiOperation.GetTransactions, {});
+ const txs = await walletClient.call(WalletApiOperation.GetTransactions, {});
console.log(`backed up transactions ${j2s(txs)}`);
- const wallet2 = new WalletCli(t, "wallet2");
+ const { walletClient: walletClient2 } = await createWalletDaemonWithClient(
+ t,
+ {
+ name: "w2",
+ },
+ );
// Check that the second wallet is a fresh wallet.
{
- const bal = await wallet2.client.call(WalletApiOperation.GetBalances, {});
+ const bal = await walletClient2.call(WalletApiOperation.GetBalances, {});
t.assertTrue(bal.balances.length === 0);
}
- await wallet2.client.call(WalletApiOperation.ImportBackupRecovery, {
+ await walletClient2.call(WalletApiOperation.ImportBackupRecovery, {
recovery: backupRecovery,
});
- await wallet2.client.call(WalletApiOperation.RunBackupCycle, {});
+ await walletClient2.call(WalletApiOperation.RunBackupCycle, {});
// Check that now the old balance is available!
{
- const bal = await wallet2.client.call(WalletApiOperation.GetBalances, {});
+ const bal = await walletClient2.call(WalletApiOperation.GetBalances, {});
t.assertTrue(bal.balances.length === 1);
console.log(bal);
}
// Now do some basic checks that the restored wallet is still functional
{
- const txs = await wallet2.client.call(
+ const txs = await walletClient2.call(
WalletApiOperation.GetTransactions,
{},
);
console.log(`restored transactions ${j2s(txs)}`);
- const bal1 = await wallet2.client.call(WalletApiOperation.GetBalances, {});
+ const bal1 = await walletClient2.call(WalletApiOperation.GetBalances, {});
t.assertAmountEquals(bal1.balances[0].available, "TESTKUDOS:14.1");
- await withdrawViaBank(t, {
- wallet: wallet2,
+ await withdrawViaBankV2(t, {
+ walletClient: walletClient2,
bank,
exchange,
amount: "TESTKUDOS:10",
@@ -151,18 +167,23 @@ export async function runWalletBackupBasicTest(t: GlobalTestState) {
await exchange.runWirewatchOnce();
- await wallet2.runUntilDone();
+ await walletClient2.call(
+ WalletApiOperation.TestingWaitTransactionsFinal,
+ {},
+ );
- const txs2 = await wallet2.client.call(
+ const txs2 = await walletClient2.call(
WalletApiOperation.GetTransactions,
{},
);
console.log(`tx after withdraw after restore ${j2s(txs2)}`);
- const bal2 = await wallet2.client.call(WalletApiOperation.GetBalances, {});
+ const bal2 = await walletClient2.call(WalletApiOperation.GetBalances, {});
t.assertAmountEquals(bal2.balances[0].available, "TESTKUDOS:23.82");
}
}
runWalletBackupBasicTest.suites = ["wallet", "wallet-backup"];
+// See https://bugs.taler.net/n/7598
+runWalletBackupBasicTest.experimental = true;
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-doublespend.ts b/packages/taler-harness/src/integrationtests/test-wallet-backup-doublespend.ts
index 8b52260e9..c761c4fb0 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-doublespend.ts
+++ b/packages/taler-harness/src/integrationtests/test-wallet-backup-doublespend.ts
@@ -17,25 +17,24 @@
/**
* Imports.
*/
-import { PreparePayResultType } from "@gnu-taler/taler-util";
+import { MerchantApiClient, PreparePayResultType } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState } from "../harness/harness.js";
import {
- GlobalTestState,
- WalletCli,
- MerchantPrivateApi,
-} from "../harness/harness.js";
-import {
- createSimpleTestkudosEnvironment,
- makeTestPayment,
- withdrawViaBank,
+ createSimpleTestkudosEnvironmentV2,
+ createWalletDaemonWithClient,
+ makeTestPaymentV2,
+ withdrawViaBankV2,
} from "../harness/helpers.js";
import { SyncService } from "../harness/sync.js";
export async function runWalletBackupDoublespendTest(t: GlobalTestState) {
// Set up test environment
- const { commonDb, merchant, wallet, bank, exchange } =
- await createSimpleTestkudosEnvironment(t);
+ const { commonDb, merchant, walletClient, bank, exchange } =
+ await createSimpleTestkudosEnvironmentV2(t);
+
+ const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl());
const sync = await SyncService.create(t, {
currency: "TESTKUDOS",
@@ -51,58 +50,66 @@ export async function runWalletBackupDoublespendTest(t: GlobalTestState) {
await sync.start();
await sync.pingUntilAvailable();
- await wallet.client.call(WalletApiOperation.AddBackupProvider, {
+ await walletClient.call(WalletApiOperation.AddBackupProvider, {
backupProviderBaseUrl: sync.baseUrl,
activate: true,
name: sync.baseUrl,
});
- await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:10" });
+ await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:10",
+ });
- await wallet.runUntilDone();
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
- await wallet.client.call(WalletApiOperation.RunBackupCycle, {});
- await wallet.runUntilDone();
- await wallet.client.call(WalletApiOperation.RunBackupCycle, {});
+ await walletClient.call(WalletApiOperation.RunBackupCycle, {});
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+ await walletClient.call(WalletApiOperation.RunBackupCycle, {});
- const backupRecovery = await wallet.client.call(
+ const backupRecovery = await walletClient.call(
WalletApiOperation.ExportBackupRecovery,
{},
);
- const wallet2 = new WalletCli(t, "wallet2");
+ const { walletClient: walletClientTwo } = await createWalletDaemonWithClient(
+ t,
+ { name: "default" },
+ );
- await wallet2.client.call(WalletApiOperation.ImportBackupRecovery, {
+ await walletClientTwo.call(WalletApiOperation.ImportBackupRecovery, {
recovery: backupRecovery,
});
- await wallet2.client.call(WalletApiOperation.RunBackupCycle, {});
+ await walletClientTwo.call(WalletApiOperation.RunBackupCycle, {});
console.log(
"wallet1 balance before spend:",
- await wallet.client.call(WalletApiOperation.GetBalances, {}),
+ await walletClient.call(WalletApiOperation.GetBalances, {}),
);
- await makeTestPayment(t, {
+ await makeTestPaymentV2(t, {
merchant,
- wallet,
+ walletClient,
order: {
summary: "foo",
amount: "TESTKUDOS:7",
},
});
- await wallet.runUntilDone();
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
console.log(
"wallet1 balance after spend:",
- await wallet.client.call(WalletApiOperation.GetBalances, {}),
+ await walletClient.call(WalletApiOperation.GetBalances, {}),
);
{
console.log(
"wallet2 balance:",
- await wallet2.client.call(WalletApiOperation.GetBalances, {}),
+ await walletClientTwo.call(WalletApiOperation.GetBalances, {}),
);
}
@@ -111,7 +118,7 @@ export async function runWalletBackupDoublespendTest(t: GlobalTestState) {
{
const instance = "default";
- const orderResp = await MerchantPrivateApi.createOrder(merchant, instance, {
+ const orderResp = await merchantClient.createOrder({
order: {
amount: "TESTKUDOS:8",
summary: "bla",
@@ -119,12 +126,9 @@ export async function runWalletBackupDoublespendTest(t: GlobalTestState) {
},
});
- let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(
- merchant,
- {
- orderId: orderResp.order_id,
- },
- );
+ let orderStatus = await merchantClient.queryPrivateOrderStatus({
+ orderId: orderResp.order_id,
+ });
t.assertTrue(orderStatus.order_status === "unpaid");
@@ -133,11 +137,11 @@ export async function runWalletBackupDoublespendTest(t: GlobalTestState) {
{
console.log(
"wallet2 balance before preparePay:",
- await wallet2.client.call(WalletApiOperation.GetBalances, {}),
+ await walletClientTwo.call(WalletApiOperation.GetBalances, {}),
);
}
- const preparePayResult = await wallet2.client.call(
+ const preparePayResult = await walletClientTwo.call(
WalletApiOperation.PreparePayForUri,
{
talerPayUri: orderStatus.taler_pay_uri,
@@ -149,26 +153,31 @@ export async function runWalletBackupDoublespendTest(t: GlobalTestState) {
PreparePayResultType.PaymentPossible,
);
- const res = await wallet2.client.call(WalletApiOperation.ConfirmPay, {
- proposalId: preparePayResult.proposalId,
+ const res = await walletClientTwo.call(WalletApiOperation.ConfirmPay, {
+ transactionId: preparePayResult.transactionId,
});
console.log(res);
// FIXME: wait for a notification that indicates insufficient funds!
- await withdrawViaBank(t, {
- wallet: wallet2,
+ await withdrawViaBankV2(t, {
+ walletClient: walletClientTwo,
bank,
exchange,
amount: "TESTKUDOS:50",
});
- const bal = await wallet2.client.call(WalletApiOperation.GetBalances, {});
+ const bal = await walletClientTwo.call(WalletApiOperation.GetBalances, {});
console.log("bal", bal);
- await wallet2.runUntilDone();
+ await walletClientTwo.call(
+ WalletApiOperation.TestingWaitTransactionsFinal,
+ {},
+ );
}
}
runWalletBackupDoublespendTest.suites = ["wallet", "wallet-backup"];
+// See https://bugs.taler.net/n/7598
+runWalletBackupDoublespendTest.experimental = true;
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-balance-notifications.ts b/packages/taler-harness/src/integrationtests/test-wallet-balance-notifications.ts
new file mode 100644
index 000000000..290ef7e2d
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-wallet-balance-notifications.ts
@@ -0,0 +1,111 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ NotificationType,
+ TalerCorebankApiClient,
+ TransactionMajorState,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState } from "../harness/harness.js";
+import { createSimpleTestkudosEnvironmentV2 } from "../harness/helpers.js";
+
+/**
+ * Test behavior when an order is deleted while the wallet is paying for it.
+ */
+export async function runWalletBalanceNotificationsTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { walletClient, bank, exchange, merchant, walletService } =
+ await createSimpleTestkudosEnvironmentV2(t);
+
+ const amount = "TESTKUDOS:20";
+
+ const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl);
+
+ const user = await bankClient.createRandomBankUser();
+ const wop = await bankClient.createWithdrawalOperation(user.username, amount);
+
+ // Hand it to the wallet
+
+ await walletClient.call(WalletApiOperation.GetWithdrawalDetailsForUri, {
+ talerWithdrawUri: wop.taler_withdraw_uri,
+ });
+
+ // Withdraw (AKA select)
+
+ const balanceChangedNotif1 = walletClient.waitForNotificationCond(
+ (x) => x.type === NotificationType.BalanceChange,
+ );
+
+ const acceptRes = await walletClient.call(
+ WalletApiOperation.AcceptBankIntegratedWithdrawal,
+ {
+ exchangeBaseUrl: exchange.baseUrl,
+ talerWithdrawUri: wop.taler_withdraw_uri,
+ },
+ );
+
+ t.logStep("wait-balance-notif-1");
+ await balanceChangedNotif1;
+ t.logStep("done-wait-balance-notif-1");
+
+ const withdrawalFinishedCond = walletClient.waitForNotificationCond(
+ (x) =>
+ x.type === NotificationType.TransactionStateTransition &&
+ x.newTxState.major === TransactionMajorState.Done &&
+ x.transactionId === acceptRes.transactionId,
+ );
+
+ // Confirm it
+
+ await bankClient.confirmWithdrawalOperation(user.username, {
+ withdrawalOperationId: wop.withdrawal_id,
+ });
+
+ await withdrawalFinishedCond;
+
+ // Second withdrawal!
+ {
+ const wop2 = await bankClient.createWithdrawalOperation(
+ user.username,
+ "TESTKUDOS:5",
+ );
+
+ await walletClient.call(WalletApiOperation.GetWithdrawalDetailsForUri, {
+ talerWithdrawUri: wop2.taler_withdraw_uri,
+ });
+
+ const acceptRes = await walletClient.call(
+ WalletApiOperation.AcceptBankIntegratedWithdrawal,
+ {
+ exchangeBaseUrl: exchange.baseUrl,
+ talerWithdrawUri: wop2.taler_withdraw_uri,
+ },
+ );
+
+ const bal = await walletClient.call(WalletApiOperation.GetBalances, {});
+ t.assertAmountEquals(bal.balances[0].available, "TESTKUDOS:19.53");
+ t.assertAmountEquals(bal.balances[0].pendingIncoming, "TESTKUDOS:4.85");
+
+ await walletService.stop();
+ }
+}
+
+runWalletBalanceNotificationsTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-balance-zero.ts b/packages/taler-harness/src/integrationtests/test-wallet-balance-zero.ts
new file mode 100644
index 000000000..7d65b60cf
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-wallet-balance-zero.ts
@@ -0,0 +1,64 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import { j2s } from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { makeNoFeeCoinConfig } from "../harness/denomStructures.js";
+import { GlobalTestState } from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironmentV2,
+ makeTestPaymentV2,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
+
+/**
+ * Related bugs:
+ * https://bugs.taler.net/n/8323
+ */
+export async function runWalletBalanceZeroTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const coinConfig = makeNoFeeCoinConfig("TESTKUDOS");
+ console.log(`coin config ${j2s(coinConfig)}`);
+ const { merchant, walletClient, exchange, bank } =
+ await createSimpleTestkudosEnvironmentV2(t, coinConfig);
+
+ const wres = await withdrawViaBankV2(t, {
+ amount: "TESTKUDOS:10",
+ bank,
+ exchange,
+ walletClient,
+ });
+ await wres.withdrawalFinishedCond;
+
+ await makeTestPaymentV2(t, {
+ walletClient,
+ merchant,
+ order: {
+ summary: "Hello, World!",
+ amount: "TESTKUDOS:10",
+ },
+ });
+
+ const bal = await walletClient.call(WalletApiOperation.GetBalances, {});
+ console.log(`${j2s(bal)}`);
+ t.assertAmountEquals(bal.balances[0].available, "TESTKUDOS:0");
+}
+
+runWalletBalanceZeroTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-balance.ts b/packages/taler-harness/src/integrationtests/test-wallet-balance.ts
new file mode 100644
index 000000000..eb7359781
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-wallet-balance.ts
@@ -0,0 +1,129 @@
+/*
+ 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 {
+ Amounts,
+ Duration,
+ MerchantApiClient,
+ MerchantContractTerms,
+ PreparePayResultType,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState } from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironmentV2,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
+
+/**
+ * Test wallet:
+ * - balance error messages
+ * - different types of insufficient balance.
+ *
+ * Related bugs:
+ * https://bugs.taler.net/n/7299
+ */
+export async function runWalletBalanceTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { merchant, walletClient, exchange, bank } =
+ await createSimpleTestkudosEnvironmentV2(t);
+
+ await merchant.addInstanceWithWireAccount({
+ id: "myinst",
+ name: "My Instance",
+ paytoUris: ["payto://void/foo"],
+ defaultWireTransferDelay: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ minutes: 1 }),
+ ),
+ });
+
+ const merchantClient = new MerchantApiClient(
+ merchant.makeInstanceBaseUrl("myinst"),
+ );
+
+ // Withdraw digital cash into the wallet.
+
+ const wres = await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ });
+
+ await wres.withdrawalFinishedCond;
+
+ console.log("withdrawal finished");
+
+ const order: Partial<MerchantContractTerms> = {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "taler://fulfillment-success/thx",
+ };
+
+ console.log("creating order");
+
+ const orderResp = await merchantClient.createOrder({
+ order,
+ });
+
+ console.log("created order with merchant");
+
+ let orderStatus = await merchantClient.queryPrivateOrderStatus({
+ orderId: orderResp.order_id,
+ });
+
+ t.assertTrue(orderStatus.order_status === "unpaid");
+
+ console.log("queried order at merchant");
+
+ // Make wallet pay for the order
+
+ const preparePayResult = await walletClient.call(
+ WalletApiOperation.PreparePayForUri,
+ {
+ talerPayUri: orderStatus.taler_pay_uri,
+ },
+ );
+
+ t.assertTrue(
+ preparePayResult.status === PreparePayResultType.InsufficientBalance,
+ );
+
+ t.assertDeepEqual(
+ preparePayResult.status,
+ PreparePayResultType.InsufficientBalance,
+ );
+
+ t.assertTrue(
+ Amounts.isNonZero(
+ preparePayResult.balanceDetails.balanceReceiverAcceptable,
+ ),
+ );
+
+ t.assertTrue(
+ Amounts.isZero(preparePayResult.balanceDetails.balanceReceiverDepositable),
+ );
+
+ console.log("waiting for transactions to finalize");
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+}
+
+runWalletBalanceTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-blocked-deposit.ts b/packages/taler-harness/src/integrationtests/test-wallet-blocked-deposit.ts
new file mode 100644
index 000000000..69b721789
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-wallet-blocked-deposit.ts
@@ -0,0 +1,150 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ AmountString,
+ NotificationType,
+ TransactionMajorState,
+ TransactionMinorState,
+ j2s,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { CoinConfig } from "../harness/denomStructures.js";
+import { GlobalTestState, generateRandomPayto } from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironmentV2,
+ createWalletDaemonWithClient,
+ makeTestPaymentV2,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
+
+const coinCommon = {
+ cipher: "RSA" as const,
+ durationLegal: "3 years",
+ durationSpend: "2 years",
+ durationWithdraw: "7 days",
+ feeDeposit: "TESTKUDOS:0",
+ feeRefresh: "TESTKUDOS:0",
+ feeRefund: "TESTKUDOS:0",
+ feeWithdraw: "TESTKUDOS:0",
+ rsaKeySize: 1024,
+};
+
+/**
+ * Run test for refreshe after a payment.
+ */
+export async function runWalletBlockedDepositTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const coinConfigList: CoinConfig[] = [
+ {
+ ...coinCommon,
+ name: "n1",
+ value: "TESTKUDOS:1",
+ },
+ {
+ ...coinCommon,
+ name: "n5",
+ value: "TESTKUDOS:5",
+ },
+ ];
+
+ const { walletClient, bank, exchange, merchant } =
+ await createSimpleTestkudosEnvironmentV2(t, coinConfigList);
+
+ // Withdraw digital cash into the wallet.
+
+ const { walletClient: w1 } = await createWalletDaemonWithClient(t, {
+ name: "w1",
+ persistent: true,
+ config: {
+ testing: {
+ devModeActive: true,
+ },
+ },
+ });
+
+ await withdrawViaBankV2(t, {
+ walletClient: w1,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ });
+
+ await w1.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+
+ // Prevent the wallet from doing refreshes by injecting a 5xx
+ // status for all refresh requests.
+ await w1.call(WalletApiOperation.ApplyDevExperiment, {
+ devExperimentUri: "taler://dev-experiment/start-block-refresh",
+ });
+
+ await makeTestPaymentV2(t, {
+ merchant,
+ walletClient: w1,
+ order: {
+ summary: "test",
+ amount: "TESTKUDOS:2",
+ },
+ });
+
+ const userPayto = generateRandomPayto("foo");
+
+ const bal = await w1.call(WalletApiOperation.GetBalances, {});
+ console.log(`balance: ${j2s(bal)}`);
+
+ const balDet = await w1.call(WalletApiOperation.GetBalanceDetail, {
+ currency: "TESTKUDOS",
+ });
+ console.log(`balance details: ${j2s(balDet)}`);
+
+ const depositCheckResp = await w1.call(WalletApiOperation.PrepareDeposit, {
+ amount: "TESTKUDOS:18" as AmountString,
+ depositPaytoUri: userPayto,
+ });
+
+ console.log(`check resp: ${j2s(depositCheckResp)}`);
+
+ const depositCreateResp = await w1.call(
+ WalletApiOperation.CreateDepositGroup,
+ {
+ amount: "TESTKUDOS:18" as AmountString,
+ depositPaytoUri: userPayto,
+ },
+ );
+
+ console.log(`create resp: ${j2s(depositCreateResp)}`);
+
+ const depositTrackCond = w1.waitForNotificationCond((n) => {
+ return (
+ n.type === NotificationType.TransactionStateTransition &&
+ n.transactionId === depositCreateResp.transactionId &&
+ n.newTxState.major === TransactionMajorState.Pending &&
+ n.newTxState.minor === TransactionMinorState.Track
+ );
+ });
+
+ await w1.call(WalletApiOperation.ApplyDevExperiment, {
+ devExperimentUri: "taler://dev-experiment/stop-block-refresh",
+ });
+
+ await depositTrackCond;
+}
+
+runWalletBlockedDepositTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-blocked-pay-merchant.ts b/packages/taler-harness/src/integrationtests/test-wallet-blocked-pay-merchant.ts
new file mode 100644
index 000000000..004de87c8
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-wallet-blocked-pay-merchant.ts
@@ -0,0 +1,142 @@
+/*
+ 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 {
+ MerchantApiClient,
+ PreparePayResultType,
+ j2s,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { CoinConfig } from "../harness/denomStructures.js";
+import { GlobalTestState } from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironmentV2,
+ createWalletDaemonWithClient,
+ makeTestPaymentV2,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
+
+const coinCommon = {
+ cipher: "RSA" as const,
+ durationLegal: "3 years",
+ durationSpend: "2 years",
+ durationWithdraw: "7 days",
+ feeDeposit: "TESTKUDOS:0",
+ feeRefresh: "TESTKUDOS:0",
+ feeRefund: "TESTKUDOS:0",
+ feeWithdraw: "TESTKUDOS:0",
+ rsaKeySize: 1024,
+};
+
+const coinConfigList: CoinConfig[] = [
+ {
+ ...coinCommon,
+ name: "n1",
+ value: "TESTKUDOS:1",
+ },
+ {
+ ...coinCommon,
+ name: "n5",
+ value: "TESTKUDOS:5",
+ },
+];
+
+/**
+ * Run test for paying a merchant with balance locked behind a pending refresh.
+ */
+export async function runWalletBlockedPayMerchantTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { bank, exchange, merchant } = await createSimpleTestkudosEnvironmentV2(
+ t,
+ coinConfigList,
+ );
+
+ // Withdraw digital cash into the wallet.
+
+ const { walletClient: w1 } = await createWalletDaemonWithClient(t, {
+ name: "w1",
+ persistent: true,
+ config: {
+ testing: {
+ devModeActive: true,
+ },
+ },
+ });
+
+ await withdrawViaBankV2(t, {
+ walletClient: w1,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ });
+
+ await w1.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+
+ // Prevent the wallet from doing refreshes by injecting a 5xx
+ // status for all refresh requests.
+ await w1.call(WalletApiOperation.ApplyDevExperiment, {
+ devExperimentUri: "taler://dev-experiment/start-block-refresh",
+ });
+
+ // Do a payment that causes a refresh.
+ await makeTestPaymentV2(t, {
+ merchant,
+ walletClient: w1,
+ order: {
+ summary: "test",
+ amount: "TESTKUDOS:2",
+ },
+ });
+
+ const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl());
+
+ const orderResp = await merchantClient.createOrder({
+ order: {
+ summary: "My Payment",
+ amount: "TESTKUDOS:18",
+ },
+ });
+
+ let orderStatus = await merchantClient.queryPrivateOrderStatus({
+ orderId: orderResp.order_id,
+ });
+
+ t.assertTrue(orderStatus.order_status === "unpaid");
+
+ // Make wallet pay for the order
+
+ const preparePayResult = await w1.call(WalletApiOperation.PreparePayForUri, {
+ talerPayUri: orderStatus.taler_pay_uri,
+ });
+
+ console.log(`prepare pay result: ${j2s(preparePayResult)}`);
+
+ t.assertTrue(
+ preparePayResult.status === PreparePayResultType.PaymentPossible,
+ );
+
+ await w1.call(WalletApiOperation.ApplyDevExperiment, {
+ devExperimentUri: "taler://dev-experiment/stop-block-refresh",
+ });
+
+ await w1.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+}
+
+runWalletBlockedPayMerchantTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-blocked-pay-peer-pull.ts b/packages/taler-harness/src/integrationtests/test-wallet-blocked-pay-peer-pull.ts
new file mode 100644
index 000000000..36a6fea05
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-wallet-blocked-pay-peer-pull.ts
@@ -0,0 +1,177 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ AbsoluteTime,
+ AmountString,
+ Duration,
+ NotificationType,
+ TransactionMajorState,
+ TransactionMinorState,
+ TransactionType,
+ j2s,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { CoinConfig } from "../harness/denomStructures.js";
+import { GlobalTestState } from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironmentV2,
+ createWalletDaemonWithClient,
+ makeTestPaymentV2,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
+
+const coinCommon = {
+ cipher: "RSA" as const,
+ durationLegal: "3 years",
+ durationSpend: "2 years",
+ durationWithdraw: "7 days",
+ feeDeposit: "TESTKUDOS:0",
+ feeRefresh: "TESTKUDOS:0",
+ feeRefund: "TESTKUDOS:0",
+ feeWithdraw: "TESTKUDOS:0",
+ rsaKeySize: 1024,
+};
+
+/**
+ * Run test for a peer push payment with balance locked behind a pending refresh.
+ */
+export async function runWalletBlockedPayPeerPullTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const coinConfigList: CoinConfig[] = [
+ {
+ ...coinCommon,
+ name: "n1",
+ value: "TESTKUDOS:1",
+ },
+ {
+ ...coinCommon,
+ name: "n5",
+ value: "TESTKUDOS:5",
+ },
+ ];
+
+ const { bank, exchange, merchant } = await createSimpleTestkudosEnvironmentV2(
+ t,
+ coinConfigList,
+ );
+
+ // Withdraw digital cash into the wallet.
+
+ const { walletClient: w1 } = await createWalletDaemonWithClient(t, {
+ name: "w1",
+ persistent: true,
+ config: {
+ testing: {
+ devModeActive: true,
+ },
+ },
+ });
+
+ const { walletClient: w2 } = await createWalletDaemonWithClient(t, {
+ name: "w2",
+ persistent: true,
+ config: {
+ testing: {
+ devModeActive: true,
+ },
+ },
+ });
+
+ await withdrawViaBankV2(t, {
+ walletClient: w1,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ });
+
+ await w1.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+
+ // Prevent the wallet from doing refreshes by injecting a 5xx
+ // status for all refresh requests.
+ await w1.call(WalletApiOperation.ApplyDevExperiment, {
+ devExperimentUri: "taler://dev-experiment/start-block-refresh",
+ });
+
+ // Do a payment that causes a refresh.
+ await makeTestPaymentV2(t, {
+ merchant,
+ walletClient: w1,
+ order: {
+ summary: "test",
+ amount: "TESTKUDOS:2",
+ },
+ });
+
+ await w2.call(WalletApiOperation.AddExchange, {
+ exchangeBaseUrl: exchange.baseUrl,
+ });
+
+ const pullCreditReadyCond = w2.waitForNotificationCond((n) => {
+ return (
+ n.type === NotificationType.TransactionStateTransition &&
+ n.transactionId.startsWith("txn:peer-pull-credit:") &&
+ n.newTxState.major === TransactionMajorState.Pending &&
+ n.newTxState.minor === TransactionMinorState.Ready
+ );
+ });
+
+ const initResp = await w2.call(WalletApiOperation.InitiatePeerPullCredit, {
+ partialContractTerms: {
+ summary: "hi!",
+ amount: "TESTKUDOS:18" as AmountString,
+ purse_expiration: AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ hours: 1 }),
+ ),
+ ),
+ },
+ });
+
+ await pullCreditReadyCond;
+
+ const initTx = await w2.call(WalletApiOperation.GetTransactionById, {
+ transactionId: initResp.transactionId,
+ });
+
+ t.assertDeepEqual(initTx.type, TransactionType.PeerPullCredit);
+ t.assertTrue(!!initTx.talerUri);
+
+ const checkResp = await w1.call(WalletApiOperation.PreparePeerPullDebit, {
+ talerUri: initTx.talerUri,
+ });
+
+ console.log(`check resp ${j2s(checkResp)}`);
+
+ const confirmResp = await w1.call(WalletApiOperation.ConfirmPeerPullDebit, {
+ transactionId: checkResp.transactionId,
+ });
+
+ console.log(`confirm resp ${j2s(confirmResp)}`);
+
+ await w1.call(WalletApiOperation.ApplyDevExperiment, {
+ devExperimentUri: "taler://dev-experiment/stop-block-refresh",
+ });
+
+ await w1.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+}
+
+runWalletBlockedPayPeerPullTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-blocked-pay-peer-push.ts b/packages/taler-harness/src/integrationtests/test-wallet-blocked-pay-peer-push.ts
new file mode 100644
index 000000000..7427f2b07
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-wallet-blocked-pay-peer-push.ts
@@ -0,0 +1,149 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ AbsoluteTime,
+ AmountString,
+ Duration,
+ NotificationType,
+ TransactionMajorState,
+ TransactionMinorState,
+ j2s,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { CoinConfig } from "../harness/denomStructures.js";
+import { GlobalTestState } from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironmentV2,
+ createWalletDaemonWithClient,
+ makeTestPaymentV2,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
+
+const coinCommon = {
+ cipher: "RSA" as const,
+ durationLegal: "3 years",
+ durationSpend: "2 years",
+ durationWithdraw: "7 days",
+ feeDeposit: "TESTKUDOS:0",
+ feeRefresh: "TESTKUDOS:0",
+ feeRefund: "TESTKUDOS:0",
+ feeWithdraw: "TESTKUDOS:0",
+ rsaKeySize: 1024,
+};
+
+/**
+ * Run test for a peer push payment with balance locked behind a pending refresh.
+ */
+export async function runWalletBlockedPayPeerPushTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const coinConfigList: CoinConfig[] = [
+ {
+ ...coinCommon,
+ name: "n1",
+ value: "TESTKUDOS:1",
+ },
+ {
+ ...coinCommon,
+ name: "n5",
+ value: "TESTKUDOS:5",
+ },
+ ];
+
+ const { bank, exchange, merchant } = await createSimpleTestkudosEnvironmentV2(
+ t,
+ coinConfigList,
+ );
+
+ // Withdraw digital cash into the wallet.
+
+ const { walletClient: w1 } = await createWalletDaemonWithClient(t, {
+ name: "w1",
+ persistent: true,
+ config: {
+ testing: {
+ devModeActive: true,
+ },
+ },
+ });
+
+ await withdrawViaBankV2(t, {
+ walletClient: w1,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ });
+
+ await w1.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+
+ // Prevent the wallet from doing refreshes by injecting a 5xx
+ // status for all refresh requests.
+ await w1.call(WalletApiOperation.ApplyDevExperiment, {
+ devExperimentUri: "taler://dev-experiment/start-block-refresh",
+ });
+
+ // Do a payment that causes a refresh.
+ await makeTestPaymentV2(t, {
+ merchant,
+ walletClient: w1,
+ order: {
+ summary: "test",
+ amount: "TESTKUDOS:2",
+ },
+ });
+
+ const checkResp = await w1.call(WalletApiOperation.CheckPeerPushDebit, {
+ amount: "TESTKUDOS:18" as AmountString,
+ });
+
+ console.log(`check resp ${j2s(checkResp)}`);
+
+ const readyCond = w1.waitForNotificationCond((n) => {
+ return (
+ n.type === NotificationType.TransactionStateTransition &&
+ n.transactionId.startsWith("txn:peer-push-debit:") &&
+ n.newTxState.major === TransactionMajorState.Pending &&
+ n.newTxState.minor === TransactionMinorState.Ready
+ );
+ });
+
+ const confirmResp = await w1.call(WalletApiOperation.InitiatePeerPushDebit, {
+ partialContractTerms: {
+ summary: "hi!",
+ amount: "TESTKUDOS:18" as AmountString,
+ purse_expiration: AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ hours: 1 }),
+ ),
+ ),
+ },
+ });
+
+ console.log(`confirm resp ${j2s(confirmResp)}`);
+
+ await w1.call(WalletApiOperation.ApplyDevExperiment, {
+ devExperimentUri: "taler://dev-experiment/stop-block-refresh",
+ });
+
+ await readyCond;
+}
+
+runWalletBlockedPayPeerPushTest.suites = ["wallet"];
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-wallet-balance.ts b/packages/taler-harness/src/integrationtests/test-wallet-cli-termination.ts
index f5226c6c0..4f015799f 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-wallet-balance.ts
+++ b/packages/taler-harness/src/integrationtests/test-wallet-cli-termination.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2020 Taler Systems S.A.
+ (C) 2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -17,33 +17,28 @@
/**
* Imports.
*/
-import { Duration, PreparePayResultType } from "@gnu-taler/taler-util";
+import { AmountString } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js";
import {
+ BankService,
ExchangeService,
- FakebankService,
- getRandomIban,
GlobalTestState,
- MerchantPrivateApi,
MerchantService,
- setupDb,
WalletCli,
+ generateRandomPayto,
+ setupDb,
} from "../harness/harness.js";
-import { withdrawViaBank } from "../harness/helpers.js";
/**
- * Test for wallet balance error messages / different types of insufficient balance.
- *
- * Related bugs:
- * https://bugs.taler.net/n/7299
+ * Test that run-until-done of taler-wallet-cli terminates.
*/
-export async function runWalletBalanceTest(t: GlobalTestState) {
- // Set up test environment
-
+export async function runWalletCliTerminationTest(t: GlobalTestState) {
const db = await setupDb(t);
- const bank = await FakebankService.create(t, {
+ const coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS"));
+
+ const bank = await BankService.create(t, {
allowRegistrations: true,
currency: "TESTKUDOS",
database: db.connStr,
@@ -76,7 +71,6 @@ export async function runWalletBalanceTest(t: GlobalTestState) {
await bank.pingUntilAvailable();
- const coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS"));
exchange.addCoinConfigList(coinConfig);
await exchange.start();
@@ -87,58 +81,21 @@ export async function runWalletBalanceTest(t: GlobalTestState) {
await merchant.start();
await merchant.pingUntilAvailable();
- // Fakebank uses x-taler-bank, but merchant is configured to only accept sepa!
- const label = "mymerchant";
- await merchant.addInstance({
+ await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [
- `payto://iban/SANDBOXX/${getRandomIban(label)}?receiver-name=${label}`,
- ],
- defaultWireTransferDelay: Duration.toTalerProtocolDuration(
- Duration.fromSpec({ minutes: 1 }),
- ),
+ paytoUris: [generateRandomPayto("merchant-default")],
});
- console.log("setup done!");
-
- const wallet = new WalletCli(t);
-
- // Withdraw digital cash into the wallet.
-
- await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
+ const wallet = new WalletCli(t, "wallet");
- const order = {
- summary: "Buy me!",
- amount: "TESTKUDOS:5",
- fulfillment_url: "taler://fulfillment-success/thx",
- };
-
- const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
- order,
- });
-
- let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
- orderId: orderResp.order_id,
+ await wallet.client.call(WalletApiOperation.WithdrawTestBalance, {
+ corebankApiBaseUrl: bank.corebankApiBaseUrl,
+ exchangeBaseUrl: exchange.baseUrl,
+ amount: "TESTKUDOS:20" as AmountString,
});
- t.assertTrue(orderStatus.order_status === "unpaid");
-
- // Make wallet pay for the order
-
- const preparePayResult = await wallet.client.call(
- WalletApiOperation.PreparePayForUri,
- {
- talerPayUri: orderStatus.taler_pay_uri,
- },
- );
-
- t.assertDeepEqual(
- preparePayResult.status,
- PreparePayResultType.InsufficientBalance,
- );
-
await wallet.runUntilDone();
}
-runWalletBalanceTest.suites = ["wallet"];
+runWalletCliTerminationTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-config.ts b/packages/taler-harness/src/integrationtests/test-wallet-config.ts
new file mode 100644
index 000000000..461574031
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-wallet-config.ts
@@ -0,0 +1,67 @@
+/*
+ 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 { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState } from "../harness/harness.js";
+import { createWalletDaemonWithClient } from "../harness/helpers.js";
+
+export async function runWalletConfigTest(t: GlobalTestState) {
+ const w1 = await createWalletDaemonWithClient(t, {
+ name: "w1",
+ config: {
+ builtin: {
+ exchanges: [],
+ },
+ },
+ });
+
+ const exchangesResp1 = await w1.walletClient.call(
+ WalletApiOperation.ListExchanges,
+ {},
+ );
+
+ t.assertDeepEqual(exchangesResp1.exchanges.length, 0);
+
+ const w2 = await createWalletDaemonWithClient(t, {
+ name: "w2",
+ config: {
+ builtin: {
+ exchanges: [
+ {
+ exchangeBaseUrl: "https://exchange.demo.taler.net/",
+ currencyHint: "KUDOS",
+ },
+ {
+ exchangeBaseUrl: "https://exchange.test.taler.net/",
+ currencyHint: "TESTKUDOS",
+ },
+ ],
+ },
+ },
+ });
+
+ const exchangesResp2 = await w2.walletClient.call(
+ WalletApiOperation.ListExchanges,
+ {},
+ );
+
+ t.assertDeepEqual(exchangesResp2.exchanges.length, 2);
+}
+
+runWalletConfigTest.suites = ["wallet"];
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-wallet-cryptoworker.ts b/packages/taler-harness/src/integrationtests/test-wallet-cryptoworker.ts
index a9f1c4d80..6c2006636 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-wallet-cryptoworker.ts
+++ b/packages/taler-harness/src/integrationtests/test-wallet-cryptoworker.ts
@@ -17,23 +17,10 @@
/**
* Imports.
*/
-import { j2s } from "@gnu-taler/taler-util";
import {
- checkReserve,
- CryptoDispatcher,
- depositCoin,
- downloadExchangeInfo,
- findDenomOrThrow,
- NodeHttpLib,
- refreshCoin,
- SynchronousCryptoWorkerFactoryNode,
- TalerError,
- topupReserveWithDemobank,
WalletApiOperation,
- withdrawCoin,
} from "@gnu-taler/taler-wallet-core";
import { GlobalTestState, WalletCli } from "../harness/harness.js";
-import { createSimpleTestkudosEnvironment } from "../harness/helpers.js";
/**
* Run test for the different crypto workers.
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-wallet-dbless.ts b/packages/taler-harness/src/integrationtests/test-wallet-dbless.ts
index 269a8b240..a089d99b5 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-wallet-dbless.ts
+++ b/packages/taler-harness/src/integrationtests/test-wallet-dbless.ts
@@ -17,22 +17,29 @@
/**
* Imports.
*/
-import { j2s } from "@gnu-taler/taler-util";
import {
- checkReserve,
+ AmountString,
+ encodeCrock,
+ getRandomBytes,
+ j2s,
+ TalerError,
+} from "@gnu-taler/taler-util";
+import {
+ applyRunConfigDefaults,
CryptoDispatcher,
+ SynchronousCryptoWorkerFactoryPlain,
+} from "@gnu-taler/taler-wallet-core";
+import {
+ checkReserve,
depositCoin,
downloadExchangeInfo,
findDenomOrThrow,
- NodeHttpLib,
refreshCoin,
- SynchronousCryptoWorkerFactoryNode,
- TalerError,
- topupReserveWithDemobank,
+ topupReserveWithBank,
withdrawCoin,
-} from "@gnu-taler/taler-wallet-core";
-import { GlobalTestState } from "../harness/harness.js";
-import { createSimpleTestkudosEnvironment } from "../harness/helpers.js";
+} from "@gnu-taler/taler-wallet-core/dbless";
+import { GlobalTestState, harnessHttpLib } from "../harness/harness.js";
+import { createSimpleTestkudosEnvironmentV2 } from "../harness/helpers.js";
/**
* Run test for basic, bank-integrated withdrawal and payment.
@@ -40,10 +47,12 @@ import { createSimpleTestkudosEnvironment } from "../harness/helpers.js";
export async function runWalletDblessTest(t: GlobalTestState) {
// Set up test environment
- const { bank, exchange } = await createSimpleTestkudosEnvironment(t);
+ const { bank, exchange } = await createSimpleTestkudosEnvironmentV2(t);
- const http = new NodeHttpLib();
- const cryptiDisp = new CryptoDispatcher(new SynchronousCryptoWorkerFactoryNode());
+ const http = harnessHttpLib;
+ const cryptiDisp = new CryptoDispatcher(
+ new SynchronousCryptoWorkerFactoryPlain(),
+ );
const cryptoApi = cryptiDisp.cryptoApi;
try {
@@ -53,20 +62,36 @@ export async function runWalletDblessTest(t: GlobalTestState) {
const reserveKeyPair = await cryptoApi.createEddsaKeypair({});
- await topupReserveWithDemobank(
+ let reserveUrl = new URL(
+ `reserves/${reserveKeyPair.pub}`,
+ exchange.baseUrl,
+ );
+ reserveUrl.searchParams.set("timeout_ms", "30000");
+ const longpollReq = http.fetch(reserveUrl.href, {
+ method: "GET",
+ });
+
+ await topupReserveWithBank({
+ amount: "TESTKUDOS:10" as AmountString,
http,
- reserveKeyPair.pub,
- bank.baseUrl,
- bank.bankAccessApiBaseUrl,
+ reservePub: reserveKeyPair.pub,
+ corebankApiBaseUrl: bank.corebankApiBaseUrl,
exchangeInfo,
- "TESTKUDOS:10",
- );
+ });
+
+ console.log("waiting for longpoll request");
+ const resp = await longpollReq;
+ console.log(`got response, status ${resp.status}`);
- await exchange.runWirewatchOnce();
+ console.log(exchangeInfo);
await checkReserve(http, exchange.baseUrl, reserveKeyPair.pub);
- const d1 = findDenomOrThrow(exchangeInfo, "TESTKUDOS:8");
+ const defaultConfig = applyRunConfigDefaults();
+
+ const d1 = findDenomOrThrow(exchangeInfo, "TESTKUDOS:8" as AmountString, {
+ denomselAllowLate: defaultConfig.testing.denomselAllowLate,
+ });
const coin = await withdrawCoin({
http,
@@ -79,8 +104,27 @@ export async function runWalletDblessTest(t: GlobalTestState) {
exchangeBaseUrl: exchange.baseUrl,
});
+ const wireSalt = encodeCrock(getRandomBytes(16));
+ const merchantPub = encodeCrock(getRandomBytes(32));
+ const contractTermsHash = encodeCrock(getRandomBytes(64));
+
+ await depositCoin({
+ contractTermsHash,
+ merchantPub,
+ wireSalt,
+ amount: "TESTKUDOS:4" as AmountString,
+ coin: coin,
+ cryptoApi,
+ exchangeBaseUrl: exchange.baseUrl,
+ http,
+ });
+
+ // Idempotency
await depositCoin({
- amount: "TESTKUDOS:4",
+ contractTermsHash,
+ merchantPub,
+ wireSalt,
+ amount: "TESTKUDOS:4" as AmountString,
coin: coin,
cryptoApi,
exchangeBaseUrl: exchange.baseUrl,
@@ -88,8 +132,12 @@ export async function runWalletDblessTest(t: GlobalTestState) {
});
const refreshDenoms = [
- findDenomOrThrow(exchangeInfo, "TESTKUDOS:1"),
- findDenomOrThrow(exchangeInfo, "TESTKUDOS:1"),
+ findDenomOrThrow(exchangeInfo, "TESTKUDOS:1" as AmountString, {
+ denomselAllowLate: defaultConfig.testing.denomselAllowLate,
+ }),
+ findDenomOrThrow(exchangeInfo, "TESTKUDOS:1" as AmountString, {
+ denomselAllowLate: defaultConfig.testing.denomselAllowLate,
+ }),
];
await refreshCoin({
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-dd48.ts b/packages/taler-harness/src/integrationtests/test-wallet-dd48.ts
new file mode 100644
index 000000000..3341b6a53
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-wallet-dd48.ts
@@ -0,0 +1,183 @@
+/*
+ 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 {
+ ExchangeEntryStatus,
+ NotificationType,
+ TalerError,
+ TalerErrorCode,
+ WalletNotification,
+ j2s,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js";
+import {
+ ExchangeService,
+ FakebankService,
+ GlobalTestState,
+ WalletClient,
+ WalletService,
+ setupDb,
+} from "../harness/harness.js";
+import { withdrawViaBankV2 } from "../harness/helpers.js";
+
+/**
+ * Test for DD48 notifications.
+ */
+export async function runWalletDd48Test(t: GlobalTestState) {
+ // Set up test environment
+
+ 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 exchangeBankAccount = await bank.createExchangeAccount(
+ "myexchange",
+ "x",
+ );
+ exchange.addBankAccount("1", exchangeBankAccount);
+
+ bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri);
+
+ await bank.start();
+
+ await bank.pingUntilAvailable();
+
+ const coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS"));
+ exchange.addCoinConfigList(coinConfig);
+
+ await exchange.start();
+ await exchange.pingUntilAvailable();
+
+ const walletService = new WalletService(t, {
+ name: "wallet",
+ useInMemoryDb: true,
+ });
+ await walletService.start();
+ await walletService.pingUntilAvailable();
+
+ const allNotifications: WalletNotification[] = [];
+
+ const walletClient = new WalletClient({
+ name: "wallet",
+ unixPath: walletService.socketPath,
+ onNotification(n) {
+ console.log("got notification", n);
+ allNotifications.push(n);
+ },
+ });
+ await walletClient.connect();
+ await walletClient.client.call(WalletApiOperation.InitWallet, {
+ config: {
+ testing: {
+ skipDefaults: true,
+ },
+ },
+ });
+
+ await walletClient.call(WalletApiOperation.AddExchange, {
+ exchangeBaseUrl: exchange.baseUrl,
+ });
+
+ {
+ const exchangeEntry = await walletClient.call(
+ WalletApiOperation.GetExchangeEntryByUrl,
+ {
+ exchangeBaseUrl: exchange.baseUrl,
+ },
+ );
+
+ t.assertDeepEqual(
+ exchangeEntry.exchangeEntryStatus,
+ ExchangeEntryStatus.Ephemeral,
+ );
+
+ const resources = await walletClient.call(
+ WalletApiOperation.GetExchangeResources,
+ {
+ exchangeBaseUrl: exchange.baseUrl,
+ },
+ );
+ t.assertDeepEqual(resources.hasResources, false);
+ }
+
+ const wres = await withdrawViaBankV2(t, {
+ walletClient,
+ amount: "TESTKUDOS:20",
+ bank,
+ exchange,
+ });
+
+ await wres.withdrawalFinishedCond;
+
+ const exchangeEntry = await walletClient.call(
+ WalletApiOperation.GetExchangeEntryByUrl,
+ {
+ exchangeBaseUrl: exchange.baseUrl,
+ },
+ );
+
+ t.assertDeepEqual(
+ exchangeEntry.exchangeEntryStatus,
+ ExchangeEntryStatus.Used,
+ );
+
+ t.assertTrue(
+ !!allNotifications.find(
+ (x) =>
+ x.type === NotificationType.ExchangeStateTransition &&
+ x.oldExchangeState == null &&
+ x.newExchangeState.exchangeEntryStatus ===
+ ExchangeEntryStatus.Ephemeral,
+ ),
+ );
+
+ console.log(j2s(allNotifications));
+
+ const delErr = await t.assertThrowsAsync(async () => {
+ await walletClient.call(WalletApiOperation.DeleteExchange, {
+ exchangeBaseUrl: exchange.baseUrl,
+ });
+ });
+
+ t.assertTrue(delErr instanceof TalerError);
+ t.assertDeepEqual(
+ delErr.errorDetail.code,
+ TalerErrorCode.WALLET_EXCHANGE_ENTRY_USED,
+ );
+
+ await walletClient.call(WalletApiOperation.DeleteExchange, {
+ exchangeBaseUrl: exchange.baseUrl,
+ purge: true,
+ });
+}
+
+runWalletDd48Test.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-denom-expire.ts b/packages/taler-harness/src/integrationtests/test-wallet-denom-expire.ts
new file mode 100644
index 000000000..4ce8cde4c
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-wallet-denom-expire.ts
@@ -0,0 +1,154 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import { Duration, Logger, NotificationType, j2s } from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { makeNoFeeCoinConfig } from "../harness/denomStructures.js";
+import {
+ BankService,
+ ExchangeService,
+ GlobalTestState,
+ MerchantService,
+ generateRandomPayto,
+ setupDb,
+} from "../harness/harness.js";
+import {
+ applyTimeTravelV2,
+ createWalletDaemonWithClient,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
+
+const logger = new Logger("test-exchange-timetravel.ts");
+
+/**
+ * Test how the wallet handles an expired denomination.
+ */
+export async function runWalletDenomExpireTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const db = await setupDb(t);
+
+ const bank = await BankService.create(t, {
+ allowRegistrations: true,
+ currency: "TESTKUDOS",
+ database: db.connStr,
+ httpPort: 8082,
+ });
+
+ const exchange = ExchangeService.create(t, {
+ name: "testexchange-1",
+ currency: "TESTKUDOS",
+ httpPort: 8081,
+ database: db.connStr,
+ });
+
+ 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(makeNoFeeCoinConfig("TESTKUDOS"));
+
+ await exchange.start();
+ await exchange.pingUntilAvailable();
+
+ merchant.addExchange(exchange);
+
+ await merchant.start();
+ await merchant.pingUntilAvailable();
+
+ console.log("merchant started, configuring instances");
+
+ await merchant.addInstanceWithWireAccount({
+ id: "default",
+ name: "Default Instance",
+ paytoUris: [generateRandomPayto("merchant-default")],
+ });
+
+ await merchant.addInstanceWithWireAccount({
+ id: "minst1",
+ name: "minst1",
+ paytoUris: [generateRandomPayto("minst1")],
+ });
+
+ console.log("setup done!");
+
+ const { walletClient } = await createWalletDaemonWithClient(t, {
+ name: "default",
+ });
+
+ // Withdraw digital cash into the wallet.
+
+ const wres = await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:15",
+ });
+ await wres.withdrawalFinishedCond;
+
+ const denomLossCond = walletClient.waitForNotificationCond((n) => {
+ return (
+ n.type === NotificationType.TransactionStateTransition &&
+ n.transactionId.startsWith("txn:denom-loss:")
+ );
+ });
+
+ // Travel into the future, the deposit expiration is two years
+ // into the future.
+ console.log("applying first time travel");
+ await applyTimeTravelV2(
+ Duration.toMilliseconds(Duration.fromSpec({ days: 800 })),
+ {
+ walletClient,
+ exchange,
+ merchant,
+ },
+ );
+
+ t.logStep("before-wait-denom-loss");
+
+ // Should be detected automatically, as exchange entry is surely outdated.
+ await denomLossCond;
+
+ const bal = await walletClient.call(WalletApiOperation.GetBalances, {});
+ console.log(`balances: ${j2s(bal)}`);
+
+ const txns = await walletClient.call(WalletApiOperation.GetTransactions, {
+ sort: "stable-ascending",
+ includeRefreshes: true,
+ });
+ console.log(`transactions: ${j2s(txns)}`);
+}
+
+runWalletDenomExpireTest.suites = ["exchange"];
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-dev-experiments.ts b/packages/taler-harness/src/integrationtests/test-wallet-dev-experiments.ts
new file mode 100644
index 000000000..1f1187d80
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-wallet-dev-experiments.ts
@@ -0,0 +1,48 @@
+/*
+ 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 { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState } from "../harness/harness.js";
+import { createWalletDaemonWithClient } from "../harness/helpers.js";
+
+export async function runWalletDevExperimentsTest(t: GlobalTestState) {
+ const w1 = await createWalletDaemonWithClient(t, {
+ name: "w1",
+ config: {
+ testing: {
+ devModeActive: true,
+ },
+ },
+ });
+
+ await w1.walletClient.call(WalletApiOperation.ApplyDevExperiment, {
+ devExperimentUri: "taler://dev-experiment/insert-pending-refresh",
+ });
+
+ const txnResp = await w1.walletClient.call(
+ WalletApiOperation.GetTransactions,
+ {
+ includeRefreshes: true,
+ },
+ );
+
+ t.assertTrue(txnResp.transactions.length > 0);
+}
+
+runWalletDevExperimentsTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-exchange-update.ts b/packages/taler-harness/src/integrationtests/test-wallet-exchange-update.ts
new file mode 100644
index 000000000..3251750da
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-wallet-exchange-update.ts
@@ -0,0 +1,165 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ AmountString,
+ ExchangeUpdateStatus,
+ NotificationType,
+ j2s,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { defaultCoinConfig } from "../harness/denomStructures.js";
+import {
+ BankService,
+ ExchangeService,
+ GlobalTestState,
+ setupDb,
+} from "../harness/harness.js";
+import {
+ createWalletDaemonWithClient,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
+
+/**
+ * Test how the wallet reacts when an exchange unexpectedly updates
+ * properties like the master public key.
+ */
+export async function runWalletExchangeUpdateTest(
+ t: GlobalTestState,
+): Promise<void> {
+ // Set up test environment
+
+ const db = await setupDb(t);
+ const db2 = await setupDb(t, {
+ nameSuffix: "two",
+ });
+
+ const bank = await BankService.create(t, {
+ allowRegistrations: true,
+ currency: "TESTKUDOS",
+ database: db.connStr,
+ httpPort: 8082,
+ });
+
+ const exchangeOne = ExchangeService.create(t, {
+ name: "testexchange-1",
+ currency: "TESTKUDOS",
+ httpPort: 8081,
+ database: db.connStr,
+ });
+
+ // Danger: The second exchange has the same port!
+ // That's because we want it to have the same base URL,
+ // and we'll only start on of them at a time.
+ const exchangeTwo = ExchangeService.create(t, {
+ name: "testexchange-2",
+ currency: "TESTKUDOS",
+ httpPort: 8081,
+ database: db2.connStr,
+ });
+
+ const exchangeBankAccount = await bank.createExchangeAccount(
+ "myexchange",
+ "x",
+ );
+
+ await exchangeOne.addBankAccount("1", exchangeBankAccount);
+ await exchangeTwo.addBankAccount("1", exchangeBankAccount);
+
+ // Same anyway.
+ bank.setSuggestedExchange(exchangeOne, exchangeBankAccount.accountPaytoUri);
+
+ await bank.start();
+
+ exchangeOne.addCoinConfigList(defaultCoinConfig.map((x) => x("TESTKUDOS")));
+ exchangeTwo.addCoinConfigList(defaultCoinConfig.map((x) => x("TESTKUDOS")));
+
+ // Only start first exchange.
+ await exchangeOne.start();
+
+ const { walletClient } = await createWalletDaemonWithClient(t, {
+ name: "wallet",
+ persistent: true,
+ });
+
+ // Since the default exchanges can change, we start the wallet in tests
+ // with no built-in defaults. Thus the list of exchanges is empty here.
+ const exchangesListResult = await walletClient.call(
+ WalletApiOperation.ListExchanges,
+ {},
+ );
+
+ t.assertDeepEqual(exchangesListResult.exchanges.length, 0);
+
+ const wres = await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange: exchangeOne,
+ amount: "TESTKUDOS:10",
+ });
+
+ await wres.withdrawalFinishedCond;
+
+ await exchangeOne.stop();
+
+ console.log("starting second exchange");
+ await exchangeTwo.start();
+
+ console.log("updating exchange entry");
+
+ await t.assertThrowsAsync(async () => {
+ await walletClient.call(WalletApiOperation.UpdateExchangeEntry, {
+ exchangeBaseUrl: exchangeOne.baseUrl,
+ force: true,
+ });
+ });
+
+ const exchangeEntry = await walletClient.call(
+ WalletApiOperation.GetExchangeEntryByUrl,
+ {
+ exchangeBaseUrl: exchangeOne.baseUrl,
+ },
+ );
+
+ console.log(`exchange entry: ${j2s(exchangeEntry)}`);
+
+ await t.assertThrowsAsync(async () => {
+ await walletClient.call(WalletApiOperation.GetWithdrawalDetailsForAmount, {
+ amount: "TESTKUDOS:10" as AmountString,
+ exchangeBaseUrl: exchangeOne.baseUrl,
+ });
+ });
+
+ const exchangeAvailableCond = walletClient.waitForNotificationCond((n) => {
+ console.log(`got notif ${j2s(n)}`);
+ return (
+ n.type === NotificationType.ExchangeStateTransition &&
+ n.newExchangeState.exchangeUpdateStatus === ExchangeUpdateStatus.Ready
+ );
+ });
+
+ await exchangeTwo.stop();
+
+ console.log("starting first exchange");
+ await exchangeOne.start();
+
+ await exchangeAvailableCond;
+}
+
+runWalletExchangeUpdateTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-gendb.ts b/packages/taler-harness/src/integrationtests/test-wallet-gendb.ts
new file mode 100644
index 000000000..9e3b60899
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-wallet-gendb.ts
@@ -0,0 +1,111 @@
+/*
+ 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 { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState } from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironmentV2,
+ withdrawViaBankV2,
+ makeTestPaymentV2,
+} from "../harness/helpers.js";
+import {
+ AbsoluteTime,
+ AmountString,
+ Duration,
+ NotificationType,
+ TransactionMajorState,
+ TransactionMinorState,
+ j2s,
+} from "@gnu-taler/taler-util";
+
+/**
+ * Test that creates various transactions and exports the resulting
+ * database. Used to generate a database export file for DB compatibility
+ * testing.
+ */
+export async function runWalletGenDbTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { walletClient, bank, exchange, merchant } =
+ await createSimpleTestkudosEnvironmentV2(t);
+
+ // Withdraw digital cash into the wallet.
+
+ await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:50",
+ });
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+
+ const order = {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:10",
+ fulfillment_url: "taler://fulfillment-success/thx",
+ };
+
+ await makeTestPaymentV2(t, { walletClient, merchant, order });
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+
+ const purseExpiration = AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ days: 2 }),
+ ),
+ );
+
+ const peerPullIniResp = await walletClient.call(
+ WalletApiOperation.InitiatePeerPullCredit,
+ {
+ exchangeBaseUrl: exchange.baseUrl,
+ partialContractTerms: {
+ summary: "Hello World",
+ amount: "TESTKUDOS:5" as AmountString,
+ purse_expiration: purseExpiration,
+ },
+ },
+ );
+
+ const peerPullCreditReadyCond = walletClient.waitForNotificationCond(
+ (x) =>
+ x.type === NotificationType.TransactionStateTransition &&
+ x.transactionId === peerPullIniResp.transactionId &&
+ x.newTxState.major === TransactionMajorState.Pending &&
+ x.newTxState.minor === TransactionMinorState.Ready,
+ );
+
+ await peerPullCreditReadyCond;
+
+ const checkResp = await walletClient.call(
+ WalletApiOperation.PreparePeerPullDebit,
+ {
+ talerUri: peerPullIniResp.talerUri,
+ },
+ );
+
+ await walletClient.call(WalletApiOperation.ConfirmPeerPullDebit, {
+ transactionId: checkResp.transactionId,
+ });
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+}
+
+runWalletGenDbTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-insufficient-balance.ts b/packages/taler-harness/src/integrationtests/test-wallet-insufficient-balance.ts
new file mode 100644
index 000000000..ac1244446
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-wallet-insufficient-balance.ts
@@ -0,0 +1,166 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ AmountString,
+ Duration,
+ PaymentInsufficientBalanceDetails,
+ TalerErrorCode,
+ WalletNotification,
+} 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,
+ WalletClient,
+ WalletService,
+ generateRandomPayto,
+ setupDb,
+} from "../harness/harness.js";
+import { withdrawViaBankV2 } from "../harness/helpers.js";
+
+export async function runWalletInsufficientBalanceTest(t: GlobalTestState) {
+ // Set up test environment
+
+ 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",
+ );
+ exchangeBankAccount.skipWireFeeCreation = true;
+ exchange.addBankAccount("1", exchangeBankAccount);
+
+ bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri);
+
+ await bank.start();
+
+ await bank.pingUntilAvailable();
+
+ const coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS"));
+ 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")],
+ defaultWireTransferDelay: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ minutes: 1 }),
+ ),
+ });
+
+ await merchant.addInstanceWithWireAccount({
+ id: "minst1",
+ name: "minst1",
+ paytoUris: [generateRandomPayto("minst1")],
+ defaultWireTransferDelay: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ minutes: 1 }),
+ ),
+ });
+
+ const walletService = new WalletService(t, {
+ name: "wallet",
+ useInMemoryDb: true,
+ });
+ await walletService.start();
+ await walletService.pingUntilAvailable();
+
+ const allNotifications: WalletNotification[] = [];
+
+ const walletClient = new WalletClient({
+ name: "wallet",
+ unixPath: walletService.socketPath,
+ onNotification(n) {
+ console.log("got notification", n);
+ allNotifications.push(n);
+ },
+ });
+ await walletClient.connect();
+ await walletClient.client.call(WalletApiOperation.InitWallet, {
+ config: {
+ testing: {
+ skipDefaults: true,
+ },
+ },
+ });
+
+ const wres = await withdrawViaBankV2(t, {
+ amount: "TESTKUDOS:10",
+ bank,
+ exchange,
+ walletClient,
+ });
+ await wres.withdrawalFinishedCond;
+
+ const exc = await t.assertThrowsTalerErrorAsync(async () => {
+ await walletClient.call(WalletApiOperation.PrepareDeposit, {
+ amount: "TESTKUDOS:5" as AmountString,
+ depositPaytoUri: "payto://x-taler-bank/localhost/foobar",
+ });
+ });
+ t.assertDeepEqual(
+ exc.errorDetail.code,
+ TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE,
+ );
+ const insufficientBalanceDetails: PaymentInsufficientBalanceDetails =
+ exc.errorDetail.insufficientBalanceDetails;
+
+ t.assertAmountEquals(
+ insufficientBalanceDetails.balanceAvailable,
+ "TESTKUDOS:9.72",
+ );
+ t.assertAmountEquals(
+ insufficientBalanceDetails.balanceExchangeDepositable,
+ "TESTKUDOS:0",
+ );
+}
+
+runWalletInsufficientBalanceTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-notifications.ts b/packages/taler-harness/src/integrationtests/test-wallet-notifications.ts
new file mode 100644
index 000000000..28b73a9f9
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-wallet-notifications.ts
@@ -0,0 +1,176 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ TalerCorebankApiClient,
+ Duration,
+ NotificationType,
+ TransactionMajorState,
+} 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,
+ WalletClient,
+ WalletService,
+ generateRandomTestIban,
+ setupDb,
+} from "../harness/harness.js";
+
+/**
+ * Test for wallet-core notifications.
+ */
+export async function runWalletNotificationsTest(t: GlobalTestState) {
+ // Set up test environment
+
+ 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();
+
+ const coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS"));
+ exchange.addCoinConfigList(coinConfig);
+
+ await exchange.start();
+ await exchange.pingUntilAvailable();
+
+ merchant.addExchange(exchange);
+
+ await merchant.start();
+ await merchant.pingUntilAvailable();
+
+ // Fakebank uses x-taler-bank, but merchant is configured to only accept sepa!
+ const label = "mymerchant";
+ await merchant.addInstanceWithWireAccount({
+ id: "default",
+ name: "Default Instance",
+ paytoUris: [
+ `payto://iban/SANDBOXX/${generateRandomTestIban(label)}?receiver-name=${label}`,
+ ],
+ defaultWireTransferDelay: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ minutes: 1 }),
+ ),
+ });
+
+ console.log("setup done!");
+
+ const walletService = new WalletService(t, {
+ name: "wallet",
+ useInMemoryDb: true,
+ });
+ await walletService.start();
+ await walletService.pingUntilAvailable();
+
+ const walletClient = new WalletClient({
+ name: "wallet",
+ unixPath: walletService.socketPath,
+ onNotification(n) {
+ console.log("got notification", n);
+ },
+ });
+ await walletClient.connect();
+ await walletClient.client.call(WalletApiOperation.InitWallet, {
+ config: {
+ testing: {
+ skipDefaults: true,
+ }
+ }
+ });
+
+ const bankAccessApiClient = new TalerCorebankApiClient(
+ bank.corebankApiBaseUrl,
+ );
+ const user = await bankAccessApiClient.createRandomBankUser();
+ bankAccessApiClient.setAuth(user);
+ const wop = await bankAccessApiClient.createWithdrawalOperation(
+ user.username,
+ "TESTKUDOS:20",
+ );
+
+ // Hand it to the wallet
+
+ await walletClient.client.call(
+ WalletApiOperation.GetWithdrawalDetailsForUri,
+ {
+ talerWithdrawUri: wop.taler_withdraw_uri,
+ },
+ );
+
+ // Withdraw (AKA select)
+
+ const acceptRes = await walletClient.client.call(
+ WalletApiOperation.AcceptBankIntegratedWithdrawal,
+ {
+ exchangeBaseUrl: exchange.baseUrl,
+ talerWithdrawUri: wop.taler_withdraw_uri,
+ },
+ );
+
+ const withdrawalFinishedReceivedPromise =
+ walletClient.waitForNotificationCond((x) => {
+ return (
+ x.type === NotificationType.TransactionStateTransition &&
+ x.newTxState.major === TransactionMajorState.Done &&
+ x.transactionId === acceptRes.transactionId
+ );
+ });
+
+ // Confirm it
+
+ await bankAccessApiClient.confirmWithdrawalOperation(user.username, {
+ withdrawalOperationId: wop.withdrawal_id,
+ });
+
+ await withdrawalFinishedReceivedPromise;
+}
+
+runWalletNotificationsTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-observability.ts b/packages/taler-harness/src/integrationtests/test-wallet-observability.ts
new file mode 100644
index 000000000..5dff8670e
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-wallet-observability.ts
@@ -0,0 +1,119 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import { NotificationType, WalletNotification } from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js";
+import {
+ ExchangeService,
+ FakebankService,
+ GlobalTestState,
+ WalletClient,
+ WalletService,
+ setupDb,
+} from "../harness/harness.js";
+import { withdrawViaBankV2 } from "../harness/helpers.js";
+
+export async function runWalletObservabilityTest(t: GlobalTestState) {
+ // Set up test environment
+
+ 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 exchangeBankAccount = await bank.createExchangeAccount(
+ "myexchange",
+ "x",
+ );
+ exchange.addBankAccount("1", exchangeBankAccount);
+
+ bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri);
+
+ await bank.start();
+
+ await bank.pingUntilAvailable();
+
+ const coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS"));
+ exchange.addCoinConfigList(coinConfig);
+
+ await exchange.start();
+ await exchange.pingUntilAvailable();
+
+ const walletService = new WalletService(t, {
+ name: "wallet",
+ useInMemoryDb: true,
+ });
+ await walletService.start();
+ await walletService.pingUntilAvailable();
+
+ const allNotifications: WalletNotification[] = [];
+
+ const walletClient = new WalletClient({
+ name: "wallet",
+ unixPath: walletService.socketPath,
+ onNotification(n) {
+ console.log("got notification", n);
+ allNotifications.push(n);
+ },
+ });
+ await walletClient.connect();
+ await walletClient.client.call(WalletApiOperation.InitWallet, {
+ config: {
+ testing: {
+ skipDefaults: true,
+ emitObservabilityEvents: true,
+ },
+ },
+ });
+
+ const wres = await withdrawViaBankV2(t, {
+ amount: "TESTKUDOS:10",
+ bank,
+ exchange,
+ walletClient,
+ });
+ await wres.withdrawalFinishedCond;
+
+ const requestObsEvents = allNotifications.filter(
+ (x) => x.type === NotificationType.RequestObservabilityEvent,
+ );
+ const taskObsEvents = allNotifications.filter(
+ (x) => x.type === NotificationType.TaskObservabilityEvent,
+ );
+
+ console.log(`req events: ${requestObsEvents.length}`);
+ console.log(`task events: ${taskObsEvents.length}`);
+
+ t.assertTrue(requestObsEvents.length > 30);
+ t.assertTrue(taskObsEvents.length > 30);
+}
+
+runWalletObservabilityTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-refresh-errors.ts b/packages/taler-harness/src/integrationtests/test-wallet-refresh-errors.ts
new file mode 100644
index 000000000..0f1efd35e
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-wallet-refresh-errors.ts
@@ -0,0 +1,107 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import { AmountString } from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { CoinConfig } from "../harness/denomStructures.js";
+import { GlobalTestState } from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironmentV2,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
+
+const coinCommon = {
+ cipher: "RSA" as const,
+ durationLegal: "3 years",
+ durationSpend: "2 years",
+ durationWithdraw: "7 days",
+ feeDeposit: "TESTKUDOS:0",
+ feeRefresh: "TESTKUDOS:0",
+ feeRefund: "TESTKUDOS:0",
+ feeWithdraw: "TESTKUDOS:0",
+ rsaKeySize: 1024,
+};
+
+/**
+ * Run test for refreshe after a payment.
+ */
+export async function runWalletRefreshErrorsTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const coinConfigList: CoinConfig[] = [
+ {
+ ...coinCommon,
+ name: "n1",
+ value: "TESTKUDOS:1",
+ },
+ {
+ ...coinCommon,
+ name: "n5",
+ value: "TESTKUDOS:5",
+ },
+ ];
+
+ const { walletClient, bank, exchange, merchant } =
+ await createSimpleTestkudosEnvironmentV2(t, coinConfigList);
+
+ const wres = await withdrawViaBankV2(t, {
+ amount: "TESTKUDOS:5",
+ bank,
+ exchange,
+ walletClient,
+ });
+ await wres.withdrawalFinishedCond;
+
+ const backupResp = await walletClient.call(
+ WalletApiOperation.CreateStoredBackup,
+ {},
+ );
+
+ const coinDump = await walletClient.call(WalletApiOperation.DumpCoins, {});
+
+ t.assertDeepEqual(coinDump.coins.length, 1);
+
+ await walletClient.call(WalletApiOperation.ForceRefresh, {
+ refreshCoinSpecs: [
+ {
+ coinPub: coinDump.coins[0].coin_pub,
+ amount: "TESTKUDOS:3" as AmountString,
+ },
+ ],
+ });
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+
+ await walletClient.call(WalletApiOperation.RecoverStoredBackup, {
+ name: backupResp.name,
+ });
+
+ await walletClient.call(WalletApiOperation.ForceRefresh, {
+ refreshCoinSpecs: [
+ {
+ coinPub: coinDump.coins[0].coin_pub,
+ amount: "TESTKUDOS:3" as AmountString,
+ },
+ ],
+ });
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+}
+
+runWalletRefreshErrorsTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-refresh.ts b/packages/taler-harness/src/integrationtests/test-wallet-refresh.ts
new file mode 100644
index 000000000..f1c544a4e
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-wallet-refresh.ts
@@ -0,0 +1,200 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ AmountString,
+ NotificationType,
+ TransactionIdStr,
+ TransactionMajorState,
+ TransactionType,
+ j2s,
+} from "@gnu-taler/taler-util";
+import {
+ WalletApiOperation,
+ parseTransactionIdentifier,
+} from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState, generateRandomPayto } from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironmentV2,
+ makeTestPaymentV2,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
+
+/**
+ * Run test for refreshe after a payment.
+ */
+export async function runWalletRefreshTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { walletClient, bank, exchange, merchant } =
+ await createSimpleTestkudosEnvironmentV2(t);
+
+ // Withdraw digital cash into the wallet.
+
+ await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ });
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+
+ const order = {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "taler://fulfillment-success/thx",
+ };
+
+ await makeTestPaymentV2(t, { walletClient, merchant, order });
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+
+ const txns = await walletClient.call(WalletApiOperation.GetTransactions, {
+ includeRefreshes: true,
+ });
+
+ console.log(j2s(txns));
+
+ t.assertDeepEqual(txns.transactions.length, 3);
+
+ const refreshListTx = txns.transactions.find(
+ (x) => x.type === TransactionType.Refresh,
+ );
+
+ t.assertTrue(!!refreshListTx);
+
+ const refreshTx = await walletClient.call(
+ WalletApiOperation.GetTransactionById,
+ {
+ transactionId: refreshListTx.transactionId,
+ },
+ );
+
+ t.assertDeepEqual(refreshTx.type, TransactionType.Refresh);
+
+ // Now we test a pending refresh operation.
+ {
+ await exchange.stop();
+
+ const refreshCreatedCond = walletClient.waitForNotificationCond((x) => {
+ if (
+ x.type === NotificationType.TransactionStateTransition &&
+ parseTransactionIdentifier(x.transactionId)?.tag ===
+ TransactionType.Refresh
+ ) {
+ return true;
+ }
+ return false;
+ });
+
+ const refreshDoneCond = walletClient.waitForNotificationCond((x) => {
+ if (
+ x.type === NotificationType.TransactionStateTransition &&
+ parseTransactionIdentifier(x.transactionId)?.tag ===
+ TransactionType.Refresh &&
+ x.newTxState.major === TransactionMajorState.Done
+ ) {
+ return true;
+ }
+ return false;
+ });
+
+ const depositGroupResult = await walletClient.client.call(
+ WalletApiOperation.CreateDepositGroup,
+ {
+ amount: "TESTKUDOS:10.5" as AmountString,
+ depositPaytoUri: generateRandomPayto("foo"),
+ },
+ );
+
+ await refreshCreatedCond;
+
+ // Here, the refresh operation should be in a pending state.
+
+ const bal1 = await walletClient.call(WalletApiOperation.GetBalances, {});
+
+ await exchange.start();
+
+ await refreshDoneCond;
+
+ const bal2 = await walletClient.call(WalletApiOperation.GetBalances, {});
+
+ // The refresh operation completing should not change the available balance,
+ // as we're accounting pending refreshes towards the available (but not material!) balance.
+ t.assertAmountEquals(
+ bal1.balances[0].available,
+ bal2.balances[0].available,
+ );
+ }
+
+ const wres = await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ });
+
+ await wres.withdrawalFinishedCond;
+
+ // Test failing a refresh transaction
+ {
+ await exchange.stop();
+
+ let refreshTransactionId: TransactionIdStr | undefined = undefined;
+
+ const refreshCreatedCond = walletClient.waitForNotificationCond((x) => {
+ if (
+ x.type === NotificationType.TransactionStateTransition &&
+ parseTransactionIdentifier(x.transactionId)?.tag ===
+ TransactionType.Refresh
+ ) {
+ refreshTransactionId = x.transactionId as TransactionIdStr;
+ return true;
+ }
+ return false;
+ });
+
+ const depositGroupResult = await walletClient.client.call(
+ WalletApiOperation.CreateDepositGroup,
+ {
+ amount: "TESTKUDOS:10.5" as AmountString,
+ depositPaytoUri: generateRandomPayto("foo"),
+ },
+ );
+
+ await refreshCreatedCond;
+
+ t.assertTrue(!!refreshTransactionId);
+
+ await walletClient.call(WalletApiOperation.FailTransaction, {
+ transactionId: refreshTransactionId,
+ });
+
+ const txn = await walletClient.call(WalletApiOperation.GetTransactionById, {
+ transactionId: refreshTransactionId,
+ });
+
+ t.assertDeepEqual(txn.type, TransactionType.Refresh);
+ t.assertDeepEqual(txn.txState.major, TransactionMajorState.Failed);
+
+ t.assertTrue(!!refreshTransactionId);
+ }
+}
+
+runWalletRefreshTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-wirefees.ts b/packages/taler-harness/src/integrationtests/test-wallet-wirefees.ts
new file mode 100644
index 000000000..1bf9bd659
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-wallet-wirefees.ts
@@ -0,0 +1,184 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ Duration,
+ MerchantApiClient,
+ MerchantContractTerms,
+ PreparePayResultType,
+ TransactionMajorState,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js";
+import {
+ BankService,
+ ExchangeService,
+ GlobalTestState,
+ MerchantService,
+ generateRandomPayto,
+ setupDb,
+} from "../harness/harness.js";
+import {
+ createWalletDaemonWithClient,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
+
+/**
+ * Test payment where the exchange charges wire fees.
+ */
+export async function runWalletWirefeesTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const db = await setupDb(t);
+
+ const bank = await BankService.create(t, {
+ allowRegistrations: true,
+ currency: "TESTKUDOS",
+ database: db.connStr,
+ httpPort: 8082,
+ });
+
+ const exchange = ExchangeService.create(t, {
+ name: "testexchange-1",
+ currency: "TESTKUDOS",
+ httpPort: 8081,
+ database: db.connStr,
+ // Ridiculously high wire fees!
+ overrideWireFee: "TESTKUDOS:5",
+ });
+
+ const merchant = await MerchantService.create(t, {
+ name: "testmerchant-1",
+ currency: "TESTKUDOS",
+ httpPort: 8083,
+ database: db.connStr,
+ });
+
+ const exchangeBankAccount = await bank.createExchangeAccount(
+ "myexchange",
+ "x",
+ );
+ await exchange.addBankAccount("1", exchangeBankAccount);
+
+ bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri);
+ await bank.start();
+
+ await bank.pingUntilAvailable();
+
+ const coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS"));
+ 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")],
+ defaultWireTransferDelay: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ minutes: 1 }),
+ ),
+ });
+
+ await merchant.addInstanceWithWireAccount({
+ id: "minst1",
+ name: "minst1",
+ paytoUris: [generateRandomPayto("minst1")],
+ defaultWireTransferDelay: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ minutes: 1 }),
+ ),
+ });
+
+ const { walletClient, walletService } = await createWalletDaemonWithClient(
+ t,
+ { name: "wallet", persistent: true },
+ );
+
+ console.log("setup done!");
+
+ // Withdraw digital cash into the wallet.
+
+ await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ });
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+
+ const order = {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:1",
+ fulfillment_url: "taler://fulfillment-success/thx",
+ //max_wire_fee: "TESTKUDOS:0.1",
+ max_fee: "TESTKUDOS:0.1",
+ } satisfies Partial<MerchantContractTerms>;
+
+ const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl());
+
+ const orderResp = await merchantClient.createOrder({
+ order,
+ });
+
+ let orderStatus = await merchantClient.queryPrivateOrderStatus({
+ orderId: orderResp.order_id,
+ });
+
+ t.assertTrue(orderStatus.order_status === "unpaid");
+
+ // Make wallet pay for the order
+
+ const preparePayResult = await walletClient.call(
+ WalletApiOperation.PreparePayForUri,
+ {
+ talerPayUri: orderStatus.taler_pay_uri,
+ },
+ );
+
+ t.assertTrue(
+ preparePayResult.status === PreparePayResultType.PaymentPossible,
+ );
+
+ console.log(`amountEffective: ${preparePayResult.amountEffective}`);
+
+ t.assertAmountEquals(preparePayResult.amountEffective, "TESTKUDOS:6.4");
+
+ await walletClient.call(WalletApiOperation.ConfirmPay, {
+ transactionId: preparePayResult.transactionId,
+ });
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+
+ const payTxn = await walletClient.call(
+ WalletApiOperation.GetTransactionById,
+ {
+ transactionId: preparePayResult.transactionId,
+ },
+ );
+
+ t.assertTrue(payTxn.txState.major === TransactionMajorState.Done);
+}
+
+runWalletWirefeesTest.suites = ["wallet"];
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-wallettesting.ts b/packages/taler-harness/src/integrationtests/test-wallettesting.ts
index 03c446db3..932284d62 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-wallettesting.ts
+++ b/packages/taler-harness/src/integrationtests/test-wallettesting.ts
@@ -22,7 +22,7 @@
/**
* Imports.
*/
-import { Amounts, CoinStatus } from "@gnu-taler/taler-util";
+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 {
@@ -31,10 +31,12 @@ import {
GlobalTestState,
MerchantService,
setupDb,
- WalletCli,
- getPayto,
+ generateRandomPayto,
} from "../harness/harness.js";
-import { SimpleTestEnvironment } from "../harness/helpers.js";
+import {
+ SimpleTestEnvironmentNg,
+ createWalletDaemonWithClient,
+} from "../harness/helpers.js";
const merchantAuthToken = "secret-token:sandbox";
@@ -45,7 +47,7 @@ const merchantAuthToken = "secret-token:sandbox";
export async function createMyEnvironment(
t: GlobalTestState,
coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")),
-): Promise<SimpleTestEnvironment> {
+): Promise<SimpleTestEnvironmentNg> {
const db = await setupDb(t);
const bank = await BankService.create(t, {
@@ -91,21 +93,27 @@ export async function createMyEnvironment(
await merchant.start();
await merchant.pingUntilAvailable();
- await merchant.addInstance({
+ await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [getPayto("merchant-default")],
+ paytoUris: [generateRandomPayto("merchant-default")],
});
console.log("setup done!");
- const wallet = new WalletCli(t);
+ const { walletClient, walletService } = await createWalletDaemonWithClient(
+ t,
+ {
+ name: "w1",
+ },
+ );
return {
commonDb: db,
exchange,
merchant,
- wallet,
+ walletClient,
+ walletService,
bank,
exchangeBankAccount,
};
@@ -115,19 +123,19 @@ export async function createMyEnvironment(
* Run test for basic, bank-integrated withdrawal.
*/
export async function runWallettestingTest(t: GlobalTestState) {
- const { wallet, bank, exchange, merchant } = await createMyEnvironment(t);
+ const { walletClient, bank, exchange, merchant } =
+ await createMyEnvironment(t);
- await wallet.client.call(WalletApiOperation.RunIntegrationTest, {
- amountToSpend: "TESTKUDOS:5",
- amountToWithdraw: "TESTKUDOS:10",
- bankBaseUrl: bank.baseUrl,
- bankAccessApiBaseUrl: bank.bankAccessApiBaseUrl,
+ await walletClient.call(WalletApiOperation.RunIntegrationTest, {
+ amountToSpend: "TESTKUDOS:5" as AmountString,
+ amountToWithdraw: "TESTKUDOS:10" as AmountString,
+ corebankApiBaseUrl: bank.corebankApiBaseUrl,
exchangeBaseUrl: exchange.baseUrl,
merchantAuthToken: merchantAuthToken,
merchantBaseUrl: merchant.makeInstanceBaseUrl(),
});
- let txns = await wallet.client.call(WalletApiOperation.GetTransactions, {});
+ let txns = await walletClient.call(WalletApiOperation.GetTransactions, {});
console.log(JSON.stringify(txns, undefined, 2));
let txTypes = txns.transactions.map((x) => x.type);
@@ -140,44 +148,42 @@ export async function runWallettestingTest(t: GlobalTestState) {
"payment",
]);
- wallet.deleteDatabase();
+ await walletClient.call(WalletApiOperation.ClearDb, {});
- await wallet.client.call(WalletApiOperation.WithdrawTestBalance, {
- amount: "TESTKUDOS:10",
- bankBaseUrl: bank.baseUrl,
- bankAccessApiBaseUrl: bank.bankAccessApiBaseUrl,
+ await walletClient.call(WalletApiOperation.WithdrawTestBalance, {
+ amount: "TESTKUDOS:10" as AmountString,
+ corebankApiBaseUrl: bank.corebankApiBaseUrl,
exchangeBaseUrl: exchange.baseUrl,
});
- await wallet.runUntilDone();
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
- await wallet.client.call(WalletApiOperation.TestPay, {
- amount: "TESTKUDOS:5",
+ await walletClient.call(WalletApiOperation.TestPay, {
+ amount: "TESTKUDOS:5" as AmountString,
merchantAuthToken: merchantAuthToken,
merchantBaseUrl: merchant.makeInstanceBaseUrl(),
summary: "foo",
});
- await wallet.runUntilDone();
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
- txns = await wallet.client.call(WalletApiOperation.GetTransactions, {});
+ txns = await walletClient.call(WalletApiOperation.GetTransactions, {});
console.log(JSON.stringify(txns, undefined, 2));
txTypes = txns.transactions.map((x) => x.type);
t.assertDeepEqual(txTypes, ["withdrawal", "payment"]);
- wallet.deleteDatabase();
+ await walletClient.call(WalletApiOperation.ClearDb, {});
- await wallet.client.call(WalletApiOperation.WithdrawTestBalance, {
- amount: "TESTKUDOS:10",
- bankBaseUrl: bank.baseUrl,
- bankAccessApiBaseUrl: bank.bankAccessApiBaseUrl,
+ await walletClient.call(WalletApiOperation.WithdrawTestBalance, {
+ amount: "TESTKUDOS:10" as AmountString,
+ corebankApiBaseUrl: bank.corebankApiBaseUrl,
exchangeBaseUrl: exchange.baseUrl,
});
- await wallet.runUntilDone();
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
- const coinDump = await wallet.client.call(WalletApiOperation.DumpCoins, {});
+ const coinDump = await walletClient.call(WalletApiOperation.DumpCoins, {});
console.log("coin dump:", JSON.stringify(coinDump, undefined, 2));
@@ -197,7 +203,7 @@ export async function runWallettestingTest(t: GlobalTestState) {
console.log("suspending coin");
- await wallet.client.call(WalletApiOperation.SetCoinSuspended, {
+ await walletClient.call(WalletApiOperation.SetCoinSuspended, {
coinPub: susp,
suspended: true,
});
@@ -205,8 +211,8 @@ export async function runWallettestingTest(t: GlobalTestState) {
// This should fail, as we've suspended a coin that we need
// to pay.
await t.assertThrowsAsync(async () => {
- await wallet.client.call(WalletApiOperation.TestPay, {
- amount: "TESTKUDOS:5",
+ await walletClient.call(WalletApiOperation.TestPay, {
+ amount: "TESTKUDOS:5" as AmountString,
merchantAuthToken: merchantAuthToken,
merchantBaseUrl: merchant.makeInstanceBaseUrl(),
summary: "foo",
@@ -215,19 +221,27 @@ export async function runWallettestingTest(t: GlobalTestState) {
console.log("unsuspending coin");
- await wallet.client.call(WalletApiOperation.SetCoinSuspended, {
+ await walletClient.call(WalletApiOperation.SetCoinSuspended, {
coinPub: susp,
suspended: false,
});
- await wallet.client.call(WalletApiOperation.TestPay, {
- amount: "TESTKUDOS:5",
+ await walletClient.call(WalletApiOperation.TestPay, {
+ amount: "TESTKUDOS:5" as AmountString,
merchantAuthToken: merchantAuthToken,
merchantBaseUrl: merchant.makeInstanceBaseUrl(),
summary: "foo",
});
- await t.shutdown();
+ await walletClient.call(WalletApiOperation.ClearDb, {});
+ await walletClient.call(WalletApiOperation.RunIntegrationTestV2, {
+ amountToSpend: "TESTKUDOS:5" as AmountString,
+ amountToWithdraw: "TESTKUDOS:10" as AmountString,
+ corebankApiBaseUrl: bank.corebankApiBaseUrl,
+ exchangeBaseUrl: exchange.baseUrl,
+ merchantAuthToken: merchantAuthToken,
+ merchantBaseUrl: merchant.makeInstanceBaseUrl(),
+ });
}
runWallettestingTest.suites = ["wallet"];
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-abort-bank.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-abort-bank.ts
index bf2dc0133..39389e3c6 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-abort-bank.ts
+++ b/packages/taler-harness/src/integrationtests/test-withdrawal-abort-bank.ts
@@ -17,14 +17,10 @@
/**
* Imports.
*/
-import { TalerErrorCode } from "@gnu-taler/taler-util";
-import {
- WalletApiOperation,
- BankApi,
- BankAccessApi,
-} from "@gnu-taler/taler-wallet-core";
+import { TalerCorebankApiClient, TalerErrorCode } from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { GlobalTestState } from "../harness/harness.js";
-import { createSimpleTestkudosEnvironment } from "../harness/helpers.js";
+import { createSimpleTestkudosEnvironmentV2 } from "../harness/helpers.js";
/**
* Run test for basic, bank-integrated withdrawal.
@@ -32,29 +28,32 @@ import { createSimpleTestkudosEnvironment } from "../harness/helpers.js";
export async function runWithdrawalAbortBankTest(t: GlobalTestState) {
// Set up test environment
- const { wallet, bank, exchange } = await createSimpleTestkudosEnvironment(t);
+ const { walletClient, bank, exchange } =
+ await createSimpleTestkudosEnvironmentV2(t);
// Create a withdrawal operation
- const user = await BankApi.createRandomBankUser(bank);
- const wop = await BankAccessApi.createWithdrawalOperation(
- bank,
- user,
+ const bankAccessApiClient = new TalerCorebankApiClient(
+ bank.corebankApiBaseUrl,
+ );
+ const user = await bankAccessApiClient.createRandomBankUser();
+ bankAccessApiClient.setAuth(user);
+ const wop = await bankAccessApiClient.createWithdrawalOperation(
+ user.username,
"TESTKUDOS:10",
);
// Hand it to the wallet
- await wallet.client.call(WalletApiOperation.GetWithdrawalDetailsForUri, {
+ await walletClient.call(WalletApiOperation.GetWithdrawalDetailsForUri, {
talerWithdrawUri: wop.taler_withdraw_uri,
});
- await wallet.runPending();
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
// Abort it
- await BankApi.abortWithdrawalOperation(bank, user, wop);
- //await BankApi.confirmWithdrawalOperation(bank, user, wop);
+ await bankAccessApiClient.abortWithdrawalOperation(wop);
// Withdraw
@@ -65,13 +64,10 @@ export async function runWithdrawalAbortBankTest(t: GlobalTestState) {
// WHY ?!
//
const e = await t.assertThrowsTalerErrorAsync(async () => {
- await wallet.client.call(
- WalletApiOperation.AcceptBankIntegratedWithdrawal,
- {
- exchangeBaseUrl: exchange.baseUrl,
- talerWithdrawUri: wop.taler_withdraw_uri,
- },
- );
+ await walletClient.call(WalletApiOperation.AcceptBankIntegratedWithdrawal, {
+ exchangeBaseUrl: exchange.baseUrl,
+ talerWithdrawUri: wop.taler_withdraw_uri,
+ });
});
t.assertDeepEqual(
e.errorDetail.code,
diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-bank-integrated.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-bank-integrated.ts
new file mode 100644
index 000000000..76dec50d3
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-withdrawal-bank-integrated.ts
@@ -0,0 +1,192 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ TalerCorebankApiClient,
+ j2s,
+ NotificationType,
+ TransactionMajorState,
+ TransactionMinorState,
+ TransactionType,
+ WithdrawalType,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState } from "../harness/harness.js";
+import { createSimpleTestkudosEnvironmentV2 } from "../harness/helpers.js";
+
+/**
+ * Run test for basic, bank-integrated withdrawal.
+ */
+export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { walletClient, bank, exchange } =
+ await createSimpleTestkudosEnvironmentV2(t);
+
+ // Create a withdrawal operation
+
+ const corebankApiClient = new TalerCorebankApiClient(
+ bank.corebankApiBaseUrl,
+ );
+ const user = await corebankApiClient.createRandomBankUser();
+ corebankApiClient.setAuth(user);
+ const wop = await corebankApiClient.createWithdrawalOperation(
+ user.username,
+ "TESTKUDOS:10",
+ );
+
+ // Hand it to the wallet
+
+ const r1 = await walletClient.client.call(
+ WalletApiOperation.GetWithdrawalDetailsForUri,
+ {
+ talerWithdrawUri: wop.taler_withdraw_uri,
+ },
+ );
+
+ // Withdraw
+
+ const r2 = await walletClient.client.call(
+ WalletApiOperation.AcceptBankIntegratedWithdrawal,
+ {
+ exchangeBaseUrl: exchange.baseUrl,
+ talerWithdrawUri: wop.taler_withdraw_uri,
+ },
+ );
+
+ const withdrawalBankConfirmedCond = walletClient.waitForNotificationCond(
+ (x) => {
+ return (
+ x.type === NotificationType.TransactionStateTransition &&
+ x.transactionId === r2.transactionId &&
+ x.newTxState.major === TransactionMajorState.Pending &&
+ x.newTxState.minor === TransactionMinorState.ExchangeWaitReserve
+ );
+ },
+ );
+
+ const withdrawalFinishedCond = walletClient.waitForNotificationCond((x) => {
+ return (
+ x.type === NotificationType.TransactionStateTransition &&
+ x.transactionId === r2.transactionId &&
+ x.newTxState.major === TransactionMajorState.Done
+ );
+ });
+
+ const withdrawalReserveReadyCond = walletClient.waitForNotificationCond(
+ (x) => {
+ return (
+ x.type === NotificationType.TransactionStateTransition &&
+ x.transactionId === r2.transactionId &&
+ x.newTxState.major === TransactionMajorState.Pending &&
+ x.newTxState.minor === TransactionMinorState.WithdrawCoins
+ );
+ },
+ );
+
+ // Do it twice to check idempotency
+ const r3 = await walletClient.client.call(
+ WalletApiOperation.AcceptBankIntegratedWithdrawal,
+ {
+ exchangeBaseUrl: exchange.baseUrl,
+ talerWithdrawUri: wop.taler_withdraw_uri,
+ },
+ );
+
+ await exchange.stopWirewatch();
+
+ // Check status before withdrawal is confirmed by bank.
+ {
+ const txn = await walletClient.client.call(
+ WalletApiOperation.GetTransactions,
+ {},
+ );
+ console.log("transactions before confirmation:", j2s(txn));
+ const tx0 = txn.transactions[0];
+ t.assertTrue(tx0.type === TransactionType.Withdrawal);
+ t.assertTrue(
+ tx0.withdrawalDetails.type === WithdrawalType.TalerBankIntegrationApi,
+ );
+ t.assertTrue(tx0.withdrawalDetails.confirmed === false);
+ t.assertTrue(tx0.withdrawalDetails.reserveIsReady === false);
+ }
+
+ // Confirm it
+
+ await corebankApiClient.confirmWithdrawalOperation(user.username, {
+ withdrawalOperationId: wop.withdrawal_id,
+ });
+
+ await withdrawalBankConfirmedCond;
+
+ // Check status after withdrawal is confirmed by bank,
+ // but before funds are wired to the exchange.
+ {
+ const txn = await walletClient.client.call(
+ WalletApiOperation.GetTransactions,
+ {},
+ );
+ console.log("transactions after confirmation:", j2s(txn));
+ const tx0 = txn.transactions[0];
+ t.assertTrue(tx0.type === TransactionType.Withdrawal);
+ t.assertTrue(
+ tx0.withdrawalDetails.type === WithdrawalType.TalerBankIntegrationApi,
+ );
+ t.assertTrue(tx0.withdrawalDetails.confirmed === true);
+ t.assertTrue(tx0.withdrawalDetails.reserveIsReady === false);
+ }
+
+ await exchange.startWirewatch();
+
+ await withdrawalReserveReadyCond;
+
+ // Check status after funds were wired.
+ {
+ const txn = await walletClient.client.call(
+ WalletApiOperation.GetTransactions,
+ {},
+ );
+ console.log("transactions after reserve ready:", j2s(txn));
+ const tx0 = txn.transactions[0];
+ t.assertTrue(tx0.type === TransactionType.Withdrawal);
+ t.assertTrue(
+ tx0.withdrawalDetails.type === WithdrawalType.TalerBankIntegrationApi,
+ );
+ t.assertTrue(tx0.withdrawalDetails.confirmed === true);
+ t.assertTrue(tx0.withdrawalDetails.reserveIsReady === true);
+ }
+
+ await withdrawalFinishedCond;
+
+ // Check balance
+
+ const balResp = await walletClient.client.call(
+ WalletApiOperation.GetBalances,
+ {},
+ );
+ t.assertAmountEquals("TESTKUDOS:9.72", balResp.balances[0].available);
+
+ const txn = await walletClient.client.call(
+ WalletApiOperation.GetTransactions,
+ {},
+ );
+ console.log(`transactions: ${j2s(txn)}`);
+}
+
+runWithdrawalBankIntegratedTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-conversion.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-conversion.ts
new file mode 100644
index 000000000..8351e5251
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-withdrawal-conversion.ts
@@ -0,0 +1,304 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ AbsoluteTime,
+ AmountString,
+ Amounts,
+ Duration,
+ Logger,
+ TalerBankConversionApi,
+ TalerCorebankApiClient,
+ TransactionType,
+ WireGatewayApiClient,
+ WithdrawalType,
+ j2s,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import * as http from "node:http";
+import { defaultCoinConfig } from "../harness/denomStructures.js";
+import {
+ BankService,
+ ExchangeService,
+ GlobalTestState,
+ MerchantService,
+ generateRandomPayto,
+ setupDb,
+} from "../harness/harness.js";
+import { createWalletDaemonWithClient } from "../harness/helpers.js";
+
+const logger = new Logger("test-withdrawal-conversion.ts");
+
+interface TestfakeConversionService {
+ stop: () => void;
+}
+
+function splitInTwoAt(s: string, separator: string): [string, string] {
+ const idx = s.indexOf(separator);
+ if (idx === -1) {
+ return [s, ""];
+ }
+ return [s.slice(0, idx), s.slice(idx + 1)];
+}
+
+/**
+ * Testfake for the kyc service that the exchange talks to.
+ */
+async function runTestfakeConversionService(): Promise<TestfakeConversionService> {
+ const server = http.createServer((req, res) => {
+ const requestUrl = req.url!;
+ logger.info(`kyc: got ${req.method} request, ${requestUrl}`);
+
+ const [path, query] = splitInTwoAt(requestUrl, "?");
+
+ const qp = new URLSearchParams(query);
+
+ if (path === "/config") {
+ res.writeHead(200, { "Content-Type": "application/json" });
+ res.end(
+ JSON.stringify({
+ version: "0:0:0",
+ name: "taler-conversion-info",
+ regional_currency: "FOO",
+ fiat_currency: "BAR",
+ regional_currency_specification: {
+ alt_unit_names: {},
+ name: "FOO",
+ num_fractional_input_digits: 2,
+ num_fractional_normal_digits: 2,
+ num_fractional_trailing_zero_digits: 2,
+ },
+ fiat_currency_specification: {
+ alt_unit_names: {},
+ name: "BAR",
+ num_fractional_input_digits: 2,
+ num_fractional_normal_digits: 2,
+ num_fractional_trailing_zero_digits: 2,
+ },
+ conversion_rate: {
+ cashin_fee: "A:1" as AmountString,
+ cashin_min_amount: "A:0.1" as AmountString,
+ cashin_ratio: "1",
+ cashin_rounding_mode: "zero",
+ cashin_tiny_amount: "A:1" as AmountString,
+ cashout_fee: "A:1" as AmountString,
+ cashout_min_amount: "A:0.1" as AmountString,
+ cashout_ratio: "1",
+ cashout_rounding_mode: "zero",
+ cashout_tiny_amount: "A:1" as AmountString,
+ }
+ } satisfies TalerBankConversionApi.IntegrationConfig),
+ );
+ } else if (path === "/cashin-rate") {
+ res.writeHead(200, { "Content-Type": "application/json" });
+ res.end(
+ JSON.stringify({
+ amount_debit: "FOO:123",
+ amount_credit: "BAR:123",
+ }),
+ );
+ } else {
+ res.writeHead(400, { "Content-Type": "application/json" });
+ res.end(JSON.stringify({ code: 1, message: "bad request" }));
+ }
+ });
+ await new Promise<void>((resolve, reject) => {
+ server.listen(8071, () => resolve());
+ });
+ return {
+ stop() {
+ server.close();
+ },
+ };
+}
+
+/**
+ * Test for currency conversion during manual withdrawal.
+ */
+export async function runWithdrawalConversionTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const db = await setupDb(t);
+
+ const bank = await BankService.create(t, {
+ allowRegistrations: true,
+ currency: "TESTKUDOS",
+ database: db.connStr,
+ httpPort: 8082,
+ });
+
+ const exchange = ExchangeService.create(t, {
+ name: "testexchange-1",
+ currency: "TESTKUDOS",
+ httpPort: 8081,
+ database: db.connStr,
+ });
+
+ const merchant = await MerchantService.create(t, {
+ name: "testmerchant-1",
+ currency: "TESTKUDOS",
+ httpPort: 8083,
+ database: db.connStr,
+ });
+
+ const exchangeBankAccount = await bank.createExchangeAccount(
+ "myexchange",
+ "x",
+ );
+ exchangeBankAccount.conversionUrl = "http://localhost:8071/";
+ await exchange.addBankAccount("1", exchangeBankAccount);
+
+ await bank.start();
+
+ await bank.pingUntilAvailable();
+
+ exchange.addOfferedCoins(defaultCoinConfig);
+
+ 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")],
+ defaultWireTransferDelay: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ minutes: 1 }),
+ ),
+ });
+
+ await merchant.addInstanceWithWireAccount({
+ id: "minst1",
+ name: "minst1",
+ paytoUris: [generateRandomPayto("minst1")],
+ defaultWireTransferDelay: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ minutes: 1 }),
+ ),
+ });
+
+ const { walletClient, walletService } = await createWalletDaemonWithClient(
+ t,
+ { name: "wallet" },
+ );
+
+ await runTestfakeConversionService();
+
+ // Create a withdrawal operation
+
+ const bankAccessApiClient = new TalerCorebankApiClient(
+ bank.corebankApiBaseUrl,
+ );
+
+ const user = await bankAccessApiClient.createRandomBankUser();
+
+ await walletClient.call(WalletApiOperation.AddExchange, {
+ exchangeBaseUrl: exchange.baseUrl,
+ });
+
+ const infoRes = await walletClient.call(
+ WalletApiOperation.GetWithdrawalDetailsForAmount,
+ {
+ exchangeBaseUrl: exchange.baseUrl,
+ amount: "TESTKUDOS:20" as AmountString,
+ },
+ );
+
+ console.log(`withdrawal details: ${j2s(infoRes)}`);
+
+ const checkTransferAmount = infoRes.withdrawalAccountsList[0].transferAmount;
+ t.assertTrue(checkTransferAmount != null);
+ t.assertAmountEquals(checkTransferAmount, "FOO:123");
+
+ const tStart = AbsoluteTime.now();
+
+ logger.info("starting AcceptManualWithdrawal request");
+ // We expect this to return immediately.
+
+ const wres = await walletClient.call(
+ WalletApiOperation.AcceptManualWithdrawal,
+ {
+ exchangeBaseUrl: exchange.baseUrl,
+ amount: "TESTKUDOS:10" as AmountString,
+ },
+ );
+
+ logger.info("AcceptManualWithdrawal finished");
+ logger.info(`result: ${j2s(wres)}`);
+
+ const acceptedTransferAmount = wres.withdrawalAccountsList[0].transferAmount;
+ t.assertTrue(acceptedTransferAmount != null);
+
+ t.assertAmountEquals(acceptedTransferAmount, "FOO:123");
+
+ const txInfo = await walletClient.call(
+ WalletApiOperation.GetTransactionById,
+ {
+ transactionId: wres.transactionId,
+ },
+ );
+
+ t.assertDeepEqual(txInfo.type, TransactionType.Withdrawal);
+ t.assertDeepEqual(
+ txInfo.withdrawalDetails.type,
+ WithdrawalType.ManualTransfer,
+ );
+ t.assertTrue(!!txInfo.withdrawalDetails.exchangeCreditAccountDetails);
+ t.assertDeepEqual(
+ txInfo.withdrawalDetails.exchangeCreditAccountDetails[0].transferAmount,
+ "FOO:123",
+ );
+
+ // Check that the request did not go into long-polling.
+ const duration = AbsoluteTime.difference(tStart, AbsoluteTime.now());
+ if (typeof duration.d_ms !== "number" || duration.d_ms > 5 * 1000) {
+ throw Error("withdrawal took too long (longpolling issue)");
+ }
+
+ const reservePub: string = wres.reservePub;
+
+ const wireGatewayApiClient = new WireGatewayApiClient(
+ exchangeBankAccount.wireGatewayApiBaseUrl,
+ {
+ auth: {
+ username: exchangeBankAccount.accountName,
+ password: exchangeBankAccount.accountPassword,
+ },
+ },
+ );
+
+ await wireGatewayApiClient.adminAddIncoming({
+ amount: "TESTKUDOS:10",
+ debitAccountPayto: user.accountPaytoUri,
+ reservePub: reservePub,
+ });
+
+ await exchange.runWirewatchOnce();
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+
+ // Check balance
+
+ const balResp = await walletClient.call(WalletApiOperation.GetBalances, {});
+ t.assertAmountEquals("TESTKUDOS:9.72", balResp.balances[0].available);
+}
+
+runWithdrawalConversionTest.suites = ["wallet"];
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-fakebank.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-fakebank.ts
index ec6e54e6c..1dc955649 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-fakebank.ts
+++ b/packages/taler-harness/src/integrationtests/test-withdrawal-fakebank.ts
@@ -17,16 +17,16 @@
/**
* Imports.
*/
+import { AmountString, URL } from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js";
import {
+ ExchangeService,
+ FakebankService,
GlobalTestState,
WalletCli,
setupDb,
- ExchangeService,
- FakebankService,
} from "../harness/harness.js";
-import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js";
-import { URL } from "@gnu-taler/taler-util";
/**
* Run test for basic, bank-integrated withdrawal.
@@ -54,10 +54,16 @@ export async function runWithdrawalFakebankTest(t: GlobalTestState) {
exchange.addBankAccount("1", {
accountName: "exchange",
accountPassword: "x",
- wireGatewayApiBaseUrl: new URL("/exchange/", bank.baseUrl).href,
- accountPaytoUri: "payto://x-taler-bank/localhost/exchange",
+ wireGatewayApiBaseUrl: new URL(
+ "/accounts/exchange/taler-wire-gateway/",
+ bank.baseUrl,
+ ).href,
+ accountPaytoUri:
+ "payto://x-taler-bank/localhost/exchange?receiver-name=Exchange",
});
+ await bank.createExchangeAccount("exchange", "x");
+
await bank.start();
await bank.pingUntilAvailable();
@@ -76,10 +82,10 @@ export async function runWithdrawalFakebankTest(t: GlobalTestState) {
exchangeBaseUrl: exchange.baseUrl,
});
- await wallet.client.call(WalletApiOperation.WithdrawFakebank, {
- exchange: exchange.baseUrl,
- amount: "TESTKUDOS:10",
- bank: bank.baseUrl,
+ await wallet.client.call(WalletApiOperation.WithdrawTestBalance, {
+ corebankApiBaseUrl: bank.corebankApiBaseUrl,
+ exchangeBaseUrl: exchange.baseUrl,
+ amount: "TESTKUDOS:10" as AmountString,
});
await exchange.runWirewatchOnce();
@@ -90,8 +96,6 @@ export async function runWithdrawalFakebankTest(t: GlobalTestState) {
const balResp = await wallet.client.call(WalletApiOperation.GetBalances, {});
t.assertAmountEquals("TESTKUDOS:9.72", balResp.balances[0].available);
-
- await t.shutdown();
}
runWithdrawalFakebankTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-fees.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-fees.ts
new file mode 100644
index 000000000..f702376e1
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-withdrawal-fees.ts
@@ -0,0 +1,171 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import { TalerCorebankApiClient, j2s } from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { CoinConfig } from "../harness/denomStructures.js";
+import {
+ BankService,
+ ExchangeService,
+ GlobalTestState,
+ WalletCli,
+ setupDb,
+} from "../harness/harness.js";
+
+const coinRsaCommon = {
+ cipher: "RSA" as const,
+ durationLegal: "3 years",
+ durationSpend: "2 years",
+ durationWithdraw: "7 days",
+ rsaKeySize: 1024,
+};
+
+const coin_u1 = (curr: string): CoinConfig => ({
+ ...coinRsaCommon,
+ name: `${curr}_u1`,
+ value: `${curr}:1`,
+ feeDeposit: `${curr}:0`,
+ feeRefresh: `${curr}:0`,
+ feeRefund: `${curr}:0`,
+ feeWithdraw: `${curr}:1`,
+});
+
+const coin_u5 = (curr: string): CoinConfig => ({
+ ...coinRsaCommon,
+ name: `${curr}_u5`,
+ value: `${curr}:5`,
+ feeDeposit: `${curr}:0`,
+ feeRefresh: `${curr}:0`,
+ feeRefund: `${curr}:0`,
+ feeWithdraw: `${curr}:1`,
+});
+
+export const weirdCoinConfig = [coin_u1, coin_u5];
+
+/**
+ * Test withdrawal with a weird denomination structure to
+ * make sure fees are computed as expected.
+ */
+export async function runWithdrawalFeesTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const db = await setupDb(t);
+
+ const bank = await BankService.create(t, {
+ allowRegistrations: true,
+ currency: "TESTKUDOS",
+ database: db.connStr,
+ httpPort: 8082,
+ });
+
+ const exchange = ExchangeService.create(t, {
+ name: "testexchange-1",
+ currency: "TESTKUDOS",
+ httpPort: 8081,
+ database: db.connStr,
+ });
+
+ const exchangeBankAccount = await bank.createExchangeAccount(
+ "myexchange",
+ "x",
+ );
+ await exchange.addBankAccount("1", exchangeBankAccount);
+
+ await bank.start();
+
+ await bank.pingUntilAvailable();
+
+ const coinConfig: CoinConfig[] = weirdCoinConfig.map((x) => x("TESTKUDOS"));
+ exchange.addCoinConfigList(coinConfig);
+
+ await exchange.start();
+ await exchange.pingUntilAvailable();
+
+ console.log("setup done!");
+
+ const wallet = new WalletCli(t);
+
+ await wallet.client.call(WalletApiOperation.AddExchange, {
+ exchangeBaseUrl: exchange.baseUrl,
+ });
+
+ const amount = "TESTKUDOS:7.5";
+
+ const bankAccessApiClient = new TalerCorebankApiClient(
+ bank.corebankApiBaseUrl,
+ );
+ const user = await bankAccessApiClient.createRandomBankUser();
+ bankAccessApiClient.setAuth(user);
+ const wop = await bankAccessApiClient.createWithdrawalOperation(
+ user.username,
+ amount,
+ );
+
+ // Hand it to the wallet
+
+ const details = await wallet.client.call(
+ WalletApiOperation.GetWithdrawalDetailsForUri,
+ {
+ talerWithdrawUri: wop.taler_withdraw_uri,
+ },
+ );
+
+ console.log(j2s(details));
+
+ const amountDetails = await wallet.client.call(
+ WalletApiOperation.GetWithdrawalDetailsForAmount,
+ {
+ amount: details.amount,
+ exchangeBaseUrl: details.possibleExchanges[0].exchangeBaseUrl,
+ },
+ );
+
+ console.log(j2s(amountDetails));
+
+ t.assertAmountEquals(amountDetails.amountEffective, "TESTKUDOS:5");
+ t.assertAmountEquals(amountDetails.amountRaw, "TESTKUDOS:7.5");
+
+ await wallet.runPending();
+
+ // Withdraw (AKA select)
+
+ await wallet.client.call(WalletApiOperation.AcceptBankIntegratedWithdrawal, {
+ exchangeBaseUrl: exchange.baseUrl,
+ talerWithdrawUri: wop.taler_withdraw_uri,
+ });
+
+ // Confirm it
+
+ await bankAccessApiClient.confirmWithdrawalOperation(user.username, {
+ withdrawalOperationId: wop.withdrawal_id,
+ });
+ await wallet.runUntilDone();
+
+ // Check balance
+
+ const balResp = await wallet.client.call(WalletApiOperation.GetBalances, {});
+ console.log(j2s(balResp));
+
+ t.assertAmountEquals(balResp.balances[0].available, "TESTKUDOS:5");
+
+ const txns = await wallet.client.call(WalletApiOperation.GetTransactions, {});
+ console.log(j2s(txns));
+}
+
+runWithdrawalFeesTest.suites = ["wallet"];
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-high.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-huge.ts
index deb0e6dde..b483b8706 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-high.ts
+++ b/packages/taler-harness/src/integrationtests/test-withdrawal-huge.ts
@@ -19,20 +19,28 @@
*/
import {
GlobalTestState,
- WalletCli,
setupDb,
ExchangeService,
FakebankService,
+ WalletService,
+ WalletClient,
} from "../harness/harness.js";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js";
-import { URL } from "@gnu-taler/taler-util";
+import {
+ AmountString,
+ NotificationType,
+ TransactionMajorState,
+ URL,
+} from "@gnu-taler/taler-util";
/**
- * Withdraw a high amount. Mostly intended
- * as a perf test.
+ * Withdraw a high amount. Mostly intended as a perf test.
+ *
+ * It is useful to see whether the wallet stays responsive while doing a huge withdrawal.
+ * (This is not automatic yet. Use taler-wallet-cli to connect to the daemon and make requests to check.)
*/
-export async function runWithdrawalHighTest(t: GlobalTestState) {
+export async function runWithdrawalHugeTest(t: GlobalTestState) {
// Set up test environment
const db = await setupDb(t);
@@ -71,29 +79,42 @@ export async function runWithdrawalHighTest(t: GlobalTestState) {
console.log("setup done!");
- const wallet = new WalletCli(t);
+ const walletService = new WalletService(t, { name: "w1" });
+ await walletService.start();
+ await walletService.pingUntilAvailable();
+
+ const wallet = new WalletClient({
+ name: "wallet",
+ unixPath: walletService.socketPath,
+ });
+ await wallet.connect();
+
+ const withdrawalFinishedCond = wallet.waitForNotificationCond(
+ (wn) =>
+ wn.type === NotificationType.TransactionStateTransition &&
+ wn.transactionId.startsWith("txn:withdrawal:") &&
+ wn.newTxState.major === TransactionMajorState.Done,
+ );
await wallet.client.call(WalletApiOperation.AddExchange, {
exchangeBaseUrl: exchange.baseUrl,
});
- await wallet.client.call(WalletApiOperation.WithdrawFakebank, {
- exchange: exchange.baseUrl,
- amount: "TESTKUDOS:5000",
- bank: bank.baseUrl,
+ // Results in about 1K coins withdrawn
+ await wallet.client.call(WalletApiOperation.WithdrawTestBalance, {
+ exchangeBaseUrl: exchange.baseUrl,
+ amount: "TESTKUDOS:10000" as AmountString,
+ corebankApiBaseUrl: bank.baseUrl,
});
- await exchange.runWirewatchOnce();
-
- await wallet.runUntilDone();
+ await withdrawalFinishedCond;
// Check balance
const balResp = await wallet.client.call(WalletApiOperation.GetBalances, {});
console.log(balResp);
-
- await t.shutdown();
}
-runWithdrawalHighTest.suites = ["wallet-perf"];
-runWithdrawalHighTest.excludeByDefault = true;
+runWithdrawalHugeTest.suites = ["wallet-perf"];
+// FIXME: Should not be "experimental" but "slow" or something similar.
+runWithdrawalHugeTest.experimental = true;
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-manual.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-manual.ts
index b691ae508..8ab029acc 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-manual.ts
+++ b/packages/taler-harness/src/integrationtests/test-withdrawal-manual.ts
@@ -17,53 +17,76 @@
/**
* Imports.
*/
-import { GlobalTestState } from "../harness/harness.js";
-import { createSimpleTestkudosEnvironment } from "../harness/helpers.js";
-import { WalletApiOperation, BankApi } from "@gnu-taler/taler-wallet-core";
import {
AbsoluteTime,
- Duration,
- TalerProtocolTimestamp,
+ TalerCorebankApiClient,
+ Logger,
+ WireGatewayApiClient,
+ j2s,
+ AmountString,
} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState } from "../harness/harness.js";
+import { createSimpleTestkudosEnvironmentV2 } from "../harness/helpers.js";
+
+const logger = new Logger("test-withdrawal-manual.ts");
/**
* Run test for basic, bank-integrated withdrawal.
*/
-export async function runTestWithdrawalManualTest(t: GlobalTestState) {
+export async function runWithdrawalManualTest(t: GlobalTestState) {
// Set up test environment
- const { wallet, bank, exchange, exchangeBankAccount } =
- await createSimpleTestkudosEnvironment(t);
+ const { walletClient, bank, exchange, exchangeBankAccount } =
+ await createSimpleTestkudosEnvironmentV2(t);
// Create a withdrawal operation
- const user = await BankApi.createRandomBankUser(bank);
+ const bankAccessApiClient = new TalerCorebankApiClient(
+ bank.corebankApiBaseUrl,
+ );
+
+ const user = await bankAccessApiClient.createRandomBankUser();
- await wallet.client.call(WalletApiOperation.AddExchange, {
+ await walletClient.call(WalletApiOperation.AddExchange, {
exchangeBaseUrl: exchange.baseUrl,
});
const tStart = AbsoluteTime.now();
+ logger.info("starting AcceptManualWithdrawal request");
// We expect this to return immediately.
- const wres = await wallet.client.call(
+
+ const wres = await walletClient.call(
WalletApiOperation.AcceptManualWithdrawal,
{
exchangeBaseUrl: exchange.baseUrl,
- amount: "TESTKUDOS:10",
+ amount: "TESTKUDOS:10" as AmountString,
},
);
+ logger.info("AcceptManualWithdrawal finished");
+ logger.info(`result: ${j2s(wres)}`);
+
// Check that the request did not go into long-polling.
const duration = AbsoluteTime.difference(tStart, AbsoluteTime.now());
- if (duration.d_ms > 5 * 1000) {
+ if (typeof duration.d_ms !== "number" || duration.d_ms > 5 * 1000) {
throw Error("withdrawal took too long (longpolling issue)");
}
const reservePub: string = wres.reservePub;
- await BankApi.adminAddIncoming(bank, {
- exchangeBankAccount,
+ const wireGatewayApiClient = new WireGatewayApiClient(
+ exchangeBankAccount.wireGatewayApiBaseUrl,
+ {
+ auth: {
+ username: exchangeBankAccount.accountName,
+ password: exchangeBankAccount.accountPassword,
+ },
+ },
+ );
+
+ await wireGatewayApiClient.adminAddIncoming({
amount: "TESTKUDOS:10",
debitAccountPayto: user.accountPaytoUri,
reservePub: reservePub,
@@ -71,14 +94,14 @@ export async function runTestWithdrawalManualTest(t: GlobalTestState) {
await exchange.runWirewatchOnce();
- await wallet.runUntilDone();
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
// Check balance
- const balResp = await wallet.client.call(WalletApiOperation.GetBalances, {});
+ const balResp = await walletClient.call(WalletApiOperation.GetBalances, {});
t.assertAmountEquals("TESTKUDOS:9.72", balResp.balances[0].available);
await t.shutdown();
}
-runTestWithdrawalManualTest.suites = ["wallet"];
+runWithdrawalManualTest.suites = ["wallet"];
diff --git a/packages/taler-wallet-cli/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts
index 4b1c28bde..2f6304773 100644
--- a/packages/taler-wallet-cli/src/integrationtests/testrunner.ts
+++ b/packages/taler-harness/src/integrationtests/testrunner.ts
@@ -14,92 +14,116 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { CancellationToken, minimatch } from "@gnu-taler/taler-util";
+import { CancellationToken, Logger, minimatch } from "@gnu-taler/taler-util";
import * as child_process from "child_process";
+import { spawnSync } from "child_process";
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
import url from "url";
import {
GlobalTestState,
+ TestRunResult,
runTestWithState,
shouldLingerInTest,
- TestRunResult,
} from "../harness/harness.js";
+import { getSharedTestDir } from "../harness/helpers.js";
+import { runAgeRestrictionsDepositTest } from "./test-age-restrictions-deposit.js";
import { runAgeRestrictionsMerchantTest } from "./test-age-restrictions-merchant.js";
+import { runAgeRestrictionsMixedMerchantTest } from "./test-age-restrictions-mixed-merchant.js";
+import { runAgeRestrictionsPeerTest } from "./test-age-restrictions-peer.js";
import { runBankApiTest } from "./test-bank-api.js";
import { runClaimLoopTest } from "./test-claim-loop.js";
import { runClauseSchnorrTest } from "./test-clause-schnorr.js";
+import { runCurrencyScopeTest } from "./test-currency-scope.js";
+import { runDenomLostTest } from "./test-denom-lost.js";
import { runDenomUnofferedTest } from "./test-denom-unoffered.js";
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 { 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 { runLibeufinApiBankaccountTest } from "./test-libeufin-api-bankaccount.js";
-import { runLibeufinApiBankconnectionTest } from "./test-libeufin-api-bankconnection.js";
-import { runLibeufinApiFacadeTest } from "./test-libeufin-api-facade.js";
-import { runLibeufinApiFacadeBadRequestTest } from "./test-libeufin-api-facade-bad-request.js";
-import { runLibeufinApiPermissionsTest } from "./test-libeufin-api-permissions.js";
-import { runLibeufinApiSandboxCamtTest } from "./test-libeufin-api-sandbox-camt.js";
-import { runLibeufinApiSandboxTransactionsTest } from "./test-libeufin-api-sandbox-transactions.js";
-import { runLibeufinApiSchedulingTest } from "./test-libeufin-api-scheduling.js";
-import { runLibeufinApiUsersTest } from "./test-libeufin-api-users.js";
-import { runLibeufinBadGatewayTest } from "./test-libeufin-bad-gateway.js";
-import { runLibeufinBasicTest } from "./test-libeufin-basic.js";
-import { runLibeufinC5xTest } from "./test-libeufin-c5x.js";
-import { runLibeufinAnastasisFacadeTest } from "./test-libeufin-facade-anastasis.js";
-import { runLibeufinKeyrotationTest } from "./test-libeufin-keyrotation.js";
-import { runLibeufinNexusBalanceTest } from "./test-libeufin-nexus-balance.js";
-import { runLibeufinRefundTest } from "./test-libeufin-refund.js";
-import { runLibeufinRefundMultipleUsersTest } from "./test-libeufin-refund-multiple-users.js";
-import { runLibeufinSandboxWireTransferCliTest } from "./test-libeufin-sandbox-wire-transfer-cli.js";
-import { runLibeufinTutorialTest } from "./test-libeufin-tutorial.js";
+import { runKycTest } from "./test-kyc.js";
+import { runLibeufinBankTest } from "./test-libeufin-bank.js";
import { runMerchantExchangeConfusionTest } from "./test-merchant-exchange-confusion.js";
-import { runMerchantInstancesTest } from "./test-merchant-instances.js";
import { runMerchantInstancesDeleteTest } from "./test-merchant-instances-delete.js";
import { runMerchantInstancesUrlsTest } from "./test-merchant-instances-urls.js";
+import { runMerchantInstancesTest } from "./test-merchant-instances.js";
import { runMerchantLongpollingTest } from "./test-merchant-longpolling.js";
import { runMerchantRefundApiTest } from "./test-merchant-refund-api.js";
import { runMerchantSpecPublicOrdersTest } from "./test-merchant-spec-public-orders.js";
+import { runMultiExchangeTest } from "./test-multiexchange.js";
+import { runOtpTest } from "./test-otp.js";
import { runPayPaidTest } from "./test-pay-paid.js";
-import { runPaymentTest } from "./test-payment.js";
+import { runPaymentAbortTest } from "./test-payment-abort.js";
import { runPaymentClaimTest } from "./test-payment-claim.js";
+import { runPaymentDeletedTest } from "./test-payment-deleted.js";
+import { runPaymentExpiredTest } from "./test-payment-expired.js";
import { runPaymentFaultTest } from "./test-payment-fault.js";
import { runPaymentForgettableTest } from "./test-payment-forgettable.js";
import { runPaymentIdempotencyTest } from "./test-payment-idempotency.js";
import { runPaymentMultipleTest } from "./test-payment-multiple.js";
-import { runPaymentDemoTest } from "./test-payment-on-demo.js";
+import { runPaymentShareTest } from "./test-payment-share.js";
+import { runPaymentTemplateTest } from "./test-payment-template.js";
import { runPaymentTransientTest } from "./test-payment-transient.js";
import { runPaymentZeroTest } from "./test-payment-zero.js";
+import { runPaymentTest } from "./test-payment.js";
import { runPaywallFlowTest } from "./test-paywall-flow.js";
+import { runPeerPullLargeTest } from "./test-peer-pull-large.js";
+import { runPeerPushLargeTest } from "./test-peer-push-large.js";
+import { runPeerRepairTest } from "./test-peer-repair.js";
import { runPeerToPeerPullTest } from "./test-peer-to-peer-pull.js";
import { runPeerToPeerPushTest } from "./test-peer-to-peer-push.js";
-import { runRefundTest } from "./test-refund.js";
import { runRefundAutoTest } from "./test-refund-auto.js";
import { runRefundGoneTest } from "./test-refund-gone.js";
import { runRefundIncrementalTest } from "./test-refund-incremental.js";
+import { runRefundTest } from "./test-refund.js";
import { runRevocationTest } from "./test-revocation.js";
+import { runSimplePaymentTest } from "./test-simple-payment.js";
+import { runStoredBackupsTest } from "./test-stored-backups.js";
import { runTimetravelAutorefreshTest } from "./test-timetravel-autorefresh.js";
import { runTimetravelWithdrawTest } from "./test-timetravel-withdraw.js";
-import { runTippingTest } from "./test-tipping.js";
+import { runTermOfServiceFormatTest } from "./test-tos-format.js";
import { runWalletBackupBasicTest } from "./test-wallet-backup-basic.js";
import { runWalletBackupDoublespendTest } from "./test-wallet-backup-doublespend.js";
+import { runWalletBalanceNotificationsTest } from "./test-wallet-balance-notifications.js";
+import { runWalletBalanceZeroTest } from "./test-wallet-balance-zero.js";
+import { runWalletBalanceTest } from "./test-wallet-balance.js";
+import { runWalletBlockedDepositTest } from "./test-wallet-blocked-deposit.js";
+import { runWalletBlockedPayMerchantTest } from "./test-wallet-blocked-pay-merchant.js";
+import { runWalletBlockedPayPeerPullTest } from "./test-wallet-blocked-pay-peer-pull.js";
+import { runWalletBlockedPayPeerPushTest } from "./test-wallet-blocked-pay-peer-push.js";
+import { runWalletCliTerminationTest } from "./test-wallet-cli-termination.js";
+import { runWalletConfigTest } from "./test-wallet-config.js";
+import { runWalletCryptoWorkerTest } from "./test-wallet-cryptoworker.js";
import { runWalletDblessTest } from "./test-wallet-dbless.js";
+import { runWalletDd48Test } from "./test-wallet-dd48.js";
+import { runWalletDenomExpireTest } from "./test-wallet-denom-expire.js";
+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 { runWalletNotificationsTest } from "./test-wallet-notifications.js";
+import { runWalletObservabilityTest } from "./test-wallet-observability.js";
+import { runWalletRefreshErrorsTest } from "./test-wallet-refresh-errors.js";
+import { runWalletRefreshTest } from "./test-wallet-refresh.js";
+import { runWalletWirefeesTest } from "./test-wallet-wirefees.js";
import { runWallettestingTest } from "./test-wallettesting.js";
import { runWithdrawalAbortBankTest } from "./test-withdrawal-abort-bank.js";
import { runWithdrawalBankIntegratedTest } from "./test-withdrawal-bank-integrated.js";
+import { runWithdrawalConversionTest } from "./test-withdrawal-conversion.js";
import { runWithdrawalFakebankTest } from "./test-withdrawal-fakebank.js";
-import { runTestWithdrawalManualTest } from "./test-withdrawal-manual.js";
-import { runAgeRestrictionsPeerTest } from "./test-age-restrictions-peer.js";
-import { runWalletBalanceTest } from "./test-wallet-balance.js";
-import { runAgeRestrictionsMixedMerchantTest } from "./test-age-restrictions-mixed-merchant.js";
-import { runWalletCryptoWorkerTest } from "./test-wallet-cryptoworker.js";
-import { runWithdrawalHighTest } from "./test-withdrawal-high.js";
+import { runWithdrawalFeesTest } from "./test-withdrawal-fees.js";
+import { runWithdrawalHugeTest } from "./test-withdrawal-huge.js";
+import { runWithdrawalManualTest } from "./test-withdrawal-manual.js";
/**
* Test runner.
*/
+const logger = new Logger("testrunner.ts");
/**
* Spec for one test.
@@ -107,94 +131,120 @@ import { runWithdrawalHighTest } from "./test-withdrawal-high.js";
interface TestMainFunction {
(t: GlobalTestState): Promise<void>;
timeoutMs?: number;
- excludeByDefault?: boolean;
+ experimental?: boolean;
suites?: string[];
}
const allTests: TestMainFunction[] = [
runAgeRestrictionsMerchantTest,
- runAgeRestrictionsPeerTest,
runAgeRestrictionsMixedMerchantTest,
+ runAgeRestrictionsPeerTest,
+ runAgeRestrictionsDepositTest,
runBankApiTest,
runClaimLoopTest,
runClauseSchnorrTest,
- runWalletCryptoWorkerTest,
- runDepositTest,
runDenomUnofferedTest,
- runExchangeManagementTest,
+ runDepositTest,
+ runSimplePaymentTest,
+ runExchangeManagementFaultTest,
runExchangeTimetravelTest,
runFeeRegressionTest,
runForcedSelectionTest,
- runLibeufinBasicTest,
- runLibeufinKeyrotationTest,
- runLibeufinTutorialTest,
- runLibeufinRefundTest,
- runLibeufinC5xTest,
- runLibeufinNexusBalanceTest,
- runLibeufinBadGatewayTest,
- runLibeufinRefundMultipleUsersTest,
- runLibeufinApiPermissionsTest,
- runLibeufinApiFacadeTest,
- runLibeufinApiFacadeBadRequestTest,
- runLibeufinAnastasisFacadeTest,
- runLibeufinApiSchedulingTest,
- runLibeufinApiUsersTest,
- runLibeufinApiBankaccountTest,
- runLibeufinApiBankconnectionTest,
- runLibeufinApiSandboxTransactionsTest,
- runLibeufinApiSandboxCamtTest,
- runLibeufinSandboxWireTransferCliTest,
+ runKycTest,
+ runExchangePurseTest,
+ runExchangeDepositTest,
runMerchantExchangeConfusionTest,
- runMerchantInstancesTest,
runMerchantInstancesDeleteTest,
+ runMerchantInstancesTest,
runMerchantInstancesUrlsTest,
runMerchantLongpollingTest,
- runMerchantSpecPublicOrdersTest,
runMerchantRefundApiTest,
+ runMerchantSpecPublicOrdersTest,
runPaymentClaimTest,
runPaymentFaultTest,
runPaymentForgettableTest,
runPaymentIdempotencyTest,
runPaymentMultipleTest,
runPaymentTest,
- runPaymentDemoTest,
+ runPaymentShareTest,
+ runPaymentTemplateTest,
+ runPaymentAbortTest,
runPaymentTransientTest,
runPaymentZeroTest,
runPayPaidTest,
+ runPeerRepairTest,
+ runMultiExchangeTest,
+ runWalletBalanceTest,
runPaywallFlowTest,
- runPeerToPeerPushTest,
runPeerToPeerPullTest,
+ runPeerToPeerPushTest,
runRefundAutoTest,
runRefundGoneTest,
runRefundIncrementalTest,
runRefundTest,
runRevocationTest,
- runTestWithdrawalManualTest,
- runWithdrawalFakebankTest,
+ runWithdrawalManualTest,
runTimetravelAutorefreshTest,
runTimetravelWithdrawTest,
- runTippingTest,
runWalletBackupBasicTest,
runWalletBackupDoublespendTest,
- runWalletBalanceTest,
- runWithdrawalHighTest,
- runWallettestingTest,
+ runWalletNotificationsTest,
+ runWalletCryptoWorkerTest,
runWalletDblessTest,
+ runWallettestingTest,
runWithdrawalAbortBankTest,
+ // runWithdrawalNotifyBeforeTxTest,
runWithdrawalBankIntegratedTest,
+ runWithdrawalFakebankTest,
+ runWithdrawalFeesTest,
+ runWithdrawalConversionTest,
+ runWithdrawalHugeTest,
+ runTermOfServiceFormatTest,
+ runStoredBackupsTest,
+ runPaymentExpiredTest,
+ runWalletGenDbTest,
+ runLibeufinBankTest,
+ runPaymentDeletedTest,
+ runWalletDd48Test,
+ runCurrencyScopeTest,
+ runWalletRefreshTest,
+ runWalletCliTerminationTest,
+ runOtpTest,
+ runWalletBalanceNotificationsTest,
+ runExchangeManagementTest,
+ runWalletConfigTest,
+ runWalletObservabilityTest,
+ runWalletDevExperimentsTest,
+ runWalletBalanceZeroTest,
+ runWalletInsufficientBalanceTest,
+ runWalletWirefeesTest,
+ runDenomLostTest,
+ runWalletDenomExpireTest,
+ runWalletBlockedDepositTest,
+ runWalletBlockedPayMerchantTest,
+ runWalletBlockedPayPeerPushTest,
+ runWalletBlockedPayPeerPullTest,
+ runWalletExchangeUpdateTest,
+ runWalletRefreshErrorsTest,
+ runPeerPullLargeTest,
+ runPeerPushLargeTest,
];
export interface TestRunSpec {
includePattern?: string;
suiteSpec?: string;
dryRun?: boolean;
+ failFast?: boolean;
+ waitOnFail?: boolean;
+ includeExperimental: boolean;
+ noTimeout: boolean;
verbosity: number;
}
export interface TestInfo {
name: string;
suites: string[];
- excludeByDefault: boolean;
+ experimental: boolean;
}
function updateCurrentSymlink(testDir: string): void {
@@ -232,7 +282,37 @@ interface RunTestChildInstruction {
testRootDir: string;
}
+function purgeSharedTestEnvironment() {
+ const rmRes = spawnSync("rm", ["-rf", `${getSharedTestDir()}`]);
+ if (rmRes.status != 0) {
+ logger.warn("can't delete shared test directory");
+ }
+ const psqlRes = spawnSync("psql", ["-Aqtl"], {
+ encoding: "utf-8",
+ });
+ if (psqlRes.status != 0) {
+ logger.warn("could not list available postgres databases");
+ return;
+ }
+ if (psqlRes.output[1]!!.indexOf("taler-integrationtest-shared") >= 0) {
+ const dropRes = spawnSync("dropdb", ["taler-integrationtest-shared"], {
+ encoding: "utf-8",
+ });
+ if (dropRes.status != 0) {
+ logger.warn("could not drop taler-integrationtest-shared database");
+ return;
+ }
+ }
+}
+
export async function runTests(spec: TestRunSpec) {
+ if (!process.env.TALER_HARNESS_KEEP) {
+ logger.info("purging shared test environment");
+ purgeSharedTestEnvironment();
+ } else {
+ logger.info("keeping shared test environment");
+ }
+
const testRootDir = fs.mkdtempSync(
path.join(os.tmpdir(), "taler-integrationtests-"),
);
@@ -268,16 +348,16 @@ export async function runTests(spec: TestRunSpec) {
continue;
}
+ if (testCase.experimental && !spec.includeExperimental) {
+ continue;
+ }
+
if (suites) {
const ts = new Set(testCase.suites ?? []);
const intersection = new Set([...suites].filter((x) => ts.has(x)));
if (intersection.size === 0) {
continue;
}
- } else {
- if (testCase.excludeByDefault) {
- continue;
- }
}
if (spec.dryRun) {
@@ -314,12 +394,27 @@ export async function runTests(spec: TestRunSpec) {
currentChild.stdout?.pipe(harnessLogStream);
currentChild.stderr?.pipe(harnessLogStream);
- const defaultTimeout = 60000;
- const testTimeoutMs = testCase.timeoutMs ?? defaultTimeout;
+ // Default timeout when the test doesn't override it.
+ let defaultTimeout = 60000;
+ const overrideDefaultTimeout = process.env.TALER_TEST_TIMEOUT;
+ if (overrideDefaultTimeout) {
+ defaultTimeout = Number.parseInt(overrideDefaultTimeout, 10) * 1000;
+ }
- console.log(`running ${testName} with timeout ${testTimeoutMs}ms`);
+ // Set the timeout to at least be the default timeout.
+ const testTimeoutMs = testCase.timeoutMs
+ ? Math.max(testCase.timeoutMs, defaultTimeout)
+ : defaultTimeout;
- const { token } = CancellationToken.timeout(testTimeoutMs);
+ if (spec.noTimeout) {
+ console.log(`running ${testName}, no timeout`);
+ } else {
+ console.log(`running ${testName} with timeout ${testTimeoutMs}ms`);
+ }
+
+ const token = spec.noTimeout
+ ? CancellationToken.CONTINUE
+ : CancellationToken.timeout(testTimeoutMs).token;
const resultPromise: Promise<TestRunResult> = new Promise(
(resolve, reject) => {
@@ -334,7 +429,7 @@ export async function runTests(spec: TestRunSpec) {
if (token.isCancelled) {
return;
}
- console.log(`process exited code=${code} signal=${signal}`);
+ logger.info(`process exited code=${code} signal=${signal}`);
if (signal) {
reject(new Error(`test worker exited with signal ${signal}`));
} else if (code != 0) {
@@ -362,6 +457,10 @@ export async function runTests(spec: TestRunSpec) {
try {
result = await token.racePromise(resultPromise);
+ if (result.status === "fail" && spec.failFast) {
+ logger.error("test failed and failing fast, exit!");
+ throw Error("exit on fail fast");
+ }
} catch (e: any) {
console.error(`test ${testName} timed out`);
if (token.isCancelled) {
@@ -434,7 +533,7 @@ export function getTestInfo(): TestInfo[] {
return allTests.map((x) => ({
name: getTestName(x),
suites: x.suites ?? [],
- excludeByDefault: x.excludeByDefault ?? false,
+ experimental: x.experimental ?? false,
}));
}
@@ -445,10 +544,9 @@ if (runTestInstrStr && process.argv.includes("__TWCLI_TESTWORKER")) {
const { testRootDir, testName } = JSON.parse(
runTestInstrStr,
) as RunTestChildInstruction;
- console.log(`running test ${testName} in worker process`);
process.on("disconnect", () => {
- console.log("got disconnect from parent");
+ logger.trace("got disconnect from parent");
process.exit(3);
});
@@ -462,35 +560,36 @@ if (runTestInstrStr && process.argv.includes("__TWCLI_TESTWORKER")) {
}
if (!process.send) {
- console.error("can't communicate with parent");
+ logger.error("can't communicate with parent");
process.exit(2);
}
if (!testMain) {
- console.log(`test ${testName} not found`);
+ logger.info(`test ${testName} not found`);
process.exit(2);
}
const testDir = path.join(testRootDir, testName);
- console.log(`running test ${testName}`);
+ logger.info(`running test ${testName}`);
const gc = new GlobalTestState({
testDir,
});
const testResult = await runTestWithState(gc, testMain, testName);
+ logger.info(`done test ${testName}: ${testResult.status}`);
process.send(testResult);
};
runTest()
.then(() => {
- console.log(`test ${testName} finished in worker`);
+ logger.trace(`test ${testName} finished in worker`);
if (shouldLingerInTest()) {
- console.log("lingering ...");
+ logger.trace("lingering ...");
return;
}
process.exit(0);
})
.catch((e) => {
- console.log(e);
+ logger.error(e);
process.exit(1);
});
}
diff --git a/packages/taler-wallet-cli/src/lint.ts b/packages/taler-harness/src/lint.ts
index 49fb9dc86..a45e6db9d 100644
--- a/packages/taler-wallet-cli/src/lint.ts
+++ b/packages/taler-harness/src/lint.ts
@@ -37,13 +37,13 @@ import {
Configuration,
decodeCrock,
} from "@gnu-taler/taler-util";
-import {
- NodeHttpLib,
- readSuccessResponseJsonOrThrow,
-} from "@gnu-taler/taler-wallet-core";
import { URL } from "url";
import { spawn } from "child_process";
import { delayMs } from "./harness/harness.js";
+import {
+ createPlatformHttpLib,
+ readSuccessResponseJsonOrThrow,
+} from "@gnu-taler/taler-util/http";
interface BasicConf {
mainCurrency: string;
@@ -53,7 +53,9 @@ interface PubkeyConf {
masterPublicKey: string;
}
-const httpLib = new NodeHttpLib();
+const httpLib = createPlatformHttpLib({
+ enableThrottling: false,
+});
interface ShellResult {
stdout: string;
@@ -133,13 +135,13 @@ function checkBasicConf(context: LintContext): BasicConf {
const currencyEntry = cfg.getString("taler", "currency");
let mainCurrency: string | undefined;
- if (!currencyEntry.value) {
+ if (!currencyEntry.isDefined()) {
context.numErr++;
console.log("error: currency not defined in section TALER option CURRENCY");
console.log("Aborting further checks.");
process.exit(1);
} else {
- mainCurrency = currencyEntry.value.toUpperCase();
+ mainCurrency = currencyEntry.required().toUpperCase();
}
if (mainCurrency === "KUDOS") {
@@ -404,7 +406,7 @@ export async function checkExchangeHttpd(
{
const mgmtUrl = new URL("management/keys", baseUrl);
- const resp = await httpLib.get(mgmtUrl.href);
+ const resp = await httpLib.fetch(mgmtUrl.href);
const futureKeys = await readSuccessResponseJsonOrThrow(
resp,
@@ -428,7 +430,7 @@ export async function checkExchangeHttpd(
{
const keysUrl = new URL("keys", baseUrl);
- const resp = await Promise.race([httpLib.get(keysUrl.href), delayMs(2000)]);
+ const resp = await Promise.race([httpLib.fetch(keysUrl.href), delayMs(2000)]);
if (!resp) {
context.numErr++;
@@ -464,7 +466,7 @@ export async function checkExchangeHttpd(
{
const keysUrl = new URL("wire", baseUrl);
- const resp = await Promise.race([httpLib.get(keysUrl.href), delayMs(2000)]);
+ const resp = await Promise.race([httpLib.fetch(keysUrl.href), delayMs(2000)]);
if (!resp) {
context.numErr++;
diff --git a/packages/taler-harness/src/sandcastle-config.ts b/packages/taler-harness/src/sandcastle-config.ts
new file mode 100644
index 000000000..a7f7233ac
--- /dev/null
+++ b/packages/taler-harness/src/sandcastle-config.ts
@@ -0,0 +1,10 @@
+// Work in progress.
+// TS-based schema for the sandcastle configuration.
+
+export interface SandcastleConfig {
+ currency: string;
+ merchant: {
+ apiKey: string;
+ baseUrl: string;
+ };
+}
diff --git a/packages/taler-harness/tsconfig.json b/packages/taler-harness/tsconfig.json
new file mode 100644
index 000000000..0453aaff0
--- /dev/null
+++ b/packages/taler-harness/tsconfig.json
@@ -0,0 +1,33 @@
+{
+ "compileOnSave": true,
+ "compilerOptions": {
+ "composite": true,
+ "target": "ES2020",
+ "module": "Node16",
+ "moduleResolution": "Node16",
+ "sourceMap": true,
+ "lib": ["ES2020"],
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "strict": true,
+ "strictPropertyInitialization": false,
+ "outDir": "lib",
+ "noImplicitAny": true,
+ "noImplicitThis": true,
+ "incremental": true,
+ "esModuleInterop": true,
+ "importHelpers": true,
+ "rootDir": "src",
+ "baseUrl": "./src",
+ "typeRoots": ["./node_modules/@types"]
+ },
+ "include": ["src/**/*"],
+ "references": [
+ {
+ "path": "../taler-wallet-core/"
+ },
+ {
+ "path": "../taler-util/"
+ }
+ ]
+}
diff --git a/packages/taler-util/.eslintrc.cjs b/packages/taler-util/.eslintrc.cjs
new file mode 100644
index 000000000..05618b499
--- /dev/null
+++ b/packages/taler-util/.eslintrc.cjs
@@ -0,0 +1,28 @@
+module.exports = {
+ extends: [
+ 'eslint:recommended',
+ 'plugin:@typescript-eslint/recommended',
+ 'plugin:react/recommended',
+ ],
+ parser: '@typescript-eslint/parser',
+ plugins: ['@typescript-eslint', 'header'],
+ root: true,
+ rules: {
+ "react/no-unknown-property": 0,
+ "react/no-unescaped-entities": 0,
+ "@typescript-eslint/no-namespace": 0,
+ "@typescript-eslint/no-unused-vars": [2,{argsIgnorePattern:"^_"}],
+ "header/header": [2,"copyleft-header.js"]
+ },
+ parserOptions: {
+ ecmaVersion: 6,
+ sourceType: 'module',
+ jsx: true,
+ },
+ settings: {
+ react: {
+ version: "18",
+ pragma: "h",
+ }
+ },
+};
diff --git a/packages/taler-util/Makefile b/packages/taler-util/Makefile
new file mode 100644
index 000000000..def16c823
--- /dev/null
+++ b/packages/taler-util/Makefile
@@ -0,0 +1,42 @@
+# This Makefile has been placed in the public domain.
+
+ifeq ($(TOPLEVEL), yes)
+ $(info top-level build)
+ -include ../../.config.mk
+ override DESTDIR := $(TOP_DESTDIR)
+else
+ $(info package-level build)
+ -include ../../.config.mk
+ -include .config.mk
+endif
+
+$(info prefix is $(prefix))
+
+all:
+ @echo use 'make install' to build and install taler-util
+
+ifndef prefix
+.PHONY: warn-noprefix install
+warn-noprefix:
+ @echo "no prefix configured, did you run ./configure?"
+install: warn-noprefix
+else
+LIBDIR = $(prefix)/share/taler-js/taler-util
+NODE_DEPS = $(shell jq "(.dependencies|keys|map(\"node_modules/\" + .)|join(\" \"))" package.json -r)
+.PHONY: install install-nodeps deps
+install-nodeps:
+ pnpm compile
+ @echo installing taler-util to $(DESTDIR)$(prefix)
+ install -d $(DESTDIR)$(LIBDIR)/lib/globbing
+ install lib/*.* $(DESTDIR)$(LIBDIR)/lib
+ install lib/globbing/*.* $(DESTDIR)$(LIBDIR)/lib/globbing
+ install package.json $(DESTDIR)$(LIBDIR)
+ tar hcf - $(NODE_DEPS) | (cd $(DESTDIR)$(LIBDIR); tar xf -)
+
+deps:
+ pnpm install --frozen-lockfile --filter @gnu-taler/taler-util...
+ pnpm run --filter @gnu-taler/taler-util... compile
+install:
+ $(MAKE) deps
+ $(MAKE) install-nodeps
+endif
diff --git a/packages/taler-util/package.json b/packages/taler-util/package.json
index 6406f3f49..74b2d6155 100644
--- a/packages/taler-util/package.json
+++ b/packages/taler-util/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/taler-util",
- "version": "0.9.0",
+ "version": "0.10.7",
"description": "Generic helper functionality for GNU Taler",
"type": "module",
"types": "./lib/index.node.d.ts",
@@ -11,33 +11,83 @@
".": {
"node": "./lib/index.node.js",
"browser": "./lib/index.browser.js",
+ "qtart": "./lib/index.qtart.js",
"default": "./lib/index.js"
+ },
+ "./twrpc": {
+ "default": "./lib/twrpc.js"
+ },
+ "./compat": {
+ "types": "./lib/compat.node.js",
+ "node": "./lib/compat.node.js",
+ "qtart": "./lib/compat.qtart.js",
+ "default": "./lib/not-implemented.js"
+ },
+ "./clk": {
+ "default": "./lib/clk.js"
+ },
+ "./http": {
+ "default": "./lib/http.js"
+ },
+ "./qtart": {
+ "types": "./lib/qtart.js",
+ "qtart": "./lib/qtart.js",
+ "default": "./lib/not-implemented.js"
+ }
+ },
+ "imports": {
+ "#twrpc-impl": {
+ "node": "./lib/twrpc-impl.node.js",
+ "qtart": "./lib/twrpc-impl.qtart.js"
+ },
+ "#compat-impl": {
+ "node": "./lib/compat.node.js",
+ "qtart": "./lib/compat.qtart.js",
+ "type": "./lib/compat.d.ts"
+ },
+ "#http-impl": {
+ "type": "./lib/http-impl.node.js",
+ "node": "./lib/http-impl.node.js",
+ "qtart": "./lib/http-impl.qtart.js",
+ "default": "./lib/http-impl.missing.js"
+ },
+ "#argon2-impl": {
+ "node": "./lib/argon2-impl.wasm.js",
+ "deno": "./lib/argon2-impl.wasm.js",
+ "worker": "./lib/argon2-impl.wasm.js",
+ "browser": "./lib/argon2-impl.wasm.js",
+ "webpack": "./lib/argon2-impl.wasm.js",
+ "default": "./lib/argon2-impl.missing.js"
}
},
"scripts": {
- "prepare": "tsc",
"compile": "tsc",
"test": "tsc && ava",
- "clean": "rimraf dist lib tsconfig.tsbuildinfo",
+ "clean": "rm -rf dist lib tsconfig.tsbuildinfo",
+ "typedoc": "typedoc --out dist/typedoc ./src/",
"pretty": "prettier --write src"
},
"devDependencies": {
- "@types/node": "^18.8.5",
- "ava": "^4.3.3",
- "esbuild": "^0.14.21",
- "prettier": "^2.5.1",
- "rimraf": "^3.0.2",
- "typescript": "^4.8.4"
+ "eslint": "^8.56.0",
+ "@typescript-eslint/eslint-plugin": "^6.19.0",
+ "@typescript-eslint/parser": "^6.19.0",
+ "@types/follow-redirects": "^1.14.4",
+ "@types/node": "^18.11.17",
+ "ava": "^6.0.1",
+ "esbuild": "^0.19.9",
+ "typescript": "^5.3.3"
},
"dependencies": {
- "big-integer": "^1.6.51",
- "fflate": "^0.7.4",
+ "big-integer": "^1.6.52",
+ "fflate": "^0.8.1",
+ "follow-redirects": "^1.15.5",
+ "hash-wasm": "^4.11.0",
"jed": "^1.1.1",
- "tslib": "^2.4.0"
+ "tslib": "^2.6.2"
},
"ava": {
"files": [
- "lib/*test*"
+ "lib/**/*test.js"
]
}
}
diff --git a/packages/taler-util/src/MerchantApiClient.ts b/packages/taler-util/src/MerchantApiClient.ts
new file mode 100644
index 000000000..c27f1d582
--- /dev/null
+++ b/packages/taler-util/src/MerchantApiClient.ts
@@ -0,0 +1,380 @@
+/*
+ This file is part of GNU Taler
+ (C) 2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { codecForAny } from "./codec.js";
+import {
+ TalerMerchantApi,
+ codecForMerchantConfig,
+ codecForMerchantOrderPrivateStatusResponse,
+} from "./http-client/types.js";
+import { HttpStatusCode } from "./http-status-codes.js";
+import {
+ createPlatformHttpLib,
+ expectSuccessResponseOrThrow,
+ readSuccessResponseJsonOrThrow,
+ readTalerErrorResponse,
+} from "./http.js";
+import { FacadeCredentials } from "./libeufin-api-types.js";
+import { LibtoolVersion } from "./libtool-version.js";
+import { Logger } from "./logging.js";
+import {
+ MerchantInstancesResponse,
+ MerchantPostOrderRequest,
+ MerchantPostOrderResponse,
+ MerchantTemplateAddDetails,
+ codecForMerchantPostOrderResponse,
+} from "./merchant-api-types.js";
+import {
+ FailCasesByMethod,
+ OperationFail,
+ OperationOk,
+ ResultByMethod,
+ opEmptySuccess,
+ opKnownHttpFailure,
+ opSuccessFromHttp,
+ opUnknownFailure,
+} from "./operation.js";
+import { AmountString } from "./taler-types.js";
+import { TalerProtocolDuration } from "./time.js";
+
+const logger = new Logger("MerchantApiClient.ts");
+
+// FIXME: Explain!
+export type TalerMerchantResultByMethod<prop extends keyof MerchantApiClient> =
+ ResultByMethod<MerchantApiClient, prop>;
+
+// FIXME: Explain!
+export type TalerMerchantErrorsByMethod<prop extends keyof MerchantApiClient> =
+ FailCasesByMethod<MerchantApiClient, prop>;
+
+export interface MerchantAuthConfiguration {
+ method: "external" | "token";
+ token?: string;
+}
+
+// FIXME: Why do we need this? Describe / fix!
+export interface PartialMerchantInstanceConfig {
+ auth?: MerchantAuthConfiguration;
+ id: string;
+ name: string;
+ paytoUris: string[];
+ address?: unknown;
+ jurisdiction?: unknown;
+ defaultWireTransferDelay?: TalerProtocolDuration;
+ defaultPayDelay?: TalerProtocolDuration;
+}
+
+export interface CreateMerchantTippingReserveRequest {
+ // Amount that the merchant promises to put into the reserve
+ initial_balance: AmountString;
+
+ // Exchange the merchant intends to use for tipping
+ exchange_url: string;
+
+ // Desired wire method, for example "iban" or "x-taler-bank"
+ wire_method: string;
+}
+
+export interface DeleteTippingReserveArgs {
+ reservePub: string;
+ purge?: boolean;
+}
+
+interface MerchantBankAccount {
+ // The payto:// URI where the wallet will send coins.
+ payto_uri: string;
+
+ // Optional base URL for a facade where the
+ // merchant backend can see incoming wire
+ // transfers to reconcile its accounting
+ // with that of the exchange. Used by
+ // taler-merchant-wirewatch.
+ credit_facade_url?: string;
+
+ // Credentials for accessing the credit facade.
+ credit_facade_credentials?: FacadeCredentials;
+}
+
+export interface MerchantInstanceConfig {
+ auth: MerchantAuthConfiguration;
+ id: string;
+ name: string;
+ address: unknown;
+ jurisdiction: unknown;
+ use_stefan: boolean;
+ default_wire_transfer_delay: TalerProtocolDuration;
+ default_pay_delay: TalerProtocolDuration;
+}
+
+export interface PrivateOrderStatusQuery {
+ instance?: string;
+ orderId: string;
+ sessionId?: string;
+}
+
+export interface OtpDeviceAddDetails {
+ // Device ID to use.
+ otp_device_id: string;
+
+ // Human-readable description for the device.
+ otp_device_description: string;
+
+ // A base64-encoded key
+ otp_key: string;
+
+ // Algorithm for computing the POS confirmation.
+ otp_algorithm: number;
+
+ // Counter for counter-based OTP devices.
+ otp_ctr?: number;
+}
+
+/**
+ * Client for the GNU Taler merchant backend.
+ */
+export class MerchantApiClient {
+ /**
+ * Base URL for the particular instance that this merchant API client
+ * is for.
+ */
+ private baseUrl: string;
+
+ readonly auth: MerchantAuthConfiguration;
+
+ public readonly PROTOCOL_VERSION = "6:0:2";
+
+ constructor(
+ baseUrl: string,
+ options: { auth?: MerchantAuthConfiguration } = {},
+ ) {
+ this.baseUrl = baseUrl;
+
+ this.auth = options?.auth ?? {
+ method: "external",
+ };
+ }
+
+ httpClient = createPlatformHttpLib();
+
+ async changeAuth(auth: MerchantAuthConfiguration): Promise<void> {
+ const url = new URL("private/auth", this.baseUrl);
+ const res = await this.httpClient.fetch(url.href, {
+ method: "POST",
+ body: auth,
+ headers: this.makeAuthHeader(),
+ });
+ await expectSuccessResponseOrThrow(res);
+ }
+
+ async getPrivateInstanceInfo(): Promise<any> {
+ const url = new URL("private", this.baseUrl);
+ const resp = await this.httpClient.fetch(url.href, {
+ method: "GET",
+ headers: this.makeAuthHeader(),
+ });
+ return await resp.json();
+ }
+
+ async deleteInstance(instanceId: string) {
+ const url = new URL(`management/instances/${instanceId}`, this.baseUrl);
+ const resp = await this.httpClient.fetch(url.href, {
+ method: "DELETE",
+ headers: this.makeAuthHeader(),
+ });
+ await expectSuccessResponseOrThrow(resp);
+ }
+
+ async createInstance(req: MerchantInstanceConfig): Promise<void> {
+ const url = new URL("management/instances", this.baseUrl);
+ await this.httpClient.fetch(url.href, {
+ method: "POST",
+ body: req,
+ headers: this.makeAuthHeader(),
+ });
+ }
+
+ async getInstances(): Promise<MerchantInstancesResponse> {
+ const url = new URL("management/instances", this.baseUrl);
+ const resp = await this.httpClient.fetch(url.href, {
+ headers: this.makeAuthHeader(),
+ });
+ return readSuccessResponseJsonOrThrow(resp, codecForAny());
+ }
+
+ async getInstanceFullDetails(instanceId: string): Promise<any> {
+ const url = new URL(`management/instances/${instanceId}`, this.baseUrl);
+ try {
+ const resp = await this.httpClient.fetch(url.href, {
+ headers: this.makeAuthHeader(),
+ });
+ return resp.json();
+ } catch (e) {
+ throw e;
+ }
+ }
+
+ async createOrder(
+ req: MerchantPostOrderRequest,
+ ): Promise<MerchantPostOrderResponse> {
+ let url = new URL("private/orders", this.baseUrl);
+ const resp = await this.httpClient.fetch(url.href, {
+ method: "POST",
+ body: req,
+ headers: this.makeAuthHeader(),
+ });
+ return readSuccessResponseJsonOrThrow(
+ resp,
+ codecForMerchantPostOrderResponse(),
+ );
+ }
+
+ async deleteOrder(req: { orderId: string; force?: boolean }): Promise<void> {
+ let url = new URL(`private/orders/${req.orderId}`, this.baseUrl);
+ if (req.force) {
+ url.searchParams.set("force", "yes");
+ }
+ const resp = await this.httpClient.fetch(url.href, {
+ method: "DELETE",
+ body: req,
+ headers: this.makeAuthHeader(),
+ });
+ if (resp.status !== 204) {
+ throw Error(`failed to delete order (status ${resp.status})`);
+ }
+ }
+
+ async queryPrivateOrderStatus(
+ query: PrivateOrderStatusQuery,
+ ): Promise<TalerMerchantApi.MerchantOrderStatusResponse> {
+ const reqUrl = new URL(`private/orders/${query.orderId}`, this.baseUrl);
+ if (query.sessionId) {
+ reqUrl.searchParams.set("session_id", query.sessionId);
+ }
+ const resp = await this.httpClient.fetch(reqUrl.href, {
+ headers: this.makeAuthHeader(),
+ });
+ return readSuccessResponseJsonOrThrow(
+ resp,
+ codecForMerchantOrderPrivateStatusResponse(),
+ );
+ }
+
+ async giveRefund(r: {
+ instance: string;
+ orderId: string;
+ amount: string;
+ justification: string;
+ }): Promise<{ talerRefundUri: string }> {
+ const reqUrl = new URL(`private/orders/${r.orderId}/refund`, this.baseUrl);
+ const resp = await this.httpClient.fetch(reqUrl.href, {
+ method: "POST",
+ body: {
+ refund: r.amount,
+ reason: r.justification,
+ },
+ });
+ const respBody = await resp.json();
+ return {
+ talerRefundUri: respBody.taler_refund_uri,
+ };
+ }
+
+ async createTemplate(req: MerchantTemplateAddDetails) {
+ let url = new URL("private/templates", this.baseUrl);
+ const resp = await this.httpClient.fetch(url.href, {
+ method: "POST",
+ body: req,
+ headers: this.makeAuthHeader(),
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ case HttpStatusCode.NoContent:
+ return opEmptySuccess(resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ async getTemplate(templateId: string) {
+ let url = new URL(`private/templates/${templateId}`, this.baseUrl);
+ const resp = await this.httpClient.fetch(url.href, {
+ method: "GET",
+ headers: this.makeAuthHeader(),
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForAny());
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ isCompatible(version: string): boolean {
+ const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version);
+ return compare?.compatible ?? false;
+ }
+ /**
+ * https://docs.taler.net/core/api-merchant.html#get--config
+ *
+ */
+ async getConfig(): Promise<OperationOk<TalerMerchantApi.VersionResponse>> {
+ const url = new URL(`config`, this.baseUrl);
+ const resp = await this.httpClient.fetch(url.href, {
+ method: "GET",
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForMerchantConfig());
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ async createOtpDevice(
+ req: OtpDeviceAddDetails,
+ ): Promise<OperationOk<void> | OperationFail<HttpStatusCode.NotFound>> {
+ let url = new URL("private/otp-devices", this.baseUrl);
+ const resp = await this.httpClient.fetch(url.href, {
+ method: "POST",
+ body: req,
+ headers: this.makeAuthHeader(),
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ case HttpStatusCode.NoContent:
+ return opEmptySuccess(resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ private makeAuthHeader(): Record<string, string> {
+ switch (this.auth.method) {
+ case "external":
+ return {};
+ case "token":
+ return {
+ Authorization: `Bearer ${this.auth.token}`,
+ };
+ }
+ }
+}
diff --git a/packages/taler-util/src/ReserveStatus.ts b/packages/taler-util/src/ReserveStatus.ts
index be9fa9e8e..3a30755ce 100644
--- a/packages/taler-util/src/ReserveStatus.ts
+++ b/packages/taler-util/src/ReserveStatus.ts
@@ -21,17 +21,9 @@
/**
* Imports.
*/
-import {
- codecForString,
- buildCodecForObject,
- codecForList,
- Codec,
-} from "./codec.js";
+import { codecForAmountString } from "./amounts.js";
+import { Codec, buildCodecForObject } from "./codec.js";
import { AmountString } from "./taler-types.js";
-import {
- ReserveTransaction,
- codecForReserveTransaction,
-} from "./ReserveTransaction.js";
/**
* Status of a reserve.
@@ -47,5 +39,5 @@ export interface ReserveStatus {
export const codecForReserveStatus = (): Codec<ReserveStatus> =>
buildCodecForObject<ReserveStatus>()
- .property("balance", codecForString())
+ .property("balance", codecForAmountString())
.build("ReserveStatus");
diff --git a/packages/taler-util/src/ReserveTransaction.ts b/packages/taler-util/src/ReserveTransaction.ts
index 5d3f86b1a..7a3c69d07 100644
--- a/packages/taler-util/src/ReserveTransaction.ts
+++ b/packages/taler-util/src/ReserveTransaction.ts
@@ -23,6 +23,7 @@
/**
* Imports.
*/
+import { codecForAmountString } from "./amounts.js";
import {
codecForString,
buildCodecForObject,
@@ -189,18 +190,18 @@ export type ReserveTransaction =
export const codecForReserveWithdrawTransaction =
(): Codec<ReserveWithdrawTransaction> =>
buildCodecForObject<ReserveWithdrawTransaction>()
- .property("amount", codecForString())
+ .property("amount", codecForAmountString())
.property("h_coin_envelope", codecForString())
.property("h_denom_pub", codecForString())
.property("reserve_sig", codecForString())
.property("type", codecForConstString(ReserveTransactionType.Withdraw))
- .property("withdraw_fee", codecForString())
+ .property("withdraw_fee", codecForAmountString())
.build("ReserveWithdrawTransaction");
export const codecForReserveCreditTransaction =
(): Codec<ReserveCreditTransaction> =>
buildCodecForObject<ReserveCreditTransaction>()
- .property("amount", codecForString())
+ .property("amount", codecForAmountString())
.property("sender_account_url", codecForString())
.property("timestamp", codecForTimestamp)
.property("wire_reference", codecForNumber())
@@ -210,8 +211,8 @@ export const codecForReserveCreditTransaction =
export const codecForReserveClosingTransaction =
(): Codec<ReserveClosingTransaction> =>
buildCodecForObject<ReserveClosingTransaction>()
- .property("amount", codecForString())
- .property("closing_fee", codecForString())
+ .property("amount", codecForAmountString())
+ .property("closing_fee", codecForAmountString())
.property("exchange_pub", codecForString())
.property("exchange_sig", codecForString())
.property("h_wire", codecForString())
@@ -223,7 +224,7 @@ export const codecForReserveClosingTransaction =
export const codecForReserveRecoupTransaction =
(): Codec<ReserveRecoupTransaction> =>
buildCodecForObject<ReserveRecoupTransaction>()
- .property("amount", codecForString())
+ .property("amount", codecForAmountString())
.property("coin_pub", codecForString())
.property("exchange_pub", codecForString())
.property("exchange_sig", codecForString())
diff --git a/packages/taler-util/src/TaskThrottler.ts b/packages/taler-util/src/TaskThrottler.ts
new file mode 100644
index 000000000..e4fb82171
--- /dev/null
+++ b/packages/taler-util/src/TaskThrottler.ts
@@ -0,0 +1,160 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 GNUnet e.V.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received 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 { Logger } from "./logging.js";
+import { AbsoluteTime, Duration } from "./time.js";
+
+/**
+ * Implementation of token bucket throttling.
+ */
+
+/**
+ * Logger.
+ */
+const logger = new Logger("OperationThrottler.ts");
+
+/**
+ * Maximum request per second, per origin.
+ */
+const MAX_PER_SECOND = 100;
+
+/**
+ * Maximum request per minute, per origin.
+ */
+const MAX_PER_MINUTE = 500;
+
+/**
+ * Maximum request per hour, per origin.
+ */
+const MAX_PER_HOUR = 2000;
+
+/**
+ * Throttling state for one task.
+ */
+class TaskState {
+ tokensSecond: number = MAX_PER_SECOND;
+ tokensMinute: number = MAX_PER_MINUTE;
+ tokensHour: number = MAX_PER_HOUR;
+ lastUpdate = AbsoluteTime.now();
+
+ private refill(): void {
+ const now = AbsoluteTime.now();
+ if (AbsoluteTime.cmp(now, this.lastUpdate) < 0) {
+ // Did the system time change?
+ this.lastUpdate = now;
+ return;
+ }
+ const d = AbsoluteTime.difference(now, this.lastUpdate);
+ if (d.d_ms === "forever") {
+ throw Error("assertion failed");
+ }
+ this.tokensSecond = Math.min(
+ MAX_PER_SECOND,
+ this.tokensSecond + d.d_ms / 1000,
+ );
+ this.tokensMinute = Math.min(
+ MAX_PER_MINUTE,
+ this.tokensMinute + d.d_ms / 1000 / 60,
+ );
+ this.tokensHour = Math.min(
+ MAX_PER_HOUR,
+ this.tokensHour + d.d_ms / 1000 / 60 / 60,
+ );
+ this.lastUpdate = now;
+ }
+
+ /**
+ * Return true if the request for this origin should be throttled.
+ * Otherwise, take a token out of the respective buckets.
+ */
+ applyThrottle(): boolean {
+ this.refill();
+ if (this.tokensSecond < 1) {
+ logger.warn("request throttled (per second limit exceeded)");
+ return true;
+ }
+ if (this.tokensMinute < 1) {
+ logger.warn("request throttled (per minute limit exceeded)");
+ return true;
+ }
+ if (this.tokensHour < 1) {
+ logger.warn("request throttled (per hour limit exceeded)");
+ return true;
+ }
+ this.tokensSecond--;
+ this.tokensMinute--;
+ this.tokensHour--;
+ return false;
+ }
+}
+
+/**
+ * Request throttler, used as a "last layer of defense" when some
+ * other part of the re-try logic is broken and we're sending too
+ * many requests to the same exchange/bank/merchant.
+ */
+export class TaskThrottler {
+ private perTaskInfo: { [taskId: string]: TaskState } = {};
+
+ /**
+ * Get the throttling state for an origin, or
+ * initialize if no state is associated with the
+ * origin yet.
+ */
+ private getState(origin: string): TaskState {
+ const s = this.perTaskInfo[origin];
+ if (s) {
+ return s;
+ }
+ const ns = (this.perTaskInfo[origin] = new TaskState());
+ return ns;
+ }
+
+ /**
+ * Apply throttling to a request.
+ *
+ * @returns whether the request should be throttled.
+ */
+ applyThrottle(taskId: string): boolean {
+ for (let [k, v] of Object.entries(this.perTaskInfo)) {
+ // Remove throttled tasks that haven't seen an update in more than one hour.
+ if (
+ Duration.cmp(
+ AbsoluteTime.difference(v.lastUpdate, AbsoluteTime.now()),
+ Duration.fromSpec({ hours: 1 }),
+ ) > 1
+ ) {
+ delete this.perTaskInfo[k];
+ }
+ }
+ return this.getState(taskId).applyThrottle();
+ }
+
+ /**
+ * Get the throttle statistics for a particular URL.
+ */
+ getThrottleStats(taskId: string): Record<string, unknown> {
+ const state = this.getState(taskId);
+ return {
+ tokensHour: state.tokensHour,
+ tokensMinute: state.tokensMinute,
+ tokensSecond: state.tokensSecond,
+ maxTokensHour: MAX_PER_HOUR,
+ maxTokensMinute: MAX_PER_MINUTE,
+ maxTokensSecond: MAX_PER_SECOND,
+ };
+ }
+}
diff --git a/packages/taler-util/src/amounts.test.ts b/packages/taler-util/src/amounts.test.ts
index 064023e2d..449a6319a 100644
--- a/packages/taler-util/src/amounts.test.ts
+++ b/packages/taler-util/src/amounts.test.ts
@@ -17,6 +17,7 @@
import test from "ava";
import { Amounts, AmountJson, amountMaxValue } from "./amounts.js";
+import { AmountString } from "./taler-types.js";
const jAmt = (
value: number,
@@ -120,21 +121,71 @@ test("amount parsing", (t) => {
});
test("amount stringification", (t) => {
- t.is(Amounts.stringify(jAmt(0, 0, "TESTKUDOS")), "TESTKUDOS:0");
- t.is(Amounts.stringify(jAmt(4, 94000000, "TESTKUDOS")), "TESTKUDOS:4.94");
- t.is(Amounts.stringify(jAmt(0, 10000000, "TESTKUDOS")), "TESTKUDOS:0.1");
- t.is(Amounts.stringify(jAmt(0, 1, "TESTKUDOS")), "TESTKUDOS:0.00000001");
- t.is(Amounts.stringify(jAmt(5, 0, "TESTKUDOS")), "TESTKUDOS:5");
+ t.is(
+ Amounts.stringify(jAmt(0, 0, "TESTKUDOS")),
+ "TESTKUDOS:0" as AmountString,
+ );
+ t.is(
+ Amounts.stringify(jAmt(4, 94000000, "TESTKUDOS")),
+ "TESTKUDOS:4.94" as AmountString,
+ );
+ t.is(
+ Amounts.stringify(jAmt(0, 10000000, "TESTKUDOS")),
+ "TESTKUDOS:0.1" as AmountString,
+ );
+ t.is(
+ Amounts.stringify(jAmt(0, 1, "TESTKUDOS")),
+ "TESTKUDOS:0.00000001" as AmountString,
+ );
+ t.is(
+ Amounts.stringify(jAmt(5, 0, "TESTKUDOS")),
+ "TESTKUDOS:5" as AmountString,
+ );
// denormalized
- t.is(Amounts.stringify(jAmt(1, 100000000, "TESTKUDOS")), "TESTKUDOS:2");
+ t.is(
+ Amounts.stringify(jAmt(1, 100000000, "TESTKUDOS")),
+ "TESTKUDOS:2" as AmountString,
+ );
t.pass();
});
test("amount multiplication", (t) => {
- t.is(Amounts.stringify(Amounts.mult(sAmt("EUR:1.11"), 0).amount), "EUR:0");
- t.is(Amounts.stringify(Amounts.mult(sAmt("EUR:1.11"), 1).amount), "EUR:1.11");
- t.is(Amounts.stringify(Amounts.mult(sAmt("EUR:1.11"), 2).amount), "EUR:2.22");
- t.is(Amounts.stringify(Amounts.mult(sAmt("EUR:1.11"), 3).amount), "EUR:3.33");
- t.is(Amounts.stringify(Amounts.mult(sAmt("EUR:1.11"), 4).amount), "EUR:4.44");
- t.is(Amounts.stringify(Amounts.mult(sAmt("EUR:1.11"), 5).amount), "EUR:5.55");
+ t.is(
+ Amounts.stringify(Amounts.mult(sAmt("EUR:1.11"), 0).amount),
+ "EUR:0" as AmountString,
+ );
+ t.is(
+ Amounts.stringify(Amounts.mult(sAmt("EUR:1.11"), 1).amount),
+ "EUR:1.11" as AmountString,
+ );
+ t.is(
+ Amounts.stringify(Amounts.mult(sAmt("EUR:1.11"), 2).amount),
+ "EUR:2.22" as AmountString,
+ );
+ t.is(
+ Amounts.stringify(Amounts.mult(sAmt("EUR:1.11"), 3).amount),
+ "EUR:3.33" as AmountString,
+ );
+ t.is(
+ Amounts.stringify(Amounts.mult(sAmt("EUR:1.11"), 4).amount),
+ "EUR:4.44" as AmountString,
+ );
+ t.is(
+ Amounts.stringify(Amounts.mult(sAmt("EUR:1.11"), 5).amount),
+ "EUR:5.55" as AmountString,
+ );
+});
+
+test("amount division", (t) => {
+ t.is(Amounts.divmod("EUR:5", "EUR:1").quotient, 5);
+ t.is(
+ Amounts.stringify(Amounts.divmod("EUR:5", "EUR:1").remainder),
+ "EUR:0" as AmountString,
+ );
+
+ t.is(Amounts.divmod("EUR:5", "EUR:2").quotient, 2);
+ t.is(
+ Amounts.stringify(Amounts.divmod("EUR:5", "EUR:2").remainder),
+ "EUR:1" as AmountString,
+ );
});
diff --git a/packages/taler-util/src/amounts.ts b/packages/taler-util/src/amounts.ts
index 991b13912..82a3d3b68 100644
--- a/packages/taler-util/src/amounts.ts
+++ b/packages/taler-util/src/amounts.ts
@@ -22,11 +22,15 @@
* Imports.
*/
import {
+ Codec,
+ Context,
+ DecodingError,
buildCodecForObject,
- codecForString,
codecForNumber,
- Codec,
+ codecForString,
+ renderContext,
} from "./codec.js";
+import { CurrencySpecification } from "./index.js";
import { AmountString } from "./taler-types.js";
/**
@@ -47,6 +51,11 @@ export const amountFractionalLength = 8;
export const amountMaxValue = 2 ** 52;
/**
+ * Separator character between integer and fractional
+ */
+export const FRAC_SEPARATOR = ".";
+
+/**
* Non-negative financial amount. Fractional values are expressed as multiples
* of 1e-8.
*/
@@ -67,6 +76,48 @@ export interface AmountJson {
readonly currency: string;
}
+/**
+ * Immutable amount.
+ */
+export class Amount {
+ static from(a: AmountLike): Amount {
+ return new Amount(Amounts.parseOrThrow(a), 0);
+ }
+
+ static zeroOfCurrency(currency: string): Amount {
+ return new Amount(Amounts.zeroOfCurrency(currency), 0);
+ }
+
+ add(...a: AmountLike[]): Amount {
+ if (this.saturated) {
+ return this;
+ }
+ const r = Amounts.add(this.val, ...a);
+ return new Amount(r.amount, r.saturated ? 1 : 0);
+ }
+
+ mult(n: number): Amount {
+ if (this.saturated) {
+ return this;
+ }
+ const r = Amounts.mult(this, n);
+ return new Amount(r.amount, r.saturated ? 1 : 0);
+ }
+
+ toJson(): AmountJson {
+ return { ...this.val };
+ }
+
+ toString(): AmountString {
+ return Amounts.stringify(this.val);
+ }
+
+ private constructor(
+ private val: AmountJson,
+ private saturated: number,
+ ) {}
+}
+
export const codecForAmountJson = (): Codec<AmountJson> =>
buildCodecForObject<AmountJson>()
.property("currency", codecForString())
@@ -74,7 +125,23 @@ export const codecForAmountJson = (): Codec<AmountJson> =>
.property("fraction", codecForNumber())
.build("AmountJson");
-export const codecForAmountString = (): Codec<AmountString> => codecForString();
+export function codecForAmountString(): Codec<AmountString> {
+ return {
+ decode(x: any, c?: Context): AmountString {
+ if (typeof x !== "string") {
+ throw new DecodingError(
+ `expected string at ${renderContext(c)} but got ${typeof x}`,
+ );
+ }
+ if (Amounts.parse(x) === undefined) {
+ throw new DecodingError(
+ `invalid amount at ${renderContext(c)} got "${x}"`,
+ );
+ }
+ return x as AmountString;
+ },
+ };
+}
/**
* Result of a possibly overflowing operation.
@@ -93,7 +160,12 @@ export interface Result {
/**
* Type for things that are treated like amounts.
*/
-export type AmountLike = AmountString | AmountJson;
+export type AmountLike = string | AmountString | AmountJson | Amount;
+
+export interface DivmodResult {
+ quotient: number;
+ remainder: AmountJson;
+}
/**
* Helper class for dealing with amounts.
@@ -132,9 +204,37 @@ export class Amounts {
if (typeof amt === "string") {
return Amounts.parseOrThrow(amt);
}
+ if (amt instanceof Amount) {
+ return amt.toJson();
+ }
return amt;
}
+ static divmod(a1: AmountLike, a2: AmountLike): DivmodResult {
+ const am1 = Amounts.jsonifyAmount(a1);
+ const am2 = Amounts.jsonifyAmount(a2);
+ if (am1.currency != am2.currency) {
+ throw Error(`incompatible currency (${am1.currency} vs${am2.currency})`);
+ }
+
+ const x1 =
+ BigInt(am1.value) * BigInt(amountFractionalBase) + BigInt(am1.fraction);
+ const x2 =
+ BigInt(am2.value) * BigInt(amountFractionalBase) + BigInt(am2.fraction);
+
+ const quotient = x1 / x2;
+ const remainderScaled = x1 % x2;
+
+ return {
+ quotient: Number(quotient),
+ remainder: {
+ currency: am1.currency,
+ value: Number(remainderScaled / BigInt(amountFractionalBase)),
+ fraction: Number(remainderScaled % BigInt(amountFractionalBase)),
+ },
+ };
+ }
+
static sum(amounts: AmountLike[]): Result {
if (amounts.length <= 0) {
throw Error("can't sum zero amounts");
@@ -303,7 +403,8 @@ export class Amounts {
/**
* Check if an amount is non-zero.
*/
- static isNonZero(a: AmountJson): boolean {
+ static isNonZero(a: AmountLike): boolean {
+ a = Amounts.jsonifyAmount(a);
return a.value > 0 || a.fraction > 0;
}
@@ -313,14 +414,24 @@ export class Amounts {
}
/**
+ * Check whether a string is a valid currency for a Taler amount.
+ */
+ static isCurrency(s: string): boolean {
+ return /^[a-zA-Z]{1,11}$/.test(s);
+ }
+
+ /**
* Parse an amount like 'EUR:20.5' for 20 Euros and 50 ct.
+ *
+ * Currency name size limit is 11 of ASCII letters
+ * Fraction size limit is 8
*/
static parse(s: string): AmountJson | undefined {
- const res = s.match(/^([a-zA-Z0-9_*-]+):([0-9]+)([.][0-9]+)?$/);
+ const res = s.match(/^([a-zA-Z]{1,11}):([0-9]+)([.][0-9]{1,8})?$/);
if (!res) {
return undefined;
}
- const tail = res[3] || ".0";
+ const tail = res[3] || FRAC_SEPARATOR + "0";
if (tail.length > amountFractionalLength + 1) {
return undefined;
}
@@ -340,6 +451,9 @@ export class Amounts {
* throw if the input is not a valid amount.
*/
static parseOrThrow(s: AmountLike): AmountJson {
+ if (s instanceof Amount) {
+ return s.toJson();
+ }
if (typeof s === "object") {
if (typeof s.currency !== "string") {
throw Error("invalid amount object");
@@ -362,20 +476,6 @@ export class Amounts {
}
}
- /**
- * Convert a float to a Taler amount.
- * Loss of precision possible.
- */
- static fromFloat(floatVal: number, currency: string): AmountJson {
- return {
- currency,
- fraction: Math.floor(
- (floatVal - Math.floor(floatVal)) * amountFractionalBase,
- ),
- value: Math.floor(floatVal),
- };
- }
-
static min(a: AmountLike, b: AmountLike): AmountJson {
const cr = Amounts.cmp(a, b);
if (cr >= 0) {
@@ -397,7 +497,7 @@ export class Amounts {
static mult(a: AmountLike, n: number): Result {
a = this.jsonifyAmount(a);
if (!Number.isInteger(n)) {
- throw Error("amount can only be multipied by an integer");
+ throw Error("amount can only be multiplied by an integer");
}
if (n < 0) {
throw Error("amount can only be multiplied by a positive integer");
@@ -449,19 +549,23 @@ export class Amounts {
* Convert to standard human-readable string representation that's
* also used in JSON formats.
*/
- static stringify(a: AmountLike): string {
+ static stringify(a: AmountLike): AmountString {
a = Amounts.jsonifyAmount(a);
const s = this.stringifyValue(a);
- return `${a.currency}:${s}`;
+ return `${a.currency}:${s}` as AmountString;
}
- static isSameCurrency(a1: AmountLike, a2: AmountLike): boolean {
+ static amountHasSameCurrency(a1: AmountLike, a2: AmountLike): boolean {
const x1 = this.jsonifyAmount(a1);
const x2 = this.jsonifyAmount(a2);
return x1.currency.toUpperCase() === x2.currency.toUpperCase();
}
+ static isSameCurrency(curr1: string, curr2: string): boolean {
+ return curr1.toLowerCase() === curr2.toLowerCase();
+ }
+
static stringifyValue(a: AmountLike, minFractional = 0): string {
const aJ = Amounts.jsonifyAmount(a);
const av = aJ.value + Math.floor(aJ.fraction / amountFractionalBase);
@@ -469,7 +573,7 @@ export class Amounts {
let s = av.toString();
if (af || minFractional) {
- s = s + ".";
+ s = s + FRAC_SEPARATOR;
let n = af;
for (let i = 0; i < amountFractionalLength; i++) {
if (!n && i >= minFractional) {
@@ -504,4 +608,77 @@ export class Amounts {
}
return amountFractionalLength - i + 1;
}
+
+ static stringifyValueWithSpec(
+ value: AmountJson,
+ spec: CurrencySpecification,
+ ): { currency: string; normal: string; small?: string } {
+ const strValue = Amounts.stringifyValue(value);
+ const pos = strValue.indexOf(FRAC_SEPARATOR);
+ const originalPosition = pos < 0 ? strValue.length : pos;
+
+ let currency = value.currency;
+ const names = Object.keys(spec.alt_unit_names);
+ let FRAC_POS_NEW_POSITION = originalPosition;
+ //find symbol
+ //FIXME: this should be based on a cache to speed up
+ if (names.length > 0) {
+ let unitIndex: string = "0"; //default entry by DD51
+ names.forEach((index) => {
+ const i = Number.parseInt(index, 10);
+ if (Number.isNaN(i)) return; //skip
+ if (originalPosition - i <= 0) return; //too big
+ if (originalPosition - i < FRAC_POS_NEW_POSITION) {
+ FRAC_POS_NEW_POSITION = originalPosition - i;
+ unitIndex = index;
+ }
+ });
+ currency = spec.alt_unit_names[unitIndex];
+ }
+
+ if (originalPosition === FRAC_POS_NEW_POSITION) {
+ const { normal, small } = splitNormalAndSmall(
+ strValue,
+ originalPosition,
+ spec,
+ );
+ return { currency, normal, small };
+ }
+
+ const intPart = strValue.substring(0, originalPosition);
+ const fracPArt = strValue.substring(originalPosition + 1);
+ //indexSize is always smaller than originalPosition
+ const newValue =
+ intPart.substring(0, FRAC_POS_NEW_POSITION) +
+ FRAC_SEPARATOR +
+ intPart.substring(FRAC_POS_NEW_POSITION) +
+ fracPArt;
+ const { normal, small } = splitNormalAndSmall(
+ newValue,
+ FRAC_POS_NEW_POSITION,
+ spec,
+ );
+ return { currency, normal, small };
+ }
+}
+
+function splitNormalAndSmall(
+ decimal: string,
+ fracSeparatorIndex: number,
+ spec: CurrencySpecification,
+): { normal: string; small?: string } {
+ let normal: string;
+ let small: string | undefined;
+ if (
+ decimal.length - fracSeparatorIndex - 1 >
+ spec.num_fractional_normal_digits
+ ) {
+ const limit = fracSeparatorIndex + spec.num_fractional_normal_digits + 1;
+ normal = decimal.substring(0, limit);
+ small = decimal.substring(limit);
+ } else {
+ normal = decimal;
+ small = undefined;
+ }
+ return { normal, small };
}
diff --git a/packages/taler-util/src/argon2-impl.missing.ts b/packages/taler-util/src/argon2-impl.missing.ts
new file mode 100644
index 000000000..2e175bc75
--- /dev/null
+++ b/packages/taler-util/src/argon2-impl.missing.ts
@@ -0,0 +1,9 @@
+export async function HashArgon2idImpl(
+ password: Uint8Array,
+ salt: Uint8Array,
+ iterations: number,
+ memorySize: number,
+ hashLength: number,
+): Promise<Uint8Array> {
+ throw new Error("Method not implemented.");
+}
diff --git a/packages/taler-util/src/argon2-impl.wasm.ts b/packages/taler-util/src/argon2-impl.wasm.ts
new file mode 100644
index 000000000..d1a36c4fe
--- /dev/null
+++ b/packages/taler-util/src/argon2-impl.wasm.ts
@@ -0,0 +1,19 @@
+import { argon2id } from "hash-wasm";
+
+export async function HashArgon2idImpl(
+ password: Uint8Array,
+ salt: Uint8Array,
+ iterations: number,
+ memorySize: number,
+ hashLength: number,
+): Promise<Uint8Array> {
+ return await argon2id({
+ password: password,
+ salt: salt,
+ iterations: iterations,
+ memorySize: memorySize,
+ hashLength: hashLength,
+ parallelism: 1,
+ outputType: "binary",
+ });
+}
diff --git a/packages/taler-util/src/argon2.ts b/packages/taler-util/src/argon2.ts
new file mode 100644
index 000000000..aebfb6962
--- /dev/null
+++ b/packages/taler-util/src/argon2.ts
@@ -0,0 +1,17 @@
+import * as impl from "#argon2-impl";
+
+export async function hashArgon2id(
+ password: Uint8Array,
+ salt: Uint8Array,
+ iterations: number,
+ memorySize: number,
+ hashLength: number,
+): Promise<Uint8Array> {
+ return await impl.HashArgon2idImpl(
+ password,
+ salt,
+ iterations,
+ memorySize,
+ hashLength,
+ );
+}
diff --git a/packages/taler-util/src/backup-types.ts b/packages/taler-util/src/backup-types.ts
index b3c6b5515..8c38b70a6 100644
--- a/packages/taler-util/src/backup-types.ts
+++ b/packages/taler-util/src/backup-types.ts
@@ -14,405 +14,14 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-/**
- * Type declarations for the backup content format.
- *
- * Contains some redundancy with the other type declarations,
- * as the backup schema must remain very stable and should be self-contained.
- *
- * Future:
- * 1. Ghost spends (coin unexpectedly spent by a wallet with shared data)
- * 2. Ghost withdrawals (reserve unexpectedly emptied by another wallet with shared data)
- * 3. Track losses through re-denomination of payments/refreshes
- * 4. (Feature:) Payments to own bank account and P2P-payments need to be backed up
- * 5. Track last/next update time, so on restore we need to do less work
- * 6. Currency render preferences?
- *
- * Questions:
- * 1. What happens when two backups are merged that have
- * the same coin in different refresh groups?
- * => Both are added, one will eventually fail
- * 2. Should we make more information forgettable? I.e. is
- * the coin selection still relevant for a purchase after the coins
- * are legally expired?
- * => Yes, still needs to be implemented
- * 3. What about re-denominations / re-selection of payment coins?
- * Is it enough to store a clock value for the selection?
- * => Coin derivation should also consider denom pub hash
- *
- * General considerations / decisions:
- * 1. Information about previously occurring errors and
- * retries is never backed up.
- * 2. The ToS text of an exchange is never backed up.
- * 3. Derived information is never backed up (hashed values, public keys
- * when we know the private key).
- *
- * Problems:
- *
- * Withdrawal group fork/merging loses money:
- * - Before the withdrawal happens, wallet forks into two backups.
- * - Both wallets need to re-denominate the withdrawal (unlikely but possible).
- * - Because the backup doesn't store planchets where a withdrawal was attempted,
- * after merging some money will be list.
- * - Fix: backup withdrawal objects also store planchets where withdrawal has been attempted
- *
- * @author Florian Dold <dold@taler.net>
- */
-
-/**
- * Imports.
- */
-import { DenominationPubKey, UnblindedSignature } from "./taler-types.js";
-import { TalerProtocolDuration, TalerProtocolTimestamp } from "./time.js";
-
-export const BACKUP_TAG = "gnu-taler-wallet-backup-content" as const;
-/**
- * Major version. Each increment means a backwards-incompatible change.
- * Typically this means that a custom converter needs to be written.
- */
-export const BACKUP_VERSION_MAJOR = 1 as const;
-
-/**
- * Minor version. Each increment means that information is added to the backup
- * in a backwards-compatible way.
- *
- * Wallets can always import a smaller minor version than their own backup code version.
- * When importing a bigger version, data loss is possible and the user should be urged to
- * upgrade their wallet first.
- */
-export const BACKUP_VERSION_MINOR = 1 as const;
-
-/**
- * Type alias for strings that are to be treated like amounts.
- */
-type BackupAmountString = string;
-
-/**
- * A human-recognizable identifier here that is
- * reasonable unique and assigned the first time the wallet is
- * started/installed, such as:
- *
- * `${wallet-implementation} ${os} ${hostname} (${short-uid})`
- * => e.g. "GNU Taler Android iceking ABC123"
- */
-type DeviceIdString = string;
-
-/**
- * Contract terms JSON.
- */
-type RawContractTerms = any;
-
-/**
- * Unique identifier for an operation, used to either (a) reference
- * the operation in a tombstone (b) disambiguate conflicting writes.
- */
-type OperationUid = string;
-
-/**
- * Content of the backup.
- *
- * The contents of the wallet must be serialized in a deterministic
- * way across implementations, so that the normalized backup content
- * JSON is identical when the wallet's content is identical.
- */
-export interface WalletBackupContentV1 {
- /**
- * Magic constant to identify that this is a backup content JSON.
- */
- schema_id: typeof BACKUP_TAG;
-
- /**
- * Version of the schema.
- */
- schema_version: typeof BACKUP_VERSION_MAJOR;
-
- minor_version: number;
-
- /**
- * Root public key of the wallet. This field is present as
- * a sanity check if the backup content JSON is loaded from file.
- */
- wallet_root_pub: string;
-
- /**
- * Current device identifier that "owns" the backup.
- *
- * This identifier allows one wallet to notice when another
- * wallet is "alive" and connected to the same sync provider.
- */
- current_device_id: DeviceIdString;
-
- /**
- * Timestamp of the backup.
- *
- * This timestamp should only be advanced if the content
- * of the backup changes.
- */
- timestamp: TalerProtocolTimestamp;
-
- /**
- * Per-exchange data sorted by exchange master public key.
- *
- * Sorted by the exchange public key.
- */
- exchanges: BackupExchange[];
-
- exchange_details: BackupExchangeDetails[];
-
- /**
- * Withdrawal groups.
- *
- * Sorted by the withdrawal group ID.
- */
- withdrawal_groups: BackupWithdrawalGroup[];
-
- /**
- * Grouped refresh sessions.
- *
- * Sorted by the refresh group ID.
- */
- refresh_groups: BackupRefreshGroup[];
-
- /**
- * Tips.
- *
- * Sorted by the wallet tip ID.
- */
- tips: BackupTip[];
-
- /**
- * Accepted purchases.
- *
- * Sorted by the proposal ID.
- */
- purchases: BackupPurchase[];
-
- /**
- * All backup providers. Backup providers
- * in this list should be considered "active".
- *
- * Sorted by the provider base URL.
- */
- backup_providers: BackupBackupProvider[];
+import { AmountString } from "./taler-types.js";
- /**
- * Recoup groups.
- */
- recoup_groups: BackupRecoupGroup[];
-
- /**
- * Trusted auditors, either for official (3 letter) or local (4-12 letter)
- * currencies.
- *
- * Auditors are sorted by their canonicalized base URL.
- */
- trusted_auditors: { [currency: string]: BackupTrustAuditor[] };
-
- /**
- * Trusted exchange. Only applicable for local currencies (4-12 letter currency code).
- *
- * Exchanges are sorted by their canonicalized base URL.
- */
- trusted_exchanges: { [currency: string]: BackupTrustExchange[] };
-
- /**
- * Interning table for forgettable values of contract terms.
- *
- * Used to reduce storage space, as many forgettable items (product image,
- * addresses, etc.) might be shared among many contract terms.
- */
- intern_table: { [hash: string]: any };
-
- /**
- * Permanent error reports.
- */
- error_reports: BackupErrorReport[];
-
- /**
- * Deletion tombstones. Lexically sorted.
- */
- tombstones: Tombstone[];
-}
-
-export enum BackupOperationStatus {
- Cancelled = "cancelled",
- Finished = "finished",
- Pending = "pending",
-}
-
-export enum BackupWgType {
- BankManual = "bank-manual",
- BankIntegrated = "bank-integrated",
- PeerPullCredit = "peer-pull-credit",
- PeerPushCredit = "peer-push-credit",
- Recoup = "recoup",
-}
-
-export type BackupWgInfo =
- | {
- type: BackupWgType.BankManual;
- }
- | {
- type: BackupWgType.BankIntegrated;
- taler_withdraw_uri: string;
-
- /**
- * URL that the user can be redirected to, and allows
- * them to confirm (or abort) the bank-integrated withdrawal.
- */
- confirm_url?: string;
-
- /**
- * Exchange payto URI that the bank will use to fund the reserve.
- */
- exchange_payto_uri: string;
-
- /**
- * Time when the information about this reserve was posted to the bank.
- *
- * Only applies if bankWithdrawStatusUrl is defined.
- *
- * Set to undefined if that hasn't happened yet.
- */
- timestamp_reserve_info_posted?: TalerProtocolTimestamp;
-
- /**
- * Time when the reserve was confirmed by the bank.
- *
- * Set to undefined if not confirmed yet.
- */
- timestamp_bank_confirmed?: TalerProtocolTimestamp;
- }
- | {
- type: BackupWgType.PeerPullCredit;
- contract_terms: any;
- contract_priv: string;
- }
- | {
- type: BackupWgType.PeerPushCredit;
- contract_terms: any;
- }
- | {
- type: BackupWgType.Recoup;
- };
-
-/**
- * FIXME: Open questions:
- * - Do we have to store the denomination selection? Why?
- * (If deterministic, amount shouldn't change. Not storing it is simpler.)
- */
-export interface BackupWithdrawalGroup {
- withdrawal_group_id: string;
-
- /**
- * Detailed info based on the type of withdrawal group.
- */
- info: BackupWgInfo;
-
- secret_seed: string;
-
- reserve_priv: string;
-
- exchange_base_url: string;
-
- timestamp_created: TalerProtocolTimestamp;
-
- timestamp_finish?: TalerProtocolTimestamp;
-
- operation_status: BackupOperationStatus;
-
- instructed_amount: BackupAmountString;
-
- /**
- * Amount including fees (i.e. the amount subtracted from the
- * reserve to withdraw all coins in this withdrawal session).
- *
- * Note that this *includes* the amount remaining in the reserve
- * that is too small to be withdrawn, and thus can't be derived
- * from selectedDenoms.
- */
- raw_withdrawal_amount: BackupAmountString;
-
- effective_withdrawal_amount: BackupAmountString;
-
- /**
- * Restrict withdrawals from this reserve to this age.
- */
- restrict_age?: number;
-
- /**
- * Multiset of denominations selected for withdrawal.
- */
- selected_denoms: BackupDenomSel;
-
- selected_denoms_uid: OperationUid;
-}
-
-/**
- * Tombstone in the format "<type>:<key>"
- */
-export type Tombstone = string;
-
-/**
- * Detailed error report.
- *
- * For auditor-relevant reports with attached cryptographic proof,
- * the error report also should contain the submission status to
- * the auditor(s).
- */
-interface BackupErrorReport {
- // FIXME: specify!
-}
-
-/**
- * Trust declaration for an auditor.
- *
- * The trust applies based on the public key of
- * the auditor, irrespective of what base URL the exchange
- * is referencing.
- */
-export interface BackupTrustAuditor {
- /**
- * Base URL of the auditor.
- */
- auditor_base_url: string;
-
- /**
- * Public key of the auditor.
- */
- auditor_pub: string;
-
- /**
- * UIDs for the operation of adding this auditor
- * as a trusted auditor.
- */
- uids: OperationUid;
-}
-
-/**
- * Trust declaration for an exchange.
- *
- * The trust only applies for the combination of base URL
- * and public key. If the master public key changes while the base
- * URL stays the same, the exchange has to be re-added by a wallet update
- * or by the user.
- */
-export interface BackupTrustExchange {
- /**
- * Canonicalized exchange base URL.
- */
- exchange_base_url: string;
-
- /**
- * Master public key of the exchange.
- */
- exchange_master_pub: string;
-
- /**
- * UIDs for the operation of adding this exchange
- * as trusted.
- */
- uids: OperationUid;
+export interface BackupRecovery {
+ walletRootPriv: string;
+ providers: {
+ name: string;
+ url: string;
+ }[];
}
export class BackupBackupProviderTerms {
@@ -424,862 +33,10 @@ export class BackupBackupProviderTerms {
/**
* Last known annual fee.
*/
- annual_fee: BackupAmountString;
+ annual_fee: AmountString;
/**
* Last known storage limit.
*/
storage_limit_in_megabytes: number;
}
-
-/**
- * Backup information about one backup storage provider.
- */
-export class BackupBackupProvider {
- /**
- * Canonicalized base URL of the provider.
- */
- base_url: string;
-
- /**
- * Last known terms. Might be unavailable in some situations, such
- * as directly after restoring form a backup recovery document.
- */
- terms?: BackupBackupProviderTerms;
-
- /**
- * Proposal IDs for payments to this provider.
- */
- pay_proposal_ids: string[];
-
- /**
- * UIDs for adding this backup provider.
- */
- uids: OperationUid[];
-}
-
-/**
- * Status of recoup operations that were grouped together.
- *
- * The remaining amount of the corresponding coins must be set to
- * zero when the recoup group is created/imported.
- */
-export interface BackupRecoupGroup {
- /**
- * Unique identifier for the recoup group record.
- */
- recoup_group_id: string;
-
- /**
- * Timestamp when the recoup was started.
- */
- timestamp_created: TalerProtocolTimestamp;
-
- timestamp_finish?: TalerProtocolTimestamp;
- finish_clock?: TalerProtocolTimestamp;
- // FIXME: Use some enum here!
- finish_is_failure?: boolean;
-
- /**
- * Information about each coin being recouped.
- */
- coins: {
- coin_pub: string;
- recoup_finished: boolean;
- }[];
-}
-
-/**
- * Types of coin sources.
- */
-export enum BackupCoinSourceType {
- Withdraw = "withdraw",
- Refresh = "refresh",
- Tip = "tip",
-}
-
-/**
- * Metadata about a coin obtained via withdrawing.
- */
-export interface BackupWithdrawCoinSource {
- type: BackupCoinSourceType.Withdraw;
-
- /**
- * Can be the empty string for orphaned coins.
- */
- withdrawal_group_id: string;
-
- /**
- * Index of the coin in the withdrawal session.
- */
- coin_index: number;
-
- /**
- * Reserve public key for the reserve we got this coin from.
- */
- reserve_pub: string;
-}
-
-/**
- * Metadata about a coin obtained from refreshing.
- *
- * FIXME: Currently does not link to the refreshGroupId because
- * the wallet DB doesn't do this. Not really necessary,
- * but would be more consistent.
- */
-export interface BackupRefreshCoinSource {
- type: BackupCoinSourceType.Refresh;
-
- /**
- * Public key of the coin that was refreshed into this coin.
- */
- old_coin_pub: string;
-
- refresh_group_id: string;
-}
-
-/**
- * Metadata about a coin obtained from a tip.
- */
-export interface BackupTipCoinSource {
- type: BackupCoinSourceType.Tip;
-
- /**
- * Wallet's identifier for the tip that this coin
- * originates from.
- */
- wallet_tip_id: string;
-
- /**
- * Index in the tip planchets of the tip.
- */
- coin_index: number;
-}
-
-/**
- * Metadata about a coin depending on the origin.
- */
-export type BackupCoinSource =
- | BackupWithdrawCoinSource
- | BackupRefreshCoinSource
- | BackupTipCoinSource;
-
-/**
- * Backup information about a coin.
- *
- * (Always part of a BackupExchange/BackupDenom)
- */
-export interface BackupCoin {
- /**
- * Where did the coin come from? Used for recouping coins.
- */
- coin_source: BackupCoinSource;
-
- /**
- * Private key to authorize operations on the coin.
- */
- coin_priv: string;
-
- /**
- * Unblinded signature by the exchange.
- */
- denom_sig: UnblindedSignature;
-
- /**
- * Information about where and how the coin was spent.
- */
- spend_allocation:
- | {
- id: string;
- amount: BackupAmountString;
- }
- | undefined;
-
- /**
- * Blinding key used when withdrawing the coin.
- * Potentionally used again during payback.
- */
- blinding_key: string;
-
- /**
- * Does the wallet think that the coin is still fresh?
- *
- * Note that even if a fresh coin is imported, it should still
- * be refreshed in most situations.
- */
- fresh: boolean;
-}
-
-/**
- * Status of a tip we got from a merchant.
- */
-export interface BackupTip {
- /**
- * Tip ID chosen by the wallet.
- */
- wallet_tip_id: string;
-
- /**
- * The merchant's identifier for this tip.
- */
- merchant_tip_id: string;
-
- /**
- * Secret seed used for the tipping planchets.
- */
- secret_seed: string;
-
- /**
- * Has the user accepted the tip? Only after the tip has been accepted coins
- * withdrawn from the tip may be used.
- */
- timestamp_accepted: TalerProtocolTimestamp | undefined;
-
- /**
- * When was the tip first scanned by the wallet?
- */
- timestamp_created: TalerProtocolTimestamp;
-
- timestamp_finished?: TalerProtocolTimestamp;
- finish_is_failure?: boolean;
-
- /**
- * The tipped amount.
- */
- tip_amount_raw: BackupAmountString;
-
- /**
- * Timestamp, the tip can't be picked up anymore after this deadline.
- */
- timestamp_expiration: TalerProtocolTimestamp;
-
- /**
- * The exchange that will sign our coins, chosen by the merchant.
- */
- exchange_base_url: string;
-
- /**
- * Base URL of the merchant that is giving us the tip.
- */
- merchant_base_url: string;
-
- /**
- * Selected denominations. Determines the effective tip amount.
- */
- selected_denoms: BackupDenomSel;
-
- /**
- * UID for the denomination selection.
- * Used to disambiguate when merging.
- */
- selected_denoms_uid: OperationUid;
-}
-
-/**
- * Reasons for why a coin is being refreshed.
- */
-export enum BackupRefreshReason {
- Manual = "manual",
- Pay = "pay",
- Refund = "refund",
- AbortPay = "abort-pay",
- Recoup = "recoup",
- BackupRestored = "backup-restored",
- Scheduled = "scheduled",
-}
-
-/**
- * Information about one refresh session, always part
- * of a refresh group.
- *
- * (Public key of the old coin is stored in the refresh group.)
- */
-export interface BackupRefreshSession {
- /**
- * Hashed denominations of the newly requested coins.
- */
- new_denoms: BackupDenomSel;
-
- /**
- * Seed used to derive the planchets and
- * transfer private keys for this refresh session.
- */
- session_secret_seed: string;
-
- /**
- * The no-reveal-index after we've done the melting.
- */
- noreveal_index?: number;
-}
-
-/**
- * Refresh session for one coin inside a refresh group.
- */
-export interface BackupRefreshOldCoin {
- /**
- * Public key of the old coin,
- */
- coin_pub: string;
-
- /**
- * Requested amount to refresh. Must be subtracted from the coin's remaining
- * amount as soon as the coin is added to the refresh group.
- */
- input_amount: BackupAmountString;
-
- /**
- * Estimated output (may change if it takes a long time to create the
- * actual session).
- */
- estimated_output_amount: BackupAmountString;
-
- /**
- * Did the refresh session finish (or was it unnecessary/impossible to create
- * one)
- */
- finished: boolean;
-
- /**
- * Refresh session (if created) or undefined it not created yet.
- */
- refresh_session: BackupRefreshSession | undefined;
-}
-
-/**
- * Information about one refresh group.
- *
- * May span more than one exchange, but typically doesn't
- */
-export interface BackupRefreshGroup {
- refresh_group_id: string;
-
- reason: BackupRefreshReason;
-
- /**
- * Details per old coin.
- */
- old_coins: BackupRefreshOldCoin[];
-
- timestamp_created: TalerProtocolTimestamp;
-
- timestamp_finish?: TalerProtocolTimestamp;
- finish_is_failure?: boolean;
-}
-
-export enum BackupRefundState {
- Failed = "failed",
- Applied = "applied",
- Pending = "pending",
-}
-
-/**
- * Common information about a refund.
- */
-export interface BackupRefundItemCommon {
- /**
- * Execution time as claimed by the merchant
- */
- execution_time: TalerProtocolTimestamp;
-
- /**
- * Time when the wallet became aware of the refund.
- */
- obtained_time: TalerProtocolTimestamp;
-
- /**
- * Amount refunded for the coin.
- */
- refund_amount: BackupAmountString;
-
- /**
- * Coin being refunded.
- */
- coin_pub: string;
-
- /**
- * The refund transaction ID for the refund.
- */
- rtransaction_id: number;
-
- /**
- * Upper bound on the refresh cost incurred by
- * applying this refund.
- *
- * Might be lower in practice when two refunds on the same
- * coin are refreshed in the same refresh operation.
- *
- * Used to display fees, and stored since it's expensive to recompute
- * accurately.
- */
- total_refresh_cost_bound: BackupAmountString;
-}
-
-/**
- * Failed refund, either because the merchant did
- * something wrong or it expired.
- */
-export interface BackupRefundFailedItem extends BackupRefundItemCommon {
- type: BackupRefundState.Failed;
-}
-
-export interface BackupRefundPendingItem extends BackupRefundItemCommon {
- type: BackupRefundState.Pending;
-}
-
-export interface BackupRefundAppliedItem extends BackupRefundItemCommon {
- type: BackupRefundState.Applied;
-}
-
-/**
- * State of one refund from the merchant, maintained by the wallet.
- */
-export type BackupRefundItem =
- | BackupRefundFailedItem
- | BackupRefundPendingItem
- | BackupRefundAppliedItem;
-
-/**
- * Data we store when the payment was accepted.
- */
-export interface BackupPayInfo {
- pay_coins: {
- /**
- * Public keys of the coins that were selected.
- */
- coin_pub: string;
-
- /**
- * Amount that each coin contributes.
- */
- contribution: BackupAmountString;
- }[];
-
- /**
- * Unique ID to disambiguate pay coin selection on merge.
- */
- pay_coins_uid: OperationUid;
-
- /**
- * Total cost initially shown to the user.
- *
- * This includes the amount taken by the merchant, fees (wire/deposit) contributed
- * by the customer, refreshing fees, fees for withdraw-after-refresh and "trimmings"
- * of coins that are too small to spend.
- *
- * Note that in rare situations, this cost might not be accurate (e.g.
- * when the payment or refresh gets re-denominated).
- * We might show adjustments to this later, but currently we don't do so.
- */
- total_pay_cost: BackupAmountString;
-}
-
-export interface BackupPurchase {
- /**
- * Proposal ID for this purchase. Uniquely identifies the
- * purchase and the proposal.
- */
- proposal_id: string;
-
- /**
- * Status of the proposal.
- */
- proposal_status: BackupProposalStatus;
-
- /**
- * Proposal that this one got "redirected" to as part of
- * the repurchase detection.
- */
- repurchase_proposal_id: string | undefined;
-
- /**
- * Session ID we got when downloading the contract.
- */
- download_session_id?: string;
-
- /**
- * Merchant-assigned order ID of the proposal.
- */
- order_id: string;
-
- /**
- * Base URL of the merchant that proposed the purchase.
- */
- merchant_base_url: string;
-
- /**
- * Claim token initially given by the merchant.
- */
- claim_token: string | undefined;
-
- /**
- * Contract terms we got from the merchant.
- */
- contract_terms_raw?: RawContractTerms;
-
- /**
- * Signature on the contract terms.
- *
- * FIXME: Better name needed.
- */
- merchant_sig?: string;
-
- /**
- * Private key for the nonce. Might eventually be used
- * to prove ownership of the contract.
- */
- nonce_priv: string;
-
- pay_info: BackupPayInfo | undefined;
-
- /**
- * Timestamp of the first time that sending a payment to the merchant
- * for this purchase was successful.
- */
- timestamp_first_successful_pay: TalerProtocolTimestamp | undefined;
-
- /**
- * Signature by the merchant confirming the payment.
- */
- merchant_pay_sig: string | undefined;
-
- timestamp_proposed: TalerProtocolTimestamp;
-
- /**
- * When was the purchase made?
- * Refers to the time that the user accepted.
- */
- timestamp_accepted: TalerProtocolTimestamp | undefined;
-
- /**
- * Pending refunds for the purchase. A refund is pending
- * when the merchant reports a transient error from the exchange.
- */
- refunds: BackupRefundItem[];
-
- /**
- * Continue querying the refund status until this deadline has expired.
- */
- auto_refund_deadline: TalerProtocolTimestamp | undefined;
-}
-
-/**
- * Info about one denomination in the backup.
- *
- * Note that the wallet only backs up validated denominations.
- */
-export interface BackupDenomination {
- /**
- * Value of one coin of the denomination.
- */
- value: BackupAmountString;
-
- /**
- * The denomination public key.
- */
- denom_pub: DenominationPubKey;
-
- /**
- * Fee for withdrawing.
- */
- fee_withdraw: BackupAmountString;
-
- /**
- * Fee for depositing.
- */
- fee_deposit: BackupAmountString;
-
- /**
- * Fee for refreshing.
- */
- fee_refresh: BackupAmountString;
-
- /**
- * Fee for refunding.
- */
- fee_refund: BackupAmountString;
-
- /**
- * Validity start date of the denomination.
- */
- stamp_start: TalerProtocolTimestamp;
-
- /**
- * Date after which the currency can't be withdrawn anymore.
- */
- stamp_expire_withdraw: TalerProtocolTimestamp;
-
- /**
- * Date after the denomination officially doesn't exist anymore.
- */
- stamp_expire_legal: TalerProtocolTimestamp;
-
- /**
- * Data after which coins of this denomination can't be deposited anymore.
- */
- stamp_expire_deposit: TalerProtocolTimestamp;
-
- /**
- * Signature by the exchange's master key over the denomination
- * information.
- */
- master_sig: string;
-
- /**
- * Was this denomination still offered by the exchange the last time
- * we checked?
- * Only false when the exchange redacts a previously published denomination.
- */
- is_offered: boolean;
-
- /**
- * Did the exchange revoke the denomination?
- * When this field is set to true in the database, the same transaction
- * should also mark all affected coins as revoked.
- */
- is_revoked: boolean;
-
- /**
- * Coins of this denomination.
- */
- coins: BackupCoin[];
-
- /**
- * The list issue date of the exchange "/keys" response
- * that this denomination was last seen in.
- */
- list_issue_date: TalerProtocolTimestamp;
-}
-
-/**
- * Denomination selection.
- */
-export type BackupDenomSel = {
- denom_pub_hash: string;
- count: number;
-}[];
-
-/**
- * Wire fee for one wire payment target type as stored in the
- * wallet's database.
- *
- * (Flattened to a list to make the declaration simpler).
- */
-export interface BackupExchangeWireFee {
- wire_type: string;
-
- /**
- * Fee for wire transfers.
- */
- wire_fee: string;
-
- /**
- * Fees to close and refund a reserve.
- */
- closing_fee: string;
-
- /**
- * Start date of the fee.
- */
- start_stamp: TalerProtocolTimestamp;
-
- /**
- * End date of the fee.
- */
- end_stamp: TalerProtocolTimestamp;
-
- /**
- * Signature made by the exchange master key.
- */
- sig: string;
-}
-
-/**
- * Global fee as stored in the wallet's database.
- *
- */
-export interface BackupExchangeGlobalFees {
- startDate: TalerProtocolTimestamp;
- endDate: TalerProtocolTimestamp;
-
- historyFee: BackupAmountString;
- accountFee: BackupAmountString;
- purseFee: BackupAmountString;
-
- historyTimeout: TalerProtocolDuration;
- purseTimeout: TalerProtocolDuration;
-
- purseLimit: number;
-
- signature: string;
-}
-/**
- * Structure of one exchange signing key in the /keys response.
- */
-export class BackupExchangeSignKey {
- stamp_start: TalerProtocolTimestamp;
- stamp_expire: TalerProtocolTimestamp;
- stamp_end: TalerProtocolTimestamp;
- key: string;
- master_sig: string;
-}
-
-/**
- * Signature by the auditor that a particular denomination key is audited.
- */
-export class BackupAuditorDenomSig {
- /**
- * Denomination public key's hash.
- */
- denom_pub_h: string;
-
- /**
- * The signature.
- */
- auditor_sig: string;
-}
-
-/**
- * Auditor information as given by the exchange in /keys.
- */
-export class BackupExchangeAuditor {
- /**
- * Auditor's public key.
- */
- auditor_pub: string;
-
- /**
- * Base URL of the auditor.
- */
- auditor_url: string;
-
- /**
- * List of signatures for denominations by the auditor.
- */
- denomination_keys: BackupAuditorDenomSig[];
-}
-
-/**
- * Backup information for an exchange. Serves effectively
- * as a pointer to the exchange details identified by
- * the base URL, master public key and currency.
- */
-export interface BackupExchange {
- base_url: string;
-
- master_public_key: string;
-
- currency: string;
-
- /**
- * Time when the pointer to the exchange details
- * was last updated.
- *
- * Used to facilitate automatic merging.
- */
- update_clock: TalerProtocolTimestamp;
-}
-
-/**
- * Backup information about an exchange's details.
- *
- * Note that one base URL can have multiple exchange
- * details. The BackupExchange stores a pointer
- * to the current exchange details.
- */
-export interface BackupExchangeDetails {
- /**
- * Canonicalized base url of the exchange.
- */
- base_url: string;
-
- /**
- * Master public key of the exchange.
- */
- master_public_key: string;
-
- /**
- * Auditors (partially) auditing the exchange.
- */
- auditors: BackupExchangeAuditor[];
-
- /**
- * Currency that the exchange offers.
- */
- currency: string;
-
- /**
- * Denominations offered by the exchange.
- */
- denominations: BackupDenomination[];
-
- /**
- * Last observed protocol version.
- */
- protocol_version: string;
-
- /**
- * Closing delay of reserves.
- */
- reserve_closing_delay: TalerProtocolDuration;
-
- /**
- * Signing keys we got from the exchange, can also contain
- * older signing keys that are not returned by /keys anymore.
- */
- signing_keys: BackupExchangeSignKey[];
-
- wire_fees: BackupExchangeWireFee[];
-
- global_fees: BackupExchangeGlobalFees[];
-
- /**
- * Bank accounts offered by the exchange;
- */
- accounts: {
- payto_uri: string;
- master_sig: string;
- }[];
-
- /**
- * ETag for last terms of service download.
- */
- tos_accepted_etag: string | undefined;
-
- /**
- * Timestamp when the ToS has been accepted.
- */
- tos_accepted_timestamp: TalerProtocolTimestamp | undefined;
-}
-
-export enum BackupProposalStatus {
- /**
- * Proposed (and either downloaded or not,
- * depending on whether contract terms are present),
- * but the user needs to accept/reject it.
- */
- Proposed = "proposed",
- /**
- * The user has rejected the proposal.
- */
- Refused = "refused",
- /**
- * Downloading or processing the proposal has failed permanently.
- *
- * FIXME: Should this be modeled as a "misbehavior report" instead?
- */
- PermanentlyFailed = "permanently-failed",
- /**
- * Downloaded proposal was detected as a re-purchase.
- */
- Repurchase = "repurchase",
-
- Paid = "paid",
-}
-
-export interface BackupRecovery {
- walletRootPriv: string;
- providers: {
- name: string;
- url: string;
- }[];
-}
diff --git a/packages/taler-util/src/bank-api-client.ts b/packages/taler-util/src/bank-api-client.ts
new file mode 100644
index 000000000..51359129d
--- /dev/null
+++ b/packages/taler-util/src/bank-api-client.ts
@@ -0,0 +1,440 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Client for the Taler (demo-)bank.
+ */
+
+/**
+ * Imports.
+ */
+import {
+ AmountString,
+ base64FromArrayBuffer,
+ buildCodecForObject,
+ Codec,
+ codecForAny,
+ codecForString,
+ encodeCrock,
+ getRandomBytes,
+ HttpStatusCode,
+ j2s,
+ Logger,
+ opEmptySuccess,
+ opKnownHttpFailure,
+ opUnknownFailure,
+ stringToBytes,
+ TalerError,
+ TalerErrorCode,
+} from "@gnu-taler/taler-util";
+import {
+ checkSuccessResponseOrThrow,
+ createPlatformHttpLib,
+ HttpRequestLibrary,
+ readSuccessResponseJsonOrThrow,
+ readTalerErrorResponse,
+} from "@gnu-taler/taler-util/http";
+
+const logger = new Logger("bank-api-client.ts");
+
+export enum CreditDebitIndicator {
+ Credit = "credit",
+ Debit = "debit",
+}
+
+export interface BankAccountBalanceResponse {
+ balance: {
+ amount: AmountString;
+ credit_debit_indicator: CreditDebitIndicator;
+ };
+}
+
+export interface BankUser {
+ username: string;
+ password: string;
+ accountPaytoUri: string;
+}
+
+export interface WithdrawalOperationInfo {
+ withdrawal_id: string;
+ taler_withdraw_uri: string;
+}
+
+/**
+ * Helper function to generate the "Authorization" HTTP header.
+ */
+function makeBasicAuthHeader(username: string, password: string): string {
+ const auth = `${username}:${password}`;
+ const authEncoded: string = base64FromArrayBuffer(stringToBytes(auth));
+ return `Basic ${authEncoded}`;
+}
+
+const codecForWithdrawalOperationInfo = (): Codec<WithdrawalOperationInfo> =>
+ buildCodecForObject<WithdrawalOperationInfo>()
+ .property("withdrawal_id", codecForString())
+ .property("taler_withdraw_uri", codecForString())
+ .build("WithdrawalOperationInfo");
+
+export interface BankAccessApiClientArgs {
+ auth?: { username: string; password: string };
+ httpClient?: HttpRequestLibrary;
+}
+
+export interface BankAccessApiCreateTransactionRequest {
+ amount: AmountString;
+ paytoUri: string;
+}
+
+export class WireGatewayApiClientArgs {
+ auth?: {
+ username: string;
+ password: string;
+ };
+ httpClient?: HttpRequestLibrary;
+}
+
+/**
+ * This API look like it belongs to harness
+ * but it will be nice to have in utils to be used by others
+ */
+export class WireGatewayApiClient {
+ httpLib;
+
+ constructor(
+ private baseUrl: string,
+ private args: WireGatewayApiClientArgs = {},
+ ) {
+ this.httpLib = args.httpClient ?? createPlatformHttpLib();
+ }
+
+ private makeAuthHeader(): Record<string, string> {
+ const auth = this.args.auth;
+ if (auth) {
+ return {
+ Authorization: makeBasicAuthHeader(auth.username, auth.password),
+ };
+ }
+ return {};
+ }
+
+ async adminAddIncoming(params: {
+ amount: string;
+ reservePub: string;
+ debitAccountPayto: string;
+ }): Promise<void> {
+ let url = new URL(`admin/add-incoming`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body: {
+ amount: params.amount,
+ reserve_pub: params.reservePub,
+ debit_account: params.debitAccountPayto,
+ },
+ headers: this.makeAuthHeader(),
+ });
+ logger.info(`add-incoming response status: ${resp.status}`);
+ await checkSuccessResponseOrThrow(resp);
+ }
+}
+
+export interface ChallengeContactData {
+ // E-Mail address
+ email?: string;
+
+ // Phone number.
+ phone?: string;
+}
+
+export interface AccountBalance {
+ amount: AmountString;
+ credit_debit_indicator: "credit" | "debit";
+}
+
+export interface RegisterAccountRequest {
+ // Username
+ username: string;
+
+ // Password.
+ password: string;
+
+ // Legal name of the account owner
+ name: string;
+
+ // Defaults to false.
+ is_public?: boolean;
+
+ // Is this a taler exchange account?
+ // If true:
+ // - incoming transactions to the account that do not
+ // have a valid reserve public key are automatically
+ // - the account provides the taler-wire-gateway-api endpoints
+ // Defaults to false.
+ is_taler_exchange?: boolean;
+
+ // Addresses where to send the TAN for transactions.
+ // Currently only used for cashouts.
+ // If missing, cashouts will fail.
+ // In the future, might be used for other transactions
+ // as well.
+ challenge_contact_data?: ChallengeContactData;
+
+ // 'payto' address pointing a bank account
+ // external to the libeufin-bank.
+ // Payments will be sent to this bank account
+ // when the user wants to convert the local currency
+ // back to fiat currency outside libeufin-bank.
+ cashout_payto_uri?: string;
+
+ // Internal payto URI of this bank account.
+ // Used mostly for testing.
+ payto_uri?: string;
+}
+
+export interface AccountData {
+ // Legal name of the account owner.
+ name: string;
+
+ // Available balance on the account.
+ balance: AccountBalance;
+
+ // payto://-URI of the account.
+ payto_uri: string;
+
+ // Number indicating the max debit allowed for the requesting user.
+ debit_threshold: AmountString;
+
+ contact_data?: ChallengeContactData;
+
+ // 'payto' address pointing the bank account
+ // where to send cashouts. This field is optional
+ // because not all the accounts are required to participate
+ // in the merchants' circuit. One example is the exchange:
+ // that never cashouts. Registering these accounts can
+ // be done via the access API.
+ cashout_payto_uri?: string;
+}
+
+export interface ConfirmWithdrawalArgs {
+ withdrawalOperationId: string;
+}
+
+/**
+ * Client for the Taler corebank API.
+ */
+export class TalerCorebankApiClient {
+ httpLib: HttpRequestLibrary;
+
+ constructor(
+ private baseUrl: string,
+ private args: BankAccessApiClientArgs = {},
+ ) {
+ this.httpLib = args.httpClient ?? createPlatformHttpLib();
+ }
+
+ setAuth(auth: { username: string; password: string }) {
+ this.args.auth = auth;
+ }
+
+ private makeAuthHeader(): Record<string, string> {
+ if (!this.args.auth) {
+ return {};
+ }
+ const authHeaderValue = makeBasicAuthHeader(
+ this.args.auth.username,
+ this.args.auth.password,
+ );
+ return {
+ Authorization: authHeaderValue,
+ };
+ }
+
+ async getAccountBalance(
+ username: string,
+ ): Promise<BankAccountBalanceResponse> {
+ const url = new URL(`accounts/${username}`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ headers: this.makeAuthHeader(),
+ });
+ return readSuccessResponseJsonOrThrow(resp, codecForAny());
+ }
+
+ async getTransactions(username: string): Promise<void> {
+ const reqUrl = new URL(`accounts/${username}/transactions`, this.baseUrl);
+ const resp = await this.httpLib.fetch(reqUrl.href, {
+ method: "GET",
+ headers: {
+ ...this.makeAuthHeader(),
+ },
+ });
+
+ const res = await readSuccessResponseJsonOrThrow(resp, codecForAny());
+ logger.info(`result: ${j2s(res)}`);
+ }
+
+ async createTransaction(
+ username: string,
+ req: BankAccessApiCreateTransactionRequest,
+ ): Promise<any> {
+ const reqUrl = new URL(`accounts/${username}/transactions`, this.baseUrl);
+
+ const resp = await this.httpLib.fetch(reqUrl.href, {
+ method: "POST",
+ body: req,
+ headers: this.makeAuthHeader(),
+ });
+
+ return await readSuccessResponseJsonOrThrow(resp, codecForAny());
+ }
+
+ async registerAccountExtended(req: RegisterAccountRequest): Promise<void> {
+ const url = new URL("accounts", this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body: req,
+ headers: this.makeAuthHeader(),
+ });
+
+ if (
+ resp.status !== 200 &&
+ resp.status !== 201 &&
+ resp.status !== 202 &&
+ resp.status !== 204
+ ) {
+ logger.error(`unexpected status ${resp.status} from POST ${url.href}`);
+ logger.error(`${j2s(await resp.json())}`);
+ throw TalerError.fromDetail(
+ TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR,
+ {
+ httpStatusCode: resp.status,
+ },
+ );
+ }
+ }
+
+ /**
+ * Register a new account and return information about it.
+ *
+ * This is a helper, as it does both the registration and the
+ * account info query.
+ */
+ async registerAccount(username: string, password: string): Promise<BankUser> {
+ const url = new URL("accounts", this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body: {
+ username,
+ password,
+ name: username,
+ },
+ headers: this.makeAuthHeader(),
+ });
+ if (
+ resp.status !== 200 &&
+ resp.status !== 201 &&
+ resp.status !== 202 &&
+ resp.status !== 204
+ ) {
+ logger.error(`unexpected status ${resp.status} from POST ${url.href}`);
+ logger.error(`${j2s(await resp.json())}`);
+ throw TalerError.fromDetail(
+ TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR,
+ {
+ httpStatusCode: resp.status,
+ },
+ );
+ }
+ // FIXME: Corebank should directly return this info!
+ const infoUrl = new URL(`accounts/${username}`, this.baseUrl);
+ const infoResp = await this.httpLib.fetch(infoUrl.href, {
+ headers: {
+ Authorization: makeBasicAuthHeader(username, password),
+ },
+ });
+ // FIXME: Validate!
+ const acctInfo: AccountData = await readSuccessResponseJsonOrThrow(
+ infoResp,
+ codecForAny(),
+ );
+ return {
+ password,
+ username,
+ accountPaytoUri: acctInfo.payto_uri,
+ };
+ }
+
+ async createRandomBankUser(): Promise<BankUser> {
+ const username = "user-" + encodeCrock(getRandomBytes(10)).toLowerCase();
+ const password = "pw-" + encodeCrock(getRandomBytes(10)).toLowerCase();
+ return await this.registerAccount(username, password);
+ }
+
+ async createWithdrawalOperation(
+ user: string,
+ amount: string,
+ ): Promise<WithdrawalOperationInfo> {
+ const url = new URL(`accounts/${user}/withdrawals`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body: {
+ amount,
+ },
+ headers: this.makeAuthHeader(),
+ });
+ return readSuccessResponseJsonOrThrow(
+ resp,
+ codecForWithdrawalOperationInfo(),
+ );
+ }
+
+ async confirmWithdrawalOperation(
+ username: string,
+ wopi: ConfirmWithdrawalArgs,
+ ) {
+ const url = new URL(
+ `accounts/${username}/withdrawals/${wopi.withdrawalOperationId}/confirm`,
+ this.baseUrl,
+ );
+ logger.info(`confirming withdrawal operation via ${url.href}`);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body: {},
+ headers: this.makeAuthHeader(),
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ case HttpStatusCode.NoContent:
+ return opEmptySuccess(resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ async abortWithdrawalOperation(wopi: WithdrawalOperationInfo): Promise<void> {
+ const url = new URL(
+ `withdrawals/${wopi.withdrawal_id}/abort`,
+ this.baseUrl,
+ );
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body: {},
+ headers: this.makeAuthHeader(),
+ });
+ await readSuccessResponseJsonOrThrow(resp, codecForAny());
+ }
+}
diff --git a/packages/taler-util/src/bitcoin.ts b/packages/taler-util/src/bitcoin.ts
index 8c22ba522..37b7ae6b9 100644
--- a/packages/taler-util/src/bitcoin.ts
+++ b/packages/taler-util/src/bitcoin.ts
@@ -69,10 +69,10 @@ export function generateFakeSegwitAddress(
addr[0] === "t" && addr[1] == "b"
? "tb"
: addr[0] === "b" && addr[1] == "c" && addr[2] === "r" && addr[3] == "t"
- ? "bcrt"
- : addr[0] === "b" && addr[1] == "c"
- ? "bc"
- : undefined;
+ ? "bcrt"
+ : addr[0] === "b" && addr[1] == "c"
+ ? "bc"
+ : undefined;
if (prefix === undefined) throw new Error("unknown bitcoin net");
const addr1 = segwit.default.encode(prefix, 0, first_part);
diff --git a/packages/taler-util/src/clk.ts b/packages/taler-util/src/clk.ts
index e99ebf733..60969af69 100644
--- a/packages/taler-util/src/clk.ts
+++ b/packages/taler-util/src/clk.ts
@@ -17,16 +17,20 @@
/**
* Imports.
*/
-import process from "process";
-import path from "path";
-import readline from "readline";
-import { devNull } from "os";
+import {
+ processExit,
+ processArgv,
+ readlinePrompt,
+ pathBasename,
+} from "#compat-impl";
+import { AmountString } from "./taler-types.js";
export namespace clk {
class Converter<T> {}
export const INT = new Converter<number>();
export const STRING: Converter<string> = new Converter<string>();
+ export const AMOUNT: Converter<AmountString> = new Converter<AmountString>();
export interface OptionArgs<T> {
help?: string;
@@ -336,6 +340,8 @@ export namespace clk {
myArgs[def.name] = Number.parseInt(value);
} else if (def.conv == null || def.conv === STRING) {
myArgs[def.name] = value;
+ } else if (def.conv == null || def.conv === AMOUNT) {
+ myArgs[def.name] = value;
} else {
throw Error("unknown converter");
}
@@ -359,13 +365,13 @@ export namespace clk {
console.error(
`error: unknown option '--${r.key}' for ${currentName}`,
);
- process.exit(-1);
+ processExit(-1);
throw Error("not reached");
}
if (d.isFlag) {
if (r.value !== undefined) {
console.error(`error: flag '--${r.key}' does not take a value`);
- process.exit(-1);
+ processExit(-1);
throw Error("not reached");
}
storeFlag(d, true);
@@ -373,7 +379,7 @@ export namespace clk {
if (r.value === undefined) {
if (i === unparsedArgs.length - 1) {
console.error(`error: option '--${r.key}' needs an argument`);
- process.exit(-1);
+ processExit(-1);
throw Error("not reached");
}
storeOption(d, unparsedArgs[i + 1]);
@@ -391,7 +397,7 @@ export namespace clk {
const opt = this.shortOptions[chr];
if (!opt) {
console.error(`error: option '-${chr}' not known`);
- process.exit(-1);
+ processExit(-1);
}
if (opt.isFlag) {
storeFlag(opt, true);
@@ -399,7 +405,7 @@ export namespace clk {
if (si == optShort.length - 1) {
if (i === unparsedArgs.length - 1) {
console.error(`error: option '-${chr}' needs an argument`);
- process.exit(-1);
+ processExit(-1);
throw Error("not reached");
} else {
storeOption(opt, unparsedArgs[i + 1]);
@@ -418,7 +424,7 @@ export namespace clk {
const subcmd = this.subcommandMap[argVal];
if (!subcmd) {
console.error(`error: unknown command '${argVal}'`);
- process.exit(-1);
+ processExit(-1);
throw Error("not reached");
}
foundSubcommand = subcmd.commandGroup;
@@ -427,7 +433,7 @@ export namespace clk {
const d = this.arguments[posArgIndex];
if (!d) {
console.error(`error: too many arguments for ${currentName}`);
- process.exit(-1);
+ processExit(-1);
throw Error("not reached");
}
myArgs[d.name] = unparsedArgs[i];
@@ -437,7 +443,7 @@ export namespace clk {
if (parsedArgs[this.argKey].help) {
this.printHelp(progname, parents);
- process.exit(0);
+ processExit(0);
throw Error("not reached");
}
@@ -450,7 +456,7 @@ export namespace clk {
console.error(
`error: missing positional argument '${d.name}' for ${currentName}`,
);
- process.exit(-1);
+ processExit(-1);
throw Error("not reached");
}
}
@@ -464,7 +470,7 @@ export namespace clk {
} else {
const name = option.flagspec.join(",");
console.error(`error: missing option '${name}'`);
- process.exit(-1);
+ processExit(-1);
throw Error("not reached");
}
}
@@ -492,16 +498,16 @@ export namespace clk {
} catch (e) {
console.error(`An error occurred while running ${currentName}`);
console.error(e);
- process.exit(1);
+ processExit(1);
}
Promise.resolve(r).catch((e) => {
console.error(`An error occurred while running ${currentName}`);
console.error(e);
- process.exit(1);
+ processExit(1);
});
} else {
this.printHelp(progname, parents);
- process.exit(-1);
+ processExit(-1);
throw Error("not reached");
}
}
@@ -524,15 +530,15 @@ export namespace clk {
if (cmdlineArgs) {
args = cmdlineArgs;
} else {
- args = process.argv.slice(1);
+ args = processArgv().slice(1);
}
if (args.length < 1) {
console.error(
"Error while parsing command line arguments: not enough arguments",
);
- process.exit(-1);
+ processExit(-1);
}
- const progname = path.basename(args[0]);
+ const progname = pathBasename(args[0]);
const rest = args.slice(1);
this.mainCommand.run(progname, [], rest, {});
@@ -611,8 +617,8 @@ export namespace clk {
export type GetArgType<T> = T extends Program<any, infer AT>
? AT
: T extends CommandGroup<any, infer AT>
- ? AT
- : any;
+ ? AT
+ : any;
export function program<PN extends keyof any>(
argKey: PN,
@@ -622,15 +628,6 @@ export namespace clk {
}
export function prompt(question: string): Promise<string> {
- const stdinReadline = readline.createInterface({
- input: process.stdin,
- output: process.stdout,
- });
- return new Promise<string>((resolve, reject) => {
- stdinReadline.question(question, (res) => {
- resolve(res);
- stdinReadline.close();
- });
- });
+ return readlinePrompt(question);
}
}
diff --git a/packages/taler-util/src/codec.ts b/packages/taler-util/src/codec.ts
index 695f3c147..678c3f092 100644
--- a/packages/taler-util/src/codec.ts
+++ b/packages/taler-util/src/codec.ts
@@ -14,12 +14,17 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+import { j2s } from "./helpers.js";
+import { Logger } from "./logging.js";
+
/**
* Type-safe codecs for converting from/to JSON.
*/
/* eslint-disable @typescript-eslint/ban-types */
+const logger = new Logger("codec.ts");
+
/**
* Error thrown when decoding fails.
*/
@@ -322,6 +327,74 @@ export function codecForString(): Codec<string> {
}
/**
+ * Return a codec for a value that must be a string.
+ */
+export function codecForStringURL(shouldEndWithSlash?: boolean): Codec<string> {
+ return {
+ decode(x: any, c?: Context): string {
+ if (typeof x !== "string") {
+ throw new DecodingError(
+ `expected string at ${renderContext(c)} but got ${typeof x}`,
+ );
+ }
+ if (shouldEndWithSlash && !x.endsWith("/")) {
+ throw new DecodingError(
+ `expected URL string that ends with slash at ${renderContext(
+ c,
+ )} but got ${x}`,
+ );
+ }
+ try {
+ const url = new URL(x);
+ return x;
+ } catch (e) {
+ if (e instanceof Error) {
+ throw new DecodingError(e.message);
+ } else {
+ throw new DecodingError(
+ `expected an URL string at ${renderContext(c)} but got "${x}"`,
+ );
+ }
+ }
+ },
+ };
+}
+
+/**
+ * Return a codec for a value that must be a string.
+ */
+export function codecForURL(shouldEndWithSlash?: boolean): Codec<URL> {
+ return {
+ decode(x: any, c?: Context): URL {
+ if (typeof x !== "string") {
+ throw new DecodingError(
+ `expected string at ${renderContext(c)} but got ${typeof x}`,
+ );
+ }
+ if (shouldEndWithSlash && !x.endsWith("/")) {
+ throw new DecodingError(
+ `expected URL string that ends with slash at ${renderContext(
+ c,
+ )} but got ${x}`,
+ );
+ }
+ try {
+ const url = new URL(x);
+ return url;
+ } catch (e) {
+ if (e instanceof Error) {
+ throw new DecodingError(e.message);
+ } else {
+ throw new DecodingError(
+ `expected an URL string at ${renderContext(c)} but got "${x}"`,
+ );
+ }
+ }
+ },
+ };
+}
+
+/**
* Codec that allows any value.
*/
export function codecForAny(): Codec<any> {
@@ -432,6 +505,9 @@ export function codecForEither<T extends Array<Codec<unknown>>>(
continue;
}
}
+ if (logger.shouldLogTrace()) {
+ logger.trace(`offending value: ${j2s(x)}`);
+ }
throw new DecodingError(
`No alternative matched at at ${renderContext(c)}`,
);
diff --git a/packages/merchant-backoffice-ui/src/context/config.ts b/packages/taler-util/src/compat.d.ts
index 5cd772380..d7ccf19f0 100644
--- a/packages/merchant-backoffice-ui/src/context/config.ts
+++ b/packages/taler-util/src/compat.d.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2023 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -14,19 +14,10 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { createContext } from 'preact'
-import { useContext } from 'preact/hooks'
-
-interface Type {
- currency: string;
- version: string;
-}
-const Context = createContext<Type>(null!)
-
-export const ConfigContextProvider = Context.Provider
-export const useConfigContext = (): Type => useContext(Context);
+export function processExit(status: number): never;
+export function processArgv(): string[];
+export function readlinePrompt(prompt: string): Promise<string>;
+export function pathBasename(s: string): string;
+export function setUnhandledRejectionHandler(h: (e: any) => void): void;
+export function getenv(name: string): string | undefined;
+export function readFile(fileName: string): string;
diff --git a/packages/taler-util/src/compat.node.ts b/packages/taler-util/src/compat.node.ts
new file mode 100644
index 000000000..2f78c9346
--- /dev/null
+++ b/packages/taler-util/src/compat.node.ts
@@ -0,0 +1,64 @@
+/*
+ This file is part of GNU Taler
+ (C) 2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import process from "node:process";
+import readline from "node:readline";
+import path from "node:path";
+import os from "node:os";
+import fs from "node:fs";
+
+export function processExit(status: number): never {
+ process.exit(status);
+}
+
+export function processArgv(): string[] {
+ return [...process.argv];
+}
+
+export function readlinePrompt(prompt: string): Promise<string> {
+ const stdinReadline = readline.createInterface({
+ input: process.stdin,
+ output: process.stdout,
+ });
+ return new Promise<string>((resolve, reject) => {
+ stdinReadline.question(prompt, (res) => {
+ resolve(res);
+ stdinReadline.close();
+ });
+ });
+}
+
+export function pathBasename(p: string): string {
+ return path.basename(p);
+}
+
+export function pathHomedir(): string {
+ return os.homedir();
+}
+
+export function setUnhandledRejectionHandler(h: (e: any) => void): void {
+ process.on("unhandledRejection", (e) => {
+ h(e);
+ });
+}
+
+export function getenv(name: string): string | undefined {
+ return process.env[name];
+}
+
+export function readFile(fileName: string): string {
+ return fs.readFileSync(fileName, "utf-8");
+}
diff --git a/packages/taler-util/src/compat.qtart.ts b/packages/taler-util/src/compat.qtart.ts
new file mode 100644
index 000000000..7d11cf375
--- /dev/null
+++ b/packages/taler-util/src/compat.qtart.ts
@@ -0,0 +1,57 @@
+/*
+ This file is part of GNU Taler
+ (C) 2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+// qtart "std" library
+// @ts-ignore
+import * as std from "std";
+
+export function processExit(status: number): never {
+ std.exit(status);
+ throw Error("not reached");
+}
+
+export function processArgv(): string[] {
+ // @ts-ignore
+ return ["qtart", ...globalThis.scriptArgs];
+}
+
+export function readlinePrompt(prompt: string): Promise<string> {
+ throw new Error("realinePrompt not yet supported in qtart");
+}
+
+export function pathBasename(p: string): string {
+ const slashIndex = p.lastIndexOf("/");
+ if (slashIndex < 0) {
+ return p;
+ }
+ return p.substring(0, slashIndex);
+}
+
+export function pathHomedir(): string {
+ return std.getenv("HOME");
+}
+
+export function setUnhandledRejectionHandler(h: (e: any) => void): void {
+ // not supported
+}
+
+export function getenv(name: string): string | undefined {
+ return std.getenv(name);
+}
+
+export function readFile(fileName: string): string {
+ throw new Error("readFile not yet supported in qtart");
+}
diff --git a/packages/taler-util/src/contract-terms.ts b/packages/taler-util/src/contract-terms.ts
index 3567b50d8..b906a1d7f 100644
--- a/packages/taler-util/src/contract-terms.ts
+++ b/packages/taler-util/src/contract-terms.ts
@@ -16,12 +16,12 @@
import { canonicalJson } from "./helpers.js";
import { Logger } from "./logging.js";
-import { kdf } from "./kdf.js";
import {
decodeCrock,
encodeCrock,
getRandomBytes,
hash,
+ kdf,
stringToBytes,
} from "./taler-crypto.js";
diff --git a/packages/taler-wallet-core/src/errors.ts b/packages/taler-util/src/errors.ts
index 3480fed3c..9378d25e8 100644
--- a/packages/taler-wallet-core/src/errors.ts
+++ b/packages/taler-util/src/errors.ts
@@ -24,11 +24,16 @@
* Imports.
*/
import {
+ AbsoluteTime,
+ CancellationToken,
+ PaymentInsufficientBalanceDetails,
TalerErrorCode,
TalerErrorDetail,
TransactionType,
} from "@gnu-taler/taler-util";
+type empty = Record<string, never>;
+
export interface DetailsMap {
[TalerErrorCode.WALLET_PENDING_OPERATION_FAILED]: {
innerError: TalerErrorDetail;
@@ -41,13 +46,17 @@ export interface DetailsMap {
exchangeProtocolVersion: string;
walletProtocolVersion: string;
};
- [TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK]: {};
- [TalerErrorCode.WALLET_TIPPING_COIN_SIGNATURE_INVALID]: {};
+ [TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK]: empty;
+ [TalerErrorCode.WALLET_REWARD_COIN_SIGNATURE_INVALID]: empty;
[TalerErrorCode.WALLET_ORDER_ALREADY_CLAIMED]: {
orderId: string;
claimUrl: string;
};
- [TalerErrorCode.WALLET_CONTRACT_TERMS_MALFORMED]: {};
+ [TalerErrorCode.WALLET_ORDER_ALREADY_PAID]: {
+ orderId: string;
+ fulfillmentUrl: string;
+ };
+ [TalerErrorCode.WALLET_CONTRACT_TERMS_MALFORMED]: empty;
[TalerErrorCode.WALLET_CONTRACT_TERMS_SIGNATURE_INVALID]: {
merchantPub: string;
orderId: string;
@@ -59,17 +68,68 @@ export interface DetailsMap {
[TalerErrorCode.WALLET_INVALID_TALER_PAY_URI]: {
talerPayUri: string;
};
- [TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR]: {};
- [TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION]: {};
- [TalerErrorCode.WALLET_BANK_INTEGRATION_PROTOCOL_VERSION_INCOMPATIBLE]: {};
- [TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN]: {};
- [TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED]: {};
- [TalerErrorCode.WALLET_NETWORK_ERROR]: {};
- [TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE]: {};
- [TalerErrorCode.WALLET_EXCHANGE_COIN_SIGNATURE_INVALID]: {};
- [TalerErrorCode.WALLET_WITHDRAWAL_GROUP_INCOMPLETE]: {};
- [TalerErrorCode.WALLET_CORE_NOT_AVAILABLE]: {};
- [TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR]: {};
+ [TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR]: {
+ requestUrl: string;
+ requestMethod: string;
+ httpStatusCode: number;
+ errorResponse?: any;
+ };
+ [TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION]: {
+ stack?: string;
+ };
+ [TalerErrorCode.WALLET_BANK_INTEGRATION_PROTOCOL_VERSION_INCOMPATIBLE]: {
+ bankProtocolVersion: string;
+ walletProtocolVersion: string;
+ };
+ [TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN]: {
+ operation: string;
+ };
+ [TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED]: {
+ requestUrl: string;
+ requestMethod: string;
+ throttleStats: Record<string, unknown>;
+ };
+ [TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT]: {
+ requestUrl: string;
+ requestMethod: string;
+ timeoutMs: number;
+ };
+ [TalerErrorCode.GENERIC_TIMEOUT]: {
+ requestUrl: string;
+ requestMethod: string;
+ timeoutMs: number;
+ };
+ [TalerErrorCode.WALLET_NETWORK_ERROR]: {
+ requestUrl: string;
+ requestMethod: string;
+ };
+ [TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE]: {
+ requestUrl: string;
+ requestMethod: string;
+ httpStatusCode: number;
+ validationError?: string;
+ /**
+ * Content type of the response, usually only specified if not the
+ * expected content type.
+ */
+ contentType?: string;
+ };
+ [TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR]: {
+ operation: string;
+ error: string;
+ detail: TalerErrorDetail | undefined;
+ };
+ [TalerErrorCode.WALLET_EXCHANGE_COIN_SIGNATURE_INVALID]: empty;
+ [TalerErrorCode.WALLET_WITHDRAWAL_GROUP_INCOMPLETE]: {
+ numErrors: number;
+ errorsPerCoin: Record<number, TalerErrorDetail>;
+ };
+ [TalerErrorCode.WALLET_CORE_NOT_AVAILABLE]: {
+ lastError?: TalerErrorDetail;
+ };
+ [TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR]: {
+ httpStatusCode: number;
+ };
[TalerErrorCode.WALLET_PAY_MERCHANT_SERVER_ERROR]: {
requestError: TalerErrorDetail;
};
@@ -80,11 +140,35 @@ export interface DetailsMap {
detail: string;
};
[TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED]: {
- // FIXME!
+ kycUrl: string;
+ };
+ [TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE]: {
+ insufficientBalanceDetails: PaymentInsufficientBalanceDetails;
+ };
+ [TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE]: {
+ insufficientBalanceDetails: PaymentInsufficientBalanceDetails;
+ };
+ [TalerErrorCode.WALLET_REFRESH_GROUP_INCOMPLETE]: {
+ numErrors: number;
+ /**
+ * Errors, can be truncated.
+ */
+ errors: TalerErrorDetail[];
+ };
+ [TalerErrorCode.WALLET_EXCHANGE_BASE_URL_MISMATCH]: {
+ urlWallet: string;
+ urlExchange: string;
+ };
+ [TalerErrorCode.WALLET_EXCHANGE_UNAVAILABLE]: {
+ exchangeBaseUrl: string;
+ innerError: TalerErrorDetail | undefined;
+ };
+ [TalerErrorCode.WALLET_DB_UNAVAILABLE]: {
+ innerError: TalerErrorDetail | undefined;
};
}
-type ErrBody<Y> = Y extends keyof DetailsMap ? DetailsMap[Y] : never;
+type ErrBody<Y> = Y extends keyof DetailsMap ? DetailsMap[Y] : empty;
export function makeErrorDetail<C extends TalerErrorCode>(
code: C,
@@ -94,7 +178,8 @@ export function makeErrorDetail<C extends TalerErrorCode>(
if (!hint && !(detail as any).hint) {
hint = getDefaultHint(code);
}
- return { code, hint, ...detail };
+ const when = AbsoluteTime.now();
+ return { code, when, hint, ...detail };
}
export function makePendingOperationFailedError(
@@ -122,7 +207,7 @@ function getDefaultHint(code: number): string {
}
}
-export class TalerProtocolViolationError<T = any> extends Error {
+export class TalerProtocolViolationError extends Error {
constructor(hint?: string) {
let msg: string;
if (hint) {
@@ -135,11 +220,28 @@ export class TalerProtocolViolationError<T = any> extends Error {
}
}
+// compute a subset of TalerError, just for http request
+type HttpErrors =
+ | TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT
+ | TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED
+ | TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE
+ | TalerErrorCode.WALLET_NETWORK_ERROR
+ | TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR;
+
+type TalerHttpErrorsDetails = {
+ [code in HttpErrors]: TalerError<DetailsMap[code]>;
+};
+
+export type TalerHttpError =
+ TalerHttpErrorsDetails[keyof TalerHttpErrorsDetails];
+
export class TalerError<T = any> extends Error {
errorDetail: TalerErrorDetail & T;
- private constructor(d: TalerErrorDetail & T) {
+ cause: Error | undefined;
+ private constructor(d: TalerErrorDetail & T, cause?: Error) {
super(d.hint ?? `Error (code ${d.code})`);
this.errorDetail = d;
+ this.cause = cause;
Object.setPrototypeOf(this, TalerError.prototype);
}
@@ -147,20 +249,22 @@ export class TalerError<T = any> extends Error {
code: C,
detail: ErrBody<C>,
hint?: string,
+ cause?: Error,
): TalerError {
if (!hint) {
hint = getDefaultHint(code);
}
- return new TalerError<unknown>({ code, hint, ...detail });
+ const when = AbsoluteTime.now();
+ return new TalerError<unknown>({ code, when, hint, ...detail }, cause);
}
- static fromUncheckedDetail(d: TalerErrorDetail): TalerError {
- return new TalerError<unknown>({ ...d });
+ static fromUncheckedDetail(d: TalerErrorDetail, c?: Error): TalerError {
+ return new TalerError<unknown>({ ...d }, c);
}
static fromException(e: any): TalerError {
const errDetail = getErrorDetailFromException(e);
- return new TalerError(errDetail);
+ return new TalerError(errDetail, e);
}
hasErrorCode<C extends keyof DetailsMap>(
@@ -168,6 +272,14 @@ export class TalerError<T = any> extends Error {
): this is TalerError<DetailsMap[C]> {
return this.errorDetail.code === code;
}
+
+ toString(): string {
+ return `TalerError: ${JSON.stringify(this.errorDetail)}`;
+ }
+}
+
+export function safeStringifyException(e: any): string {
+ return JSON.stringify(getErrorDetailFromException(e), undefined, 2);
}
/**
@@ -178,6 +290,13 @@ export function getErrorDetailFromException(e: any): TalerErrorDetail {
if (e instanceof TalerError) {
return e.errorDetail;
}
+ if (e instanceof CancellationToken.CancellationError) {
+ const err = makeErrorDetail(
+ TalerErrorCode.WALLET_CORE_REQUEST_CANCELLED,
+ {},
+ );
+ return err;
+ }
if (e instanceof Error) {
const err = makeErrorDetail(
TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
@@ -204,3 +323,7 @@ export function getErrorDetailFromException(e: any): TalerErrorDetail {
);
return err;
}
+
+export function assertUnreachable(x: never): never {
+ throw new Error("Didn't expect to get here");
+}
diff --git a/packages/taler-util/src/globbing/minimatch.ts b/packages/taler-util/src/globbing/minimatch.ts
index b23de4e92..30391ba26 100644
--- a/packages/taler-util/src/globbing/minimatch.ts
+++ b/packages/taler-util/src/globbing/minimatch.ts
@@ -341,9 +341,9 @@ export class Minimatch {
pattern.charAt(0) === "."
? "" // anything
: // not (start or / followed by . or .. followed by / or end)
- options.dot
- ? "(?!(?:^|\\/)\\.{1,2}(?:$|\\/))"
- : "(?!\\.)";
+ options.dot
+ ? "(?!(?:^|\\/)\\.{1,2}(?:$|\\/))"
+ : "(?!\\.)";
var self = this;
function clearStateChar() {
@@ -874,8 +874,8 @@ export class Minimatch {
var twoStar = options.noglobstar
? star
: options.dot
- ? twoStarDot
- : twoStarNoDot;
+ ? twoStarDot
+ : twoStarNoDot;
var flags = options.nocase ? "i" : "";
var re = (set as any)
@@ -885,8 +885,8 @@ export class Minimatch {
return p === GLOBSTAR
? twoStar
: typeof p === "string"
- ? regExpEscape(p)
- : (p as any)._src;
+ ? regExpEscape(p)
+ : (p as any)._src;
})
.join("\\/");
})
diff --git a/packages/taler-util/src/helpers.ts b/packages/taler-util/src/helpers.ts
index 7d84d434e..d4c3c86b5 100644
--- a/packages/taler-util/src/helpers.ts
+++ b/packages/taler-util/src/helpers.ts
@@ -121,3 +121,19 @@ export function j2s(x: any): string {
export function notEmpty<T>(value: T | null | undefined): value is T {
return value !== null && value !== undefined;
}
+
+/**
+ * Safe function to stringify errors.
+ */
+export function stringifyError(x: any): string {
+ if (typeof x === "undefined") {
+ return "<thrown undefined>";
+ }
+ if (x === null) {
+ return `<thrown null>`;
+ }
+ if (typeof x === "object") {
+ return x.toString();
+ }
+ return `<thrown ${typeof x}>`;
+}
diff --git a/packages/taler-util/src/http-client/README.md b/packages/taler-util/src/http-client/README.md
new file mode 100644
index 000000000..33d1a8645
--- /dev/null
+++ b/packages/taler-util/src/http-client/README.md
@@ -0,0 +1,19 @@
+## HTTP Cclients
+
+This folder contain class or function specifically designed to facilitate HTTP client
+interactions with a the core systems.
+
+These API defines:
+
+1. **API Communication**: Handle communication with the component API,
+ abstracting away the details of HTTP requests and responses.
+ This includes making GET, POST, PUT, and DELETE requests to the servers.
+2. **Data Formatting**: Responsible for formatting requests to the API in a
+ way that's expected by the servers (JSON) and parsing the responses back
+ into formats usable by the client.
+3. **Authentication and Security**: Handling authentication with the server API,
+ which could involve sending API keys, client credentials, or managing tokens.
+ It might also implement security features to ensure data integrity and confidentiality during transit.
+4. **Error Handling**: Providing robust error handling and retry mechanisms
+ for failed HTTP requests, including logging and potentially user notifications for critical failures.
+5. **Data Validation**: Before sending requests, it could validate the data to ensure it meets the API's expected format, types, and value ranges, reducing the likelihood of errors and improving system reliability.
diff --git a/packages/taler-util/src/http-client/authentication.ts b/packages/taler-util/src/http-client/authentication.ts
new file mode 100644
index 000000000..8897a2fa0
--- /dev/null
+++ b/packages/taler-util/src/http-client/authentication.ts
@@ -0,0 +1,137 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import { HttpStatusCode } from "../http-status-codes.js";
+import {
+ HttpRequestLibrary,
+ createPlatformHttpLib,
+ makeBasicAuthHeader,
+ readTalerErrorResponse,
+} from "../http.js";
+import { LibtoolVersion } from "../libtool-version.js";
+import {
+ opEmptySuccess,
+ opKnownHttpFailure,
+ opSuccessFromHttp,
+ opUnknownFailure,
+} from "../operation.js";
+import {
+ AccessToken,
+ TalerAuthentication,
+ codecForTokenSuccessResponse,
+ codecForTokenSuccessResponseMerchant,
+} from "./types.js";
+import { makeBearerTokenAuthHeader } from "./utils.js";
+
+export class TalerAuthenticationHttpClient {
+ public readonly PROTOCOL_VERSION = "0:0:0";
+
+ httpLib: HttpRequestLibrary;
+
+ constructor(
+ readonly baseUrl: string,
+ httpClient?: HttpRequestLibrary,
+ ) {
+ this.httpLib = httpClient ?? createPlatformHttpLib();
+ }
+
+ isCompatible(version: string): boolean {
+ const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version);
+ return compare?.compatible ?? false;
+ }
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-token
+ *
+ * @returns
+ */
+ async createAccessTokenBasic(
+ username: string,
+ password: string,
+ body: TalerAuthentication.TokenRequest,
+ ) {
+ const url = new URL(`token`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ headers: {
+ Authorization: makeBasicAuthHeader(username, password),
+ },
+ body,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForTokenSuccessResponse());
+ //FIXME: missing in docs
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ *
+ * @returns
+ */
+ async createAccessTokenBearer(
+ token: AccessToken,
+ body: TalerAuthentication.TokenRequest,
+ ) {
+ const url = new URL(`token`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(token),
+ },
+ body,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForTokenSuccessResponseMerchant());
+ //FIXME: missing in docs
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ async deleteAccessToken(token: AccessToken) {
+ const url = new URL(`token`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "DELETE",
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(token),
+ },
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opEmptySuccess(resp);
+ //FIXME: missing in docs
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+}
diff --git a/packages/taler-util/src/http-client/bank-conversion.ts b/packages/taler-util/src/http-client/bank-conversion.ts
new file mode 100644
index 000000000..cb14d8b34
--- /dev/null
+++ b/packages/taler-util/src/http-client/bank-conversion.ts
@@ -0,0 +1,223 @@
+/*
+ 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/>
+ */
+
+/**
+ * Imports.
+ */
+import { AmountJson, Amounts } from "../amounts.js";
+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 {
+ FailCasesByMethod,
+ ResultByMethod,
+ opEmptySuccess,
+ opKnownHttpFailure,
+ opSuccessFromHttp,
+ opUnknownFailure,
+} from "../operation.js";
+import { TalerErrorCode } from "../taler-error-codes.js";
+import { codecForTalerErrorDetail } from "../wallet-types.js";
+import {
+ AccessToken,
+ TalerBankConversionApi,
+ codecForCashinConversionResponse,
+ codecForCashoutConversionResponse,
+ codecForConversionBankConfig,
+} from "./types.js";
+import {
+ CacheEvictor,
+ makeBearerTokenAuthHeader,
+ nullEvictor,
+} from "./utils.js";
+
+export type TalerBankConversionResultByMethod<
+ prop extends keyof TalerBankConversionHttpClient,
+> = ResultByMethod<TalerBankConversionHttpClient, prop>;
+export type TalerBankConversionErrorsByMethod<
+ prop extends keyof TalerBankConversionHttpClient,
+> = FailCasesByMethod<TalerBankConversionHttpClient, prop>;
+
+export enum TalerBankConversionCacheEviction {
+ UPDATE_RATE,
+}
+
+/**
+ * The API is used by the wallets.
+ */
+export class TalerBankConversionHttpClient {
+ public readonly PROTOCOL_VERSION = "0:0:0";
+
+ httpLib: HttpRequestLibrary;
+ cacheEvictor: CacheEvictor<TalerBankConversionCacheEviction>;
+
+ constructor(
+ readonly baseUrl: string,
+ httpClient?: HttpRequestLibrary,
+ cacheEvictor?: CacheEvictor<TalerBankConversionCacheEviction>,
+ ) {
+ this.httpLib = httpClient ?? createPlatformHttpLib();
+ this.cacheEvictor = cacheEvictor ?? nullEvictor;
+ }
+
+ isCompatible(version: string): boolean {
+ const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version);
+ return compare?.compatible ?? false;
+ }
+
+ /**
+ * https://docs.taler.net/core/api-bank-conversion-info.html#get--config
+ *
+ */
+ async getConfig() {
+ const url = new URL(`config`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForConversionBankConfig());
+ case HttpStatusCode.NotImplemented:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-bank-conversion-info.html#get--cashin-rate
+ *
+ */
+ async getCashinRate(conversion: { debit?: AmountJson; credit?: AmountJson }) {
+ const url = new URL(`cashin-rate`, this.baseUrl);
+ if (conversion.debit) {
+ url.searchParams.set("amount_debit", Amounts.stringify(conversion.debit));
+ }
+ if (conversion.credit) {
+ url.searchParams.set(
+ "amount_credit",
+ Amounts.stringify(conversion.credit),
+ );
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForCashinConversionResponse());
+ case HttpStatusCode.BadRequest: {
+ const body = await resp.json();
+ const details = codecForTalerErrorDetail().decode(body);
+ switch (details.code) {
+ case TalerErrorCode.GENERIC_PARAMETER_MISSING:
+ return opKnownHttpFailure(resp.status, resp);
+ case TalerErrorCode.GENERIC_PARAMETER_MALFORMED:
+ return opKnownHttpFailure(resp.status, resp);
+ case TalerErrorCode.GENERIC_CURRENCY_MISMATCH:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, body);
+ }
+ }
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotImplemented:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-bank-conversion-info.html#get--cashout-rate
+ *
+ */
+ async getCashoutRate(conversion: {
+ debit?: AmountJson;
+ credit?: AmountJson;
+ }) {
+ const url = new URL(`cashout-rate`, this.baseUrl);
+ if (conversion.debit) {
+ url.searchParams.set("amount_debit", Amounts.stringify(conversion.debit));
+ }
+ if (conversion.credit) {
+ url.searchParams.set(
+ "amount_credit",
+ Amounts.stringify(conversion.credit),
+ );
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForCashoutConversionResponse());
+ case HttpStatusCode.BadRequest: {
+ const body = await resp.json();
+ const details = codecForTalerErrorDetail().decode(body);
+ switch (details.code) {
+ case TalerErrorCode.GENERIC_PARAMETER_MISSING:
+ return opKnownHttpFailure(resp.status, resp);
+ case TalerErrorCode.GENERIC_PARAMETER_MALFORMED:
+ return opKnownHttpFailure(resp.status, resp);
+ case TalerErrorCode.GENERIC_CURRENCY_MISMATCH:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, body);
+ }
+ }
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotImplemented:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-bank-conversion-info.html#post--conversion-rate
+ *
+ */
+ async updateConversionRate(
+ auth: AccessToken,
+ body: TalerBankConversionApi.ConversionRate,
+ ) {
+ const url = new URL(`conversion-rate`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(auth),
+ },
+ body,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerBankConversionCacheEviction.UPDATE_RATE,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotImplemented:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+}
diff --git a/packages/taler-util/src/http-client/bank-core.ts b/packages/taler-util/src/http-client/bank-core.ts
new file mode 100644
index 000000000..97c1727ff
--- /dev/null
+++ b/packages/taler-util/src/http-client/bank-core.ts
@@ -0,0 +1,1032 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ AbsoluteTime,
+ HttpStatusCode,
+ LibtoolVersion,
+ LongPollParams,
+ OperationAlternative,
+ OperationFail,
+ OperationOk,
+ TalerErrorCode,
+ codecForChallenge,
+ codecForTanTransmission,
+ opKnownAlternativeFailure,
+ opKnownHttpFailure,
+ opKnownTalerFailure
+} from "@gnu-taler/taler-util";
+import {
+ HttpRequestLibrary,
+ createPlatformHttpLib,
+ readTalerErrorResponse,
+} from "@gnu-taler/taler-util/http";
+import {
+ FailCasesByMethod,
+ ResultByMethod,
+ opEmptySuccess,
+ opFixedSuccess,
+ opSuccessFromHttp,
+ opUnknownFailure,
+} from "../operation.js";
+import {
+ AccessToken,
+ PaginationParams,
+ TalerCorebankApi,
+ UserAndToken,
+ WithdrawalOperationStatus,
+ codecForAccountData,
+ codecForBankAccountCreateWithdrawalResponse,
+ codecForBankAccountTransactionInfo,
+ codecForBankAccountTransactionsResponse,
+ codecForCashoutPending,
+ codecForCashoutStatusResponse,
+ codecForCashouts,
+ codecForCoreBankConfig,
+ codecForCreateTransactionResponse,
+ codecForGlobalCashouts,
+ codecForListBankAccountsResponse,
+ codecForMonitorResponse,
+ codecForPublicAccountsResponse,
+ codecForRegisterAccountResponse,
+ codecForWithdrawalPublicInfo,
+} from "./types.js";
+import {
+ CacheEvictor,
+ IdempotencyRetry,
+ addLongPollingParam,
+ addPaginationParams,
+ makeBearerTokenAuthHeader,
+ nullEvictor,
+} from "./utils.js";
+
+export type TalerCoreBankResultByMethod<
+ prop extends keyof TalerCoreBankHttpClient,
+> = ResultByMethod<TalerCoreBankHttpClient, prop>;
+export type TalerCoreBankErrorsByMethod<
+ prop extends keyof TalerCoreBankHttpClient,
+> = FailCasesByMethod<TalerCoreBankHttpClient, prop>;
+
+export enum TalerCoreBankCacheEviction {
+ DELETE_ACCOUNT,
+ CREATE_ACCOUNT,
+ UPDATE_ACCOUNT,
+ UPDATE_PASSWORD,
+ CREATE_TRANSACTION,
+ CONFIRM_WITHDRAWAL,
+ ABORT_WITHDRAWAL,
+ CREATE_WITHDRAWAL,
+ CREATE_CASHOUT,
+}
+/**
+ * Protocol version spoken with the core bank.
+ *
+ * Endpoint must be ordered in the same way that in the docs
+ * Response code (http and taler) must have the same order that in the docs
+ * That way is easier to see changes
+ *
+ * Uses libtool's current:revision:age versioning.
+ */
+export class TalerCoreBankHttpClient {
+ public readonly PROTOCOL_VERSION = "4:0:0";
+
+ httpLib: HttpRequestLibrary;
+ cacheEvictor: CacheEvictor<TalerCoreBankCacheEviction>;
+ constructor(
+ readonly baseUrl: string,
+ httpClient?: HttpRequestLibrary,
+ cacheEvictor?: CacheEvictor<TalerCoreBankCacheEviction>,
+ ) {
+ this.httpLib = httpClient ?? createPlatformHttpLib();
+ this.cacheEvictor = cacheEvictor ?? nullEvictor;
+ }
+
+ isCompatible(version: string): boolean {
+ const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version);
+ return compare?.compatible ?? false;
+ }
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#config
+ *
+ */
+ async getConfig() {
+ const url = new URL(`config`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForCoreBankConfig());
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ //
+ // ACCOUNTS
+ //
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#post--accounts
+ *
+ */
+ async createAccount(
+ auth: AccessToken | undefined,
+ body: TalerCorebankApi.RegisterAccountRequest,
+ ) {
+ const url = new URL(`accounts`, this.baseUrl);
+ const headers: Record<string, string> = {};
+ if (auth) {
+ headers.Authorization = makeBearerTokenAuthHeader(auth);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ headers: headers,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok: {
+ await this.cacheEvictor.notifySuccess(
+ TalerCoreBankCacheEviction.CREATE_ACCOUNT,
+ );
+ return opSuccessFromHttp(resp, codecForRegisterAccountResponse());
+ }
+ case HttpStatusCode.BadRequest:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict: {
+ const details = await readTalerErrorResponse(resp);
+ switch (details.code) {
+ case TalerErrorCode.BANK_REGISTER_USERNAME_REUSE:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_REGISTER_PAYTO_URI_REUSE:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_UNALLOWED_DEBIT:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_NON_ADMIN_SET_TAN_CHANNEL:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_MISSING_TAN_INFO:
+ return opKnownTalerFailure(details.code, details);
+ default:
+ return opUnknownFailure(resp, details);
+ }
+ }
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+ /**
+ * https://docs.taler.net/core/api-corebank.html#delete--accounts-$USERNAME
+ *
+ */
+ async deleteAccount(auth: UserAndToken, cid?: string) {
+ const url = new URL(`accounts/${auth.username}`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "DELETE",
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(auth.token),
+ "X-Challenge-Id": cid,
+ },
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Accepted:
+ return opKnownAlternativeFailure(
+ resp,
+ resp.status,
+ codecForChallenge(),
+ );
+ case HttpStatusCode.NoContent:
+ return opEmptySuccess(resp);
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict: {
+ const details = await readTalerErrorResponse(resp);
+ switch (details.code) {
+ case TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_ACCOUNT_BALANCE_NOT_ZERO:
+ return opKnownTalerFailure(details.code, details);
+ default:
+ return opUnknownFailure(resp, details);
+ }
+ }
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#patch--accounts-$USERNAME
+ *
+ */
+ async updateAccount(
+ auth: UserAndToken,
+ body: TalerCorebankApi.AccountReconfiguration,
+ cid?: string,
+ ) {
+ const url = new URL(`accounts/${auth.username}`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "PATCH",
+ body,
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(auth.token),
+ "X-Challenge-Id": cid,
+ },
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Accepted:
+ return opKnownAlternativeFailure(
+ resp,
+ resp.status,
+ codecForChallenge(),
+ );
+ case HttpStatusCode.NoContent:
+ return opEmptySuccess(resp);
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict: {
+ const details = await readTalerErrorResponse(resp);
+ switch (details.code) {
+ case TalerErrorCode.BANK_NON_ADMIN_PATCH_LEGAL_NAME:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_NON_ADMIN_PATCH_CASHOUT:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_MISSING_TAN_INFO:
+ return opKnownTalerFailure(details.code, details);
+ default:
+ return opUnknownFailure(resp, details);
+ }
+ }
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#patch--accounts-$USERNAME-auth
+ *
+ */
+ async updatePassword(
+ auth: UserAndToken,
+ body: TalerCorebankApi.AccountPasswordChange,
+ cid?: string,
+ ) {
+ const url = new URL(`accounts/${auth.username}/auth`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "PATCH",
+ body,
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(auth.token),
+ "X-Challenge-Id": cid,
+ },
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Accepted:
+ return opKnownAlternativeFailure(
+ resp,
+ resp.status,
+ codecForChallenge(),
+ );
+ case HttpStatusCode.NoContent:
+ return opEmptySuccess(resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict: {
+ const details = await readTalerErrorResponse(resp);
+ switch (details.code) {
+ case TalerErrorCode.BANK_NON_ADMIN_PATCH_MISSING_OLD_PASSWORD:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_PATCH_BAD_OLD_PASSWORD:
+ return opKnownTalerFailure(details.code, details);
+ default:
+ return opUnknownFailure(resp, details);
+ }
+ }
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#get--public-accounts
+ *
+ */
+ async getPublicAccounts(
+ filter: { account?: string } = {},
+ pagination?: PaginationParams,
+ ) {
+ const url = new URL(`public-accounts`, this.baseUrl);
+ addPaginationParams(url, pagination);
+ if (filter.account !== undefined) {
+ url.searchParams.set("filter_name", filter.account);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForPublicAccountsResponse());
+ case HttpStatusCode.NoContent:
+ return opFixedSuccess({ public_accounts: [] });
+ case HttpStatusCode.NotFound:
+ return opFixedSuccess({ public_accounts: [] });
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#get--accounts
+ *
+ */
+ async getAccounts(
+ auth: AccessToken,
+ filter: { account?: string } = {},
+ pagination?: PaginationParams,
+ ) {
+ const url = new URL(`accounts`, this.baseUrl);
+ addPaginationParams(url, pagination);
+ if (filter.account !== undefined) {
+ url.searchParams.set("filter_name", filter.account);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(auth),
+ },
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForListBankAccountsResponse());
+ case HttpStatusCode.NoContent:
+ return opFixedSuccess({ accounts: [] });
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#get--accounts-$USERNAME
+ *
+ */
+ async getAccount(auth: UserAndToken) {
+ const url = new URL(`accounts/${auth.username}`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(auth.token),
+ },
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForAccountData());
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ //
+ // TRANSACTIONS
+ //
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#get--accounts-$USERNAME-transactions
+ *
+ */
+ async getTransactions(
+ auth: UserAndToken,
+ params?: PaginationParams & LongPollParams,
+ ) {
+ const url = new URL(`accounts/${auth.username}/transactions`, this.baseUrl);
+ addPaginationParams(url, params);
+ addLongPollingParam(url, params);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(auth.token),
+ },
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(
+ resp,
+ codecForBankAccountTransactionsResponse(),
+ );
+ case HttpStatusCode.NoContent:
+ return opFixedSuccess({ transactions: [] });
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#get--accounts-$USERNAME-transactions-$TRANSACTION_ID
+ *
+ */
+ async getTransactionById(auth: UserAndToken, txid: number) {
+ const url = new URL(
+ `accounts/${auth.username}/transactions/${String(txid)}`,
+ this.baseUrl,
+ );
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(auth.token),
+ },
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForBankAccountTransactionInfo());
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-transactions
+ *
+ */
+ async createTransaction(
+ auth: UserAndToken,
+ body: TalerCorebankApi.CreateTransactionRequest,
+ idempotencyCheck: IdempotencyRetry | undefined,
+ cid?: string,
+ ): Promise<
+ //manually definition all return types because of recursion
+ | OperationOk<TalerCorebankApi.CreateTransactionResponse>
+ | OperationAlternative<HttpStatusCode.Accepted, TalerCorebankApi.Challenge>
+ | OperationFail<HttpStatusCode.NotFound>
+ | OperationFail<HttpStatusCode.BadRequest>
+ | OperationFail<HttpStatusCode.Unauthorized>
+ | OperationFail<TalerErrorCode.BANK_UNALLOWED_DEBIT>
+ | OperationFail<TalerErrorCode.BANK_ADMIN_CREDITOR>
+ | OperationFail<TalerErrorCode.BANK_SAME_ACCOUNT>
+ | OperationFail<TalerErrorCode.BANK_UNKNOWN_CREDITOR>
+ | OperationFail<TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED>
+ > {
+ const url = new URL(`accounts/${auth.username}/transactions`, this.baseUrl);
+ if (idempotencyCheck) {
+ body.request_uid = idempotencyCheck.uid
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(auth.token),
+ "X-Challenge-Id": cid,
+ },
+ body,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForCreateTransactionResponse());
+ case HttpStatusCode.Accepted:
+ return opKnownAlternativeFailure(
+ resp,
+ resp.status,
+ codecForChallenge(),
+ );
+ case HttpStatusCode.BadRequest:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict: {
+ const details = await readTalerErrorResponse(resp);
+ switch (details.code) {
+ case TalerErrorCode.BANK_ADMIN_CREDITOR:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_SAME_ACCOUNT:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_UNKNOWN_CREDITOR:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_UNALLOWED_DEBIT:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED:
+ if (!idempotencyCheck) {
+ return opKnownTalerFailure(details.code, details);
+ }
+ const nextRetry = idempotencyCheck.next();
+ return this.createTransaction(auth, body, nextRetry, cid);
+ default:
+ return opUnknownFailure(resp, details);
+ }
+ }
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ //
+ // WITHDRAWALS
+ //
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-withdrawals
+ *
+ */
+ async createWithdrawal(
+ auth: UserAndToken,
+ body: TalerCorebankApi.BankAccountCreateWithdrawalRequest,
+ ) {
+ const url = new URL(`accounts/${auth.username}/withdrawals`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(auth.token),
+ },
+ body,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(
+ resp,
+ codecForBankAccountCreateWithdrawalResponse(),
+ );
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ //FIXME: missing in docs
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-withdrawals-$WITHDRAWAL_ID-confirm
+ *
+ */
+ async confirmWithdrawalById(auth: UserAndToken, wid: string, cid?: string) {
+ const url = new URL(
+ `accounts/${auth.username}/withdrawals/${wid}/confirm`,
+ this.baseUrl,
+ );
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(auth.token),
+ "X-Challenge-Id": cid,
+ },
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Accepted:
+ return opKnownAlternativeFailure(
+ resp,
+ resp.status,
+ codecForChallenge(),
+ );
+ case HttpStatusCode.NoContent:
+ return opEmptySuccess(resp);
+ //FIXME: missing in docs
+ case HttpStatusCode.BadRequest:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict: {
+ const details = await readTalerErrorResponse(resp);
+ switch (details.code) {
+ case TalerErrorCode.BANK_CONFIRM_ABORT_CONFLICT:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_CONFIRM_INCOMPLETE:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_UNALLOWED_DEBIT:
+ return opKnownTalerFailure(details.code, details);
+ default:
+ return opUnknownFailure(resp, details);
+ }
+ }
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-withdrawals-$WITHDRAWAL_ID-abort
+ *
+ */
+ async abortWithdrawalById(auth: UserAndToken, wid: string) {
+ const url = new URL(
+ `accounts/${auth.username}/withdrawals/${wid}/abort`,
+ this.baseUrl,
+ );
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(auth.token),
+ },
+ });
+ switch (resp.status) {
+ case HttpStatusCode.NoContent:
+ return opEmptySuccess(resp);
+ //FIXME: missing in docs
+ case HttpStatusCode.BadRequest:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#get--withdrawals-$WITHDRAWAL_ID
+ *
+ */
+ async getWithdrawalById(
+ wid: string,
+ params?: {
+ old_state?: WithdrawalOperationStatus;
+ } & LongPollParams,
+ ) {
+ const url = new URL(`withdrawals/${wid}`, this.baseUrl);
+ addLongPollingParam(url, params);
+ if (params) {
+ url.searchParams.set(
+ "old_state",
+ !params.old_state ? "pending" : params.old_state,
+ );
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForWithdrawalPublicInfo());
+ //FIXME: missing in docs
+ case HttpStatusCode.BadRequest:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ //
+ // CASHOUTS
+ //
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-cashouts
+ *
+ */
+ async createCashout(
+ auth: UserAndToken,
+ body: TalerCorebankApi.CashoutRequest,
+ cid?: string,
+ ) {
+ const url = new URL(`accounts/${auth.username}/cashouts`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(auth.token),
+ "X-Challenge-Id": cid,
+ },
+ body,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForCashoutPending());
+ case HttpStatusCode.Accepted:
+ return opKnownAlternativeFailure(
+ resp,
+ resp.status,
+ codecForChallenge(),
+ );
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict: {
+ const details = await readTalerErrorResponse(resp);
+ switch (details.code) {
+ case TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_BAD_CONVERSION:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_UNALLOWED_DEBIT:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_CONFIRM_INCOMPLETE:
+ return opKnownTalerFailure(details.code, details);
+ default:
+ return opUnknownFailure(resp, details);
+ }
+ }
+ case HttpStatusCode.BadGateway: {
+ const details = await readTalerErrorResponse(resp);
+ switch (details.code) {
+ case TalerErrorCode.BANK_TAN_CHANNEL_SCRIPT_FAILED:
+ return opKnownTalerFailure(details.code, details);
+ default:
+ return opUnknownFailure(resp, details);
+ }
+ }
+ case HttpStatusCode.NotImplemented:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#get--accounts-$USERNAME-cashouts-$CASHOUT_ID
+ *
+ */
+ async getCashoutById(auth: UserAndToken, cid: number) {
+ const url = new URL(
+ `accounts/${auth.username}/cashouts/${cid}`,
+ this.baseUrl,
+ );
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(auth.token),
+ },
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForCashoutStatusResponse());
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotImplemented:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#get--accounts-$USERNAME-cashouts
+ *
+ */
+ async getAccountCashouts(auth: UserAndToken, pagination?: PaginationParams) {
+ const url = new URL(`accounts/${auth.username}/cashouts`, this.baseUrl);
+ addPaginationParams(url, pagination);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(auth.token),
+ },
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForCashouts());
+ case HttpStatusCode.NoContent:
+ return opFixedSuccess({ cashouts: [] });
+ case HttpStatusCode.NotImplemented:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#get--cashouts
+ *
+ */
+ async getGlobalCashouts(auth: AccessToken, pagination?: PaginationParams) {
+ const url = new URL(`cashouts`, this.baseUrl);
+ addPaginationParams(url, pagination);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(auth),
+ },
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForGlobalCashouts());
+ case HttpStatusCode.NoContent:
+ return opFixedSuccess({ cashouts: [] });
+ case HttpStatusCode.NotImplemented:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ //
+ // 2FA
+ //
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-challenge-$CHALLENGE_ID
+ *
+ */
+ async sendChallenge(auth: UserAndToken, cid: string) {
+ const url = new URL(
+ `accounts/${auth.username}/challenge/${cid}`,
+ this.baseUrl,
+ );
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(auth.token),
+ },
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForTanTransmission());
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.BadGateway: {
+ const details = await readTalerErrorResponse(resp);
+ switch (details.code) {
+ case TalerErrorCode.BANK_TAN_CHANNEL_SCRIPT_FAILED:
+ return opKnownTalerFailure(details.code, details);
+ default:
+ return opUnknownFailure(resp, details);
+ }
+ }
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-challenge-$CHALLENGE_ID-confirm
+ *
+ */
+ async confirmChallenge(
+ auth: UserAndToken,
+ cid: string,
+ body: TalerCorebankApi.ChallengeSolve,
+ ) {
+ const url = new URL(
+ `accounts/${auth.username}/challenge/${cid}/confirm`,
+ this.baseUrl,
+ );
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(auth.token),
+ },
+ body,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.NoContent:
+ return opEmptySuccess(resp);
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict: {
+ const details = await readTalerErrorResponse(resp);
+ switch (details.code) {
+ case TalerErrorCode.BANK_TAN_CHALLENGE_EXPIRED:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_TAN_CHALLENGE_FAILED:
+ return opKnownTalerFailure(details.code, details);
+ default:
+ return opUnknownFailure(resp, details);
+ }
+ }
+ case HttpStatusCode.TooManyRequests:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ //
+ // MONITOR
+ //
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#get--monitor
+ *
+ */
+ async getMonitor(
+ auth: AccessToken,
+ params: {
+ timeframe?: TalerCorebankApi.MonitorTimeframeParam;
+ date?: AbsoluteTime;
+ } = {},
+ ) {
+ const url = new URL(`monitor`, this.baseUrl);
+ if (params.timeframe) {
+ url.searchParams.set(
+ "timeframe",
+ TalerCorebankApi.MonitorTimeframeParam[params.timeframe],
+ );
+ }
+ if (params.date) {
+ const { t_s: seconds } = AbsoluteTime.toProtocolTimestamp(params.date);
+ if (seconds !== "never") {
+ url.searchParams.set("date_s", String(seconds));
+ }
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(auth),
+ },
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForMonitorResponse());
+ case HttpStatusCode.BadRequest:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ //
+ // Others API
+ //
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#taler-bank-integration-api
+ *
+ */
+ getIntegrationAPI(): URL {
+ return new URL(`taler-integration/`, this.baseUrl);
+ }
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#taler-bank-integration-api
+ *
+ */
+ getWireGatewayAPI(username: string): URL {
+ return new URL(`accounts/${username}/taler-wire-gateway/`, this.baseUrl);
+ }
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#taler-bank-integration-api
+ *
+ */
+ getRevenueAPI(username: string): URL {
+ return new URL(`accounts/${username}/taler-revenue/`, this.baseUrl);
+ }
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-token
+ *
+ */
+ getAuthenticationAPI(username: string): URL {
+ return new URL(`accounts/${username}/`, this.baseUrl);
+ }
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-token
+ *
+ */
+ getConversionInfoAPI(): URL {
+ return new URL(`conversion-info/`, this.baseUrl);
+ }
+}
diff --git a/packages/taler-util/src/http-client/bank-integration.ts b/packages/taler-util/src/http-client/bank-integration.ts
new file mode 100644
index 000000000..75e6a627a
--- /dev/null
+++ b/packages/taler-util/src/http-client/bank-integration.ts
@@ -0,0 +1,179 @@
+/*
+ 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 { 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 {
+ FailCasesByMethod,
+ ResultByMethod,
+ opEmptySuccess,
+ opKnownHttpFailure,
+ opKnownTalerFailure,
+ opSuccessFromHttp,
+ opUnknownFailure,
+} from "../operation.js";
+import { TalerErrorCode } from "../taler-error-codes.js";
+import { codecForTalerErrorDetail } from "../wallet-types.js";
+import {
+ LongPollParams,
+ TalerBankIntegrationApi,
+ WithdrawalOperationStatus,
+ codecForBankWithdrawalOperationPostResponse,
+ codecForBankWithdrawalOperationStatus,
+ codecForIntegrationBankConfig,
+} from "./types.js";
+import { addLongPollingParam } from "./utils.js";
+
+export type TalerBankIntegrationResultByMethod<
+ prop extends keyof TalerBankIntegrationHttpClient,
+> = ResultByMethod<TalerBankIntegrationHttpClient, prop>;
+export type TalerBankIntegrationErrorsByMethod<
+ prop extends keyof TalerBankIntegrationHttpClient,
+> = FailCasesByMethod<TalerBankIntegrationHttpClient, prop>;
+
+/**
+ * The API is used by the wallets.
+ */
+export class TalerBankIntegrationHttpClient {
+ public readonly PROTOCOL_VERSION = "2:0:2";
+
+ httpLib: HttpRequestLibrary;
+
+ constructor(
+ readonly baseUrl: string,
+ httpClient?: HttpRequestLibrary,
+ ) {
+ this.httpLib = httpClient ?? createPlatformHttpLib();
+ }
+
+ isCompatible(version: string): boolean {
+ const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version);
+ return compare?.compatible ?? false;
+ }
+
+ /**
+ * https://docs.taler.net/core/api-bank-integration.html#get--config
+ *
+ */
+ async getConfig() {
+ const url = new URL(`config`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForIntegrationBankConfig());
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-bank-integration.html#get--withdrawal-operation-$WITHDRAWAL_ID
+ *
+ */
+ async getWithdrawalOperationById(
+ woid: string,
+ params?: {
+ old_state?: WithdrawalOperationStatus;
+ } & LongPollParams,
+ ) {
+ const url = new URL(`withdrawal-operation/${woid}`, this.baseUrl);
+ addLongPollingParam(url, params);
+ if (params) {
+ url.searchParams.set(
+ "old_state",
+ !params.old_state ? "pending" : params.old_state,
+ );
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForBankWithdrawalOperationStatus());
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-bank-integration.html#post-$BANK_API_BASE_URL-withdrawal-operation-$wopid
+ *
+ */
+ async completeWithdrawalOperationById(
+ woid: string,
+ body: TalerBankIntegrationApi.BankWithdrawalOperationPostRequest,
+ ) {
+ const url = new URL(`withdrawal-operation/${woid}`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(
+ resp,
+ codecForBankWithdrawalOperationPostResponse(),
+ );
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict: {
+ const body = await readTalerErrorResponse(resp);
+ const details = codecForTalerErrorDetail().decode(body);
+ switch (details.code) {
+ case TalerErrorCode.BANK_WITHDRAWAL_OPERATION_RESERVE_SELECTION_CONFLICT:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_DUPLICATE_RESERVE_PUB_SUBJECT:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_UNKNOWN_ACCOUNT:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_ACCOUNT_IS_NOT_EXCHANGE:
+ return opKnownTalerFailure(details.code, details);
+ default:
+ return opUnknownFailure(resp, details);
+ }
+ }
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-bank-integration.html#post-$BANK_API_BASE_URL-withdrawal-operation-$wopid
+ *
+ */
+ async abortWithdrawalOperationById(woid: string) {
+ const url = new URL(`withdrawal-operation/${woid}/abort`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ });
+ switch (resp.status) {
+ case HttpStatusCode.NoContent:
+ return opEmptySuccess(resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+}
diff --git a/packages/taler-util/src/http-client/bank-revenue.ts b/packages/taler-util/src/http-client/bank-revenue.ts
new file mode 100644
index 000000000..34afe7d86
--- /dev/null
+++ b/packages/taler-util/src/http-client/bank-revenue.ts
@@ -0,0 +1,130 @@
+/*
+ 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 {
+ HttpRequestLibrary,
+ makeBasicAuthHeader,
+ readTalerErrorResponse,
+} from "../http-common.js";
+import { HttpStatusCode } from "../http-status-codes.js";
+import { createPlatformHttpLib } from "../http.js";
+import { LibtoolVersion } from "../libtool-version.js";
+import {
+ FailCasesByMethod,
+ ResultByMethod,
+ opKnownHttpFailure,
+ opSuccessFromHttp,
+ opUnknownFailure,
+} from "../operation.js";
+import {
+ LongPollParams,
+ PaginationParams,
+ codecForRevenueConfig,
+ codecForRevenueIncomingHistory,
+} from "./types.js";
+import { addLongPollingParam, addPaginationParams } from "./utils.js";
+
+export type TalerBankRevenueResultByMethod<
+ prop extends keyof TalerRevenueHttpClient,
+> = ResultByMethod<TalerRevenueHttpClient, prop>;
+export type TalerBankRevenueErrorsByMethod<
+ prop extends keyof TalerRevenueHttpClient,
+> = FailCasesByMethod<TalerRevenueHttpClient, prop>;
+
+type UsernameAndPassword = {
+ username: string;
+ password: string;
+};
+/**
+ * The API is used by the merchant (or other parties) to query
+ * for incoming transactions to their account.
+ */
+export class TalerRevenueHttpClient {
+ httpLib: HttpRequestLibrary;
+
+ constructor(
+ readonly baseUrl: string,
+ httpClient?: HttpRequestLibrary,
+ ) {
+ this.httpLib = httpClient ?? createPlatformHttpLib();
+ }
+
+ public readonly PROTOCOL_VERSION = "0:0:0";
+
+ isCompatible(version: string): boolean {
+ const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version);
+ return compare?.compatible ?? false;
+ }
+
+ /**
+ * https://docs.taler.net/core/api-bank-revenue.html#get--config
+ *
+ */
+ async getConfig(auth?: UsernameAndPassword) {
+ const url = new URL(`config`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers: {
+ Authorization: auth
+ ? makeBasicAuthHeader(auth.username, auth.password)
+ : undefined,
+ },
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForRevenueConfig());
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+ /**
+ * https://docs.taler.net/core/api-bank-revenue.html#get--history
+ *
+ * @returns
+ */
+ async getHistory(
+ auth?: UsernameAndPassword,
+ params?: PaginationParams & LongPollParams,
+ ) {
+ const url = new URL(`history`, this.baseUrl);
+ addPaginationParams(url, params);
+ addLongPollingParam(url, params);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers: {
+ Authorization: auth
+ ? makeBasicAuthHeader(auth.username, auth.password)
+ : undefined,
+ },
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForRevenueIncomingHistory());
+ case HttpStatusCode.BadRequest:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+}
diff --git a/packages/taler-util/src/http-client/bank-wire.ts b/packages/taler-util/src/http-client/bank-wire.ts
new file mode 100644
index 000000000..a8c976a80
--- /dev/null
+++ b/packages/taler-util/src/http-client/bank-wire.ts
@@ -0,0 +1,226 @@
+/*
+ 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 { HttpRequestLibrary, makeBasicAuthHeader, readTalerErrorResponse } from "../http-common.js";
+import { HttpStatusCode } from "../http-status-codes.js";
+import { createPlatformHttpLib } from "../http.js";
+import {
+ FailCasesByMethod,
+ ResultByMethod,
+ opFixedSuccess,
+ opKnownHttpFailure,
+ opSuccessFromHttp,
+ opUnknownFailure,
+} from "../operation.js";
+import {
+ LongPollParams,
+ PaginationParams,
+ TalerWireGatewayApi,
+ codecForAddIncomingResponse,
+ codecForIncomingHistory,
+ codecForOutgoingHistory,
+ codecForTransferResponse,
+} from "./types.js";
+import { addLongPollingParam, addPaginationParams } from "./utils.js";
+
+export type TalerWireGatewayResultByMethod<
+ prop extends keyof TalerWireGatewayHttpClient,
+> = ResultByMethod<TalerWireGatewayHttpClient, prop>;
+export type TalerWireGatewayErrorsByMethod<
+ prop extends keyof TalerWireGatewayHttpClient,
+> = FailCasesByMethod<TalerWireGatewayHttpClient, prop>;
+
+/**
+ * The API is used by the exchange to trigger transactions and query
+ * incoming transactions, as well as by the auditor to query incoming
+ * and outgoing transactions.
+ *
+ * https://docs.taler.net/core/api-bank-wire.html
+ */
+export class TalerWireGatewayHttpClient {
+ httpLib: HttpRequestLibrary;
+
+ constructor(
+ readonly baseUrl: string,
+ readonly username: string,
+ httpClient?: HttpRequestLibrary,
+ ) {
+ this.httpLib = httpClient ?? createPlatformHttpLib();
+ }
+ // public readonly PROTOCOL_VERSION = "4:0:0";
+ // isCompatible(version: string): boolean {
+ // const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version)
+ // return compare?.compatible ?? false
+ // }
+
+ // /**
+ // * https://docs.taler.net/core/api-corebank.html#config
+ // *
+ // */
+ // async getConfig() {
+ // const url = new URL(`config`, this.baseUrl);
+ // const resp = await this.httpLib.fetch(url.href, {
+ // method: "GET"
+ // });
+ // switch (resp.status) {
+ // case HttpStatusCode.Ok: return opSuccess(resp, codecForCoreBankConfig())
+ // default: return opUnknownFailure(resp, await readTalerErrorResponse(resp))
+ // }
+ // }
+
+ /**
+ * https://docs.taler.net/core/api-bank-wire.html#post--transfer
+ *
+ */
+ async transfer(auth: string, body: TalerWireGatewayApi.TransferRequest) {
+ const url = new URL(`transfer`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ headers: {
+ Authorization: makeBasicAuthHeader(this.username, auth),
+ },
+ body,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForTransferResponse());
+ //FIXME: show more details in docs
+ case HttpStatusCode.BadRequest:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ //FIXME: show more details in docs
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-bank-wire.html#get--history-incoming
+ *
+ */
+ async getHistoryIncoming(
+ auth: string,
+ params?: PaginationParams & LongPollParams,
+ ) {
+ const url = new URL(`history/incoming`, this.baseUrl);
+ addPaginationParams(url, params);
+ addLongPollingParam(url, params);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers: {
+ Authorization: makeBasicAuthHeader(this.username, auth),
+ },
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForIncomingHistory());
+ //FIXME: account should not be returned or make it optional
+ case HttpStatusCode.NoContent:
+ return opFixedSuccess({
+ incoming_transactions: [],
+ credit_account: undefined,
+ });
+ //FIXME: show more details in docs
+ case HttpStatusCode.BadRequest:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ //FIXME: show more details in docs
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-bank-wire.html#get--history-outgoing
+ *
+ */
+ async getHistoryOutgoing(
+ auth: string,
+ params?: PaginationParams & LongPollParams,
+ ) {
+ const url = new URL(`history/outgoing`, this.baseUrl);
+ addPaginationParams(url, params);
+ addLongPollingParam(url, params);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers: {
+ Authorization: makeBasicAuthHeader(this.username, auth),
+ },
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForOutgoingHistory());
+ //FIXME: account should not be returned or make it optional
+ case HttpStatusCode.NoContent:
+ return opFixedSuccess({
+ outgoing_transactions: [],
+ debit_account: undefined,
+ });
+ //FIXME: show more details in docs
+ case HttpStatusCode.BadRequest:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ //FIXME: show more details in docs
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-bank-wire.html#post--admin-add-incoming
+ *
+ */
+ async addIncoming(
+ auth: string,
+ body: TalerWireGatewayApi.AddIncomingRequest,
+ ) {
+ const url = new URL(`admin/add-incoming`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ headers: {
+ Authorization: makeBasicAuthHeader(this.username, auth),
+ },
+ body,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForAddIncomingResponse());
+ //FIXME: show more details in docs
+ case HttpStatusCode.BadRequest:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ //FIXME: show more details in docs
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ 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
new file mode 100644
index 000000000..aa530570d
--- /dev/null
+++ b/packages/taler-util/src/http-client/challenger.ts
@@ -0,0 +1,291 @@
+import { HttpRequestLibrary, readTalerErrorResponse } from "../http-common.js";
+import { HttpStatusCode } from "../http-status-codes.js";
+import { createPlatformHttpLib } from "../http.js";
+import { TalerCoreBankCacheEviction } from "../index.node.js";
+import { LibtoolVersion } from "../libtool-version.js";
+import {
+ FailCasesByMethod,
+ RedirectResult,
+ ResultByMethod,
+ opFixedSuccess,
+ opKnownAlternativeFailure,
+ opKnownHttpFailure,
+ opSuccessFromHttp,
+ opUnknownFailure,
+} from "../operation.js";
+import {
+ AccessToken,
+ codecForChallengeCreateResponse,
+ codecForChallengeSetupResponse,
+ codecForChallengeStatus,
+ codecForChallengerAuthResponse,
+ codecForChallengerInfoResponse,
+ codecForChallengerTermsOfServiceResponse,
+ codecForInvalidPinResponse,
+} from "./types.js";
+import { CacheEvictor, makeBearerTokenAuthHeader, nullEvictor } from "./utils.js";
+
+export type ChallengerResultByMethod<prop extends keyof ChallengerHttpClient> =
+ ResultByMethod<ChallengerHttpClient, prop>;
+export type ChallengerErrorsByMethod<prop extends keyof ChallengerHttpClient> =
+ FailCasesByMethod<ChallengerHttpClient, prop>;
+
+export enum ChallengerCacheEviction {
+ CREATE_CHALLENGE,
+}
+
+/**
+ */
+export class ChallengerHttpClient {
+ httpLib: HttpRequestLibrary;
+ cacheEvictor: CacheEvictor<ChallengerCacheEviction>;
+ public readonly PROTOCOL_VERSION = "1:0:0";
+
+ constructor(
+ readonly baseUrl: string,
+ httpClient?: HttpRequestLibrary,
+ cacheEvictor?: CacheEvictor<ChallengerCacheEviction>,
+ ) {
+ this.httpLib = httpClient ?? createPlatformHttpLib();
+ this.cacheEvictor = cacheEvictor ?? nullEvictor;
+ }
+
+ isCompatible(version: string): boolean {
+ const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version);
+ return compare?.compatible ?? false;
+ }
+ /**
+ * https://docs.taler.net/core/api-challenger.html#get--config
+ *
+ */
+ async getConfig() {
+ const url = new URL(`config`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(
+ resp,
+ codecForChallengerTermsOfServiceResponse(),
+ );
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+ /**
+ * https://docs.taler.net/core/api-challenger.html#post--setup-$CLIENT_ID
+ *
+ */
+ async setup(clientId: string, token: AccessToken) {
+ const url = new URL(`setup/${clientId}`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(token),
+ },
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForChallengeSetupResponse());
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ // LOGIN
+
+ /**
+ * https://docs.taler.net/core/api-challenger.html#post--authorize-$NONCE
+ *
+ */
+ async login(
+ nonce: string,
+ clientId: string,
+ redirectUri: string,
+ state: string | undefined,
+ ) {
+ const url = new URL(`authorize/${nonce}`, this.baseUrl);
+ url.searchParams.set("response_type", "code");
+ url.searchParams.set("client_id", clientId);
+ url.searchParams.set("redirect_uri", redirectUri);
+ if (state) {
+ url.searchParams.set("state", state);
+ }
+ // url.searchParams.set("scope", "code");
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForChallengeStatus());
+ case HttpStatusCode.BadRequest:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotAcceptable:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.InternalServerError:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ // CHALLENGE
+
+ /**
+ * https://docs.taler.net/core/api-challenger.html#post--challenge-$NONCE
+ *
+ */
+ async challenge(nonce: string, body: Record<"email", string>) {
+ const url = new URL(`challenge/${nonce}`, this.baseUrl);
+
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body: new URLSearchParams(Object.entries(body)).toString(),
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ redirect: "manual",
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok: {
+ await this.cacheEvictor.notifySuccess(
+ ChallengerCacheEviction.CREATE_CHALLENGE,
+ );
+ return opSuccessFromHttp(resp, codecForChallengeCreateResponse());
+ }
+ 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:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotAcceptable:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.TooManyRequests:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.InternalServerError:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ // SOLVE
+
+ /**
+ * https://docs.taler.net/core/api-challenger.html#post--solve-$NONCE
+ *
+ */
+ async solve(nonce: string, body: Record<string, string>) {
+ const url = new URL(`solve/${nonce}`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body: new URLSearchParams(Object.entries(body)).toString(),
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ redirect: "manual",
+ });
+ switch (resp.status) {
+ 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.Forbidden:
+ return opKnownAlternativeFailure(
+ resp,
+ resp.status,
+ codecForInvalidPinResponse(),
+ );
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotAcceptable:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.TooManyRequests:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.InternalServerError:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ // AUTH
+
+ /**
+ * https://docs.taler.net/core/api-challenger.html#post--token
+ *
+ */
+ async token(
+ client_id: string,
+ redirect_uri: string,
+ client_secret: AccessToken,
+ code: string,
+ ) {
+ const url = new URL(`token`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ body: new URLSearchParams(
+ Object.entries({
+ client_id,
+ redirect_uri,
+ client_secret,
+ code,
+ grant_type: "authorization_code",
+ }),
+ ).toString(),
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForChallengerAuthResponse());
+ case HttpStatusCode.Forbidden:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ // INFO
+
+ /**
+ * https://docs.taler.net/core/api-challenger.html#get--info
+ *
+ */
+ async info(token: AccessToken) {
+ const url = new URL(`info`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(token),
+ },
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForChallengerInfoResponse());
+ case HttpStatusCode.Forbidden:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+}
diff --git a/packages/taler-util/src/http-client/exchange.ts b/packages/taler-util/src/http-client/exchange.ts
new file mode 100644
index 000000000..ea7f44cf9
--- /dev/null
+++ b/packages/taler-util/src/http-client/exchange.ts
@@ -0,0 +1,242 @@
+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 { hash } from "../nacl-fast.js";
+import {
+ FailCasesByMethod,
+ ResultByMethod,
+ opEmptySuccess,
+ opFixedSuccess,
+ opKnownHttpFailure,
+ opSuccessFromHttp,
+ opUnknownFailure,
+} from "../operation.js";
+import {
+ TalerSignaturePurpose,
+ amountToBuffer,
+ bufferForUint32,
+ buildSigPS,
+ decodeCrock,
+ eddsaSign,
+ encodeCrock,
+ stringToBytes,
+ timestampRoundedToBuffer,
+} from "../taler-crypto.js";
+import {
+ OfficerAccount,
+ PaginationParams,
+ SigningKey,
+ TalerExchangeApi,
+ codecForAmlDecisionDetails,
+ codecForAmlRecords,
+ codecForExchangeConfig,
+ codecForExchangeKeys,
+} from "./types.js";
+import { addPaginationParams } from "./utils.js";
+
+export type TalerExchangeResultByMethod<
+ prop extends keyof TalerExchangeHttpClient,
+> = ResultByMethod<TalerExchangeHttpClient, prop>;
+export type TalerExchangeErrorsByMethod<
+ prop extends keyof TalerExchangeHttpClient,
+> = FailCasesByMethod<TalerExchangeHttpClient, prop>;
+
+/**
+ */
+export class TalerExchangeHttpClient {
+ httpLib: HttpRequestLibrary;
+ public readonly PROTOCOL_VERSION = "18:0:1";
+
+ constructor(
+ readonly baseUrl: string,
+ httpClient?: HttpRequestLibrary,
+ ) {
+ this.httpLib = httpClient ?? createPlatformHttpLib();
+ }
+
+ isCompatible(version: string): boolean {
+ const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version);
+ return compare?.compatible ?? false;
+ }
+ /**
+ * https://docs.taler.net/core/api-exchange.html#get--config
+ *
+ */
+ async getConfig() {
+ const url = new URL(`config`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForExchangeConfig());
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+ /**
+ * https://docs.taler.net/core/api-merchant.html#get--config
+ *
+ * PARTIALLY IMPLEMENTED!!
+ */
+ async getKeys() {
+ const url = new URL(`keys`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForExchangeKeys());
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ // TERMS
+
+ //
+ // AML operations
+ //
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-decisions-$STATE
+ *
+ */
+ async getDecisionsByState(
+ auth: OfficerAccount,
+ state: TalerExchangeApi.AmlState,
+ pagination?: PaginationParams,
+ ) {
+ const url = new URL(
+ `aml/${auth.id}/decisions/${TalerExchangeApi.AmlState[state]}`,
+ this.baseUrl,
+ );
+ addPaginationParams(url, pagination);
+
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers: {
+ "Taler-AML-Officer-Signature": buildQuerySignature(auth.signingKey),
+ },
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForAmlRecords());
+ case HttpStatusCode.NoContent:
+ return opFixedSuccess({ records: [] });
+ //this should be unauthorized
+ case HttpStatusCode.Forbidden:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-decision-$H_PAYTO
+ *
+ */
+ async getDecisionDetails(auth: OfficerAccount, account: string) {
+ const url = new URL(`aml/${auth.id}/decision/${account}`, this.baseUrl);
+
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers: {
+ "Taler-AML-Officer-Signature": buildQuerySignature(auth.signingKey),
+ },
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForAmlDecisionDetails());
+ case HttpStatusCode.NoContent:
+ return opFixedSuccess({ aml_history: [], kyc_attributes: [] });
+ //this should be unauthorized
+ case HttpStatusCode.Forbidden:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--aml-$OFFICER_PUB-decision
+ *
+ */
+ async addDecisionDetails(
+ auth: OfficerAccount,
+ decision: Omit<TalerExchangeApi.AmlDecision, "officer_sig">,
+ ) {
+ const url = new URL(`aml/${auth.id}/decision`, this.baseUrl);
+
+ const body = buildDecisionSignature(auth.signingKey, decision);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.NoContent:
+ return opEmptySuccess(resp);
+ //FIXME: this should be unauthorized
+ case HttpStatusCode.Forbidden:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ //FIXME: this two need to be split by error code
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+}
+
+function buildQuerySignature(key: SigningKey): string {
+ const sigBlob = buildSigPS(
+ TalerSignaturePurpose.TALER_SIGNATURE_AML_QUERY,
+ ).build();
+
+ return encodeCrock(eddsaSign(sigBlob, key));
+}
+
+function buildDecisionSignature(
+ key: SigningKey,
+ decision: Omit<TalerExchangeApi.AmlDecision, "officer_sig">,
+): TalerExchangeApi.AmlDecision {
+ const zero = new Uint8Array(new ArrayBuffer(64));
+
+ const sigBlob = buildSigPS(TalerSignaturePurpose.TALER_SIGNATURE_AML_DECISION)
+ //TODO: new need the null terminator, also in the exchange
+ .put(hash(stringToBytes(decision.justification))) //check null
+ .put(timestampRoundedToBuffer(decision.decision_time))
+ .put(amountToBuffer(decision.new_threshold))
+ .put(decodeCrock(decision.h_payto))
+ .put(zero) //kyc_requirement
+ .put(bufferForUint32(decision.new_state))
+ .build();
+
+ const officer_sig = encodeCrock(eddsaSign(sigBlob, key));
+ return {
+ ...decision,
+ officer_sig,
+ };
+}
diff --git a/packages/taler-util/src/http-client/merchant.ts b/packages/taler-util/src/http-client/merchant.ts
new file mode 100644
index 000000000..d682dcfa0
--- /dev/null
+++ b/packages/taler-util/src/http-client/merchant.ts
@@ -0,0 +1,2343 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ AccessToken,
+ FailCasesByMethod,
+ HttpStatusCode,
+ LibtoolVersion,
+ PaginationParams,
+ ResultByMethod,
+ TalerMerchantApi,
+ codecForAbortResponse,
+ codecForAccountAddResponse,
+ codecForAccountKycRedirects,
+ codecForAccountsSummaryResponse,
+ codecForBankAccountEntry,
+ codecForClaimResponse,
+ codecForInstancesResponse,
+ codecForInventorySummaryResponse,
+ codecForMerchantConfig,
+ codecForMerchantOrderPrivateStatusResponse,
+ codecForMerchantRefundResponse,
+ codecForOrderHistory,
+ codecForOtpDeviceDetails,
+ codecForOtpDeviceSummaryResponse,
+ codecForOutOfStockResponse,
+ codecForPaidRefundStatusResponse,
+ codecForPaymentResponse,
+ codecForPostOrderResponse,
+ codecForProductDetail,
+ codecForQueryInstancesResponse,
+ codecForStatusGoto,
+ codecForStatusPaid,
+ codecForStatusStatusUnpaid,
+ codecForTansferList,
+ codecForTemplateDetails,
+ codecForTemplateSummaryResponse,
+ codecForTokenFamiliesList,
+ codecForTokenFamilyDetails,
+ codecForWalletRefundResponse,
+ codecForWalletTemplateDetails,
+ codecForWebhookDetails,
+ codecForWebhookSummaryResponse,
+ opEmptySuccess,
+ opKnownAlternativeFailure,
+ opKnownHttpFailure,
+} from "@gnu-taler/taler-util";
+import {
+ HttpRequestLibrary,
+ HttpResponse,
+ createPlatformHttpLib,
+ readTalerErrorResponse,
+} from "@gnu-taler/taler-util/http";
+import { opSuccessFromHttp, opUnknownFailure } from "../operation.js";
+import {
+ CacheEvictor,
+ addMerchantPaginationParams,
+ makeBearerTokenAuthHeader,
+ nullEvictor,
+} from "./utils.js";
+
+export type TalerMerchantInstanceResultByMethod<
+ prop extends keyof TalerMerchantInstanceHttpClient,
+> = ResultByMethod<TalerMerchantInstanceHttpClient, prop>;
+export type TalerMerchantInstanceErrorsByMethod<
+ prop extends keyof TalerMerchantInstanceHttpClient,
+> = FailCasesByMethod<TalerMerchantInstanceHttpClient, prop>;
+
+export enum TalerMerchantInstanceCacheEviction {
+ CREATE_ORDER,
+ UPDATE_ORDER,
+ DELETE_ORDER,
+ UPDATE_CURRENT_INSTANCE,
+ DELETE_CURRENT_INSTANCE,
+ CREATE_BANK_ACCOUNT,
+ UPDATE_BANK_ACCOUNT,
+ DELETE_BANK_ACCOUNT,
+ CREATE_PRODUCT,
+ UPDATE_PRODUCT,
+ DELETE_PRODUCT,
+ CREATE_TRANSFER,
+ DELETE_TRANSFER,
+ CREATE_DEVICE,
+ UPDATE_DEVICE,
+ DELETE_DEVICE,
+ CREATE_TEMPLATE,
+ UPDATE_TEMPLATE,
+ DELETE_TEMPLATE,
+ CREATE_WEBHOOK,
+ UPDATE_WEBHOOK,
+ DELETE_WEBHOOK,
+ CREATE_TOKENFAMILY,
+ UPDATE_TOKENFAMILY,
+ DELETE_TOKENFAMILY,
+ LAST,
+}
+export enum TalerMerchantManagementCacheEviction {
+ CREATE_INSTANCE = TalerMerchantInstanceCacheEviction.LAST + 1,
+ UPDATE_INSTANCE,
+ DELETE_INSTANCE,
+}
+/**
+ * Protocol version spoken with the core bank.
+ *
+ * Endpoint must be ordered in the same way that in the docs
+ * Response code (http and taler) must have the same order that in the docs
+ * That way is easier to see changes
+ *
+ * Uses libtool's current:revision:age versioning.
+ */
+export class TalerMerchantInstanceHttpClient {
+ public readonly PROTOCOL_VERSION = "10:0:6";
+
+ readonly httpLib: HttpRequestLibrary;
+ readonly cacheEvictor: CacheEvictor<TalerMerchantInstanceCacheEviction>;
+
+ constructor(
+ readonly baseUrl: string,
+ httpClient?: HttpRequestLibrary,
+ cacheEvictor?: CacheEvictor<TalerMerchantInstanceCacheEviction>,
+ ) {
+ this.httpLib = httpClient ?? createPlatformHttpLib();
+ this.cacheEvictor = cacheEvictor ?? nullEvictor;
+ }
+
+ isCompatible(version: string): boolean {
+ const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version);
+ return compare?.compatible ?? false;
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#get--config
+ *
+ */
+ async getConfig() {
+ const url = new URL(`config`, this.baseUrl);
+
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForMerchantConfig());
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ //
+ // Wallet API
+ //
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-orders-$ORDER_ID-claim
+ */
+ async claimOrder(orderId: string, body: TalerMerchantApi.ClaimRequest) {
+ const url = new URL(`orders/${orderId}/claim`, this.baseUrl);
+
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.UPDATE_ORDER,
+ );
+ return opSuccessFromHttp(resp, codecForClaimResponse());
+ }
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-orders-$ORDER_ID-pay
+ */
+ async makePayment(orderId: string, body: TalerMerchantApi.PayRequest) {
+ const url = new URL(`orders/${orderId}/pay`, this.baseUrl);
+
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.UPDATE_ORDER,
+ );
+ return opSuccessFromHttp(resp, codecForPaymentResponse());
+ }
+ case HttpStatusCode.BadRequest:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.PaymentRequired:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Forbidden:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.RequestTimeout:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Gone:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.PreconditionFailed:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.BadGateway:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.GatewayTimeout:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-orders-$ORDER_ID
+ */
+
+ async getPaymentStatus(
+ orderId: string,
+ params: TalerMerchantApi.PaymentStatusRequestParams = {},
+ ) {
+ const url = new URL(`orders/${orderId}`, this.baseUrl);
+
+ if (params.allowRefundedForRepurchase !== undefined) {
+ url.searchParams.set(
+ "allow_refunded_for_repurchase",
+ params.allowRefundedForRepurchase ? "YES" : "NO",
+ );
+ }
+ if (params.awaitRefundObtained !== undefined) {
+ url.searchParams.set(
+ "await_refund_obtained",
+ params.allowRefundedForRepurchase ? "YES" : "NO",
+ );
+ }
+ if (params.claimToken !== undefined) {
+ url.searchParams.set("token", params.claimToken);
+ }
+ if (params.contractTermHash !== undefined) {
+ url.searchParams.set("h_contract", params.contractTermHash);
+ }
+ if (params.refund !== undefined) {
+ url.searchParams.set("refund", params.refund);
+ }
+ if (params.sessionId !== undefined) {
+ url.searchParams.set("session_id", params.sessionId);
+ }
+ if (params.timeout !== undefined) {
+ url.searchParams.set("timeout_ms", String(params.timeout));
+ }
+
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ // body,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForStatusPaid());
+ case HttpStatusCode.Accepted:
+ return opSuccessFromHttp(resp, codecForStatusGoto());
+ // case HttpStatusCode.Found: not possible since content is not HTML
+ case HttpStatusCode.PaymentRequired:
+ return opSuccessFromHttp(resp, codecForStatusStatusUnpaid());
+ case HttpStatusCode.Forbidden:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotAcceptable:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#demonstrating-payment
+ */
+ async demostratePayment(orderId: string, body: TalerMerchantApi.PaidRequest) {
+ const url = new URL(`orders/${orderId}/paid`, this.baseUrl);
+
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.UPDATE_ORDER,
+ );
+ return opSuccessFromHttp(resp, codecForPaidRefundStatusResponse());
+ }
+ case HttpStatusCode.BadRequest:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Forbidden:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#aborting-incomplete-payments
+ */
+ async abortIncompletePayment(
+ orderId: string,
+ body: TalerMerchantApi.AbortRequest,
+ ) {
+ const url = new URL(`orders/${orderId}/abort`, this.baseUrl);
+
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.UPDATE_ORDER,
+ );
+ return opSuccessFromHttp(resp, codecForAbortResponse());
+ }
+ case HttpStatusCode.BadRequest:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Forbidden:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#obtaining-refunds
+ */
+ async obtainRefund(
+ orderId: string,
+ body: TalerMerchantApi.WalletRefundRequest,
+ ) {
+ const url = new URL(`orders/${orderId}/refund`, this.baseUrl);
+
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.UPDATE_ORDER,
+ );
+ return opSuccessFromHttp(resp, codecForWalletRefundResponse());
+ }
+ case HttpStatusCode.BadRequest:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Forbidden:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ //
+ // Management
+ //
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-auth
+ */
+ async updateCurrentInstanceAuthentication(
+ token: AccessToken | undefined,
+ body: TalerMerchantApi.InstanceAuthConfigurationMessage,
+ ) {
+ const url = new URL(`private/auth`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok: // FIXME: missing in docs
+ return opEmptySuccess(resp);
+ case HttpStatusCode.NoContent:
+ return opEmptySuccess(resp);
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#patch-[-instances-$INSTANCE]-private
+ */
+ async updateCurrentInstance(
+ token: AccessToken | undefined,
+ body: TalerMerchantApi.InstanceReconfigurationMessage,
+ ) {
+ const url = new URL(`private`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "PATCH",
+ body,
+ headers,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.UPDATE_CURRENT_INSTANCE,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private
+ *
+ */
+ async getCurrentInstanceDetails(token: AccessToken) {
+ const url = new URL(`private`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForQueryInstancesResponse());
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCE]-private
+ */
+ async deleteCurrentInstance(
+ token: AccessToken | undefined,
+ params: { purge?: boolean } = {},
+ ) {
+ const url = new URL(`private`, this.baseUrl);
+
+ if (params.purge !== undefined) {
+ url.searchParams.set("purge", params.purge ? "YES" : "NO");
+ }
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "DELETE",
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.DELETE_CURRENT_INSTANCE,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#get--instances-$INSTANCE-private-kyc
+ */
+ async getCurrentIntanceKycStatus(
+ token: AccessToken | undefined,
+ params: TalerMerchantApi.GetKycStatusRequestParams = {},
+ ) {
+ const url = new URL(`private/kyc`, this.baseUrl);
+
+ if (params.wireHash) {
+ url.searchParams.set("h_wire", params.wireHash);
+ }
+ if (params.exchangeURL) {
+ url.searchParams.set("exchange_url", params.exchangeURL);
+ }
+ if (params.timeout) {
+ url.searchParams.set("timeout_ms", String(params.timeout));
+ }
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Accepted:
+ return opSuccessFromHttp(resp, codecForAccountKycRedirects());
+ case HttpStatusCode.NoContent:
+ return opEmptySuccess(resp);
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.BadGateway:
+ return opKnownAlternativeFailure(
+ resp,
+ resp.status,
+ codecForAccountKycRedirects(),
+ );
+ case HttpStatusCode.ServiceUnavailable:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.GatewayTimeout:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ //
+ // Bank Accounts
+ //
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-accounts
+ */
+ async addBankAccount(
+ token: AccessToken | undefined,
+ body: TalerMerchantApi.AccountAddDetails,
+ ) {
+ const url = new URL(`private/accounts`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.CREATE_BANK_ACCOUNT,
+ );
+ return opSuccessFromHttp(resp, codecForAccountAddResponse());
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#patch-[-instances-$INSTANCE]-private-accounts-$H_WIRE
+ */
+ async updateBankAccount(
+ token: AccessToken | undefined,
+ wireAccount: string,
+ body: TalerMerchantApi.AccountPatchDetails,
+ ) {
+ const url = new URL(`private/accounts/${wireAccount}`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "PATCH",
+ body,
+ headers,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.UPDATE_BANK_ACCOUNT,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-accounts
+ */
+ async listBankAccounts(token: AccessToken, params?: PaginationParams) {
+ const url = new URL(`private/accounts`, this.baseUrl);
+
+ // addMerchantPaginationParams(url, params);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForAccountsSummaryResponse());
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-accounts-$H_WIRE
+ */
+ async getBankAccountDetails(
+ token: AccessToken | undefined,
+ wireAccount: string,
+ ) {
+ const url = new URL(`private/accounts/${wireAccount}`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForBankAccountEntry());
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCE]-private-accounts-$H_WIRE
+ */
+ async deleteBankAccount(token: AccessToken | undefined, wireAccount: string) {
+ const url = new URL(`private/accounts/${wireAccount}`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "DELETE",
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.DELETE_BANK_ACCOUNT,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ //
+ // Inventory Management
+ //
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-products
+ */
+ async addProduct(
+ token: AccessToken | undefined,
+ body: TalerMerchantApi.ProductAddDetail,
+ ) {
+ const url = new URL(`private/products`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.CREATE_PRODUCT,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#patch-[-instances-$INSTANCE]-private-products-$PRODUCT_ID
+ */
+ async updateProduct(
+ token: AccessToken | undefined,
+ productId: string,
+ body: TalerMerchantApi.ProductPatchDetail,
+ ) {
+ const url = new URL(`private/products/${productId}`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "PATCH",
+ body,
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.UPDATE_PRODUCT,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-products
+ */
+ async listProducts(
+ token: AccessToken | undefined,
+ params?: PaginationParams,
+ ) {
+ const url = new URL(`private/products`, this.baseUrl);
+
+ addMerchantPaginationParams(url, params);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForInventorySummaryResponse());
+ case HttpStatusCode.Unauthorized: // FIXME: not in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-products-$PRODUCT_ID
+ */
+ async getProductDetails(token: AccessToken | undefined, productId: string) {
+ const url = new URL(`private/products/${productId}`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForProductDetail());
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#reserving-inventory
+ */
+ async lockProduct(
+ token: AccessToken | undefined,
+ productId: string,
+ body: TalerMerchantApi.LockRequest,
+ ) {
+ const url = new URL(`private/products/${productId}/lock`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.UPDATE_PRODUCT,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Gone:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#removing-products-from-inventory
+ */
+ async deleteProduct(token: AccessToken | undefined, productId: string) {
+ const url = new URL(`private/products/${productId}`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "DELETE",
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.DELETE_PRODUCT,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ //
+ // Payment processing
+ //
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-orders
+ */
+ async createOrder(
+ token: AccessToken | undefined,
+ body: TalerMerchantApi.PostOrderRequest,
+ ) {
+ const url = new URL(`private/orders`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ headers,
+ });
+ return this.procesOrderCreationResponse(resp);
+ }
+
+ private async procesOrderCreationResponse(resp: HttpResponse) {
+ switch (resp.status) {
+ case HttpStatusCode.Ok: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.CREATE_ORDER,
+ );
+ return opSuccessFromHttp(resp, codecForPostOrderResponse());
+ }
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Gone:
+ return opKnownAlternativeFailure(
+ resp,
+ resp.status,
+ codecForOutOfStockResponse(),
+ );
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#inspecting-orders
+ */
+ async listOrders(
+ token: AccessToken | undefined,
+ params: TalerMerchantApi.ListOrdersRequestParams = {},
+ ) {
+ const url = new URL(`private/orders`, this.baseUrl);
+
+ if (params.date) {
+ url.searchParams.set("date_s", String(params.date));
+ }
+ if (params.fulfillmentUrl) {
+ url.searchParams.set("fulfillment_url", params.fulfillmentUrl);
+ }
+ if (params.paid !== undefined) {
+ url.searchParams.set("paid", params.paid ? "YES" : "NO");
+ }
+ if (params.refunded !== undefined) {
+ url.searchParams.set("refunded", params.refunded ? "YES" : "NO");
+ }
+ if (params.sessionId) {
+ url.searchParams.set("session_id", params.sessionId);
+ }
+ if (params.timeout) {
+ url.searchParams.set("timeout", String(params.timeout));
+ }
+ if (params.wired !== undefined) {
+ url.searchParams.set("wired", params.wired ? "YES" : "NO");
+ }
+ addMerchantPaginationParams(url, params);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForOrderHistory());
+ case HttpStatusCode.NotFound: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-orders-$ORDER_ID
+ */
+ async getOrderDetails(
+ token: AccessToken | undefined,
+ orderId: string,
+ params: TalerMerchantApi.GetOrderRequestParams = {},
+ ) {
+ const url = new URL(`private/orders/${orderId}`, this.baseUrl);
+
+ if (params.allowRefundedForRepurchase !== undefined) {
+ url.searchParams.set(
+ "allow_refunded_for_repurchase",
+ params.allowRefundedForRepurchase ? "YES" : "NO",
+ );
+ }
+ if (params.sessionId) {
+ url.searchParams.set("session_id", params.sessionId);
+ }
+ if (params.timeout) {
+ url.searchParams.set("timeout_ms", String(params.timeout));
+ }
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(
+ resp,
+ codecForMerchantOrderPrivateStatusResponse(),
+ );
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.BadGateway:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.GatewayTimeout:
+ return opKnownAlternativeFailure(
+ resp,
+ resp.status,
+ codecForOutOfStockResponse(),
+ );
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#private-order-data-cleanup
+ */
+ async forgetOrder(
+ token: AccessToken | undefined,
+ orderId: string,
+ body: TalerMerchantApi.ForgetRequest,
+ ) {
+ const url = new URL(`private/orders/${orderId}/forget`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "PATCH",
+ body,
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.UPDATE_ORDER,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.NoContent:
+ return opEmptySuccess(resp);
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.BadRequest:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCE]-private-orders-$ORDER_ID
+ */
+ async deleteOrder(token: AccessToken | undefined, orderId: string) {
+ const url = new URL(`private/orders/${orderId}`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "DELETE",
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.DELETE_ORDER,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ //
+ // Refunds
+ //
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-orders-$ORDER_ID-refund
+ */
+ async addRefund(
+ token: AccessToken | undefined,
+ orderId: string,
+ body: TalerMerchantApi.RefundRequest,
+ ) {
+ const url = new URL(`private/orders/${orderId}/refund`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.UPDATE_ORDER,
+ );
+ return opSuccessFromHttp(resp, codecForMerchantRefundResponse());
+ }
+ case HttpStatusCode.Forbidden:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Gone:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ //
+ // Wire Transfer
+ //
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-transfers
+ */
+ async informWireTransfer(
+ token: AccessToken | undefined,
+ body: TalerMerchantApi.TransferInformation,
+ ) {
+ const url = new URL(`private/transfers`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.CREATE_TRANSFER,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-transfers
+ */
+ async listWireTransfers(
+ token: AccessToken | undefined,
+ params: TalerMerchantApi.ListWireTransferRequestParams = {},
+ ) {
+ const url = new URL(`private/transfers`, this.baseUrl);
+
+ if (params.after) {
+ url.searchParams.set("after", String(params.after));
+ }
+ if (params.before) {
+ url.searchParams.set("before", String(params.before));
+ }
+ if (params.paytoURI) {
+ url.searchParams.set("payto_uri", params.paytoURI);
+ }
+ if (params.verified !== undefined) {
+ url.searchParams.set("verified", params.verified ? "YES" : "NO");
+ }
+ addMerchantPaginationParams(url, params);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForTansferList());
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCE]-private-transfers-$TID
+ */
+ async deleteWireTransfer(token: AccessToken | undefined, transferId: string) {
+ const url = new URL(`private/transfers/${transferId}`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "DELETE",
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.DELETE_TRANSFER,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ //
+ // OTP Devices
+ //
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-otp-devices
+ */
+ async addOtpDevice(
+ token: AccessToken | undefined,
+ body: TalerMerchantApi.OtpDeviceAddDetails,
+ ) {
+ const url = new URL(`private/otp-devices`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.CREATE_DEVICE,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#patch-[-instances-$INSTANCE]-private-otp-devices-$DEVICE_ID
+ */
+ async updateOtpDevice(
+ token: AccessToken | undefined,
+ deviceId: string,
+ body: TalerMerchantApi.OtpDevicePatchDetails,
+ ) {
+ const url = new URL(`private/otp-devices/${deviceId}`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "PATCH",
+ body,
+ headers,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.UPDATE_DEVICE,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-otp-devices
+ */
+ async listOtpDevices(
+ token: AccessToken | undefined,
+ params?: PaginationParams,
+ ) {
+ const url = new URL(`private/otp-devices`, this.baseUrl);
+
+ addMerchantPaginationParams(url, params);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForOtpDeviceSummaryResponse());
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-otp-devices-$DEVICE_ID
+ */
+ async getOtpDeviceDetails(
+ token: AccessToken | undefined,
+ deviceId: string,
+ params: TalerMerchantApi.GetOtpDeviceRequestParams = {},
+ ) {
+ const url = new URL(`private/otp-devices/${deviceId}`, this.baseUrl);
+
+ if (params.faketime) {
+ url.searchParams.set("faketime", String(params.faketime));
+ }
+ if (params.price) {
+ url.searchParams.set("price", params.price);
+ }
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForOtpDeviceDetails());
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCE]-private-otp-devices-$DEVICE_ID
+ */
+ async deleteOtpDevice(token: AccessToken | undefined, deviceId: string) {
+ const url = new URL(`private/otp-devices/${deviceId}`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "DELETE",
+ headers,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.DELETE_DEVICE,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ //
+ // Templates
+ //
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-templates
+ */
+ async addTemplate(
+ token: AccessToken | undefined,
+ body: TalerMerchantApi.TemplateAddDetails,
+ ) {
+ const url = new URL(`private/templates`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ headers,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.CREATE_TEMPLATE,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#patch-[-instances-$INSTANCE]-private-templates-$TEMPLATE_ID
+ */
+ async updateTemplate(
+ token: AccessToken | undefined,
+ templateId: string,
+ body: TalerMerchantApi.TemplatePatchDetails,
+ ) {
+ const url = new URL(`private/templates/${templateId}`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "PATCH",
+ body,
+ headers,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.UPDATE_TEMPLATE,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#inspecting-template
+ */
+ async listTemplates(
+ token: AccessToken | undefined,
+ params?: PaginationParams,
+ ) {
+ const url = new URL(`private/templates`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForTemplateSummaryResponse());
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-templates-$TEMPLATE_ID
+ */
+ async getTemplateDetails(token: AccessToken | undefined, templateId: string) {
+ const url = new URL(`private/templates/${templateId}`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForTemplateDetails());
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCE]-private-templates-$TEMPLATE_ID
+ */
+ async deleteTemplate(token: AccessToken | undefined, templateId: string) {
+ const url = new URL(`private/templates/${templateId}`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "DELETE",
+ headers,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.DELETE_TEMPLATE,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-templates-$TEMPLATE_ID
+ */
+ async useTemplateGetInfo(templateId: string) {
+ const url = new URL(`templates/${templateId}`, this.baseUrl);
+
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForWalletTemplateDetails());
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCES]-templates-$TEMPLATE_ID
+ */
+ async useTemplateCreateOrder(
+ templateId: string,
+ body: TalerMerchantApi.UsingTemplateDetails,
+ ) {
+ const url = new URL(`templates/${templateId}`, this.baseUrl);
+
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ });
+
+ return this.procesOrderCreationResponse(resp);
+ }
+
+ //
+ // Webhooks
+ //
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCES]-private-webhooks
+ */
+ async addWebhook(
+ token: AccessToken | undefined,
+ body: TalerMerchantApi.WebhookAddDetails,
+ ) {
+ const url = new URL(`private/webhooks`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.CREATE_WEBHOOK,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#patch-[-instances-$INSTANCES]-private-webhooks-$WEBHOOK_ID
+ */
+ async updateWebhook(
+ token: AccessToken | undefined,
+ webhookId: string,
+ body: TalerMerchantApi.WebhookPatchDetails,
+ ) {
+ const url = new URL(`private/webhooks/${webhookId}`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "PATCH",
+ body,
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.UPDATE_WEBHOOK,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCES]-private-webhooks
+ */
+ async listWebhooks(
+ token: AccessToken | undefined,
+ params?: PaginationParams,
+ ) {
+ const url = new URL(`private/webhooks`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForWebhookSummaryResponse());
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCES]-private-webhooks-$WEBHOOK_ID
+ */
+ async getWebhookDetails(token: AccessToken | undefined, webhookId: string) {
+ const url = new URL(`private/webhooks/${webhookId}`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.NoContent:
+ return opSuccessFromHttp(resp, codecForWebhookDetails());
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCES]-private-webhooks-$WEBHOOK_ID
+ */
+ async deleteWebhook(token: AccessToken | undefined, webhookId: string) {
+ const url = new URL(`private/webhooks/${webhookId}`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "DELETE",
+ headers,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.DELETE_WEBHOOK,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ //
+ // token families
+ //
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCES]-private-tokenfamilies
+ */
+ async createTokenFamily(
+ token: AccessToken | undefined,
+ body: TalerMerchantApi.TokenFamilyCreateRequest,
+ ) {
+ const url = new URL(`private/tokenfamilies`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.CREATE_TOKENFAMILY,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#patch-[-instances-$INSTANCES]-private-tokenfamilies-$TOKEN_FAMILY_SLUG
+ */
+ async updateTokenFamily(
+ token: AccessToken | undefined,
+ tokenSlug: string,
+ body: TalerMerchantApi.TokenFamilyUpdateRequest,
+ ) {
+ const url = new URL(`private/tokenfamilies/${tokenSlug}`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ headers,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.UPDATE_TOKENFAMILY,
+ );
+ return opSuccessFromHttp(resp, codecForTokenFamilyDetails());
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCES]-private-tokenfamilies
+ */
+ async listTokenFamilies(
+ token: AccessToken | undefined,
+ params?: PaginationParams,
+ ) {
+ const url = new URL(`private/tokenfamilies`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForTokenFamiliesList());
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCES]-private-tokenfamilies-$TOKEN_FAMILY_SLUG
+ */
+ async getTokenFamilyDetails(
+ token: AccessToken | undefined,
+ tokenSlug: string,
+ ) {
+ const url = new URL(`private/tokenfamilies/${tokenSlug}`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForTokenFamilyDetails());
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCES]-private-tokenfamilies-$TOKEN_FAMILY_SLUG
+ */
+ async deleteTokenFamily(token: AccessToken | undefined, tokenSlug: string) {
+ const url = new URL(`private/tokenfamilies/${tokenSlug}`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "DELETE",
+ headers,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.DELETE_TOKENFAMILY,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * Get the auth api against the current instance
+ *
+ * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-token
+ * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCE]-private-token
+ */
+ getAuthenticationAPI(): URL {
+ return new URL(`private/`, this.baseUrl);
+ }
+}
+
+export type TalerMerchantManagementResultByMethod<
+ prop extends keyof TalerMerchantManagementHttpClient,
+> = ResultByMethod<TalerMerchantManagementHttpClient, prop>;
+export type TalerMerchantManagementErrorsByMethod<
+ prop extends keyof TalerMerchantManagementHttpClient,
+> = FailCasesByMethod<TalerMerchantManagementHttpClient, prop>;
+
+export class TalerMerchantManagementHttpClient extends TalerMerchantInstanceHttpClient {
+ readonly cacheManagementEvictor: CacheEvictor<
+ TalerMerchantInstanceCacheEviction | TalerMerchantManagementCacheEviction
+ >;
+ constructor(
+ readonly baseUrl: string,
+ httpClient?: HttpRequestLibrary,
+ // cacheManagementEvictor?: CacheEvictor<TalerMerchantManagementCacheEviction>,
+ cacheEvictor?: CacheEvictor<
+ TalerMerchantInstanceCacheEviction | TalerMerchantManagementCacheEviction
+ >,
+ ) {
+ super(baseUrl, httpClient, cacheEvictor);
+ this.cacheManagementEvictor = cacheEvictor ?? nullEvictor;
+ }
+
+ getSubInstanceAPI(instanceId: string) {
+ return new URL(`instances/${instanceId}/`, this.baseUrl);
+ }
+
+ //
+ // Instance Management
+ //
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#post--management-instances
+ */
+ async createInstance(
+ token: AccessToken | undefined,
+ body: TalerMerchantApi.InstanceConfigurationMessage,
+ ) {
+ const url = new URL(`management/instances`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheManagementEvictor.notifySuccess(
+ TalerMerchantManagementCacheEviction.CREATE_INSTANCE,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#post--management-instances-$INSTANCE-auth
+ */
+ async updateInstanceAuthentication(
+ token: AccessToken | undefined,
+ instanceId: string,
+ body: TalerMerchantApi.InstanceAuthConfigurationMessage,
+ ) {
+ const url = new URL(
+ `management/instances/${instanceId}/auth`,
+ this.baseUrl,
+ );
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.NoContent:
+ return opEmptySuccess(resp);
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#patch--management-instances-$INSTANCE
+ */
+ async updateInstance(
+ token: AccessToken | undefined,
+ instanceId: string,
+ body: TalerMerchantApi.InstanceReconfigurationMessage,
+ ) {
+ const url = new URL(`management/instances/${instanceId}`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "PATCH",
+ body,
+ headers,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheManagementEvictor.notifySuccess(
+ TalerMerchantManagementCacheEviction.UPDATE_INSTANCE,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#get--management-instances
+ */
+ async listInstances(
+ token: AccessToken | undefined,
+ params?: PaginationParams,
+ ) {
+ const url = new URL(`management/instances`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForInstancesResponse());
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#get--management-instances-$INSTANCE
+ *
+ */
+ async getInstanceDetails(token: AccessToken | undefined, instanceId: string) {
+ const url = new URL(`management/instances/${instanceId}`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForQueryInstancesResponse());
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#delete--management-instances-$INSTANCE
+ */
+ async deleteInstance(
+ token: AccessToken | undefined,
+ instanceId: string,
+ params: { purge?: boolean } = {},
+ ) {
+ const url = new URL(`management/instances/${instanceId}`, this.baseUrl);
+
+ if (params.purge !== undefined) {
+ url.searchParams.set("purge", params.purge ? "YES" : "NO");
+ }
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "DELETE",
+ headers,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheManagementEvictor.notifySuccess(
+ TalerMerchantManagementCacheEviction.DELETE_INSTANCE,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#get--management-instances-$INSTANCE-kyc
+ */
+ async getIntanceKycStatus(
+ token: AccessToken | undefined,
+ instanceId: string,
+ params: TalerMerchantApi.GetKycStatusRequestParams,
+ ) {
+ const url = new URL(`management/instances/${instanceId}/kyc`, this.baseUrl);
+
+ if (params.wireHash) {
+ url.searchParams.set("h_wire", params.wireHash);
+ }
+ if (params.exchangeURL) {
+ url.searchParams.set("exchange_url", params.exchangeURL);
+ }
+ if (params.timeout) {
+ url.searchParams.set("timeout_ms", String(params.timeout));
+ }
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Accepted:
+ return opSuccessFromHttp(resp, codecForAccountKycRedirects());
+ case HttpStatusCode.NoContent:
+ return opEmptySuccess(resp);
+ case HttpStatusCode.NotFound:
+ return opEmptySuccess(resp);
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.BadGateway:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.ServiceUnavailable:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+}
diff --git a/packages/taler-util/src/http-client/officer-account.ts b/packages/taler-util/src/http-client/officer-account.ts
new file mode 100644
index 000000000..2c1426be2
--- /dev/null
+++ b/packages/taler-util/src/http-client/officer-account.ts
@@ -0,0 +1,105 @@
+/*
+ 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 {
+ EncryptionNonce,
+ LockedAccount,
+ OfficerAccount,
+ OfficerId,
+ SigningKey,
+ createEddsaKeyPair,
+ decodeCrock,
+ decryptWithDerivedKey,
+ eddsaGetPublic,
+ encodeCrock,
+ encryptWithDerivedKey,
+ getRandomBytesF,
+ kdf,
+ stringToBytes,
+} from "@gnu-taler/taler-util";
+
+/**
+ * Restore previous session and unlock account with password
+ *
+ * @param salt string from which crypto params will be derived
+ * @param key secured private key
+ * @param password password for the private key
+ * @returns
+ */
+export async function unlockOfficerAccount(
+ account: LockedAccount,
+ password: string,
+): Promise<OfficerAccount> {
+ const rawKey = decodeCrock(account);
+ const rawPassword = stringToBytes(password);
+
+ const signingKey = (await decryptWithDerivedKey(
+ rawKey,
+ rawPassword,
+ password,
+ ).catch((e: Error) => {
+ throw new UnwrapKeyError(e.message);
+ })) as SigningKey;
+
+ const publicKey = eddsaGetPublic(signingKey);
+
+ const accountId = encodeCrock(publicKey) as OfficerId;
+
+ return { id: accountId, signingKey };
+}
+
+/**
+ * Create new account (secured private key)
+ * secured with the given password
+ *
+ * @param sessionId
+ * @param password
+ * @returns
+ */
+export async function createNewOfficerAccount(
+ password: string,
+ extraNonce: EncryptionNonce,
+): Promise<OfficerAccount & { safe: LockedAccount }> {
+ const { eddsaPriv, eddsaPub } = createEddsaKeyPair();
+
+ const key = stringToBytes(password);
+
+ const localRnd = getRandomBytesF(24);
+ const mergedRnd: EncryptionNonce = extraNonce
+ ? kdf(24, stringToBytes("aml-officer"), extraNonce, localRnd)
+ : localRnd;
+
+ const protectedPrivKey = await encryptWithDerivedKey(
+ mergedRnd,
+ key,
+ eddsaPriv,
+ password,
+ );
+
+ const signingKey = eddsaPriv as SigningKey;
+ const accountId = encodeCrock(eddsaPub) as OfficerId;
+ const safe = encodeCrock(protectedPrivKey) as LockedAccount;
+
+ return { id: accountId, signingKey, safe };
+}
+
+export class UnwrapKeyError extends Error {
+ public cause: string;
+ constructor(cause: string) {
+ super(`Recovering private key failed on: ${cause}`);
+ this.cause = cause;
+ }
+}
diff --git a/packages/taler-util/src/http-client/types.ts b/packages/taler-util/src/http-client/types.ts
new file mode 100644
index 000000000..a2f709769
--- /dev/null
+++ b/packages/taler-util/src/http-client/types.ts
@@ -0,0 +1,5392 @@
+import { deprecate } from "util";
+import { codecForAmountString } from "../amounts.js";
+import {
+ Codec,
+ buildCodecForObject,
+ buildCodecForUnion,
+ codecForAny,
+ codecForBoolean,
+ codecForConstNumber,
+ codecForConstString,
+ codecForEither,
+ codecForList,
+ codecForMap,
+ codecForNumber,
+ codecForString,
+ codecOptional,
+} from "../codec.js";
+import { PaytoString, codecForPaytoString } from "../payto.js";
+import {
+ AmountString,
+ InternationalizedString,
+ codecForInternationalizedString,
+ codecForLocation,
+} from "../taler-types.js";
+import { TalerUriString, codecForTalerUriString } from "../taleruri.js";
+import {
+ AbsoluteTime,
+ TalerProtocolDuration,
+ TalerProtocolTimestamp,
+ codecForAbsoluteTime,
+ codecForDuration,
+ codecForTimestamp,
+} from "../time.js";
+
+export type UserAndPassword = {
+ username: string;
+ password: string;
+};
+
+export type UserAndToken = {
+ username: string;
+ token: AccessToken;
+};
+
+declare const opaque_OfficerAccount: unique symbol;
+export type LockedAccount = string & { [opaque_OfficerAccount]: true };
+
+declare const opaque_OfficerId: unique symbol;
+export type OfficerId = string & { [opaque_OfficerId]: true };
+
+declare const opaque_OfficerSigningKey: unique symbol;
+export type SigningKey = Uint8Array & { [opaque_OfficerSigningKey]: true };
+
+export interface OfficerAccount {
+ id: OfficerId;
+ signingKey: SigningKey;
+}
+
+export type PaginationParams = {
+ /**
+ * row identifier as the starting point of the query
+ */
+ offset?: string;
+ /**
+ * max number of element in the result response
+ * always greater than 0
+ */
+ limit?: number;
+ /**
+ * order
+ */
+ order?: "asc" | "dec";
+};
+
+export type LongPollParams = {
+ /**
+ * milliseconds the server should wait for at least one result to be shown
+ */
+ timeoutMs?: number;
+};
+///
+/// HASH
+///
+
+// 64-byte hash code.
+type HashCode = string;
+
+type PaytoHash = string;
+
+type AmlOfficerPublicKeyP = string;
+
+// 32-byte hash code.
+type ShortHashCode = string;
+
+// 16-byte salt.
+type WireSalt = string;
+
+type SHA256HashCode = ShortHashCode;
+
+type SHA512HashCode = HashCode;
+
+// 32-byte nonce value, must only be used once.
+type CSNonce = string;
+
+// 32-byte nonce value, must only be used once.
+type RefreshMasterSeed = string;
+
+// 32-byte value representing a point on Curve25519.
+type Cs25519Point = string;
+
+// 32-byte value representing a scalar multiplier
+// for scalar operations on points on Curve25519.
+type Cs25519Scalar = string;
+
+///
+/// KEYS
+///
+
+// 16-byte access token used to authorize access.
+type ClaimToken = string;
+
+// EdDSA and ECDHE public keys always point on Curve25519
+// and represented using the standard 256 bits Ed25519 compact format,
+// converted to Crockford Base32.
+type EddsaPublicKey = string;
+
+// EdDSA and ECDHE public keys always point on Curve25519
+// and represented using the standard 256 bits Ed25519 compact format,
+// converted to Crockford Base32.
+type EddsaPrivateKey = string;
+
+// Edx25519 public keys are points on Curve25519 and represented using the
+// standard 256 bits Ed25519 compact format converted to Crockford
+// Base32.
+type Edx25519PublicKey = string;
+
+// Edx25519 private keys are always points on Curve25519
+// and represented using the standard 256 bits Ed25519 compact format,
+// converted to Crockford Base32.
+type Edx25519PrivateKey = string;
+
+// EdDSA and ECDHE public keys always point on Curve25519
+// and represented using the standard 256 bits Ed25519 compact format,
+// converted to Crockford Base32.
+type EcdhePublicKey = string;
+
+// Point on Curve25519 represented using the standard 256 bits Ed25519 compact format,
+// converted to Crockford Base32.
+type CsRPublic = string;
+
+// EdDSA and ECDHE public keys always point on Curve25519
+// and represented using the standard 256 bits Ed25519 compact format,
+// converted to Crockford Base32.
+type EcdhePrivateKey = string;
+
+type CoinPublicKey = EddsaPublicKey;
+
+// RSA public key converted to Crockford Base32.
+type RsaPublicKey = string;
+
+type Integer = number;
+
+type WireTransferIdentifierRawP = string;
+// Subset of numbers: Integers in the
+// inclusive range 0 .. (2^53 - 1).
+type SafeUint64 = number;
+
+// The string must be a data URL according to RFC 2397
+// with explicit mediatype and base64 parameters.
+//
+// data:<mediatype>;base64,<data>
+//
+// Supported mediatypes are image/jpeg and image/png.
+// Invalid strings will be rejected by the wallet.
+type ImageDataUrl = string;
+
+type WadId = string;
+
+type Timestamp = TalerProtocolTimestamp;
+
+type RelativeTime = TalerProtocolDuration;
+
+export interface LoginToken {
+ token: AccessToken;
+ expiration: Timestamp;
+}
+
+declare const __ac_token: unique symbol;
+/**
+ * Use `createAccessToken(string)` function to build one.
+ */
+export type AccessToken = string & {
+ [__ac_token]: true;
+};
+
+/**
+ * Create a rfc8959 access token.
+ * Adds secret-token: prefix if there is none.
+ *
+ * @deprecated use createRFC8959AccessToken
+ * @param token
+ * @returns
+ */
+export function createAccessToken(token: string): AccessToken {
+ return (
+ token.startsWith("secret-token:") ? token : `secret-token:${token}`
+ ) as AccessToken;
+}
+/**
+ * Create a rfc8959 access token.
+ * Adds secret-token: prefix if there is none.
+ *
+ * @param token
+ * @returns
+ */
+export function createRFC8959AccessToken(token: string): AccessToken {
+ return (
+ token.startsWith("secret-token:") ? token : `secret-token:${token}`
+ ) as AccessToken;
+}
+/**
+ * Convert string to access token.
+ *
+ * @param clientSecret
+ * @returns
+ */
+export function createClientSecretAccessToken(
+ clientSecret: string,
+): AccessToken {
+ return clientSecret as AccessToken;
+}
+
+declare const __officer_signature: unique symbol;
+export type OfficerSignature = string & {
+ [__officer_signature]: true;
+};
+
+export namespace TalerAuthentication {
+ export interface TokenRequest {
+ // Service-defined scope for the token.
+ // Typical scopes would be "readonly" or "readwrite".
+ scope: string;
+
+ // Server may impose its own upper bound
+ // on the token validity duration
+ duration?: RelativeTime;
+
+ // Is the token refreshable into a new token during its
+ // validity?
+ // Refreshable tokens effectively provide indefinite
+ // access if they are refreshed in time.
+ refreshable?: boolean;
+ }
+
+ export interface TokenSuccessResponse {
+ // Expiration determined by the server.
+ // Can be based on the token_duration
+ // from the request, but ultimately the
+ // server decides the expiration.
+ expiration: Timestamp;
+
+ // Opque access token.
+ access_token: AccessToken;
+ }
+ export interface TokenSuccessResponseMerchant {
+ // Expiration determined by the server.
+ // Can be based on the token_duration
+ // from the request, but ultimately the
+ // server decides the expiration.
+ expiration: Timestamp;
+
+ // Opque access token.
+ token: AccessToken;
+ }
+}
+
+// DD51 https://docs.taler.net/design-documents/051-fractional-digits.html
+export interface CurrencySpecification {
+ // Name of the currency.
+ name: string;
+
+ // how many digits the user may enter after the decimal_separator
+ num_fractional_input_digits: Integer;
+
+ // Number of fractional digits to render in normal font and size.
+ num_fractional_normal_digits: Integer;
+
+ // Number of fractional digits to render always, if needed by
+ // padding with zeros.
+ num_fractional_trailing_zero_digits: Integer;
+
+ // map of powers of 10 to alternative currency names / symbols, must
+ // always have an entry under "0" that defines the base name,
+ // e.g. "0 => €" or "3 => k€". For BTC, would be "0 => BTC, -3 => mBTC".
+ // Communicates the currency symbol to be used.
+ alt_unit_names: { [log10: string]: string };
+}
+
+//FIXME: implement this codec
+export const codecForAccessToken = codecForString as () => Codec<AccessToken>;
+export const codecForTokenSuccessResponse =
+ (): Codec<TalerAuthentication.TokenSuccessResponse> =>
+ buildCodecForObject<TalerAuthentication.TokenSuccessResponse>()
+ .property("access_token", codecForAccessToken())
+ .property("expiration", codecForTimestamp)
+ .build("TalerAuthentication.TokenSuccessResponse");
+
+export const codecForTokenSuccessResponseMerchant =
+ (): Codec<TalerAuthentication.TokenSuccessResponseMerchant> =>
+ buildCodecForObject<TalerAuthentication.TokenSuccessResponseMerchant>()
+ .property("token", codecForAccessToken())
+ .property("expiration", codecForTimestamp)
+ .build("TalerAuthentication.TokenSuccessResponseMerchant");
+
+export const codecForCurrencySpecificiation =
+ (): Codec<CurrencySpecification> =>
+ buildCodecForObject<CurrencySpecification>()
+ .property("name", codecForString())
+ .property("num_fractional_input_digits", codecForNumber())
+ .property("num_fractional_normal_digits", codecForNumber())
+ .property("num_fractional_trailing_zero_digits", codecForNumber())
+ .property("alt_unit_names", codecForMap(codecForString()))
+ .build("CurrencySpecification");
+
+export const codecForIntegrationBankConfig =
+ (): Codec<TalerCorebankApi.IntegrationConfig> =>
+ buildCodecForObject<TalerCorebankApi.IntegrationConfig>()
+ .property("name", codecForConstString("taler-bank-integration"))
+ .property("version", codecForString())
+ .property("currency", codecForString())
+ .property("currency_specification", codecForCurrencySpecificiation())
+ .build("TalerCorebankApi.IntegrationConfig");
+
+export const codecForCoreBankConfig = (): Codec<TalerCorebankApi.Config> =>
+ buildCodecForObject<TalerCorebankApi.Config>()
+ .property("name", codecForConstString("libeufin-bank"))
+ .property("version", codecForString())
+ .property("bank_name", codecForString())
+ .property("allow_conversion", codecForBoolean())
+ .property("allow_registrations", codecForBoolean())
+ .property("allow_deletions", codecForBoolean())
+ .property("allow_edit_name", codecForBoolean())
+ .property("allow_edit_cashout_payto_uri", codecForBoolean())
+ .property("default_debit_threshold", codecForAmountString())
+ .property("currency", codecForString())
+ .property("currency_specification", codecForCurrencySpecificiation())
+ .property(
+ "supported_tan_channels",
+ codecForList(
+ codecForEither(
+ codecForConstString(TalerCorebankApi.TanChannel.SMS),
+ codecForConstString(TalerCorebankApi.TanChannel.EMAIL),
+ ),
+ ),
+ )
+ .property("wire_type", codecForString())
+ .build("TalerCorebankApi.Config");
+
+//FIXME: implement this codec
+export const codecForURN = codecForString;
+
+export const codecForExchangeConfigInfo =
+ (): Codec<TalerMerchantApi.ExchangeConfigInfo> =>
+ buildCodecForObject<TalerMerchantApi.ExchangeConfigInfo>()
+ .property("base_url", codecForString())
+ .property("currency", codecForString())
+ .property("master_pub", codecForString())
+ .build("TalerMerchantApi.ExchangeConfigInfo");
+
+export const codecForMerchantConfig =
+ (): Codec<TalerMerchantApi.VersionResponse> =>
+ buildCodecForObject<TalerMerchantApi.VersionResponse>()
+ .property("name", codecForConstString("taler-merchant"))
+ .property("currency", codecForString())
+ .property("version", codecForString())
+ .property("currencies", codecForMap(codecForCurrencySpecificiation()))
+ .property("exchanges", codecForList(codecForExchangeConfigInfo()))
+ .build("TalerMerchantApi.VersionResponse");
+
+export const codecForClaimResponse =
+ (): Codec<TalerMerchantApi.ClaimResponse> =>
+ buildCodecForObject<TalerMerchantApi.ClaimResponse>()
+ .property("contract_terms", codecForContractTerms())
+ .property("sig", codecForString())
+ .build("TalerMerchantApi.ClaimResponse");
+
+export const codecForPaymentResponse =
+ (): Codec<TalerMerchantApi.PaymentResponse> =>
+ buildCodecForObject<TalerMerchantApi.PaymentResponse>()
+ .property("pos_confirmation", codecOptional(codecForString()))
+ .property("sig", codecForString())
+ .build("TalerMerchantApi.PaymentResponse");
+
+export const codecForStatusPaid = (): Codec<TalerMerchantApi.StatusPaid> =>
+ buildCodecForObject<TalerMerchantApi.StatusPaid>()
+ .property("refund_amount", codecForAmountString())
+ .property("refund_pending", codecForBoolean())
+ .property("refund_taken", codecForAmountString())
+ .property("refunded", codecForBoolean())
+ .property("type", codecForConstString("paid"))
+ .build("TalerMerchantApi.StatusPaid");
+
+export const codecForStatusGoto =
+ (): Codec<TalerMerchantApi.StatusGotoResponse> =>
+ buildCodecForObject<TalerMerchantApi.StatusGotoResponse>()
+ .property("public_reorder_url", codecForURL())
+ .property("type", codecForConstString("goto"))
+ .build("TalerMerchantApi.StatusGotoResponse");
+
+export const codecForStatusStatusUnpaid =
+ (): Codec<TalerMerchantApi.StatusUnpaidResponse> =>
+ buildCodecForObject<TalerMerchantApi.StatusUnpaidResponse>()
+ .property("type", codecForConstString("unpaid"))
+ .property("already_paid_order_id", codecOptional(codecForString()))
+ .property("fulfillment_url", codecOptional(codecForString()))
+ .property("taler_pay_uri", codecForTalerUriString())
+ .build("TalerMerchantApi.PaymentResponse");
+
+export const codecForPaidRefundStatusResponse =
+ (): Codec<TalerMerchantApi.PaidRefundStatusResponse> =>
+ buildCodecForObject<TalerMerchantApi.PaidRefundStatusResponse>()
+ .property("pos_confirmation", codecOptional(codecForString()))
+ .property("refunded", codecForBoolean())
+ .build("TalerMerchantApi.PaidRefundStatusResponse");
+
+export const codecForMerchantAbortPayRefundSuccessStatus =
+ (): Codec<TalerMerchantApi.MerchantAbortPayRefundSuccessStatus> =>
+ buildCodecForObject<TalerMerchantApi.MerchantAbortPayRefundSuccessStatus>()
+ .property("exchange_pub", codecForString())
+ .property("exchange_sig", codecForString())
+ .property("exchange_status", codecForConstNumber(200))
+ .property("type", codecForConstString("success"))
+ .build("TalerMerchantApi.MerchantAbortPayRefundSuccessStatus");
+
+export const codecForMerchantAbortPayRefundFailureStatus =
+ (): Codec<TalerMerchantApi.MerchantAbortPayRefundFailureStatus> =>
+ buildCodecForObject<TalerMerchantApi.MerchantAbortPayRefundFailureStatus>()
+ .property("exchange_code", codecForNumber())
+ .property("exchange_reply", codecForAny())
+ .property("exchange_status", codecForNumber())
+ .property("type", codecForConstString("failure"))
+ .build("TalerMerchantApi.MerchantAbortPayRefundFailureStatus");
+
+export const codecForMerchantAbortPayRefundStatus =
+ (): Codec<TalerMerchantApi.MerchantAbortPayRefundStatus> =>
+ buildCodecForUnion<TalerMerchantApi.MerchantAbortPayRefundStatus>()
+ .discriminateOn("type")
+ .alternative("success", codecForMerchantAbortPayRefundSuccessStatus())
+ .alternative("failure", codecForMerchantAbortPayRefundFailureStatus())
+ .build("TalerMerchantApi.MerchantAbortPayRefundStatus");
+
+export const codecForAbortResponse =
+ (): Codec<TalerMerchantApi.AbortResponse> =>
+ buildCodecForObject<TalerMerchantApi.AbortResponse>()
+ .property("refunds", codecForList(codecForMerchantAbortPayRefundStatus()))
+ .build("TalerMerchantApi.AbortResponse");
+
+export const codecForWalletRefundResponse =
+ (): Codec<TalerMerchantApi.WalletRefundResponse> =>
+ buildCodecForObject<TalerMerchantApi.WalletRefundResponse>()
+ .property("merchant_pub", codecForString())
+ .property("refund_amount", codecForAmountString())
+ .property("refunds", codecForList(codecForMerchantCoinRefundStatus()))
+ .build("TalerMerchantApi.AbortResponse");
+
+export const codecForMerchantCoinRefundSuccessStatus =
+ (): Codec<TalerMerchantApi.MerchantCoinRefundSuccessStatus> =>
+ buildCodecForObject<TalerMerchantApi.MerchantCoinRefundSuccessStatus>()
+ .property("type", codecForConstString("success"))
+ .property("coin_pub", codecForString())
+ .property("exchange_status", codecForConstNumber(200))
+ .property("exchange_sig", codecForString())
+ .property("rtransaction_id", codecForNumber())
+ .property("refund_amount", codecForAmountString())
+ .property("exchange_pub", codecForString())
+ .property("execution_time", codecForTimestamp)
+ .build("TalerMerchantApi.MerchantCoinRefundSuccessStatus");
+
+export const codecForMerchantCoinRefundFailureStatus =
+ (): Codec<TalerMerchantApi.MerchantCoinRefundFailureStatus> =>
+ buildCodecForObject<TalerMerchantApi.MerchantCoinRefundFailureStatus>()
+ .property("type", codecForConstString("failure"))
+ .property("coin_pub", codecForString())
+ .property("exchange_status", codecForNumber())
+ .property("rtransaction_id", codecForNumber())
+ .property("refund_amount", codecForAmountString())
+ .property("exchange_code", codecOptional(codecForNumber()))
+ .property("exchange_reply", codecOptional(codecForAny()))
+ .property("execution_time", codecForTimestamp)
+ .build("TalerMerchantApi.MerchantCoinRefundFailureStatus");
+
+export const codecForMerchantCoinRefundStatus =
+ (): Codec<TalerMerchantApi.MerchantCoinRefundStatus> =>
+ buildCodecForUnion<TalerMerchantApi.MerchantCoinRefundStatus>()
+ .discriminateOn("type")
+ .alternative("success", codecForMerchantCoinRefundSuccessStatus())
+ .alternative("failure", codecForMerchantCoinRefundFailureStatus())
+ .build("TalerMerchantApi.MerchantCoinRefundStatus");
+
+export const codecForQueryInstancesResponse =
+ (): Codec<TalerMerchantApi.QueryInstancesResponse> =>
+ buildCodecForObject<TalerMerchantApi.QueryInstancesResponse>()
+ .property("name", codecForString())
+ .property("user_type", codecForString())
+ .property("email", codecOptional(codecForString()))
+ .property("website", codecOptional(codecForString()))
+ .property("logo", codecOptional(codecForString()))
+ .property("merchant_pub", codecForString())
+ .property("address", codecForLocation())
+ .property("jurisdiction", codecForLocation())
+ .property("use_stefan", codecForBoolean())
+ .property("default_wire_transfer_delay", codecForDuration)
+ .property("default_pay_delay", codecForDuration)
+ .property(
+ "auth",
+ buildCodecForObject<{
+ method: "external" | "token";
+ }>()
+ .property(
+ "method",
+ codecForEither(
+ codecForConstString("token"),
+ codecForConstString("external"),
+ ),
+ )
+ .build("TalerMerchantApi.QueryInstancesResponse.auth"),
+ )
+ .build("TalerMerchantApi.QueryInstancesResponse");
+
+export const codecForAccountKycRedirects =
+ (): Codec<TalerMerchantApi.AccountKycRedirects> =>
+ buildCodecForObject<TalerMerchantApi.AccountKycRedirects>()
+ .property(
+ "pending_kycs",
+ codecForList(codecForMerchantAccountKycRedirect()),
+ )
+ .property("timeout_kycs", codecForList(codecForExchangeKycTimeout()))
+
+ .build("TalerMerchantApi.AccountKycRedirects");
+
+export const codecForMerchantAccountKycRedirect =
+ (): Codec<TalerMerchantApi.MerchantAccountKycRedirect> =>
+ buildCodecForObject<TalerMerchantApi.MerchantAccountKycRedirect>()
+ .property("kyc_url", codecForURL())
+ .property("aml_status", codecForNumber())
+ .property("exchange_url", codecForURL())
+ .property("payto_uri", codecForPaytoString())
+ .build("TalerMerchantApi.MerchantAccountKycRedirect");
+
+export const codecForExchangeKycTimeout =
+ (): Codec<TalerMerchantApi.ExchangeKycTimeout> =>
+ buildCodecForObject<TalerMerchantApi.ExchangeKycTimeout>()
+ .property("exchange_url", codecForURL())
+ .property("exchange_code", codecForNumber())
+ .property("exchange_http_status", codecForNumber())
+ .build("TalerMerchantApi.ExchangeKycTimeout");
+
+export const codecForAccountAddResponse =
+ (): Codec<TalerMerchantApi.AccountAddResponse> =>
+ buildCodecForObject<TalerMerchantApi.AccountAddResponse>()
+ .property("h_wire", codecForString())
+ .property("salt", codecForString())
+ .build("TalerMerchantApi.AccountAddResponse");
+
+export const codecForAccountsSummaryResponse =
+ (): Codec<TalerMerchantApi.AccountsSummaryResponse> =>
+ buildCodecForObject<TalerMerchantApi.AccountsSummaryResponse>()
+ .property("accounts", codecForList(codecForBankAccountSummaryEntry()))
+ .build("TalerMerchantApi.AccountsSummaryResponse");
+
+export const codecForBankAccountSummaryEntry =
+ (): Codec<TalerMerchantApi.BankAccountSummaryEntry> =>
+ buildCodecForObject<TalerMerchantApi.BankAccountSummaryEntry>()
+ .property("payto_uri", codecForPaytoString())
+ .property("h_wire", codecForString())
+ .build("TalerMerchantApi.BankAccountSummaryEntry");
+
+export const codecForBankAccountEntry =
+ (): Codec<TalerMerchantApi.BankAccountEntry> =>
+ buildCodecForObject<TalerMerchantApi.BankAccountEntry>()
+ .property("payto_uri", codecForPaytoString())
+ .property("h_wire", codecForString())
+ .property("salt", codecForString())
+ .property("credit_facade_url", codecOptional(codecForURL()))
+ .property("active", codecOptional(codecForBoolean()))
+ .build("TalerMerchantApi.BankAccountEntry");
+
+export const codecForInventorySummaryResponse =
+ (): Codec<TalerMerchantApi.InventorySummaryResponse> =>
+ buildCodecForObject<TalerMerchantApi.InventorySummaryResponse>()
+ .property("products", codecForList(codecForInventoryEntry()))
+ .build("TalerMerchantApi.InventorySummaryResponse");
+
+export const codecForInventoryEntry =
+ (): Codec<TalerMerchantApi.InventoryEntry> =>
+ buildCodecForObject<TalerMerchantApi.InventoryEntry>()
+ .property("product_id", codecForString())
+ .property("product_serial", codecForNumber())
+ .build("TalerMerchantApi.InventoryEntry");
+
+export const codecForProductDetail =
+ (): Codec<TalerMerchantApi.ProductDetail> =>
+ buildCodecForObject<TalerMerchantApi.ProductDetail>()
+ .property("description", codecForString())
+ .property("description_i18n", codecForInternationalizedString())
+ .property("unit", codecForString())
+ .property("price", codecForAmountString())
+ .property("image", codecForString())
+ .property("taxes", codecForList(codecForTax()))
+ .property("address", codecForLocation())
+ .property("next_restock", codecForTimestamp)
+ .property("total_stock", codecForNumber())
+ .property("total_sold", codecForNumber())
+ .property("total_lost", codecForNumber())
+ .property("minimum_age", codecOptional(codecForNumber()))
+ .build("TalerMerchantApi.ProductDetail");
+
+export const codecForTax = (): Codec<TalerMerchantApi.Tax> =>
+ buildCodecForObject<TalerMerchantApi.Tax>()
+ .property("name", codecForString())
+ .property("tax", codecForAmountString())
+ .build("TalerMerchantApi.Tax");
+
+export const codecForPostOrderResponse =
+ (): Codec<TalerMerchantApi.PostOrderResponse> =>
+ buildCodecForObject<TalerMerchantApi.PostOrderResponse>()
+ .property("order_id", codecForString())
+ .property("token", codecOptional(codecForString()))
+ .build("TalerMerchantApi.PostOrderResponse");
+
+export const codecForOutOfStockResponse =
+ (): Codec<TalerMerchantApi.OutOfStockResponse> =>
+ buildCodecForObject<TalerMerchantApi.OutOfStockResponse>()
+ .property("product_id", codecForString())
+ .property("available_quantity", codecForNumber())
+ .property("requested_quantity", codecForNumber())
+ .property("restock_expected", codecForTimestamp)
+ .build("TalerMerchantApi.OutOfStockResponse");
+
+export const codecForOrderHistory = (): Codec<TalerMerchantApi.OrderHistory> =>
+ buildCodecForObject<TalerMerchantApi.OrderHistory>()
+ .property("orders", codecForList(codecForOrderHistoryEntry()))
+ .build("TalerMerchantApi.OrderHistory");
+
+export const codecForOrderHistoryEntry =
+ (): Codec<TalerMerchantApi.OrderHistoryEntry> =>
+ buildCodecForObject<TalerMerchantApi.OrderHistoryEntry>()
+ .property("order_id", codecForString())
+ .property("row_id", codecForNumber())
+ .property("timestamp", codecForTimestamp)
+ .property("amount", codecForAmountString())
+ .property("summary", codecForString())
+ .property("refundable", codecForBoolean())
+ .property("paid", codecForBoolean())
+ .build("TalerMerchantApi.OrderHistoryEntry");
+
+export const codecForMerchant = (): Codec<TalerMerchantApi.Merchant> =>
+ buildCodecForObject<TalerMerchantApi.Merchant>()
+ .property("name", codecForString())
+ .property("email", codecOptional(codecForString()))
+ .property("logo", codecOptional(codecForString()))
+ .property("website", codecOptional(codecForString()))
+ .property("address", codecOptional(codecForLocation()))
+ .property("jurisdiction", codecOptional(codecForLocation()))
+ .build("TalerMerchantApi.MerchantInfo");
+
+export const codecForExchange = (): Codec<TalerMerchantApi.Exchange> =>
+ buildCodecForObject<TalerMerchantApi.Exchange>()
+ .property("master_pub", codecForString())
+ .property("priority", codecForNumber())
+ .property("url", codecForString())
+ .build("TalerMerchantApi.Exchange");
+
+export const codecForContractTerms =
+ (): Codec<TalerMerchantApi.ContractTerms> =>
+ buildCodecForObject<TalerMerchantApi.ContractTerms>()
+ .property("order_id", codecForString())
+ .property("fulfillment_url", codecOptional(codecForString()))
+ .property("fulfillment_message", codecOptional(codecForString()))
+ .property(
+ "fulfillment_message_i18n",
+ codecOptional(codecForInternationalizedString()),
+ )
+ .property("merchant_base_url", codecForString())
+ .property("h_wire", codecForString())
+ .property("auto_refund", codecOptional(codecForDuration))
+ .property("wire_method", codecForString())
+ .property("summary", codecForString())
+ .property(
+ "summary_i18n",
+ codecOptional(codecForInternationalizedString()),
+ )
+ .property("nonce", codecForString())
+ .property("amount", codecForAmountString())
+ .property("pay_deadline", codecForTimestamp)
+ .property("refund_deadline", codecForTimestamp)
+ .property("wire_transfer_deadline", codecForTimestamp)
+ .property("timestamp", codecForTimestamp)
+ .property("delivery_location", codecOptional(codecForLocation()))
+ .property("delivery_date", codecOptional(codecForTimestamp))
+ .property("max_fee", codecForAmountString())
+ .property("merchant", codecForMerchant())
+ .property("merchant_pub", codecForString())
+ .property("exchanges", codecForList(codecForExchange()))
+ .property("products", codecForList(codecForProduct()))
+ .property("extra", codecForAny())
+ .build("TalerMerchantApi.ContractTerms");
+
+export const codecForProduct = (): Codec<TalerMerchantApi.Product> =>
+ buildCodecForObject<TalerMerchantApi.Product>()
+ .property("product_id", codecOptional(codecForString()))
+ .property("description", codecForString())
+ .property(
+ "description_i18n",
+ codecOptional(codecForInternationalizedString()),
+ )
+ .property("quantity", codecOptional(codecForNumber()))
+ .property("unit", codecOptional(codecForString()))
+ .property("price", codecOptional(codecForAmountString()))
+ .property("image", codecOptional(codecForString()))
+ .property("taxes", codecOptional(codecForList(codecForTax())))
+ .property("delivery_date", codecOptional(codecForTimestamp))
+ .build("TalerMerchantApi.Product");
+
+export const codecForCheckPaymentPaidResponse =
+ (): Codec<TalerMerchantApi.CheckPaymentPaidResponse> =>
+ buildCodecForObject<TalerMerchantApi.CheckPaymentPaidResponse>()
+ .property("order_status", codecForConstString("paid"))
+ .property("refunded", codecForBoolean())
+ .property("refund_pending", codecForBoolean())
+ .property("wired", codecForBoolean())
+ .property("deposit_total", codecForAmountString())
+ .property("exchange_code", codecForNumber())
+ .property("exchange_http_status", codecForNumber())
+ .property("refund_amount", codecForAmountString())
+ .property("contract_terms", codecForContractTerms())
+ .property("wire_reports", codecForList(codecForTransactionWireReport()))
+ .property("wire_details", codecForList(codecForTransactionWireTransfer()))
+ .property("refund_details", codecForList(codecForRefundDetails()))
+ .property("order_status_url", codecForURL())
+ .build("TalerMerchantApi.CheckPaymentPaidResponse");
+
+export const codecForCheckPaymentUnpaidResponse =
+ (): Codec<TalerMerchantApi.CheckPaymentUnpaidResponse> =>
+ buildCodecForObject<TalerMerchantApi.CheckPaymentUnpaidResponse>()
+ .property("order_status", codecForConstString("unpaid"))
+ .property("taler_pay_uri", codecForTalerUriString())
+ .property("creation_time", codecForTimestamp)
+ .property("summary", codecForString())
+ .property("total_amount", codecForAmountString())
+ .property("already_paid_order_id", codecOptional(codecForString()))
+ .property("already_paid_fulfillment_url", codecOptional(codecForString()))
+ .property("order_status_url", codecForString())
+ .build("TalerMerchantApi.CheckPaymentPaidResponse");
+
+export const codecForCheckPaymentClaimedResponse =
+ (): Codec<TalerMerchantApi.CheckPaymentClaimedResponse> =>
+ buildCodecForObject<TalerMerchantApi.CheckPaymentClaimedResponse>()
+ .property("order_status", codecForConstString("claimed"))
+ .property("contract_terms", codecForContractTerms())
+ .build("TalerMerchantApi.CheckPaymentClaimedResponse");
+
+export const codecForMerchantOrderPrivateStatusResponse =
+ (): Codec<TalerMerchantApi.MerchantOrderStatusResponse> =>
+ buildCodecForUnion<TalerMerchantApi.MerchantOrderStatusResponse>()
+ .discriminateOn("order_status")
+ .alternative("paid", codecForCheckPaymentPaidResponse())
+ .alternative("unpaid", codecForCheckPaymentUnpaidResponse())
+ .alternative("claimed", codecForCheckPaymentClaimedResponse())
+ .build("TalerMerchantApi.MerchantOrderStatusResponse");
+
+export const codecForRefundDetails =
+ (): Codec<TalerMerchantApi.RefundDetails> =>
+ buildCodecForObject<TalerMerchantApi.RefundDetails>()
+ .property("reason", codecForString())
+ .property("pending", codecForBoolean())
+ .property("timestamp", codecForTimestamp)
+ .property("amount", codecForAmountString())
+ .build("TalerMerchantApi.RefundDetails");
+
+export const codecForTransactionWireTransfer =
+ (): Codec<TalerMerchantApi.TransactionWireTransfer> =>
+ buildCodecForObject<TalerMerchantApi.TransactionWireTransfer>()
+ .property("exchange_url", codecForURL())
+ .property("wtid", codecForString())
+ .property("execution_time", codecForTimestamp)
+ .property("amount", codecForAmountString())
+ .property("confirmed", codecForBoolean())
+ .build("TalerMerchantApi.TransactionWireTransfer");
+
+export const codecForTransactionWireReport =
+ (): Codec<TalerMerchantApi.TransactionWireReport> =>
+ buildCodecForObject<TalerMerchantApi.TransactionWireReport>()
+ .property("code", codecForNumber())
+ .property("hint", codecForString())
+ .property("exchange_code", codecForNumber())
+ .property("exchange_http_status", codecForNumber())
+ .property("coin_pub", codecForString())
+ .build("TalerMerchantApi.TransactionWireReport");
+
+export const codecForMerchantRefundResponse =
+ (): Codec<TalerMerchantApi.MerchantRefundResponse> =>
+ buildCodecForObject<TalerMerchantApi.MerchantRefundResponse>()
+ .property("taler_refund_uri", codecForTalerUriString())
+ .property("h_contract", codecForString())
+ .build("TalerMerchantApi.MerchantRefundResponse");
+
+export const codecForTansferList = (): Codec<TalerMerchantApi.TransferList> =>
+ buildCodecForObject<TalerMerchantApi.TransferList>()
+ .property("transfers", codecForList(codecForTransferDetails()))
+ .build("TalerMerchantApi.TransferList");
+
+export const codecForTransferDetails =
+ (): Codec<TalerMerchantApi.TransferDetails> =>
+ buildCodecForObject<TalerMerchantApi.TransferDetails>()
+ .property("credit_amount", codecForAmountString())
+ .property("wtid", codecForString())
+ .property("payto_uri", codecForPaytoString())
+ .property("exchange_url", codecForURL())
+ .property("transfer_serial_id", codecForNumber())
+ .property("execution_time", codecOptional(codecForTimestamp))
+ .property("verified", codecOptional(codecForBoolean()))
+ .property("confirmed", codecOptional(codecForBoolean()))
+ .build("TalerMerchantApi.TransferDetails");
+
+export const codecForOtpDeviceSummaryResponse =
+ (): Codec<TalerMerchantApi.OtpDeviceSummaryResponse> =>
+ buildCodecForObject<TalerMerchantApi.OtpDeviceSummaryResponse>()
+ .property("otp_devices", codecForList(codecForOtpDeviceEntry()))
+ .build("TalerMerchantApi.OtpDeviceSummaryResponse");
+
+export const codecForOtpDeviceEntry =
+ (): Codec<TalerMerchantApi.OtpDeviceEntry> =>
+ buildCodecForObject<TalerMerchantApi.OtpDeviceEntry>()
+ .property("otp_device_id", codecForString())
+ .property("device_description", codecForString())
+ .build("TalerMerchantApi.OtpDeviceEntry");
+
+export const codecForOtpDeviceDetails =
+ (): Codec<TalerMerchantApi.OtpDeviceDetails> =>
+ buildCodecForObject<TalerMerchantApi.OtpDeviceDetails>()
+ .property("device_description", codecForString())
+ .property("otp_algorithm", codecForNumber())
+ .property("otp_ctr", codecOptional(codecForNumber()))
+ .property("otp_timestamp", codecForNumber())
+ .property("otp_code", codecOptional(codecForString()))
+ .build("TalerMerchantApi.OtpDeviceDetails");
+
+export const codecForTemplateSummaryResponse =
+ (): Codec<TalerMerchantApi.TemplateSummaryResponse> =>
+ buildCodecForObject<TalerMerchantApi.TemplateSummaryResponse>()
+ .property("templates", codecForList(codecForTemplateEntry()))
+ .build("TalerMerchantApi.TemplateSummaryResponse");
+
+export const codecForTemplateEntry =
+ (): Codec<TalerMerchantApi.TemplateEntry> =>
+ buildCodecForObject<TalerMerchantApi.TemplateEntry>()
+ .property("template_id", codecForString())
+ .property("template_description", codecForString())
+ .build("TalerMerchantApi.TemplateEntry");
+
+export const codecForTemplateDetails =
+ (): Codec<TalerMerchantApi.TemplateDetails> =>
+ buildCodecForObject<TalerMerchantApi.TemplateDetails>()
+ .property("template_description", codecForString())
+ .property("otp_id", codecOptional(codecForString()))
+ .property("template_contract", codecForTemplateContractDetails())
+ .property("required_currency", codecOptional(codecForString()))
+ .property(
+ "editable_defaults",
+ codecOptional(codecForTemplateContractDetailsDefaults()),
+ )
+ .build("TalerMerchantApi.TemplateDetails");
+
+export const codecForTemplateContractDetails =
+ (): Codec<TalerMerchantApi.TemplateContractDetails> =>
+ buildCodecForObject<TalerMerchantApi.TemplateContractDetails>()
+ .property("summary", codecOptional(codecForString()))
+ .property("currency", codecOptional(codecForString()))
+ .property("amount", codecOptional(codecForAmountString()))
+ .property("minimum_age", codecForNumber())
+ .property("pay_duration", codecForDuration)
+ .build("TalerMerchantApi.TemplateContractDetails");
+
+export const codecForTemplateContractDetailsDefaults =
+ (): Codec<TalerMerchantApi.TemplateContractDetailsDefaults> =>
+ buildCodecForObject<TalerMerchantApi.TemplateContractDetailsDefaults>()
+ .property("summary", codecOptional(codecForString()))
+ .property("currency", codecOptional(codecForString()))
+ .property("amount", codecOptional(codecForAmountString()))
+ .property("minimum_age", codecOptional(codecForNumber()))
+ .property("pay_duration", codecOptional(codecForDuration))
+ .build("TalerMerchantApi.TemplateContractDetailsDefaults");
+
+export const codecForWalletTemplateDetails =
+ (): Codec<TalerMerchantApi.WalletTemplateDetails> =>
+ buildCodecForObject<TalerMerchantApi.WalletTemplateDetails>()
+ .property("template_contract", codecForTemplateContractDetails())
+ .property("required_currency", codecOptional(codecForString()))
+ .property(
+ "editable_defaults",
+ codecOptional(codecForTemplateContractDetailsDefaults()),
+ )
+ .build("TalerMerchantApi.WalletTemplateDetails");
+
+export const codecForWebhookSummaryResponse =
+ (): Codec<TalerMerchantApi.WebhookSummaryResponse> =>
+ buildCodecForObject<TalerMerchantApi.WebhookSummaryResponse>()
+ .property("webhooks", codecForList(codecForWebhookEntry()))
+ .build("TalerMerchantApi.WebhookSummaryResponse");
+
+export const codecForWebhookEntry = (): Codec<TalerMerchantApi.WebhookEntry> =>
+ buildCodecForObject<TalerMerchantApi.WebhookEntry>()
+ .property("webhook_id", codecForString())
+ .property("event_type", codecForString())
+ .build("TalerMerchantApi.WebhookEntry");
+
+export const codecForWebhookDetails =
+ (): Codec<TalerMerchantApi.WebhookDetails> =>
+ buildCodecForObject<TalerMerchantApi.WebhookDetails>()
+ .property("event_type", codecForString())
+ .property("url", codecForString())
+ .property("http_method", codecForString())
+ .property("header_template", codecOptional(codecForString()))
+ .property("body_template", codecOptional(codecForString()))
+ .build("TalerMerchantApi.WebhookDetails");
+
+export const codecForTokenFamilyKind =
+ (): Codec<TalerMerchantApi.TokenFamilyKind> =>
+ codecForEither(
+ codecForConstString("discount"),
+ codecForConstString("subscription"),
+ ) as any; //FIXME: create a codecForEnum
+export const codecForTokenFamilyDetails =
+ (): Codec<TalerMerchantApi.TokenFamilyDetails> =>
+ buildCodecForObject<TalerMerchantApi.TokenFamilyDetails>()
+ .property("slug", codecForString())
+ .property("name", codecForString())
+ .property("description", codecForString())
+ .property("description_i18n", codecForInternationalizedString())
+ .property("valid_after", codecForTimestamp)
+ .property("valid_before", codecForTimestamp)
+ .property("duration", codecForDuration)
+ .property("kind", codecForTokenFamilyKind())
+ .property("issued", codecForNumber())
+ .property("redeemed", codecForNumber())
+ .build("TalerMerchantApi.TokenFamilyDetails");
+
+export const codecForTokenFamiliesList =
+ (): Codec<TalerMerchantApi.TokenFamiliesList> =>
+ buildCodecForObject<TalerMerchantApi.TokenFamiliesList>()
+ .property("token_families", codecForList(codecForTokenFamilySummary()))
+ .build("TalerMerchantApi.TokenFamiliesList");
+
+export const codecForTokenFamilySummary =
+ (): Codec<TalerMerchantApi.TokenFamilySummary> =>
+ buildCodecForObject<TalerMerchantApi.TokenFamilySummary>()
+ .property("slug", codecForString())
+ .property("name", codecForString())
+ .property("valid_after", codecForTimestamp)
+ .property("valid_before", codecForTimestamp)
+ .property("kind", codecForTokenFamilyKind())
+ .build("TalerMerchantApi.TokenFamilySummary");
+
+export const codecForInstancesResponse =
+ (): Codec<TalerMerchantApi.InstancesResponse> =>
+ buildCodecForObject<TalerMerchantApi.InstancesResponse>()
+ .property("instances", codecForList(codecForInstance()))
+ .build("TalerMerchantApi.InstancesResponse");
+
+export const codecForInstance = (): Codec<TalerMerchantApi.Instance> =>
+ buildCodecForObject<TalerMerchantApi.Instance>()
+ .property("name", codecForString())
+ .property("user_type", codecForString())
+ .property("website", codecOptional(codecForString()))
+ .property("logo", codecOptional(codecForString()))
+ .property("id", codecForString())
+ .property("merchant_pub", codecForString())
+ .property("payment_targets", codecForList(codecForString()))
+ .property("deleted", codecForBoolean())
+ .build("TalerMerchantApi.Instance");
+
+export const codecForExchangeConfig =
+ (): Codec<TalerExchangeApi.ExchangeVersionResponse> =>
+ buildCodecForObject<TalerExchangeApi.ExchangeVersionResponse>()
+ .property("version", codecForString())
+ .property("name", codecForConstString("taler-exchange"))
+ .property("implementation", codecOptional(codecForURN()))
+ .property("currency", codecForString())
+ .property("currency_specification", codecForCurrencySpecificiation())
+ .property("supported_kyc_requirements", codecForList(codecForString()))
+ .build("TalerExchangeApi.ExchangeVersionResponse");
+
+export const codecForExchangeKeys =
+ (): Codec<TalerExchangeApi.ExchangeKeysResponse> =>
+ buildCodecForObject<TalerExchangeApi.ExchangeKeysResponse>()
+ .property("version", codecForString())
+ .property("base_url", codecForString())
+ .property("currency", codecForString())
+ .build("TalerExchangeApi.ExchangeKeysResponse");
+
+const codecForBalance = (): Codec<TalerCorebankApi.Balance> =>
+ buildCodecForObject<TalerCorebankApi.Balance>()
+ .property("amount", codecForAmountString())
+ .property(
+ "credit_debit_indicator",
+ codecForEither(
+ codecForConstString("credit"),
+ codecForConstString("debit"),
+ ),
+ )
+ .build("TalerCorebankApi.Balance");
+
+const codecForPublicAccount = (): Codec<TalerCorebankApi.PublicAccount> =>
+ buildCodecForObject<TalerCorebankApi.PublicAccount>()
+ .property("username", codecForString())
+ .property("balance", codecForBalance())
+ .property("payto_uri", codecForPaytoString())
+ .property("is_taler_exchange", codecForBoolean())
+ .property("row_id", codecOptional(codecForNumber()))
+ .build("TalerCorebankApi.PublicAccount");
+
+export const codecForPublicAccountsResponse =
+ (): Codec<TalerCorebankApi.PublicAccountsResponse> =>
+ buildCodecForObject<TalerCorebankApi.PublicAccountsResponse>()
+ .property("public_accounts", codecForList(codecForPublicAccount()))
+ .build("TalerCorebankApi.PublicAccountsResponse");
+
+export const codecForAccountMinimalData =
+ (): Codec<TalerCorebankApi.AccountMinimalData> =>
+ buildCodecForObject<TalerCorebankApi.AccountMinimalData>()
+ .property("username", codecForString())
+ .property("name", codecForString())
+ .property("payto_uri", codecForPaytoString())
+ .property("balance", codecForBalance())
+ .property("debit_threshold", codecForAmountString())
+ .property("is_public", codecForBoolean())
+ .property("is_taler_exchange", codecForBoolean())
+ .property("row_id", codecOptional(codecForNumber()))
+ .build("TalerCorebankApi.AccountMinimalData");
+
+export const codecForListBankAccountsResponse =
+ (): Codec<TalerCorebankApi.ListBankAccountsResponse> =>
+ buildCodecForObject<TalerCorebankApi.ListBankAccountsResponse>()
+ .property("accounts", codecForList(codecForAccountMinimalData()))
+ .build("TalerCorebankApi.ListBankAccountsResponse");
+
+export const codecForAccountData = (): Codec<TalerCorebankApi.AccountData> =>
+ buildCodecForObject<TalerCorebankApi.AccountData>()
+ .property("name", codecForString())
+ .property("balance", codecForBalance())
+ .property("payto_uri", codecForPaytoString())
+ .property("debit_threshold", codecForAmountString())
+ .property("contact_data", codecOptional(codecForChallengeContactData()))
+ .property("cashout_payto_uri", codecOptional(codecForPaytoString()))
+ .property("is_public", codecForBoolean())
+ .property("is_taler_exchange", codecForBoolean())
+ .property(
+ "tan_channel",
+ codecOptional(
+ codecForEither(
+ codecForConstString(TalerCorebankApi.TanChannel.SMS),
+ codecForConstString(TalerCorebankApi.TanChannel.EMAIL),
+ ),
+ ),
+ )
+ .build("TalerCorebankApi.AccountData");
+
+export const codecForChallengeContactData =
+ (): Codec<TalerCorebankApi.ChallengeContactData> =>
+ buildCodecForObject<TalerCorebankApi.ChallengeContactData>()
+ .property("email", codecOptional(codecForString()))
+ .property("phone", codecOptional(codecForString()))
+ .build("TalerCorebankApi.ChallengeContactData");
+
+export const codecForWithdrawalPublicInfo =
+ (): Codec<TalerCorebankApi.WithdrawalPublicInfo> =>
+ buildCodecForObject<TalerCorebankApi.WithdrawalPublicInfo>()
+ .property(
+ "status",
+ codecForEither(
+ codecForConstString("pending"),
+ codecForConstString("selected"),
+ codecForConstString("aborted"),
+ codecForConstString("confirmed"),
+ ),
+ )
+ .property("amount", codecForAmountString())
+ .property("username", codecForString())
+ .property("selected_reserve_pub", codecOptional(codecForString()))
+ .property(
+ "selected_exchange_account",
+ codecOptional(codecForPaytoString()),
+ )
+ .build("TalerCorebankApi.WithdrawalPublicInfo");
+
+export const codecForBankAccountTransactionsResponse =
+ (): Codec<TalerCorebankApi.BankAccountTransactionsResponse> =>
+ buildCodecForObject<TalerCorebankApi.BankAccountTransactionsResponse>()
+ .property(
+ "transactions",
+ codecForList(codecForBankAccountTransactionInfo()),
+ )
+ .build("TalerCorebankApi.BankAccountTransactionsResponse");
+
+export const codecForBankAccountTransactionInfo =
+ (): Codec<TalerCorebankApi.BankAccountTransactionInfo> =>
+ buildCodecForObject<TalerCorebankApi.BankAccountTransactionInfo>()
+ .property("creditor_payto_uri", codecForPaytoString())
+ .property("debtor_payto_uri", codecForPaytoString())
+ .property("amount", codecForAmountString())
+ .property(
+ "direction",
+ codecForEither(
+ codecForConstString("debit"),
+ codecForConstString("credit"),
+ ),
+ )
+ .property("subject", codecForString())
+ .property("row_id", codecForNumber())
+ .property("date", codecForTimestamp)
+ .build("TalerCorebankApi.BankAccountTransactionInfo");
+
+export const codecForCreateTransactionResponse =
+ (): Codec<TalerCorebankApi.CreateTransactionResponse> =>
+ buildCodecForObject<TalerCorebankApi.CreateTransactionResponse>()
+ .property("row_id", codecForNumber())
+ .build("TalerCorebankApi.CreateTransactionResponse");
+
+export const codecForRegisterAccountResponse =
+ (): Codec<TalerCorebankApi.RegisterAccountResponse> =>
+ buildCodecForObject<TalerCorebankApi.RegisterAccountResponse>()
+ .property("internal_payto_uri", codecForPaytoString())
+ .build("TalerCorebankApi.RegisterAccountResponse");
+
+export const codecForBankAccountCreateWithdrawalResponse =
+ (): Codec<TalerCorebankApi.BankAccountCreateWithdrawalResponse> =>
+ buildCodecForObject<TalerCorebankApi.BankAccountCreateWithdrawalResponse>()
+ .property("taler_withdraw_uri", codecForTalerUriString())
+ .property("withdrawal_id", codecForString())
+ .build("TalerCorebankApi.BankAccountCreateWithdrawalResponse");
+
+export const codecForCashoutPending =
+ (): Codec<TalerCorebankApi.CashoutResponse> =>
+ buildCodecForObject<TalerCorebankApi.CashoutResponse>()
+ .property("cashout_id", codecForNumber())
+ .build("TalerCorebankApi.CashoutPending");
+
+export const codecForCashoutConversionResponse =
+ (): Codec<TalerBankConversionApi.CashoutConversionResponse> =>
+ buildCodecForObject<TalerBankConversionApi.CashoutConversionResponse>()
+ .property("amount_credit", codecForAmountString())
+ .property("amount_debit", codecForAmountString())
+ .build("TalerCorebankApi.CashoutConversionResponse");
+
+export const codecForCashinConversionResponse =
+ (): Codec<TalerBankConversionApi.CashinConversionResponse> =>
+ buildCodecForObject<TalerBankConversionApi.CashinConversionResponse>()
+ .property("amount_credit", codecForAmountString())
+ .property("amount_debit", codecForAmountString())
+ .build("TalerCorebankApi.CashinConversionResponse");
+
+export const codecForCashouts = (): Codec<TalerCorebankApi.Cashouts> =>
+ buildCodecForObject<TalerCorebankApi.Cashouts>()
+ .property("cashouts", codecForList(codecForCashoutInfo()))
+ .build("TalerCorebankApi.Cashouts");
+
+export const codecForCashoutInfo = (): Codec<TalerCorebankApi.CashoutInfo> =>
+ buildCodecForObject<TalerCorebankApi.CashoutInfo>()
+ .property("cashout_id", codecForNumber())
+ .build("TalerCorebankApi.CashoutInfo");
+
+export const codecForGlobalCashouts =
+ (): Codec<TalerCorebankApi.GlobalCashouts> =>
+ buildCodecForObject<TalerCorebankApi.GlobalCashouts>()
+ .property("cashouts", codecForList(codecForGlobalCashoutInfo()))
+ .build("TalerCorebankApi.GlobalCashouts");
+
+export const codecForGlobalCashoutInfo =
+ (): Codec<TalerCorebankApi.GlobalCashoutInfo> =>
+ buildCodecForObject<TalerCorebankApi.GlobalCashoutInfo>()
+ .property("cashout_id", codecForNumber())
+ .property("username", codecForString())
+ .build("TalerCorebankApi.GlobalCashoutInfo");
+
+export const codecForCashoutStatusResponse =
+ (): Codec<TalerCorebankApi.CashoutStatusResponse> =>
+ buildCodecForObject<TalerCorebankApi.CashoutStatusResponse>()
+ .property("amount_debit", codecForAmountString())
+ .property("amount_credit", codecForAmountString())
+ .property("subject", codecForString())
+ .property("creation_time", codecForTimestamp)
+ .build("TalerCorebankApi.CashoutStatusResponse");
+
+export const codecForConversionRatesResponse =
+ (): Codec<TalerCorebankApi.ConversionRatesResponse> =>
+ buildCodecForObject<TalerCorebankApi.ConversionRatesResponse>()
+ .property("buy_at_ratio", codecForDecimalNumber())
+ .property("buy_in_fee", codecForDecimalNumber())
+ .property("sell_at_ratio", codecForDecimalNumber())
+ .property("sell_out_fee", codecForDecimalNumber())
+ .build("TalerCorebankApi.ConversionRatesResponse");
+
+export const codecForMonitorResponse =
+ (): Codec<TalerCorebankApi.MonitorResponse> =>
+ buildCodecForUnion<TalerCorebankApi.MonitorResponse>()
+ .discriminateOn("type")
+ .alternative("no-conversions", codecForMonitorNoConversion())
+ .alternative("with-conversions", codecForMonitorWithCashout())
+ .build("TalerWireGatewayApi.IncomingBankTransaction");
+
+export const codecForMonitorNoConversion =
+ (): Codec<TalerCorebankApi.MonitorNoConversion> =>
+ buildCodecForObject<TalerCorebankApi.MonitorNoConversion>()
+ .property("type", codecForConstString("no-conversions"))
+ .property("talerInCount", codecForNumber())
+ .property("talerInVolume", codecForAmountString())
+ .property("talerOutCount", codecForNumber())
+ .property("talerOutVolume", codecForAmountString())
+ .build("TalerCorebankApi.MonitorJustPayouts");
+
+export const codecForMonitorWithCashout =
+ (): Codec<TalerCorebankApi.MonitorWithConversion> =>
+ buildCodecForObject<TalerCorebankApi.MonitorWithConversion>()
+ .property("type", codecForConstString("with-conversions"))
+ .property("cashinCount", codecForNumber())
+ .property("cashinFiatVolume", codecForAmountString())
+ .property("cashinRegionalVolume", codecForAmountString())
+ .property("cashoutCount", codecForNumber())
+ .property("cashoutFiatVolume", codecForAmountString())
+ .property("cashoutRegionalVolume", codecForAmountString())
+ .property("talerInCount", codecForNumber())
+ .property("talerInVolume", codecForAmountString())
+ .property("talerOutCount", codecForNumber())
+ .property("talerOutVolume", codecForAmountString())
+ .build("TalerCorebankApi.MonitorWithCashout");
+
+export const codecForBankVersion =
+ (): Codec<TalerBankIntegrationApi.BankVersion> =>
+ buildCodecForObject<TalerBankIntegrationApi.BankVersion>()
+ .property("currency", codecForCurrencyName())
+ .property("currency_specification", codecForCurrencySpecificiation())
+ .property("name", codecForConstString("taler-bank-integration"))
+ .property("version", codecForLibtoolVersion())
+ .build("TalerBankIntegrationApi.BankVersion");
+
+export const codecForBankWithdrawalOperationStatus =
+ (): Codec<TalerBankIntegrationApi.BankWithdrawalOperationStatus> =>
+ buildCodecForObject<TalerBankIntegrationApi.BankWithdrawalOperationStatus>()
+ .property(
+ "status",
+ codecForEither(
+ codecForConstString("pending"),
+ codecForConstString("selected"),
+ codecForConstString("aborted"),
+ codecForConstString("confirmed"),
+ ),
+ )
+ .property("amount", codecForAmountString())
+ .property("sender_wire", codecOptional(codecForPaytoString()))
+ .property("suggested_exchange", codecOptional(codecForString()))
+ .property("confirm_transfer_url", codecOptional(codecForURL()))
+ .property("wire_types", codecForList(codecForString()))
+ .property("selected_reserve_pub", codecOptional(codecForString()))
+ .property("selected_exchange_account", codecOptional(codecForString()))
+ .build("TalerBankIntegrationApi.BankWithdrawalOperationStatus");
+
+export const codecForBankWithdrawalOperationPostResponse =
+ (): Codec<TalerBankIntegrationApi.BankWithdrawalOperationPostResponse> =>
+ buildCodecForObject<TalerBankIntegrationApi.BankWithdrawalOperationPostResponse>()
+ .property(
+ "status",
+ codecForEither(
+ codecForConstString("selected"),
+ codecForConstString("aborted"),
+ codecForConstString("confirmed"),
+ ),
+ )
+ .property("confirm_transfer_url", codecOptional(codecForURL()))
+ .build("TalerBankIntegrationApi.BankWithdrawalOperationPostResponse");
+
+export const codecForRevenueConfig = (): Codec<TalerRevenueApi.RevenueConfig> =>
+ buildCodecForObject<TalerRevenueApi.RevenueConfig>()
+ .property("name", codecForConstString("taler-revenue"))
+ .property("version", codecForString())
+ .property("currency", codecForString())
+ .property("implementation", codecOptional(codecForString()))
+ .build("TalerRevenueApi.RevenueConfig");
+
+export const codecForRevenueIncomingHistory =
+ (): Codec<TalerRevenueApi.RevenueIncomingHistory> =>
+ buildCodecForObject<TalerRevenueApi.RevenueIncomingHistory>()
+ .property("credit_account", codecForPaytoString())
+ .property(
+ "incoming_transactions",
+ codecForList(codecForRevenueIncomingBankTransaction()),
+ )
+ .build("TalerRevenueApi.MerchantIncomingHistory");
+
+export const codecForRevenueIncomingBankTransaction =
+ (): Codec<TalerRevenueApi.RevenueIncomingBankTransaction> =>
+ buildCodecForObject<TalerRevenueApi.RevenueIncomingBankTransaction>()
+ .property("amount", codecForAmountString())
+ .property("date", codecForTimestamp)
+ .property("debit_account", codecForPaytoString())
+ .property("row_id", codecForNumber())
+ .property("subject", codecForString())
+ .build("TalerRevenueApi.RevenueIncomingBankTransaction");
+
+export const codecForTransferResponse =
+ (): Codec<TalerWireGatewayApi.TransferResponse> =>
+ buildCodecForObject<TalerWireGatewayApi.TransferResponse>()
+ .property("row_id", codecForNumber())
+ .property("timestamp", codecForTimestamp)
+ .build("TalerWireGatewayApi.TransferResponse");
+
+export const codecForIncomingHistory =
+ (): Codec<TalerWireGatewayApi.IncomingHistory> =>
+ buildCodecForObject<TalerWireGatewayApi.IncomingHistory>()
+ .property("credit_account", codecForPaytoString())
+ .property(
+ "incoming_transactions",
+ codecForList(codecForIncomingBankTransaction()),
+ )
+ .build("TalerWireGatewayApi.IncomingHistory");
+
+export const codecForIncomingBankTransaction =
+ (): Codec<TalerWireGatewayApi.IncomingBankTransaction> =>
+ buildCodecForUnion<TalerWireGatewayApi.IncomingBankTransaction>()
+ .discriminateOn("type")
+ .alternative("RESERVE", codecForIncomingReserveTransaction())
+ .alternative("WAD", codecForIncomingWadTransaction())
+ .build("TalerWireGatewayApi.IncomingBankTransaction");
+
+export const codecForIncomingReserveTransaction =
+ (): Codec<TalerWireGatewayApi.IncomingReserveTransaction> =>
+ buildCodecForObject<TalerWireGatewayApi.IncomingReserveTransaction>()
+ .property("amount", codecForAmountString())
+ .property("date", codecForTimestamp)
+ .property("debit_account", codecForPaytoString())
+ .property("reserve_pub", codecForString())
+ .property("row_id", codecForNumber())
+ .property("type", codecForConstString("RESERVE"))
+ .build("TalerWireGatewayApi.IncomingReserveTransaction");
+
+export const codecForIncomingWadTransaction =
+ (): Codec<TalerWireGatewayApi.IncomingWadTransaction> =>
+ buildCodecForObject<TalerWireGatewayApi.IncomingWadTransaction>()
+ .property("amount", codecForAmountString())
+ .property("credit_account", codecForPaytoString())
+ .property("date", codecForTimestamp)
+ .property("debit_account", codecForPaytoString())
+ .property("origin_exchange_url", codecForURL())
+ .property("row_id", codecForNumber())
+ .property("type", codecForConstString("WAD"))
+ .property("wad_id", codecForString())
+ .build("TalerWireGatewayApi.IncomingWadTransaction");
+
+export const codecForOutgoingHistory =
+ (): Codec<TalerWireGatewayApi.OutgoingHistory> =>
+ buildCodecForObject<TalerWireGatewayApi.OutgoingHistory>()
+ .property("debit_account", codecForPaytoString())
+ .property(
+ "outgoing_transactions",
+ codecForList(codecForOutgoingBankTransaction()),
+ )
+ .build("TalerWireGatewayApi.OutgoingHistory");
+
+export const codecForOutgoingBankTransaction =
+ (): Codec<TalerWireGatewayApi.OutgoingBankTransaction> =>
+ buildCodecForObject<TalerWireGatewayApi.OutgoingBankTransaction>()
+ .property("amount", codecForAmountString())
+ .property("credit_account", codecForPaytoString())
+ .property("date", codecForTimestamp)
+ .property("exchange_base_url", codecForURL())
+ .property("row_id", codecForNumber())
+ .property("wtid", codecForString())
+ .build("TalerWireGatewayApi.OutgoingBankTransaction");
+
+export const codecForAddIncomingResponse =
+ (): Codec<TalerWireGatewayApi.AddIncomingResponse> =>
+ buildCodecForObject<TalerWireGatewayApi.AddIncomingResponse>()
+ .property("row_id", codecForNumber())
+ .property("timestamp", codecForTimestamp)
+ .build("TalerWireGatewayApi.AddIncomingResponse");
+
+export const codecForAmlRecords = (): Codec<TalerExchangeApi.AmlRecords> =>
+ buildCodecForObject<TalerExchangeApi.AmlRecords>()
+ .property("records", codecForList(codecForAmlRecord()))
+ .build("TalerExchangeApi.PublicAccountsResponse");
+
+export const codecForAmlRecord = (): Codec<TalerExchangeApi.AmlRecord> =>
+ buildCodecForObject<TalerExchangeApi.AmlRecord>()
+ .property("current_state", codecForNumber())
+ .property("h_payto", codecForString())
+ .property("rowid", codecForNumber())
+ .property("threshold", codecForAmountString())
+ .build("TalerExchangeApi.AmlRecord");
+
+export const codecForAmlDecisionDetails =
+ (): Codec<TalerExchangeApi.AmlDecisionDetails> =>
+ buildCodecForObject<TalerExchangeApi.AmlDecisionDetails>()
+ .property("aml_history", codecForList(codecForAmlDecisionDetail()))
+ .property("kyc_attributes", codecForList(codecForKycDetail()))
+ .build("TalerExchangeApi.AmlDecisionDetails");
+
+export const codecForAmlDecisionDetail =
+ (): Codec<TalerExchangeApi.AmlDecisionDetail> =>
+ buildCodecForObject<TalerExchangeApi.AmlDecisionDetail>()
+ .property("justification", codecForString())
+ .property("new_state", codecForNumber())
+ .property("decision_time", codecForTimestamp)
+ .property("new_threshold", codecForAmountString())
+ .property("decider_pub", codecForString())
+ .build("TalerExchangeApi.AmlDecisionDetail");
+
+export const codecForChallenge = (): Codec<TalerCorebankApi.Challenge> =>
+ buildCodecForObject<TalerCorebankApi.Challenge>()
+ .property("challenge_id", codecForNumber())
+ .build("TalerCorebankApi.Challenge");
+
+export const codecForTanTransmission =
+ (): Codec<TalerCorebankApi.TanTransmission> =>
+ buildCodecForObject<TalerCorebankApi.TanTransmission>()
+ .property(
+ "tan_channel",
+ codecForEither(
+ codecForConstString(TalerCorebankApi.TanChannel.SMS),
+ codecForConstString(TalerCorebankApi.TanChannel.EMAIL),
+ ),
+ )
+ .property("tan_info", codecForString())
+ .build("TalerCorebankApi.TanTransmission");
+
+interface KycDetail {
+ provider_section: string;
+ attributes?: Object;
+ collection_time: Timestamp;
+ expiration_time: Timestamp;
+}
+export const codecForKycDetail = (): Codec<TalerExchangeApi.KycDetail> =>
+ buildCodecForObject<TalerExchangeApi.KycDetail>()
+ .property("provider_section", codecForString())
+ .property("attributes", codecOptional(codecForAny()))
+ .property("collection_time", codecForTimestamp)
+ .property("expiration_time", codecForTimestamp)
+ .build("TalerExchangeApi.KycDetail");
+
+export const codecForAmlDecision = (): Codec<TalerExchangeApi.AmlDecision> =>
+ buildCodecForObject<TalerExchangeApi.AmlDecision>()
+ .property("justification", codecForString())
+ .property("new_threshold", codecForAmountString())
+ .property("h_payto", codecForString())
+ .property("new_state", codecForNumber())
+ .property("officer_sig", codecForString())
+ .property("decision_time", codecForTimestamp)
+ .property("kyc_requirements", codecOptional(codecForList(codecForString())))
+ .build("TalerExchangeApi.AmlDecision");
+
+export const codecForConversionInfo =
+ (): Codec<TalerBankConversionApi.ConversionInfo> =>
+ buildCodecForObject<TalerBankConversionApi.ConversionInfo>()
+ .property("cashin_fee", codecForAmountString())
+ .property("cashin_min_amount", codecForAmountString())
+ .property("cashin_ratio", codecForDecimalNumber())
+ .property(
+ "cashin_rounding_mode",
+ codecForEither(
+ codecForConstString("zero"),
+ codecForConstString("up"),
+ codecForConstString("nearest"),
+ ),
+ )
+ .property("cashin_tiny_amount", codecForAmountString())
+ .property("cashout_fee", codecForAmountString())
+ .property("cashout_min_amount", codecForAmountString())
+ .property("cashout_ratio", codecForDecimalNumber())
+ .property(
+ "cashout_rounding_mode",
+ codecForEither(
+ codecForConstString("zero"),
+ codecForConstString("up"),
+ codecForConstString("nearest"),
+ ),
+ )
+ .property("cashout_tiny_amount", codecForAmountString())
+ .build("ConversionBankConfig.ConversionInfo");
+
+export const codecForConversionBankConfig =
+ (): Codec<TalerBankConversionApi.IntegrationConfig> =>
+ buildCodecForObject<TalerBankConversionApi.IntegrationConfig>()
+ .property("name", codecForConstString("taler-conversion-info"))
+ .property("version", codecForString())
+ .property("regional_currency", codecForString())
+ .property(
+ "regional_currency_specification",
+ codecForCurrencySpecificiation(),
+ )
+ .property("fiat_currency", codecForString())
+ .property("fiat_currency_specification", codecForCurrencySpecificiation())
+
+ .property("conversion_rate", codecForConversionInfo())
+ .build("ConversionBankConfig.IntegrationConfig");
+
+export const codecForChallengerTermsOfServiceResponse =
+ (): Codec<ChallengerApi.ChallengerTermsOfServiceResponse> =>
+ buildCodecForObject<ChallengerApi.ChallengerTermsOfServiceResponse>()
+ .property("name", codecForConstString("challenger"))
+ .property("version", codecForString())
+ .property("implementation", codecOptional(codecForString()))
+ .build("ChallengerApi.ChallengerTermsOfServiceResponse");
+
+export const codecForChallengeSetupResponse =
+ (): Codec<ChallengerApi.ChallengeSetupResponse> =>
+ buildCodecForObject<ChallengerApi.ChallengeSetupResponse>()
+ .property("nonce", codecForString())
+ .build("ChallengerApi.ChallengeSetupResponse");
+
+export const codecForChallengeStatus =
+ (): Codec<ChallengerApi.ChallengeStatus> =>
+ buildCodecForObject<ChallengerApi.ChallengeStatus>()
+ .property("restrictions", codecOptional(codecForMap(codecForAny())))
+ .property("fix_address", codecForBoolean())
+ .property("last_address", codecOptional(codecForMap(codecForAny())))
+ .property("changes_left", codecForNumber())
+ .build("ChallengerApi.ChallengeStatus");
+export const codecForChallengeCreateResponse =
+ (): Codec<ChallengerApi.ChallengeCreateResponse> =>
+ buildCodecForObject<ChallengerApi.ChallengeCreateResponse>()
+ .property("attempts_left", codecForNumber())
+ .property("address", codecForAny())
+ .property("transmitted", codecForBoolean())
+ .property("next_tx_time", codecForString())
+ .build("ChallengerApi.ChallengeCreateResponse");
+
+export const codecForInvalidPinResponse =
+ (): Codec<ChallengerApi.InvalidPinResponse> =>
+ buildCodecForObject<ChallengerApi.InvalidPinResponse>()
+ .property("ec", codecOptional(codecForNumber()))
+ .property("hint", codecForAny())
+ .property("addresses_left", codecForNumber())
+ .property("pin_transmissions_left", codecForNumber())
+ .property("auth_attempts_left", codecForNumber())
+ .property("exhausted", codecForBoolean())
+ .property("no_challenge", codecForBoolean())
+ .build("ChallengerApi.InvalidPinResponse");
+
+export const codecForChallengerAuthResponse =
+ (): Codec<ChallengerApi.ChallengerAuthResponse> =>
+ buildCodecForObject<ChallengerApi.ChallengerAuthResponse>()
+ .property("access_token", codecForString())
+ .property("token_type", codecForAny())
+ .property("expires_in", codecForNumber())
+ .build("ChallengerApi.ChallengerAuthResponse");
+
+export const codecForChallengerInfoResponse =
+ (): Codec<ChallengerApi.ChallengerInfoResponse> =>
+ buildCodecForObject<ChallengerApi.ChallengerInfoResponse>()
+ .property("id", codecForNumber())
+ .property("address", codecForAny())
+ .property("address_type", codecForString())
+ .property("expires", codecForTimestamp)
+ .build("ChallengerApi.ChallengerInfoResponse");
+
+type EmailAddress = string;
+type PhoneNumber = string;
+type EddsaSignature = string;
+// base32 encoded RSA blinded signature.
+type BlindedRsaSignature = string;
+type Base32 = string;
+
+type DecimalNumber = string;
+type RsaSignature = string;
+type Float = number;
+type LibtoolVersion = string;
+// The type of a coin's blinded envelope depends on the cipher that is used
+// for signing with a denomination key.
+type CoinEnvelope = RSACoinEnvelope | CSCoinEnvelope;
+// For denomination signatures based on RSA, the planchet is just a blinded
+// coin's public EdDSA key.
+interface RSACoinEnvelope {
+ cipher: "RSA" | "RSA+age_restricted";
+ rsa_blinded_planchet: string; // Crockford Base32 encoded
+}
+// For denomination signatures based on Blind Clause-Schnorr, the planchet
+// consists of the public nonce and two Curve25519 scalars which are two
+// blinded challenges in the Blinded Clause-Schnorr signature scheme.
+// See https://taler.net/papers/cs-thesis.pdf for details.
+interface CSCoinEnvelope {
+ cipher: "CS" | "CS+age_restricted";
+ cs_nonce: string; // Crockford Base32 encoded
+ cs_blinded_c0: string; // Crockford Base32 encoded
+ cs_blinded_c1: string; // Crockford Base32 encoded
+}
+// Secret for blinding/unblinding.
+// An RSA blinding secret, which is basically
+// a 256-bit nonce, converted to Crockford Base32.
+type DenominationBlindingKeyP = string;
+
+//FIXME: implement this codec
+const codecForURL = codecForString;
+//FIXME: implement this codec
+const codecForLibtoolVersion = codecForString;
+//FIXME: implement this codec
+const codecForCurrencyName = codecForString;
+//FIXME: implement this codec
+const codecForDecimalNumber = codecForString;
+
+export type WithdrawalOperationStatus =
+ | "pending"
+ | "selected"
+ | "aborted"
+ | "confirmed";
+
+export namespace TalerWireGatewayApi {
+ export interface TransferResponse {
+ // Timestamp that indicates when the wire transfer will be executed.
+ // In cases where the wire transfer gateway is unable to know when
+ // the wire transfer will be executed, the time at which the request
+ // has been received and stored will be returned.
+ // The purpose of this field is for debugging (humans trying to find
+ // the transaction) as well as for taxation (determining which
+ // time period a transaction belongs to).
+ timestamp: Timestamp;
+
+ // Opaque ID of the transaction that the bank has made.
+ row_id: SafeUint64;
+ }
+
+ export interface TransferRequest {
+ // Nonce to make the request idempotent. Requests with the same
+ // transaction_uid that differ in any of the other fields
+ // are rejected.
+ request_uid: HashCode;
+
+ // Amount to transfer.
+ amount: AmountString;
+
+ // Base URL of the exchange. Shall be included by the bank gateway
+ // in the appropriate section of the wire transfer details.
+ exchange_base_url: string;
+
+ // Wire transfer identifier chosen by the exchange,
+ // used by the merchant to identify the Taler order(s)
+ // associated with this wire transfer.
+ wtid: ShortHashCode;
+
+ // The recipient's account identifier as a payto URI.
+ credit_account: PaytoString;
+ }
+
+ export interface IncomingHistory {
+ // Array of incoming transactions.
+ incoming_transactions: IncomingBankTransaction[];
+
+ // Payto URI to identify the receiver of funds.
+ // This must be one of the exchange's bank accounts.
+ // Credit account is shared by all incoming transactions
+ // as per the nature of the request.
+
+ // undefined if incoming transaction is empty
+ credit_account?: PaytoString;
+ }
+
+ // Union discriminated by the "type" field.
+ export type IncomingBankTransaction =
+ | IncomingReserveTransaction
+ | IncomingWadTransaction;
+
+ export interface IncomingReserveTransaction {
+ type: "RESERVE";
+
+ // Opaque identifier of the returned record.
+ row_id: SafeUint64;
+
+ // Date of the transaction.
+ date: Timestamp;
+
+ // Amount transferred.
+ amount: AmountString;
+
+ // Payto URI to identify the sender of funds.
+ debit_account: PaytoString;
+
+ // The reserve public key extracted from the transaction details.
+ reserve_pub: EddsaPublicKey;
+ }
+
+ export interface IncomingWadTransaction {
+ type: "WAD";
+
+ // Opaque identifier of the returned record.
+ row_id: SafeUint64;
+
+ // Date of the transaction.
+ date: Timestamp;
+
+ // Amount transferred.
+ amount: AmountString;
+
+ // Payto URI to identify the receiver of funds.
+ // This must be one of the exchange's bank accounts.
+ credit_account: PaytoString;
+
+ // Payto URI to identify the sender of funds.
+ debit_account: PaytoString;
+
+ // Base URL of the exchange that originated the wad.
+ origin_exchange_url: string;
+
+ // The reserve public key extracted from the transaction details.
+ wad_id: WadId;
+ }
+
+ export interface OutgoingHistory {
+ // Array of outgoing transactions.
+ outgoing_transactions: OutgoingBankTransaction[];
+
+ // Payto URI to identify the sender of funds.
+ // This must be one of the exchange's bank accounts.
+ // Credit account is shared by all incoming transactions
+ // as per the nature of the request.
+
+ // undefined if outgoing transactions is empty
+ debit_account?: PaytoString;
+ }
+
+ export interface OutgoingBankTransaction {
+ // Opaque identifier of the returned record.
+ row_id: SafeUint64;
+
+ // Date of the transaction.
+ date: Timestamp;
+
+ // Amount transferred.
+ amount: AmountString;
+
+ // Payto URI to identify the receiver of funds.
+ credit_account: PaytoString;
+
+ // The wire transfer ID in the outgoing transaction.
+ wtid: ShortHashCode;
+
+ // Base URL of the exchange.
+ exchange_base_url: string;
+ }
+
+ export interface AddIncomingRequest {
+ // Amount to transfer.
+ amount: AmountString;
+
+ // Reserve public key that is included in the wire transfer details
+ // to identify the reserve that is being topped up.
+ reserve_pub: EddsaPublicKey;
+
+ // Account (as payto URI) that makes the wire transfer to the exchange.
+ // Usually this account must be created by the test harness before this API is
+ // used. An exception is the "exchange-fakebank", where any debit account can be
+ // specified, as it is automatically created.
+ debit_account: PaytoString;
+ }
+
+ export interface AddIncomingResponse {
+ // Timestamp that indicates when the wire transfer will be executed.
+ // In cases where the wire transfer gateway is unable to know when
+ // the wire transfer will be executed, the time at which the request
+ // has been received and stored will be returned.
+ // The purpose of this field is for debugging (humans trying to find
+ // the transaction) as well as for taxation (determining which
+ // time period a transaction belongs to).
+ timestamp: Timestamp;
+
+ // Opaque ID of the transaction that the bank has made.
+ row_id: SafeUint64;
+ }
+}
+
+export namespace TalerRevenueApi {
+ export interface RevenueConfig {
+ // Name of the API.
+ name: "taler-revenue";
+
+ // libtool-style representation of the Bank protocol version, see
+ // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning
+ // The format is "current:revision:age".
+ version: string;
+
+ // Currency used by this gateway.
+ currency: string;
+
+ // URN of the implementation (needed to interpret 'revision' in version).
+ // @since v0, may become mandatory in the future.
+ implementation?: string;
+ }
+
+ export interface RevenueIncomingHistory {
+ // Array of incoming transactions.
+ incoming_transactions: RevenueIncomingBankTransaction[];
+
+ // Payto URI to identify the receiver of funds.
+ // Credit account is shared by all incoming transactions
+ // as per the nature of the request.
+ credit_account: string;
+ }
+
+ export interface RevenueIncomingBankTransaction {
+ // Opaque identifier of the returned record.
+ row_id: SafeUint64;
+
+ // Date of the transaction.
+ date: Timestamp;
+
+ // Amount transferred.
+ amount: AmountString;
+
+ // Payto URI to identify the sender of funds.
+ debit_account: string;
+
+ // The wire transfer subject.
+ subject: string;
+ }
+}
+
+export namespace TalerBankConversionApi {
+ export interface ConversionInfo {
+ // Exchange rate to buy regional currency from fiat
+ cashin_ratio: DecimalNumber;
+
+ // Exchange rate to sell regional currency for fiat
+ cashout_ratio: DecimalNumber;
+
+ // Fee to subtract after applying the cashin ratio.
+ cashin_fee: AmountString;
+
+ // Fee to subtract after applying the cashout ratio.
+ cashout_fee: AmountString;
+
+ // Minimum amount authorised for cashin, in fiat before conversion
+ cashin_min_amount: AmountString;
+
+ // Minimum amount authorised for cashout, in regional before conversion
+ cashout_min_amount: AmountString;
+
+ // Smallest possible regional amount, converted amount is rounded to this amount
+ cashin_tiny_amount: AmountString;
+
+ // Smallest possible fiat amount, converted amount is rounded to this amount
+ cashout_tiny_amount: AmountString;
+
+ // Rounding mode used during cashin conversion
+ cashin_rounding_mode: "zero" | "up" | "nearest";
+
+ // Rounding mode used during cashout conversion
+ cashout_rounding_mode: "zero" | "up" | "nearest";
+ }
+
+ export interface IntegrationConfig {
+ // libtool-style representation of the Bank protocol version, see
+ // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning
+ // The format is "current:revision:age".
+ version: string;
+
+ // Name of the API.
+ name: "taler-conversion-info";
+
+ // Currency used by this bank.
+ regional_currency: string;
+
+ // How the bank SPA should render this currency.
+ regional_currency_specification: CurrencySpecification;
+
+ // External currency used during conversion.
+ fiat_currency: string;
+
+ // How the bank SPA should render this currency.
+ fiat_currency_specification: CurrencySpecification;
+
+ // Extra conversion rate information.
+ // Only present if server opts in to report the static conversion rate.
+ conversion_rate: ConversionInfo;
+ }
+
+ export interface CashinConversionResponse {
+ // Amount that the user will get deducted from their fiat
+ // bank account, according to the 'amount_credit' value.
+ amount_debit: AmountString;
+ // Amount that the user will receive in their regional
+ // bank account, according to 'amount_debit'.
+ amount_credit: AmountString;
+ }
+
+ export interface CashoutConversionResponse {
+ // Amount that the user will get deducted from their regional
+ // bank account, according to the 'amount_credit' value.
+ amount_debit: AmountString;
+ // Amount that the user will receive in their fiat
+ // bank account, according to 'amount_debit'.
+ amount_credit: AmountString;
+ }
+
+ export type RoundingMode = "zero" | "up" | "nearest";
+
+ export interface ConversionRate {
+ // Exchange rate to buy regional currency from fiat
+ cashin_ratio: DecimalNumber;
+
+ // Fee to subtract after applying the cashin ratio.
+ cashin_fee: AmountString;
+
+ // Minimum amount authorised for cashin, in fiat before conversion
+ cashin_min_amount: AmountString;
+
+ // Smallest possible regional amount, converted amount is rounded to this amount
+ cashin_tiny_amount: AmountString;
+
+ // Rounding mode used during cashin conversion
+ cashin_rounding_mode: RoundingMode;
+
+ // Exchange rate to sell regional currency for fiat
+ cashout_ratio: DecimalNumber;
+
+ // Fee to subtract after applying the cashout ratio.
+ cashout_fee: AmountString;
+
+ // Minimum amount authorised for cashout, in regional before conversion
+ cashout_min_amount: AmountString;
+
+ // Smallest possible fiat amount, converted amount is rounded to this amount
+ cashout_tiny_amount: AmountString;
+
+ // Rounding mode used during cashout conversion
+ cashout_rounding_mode: RoundingMode;
+ }
+}
+
+export namespace TalerBankIntegrationApi {
+ export interface BankVersion {
+ // libtool-style representation of the Bank protocol version, see
+ // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning
+ // The format is "current:revision:age".
+ version: string;
+
+ // Currency used by this bank.
+ currency: string;
+
+ // How the bank SPA should render this currency.
+ currency_specification?: CurrencySpecification;
+
+ // Name of the API.
+ name: "taler-bank-integration";
+ }
+
+ export interface BankWithdrawalOperationStatus {
+ // Current status of the operation
+ // pending: the operation is pending parameters selection (exchange and reserve public key)
+ // selected: the operations has been selected and is pending confirmation
+ // aborted: the operation has been aborted
+ // confirmed: the transfer has been confirmed and registered by the bank
+ status: WithdrawalOperationStatus;
+
+ // Amount that will be withdrawn with this operation
+ // (raw amount without fee considerations).
+ amount: AmountString;
+
+ // Bank account of the customer that is withdrawing, as a
+ // payto URI.
+ sender_wire?: PaytoString;
+
+ // Suggestion for an exchange given by the bank.
+ suggested_exchange?: string;
+
+ // URL that the user needs to navigate to in order to
+ // complete some final confirmation (e.g. 2FA).
+ // It may contain withdrawal operation id
+ confirm_transfer_url?: string;
+
+ // Wire transfer types supported by the bank.
+ wire_types: string[];
+
+ // Reserve public key selected by the exchange,
+ // only non-null if status is selected or confirmed.
+ selected_reserve_pub?: string;
+
+ // Exchange account selected by the wallet
+ // only non-null if status is selected or confirmed.
+ selected_exchange_account?: string;
+ }
+
+ export interface BankWithdrawalOperationPostRequest {
+ // Reserve public key.
+ reserve_pub: string;
+
+ // Payto address of the exchange selected for the withdrawal.
+ selected_exchange: PaytoString;
+ }
+
+ export interface BankWithdrawalOperationPostResponse {
+ // Current status of the operation
+ // pending: the operation is pending parameters selection (exchange and reserve public key)
+ // selected: the operations has been selected and is pending confirmation
+ // aborted: the operation has been aborted
+ // confirmed: the transfer has been confirmed and registered by the bank
+ status: Omit<"pending", WithdrawalOperationStatus>;
+
+ // URL that the user needs to navigate to in order to
+ // complete some final confirmation (e.g. 2FA).
+ //
+ // Only applicable when status is selected.
+ // It may contain withdrawal operation id
+ confirm_transfer_url?: string;
+ }
+}
+
+export namespace TalerCorebankApi {
+ export interface IntegrationConfig {
+ // libtool-style representation of the Bank protocol version, see
+ // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning
+ // The format is "current:revision:age".
+ version: string;
+
+ currency: string;
+
+ // How the bank SPA should render this currency.
+ currency_specification: CurrencySpecification;
+
+ // Name of the API.
+ name: "taler-bank-integration";
+ }
+ export interface Config {
+ // Name of this API, always "taler-corebank".
+ name: "libeufin-bank";
+ // name: "taler-corebank";
+
+ // API version in the form $n:$n:$n
+ version: string;
+
+ // Bank display name to be used in user interfaces.
+ // For consistency use "Taler Bank" if missing.
+ // @since v4, will become mandatory in the next version.
+ bank_name: string;
+
+ // If 'true' the server provides local currency conversion support
+ // If 'false' some parts of the API are not supported and return 501
+ allow_conversion: boolean;
+
+ // If 'true' anyone can register
+ // If 'false' only the admin can
+ allow_registrations: boolean;
+
+ // If 'true' account can delete themselves
+ // If 'false' only the admin can delete accounts
+ allow_deletions: boolean;
+
+ // If 'true' anyone can edit their name
+ // If 'false' only admin can
+ allow_edit_name: boolean;
+
+ // If 'true' anyone can edit their cashout account
+ // If 'false' only the admin
+ allow_edit_cashout_payto_uri: boolean;
+
+ // Default debt limit for newly created accounts
+ default_debit_threshold: AmountString;
+
+ // Currency used by this bank.
+ currency: string;
+
+ // How the bank SPA should render this currency.
+ currency_specification: CurrencySpecification;
+
+ // TAN channels supported by the server
+ supported_tan_channels: TanChannel[];
+
+ // Wire transfer type supported by the bank.
+ // Default to 'iban' is missing
+ // @since v4, may become mandatory in the future.
+ wire_type: string;
+ }
+
+ export interface BankAccountCreateWithdrawalRequest {
+ // Amount to withdraw.
+ amount: AmountString;
+ }
+ export interface BankAccountCreateWithdrawalResponse {
+ // ID of the withdrawal, can be used to view/modify the withdrawal operation.
+ withdrawal_id: string;
+
+ // URI that can be passed to the wallet to initiate the withdrawal.
+ taler_withdraw_uri: TalerUriString;
+ }
+ export interface WithdrawalPublicInfo {
+ // Current status of the operation
+ // pending: the operation is pending parameters selection (exchange and reserve public key)
+ // selected: the operations has been selected and is pending confirmation
+ // aborted: the operation has been aborted
+ // confirmed: the transfer has been confirmed and registered by the bank
+ status: WithdrawalOperationStatus;
+
+ // Amount that will be withdrawn with this operation
+ // (raw amount without fee considerations).
+ amount: AmountString;
+
+ // Account username
+ username: string;
+
+ // Reserve public key selected by the exchange,
+ // only non-null if status is selected or confirmed.
+ selected_reserve_pub?: string;
+
+ // Exchange account selected by the wallet
+ // only non-null if status is selected or confirmed.
+ selected_exchange_account?: PaytoString;
+ }
+
+ export interface BankAccountTransactionsResponse {
+ transactions: BankAccountTransactionInfo[];
+ }
+
+ export interface BankAccountTransactionInfo {
+ creditor_payto_uri: PaytoString;
+ debtor_payto_uri: PaytoString;
+
+ amount: AmountString;
+ direction: "debit" | "credit";
+
+ subject: string;
+
+ // Transaction unique ID. Matches
+ // $transaction_id from the URI.
+ row_id: number;
+ date: Timestamp;
+ }
+
+ export interface CreateTransactionRequest {
+ // Address in the Payto format of the wire transfer receiver.
+ // It needs at least the 'message' query string parameter.
+ payto_uri: PaytoString;
+
+ // Transaction amount (in the $currency:x.y format), optional.
+ // However, when not given, its value must occupy the 'amount'
+ // query string parameter of the 'payto' field. In case it
+ // is given in both places, the paytoUri's takes the precedence.
+ amount?: AmountString;
+
+ // Nonce to make the request idempotent. Requests with the same
+ // request_uid that differ in any of the other fields
+ // are rejected.
+ // @since v4, will become mandatory in the next version.
+ request_uid?: ShortHashCode;
+ }
+
+ export interface CreateTransactionResponse {
+ // ID identifying the transaction being created
+ row_id: Integer;
+ }
+
+ export interface RegisterAccountResponse {
+ // Internal payto URI of this bank account.
+ internal_payto_uri: PaytoString;
+ }
+
+ export interface RegisterAccountRequest {
+ // Username
+ username: string;
+
+ // Password.
+ password: string;
+
+ // Legal name of the account owner
+ name: string;
+
+ // Defaults to false.
+ is_public?: boolean;
+
+ // Is this a taler exchange account?
+ // If true:
+ // - incoming transactions to the account that do not
+ // have a valid reserve public key are automatically
+ // - the account provides the taler-wire-gateway-api endpoints
+ // Defaults to false.
+ is_taler_exchange?: boolean;
+
+ // Addresses where to send the TAN for transactions.
+ contact_data?: ChallengeContactData;
+
+ // 'payto' address of a fiat bank account.
+ // Payments will be sent to this bank account
+ // when the user wants to convert the regional currency
+ // back to fiat currency outside bank.
+ cashout_payto_uri?: PaytoString;
+
+ // Internal payto URI of this bank account.
+ // Used mostly for testing.
+ payto_uri?: PaytoString;
+
+ // If present, set the max debit allowed for this user
+ // Only admin can set this property.
+ debit_threshold?: AmountString;
+
+ // If present, enables 2FA and set the TAN channel used for challenges
+ // Only admin can set this property, other user can reconfig their account
+ // after creation.
+ tan_channel?: TanChannel;
+ }
+
+ export interface ChallengeContactData {
+ // E-Mail address
+ email?: EmailAddress;
+
+ // Phone number.
+ phone?: PhoneNumber;
+ }
+
+ export interface AccountReconfiguration {
+ // Addresses where to send the TAN for transactions.
+ // Currently only used for cashouts.
+ // If missing, cashouts will fail.
+ // In the future, might be used for other transactions
+ // as well.
+ // Only admin can change this property.
+ contact_data?: ChallengeContactData;
+
+ // 'payto' URI of a fiat bank account.
+ // Payments will be sent to this bank account
+ // when the user wants to convert the regional currency
+ // back to fiat currency outside bank.
+ // Only admin can change this property if not allowed in config
+ cashout_payto_uri?: PaytoString;
+
+ // If present, change the legal name associated with $username.
+ // Only admin can change this property if not allowed in config
+ name?: string;
+
+ // Make this account visible to anyone?
+ is_public?: boolean;
+
+ // If present, change the max debit allowed for this user
+ // Only admin can change this property.
+ debit_threshold?: AmountString;
+
+ //FIX: missing in SPEC
+ // If present, enables 2FA and set the TAN channel used for challenges
+ tan_channel?: TanChannel | null;
+ }
+
+ export interface AccountPasswordChange {
+ // New password.
+ new_password: string;
+ // Old password. If present, check that the old password matches.
+ // Optional for admin account.
+ old_password?: string;
+ }
+
+ export interface PublicAccountsResponse {
+ public_accounts: PublicAccount[];
+ }
+ export interface PublicAccount {
+ // Username of the account
+ username: string;
+
+ // Internal payto URI of this bank account.
+ payto_uri: string;
+
+ // Current balance of the account
+ balance: Balance;
+
+ // Is this a taler exchange account?
+ is_taler_exchange: boolean;
+
+ // Opaque unique ID used for pagination.
+ // @since v4, will become mandatory in the future.
+ row_id?: Integer;
+ }
+
+ export interface ListBankAccountsResponse {
+ accounts: AccountMinimalData[];
+ }
+ export interface Balance {
+ amount: AmountString;
+ credit_debit_indicator: "credit" | "debit";
+ }
+ export interface AccountMinimalData {
+ // Username
+ username: string;
+
+ // Legal name of the account owner.
+ name: string;
+
+ // Internal payto URI of this bank account.
+ payto_uri: PaytoString;
+
+ // current balance of the account
+ balance: Balance;
+
+ // Number indicating the max debit allowed for the requesting user.
+ debit_threshold: AmountString;
+
+ // Is this account visible to anyone?
+ is_public: boolean;
+
+ // Is this a taler exchange account?
+ is_taler_exchange: boolean;
+
+ // Opaque unique ID used for pagination.
+ // @since v4, will become mandatory in the future.
+ row_id?: Integer;
+ }
+
+ export interface AccountData {
+ // Legal name of the account owner.
+ name: string;
+
+ // Available balance on the account.
+ balance: Balance;
+
+ // payto://-URI of the account.
+ payto_uri: PaytoString;
+
+ // Number indicating the max debit allowed for the requesting user.
+ debit_threshold: AmountString;
+
+ contact_data?: ChallengeContactData;
+
+ // 'payto' address pointing the bank account
+ // where to send cashouts. This field is optional
+ // because not all the accounts are required to participate
+ // in the merchants' circuit. One example is the exchange:
+ // that never cashouts. Registering these accounts can
+ // be done via the access API.
+ cashout_payto_uri?: PaytoString;
+
+ // Is this account visible to anyone?
+ is_public: boolean;
+
+ // Is this a taler exchange account?
+ is_taler_exchange: boolean;
+
+ // Is 2FA enabled and what channel is used for challenges?
+ tan_channel?: TanChannel;
+ }
+
+ export interface CashoutRequest {
+ // Nonce to make the request idempotent. Requests with the same
+ // request_uid that differ in any of the other fields
+ // are rejected.
+ request_uid: ShortHashCode;
+
+ // Optional subject to associate to the
+ // cashout operation. This data will appear
+ // as the incoming wire transfer subject in
+ // the user's fiat bank account.
+ subject?: string;
+
+ // That is the plain amount that the user specified
+ // to cashout. Its $currency is the (regional) currency of the
+ // bank instance.
+ amount_debit: AmountString;
+
+ // That is the amount that will effectively be
+ // transferred by the bank to the user's bank
+ // account, that is external to the regional currency.
+ // It is expressed in the fiat currency and
+ // is calculated after the cashout fee and the
+ // exchange rate. See the /cashout-rates call.
+ // The client needs to calculate this amount
+ // correctly based on the amount_debit and the cashout rate,
+ // otherwise the request will fail.
+ amount_credit: AmountString;
+ }
+
+ export interface CashoutResponse {
+ // ID identifying the operation being created
+ cashout_id: number;
+ }
+
+ /**
+ * @deprecated since 4, use 2fa
+ */
+ export interface CashoutConfirmRequest {
+ // the TAN that confirms $CASHOUT_ID.
+ tan: string;
+ }
+
+ export interface Cashouts {
+ // Every string represents a cash-out operation ID.
+ cashouts: CashoutInfo[];
+ }
+
+ export interface CashoutInfo {
+ cashout_id: number;
+ /**
+ * @deprecated since 4, use new 2fa
+ */
+ status?: "pending" | "aborted" | "confirmed";
+ }
+ export interface GlobalCashouts {
+ // Every string represents a cash-out operation ID.
+ cashouts: GlobalCashoutInfo[];
+ }
+ export interface GlobalCashoutInfo {
+ cashout_id: number;
+ username: string;
+ }
+
+ export interface CashoutStatusResponse {
+ // Amount debited to the internal
+ // regional currency bank account.
+ amount_debit: AmountString;
+
+ // Amount credited to the external bank account.
+ amount_credit: AmountString;
+
+ // Transaction subject.
+ subject: string;
+
+ // Time when the cashout was created.
+ creation_time: Timestamp;
+ }
+
+ export interface ConversionRatesResponse {
+ // Exchange rate to buy the local currency from the external one
+ buy_at_ratio: DecimalNumber;
+
+ // Exchange rate to sell the local currency for the external one
+ sell_at_ratio: DecimalNumber;
+
+ // Fee to subtract after applying the buy ratio.
+ buy_in_fee: DecimalNumber;
+
+ // Fee to subtract after applying the sell ratio.
+ sell_out_fee: DecimalNumber;
+ }
+
+ export enum MonitorTimeframeParam {
+ hour,
+ day,
+ month,
+ year,
+ decade,
+ }
+
+ export type MonitorResponse = MonitorNoConversion | MonitorWithConversion;
+
+ // Monitoring stats when conversion is not supported
+ export interface MonitorNoConversion {
+ type: "no-conversions";
+
+ // How many payments were made to a Taler exchange by another
+ // bank account.
+ talerInCount: number;
+
+ // Overall volume that has been paid to a Taler
+ // exchange by another bank account.
+ talerInVolume: AmountString;
+
+ // How many payments were made by a Taler exchange to another
+ // bank account.
+ talerOutCount: number;
+
+ // Overall volume that has been paid by a Taler
+ // exchange to another bank account.
+ talerOutVolume: AmountString;
+ }
+ // Monitoring stats when conversion is supported
+ export interface MonitorWithConversion {
+ type: "with-conversions";
+
+ // How many cashin operations were confirmed by a
+ // wallet owner. Note: wallet owners
+ // are NOT required to be customers of the libeufin-bank.
+ cashinCount: number;
+
+ // Overall regional currency that has been paid by the regional admin account
+ // to regional bank accounts to fulfill all the confirmed cashin operations.
+ cashinRegionalVolume: AmountString;
+
+ // Overall fiat currency that has been paid to the fiat admin account
+ // by fiat bank accounts to fulfill all the confirmed cashin operations.
+ cashinFiatVolume: AmountString;
+
+ // How many cashout operations were confirmed.
+ cashoutCount: number;
+
+ // Overall regional currency that has been paid to the regional admin account
+ // by fiat bank accounts to fulfill all the confirmed cashout operations.
+ cashoutRegionalVolume: AmountString;
+
+ // Overall fiat currency that has been paid by the fiat admin account
+ // to fiat bank accounts to fulfill all the confirmed cashout operations.
+ cashoutFiatVolume: AmountString;
+
+ // How many payments were made to a Taler exchange by another
+ // bank account.
+ talerInCount: number;
+
+ // Overall volume that has been paid to a Taler
+ // exchange by another bank account.
+ talerInVolume: AmountString;
+
+ // How many payments were made by a Taler exchange to another
+ // bank account.
+ talerOutCount: number;
+
+ // Overall volume that has been paid by a Taler
+ // exchange to another bank account.
+ talerOutVolume: AmountString;
+ }
+ export interface TanTransmission {
+ // Channel of the last successful transmission of the TAN challenge.
+ tan_channel: TanChannel;
+
+ // Info of the last successful transmission of the TAN challenge.
+ tan_info: string;
+ }
+
+ export interface Challenge {
+ // Unique identifier of the challenge to solve to run this protected
+ // operation.
+ challenge_id: number;
+ }
+
+ export interface ChallengeSolve {
+ // The TAN code that solves $CHALLENGE_ID
+ tan: string;
+ }
+
+ export enum TanChannel {
+ SMS = "sms",
+ EMAIL = "email",
+ }
+}
+
+export namespace TalerExchangeApi {
+ export enum AmlState {
+ normal = 0,
+ pending = 1,
+ frozen = 2,
+ }
+
+ export interface AmlRecords {
+ // Array of AML records matching the query.
+ records: AmlRecord[];
+ }
+ export interface AmlRecord {
+ // Which payto-address is this record about.
+ // Identifies a GNU Taler wallet or an affected bank account.
+ h_payto: PaytoHash;
+
+ // What is the current AML state.
+ current_state: AmlState;
+
+ // Monthly transaction threshold before a review will be triggered
+ threshold: AmountString;
+
+ // RowID of the record.
+ rowid: Integer;
+ }
+
+ export interface AmlDecisionDetails {
+ // Array of AML decisions made for this account. Possibly
+ // contains only the most recent decision if "history" was
+ // not set to 'true'.
+ aml_history: AmlDecisionDetail[];
+
+ // Array of KYC attributes obtained for this account.
+ kyc_attributes: KycDetail[];
+ }
+ export interface AmlDecisionDetail {
+ // What was the justification given?
+ justification: string;
+
+ // What is the new AML state.
+ new_state: Integer;
+
+ // When was this decision made?
+ decision_time: Timestamp;
+
+ // What is the new AML decision threshold (in monthly transaction volume)?
+ new_threshold: AmountString;
+
+ // Who made the decision?
+ decider_pub: AmlOfficerPublicKeyP;
+ }
+ export interface KycDetail {
+ // Name of the configuration section that specifies the provider
+ // which was used to collect the KYC details
+ provider_section: string;
+
+ // The collected KYC data. NULL if the attribute data could not
+ // be decrypted (internal error of the exchange, likely the
+ // attribute key was changed).
+ attributes?: Object;
+
+ // Time when the KYC data was collected
+ collection_time: Timestamp;
+
+ // Time when the validity of the KYC data will expire
+ expiration_time: Timestamp;
+ }
+
+ export interface AmlDecision {
+ // Human-readable justification for the decision.
+ justification: string;
+
+ // At what monthly transaction volume should the
+ // decision be automatically reviewed?
+ new_threshold: AmountString;
+
+ // Which payto-address is the decision about?
+ // Identifies a GNU Taler wallet or an affected bank account.
+ h_payto: PaytoHash;
+
+ // What is the new AML state (e.g. frozen, unfrozen, etc.)
+ // Numerical values are defined in AmlDecisionState.
+ new_state: Integer;
+
+ // Signature by the AML officer over a
+ // TALER_MasterAmlOfficerStatusPS.
+ // Must have purpose TALER_SIGNATURE_MASTER_AML_KEY.
+ officer_sig: EddsaSignature;
+
+ // When was the decision made?
+ decision_time: Timestamp;
+
+ // Optional argument to impose new KYC requirements
+ // that the customer has to satisfy to unblock transactions.
+ kyc_requirements?: string[];
+ }
+
+ export interface ExchangeVersionResponse {
+ // libtool-style representation of the Exchange protocol version, see
+ // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning
+ // The format is "current:revision:age".
+ version: string;
+
+ // Name of the protocol.
+ name: "taler-exchange";
+
+ // URN of the implementation (needed to interpret 'revision' in version).
+ // @since v18, may become mandatory in the future.
+ implementation?: string;
+
+ // Currency supported by this exchange, given
+ // as a currency code ("USD" or "EUR").
+ currency: string;
+
+ // How wallets should render this currency.
+ currency_specification: CurrencySpecification;
+
+ // Names of supported KYC requirements.
+ supported_kyc_requirements: string[];
+ }
+
+ export type AccountRestriction =
+ | RegexAccountRestriction
+ | DenyAllAccountRestriction;
+ // Account restriction that disables this type of
+ // account for the indicated operation categorically.
+ export interface DenyAllAccountRestriction {
+ type: "deny";
+ }
+ // Accounts interacting with this type of account
+ // restriction must have a payto://-URI matching
+ // the given regex.
+ export interface RegexAccountRestriction {
+ type: "regex";
+
+ // Regular expression that the payto://-URI of the
+ // partner account must follow. The regular expression
+ // should follow posix-egrep, but without support for character
+ // classes, GNU extensions, back-references or intervals. See
+ // https://www.gnu.org/software/findutils/manual/html_node/find_html/posix_002degrep-regular-expression-syntax.html
+ // for a description of the posix-egrep syntax. Applications
+ // may support regexes with additional features, but exchanges
+ // must not use such regexes.
+ payto_regex: string;
+
+ // Hint for a human to understand the restriction
+ // (that is hopefully easier to comprehend than the regex itself).
+ human_hint: string;
+
+ // Map from IETF BCP 47 language tags to localized
+ // human hints.
+ human_hint_i18n?: { [lang_tag: string]: string };
+ }
+
+ export interface WireAccount {
+ // payto:// URI identifying the account and wire method
+ payto_uri: PaytoString;
+
+ // URI to convert amounts from or to the currency used by
+ // this wire account of the exchange. Missing if no
+ // conversion is applicable.
+ conversion_url?: string;
+
+ // Restrictions that apply to bank accounts that would send
+ // funds to the exchange (crediting this exchange bank account).
+ // Optional, empty array for unrestricted.
+ credit_restrictions: AccountRestriction[];
+
+ // Restrictions that apply to bank accounts that would receive
+ // funds from the exchange (debiting this exchange bank account).
+ // Optional, empty array for unrestricted.
+ debit_restrictions: AccountRestriction[];
+
+ // Signature using the exchange's offline key over
+ // a TALER_MasterWireDetailsPS
+ // with purpose TALER_SIGNATURE_MASTER_WIRE_DETAILS.
+ master_sig: EddsaSignature;
+ }
+
+ export interface ExchangeKeysResponse {
+ // libtool-style representation of the Exchange protocol version, see
+ // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning
+ // The format is "current:revision:age".
+ version: string;
+
+ // The exchange's base URL.
+ base_url: string;
+
+ // The exchange's currency or asset unit.
+ currency: string;
+
+ /**
+ * FIXME: PARTIALLY IMPLEMENTED!!
+ */
+
+ // How wallets should render this currency.
+ // currency_specification: CurrencySpecification;
+
+ // // Absolute cost offset for the STEFAN curve used
+ // // to (over) approximate fees payable by amount.
+ // stefan_abs: AmountString;
+
+ // // Factor to multiply the logarithm of the amount
+ // // with to (over) approximate fees payable by amount.
+ // // Note that the total to be paid is first to be
+ // // divided by the smallest denomination to obtain
+ // // the value that the logarithm is to be taken of.
+ // stefan_log: AmountString;
+
+ // // Linear cost factor for the STEFAN curve used
+ // // to (over) approximate fees payable by amount.
+ // //
+ // // Note that this is a scalar, as it is multiplied
+ // // with the actual amount.
+ // stefan_lin: Float;
+
+ // // Type of the asset. "fiat", "crypto", "regional"
+ // // or "stock". Wallets should adjust their UI/UX
+ // // based on this value.
+ // asset_type: string;
+
+ // // Array of wire accounts operated by the exchange for
+ // // incoming wire transfers.
+ // accounts: WireAccount[];
+
+ // // Object mapping names of wire methods (i.e. "iban" or "x-taler-bank")
+ // // to wire fees.
+ // wire_fees: { method: AggregateTransferFee[] };
+
+ // // List of exchanges that this exchange is partnering
+ // // with to enable wallet-to-wallet transfers.
+ // wads: ExchangePartner[];
+
+ // // Set to true if this exchange allows the use
+ // // of reserves for rewards.
+ // // @deprecated in protocol v18.
+ // rewards_allowed: false;
+
+ // // EdDSA master public key of the exchange, used to sign entries
+ // // in denoms and signkeys.
+ // master_public_key: EddsaPublicKey;
+
+ // // Relative duration until inactive reserves are closed;
+ // // not signed (!), can change without notice.
+ // reserve_closing_delay: RelativeTime;
+
+ // // Threshold amounts beyond which wallet should
+ // // trigger the KYC process of the issuing
+ // // exchange. Optional option, if not given there is no limit.
+ // // Currency must match currency.
+ // wallet_balance_limit_without_kyc?: AmountString[];
+
+ // // Denominations offered by this exchange
+ // denominations: DenomGroup[];
+
+ // // Compact EdDSA signature (binary-only) over the
+ // // contatentation of all of the master_sigs (in reverse
+ // // chronological order by group) in the arrays under
+ // // "denominations". Signature of TALER_ExchangeKeySetPS
+ // exchange_sig: EddsaSignature;
+
+ // // Public EdDSA key of the exchange that was used to generate the signature.
+ // // Should match one of the exchange's signing keys from signkeys. It is given
+ // // explicitly as the client might otherwise be confused by clock skew as to
+ // // which signing key was used for the exchange_sig.
+ // exchange_pub: EddsaPublicKey;
+
+ // // Denominations for which the exchange currently offers/requests recoup.
+ // recoup: Recoup[];
+
+ // // Array of globally applicable fees by time range.
+ // global_fees: GlobalFees[];
+
+ // // The date when the denomination keys were last updated.
+ // list_issue_date: Timestamp;
+
+ // // Auditors of the exchange.
+ // auditors: AuditorKeys[];
+
+ // // The exchange's signing keys.
+ // signkeys: SignKey[];
+
+ // // Optional field with a dictionary of (name, object) pairs defining the
+ // // supported and enabled extensions, such as age_restriction.
+ // extensions?: { name: ExtensionManifest };
+
+ // // Signature by the exchange master key of the SHA-256 hash of the
+ // // normalized JSON-object of field extensions, if it was set.
+ // // The signature has purpose TALER_SIGNATURE_MASTER_EXTENSIONS.
+ // extensions_sig?: EddsaSignature;
+ }
+
+ interface ExtensionManifest {
+ // The criticality of the extension MUST be provided. It has the same
+ // semantics as "critical" has for extensions in X.509:
+ // - if "true", the client must "understand" the extension before
+ // proceeding,
+ // - if "false", clients can safely skip extensions they do not
+ // understand.
+ // (see https://datatracker.ietf.org/doc/html/rfc5280#section-4.2)
+ critical: boolean;
+
+ // The version information MUST be provided in Taler's protocol version
+ // ranges notation, see
+ // https://docs.taler.net/core/api-common.html#protocol-version-ranges
+ version: LibtoolVersion;
+
+ // Optional configuration object, defined by the feature itself
+ config?: object;
+ }
+
+ interface SignKey {
+ // The actual exchange's EdDSA signing public key.
+ key: EddsaPublicKey;
+
+ // Initial validity date for the signing key.
+ stamp_start: Timestamp;
+
+ // Date when the exchange will stop using the signing key, allowed to overlap
+ // slightly with the next signing key's validity to allow for clock skew.
+ stamp_expire: Timestamp;
+
+ // Date when all signatures made by the signing key expire and should
+ // henceforth no longer be considered valid in legal disputes.
+ stamp_end: Timestamp;
+
+ // Signature over key and stamp_expire by the exchange master key.
+ // Signature of TALER_ExchangeSigningKeyValidityPS.
+ // Must have purpose TALER_SIGNATURE_MASTER_SIGNING_KEY_VALIDITY.
+ master_sig: EddsaSignature;
+ }
+
+ interface AuditorKeys {
+ // The auditor's EdDSA signing public key.
+ auditor_pub: EddsaPublicKey;
+
+ // The auditor's URL.
+ auditor_url: string;
+
+ // The auditor's name (for humans).
+ auditor_name: string;
+
+ // An array of denomination keys the auditor affirms with its signature.
+ // Note that the message only includes the hash of the public key, while the
+ // signature is actually over the expanded information including expiration
+ // times and fees. The exact format is described below.
+ denomination_keys: AuditorDenominationKey[];
+ }
+ interface AuditorDenominationKey {
+ // Hash of the public RSA key used to sign coins of the respective
+ // denomination. Note that the auditor's signature covers more than just
+ // the hash, but this other information is already provided in denoms and
+ // thus not repeated here.
+ denom_pub_h: HashCode;
+
+ // Signature of TALER_ExchangeKeyValidityPS.
+ auditor_sig: EddsaSignature;
+ }
+
+ interface GlobalFees {
+ // What date (inclusive) does these fees go into effect?
+ start_date: Timestamp;
+
+ // What date (exclusive) does this fees stop going into effect?
+ end_date: Timestamp;
+
+ // Account history fee, charged when a user wants to
+ // obtain a reserve/account history.
+ history_fee: AmountString;
+
+ // Annual fee charged for having an open account at the
+ // exchange. Charged to the account. If the account
+ // balance is insufficient to cover this fee, the account
+ // is automatically deleted/closed. (Note that the exchange
+ // will keep the account history around for longer for
+ // regulatory reasons.)
+ account_fee: AmountString;
+
+ // Purse fee, charged only if a purse is abandoned
+ // and was not covered by the account limit.
+ purse_fee: AmountString;
+
+ // How long will the exchange preserve the account history?
+ // After an account was deleted/closed, the exchange will
+ // retain the account history for legal reasons until this time.
+ history_expiration: RelativeTime;
+
+ // Non-negative number of concurrent purses that any
+ // account holder is allowed to create without having
+ // to pay the purse_fee.
+ purse_account_limit: Integer;
+
+ // How long does an exchange keep a purse around after a purse
+ // has expired (or been successfully merged)? A 'GET' request
+ // for a purse will succeed until the purse expiration time
+ // plus this value.
+ purse_timeout: RelativeTime;
+
+ // Signature of TALER_GlobalFeesPS.
+ master_sig: EddsaSignature;
+ }
+
+ interface Recoup {
+ // Hash of the public key of the denomination that is being revoked under
+ // emergency protocol (see /recoup).
+ h_denom_pub: HashCode;
+
+ // We do not include any signature here, as the primary use-case for
+ // this emergency involves the exchange having lost its signing keys,
+ // so such a signature here would be pretty worthless. However, the
+ // exchange will not honor /recoup requests unless they are for
+ // denomination keys listed here.
+ }
+
+ interface AggregateTransferFee {
+ // Per transfer wire transfer fee.
+ wire_fee: AmountString;
+
+ // Per transfer closing fee.
+ closing_fee: AmountString;
+
+ // What date (inclusive) does this fee go into effect?
+ // The different fees must cover the full time period in which
+ // any of the denomination keys are valid without overlap.
+ start_date: Timestamp;
+
+ // What date (exclusive) does this fee stop going into effect?
+ // The different fees must cover the full time period in which
+ // any of the denomination keys are valid without overlap.
+ end_date: Timestamp;
+
+ // Signature of TALER_MasterWireFeePS with
+ // purpose TALER_SIGNATURE_MASTER_WIRE_FEES.
+ sig: EddsaSignature;
+ }
+
+ interface ExchangePartner {
+ // Base URL of the partner exchange.
+ partner_base_url: string;
+
+ // Public master key of the partner exchange.
+ partner_master_pub: EddsaPublicKey;
+
+ // Per exchange-to-exchange transfer (wad) fee.
+ wad_fee: AmountString;
+
+ // Exchange-to-exchange wad (wire) transfer frequency.
+ wad_frequency: RelativeTime;
+
+ // When did this partnership begin (under these conditions)?
+ start_date: Timestamp;
+
+ // How long is this partnership expected to last?
+ end_date: Timestamp;
+
+ // Signature using the exchange's offline key over
+ // TALER_WadPartnerSignaturePS
+ // with purpose TALER_SIGNATURE_MASTER_PARTNER_DETAILS.
+ master_sig: EddsaSignature;
+ }
+
+ type DenomGroup =
+ | DenomGroupRsa
+ | DenomGroupCs
+ | DenomGroupRsaAgeRestricted
+ | DenomGroupCsAgeRestricted;
+ interface DenomGroupRsa extends DenomGroupCommon {
+ cipher: "RSA";
+
+ denoms: ({
+ rsa_pub: RsaPublicKey;
+ } & DenomCommon)[];
+ }
+ interface DenomGroupCs extends DenomGroupCommon {
+ cipher: "CS";
+
+ denoms: ({
+ cs_pub: Cs25519Point;
+ } & DenomCommon)[];
+ }
+
+ // Binary representation of the age groups.
+ // The bits set in the mask mark the edges at the beginning of a next age
+ // group. F.e. for the age groups
+ // 0-7, 8-9, 10-11, 12-13, 14-15, 16-17, 18-21, 21-*
+ // the following bits are set:
+ //
+ // 31 24 16 8 0
+ // | | | | |
+ // oooooooo oo1oo1o1 o1o1o1o1 ooooooo1
+ //
+ // A value of 0 means that the exchange does not support the extension for
+ // age-restriction.
+ type AgeMask = Integer;
+
+ interface DenomGroupRsaAgeRestricted extends DenomGroupCommon {
+ cipher: "RSA+age_restricted";
+ age_mask: AgeMask;
+
+ denoms: ({
+ rsa_pub: RsaPublicKey;
+ } & DenomCommon)[];
+ }
+ interface DenomGroupCsAgeRestricted extends DenomGroupCommon {
+ cipher: "CS+age_restricted";
+ age_mask: AgeMask;
+
+ denoms: ({
+ cs_pub: Cs25519Point;
+ } & DenomCommon)[];
+ }
+ // Common attributes for all denomination groups
+ interface DenomGroupCommon {
+ // How much are coins of this denomination worth?
+ value: AmountString;
+
+ // Fee charged by the exchange for withdrawing a coin of this denomination.
+ fee_withdraw: AmountString;
+
+ // Fee charged by the exchange for depositing a coin of this denomination.
+ fee_deposit: AmountString;
+
+ // Fee charged by the exchange for refreshing a coin of this denomination.
+ fee_refresh: AmountString;
+
+ // Fee charged by the exchange for refunding a coin of this denomination.
+ fee_refund: AmountString;
+ }
+ interface DenomCommon {
+ // Signature of TALER_DenominationKeyValidityPS.
+ master_sig: EddsaSignature;
+
+ // When does the denomination key become valid?
+ stamp_start: Timestamp;
+
+ // When is it no longer possible to withdraw coins
+ // of this denomination?
+ stamp_expire_withdraw: Timestamp;
+
+ // When is it no longer possible to deposit coins
+ // of this denomination?
+ stamp_expire_deposit: Timestamp;
+
+ // Timestamp indicating by when legal disputes relating to these coins must
+ // be settled, as the exchange will afterwards destroy its evidence relating to
+ // transactions involving this coin.
+ stamp_expire_legal: Timestamp;
+
+ // Set to 'true' if the exchange somehow "lost"
+ // the private key. The denomination was not
+ // necessarily revoked, but still cannot be used
+ // to withdraw coins at this time (theoretically,
+ // the private key could be recovered in the
+ // future; coins signed with the private key
+ // remain valid).
+ lost?: boolean;
+ }
+ type DenominationKey = RsaDenominationKey | CSDenominationKey;
+ interface RsaDenominationKey {
+ cipher: "RSA";
+
+ // 32-bit age mask.
+ age_mask: Integer;
+
+ // RSA public key
+ rsa_public_key: RsaPublicKey;
+ }
+ interface CSDenominationKey {
+ cipher: "CS";
+
+ // 32-bit age mask.
+ age_mask: Integer;
+
+ // Public key of the denomination.
+ cs_public_key: Cs25519Point;
+ }
+}
+
+export namespace TalerMerchantApi {
+ export interface VersionResponse {
+ // libtool-style representation of the Merchant protocol version, see
+ // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning
+ // The format is "current:revision:age".
+ version: string;
+
+ // Name of the protocol.
+ name: "taler-merchant";
+
+ // URN of the implementation (needed to interpret 'revision' in version).
+ // @since **v8**, may become mandatory in the future.
+ implementation?: string;
+
+ // Default (!) currency supported by this backend.
+ // This is the currency that the backend should
+ // suggest by default to the user when entering
+ // amounts. See currencies for a list of
+ // supported currencies and how to render them.
+ currency: string;
+
+ // How services should render currencies supported
+ // by this backend. Maps
+ // currency codes (e.g. "EUR" or "KUDOS") to
+ // the respective currency specification.
+ // All currencies in this map are supported by
+ // the backend. Note that the actual currency
+ // specifications are a *hint* for applications
+ // that would like *advice* on how to render amounts.
+ // Applications *may* ignore the currency specification
+ // if they know how to render currencies that they are
+ // used with.
+ currencies: { [currency: string]: CurrencySpecification };
+
+ // Array of exchanges trusted by the merchant.
+ // Since protocol **v6**.
+ exchanges: ExchangeConfigInfo[];
+ }
+
+ export interface ExchangeConfigInfo {
+ // Base URL of the exchange REST API.
+ base_url: string;
+
+ // Currency for which the merchant is configured
+ // to trust the exchange.
+ // May not be the one the exchange actually uses,
+ // but is the only one we would trust this exchange for.
+ currency: string;
+
+ // Offline master public key of the exchange. The
+ // /keys data must be signed with this public
+ // key for us to trust it.
+ master_pub: EddsaPublicKey;
+ }
+ export interface ClaimRequest {
+ // Nonce to identify the wallet that claimed the order.
+ nonce: string;
+
+ // Token that authorizes the wallet to claim the order.
+ // *Optional* as the merchant may not have required it
+ // (create_token set to false in PostOrderRequest).
+ token?: ClaimToken;
+ }
+
+ export interface ClaimResponse {
+ // Contract terms of the claimed order
+ contract_terms: ContractTerms;
+
+ // Signature by the merchant over the contract terms.
+ sig: EddsaSignature;
+ }
+
+ export interface PaymentResponse {
+ // Signature on TALER_PaymentResponsePS with the public
+ // key of the merchant instance.
+ sig: EddsaSignature;
+
+ // Text to be shown to the point-of-sale staff as a proof of
+ // payment.
+ pos_confirmation?: string;
+ }
+
+ export interface PaymentStatusRequestParams {
+ // Hash of the order’s contract terms (this is used to
+ // authenticate the wallet/customer in case
+ // $ORDER_ID is guessable).
+ // Required once an order was claimed.
+ contractTermHash?: string;
+ // Authorizes the request via the claim token that
+ // was returned in the PostOrderResponse. Used with
+ // unclaimed orders only. Whether token authorization is
+ // required is determined by the merchant when the
+ // frontend creates the order.
+ claimToken?: string;
+ // Session ID that the payment must be bound to.
+ // If not specified, the payment is not session-bound.
+ sessionId?: string;
+ // If specified, the merchant backend will wait up to
+ // timeout_ms milliseconds for completion of the payment
+ // before sending the HTTP response. A client must never
+ // rely on this behavior, as the merchant backend may return
+ // a response immediately.
+ timeout?: number;
+ // If set to “yes”, poll for the order’s pending refunds
+ // to be picked up. timeout_ms specifies how long we
+ // will wait for the refund.
+ awaitRefundObtained?: boolean;
+ // Indicates that we are polling for a refund above the
+ // given AMOUNT. timeout_ms will specify how long we
+ // will wait for the refund.
+ refund?: AmountString;
+ // Since protocol v9 refunded orders are only returned
+ // under “already_paid_order_id” if this flag is set
+ // explicitly to “YES”.
+ allowRefundedForRepurchase?: boolean;
+ }
+ export interface GetKycStatusRequestParams {
+ // If specified, the KYC check should return
+ // the KYC status only for this wire account.
+ // Otherwise, for all wire accounts.
+ wireHash?: string;
+ // If specified, the KYC check should return
+ // the KYC status only for the given exchange.
+ // Otherwise, for all exchanges we interacted with.
+ exchangeURL?: string;
+ // If specified, the merchant will wait up to
+ // timeout_ms milliseconds for the exchanges to
+ // confirm completion of the KYC process(es).
+ timeout?: number;
+ }
+ export interface GetOtpDeviceRequestParams {
+ // Timestamp in seconds to use when calculating
+ // the current OTP code of the device. Since protocol v10.
+ faketime?: number;
+ // Price to use when calculating the current OTP
+ // code of the device. Since protocol v10.
+ price?: AmountString;
+ }
+ export interface GetOrderRequestParams {
+ // Session ID that the payment must be bound to.
+ // If not specified, the payment is not session-bound.
+ sessionId?: string;
+ // Timeout in milliseconds to wait for a payment if
+ // the answer would otherwise be negative (long polling).
+ timeout?: number;
+ // Since protocol v9 refunded orders are only returned
+ // under “already_paid_order_id” if this flag is set
+ // explicitly to “YES”.
+ allowRefundedForRepurchase?: boolean;
+ }
+ export interface ListWireTransferRequestParams {
+ // Filter for transfers to the given bank account
+ // (subject and amount MUST NOT be given in the payto URI).
+ paytoURI?: string;
+ // Filter for transfers executed before the given timestamp.
+ before?: number;
+ // Filter for transfers executed after the given timestamp.
+ after?: number;
+ // At most return the given number of results. Negative for
+ // descending in execution time, positive for ascending in
+ // execution time. Default is -20.
+ limit?: number;
+ // Starting transfer_serial_id for an iteration.
+ offset?: string;
+ // Filter transfers by verification status.
+ verified?: boolean;
+ order?: "asc" | "dec";
+ }
+ export interface ListOrdersRequestParams {
+ // If set to yes, only return paid orders, if no only
+ // unpaid orders. Do not give (or use “all”) to see all
+ // orders regardless of payment status.
+ paid?: boolean;
+ // If set to yes, only return refunded orders, if no only
+ // unrefunded orders. Do not give (or use “all”) to see
+ // all orders regardless of refund status.
+ refunded?: boolean;
+ // If set to yes, only return wired orders, if no only
+ // orders with missing wire transfers. Do not give (or
+ // use “all”) to see all orders regardless of wire transfer
+ // status.
+ wired?: boolean;
+ // At most return the given number of results. Negative
+ // for descending by row ID, positive for ascending by
+ // row ID. Default is 20. Since protocol v12.
+ limit?: number;
+ // Non-negative date in seconds after the UNIX Epoc, see delta
+ // for its interpretation. If not specified, we default to the
+ // oldest or most recent entry, depending on delta.
+ date?: AbsoluteTime;
+ // Starting product_serial_id for an iteration.
+ // Since protocol v12.
+ offset?: string;
+ // Timeout in milliseconds to wait for additional orders if the
+ // answer would otherwise be negative (long polling). Only useful
+ // if delta is positive. Note that the merchant MAY still return
+ // a response that contains fewer than delta orders.
+ timeout?: number;
+ // Since protocol v6. Filters by session ID.
+ sessionId?: string;
+ // Since protocol v6. Filters by fulfillment URL.
+ fulfillmentUrl?: string;
+
+ order?: "asc" | "dec";
+ }
+
+ export interface PayRequest {
+ // The coins used to make the payment.
+ coins: CoinPaySig[];
+
+ // Custom inputs from the wallet for the contract.
+ wallet_data?: Object;
+
+ // The session for which the payment is made (or replayed).
+ // Only set for session-based payments.
+ session_id?: string;
+ }
+ export interface CoinPaySig {
+ // Signature by the coin.
+ coin_sig: EddsaSignature;
+
+ // Public key of the coin being spent.
+ coin_pub: EddsaPublicKey;
+
+ // Signature made by the denomination public key.
+ ub_sig: RsaSignature;
+
+ // The hash of the denomination public key associated with this coin.
+ h_denom: HashCode;
+
+ // The amount that is subtracted from this coin with this payment.
+ contribution: AmountString;
+
+ // URL of the exchange this coin was withdrawn from.
+ exchange_url: string;
+ }
+
+ export interface StatusPaid {
+ type: "paid";
+
+ // Was the payment refunded (even partially, via refund or abort)?
+ refunded: boolean;
+
+ // Is any amount of the refund still waiting to be picked up (even partially)?
+ refund_pending: boolean;
+
+ // Amount that was refunded in total.
+ refund_amount: AmountString;
+
+ // Amount that already taken by the wallet.
+ refund_taken: AmountString;
+ }
+ export interface StatusGotoResponse {
+ type: "goto";
+ // The client should go to the reorder URL, there a fresh
+ // order might be created as this one is taken by another
+ // customer or wallet (or repurchase detection logic may
+ // apply).
+ public_reorder_url: string;
+ }
+ export interface StatusUnpaidResponse {
+ type: "unpaid";
+ // URI that the wallet must process to complete the payment.
+ taler_pay_uri: string;
+
+ // Status URL, can be used as a redirect target for the browser
+ // to show the order QR code / trigger the wallet.
+ fulfillment_url?: string;
+
+ // Alternative order ID which was paid for already in the same session.
+ // Only given if the same product was purchased before in the same session.
+ already_paid_order_id?: string;
+ }
+
+ export interface PaidRefundStatusResponse {
+ // Text to be shown to the point-of-sale staff as a proof of
+ // payment (present only if reusable OTP algorithm is used).
+ pos_confirmation?: string;
+
+ // True if the order has been subjected to
+ // refunds. False if it was simply paid.
+ refunded: boolean;
+ }
+ export interface PaidRequest {
+ // Signature on TALER_PaymentResponsePS with the public
+ // key of the merchant instance.
+ sig: EddsaSignature;
+
+ // Hash of the order's contract terms (this is used to authenticate the
+ // wallet/customer and to enable signature verification without
+ // database access).
+ h_contract: HashCode;
+
+ // Hash over custom inputs from the wallet for the contract.
+ wallet_data_hash?: HashCode;
+
+ // Session id for which the payment is proven.
+ session_id: string;
+ }
+
+ export interface AbortRequest {
+ // Hash of the order's contract terms (this is used to authenticate the
+ // wallet/customer in case $ORDER_ID is guessable).
+ h_contract: HashCode;
+
+ // List of coins the wallet would like to see refunds for.
+ // (Should be limited to the coins for which the original
+ // payment succeeded, as far as the wallet knows.)
+ coins: AbortingCoin[];
+ }
+ interface AbortingCoin {
+ // Public key of a coin for which the wallet is requesting an abort-related refund.
+ coin_pub: EddsaPublicKey;
+
+ // The amount to be refunded (matches the original contribution)
+ contribution: AmountString;
+
+ // URL of the exchange this coin was withdrawn from.
+ exchange_url: string;
+ }
+ export interface AbortResponse {
+ // List of refund responses about the coins that the wallet
+ // requested an abort for. In the same order as the coins
+ // from the original request.
+ // The rtransaction_id is implied to be 0.
+ refunds: MerchantAbortPayRefundStatus[];
+ }
+ export type MerchantAbortPayRefundStatus =
+ | MerchantAbortPayRefundSuccessStatus
+ | MerchantAbortPayRefundFailureStatus;
+ // Details about why a refund failed.
+ export interface MerchantAbortPayRefundFailureStatus {
+ // Used as tag for the sum type RefundStatus sum type.
+ type: "failure";
+
+ // HTTP status of the exchange request, must NOT be 200.
+ exchange_status: Integer;
+
+ // Taler error code from the exchange reply, if available.
+ exchange_code?: Integer;
+
+ // If available, HTTP reply from the exchange.
+ exchange_reply?: Object;
+ }
+ // Additional details needed to verify the refund confirmation signature
+ // (h_contract_terms and merchant_pub) are already known
+ // to the wallet and thus not included.
+ export interface MerchantAbortPayRefundSuccessStatus {
+ // Used as tag for the sum type MerchantCoinRefundStatus sum type.
+ type: "success";
+
+ // HTTP status of the exchange request, 200 (integer) required for refund confirmations.
+ exchange_status: 200;
+
+ // The EdDSA :ref:signature (binary-only) with purpose
+ // TALER_SIGNATURE_EXCHANGE_CONFIRM_REFUND using a current signing key of the
+ // exchange affirming the successful refund.
+ exchange_sig: EddsaSignature;
+
+ // Public EdDSA key of the exchange that was used to generate the signature.
+ // Should match one of the exchange's signing keys from /keys. It is given
+ // explicitly as the client might otherwise be confused by clock skew as to
+ // which signing key was used.
+ exchange_pub: EddsaPublicKey;
+ }
+
+ export interface WalletRefundRequest {
+ // Hash of the order's contract terms (this is used to authenticate the
+ // wallet/customer).
+ h_contract: HashCode;
+ }
+ export interface WalletRefundResponse {
+ // Amount that was refunded in total.
+ refund_amount: AmountString;
+
+ // Successful refunds for this payment, empty array for none.
+ refunds: MerchantCoinRefundStatus[];
+
+ // Public key of the merchant.
+ merchant_pub: EddsaPublicKey;
+ }
+ export type MerchantCoinRefundStatus =
+ | MerchantCoinRefundSuccessStatus
+ | MerchantCoinRefundFailureStatus;
+ // Details about why a refund failed.
+ export interface MerchantCoinRefundFailureStatus {
+ // Used as tag for the sum type RefundStatus sum type.
+ type: "failure";
+
+ // HTTP status of the exchange request, must NOT be 200.
+ exchange_status: Integer;
+
+ // Taler error code from the exchange reply, if available.
+ exchange_code?: Integer;
+
+ // If available, HTTP reply from the exchange.
+ exchange_reply?: Object;
+
+ // Refund transaction ID.
+ rtransaction_id: Integer;
+
+ // Public key of a coin that was refunded.
+ coin_pub: EddsaPublicKey;
+
+ // Amount that was refunded, including refund fee charged by the exchange
+ // to the customer.
+ refund_amount: AmountString;
+
+ // Timestamp when the merchant approved the refund.
+ // Useful for grouping refunds.
+ execution_time: Timestamp;
+ }
+ // Additional details needed to verify the refund confirmation signature
+ // (h_contract_terms and merchant_pub) are already known
+ // to the wallet and thus not included.
+ export interface MerchantCoinRefundSuccessStatus {
+ // Used as tag for the sum type MerchantCoinRefundStatus sum type.
+ type: "success";
+
+ // HTTP status of the exchange request, 200 (integer) required for refund confirmations.
+ exchange_status: 200;
+
+ // The EdDSA :ref:signature (binary-only) with purpose
+ // TALER_SIGNATURE_EXCHANGE_CONFIRM_REFUND using a current signing key of the
+ // exchange affirming the successful refund.
+ exchange_sig: EddsaSignature;
+
+ // Public EdDSA key of the exchange that was used to generate the signature.
+ // Should match one of the exchange's signing keys from /keys. It is given
+ // explicitly as the client might otherwise be confused by clock skew as to
+ // which signing key was used.
+ exchange_pub: EddsaPublicKey;
+
+ // Refund transaction ID.
+ rtransaction_id: Integer;
+
+ // Public key of a coin that was refunded.
+ coin_pub: EddsaPublicKey;
+
+ // Amount that was refunded, including refund fee charged by the exchange
+ // to the customer.
+ refund_amount: AmountString;
+
+ // Timestamp when the merchant approved the refund.
+ // Useful for grouping refunds.
+ execution_time: Timestamp;
+ }
+
+ interface RewardInformation {
+ // Exchange from which the reward will be withdrawn. Needed by the
+ // wallet to determine denominations, fees, etc.
+ exchange_url: string;
+
+ // URL where to go after obtaining the reward.
+ next_url: string;
+
+ // (Remaining) amount of the reward (including fees).
+ reward_amount: AmountString;
+
+ // Timestamp indicating when the reward is set to expire (may be in the past).
+ // Note that rewards that have expired MAY also result in a 404 response.
+ expiration: Timestamp;
+ }
+
+ interface RewardPickupRequest {
+ // List of planchets the wallet wants to use for the reward.
+ planchets: PlanchetDetail[];
+ }
+ interface PlanchetDetail {
+ // Hash of the denomination's public key (hashed to reduce
+ // bandwidth consumption).
+ denom_pub_hash: HashCode;
+
+ // Coin's blinded public key.
+ coin_ev: CoinEnvelope;
+ }
+ interface RewardResponse {
+ // Blind RSA signatures over the planchets.
+ // The order of the signatures matches the planchets list.
+ blind_sigs: BlindSignature[];
+ }
+ interface BlindSignature {
+ // The (blind) RSA signature. Still needs to be unblinded.
+ blind_sig: BlindedRsaSignature;
+ }
+
+ export interface InstanceConfigurationMessage {
+ // Name of the merchant instance to create (will become $INSTANCE).
+ // Must match the regex ^[A-Za-z0-9][A-Za-z0-9_.@-]+$.
+ id: string;
+
+ // Merchant name corresponding to this instance.
+ name: string;
+
+ // Type of the user (business or individual).
+ // Defaults to 'business'. Should become mandatory field
+ // in the future, left as optional for API compatibility for now.
+ user_type?: string;
+
+ // Merchant email for customer contact.
+ email?: string;
+
+ // Merchant public website.
+ website?: string;
+
+ // Merchant logo.
+ logo?: ImageDataUrl;
+
+ // Authentication settings for this instance
+ auth: InstanceAuthConfigurationMessage;
+
+ // The merchant's physical address (to be put into contracts).
+ address: Location;
+
+ // The jurisdiction under which the merchant conducts its business
+ // (to be put into contracts).
+ jurisdiction: Location;
+
+ // Use STEFAN curves to determine default fees?
+ // If false, no fees are allowed by default.
+ // Can always be overridden by the frontend on a per-order basis.
+ use_stefan: boolean;
+
+ // If the frontend does NOT specify an execution date, how long should
+ // we tell the exchange to wait to aggregate transactions before
+ // executing the wire transfer? This delay is added to the current
+ // time when we generate the advisory execution time for the exchange.
+ default_wire_transfer_delay: RelativeTime;
+
+ // If the frontend does NOT specify a payment deadline, how long should
+ // offers we make be valid by default?
+ default_pay_delay: RelativeTime;
+ }
+
+ export interface InstanceAuthConfigurationMessage {
+ // Type of authentication.
+ // "external": The mechant backend does not do
+ // any authentication checks. Instead an API
+ // gateway must do the authentication.
+ // "token": The merchant checks an auth token.
+ // See "token" for details.
+ method: "external" | "token";
+
+ // For method "token", this field is mandatory.
+ // The token MUST begin with the string "secret-token:".
+ // After the auth token has been set (with method "token"),
+ // the value must be provided in a "Authorization: Bearer $token"
+ // header.
+ token?: AccessToken;
+ }
+
+ export interface InstanceReconfigurationMessage {
+ // Merchant name corresponding to this instance.
+ name: string;
+
+ // Type of the user (business or individual).
+ // Defaults to 'business'. Should become mandatory field
+ // in the future, left as optional for API compatibility for now.
+ user_type?: string;
+
+ // Merchant email for customer contact.
+ email?: string;
+
+ // Merchant public website.
+ website?: string;
+
+ // Merchant logo.
+ logo?: ImageDataUrl;
+
+ // The merchant's physical address (to be put into contracts).
+ address: Location;
+
+ // The jurisdiction under which the merchant conducts its business
+ // (to be put into contracts).
+ jurisdiction: Location;
+
+ // Use STEFAN curves to determine default fees?
+ // If false, no fees are allowed by default.
+ // Can always be overridden by the frontend on a per-order basis.
+ use_stefan: boolean;
+
+ // If the frontend does NOT specify an execution date, how long should
+ // we tell the exchange to wait to aggregate transactions before
+ // executing the wire transfer? This delay is added to the current
+ // time when we generate the advisory execution time for the exchange.
+ default_wire_transfer_delay: RelativeTime;
+
+ // If the frontend does NOT specify a payment deadline, how long should
+ // offers we make be valid by default?
+ default_pay_delay: RelativeTime;
+ }
+
+ export interface InstancesResponse {
+ // List of instances that are present in the backend (see Instance).
+ instances: Instance[];
+ }
+
+ export interface Instance {
+ // Merchant name corresponding to this instance.
+ name: string;
+
+ // Type of the user ("business" or "individual").
+ user_type: string;
+
+ // Merchant public website.
+ website?: string;
+
+ // Merchant logo.
+ logo?: ImageDataUrl;
+
+ // Merchant instance this response is about ($INSTANCE).
+ id: string;
+
+ // Public key of the merchant/instance, in Crockford Base32 encoding.
+ merchant_pub: EddsaPublicKey;
+
+ // List of the payment targets supported by this instance. Clients can
+ // specify the desired payment target in /order requests. Note that
+ // front-ends do not have to support wallets selecting payment targets.
+ payment_targets: string[];
+
+ // Has this instance been deleted (but not purged)?
+ deleted: boolean;
+ }
+
+ export interface QueryInstancesResponse {
+ // Merchant name corresponding to this instance.
+ name: string;
+
+ // Type of the user ("business" or "individual").
+ user_type: string;
+
+ // Merchant email for customer contact.
+ email?: string;
+
+ // Merchant public website.
+ website?: string;
+
+ // Merchant logo.
+ logo?: ImageDataUrl;
+
+ // Public key of the merchant/instance, in Crockford Base32 encoding.
+ merchant_pub: EddsaPublicKey;
+
+ // The merchant's physical address (to be put into contracts).
+ address: Location;
+
+ // The jurisdiction under which the merchant conducts its business
+ // (to be put into contracts).
+ jurisdiction: Location;
+
+ // Use STEFAN curves to determine default fees?
+ // If false, no fees are allowed by default.
+ // Can always be overridden by the frontend on a per-order basis.
+ use_stefan: boolean;
+
+ // If the frontend does NOT specify an execution date, how long should
+ // we tell the exchange to wait to aggregate transactions before
+ // executing the wire transfer? This delay is added to the current
+ // time when we generate the advisory execution time for the exchange.
+ default_wire_transfer_delay: RelativeTime;
+
+ // If the frontend does NOT specify a payment deadline, how long should
+ // offers we make be valid by default?
+ default_pay_delay: RelativeTime;
+
+ // Authentication configuration.
+ // Does not contain the token when token auth is configured.
+ auth: {
+ method: "external" | "token";
+ };
+ }
+
+ export interface AccountKycRedirects {
+ // Array of pending KYCs.
+ pending_kycs: MerchantAccountKycRedirect[];
+
+ // Array of exchanges with no reply.
+ timeout_kycs: ExchangeKycTimeout[];
+ }
+
+ export interface MerchantAccountKycRedirect {
+ // URL that the user should open in a browser to
+ // proceed with the KYC process (as returned
+ // by the exchange's /kyc-check/ endpoint).
+ // Optional, missing if the account is blocked
+ // due to AML and not due to KYC.
+ kyc_url?: string;
+
+ // AML status of the account.
+ aml_status: Integer;
+
+ // Base URL of the exchange this is about.
+ exchange_url: string;
+
+ // Our bank wire account this is about.
+ payto_uri: PaytoString;
+ }
+
+ export interface ExchangeKycTimeout {
+ // Base URL of the exchange this is about.
+ exchange_url: string;
+
+ // Numeric error code indicating errors the exchange
+ // returned, or TALER_EC_INVALID for none.
+ exchange_code: number;
+
+ // HTTP status code returned by the exchange when we asked for
+ // information about the KYC status.
+ // 0 if there was no response at all.
+ exchange_http_status: number;
+ }
+
+ export interface AccountAddDetails {
+ // payto:// URI of the account.
+ payto_uri: PaytoString;
+
+ // URL from where the merchant can download information
+ // about incoming wire transfers to this account.
+ credit_facade_url?: string;
+
+ // Credentials to use when accessing the credit facade.
+ // Never returned on a GET (as this may be somewhat
+ // sensitive data). Can be set in POST
+ // or PATCH requests to update (or delete) credentials.
+ // To really delete credentials, set them to the type: "none".
+ credit_facade_credentials?: FacadeCredentials;
+ }
+
+ export type FacadeCredentials =
+ | NoFacadeCredentials
+ | BasicAuthFacadeCredentials;
+ export interface NoFacadeCredentials {
+ type: "none";
+ }
+ export interface BasicAuthFacadeCredentials {
+ type: "basic";
+
+ // Username to use to authenticate
+ username: string;
+
+ // Password to use to authenticate
+ password: string;
+ }
+ export interface AccountAddResponse {
+ // Hash over the wire details (including over the salt).
+ h_wire: HashCode;
+
+ // Salt used to compute h_wire.
+ salt: HashCode;
+ }
+
+ export interface AccountPatchDetails {
+ // URL from where the merchant can download information
+ // about incoming wire transfers to this account.
+ credit_facade_url?: string;
+
+ // Credentials to use when accessing the credit facade.
+ // Never returned on a GET (as this may be somewhat
+ // sensitive data). Can be set in POST
+ // or PATCH requests to update (or delete) credentials.
+ // To really delete credentials, set them to the type: "none".
+ // If the argument is omitted, the old credentials
+ // are simply preserved.
+ credit_facade_credentials?: FacadeCredentials;
+ }
+
+ export interface AccountsSummaryResponse {
+ // List of accounts that are known for the instance.
+ accounts: BankAccountSummaryEntry[];
+ }
+
+ // TODO: missing in docs
+ export interface BankAccountSummaryEntry {
+ // payto:// URI of the account.
+ payto_uri: PaytoString;
+
+ // Hash over the wire details (including over the salt).
+ h_wire: HashCode;
+ }
+ export interface BankAccountEntry {
+ // payto:// URI of the account.
+ payto_uri: PaytoString;
+
+ // Hash over the wire details (including over the salt).
+ h_wire: HashCode;
+
+ // Salt used to compute h_wire.
+ salt: HashCode;
+
+ // URL from where the merchant can download information
+ // about incoming wire transfers to this account.
+ credit_facade_url?: string;
+
+ // true if this account is active,
+ // false if it is historic.
+ active?: boolean;
+ }
+
+ export interface ProductAddDetail {
+ // Product ID to use.
+ product_id: string;
+
+ // Human-readable product description.
+ description: string;
+
+ // Map from IETF BCP 47 language tags to localized descriptions.
+ description_i18n?: { [lang_tag: string]: string };
+
+ // Unit in which the product is measured (liters, kilograms, packages, etc.).
+ unit: string;
+
+ // The price for one unit of the product. Zero is used
+ // to imply that this product is not sold separately, or
+ // that the price is not fixed, and must be supplied by the
+ // front-end. If non-zero, this price MUST include applicable
+ // taxes.
+ price: AmountString;
+
+ // An optional base64-encoded product image.
+ image?: ImageDataUrl;
+
+ // A list of taxes paid by the merchant for one unit of this product.
+ taxes?: Tax[];
+
+ // Number of units of the product in stock in sum in total,
+ // including all existing sales ever. Given in product-specific
+ // units.
+ // A value of -1 indicates "infinite" (i.e. for "electronic" books).
+ total_stock: Integer;
+
+ // Identifies where the product is in stock.
+ address?: Location;
+
+ // Identifies when we expect the next restocking to happen.
+ next_restock?: Timestamp;
+
+ // Minimum age buyer must have (in years). Default is 0.
+ minimum_age?: Integer;
+ }
+
+ export interface ProductPatchDetail {
+ // Human-readable product description.
+ description: string;
+
+ // Map from IETF BCP 47 language tags to localized descriptions.
+ description_i18n?: { [lang_tag: string]: string };
+
+ // Unit in which the product is measured (liters, kilograms, packages, etc.).
+ unit: string;
+
+ // The price for one unit of the product. Zero is used
+ // to imply that this product is not sold separately, or
+ // that the price is not fixed, and must be supplied by the
+ // front-end. If non-zero, this price MUST include applicable
+ // taxes.
+ price: AmountString;
+
+ // An optional base64-encoded product image.
+ image?: ImageDataUrl;
+
+ // A list of taxes paid by the merchant for one unit of this product.
+ taxes?: Tax[];
+
+ // Number of units of the product in stock in sum in total,
+ // including all existing sales ever. Given in product-specific
+ // units.
+ // A value of -1 indicates "infinite" (i.e. for "electronic" books).
+ total_stock: Integer;
+
+ // Number of units of the product that were lost (spoiled, stolen, etc.).
+ total_lost?: Integer;
+
+ // Identifies where the product is in stock.
+ address?: Location;
+
+ // Identifies when we expect the next restocking to happen.
+ next_restock?: Timestamp;
+
+ // Minimum age buyer must have (in years). Default is 0.
+ minimum_age?: Integer;
+ }
+
+ export interface InventorySummaryResponse {
+ // List of products that are present in the inventory.
+ products: InventoryEntry[];
+ }
+
+ export interface InventoryEntry {
+ // Product identifier, as found in the product.
+ product_id: string;
+ // product_serial_id of the product in the database.
+ product_serial: Integer;
+ }
+
+ export interface ProductDetail {
+ // Human-readable product description.
+ description: string;
+
+ // Map from IETF BCP 47 language tags to localized descriptions.
+ description_i18n: { [lang_tag: string]: string };
+
+ // Unit in which the product is measured (liters, kilograms, packages, etc.).
+ unit: string;
+
+ // The price for one unit of the product. Zero is used
+ // to imply that this product is not sold separately, or
+ // that the price is not fixed, and must be supplied by the
+ // front-end. If non-zero, this price MUST include applicable
+ // taxes.
+ price: AmountString;
+
+ // An optional base64-encoded product image.
+ image: ImageDataUrl;
+
+ // A list of taxes paid by the merchant for one unit of this product.
+ taxes: Tax[];
+
+ // Number of units of the product in stock in sum in total,
+ // including all existing sales ever. Given in product-specific
+ // units.
+ // A value of -1 indicates "infinite" (i.e. for "electronic" books).
+ total_stock: Integer;
+
+ // Number of units of the product that have already been sold.
+ total_sold: Integer;
+
+ // Number of units of the product that were lost (spoiled, stolen, etc.).
+ total_lost: Integer;
+
+ // Identifies where the product is in stock.
+ address: Location;
+
+ // Identifies when we expect the next restocking to happen.
+ next_restock?: Timestamp;
+
+ // Minimum age buyer must have (in years).
+ minimum_age?: Integer;
+ }
+ export interface LockRequest {
+ // UUID that identifies the frontend performing the lock
+ // Must be unique for the lifetime of the lock.
+ lock_uuid: string;
+
+ // How long does the frontend intend to hold the lock?
+ duration: RelativeTime;
+
+ // How many units should be locked?
+ quantity: Integer;
+ }
+
+ export interface PostOrderRequest {
+ // The order must at least contain the minimal
+ // order detail, but can override all.
+ order: Order;
+
+ // If set, the backend will then set the refund deadline to the current
+ // time plus the specified delay. If it's not set, refunds will not be
+ // possible.
+ refund_delay?: RelativeTime;
+
+ // Specifies the payment target preferred by the client. Can be used
+ // to select among the various (active) wire methods supported by the instance.
+ payment_target?: string;
+
+ // Specifies that some products are to be included in the
+ // order from the inventory. For these inventory management
+ // is performed (so the products must be in stock) and
+ // details are completed from the product data of the backend.
+ inventory_products?: MinimalInventoryProduct[];
+
+ // Specifies a lock identifier that was used to
+ // lock a product in the inventory. Only useful if
+ // inventory_products is set. Used in case a frontend
+ // reserved quantities of the individual products while
+ // the shopping cart was being built. Multiple UUIDs can
+ // be used in case different UUIDs were used for different
+ // products (i.e. in case the user started with multiple
+ // shopping sessions that were combined during checkout).
+ lock_uuids?: string[];
+
+ // Should a token for claiming the order be generated?
+ // False can make sense if the ORDER_ID is sufficiently
+ // high entropy to prevent adversarial claims (like it is
+ // if the backend auto-generates one). Default is 'true'.
+ create_token?: boolean;
+
+ // OTP device ID to associate with the order.
+ // This parameter is optional.
+ otp_id?: string;
+ }
+
+ type Order = MinimalOrderDetail | ContractTerms;
+
+ interface MinimalOrderDetail {
+ // Amount to be paid by the customer.
+ amount: AmountString;
+
+ // Short summary of the order.
+ summary: string;
+
+ // See documentation of fulfillment_url in ContractTerms.
+ // Either fulfillment_url or fulfillment_message must be specified.
+ // When creating an order, the fulfillment URL can
+ // contain ${ORDER_ID} which will be substituted with the
+ // order ID of the newly created order.
+ fulfillment_url?: string;
+
+ // See documentation of fulfillment_message in ContractTerms.
+ // Either fulfillment_url or fulfillment_message must be specified.
+ fulfillment_message?: string;
+ }
+
+ interface MinimalInventoryProduct {
+ // Which product is requested (here mandatory!).
+ product_id: string;
+
+ // How many units of the product are requested.
+ quantity: Integer;
+ }
+
+ export interface PostOrderResponse {
+ // Order ID of the response that was just created.
+ order_id: string;
+
+ // Token that authorizes the wallet to claim the order.
+ // Provided only if "create_token" was set to 'true'
+ // in the request.
+ token?: ClaimToken;
+ }
+ export interface OutOfStockResponse {
+ // Product ID of an out-of-stock item.
+ product_id: string;
+
+ // Requested quantity.
+ requested_quantity: Integer;
+
+ // Available quantity (must be below requested_quantity).
+ available_quantity: Integer;
+
+ // When do we expect the product to be again in stock?
+ // Optional, not given if unknown.
+ restock_expected?: Timestamp;
+ }
+
+ export interface OrderHistory {
+ // Timestamp-sorted array of all orders matching the query.
+ // The order of the sorting depends on the sign of delta.
+ orders: OrderHistoryEntry[];
+ }
+ export interface OrderHistoryEntry {
+ // Order ID of the transaction related to this entry.
+ order_id: string;
+
+ // Row ID of the order in the database.
+ row_id: number;
+
+ // When the order was created.
+ timestamp: Timestamp;
+
+ // The amount of money the order is for.
+ amount: AmountString;
+
+ // The summary of the order.
+ summary: string;
+
+ // Whether some part of the order is refundable,
+ // that is the refund deadline has not yet expired
+ // and the total amount refunded so far is below
+ // the value of the original transaction.
+ refundable: boolean;
+
+ // Whether the order has been paid or not.
+ paid: boolean;
+ }
+
+ export type MerchantOrderStatusResponse =
+ | CheckPaymentPaidResponse
+ | CheckPaymentClaimedResponse
+ | CheckPaymentUnpaidResponse;
+ export interface CheckPaymentPaidResponse {
+ // The customer paid for this contract.
+ order_status: "paid";
+
+ // Was the payment refunded (even partially)?
+ refunded: boolean;
+
+ // True if there are any approved refunds that the wallet has
+ // not yet obtained.
+ refund_pending: boolean;
+
+ // Did the exchange wire us the funds?
+ wired: boolean;
+
+ // Total amount the exchange deposited into our bank account
+ // for this contract, excluding fees.
+ deposit_total: AmountString;
+
+ // Numeric error code indicating errors the exchange
+ // encountered tracking the wire transfer for this purchase (before
+ // we even got to specific coin issues).
+ // 0 if there were no issues.
+ exchange_code: number;
+
+ // HTTP status code returned by the exchange when we asked for
+ // information to track the wire transfer for this purchase.
+ // 0 if there were no issues.
+ exchange_http_status: number;
+
+ // Total amount that was refunded, 0 if refunded is false.
+ refund_amount: AmountString;
+
+ // Contract terms.
+ contract_terms: ContractTerms;
+
+ // The wire transfer status from the exchange for this order if
+ // available, otherwise empty array.
+ wire_details: TransactionWireTransfer[];
+
+ // Reports about trouble obtaining wire transfer details,
+ // empty array if no trouble were encountered.
+ wire_reports: TransactionWireReport[];
+
+ // The refund details for this order. One entry per
+ // refunded coin; empty array if there are no refunds.
+ refund_details: RefundDetails[];
+
+ // Status URL, can be used as a redirect target for the browser
+ // to show the order QR code / trigger the wallet.
+ order_status_url: string;
+ }
+ export interface CheckPaymentClaimedResponse {
+ // A wallet claimed the order, but did not yet pay for the contract.
+ order_status: "claimed";
+
+ // Contract terms.
+ contract_terms: ContractTerms;
+ }
+ export interface CheckPaymentUnpaidResponse {
+ // The order was neither claimed nor paid.
+ order_status: "unpaid";
+
+ // URI that the wallet must process to complete the payment.
+ taler_pay_uri: string;
+
+ // when was the order created
+ creation_time: Timestamp;
+
+ // Order summary text.
+ summary: string;
+
+ // Total amount of the order (to be paid by the customer).
+ total_amount: AmountString;
+
+ // Alternative order ID which was paid for already in the same session.
+ // Only given if the same product was purchased before in the same session.
+ already_paid_order_id?: string;
+
+ // Fulfillment URL of an already paid order. Only given if under this
+ // session an already paid order with a fulfillment URL exists.
+ already_paid_fulfillment_url?: string;
+
+ // Status URL, can be used as a redirect target for the browser
+ // to show the order QR code / trigger the wallet.
+ order_status_url: string;
+
+ // We do we NOT return the contract terms here because they may not
+ // exist in case the wallet did not yet claim them.
+ }
+ export interface RefundDetails {
+ // Reason given for the refund.
+ reason: string;
+
+ // Set to true if a refund is still available for the wallet for this payment.
+ pending: boolean;
+
+ // When was the refund approved.
+ timestamp: Timestamp;
+
+ // Total amount that was refunded (minus a refund fee).
+ amount: AmountString;
+ }
+ export interface TransactionWireTransfer {
+ // Responsible exchange.
+ exchange_url: string;
+
+ // 32-byte wire transfer identifier.
+ wtid: Base32;
+
+ // Execution time of the wire transfer.
+ execution_time: Timestamp;
+
+ // Total amount that has been wire transferred
+ // to the merchant.
+ amount: AmountString;
+
+ // Was this transfer confirmed by the merchant via the
+ // POST /transfers API, or is it merely claimed by the exchange?
+ confirmed: boolean;
+ }
+ export interface TransactionWireReport {
+ // Numerical error code.
+ code: number;
+
+ // Human-readable error description.
+ hint: string;
+
+ // Numerical error code from the exchange.
+ exchange_code: number;
+
+ // HTTP status code received from the exchange.
+ exchange_http_status: number;
+
+ // Public key of the coin for which we got the exchange error.
+ coin_pub: CoinPublicKey;
+ }
+
+ export interface ForgetRequest {
+ // Array of valid JSON paths to forgettable fields in the order's
+ // contract terms.
+ fields: string[];
+ }
+
+ export interface RefundRequest {
+ // Amount to be refunded.
+ refund: AmountString;
+
+ // Human-readable refund justification.
+ reason: string;
+ }
+ export interface MerchantRefundResponse {
+ // URL (handled by the backend) that the wallet should access to
+ // trigger refund processing.
+ // taler://refund/...
+ taler_refund_uri: string;
+
+ // Contract hash that a client may need to authenticate an
+ // HTTP request to obtain the above URI in a wallet-friendly way.
+ h_contract: HashCode;
+ }
+
+ export interface TransferInformation {
+ // How much was wired to the merchant (minus fees).
+ credit_amount: AmountString;
+
+ // Raw wire transfer identifier identifying the wire transfer (a base32-encoded value).
+ wtid: WireTransferIdentifierRawP;
+
+ // Target account that received the wire transfer.
+ payto_uri: PaytoString;
+
+ // Base URL of the exchange that made the wire transfer.
+ exchange_url: string;
+ }
+
+ export interface TransferList {
+ // List of all the transfers that fit the filter that we know.
+ transfers: TransferDetails[];
+ }
+ export interface TransferDetails {
+ // How much was wired to the merchant (minus fees).
+ credit_amount: AmountString;
+
+ // Raw wire transfer identifier identifying the wire transfer (a base32-encoded value).
+ wtid: WireTransferIdentifierRawP;
+
+ // Target account that received the wire transfer.
+ payto_uri: PaytoString;
+
+ // Base URL of the exchange that made the wire transfer.
+ exchange_url: string;
+
+ // Serial number identifying the transfer in the merchant backend.
+ // Used for filtering via offset.
+ transfer_serial_id: number;
+
+ // Time of the execution of the wire transfer by the exchange, according to the exchange
+ // Only provided if we did get an answer from the exchange.
+ execution_time?: Timestamp;
+
+ // True if we checked the exchange's answer and are happy with it.
+ // False if we have an answer and are unhappy, missing if we
+ // do not have an answer from the exchange.
+ verified?: boolean;
+
+ // True if the merchant uses the POST /transfers API to confirm
+ // that this wire transfer took place (and it is thus not
+ // something merely claimed by the exchange).
+ confirmed?: boolean;
+ }
+
+ interface ReserveCreateRequest {
+ // Amount that the merchant promises to put into the reserve.
+ initial_balance: AmountString;
+
+ // Exchange the merchant intends to use for rewards.
+ 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: TalerExchangeApi.WireAccount[];
+ }
+
+ 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: AmountString;
+
+ // Initial amount as per exchange, 0 if exchange did
+ // not confirm reserve creation yet.
+ exchange_initial_amount: AmountString;
+
+ // Amount picked up so far.
+ pickup_amount: AmountString;
+
+ // Amount approved for rewards that exceeds the pickup_amount.
+ committed_amount: AmountString;
+
+ // Is this reserve active (false if it was deleted but not purged)?
+ active: 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: AmountString;
+
+ // Initial amount as per exchange, 0 if exchange did
+ // not confirm reserve creation yet.
+ exchange_initial_amount: AmountString;
+
+ // Amount picked up so far.
+ pickup_amount: AmountString;
+
+ // Amount approved for rewards that exceeds the pickup_amount.
+ committed_amount: AmountString;
+
+ // 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?: TalerExchangeApi.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: AmountString;
+
+ // Human-readable reason for why the reward was granted.
+ reason: string;
+ }
+
+ interface RewardCreateRequest {
+ // Amount that the customer should be rewarded.
+ amount: AmountString;
+
+ // Justification for giving the reward.
+ justification: string;
+
+ // URL that the user should be directed to after receiving the reward,
+ // 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 RewardDetails {
+ // Amount that we authorized for this reward.
+ total_authorized: AmountString;
+
+ // Amount that was picked up by the user already.
+ total_picked_up: AmountString;
+
+ // 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: AmountString;
+ }
+
+ interface RewardsResponse {
+ // List of rewards that are present in the backend.
+ rewards: Reward[];
+ }
+ interface Reward {
+ // ID of the reward in the backend database.
+ row_id: number;
+
+ // Unique identifier for the reward.
+ reward_id: HashCode;
+
+ // (Remaining) amount of the reward (including fees).
+ reward_amount: AmountString;
+ }
+
+ export interface OtpDeviceAddDetails {
+ // Device ID to use.
+ otp_device_id: string;
+
+ // Human-readable description for the device.
+ otp_device_description: string;
+
+ // A key encoded with RFC 3548 Base32.
+ // IMPORTANT: This is not using the typical
+ // Taler base32-crockford encoding.
+ // Instead it uses the RFC 3548 encoding to
+ // be compatible with the TOTP standard.
+ otp_key: string;
+
+ // Algorithm for computing the POS confirmation.
+ // "NONE" or 0: No algorithm (no pos confirmation will be generated)
+ // "TOTP_WITHOUT_PRICE" or 1: Without amounts (typical OTP device)
+ // "TOTP_WITH_PRICE" or 2: With amounts (special-purpose OTP device)
+ // The "String" variants are supported @since protocol **v7**.
+ otp_algorithm: Integer | string;
+
+ // Counter for counter-based OTP devices.
+ otp_ctr?: Integer;
+ }
+
+ export interface OtpDevicePatchDetails {
+ // Human-readable description for the device.
+ otp_device_description: string;
+
+ // A key encoded with RFC 3548 Base32.
+ // IMPORTANT: This is not using the typical
+ // Taler base32-crockford encoding.
+ // Instead it uses the RFC 3548 encoding to
+ // be compatible with the TOTP standard.
+ otp_key: string;
+
+ // Algorithm for computing the POS confirmation.
+ otp_algorithm: Integer;
+
+ // Counter for counter-based OTP devices.
+ otp_ctr?: Integer;
+ }
+
+ export interface OtpDeviceSummaryResponse {
+ // Array of devices that are present in our backend.
+ otp_devices: OtpDeviceEntry[];
+ }
+ export interface OtpDeviceEntry {
+ // Device identifier.
+ otp_device_id: string;
+
+ // Human-readable description for the device.
+ device_description: string;
+ }
+
+ export interface OtpDeviceDetails {
+ // Human-readable description for the device.
+ device_description: string;
+
+ // Algorithm for computing the POS confirmation.
+ //
+ // Currently, the following numbers are defined:
+ // 0: None
+ // 1: TOTP without price
+ // 2: TOTP with price
+ otp_algorithm: Integer;
+
+ // Counter for counter-based OTP devices.
+ otp_ctr?: Integer;
+
+ // Current time for time-based OTP devices.
+ // Will match the faketime argument of the
+ // query if one was present, otherwise the current
+ // time at the backend.
+ //
+ // Available since protocol **v10**.
+ otp_timestamp: Integer;
+
+ // Current OTP confirmation string of the device.
+ // Matches exactly the string that would be returned
+ // as part of a payment confirmation for the given
+ // amount and time (so may contain multiple OTP codes).
+ //
+ // If the otp_algorithm is time-based, the code is
+ // returned for the current time, or for the faketime
+ // if a TIMESTAMP query argument was provided by the client.
+ //
+ // When using OTP with counters, the counter is **NOT**
+ // increased merely because this endpoint created
+ // an OTP code (this is a GET request, after all!).
+ //
+ // If the otp_algorithm requires an amount, the
+ // amount argument must be specified in the
+ // query, otherwise the otp_code is not
+ // generated.
+ //
+ // This field is *optional* in the response, as it is
+ // only provided if we could compute it based on the
+ // otp_algorithm and matching client query arguments.
+ //
+ // Available since protocol **v10**.
+ otp_code?: string;
+ }
+ export interface TemplateAddDetails {
+ // Template ID to use.
+ template_id: string;
+
+ // Human-readable description for the template.
+ template_description: string;
+
+ // OTP device ID.
+ // This parameter is optional.
+ otp_id?: string;
+
+ // Additional information in a separate template.
+ template_contract: TemplateContractDetails;
+
+ // Key-value pairs matching a subset of the
+ // fields from template_contract that are
+ // user-editable defaults for this template.
+ // Since protocol **v13**.
+ editable_defaults?: TemplateContractDetailsDefaults;
+
+ // Required currency for payments. Useful if no
+ // amount is specified in the template_contract
+ // but the user should be required to pay in a
+ // particular currency anyway. Merchant backends
+ // may reject requests if the template_contract
+ // or editable_defaults do
+ // specify an amount in a different currency.
+ // This parameter is optional.
+ // Since protocol **v13**.
+ required_currency?: string;
+ }
+ export interface TemplateContractDetails {
+ // Human-readable summary for the template.
+ summary?: string;
+
+ // Required currency for payments to the template.
+ // The user may specify any amount, but it must be
+ // in this currency.
+ // This parameter is optional and should not be present
+ // if "amount" is given.
+ currency?: string;
+
+ // The price is imposed by the merchant and cannot be changed by the customer.
+ // This parameter is optional.
+ amount?: AmountString;
+
+ // Minimum age buyer must have (in years). Default is 0.
+ minimum_age: Integer;
+
+ // The time the customer need to pay before his order will be deleted.
+ // It is deleted if the customer did not pay and if the duration is over.
+ pay_duration: RelativeTime;
+ }
+
+ export interface TemplateContractDetailsDefaults {
+ summary?: string;
+
+ currency?: string;
+
+ amount?: AmountString;
+
+ minimum_age?: Integer;
+
+ pay_duration?: RelativeTime;
+ }
+ export interface TemplatePatchDetails {
+ // Human-readable description for the template.
+ template_description: string;
+
+ // OTP device ID.
+ // This parameter is optional.
+ otp_id?: string;
+
+ // Additional information in a separate template.
+ template_contract: TemplateContractDetails;
+
+ // Key-value pairs matching a subset of the
+ // fields from template_contract that are
+ // user-editable defaults for this template.
+ // Since protocol **v13**.
+ editable_defaults?: TemplateContractDetailsDefaults;
+
+ // Required currency for payments. Useful if no
+ // amount is specified in the template_contract
+ // but the user should be required to pay in a
+ // particular currency anyway. Merchant backends
+ // may reject requests if the template_contract
+ // or editable_defaults do
+ // specify an amount in a different currency.
+ // This parameter is optional.
+ // Since protocol **v13**.
+ required_currency?: string;
+ }
+
+ export interface TemplateSummaryResponse {
+ // List of templates that are present in our backend.
+ templates: TemplateEntry[];
+ }
+
+ export interface TemplateEntry {
+ // Template identifier, as found in the template.
+ template_id: string;
+
+ // Human-readable description for the template.
+ template_description: string;
+ }
+
+ export interface WalletTemplateDetails {
+ // Hard-coded information about the contrac terms
+ // for this template.
+ template_contract: TemplateContractDetails;
+
+ // Key-value pairs matching a subset of the
+ // fields from template_contract that are
+ // user-editable defaults for this template.
+ // Since protocol **v13**.
+ editable_defaults?: TemplateContractDetailsDefaults;
+
+ // Required currency for payments. Useful if no
+ // amount is specified in the template_contract
+ // but the user should be required to pay in a
+ // particular currency anyway. Merchant backends
+ // may reject requests if the template_contract
+ // or editable_defaults do
+ // specify an amount in a different currency.
+ // This parameter is optional.
+ // Since protocol **v13**.
+ required_currency?: string;
+ }
+
+ export interface TemplateDetails {
+ // Human-readable description for the template.
+ template_description: string;
+
+ // OTP device ID.
+ // This parameter is optional.
+ otp_id?: string;
+
+ // Additional information in a separate template.
+ template_contract: TemplateContractDetails;
+
+ // Key-value pairs matching a subset of the
+ // fields from template_contract that are
+ // user-editable defaults for this template.
+ // Since protocol **v13**.
+ editable_defaults?: TemplateContractDetailsDefaults;
+
+ // Required currency for payments. Useful if no
+ // amount is specified in the template_contract
+ // but the user should be required to pay in a
+ // particular currency anyway. Merchant backends
+ // may reject requests if the template_contract
+ // or editable_defaults do
+ // specify an amount in a different currency.
+ // This parameter is optional.
+ // Since protocol **v13**.
+ required_currency?: string;
+ }
+ export interface UsingTemplateDetails {
+ // Summary of the template
+ summary?: string;
+
+ // The amount entered by the customer.
+ amount?: AmountString;
+ }
+
+ export interface WebhookAddDetails {
+ // Webhook ID to use.
+ webhook_id: string;
+
+ // The event of the webhook: why the webhook is used.
+ event_type: string;
+
+ // URL of the webhook where the customer will be redirected.
+ url: string;
+
+ // Method used by the webhook
+ http_method: string;
+
+ // Header template of the webhook
+ header_template?: string;
+
+ // Body template by the webhook
+ body_template?: string;
+ }
+
+ export interface WebhookPatchDetails {
+ // The event of the webhook: why the webhook is used.
+ event_type: string;
+
+ // URL of the webhook where the customer will be redirected.
+ url: string;
+
+ // Method used by the webhook
+ http_method: string;
+
+ // Header template of the webhook
+ header_template?: string;
+
+ // Body template by the webhook
+ body_template?: string;
+ }
+
+ export interface WebhookSummaryResponse {
+ // Return webhooks that are present in our backend.
+ webhooks: WebhookEntry[];
+ }
+
+ export interface WebhookEntry {
+ // Webhook identifier, as found in the webhook.
+ webhook_id: string;
+
+ // The event of the webhook: why the webhook is used.
+ event_type: string;
+ }
+
+ export interface WebhookDetails {
+ // The event of the webhook: why the webhook is used.
+ event_type: string;
+
+ // URL of the webhook where the customer will be redirected.
+ url: string;
+
+ // Method used by the webhook
+ http_method: string;
+
+ // Header template of the webhook
+ header_template?: string;
+
+ // Body template by the webhook
+ body_template?: string;
+ }
+
+ export interface TokenFamilyCreateRequest {
+ // Identifier for the token family consisting of unreserved characters
+ // according to RFC 3986.
+ slug: string;
+
+ // Human-readable name for the token family.
+ name: string;
+
+ // Human-readable description for the token family.
+ description: string;
+
+ // Optional map from IETF BCP 47 language tags to localized descriptions.
+ description_i18n?: { [lang_tag: string]: string };
+
+ // Start time of the token family's validity period.
+ // If not specified, merchant backend will use the current time.
+ valid_after?: Timestamp;
+
+ // End time of the token family's validity period.
+ valid_before: Timestamp;
+
+ // Validity duration of an issued token.
+ duration: RelativeTime;
+
+ // Kind of the token family.
+ kind: TokenFamilyKind;
+ }
+
+ export enum TokenFamilyKind {
+ Discount = "discount",
+ Subscription = "subscription",
+ }
+
+ export interface TokenFamilyUpdateRequest {
+ // Human-readable name for the token family.
+ name: string;
+
+ // Human-readable description for the token family.
+ description: string;
+
+ // Optional map from IETF BCP 47 language tags to localized descriptions.
+ description_i18n: { [lang_tag: string]: string };
+
+ // Start time of the token family's validity period.
+ valid_after: Timestamp;
+
+ // End time of the token family's validity period.
+ valid_before: Timestamp;
+
+ // Validity duration of an issued token.
+ duration: RelativeTime;
+ }
+
+ export interface TokenFamiliesList {
+ // All configured token families of this instance.
+ token_families: TokenFamilySummary[];
+ }
+
+ export interface TokenFamilySummary {
+ // Identifier for the token family consisting of unreserved characters
+ // according to RFC 3986.
+ slug: string;
+
+ // Human-readable name for the token family.
+ name: string;
+
+ // Start time of the token family's validity period.
+ valid_after: Timestamp;
+
+ // End time of the token family's validity period.
+ valid_before: Timestamp;
+
+ // Kind of the token family.
+ kind: TokenFamilyKind;
+ }
+
+ export interface TokenFamilyDetails {
+ // Identifier for the token family consisting of unreserved characters
+ // according to RFC 3986.
+ slug: string;
+
+ // Human-readable name for the token family.
+ name: string;
+
+ // Human-readable description for the token family.
+ description: string;
+
+ // Optional map from IETF BCP 47 language tags to localized descriptions.
+ description_i18n?: { [lang_tag: string]: string };
+
+ // Start time of the token family's validity period.
+ valid_after: Timestamp;
+
+ // End time of the token family's validity period.
+ valid_before: Timestamp;
+
+ // Validity duration of an issued token.
+ duration: RelativeTime;
+
+ // Kind of the token family.
+ kind: TokenFamilyKind;
+
+ // How many tokens have been issued for this family.
+ issued: Integer;
+
+ // How many tokens have been redeemed for this family.
+ redeemed: Integer;
+ }
+ export interface ContractTerms {
+ // Human-readable description of the whole purchase.
+ summary: string;
+
+ // Map from IETF BCP 47 language tags to localized summaries.
+ summary_i18n?: { [lang_tag: string]: string };
+
+ // Unique, free-form identifier for the proposal.
+ // Must be unique within a merchant instance.
+ // For merchants that do not store proposals in their DB
+ // before the customer paid for them, the order_id can be used
+ // by the frontend to restore a proposal from the information
+ // encoded in it (such as a short product identifier and timestamp).
+ order_id: string;
+
+ // Total price for the transaction.
+ // The exchange will subtract deposit fees from that amount
+ // before transferring it to the merchant.
+ amount: AmountString;
+
+ // URL where the same contract could be ordered again (if
+ // available). Returned also at the public order endpoint
+ // for people other than the actual buyer (hence public,
+ // in case order IDs are guessable).
+ public_reorder_url?: string;
+
+ // URL that will show that the order was successful after
+ // it has been paid for. Optional. When POSTing to the
+ // merchant, the placeholder "${ORDER_ID}" will be
+ // replaced with the actual order ID (useful if the
+ // order ID is generated server-side and needs to be
+ // in the URL).
+ // Note that this placeholder can only be used once.
+ // Either fulfillment_url or fulfillment_message must be specified.
+ fulfillment_url?: string;
+
+ // Message shown to the customer after paying for the order.
+ // Either fulfillment_url or fulfillment_message must be specified.
+ fulfillment_message?: string;
+
+ // Map from IETF BCP 47 language tags to localized fulfillment
+ // messages.
+ fulfillment_message_i18n?: { [lang_tag: string]: string };
+
+ // Maximum total deposit fee accepted by the merchant for this contract.
+ // Overrides defaults of the merchant instance.
+ max_fee: AmountString;
+
+ // List of products that are part of the purchase (see Product).
+ products: Product[];
+
+ // Time when this contract was generated.
+ timestamp: Timestamp;
+
+ // After this deadline has passed, no refunds will be accepted.
+ refund_deadline: Timestamp;
+
+ // After this deadline, the merchant won't accept payments for the contract.
+ pay_deadline: Timestamp;
+
+ // Transfer deadline for the exchange. Must be in the
+ // deposit permissions of coins used to pay for this order.
+ wire_transfer_deadline: Timestamp;
+
+ // Merchant's public key used to sign this proposal; this information
+ // is typically added by the backend. Note that this can be an ephemeral key.
+ merchant_pub: EddsaPublicKey;
+
+ // Base URL of the (public!) merchant backend API.
+ // Must be an absolute URL that ends with a slash.
+ merchant_base_url: string;
+
+ // More info about the merchant, see below.
+ merchant: Merchant;
+
+ // The hash of the merchant instance's wire details.
+ h_wire: HashCode;
+
+ // Wire transfer method identifier for the wire method associated with h_wire.
+ // The wallet may only select exchanges via a matching auditor if the
+ // exchange also supports this wire method.
+ // The wire transfer fees must be added based on this wire transfer method.
+ wire_method: string;
+
+ // Exchanges that the merchant accepts even if it does not accept any auditors that audit them.
+ exchanges: Exchange[];
+
+ // Delivery location for (all!) products.
+ delivery_location?: Location;
+
+ // Time indicating when the order should be delivered.
+ // May be overwritten by individual products.
+ delivery_date?: Timestamp;
+
+ // Nonce generated by the wallet and echoed by the merchant
+ // in this field when the proposal is generated.
+ nonce: string;
+
+ // Specifies for how long the wallet should try to get an
+ // automatic refund for the purchase. If this field is
+ // present, the wallet should wait for a few seconds after
+ // the purchase and then automatically attempt to obtain
+ // a refund. The wallet should probe until "delay"
+ // after the payment was successful (i.e. via long polling
+ // or via explicit requests with exponential back-off).
+ //
+ // In particular, if the wallet is offline
+ // at that time, it MUST repeat the request until it gets
+ // one response from the merchant after the delay has expired.
+ // If the refund is granted, the wallet MUST automatically
+ // recover the payment. This is used in case a merchant
+ // knows that it might be unable to satisfy the contract and
+ // desires for the wallet to attempt to get the refund without any
+ // customer interaction. Note that it is NOT an error if the
+ // merchant does not grant a refund.
+ auto_refund?: RelativeTime;
+
+ // Extra data that is only interpreted by the merchant frontend.
+ // Useful when the merchant needs to store extra information on a
+ // contract without storing it separately in their database.
+ extra?: any;
+
+ // Minimum age the buyer must have (in years). Default is 0.
+ // This value is at least as large as the maximum over all
+ // minimum age requirements of the products in this contract.
+ // It might also be set independent of any product, due to
+ // legal requirements.
+ minimum_age?: Integer;
+ }
+
+ export interface Product {
+ // Merchant-internal identifier for the product.
+ product_id?: string;
+
+ // Human-readable product description.
+ description: string;
+
+ // Map from IETF BCP 47 language tags to localized descriptions.
+ description_i18n?: { [lang_tag: string]: string };
+
+ // The number of units of the product to deliver to the customer.
+ quantity?: Integer;
+
+ // Unit in which the product is measured (liters, kilograms, packages, etc.).
+ unit?: string;
+
+ // The price of the product; this is the total price for quantity times unit of this product.
+ price?: AmountString;
+
+ // An optional base64-encoded product image.
+ image?: ImageDataUrl;
+
+ // A list of taxes paid by the merchant for this product. Can be empty.
+ taxes?: Tax[];
+
+ // Time indicating when this product should be delivered.
+ delivery_date?: Timestamp;
+ }
+
+ export interface Tax {
+ // The name of the tax.
+ name: string;
+
+ // Amount paid in tax.
+ tax: AmountString;
+ }
+ export interface Merchant {
+ // The merchant's legal name of business.
+ name: string;
+
+ // Label for a location with the business address of the merchant.
+ email?: string;
+
+ // Label for a location with the business address of the merchant.
+ website?: string;
+
+ // An optional base64-encoded product image.
+ logo?: ImageDataUrl;
+
+ // Label for a location with the business address of the merchant.
+ address?: Location;
+
+ // Label for a location that denotes the jurisdiction for disputes.
+ // Some of the typical fields for a location (such as a street address) may be absent.
+ jurisdiction?: Location;
+ }
+ // Delivery location, loosely modeled as a subset of
+ // ISO20022's PostalAddress25.
+ export interface Location {
+ // Nation with its own government.
+ country?: string;
+
+ // Identifies a subdivision of a country such as state, region, county.
+ country_subdivision?: string;
+
+ // Identifies a subdivision within a country sub-division.
+ district?: string;
+
+ // Name of a built-up area, with defined boundaries, and a local government.
+ town?: string;
+
+ // Specific location name within the town.
+ town_location?: string;
+
+ // Identifier consisting of a group of letters and/or numbers that
+ // is added to a postal address to assist the sorting of mail.
+ post_code?: string;
+
+ // Name of a street or thoroughfare.
+ street?: string;
+
+ // Name of the building or house.
+ building_name?: string;
+
+ // Number that identifies the position of a building on a street.
+ building_number?: string;
+
+ // Free-form address lines, should not exceed 7 elements.
+ address_lines?: string[];
+ }
+ interface Auditor {
+ // Official name.
+ name: string;
+
+ // Auditor's public key.
+ auditor_pub: EddsaPublicKey;
+
+ // Base URL of the auditor.
+ url: string;
+ }
+ export interface Exchange {
+ // The exchange's base URL.
+ url: string;
+
+ // How much would the merchant like to use this exchange.
+ // The wallet should use a suitable exchange with high
+ // priority. The following priority values are used, but
+ // it should be noted that they are NOT in any way normative.
+ //
+ // 0: likely it will not work (recently seen with account
+ // restriction that would be bad for this merchant)
+ // 512: merchant does not know, might be down (merchant
+ // did not yet get /wire response).
+ // 1024: good choice (recently confirmed working)
+ priority: Integer;
+
+ // Master public key of the exchange.
+ master_pub: EddsaPublicKey;
+ }
+}
+
+export namespace ChallengerApi {
+ export interface ChallengerTermsOfServiceResponse {
+ // Name of the service
+ name: "challenger";
+
+ // libtool-style representation of the Challenger protocol version, see
+ // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning
+ // The format is "current:revision:age".
+ version: string;
+
+ // URN of the implementation (needed to interpret 'revision' in version).
+ // @since v0, may become mandatory in the future.
+ implementation?: string;
+ }
+
+ export interface ChallengeSetupResponse {
+ // Nonce to use when constructing /authorize endpoint.
+ nonce: string;
+ }
+
+ export interface Restriction {
+ regex?: string;
+ hint?: string;
+ hint_i18n?: InternationalizedString;
+ }
+
+ export interface ChallengeStatus {
+ // Object; map of keys (names of the fields of the address
+ // to be entered by the user) to objects with a "regex" (string)
+ // containing an extended Posix regular expression for allowed
+ // address field values, and a "hint"/"hint_i18n" giving a
+ // human-readable explanation to display if the value entered
+ // by the user does not match the regex. Keys that are not mapped
+ // to such an object have no restriction on the value provided by
+ // the user. See "ADDRESS_RESTRICTIONS" in the challenger configuration.
+ restrictions: Record<string, Restriction> | undefined;
+
+ // indicates if the given address cannot be changed anymore, the
+ // form should be read-only if set to true.
+ fix_address: boolean;
+
+ // form values from the previous submission if available, details depend
+ // on the ADDRESS_TYPE, should be used to pre-populate the form
+ last_address: Record<string, string> | undefined;
+
+ // number of times the address can still be changed, may or may not be
+ // shown to the user
+ changes_left: Integer;
+ }
+
+ export interface ChallengeCreateResponse {
+ // how many more attempts are allowed, might be shown to the user,
+ // highlighting might be appropriate for low values such as 1 or 2 (the
+ // form will never be used if the value is zero)
+ attempts_left: Integer;
+
+ // the address that is being validated, might be shown or not
+ address: Object;
+
+ // true if we just retransmitted the challenge, false if we sent a
+ // challenge recently and thus refused to transmit it again this time;
+ // might make a useful hint to the user
+ transmitted: boolean;
+
+ // timestamp explaining when we would re-transmit the challenge the next
+ // time (at the earliest) if requested by the user
+ next_tx_time: string;
+ }
+
+ export interface InvalidPinResponse {
+ // numeric Taler error code, should be shown to indicate the error
+ // compactly for reporting to developers
+ ec?: number;
+
+ // human-readable Taler error code, should be shown for the user to
+ // understand the error
+ hint: string;
+
+ // how many times is the user still allowed to change the address;
+ // if 0, the user should not be shown a link to jump to the
+ // address entry form
+ addresses_left: Integer;
+
+ // how many times might the PIN still be retransmitted
+ pin_transmissions_left: Integer;
+
+ // how many times might the user still try entering the PIN code
+ auth_attempts_left: Integer;
+
+ // if true, the PIN was not even evaluated as the user previously
+ // exhausted the number of attempts
+ exhausted: boolean;
+
+ // if true, the PIN was not even evaluated as no challenge was ever
+ // issued (the user must have skipped the step of providing their
+ // address first!)
+ no_challenge: boolean;
+ }
+
+ export interface ChallengerAuthResponse {
+ // Token used to authenticate access in /info.
+ access_token: string;
+
+ // Type of the access token.
+ token_type: "Bearer";
+
+ // Amount of time that an access token is valid (in seconds).
+ expires_in: Integer;
+ }
+
+ export interface ChallengerInfoResponse {
+ // Unique ID of the record within Challenger
+ // (identifies the rowid of the token).
+ id: Integer;
+
+ // Address that was validated.
+ // Key-value pairs, details depend on the
+ // address_type.
+ address: Object;
+
+ // Type of the address.
+ address_type: string;
+
+ // How long do we consider the address to be
+ // valid for this user.
+ expires: Timestamp;
+ }
+}
diff --git a/packages/taler-util/src/http-client/utils.ts b/packages/taler-util/src/http-client/utils.ts
new file mode 100644
index 000000000..bf186ce46
--- /dev/null
+++ b/packages/taler-util/src/http-client/utils.ts
@@ -0,0 +1,116 @@
+/*
+ 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/>
+ */
+
+/**
+ * Imports.
+ */
+import { base64FromArrayBuffer } from "../base64.js";
+import { encodeCrock, getRandomBytes, stringToBytes } from "../taler-crypto.js";
+import { AccessToken, LongPollParams, PaginationParams } from "./types.js";
+
+/**
+ * Helper function to generate the "Authorization" HTTP header.
+ */
+export function makeBasicAuthHeader(
+ username: string,
+ password: string,
+): string {
+ const auth = `${username}:${password}`;
+ const authEncoded: string = base64FromArrayBuffer(stringToBytes(auth));
+ return `Basic ${authEncoded}`;
+}
+
+/**
+ * rfc8959
+ * @param token
+ * @returns
+ */
+export function makeBearerTokenAuthHeader(token: AccessToken): string {
+ return `Bearer ${token}`;
+}
+
+/**
+ * https://bugs.gnunet.org/view.php?id=7949
+ */
+export function addPaginationParams(url: URL, pagination?: PaginationParams) {
+ if (!pagination) return;
+ if (pagination.offset) {
+ url.searchParams.set("start", pagination.offset);
+ }
+ const order = !pagination || pagination.order === "asc" ? 1 : -1;
+ const limit =
+ !pagination || !pagination.limit || pagination.limit === 0
+ ? 5
+ : Math.abs(pagination.limit);
+ //always send delta
+ url.searchParams.set("delta", String(order * limit));
+}
+
+export function addMerchantPaginationParams(
+ url: URL,
+ pagination?: PaginationParams,
+) {
+ if (!pagination) return;
+ if (pagination.offset) {
+ url.searchParams.set("offset", pagination.offset);
+ }
+ const order = !pagination || pagination.order === "asc" ? 1 : -1;
+ const limit =
+ !pagination || !pagination.limit || pagination.limit === 0
+ ? 5
+ : Math.abs(pagination.limit);
+ //always send delta
+ url.searchParams.set("limit", String(order * limit));
+}
+
+export function addLongPollingParam(url: URL, param?: LongPollParams) {
+ if (!param) return;
+ if (param.timeoutMs) {
+ url.searchParams.set("long_poll_ms", String(param.timeoutMs));
+ }
+}
+
+export interface CacheEvictor<T> {
+ notifySuccess: (op: T) => Promise<void>;
+}
+
+export const nullEvictor: CacheEvictor<unknown> = {
+ notifySuccess: () => Promise.resolve(),
+};
+
+export class IdempotencyRetry {
+ public readonly uid: string;
+ public readonly timesLeft: number;
+ public readonly maxTries: number;
+
+ private constructor(timesLeft: number, maxTimesLeft: number) {
+ this.timesLeft = timesLeft;
+ this.maxTries = maxTimesLeft;
+ this.uid = encodeCrock(getRandomBytes(32))
+ }
+
+ static tryFiveTimes() {
+ return new IdempotencyRetry(5, 5)
+ }
+
+ next(): IdempotencyRetry | undefined {
+ const left = this.timesLeft -1
+ if (left <= 0) {
+ return undefined
+ }
+ return new IdempotencyRetry(left, this.maxTries);
+ }
+}
diff --git a/packages/taler-wallet-core/src/util/http.ts b/packages/taler-util/src/http-common.ts
index 118da40fe..d8cd36287 100644
--- a/packages/taler-wallet-core/src/util/http.ts
+++ b/packages/taler-util/src/http-common.ts
@@ -1,40 +1,36 @@
/*
- This file is part of TALER
- (C) 2016 GNUnet e.V.
+ This file is part of GNU Taler
+ (C) 2023 Taler Systems S.A.
- TALER is free software; you can redistribute it and/or modify it under the
+ GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
-/**
- * Helpers for doing XMLHttpRequest-s that are based on ES6 promises.
- * Allows for easy mocking for test cases.
- *
- * The API is inspired by the HTML5 fetch API.
- */
+ SPDX-License-Identifier: AGPL3.0-or-later
+*/
-/**
- * Imports
- */
+import type { CancellationToken } from "./CancellationToken.js";
+import { Codec } from "./codec.js";
+import { j2s } from "./helpers.js";
import {
- Logger,
- Duration,
- AbsoluteTime,
- TalerErrorDetail,
- Codec,
- j2s,
- CancellationToken,
-} from "@gnu-taler/taler-util";
-import { TalerErrorCode } from "@gnu-taler/taler-util";
-import { makeErrorDetail, TalerError } from "../errors.js";
+ TalerError,
+ base64FromArrayBuffer,
+ makeErrorDetail,
+ stringToBytes,
+} from "./index.js";
+import { Logger } from "./logging.js";
+import { TalerErrorCode } from "./taler-error-codes.js";
+import { AbsoluteTime, Duration } from "./time.js";
+import { TalerErrorDetail } from "./wallet-types.js";
+
+const textEncoder = new TextEncoder();
const logger = new Logger("http.ts");
@@ -54,8 +50,8 @@ export interface HttpResponse {
export const DEFAULT_REQUEST_TIMEOUT_MS = 60000;
export interface HttpRequestOptions {
- method?: "POST" | "PUT" | "GET";
- headers?: { [name: string]: string };
+ method?: "POST" | "PATCH" | "PUT" | "GET" | "DELETE";
+ headers?: { [name: string]: string | undefined };
/**
* Timeout after which the request should be aborted.
@@ -68,7 +64,13 @@ export interface HttpRequestOptions {
*/
cancellationToken?: CancellationToken;
- body?: string | ArrayBuffer | Object;
+ body?: string | ArrayBuffer | object;
+
+ /**
+ * How to handle redirects.
+ * Same semantics as WHATWG fetch.
+ */
+ redirect?: "follow" | "error" | "manual";
}
/**
@@ -110,24 +112,6 @@ export class Headers {
*/
export interface HttpRequestLibrary {
/**
- * Make an HTTP GET request.
- *
- * FIXME: Get rid of this, we want the API surface to be minimal.
- */
- get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse>;
-
- /**
- * Make an HTTP POST request with a JSON body.
- *
- * FIXME: Get rid of this, we want the API surface to be minimal.
- */
- postJson(
- url: string,
- body: any,
- opt?: HttpRequestOptions,
- ): Promise<HttpResponse>;
-
- /**
* Make an HTTP POST request with a JSON body.
*/
fetch(url: string, opt?: HttpRequestOptions): Promise<HttpResponse>;
@@ -141,10 +125,41 @@ type ResponseOrError<T> =
| { isError: false; response: T }
| { isError: true; talerErrorResponse: TalerErrorResponse };
+/**
+ * Read Taler error details from an HTTP response.
+ */
export async function readTalerErrorResponse(
httpResponse: HttpResponse,
): Promise<TalerErrorDetail> {
- const errJson = await httpResponse.json();
+ const contentType = httpResponse.headers.get("content-type");
+ if (contentType !== "application/json") {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ {
+ requestUrl: httpResponse.requestUrl,
+ requestMethod: httpResponse.requestMethod,
+ httpStatusCode: httpResponse.status,
+ contentType: contentType || "<null>",
+ },
+ "Error response did not even contain JSON. The request URL might be wrong or the service might be unavailable.",
+ );
+ }
+ let errJson;
+ try {
+ errJson = await httpResponse.json();
+ } catch (e: any) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ {
+ requestUrl: httpResponse.requestUrl,
+ requestMethod: httpResponse.requestMethod,
+ httpStatusCode: httpResponse.status,
+ validationError: e.toString(),
+ },
+ "Couldn't parse JSON format from error response",
+ );
+ }
+
const talerErrorCode = errJson.code;
if (typeof talerErrorCode !== "number") {
logger.warn(
@@ -168,7 +183,21 @@ export async function readTalerErrorResponse(
export async function readUnexpectedResponseDetails(
httpResponse: HttpResponse,
): Promise<TalerErrorDetail> {
- const errJson = await httpResponse.json();
+ let errJson;
+ try {
+ errJson = await httpResponse.json();
+ } catch (e: any) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ {
+ requestUrl: httpResponse.requestUrl,
+ requestMethod: httpResponse.requestMethod,
+ httpStatusCode: httpResponse.status,
+ validationError: e.toString(),
+ },
+ "Couldn't parse JSON format from error response",
+ );
+ }
const talerErrorCode = errJson.code;
if (typeof talerErrorCode !== "number") {
return makeErrorDetail(
@@ -185,6 +214,7 @@ export async function readUnexpectedResponseDetails(
TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
{
requestUrl: httpResponse.requestUrl,
+ requestMethod: httpResponse.requestMethod,
httpStatusCode: httpResponse.status,
errorResponse: errJson,
},
@@ -202,7 +232,21 @@ export async function readSuccessResponseJsonOrErrorCode<T>(
talerErrorResponse: await readTalerErrorResponse(httpResponse),
};
}
- const respJson = await httpResponse.json();
+ let respJson;
+ try {
+ respJson = await httpResponse.json();
+ } catch (e: any) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ {
+ requestUrl: httpResponse.requestUrl,
+ requestMethod: httpResponse.requestMethod,
+ httpStatusCode: httpResponse.status,
+ validationError: e.toString(),
+ },
+ "Couldn't parse JSON format from response",
+ );
+ }
let parsedResponse: T;
try {
parsedResponse = codec.decode(respJson);
@@ -211,6 +255,7 @@ export async function readSuccessResponseJsonOrErrorCode<T>(
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
{
requestUrl: httpResponse.requestUrl,
+ requestMethod: httpResponse.requestMethod,
httpStatusCode: httpResponse.status,
validationError: e.toString(),
},
@@ -223,11 +268,59 @@ export async function readSuccessResponseJsonOrErrorCode<T>(
};
}
+export async function readResponseJsonOrErrorCode<T>(
+ httpResponse: HttpResponse,
+ codec: Codec<T>,
+): Promise<{ isError: boolean; response: T }> {
+ let respJson;
+ try {
+ respJson = await httpResponse.json();
+ } catch (e: any) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ {
+ requestUrl: httpResponse.requestUrl,
+ requestMethod: httpResponse.requestMethod,
+ httpStatusCode: httpResponse.status,
+ validationError: e.toString(),
+ },
+ "Couldn't parse JSON format from response",
+ );
+ }
+ let parsedResponse: T;
+ try {
+ parsedResponse = codec.decode(respJson);
+ } catch (e: any) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ {
+ requestUrl: httpResponse.requestUrl,
+ requestMethod: httpResponse.requestMethod,
+ httpStatusCode: httpResponse.status,
+ validationError: e.toString(),
+ },
+ "Response invalid",
+ );
+ }
+ return {
+ isError: !(httpResponse.status >= 200 && httpResponse.status < 300),
+ response: parsedResponse,
+ };
+}
+
+
+type HttpErrorDetails = {
+ requestUrl: string;
+ requestMethod: string;
+ httpStatusCode: number;
+};
+
export function getHttpResponseErrorDetails(
httpResponse: HttpResponse,
-): Record<string, unknown> {
+): HttpErrorDetails {
return {
requestUrl: httpResponse.requestUrl,
+ requestMethod: httpResponse.requestMethod,
httpStatusCode: httpResponse.status,
};
}
@@ -240,6 +333,7 @@ export function throwUnexpectedRequestError(
TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
{
requestUrl: httpResponse.requestUrl,
+ requestMethod: httpResponse.requestMethod,
httpStatusCode: httpResponse.status,
errorResponse: talerErrorResponse,
},
@@ -258,11 +352,36 @@ export async function readSuccessResponseJsonOrThrow<T>(
throwUnexpectedRequestError(httpResponse, r.talerErrorResponse);
}
+export async function expectSuccessResponseOrThrow<T>(
+ httpResponse: HttpResponse,
+): Promise<void> {
+ if (httpResponse.status >= 200 && httpResponse.status <= 299) {
+ return;
+ }
+ const errResp = await readTalerErrorResponse(httpResponse);
+ throwUnexpectedRequestError(httpResponse, errResp);
+}
+
export async function readSuccessResponseTextOrErrorCode<T>(
httpResponse: HttpResponse,
): Promise<ResponseOrError<string>> {
if (!(httpResponse.status >= 200 && httpResponse.status < 300)) {
- const errJson = await httpResponse.json();
+ let errJson;
+ try {
+ errJson = await httpResponse.json();
+ } catch (e: any) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ {
+ requestUrl: httpResponse.requestUrl,
+ requestMethod: httpResponse.requestMethod,
+ httpStatusCode: httpResponse.status,
+ validationError: e.toString(),
+ },
+ "Couldn't parse JSON format from error response",
+ );
+ }
+
const talerErrorCode = errJson.code;
if (typeof talerErrorCode !== "number") {
throw TalerError.fromDetail(
@@ -291,7 +410,22 @@ export async function checkSuccessResponseOrThrow(
httpResponse: HttpResponse,
): Promise<void> {
if (!(httpResponse.status >= 200 && httpResponse.status < 300)) {
- const errJson = await httpResponse.json();
+ let errJson;
+ try {
+ errJson = await httpResponse.json();
+ } catch (e: any) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ {
+ requestUrl: httpResponse.requestUrl,
+ requestMethod: httpResponse.requestMethod,
+ httpStatusCode: httpResponse.status,
+ validationError: e.toString(),
+ },
+ "Couldn't parse JSON format from error response",
+ );
+ }
+
const talerErrorCode = errJson.code;
if (typeof talerErrorCode !== "number") {
throw TalerError.fromDetail(
@@ -332,9 +466,7 @@ export function getExpiry(
if (Number.isNaN(expiryDateMs)) {
t = AbsoluteTime.now();
} else {
- t = {
- t_ms: expiryDateMs,
- };
+ t = AbsoluteTime.fromMilliseconds(expiryDateMs);
}
if (opt.minDuration) {
const t2 = AbsoluteTime.addDuration(AbsoluteTime.now(), opt.minDuration);
@@ -342,3 +474,53 @@ export function getExpiry(
}
return t;
}
+
+export interface HttpLibArgs {
+ enableThrottling?: boolean;
+ /**
+ * Only allow HTTPS connections, not plain http.
+ */
+ requireTls?: boolean;
+ printAsCurl?: boolean;
+}
+
+export function encodeBody(body: any): ArrayBuffer {
+ if (body == null) {
+ return new ArrayBuffer(0);
+ }
+ if (typeof body === "string") {
+ return textEncoder.encode(body).buffer;
+ } else if (ArrayBuffer.isView(body)) {
+ return body.buffer;
+ } else if (body instanceof ArrayBuffer) {
+ return body;
+ } else if (typeof body === "object") {
+ return textEncoder.encode(JSON.stringify(body)).buffer;
+ }
+ throw new TypeError("unsupported request body type");
+}
+
+export function getDefaultHeaders(method: string): Record<string, string> {
+ const headers: Record<string, string> = {};
+
+ if (method === "POST" || method === "PUT" || method === "PATCH") {
+ // Default to JSON if we have a body
+ headers["Content-Type"] = "application/json";
+ }
+
+ headers["Accept"] = "application/json";
+
+ return headers;
+}
+
+/**
+ * Helper function to generate the "Authorization" HTTP header.
+ */
+export function makeBasicAuthHeader(
+ username: string,
+ password: string,
+): string {
+ const auth = `${username}:${password}`;
+ const authEncoded: string = base64FromArrayBuffer(stringToBytes(auth));
+ return `Basic ${authEncoded}`;
+}
diff --git a/packages/merchant-backend-ui/src/context/listener.ts b/packages/taler-util/src/http-impl.missing.ts
index 659db0a03..6ae6b93ec 100644
--- a/packages/merchant-backend-ui/src/context/listener.ts
+++ b/packages/taler-util/src/http-impl.missing.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2019 Taler Systems S.A.
GNU Taler is free 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,24 +12,27 @@
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
+ SPDX-License-Identifier: AGPL3.0-or-later
*/
-import { createContext } from 'preact'
-import { useContext } from 'preact/hooks'
+/**
+ * Imports.
+ */
+import {
+ HttpRequestLibrary,
+ HttpRequestOptions,
+ HttpResponse,
+} from "./http.js";
-interface Type {
- id: string;
- token?: string;
- admin?: boolean;
- changeToken: (t?:string) => void;
+/**
+ * Implementation of the HTTP request library interface for node.
+ */
+export class HttpLibImpl implements HttpRequestLibrary {
+ fetch(
+ url: string,
+ opt?: HttpRequestOptions | undefined,
+ ): Promise<HttpResponse> {
+ throw new Error("Method not implemented.");
+ }
}
-
-const Context = createContext<Type>({} as any)
-
-export const ListenerContextProvider = Context.Provider
-export const useListenerContext = (): Type => useContext(Context);
diff --git a/packages/taler-util/src/http-impl.node.d.ts b/packages/taler-util/src/http-impl.node.d.ts
new file mode 100644
index 000000000..771dd991c
--- /dev/null
+++ b/packages/taler-util/src/http-impl.node.d.ts
@@ -0,0 +1,25 @@
+import { HttpLibArgs } from "./http-common.js";
+import {
+ HttpRequestLibrary,
+ HttpRequestOptions,
+ HttpResponse,
+} from "./http.js";
+/**
+ * Implementation of the HTTP request library interface for node.
+ */
+export declare class HttpLibImpl implements HttpRequestLibrary {
+ private throttle;
+ private throttlingEnabled;
+ constructor(args?: HttpLibArgs);
+ /**
+ * Set whether requests should be throttled.
+ */
+ setThrottling(enabled: boolean): void;
+ fetch(url: string, opt?: HttpRequestOptions): Promise<HttpResponse>;
+ get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse>;
+ postJson(
+ url: string,
+ body: any,
+ opt?: HttpRequestOptions,
+ ): Promise<HttpResponse>;
+}
diff --git a/packages/taler-util/src/http-impl.node.ts b/packages/taler-util/src/http-impl.node.ts
new file mode 100644
index 000000000..45a12c258
--- /dev/null
+++ b/packages/taler-util/src/http-impl.node.ts
@@ -0,0 +1,324 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+
+ SPDX-License-Identifier: AGPL3.0-or-later
+*/
+
+/**
+ * Imports.
+ */
+import type { FollowOptions, RedirectableRequest } from "follow-redirects";
+import followRedirects from "follow-redirects";
+import type { ClientRequest, IncomingMessage } from "node:http";
+import { RequestOptions } from "node:http";
+import * as net from "node:net";
+import { TalerError } from "./errors.js";
+import { HttpLibArgs, encodeBody, getDefaultHeaders } from "./http-common.js";
+import {
+ DEFAULT_REQUEST_TIMEOUT_MS,
+ Headers,
+ HttpRequestLibrary,
+ HttpRequestOptions,
+ HttpResponse,
+} from "./http.js";
+import {
+ Logger,
+ RequestThrottler,
+ TalerErrorCode,
+ URL,
+ typedArrayConcat,
+} from "./index.js";
+
+const http = followRedirects.http;
+const https = followRedirects.https;
+
+// Work around a node v20.0.0, v20.1.0, and v20.2.0 bug. The issue was fixed
+// in v20.3.0.
+// https://github.com/nodejs/node/issues/47822#issuecomment-1564708870
+// Safe to remove once support for Node v20 is dropped.
+if (
+ // check for `node` in case we want to use this in "exotic" JS envs
+ process.versions.node &&
+ process.versions.node.match(/20\.[0-2]\.0/)
+) {
+ //@ts-ignore
+ net.setDefaultAutoSelectFamily(false);
+}
+
+const logger = new Logger("http-impl.node.ts");
+
+const textDecoder = new TextDecoder();
+let SHOW_CURL_HTTP_REQUEST = false;
+export function setPrintHttpRequestAsCurl(b: boolean) {
+ SHOW_CURL_HTTP_REQUEST = b;
+}
+
+/**
+ * Implementation of the HTTP request library interface for node.
+ */
+export class HttpLibImpl implements HttpRequestLibrary {
+ private throttle = new RequestThrottler();
+ private throttlingEnabled = true;
+ private requireTls = false;
+
+ constructor(args?: HttpLibArgs) {
+ this.throttlingEnabled = args?.enableThrottling ?? true;
+ this.requireTls = args?.requireTls ?? false;
+ }
+
+ /**
+ * Set whether requests should be throttled.
+ */
+ setThrottling(enabled: boolean): void {
+ this.throttlingEnabled = enabled;
+ }
+
+ async fetch(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> {
+ const method = opt?.method?.toUpperCase() ?? "GET";
+
+ logger.trace(`Requesting ${method} ${url}`);
+
+ const parsedUrl = new URL(url);
+ if (this.throttlingEnabled && this.throttle.applyThrottle(url)) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED,
+ {
+ requestMethod: method,
+ requestUrl: url,
+ throttleStats: this.throttle.getThrottleStats(url),
+ },
+ `request to origin ${parsedUrl.origin} was throttled`,
+ );
+ }
+ if (this.requireTls && parsedUrl.protocol !== "https:") {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_NETWORK_ERROR,
+ {
+ requestMethod: method,
+ requestUrl: url,
+ },
+ `request to ${parsedUrl.origin} is not possible with protocol ${parsedUrl.protocol}`,
+ );
+ }
+ let timeoutMs: number | undefined;
+ if (typeof opt?.timeout?.d_ms === "number") {
+ timeoutMs = opt.timeout.d_ms;
+ } else {
+ timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS;
+ }
+
+ const requestHeadersMap = getDefaultHeaders(method);
+ if (opt?.headers) {
+ Object.entries(opt?.headers).forEach(([key, value]) => {
+ if (value === undefined) return;
+ requestHeadersMap[key] = value;
+ });
+ }
+ logger.trace(`request timeout ${timeoutMs} ms`);
+
+ let reqBody: ArrayBuffer | undefined;
+
+ if (
+ opt?.method == "POST" ||
+ opt?.method == "PATCH" ||
+ opt?.method == "PUT"
+ ) {
+ reqBody = encodeBody(opt.body);
+ }
+
+ let path = parsedUrl.pathname;
+ if (parsedUrl.search != null) {
+ path += parsedUrl.search;
+ }
+
+ let protocol: string;
+ if (parsedUrl.protocol === "https:") {
+ protocol = "https:";
+ } else if (parsedUrl.protocol === "http:") {
+ protocol = "http:";
+ } else {
+ throw Error(`unsupported protocol (${parsedUrl.protocol})`);
+ }
+
+ const options: RequestOptions & FollowOptions<RequestOptions> = {
+ protocol,
+ port: parsedUrl.port,
+ host: parsedUrl.hostname,
+ method: method,
+ path,
+ headers: requestHeadersMap,
+ timeout: timeoutMs,
+ followRedirects: opt?.redirect !== "manual",
+ };
+
+ const chunks: Uint8Array[] = [];
+
+ if (SHOW_CURL_HTTP_REQUEST) {
+ const payload =
+ !reqBody || reqBody.byteLength === 0
+ ? undefined
+ : textDecoder.decode(reqBody);
+ const headers = Object.entries(requestHeadersMap).reduce(
+ (prev, [key, value]) => {
+ return `${prev} -H "${key}: ${value}"`;
+ },
+ "",
+ );
+ function ifUndefined<T>(arg: string, v: undefined | T): string {
+ if (v === undefined) return "";
+ return arg + " '" + String(v) + "'";
+ }
+ console.log(
+ `curl -X ${options.method} "${parsedUrl.href}" ${headers} ${ifUndefined(
+ "-d",
+ payload,
+ )}`,
+ );
+ }
+
+ let timeoutHandle: NodeJS.Timer | undefined = undefined;
+ let cancelCancelledHandler: (() => void) | undefined = undefined;
+
+ const doCleanup = () => {
+ if (timeoutHandle != null) {
+ clearTimeout(timeoutHandle);
+ }
+ if (cancelCancelledHandler) {
+ cancelCancelledHandler();
+ }
+ };
+
+ return new Promise((resolve, reject) => {
+ const handler = (res: IncomingMessage) => {
+ res.on("data", (d) => {
+ chunks.push(d);
+ });
+ res.on("end", () => {
+ const headers: Headers = new Headers();
+ for (const [k, v] of Object.entries(res.headers)) {
+ if (!v) {
+ continue;
+ }
+ if (typeof v === "string") {
+ headers.set(k, v);
+ } else {
+ headers.set(k, v.join(", "));
+ }
+ }
+ const data = typedArrayConcat(chunks);
+ const resp: HttpResponse = {
+ requestMethod: method,
+ requestUrl: parsedUrl.href,
+ status: res.statusCode || 0,
+ headers,
+ async bytes() {
+ return data;
+ },
+ json() {
+ const text = textDecoder.decode(data);
+ return JSON.parse(text);
+ },
+ async text() {
+ const text = textDecoder.decode(data);
+ return text;
+ },
+ };
+ doCleanup();
+ resolve(resp);
+ });
+ res.on("error", (e) => {
+ const code = "code" in e ? e.code : "unknown";
+ const err = TalerError.fromDetail(
+ TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
+ {
+ requestUrl: url,
+ requestMethod: method,
+ httpStatusCode: 0,
+ },
+ `Error in HTTP response handler: ${code}`,
+ );
+ doCleanup();
+ reject(err);
+ });
+ };
+
+ let req: RedirectableRequest<ClientRequest, IncomingMessage>;
+ if (options.protocol === "http:") {
+ req = http.request(options, handler);
+ } else if (options.protocol === "https:") {
+ req = https.request(options, handler);
+ } else {
+ throw new Error(`unsupported protocol ${options.protocol}`);
+ }
+
+ if (timeoutMs != null) {
+ timeoutHandle = setTimeout(() => {
+ logger.info(`request to ${url} timed out`);
+ const err = TalerError.fromDetail(
+ TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
+ {
+ requestUrl: url,
+ requestMethod: method,
+ httpStatusCode: 0,
+ },
+ `Request timed out after ${timeoutMs} ms`,
+ );
+ timeoutHandle = undefined;
+ req.destroy();
+ doCleanup();
+ reject(err);
+ req.destroy();
+ }, timeoutMs);
+ }
+
+ if (opt?.cancellationToken) {
+ cancelCancelledHandler = opt.cancellationToken.onCancelled(() => {
+ const err = TalerError.fromDetail(
+ TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
+ {
+ requestUrl: url,
+ requestMethod: method,
+ httpStatusCode: 0,
+ },
+ `Request cancelled`,
+ );
+ req.destroy();
+ doCleanup();
+ reject(err);
+ });
+ }
+
+ req.on("error", (e: Error) => {
+ const code = "code" in e ? e.code : "unknown";
+ const err = TalerError.fromDetail(
+ TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
+ {
+ requestUrl: url,
+ requestMethod: method,
+ httpStatusCode: 0,
+ },
+ `Error in HTTP request: ${code}`,
+ );
+ doCleanup();
+ reject(err);
+ });
+
+ if (reqBody) {
+ req.write(new Uint8Array(reqBody));
+ }
+ req.end();
+ });
+ }
+}
diff --git a/packages/taler-util/src/http-impl.qtart.ts b/packages/taler-util/src/http-impl.qtart.ts
new file mode 100644
index 000000000..b4e4ebbe7
--- /dev/null
+++ b/packages/taler-util/src/http-impl.qtart.ts
@@ -0,0 +1,211 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+
+ SPDX-License-Identifier: AGPL3.0-or-later
+*/
+
+/**
+ * Imports.
+ */
+import { Logger, openPromise } from "@gnu-taler/taler-util";
+import { TalerError } from "./errors.js";
+import { HttpLibArgs, encodeBody, getDefaultHeaders } from "./http-common.js";
+import {
+ Headers,
+ HttpRequestLibrary,
+ HttpRequestOptions,
+ HttpResponse,
+} from "./http.js";
+import { RequestThrottler, TalerErrorCode, URL } from "./index.js";
+import { QjsHttpResp, qjsOs } from "./qtart.js";
+
+const logger = new Logger("http-impl.qtart.ts");
+
+const textDecoder = new TextDecoder();
+
+export class RequestTimeoutError extends Error {
+ public constructor() {
+ super("Request timed out");
+ Object.setPrototypeOf(this, RequestTimeoutError.prototype);
+ }
+}
+
+export class RequestCancelledError extends Error {
+ public constructor() {
+ super("Request cancelled");
+ Object.setPrototypeOf(this, RequestCancelledError.prototype);
+ }
+}
+
+/**
+ * Implementation of the HTTP request library interface for node.
+ */
+export class HttpLibImpl implements HttpRequestLibrary {
+ private throttle = new RequestThrottler();
+ private throttlingEnabled = true;
+ private requireTls = false;
+
+ constructor(args?: HttpLibArgs) {
+ this.throttlingEnabled = args?.enableThrottling ?? true;
+ this.requireTls = args?.requireTls ?? false;
+ }
+
+ /**
+ * Set whether requests should be throttled.
+ */
+ setThrottling(enabled: boolean): void {
+ this.throttlingEnabled = enabled;
+ }
+
+ async fetch(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> {
+ const method = (opt?.method ?? "GET").toUpperCase();
+
+ logger.trace(`Requesting ${method} ${url}`);
+
+ const parsedUrl = new URL(url);
+ if (this.throttlingEnabled && this.throttle.applyThrottle(url)) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED,
+ {
+ requestMethod: method,
+ requestUrl: url,
+ throttleStats: this.throttle.getThrottleStats(url),
+ },
+ `request to origin ${parsedUrl.origin} was throttled`,
+ );
+ }
+ if (this.requireTls && parsedUrl.protocol !== "https:") {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_NETWORK_ERROR,
+ {
+ requestMethod: method,
+ requestUrl: url,
+ },
+ `request to ${parsedUrl.origin} is not possible with protocol ${parsedUrl.protocol}`,
+ );
+ }
+
+ let data: ArrayBuffer | undefined = undefined;
+ const requestHeadersMap = getDefaultHeaders(method);
+ if (opt?.headers) {
+ Object.entries(opt?.headers).forEach(([key, value]) => {
+ if (value === undefined) return;
+ requestHeadersMap[key] = value
+ })
+ }
+ let headersList: string[] = [];
+ for (let headerName of Object.keys(requestHeadersMap)) {
+ headersList.push(`${headerName}: ${requestHeadersMap[headerName]}`);
+ }
+ if (method === "POST") {
+ data = encodeBody(opt?.body);
+ }
+
+ const cancelPromCap = openPromise<QjsHttpResp>();
+
+ // Just like WHATWG fetch(), the qjs http client doesn't
+ // really support cancellation, so cancellation here just
+ // means that the result is ignored!
+ const fetchProm = qjsOs.fetchHttp(url, {
+ method,
+ data,
+ headers: headersList,
+ });
+
+ let timeoutHandle: any = undefined;
+ let cancelCancelledHandler: (() => void) | undefined = undefined;
+
+ if (opt?.timeout && opt.timeout.d_ms !== "forever") {
+ timeoutHandle = setTimeout(() => {
+ cancelPromCap.reject(new RequestTimeoutError());
+ }, opt.timeout.d_ms);
+ }
+
+ if (opt?.cancellationToken) {
+ cancelCancelledHandler = opt.cancellationToken.onCancelled(() => {
+ cancelPromCap.reject(new RequestCancelledError());
+ });
+ }
+
+ let res: QjsHttpResp;
+ try {
+ res = await Promise.race([fetchProm, cancelPromCap.promise]);
+ } catch (e) {
+ if (e instanceof RequestCancelledError) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
+ {
+ requestUrl: url,
+ requestMethod: method,
+ httpStatusCode: 0,
+ },
+ `Request cancelled`,
+ );
+ }
+ if (e instanceof RequestTimeoutError) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
+ {
+ requestUrl: url,
+ requestMethod: method,
+ httpStatusCode: 0,
+ },
+ `Request timed out`,
+ );
+ }
+ throw e;
+ }
+
+ if (timeoutHandle != null) {
+ clearTimeout(timeoutHandle);
+ }
+
+ if (cancelCancelledHandler != null) {
+ cancelCancelledHandler();
+ }
+
+ const headers: Headers = new Headers();
+
+ if (res.headers) {
+ for (const headerStr of res.headers) {
+ const splitPos = headerStr.indexOf(":");
+ if (splitPos < 0) {
+ continue;
+ }
+ const headerName = headerStr.slice(0, splitPos).trim().toLowerCase();
+ const headerValue = headerStr.slice(splitPos + 1).trim();
+ headers.set(headerName, headerValue);
+ }
+ }
+
+ return {
+ requestMethod: method,
+ headers,
+ async bytes() {
+ return res.data;
+ },
+ json() {
+ const text = textDecoder.decode(res.data);
+ return JSON.parse(text);
+ },
+ async text() {
+ const text = textDecoder.decode(res.data);
+ return text;
+ },
+ requestUrl: url,
+ status: res.status,
+ };
+ }
+}
diff --git a/packages/taler-util/src/http.ts b/packages/taler-util/src/http.ts
new file mode 100644
index 000000000..8bf10d0e2
--- /dev/null
+++ b/packages/taler-util/src/http.ts
@@ -0,0 +1,37 @@
+/*
+ This file is part of TALER
+ (C) 2016 GNUnet e.V.
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Helpers for doing XMLHttpRequest-s that are based on ES6 promises.
+ * Allows for easy mocking for test cases.
+ *
+ * The API is inspired by the HTML5 fetch API.
+ */
+
+/**
+ * Imports
+ */
+
+import * as impl from "#http-impl";
+import * as common from "./http-common.js";
+
+export * from "./http-common.js";
+
+export function createPlatformHttpLib(
+ args?: common.HttpLibArgs,
+): common.HttpRequestLibrary {
+ return new impl.HttpLibImpl(args);
+}
diff --git a/packages/taler-util/src/i18n.ts b/packages/taler-util/src/i18n.ts
index 001735325..f43f543ea 100644
--- a/packages/taler-util/src/i18n.ts
+++ b/packages/taler-util/src/i18n.ts
@@ -10,7 +10,7 @@ export let jed: any = undefined;
* Set up jed library for internationalization,
* based on browser language settings.
*/
-export function setupI18n(lang: string, strings: { [s: string]: any }): any {
+export function setupI18n(lang: string, strings: { [s: string]: any }): void {
lang = lang.replace("_", "-");
if (!strings[lang]) {
@@ -28,10 +28,13 @@ export function internalSetStrings(langStrings: any): void {
jed = new jedLib.Jed(langStrings);
}
+declare const __translated: unique symbol;
+export type TranslatedString = string & { [__translated]: true };
+
/**
* Convert template strings to a msgid
*/
-function toI18nString(stringSeq: ReadonlyArray<string>): string {
+function toI18nString(stringSeq: ReadonlyArray<string>): TranslatedString {
let s = "";
for (let i = 0; i < stringSeq.length; i++) {
s += stringSeq[i];
@@ -39,7 +42,7 @@ function toI18nString(stringSeq: ReadonlyArray<string>): string {
s += `%${i + 1}$s`;
}
}
- return s;
+ return s as TranslatedString;
}
/**
@@ -48,7 +51,7 @@ function toI18nString(stringSeq: ReadonlyArray<string>): string {
export function singular(
stringSeq: TemplateStringsArray,
...values: any[]
-): string {
+): TranslatedString {
const s = toI18nString(stringSeq);
const tr = jed
.translate(s)
@@ -63,10 +66,10 @@ export function singular(
export function translate(
stringSeq: TemplateStringsArray,
...values: any[]
-): any[] {
+): TranslatedString[] {
const s = toI18nString(stringSeq);
if (!s) return [];
- const translation: string = jed.ngettext(s, s, 1);
+ const translation: TranslatedString = jed.ngettext(s, s, 1);
return replacePlaceholderWithValues(translation, values);
}
@@ -83,7 +86,7 @@ export function Translate({
const c = [].concat(children);
const s = stringifyArray(c);
if (!s) return [];
- const translation: string = jed.ngettext(s, s, 1);
+ const translation: TranslatedString = jed.ngettext(s, s, 1);
if (debug) {
console.log("looking for ", s, "got", translation);
}
@@ -104,12 +107,12 @@ export function getJsonI18n<K extends string>(
export function getTranslatedArray(array: Array<any>) {
const s = stringifyArray(array);
- const translation: string = jed.ngettext(s, s, 1);
+ const translation: TranslatedString = jed.ngettext(s, s, 1);
return replacePlaceholderWithValues(translation, array);
}
function replacePlaceholderWithValues(
- translation: string,
+ translation: TranslatedString,
childArray: Array<any>,
): Array<any> {
const tr = translation.split(/%(\d+)\$s/);
diff --git a/packages/merchant-backoffice-ui/src/context/instance.ts b/packages/taler-util/src/iban.test.ts
index fecf36426..a00e3b50a 100644
--- a/packages/merchant-backoffice-ui/src/context/instance.ts
+++ b/packages/taler-util/src/iban.test.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2023 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -14,22 +14,17 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+import test from "ava";
+import { generateIban, validateIban } from "./iban.js";
-import { createContext } from 'preact'
-import { useContext } from 'preact/hooks'
+test("iban validation", (t) => {
+ t.assert(validateIban("foo").type === "invalid");
+ t.assert(validateIban("NL71RABO9996666778").type === "valid");
+ t.assert(validateIban("NL71RABO9996666779").type === "invalid");
+});
-interface Type {
- id: string;
- token?: string;
- admin?: boolean;
- changeToken: (t?:string) => void;
-}
-
-const Context = createContext<Type>({} as any)
-
-export const InstanceContextProvider = Context.Provider
-export const useInstanceContext = (): Type => useContext(Context);
+test("iban generation", (t) => {
+ let iban1 = generateIban("DE", 10);
+ console.log("generated IBAN", iban1);
+ t.assert(validateIban(iban1).type === "valid");
+});
diff --git a/packages/taler-util/src/iban.ts b/packages/taler-util/src/iban.ts
new file mode 100644
index 000000000..d386f90e0
--- /dev/null
+++ b/packages/taler-util/src/iban.ts
@@ -0,0 +1,296 @@
+/*
+ This file is part of GNU Taler
+ (C) 2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * IBAN validation.
+ *
+ * Currently only validates the checksum.
+ *
+ * It does not validate:
+ * - Country-specific length
+ * - Country-specific checksums
+ *
+ * The country list is also not complete.
+ *
+ * @author Florian Dold <dold@taler.net>
+ */
+
+export type IbanValidationResult =
+ | { type: "invalid" }
+ | {
+ type: "valid";
+ normalizedIban: string;
+ };
+
+export interface IbanCountryInfo {
+ name: string;
+ isSepa?: boolean;
+ length?: number;
+}
+
+/**
+ * Incomplete list, see https://www.swift.com/resource/iban-registry-pdf
+ */
+export const ibanCountryInfoTable: Record<string, IbanCountryInfo> = {
+ AE: { name: "U.A.E." },
+ AF: { name: "Afghanistan" },
+ AL: { name: "Albania" },
+ AM: { name: "Armenia" },
+ AN: { name: "Netherlands Antilles" },
+ AR: { name: "Argentina" },
+ AT: { name: "Austria" },
+ AU: { name: "Australia" },
+ AZ: { name: "Azerbaijan" },
+ BA: { name: "Bosnia and Herzegovina" },
+ BD: { name: "Bangladesh" },
+ BE: { name: "Belgium" },
+ BG: { name: "Bulgaria" },
+ BH: { name: "Bahrain" },
+ BN: { name: "Brunei Darussalam" },
+ BO: { name: "Bolivia" },
+ BR: { name: "Brazil" },
+ BT: { name: "Bhutan" },
+ BY: { name: "Belarus" },
+ BZ: { name: "Belize" },
+ CA: { name: "Canada" },
+ CG: { name: "Congo" },
+ CH: { name: "Switzerland" },
+ CI: { name: "Cote d'Ivoire" },
+ CL: { name: "Chile" },
+ CM: { name: "Cameroon" },
+ CN: { name: "People's Republic of China" },
+ CO: { name: "Colombia" },
+ CR: { name: "Costa Rica" },
+ CS: { name: "Serbia and Montenegro" },
+ CZ: { name: "Czech Republic" },
+ DE: { name: "Germany" },
+ DK: { name: "Denmark" },
+ DO: { name: "Dominican Republic" },
+ DZ: { name: "Algeria" },
+ EC: { name: "Ecuador" },
+ EE: { name: "Estonia" },
+ EG: { name: "Egypt" },
+ ER: { name: "Eritrea" },
+ ES: { name: "Spain" },
+ ET: { name: "Ethiopia" },
+ FI: { name: "Finland" },
+ FO: { name: "Faroe Islands" },
+ FR: { name: "France" },
+ GB: { name: "United Kingdom" },
+ GD: { name: "Caribbean" },
+ GE: { name: "Georgia" },
+ GL: { name: "Greenland" },
+ GR: { name: "Greece" },
+ GT: { name: "Guatemala" },
+ HK: { name: "Hong Kong S.A.R." },
+ HN: { name: "Honduras" },
+ HR: { name: "Croatia" },
+ HT: { name: "Haiti" },
+ HU: { name: "Hungary" },
+ ID: { name: "Indonesia" },
+ IE: { name: "Ireland" },
+ IL: { name: "Israel" },
+ IN: { name: "India" },
+ IQ: { name: "Iraq" },
+ IR: { name: "Iran" },
+ IS: { name: "Iceland" },
+ IT: { name: "Italy" },
+ JM: { name: "Jamaica" },
+ JO: { name: "Jordan" },
+ JP: { name: "Japan" },
+ KE: { name: "Kenya" },
+ KG: { name: "Kyrgyzstan" },
+ KH: { name: "Cambodia" },
+ KR: { name: "South Korea" },
+ KW: { name: "Kuwait" },
+ KZ: { name: "Kazakhstan" },
+ LA: { name: "Laos" },
+ LB: { name: "Lebanon" },
+ LI: { name: "Liechtenstein" },
+ LK: { name: "Sri Lanka" },
+ LT: { name: "Lithuania" },
+ LU: { name: "Luxembourg" },
+ LV: { name: "Latvia" },
+ LY: { name: "Libya" },
+ MA: { name: "Morocco" },
+ MC: { name: "Principality of Monaco" },
+ MD: { name: "Moldava" },
+ ME: { name: "Montenegro" },
+ MK: { name: "Former Yugoslav Republic of Macedonia" },
+ ML: { name: "Mali" },
+ MM: { name: "Myanmar" },
+ MN: { name: "Mongolia" },
+ MO: { name: "Macau S.A.R." },
+ MT: { name: "Malta" },
+ MV: { name: "Maldives" },
+ MX: { name: "Mexico" },
+ MY: { name: "Malaysia" },
+ NG: { name: "Nigeria" },
+ NI: { name: "Nicaragua" },
+ NL: { name: "Netherlands" },
+ NO: { name: "Norway" },
+ NP: { name: "Nepal" },
+ NZ: { name: "New Zealand" },
+ OM: { name: "Oman" },
+ PA: { name: "Panama" },
+ PE: { name: "Peru" },
+ PH: { name: "Philippines" },
+ PK: { name: "Islamic Republic of Pakistan" },
+ PL: { name: "Poland" },
+ PR: { name: "Puerto Rico" },
+ PT: { name: "Portugal" },
+ PY: { name: "Paraguay" },
+ QA: { name: "Qatar" },
+ RE: { name: "Reunion" },
+ RO: { name: "Romania" },
+ RS: { name: "Serbia" },
+ RU: { name: "Russia" },
+ RW: { name: "Rwanda" },
+ SA: { name: "Saudi Arabia" },
+ SE: { name: "Sweden" },
+ SG: { name: "Singapore" },
+ SI: { name: "Slovenia" },
+ SK: { name: "Slovak" },
+ SN: { name: "Senegal" },
+ SO: { name: "Somalia" },
+ SR: { name: "Suriname" },
+ SV: { name: "El Salvador" },
+ SY: { name: "Syria" },
+ TH: { name: "Thailand" },
+ TJ: { name: "Tajikistan" },
+ TM: { name: "Turkmenistan" },
+ TN: { name: "Tunisia" },
+ TR: { name: "Turkey" },
+ TT: { name: "Trinidad and Tobago" },
+ TW: { name: "Taiwan" },
+ TZ: { name: "Tanzania" },
+ UA: { name: "Ukraine" },
+ US: { name: "United States" },
+ UY: { name: "Uruguay" },
+ VA: { name: "Vatican" },
+ VE: { name: "Venezuela" },
+ VN: { name: "Viet Nam" },
+ YE: { name: "Yemen" },
+ ZA: { name: "South Africa" },
+ ZW: { name: "Zimbabwe" },
+};
+
+let ccZero = "0".charCodeAt(0);
+let ccNine = "9".charCodeAt(0);
+let ccA = "A".charCodeAt(0);
+let ccZ = "Z".charCodeAt(0);
+
+/**
+ * Append a IBAN digit(s) based on a char code.
+ */
+function appendDigit(digits: number[], cc: number): boolean {
+ if (cc >= ccZero && cc <= ccNine) {
+ digits.push(cc - ccZero);
+ } else if (cc >= ccA && cc <= ccZ) {
+ const n = cc - ccA + 10;
+ digits.push(Math.floor(n / 10) % 10);
+ digits.push(n % 10);
+ } else {
+ return false;
+ }
+ return true;
+}
+
+/**
+ * Compute MOD-97-10 as per ISO/IEC 7064:2003.
+ */
+function mod97(digits: number[]): number {
+ let i = 0;
+ let modAccum = 0;
+ while (i < digits.length) {
+ let n = 0;
+ while (n < 9 && i < digits.length) {
+ modAccum = modAccum * 10 + digits[i];
+ i++;
+ n++;
+ }
+ modAccum = modAccum % 97;
+ }
+ return modAccum;
+}
+
+export function validateIban(ibanString: string): IbanValidationResult {
+ let myIban = ibanString.toLocaleUpperCase().replace(" ", "");
+ let countryCode = myIban.substring(0, 2);
+ let countryInfo = ibanCountryInfoTable[countryCode];
+
+ if (!countryInfo) {
+ return {
+ type: "invalid",
+ };
+ }
+
+ let digits: number[] = [];
+
+ for (let i = 4; i < myIban.length; i++) {
+ const cc = myIban.charCodeAt(i);
+ if (!appendDigit(digits, cc)) {
+ return {
+ type: "invalid",
+ };
+ }
+ }
+
+ for (let i = 0; i < 4; i++) {
+ if (!appendDigit(digits, ibanString.charCodeAt(i))) {
+ return {
+ type: "invalid",
+ };
+ }
+ }
+
+ const rem = mod97(digits);
+ if (rem === 1) {
+ return {
+ type: "valid",
+ normalizedIban: myIban,
+ };
+ } else {
+ return {
+ type: "invalid",
+ };
+ }
+}
+
+export function generateIban(countryCode: string, length: number): string {
+ let ibanSuffix = "";
+ let digits: number[] = [];
+
+ for (let i = 0; i < length; i++) {
+ const cc = ccZero + (Math.floor(Math.random() * 100) % 10);
+ appendDigit(digits, cc);
+ ibanSuffix += String.fromCharCode(cc);
+ }
+
+ appendDigit(digits, countryCode.charCodeAt(0));
+ appendDigit(digits, countryCode.charCodeAt(1));
+
+ // Try using "00" as check digits
+ appendDigit(digits, ccZero);
+ appendDigit(digits, ccZero);
+
+ const requiredChecksum = 98 - mod97(digits);
+
+ const checkDigit1 = Math.floor(requiredChecksum / 10) % 10;
+ const checkDigit2 = requiredChecksum % 10;
+
+ return countryCode + checkDigit1 + checkDigit2 + ibanSuffix;
+}
diff --git a/packages/taler-util/src/index.browser.ts b/packages/taler-util/src/index.browser.ts
index 3b8e194b3..ec77b10c0 100644
--- a/packages/taler-util/src/index.browser.ts
+++ b/packages/taler-util/src/index.browser.ts
@@ -19,3 +19,7 @@
import { loadBrowserPrng } from "./prng-browser.js";
loadBrowserPrng();
export * from "./index.js";
+
+// The web stuff doesn't support package.json export declarations yet,
+// so we export more stuff here than we should.
+export * from "./http-common.js";
diff --git a/packages/taler-util/src/index.node.ts b/packages/taler-util/src/index.node.ts
index bd59f320a..ba4c6cf4e 100644
--- a/packages/taler-util/src/index.node.ts
+++ b/packages/taler-util/src/index.node.ts
@@ -21,4 +21,4 @@ initNodePrng();
export * from "./index.js";
export * from "./talerconfig.js";
export * from "./globbing/minimatch.js";
-export { clk } from "./clk.js";
+export { setPrintHttpRequestAsCurl } from "./http-impl.node.js";
diff --git a/packages/taler-util/src/index.qtart.ts b/packages/taler-util/src/index.qtart.ts
new file mode 100644
index 000000000..ddb9bcfd4
--- /dev/null
+++ b/packages/taler-util/src/index.qtart.ts
@@ -0,0 +1,27 @@
+/*
+ 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/>
+ */
+
+import { setPRNG } from "./nacl-fast.js";
+
+setPRNG(function (x: Uint8Array, n: number) {
+ // @ts-ignore
+ const va = globalThis._tart.randomBytes(n);
+ const v = new Uint8Array(va);
+ for (let i = 0; i < n; i++) x[i] = v[i];
+ for (let i = 0; i < v.length; i++) v[i] = 0;
+});
+
+export * from "./index.js";
diff --git a/packages/taler-util/src/index.ts b/packages/taler-util/src/index.ts
index 2f674d097..24d6e9950 100644
--- a/packages/taler-util/src/index.ts
+++ b/packages/taler-util/src/index.ts
@@ -2,36 +2,62 @@ import { TalerErrorCode } from "./taler-error-codes.js";
export { TalerErrorCode };
+export * from "./CancellationToken.js";
+export * from "./MerchantApiClient.js";
+export { RequestThrottler } from "./RequestThrottler.js";
+export * from "./ReserveStatus.js";
+export * from "./ReserveTransaction.js";
+export { TaskThrottler } from "./TaskThrottler.js";
export * from "./amounts.js";
export * from "./backup-types.js";
+export * from "./bank-api-client.js";
+export * from "./base64.js";
+export * from "./bitcoin.js";
export * from "./codec.js";
+export * from "./contract-terms.js";
+export * from "./errors.js";
+export { fnutil } from "./fnutils.js";
export * from "./helpers.js";
-export * from "./libtool-version.js";
-export * from "./notifications.js";
-export * from "./payto.js";
-export * from "./ReserveStatus.js";
-export * from "./ReserveTransaction.js";
-export * from "./taler-types.js";
-export * from "./taleruri.js";
-export * from "./time.js";
-export * from "./transactions-types.js";
-export * from "./wallet-types.js";
+export * from "./http-client/bank-conversion.js";
+export * from "./http-client/authentication.js";
+export * from "./http-client/bank-core.js";
+export * from "./http-client/merchant.js";
+export * from "./http-client/challenger.js";
+export * from "./http-client/bank-integration.js";
+export * from "./http-client/bank-revenue.js";
+export * from "./http-client/bank-wire.js";
+export * from "./http-client/exchange.js";
+export { CacheEvictor } from "./http-client/utils.js";
+export * from "./http-client/officer-account.js";
+export * from "./http-client/types.js";
+export * from "./http-status-codes.js";
export * from "./i18n.js";
-export * from "./logging.js";
-export * from "./url.js";
-export { fnutil } from "./fnutils.js";
+export * from "./iban.js";
+export * from "./invariants.js";
export * from "./kdf.js";
-export * from "./taler-crypto.js";
-export * from "./http-status-codes.js";
-export * from "./bitcoin.js";
+export * from "./libeufin-api-types.js";
+export * from "./libtool-version.js";
+export * from "./logging.js";
+export * from "./merchant-api-types.js";
export {
+ crypto_sign_keyPair_fromSeed,
randomBytes,
secretbox,
secretbox_open,
- crypto_sign_keyPair_fromSeed,
setPRNG,
} from "./nacl-fast.js";
-export { RequestThrottler } from "./RequestThrottler.js";
-export * from "./CancellationToken.js";
-export * from "./contract-terms.js";
-export * from "./base64.js";
+export * from "./notifications.js";
+export * from "./observability.js";
+export * from "./operation.js";
+export * from "./payto.js";
+export * from "./promises.js";
+export * from "./rfc3548.js";
+export * from "./taler-crypto.js";
+export * from "./taler-types.js";
+export * from "./taleruri.js";
+export * from "./time.js";
+export * from "./timer.js";
+export * from "./transaction-test-data.js";
+export * from "./transactions-types.js";
+export * from "./url.js";
+export * from "./wallet-types.js";
diff --git a/packages/taler-wallet-core/src/util/invariants.ts b/packages/taler-util/src/invariants.ts
index 3598d857c..c6e9b8113 100644
--- a/packages/taler-wallet-core/src/util/invariants.ts
+++ b/packages/taler-util/src/invariants.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2020 Taler Systems S.A.
+ (C) 2019-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -14,6 +14,13 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+/**
+ * Helpers for invariants.
+ */
+
+/**
+ * An invariant has been violated.
+ */
export class InvariantViolatedError extends Error {
constructor(message?: string) {
super(message);
@@ -22,9 +29,10 @@ export class InvariantViolatedError extends Error {
}
/**
- * Helpers for invariants.
+ * Check a database invariant.
+ *
+ * A violation of this invariant means that the database is inconsistent.
*/
-
export function checkDbInvariant(b: boolean, m?: string): asserts b {
if (!b) {
if (m) {
@@ -35,6 +43,11 @@ export function checkDbInvariant(b: boolean, m?: string): asserts b {
}
}
+/**
+ * Check a logic invariant.
+ *
+ * A violation of this invariant means that there is a logic bug in the program.
+ */
export function checkLogicInvariant(b: boolean, m?: string): asserts b {
if (!b) {
if (m) {
diff --git a/packages/taler-util/src/iso-4217.ts b/packages/taler-util/src/iso-4217.ts
new file mode 100644
index 000000000..b155676ff
--- /dev/null
+++ b/packages/taler-util/src/iso-4217.ts
@@ -0,0 +1,1717 @@
+/*
+ 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/>
+ */
+
+// From https://en.wikipedia.org/wiki/ISO_4217
+
+//modifications to the original data
+// * currency without decimal represented with 0
+// * removed 4 with label "No universal currency"
+// * numeric as number
+// * removed all field except:
+// - c: currency name
+// - a: alphabetic code
+// - n: numeric code
+// - d: minor unit
+type CurrencyInfo = {
+ /**
+ * name
+ */
+ c: string;
+ /**
+ * alphabetic code
+ */
+ a: string;
+ /**
+ * numeric code
+ */
+ n: number;
+ /**
+ * minor unit
+ * "0" means that there is no minor unit for that currency, whereas "1", "2"
+ * and "3" signify a ratio of 10:1, 100:1 and 1000:1 respectively.
+ */
+ d: number;
+};
+export const data: Array<CurrencyInfo> = [
+ {
+ c: "Afghani",
+ a: "AFN",
+ n: 971,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Lek",
+ a: "ALL",
+ n: 8,
+ d: 2,
+ },
+ {
+ c: "Algerian Dinar",
+ a: "DZD",
+ n: 12,
+ d: 2,
+ },
+ {
+ c: "US Dollar",
+ a: "USD",
+ n: 840,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Kwanza",
+ a: "AOA",
+ n: 973,
+ d: 2,
+ },
+ {
+ c: "East Caribbean Dollar",
+ a: "XCD",
+ n: 951,
+ d: 2,
+ },
+ {
+ c: "East Caribbean Dollar",
+ a: "XCD",
+ n: 951,
+ d: 2,
+ },
+ {
+ c: "Argentine Peso",
+ a: "ARS",
+ n: 32,
+ d: 2,
+ },
+ {
+ c: "Armenian Dram",
+ a: "AMD",
+ n: 51,
+ d: 2,
+ },
+ {
+ c: "Aruban Florin",
+ a: "AWG",
+ n: 533,
+ d: 2,
+ },
+ {
+ c: "Australian Dollar",
+ a: "AUD",
+ n: 36,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Azerbaijan Manat",
+ a: "AZN",
+ n: 944,
+ d: 2,
+ },
+ {
+ c: "Bahamian Dollar",
+ a: "BSD",
+ n: 44,
+ d: 2,
+ },
+ {
+ c: "Bahraini Dinar",
+ a: "BHD",
+ n: 48,
+ d: 3,
+ },
+ {
+ c: "Taka",
+ a: "BDT",
+ n: 50,
+ d: 2,
+ },
+ {
+ c: "Barbados Dollar",
+ a: "BBD",
+ n: 52,
+ d: 2,
+ },
+ {
+ c: "Belarusian Ruble",
+ a: "BYN",
+ n: 933,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Belize Dollar",
+ a: "BZD",
+ n: 84,
+ d: 2,
+ },
+ {
+ c: "CFA Franc BCEAO",
+ a: "XOF",
+ n: 952,
+ d: 0,
+ },
+ {
+ c: "Bermudian Dollar",
+ a: "BMD",
+ n: 60,
+ d: 2,
+ },
+ {
+ c: "Indian Rupee",
+ a: "INR",
+ n: 356,
+ d: 2,
+ },
+ {
+ c: "Ngultrum",
+ a: "BTN",
+ n: 64,
+ d: 2,
+ },
+ {
+ c: "Boliviano",
+ a: "BOB",
+ n: 68,
+ d: 2,
+ },
+ {
+ c: "Mvdol",
+ a: "BOV",
+ n: 984,
+ d: 2,
+ },
+ {
+ c: "US Dollar",
+ a: "USD",
+ n: 840,
+ d: 2,
+ },
+ {
+ c: "Convertible Mark",
+ a: "BAM",
+ n: 977,
+ d: 2,
+ },
+ {
+ c: "Pula",
+ a: "BWP",
+ n: 72,
+ d: 2,
+ },
+ {
+ c: "Norwegian Krone",
+ a: "NOK",
+ n: 578,
+ d: 2,
+ },
+ {
+ c: "Brazilian Real",
+ a: "BRL",
+ n: 986,
+ d: 2,
+ },
+ {
+ c: "US Dollar",
+ a: "USD",
+ n: 840,
+ d: 2,
+ },
+ {
+ c: "Brunei Dollar",
+ a: "BND",
+ n: 96,
+ d: 2,
+ },
+ {
+ c: "Bulgarian Lev",
+ a: "BGN",
+ n: 975,
+ d: 2,
+ },
+ {
+ c: "CFA Franc BCEAO",
+ a: "XOF",
+ n: 952,
+ d: 0,
+ },
+ {
+ c: "Burundi Franc",
+ a: "BIF",
+ n: 108,
+ d: 0,
+ },
+ {
+ c: "Cabo Verde Escudo",
+ a: "CVE",
+ n: 132,
+ d: 2,
+ },
+ {
+ c: "Riel",
+ a: "KHR",
+ n: 116,
+ d: 2,
+ },
+ {
+ c: "CFA Franc BEAC",
+ a: "XAF",
+ n: 950,
+ d: 0,
+ },
+ {
+ c: "Canadian Dollar",
+ a: "CAD",
+ n: 124,
+ d: 2,
+ },
+ {
+ c: "Cayman Islands Dollar",
+ a: "KYD",
+ n: 136,
+ d: 2,
+ },
+ {
+ c: "CFA Franc BEAC",
+ a: "XAF",
+ n: 950,
+ d: 0,
+ },
+ {
+ c: "CFA Franc BEAC",
+ a: "XAF",
+ n: 950,
+ d: 0,
+ },
+ {
+ c: "Chilean Peso",
+ a: "CLP",
+ n: 152,
+ d: 0,
+ },
+ {
+ c: "Unidad de Fomento",
+ a: "CLF",
+ n: 990,
+ d: 4,
+ },
+ {
+ c: "Yuan Renminbi",
+ a: "CNY",
+ n: 156,
+ d: 2,
+ },
+ {
+ c: "Australian Dollar",
+ a: "AUD",
+ n: 36,
+ d: 2,
+ },
+ {
+ c: "Australian Dollar",
+ a: "AUD",
+ n: 36,
+ d: 2,
+ },
+ {
+ c: "Colombian Peso",
+ a: "COP",
+ n: 170,
+ d: 2,
+ },
+ {
+ c: "Unidad de Valor Real",
+ a: "COU",
+ n: 970,
+ d: 2,
+ },
+ {
+ c: "Comorian Franc",
+ a: "KMF",
+ n: 174,
+ d: 0,
+ },
+ {
+ c: "Congolese Franc",
+ a: "CDF",
+ n: 976,
+ d: 2,
+ },
+ {
+ c: "CFA Franc BEAC",
+ a: "XAF",
+ n: 950,
+ d: 0,
+ },
+ {
+ c: "New Zealand Dollar",
+ a: "NZD",
+ n: 554,
+ d: 2,
+ },
+ {
+ c: "Costa Rican Colon",
+ a: "CRC",
+ n: 188,
+ d: 2,
+ },
+ {
+ c: "CFA Franc BCEAO",
+ a: "XOF",
+ n: 952,
+ d: 0,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Cuban Peso",
+ a: "CUP",
+ n: 192,
+ d: 2,
+ },
+ {
+ c: "Peso Convertible",
+ a: "CUC",
+ n: 931,
+ d: 2,
+ },
+ {
+ c: "Netherlands Antillean Guilder",
+ a: "ANG",
+ n: 532,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Czech Koruna",
+ a: "CZK",
+ n: 203,
+ d: 2,
+ },
+ {
+ c: "Danish Krone",
+ a: "DKK",
+ n: 208,
+ d: 2,
+ },
+ {
+ c: "Djibouti Franc",
+ a: "DJF",
+ n: 262,
+ d: 0,
+ },
+ {
+ c: "East Caribbean Dollar",
+ a: "XCD",
+ n: 951,
+ d: 2,
+ },
+ {
+ c: "Dominican Peso",
+ a: "DOP",
+ n: 214,
+ d: 2,
+ },
+ {
+ c: "US Dollar",
+ a: "USD",
+ n: 840,
+ d: 2,
+ },
+ {
+ c: "Egyptian Pound",
+ a: "EGP",
+ n: 818,
+ d: 2,
+ },
+ {
+ c: "El Salvador Colon",
+ a: "SVC",
+ n: 222,
+ d: 2,
+ },
+ {
+ c: "US Dollar",
+ a: "USD",
+ n: 840,
+ d: 2,
+ },
+ {
+ c: "CFA Franc BEAC",
+ a: "XAF",
+ n: 950,
+ d: 0,
+ },
+ {
+ c: "Nakfa",
+ a: "ERN",
+ n: 232,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Lilangeni",
+ a: "SZL",
+ n: 748,
+ d: 2,
+ },
+ {
+ c: "Ethiopian Birr",
+ a: "ETB",
+ n: 230,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Falkland Islands Pound",
+ a: "FKP",
+ n: 238,
+ d: 2,
+ },
+ {
+ c: "Danish Krone",
+ a: "DKK",
+ n: 208,
+ d: 2,
+ },
+ {
+ c: "Fiji Dollar",
+ a: "FJD",
+ n: 242,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "CFP Franc",
+ a: "XPF",
+ n: 953,
+ d: 0,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "CFA Franc BEAC",
+ a: "XAF",
+ n: 950,
+ d: 0,
+ },
+ {
+ c: "Dalasi",
+ a: "GMD",
+ n: 270,
+ d: 2,
+ },
+ {
+ c: "Lari",
+ a: "GEL",
+ n: 981,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Ghana Cedi",
+ a: "GHS",
+ n: 936,
+ d: 2,
+ },
+ {
+ c: "Gibraltar Pound",
+ a: "GIP",
+ n: 292,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Danish Krone",
+ a: "DKK",
+ n: 208,
+ d: 2,
+ },
+ {
+ c: "East Caribbean Dollar",
+ a: "XCD",
+ n: 951,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "US Dollar",
+ a: "USD",
+ n: 840,
+ d: 2,
+ },
+ {
+ c: "Quetzal",
+ a: "GTQ",
+ n: 320,
+ d: 2,
+ },
+ {
+ c: "Pound Sterling",
+ a: "GBP",
+ n: 826,
+ d: 2,
+ },
+ {
+ c: "Guinean Franc",
+ a: "GNF",
+ n: 324,
+ d: 0,
+ },
+ {
+ c: "CFA Franc BCEAO",
+ a: "XOF",
+ n: 952,
+ d: 0,
+ },
+ {
+ c: "Guyana Dollar",
+ a: "GYD",
+ n: 328,
+ d: 2,
+ },
+ {
+ c: "Gourde",
+ a: "HTG",
+ n: 332,
+ d: 2,
+ },
+ {
+ c: "US Dollar",
+ a: "USD",
+ n: 840,
+ d: 2,
+ },
+ {
+ c: "Australian Dollar",
+ a: "AUD",
+ n: 36,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Lempira",
+ a: "HNL",
+ n: 340,
+ d: 2,
+ },
+ {
+ c: "Hong Kong Dollar",
+ a: "HKD",
+ n: 344,
+ d: 2,
+ },
+ {
+ c: "Forint",
+ a: "HUF",
+ n: 348,
+ d: 2,
+ },
+ {
+ c: "Iceland Krona",
+ a: "ISK",
+ n: 352,
+ d: 0,
+ },
+ {
+ c: "Indian Rupee",
+ a: "INR",
+ n: 356,
+ d: 2,
+ },
+ {
+ c: "Rupiah",
+ a: "IDR",
+ n: 360,
+ d: 2,
+ },
+ {
+ c: "SDR (Special Drawing Right)",
+ a: "XDR",
+ n: 960,
+ d: 0,
+ },
+ {
+ c: "Iranian Rial",
+ a: "IRR",
+ n: 364,
+ d: 2,
+ },
+ {
+ c: "Iraqi Dinar",
+ a: "IQD",
+ n: 368,
+ d: 3,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Pound Sterling",
+ a: "GBP",
+ n: 826,
+ d: 2,
+ },
+ {
+ c: "New Israeli Sheqel",
+ a: "ILS",
+ n: 376,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Jamaican Dollar",
+ a: "JMD",
+ n: 388,
+ d: 2,
+ },
+ {
+ c: "Yen",
+ a: "JPY",
+ n: 392,
+ d: 0,
+ },
+ {
+ c: "Pound Sterling",
+ a: "GBP",
+ n: 826,
+ d: 2,
+ },
+ {
+ c: "Jordanian Dinar",
+ a: "JOD",
+ n: 400,
+ d: 3,
+ },
+ {
+ c: "Tenge",
+ a: "KZT",
+ n: 398,
+ d: 2,
+ },
+ {
+ c: "Kenyan Shilling",
+ a: "KES",
+ n: 404,
+ d: 2,
+ },
+ {
+ c: "Australian Dollar",
+ a: "AUD",
+ n: 36,
+ d: 2,
+ },
+ {
+ c: "North Korean Won",
+ a: "KPW",
+ n: 408,
+ d: 2,
+ },
+ {
+ c: "Won",
+ a: "KRW",
+ n: 410,
+ d: 0,
+ },
+ {
+ c: "Kuwaiti Dinar",
+ a: "KWD",
+ n: 414,
+ d: 3,
+ },
+ {
+ c: "Som",
+ a: "KGS",
+ n: 417,
+ d: 2,
+ },
+ {
+ c: "Lao Kip",
+ a: "LAK",
+ n: 418,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Lebanese Pound",
+ a: "LBP",
+ n: 422,
+ d: 2,
+ },
+ {
+ c: "Loti",
+ a: "LSL",
+ n: 426,
+ d: 2,
+ },
+ {
+ c: "Rand",
+ a: "ZAR",
+ n: 710,
+ d: 2,
+ },
+ {
+ c: "Liberian Dollar",
+ a: "LRD",
+ n: 430,
+ d: 2,
+ },
+ {
+ c: "Libyan Dinar",
+ a: "LYD",
+ n: 434,
+ d: 3,
+ },
+ {
+ c: "Swiss Franc",
+ a: "CHF",
+ n: 756,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Pataca",
+ a: "MOP",
+ n: 446,
+ d: 2,
+ },
+ {
+ c: "Denar",
+ a: "MKD",
+ n: 807,
+ d: 2,
+ },
+ {
+ c: "Malagasy Ariary",
+ a: "MGA",
+ n: 969,
+ d: 2,
+ },
+ {
+ c: "Malawi Kwacha",
+ a: "MWK",
+ n: 454,
+ d: 2,
+ },
+ {
+ c: "Malaysian Ringgit",
+ a: "MYR",
+ n: 458,
+ d: 2,
+ },
+ {
+ c: "Rufiyaa",
+ a: "MVR",
+ n: 462,
+ d: 2,
+ },
+ {
+ c: "CFA Franc BCEAO",
+ a: "XOF",
+ n: 952,
+ d: 0,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "US Dollar",
+ a: "USD",
+ n: 840,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Ouguiya",
+ a: "MRU",
+ n: 929,
+ d: 2,
+ },
+ {
+ c: "Mauritius Rupee",
+ a: "MUR",
+ n: 480,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "ADB Unit of Account",
+ a: "XUA",
+ n: 965,
+ d: 0,
+ },
+ {
+ c: "Mexican Peso",
+ a: "MXN",
+ n: 484,
+ d: 2,
+ },
+ {
+ c: "Mexican Unidad de Inversion",
+ a: "MXV",
+ n: 979,
+ d: 2,
+ },
+ {
+ c: "US Dollar",
+ a: "USD",
+ n: 840,
+ d: 2,
+ },
+ {
+ c: "Moldovan Leu",
+ a: "MDL",
+ n: 498,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Tugrik",
+ a: "MNT",
+ n: 496,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "East Caribbean Dollar",
+ a: "XCD",
+ n: 951,
+ d: 2,
+ },
+ {
+ c: "Moroccan Dirham",
+ a: "MAD",
+ n: 504,
+ d: 2,
+ },
+ {
+ c: "Mozambique Metical",
+ a: "MZN",
+ n: 943,
+ d: 2,
+ },
+ {
+ c: "Kyat",
+ a: "MMK",
+ n: 104,
+ d: 2,
+ },
+ {
+ c: "Namibia Dollar",
+ a: "NAD",
+ n: 516,
+ d: 2,
+ },
+ {
+ c: "Rand",
+ a: "ZAR",
+ n: 710,
+ d: 2,
+ },
+ {
+ c: "Australian Dollar",
+ a: "AUD",
+ n: 36,
+ d: 2,
+ },
+ {
+ c: "Nepalese Rupee",
+ a: "NPR",
+ n: 524,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "CFP Franc",
+ a: "XPF",
+ n: 953,
+ d: 0,
+ },
+ {
+ c: "New Zealand Dollar",
+ a: "NZD",
+ n: 554,
+ d: 2,
+ },
+ {
+ c: "Cordoba Oro",
+ a: "NIO",
+ n: 558,
+ d: 2,
+ },
+ {
+ c: "CFA Franc BCEAO",
+ a: "XOF",
+ n: 952,
+ d: 0,
+ },
+ {
+ c: "Naira",
+ a: "NGN",
+ n: 566,
+ d: 2,
+ },
+ {
+ c: "New Zealand Dollar",
+ a: "NZD",
+ n: 554,
+ d: 2,
+ },
+ {
+ c: "Australian Dollar",
+ a: "AUD",
+ n: 36,
+ d: 2,
+ },
+ {
+ c: "US Dollar",
+ a: "USD",
+ n: 840,
+ d: 2,
+ },
+ {
+ c: "Norwegian Krone",
+ a: "NOK",
+ n: 578,
+ d: 2,
+ },
+ {
+ c: "Rial Omani",
+ a: "OMR",
+ n: 512,
+ d: 3,
+ },
+ {
+ c: "Pakistan Rupee",
+ a: "PKR",
+ n: 586,
+ d: 2,
+ },
+ {
+ c: "US Dollar",
+ a: "USD",
+ n: 840,
+ d: 2,
+ },
+ {
+ c: "Balboa",
+ a: "PAB",
+ n: 590,
+ d: 2,
+ },
+ {
+ c: "US Dollar",
+ a: "USD",
+ n: 840,
+ d: 2,
+ },
+ {
+ c: "Kina",
+ a: "PGK",
+ n: 598,
+ d: 2,
+ },
+ {
+ c: "Guarani",
+ a: "PYG",
+ n: 600,
+ d: 0,
+ },
+ {
+ c: "Sol",
+ a: "PEN",
+ n: 604,
+ d: 2,
+ },
+ {
+ c: "Philippine Peso",
+ a: "PHP",
+ n: 608,
+ d: 2,
+ },
+ {
+ c: "New Zealand Dollar",
+ a: "NZD",
+ n: 554,
+ d: 2,
+ },
+ {
+ c: "Zloty",
+ a: "PLN",
+ n: 985,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "US Dollar",
+ a: "USD",
+ n: 840,
+ d: 2,
+ },
+ {
+ c: "Qatari Rial",
+ a: "QAR",
+ n: 634,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Romanian Leu",
+ a: "RON",
+ n: 946,
+ d: 2,
+ },
+ {
+ c: "Russian Ruble",
+ a: "RUB",
+ n: 643,
+ d: 2,
+ },
+ {
+ c: "Rwanda Franc",
+ a: "RWF",
+ n: 646,
+ d: 0,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Saint Helena Pound",
+ a: "SHP",
+ n: 654,
+ d: 2,
+ },
+ {
+ c: "East Caribbean Dollar",
+ a: "XCD",
+ n: 951,
+ d: 2,
+ },
+ {
+ c: "East Caribbean Dollar",
+ a: "XCD",
+ n: 951,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "East Caribbean Dollar",
+ a: "XCD",
+ n: 951,
+ d: 2,
+ },
+ {
+ c: "Tala",
+ a: "WST",
+ n: 882,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Dobra",
+ a: "STN",
+ n: 930,
+ d: 2,
+ },
+ {
+ c: "Saudi Riyal",
+ a: "SAR",
+ n: 682,
+ d: 2,
+ },
+ {
+ c: "CFA Franc BCEAO",
+ a: "XOF",
+ n: 952,
+ d: 0,
+ },
+ {
+ c: "Serbian Dinar",
+ a: "RSD",
+ n: 941,
+ d: 2,
+ },
+ {
+ c: "Seychelles Rupee",
+ a: "SCR",
+ n: 690,
+ d: 2,
+ },
+ {
+ c: "Leone",
+ a: "SLL",
+ n: 694,
+ d: 2,
+ },
+ {
+ c: "Leone",
+ a: "SLE",
+ n: 925,
+ d: 2,
+ },
+ {
+ c: "Singapore Dollar",
+ a: "SGD",
+ n: 702,
+ d: 2,
+ },
+ {
+ c: "Netherlands Antillean Guilder",
+ a: "ANG",
+ n: 532,
+ d: 2,
+ },
+ {
+ c: "Sucre",
+ a: "XSU",
+ n: 994,
+ d: 0,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Solomon Islands Dollar",
+ a: "SBD",
+ n: 90,
+ d: 2,
+ },
+ {
+ c: "Somali Shilling",
+ a: "SOS",
+ n: 706,
+ d: 2,
+ },
+ {
+ c: "Rand",
+ a: "ZAR",
+ n: 710,
+ d: 2,
+ },
+ {
+ c: "South Sudanese Pound",
+ a: "SSP",
+ n: 728,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Sri Lanka Rupee",
+ a: "LKR",
+ n: 144,
+ d: 2,
+ },
+ {
+ c: "Sudanese Pound",
+ a: "SDG",
+ n: 938,
+ d: 2,
+ },
+ {
+ c: "Surinam Dollar",
+ a: "SRD",
+ n: 968,
+ d: 2,
+ },
+ {
+ c: "Norwegian Krone",
+ a: "NOK",
+ n: 578,
+ d: 2,
+ },
+ {
+ c: "Swedish Krona",
+ a: "SEK",
+ n: 752,
+ d: 2,
+ },
+ {
+ c: "Swiss Franc",
+ a: "CHF",
+ n: 756,
+ d: 2,
+ },
+ {
+ c: "WIR Euro",
+ a: "CHE",
+ n: 947,
+ d: 2,
+ },
+ {
+ c: "WIR Franc",
+ a: "CHW",
+ n: 948,
+ d: 2,
+ },
+ {
+ c: "Syrian Pound",
+ a: "SYP",
+ n: 760,
+ d: 2,
+ },
+ {
+ c: "New Taiwan Dollar",
+ a: "TWD",
+ n: 901,
+ d: 2,
+ },
+ {
+ c: "Somoni",
+ a: "TJS",
+ n: 972,
+ d: 2,
+ },
+ {
+ c: "Tanzanian Shilling",
+ a: "TZS",
+ n: 834,
+ d: 2,
+ },
+ {
+ c: "Baht",
+ a: "THB",
+ n: 764,
+ d: 2,
+ },
+ {
+ c: "US Dollar",
+ a: "USD",
+ n: 840,
+ d: 2,
+ },
+ {
+ c: "CFA Franc BCEAO",
+ a: "XOF",
+ n: 952,
+ d: 0,
+ },
+ {
+ c: "New Zealand Dollar",
+ a: "NZD",
+ n: 554,
+ d: 2,
+ },
+ {
+ c: "Pa'anga",
+ a: "TOP",
+ n: 776,
+ d: 2,
+ },
+ {
+ c: "Trinidad and Tobago Dollar",
+ a: "TTD",
+ n: 780,
+ d: 2,
+ },
+ {
+ c: "Tunisian Dinar",
+ a: "TND",
+ n: 788,
+ d: 3,
+ },
+ {
+ c: "Turkish Lira",
+ a: "TRY",
+ n: 949,
+ d: 2,
+ },
+ {
+ c: "Turkmenistan New Manat",
+ a: "TMT",
+ n: 934,
+ d: 2,
+ },
+ {
+ c: "US Dollar",
+ a: "USD",
+ n: 840,
+ d: 2,
+ },
+ {
+ c: "Australian Dollar",
+ a: "AUD",
+ n: 36,
+ d: 2,
+ },
+ {
+ c: "Uganda Shilling",
+ a: "UGX",
+ n: 800,
+ d: 0,
+ },
+ {
+ c: "Hryvnia",
+ a: "UAH",
+ n: 980,
+ d: 2,
+ },
+ {
+ c: "UAE Dirham",
+ a: "AED",
+ n: 784,
+ d: 2,
+ },
+ {
+ c: "Pound Sterling",
+ a: "GBP",
+ n: 826,
+ d: 2,
+ },
+ {
+ c: "US Dollar",
+ a: "USD",
+ n: 840,
+ d: 2,
+ },
+ {
+ c: "US Dollar",
+ a: "USD",
+ n: 840,
+ d: 2,
+ },
+ {
+ c: "US Dollar (Next day)",
+ a: "USN",
+ n: 997,
+ d: 2,
+ },
+ {
+ c: "Peso Uruguayo",
+ a: "UYU",
+ n: 858,
+ d: 2,
+ },
+ {
+ c: "Uruguay Peso en Unidades Indexadas (UI)",
+ a: "UYI",
+ n: 940,
+ d: 0,
+ },
+ {
+ c: "Unidad Previsional",
+ a: "UYW",
+ n: 927,
+ d: 4,
+ },
+ {
+ c: "Uzbekistan Sum",
+ a: "UZS",
+ n: 860,
+ d: 2,
+ },
+ {
+ c: "Vatu",
+ a: "VUV",
+ n: 548,
+ d: 0,
+ },
+ {
+ c: "Bolívar Soberano",
+ a: "VES",
+ n: 928,
+ d: 2,
+ },
+ {
+ c: "Bolívar Soberano",
+ a: "VED",
+ n: 926,
+ d: 2,
+ },
+ {
+ c: "Dong",
+ a: "VND",
+ n: 704,
+ d: 0,
+ },
+ {
+ c: "US Dollar",
+ a: "USD",
+ n: 840,
+ d: 2,
+ },
+ {
+ c: "US Dollar",
+ a: "USD",
+ n: 840,
+ d: 2,
+ },
+ {
+ c: "CFP Franc",
+ a: "XPF",
+ n: 953,
+ d: 0,
+ },
+ {
+ c: "Moroccan Dirham",
+ a: "MAD",
+ n: 504,
+ d: 2,
+ },
+ {
+ c: "Yemeni Rial",
+ a: "YER",
+ n: 886,
+ d: 2,
+ },
+ {
+ c: "Zambian Kwacha",
+ a: "ZMW",
+ n: 967,
+ d: 2,
+ },
+ {
+ c: "Zimbabwe Dollar",
+ a: "ZWL",
+ n: 932,
+ d: 2,
+ },
+ {
+ c: "Bond Markets Unit European Composite Unit (EURCO)",
+ a: "XBA",
+ n: 955,
+ d: 0,
+ },
+ {
+ c: "Bond Markets Unit European Monetary Unit (E.M.U.-6)",
+ a: "XBB",
+ n: 956,
+ d: 0,
+ },
+ {
+ c: "Bond Markets Unit European Unit of Account 9 (E.U.A.-9)",
+ a: "XBC",
+ n: 957,
+ d: 0,
+ },
+ {
+ c: "Bond Markets Unit European Unit of Account 17 (E.U.A.-17)",
+ a: "XBD",
+ n: 958,
+ d: 0,
+ },
+ {
+ c: "Codes specifically reserved for testing purposes",
+ a: "XTS",
+ n: 963,
+ d: 0,
+ },
+ {
+ c: "The codes assigned for transactions where no currency is involved",
+ a: "XXX",
+ n: 999,
+ d: 0,
+ },
+ {
+ c: "Gold",
+ a: "XAU",
+ n: 959,
+ d: 0,
+ },
+ {
+ c: "Palladium",
+ a: "XPD",
+ n: 964,
+ d: 0,
+ },
+ {
+ c: "Platinum",
+ a: "XPT",
+ n: 962,
+ d: 0,
+ },
+ {
+ c: "Silver",
+ a: "XAG",
+ n: 961,
+ d: 0,
+ },
+];
diff --git a/packages/taler-util/src/kdf.ts b/packages/taler-util/src/kdf.ts
index 5fcaa1b4c..8f4314340 100644
--- a/packages/taler-util/src/kdf.ts
+++ b/packages/taler-util/src/kdf.ts
@@ -58,38 +58,3 @@ export function hmacSha512(key: Uint8Array, message: Uint8Array): Uint8Array {
export function hmacSha256(key: Uint8Array, message: Uint8Array): Uint8Array {
return hmac(sha256, 64, key, message);
}
-
-export function kdf(
- outputLength: number,
- ikm: Uint8Array,
- salt?: Uint8Array,
- info?: Uint8Array,
-): Uint8Array {
- salt = salt ?? new Uint8Array(64);
- // extract
- const prk = hmacSha512(salt, ikm);
-
- info = info ?? new Uint8Array(0);
-
- // expand
- const N = Math.ceil(outputLength / 32);
- const output = new Uint8Array(N * 32);
- for (let i = 0; i < N; i++) {
- let buf;
- if (i == 0) {
- buf = new Uint8Array(info.byteLength + 1);
- buf.set(info, 0);
- } else {
- buf = new Uint8Array(info.byteLength + 1 + 32);
- for (let j = 0; j < 32; j++) {
- buf[j] = output[(i - 1) * 32 + j];
- }
- buf.set(info, 32);
- }
- buf[buf.length - 1] = i + 1;
- const chunk = hmacSha256(prk, buf);
- output.set(chunk, i * 32);
- }
-
- return output.slice(0, outputLength);
-}
diff --git a/packages/taler-wallet-core/src/util/debugFlags.ts b/packages/taler-util/src/libeufin-api-types.ts
index cea249d27..aa3d0cb7a 100644
--- a/packages/taler-wallet-core/src/util/debugFlags.ts
+++ b/packages/taler-util/src/libeufin-api-types.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2023 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -14,19 +14,18 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-/**
- * Debug flags for wallet-core.
- *
- * @author Florian Dold
- */
-
-export interface WalletCoreDebugFlags {
- /**
- * Allow withdrawal of denominations even though they are about to expire.
- */
- denomselAllowLate: boolean;
+export type FacadeCredentials =
+ | NoFacadeCredentials
+ | BasicAuthFacadeCredentials;
+export interface NoFacadeCredentials {
+ type: "none";
}
+export interface BasicAuthFacadeCredentials {
+ type: "basic";
-export const walletCoreDebugFlags: WalletCoreDebugFlags = {
- denomselAllowLate: false,
-};
+ // Username to use to authenticate
+ username: string;
+
+ // Password to use to authenticate
+ password: string;
+}
diff --git a/packages/taler-util/src/libtool-version.test.ts b/packages/taler-util/src/libtool-version.test.ts
index c1683f0df..addd1b418 100644
--- a/packages/taler-util/src/libtool-version.test.ts
+++ b/packages/taler-util/src/libtool-version.test.ts
@@ -45,4 +45,6 @@ test("version comparison", (t) => {
compatible: true,
currentCmp: 0,
});
+ t.true(LibtoolVersion.compare("42:0:1", "41:0:0")?.compatible);
+ t.true(LibtoolVersion.compare("41:0:0", "42:0:1")?.compatible);
});
diff --git a/packages/taler-util/src/logging.ts b/packages/taler-util/src/logging.ts
index 840402d6f..17bb184f7 100644
--- a/packages/taler-util/src/logging.ts
+++ b/packages/taler-util/src/logging.ts
@@ -32,36 +32,86 @@ export enum LogLevel {
None = "none",
}
-export let globalLogLevel = LogLevel.Info;
+let globalLogLevel = LogLevel.Info;
+const byTagLogLevel: Record<string, LogLevel> = {};
-export function setGlobalLogLevelFromString(logLevelStr: string) {
- let level: LogLevel;
+let nativeLogging: boolean = false;
+
+// from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/toString
+Error.prototype.toString = function () {
+ if (
+ this === null ||
+ (typeof this !== "object" && typeof this !== "function")
+ ) {
+ throw new TypeError();
+ }
+ let name = this.name;
+ name = name === undefined ? "Error" : `${name}`;
+ let msg = this.message;
+ msg = msg === undefined ? "" : `${msg}`;
+
+ let cause = "";
+ if ("cause" in this) {
+ cause = `\n Caused by: ${this.cause}`;
+ }
+ return `${name}: ${msg}${cause}`;
+};
+
+export function getGlobalLogLevel(): string {
+ return globalLogLevel;
+}
+
+export function setGlobalLogLevelFromString(logLevelStr: string): void {
+ globalLogLevel = getLevelForString(logLevelStr);
+}
+
+export function setLogLevelFromString(tag: string, logLevelStr: string): void {
+ byTagLogLevel[tag] = getLevelForString(logLevelStr);
+}
+
+export function enableNativeLogging() {
+ nativeLogging = true;
+}
+
+function getLevelForString(logLevelStr: string): LogLevel {
switch (logLevelStr.toLowerCase()) {
case "trace":
- level = LogLevel.Trace;
- break;
+ return LogLevel.Trace;
case "info":
- level = LogLevel.Info;
- break;
+ return LogLevel.Info;
case "warn":
case "warning":
- level = LogLevel.Warn;
- break;
+ return LogLevel.Warn;
case "error":
- level = LogLevel.Error;
- break;
+ return LogLevel.Error;
case "none":
- level = LogLevel.None;
- break;
+ return LogLevel.None;
default:
if (isNode) {
process.stderr.write(`Invalid log level, defaulting to WARNING\n`);
} else {
console.warn(`Invalid log level, defaulting to WARNING`);
}
- level = LogLevel.Warn;
+ return LogLevel.Warn;
+ }
+}
+
+function writeNativeLog(
+ message: any,
+ tag: string,
+ level: number,
+ args: any[],
+): void {
+ const logFn = (globalThis as any).__nativeLog;
+ if (logFn) {
+ let m: string;
+ if (args.length == 0) {
+ m = message;
+ } else {
+ m = message + " " + args.toString();
+ }
+ logFn(level, tag, message);
}
- globalLogLevel = level;
}
function writeNodeLog(
@@ -98,8 +148,9 @@ function writeNodeLog(
export class Logger {
constructor(private tag: string) {}
- shouldLogTrace() {
- switch (globalLogLevel) {
+ shouldLogTrace(): boolean {
+ const level = byTagLogLevel[this.tag] ?? globalLogLevel;
+ switch (level) {
case LogLevel.Trace:
return true;
case LogLevel.Message:
@@ -111,8 +162,9 @@ export class Logger {
}
}
- shouldLogInfo() {
- switch (globalLogLevel) {
+ shouldLogInfo(): boolean {
+ const level = byTagLogLevel[this.tag] ?? globalLogLevel;
+ switch (level) {
case LogLevel.Trace:
case LogLevel.Message:
case LogLevel.Info:
@@ -124,8 +176,9 @@ export class Logger {
}
}
- shouldLogWarn() {
- switch (globalLogLevel) {
+ shouldLogWarn(): boolean {
+ const level = byTagLogLevel[this.tag] ?? globalLogLevel;
+ switch (level) {
case LogLevel.Trace:
case LogLevel.Message:
case LogLevel.Info:
@@ -137,8 +190,9 @@ export class Logger {
}
}
- shouldLogError() {
- switch (globalLogLevel) {
+ shouldLogError(): boolean {
+ const level = byTagLogLevel[this.tag] ?? globalLogLevel;
+ switch (level) {
case LogLevel.Trace:
case LogLevel.Message:
case LogLevel.Info:
@@ -154,6 +208,10 @@ export class Logger {
if (!this.shouldLogInfo()) {
return;
}
+ if (nativeLogging) {
+ writeNativeLog(message, this.tag, 2, args);
+ return;
+ }
if (isNode) {
writeNodeLog(message, this.tag, "INFO", args);
} else {
@@ -168,6 +226,10 @@ export class Logger {
if (!this.shouldLogWarn()) {
return;
}
+ if (nativeLogging) {
+ writeNativeLog(message, this.tag, 3, args);
+ return;
+ }
if (isNode) {
writeNodeLog(message, this.tag, "WARN", args);
} else {
@@ -182,6 +244,10 @@ export class Logger {
if (!this.shouldLogError()) {
return;
}
+ if (nativeLogging) {
+ writeNativeLog(message, this.tag, 4, args);
+ return;
+ }
if (isNode) {
writeNodeLog(message, this.tag, "ERROR", args);
} else {
@@ -192,10 +258,14 @@ export class Logger {
}
}
- trace(message: any, ...args: any[]): void {
+ trace(message: string, ...args: any[]): void {
if (!this.shouldLogTrace()) {
return;
}
+ if (nativeLogging) {
+ writeNativeLog(message, this.tag, 1, args);
+ return;
+ }
if (isNode) {
writeNodeLog(message, this.tag, "TRACE", args);
} else {
diff --git a/packages/taler-wallet-cli/src/harness/merchantApiTypes.ts b/packages/taler-util/src/merchant-api-types.ts
index 2a59b0160..639ae8d13 100644
--- a/packages/taler-wallet-cli/src/harness/merchantApiTypes.ts
+++ b/packages/taler-util/src/merchant-api-types.ts
@@ -25,29 +25,34 @@
* Imports.
*/
import {
- MerchantContractTerms,
- Duration,
- Codec,
- buildCodecForObject,
- codecForString,
- codecOptional,
- codecForConstString,
- codecForBoolean,
- codecForNumber,
- codecForMerchantContractTerms,
- codecForAny,
- buildCodecForUnion,
- AmountString,
AbsoluteTime,
+ AmountString,
+ Codec,
CoinPublicKeyString,
EddsaPublicKeyString,
- codecForAmountString,
+ ExchangeWireAccount,
+ FacadeCredentials,
+ MerchantContractTerms,
TalerProtocolDuration,
- codecForTimestamp,
TalerProtocolTimestamp,
+ buildCodecForObject,
+ buildCodecForUnion,
+ codecForAmountString,
+ codecForAny,
+ codecForBoolean,
+ codecForCheckPaymentClaimedResponse,
+ codecForCheckPaymentUnpaidResponse,
+ codecForConstString,
+ codecForExchangeWireAccount,
+ codecForList,
+ codecForMerchantContractTerms,
+ codecForNumber,
+ codecForString,
+ codecForTimestamp,
+ codecOptional,
} from "@gnu-taler/taler-util";
-export interface PostOrderRequest {
+export interface MerchantPostOrderRequest {
// The order must at least contain the minimal
// order detail, but can override all
order: Partial<MerchantContractTerms>;
@@ -71,29 +76,29 @@ export interface PostOrderRequest {
export type ClaimToken = string;
-export interface PostOrderResponse {
+export interface MerchantPostOrderResponse {
order_id: string;
token?: ClaimToken;
}
-export const codecForPostOrderResponse = (): Codec<PostOrderResponse> =>
- buildCodecForObject<PostOrderResponse>()
- .property("order_id", codecForString())
- .property("token", codecOptional(codecForString()))
- .build("PostOrderResponse");
-
+export const codecForMerchantPostOrderResponse =
+ (): Codec<MerchantPostOrderResponse> =>
+ buildCodecForObject<MerchantPostOrderResponse>()
+ .property("order_id", codecForString())
+ .property("token", codecOptional(codecForString()))
+ .build("PostOrderResponse");
-export const codecForRefundDetails = (): Codec<RefundDetails> =>
+export const codecForMerchantRefundDetails = (): Codec<RefundDetails> =>
buildCodecForObject<RefundDetails>()
.property("reason", codecForString())
.property("pending", codecForBoolean())
- .property("amount", codecForString())
+ .property("amount", codecForAmountString())
.property("timestamp", codecForTimestamp)
.build("PostOrderResponse");
-export const codecForCheckPaymentPaidResponse =
- (): Codec<CheckPaymentPaidResponse> =>
- buildCodecForObject<CheckPaymentPaidResponse>()
+export const codecForMerchantCheckPaymentPaidResponse =
+ (): Codec<MerchantCheckPaymentPaidResponse> =>
+ buildCodecForObject<MerchantCheckPaymentPaidResponse>()
.property("order_status_url", codecForString())
.property("order_status", codecForConstString("paid"))
.property("refunded", codecForBoolean())
@@ -109,33 +114,8 @@ export const codecForCheckPaymentPaidResponse =
.property("refund_details", codecForAny())
.build("CheckPaymentPaidResponse");
-export const codecForCheckPaymentUnpaidResponse =
- (): Codec<CheckPaymentUnpaidResponse> =>
- buildCodecForObject<CheckPaymentUnpaidResponse>()
- .property("order_status", codecForConstString("unpaid"))
- .property("taler_pay_uri", codecForString())
- .property("order_status_url", codecForString())
- .property("already_paid_order_id", codecOptional(codecForString()))
- .build("CheckPaymentPaidResponse");
-
-export const codecForCheckPaymentClaimedResponse =
- (): Codec<CheckPaymentClaimedResponse> =>
- buildCodecForObject<CheckPaymentClaimedResponse>()
- .property("order_status", codecForConstString("claimed"))
- .property("contract_terms", codecForMerchantContractTerms())
- .build("CheckPaymentClaimedResponse");
-
-export const codecForMerchantOrderPrivateStatusResponse =
- (): Codec<MerchantOrderPrivateStatusResponse> =>
- buildCodecForUnion<MerchantOrderPrivateStatusResponse>()
- .discriminateOn("order_status")
- .alternative("paid", codecForCheckPaymentPaidResponse())
- .alternative("unpaid", codecForCheckPaymentUnpaidResponse())
- .alternative("claimed", codecForCheckPaymentClaimedResponse())
- .build("MerchantOrderPrivateStatusResponse");
-
export type MerchantOrderPrivateStatusResponse =
- | CheckPaymentPaidResponse
+ | MerchantCheckPaymentPaidResponse
| CheckPaymentUnpaidResponse
| CheckPaymentClaimedResponse;
@@ -146,7 +126,7 @@ export interface CheckPaymentClaimedResponse {
contract_terms: MerchantContractTerms;
}
-export interface CheckPaymentPaidResponse {
+export interface MerchantCheckPaymentPaidResponse {
// did the customer pay for this contract
order_status: "paid";
@@ -256,11 +236,6 @@ export interface TransactionWireReport {
coin_pub: CoinPublicKeyString;
}
-export interface TippingReserveStatus {
- // Array of all known reserves (possibly empty!)
- reserves: ReserveStatusEntry[];
-}
-
export interface ReserveStatusEntry {
// Public key of the reserve
reserve_pub: string;
@@ -288,33 +263,6 @@ export interface ReserveStatusEntry {
active: boolean;
}
-export interface TipCreateConfirmation {
- // Unique tip identifier for the tip that was created.
- tip_id: string;
-
- // taler://tip URI for the tip
- taler_tip_uri: string;
-
- // URL that will directly trigger processing
- // the tip when the browser is redirected to it
- tip_status_url: string;
-
- // when does the tip expire
- tip_expiration: AbsoluteTime;
-}
-
-export interface TipCreateRequest {
- // Amount that the customer should be tipped
- amount: AmountString;
-
- // Justification for giving the tip
- justification: string;
-
- // URL that the user should be directed to after tipping,
- // will be included in the tip_token.
- next_url: string;
-}
-
export interface MerchantInstancesResponse {
// List of instances that are present in the backend (see Instance)
instances: MerchantInstanceDetail[];
@@ -335,3 +283,70 @@ export interface MerchantInstanceDetail {
// front-ends do not have to support wallets selecting payment targets.
payment_targets: string[];
}
+
+export interface MerchantTemplateContractDetails {
+ // Human-readable summary for the template.
+ summary?: string;
+
+ // The price is imposed by the merchant and cannot be changed by the customer.
+ // This parameter is optional.
+ amount?: string;
+
+ // Minimum age buyer must have (in years). Default is 0.
+ minimum_age: number;
+
+ // The time the customer need to pay before his order will be deleted.
+ // It is deleted if the customer did not pay and if the duration is over.
+ pay_duration: TalerProtocolDuration;
+}
+
+export interface MerchantTemplateAddDetails {
+ // Template ID to use.
+ template_id: string;
+
+ // Human-readable description for the template.
+ template_description: string;
+
+ // A base64-encoded image selected by the merchant.
+ // This parameter is optional.
+ // We are not sure about it.
+ image?: string;
+
+ // Additional information in a separate template.
+ template_contract: MerchantTemplateContractDetails;
+
+ // OTP device ID.
+ // This parameter is optional.
+ otp_id?: string;
+}
+
+export interface MerchantReserveCreateConfirmation {
+ // Public key identifying the reserve.
+ reserve_pub: EddsaPublicKeyString;
+
+ // Wire accounts of the exchange where to transfer the funds.
+ accounts: ExchangeWireAccount[];
+}
+
+export const codecForMerchantReserveCreateConfirmation =
+ (): Codec<MerchantReserveCreateConfirmation> =>
+ buildCodecForObject<MerchantReserveCreateConfirmation>()
+ .property("accounts", codecForList(codecForExchangeWireAccount()))
+ .property("reserve_pub", codecForString())
+ .build("MerchantReserveCreateConfirmation");
+
+export 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;
+}
diff --git a/packages/taler-util/src/notifications.ts b/packages/taler-util/src/notifications.ts
index c50cc72de..b60fb267c 100644
--- a/packages/taler-util/src/notifications.ts
+++ b/packages/taler-util/src/notifications.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2019 GNUnet e.V.
+ (C) 2019-2023 Taler Systems S.A.
GNU Taler is free 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,265 +22,225 @@
/**
* Imports.
*/
-import { TalerErrorDetail } from "./wallet-types.js";
+import { AbsoluteTime } from "./time.js";
+import { TransactionState } from "./transactions-types.js";
+import { ExchangeEntryState, TalerErrorDetail } from "./wallet-types.js";
export enum NotificationType {
- CoinWithdrawn = "coin-withdrawn",
- ProposalAccepted = "proposal-accepted",
- ProposalDownloaded = "proposal-downloaded",
- RefundsSubmitted = "refunds-submitted",
- RecoupStarted = "recoup-started",
- RecoupFinished = "recoup-finished",
- RefreshRevealed = "refresh-revealed",
- RefreshMelted = "refresh-melted",
- RefreshStarted = "refresh-started",
- RefreshUnwarranted = "refresh-unwarranted",
- ReserveUpdated = "reserve-updated",
- ReserveConfirmed = "reserve-confirmed",
- ReserveCreated = "reserve-created",
- WithdrawGroupCreated = "withdraw-group-created",
- WithdrawGroupFinished = "withdraw-group-finished",
- WaitingForRetry = "waiting-for-retry",
- RefundStarted = "refund-started",
- RefundQueried = "refund-queried",
- RefundFinished = "refund-finished",
- ExchangeOperationError = "exchange-operation-error",
- ExchangeAdded = "exchange-added",
- RefreshOperationError = "refresh-operation-error",
- RecoupOperationError = "recoup-operation-error",
- RefundApplyOperationError = "refund-apply-error",
- RefundStatusOperationError = "refund-status-error",
- ProposalOperationError = "proposal-error",
+ BalanceChange = "balance-change",
BackupOperationError = "backup-error",
- TipOperationError = "tip-error",
- PayOperationError = "pay-error",
- PayOperationSuccess = "pay-operation-success",
- WithdrawOperationError = "withdraw-error",
- ReserveNotYetFound = "reserve-not-yet-found",
- ReserveOperationError = "reserve-error",
- InternalError = "internal-error",
- PendingOperationProcessed = "pending-operation-processed",
- ProposalRefused = "proposal-refused",
- ReserveRegisteredWithBank = "reserve-registered-with-bank",
- DepositOperationError = "deposit-operation-error",
-}
-
-export interface ProposalAcceptedNotification {
- type: NotificationType.ProposalAccepted;
- proposalId: string;
-}
-
-export interface InternalErrorNotification {
- type: NotificationType.InternalError;
- message: string;
- exception: any;
-}
-
-export interface ReserveNotYetFoundNotification {
- type: NotificationType.ReserveNotYetFound;
- reservePub: string;
-}
-
-export interface CoinWithdrawnNotification {
- type: NotificationType.CoinWithdrawn;
-}
-
-export interface RefundStartedNotification {
- type: NotificationType.RefundStarted;
-}
-
-export interface RefundQueriedNotification {
- type: NotificationType.RefundQueried;
-}
-
-export interface ProposalDownloadedNotification {
- type: NotificationType.ProposalDownloaded;
- proposalId: string;
-}
-
-export interface RefundsSubmittedNotification {
- type: NotificationType.RefundsSubmitted;
- proposalId: string;
-}
-
-export interface RecoupStartedNotification {
- type: NotificationType.RecoupStarted;
-}
-
-export interface RecoupFinishedNotification {
- type: NotificationType.RecoupFinished;
-}
-
-export interface RefreshMeltedNotification {
- type: NotificationType.RefreshMelted;
-}
-
-export interface RefreshRevealedNotification {
- type: NotificationType.RefreshRevealed;
-}
-
-export interface RefreshStartedNotification {
- type: NotificationType.RefreshStarted;
-}
-
-export interface RefreshRefusedNotification {
- type: NotificationType.RefreshUnwarranted;
-}
-
-export interface ReserveConfirmedNotification {
- type: NotificationType.ReserveConfirmed;
-}
-
-export interface WithdrawalGroupCreatedNotification {
- type: NotificationType.WithdrawGroupCreated;
- withdrawalGroupId: string;
-}
-
-export interface WithdrawalGroupFinishedNotification {
- type: NotificationType.WithdrawGroupFinished;
- reservePub: string;
-}
-
-export interface WaitingForRetryNotification {
- type: NotificationType.WaitingForRetry;
- numPending: number;
- numGivingLiveness: number;
- numDue: number;
-}
-
-export interface RefundFinishedNotification {
- type: NotificationType.RefundFinished;
-}
-
-export interface ExchangeAddedNotification {
- type: NotificationType.ExchangeAdded;
-}
-
-export interface ExchangeOperationErrorNotification {
- type: NotificationType.ExchangeOperationError;
- error: TalerErrorDetail;
-}
-
-export interface RefreshOperationErrorNotification {
- type: NotificationType.RefreshOperationError;
- error: TalerErrorDetail;
-}
+ TransactionStateTransition = "transaction-state-transition",
+ WithdrawalOperationTransition = "withdrawal-operation-transition",
+ ExchangeStateTransition = "exchange-state-transition",
+ Idle = "idle",
+ TaskObservabilityEvent = "task-observability-event",
+ RequestObservabilityEvent = "request-observability-event",
+}
+
+export interface ErrorInfoSummary {
+ code: number;
+ hint?: string;
+ message?: string;
+}
+
+export interface TransactionStateTransitionNotification {
+ type: NotificationType.TransactionStateTransition;
+ transactionId: string;
+ oldTxState: TransactionState;
+ newTxState: TransactionState;
+ errorInfo?: ErrorInfoSummary;
+
+ /**
+ * Additional "user data" that is dependent on the
+ * state transition.
+ *
+ * Usage should be avoided.
+ *
+ * Currently used to notify the iOS app about
+ * the KYC URL.
+ */
+ experimentalUserData?: any;
+}
+
+export interface ExchangeStateTransitionNotification {
+ type: NotificationType.ExchangeStateTransition;
+ /**
+ * Identification of the exchange entry that this
+ * notification is about.
+ */
+ exchangeBaseUrl: string;
+
+ /**
+ * If missing, the notification means that
+ * the exchange entry is newly created.
+ */
+ oldExchangeState?: ExchangeEntryState;
+
+ /**
+ * New state of the exchange.
+ */
+ newExchangeState: ExchangeEntryState;
+
+ /**
+ * Summary of the error that occurred when trying to update the exchange entry,
+ * if applicable.
+ */
+ errorInfo?: ErrorInfoSummary;
+}
+
+export interface BalanceChangeNotification {
+ type: NotificationType.BalanceChange;
+
+ /**
+ * Transaction ID of the transaction that caused the balance update.
+ *
+ * Only used as a hint for debugging, should not be relied upon by clients.
+ */
+ hintTransactionId: string;
+}
+
+export interface TaskProgressNotification {
+ type: NotificationType.TaskObservabilityEvent;
+ taskId: string;
+ event: ObservabilityEvent;
+}
+
+export interface RequestProgressNotification {
+ type: NotificationType.RequestObservabilityEvent;
+ requestId: string;
+ operation: string;
+ event: ObservabilityEvent;
+}
+
+export enum ObservabilityEventType {
+ HttpFetchStart = "http-fetch-start",
+ HttpFetchFinishError = "http-fetch-finish-error",
+ HttpFetchFinishSuccess = "http-fetch-finish-success",
+ DbQueryStart = "db-query-start",
+ DbQueryFinishSuccess = "db-query-finish-success",
+ DbQueryFinishError = "db-query-finish-error",
+ RequestStart = "request-start",
+ RequestFinishSuccess = "request-finish-success",
+ RequestFinishError = "request-finish-error",
+ TaskStart = "task-start",
+ TaskStop = "task-stop",
+ TaskReset = "task-reset",
+ ShepherdTaskResult = "sheperd-task-result",
+ DeclareTaskDependency = "declare-task-dependency",
+ CryptoStart = "crypto-start",
+ CryptoFinishSuccess = "crypto-finish-success",
+ CryptoFinishError = "crypto-finish-error",
+ Message = "message",
+}
+
+export type ObservabilityEvent =
+ | {
+ id: string;
+ when: AbsoluteTime;
+ type: ObservabilityEventType.HttpFetchStart;
+ url: string;
+ }
+ | {
+ id: string;
+ when: AbsoluteTime;
+ type: ObservabilityEventType.HttpFetchFinishSuccess;
+ url: string;
+ status: number;
+ }
+ | {
+ id: string;
+ when: AbsoluteTime;
+ type: ObservabilityEventType.HttpFetchFinishError;
+ url: string;
+ error: TalerErrorDetail;
+ }
+ | {
+ type: ObservabilityEventType.DbQueryStart;
+ name: string;
+ location: string;
+ }
+ | {
+ type: ObservabilityEventType.DbQueryFinishSuccess;
+ name: string;
+ location: string;
+ }
+ | {
+ type: ObservabilityEventType.DbQueryFinishError;
+ name: string;
+ location: string;
+ }
+ | {
+ type: ObservabilityEventType.RequestStart;
+ }
+ | {
+ type: ObservabilityEventType.RequestFinishSuccess;
+ durationMs: number;
+ }
+ | {
+ type: ObservabilityEventType.RequestFinishError;
+ }
+ | {
+ type: ObservabilityEventType.TaskStart;
+ taskId: string;
+ }
+ | {
+ type: ObservabilityEventType.TaskStop;
+ taskId: string;
+ }
+ | {
+ type: ObservabilityEventType.TaskReset;
+ taskId: string;
+ }
+ | {
+ type: ObservabilityEventType.DeclareTaskDependency;
+ taskId: string;
+ }
+ | {
+ type: ObservabilityEventType.CryptoStart;
+ operation: string;
+ }
+ | {
+ type: ObservabilityEventType.CryptoFinishSuccess;
+ operation: string;
+ }
+ | {
+ type: ObservabilityEventType.CryptoFinishError;
+ operation: string;
+ }
+ | {
+ type: ObservabilityEventType.ShepherdTaskResult;
+ resultType: string;
+ }
+ | {
+ type: ObservabilityEventType.Message;
+ contents: string;
+ };
export interface BackupOperationErrorNotification {
type: NotificationType.BackupOperationError;
error: TalerErrorDetail;
}
-
-export interface RefundStatusOperationErrorNotification {
- type: NotificationType.RefundStatusOperationError;
- error: TalerErrorDetail;
-}
-
-export interface RefundApplyOperationErrorNotification {
- type: NotificationType.RefundApplyOperationError;
- error: TalerErrorDetail;
-}
-
-export interface PayOperationErrorNotification {
- type: NotificationType.PayOperationError;
- error: TalerErrorDetail;
-}
-
-export interface ProposalOperationErrorNotification {
- type: NotificationType.ProposalOperationError;
- error: TalerErrorDetail;
-}
-
-export interface TipOperationErrorNotification {
- type: NotificationType.TipOperationError;
- error: TalerErrorDetail;
-}
-
-export interface WithdrawOperationErrorNotification {
- type: NotificationType.WithdrawOperationError;
- error: TalerErrorDetail;
-}
-
-export interface RecoupOperationErrorNotification {
- type: NotificationType.RecoupOperationError;
- error: TalerErrorDetail;
-}
-
-export interface DepositOperationErrorNotification {
- type: NotificationType.DepositOperationError;
- error: TalerErrorDetail;
-}
-
-export interface ReserveOperationErrorNotification {
- type: NotificationType.ReserveOperationError;
- error: TalerErrorDetail;
-}
-
-export interface ReserveCreatedNotification {
- type: NotificationType.ReserveCreated;
- reservePub: string;
-}
-
-export interface PendingOperationProcessedNotification {
- type: NotificationType.PendingOperationProcessed;
- id: string;
-}
-
-export interface ProposalRefusedNotification {
- type: NotificationType.ProposalRefused;
-}
-
-export interface ReserveRegisteredWithBankNotification {
- type: NotificationType.ReserveRegisteredWithBank;
-}
-
/**
- * Notification sent when a pay (or pay replay) operation succeeded.
+ * This notification is required to signal UI that
+ * the withdrawal operation changed the state.
*
- * We send this notification because the confirmPay request can return
- * a "confirmed" response that indicates that the payment has been confirmed
- * by the user, but we're still waiting for the payment to succeed or fail.
+ * https://bugs.gnunet.org/view.php?id=8099
*/
-export interface PayOperationSuccessNotification {
- type: NotificationType.PayOperationSuccess;
- proposalId: string;
+export interface WithdrawalOperationTransitionNotification {
+ type: NotificationType.WithdrawalOperationTransition;
+ uri: string;
+}
+
+export interface IdleNotification {
+ type: NotificationType.Idle;
}
export type WalletNotification =
+ | BalanceChangeNotification
+ | WithdrawalOperationTransitionNotification
| BackupOperationErrorNotification
- | WithdrawOperationErrorNotification
- | ReserveOperationErrorNotification
- | ExchangeAddedNotification
- | ExchangeOperationErrorNotification
- | RefreshOperationErrorNotification
- | RefundStatusOperationErrorNotification
- | RefundApplyOperationErrorNotification
- | ProposalOperationErrorNotification
- | PayOperationErrorNotification
- | TipOperationErrorNotification
- | ProposalAcceptedNotification
- | ProposalDownloadedNotification
- | RefundsSubmittedNotification
- | RecoupStartedNotification
- | RecoupFinishedNotification
- | RefreshMeltedNotification
- | RefreshRevealedNotification
- | RefreshStartedNotification
- | RefreshRefusedNotification
- | ReserveCreatedNotification
- | ReserveConfirmedNotification
- | WithdrawalGroupFinishedNotification
- | WaitingForRetryNotification
- | RefundStartedNotification
- | RefundFinishedNotification
- | RefundQueriedNotification
- | WithdrawalGroupCreatedNotification
- | CoinWithdrawnNotification
- | RecoupOperationErrorNotification
- | DepositOperationErrorNotification
- | InternalErrorNotification
- | PendingOperationProcessedNotification
- | ProposalRefusedNotification
- | ReserveRegisteredWithBankNotification
- | ReserveNotYetFoundNotification
- | PayOperationSuccessNotification;
+ | ExchangeStateTransitionNotification
+ | TransactionStateTransitionNotification
+ | TaskProgressNotification
+ | RequestProgressNotification
+ | IdleNotification;
diff --git a/packages/taler-util/src/observability.ts b/packages/taler-util/src/observability.ts
new file mode 100644
index 000000000..0171142c8
--- /dev/null
+++ b/packages/taler-util/src/observability.ts
@@ -0,0 +1,98 @@
+/*
+ This file is part of GNU Taler
+ (C) 2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ AbsoluteTime,
+ CancellationToken,
+ ObservabilityEvent,
+} from "./index.js";
+import {
+ HttpRequestLibrary,
+ HttpRequestOptions,
+ HttpResponse,
+} from "./http-common.js";
+import { ObservabilityEventType } from "./notifications.js";
+import { getErrorDetailFromException } from "./errors.js";
+
+/**
+ * Observability sink can be passed into various operations (HTTP requests, DB access)
+ * to do structured logging within a particular context (task, request, ...).
+ */
+export interface ObservabilityContext {
+ observe(evt: ObservabilityEvent): void;
+}
+
+let seqId = 1000;
+
+export class ObservableHttpClientLibrary implements HttpRequestLibrary {
+ private readonly cancelatorById = new Map<string, CancellationToken.Source>();
+ constructor(
+ private impl: HttpRequestLibrary,
+ private oc: ObservabilityContext,
+ ) {}
+
+ public cancelRequest(id: string): void {
+ const cancelator = this.cancelatorById.get(id);
+ if (!cancelator) return;
+ cancelator.cancel();
+ }
+
+ async fetch(
+ url: string,
+ opt?: HttpRequestOptions | undefined,
+ ): Promise<HttpResponse> {
+ const id = `req-${seqId}`;
+ seqId = seqId + 1;
+
+ const cancelator = CancellationToken.create();
+ if (opt?.cancellationToken) {
+ opt.cancellationToken.onCancelled(cancelator.cancel);
+ }
+ this.cancelatorById.set(id, cancelator);
+
+ this.oc.observe({
+ id,
+ when: AbsoluteTime.now(),
+ type: ObservabilityEventType.HttpFetchStart,
+ url: url,
+ });
+
+ const optsWithCancel = opt ?? {};
+ optsWithCancel.cancellationToken = cancelator.token;
+ try {
+ const res = await this.impl.fetch(url, optsWithCancel);
+ this.oc.observe({
+ id,
+ when: AbsoluteTime.now(),
+ type: ObservabilityEventType.HttpFetchFinishSuccess,
+ url,
+ status: res.status,
+ });
+ return res;
+ } catch (e) {
+ this.oc.observe({
+ id,
+ when: AbsoluteTime.now(),
+ type: ObservabilityEventType.HttpFetchFinishError,
+ url,
+ error: getErrorDetailFromException(e),
+ });
+ throw e;
+ } finally {
+ this.cancelatorById.delete(id);
+ }
+ }
+}
diff --git a/packages/taler-util/src/operation.ts b/packages/taler-util/src/operation.ts
new file mode 100644
index 000000000..e2ab9d4e4
--- /dev/null
+++ b/packages/taler-util/src/operation.ts
@@ -0,0 +1,198 @@
+/*
+ This file is part of GNU Taler
+ (C) 2023-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 {
+ HttpResponse,
+ readResponseJsonOrErrorCode,
+ readSuccessResponseJsonOrThrow,
+ readTalerErrorResponse,
+} from "./http-common.js";
+import {
+ Codec,
+ HttpStatusCode,
+ TalerError,
+ TalerErrorCode,
+ TalerErrorDetail,
+} from "./index.js";
+
+type OperationFailWithBodyOrNever<ErrorEnum, ErrorMap> =
+ ErrorEnum extends keyof ErrorMap ? OperationFailWithBody<ErrorMap> : never;
+
+export type OperationResult<Body, ErrorEnum, K = never> =
+ | OperationOk<Body>
+ | OperationAlternative<ErrorEnum, any>
+ | OperationFail<ErrorEnum>
+ | OperationFailWithBodyOrNever<ErrorEnum, K>;
+
+export function isOperationOk<T, E>(
+ c: OperationResult<T, E>,
+): c is OperationOk<T> {
+ return c.type === "ok";
+}
+
+export function isOperationFail<T, E>(
+ c: OperationResult<T, E>,
+): c is OperationFail<E> {
+ return c.type === "fail";
+}
+
+/**
+ * successful operation
+ */
+export interface OperationOk<BodyT> {
+ type: "ok";
+
+ /**
+ * Parsed response body.
+ */
+ body: BodyT;
+}
+
+/**
+ * unsuccessful operation, see details
+ */
+export interface OperationFail<T> {
+ type: "fail";
+
+ /**
+ * Error case (either HTTP status code or TalerErrorCode)
+ */
+ case: T;
+
+ detail: TalerErrorDetail;
+}
+
+/**
+ * unsuccessful operation, see body
+ */
+export interface OperationAlternative<T, B> {
+ type: "fail";
+
+ case: T;
+ body: B;
+}
+
+export interface OperationFailWithBody<B> {
+ type: "fail";
+
+ case: keyof B;
+ body: B[OperationFailWithBody<B>["case"]];
+}
+
+export async function opSuccessFromHttp<T>(
+ resp: HttpResponse,
+ codec: Codec<T>,
+): Promise<OperationOk<T>> {
+ const body = await readSuccessResponseJsonOrThrow(resp, codec);
+ return { type: "ok" as const, body };
+}
+
+/**
+ * Success case, but instead of the body we're returning a fixed response
+ * to the client.
+ */
+export function opFixedSuccess<T>(body: T): OperationOk<T> {
+ return { type: "ok" as const, body };
+}
+
+export function opEmptySuccess(resp: HttpResponse): OperationOk<void> {
+ return { type: "ok" as const, body: void 0 };
+}
+
+export async function opKnownFailureWithBody<B>(
+ case_: keyof B,
+ body: B[typeof case_],
+): Promise<OperationFailWithBody<B>> {
+ return { type: "fail", case: case_, body };
+}
+
+export async function opKnownAlternativeFailure<T extends HttpStatusCode, B>(
+ resp: HttpResponse,
+ s: T,
+ codec: Codec<B>,
+): Promise<OperationAlternative<T, B>> {
+ const body = (await readResponseJsonOrErrorCode(resp, codec)).response;
+ return { type: "fail", case: s, body };
+}
+
+export async function opKnownHttpFailure<T extends HttpStatusCode>(
+ s: T,
+ resp: HttpResponse,
+): Promise<OperationFail<T>> {
+ const detail = await readTalerErrorResponse(resp);
+ return { type: "fail", case: s, detail };
+}
+
+export function opKnownTalerFailure<T extends TalerErrorCode>(
+ s: T,
+ detail: TalerErrorDetail,
+): OperationFail<T> {
+ return { type: "fail", case: s, detail };
+}
+
+export function opUnknownFailure(resp: HttpResponse, error: TalerErrorDetail): never {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
+ {
+ requestUrl: resp.requestUrl,
+ requestMethod: resp.requestMethod,
+ httpStatusCode: resp.status,
+ errorResponse: error,
+ },
+ `Unexpected HTTP status ${resp.status} in response`,
+ );
+}
+
+/**
+ * Convenience function to throw an error if the operation is not a success.
+ */
+export function narrowOpSuccessOrThrow<Body, ErrorEnum>(
+ opName: string,
+ opRes: OperationResult<Body, ErrorEnum>,
+): asserts opRes is OperationOk<Body> {
+ if (opRes.type !== "ok") {
+ throw TalerError.fromDetail(
+ TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR,
+ {
+ operation: opName,
+ error: String(opRes.case),
+ detail: "detail" in opRes ? opRes.detail : undefined,
+ },
+ `Operation ${opName} failed: ${String(opRes.case)}`,
+ );
+ }
+}
+
+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
+ : never; //error cases just for functions
+
+export type FailCasesByMethod<TT extends object, p extends keyof TT> = Exclude<
+ ResultByMethod<TT, p>,
+ OperationOk<any>
+>;
+
+export type RedirectResult = { redirectURL: URL }
diff --git a/packages/taler-util/src/payto.ts b/packages/taler-util/src/payto.ts
index 8eb0b88a8..a471d0b87 100644
--- a/packages/taler-util/src/payto.ts
+++ b/packages/taler-util/src/payto.ts
@@ -15,6 +15,7 @@
*/
import { generateFakeSegwitAddress } from "./bitcoin.js";
+import { Codec, Context, DecodingError, renderContext } from "./codec.js";
import { URLSearchParams } from "./url.js";
export type PaytoUri =
@@ -23,8 +24,29 @@ export type PaytoUri =
| PaytoUriTalerBank
| PaytoUriBitcoin;
+declare const __payto_str: unique symbol;
+export type PaytoString = string & { [__payto_str]: true };
+
+export function codecForPaytoString(): Codec<PaytoString> {
+ return {
+ decode(x: any, c?: Context): PaytoString {
+ if (typeof x !== "string") {
+ throw new DecodingError(
+ `expected string at ${renderContext(c)} but got ${typeof x}`,
+ );
+ }
+ if (!x.startsWith(paytoPfx)) {
+ throw new DecodingError(
+ `expected start with payto at ${renderContext(c)} but got "${x}"`,
+ );
+ }
+ return x as PaytoString;
+ },
+ };
+}
+
export interface PaytoUriGeneric {
- targetType: string;
+ targetType: PaytoType | string;
targetPath: string;
params: { [name: string]: string };
}
@@ -50,11 +72,77 @@ export interface PaytoUriTalerBank extends PaytoUriGeneric {
export interface PaytoUriBitcoin extends PaytoUriGeneric {
isKnown: true;
targetType: "bitcoin";
+ address: string;
segwitAddrs: Array<string>;
}
const paytoPfx = "payto://";
+export type PaytoType = "iban" | "bitcoin" | "x-taler-bank";
+
+export function buildPayto(
+ type: "iban",
+ iban: string,
+ bic: string | undefined,
+): PaytoUriIBAN;
+export function buildPayto(
+ type: "bitcoin",
+ address: string,
+ reserve: string | undefined,
+): PaytoUriBitcoin;
+export function buildPayto(
+ type: "x-taler-bank",
+ host: string,
+ account: string,
+): PaytoUriTalerBank;
+export function buildPayto(
+ type: PaytoType,
+ first: string,
+ second?: string,
+): PaytoUriGeneric {
+ switch (type) {
+ case "bitcoin": {
+ const uppercased = first.toUpperCase();
+ const result: PaytoUriBitcoin = {
+ isKnown: true,
+ targetType: "bitcoin",
+ targetPath: first,
+ address: uppercased,
+ params: {},
+ segwitAddrs: !second ? [] : generateFakeSegwitAddress(second, first),
+ };
+ return result;
+ }
+ case "iban": {
+ const uppercased = first.toUpperCase();
+ const result: PaytoUriIBAN = {
+ isKnown: true,
+ targetType: "iban",
+ iban: uppercased,
+ params: {},
+ targetPath: !second ? uppercased : `${second}/${uppercased}`,
+ };
+ return result;
+ }
+ case "x-taler-bank": {
+ if (!second) throw Error("missing account for payto://x-taler-bank");
+ const result: PaytoUriTalerBank = {
+ isKnown: true,
+ targetType: "x-taler-bank",
+ host: first,
+ account: second,
+ params: {},
+ targetPath: `${first}/${second}`,
+ };
+ return result;
+ }
+ default: {
+ const unknownType: never = type;
+ throw Error(`unknown payto:// type ${unknownType}`);
+ }
+ }
+}
+
/**
* Add query parameters to a payto URI
*/
@@ -80,14 +168,13 @@ export function addPaytoQueryParams(
* @param p
* @returns
*/
-export function stringifyPaytoUri(p: PaytoUri): string {
- const url = `${paytoPfx}${p.targetType}/${p.targetPath}`;
+export function stringifyPaytoUri(p: PaytoUri): PaytoString {
+ const url = new URL(`${paytoPfx}${p.targetType}/${p.targetPath}`);
const paramList = !p.params ? [] : Object.entries(p.params);
- if (paramList.length > 0) {
- const search = paramList.map(([key, value]) => `${key}=${value}`).join("&");
- return `${url}?${search}`;
- }
- return url;
+ paramList.forEach(([key, value]) => {
+ url.searchParams.set(key, value);
+ });
+ return url.href as PaytoString;
}
/**
@@ -139,13 +226,13 @@ export function parsePaytoUri(s: string): PaytoUri | undefined {
let iban: string | undefined = undefined;
let bic: string | undefined = undefined;
if (parts.length === 1) {
- iban = parts[0];
+ iban = parts[0].toUpperCase();
}
if (parts.length === 2) {
bic = parts[0];
- iban = parts[1];
+ iban = parts[1].toUpperCase();
} else {
- iban = targetPath;
+ iban = targetPath.toUpperCase();
}
return {
isKnown: true,
@@ -163,10 +250,12 @@ export function parsePaytoUri(s: string): PaytoUri | undefined {
? []
: generateFakeSegwitAddress(reserve, targetPath);
+ const uppercased = targetType.toUpperCase();
const result: PaytoUriBitcoin = {
isKnown: true,
targetPath,
targetType,
+ address: uppercased,
params,
segwitAddrs,
};
@@ -180,3 +269,25 @@ export function parsePaytoUri(s: string): PaytoUri | undefined {
isKnown: false,
};
}
+
+export function talerPaytoFromExchangeReserve(
+ exchangeBaseUrl: string,
+ reservePub: string,
+): string {
+ const url = new URL(exchangeBaseUrl);
+ let proto: string;
+ if (url.protocol === "http:") {
+ proto = "taler-reserve-http";
+ } else if (url.protocol === "https:") {
+ proto = "taler-reserve";
+ } else {
+ throw Error(`unsupported exchange base URL protocol (${url.protocol})`);
+ }
+
+ let path = url.pathname;
+ if (!path.endsWith("/")) {
+ path = path + "/";
+ }
+
+ return `payto://${proto}/${url.host}${url.pathname}${reservePub}`;
+}
diff --git a/packages/taler-util/src/promises.ts b/packages/taler-util/src/promises.ts
new file mode 100644
index 000000000..bc1e40260
--- /dev/null
+++ b/packages/taler-util/src/promises.ts
@@ -0,0 +1,112 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 GNUnet e.V.
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * An opened promise.
+ *
+ * @see {@link openPromise}
+ */
+export interface OpenedPromise<T> {
+ promise: Promise<T>;
+ resolve: (val: T) => void;
+ reject: (err: any) => void;
+ lastError?: any;
+}
+
+/**
+ * Get an unresolved promise together with its extracted resolve / reject
+ * function.
+ *
+ * Recent ECMAScript proposals also call this a promise capability.
+ */
+export function openPromise<T>(): OpenedPromise<T> {
+ let resolve: ((x?: any) => void) | null = null;
+ let promiseReject: ((reason?: any) => void) | null = null;
+ const promise = new Promise<T>((res, rej) => {
+ resolve = res;
+ promiseReject = rej;
+ });
+ if (!(resolve && promiseReject)) {
+ // Never happens, unless JS implementation is broken
+ throw Error("JS implementation is broken");
+ }
+ const result: OpenedPromise<T> = { resolve, reject: promiseReject, promise };
+ function saveLastError(reason?: any) {
+ result.lastError = reason;
+ promiseReject!(reason);
+ }
+ result.reject = saveLastError;
+ return result;
+}
+
+export class AsyncCondition {
+ private promCap?: OpenedPromise<void> = undefined;
+ constructor() {}
+
+ wait(): Promise<void> {
+ if (!this.promCap) {
+ this.promCap = openPromise<void>();
+ }
+ return this.promCap.promise;
+ }
+
+ trigger(): void {
+ if (this.promCap) {
+ this.promCap.resolve();
+ }
+ this.promCap = undefined;
+ }
+}
+
+/**
+ * Flag that can be raised to notify asynchronous waiters.
+ *
+ * You can think of it as a promise that can
+ * be un-resolved.
+ */
+export class AsyncFlag {
+ private promCap?: OpenedPromise<void> = undefined;
+ private internalFlagRaised: boolean = false;
+
+ constructor() {}
+
+ /**
+ * Wait until the flag is raised.
+ *
+ * Reset if before returning.
+ */
+ wait(): Promise<void> {
+ if (this.internalFlagRaised) {
+ return Promise.resolve();
+ }
+ if (!this.promCap) {
+ this.promCap = openPromise<void>();
+ }
+ return this.promCap.promise;
+ }
+
+ raise(): void {
+ this.internalFlagRaised = true;
+ if (this.promCap) {
+ this.promCap.resolve();
+ }
+ }
+
+ reset(): void {
+ this.internalFlagRaised = false;
+ this.promCap = undefined;
+ }
+}
diff --git a/packages/taler-util/src/qtart.ts b/packages/taler-util/src/qtart.ts
new file mode 100644
index 000000000..e298a157c
--- /dev/null
+++ b/packages/taler-util/src/qtart.ts
@@ -0,0 +1,35 @@
+// @ts-ignore
+import * as _qjsOsImp from "os";
+// @ts-ignore
+import * as _qjsStdImp from "std";
+
+export interface QjsHttpResp {
+ status: number;
+ data: ArrayBuffer;
+ headers?: string[];
+}
+
+export interface QjsHttpOptions {
+ method: string;
+ debug?: boolean;
+ data?: ArrayBuffer;
+ headers?: string[];
+}
+
+export interface QjsOsLib {
+ fetchHttp(url: string, options?: QjsHttpOptions): Promise<QjsHttpResp>;
+ postMessageToHost(s: string): void;
+ setMessageFromHostHandler(h: (s: string) => void): void;
+ rename(oldPath: string, newPath: string): number;
+ remove(path: string): number;
+}
+
+export interface QjsStdLib {
+ writeFile(filename: string, contents: string): void;
+ loadFile(filename: string): string;
+}
+
+// This is not the nodejs "os" module, but the qjs "os" module.
+export const qjsOs: QjsOsLib = _qjsOsImp as any;
+
+export const qjsStd: QjsStdLib = _qjsStdImp as any;
diff --git a/packages/taler-util/src/rfc3548.ts b/packages/taler-util/src/rfc3548.ts
new file mode 100644
index 000000000..2dd18cdfc
--- /dev/null
+++ b/packages/taler-util/src/rfc3548.ts
@@ -0,0 +1,60 @@
+/*
+ This file is part of GNU Taler
+ (C) 2024 Taler Systems SA
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received 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 { getRandomBytes } from "./taler-crypto.js";
+
+const encTable = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
+
+/**
+ * base32 RFC 3548
+ */
+export function encodeRfc3548Base32(data: ArrayBuffer) {
+ const dataBytes = new Uint8Array(data);
+ let sb = "";
+ const size = data.byteLength;
+ let bitBuf = 0;
+ let numBits = 0;
+ let pos = 0;
+ while (pos < size || numBits > 0) {
+ if (pos < size && numBits < 5) {
+ const d = dataBytes[pos++];
+ bitBuf = (bitBuf << 8) | d;
+ numBits += 8;
+ }
+ if (numBits < 5) {
+ // zero-padding
+ bitBuf = bitBuf << (5 - numBits);
+ numBits = 5;
+ }
+ const v = (bitBuf >>> (numBits - 5)) & 31;
+ sb += encTable[v];
+ numBits -= 5;
+ }
+ return sb;
+}
+
+export function isRfc3548Base32Charset(s: string): boolean {
+ for (let idx = 0; idx < s.length; idx++) {
+ const c = s.charAt(idx);
+ if (encTable.indexOf(c) === -1) return false;
+ }
+ return true;
+}
+
+export function randomRfc3548Base32Key(): string {
+ const buf = getRandomBytes(20);
+ return encodeRfc3548Base32(buf);
+}
diff --git a/packages/taler-util/src/sha256.ts b/packages/taler-util/src/sha256.ts
index 321e5d827..ba8f09279 100644
--- a/packages/taler-util/src/sha256.ts
+++ b/packages/taler-util/src/sha256.ts
@@ -145,7 +145,7 @@ export class HashSha256 {
}
// Resets hash state making it possible
- // to re-use this instance to hash other data.
+ // to reuse this instance to hash other data.
reset(): this {
this.state[0] = 0x6a09e667;
this.state[1] = 0xbb67ae85;
diff --git a/packages/taler-util/src/taler-crypto.test.ts b/packages/taler-util/src/taler-crypto.test.ts
index 913bf4348..021730c7e 100644
--- a/packages/taler-util/src/taler-crypto.test.ts
+++ b/packages/taler-util/src/taler-crypto.test.ts
@@ -21,10 +21,10 @@ import test from "ava";
import {
encodeCrock,
decodeCrock,
- ecdheGetPublic,
+ ecdhGetPublic,
eddsaGetPublic,
- keyExchangeEddsaEcdhe,
- keyExchangeEcdheEddsa,
+ keyExchangeEddsaEcdh,
+ keyExchangeEcdhEddsa,
stringToBytes,
bytesToString,
deriveBSeed,
@@ -37,8 +37,9 @@ import {
getRandomBytes,
bigintToNaclArr,
bigintFromNaclArr,
+ kdf,
} from "./taler-crypto.js";
-import { sha512, kdf } from "./kdf.js";
+import { sha512 } from "./kdf.js";
import * as nacl from "./nacl-fast.js";
import { initNodePrng } from "./prng-node.js";
@@ -127,19 +128,19 @@ test("taler-exchange-tvg eddsa_ecdh", (t) => {
const key_material =
"PKZ42Z56SVK2796HG1QYBRJ6ZQM2T9QGA3JA4AAZ8G7CWK9FPX175Q9JE5P0ZAX3HWWPHAQV4DPCK10R9X3SAXHRV0WF06BHEC2ZTKR";
- const myEcdhePub = ecdheGetPublic(decodeCrock(priv_ecdhe));
+ const myEcdhePub = ecdhGetPublic(decodeCrock(priv_ecdhe));
t.deepEqual(encodeCrock(myEcdhePub), pub_ecdhe);
const myEddsaPub = eddsaGetPublic(decodeCrock(priv_eddsa));
t.deepEqual(encodeCrock(myEddsaPub), pub_eddsa);
- const myKm1 = keyExchangeEddsaEcdhe(
+ const myKm1 = keyExchangeEddsaEcdh(
decodeCrock(priv_eddsa),
decodeCrock(pub_ecdhe),
);
t.deepEqual(encodeCrock(myKm1), key_material);
- const myKm2 = keyExchangeEcdheEddsa(
+ const myKm2 = keyExchangeEcdhEddsa(
decodeCrock(priv_ecdhe),
decodeCrock(pub_eddsa),
);
@@ -193,19 +194,19 @@ test("taler-exchange-tvg eddsa_ecdh #2", (t) => {
const key_material =
"G6RA58N61K7MT3WA13Q7VRTE1FQS6H43RX9HK8Z5TGAB61601GEGX51JRHHQMNKNM2R9AVC1STSGQDRHGKWVYP584YGBCTVMMJYQF30";
- const myEcdhePub = ecdheGetPublic(decodeCrock(priv_ecdhe));
+ const myEcdhePub = ecdhGetPublic(decodeCrock(priv_ecdhe));
t.deepEqual(encodeCrock(myEcdhePub), pub_ecdhe);
const myEddsaPub = eddsaGetPublic(decodeCrock(priv_eddsa));
t.deepEqual(encodeCrock(myEddsaPub), pub_eddsa);
- const myKm1 = keyExchangeEddsaEcdhe(
+ const myKm1 = keyExchangeEddsaEcdh(
decodeCrock(priv_eddsa),
decodeCrock(pub_ecdhe),
);
t.deepEqual(encodeCrock(myKm1), key_material);
- const myKm2 = keyExchangeEcdheEddsa(
+ const myKm2 = keyExchangeEcdhEddsa(
decodeCrock(priv_ecdhe),
decodeCrock(pub_eddsa),
);
diff --git a/packages/taler-util/src/taler-crypto.ts b/packages/taler-util/src/taler-crypto.ts
index 113e4194b..e587773e2 100644
--- a/packages/taler-util/src/taler-crypto.ts
+++ b/packages/taler-util/src/taler-crypto.ts
@@ -22,8 +22,9 @@
* Imports.
*/
import * as nacl from "./nacl-fast.js";
-import { kdf } from "./kdf.js";
+import { hmacSha256, hmacSha512 } from "./kdf.js";
import bigint from "big-integer";
+import * as argon2 from "./argon2.js";
import {
CoinEnvelope,
CoinPublicKeyString,
@@ -35,6 +36,8 @@ import { Logger } from "./logging.js";
import { secretbox } from "./nacl-fast.js";
import * as fflate from "fflate";
import { canonicalJson } from "./helpers.js";
+import { TalerProtocolDuration, TalerProtocolTimestamp } from "./time.js";
+import { AmountLike, Amounts } from "./amounts.js";
export type Flavor<T, FlavorT extends string> = T & {
_flavor?: `taler.${FlavorT}`;
@@ -55,7 +58,56 @@ export function getRandomBytesF<T extends number, N extends string>(
return nacl.randomBytes(n);
}
-const useNative = true;
+export const useNative = true;
+
+/**
+ * Interface of the native Taler runtime library.
+ */
+interface NativeTartLib {
+ decodeUtf8(buf: Uint8Array): string;
+ decodeUtf8(str: string): Uint8Array;
+ randomBytes(n: number): Uint8Array;
+ encodeCrock(buf: Uint8Array | ArrayBuffer): string;
+ decodeCrock(str: string): Uint8Array;
+ hash(buf: Uint8Array): Uint8Array;
+ hashArgon2id(
+ password: Uint8Array,
+ salt: Uint8Array,
+ iterations: number,
+ memorySize: number,
+ hashLength: number,
+ ): Uint8Array;
+ eddsaGetPublic(buf: Uint8Array): Uint8Array;
+ ecdheGetPublic(buf: Uint8Array): Uint8Array;
+ eddsaSign(msg: Uint8Array, priv: Uint8Array): Uint8Array;
+ eddsaVerify(msg: Uint8Array, sig: Uint8Array, pub: Uint8Array): boolean;
+ kdf(
+ outLen: number,
+ ikm: Uint8Array,
+ salt?: Uint8Array,
+ info?: Uint8Array,
+ ): Uint8Array;
+ keyExchangeEcdhEddsa(ecdhPriv: Uint8Array, eddsaPub: Uint8Array): Uint8Array;
+ keyExchangeEddsaEcdh(eddsaPriv: Uint8Array, ecdhPub: Uint8Array): Uint8Array;
+ rsaBlind(hmsg: Uint8Array, bks: Uint8Array, rsaPub: Uint8Array): Uint8Array;
+ rsaUnblind(
+ blindSig: Uint8Array,
+ rsaPub: Uint8Array,
+ bks: Uint8Array,
+ ): Uint8Array;
+ rsaVerify(hmsg: Uint8Array, rsaSig: Uint8Array, rsaPub: Uint8Array): boolean;
+ hashStateInit(): any;
+ hashStateUpdate(st: any, data: Uint8Array): any;
+ hashStateFinish(st: any): Uint8Array;
+}
+
+// @ts-ignore
+let tart: NativeTartLib | undefined;
+
+if (useNative) {
+ // @ts-ignore
+ tart = globalThis._tart;
+}
const encTable = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
@@ -71,7 +123,7 @@ function getValue(chr: string): number {
switch (chr) {
case "O":
case "o":
- a = "0;";
+ a = "0";
break;
case "i":
case "I":
@@ -101,9 +153,8 @@ function getValue(chr: string): number {
}
export function encodeCrock(data: ArrayBuffer): string {
- if (useNative && "_encodeCrock" in globalThis) {
- // @ts-ignore
- return globalThis._encodeCrock(data);
+ if (tart) {
+ return tart.encodeCrock(data);
}
const dataBytes = new Uint8Array(data);
let sb = "";
@@ -129,6 +180,44 @@ export function encodeCrock(data: ArrayBuffer): string {
return sb;
}
+export function kdf(
+ outputLength: number,
+ ikm: Uint8Array,
+ salt?: Uint8Array,
+ info?: Uint8Array,
+): Uint8Array {
+ if (tart) {
+ return tart.kdf(outputLength, ikm, salt, info);
+ }
+ salt = salt ?? new Uint8Array(64);
+ // extract
+ const prk = hmacSha512(salt, ikm);
+
+ info = info ?? new Uint8Array(0);
+
+ // expand
+ const N = Math.ceil(outputLength / 32);
+ const output = new Uint8Array(N * 32);
+ for (let i = 0; i < N; i++) {
+ let buf;
+ if (i == 0) {
+ buf = new Uint8Array(info.byteLength + 1);
+ buf.set(info, 0);
+ } else {
+ buf = new Uint8Array(info.byteLength + 1 + 32);
+ for (let j = 0; j < 32; j++) {
+ buf[j] = output[(i - 1) * 32 + j];
+ }
+ buf.set(info, 32);
+ }
+ buf[buf.length - 1] = i + 1;
+ const chunk = hmacSha256(prk, buf);
+ output.set(chunk, i * 32);
+ }
+
+ return output.slice(0, outputLength);
+}
+
/**
* HMAC-SHA512-SHA256 (see RFC 5869).
*/
@@ -142,9 +231,8 @@ export function kdfKw(args: {
}
export function decodeCrock(encoded: string): Uint8Array {
- if (useNative && "_decodeCrock" in globalThis) {
- // @ts-ignore
- return globalThis._decodeCrock(encoded);
+ if (tart) {
+ return tart.decodeCrock(encoded);
}
const size = encoded.length;
let bitpos = 0;
@@ -173,38 +261,71 @@ export function decodeCrock(encoded: string): Uint8Array {
return out;
}
+export async function hashArgon2id(
+ password: Uint8Array,
+ salt: Uint8Array,
+ iterations: number,
+ memorySize: number,
+ hashLength: number,
+): Promise<Uint8Array> {
+ if (tart) {
+ return tart.hashArgon2id(
+ password,
+ salt,
+ iterations,
+ memorySize,
+ hashLength,
+ );
+ }
+ return await argon2.hashArgon2id(
+ password,
+ salt,
+ iterations,
+ memorySize,
+ hashLength,
+ );
+}
+
export function eddsaGetPublic(eddsaPriv: Uint8Array): Uint8Array {
- if (useNative && "_eddsaGetPublic" in globalThis) {
- // @ts-ignore
- return globalThis._eddsaGetPublic(eddsaPriv);
+ if (tart) {
+ return tart.eddsaGetPublic(eddsaPriv);
}
const pair = nacl.crypto_sign_keyPair_fromSeed(eddsaPriv);
return pair.publicKey;
}
-export function ecdheGetPublic(ecdhePriv: Uint8Array): Uint8Array {
+export function ecdhGetPublic(ecdhePriv: Uint8Array): Uint8Array {
+ if (tart) {
+ return tart.ecdheGetPublic(ecdhePriv);
+ }
return nacl.scalarMult_base(ecdhePriv);
}
-export function keyExchangeEddsaEcdhe(
+export function keyExchangeEddsaEcdh(
eddsaPriv: Uint8Array,
- ecdhePub: Uint8Array,
+ ecdhPub: Uint8Array,
): Uint8Array {
+ if (tart) {
+ return tart.keyExchangeEddsaEcdh(eddsaPriv, ecdhPub);
+ }
const ph = hash(eddsaPriv);
const a = new Uint8Array(32);
for (let i = 0; i < 32; i++) {
a[i] = ph[i];
}
- const x = nacl.scalarMult(a, ecdhePub);
+ const x = nacl.scalarMult(a, ecdhPub);
return hash(x);
}
-export function keyExchangeEcdheEddsa(
- ecdhePriv: Uint8Array & MaterialEcdhePriv,
+export function keyExchangeEcdhEddsa(
+ ecdhPriv: Uint8Array & MaterialEcdhePriv,
eddsaPub: Uint8Array & MaterialEddsaPub,
): Uint8Array {
+ if (tart) {
+ return tart.keyExchangeEcdhEddsa(ecdhPriv, eddsaPub);
+ }
const curve25519Pub = nacl.sign_ed25519_pk_to_curve25519(eddsaPub);
- const x = nacl.scalarMult(ecdhePriv, curve25519Pub);
+ const x = nacl.scalarMult(ecdhPriv, curve25519Pub);
return hash(x);
}
@@ -271,7 +392,7 @@ function csKdfMod(
// Newer versions of node have TextEncoder and TextDecoder as a global,
// just like modern browsers.
// In older versions of node or environments that do not have these
-// globals, they must be polyfilled (by adding them to globa/globalThis)
+// globals, they must be polyfilled (by adding them to global/globalThis)
// before stringToBytes or bytesToString is called the first time.
let encoder: any;
@@ -365,6 +486,9 @@ export function rsaBlind(
bks: Uint8Array,
rsaPubEnc: Uint8Array,
): Uint8Array {
+ if (tart) {
+ return tart.rsaBlind(hm, bks, rsaPubEnc);
+ }
const rsaPub = rsaPubDecode(rsaPubEnc);
const data = rsaFullDomainHash(hm, rsaPub);
const r = rsaBlindingKeyDerive(rsaPub, bks);
@@ -378,6 +502,9 @@ export function rsaUnblind(
rsaPubEnc: Uint8Array,
bks: Uint8Array,
): Uint8Array {
+ if (tart) {
+ return tart.rsaUnblind(sig, rsaPubEnc, bks);
+ }
const rsaPub = rsaPubDecode(rsaPubEnc);
const blinded_s = loadBigInt(sig);
const r = rsaBlindingKeyDerive(rsaPub, bks);
@@ -391,6 +518,9 @@ export function rsaVerify(
rsaSig: Uint8Array,
rsaPubEnc: Uint8Array,
): boolean {
+ if (tart) {
+ return tart.rsaVerify(hm, rsaSig, rsaPubEnc);
+ }
const rsaPub = rsaPubDecode(rsaPubEnc);
const d = rsaFullDomainHash(hm, rsaPub);
const sig = loadBigInt(rsaSig);
@@ -563,7 +693,7 @@ export async function csBlind(
* Unblind operation to unblind the signature
* @param bseed seed to derive secrets
* @param rPub public R received from /csr
- * @param csPub denomination publick key
+ * @param csPub denomination public key
* @param b returned from exchange to select c
* @param csSig blinded signature
* @returns unblinded signature
@@ -591,7 +721,7 @@ export async function csUnblind(
* Verification algorithm for CS signatures
* @param hm message signed
* @param csSig unblinded signature
- * @param csPub denomination publick key
+ * @param csPub denomination public key
* @returns true if valid, false if invalid
*/
export async function csVerify(
@@ -629,14 +759,13 @@ export function createEddsaKeyPair(): EddsaKeyPair {
export function createEcdheKeyPair(): EcdheKeyPair {
const ecdhePriv = nacl.randomBytes(32);
- const ecdhePub = ecdheGetPublic(ecdhePriv);
+ const ecdhePub = ecdhGetPublic(ecdhePriv);
return { ecdhePriv, ecdhePub };
}
export function hash(d: Uint8Array): Uint8Array {
- if (useNative && "_hash" in globalThis) {
- // @ts-ignore
- return globalThis._hash(d);
+ if (tart) {
+ return tart.hash(d);
}
return nacl.hash(d);
}
@@ -664,7 +793,7 @@ const logger = new Logger("talerCrypto.ts");
export function hashCoinEvInner(
coinEv: CoinEnvelope,
- hashState: nacl.HashState,
+ hashState: TalerHashState,
): void {
const hashInputBuf = new ArrayBuffer(4);
const uint8ArrayBuf = new Uint8Array(hashInputBuf);
@@ -723,9 +852,8 @@ export function hashDenomPub(pub: DenominationPubKey): Uint8Array {
}
export function eddsaSign(msg: Uint8Array, eddsaPriv: Uint8Array): Uint8Array {
- if (useNative && "_eddsaSign" in globalThis) {
- // @ts-ignore
- return globalThis._eddsaSign(msg, eddsaPriv);
+ if (tart) {
+ return tart.eddsaSign(msg, eddsaPriv);
}
const pair = nacl.crypto_sign_keyPair_fromSeed(eddsaPriv);
return nacl.sign_detached(msg, pair.secretKey);
@@ -736,14 +864,26 @@ export function eddsaVerify(
sig: Uint8Array,
eddsaPub: Uint8Array,
): boolean {
- if (useNative && "_eddsaVerify" in globalThis) {
- // @ts-ignore
- return globalThis._eddsaVerify(msg, sig, eddsaPub);
+ if (tart) {
+ return tart.eddsaVerify(msg, sig, eddsaPub);
}
return nacl.sign_detached_verify(msg, sig, eddsaPub);
}
-export function createHashContext(): nacl.HashState {
+export interface TalerHashState {
+ update(data: Uint8Array): void;
+ finish(): Uint8Array;
+}
+
+export function createHashContext(): TalerHashState {
+ if (tart) {
+ const t = tart;
+ const st = tart.hashStateInit();
+ return {
+ finish: () => t.hashStateFinish(st),
+ update: (d) => t.hashStateUpdate(st, d),
+ };
+ }
return new nacl.HashState();
}
@@ -763,6 +903,21 @@ export function bufferForUint32(n: number): Uint8Array {
return buf;
}
+/**
+ * This makes the assumption that the uint64 fits a float,
+ * which should be true for all Taler protocol messages.
+ */
+export function bufferForUint64(n: number): Uint8Array {
+ const arrBuf = new ArrayBuffer(8);
+ const buf = new Uint8Array(arrBuf);
+ const dv = new DataView(arrBuf);
+ if (n < 0 || !Number.isInteger(n)) {
+ throw Error("non-negative integer expected");
+ }
+ dv.setBigUint64(0, BigInt(n));
+ return buf;
+}
+
export function bufferForUint8(n: number): Uint8Array {
const arrBuf = new ArrayBuffer(1);
const buf = new Uint8Array(arrBuf);
@@ -828,6 +983,7 @@ export enum TalerSignaturePurpose {
TEST = 4242,
MERCHANT_PAYMENT_OK = 1104,
MERCHANT_CONTRACT = 1101,
+ MERCHANT_REFUND = 1102,
WALLET_COIN_RECOUP = 1203,
WALLET_COIN_LINK = 1204,
WALLET_COIN_RECOUP_REFRESH = 1206,
@@ -837,14 +993,19 @@ export enum TalerSignaturePurpose {
WALLET_PURSE_MERGE = 1213,
WALLET_ACCOUNT_MERGE = 1214,
WALLET_PURSE_ECONTRACT = 1216,
+ WALLET_PURSE_DELETE = 1220,
+ WALLET_COIN_HISTORY = 1209,
EXCHANGE_CONFIRM_RECOUP = 1039,
EXCHANGE_CONFIRM_RECOUP_REFRESH = 1041,
+ TALER_SIGNATURE_AML_DECISION = 1350,
+ TALER_SIGNATURE_AML_QUERY = 1351,
+ TALER_SIGNATURE_MASTER_AML_KEY = 1017,
ANASTASIS_POLICY_UPLOAD = 1400,
ANASTASIS_POLICY_DOWNLOAD = 1401,
SYNC_BACKUP_UPLOAD = 1450,
}
-export const enum WalletAccountMergeFlags {
+export enum WalletAccountMergeFlags {
/**
* Not a legal mode!
*/
@@ -1120,6 +1281,10 @@ export namespace AgeRestriction {
};
}
+ const PublishedAgeRestrictionBaseKey: Edx25519PublicKey = decodeCrock(
+ "CH0VKFDZ2GWRWHQBBGEK9MWV5YDQVJ0RXEE0KYT3NMB69F0R96TG",
+ );
+
export async function restrictionCommitSeeded(
ageMask: number,
age: number,
@@ -1132,19 +1297,32 @@ export namespace AgeRestriction {
const pubs: Edx25519PublicKey[] = [];
const privs: Edx25519PrivateKey[] = [];
- for (let i = 0; i < numPubs; i++) {
+ for (let i = 0; i < numPrivs; i++) {
const privSeed = await kdfKw({
outputLength: 32,
ikm: seed,
- info: stringToBytes("age-restriction-commit"),
+ info: stringToBytes("age-commitment"),
salt: bufferForUint32(i),
});
+
const priv = await Edx25519.keyCreateFromSeed(privSeed);
const pub = await Edx25519.getPublic(priv);
pubs.push(pub);
- if (i < numPrivs) {
- privs.push(priv);
- }
+ privs.push(priv);
+ }
+
+ for (let i = numPrivs; i < numPubs; i++) {
+ const deriveSeed = await kdfKw({
+ outputLength: 32,
+ ikm: seed,
+ info: stringToBytes("age-factor"),
+ salt: bufferForUint32(i),
+ });
+ const pub = await Edx25519.publicKeyDerive(
+ PublishedAgeRestrictionBaseKey,
+ deriveSeed,
+ );
+ pubs.push(pub);
}
return {
@@ -1257,7 +1435,7 @@ export namespace AgeRestriction {
}
// FIXME: make it a branded type!
-type EncryptionNonce = FlavorP<Uint8Array, "EncryptionNonce", 24>;
+export type EncryptionNonce = FlavorP<Uint8Array, "EncryptionNonce", 24>;
async function deriveKey(
keySeed: OpaqueData,
@@ -1272,7 +1450,7 @@ async function deriveKey(
});
}
-async function encryptWithDerivedKey(
+export async function encryptWithDerivedKey(
nonce: EncryptionNonce,
keySeed: OpaqueData,
plaintext: OpaqueData,
@@ -1285,7 +1463,7 @@ async function encryptWithDerivedKey(
const nonceSize = 24;
-async function decryptWithDerivedKey(
+export async function decryptWithDerivedKey(
ciphertext: OpaqueData,
keySeed: OpaqueData,
salt: string,
@@ -1343,6 +1521,7 @@ export function encryptContractForMerge(
contractPriv: ContractPrivateKey,
mergePriv: MergePrivateKey,
contractTerms: any,
+ nonce: EncryptionNonce,
): Promise<OpaqueData> {
const contractTermsCanon = canonicalJson(contractTerms) + "\0";
const contractTermsBytes = stringToBytes(contractTermsCanon);
@@ -1353,14 +1532,15 @@ export function encryptContractForMerge(
mergePriv,
contractTermsCompressed,
]);
- const key = keyExchangeEcdheEddsa(contractPriv, pursePub);
- return encryptWithDerivedKey(getRandomBytesF(24), key, data, mergeSalt);
+ const key = keyExchangeEcdhEddsa(contractPriv, pursePub);
+ return encryptWithDerivedKey(nonce, key, data, mergeSalt);
}
export function encryptContractForDeposit(
pursePub: PursePublicKey,
contractPriv: ContractPrivateKey,
contractTerms: any,
+ nonce: EncryptionNonce,
): Promise<OpaqueData> {
const contractTermsCanon = canonicalJson(contractTerms) + "\0";
const contractTermsBytes = stringToBytes(contractTermsCanon);
@@ -1370,8 +1550,8 @@ export function encryptContractForDeposit(
bufferForUint32(contractTermsBytes.length),
contractTermsCompressed,
]);
- const key = keyExchangeEcdheEddsa(contractPriv, pursePub);
- return encryptWithDerivedKey(getRandomBytesF(24), key, data, depositSalt);
+ const key = keyExchangeEcdhEddsa(contractPriv, pursePub);
+ return encryptWithDerivedKey(nonce, key, data, depositSalt);
}
export interface DecryptForMergeResult {
@@ -1388,7 +1568,7 @@ export async function decryptContractForMerge(
pursePub: PursePublicKey,
contractPriv: ContractPrivateKey,
): Promise<DecryptForMergeResult> {
- const key = keyExchangeEcdheEddsa(contractPriv, pursePub);
+ const key = keyExchangeEcdhEddsa(contractPriv, pursePub);
const dec = await decryptWithDerivedKey(enc, key, mergeSalt);
const mergePriv = dec.slice(8, 8 + 32);
const contractTermsCompressed = dec.slice(8 + 32);
@@ -1408,7 +1588,7 @@ export async function decryptContractForDeposit(
pursePub: PursePublicKey,
contractPriv: ContractPrivateKey,
): Promise<DecryptForDepositResult> {
- const key = keyExchangeEcdheEddsa(contractPriv, pursePub);
+ const key = keyExchangeEcdhEddsa(contractPriv, pursePub);
const dec = await decryptWithDerivedKey(enc, key, depositSalt);
const contractTermsCompressed = dec.slice(8);
const contractTermsBuf = fflate.unzlibSync(contractTermsCompressed);
@@ -1420,3 +1600,63 @@ export async function decryptContractForDeposit(
contractTerms: JSON.parse(contractTermsString),
};
}
+
+export function amountToBuffer(amount: AmountLike): Uint8Array {
+ const amountJ = Amounts.jsonifyAmount(amount);
+ const buffer = new ArrayBuffer(8 + 4 + 12);
+ const dvbuf = new DataView(buffer);
+ const u8buf = new Uint8Array(buffer);
+ const curr = stringToBytes(amountJ.currency);
+ if (typeof dvbuf.setBigUint64 !== "undefined") {
+ dvbuf.setBigUint64(0, BigInt(amountJ.value));
+ } else {
+ const arr = bigint(amountJ.value).toArray(2 ** 8).value;
+ let offset = 8 - arr.length;
+ for (let i = 0; i < arr.length; i++) {
+ dvbuf.setUint8(offset++, arr[i]);
+ }
+ }
+ dvbuf.setUint32(8, amountJ.fraction);
+ u8buf.set(curr, 8 + 4);
+
+ return u8buf;
+}
+
+export function timestampRoundedToBuffer(
+ ts: TalerProtocolTimestamp,
+): Uint8Array {
+ const b = new ArrayBuffer(8);
+ const v = new DataView(b);
+ // The buffer we sign over represents the timestamp in microseconds.
+ if (typeof v.setBigUint64 !== "undefined") {
+ const s = BigInt(ts.t_s) * BigInt(1000 * 1000);
+ v.setBigUint64(0, s);
+ } else {
+ const s =
+ ts.t_s === "never" ? bigint.zero : bigint(ts.t_s).multiply(1000 * 1000);
+ const arr = s.toArray(2 ** 8).value;
+ let offset = 8 - arr.length;
+ for (let i = 0; i < arr.length; i++) {
+ v.setUint8(offset++, arr[i]);
+ }
+ }
+ return new Uint8Array(b);
+}
+
+export function durationRoundedToBuffer(ts: TalerProtocolDuration): Uint8Array {
+ const b = new ArrayBuffer(8);
+ const v = new DataView(b);
+ // The buffer we sign over represents the timestamp in microseconds.
+ if (typeof v.setBigUint64 !== "undefined") {
+ const s = BigInt(ts.d_us);
+ v.setBigUint64(0, s);
+ } else {
+ const s = ts.d_us === "forever" ? bigint.zero : bigint(ts.d_us);
+ const arr = s.toArray(2 ** 8).value;
+ let offset = 8 - arr.length;
+ for (let i = 0; i < arr.length; i++) {
+ v.setUint8(offset++, arr[i]);
+ }
+ }
+ return new Uint8Array(b);
+}
diff --git a/packages/taler-util/src/taler-error-codes.ts b/packages/taler-util/src/taler-error-codes.ts
index ef629954a..c3c008a1c 100644
--- a/packages/taler-util/src/taler-error-codes.ts
+++ b/packages/taler-util/src/taler-error-codes.ts
@@ -22,6 +22,8 @@
*/
export enum TalerErrorCode {
+
+
/**
* Special code to indicate success (no error).
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -29,251 +31,343 @@ export enum TalerErrorCode {
*/
NONE = 0,
+
/**
- * A non-integer error code was returned in the JSON response.
+ * An error response did not include an error code in the format expected by the client. Most likely, the server does not speak the GNU Taler protocol. Check the URL and/or the network connection to the server.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
* (A value of 0 indicates that the error is generated client-side).
*/
INVALID = 1,
+
/**
- * An internal failure happened on the client side.
+ * An internal failure happened on the client side. Details should be in the local logs. Check if you are using the latest available version or file a report with the developers.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_CLIENT_INTERNAL_ERROR = 2,
+
/**
- * The response we got from the server was not even in JSON format.
+ * The response we got from the server was not in the expected format. Most likely, the server does not speak the GNU Taler protocol. Check the URL and/or the network connection to the server.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_INVALID_RESPONSE = 10,
+
/**
- * An operation timed out.
+ * The operation timed out. Trying again might help. Check the network connection.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_TIMEOUT = 11,
+
/**
- * The version string given does not follow the expected CURRENT:REVISION:AGE Format.
+ * The protocol version given by the server does not follow the required format. Most likely, the server does not speak the GNU Taler protocol. Check the URL and/or the network connection to the server.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_VERSION_MALFORMED = 12,
+
/**
- * The service responded with a reply that was in JSON but did not satsify the protocol. Note that invalid cryptographic signatures should have signature-specific error codes.
+ * The service responded with a reply that was in the right data format, but the content did not satisfy the protocol. Please file a bug report.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_REPLY_MALFORMED = 13,
+
/**
- * There is an error in the client-side configuration, for example the base URL specified is malformed.
+ * There is an error in the client-side configuration, for example an option is set to an invalid value. Check the logs and fix the local configuration.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_CONFIGURATION_INVALID = 14,
+
/**
- * The client made a request to a service, but received an error response it does not know how to handle.
+ * The client made a request to a service, but received an error response it does not know how to handle. Please file a bug report.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_UNEXPECTED_REQUEST_ERROR = 15,
+
/**
- * The HTTP method used is invalid for this endpoint.
+ * The token used by the client to authorize the request does not grant the required permissions for the request. Check the requirements and obtain a suitable authorization token to proceed.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GENERIC_TOKEN_PERMISSION_INSUFFICIENT = 16,
+
+
+ /**
+ * The HTTP method used is invalid for this endpoint. This is likely a bug in the client implementation. Check if you are using the latest available version and/or file a report with the developers.
* Returned with an HTTP status code of #MHD_HTTP_METHOD_NOT_ALLOWED (405).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_METHOD_INVALID = 20,
+
/**
- * There is no endpoint defined for the URL provided by the client.
+ * There is no endpoint defined for the URL provided by the client. Check if you used the correct URL and/or file a report with the developers of the client software.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_ENDPOINT_UNKNOWN = 21,
+
/**
- * The JSON in the client's request was malformed (generic parse error).
+ * The JSON in the client's request was malformed. This is likely a bug in the client implementation. Check if you are using the latest available version and/or file a report with the developers.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_JSON_INVALID = 22,
+
/**
- * Some of the HTTP headers provided by the client caused the server to not be able to handle the request.
+ * Some of the HTTP headers provided by the client were malformed and caused the server to not be able to handle the request. This is likely a bug in the client implementation. Check if you are using the latest available version and/or file a report with the developers.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_HTTP_HEADERS_MALFORMED = 23,
+
/**
- * The payto:// URI provided by the client is malformed.
+ * The payto:// URI provided by the client is malformed. Check that you are using the correct syntax as of RFC 8905 and/or that you entered the bank account number correctly.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_PAYTO_URI_MALFORMED = 24,
+
/**
- * A required parameter in the request was missing.
+ * A required parameter in the request was missing. This is likely a bug in the client implementation. Check if you are using the latest available version and/or file a report with the developers.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_PARAMETER_MISSING = 25,
+
/**
- * A parameter in the request was malformed.
+ * A parameter in the request was malformed. This is likely a bug in the client implementation. Check if you are using the latest available version and/or file a report with the developers.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_PARAMETER_MALFORMED = 26,
+
/**
- * The reserve public key given as part of a /reserves/ endpoint was malformed.
+ * The reserve public key was malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_RESERVE_PUB_MALFORMED = 27,
+
/**
- * The currencies involved in the operation do not match.
+ * The body in the request could not be decompressed by the server. This is likely a bug in the client implementation. Check if you are using the latest available version and/or file a report with the developers.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GENERIC_COMPRESSION_INVALID = 28,
+
+
+ /**
+ * The currency involved in the operation is not acceptable for this server. Check your configuration and make sure the currency specified for a given service provider is one of the currencies supported by that provider.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_CURRENCY_MISMATCH = 30,
+
/**
- * The URI is longer than the longest URI the HTTP server is willing to parse.
+ * The URI is longer than the longest URI the HTTP server is willing to parse. If you believe this was a legitimate request, contact the server administrators and/or the software developers to increase the limit.
* Returned with an HTTP status code of #MHD_HTTP_URI_TOO_LONG (414).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_URI_TOO_LONG = 31,
+
/**
- * The body is too large to be permissible for the endpoint.
- * Returned with an HTTP status code of #MHD_HTTP_PAYLOAD_TOO_LARGE (413).
+ * The body is too large to be permissible for the endpoint. If you believe this was a legitimate request, contact the server administrators and/or the software developers to increase the limit.
+ * Returned with an HTTP status code of #MHD_HTTP_CONTENT_TOO_LARGE (413).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_UPLOAD_EXCEEDS_LIMIT = 32,
+
/**
- * The service failed initialize its connection to the database.
+ * The service refused the request due to lack of proper authorization.
+ * Returned with an HTTP status code of #MHD_HTTP_UNAUTHORIZED (401).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GENERIC_UNAUTHORIZED = 40,
+
+
+ /**
+ * The service refused the request as the given authorization token is unknown.
+ * Returned with an HTTP status code of #MHD_HTTP_UNAUTHORIZED (401).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GENERIC_TOKEN_UNKNOWN = 41,
+
+
+ /**
+ * The service refused the request as the given authorization token expired.
+ * Returned with an HTTP status code of #MHD_HTTP_UNAUTHORIZED (401).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GENERIC_TOKEN_EXPIRED = 42,
+
+
+ /**
+ * The service refused the request as the given authorization token is malformed.
+ * Returned with an HTTP status code of #MHD_HTTP_UNAUTHORIZED (401).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GENERIC_TOKEN_MALFORMED = 43,
+
+
+ /**
+ * The service refused the request due to lack of proper rights on the resource.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GENERIC_FORBIDDEN = 44,
+
+
+ /**
+ * The service failed initialize its connection to the database. The system administrator should check that the service has permissions to access the database and that the database is running.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_DB_SETUP_FAILED = 50,
+
/**
- * The service encountered an error event to just start the database transaction.
+ * The service encountered an error event to just start the database transaction. The system administrator should check that the database is running.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_DB_START_FAILED = 51,
+
/**
- * The service failed to store information in its database.
+ * The service failed to store information in its database. The system administrator should check that the database is running and review the service logs.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_DB_STORE_FAILED = 52,
+
/**
- * The service failed to fetch information from its database.
+ * The service failed to fetch information from its database. The system administrator should check that the database is running and review the service logs.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_DB_FETCH_FAILED = 53,
+
/**
- * The service encountered an error event to commit the database transaction (hard, unrecoverable error).
+ * The service encountered an unrecoverable error trying to commit a transaction to the database. The system administrator should check that the database is running and review the service logs.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_DB_COMMIT_FAILED = 54,
+
/**
- * The service encountered an error event to commit the database transaction, even after repeatedly retrying it there was always a conflicting transaction. (This indicates a repeated serialization error; should only happen if some client maliciously tries to create conflicting concurrent transactions.)
+ * The service encountered an error event to commit the database transaction, even after repeatedly retrying it there was always a conflicting transaction. This indicates a repeated serialization error; it should only happen if some client maliciously tries to create conflicting concurrent transactions. It could also be a sign of a missing index. Check if you are using the latest available version and/or file a report with the developers.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_DB_SOFT_FAILURE = 55,
+
/**
- * The service's database is inconsistent and violates service-internal invariants.
+ * The service's database is inconsistent and violates service-internal invariants. Check if you are using the latest available version and/or file a report with the developers.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_DB_INVARIANT_FAILURE = 56,
+
/**
- * The HTTP server experienced an internal invariant failure (bug).
+ * The HTTP server experienced an internal invariant failure (bug). Check if you are using the latest available version and/or file a report with the developers.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_INTERNAL_INVARIANT_FAILURE = 60,
+
/**
- * The service could not compute a cryptographic hash over some JSON value.
+ * The service could not compute a cryptographic hash over some JSON value. Check if you are using the latest available version and/or file a report with the developers.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_FAILED_COMPUTE_JSON_HASH = 61,
+
/**
- * The service could not compute an amount.
+ * The service could not compute an amount. Check if you are using the latest available version and/or file a report with the developers.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_FAILED_COMPUTE_AMOUNT = 62,
+
/**
- * The HTTP server had insufficient memory to parse the request.
+ * The HTTP server had insufficient memory to parse the request. Restarting services periodically can help, especially if Postgres is using excessive amounts of memory. Check with the system administrator to investigate.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_PARSER_OUT_OF_MEMORY = 70,
+
/**
- * The HTTP server failed to allocate memory.
+ * The HTTP server failed to allocate memory. Restarting services periodically can help, especially if Postgres is using excessive amounts of memory. Check with the system administrator to investigate.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_ALLOCATION_FAILURE = 71,
+
/**
- * The HTTP server failed to allocate memory for building JSON reply.
+ * The HTTP server failed to allocate memory for building JSON reply. Restarting services periodically can help, especially if Postgres is using excessive amounts of memory. Check with the system administrator to investigate.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_JSON_ALLOCATION_FAILURE = 72,
+
/**
- * The HTTP server failed to allocate memory for making a CURL request.
+ * The HTTP server failed to allocate memory for making a CURL request. Restarting services periodically can help, especially if Postgres is using excessive amounts of memory. Check with the system administrator to investigate.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_CURL_ALLOCATION_FAILURE = 73,
+
/**
- * The backend could not locate a required template to generate an HTML reply.
- * Returned with an HTTP status code of #MHD_HTTP_NOT_ACCEPTABLE (406).
+ * The backend could not locate a required template to generate an HTML reply. The system administrator should check if the resource files are installed in the correct location and are readable to the service.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_FAILED_TO_LOAD_TEMPLATE = 74,
+
/**
- * The backend could not expand the template to generate an HTML reply.
+ * The backend could not expand the template to generate an HTML reply. The system administrator should investigate the logs and check if the templates are well-formed.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_FAILED_TO_EXPAND_TEMPLATE = 75,
+
/**
* Exchange is badly configured and thus cannot operate.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -281,6 +375,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_BAD_CONFIGURATION = 1000,
+
/**
* Operation specified unknown for this endpoint.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -288,6 +383,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_OPERATION_UNKNOWN = 1001,
+
/**
* The number of segments included in the URI does not match the number of segments expected by the endpoint.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -295,6 +391,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_WRONG_NUMBER_OF_SEGMENTS = 1002,
+
/**
* The same coin was already used with a different denomination previously.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -302,6 +399,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY = 1003,
+
/**
* The public key of given to a "/coins/" endpoint of the exchange was malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -309,6 +407,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_COINS_INVALID_COIN_PUB = 1004,
+
/**
* The exchange is not aware of the denomination key the wallet requested for the operation.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -316,6 +415,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN = 1005,
+
/**
* The signature of the denomination key over the coin is not valid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -323,6 +423,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DENOMINATION_SIGNATURE_INVALID = 1006,
+
/**
* The exchange failed to perform the operation as it could not find the private keys. This is a problem with the exchange setup, not with the client's request.
* Returned with an HTTP status code of #MHD_HTTP_SERVICE_UNAVAILABLE (503).
@@ -330,6 +431,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_KEYS_MISSING = 1007,
+
/**
* Validity period of the denomination lies in the future.
* Returned with an HTTP status code of #MHD_HTTP_PRECONDITION_FAILED (412).
@@ -337,6 +439,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE = 1008,
+
/**
* Denomination key of the coin is past its expiration time for the requested operation.
* Returned with an HTTP status code of #MHD_HTTP_GONE (410).
@@ -344,6 +447,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_DENOMINATION_EXPIRED = 1009,
+
/**
* Denomination key of the coin has been revoked.
* Returned with an HTTP status code of #MHD_HTTP_GONE (410).
@@ -351,6 +455,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_DENOMINATION_REVOKED = 1010,
+
/**
* An operation where the exchange interacted with a security module timed out.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -358,6 +463,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_SECMOD_TIMEOUT = 1011,
+
/**
* The respective coin did not have sufficient residual value for the operation. The "history" in this response provides the "residual_value" of the coin, which may be less than its "original_value".
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -365,6 +471,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_INSUFFICIENT_FUNDS = 1012,
+
/**
* The exchange had an internal error reconstructing the transaction history of the coin that was being processed.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -372,6 +479,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_COIN_HISTORY_COMPUTATION_FAILED = 1013,
+
/**
* The exchange failed to obtain the transaction history of the given coin from the database while generating an insufficient funds errors.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -379,6 +487,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_HISTORY_DB_ERROR_INSUFFICIENT_FUNDS = 1014,
+
/**
* The same coin was already used with a different age hash previously.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -386,6 +495,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH = 1015,
+
/**
* The requested operation is not valid for the cipher used by the selected denomination.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -393,6 +503,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_INVALID_DENOMINATION_CIPHER_FOR_OPERATION = 1016,
+
/**
* The provided arguments for the operation use inconsistent ciphers.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -400,6 +511,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_CIPHER_MISMATCH = 1017,
+
/**
* The number of denominations specified in the request exceeds the limit of the exchange.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -407,6 +519,15 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_NEW_DENOMS_ARRAY_SIZE_EXCESSIVE = 1018,
+
+ /**
+ * The coin is not known to the exchange (yet).
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_COIN_UNKNOWN = 1019,
+
+
/**
* The time at the server is too far off from the time specified in the request. Most likely the client system time is wrong.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -414,6 +535,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_CLOCK_SKEW = 1020,
+
/**
* The specified amount for the coin is higher than the value of the denomination of the coin.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -421,6 +543,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_AMOUNT_EXCEEDS_DENOMINATION_VALUE = 1021,
+
/**
* The exchange was not properly configured with global fees.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -428,6 +551,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_GLOBAL_FEES_MISSING = 1022,
+
/**
* The exchange was not properly configured with wire fees.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -435,6 +559,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_WIRE_FEES_MISSING = 1023,
+
/**
* The purse public key was malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -442,6 +567,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_PURSE_PUB_MALFORMED = 1024,
+
/**
* The purse is unknown.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -449,6 +575,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_PURSE_UNKNOWN = 1025,
+
/**
* The purse has expired.
* Returned with an HTTP status code of #MHD_HTTP_GONE (410).
@@ -456,6 +583,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_PURSE_EXPIRED = 1026,
+
/**
* The exchange has no information about the "reserve_pub" that was given.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -463,6 +591,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_RESERVE_UNKNOWN = 1027,
+
/**
* The exchange is not allowed to proceed with the operation until the client has satisfied a KYC check.
* Returned with an HTTP status code of #MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS (451).
@@ -470,6 +599,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_KYC_REQUIRED = 1028,
+
/**
* Inconsistency between provided age commitment and attest: either none or both must be provided
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -477,6 +607,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_DEPOSIT_COIN_CONFLICTING_ATTEST_VS_AGE_COMMITMENT = 1029,
+
/**
* The provided attestation for the minimum age couldn't be verified by the exchange.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -484,6 +615,63 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_DEPOSIT_COIN_AGE_ATTESTATION_FAILURE = 1030,
+
+ /**
+ * The purse was deleted.
+ * Returned with an HTTP status code of #MHD_HTTP_GONE (410).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_PURSE_DELETED = 1031,
+
+
+ /**
+ * The public key of the AML officer in the URL was malformed.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_AML_OFFICER_PUB_MALFORMED = 1032,
+
+
+ /**
+ * The signature affirming the GET request of the AML officer is invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_AML_OFFICER_GET_SIGNATURE_INVALID = 1033,
+
+
+ /**
+ * The specified AML officer does not have access at this time.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_AML_OFFICER_ACCESS_DENIED = 1034,
+
+
+ /**
+ * The requested operation is denied pending the resolution of an anti-money laundering investigation by the exchange operator. This is a manual process, please wait and retry later.
+ * Returned with an HTTP status code of #MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS (451).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_AML_PENDING = 1035,
+
+
+ /**
+ * The requested operation is denied as the account was frozen on suspicion of money laundering. Please contact the exchange operator.
+ * Returned with an HTTP status code of #MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS (451).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_AML_FROZEN = 1036,
+
+
+ /**
+ * The exchange failed to start a KYC attribute conversion helper process. It is likely configured incorrectly.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_KYC_CONVERTER_FAILED = 1037,
+
+
/**
* The exchange did not find information about the specified transaction in the database.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -491,6 +679,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DEPOSITS_GET_NOT_FOUND = 1100,
+
/**
* The wire hash of given to a "/deposits/" handler was malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -498,6 +687,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DEPOSITS_GET_INVALID_H_WIRE = 1101,
+
/**
* The merchant key of given to a "/deposits/" handler was malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -505,6 +695,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DEPOSITS_GET_INVALID_MERCHANT_PUB = 1102,
+
/**
* The hash of the contract terms given to a "/deposits/" handler was malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -512,6 +703,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DEPOSITS_GET_INVALID_H_CONTRACT_TERMS = 1103,
+
/**
* The coin public key of given to a "/deposits/" handler was malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -519,6 +711,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DEPOSITS_GET_INVALID_COIN_PUB = 1104,
+
/**
* The signature returned by the exchange in a /deposits/ request was malformed.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -526,6 +719,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DEPOSITS_GET_INVALID_SIGNATURE_BY_EXCHANGE = 1105,
+
/**
* The signature of the merchant is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -533,6 +727,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DEPOSITS_GET_MERCHANT_SIGNATURE_INVALID = 1106,
+
/**
* The provided policy data was not accepted
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -540,6 +735,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DEPOSITS_POLICY_NOT_ACCEPTED = 1107,
+
/**
* The given reserve does not have sufficient funds to admit the requested withdraw operation at this time. The response includes the current "balance" of the reserve as well as the transaction "history" that lead to this balance.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -547,6 +743,15 @@ export enum TalerErrorCode {
*/
EXCHANGE_WITHDRAW_INSUFFICIENT_FUNDS = 1150,
+
+ /**
+ * The given reserve does not have sufficient funds to admit the requested age-withdraw operation at this time. The response includes the current "balance" of the reserve as well as the transaction "history" that lead to this balance.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_AGE_WITHDRAW_INSUFFICIENT_FUNDS = 1151,
+
+
/**
* The amount to withdraw together with the fee exceeds the numeric range for Taler amounts. This is not a client failure, as the coin value and fees come from the exchange's configuration.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -554,6 +759,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_WITHDRAW_AMOUNT_FEE_OVERFLOW = 1152,
+
/**
* The exchange failed to create the signature using the denomination key.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -561,6 +767,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_WITHDRAW_SIGNATURE_FAILED = 1153,
+
/**
* The signature of the reserve is not valid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -568,12 +775,22 @@ export enum TalerErrorCode {
*/
EXCHANGE_WITHDRAW_RESERVE_SIGNATURE_INVALID = 1154,
+
/**
* When computing the reserve history, we ended up with a negative overall balance, which should be impossible.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
* (A value of 0 indicates that the error is generated client-side).
*/
- EXCHANGE_WITHDRAW_HISTORY_ERROR_INSUFFICIENT_FUNDS = 1155,
+ EXCHANGE_RESERVE_HISTORY_ERROR_INSUFFICIENT_FUNDS = 1155,
+
+
+ /**
+ * The reserve did not have sufficient funds in it to pay for a full reserve history statement.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GET_RESERVE_HISTORY_ERROR_INSUFFICIENT_BALANCE = 1156,
+
/**
* Withdraw period of the coin to be withdrawn is in the past.
@@ -582,6 +799,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_WITHDRAW_DENOMINATION_KEY_LOST = 1158,
+
/**
* The client failed to unblind the blind signature.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -589,6 +807,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_WITHDRAW_UNBLIND_FAILURE = 1159,
+
/**
* The client re-used a withdraw nonce, which is not allowed.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -596,6 +815,47 @@ export enum TalerErrorCode {
*/
EXCHANGE_WITHDRAW_NONCE_REUSE = 1160,
+
+ /**
+ * The client provided an unknown commitment for an age-withdraw request.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_AGE_WITHDRAW_COMMITMENT_UNKNOWN = 1161,
+
+
+ /**
+ * The total sum of amounts from the denominations did overflow.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_AGE_WITHDRAW_AMOUNT_OVERFLOW = 1162,
+
+
+ /**
+ * The total sum of value and fees from the denominations differs from the committed amount with fees.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_AGE_WITHDRAW_AMOUNT_INCORRECT = 1163,
+
+
+ /**
+ * The original commitment differs from the calculated hash
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_AGE_WITHDRAW_REVEAL_INVALID_HASH = 1164,
+
+
+ /**
+ * The maximum age in the commitment is too large for the reserve
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_AGE_WITHDRAW_MAXIMUM_AGE_TOO_LARGE = 1165,
+
+
/**
* The batch withdraw included a planchet that was already withdrawn. This is not allowed.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -603,6 +863,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_WITHDRAW_BATCH_IDEMPOTENT_PLANCHET = 1175,
+
/**
* The signature made by the coin over the deposit permission is not valid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -610,6 +871,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DEPOSIT_COIN_SIGNATURE_INVALID = 1205,
+
/**
* The same coin was already deposited for the same merchant and contract with other details.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -617,6 +879,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DEPOSIT_CONFLICTING_CONTRACT = 1206,
+
/**
* The stated value of the coin after the deposit fee is subtracted would be negative.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -624,6 +887,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DEPOSIT_NEGATIVE_VALUE_AFTER_FEE = 1207,
+
/**
* The stated refund deadline is after the wire deadline.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -631,6 +895,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DEPOSIT_REFUND_DEADLINE_AFTER_WIRE_DEADLINE = 1208,
+
/**
* The stated wire deadline is "never", which makes no sense.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -638,6 +903,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DEPOSIT_WIRE_DEADLINE_IS_NEVER = 1209,
+
/**
* The exchange failed to canonicalize and hash the given wire format. For example, the merchant failed to provide the "salt" or a valid payto:// URI in the wire details. Note that while the exchange will do some basic sanity checking on the wire details, it cannot warrant that the banking system will ultimately be able to route to the specified address, even if this check passed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -645,6 +911,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DEPOSIT_INVALID_WIRE_FORMAT_JSON = 1210,
+
/**
* The hash of the given wire address does not match the wire hash specified in the proposal data.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -652,6 +919,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DEPOSIT_INVALID_WIRE_FORMAT_CONTRACT_HASH_CONFLICT = 1211,
+
/**
* The signature provided by the exchange is not valid.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -659,6 +927,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DEPOSIT_INVALID_SIGNATURE_BY_EXCHANGE = 1221,
+
/**
* The deposited amount is smaller than the deposit fee, which would result in a negative contribution.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -666,6 +935,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DEPOSIT_FEE_ABOVE_AMOUNT = 1222,
+
/**
* The proof of policy fulfillment was invalid.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -673,26 +943,22 @@ export enum TalerErrorCode {
*/
EXCHANGE_EXTENSIONS_INVALID_FULFILLMENT = 1240,
- /**
- * The reserve balance, status or history was requested for a reserve which is not known to the exchange.
- * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
- * (A value of 0 indicates that the error is generated client-side).
- */
- EXCHANGE_RESERVES_STATUS_UNKNOWN = 1250,
/**
- * The reserve status was requested with a bad signature.
+ * The coin history was requested with a bad signature.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
* (A value of 0 indicates that the error is generated client-side).
*/
- EXCHANGE_RESERVES_STATUS_BAD_SIGNATURE = 1251,
+ EXCHANGE_COIN_HISTORY_BAD_SIGNATURE = 1251,
+
/**
* The reserve history was requested with a bad signature.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
* (A value of 0 indicates that the error is generated client-side).
*/
- EXCHANGE_RESERVES_HISTORY_BAD_SIGNATURE = 1252,
+ EXCHANGE_RESERVE_HISTORY_BAD_SIGNATURE = 1252,
+
/**
* The exchange encountered melt fees exceeding the melted coin's contribution.
@@ -701,6 +967,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MELT_FEES_EXCEED_CONTRIBUTION = 1302,
+
/**
* The signature made with the coin to be melted is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -708,6 +975,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MELT_COIN_SIGNATURE_INVALID = 1303,
+
/**
* The denomination of the given coin has past its expiration date and it is also not a valid zombie (that is, was not refreshed with the fresh coin being subjected to recoup).
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -715,6 +983,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MELT_COIN_EXPIRED_NO_ZOMBIE = 1305,
+
/**
* The signature returned by the exchange in a melt request was malformed.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -722,6 +991,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MELT_INVALID_SIGNATURE_BY_EXCHANGE = 1306,
+
/**
* The provided transfer keys do not match up with the original commitment. Information about the original commitment is included in the response.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -729,6 +999,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFRESHES_REVEAL_COMMITMENT_VIOLATION = 1353,
+
/**
* Failed to produce the blinded signatures over the coins to be returned.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -736,6 +1007,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFRESHES_REVEAL_SIGNING_ERROR = 1354,
+
/**
* The exchange is unaware of the refresh session specified in the request.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -743,6 +1015,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFRESHES_REVEAL_SESSION_UNKNOWN = 1355,
+
/**
* The size of the cut-and-choose dimension of the private transfer keys request does not match #TALER_CNC_KAPPA - 1.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -750,6 +1023,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFRESHES_REVEAL_CNC_TRANSFER_ARRAY_SIZE_INVALID = 1356,
+
/**
* The number of envelopes given does not match the number of denomination keys given.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -757,6 +1031,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFRESHES_REVEAL_NEW_DENOMS_ARRAY_SIZE_MISMATCH = 1358,
+
/**
* The exchange encountered a numeric overflow totaling up the cost for the refresh operation.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -764,6 +1039,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFRESHES_REVEAL_COST_CALCULATION_OVERFLOW = 1359,
+
/**
* The exchange's cost calculation shows that the melt amount is below the costs of the transaction.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -771,6 +1047,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFRESHES_REVEAL_AMOUNT_INSUFFICIENT = 1360,
+
/**
* The signature made with the coin over the link data is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -778,6 +1055,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFRESHES_REVEAL_LINK_SIGNATURE_INVALID = 1361,
+
/**
* The refresh session hash given to a /refreshes/ handler was malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -785,6 +1063,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFRESHES_REVEAL_INVALID_RCH = 1362,
+
/**
* Operation specified invalid for this endpoint.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -792,6 +1071,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFRESHES_REVEAL_OPERATION_INVALID = 1363,
+
/**
* The client provided age commitment data, but age restriction is not supported on this server.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -799,6 +1079,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFRESHES_REVEAL_AGE_RESTRICTION_NOT_SUPPORTED = 1364,
+
/**
* The client provided invalid age commitment data: missing, not an array, or array of invalid size.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -806,6 +1087,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFRESHES_REVEAL_AGE_RESTRICTION_COMMITMENT_INVALID = 1365,
+
/**
* The coin specified in the link request is unknown to the exchange.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -813,6 +1095,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_LINK_COIN_UNKNOWN = 1400,
+
/**
* The public key of given to a /transfers/ handler was malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -820,6 +1103,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_TRANSFERS_GET_WTID_MALFORMED = 1450,
+
/**
* The exchange did not find information about the specified wire transfer identifier in the database.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -827,6 +1111,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_TRANSFERS_GET_WTID_NOT_FOUND = 1451,
+
/**
* The exchange did not find information about the wire transfer fees it charged.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -834,6 +1119,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_TRANSFERS_GET_WIRE_FEE_NOT_FOUND = 1452,
+
/**
* The exchange found a wire fee that was above the total transfer value (and thus could not have been charged).
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -841,6 +1127,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_TRANSFERS_GET_WIRE_FEE_INCONSISTENT = 1453,
+
/**
* The wait target of the URL was not in the set of expected values.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -848,6 +1135,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSES_INVALID_WAIT_TARGET = 1475,
+
/**
* The signature on the purse status returned by the exchange was invalid.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -855,6 +1143,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSES_GET_INVALID_SIGNATURE_BY_EXCHANGE = 1476,
+
/**
* The exchange knows literally nothing about the coin we were asked to refund. But without a transaction history, we cannot issue a refund. This is kind-of OK, the owner should just refresh it directly without executing the refund.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -862,6 +1151,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFUND_COIN_NOT_FOUND = 1500,
+
/**
* We could not process the refund request as the coin's transaction history does not permit the requested refund because then refunds would exceed the deposit amount. The "history" in the response proves this.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -869,6 +1159,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFUND_CONFLICT_DEPOSIT_INSUFFICIENT = 1501,
+
/**
* The exchange knows about the coin we were asked to refund, but not about the specific /deposit operation. Hence, we cannot issue a refund (as we do not know if this merchant public key is authorized to do a refund).
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -876,6 +1167,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFUND_DEPOSIT_NOT_FOUND = 1502,
+
/**
* The exchange can no longer refund the customer/coin as the money was already transferred (paid out) to the merchant. (It should be past the refund deadline.)
* Returned with an HTTP status code of #MHD_HTTP_GONE (410).
@@ -883,6 +1175,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFUND_MERCHANT_ALREADY_PAID = 1503,
+
/**
* The refund fee specified for the request is lower than the refund fee charged by the exchange for the given denomination key of the refunded coin.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -890,6 +1183,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFUND_FEE_TOO_LOW = 1504,
+
/**
* The refunded amount is smaller than the refund fee, which would result in a negative refund.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -897,6 +1191,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFUND_FEE_ABOVE_AMOUNT = 1505,
+
/**
* The signature of the merchant is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -904,6 +1199,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFUND_MERCHANT_SIGNATURE_INVALID = 1506,
+
/**
* Merchant backend failed to create the refund confirmation signature.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -911,6 +1207,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFUND_MERCHANT_SIGNING_FAILED = 1507,
+
/**
* The signature returned by the exchange in a refund request was malformed.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -918,6 +1215,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFUND_INVALID_SIGNATURE_BY_EXCHANGE = 1508,
+
/**
* The failure proof returned by the exchange is incorrect.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -925,6 +1223,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFUND_INVALID_FAILURE_PROOF_BY_EXCHANGE = 1509,
+
/**
* Conflicting refund granted before with different amount but same refund transaction ID.
* Returned with an HTTP status code of #MHD_HTTP_FAILED_DEPENDENCY (424).
@@ -932,6 +1231,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFUND_INCONSISTENT_AMOUNT = 1510,
+
/**
* The given coin signature is invalid for the request.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -939,6 +1239,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RECOUP_SIGNATURE_INVALID = 1550,
+
/**
* The exchange could not find the corresponding withdraw operation. The request is denied.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -946,6 +1247,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RECOUP_WITHDRAW_NOT_FOUND = 1551,
+
/**
* The coin's remaining balance is zero. The request is denied.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -953,6 +1255,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RECOUP_COIN_BALANCE_ZERO = 1552,
+
/**
* The exchange failed to reproduce the coin's blinding.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -960,6 +1263,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RECOUP_BLINDING_FAILED = 1553,
+
/**
* The coin's remaining balance is zero. The request is denied.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -967,6 +1271,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RECOUP_COIN_BALANCE_NEGATIVE = 1554,
+
/**
* The coin's denomination has not been revoked yet.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -974,6 +1279,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RECOUP_NOT_ELIGIBLE = 1555,
+
/**
* The given coin signature is invalid for the request.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -981,6 +1287,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RECOUP_REFRESH_SIGNATURE_INVALID = 1575,
+
/**
* The exchange could not find the corresponding melt operation. The request is denied.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -988,6 +1295,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RECOUP_REFRESH_MELT_NOT_FOUND = 1576,
+
/**
* The exchange failed to reproduce the coin's blinding.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -995,6 +1303,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RECOUP_REFRESH_BLINDING_FAILED = 1578,
+
/**
* The coin's denomination has not been revoked yet.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1002,6 +1311,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RECOUP_REFRESH_NOT_ELIGIBLE = 1580,
+
/**
* This exchange does not allow clients to request /keys for times other than the current (exchange) time.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1009,6 +1319,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_KEYS_TIMETRAVEL_FORBIDDEN = 1600,
+
/**
* A signature in the server's response was malformed.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -1016,6 +1327,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_WIRE_SIGNATURE_INVALID = 1650,
+
/**
* No bank accounts are enabled for the exchange. The administrator should enable-account using the taler-exchange-offline tool.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -1023,6 +1335,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_WIRE_NO_ACCOUNTS_CONFIGURED = 1651,
+
/**
* The payto:// URI stored in the exchange database for its bank account is malformed.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -1030,6 +1343,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_WIRE_INVALID_PAYTO_CONFIGURED = 1652,
+
/**
* No wire fees are configured for an enabled wire method of the exchange. The administrator must set the wire-fee using the taler-exchange-offline tool.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -1037,6 +1351,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_WIRE_FEES_NOT_CONFIGURED = 1653,
+
/**
* This purse was previously created with different meta data.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1044,6 +1359,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RESERVES_PURSE_CREATE_CONFLICTING_META_DATA = 1675,
+
/**
* This purse was previously merged with different meta data.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1051,6 +1367,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RESERVES_PURSE_MERGE_CONFLICTING_META_DATA = 1676,
+
/**
* The reserve has insufficient funds to create another purse.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1058,6 +1375,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RESERVES_PURSE_CREATE_INSUFFICIENT_FUNDS = 1677,
+
/**
* The purse fee specified for the request is lower than the purse fee charged by the exchange at this time.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -1065,13 +1383,39 @@ export enum TalerErrorCode {
*/
EXCHANGE_RESERVES_PURSE_FEE_TOO_LOW = 1678,
+
/**
- * The exchange failed to talk to the process responsible for its private denomination keys.
- * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * The payment request cannot be deleted anymore, as it either already completed or timed out.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_PURSE_DELETE_ALREADY_DECIDED = 1679,
+
+
+ /**
+ * The signature affirming the purse deletion is invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_PURSE_DELETE_SIGNATURE_INVALID = 1680,
+
+
+ /**
+ * Withdrawal from the reserve requires age restriction to be set.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_RESERVES_AGE_RESTRICTION_REQUIRED = 1681,
+
+
+ /**
+ * The exchange failed to talk to the process responsible for its private denomination keys or the helpers had no denominations (properly) configured.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
* (A value of 0 indicates that the error is generated client-side).
*/
EXCHANGE_DENOMINATION_HELPER_UNAVAILABLE = 1700,
+
/**
* The response from the denomination key helper process was malformed.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -1079,6 +1423,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DENOMINATION_HELPER_BUG = 1701,
+
/**
* The helper refuses to sign with the key, because it is too early: the validity period has not yet started.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -1086,6 +1431,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DENOMINATION_HELPER_TOO_EARLY = 1702,
+
/**
* The signature of the exchange on the reply was invalid.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -1093,13 +1439,15 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_DEPOSIT_EXCHANGE_SIGNATURE_INVALID = 1725,
+
/**
* The exchange failed to talk to the process responsible for its private signing keys.
- * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
* (A value of 0 indicates that the error is generated client-side).
*/
EXCHANGE_SIGNKEY_HELPER_UNAVAILABLE = 1750,
+
/**
* The response from the online signing key helper process was malformed.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -1107,6 +1455,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_SIGNKEY_HELPER_BUG = 1751,
+
/**
* The helper refuses to sign with the key, because it is too early: the validity period has not yet started.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -1114,6 +1463,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_SIGNKEY_HELPER_TOO_EARLY = 1752,
+
/**
* The purse expiration time is in the past at the time of its creation.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -1121,6 +1471,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RESERVES_PURSE_EXPIRATION_BEFORE_NOW = 1775,
+
/**
* The purse expiration time is set to never, which is not allowed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -1128,6 +1479,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RESERVES_PURSE_EXPIRATION_IS_NEVER = 1776,
+
/**
* The signature affirming the merge of the purse is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1135,6 +1487,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RESERVES_PURSE_MERGE_SIGNATURE_INVALID = 1777,
+
/**
* The signature by the reserve affirming the merge is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1142,6 +1495,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RESERVES_RESERVE_MERGE_SIGNATURE_INVALID = 1778,
+
/**
* The signature by the reserve affirming the open operation is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1149,6 +1503,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RESERVES_OPEN_BAD_SIGNATURE = 1785,
+
/**
* The signature by the reserve affirming the close operation is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1156,6 +1511,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RESERVES_CLOSE_BAD_SIGNATURE = 1786,
+
/**
* The signature by the reserve affirming the attestion request is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1163,6 +1519,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RESERVES_ATTEST_BAD_SIGNATURE = 1787,
+
/**
* The exchange does not know an origin account to which the remaining reserve balance could be wired to, and the wallet failed to provide one.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1170,6 +1527,15 @@ export enum TalerErrorCode {
*/
EXCHANGE_RESERVES_CLOSE_NO_TARGET_ACCOUNT = 1788,
+
+ /**
+ * The reserve balance is insufficient to pay for the open operation.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_RESERVES_OPEN_INSUFFICIENT_FUNDS = 1789,
+
+
/**
* The auditor that was supposed to be disabled is unknown to this exchange.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1177,6 +1543,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_AUDITOR_NOT_FOUND = 1800,
+
/**
* The exchange has a more recently signed conflicting instruction and is thus refusing the current change (replay detected).
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1184,6 +1551,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_AUDITOR_MORE_RECENT_PRESENT = 1801,
+
/**
* The signature to add or enable the auditor does not validate.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1191,6 +1559,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_AUDITOR_ADD_SIGNATURE_INVALID = 1802,
+
/**
* The signature to disable the auditor does not validate.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1198,6 +1567,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_AUDITOR_DEL_SIGNATURE_INVALID = 1803,
+
/**
* The signature to revoke the denomination does not validate.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1205,6 +1575,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_DENOMINATION_REVOKE_SIGNATURE_INVALID = 1804,
+
/**
* The signature to revoke the online signing key does not validate.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1212,6 +1583,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_SIGNKEY_REVOKE_SIGNATURE_INVALID = 1805,
+
/**
* The exchange has a more recently signed conflicting instruction and is thus refusing the current change (replay detected).
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1219,6 +1591,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_WIRE_MORE_RECENT_PRESENT = 1806,
+
/**
* The signingkey specified is unknown to the exchange.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1226,6 +1599,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_KEYS_SIGNKEY_UNKNOWN = 1807,
+
/**
* The signature to publish wire account does not validate.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1233,6 +1607,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_WIRE_DETAILS_SIGNATURE_INVALID = 1808,
+
/**
* The signature to add the wire account does not validate.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1240,6 +1615,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_WIRE_ADD_SIGNATURE_INVALID = 1809,
+
/**
* The signature to disable the wire account does not validate.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1247,6 +1623,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_WIRE_DEL_SIGNATURE_INVALID = 1810,
+
/**
* The wire account to be disabled is unknown to the exchange.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1254,6 +1631,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_WIRE_NOT_FOUND = 1811,
+
/**
* The signature to affirm wire fees does not validate.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1261,6 +1639,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_WIRE_FEE_SIGNATURE_INVALID = 1812,
+
/**
* The signature conflicts with a previous signature affirming different fees.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1268,6 +1647,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_WIRE_FEE_MISMATCH = 1813,
+
/**
* The signature affirming the denomination key is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1275,6 +1655,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_KEYS_DENOMKEY_ADD_SIGNATURE_INVALID = 1814,
+
/**
* The signature affirming the signing key is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1282,6 +1663,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_KEYS_SIGNKEY_ADD_SIGNATURE_INVALID = 1815,
+
/**
* The signature conflicts with a previous signature affirming different fees.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1289,6 +1671,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_GLOBAL_FEE_MISMATCH = 1816,
+
/**
* The signature affirming the fee structure is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1296,6 +1679,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_GLOBAL_FEE_SIGNATURE_INVALID = 1817,
+
/**
* The signature affirming the profit drain is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1303,6 +1687,55 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_DRAIN_PROFITS_SIGNATURE_INVALID = 1818,
+
+ /**
+ * The signature affirming the AML decision is invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_AML_DECISION_ADD_SIGNATURE_INVALID = 1825,
+
+
+ /**
+ * The AML officer specified is not allowed to make AML decisions right now.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_AML_DECISION_INVALID_OFFICER = 1826,
+
+
+ /**
+ * There is a more recent AML decision on file. The decision was rejected as timestamps of AML decisions must be monotonically increasing.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_AML_DECISION_MORE_RECENT_PRESENT = 1827,
+
+
+ /**
+ * There AML decision would impose an AML check of a type that is not provided by any KYC provider known to the exchange.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_AML_DECISION_UNKNOWN_CHECK = 1828,
+
+
+ /**
+ * The signature affirming the change in the AML officer status is invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_MANAGEMENT_UPDATE_AML_OFFICER_SIGNATURE_INVALID = 1830,
+
+
+ /**
+ * A more recent decision about the AML officer status is known to the exchange.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_MANAGEMENT_AML_OFFICERS_MORE_RECENT_PRESENT = 1831,
+
+
/**
* The purse was previously created with different meta data.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1310,6 +1743,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_CREATE_CONFLICTING_META_DATA = 1850,
+
/**
* The purse was previously created with a different contract.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1317,6 +1751,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_CREATE_CONFLICTING_CONTRACT_STORED = 1851,
+
/**
* A coin signature for a deposit into the purse is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1324,6 +1759,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_CREATE_COIN_SIGNATURE_INVALID = 1852,
+
/**
* The purse expiration time is in the past.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -1331,6 +1767,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_CREATE_EXPIRATION_BEFORE_NOW = 1853,
+
/**
* The purse expiration time is "never".
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -1338,6 +1775,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_CREATE_EXPIRATION_IS_NEVER = 1854,
+
/**
* The purse signature over the purse meta data is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1345,6 +1783,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_CREATE_SIGNATURE_INVALID = 1855,
+
/**
* The signature over the encrypted contract is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1352,6 +1791,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_ECONTRACT_SIGNATURE_INVALID = 1856,
+
/**
* The signature from the exchange over the confirmation is invalid.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -1359,6 +1799,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_CREATE_EXCHANGE_SIGNATURE_INVALID = 1857,
+
/**
* The coin was previously deposited with different meta data.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1366,6 +1807,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_DEPOSIT_CONFLICTING_META_DATA = 1858,
+
/**
* The encrypted contract was previously uploaded with different meta data.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1373,6 +1815,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_ECONTRACT_CONFLICTING_META_DATA = 1859,
+
/**
* The deposited amount is less than the purse fee.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -1380,6 +1823,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_CREATE_PURSE_NEGATIVE_VALUE_AFTER_FEE = 1860,
+
/**
* The signature using the merge key is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1387,6 +1831,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_MERGE_INVALID_MERGE_SIGNATURE = 1876,
+
/**
* The signature using the reserve key is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1394,6 +1839,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_MERGE_INVALID_RESERVE_SIGNATURE = 1877,
+
/**
* The targeted purse is not yet full and thus cannot be merged. Retrying the request later may succeed.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1401,6 +1847,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_NOT_FULL = 1878,
+
/**
* The signature from the exchange over the confirmation is invalid.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -1408,6 +1855,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_MERGE_EXCHANGE_SIGNATURE_INVALID = 1879,
+
/**
* The exchange of the target account is not a partner of this exchange.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1415,6 +1863,23 @@ export enum TalerErrorCode {
*/
EXCHANGE_MERGE_PURSE_PARTNER_UNKNOWN = 1880,
+
+ /**
+ * The signature affirming the new partner is invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_MANAGEMENT_ADD_PARTNER_SIGNATURE_INVALID = 1890,
+
+
+ /**
+ * Conflicting data for the partner already exists with the exchange.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_MANAGEMENT_ADD_PARTNER_DATA_CONFLICT = 1891,
+
+
/**
* The auditor signature over the denomination meta data is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1422,6 +1887,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_AUDITORS_AUDITOR_SIGNATURE_INVALID = 1900,
+
/**
* The auditor that was specified is unknown to this exchange.
* Returned with an HTTP status code of #MHD_HTTP_PRECONDITION_FAILED (412).
@@ -1429,6 +1895,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_AUDITORS_AUDITOR_UNKNOWN = 1901,
+
/**
* The auditor that was specified is no longer used by this exchange.
* Returned with an HTTP status code of #MHD_HTTP_GONE (410).
@@ -1436,6 +1903,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_AUDITORS_AUDITOR_INACTIVE = 1902,
+
/**
* The signature affirming the wallet's KYC request was invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1443,6 +1911,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_KYC_WALLET_SIGNATURE_INVALID = 1925,
+
/**
* The exchange received an unexpected malformed response from its KYC backend.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
@@ -1450,6 +1919,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE = 1926,
+
/**
* The backend signaled an unexpected failure.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
@@ -1457,6 +1927,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_KYC_PROOF_BACKEND_ERROR = 1927,
+
/**
* The backend signaled an authorization failure.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1464,6 +1935,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_KYC_PROOF_BACKEND_AUTHORIZATION_FAILED = 1928,
+
/**
* The exchange is unaware of having made an the authorization request.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1471,6 +1943,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_KYC_PROOF_REQUEST_UNKNOWN = 1929,
+
/**
* The payto-URI hash did not match. Hence the request was denied.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1478,6 +1951,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_KYC_CHECK_AUTHORIZATION_FAILED = 1930,
+
/**
* The request used a logic specifier that is not known to the exchange.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1485,6 +1959,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_KYC_GENERIC_LOGIC_UNKNOWN = 1931,
+
/**
* The request requires a logic which is no longer configured at the exchange.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -1492,6 +1967,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_KYC_GENERIC_LOGIC_GONE = 1932,
+
/**
* The logic plugin had a bug in its interaction with the KYC provider.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -1499,6 +1975,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_KYC_GENERIC_LOGIC_BUG = 1933,
+
/**
* The exchange could not process the request with its KYC provider because the provider refused access to the service. This indicates some configuration issue at the Taler exchange operator.
* Returned with an HTTP status code of #MHD_HTTP_NETWORK_AUTHENTICATION_REQUIRED (511).
@@ -1506,6 +1983,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_KYC_GENERIC_PROVIDER_ACCESS_REFUSED = 1934,
+
/**
* There was a timeout in the interaction between the exchange and the KYC provider. The most likely cause is some networking problem. Trying again later might succeed.
* Returned with an HTTP status code of #MHD_HTTP_GATEWAY_TIMEOUT (504).
@@ -1513,6 +1991,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_KYC_GENERIC_PROVIDER_TIMEOUT = 1935,
+
/**
* The KYC provider responded with a status that was completely unexpected by the KYC logic of the exchange.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
@@ -1520,6 +1999,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_KYC_GENERIC_PROVIDER_UNEXPECTED_REPLY = 1936,
+
/**
* The rate limit of the exchange at the KYC provider has been exceeded. Trying much later might work.
* Returned with an HTTP status code of #MHD_HTTP_SERVICE_UNAVAILABLE (503).
@@ -1527,6 +2007,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_KYC_GENERIC_PROVIDER_RATE_LIMIT_EXCEEDED = 1937,
+
/**
* The request to the webhook lacked proper authorization or authentication data.
* Returned with an HTTP status code of #MHD_HTTP_UNAUTHORIZED (401).
@@ -1534,6 +2015,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_KYC_WEBHOOK_UNAUTHORIZED = 1938,
+
/**
* The exchange does not know a contract under the given contract public key.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1541,6 +2023,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_CONTRACTS_UNKNOWN = 1950,
+
/**
* The URL does not encode a valid exchange public key in its path.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -1548,6 +2031,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_CONTRACTS_INVALID_CONTRACT_PUB = 1951,
+
/**
* The returned encrypted contract did not decrypt.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -1555,6 +2039,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_CONTRACTS_DECRYPTION_FAILED = 1952,
+
/**
* The signature on the encrypted contract did not validate.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -1562,6 +2047,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_CONTRACTS_SIGNATURE_INVALID = 1953,
+
/**
* The decrypted contract was malformed.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -1569,6 +2055,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_CONTRACTS_DECODING_FAILED = 1954,
+
/**
* A coin signature for a deposit into the purse is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1576,6 +2063,23 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_DEPOSIT_COIN_SIGNATURE_INVALID = 1975,
+
+ /**
+ * It is too late to deposit coins into the purse.
+ * Returned with an HTTP status code of #MHD_HTTP_GONE (410).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_PURSE_DEPOSIT_DECIDED_ALREADY = 1976,
+
+
+ /**
+ * TOTP key is not valid.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_TOTP_KEY_INVALID = 1980,
+
+
/**
* The backend could not find the merchant instance specified in the request.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1583,6 +2087,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GENERIC_INSTANCE_UNKNOWN = 2000,
+
/**
* The start and end-times in the wire fee structure leave a hole. This is not allowed.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -1590,6 +2095,15 @@ export enum TalerErrorCode {
*/
MERCHANT_GENERIC_HOLE_IN_WIRE_FEE_STRUCTURE = 2001,
+
+ /**
+ * The merchant was unable to obtain a valid answer to /wire from the exchange.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GENERIC_EXCHANGE_WIRE_REQUEST_FAILED = 2002,
+
+
/**
* The proposal is not known to the backend.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1597,6 +2111,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GENERIC_ORDER_UNKNOWN = 2005,
+
/**
* The order provided to the backend could not be completed, because a product to be completed via inventory data is not actually in our inventory.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1604,12 +2119,14 @@ export enum TalerErrorCode {
*/
MERCHANT_GENERIC_PRODUCT_UNKNOWN = 2006,
+
/**
- * The tip ID is unknown. This could happen if the tip has expired.
+ * The reward ID is unknown. This could happen if the reward has expired.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
* (A value of 0 indicates that the error is generated client-side).
*/
- MERCHANT_GENERIC_TIP_ID_UNKNOWN = 2007,
+ MERCHANT_GENERIC_REWARD_ID_UNKNOWN = 2007,
+
/**
* The contract obtained from the merchant backend was malformed.
@@ -1618,6 +2135,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID = 2008,
+
/**
* The order we found does not match the provided contract hash.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1625,6 +2143,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GENERIC_CONTRACT_HASH_DOES_NOT_MATCH_ORDER = 2009,
+
/**
* The exchange failed to provide a valid response to the merchant's /keys request.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
@@ -1632,6 +2151,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GENERIC_EXCHANGE_KEYS_FAILURE = 2010,
+
/**
* The exchange failed to respond to the merchant on time.
* Returned with an HTTP status code of #MHD_HTTP_GATEWAY_TIMEOUT (504).
@@ -1639,6 +2159,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GENERIC_EXCHANGE_TIMEOUT = 2011,
+
/**
* The merchant failed to talk to the exchange.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -1646,6 +2167,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GENERIC_EXCHANGE_CONNECT_FAILURE = 2012,
+
/**
* The exchange returned a maformed response.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
@@ -1653,6 +2175,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GENERIC_EXCHANGE_REPLY_MALFORMED = 2013,
+
/**
* The exchange returned an unexpected response status.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
@@ -1660,6 +2183,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GENERIC_EXCHANGE_UNEXPECTED_STATUS = 2014,
+
/**
* The merchant refused the request due to lack of authorization.
* Returned with an HTTP status code of #MHD_HTTP_UNAUTHORIZED (401).
@@ -1667,6 +2191,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GENERIC_UNAUTHORIZED = 2015,
+
/**
* The merchant instance specified in the request was deleted.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1674,6 +2199,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GENERIC_INSTANCE_DELETED = 2016,
+
/**
* The backend could not find the inbound wire transfer specified in the request.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1681,6 +2207,63 @@ export enum TalerErrorCode {
*/
MERCHANT_GENERIC_TRANSFER_UNKNOWN = 2017,
+
+ /**
+ * The backend could not find the template(id) because it is not exist.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GENERIC_TEMPLATE_UNKNOWN = 2018,
+
+
+ /**
+ * The backend could not find the webhook(id) because it is not exist.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GENERIC_WEBHOOK_UNKNOWN = 2019,
+
+
+ /**
+ * The backend could not find the webhook(serial) because it is not exist.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GENERIC_PENDING_WEBHOOK_UNKNOWN = 2020,
+
+
+ /**
+ * The backend could not find the OTP device(id) because it is not exist.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GENERIC_OTP_DEVICE_UNKNOWN = 2021,
+
+
+ /**
+ * The account is not known to the backend.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GENERIC_ACCOUNT_UNKNOWN = 2022,
+
+
+ /**
+ * The wire hash was malformed.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GENERIC_H_WIRE_MALFORMED = 2023,
+
+
+ /**
+ * The currency specified in the operation does not work with the current state of the given resource.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GENERIC_CURRENCY_MISMATCH = 2024,
+
+
/**
* The exchange failed to provide a valid answer to the tracking request, thus those details are not in the response.
* Returned with an HTTP status code of #MHD_HTTP_OK (200).
@@ -1688,6 +2271,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GET_ORDERS_EXCHANGE_TRACKING_FAILURE = 2100,
+
/**
* The merchant backend failed to construct the request for tracking to the exchange, thus tracking details are not in the response.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -1695,6 +2279,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GET_ORDERS_ID_EXCHANGE_REQUEST_FAILURE = 2103,
+
/**
* The merchant backend failed trying to contact the exchange for tracking details, thus those details are not in the response.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -1702,6 +2287,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GET_ORDERS_ID_EXCHANGE_LOOKUP_START_FAILURE = 2104,
+
/**
* The claim token used to authenticate the client is invalid for this order.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1709,6 +2295,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GET_ORDERS_ID_INVALID_TOKEN = 2105,
+
/**
* The contract terms hash used to authenticate the client is invalid for this order.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1716,6 +2303,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GET_ORDERS_ID_INVALID_CONTRACT_HASH = 2106,
+
/**
* The exchange responded saying that funds were insufficient (for example, due to double-spending).
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1723,6 +2311,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_INSUFFICIENT_FUNDS = 2150,
+
/**
* The denomination key used for payment is not listed among the denomination keys of the exchange.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -1730,6 +2319,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_DENOMINATION_KEY_NOT_FOUND = 2151,
+
/**
* The denomination key used for payment is not audited by an auditor approved by the merchant.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -1737,6 +2327,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_DENOMINATION_KEY_AUDITOR_FAILURE = 2152,
+
/**
* There was an integer overflow totaling up the amounts or deposit fees in the payment.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -1744,6 +2335,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_AMOUNT_OVERFLOW = 2153,
+
/**
* The deposit fees exceed the total value of the payment.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -1751,20 +2343,23 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_FEES_EXCEED_PAYMENT = 2154,
+
/**
* After considering deposit and wire fees, the payment is insufficient to satisfy the required amount for the contract. The client should revisit the logic used to calculate fees it must cover.
- * Returned with an HTTP status code of #MHD_HTTP_NOT_ACCEPTABLE (406).
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
* (A value of 0 indicates that the error is generated client-side).
*/
MERCHANT_POST_ORDERS_ID_PAY_INSUFFICIENT_DUE_TO_FEES = 2155,
+
/**
* Even if we do not consider deposit and wire fees, the payment is insufficient to satisfy the required amount for the contract.
- * Returned with an HTTP status code of #MHD_HTTP_NOT_ACCEPTABLE (406).
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
* (A value of 0 indicates that the error is generated client-side).
*/
MERCHANT_POST_ORDERS_ID_PAY_PAYMENT_INSUFFICIENT = 2156,
+
/**
* The signature over the contract of one of the coins was invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1772,6 +2367,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_COIN_SIGNATURE_INVALID = 2157,
+
/**
* When we tried to find information about the exchange to issue the deposit, we failed. This usually only happens if the merchant backend is somehow unable to get its own HTTP client logic to work.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -1779,6 +2375,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_LOOKUP_FAILED = 2158,
+
/**
* The refund deadline in the contract is after the transfer deadline.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -1786,6 +2383,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_REFUND_DEADLINE_PAST_WIRE_TRANSFER_DEADLINE = 2159,
+
/**
* The order was already paid (maybe by another wallet).
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1793,6 +2391,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_ALREADY_PAID = 2160,
+
/**
* The payment is too late, the offer has expired.
* Returned with an HTTP status code of #MHD_HTTP_GONE (410).
@@ -1800,6 +2399,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_OFFER_EXPIRED = 2161,
+
/**
* The "merchant" field is missing in the proposal data. This is an internal error as the proposal is from the merchant's own database at this point.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -1807,6 +2407,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_MERCHANT_FIELD_MISSING = 2162,
+
/**
* Failed to locate merchant's account information matching the wire hash given in the proposal.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -1814,6 +2415,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_WIRE_HASH_UNKNOWN = 2163,
+
/**
* The deposit time for the denomination has expired.
* Returned with an HTTP status code of #MHD_HTTP_GONE (410).
@@ -1821,6 +2423,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_DENOMINATION_DEPOSIT_EXPIRED = 2165,
+
/**
* The exchange of the deposited coin charges a wire fee that could not be added to the total (total amount too high).
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -1828,6 +2431,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_WIRE_FEE_ADDITION_FAILED = 2166,
+
/**
* The contract was not fully paid because of refunds. Note that clients MAY treat this as paid if, for example, contracts must be executed despite of refunds.
* Returned with an HTTP status code of #MHD_HTTP_PAYMENT_REQUIRED (402).
@@ -1835,6 +2439,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_REFUNDED = 2167,
+
/**
* According to our database, we have refunded more than we were paid (which should not be possible).
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -1842,6 +2447,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_REFUNDS_EXCEED_PAYMENTS = 2168,
+
/**
* Legacy stuff. Remove me with protocol v1.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -1849,6 +2455,7 @@ export enum TalerErrorCode {
*/
DEAD_QQQ_PAY_MERCHANT_POST_ORDERS_ID_ABORT_REFUND_REFUSED_PAYMENT_COMPLETE = 2169,
+
/**
* The payment failed at the exchange.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
@@ -1856,6 +2463,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_FAILED = 2170,
+
/**
* The payment required a minimum age but one of the coins (of a denomination with support for age restriction) did not provide any age_commitment.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -1863,6 +2471,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_AGE_COMMITMENT_MISSING = 2171,
+
/**
* The payment required a minimum age but one of the coins provided an age_commitment that contained a wrong number of public keys compared to the number of age groups defined in the denomination of the coin.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -1870,6 +2479,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_AGE_COMMITMENT_SIZE_MISMATCH = 2172,
+
/**
* The payment required a minimum age but one of the coins provided a minimum_age_sig that couldn't be verified with the given age_commitment for that particular minimum age.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -1877,6 +2487,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_AGE_VERIFICATION_FAILED = 2173,
+
/**
* The payment required no minimum age but one of the coins (of a denomination with support for age restriction) did not provide the required h_age_commitment.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -1884,6 +2495,15 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_AGE_COMMITMENT_HASH_MISSING = 2174,
+
+ /**
+ * The exchange does not support the selected bank account of the merchant. Likely the merchant had stale data on the bank accounts of the exchange and thus selected an inappropriate exchange when making the offer.
+ * 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_WIRE_METHOD_UNSUPPORTED = 2175,
+
+
/**
* The contract hash does not match the given order ID.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -1891,6 +2511,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAID_CONTRACT_HASH_MISMATCH = 2200,
+
/**
* The signature of the merchant is not valid for the given contract hash.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1898,6 +2519,23 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAID_COIN_SIGNATURE_INVALID = 2201,
+
+ /**
+ * A token family with this ID but conflicting data exists.
+ * 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_TOKEN_FAMILY_CONFLICT = 2225,
+
+
+ /**
+ * The backend is unaware of a token family with the given ID.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PATCH_TOKEN_FAMILY_NOT_FOUND = 2226,
+
+
/**
* The merchant failed to send the exchange the refund request.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -1905,6 +2543,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_ABORT_EXCHANGE_REFUND_FAILED = 2251,
+
/**
* The merchant failed to find the exchange to process the lookup.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -1912,6 +2551,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_ABORT_EXCHANGE_LOOKUP_FAILED = 2252,
+
/**
* The merchant could not find the contract.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1919,6 +2559,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_ABORT_CONTRACT_NOT_FOUND = 2253,
+
/**
* The payment was already completed and thus cannot be aborted anymore.
* Returned with an HTTP status code of #MHD_HTTP_PRECONDITION_FAILED (412).
@@ -1926,6 +2567,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_ABORT_REFUND_REFUSED_PAYMENT_COMPLETE = 2254,
+
/**
* The hash provided by the wallet does not match the order.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1933,6 +2575,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_ABORT_CONTRACT_HASH_MISSMATCH = 2255,
+
/**
* The array of coins cannot be empty.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -1940,6 +2583,63 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_ABORT_COINS_ARRAY_EMPTY = 2256,
+
+ /**
+ * We are waiting for the exchange to provide us with key material before checking the wire transfer.
+ * Returned with an HTTP status code of #MHD_HTTP_ACCEPTED (202).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_EXCHANGE_TRANSFERS_AWAITING_KEYS = 2258,
+
+
+ /**
+ * We are waiting for the exchange to provide us with the list of aggregated transactions.
+ * Returned with an HTTP status code of #MHD_HTTP_ACCEPTED (202).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_EXCHANGE_TRANSFERS_AWAITING_LIST = 2259,
+
+
+ /**
+ * The endpoint indicated in the wire transfer does not belong to a GNU Taler exchange.
+ * Returned with an HTTP status code of #MHD_HTTP_OK (200).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_EXCHANGE_TRANSFERS_FATAL_NO_EXCHANGE = 2260,
+
+
+ /**
+ * The exchange indicated in the wire transfer claims to know nothing about the wire transfer.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_EXCHANGE_TRANSFERS_FATAL_NOT_FOUND = 2261,
+
+
+ /**
+ * The interaction with the exchange is delayed due to rate limiting.
+ * Returned with an HTTP status code of #MHD_HTTP_ACCEPTED (202).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_EXCHANGE_TRANSFERS_RATE_LIMITED = 2262,
+
+
+ /**
+ * We experienced a transient failure in our interaction with the exchange.
+ * Returned with an HTTP status code of #MHD_HTTP_ACCEPTED (202).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_EXCHANGE_TRANSFERS_TRANSIENT_FAILURE = 2263,
+
+
+ /**
+ * The response from the exchange was unacceptable and should be reviewed with an auditor.
+ * Returned with an HTTP status code of #MHD_HTTP_OK (200).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_EXCHANGE_TRANSFERS_HARD_FAILURE = 2264,
+
+
/**
* We could not claim the order because the backend is unaware of it.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1947,6 +2647,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_CLAIM_NOT_FOUND = 2300,
+
/**
* We could not claim the order because someone else claimed it first.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1954,6 +2655,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_CLAIM_ALREADY_CLAIMED = 2301,
+
/**
* The client-side experienced an internal failure.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -1961,6 +2663,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_CLAIM_CLIENT_INTERNAL_FAILURE = 2302,
+
/**
* The backend failed to sign the refund request.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -1968,97 +2671,135 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_REFUND_SIGNATURE_FAILED = 2350,
+
/**
* The client failed to unblind the signature returned by the merchant.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
* (A value of 0 indicates that the error is generated client-side).
*/
- MERCHANT_TIP_PICKUP_UNBLIND_FAILURE = 2400,
+ MERCHANT_REWARD_PICKUP_UNBLIND_FAILURE = 2400,
+
/**
* The exchange returned a failure code for the withdraw operation.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
* (A value of 0 indicates that the error is generated client-side).
*/
- MERCHANT_TIP_PICKUP_EXCHANGE_ERROR = 2403,
+ MERCHANT_REWARD_PICKUP_EXCHANGE_ERROR = 2403,
+
/**
* The merchant failed to add up the amounts to compute the pick up value.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
* (A value of 0 indicates that the error is generated client-side).
*/
- MERCHANT_TIP_PICKUP_SUMMATION_FAILED = 2404,
+ MERCHANT_REWARD_PICKUP_SUMMATION_FAILED = 2404,
+
/**
- * The tip expired.
+ * The reward expired.
* Returned with an HTTP status code of #MHD_HTTP_GONE (410).
* (A value of 0 indicates that the error is generated client-side).
*/
- MERCHANT_TIP_PICKUP_HAS_EXPIRED = 2405,
+ MERCHANT_REWARD_PICKUP_HAS_EXPIRED = 2405,
+
/**
* The requested withdraw amount exceeds the amount remaining to be picked up.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
* (A value of 0 indicates that the error is generated client-side).
*/
- MERCHANT_TIP_PICKUP_AMOUNT_EXCEEDS_TIP_REMAINING = 2406,
+ MERCHANT_REWARD_PICKUP_AMOUNT_EXCEEDS_REWARD_REMAINING = 2406,
+
/**
* The merchant did not find the specified denomination key in the exchange's key set.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
* (A value of 0 indicates that the error is generated client-side).
*/
- MERCHANT_TIP_PICKUP_DENOMINATION_UNKNOWN = 2407,
+ MERCHANT_REWARD_PICKUP_DENOMINATION_UNKNOWN = 2407,
+
/**
- * The backend lacks a wire transfer method configuration option for the given instance. Thus, this instance is unavailable (not findable for creating new orders).
+ * The merchant instance has no active bank accounts configured. However, at least one bank account must be available to create new orders.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
* (A value of 0 indicates that the error is generated client-side).
*/
MERCHANT_PRIVATE_POST_ORDERS_INSTANCE_CONFIGURATION_LACKS_WIRE = 2500,
+
/**
- * The proposal had no timestamp and the backend failed to obtain the local time. Likely to be an internal error.
+ * The proposal had no timestamp and the merchant backend failed to obtain the current local time.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
* (A value of 0 indicates that the error is generated client-side).
*/
MERCHANT_PRIVATE_POST_ORDERS_NO_LOCALTIME = 2501,
+
/**
- * The order provided to the backend could not be parsed, some required fields were missing or ill-formed.
+ * The order provided to the backend could not be parsed; likely some required fields were missing or ill-formed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
* (A value of 0 indicates that the error is generated client-side).
*/
MERCHANT_PRIVATE_POST_ORDERS_PROPOSAL_PARSE_ERROR = 2502,
+
/**
- * The backend encountered an error: the proposal already exists.
+ * A conflicting order (sharing the same order identifier) already exists at this merchant backend instance.
* 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_ALREADY_EXISTS = 2503,
+
/**
- * The request is invalid: the wire deadline is before the refund deadline.
+ * The order creation request is invalid because the given wire deadline is before the refund deadline.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
* (A value of 0 indicates that the error is generated client-side).
*/
MERCHANT_PRIVATE_POST_ORDERS_REFUND_AFTER_WIRE_DEADLINE = 2504,
+
/**
- * The request is invalid: a delivery date was given, but it is in the past.
+ * The order creation request is invalid because the delivery date given is in the past.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
* (A value of 0 indicates that the error is generated client-side).
*/
MERCHANT_PRIVATE_POST_ORDERS_DELIVERY_DATE_IN_PAST = 2505,
+
/**
- * The request is invalid: the wire deadline for the order would be "never".
+ * The order creation request is invalid because a wire deadline of "never" is not allowed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
* (A value of 0 indicates that the error is generated client-side).
*/
MERCHANT_PRIVATE_POST_ORDERS_WIRE_DEADLINE_IS_NEVER = 2506,
+
+ /**
+ * The order creation request is invalid because the given payment deadline is in the past.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_ORDERS_PAY_DEADLINE_IN_PAST = 2507,
+
+
+ /**
+ * The order creation request is invalid because the given refund deadline is in the past.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_ORDERS_REFUND_DEADLINE_IN_PAST = 2508,
+
+
+ /**
+ * The backend does not trust any exchange that would allow funds to be wired to any bank account of this instance using the wire method specified with the order. Note that right now, we do not support the use of exchange bank accounts with mandatory currency conversion.
+ * 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_NO_EXCHANGES_FOR_WIRE_METHOD = 2509,
+
+
/**
* One of the paths to forget is malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2066,6 +2807,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_PATCH_ORDERS_ID_FORGET_PATH_SYNTAX_INCORRECT = 2510,
+
/**
* One of the paths to forget was not marked as forgettable.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2073,13 +2815,15 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_PATCH_ORDERS_ID_FORGET_PATH_NOT_FORGETTABLE = 2511,
+
/**
- * The order provided to the backend could not be deleted, our offer is still valid and awaiting payment.
+ * The order provided to the backend could not be deleted, our offer is still valid and awaiting payment. Deletion may work later after the offer has expired if it remains unpaid.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
* (A value of 0 indicates that the error is generated client-side).
*/
MERCHANT_PRIVATE_DELETE_ORDERS_AWAITING_PAYMENT = 2520,
+
/**
* The order provided to the backend could not be deleted as the order was already paid.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2087,27 +2831,31 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_DELETE_ORDERS_ALREADY_PAID = 2521,
+
/**
- * The amount to be refunded is inconsistent: either is lower than the previous amount being awarded, or it is too big to be paid back. In this second case, the fault stays on the business dept. side.
+ * The amount to be refunded is inconsistent: either is lower than the previous amount being awarded, or it exceeds the original price paid by the customer.
* 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_ID_REFUND_INCONSISTENT_AMOUNT = 2530,
+
/**
- * The frontend gave an unpaid order id to issue the refund to.
+ * Only paid orders can be refunded, and the frontend specified an unpaid order to issue a refund for.
* 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_ID_REFUND_ORDER_UNPAID = 2531,
+
/**
- * The refund delay was set to 0 and thus no refunds are allowed for this order.
+ * The refund delay was set to 0 and thus no refunds are ever allowed for this order.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
* (A value of 0 indicates that the error is generated client-side).
*/
MERCHANT_PRIVATE_POST_ORDERS_ID_REFUND_NOT_ALLOWED_BY_CONTRACT = 2532,
+
/**
* The exchange says it does not know this transfer.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
@@ -2115,6 +2863,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_TRANSFERS_EXCHANGE_UNKNOWN = 2550,
+
/**
* We internally failed to execute the /track/transfer request.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
@@ -2122,6 +2871,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_TRANSFERS_REQUEST_ERROR = 2551,
+
/**
* The amount transferred differs between what was submitted and what the exchange claimed.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2129,6 +2879,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_TRANSFERS_CONFLICTING_TRANSFERS = 2552,
+
/**
* The exchange gave conflicting information about a coin which has been wire transferred.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2136,6 +2887,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_TRANSFERS_CONFLICTING_REPORTS = 2553,
+
/**
* The exchange charged a different wire fee than what it originally advertised, and it is higher.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
@@ -2143,6 +2895,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_TRANSFERS_BAD_WIRE_FEE = 2554,
+
/**
* We did not find the account that the transfer was made to.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -2150,6 +2903,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_TRANSFERS_ACCOUNT_NOT_FOUND = 2555,
+
/**
* The backend could not delete the transfer as the echange already replied to our inquiry about it and we have integrated the result.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2157,6 +2911,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_DELETE_TRANSFERS_ALREADY_CONFIRMED = 2556,
+
/**
* The backend was previously informed about a wire transfer with the same ID but a different amount. Multiple wire transfers with the same ID are not allowed. If the new amount is correct, the old transfer should first be deleted.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2164,6 +2919,15 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_TRANSFERS_CONFLICTING_SUBMISSION = 2557,
+
+ /**
+ * The amount transferred differs between what was submitted and what the exchange claimed.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_EXCHANGE_TRANSFERS_CONFLICTING_TRANSFERS = 2563,
+
+
/**
* The merchant backend cannot create an instance under the given identifier as one already exists. Use PATCH to modify the existing entry.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2171,6 +2935,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_INSTANCES_ALREADY_EXISTS = 2600,
+
/**
* The merchant backend cannot create an instance because the authentication configuration field is malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2178,6 +2943,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_INSTANCES_BAD_AUTH = 2601,
+
/**
* The merchant backend cannot update an instance's authentication settings because the provided authentication settings are malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2185,6 +2951,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_INSTANCE_AUTH_BAD_AUTH = 2602,
+
/**
* The merchant backend cannot create an instance under the given identifier, the previous one was deleted but must be purged first.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2192,6 +2959,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_INSTANCES_PURGE_REQUIRED = 2603,
+
/**
* The merchant backend cannot update an instance under the given identifier, the previous one was deleted but must be purged first.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2199,6 +2967,23 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_PATCH_INSTANCES_PURGE_REQUIRED = 2625,
+
+ /**
+ * The bank account referenced in the requested operation was not found.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_ACCOUNT_DELETE_UNKNOWN_ACCOUNT = 2626,
+
+
+ /**
+ * The bank account specified in the request already exists at the merchant.
+ * 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_ACCOUNT_EXISTS = 2627,
+
+
/**
* The product ID exists.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2206,6 +2991,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_PRODUCTS_CONFLICT_PRODUCT_EXISTS = 2650,
+
/**
* The update would have reduced the total amount of product lost, which is not allowed.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2213,6 +2999,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_LOST_REDUCED = 2660,
+
/**
* The update would have mean that more stocks were lost than what remains from total inventory after sales, which is not allowed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2220,6 +3007,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_LOST_EXCEEDS_STOCKS = 2661,
+
/**
* The update would have reduced the total amount of product in stock, which is not allowed.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2227,6 +3015,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_STOCKED_REDUCED = 2662,
+
/**
* The update would have reduced the total amount of product sold, which is not allowed.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2234,6 +3023,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_SOLD_REDUCED = 2663,
+
/**
* The lock request is for more products than we have left (unlocked) in stock.
* Returned with an HTTP status code of #MHD_HTTP_GONE (410).
@@ -2241,6 +3031,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_PRODUCTS_LOCK_INSUFFICIENT_STOCKS = 2670,
+
/**
* The deletion request is for a product that is locked.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2248,6 +3039,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_DELETE_PRODUCTS_CONFLICTING_LOCK = 2680,
+
/**
* The requested wire method is not supported by the exchange.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2255,6 +3047,15 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_RESERVES_UNSUPPORTED_WIRE_METHOD = 2700,
+
+ /**
+ * The requested exchange does not allow rewards.
+ * 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_RESERVES_REWARDS_NOT_ALLOWED = 2701,
+
+
/**
* The reserve could not be deleted because it is unknown.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -2262,33 +3063,38 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_DELETE_RESERVES_NO_SUCH_RESERVE = 2710,
+
/**
- * The reserve that was used to fund the tips has expired.
+ * The reserve that was used to fund the rewards has expired.
* Returned with an HTTP status code of #MHD_HTTP_GONE (410).
* (A value of 0 indicates that the error is generated client-side).
*/
- MERCHANT_PRIVATE_POST_TIP_AUTHORIZE_RESERVE_EXPIRED = 2750,
+ MERCHANT_PRIVATE_POST_REWARD_AUTHORIZE_RESERVE_EXPIRED = 2750,
+
/**
- * The reserve that was used to fund the tips was not found in the DB.
+ * The reserve that was used to fund the rewards was not found in the DB.
* Returned with an HTTP status code of #MHD_HTTP_SERVICE_UNAVAILABLE (503).
* (A value of 0 indicates that the error is generated client-side).
*/
- MERCHANT_PRIVATE_POST_TIP_AUTHORIZE_RESERVE_UNKNOWN = 2751,
+ MERCHANT_PRIVATE_POST_REWARD_AUTHORIZE_RESERVE_UNKNOWN = 2751,
+
/**
- * The backend knows the instance that was supposed to support the tip, and it was configured for tipping. However, the funds remaining are insufficient to cover the tip, and the merchant should top up the reserve.
+ * The backend knows the instance that was supposed to support the reward, and it was configured for rewardping. However, the funds remaining are insufficient to cover the reward, and the merchant should top up the reserve.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
* (A value of 0 indicates that the error is generated client-side).
*/
- MERCHANT_PRIVATE_POST_TIP_AUTHORIZE_INSUFFICIENT_FUNDS = 2752,
+ MERCHANT_PRIVATE_POST_REWARD_AUTHORIZE_INSUFFICIENT_FUNDS = 2752,
+
/**
- * The backend failed to find a reserve needed to authorize the tip.
+ * The backend failed to find a reserve needed to authorize the reward.
* Returned with an HTTP status code of #MHD_HTTP_SERVICE_UNAVAILABLE (503).
* (A value of 0 indicates that the error is generated client-side).
*/
- MERCHANT_PRIVATE_POST_TIP_AUTHORIZE_RESERVE_NOT_FOUND = 2753,
+ MERCHANT_PRIVATE_POST_REWARD_AUTHORIZE_RESERVE_NOT_FOUND = 2753,
+
/**
* The merchant backend encountered a failure in computing the deposit total.
@@ -2297,6 +3103,71 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_GET_ORDERS_ID_AMOUNT_ARITHMETIC_FAILURE = 2800,
+
+ /**
+ * The template ID already exists.
+ * 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_TEMPLATES_CONFLICT_TEMPLATE_EXISTS = 2850,
+
+
+ /**
+ * The OTP device ID already exists.
+ * 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_OTP_DEVICES_CONFLICT_OTP_DEVICE_EXISTS = 2851,
+
+
+ /**
+ * Amount given in the using template and in the template contract. There is a conflict.
+ * 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_USING_TEMPLATES_AMOUNT_CONFLICT_TEMPLATES_CONTRACT_AMOUNT = 2860,
+
+
+ /**
+ * Subject given in the using template and in the template contract. There is a conflict.
+ * 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_USING_TEMPLATES_SUMMARY_CONFLICT_TEMPLATES_CONTRACT_SUBJECT = 2861,
+
+
+ /**
+ * Amount not given in the using template and in the template contract. There is a conflict.
+ * 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_USING_TEMPLATES_NO_AMOUNT = 2862,
+
+
+ /**
+ * Subject not given in the using template and in the template contract. There is a conflict.
+ * 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_USING_TEMPLATES_NO_SUMMARY = 2863,
+
+
+ /**
+ * The webhook ID elready exists.
+ * 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_WEBHOOKS_CONFLICT_WEBHOOK_EXISTS = 2900,
+
+
+ /**
+ * The webhook serial elready exists.
+ * 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_PENDING_WEBHOOKS_CONFLICT_PENDING_WEBHOOK_EXISTS = 2910,
+
+
/**
* The signature from the exchange on the deposit confirmation is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -2304,6 +3175,7 @@ export enum TalerErrorCode {
*/
AUDITOR_DEPOSIT_CONFIRMATION_SIGNATURE_INVALID = 3100,
+
/**
* The exchange key used for the signature on the deposit confirmation was revoked.
* Returned with an HTTP status code of #MHD_HTTP_GONE (410).
@@ -2311,6 +3183,7 @@ export enum TalerErrorCode {
*/
AUDITOR_EXCHANGE_SIGNING_KEY_REVOKED = 3101,
+
/**
* Wire transfer attempted with credit and debit party being the same bank account.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2318,6 +3191,7 @@ export enum TalerErrorCode {
*/
BANK_SAME_ACCOUNT = 5101,
+
/**
* Wire transfer impossible, due to financial limitation of the party that attempted the payment.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2325,6 +3199,7 @@ export enum TalerErrorCode {
*/
BANK_UNALLOWED_DEBIT = 5102,
+
/**
* Negative numbers are not allowed (as value and/or fraction) to instantiate an amount object.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2332,6 +3207,7 @@ export enum TalerErrorCode {
*/
BANK_NEGATIVE_NUMBER_AMOUNT = 5103,
+
/**
* A too big number was used (as value and/or fraction) to instantiate an amount object.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2339,12 +3215,6 @@ export enum TalerErrorCode {
*/
BANK_NUMBER_TOO_BIG = 5104,
- /**
- * Could not login for the requested operation.
- * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
- * (A value of 0 indicates that the error is generated client-side).
- */
- BANK_LOGIN_FAILED = 5105,
/**
* The bank account referenced in the requested operation was not found.
@@ -2353,6 +3223,7 @@ export enum TalerErrorCode {
*/
BANK_UNKNOWN_ACCOUNT = 5106,
+
/**
* The transaction referenced in the requested operation (typically a reject operation), was not found.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -2360,6 +3231,7 @@ export enum TalerErrorCode {
*/
BANK_TRANSACTION_NOT_FOUND = 5107,
+
/**
* Bank received a malformed amount string.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2367,6 +3239,7 @@ export enum TalerErrorCode {
*/
BANK_BAD_FORMAT_AMOUNT = 5108,
+
/**
* The client does not own the account credited by the transaction which is to be rejected, so it has no rights do reject it.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -2374,6 +3247,7 @@ export enum TalerErrorCode {
*/
BANK_REJECT_NO_RIGHTS = 5109,
+
/**
* This error code is returned when no known exception types captured the exception.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -2381,6 +3255,7 @@ export enum TalerErrorCode {
*/
BANK_UNMANAGED_EXCEPTION = 5110,
+
/**
* This error code is used for all those exceptions that do not really need a specific error code to return to the client. Used for example when a client is trying to register with a unavailable username.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -2388,6 +3263,7 @@ export enum TalerErrorCode {
*/
BANK_SOFT_EXCEPTION = 5111,
+
/**
* The request UID for a request to transfer funds has already been used, but with different details for the transfer.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2395,6 +3271,7 @@ export enum TalerErrorCode {
*/
BANK_TRANSFER_REQUEST_UID_REUSED = 5112,
+
/**
* The withdrawal operation already has a reserve selected. The current request conflicts with the existing selection.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2402,6 +3279,7 @@ export enum TalerErrorCode {
*/
BANK_WITHDRAWAL_OPERATION_RESERVE_SELECTION_CONFLICT = 5113,
+
/**
* The wire transfer subject duplicates an existing reserve public key. But wire transfer subjects must be unique.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2409,6 +3287,7 @@ export enum TalerErrorCode {
*/
BANK_DUPLICATE_RESERVE_PUB_SUBJECT = 5114,
+
/**
* The client requested a transaction that is so far in the past, that it has been forgotten by the bank.
* Returned with an HTTP status code of #MHD_HTTP_GONE (410).
@@ -2416,6 +3295,7 @@ export enum TalerErrorCode {
*/
BANK_ANCIENT_TRANSACTION_GONE = 5115,
+
/**
* The client attempted to abort a transaction that was already confirmed.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2423,6 +3303,7 @@ export enum TalerErrorCode {
*/
BANK_ABORT_CONFIRM_CONFLICT = 5116,
+
/**
* The client attempted to confirm a transaction that was already aborted.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2430,6 +3311,7 @@ export enum TalerErrorCode {
*/
BANK_CONFIRM_ABORT_CONFLICT = 5117,
+
/**
* The client attempted to register an account with the same name.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2437,6 +3319,7 @@ export enum TalerErrorCode {
*/
BANK_REGISTER_CONFLICT = 5118,
+
/**
* The client attempted to confirm a withdrawal operation before the wallet posted the required details.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2444,6 +3327,215 @@ export enum TalerErrorCode {
*/
BANK_POST_WITHDRAWAL_OPERATION_REQUIRED = 5119,
+
+ /**
+ * The client tried to register a new account under a reserved username (like 'admin' for example).
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_RESERVED_USERNAME_CONFLICT = 5120,
+
+
+ /**
+ * The client tried to register a new account with an username already in use.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_REGISTER_USERNAME_REUSE = 5121,
+
+
+ /**
+ * The client tried to register a new account with a payto:// URI already in use.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_REGISTER_PAYTO_URI_REUSE = 5122,
+
+
+ /**
+ * The client tried to delete an account with a non null balance.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_ACCOUNT_BALANCE_NOT_ZERO = 5123,
+
+
+ /**
+ * The client tried to create a transaction or an operation that credit an unknown account.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_UNKNOWN_CREDITOR = 5124,
+
+
+ /**
+ * The client tried to create a transaction or an operation that debit an unknown account.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_UNKNOWN_DEBTOR = 5125,
+
+
+ /**
+ * The client tried to perform an action prohibited for exchange accounts.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_ACCOUNT_IS_EXCHANGE = 5126,
+
+
+ /**
+ * The client tried to perform an action reserved for exchange accounts.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_ACCOUNT_IS_NOT_EXCHANGE = 5127,
+
+
+ /**
+ * Received currency conversion is wrong.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_BAD_CONVERSION = 5128,
+
+
+ /**
+ * The account referenced in this operation is missing tan info for the chosen channel.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_MISSING_TAN_INFO = 5129,
+
+
+ /**
+ * The client attempted to confirm a transaction with incomplete info.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_CONFIRM_INCOMPLETE = 5130,
+
+
+ /**
+ * The request rate is too high. The server is refusing requests to guard against brute-force attacks.
+ * Returned with an HTTP status code of #MHD_HTTP_TOO_MANY_REQUESTS (429).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_TAN_RATE_LIMITED = 5131,
+
+
+ /**
+ * This TAN channel is not supported.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_IMPLEMENTED (501).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_TAN_CHANNEL_NOT_SUPPORTED = 5132,
+
+
+ /**
+ * Failed to send TAN using the helper script. Either script is not found, or script timeout, or script terminated with a non-successful result.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_TAN_CHANNEL_SCRIPT_FAILED = 5133,
+
+
+ /**
+ * The client's response to the challenge was invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_TAN_CHALLENGE_FAILED = 5134,
+
+
+ /**
+ * A non-admin user has tried to change their legal name.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_NON_ADMIN_PATCH_LEGAL_NAME = 5135,
+
+
+ /**
+ * A non-admin user has tried to change their debt limit.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_NON_ADMIN_PATCH_DEBT_LIMIT = 5136,
+
+
+ /**
+ * A non-admin user has tried to change their password whihout providing the current one.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_NON_ADMIN_PATCH_MISSING_OLD_PASSWORD = 5137,
+
+
+ /**
+ * Provided old password does not match current password.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_PATCH_BAD_OLD_PASSWORD = 5138,
+
+
+ /**
+ * An admin user has tried to become an exchange.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_PATCH_ADMIN_EXCHANGE = 5139,
+
+
+ /**
+ * A non-admin user has tried to change their cashout account.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_NON_ADMIN_PATCH_CASHOUT = 5140,
+
+
+ /**
+ * A non-admin user has tried to change their contact info.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_NON_ADMIN_PATCH_CONTACT = 5141,
+
+
+ /**
+ * The client tried to create a transaction that credit the admin account.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_ADMIN_CREDITOR = 5142,
+
+
+ /**
+ * The referenced challenge was not found.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_CHALLENGE_NOT_FOUND = 5143,
+
+
+ /**
+ * The referenced challenge has expired.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_TAN_CHALLENGE_EXPIRED = 5144,
+
+
+ /**
+ * A non-admin user has tried to create an account with 2fa.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_NON_ADMIN_SET_TAN_CHANNEL = 5145,
+
+
/**
* The sync service failed find the account in its database.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -2451,6 +3543,7 @@ export enum TalerErrorCode {
*/
SYNC_ACCOUNT_UNKNOWN = 6100,
+
/**
* The SHA-512 hash provided in the If-None-Match header is malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2458,6 +3551,7 @@ export enum TalerErrorCode {
*/
SYNC_BAD_IF_NONE_MATCH = 6101,
+
/**
* The SHA-512 hash provided in the If-Match header is malformed or missing.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2465,6 +3559,7 @@ export enum TalerErrorCode {
*/
SYNC_BAD_IF_MATCH = 6102,
+
/**
* The signature provided in the "Sync-Signature" header is malformed or missing.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2472,6 +3567,7 @@ export enum TalerErrorCode {
*/
SYNC_BAD_SYNC_SIGNATURE = 6103,
+
/**
* The signature provided in the "Sync-Signature" header does not match the account, old or new Etags.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -2479,6 +3575,7 @@ export enum TalerErrorCode {
*/
SYNC_INVALID_SIGNATURE = 6104,
+
/**
* The "Content-length" field for the upload is not a number.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2486,20 +3583,23 @@ export enum TalerErrorCode {
*/
SYNC_MALFORMED_CONTENT_LENGTH = 6105,
+
/**
* The "Content-length" field for the upload is too big based on the server's terms of service.
- * Returned with an HTTP status code of #MHD_HTTP_PAYLOAD_TOO_LARGE (413).
+ * Returned with an HTTP status code of #MHD_HTTP_CONTENT_TOO_LARGE (413).
* (A value of 0 indicates that the error is generated client-side).
*/
SYNC_EXCESSIVE_CONTENT_LENGTH = 6106,
+
/**
* The server is out of memory to handle the upload. Trying again later may succeed.
- * Returned with an HTTP status code of #MHD_HTTP_PAYLOAD_TOO_LARGE (413).
+ * Returned with an HTTP status code of #MHD_HTTP_CONTENT_TOO_LARGE (413).
* (A value of 0 indicates that the error is generated client-side).
*/
SYNC_OUT_OF_MEMORY_ON_CONTENT_LENGTH = 6107,
+
/**
* The uploaded data does not match the Etag.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2507,6 +3607,7 @@ export enum TalerErrorCode {
*/
SYNC_INVALID_UPLOAD = 6108,
+
/**
* HTTP server experienced a timeout while awaiting promised payment.
* Returned with an HTTP status code of #MHD_HTTP_REQUEST_TIMEOUT (408).
@@ -2514,6 +3615,7 @@ export enum TalerErrorCode {
*/
SYNC_PAYMENT_GENERIC_TIMEOUT = 6109,
+
/**
* Sync could not setup the payment request with its own backend.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -2521,6 +3623,7 @@ export enum TalerErrorCode {
*/
SYNC_PAYMENT_CREATE_BACKEND_ERROR = 6110,
+
/**
* The sync service failed find the backup to be updated in its database.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -2528,6 +3631,7 @@ export enum TalerErrorCode {
*/
SYNC_PREVIOUS_BACKUP_UNKNOWN = 6111,
+
/**
* The "Content-length" field for the upload is missing.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2535,6 +3639,7 @@ export enum TalerErrorCode {
*/
SYNC_MISSING_CONTENT_LENGTH = 6112,
+
/**
* Sync had problems communicating with its payment backend.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
@@ -2542,6 +3647,7 @@ export enum TalerErrorCode {
*/
SYNC_GENERIC_BACKEND_ERROR = 6113,
+
/**
* Sync experienced a timeout communicating with its payment backend.
* Returned with an HTTP status code of #MHD_HTTP_GATEWAY_TIMEOUT (504).
@@ -2549,6 +3655,7 @@ export enum TalerErrorCode {
*/
SYNC_GENERIC_BACKEND_TIMEOUT = 6114,
+
/**
* The wallet does not implement a version of the exchange protocol that is compatible with the protocol version of the exchange.
* Returned with an HTTP status code of #MHD_HTTP_NOT_IMPLEMENTED (501).
@@ -2556,6 +3663,7 @@ export enum TalerErrorCode {
*/
WALLET_EXCHANGE_PROTOCOL_VERSION_INCOMPATIBLE = 7000,
+
/**
* The wallet encountered an unexpected exception. This is likely a bug in the wallet implementation.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -2563,6 +3671,7 @@ export enum TalerErrorCode {
*/
WALLET_UNEXPECTED_EXCEPTION = 7001,
+
/**
* The wallet received a response from a server, but the response can't be parsed.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -2570,6 +3679,7 @@ export enum TalerErrorCode {
*/
WALLET_RECEIVED_MALFORMED_RESPONSE = 7002,
+
/**
* The wallet tried to make a network request, but it received no response.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -2577,6 +3687,7 @@ export enum TalerErrorCode {
*/
WALLET_NETWORK_ERROR = 7003,
+
/**
* The wallet tried to make a network request, but it was throttled.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -2584,6 +3695,7 @@ export enum TalerErrorCode {
*/
WALLET_HTTP_REQUEST_THROTTLED = 7004,
+
/**
* The wallet made a request to a service, but received an error response it does not know how to handle.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -2591,6 +3703,7 @@ export enum TalerErrorCode {
*/
WALLET_UNEXPECTED_REQUEST_ERROR = 7005,
+
/**
* The denominations offered by the exchange are insufficient. Likely the exchange is badly configured or not maintained.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -2598,6 +3711,7 @@ export enum TalerErrorCode {
*/
WALLET_EXCHANGE_DENOMINATIONS_INSUFFICIENT = 7006,
+
/**
* The wallet does not support the operation requested by a client.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -2605,6 +3719,7 @@ export enum TalerErrorCode {
*/
WALLET_CORE_API_OPERATION_UNKNOWN = 7007,
+
/**
* The given taler://pay URI is invalid.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -2612,6 +3727,7 @@ export enum TalerErrorCode {
*/
WALLET_INVALID_TALER_PAY_URI = 7008,
+
/**
* The signature on a coin by the exchange's denomination key is invalid after unblinding it.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -2619,6 +3735,7 @@ export enum TalerErrorCode {
*/
WALLET_EXCHANGE_COIN_SIGNATURE_INVALID = 7009,
+
/**
* The exchange does not know about the reserve (yet), and thus withdrawal can't progress.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -2626,6 +3743,7 @@ export enum TalerErrorCode {
*/
WALLET_EXCHANGE_WITHDRAW_RESERVE_UNKNOWN_AT_EXCHANGE = 7010,
+
/**
* The wallet core service is not available.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -2633,6 +3751,7 @@ export enum TalerErrorCode {
*/
WALLET_CORE_NOT_AVAILABLE = 7011,
+
/**
* The bank has aborted a withdrawal operation, and thus a withdrawal can't complete.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -2640,6 +3759,7 @@ export enum TalerErrorCode {
*/
WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK = 7012,
+
/**
* An HTTP request made by the wallet timed out.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -2647,6 +3767,7 @@ export enum TalerErrorCode {
*/
WALLET_HTTP_REQUEST_GENERIC_TIMEOUT = 7013,
+
/**
* The order has already been claimed by another wallet.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -2654,6 +3775,7 @@ export enum TalerErrorCode {
*/
WALLET_ORDER_ALREADY_CLAIMED = 7014,
+
/**
* A group of withdrawal operations (typically for the same reserve at the same exchange) has errors and will be tried again later.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -2661,12 +3783,14 @@ export enum TalerErrorCode {
*/
WALLET_WITHDRAWAL_GROUP_INCOMPLETE = 7015,
+
/**
- * The signature on a coin by the exchange's denomination key (obtained through the merchant via tipping) is invalid after unblinding it.
+ * The signature on a coin by the exchange's denomination key (obtained through the merchant via a reward) is invalid after unblinding it.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
* (A value of 0 indicates that the error is generated client-side).
*/
- WALLET_TIPPING_COIN_SIGNATURE_INVALID = 7016,
+ WALLET_REWARD_COIN_SIGNATURE_INVALID = 7016,
+
/**
* The wallet does not implement a version of the bank integration API that is compatible with the version offered by the bank.
@@ -2675,6 +3799,7 @@ export enum TalerErrorCode {
*/
WALLET_BANK_INTEGRATION_PROTOCOL_VERSION_INCOMPATIBLE = 7017,
+
/**
* The wallet processed a taler://pay URI, but the merchant base URL in the downloaded contract terms does not match the merchant base URL derived from the URI.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -2682,6 +3807,7 @@ export enum TalerErrorCode {
*/
WALLET_CONTRACT_TERMS_BASE_URL_MISMATCH = 7018,
+
/**
* The merchant's signature on the contract terms is invalid.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -2689,6 +3815,7 @@ export enum TalerErrorCode {
*/
WALLET_CONTRACT_TERMS_SIGNATURE_INVALID = 7019,
+
/**
* The contract terms given by the merchant are malformed.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -2696,6 +3823,7 @@ export enum TalerErrorCode {
*/
WALLET_CONTRACT_TERMS_MALFORMED = 7020,
+
/**
* A pending operation failed, and thus the request can't be completed.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -2703,6 +3831,7 @@ export enum TalerErrorCode {
*/
WALLET_PENDING_OPERATION_FAILED = 7021,
+
/**
* A payment was attempted, but the merchant had an internal server error (5xx).
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -2710,6 +3839,7 @@ export enum TalerErrorCode {
*/
WALLET_PAY_MERCHANT_SERVER_ERROR = 7022,
+
/**
* The crypto worker failed.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -2717,6 +3847,7 @@ export enum TalerErrorCode {
*/
WALLET_CRYPTO_WORKER_ERROR = 7023,
+
/**
* The crypto worker received a bad request.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -2724,6 +3855,7 @@ export enum TalerErrorCode {
*/
WALLET_CRYPTO_WORKER_BAD_REQUEST = 7024,
+
/**
* A KYC step is required before withdrawal can proceed.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -2731,6 +3863,95 @@ export enum TalerErrorCode {
*/
WALLET_WITHDRAWAL_KYC_REQUIRED = 7025,
+
+ /**
+ * The wallet does not have sufficient balance to create a deposit group.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE = 7026,
+
+
+ /**
+ * The wallet does not have sufficient balance to create a peer push payment.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE = 7027,
+
+
+ /**
+ * The wallet does not have sufficient balance to pay for an invoice.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_PEER_PULL_PAYMENT_INSUFFICIENT_BALANCE = 7028,
+
+
+ /**
+ * A group of refresh operations has errors and will be tried again later.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_REFRESH_GROUP_INCOMPLETE = 7029,
+
+
+ /**
+ * The exchange's self-reported base URL does not match the one that the wallet is using.
+ * 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_BASE_URL_MISMATCH = 7030,
+
+
+ /**
+ * The order has already been paid by another wallet.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_ORDER_ALREADY_PAID = 7031,
+
+
+ /**
+ * An exchange that is required for some request is currently not available.
+ * 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_UNAVAILABLE = 7032,
+
+
+ /**
+ * An exchange entry is still used by the exchange, thus it can't be deleted without purging.
+ * 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_USED = 7033,
+
+
+ /**
+ * The wallet database is unavailable and the wallet thus is not operational.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_DB_UNAVAILABLE = 7034,
+
+
+ /**
+ * A taler:// URI is malformed and can't be parsed.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_TALER_URI_MALFORMED = 7035,
+
+
+ /**
+ * A wallet-core request was cancelled and thus can't provide a response.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_CORE_REQUEST_CANCELLED = 7036,
+
+
/**
* We encountered a timeout with our payment backend.
* Returned with an HTTP status code of #MHD_HTTP_GATEWAY_TIMEOUT (504).
@@ -2738,6 +3959,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_GENERIC_BACKEND_TIMEOUT = 8000,
+
/**
* The backend requested payment, but the request is malformed.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -2745,6 +3967,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_GENERIC_INVALID_PAYMENT_REQUEST = 8001,
+
/**
* The backend got an unexpected reply from the payment processor.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
@@ -2752,6 +3975,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_GENERIC_BACKEND_ERROR = 8002,
+
/**
* The "Content-length" field for the upload is missing.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2759,6 +3983,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_GENERIC_MISSING_CONTENT_LENGTH = 8003,
+
/**
* The "Content-length" field for the upload is malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2766,6 +3991,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_GENERIC_MALFORMED_CONTENT_LENGTH = 8004,
+
/**
* The backend failed to setup an order with the payment processor.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
@@ -2773,6 +3999,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_GENERIC_ORDER_CREATE_BACKEND_ERROR = 8005,
+
/**
* The backend was not authorized to check for payment with the payment processor.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -2780,6 +4007,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_GENERIC_PAYMENT_CHECK_UNAUTHORIZED = 8006,
+
/**
* The backend could not check payment status with the payment processor.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -2787,6 +4015,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_GENERIC_PAYMENT_CHECK_START_FAILED = 8007,
+
/**
* The Anastasis provider could not be reached.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -2794,6 +4023,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_GENERIC_PROVIDER_UNREACHABLE = 8008,
+
/**
* HTTP server experienced a timeout while awaiting promised payment.
* Returned with an HTTP status code of #MHD_HTTP_REQUEST_TIMEOUT (408).
@@ -2801,6 +4031,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_PAYMENT_GENERIC_TIMEOUT = 8009,
+
/**
* The key share is unknown to the provider.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -2808,6 +4039,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TRUTH_UNKNOWN = 8108,
+
/**
* The authorization method used for the key share is no longer supported by the provider.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -2815,6 +4047,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TRUTH_AUTHORIZATION_METHOD_NO_LONGER_SUPPORTED = 8109,
+
/**
* The client needs to respond to the challenge.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -2822,6 +4055,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TRUTH_CHALLENGE_RESPONSE_REQUIRED = 8110,
+
/**
* The client's response to the challenge was invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -2829,6 +4063,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TRUTH_CHALLENGE_FAILED = 8111,
+
/**
* The backend is not aware of having issued the provided challenge code. Either this is the wrong code, or it has expired.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -2836,6 +4071,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TRUTH_CHALLENGE_UNKNOWN = 8112,
+
/**
* The backend failed to initiate the authorization process.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -2843,6 +4079,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TRUTH_AUTHORIZATION_START_FAILED = 8114,
+
/**
* The authorization succeeded, but the key share is no longer available.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -2850,6 +4087,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TRUTH_KEY_SHARE_GONE = 8115,
+
/**
* The backend forgot the order we asked the client to pay for
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
@@ -2857,6 +4095,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TRUTH_ORDER_DISAPPEARED = 8116,
+
/**
* The backend itself reported a bad exchange interaction.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
@@ -2864,6 +4103,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TRUTH_BACKEND_EXCHANGE_BAD = 8117,
+
/**
* The backend reported a payment status we did not expect.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -2871,6 +4111,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TRUTH_UNEXPECTED_PAYMENT_STATUS = 8118,
+
/**
* The backend failed to setup the order for payment.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
@@ -2878,6 +4119,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TRUTH_PAYMENT_CREATE_BACKEND_ERROR = 8119,
+
/**
* The decryption of the key share failed with the provided key.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2885,6 +4127,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TRUTH_DECRYPTION_FAILED = 8120,
+
/**
* The request rate is too high. The server is refusing requests to guard against brute-force attacks.
* Returned with an HTTP status code of #MHD_HTTP_TOO_MANY_REQUESTS (429).
@@ -2892,6 +4135,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TRUTH_RATE_LIMITED = 8121,
+
/**
* A request to issue a challenge is not valid for this authentication method.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2899,6 +4143,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TRUTH_CHALLENGE_WRONG_METHOD = 8123,
+
/**
* The backend failed to store the key share because the UUID is already in use.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2906,6 +4151,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TRUTH_UPLOAD_UUID_EXISTS = 8150,
+
/**
* The backend failed to store the key share because the authorization method is not supported.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2913,6 +4159,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TRUTH_UPLOAD_METHOD_NOT_SUPPORTED = 8151,
+
/**
* The provided phone number is not an acceptable number.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2920,6 +4167,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_SMS_PHONE_INVALID = 8200,
+
/**
* Failed to run the SMS transmission helper process.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -2927,6 +4175,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_SMS_HELPER_EXEC_FAILED = 8201,
+
/**
* Provider failed to send SMS. Helper terminated with a non-successful result.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -2934,6 +4183,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_SMS_HELPER_COMMAND_FAILED = 8202,
+
/**
* The provided email address is not an acceptable address.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2941,6 +4191,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_EMAIL_INVALID = 8210,
+
/**
* Failed to run the E-mail transmission helper process.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -2948,6 +4199,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_EMAIL_HELPER_EXEC_FAILED = 8211,
+
/**
* Provider failed to send E-mail. Helper terminated with a non-successful result.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -2955,6 +4207,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_EMAIL_HELPER_COMMAND_FAILED = 8212,
+
/**
* The provided postal address is not an acceptable address.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2962,6 +4215,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_POST_INVALID = 8220,
+
/**
* Failed to run the mail transmission helper process.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -2969,6 +4223,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_POST_HELPER_EXEC_FAILED = 8221,
+
/**
* Provider failed to send mail. Helper terminated with a non-successful result.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -2976,6 +4231,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_POST_HELPER_COMMAND_FAILED = 8222,
+
/**
* The provided IBAN address is not an acceptable IBAN.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2983,6 +4239,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_IBAN_INVALID = 8230,
+
/**
* The provider has not yet received the IBAN wire transfer authorizing the disclosure of the key share.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -2990,6 +4247,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_IBAN_MISSING_TRANSFER = 8231,
+
/**
* The backend did not find a TOTP key in the data provided.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2997,6 +4255,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TOTP_KEY_MISSING = 8240,
+
/**
* The key provided does not satisfy the format restrictions for an Anastasis TOTP key.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -3004,6 +4263,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TOTP_KEY_INVALID = 8241,
+
/**
* The given if-none-match header is malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -3011,13 +4271,15 @@ export enum TalerErrorCode {
*/
ANASTASIS_POLICY_BAD_IF_NONE_MATCH = 8301,
+
/**
* The server is out of memory to handle the upload. Trying again later may succeed.
- * Returned with an HTTP status code of #MHD_HTTP_PAYLOAD_TOO_LARGE (413).
+ * Returned with an HTTP status code of #MHD_HTTP_CONTENT_TOO_LARGE (413).
* (A value of 0 indicates that the error is generated client-side).
*/
ANASTASIS_POLICY_OUT_OF_MEMORY_ON_CONTENT_LENGTH = 8304,
+
/**
* The signature provided in the "Anastasis-Policy-Signature" header is malformed or missing.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -3025,6 +4287,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_POLICY_BAD_SIGNATURE = 8305,
+
/**
* The given if-match header is malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -3032,6 +4295,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_POLICY_BAD_IF_MATCH = 8306,
+
/**
* The uploaded data does not match the Etag.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -3039,6 +4303,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_POLICY_INVALID_UPLOAD = 8307,
+
/**
* The provider is unaware of the requested policy.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -3046,6 +4311,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_POLICY_NOT_FOUND = 8350,
+
/**
* The given action is invalid for the current state of the reducer.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3053,6 +4319,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_ACTION_INVALID = 8400,
+
/**
* The given state of the reducer is invalid.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3060,6 +4327,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_STATE_INVALID = 8401,
+
/**
* The given input to the reducer is invalid.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3067,6 +4335,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_INPUT_INVALID = 8402,
+
/**
* The selected authentication method does not work for the Anastasis provider.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3074,6 +4343,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_AUTHENTICATION_METHOD_NOT_SUPPORTED = 8403,
+
/**
* The given input and action do not work for the current state.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3081,6 +4351,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_INPUT_INVALID_FOR_STATE = 8404,
+
/**
* We experienced an unexpected failure interacting with the backend.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3088,6 +4359,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_BACKEND_FAILURE = 8405,
+
/**
* The contents of a resource file did not match our expectations.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3095,6 +4367,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_RESOURCE_MALFORMED = 8406,
+
/**
* A required resource file is missing.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3102,6 +4375,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_RESOURCE_MISSING = 8407,
+
/**
* An input did not match the regular expression.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3109,6 +4383,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_INPUT_REGEX_FAILED = 8408,
+
/**
* An input did not match the custom validation logic.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3116,6 +4391,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_INPUT_VALIDATION_FAILED = 8409,
+
/**
* Our attempts to download the recovery document failed with all providers. Most likely the personal information you entered differs from the information you provided during the backup process and you should go back to the previous step. Alternatively, if you used a backup provider that is unknown to this application, you should add that provider manually.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3123,6 +4399,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_POLICY_LOOKUP_FAILED = 8410,
+
/**
* Anastasis provider reported a fatal failure.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3130,6 +4407,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_BACKUP_PROVIDER_FAILED = 8411,
+
/**
* Anastasis provider failed to respond to the configuration request.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3137,6 +4415,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_PROVIDER_CONFIG_FAILED = 8412,
+
/**
* The policy we downloaded is malformed. Must have been a client error while creating the backup.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3144,6 +4423,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_POLICY_MALFORMED = 8413,
+
/**
* We failed to obtain the policy, likely due to a network issue.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3151,6 +4431,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_NETWORK_FAILED = 8414,
+
/**
* The recovered secret did not match the required syntax.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3158,6 +4439,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_SECRET_MALFORMED = 8415,
+
/**
* The challenge data provided is too large for the available providers.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3165,6 +4447,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_CHALLENGE_DATA_TOO_BIG = 8416,
+
/**
* The provided core secret is too large for some of the providers.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3172,6 +4455,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_SECRET_TOO_BIG = 8417,
+
/**
* The provider returned in invalid configuration.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3179,6 +4463,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_PROVIDER_INVALID_CONFIG = 8418,
+
/**
* The reducer encountered an internal error, likely a bug that needs to be reported.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3186,6 +4471,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_INTERNAL_ERROR = 8419,
+
/**
* The reducer already synchronized with all providers.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3193,6 +4479,31 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_PROVIDERS_ALREADY_SYNCED = 8420,
+
+ /**
+ * The Donau failed to perform the operation as it could not find the private keys. This is a problem with the Donau setup, not with the client's request.
+ * Returned with an HTTP status code of #MHD_HTTP_SERVICE_UNAVAILABLE (503).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DONAU_GENERIC_KEYS_MISSING = 8607,
+
+
+ /**
+ * The signature of the charity key is not valid.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DONAU_CHARITY_SIGNATURE_INVALID = 8608,
+
+
+ /**
+ * The charity is unknown.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DONAU_CHARITY_NOT_FOUND = 8609,
+
+
/**
* A generic error happened in the LibEuFin nexus. See the enclose details JSON for more information.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3200,6 +4511,7 @@ export enum TalerErrorCode {
*/
LIBEUFIN_NEXUS_GENERIC_ERROR = 9000,
+
/**
* An uncaught exception happened in the LibEuFin nexus service.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -3207,6 +4519,7 @@ export enum TalerErrorCode {
*/
LIBEUFIN_NEXUS_UNCAUGHT_EXCEPTION = 9001,
+
/**
* A generic error happened in the LibEuFin sandbox. See the enclose details JSON for more information.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3214,6 +4527,7 @@ export enum TalerErrorCode {
*/
LIBEUFIN_SANDBOX_GENERIC_ERROR = 9500,
+
/**
* An uncaught exception happened in the LibEuFin sandbox service.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -3221,6 +4535,7 @@ export enum TalerErrorCode {
*/
LIBEUFIN_SANDBOX_UNCAUGHT_EXCEPTION = 9501,
+
/**
* This validation method is not supported by the service.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -3228,6 +4543,7 @@ export enum TalerErrorCode {
*/
TALDIR_METHOD_NOT_SUPPORTED = 9600,
+
/**
* Number of allowed attempts for initiating a challenge exceeded.
* Returned with an HTTP status code of #MHD_HTTP_TOO_MANY_REQUESTS (429).
@@ -3235,10 +4551,93 @@ export enum TalerErrorCode {
*/
TALDIR_REGISTER_RATE_LIMITED = 9601,
+
+ /**
+ * The client is unknown or unauthorized.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ CHALLENGER_GENERIC_CLIENT_UNKNOWN = 9750,
+
+
+ /**
+ * The client is not authorized to use the given redirect URI.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ CHALLENGER_GENERIC_CLIENT_FORBIDDEN_BAD_REDIRECT_URI = 9751,
+
+
+ /**
+ * The service failed to execute its helper process to send the challenge.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ CHALLENGER_HELPER_EXEC_FAILED = 9752,
+
+
+ /**
+ * The grant is unknown to the service (it could also have expired).
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ CHALLENGER_GRANT_UNKNOWN = 9753,
+
+
+ /**
+ * The code given is not even well-formed.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ CHALLENGER_CLIENT_FORBIDDEN_BAD_CODE = 9754,
+
+
+ /**
+ * The service is not aware of the referenced validation process.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ CHALLENGER_GENERIC_VALIDATION_UNKNOWN = 9755,
+
+
+ /**
+ * The code given is not valid.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ CHALLENGER_CLIENT_FORBIDDEN_INVALID_CODE = 9756,
+
+
+ /**
+ * Too many attempts have been made, validation is temporarily disabled for this address.
+ * Returned with an HTTP status code of #MHD_HTTP_TOO_MANY_REQUESTS (429).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ CHALLENGER_TOO_MANY_ATTEMPTS = 9757,
+
+
+ /**
+ * The PIN code provided is incorrect.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ CHALLENGER_INVALID_PIN = 9758,
+
+
+ /**
+ * The token cannot be valid as no address was ever provided by the client.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ CHALLENGER_MISSING_ADDRESS = 9759,
+
+
/**
* End of error code range.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
* (A value of 0 indicates that the error is generated client-side).
*/
END = 9999,
+
+
}
diff --git a/packages/taler-util/src/taler-types.ts b/packages/taler-util/src/taler-types.ts
index 292ace94b..2b8e55e38 100644
--- a/packages/taler-util/src/taler-types.ts
+++ b/packages/taler-util/src/taler-types.ts
@@ -25,29 +25,34 @@
* Imports.
*/
-import { codecForAmountString } from "./amounts.js";
+import { Amounts, codecForAmountString } from "./amounts.js";
import {
+ Codec,
buildCodecForObject,
buildCodecForUnion,
- Codec,
codecForAny,
codecForBoolean,
- codecForConstNumber,
codecForConstString,
codecForList,
codecForMap,
codecForNumber,
codecForString,
+ codecForStringURL,
codecOptional,
} from "./codec.js";
import { strcmp } from "./helpers.js";
-import { AgeCommitmentProof, Edx25519PublicKeyEnc } from "./taler-crypto.js";
import {
- codecForAbsoluteTime,
- codecForDuration,
- codecForTimestamp,
+ CurrencySpecification,
+ codecForCurrencySpecificiation,
+ codecForEither,
+ codecForProduct,
+} from "./index.js";
+import { Edx25519PublicKeyEnc } from "./taler-crypto.js";
+import {
TalerProtocolDuration,
TalerProtocolTimestamp,
+ codecForDuration,
+ codecForTimestamp,
} from "./time.js";
/**
@@ -298,15 +303,11 @@ export interface CoinDepositPermission {
* merchant's contract terms.
*/
export interface ExchangeHandle {
- /**
- * Master public signing key of the exchange.
- */
- master_pub: string;
-
- /**
- * Base URL of the exchange.
- */
+ // The exchange's base URL.
url: string;
+
+ // Master public key of the exchange.
+ master_pub: EddsaPublicKeyString;
}
export interface AuditorHandle {
@@ -318,7 +319,7 @@ export interface AuditorHandle {
/**
* Master public signing key of the auditor.
*/
- auditor_pub: string;
+ auditor_pub: EddsaPublicKeyString;
/**
* Base URL of the auditor.
@@ -362,12 +363,24 @@ export interface Location {
}
export interface MerchantInfo {
+ // The merchant's legal name of business.
name: string;
- jurisdiction?: Location;
- address?: Location;
- logo?: string;
- website?: string;
+
+ // Label for a location with the business address of the merchant.
email?: string;
+
+ // Label for a location with the business address of the merchant.
+ website?: string;
+
+ // An optional base64-encoded product image.
+ logo?: ImageDataUrl;
+
+ // Label for a location with the business address of the merchant.
+ address?: Location;
+
+ // Label for a location that denotes the jurisdiction for disputes.
+ // Some of the typical fields for a location (such as a street address) may be absent.
+ jurisdiction?: Location;
}
export interface Tax {
@@ -386,10 +399,10 @@ export interface Product {
description: string;
// Map from IETF BCP 47 language tags to localized descriptions
- description_i18n?: { [lang_tag: string]: string };
+ description_i18n?: InternationalizedString;
// The number of units of the product to deliver to the customer.
- quantity?: number;
+ quantity?: Integer;
// The unit in which the product is measured (liters, kilograms, packages, etc.)
unit?: string;
@@ -398,7 +411,7 @@ export interface Product {
price?: AmountString;
// An optional base64-encoded product image
- image?: string;
+ image?: ImageDataUrl;
// a list of taxes paid by the merchant for this product. Can be empty.
taxes?: Tax[];
@@ -416,147 +429,147 @@ export interface InternationalizedString {
* FIXME: Add type field!
*/
export interface MerchantContractTerms {
- /**
- * Hash of the merchant's wire details.
- */
+ // The hash of the merchant instance's wire details.
h_wire: string;
- /**
- * Hash of the merchant's wire details.
- */
+ // Specifies for how long the wallet should try to get an
+ // automatic refund for the purchase. If this field is
+ // present, the wallet should wait for a few seconds after
+ // the purchase and then automatically attempt to obtain
+ // a refund. The wallet should probe until "delay"
+ // after the payment was successful (i.e. via long polling
+ // or via explicit requests with exponential back-off).
+ //
+ // In particular, if the wallet is offline
+ // at that time, it MUST repeat the request until it gets
+ // one response from the merchant after the delay has expired.
+ // If the refund is granted, the wallet MUST automatically
+ // recover the payment. This is used in case a merchant
+ // knows that it might be unable to satisfy the contract and
+ // desires for the wallet to attempt to get the refund without any
+ // customer interaction. Note that it is NOT an error if the
+ // merchant does not grant a refund.
auto_refund?: TalerProtocolDuration;
- /**
- * Wire method the merchant wants to use.
- */
+ // Wire transfer method identifier for the wire method associated with h_wire.
+ // The wallet may only select exchanges via a matching auditor if the
+ // exchange also supports this wire method.
+ // The wire transfer fees must be added based on this wire transfer method.
wire_method: string;
- /**
- * Human-readable short summary of the contract.
- */
+ // Human-readable description of the whole purchase.
summary: string;
+ // Map from IETF BCP 47 language tags to localized summaries.
summary_i18n?: InternationalizedString;
- /**
- * Nonce used to ensure freshness.
- */
- nonce: 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 amount payable.
- */
+ // Total price for the transaction.
+ // The exchange will subtract deposit fees from that amount
+ // before transferring it to the merchant.
amount: string;
- /**
- * Auditors accepted by the merchant.
- */
- auditors: AuditorHandle[];
+ // Nonce generated by the wallet and echoed by the merchant
+ // in this field when the proposal is generated.
+ nonce: string;
- /**
- * Deadline to pay for the contract.
- */
+ // After this deadline, the merchant won't accept payments for the contract.
pay_deadline: TalerProtocolTimestamp;
- /**
- * Maximum deposit fee covered by the merchant.
- */
- max_fee: string;
-
- /**
- * Information about the merchant.
- */
+ // More info about the merchant, see below.
merchant: MerchantInfo;
- /**
- * Public key of the merchant.
- */
+ // Merchant's public key used to sign this proposal; this information
+ // is typically added by the backend. Note that this can be an ephemeral key.
merchant_pub: string;
- /**
- * Time indicating when the order should be delivered.
- * May be overwritten by individual products.
- */
+ // Time indicating when the order should be delivered.
+ // May be overwritten by individual products.
delivery_date?: TalerProtocolTimestamp;
- /**
- * Delivery location for (all!) products.
- */
+ // Delivery location for (all!) products.
delivery_location?: Location;
- /**
- * List of accepted exchanges.
- */
+ // Exchanges that the merchant accepts even if it does not accept any auditors that audit them.
exchanges: ExchangeHandle[];
- /**
- * Products that are sold in this contract.
- */
+ // List of products that are part of the purchase (see Product).
products?: Product[];
- /**
- * Deadline for refunds.
- */
+ // After this deadline has passed, no refunds will be accepted.
refund_deadline: TalerProtocolTimestamp;
- /**
- * Deadline for the wire transfer.
- */
+ // Transfer deadline for the exchange. Must be in the
+ // deposit permissions of coins used to pay for this order.
wire_transfer_deadline: TalerProtocolTimestamp;
- /**
- * Time when the contract was generated by the merchant.
- */
+ // Time when this contract was generated.
timestamp: TalerProtocolTimestamp;
- /**
- * Order id to uniquely identify the purchase within
- * one merchant instance.
- */
- order_id: string;
-
- /**
- * Base URL of the merchant's backend.
- */
+ // Base URL of the (public!) merchant backend API.
+ // Must be an absolute URL that ends with a slash.
merchant_base_url: string;
- /**
- * Fulfillment URL to view the product or
- * delivery status.
- */
+ // URL that will show that the order was successful after
+ // it has been paid for. Optional, but either fulfillment_url
+ // or fulfillment_message must be specified in every
+ // contract terms.
+ //
+ // If a non-unique fulfillment URL is used, a customer can only
+ // buy the order once and will be redirected to a previous purchase
+ // when trying to buy an order with the same fulfillment URL a second
+ // time. This is useful for digital goods that a customer only needs
+ // to buy once but should be able to repeatedly download.
+ //
+ // For orders where the customer is expected to be able to make
+ // repeated purchases (for equivalent goods), the fulfillment URL
+ // should be made unique for every order. The easiest way to do
+ // this is to include a unique order ID in the fulfillment URL.
+ //
+ // When POSTing to the merchant, the placeholder text "${ORDER_ID}"
+ // is be replaced with the actual order ID (useful if the
+ // order ID is generated server-side and needs to be
+ // in the URL). Note that this placeholder can only be used once.
+ // Front-ends may use other means to generate a unique fulfillment URL.
fulfillment_url?: string;
- /**
- * URL meant to share the shopping cart.
- */
+ // URL where the same contract could be ordered again (if
+ // available). Returned also at the public order endpoint
+ // for people other than the actual buyer (hence public,
+ // in case order IDs are guessable).
public_reorder_url?: string;
- /**
- * Plain text fulfillment message in the merchant's default language.
- */
+ // Message shown to the customer after paying for the order.
+ // Either fulfillment_url or fulfillment_message must be specified.
fulfillment_message?: string;
- /**
- * Internationalized fulfillment messages.
- */
+ // Map from IETF BCP 47 language tags to localized fulfillment
+ // messages.
fulfillment_message_i18n?: InternationalizedString;
- /**
- * Share of the wire fee that must be settled with one payment.
- */
- wire_fee_amortization?: number;
-
- /**
- * Maximum wire fee that the merchant agrees to pay for.
- */
- max_wire_fee?: string;
-
- minimum_age?: number;
+ // Maximum total deposit fee accepted by the merchant for this contract.
+ // Overrides defaults of the merchant instance.
+ max_fee: string;
- /**
- * Extra data, interpreted by the mechant only.
- */
+ // Extra data that is only interpreted by the merchant frontend.
+ // Useful when the merchant needs to store extra information on a
+ // contract without storing it separately in their database.
+ // Must really be an Object (not a string, integer, float or array).
extra?: any;
+
+ // Minimum age the buyer must have (in years). Default is 0.
+ // This value is at least as large as the maximum over all
+ // minimum age requirements of the products in this contract.
+ // It might also be set independent of any product, due to
+ // legal requirements.
+ minimum_age?: Integer;
}
/**
@@ -611,27 +624,6 @@ export interface MerchantAbortPayRefundDetails {
}
/**
- * Response for a refund pickup or a /pay in abort mode.
- */
-export interface MerchantRefundResponse {
- /**
- * Public key of the merchant
- */
- merchant_pub: string;
-
- /**
- * Contract terms hash of the contract that
- * is being refunded.
- */
- h_contract_terms: string;
-
- /**
- * The signed refund permissions, to be sent to the exchange.
- */
- refunds: MerchantAbortPayRefundDetails[];
-}
-
-/**
* Planchet detail sent to the merchant.
*/
export interface TipPlanchetDetail {
@@ -725,9 +717,11 @@ export class ExchangeSignKeyJson {
*/
export class ExchangeKeysJson {
/**
- * List of offered denominations.
+ * Canonical, public base URL of the exchange.
*/
- denoms: ExchangeDenomination[];
+ base_url: string;
+
+ currency: string;
/**
* The exchange's master public key.
@@ -763,6 +757,112 @@ export class ExchangeKeysJson {
reserve_closing_delay: TalerProtocolDuration;
global_fees: GlobalFees[];
+
+ accounts: ExchangeWireAccount[];
+
+ wire_fees: { [methodName: string]: WireFeesJson[] };
+
+ denominations: DenomGroup[];
+}
+
+export type DenomGroup =
+ | DenomGroupRsa
+ | DenomGroupCs
+ | DenomGroupRsaAgeRestricted
+ | DenomGroupCsAgeRestricted;
+
+export interface DenomGroupCommon {
+ // How much are coins of this denomination worth?
+ value: AmountString;
+
+ // Fee charged by the exchange for withdrawing a coin of this denomination.
+ fee_withdraw: AmountString;
+
+ // Fee charged by the exchange for depositing a coin of this denomination.
+ fee_deposit: AmountString;
+
+ // Fee charged by the exchange for refreshing a coin of this denomination.
+ fee_refresh: AmountString;
+
+ // Fee charged by the exchange for refunding a coin of this denomination.
+ fee_refund: AmountString;
+
+ // XOR of all the SHA-512 hash values of the denominations' public keys
+ // in this group. Note that for hashing, the binary format of the
+ // public keys is used, and not their base32 encoding.
+ hash: HashCodeString;
+}
+
+export interface DenomCommon {
+ // Signature of TALER_DenominationKeyValidityPS.
+ master_sig: EddsaSignatureString;
+
+ // When does the denomination key become valid?
+ stamp_start: TalerProtocolTimestamp;
+
+ // When is it no longer possible to deposit coins
+ // of this denomination?
+ stamp_expire_withdraw: TalerProtocolTimestamp;
+
+ // Timestamp indicating by when legal disputes relating to these coins must
+ // be settled, as the exchange will afterwards destroy its evidence relating to
+ // transactions involving this coin.
+ stamp_expire_legal: TalerProtocolTimestamp;
+
+ stamp_expire_deposit: TalerProtocolTimestamp;
+
+ // Set to 'true' if the exchange somehow "lost"
+ // the private key. The denomination was not
+ // necessarily revoked, but still cannot be used
+ // to withdraw coins at this time (theoretically,
+ // the private key could be recovered in the
+ // future; coins signed with the private key
+ // remain valid).
+ lost?: boolean;
+}
+
+export type RsaPublicKeySring = string;
+export type AgeMask = number;
+export type ImageDataUrl = string;
+
+/**
+ * 32-byte value representing a point on Curve25519.
+ */
+export type Cs25519Point = string;
+
+export interface DenomGroupRsa extends DenomGroupCommon {
+ cipher: "RSA";
+
+ denoms: ({
+ rsa_pub: RsaPublicKeySring;
+ } & DenomCommon)[];
+}
+
+export interface DenomGroupRsaAgeRestricted extends DenomGroupCommon {
+ cipher: "RSA+age_restricted";
+ age_mask: AgeMask;
+
+ denoms: ({
+ rsa_pub: RsaPublicKeySring;
+ } & DenomCommon)[];
+}
+
+export interface DenomGroupCs extends DenomGroupCommon {
+ cipher: "CS";
+ age_mask: AgeMask;
+
+ denoms: ({
+ cs_pub: Cs25519Point;
+ } & DenomCommon)[];
+}
+
+export interface DenomGroupCsAgeRestricted extends DenomGroupCommon {
+ cipher: "CS+age_restricted";
+ age_mask: AgeMask;
+
+ denoms: ({
+ cs_pub: Cs25519Point;
+ } & DenomCommon)[];
}
export interface GlobalFees {
@@ -837,16 +937,6 @@ export class WireFeesJson {
end_date: TalerProtocolTimestamp;
}
-export interface AccountInfo {
- payto_uri: string;
- master_sig: string;
-}
-
-export interface ExchangeWireJson {
- accounts: AccountInfo[];
- fees: { [methodName: string]: WireFeesJson[] };
-}
-
/**
* Proposal returned from the contract URL.
*/
@@ -880,6 +970,8 @@ export class CheckPaymentResponse {
* Response from the bank.
*/
export class WithdrawOperationStatusResponse {
+ status: "selected" | "aborted" | "confirmed" | "pending";
+
selection_done: boolean;
transfer_done: boolean;
@@ -900,11 +992,13 @@ export class WithdrawOperationStatusResponse {
/**
* Response from the merchant.
*/
-export class TipPickupGetResponse {
- tip_amount: string;
+export class RewardPickupGetResponse {
+ reward_amount: string;
exchange_url: string;
+ next_url?: string;
+
expiration: TalerProtocolTimestamp;
}
@@ -949,16 +1043,17 @@ export const codecForBlindedDenominationSignature = () =>
.alternative(DenomKeyType.Rsa, codecForRsaBlindedDenominationSignature())
.build("BlindedDenominationSignature");
-export class WithdrawResponse {
+export class ExchangeWithdrawResponse {
ev_sig: BlindedDenominationSignature;
}
-export class WithdrawBatchResponse {
- ev_sigs: WithdrawResponse[];
+export class ExchangeWithdrawBatchResponse {
+ ev_sigs: ExchangeWithdrawResponse[];
}
export interface MerchantPayResponse {
sig: string;
+ pos_confirmation?: string;
}
export interface ExchangeMeltRequest {
@@ -1128,9 +1223,42 @@ export interface MerchantOrderStatusUnpaid {
* POST {talerBankIntegrationApi}/withdrawal-operation/{wopid}
*/
export interface BankWithdrawalOperationPostResponse {
+ // Current status of the operation
+ // pending: the operation is pending parameters selection (exchange and reserve public key)
+ // selected: the operations has been selected and is pending confirmation
+ // aborted: the operation has been aborted
+ // confirmed: the transfer has been confirmed and registered by the bank
+ status: "selected" | "aborted" | "confirmed" | "pending";
+
+ // URL that the user needs to navigate to in order to
+ // complete some final confirmation (e.g. 2FA).
+ //
+ // Only applicable when status is selected or pending.
+ // It may contain withdrawal operation id
+ confirm_transfer_url?: string;
+
+ // Deprecated field use status instead
+ // The transfer has been confirmed and registered by the bank.
+ // Does not guarantee that the funds have arrived at the exchange already.
transfer_done: boolean;
}
+export const codeForBankWithdrawalOperationPostResponse =
+ (): Codec<BankWithdrawalOperationPostResponse> =>
+ buildCodecForObject<BankWithdrawalOperationPostResponse>()
+ .property(
+ "status",
+ codecForEither(
+ codecForConstString("selected"),
+ codecForConstString("confirmed"),
+ codecForConstString("aborted"),
+ codecForConstString("pending"),
+ ),
+ )
+ .property("confirm_transfer_url", codecOptional(codecForString()))
+ .property("transfer_done", codecForBoolean())
+ .build("BankWithdrawalOperationPostResponse");
+
export type DenominationPubKey = RsaDenominationPubKey | CsDenominationPubKey;
export interface RsaDenominationPubKey {
@@ -1201,13 +1329,9 @@ export const codecForDenominationPubKey = () =>
.alternative(DenomKeyType.ClauseSchnorr, codecForCsDenominationPubKey())
.build("DenominationPubKey");
-export const codecForBankWithdrawalOperationPostResponse =
- (): Codec<BankWithdrawalOperationPostResponse> =>
- buildCodecForObject<BankWithdrawalOperationPostResponse>()
- .property("transfer_done", codecForBoolean())
- .build("BankWithdrawalOperationPostResponse");
-
-export type AmountString = string;
+declare const __amount_str: unique symbol;
+export type AmountString = string & { [__amount_str]: true };
+// export type AmountString = string;
export type Base32String = string;
export type EddsaSignatureString = string;
export type EddsaPublicKeyString = string;
@@ -1275,28 +1399,9 @@ export const codecForMerchantInfo = (): Codec<MerchantInfo> =>
.property("jurisdiction", codecOptional(codecForLocation()))
.build("MerchantInfo");
-export const codecForTax = (): Codec<Tax> =>
- buildCodecForObject<Tax>()
- .property("name", codecForString())
- .property("tax", codecForString())
- .build("Tax");
-
export const codecForInternationalizedString =
(): Codec<InternationalizedString> => codecForMap(codecForString());
-export const codecForProduct = (): Codec<Product> =>
- buildCodecForObject<Product>()
- .property("product_id", codecOptional(codecForString()))
- .property("description", codecForString())
- .property(
- "description_i18n",
- codecOptional(codecForInternationalizedString()),
- )
- .property("quantity", codecOptional(codecForNumber()))
- .property("unit", codecOptional(codecForString()))
- .property("price", codecOptional(codecForString()))
- .build("Tax");
-
export const codecForMerchantContractTerms = (): Codec<MerchantContractTerms> =>
buildCodecForObject<MerchantContractTerms>()
.property("order_id", codecForString())
@@ -1313,16 +1418,14 @@ export const codecForMerchantContractTerms = (): Codec<MerchantContractTerms> =>
.property("summary", codecForString())
.property("summary_i18n", codecOptional(codecForInternationalizedString()))
.property("nonce", codecForString())
- .property("amount", codecForString())
- .property("auditors", codecForList(codecForAuditorHandle()))
+ .property("amount", codecForAmountString())
.property("pay_deadline", codecForTimestamp)
.property("refund_deadline", codecForTimestamp)
.property("wire_transfer_deadline", codecForTimestamp)
.property("timestamp", codecForTimestamp)
.property("delivery_location", codecOptional(codecForLocation()))
.property("delivery_date", codecOptional(codecForTimestamp))
- .property("max_fee", codecForString())
- .property("max_wire_fee", codecOptional(codecForString()))
+ .property("max_fee", codecForAmountString())
.property("merchant", codecForMerchantInfo())
.property("merchant_pub", codecForString())
.property("exchanges", codecForList(codecForExchangeHandle()))
@@ -1334,7 +1437,7 @@ export const codecForMerchantContractTerms = (): Codec<MerchantContractTerms> =>
export const codecForPeerContractTerms = (): Codec<PeerContractTerms> =>
buildCodecForObject<PeerContractTerms>()
.property("summary", codecForString())
- .property("amount", codecForString())
+ .property("amount", codecForAmountString())
.property("purse_expiration", codecForTimestamp)
.build("PeerContractTerms");
@@ -1352,14 +1455,6 @@ export const codecForMerchantRefundPermission =
.property("exchange_pub", codecOptional(codecForString()))
.build("MerchantRefundPermission");
-export const codecForMerchantRefundResponse =
- (): Codec<MerchantRefundResponse> =>
- buildCodecForObject<MerchantRefundResponse>()
- .property("merchant_pub", codecForString())
- .property("h_contract_terms", codecForString())
- .property("refunds", codecForList(codecForMerchantRefundPermission()))
- .build("MerchantRefundResponse");
-
export const codecForBlindSigWrapperV2 = (): Codec<MerchantBlindSigWrapperV2> =>
buildCodecForObject<MerchantBlindSigWrapperV2>()
.property("blind_sig", codecForBlindedDenominationSignature())
@@ -1397,9 +1492,13 @@ export const codecForGlobalFees = (): Codec<GlobalFees> =>
.property("master_sig", codecForString())
.build("GlobalFees");
+// FIXME: Validate properly!
+export const codecForNgDenominations: Codec<DenomGroup> = codecForAny();
+
export const codecForExchangeKeysJson = (): Codec<ExchangeKeysJson> =>
buildCodecForObject<ExchangeKeysJson>()
- .property("denoms", codecForList(codecForDenomination()))
+ .property("base_url", codecForString())
+ .property("currency", codecForString())
.property("master_public_key", codecForString())
.property("auditors", codecForList(codecForAuditor()))
.property("list_issue_date", codecForTimestamp)
@@ -1408,6 +1507,9 @@ export const codecForExchangeKeysJson = (): Codec<ExchangeKeysJson> =>
.property("version", codecForString())
.property("reserve_closing_delay", codecForDuration)
.property("global_fees", codecForList(codecForGlobalFees()))
+ .property("accounts", codecForList(codecForExchangeWireAccount()))
+ .property("wire_fees", codecForMap(codecForList(codecForWireFeesJson())))
+ .property("denominations", codecForList(codecForNgDenominations))
.build("ExchangeKeysJson");
export const codecForWireFeesJson = (): Codec<WireFeesJson> =>
@@ -1419,18 +1521,6 @@ export const codecForWireFeesJson = (): Codec<WireFeesJson> =>
.property("end_date", codecForTimestamp)
.build("WireFeesJson");
-export const codecForAccountInfo = (): Codec<AccountInfo> =>
- buildCodecForObject<AccountInfo>()
- .property("payto_uri", codecForString())
- .property("master_sig", codecForString())
- .build("AccountInfo");
-
-export const codecForExchangeWireJson = (): Codec<ExchangeWireJson> =>
- buildCodecForObject<ExchangeWireJson>()
- .property("accounts", codecForList(codecForAccountInfo()))
- .property("fees", codecForMap(codecForList(codecForWireFeesJson())))
- .build("ExchangeWireJson");
-
export const codecForProposal = (): Codec<Proposal> =>
buildCodecForObject<Proposal>()
.property("contract_terms", codecForAny())
@@ -1450,6 +1540,15 @@ export const codecForCheckPaymentResponse = (): Codec<CheckPaymentResponse> =>
export const codecForWithdrawOperationStatusResponse =
(): Codec<WithdrawOperationStatusResponse> =>
buildCodecForObject<WithdrawOperationStatusResponse>()
+ .property(
+ "status",
+ codecForEither(
+ codecForConstString("selected"),
+ codecForConstString("confirmed"),
+ codecForConstString("aborted"),
+ codecForConstString("pending"),
+ ),
+ )
.property("selection_done", codecForBoolean())
.property("transfer_done", codecForBoolean())
.property("aborted", codecForBoolean())
@@ -1460,12 +1559,14 @@ export const codecForWithdrawOperationStatusResponse =
.property("wire_types", codecForList(codecForString()))
.build("WithdrawOperationStatusResponse");
-export const codecForTipPickupGetResponse = (): Codec<TipPickupGetResponse> =>
- buildCodecForObject<TipPickupGetResponse>()
- .property("tip_amount", codecForString())
- .property("exchange_url", codecForString())
- .property("expiration", codecForTimestamp)
- .build("TipPickupGetResponse");
+export const codecForRewardPickupGetResponse =
+ (): Codec<RewardPickupGetResponse> =>
+ buildCodecForObject<RewardPickupGetResponse>()
+ .property("reward_amount", codecForString())
+ .property("exchange_url", codecForString())
+ .property("next_url", codecOptional(codecForString()))
+ .property("expiration", codecForTimestamp)
+ .build("TipPickupGetResponse");
export const codecForRecoupConfirmation = (): Codec<RecoupConfirmation> =>
buildCodecForObject<RecoupConfirmation>()
@@ -1473,19 +1574,21 @@ export const codecForRecoupConfirmation = (): Codec<RecoupConfirmation> =>
.property("old_coin_pub", codecOptional(codecForString()))
.build("RecoupConfirmation");
-export const codecForWithdrawResponse = (): Codec<WithdrawResponse> =>
- buildCodecForObject<WithdrawResponse>()
+export const codecForWithdrawResponse = (): Codec<ExchangeWithdrawResponse> =>
+ buildCodecForObject<ExchangeWithdrawResponse>()
.property("ev_sig", codecForBlindedDenominationSignature())
.build("WithdrawResponse");
-export const codecForWithdrawBatchResponse = (): Codec<WithdrawBatchResponse> =>
- buildCodecForObject<WithdrawBatchResponse>()
- .property("ev_sigs", codecForList(codecForWithdrawResponse()))
- .build("WithdrawBatchResponse");
+export const codecForExchangeWithdrawBatchResponse =
+ (): Codec<ExchangeWithdrawBatchResponse> =>
+ buildCodecForObject<ExchangeWithdrawBatchResponse>()
+ .property("ev_sigs", codecForList(codecForWithdrawResponse()))
+ .build("WithdrawBatchResponse");
export const codecForMerchantPayResponse = (): Codec<MerchantPayResponse> =>
buildCodecForObject<MerchantPayResponse>()
.property("sig", codecForString())
+ .property("pos_confirmation", codecOptional(codecForString()))
.build("MerchantPayResponse");
export const codecForExchangeMeltResponse = (): Codec<ExchangeMeltResponse> =>
@@ -1507,57 +1610,15 @@ export const codecForExchangeRevealResponse =
.property("ev_sigs", codecForList(codecForExchangeRevealItem()))
.build("ExchangeRevealResponse");
-export const codecForMerchantCoinRefundSuccessStatus =
- (): Codec<MerchantCoinRefundSuccessStatus> =>
- buildCodecForObject<MerchantCoinRefundSuccessStatus>()
- .property("type", codecForConstString("success"))
- .property("coin_pub", codecForString())
- .property("exchange_status", codecForConstNumber(200))
- .property("exchange_sig", codecForString())
- .property("rtransaction_id", codecForNumber())
- .property("refund_amount", codecForString())
- .property("exchange_pub", codecForString())
- .property("execution_time", codecForTimestamp)
- .build("MerchantCoinRefundSuccessStatus");
-
-export const codecForMerchantCoinRefundFailureStatus =
- (): Codec<MerchantCoinRefundFailureStatus> =>
- buildCodecForObject<MerchantCoinRefundFailureStatus>()
- .property("type", codecForConstString("failure"))
- .property("coin_pub", codecForString())
- .property("exchange_status", codecForNumber())
- .property("rtransaction_id", codecForNumber())
- .property("refund_amount", codecForString())
- .property("exchange_code", codecOptional(codecForNumber()))
- .property("exchange_reply", codecOptional(codecForAny()))
- .property("execution_time", codecForTimestamp)
- .build("MerchantCoinRefundFailureStatus");
-
-export const codecForMerchantCoinRefundStatus =
- (): Codec<MerchantCoinRefundStatus> =>
- buildCodecForUnion<MerchantCoinRefundStatus>()
- .discriminateOn("type")
- .alternative("success", codecForMerchantCoinRefundSuccessStatus())
- .alternative("failure", codecForMerchantCoinRefundFailureStatus())
- .build("MerchantCoinRefundStatus");
-
export const codecForMerchantOrderStatusPaid =
(): Codec<MerchantOrderStatusPaid> =>
buildCodecForObject<MerchantOrderStatusPaid>()
- .property("refund_amount", codecForString())
- .property("refund_taken", codecForString())
+ .property("refund_amount", codecForAmountString())
+ .property("refund_taken", codecForAmountString())
.property("refund_pending", codecForBoolean())
.property("refunded", codecForBoolean())
.build("MerchantOrderStatusPaid");
-export const codecForMerchantOrderRefundPickupResponse =
- (): Codec<MerchantOrderRefundResponse> =>
- buildCodecForObject<MerchantOrderRefundResponse>()
- .property("merchant_pub", codecForString())
- .property("refund_amount", codecForString())
- .property("refunds", codecForList(codecForMerchantCoinRefundStatus()))
- .build("MerchantOrderRefundPickupResponse");
-
export const codecForMerchantOrderStatusUnpaid =
(): Codec<MerchantOrderStatusUnpaid> =>
buildCodecForObject<MerchantOrderStatusUnpaid>()
@@ -1595,37 +1656,6 @@ export interface AbortResponse {
refunds: MerchantAbortPayRefundStatus[];
}
-export const codecForMerchantAbortPayRefundSuccessStatus =
- (): Codec<MerchantAbortPayRefundSuccessStatus> =>
- buildCodecForObject<MerchantAbortPayRefundSuccessStatus>()
- .property("exchange_pub", codecForString())
- .property("exchange_sig", codecForString())
- .property("exchange_status", codecForConstNumber(200))
- .property("type", codecForConstString("success"))
- .build("MerchantAbortPayRefundSuccessStatus");
-
-export const codecForMerchantAbortPayRefundFailureStatus =
- (): Codec<MerchantAbortPayRefundFailureStatus> =>
- buildCodecForObject<MerchantAbortPayRefundFailureStatus>()
- .property("exchange_code", codecForNumber())
- .property("exchange_reply", codecForAny())
- .property("exchange_status", codecForNumber())
- .property("type", codecForConstString("failure"))
- .build("MerchantAbortPayRefundFailureStatus");
-
-export const codecForMerchantAbortPayRefundStatus =
- (): Codec<MerchantAbortPayRefundStatus> =>
- buildCodecForUnion<MerchantAbortPayRefundStatus>()
- .discriminateOn("type")
- .alternative("success", codecForMerchantAbortPayRefundSuccessStatus())
- .alternative("failure", codecForMerchantAbortPayRefundFailureStatus())
- .build("MerchantAbortPayRefundStatus");
-
-export const codecForAbortResponse = (): Codec<AbortResponse> =>
- buildCodecForObject<AbortResponse>()
- .property("refunds", codecForList(codecForMerchantAbortPayRefundStatus()))
- .build("AbortResponse");
-
export type MerchantAbortPayRefundStatus =
| MerchantAbortPayRefundSuccessStatus
| MerchantAbortPayRefundFailureStatus;
@@ -1667,19 +1697,6 @@ export interface MerchantAbortPayRefundSuccessStatus {
exchange_pub: string;
}
-export interface TalerConfigResponse {
- name: string;
- version: string;
- currency?: string;
-}
-
-export const codecForTalerConfigResponse = (): Codec<TalerConfigResponse> =>
- buildCodecForObject<TalerConfigResponse>()
- .property("name", codecForString())
- .property("version", codecForString())
- .property("currency", codecOptional(codecForString()))
- .build("TalerConfigResponse");
-
export interface FutureKeysResponse {
future_denoms: any[];
@@ -1750,6 +1767,10 @@ export interface ExchangeWithdrawRequest {
coin_ev: CoinEnvelope;
}
+export interface ExchangeBatchWithdrawRequest {
+ planchets: ExchangeWithdrawRequest[];
+}
+
export interface ExchangeRefreshRevealRequest {
new_denoms_h: HashCodeString[];
coin_evs: CoinEnvelope[];
@@ -1770,42 +1791,112 @@ export interface ExchangeRefreshRevealRequest {
old_age_commitment?: Edx25519PublicKeyEnc[];
}
-export interface DepositSuccess {
+interface DepositConfirmationSignature {
+ // The EdDSA signature of `TALER_DepositConfirmationPS` using a current
+ // `signing key of the exchange <sign-key-priv>` affirming the successful
+ // deposit and that the exchange will transfer the funds after the refund
+ // deadline, or as soon as possible if the refund deadline is zero.
+ exchange_sig: EddsaSignatureString;
+}
+
+export interface BatchDepositSuccess {
// Optional base URL of the exchange for looking up wire transfers
// associated with this transaction. If not given,
// the base URL is the same as the one used for this request.
- // Can be used if the base URL for /transactions/ differs from that
- // for /coins/, i.e. for load balancing. Clients SHOULD
- // respect the transaction_base_url if provided. Any HTTP server
+ // Can be used if the base URL for ``/transactions/`` differs from that
+ // for ``/coins/``, i.e. for load balancing. Clients SHOULD
+ // respect the ``transaction_base_url`` if provided. Any HTTP server
// belonging to an exchange MUST generate a 307 or 308 redirection
// to the correct base URL should a client uses the wrong base
// URL, or if the base URL has changed since the deposit.
transaction_base_url?: string;
- // timestamp when the deposit was received by the exchange.
+ // Timestamp when the deposit was received by the exchange.
exchange_timestamp: TalerProtocolTimestamp;
- // the EdDSA signature of TALER_DepositConfirmationPS using a current
- // signing key of the exchange affirming the successful
- // deposit and that the exchange will transfer the funds after the refund
- // deadline, or as soon as possible if the refund deadline is zero.
- exchange_sig: string;
-
- // public EdDSA key of the exchange that was used to
+ // `Public EdDSA key of the exchange <sign-key-pub>` that was used to
// generate the signature.
- // Should match one of the exchange's signing keys from /keys. It is given
+ // Should match one of the exchange's signing keys from ``/keys``. It is given
// explicitly as the client might otherwise be confused by clock skew as to
// which signing key was used.
- exchange_pub: string;
+ exchange_pub: EddsaPublicKeyString;
+
+ // Array of deposit confirmation signatures from the exchange
+ // Entries must be in the same order the coins were given
+ // in the batch deposit request.
+ exchange_sig: EddsaSignatureString;
}
-export const codecForDepositSuccess = (): Codec<DepositSuccess> =>
- buildCodecForObject<DepositSuccess>()
+export const codecForBatchDepositSuccess = (): Codec<BatchDepositSuccess> =>
+ buildCodecForObject<BatchDepositSuccess>()
.property("exchange_pub", codecForString())
.property("exchange_sig", codecForString())
.property("exchange_timestamp", codecForTimestamp)
.property("transaction_base_url", codecOptional(codecForString()))
- .build("DepositSuccess");
+ .build("BatchDepositSuccess");
+
+export interface TrackTransactionWired {
+ // Raw wire transfer identifier of the deposit.
+ wtid: Base32String;
+
+ // When was the wire transfer given to the bank.
+ execution_time: TalerProtocolTimestamp;
+
+ // The contribution of this coin to the total (without fees)
+ coin_contribution: AmountString;
+
+ // Binary-only Signature_ with purpose TALER_SIGNATURE_EXCHANGE_CONFIRM_WIRE
+ // over a TALER_ConfirmWirePS
+ // whereby the exchange affirms the successful wire transfer.
+ exchange_sig: EddsaSignatureString;
+
+ // Public EdDSA key of the exchange that was used to generate the signature.
+ // Should match one of the exchange's signing keys from /keys. Again given
+ // explicitly as the client might otherwise be confused by clock skew as to
+ // which signing key was used.
+ exchange_pub: EddsaPublicKeyString;
+}
+
+export const codecForTackTransactionWired = (): Codec<TrackTransactionWired> =>
+ buildCodecForObject<TrackTransactionWired>()
+ .property("wtid", codecForString())
+ .property("execution_time", codecForTimestamp)
+ .property("coin_contribution", codecForAmountString())
+ .property("exchange_sig", codecForString())
+ .property("exchange_pub", codecForString())
+ .build("TackTransactionWired");
+
+interface TrackTransactionAccepted {
+ // Legitimization target that the merchant should
+ // use to check for its KYC status using
+ // the /kyc-check/$REQUIREMENT_ROW/... endpoint.
+ // Optional, not present if the deposit has not
+ // yet been aggregated to the point that a KYC
+ // need has been evaluated.
+ requirement_row?: number;
+
+ // True if the KYC check for the merchant has been
+ // satisfied. False does not mean that KYC
+ // is strictly needed, unless also a
+ // legitimization_uuid is provided.
+ kyc_ok: boolean;
+
+ // Time by which the exchange currently thinks the deposit will be executed.
+ // Actual execution may be later if the KYC check is not satisfied by then.
+ execution_time: TalerProtocolTimestamp;
+}
+
+export const codecForTackTransactionAccepted =
+ (): Codec<TrackTransactionAccepted> =>
+ buildCodecForObject<TrackTransactionAccepted>()
+ .property("requirement_row", codecOptional(codecForNumber()))
+ .property("kyc_ok", codecForBoolean())
+ .property("execution_time", codecForTimestamp)
+ .build("TackTransactionAccepted");
+
+export type TrackTransaction =
+ | ({ type: "accepted" } & TrackTransactionAccepted)
+ | ({ type: "wired" } & TrackTransactionWired);
export interface PurseDeposit {
/**
@@ -1966,6 +2057,9 @@ export interface ExchangePurseDeposits {
deposits: PurseDeposit[];
}
+/**
+ * @deprecated batch deposit should be used.
+ */
export interface ExchangeDepositRequest {
// Amount to be deposited, can be a fraction of the
// coin's total value.
@@ -2027,3 +2121,296 @@ export interface ExchangeDepositRequest {
h_age_commitment?: string;
}
+
+export type WireSalt = string;
+
+export interface ExchangeBatchDepositRequest {
+ // The merchant's account details.
+ merchant_payto_uri: string;
+
+ // The salt is used to hide the ``payto_uri`` from customers
+ // when computing the ``h_wire`` of the merchant.
+ wire_salt: WireSalt;
+
+ // SHA-512 hash of the contract of the merchant with the customer. Further
+ // details are never disclosed to the exchange.
+ h_contract_terms: HashCodeString;
+
+ // The list of coins that are going to be deposited with this Request.
+ coins: BatchDepositRequestCoin[];
+
+ // Timestamp when the contract was finalized.
+ timestamp: TalerProtocolTimestamp;
+
+ // Indicative time by which the exchange undertakes to transfer the funds to
+ // the merchant, in case of successful payment. A wire transfer deadline of 'never'
+ // is not allowed.
+ wire_transfer_deadline: TalerProtocolTimestamp;
+
+ // EdDSA `public key of the merchant <merchant-pub>`, so that the client can identify the
+ // merchant for refund requests.
+ merchant_pub: EddsaPublicKeyString;
+
+ // Date until which the merchant can issue a refund to the customer via the
+ // exchange, to be omitted if refunds are not allowed.
+ //
+ // THIS FIELD WILL BE DEPRECATED, once the refund mechanism becomes a
+ // policy via extension.
+ refund_deadline?: TalerProtocolTimestamp;
+
+ // CAVEAT: THIS IS WORK IN PROGRESS
+ // (Optional) policy for the batch-deposit.
+ // This might be a refund, auction or escrow policy.
+ policy?: any;
+}
+
+export interface BatchDepositRequestCoin {
+ // EdDSA public key of the coin being deposited.
+ coin_pub: EddsaPublicKeyString;
+
+ // Hash of denomination RSA key with which the coin is signed.
+ denom_pub_hash: HashCodeString;
+
+ // Exchange's unblinded RSA signature of the coin.
+ ub_sig: UnblindedSignature;
+
+ // Amount to be deposited, can be a fraction of the
+ // coin's total value.
+ contribution: Amounts;
+
+ // Signature over `TALER_DepositRequestPS`, made by the customer with the
+ // `coin's private key <coin-priv>`.
+ coin_sig: EddsaSignatureString;
+
+ h_age_commitment?: string;
+}
+
+export interface WalletKycUuid {
+ // UUID that the wallet should use when initiating
+ // the KYC check.
+ requirement_row: number;
+
+ // Hash of the payto:// account URI for the wallet.
+ h_payto: string;
+}
+
+export const codecForWalletKycUuid = (): Codec<WalletKycUuid> =>
+ buildCodecForObject<WalletKycUuid>()
+ .property("requirement_row", codecForNumber())
+ .property("h_payto", codecForString())
+ .build("WalletKycUuid");
+
+export interface MerchantUsingTemplateDetails {
+ summary?: string;
+ amount?: AmountString;
+}
+
+export interface ExchangeRefundRequest {
+ // Amount to be refunded, can be a fraction of the
+ // coin's total deposit value (including deposit fee);
+ // must be larger than the refund fee.
+ refund_amount: AmountString;
+
+ // SHA-512 hash of the contact of the merchant with the customer.
+ h_contract_terms: HashCodeString;
+
+ // 64-bit transaction id of the refund transaction between merchant and customer.
+ rtransaction_id: number;
+
+ // EdDSA public key of the merchant.
+ merchant_pub: EddsaPublicKeyString;
+
+ // EdDSA signature of the merchant over a
+ // TALER_RefundRequestPS with purpose
+ // TALER_SIGNATURE_MERCHANT_REFUND
+ // affirming the refund.
+ merchant_sig: EddsaPublicKeyString;
+}
+
+export interface ExchangeRefundSuccessResponse {
+ // The EdDSA :ref:signature (binary-only) with purpose
+ // TALER_SIGNATURE_EXCHANGE_CONFIRM_REFUND over
+ // a TALER_RecoupRefreshConfirmationPS
+ // using a current signing key of the
+ // exchange affirming the successful refund.
+ exchange_sig: EddsaSignatureString;
+
+ // Public EdDSA key of the exchange that was used to generate the signature.
+ // Should match one of the exchange's signing keys from /keys. It is given
+ // explicitly as the client might otherwise be confused by clock skew as to
+ // which signing key was used.
+ exchange_pub: EddsaPublicKeyString;
+}
+
+export const codecForExchangeRefundSuccessResponse =
+ (): Codec<ExchangeRefundSuccessResponse> =>
+ buildCodecForObject<ExchangeRefundSuccessResponse>()
+ .property("exchange_pub", codecForString())
+ .property("exchange_sig", codecForString())
+ .build("ExchangeRefundSuccessResponse");
+
+export type AccountRestriction =
+ | RegexAccountRestriction
+ | DenyAllAccountRestriction;
+
+export interface DenyAllAccountRestriction {
+ type: "deny";
+}
+
+// Accounts interacting with this type of account
+// restriction must have a payto://-URI matching
+// the given regex.
+export interface RegexAccountRestriction {
+ type: "regex";
+
+ // Regular expression that the payto://-URI of the
+ // partner account must follow. The regular expression
+ // should follow posix-egrep, but without support for character
+ // classes, GNU extensions, back-references or intervals. See
+ // https://www.gnu.org/software/findutils/manual/html_node/find_html/posix_002degrep-regular-expression-syntax.html
+ // for a description of the posix-egrep syntax. Applications
+ // may support regexes with additional features, but exchanges
+ // must not use such regexes.
+ payto_regex: string;
+
+ // Hint for a human to understand the restriction
+ // (that is hopefully easier to comprehend than the regex itself).
+ human_hint: string;
+
+ // Map from IETF BCP 47 language tags to localized
+ // human hints.
+ human_hint_i18n?: InternationalizedString;
+}
+
+export interface ExchangeWireAccount {
+ // payto:// URI identifying the account and wire method
+ payto_uri: string;
+
+ // URI to convert amounts from or to the currency used by
+ // this wire account of the exchange. Missing if no
+ // conversion is applicable.
+ conversion_url?: string;
+
+ // Restrictions that apply to bank accounts that would send
+ // funds to the exchange (crediting this exchange bank account).
+ // Optional, empty array for unrestricted.
+ credit_restrictions: AccountRestriction[];
+
+ // Restrictions that apply to bank accounts that would receive
+ // funds from the exchange (debiting this exchange bank account).
+ // Optional, empty array for unrestricted.
+ debit_restrictions: AccountRestriction[];
+
+ // Signature using the exchange's offline key over
+ // a TALER_MasterWireDetailsPS
+ // with purpose TALER_SIGNATURE_MASTER_WIRE_DETAILS.
+ master_sig: EddsaSignatureString;
+
+ // Display label wallets should use to show this
+ // bank account.
+ // Since protocol **v19**.
+ bank_label?: string;
+ priority?: number;
+}
+
+export const codecForExchangeWireAccount = (): Codec<ExchangeWireAccount> =>
+ buildCodecForObject<ExchangeWireAccount>()
+ .property("conversion_url", codecOptional(codecForStringURL()))
+ .property("credit_restrictions", codecForList(codecForAny()))
+ .property("debit_restrictions", codecForList(codecForAny()))
+ .property("master_sig", codecForString())
+ .property("payto_uri", codecForString())
+ .property("bank_label", codecOptional(codecForString()))
+ .property("priority", codecOptional(codecForNumber()))
+ .build("WireAccount");
+
+export type Integer = number;
+
+export interface BankConversionInfoConfig {
+ // libtool-style representation of the Bank protocol version, see
+ // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning
+ // The format is "current:revision:age".
+ version: string;
+
+ // Name of the API.
+ name: "taler-conversion-info";
+
+ regional_currency: string;
+
+ fiat_currency: string;
+
+ // Currency used by this bank.
+ regional_currency_specification: CurrencySpecification;
+
+ // External currency used during conversion.
+ fiat_currency_specification: CurrencySpecification;
+}
+
+export const codecForBankConversionInfoConfig =
+ (): Codec<BankConversionInfoConfig> =>
+ buildCodecForObject<BankConversionInfoConfig>()
+ .property("name", codecForConstString("taler-conversion-info"))
+ .property("version", codecForString())
+ .property("fiat_currency", codecForString())
+ .property("regional_currency", codecForString())
+ .property("fiat_currency_specification", codecForCurrencySpecificiation())
+ .property(
+ "regional_currency_specification",
+ codecForCurrencySpecificiation(),
+ )
+ .build("BankConversionInfoConfig");
+
+export interface DenominationExpiredMessage {
+ // Taler error code. Note that beyond
+ // expiration this message format is also
+ // used if the key is not yet valid, or
+ // has been revoked.
+ code: number;
+
+ // Signature by the exchange over a
+ // TALER_DenominationExpiredAffirmationPS.
+ // Must have purpose TALER_SIGNATURE_EXCHANGE_AFFIRM_DENOM_EXPIRED.
+ exchange_sig: EddsaSignatureString;
+
+ // Public key of the exchange used to create
+ // the 'exchange_sig.
+ exchange_pub: EddsaPublicKeyString;
+
+ // Hash of the denomination public key that is unknown.
+ h_denom_pub: HashCodeString;
+
+ // When was the signature created.
+ timestamp: TalerProtocolTimestamp;
+
+ // What kind of operation was requested that now
+ // failed?
+ oper: string;
+}
+
+export const codecForDenominationExpiredMessage = () =>
+ buildCodecForObject<DenominationExpiredMessage>()
+ .property("code", codecForNumber())
+ .property("exchange_sig", codecForString())
+ .property("exchange_pub", codecForString())
+ .property("h_denom_pub", codecForString())
+ .property("timestamp", codecForTimestamp)
+ .property("oper", codecForString())
+ .build("DenominationExpiredMessage");
+
+export interface CoinHistoryResponse {
+ // Current balance of the coin.
+ balance: AmountString;
+
+ // Hash of the coin's denomination.
+ h_denom_pub: HashCodeString;
+
+ // Transaction history for the coin.
+ history: any[];
+}
+
+export const codecForCoinHistoryResponse = () =>
+ buildCodecForObject<CoinHistoryResponse>()
+ .property("balance", codecForAmountString())
+ .property("h_denom_pub", codecForString())
+ .property("history", codecForAny())
+ .build("CoinHistoryResponse");
diff --git a/packages/taler-util/src/talerconfig.ts b/packages/taler-util/src/talerconfig.ts
index 59c789cae..83c0044be 100644
--- a/packages/taler-util/src/talerconfig.ts
+++ b/packages/taler-util/src/talerconfig.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2020 Taler Systems S.A.
+ (C) 2020-2023 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -25,11 +25,14 @@
*/
import { AmountJson } from "./amounts.js";
import { Amounts } from "./amounts.js";
+import { Logger } from "./logging.js";
import nodejs_path from "path";
import nodejs_os from "os";
import nodejs_fs from "fs";
+const logger = new Logger("talerconfig.ts");
+
export class ConfigError extends Error {
constructor(message: string) {
super();
@@ -39,10 +42,30 @@ export class ConfigError extends Error {
}
}
+enum EntryOrigin {
+ /**
+ * From a default file.
+ */
+ DefaultFile = 1,
+ /**
+ * From a system/installation specific default value.
+ */
+ DefaultSystem = 2,
+ /**
+ * Loaded from file or string
+ */
+ Loaded = 3,
+ /**
+ * Changed after loading
+ */
+ Changed = 4,
+}
+
interface Entry {
value: string;
sourceLine: number;
sourceFile: string;
+ origin: EntryOrigin;
}
interface Section {
@@ -57,7 +80,7 @@ export class ConfigValue<T> {
constructor(
private sectionName: string,
private optionName: string,
- public value: string | undefined,
+ private value: string | undefined,
private converter: (x: string) => T,
) {}
@@ -89,6 +112,10 @@ export class ConfigValue<T> {
isDefined(): boolean {
return this.value !== undefined;
}
+
+ getValue(): string | undefined {
+ return this.value;
+ }
}
/**
@@ -116,9 +143,9 @@ export function expandPath(path: string): string {
export function pathsub(
x: string,
lookup: (s: string, depth: number) => string | undefined,
- depth = 0,
+ recursionDepth = 0,
): string {
- if (depth >= 10) {
+ if (recursionDepth >= 128) {
throw Error("recursion in path substitution");
}
let s = x;
@@ -157,14 +184,14 @@ export function pathsub(
defaultValue = undefined;
}
- const r = lookup(inner, depth + 1);
+ const r = lookup(varname, depth + 1);
if (r !== undefined) {
- s = s.substr(0, start) + r + s.substr(p + 1);
+ s = s.substring(0, start) + r + s.substring(p + 1);
l = start + r.length;
continue;
} else if (defaultValue !== undefined) {
const resolvedDefault = pathsub(defaultValue, lookup, depth + 1);
- s = s.substr(0, start) + resolvedDefault + s.substr(p + 1);
+ s = s.substring(0, start) + resolvedDefault + s.substring(p + 1);
l = start + resolvedDefault.length;
continue;
}
@@ -174,9 +201,9 @@ export function pathsub(
} else {
const m = /^[a-zA-Z-_][a-zA-Z0-9-_]*/.exec(s.substring(l + 1));
if (m && m[0]) {
- const r = lookup(m[0], depth + 1);
+ const r = lookup(m[0], recursionDepth + 1);
if (r !== undefined) {
- s = s.substr(0, l) + r + s.substr(l + 1 + m[0].length);
+ s = s.substring(0, l) + r + s.substring(l + 1 + m[0].length);
l = l + r.length;
continue;
}
@@ -195,6 +222,7 @@ export interface LoadOptions {
export interface StringifyOptions {
diagnostics?: boolean;
+ excludeDefaults?: boolean;
}
export interface LoadedFile {
@@ -282,7 +310,11 @@ export class Configuration {
private nestLevel = 0;
- private loadFromFilename(filename: string, opts: LoadOptions = {}): void {
+ private loadFromFilename(
+ filename: string,
+ isDefaultSource: boolean,
+ opts: LoadOptions = {},
+ ): void {
filename = expandPath(filename);
const checkCycle = () => {
@@ -309,7 +341,7 @@ export class Configuration {
const oldNestLevel = this.nestLevel;
this.nestLevel += 1;
try {
- this.loadFromString(s, {
+ this.internalLoadFromString(s, isDefaultSource, {
...opts,
filename: filename,
});
@@ -318,7 +350,11 @@ export class Configuration {
}
}
- private loadGlob(parentFilename: string, fileglob: string): void {
+ private loadGlob(
+ parentFilename: string,
+ isDefaultSource: boolean,
+ fileglob: string,
+ ): void {
const resolvedParent = nodejs_fs.realpathSync(parentFilename);
const parentDir = nodejs_path.dirname(resolvedParent);
@@ -339,12 +375,16 @@ export class Configuration {
for (const f of files) {
if (globMatch(tail, f)) {
const fullPath = nodejs_path.join(head, f);
- this.loadFromFilename(fullPath);
+ this.loadFromFilename(fullPath, isDefaultSource);
}
}
}
- private loadSecret(sectionName: string, filename: string): void {
+ private loadSecret(
+ sectionName: string,
+ filename: string,
+ isDefaultSource: boolean,
+ ): void {
const sec = this.provideSection(sectionName);
sec.secretFilename = filename;
const otherCfg = new Configuration();
@@ -354,7 +394,7 @@ export class Configuration {
sec.inaccessible = true;
return;
}
- otherCfg.loadFromFilename(filename, {
+ otherCfg.loadFromFilename(filename, isDefaultSource, {
banDirectives: true,
});
const otherSec = otherCfg.provideSection(sectionName);
@@ -363,7 +403,11 @@ export class Configuration {
}
}
- loadFromString(s: string, opts: LoadOptions = {}): void {
+ private internalLoadFromString(
+ s: string,
+ isDefaultSource: boolean,
+ opts: LoadOptions = {},
+ ): void {
let lineNo = 0;
const fn = opts.filename ?? "<input>";
const reComment = /^\s*#.*$/;
@@ -399,7 +443,10 @@ export class Configuration {
);
}
const arg = directiveMatch[2].trim();
- this.loadFromFilename(normalizeInlineFilename(opts.filename, arg));
+ this.loadFromFilename(
+ normalizeInlineFilename(opts.filename, arg),
+ isDefaultSource,
+ );
break;
}
case "inline-secret": {
@@ -419,7 +466,7 @@ export class Configuration {
opts.filename,
sp[1],
);
- this.loadSecret(sp[0], secretFilename);
+ this.loadSecret(sp[0], secretFilename, isDefaultSource);
break;
}
case "inline-matching": {
@@ -429,7 +476,7 @@ export class Configuration {
`invalid configuration, @inline-matching@ directive in ${fn}:${lineNo} can only be used from a file`,
);
}
- this.loadGlob(opts.filename, arg);
+ this.loadGlob(opts.filename, isDefaultSource, arg);
break;
}
default:
@@ -462,6 +509,9 @@ export class Configuration {
value: val,
sourceFile: opts.filename ?? "<unknown>",
sourceLine: lineNo,
+ origin: isDefaultSource
+ ? EntryOrigin.DefaultFile
+ : EntryOrigin.Loaded,
};
continue;
}
@@ -471,6 +521,10 @@ export class Configuration {
}
}
+ loadFromString(s: string, opts: LoadOptions = {}): void {
+ return this.internalLoadFromString(s, false, opts);
+ }
+
private provideSection(section: string): Section {
const secNorm = section.toUpperCase();
if (this.sectionMap[secNorm]) {
@@ -496,6 +550,24 @@ export class Configuration {
value,
sourceLine: 0,
sourceFile: "<unknown>",
+ origin: EntryOrigin.Changed,
+ };
+ }
+
+ /**
+ * Set a string value to a value from default locations.
+ */
+ private setStringSystemDefault(
+ section: string,
+ option: string,
+ value: string,
+ ): void {
+ const sec = this.provideSection(section);
+ sec.entries[option.toUpperCase()] = {
+ value,
+ sourceLine: 0,
+ sourceFile: "<unknown>",
+ origin: EntryOrigin.DefaultSystem,
};
}
@@ -563,11 +635,14 @@ export class Configuration {
if (val !== undefined) {
return pathsub(val, (v, d) => this.lookupVariable(v, d), depth);
}
+
// Environment variables can be case sensitive, respect that.
const envVal = process.env[x];
if (envVal !== undefined) {
return envVal;
}
+
+ logger.warn(`unable to resolve variable '${x}'`);
return;
}
@@ -578,30 +653,79 @@ export class Configuration {
);
}
- loadFrom(dirname: string): void {
+ loadDefaultsFromDir(dirname: string): void {
const files = nodejs_fs.readdirSync(dirname);
for (const f of files) {
const fn = nodejs_path.join(dirname, f);
- this.loadFromFilename(fn);
+ this.loadFromFilename(fn, true);
}
}
private loadDefaults(): void {
- let bc = process.env["TALER_BASE_CONFIG"];
- if (!bc) {
+ let baseConfigDir = process.env["TALER_BASE_CONFIG"];
+ if (!baseConfigDir) {
/* Try to locate the configuration based on the location
* of the taler-config binary. */
const path = which("taler-config");
if (path) {
- bc = nodejs_fs.realpathSync(
+ baseConfigDir = nodejs_fs.realpathSync(
nodejs_path.dirname(path) + "/../share/taler/config.d",
);
}
}
- if (!bc) {
- bc = "/usr/share/taler/config.d";
+ if (!baseConfigDir) {
+ baseConfigDir = "/usr/share/taler/config.d";
+ }
+
+ let installPrefix = process.env["TALER_PREFIX"];
+ if (!installPrefix) {
+ /* Try to locate install path based on the location
+ * of the taler-config binary. */
+ const path = which("taler-config");
+ if (path) {
+ installPrefix = nodejs_fs.realpathSync(
+ nodejs_path.dirname(path) + "/..",
+ );
+ }
+ }
+ if (!installPrefix) {
+ installPrefix = "/usr";
}
- this.loadFrom(bc);
+
+ this.setStringSystemDefault(
+ "PATHS",
+ "LIBEXECDIR",
+ `${installPrefix}/taler/libexec/`,
+ );
+ this.setStringSystemDefault(
+ "PATHS",
+ "DOCDIR",
+ `${installPrefix}/share/doc/taler/`,
+ );
+ this.setStringSystemDefault(
+ "PATHS",
+ "ICONDIR",
+ `${installPrefix}/share/icons/`,
+ );
+ this.setStringSystemDefault(
+ "PATHS",
+ "LOCALEDIR",
+ `${installPrefix}/share/locale/`,
+ );
+ this.setStringSystemDefault("PATHS", "PREFIX", `${installPrefix}/`);
+ this.setStringSystemDefault("PATHS", "BINDIR", `${installPrefix}/bin`);
+ this.setStringSystemDefault(
+ "PATHS",
+ "LIBDIR",
+ `${installPrefix}/lib/taler/`,
+ );
+ this.setStringSystemDefault(
+ "PATHS",
+ "DATADIR",
+ `${installPrefix}/share/taler/`,
+ );
+
+ this.loadDefaultsFromDir(baseConfigDir);
}
getDefaultConfigFilename(): string | undefined {
@@ -631,11 +755,13 @@ export class Configuration {
const cfg = new Configuration();
cfg.loadDefaults();
if (filename) {
- cfg.loadFromFilename(filename);
+ cfg.loadFromFilename(filename, false);
} else {
const fn = cfg.getDefaultConfigFilename();
if (fn) {
- cfg.loadFromFilename(fn);
+ // It's the default filename for the main config file,
+ // but we don't consider the values default values.
+ cfg.loadFromFilename(fn, false);
}
}
cfg.hintEntrypoint = filename;
@@ -657,26 +783,51 @@ export class Configuration {
}
for (const sectionName of Object.keys(this.sectionMap)) {
const sec = this.sectionMap[sectionName];
- if (opts.diagnostics && sec.secretFilename) {
- s += `# Secret section from ${sec.secretFilename}\n`;
- s += `# Secret accessible: ${!sec.inaccessible}\n`;
- }
- s += `[${sectionName}]\n`;
+ let headerWritten = false;
for (const optionName of Object.keys(sec.entries)) {
const entry = this.sectionMap[sectionName].entries[optionName];
+ if (
+ opts.excludeDefaults &&
+ (entry.origin === EntryOrigin.DefaultSystem ||
+ entry.origin === EntryOrigin.DefaultFile)
+ ) {
+ continue;
+ }
+ if (!headerWritten) {
+ if (opts.diagnostics && sec.secretFilename) {
+ s += `# Secret section from ${sec.secretFilename}\n`;
+ s += `# Secret accessible: ${!sec.inaccessible}\n`;
+ }
+ s += `[${sectionName}]\n`;
+ headerWritten = true;
+ }
if (entry !== undefined) {
if (opts.diagnostics) {
- s += `# ${entry.sourceFile}:${entry.sourceLine}\n`;
+ switch (entry.origin) {
+ case EntryOrigin.DefaultFile:
+ case EntryOrigin.Changed:
+ case EntryOrigin.Loaded:
+ s += `# ${entry.sourceFile}:${entry.sourceLine}\n`;
+ break;
+ case EntryOrigin.DefaultSystem:
+ s += `# (system/installation default)\n`;
+ break;
+ }
}
s += `${optionName} = ${entry.value}\n`;
}
}
- s += "\n";
+ if (headerWritten) {
+ s += "\n";
+ }
}
return s;
}
- write(filename: string): void {
- nodejs_fs.writeFileSync(filename, this.stringify());
+ write(filename: string, opts: { excludeDefaults?: boolean } = {}): void {
+ nodejs_fs.writeFileSync(
+ filename,
+ this.stringify({ excludeDefaults: opts.excludeDefaults }),
+ );
}
}
diff --git a/packages/taler-util/src/taleruri.test.ts b/packages/taler-util/src/taleruri.test.ts
index 3ee243fb3..7f10d21fd 100644
--- a/packages/taler-util/src/taleruri.test.ts
+++ b/packages/taler-util/src/taleruri.test.ts
@@ -15,25 +15,67 @@
*/
import test from "ava";
+import { AmountString } from "./taler-types.js";
import {
+ parseAddExchangeUri,
+ parseDevExperimentUri,
+ parsePayPullUri,
+ parsePayPushUri,
+ parsePayTemplateUri,
parsePayUri,
- parseWithdrawUri,
parseRefundUri,
- parseTipUri,
- parsePayPushUri,
- constructPayPushUri,
+ parseRestoreUri,
+ parseWithdrawExchangeUri,
+ parseWithdrawUri,
+ stringifyAddExchange,
+ stringifyDevExperimentUri,
+ stringifyPayPullUri,
+ stringifyPayPushUri,
+ stringifyPayTemplateUri,
+ stringifyPayUri,
+ stringifyRefundUri,
+ stringifyRestoreUri,
+ stringifyWithdrawExchange,
+ stringifyWithdrawUri,
} from "./taleruri.js";
-test("taler pay url parsing: wrong scheme", (t) => {
- const url1 = "talerfoo://";
- const r1 = parsePayUri(url1);
- t.is(r1, undefined);
+/**
+ * 5.1 action: withdraw https://lsd.gnunet.org/lsd0006/#name-action-withdraw
+ */
- const url2 = "taler://refund/a/b/c/d/e/f";
- const r2 = parsePayUri(url2);
- t.is(r2, undefined);
+test("taler withdraw uri parsing", (t) => {
+ const url1 = "taler://withdraw/bank.example.com/12345";
+ const r1 = parseWithdrawUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ 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);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.is(r1.withdrawalOperationId, "12345");
+ t.is(r1.bankIntegrationApiBaseUrl, "http://bank.example.com/");
+});
+
+test("taler withdraw URI (stringify)", (t) => {
+ const url = stringifyWithdrawUri({
+ bankIntegrationApiBaseUrl: "https://bank.taler.test/integration-api/",
+ withdrawalOperationId: "123",
+ });
+ t.deepEqual(url, "taler://withdraw/bank.taler.test/integration-api/123");
+});
+
+/**
+ * 5.2 action: pay https://lsd.gnunet.org/lsd0006/#name-action-pay
+ */
test("taler pay url parsing: defaults", (t) => {
const url1 = "taler://pay/example.com/myorder/";
const r1 = parsePayUri(url1);
@@ -77,17 +119,6 @@ test("taler pay url parsing (claim token)", (t) => {
t.is(r1.claimToken, "ASDF");
});
-test("taler refund uri parsing: non-https #1", (t) => {
- const url1 = "taler+http://refund/example.com/myorder/";
- const r1 = parseRefundUri(url1);
- if (!r1) {
- t.fail();
- return;
- }
- t.is(r1.merchantBaseUrl, "http://example.com/");
- t.is(r1.orderId, "myorder");
-});
-
test("taler pay uri parsing: non-https", (t) => {
const url1 = "taler+http://pay/example.com/myorder/";
const r1 = parsePayUri(url1);
@@ -109,26 +140,52 @@ test("taler pay uri parsing: missing session component", (t) => {
t.pass();
});
-test("taler withdraw uri parsing", (t) => {
- const url1 = "taler://withdraw/bank.example.com/12345";
- const r1 = parseWithdrawUri(url1);
- if (!r1) {
- t.fail();
- return;
- }
- t.is(r1.withdrawalOperationId, "12345");
- t.is(r1.bankIntegrationApiBaseUrl, "https://bank.example.com/");
+test("taler pay URI (stringify)", (t) => {
+ const url1 = stringifyPayUri({
+ merchantBaseUrl: "http://localhost:123/",
+ orderId: "foo",
+ sessionId: "",
+ });
+ t.deepEqual(url1, "taler+http://pay/localhost:123/foo/");
+
+ const url2 = stringifyPayUri({
+ merchantBaseUrl: "http://localhost:123/",
+ orderId: "foo",
+ sessionId: "bla",
+ });
+ t.deepEqual(url2, "taler+http://pay/localhost:123/foo/bla");
});
-test("taler withdraw uri parsing (http)", (t) => {
- const url1 = "taler+http://withdraw/bank.example.com/12345";
- const r1 = parseWithdrawUri(url1);
+test("taler pay URI (stringify with https)", (t) => {
+ const url1 = stringifyPayUri({
+ merchantBaseUrl: "https://localhost:123/",
+ orderId: "foo",
+ sessionId: "",
+ });
+ t.deepEqual(url1, "taler://pay/localhost:123/foo/");
+
+ const url2 = stringifyPayUri({
+ merchantBaseUrl: "https://localhost/",
+ orderId: "foo",
+ sessionId: "bla",
+ noncePriv: "123",
+ });
+ t.deepEqual(url2, "taler://pay/localhost/foo/bla?n=123");
+});
+
+/**
+ * 5.3 action: refund https://lsd.gnunet.org/lsd0006/#name-action-refund
+ */
+
+test("taler refund uri parsing: non-https #1", (t) => {
+ const url1 = "taler+http://refund/example.com/myorder/";
+ const r1 = parseRefundUri(url1);
if (!r1) {
t.fail();
return;
}
- t.is(r1.withdrawalOperationId, "12345");
- t.is(r1.bankIntegrationApiBaseUrl, "http://bank.example.com/");
+ t.is(r1.merchantBaseUrl, "http://example.com/");
+ t.is(r1.orderId, "myorder");
});
test("taler refund uri parsing", (t) => {
@@ -153,41 +210,66 @@ test("taler refund uri parsing with instance", (t) => {
t.is(r1.merchantBaseUrl, "https://merchant.example.com/instances/myinst/");
});
-test("taler tip pickup uri", (t) => {
- const url1 = "taler://tip/merchant.example.com/tipid";
- const r1 = parseTipUri(url1);
+test("taler refund URI (stringify)", (t) => {
+ const url = stringifyRefundUri({
+ merchantBaseUrl: "https://merchant.test/instance/pepe/",
+ orderId: "123",
+ });
+ t.deepEqual(url, "taler://refund/merchant.test/instance/pepe/123/");
+});
+
+/**
+ * 5.5 action: pay-push https://lsd.gnunet.org/lsd0006/#name-action-pay-push
+ */
+
+test("taler peer to peer push URI", (t) => {
+ const url1 = "taler://pay-push/exch.example.com/foo";
+ const r1 = parsePayPushUri(url1);
if (!r1) {
t.fail();
return;
}
- t.is(r1.merchantBaseUrl, "https://merchant.example.com/");
+ t.is(r1.exchangeBaseUrl, "https://exch.example.com/");
+ t.is(r1.contractPriv, "foo");
});
-test("taler tip pickup uri with instance", (t) => {
- const url1 = "taler://tip/merchant.example.com/instances/tipm/tipid";
- const r1 = parseTipUri(url1);
+test("taler peer to peer push URI (path)", (t) => {
+ const url1 = "taler://pay-push/exch.example.com:123/bla/foo";
+ const r1 = parsePayPushUri(url1);
if (!r1) {
t.fail();
return;
}
- t.is(r1.merchantBaseUrl, "https://merchant.example.com/instances/tipm/");
- t.is(r1.merchantTipId, "tipid");
+ t.is(r1.exchangeBaseUrl, "https://exch.example.com:123/bla/");
+ t.is(r1.contractPriv, "foo");
});
-test("taler tip pickup uri with instance and prefix", (t) => {
- const url1 = "taler://tip/merchant.example.com/my/pfx/tipm/tipid";
- const r1 = parseTipUri(url1);
+test("taler peer to peer push URI (http)", (t) => {
+ const url1 = "taler+http://pay-push/exch.example.com:123/bla/foo";
+ const r1 = parsePayPushUri(url1);
if (!r1) {
t.fail();
return;
}
- t.is(r1.merchantBaseUrl, "https://merchant.example.com/my/pfx/tipm/");
- t.is(r1.merchantTipId, "tipid");
+ t.is(r1.exchangeBaseUrl, "http://exch.example.com:123/bla/");
+ t.is(r1.contractPriv, "foo");
});
-test("taler peer to peer push URI", (t) => {
- const url1 = "taler://pay-push/exch.example.com/foo";
- const r1 = parsePayPushUri(url1);
+test("taler peer to peer push URI (stringify)", (t) => {
+ const url = stringifyPayPushUri({
+ exchangeBaseUrl: "https://foo.example.com/bla/",
+ contractPriv: "123",
+ });
+ t.deepEqual(url, "taler://pay-push/foo.example.com/bla/123");
+});
+
+/**
+ * 5.6 action: pay-pull https://lsd.gnunet.org/lsd0006/#name-action-pay-pull
+ */
+
+test("taler peer to peer pull URI", (t) => {
+ const url1 = "taler://pay-pull/exch.example.com/foo";
+ const r1 = parsePayPullUri(url1);
if (!r1) {
t.fail();
return;
@@ -196,9 +278,9 @@ test("taler peer to peer push URI", (t) => {
t.is(r1.contractPriv, "foo");
});
-test("taler peer to peer push URI (path)", (t) => {
- const url1 = "taler://pay-push/exch.example.com:123/bla/foo";
- const r1 = parsePayPushUri(url1);
+test("taler peer to peer pull URI (path)", (t) => {
+ const url1 = "taler://pay-pull/exch.example.com:123/bla/foo";
+ const r1 = parsePayPullUri(url1);
if (!r1) {
t.fail();
return;
@@ -207,9 +289,9 @@ test("taler peer to peer push URI (path)", (t) => {
t.is(r1.contractPriv, "foo");
});
-test("taler peer to peer push URI (http)", (t) => {
- const url1 = "taler+http://pay-push/exch.example.com:123/bla/foo";
- const r1 = parsePayPushUri(url1);
+test("taler peer to peer pull URI (http)", (t) => {
+ const url1 = "taler+http://pay-pull/exch.example.com:123/bla/foo";
+ const r1 = parsePayPullUri(url1);
if (!r1) {
t.fail();
return;
@@ -218,10 +300,268 @@ test("taler peer to peer push URI (http)", (t) => {
t.is(r1.contractPriv, "foo");
});
-test("taler peer to peer push URI (construction)", (t) => {
- const url = constructPayPushUri({
+test("taler peer to peer pull URI (stringify)", (t) => {
+ const url = stringifyPayPullUri({
exchangeBaseUrl: "https://foo.example.com/bla/",
contractPriv: "123",
});
- t.deepEqual(url, "taler://pay-push/foo.example.com/bla/123");
+ t.deepEqual(url, "taler://pay-pull/foo.example.com/bla/123");
+});
+
+/**
+ * 5.7 action: pay-template https://lsd.gnunet.org/lsd0006/#name-action-pay-template
+ */
+
+test("taler pay template URI (parsing)", (t) => {
+ const url1 =
+ "taler://pay-template/merchant.example.com/FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY?amount=KUDOS:5";
+ const r1 = parsePayTemplateUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.deepEqual(r1.merchantBaseUrl, "https://merchant.example.com/");
+ t.deepEqual(r1.templateId, "FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY");
+ t.deepEqual(r1.templateParams.amount, "KUDOS:5");
+});
+
+test("taler pay template URI (parsing, http with port)", (t) => {
+ const url1 =
+ "taler+http://pay-template/merchant.example.com:1234/FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY?amount=KUDOS:5";
+ const r1 = parsePayTemplateUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.deepEqual(r1.merchantBaseUrl, "http://merchant.example.com:1234/");
+ t.deepEqual(r1.templateId, "FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY");
+ t.deepEqual(r1.templateParams.amount, "KUDOS:5");
+});
+
+test("taler pay template URI (stringify)", (t) => {
+ const url1 = stringifyPayTemplateUri({
+ merchantBaseUrl: "http://merchant.example.com:1234/",
+ templateId: "FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY",
+ templateParams: {
+ amount: "KUDOS:5",
+ },
+ });
+ t.deepEqual(
+ url1,
+ "taler+http://pay-template/merchant.example.com:1234/FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY?amount=KUDOS%3A5",
+ );
+});
+
+/**
+ * 5.10 action: restore https://lsd.gnunet.org/lsd0006/#name-action-restore
+ */
+test("taler restore URI (parsing, http with port)", (t) => {
+ const r1 = parseRestoreUri(
+ "taler+http://restore/GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0/prov1.example.com,prov2.example.com:123",
+ );
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.deepEqual(
+ r1.walletRootPriv,
+ "GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0",
+ );
+ t.deepEqual(r1.providers[0], "http://prov1.example.com/");
+ t.deepEqual(r1.providers[1], "http://prov2.example.com:123/");
+});
+test("taler restore URI (parsing, https with port)", (t) => {
+ const r1 = parseRestoreUri(
+ "taler://restore/GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0/prov1.example.com,prov2.example.com:234,https%3A%2F%2Fprov1.example.com%2F",
+ );
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.deepEqual(
+ r1.walletRootPriv,
+ "GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0",
+ );
+ t.deepEqual(r1.providers[0], "https://prov1.example.com/");
+ t.deepEqual(r1.providers[1], "https://prov2.example.com:234/");
+});
+
+test("taler restore URI (stringify)", (t) => {
+ const url = stringifyRestoreUri({
+ walletRootPriv: "GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0",
+ providers: ["http://prov1.example.com", "https://prov2.example.com:234/"],
+ });
+ t.deepEqual(
+ url,
+ "taler://restore/GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0/http%3A%2F%2Fprov1.example.com%2F,https%3A%2F%2Fprov2.example.com%3A234%2F",
+ );
+});
+
+/**
+ * 5.11 action: dev-experiment https://lsd.gnunet.org/lsd0006/#name-action-dev-experiment
+ */
+
+test("taler dev exp URI (parsing)", (t) => {
+ const url1 = "taler://dev-experiment/123";
+ const r1 = parseDevExperimentUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.deepEqual(r1.devExperimentId, "123");
+});
+
+test("taler dev exp URI (stringify)", (t) => {
+ const url1 = stringifyDevExperimentUri({
+ devExperimentId: "123",
+ });
+ t.deepEqual(url1, "taler://dev-experiment/123");
+});
+
+/**
+ * 5.12 action: withdraw-exchange https://lsd.gnunet.org/lsd0006/#name-action-withdraw-exchange
+ */
+
+test("taler withdraw exchange URI (parse)", (t) => {
+ {
+ const r1 = parseWithdrawExchangeUri(
+ "taler://withdraw-exchange/exchange.demo.taler.net/someroot/GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0?a=KUDOS%3A2",
+ );
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.deepEqual(
+ r1.exchangePub,
+ "GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0",
+ );
+ t.deepEqual(
+ r1.exchangeBaseUrl,
+ "https://exchange.demo.taler.net/someroot/",
+ );
+ t.deepEqual(r1.amount, "KUDOS:2");
+ }
+ {
+ const r2 = parseWithdrawExchangeUri(
+ "taler://withdraw-exchange/exchange.demo.taler.net/someroot/",
+ );
+ if (!r2) {
+ t.fail();
+ return;
+ }
+ t.deepEqual(r2.exchangePub, undefined);
+ t.deepEqual(r2.amount, undefined);
+ t.deepEqual(
+ r2.exchangeBaseUrl,
+ "https://exchange.demo.taler.net/someroot/",
+ );
+ }
+
+ {
+ const r3 = parseWithdrawExchangeUri(
+ "taler://withdraw-exchange/exchange.demo.taler.net/",
+ );
+ if (!r3) {
+ t.fail();
+ return;
+ }
+ t.deepEqual(r3.exchangePub, undefined);
+ t.deepEqual(r3.amount, undefined);
+ t.deepEqual(r3.exchangeBaseUrl, "https://exchange.demo.taler.net/");
+ }
+
+ {
+ // No trailing slash, no path component
+ const r4 = parseWithdrawExchangeUri(
+ "taler://withdraw-exchange/exchange.demo.taler.net",
+ );
+ if (!r4) {
+ t.fail();
+ return;
+ }
+ t.deepEqual(r4.exchangePub, undefined);
+ t.deepEqual(r4.amount, undefined);
+ t.deepEqual(r4.exchangeBaseUrl, "https://exchange.demo.taler.net/");
+ }
+});
+
+test("taler withdraw exchange URI (stringify)", (t) => {
+ const url = stringifyWithdrawExchange({
+ exchangeBaseUrl: "https://exchange.demo.taler.net",
+ exchangePub: "GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0",
+ });
+ t.deepEqual(
+ url,
+ "taler://withdraw-exchange/exchange.demo.taler.net/GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0",
+ );
+});
+
+test("taler withdraw exchange URI with amount (stringify)", (t) => {
+ const url = stringifyWithdrawExchange({
+ exchangeBaseUrl: "https://exchange.demo.taler.net",
+ exchangePub: "GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0",
+ amount: "KUDOS:19" as AmountString,
+ });
+ t.deepEqual(
+ url,
+ "taler://withdraw-exchange/exchange.demo.taler.net/GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0?a=KUDOS%3A19",
+ );
+});
+
+
+/**
+ * 5.13 action: add-exchange https://lsd.gnunet.org/lsd0006/#name-action-add-exchange
+ */
+
+test("taler add exchange URI (parse)", (t) => {
+ {
+ const r1 = parseAddExchangeUri(
+ "taler://add-exchange/exchange.example.com/",
+ );
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.deepEqual(
+ r1.exchangeBaseUrl,
+ "https://exchange.example.com/",
+ );
+ }
+ {
+ const r2 = parseAddExchangeUri(
+ "taler://add-exchange/exchanges.example.com/api/",
+ );
+ if (!r2) {
+ t.fail();
+ return;
+ }
+ t.deepEqual(
+ r2.exchangeBaseUrl,
+ "https://exchanges.example.com/api/",
+ );
+ }
+
+});
+
+test("taler add exchange URI (stringify)", (t) => {
+ const url = stringifyAddExchange({
+ exchangeBaseUrl: "https://exchange.demo.taler.net",
+ });
+ t.deepEqual(
+ url,
+ "taler://add-exchange/exchange.demo.taler.net/",
+ );
+});
+
+/**
+ * wrong uris
+ */
+test("taler pay url parsing: wrong scheme", (t) => {
+ const url1 = "talerfoo://";
+ const r1 = parsePayUri(url1);
+ t.is(r1, undefined);
+
+ const url2 = "taler://refund/a/b/c/d/e/f";
+ const r2 = parsePayUri(url2);
+ t.is(r2, undefined);
});
diff --git a/packages/taler-util/src/taleruri.ts b/packages/taler-util/src/taleruri.ts
index 4e47acbce..b4f9db6ef 100644
--- a/packages/taler-util/src/taleruri.ts
+++ b/packages/taler-util/src/taleruri.ts
@@ -14,61 +14,140 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { BackupRecovery } from "./backup-types.js";
+/**
+ * @fileoverview
+ * Construction and parsing of taler:// URIs.
+ * Specification: https://lsd.gnunet.org/lsd0006/
+ */
+
+/**
+ * Imports.
+ */
+import { Codec, Context, DecodingError, renderContext } from "./codec.js";
import { canonicalizeBaseUrl } from "./helpers.js";
-import { initNodePrng } from "./prng-node.js";
-import { URLSearchParams, URL } from "./url.js";
+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.
+ */
+export type TalerUri =
+ | PayUriResult
+ | PayTemplateUriResult
+ | DevExperimentUri
+ | PayPullUriResult
+ | PayPushUriResult
+ | BackupRestoreUri
+ | RefundUriResult
+ | WithdrawUriResult
+ | WithdrawExchangeUri
+ | AddExchangeUri;
+
+declare const __action_str: unique symbol;
+export type TalerUriString = string & { [__action_str]: true };
+
+export function codecForTalerUriString(): Codec<TalerUriString> {
+ return {
+ decode(x: any, c?: Context): TalerUriString {
+ if (typeof x !== "string") {
+ throw new DecodingError(
+ `expected string at ${renderContext(c)} but got ${typeof x}`,
+ );
+ }
+ if (parseTalerUri(x) === undefined) {
+ throw new DecodingError(
+ `invalid taler URI at ${renderContext(c)} but got "${x}"`,
+ );
+ }
+ return x as TalerUriString;
+ },
+ };
+}
export interface PayUriResult {
+ type: TalerUriAction.Pay;
merchantBaseUrl: string;
orderId: string;
sessionId: string;
- claimToken: string | undefined;
- noncePriv: string | undefined;
+ claimToken?: string;
+ noncePriv?: string;
+}
+
+export type TemplateParams = {
+ amount?: string;
+ summary?: string;
+};
+
+export interface PayTemplateUriResult {
+ type: TalerUriAction.PayTemplate;
+ merchantBaseUrl: string;
+ templateId: string;
+ templateParams: TemplateParams;
}
export interface WithdrawUriResult {
+ type: TalerUriAction.Withdraw;
bankIntegrationApiBaseUrl: string;
withdrawalOperationId: string;
}
export interface RefundUriResult {
+ type: TalerUriAction.Refund;
merchantBaseUrl: string;
orderId: string;
}
-export interface TipUriResult {
- merchantTipId: string;
- merchantBaseUrl: string;
-}
-
export interface PayPushUriResult {
+ type: TalerUriAction.PayPush;
exchangeBaseUrl: string;
contractPriv: string;
}
export interface PayPullUriResult {
+ type: TalerUriAction.PayPull;
exchangeBaseUrl: string;
contractPriv: string;
}
export interface DevExperimentUri {
+ type: TalerUriAction.DevExperiment;
devExperimentId: string;
}
+export interface BackupRestoreUri {
+ type: TalerUriAction.Restore;
+ walletRootPriv: string;
+ providers: Array<string>;
+}
+
+export interface WithdrawExchangeUri {
+ type: TalerUriAction.WithdrawExchange;
+ exchangeBaseUrl: string;
+ exchangePub?: string;
+ amount?: AmountString;
+}
+
+export interface AddExchangeUri {
+ type: TalerUriAction.AddExchange;
+ exchangeBaseUrl: string;
+}
+
/**
* Parse a taler[+http]://withdraw URI.
* Return undefined if not passed a valid URI.
*/
-export function parseWithdrawUri(s: string): WithdrawUriResult | undefined {
- const pi = parseProtoInfo(s, "withdraw");
- if (!pi) {
- return undefined;
+export function parseWithdrawUriWithError(s: string) {
+ const pi = parseProtoInfoWithError(s, "withdraw");
+ if (pi.type === "fail") {
+ return pi;
}
- const parts = pi.rest.split("/");
+ const parts = pi.body.rest.split("/");
if (parts.length < 2) {
- return undefined;
+ return opKnownTalerFailure(TalerErrorCode.WALLET_TALER_URI_MALFORMED, {
+ code: TalerErrorCode.WALLET_TALER_URI_MALFORMED,
+ });
}
const host = parts[0].toLowerCase();
@@ -83,14 +162,80 @@ export function parseWithdrawUri(s: string): WithdrawUriResult | undefined {
const withdrawId = parts[parts.length - 1];
const p = [host, ...pathSegments].join("/");
- return {
- bankIntegrationApiBaseUrl: canonicalizeBaseUrl(`${pi.innerProto}://${p}/`),
+ const result: WithdrawUriResult = {
+ type: TalerUriAction.Withdraw,
+ bankIntegrationApiBaseUrl: canonicalizeBaseUrl(
+ `${pi.body.innerProto}://${p}/`,
+ ),
withdrawalOperationId: withdrawId,
};
+ return opFixedSuccess(result);
+}
+
+/**
+ *
+ * @deprecated use parseWithdrawUriWithError
+ */
+export function parseWithdrawUri(s: string): WithdrawUriResult | undefined {
+ const r = parseWithdrawUriWithError(s);
+ if (r.type === "fail") return undefined;
+ return r.body;
+}
+
+/**
+ * Parse a taler[+http]://withdraw URI.
+ * Return undefined if not passed a valid URI.
+ */
+export function parseAddExchangeUriWithError(s: string) {
+ const pi = parseProtoInfoWithError(s, "add-exchange");
+ if (pi.type === "fail") {
+ return pi;
+ }
+ const parts = pi.body.rest.split("/");
+
+ if (parts.length < 2) {
+ return opKnownTalerFailure(TalerErrorCode.WALLET_TALER_URI_MALFORMED, {
+ code: TalerErrorCode.WALLET_TALER_URI_MALFORMED,
+ });
+ }
+
+ const host = parts[0].toLowerCase();
+ const pathSegments = parts.slice(1, parts.length - 1);
+ /**
+ * The statement below does not tolerate a slash-ended URI.
+ * This results in (1) the withdrawalId being passed as the
+ * empty string, and (2) the bankIntegrationApi ending with the
+ * actual withdrawal operation ID. That can be fixed by
+ * trimming the parts-list. FIXME
+ */
+ const p = [host, ...pathSegments].join("/");
+
+ const result: AddExchangeUri = {
+ type: TalerUriAction.AddExchange,
+ exchangeBaseUrl: canonicalizeBaseUrl(
+ `${pi.body.innerProto}://${p}/`,
+ ),
+ };
+ return opFixedSuccess(result);
+}
+
+/**
+ *
+ * @deprecated use parseWithdrawUriWithError
+ */
+export function parseAddExchangeUri(s: string): AddExchangeUri | undefined {
+ const r = parseAddExchangeUriWithError(s);
+ if (r.type === "fail") return undefined;
+ return r.body;
}
+/**
+ * @deprecated use TalerUriAction
+ */
export enum TalerUriType {
TalerPay = "taler-pay",
+ TalerTemplate = "taler-template",
+ TalerPayTemplate = "taler-pay-template",
TalerWithdraw = "taler-withdraw",
TalerTip = "taler-tip",
TalerRefund = "taler-refund",
@@ -101,60 +246,17 @@ export enum TalerUriType {
Unknown = "unknown",
}
-const talerActionPayPull = "pay-pull";
-const talerActionPayPush = "pay-push";
-
-/**
- * Classify a taler:// URI.
- */
-export function classifyTalerUri(s: string): TalerUriType {
- const sl = s.toLowerCase();
- if (sl.startsWith("taler://recovery/")) {
- return TalerUriType.TalerRecovery;
- }
- if (sl.startsWith("taler+http://recovery/")) {
- return TalerUriType.TalerRecovery;
- }
- if (sl.startsWith("taler://pay/")) {
- return TalerUriType.TalerPay;
- }
- if (sl.startsWith("taler+http://pay/")) {
- return TalerUriType.TalerPay;
- }
- if (sl.startsWith("taler://tip/")) {
- return TalerUriType.TalerTip;
- }
- if (sl.startsWith("taler+http://tip/")) {
- return TalerUriType.TalerTip;
- }
- if (sl.startsWith("taler://refund/")) {
- return TalerUriType.TalerRefund;
- }
- if (sl.startsWith("taler+http://refund/")) {
- return TalerUriType.TalerRefund;
- }
- if (sl.startsWith("taler://withdraw/")) {
- return TalerUriType.TalerWithdraw;
- }
- if (sl.startsWith("taler+http://withdraw/")) {
- return TalerUriType.TalerWithdraw;
- }
- if (sl.startsWith(`taler://${talerActionPayPush}/`)) {
- return TalerUriType.TalerPayPush;
- }
- if (sl.startsWith(`taler+http://${talerActionPayPush}/`)) {
- return TalerUriType.TalerPayPush;
- }
- if (sl.startsWith(`taler://${talerActionPayPull}/`)) {
- return TalerUriType.TalerPayPull;
- }
- if (sl.startsWith(`taler+http://${talerActionPayPull}/`)) {
- return TalerUriType.TalerPayPull;
- }
- if (sl.startsWith("taler://dev-experiment/")) {
- return TalerUriType.TalerDevExperiment;
- }
- return TalerUriType.Unknown;
+export enum TalerUriAction {
+ Pay = "pay",
+ Withdraw = "withdraw",
+ Refund = "refund",
+ PayPull = "pay-pull",
+ PayPush = "pay-push",
+ PayTemplate = "pay-template",
+ Restore = "restore",
+ DevExperiment = "dev-experiment",
+ WithdrawExchange = "withdraw-exchange",
+ AddExchange = "add-exchange",
}
interface TalerUriProtoInfo {
@@ -183,6 +285,95 @@ function parseProtoInfo(
}
}
+function parseProtoInfoWithError(s: string, action: string) {
+ if (
+ !s.toLowerCase().startsWith("taler://") &&
+ !s.toLowerCase().startsWith("taler+http://")
+ ) {
+ return opKnownTalerFailure(TalerErrorCode.WALLET_TALER_URI_MALFORMED, {
+ code: TalerErrorCode.WALLET_TALER_URI_MALFORMED,
+ });
+ }
+ const pfxPlain = `taler://${action}/`;
+ const pfxHttp = `taler+http://${action}/`;
+ if (s.toLowerCase().startsWith(pfxPlain)) {
+ return opFixedSuccess({
+ innerProto: "https",
+ rest: s.substring(pfxPlain.length),
+ });
+ } else if (s.toLowerCase().startsWith(pfxHttp)) {
+ return opFixedSuccess({
+ innerProto: "http",
+ rest: s.substring(pfxHttp.length),
+ });
+ } else {
+ return opKnownTalerFailure(TalerErrorCode.WALLET_TALER_URI_MALFORMED, {
+ code: TalerErrorCode.WALLET_TALER_URI_MALFORMED,
+ });
+ }
+}
+
+type Parser = (s: string) => TalerUri | undefined;
+const parsers: { [A in TalerUriAction]: Parser } = {
+ [TalerUriAction.Pay]: parsePayUri,
+ [TalerUriAction.PayPull]: parsePayPullUri,
+ [TalerUriAction.PayPush]: parsePayPushUri,
+ [TalerUriAction.PayTemplate]: parsePayTemplateUri,
+ [TalerUriAction.Restore]: parseRestoreUri,
+ [TalerUriAction.Refund]: parseRefundUri,
+ [TalerUriAction.Withdraw]: parseWithdrawUri,
+ [TalerUriAction.DevExperiment]: parseDevExperimentUri,
+ [TalerUriAction.WithdrawExchange]: parseWithdrawExchangeUri,
+ [TalerUriAction.AddExchange]: parseAddExchangeUri,
+};
+
+export function parseTalerUri(string: string): TalerUri | undefined {
+ const https = string.startsWith("taler://");
+ const http = string.startsWith("taler+http://");
+ if (!https && !http) return undefined;
+ const actionStart = https ? 8 : 13;
+ const actionEnd = string.indexOf("/", actionStart + 1);
+ const action = string.substring(actionStart, actionEnd);
+ const found = Object.values(TalerUriAction).find((x) => x === action);
+ if (!found) return undefined;
+ return parsers[found](string);
+}
+
+export function stringifyTalerUri(uri: TalerUri): string {
+ switch (uri.type) {
+ case TalerUriAction.DevExperiment: {
+ return stringifyDevExperimentUri(uri);
+ }
+ case TalerUriAction.Pay: {
+ return stringifyPayUri(uri);
+ }
+ case TalerUriAction.PayPull: {
+ return stringifyPayPullUri(uri);
+ }
+ case TalerUriAction.PayPush: {
+ return stringifyPayPushUri(uri);
+ }
+ case TalerUriAction.PayTemplate: {
+ return stringifyPayTemplateUri(uri);
+ }
+ case TalerUriAction.Restore: {
+ return stringifyRestoreUri(uri);
+ }
+ case TalerUriAction.Refund: {
+ return stringifyRefundUri(uri);
+ }
+ case TalerUriAction.Withdraw: {
+ return stringifyWithdrawUri(uri);
+ }
+ case TalerUriAction.WithdrawExchange: {
+ return stringifyWithdrawExchange(uri);
+ }
+ case TalerUriAction.AddExchange: {
+ return stringifyAddExchange(uri);
+ }
+ }
+}
+
/**
* Parse a taler[+http]://pay URI.
* Return undefined if not passed a valid URI.
@@ -208,33 +399,53 @@ export function parsePayUri(s: string): PayUriResult | undefined {
const merchantBaseUrl = canonicalizeBaseUrl(`${pi.innerProto}://${p}/`);
return {
+ type: TalerUriAction.Pay,
merchantBaseUrl,
orderId,
- sessionId: sessionId,
+ sessionId,
claimToken,
noncePriv,
};
}
-export function constructPayUri(
- merchantBaseUrl: string,
- orderId: string,
- sessionId: string,
- claimToken?: string,
- noncePriv?: string,
-): string {
- const base = canonicalizeBaseUrl(merchantBaseUrl);
- const url = new URL(base);
- const isHttp = base.startsWith("http://");
- let result = isHttp ? `taler+http://pay/` : `taler://pay/`;
- result += `${url.hostname}${url.pathname}${orderId}/${sessionId}?`;
- if (claimToken) result += `c=${claimToken}`;
- if (noncePriv) result += `n=${noncePriv}`;
- return result;
+export function parsePayTemplateUri(
+ uriString: string,
+): PayTemplateUriResult | undefined {
+ const pi = parseProtoInfo(uriString, TalerUriAction.PayTemplate);
+ if (!pi) {
+ return undefined;
+ }
+ const c = pi.rest.split("?");
+
+ const parts = c[0].split("/");
+ if (parts.length < 2) {
+ return undefined;
+ }
+
+ const q = new URLSearchParams(c[1] ?? "");
+ const params: Record<string, string> = {};
+ q.forEach((v, k) => {
+ params[k] = v;
+ });
+
+ const host = parts[0].toLowerCase();
+ const templateId = parts[parts.length - 1];
+ const pathSegments = parts.slice(1, parts.length - 1);
+ const hostAndSegments = [host, ...pathSegments].join("/");
+ const merchantBaseUrl = canonicalizeBaseUrl(
+ `${pi.innerProto}://${hostAndSegments}/`,
+ );
+
+ return {
+ type: TalerUriAction.PayTemplate,
+ merchantBaseUrl,
+ templateId,
+ templateParams: params,
+ };
}
export function parsePayPushUri(s: string): PayPushUriResult | undefined {
- const pi = parseProtoInfo(s, talerActionPayPush);
+ const pi = parseProtoInfo(s, TalerUriAction.PayPush);
if (!pi) {
return undefined;
}
@@ -246,17 +457,20 @@ export function parsePayPushUri(s: string): PayPushUriResult | undefined {
const host = parts[0].toLowerCase();
const contractPriv = parts[parts.length - 1];
const pathSegments = parts.slice(1, parts.length - 1);
- const p = [host, ...pathSegments].join("/");
- const exchangeBaseUrl = canonicalizeBaseUrl(`${pi.innerProto}://${p}/`);
+ const hostAndSegments = [host, ...pathSegments].join("/");
+ const exchangeBaseUrl = canonicalizeBaseUrl(
+ `${pi.innerProto}://${hostAndSegments}/`,
+ );
return {
+ type: TalerUriAction.PayPush,
exchangeBaseUrl,
contractPriv,
};
}
export function parsePayPullUri(s: string): PayPullUriResult | undefined {
- const pi = parseProtoInfo(s, talerActionPayPull);
+ const pi = parseProtoInfo(s, TalerUriAction.PayPull);
if (!pi) {
return undefined;
}
@@ -268,38 +482,45 @@ export function parsePayPullUri(s: string): PayPullUriResult | undefined {
const host = parts[0].toLowerCase();
const contractPriv = parts[parts.length - 1];
const pathSegments = parts.slice(1, parts.length - 1);
- const p = [host, ...pathSegments].join("/");
- const exchangeBaseUrl = canonicalizeBaseUrl(`${pi.innerProto}://${p}/`);
+ const hostAndSegments = [host, ...pathSegments].join("/");
+ const exchangeBaseUrl = canonicalizeBaseUrl(
+ `${pi.innerProto}://${hostAndSegments}/`,
+ );
return {
+ type: TalerUriAction.PayPull,
exchangeBaseUrl,
contractPriv,
};
}
-/**
- * Parse a taler[+http]://tip URI.
- * Return undefined if not passed a valid URI.
- */
-export function parseTipUri(s: string): TipUriResult | undefined {
- const pi = parseProtoInfo(s, "tip");
+export function parseWithdrawExchangeUri(
+ s: string,
+): WithdrawExchangeUri | undefined {
+ const pi = parseProtoInfo(s, "withdraw-exchange");
if (!pi) {
return undefined;
}
const c = pi?.rest.split("?");
const parts = c[0].split("/");
- if (parts.length < 2) {
+ if (parts.length < 1) {
return undefined;
}
const host = parts[0].toLowerCase();
- const tipId = parts[parts.length - 1];
+ const exchangePub = parts.length > 1 ? parts[parts.length - 1] : undefined;
const pathSegments = parts.slice(1, parts.length - 1);
- const p = [host, ...pathSegments].join("/");
- const merchantBaseUrl = canonicalizeBaseUrl(`${pi.innerProto}://${p}/`);
+ const hostAndSegments = [host, ...pathSegments].join("/");
+ const exchangeBaseUrl = canonicalizeBaseUrl(
+ `${pi.innerProto}://${hostAndSegments}/`,
+ );
+ const q = new URLSearchParams(c[1] ?? "");
+ const amount = (q.get("a") ?? undefined) as AmountString | undefined;
return {
- merchantBaseUrl,
- merchantTipId: tipId,
+ type: TalerUriAction.WithdrawExchange,
+ exchangeBaseUrl,
+ exchangePub: exchangePub != "" ? exchangePub : undefined,
+ amount,
};
}
@@ -321,10 +542,13 @@ export function parseRefundUri(s: string): RefundUriResult | undefined {
const sessionId = parts[parts.length - 1];
const orderId = parts[parts.length - 2];
const pathSegments = parts.slice(1, parts.length - 2);
- const p = [host, ...pathSegments].join("/");
- const merchantBaseUrl = canonicalizeBaseUrl(`${pi.innerProto}://${p}/`);
+ const hostAndSegments = [host, ...pathSegments].join("/");
+ const merchantBaseUrl = canonicalizeBaseUrl(
+ `${pi.innerProto}://${hostAndSegments}/`,
+ );
return {
+ type: TalerUriAction.Refund,
merchantBaseUrl,
orderId,
};
@@ -336,89 +560,172 @@ export function parseDevExperimentUri(s: string): DevExperimentUri | undefined {
if (!c) {
return undefined;
}
- // const q = new URLSearchParams(c[1] ?? "");
const parts = c[0].split("/");
return {
+ type: TalerUriAction.DevExperiment,
devExperimentId: parts[0],
};
}
-export function constructPayPushUri(args: {
- exchangeBaseUrl: string;
- contractPriv: string;
-}): string {
- const url = new URL(args.exchangeBaseUrl);
- let proto: string;
- if (url.protocol === "https:") {
- proto = "taler";
- } else if (url.protocol === "http:") {
- proto = "taler+http";
- } else {
- throw Error(`Unsupported exchange URL protocol ${args.exchangeBaseUrl}`);
+export function parseRestoreUri(uri: string): BackupRestoreUri | undefined {
+ const pi = parseProtoInfo(uri, "restore");
+ if (!pi) {
+ return undefined;
}
- if (!url.pathname.endsWith("/")) {
- throw Error(
- `exchange base URL must end with a slash (got ${args.exchangeBaseUrl}instead)`,
- );
+ const c = pi.rest.split("?");
+ const parts = c[0].split("/");
+ if (parts.length < 2) {
+ return undefined;
}
- return `${proto}://pay-push/${url.host}${url.pathname}${args.contractPriv}`;
+
+ const walletRootPriv = parts[0];
+ if (!walletRootPriv) return undefined;
+ const providers = new Array<string>();
+ parts[1].split(",").map((name) => {
+ const url = canonicalizeBaseUrl(
+ `${pi.innerProto}://${decodeURIComponent(name)}/`,
+ );
+ providers.push(url);
+ });
+ return {
+ type: TalerUriAction.Restore,
+ walletRootPriv,
+ providers,
+ };
}
-export function constructPayPullUri(args: {
- exchangeBaseUrl: string;
- contractPriv: string;
-}): string {
- const url = new URL(args.exchangeBaseUrl);
+// ================================================
+// To string functions
+// ================================================
+
+export function stringifyPayUri({
+ merchantBaseUrl,
+ orderId,
+ sessionId,
+ claimToken,
+ noncePriv,
+}: Omit<PayUriResult, "type">): string {
+ const { proto, path, query } = getUrlInfo(merchantBaseUrl, {
+ c: claimToken,
+ n: noncePriv,
+ });
+ return `${proto}://pay/${path}${orderId}/${sessionId}${query}`;
+}
+
+export function stringifyPayPullUri({
+ contractPriv,
+ exchangeBaseUrl,
+}: Omit<PayPullUriResult, "type">): string {
+ const { proto, path } = getUrlInfo(exchangeBaseUrl);
+ return `${proto}://pay-pull/${path}${contractPriv}`;
+}
+
+export function stringifyPayPushUri({
+ contractPriv,
+ exchangeBaseUrl,
+}: Omit<PayPushUriResult, "type">): string {
+ const { proto, path } = getUrlInfo(exchangeBaseUrl);
+
+ return `${proto}://pay-push/${path}${contractPriv}`;
+}
+
+export function stringifyRestoreUri({
+ providers,
+ walletRootPriv,
+}: Omit<BackupRestoreUri, "type">): string {
+ const list = providers
+ .map((url) => `${encodeURIComponent(new URL(url).href)}`)
+ .join(",");
+ return `taler://restore/${walletRootPriv}/${list}`;
+}
+
+export function stringifyWithdrawExchange({
+ exchangeBaseUrl,
+ exchangePub,
+ amount,
+}: Omit<WithdrawExchangeUri, "type">): string {
+ const { proto, path, query } = getUrlInfo(exchangeBaseUrl, {
+ a: amount,
+ });
+ return `${proto}://withdraw-exchange/${path}${exchangePub ?? ""}${query}`;
+}
+
+export function stringifyAddExchange({
+ exchangeBaseUrl,
+}: Omit<AddExchangeUri, "type">): string {
+ const { proto, path } = getUrlInfo(exchangeBaseUrl);
+ return `${proto}://add-exchange/${path}`;
+}
+
+export function stringifyDevExperimentUri({
+ devExperimentId,
+}: Omit<DevExperimentUri, "type">): string {
+ return `taler://dev-experiment/${devExperimentId}`;
+}
+
+export function stringifyPayTemplateUri({
+ merchantBaseUrl,
+ templateId,
+ templateParams,
+}: Omit<PayTemplateUriResult, "type">): string {
+ const { proto, path, query } = getUrlInfo(merchantBaseUrl, templateParams);
+ return `${proto}://pay-template/${path}${templateId}${query}`;
+}
+
+export function stringifyRefundUri({
+ merchantBaseUrl,
+ orderId,
+}: Omit<RefundUriResult, "type">): string {
+ const { proto, path } = getUrlInfo(merchantBaseUrl);
+ return `${proto}://refund/${path}${orderId}/`;
+}
+
+export function stringifyWithdrawUri({
+ bankIntegrationApiBaseUrl,
+ withdrawalOperationId,
+}: Omit<WithdrawUriResult, "type">): string {
+ const { proto, path } = getUrlInfo(bankIntegrationApiBaseUrl);
+ return `${proto}://withdraw/${path}${withdrawalOperationId}`;
+}
+
+/**
+ * Use baseUrl to defined http or https
+ * create path using host+port+pathname
+ * use params to create a query parameter string or empty
+ */
+function getUrlInfo(
+ baseUrl: string,
+ params: Record<string, string | undefined> = {},
+): { proto: string; path: string; query: string } {
+ const url = new URL(baseUrl);
let proto: string;
if (url.protocol === "https:") {
proto = "taler";
} else if (url.protocol === "http:") {
proto = "taler+http";
} else {
- throw Error(`Unsupported exchange URL protocol ${args.exchangeBaseUrl}`);
+ throw Error(`Unsupported URL protocol in ${baseUrl}`);
}
- if (!url.pathname.endsWith("/")) {
- throw Error(
- `exchange base URL must end with a slash (got ${args.exchangeBaseUrl}instead)`,
- );
+ let path = url.hostname;
+ if (url.port) {
+ path = path + ":" + url.port;
}
- return `${proto}://pay-pull/${url.host}${url.pathname}${args.contractPriv}`;
-}
-
-export function constructRecoveryUri(args: BackupRecovery): string {
- const key = args.walletRootPriv;
- //FIXME: name may contain non valid characters
- const urls = args.providers
- .map((p) => `${p.name}=${canonicalizeBaseUrl(p.url)}`)
- .join("&");
-
- return `taler://recovery/${key}?${urls}`;
-}
-export function parseRecoveryUri(uri: string): BackupRecovery | undefined {
- const pi = parseProtoInfo(uri, "recovery");
- if (!pi) {
- return undefined;
+ if (url.pathname) {
+ path = path + url.pathname;
}
- const idx = pi.rest.indexOf("?");
- if (idx === -1) {
- return undefined;
- }
- const path = pi.rest.slice(0, idx);
- const params = pi.rest.slice(idx + 1);
- if (!path || !params) {
- return undefined;
+ if (!path.endsWith("/")) {
+ path = path + "/";
}
- const parts = path.split("/");
- const walletRootPriv = parts[0];
- if (!walletRootPriv) return undefined;
- const providers = new Array<{ name: string; url: string }>();
- const args = params.split("&");
- for (const param in args) {
- const eq = args[param].indexOf("=");
- if (eq === -1) return undefined;
- const name = args[param].slice(0, eq);
- const url = args[param].slice(eq + 1);
- providers.push({ name, url });
- }
- return { walletRootPriv, providers };
+
+ const qp = new URLSearchParams();
+ let withParams = false;
+ Object.entries(params).forEach(([name, value]) => {
+ if (value !== undefined) {
+ withParams = true;
+ qp.append(name, value);
+ }
+ });
+ const query = withParams ? "?" + qp.toString() : "";
+
+ return { proto, path, query };
}
diff --git a/packages/taler-util/src/time.test.ts b/packages/taler-util/src/time.test.ts
new file mode 100644
index 000000000..5dd8c7715
--- /dev/null
+++ b/packages/taler-util/src/time.test.ts
@@ -0,0 +1,39 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 GNUnet e.V.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import test from "ava";
+import { AbsoluteTime, Duration } from "./time.js";
+
+test("duration parsing", (t) => {
+ const d1 = Duration.fromPrettyString("1h");
+ t.deepEqual(d1.d_ms, 60 * 60 * 1000);
+
+ const d2 = Duration.fromPrettyString(" 2h 1s 3m");
+ t.deepEqual(d2.d_ms, 2 * 60 * 60 * 1000 + 3 * 60 * 1000 + 1000);
+
+ t.throws(() => {
+ Duration.fromPrettyString("5g");
+ });
+ t.throws(() => {
+ Duration.fromPrettyString("s");
+ });
+ t.throws(() => {
+ Duration.fromPrettyString("s5");
+ });
+ t.throws(() => {
+ Duration.fromPrettyString("5 5 s");
+ });
+});
diff --git a/packages/taler-util/src/time.ts b/packages/taler-util/src/time.ts
index 6ffabd495..95b4911a0 100644
--- a/packages/taler-util/src/time.ts
+++ b/packages/taler-util/src/time.ts
@@ -21,22 +21,85 @@
/**
* Imports.
*/
-import { Codec, renderContext, Context } from "./codec.js";
+import { Codec, Context, renderContext } from "./codec.js";
+declare const flavor_AbsoluteTime: unique symbol;
+declare const flavor_TalerProtocolTimestamp: unique symbol;
+declare const flavor_TalerPreciseTimestamp: unique symbol;
+
+const opaque_AbsoluteTime: unique symbol = Symbol("opaque_AbsoluteTime");
+
+// FIXME: Make this opaque!
export interface AbsoluteTime {
/**
* Timestamp in milliseconds.
*/
readonly t_ms: number | "never";
+
+ readonly _flavor?: typeof flavor_AbsoluteTime;
+
+ // Make the type opaque, we only want our constructors
+ // to able to create an AbsoluteTime value.
+ [opaque_AbsoluteTime]: true;
}
export interface TalerProtocolTimestamp {
+ /**
+ * Seconds (as integer) since epoch.
+ */
+ readonly t_s: number | "never";
+
+ readonly _flavor?: typeof flavor_TalerProtocolTimestamp;
+}
+
+/**
+ * Precise timestamp, typically used in the wallet-core
+ * API but not in other Taler APIs so far.
+ */
+export interface TalerPreciseTimestamp {
+ /**
+ * Seconds (as integer) since epoch.
+ */
readonly t_s: number | "never";
+
+ /**
+ * Optional microsecond offset (non-negative integer).
+ */
+ readonly off_us?: number;
+
+ readonly _flavor?: typeof flavor_TalerPreciseTimestamp;
+}
+
+export namespace TalerPreciseTimestamp {
+ export function now(): TalerPreciseTimestamp {
+ const absNow = AbsoluteTime.now();
+ return AbsoluteTime.toPreciseTimestamp(absNow);
+ }
+
+ export function round(t: TalerPreciseTimestamp): TalerProtocolTimestamp {
+ return {
+ t_s: t.t_s,
+ };
+ }
+
+ export function fromSeconds(s: number): TalerPreciseTimestamp {
+ return {
+ t_s: Math.floor(s),
+ off_us: Math.floor((s - Math.floor(s)) / 1000 / 1000),
+ };
+ }
+
+ export function fromMilliseconds(ms: number): TalerPreciseTimestamp {
+ return {
+ t_s: Math.floor(ms / 1000),
+ off_us: Math.floor((ms - Math.floor(ms / 1000) * 1000) * 1000),
+ };
+ }
}
export namespace TalerProtocolTimestamp {
export function now(): TalerProtocolTimestamp {
- return AbsoluteTime.toTimestamp(AbsoluteTime.now());
+ return AbsoluteTime.toProtocolTimestamp(AbsoluteTime.now());
}
export function zero(): TalerProtocolTimestamp {
@@ -51,11 +114,16 @@ export namespace TalerProtocolTimestamp {
};
}
+ export function isNever(t: TalerProtocolTimestamp): boolean {
+ return t.t_s === "never";
+ }
+
export function fromSeconds(s: number): TalerProtocolTimestamp {
return {
t_s: s,
};
}
+
export function min(
t1: TalerProtocolTimestamp,
t2: TalerProtocolTimestamp,
@@ -64,10 +132,19 @@ export namespace TalerProtocolTimestamp {
return { t_s: t2.t_s };
}
if (t2.t_s === "never") {
- return { t_s: t2.t_s };
+ return { t_s: t1.t_s };
}
return { t_s: Math.min(t1.t_s, t2.t_s) };
}
+ export function max(
+ t1: TalerProtocolTimestamp,
+ t2: TalerProtocolTimestamp,
+ ): TalerProtocolTimestamp {
+ if (t1.t_s === "never" || t2.t_s === "never") {
+ return { t_s: "never" };
+ }
+ return { t_s: Math.max(t1.t_s, t2.t_s) };
+ }
}
export interface Duration {
@@ -81,13 +158,27 @@ export interface TalerProtocolDuration {
readonly d_us: number | "forever";
}
+/**
+ * Timeshift in milliseconds.
+ */
let timeshift = 0;
+/**
+ * Set timetravel offset in milliseconds.
+ *
+ * Use carefully and only for testing.
+ */
export function setDangerousTimetravel(dt: number): void {
timeshift = dt;
}
export namespace Duration {
+ export function toMilliseconds(d: Duration): number {
+ if (d.d_ms === "forever") {
+ return Number.MAX_VALUE;
+ }
+ return d.d_ms;
+ }
export function getRemaining(
deadline: AbsoluteTime,
now = AbsoluteTime.now(),
@@ -104,6 +195,72 @@ export namespace Duration {
return { d_ms: deadline.t_ms - now.t_ms };
}
+ export function fromPrettyString(s: string): Duration {
+ let dMs = 0;
+ let currentNum = "";
+ let parsingNum = true;
+ for (let i = 0; i < s.length; i++) {
+ const cc = s.charCodeAt(i);
+ if (cc >= "0".charCodeAt(0) && cc <= "9".charCodeAt(0)) {
+ if (!parsingNum) {
+ throw Error("invalid duration, unexpected number");
+ }
+ currentNum += s[i];
+ continue;
+ }
+ if (s[i] == " ") {
+ if (currentNum != "") {
+ parsingNum = false;
+ }
+ continue;
+ }
+
+ if (currentNum == "") {
+ throw Error("invalid duration, missing number");
+ }
+
+ if (s[i] === "s") {
+ dMs += 1000 * Number.parseInt(currentNum, 10);
+ } else if (s[i] === "m") {
+ dMs += 60 * 1000 * Number.parseInt(currentNum, 10);
+ } else if (s[i] === "h") {
+ dMs += 60 * 60 * 1000 * Number.parseInt(currentNum, 10);
+ } else if (s[i] === "d") {
+ dMs += 24 * 60 * 60 * 1000 * Number.parseInt(currentNum, 10);
+ } else {
+ throw Error("invalid duration, unsupported unit");
+ }
+ currentNum = "";
+ parsingNum = true;
+ }
+ return {
+ d_ms: dMs,
+ };
+ }
+
+ /**
+ * Compare two durations. Returns 0 when equal, -1 when a < b
+ * and +1 when a > b.
+ */
+ export function cmp(d1: Duration, d2: Duration): 1 | 0 | -1 {
+ if (d1.d_ms === "forever") {
+ if (d2.d_ms === "forever") {
+ return 0;
+ }
+ return 1;
+ }
+ if (d2.d_ms === "forever") {
+ return -1;
+ }
+ if (d1.d_ms == d2.d_ms) {
+ return 0;
+ }
+ if (d1.d_ms > d2.d_ms) {
+ return 1;
+ }
+ return -1;
+ }
+
export function max(d1: Duration, d2: Duration): Duration {
return durationMax(d1, d2);
}
@@ -123,7 +280,23 @@ export namespace Duration {
return Math.ceil(d.d_ms / 1000 / 60 / 60 / 24 / 365);
}
- export const fromSpec = durationFromSpec;
+ export function fromSpec(spec: {
+ seconds?: number;
+ minutes?: number;
+ hours?: number;
+ days?: number;
+ months?: number;
+ years?: number;
+ }): Duration {
+ let d_ms = 0;
+ d_ms += (spec.seconds ?? 0) * SECONDS;
+ d_ms += (spec.minutes ?? 0) * MINUTES;
+ d_ms += (spec.hours ?? 0) * HOURS;
+ d_ms += (spec.days ?? 0) * DAYS;
+ d_ms += (spec.months ?? 0) * MONTHS;
+ d_ms += (spec.years ?? 0) * YEARS;
+ return { d_ms };
+ }
export function getForever(): Duration {
return { d_ms: "forever" };
@@ -142,7 +315,7 @@ export namespace Duration {
};
}
return {
- d_ms: d.d_us / 1000,
+ d_ms: Math.floor(d.d_us / 1000),
};
}
@@ -157,6 +330,12 @@ export namespace Duration {
};
}
+ export function fromMilliseconds(ms: number): Duration {
+ return {
+ d_ms: ms,
+ };
+ }
+
export function clamp(args: {
lower: Duration;
upper: Duration;
@@ -167,15 +346,32 @@ export namespace Duration {
}
export namespace AbsoluteTime {
+ export function getStampMsNow(): number {
+ return new Date().getTime();
+ }
+
+ export function getStampMsNever(): number {
+ return Number.MAX_SAFE_INTEGER;
+ }
+
export function now(): AbsoluteTime {
return {
t_ms: new Date().getTime() + timeshift,
+ [opaque_AbsoluteTime]: true,
};
}
export function never(): AbsoluteTime {
return {
t_ms: "never",
+ [opaque_AbsoluteTime]: true,
+ };
+ }
+
+ export function fromMilliseconds(ms: number): AbsoluteTime {
+ return {
+ t_ms: ms,
+ [opaque_AbsoluteTime]: true,
};
}
@@ -200,22 +396,22 @@ export namespace AbsoluteTime {
export function min(t1: AbsoluteTime, t2: AbsoluteTime): AbsoluteTime {
if (t1.t_ms === "never") {
- return { t_ms: t2.t_ms };
+ return { t_ms: t2.t_ms, [opaque_AbsoluteTime]: true };
}
if (t2.t_ms === "never") {
- return { t_ms: t2.t_ms };
+ return { t_ms: t2.t_ms, [opaque_AbsoluteTime]: true };
}
- return { t_ms: Math.min(t1.t_ms, t2.t_ms) };
+ return { t_ms: Math.min(t1.t_ms, t2.t_ms), [opaque_AbsoluteTime]: true };
}
export function max(t1: AbsoluteTime, t2: AbsoluteTime): AbsoluteTime {
if (t1.t_ms === "never") {
- return { t_ms: "never" };
+ return { t_ms: "never", [opaque_AbsoluteTime]: true };
}
if (t2.t_ms === "never") {
- return { t_ms: "never" };
+ return { t_ms: "never", [opaque_AbsoluteTime]: true };
}
- return { t_ms: Math.max(t1.t_ms, t2.t_ms) };
+ return { t_ms: Math.max(t1.t_ms, t2.t_ms), [opaque_AbsoluteTime]: true };
}
export function difference(t1: AbsoluteTime, t2: AbsoluteTime): Duration {
@@ -232,16 +428,64 @@ export namespace AbsoluteTime {
return cmp(t, now()) <= 0;
}
- export function fromTimestamp(t: TalerProtocolTimestamp): AbsoluteTime {
+ export function isNever(t: AbsoluteTime): boolean {
+ return t.t_ms === "never";
+ }
+
+ export function fromProtocolTimestamp(
+ t: TalerProtocolTimestamp,
+ ): AbsoluteTime {
if (t.t_s === "never") {
- return { t_ms: "never" };
+ return { t_ms: "never", [opaque_AbsoluteTime]: true };
}
return {
t_ms: t.t_s * 1000,
+ [opaque_AbsoluteTime]: true,
};
}
- export function toTimestamp(at: AbsoluteTime): TalerProtocolTimestamp {
+ export function fromStampMs(stampMs: number): AbsoluteTime {
+ return {
+ t_ms: stampMs,
+ [opaque_AbsoluteTime]: true,
+ };
+ }
+
+ export function fromPreciseTimestamp(t: TalerPreciseTimestamp): AbsoluteTime {
+ if (t.t_s === "never") {
+ return { t_ms: "never", [opaque_AbsoluteTime]: true };
+ }
+ const offsetUs = t.off_us ?? 0;
+ return {
+ t_ms: t.t_s * 1000 + Math.floor(offsetUs / 1000),
+ [opaque_AbsoluteTime]: true,
+ };
+ }
+
+ export function toStampMs(at: AbsoluteTime): number {
+ if (at.t_ms === "never") {
+ return Number.MAX_SAFE_INTEGER;
+ }
+ return at.t_ms;
+ }
+
+ export function toPreciseTimestamp(at: AbsoluteTime): TalerPreciseTimestamp {
+ if (at.t_ms == "never") {
+ return {
+ t_s: "never",
+ };
+ }
+ const t_s = Math.floor(at.t_ms / 1000);
+ const off_us = Math.floor(1000 * (at.t_ms - t_s * 1000));
+ return {
+ t_s,
+ off_us,
+ };
+ }
+
+ export function toProtocolTimestamp(
+ at: AbsoluteTime,
+ ): TalerProtocolTimestamp {
if (at.t_ms === "never") {
return { t_s: "never" };
}
@@ -274,9 +518,26 @@ export namespace AbsoluteTime {
export function addDuration(t1: AbsoluteTime, d: Duration): AbsoluteTime {
if (t1.t_ms === "never" || d.d_ms === "forever") {
- return { t_ms: "never" };
+ return { t_ms: "never", [opaque_AbsoluteTime]: true };
+ }
+ return { t_ms: t1.t_ms + d.d_ms, [opaque_AbsoluteTime]: true };
+ }
+
+ /**
+ * Get the remaining duration until {@param t1}.
+ *
+ * If {@param t1} already happened, the remaining duration
+ * is zero.
+ */
+ export function remaining(t1: AbsoluteTime): Duration {
+ if (t1.t_ms === "never") {
+ return Duration.getForever();
}
- return { t_ms: t1.t_ms + d.d_ms };
+ const stampNow = now();
+ if (stampNow.t_ms === "never") {
+ throw Error("invariant violated");
+ }
+ return Duration.fromMilliseconds(Math.max(0, t1.t_ms - stampNow.t_ms));
}
export function subtractDuraction(
@@ -284,12 +545,12 @@ export namespace AbsoluteTime {
d: Duration,
): AbsoluteTime {
if (t1.t_ms === "never") {
- return { t_ms: "never" };
+ return { t_ms: "never", [opaque_AbsoluteTime]: true };
}
if (d.d_ms === "forever") {
- return { t_ms: 0 };
+ return { t_ms: 0, [opaque_AbsoluteTime]: true };
}
- return { t_ms: Math.max(0, t1.t_ms - d.d_ms) };
+ return { t_ms: Math.max(0, t1.t_ms - d.d_ms), [opaque_AbsoluteTime]: true };
}
export function stringify(t: AbsoluteTime): string {
@@ -307,24 +568,6 @@ const DAYS = HOURS * 24;
const MONTHS = DAYS * 30;
const YEARS = DAYS * 365;
-export function durationFromSpec(spec: {
- seconds?: number;
- minutes?: number;
- hours?: number;
- days?: number;
- months?: number;
- years?: number;
-}): Duration {
- let d_ms = 0;
- d_ms += (spec.seconds ?? 0) * SECONDS;
- d_ms += (spec.minutes ?? 0) * MINUTES;
- d_ms += (spec.hours ?? 0) * HOURS;
- d_ms += (spec.days ?? 0) * DAYS;
- d_ms += (spec.months ?? 0) * MONTHS;
- d_ms += (spec.years ?? 0) * YEARS;
- return { d_ms };
-}
-
export function durationMin(d1: Duration, d2: Duration): Duration {
if (d1.d_ms === "forever") {
return { d_ms: d2.d_ms };
@@ -361,13 +604,16 @@ export function durationAdd(d1: Duration, d2: Duration): Duration {
export const codecForAbsoluteTime: Codec<AbsoluteTime> = {
decode(x: any, c?: Context): AbsoluteTime {
+ if (x === undefined) {
+ throw Error(`got undefined and expected absolute time at ${renderContext(c)}`);
+ }
const t_ms = x.t_ms;
if (typeof t_ms === "string") {
if (t_ms === "never") {
- return { t_ms: "never" };
+ return { t_ms: "never", [opaque_AbsoluteTime]: true };
}
} else if (typeof t_ms === "number") {
- return { t_ms };
+ return { t_ms, [opaque_AbsoluteTime]: true };
}
throw Error(`expected timestamp at ${renderContext(c)}`);
},
@@ -376,6 +622,9 @@ export const codecForAbsoluteTime: Codec<AbsoluteTime> = {
export const codecForTimestamp: Codec<TalerProtocolTimestamp> = {
decode(x: any, c?: Context): TalerProtocolTimestamp {
// Compatibility, should be removed soon.
+ if (x === undefined) {
+ throw Error(`got undefined and expected timestamp at ${renderContext(c)}`);
+ }
const t_ms = x.t_ms;
if (typeof t_ms === "string") {
if (t_ms === "never") {
@@ -394,7 +643,21 @@ export const codecForTimestamp: Codec<TalerProtocolTimestamp> = {
if (typeof t_s === "number") {
return { t_s };
}
- throw Error(`expected timestamp at ${renderContext(c)}`);
+ throw Error(`expected protocol timestamp at ${renderContext(c)}`);
+ },
+};
+
+export const codecForPreciseTimestamp: Codec<TalerPreciseTimestamp> = {
+ decode(x: any, c?: Context): TalerPreciseTimestamp {
+ const t_ms = x.t_ms;
+ if (typeof t_ms === "string") {
+ if (t_ms === "never") {
+ return { t_s: "never" };
+ }
+ } else if (typeof t_ms === "number") {
+ return { t_s: Math.floor(t_ms / 1000) };
+ }
+ throw Error(`expected precise timestamp at ${renderContext(c)}`);
},
};
diff --git a/packages/taler-wallet-core/src/util/timer.ts b/packages/taler-util/src/timer.ts
index d198e03c9..8db024512 100644
--- a/packages/taler-wallet-core/src/util/timer.ts
+++ b/packages/taler-util/src/timer.ts
@@ -94,7 +94,7 @@ export const performanceNow: () => bigint = (() => {
return () => BigInt(Math.floor(performance.now() * 1000)) * BigInt(1000);
}
- return () => BigInt(0);
+ return () => BigInt(new Date().getTime()) * BigInt(1000) * BigInt(1000);
})();
const nullTimerHandle = {
diff --git a/packages/taler-util/src/transaction-test-data.ts b/packages/taler-util/src/transaction-test-data.ts
new file mode 100644
index 000000000..378028144
--- /dev/null
+++ b/packages/taler-util/src/transaction-test-data.ts
@@ -0,0 +1,113 @@
+/*
+ This file is part of GNU Taler
+ (C) 2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ TransactionType,
+ PaymentStatus,
+ TransactionMajorState,
+} from "./transactions-types.js";
+import { RefreshReason } from "./wallet-types.js";
+
+/**
+ * Sample transaction list entries.
+ */
+export const sampleWalletCoreTransactions = [
+ {
+ type: TransactionType.Payment,
+ txState: {
+ major: TransactionMajorState.Done,
+ },
+ amountRaw: "KUDOS:10",
+ amountEffective: "KUDOS:10",
+ totalRefundRaw: "KUDOS:0",
+ totalRefundEffective: "KUDOS:0",
+ status: PaymentStatus.Paid,
+ refundPending: undefined,
+ posConfirmation: undefined,
+ pending: false,
+ refunds: [],
+ timestamp: {
+ t_s: 1677166045,
+ },
+ transactionId:
+ "txn:payment:NRRD9KJ8970P5HDAGPW1MBA6HZHB1XMFKF5M3CNR6WA0GT98DHY0",
+ proposalId: "NRRD9KJ8970P5HDAGPW1MBA6HZHB1XMFKF5M3CNR6WA0GT98DHY0",
+ info: {
+ merchant: {
+ name: "woocommerce",
+ website: "woocommerce.demo.taler.net",
+ email: "foo@example.com",
+ address: {},
+ jurisdiction: {},
+ },
+ orderId: "wc_order_KQCRldghIgDRB-100",
+ products: [
+ {
+ description: "Using GCC",
+ quantity: 1,
+ price: "KUDOS:10",
+ product_id: "28",
+ },
+ ],
+ summary: "WooTalerShop #100",
+ contractTermsHash:
+ "A02E1M6ARWKBJ87K2TV4S6WQ4X5YH7BRVR6MYCHCTVAED8MBXTFD6PZ5Q50Y7Z5K18PYBTDA14NQ56XPC1VCQW1EVRWTSB7ZYT65B5G",
+ fulfillmentUrl:
+ "https://woocommerce.demo.taler.net/?wc-api=wc_gnutaler_gateway&order_id=wc_order_KQCRldghIgDRB-100",
+ },
+ refundQueryActive: false,
+ frozen: false,
+ },
+ {
+ type: TransactionType.Refresh,
+ txState: {
+ major: TransactionMajorState.Pending,
+ },
+ refreshReason: RefreshReason.PayMerchant,
+ amountEffective: "KUDOS:0",
+ amountRaw: "KUDOS:0",
+ refreshInputAmount: "KUDOS:1.5",
+ refreshOutputAmount: "KUDOS:1.4",
+ originatingTransactionId:
+ "txn:proposal:ZCGBZFE8KZ1CBYYGSC3ZC8E40KVJWV16VYCTHGC8FFSVZ5HD24BG",
+ pending: true,
+ timestamp: {
+ t_s: 1681376214,
+ },
+ transactionId:
+ "txn:refresh:QQSWHHXCRQ269G0E3RW14JMC6F7NFDYDW26NSFHRTXSKDS6CMCZ0",
+ frozen: false,
+ error: {
+ code: 7029,
+ when: {
+ t_ms: 1681376473665,
+ },
+ hint: "Error (WALLET_REFRESH_GROUP_INCOMPLETE)",
+ numErrors: 1,
+ errors: [
+ {
+ code: 7001,
+ when: {
+ t_ms: 1681376473189,
+ },
+ hint: "unexpected exception (message: exchange wire fee signature invalid)",
+ stack:
+ " at validateWireInfo (../taler-wallet-core-qjs.mjs:23166)\n",
+ },
+ ],
+ },
+ },
+];
diff --git a/packages/taler-util/src/transactions-types.ts b/packages/taler-util/src/transactions-types.ts
index 62caaa055..ac4c3d717 100644
--- a/packages/taler-util/src/transactions-types.ts
+++ b/packages/taler-util/src/transactions-types.ts
@@ -24,41 +24,142 @@
/**
* Imports.
*/
-import { TalerProtocolTimestamp } from "./time.js";
+import {
+ Codec,
+ buildCodecForObject,
+ codecForAny,
+ codecForBoolean,
+ codecForConstString,
+ codecForEither,
+ codecForList,
+ codecForString,
+ codecOptional,
+} from "./codec.js";
import {
AmountString,
- Product,
InternationalizedString,
MerchantInfo,
codecForInternationalizedString,
codecForMerchantInfo,
- codecForProduct,
- Location,
} from "./taler-types.js";
-import {
- Codec,
- buildCodecForObject,
- codecOptional,
- codecForString,
- codecForList,
- codecForAny,
-} from "./codec.js";
+import { TalerPreciseTimestamp, TalerProtocolTimestamp } from "./time.js";
import {
RefreshReason,
+ ScopeInfo,
TalerErrorDetail,
TransactionIdStr,
+ TransactionStateFilter,
+ WithdrawalExchangeAccountDetails,
+ codecForScopeInfo,
} from "./wallet-types.js";
export interface TransactionsRequest {
/**
* return only transactions in the given currency
+ *
+ * it will be removed in next release
+ *
+ * @deprecated use scopeInfo
*/
currency?: string;
/**
+ * return only transactions in the given scopeInfo
+ */
+ scopeInfo?: ScopeInfo;
+
+ /**
* if present, results will be limited to transactions related to the given search string
*/
search?: string;
+
+ /**
+ * Sort order of the transaction items.
+ * By default, items are sorted ascending by their
+ * main timestamp.
+ *
+ * ascending: ascending by timestamp, but pending transactions first
+ * descending: ascending by timestamp, but pending transactions first
+ * stable-ascending: ascending by timestamp, with pending transactions amidst other transactions
+ * (stable in the sense of: pending transactions don't jump around)
+ */
+ sort?: "ascending" | "descending" | "stable-ascending";
+
+ /**
+ * If true, include all refreshes in the transactions list.
+ */
+ includeRefreshes?: boolean;
+
+ filterByState?: TransactionStateFilter;
+}
+
+export interface TransactionState {
+ major: TransactionMajorState;
+ minor?: TransactionMinorState;
+}
+
+export enum TransactionMajorState {
+ // No state, only used when reporting transitions into the initial state
+ None = "none",
+ Pending = "pending",
+ Done = "done",
+ Aborting = "aborting",
+ Aborted = "aborted",
+ Suspended = "suspended",
+ Dialog = "dialog",
+ SuspendedAborting = "suspended-aborting",
+ Failed = "failed",
+ Expired = "expired",
+ // Only used for the notification, never in the transaction history
+ Deleted = "deleted",
+}
+
+export enum TransactionMinorState {
+ // Placeholder until D37 is fully implemented
+ Unknown = "unknown",
+ Deposit = "deposit",
+ KycRequired = "kyc",
+ AmlRequired = "aml",
+ MergeKycRequired = "merge-kyc",
+ Track = "track",
+ SubmitPayment = "submit-payment",
+ RebindSession = "rebind-session",
+ Refresh = "refresh",
+ Pickup = "pickup",
+ AutoRefund = "auto-refund",
+ User = "user",
+ Bank = "bank",
+ Exchange = "exchange",
+ ClaimProposal = "claim-proposal",
+ CheckRefund = "check-refund",
+ CreatePurse = "create-purse",
+ DeletePurse = "delete-purse",
+ RefreshExpired = "refresh-expired",
+ Ready = "ready",
+ Merge = "merge",
+ Repurchase = "repurchase",
+ BankRegisterReserve = "bank-register-reserve",
+ BankConfirmTransfer = "bank-confirm-transfer",
+ WithdrawCoins = "withdraw-coins",
+ ExchangeWaitReserve = "exchange-wait-reserve",
+ AbortingBank = "aborting-bank",
+ Aborting = "aborting",
+ Refused = "refused",
+ Withdraw = "withdraw",
+ MerchantOrderProposed = "merchant-order-proposed",
+ Proposed = "proposed",
+ RefundAvailable = "refund-available",
+ AcceptRefund = "accept-refund",
+ PaidByOther = "paid-by-other",
+}
+
+export enum TransactionAction {
+ Delete = "delete",
+ Suspend = "suspend",
+ Resume = "resume",
+ Abort = "abort",
+ Fail = "fail",
+ Retry = "retry",
}
export interface TransactionsResponse {
@@ -78,18 +179,17 @@ export interface TransactionCommon {
type: TransactionType;
// main timestamp of the transaction
- timestamp: TalerProtocolTimestamp;
+ timestamp: TalerPreciseTimestamp;
- // true if the transaction is still pending, false otherwise
- // If a transaction is not longer pending, its timestamp will be updated,
- // but its transactionId will remain unchanged
- pending: boolean;
+ /**
+ * Transaction state, as per DD37.
+ */
+ txState: TransactionState;
/**
- * True if the transaction encountered a problem that might be
- * permanent. A frozen transaction won't be automatically retried.
+ * Possible transitions based on the current state.
*/
- frozen: boolean;
+ txActions: TransactionAction[];
/**
* Raw amount of the transaction (exclusive of fees or other extra costs).
@@ -102,31 +202,41 @@ export interface TransactionCommon {
amountEffective: AmountString;
error?: TalerErrorDetail;
+
+ /**
+ * If the transaction minor state is in KycRequired this field is going to
+ * have the location where the user need to go to complete KYC information.
+ */
+ kycUrl?: string;
}
export type Transaction =
| TransactionWithdrawal
| TransactionPayment
| TransactionRefund
- | TransactionTip
| TransactionRefresh
| TransactionDeposit
| TransactionPeerPullCredit
| TransactionPeerPullDebit
| TransactionPeerPushCredit
- | TransactionPeerPushDebit;
+ | TransactionPeerPushDebit
+ | TransactionInternalWithdrawal
+ | TransactionRecoup
+ | TransactionDenomLoss;
export enum TransactionType {
Withdrawal = "withdrawal",
+ InternalWithdrawal = "internal-withdrawal",
Payment = "payment",
Refund = "refund",
Refresh = "refresh",
- Tip = "tip",
Deposit = "deposit",
PeerPushDebit = "peer-push-debit",
PeerPushCredit = "peer-push-credit",
PeerPullDebit = "peer-pull-debit",
PeerPullCredit = "peer-pull-credit",
+ Recoup = "recoup",
+ DenomLoss = "denom-loss",
}
export enum WithdrawalType {
@@ -145,11 +255,20 @@ interface WithdrawalDetailsForManualTransfer {
* Payto URIs that the exchange supports.
*
* Already contains the amount and message.
+ *
+ * @deprecated in favor of exchangeCreditAccounts
*/
exchangePaytoUris: string[];
+ exchangeCreditAccountDetails?: WithdrawalExchangeAccountDetails[];
+
// Public key of the reserve
reservePub: string;
+
+ /**
+ * Is the reserve ready for withdrawal?
+ */
+ reserveIsReady: boolean;
}
interface WithdrawalDetailsForTalerBankIntegrationApi {
@@ -170,10 +289,34 @@ interface WithdrawalDetailsForTalerBankIntegrationApi {
// Public key of the reserve
reservePub: string;
+
+ /**
+ * Is the reserve ready for withdrawal?
+ */
+ reserveIsReady: boolean;
+
+ exchangeCreditAccountDetails?: WithdrawalExchangeAccountDetails[];
+}
+
+export enum DenomLossEventType {
+ DenomExpired = "denom-expired",
+ DenomVanished = "denom-vanished",
+ DenomUnoffered = "denom-unoffered",
+}
+
+/**
+ * A transaction to indicate financial loss due to denominations
+ * that became unusable for deposits.
+ */
+export interface TransactionDenomLoss extends TransactionCommon {
+ type: TransactionType.DenomLoss;
+ lossEventType: DenomLossEventType;
+ exchangeBaseUrl: string;
}
-// This should only be used for actual withdrawals
-// and not for tips that have their own transactions type.
+/**
+ * A withdrawal transaction (either bank-integrated or manual).
+ */
export interface TransactionWithdrawal extends TransactionCommon {
type: TransactionType.Withdrawal;
@@ -195,6 +338,40 @@ export interface TransactionWithdrawal extends TransactionCommon {
withdrawalDetails: WithdrawalDetails;
}
+/**
+ * Internal withdrawal operation, only reported on request.
+ *
+ * Some transactions (peer-*-credit) internally do a withdrawal,
+ * but only the peer-*-credit transaction is reported.
+ *
+ * The internal withdrawal transaction allows to access the details of
+ * the underlying withdrawal for testing/debugging.
+ *
+ * It is usually not reported, so that amounts of transactions properly
+ * add up, since the amountEffecive of the withdrawal is already reported
+ * in the peer-*-credit transaction.
+ */
+export interface TransactionInternalWithdrawal extends TransactionCommon {
+ type: TransactionType.InternalWithdrawal;
+
+ /**
+ * Exchange of the withdrawal.
+ */
+ exchangeBaseUrl: string;
+
+ /**
+ * Amount that got subtracted from the reserve balance.
+ */
+ amountRaw: AmountString;
+
+ /**
+ * Amount that actually was (or will be) added to the wallet's balance.
+ */
+ amountEffective: AmountString;
+
+ withdrawalDetails: WithdrawalDetails;
+}
+
export interface PeerInfoShort {
expiration: TalerProtocolTimestamp | undefined;
summary: string | undefined;
@@ -224,8 +401,10 @@ export interface TransactionPeerPullCredit extends TransactionCommon {
/**
* URI to send to the other party.
+ *
+ * Only available in the right state.
*/
- talerUri: string;
+ talerUri: string | undefined;
}
/**
@@ -269,8 +448,11 @@ export interface TransactionPeerPushDebit extends TransactionCommon {
/**
* URI to accept the payment.
+ *
+ * Only present if the transaction is in a state where the other party can
+ * accept the payment.
*/
- talerUri: string;
+ talerUri?: string;
}
/**
@@ -296,6 +478,13 @@ export interface TransactionPeerPushCredit extends TransactionCommon {
amountEffective: AmountString;
}
+/**
+ * The exchange revoked a key and the wallet recoups funds.
+ */
+export interface TransactionRecoup extends TransactionCommon {
+ type: TransactionType.Recoup;
+}
+
export enum PaymentStatus {
/**
* Explicitly aborted after timeout / failure
@@ -333,11 +522,6 @@ export interface TransactionPayment extends TransactionCommon {
proposalId: string;
/**
- * How far did the wallet get with processing the payment?
- */
- status: PaymentStatus;
-
- /**
* Amount that must be paid for the contract
*/
amountRaw: AmountString;
@@ -366,6 +550,16 @@ export interface TransactionPayment extends TransactionCommon {
* Reference to applied refunds
*/
refunds: RefundInfoShort[];
+
+ /**
+ * Is the wallet currently checking for a refund?
+ */
+ refundQueryActive: boolean;
+
+ /**
+ * Does this purchase has an pos validation
+ */
+ posConfirmation: string | undefined;
}
export interface OrderShortInfo {
@@ -395,22 +589,6 @@ export interface OrderShortInfo {
summary_i18n?: InternationalizedString;
/**
- * List of products that are part of the order
- */
- products: Product[] | undefined;
-
- /**
- * Time indicating when the order should be delivered.
- * May be overwritten by individual products.
- */
- delivery_date?: TalerProtocolTimestamp;
-
- /**
- * Delivery location for (all!) products.
- */
- delivery_location?: Location;
-
- /**
* URL of the fulfillment, given by the merchant
*/
fulfillmentUrl?: string;
@@ -434,42 +612,31 @@ export interface RefundInfoShort {
amountRaw: AmountString;
}
-export interface TransactionRefund extends TransactionCommon {
- type: TransactionType.Refund;
-
- // ID for the transaction that is refunded
- refundedTransactionId: string;
-
- // Additional information about the refunded payment
- info: OrderShortInfo;
-
+/**
+ * Summary information about the payment that we got a refund for.
+ */
+export interface RefundPaymentInfo {
+ summary: string;
+ summary_i18n?: InternationalizedString;
/**
- * Amount pending to be picked up
+ * More information about the merchant
*/
- refundPending: AmountString | undefined;
+ merchant: MerchantInfo;
+}
+
+export interface TransactionRefund extends TransactionCommon {
+ type: TransactionType.Refund;
// Amount that has been refunded by the merchant
amountRaw: AmountString;
// Amount will be added to the wallet's balance after fees and refreshing
amountEffective: AmountString;
-}
-export interface TransactionTip extends TransactionCommon {
- type: TransactionType.Tip;
-
- // Raw amount of the tip, without extra fees that apply
- amountRaw: AmountString;
-
- /**
- * More information about the merchant
- */
- // merchant: MerchantInfo;
-
- // Amount will be (or was) added to the wallet's balance after fees and refreshing
- amountEffective: AmountString;
+ // ID for the transaction that is refunded
+ refundedTransactionId: string;
- merchantBaseUrl: string;
+ paymentInfo: RefundPaymentInfo | undefined;
}
/**
@@ -480,11 +647,6 @@ export interface TransactionTip extends TransactionCommon {
export interface TransactionRefresh extends TransactionCommon {
type: TransactionType.Refresh;
- /**
- * Exchange that the coins are refreshed with
- */
- exchangeBaseUrl: string;
-
refreshReason: RefreshReason;
/**
@@ -500,8 +662,26 @@ export interface TransactionRefresh extends TransactionCommon {
/**
* Fees, i.e. the effective, negative effect of the refresh
* on the balance.
+ *
+ * Only applicable for stand-alone refreshes, and zero for
+ * other refreshes where the transaction itself accounts for the
+ * refresh fee.
*/
amountEffective: AmountString;
+
+ refreshInputAmount: AmountString;
+ refreshOutputAmount: AmountString;
+}
+
+export interface DepositTransactionTrackingState {
+ // Raw wire transfer identifier of the deposit.
+ wireTransferId: string;
+ // When was the wire transfer given to the bank.
+ timestampExecuted: TalerProtocolTimestamp;
+ // Total amount transfer for this wtid (including fees)
+ amountRaw: AmountString;
+ // Wire fee amount for this exchange
+ wireFee: AmountString;
}
/**
@@ -529,6 +709,15 @@ export interface TransactionDeposit extends TransactionCommon {
amountEffective: AmountString;
wireTransferDeadline: TalerProtocolTimestamp;
+
+ wireTransferProgress: number;
+
+ /**
+ * Did all the deposit requests succeed?
+ */
+ deposited: boolean;
+
+ trackingState: Array<DepositTransactionTrackingState>;
}
export interface TransactionByIdRequest {
@@ -541,10 +730,32 @@ export const codecForTransactionByIdRequest =
.property("transactionId", codecForString())
.build("TransactionByIdRequest");
+export interface WithdrawalTransactionByURIRequest {
+ talerWithdrawUri: string;
+}
+
+export const codecForWithdrawalTransactionByURIRequest =
+ (): Codec<WithdrawalTransactionByURIRequest> =>
+ buildCodecForObject<WithdrawalTransactionByURIRequest>()
+ .property("talerWithdrawUri", codecForString())
+ .build("WithdrawalTransactionByURIRequest");
+
export const codecForTransactionsRequest = (): Codec<TransactionsRequest> =>
buildCodecForObject<TransactionsRequest>()
.property("currency", codecOptional(codecForString()))
+ .property("scopeInfo", codecOptional(codecForScopeInfo()))
.property("search", codecOptional(codecForString()))
+ .property(
+ "sort",
+ codecOptional(
+ codecForEither(
+ codecForConstString("ascending"),
+ codecForConstString("descending"),
+ codecForConstString("stable-ascending"),
+ ),
+ ),
+ )
+ .property("includeRefreshes", codecOptional(codecForBoolean()))
.build("TransactionsRequest");
// FIXME: do full validation here!
@@ -564,7 +775,20 @@ export const codecForOrderShortInfo = (): Codec<OrderShortInfo> =>
.property("fulfillmentUrl", codecOptional(codecForString()))
.property("merchant", codecForMerchantInfo())
.property("orderId", codecForString())
- .property("products", codecOptional(codecForList(codecForProduct())))
.property("summary", codecForString())
.property("summary_i18n", codecOptional(codecForInternationalizedString()))
.build("OrderShortInfo");
+
+export interface ListAssociatedRefreshesRequest {
+ transactionId: string;
+}
+
+export const codecForListAssociatedRefreshesRequest =
+ (): Codec<ListAssociatedRefreshesRequest> =>
+ buildCodecForObject<ListAssociatedRefreshesRequest>()
+ .property("transactionId", codecForString())
+ .build("ListAssociatedRefreshesRequest");
+
+export interface ListAssociatedRefreshesResponse {
+ transactionIds: string[];
+}
diff --git a/packages/taler-util/src/twrpc-impl.missing.ts b/packages/taler-util/src/twrpc-impl.missing.ts
new file mode 100644
index 000000000..7d7fa84ae
--- /dev/null
+++ b/packages/taler-util/src/twrpc-impl.missing.ts
@@ -0,0 +1,26 @@
+/*
+ This file is part of GNU Taler
+ (C) 2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import type { RpcConnectArgs, RpcServerArgs } from "./twrpc.js";
+
+// Not implemented.
+export async function connectRpc<T>(args: RpcConnectArgs<T>): Promise<T> {
+ throw Error("not implemented");
+}
+
+export async function runRpcServer(args: RpcServerArgs): Promise<void> {
+ throw Error("not implemented");
+}
diff --git a/packages/taler-util/src/twrpc-impl.node.ts b/packages/taler-util/src/twrpc-impl.node.ts
new file mode 100644
index 000000000..30e362e5b
--- /dev/null
+++ b/packages/taler-util/src/twrpc-impl.node.ts
@@ -0,0 +1,216 @@
+/*
+ This file is part of GNU Taler
+ (C) 2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import * as net from "node:net";
+import * as fs from "node:fs";
+import { Logger } from "./logging.js";
+import { bytesToString, typedArrayConcat } from "./taler-crypto.js";
+import type { RpcConnectArgs, RpcServerArgs } from "./twrpc.js";
+
+interface ReadLinewiseArgs {
+ onLine(lineData: Uint8Array): void;
+ sock: net.Socket;
+}
+
+const logger = new Logger("twrpc-impl.node.ts");
+
+function readStreamLinewise(args: ReadLinewiseArgs): void {
+ let chunks: Uint8Array[] = [];
+ args.sock.on("data", (buf: Uint8Array) => {
+ // Process all newlines in the newly received buffer
+ while (1) {
+ const newlineIdx = buf.indexOf("\n".charCodeAt(0));
+ if (newlineIdx >= 0) {
+ let left = buf.subarray(0, newlineIdx + 1);
+ let right = buf.subarray(newlineIdx + 1);
+ chunks.push(left);
+ const line = typedArrayConcat(chunks);
+ args.onLine(line);
+ chunks = [];
+ buf = right;
+ } else {
+ chunks.push(buf);
+ break;
+ }
+ }
+ });
+}
+
+export async function connectRpc<T>(args: RpcConnectArgs<T>): Promise<T> {
+ let sockFilename = args.socketFilename;
+ return new Promise((resolve, reject) => {
+ const client = net.createConnection(sockFilename);
+ client.on("error", (e) => {
+ reject(e);
+ });
+ client.on("connect", () => {
+ let parsingBody: string | undefined = undefined;
+ let bodyChunks: string[] = [];
+
+ logger.info("connected!");
+ client.write("%hello-from-client\n");
+ const res = args.onEstablished({
+ sendMessage(m) {
+ client.write("%request\n");
+ client.write(JSON.stringify(m));
+ client.write("\n");
+ client.write("%end\n");
+ },
+ close() {
+ client.destroy();
+ },
+ });
+ readStreamLinewise({
+ sock: client,
+ onLine(line) {
+ const lineStr = bytesToString(line);
+ // Are we currently parsing the body of a request?
+ if (!parsingBody) {
+ const strippedLine = lineStr.trim();
+ if (strippedLine == "%message") {
+ parsingBody = "message";
+ } else if (strippedLine == "%hello-from-server") {
+ } else if (strippedLine.startsWith("%error:")) {
+ client.end();
+ res.onDisconnect();
+ } else {
+ logger.warn("got unknown request");
+ client.write("%error: invalid message\n");
+ client.end();
+ }
+ } else if (parsingBody == "message") {
+ const strippedLine = lineStr.trim();
+ if (strippedLine == "%end") {
+ let req = bodyChunks.join("");
+ let reqJson: any = undefined;
+ try {
+ reqJson = JSON.parse(req);
+ } catch (e) {
+ logger.warn("JSON message from server was invalid");
+ logger.info(`message was: ${req}`);
+ }
+ if (reqJson !== undefined) {
+ res.onMessage(reqJson);
+ } else {
+ client.write("%error: invalid JSON");
+ client.end();
+ }
+ bodyChunks = [];
+ parsingBody = undefined;
+ } else {
+ bodyChunks.push(lineStr);
+ }
+ } else {
+ logger.info("invalid parser state");
+ client.write("%error: internal error\n");
+ client.end();
+ }
+ },
+ });
+ client.on("close", () => {
+ res.onDisconnect();
+ });
+ client.on("data", () => {});
+ resolve(res.result);
+ });
+ });
+}
+
+export async function runRpcServer(args: RpcServerArgs): Promise<void> {
+ let sockFilename = args.socketFilename;
+ try {
+ fs.unlinkSync(sockFilename);
+ } catch (e) {
+ // Do nothing!
+ }
+ return new Promise((resolve, reject) => {
+ const server = net.createServer((sock) => {
+ // Are we currently parsing the body of a request?
+ let parsingBody: string | undefined = undefined;
+ let bodyChunks: string[] = [];
+
+ sock.write("%hello-from-server\n");
+ const handlers = args.onConnect({
+ sendResponse(message) {
+ sock.write("%message\n");
+ sock.write(JSON.stringify(message));
+ sock.write("\n");
+ sock.write("%end\n");
+ },
+ });
+
+ sock.on("error", (err) => {
+ logger.error(`connection error: ${err}`);
+ });
+
+ function processLine(line: Uint8Array) {
+ const lineStr = bytesToString(line);
+ if (!parsingBody) {
+ const strippedLine = lineStr.trim();
+ if (strippedLine == "%request") {
+ parsingBody = "request";
+ } else if (strippedLine === "%hello-from-client") {
+ // Nothing to do, ignore hello
+ } else if (strippedLine.startsWith("%error:")) {
+ logger.warn("got error from client");
+ sock.end();
+ handlers.onDisconnect();
+ } else {
+ logger.info("got unknown request");
+ sock.write("%error: invalid request\n");
+ sock.end();
+ }
+ } else if (parsingBody == "request") {
+ const strippedLine = lineStr.trim();
+ if (strippedLine == "%end") {
+ let req = bodyChunks.join("");
+ let reqJson: any = undefined;
+ try {
+ reqJson = JSON.parse(req);
+ } catch (e) {
+ logger.warn("JSON request from client was invalid");
+ }
+ if (reqJson !== undefined) {
+ handlers.onMessage(reqJson);
+ } else {
+ sock.write("%error: invalid JSON");
+ sock.end();
+ }
+ bodyChunks = [];
+ parsingBody = undefined;
+ } else {
+ bodyChunks.push(lineStr);
+ }
+ } else {
+ logger.error("invalid parser state");
+ sock.write("%error: internal error\n");
+ sock.end();
+ }
+ }
+
+ readStreamLinewise({
+ sock,
+ onLine: processLine,
+ });
+
+ sock.on("close", (hadError: boolean) => {
+ logger.trace(`connection closed, hadError=${hadError}`);
+ handlers.onDisconnect();
+ });
+ });
+ server.listen(args.socketFilename);
+ });
+}
diff --git a/packages/taler-util/src/twrpc-impl.qtart.ts b/packages/taler-util/src/twrpc-impl.qtart.ts
new file mode 100644
index 000000000..7d7fa84ae
--- /dev/null
+++ b/packages/taler-util/src/twrpc-impl.qtart.ts
@@ -0,0 +1,26 @@
+/*
+ This file is part of GNU Taler
+ (C) 2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import type { RpcConnectArgs, RpcServerArgs } from "./twrpc.js";
+
+// Not implemented.
+export async function connectRpc<T>(args: RpcConnectArgs<T>): Promise<T> {
+ throw Error("not implemented");
+}
+
+export async function runRpcServer(args: RpcServerArgs): Promise<void> {
+ throw Error("not implemented");
+}
diff --git a/packages/taler-util/src/twrpc.ts b/packages/taler-util/src/twrpc.ts
new file mode 100644
index 000000000..d221630d0
--- /dev/null
+++ b/packages/taler-util/src/twrpc.ts
@@ -0,0 +1,63 @@
+/*
+ This file is part of GNU Taler
+ (C) 2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { CoreApiResponse } from "./wallet-types.js";
+
+/**
+ * Implementation for the wallet-core IPC protocol.
+ *
+ * Currently the protocol is completely unstable and only used internally
+ * by the wallet for testing purposes.
+ */
+
+// Platform-specific implementation
+export { connectRpc, runRpcServer } from "#twrpc-impl";
+
+export type JsonMessage =
+ | string
+ | number
+ | boolean
+ | null
+ | JsonMessage[]
+ | { [key: string]: JsonMessage };
+
+export interface RpcServerClientHandlers {
+ onMessage(msg: JsonMessage): void;
+ onDisconnect(): void;
+}
+
+export interface RpcServerClient {
+ sendResponse(message: JsonMessage): void;
+}
+
+export interface RpcServerArgs {
+ socketFilename: string;
+ onConnect(client: RpcServerClient): RpcServerClientHandlers;
+}
+
+export interface RpcClientServerConnection {
+ sendMessage(m: JsonMessage): void;
+ close(): void;
+}
+
+export interface RpcConnectArgs<T> {
+ socketFilename: string;
+ onEstablished(connection: RpcClientServerConnection): {
+ result: T;
+ onDisconnect(): void;
+ onMessage(m: JsonMessage): void;
+ };
+}
diff --git a/packages/taler-util/src/wallet-types.ts b/packages/taler-util/src/wallet-types.ts
index 61f2f3b73..0564c45f7 100644
--- a/packages/taler-util/src/wallet-types.ts
+++ b/packages/taler-util/src/wallet-types.ts
@@ -27,16 +27,14 @@
/**
* Imports.
*/
-import {
- AmountJson,
- codecForAmountJson,
- codecForAmountString,
-} from "./amounts.js";
+import { AmountJson, codecForAmountString } from "./amounts.js";
import { BackupRecovery } from "./backup-types.js";
import {
+ Codec,
+ Context,
+ DecodingError,
buildCodecForObject,
buildCodecForUnion,
- Codec,
codecForAny,
codecForBoolean,
codecForConstString,
@@ -46,47 +44,109 @@ import {
codecForNumber,
codecForString,
codecOptional,
+ renderContext,
} from "./codec.js";
+import {
+ CurrencySpecification,
+ TemplateParams,
+ WithdrawalOperationStatus,
+} from "./index.js";
import { VersionMatchResult } from "./libtool-version.js";
import { PaytoUri } from "./payto.js";
import { AgeCommitmentProof } from "./taler-crypto.js";
import { TalerErrorCode } from "./taler-error-codes.js";
import {
+ AccountRestriction,
AmountString,
AuditorDenomSig,
- codecForMerchantContractTerms,
CoinEnvelope,
- MerchantContractTerms,
- PeerContractTerms,
- DenominationPubKey,
DenomKeyType,
+ DenominationPubKey,
ExchangeAuditor,
+ ExchangeWireAccount,
+ InternationalizedString,
+ MerchantContractTerms,
+ MerchantInfo,
+ PeerContractTerms,
UnblindedSignature,
+ codecForExchangeWireAccount,
+ codecForMerchantContractTerms,
codecForPeerContractTerms,
} from "./taler-types.js";
import {
AbsoluteTime,
- codecForAbsoluteTime,
- codecForTimestamp,
+ TalerPreciseTimestamp,
TalerProtocolDuration,
TalerProtocolTimestamp,
+ codecForAbsoluteTime,
+ codecForPreciseTimestamp,
+ codecForTimestamp,
} from "./time.js";
import {
- codecForOrderShortInfo,
OrderShortInfo,
+ TransactionState,
+ TransactionType,
} from "./transactions-types.js";
/**
* Identifier for a transaction in the wallet.
*/
-export type TransactionIdStr = `txn:${string}:${string}`;
+declare const __txId: unique symbol;
+export type TransactionIdStr = `txn:${string}:${string}` & { [__txId]: true };
/**
* Identifier for a pending task in the wallet.
*/
-export type PendingIdStr = `pnd:${string}:${string}`;
-
-export type TombstoneIdStr = `tmb:${string}:${string}`;
+declare const __pndId: unique symbol;
+export type PendingIdStr = `pnd:${string}:${string}` & { [__pndId]: true };
+
+declare const __tmbId: unique symbol;
+export type TombstoneIdStr = `tmb:${string}:${string}` & { [__tmbId]: true };
+
+function codecForTransactionIdStr(): Codec<TransactionIdStr> {
+ return {
+ decode(x: any, c?: Context): TransactionIdStr {
+ if (typeof x === "string" && x.startsWith("txn:")) {
+ return x as TransactionIdStr;
+ }
+ throw new DecodingError(
+ `expected string starting with "txn:" at ${renderContext(
+ c,
+ )} but got ${x}`,
+ );
+ },
+ };
+}
+
+function codecForPendingIdStr(): Codec<PendingIdStr> {
+ return {
+ decode(x: any, c?: Context): PendingIdStr {
+ if (typeof x === "string" && x.startsWith("txn:")) {
+ return x as PendingIdStr;
+ }
+ throw new DecodingError(
+ `expected string starting with "txn:" at ${renderContext(
+ c,
+ )} but got ${x}`,
+ );
+ },
+ };
+}
+
+function codecForTombstoneIdStr(): Codec<TombstoneIdStr> {
+ return {
+ decode(x: any, c?: Context): TombstoneIdStr {
+ if (typeof x === "string" && x.startsWith("tmb:")) {
+ return x as TombstoneIdStr;
+ }
+ throw new DecodingError(
+ `expected string starting with "tmb:" at ${renderContext(
+ c,
+ )} but got ${x}`,
+ );
+ },
+ };
+}
/**
* Response for the create reserve request to the wallet.
@@ -104,35 +164,393 @@ export class CreateReserveResponse {
reservePub: string;
}
-export interface Balance {
+export interface GetBalanceDetailRequest {
+ currency: string;
+}
+
+export const codecForGetBalanceDetailRequest =
+ (): Codec<GetBalanceDetailRequest> =>
+ buildCodecForObject<GetBalanceDetailRequest>()
+ .property("currency", codecForString())
+ .build("GetBalanceDetailRequest");
+
+/**
+ * How the amount should be interpreted in a transaction
+ * Effective = how the balance is change
+ * Raw = effective amount without fee
+ *
+ * Depending on the transaction, raw can be higher than effective
+ */
+export enum TransactionAmountMode {
+ Effective = "effective",
+ Raw = "raw",
+}
+
+export type GetPlanForOperationRequest =
+ | GetPlanForWithdrawRequest
+ | GetPlanForDepositRequest;
+// | GetPlanForPushDebitRequest
+// | GetPlanForPullCreditRequest
+// | GetPlanForPaymentRequest
+// | GetPlanForTipRequest
+// | GetPlanForRefundRequest
+// | GetPlanForPullDebitRequest
+// | GetPlanForPushCreditRequest;
+
+interface GetPlanForWalletInitiatedOperation {
+ instructedAmount: AmountString;
+ mode: TransactionAmountMode;
+}
+
+export interface ConvertAmountRequest {
+ amount: AmountString;
+ type: TransactionAmountMode;
+}
+
+export const codecForConvertAmountRequest =
+ buildCodecForObject<ConvertAmountRequest>()
+ .property("amount", codecForAmountString())
+ .property(
+ "type",
+ codecForEither(
+ codecForConstString(TransactionAmountMode.Raw),
+ codecForConstString(TransactionAmountMode.Effective),
+ ),
+ )
+ .build("ConvertAmountRequest");
+
+export interface GetAmountRequest {
+ currency: string;
+}
+
+export const codecForGetAmountRequest = buildCodecForObject<GetAmountRequest>()
+ .property("currency", codecForString())
+ .build("GetAmountRequest");
+
+interface GetPlanToCompleteOperation {
+ instructedAmount: AmountString;
+}
+
+const codecForGetPlanForWalletInitiatedOperation = <
+ T extends GetPlanForWalletInitiatedOperation,
+>() =>
+ buildCodecForObject<T>()
+ .property(
+ "mode",
+ codecForEither(
+ codecForConstString(TransactionAmountMode.Raw),
+ codecForConstString(TransactionAmountMode.Effective),
+ ),
+ )
+ .property("instructedAmount", codecForAmountString());
+
+interface GetPlanForWithdrawRequest extends GetPlanForWalletInitiatedOperation {
+ type: TransactionType.Withdrawal;
+ exchangeUrl?: string;
+}
+interface GetPlanForDepositRequest extends GetPlanForWalletInitiatedOperation {
+ type: TransactionType.Deposit;
+ account: string; //payto string
+}
+interface GetPlanForPushDebitRequest
+ extends GetPlanForWalletInitiatedOperation {
+ type: TransactionType.PeerPushDebit;
+}
+
+interface GetPlanForPullCreditRequest
+ extends GetPlanForWalletInitiatedOperation {
+ type: TransactionType.PeerPullCredit;
+ exchangeUrl: string;
+}
+
+const codecForGetPlanForWithdrawRequest =
+ codecForGetPlanForWalletInitiatedOperation<GetPlanForWithdrawRequest>()
+ .property("type", codecForConstString(TransactionType.Withdrawal))
+ .property("exchangeUrl", codecOptional(codecForString()))
+ .build("GetPlanForWithdrawRequest");
+
+const codecForGetPlanForDepositRequest =
+ codecForGetPlanForWalletInitiatedOperation<GetPlanForDepositRequest>()
+ .property("type", codecForConstString(TransactionType.Deposit))
+ .property("account", codecForString())
+ .build("GetPlanForDepositRequest");
+
+const codecForGetPlanForPushDebitRequest =
+ codecForGetPlanForWalletInitiatedOperation<GetPlanForPushDebitRequest>()
+ .property("type", codecForConstString(TransactionType.PeerPushDebit))
+ .build("GetPlanForPushDebitRequest");
+
+const codecForGetPlanForPullCreditRequest =
+ codecForGetPlanForWalletInitiatedOperation<GetPlanForPullCreditRequest>()
+ .property("type", codecForConstString(TransactionType.PeerPullCredit))
+ .property("exchangeUrl", codecForString())
+ .build("GetPlanForPullCreditRequest");
+
+interface GetPlanForPaymentRequest extends GetPlanToCompleteOperation {
+ type: TransactionType.Payment;
+ wireMethod: string;
+ ageRestriction: number;
+ maxDepositFee: AmountString;
+}
+
+// interface GetPlanForTipRequest extends GetPlanForOperationBase {
+// type: TransactionType.Tip;
+// }
+// interface GetPlanForRefundRequest extends GetPlanForOperationBase {
+// type: TransactionType.Refund;
+// }
+interface GetPlanForPullDebitRequest extends GetPlanToCompleteOperation {
+ type: TransactionType.PeerPullDebit;
+}
+interface GetPlanForPushCreditRequest extends GetPlanToCompleteOperation {
+ type: TransactionType.PeerPushCredit;
+}
+
+const codecForGetPlanForPaymentRequest =
+ buildCodecForObject<GetPlanForPaymentRequest>()
+ .property("type", codecForConstString(TransactionType.Payment))
+ .property("maxDepositFee", codecForAmountString())
+ .build("GetPlanForPaymentRequest");
+
+const codecForGetPlanForPullDebitRequest =
+ buildCodecForObject<GetPlanForPullDebitRequest>()
+ .property("type", codecForConstString(TransactionType.PeerPullDebit))
+ .build("GetPlanForPullDebitRequest");
+
+const codecForGetPlanForPushCreditRequest =
+ buildCodecForObject<GetPlanForPushCreditRequest>()
+ .property("type", codecForConstString(TransactionType.PeerPushCredit))
+ .build("GetPlanForPushCreditRequest");
+
+export const codecForGetPlanForOperationRequest =
+ (): Codec<GetPlanForOperationRequest> =>
+ buildCodecForUnion<GetPlanForOperationRequest>()
+ .discriminateOn("type")
+ .alternative(
+ TransactionType.Withdrawal,
+ codecForGetPlanForWithdrawRequest,
+ )
+ .alternative(TransactionType.Deposit, codecForGetPlanForDepositRequest)
+ // .alternative(
+ // TransactionType.PeerPushDebit,
+ // codecForGetPlanForPushDebitRequest,
+ // )
+ // .alternative(
+ // TransactionType.PeerPullCredit,
+ // codecForGetPlanForPullCreditRequest,
+ // )
+ // .alternative(TransactionType.Payment, codecForGetPlanForPaymentRequest)
+ // .alternative(
+ // TransactionType.PeerPullDebit,
+ // codecForGetPlanForPullDebitRequest,
+ // )
+ // .alternative(
+ // TransactionType.PeerPushCredit,
+ // codecForGetPlanForPushCreditRequest,
+ // )
+ .build("GetPlanForOperationRequest");
+
+export interface GetPlanForOperationResponse {
+ effectiveAmount: AmountString;
+ rawAmount: AmountString;
+ counterPartyAmount?: AmountString;
+ details: any;
+}
+
+export const codecForGetPlanForOperationResponse =
+ (): Codec<GetPlanForOperationResponse> =>
+ buildCodecForObject<GetPlanForOperationResponse>()
+ .property("effectiveAmount", codecForAmountString())
+ .property("rawAmount", codecForAmountString())
+ .property("details", codecForAny())
+ .property("counterPartyAmount", codecOptional(codecForAmountString()))
+ .build("GetPlanForOperationResponse");
+
+export interface AmountResponse {
+ effectiveAmount: AmountString;
+ rawAmount: AmountString;
+}
+
+export const codecForAmountResponse = (): Codec<AmountResponse> =>
+ buildCodecForObject<AmountResponse>()
+ .property("effectiveAmount", codecForAmountString())
+ .property("rawAmount", codecForAmountString())
+ .build("AmountResponse");
+
+export enum BalanceFlag {
+ IncomingKyc = "incoming-kyc",
+ IncomingAml = "incoming-aml",
+ IncomingConfirmation = "incoming-confirmation",
+ OutgoingKyc = "outgoing-kyc",
+}
+
+export interface WalletBalance {
+ scopeInfo: ScopeInfo;
available: AmountString;
pendingIncoming: AmountString;
pendingOutgoing: AmountString;
- // Does the balance for this currency have a pending
- // transaction?
+ /**
+ * Does the balance for this currency have a pending
+ * transaction?
+ *
+ * @deprecated use flags and pendingIncoming/pendingOutgoing instead
+ */
hasPendingTransactions: boolean;
- // Is there a pending transaction that would affect the balance
- // and requires user input?
+ /**
+ * Is there a transaction that requires user input?
+ *
+ * @deprecated use flags instead
+ */
requiresUserInput: boolean;
+
+ flags: BalanceFlag[];
+}
+
+export const codecForScopeInfoGlobal = (): Codec<ScopeInfoGlobal> =>
+ buildCodecForObject<ScopeInfoGlobal>()
+ .property("currency", codecForString())
+ .property("type", codecForConstString(ScopeType.Global))
+ .build("ScopeInfoGlobal");
+
+export const codecForScopeInfoExchange = (): Codec<ScopeInfoExchange> =>
+ buildCodecForObject<ScopeInfoExchange>()
+ .property("currency", codecForString())
+ .property("type", codecForConstString(ScopeType.Exchange))
+ .property("url", codecForString())
+ .build("ScopeInfoExchange");
+
+export const codecForScopeInfoAuditor = (): Codec<ScopeInfoAuditor> =>
+ buildCodecForObject<ScopeInfoAuditor>()
+ .property("currency", codecForString())
+ .property("type", codecForConstString(ScopeType.Auditor))
+ .property("url", codecForString())
+ .build("ScopeInfoAuditor");
+
+export const codecForScopeInfo = (): Codec<ScopeInfo> =>
+ buildCodecForUnion<ScopeInfo>()
+ .discriminateOn("type")
+ .alternative(ScopeType.Global, codecForScopeInfoGlobal())
+ .alternative(ScopeType.Exchange, codecForScopeInfoExchange())
+ .alternative(ScopeType.Auditor, codecForScopeInfoAuditor())
+ .build("ScopeInfo");
+
+export interface GetCurrencySpecificationRequest {
+ scope: ScopeInfo;
+}
+
+export const codecForGetCurrencyInfoRequest =
+ (): Codec<GetCurrencySpecificationRequest> =>
+ buildCodecForObject<GetCurrencySpecificationRequest>()
+ .property("scope", codecForScopeInfo())
+ .build("GetCurrencySpecificationRequest");
+
+export interface ListExchangesForScopedCurrencyRequest {
+ scope: ScopeInfo;
+}
+
+export const codecForListExchangesForScopedCurrencyRequest =
+ (): Codec<ListExchangesForScopedCurrencyRequest> =>
+ buildCodecForObject<ListExchangesForScopedCurrencyRequest>()
+ .property("scope", codecForScopeInfo())
+ .build("ListExchangesForScopedCurrencyRequest");
+
+export interface GetCurrencySpecificationResponse {
+ currencySpecification: CurrencySpecification;
+}
+
+export interface BuiltinExchange {
+ exchangeBaseUrl: string;
+ currencyHint: string;
+}
+
+export interface PartialWalletRunConfig {
+ builtin?: Partial<WalletRunConfig["builtin"]>;
+ testing?: Partial<WalletRunConfig["testing"]>;
+ features?: Partial<WalletRunConfig["features"]>;
+}
+
+export interface WalletRunConfig {
+ /**
+ * Initialization values useful for a complete startup.
+ *
+ * These are values may be overridden by different wallets
+ */
+ builtin: {
+ exchanges: BuiltinExchange[];
+ };
+
+ /**
+ * Unsafe options which it should only be used to create
+ * testing environment.
+ */
+ testing: {
+ /**
+ * Allow withdrawal of denominations even though they are about to expire.
+ */
+ denomselAllowLate: boolean;
+ devModeActive: boolean;
+ insecureTrustExchange: boolean;
+ preventThrottling: boolean;
+ skipDefaults: boolean;
+ emitObservabilityEvents?: boolean;
+ };
+
+ /**
+ * Configurations values that may be safe to show to the user
+ */
+ features: {
+ allowHttp: boolean;
+ };
}
+export interface InitRequest {
+ config?: PartialWalletRunConfig;
+}
+
+export const codecForInitRequest = (): Codec<InitRequest> =>
+ buildCodecForObject<InitRequest>()
+ .property("config", codecForAny())
+ .build("InitRequest");
+
export interface InitResponse {
versionInfo: WalletCoreVersion;
}
+export enum ScopeType {
+ Global = "global",
+ Exchange = "exchange",
+ Auditor = "auditor",
+}
+
+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;
+ url: string;
+};
+
+export type ScopeInfo = ScopeInfoGlobal | ScopeInfoExchange | ScopeInfoAuditor;
+
export interface BalancesResponse {
- balances: Balance[];
+ balances: WalletBalance[];
}
-export const codecForBalance = (): Codec<Balance> =>
- buildCodecForObject<Balance>()
- .property("available", codecForString())
+export const codecForBalance = (): Codec<WalletBalance> =>
+ buildCodecForObject<WalletBalance>()
+ .property("scopeInfo", codecForAny()) // FIXME
+ .property("available", codecForAmountString())
.property("hasPendingTransactions", codecForBoolean())
- .property("pendingIncoming", codecForString())
- .property("pendingOutgoing", codecForString())
+ .property("pendingIncoming", codecForAmountString())
+ .property("pendingOutgoing", codecForAmountString())
.property("requiresUserInput", codecForBoolean())
+ .property("flags", codecForAny()) // FIXME
.build("Balance");
export const codecForBalancesResponse = (): Codec<BalancesResponse> =>
@@ -161,6 +579,11 @@ export enum CoinStatus {
Fresh = "fresh",
/**
+ * Coin was lost as the denomination is not usable anymore.
+ */
+ DenomLoss = "denom-loss",
+
+ /**
* Fresh, but currently marked as "suspended", thus won't be used
* for spending. Used for testing.
*/
@@ -212,7 +635,7 @@ export interface CoinDumpJson {
spend_allocation:
| {
id: string;
- amount: string;
+ amount: AmountString;
}
| undefined;
/**
@@ -233,18 +656,19 @@ export enum ConfirmPayResultType {
export interface ConfirmPayResultDone {
type: ConfirmPayResultType.Done;
contractTerms: MerchantContractTerms;
- transactionId: string;
+ transactionId: TransactionIdStr;
}
export interface ConfirmPayResultPending {
type: ConfirmPayResultType.Pending;
- transactionId: string;
+ transactionId: TransactionIdStr;
lastError: TalerErrorDetail | undefined;
}
export const codecForTalerErrorDetail = (): Codec<TalerErrorDetail> =>
buildCodecForObject<TalerErrorDetail>()
.property("code", codecForNumber())
+ .property("when", codecOptional(codecForAbsoluteTime))
.property("hint", codecOptional(codecForString()))
.build("TalerErrorDetail");
@@ -254,14 +678,14 @@ export const codecForConfirmPayResultPending =
(): Codec<ConfirmPayResultPending> =>
buildCodecForObject<ConfirmPayResultPending>()
.property("lastError", codecOptional(codecForTalerErrorDetail()))
- .property("transactionId", codecForString())
+ .property("transactionId", codecForTransactionIdStr())
.property("type", codecForConstString(ConfirmPayResultType.Pending))
.build("ConfirmPayResultPending");
export const codecForConfirmPayResultDone = (): Codec<ConfirmPayResultDone> =>
buildCodecForObject<ConfirmPayResultDone>()
.property("type", codecForConstString(ConfirmPayResultType.Done))
- .property("transactionId", codecForString())
+ .property("transactionId", codecForTransactionIdStr())
.property("contractTerms", codecForMerchantContractTerms())
.build("ConfirmPayResultDone");
@@ -324,8 +748,15 @@ export interface PrepareTipResult {
/**
* Unique ID for the tip assigned by the wallet.
* Typically different from the merchant-generated tip ID.
+ *
+ * @deprecated use transactionId instead
*/
- walletTipId: string;
+ walletRewardId: string;
+
+ /**
+ * Tip transaction ID.
+ */
+ transactionId: TransactionIdStr;
/**
* Has the tip already been accepted?
@@ -335,13 +766,13 @@ export interface PrepareTipResult {
/**
* Amount that the merchant gave.
*/
- tipAmountRaw: AmountString;
+ rewardAmountRaw: AmountString;
/**
* Amount that arrived at the wallet.
* Might be lower than the raw amount due to fees.
*/
- tipAmountEffective: AmountString;
+ rewardAmountEffective: AmountString;
/**
* Base URL of the merchant backend giving then tip.
@@ -362,19 +793,21 @@ export interface PrepareTipResult {
}
export interface AcceptTipResponse {
- transactionId: string;
+ transactionId: TransactionIdStr;
+ next_url?: string;
}
export const codecForPrepareTipResult = (): Codec<PrepareTipResult> =>
buildCodecForObject<PrepareTipResult>()
.property("accepted", codecForBoolean())
- .property("tipAmountRaw", codecForAmountString())
- .property("tipAmountEffective", codecForAmountString())
+ .property("rewardAmountRaw", codecForAmountString())
+ .property("rewardAmountEffective", codecForAmountString())
.property("exchangeBaseUrl", codecForString())
.property("merchantBaseUrl", codecForString())
.property("expirationTimestamp", codecForTimestamp)
- .property("walletTipId", codecForString())
- .build("PrepareTipResult");
+ .property("walletRewardId", codecForString())
+ .property("transactionId", codecForTransactionIdStr())
+ .build("PrepareRewardResult");
export interface BenchmarkResult {
time: { [s: string]: number };
@@ -393,16 +826,92 @@ export const codecForPreparePayResultPaymentPossible =
.property("amountEffective", codecForAmountString())
.property("amountRaw", codecForAmountString())
.property("contractTerms", codecForMerchantContractTerms())
+ .property("transactionId", codecForTransactionIdStr())
.property("proposalId", codecForString())
.property("contractTermsHash", codecForString())
.property("talerUri", codecForString())
- .property("noncePriv", codecForString())
.property(
"status",
codecForConstString(PreparePayResultType.PaymentPossible),
)
.build("PreparePayResultPaymentPossible");
+export interface BalanceDetails {}
+
+/**
+ * Detailed reason for why the wallet's balance is insufficient.
+ */
+export interface PaymentInsufficientBalanceDetails {
+ /**
+ * Amount requested by the merchant.
+ */
+ amountRequested: AmountString;
+
+ /**
+ * Balance of type "available" (see balance.ts for definition).
+ */
+ balanceAvailable: AmountString;
+
+ /**
+ * Balance of type "material" (see balance.ts for definition).
+ */
+ balanceMaterial: AmountString;
+
+ /**
+ * Balance of type "age-acceptable" (see balance.ts for definition).
+ */
+ balanceAgeAcceptable: AmountString;
+
+ /**
+ * Balance of type "merchant-acceptable" (see balance.ts for definition).
+ */
+ balanceReceiverAcceptable: AmountString;
+
+ /**
+ * Balance of type "merchant-depositable" (see balance.ts for definition).
+ */
+ balanceReceiverDepositable: AmountString;
+
+ balanceExchangeDepositable: AmountString;
+
+ /**
+ * Maximum effective amount that the wallet can spend,
+ * when all fees are paid by the wallet.
+ */
+ maxEffectiveSpendAmount: AmountString;
+
+ perExchange: {
+ [url: string]: {
+ balanceAvailable: AmountString;
+ balanceMaterial: AmountString;
+ balanceExchangeDepositable: AmountString;
+ balanceAgeAcceptable: AmountString;
+ balanceReceiverAcceptable: AmountString;
+ balanceReceiverDepositable: AmountString;
+ maxEffectiveSpendAmount: AmountString;
+ /**
+ * Exchange doesn't have global fees configured for the relevant year,
+ * p2p payments aren't possible.
+ */
+ missingGlobalFees: boolean;
+ };
+ };
+}
+
+export const codecForPayMerchantInsufficientBalanceDetails =
+ (): Codec<PaymentInsufficientBalanceDetails> =>
+ buildCodecForObject<PaymentInsufficientBalanceDetails>()
+ .property("amountRequested", codecForAmountString())
+ .property("balanceAgeAcceptable", codecForAmountString())
+ .property("balanceAvailable", codecForAmountString())
+ .property("balanceMaterial", codecForAmountString())
+ .property("balanceReceiverAcceptable", codecForAmountString())
+ .property("balanceReceiverDepositable", codecForAmountString())
+ .property("balanceExchangeDepositable", codecForAmountString())
+ .property("perExchange", codecForAny())
+ .property("maxEffectiveSpendAmount", codecForAmountString())
+ .build("PayMerchantInsufficientBalanceDetails");
+
export const codecForPreparePayResultInsufficientBalance =
(): Codec<PreparePayResultInsufficientBalance> =>
buildCodecForObject<PreparePayResultInsufficientBalance>()
@@ -410,11 +919,15 @@ export const codecForPreparePayResultInsufficientBalance =
.property("contractTerms", codecForAny())
.property("talerUri", codecForString())
.property("proposalId", codecForString())
- .property("noncePriv", codecForString())
+ .property("transactionId", codecForTransactionIdStr())
.property(
"status",
codecForConstString(PreparePayResultType.InsufficientBalance),
)
+ .property(
+ "balanceDetails",
+ codecForPayMerchantInsufficientBalanceDetails(),
+ )
.build("PreparePayResultInsufficientBalance");
export const codecForPreparePayResultAlreadyConfirmed =
@@ -424,12 +937,13 @@ export const codecForPreparePayResultAlreadyConfirmed =
"status",
codecForConstString(PreparePayResultType.AlreadyConfirmed),
)
- .property("amountEffective", codecForAmountString())
+ .property("amountEffective", codecOptional(codecForAmountString()))
.property("amountRaw", codecForAmountString())
.property("paid", codecForBoolean())
- .property("talerUri", codecOptional(codecForString()))
+ .property("talerUri", codecForString())
.property("contractTerms", codecForAny())
.property("contractTermsHash", codecForString())
+ .property("transactionId", codecForTransactionIdStr())
.property("proposalId", codecForString())
.build("PreparePayResultAlreadyConfirmed");
@@ -463,49 +977,61 @@ export type PreparePayResult =
*/
export interface PreparePayResultPaymentPossible {
status: PreparePayResultType.PaymentPossible;
+ transactionId: TransactionIdStr;
+ /**
+ * @deprecated use transactionId instead
+ */
proposalId: string;
contractTerms: MerchantContractTerms;
contractTermsHash: string;
- amountRaw: string;
- amountEffective: string;
- noncePriv: string;
+ amountRaw: AmountString;
+ amountEffective: AmountString;
talerUri: string;
}
export interface PreparePayResultInsufficientBalance {
status: PreparePayResultType.InsufficientBalance;
+ transactionId: TransactionIdStr;
+ /**
+ * @deprecated use transactionId
+ */
proposalId: string;
contractTerms: MerchantContractTerms;
- amountRaw: string;
- noncePriv: string;
+ amountRaw: AmountString;
talerUri: string;
+ balanceDetails: PaymentInsufficientBalanceDetails;
}
export interface PreparePayResultAlreadyConfirmed {
status: PreparePayResultType.AlreadyConfirmed;
+ transactionId: TransactionIdStr;
contractTerms: MerchantContractTerms;
paid: boolean;
- amountRaw: string;
- amountEffective: string;
+ amountRaw: AmountString;
+ amountEffective: AmountString | undefined;
contractTermsHash: string;
+ /**
+ * @deprecated use transactionId
+ */
proposalId: string;
- talerUri?: string;
+ talerUri: string;
}
export interface BankWithdrawDetails {
- selectionDone: boolean;
- transferDone: boolean;
+ status: WithdrawalOperationStatus;
amount: AmountJson;
senderWire?: string;
suggestedExchange?: string;
confirmTransferUrl?: string;
wireTypes: string[];
+ operationId: string;
+ apiBaseUrl: string;
}
export interface AcceptWithdrawalResponse {
reservePub: string;
confirmTransferUrl?: string;
- transactionId: string;
+ transactionId: TransactionIdStr;
}
/**
@@ -528,6 +1054,7 @@ export interface WalletDiagnostics {
export interface TalerErrorDetail {
code: TalerErrorCode;
+ when?: AbsoluteTime;
hint?: string;
[x: string]: unknown;
}
@@ -578,6 +1105,9 @@ export enum RefreshReason {
PayPeerPull = "pay-peer-pull",
Refund = "refund",
AbortPay = "abort-pay",
+ AbortDeposit = "abort-deposit",
+ AbortPeerPushDebit = "abort-peer-push-debit",
+ AbortPeerPullDebit = "abort-peer-pull-debit",
Recoup = "recoup",
BackupRestored = "backup-restored",
Scheduled = "scheduled",
@@ -592,13 +1122,6 @@ export interface CoinRefreshRequest {
}
/**
- * Wrapper for refresh group IDs.
- */
-export interface RefreshGroupId {
- readonly refreshGroupId: string;
-}
-
-/**
* Private data required to make a deposit permission.
*/
export interface DepositInfo {
@@ -621,6 +1144,10 @@ export interface DepositInfo {
ageCommitmentProof?: AgeCommitmentProof;
}
+export interface ExchangesShortListResponse {
+ exchanges: ShortExchangeListItem[];
+}
+
export interface ExchangesListResponse {
exchanges: ExchangeListItem[];
}
@@ -630,11 +1157,34 @@ export interface ExchangeDetailedResponse {
}
export interface WalletCoreVersion {
- hash: string | undefined;
+ implementationSemver: string;
+ implementationGitHash: string;
+
+ /**
+ * Wallet-core protocol version supported by this implementation
+ * of the API ("server" version).
+ */
version: string;
exchange: string;
merchant: string;
+
+ bankIntegrationApiRange: string;
+ bankConversionApiRange: string;
+ corebankApiRange: string;
+
+ /**
+ * @deprecated as bank was split into multiple APIs with separate versioning
+ */
bank: string;
+
+ /**
+ * @deprecated
+ */
+ hash: string | undefined;
+
+ /**
+ * @deprecated will be removed
+ */
devMode: boolean;
}
@@ -649,13 +1199,6 @@ export interface KnownBankAccounts {
accounts: KnownBankAccountsInfo[];
}
-export interface ExchangeTosStatusDetails {
- acceptedVersion?: string;
- currentVersion?: string;
- contentType?: string;
- content?: string;
-}
-
/**
* Wire fee for one wire method
*/
@@ -686,19 +1229,11 @@ export interface WireFee {
sig: string;
}
-/**
- * Information about one of the exchange's bank accounts.
- */
-export interface ExchangeAccount {
- payto_uri: string;
- master_sig: string;
-}
-
export type WireFeeMap = { [wireMethod: string]: WireFee[] };
export interface WireInfo {
feesForType: WireFeeMap;
- accounts: ExchangeAccount[];
+ accounts: ExchangeWireAccount[];
}
export interface ExchangeGlobalFees {
@@ -717,12 +1252,6 @@ export interface ExchangeGlobalFees {
signature: string;
}
-const codecForExchangeAccount = (): Codec<ExchangeAccount> =>
- buildCodecForObject<ExchangeAccount>()
- .property("payto_uri", codecForString())
- .property("master_sig", codecForString())
- .build("codecForExchangeAccount");
-
const codecForWireFee = (): Codec<WireFee> =>
buildCodecForObject<WireFee>()
.property("sig", codecForString())
@@ -735,7 +1264,7 @@ const codecForWireFee = (): Codec<WireFee> =>
const codecForWireInfo = (): Codec<WireInfo> =>
buildCodecForObject<WireInfo>()
.property("feesForType", codecForMap(codecForList(codecForWireFee())))
- .property("accounts", codecForList(codecForExchangeAccount()))
+ .property("accounts", codecForList(codecForExchangeWireAccount()))
.build("codecForWireInfo");
export interface DenominationInfo {
@@ -826,7 +1355,6 @@ export interface ExchangeFullDetails {
exchangeBaseUrl: string;
currency: string;
paytoUris: string[];
- tos: ExchangeTosStatusDetails;
auditors: ExchangeAuditor[];
wireInfo: WireInfo;
denomFees: DenomOperationMap<FeeDescription[]>;
@@ -835,36 +1363,61 @@ export interface ExchangeFullDetails {
}
export enum ExchangeTosStatus {
- New = "new",
+ Pending = "pending",
+ Proposed = "proposed",
Accepted = "accepted",
- Changed = "changed",
- NotFound = "not-found",
- Unknown = "unknown",
}
export enum ExchangeEntryStatus {
- Unknown = "unknown",
- Outdated = "outdated",
- Ok = "ok",
+ Preset = "preset",
+ Ephemeral = "ephemeral",
+ Used = "used",
+}
+
+export enum ExchangeUpdateStatus {
+ Initial = "initial",
+ InitialUpdate = "initial-update",
+ Suspended = "suspended",
+ UnavailableUpdate = "unavailable-update",
+ Ready = "ready",
+ ReadyUpdate = "ready-update",
}
export interface OperationErrorInfo {
error: TalerErrorDetail;
}
-// FIXME: This should probably include some error status.
+export interface ShortExchangeListItem {
+ exchangeBaseUrl: string;
+}
+
+/**
+ * Info about an exchange entry in the wallet.
+ */
export interface ExchangeListItem {
exchangeBaseUrl: string;
- currency: string | undefined;
+ masterPub: string | undefined;
+ currency: string;
paytoUris: string[];
tosStatus: ExchangeTosStatus;
- exchangeStatus: ExchangeEntryStatus;
+ exchangeEntryStatus: ExchangeEntryStatus;
+ exchangeUpdateStatus: ExchangeUpdateStatus;
ageRestrictionOptions: number[];
+
/**
- * Permanently added to the wallet, as opposed to just
- * temporarily queried.
+ * P2P payments are disabled with this exchange
+ * (e.g. because no global fees are configured).
*/
- permanent: boolean;
+ peerPaymentsDisabled: boolean;
+
+ /**
+ * Set to true if this exchange doesn't charge any fees.
+ */
+ noFees: boolean;
+
+ scopeInfo: ScopeInfo;
+
+ lastUpdateTimestamp: TalerPreciseTimestamp | undefined;
/**
* Information about the last error that occurred when trying
@@ -886,14 +1439,6 @@ const codecForExchangeAuditor = (): Codec<ExchangeAuditor> =>
.property("denomination_keys", codecForList(codecForAuditorDenomSig()))
.build("codecForExchangeAuditor");
-const codecForExchangeTos = (): Codec<ExchangeTosStatusDetails> =>
- buildCodecForObject<ExchangeTosStatusDetails>()
- .property("acceptedVersion", codecOptional(codecForString()))
- .property("currentVersion", codecOptional(codecForString()))
- .property("contentType", codecOptional(codecForString()))
- .property("content", codecOptional(codecForString()))
- .build("ExchangeTos");
-
export const codecForFeeDescriptionPair = (): Codec<FeeDescriptionPair> =>
buildCodecForObject<FeeDescriptionPair>()
.property("group", codecForString())
@@ -926,7 +1471,6 @@ export const codecForExchangeFullDetails = (): Codec<ExchangeFullDetails> =>
.property("currency", codecForString())
.property("exchangeBaseUrl", codecForString())
.property("paytoUris", codecForList(codecForString()))
- .property("tos", codecForExchangeTos())
.property("auditors", codecForList(codecForExchangeAuditor()))
.property("wireInfo", codecForWireInfo())
.property("denomFees", codecForFeesByOperations())
@@ -941,11 +1485,17 @@ export const codecForExchangeListItem = (): Codec<ExchangeListItem> =>
buildCodecForObject<ExchangeListItem>()
.property("currency", codecForString())
.property("exchangeBaseUrl", codecForString())
+ .property("masterPub", codecOptional(codecForString()))
.property("paytoUris", codecForList(codecForString()))
.property("tosStatus", codecForAny())
- .property("exchangeStatus", codecForAny())
- .property("permanent", codecForBoolean())
+ .property("exchangeEntryStatus", codecForAny())
+ .property("exchangeUpdateStatus", codecForAny())
.property("ageRestrictionOptions", codecForList(codecForNumber()))
+ .property("scopeInfo", codecForScopeInfo())
+ .property("lastUpdateErrorInfo", codecForAny())
+ .property("lastUpdateTimestamp", codecOptional(codecForPreciseTimestamp))
+ .property("noFees", codecForBoolean())
+ .property("peerPaymentsDisabled", codecForBoolean())
.build("ExchangeListItem");
export const codecForExchangesListResponse = (): Codec<ExchangesListResponse> =>
@@ -956,6 +1506,8 @@ export const codecForExchangesListResponse = (): Codec<ExchangesListResponse> =>
export interface AcceptManualWithdrawalResult {
/**
* Payto URIs that can be used to fund the withdrawal.
+ *
+ * @deprecated in favor of withdrawalAccountsList
*/
exchangePaytoUris: string[];
@@ -964,13 +1516,17 @@ export interface AcceptManualWithdrawalResult {
*/
reservePub: string;
- transactionId: string;
+ withdrawalAccountsList: WithdrawalExchangeAccountDetails[];
+
+ transactionId: TransactionIdStr;
}
-export interface ManualWithdrawalDetails {
+export interface WithdrawalDetailsForAmount {
/**
* Did the user accept the current version of the exchange's
* terms of service?
+ *
+ * @deprecated the client should query the exchange entry instead
*/
tosAccepted: boolean;
@@ -985,15 +1541,44 @@ export interface ManualWithdrawalDetails {
amountEffective: AmountString;
/**
+ * Number of coins that would be used for withdrawal.
+ *
+ * The UIs should warn if this number is too high (roughly at >100).
+ */
+ numCoins: number;
+
+ /**
* Ways to pay the exchange.
+ *
+ * @deprecated in favor of withdrawalAccountsList
*/
paytoUris: string[];
/**
+ * Ways to pay the exchange, including accounts that require currency conversion.
+ */
+ withdrawalAccountsList: WithdrawalExchangeAccountDetails[];
+
+ /**
* If the exchange supports age-restricted coins it will return
* the array of ages.
*/
ageRestrictionOptions?: number[];
+
+ /**
+ * Scope info of the currency withdrawn.
+ */
+ scopeInfo: ScopeInfo;
+}
+
+export interface DenomSelItem {
+ denomPubHash: string;
+ count: number;
+ /**
+ * Number of denoms/planchets to skip, because
+ * a re-denomination effectively deleted them.
+ */
+ skip?: number;
}
/**
@@ -1002,10 +1587,9 @@ export interface ManualWithdrawalDetails {
export interface DenomSelectionState {
totalCoinValue: AmountString;
totalWithdrawCost: AmountString;
- selectedDenoms: {
- denomPubHash: string;
- count: number;
- }[];
+ selectedDenoms: DenomSelItem[];
+ earliestDepositExpiration: TalerProtocolTimestamp;
+ hasDenomWithAgeRestriction: boolean;
}
/**
@@ -1021,43 +1605,24 @@ export interface ExchangeWithdrawalDetails {
*/
exchangeWireAccounts: string[];
+ exchangeCreditAccountDetails: WithdrawalExchangeAccountDetails[];
+
/**
* Selected denominations for withdraw.
*/
selectedDenoms: DenomSelectionState;
/**
- * Does the wallet know about an auditor for
- * the exchange that the reserve.
- */
- isAudited: boolean;
-
- /**
* Did the user already accept the current terms of service for the exchange?
*/
termsOfServiceAccepted: boolean;
/**
- * The exchange is trusted directly.
- */
- isTrusted: boolean;
-
- /**
* The earliest deposit expiration of the selected coins.
*/
earliestDepositExpiration: TalerProtocolTimestamp;
/**
- * Number of currently offered denominations.
- */
- numOfferedDenoms: number;
-
- /**
- * Public keys of trusted auditors for the currency we're withdrawing.
- */
- trustedAuditorPubs: string[];
-
- /**
* Result of checking the wallet's version
* against the exchange's version.
*
@@ -1092,6 +1657,8 @@ export interface ExchangeWithdrawalDetails {
*
*/
ageRestrictionOptions?: number[];
+
+ scopeInfo: ScopeInfo;
}
export interface GetExchangeTosResult {
@@ -1116,13 +1683,25 @@ export interface GetExchangeTosResult {
*/
contentType: string;
+ /**
+ * Language of the returned content.
+ *
+ * If missing, language is unknown.
+ */
+ contentLanguage: string | undefined;
+
+ /**
+ * Available languages as advertised by the exchange.
+ */
+ tosAvailableLanguages: string[];
+
tosStatus: ExchangeTosStatus;
}
export interface TestPayArgs {
merchantBaseUrl: string;
merchantAuthToken?: string;
- amount: string;
+ amount: AmountString;
summary: string;
forcedCoinSel?: ForcedCoinSel;
}
@@ -1131,43 +1710,112 @@ export const codecForTestPayArgs = (): Codec<TestPayArgs> =>
buildCodecForObject<TestPayArgs>()
.property("merchantBaseUrl", codecForString())
.property("merchantAuthToken", codecOptional(codecForString()))
- .property("amount", codecForString())
+ .property("amount", codecForAmountString())
.property("summary", codecForString())
.property("forcedCoinSel", codecForAny())
.build("TestPayArgs");
export interface IntegrationTestArgs {
exchangeBaseUrl: string;
- bankBaseUrl: string;
- bankAccessApiBaseUrl?: string;
+ corebankApiBaseUrl: string;
merchantBaseUrl: string;
merchantAuthToken?: string;
- amountToWithdraw: string;
- amountToSpend: string;
+ amountToWithdraw: AmountString;
+ amountToSpend: AmountString;
}
export const codecForIntegrationTestArgs = (): Codec<IntegrationTestArgs> =>
buildCodecForObject<IntegrationTestArgs>()
.property("exchangeBaseUrl", codecForString())
- .property("bankBaseUrl", codecForString())
.property("merchantBaseUrl", codecForString())
.property("merchantAuthToken", codecOptional(codecForString()))
.property("amountToSpend", codecForAmountString())
.property("amountToWithdraw", codecForAmountString())
- .property("bankAccessApiBaseUrl", codecOptional(codecForAmountString()))
+ .property("corebankApiBaseUrl", codecForString())
.build("IntegrationTestArgs");
+export interface IntegrationTestV2Args {
+ exchangeBaseUrl: string;
+ corebankApiBaseUrl: string;
+ merchantBaseUrl: string;
+ merchantAuthToken?: string;
+}
+
+export const codecForIntegrationTestV2Args = (): Codec<IntegrationTestV2Args> =>
+ buildCodecForObject<IntegrationTestV2Args>()
+ .property("exchangeBaseUrl", codecForString())
+ .property("merchantBaseUrl", codecForString())
+ .property("merchantAuthToken", codecOptional(codecForString()))
+ .property("corebankApiBaseUrl", codecForString())
+ .build("IntegrationTestV2Args");
+
+export interface GetExchangeEntryByUrlRequest {
+ exchangeBaseUrl: string;
+}
+
+export const codecForGetExchangeEntryByUrlRequest =
+ (): Codec<GetExchangeEntryByUrlRequest> =>
+ buildCodecForObject<GetExchangeEntryByUrlRequest>()
+ .property("exchangeBaseUrl", codecForString())
+ .build("GetExchangeEntryByUrlRequest");
+
+export type GetExchangeEntryByUrlResponse = ExchangeListItem;
+
export interface AddExchangeRequest {
exchangeBaseUrl: string;
+
+ /**
+ * @deprecated use a separate API call to start a forced exchange update instead
+ */
forceUpdate?: boolean;
+
+ masterPub?: string;
}
export const codecForAddExchangeRequest = (): Codec<AddExchangeRequest> =>
buildCodecForObject<AddExchangeRequest>()
.property("exchangeBaseUrl", codecForString())
.property("forceUpdate", codecOptional(codecForBoolean()))
+ .property("masterPub", codecOptional(codecForString()))
.build("AddExchangeRequest");
+export interface UpdateExchangeEntryRequest {
+ exchangeBaseUrl: string;
+ force?: boolean;
+}
+
+export const codecForUpdateExchangeEntryRequest =
+ (): Codec<UpdateExchangeEntryRequest> =>
+ buildCodecForObject<UpdateExchangeEntryRequest>()
+ .property("exchangeBaseUrl", codecForString())
+ .property("force", codecOptional(codecForBoolean()))
+ .build("UpdateExchangeEntryRequest");
+
+export interface GetExchangeResourcesRequest {
+ exchangeBaseUrl: string;
+}
+
+export const codecForGetExchangeResourcesRequest =
+ (): Codec<GetExchangeResourcesRequest> =>
+ buildCodecForObject<GetExchangeResourcesRequest>()
+ .property("exchangeBaseUrl", codecForString())
+ .build("GetExchangeResourcesRequest");
+
+export interface GetExchangeResourcesResponse {
+ hasResources: boolean;
+}
+
+export interface DeleteExchangeRequest {
+ exchangeBaseUrl: string;
+ purge?: boolean;
+}
+
+export const codecForDeleteExchangeRequest = (): Codec<DeleteExchangeRequest> =>
+ buildCodecForObject<DeleteExchangeRequest>()
+ .property("exchangeBaseUrl", codecForString())
+ .property("purge", codecOptional(codecForBoolean()))
+ .build("DeleteExchangeRequest");
+
export interface ForceExchangeUpdateRequest {
exchangeBaseUrl: string;
}
@@ -1181,32 +1829,47 @@ export const codecForForceExchangeUpdateRequest =
export interface GetExchangeTosRequest {
exchangeBaseUrl: string;
acceptedFormat?: string[];
+ acceptLanguage?: string;
}
export const codecForGetExchangeTosRequest = (): Codec<GetExchangeTosRequest> =>
buildCodecForObject<GetExchangeTosRequest>()
.property("exchangeBaseUrl", codecForString())
.property("acceptedFormat", codecOptional(codecForList(codecForString())))
+ .property("acceptLanguage", codecOptional(codecForString()))
.build("GetExchangeTosRequest");
export interface AcceptManualWithdrawalRequest {
exchangeBaseUrl: string;
- amount: string;
+ amount: AmountString;
restrictAge?: number;
}
-export const codecForAcceptManualWithdrawalRequet =
+export const codecForAcceptManualWithdrawalRequest =
(): Codec<AcceptManualWithdrawalRequest> =>
buildCodecForObject<AcceptManualWithdrawalRequest>()
.property("exchangeBaseUrl", codecForString())
- .property("amount", codecForString())
+ .property("amount", codecForAmountString())
.property("restrictAge", codecOptional(codecForNumber()))
.build("AcceptManualWithdrawalRequest");
export interface GetWithdrawalDetailsForAmountRequest {
exchangeBaseUrl: string;
- amount: string;
+ amount: AmountString;
restrictAge?: number;
+
+ /**
+ * ID provided by the client to cancel the request.
+ *
+ * If the same request is made again with the same clientCancellationId,
+ * all previous requests are cancelled.
+ *
+ * The cancelled request will receive an error response with
+ * an error code that indicates the cancellation.
+ *
+ * The cancellation is best-effort, responses might still arrive.
+ */
+ clientCancellationId?: string;
}
export interface AcceptBankIntegratedWithdrawalRequest {
@@ -1229,30 +1892,39 @@ export const codecForGetWithdrawalDetailsForAmountRequest =
(): Codec<GetWithdrawalDetailsForAmountRequest> =>
buildCodecForObject<GetWithdrawalDetailsForAmountRequest>()
.property("exchangeBaseUrl", codecForString())
- .property("amount", codecForString())
+ .property("amount", codecForAmountString())
.property("restrictAge", codecOptional(codecForNumber()))
+ .property("clientCancellationId", codecOptional(codecForString()))
.build("GetWithdrawalDetailsForAmountRequest");
export interface AcceptExchangeTosRequest {
exchangeBaseUrl: string;
- etag: string | undefined;
}
export const codecForAcceptExchangeTosRequest =
(): Codec<AcceptExchangeTosRequest> =>
buildCodecForObject<AcceptExchangeTosRequest>()
.property("exchangeBaseUrl", codecForString())
- .property("etag", codecOptional(codecForString()))
.build("AcceptExchangeTosRequest");
-export interface ApplyRefundRequest {
- talerRefundUri: string;
+export interface ForgetExchangeTosRequest {
+ exchangeBaseUrl: string;
}
-export const codecForApplyRefundRequest = (): Codec<ApplyRefundRequest> =>
- buildCodecForObject<ApplyRefundRequest>()
- .property("talerRefundUri", codecForString())
- .build("ApplyRefundRequest");
+export const codecForForgetExchangeTosRequest =
+ (): Codec<ForgetExchangeTosRequest> =>
+ buildCodecForObject<ForgetExchangeTosRequest>()
+ .property("exchangeBaseUrl", codecForString())
+ .build("ForgetExchangeTosRequest");
+
+export interface AcceptRefundRequest {
+ transactionId: TransactionIdStr;
+}
+
+export const codecForApplyRefundRequest = (): Codec<AcceptRefundRequest> =>
+ buildCodecForObject<AcceptRefundRequest>()
+ .property("transactionId", codecForTransactionIdStr())
+ .build("AcceptRefundRequest");
export interface ApplyRefundFromPurchaseIdRequest {
purchaseId: string;
@@ -1267,6 +1939,7 @@ export const codecForApplyRefundFromPurchaseIdRequest =
export interface GetWithdrawalDetailsForUriRequest {
talerWithdrawUri: string;
restrictAge?: number;
+ notifyChangeFromPendingTimeoutMs?: number;
}
export const codecForGetWithdrawalDetailsForUri =
@@ -1274,6 +1947,10 @@ export const codecForGetWithdrawalDetailsForUri =
buildCodecForObject<GetWithdrawalDetailsForUriRequest>()
.property("talerWithdrawUri", codecForString())
.property("restrictAge", codecOptional(codecForNumber()))
+ .property(
+ "notifyChangeFromPendingTimeoutMs",
+ codecOptional(codecForNumber()),
+ )
.build("GetWithdrawalDetailsForUriRequest");
export interface ListKnownBankAccountsRequest {
@@ -1319,13 +1996,16 @@ export const codecForAbortProposalRequest = (): Codec<AbortProposalRequest> =>
.build("AbortProposalRequest");
export interface GetContractTermsDetailsRequest {
- proposalId: string;
+ // @deprecated use transaction id
+ proposalId?: string;
+ transactionId?: string;
}
export const codecForGetContractTermsDetails =
(): Codec<GetContractTermsDetailsRequest> =>
buildCodecForObject<GetContractTermsDetailsRequest>()
- .property("proposalId", codecForString())
+ .property("proposalId", codecOptional(codecForString()))
+ .property("transactionId", codecOptional(codecForString()))
.build("GetContractTermsDetails");
export interface PreparePayRequest {
@@ -1337,22 +2017,63 @@ export const codecForPreparePayRequest = (): Codec<PreparePayRequest> =>
.property("talerPayUri", codecForString())
.build("PreparePay");
+export interface SharePaymentRequest {
+ merchantBaseUrl: string;
+ orderId: string;
+}
+export const codecForSharePaymentRequest = (): Codec<SharePaymentRequest> =>
+ buildCodecForObject<SharePaymentRequest>()
+ .property("merchantBaseUrl", codecForString())
+ .property("orderId", codecForString())
+ .build("SharePaymentRequest");
+
+export interface SharePaymentResult {
+ privatePayUri: string;
+}
+export const codecForSharePaymentResult = (): Codec<SharePaymentResult> =>
+ buildCodecForObject<SharePaymentResult>()
+ .property("privatePayUri", codecForString())
+ .build("SharePaymentResult");
+
+export interface PreparePayTemplateRequest {
+ talerPayTemplateUri: string;
+ templateParams?: TemplateParams;
+}
+
+export const codecForPreparePayTemplateRequest =
+ (): Codec<PreparePayTemplateRequest> =>
+ buildCodecForObject<PreparePayTemplateRequest>()
+ .property("talerPayTemplateUri", codecForString())
+ .property("templateParams", codecForAny())
+ .build("PreparePayTemplate");
+
export interface ConfirmPayRequest {
- proposalId: string;
+ /**
+ * @deprecated use transactionId instead
+ */
+ proposalId?: string;
+ transactionId?: TransactionIdStr;
sessionId?: string;
forcedCoinSel?: ForcedCoinSel;
}
export const codecForConfirmPayRequest = (): Codec<ConfirmPayRequest> =>
buildCodecForObject<ConfirmPayRequest>()
- .property("proposalId", codecForString())
+ .property("proposalId", codecOptional(codecForString()))
+ .property("transactionId", codecOptional(codecForTransactionIdStr()))
.property("sessionId", codecOptional(codecForString()))
.property("forcedCoinSel", codecForAny())
.build("ConfirmPay");
+export interface CoreApiRequestEnvelope {
+ id: string;
+ operation: string;
+ args: unknown;
+}
+
export type CoreApiResponse = CoreApiResponseSuccess | CoreApiResponseError;
-export type CoreApiEnvelope = CoreApiResponse | CoreApiNotification;
+export type CoreApiMessageEnvelope = CoreApiResponse | CoreApiNotification;
export interface CoreApiNotification {
type: "notification";
@@ -1376,22 +2097,15 @@ export interface CoreApiResponseError {
}
export interface WithdrawTestBalanceRequest {
- amount: string;
- bankBaseUrl: string;
+ amount: AmountString;
/**
- * Bank access API base URL. Defaults to the bankBaseUrl.
+ * Corebank API base URL.
*/
- bankAccessApiBaseUrl?: string;
+ corebankApiBaseUrl: string;
exchangeBaseUrl: string;
forcedDenomSel?: ForcedDenomSel;
}
-export const withdrawTestBalanceDefaults = {
- amount: "TESTKUDOS:10",
- bankBaseUrl: "https://bank.test.taler.net/",
- exchangeBaseUrl: "https://exchange.test.taler.net/",
-};
-
/**
* Request to the crypto worker to make a sync signature.
*/
@@ -1457,43 +2171,12 @@ export interface RecoveryLoadRequest {
export const codecForWithdrawTestBalance =
(): Codec<WithdrawTestBalanceRequest> =>
buildCodecForObject<WithdrawTestBalanceRequest>()
- .property("amount", codecForString())
- .property("bankBaseUrl", codecForString())
+ .property("amount", codecForAmountString())
.property("exchangeBaseUrl", codecForString())
.property("forcedDenomSel", codecForAny())
- .property("bankAccessApiBaseUrl", codecOptional(codecForString()))
+ .property("corebankApiBaseUrl", codecForString())
.build("WithdrawTestBalanceRequest");
-export interface ApplyRefundResponse {
- contractTermsHash: string;
-
- transactionId: string;
-
- proposalId: string;
-
- amountEffectivePaid: AmountString;
-
- amountRefundGranted: AmountString;
-
- amountRefundGone: AmountString;
-
- pendingAtExchange: boolean;
-
- info: OrderShortInfo;
-}
-
-export const codecForApplyRefundResponse = (): Codec<ApplyRefundResponse> =>
- buildCodecForObject<ApplyRefundResponse>()
- .property("amountEffectivePaid", codecForAmountString())
- .property("amountRefundGone", codecForAmountString())
- .property("amountRefundGranted", codecForAmountString())
- .property("contractTermsHash", codecForString())
- .property("pendingAtExchange", codecForBoolean())
- .property("proposalId", codecForString())
- .property("transactionId", codecForString())
- .property("info", codecForOrderShortInfo())
- .build("ApplyRefundResponse");
-
export interface SetCoinSuspendedRequest {
coinPub: string;
suspended: boolean;
@@ -1506,57 +2189,120 @@ export const codecForSetCoinSuspendedRequest =
.property("suspended", codecForBoolean())
.build("SetCoinSuspendedRequest");
+export interface RefreshCoinSpec {
+ coinPub: string;
+ amount?: AmountString;
+}
+
+export const codecForRefreshCoinSpec = (): Codec<RefreshCoinSpec> =>
+ buildCodecForObject<RefreshCoinSpec>()
+ .property("amount", codecForAmountString())
+ .property("coinPub", codecForString())
+ .build("ForceRefreshRequest");
+
export interface ForceRefreshRequest {
- coinPubList: string[];
+ refreshCoinSpecs: RefreshCoinSpec[];
}
export const codecForForceRefreshRequest = (): Codec<ForceRefreshRequest> =>
buildCodecForObject<ForceRefreshRequest>()
- .property("coinPubList", codecForList(codecForString()))
+ .property("refreshCoinSpecs", codecForList(codecForRefreshCoinSpec()))
.build("ForceRefreshRequest");
export interface PrepareRefundRequest {
talerRefundUri: string;
}
+export interface StartRefundQueryForUriResponse {
+ /**
+ * Transaction id of the *payment* where the refund query was started.
+ */
+ transactionId: TransactionIdStr;
+}
+
export const codecForPrepareRefundRequest = (): Codec<PrepareRefundRequest> =>
buildCodecForObject<PrepareRefundRequest>()
.property("talerRefundUri", codecForString())
.build("PrepareRefundRequest");
-export interface PrepareTipRequest {
- talerTipUri: string;
+export interface StartRefundQueryRequest {
+ transactionId: TransactionIdStr;
}
-export const codecForPrepareTipRequest = (): Codec<PrepareTipRequest> =>
- buildCodecForObject<PrepareTipRequest>()
- .property("talerTipUri", codecForString())
- .build("PrepareTipRequest");
+export const codecForStartRefundQueryRequest =
+ (): Codec<StartRefundQueryRequest> =>
+ buildCodecForObject<StartRefundQueryRequest>()
+ .property("transactionId", codecForTransactionIdStr())
+ .build("StartRefundQueryRequest");
-export interface AcceptTipRequest {
- walletTipId: string;
+export interface PrepareRewardRequest {
+ talerRewardUri: string;
}
-export const codecForAcceptTipRequest = (): Codec<AcceptTipRequest> =>
- buildCodecForObject<AcceptTipRequest>()
- .property("walletTipId", codecForString())
- .build("AcceptTipRequest");
+export const codecForPrepareRewardRequest = (): Codec<PrepareRewardRequest> =>
+ buildCodecForObject<PrepareRewardRequest>()
+ .property("talerRewardUri", codecForString())
+ .build("PrepareRewardRequest");
-export interface AbortPayWithRefundRequest {
- proposalId: string;
+export interface AcceptRewardRequest {
+ /**
+ * @deprecated use transactionId
+ */
+ walletRewardId?: string;
+ /**
+ * it will be required when "walletRewardId" is removed
+ */
+ transactionId?: TransactionIdStr;
}
-export const codecForAbortPayWithRefundRequest =
- (): Codec<AbortPayWithRefundRequest> =>
- buildCodecForObject<AbortPayWithRefundRequest>()
- .property("proposalId", codecForString())
- .build("AbortPayWithRefundRequest");
+export const codecForAcceptTipRequest = (): Codec<AcceptRewardRequest> =>
+ buildCodecForObject<AcceptRewardRequest>()
+ .property("walletRewardId", codecOptional(codecForString()))
+ .property("transactionId", codecOptional(codecForTransactionIdStr()))
+ .build("AcceptRewardRequest");
-export interface GetFeeForDepositRequest {
- depositPaytoUri: string;
- amount: AmountString;
+export interface FailTransactionRequest {
+ transactionId: TransactionIdStr;
+}
+
+export const codecForFailTransactionRequest =
+ (): Codec<FailTransactionRequest> =>
+ buildCodecForObject<FailTransactionRequest>()
+ .property("transactionId", codecForTransactionIdStr())
+ .build("FailTransactionRequest");
+
+export interface SuspendTransactionRequest {
+ transactionId: TransactionIdStr;
+}
+
+export const codecForSuspendTransaction =
+ (): Codec<SuspendTransactionRequest> =>
+ buildCodecForObject<AbortTransactionRequest>()
+ .property("transactionId", codecForTransactionIdStr())
+ .build("SuspendTransactionRequest");
+
+export interface ResumeTransactionRequest {
+ transactionId: TransactionIdStr;
+}
+
+export const codecForResumeTransaction = (): Codec<ResumeTransactionRequest> =>
+ buildCodecForObject<ResumeTransactionRequest>()
+ .property("transactionId", codecForTransactionIdStr())
+ .build("ResumeTransactionRequest");
+
+export interface AbortTransactionRequest {
+ transactionId: TransactionIdStr;
+}
+
+export interface FailTransactionRequest {
+ transactionId: TransactionIdStr;
}
+export const codecForAbortTransaction = (): Codec<AbortTransactionRequest> =>
+ buildCodecForObject<AbortTransactionRequest>()
+ .property("transactionId", codecForTransactionIdStr())
+ .build("AbortTransactionRequest");
+
export interface DepositGroupFees {
coin: AmountString;
wire: AmountString;
@@ -1564,16 +2310,17 @@ export interface DepositGroupFees {
}
export interface CreateDepositGroupRequest {
+ /**
+ * Pre-allocated transaction ID.
+ * Allows clients to easily handle notifications
+ * that occur while the operation has been created but
+ * before the creation request has returned.
+ */
+ transactionId?: TransactionIdStr;
depositPaytoUri: string;
amount: AmountString;
}
-export const codecForGetFeeForDeposit = (): Codec<GetFeeForDepositRequest> =>
- buildCodecForObject<GetFeeForDepositRequest>()
- .property("amount", codecForAmountString())
- .property("depositPaytoUri", codecForString())
- .build("GetFeeForDepositRequest");
-
export interface PrepareDepositRequest {
depositPaytoUri: string;
amount: AmountString;
@@ -1587,6 +2334,7 @@ export const codecForPrepareDepositRequest = (): Codec<PrepareDepositRequest> =>
export interface PrepareDepositResponse {
totalDepositCost: AmountString;
effectiveDepositAmount: AmountString;
+ fees: DepositGroupFees;
}
export const codecForCreateDepositGroupRequest =
@@ -1594,31 +2342,22 @@ export const codecForCreateDepositGroupRequest =
buildCodecForObject<CreateDepositGroupRequest>()
.property("amount", codecForAmountString())
.property("depositPaytoUri", codecForString())
+ .property("transactionId", codecOptional(codecForTransactionIdStr()))
.build("CreateDepositGroupRequest");
export interface CreateDepositGroupResponse {
depositGroupId: string;
- transactionId: string;
+ transactionId: TransactionIdStr;
}
-export interface TrackDepositGroupRequest {
- depositGroupId: string;
+export interface TxIdResponse {
+ transactionId: TransactionIdStr;
}
-export interface TrackDepositGroupResponse {
- responses: {
- status: number;
- body: any;
- }[];
-}
-
-export const codecForTrackDepositGroupRequest =
- (): Codec<TrackDepositGroupRequest> =>
- buildCodecForObject<TrackDepositGroupRequest>()
- .property("depositGroupId", codecForAmountString())
- .build("TrackDepositGroupRequest");
-
export interface WithdrawUriInfoResponse {
+ operationId: string;
+ status: WithdrawalOperationStatus;
+ confirmTransferUrl?: string;
amount: AmountString;
defaultExchangeBaseUrl?: string;
possibleExchanges: ExchangeListItem[];
@@ -1627,6 +2366,17 @@ export interface WithdrawUriInfoResponse {
export const codecForWithdrawUriInfoResponse =
(): Codec<WithdrawUriInfoResponse> =>
buildCodecForObject<WithdrawUriInfoResponse>()
+ .property("operationId", codecForString())
+ .property("confirmTransferUrl", codecOptional(codecForString()))
+ .property(
+ "status",
+ codecForEither(
+ codecForConstString("pending"),
+ codecForConstString("selected"),
+ codecForConstString("aborted"),
+ codecForConstString("confirmed"),
+ ),
+ )
.property("amount", codecForAmountString())
.property("defaultExchangeBaseUrl", codecOptional(codecForString()))
.property("possibleExchanges", codecForList(codecForExchangeListItem()))
@@ -1645,24 +2395,38 @@ export interface WalletCurrencyInfo {
}[];
}
+export interface TestingListTasksForTransactionRequest {
+ transactionId: TransactionIdStr;
+}
+
+export interface TestingListTasksForTransactionsResponse {
+ taskIdList: string[];
+}
+
+export const codecForTestingListTasksForTransactionRequest =
+ (): Codec<TestingListTasksForTransactionRequest> =>
+ buildCodecForObject<TestingListTasksForTransactionRequest>()
+ .property("transactionId", codecForTransactionIdStr())
+ .build("TestingListTasksForTransactionRequest");
+
export interface DeleteTransactionRequest {
- transactionId: string;
+ transactionId: TransactionIdStr;
}
export interface RetryTransactionRequest {
- transactionId: string;
+ transactionId: TransactionIdStr;
}
export const codecForDeleteTransactionRequest =
(): Codec<DeleteTransactionRequest> =>
buildCodecForObject<DeleteTransactionRequest>()
- .property("transactionId", codecForString())
+ .property("transactionId", codecForTransactionIdStr())
.build("DeleteTransactionRequest");
export const codecForRetryTransactionRequest =
(): Codec<RetryTransactionRequest> =>
buildCodecForObject<RetryTransactionRequest>()
- .property("transactionId", codecForString())
+ .property("transactionId", codecForTransactionIdStr())
.build("RetryTransactionRequest");
export interface SetWalletDeviceIdRequest {
@@ -1791,12 +2555,12 @@ interface AttentionBackupUnpaid {
interface AttentionMerchantRefund {
type: AttentionType.MerchantRefund;
- transactionId: string;
+ transactionId: TransactionIdStr;
}
interface AttentionKycWithdrawal {
type: AttentionType.KycWithdrawal;
- transactionId: string;
+ transactionId: TransactionIdStr;
}
interface AttentionExchangeTosChanged {
@@ -1826,17 +2590,17 @@ interface AttentionAuditorDenominationExpires {
}
interface AttentionPullPaymentPaid {
type: AttentionType.PullPaymentPaid;
- transactionId: string;
+ transactionId: TransactionIdStr;
}
interface AttentionPushPaymentReceived {
type: AttentionType.PushPaymentReceived;
- transactionId: string;
+ transactionId: TransactionIdStr;
}
export type UserAttentionUnreadList = Array<{
info: AttentionInfo;
- when: AbsoluteTime;
+ when: TalerPreciseTimestamp;
read: boolean;
}>;
@@ -1856,12 +2620,40 @@ export const codecForWithdrawFakebankRequest =
.property("exchange", codecForString())
.build("WithdrawFakebankRequest");
-export interface ImportDb {
+export interface ActiveTask {
+ taskId: string;
+ transaction: TransactionIdStr | undefined;
+ firstTry: AbsoluteTime | undefined;
+ nextTry: AbsoluteTime | undefined;
+ retryCounter: number | undefined;
+ lastError: TalerErrorDetail | undefined;
+}
+
+export interface GetActiveTasksResponse {
+ tasks: ActiveTask[];
+}
+
+export const codecForActiveTask = (): Codec<ActiveTask> =>
+ buildCodecForObject<ActiveTask>()
+ .property("taskId", codecForString())
+ .property("transaction", codecOptional(codecForTransactionIdStr()))
+ .property("retryCounter", codecOptional(codecForNumber()))
+ .property("firstTry", codecOptional(codecForAbsoluteTime))
+ .property("nextTry", codecOptional(codecForAbsoluteTime))
+ .property("lastError", codecOptional(codecForTalerErrorDetail()))
+ .build("ActiveTask");
+
+export const codecForGetActiveTasks = (): Codec<GetActiveTasksResponse> =>
+ buildCodecForObject<GetActiveTasksResponse>()
+ .property("tasks", codecForList(codecForActiveTask()))
+ .build("GetActiveTasks");
+
+export interface ImportDbRequest {
dump: any;
}
-export const codecForImportDbRequest = (): Codec<ImportDb> =>
- buildCodecForObject<ImportDb>()
+export const codecForImportDbRequest = (): Codec<ImportDbRequest> =>
+ buildCodecForObject<ImportDbRequest>()
.property("dump", codecForAny())
.build("ImportDbRequest");
@@ -1883,7 +2675,23 @@ export interface ForcedCoinSel {
}
export interface TestPayResult {
- payCoinSelection: PayCoinSelection;
+ /**
+ * Number of coins used for the payment.
+ */
+ numCoins: number;
+}
+
+export interface SelectedCoin {
+ denomPubHash: string;
+ coinPub: string;
+ contribution: AmountString;
+ exchangeBaseUrl: string;
+}
+
+export interface SelectedProspectiveCoin {
+ denomPubHash: string;
+ contribution: AmountString;
+ exchangeBaseUrl: string;
}
/**
@@ -1891,20 +2699,21 @@ export interface TestPayResult {
* coins with their denomination.
*/
export interface PayCoinSelection {
- /**
- * Amount requested by the merchant.
- */
- paymentAmount: AmountString;
+ coins: SelectedCoin[];
/**
- * Public keys of the coins that were selected.
+ * How much of the wire fees is the customer paying?
*/
- coinPubs: string[];
+ customerWireFees: AmountString;
/**
- * Amount that each coin contributes.
+ * How much of the deposit fees is the customer paying?
*/
- coinContributions: AmountString[];
+ customerDepositFees: AmountString;
+}
+
+export interface ProspectivePayCoinSelection {
+ prospectiveCoins: SelectedProspectiveCoin[];
/**
* How much of the wire fees is the customer paying?
@@ -1917,111 +2726,138 @@ export interface PayCoinSelection {
customerDepositFees: AmountString;
}
-export interface PreparePeerPushPaymentRequest {
+export interface CheckPeerPushDebitRequest {
+ /**
+ * Preferred exchange to use for the p2p payment.
+ */
exchangeBaseUrl?: string;
+
+ /**
+ * Instructed amount.
+ *
+ * FIXME: Allow specifying the instructed amount type.
+ */
amount: AmountString;
}
-export const codecForPreparePeerPushPaymentRequest =
- (): Codec<PreparePeerPushPaymentRequest> =>
- buildCodecForObject<PreparePeerPushPaymentRequest>()
+export const codecForCheckPeerPushDebitRequest =
+ (): Codec<CheckPeerPushDebitRequest> =>
+ buildCodecForObject<CheckPeerPushDebitRequest>()
.property("exchangeBaseUrl", codecOptional(codecForString()))
.property("amount", codecForAmountString())
- .build("InitiatePeerPushPaymentRequest");
+ .build("CheckPeerPushDebitRequest");
-export interface PreparePeerPushPaymentResponse {
+export interface CheckPeerPushDebitResponse {
amountRaw: AmountString;
amountEffective: AmountString;
+ exchangeBaseUrl: string;
+ /**
+ * Maximum expiration date, based on how close the coins
+ * used for the payment are to expiry.
+ *
+ * The value is based on when the wallet would typically
+ * automatically refresh the coins on its own, leaving enough
+ * time to get a refund for the push payment and refresh the
+ * coin.
+ */
+ maxExpirationDate: TalerProtocolTimestamp;
}
-export interface InitiatePeerPushPaymentRequest {
+export interface InitiatePeerPushDebitRequest {
exchangeBaseUrl?: string;
partialContractTerms: PeerContractTerms;
}
-export interface InitiatePeerPushPaymentResponse {
+export interface InitiatePeerPushDebitResponse {
exchangeBaseUrl: string;
pursePub: string;
mergePriv: string;
contractPriv: string;
- talerUri: string;
- transactionId: string;
+ transactionId: TransactionIdStr;
}
-export const codecForInitiatePeerPushPaymentRequest =
- (): Codec<InitiatePeerPushPaymentRequest> =>
- buildCodecForObject<InitiatePeerPushPaymentRequest>()
+export const codecForInitiatePeerPushDebitRequest =
+ (): Codec<InitiatePeerPushDebitRequest> =>
+ buildCodecForObject<InitiatePeerPushDebitRequest>()
.property("partialContractTerms", codecForPeerContractTerms())
- .build("InitiatePeerPushPaymentRequest");
+ .build("InitiatePeerPushDebitRequest");
-export interface CheckPeerPushPaymentRequest {
+export interface PreparePeerPushCreditRequest {
talerUri: string;
}
-export interface CheckPeerPullPaymentRequest {
+export interface PreparePeerPullDebitRequest {
talerUri: string;
}
-export interface CheckPeerPushPaymentResponse {
+export interface PreparePeerPushCreditResponse {
contractTerms: PeerContractTerms;
+ amountRaw: AmountString;
+ amountEffective: AmountString;
+
+ transactionId: TransactionIdStr;
+
+ exchangeBaseUrl: string;
+
+ /**
+ * @deprecated use transaction ID instead.
+ */
+ peerPushCreditId: string;
+
+ /**
+ * @deprecated
+ */
amount: AmountString;
- peerPushPaymentIncomingId: string;
}
-export interface CheckPeerPullPaymentResponse {
+export interface PreparePeerPullDebitResponse {
contractTerms: PeerContractTerms;
+ /**
+ * @deprecated Redundant field with bad name, will be removed soon.
+ */
amount: AmountString;
- peerPullPaymentIncomingId: string;
+
+ amountRaw: AmountString;
+ amountEffective: AmountString;
+
+ peerPullDebitId: string;
+
+ transactionId: TransactionIdStr;
}
-export const codecForCheckPeerPushPaymentRequest =
- (): Codec<CheckPeerPushPaymentRequest> =>
- buildCodecForObject<CheckPeerPushPaymentRequest>()
+export const codecForPreparePeerPushCreditRequest =
+ (): Codec<PreparePeerPushCreditRequest> =>
+ buildCodecForObject<PreparePeerPushCreditRequest>()
.property("talerUri", codecForString())
.build("CheckPeerPushPaymentRequest");
export const codecForCheckPeerPullPaymentRequest =
- (): Codec<CheckPeerPullPaymentRequest> =>
- buildCodecForObject<CheckPeerPullPaymentRequest>()
+ (): Codec<PreparePeerPullDebitRequest> =>
+ buildCodecForObject<PreparePeerPullDebitRequest>()
.property("talerUri", codecForString())
- .build("CheckPeerPullPaymentRequest");
+ .build("PreparePeerPullDebitRequest");
-export interface AcceptPeerPushPaymentRequest {
- /**
- * Transparent identifier of the incoming peer push payment.
- */
- peerPushPaymentIncomingId: string;
+export interface ConfirmPeerPushCreditRequest {
+ transactionId: string;
}
export interface AcceptPeerPushPaymentResponse {
- transactionId: string;
+ transactionId: TransactionIdStr;
}
export interface AcceptPeerPullPaymentResponse {
- transactionId: string;
+ transactionId: TransactionIdStr;
}
-export const codecForAcceptPeerPushPaymentRequest =
- (): Codec<AcceptPeerPushPaymentRequest> =>
- buildCodecForObject<AcceptPeerPushPaymentRequest>()
- .property("peerPushPaymentIncomingId", codecForString())
- .build("AcceptPeerPushPaymentRequest");
-
-export interface AcceptPeerPullPaymentRequest {
- /**
- * Transparent identifier of the incoming peer pull payment.
- */
- peerPullPaymentIncomingId: string;
-}
+export const codecForConfirmPeerPushPaymentRequest =
+ (): Codec<ConfirmPeerPushCreditRequest> =>
+ buildCodecForObject<ConfirmPeerPushCreditRequest>()
+ .property("transactionId", codecForString())
+ .build("ConfirmPeerPushCreditRequest");
-export interface SetDevModeRequest {
- devModeEnabled: boolean;
+export interface ConfirmPeerPullDebitRequest {
+ transactionId: TransactionIdStr;
}
-export const codecForSetDevModeRequest = (): Codec<SetDevModeRequest> =>
- buildCodecForObject<SetDevModeRequest>()
- .property("devModeEnabled", codecForBoolean())
- .build("SetDevModeRequest");
-
export interface ApplyDevExperimentRequest {
devExperimentUri: string;
}
@@ -2033,47 +2869,452 @@ export const codecForApplyDevExperiment =
.build("ApplyDevExperimentRequest");
export const codecForAcceptPeerPullPaymentRequest =
- (): Codec<AcceptPeerPullPaymentRequest> =>
- buildCodecForObject<AcceptPeerPullPaymentRequest>()
- .property("peerPullPaymentIncomingId", codecForString())
- .build("AcceptPeerPllPaymentRequest");
+ (): Codec<ConfirmPeerPullDebitRequest> =>
+ buildCodecForObject<ConfirmPeerPullDebitRequest>()
+ .property("transactionId", codecForTransactionIdStr())
+ .build("ConfirmPeerPullDebitRequest");
-export interface PreparePeerPullPaymentRequest {
- exchangeBaseUrl: string;
+export interface CheckPeerPullCreditRequest {
+ exchangeBaseUrl?: string;
amount: AmountString;
}
export const codecForPreparePeerPullPaymentRequest =
- (): Codec<PreparePeerPullPaymentRequest> =>
- buildCodecForObject<PreparePeerPullPaymentRequest>()
+ (): Codec<CheckPeerPullCreditRequest> =>
+ buildCodecForObject<CheckPeerPullCreditRequest>()
.property("amount", codecForAmountString())
- .property("exchangeBaseUrl", codecForString())
- .build("PreparePeerPullPaymentRequest");
+ .property("exchangeBaseUrl", codecOptional(codecForString()))
+ .build("CheckPeerPullCreditRequest");
-export interface PreparePeerPullPaymentResponse {
+export interface CheckPeerPullCreditResponse {
+ exchangeBaseUrl: string;
amountRaw: AmountString;
amountEffective: AmountString;
-}
-export interface InitiatePeerPullPaymentRequest {
+
/**
- * FIXME: Make this optional?
+ * Number of coins that will be used,
+ * can be used by the UI to warn if excessively large.
*/
- exchangeBaseUrl: string;
+ numCoins: number;
+}
+export interface InitiatePeerPullCreditRequest {
+ exchangeBaseUrl?: string;
partialContractTerms: PeerContractTerms;
}
export const codecForInitiatePeerPullPaymentRequest =
- (): Codec<InitiatePeerPullPaymentRequest> =>
- buildCodecForObject<InitiatePeerPullPaymentRequest>()
+ (): Codec<InitiatePeerPullCreditRequest> =>
+ buildCodecForObject<InitiatePeerPullCreditRequest>()
.property("partialContractTerms", codecForPeerContractTerms())
- .property("exchangeBaseUrl", codecForString())
- .build("InitiatePeerPullPaymentRequest");
+ .property("exchangeBaseUrl", codecOptional(codecForString()))
+ .build("InitiatePeerPullCreditRequest");
-export interface InitiatePeerPullPaymentResponse {
+export interface InitiatePeerPullCreditResponse {
/**
* Taler URI for the other party to make the payment
* that was requested.
+ *
+ * @deprecated since it's not necessarily valid yet until the tx is in the right state
*/
talerUri: string;
- transactionId: string;
+ transactionId: TransactionIdStr;
+}
+
+export interface ValidateIbanRequest {
+ iban: string;
+}
+
+export const codecForValidateIbanRequest = (): Codec<ValidateIbanRequest> =>
+ buildCodecForObject<ValidateIbanRequest>()
+ .property("iban", codecForString())
+ .build("ValidateIbanRequest");
+
+export interface ValidateIbanResponse {
+ valid: boolean;
+}
+
+export const codecForValidateIbanResponse = (): Codec<ValidateIbanResponse> =>
+ buildCodecForObject<ValidateIbanResponse>()
+ .property("valid", codecForBoolean())
+ .build("ValidateIbanResponse");
+
+export type TransactionStateFilter = "nonfinal";
+
+export interface TransactionRecordFilter {
+ onlyState?: TransactionStateFilter;
+ onlyCurrency?: string;
}
+
+export interface StoredBackupList {
+ storedBackups: {
+ name: string;
+ }[];
+}
+
+export interface CreateStoredBackupResponse {
+ name: string;
+}
+
+export interface RecoverStoredBackupRequest {
+ name: string;
+}
+
+export interface DeleteStoredBackupRequest {
+ name: string;
+}
+
+export const codecForDeleteStoredBackupRequest =
+ (): Codec<DeleteStoredBackupRequest> =>
+ buildCodecForObject<DeleteStoredBackupRequest>()
+ .property("name", codecForString())
+ .build("DeleteStoredBackupRequest");
+
+export const codecForRecoverStoredBackupRequest =
+ (): Codec<RecoverStoredBackupRequest> =>
+ buildCodecForObject<RecoverStoredBackupRequest>()
+ .property("name", codecForString())
+ .build("RecoverStoredBackupRequest");
+
+export interface TestingSetTimetravelRequest {
+ offsetMs: number;
+}
+
+export const codecForTestingSetTimetravelRequest =
+ (): Codec<TestingSetTimetravelRequest> =>
+ buildCodecForObject<TestingSetTimetravelRequest>()
+ .property("offsetMs", codecForNumber())
+ .build("TestingSetTimetravelRequest");
+
+export interface AllowedAuditorInfo {
+ auditorBaseUrl: string;
+ auditorPub: string;
+}
+
+export interface AllowedExchangeInfo {
+ exchangeBaseUrl: string;
+ exchangePub: string;
+}
+
+/**
+ * Data extracted from the contract terms that is relevant for payment
+ * processing in the wallet.
+ */
+export interface WalletContractData {
+ /**
+ * Fulfillment URL, or the empty string if the order has no fulfillment URL.
+ *
+ * Stored as a non-nullable string as we use this field for IndexedDB indexing.
+ */
+ fulfillmentUrl: string;
+
+ contractTermsHash: string;
+ fulfillmentMessage?: string;
+ fulfillmentMessageI18n?: InternationalizedString;
+ merchantSig: string;
+ merchantPub: string;
+ merchant: MerchantInfo;
+ amount: AmountString;
+ orderId: string;
+ merchantBaseUrl: string;
+ summary: string;
+ summaryI18n: { [lang_tag: string]: string } | undefined;
+ autoRefund: TalerProtocolDuration | undefined;
+ payDeadline: TalerProtocolTimestamp;
+ refundDeadline: TalerProtocolTimestamp;
+ allowedExchanges: AllowedExchangeInfo[];
+ timestamp: TalerProtocolTimestamp;
+ wireMethod: string;
+ wireInfoHash: string;
+ maxDepositFee: AmountString;
+ minimumAge?: number;
+}
+
+export interface TestingWaitTransactionRequest {
+ transactionId: TransactionIdStr;
+ txState: TransactionState;
+}
+
+export interface TestingGetDenomStatsRequest {
+ exchangeBaseUrl: string;
+}
+
+export interface TestingGetDenomStatsResponse {
+ numKnown: number;
+ numOffered: number;
+ numLost: number;
+}
+
+export const codecForTestingGetDenomStatsRequest =
+ (): Codec<TestingGetDenomStatsRequest> =>
+ buildCodecForObject<TestingGetDenomStatsRequest>()
+ .property("exchangeBaseUrl", codecForString())
+ .build("TestingGetDenomStatsRequest");
+
+export interface WithdrawalExchangeAccountDetails {
+ /**
+ * Payto URI to credit the exchange.
+ *
+ * Depending on whether the (manual!) withdrawal is accepted or just
+ * being checked, this already includes the subject with the
+ * reserve public key.
+ */
+ paytoUri: string;
+
+ /**
+ * Status that indicates whether the account can be used
+ * by the user to send funds for a withdrawal.
+ *
+ * ok: account should be shown to the user
+ * error: account should not be shown to the user, UIs might render the error (in conversionError),
+ * especially in dev mode.
+ */
+ status: "ok" | "error";
+
+ /**
+ * Transfer amount. Might be in a different currency than the requested
+ * amount for withdrawal.
+ *
+ * Absent if this is a conversion account and the conversion failed.
+ */
+ transferAmount?: AmountString;
+
+ /**
+ * Currency specification for the external currency.
+ *
+ * Only included if this account requires a currency conversion.
+ */
+ currencySpecification?: CurrencySpecification;
+
+ /**
+ * Further restrictions for sending money to the
+ * exchange.
+ */
+ creditRestrictions?: AccountRestriction[];
+
+ /**
+ * Label given to the account or the account's bank by the exchange.
+ */
+ bankLabel?: string;
+
+ /*
+ * Display priority assigned to this bank account by the exchange.
+ */
+ priority?: number;
+
+ /**
+ * Error that happened when attempting to request the conversion rate.
+ */
+ conversionError?: TalerErrorDetail;
+}
+
+export interface PrepareWithdrawExchangeRequest {
+ /**
+ * A taler://withdraw-exchange URI.
+ */
+ talerUri: string;
+}
+
+export const codecForPrepareWithdrawExchangeRequest =
+ (): Codec<PrepareWithdrawExchangeRequest> =>
+ buildCodecForObject<PrepareWithdrawExchangeRequest>()
+ .property("talerUri", codecForString())
+ .build("PrepareWithdrawExchangeRequest");
+
+export interface PrepareWithdrawExchangeResponse {
+ /**
+ * Base URL of the exchange that already existed
+ * or was ephemerally added as an exchange entry to
+ * the wallet.
+ */
+ exchangeBaseUrl: string;
+
+ /**
+ * Amount from the taler://withdraw-exchange URI.
+ * Only present if specified in the URI.
+ */
+ amount?: AmountString;
+}
+
+export interface ExchangeEntryState {
+ tosStatus: ExchangeTosStatus;
+ exchangeEntryStatus: ExchangeEntryStatus;
+ exchangeUpdateStatus: ExchangeUpdateStatus;
+}
+
+export interface ListGlobalCurrencyAuditorsResponse {
+ auditors: {
+ currency: string;
+ auditorBaseUrl: string;
+ auditorPub: string;
+ }[];
+}
+
+export interface ListGlobalCurrencyExchangesResponse {
+ exchanges: {
+ currency: string;
+ exchangeBaseUrl: string;
+ exchangeMasterPub: string;
+ }[];
+}
+
+export interface AddGlobalCurrencyExchangeRequest {
+ currency: string;
+ exchangeBaseUrl: string;
+ exchangeMasterPub: string;
+}
+
+export const codecForAddGlobalCurrencyExchangeRequest =
+ (): Codec<AddGlobalCurrencyExchangeRequest> =>
+ buildCodecForObject<AddGlobalCurrencyExchangeRequest>()
+ .property("currency", codecForString())
+ .property("exchangeBaseUrl", codecForString())
+ .property("exchangeMasterPub", codecForString())
+ .build("AddGlobalCurrencyExchangeRequest");
+
+export interface RemoveGlobalCurrencyExchangeRequest {
+ currency: string;
+ exchangeBaseUrl: string;
+ exchangeMasterPub: string;
+}
+
+export const codecForRemoveGlobalCurrencyExchangeRequest =
+ (): Codec<RemoveGlobalCurrencyExchangeRequest> =>
+ buildCodecForObject<RemoveGlobalCurrencyExchangeRequest>()
+ .property("currency", codecForString())
+ .property("exchangeBaseUrl", codecForString())
+ .property("exchangeMasterPub", codecForString())
+ .build("RemoveGlobalCurrencyExchangeRequest");
+
+export interface AddGlobalCurrencyAuditorRequest {
+ currency: string;
+ auditorBaseUrl: string;
+ auditorPub: string;
+}
+
+export const codecForAddGlobalCurrencyAuditorRequest =
+ (): Codec<AddGlobalCurrencyAuditorRequest> =>
+ buildCodecForObject<AddGlobalCurrencyAuditorRequest>()
+ .property("currency", codecForString())
+ .property("auditorBaseUrl", codecForString())
+ .property("auditorPub", codecForString())
+ .build("AddGlobalCurrencyAuditorRequest");
+
+export interface RemoveGlobalCurrencyAuditorRequest {
+ currency: string;
+ auditorBaseUrl: string;
+ auditorPub: string;
+}
+
+export const codecForRemoveGlobalCurrencyAuditorRequest =
+ (): Codec<RemoveGlobalCurrencyAuditorRequest> =>
+ buildCodecForObject<RemoveGlobalCurrencyAuditorRequest>()
+ .property("currency", codecForString())
+ .property("auditorBaseUrl", codecForString())
+ .property("auditorPub", codecForString())
+ .build("RemoveGlobalCurrencyAuditorRequest");
+
+/**
+ * Information about one provider.
+ *
+ * We don't store the account key here,
+ * as that's derived from the wallet root key.
+ */
+export interface ProviderInfo {
+ active: boolean;
+ syncProviderBaseUrl: string;
+ name: string;
+ terms?: BackupProviderTerms;
+ /**
+ * Last communication issue with the provider.
+ */
+ lastError?: TalerErrorDetail;
+ lastSuccessfulBackupTimestamp?: TalerPreciseTimestamp;
+ lastAttemptedBackupTimestamp?: TalerPreciseTimestamp;
+ paymentProposalIds: string[];
+ backupProblem?: BackupProblem;
+ paymentStatus: ProviderPaymentStatus;
+}
+
+export interface BackupProviderTerms {
+ supportedProtocolVersion: string;
+ annualFee: AmountString;
+ storageLimitInMegabytes: number;
+}
+
+export type BackupProblem =
+ | BackupUnreadableProblem
+ | BackupConflictingDeviceProblem;
+
+export interface BackupUnreadableProblem {
+ type: "backup-unreadable";
+}
+
+export interface BackupConflictingDeviceProblem {
+ type: "backup-conflicting-device";
+ otherDeviceId: string;
+ myDeviceId: string;
+ backupTimestamp: AbsoluteTime;
+}
+
+export type ProviderPaymentStatus =
+ | ProviderPaymentTermsChanged
+ | ProviderPaymentPaid
+ | ProviderPaymentInsufficientBalance
+ | ProviderPaymentUnpaid
+ | ProviderPaymentPending;
+
+export enum ProviderPaymentType {
+ Unpaid = "unpaid",
+ Pending = "pending",
+ InsufficientBalance = "insufficient-balance",
+ Paid = "paid",
+ TermsChanged = "terms-changed",
+}
+
+export interface ProviderPaymentUnpaid {
+ type: ProviderPaymentType.Unpaid;
+}
+
+export interface ProviderPaymentInsufficientBalance {
+ type: ProviderPaymentType.InsufficientBalance;
+ amount: AmountString;
+}
+
+export interface ProviderPaymentPending {
+ type: ProviderPaymentType.Pending;
+ talerUri?: string;
+}
+
+export interface ProviderPaymentPaid {
+ type: ProviderPaymentType.Paid;
+ paidUntil: AbsoluteTime;
+}
+
+export interface ProviderPaymentTermsChanged {
+ type: ProviderPaymentType.TermsChanged;
+ paidUntil: AbsoluteTime;
+ oldTerms: BackupProviderTerms;
+ newTerms: BackupProviderTerms;
+}
+
+// FIXME: Does not really belong here, move to sync API
+export interface SyncTermsOfServiceResponse {
+ // maximum backup size supported
+ storage_limit_in_megabytes: number;
+
+ // Fee for an account, per year.
+ annual_fee: AmountString;
+
+ // protocol version supported by the server,
+ // for now always "0.0".
+ version: string;
+}
+
+// FIXME: Does not really belong here, move to sync API
+export const codecForSyncTermsOfServiceResponse =
+ (): Codec<SyncTermsOfServiceResponse> =>
+ buildCodecForObject<SyncTermsOfServiceResponse>()
+ .property("storage_limit_in_megabytes", codecForNumber())
+ .property("annual_fee", codecForAmountString())
+ .property("version", codecForString())
+ .build("SyncTermsOfServiceResponse");
diff --git a/packages/taler-util/src/whatwg-url.ts b/packages/taler-util/src/whatwg-url.ts
index 991528ae6..13abf5397 100644
--- a/packages/taler-util/src/whatwg-url.ts
+++ b/packages/taler-util/src/whatwg-url.ts
@@ -1908,15 +1908,22 @@ function parseURL(
}
export class URLImpl {
- constructor(url: string, base?: string) {
+ //Include URL type for "url" and "base" params.
+ constructor(url: string | URL, base?: string | URL) {
let parsedBase = null;
if (base !== undefined) {
+ if (base instanceof URL) {
+ base = base.href;
+ }
parsedBase = basicURLParse(base);
if (parsedBase === null) {
throw new TypeError(`Invalid base URL: ${base}`);
}
}
+ if (url instanceof URL) {
+ url = url.href;
+ }
const parsedURL = basicURLParse(url, { baseURL: parsedBase });
if (parsedURL === null) {
throw new TypeError(`Invalid URL: ${url}`);
diff --git a/packages/taler-util/tsconfig.json b/packages/taler-util/tsconfig.json
index cbf393f5a..2e97142ce 100644
--- a/packages/taler-util/tsconfig.json
+++ b/packages/taler-util/tsconfig.json
@@ -4,11 +4,11 @@
"composite": true,
"declaration": true,
"declarationMap": false,
- "target": "ES6",
- "module": "ESNext",
+ "target": "ES2020",
+ "module": "Node16",
"moduleResolution": "Node16",
"sourceMap": true,
- "lib": ["es6"],
+ "lib": ["ES2020"],
"types": ["node"],
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
diff --git a/packages/taler-wallet-cli/Makefile b/packages/taler-wallet-cli/Makefile
index 4cd9aeb90..c8529c768 100644
--- a/packages/taler-wallet-cli/Makefile
+++ b/packages/taler-wallet-cli/Makefile
@@ -1,7 +1,14 @@
# This Makefile has been placed in the public domain.
--include ../../.config.mk
-include .config.mk
+ifeq ($(TOPLEVEL), yes)
+ $(info top-level build)
+ -include ../../.config.mk
+ override DESTDIR := $(TOP_DESTDIR)
+else
+ $(info package-level build)
+ -include ../../.config.mk
+ -include .config.mk
+endif
$(info prefix is $(prefix))
@@ -14,17 +21,31 @@ warn-noprefix:
@echo "no prefix configured, did you run ./configure?"
install: warn-noprefix
else
-install_target = $(prefix)/lib/taler-wallet-cli
-.PHONY: install install-nodeps
-install:
+LIBDIR = $(prefix)/lib/taler-wallet-cli
+BINDIR=$(prefix)/bin
+NODEDIR=$(LIBDIR)/node_modules/taler-wallet-cli
+.PHONY: install install-nodeps deps
+install-nodeps:
+ ./build-node.mjs
+ @echo installing wallet CLI to $(DESTDIR)$(prefix)
+ install -d $(DESTDIR)$(BINDIR)
+ install -d $(DESTDIR)$(LIBDIR)/build
+ install -d $(DESTDIR)$(NODEDIR)/bin
+ install -d $(DESTDIR)$(NODEDIR)/dist
+ install ./dist/taler-wallet-cli-bundled.cjs $(DESTDIR)$(NODEDIR)/dist/
+ install ./dist/taler-wallet-cli-bundled.cjs.map $(DESTDIR)$(NODEDIR)/dist/
+ install ./bin/taler-wallet-cli.mjs $(DESTDIR)$(NODEDIR)/bin/
+ install ../idb-bridge/node_modules/better-sqlite3/build/Release/better_sqlite3.node $(DESTDIR)$(LIBDIR)/build/ \
+ || echo "sqlite3 unavailable, better-sqlite3 native module not found"
+ ln -sf ../lib/taler-wallet-cli/node_modules/taler-wallet-cli/bin/taler-wallet-cli.mjs $(DESTDIR)$(BINDIR)/taler-wallet-cli
+deps:
pnpm install --frozen-lockfile --filter @gnu-taler/taler-wallet-cli...
- install -d $(prefix)/bin
- install -d $(install_target)/bin
- install -d $(install_target)/node_modules/taler-wallet-cli
- install -d $(install_target)/node_modules/taler-wallet-cli/bin
- install -d $(install_target)/node_modules/taler-wallet-cli/dist
- install ./dist/taler-wallet-cli.mjs $(install_target)/node_modules/taler-wallet-cli/dist/
- install ./dist/taler-wallet-cli.mjs.map $(install_target)/node_modules/taler-wallet-cli/dist/
- install ./bin/taler-wallet-cli.mjs $(install_target)/node_modules/taler-wallet-cli/bin/
- ln -sf $(install_target)/node_modules/taler-wallet-cli/bin/taler-wallet-cli.mjs $(prefix)/bin/taler-wallet-cli
+ pnpm run --filter @gnu-taler/taler-wallet-cli... compile
+install:
+ $(MAKE) deps
+ $(MAKE) install-nodeps
endif
+
+.PHONY: deb
+deb:
+ dpkg-buildpackage -rfakeroot -b -uc -us
diff --git a/packages/taler-wallet-cli/README.md b/packages/taler-wallet-cli/README.md
new file mode 100644
index 000000000..27b2373c9
--- /dev/null
+++ b/packages/taler-wallet-cli/README.md
@@ -0,0 +1,9 @@
+# taler-wallet-cli
+
+This package provides `taler-wallet-cli`, the command-line interface for the
+GNU Taler wallet.
+
+## sqlite3 backend
+
+To be able to use the sqlite3 backend, make sure that better-sqlite3
+is installed as an optional dependency in the ../idb-bridge package.
diff --git a/packages/taler-wallet-cli/assets/.gitkeep b/packages/taler-wallet-cli/assets/.gitkeep
deleted file mode 100644
index e69de29bb..000000000
--- a/packages/taler-wallet-cli/assets/.gitkeep
+++ /dev/null
diff --git a/packages/taler-wallet-cli/bin/taler-wallet-cli-local.mjs b/packages/taler-wallet-cli/bin/taler-wallet-cli-local.mjs
new file mode 100755
index 000000000..3620330b0
--- /dev/null
+++ b/packages/taler-wallet-cli/bin/taler-wallet-cli-local.mjs
@@ -0,0 +1,8 @@
+#!/usr/bin/env node
+
+// Execute the wallet CLI from the source directory.
+// This script is meant for testing and must not
+// be installed.
+
+import { main } from '../lib/index.js';
+main();
diff --git a/packages/taler-wallet-cli/bin/taler-wallet-cli.mjs b/packages/taler-wallet-cli/bin/taler-wallet-cli.mjs
index e3378471c..082007632 100755
--- a/packages/taler-wallet-cli/bin/taler-wallet-cli.mjs
+++ b/packages/taler-wallet-cli/bin/taler-wallet-cli.mjs
@@ -15,5 +15,5 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { main } from '../dist/taler-wallet-cli.mjs';
+import { main } from '../dist/taler-wallet-cli-bundled.cjs';
main();
diff --git a/packages/taler-wallet-cli/build-node.mjs b/packages/taler-wallet-cli/build-node.mjs
new file mode 100755
index 000000000..047fc4527
--- /dev/null
+++ b/packages/taler-wallet-cli/build-node.mjs
@@ -0,0 +1,75 @@
+#!/usr/bin/env node
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import esbuild from "esbuild";
+import path from "path";
+import fs from "fs";
+
+const BASE = process.cwd();
+
+let GIT_ROOT = BASE;
+while (!fs.existsSync(path.join(GIT_ROOT, ".git")) && GIT_ROOT !== "/") {
+ GIT_ROOT = path.join(GIT_ROOT, "../");
+}
+if (GIT_ROOT === "/") {
+ console.log("not found");
+ process.exit(1);
+}
+const GIT_HASH = GIT_ROOT === "/" ? undefined : git_hash();
+
+let _package = JSON.parse(fs.readFileSync(path.join(BASE, "package.json")));
+
+function git_hash() {
+ const rev = fs
+ .readFileSync(path.join(GIT_ROOT, ".git", "HEAD"))
+ .toString()
+ .trim()
+ .split(/.*[: ]/)
+ .slice(-1)[0];
+ if (rev.indexOf("/") === -1) {
+ return rev;
+ } else {
+ return fs
+ .readFileSync(path.join(GIT_ROOT, ".git", rev))
+ .toString()
+ .trim();
+ }
+}
+
+export const buildConfig = {
+ entryPoints: ["src/index.ts"],
+ outfile: "dist/taler-wallet-cli-bundled.cjs",
+ bundle: true,
+ minify: false,
+ target: ["es2020"],
+ format: "cjs",
+ platform: "node",
+ sourcemap: true,
+ inject: ["src/import-meta-url.js"],
+ define: {
+ __VERSION__: `"${_package.version}"`,
+ __GIT_HASH__: `"${GIT_HASH}"`,
+ "walletCoreBuildInfo.implementationSemver": `"${_package.version}"`,
+ "walletCoreBuildInfo.implementationGitHash": `"${GIT_HASH}"`,
+ ["import.meta.url"]: "import_meta_url",
+ },
+};
+
+esbuild.build(buildConfig).catch((e) => {
+ console.log(e);
+ process.exit(1);
+});
diff --git a/packages/taler-wallet-cli/build-qtart.mjs b/packages/taler-wallet-cli/build-qtart.mjs
new file mode 100755
index 000000000..04b2e718f
--- /dev/null
+++ b/packages/taler-wallet-cli/build-qtart.mjs
@@ -0,0 +1,77 @@
+#!/usr/bin/env node
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import esbuild from "esbuild";
+import path from "path";
+import fs from "fs";
+
+const BASE = process.cwd();
+
+let GIT_ROOT = BASE;
+while (!fs.existsSync(path.join(GIT_ROOT, ".git")) && GIT_ROOT !== "/") {
+ GIT_ROOT = path.join(GIT_ROOT, "../");
+}
+if (GIT_ROOT === "/") {
+ console.log("not found");
+ process.exit(1);
+}
+const GIT_HASH = GIT_ROOT === "/" ? undefined : git_hash();
+
+let _package = JSON.parse(fs.readFileSync(path.join(BASE, "package.json")));
+
+function git_hash() {
+ const rev = fs
+ .readFileSync(path.join(GIT_ROOT, ".git", "HEAD"))
+ .toString()
+ .trim()
+ .split(/.*[: ]/)
+ .slice(-1)[0];
+ if (rev.indexOf("/") === -1) {
+ return rev;
+ } else {
+ return fs
+ .readFileSync(path.join(GIT_ROOT, ".git", rev))
+ .toString()
+ .trim();
+ }
+}
+
+export const buildConfig = {
+ entryPoints: ["src/index.ts"],
+ outfile: "dist/taler-wallet-cli.qtart.mjs",
+ bundle: true,
+ minify: false,
+ target: ["es2020"],
+ format: "esm",
+ platform: "neutral",
+ mainFields: ["module", "main"],
+ conditions: ["qtart"],
+ sourcemap: true,
+ // quickjs standard library
+ external: ["std", "os", "better-sqlite3"],
+ define: {
+ __VERSION__: `"${_package.version}"`,
+ __GIT_HASH__: `"${GIT_HASH}"`,
+ "walletCoreBuildInfo.implementationSemver": `"${_package.version}"`,
+ "walletCoreBuildInfo.implementationGitHash": `"${GIT_HASH}"`,
+ },
+};
+
+esbuild.build(buildConfig).catch((e) => {
+ console.log(e);
+ process.exit(1);
+});
diff --git a/packages/taler-wallet-cli/build.mjs b/packages/taler-wallet-cli/build.mjs
deleted file mode 100755
index b6b3f4a16..000000000
--- a/packages/taler-wallet-cli/build.mjs
+++ /dev/null
@@ -1,79 +0,0 @@
-#!/usr/bin/env node
-/*
- This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-import esbuild from 'esbuild'
-import path from "path"
-import fs from "fs"
-
-const BASE = process.cwd()
-
-const preact = path.join(BASE, "node_modules", "preact", "compat", "dist", "compat.module.js");
-const preactCompatPlugin = {
- name: "preact-compat",
- setup(build) {
- build.onResolve({ filter: /^(react-dom|react)$/ }, args => ({ path: preact }));
- }
-}
-
-let GIT_ROOT = BASE
-while (!fs.existsSync(path.join(GIT_ROOT, '.git')) && GIT_ROOT !== '/') {
- GIT_ROOT = path.join(GIT_ROOT, '../')
-}
-if (GIT_ROOT === '/') {
- console.log("not found")
- process.exit(1);
-}
-const GIT_HASH = GIT_ROOT === '/' ? undefined : git_hash()
-
-
-let _package = JSON.parse(fs.readFileSync(path.join(BASE, 'package.json')));
-
-function git_hash() {
- const rev = fs.readFileSync(path.join(GIT_ROOT, '.git', 'HEAD')).toString().trim().split(/.*[: ]/).slice(-1)[0];
- if (rev.indexOf('/') === -1) {
- return rev;
- } else {
- return fs.readFileSync(path.join(GIT_ROOT, '.git', rev)).toString().trim();
- }
-}
-
-export const buildConfig = {
- entryPoints: ["src/index.ts"],
- outfile: "dist/taler-wallet-cli.cjs",
- bundle: true,
- minify: false,
- target: [
- 'es6'
- ],
- format: 'cjs',
- platform: 'node',
- sourcemap: true,
- jsxFactory: 'h',
- jsxFragment: 'Fragment',
- define: {
- '__VERSION__': `"${_package.version}"`,
- '__GIT_HASH__': `"${GIT_HASH}"`,
- },
-}
-
-esbuild
- .build(buildConfig)
- .catch((e) => {
- console.log(e)
- process.exit(1)
- });
-
diff --git a/packages/taler-wallet-cli/debian/README b/packages/taler-wallet-cli/debian/README
index 2b4f83aa2..d221c6e37 100644
--- a/packages/taler-wallet-cli/debian/README
+++ b/packages/taler-wallet-cli/debian/README
@@ -1,7 +1,8 @@
-For the moment, building the debian package needs
-a preliminary manual step to compile the taler-wallet-cli
+For the moment, building the Debian package needs
+a preliminary manual step to install the taler-wallet-cli
Node.JS package. In the future, this will either be invoked
by DH, or added as packaging instructions to Debian.
-$ pnpm run compile
+$ ./configure --prefix=/usr
+$ make install
$ dpkg-buildpackage -rfakeroot -b -uc -us
diff --git a/packages/taler-wallet-cli/debian/changelog b/packages/taler-wallet-cli/debian/changelog
index 50f265c16..e136caa61 100644
--- a/packages/taler-wallet-cli/debian/changelog
+++ b/packages/taler-wallet-cli/debian/changelog
@@ -1,3 +1,63 @@
+taler-wallet-cli (0.10.7) unstable; urgency=low
+
+ * Release 0.10.7
+
+ -- Florian Dold <dold@taler.net> Mon, 22 Apr 2024 20:16:39 +0200
+
+taler-wallet-cli (0.10.6) unstable; urgency=low
+
+ * Release 0.10.6
+
+ -- Florian Dold <dold@taler.net> Wed, 10 Apr 2024 15:19:33 +0200
+
+taler-wallet-cli (0.10.5) unstable; urgency=low
+
+ * Release 0.10.5
+
+ -- Florian Dold <dold@taler.net> Tue, 09 Apr 2024 16:13:58 +0200
+
+taler-wallet-cli (0.10.4) unstable; urgency=low
+
+ * Release 0.10.4
+
+ -- Florian Dold <dold@taler.net> Tue, 09 Apr 2024 14:39:44 +0200
+
+taler-wallet-cli (0.9.4a) unstable; urgency=low
+
+ * Release v0.9.4a.
+
+ -- Florian Dold <dold@taler.net> Thu, 07 Mar 2024 23:43:05 +0100
+
+taler-wallet-cli (0.9.3-1) unstable; urgency=low
+
+ * Misc bugfixes.
+
+ -- Christian Grothoff <grothoff@gnu.org> Tue, 13 Dec 2023 18:53:15 -0700
+
+taler-wallet-cli (0.9.3) unstable; urgency=low
+
+ * First release for GNU Taler v0.9.3.
+
+ -- Christian Grothoff <grothoff@gnu.org> Wed, 29 Nov 2023 08:53:15 -0800
+
+taler-wallet-cli (0.9.2-2) unstable; urgency=low
+
+ * Release with various Debian package fixes.
+
+ -- Christian Grothoff <grothoff@gnu.org> Sat, 3 Mar 2023 23:47:15 -0300
+
+taler-wallet-cli (0.9.2) unstable; urgency=low
+
+ * Official 0.9.2 release.
+
+ -- Christian Grothoff <grothoff@gnu.org> Sat, 25 Feb 2023 12:47:15 -0300
+
+taler-wallet-cli (0.9.1) unstable; urgency=low
+
+ * Official 0.9.1 release.
+
+ -- Christian Grothoff <grothoff@gnu.org> Thu, 26 Jan 2023 12:47:15 -0300
+
taler-wallet-cli (0.9.0) unstable; urgency=low
* Official 0.9.0 release.
diff --git a/packages/taler-wallet-cli/debian/control b/packages/taler-wallet-cli/debian/control
index d44a2e3ee..41b507480 100644
--- a/packages/taler-wallet-cli/debian/control
+++ b/packages/taler-wallet-cli/debian/control
@@ -13,4 +13,4 @@ Architecture: all
Depends: nodejs,
${misc:Depends}
Recommends:
-Description: Software package to test Taler installations.
+Description: This is a command-line interface version of the GNU Taler wallet.
diff --git a/packages/taler-wallet-cli/debian/rules b/packages/taler-wallet-cli/debian/rules
index b1b30ea82..0a8d6408c 100755
--- a/packages/taler-wallet-cli/debian/rules
+++ b/packages/taler-wallet-cli/debian/rules
@@ -1,32 +1,15 @@
#!/usr/bin/make -f
-include /usr/share/dpkg/default.mk
-TALER_WALLET_HOME = /usr/share/taler-wallet-cli
+%:
+ dh ${@}
-build: build-arch build-indep
-build-arch:
- true
-build-indep:
- true
-override_dh_auto_install:
- dh_install bin/taler-wallet-cli.mjs $(TALER_WALLET_HOME)/node_modules/taler-wallet-cli/bin
- dh_install dist/taler-wallet-cli.mjs $(TALER_WALLET_HOME)/node_modules/taler-wallet-cli/dist
- dh_install dist/taler-wallet-cli.mjs.map $(TALER_WALLET_HOME)/node_modules/taler-wallet-cli/dist
- dh_link $(TALER_WALLET_HOME)/node_modules/taler-wallet-cli/bin/taler-wallet-cli.mjs /usr/bin/taler-wallet-cli
+# Override because our configure doesn't like extra arguments.
+override_dh_auto_configure:
+ ./configure --prefix=/usr
override_dh_builddeb:
dh_builddeb -- -Zgzip
-binary:
- dh $@
-binary-arch:
- dh $@
-binary-indep:
- dh $@
-
-clean:
- true
-
# Override this step because it's very slow and likely
# unnecessary for us.
override_dh_strip_nondeterminism:
diff --git a/packages/taler-wallet-cli/package.json b/packages/taler-wallet-cli/package.json
index 455c5d0cb..922556749 100644
--- a/packages/taler-wallet-cli/package.json
+++ b/packages/taler-wallet-cli/package.json
@@ -1,9 +1,9 @@
{
"name": "@gnu-taler/taler-wallet-cli",
- "version": "0.9.0-dev.2",
+ "version": "0.10.7",
"description": "",
"engines": {
- "node": ">=0.12.0"
+ "node": ">=0.18.0"
},
"repository": {
"type": "git",
@@ -11,15 +11,15 @@
},
"author": "Florian Dold",
"license": "GPL-3.0",
- "main": "dist/taler-wallet-cli.mjs",
"bin": {
"taler-wallet-cli": "./bin/taler-wallet-cli.mjs"
},
"type": "module",
"scripts": {
- "prepare": "tsc && rollup -c",
- "compile": "tsc && rollup -c",
- "clean": "rimraf lib dist tsconfig.tsbuildinfo",
+ "compile": "tsc && ./build-node.mjs",
+ "test": "tsc",
+ "typedoc": "typedoc --out dist/typedoc ./src/",
+ "clean": "rm -rf lib dist tsconfig.tsbuildinfo",
"pretty": "prettier --write src"
},
"files": [
@@ -31,23 +31,14 @@
"src/"
],
"devDependencies": {
- "@rollup/plugin-commonjs": "^22.0.2",
- "@rollup/plugin-json": "^4.1.0",
- "@rollup/plugin-node-resolve": "^13.3.0",
- "@rollup/plugin-replace": "^4.0.0",
- "@types/node": "^18.8.5",
- "prettier": "^2.5.1",
- "rimraf": "^3.0.2",
- "rollup": "^2.79.0",
- "rollup-plugin-sourcemaps": "^0.6.3",
- "rollup-plugin-terser": "^7.0.2",
- "typedoc": "^0.23.16",
- "typescript": "^4.8.4"
+ "@types/node": "^18.11.17",
+ "prettier": "^3.1.1",
+ "typedoc": "^0.25.4",
+ "typescript": "^5.3.3"
},
"dependencies": {
"@gnu-taler/taler-util": "workspace:*",
"@gnu-taler/taler-wallet-core": "workspace:*",
- "axios": "^0.27.2",
- "tslib": "^2.4.0"
+ "tslib": "^2.6.2"
}
}
diff --git a/packages/taler-wallet-cli/rollup.config.js b/packages/taler-wallet-cli/rollup.config.js
deleted file mode 100644
index fd3eed97b..000000000
--- a/packages/taler-wallet-cli/rollup.config.js
+++ /dev/null
@@ -1,58 +0,0 @@
-// rollup.config.js
-import commonjs from "@rollup/plugin-commonjs";
-import nodeResolve from "@rollup/plugin-node-resolve";
-import json from "@rollup/plugin-json";
-import builtins from "builtin-modules";
-import pkg from "./package.json";
-import sourcemaps from "rollup-plugin-sourcemaps";
-import path from "path";
-import replace from "@rollup/plugin-replace";
-import child_process from 'child_process';
-
-const printedVersion = `${pkg.version}-${getGitRevision()}`
-
-export default {
- input: "lib/index.js",
- output: {
- file: pkg.main,
- format: "es",
- sourcemap: true,
- sourcemapPathTransform: (relativeSourcePath, sourcemapPath) => {
- // Transform to source map paths to virtual path. Otherwise,
- // error messages would contain paths that look like they should exist (relative to
- // the bundle) but don't.
- const res = path.normalize(
- path.join("/_walletsrc/packages/taler-wallet-cli/src/", relativeSourcePath),
- );
- return res;
- },
- },
- external: builtins,
- plugins: [
- replace({
- __VERSION__: `"${printedVersion}"`,
- preventAssignment: true,
- }),
-
- nodeResolve({
- preferBuiltins: true,
- exportConditions: ["node"],
- }),
-
- sourcemaps(),
-
- commonjs({
- sourceMap: true,
- transformMixedEsModules: true,
- }),
-
- json(),
- ],
-};
-
-function getGitRevision() {
- return child_process.execSync(`git rev-parse --short HEAD`, {
- encoding: 'utf-8',
- windowsHide: true,
- }).trim();
-}
diff --git a/packages/taler-wallet-cli/src/assets.ts b/packages/taler-wallet-cli/src/assets.ts
deleted file mode 100644
index 8e9306780..000000000
--- a/packages/taler-wallet-cli/src/assets.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- 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 path from "path";
-import fs from "fs";
-
-const assetFileUrl = import.meta.url;
-
-/**
- * Resolve an asset name into an absolute filename.
- *
- * The asset file should be placed in the "assets" directory
- * at the top level of the package (i.e. next to package.json).
- */
-export function resolveAsset(name: string): string {
- const n = path.basename(assetFileUrl);
- const d = path.dirname(assetFileUrl);
- let assetPath: string;
- // Currently both asset paths are the same.
- // This might change if the file that contains "resolveAsset"
- // ever moves. Thus, we're keeping the case distinction.
- if (n.endsWith("assets.js")) {
- // We're not bundled. Path is relative to the current file.
- assetPath = path.resolve(path.join(d, "..", "assets", name));
- } else if (n.endsWith("taler-wallet-cli.js")) {
- // We're bundled. Currently, this path is the same
- // FIXME: Take into account some ASSETS environment variable?
- assetPath = path.resolve(path.join(d, "..", "assets", name));
- } else {
- throw Error("Can't resolve asset (unknown)");
- }
- if (!fs.existsSync(assetPath)) {
- throw Error(`Asset '${name} not found'`);
- }
- return assetPath;
-}
diff --git a/packages/taler-wallet-cli/src/harness/helpers.ts b/packages/taler-wallet-cli/src/harness/helpers.ts
deleted file mode 100644
index affaccd61..000000000
--- a/packages/taler-wallet-cli/src/harness/helpers.ts
+++ /dev/null
@@ -1,444 +0,0 @@
-/*
- 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/>
- */
-
-/**
- * Helpers to create typical test environments.
- *
- * @author Florian Dold <dold@taler.net>
- */
-
-/**
- * Imports
- */
-import {
- AmountString,
- ConfirmPayResultType,
- MerchantContractTerms,
- Duration,
- PreparePayResultType,
-} from "@gnu-taler/taler-util";
-import {
- BankAccessApi,
- BankApi,
- HarnessExchangeBankAccount,
- WalletApiOperation,
-} from "@gnu-taler/taler-wallet-core";
-import { CoinConfig, defaultCoinConfig } from "./denomStructures.js";
-import {
- FaultInjectedExchangeService,
- FaultInjectedMerchantService,
-} from "./faultInjection.js";
-import {
- BankService,
- DbInfo,
- ExchangeService,
- ExchangeServiceInterface,
- getPayto,
- GlobalTestState,
- MerchantPrivateApi,
- MerchantService,
- MerchantServiceInterface,
- setupDb,
- WalletCli,
- WithAuthorization,
-} from "./harness.js";
-
-export interface SimpleTestEnvironment {
- commonDb: DbInfo;
- bank: BankService;
- exchange: ExchangeService;
- exchangeBankAccount: HarnessExchangeBankAccount;
- merchant: MerchantService;
- wallet: WalletCli;
-}
-
-export interface EnvOptions {
- /**
- * If provided, enable age restrictions with the specified age mask string.
- */
- ageMaskSpec?: string;
-
- mixedAgeRestriction?: boolean;
-}
-
-/**
- * Run a test case with a simple TESTKUDOS Taler environment, consisting
- * of one exchange, one bank and one merchant.
- */
-export async function createSimpleTestkudosEnvironment(
- t: GlobalTestState,
- coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")),
- opts: EnvOptions = {},
-): Promise<SimpleTestEnvironment> {
- const db = await setupDb(t);
-
- const bank = await BankService.create(t, {
- allowRegistrations: true,
- currency: "TESTKUDOS",
- database: db.connStr,
- httpPort: 8082,
- });
-
- const exchange = ExchangeService.create(t, {
- name: "testexchange-1",
- currency: "TESTKUDOS",
- httpPort: 8081,
- database: db.connStr,
- });
-
- 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 ageMaskSpec = opts.ageMaskSpec;
-
- if (ageMaskSpec) {
- exchange.enableAgeRestrictions(ageMaskSpec);
- // Enable age restriction for all coins.
- exchange.addCoinConfigList(
- coinConfig.map((x) => ({
- ...x,
- name: `${x.name}-age`,
- ageRestricted: true,
- })),
- );
- // For mixed age restrictions, we also offer coins without age restrictions
- if (opts.mixedAgeRestriction) {
- exchange.addCoinConfigList(
- coinConfig.map((x) => ({ ...x, ageRestricted: false })),
- );
- }
- } else {
- exchange.addCoinConfigList(coinConfig);
- }
-
- await exchange.start();
- await exchange.pingUntilAvailable();
-
- merchant.addExchange(exchange);
-
- await merchant.start();
- await merchant.pingUntilAvailable();
-
- await merchant.addInstance({
- id: "default",
- name: "Default Instance",
- paytoUris: [getPayto("merchant-default")],
- defaultWireTransferDelay: Duration.toTalerProtocolDuration(
- Duration.fromSpec({ minutes: 1 }),
- ),
- });
-
- await merchant.addInstance({
- id: "minst1",
- name: "minst1",
- paytoUris: [getPayto("minst1")],
- defaultWireTransferDelay: Duration.toTalerProtocolDuration(
- Duration.fromSpec({ minutes: 1 }),
- ),
- });
-
- console.log("setup done!");
-
- const wallet = new WalletCli(t);
-
- return {
- commonDb: db,
- exchange,
- merchant,
- wallet,
- bank,
- exchangeBankAccount,
- };
-}
-
-export interface FaultyMerchantTestEnvironment {
- commonDb: DbInfo;
- bank: BankService;
- exchange: ExchangeService;
- faultyExchange: FaultInjectedExchangeService;
- exchangeBankAccount: HarnessExchangeBankAccount;
- merchant: MerchantService;
- faultyMerchant: FaultInjectedMerchantService;
- wallet: WalletCli;
-}
-
-/**
- * Run a test case with a simple TESTKUDOS Taler environment, consisting
- * of one exchange, one bank and one merchant.
- */
-export async function createFaultInjectedMerchantTestkudosEnvironment(
- t: GlobalTestState,
-): Promise<FaultyMerchantTestEnvironment> {
- const db = await setupDb(t);
-
- const bank = await BankService.create(t, {
- allowRegistrations: true,
- currency: "TESTKUDOS",
- database: db.connStr,
- httpPort: 8082,
- });
-
- const exchange = ExchangeService.create(t, {
- name: "testexchange-1",
- currency: "TESTKUDOS",
- httpPort: 8081,
- database: db.connStr,
- });
-
- const merchant = await MerchantService.create(t, {
- name: "testmerchant-1",
- currency: "TESTKUDOS",
- httpPort: 8083,
- database: db.connStr,
- });
-
- const faultyMerchant = new FaultInjectedMerchantService(t, merchant, 9083);
- const faultyExchange = new FaultInjectedExchangeService(t, exchange, 9081);
-
- const exchangeBankAccount = await bank.createExchangeAccount(
- "myexchange",
- "x",
- );
- exchange.addBankAccount("1", exchangeBankAccount);
-
- bank.setSuggestedExchange(
- faultyExchange,
- exchangeBankAccount.accountPaytoUri,
- );
-
- await bank.start();
-
- await bank.pingUntilAvailable();
-
- exchange.addOfferedCoins(defaultCoinConfig);
-
- await exchange.start();
- await exchange.pingUntilAvailable();
-
- merchant.addExchange(faultyExchange);
-
- await merchant.start();
- await merchant.pingUntilAvailable();
-
- await merchant.addInstance({
- id: "default",
- name: "Default Instance",
- paytoUris: [getPayto("merchant-default")],
- });
-
- await merchant.addInstance({
- id: "minst1",
- name: "minst1",
- paytoUris: [getPayto("minst1")],
- });
-
- console.log("setup done!");
-
- const wallet = new WalletCli(t);
-
- return {
- commonDb: db,
- exchange,
- merchant,
- wallet,
- bank,
- exchangeBankAccount,
- faultyMerchant,
- faultyExchange,
- };
-}
-
-/**
- * Start withdrawing into the wallet.
- *
- * Only starts the operation, does not wait for it to finish.
- */
-export async function startWithdrawViaBank(
- t: GlobalTestState,
- p: {
- wallet: WalletCli;
- bank: BankService;
- exchange: ExchangeServiceInterface;
- amount: AmountString;
- restrictAge?: number;
- },
-): Promise<void> {
- const { wallet, bank, exchange, amount } = p;
-
- const user = await BankApi.createRandomBankUser(bank);
- const wop = await BankAccessApi.createWithdrawalOperation(bank, user, amount);
-
- // Hand it to the wallet
-
- await wallet.client.call(WalletApiOperation.GetWithdrawalDetailsForUri, {
- talerWithdrawUri: wop.taler_withdraw_uri,
- restrictAge: p.restrictAge,
- });
-
- await wallet.runPending();
-
- // Withdraw (AKA select)
-
- await wallet.client.call(WalletApiOperation.AcceptBankIntegratedWithdrawal, {
- exchangeBaseUrl: exchange.baseUrl,
- talerWithdrawUri: wop.taler_withdraw_uri,
- restrictAge: p.restrictAge,
- });
-
- // Confirm it
-
- await BankApi.confirmWithdrawalOperation(bank, user, wop);
-
- // We do *not* call runPending / runUntilDone on the wallet here.
- // Some tests rely on the final withdraw failing.
-}
-
-/**
- * Withdraw balance.
- */
-export async function withdrawViaBank(
- t: GlobalTestState,
- p: {
- wallet: WalletCli;
- bank: BankService;
- exchange: ExchangeServiceInterface;
- amount: AmountString;
- restrictAge?: number;
- },
-): Promise<void> {
- const { wallet } = p;
-
- await startWithdrawViaBank(t, p);
-
- await wallet.runUntilDone();
-
- // Check balance
-
- await wallet.client.call(WalletApiOperation.GetBalances, {});
-}
-
-export async function applyTimeTravel(
- timetravelDuration: Duration,
- s: {
- exchange?: ExchangeService;
- merchant?: MerchantService;
- wallet?: WalletCli;
- },
-): Promise<void> {
- if (s.exchange) {
- await s.exchange.stop();
- s.exchange.setTimetravel(timetravelDuration);
- await s.exchange.start();
- await s.exchange.pingUntilAvailable();
- }
-
- if (s.merchant) {
- await s.merchant.stop();
- s.merchant.setTimetravel(timetravelDuration);
- await s.merchant.start();
- await s.merchant.pingUntilAvailable();
- }
-
- if (s.wallet) {
- s.wallet.setTimetravel(timetravelDuration);
- }
-}
-
-/**
- * Make a simple payment and check that it succeeded.
- */
-export async function makeTestPayment(
- t: GlobalTestState,
- args: {
- merchant: MerchantServiceInterface;
- wallet: WalletCli;
- order: Partial<MerchantContractTerms>;
- instance?: string;
- },
- auth: WithAuthorization = {},
-): Promise<void> {
- // Set up order.
-
- const { wallet, merchant } = args;
- const instance = args.instance ?? "default";
-
- const orderResp = await MerchantPrivateApi.createOrder(
- merchant,
- instance,
- {
- order: args.order,
- },
- auth,
- );
-
- let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(
- merchant,
- {
- orderId: orderResp.order_id,
- },
- auth,
- );
-
- t.assertTrue(orderStatus.order_status === "unpaid");
-
- // Make wallet pay for the order
-
- const preparePayResult = await wallet.client.call(
- WalletApiOperation.PreparePayForUri,
- {
- talerPayUri: orderStatus.taler_pay_uri,
- },
- );
-
- t.assertTrue(
- preparePayResult.status === PreparePayResultType.PaymentPossible,
- );
-
- const r2 = await wallet.client.call(WalletApiOperation.ConfirmPay, {
- proposalId: preparePayResult.proposalId,
- });
-
- t.assertTrue(r2.type === ConfirmPayResultType.Done);
-
- // Check if payment was successful.
-
- orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(
- merchant,
- {
- orderId: orderResp.order_id,
- instance,
- },
- auth,
- );
-
- t.assertTrue(orderStatus.order_status === "paid");
-}
diff --git a/packages/taler-wallet-cli/src/harness/libeufin-apis.ts b/packages/taler-wallet-cli/src/harness/libeufin-apis.ts
deleted file mode 100644
index 538d8e93f..000000000
--- a/packages/taler-wallet-cli/src/harness/libeufin-apis.ts
+++ /dev/null
@@ -1,860 +0,0 @@
-/**
- * This file defines most of the API calls offered
- * by Nexus and Sandbox. They don't have state,
- * therefore got moved away from libeufin.ts where
- * the services get actually started and managed.
- */
-
-import axiosImp from "axios";
-const axios = axiosImp.default;
-import { URL } from "@gnu-taler/taler-util";
-
-export interface LibeufinSandboxServiceInterface {
- baseUrl: string;
-}
-
-export interface LibeufinNexusServiceInterface {
- baseUrl: string;
-}
-
-export interface CreateEbicsSubscriberRequest {
- hostID: string;
- userID: string;
- partnerID: string;
- systemID?: string;
-}
-
-export interface BankAccountInfo {
- iban: string;
- bic: string;
- name: string;
- label: string;
-}
-
-export interface CreateEbicsBankConnectionRequest {
- name: string; // connection name.
- ebicsURL: string;
- hostID: string;
- userID: string;
- partnerID: string;
- systemID?: string;
-}
-
-export interface UpdateNexusUserRequest {
- newPassword: string;
-}
-
-export interface NexusAuth {
- auth: {
- username: string;
- password: string;
- };
-}
-
-export interface PostNexusTaskRequest {
- name: string;
- cronspec: string;
- type: string; // fetch | submit
- params:
- | {
- level: string; // report | statement | all
- rangeType: string; // all | since-last | previous-days | latest
- }
- | {};
-}
-
-export interface CreateNexusUserRequest {
- username: string;
- password: string;
-}
-
-export interface PostNexusPermissionRequest {
- action: "revoke" | "grant";
- permission: {
- subjectType: string;
- subjectId: string;
- resourceType: string;
- resourceId: string;
- permissionName: string;
- };
-}
-
-export interface CreateAnastasisFacadeRequest {
- name: string;
- connectionName: string;
- accountName: string;
- currency: string;
- reserveTransferLevel: "report" | "statement" | "notification";
-}
-
-export interface CreateTalerWireGatewayFacadeRequest {
- name: string;
- connectionName: string;
- accountName: string;
- currency: string;
- reserveTransferLevel: "report" | "statement" | "notification";
-}
-
-export interface SandboxAccountTransactions {
- payments: {
- accountLabel: string;
- creditorIban: string;
- creditorBic?: string;
- creditorName: string;
- debtorIban: string;
- debtorBic: string;
- debtorName: string;
- amount: string;
- currency: string;
- subject: string;
- date: string;
- creditDebitIndicator: "debit" | "credit";
- accountServicerReference: string;
- }[];
-}
-
-export interface DeleteBankConnectionRequest {
- bankConnectionId: string;
-}
-
-export interface SimulateIncomingTransactionRequest {
- debtorIban: string;
- debtorBic: string;
- debtorName: string;
-
- /**
- * Subject / unstructured remittance info.
- */
- subject: string;
-
- /**
- * Decimal amount without currency.
- */
- amount: string;
-}
-
-export interface CreateEbicsBankAccountRequest {
- subscriber: {
- hostID: string;
- partnerID: string;
- userID: string;
- systemID?: string;
- };
- // IBAN
- iban: string;
- // BIC
- bic: string;
- // human name
- name: string;
- label: string;
-}
-
-export interface LibeufinSandboxAddIncomingRequest {
- creditorIban: string;
- creditorBic: string;
- creditorName: string;
- debtorIban: string;
- debtorBic: string;
- debtorName: string;
- subject: string;
- amount: string;
- currency: string;
- uid: string;
- direction: string;
-}
-
-function getRandomString(): string {
- return Math.random().toString(36).substring(2);
-}
-
-export namespace LibeufinSandboxApi {
- /**
- * Return balance and payto-address of 'accountLabel'.
- * Note: the demobank serving the request is hard-coded
- * inside the base URL, and therefore contained in
- * 'libeufinSandboxService'.
- */
- export async function demobankAccountInfo(
- username: string,
- password: string,
- libeufinSandboxService: LibeufinSandboxServiceInterface,
- accountLabel: string,
- ) {
- let url = new URL(
- `accounts/${accountLabel}`,
- libeufinSandboxService.baseUrl,
- );
- return await axios.get(url.href, {
- auth: {
- username: username,
- password: password,
- },
- });
- }
-
- // Creates one bank account via the Access API.
- export async function createDemobankAccount(
- username: string,
- password: string,
- libeufinSandboxService: LibeufinSandboxServiceInterface,
- ) {
- let url = new URL("testing/register", libeufinSandboxService.baseUrl);
- await axios.post(url.href, {
- username: username,
- password: password,
- });
- }
-
- export async function createDemobankEbicsSubscriber(
- req: CreateEbicsSubscriberRequest,
- demobankAccountLabel: string,
- libeufinSandboxService: LibeufinSandboxServiceInterface,
- username: string = "admin",
- password: string = "secret",
- ) {
- // baseUrl should already be pointed to one demobank.
- let url = new URL("ebics/subscribers", libeufinSandboxService.baseUrl);
- await axios.post(
- url.href,
- {
- userID: req.userID,
- hostID: req.hostID,
- partnerID: req.partnerID,
- demobankAccountLabel: demobankAccountLabel,
- },
- {
- auth: {
- username: "admin",
- password: "secret",
- },
- },
- );
- }
-
- export async function rotateKeys(
- libeufinSandboxService: LibeufinSandboxServiceInterface,
- hostID: string,
- ) {
- const baseUrl = libeufinSandboxService.baseUrl;
- let url = new URL(`admin/ebics/hosts/${hostID}/rotate-keys`, baseUrl);
- await axios.post(
- url.href,
- {},
- {
- auth: {
- username: "admin",
- password: "secret",
- },
- },
- );
- }
- export async function createEbicsHost(
- libeufinSandboxService: LibeufinSandboxServiceInterface,
- hostID: string,
- ) {
- const baseUrl = libeufinSandboxService.baseUrl;
- let url = new URL("admin/ebics/hosts", baseUrl);
- await axios.post(
- url.href,
- {
- hostID,
- ebicsVersion: "2.5",
- },
- {
- auth: {
- username: "admin",
- password: "secret",
- },
- },
- );
- }
-
- export async function createBankAccount(
- libeufinSandboxService: LibeufinSandboxServiceInterface,
- req: BankAccountInfo,
- ) {
- const baseUrl = libeufinSandboxService.baseUrl;
- let url = new URL(`admin/bank-accounts/${req.label}`, baseUrl);
- await axios.post(url.href, req, {
- auth: {
- username: "admin",
- password: "secret",
- },
- });
- }
-
- /**
- * This function is useless. It creates a Ebics subscriber
- * but never gives it a bank account. To be removed
- */
- export async function createEbicsSubscriber(
- libeufinSandboxService: LibeufinSandboxServiceInterface,
- req: CreateEbicsSubscriberRequest,
- ) {
- const baseUrl = libeufinSandboxService.baseUrl;
- let url = new URL("admin/ebics/subscribers", baseUrl);
- await axios.post(url.href, req, {
- auth: {
- username: "admin",
- password: "secret",
- },
- });
- }
-
- export async function createEbicsBankAccount(
- libeufinSandboxService: LibeufinSandboxServiceInterface,
- req: CreateEbicsBankAccountRequest,
- ) {
- const baseUrl = libeufinSandboxService.baseUrl;
- let url = new URL("admin/ebics/bank-accounts", baseUrl);
- await axios.post(url.href, req, {
- auth: {
- username: "admin",
- password: "secret",
- },
- });
- }
-
- export async function simulateIncomingTransaction(
- libeufinSandboxService: LibeufinSandboxServiceInterface,
- accountLabel: string,
- req: SimulateIncomingTransactionRequest,
- ) {
- const baseUrl = libeufinSandboxService.baseUrl;
- let url = new URL(
- `admin/bank-accounts/${accountLabel}/simulate-incoming-transaction`,
- baseUrl,
- );
- await axios.post(url.href, req, {
- auth: {
- username: "admin",
- password: "secret",
- },
- });
- }
-
- export async function getAccountTransactions(
- libeufinSandboxService: LibeufinSandboxServiceInterface,
- accountLabel: string,
- ): Promise<SandboxAccountTransactions> {
- const baseUrl = libeufinSandboxService.baseUrl;
- let url = new URL(
- `admin/bank-accounts/${accountLabel}/transactions`,
- baseUrl,
- );
- const res = await axios.get(url.href, {
- auth: {
- username: "admin",
- password: "secret",
- },
- });
- return res.data as SandboxAccountTransactions;
- }
-
- export async function getCamt053(
- libeufinSandboxService: LibeufinSandboxServiceInterface,
- accountLabel: string,
- ): Promise<any> {
- const baseUrl = libeufinSandboxService.baseUrl;
- let url = new URL("admin/payments/camt", baseUrl);
- return await axios.post(
- url.href,
- {
- bankaccount: accountLabel,
- type: 53,
- },
- {
- auth: {
- username: "admin",
- password: "secret",
- },
- },
- );
- }
-
- export async function getAccountInfoWithBalance(
- libeufinSandboxService: LibeufinSandboxServiceInterface,
- accountLabel: string,
- ): Promise<any> {
- const baseUrl = libeufinSandboxService.baseUrl;
- let url = new URL(`admin/bank-accounts/${accountLabel}`, baseUrl);
- return await axios.get(url.href, {
- auth: {
- username: "admin",
- password: "secret",
- },
- });
- }
-}
-
-export namespace LibeufinNexusApi {
- export async function getAllConnections(
- nexus: LibeufinNexusServiceInterface,
- ): Promise<any> {
- let url = new URL("bank-connections", nexus.baseUrl);
- const res = await axios.get(url.href, {
- auth: {
- username: "admin",
- password: "test",
- },
- });
- return res;
- }
-
- export async function deleteBankConnection(
- libeufinNexusService: LibeufinNexusServiceInterface,
- req: DeleteBankConnectionRequest,
- ): Promise<any> {
- const baseUrl = libeufinNexusService.baseUrl;
- let url = new URL("bank-connections/delete-connection", baseUrl);
- return await axios.post(url.href, req, {
- auth: {
- username: "admin",
- password: "test",
- },
- });
- }
-
- export async function createEbicsBankConnection(
- libeufinNexusService: LibeufinNexusServiceInterface,
- req: CreateEbicsBankConnectionRequest,
- ): Promise<void> {
- const baseUrl = libeufinNexusService.baseUrl;
- let url = new URL("bank-connections", baseUrl);
- await axios.post(
- url.href,
- {
- source: "new",
- type: "ebics",
- name: req.name,
- data: {
- ebicsURL: req.ebicsURL,
- hostID: req.hostID,
- userID: req.userID,
- partnerID: req.partnerID,
- systemID: req.systemID,
- },
- },
- {
- auth: {
- username: "admin",
- password: "test",
- },
- },
- );
- }
-
- export async function getBankAccount(
- libeufinNexusService: LibeufinNexusServiceInterface,
- accountName: string,
- ): Promise<any> {
- const baseUrl = libeufinNexusService.baseUrl;
- let url = new URL(`bank-accounts/${accountName}`, baseUrl);
- return await axios.get(url.href, {
- auth: {
- username: "admin",
- password: "test",
- },
- });
- }
-
- export async function submitInitiatedPayment(
- libeufinNexusService: LibeufinNexusServiceInterface,
- accountName: string,
- paymentId: string,
- ): Promise<void> {
- const baseUrl = libeufinNexusService.baseUrl;
- let url = new URL(
- `bank-accounts/${accountName}/payment-initiations/${paymentId}/submit`,
- baseUrl,
- );
- await axios.post(
- url.href,
- {},
- {
- auth: {
- username: "admin",
- password: "test",
- },
- },
- );
- }
-
- export async function fetchAccounts(
- libeufinNexusService: LibeufinNexusServiceInterface,
- connectionName: string,
- ): Promise<void> {
- const baseUrl = libeufinNexusService.baseUrl;
- let url = new URL(
- `bank-connections/${connectionName}/fetch-accounts`,
- baseUrl,
- );
- await axios.post(
- url.href,
- {},
- {
- auth: {
- username: "admin",
- password: "test",
- },
- },
- );
- }
-
- export async function importConnectionAccount(
- libeufinNexusService: LibeufinNexusServiceInterface,
- connectionName: string,
- offeredAccountId: string,
- nexusBankAccountId: string,
- ): Promise<void> {
- const baseUrl = libeufinNexusService.baseUrl;
- let url = new URL(
- `bank-connections/${connectionName}/import-account`,
- baseUrl,
- );
- await axios.post(
- url.href,
- {
- offeredAccountId,
- nexusBankAccountId,
- },
- {
- auth: {
- username: "admin",
- password: "test",
- },
- },
- );
- }
-
- export async function connectBankConnection(
- libeufinNexusService: LibeufinNexusServiceInterface,
- connectionName: string,
- ) {
- const baseUrl = libeufinNexusService.baseUrl;
- let url = new URL(`bank-connections/${connectionName}/connect`, baseUrl);
- await axios.post(
- url.href,
- {},
- {
- auth: {
- username: "admin",
- password: "test",
- },
- },
- );
- }
-
- export async function getPaymentInitiations(
- libeufinNexusService: LibeufinNexusServiceInterface,
- accountName: string,
- username: string = "admin",
- password: string = "test",
- ): Promise<void> {
- const baseUrl = libeufinNexusService.baseUrl;
- let url = new URL(
- `/bank-accounts/${accountName}/payment-initiations`,
- baseUrl,
- );
- let response = await axios.get(url.href, {
- auth: {
- username: username,
- password: password,
- },
- });
- console.log(
- `Payment initiations of: ${accountName}`,
- JSON.stringify(response.data, null, 2),
- );
- }
-
- export async function getConfig(
- libeufinNexusService: LibeufinNexusServiceInterface,
- ): Promise<void> {
- const baseUrl = libeufinNexusService.baseUrl;
- let url = new URL(`/config`, baseUrl);
- let response = await axios.get(url.href);
- }
-
- // Uses the Anastasis API to get a list of transactions.
- export async function getAnastasisTransactions(
- libeufinNexusService: LibeufinNexusServiceInterface,
- anastasisBaseUrl: string,
- params: {}, // of the request: {delta: 5, ..}
- username: string = "admin",
- password: string = "test",
- ): Promise<any> {
- let url = new URL("history/incoming", anastasisBaseUrl);
- let response = await axios.get(url.href, {
- params: params,
- auth: {
- username: username,
- password: password,
- },
- });
- return response;
- }
-
- // FIXME: this function should return some structured
- // object that represents a history.
- export async function getAccountTransactions(
- libeufinNexusService: LibeufinNexusServiceInterface,
- accountName: string,
- username: string = "admin",
- password: string = "test",
- ): Promise<any> {
- const baseUrl = libeufinNexusService.baseUrl;
- let url = new URL(`/bank-accounts/${accountName}/transactions`, baseUrl);
- let response = await axios.get(url.href, {
- auth: {
- username: username,
- password: password,
- },
- });
- return response;
- }
-
- export async function fetchTransactions(
- libeufinNexusService: LibeufinNexusServiceInterface,
- accountName: string,
- rangeType: string = "all",
- level: string = "report",
- username: string = "admin",
- password: string = "test",
- ): Promise<any> {
- const baseUrl = libeufinNexusService.baseUrl;
- let url = new URL(
- `/bank-accounts/${accountName}/fetch-transactions`,
- baseUrl,
- );
- return await axios.post(
- url.href,
- {
- rangeType: rangeType,
- level: level,
- },
- {
- auth: {
- username: username,
- password: password,
- },
- },
- );
- }
-
- export async function changePassword(
- libeufinNexusService: LibeufinNexusServiceInterface,
- username: string,
- req: UpdateNexusUserRequest,
- auth: NexusAuth,
- ) {
- const baseUrl = libeufinNexusService.baseUrl;
- let url = new URL(`/users/${username}/password`, baseUrl);
- await axios.post(url.href, req, auth);
- }
-
- export async function getUser(
- libeufinNexusService: LibeufinNexusServiceInterface,
- auth: NexusAuth,
- ): Promise<any> {
- const baseUrl = libeufinNexusService.baseUrl;
- let url = new URL(`/user`, baseUrl);
- return await axios.get(url.href, auth);
- }
-
- export async function createUser(
- libeufinNexusService: LibeufinNexusServiceInterface,
- req: CreateNexusUserRequest,
- ) {
- const baseUrl = libeufinNexusService.baseUrl;
- let url = new URL(`/users`, baseUrl);
- await axios.post(url.href, req, {
- auth: {
- username: "admin",
- password: "test",
- },
- });
- }
-
- export async function getAllPermissions(
- libeufinNexusService: LibeufinNexusServiceInterface,
- ): Promise<any> {
- const baseUrl = libeufinNexusService.baseUrl;
- let url = new URL(`/permissions`, baseUrl);
- return await axios.get(url.href, {
- auth: {
- username: "admin",
- password: "test",
- },
- });
- }
-
- export async function postPermission(
- libeufinNexusService: LibeufinNexusServiceInterface,
- req: PostNexusPermissionRequest,
- ) {
- const baseUrl = libeufinNexusService.baseUrl;
- let url = new URL(`/permissions`, baseUrl);
- await axios.post(url.href, req, {
- auth: {
- username: "admin",
- password: "test",
- },
- });
- }
-
- export async function getTasks(
- libeufinNexusService: LibeufinNexusServiceInterface,
- bankAccountName: string,
- // When void, the request returns the list of all the
- // tasks under this bank account.
- taskName: string | void,
- ): Promise<any> {
- const baseUrl = libeufinNexusService.baseUrl;
- let url = new URL(`/bank-accounts/${bankAccountName}/schedule`, baseUrl);
- if (taskName) url = new URL(taskName, `${url}/`);
-
- // It's caller's responsibility to interpret the response.
- return await axios.get(url.href, {
- auth: {
- username: "admin",
- password: "test",
- },
- });
- }
-
- export async function deleteTask(
- libeufinNexusService: LibeufinNexusServiceInterface,
- bankAccountName: string,
- taskName: string,
- ) {
- const baseUrl = libeufinNexusService.baseUrl;
- let url = new URL(
- `/bank-accounts/${bankAccountName}/schedule/${taskName}`,
- baseUrl,
- );
- await axios.delete(url.href, {
- auth: {
- username: "admin",
- password: "test",
- },
- });
- }
-
- export async function postTask(
- libeufinNexusService: LibeufinNexusServiceInterface,
- bankAccountName: string,
- req: PostNexusTaskRequest,
- ): Promise<any> {
- const baseUrl = libeufinNexusService.baseUrl;
- let url = new URL(`/bank-accounts/${bankAccountName}/schedule`, baseUrl);
- return await axios.post(url.href, req, {
- auth: {
- username: "admin",
- password: "test",
- },
- });
- }
-
- export async function deleteFacade(
- libeufinNexusService: LibeufinNexusServiceInterface,
- facadeName: string,
- ): Promise<any> {
- const baseUrl = libeufinNexusService.baseUrl;
- let url = new URL(`facades/${facadeName}`, baseUrl);
- return await axios.delete(url.href, {
- auth: {
- username: "admin",
- password: "test",
- },
- });
- }
-
- export async function getAllFacades(
- libeufinNexusService: LibeufinNexusServiceInterface,
- ): Promise<any> {
- const baseUrl = libeufinNexusService.baseUrl;
- let url = new URL("facades", baseUrl);
- return await axios.get(url.href, {
- auth: {
- username: "admin",
- password: "test",
- },
- });
- }
-
- export async function createAnastasisFacade(
- libeufinNexusService: LibeufinNexusServiceInterface,
- req: CreateAnastasisFacadeRequest,
- ) {
- const baseUrl = libeufinNexusService.baseUrl;
- let url = new URL("facades", baseUrl);
- await axios.post(
- url.href,
- {
- name: req.name,
- type: "anastasis",
- config: {
- bankAccount: req.accountName,
- bankConnection: req.connectionName,
- currency: req.currency,
- reserveTransferLevel: req.reserveTransferLevel,
- },
- },
- {
- auth: {
- username: "admin",
- password: "test",
- },
- },
- );
- }
-
- export async function createTwgFacade(
- libeufinNexusService: LibeufinNexusServiceInterface,
- req: CreateTalerWireGatewayFacadeRequest,
- ) {
- const baseUrl = libeufinNexusService.baseUrl;
- let url = new URL("facades", baseUrl);
- await axios.post(
- url.href,
- {
- name: req.name,
- type: "taler-wire-gateway",
- config: {
- bankAccount: req.accountName,
- bankConnection: req.connectionName,
- currency: req.currency,
- reserveTransferLevel: req.reserveTransferLevel,
- },
- },
- {
- auth: {
- username: "admin",
- password: "test",
- },
- },
- );
- }
-
- export async function submitAllPaymentInitiations(
- libeufinNexusService: LibeufinNexusServiceInterface,
- accountId: string,
- ) {
- const baseUrl = libeufinNexusService.baseUrl;
- let url = new URL(
- `/bank-accounts/${accountId}/submit-all-payment-initiations`,
- baseUrl,
- );
- await axios.post(
- url.href,
- {},
- {
- auth: {
- username: "admin",
- password: "test",
- },
- },
- );
- }
-}
diff --git a/packages/taler-wallet-cli/src/harness/libeufin.ts b/packages/taler-wallet-cli/src/harness/libeufin.ts
deleted file mode 100644
index bc210d132..000000000
--- a/packages/taler-wallet-cli/src/harness/libeufin.ts
+++ /dev/null
@@ -1,886 +0,0 @@
-/*
- 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/>
- */
-
-/**
- * This file defines euFin test logic that needs state
- * and that depends on the main harness.ts. The other
- * definitions - mainly helper functions to call RESTful
- * APIs - moved to libeufin-apis.ts. That enables harness.ts
- * to depend on such API calls, in contrast to the previous
- * situation where harness.ts had to include this file causing
- * a circular dependency. */
-
-/**
- * Imports.
- */
-import axios from "axios";
-import { URL } from "@gnu-taler/taler-util";
-import {
- GlobalTestState,
- DbInfo,
- pingProc,
- ProcessWrapper,
- runCommand,
- setupDb,
- sh,
- getRandomIban,
-} from "../harness/harness.js";
-import {
- LibeufinSandboxApi,
- LibeufinNexusApi,
- CreateEbicsBankAccountRequest,
- LibeufinSandboxServiceInterface,
- CreateTalerWireGatewayFacadeRequest,
- SimulateIncomingTransactionRequest,
- SandboxAccountTransactions,
- DeleteBankConnectionRequest,
- CreateEbicsBankConnectionRequest,
- UpdateNexusUserRequest,
- NexusAuth,
- CreateAnastasisFacadeRequest,
- PostNexusTaskRequest,
- PostNexusPermissionRequest,
- CreateNexusUserRequest,
-} from "../harness/libeufin-apis.js";
-
-export { LibeufinSandboxApi, LibeufinNexusApi };
-
-export interface LibeufinServices {
- libeufinSandbox: LibeufinSandboxService;
- libeufinNexus: LibeufinNexusService;
- commonDb: DbInfo;
-}
-
-export interface LibeufinSandboxConfig {
- httpPort: number;
- databaseJdbcUri: string;
-}
-
-export interface LibeufinNexusConfig {
- httpPort: number;
- databaseJdbcUri: string;
-}
-
-interface LibeufinNexusMoneyMovement {
- amount: string;
- creditDebitIndicator: string;
- details: {
- debtor: {
- name: string;
- };
- debtorAccount: {
- iban: string;
- };
- debtorAgent: {
- bic: string;
- };
- creditor: {
- name: string;
- };
- creditorAccount: {
- iban: string;
- };
- creditorAgent: {
- bic: string;
- };
- endToEndId: string;
- unstructuredRemittanceInformation: string;
- };
-}
-
-interface LibeufinNexusBatches {
- batchTransactions: Array<LibeufinNexusMoneyMovement>;
-}
-
-interface LibeufinNexusTransaction {
- amount: string;
- creditDebitIndicator: string;
- status: string;
- bankTransactionCode: string;
- valueDate: string;
- bookingDate: string;
- accountServicerRef: string;
- batches: Array<LibeufinNexusBatches>;
-}
-
-interface LibeufinNexusTransactions {
- transactions: Array<LibeufinNexusTransaction>;
-}
-
-export interface LibeufinCliDetails {
- nexusUrl: string;
- sandboxUrl: string;
- nexusDatabaseUri: string;
- sandboxDatabaseUri: string;
- user: LibeufinNexusUser;
-}
-
-export interface LibeufinEbicsSubscriberDetails {
- hostId: string;
- partnerId: string;
- userId: string;
-}
-
-export interface LibeufinEbicsConnectionDetails {
- subscriberDetails: LibeufinEbicsSubscriberDetails;
- ebicsUrl: string;
- connectionName: string;
-}
-
-export interface LibeufinBankAccountDetails {
- currency: string;
- iban: string;
- bic: string;
- personName: string;
- accountName: string;
-}
-
-export interface LibeufinNexusUser {
- username: string;
- password: string;
-}
-
-export interface LibeufinBackupFileDetails {
- passphrase: string;
- outputFile: string;
- connectionName: string;
-}
-
-export interface LibeufinKeyLetterDetails {
- outputFile: string;
- connectionName: string;
-}
-
-export interface LibeufinBankAccountImportDetails {
- offeredBankAccountName: string;
- nexusBankAccountName: string;
- connectionName: string;
-}
-
-export interface LibeufinPreparedPaymentDetails {
- creditorIban: string;
- creditorBic: string;
- creditorName: string;
- subject: string;
- amount: string;
- currency: string;
- nexusBankAccountName: string;
-}
-
-export class LibeufinSandboxService implements LibeufinSandboxServiceInterface {
- static async create(
- gc: GlobalTestState,
- sandboxConfig: LibeufinSandboxConfig,
- ): Promise<LibeufinSandboxService> {
- return new LibeufinSandboxService(gc, sandboxConfig);
- }
-
- sandboxProc: ProcessWrapper | undefined;
- globalTestState: GlobalTestState;
-
- constructor(
- gc: GlobalTestState,
- private sandboxConfig: LibeufinSandboxConfig,
- ) {
- this.globalTestState = gc;
- }
-
- get baseUrl(): string {
- return `http://localhost:${this.sandboxConfig.httpPort}/`;
- }
-
- async start(): Promise<void> {
- await sh(
- this.globalTestState,
- "libeufin-sandbox-config",
- "libeufin-sandbox config default",
- {
- ...process.env,
- LIBEUFIN_SANDBOX_DB_CONNECTION: this.sandboxConfig.databaseJdbcUri,
- },
- );
-
- this.sandboxProc = this.globalTestState.spawnService(
- "libeufin-sandbox",
- ["serve", "--port", `${this.sandboxConfig.httpPort}`],
- "libeufin-sandbox",
- {
- ...process.env,
- LIBEUFIN_SANDBOX_DB_CONNECTION: this.sandboxConfig.databaseJdbcUri,
- LIBEUFIN_SANDBOX_ADMIN_PASSWORD: "secret",
- },
- );
- }
-
- async c53tick(): Promise<string> {
- const stdout = await sh(
- this.globalTestState,
- "libeufin-sandbox-c53tick",
- "libeufin-sandbox camt053tick",
- {
- ...process.env,
- LIBEUFIN_SANDBOX_DB_CONNECTION: this.sandboxConfig.databaseJdbcUri,
- },
- );
- return stdout;
- }
-
- async makeTransaction(
- debit: string,
- credit: string,
- amount: string, // $currency:x.y
- subject: string,
- ): Promise<string> {
- const stdout = await sh(
- this.globalTestState,
- "libeufin-sandbox-maketransfer",
- `libeufin-sandbox make-transaction --debit-account=${debit} --credit-account=${credit} ${amount} "${subject}"`,
- {
- ...process.env,
- LIBEUFIN_SANDBOX_DB_CONNECTION: this.sandboxConfig.databaseJdbcUri,
- },
- );
- return stdout;
- }
-
- async pingUntilAvailable(): Promise<void> {
- const url = this.baseUrl;
- await pingProc(this.sandboxProc, url, "libeufin-sandbox");
- }
-}
-
-export class LibeufinNexusService {
- static async create(
- gc: GlobalTestState,
- nexusConfig: LibeufinNexusConfig,
- ): Promise<LibeufinNexusService> {
- return new LibeufinNexusService(gc, nexusConfig);
- }
-
- nexusProc: ProcessWrapper | undefined;
- globalTestState: GlobalTestState;
-
- constructor(gc: GlobalTestState, private nexusConfig: LibeufinNexusConfig) {
- this.globalTestState = gc;
- }
-
- get baseUrl(): string {
- return `http://localhost:${this.nexusConfig.httpPort}/`;
- }
-
- async start(): Promise<void> {
- await runCommand(
- this.globalTestState,
- "libeufin-nexus-superuser",
- "libeufin-nexus",
- ["superuser", "admin", "--password", "test"],
- {
- ...process.env,
- LIBEUFIN_NEXUS_DB_CONNECTION: this.nexusConfig.databaseJdbcUri,
- },
- );
-
- this.nexusProc = this.globalTestState.spawnService(
- "libeufin-nexus",
- ["serve", "--port", `${this.nexusConfig.httpPort}`],
- "libeufin-nexus",
- {
- ...process.env,
- LIBEUFIN_NEXUS_DB_CONNECTION: this.nexusConfig.databaseJdbcUri,
- },
- );
- }
-
- async pingUntilAvailable(): Promise<void> {
- const url = `${this.baseUrl}config`;
- await pingProc(this.nexusProc, url, "libeufin-nexus");
- }
-
- async createNexusSuperuser(details: LibeufinNexusUser): Promise<void> {
- const stdout = await sh(
- this.globalTestState,
- "libeufin-nexus",
- `libeufin-nexus superuser ${details.username} --password=${details.password}`,
- {
- ...process.env,
- LIBEUFIN_NEXUS_DB_CONNECTION: this.nexusConfig.databaseJdbcUri,
- },
- );
- console.log(stdout);
- }
-}
-
-export interface TwgAddIncomingRequest {
- amount: string;
- reserve_pub: string;
- debit_account: string;
-}
-
-/**
- * The bundle aims at minimizing the amount of input
- * data that is required to initialize a new user + Ebics
- * connection.
- */
-export class NexusUserBundle {
- userReq: CreateNexusUserRequest;
- connReq: CreateEbicsBankConnectionRequest;
- anastasisReq: CreateAnastasisFacadeRequest;
- twgReq: CreateTalerWireGatewayFacadeRequest;
- twgTransferPermission: PostNexusPermissionRequest;
- twgHistoryPermission: PostNexusPermissionRequest;
- twgAddIncomingPermission: PostNexusPermissionRequest;
- localAccountName: string;
- remoteAccountName: string;
-
- constructor(salt: string, ebicsURL: string) {
- this.userReq = {
- username: `username-${salt}`,
- password: `password-${salt}`,
- };
-
- this.connReq = {
- name: `connection-${salt}`,
- ebicsURL: ebicsURL,
- hostID: `ebicshost,${salt}`,
- partnerID: `ebicspartner,${salt}`,
- userID: `ebicsuser,${salt}`,
- };
-
- this.twgReq = {
- currency: "EUR",
- name: `twg-${salt}`,
- reserveTransferLevel: "report",
- accountName: `local-account-${salt}`,
- connectionName: `connection-${salt}`,
- };
- this.anastasisReq = {
- currency: "EUR",
- name: `anastasis-${salt}`,
- reserveTransferLevel: "report",
- accountName: `local-account-${salt}`,
- connectionName: `connection-${salt}`,
- };
- this.remoteAccountName = `remote-account-${salt}`;
- this.localAccountName = `local-account-${salt}`;
- this.twgTransferPermission = {
- action: "grant",
- permission: {
- subjectId: `username-${salt}`,
- subjectType: "user",
- resourceType: "facade",
- resourceId: `twg-${salt}`,
- permissionName: "facade.talerWireGateway.transfer",
- },
- };
- this.twgHistoryPermission = {
- action: "grant",
- permission: {
- subjectId: `username-${salt}`,
- subjectType: "user",
- resourceType: "facade",
- resourceId: `twg-${salt}`,
- permissionName: "facade.talerWireGateway.history",
- },
- };
- }
-}
-
-/**
- * The bundle aims at minimizing the amount of input
- * data that is required to initialize a new Sandbox
- * customer, associating their bank account with a Ebics
- * subscriber.
- */
-export class SandboxUserBundle {
- ebicsBankAccount: CreateEbicsBankAccountRequest;
- constructor(salt: string) {
- this.ebicsBankAccount = {
- bic: "BELADEBEXXX",
- iban: getRandomIban(),
- label: `remote-account-${salt}`,
- name: `Taler Exchange: ${salt}`,
- subscriber: {
- hostID: `ebicshost,${salt}`,
- partnerID: `ebicspartner,${salt}`,
- userID: `ebicsuser,${salt}`,
- },
- };
- }
-}
-
-export class LibeufinCli {
- cliDetails: LibeufinCliDetails;
- globalTestState: GlobalTestState;
-
- constructor(gc: GlobalTestState, cd: LibeufinCliDetails) {
- this.globalTestState = gc;
- this.cliDetails = cd;
- }
-
- env(): any {
- return {
- ...process.env,
- LIBEUFIN_SANDBOX_URL: this.cliDetails.sandboxUrl,
- LIBEUFIN_SANDBOX_USERNAME: "admin",
- LIBEUFIN_SANDBOX_PASSWORD: "secret",
- };
- }
-
- async checkSandbox(): Promise<void> {
- const stdout = await sh(
- this.globalTestState,
- "libeufin-cli-checksandbox",
- "libeufin-cli sandbox check",
- this.env(),
- );
- }
-
- async createEbicsHost(hostId: string): Promise<void> {
- const stdout = await sh(
- this.globalTestState,
- "libeufin-cli-createebicshost",
- `libeufin-cli sandbox ebicshost create --host-id=${hostId}`,
- this.env(),
- );
- console.log(stdout);
- }
-
- async createEbicsSubscriber(
- details: LibeufinEbicsSubscriberDetails,
- ): Promise<void> {
- const stdout = await sh(
- this.globalTestState,
- "libeufin-cli-createebicssubscriber",
- "libeufin-cli sandbox ebicssubscriber create" +
- ` --host-id=${details.hostId}` +
- ` --partner-id=${details.partnerId}` +
- ` --user-id=${details.userId}`,
- this.env(),
- );
- console.log(stdout);
- }
-
- async createEbicsBankAccount(
- sd: LibeufinEbicsSubscriberDetails,
- bankAccountDetails: LibeufinBankAccountDetails,
- ): Promise<void> {
- const stdout = await sh(
- this.globalTestState,
- "libeufin-cli-createebicsbankaccount",
- "libeufin-cli sandbox ebicsbankaccount create" +
- ` --iban=${bankAccountDetails.iban}` +
- ` --bic=${bankAccountDetails.bic}` +
- ` --person-name='${bankAccountDetails.personName}'` +
- ` --account-name=${bankAccountDetails.accountName}` +
- ` --ebics-host-id=${sd.hostId}` +
- ` --ebics-partner-id=${sd.partnerId}` +
- ` --ebics-user-id=${sd.userId}`,
- this.env(),
- );
- console.log(stdout);
- }
-
- async generateTransactions(accountName: string): Promise<void> {
- const stdout = await sh(
- this.globalTestState,
- "libeufin-cli-generatetransactions",
- `libeufin-cli sandbox bankaccount generate-transactions ${accountName}`,
- this.env(),
- );
- console.log(stdout);
- }
-
- async showSandboxTransactions(accountName: string): Promise<void> {
- const stdout = await sh(
- this.globalTestState,
- "libeufin-cli-showsandboxtransactions",
- `libeufin-cli sandbox bankaccount transactions ${accountName}`,
- this.env(),
- );
- console.log(stdout);
- }
-
- async createEbicsConnection(
- connectionDetails: LibeufinEbicsConnectionDetails,
- ): Promise<void> {
- const stdout = await sh(
- this.globalTestState,
- "libeufin-cli-createebicsconnection",
- `libeufin-cli connections new-ebics-connection` +
- ` --ebics-url=${connectionDetails.ebicsUrl}` +
- ` --host-id=${connectionDetails.subscriberDetails.hostId}` +
- ` --partner-id=${connectionDetails.subscriberDetails.partnerId}` +
- ` --ebics-user-id=${connectionDetails.subscriberDetails.userId}` +
- ` ${connectionDetails.connectionName}`,
- {
- ...process.env,
- LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl,
- LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username,
- LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password,
- },
- );
- console.log(stdout);
- }
-
- async createBackupFile(details: LibeufinBackupFileDetails): Promise<void> {
- const stdout = await sh(
- this.globalTestState,
- "libeufin-cli-createbackupfile",
- `libeufin-cli connections export-backup` +
- ` --passphrase=${details.passphrase}` +
- ` --output-file=${details.outputFile}` +
- ` ${details.connectionName}`,
- {
- ...process.env,
- LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl,
- LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username,
- LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password,
- },
- );
- console.log(stdout);
- }
-
- async createKeyLetter(details: LibeufinKeyLetterDetails): Promise<void> {
- const stdout = await sh(
- this.globalTestState,
- "libeufin-cli-createkeyletter",
- `libeufin-cli connections get-key-letter` +
- ` ${details.connectionName} ${details.outputFile}`,
- {
- ...process.env,
- LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl,
- LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username,
- LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password,
- },
- );
- console.log(stdout);
- }
-
- async connect(connectionName: string): Promise<void> {
- const stdout = await sh(
- this.globalTestState,
- "libeufin-cli-connect",
- `libeufin-cli connections connect ${connectionName}`,
- {
- ...process.env,
- LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl,
- LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username,
- LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password,
- },
- );
- console.log(stdout);
- }
-
- async downloadBankAccounts(connectionName: string): Promise<void> {
- const stdout = await sh(
- this.globalTestState,
- "libeufin-cli-downloadbankaccounts",
- `libeufin-cli connections download-bank-accounts ${connectionName}`,
- {
- ...process.env,
- LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl,
- LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username,
- LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password,
- },
- );
- console.log(stdout);
- }
-
- async listOfferedBankAccounts(connectionName: string): Promise<void> {
- const stdout = await sh(
- this.globalTestState,
- "libeufin-cli-listofferedbankaccounts",
- `libeufin-cli connections list-offered-bank-accounts ${connectionName}`,
- {
- ...process.env,
- LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl,
- LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username,
- LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password,
- },
- );
- console.log(stdout);
- }
-
- async importBankAccount(
- importDetails: LibeufinBankAccountImportDetails,
- ): Promise<void> {
- const stdout = await sh(
- this.globalTestState,
- "libeufin-cli-importbankaccount",
- "libeufin-cli connections import-bank-account" +
- ` --offered-account-id=${importDetails.offeredBankAccountName}` +
- ` --nexus-bank-account-id=${importDetails.nexusBankAccountName}` +
- ` ${importDetails.connectionName}`,
- {
- ...process.env,
- LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl,
- LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username,
- LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password,
- },
- );
- console.log(stdout);
- }
-
- async fetchTransactions(bankAccountName: string): Promise<void> {
- const stdout = await sh(
- this.globalTestState,
- "libeufin-cli-fetchtransactions",
- `libeufin-cli accounts fetch-transactions ${bankAccountName}`,
- {
- ...process.env,
- LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl,
- LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username,
- LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password,
- },
- );
- console.log(stdout);
- }
-
- async transactions(bankAccountName: string): Promise<void> {
- const stdout = await sh(
- this.globalTestState,
- "libeufin-cli-transactions",
- `libeufin-cli accounts transactions ${bankAccountName}`,
- {
- ...process.env,
- LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl,
- LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username,
- LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password,
- },
- );
- console.log(stdout);
- }
-
- async preparePayment(details: LibeufinPreparedPaymentDetails): Promise<void> {
- const stdout = await sh(
- this.globalTestState,
- "libeufin-cli-preparepayment",
- `libeufin-cli accounts prepare-payment` +
- ` --creditor-iban=${details.creditorIban}` +
- ` --creditor-bic=${details.creditorBic}` +
- ` --creditor-name='${details.creditorName}'` +
- ` --payment-subject='${details.subject}'` +
- ` --payment-amount=${details.currency}:${details.amount}` +
- ` ${details.nexusBankAccountName}`,
- {
- ...process.env,
- LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl,
- LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username,
- LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password,
- },
- );
- console.log(stdout);
- }
-
- async submitPayment(
- details: LibeufinPreparedPaymentDetails,
- paymentUuid: string,
- ): Promise<void> {
- const stdout = await sh(
- this.globalTestState,
- "libeufin-cli-submitpayments",
- `libeufin-cli accounts submit-payments` +
- ` --payment-uuid=${paymentUuid}` +
- ` ${details.nexusBankAccountName}`,
- {
- ...process.env,
- LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl,
- LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username,
- LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password,
- },
- );
- console.log(stdout);
- }
-
- async newAnastasisFacade(req: NewAnastasisFacadeReq): Promise<void> {
- const stdout = await sh(
- this.globalTestState,
- "libeufin-cli-new-anastasis-facade",
- `libeufin-cli facades new-anastasis-facade` +
- ` --currency ${req.currency}` +
- ` --facade-name ${req.facadeName}` +
- ` ${req.connectionName} ${req.accountName}`,
- {
- ...process.env,
- LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl,
- LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username,
- LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password,
- },
- );
- console.log(stdout);
- }
-
- async newTalerWireGatewayFacade(req: NewTalerWireGatewayReq): Promise<void> {
- const stdout = await sh(
- this.globalTestState,
- "libeufin-cli-new-taler-wire-gateway-facade",
- `libeufin-cli facades new-taler-wire-gateway-facade` +
- ` --currency ${req.currency}` +
- ` --facade-name ${req.facadeName}` +
- ` ${req.connectionName} ${req.accountName}`,
- {
- ...process.env,
- LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl,
- LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username,
- LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password,
- },
- );
- console.log(stdout);
- }
-
- async listFacades(): Promise<void> {
- const stdout = await sh(
- this.globalTestState,
- "libeufin-cli-facades-list",
- `libeufin-cli facades list`,
- {
- ...process.env,
- LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl,
- LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username,
- LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password,
- },
- );
- console.log(stdout);
- }
-}
-
-interface NewAnastasisFacadeReq {
- facadeName: string;
- connectionName: string;
- accountName: string;
- currency: string;
-}
-
-interface NewTalerWireGatewayReq {
- facadeName: string;
- connectionName: string;
- accountName: string;
- currency: string;
-}
-
-/**
- * Launch Nexus and Sandbox AND creates users / facades / bank accounts /
- * .. all that's required to start making banking traffic.
- */
-export async function launchLibeufinServices(
- t: GlobalTestState,
- nexusUserBundle: NexusUserBundle[],
- sandboxUserBundle: SandboxUserBundle[] = [],
- withFacades: string[] = [], // takes only "twg" and/or "anastasis"
-): Promise<LibeufinServices> {
- const db = await setupDb(t);
-
- const libeufinSandbox = await LibeufinSandboxService.create(t, {
- httpPort: 5010,
- databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-sandbox.sqlite3`,
- });
-
- await libeufinSandbox.start();
- await libeufinSandbox.pingUntilAvailable();
-
- const libeufinNexus = await LibeufinNexusService.create(t, {
- httpPort: 5011,
- databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-nexus.sqlite3`,
- });
-
- await libeufinNexus.start();
- await libeufinNexus.pingUntilAvailable();
- console.log("Libeufin services launched!");
-
- for (let sb of sandboxUserBundle) {
- await LibeufinSandboxApi.createEbicsHost(
- libeufinSandbox,
- sb.ebicsBankAccount.subscriber.hostID,
- );
- await LibeufinSandboxApi.createEbicsSubscriber(
- libeufinSandbox,
- sb.ebicsBankAccount.subscriber,
- );
- await LibeufinSandboxApi.createEbicsBankAccount(
- libeufinSandbox,
- sb.ebicsBankAccount,
- );
- }
- console.log("Sandbox user(s) / account(s) / subscriber(s): created");
-
- for (let nb of nexusUserBundle) {
- await LibeufinNexusApi.createEbicsBankConnection(libeufinNexus, nb.connReq);
- await LibeufinNexusApi.connectBankConnection(
- libeufinNexus,
- nb.connReq.name,
- );
- await LibeufinNexusApi.fetchAccounts(libeufinNexus, nb.connReq.name);
- await LibeufinNexusApi.importConnectionAccount(
- libeufinNexus,
- nb.connReq.name,
- nb.remoteAccountName,
- nb.localAccountName,
- );
- await LibeufinNexusApi.createUser(libeufinNexus, nb.userReq);
- for (let facade of withFacades) {
- switch (facade) {
- case "twg":
- await LibeufinNexusApi.createTwgFacade(libeufinNexus, nb.twgReq);
- await LibeufinNexusApi.postPermission(
- libeufinNexus,
- nb.twgTransferPermission,
- );
- await LibeufinNexusApi.postPermission(
- libeufinNexus,
- nb.twgHistoryPermission,
- );
- break;
- case "anastasis":
- await LibeufinNexusApi.createAnastasisFacade(
- libeufinNexus,
- nb.anastasisReq,
- );
- }
- }
- }
- console.log(
- "Nexus user(s) / connection(s) / facade(s) / permission(s): created",
- );
-
- return {
- commonDb: db,
- libeufinNexus: libeufinNexus,
- libeufinSandbox: libeufinSandbox,
- };
-}
-
-/**
- * Helper function that searches a payment among
- * a list, as returned by Nexus. The key is just
- * the payment subject.
- */
-export function findNexusPayment(
- key: string,
- payments: LibeufinNexusTransactions,
-): LibeufinNexusMoneyMovement | void {
- let transactions = payments["transactions"];
- for (let i = 0; i < transactions.length; i++) {
- let batches = transactions[i]["batches"];
- for (let y = 0; y < batches.length; y++) {
- let movements = batches[y]["batchTransactions"];
- for (let z = 0; z < movements.length; z++) {
- let movement = movements[z];
- if (movement["details"]["unstructuredRemittanceInformation"] == key)
- return movement;
- }
- }
- }
-}
diff --git a/packages/taler-wallet-cli/src/import-meta-url.js b/packages/taler-wallet-cli/src/import-meta-url.js
new file mode 100644
index 000000000..c0e657160
--- /dev/null
+++ b/packages/taler-wallet-cli/src/import-meta-url.js
@@ -0,0 +1,2 @@
+// Helper to make 'import.meta.url' available in esbuild-bundled code as well.
+export const import_meta_url = require("url").pathToFileURL(__filename);
diff --git a/packages/taler-wallet-cli/src/index.ts b/packages/taler-wallet-cli/src/index.ts
index 40f2273a7..b85995052 100644
--- a/packages/taler-wallet-cli/src/index.ts
+++ b/packages/taler-wallet-cli/src/index.ts
@@ -17,62 +17,57 @@
/**
* Imports.
*/
-import { deepStrictEqual } from "assert";
-import fs from "fs";
-import os from "os";
-import path from "path";
-// Polyfill for encoding which isn't present globally in older nodejs versions
import {
+ AbsoluteTime,
addPaytoQueryParams,
AgeRestriction,
- Amounts,
- classifyTalerUri,
- clk,
+ AmountString,
codecForList,
codecForString,
- Configuration,
- decodeCrock,
+ CoreApiResponse,
+ Duration,
encodeCrock,
+ getErrorDetailFromException,
getRandomBytes,
j2s,
Logger,
+ NotificationType,
parsePaytoUri,
+ parseTalerUri,
PreparePayResultType,
- RecoveryMergeStrategy,
- rsaBlind,
+ sampleWalletCoreTransactions,
setDangerousTimetravel,
setGlobalLogLevelFromString,
- TalerUriType,
- parseDevExperimentUri,
+ summarizeTalerErrorDetail,
+ TalerUriAction,
+ TransactionIdStr,
+ WalletNotification,
} from "@gnu-taler/taler-util";
+import { clk } from "@gnu-taler/taler-util/clk";
import {
- CryptoDispatcher,
- getDefaultNodeWallet,
- getErrorDetailFromException,
- nativeCrypto,
- NodeHttpLib,
- NodeThreadCryptoWorkerFactory,
- summarizeTalerErrorDetail,
- SynchronousCryptoWorkerFactoryNode,
+ getenv,
+ pathHomedir,
+ processExit,
+ readFile,
+ readlinePrompt,
+ setUnhandledRejectionHandler,
+} from "@gnu-taler/taler-util/compat";
+import { createPlatformHttpLib } from "@gnu-taler/taler-util/http";
+import { JsonMessage, runRpcServer } from "@gnu-taler/taler-util/twrpc";
+import {
+ AccessStats,
+ createNativeWalletHost2,
Wallet,
WalletApiOperation,
WalletCoreApiClient,
- walletCoreDebugFlags,
} from "@gnu-taler/taler-wallet-core";
-import type { TalerCryptoInterface } from "@gnu-taler/taler-wallet-core";
-import { TextDecoder, TextEncoder } from "util";
-import { runBench1 } from "./bench1.js";
-import { runBench2 } from "./bench2.js";
-import { runBench3 } from "./bench3.js";
-import { runEnv1 } from "./env1.js";
-import { GlobalTestState, runTestWithState } from "./harness/harness.js";
-import { getTestInfo, runTests } from "./integrationtests/testrunner.js";
-import { lintExchangeDeployment } from "./lint.js";
-import { runEnvFull } from "./env-full.js";
-// @ts-ignore
-global.TextEncoder = TextEncoder;
-// @ts-ignore
-global.TextDecoder = TextDecoder;
+import {
+ createRemoteWallet,
+ getClientFromRemoteWallet,
+ makeNotificationWaiter,
+} from "@gnu-taler/taler-wallet-core/remote";
+
+import * as fs from "node:fs";
// This module also serves as the entry point for the crypto
// thread worker, and thus must expose these two handlers.
@@ -83,17 +78,19 @@ export {
const logger = new Logger("taler-wallet-cli.ts");
+let observabilityEventFile: string | undefined = undefined;
+
const EXIT_EXCEPTION = 4;
const EXIT_API_ERROR = 5;
-const EXIT_RETRIES_EXCEEDED = 6;
-process.on("unhandledRejection", (error: any) => {
+setUnhandledRejectionHandler((error: any) => {
logger.error("unhandledRejection", error.message);
logger.error("stack", error.stack);
- process.exit(2);
+ processExit(1);
});
-const defaultWalletDbPath = os.homedir + "/" + ".talerwalletdb.json";
+const defaultWalletDbPath = pathHomedir() + "/" + ".talerwalletdb.sqlite3";
+const defaultWalletCoreSocket = pathHomedir() + "/" + ".wallet-core.sock";
function assertUnreachable(x: never): never {
throw new Error("Didn't expect to get here");
@@ -110,7 +107,7 @@ async function doPay(
if (result.status === PreparePayResultType.InsufficientBalance) {
console.log("contract", result.contractTerms);
console.error("insufficient balance");
- process.exit(1);
+ processExit(1);
return;
}
if (result.status === PreparePayResultType.AlreadyConfirmed) {
@@ -119,8 +116,7 @@ async function doPay(
} else {
console.log("payment already in progress");
}
-
- process.exit(0);
+ processExit(0);
return;
}
if (result.status === "payment-possible") {
@@ -163,9 +159,10 @@ function applyVerbose(verbose: boolean): void {
}
declare const __VERSION__: string;
+declare const __GIT_HASH__: string;
function printVersion(): void {
- console.log(__VERSION__);
- process.exit(0);
+ console.log(`${__VERSION__} ${__GIT_HASH__}`);
+ processExit(0);
}
export const walletCli = clk
@@ -173,7 +170,10 @@ export const walletCli = clk
help: "Command line interface for the GNU Taler wallet.",
})
.maybeOption("walletDbFile", ["--wallet-db"], clk.STRING, {
- help: "location of the wallet database file",
+ help: "Location of the wallet database file",
+ })
+ .maybeOption("walletConnection", ["--wallet-connection"], clk.STRING, {
+ help: "Connect to an RPC wallet",
})
.maybeOption("timetravel", ["--timetravel"], clk.INT, {
help: "modify system time by given offset in microseconds",
@@ -198,6 +198,9 @@ export const walletCli = clk
.flag("noThrottle", ["--no-throttle"], {
help: "Don't do any request throttling.",
})
+ .flag("noHttp", ["--no-http"], {
+ help: "Allow unsafe http connections.",
+ })
.flag("version", ["-v", "--version"], {
onPresentHandler: printVersion,
})
@@ -211,55 +214,147 @@ export const walletCli = clk
type WalletCliArgsType = clk.GetArgType<typeof walletCli>;
function checkEnvFlag(name: string): boolean {
- const val = process.env[name];
+ const val = getenv(name);
if (val == "1") {
return true;
}
return false;
}
-async function withWallet<T>(
+export interface WalletContext {
+ /**
+ * High-level client for making API requests to wallet-core.
+ */
+ client: WalletCoreApiClient;
+
+ /**
+ * Low-level interface for making API requests to wallet-core.
+ */
+ makeCoreApiRequest(
+ operation: string,
+ payload: unknown,
+ ): Promise<CoreApiResponse>;
+
+ /**
+ * Return a promise that resolves after the wallet has emitted a notification
+ * that meets the criteria of the "cond" predicate.
+ */
+ waitForNotificationCond<T>(
+ cond: (n: WalletNotification) => T | false | undefined,
+ ): Promise<T>;
+}
+
+interface CreateWalletResult {
+ wallet: Wallet;
+ getStats: () => AccessStats;
+}
+
+async function createLocalWallet(
walletCliArgs: WalletCliArgsType,
- f: (w: { client: WalletCoreApiClient; ws: Wallet }) => Promise<T>,
-): Promise<T> {
+ notificationHandler?: (n: WalletNotification) => void,
+ noInit?: boolean,
+): Promise<CreateWalletResult> {
const dbPath = walletCliArgs.wallet.walletDbFile ?? defaultWalletDbPath;
- const myHttpLib = new NodeHttpLib();
- if (walletCliArgs.wallet.noThrottle) {
- myHttpLib.setThrottling(false);
- }
- const wallet = await getDefaultNodeWallet({
- persistentStoragePath: dbPath,
+ const myHttpLib = createPlatformHttpLib({
+ enableThrottling: walletCliArgs.wallet.noThrottle ? false : true,
+ requireTls: walletCliArgs.wallet.noHttp,
+ });
+ const wh = await createNativeWalletHost2({
+ persistentStoragePath: dbPath !== ":memory:" ? dbPath : undefined,
httpLib: myHttpLib,
notifyHandler: (n) => {
logger.info(`wallet notification: ${j2s(n)}`);
+ if (notificationHandler) {
+ notificationHandler(n);
+ }
},
cryptoWorkerType: walletCliArgs.wallet.cryptoWorker as any,
});
- if (checkEnvFlag("TALER_WALLET_BATCH_WITHDRAWAL")) {
- wallet.setBatchWithdrawal(true);
- }
-
applyVerbose(walletCliArgs.wallet.verbose);
+ const res = { wallet: wh.wallet, getStats: wh.getDbStats };
+
+ if (noInit) {
+ return res;
+ }
try {
- const w = {
- ws: wallet,
- client: wallet.client,
- };
- await wallet.handleCoreApiRequest("initWallet", "native-init", {
- skipDefaults: walletCliArgs.wallet.skipDefaults,
+ await wh.wallet.handleCoreApiRequest("initWallet", "native-init", {
+ config: {
+ features: {},
+ testing: {
+ devModeActive: checkEnvFlag("TALER_WALLET_DEV_MODE"),
+ denomselAllowLate: checkEnvFlag(
+ "TALER_WALLET_DEBUG_DENOMSEL_ALLOW_LATE",
+ ),
+ emitObservabilityEvents: observabilityEventFile != null,
+ skipDefaults: walletCliArgs.wallet.skipDefaults,
+ },
+ },
});
- const ret = await f(w);
- return ret;
+ return res;
} catch (e) {
const ed = getErrorDetailFromException(e);
console.error("Operation failed: " + summarizeTalerErrorDetail(ed));
console.error("Error details:", JSON.stringify(ed, undefined, 2));
- process.exit(1);
- } finally {
- logger.trace("operation with wallet finished, stopping");
- wallet.stop();
- logger.trace("stopped wallet");
+ processExit(1);
+ }
+}
+
+function writeObservabilityLog(notif: WalletNotification): void {
+ if (observabilityEventFile) {
+ switch (notif.type) {
+ case NotificationType.RequestObservabilityEvent:
+ case NotificationType.TaskObservabilityEvent:
+ fs.appendFileSync(observabilityEventFile, JSON.stringify(notif) + "\n");
+ break;
+ }
+ }
+}
+
+async function withWallet<T>(
+ walletCliArgs: WalletCliArgsType,
+ f: (ctx: WalletContext) => Promise<T>,
+): Promise<T> {
+ const waiter = makeNotificationWaiter();
+
+ const onNotif = (notif: WalletNotification) => {
+ waiter.notify(notif);
+ writeObservabilityLog(notif);
+ };
+
+ if (walletCliArgs.wallet.walletConnection) {
+ logger.info("creating remote wallet");
+ const w = await createRemoteWallet({
+ name: "wallet",
+ notificationHandler: onNotif,
+ socketFilename: walletCliArgs.wallet.walletConnection,
+ });
+ const ctx: WalletContext = {
+ makeCoreApiRequest(operation, payload) {
+ return w.makeCoreApiRequest(operation, payload);
+ },
+ client: getClientFromRemoteWallet(w),
+ waitForNotificationCond: waiter.waitForNotificationCond,
+ };
+ const res = await f(ctx);
+ w.close();
+ return res;
+ } else {
+ const wh = await createLocalWallet(walletCliArgs, onNotif);
+ const ctx: WalletContext = {
+ client: wh.wallet.client,
+ waitForNotificationCond: waiter.waitForNotificationCond,
+ makeCoreApiRequest(operation, payload) {
+ return wh.wallet.handleCoreApiRequest(operation, "my-req", payload);
+ },
+ };
+ const result = await f(ctx);
+ wh.wallet.stop();
+ if (process.env.TALER_WALLET_DBSTATS) {
+ console.log("database stats:");
+ console.log(j2s(wh.getStats()));
+ }
+ return result;
}
}
@@ -289,27 +384,29 @@ walletCli
await withWallet(args, async (wallet) => {
let requestJson;
logger.info(`handling 'api' request (${args.api.operation})`);
+ const jsonContent = args.api.request.startsWith("@")
+ ? readFile(args.api.request.substring(1))
+ : args.api.request;
try {
- requestJson = JSON.parse(args.api.request);
+ requestJson = JSON.parse(jsonContent);
} catch (e) {
console.error("Invalid JSON");
- process.exit(1);
+ processExit(1);
}
try {
- const resp = await wallet.ws.handleCoreApiRequest(
+ const resp = await wallet.makeCoreApiRequest(
args.api.operation,
- "reqid-1",
requestJson,
);
console.log(JSON.stringify(resp, undefined, 2));
if (resp.type === "error") {
if (args.api.expectSuccess) {
- process.exit(EXIT_API_ERROR);
+ processExit(EXIT_API_ERROR);
}
}
} catch (e) {
logger.error(`Got exception while handling API request ${e}`);
- process.exit(EXIT_EXCEPTION);
+ processExit(EXIT_EXCEPTION);
}
});
logger.info("finished handling API request");
@@ -317,8 +414,13 @@ walletCli
const transactionsCli = walletCli
.subcommand("transactions", "transactions", { help: "Manage transactions." })
- .maybeOption("currency", ["--currency"], clk.STRING)
- .maybeOption("search", ["--search"], clk.STRING);
+ .maybeOption("currency", ["--currency"], clk.STRING, {
+ help: "Filter by currency.",
+ })
+ .maybeOption("search", ["--search"], clk.STRING, {
+ help: "Filter by search string",
+ })
+ .flag("includeRefreshes", ["--include-refreshes"]);
// Default action
transactionsCli.action(async (args) => {
@@ -328,6 +430,8 @@ transactionsCli.action(async (args) => {
{
currency: args.transactions.currency,
search: args.transactions.search,
+ includeRefreshes: args.transactions.includeRefreshes,
+ sort: "stable-ascending",
},
);
console.log(JSON.stringify(pending, undefined, 2));
@@ -344,7 +448,87 @@ transactionsCli
.action(async (args) => {
await withWallet(args, async (wallet) => {
await wallet.client.call(WalletApiOperation.DeleteTransaction, {
- transactionId: args.deleteTransaction.transactionId,
+ transactionId: args.deleteTransaction.transactionId as TransactionIdStr,
+ });
+ });
+ });
+
+transactionsCli
+ .subcommand("suspendTransaction", "suspend", {
+ help: "Suspend a transaction.",
+ })
+ .requiredArgument("transactionId", clk.STRING, {
+ help: "Identifier of the transaction to suspend.",
+ })
+ .action(async (args) => {
+ await withWallet(args, async (wallet) => {
+ await wallet.client.call(WalletApiOperation.SuspendTransaction, {
+ transactionId: args.suspendTransaction
+ .transactionId as TransactionIdStr,
+ });
+ });
+ });
+
+transactionsCli
+ .subcommand("fail", "fail", {
+ help: "Fail a transaction (when it can't be aborted).",
+ })
+ .requiredArgument("transactionId", clk.STRING, {
+ help: "Identifier of the transaction to fail.",
+ })
+ .action(async (args) => {
+ await withWallet(args, async (wallet) => {
+ await wallet.client.call(WalletApiOperation.FailTransaction, {
+ transactionId: args.fail.transactionId as TransactionIdStr,
+ });
+ });
+ });
+
+transactionsCli
+ .subcommand("resumeTransaction", "resume", {
+ help: "Resume a transaction.",
+ })
+ .requiredArgument("transactionId", clk.STRING, {
+ help: "Identifier of the transaction to suspend.",
+ })
+ .action(async (args) => {
+ await withWallet(args, async (wallet) => {
+ await wallet.client.call(WalletApiOperation.ResumeTransaction, {
+ transactionId: args.resumeTransaction.transactionId as TransactionIdStr,
+ });
+ });
+ });
+
+transactionsCli
+ .subcommand("lookup", "lookup", {
+ help: "Look up a single transaction based on the transaction identifier.",
+ })
+ .requiredArgument("transactionId", clk.STRING, {
+ help: "Identifier of the transaction to delete",
+ })
+ .action(async (args) => {
+ await withWallet(args, async (wallet) => {
+ const tx = await wallet.client.call(
+ WalletApiOperation.GetTransactionById,
+ {
+ transactionId: args.lookup.transactionId,
+ },
+ );
+ console.log(j2s(tx));
+ });
+ });
+
+transactionsCli
+ .subcommand("abortTransaction", "abort", {
+ help: "Abort a transaction.",
+ })
+ .requiredArgument("transactionId", clk.STRING, {
+ help: "Identifier of the transaction to delete",
+ })
+ .action(async (args) => {
+ await withWallet(args, async (wallet) => {
+ await wallet.client.call(WalletApiOperation.AbortTransaction, {
+ transactionId: args.abortTransaction.transactionId as TransactionIdStr,
});
});
});
@@ -371,7 +555,7 @@ transactionsCli
.action(async (args) => {
await withWallet(args, async (wallet) => {
await wallet.client.call(WalletApiOperation.RetryTransaction, {
- transactionId: args.retryTransaction.transactionId,
+ transactionId: args.retryTransaction.transactionId as TransactionIdStr,
});
});
});
@@ -380,19 +564,9 @@ walletCli
.subcommand("finishPendingOpt", "run-until-done", {
help: "Run until no more work is left.",
})
- .maybeOption("maxRetries", ["--max-retries"], clk.INT)
- .flag("failOnMaxRetries", ["--fail-on-max-retries"])
.action(async (args) => {
- await withWallet(args, async (wallet) => {
- logger.info("running until pending operations are finished");
- const resp = await wallet.ws.runTaskLoop({
- maxRetries: args.finishPendingOpt.maxRetries,
- stopWhenDone: true,
- });
- wallet.ws.stop();
- if (resp.retriesExceeded && args.finishPendingOpt.failOnMaxRetries) {
- process.exit(EXIT_RETRIES_EXCEEDED);
- }
+ await withWallet(args, async (ctx) => {
+ await ctx.client.call(WalletApiOperation.TestingWaitTasksDone, {});
});
});
@@ -423,7 +597,7 @@ withdrawCli
withdrawCli
.subcommand("withdrawCheckAmount", "check-amount")
.requiredArgument("exchange", clk.STRING)
- .requiredArgument("amount", clk.STRING)
+ .requiredArgument("amount", clk.AMOUNT)
.maybeOption("restrictAge", ["--restrict-age"], clk.INT)
.action(async (args) => {
const restrictAge = args.withdrawCheckAmount.restrictAge;
@@ -467,70 +641,72 @@ walletCli
.subcommand("handleUri", "handle-uri", {
help: "Handle a taler:// URI.",
})
- .requiredArgument("uri", clk.STRING)
+ .maybeArgument("uri", clk.STRING)
+ .maybeOption("withdrawalExchange", ["--withdrawal-exchange"], clk.STRING, {
+ help: "Exchange to use for withdrawal operations.",
+ })
+ .maybeOption("restrictAge", ["--restrict-age"], clk.INT)
.flag("autoYes", ["-y", "--yes"])
.action(async (args) => {
await withWallet(args, async (wallet) => {
- const uri: string = args.handleUri.uri;
- const uriType = classifyTalerUri(uri);
- switch (uriType) {
- case TalerUriType.TalerPay:
+ let uri;
+ if (args.handleUri.uri) {
+ uri = args.handleUri.uri;
+ } else {
+ uri = await readlinePrompt("Taler URI: ");
+ }
+ const parsedTalerUri = parseTalerUri(uri);
+ if (!parsedTalerUri) {
+ throw Error("invalid taler URI");
+ }
+ switch (parsedTalerUri.type) {
+ case TalerUriAction.Pay:
await doPay(wallet.client, uri, {
alwaysYes: args.handleUri.autoYes,
});
break;
- case TalerUriType.TalerTip:
- {
- const res = await wallet.client.call(
- WalletApiOperation.PrepareTip,
- {
- talerTipUri: uri,
- },
- );
- console.log("tip status", res);
- await wallet.client.call(WalletApiOperation.AcceptTip, {
- walletTipId: res.walletTipId,
- });
- }
- break;
- case TalerUriType.TalerRefund:
- await wallet.client.call(WalletApiOperation.ApplyRefund, {
+ case TalerUriAction.Refund:
+ await wallet.client.call(WalletApiOperation.StartRefundQueryForUri, {
talerRefundUri: uri,
});
break;
- case TalerUriType.TalerWithdraw:
- {
- const withdrawInfo = await wallet.client.call(
- WalletApiOperation.GetWithdrawalDetailsForUri,
- {
- talerWithdrawUri: uri,
- },
+ case TalerUriAction.Withdraw: {
+ const withdrawInfo = await wallet.client.call(
+ WalletApiOperation.GetWithdrawalDetailsForUri,
+ {
+ talerWithdrawUri: uri,
+ restrictAge: args.handleUri.restrictAge,
+ },
+ );
+ console.log("withdrawInfo", withdrawInfo);
+ const selectedExchange =
+ args.handleUri.withdrawalExchange ??
+ withdrawInfo.defaultExchangeBaseUrl;
+ if (!selectedExchange) {
+ console.error(
+ "no exchange specified for withdrawal (and no exchange suggested by the bank)",
);
- console.log("withdrawInfo", withdrawInfo);
- const selectedExchange = withdrawInfo.defaultExchangeBaseUrl;
- if (!selectedExchange) {
- console.error("no suggested exchange!");
- process.exit(1);
- return;
- }
- const res = await wallet.client.call(
- WalletApiOperation.AcceptBankIntegratedWithdrawal,
- {
- exchangeBaseUrl: selectedExchange,
- talerWithdrawUri: uri,
- },
- );
- console.log("accept withdrawal response", res);
+ processExit(1);
+ return;
}
+ const res = await wallet.client.call(
+ WalletApiOperation.AcceptBankIntegratedWithdrawal,
+ {
+ exchangeBaseUrl: selectedExchange,
+ talerWithdrawUri: uri,
+ },
+ );
+ console.log("accept withdrawal response", res);
break;
- case TalerUriType.TalerDevExperiment: {
+ }
+ case TalerUriAction.DevExperiment: {
await wallet.client.call(WalletApiOperation.ApplyDevExperiment, {
devExperimentUri: uri,
});
break;
}
default:
- console.log(`URI type (${uriType}) not handled`);
+ console.log(`URI type (${parsedTalerUri.type}) not handled`);
break;
}
return;
@@ -544,7 +720,7 @@ withdrawCli
.requiredOption("exchange", ["--exchange"], clk.STRING, {
help: "Base URL of the exchange.",
})
- .requiredOption("amount", ["--amount"], clk.STRING, {
+ .requiredOption("amount", ["--amount"], clk.AMOUNT, {
help: "Amount to withdraw",
})
.maybeOption("restrictAge", ["--restrict-age"], clk.INT)
@@ -611,9 +787,9 @@ exchangesCli
.flag("force", ["-f", "--force"])
.action(async (args) => {
await withWallet(args, async (wallet) => {
- await wallet.client.call(WalletApiOperation.AddExchange, {
+ await wallet.client.call(WalletApiOperation.UpdateExchangeEntry, {
exchangeBaseUrl: args.exchangesUpdateCmd.url,
- forceUpdate: args.exchangesUpdateCmd.force,
+ force: args.exchangesUpdateCmd.force,
});
});
});
@@ -653,19 +829,32 @@ exchangesCli
});
exchangesCli
+ .subcommand("exchangesAddCmd", "delete", {
+ help: "Delete an exchange by base URL.",
+ })
+ .requiredArgument("url", clk.STRING, {
+ help: "Base URL of the exchange.",
+ })
+ .flag("purge", ["--purge"])
+ .action(async (args) => {
+ await withWallet(args, async (wallet) => {
+ await wallet.client.call(WalletApiOperation.DeleteExchange, {
+ exchangeBaseUrl: args.exchangesAddCmd.url,
+ purge: args.exchangesAddCmd.purge,
+ });
+ });
+ });
+
+exchangesCli
.subcommand("exchangesAcceptTosCmd", "accept-tos", {
help: "Accept terms of service.",
})
.requiredArgument("url", clk.STRING, {
help: "Base URL of the exchange.",
})
- .requiredArgument("etag", clk.STRING, {
- help: "ToS version tag to accept",
- })
.action(async (args) => {
await withWallet(args, async (wallet) => {
await wallet.client.call(WalletApiOperation.SetExchangeTosAccepted, {
- etag: args.exchangesAcceptTosCmd.etag,
exchangeBaseUrl: args.exchangesAcceptTosCmd.url,
});
});
@@ -703,95 +892,72 @@ const backupCli = walletCli.subcommand("backupArgs", "backup", {
help: "Subcommands for backups",
});
-backupCli
- .subcommand("setDeviceId", "set-device-id")
- .requiredArgument("deviceId", clk.STRING, {
- help: "new device ID",
- })
- .action(async (args) => {
- await withWallet(args, async (wallet) => {
- await wallet.client.call(WalletApiOperation.SetWalletDeviceId, {
- walletDeviceId: args.setDeviceId.deviceId,
- });
- });
- });
-
-backupCli.subcommand("exportPlain", "export-plain").action(async (args) => {
+backupCli.subcommand("exportDb", "export-db").action(async (args) => {
await withWallet(args, async (wallet) => {
- const backup = await wallet.client.call(
- WalletApiOperation.ExportBackupPlain,
- {},
- );
+ const backup = await wallet.client.call(WalletApiOperation.ExportDb, {});
console.log(JSON.stringify(backup, undefined, 2));
});
});
-backupCli.subcommand("recoverySave", "save-recovery").action(async (args) => {
+backupCli.subcommand("storeBackup", "store").action(async (args) => {
await withWallet(args, async (wallet) => {
- const recoveryJson = await wallet.client.call(
- WalletApiOperation.ExportBackupRecovery,
+ const resp = await wallet.client.call(
+ WalletApiOperation.CreateStoredBackup,
{},
);
- console.log(JSON.stringify(recoveryJson, undefined, 2));
- });
-});
-
-backupCli.subcommand("run", "run").action(async (args) => {
- await withWallet(args, async (wallet) => {
- await wallet.client.call(WalletApiOperation.RunBackupCycle, {});
+ console.log(JSON.stringify(resp, undefined, 2));
});
});
-backupCli.subcommand("status", "status").action(async (args) => {
+backupCli.subcommand("storeBackup", "list-stored").action(async (args) => {
await withWallet(args, async (wallet) => {
- const status = await wallet.client.call(
- WalletApiOperation.GetBackupInfo,
+ const resp = await wallet.client.call(
+ WalletApiOperation.ListStoredBackups,
{},
);
- console.log(JSON.stringify(status, undefined, 2));
+ console.log(JSON.stringify(resp, undefined, 2));
});
});
backupCli
- .subcommand("recoveryLoad", "load-recovery")
- .maybeOption("strategy", ["--strategy"], clk.STRING, {
- help: "Strategy for resolving a conflict with the existing wallet key ('theirs' or 'ours')",
- })
+ .subcommand("storeBackup", "delete-stored")
+ .requiredArgument("name", clk.STRING)
.action(async (args) => {
await withWallet(args, async (wallet) => {
- const data = JSON.parse(await read(process.stdin));
- let strategy: RecoveryMergeStrategy | undefined;
- const stratStr = args.recoveryLoad.strategy;
- if (stratStr) {
- if (stratStr === "theirs") {
- strategy = RecoveryMergeStrategy.Theirs;
- } else if (stratStr === "ours") {
- strategy = RecoveryMergeStrategy.Theirs;
- } else {
- throw Error("invalid recovery strategy");
- }
- }
- await wallet.client.call(WalletApiOperation.ImportBackupRecovery, {
- recovery: data,
- strategy,
- });
+ const resp = await wallet.client.call(
+ WalletApiOperation.DeleteStoredBackup,
+ {
+ name: args.storeBackup.name,
+ },
+ );
+ console.log(JSON.stringify(resp, undefined, 2));
});
});
backupCli
- .subcommand("addProvider", "add-provider")
- .requiredArgument("url", clk.STRING)
- .maybeArgument("name", clk.STRING)
- .flag("activate", ["--activate"])
+ .subcommand("recoverBackup", "recover-stored")
+ .requiredArgument("name", clk.STRING)
.action(async (args) => {
await withWallet(args, async (wallet) => {
- await wallet.client.call(WalletApiOperation.AddBackupProvider, {
- backupProviderBaseUrl: args.addProvider.url,
- activate: args.addProvider.activate,
- name: args.addProvider.name || args.addProvider.url,
- });
+ const resp = await wallet.client.call(
+ WalletApiOperation.RecoverStoredBackup,
+ {
+ name: args.recoverBackup.name,
+ },
+ );
+ console.log(JSON.stringify(resp, undefined, 2));
+ });
+ });
+
+backupCli.subcommand("importDb", "import-db").action(async (args) => {
+ await withWallet(args, async (wallet) => {
+ const dumpRaw = await read(process.stdin);
+ const dump = JSON.parse(dumpRaw);
+ await wallet.client.call(WalletApiOperation.ImportDb, {
+ dump,
});
});
+});
const depositCli = walletCli.subcommand("depositArgs", "deposit", {
help: "Subcommands for depositing money to payto:// accounts",
@@ -799,7 +965,7 @@ const depositCli = walletCli.subcommand("depositArgs", "deposit", {
depositCli
.subcommand("createDepositArgs", "create")
- .requiredArgument("amount", clk.STRING)
+ .requiredArgument("amount", clk.AMOUNT)
.requiredArgument("targetPayto", clk.STRING)
.action(async (args) => {
await withWallet(args, async (wallet) => {
@@ -811,159 +977,352 @@ depositCli
},
);
console.log(`Created deposit ${resp.depositGroupId}`);
- await wallet.ws.runPending();
});
});
-depositCli
- .subcommand("trackDepositArgs", "track")
- .requiredArgument("depositGroupId", clk.STRING)
+const peerCli = walletCli.subcommand("peerArgs", "p2p", {
+ help: "Subcommands for peer-to-peer payments.",
+});
+
+peerCli
+ .subcommand("checkPayPush", "check-push-debit", {
+ help: "Check fees for starting a peer-push debit transaction.",
+ })
+ .requiredArgument("amount", clk.AMOUNT, {
+ help: "Amount to pay",
+ })
.action(async (args) => {
await withWallet(args, async (wallet) => {
const resp = await wallet.client.call(
- WalletApiOperation.TrackDepositGroup,
+ WalletApiOperation.CheckPeerPushDebit,
{
- depositGroupId: args.trackDepositArgs.depositGroupId,
+ amount: args.checkPayPush.amount,
},
);
console.log(JSON.stringify(resp, undefined, 2));
});
});
-const advancedCli = walletCli.subcommand("advancedArgs", "advanced", {
- help: "Subcommands for advanced operations (only use if you know what you're doing!).",
-});
-
-advancedCli
- .subcommand("init", "init", {
- help: "Initialize the wallet (with DB) and exit.",
+peerCli
+ .subcommand("checkPayPull", "check-pull-credit", {
+ help: "Check fees for a starting peer-pull credit transaction.",
+ })
+ .requiredArgument("amount", clk.AMOUNT, {
+ help: "Amount to request",
})
.action(async (args) => {
- await withWallet(args, async () => {});
+ await withWallet(args, async (wallet) => {
+ const resp = await wallet.client.call(
+ WalletApiOperation.CheckPeerPullCredit,
+ {
+ amount: args.checkPayPull.amount,
+ },
+ );
+ console.log(JSON.stringify(resp, undefined, 2));
+ });
});
-advancedCli
- .subcommand("runPendingOpt", "run-pending", {
- help: "Run pending operations.",
- })
- .flag("forceNow", ["-f", "--force-now"])
+peerCli
+ .subcommand("prepareIncomingPayPull", "prepare-pull-debit")
+ .requiredArgument("talerUri", clk.STRING)
.action(async (args) => {
await withWallet(args, async (wallet) => {
- await wallet.ws.runPending(args.runPendingOpt.forceNow);
+ const resp = await wallet.client.call(
+ WalletApiOperation.PreparePeerPullDebit,
+ {
+ talerUri: args.prepareIncomingPayPull.talerUri,
+ },
+ );
+ console.log(JSON.stringify(resp, undefined, 2));
});
});
-advancedCli
- .subcommand("", "pending", { help: "Show pending operations." })
+peerCli
+ .subcommand("confirmIncomingPayPull", "confirm-pull-debit")
+ .requiredArgument("transactionId", clk.STRING)
.action(async (args) => {
await withWallet(args, async (wallet) => {
- const pending = await wallet.client.call(
- WalletApiOperation.GetPendingOperations,
- {},
+ const resp = await wallet.client.call(
+ WalletApiOperation.ConfirmPeerPullDebit,
+ {
+ transactionId: args.confirmIncomingPayPull
+ .transactionId as TransactionIdStr,
+ },
);
- console.log(JSON.stringify(pending, undefined, 2));
+ console.log(JSON.stringify(resp, undefined, 2));
});
});
-advancedCli
- .subcommand("bench1", "bench1", {
- help: "Run the 'bench1' benchmark",
- })
- .requiredOption("configJson", ["--config-json"], clk.STRING)
+peerCli
+ .subcommand("confirmIncomingPayPush", "confirm-push-credit")
+ .requiredArgument("transactionId", clk.STRING)
.action(async (args) => {
- let config: any;
- try {
- config = JSON.parse(args.bench1.configJson);
- } catch (e) {
- console.log("Could not parse config JSON");
- }
- await runBench1(config);
+ await withWallet(args, async (wallet) => {
+ const resp = await wallet.client.call(
+ WalletApiOperation.ConfirmPeerPushCredit,
+ {
+ transactionId: args.confirmIncomingPayPush.transactionId,
+ },
+ );
+ console.log(JSON.stringify(resp, undefined, 2));
+ });
});
-advancedCli
- .subcommand("bench2", "bench2", {
- help: "Run the 'bench2' benchmark",
+peerCli
+ .subcommand("initiatePayPull", "initiate-pull-credit", {
+ help: "Initiate a peer-pull payment.",
+ })
+ .requiredArgument("amount", clk.AMOUNT, {
+ help: "Amount to request",
+ })
+ .maybeOption("summary", ["--summary"], clk.STRING, {
+ help: "Summary to use in the contract terms.",
})
- .requiredOption("configJson", ["--config-json"], clk.STRING)
+ .maybeOption("purseExpiration", ["--purse-expiration"], clk.STRING)
+ .maybeOption("exchangeBaseUrl", ["--exchange"], clk.STRING)
.action(async (args) => {
- let config: any;
- try {
- config = JSON.parse(args.bench2.configJson);
- } catch (e) {
- console.log("Could not parse config JSON");
+ let purseExpiration: AbsoluteTime;
+
+ if (args.initiatePayPull.purseExpiration) {
+ purseExpiration = AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromPrettyString(args.initiatePayPull.purseExpiration),
+ );
+ } else {
+ purseExpiration = AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ hours: 1 }),
+ );
}
- await runBench2(config);
+
+ await withWallet(args, async (wallet) => {
+ const resp = await wallet.client.call(
+ WalletApiOperation.InitiatePeerPullCredit,
+ {
+ exchangeBaseUrl: args.initiatePayPull.exchangeBaseUrl,
+ partialContractTerms: {
+ amount: args.initiatePayPull.amount,
+ summary: args.initiatePayPull.summary ?? "Invoice",
+ purse_expiration: AbsoluteTime.toProtocolTimestamp(purseExpiration),
+ },
+ },
+ );
+ console.log(JSON.stringify(resp, undefined, 2));
+ });
});
-advancedCli
- .subcommand("bench3", "bench3", {
- help: "Run the 'bench3' benchmark",
+peerCli
+ .subcommand("preparePushCredit", "prepare-push-credit")
+ .requiredArgument("talerUri", clk.STRING)
+ .action(async (args) => {
+ await withWallet(args, async (wallet) => {
+ const resp = await wallet.client.call(
+ WalletApiOperation.PreparePeerPushCredit,
+ {
+ talerUri: args.preparePushCredit.talerUri,
+ },
+ );
+ console.log(JSON.stringify(resp, undefined, 2));
+ });
+ });
+
+peerCli
+ .subcommand("payPush", "initiate-push-debit", {
+ help: "Initiate a peer-push payment.",
+ })
+ .requiredArgument("amount", clk.AMOUNT, {
+ help: "Amount to pay",
})
- .requiredOption("configJson", ["--config-json"], clk.STRING)
+ .maybeOption("summary", ["--summary"], clk.STRING, {
+ help: "Summary to use in the contract terms.",
+ })
+ .maybeOption("purseExpiration", ["--purse-expiration"], clk.STRING)
.action(async (args) => {
- let config: any;
- try {
- config = JSON.parse(args.bench3.configJson);
- } catch (e) {
- console.log("Could not parse config JSON");
+ let purseExpiration: AbsoluteTime;
+
+ if (args.payPush.purseExpiration) {
+ purseExpiration = AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromPrettyString(args.payPush.purseExpiration),
+ );
+ } else {
+ purseExpiration = AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ hours: 1 }),
+ );
}
- await runBench3(config);
+
+ await withWallet(args, async (wallet) => {
+ const resp = await wallet.client.call(
+ WalletApiOperation.InitiatePeerPushDebit,
+ {
+ partialContractTerms: {
+ amount: args.payPush.amount,
+ summary: args.payPush.summary ?? "Payment",
+ purse_expiration: AbsoluteTime.toProtocolTimestamp(purseExpiration),
+ },
+ },
+ );
+ console.log(JSON.stringify(resp, undefined, 2));
+ });
});
+const advancedCli = walletCli.subcommand("advancedArgs", "advanced", {
+ help: "Subcommands for advanced operations (only use if you know what you're doing!).",
+});
+
advancedCli
- .subcommand("envFull", "env-full", {
- help: "Run a test environment for bench1",
+ .subcommand("tasks", "tasks", {
+ help: "Show active wallet-core tasks.",
})
.action(async (args) => {
- const testDir = fs.mkdtempSync(path.join(os.tmpdir(), "taler-env-full-"));
- const testState = new GlobalTestState({
- testDir,
+ await withWallet(args, async (wallet) => {
+ const tasks = await wallet.client.call(
+ WalletApiOperation.GetActiveTasks,
+ {},
+ );
+ console.log(j2s(tasks));
});
- await runTestWithState(testState, runEnvFull, "env-full", true);
});
advancedCli
- .subcommand("env1", "env1", {
- help: "Run a test environment for bench1",
+ .subcommand("sampleTransactions", "sample-transactions", {
+ help: "Print sample wallet-core transactions",
})
.action(async (args) => {
- const testDir = fs.mkdtempSync(path.join(os.tmpdir(), "taler-env1-"));
- const testState = new GlobalTestState({
- testDir,
- });
- await runTestWithState(testState, runEnv1, "env1", true);
+ console.log(JSON.stringify(sampleWalletCoreTransactions, undefined, 2));
});
advancedCli
- .subcommand("withdrawFakebank", "withdraw-fakebank", {
- help: "Withdraw via a fakebank.",
+ .subcommand("serve", "serve", {
+ help: "Serve the wallet API via a unix domain socket.",
})
- .requiredOption("exchange", ["--exchange"], clk.STRING, {
- help: "Base URL of the exchange to use",
+ .requiredOption("unixPath", ["--unix-path"], clk.STRING, {
+ default: defaultWalletCoreSocket,
})
- .requiredOption("amount", ["--amount"], clk.STRING, {
- help: "Amount to withdraw (before fees).",
+ .flag("noInit", ["--no-init"], {
+ help: "Do not initialize the wallet. The client must send the initWallet message.",
})
- .requiredOption("bank", ["--bank"], clk.STRING, {
- help: "Base URL of the Taler fakebank service.",
+ .action(async (args) => {
+ const socketPath = args.serve.unixPath;
+ logger.info(`serving at ${socketPath}`);
+ let cleanupCalled = false;
+
+ const cleanupSocket = (signal: string, code: number) => {
+ if (cleanupCalled) {
+ return;
+ }
+ cleanupCalled = true;
+ try {
+ logger.info("cleaning up socket");
+ fs.unlinkSync(socketPath);
+ } catch (e) {
+ logger.warn(`unable to clean up socket: ${e}`);
+ }
+ process.exit(128 + code);
+ };
+ process.on("SIGTERM", cleanupSocket);
+ process.on("SIGINT", cleanupSocket);
+
+ const onNotif = (notif: WalletNotification) => {
+ writeObservabilityLog(notif);
+ };
+ const wh = await createLocalWallet(args, onNotif, args.serve.noInit);
+ const w = wh.wallet;
+ let nextClientId = 1;
+ const notifyHandlers = new Map<number, (n: WalletNotification) => void>();
+ w.addNotificationListener((n) => {
+ notifyHandlers.forEach((v, k) => {
+ v(n);
+ });
+ });
+ await runRpcServer({
+ socketFilename: args.serve.unixPath,
+ onConnect(client) {
+ logger.info("connected");
+ const clientId = nextClientId++;
+ notifyHandlers.set(clientId, (n: WalletNotification) => {
+ client.sendResponse({
+ type: "notification",
+ payload: n as unknown as JsonMessage,
+ });
+ });
+ return {
+ onDisconnect() {
+ notifyHandlers.delete(clientId);
+ logger.info("disconnected");
+ },
+ onMessage(msg) {
+ logger.info(`message: ${j2s(msg)}`);
+ const op = (msg as any).operation;
+ const id = (msg as any).id;
+ const payload = (msg as any).args;
+ w.handleCoreApiRequest(op, id, payload)
+ .then((resp) => {
+ logger.info("sending response");
+ client.sendResponse(resp as unknown as JsonMessage);
+ })
+ .catch((e) => {
+ logger.error(`unexpected error: ${e}`);
+ });
+ },
+ };
+ },
+ });
+ });
+
+advancedCli
+ .subcommand("init", "init", {
+ help: "Initialize the wallet (with DB) and exit.",
})
.action(async (args) => {
+ await withWallet(args, async () => {});
+ });
+
+advancedCli
+ .subcommand("runPendingOpt", "run-pending", {
+ help: "Run pending operations.",
+ })
+ .action(async (args) => {
+ logger.error(
+ "Subcommand run-pending not supported anymore. Please use run-until-done or the client/server wallet.",
+ );
+ });
+
+advancedCli
+ .subcommand("pending", "pending", { help: "Show pending operations." })
+ .action(async (args) => {
await withWallet(args, async (wallet) => {
- await wallet.client.call(WalletApiOperation.WithdrawFakebank, {
- amount: args.withdrawFakebank.amount,
- bank: args.withdrawFakebank.bank,
- exchange: args.withdrawFakebank.exchange,
- });
+ const pending = await wallet.client.call(
+ WalletApiOperation.GetPendingOperations,
+ {},
+ );
+ console.log(JSON.stringify(pending, undefined, 2));
});
});
advancedCli
- .subcommand("decode", "decode", {
- help: "Decode base32-crockford.",
+ .subcommand("benchInternal", "bench-internal", {
+ help: "Run the 'bench-internal' benchmark",
})
- .action((args) => {
- const enc = fs.readFileSync(0, "utf8");
- fs.writeFileSync(1, decodeCrock(enc.trim()));
+ .action(async (args) => {
+ const myHttpLib = createPlatformHttpLib();
+ const res = await createNativeWalletHost2({
+ // No persistent DB storage.
+ persistentStoragePath: undefined,
+ httpLib: myHttpLib,
+ });
+ const wallet = res.wallet;
+ await wallet.client.call(WalletApiOperation.InitWallet, {});
+ await wallet.client.call(WalletApiOperation.RunIntegrationTest, {
+ amountToSpend: "TESTKUDOS:1" as AmountString,
+ amountToWithdraw: "TESTKUDOS:3" as AmountString,
+ corebankApiBaseUrl: "http://localhost:8082/taler-bank-access/",
+ exchangeBaseUrl: "http://localhost:8081/",
+ merchantBaseUrl: "http://localhost:8083/",
+ });
+ await wallet.client.call(WalletApiOperation.TestingWaitTasksDone, {});
+ wallet.stop();
});
advancedCli
@@ -980,17 +1339,117 @@ const currenciesCli = walletCli.subcommand("currencies", "currencies", {
});
currenciesCli
- .subcommand("show", "show", { help: "Show currencies." })
+ .subcommand("listGlobalAuditors", "list-global-auditors", {
+ help: "List global-currency auditors.",
+ })
.action(async (args) => {
await withWallet(args, async (wallet) => {
const currencies = await wallet.client.call(
- WalletApiOperation.ListCurrencies,
+ WalletApiOperation.ListGlobalCurrencyAuditors,
{},
);
console.log(JSON.stringify(currencies, undefined, 2));
});
});
+currenciesCli
+ .subcommand("listGlobalExchanges", "list-global-exchanges", {
+ help: "List global-currency exchanges.",
+ })
+ .action(async (args) => {
+ await withWallet(args, async (wallet) => {
+ const currencies = await wallet.client.call(
+ WalletApiOperation.ListGlobalCurrencyExchanges,
+ {},
+ );
+ console.log(JSON.stringify(currencies, undefined, 2));
+ });
+ });
+
+currenciesCli
+ .subcommand("addGlobalExchange", "add-global-exchange", {
+ help: "Add a global-currency exchange.",
+ })
+ .requiredOption("currency", ["--currency"], clk.STRING)
+ .requiredOption("exchangeBaseUrl", ["--url"], clk.STRING)
+ .requiredOption("exchangePub", ["--pub"], clk.STRING)
+ .action(async (args) => {
+ await withWallet(args, async (wallet) => {
+ const currencies = await wallet.client.call(
+ WalletApiOperation.AddGlobalCurrencyExchange,
+ {
+ currency: args.addGlobalExchange.currency,
+ exchangeBaseUrl: args.addGlobalExchange.exchangeBaseUrl,
+ exchangeMasterPub: args.addGlobalExchange.exchangePub,
+ },
+ );
+ console.log(JSON.stringify(currencies, undefined, 2));
+ });
+ });
+
+currenciesCli
+ .subcommand("removeGlobalExchange", "remove-global-exchange", {
+ help: "Remove a global-currency exchange.",
+ })
+ .requiredOption("currency", ["--currency"], clk.STRING)
+ .requiredOption("exchangeBaseUrl", ["--url"], clk.STRING)
+ .requiredOption("exchangePub", ["--pub"], clk.STRING)
+ .action(async (args) => {
+ await withWallet(args, async (wallet) => {
+ const currencies = await wallet.client.call(
+ WalletApiOperation.RemoveGlobalCurrencyExchange,
+ {
+ currency: args.removeGlobalExchange.currency,
+ exchangeBaseUrl: args.removeGlobalExchange.exchangeBaseUrl,
+ exchangeMasterPub: args.removeGlobalExchange.exchangePub,
+ },
+ );
+ console.log(JSON.stringify(currencies, undefined, 2));
+ });
+ });
+
+currenciesCli
+ .subcommand("addGlobalAuditor", "add-global-auditor", {
+ help: "Add a global-currency auditor.",
+ })
+ .requiredOption("currency", ["--currency"], clk.STRING)
+ .requiredOption("auditorBaseUrl", ["--url"], clk.STRING)
+ .requiredOption("auditorPub", ["--pub"], clk.STRING)
+ .action(async (args) => {
+ await withWallet(args, async (wallet) => {
+ const currencies = await wallet.client.call(
+ WalletApiOperation.AddGlobalCurrencyAuditor,
+ {
+ currency: args.addGlobalAuditor.currency,
+ auditorBaseUrl: args.addGlobalAuditor.auditorBaseUrl,
+ auditorPub: args.addGlobalAuditor.auditorPub,
+ },
+ );
+ console.log(JSON.stringify(currencies, undefined, 2));
+ });
+ });
+
+currenciesCli
+ .subcommand("removeGlobalAuditor", "remove-global-auditor", {
+ help: "Remove a global-currency auditor.",
+ })
+ .requiredOption("currency", ["--currency"], clk.STRING)
+ .requiredOption("auditorBaseUrl", ["--url"], clk.STRING)
+ .requiredOption("auditorPub", ["--pub"], clk.STRING)
+ .action(async (args) => {
+ await withWallet(args, async (wallet) => {
+ const currencies = await wallet.client.call(
+ WalletApiOperation.RemoveGlobalCurrencyAuditor,
+ {
+ currency: args.removeGlobalAuditor.currency,
+ auditorBaseUrl: args.removeGlobalAuditor.auditorBaseUrl,
+ auditorPub: args.removeGlobalAuditor.auditorPub,
+ },
+ );
+ console.log(JSON.stringify(currencies, undefined, 2));
+ });
+ });
+
advancedCli
.subcommand("clearDatabase", "clear-database", {
help: "Clear the database, irrevocable deleting all data in the wallet.",
@@ -1045,6 +1504,19 @@ advancedCli
});
advancedCli
+ .subcommand("queryRefund", "query-refund", {
+ help: "Query refunds for a payment transaction.",
+ })
+ .requiredArgument("transactionId", clk.STRING)
+ .action(async (args) => {
+ await withWallet(args, async (wallet) => {
+ await wallet.client.call(WalletApiOperation.StartRefundQuery, {
+ transactionId: args.queryRefund.transactionId as TransactionIdStr,
+ });
+ });
+ });
+
+advancedCli
.subcommand("payConfirm", "pay-confirm", {
help: "Confirm payment proposed by a merchant.",
})
@@ -1067,7 +1539,11 @@ advancedCli
.action(async (args) => {
await withWallet(args, async (wallet) => {
await wallet.client.call(WalletApiOperation.ForceRefresh, {
- coinPubList: [args.refresh.coinPub],
+ refreshCoinSpecs: [
+ {
+ coinPub: args.refresh.coinPub,
+ },
+ ],
});
});
});
@@ -1086,30 +1562,6 @@ advancedCli
});
});
-advancedCli
- .subcommand("enableDevMode", "enable-dev-mode", {
- help: "Enable developer mode (dangerous!)",
- })
- .action(async (args) => {
- await withWallet(args, async (wallet) => {
- await wallet.client.call(WalletApiOperation.SetDevMode, {
- devModeEnabled: true,
- });
- });
- });
-
-advancedCli
- .subcommand("disableDevMode", "disable-dev-mode", {
- help: "Disable developer mode",
- })
- .action(async (args) => {
- await withWallet(args, async (wallet) => {
- await wallet.client.call(WalletApiOperation.SetDevMode, {
- devModeEnabled: false,
- });
- });
- });
-
const coinPubListCodec = codecForList(codecForString());
advancedCli
@@ -1126,7 +1578,7 @@ advancedCli
);
} catch (e: any) {
console.log("could not parse coin list:", e.message);
- process.exit(1);
+ processExit(1);
}
for (const c of coinPubList) {
await wallet.client.call(WalletApiOperation.SetCoinSuspended, {
@@ -1151,7 +1603,7 @@ advancedCli
);
} catch (e: any) {
console.log("could not parse coin list:", e.message);
- process.exit(1);
+ processExit(1);
}
for (const c of coinPubList) {
await wallet.client.call(WalletApiOperation.SetCoinSuspended, {
@@ -1178,92 +1630,6 @@ advancedCli
});
});
-const deploymentCli = walletCli.subcommand("deploymentArgs", "deployment", {
- help: "Subcommands for handling GNU Taler deployments.",
-});
-
-deploymentCli
- .subcommand("lintExchange", "lint-exchange", {
- help: "Run checks on the exchange deployment.",
- })
- .flag("cont", ["--continue"], {
- help: "Continue after errors if possible",
- })
- .flag("debug", ["--debug"], {
- help: "Output extra debug info",
- })
- .action(async (args) => {
- await lintExchangeDeployment(
- args.lintExchange.debug,
- args.lintExchange.cont,
- );
- });
-
-deploymentCli
- .subcommand("coincfg", "gen-coin-config", {
- help: "Generate a coin/denomination configuration for the exchange.",
- })
- .requiredOption("minAmount", ["--min-amount"], clk.STRING, {
- help: "Smallest denomination",
- })
- .requiredOption("maxAmount", ["--max-amount"], clk.STRING, {
- help: "Largest denomination",
- })
- .action(async (args) => {
- let out = "";
-
- const stamp = Math.floor(new Date().getTime() / 1000);
-
- const min = Amounts.parseOrThrow(args.coincfg.minAmount);
- const max = Amounts.parseOrThrow(args.coincfg.maxAmount);
- if (min.currency != max.currency) {
- console.error("currency mismatch");
- process.exit(1);
- }
- const currency = min.currency;
- let x = min;
- let n = 1;
-
- out += "# Coin configuration for the exchange.\n";
- out += '# Should be placed in "/etc/taler/conf.d/exchange-coins.conf".\n';
- out += "\n";
-
- while (Amounts.cmp(x, max) < 0) {
- out += `[COIN-${currency}-n${n}-t${stamp}]\n`;
- out += `VALUE = ${Amounts.stringify(x)}\n`;
- out += `DURATION_WITHDRAW = 7 days\n`;
- out += `DURATION_SPEND = 2 years\n`;
- out += `DURATION_LEGAL = 6 years\n`;
- out += `FEE_WITHDRAW = ${currency}:0\n`;
- out += `FEE_DEPOSIT = ${Amounts.stringify(min)}\n`;
- out += `FEE_REFRESH = ${currency}:0\n`;
- out += `FEE_REFUND = ${currency}:0\n`;
- out += `RSA_KEYSIZE = 2048\n`;
- out += "\n";
- x = Amounts.add(x, x).amount;
- n++;
- }
-
- console.log(out);
- });
-
-const deploymentConfigCli = deploymentCli.subcommand("configArgs", "config", {
- help: "Subcommands the Taler configuration.",
-});
-
-deploymentConfigCli
- .subcommand("show", "show")
- .flag("diagnostics", ["-d", "--diagnostics"])
- .maybeArgument("cfgfile", clk.STRING, {})
- .action(async (args) => {
- const cfg = Configuration.load(args.show.cfgfile);
- console.log(
- cfg.stringify({
- diagnostics: args.show.diagnostics,
- }),
- );
- });
-
const testCli = walletCli.subcommand("testingArgs", "testing", {
help: "Subcommands for testing.",
});
@@ -1276,6 +1642,16 @@ testCli
});
});
+testCli.subcommand("withdrawKudos", "withdraw-kudos").action(async (args) => {
+ await withWallet(args, async (wallet) => {
+ await wallet.client.call(WalletApiOperation.WithdrawTestBalance, {
+ amount: "KUDOS:50" as AmountString,
+ corebankApiBaseUrl: "https://bank.demo.taler.net/",
+ exchangeBaseUrl: "https://exchange.demo.taler.net/",
+ });
+ });
+});
+
class PerfTimer {
tStarted: bigint | undefined;
tSum = BigInt(0);
@@ -1397,131 +1773,16 @@ testCli.subcommand("logtest", "logtest").action(async (args) => {
logger.error("This is an error message.");
});
-testCli
- .subcommand("listIntegrationtests", "list-integrationtests")
- .action(async (args) => {
- for (const t of getTestInfo()) {
- let s = t.name;
- if (t.suites.length > 0) {
- s += ` (suites: ${t.suites.join(",")})`;
- }
- if (t.excludeByDefault) {
- s += ` [excluded by default]`;
- }
- console.log(s);
- }
- });
-
-testCli
- .subcommand("runIntegrationtests", "run-integrationtests")
- .maybeArgument("pattern", clk.STRING, {
- help: "Glob pattern to select which tests to run",
- })
- .maybeOption("suites", ["--suites"], clk.STRING, {
- help: "Only run selected suites (comma-separated list)",
- })
- .flag("dryRun", ["--dry"], {
- help: "Only print tests that will be selected to run.",
- })
- .flag("quiet", ["--quiet"], {
- help: "Produce less output.",
- })
- .action(async (args) => {
- await runTests({
- includePattern: args.runIntegrationtests.pattern,
- suiteSpec: args.runIntegrationtests.suites,
- dryRun: args.runIntegrationtests.dryRun,
- verbosity: args.runIntegrationtests.quiet ? 0 : 1,
- });
- });
-
async function read(stream: NodeJS.ReadStream) {
const chunks = [];
for await (const chunk of stream) chunks.push(chunk);
return Buffer.concat(chunks).toString("utf8");
}
-testCli.subcommand("tvgcheck", "tvgcheck").action(async (args) => {
- const data = await read(process.stdin);
-
- const lines = data.match(/[^\r\n]+/g);
-
- if (!lines) {
- throw Error("can't split lines");
- }
-
- const vals: Record<string, string> = {};
-
- let inBlindSigningSection = false;
-
- for (const line of lines) {
- if (line === "blind signing:") {
- inBlindSigningSection = true;
- continue;
- }
- if (line[0] !== " ") {
- inBlindSigningSection = false;
- continue;
- }
- if (inBlindSigningSection) {
- const m = line.match(/ (\w+) (\w+)/);
- if (!m) {
- console.log("bad format");
- process.exit(2);
- }
- vals[m[1]] = m[2];
- }
- }
-
- console.log(vals);
-
- const req = (k: string) => {
- if (!vals[k]) {
- throw Error(`no value for ${k}`);
- }
- return decodeCrock(vals[k]);
- };
-
- const myBm = rsaBlind(
- req("message_hash"),
- req("blinding_key_secret"),
- req("rsa_public_key"),
- );
-
- deepStrictEqual(req("blinded_message"), myBm);
-
- console.log("check passed!");
-});
-
-testCli
- .subcommand("cryptoworker", "cryptoworker")
- .maybeOption("impl", ["--impl"], clk.STRING)
- .action(async (args) => {
- let cryptoApi: TalerCryptoInterface;
- if (!args.cryptoworker.impl || args.cryptoworker.impl === "node") {
- const workerFactory = new NodeThreadCryptoWorkerFactory();
- const cryptoDisp = new CryptoDispatcher(workerFactory);
- cryptoApi = cryptoDisp.cryptoApi;
- } else if (args.cryptoworker.impl === "sync") {
- const workerFactory = new SynchronousCryptoWorkerFactoryNode();
- const cryptoDisp = new CryptoDispatcher(workerFactory);
- cryptoApi = cryptoDisp.cryptoApi;
- } else if (args.cryptoworker.impl === "none") {
- cryptoApi = nativeCrypto;
- } else {
- throw Error(`invalid crypto worker type ${args.cryptoworker.impl}`);
- }
-
- const input = "foo";
- console.log(`testing crypto worker by hashing string '${input}'`);
- const res = await cryptoApi.hashString({ str: input });
- console.log(res);
- });
-
export function main() {
- if (process.env["TALER_WALLET_DEBUG_DENOMSEL_ALLOW_LATE"]) {
- logger.warn("Allowing withdrawal of late denominations for debugging");
- walletCoreDebugFlags.denomselAllowLate = true;
+ const maybeFilename = getenv("TALER_WALLET_DEBUG_OBSERVE");
+ if (!!maybeFilename) {
+ observabilityEventFile = maybeFilename;
}
walletCli.run();
}
diff --git a/packages/taler-wallet-cli/src/integrationtests/scenario-prompt-payment.ts b/packages/taler-wallet-cli/src/integrationtests/scenario-prompt-payment.ts
deleted file mode 100644
index ea05de8e9..000000000
--- a/packages/taler-wallet-cli/src/integrationtests/scenario-prompt-payment.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- 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 { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js";
-import { createSimpleTestkudosEnvironment, withdrawViaBank } from "../harness/helpers.js";
-
-/**
- * Run test for basic, bank-integrated withdrawal.
- */
-export async function runPromptPaymentScenario(t: GlobalTestState) {
- // Set up test environment
-
- const {
- wallet,
- bank,
- exchange,
- merchant,
- } = await createSimpleTestkudosEnvironment(t);
-
- // Withdraw digital cash into the wallet.
-
- await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
-
- // Set up order.
-
- const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
- order: {
- summary: "Buy me!",
- amount: "TESTKUDOS:5",
- fulfillment_url: "taler://fulfillment-success/thx",
- },
- });
-
- let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
- orderId: orderResp.order_id,
- });
-
- t.assertTrue(orderStatus.order_status === "unpaid");
-
- console.log(orderStatus);
-
- // Wait "forever"
- await new Promise(() => {});
-}
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-age-restrictions-merchant.ts b/packages/taler-wallet-cli/src/integrationtests/test-age-restrictions-merchant.ts
deleted file mode 100644
index ff589dd79..000000000
--- a/packages/taler-wallet-cli/src/integrationtests/test-age-restrictions-merchant.ts
+++ /dev/null
@@ -1,201 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- * Imports.
- */
-import { BankApi, WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-import { defaultCoinConfig } from "../harness/denomStructures.js";
-import {
- getWireMethodForTest,
- GlobalTestState,
- MerchantPrivateApi,
- WalletCli,
-} from "../harness/harness.js";
-import {
- createSimpleTestkudosEnvironment,
- withdrawViaBank,
- makeTestPayment,
-} from "../harness/helpers.js";
-
-/**
- * Run test for basic, bank-integrated withdrawal and payment.
- */
-export async function runAgeRestrictionsMerchantTest(t: GlobalTestState) {
- // Set up test environment
-
- const {
- wallet: walletOne,
- bank,
- exchange,
- merchant,
- exchangeBankAccount,
- } = await createSimpleTestkudosEnvironment(
- t,
- defaultCoinConfig.map((x) => x("TESTKUDOS")),
- {
- ageMaskSpec: "8:10:12:14:16:18:21",
- },
- );
-
- const walletTwo = new WalletCli(t, "walletTwo");
- const walletThree = new WalletCli(t, "walletThree");
-
- {
- const walletZero = new WalletCli(t, "walletZero");
-
- await withdrawViaBank(t, {
- wallet: walletZero,
- bank,
- exchange,
- amount: "TESTKUDOS:20",
- restrictAge: 13,
- });
-
- const order = {
- summary: "Buy me!",
- amount: "TESTKUDOS:5",
- fulfillment_url: "taler://fulfillment-success/thx",
- minimum_age: 9,
- };
-
- await makeTestPayment(t, { wallet: walletZero, merchant, order });
- await walletZero.runUntilDone();
- }
-
- {
- const wallet = walletOne;
-
- await withdrawViaBank(t, {
- wallet,
- bank,
- exchange,
- amount: "TESTKUDOS:20",
- restrictAge: 13,
- });
-
- const order = {
- summary: "Buy me!",
- amount: "TESTKUDOS:5",
- fulfillment_url: "taler://fulfillment-success/thx",
- minimum_age: 9,
- };
-
- await makeTestPayment(t, { wallet, merchant, order });
- await wallet.runUntilDone();
- }
-
- {
- const wallet = walletTwo;
-
- await withdrawViaBank(t, {
- wallet,
- bank,
- exchange,
- amount: "TESTKUDOS:20",
- restrictAge: 13,
- });
-
- const order = {
- summary: "Buy me!",
- amount: "TESTKUDOS:5",
- fulfillment_url: "taler://fulfillment-success/thx",
- };
-
- await makeTestPayment(t, { wallet, merchant, order });
- await wallet.runUntilDone();
- }
-
- {
- const wallet = walletThree;
-
- await withdrawViaBank(t, {
- wallet,
- bank,
- exchange,
- amount: "TESTKUDOS:20",
- });
-
- const order = {
- summary: "Buy me!",
- amount: "TESTKUDOS:5",
- fulfillment_url: "taler://fulfillment-success/thx",
- minimum_age: 9,
- };
-
- await makeTestPayment(t, { wallet, merchant, order });
- await wallet.runUntilDone();
- }
-
- // Pay with coin from tipping
- {
- const mbu = await BankApi.createRandomBankUser(bank);
- const tipReserveResp = await MerchantPrivateApi.createTippingReserve(
- merchant,
- "default",
- {
- exchange_url: exchange.baseUrl,
- initial_balance: "TESTKUDOS:10",
- wire_method: getWireMethodForTest(),
- },
- );
-
- t.assertDeepEqual(
- tipReserveResp.payto_uri,
- exchangeBankAccount.accountPaytoUri,
- );
-
- await BankApi.adminAddIncoming(bank, {
- amount: "TESTKUDOS:10",
- debitAccountPayto: mbu.accountPaytoUri,
- exchangeBankAccount,
- reservePub: tipReserveResp.reserve_pub,
- });
-
- await exchange.runWirewatchOnce();
-
- const tip = await MerchantPrivateApi.giveTip(merchant, "default", {
- amount: "TESTKUDOS:5",
- justification: "why not?",
- next_url: "https://example.com/after-tip",
- });
-
- const walletTipping = new WalletCli(t, "age-tipping");
-
- const ptr = await walletTipping.client.call(WalletApiOperation.PrepareTip, {
- talerTipUri: tip.taler_tip_uri,
- });
-
- await walletTipping.client.call(WalletApiOperation.AcceptTip, {
- walletTipId: ptr.walletTipId,
- });
-
- await walletTipping.runUntilDone();
-
- const order = {
- summary: "Buy me!",
- amount: "TESTKUDOS:4",
- fulfillment_url: "taler://fulfillment-success/thx",
- minimum_age: 9,
- };
-
- await makeTestPayment(t, { wallet: walletTipping, merchant, order });
- await walletTipping.runUntilDone();
- }
-}
-
-runAgeRestrictionsMerchantTest.suites = ["wallet"];
-runAgeRestrictionsMerchantTest.timeoutMs = 120 * 1000;
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-age-restrictions-peer.ts b/packages/taler-wallet-cli/src/integrationtests/test-age-restrictions-peer.ts
deleted file mode 100644
index af5b4df52..000000000
--- a/packages/taler-wallet-cli/src/integrationtests/test-age-restrictions-peer.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- * Imports.
- */
-import { AbsoluteTime, Duration } from "@gnu-taler/taler-util";
-import { getDefaultNodeWallet2, WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-import { defaultCoinConfig } from "../harness/denomStructures.js";
-import { GlobalTestState, WalletCli } from "../harness/harness.js";
-import {
- createSimpleTestkudosEnvironment,
- withdrawViaBank,
- makeTestPayment,
-} from "../harness/helpers.js";
-
-/**
- * Run test for basic, bank-integrated withdrawal and payment.
- */
-export async function runAgeRestrictionsPeerTest(t: GlobalTestState) {
- // Set up test environment
-
- const {
- wallet: walletOne,
- bank,
- exchange,
- merchant,
- } = await createSimpleTestkudosEnvironment(
- t,
- defaultCoinConfig.map((x) => x("TESTKUDOS")),
- {
- ageMaskSpec: "8:10:12:14:16:18:21",
- },
- );
-
- const walletTwo = new WalletCli(t, "walletTwo");
- const walletThree = new WalletCli(t, "walletThree");
-
- {
- const wallet = walletOne;
-
- await withdrawViaBank(t, {
- wallet,
- bank,
- exchange,
- amount: "TESTKUDOS:20",
- restrictAge: 13,
- });
-
- const purse_expiration = AbsoluteTime.toTimestamp(
- AbsoluteTime.addDuration(
- AbsoluteTime.now(),
- Duration.fromSpec({ days: 2 }),
- ),
- );
-
- const initResp = await wallet.client.call(WalletApiOperation.InitiatePeerPushPayment, {
- partialContractTerms: {
- summary: "Hello, World",
- amount: "TESTKUDOS:1",
- purse_expiration,
- },
- });
-
- await wallet.runUntilDone();
-
- const checkResp = await walletTwo.client.call(WalletApiOperation.CheckPeerPushPayment, {
- talerUri: initResp.talerUri,
- });
-
- await walletTwo.client.call(WalletApiOperation.AcceptPeerPushPayment, {
- peerPushPaymentIncomingId: checkResp.peerPushPaymentIncomingId,
- });
-
- await walletTwo.runUntilDone();
- }
-}
-
-runAgeRestrictionsPeerTest.suites = ["wallet"];
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-denom-unoffered.ts b/packages/taler-wallet-cli/src/integrationtests/test-denom-unoffered.ts
deleted file mode 100644
index b5ecbee4a..000000000
--- a/packages/taler-wallet-cli/src/integrationtests/test-denom-unoffered.ts
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- 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 { PreparePayResultType, TalerErrorCode } from "@gnu-taler/taler-util";
-import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js";
-import {
- createSimpleTestkudosEnvironment,
- withdrawViaBank,
-} from "../harness/helpers.js";
-
-export async function runDenomUnofferedTest(t: GlobalTestState) {
- // Set up test environment
-
- const { wallet, bank, exchange, merchant } =
- await createSimpleTestkudosEnvironment(t);
-
- // Withdraw digital cash into the wallet.
-
- await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
-
- // Make the exchange forget the denomination.
- // Effectively we completely reset the exchange,
- // but keep the exchange master public key.
-
- await exchange.stop();
- await exchange.purgeDatabase();
- await exchange.purgeSecmodKeys();
- await exchange.start();
- await exchange.pingUntilAvailable();
-
- await merchant.stop();
- await merchant.start();
- await merchant.pingUntilAvailable();
-
- const order = {
- summary: "Buy me!",
- amount: "TESTKUDOS:5",
- fulfillment_url: "taler://fulfillment-success/thx",
- };
-
- {
- const orderResp = await MerchantPrivateApi.createOrder(
- merchant,
- "default",
- {
- order: order,
- },
- );
-
- let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(
- merchant,
- {
- orderId: orderResp.order_id,
- },
- );
-
- t.assertTrue(orderStatus.order_status === "unpaid");
-
- // Make wallet pay for the order
-
- const preparePayResult = await wallet.client.call(
- WalletApiOperation.PreparePayForUri,
- {
- talerPayUri: orderStatus.taler_pay_uri,
- },
- );
-
- t.assertTrue(
- preparePayResult.status === PreparePayResultType.PaymentPossible,
- );
-
- const exc = await t.assertThrowsTalerErrorAsync(async () => {
- await wallet.client.call(WalletApiOperation.ConfirmPay, {
- proposalId: preparePayResult.proposalId,
- });
- });
-
- t.assertTrue(
- exc.hasErrorCode(TalerErrorCode.WALLET_PENDING_OPERATION_FAILED),
- );
-
- // FIXME: We might want a more specific error code here!
- t.assertDeepEqual(
- exc.errorDetail.innerError.code,
- TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
- );
- const merchantErrorCode = (exc.errorDetail.innerError.errorResponse as any)
- .code;
- t.assertDeepEqual(
- merchantErrorCode,
- TalerErrorCode.MERCHANT_POST_ORDERS_ID_PAY_DENOMINATION_KEY_NOT_FOUND,
- );
- }
-
- await wallet.client.call(WalletApiOperation.AddExchange, {
- exchangeBaseUrl: exchange.baseUrl,
- forceUpdate: true,
- });
-
- // Now withdrawal should work again.
- await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
-
- await wallet.runUntilDone();
-
- const txs = await wallet.client.call(WalletApiOperation.GetTransactions, {});
- console.log(JSON.stringify(txs, undefined, 2));
-}
-
-runDenomUnofferedTest.suites = ["wallet"];
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-deposit.ts b/packages/taler-wallet-cli/src/integrationtests/test-deposit.ts
deleted file mode 100644
index 07382c43e..000000000
--- a/packages/taler-wallet-cli/src/integrationtests/test-deposit.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- 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 { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-import { GlobalTestState, getPayto } from "../harness/harness.js";
-import { createSimpleTestkudosEnvironment, withdrawViaBank } from "../harness/helpers.js";
-
-/**
- * Run test for basic, bank-integrated withdrawal and payment.
- */
-export async function runDepositTest(t: GlobalTestState) {
- // Set up test environment
-
- const {
- wallet,
- bank,
- exchange,
- merchant,
- } = await createSimpleTestkudosEnvironment(t);
-
- // Withdraw digital cash into the wallet.
-
- await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
-
- await wallet.runUntilDone();
-
- const { depositGroupId } = await wallet.client.call(
- WalletApiOperation.CreateDepositGroup,
- {
- amount: "TESTKUDOS:10",
- depositPaytoUri: getPayto("foo"),
- },
- );
-
- await wallet.runUntilDone();
-
- const transactions = await wallet.client.call(
- WalletApiOperation.GetTransactions,
- {},
- );
- console.log("transactions", JSON.stringify(transactions, undefined, 2));
- t.assertDeepEqual(transactions.transactions[0].type, "withdrawal");
- t.assertTrue(!transactions.transactions[0].pending);
- t.assertDeepEqual(transactions.transactions[1].type, "deposit");
- t.assertTrue(!transactions.transactions[1].pending);
- // The raw amount is what ends up on the bank account, which includes
- // deposit and wire fees.
- t.assertDeepEqual(transactions.transactions[1].amountRaw, "TESTKUDOS:9.79");
-
- const trackResult = wallet.client.call(WalletApiOperation.TrackDepositGroup, {
- depositGroupId,
- });
-
- console.log(JSON.stringify(trackResult, undefined, 2));
-}
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-bankaccount.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-bankaccount.ts
deleted file mode 100644
index 26fecd77e..000000000
--- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-bankaccount.ts
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- 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 { GlobalTestState } from "../harness/harness.js";
-import {
- NexusUserBundle,
- LibeufinNexusApi,
- LibeufinNexusService,
- LibeufinSandboxService,
- LibeufinSandboxApi,
- findNexusPayment,
-} from "../harness/libeufin.js";
-
-/**
- * Run basic test with LibEuFin.
- */
-export async function runLibeufinApiBankaccountTest(t: GlobalTestState) {
- const nexus = await LibeufinNexusService.create(t, {
- httpPort: 5011,
- databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-nexus.sqlite3`,
- });
- await nexus.start();
- await nexus.pingUntilAvailable();
-
- await LibeufinNexusApi.createUser(nexus, {
- username: "one",
- password: "testing-the-bankaccount-api",
- });
- const sandbox = await LibeufinSandboxService.create(t, {
- httpPort: 5012,
- databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-sandbox.sqlite3`,
- });
- await sandbox.start();
- await sandbox.pingUntilAvailable();
- await LibeufinSandboxApi.createEbicsHost(sandbox, "mock");
- await LibeufinSandboxApi.createEbicsSubscriber(sandbox, {
- hostID: "mock",
- userID: "mock",
- partnerID: "mock",
- });
- await LibeufinSandboxApi.createEbicsBankAccount(sandbox, {
- subscriber: {
- hostID: "mock",
- partnerID: "mock",
- userID: "mock",
- },
- iban: "DE71500105179674997361",
- bic: "BELADEBEXXX",
- name: "mock",
- label: "mock",
- });
- await LibeufinNexusApi.createEbicsBankConnection(nexus, {
- name: "bankaccount-api-test-connection",
- ebicsURL: "http://localhost:5012/ebicsweb",
- hostID: "mock",
- userID: "mock",
- partnerID: "mock",
- });
- await LibeufinNexusApi.connectBankConnection(
- nexus,
- "bankaccount-api-test-connection",
- );
- await LibeufinNexusApi.fetchAccounts(
- nexus,
- "bankaccount-api-test-connection",
- );
-
- await LibeufinNexusApi.importConnectionAccount(
- nexus,
- "bankaccount-api-test-connection",
- "mock",
- "local-mock",
- );
- await LibeufinSandboxApi.simulateIncomingTransaction(
- sandbox,
- "mock", // creditor bankaccount label
- {
- debtorIban: "DE84500105176881385584",
- debtorBic: "BELADEBEXXX",
- debtorName: "mock2",
- amount: "1",
- subject: "mock subject",
- },
- );
- await LibeufinNexusApi.fetchTransactions(nexus, "local-mock");
- let transactions = await LibeufinNexusApi.getAccountTransactions(
- nexus,
- "local-mock",
- );
- let el = findNexusPayment("mock subject", transactions.data);
- t.assertTrue(el instanceof Object);
-}
-
-runLibeufinApiBankaccountTest.suites = ["libeufin"];
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-bankconnection.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-bankconnection.ts
deleted file mode 100644
index 912b7b2ac..000000000
--- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-bankconnection.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- 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 { GlobalTestState } from "../harness/harness.js";
-import { LibeufinNexusApi, LibeufinNexusService } from "../harness/libeufin.js";
-
-/**
- * Run basic test with LibEuFin.
- */
-export async function runLibeufinApiBankconnectionTest(t: GlobalTestState) {
- const nexus = await LibeufinNexusService.create(t, {
- httpPort: 5011,
- databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-nexus.sqlite3`,
- });
- await nexus.start();
- await nexus.pingUntilAvailable();
-
- await LibeufinNexusApi.createUser(nexus, {
- username: "one",
- password: "testing-the-bankconnection-api",
- });
-
- await LibeufinNexusApi.createEbicsBankConnection(nexus, {
- name: "bankconnection-api-test-connection",
- ebicsURL: "http://localhost:5012/ebicsweb",
- hostID: "mock",
- userID: "mock",
- partnerID: "mock",
- });
-
- let connections = await LibeufinNexusApi.getAllConnections(nexus);
- t.assertTrue(connections.data["bankConnections"].length == 1);
-
- await LibeufinNexusApi.deleteBankConnection(nexus, {
- bankConnectionId: "bankconnection-api-test-connection",
- });
- connections = await LibeufinNexusApi.getAllConnections(nexus);
- t.assertTrue(connections.data["bankConnections"].length == 0);
-}
-runLibeufinApiBankconnectionTest.suites = ["libeufin"];
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-facade-bad-request.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-facade-bad-request.ts
deleted file mode 100644
index a1da9e0da..000000000
--- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-facade-bad-request.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- 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 { URL } from "@gnu-taler/taler-util";
-import axiosImp from "axios";
-import { GlobalTestState } from "../harness/harness.js";
-import {
- launchLibeufinServices,
- NexusUserBundle,
- SandboxUserBundle,
-} from "../harness/libeufin.js";
-
-const axios = axiosImp.default;
-
-export async function runLibeufinApiFacadeBadRequestTest(t: GlobalTestState) {
- /**
- * User saltetd "01"
- */
- const user01nexus = new NexusUserBundle(
- "01",
- "http://localhost:5010/ebicsweb",
- );
- const user01sandbox = new SandboxUserBundle("01");
-
- /**
- * Launch Sandbox and Nexus.
- */
- const libeufinServices = await launchLibeufinServices(
- t,
- [user01nexus],
- [user01sandbox],
- ["twg"],
- );
- console.log("malformed facade");
- const baseUrl = libeufinServices.libeufinNexus.baseUrl;
- let url = new URL("facades", baseUrl);
- let resp = await axios.post(
- url.href,
- {
- name: "malformed-facade",
- type: "taler-wire-gateway",
- config: {}, // malformation here.
- },
- {
- auth: {
- username: "admin",
- password: "test",
- },
- validateStatus: () => true,
- },
- );
- t.assertTrue(resp.status == 400);
-}
-
-runLibeufinApiFacadeBadRequestTest.suites = ["libeufin"];
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-facade.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-facade.ts
deleted file mode 100644
index 946c565d4..000000000
--- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-facade.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- 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 { GlobalTestState } from "../harness/harness.js";
-import {
- SandboxUserBundle,
- NexusUserBundle,
- launchLibeufinServices,
- LibeufinNexusApi,
-} from "../harness/libeufin.js";
-
-/**
- * Run basic test with LibEuFin.
- */
-export async function runLibeufinApiFacadeTest(t: GlobalTestState) {
- /**
- * User saltetd "01"
- */
- const user01nexus = new NexusUserBundle(
- "01",
- "http://localhost:5010/ebicsweb",
- );
- const user01sandbox = new SandboxUserBundle("01");
-
- /**
- * Launch Sandbox and Nexus.
- */
- const libeufinServices = await launchLibeufinServices(
- t,
- [user01nexus],
- [user01sandbox],
- ["twg"],
- );
- let resp = await LibeufinNexusApi.getAllFacades(
- libeufinServices.libeufinNexus,
- );
- // check that original facade shows up.
- t.assertTrue(resp.data["facades"][0]["name"] == user01nexus.twgReq["name"]);
-
- const twgBaseUrl: string = resp.data["facades"][0]["baseUrl"];
- t.assertTrue(typeof twgBaseUrl === "string");
- t.assertTrue(twgBaseUrl.startsWith("http://"));
- t.assertTrue(twgBaseUrl.endsWith("/"));
-
- // delete it.
- resp = await LibeufinNexusApi.deleteFacade(
- libeufinServices.libeufinNexus,
- user01nexus.twgReq["name"],
- );
- // check that no facades show up.
- t.assertTrue(!resp.data.hasOwnProperty("facades"));
-}
-
-runLibeufinApiFacadeTest.suites = ["libeufin"];
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-permissions.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-permissions.ts
deleted file mode 100644
index f8f2d7d80..000000000
--- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-permissions.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- 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 { GlobalTestState } from "../harness/harness.js";
-import {
- NexusUserBundle,
- LibeufinNexusApi,
- LibeufinNexusService,
-} from "../harness/libeufin.js";
-
-/**
- * Run basic test with LibEuFin.
- */
-export async function runLibeufinApiPermissionsTest(t: GlobalTestState) {
- const nexus = await LibeufinNexusService.create(t, {
- httpPort: 5011,
- databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-nexus.sqlite3`,
- });
- await nexus.start();
- await nexus.pingUntilAvailable();
-
- const user01nexus = new NexusUserBundle(
- "01",
- "http://localhost:5010/ebicsweb",
- );
-
- await LibeufinNexusApi.createUser(nexus, user01nexus.userReq);
- await LibeufinNexusApi.postPermission(
- nexus,
- user01nexus.twgTransferPermission,
- );
- let transferPermission = await LibeufinNexusApi.getAllPermissions(nexus);
- let element = transferPermission.data["permissions"].pop();
- t.assertTrue(
- element["permissionName"] == "facade.talerwiregateway.transfer" &&
- element["subjectId"] == "username-01",
- );
- let denyTransfer = user01nexus.twgTransferPermission;
-
- // Now revoke permission.
- denyTransfer["action"] = "revoke";
- await LibeufinNexusApi.postPermission(nexus, denyTransfer);
-
- transferPermission = await LibeufinNexusApi.getAllPermissions(nexus);
- t.assertTrue(transferPermission.data["permissions"].length == 0);
-}
-
-runLibeufinApiPermissionsTest.suites = ["libeufin"];
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-sandbox-camt.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-sandbox-camt.ts
deleted file mode 100644
index 250d17d3d..000000000
--- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-sandbox-camt.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- 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 { GlobalTestState } from "../harness/harness.js";
-import {
- LibeufinSandboxApi,
- LibeufinSandboxService,
-} from "../harness/libeufin.js";
-
-// This test only checks that LibEuFin doesn't fail when
-// it generates Camt statements - no assertions take place.
-// Furthermore, it prints the Camt.053 being generated.
-export async function runLibeufinApiSandboxCamtTest(t: GlobalTestState) {
- const sandbox = await LibeufinSandboxService.create(t, {
- httpPort: 5012,
- databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-sandbox.sqlite3`,
- });
- await sandbox.start();
- await sandbox.pingUntilAvailable();
- await LibeufinSandboxApi.createBankAccount(sandbox, {
- iban: "DE71500105179674997361",
- bic: "BELADEBEXXX",
- name: "Mock Name",
- label: "mock-account-0",
- });
- await LibeufinSandboxApi.createBankAccount(sandbox, {
- iban: "DE71500105179674997361",
- bic: "BELADEBEXXX",
- name: "Mock Name",
- label: "mock-account-1",
- });
- await sandbox.makeTransaction(
- "mock-account-0",
- "mock-account-1",
- "EUR:1",
- "+1",
- );
- await sandbox.makeTransaction(
- "mock-account-0",
- "mock-account-1",
- "EUR:1",
- "+1",
- );
- await sandbox.makeTransaction(
- "mock-account-0",
- "mock-account-1",
- "EUR:1",
- "+1",
- );
- await sandbox.makeTransaction(
- "mock-account-1",
- "mock-account-0",
- "EUR:5",
- "minus 5",
- );
- await sandbox.c53tick();
- let ret = await LibeufinSandboxApi.getCamt053(sandbox, "mock-account-1");
- console.log(ret);
-}
-runLibeufinApiSandboxCamtTest.excludeByDefault = true;
-runLibeufinApiSandboxCamtTest.suites = ["libeufin"];
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-sandbox-transactions.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-sandbox-transactions.ts
deleted file mode 100644
index 224c45970..000000000
--- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-sandbox-transactions.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- 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 { GlobalTestState } from "../harness/harness.js";
-import {
- LibeufinSandboxApi,
- LibeufinSandboxService,
-} from "../harness/libeufin.js";
-
-export async function runLibeufinApiSandboxTransactionsTest(
- t: GlobalTestState,
-) {
- const sandbox = await LibeufinSandboxService.create(t, {
- httpPort: 5012,
- databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-sandbox.sqlite3`,
- });
- await sandbox.start();
- await sandbox.pingUntilAvailable();
- await LibeufinSandboxApi.createBankAccount(sandbox, {
- iban: "DE71500105179674997361",
- bic: "BELADEBEXXX",
- name: "Mock Name",
- label: "mock-account",
- });
- await LibeufinSandboxApi.simulateIncomingTransaction(
- sandbox,
- "mock-account",
- {
- debtorIban: "DE84500105176881385584",
- debtorBic: "BELADEBEXXX",
- debtorName: "mock2",
- subject: "mock subject",
- amount: "1", // EUR is default.
- },
- );
- await LibeufinSandboxApi.simulateIncomingTransaction(
- sandbox,
- "mock-account",
- {
- debtorIban: "DE84500105176881385584",
- debtorBic: "BELADEBEXXX",
- debtorName: "mock2",
- subject: "mock subject 2",
- amount: "1.1", // EUR is default.
- },
- );
- let ret = await LibeufinSandboxApi.getAccountInfoWithBalance(
- sandbox,
- "mock-account",
- );
- t.assertAmountEquals(ret.data.balance, "EUR:2.1");
-}
-runLibeufinApiSandboxTransactionsTest.suites = ["libeufin"];
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-scheduling.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-scheduling.ts
deleted file mode 100644
index 95f4bfaa0..000000000
--- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-scheduling.ts
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- 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 { GlobalTestState } from "../harness/harness.js";
-import {
- launchLibeufinServices,
- LibeufinNexusApi,
- LibeufinNexusService,
- NexusUserBundle,
- SandboxUserBundle,
-} from "../harness/libeufin.js";
-
-/**
- * Test Nexus scheduling API. It creates a task, check whether it shows
- * up, then deletes it, and check if it's gone. Ideally, a check over the
- * _liveliness_ of a scheduled task should happen.
- */
-export async function runLibeufinApiSchedulingTest(t: GlobalTestState) {
- const nexus = await LibeufinNexusService.create(t, {
- httpPort: 5011,
- databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-nexus.sqlite3`,
- });
- await nexus.start();
- await nexus.pingUntilAvailable();
-
- const user01nexus = new NexusUserBundle(
- "01",
- "http://localhost:5010/ebicsweb",
- );
- const user01sandbox = new SandboxUserBundle("01");
- await launchLibeufinServices(t, [user01nexus], [user01sandbox]);
- await LibeufinNexusApi.postTask(nexus, user01nexus.localAccountName, {
- name: "test-task",
- cronspec: "* * *",
- type: "fetch",
- params: {
- level: "all",
- rangeType: "all",
- },
- });
- let resp = await LibeufinNexusApi.getTasks(
- nexus,
- user01nexus.localAccountName,
- "test-task",
- );
- t.assertTrue(resp.data["taskName"] == "test-task");
- await LibeufinNexusApi.deleteTask(
- nexus,
- user01nexus.localAccountName,
- "test-task",
- );
- try {
- await LibeufinNexusApi.getTasks(
- nexus,
- user01nexus.localAccountName,
- "test-task",
- );
- } catch (err: any) {
- t.assertTrue(err.response.status == 404);
- }
-
- // Same with submit task.
- await LibeufinNexusApi.postTask(nexus, user01nexus.localAccountName, {
- name: "test-task",
- cronspec: "* * *",
- type: "submit",
- params: {},
- });
- resp = await LibeufinNexusApi.getTasks(
- nexus,
- user01nexus.localAccountName,
- "test-task",
- );
- t.assertTrue(resp.data["taskName"] == "test-task");
- await LibeufinNexusApi.deleteTask(
- nexus,
- user01nexus.localAccountName,
- "test-task",
- );
- try {
- await LibeufinNexusApi.getTasks(
- nexus,
- user01nexus.localAccountName,
- "test-task",
- );
- } catch (err: any) {
- t.assertTrue(err.response.status == 404);
- }
-}
-runLibeufinApiSchedulingTest.suites = ["libeufin"];
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-users.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-users.ts
deleted file mode 100644
index bc3103c7e..000000000
--- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-users.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- 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 { GlobalTestState } from "../harness/harness.js";
-import { LibeufinNexusApi, LibeufinNexusService } from "../harness/libeufin.js";
-
-/**
- * Run basic test with LibEuFin.
- */
-export async function runLibeufinApiUsersTest(t: GlobalTestState) {
- const nexus = await LibeufinNexusService.create(t, {
- httpPort: 5011,
- databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-nexus.sqlite3`,
- });
- await nexus.start();
- await nexus.pingUntilAvailable();
-
- await LibeufinNexusApi.createUser(nexus, {
- username: "one",
- password: "will-be-changed",
- });
-
- await LibeufinNexusApi.changePassword(
- nexus,
- "one",
- {
- newPassword: "got-changed",
- },
- {
- auth: {
- username: "admin",
- password: "test",
- },
- },
- );
-
- let resp = await LibeufinNexusApi.getUser(nexus, {
- auth: {
- username: "one",
- password: "got-changed",
- },
- });
- console.log(resp.data);
- t.assertTrue(resp.data["username"] == "one" && !resp.data["superuser"]);
-}
-
-runLibeufinApiUsersTest.suites = ["libeufin"];
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-bad-gateway.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-bad-gateway.ts
deleted file mode 100644
index 53aacca84..000000000
--- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-bad-gateway.ts
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- 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 { GlobalTestState, delayMs } from "../harness/harness.js";
-import {
- NexusUserBundle,
- LibeufinNexusApi,
- LibeufinNexusService,
- LibeufinSandboxService,
-} from "../harness/libeufin.js";
-
-/**
- * Testing how Nexus reacts when the Sandbox is unreachable.
- * Typically, because the user specified a wrong EBICS endpoint.
- */
-export async function runLibeufinBadGatewayTest(t: GlobalTestState) {
- /**
- * User saltetd "01"
- */
- const user01nexus = new NexusUserBundle(
- "01", "http://localhost:5010/not-found", // the EBICS endpoint at Sandbox
- );
-
- // Start Nexus
- const libeufinNexus = await LibeufinNexusService.create(t, {
- httpPort: 5011,
- databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-nexus.sqlite3`,
- });
- await libeufinNexus.start();
- await libeufinNexus.pingUntilAvailable();
-
- // Start Sandbox
- const libeufinSandbox = await LibeufinSandboxService.create(t, {
- httpPort: 5010,
- databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-sandbox.sqlite3`,
- });
- await libeufinSandbox.start();
- await libeufinSandbox.pingUntilAvailable();
-
- // Connecting to a non-existent Sandbox endpoint.
- await LibeufinNexusApi.createEbicsBankConnection(
- libeufinNexus,
- user01nexus.connReq
- );
-
- // 502 Bad Gateway expected.
- try {
- await LibeufinNexusApi.connectBankConnection(
- libeufinNexus,
- user01nexus.connReq.name,
- );
- } catch(e: any) {
- t.assertTrue(e.response.status == 502);
- return;
- }
- t.assertTrue(false);
-}
-runLibeufinBadGatewayTest.suites = ["libeufin"];
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-basic.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-basic.ts
deleted file mode 100644
index 8002f093f..000000000
--- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-basic.ts
+++ /dev/null
@@ -1,312 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2020 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- * Imports.
- */
-import { AbsoluteTime, MerchantContractTerms, Duration } from "@gnu-taler/taler-util";
-import {
- WalletApiOperation,
- HarnessExchangeBankAccount,
-} from "@gnu-taler/taler-wallet-core";
-import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js";
-import {
- DbInfo,
- ExchangeService,
- GlobalTestState,
- MerchantService,
- setupDb,
- WalletCli,
-} from "../harness/harness.js";
-import { makeTestPayment } from "../harness/helpers.js";
-import {
- LibeufinNexusApi,
- LibeufinNexusService,
- LibeufinSandboxApi,
- LibeufinSandboxService,
-} from "../harness/libeufin.js";
-
-const exchangeIban = "DE71500105179674997361";
-const customerIban = "DE84500105176881385584";
-const customerBic = "BELADEBEXXX";
-const merchantIban = "DE42500105171245624648";
-
-export interface LibeufinTestEnvironment {
- commonDb: DbInfo;
- exchange: ExchangeService;
- exchangeBankAccount: HarnessExchangeBankAccount;
- merchant: MerchantService;
- wallet: WalletCli;
- libeufinSandbox: LibeufinSandboxService;
- libeufinNexus: LibeufinNexusService;
-}
-
-/**
- * Create a Taler environment with LibEuFin and an EBICS account.
- */
-export async function createLibeufinTestEnvironment(
- t: GlobalTestState,
- coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("EUR")),
-): Promise<LibeufinTestEnvironment> {
- const db = await setupDb(t);
-
- const libeufinSandbox = await LibeufinSandboxService.create(t, {
- httpPort: 5010,
- databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-sandbox.sqlite3`,
- });
-
- await libeufinSandbox.start();
- await libeufinSandbox.pingUntilAvailable();
-
- const libeufinNexus = await LibeufinNexusService.create(t, {
- httpPort: 5011,
- databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-nexus.sqlite3`,
- });
-
- await libeufinNexus.start();
- await libeufinNexus.pingUntilAvailable();
-
- await LibeufinSandboxApi.createEbicsHost(libeufinSandbox, "host01");
- // Subscriber and bank Account for the exchange
- await LibeufinSandboxApi.createEbicsSubscriber(libeufinSandbox, {
- hostID: "host01",
- partnerID: "partner01",
- userID: "user01",
- });
- await LibeufinSandboxApi.createEbicsBankAccount(libeufinSandbox, {
- bic: "DEUTDEBB101",
- iban: exchangeIban,
- label: "exchangeacct",
- name: "Taler Exchange",
- subscriber: {
- hostID: "host01",
- partnerID: "partner01",
- userID: "user01",
- },
- });
- // Subscriber and bank Account for the merchant
- // (Merchant doesn't need EBICS access, but sandbox right now only supports EBICS
- // accounts.)
- await LibeufinSandboxApi.createEbicsSubscriber(libeufinSandbox, {
- hostID: "host01",
- partnerID: "partner02",
- userID: "user02",
- });
- await LibeufinSandboxApi.createEbicsBankAccount(libeufinSandbox, {
- bic: "AUTOATW1XXX",
- iban: merchantIban,
- label: "merchantacct",
- name: "Merchant",
- subscriber: {
- hostID: "host01",
- partnerID: "partner02",
- userID: "user02",
- },
- });
-
- await LibeufinNexusApi.createEbicsBankConnection(libeufinNexus, {
- name: "myconn",
- ebicsURL: "http://localhost:5010/ebicsweb",
- hostID: "host01",
- partnerID: "partner01",
- userID: "user01",
- });
- await LibeufinNexusApi.connectBankConnection(libeufinNexus, "myconn");
- await LibeufinNexusApi.fetchAccounts(libeufinNexus, "myconn");
- await LibeufinNexusApi.importConnectionAccount(
- libeufinNexus,
- "myconn",
- "exchangeacct",
- "myacct",
- );
-
- await LibeufinNexusApi.createTwgFacade(libeufinNexus, {
- name: "twg1",
- accountName: "myacct",
- connectionName: "myconn",
- currency: "EUR",
- reserveTransferLevel: "report",
- });
-
- await LibeufinNexusApi.createUser(libeufinNexus, {
- username: "twguser",
- password: "twgpw",
- });
-
- await LibeufinNexusApi.postPermission(libeufinNexus, {
- action: "grant",
- permission: {
- subjectType: "user",
- subjectId: "twguser",
- resourceType: "facade",
- resourceId: "twg1",
- permissionName: "facade.talerWireGateway.history",
- },
- });
-
- await LibeufinNexusApi.postPermission(libeufinNexus, {
- action: "grant",
- permission: {
- subjectType: "user",
- subjectId: "twguser",
- resourceType: "facade",
- resourceId: "twg1",
- permissionName: "facade.talerWireGateway.transfer",
- },
- });
-
- const exchange = ExchangeService.create(t, {
- name: "testexchange-1",
- currency: "EUR",
- httpPort: 8081,
- database: db.connStr,
- });
-
- const merchant = await MerchantService.create(t, {
- name: "testmerchant-1",
- currency: "EUR",
- httpPort: 8083,
- database: db.connStr,
- });
-
- const exchangeBankAccount: HarnessExchangeBankAccount = {
- accountName: "twguser",
- accountPassword: "twgpw",
- accountPaytoUri: `payto://iban/${exchangeIban}?receiver-name=Exchange`,
- wireGatewayApiBaseUrl:
- "http://localhost:5011/facades/twg1/taler-wire-gateway/",
- };
-
- exchange.addBankAccount("1", exchangeBankAccount);
-
- exchange.addCoinConfigList(coinConfig);
-
- await exchange.start();
- await exchange.pingUntilAvailable();
-
- merchant.addExchange(exchange);
-
- await merchant.start();
- await merchant.pingUntilAvailable();
-
- await merchant.addInstance({
- id: "default",
- name: "Default Instance",
- paytoUris: [`payto://iban/${merchantIban}?receiver-name=Merchant`],
- defaultWireTransferDelay: Duration.toTalerProtocolDuration(
- Duration.getZero(),
- ),
- });
-
- console.log("setup done!");
-
- const wallet = new WalletCli(t);
-
- return {
- commonDb: db,
- exchange,
- merchant,
- wallet,
- exchangeBankAccount,
- libeufinNexus,
- libeufinSandbox,
- };
-}
-
-/**
- * Run basic test with LibEuFin.
- */
-export async function runLibeufinBasicTest(t: GlobalTestState) {
- // Set up test environment
-
- const { wallet, exchange, merchant, libeufinSandbox, libeufinNexus } =
- await createLibeufinTestEnvironment(t);
-
- await wallet.client.call(WalletApiOperation.AddExchange, {
- exchangeBaseUrl: exchange.baseUrl,
- });
-
- const wr = await wallet.client.call(
- WalletApiOperation.AcceptManualWithdrawal,
- {
- exchangeBaseUrl: exchange.baseUrl,
- amount: "EUR:15",
- },
- );
-
- const reservePub: string = wr.reservePub;
-
- await LibeufinSandboxApi.simulateIncomingTransaction(
- libeufinSandbox,
- "exchangeacct",
- {
- amount: "15.00",
- debtorBic: customerBic,
- debtorIban: customerIban,
- debtorName: "Jane Customer",
- subject: `Taler Top-up ${reservePub}`,
- },
- );
-
- await LibeufinNexusApi.fetchTransactions(libeufinNexus, "myacct");
-
- await exchange.runWirewatchOnce();
-
- await wallet.runUntilDone();
-
- const bal = await wallet.client.call(WalletApiOperation.GetBalances, {});
- console.log("balances", JSON.stringify(bal, undefined, 2));
- t.assertAmountEquals(bal.balances[0].available, "EUR:14.7");
-
- const order: Partial<MerchantContractTerms> = {
- summary: "Buy me!",
- amount: "EUR:5",
- fulfillment_url: "taler://fulfillment-success/thx",
- wire_transfer_deadline: AbsoluteTime.toTimestamp(AbsoluteTime.now()),
- };
-
- await makeTestPayment(t, { wallet, merchant, order });
-
- await exchange.runAggregatorOnce();
- await exchange.runTransferOnce();
-
- await LibeufinNexusApi.submitAllPaymentInitiations(libeufinNexus, "myacct");
-
- const exchangeTransactions = await LibeufinSandboxApi.getAccountTransactions(
- libeufinSandbox,
- "exchangeacct",
- );
-
- console.log(
- "exchange transactions:",
- JSON.stringify(exchangeTransactions, undefined, 2),
- );
-
- t.assertDeepEqual(
- exchangeTransactions.payments[0].creditDebitIndicator,
- "credit",
- );
- t.assertDeepEqual(
- exchangeTransactions.payments[1].creditDebitIndicator,
- "debit",
- );
- t.assertDeepEqual(exchangeTransactions.payments[1].debtorIban, exchangeIban);
- t.assertDeepEqual(
- exchangeTransactions.payments[1].creditorIban,
- merchantIban,
- );
-}
-runLibeufinBasicTest.suites = ["libeufin"];
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-c5x.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-c5x.ts
deleted file mode 100644
index cc46c6d33..000000000
--- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-c5x.ts
+++ /dev/null
@@ -1,137 +0,0 @@
-/*
- 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 { GlobalTestState } from "../harness/harness.js";
-import {
- launchLibeufinServices,
- LibeufinNexusApi,
- NexusUserBundle,
- SandboxUserBundle,
-} from "../harness/libeufin.js";
-
-/**
- * This test checks how the C52 and C53 coordinate. It'll test
- * whether fresh transactions stop showing as C52 after they get
- * included in a bank statement.
- */
-export async function runLibeufinC5xTest(t: GlobalTestState) {
- /**
- * User saltetd "01"
- */
- const user01nexus = new NexusUserBundle(
- "01",
- "http://localhost:5010/ebicsweb",
- );
- const user01sandbox = new SandboxUserBundle("01");
-
- /**
- * User saltetd "02".
- */
- const user02nexus = new NexusUserBundle(
- "02",
- "http://localhost:5010/ebicsweb",
- );
- const user02sandbox = new SandboxUserBundle("02");
-
- /**
- * Launch Sandbox and Nexus.
- */
- const libeufinServices = await launchLibeufinServices(
- t,
- [user01nexus, user02nexus],
- [user01sandbox, user02sandbox],
- ["twg"],
- );
-
- // Check that C52 and C53 have zero entries.
-
- // C52
- await LibeufinNexusApi.fetchTransactions(
- libeufinServices.libeufinNexus,
- user01nexus.localAccountName,
- "all", // range
- "report", // level
- );
- // C53
- await LibeufinNexusApi.fetchTransactions(
- libeufinServices.libeufinNexus,
- user01nexus.localAccountName,
- "all", // range
- "statement", // level
- );
- const nexusTxs = await LibeufinNexusApi.getAccountTransactions(
- libeufinServices.libeufinNexus,
- user01nexus.localAccountName,
- );
- t.assertTrue(nexusTxs.data["transactions"].length == 0);
-
- // Addressing one payment to user 01
- await libeufinServices.libeufinSandbox.makeTransaction(
- user02sandbox.ebicsBankAccount.label, // debit
- user01sandbox.ebicsBankAccount.label, // credit
- "EUR:10",
- "first payment",
- );
-
- // Checking that C52 has one and C53 has zero.
-
- let expectOne = await LibeufinNexusApi.fetchTransactions(
- libeufinServices.libeufinNexus,
- user01nexus.localAccountName,
- "all", // range
- "report", // C52
- );
- t.assertTrue(expectOne.data.newTransactions == 1);
- t.assertTrue(expectOne.data.downloadedTransactions == 1);
-
- let expectZero = await LibeufinNexusApi.fetchTransactions(
- libeufinServices.libeufinNexus,
- user01nexus.localAccountName,
- "all", // range
- "statement", // C53
- );
- t.assertTrue(expectZero.data.newTransactions == 0);
- t.assertTrue(expectZero.data.downloadedTransactions == 0);
-
- // Ticking now: the one payment should be downloaded
- // in a C53 but not in a C52. In any case, the payment
- // is not new anymore, because it was already ingested
- // when it was downloaded for the first time along the
- // c52 above.
- await libeufinServices.libeufinSandbox.c53tick();
-
- expectOne = await LibeufinNexusApi.fetchTransactions(
- libeufinServices.libeufinNexus,
- user01nexus.localAccountName,
- "all", // range
- "statement", // C53
- );
- t.assertTrue(expectOne.data.downloadedTransactions == 1);
- t.assertTrue(expectOne.data.newTransactions == 0);
-
- expectZero = await LibeufinNexusApi.fetchTransactions(
- libeufinServices.libeufinNexus,
- user01nexus.localAccountName,
- "all", // range
- "report", // C52
- );
- t.assertTrue(expectZero.data.downloadedTransactions == 0);
- t.assertTrue(expectZero.data.newTransactions == 0);
-}
-runLibeufinC5xTest.suites = ["libeufin"];
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-facade-anastasis.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-facade-anastasis.ts
deleted file mode 100644
index 1ed258c3a..000000000
--- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-facade-anastasis.ts
+++ /dev/null
@@ -1,169 +0,0 @@
-/*
- 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 { GlobalTestState } from "../harness/harness.js";
-import {
- SandboxUserBundle,
- NexusUserBundle,
- launchLibeufinServices,
- LibeufinNexusApi,
- LibeufinSandboxApi,
-} from "../harness/libeufin.js";
-
-/**
- * Testing the Anastasis API, offered by the Anastasis facade.
- */
-export async function runLibeufinAnastasisFacadeTest(t: GlobalTestState) {
- /**
- * User saltetd "01"
- */
- const user01nexus = new NexusUserBundle(
- "01",
- "http://localhost:5010/ebicsweb",
- );
- const user01sandbox = new SandboxUserBundle("01");
-
- /**
- * Launch Sandbox and Nexus.
- */
- const libeufinServices = await launchLibeufinServices(
- t,
- [user01nexus],
- [user01sandbox],
- ["anastasis"], // create only one Anastasis facade.
- );
- let resp = await LibeufinNexusApi.getAllFacades(
- libeufinServices.libeufinNexus,
- );
- // check that original facade shows up.
- t.assertTrue(resp.data["facades"][0]["name"] == user01nexus.anastasisReq["name"]);
- const anastasisBaseUrl: string = resp.data["facades"][0]["baseUrl"];
- t.assertTrue(typeof anastasisBaseUrl === "string");
- t.assertTrue(anastasisBaseUrl.startsWith("http://"));
- t.assertTrue(anastasisBaseUrl.endsWith("/"));
-
- await LibeufinNexusApi.fetchTransactions(
- libeufinServices.libeufinNexus,
- user01nexus.localAccountName,
- );
-
- await LibeufinNexusApi.postPermission(
- libeufinServices.libeufinNexus, {
- action: "grant",
- permission: {
- subjectId: user01nexus.userReq.username,
- subjectType: "user",
- resourceType: "facade",
- resourceId: user01nexus.anastasisReq.name,
- permissionName: "facade.anastasis.history",
- },
- }
- );
-
- // check if empty.
- let txsEmpty = await LibeufinNexusApi.getAnastasisTransactions(
- libeufinServices.libeufinNexus,
- anastasisBaseUrl, {delta: 5})
-
- t.assertTrue(txsEmpty.data.incoming_transactions.length == 0);
-
- LibeufinSandboxApi.simulateIncomingTransaction(
- libeufinServices.libeufinSandbox,
- user01sandbox.ebicsBankAccount.label,
- {
- debtorIban: "ES3314655813489414469157",
- debtorBic: "BCMAESM1XXX",
- debtorName: "Mock Donor",
- subject: "Anastasis donation",
- amount: "3", // Sandbox takes currency from its 'config'
- },
- )
-
- LibeufinSandboxApi.simulateIncomingTransaction(
- libeufinServices.libeufinSandbox,
- user01sandbox.ebicsBankAccount.label,
- {
- debtorIban: "ES3314655813489414469157",
- debtorBic: "BCMAESM1XXX",
- debtorName: "Mock Donor",
- subject: "another Anastasis donation",
- amount: "1", // Sandbox takes currency from its "config"
- },
- )
-
- await LibeufinNexusApi.fetchTransactions(
- libeufinServices.libeufinNexus,
- user01nexus.localAccountName,
- );
-
- let txs = await LibeufinNexusApi.getAnastasisTransactions(
- libeufinServices.libeufinNexus,
- anastasisBaseUrl,
- {delta: 5},
- user01nexus.userReq.username,
- user01nexus.userReq.password,
- );
-
- // check the two payments show up
- let txsList = txs.data.incoming_transactions
- t.assertTrue(txsList.length == 2);
- t.assertTrue([txsList[0].subject, txsList[1].subject].includes("Anastasis donation"));
- t.assertTrue([txsList[0].subject, txsList[1].subject].includes("another Anastasis donation"));
- t.assertTrue(txsList[0].row_id == 1)
- t.assertTrue(txsList[1].row_id == 2)
-
- LibeufinSandboxApi.simulateIncomingTransaction(
- libeufinServices.libeufinSandbox,
- user01sandbox.ebicsBankAccount.label,
- {
- debtorIban: "ES3314655813489414469157",
- debtorBic: "BCMAESM1XXX",
- debtorName: "Mock Donor",
- subject: "last Anastasis donation",
- amount: "10.10", // Sandbox takes currency from its "config"
- },
- )
-
- await LibeufinNexusApi.fetchTransactions(
- libeufinServices.libeufinNexus,
- user01nexus.localAccountName,
- );
-
- let txsLast = await LibeufinNexusApi.getAnastasisTransactions(
- libeufinServices.libeufinNexus,
- anastasisBaseUrl,
- {delta: 5, start: 2},
- user01nexus.userReq.username,
- user01nexus.userReq.password,
- );
- console.log(txsLast.data.incoming_transactions[0].subject == "last Anastasis donation");
-
- let txsReverse = await LibeufinNexusApi.getAnastasisTransactions(
- libeufinServices.libeufinNexus,
- anastasisBaseUrl,
- {delta: -5, start: 4},
- user01nexus.userReq.username,
- user01nexus.userReq.password,
- );
- t.assertTrue(txsReverse.data.incoming_transactions[0].row_id == 3);
- t.assertTrue(txsReverse.data.incoming_transactions[1].row_id == 2);
- t.assertTrue(txsReverse.data.incoming_transactions[2].row_id == 1);
-}
-
-runLibeufinAnastasisFacadeTest.suites = ["libeufin"];
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-keyrotation.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-keyrotation.ts
deleted file mode 100644
index 21bf07de2..000000000
--- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-keyrotation.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- 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 { GlobalTestState } from "../harness/harness.js";
-import {
- SandboxUserBundle,
- NexusUserBundle,
- launchLibeufinServices,
- LibeufinSandboxApi,
- LibeufinNexusApi,
-} from "../harness/libeufin.js";
-
-/**
- * Run basic test with LibEuFin.
- */
-export async function runLibeufinKeyrotationTest(t: GlobalTestState) {
- /**
- * User saltetd "01"
- */
- const user01nexus = new NexusUserBundle(
- "01",
- "http://localhost:5010/ebicsweb",
- );
- const user01sandbox = new SandboxUserBundle("01");
-
- /**
- * Launch Sandbox and Nexus.
- */
- const libeufinServices = await launchLibeufinServices(
- t, [user01nexus], [user01sandbox],
- );
-
- await LibeufinNexusApi.fetchTransactions(
- libeufinServices.libeufinNexus,
- user01nexus.localAccountName,
- );
-
- /* Rotate the Sandbox keys, and fetch the transactions again */
- await LibeufinSandboxApi.rotateKeys(
- libeufinServices.libeufinSandbox,
- user01sandbox.ebicsBankAccount.subscriber.hostID,
- );
-
- try {
- await LibeufinNexusApi.fetchTransactions(
- libeufinServices.libeufinNexus,
- user01nexus.localAccountName,
- );
- } catch (e: any) {
- /**
- * Asserting that Nexus responded with a 500 Internal server
- * error, because the bank signed the last response with a new
- * key pair that was never downloaded by Nexus.
- *
- * NOTE: the bank accepted the request addressed to the old
- * public key. Should it in this case reject the request even
- * before trying to verify it?
- */
- t.assertTrue(e.response.status == 500);
- t.assertTrue(e.response.data.code == 9000);
- }
-}
-runLibeufinKeyrotationTest.suites = ["libeufin"];
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-nexus-balance.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-nexus-balance.ts
deleted file mode 100644
index a52de8d96..000000000
--- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-nexus-balance.ts
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- 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 { GlobalTestState } from "../harness/harness.js";
-import {
- SandboxUserBundle,
- NexusUserBundle,
- launchLibeufinServices,
- LibeufinNexusApi,
-} from "../harness/libeufin.js";
-
-/**
- * This test checks how the C52 and C53 coordinate. It'll test
- * whether fresh transactions stop showing as C52 after they get
- * included in a bank statement.
- */
-export async function runLibeufinNexusBalanceTest(t: GlobalTestState) {
- /**
- * User saltetd "01"
- */
- const user01nexus = new NexusUserBundle(
- "01",
- "http://localhost:5010/ebicsweb",
- );
- const user01sandbox = new SandboxUserBundle("01");
-
- /**
- * User saltetd "02".
- */
- const user02nexus = new NexusUserBundle(
- "02",
- "http://localhost:5010/ebicsweb",
- );
- const user02sandbox = new SandboxUserBundle("02");
-
- /**
- * Launch Sandbox and Nexus.
- */
- const libeufinServices = await launchLibeufinServices(
- t,
- [user01nexus, user02nexus],
- [user01sandbox, user02sandbox],
- ["twg"],
- );
-
- // user 01 gets 10
- await libeufinServices.libeufinSandbox.makeTransaction(
- user02sandbox.ebicsBankAccount.label, // debit
- user01sandbox.ebicsBankAccount.label, // credit
- "EUR:10",
- "first payment",
- );
-
- // user 01 gets another 10
- await libeufinServices.libeufinSandbox.makeTransaction(
- user02sandbox.ebicsBankAccount.label, // debit
- user01sandbox.ebicsBankAccount.label, // credit
- "EUR:10",
- "second payment",
- );
-
- await LibeufinNexusApi.fetchTransactions(
- libeufinServices.libeufinNexus,
- user01nexus.localAccountName,
- "all", // range
- "report", // level
- );
-
- // Check that user 01 has 20, via Nexus.
- let accountInfo = await LibeufinNexusApi.getBankAccount(
- libeufinServices.libeufinNexus,
- user01nexus.localAccountName,
- );
- t.assertAmountEquals(accountInfo.data.lastSeenBalance, "EUR:20");
-
- // user 01 gives 30
- await libeufinServices.libeufinSandbox.makeTransaction(
- user01sandbox.ebicsBankAccount.label, // credit
- user02sandbox.ebicsBankAccount.label, // debit
- "EUR:30",
- "third payment",
- );
-
- await LibeufinNexusApi.fetchTransactions(
- libeufinServices.libeufinNexus,
- user01nexus.localAccountName,
- "all", // range
- "report", // level
- );
-
- let accountInfoDebit = await LibeufinNexusApi.getBankAccount(
- libeufinServices.libeufinNexus,
- user01nexus.localAccountName,
- );
- t.assertDeepEqual(accountInfoDebit.data.lastSeenBalance, "-EUR:10");
-}
-
-runLibeufinNexusBalanceTest.suites = ["libeufin"];
-runLibeufinNexusBalanceTest.excludeByDefault = true;
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-refund-multiple-users.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-refund-multiple-users.ts
deleted file mode 100644
index 245f34331..000000000
--- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-refund-multiple-users.ts
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- 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 { GlobalTestState, delayMs } from "../harness/harness.js";
-import {
- SandboxUserBundle,
- NexusUserBundle,
- launchLibeufinServices,
- LibeufinSandboxApi,
- LibeufinNexusApi,
-} from "../harness/libeufin.js";
-
-/**
- * User 01 expects a refund from user 02, and expectedly user 03
- * should not be involved in the process.
- */
-export async function runLibeufinRefundMultipleUsersTest(t: GlobalTestState) {
- /**
- * User saltetd "01"
- */
- const user01nexus = new NexusUserBundle(
- "01",
- "http://localhost:5010/ebicsweb",
- );
- const user01sandbox = new SandboxUserBundle("01");
-
- /**
- * User saltetd "02"
- */
- const user02nexus = new NexusUserBundle(
- "02",
- "http://localhost:5010/ebicsweb",
- );
- const user02sandbox = new SandboxUserBundle("02");
-
- /**
- * User saltetd "03"
- */
- const user03nexus = new NexusUserBundle(
- "03",
- "http://localhost:5010/ebicsweb",
- );
- const user03sandbox = new SandboxUserBundle("03");
-
- /**
- * Launch Sandbox and Nexus.
- */
- const libeufinServices = await launchLibeufinServices(
- t,
- [user01nexus, user02nexus],
- [user01sandbox, user02sandbox],
- ["twg"],
- );
-
- // user 01 gets the payment
- await libeufinServices.libeufinSandbox.makeTransaction(
- user02sandbox.ebicsBankAccount.label, // debit
- user01sandbox.ebicsBankAccount.label, // credit
- "EUR:1",
- "not a public key",
- );
-
- // user 01 fetches the payments
- await LibeufinNexusApi.fetchTransactions(
- libeufinServices.libeufinNexus,
- user01nexus.localAccountName,
- );
-
- // user 01 tries to submit the reimbursement, as
- // the payment didn't have a valid public key in
- // the subject.
- await LibeufinNexusApi.submitInitiatedPayment(
- libeufinServices.libeufinNexus,
- user01nexus.localAccountName,
- "1", // so far the only one that can exist.
- );
-
- // user 02 checks whether a reimbursement arrived.
- let history = await LibeufinSandboxApi.getAccountTransactions(
- libeufinServices.libeufinSandbox,
- user02sandbox.ebicsBankAccount["label"],
- );
- // reimbursement arrived IFF the total payments are 2:
- // 1 the original (faulty) transaction + 1 the reimbursement.
- t.assertTrue(history["payments"].length == 2);
-}
-
-runLibeufinRefundMultipleUsersTest.suites = ["libeufin"];
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-refund.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-refund.ts
deleted file mode 100644
index 9d90121a0..000000000
--- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-refund.ts
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- 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 { GlobalTestState, delayMs } from "../harness/harness.js";
-import {
- SandboxUserBundle,
- NexusUserBundle,
- launchLibeufinServices,
- LibeufinSandboxApi,
- LibeufinNexusApi,
-} from "../harness/libeufin.js";
-
-/**
- * Run basic test with LibEuFin.
- */
-export async function runLibeufinRefundTest(t: GlobalTestState) {
- /**
- * User saltetd "01"
- */
- const user01nexus = new NexusUserBundle(
- "01",
- "http://localhost:5010/ebicsweb",
- );
- const user01sandbox = new SandboxUserBundle("01");
-
- /**
- * User saltetd "02"
- */
- const user02nexus = new NexusUserBundle(
- "02",
- "http://localhost:5010/ebicsweb",
- );
- const user02sandbox = new SandboxUserBundle("02");
-
- /**
- * Launch Sandbox and Nexus.
- */
- const libeufinServices = await launchLibeufinServices(
- t,
- [user01nexus, user02nexus],
- [user01sandbox, user02sandbox],
- ["twg"],
- );
-
- // user 02 pays user 01 with a faulty (non Taler) subject.
- await libeufinServices.libeufinSandbox.makeTransaction(
- user02sandbox.ebicsBankAccount.label, // debit
- user01sandbox.ebicsBankAccount.label, // credit
- "EUR:1",
- "not a public key",
- );
-
- // The bad payment should be now ingested and prepared as
- // a reimbursement.
- await LibeufinNexusApi.fetchTransactions(
- libeufinServices.libeufinNexus,
- user01nexus.localAccountName,
- );
- // Check that the payment arrived at the Nexus.
- const nexusTxs = await LibeufinNexusApi.getAccountTransactions(
- libeufinServices.libeufinNexus,
- user01nexus.localAccountName,
- );
- t.assertTrue(nexusTxs.data["transactions"].length == 1);
-
- // Submit the reimbursement
- await LibeufinNexusApi.submitInitiatedPayment(
- libeufinServices.libeufinNexus,
- user01nexus.localAccountName,
- // The initiated payment (= the reimbursement) ID below
- // got set by the Taler facade; at this point only one must
- // exist. If "1" is not found, a 404 will make this test fail.
- "1",
- );
-
- // user 02 checks whether the reimbursement arrived.
- let history = await LibeufinSandboxApi.getAccountTransactions(
- libeufinServices.libeufinSandbox,
- user02sandbox.ebicsBankAccount["label"],
- );
- // 2 payments must exist: 1 the original (faulty) payment +
- // 1 the reimbursement.
- t.assertTrue(history["payments"].length == 2);
-}
-runLibeufinRefundTest.suites = ["libeufin"];
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-sandbox-wire-transfer-cli.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-sandbox-wire-transfer-cli.ts
deleted file mode 100644
index 7f274f554..000000000
--- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-sandbox-wire-transfer-cli.ts
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- 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 { GlobalTestState } from "../harness/harness.js";
-import {
- LibeufinSandboxApi,
- LibeufinSandboxService,
-} from "../harness/libeufin.js";
-
-export async function runLibeufinSandboxWireTransferCliTest(
- t: GlobalTestState,
-) {
- const sandbox = await LibeufinSandboxService.create(t, {
- httpPort: 5012,
- databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-sandbox.sqlite3`,
- });
- await sandbox.start();
- await sandbox.pingUntilAvailable();
- await LibeufinSandboxApi.createBankAccount(sandbox, {
- iban: "DE71500105179674997361",
- bic: "BELADEBEXXX",
- name: "Mock Name",
- label: "mock-account",
- });
-
- await LibeufinSandboxApi.createBankAccount(sandbox, {
- iban: "DE71500105179674997364",
- bic: "BELADEBEXXX",
- name: "Mock Name 2",
- label: "mock-account-2",
- });
- await sandbox.makeTransaction(
- "mock-account",
- "mock-account-2",
- "EUR:1",
- "one!",
- );
- await sandbox.makeTransaction(
- "mock-account",
- "mock-account-2",
- "EUR:1",
- "two!",
- );
- await sandbox.makeTransaction(
- "mock-account",
- "mock-account-2",
- "EUR:1",
- "three!",
- );
- await sandbox.makeTransaction(
- "mock-account-2",
- "mock-account",
- "EUR:1",
- "Give one back.",
- );
- await sandbox.makeTransaction(
- "mock-account-2",
- "mock-account",
- "EUR:0.11",
- "Give fraction back.",
- );
- let ret = await LibeufinSandboxApi.getAccountInfoWithBalance(
- sandbox,
- "mock-account-2",
- );
- console.log(ret.data.balance);
- t.assertTrue(ret.data.balance == "EUR:1.89");
-}
-runLibeufinSandboxWireTransferCliTest.suites = ["libeufin"];
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-tutorial.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-tutorial.ts
deleted file mode 100644
index 0701fd1c5..000000000
--- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-tutorial.ts
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- 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 { GlobalTestState } from "../harness/harness.js";
-import {
- LibeufinNexusService,
- LibeufinSandboxService,
- LibeufinCli,
-} from "../harness/libeufin.js";
-
-/**
- * Run basic test with LibEuFin.
- */
-export async function runLibeufinTutorialTest(t: GlobalTestState) {
- // Set up test environment
-
- const libeufinSandbox = await LibeufinSandboxService.create(t, {
- httpPort: 5010,
- databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-sandbox.sqlite3`,
- });
-
- await libeufinSandbox.start();
- await libeufinSandbox.pingUntilAvailable();
-
- const libeufinNexus = await LibeufinNexusService.create(t, {
- httpPort: 5011,
- databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-nexus.sqlite3`,
- });
-
- const nexusUser = { username: "foo", password: "secret" };
- const libeufinCli = new LibeufinCli(t, {
- sandboxUrl: libeufinSandbox.baseUrl,
- nexusUrl: libeufinNexus.baseUrl,
- sandboxDatabaseUri: `jdbc:sqlite:${t.testDir}/libeufin-sandbox.sqlite3`,
- nexusDatabaseUri: `jdbc:sqlite:${t.testDir}/libeufin-nexus.sqlite3`,
- user: nexusUser,
- });
-
- const ebicsDetails = {
- hostId: "testhost",
- partnerId: "partner01",
- userId: "user01",
- };
- const bankAccountDetails = {
- currency: "EUR",
- iban: "DE18500105172929531888",
- bic: "INGDDEFFXXX",
- personName: "Jane Normal",
- accountName: "testacct01",
- };
-
- await libeufinCli.checkSandbox();
- await libeufinCli.createEbicsHost("testhost");
- await libeufinCli.createEbicsSubscriber(ebicsDetails);
- await libeufinCli.createEbicsBankAccount(ebicsDetails, bankAccountDetails);
- await libeufinCli.generateTransactions(bankAccountDetails.accountName);
-
- await libeufinNexus.start();
- await libeufinNexus.pingUntilAvailable();
-
- await libeufinNexus.createNexusSuperuser(nexusUser);
- const connectionDetails = {
- subscriberDetails: ebicsDetails,
- ebicsUrl: `${libeufinSandbox.baseUrl}ebicsweb`, // FIXME: need appropriate URL concatenation
- connectionName: "my-ebics-conn",
- };
- await libeufinCli.createEbicsConnection(connectionDetails);
- await libeufinCli.createBackupFile({
- passphrase: "secret",
- outputFile: `${t.testDir}/connection-backup.json`,
- connectionName: connectionDetails.connectionName,
- });
- await libeufinCli.createKeyLetter({
- outputFile: `${t.testDir}/letter.pdf`,
- connectionName: connectionDetails.connectionName,
- });
- await libeufinCli.connect(connectionDetails.connectionName);
- await libeufinCli.downloadBankAccounts(connectionDetails.connectionName);
- await libeufinCli.listOfferedBankAccounts(connectionDetails.connectionName);
-
- const bankAccountImportDetails = {
- offeredBankAccountName: bankAccountDetails.accountName,
- nexusBankAccountName: "at-nexus-testacct01",
- connectionName: connectionDetails.connectionName,
- };
-
- await libeufinCli.importBankAccount(bankAccountImportDetails);
- await libeufinSandbox.c53tick()
- await libeufinCli.fetchTransactions(bankAccountImportDetails.nexusBankAccountName);
- await libeufinCli.transactions(bankAccountImportDetails.nexusBankAccountName);
-
- const paymentDetails = {
- creditorIban: "DE42500105171245624648",
- creditorBic: "BELADEBEXXX",
- creditorName: "Mina Musterfrau",
- subject: "Purchase 01234",
- amount: "1.0",
- currency: "EUR",
- nexusBankAccountName: bankAccountImportDetails.nexusBankAccountName,
- };
- await libeufinCli.preparePayment(paymentDetails);
- await libeufinCli.submitPayment(paymentDetails, "1");
-
- await libeufinCli.newTalerWireGatewayFacade({
- accountName: bankAccountImportDetails.nexusBankAccountName,
- connectionName: "my-ebics-conn",
- currency: "EUR",
- facadeName: "my-twg",
- });
- await libeufinCli.listFacades();
-}
-runLibeufinTutorialTest.suites = ["libeufin"];
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-payment-on-demo.ts b/packages/taler-wallet-cli/src/integrationtests/test-payment-on-demo.ts
deleted file mode 100644
index 737620ce7..000000000
--- a/packages/taler-wallet-cli/src/integrationtests/test-payment-on-demo.ts
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- 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 { GlobalTestState, WalletCli } from "../harness/harness.js";
-import { makeTestPayment } from "../harness/helpers.js";
-import {
- WalletApiOperation,
- BankApi,
- BankAccessApi,
- BankServiceHandle,
- NodeHttpLib,
-} from "@gnu-taler/taler-wallet-core";
-
-/**
- * Run test for basic, bank-integrated withdrawal and payment.
- */
-export async function runPaymentDemoTest(t: GlobalTestState) {
- // Withdraw digital cash into the wallet.
- let bankInterface: BankServiceHandle = {
- baseUrl: "https://bank.demo.taler.net/",
- bankAccessApiBaseUrl: "https://bank.demo.taler.net/",
- http: new NodeHttpLib(),
- };
- let user = await BankApi.createRandomBankUser(bankInterface);
- let wop = await BankAccessApi.createWithdrawalOperation(
- bankInterface,
- user,
- "KUDOS:20",
- );
-
- let wallet = new WalletCli(t);
- await wallet.client.call(WalletApiOperation.GetWithdrawalDetailsForUri, {
- talerWithdrawUri: wop.taler_withdraw_uri,
- });
-
- await wallet.runPending();
-
- // Confirm it
-
- await BankApi.confirmWithdrawalOperation(bankInterface, user, wop);
-
- // Withdraw
-
- await wallet.client.call(WalletApiOperation.AcceptBankIntegratedWithdrawal, {
- exchangeBaseUrl: "https://exchange.demo.taler.net/",
- talerWithdrawUri: wop.taler_withdraw_uri,
- });
- await wallet.runUntilDone();
-
- let balanceBefore = await wallet.client.call(
- WalletApiOperation.GetBalances,
- {},
- );
- t.assertTrue(balanceBefore["balances"].length == 1);
-
- const order = {
- summary: "Buy me!",
- amount: "KUDOS:5",
- fulfillment_url: "taler://fulfillment-success/thx",
- };
-
- let merchant = {
- makeInstanceBaseUrl: function (instanceName?: string) {
- return "https://backend.demo.taler.net/instances/donations/";
- },
- port: 0,
- name: "donations",
- };
-
- t.assertTrue("TALER_ENV_FRONTENDS_APITOKEN" in process.env);
-
- await makeTestPayment(
- t,
- {
- merchant,
- wallet,
- order,
- },
- {
- Authorization: `Bearer ${process.env["TALER_ENV_FRONTENDS_APITOKEN"]}`,
- },
- );
-
- await wallet.runUntilDone();
-
- let balanceAfter = await wallet.client.call(
- WalletApiOperation.GetBalances,
- {},
- );
- t.assertTrue(balanceAfter["balances"].length == 1);
- t.assertTrue(
- balanceBefore["balances"][0]["available"] >
- balanceAfter["balances"][0]["available"],
- );
-}
-
-runPaymentDemoTest.excludeByDefault = true;
-runPaymentDemoTest.suites = ["buildbot"];
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-peer-to-peer-pull.ts b/packages/taler-wallet-cli/src/integrationtests/test-peer-to-peer-pull.ts
deleted file mode 100644
index 211f20494..000000000
--- a/packages/taler-wallet-cli/src/integrationtests/test-peer-to-peer-pull.ts
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2020 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- * Imports.
- */
-import { AbsoluteTime, Duration, j2s } from "@gnu-taler/taler-util";
-import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-import { GlobalTestState, WalletCli } from "../harness/harness.js";
-import {
- createSimpleTestkudosEnvironment,
- withdrawViaBank,
-} from "../harness/helpers.js";
-
-/**
- * Run test for basic, bank-integrated withdrawal and payment.
- */
-export async function runPeerToPeerPullTest(t: GlobalTestState) {
- // Set up test environment
-
- const { bank, exchange, merchant } = await createSimpleTestkudosEnvironment(
- t,
- );
-
- // Withdraw digital cash into the wallet.
- const wallet1 = new WalletCli(t, "w1");
- const wallet2 = new WalletCli(t, "w2");
- await withdrawViaBank(t, {
- wallet: wallet2,
- bank,
- exchange,
- amount: "TESTKUDOS:20",
- });
-
- await wallet1.runUntilDone();
-
- const purse_expiration = AbsoluteTime.toTimestamp(
- AbsoluteTime.addDuration(
- AbsoluteTime.now(),
- Duration.fromSpec({ days: 2 }),
- ),
- );
-
- const resp = await wallet1.client.call(
- WalletApiOperation.InitiatePeerPullPayment,
- {
- exchangeBaseUrl: exchange.baseUrl,
- partialContractTerms: {
- summary: "Hello World",
- amount: "TESTKUDOS:5",
- purse_expiration
- },
- },
- );
-
- const checkResp = await wallet2.client.call(
- WalletApiOperation.CheckPeerPullPayment,
- {
- talerUri: resp.talerUri,
- },
- );
-
- console.log(`checkResp: ${j2s(checkResp)}`);
-
- const acceptResp = await wallet2.client.call(
- WalletApiOperation.AcceptPeerPullPayment,
- {
- peerPullPaymentIncomingId: checkResp.peerPullPaymentIncomingId,
- },
- );
-
- await wallet1.runUntilDone();
- await wallet2.runUntilDone();
-
- const txn1 = await wallet1.client.call(
- WalletApiOperation.GetTransactions,
- {},
- );
- const txn2 = await wallet2.client.call(
- WalletApiOperation.GetTransactions,
- {},
- );
-
- console.log(`txn1: ${j2s(txn1)}`);
- console.log(`txn2: ${j2s(txn2)}`);
-}
-
-runPeerToPeerPullTest.suites = ["wallet"];
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-peer-to-peer-push.ts b/packages/taler-wallet-cli/src/integrationtests/test-peer-to-peer-push.ts
deleted file mode 100644
index 4aaeca624..000000000
--- a/packages/taler-wallet-cli/src/integrationtests/test-peer-to-peer-push.ts
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2020 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- * Imports.
- */
-import { AbsoluteTime, Duration, j2s } from "@gnu-taler/taler-util";
-import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-import { GlobalTestState, WalletCli } from "../harness/harness.js";
-import {
- createSimpleTestkudosEnvironment,
- withdrawViaBank,
-} from "../harness/helpers.js";
-
-/**
- * Run test for basic, bank-integrated withdrawal and payment.
- */
-export async function runPeerToPeerPushTest(t: GlobalTestState) {
- // Set up test environment
-
- const { bank, exchange } = await createSimpleTestkudosEnvironment(t);
-
- const wallet1 = new WalletCli(t, "w1");
- const wallet2 = new WalletCli(t, "w2");
-
- // Withdraw digital cash into the wallet.
-
- await withdrawViaBank(t, {
- wallet: wallet1,
- bank,
- exchange,
- amount: "TESTKUDOS:20",
- });
-
- await wallet1.runUntilDone();
-
- const purse_expiration = AbsoluteTime.toTimestamp(
- AbsoluteTime.addDuration(
- AbsoluteTime.now(),
- Duration.fromSpec({ days: 2 }),
- ),
- );
-
- {
- const resp = await wallet1.client.call(
- WalletApiOperation.InitiatePeerPushPayment,
- {
- partialContractTerms: {
- summary: "Hello World",
- amount: "TESTKUDOS:5",
- purse_expiration
- },
- },
- );
-
- console.log(resp);
-
- }
- const resp = await wallet1.client.call(
- WalletApiOperation.InitiatePeerPushPayment,
- {
- partialContractTerms: {
- summary: "Hello World",
- amount: "TESTKUDOS:5",
- purse_expiration
- },
- },
- );
-
- console.log(resp);
-
- const checkResp = await wallet2.client.call(
- WalletApiOperation.CheckPeerPushPayment,
- {
- talerUri: resp.talerUri,
- },
- );
-
- console.log(checkResp);
-
- const acceptResp = await wallet2.client.call(
- WalletApiOperation.AcceptPeerPushPayment,
- {
- peerPushPaymentIncomingId: checkResp.peerPushPaymentIncomingId,
- },
- );
-
- console.log(acceptResp);
-
- await wallet1.runUntilDone();
- await wallet2.runUntilDone();
-
- const txn1 = await wallet1.client.call(
- WalletApiOperation.GetTransactions,
- {},
- );
- const txn2 = await wallet2.client.call(
- WalletApiOperation.GetTransactions,
- {},
- );
-
- console.log(`txn1: ${j2s(txn1)}`);
- console.log(`txn2: ${j2s(txn2)}`);
-}
-
-runPeerToPeerPushTest.suites = ["wallet"];
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-tipping.ts b/packages/taler-wallet-cli/src/integrationtests/test-tipping.ts
deleted file mode 100644
index d31e0c06b..000000000
--- a/packages/taler-wallet-cli/src/integrationtests/test-tipping.ts
+++ /dev/null
@@ -1,129 +0,0 @@
-/*
- 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 { WalletApiOperation, BankApi } from "@gnu-taler/taler-wallet-core";
-import {
- GlobalTestState,
- MerchantPrivateApi,
- getWireMethodForTest,
-} from "../harness/harness.js";
-import { createSimpleTestkudosEnvironment } from "../harness/helpers.js";
-
-/**
- * Run test for basic, bank-integrated withdrawal.
- */
-export async function runTippingTest(t: GlobalTestState) {
- // Set up test environment
-
- const { wallet, bank, exchange, merchant, exchangeBankAccount } =
- await createSimpleTestkudosEnvironment(t);
-
- const mbu = await BankApi.createRandomBankUser(bank);
-
- const tipReserveResp = await MerchantPrivateApi.createTippingReserve(
- merchant,
- "default",
- {
- exchange_url: exchange.baseUrl,
- initial_balance: "TESTKUDOS:10",
- wire_method: getWireMethodForTest(),
- },
- );
-
- console.log("tipReserveResp:", tipReserveResp);
-
- t.assertDeepEqual(
- tipReserveResp.payto_uri,
- exchangeBankAccount.accountPaytoUri,
- );
-
- await BankApi.adminAddIncoming(bank, {
- amount: "TESTKUDOS:10",
- debitAccountPayto: mbu.accountPaytoUri,
- exchangeBankAccount,
- reservePub: tipReserveResp.reserve_pub,
- });
-
- await exchange.runWirewatchOnce();
-
- await merchant.stop();
- await merchant.start();
- await merchant.pingUntilAvailable();
-
- const r = await MerchantPrivateApi.queryTippingReserves(merchant, "default");
- console.log("tipping reserves:", JSON.stringify(r, undefined, 2));
-
- t.assertTrue(r.reserves.length === 1);
- t.assertDeepEqual(
- r.reserves[0].exchange_initial_amount,
- r.reserves[0].merchant_initial_amount,
- );
-
- const tip = await MerchantPrivateApi.giveTip(merchant, "default", {
- amount: "TESTKUDOS:5",
- justification: "why not?",
- next_url: "https://example.com/after-tip",
- });
-
- console.log("created tip", tip);
-
- const doTip = async (): Promise<void> => {
- const ptr = await wallet.client.call(WalletApiOperation.PrepareTip, {
- talerTipUri: tip.taler_tip_uri,
- });
-
- console.log(ptr);
-
- t.assertAmountEquals(ptr.tipAmountRaw, "TESTKUDOS:5");
- t.assertAmountEquals(ptr.tipAmountEffective, "TESTKUDOS:4.85");
-
- await wallet.client.call(WalletApiOperation.AcceptTip, {
- walletTipId: ptr.walletTipId,
- });
-
- await wallet.runUntilDone();
-
- const bal = await wallet.client.call(WalletApiOperation.GetBalances, {});
-
- console.log(bal);
-
- t.assertAmountEquals(bal.balances[0].available, "TESTKUDOS:4.85");
-
- const txns = await wallet.client.call(
- WalletApiOperation.GetTransactions,
- {},
- );
-
- console.log("Transactions:", JSON.stringify(txns, undefined, 2));
-
- t.assertDeepEqual(txns.transactions[0].type, "tip");
- t.assertDeepEqual(txns.transactions[0].pending, false);
- t.assertAmountEquals(
- txns.transactions[0].amountEffective,
- "TESTKUDOS:4.85",
- );
- t.assertAmountEquals(txns.transactions[0].amountRaw, "TESTKUDOS:5.0");
- };
-
- // Check twice so make sure tip handling is idempotent
- await doTip();
- await doTip();
-}
-
-runTippingTest.suites = ["wallet", "wallet-tipping"];
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-bank-integrated.ts b/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-bank-integrated.ts
deleted file mode 100644
index dc7298e5d..000000000
--- a/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-bank-integrated.ts
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- 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 { GlobalTestState } from "../harness/harness.js";
-import { createSimpleTestkudosEnvironment } from "../harness/helpers.js";
-import {
- WalletApiOperation,
- BankApi,
- BankAccessApi,
-} from "@gnu-taler/taler-wallet-core";
-import { j2s } from "@gnu-taler/taler-util";
-
-/**
- * Run test for basic, bank-integrated withdrawal.
- */
-export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) {
- // Set up test environment
-
- const { wallet, bank, exchange } = await createSimpleTestkudosEnvironment(t);
-
- // Create a withdrawal operation
-
- const user = await BankApi.createRandomBankUser(bank);
- const wop = await BankAccessApi.createWithdrawalOperation(
- bank,
- user,
- "TESTKUDOS:10",
- );
-
- // Hand it to the wallet
-
- const r1 = await wallet.client.call(
- WalletApiOperation.GetWithdrawalDetailsForUri,
- {
- talerWithdrawUri: wop.taler_withdraw_uri,
- },
- );
-
- await wallet.runPending();
-
- // Withdraw
-
- const r2 = await wallet.client.call(
- WalletApiOperation.AcceptBankIntegratedWithdrawal,
- {
- exchangeBaseUrl: exchange.baseUrl,
- talerWithdrawUri: wop.taler_withdraw_uri,
- },
- );
- // Do it twice to check idempotency
- const r3 = await wallet.client.call(
- WalletApiOperation.AcceptBankIntegratedWithdrawal,
- {
- exchangeBaseUrl: exchange.baseUrl,
- talerWithdrawUri: wop.taler_withdraw_uri,
- },
- );
- await wallet.runPending();
-
- // Confirm it
-
- await BankApi.confirmWithdrawalOperation(bank, user, wop);
-
- await wallet.runUntilDone();
-
- // Check balance
-
- const balResp = await wallet.client.call(WalletApiOperation.GetBalances, {});
- t.assertAmountEquals("TESTKUDOS:9.72", balResp.balances[0].available);
-
- const txn = await wallet.client.call(WalletApiOperation.GetTransactions, {});
- console.log(`transactions: ${j2s(txn)}`);
-}
-
-runWithdrawalBankIntegratedTest.suites = ["wallet"];
diff --git a/packages/taler-wallet-cli/tsconfig.json b/packages/taler-wallet-cli/tsconfig.json
index 447d3f946..4b46790c8 100644
--- a/packages/taler-wallet-cli/tsconfig.json
+++ b/packages/taler-wallet-cli/tsconfig.json
@@ -2,11 +2,11 @@
"compileOnSave": true,
"compilerOptions": {
"composite": true,
- "target": "ES2018",
- "module": "ESNext",
+ "target": "ES2020",
+ "module": "Node16",
"moduleResolution": "Node16",
"sourceMap": true,
- "lib": ["es6"],
+ "lib": ["ES2020"],
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"strict": true,
@@ -21,7 +21,7 @@
"baseUrl": "./src",
"typeRoots": ["./node_modules/@types"]
},
- "include": ["src/**/*"],
+ "include": ["src/**/*", "../taler-util/src/twrpc.ts"],
"references": [
{
"path": "../taler-wallet-core/"
diff --git a/packages/taler-wallet-core/.gitignore b/packages/taler-wallet-core/.gitignore
index cb5a51ac5..13d7285e1 100644
--- a/packages/taler-wallet-core/.gitignore
+++ b/packages/taler-wallet-core/.gitignore
@@ -1,2 +1,3 @@
/lib
/coverage
+/src/version.json
diff --git a/packages/taler-wallet-core/README.md b/packages/taler-wallet-core/README.md
new file mode 100644
index 000000000..73da40caf
--- /dev/null
+++ b/packages/taler-wallet-core/README.md
@@ -0,0 +1,4 @@
+# taler-wallet-core
+
+This package implements `taler-wallet-core`, the main logic and storage
+for the GNU Taler wallet.
diff --git a/packages/taler-wallet-core/package.json b/packages/taler-wallet-core/package.json
index 41448a01f..46b3cef4e 100644
--- a/packages/taler-wallet-core/package.json
+++ b/packages/taler-wallet-core/package.json
@@ -1,9 +1,9 @@
{
"name": "@gnu-taler/taler-wallet-core",
- "version": "0.9.0",
+ "version": "0.10.7",
"description": "",
"engines": {
- "node": ">=0.12.0"
+ "node": ">=0.18.0"
},
"repository": {
"type": "git",
@@ -12,13 +12,13 @@
"author": "Florian Dold",
"license": "GPL-3.0",
"scripts": {
- "prepare": "tsc",
"compile": "tsc",
"pretty": "prettier --write src",
"test": "tsc && ava",
+ "typedoc": "typedoc --out dist/typedoc ./src/",
"coverage": "tsc && c8 --src src --all ava",
"coverage:html": "tsc && c8 -r html --src src --all ava",
- "clean": "rimraf dist lib tsconfig.tsbuildinfo"
+ "clean": "rm -rf dist lib tsconfig.tsbuildinfo"
},
"files": [
"AUTHORS",
@@ -36,36 +36,48 @@
"browser": "./lib/index.browser.js",
"node": "./lib/index.node.js",
"default": "./lib/index.js"
+ },
+ "./remote": {
+ "default": "./lib/remote.js"
+ },
+ "./dbless": {
+ "default": "./lib/dbless.js"
+ }
+ },
+ "imports": {
+ "#host-impl": {
+ "types": "./lib/host-impl.node.js",
+ "node": "./lib/host-impl.node.js",
+ "qtart": "./lib/host-impl.qtart.js",
+ "default": "./lib/host-impl.missing.js"
}
},
"devDependencies": {
- "@ava/typescript": "^3.0.1",
+ "@ava/typescript": "^4.1.0",
"@gnu-taler/pogen": "workspace:*",
"@typescript-eslint/eslint-plugin": "^5.36.1",
"@typescript-eslint/parser": "^5.36.1",
- "ava": "^4.3.3",
- "c8": "^7.11.0",
+ "ava": "^6.0.1",
+ "c8": "^8.0.1",
"eslint": "^8.8.0",
- "eslint-config-airbnb-typescript": "^16.1.0",
- "eslint-plugin-import": "^2.25.4",
- "eslint-plugin-jsx-a11y": "^6.5.1",
- "eslint-plugin-react": "^7.28.0",
+ "eslint-config-airbnb-typescript": "^17.1.0",
+ "eslint-plugin-import": "^2.29.1",
+ "eslint-plugin-jsx-a11y": "^6.8.0",
+ "eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.3.0",
"jed": "^1.1.1",
"po2json": "^0.4.5",
- "prettier": "^2.5.1",
- "rimraf": "^3.0.2",
- "typedoc": "^0.23.16",
- "typescript": "^4.8.4"
+ "prettier": "^3.1.1",
+ "typedoc": "^0.25.4",
+ "typescript": "^5.3.3"
},
"dependencies": {
"@gnu-taler/idb-bridge": "workspace:*",
"@gnu-taler/taler-util": "workspace:*",
- "@types/node": "^18.8.5",
- "axios": "^0.27.2",
- "big-integer": "^1.6.51",
- "fflate": "^0.7.4",
- "tslib": "^2.4.0"
+ "@types/node": "^18.11.17",
+ "big-integer": "^1.6.52",
+ "fflate": "^0.8.1",
+ "tslib": "^2.6.2"
},
"ava": {
"files": [
diff --git a/packages/taler-wallet-core/src/operations/attention.ts b/packages/taler-wallet-core/src/attention.ts
index 95db7bde0..7a52ceaa3 100644
--- a/packages/taler-wallet-core/src/operations/attention.ts
+++ b/packages/taler-wallet-core/src/attention.ts
@@ -18,28 +18,28 @@
* Imports.
*/
import {
- AbsoluteTime,
AttentionInfo,
Logger,
- TalerProtocolTimestamp,
+ TalerPreciseTimestamp,
UserAttentionByIdRequest,
UserAttentionPriority,
+ UserAttentionUnreadList,
UserAttentionsCountResponse,
UserAttentionsRequest,
UserAttentionsResponse,
- UserAttentionUnreadList,
} from "@gnu-taler/taler-util";
-import { InternalWalletState } from "../internal-wallet-state.js";
+import { timestampPreciseFromDb, timestampPreciseToDb } from "./db.js";
+import { WalletExecutionContext } from "./wallet.js";
const logger = new Logger("operations/attention.ts");
export async function getUserAttentionsUnreadCount(
- ws: InternalWalletState,
+ wex: WalletExecutionContext,
req: UserAttentionsRequest,
): Promise<UserAttentionsCountResponse> {
- const total = await ws.db
- .mktx((x) => [x.userAttention])
- .runReadOnly(async (tx) => {
+ const total = await wex.db.runReadOnlyTx(
+ { storeNames: ["userAttention"] },
+ async (tx) => {
let count = 0;
await tx.userAttention.iter().forEach((x) => {
if (
@@ -52,18 +52,19 @@ export async function getUserAttentionsUnreadCount(
});
return count;
- });
+ },
+ );
return { total };
}
export async function getUserAttentions(
- ws: InternalWalletState,
+ wex: WalletExecutionContext,
req: UserAttentionsRequest,
): Promise<UserAttentionsResponse> {
- return await ws.db
- .mktx((x) => [x.userAttention])
- .runReadOnly(async (tx) => {
+ return await wex.db.runReadOnlyTx(
+ { storeNames: ["userAttention"] },
+ async (tx) => {
const pending: UserAttentionUnreadList = [];
await tx.userAttention.iter().forEach((x) => {
if (
@@ -73,73 +74,66 @@ export async function getUserAttentions(
return;
pending.push({
info: x.info,
- when: {
- t_ms: x.createdMs,
- },
+ when: timestampPreciseFromDb(x.created),
read: x.read !== undefined,
});
});
return { pending };
- });
+ },
+ );
}
export async function markAttentionRequestAsRead(
- ws: InternalWalletState,
+ wex: WalletExecutionContext,
req: UserAttentionByIdRequest,
): Promise<void> {
- await ws.db
- .mktx((x) => [x.userAttention])
- .runReadWrite(async (tx) => {
- const ua = await tx.userAttention.get([req.entityId, req.type]);
- if (!ua) throw Error("attention request not found");
- tx.userAttention.put({
- ...ua,
- read: TalerProtocolTimestamp.now(),
- });
+ await wex.db.runReadWriteTx({ storeNames: ["userAttention"] }, async (tx) => {
+ const ua = await tx.userAttention.get([req.entityId, req.type]);
+ if (!ua) throw Error("attention request not found");
+ tx.userAttention.put({
+ ...ua,
+ read: timestampPreciseToDb(TalerPreciseTimestamp.now()),
});
+ });
}
/**
* the wallet need the user attention to complete a task
* internal API
*
- * @param ws
+ * @param wex
* @param info
*/
export async function addAttentionRequest(
- ws: InternalWalletState,
+ wex: WalletExecutionContext,
info: AttentionInfo,
entityId: string,
): Promise<void> {
- await ws.db
- .mktx((x) => [x.userAttention])
- .runReadWrite(async (tx) => {
- await tx.userAttention.put({
- info,
- entityId,
- createdMs: AbsoluteTime.now().t_ms as number,
- read: undefined,
- });
+ await wex.db.runReadWriteTx({ storeNames: ["userAttention"] }, async (tx) => {
+ await tx.userAttention.put({
+ info,
+ entityId,
+ created: timestampPreciseToDb(TalerPreciseTimestamp.now()),
+ read: undefined,
});
+ });
}
/**
* user completed the task, attention request is not needed
* internal API
*
- * @param ws
+ * @param wex
* @param created
*/
export async function removeAttentionRequest(
- ws: InternalWalletState,
+ wex: WalletExecutionContext,
req: UserAttentionByIdRequest,
): Promise<void> {
- await ws.db
- .mktx((x) => [x.userAttention])
- .runReadWrite(async (tx) => {
- const ua = await tx.userAttention.get([req.entityId, req.type]);
- if (!ua) throw Error("attention request not found");
- await tx.userAttention.delete([req.entityId, req.type]);
- });
+ await wex.db.runReadWriteTx({ storeNames: ["userAttention"] }, async (tx) => {
+ const ua = await tx.userAttention.get([req.entityId, req.type]);
+ if (!ua) throw Error("attention request not found");
+ await tx.userAttention.delete([req.entityId, req.type]);
+ });
}
diff --git a/packages/taler-wallet-core/src/operations/backup/index.ts b/packages/taler-wallet-core/src/backup/index.ts
index a44e8f55a..16b5488e7 100644
--- a/packages/taler-wallet-core/src/operations/backup/index.ts
+++ b/packages/taler-wallet-core/src/backup/index.ts
@@ -26,91 +26,71 @@
*/
import {
AbsoluteTime,
- AmountString,
AttentionType,
BackupRecovery,
+ Codec,
+ Duration,
+ EddsaKeyPair,
+ HttpStatusCode,
+ Logger,
+ PreparePayResult,
+ ProviderInfo,
+ ProviderPaymentStatus,
+ RecoveryLoadRequest,
+ RecoveryMergeStrategy,
+ TalerError,
+ TalerErrorCode,
+ TalerPreciseTimestamp,
+ URL,
buildCodecForObject,
buildCodecForUnion,
bytesToString,
- canonicalizeBaseUrl,
canonicalJson,
- Codec,
- codecForAmountString,
+ canonicalizeBaseUrl,
+ checkDbInvariant,
+ checkLogicInvariant,
codecForBoolean,
codecForConstString,
codecForList,
- codecForNumber,
codecForString,
- codecForTalerErrorDetail,
+ codecForSyncTermsOfServiceResponse,
codecOptional,
- ConfirmPayResultType,
decodeCrock,
- DenomKeyType,
- durationFromSpec,
eddsaGetPublic,
- EddsaKeyPair,
encodeCrock,
getRandomBytes,
hash,
- hashDenomPub,
- HttpStatusCode,
j2s,
kdf,
- Logger,
notEmpty,
- PaymentStatus,
- PreparePayResult,
- PreparePayResultType,
- RecoveryLoadRequest,
- RecoveryMergeStrategy,
- ReserveTransactionType,
- rsaBlind,
secretbox,
secretbox_open,
stringToBytes,
- TalerErrorCode,
- TalerErrorDetail,
- TalerProtocolTimestamp,
- URL,
- WalletBackupContentV1,
} from "@gnu-taler/taler-util";
+import {
+ readSuccessResponseJsonOrThrow,
+ readTalerErrorResponse,
+} from "@gnu-taler/taler-util/http";
import { gunzipSync, gzipSync } from "fflate";
-import { TalerCryptoInterface } from "../../crypto/cryptoImplementation.js";
+import { addAttentionRequest, removeAttentionRequest } from "../attention.js";
+import {
+ TaskIdentifiers,
+ TaskRunResult,
+ TaskRunResultType,
+} from "../common.js";
import {
BackupProviderRecord,
BackupProviderState,
BackupProviderStateTag,
- BackupProviderTerms,
ConfigRecord,
ConfigRecordKey,
WalletBackupConfState,
-} from "../../db.js";
-import { TalerError } from "../../errors.js";
-import { InternalWalletState } from "../../internal-wallet-state.js";
-import { assertUnreachable } from "../../util/assertUnreachable.js";
-import {
- readSuccessResponseJsonOrThrow,
- readTalerErrorResponse,
-} from "../../util/http.js";
-import {
- checkDbInvariant,
- checkLogicInvariant,
-} from "../../util/invariants.js";
-import {
- OperationAttemptResult,
- OperationAttemptResultType,
- RetryTags,
- scheduleRetryInTx,
-} from "../../util/retries.js";
-import { addAttentionRequest, removeAttentionRequest } from "../attention.js";
-import {
- checkPaymentByProposalId,
- confirmPay,
- preparePayForUri,
-} from "../pay-merchant.js";
-import { exportBackup } from "./export.js";
-import { BackupCryptoPrecomputedData, importBackup } from "./import.js";
-import { getWalletBackupState, provideBackupState } from "./state.js";
+ WalletDbReadOnlyTransaction,
+ timestampOptionalPreciseFromDb,
+ timestampPreciseToDb,
+} from "../db.js";
+import { preparePayForUri } from "../pay-merchant.js";
+import { InternalWalletState, WalletExecutionContext } from "../wallet.js";
const logger = new Logger("operations/backup.ts");
@@ -140,7 +120,7 @@ const magic = "TLRWBK01";
*/
export async function encryptBackup(
config: WalletBackupConfState,
- blob: WalletBackupContentV1,
+ blob: any,
): Promise<Uint8Array> {
const chunks: Uint8Array[] = [];
chunks.push(stringToBytes(magic));
@@ -159,64 +139,6 @@ export async function encryptBackup(
return concatArrays(chunks);
}
-/**
- * Compute cryptographic values for a backup blob.
- *
- * FIXME: Take data that we already know from the DB.
- * FIXME: Move computations into crypto worker.
- */
-async function computeBackupCryptoData(
- cryptoApi: TalerCryptoInterface,
- backupContent: WalletBackupContentV1,
-): Promise<BackupCryptoPrecomputedData> {
- const cryptoData: BackupCryptoPrecomputedData = {
- coinPrivToCompletedCoin: {},
- rsaDenomPubToHash: {},
- proposalIdToContractTermsHash: {},
- proposalNoncePrivToPub: {},
- reservePrivToPub: {},
- };
- for (const backupExchangeDetails of backupContent.exchange_details) {
- for (const backupDenom of backupExchangeDetails.denominations) {
- if (backupDenom.denom_pub.cipher !== DenomKeyType.Rsa) {
- throw Error("unsupported cipher");
- }
- for (const backupCoin of backupDenom.coins) {
- const coinPub = encodeCrock(
- eddsaGetPublic(decodeCrock(backupCoin.coin_priv)),
- );
- const blindedCoin = rsaBlind(
- hash(decodeCrock(backupCoin.coin_priv)),
- decodeCrock(backupCoin.blinding_key),
- decodeCrock(backupDenom.denom_pub.rsa_public_key),
- );
- cryptoData.coinPrivToCompletedCoin[backupCoin.coin_priv] = {
- coinEvHash: encodeCrock(hash(blindedCoin)),
- coinPub,
- };
- }
- cryptoData.rsaDenomPubToHash[backupDenom.denom_pub.rsa_public_key] =
- encodeCrock(hashDenomPub(backupDenom.denom_pub));
- }
- }
- for (const backupWg of backupContent.withdrawal_groups) {
- cryptoData.reservePrivToPub[backupWg.reserve_priv] = encodeCrock(
- eddsaGetPublic(decodeCrock(backupWg.reserve_priv)),
- );
- }
- for (const purch of backupContent.purchases) {
- if (!purch.contract_terms_raw) continue;
- const { h: contractTermsHash } = await cryptoApi.hashString({
- str: canonicalJson(purch.contract_terms_raw),
- });
- const noncePub = encodeCrock(eddsaGetPublic(decodeCrock(purch.nonce_priv)));
- cryptoData.proposalNoncePrivToPub[purch.nonce_priv] = noncePub;
- cryptoData.proposalIdToContractTermsHash[purch.proposal_id] =
- contractTermsHash;
- }
- return cryptoData;
-}
-
function deriveAccountKeyPair(
bc: WalletBackupConfState,
providerUrl: string,
@@ -246,36 +168,36 @@ interface BackupForProviderArgs {
backupProviderBaseUrl: string;
}
-function getNextBackupTimestamp(): TalerProtocolTimestamp {
+function getNextBackupTimestamp(): TalerPreciseTimestamp {
// FIXME: Randomize!
- return AbsoluteTime.toTimestamp(
+ return AbsoluteTime.toPreciseTimestamp(
AbsoluteTime.addDuration(
AbsoluteTime.now(),
- durationFromSpec({ minutes: 5 }),
+ Duration.fromSpec({ minutes: 5 }),
),
);
}
async function runBackupCycleForProvider(
- ws: InternalWalletState,
+ wex: WalletExecutionContext,
args: BackupForProviderArgs,
-): Promise<OperationAttemptResult<unknown, { talerUri?: string }>> {
- const provider = await ws.db
- .mktx((x) => [x.backupProviders])
- .runReadOnly(async (tx) => {
+): Promise<TaskRunResult> {
+ const provider = await wex.db.runReadOnlyTx(
+ { storeNames: ["backupProviders"] },
+ async (tx) => {
return tx.backupProviders.get(args.backupProviderBaseUrl);
- });
+ },
+ );
if (!provider) {
logger.warn("provider disappeared");
- return {
- type: OperationAttemptResultType.Finished,
- result: undefined,
- };
+ return TaskRunResult.finished();
}
- const backupJson = await exportBackup(ws);
- const backupConfig = await provideBackupState(ws);
+ //const backupJson = await exportBackup(ws);
+ // FIXME: re-implement backup
+ const backupJson = {};
+ const backupConfig = await provideBackupState(wex);
const encBackup = await encryptBackup(backupConfig, backupJson);
const currentBackupHash = hash(encBackup);
@@ -287,7 +209,7 @@ async function runBackupCycleForProvider(
logger.trace(`trying to upload backup to ${provider.baseUrl}`);
logger.trace(`old hash ${oldHash}, new hash ${newHash}`);
- const syncSigResp = await ws.cryptoApi.makeSyncSignature({
+ const syncSigResp = await wex.cryptoApi.makeSyncSignature({
newHash: encodeCrock(currentBackupHash),
oldHash: provider.lastBackupHash,
accountPriv: encodeCrock(accountKeyPair.eddsaPriv),
@@ -304,16 +226,16 @@ async function runBackupCycleForProvider(
accountBackupUrl.searchParams.set("fresh", "yes");
}
- const resp = await ws.http.fetch(accountBackupUrl.href, {
+ const resp = await wex.http.fetch(accountBackupUrl.href, {
method: "POST",
body: encBackup,
headers: {
"content-type": "application/octet-stream",
"sync-signature": syncSigResp.sig,
- "if-none-match": newHash,
+ "if-none-match": JSON.stringify(newHash),
...(provider.lastBackupHash
? {
- "if-match": provider.lastBackupHash,
+ "if-match": JSON.stringify(provider.lastBackupHash),
}
: {}),
},
@@ -322,30 +244,30 @@ async function runBackupCycleForProvider(
logger.trace(`sync response status: ${resp.status}`);
if (resp.status === HttpStatusCode.NotModified) {
- await ws.db
- .mktx((x) => [x.backupProviders])
- .runReadWrite(async (tx) => {
+ await wex.db.runReadWriteTx(
+ { storeNames: ["backupProviders"] },
+ async (tx) => {
const prov = await tx.backupProviders.get(provider.baseUrl);
if (!prov) {
return;
}
- prov.lastBackupCycleTimestamp = TalerProtocolTimestamp.now();
+ prov.lastBackupCycleTimestamp = timestampPreciseToDb(
+ TalerPreciseTimestamp.now(),
+ );
prov.state = {
tag: BackupProviderStateTag.Ready,
- nextBackupTimestamp: getNextBackupTimestamp(),
+ nextBackupTimestamp: timestampPreciseToDb(getNextBackupTimestamp()),
};
await tx.backupProviders.put(prov);
- });
+ },
+ );
- removeAttentionRequest(ws, {
+ removeAttentionRequest(wex, {
entityId: provider.baseUrl,
type: AttentionType.BackupUnpaid,
});
- return {
- type: OperationAttemptResultType.Finished,
- result: undefined,
- };
+ return TaskRunResult.finished();
}
if (resp.status === HttpStatusCode.PaymentRequired) {
@@ -360,7 +282,7 @@ async function runBackupCycleForProvider(
//FIXME: check download errors
let res: PreparePayResult | undefined = undefined;
try {
- res = await preparePayForUri(ws, talerUri);
+ res = await preparePayForUri(wex, talerUri);
} catch (e) {
const error = TalerError.fromException(e);
if (!error.hasErrorCode(TalerErrorCode.WALLET_ORDER_ALREADY_CLAIMED)) {
@@ -371,52 +293,50 @@ async function runBackupCycleForProvider(
if (res === undefined) {
//claimed
- await ws.db
- .mktx((x) => [x.backupProviders, x.operationRetries])
- .runReadWrite(async (tx) => {
+ await wex.db.runReadWriteTx(
+ { storeNames: ["backupProviders"] },
+ async (tx) => {
const prov = await tx.backupProviders.get(provider.baseUrl);
if (!prov) {
logger.warn("backup provider not found anymore");
return;
}
- const opId = RetryTags.forBackup(prov);
- await scheduleRetryInTx(ws, tx, opId);
prov.shouldRetryFreshProposal = true;
prov.state = {
tag: BackupProviderStateTag.Retrying,
};
await tx.backupProviders.put(prov);
- });
-
- return {
- type: OperationAttemptResultType.Pending,
- result: {
- talerUri,
},
- };
+ );
+
+ throw Error("not implemented");
+ // return {
+ // type: TaskRunResultType.Pending,
+ // };
}
const result = res;
- await ws.db
- .mktx((x) => [x.backupProviders, x.operationRetries])
- .runReadWrite(async (tx) => {
+ await wex.db.runReadWriteTx(
+ { storeNames: ["backupProviders"] },
+ async (tx) => {
const prov = await tx.backupProviders.get(provider.baseUrl);
if (!prov) {
logger.warn("backup provider not found anymore");
return;
}
- const opId = RetryTags.forBackup(prov);
- await scheduleRetryInTx(ws, tx, opId);
+ // const opId = TaskIdentifiers.forBackup(prov);
+ // await scheduleRetryInTx(ws, tx, opId);
prov.currentPaymentProposalId = result.proposalId;
prov.shouldRetryFreshProposal = false;
prov.state = {
tag: BackupProviderStateTag.Retrying,
};
await tx.backupProviders.put(prov);
- });
+ },
+ );
addAttentionRequest(
- ws,
+ wex,
{
type: AttentionType.BackupUnpaid,
provider_base_url: provider.baseUrl,
@@ -425,52 +345,52 @@ async function runBackupCycleForProvider(
provider.baseUrl,
);
- return {
- type: OperationAttemptResultType.Pending,
- result: {
- talerUri,
- },
- };
+ throw Error("not implemented");
+ // return {
+ // type: TaskRunResultType.Pending,
+ // };
}
if (resp.status === HttpStatusCode.NoContent) {
- await ws.db
- .mktx((x) => [x.backupProviders])
- .runReadWrite(async (tx) => {
+ await wex.db.runReadWriteTx(
+ { storeNames: ["backupProviders"] },
+ async (tx) => {
const prov = await tx.backupProviders.get(provider.baseUrl);
if (!prov) {
return;
}
prov.lastBackupHash = encodeCrock(currentBackupHash);
- prov.lastBackupCycleTimestamp = TalerProtocolTimestamp.now();
+ prov.lastBackupCycleTimestamp = timestampPreciseToDb(
+ TalerPreciseTimestamp.now(),
+ );
prov.state = {
tag: BackupProviderStateTag.Ready,
- nextBackupTimestamp: getNextBackupTimestamp(),
+ nextBackupTimestamp: timestampPreciseToDb(getNextBackupTimestamp()),
};
await tx.backupProviders.put(prov);
- });
+ },
+ );
- removeAttentionRequest(ws, {
+ removeAttentionRequest(wex, {
entityId: provider.baseUrl,
type: AttentionType.BackupUnpaid,
});
return {
- type: OperationAttemptResultType.Finished,
- result: undefined,
+ type: TaskRunResultType.Finished,
};
}
if (resp.status === HttpStatusCode.Conflict) {
logger.info("conflicting backup found");
const backupEnc = new Uint8Array(await resp.bytes());
- const backupConfig = await provideBackupState(ws);
- const blob = await decryptBackup(backupConfig, backupEnc);
- const cryptoData = await computeBackupCryptoData(ws.cryptoApi, blob);
- await importBackup(ws, blob, cryptoData);
- await ws.db
- .mktx((x) => [x.backupProviders, x.operationRetries])
- .runReadWrite(async (tx) => {
+ const backupConfig = await provideBackupState(wex);
+ // const blob = await decryptBackup(backupConfig, backupEnc);
+ // FIXME: Re-implement backup import with merging
+ // await importBackup(ws, blob, cryptoData);
+ await wex.db.runReadWriteTx(
+ { storeNames: ["backupProviders"] },
+ async (tx) => {
const prov = await tx.backupProviders.get(provider.baseUrl);
if (!prov) {
logger.warn("backup provider not found anymore");
@@ -479,16 +399,17 @@ async function runBackupCycleForProvider(
prov.lastBackupHash = encodeCrock(hash(backupEnc));
// FIXME: Allocate error code for this situation?
// FIXME: Add operation retry record!
- const opId = RetryTags.forBackup(prov);
- await scheduleRetryInTx(ws, tx, opId);
+ const opId = TaskIdentifiers.forBackup(prov);
+ //await scheduleRetryInTx(ws, tx, opId);
prov.state = {
tag: BackupProviderStateTag.Retrying,
};
await tx.backupProviders.put(prov);
- });
+ },
+ );
logger.info("processed existing backup");
// Now upload our own, merged backup.
- return await runBackupCycleForProvider(ws, args);
+ return await runBackupCycleForProvider(wex, args);
}
// Some other response that we did not expect!
@@ -498,27 +419,28 @@ async function runBackupCycleForProvider(
const err = await readTalerErrorResponse(resp);
logger.error(`got error response from backup provider: ${j2s(err)}`);
return {
- type: OperationAttemptResultType.Error,
+ type: TaskRunResultType.Error,
errorDetail: err,
};
}
export async function processBackupForProvider(
- ws: InternalWalletState,
+ wex: WalletExecutionContext,
backupProviderBaseUrl: string,
-): Promise<OperationAttemptResult> {
- const provider = await ws.db
- .mktx((x) => [x.backupProviders])
- .runReadOnly(async (tx) => {
+): Promise<TaskRunResult> {
+ const provider = await wex.db.runReadOnlyTx(
+ { storeNames: ["backupProviders"] },
+ async (tx) => {
return await tx.backupProviders.get(backupProviderBaseUrl);
- });
+ },
+ );
if (!provider) {
throw Error("unknown backup provider");
}
logger.info(`running backup for provider ${backupProviderBaseUrl}`);
- return await runBackupCycleForProvider(ws, {
+ return await runBackupCycleForProvider(wex, {
backupProviderBaseUrl: provider.baseUrl,
});
}
@@ -534,14 +456,15 @@ export const codecForRemoveBackupProvider =
.build("RemoveBackupProviderRequest");
export async function removeBackupProvider(
- ws: InternalWalletState,
+ wex: WalletExecutionContext,
req: RemoveBackupProviderRequest,
): Promise<void> {
- await ws.db
- .mktx((x) => [x.backupProviders])
- .runReadWrite(async (tx) => {
+ await wex.db.runReadWriteTx(
+ { storeNames: ["backupProviders"] },
+ async (tx) => {
await tx.backupProviders.delete(req.provider);
- });
+ },
+ );
}
export interface RunBackupCycleRequest {
@@ -564,12 +487,12 @@ export const codecForRunBackupCycle = (): Codec<RunBackupCycleRequest> =>
* 3. Upload the updated backup blob.
*/
export async function runBackupCycle(
- ws: InternalWalletState,
+ wex: WalletExecutionContext,
req: RunBackupCycleRequest,
): Promise<void> {
- const providers = await ws.db
- .mktx((x) => [x.backupProviders])
- .runReadOnly(async (tx) => {
+ const providers = await wex.db.runReadOnlyTx(
+ { storeNames: ["backupProviders"] },
+ async (tx) => {
if (req.providers) {
const rs = await Promise.all(
req.providers.map((id) => tx.backupProviders.get(id)),
@@ -577,35 +500,16 @@ export async function runBackupCycle(
return rs.filter(notEmpty);
}
return await tx.backupProviders.iter().toArray();
- });
+ },
+ );
for (const provider of providers) {
- await runBackupCycleForProvider(ws, {
+ await runBackupCycleForProvider(wex, {
backupProviderBaseUrl: provider.baseUrl,
});
}
}
-export interface SyncTermsOfServiceResponse {
- // maximum backup size supported
- storage_limit_in_megabytes: number;
-
- // Fee for an account, per year.
- annual_fee: AmountString;
-
- // protocol version supported by the server,
- // for now always "0.0".
- version: string;
-}
-
-export const codecForSyncTermsOfServiceResponse =
- (): Codec<SyncTermsOfServiceResponse> =>
- buildCodecForObject<SyncTermsOfServiceResponse>()
- .property("storage_limit_in_megabytes", codecForNumber())
- .property("annual_fee", codecForAmountString())
- .property("version", codecForString())
- .build("SyncTermsOfServiceResponse");
-
export interface AddBackupProviderRequest {
backupProviderBaseUrl: string;
@@ -627,8 +531,7 @@ export const codecForAddBackupProviderRequest =
export type AddBackupProviderResponse =
| AddBackupProviderOk
- | AddBackupProviderPaymentRequired
- | AddBackupProviderError;
+ | AddBackupProviderPaymentRequired;
interface AddBackupProviderOk {
status: "ok";
@@ -637,10 +540,6 @@ interface AddBackupProviderPaymentRequired {
status: "payment-required";
talerUri?: string;
}
-interface AddBackupProviderError {
- status: "error";
- error: TalerErrorDetail;
-}
export const codecForAddBackupProviderOk = (): Codec<AddBackupProviderOk> =>
buildCodecForObject<AddBackupProviderOk>()
@@ -654,13 +553,6 @@ export const codecForAddBackupProviderPaymenrRequired =
.property("talerUri", codecOptional(codecForString()))
.build("AddBackupProviderPaymentRequired");
-export const codecForAddBackupProviderError =
- (): Codec<AddBackupProviderError> =>
- buildCodecForObject<AddBackupProviderError>()
- .property("status", codecForConstString("error"))
- .property("error", codecForTalerErrorDetail())
- .build("AddBackupProviderError");
-
export const codecForAddBackupProviderResponse =
(): Codec<AddBackupProviderResponse> =>
buildCodecForUnion<AddBackupProviderResponse>()
@@ -670,48 +562,52 @@ export const codecForAddBackupProviderResponse =
"payment-required",
codecForAddBackupProviderPaymenrRequired(),
)
- .alternative("error", codecForAddBackupProviderError())
.build("AddBackupProviderResponse");
export async function addBackupProvider(
- ws: InternalWalletState,
+ wex: WalletExecutionContext,
req: AddBackupProviderRequest,
): Promise<AddBackupProviderResponse> {
logger.info(`adding backup provider ${j2s(req)}`);
- await provideBackupState(ws);
+ await provideBackupState(wex);
const canonUrl = canonicalizeBaseUrl(req.backupProviderBaseUrl);
- await ws.db
- .mktx((x) => [x.backupProviders])
- .runReadWrite(async (tx) => {
+ await wex.db.runReadWriteTx(
+ { storeNames: ["backupProviders"] },
+ async (tx) => {
const oldProv = await tx.backupProviders.get(canonUrl);
if (oldProv) {
logger.info("old backup provider found");
if (req.activate) {
oldProv.state = {
tag: BackupProviderStateTag.Ready,
- nextBackupTimestamp: TalerProtocolTimestamp.now(),
+ nextBackupTimestamp: timestampPreciseToDb(
+ TalerPreciseTimestamp.now(),
+ ),
};
logger.info("setting existing backup provider to active");
await tx.backupProviders.put(oldProv);
}
return;
}
- });
+ },
+ );
const termsUrl = new URL("config", canonUrl);
- const resp = await ws.http.get(termsUrl.href);
+ const resp = await wex.http.fetch(termsUrl.href);
const terms = await readSuccessResponseJsonOrThrow(
resp,
codecForSyncTermsOfServiceResponse(),
);
- await ws.db
- .mktx((x) => [x.backupProviders])
- .runReadWrite(async (tx) => {
+ await wex.db.runReadWriteTx(
+ { storeNames: ["backupProviders"] },
+ async (tx) => {
let state: BackupProviderState;
//FIXME: what is the difference provisional and ready?
if (req.activate) {
state = {
tag: BackupProviderStateTag.Ready,
- nextBackupTimestamp: TalerProtocolTimestamp.now(),
+ nextBackupTimestamp: timestampPreciseToDb(
+ TalerPreciseTimestamp.now(),
+ ),
};
} else {
state = {
@@ -731,228 +627,137 @@ export async function addBackupProvider(
baseUrl: canonUrl,
uids: [encodeCrock(getRandomBytes(32))],
});
- });
+ },
+ );
- return await runFirstBackupCycleForProvider(ws, {
+ return await runFirstBackupCycleForProvider(wex, {
backupProviderBaseUrl: canonUrl,
});
}
async function runFirstBackupCycleForProvider(
- ws: InternalWalletState,
+ wex: WalletExecutionContext,
args: BackupForProviderArgs,
): Promise<AddBackupProviderResponse> {
- const resp = await runBackupCycleForProvider(ws, args);
- switch (resp.type) {
- case OperationAttemptResultType.Error:
- return {
- status: "error",
- error: resp.errorDetail,
- };
- case OperationAttemptResultType.Finished:
- return {
- status: "ok",
- };
- case OperationAttemptResultType.Longpoll:
- throw Error(
- "unexpected runFirstBackupCycleForProvider result (longpoll)",
- );
- case OperationAttemptResultType.Pending:
- return {
- status: "payment-required",
- talerUri: resp.result.talerUri,
- };
- default:
- assertUnreachable(resp);
- }
+ throw Error("not implemented");
+ // const resp = await runBackupCycleForProvider(ws, args);
+ // switch (resp.type) {
+ // case TaskRunResultType.Error:
+ // throw TalerError.fromDetail(
+ // TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
+ // resp.errorDetail as any, //FIXME create an error for backup problems
+ // );
+ // case TaskRunResultType.Finished:
+ // return {
+ // status: "ok",
+ // };
+ // case TaskRunResultType.Pending:
+ // return {
+ // status: "payment-required",
+ // talerUri: "FIXME",
+ // //talerUri: resp.result.talerUri,
+ // };
+ // default:
+ // assertUnreachable(resp);
+ // }
}
export async function restoreFromRecoverySecret(): Promise<void> {
return;
}
-/**
- * Information about one provider.
- *
- * We don't store the account key here,
- * as that's derived from the wallet root key.
- */
-export interface ProviderInfo {
- active: boolean;
- syncProviderBaseUrl: string;
- name: string;
- terms?: BackupProviderTerms;
- /**
- * Last communication issue with the provider.
- */
- lastError?: TalerErrorDetail;
- lastSuccessfulBackupTimestamp?: TalerProtocolTimestamp;
- lastAttemptedBackupTimestamp?: TalerProtocolTimestamp;
- paymentProposalIds: string[];
- backupProblem?: BackupProblem;
- paymentStatus: ProviderPaymentStatus;
-}
-
-export type BackupProblem =
- | BackupUnreadableProblem
- | BackupConflictingDeviceProblem;
-
-export interface BackupUnreadableProblem {
- type: "backup-unreadable";
-}
-
-export interface BackupUnreadableProblem {
- type: "backup-unreadable";
-}
-
-export interface BackupConflictingDeviceProblem {
- type: "backup-conflicting-device";
- otherDeviceId: string;
- myDeviceId: string;
- backupTimestamp: AbsoluteTime;
-}
-
-export type ProviderPaymentStatus =
- | ProviderPaymentTermsChanged
- | ProviderPaymentPaid
- | ProviderPaymentInsufficientBalance
- | ProviderPaymentUnpaid
- | ProviderPaymentPending;
-
export interface BackupInfo {
walletRootPub: string;
deviceId: string;
providers: ProviderInfo[];
}
-export async function importBackupPlain(
- ws: InternalWalletState,
- blob: any,
-): Promise<void> {
- // FIXME: parse
- const backup: WalletBackupContentV1 = blob;
-
- const cryptoData = await computeBackupCryptoData(ws.cryptoApi, backup);
-
- await importBackup(ws, blob, cryptoData);
-}
-
-export enum ProviderPaymentType {
- Unpaid = "unpaid",
- Pending = "pending",
- InsufficientBalance = "insufficient-balance",
- Paid = "paid",
- TermsChanged = "terms-changed",
-}
-
-export interface ProviderPaymentUnpaid {
- type: ProviderPaymentType.Unpaid;
-}
-
-export interface ProviderPaymentInsufficientBalance {
- type: ProviderPaymentType.InsufficientBalance;
- amount: AmountString;
-}
-
-export interface ProviderPaymentPending {
- type: ProviderPaymentType.Pending;
- talerUri?: string;
-}
-
-export interface ProviderPaymentPaid {
- type: ProviderPaymentType.Paid;
- paidUntil: AbsoluteTime;
-}
-
-export interface ProviderPaymentTermsChanged {
- type: ProviderPaymentType.TermsChanged;
- paidUntil: AbsoluteTime;
- oldTerms: BackupProviderTerms;
- newTerms: BackupProviderTerms;
-}
-
async function getProviderPaymentInfo(
- ws: InternalWalletState,
+ wex: WalletExecutionContext,
provider: BackupProviderRecord,
): Promise<ProviderPaymentStatus> {
- if (!provider.currentPaymentProposalId) {
- return {
- type: ProviderPaymentType.Unpaid,
- };
- }
- const status = await checkPaymentByProposalId(
- ws,
- provider.currentPaymentProposalId,
- ).catch(() => undefined);
-
- if (!status) {
- return {
- type: ProviderPaymentType.Unpaid,
- };
- }
-
- switch (status.status) {
- case PreparePayResultType.InsufficientBalance:
- return {
- type: ProviderPaymentType.InsufficientBalance,
- amount: status.amountRaw,
- };
- case PreparePayResultType.PaymentPossible:
- return {
- type: ProviderPaymentType.Pending,
- talerUri: status.talerUri,
- };
- case PreparePayResultType.AlreadyConfirmed:
- if (status.paid) {
- return {
- type: ProviderPaymentType.Paid,
- paidUntil: AbsoluteTime.addDuration(
- AbsoluteTime.fromTimestamp(status.contractTerms.timestamp),
- durationFromSpec({ years: 1 }), //FIXME: take this from the contract term
- ),
- };
- } else {
- return {
- type: ProviderPaymentType.Pending,
- talerUri: status.talerUri,
- };
- }
- default:
- assertUnreachable(status);
- }
+ throw Error("not implemented");
+ // if (!provider.currentPaymentProposalId) {
+ // return {
+ // type: ProviderPaymentType.Unpaid,
+ // };
+ // }
+ // const status = await checkPaymentByProposalId(
+ // ws,
+ // provider.currentPaymentProposalId,
+ // ).catch(() => undefined);
+
+ // if (!status) {
+ // return {
+ // type: ProviderPaymentType.Unpaid,
+ // };
+ // }
+
+ // switch (status.status) {
+ // case PreparePayResultType.InsufficientBalance:
+ // return {
+ // type: ProviderPaymentType.InsufficientBalance,
+ // amount: status.amountRaw,
+ // };
+ // case PreparePayResultType.PaymentPossible:
+ // return {
+ // type: ProviderPaymentType.Pending,
+ // talerUri: status.talerUri,
+ // };
+ // case PreparePayResultType.AlreadyConfirmed:
+ // if (status.paid) {
+ // return {
+ // type: ProviderPaymentType.Paid,
+ // paidUntil: AbsoluteTime.addDuration(
+ // AbsoluteTime.fromProtocolTimestamp(status.contractTerms.timestamp),
+ // durationFromSpec({ years: 1 }), //FIXME: take this from the contract term
+ // ),
+ // };
+ // } else {
+ // return {
+ // type: ProviderPaymentType.Pending,
+ // talerUri: status.talerUri,
+ // };
+ // }
+ // default:
+ // assertUnreachable(status);
+ // }
}
/**
* Get information about the current state of wallet backups.
*/
export async function getBackupInfo(
- ws: InternalWalletState,
+ wex: WalletExecutionContext,
): Promise<BackupInfo> {
- const backupConfig = await provideBackupState(ws);
- const providerRecords = await ws.db
- .mktx((x) => [x.backupProviders, x.operationRetries])
- .runReadOnly(async (tx) => {
+ const backupConfig = await provideBackupState(wex);
+ const providerRecords = await wex.db.runReadOnlyTx(
+ { storeNames: ["backupProviders", "operationRetries"] },
+ async (tx) => {
return await tx.backupProviders.iter().mapAsync(async (bp) => {
- const opId = RetryTags.forBackup(bp);
+ const opId = TaskIdentifiers.forBackup(bp);
const retryRecord = await tx.operationRetries.get(opId);
return {
provider: bp,
retryRecord,
};
});
- });
+ },
+ );
const providers: ProviderInfo[] = [];
for (const x of providerRecords) {
providers.push({
active: x.provider.state.tag !== BackupProviderStateTag.Provisional,
syncProviderBaseUrl: x.provider.baseUrl,
- lastSuccessfulBackupTimestamp: x.provider.lastBackupCycleTimestamp,
+ lastSuccessfulBackupTimestamp: timestampOptionalPreciseFromDb(
+ x.provider.lastBackupCycleTimestamp,
+ ),
paymentProposalIds: x.provider.paymentProposalIds,
lastError:
x.provider.state.tag === BackupProviderStateTag.Retrying
? x.retryRecord?.lastError
: undefined,
- paymentStatus: await getProviderPaymentInfo(ws, x.provider),
+ paymentStatus: await getProviderPaymentInfo(wex, x.provider),
terms: x.provider.terms,
name: x.provider.name,
});
@@ -969,14 +774,15 @@ export async function getBackupInfo(
* private key.
*/
export async function getBackupRecovery(
- ws: InternalWalletState,
+ wex: WalletExecutionContext,
): Promise<BackupRecovery> {
- const bs = await provideBackupState(ws);
- const providers = await ws.db
- .mktx((x) => [x.backupProviders])
- .runReadOnly(async (tx) => {
+ const bs = await provideBackupState(wex);
+ const providers = await wex.db.runReadOnlyTx(
+ { storeNames: ["backupProviders"] },
+ async (tx) => {
return await tx.backupProviders.iter().toArray();
- });
+ },
+ );
return {
providers: providers
.filter((x) => x.state.tag !== BackupProviderStateTag.Provisional)
@@ -991,12 +797,12 @@ export async function getBackupRecovery(
}
async function backupRecoveryTheirs(
- ws: InternalWalletState,
+ wex: WalletExecutionContext,
br: BackupRecovery,
) {
- await ws.db
- .mktx((x) => [x.config, x.backupProviders])
- .runReadWrite(async (tx) => {
+ await wex.db.runReadWriteTx(
+ { storeNames: ["backupProviders", "config"] },
+ async (tx) => {
let backupStateEntry: ConfigRecord | undefined = await tx.config.get(
ConfigRecordKey.WalletBackupState,
);
@@ -1023,7 +829,9 @@ async function backupRecoveryTheirs(
shouldRetryFreshProposal: false,
state: {
tag: BackupProviderStateTag.Ready,
- nextBackupTimestamp: TalerProtocolTimestamp.now(),
+ nextBackupTimestamp: timestampPreciseToDb(
+ TalerPreciseTimestamp.now(),
+ ),
},
uids: [encodeCrock(getRandomBytes(32))],
});
@@ -1035,23 +843,28 @@ async function backupRecoveryTheirs(
prov.lastBackupHash = undefined;
await tx.backupProviders.put(prov);
}
- });
+ },
+ );
}
-async function backupRecoveryOurs(ws: InternalWalletState, br: BackupRecovery) {
+async function backupRecoveryOurs(
+ wex: WalletExecutionContext,
+ br: BackupRecovery,
+) {
throw Error("not implemented");
}
export async function loadBackupRecovery(
- ws: InternalWalletState,
+ wex: WalletExecutionContext,
br: RecoveryLoadRequest,
): Promise<void> {
- const bs = await provideBackupState(ws);
- const providers = await ws.db
- .mktx((x) => [x.backupProviders])
- .runReadOnly(async (tx) => {
+ const bs = await provideBackupState(wex);
+ const providers = await wex.db.runReadOnlyTx(
+ { storeNames: ["backupProviders"] },
+ async (tx) => {
return await tx.backupProviders.iter().toArray();
- });
+ },
+ );
let strategy = br.strategy;
if (
br.recovery.walletRootPriv != bs.walletRootPriv &&
@@ -1066,29 +879,16 @@ export async function loadBackupRecovery(
strategy = RecoveryMergeStrategy.Theirs;
}
if (strategy === RecoveryMergeStrategy.Theirs) {
- return backupRecoveryTheirs(ws, br.recovery);
+ return backupRecoveryTheirs(wex, br.recovery);
} else {
- return backupRecoveryOurs(ws, br.recovery);
+ return backupRecoveryOurs(wex, br.recovery);
}
}
-export async function exportBackupEncrypted(
- ws: InternalWalletState,
-): Promise<Uint8Array> {
- await provideBackupState(ws);
- const blob = await exportBackup(ws);
- const bs = await ws.db
- .mktx((x) => [x.config])
- .runReadOnly(async (tx) => {
- return await getWalletBackupState(ws, tx);
- });
- return encryptBackup(bs, blob);
-}
-
export async function decryptBackup(
backupConfig: WalletBackupConfState,
data: Uint8Array,
-): Promise<WalletBackupContentV1> {
+): Promise<any> {
const rMagic = bytesToString(data.slice(0, 8));
if (rMagic != magic) {
throw Error("invalid backup file (magic tag mismatch)");
@@ -1104,12 +904,82 @@ export async function decryptBackup(
return JSON.parse(bytesToString(gunzipSync(dataCompressed)));
}
-export async function importBackupEncrypted(
+export async function provideBackupState(
+ wex: WalletExecutionContext,
+): Promise<WalletBackupConfState> {
+ const bs: ConfigRecord | undefined = await wex.db.runReadOnlyTx(
+ { storeNames: ["config"] },
+ async (tx) => {
+ return await tx.config.get(ConfigRecordKey.WalletBackupState);
+ },
+ );
+ if (bs) {
+ checkDbInvariant(bs.key === ConfigRecordKey.WalletBackupState);
+ return bs.value;
+ }
+ // We need to generate the key outside of the transaction
+ // due to how IndexedDB works.
+ const k = await wex.cryptoApi.createEddsaKeypair({});
+ const d = getRandomBytes(5);
+ // FIXME: device ID should be configured when wallet is initialized
+ // and be based on hostname
+ const deviceId = `wallet-core-${encodeCrock(d)}`;
+ return await wex.db.runReadWriteTx({ storeNames: ["config"] }, async (tx) => {
+ let backupStateEntry: ConfigRecord | undefined = await tx.config.get(
+ ConfigRecordKey.WalletBackupState,
+ );
+ if (!backupStateEntry) {
+ backupStateEntry = {
+ key: ConfigRecordKey.WalletBackupState,
+ value: {
+ deviceId,
+ walletRootPub: k.pub,
+ walletRootPriv: k.priv,
+ lastBackupPlainHash: undefined,
+ },
+ };
+ await tx.config.put(backupStateEntry);
+ }
+ checkDbInvariant(
+ backupStateEntry.key === ConfigRecordKey.WalletBackupState,
+ );
+ return backupStateEntry.value;
+ });
+}
+
+export async function getWalletBackupState(
ws: InternalWalletState,
- data: Uint8Array,
+ tx: WalletDbReadOnlyTransaction<["config"]>,
+): Promise<WalletBackupConfState> {
+ const bs = await tx.config.get(ConfigRecordKey.WalletBackupState);
+ checkDbInvariant(!!bs, "wallet backup state should be in DB");
+ checkDbInvariant(bs.key === ConfigRecordKey.WalletBackupState);
+ return bs.value;
+}
+
+export async function setWalletDeviceId(
+ wex: WalletExecutionContext,
+ deviceId: string,
): Promise<void> {
- const backupConfig = await provideBackupState(ws);
- const blob = await decryptBackup(backupConfig, data);
- const cryptoData = await computeBackupCryptoData(ws.cryptoApi, blob);
- await importBackup(ws, blob, cryptoData);
+ await provideBackupState(wex);
+ await wex.db.runReadWriteTx({ storeNames: ["config"] }, async (tx) => {
+ let backupStateEntry: ConfigRecord | undefined = await tx.config.get(
+ ConfigRecordKey.WalletBackupState,
+ );
+ if (
+ !backupStateEntry ||
+ backupStateEntry.key !== ConfigRecordKey.WalletBackupState
+ ) {
+ return;
+ }
+ backupStateEntry.value.deviceId = deviceId;
+ await tx.config.put(backupStateEntry);
+ });
+}
+
+export async function getWalletDeviceId(
+ wex: WalletExecutionContext,
+): Promise<string> {
+ const bs = await provideBackupState(wex);
+ return bs.deviceId;
}
diff --git a/packages/taler-wallet-core/src/backup/state.ts b/packages/taler-wallet-core/src/backup/state.ts
new file mode 100644
index 000000000..72f850b25
--- /dev/null
+++ b/packages/taler-wallet-core/src/backup/state.ts
@@ -0,0 +1,15 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems SA
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
diff --git a/packages/taler-wallet-core/src/balance.ts b/packages/taler-wallet-core/src/balance.ts
new file mode 100644
index 000000000..1fef9876e
--- /dev/null
+++ b/packages/taler-wallet-core/src/balance.ts
@@ -0,0 +1,769 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 GNUnet e.V.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Functions to compute the wallet's balance.
+ *
+ * There are multiple definition of the wallet's balance.
+ * We use the following terminology:
+ *
+ * - "available": Balance that is available
+ * for spending from transactions in their final state and
+ * expected to be available from pending refreshes.
+ *
+ * - "pending-incoming": Expected (positive!) delta
+ * to the available balance that we expect to have
+ * after pending operations reach the "done" state.
+ *
+ * - "pending-outgoing": Amount that is currently allocated
+ * to be spent, but the spend operation could still be aborted
+ * and part of the pending-outgoing amount could be recovered.
+ *
+ * - "material": Balance that the wallet believes it could spend *right now*,
+ * without waiting for any operations to complete.
+ * This balance type is important when showing "insufficient balance" error messages.
+ *
+ * - "age-acceptable": Subset of the material balance that can be spent
+ * with age restrictions applied.
+ *
+ * - "merchant-acceptable": Subset of the material balance that can be spent with a particular
+ * merchant (restricted via min age, exchange, auditor, wire_method).
+ *
+ * - "merchant-depositable": Subset of the merchant-acceptable balance that the merchant
+ * can accept via their supported wire methods.
+ */
+
+/**
+ * Imports.
+ */
+import { GlobalIDB } from "@gnu-taler/idb-bridge";
+import {
+ AmountJson,
+ AmountLike,
+ Amounts,
+ assertUnreachable,
+ BalanceFlag,
+ BalancesResponse,
+ GetBalanceDetailRequest,
+ j2s,
+ Logger,
+ parsePaytoUri,
+ ScopeInfo,
+ ScopeType,
+} from "@gnu-taler/taler-util";
+import { ExchangeRestrictionSpec, findMatchingWire } from "./coinSelection.js";
+import {
+ DepositOperationStatus,
+ ExchangeEntryDbRecordStatus,
+ OPERATION_STATUS_ACTIVE_FIRST,
+ OPERATION_STATUS_ACTIVE_LAST,
+ PeerPushDebitStatus,
+ RefreshGroupRecord,
+ RefreshOperationStatus,
+ WalletDbReadOnlyTransaction,
+ WithdrawalGroupStatus,
+} from "./db.js";
+import {
+ getExchangeScopeInfo,
+ getExchangeWireDetailsInTx,
+} from "./exchanges.js";
+import { getDenomInfo, WalletExecutionContext } from "./wallet.js";
+
+/**
+ * Logger.
+ */
+const logger = new Logger("operations/balance.ts");
+
+interface WalletBalance {
+ scopeInfo: ScopeInfo;
+ available: AmountJson;
+ pendingIncoming: AmountJson;
+ pendingOutgoing: AmountJson;
+ flagIncomingKyc: boolean;
+ flagIncomingAml: boolean;
+ flagIncomingConfirmation: boolean;
+ flagOutgoingKyc: boolean;
+}
+
+/**
+ * Compute the available amount that the wallet expects to get
+ * out of a refresh group.
+ */
+function computeRefreshGroupAvailableAmount(r: RefreshGroupRecord): AmountJson {
+ // Don't count finished refreshes, since the refresh already resulted
+ // in coins being added to the wallet.
+ let available = Amounts.zeroOfCurrency(r.currency);
+ if (r.timestampFinished) {
+ return available;
+ }
+ for (let i = 0; i < r.oldCoinPubs.length; i++) {
+ available = Amounts.add(available, r.expectedOutputPerCoin[i]).amount;
+ }
+ return available;
+}
+
+function getBalanceKey(scopeInfo: ScopeInfo): string {
+ switch (scopeInfo.type) {
+ case ScopeType.Auditor:
+ return `${scopeInfo.type};${scopeInfo.currency};${scopeInfo.url}`;
+ case ScopeType.Exchange:
+ return `${scopeInfo.type};${scopeInfo.currency};${scopeInfo.url}`;
+ case ScopeType.Global:
+ return `${scopeInfo.type};${scopeInfo.currency}`;
+ }
+}
+
+class BalancesStore {
+ private exchangeScopeCache: Record<string, ScopeInfo> = {};
+ private balanceStore: Record<string, WalletBalance> = {};
+
+ constructor(
+ private wex: WalletExecutionContext,
+ private tx: WalletDbReadOnlyTransaction<
+ [
+ "globalCurrencyAuditors",
+ "globalCurrencyExchanges",
+ "exchanges",
+ "exchangeDetails",
+ ]
+ >,
+ ) {}
+
+ /**
+ * Add amount to a balance field, both for
+ * the slicing by exchange and currency.
+ */
+ private async initBalance(
+ currency: string,
+ exchangeBaseUrl: string,
+ ): Promise<WalletBalance> {
+ let scopeInfo: ScopeInfo | undefined =
+ this.exchangeScopeCache[exchangeBaseUrl];
+ if (!scopeInfo) {
+ scopeInfo = await getExchangeScopeInfo(
+ this.tx,
+ exchangeBaseUrl,
+ currency,
+ );
+ this.exchangeScopeCache[exchangeBaseUrl] = scopeInfo;
+ }
+ const balanceKey = getBalanceKey(scopeInfo);
+ const b = this.balanceStore[balanceKey];
+ if (!b) {
+ const zero = Amounts.zeroOfCurrency(currency);
+ this.balanceStore[balanceKey] = {
+ scopeInfo,
+ available: zero,
+ pendingIncoming: zero,
+ pendingOutgoing: zero,
+ flagIncomingAml: false,
+ flagIncomingConfirmation: false,
+ flagIncomingKyc: false,
+ flagOutgoingKyc: false,
+ };
+ }
+ return this.balanceStore[balanceKey];
+ }
+
+ async addZero(currency: string, exchangeBaseUrl: string): Promise<void> {
+ await this.initBalance(currency, exchangeBaseUrl);
+ }
+
+ async addAvailable(
+ currency: string,
+ exchangeBaseUrl: string,
+ amount: AmountLike,
+ ): Promise<void> {
+ const b = await this.initBalance(currency, exchangeBaseUrl);
+ b.available = Amounts.add(b.available, amount).amount;
+ }
+
+ async addPendingIncoming(
+ currency: string,
+ exchangeBaseUrl: string,
+ amount: AmountLike,
+ ): Promise<void> {
+ const b = await this.initBalance(currency, exchangeBaseUrl);
+ b.pendingIncoming = Amounts.add(b.pendingIncoming, amount).amount;
+ }
+
+ async addPendingOutgoing(
+ currency: string,
+ exchangeBaseUrl: string,
+ amount: AmountLike,
+ ): Promise<void> {
+ const b = await this.initBalance(currency, exchangeBaseUrl);
+ b.pendingOutgoing = Amounts.add(b.pendingOutgoing, amount).amount;
+ }
+
+ async setFlagIncomingAml(
+ currency: string,
+ exchangeBaseUrl: string,
+ ): Promise<void> {
+ const b = await this.initBalance(currency, exchangeBaseUrl);
+ b.flagIncomingAml = true;
+ }
+
+ async setFlagIncomingKyc(
+ currency: string,
+ exchangeBaseUrl: string,
+ ): Promise<void> {
+ const b = await this.initBalance(currency, exchangeBaseUrl);
+ b.flagIncomingKyc = true;
+ }
+
+ async setFlagIncomingConfirmation(
+ currency: string,
+ exchangeBaseUrl: string,
+ ): Promise<void> {
+ const b = await this.initBalance(currency, exchangeBaseUrl);
+ b.flagIncomingConfirmation = true;
+ }
+
+ async setFlagOutgoingKyc(
+ currency: string,
+ exchangeBaseUrl: string,
+ ): Promise<void> {
+ const b = await this.initBalance(currency, exchangeBaseUrl);
+ b.flagOutgoingKyc = true;
+ }
+
+ toBalancesResponse(): BalancesResponse {
+ const balancesResponse: BalancesResponse = {
+ balances: [],
+ };
+
+ const balanceStore = this.balanceStore;
+
+ Object.keys(balanceStore)
+ .sort()
+ .forEach((c) => {
+ const v = balanceStore[c];
+ const flags: BalanceFlag[] = [];
+ if (v.flagIncomingAml) {
+ flags.push(BalanceFlag.IncomingAml);
+ }
+ if (v.flagIncomingKyc) {
+ flags.push(BalanceFlag.IncomingKyc);
+ }
+ if (v.flagIncomingConfirmation) {
+ flags.push(BalanceFlag.IncomingConfirmation);
+ }
+ if (v.flagOutgoingKyc) {
+ flags.push(BalanceFlag.OutgoingKyc);
+ }
+ balancesResponse.balances.push({
+ scopeInfo: v.scopeInfo,
+ available: Amounts.stringify(v.available),
+ pendingIncoming: Amounts.stringify(v.pendingIncoming),
+ pendingOutgoing: Amounts.stringify(v.pendingOutgoing),
+ // FIXME: This field is basically not implemented, do we even need it?
+ hasPendingTransactions: false,
+ // FIXME: This field is basically not implemented, do we even need it?
+ requiresUserInput: false,
+ flags,
+ });
+ });
+ return balancesResponse;
+ }
+}
+
+/**
+ * Get balance information.
+ */
+export async function getBalancesInsideTransaction(
+ wex: WalletExecutionContext,
+ tx: WalletDbReadOnlyTransaction<
+ [
+ "exchanges",
+ "exchangeDetails",
+ "coinAvailability",
+ "refreshGroups",
+ "depositGroups",
+ "withdrawalGroups",
+ "globalCurrencyAuditors",
+ "globalCurrencyExchanges",
+ "peerPushDebit",
+ ]
+ >,
+): Promise<BalancesResponse> {
+ const balanceStore: BalancesStore = new BalancesStore(wex, tx);
+
+ const keyRangeActive = GlobalIDB.KeyRange.bound(
+ OPERATION_STATUS_ACTIVE_FIRST,
+ OPERATION_STATUS_ACTIVE_LAST,
+ );
+
+ await tx.exchanges.iter().forEachAsync(async (ex) => {
+ if (
+ ex.entryStatus === ExchangeEntryDbRecordStatus.Used ||
+ ex.tosAcceptedTimestamp != null
+ ) {
+ const det = await getExchangeWireDetailsInTx(tx, ex.baseUrl);
+ if (det) {
+ await balanceStore.addZero(det.currency, ex.baseUrl);
+ }
+ }
+ });
+
+ await tx.coinAvailability.iter().forEachAsync(async (ca) => {
+ const count = ca.visibleCoinCount ?? 0;
+ await balanceStore.addZero(ca.currency, ca.exchangeBaseUrl);
+ for (let i = 0; i < count; i++) {
+ await balanceStore.addAvailable(
+ ca.currency,
+ ca.exchangeBaseUrl,
+ ca.value,
+ );
+ }
+ });
+
+ await tx.refreshGroups.iter().forEachAsync(async (r) => {
+ switch (r.operationStatus) {
+ case RefreshOperationStatus.Pending:
+ case RefreshOperationStatus.Suspended:
+ break;
+ default:
+ return;
+ }
+ const perExchange = r.infoPerExchange;
+ if (!perExchange) {
+ return;
+ }
+ for (const [e, x] of Object.entries(perExchange)) {
+ await balanceStore.addAvailable(r.currency, e, x.outputEffective);
+ }
+ });
+
+ await tx.withdrawalGroups.indexes.byStatus
+ .iter(keyRangeActive)
+ .forEachAsync(async (wgRecord) => {
+ const currency = Amounts.currencyOf(wgRecord.denomsSel.totalCoinValue);
+ switch (wgRecord.status) {
+ case WithdrawalGroupStatus.AbortedBank:
+ case WithdrawalGroupStatus.AbortedExchange:
+ case WithdrawalGroupStatus.FailedAbortingBank:
+ case WithdrawalGroupStatus.FailedBankAborted:
+ case WithdrawalGroupStatus.Done:
+ // Does not count as pendingIncoming
+ return;
+ case WithdrawalGroupStatus.PendingReady:
+ case WithdrawalGroupStatus.AbortingBank:
+ case WithdrawalGroupStatus.PendingQueryingStatus:
+ case WithdrawalGroupStatus.SuspendedWaitConfirmBank:
+ case WithdrawalGroupStatus.SuspendedReady:
+ case WithdrawalGroupStatus.SuspendedRegisteringBank:
+ case WithdrawalGroupStatus.SuspendedAbortingBank:
+ case WithdrawalGroupStatus.SuspendedQueryingStatus:
+ // Pending, but no special flag.
+ break;
+ case WithdrawalGroupStatus.SuspendedKyc:
+ case WithdrawalGroupStatus.PendingKyc:
+ await balanceStore.setFlagIncomingKyc(
+ currency,
+ wgRecord.exchangeBaseUrl,
+ );
+ break;
+ case WithdrawalGroupStatus.PendingAml:
+ case WithdrawalGroupStatus.SuspendedAml:
+ await balanceStore.setFlagIncomingAml(
+ currency,
+ wgRecord.exchangeBaseUrl,
+ );
+ break;
+ case WithdrawalGroupStatus.PendingRegisteringBank:
+ case WithdrawalGroupStatus.PendingWaitConfirmBank:
+ await balanceStore.setFlagIncomingConfirmation(
+ currency,
+ wgRecord.exchangeBaseUrl,
+ );
+ break;
+ default:
+ assertUnreachable(wgRecord.status);
+ }
+ await balanceStore.addPendingIncoming(
+ currency,
+ wgRecord.exchangeBaseUrl,
+ wgRecord.denomsSel.totalCoinValue,
+ );
+ });
+
+ await tx.peerPushDebit.indexes.byStatus
+ .iter(keyRangeActive)
+ .forEachAsync(async (ppdRecord) => {
+ switch (ppdRecord.status) {
+ case PeerPushDebitStatus.AbortingDeletePurse:
+ case PeerPushDebitStatus.SuspendedAbortingDeletePurse:
+ case PeerPushDebitStatus.PendingReady:
+ case PeerPushDebitStatus.SuspendedReady:
+ case PeerPushDebitStatus.PendingCreatePurse:
+ case PeerPushDebitStatus.SuspendedCreatePurse: {
+ const currency = Amounts.currencyOf(ppdRecord.amount);
+ await balanceStore.addPendingOutgoing(
+ currency,
+ ppdRecord.exchangeBaseUrl,
+ ppdRecord.totalCost,
+ );
+ break;
+ }
+ }
+ });
+
+ await tx.depositGroups.indexes.byStatus
+ .iter(keyRangeActive)
+ .forEachAsync(async (dgRecord) => {
+ const perExchange = dgRecord.infoPerExchange;
+ if (!perExchange) {
+ return;
+ }
+ for (const [e, x] of Object.entries(perExchange)) {
+ const currency = Amounts.currencyOf(dgRecord.amount);
+ switch (dgRecord.operationStatus) {
+ case DepositOperationStatus.SuspendedKyc:
+ case DepositOperationStatus.PendingKyc:
+ await balanceStore.setFlagOutgoingKyc(currency, e);
+ }
+
+ switch (dgRecord.operationStatus) {
+ case DepositOperationStatus.SuspendedKyc:
+ case DepositOperationStatus.PendingKyc:
+ case DepositOperationStatus.PendingTrack:
+ case DepositOperationStatus.SuspendedAborting:
+ case DepositOperationStatus.SuspendedDeposit:
+ case DepositOperationStatus.SuspendedTrack:
+ case DepositOperationStatus.PendingDeposit: {
+ const perExchange = dgRecord.infoPerExchange;
+ if (perExchange) {
+ for (const [e, v] of Object.entries(perExchange)) {
+ await balanceStore.addPendingOutgoing(
+ currency,
+ e,
+ v.amountEffective,
+ );
+ }
+ }
+ }
+ }
+ }
+ });
+
+ return balanceStore.toBalancesResponse();
+}
+
+/**
+ * Get detailed balance information, sliced by exchange and by currency.
+ */
+export async function getBalances(
+ wex: WalletExecutionContext,
+): Promise<BalancesResponse> {
+ logger.trace("starting to compute balance");
+
+ const wbal = await wex.db.runReadWriteTx(
+ {
+ storeNames: [
+ "coinAvailability",
+ "coins",
+ "depositGroups",
+ "exchangeDetails",
+ "exchanges",
+ "globalCurrencyAuditors",
+ "globalCurrencyExchanges",
+ "purchases",
+ "refreshGroups",
+ "withdrawalGroups",
+ "peerPushDebit",
+ ],
+ },
+ async (tx) => {
+ return getBalancesInsideTransaction(wex, tx);
+ },
+ );
+
+ logger.trace("finished computing wallet balance");
+
+ return wbal;
+}
+
+export interface PaymentRestrictionsForBalance {
+ currency: string;
+ minAge: number;
+ restrictExchanges: ExchangeRestrictionSpec | undefined;
+ restrictWireMethods: string[] | undefined;
+ depositPaytoUri: string | undefined;
+}
+
+export interface AcceptableExchanges {
+ /**
+ * Exchanges accepted by the merchant, but wire method might not match.
+ */
+ acceptableExchanges: string[];
+
+ /**
+ * Exchanges accepted by the merchant, including a matching
+ * wire method, i.e. the merchant can deposit coins there.
+ */
+ depositableExchanges: string[];
+}
+
+export interface PaymentBalanceDetails {
+ /**
+ * Balance of type "available" (see balance.ts for definition).
+ */
+ balanceAvailable: AmountJson;
+
+ /**
+ * Balance of type "material" (see balance.ts for definition).
+ */
+ balanceMaterial: AmountJson;
+
+ /**
+ * Balance of type "age-acceptable" (see balance.ts for definition).
+ */
+ balanceAgeAcceptable: AmountJson;
+
+ /**
+ * Balance of type "merchant-acceptable" (see balance.ts for definition).
+ */
+ balanceReceiverAcceptable: AmountJson;
+
+ /**
+ * Balance of type "merchant-depositable" (see balance.ts for definition).
+ */
+ balanceReceiverDepositable: AmountJson;
+
+ /**
+ * Balance that's depositable with the exchange.
+ * This balance is reduced by the exchange's debit restrictions
+ * and wire fee configuration.
+ */
+ balanceExchangeDepositable: AmountJson;
+
+ maxEffectiveSpendAmount: AmountJson;
+}
+
+export async function getPaymentBalanceDetails(
+ wex: WalletExecutionContext,
+ req: PaymentRestrictionsForBalance,
+): Promise<PaymentBalanceDetails> {
+ return await wex.db.runReadOnlyTx(
+ {
+ storeNames: [
+ "coinAvailability",
+ "refreshGroups",
+ "exchanges",
+ "exchangeDetails",
+ "denominations",
+ ],
+ },
+ async (tx) => {
+ return getPaymentBalanceDetailsInTx(wex, tx, req);
+ },
+ );
+}
+
+export async function getPaymentBalanceDetailsInTx(
+ wex: WalletExecutionContext,
+ tx: WalletDbReadOnlyTransaction<
+ [
+ "coinAvailability",
+ "refreshGroups",
+ "exchanges",
+ "exchangeDetails",
+ "denominations",
+ ]
+ >,
+ req: PaymentRestrictionsForBalance,
+): Promise<PaymentBalanceDetails> {
+ const d: PaymentBalanceDetails = {
+ balanceAvailable: Amounts.zeroOfCurrency(req.currency),
+ balanceMaterial: Amounts.zeroOfCurrency(req.currency),
+ balanceAgeAcceptable: Amounts.zeroOfCurrency(req.currency),
+ balanceReceiverAcceptable: Amounts.zeroOfCurrency(req.currency),
+ balanceReceiverDepositable: Amounts.zeroOfCurrency(req.currency),
+ maxEffectiveSpendAmount: Amounts.zeroOfCurrency(req.currency),
+ balanceExchangeDepositable: Amounts.zeroOfCurrency(req.currency),
+ };
+
+ logger.info(`computing balance details for ${j2s(req)}`);
+
+ const availableCoins = await tx.coinAvailability.getAll();
+
+ for (const ca of availableCoins) {
+ if (ca.currency != req.currency) {
+ continue;
+ }
+
+ const denom = await getDenomInfo(
+ wex,
+ tx,
+ ca.exchangeBaseUrl,
+ ca.denomPubHash,
+ );
+ if (!denom) {
+ continue;
+ }
+
+ const wireDetails = await getExchangeWireDetailsInTx(
+ tx,
+ ca.exchangeBaseUrl,
+ );
+ if (!wireDetails) {
+ continue;
+ }
+
+ const singleCoinAmount: AmountJson = Amounts.parseOrThrow(ca.value);
+ const coinAmount: AmountJson = Amounts.mult(
+ singleCoinAmount,
+ ca.freshCoinCount,
+ ).amount;
+
+ let wireOkay = false;
+ if (req.restrictWireMethods == null) {
+ wireOkay = true;
+ } else {
+ for (const wm of req.restrictWireMethods) {
+ const wmf = findMatchingWire(wm, req.depositPaytoUri, wireDetails);
+ if (wmf) {
+ wireOkay = true;
+ break;
+ }
+ }
+ }
+
+ if (wireOkay) {
+ d.balanceExchangeDepositable = Amounts.add(
+ d.balanceExchangeDepositable,
+ coinAmount,
+ ).amount;
+ }
+
+ let ageOkay = ca.maxAge === 0 || ca.maxAge > req.minAge;
+
+ let merchantExchangeAcceptable = false;
+
+ if (!req.restrictExchanges) {
+ merchantExchangeAcceptable = true;
+ } else {
+ for (const ex of req.restrictExchanges.exchanges) {
+ if (ex.exchangeBaseUrl === ca.exchangeBaseUrl) {
+ merchantExchangeAcceptable = true;
+ break;
+ }
+ }
+ for (const acceptedAuditor of req.restrictExchanges.auditors) {
+ for (const exchangeAuditor of wireDetails.auditors) {
+ if (acceptedAuditor.auditorBaseUrl === exchangeAuditor.auditor_url) {
+ merchantExchangeAcceptable = true;
+ break;
+ }
+ }
+ }
+ }
+
+ const merchantExchangeDepositable = merchantExchangeAcceptable && wireOkay;
+
+ d.balanceAvailable = Amounts.add(d.balanceAvailable, coinAmount).amount;
+ d.balanceMaterial = Amounts.add(d.balanceMaterial, coinAmount).amount;
+ if (ageOkay) {
+ d.balanceAgeAcceptable = Amounts.add(
+ d.balanceAgeAcceptable,
+ coinAmount,
+ ).amount;
+ if (merchantExchangeAcceptable) {
+ d.balanceReceiverAcceptable = Amounts.add(
+ d.balanceReceiverAcceptable,
+ coinAmount,
+ ).amount;
+ if (merchantExchangeDepositable) {
+ d.balanceReceiverDepositable = Amounts.add(
+ d.balanceReceiverDepositable,
+ coinAmount,
+ ).amount;
+ }
+ }
+ }
+
+ if (
+ ageOkay &&
+ wireOkay &&
+ merchantExchangeAcceptable &&
+ merchantExchangeDepositable
+ ) {
+ d.maxEffectiveSpendAmount = Amounts.add(
+ d.maxEffectiveSpendAmount,
+ Amounts.mult(ca.value, ca.freshCoinCount).amount,
+ ).amount;
+
+ d.maxEffectiveSpendAmount = Amounts.sub(
+ d.maxEffectiveSpendAmount,
+ Amounts.mult(denom.feeDeposit, ca.freshCoinCount).amount,
+ ).amount;
+ }
+ }
+
+ await tx.refreshGroups.iter().forEach((r) => {
+ if (r.currency != req.currency) {
+ return;
+ }
+ d.balanceAvailable = Amounts.add(
+ d.balanceAvailable,
+ computeRefreshGroupAvailableAmount(r),
+ ).amount;
+ });
+
+ return d;
+}
+
+export async function getBalanceDetail(
+ wex: WalletExecutionContext,
+ req: GetBalanceDetailRequest,
+): Promise<PaymentBalanceDetails> {
+ const exchanges: { exchangeBaseUrl: string; exchangePub: string }[] = [];
+ const wires = new Array<string>();
+ await wex.db.runReadOnlyTx(
+ { storeNames: ["exchanges", "exchangeDetails"] },
+ async (tx) => {
+ const allExchanges = await tx.exchanges.iter().toArray();
+ for (const e of allExchanges) {
+ const details = await getExchangeWireDetailsInTx(tx, e.baseUrl);
+ if (!details || req.currency !== details.currency) {
+ continue;
+ }
+ details.wireInfo.accounts.forEach((a) => {
+ const payto = parsePaytoUri(a.payto_uri);
+ if (payto && !wires.includes(payto.targetType)) {
+ wires.push(payto.targetType);
+ }
+ });
+ exchanges.push({
+ exchangePub: details.masterPublicKey,
+ exchangeBaseUrl: e.baseUrl,
+ });
+ }
+ },
+ );
+
+ return await getPaymentBalanceDetails(wex, {
+ currency: req.currency,
+ restrictExchanges: {
+ auditors: [],
+ exchanges,
+ },
+ restrictWireMethods: wires,
+ minAge: 0,
+ depositPaytoUri: undefined,
+ });
+}
diff --git a/packages/taler-wallet-core/src/bank-api-client.ts b/packages/taler-wallet-core/src/bank-api-client.ts
deleted file mode 100644
index dc7845150..000000000
--- a/packages/taler-wallet-core/src/bank-api-client.ts
+++ /dev/null
@@ -1,279 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- * Client for the Taler (demo-)bank.
- */
-
-/**
- * Imports.
- */
-import {
- AmountString,
- base64FromArrayBuffer,
- buildCodecForObject,
- Codec,
- codecForAny,
- codecForString,
- encodeCrock,
- getRandomBytes,
- j2s,
- Logger,
- stringToBytes,
- TalerErrorCode,
-} from "@gnu-taler/taler-util";
-import { TalerError } from "./errors.js";
-import {
- HttpRequestLibrary,
- readSuccessResponseJsonOrThrow,
-} from "./util/http.js";
-
-const logger = new Logger("bank-api-client.ts");
-
-export enum CreditDebitIndicator {
- Credit = "credit",
- Debit = "debit",
-}
-
-export interface BankAccountBalanceResponse {
- balance: {
- amount: AmountString;
- credit_debit_indicator: CreditDebitIndicator;
- };
-}
-
-export interface BankServiceHandle {
- readonly baseUrl: string;
- readonly bankAccessApiBaseUrl: string;
- readonly http: HttpRequestLibrary;
-}
-
-export interface BankUser {
- username: string;
- password: string;
- accountPaytoUri: string;
-}
-
-export interface WithdrawalOperationInfo {
- withdrawal_id: string;
- taler_withdraw_uri: string;
-}
-
-/**
- * FIXME: Rename, this is not part of the integration test harness anymore.
- */
-export interface HarnessExchangeBankAccount {
- accountName: string;
- accountPassword: string;
- accountPaytoUri: string;
- wireGatewayApiBaseUrl: string;
-}
-
-/**
- * Helper function to generate the "Authorization" HTTP header.
- */
-function makeBasicAuthHeader(username: string, password: string): string {
- const auth = `${username}:${password}`;
- const authEncoded: string = base64FromArrayBuffer(stringToBytes(auth));
- return `Basic ${authEncoded}`;
-}
-
-const codecForWithdrawalOperationInfo = (): Codec<WithdrawalOperationInfo> =>
- buildCodecForObject<WithdrawalOperationInfo>()
- .property("withdrawal_id", codecForString())
- .property("taler_withdraw_uri", codecForString())
- .build("WithdrawalOperationInfo");
-
-export namespace BankApi {
- // FIXME: Move to BankAccessApi?!
- export async function registerAccount(
- bank: BankServiceHandle,
- username: string,
- password: string,
- ): Promise<BankUser> {
- const url = new URL("testing/register", bank.bankAccessApiBaseUrl);
- const resp = await bank.http.postJson(url.href, { username, password });
- let paytoUri = `payto://x-taler-bank/localhost/${username}`;
- if (resp.status !== 200 && resp.status !== 202 && resp.status !== 204) {
- logger.error(`${j2s(await resp.json())}`);
- throw TalerError.fromDetail(
- TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR,
- {
- httpStatusCode: resp.status,
- },
- );
- }
- try {
- // Pybank has no body, thus this might throw.
- const respJson = await resp.json();
- // LibEuFin demobank returns payto URI in response
- if (respJson.paytoUri) {
- paytoUri = respJson.paytoUri;
- }
- } catch (e) {
- // Do nothing
- }
- return {
- password,
- username,
- accountPaytoUri: paytoUri,
- };
- }
-
- // FIXME: Move to BankAccessApi?!
- export async function createRandomBankUser(
- bank: BankServiceHandle,
- ): Promise<BankUser> {
- const username = "user-" + encodeCrock(getRandomBytes(10)).toLowerCase();
- const password = "pw-" + encodeCrock(getRandomBytes(10)).toLowerCase();
- return await registerAccount(bank, username, password);
- }
-
- export async function adminAddIncoming(
- bank: BankServiceHandle,
- params: {
- exchangeBankAccount: HarnessExchangeBankAccount;
- amount: string;
- reservePub: string;
- debitAccountPayto: string;
- },
- ) {
- let maybeBaseUrl = bank.baseUrl;
- let url = new URL(
- `taler-wire-gateway/${params.exchangeBankAccount.accountName}/admin/add-incoming`,
- maybeBaseUrl,
- );
- await bank.http.postJson(
- url.href,
- {
- amount: params.amount,
- reserve_pub: params.reservePub,
- debit_account: params.debitAccountPayto,
- },
- {
- headers: {
- Authorization: makeBasicAuthHeader(
- params.exchangeBankAccount.accountName,
- params.exchangeBankAccount.accountPassword,
- ),
- },
- },
- );
- }
-
- export async function confirmWithdrawalOperation(
- bank: BankServiceHandle,
- bankUser: BankUser,
- wopi: WithdrawalOperationInfo,
- ): Promise<void> {
- const url = new URL(
- `accounts/${bankUser.username}/withdrawals/${wopi.withdrawal_id}/confirm`,
- bank.bankAccessApiBaseUrl,
- );
- logger.info(`confirming withdrawal operation via ${url.href}`);
- const resp = await bank.http.postJson(
- url.href,
- {},
- {
- headers: {
- Authorization: makeBasicAuthHeader(
- bankUser.username,
- bankUser.password,
- ),
- },
- },
- );
-
- logger.info(`response status ${resp.status}`);
- const respJson = await readSuccessResponseJsonOrThrow(resp, codecForAny());
-
- // FIXME: We don't check the status here!
- }
-
- export async function abortWithdrawalOperation(
- bank: BankServiceHandle,
- bankUser: BankUser,
- wopi: WithdrawalOperationInfo,
- ): Promise<void> {
- const url = new URL(
- `accounts/${bankUser.username}/withdrawals/${wopi.withdrawal_id}/abort`,
- bank.bankAccessApiBaseUrl,
- );
- const resp = await bank.http.postJson(
- url.href,
- {},
- {
- headers: {
- Authorization: makeBasicAuthHeader(
- bankUser.username,
- bankUser.password,
- ),
- },
- },
- );
- await readSuccessResponseJsonOrThrow(resp, codecForAny());
- }
-}
-
-export namespace BankAccessApi {
- export async function getAccountBalance(
- bank: BankServiceHandle,
- bankUser: BankUser,
- ): Promise<BankAccountBalanceResponse> {
- const url = new URL(
- `accounts/${bankUser.username}`,
- bank.bankAccessApiBaseUrl,
- );
- const resp = await bank.http.get(url.href, {
- headers: {
- Authorization: makeBasicAuthHeader(
- bankUser.username,
- bankUser.password,
- ),
- },
- });
- return await resp.json();
- }
-
- export async function createWithdrawalOperation(
- bank: BankServiceHandle,
- bankUser: BankUser,
- amount: string,
- ): Promise<WithdrawalOperationInfo> {
- const url = new URL(
- `accounts/${bankUser.username}/withdrawals`,
- bank.bankAccessApiBaseUrl,
- );
- const resp = await bank.http.postJson(
- url.href,
- {
- amount,
- },
- {
- headers: {
- Authorization: makeBasicAuthHeader(
- bankUser.username,
- bankUser.password,
- ),
- },
- },
- );
- return readSuccessResponseJsonOrThrow(
- resp,
- codecForWithdrawalOperationInfo(),
- );
- }
-}
diff --git a/packages/taler-wallet-core/src/coinSelection.test.ts b/packages/taler-wallet-core/src/coinSelection.test.ts
new file mode 100644
index 000000000..c7cb2857e
--- /dev/null
+++ b/packages/taler-wallet-core/src/coinSelection.test.ts
@@ -0,0 +1,281 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ AbsoluteTime,
+ AmountString,
+ Amounts,
+ DenomKeyType,
+ Duration,
+ j2s,
+} from "@gnu-taler/taler-util";
+import test from "ava";
+import {
+ AvailableDenom,
+ CoinSelectionTally,
+ emptyTallyForPeerPayment,
+ testing_selectGreedy,
+} from "./coinSelection.js";
+
+const inTheDistantFuture = AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.addDuration(AbsoluteTime.now(), Duration.fromSpec({ hours: 1 })),
+);
+
+const inThePast = AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.subtractDuraction(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ hours: 1 }),
+ ),
+);
+
+test("p2p: should select the coin", (t) => {
+ const instructedAmount = Amounts.parseOrThrow("LOCAL:2");
+ const tally = emptyTallyForPeerPayment(instructedAmount);
+ t.log(`tally before: ${j2s(tally)}`);
+ const coins = testing_selectGreedy(
+ {
+ wireFeesPerExchange: {},
+ },
+ createCandidates([
+ {
+ amount: "LOCAL:10" as AmountString,
+ numAvailable: 5,
+ depositFee: "LOCAL:0.1" as AmountString,
+ fromExchange: "http://exchange.localhost/",
+ },
+ ]),
+ tally,
+ );
+
+ t.log(`coins: ${j2s(coins)}`);
+ t.log(`tally: ${j2s(tally)}`);
+
+ t.assert(coins != null);
+
+ t.deepEqual(coins, {
+ "hash0;32;http://exchange.localhost/": {
+ exchangeBaseUrl: "http://exchange.localhost/",
+ denomPubHash: "hash0",
+ maxAge: 32,
+ contributions: [Amounts.parseOrThrow("LOCAL:2.1")],
+ },
+ });
+});
+
+test("p2p: should select 3 coins", (t) => {
+ const instructedAmount = Amounts.parseOrThrow("LOCAL:20");
+ const tally = emptyTallyForPeerPayment(instructedAmount);
+ const coins = testing_selectGreedy(
+ {
+ wireFeesPerExchange: {},
+ },
+ createCandidates([
+ {
+ amount: "LOCAL:10" as AmountString,
+ numAvailable: 5,
+ depositFee: "LOCAL:0.1" as AmountString,
+ fromExchange: "http://exchange.localhost/",
+ },
+ ]),
+ tally,
+ );
+
+ t.deepEqual(coins, {
+ "hash0;32;http://exchange.localhost/": {
+ exchangeBaseUrl: "http://exchange.localhost/",
+ denomPubHash: "hash0",
+ maxAge: 32,
+ contributions: [
+ Amounts.parseOrThrow("LOCAL:10"),
+ Amounts.parseOrThrow("LOCAL:10"),
+ Amounts.parseOrThrow("LOCAL:0.3"),
+ ],
+ },
+ });
+});
+
+test("p2p: can't select since the instructed amount is too high", (t) => {
+ const instructedAmount = Amounts.parseOrThrow("LOCAL:60");
+ const tally = emptyTallyForPeerPayment(instructedAmount);
+ const coins = testing_selectGreedy(
+ {
+ wireFeesPerExchange: {},
+ },
+ createCandidates([
+ {
+ amount: "LOCAL:10" as AmountString,
+ numAvailable: 5,
+ depositFee: "LOCAL:0.1" as AmountString,
+ fromExchange: "http://exchange.localhost/",
+ },
+ ]),
+ tally,
+ );
+
+ t.is(coins, undefined);
+});
+
+test("pay: select one coin to pay with fee", (t) => {
+ const payment = Amounts.parseOrThrow("LOCAL:2");
+ const exchangeWireFee = Amounts.parseOrThrow("LOCAL:0.1");
+ const zero = Amounts.zeroOfCurrency(payment.currency);
+ const tally = {
+ amountPayRemaining: payment,
+ amountDepositFeeLimitRemaining: zero,
+ customerDepositFees: zero,
+ customerWireFees: zero,
+ wireFeeCoveredForExchange: new Set<string>(),
+ lastDepositFee: zero,
+ } satisfies CoinSelectionTally;
+ const coins = testing_selectGreedy(
+ {
+ wireFeesPerExchange: { "http://exchange.localhost/": exchangeWireFee },
+ },
+ createCandidates([
+ {
+ amount: "LOCAL:10" as AmountString,
+ numAvailable: 5,
+ depositFee: "LOCAL:0.1" as AmountString,
+ fromExchange: "http://exchange.localhost/",
+ },
+ ]),
+ tally,
+ );
+
+ t.deepEqual(coins, {
+ "hash0;32;http://exchange.localhost/": {
+ exchangeBaseUrl: "http://exchange.localhost/",
+ denomPubHash: "hash0",
+ maxAge: 32,
+ contributions: [Amounts.parseOrThrow("LOCAL:2.2")],
+ },
+ });
+
+ t.deepEqual(tally, {
+ amountPayRemaining: Amounts.parseOrThrow("LOCAL:0"),
+ amountDepositFeeLimitRemaining: zero,
+ customerDepositFees: Amounts.parse("LOCAL:0.1"),
+ customerWireFees: Amounts.parse("LOCAL:0.1"),
+ wireFeeCoveredForExchange: new Set(["http://exchange.localhost/"]),
+ lastDepositFee: Amounts.parse("LOCAL:0.1"),
+ });
+});
+
+function createCandidates(
+ ar: {
+ amount: AmountString;
+ depositFee: AmountString;
+ numAvailable: number;
+ fromExchange: string;
+ }[],
+): AvailableDenom[] {
+ return ar.map((r, idx) => {
+ return {
+ denomPub: {
+ age_mask: 0,
+ cipher: DenomKeyType.Rsa,
+ rsa_public_key: "PPP",
+ },
+ denomPubHash: `hash${idx}`,
+ value: r.amount,
+ feeDeposit: r.depositFee,
+ feeRefresh: "LOCAL:0" as AmountString,
+ feeRefund: "LOCAL:0" as AmountString,
+ feeWithdraw: "LOCAL:0" as AmountString,
+ stampExpireDeposit: inTheDistantFuture,
+ stampExpireLegal: inTheDistantFuture,
+ stampExpireWithdraw: inTheDistantFuture,
+ stampStart: inThePast,
+ exchangeBaseUrl: r.fromExchange,
+ numAvailable: r.numAvailable,
+ maxAge: 32,
+ };
+ });
+}
+
+test("p2p: regression STATER", (t) => {
+ const candidates = [
+ {
+ denomPub: {
+ age_mask: 349441,
+ cipher: "RSA",
+ rsa_public_key:
+ "040000WTR9ERP6FYDM4581C1WY4DX6EA6ZP0RKDEY1VCEG1HGZQDB1E1MT0HSPWKVWYY8GN99YG8JV2BQHCV608V3AP00HZ44M4R2RDK3MEG1HY3H5VP2YESFDXC8C2J0BT6E662JJYN4MCFR8Q8ZFD7ZCA8HGBNVG4JMTS5MBDTF9CX3JC25H702K1FG2C54HR48767D18F2H11HMVK7EEF51QRGE08T704VRCNZ6WTM3Z73Z5DW4W26GBEWTDZZ4HX94HRJEH8YENXAW5T5E39TQQN7MZ7HEPB59BQWB0DDMM8MAE274BV3HC2AJVCSXFJSKBAK1B9HKERPWF7Z5556VJG6YJ9236G5SFM3RC22PJM2SXHYBWFV1WBAYF1F2026C0CM5Q3RPQETHCWZTEX8KJ2J1K904002",
+ },
+ denomPubHash:
+ "TF5S4VJ8P3NN0SM5R1KW5MP665KEFMGAT2RPR70BMG0WQ5A72J53GDDE0YSCTWEXHRW8FMMX3X27RQK4D1VH69GVJBYR5RSJY3X5FS8",
+ feeDeposit: "STATER:1",
+ feeRefresh: "STATER:0",
+ feeRefund: "STATER:0",
+ feeWithdraw: "STATER:0",
+ stampExpireDeposit: {
+ t_s: 1772722025,
+ },
+ stampExpireLegal: {
+ t_s: 1961938025,
+ },
+ stampExpireWithdraw: {
+ t_s: 1709650025,
+ },
+ stampStart: {
+ t_s: 1709045225,
+ },
+ value: "STATER:2",
+ exchangeBaseUrl: "https://exchange.taler.grothoff.org/",
+ numAvailable: 6,
+ maxAge: 32,
+ },
+ {
+ denomPub: {
+ age_mask: 349441,
+ cipher: "RSA",
+ rsa_public_key:
+ "040000Y84BTTQCZ28AS2KZ867V05WES3YPN34X51DNF14ADGW2HNG9YFXCCNVQ2JA9ZT3KSBD17ZN9Y71KGWAWEFYMHE0S61DW63WN58VWRXQ92440V1JSZDD7FDTYEVNGG8ZVARVZ4GGF1RCDM93R28M067S5CPRZFCCQBRFFM9YDK2W06WDXE96BDCB8MZEYPHSGK5CTDY6XJE18EMRWYRBAG0H8P6QGQS73REXX66PTJ3MRX3AK3ARZF8417QKMZZPNS1JV5EYPAC7X8R1F9G1GWAQXVVQ2XTA5NMVMNJDJ0KEM93AXD4W2C7XMVJFSQN8RVB9KZ8JXWGN1YJQK7P6476HV896THKQ05QK4F0C65P4HA7QDX84C91F42PZVMH8AMYMA2NBXEYXS0EV8NXZHMZ30JF04002",
+ },
+ denomPubHash:
+ "WCMKBGR8ZKJ62YZXCRNT3EHPFQQ2M0B5CGZXW0PYA76G8PPXJMXZ7Q3WBP2DA3Z4BF21K3X9AG769RYCC39C3PT0R1DCTJA2PRTSHSR",
+ feeDeposit: "STATER:1",
+ feeRefresh: "STATER:0",
+ feeRefund: "STATER:0",
+ feeWithdraw: "STATER:0",
+ stampExpireDeposit: {
+ t_s: 1772722025,
+ },
+ stampExpireLegal: {
+ t_s: 1961938025,
+ },
+ stampExpireWithdraw: {
+ t_s: 1709650025,
+ },
+ stampStart: {
+ t_s: 1709045225,
+ },
+ value: "STATER:1",
+ exchangeBaseUrl: "https://exchange.taler.grothoff.org/",
+ numAvailable: 1,
+ maxAge: 32,
+ },
+ ];
+ const instructedAmount = Amounts.parseOrThrow("STATER:1");
+ const tally = emptyTallyForPeerPayment(instructedAmount);
+ const res = testing_selectGreedy(
+ {
+ wireFeesPerExchange: {},
+ },
+ candidates as any,
+ tally,
+ );
+ t.assert(!!res);
+});
diff --git a/packages/taler-wallet-core/src/coinSelection.ts b/packages/taler-wallet-core/src/coinSelection.ts
new file mode 100644
index 000000000..a60e41ecd
--- /dev/null
+++ b/packages/taler-wallet-core/src/coinSelection.ts
@@ -0,0 +1,1258 @@
+/*
+ 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/>
+ */
+
+/**
+ * Selection of coins for payments.
+ *
+ * @author Florian Dold
+ */
+
+/**
+ * Imports.
+ */
+import { GlobalIDB } from "@gnu-taler/idb-bridge";
+import {
+ AbsoluteTime,
+ AccountRestriction,
+ AgeRestriction,
+ AllowedAuditorInfo,
+ AllowedExchangeInfo,
+ AmountJson,
+ Amounts,
+ checkDbInvariant,
+ checkLogicInvariant,
+ CoinStatus,
+ DenominationInfo,
+ ExchangeGlobalFees,
+ ForcedCoinSel,
+ InternationalizedString,
+ j2s,
+ Logger,
+ parsePaytoUri,
+ PayCoinSelection,
+ PaymentInsufficientBalanceDetails,
+ ProspectivePayCoinSelection,
+ SelectedCoin,
+ SelectedProspectiveCoin,
+ strcmp,
+ TalerProtocolTimestamp,
+} from "@gnu-taler/taler-util";
+import { getPaymentBalanceDetailsInTx } from "./balance.js";
+import { getAutoRefreshExecuteThreshold } from "./common.js";
+import { DenominationRecord, WalletDbReadOnlyTransaction } from "./db.js";
+import {
+ ExchangeWireDetails,
+ getExchangeWireDetailsInTx,
+} from "./exchanges.js";
+import { getDenomInfo, WalletExecutionContext } from "./wallet.js";
+
+const logger = new Logger("coinSelection.ts");
+
+export type PreviousPayCoins = {
+ coinPub: string;
+ contribution: AmountJson;
+}[];
+
+export interface ExchangeRestrictionSpec {
+ exchanges: AllowedExchangeInfo[];
+ auditors: AllowedAuditorInfo[];
+}
+
+export interface CoinSelectionTally {
+ /**
+ * Amount that still needs to be paid.
+ * May increase during the computation when fees need to be covered.
+ */
+ amountPayRemaining: AmountJson;
+
+ /**
+ * Allowance given by the merchant towards deposit fees
+ * (and wire fees after wire fee limit is exhausted)
+ */
+ amountDepositFeeLimitRemaining: AmountJson;
+
+ customerDepositFees: AmountJson;
+
+ customerWireFees: AmountJson;
+
+ wireFeeCoveredForExchange: Set<string>;
+
+ lastDepositFee: AmountJson;
+}
+
+/**
+ * Account for the fees of spending a coin.
+ */
+function tallyFees(
+ tally: CoinSelectionTally,
+ wireFeesPerExchange: Record<string, AmountJson>,
+ exchangeBaseUrl: string,
+ feeDeposit: AmountJson,
+): void {
+ const currency = tally.amountPayRemaining.currency;
+
+ if (!tally.wireFeeCoveredForExchange.has(exchangeBaseUrl)) {
+ const wf =
+ wireFeesPerExchange[exchangeBaseUrl] ?? Amounts.zeroOfCurrency(currency);
+ // The remaining, amortized amount needs to be paid by the
+ // wallet or covered by the deposit fee allowance.
+ let wfRemaining = wf;
+ // This is the amount forgiven via the deposit fee allowance.
+ const wfDepositForgiven = Amounts.min(
+ tally.amountDepositFeeLimitRemaining,
+ wfRemaining,
+ );
+ tally.amountDepositFeeLimitRemaining = Amounts.sub(
+ tally.amountDepositFeeLimitRemaining,
+ wfDepositForgiven,
+ ).amount;
+ wfRemaining = Amounts.sub(wfRemaining, wfDepositForgiven).amount;
+ tally.customerWireFees = Amounts.add(
+ tally.customerWireFees,
+ wfRemaining,
+ ).amount;
+ tally.amountPayRemaining = Amounts.add(
+ tally.amountPayRemaining,
+ wfRemaining,
+ ).amount;
+ tally.wireFeeCoveredForExchange.add(exchangeBaseUrl);
+ }
+
+ const dfForgiven = Amounts.min(
+ feeDeposit,
+ tally.amountDepositFeeLimitRemaining,
+ );
+
+ tally.amountDepositFeeLimitRemaining = Amounts.sub(
+ tally.amountDepositFeeLimitRemaining,
+ dfForgiven,
+ ).amount;
+
+ // How much does the user spend on deposit fees for this coin?
+ const dfRemaining = Amounts.sub(feeDeposit, dfForgiven).amount;
+ tally.customerDepositFees = Amounts.add(
+ tally.customerDepositFees,
+ dfRemaining,
+ ).amount;
+ tally.amountPayRemaining = Amounts.add(
+ tally.amountPayRemaining,
+ dfRemaining,
+ ).amount;
+ tally.lastDepositFee = feeDeposit;
+}
+
+export type SelectPayCoinsResult =
+ | {
+ type: "failure";
+ insufficientBalanceDetails: PaymentInsufficientBalanceDetails;
+ }
+ | { type: "prospective"; result: ProspectivePayCoinSelection }
+ | { type: "success"; coinSel: PayCoinSelection };
+
+async function internalSelectPayCoins(
+ wex: WalletExecutionContext,
+ tx: WalletDbReadOnlyTransaction<
+ [
+ "coinAvailability",
+ "denominations",
+ "refreshGroups",
+ "exchanges",
+ "exchangeDetails",
+ "coins",
+ ]
+ >,
+ req: SelectPayCoinRequestNg,
+ includePendingCoins: boolean,
+): Promise<
+ | { sel: SelResult; coinRes: SelectedCoin[]; tally: CoinSelectionTally }
+ | undefined
+> {
+ const { contractTermsAmount, depositFeeLimit } = req;
+ const [candidateDenoms, wireFeesPerExchange] = await selectPayCandidates(
+ wex,
+ tx,
+ {
+ restrictExchanges: req.restrictExchanges,
+ instructedAmount: req.contractTermsAmount,
+ restrictWireMethod: req.restrictWireMethod,
+ depositPaytoUri: req.depositPaytoUri,
+ requiredMinimumAge: req.requiredMinimumAge,
+ includePendingCoins,
+ },
+ );
+
+ if (logger.shouldLogTrace()) {
+ logger.trace(
+ `instructed amount: ${Amounts.stringify(req.contractTermsAmount)}`,
+ );
+ logger.trace(`wire fees per exchange: ${j2s(wireFeesPerExchange)}`);
+ logger.trace(`candidates: ${j2s(candidateDenoms)}`);
+ }
+
+ const coinRes: SelectedCoin[] = [];
+ const currency = contractTermsAmount.currency;
+
+ let tally: CoinSelectionTally = {
+ amountPayRemaining: contractTermsAmount,
+ amountDepositFeeLimitRemaining: depositFeeLimit,
+ customerDepositFees: Amounts.zeroOfCurrency(currency),
+ customerWireFees: Amounts.zeroOfCurrency(currency),
+ wireFeeCoveredForExchange: new Set(),
+ lastDepositFee: Amounts.zeroOfCurrency(currency),
+ };
+
+ await maybeRepairCoinSelection(
+ wex,
+ tx,
+ req.prevPayCoins ?? [],
+ coinRes,
+ tally,
+ {
+ wireFeesPerExchange: wireFeesPerExchange,
+ },
+ );
+
+ let selectedDenom: SelResult | undefined;
+ if (req.forcedSelection) {
+ selectedDenom = selectForced(req, candidateDenoms);
+ } else {
+ // FIXME: Here, we should select coins in a smarter way.
+ // Instead of always spending the next-largest coin,
+ // we should try to find the smallest coin that covers the
+ // amount.
+ selectedDenom = selectGreedy(
+ {
+ wireFeesPerExchange: wireFeesPerExchange,
+ },
+ candidateDenoms,
+ tally,
+ );
+ }
+
+ if (!selectedDenom) {
+ return undefined;
+ }
+ return {
+ sel: selectedDenom,
+ coinRes,
+ tally,
+ };
+}
+
+/**
+ * Select coins to spend under the merchant's constraints.
+ *
+ * The prevPayCoins can be specified to "repair" a coin selection
+ * by adding additional coins, after a broken (e.g. double-spent) coin
+ * has been removed from the selection.
+ */
+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: [
+ "coinAvailability",
+ "denominations",
+ "refreshGroups",
+ "exchanges",
+ "exchangeDetails",
+ "coins",
+ ],
+ },
+ 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,
+ };
+ },
+ );
+}
+
+async function maybeRepairCoinSelection(
+ wex: WalletExecutionContext,
+ tx: WalletDbReadOnlyTransaction<["coins", "denominations"]>,
+ prevPayCoins: PreviousPayCoins,
+ coinRes: SelectedCoin[],
+ tally: CoinSelectionTally,
+ feeInfo: {
+ wireFeesPerExchange: Record<string, AmountJson>;
+ },
+): Promise<void> {
+ // Look at existing pay coin selection and tally up
+ for (const prev of prevPayCoins) {
+ const coin = await tx.coins.get(prev.coinPub);
+ if (!coin) {
+ continue;
+ }
+ const denom = await getDenomInfo(
+ wex,
+ tx,
+ coin.exchangeBaseUrl,
+ coin.denomPubHash,
+ );
+ if (!denom) {
+ continue;
+ }
+ tallyFees(
+ tally,
+ feeInfo.wireFeesPerExchange,
+ coin.exchangeBaseUrl,
+ Amounts.parseOrThrow(denom.feeDeposit),
+ );
+ tally.amountPayRemaining = Amounts.sub(
+ tally.amountPayRemaining,
+ prev.contribution,
+ ).amount;
+
+ coinRes.push({
+ exchangeBaseUrl: coin.exchangeBaseUrl,
+ denomPubHash: coin.denomPubHash,
+ coinPub: prev.coinPub,
+ contribution: Amounts.stringify(prev.contribution),
+ });
+ }
+}
+
+/**
+ * Returns undefined if the success response could not be assembled,
+ * as not enough coins are actually available.
+ */
+async function assembleSelectPayCoinsSuccessResult(
+ tx: WalletDbReadOnlyTransaction<["coins"]>,
+ finalSel: SelResult,
+ coinRes: SelectedCoin[],
+ tally: CoinSelectionTally,
+): Promise<PayCoinSelection> {
+ for (const dph of Object.keys(finalSel)) {
+ const selInfo = finalSel[dph];
+ const numRequested = selInfo.contributions.length;
+ const query = [
+ selInfo.exchangeBaseUrl,
+ selInfo.denomPubHash,
+ selInfo.maxAge,
+ CoinStatus.Fresh,
+ ];
+ logger.trace(`query: ${j2s(query)}`);
+ const coins =
+ await tx.coins.indexes.byExchangeDenomPubHashAndAgeAndStatus.getAll(
+ query,
+ numRequested,
+ );
+ if (coins.length != numRequested) {
+ throw Error(
+ `coin selection failed (not available anymore, got only ${coins.length}/${numRequested})`,
+ );
+ }
+
+ for (let i = 0; i < selInfo.contributions.length; i++) {
+ coinRes.push({
+ denomPubHash: coins[i].denomPubHash,
+ coinPub: coins[i].coinPub,
+ contribution: Amounts.stringify(selInfo.contributions[i]),
+ exchangeBaseUrl: coins[i].exchangeBaseUrl,
+ });
+ }
+ }
+
+ return {
+ coins: coinRes,
+ customerDepositFees: Amounts.stringify(tally.customerDepositFees),
+ customerWireFees: Amounts.stringify(tally.customerWireFees),
+ };
+}
+
+interface ReportInsufficientBalanceRequest {
+ instructedAmount: AmountJson;
+ requiredMinimumAge: number | undefined;
+ restrictExchanges: ExchangeRestrictionSpec | undefined;
+ wireMethod: string | undefined;
+ depositPaytoUri: string | undefined;
+}
+
+export async function reportInsufficientBalanceDetails(
+ wex: WalletExecutionContext,
+ tx: WalletDbReadOnlyTransaction<
+ [
+ "coinAvailability",
+ "exchanges",
+ "exchangeDetails",
+ "refreshGroups",
+ "denominations",
+ ]
+ >,
+ req: ReportInsufficientBalanceRequest,
+): Promise<PaymentInsufficientBalanceDetails> {
+ const details = await getPaymentBalanceDetailsInTx(wex, tx, {
+ restrictExchanges: req.restrictExchanges,
+ restrictWireMethods: req.wireMethod ? [req.wireMethod] : undefined,
+ currency: Amounts.currencyOf(req.instructedAmount),
+ minAge: req.requiredMinimumAge ?? 0,
+ depositPaytoUri: req.depositPaytoUri,
+ });
+ const perExchange: PaymentInsufficientBalanceDetails["perExchange"] = {};
+ const exchanges = await tx.exchanges.getAll();
+
+ for (const exch of exchanges) {
+ if (!exch.detailsPointer) {
+ continue;
+ }
+ let missingGlobalFees = false;
+ const exchWire = await getExchangeWireDetailsInTx(tx, exch.baseUrl);
+ if (!exchWire) {
+ missingGlobalFees = true;
+ } else {
+ const globalFees = getGlobalFees(exchWire);
+ if (!globalFees) {
+ missingGlobalFees = true;
+ }
+ }
+ const exchDet = await getPaymentBalanceDetailsInTx(wex, tx, {
+ restrictExchanges: {
+ exchanges: [
+ {
+ exchangeBaseUrl: exch.baseUrl,
+ exchangePub: exch.detailsPointer?.masterPublicKey,
+ },
+ ],
+ auditors: [],
+ },
+ restrictWireMethods: req.wireMethod ? [req.wireMethod] : [],
+ currency: Amounts.currencyOf(req.instructedAmount),
+ minAge: req.requiredMinimumAge ?? 0,
+ depositPaytoUri: req.depositPaytoUri,
+ });
+ perExchange[exch.baseUrl] = {
+ balanceAvailable: Amounts.stringify(exchDet.balanceAvailable),
+ balanceMaterial: Amounts.stringify(exchDet.balanceMaterial),
+ balanceExchangeDepositable: Amounts.stringify(
+ exchDet.balanceExchangeDepositable,
+ ),
+ balanceAgeAcceptable: Amounts.stringify(exchDet.balanceAgeAcceptable),
+ balanceReceiverAcceptable: Amounts.stringify(
+ exchDet.balanceReceiverAcceptable,
+ ),
+ balanceReceiverDepositable: Amounts.stringify(
+ exchDet.balanceReceiverDepositable,
+ ),
+ maxEffectiveSpendAmount: Amounts.stringify(
+ exchDet.maxEffectiveSpendAmount,
+ ),
+ missingGlobalFees,
+ };
+ }
+
+ return {
+ amountRequested: Amounts.stringify(req.instructedAmount),
+ balanceAgeAcceptable: Amounts.stringify(details.balanceAgeAcceptable),
+ balanceAvailable: Amounts.stringify(details.balanceAvailable),
+ balanceMaterial: Amounts.stringify(details.balanceMaterial),
+ balanceReceiverAcceptable: Amounts.stringify(
+ details.balanceReceiverAcceptable,
+ ),
+ balanceExchangeDepositable: Amounts.stringify(
+ details.balanceExchangeDepositable,
+ ),
+ balanceReceiverDepositable: Amounts.stringify(
+ details.balanceReceiverDepositable,
+ ),
+ maxEffectiveSpendAmount: Amounts.stringify(details.maxEffectiveSpendAmount),
+ perExchange,
+ };
+}
+
+function makeAvailabilityKey(
+ exchangeBaseUrl: string,
+ denomPubHash: string,
+ maxAge: number,
+): string {
+ return `${denomPubHash};${maxAge};${exchangeBaseUrl}`;
+}
+
+/**
+ * Selection result.
+ */
+interface SelResult {
+ /**
+ * Map from an availability key
+ * to an array of contributions.
+ */
+ [avKey: string]: {
+ exchangeBaseUrl: string;
+ denomPubHash: string;
+ maxAge: number;
+ contributions: AmountJson[];
+ };
+}
+
+export function testing_selectGreedy(
+ ...args: Parameters<typeof selectGreedy>
+): ReturnType<typeof selectGreedy> {
+ return selectGreedy(...args);
+}
+
+export interface SelectGreedyRequest {
+ wireFeesPerExchange: Record<string, AmountJson>;
+}
+
+function selectGreedy(
+ req: SelectGreedyRequest,
+ candidateDenoms: AvailableDenom[],
+ tally: CoinSelectionTally,
+): SelResult | undefined {
+ const selectedDenom: SelResult = {};
+ for (const denom of candidateDenoms) {
+ const contributions: AmountJson[] = [];
+
+ // Don't use this coin if depositing it is more expensive than
+ // the amount it would give the merchant.
+ if (Amounts.cmp(denom.feeDeposit, denom.value) > 0) {
+ tally.lastDepositFee = Amounts.parseOrThrow(denom.feeDeposit);
+ continue;
+ }
+
+ for (
+ let i = 0;
+ i < denom.numAvailable && Amounts.isNonZero(tally.amountPayRemaining);
+ i++
+ ) {
+ tallyFees(
+ tally,
+ req.wireFeesPerExchange,
+ denom.exchangeBaseUrl,
+ Amounts.parseOrThrow(denom.feeDeposit),
+ );
+
+ const coinSpend = Amounts.max(
+ Amounts.min(tally.amountPayRemaining, denom.value),
+ denom.feeDeposit,
+ );
+
+ tally.amountPayRemaining = Amounts.sub(
+ tally.amountPayRemaining,
+ coinSpend,
+ ).amount;
+
+ contributions.push(coinSpend);
+ }
+
+ if (contributions.length) {
+ const avKey = makeAvailabilityKey(
+ denom.exchangeBaseUrl,
+ denom.denomPubHash,
+ denom.maxAge,
+ );
+ let sd = selectedDenom[avKey];
+ if (!sd) {
+ sd = {
+ contributions: [],
+ denomPubHash: denom.denomPubHash,
+ exchangeBaseUrl: denom.exchangeBaseUrl,
+ maxAge: denom.maxAge,
+ };
+ }
+ sd.contributions.push(...contributions);
+ selectedDenom[avKey] = sd;
+ }
+ }
+ return Amounts.isZero(tally.amountPayRemaining) ? selectedDenom : undefined;
+}
+
+function selectForced(
+ req: SelectPayCoinRequestNg,
+ candidateDenoms: AvailableDenom[],
+): SelResult | undefined {
+ const selectedDenom: SelResult = {};
+
+ const forcedSelection = req.forcedSelection;
+ checkLogicInvariant(!!forcedSelection);
+
+ for (const forcedCoin of forcedSelection.coins) {
+ let found = false;
+ for (const aci of candidateDenoms) {
+ if (aci.numAvailable <= 0) {
+ continue;
+ }
+ if (Amounts.cmp(aci.value, forcedCoin.value) === 0) {
+ aci.numAvailable--;
+ const avKey = makeAvailabilityKey(
+ aci.exchangeBaseUrl,
+ aci.denomPubHash,
+ aci.maxAge,
+ );
+ let sd = selectedDenom[avKey];
+ if (!sd) {
+ sd = {
+ contributions: [],
+ denomPubHash: aci.denomPubHash,
+ exchangeBaseUrl: aci.exchangeBaseUrl,
+ maxAge: aci.maxAge,
+ };
+ }
+ sd.contributions.push(Amounts.parseOrThrow(forcedCoin.value));
+ selectedDenom[avKey] = sd;
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ throw Error("can't find coin for forced coin selection");
+ }
+ }
+ return selectedDenom;
+}
+
+export function checkAccountRestriction(
+ paytoUri: string,
+ restrictions: AccountRestriction[],
+): { ok: boolean; hint?: string; hintI18n?: InternationalizedString } {
+ for (const myRestriction of restrictions) {
+ switch (myRestriction.type) {
+ case "deny":
+ return { ok: false };
+ case "regex":
+ const regex = new RegExp(myRestriction.payto_regex);
+ if (!regex.test(paytoUri)) {
+ return {
+ ok: false,
+ hint: myRestriction.human_hint,
+ hintI18n: myRestriction.human_hint_i18n,
+ };
+ }
+ }
+ }
+ return {
+ ok: true,
+ };
+}
+
+export interface SelectPayCoinRequestNg {
+ restrictExchanges: ExchangeRestrictionSpec | undefined;
+ restrictWireMethod: string;
+ contractTermsAmount: AmountJson;
+ depositFeeLimit: AmountJson;
+ prevPayCoins?: PreviousPayCoins;
+ requiredMinimumAge?: number;
+ forcedSelection?: ForcedCoinSel;
+
+ /**
+ * Deposit payto URI, in case we already know the account that
+ * will be deposited into.
+ *
+ * That is typically the case when the wallet does a deposit to
+ * return funds to the user's own bank account.
+ */
+ depositPaytoUri?: string;
+}
+
+export type AvailableDenom = DenominationInfo & {
+ maxAge: number;
+ numAvailable: number;
+};
+
+export function findMatchingWire(
+ wireMethod: string,
+ depositPaytoUri: string | undefined,
+ exchangeWireDetails: ExchangeWireDetails,
+): { wireFee: AmountJson } | undefined {
+ for (const acc of exchangeWireDetails.wireInfo.accounts) {
+ const pp = parsePaytoUri(acc.payto_uri);
+ checkLogicInvariant(!!pp);
+ if (pp.targetType !== wireMethod) {
+ continue;
+ }
+ const wireFeeStr = exchangeWireDetails.wireInfo.feesForType[
+ wireMethod
+ ]?.find((x) => {
+ return AbsoluteTime.isBetween(
+ AbsoluteTime.now(),
+ AbsoluteTime.fromProtocolTimestamp(x.startStamp),
+ AbsoluteTime.fromProtocolTimestamp(x.endStamp),
+ );
+ })?.wireFee;
+
+ if (!wireFeeStr) {
+ continue;
+ }
+
+ let debitAccountCheckOk = false;
+ if (depositPaytoUri) {
+ // FIXME: We should somehow propagate the hint here!
+ const checkResult = checkAccountRestriction(
+ depositPaytoUri,
+ acc.debit_restrictions,
+ );
+ if (checkResult.ok) {
+ debitAccountCheckOk = true;
+ }
+ } else {
+ debitAccountCheckOk = true;
+ }
+
+ if (!debitAccountCheckOk) {
+ continue;
+ }
+
+ return {
+ wireFee: Amounts.parseOrThrow(wireFeeStr),
+ };
+ }
+ return undefined;
+}
+
+function checkExchangeAccepted(
+ exchangeDetails: ExchangeWireDetails,
+ exchangeRestrictions: ExchangeRestrictionSpec | undefined,
+): boolean {
+ if (!exchangeRestrictions) {
+ return true;
+ }
+ let accepted = false;
+ for (const allowedExchange of exchangeRestrictions.exchanges) {
+ if (allowedExchange.exchangePub === exchangeDetails.masterPublicKey) {
+ accepted = true;
+ break;
+ }
+ }
+ for (const allowedAuditor of exchangeRestrictions.auditors) {
+ for (const providedAuditor of exchangeDetails.auditors) {
+ if (allowedAuditor.auditorPub === providedAuditor.auditor_pub) {
+ accepted = true;
+ break;
+ }
+ }
+ }
+ return accepted;
+}
+
+interface SelectPayCandidatesRequest {
+ instructedAmount: AmountJson;
+ restrictWireMethod: string | undefined;
+ depositPaytoUri?: string;
+ restrictExchanges: ExchangeRestrictionSpec | undefined;
+ requiredMinimumAge?: number;
+
+ /**
+ * If set to true, the coin selection will also use coins that are not
+ * materially available yet, but that are expected to become available
+ * as the output of a refresh operation.
+ */
+ includePendingCoins: boolean;
+}
+
+async function selectPayCandidates(
+ wex: WalletExecutionContext,
+ tx: WalletDbReadOnlyTransaction<
+ ["exchanges", "coinAvailability", "exchangeDetails", "denominations"]
+ >,
+ req: SelectPayCandidatesRequest,
+): Promise<[AvailableDenom[], Record<string, AmountJson>]> {
+ // FIXME: Use the existing helper (from balance.ts) to
+ // get acceptable exchanges.
+ logger.shouldLogTrace() &&
+ logger.trace(`selecting available coin candidates for ${j2s(req)}`);
+ const denoms: AvailableDenom[] = [];
+ const exchanges = await tx.exchanges.iter().toArray();
+ const wfPerExchange: Record<string, AmountJson> = {};
+ for (const exchange of exchanges) {
+ const exchangeDetails = await getExchangeWireDetailsInTx(
+ tx,
+ exchange.baseUrl,
+ );
+ // 1. exchange has same currency
+ if (exchangeDetails?.currency !== req.instructedAmount.currency) {
+ logger.shouldLogTrace() &&
+ logger.trace(`skipping ${exchange.baseUrl} due to currency mismatch`);
+ continue;
+ }
+
+ // 2. Exchange supports wire method (only for pay/deposit)
+ if (req.restrictWireMethod) {
+ const wire = findMatchingWire(
+ req.restrictWireMethod,
+ req.depositPaytoUri,
+ exchangeDetails,
+ );
+ if (!wire) {
+ if (logger.shouldLogTrace()) {
+ logger.trace(
+ `skipping ${exchange.baseUrl} due to missing wire info mismatch`,
+ );
+ }
+ continue;
+ }
+ wfPerExchange[exchange.baseUrl] = wire.wireFee;
+ }
+
+ // 3. exchange is trusted in the exchange list or auditor list
+ let accepted = checkExchangeAccepted(
+ exchangeDetails,
+ req.restrictExchanges,
+ );
+ if (!accepted) {
+ if (logger.shouldLogTrace()) {
+ logger.trace(`skipping ${exchange.baseUrl} due to unacceptability`);
+ }
+ continue;
+ }
+
+ // 4. filter coins restricted by age
+ let ageLower = 0;
+ let ageUpper = AgeRestriction.AGE_UNRESTRICTED;
+ if (req.requiredMinimumAge) {
+ ageLower = req.requiredMinimumAge;
+ }
+
+ const myExchangeCoins =
+ await tx.coinAvailability.indexes.byExchangeAgeAvailability.getAll(
+ GlobalIDB.KeyRange.bound(
+ [exchangeDetails.exchangeBaseUrl, ageLower, 1],
+ [exchangeDetails.exchangeBaseUrl, ageUpper, Number.MAX_SAFE_INTEGER],
+ ),
+ );
+
+ if (logger.shouldLogTrace()) {
+ logger.trace(
+ `exchange ${exchange.baseUrl} has ${myExchangeCoins.length} candidate records`,
+ );
+ }
+
+ let numUsable = 0;
+
+ // 5. save denoms with how many coins are available
+ // FIXME: Check that the individual denomination is audited!
+ // FIXME: Should we exclude denominations that are
+ // not spendable anymore?
+ for (const coinAvail of myExchangeCoins) {
+ const denom = await tx.denominations.get([
+ coinAvail.exchangeBaseUrl,
+ coinAvail.denomPubHash,
+ ]);
+ checkDbInvariant(!!denom);
+ if (denom.isRevoked) {
+ logger.trace("denom is revoked");
+ continue;
+ }
+ if (!denom.isOffered) {
+ logger.trace("denom is unoffered");
+ continue;
+ }
+ numUsable++;
+ let numAvailable = coinAvail.freshCoinCount ?? 0;
+ if (req.includePendingCoins) {
+ numAvailable += coinAvail.pendingRefreshOutputCount ?? 0;
+ }
+ denoms.push({
+ ...DenominationRecord.toDenomInfo(denom),
+ numAvailable,
+ maxAge: coinAvail.maxAge,
+ });
+ }
+
+ if (logger.shouldLogTrace()) {
+ logger.trace(
+ `exchange ${exchange.baseUrl} has ${numUsable} candidate records with usable denominations`,
+ );
+ }
+ }
+ // Sort by available amount (descending), deposit fee (ascending) and
+ // denomPub (ascending) if deposit fee is the same
+ // (to guarantee deterministic results)
+ denoms.sort(
+ (o1, o2) =>
+ -Amounts.cmp(o1.value, o2.value) ||
+ Amounts.cmp(o1.feeDeposit, o2.feeDeposit) ||
+ strcmp(o1.denomPubHash, o2.denomPubHash),
+ );
+ return [denoms, wfPerExchange];
+}
+
+export interface PeerCoinSelectionDetails {
+ exchangeBaseUrl: string;
+
+ /**
+ * Info of Coins that were selected.
+ */
+ coins: SelectedCoin[];
+
+ /**
+ * How much of the deposit fees is the customer paying?
+ */
+ depositFees: AmountJson;
+
+ maxExpirationDate: TalerProtocolTimestamp;
+}
+
+export interface ProspectivePeerCoinSelectionDetails {
+ exchangeBaseUrl: string;
+
+ prospectiveCoins: SelectedProspectiveCoin[];
+
+ /**
+ * How much of the deposit fees is the customer paying?
+ */
+ depositFees: AmountJson;
+
+ maxExpirationDate: TalerProtocolTimestamp;
+}
+
+export type SelectPeerCoinsResult =
+ | { type: "success"; result: PeerCoinSelectionDetails }
+ // Successful, but using coins that are not materially available yet.
+ | { type: "prospective"; result: ProspectivePeerCoinSelectionDetails }
+ | {
+ type: "failure";
+ insufficientBalanceDetails: PaymentInsufficientBalanceDetails;
+ };
+
+export interface PeerCoinSelectionRequest {
+ instructedAmount: AmountJson;
+
+ /**
+ * Instruct the coin selection to repair this coin
+ * selection instead of selecting completely new coins.
+ */
+ repair?: PreviousPayCoins;
+}
+
+export async function computeCoinSelMaxExpirationDate(
+ wex: WalletExecutionContext,
+ tx: WalletDbReadOnlyTransaction<["coins", "denominations"]>,
+ selectedDenom: SelResult,
+): Promise<TalerProtocolTimestamp> {
+ let minAutorefreshExecuteThreshold = TalerProtocolTimestamp.never();
+ for (const dph of Object.keys(selectedDenom)) {
+ const selInfo = selectedDenom[dph];
+ const denom = await getDenomInfo(
+ wex,
+ tx,
+ selInfo.exchangeBaseUrl,
+ selInfo.denomPubHash,
+ );
+ if (!denom) {
+ continue;
+ }
+ // Compute earliest time that a selected denom
+ // would have its coins auto-refreshed.
+ minAutorefreshExecuteThreshold = TalerProtocolTimestamp.min(
+ minAutorefreshExecuteThreshold,
+ AbsoluteTime.toProtocolTimestamp(
+ getAutoRefreshExecuteThreshold({
+ stampExpireDeposit: denom.stampExpireDeposit,
+ stampExpireWithdraw: denom.stampExpireWithdraw,
+ }),
+ ),
+ );
+ }
+ return minAutorefreshExecuteThreshold;
+}
+
+export function emptyTallyForPeerPayment(
+ instructedAmount: AmountJson,
+): CoinSelectionTally {
+ const currency = instructedAmount.currency;
+ const zero = Amounts.zeroOfCurrency(currency);
+ return {
+ amountPayRemaining: instructedAmount,
+ customerDepositFees: zero,
+ lastDepositFee: zero,
+ amountDepositFeeLimitRemaining: zero,
+ customerWireFees: zero,
+ wireFeeCoveredForExchange: new Set(),
+ };
+}
+
+function getGlobalFees(
+ wireDetails: ExchangeWireDetails,
+): ExchangeGlobalFees | undefined {
+ const now = AbsoluteTime.now();
+ for (let gf of wireDetails.globalFees) {
+ const isActive = AbsoluteTime.isBetween(
+ now,
+ AbsoluteTime.fromProtocolTimestamp(gf.startDate),
+ AbsoluteTime.fromProtocolTimestamp(gf.endDate),
+ );
+ if (!isActive) {
+ continue;
+ }
+ return gf;
+ }
+ return undefined;
+}
+
+async function internalSelectPeerCoins(
+ wex: WalletExecutionContext,
+ tx: WalletDbReadOnlyTransaction<
+ [
+ "exchanges",
+ "contractTerms",
+ "coins",
+ "coinAvailability",
+ "denominations",
+ "refreshGroups",
+ "exchangeDetails",
+ ]
+ >,
+ req: PeerCoinSelectionRequest,
+ exch: ExchangeWireDetails,
+ includePendingCoins: boolean,
+): Promise<
+ | { sel: SelResult; tally: CoinSelectionTally; resCoins: SelectedCoin[] }
+ | undefined
+> {
+ const candidatesRes = await selectPayCandidates(wex, tx, {
+ instructedAmount: req.instructedAmount,
+ restrictExchanges: {
+ auditors: [],
+ exchanges: [
+ {
+ exchangeBaseUrl: exch.exchangeBaseUrl,
+ exchangePub: exch.masterPublicKey,
+ },
+ ],
+ },
+ restrictWireMethod: undefined,
+ includePendingCoins,
+ });
+ const candidates = candidatesRes[0];
+ if (logger.shouldLogTrace()) {
+ logger.trace(`peer payment candidate coins: ${j2s(candidates)}`);
+ }
+ const tally = emptyTallyForPeerPayment(req.instructedAmount);
+ const resCoins: SelectedCoin[] = [];
+
+ await maybeRepairCoinSelection(wex, tx, req.repair ?? [], resCoins, tally, {
+ wireFeesPerExchange: {},
+ });
+
+ if (logger.shouldLogTrace()) {
+ logger.trace(`candidates: ${j2s(candidates)}`);
+ logger.trace(`instructedAmount: ${j2s(req.instructedAmount)}`);
+ logger.trace(`tally: ${j2s(tally)}`);
+ }
+
+ const selRes = selectGreedy(
+ {
+ wireFeesPerExchange: {},
+ },
+ candidates,
+ tally,
+ );
+ if (!selRes) {
+ return undefined;
+ }
+
+ return {
+ sel: selRes,
+ tally,
+ resCoins,
+ };
+}
+
+export async function selectPeerCoins(
+ wex: WalletExecutionContext,
+ 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");
+ }
+
+ return await wex.db.runReadWriteTx(
+ {
+ storeNames: [
+ "exchanges",
+ "contractTerms",
+ "coins",
+ "coinAvailability",
+ "denominations",
+ "refreshGroups",
+ "exchangeDetails",
+ ],
+ },
+ 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,
+ };
+ },
+ );
+}
diff --git a/packages/taler-wallet-core/src/common.ts b/packages/taler-wallet-core/src/common.ts
new file mode 100644
index 000000000..edaba5ba4
--- /dev/null
+++ b/packages/taler-wallet-core/src/common.ts
@@ -0,0 +1,823 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 GNUnet e.V.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ AbsoluteTime,
+ AmountJson,
+ Amounts,
+ AsyncFlag,
+ CoinRefreshRequest,
+ CoinStatus,
+ Duration,
+ ExchangeEntryState,
+ ExchangeEntryStatus,
+ ExchangeTosStatus,
+ ExchangeUpdateStatus,
+ Logger,
+ RefreshReason,
+ TalerErrorDetail,
+ TalerPreciseTimestamp,
+ TalerProtocolTimestamp,
+ TombstoneIdStr,
+ TransactionIdStr,
+ WalletNotification,
+ assertUnreachable,
+ checkDbInvariant,
+ checkLogicInvariant,
+ durationMul,
+} from "@gnu-taler/taler-util";
+import {
+ BackupProviderRecord,
+ CoinRecord,
+ DbPreciseTimestamp,
+ DepositGroupRecord,
+ ExchangeEntryDbRecordStatus,
+ ExchangeEntryDbUpdateStatus,
+ ExchangeEntryRecord,
+ PeerPullCreditRecord,
+ PeerPullPaymentIncomingRecord,
+ PeerPushDebitRecord,
+ PeerPushPaymentIncomingRecord,
+ PurchaseRecord,
+ RecoupGroupRecord,
+ RefreshGroupRecord,
+ RewardRecord,
+ WalletDbReadWriteTransaction,
+ WithdrawalGroupRecord,
+ timestampPreciseToDb,
+} from "./db.js";
+import { createRefreshGroup } from "./refresh.js";
+import { WalletExecutionContext, getDenomInfo } from "./wallet.js";
+
+const logger = new Logger("operations/common.ts");
+
+export interface CoinsSpendInfo {
+ coinPubs: string[];
+ contributions: AmountJson[];
+ refreshReason: RefreshReason;
+ /**
+ * Identifier for what the coin has been spent for.
+ */
+ allocationId: TransactionIdStr;
+}
+
+export async function makeCoinsVisible(
+ wex: WalletExecutionContext,
+ tx: WalletDbReadWriteTransaction<["coins", "coinAvailability"]>,
+ transactionId: string,
+): Promise<void> {
+ const coins =
+ await tx.coins.indexes.bySourceTransactionId.getAll(transactionId);
+ for (const coinRecord of coins) {
+ if (!coinRecord.visible) {
+ coinRecord.visible = 1;
+ await tx.coins.put(coinRecord);
+ const ageRestriction = coinRecord.maxAge;
+ const car = await tx.coinAvailability.get([
+ coinRecord.exchangeBaseUrl,
+ coinRecord.denomPubHash,
+ ageRestriction,
+ ]);
+ if (!car) {
+ logger.error("missing coin availability record");
+ continue;
+ }
+ const visCount = car.visibleCoinCount ?? 0;
+ car.visibleCoinCount = visCount + 1;
+ await tx.coinAvailability.put(car);
+ }
+ }
+}
+
+export async function makeCoinAvailable(
+ wex: WalletExecutionContext,
+ tx: WalletDbReadWriteTransaction<
+ ["coins", "coinAvailability", "denominations"]
+ >,
+ coinRecord: CoinRecord,
+): Promise<void> {
+ checkLogicInvariant(coinRecord.status === CoinStatus.Fresh);
+ const existingCoin = await tx.coins.get(coinRecord.coinPub);
+ if (existingCoin) {
+ return;
+ }
+ const denom = await tx.denominations.get([
+ coinRecord.exchangeBaseUrl,
+ coinRecord.denomPubHash,
+ ]);
+ checkDbInvariant(!!denom);
+ const ageRestriction = coinRecord.maxAge;
+ let car = await tx.coinAvailability.get([
+ coinRecord.exchangeBaseUrl,
+ coinRecord.denomPubHash,
+ ageRestriction,
+ ]);
+ if (!car) {
+ car = {
+ maxAge: ageRestriction,
+ value: denom.value,
+ currency: denom.currency,
+ denomPubHash: denom.denomPubHash,
+ exchangeBaseUrl: denom.exchangeBaseUrl,
+ freshCoinCount: 0,
+ visibleCoinCount: 0,
+ };
+ }
+ car.freshCoinCount++;
+ await tx.coins.put(coinRecord);
+ await tx.coinAvailability.put(car);
+}
+
+export async function spendCoins(
+ wex: WalletExecutionContext,
+ tx: WalletDbReadWriteTransaction<
+ [
+ "coins",
+ "coinAvailability",
+ "refreshGroups",
+ "refreshSessions",
+ "denominations",
+ ]
+ >,
+ csi: CoinsSpendInfo,
+): Promise<void> {
+ if (csi.coinPubs.length != csi.contributions.length) {
+ throw Error("assertion failed");
+ }
+ if (csi.coinPubs.length === 0) {
+ return;
+ }
+ let refreshCoinPubs: CoinRefreshRequest[] = [];
+ for (let i = 0; i < csi.coinPubs.length; i++) {
+ const coin = await tx.coins.get(csi.coinPubs[i]);
+ if (!coin) {
+ throw Error("coin allocated for payment doesn't exist anymore");
+ }
+ const denom = await getDenomInfo(
+ wex,
+ tx,
+ coin.exchangeBaseUrl,
+ coin.denomPubHash,
+ );
+ checkDbInvariant(!!denom);
+ const coinAvailability = await tx.coinAvailability.get([
+ coin.exchangeBaseUrl,
+ coin.denomPubHash,
+ coin.maxAge,
+ ]);
+ checkDbInvariant(!!coinAvailability);
+ 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");
+ }
+ refreshCoinPubs.push({
+ amount: Amounts.stringify(remaining.amount),
+ coinPub: coin.coinPub,
+ });
+ checkDbInvariant(!!coinAvailability);
+ if (coinAvailability.freshCoinCount === 0) {
+ throw Error(
+ `invalid coin count ${coinAvailability.freshCoinCount} in DB`,
+ );
+ }
+ coinAvailability.freshCoinCount--;
+ if (coin.visible) {
+ if (!coinAvailability.visibleCoinCount) {
+ logger.error("coin availability inconsistent");
+ } else {
+ coinAvailability.visibleCoinCount--;
+ }
+ }
+ await tx.coins.put(coin);
+ await tx.coinAvailability.put(coinAvailability);
+ }
+
+ await createRefreshGroup(
+ wex,
+ tx,
+ Amounts.currencyOf(csi.contributions[0]),
+ refreshCoinPubs,
+ csi.refreshReason,
+ csi.allocationId,
+ );
+}
+
+export enum TombstoneTag {
+ DeleteWithdrawalGroup = "delete-withdrawal-group",
+ DeleteReserve = "delete-reserve",
+ DeletePayment = "delete-payment",
+ DeleteReward = "delete-reward",
+ DeleteRefreshGroup = "delete-refresh-group",
+ DeleteDepositGroup = "delete-deposit-group",
+ DeleteRefund = "delete-refund",
+ DeletePeerPullDebit = "delete-peer-pull-debit",
+ DeletePeerPushDebit = "delete-peer-push-debit",
+ DeletePeerPullCredit = "delete-peer-pull-credit",
+ DeletePeerPushCredit = "delete-peer-push-credit",
+}
+
+export function getExchangeTosStatusFromRecord(
+ exchange: ExchangeEntryRecord,
+): ExchangeTosStatus {
+ if (!exchange.tosAcceptedEtag) {
+ return ExchangeTosStatus.Proposed;
+ }
+ if (exchange.tosAcceptedEtag == exchange.tosCurrentEtag) {
+ return ExchangeTosStatus.Accepted;
+ }
+ return ExchangeTosStatus.Proposed;
+}
+
+export function getExchangeUpdateStatusFromRecord(
+ r: ExchangeEntryRecord,
+): ExchangeUpdateStatus {
+ switch (r.updateStatus) {
+ case ExchangeEntryDbUpdateStatus.UnavailableUpdate:
+ return ExchangeUpdateStatus.UnavailableUpdate;
+ case ExchangeEntryDbUpdateStatus.Initial:
+ return ExchangeUpdateStatus.Initial;
+ case ExchangeEntryDbUpdateStatus.InitialUpdate:
+ return ExchangeUpdateStatus.InitialUpdate;
+ case ExchangeEntryDbUpdateStatus.Ready:
+ return ExchangeUpdateStatus.Ready;
+ case ExchangeEntryDbUpdateStatus.ReadyUpdate:
+ return ExchangeUpdateStatus.ReadyUpdate;
+ case ExchangeEntryDbUpdateStatus.Suspended:
+ return ExchangeUpdateStatus.Suspended;
+ default:
+ assertUnreachable(r.updateStatus);
+ }
+}
+
+export function getExchangeEntryStatusFromRecord(
+ r: ExchangeEntryRecord,
+): ExchangeEntryStatus {
+ switch (r.entryStatus) {
+ case ExchangeEntryDbRecordStatus.Ephemeral:
+ return ExchangeEntryStatus.Ephemeral;
+ case ExchangeEntryDbRecordStatus.Preset:
+ return ExchangeEntryStatus.Preset;
+ case ExchangeEntryDbRecordStatus.Used:
+ return ExchangeEntryStatus.Used;
+ default:
+ assertUnreachable(r.entryStatus);
+ }
+}
+
+/**
+ * Compute the state of an exchange entry from the DB
+ * record.
+ */
+export function getExchangeState(r: ExchangeEntryRecord): ExchangeEntryState {
+ return {
+ exchangeEntryStatus: getExchangeEntryStatusFromRecord(r),
+ exchangeUpdateStatus: getExchangeUpdateStatusFromRecord(r),
+ tosStatus: getExchangeTosStatusFromRecord(r),
+ };
+}
+
+export type ParsedTombstone =
+ | {
+ tag: TombstoneTag.DeleteWithdrawalGroup;
+ withdrawalGroupId: string;
+ }
+ | { tag: TombstoneTag.DeleteRefund; refundGroupId: string }
+ | { tag: TombstoneTag.DeleteReserve; reservePub: string }
+ | { tag: TombstoneTag.DeleteRefreshGroup; refreshGroupId: string }
+ | { tag: TombstoneTag.DeleteReward; walletTipId: string }
+ | { tag: TombstoneTag.DeletePayment; proposalId: string };
+
+export function constructTombstone(p: ParsedTombstone): TombstoneIdStr {
+ switch (p.tag) {
+ case TombstoneTag.DeleteWithdrawalGroup:
+ return `tmb:${p.tag}:${p.withdrawalGroupId}` as TombstoneIdStr;
+ case TombstoneTag.DeleteRefund:
+ return `tmb:${p.tag}:${p.refundGroupId}` as TombstoneIdStr;
+ case TombstoneTag.DeleteReserve:
+ return `tmb:${p.tag}:${p.reservePub}` as TombstoneIdStr;
+ case TombstoneTag.DeletePayment:
+ return `tmb:${p.tag}:${p.proposalId}` as TombstoneIdStr;
+ case TombstoneTag.DeleteRefreshGroup:
+ return `tmb:${p.tag}:${p.refreshGroupId}` as TombstoneIdStr;
+ case TombstoneTag.DeleteReward:
+ return `tmb:${p.tag}:${p.walletTipId}` as TombstoneIdStr;
+ default:
+ assertUnreachable(p);
+ }
+}
+
+/**
+ * Uniform interface for a particular wallet transaction.
+ */
+export interface TransactionManager {
+ get taskId(): TaskIdStr;
+ get transactionId(): TransactionIdStr;
+ fail(): Promise<void>;
+ abort(): Promise<void>;
+ suspend(): Promise<void>;
+ resume(): Promise<void>;
+ process(): Promise<TaskRunResult>;
+}
+
+export enum TaskRunResultType {
+ Finished = "finished",
+ Backoff = "backoff",
+ Progress = "progress",
+ Error = "error",
+ LongpollReturnedPending = "longpoll-returned-pending",
+ ScheduleLater = "schedule-later",
+}
+
+export type TaskRunResult =
+ | TaskRunFinishedResult
+ | TaskRunErrorResult
+ | TaskRunBackoffResult
+ | TaskRunProgressResult
+ | TaskRunLongpollReturnedPendingResult
+ | TaskRunScheduleLaterResult;
+
+export namespace TaskRunResult {
+ /**
+ * Task is finished and does not need to be processed again.
+ */
+ export function finished(): TaskRunResult {
+ return {
+ type: TaskRunResultType.Finished,
+ };
+ }
+ /**
+ * Task is waiting for something, should be invoked
+ * again with exponentiall back-off until some other
+ * result is returned.
+ */
+ export function backoff(): TaskRunResult {
+ return {
+ type: TaskRunResultType.Backoff,
+ };
+ }
+ /**
+ * Task made progress and should be processed again.
+ */
+ export function progress(): TaskRunResult {
+ return {
+ type: TaskRunResultType.Progress,
+ };
+ }
+ /**
+ * Run the task again at a fixed time in the future.
+ */
+ export function runAgainAt(runAt: AbsoluteTime): TaskRunResult {
+ return {
+ type: TaskRunResultType.ScheduleLater,
+ runAt,
+ };
+ }
+ /**
+ * Longpolling returned, but what we're waiting for
+ * is still pending on the other side.
+ */
+ export function longpollReturnedPending(): TaskRunLongpollReturnedPendingResult {
+ return {
+ type: TaskRunResultType.LongpollReturnedPending,
+ };
+ }
+}
+
+export interface TaskRunFinishedResult {
+ type: TaskRunResultType.Finished;
+}
+
+export interface TaskRunBackoffResult {
+ type: TaskRunResultType.Backoff;
+}
+
+export interface TaskRunProgressResult {
+ type: TaskRunResultType.Progress;
+}
+
+export interface TaskRunScheduleLaterResult {
+ type: TaskRunResultType.ScheduleLater;
+ runAt: AbsoluteTime;
+}
+
+export interface TaskRunLongpollReturnedPendingResult {
+ type: TaskRunResultType.LongpollReturnedPending;
+}
+
+export interface TaskRunErrorResult {
+ type: TaskRunResultType.Error;
+ errorDetail: TalerErrorDetail;
+}
+
+export interface DbRetryInfo {
+ firstTry: DbPreciseTimestamp;
+ nextRetry: DbPreciseTimestamp;
+ retryCounter: number;
+}
+
+export interface RetryPolicy {
+ readonly backoffDelta: Duration;
+ readonly backoffBase: number;
+ readonly maxTimeout: Duration;
+}
+
+const defaultRetryPolicy: RetryPolicy = {
+ backoffBase: 1.5,
+ backoffDelta: Duration.fromSpec({ seconds: 1 }),
+ maxTimeout: Duration.fromSpec({ minutes: 2 }),
+};
+
+function updateTimeout(
+ r: DbRetryInfo,
+ p: RetryPolicy = defaultRetryPolicy,
+): void {
+ const now = AbsoluteTime.now();
+ if (now.t_ms === "never") {
+ throw Error("assertion failed");
+ }
+ if (p.backoffDelta.d_ms === "forever") {
+ r.nextRetry = timestampPreciseToDb(
+ AbsoluteTime.toPreciseTimestamp(AbsoluteTime.never()),
+ );
+ return;
+ }
+
+ const nextIncrement =
+ p.backoffDelta.d_ms * Math.pow(p.backoffBase, r.retryCounter);
+
+ const t =
+ now.t_ms +
+ (p.maxTimeout.d_ms === "forever"
+ ? nextIncrement
+ : Math.min(p.maxTimeout.d_ms, nextIncrement));
+ r.nextRetry = timestampPreciseToDb(TalerPreciseTimestamp.fromMilliseconds(t));
+}
+
+export function computeDbBackoff(retryCounter: number): DbPreciseTimestamp {
+ const now = AbsoluteTime.now();
+ if (now.t_ms === "never") {
+ throw Error("assertion failed");
+ }
+ const p = defaultRetryPolicy;
+ if (p.backoffDelta.d_ms === "forever") {
+ throw Error("assertion failed");
+ }
+
+ const nextIncrement =
+ p.backoffDelta.d_ms * Math.pow(p.backoffBase, retryCounter);
+
+ const t =
+ now.t_ms +
+ (p.maxTimeout.d_ms === "forever"
+ ? nextIncrement
+ : Math.min(p.maxTimeout.d_ms, nextIncrement));
+ return timestampPreciseToDb(TalerPreciseTimestamp.fromMilliseconds(t));
+}
+
+export namespace DbRetryInfo {
+ export function reset(p: RetryPolicy = defaultRetryPolicy): DbRetryInfo {
+ const now = TalerPreciseTimestamp.now();
+ const info: DbRetryInfo = {
+ firstTry: timestampPreciseToDb(now),
+ nextRetry: timestampPreciseToDb(now),
+ retryCounter: 0,
+ };
+ updateTimeout(info, p);
+ return info;
+ }
+
+ export function increment(
+ r: DbRetryInfo | undefined,
+ p: RetryPolicy = defaultRetryPolicy,
+ ): DbRetryInfo {
+ if (!r) {
+ return reset(p);
+ }
+ const r2 = { ...r };
+ r2.retryCounter++;
+ updateTimeout(r2, p);
+ return r2;
+ }
+}
+
+/**
+ * Timestamp after which the wallet would do an auto-refresh.
+ */
+export function getAutoRefreshExecuteThreshold(d: {
+ stampExpireWithdraw: TalerProtocolTimestamp;
+ stampExpireDeposit: TalerProtocolTimestamp;
+}): AbsoluteTime {
+ const expireWithdraw = AbsoluteTime.fromProtocolTimestamp(
+ d.stampExpireWithdraw,
+ );
+ const expireDeposit = AbsoluteTime.fromProtocolTimestamp(
+ d.stampExpireDeposit,
+ );
+ const delta = AbsoluteTime.difference(expireWithdraw, expireDeposit);
+ const deltaDiv = durationMul(delta, 0.5);
+ return AbsoluteTime.addDuration(expireWithdraw, deltaDiv);
+}
+
+/**
+ * Parsed representation of task identifiers.
+ */
+export type ParsedTaskIdentifier =
+ | {
+ tag: PendingTaskType.Withdraw;
+ withdrawalGroupId: string;
+ }
+ | { tag: PendingTaskType.ExchangeUpdate; exchangeBaseUrl: string }
+ | { tag: PendingTaskType.Backup; backupProviderBaseUrl: string }
+ | { tag: PendingTaskType.Deposit; depositGroupId: string }
+ | { tag: PendingTaskType.PeerPullDebit; peerPullDebitId: string }
+ | { tag: PendingTaskType.PeerPullCredit; pursePub: string }
+ | { tag: PendingTaskType.PeerPushCredit; peerPushCreditId: string }
+ | { tag: PendingTaskType.PeerPushDebit; pursePub: string }
+ | { tag: PendingTaskType.Purchase; proposalId: string }
+ | { tag: PendingTaskType.Recoup; recoupGroupId: string }
+ | { tag: PendingTaskType.RewardPickup; walletRewardId: string }
+ | { tag: PendingTaskType.Refresh; refreshGroupId: string };
+
+export function parseTaskIdentifier(x: string): ParsedTaskIdentifier {
+ const task = x.split(":");
+
+ if (task.length < 2) {
+ throw Error("task id should have al least 2 parts separated by ':'");
+ }
+
+ const [type, ...rest] = task;
+ switch (type) {
+ case PendingTaskType.Backup:
+ return { tag: type, backupProviderBaseUrl: decodeURIComponent(rest[0]) };
+ case PendingTaskType.Deposit:
+ return { tag: type, depositGroupId: rest[0] };
+ case PendingTaskType.ExchangeUpdate:
+ return { tag: type, exchangeBaseUrl: decodeURIComponent(rest[0]) };
+ case PendingTaskType.PeerPullCredit:
+ return { tag: type, pursePub: rest[0] };
+ case PendingTaskType.PeerPullDebit:
+ return { tag: type, peerPullDebitId: rest[0] };
+ case PendingTaskType.PeerPushCredit:
+ return { tag: type, peerPushCreditId: rest[0] };
+ case PendingTaskType.PeerPushDebit:
+ return { tag: type, pursePub: rest[0] };
+ case PendingTaskType.Purchase:
+ return { tag: type, proposalId: rest[0] };
+ case PendingTaskType.Recoup:
+ return { tag: type, recoupGroupId: rest[0] };
+ case PendingTaskType.Refresh:
+ return { tag: type, refreshGroupId: rest[0] };
+ case PendingTaskType.RewardPickup:
+ return { tag: type, walletRewardId: rest[0] };
+ case PendingTaskType.Withdraw:
+ return { tag: type, withdrawalGroupId: rest[0] };
+ default:
+ throw Error("invalid task identifier");
+ }
+}
+
+export function constructTaskIdentifier(p: ParsedTaskIdentifier): TaskIdStr {
+ switch (p.tag) {
+ case PendingTaskType.Backup:
+ return `${p.tag}:${p.backupProviderBaseUrl}` as TaskIdStr;
+ case PendingTaskType.Deposit:
+ return `${p.tag}:${p.depositGroupId}` as TaskIdStr;
+ case PendingTaskType.ExchangeUpdate:
+ return `${p.tag}:${encodeURIComponent(p.exchangeBaseUrl)}` as TaskIdStr;
+ case PendingTaskType.PeerPullDebit:
+ return `${p.tag}:${p.peerPullDebitId}` as TaskIdStr;
+ case PendingTaskType.PeerPushCredit:
+ return `${p.tag}:${p.peerPushCreditId}` as TaskIdStr;
+ case PendingTaskType.PeerPullCredit:
+ return `${p.tag}:${p.pursePub}` as TaskIdStr;
+ case PendingTaskType.PeerPushDebit:
+ return `${p.tag}:${p.pursePub}` as TaskIdStr;
+ case PendingTaskType.Purchase:
+ return `${p.tag}:${p.proposalId}` as TaskIdStr;
+ case PendingTaskType.Recoup:
+ return `${p.tag}:${p.recoupGroupId}` as TaskIdStr;
+ case PendingTaskType.Refresh:
+ return `${p.tag}:${p.refreshGroupId}` as TaskIdStr;
+ case PendingTaskType.RewardPickup:
+ return `${p.tag}:${p.walletRewardId}` as TaskIdStr;
+ case PendingTaskType.Withdraw:
+ return `${p.tag}:${p.withdrawalGroupId}` as TaskIdStr;
+ default:
+ assertUnreachable(p);
+ }
+}
+
+export namespace TaskIdentifiers {
+ export function forWithdrawal(wg: WithdrawalGroupRecord): TaskIdStr {
+ return `${PendingTaskType.Withdraw}:${wg.withdrawalGroupId}` as TaskIdStr;
+ }
+ export function forExchangeUpdate(exch: ExchangeEntryRecord): TaskIdStr {
+ return `${PendingTaskType.ExchangeUpdate}:${encodeURIComponent(
+ exch.baseUrl,
+ )}` as TaskIdStr;
+ }
+ export function forExchangeUpdateFromUrl(exchBaseUrl: string): TaskIdStr {
+ return `${PendingTaskType.ExchangeUpdate}:${encodeURIComponent(
+ exchBaseUrl,
+ )}` as TaskIdStr;
+ }
+ export function forTipPickup(tipRecord: RewardRecord): TaskIdStr {
+ return `${PendingTaskType.RewardPickup}:${tipRecord.walletRewardId}` as TaskIdStr;
+ }
+ export function forRefresh(
+ refreshGroupRecord: RefreshGroupRecord,
+ ): TaskIdStr {
+ return `${PendingTaskType.Refresh}:${refreshGroupRecord.refreshGroupId}` as TaskIdStr;
+ }
+ export function forPay(purchaseRecord: PurchaseRecord): TaskIdStr {
+ return `${PendingTaskType.Purchase}:${purchaseRecord.proposalId}` as TaskIdStr;
+ }
+ export function forRecoup(recoupRecord: RecoupGroupRecord): TaskIdStr {
+ return `${PendingTaskType.Recoup}:${recoupRecord.recoupGroupId}` as TaskIdStr;
+ }
+ export function forDeposit(depositRecord: DepositGroupRecord): TaskIdStr {
+ return `${PendingTaskType.Deposit}:${depositRecord.depositGroupId}` as TaskIdStr;
+ }
+ export function forBackup(backupRecord: BackupProviderRecord): TaskIdStr {
+ return `${PendingTaskType.Backup}:${encodeURIComponent(
+ backupRecord.baseUrl,
+ )}` as TaskIdStr;
+ }
+ export function forPeerPushPaymentInitiation(
+ ppi: PeerPushDebitRecord,
+ ): TaskIdStr {
+ return `${PendingTaskType.PeerPushDebit}:${ppi.pursePub}` as TaskIdStr;
+ }
+ export function forPeerPullPaymentInitiation(
+ ppi: PeerPullCreditRecord,
+ ): TaskIdStr {
+ return `${PendingTaskType.PeerPullCredit}:${ppi.pursePub}` as TaskIdStr;
+ }
+ export function forPeerPullPaymentDebit(
+ ppi: PeerPullPaymentIncomingRecord,
+ ): TaskIdStr {
+ return `${PendingTaskType.PeerPullDebit}:${ppi.peerPullDebitId}` as TaskIdStr;
+ }
+ export function forPeerPushCredit(
+ ppi: PeerPushPaymentIncomingRecord,
+ ): TaskIdStr {
+ return `${PendingTaskType.PeerPushCredit}:${ppi.peerPushCreditId}` as TaskIdStr;
+ }
+}
+
+/**
+ * Result of a transaction transition.
+ */
+export enum TransitionResultType {
+ Transition = 1,
+ Stay = 2,
+ Delete = 3,
+}
+
+export type TransitionResult<R> =
+ | { type: TransitionResultType.Stay }
+ | { type: TransitionResultType.Transition; rec: R }
+ | { type: TransitionResultType.Delete };
+
+export const TransitionResult = {
+ stay<T>(): TransitionResult<T> {
+ return { type: TransitionResultType.Stay };
+ },
+ delete<T>(): TransitionResult<T> {
+ return { type: TransitionResultType.Delete };
+ },
+ transition<T>(rec: T): TransitionResult<T> {
+ return {
+ type: TransitionResultType.Transition,
+ rec,
+ };
+ },
+};
+
+/**
+ * Transaction context.
+ * Uniform interface to all transactions.
+ */
+export interface TransactionContext {
+ get taskId(): TaskIdStr | undefined;
+ get transactionId(): TransactionIdStr;
+ abortTransaction(): Promise<void>;
+ suspendTransaction(): Promise<void>;
+ resumeTransaction(): Promise<void>;
+ failTransaction(): Promise<void>;
+ deleteTransaction(): Promise<void>;
+}
+
+/**
+ * Type and schema definitions for pending tasks in the wallet.
+ *
+ * These are only used internally, and are not part of the stable public
+ * interface to the wallet.
+ */
+
+export enum PendingTaskType {
+ ExchangeUpdate = "exchange-update",
+ Purchase = "purchase",
+ Refresh = "refresh",
+ Recoup = "recoup",
+ RewardPickup = "reward-pickup",
+ Withdraw = "withdraw",
+ Deposit = "deposit",
+ Backup = "backup",
+ PeerPushDebit = "peer-push-debit",
+ PeerPullCredit = "peer-pull-credit",
+ PeerPushCredit = "peer-push-credit",
+ PeerPullDebit = "peer-pull-debit",
+}
+
+declare const __taskIdStr: unique symbol;
+export type TaskIdStr = string & { [__taskIdStr]: true };
+
+/**
+ * Wait until the wallet is in a particular state.
+ *
+ * Two functions must be provided:
+ * 1. checkState, which checks if the wallet is in the
+ * desired state.
+ * 2. filterNotification, which checks whether a notification
+ * might have lead to a state change.
+ */
+export async function genericWaitForState(
+ wex: WalletExecutionContext,
+ args: {
+ checkState: () => Promise<boolean>;
+ filterNotification: (notif: WalletNotification) => boolean;
+ },
+): Promise<void> {
+ await wex.taskScheduler.ensureRunning();
+
+ // FIXME: Clean up using the new JS "using" / Symbol.dispose syntax.
+ const flag = new AsyncFlag();
+ // Raise purchaseNotifFlag whenever we get a notification
+ // about our refresh.
+ const cancelNotif = wex.ws.addNotificationListener((notif) => {
+ if (args.filterNotification(notif)) {
+ flag.raise();
+ }
+ });
+ const unregisterOnCancelled = wex.cancellationToken.onCancelled(() => {
+ cancelNotif();
+ flag.raise();
+ });
+
+ try {
+ while (true) {
+ if (wex.cancellationToken.isCancelled) {
+ throw Error("cancelled");
+ }
+ if (await args.checkState()) {
+ return;
+ }
+ // Wait for the next transition
+ await flag.wait();
+ flag.reset();
+ }
+ } catch (e) {
+ unregisterOnCancelled();
+ cancelNotif();
+ }
+}
diff --git a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts
index d239270c8..0745d70c4 100644
--- a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts
+++ b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts
@@ -28,12 +28,14 @@ import {
AgeCommitmentProof,
AgeRestriction,
AmountJson,
- AmountLike,
Amounts,
AmountString,
+ amountToBuffer,
BlindedDenominationSignature,
bufferForUint32,
+ bufferForUint64,
buildSigPS,
+ canonicalJson,
CoinDepositPermission,
CoinEnvelope,
createHashContext,
@@ -42,7 +44,8 @@ import {
decryptContractForMerge,
DenomKeyType,
DepositInfo,
- ecdheGetPublic,
+ durationRoundedToBuffer,
+ ecdhGetPublic,
eddsaGetPublic,
EddsaPublicKeyString,
eddsaSign,
@@ -62,7 +65,7 @@ import {
hashTruncate32,
kdf,
kdfKw,
- keyExchangeEcdheEddsa,
+ keyExchangeEcdhEddsa,
Logger,
MakeSyncSignatureRequest,
PlanchetCreationRequest,
@@ -76,16 +79,15 @@ import {
rsaVerify,
setupTipPlanchet,
stringToBytes,
- TalerProtocolDuration,
TalerProtocolTimestamp,
TalerSignaturePurpose,
+ timestampRoundedToBuffer,
UnblindedSignature,
WireFee,
WithdrawalPlanchet,
} from "@gnu-taler/taler-util";
-import bigint from "big-integer";
// FIXME: Crypto should not use DB Types!
-import { DenominationRecord } from "../db.js";
+import { DenominationRecord, timestampProtocolFromDb } from "../db.js";
import {
CreateRecoupRefreshReqRequest,
CreateRecoupReqRequest,
@@ -101,9 +103,14 @@ import {
EncryptContractForDepositResponse,
EncryptContractRequest,
EncryptContractResponse,
- EncryptedContract,
+ SignCoinHistoryRequest,
+ SignCoinHistoryResponse,
+ SignDeletePurseRequest,
+ SignDeletePurseResponse,
SignPurseMergeRequest,
SignPurseMergeResponse,
+ SignRefundRequest,
+ SignRefundResponse,
SignReservePurseCreateRequest,
SignReservePurseCreateResponse,
SignTrackTransactionRequest,
@@ -159,7 +166,7 @@ export interface TalerCryptoInterface {
req: ContractTermsValidationRequest,
): Promise<ValidationResult>;
- createEddsaKeypair(req: unknown): Promise<EddsaKeypair>;
+ createEddsaKeypair(req: {}): Promise<EddsaKeypair>;
eddsaGetPublic(req: EddsaGetPublicRequest): Promise<EddsaGetPublicResponse>;
@@ -232,6 +239,16 @@ export interface TalerCryptoInterface {
signReservePurseCreate(
req: SignReservePurseCreateRequest,
): Promise<SignReservePurseCreateResponse>;
+
+ signRefund(req: SignRefundRequest): Promise<SignRefundResponse>;
+
+ signDeletePurse(
+ req: SignDeletePurseRequest,
+ ): Promise<SignDeletePurseResponse>;
+
+ signCoinHistoryRequest(
+ req: SignCoinHistoryRequest,
+ ): Promise<SignCoinHistoryResponse>;
}
/**
@@ -408,6 +425,19 @@ export const nullCrypto: TalerCryptoInterface = {
): Promise<SignReservePurseCreateResponse> {
throw new Error("Function not implemented.");
},
+ signRefund: function (req: SignRefundRequest): Promise<SignRefundResponse> {
+ throw new Error("Function not implemented.");
+ },
+ signDeletePurse: function (
+ req: SignDeletePurseRequest,
+ ): Promise<SignDeletePurseResponse> {
+ throw new Error("Function not implemented.");
+ },
+ signCoinHistoryRequest: function (
+ req: SignCoinHistoryRequest,
+ ): Promise<SignCoinHistoryResponse> {
+ throw new Error("Function not implemented.");
+ },
};
export type WithArg<X> = X extends (req: infer T) => infer R
@@ -445,17 +475,19 @@ export interface SignPurseCreationRequest {
minAge: number;
}
+export interface SpendCoinDetails {
+ coinPub: string;
+ coinPriv: string;
+ contribution: AmountString;
+ denomPubHash: string;
+ denomSig: UnblindedSignature;
+ ageCommitmentProof: AgeCommitmentProof | undefined;
+}
+
export interface SignPurseDepositsRequest {
pursePub: string;
exchangeBaseUrl: string;
- coins: {
- coinPub: string;
- coinPriv: string;
- contribution: AmountString;
- denomPubHash: string;
- denomSig: UnblindedSignature;
- ageCommitmentProof: AgeCommitmentProof | undefined;
- }[];
+ coins: SpendCoinDetails[];
}
export interface SignPurseDepositsResponse {
@@ -523,6 +555,9 @@ export interface WireAccountValidationRequest {
paytoUri: string;
sig: string;
masterPub: string;
+ conversionUrl?: string;
+ debitRestrictions?: any[];
+ creditRestrictions?: any[];
}
export interface EddsaKeypair {
@@ -664,7 +699,8 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
const salt = new Uint8Array(saltArrBuf);
const saltDataView = new DataView(saltArrBuf);
saltDataView.setUint32(0, req.coinNumber);
- const out = kdf(64, decodeCrock(req.secretSeed), salt, info);
+ const secretSeedDec = decodeCrock(req.secretSeed);
+ const out = kdf(64, secretSeedDec, salt, info);
const coinPriv = out.slice(0, 32);
const bks = out.slice(32, 64);
const coinPrivEnc = encodeCrock(coinPriv);
@@ -695,9 +731,10 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
if (denomPub.age_mask) {
const age = req.restrictAge || AgeRestriction.AGE_UNRESTRICTED;
logger.info(`creating age-restricted planchet (age ${age})`);
- maybeAcp = await AgeRestriction.restrictionCommit(
+ maybeAcp = await AgeRestriction.restrictionCommitSeeded(
denomPub.age_mask,
age,
+ stringToBytes(req.secretSeed),
);
maybeAgeCommitmentHash = AgeRestriction.hashCommitment(
maybeAcp.commitment,
@@ -799,7 +836,6 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
const p = buildSigPS(TalerSignaturePurpose.MERCHANT_TRACK_TRANSACTION)
.put(decodeCrock(req.contractTermsHash))
.put(decodeCrock(req.wireHash))
- .put(decodeCrock(req.merchantPub))
.put(decodeCrock(req.coinPub))
.build();
return { sig: encodeCrock(eddsaSign(p, decodeCrock(req.merchantPriv))) };
@@ -925,6 +961,7 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
const pub = decodeCrock(masterPub);
return { valid: eddsaVerify(p, sig, pub) };
},
+
/**
* Check if the signature of a denomination is valid.
*/
@@ -933,17 +970,25 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
req: DenominationValidationRequest,
): Promise<ValidationResult> {
const { masterPub, denom } = req;
- const value: AmountJson = {
- currency: denom.currency,
- fraction: denom.amountFrac,
- value: denom.amountVal,
- };
+ const value: AmountJson = Amounts.parseOrThrow(denom.value);
const p = buildSigPS(TalerSignaturePurpose.MASTER_DENOMINATION_KEY_VALIDITY)
.put(decodeCrock(masterPub))
- .put(timestampRoundedToBuffer(denom.stampStart))
- .put(timestampRoundedToBuffer(denom.stampExpireWithdraw))
- .put(timestampRoundedToBuffer(denom.stampExpireDeposit))
- .put(timestampRoundedToBuffer(denom.stampExpireLegal))
+ .put(timestampRoundedToBuffer(timestampProtocolFromDb(denom.stampStart)))
+ .put(
+ timestampRoundedToBuffer(
+ timestampProtocolFromDb(denom.stampExpireWithdraw),
+ ),
+ )
+ .put(
+ timestampRoundedToBuffer(
+ timestampProtocolFromDb(denom.stampExpireDeposit),
+ ),
+ )
+ .put(
+ timestampRoundedToBuffer(
+ timestampProtocolFromDb(denom.stampExpireLegal),
+ ),
+ )
.put(amountToBuffer(value))
.put(amountToBuffer(denom.fees.feeWithdraw))
.put(amountToBuffer(denom.fees.feeDeposit))
@@ -963,9 +1008,20 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
): Promise<ValidationResult> {
const { sig, masterPub, paytoUri } = req;
const paytoHash = hashTruncate32(stringToBytes(paytoUri + "\0"));
- const p = buildSigPS(TalerSignaturePurpose.MASTER_WIRE_DETAILS)
- .put(paytoHash)
- .build();
+ const pb = buildSigPS(TalerSignaturePurpose.MASTER_WIRE_DETAILS);
+ pb.put(paytoHash);
+ if (req.versionCurrent >= 15) {
+ let conversionUrlHash;
+ if (!req.conversionUrl) {
+ conversionUrlHash = new Uint8Array(64);
+ } else {
+ conversionUrlHash = hash(stringToBytes(req.conversionUrl + "\0"));
+ }
+ pb.put(conversionUrlHash);
+ pb.put(hash(stringToBytes(canonicalJson(req.debitRestrictions) + "\0")));
+ pb.put(hash(stringToBytes(canonicalJson(req.creditRestrictions) + "\0")));
+ }
+ const p = pb.build();
return { valid: eddsaVerify(p, decodeCrock(sig), decodeCrock(masterPub)) };
},
@@ -1078,7 +1134,7 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
depositInfo.ageCommitmentProof.commitment,
);
hAgeCommitment = decodeCrock(ach);
- if (depositInfo.requiredMinimumAge != null) {
+ if (depositInfo.requiredMinimumAge) {
minimumAgeSig = encodeCrock(
AgeRestriction.commitmentAttest(
depositInfo.ageCommitmentProof,
@@ -1090,6 +1146,8 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
// All zeros.
hAgeCommitment = new Uint8Array(32);
}
+ // FIXME: Actually allow passing user data here!
+ const walletDataHash = new Uint8Array(64);
let d: Uint8Array;
if (depositInfo.denomKeyType === DenomKeyType.Rsa) {
d = buildSigPS(TalerSignaturePurpose.WALLET_COIN_DEPOSIT)
@@ -1103,6 +1161,7 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
.put(amountToBuffer(depositInfo.spendAmount))
.put(amountToBuffer(depositInfo.feeDeposit))
.put(decodeCrock(depositInfo.merchantPub))
+ .put(walletDataHash)
.build();
} else {
throw Error("unsupported exchange protocol version");
@@ -1125,7 +1184,7 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
},
};
- if (depositInfo.requiredMinimumAge != null) {
+ if (depositInfo.requiredMinimumAge) {
// These are only required by the merchant
s.minimum_age_sig = minimumAgeSig;
s.age_commitment =
@@ -1355,7 +1414,7 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
): Promise<KeyExchangeResult> {
return {
h: encodeCrock(
- keyExchangeEcdheEddsa(
+ keyExchangeEcdhEddsa(
decodeCrock(req.ecdhePriv),
decodeCrock(req.eddsaPub),
),
@@ -1367,7 +1426,7 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
req: EcdheGetPublicRequest,
): Promise<EcdheGetPublicResponse> {
return {
- pub: encodeCrock(ecdheGetPublic(decodeCrock(req.priv))),
+ pub: encodeCrock(ecdhGetPublic(decodeCrock(req.priv))),
};
},
async setupRefreshTransferPub(
@@ -1409,15 +1468,12 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
const hExchangeBaseUrl = hash(stringToBytes(req.exchangeBaseUrl + "\0"));
const deposits: PurseDeposit[] = [];
for (const c of req.coins) {
- let haveAch: boolean;
let maybeAch: Uint8Array;
if (c.ageCommitmentProof) {
- haveAch = true;
maybeAch = decodeCrock(
AgeRestriction.hashCommitment(c.ageCommitmentProof.commitment),
);
} else {
- haveAch = false;
maybeAch = new Uint8Array(32);
}
const sigBlob = buildSigPS(TalerSignaturePurpose.WALLET_PURSE_DEPOSIT)
@@ -1450,25 +1506,24 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
tci: TalerCryptoInterfaceR,
req: EncryptContractRequest,
): Promise<EncryptContractResponse> {
- const contractKeyPair = await this.createEddsaKeypair(tci, {});
const enc = await encryptContractForMerge(
decodeCrock(req.pursePub),
- decodeCrock(contractKeyPair.priv),
+ decodeCrock(req.contractPriv),
decodeCrock(req.mergePriv),
req.contractTerms,
+ decodeCrock(req.nonce),
);
const sigBlob = buildSigPS(TalerSignaturePurpose.WALLET_PURSE_ECONTRACT)
.put(hash(enc))
- .put(decodeCrock(contractKeyPair.pub))
+ .put(decodeCrock(req.contractPub))
.build();
const sig = eddsaSign(sigBlob, decodeCrock(req.pursePriv));
return {
econtract: {
- contract_pub: contractKeyPair.pub,
+ contract_pub: req.contractPub,
econtract: encodeCrock(enc),
econtract_sig: encodeCrock(sig),
},
- contractPriv: contractKeyPair.priv,
};
},
async decryptContractForMerge(
@@ -1489,24 +1544,23 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
tci: TalerCryptoInterfaceR,
req: EncryptContractForDepositRequest,
): Promise<EncryptContractForDepositResponse> {
- const contractKeyPair = await this.createEddsaKeypair(tci, {});
const enc = await encryptContractForDeposit(
decodeCrock(req.pursePub),
- decodeCrock(contractKeyPair.priv),
+ decodeCrock(req.contractPriv),
req.contractTerms,
+ decodeCrock(req.nonce),
);
const sigBlob = buildSigPS(TalerSignaturePurpose.WALLET_PURSE_ECONTRACT)
.put(hash(enc))
- .put(decodeCrock(contractKeyPair.pub))
+ .put(decodeCrock(req.contractPub))
.build();
const sig = eddsaSign(sigBlob, decodeCrock(req.pursePriv));
return {
econtract: {
- contract_pub: contractKeyPair.pub,
+ contract_pub: req.contractPub,
econtract: encodeCrock(enc),
econtract_sig: encodeCrock(sig),
},
- contractPriv: contractKeyPair.priv,
};
},
async decryptContractForDeposit(
@@ -1626,66 +1680,58 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
purseSig: purseSigResp.sig,
};
},
+ async signRefund(
+ tci: TalerCryptoInterfaceR,
+ req: SignRefundRequest,
+ ): Promise<SignRefundResponse> {
+ const refundSigBlob = buildSigPS(TalerSignaturePurpose.MERCHANT_REFUND)
+ .put(decodeCrock(req.contractTermsHash))
+ .put(decodeCrock(req.coinPub))
+ .put(bufferForUint64(req.rtransactionId))
+ .put(amountToBuffer(req.refundAmount))
+ .build();
+ const refundSigResp = await tci.eddsaSign(tci, {
+ msg: encodeCrock(refundSigBlob),
+ priv: req.merchantPriv,
+ });
+ return {
+ sig: refundSigResp.sig,
+ };
+ },
+ async signDeletePurse(
+ tci: TalerCryptoInterfaceR,
+ req: SignDeletePurseRequest,
+ ): Promise<SignDeletePurseResponse> {
+ const deleteSigBlob = buildSigPS(
+ TalerSignaturePurpose.WALLET_PURSE_DELETE,
+ ).build();
+ const sigResp = await tci.eddsaSign(tci, {
+ msg: encodeCrock(deleteSigBlob),
+ priv: req.pursePriv,
+ });
+ return {
+ sig: sigResp.sig,
+ };
+ },
+ async signCoinHistoryRequest(
+ tci: TalerCryptoInterfaceR,
+ req: SignCoinHistoryRequest,
+ ): Promise<SignCoinHistoryResponse> {
+ const coinHistorySigBlob = buildSigPS(
+ TalerSignaturePurpose.WALLET_COIN_HISTORY,
+ )
+ .put(bufferForUint64(req.startOffset))
+ .build();
+ const sigResp = await tci.eddsaSign(tci, {
+ msg: encodeCrock(coinHistorySigBlob),
+ priv: req.coinPriv,
+ });
+ return {
+ sig: sigResp.sig,
+ };
+ },
};
-function amountToBuffer(amount: AmountLike): Uint8Array {
- const amountJ = Amounts.jsonifyAmount(amount);
- const buffer = new ArrayBuffer(8 + 4 + 12);
- const dvbuf = new DataView(buffer);
- const u8buf = new Uint8Array(buffer);
- const curr = stringToBytes(amountJ.currency);
- if (typeof dvbuf.setBigUint64 !== "undefined") {
- dvbuf.setBigUint64(0, BigInt(amountJ.value));
- } else {
- const arr = bigint(amountJ.value).toArray(2 ** 8).value;
- let offset = 8 - arr.length;
- for (let i = 0; i < arr.length; i++) {
- dvbuf.setUint8(offset++, arr[i]);
- }
- }
- dvbuf.setUint32(8, amountJ.fraction);
- u8buf.set(curr, 8 + 4);
-
- return u8buf;
-}
-
-function timestampRoundedToBuffer(ts: TalerProtocolTimestamp): Uint8Array {
- const b = new ArrayBuffer(8);
- const v = new DataView(b);
- // The buffer we sign over represents the timestamp in microseconds.
- if (typeof v.setBigUint64 !== "undefined") {
- const s = BigInt(ts.t_s) * BigInt(1000 * 1000);
- v.setBigUint64(0, s);
- } else {
- const s =
- ts.t_s === "never" ? bigint.zero : bigint(ts.t_s).multiply(1000 * 1000);
- const arr = s.toArray(2 ** 8).value;
- let offset = 8 - arr.length;
- for (let i = 0; i < arr.length; i++) {
- v.setUint8(offset++, arr[i]);
- }
- }
- return new Uint8Array(b);
-}
-
-function durationRoundedToBuffer(ts: TalerProtocolDuration): Uint8Array {
- const b = new ArrayBuffer(8);
- const v = new DataView(b);
- // The buffer we sign over represents the timestamp in microseconds.
- if (typeof v.setBigUint64 !== "undefined") {
- const s = BigInt(ts.d_us);
- v.setBigUint64(0, s);
- } else {
- const s = ts.d_us === "forever" ? bigint.zero : bigint(ts.d_us);
- const arr = s.toArray(2 ** 8).value;
- let offset = 8 - arr.length;
- for (let i = 0; i < arr.length; i++) {
- v.setUint8(offset++, arr[i]);
- }
- }
- return new Uint8Array(b);
-}
-
export interface EddsaSignRequest {
msg: string;
priv: string;
diff --git a/packages/taler-wallet-core/src/crypto/cryptoTypes.ts b/packages/taler-wallet-core/src/crypto/cryptoTypes.ts
index a083f453c..df25b87e4 100644
--- a/packages/taler-wallet-core/src/crypto/cryptoTypes.ts
+++ b/packages/taler-wallet-core/src/crypto/cryptoTypes.ts
@@ -176,30 +176,32 @@ export interface EncryptedContract {
export interface EncryptContractRequest {
contractTerms: any;
-
+ contractPriv: string;
+ contractPub: string;
pursePub: string;
pursePriv: string;
-
mergePriv: string;
+ nonce: string;
}
export interface EncryptContractResponse {
econtract: EncryptedContract;
-
- contractPriv: string;
}
export interface EncryptContractForDepositRequest {
contractTerms: any;
+ contractPriv: string;
+ contractPub: string;
+
pursePub: string;
pursePriv: string;
+
+ nonce: string;
}
export interface EncryptContractForDepositResponse {
econtract: EncryptedContract;
-
- contractPriv: string;
}
export interface DecryptContractRequest {
@@ -256,6 +258,37 @@ export interface SignPurseMergeResponse {
accountSig: string;
}
+export interface SignRefundRequest {
+ merchantPriv: string;
+ merchantPub: string;
+ contractTermsHash: string;
+ coinPub: string;
+ rtransactionId: number;
+ refundAmount: AmountString;
+}
+
+export interface SignRefundResponse {
+ sig: string;
+}
+
+export interface SignDeletePurseRequest {
+ pursePriv: string;
+}
+
+export interface SignDeletePurseResponse {
+ sig: EddsaSignatureString;
+}
+
+export interface SignCoinHistoryRequest {
+ coinPub: string;
+ coinPriv: string;
+ startOffset: number;
+}
+
+export interface SignCoinHistoryResponse {
+ sig: EddsaSignatureString;
+}
+
export interface SignReservePurseCreateRequest {
mergeTimestamp: TalerProtocolTimestamp;
diff --git a/packages/taler-wallet-core/src/crypto/workers/crypto-dispatcher.test.ts b/packages/taler-wallet-core/src/crypto/workers/crypto-dispatcher.test.ts
new file mode 100644
index 000000000..96e2ee735
--- /dev/null
+++ b/packages/taler-wallet-core/src/crypto/workers/crypto-dispatcher.test.ts
@@ -0,0 +1,128 @@
+/*
+ This file is part of GNU Taler
+ (C) 2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { AbsoluteTime, TalerErrorCode } from "@gnu-taler/taler-util";
+import test from "ava";
+import { CryptoDispatcher, CryptoWorkerFactory } from "./crypto-dispatcher.js";
+import {
+ CryptoWorker,
+ CryptoWorkerResponseMessage,
+} from "./cryptoWorkerInterface.js";
+
+export class MyCryptoWorker implements CryptoWorker {
+ /**
+ * Function to be called when we receive a message from the worker thread.
+ */
+ onmessage: undefined | ((m: any) => void) = undefined;
+
+ /**
+ * Function to be called when we receive an error from the worker thread.
+ */
+ onerror: undefined | ((m: any) => void) = undefined;
+
+ /**
+ * Add an event listener for either an "error" or "message" event.
+ */
+ addEventListener(event: "message" | "error", fn: (x: any) => void): void {
+ switch (event) {
+ case "message":
+ this.onmessage = fn;
+ break;
+ case "error":
+ this.onerror = fn;
+ break;
+ }
+ }
+
+ private dispatchMessage(msg: any): void {
+ if (this.onmessage) {
+ this.onmessage(msg);
+ }
+ }
+
+ /**
+ * Send a message to the worker thread.
+ */
+ postMessage(msg: any): void {
+ const handleRequest = async () => {
+ let responseMsg: CryptoWorkerResponseMessage;
+ if (msg.operation === "testSuccess") {
+ responseMsg = {
+ id: msg.id,
+ type: "success",
+ result: {
+ testResult: 42,
+ },
+ };
+ } else if (msg.operation === "testError") {
+ responseMsg = {
+ id: msg.id,
+ type: "error",
+ error: {
+ code: TalerErrorCode.ANASTASIS_EMAIL_INVALID,
+ when: AbsoluteTime.now(),
+ hint: "bla",
+ },
+ };
+ } else if (msg.operation === "testTimeout") {
+ // Don't respond
+ return;
+ }
+ try {
+ setTimeout(() => this.dispatchMessage(responseMsg), 0);
+ } catch (e) {
+ console.error("got error during dispatch", e);
+ }
+ };
+ handleRequest().catch((e) => {
+ console.error("Error while handling crypto request:", e);
+ });
+ }
+
+ /**
+ * Forcibly terminate the worker thread.
+ */
+ terminate(): void {
+ // This is a no-op.
+ }
+}
+
+export class MyCryptoWorkerFactory implements CryptoWorkerFactory {
+ startWorker(): CryptoWorker {
+ return new MyCryptoWorker();
+ }
+
+ getConcurrency(): number {
+ return 1;
+ }
+}
+
+test("continues after error", async (t) => {
+ const cryptoDisp = new CryptoDispatcher(new MyCryptoWorkerFactory());
+ const resp1 = await cryptoDisp.doRpc("testSuccess", 0, {});
+ t.assert((resp1 as any).testResult === 42);
+ const exc = await t.throwsAsync(async () => {
+ const resp2 = await cryptoDisp.doRpc("testError", 0, {});
+ });
+
+ // Check that it still works after one error.
+ const resp2 = await cryptoDisp.doRpc("testSuccess", 0, {});
+ t.assert((resp2 as any).testResult === 42);
+
+ // Check that it still works after timeout.
+ const resp3 = await cryptoDisp.doRpc("testSuccess", 0, {});
+ t.assert((resp3 as any).testResult === 42);
+});
diff --git a/packages/taler-wallet-core/src/crypto/workers/cryptoDispatcher.ts b/packages/taler-wallet-core/src/crypto/workers/crypto-dispatcher.ts
index 1c0d509e6..f86163723 100644
--- a/packages/taler-wallet-core/src/crypto/workers/cryptoDispatcher.ts
+++ b/packages/taler-wallet-core/src/crypto/workers/crypto-dispatcher.ts
@@ -23,10 +23,16 @@
/**
* Imports.
*/
-import { j2s, Logger, TalerErrorCode } from "@gnu-taler/taler-util";
-import { TalerError } from "../../errors.js";
-import { openPromise } from "../../util/promiseUtils.js";
-import { timer, performanceNow, TimerHandle } from "../../util/timer.js";
+import {
+ j2s,
+ Logger,
+ openPromise,
+ performanceNow,
+ TalerError,
+ TalerErrorCode,
+ timer,
+ TimerHandle,
+} from "@gnu-taler/taler-util";
import { nullCrypto, TalerCryptoInterface } from "../cryptoImplementation.js";
import { CryptoWorker } from "./cryptoWorkerInterface.js";
@@ -203,13 +209,7 @@ export class CryptoDispatcher {
ws.idleTimeoutHandle.unref();
}
- handleWorkerError(ws: WorkerInfo, e: any): void {
- if (ws.currentWorkItem) {
- logger.error(`error in worker during ${ws.currentWorkItem.operation}`, e);
- } else {
- logger.error("error in worker", e);
- }
- logger.error(e.message);
+ private resetWorker(ws: WorkerInfo, e: any): void {
try {
if (ws.w) {
ws.w.terminate();
@@ -227,6 +227,16 @@ export class CryptoDispatcher {
this.findWork(ws);
}
+ handleWorkerError(ws: WorkerInfo, e: any): void {
+ if (ws.currentWorkItem) {
+ logger.error(`error in worker during ${ws.currentWorkItem.operation}`, e);
+ } else {
+ logger.error("error in worker", e);
+ }
+ logger.error(e.message);
+ this.resetWorker(ws, e);
+ }
+
private findWork(ws: WorkerInfo): void {
// try to find more work for this worker
for (let i = 0; i < NUM_PRIO; i++) {
@@ -304,11 +314,7 @@ export class CryptoDispatcher {
}
}
- private doRpc<T>(
- operation: string,
- priority: number,
- req: unknown,
- ): Promise<T> {
+ doRpc<T>(operation: string, priority: number, req: unknown): Promise<T> {
if (this.stopped) {
throw new CryptoApiStoppedError();
}
@@ -355,30 +361,24 @@ export class CryptoDispatcher {
// (The worker child process won't keep us alive either, because we un-ref
// it to make sure it doesn't keep us alive if there is no work.)
return new Promise<T>((resolve, reject) => {
- let timedOut = false;
- const timeout = timer.after(5000, () => {
- logger.warn(`crypto RPC call ('${operation}') timed out`);
- timedOut = true;
- reject(new Error(`crypto RPC call ('${operation}') timed out`));
- if (workItem.state === WorkItemState.Running) {
- workItem.state = WorkItemState.Finished;
- this.numBusy--;
- }
- });
+ let timeoutHandle: TimerHandle | undefined = undefined;
+ const timeoutMs = 5000;
+ const onTimeout = () => {
+ // FIXME: Maybe destroy and re-init worker if request is in processing
+ // state and really taking too long?
+ logger.warn(
+ `crypto RPC call ('${operation}') has been queued for a long time`,
+ );
+ timeoutHandle = timer.after(timeoutMs, onTimeout);
+ };
myProm.promise
.then((x) => {
- if (timedOut) {
- return;
- }
- timeout.clear();
+ timeoutHandle?.clear();
resolve(x);
})
.catch((x) => {
logger.info(`crypto RPC call ${operation} threw`);
- if (timedOut) {
- return;
- }
- timeout.clear();
+ timeoutHandle?.clear();
reject(x);
});
});
diff --git a/packages/taler-wallet-core/src/crypto/workers/nodeThreadWorker.ts b/packages/taler-wallet-core/src/crypto/workers/nodeThreadWorker.ts
index f255e3cfd..eaa0108bb 100644
--- a/packages/taler-wallet-core/src/crypto/workers/nodeThreadWorker.ts
+++ b/packages/taler-wallet-core/src/crypto/workers/nodeThreadWorker.ts
@@ -21,13 +21,15 @@ import { Logger } from "@gnu-taler/taler-util";
import os from "os";
import url from "url";
import { nativeCryptoR } from "../cryptoImplementation.js";
-import { CryptoWorkerFactory } from "./cryptoDispatcher.js";
+import { CryptoWorkerFactory } from "./crypto-dispatcher.js";
import { CryptoWorker } from "./cryptoWorkerInterface.js";
import { processRequestWithImpl } from "./worker-common.js";
const logger = new Logger("nodeThreadWorker.ts");
-const f = url.fileURLToPath(import.meta.url);
+const f = import.meta.url
+ ? url.fileURLToPath(import.meta.url)
+ : "__not_available__";
const workerCode = `
// Try loading the glue library for embedded
@@ -149,7 +151,7 @@ class NodeThreadCryptoWorker implements CryptoWorker {
this.onmessage(v);
}
});
- this.nodeWorker.unref();
+ //this.nodeWorker.unref();
}
/**
diff --git a/packages/taler-wallet-core/src/crypto/workers/rpcClient.ts b/packages/taler-wallet-core/src/crypto/workers/rpcClient.ts
deleted file mode 100644
index 21d88fffa..000000000
--- a/packages/taler-wallet-core/src/crypto/workers/rpcClient.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- * Imports.
- */
-import { Logger } from "@gnu-taler/taler-util";
-import child_process from "child_process";
-import type internal from "stream";
-import { OpenedPromise, openPromise } from "../../util/promiseUtils.js";
-
-const logger = new Logger("synchronousWorkerFactory.ts");
-
-/**
- * Client for the crypto helper process (taler-crypto-worker from exchange.git).
- */
-export class CryptoRpcClient {
- proc: child_process.ChildProcessByStdio<
- internal.Writable,
- internal.Readable,
- null
- >;
- requests: Array<{
- p: OpenedPromise<any>;
- req: any;
- }> = [];
-
- constructor() {
- const stdoutChunks: Buffer[] = [];
- this.proc = child_process.spawn("taler-crypto-worker", {
- //stdio: ["pipe", "pipe", "inherit"],
- stdio: ["pipe", "pipe", "inherit"],
- detached: true,
- });
- this.proc.on("close", (): void => {
- logger.error("child process exited");
- });
- (this.proc.stdout as any).unref();
- (this.proc.stdin as any).unref();
- this.proc.unref();
-
- this.proc.stdout.on("data", (x) => {
- if (x instanceof Buffer) {
- const nlIndex = x.indexOf("\n");
- if (nlIndex >= 0) {
- const before = x.slice(0, nlIndex);
- const after = x.slice(nlIndex + 1);
- stdoutChunks.push(after);
- const str = Buffer.concat([...stdoutChunks, before]).toString(
- "utf-8",
- );
- const req = this.requests.shift();
- if (!req) {
- throw Error("request was undefined");
- }
- if (this.requests.length === 0) {
- this.proc.unref();
- }
- //logger.info(`got response: ${str}`);
- req.p.resolve(JSON.parse(str));
- } else {
- stdoutChunks.push(x);
- }
- } else {
- throw Error(`unexpected data chunk type (${typeof x})`);
- }
- });
- }
-
- async queueRequest(req: any): Promise<any> {
- const p = openPromise<any>();
- if (this.requests.length === 0) {
- this.proc.ref();
- }
- this.requests.push({ req, p });
- this.proc.stdin.write(`${JSON.stringify(req)}\n`);
- return p.promise;
- }
-}
diff --git a/packages/taler-wallet-core/src/crypto/workers/synchronousWorkerFactoryPlain.ts b/packages/taler-wallet-core/src/crypto/workers/synchronousWorkerFactoryPlain.ts
index d0c8e4b3a..66381bc0e 100644
--- a/packages/taler-wallet-core/src/crypto/workers/synchronousWorkerFactoryPlain.ts
+++ b/packages/taler-wallet-core/src/crypto/workers/synchronousWorkerFactoryPlain.ts
@@ -17,7 +17,7 @@
/**
* Imports.
*/
-import { CryptoWorkerFactory } from "./cryptoDispatcher.js";
+import { CryptoWorkerFactory } from "./crypto-dispatcher.js";
import { CryptoWorker } from "./cryptoWorkerInterface.js";
import { SynchronousCryptoWorkerPlain } from "./synchronousWorkerPlain.js";
diff --git a/packages/taler-wallet-core/src/crypto/workers/synchronousWorkerNode.ts b/packages/taler-wallet-core/src/crypto/workers/synchronousWorkerNode.ts
deleted file mode 100644
index b2653158c..000000000
--- a/packages/taler-wallet-core/src/crypto/workers/synchronousWorkerNode.ts
+++ /dev/null
@@ -1,174 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2019 GNUnet e.V.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-import { j2s, Logger } from "@gnu-taler/taler-util";
-import {
- nativeCryptoR,
- TalerCryptoInterfaceR,
-} from "../cryptoImplementation.js";
-import { CryptoWorker } from "./cryptoWorkerInterface.js";
-import { CryptoRpcClient } from "./rpcClient.js";
-import { processRequestWithImpl } from "./worker-common.js";
-
-const logger = new Logger("synchronousWorker.ts");
-
-/**
- * Worker implementation that uses node subprocesses.
- *
- * The node crypto worker can also use IPC to offload cryptographic
- * operations to a helper process (usually written in C / part of taler-exchange).
- */
-export class SynchronousCryptoWorkerNode implements CryptoWorker {
- /**
- * Function to be called when we receive a message from the worker thread.
- */
- onmessage: undefined | ((m: any) => void);
-
- /**
- * Function to be called when we receive an error from the worker thread.
- */
- onerror: undefined | ((m: any) => void);
-
- cryptoImplR: TalerCryptoInterfaceR;
-
- rpcClient: CryptoRpcClient | undefined;
-
- constructor() {
- this.onerror = undefined;
- this.onmessage = undefined;
-
- this.cryptoImplR = { ...nativeCryptoR };
-
- if (process.env["TALER_WALLET_PRIMITIVE_WORKER"]) {
- logger.info("using RPC for some crypto operations");
- const rpc = (this.rpcClient = new CryptoRpcClient());
- this.cryptoImplR.eddsaSign = async (_, req) => {
- return await rpc.queueRequest({
- op: "eddsa_sign",
- args: {
- msg: req.msg,
- priv: req.priv,
- },
- });
- };
- this.cryptoImplR.setupRefreshPlanchet = async (_, req) => {
- const res = await rpc.queueRequest({
- op: "setup_refresh_planchet",
- args: {
- coin_index: req.coinNumber,
- transfer_secret: req.transferSecret,
- },
- });
- return {
- bks: res.blinding_key,
- coinPriv: res.coin_priv,
- coinPub: res.coin_pub,
- };
- };
- this.cryptoImplR.rsaBlind = async (_, req) => {
- const res = await rpc.queueRequest({
- op: "rsa_blind",
- args: {
- bks: req.bks,
- hm: req.hm,
- pub: req.pub,
- },
- });
- return {
- blinded: res.blinded,
- };
- };
- this.cryptoImplR.keyExchangeEcdheEddsa = async (_, req) => {
- const res = await rpc.queueRequest({
- op: "kx_ecdhe_eddsa",
- args: {
- ecdhe_priv: req.ecdhePriv,
- eddsa_pub: req.eddsaPub,
- },
- });
- return {
- h: res.h,
- };
- };
- this.cryptoImplR.eddsaGetPublic = async (_, req) => {
- const res = await rpc.queueRequest({
- op: "eddsa_get_public",
- args: {
- eddsa_priv: req.priv,
- },
- });
- return {
- pub: res.eddsa_pub,
- };
- };
- this.cryptoImplR.ecdheGetPublic = async (_, req) => {
- const res = await rpc.queueRequest({
- op: "ecdhe_get_public",
- args: {
- ecdhe_priv: req.priv,
- },
- });
- return {
- pub: res.ecdhe_pub,
- };
- };
- }
- }
-
- /**
- * Add an event listener for either an "error" or "message" event.
- */
- addEventListener(event: "message" | "error", fn: (x: any) => void): void {
- switch (event) {
- case "message":
- this.onmessage = fn;
- break;
- case "error":
- this.onerror = fn;
- break;
- }
- }
-
- private dispatchMessage(msg: any): void {
- if (this.onmessage) {
- this.onmessage(msg);
- }
- }
-
- /**
- * Send a message to the worker thread.
- */
- postMessage(msg: any): void {
- const handleRequest = async () => {
- const responseMsg = await processRequestWithImpl(msg, this.cryptoImplR);
- try {
- setTimeout(() => this.dispatchMessage(responseMsg), 0);
- } catch (e) {
- logger.error("got error during dispatch", e);
- }
- };
- handleRequest().catch((e) => {
- logger.error("Error while handling crypto request:", e);
- });
- }
-
- /**
- * Forcibly terminate the worker thread.
- */
- terminate(): void {
- // This is a no-op.
- }
-}
diff --git a/packages/taler-wallet-core/src/crypto/workers/synchronousWorkerPlain.ts b/packages/taler-wallet-core/src/crypto/workers/synchronousWorkerPlain.ts
index 058896828..c80f2f58f 100644
--- a/packages/taler-wallet-core/src/crypto/workers/synchronousWorkerPlain.ts
+++ b/packages/taler-wallet-core/src/crypto/workers/synchronousWorkerPlain.ts
@@ -17,7 +17,7 @@
/**
* Imports.
*/
-import { Logger } from "@gnu-taler/taler-util";
+import { j2s, Logger } from "@gnu-taler/taler-util";
import {
nativeCryptoR,
TalerCryptoInterfaceR,
@@ -84,6 +84,8 @@ export class SynchronousCryptoWorkerPlain implements CryptoWorker {
};
handleRequest().catch((e) => {
logger.error("Error while handling crypto request:", e);
+ logger.error("Stack:", e.stack);
+ logger.error(`request was ${j2s(msg)}`);
});
}
diff --git a/packages/taler-wallet-core/src/crypto/workers/worker-common.ts b/packages/taler-wallet-core/src/crypto/workers/worker-common.ts
index 459033526..63147ce92 100644
--- a/packages/taler-wallet-core/src/crypto/workers/worker-common.ts
+++ b/packages/taler-wallet-core/src/crypto/workers/worker-common.ts
@@ -17,8 +17,16 @@
/**
* Imports.
*/
-import { j2s, Logger, TalerErrorCode } from "@gnu-taler/taler-util";
-import { getErrorDetailFromException, makeErrorDetail } from "../../errors.js";
+import {
+ j2s,
+ Logger,
+ stringifyError as safeStringifyError,
+ TalerErrorCode,
+} from "@gnu-taler/taler-util";
+import {
+ getErrorDetailFromException,
+ makeErrorDetail,
+} from "@gnu-taler/taler-util";
import { TalerCryptoInterfaceR } from "../cryptoImplementation.js";
import {
CryptoWorkerRequestMessage,
@@ -88,7 +96,7 @@ export async function processRequestWithImpl(
const result = await (impl as any)[operation](impl, reqMsg.req);
responseMsg = { type: "success", result, id };
} catch (e: any) {
- logger.error(`error during operation: ${e.stack ?? e.toString()}`);
+ logger.error(`error during operation: ${safeStringifyError(e)}`);
responseMsg = {
type: "error",
error: getErrorDetailFromException(e),
diff --git a/packages/taler-wallet-core/src/db-utils.ts b/packages/taler-wallet-core/src/db-utils.ts
deleted file mode 100644
index fe39a0fda..000000000
--- a/packages/taler-wallet-core/src/db-utils.ts
+++ /dev/null
@@ -1,236 +0,0 @@
-/*
- 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 { IDBDatabase, IDBFactory, IDBTransaction } from "@gnu-taler/idb-bridge";
-import { Logger } from "@gnu-taler/taler-util";
-import {
- CURRENT_DB_CONFIG_KEY,
- TALER_DB_NAME,
- TALER_META_DB_NAME,
- walletMetadataStore,
- WalletStoresV1,
- WALLET_DB_MINOR_VERSION,
-} from "./db.js";
-import {
- DbAccess,
- IndexDescriptor,
- openDatabase,
- StoreDescriptor,
- StoreWithIndexes,
-} from "./util/query.js";
-
-const logger = new Logger("db-utils.ts");
-
-function upgradeFromStoreMap(
- storeMap: any,
- db: IDBDatabase,
- oldVersion: number,
- newVersion: number,
- upgradeTransaction: IDBTransaction,
-): void {
- if (oldVersion === 0) {
- for (const n in storeMap) {
- const swi: StoreWithIndexes<
- any,
- StoreDescriptor<unknown>,
- any
- > = storeMap[n];
- const storeDesc: StoreDescriptor<unknown> = swi.store;
- const s = db.createObjectStore(swi.storeName, {
- autoIncrement: storeDesc.autoIncrement,
- keyPath: storeDesc.keyPath,
- });
- for (const indexName in swi.indexMap as any) {
- const indexDesc: IndexDescriptor = swi.indexMap[indexName];
- s.createIndex(indexDesc.name, indexDesc.keyPath, {
- multiEntry: indexDesc.multiEntry,
- unique: indexDesc.unique,
- });
- }
- }
- return;
- }
- if (oldVersion === newVersion) {
- return;
- }
- logger.info(`upgrading database from ${oldVersion} to ${newVersion}`);
- for (const n in storeMap) {
- const swi: StoreWithIndexes<any, StoreDescriptor<unknown>, any> = storeMap[
- n
- ];
- const storeDesc: StoreDescriptor<unknown> = swi.store;
- const storeAddedVersion = storeDesc.versionAdded ?? 0;
- if (storeAddedVersion <= oldVersion) {
- continue;
- }
- const s = db.createObjectStore(swi.storeName, {
- autoIncrement: storeDesc.autoIncrement,
- keyPath: storeDesc.keyPath,
- });
- for (const indexName in swi.indexMap as any) {
- const indexDesc: IndexDescriptor = swi.indexMap[indexName];
- const indexAddedVersion = indexDesc.versionAdded ?? 0;
- if (indexAddedVersion <= oldVersion) {
- continue;
- }
- s.createIndex(indexDesc.name, indexDesc.keyPath, {
- multiEntry: indexDesc.multiEntry,
- unique: indexDesc.unique,
- });
- }
- }
-}
-
-function promiseFromTransaction(transaction: IDBTransaction): Promise<void> {
- return new Promise<void>((resolve, reject) => {
- transaction.oncomplete = () => {
- resolve();
- };
- transaction.onerror = () => {
- reject();
- };
- });
-}
-
-/**
- * Purge all data in the given database.
- */
-export function clearDatabase(db: IDBDatabase): Promise<void> {
- // db.objectStoreNames is a DOMStringList, so we need to convert
- let stores: string[] = [];
- for (let i = 0; i < db.objectStoreNames.length; i++) {
- stores.push(db.objectStoreNames[i]);
- }
- const tx = db.transaction(stores, "readwrite");
- for (const store of stores) {
- tx.objectStore(store).clear();
- }
- return promiseFromTransaction(tx);
-}
-
-function onTalerDbUpgradeNeeded(
- db: IDBDatabase,
- oldVersion: number,
- newVersion: number,
- upgradeTransaction: IDBTransaction,
-) {
- upgradeFromStoreMap(
- WalletStoresV1,
- db,
- oldVersion,
- newVersion,
- upgradeTransaction,
- );
-}
-
-function onMetaDbUpgradeNeeded(
- db: IDBDatabase,
- oldVersion: number,
- newVersion: number,
- upgradeTransaction: IDBTransaction,
-) {
- upgradeFromStoreMap(
- walletMetadataStore,
- db,
- oldVersion,
- newVersion,
- upgradeTransaction,
- );
-}
-
-/**
- * Return a promise that resolves
- * to the taler wallet db.
- */
-export async function openTalerDatabase(
- idbFactory: IDBFactory,
- onVersionChange: () => void,
-): Promise<DbAccess<typeof WalletStoresV1>> {
- const metaDbHandle = await openDatabase(
- idbFactory,
- TALER_META_DB_NAME,
- 1,
- () => {},
- onMetaDbUpgradeNeeded,
- );
-
- const metaDb = new DbAccess(metaDbHandle, walletMetadataStore);
- let currentMainVersion: string | undefined;
- await metaDb
- .mktx((stores) => [stores.metaConfig])
- .runReadWrite(async (tx) => {
- const dbVersionRecord = await tx.metaConfig.get(CURRENT_DB_CONFIG_KEY);
- if (!dbVersionRecord) {
- currentMainVersion = TALER_DB_NAME;
- await tx.metaConfig.put({
- key: CURRENT_DB_CONFIG_KEY,
- value: TALER_DB_NAME,
- });
- } else {
- currentMainVersion = dbVersionRecord.value;
- }
- });
-
- if (currentMainVersion !== TALER_DB_NAME) {
- switch (currentMainVersion) {
- case "taler-wallet-main-v2":
- case "taler-wallet-main-v3":
- case "taler-wallet-main-v4": // temporary, we might migrate v4 later
- case "taler-wallet-main-v5":
- case "taler-wallet-main-v6":
- case "taler-wallet-main-v7":
- case "taler-wallet-main-v8":
- // We consider this a pre-release
- // development version, no migration is done.
- await metaDb
- .mktx((stores) => [stores.metaConfig])
- .runReadWrite(async (tx) => {
- await tx.metaConfig.put({
- key: CURRENT_DB_CONFIG_KEY,
- value: TALER_DB_NAME,
- });
- });
- break;
- default:
- throw Error(
- `migration from database ${currentMainVersion} not supported`,
- );
- }
- }
-
- const mainDbHandle = await openDatabase(
- idbFactory,
- TALER_DB_NAME,
- WALLET_DB_MINOR_VERSION,
- onVersionChange,
- onTalerDbUpgradeNeeded,
- );
-
- return new DbAccess(mainDbHandle, WalletStoresV1);
-}
-
-export async function deleteTalerDatabase(
- idbFactory: IDBFactory,
-): Promise<void> {
- return new Promise((resolve, reject) => {
- const req = idbFactory.deleteDatabase(TALER_DB_NAME);
- req.onerror = () => reject(req.error);
- req.onsuccess = () => resolve();
- });
-}
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
index 2bf417cac..085e909cf 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2022 Taler Systems S.A.
+ (C) 2021-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -17,47 +17,67 @@
/**
* Imports.
*/
-import { Event, IDBDatabase } from "@gnu-taler/idb-bridge";
import {
+ Event,
+ IDBDatabase,
+ IDBFactory,
+ IDBObjectStore,
+ IDBRequest,
+ IDBTransaction,
+ structuredEncapsulate,
+ structuredRevive,
+} from "@gnu-taler/idb-bridge";
+import {
+ AbsoluteTime,
AgeCommitmentProof,
- AmountJson,
AmountString,
+ Amounts,
+ AttentionInfo,
+ BackupProviderTerms,
+ CancellationToken,
+ Codec,
CoinEnvelope,
+ CoinPublicKeyString,
CoinRefreshRequest,
CoinStatus,
- MerchantContractTerms,
+ DenomLossEventType,
+ DenomSelectionState,
DenominationInfo,
DenominationPubKey,
- DenomSelectionState,
EddsaPublicKeyString,
EddsaSignatureString,
ExchangeAuditor,
ExchangeGlobalFees,
- InternationalizedString,
- Location,
- MerchantInfo,
- PayCoinSelection,
- PeerContractTerms,
- Product,
+ HashCodeString,
+ Logger,
RefreshReason,
TalerErrorDetail,
+ TalerPreciseTimestamp,
TalerProtocolDuration,
TalerProtocolTimestamp,
+ Transaction,
TransactionIdStr,
UnblindedSignature,
WireInfo,
- HashCodeString,
- Amounts,
- AttentionPriority,
- AttentionInfo,
- AbsoluteTime,
+ WithdrawalExchangeAccountDetails,
+ codecForAny,
} from "@gnu-taler/taler-util";
+import { DbRetryInfo, TaskIdentifiers } from "./common.js";
import {
+ DbAccess,
+ DbAccessImpl,
+ DbReadOnlyTransaction,
+ DbReadWriteTransaction,
+ IndexDescriptor,
+ StoreDescriptor,
+ StoreNames,
+ StoreWithIndexes,
describeContents,
describeIndex,
describeStore,
-} from "./util/query.js";
-import { RetryInfo, RetryTags } from "./util/retries.js";
+ describeStoreV2,
+ openDatabase,
+} from "./query.js";
/**
* This file contains the database schema of the Taler wallet together
@@ -82,12 +102,25 @@ import { RetryInfo, RetryTags } from "./util/retries.js";
*/
/**
+ FIXMEs:
+ - Contract terms can be quite large. We currently tend to read the
+ full contract terms from the DB quite often.
+ Instead, we should probably extract what we need into a separate object
+ store.
+ - More object stores should have an "id" primary key,
+ as this makes referencing less expensive.
+ - Coin selections should probably go into a separate object store.
+ - Some records should be split up into an extra "details" record
+ that we don't always need to iterate over.
+ */
+
+/**
* Name of the Taler database. This is effectively the major
* version of the DB schema. Whenever it changes, custom import logic
* for all previous versions must be written, which should be
* avoided.
*/
-export const TALER_DB_NAME = "taler-wallet-main-v9";
+export const TALER_WALLET_MAIN_DB_NAME = "taler-wallet-main-v10";
/**
* Name of the metadata database. This database is used
@@ -95,8 +128,20 @@ export const TALER_DB_NAME = "taler-wallet-main-v9";
*
* (Minor migrations are handled via upgrade transactions.)
*/
-export const TALER_META_DB_NAME = "taler-wallet-meta";
+export const TALER_WALLET_META_DB_NAME = "taler-wallet-meta";
+
+/**
+ * Name of the "stored backups" database.
+ * Stored backups are created before manually importing a backup.
+ * We use IndexedDB for this purpose, since we don't have file system
+ * access on some platforms.
+ */
+export const TALER_WALLET_STORED_BACKUPS_DB_NAME =
+ "taler-wallet-stored-backups";
+/**
+ * Name of the "meta config" database.
+ */
export const CURRENT_DB_CONFIG_KEY = "currentMainDbName";
/**
@@ -106,27 +151,122 @@ export const CURRENT_DB_CONFIG_KEY = "currentMainDbName";
* backwards-compatible way or object stores and indices
* are added.
*/
-export const WALLET_DB_MINOR_VERSION = 1;
+export const WALLET_DB_MINOR_VERSION = 10;
+
+declare const symDbProtocolTimestamp: unique symbol;
+
+declare const symDbPreciseTimestamp: unique symbol;
/**
- * Ranges for operation status fields.
+ * Timestamp, stored as microseconds.
*
- * All individual enums should make sure that the values they
- * defined are in the right range.
+ * Always rounded to a full second.
*/
-export enum OperationStatusRange {
- // Operations that need to be actively processed.
- ACTIVE_START = 10,
- ACTIVE_END = 29,
- // Operations that need user input, but nothing can be done
- // automatically.
- USER_ATTENTION_START = 30,
- USER_ATTENTION_END = 49,
- // Operations that don't need any attention or processing.
- DORMANT_START = 50,
- DORMANT_END = 69,
+export type DbProtocolTimestamp = number & { [symDbProtocolTimestamp]: true };
+
+/**
+ * Timestamp, stored as microseconds.
+ */
+export type DbPreciseTimestamp = number & { [symDbPreciseTimestamp]: true };
+
+const DB_TIMESTAMP_FOREVER = Number.MAX_SAFE_INTEGER;
+
+export function timestampPreciseFromDb(
+ dbTs: DbPreciseTimestamp,
+): TalerPreciseTimestamp {
+ return TalerPreciseTimestamp.fromMilliseconds(Math.floor(dbTs / 1000));
+}
+
+export function timestampOptionalPreciseFromDb(
+ dbTs: DbPreciseTimestamp | undefined,
+): TalerPreciseTimestamp | undefined {
+ if (!dbTs) {
+ return undefined;
+ }
+ return TalerPreciseTimestamp.fromMilliseconds(Math.floor(dbTs / 1000));
+}
+
+export function timestampPreciseToDb(
+ stamp: TalerPreciseTimestamp,
+): DbPreciseTimestamp {
+ if (stamp.t_s === "never") {
+ return DB_TIMESTAMP_FOREVER as DbPreciseTimestamp;
+ } else {
+ let tUs = stamp.t_s * 1000000;
+ if (stamp.off_us) {
+ tUs += stamp.off_us;
+ }
+ return tUs as DbPreciseTimestamp;
+ }
+}
+
+export function timestampProtocolToDb(
+ stamp: TalerProtocolTimestamp,
+): DbProtocolTimestamp {
+ if (stamp.t_s === "never") {
+ return DB_TIMESTAMP_FOREVER as DbProtocolTimestamp;
+ } else {
+ let tUs = stamp.t_s * 1000000;
+ return tUs as DbProtocolTimestamp;
+ }
}
+export function timestampProtocolFromDb(
+ stamp: DbProtocolTimestamp,
+): TalerProtocolTimestamp {
+ return TalerProtocolTimestamp.fromSeconds(Math.floor(stamp / 1000000));
+}
+
+export function timestampAbsoluteFromDb(
+ stamp: DbProtocolTimestamp | DbPreciseTimestamp,
+): AbsoluteTime {
+ if (stamp >= DB_TIMESTAMP_FOREVER) {
+ return AbsoluteTime.never();
+ }
+ return AbsoluteTime.fromMilliseconds(Math.floor(stamp / 1000));
+}
+
+export function timestampOptionalAbsoluteFromDb(
+ stamp: DbProtocolTimestamp | DbPreciseTimestamp | undefined,
+): AbsoluteTime | undefined {
+ if (stamp == null) {
+ return undefined;
+ }
+ if (stamp >= DB_TIMESTAMP_FOREVER) {
+ return AbsoluteTime.never();
+ }
+ return AbsoluteTime.fromMilliseconds(Math.floor(stamp / 1000));
+}
+
+/**
+ * Format of the operation status code: 0x0abc_nnnn
+
+ * a=1: active
+ * 0x0100_nnnn: pending
+ * 0x0101_nnnn: dialog
+ * 0x0102_nnnn: (reserved)
+ * 0x0103_nnnn: aborting
+ * 0x0110_nnnn: suspended
+ * 0x0113_nnnn: suspended-aborting
+ * a=5: final
+ * 0x0500_nnnn: done
+ * 0x0501_nnnn: failed
+ * 0x0502_nnnn: expired
+ * 0x0503_nnnn: aborted
+ *
+ * nnnn=0000 should always be the most generic minor state for the major state
+ */
+
+/**
+ * First possible operation status in the active range (inclusive).
+ */
+export const OPERATION_STATUS_ACTIVE_FIRST = 0x0100_0000;
+
+/**
+ * LAST possible operation status in the active range (inclusive).
+ */
+export const OPERATION_STATUS_ACTIVE_LAST = 0x0113_ffff;
+
/**
* Status of a withdrawal.
*/
@@ -134,36 +274,70 @@ export enum WithdrawalGroupStatus {
/**
* Reserve must be registered with the bank.
*/
- RegisteringBank = 10,
+ PendingRegisteringBank = 0x0100_0001,
+ SuspendedRegisteringBank = 0x0110_0001,
/**
* We've registered reserve's information with the bank
* and are now waiting for the user to confirm the withdraw
* with the bank (typically 2nd factor auth).
*/
- WaitConfirmBank = 11,
+ PendingWaitConfirmBank = 0x0100_0002,
+ SuspendedWaitConfirmBank = 0x0110_0002,
/**
* Querying reserve status with the exchange.
*/
- QueryingStatus = 12,
+ PendingQueryingStatus = 0x0100_0003,
+ SuspendedQueryingStatus = 0x0110_0003,
/**
* Ready for withdrawal.
*/
- Ready = 13,
+ PendingReady = 0x0100_0004,
+ SuspendedReady = 0x0110_0004,
+
+ /**
+ * We are telling the bank that we don't want to complete
+ * the withdrawal!
+ */
+ AbortingBank = 0x0103_0001,
+ SuspendedAbortingBank = 0x0113_0001,
+
+ /**
+ * Exchange wants KYC info from the user.
+ */
+ PendingKyc = 0x0100_0005,
+ SuspendedKyc = 0x0110_005,
+
+ /**
+ * Exchange is doing AML checks.
+ */
+ PendingAml = 0x0100_0006,
+ SuspendedAml = 0x0110_0006,
/**
* The corresponding withdraw record has been created.
* No further processing is done, unless explicitly requested
* by the user.
*/
- Finished = 50,
+ Done = 0x0500_0000,
/**
* The bank aborted the withdrawal.
*/
- BankAborted = 51,
+ FailedBankAborted = 0x0501_0001,
+
+ FailedAbortingBank = 0x0501_0002,
+
+ /**
+ * Aborted in a state where we were supposed to
+ * talk to the exchange. Money might have been
+ * wired or not.
+ */
+ AbortedExchange = 0x0503_0001,
+
+ AbortedBank = 0x0503_0002,
}
/**
@@ -191,69 +365,14 @@ export interface ReserveBankInfo {
*
* Set to undefined if that hasn't happened yet.
*/
- timestampReserveInfoPosted: TalerProtocolTimestamp | undefined;
+ timestampReserveInfoPosted: DbPreciseTimestamp | undefined;
/**
* Time when the reserve was confirmed by the bank.
*
* Set to undefined if not confirmed yet.
*/
- timestampBankConfirmed: TalerProtocolTimestamp | undefined;
-}
-
-/**
- * Record that indicates the wallet trusts
- * a particular auditor.
- */
-export interface AuditorTrustRecord {
- /**
- * Currency that we trust this auditor for.
- */
- currency: string;
-
- /**
- * Base URL of the auditor.
- */
- auditorBaseUrl: string;
-
- /**
- * Public key of the auditor.
- */
- auditorPub: string;
-
- /**
- * UIDs for the operation of adding this auditor
- * as a trusted auditor.
- *
- * (Used for backup/sync merging and tombstones.)
- */
- uids: string[];
-}
-
-/**
- * Record to indicate trust for a particular exchange.
- */
-export interface ExchangeTrustRecord {
- /**
- * Currency that we trust this exchange for.
- */
- currency: string;
-
- /**
- * Canonicalized exchange base URL.
- */
- exchangeBaseUrl: string;
-
- /**
- * Master public key of the exchange.
- */
- exchangeMasterPub: string;
-
- /**
- * UIDs for the operation of adding this exchange
- * as trusted.
- */
- uids: string[];
+ timestampBankConfirmed: DbPreciseTimestamp | undefined;
}
/**
@@ -261,19 +380,19 @@ export interface ExchangeTrustRecord {
*/
export enum DenominationVerificationStatus {
/**
- * Verification was delayed.
+ * Verification was delayed (pending).
*/
- Unverified = OperationStatusRange.ACTIVE_START,
+ Unverified = 0x0100_0000,
/**
* Verified as valid.
*/
- VerifiedGood = OperationStatusRange.DORMANT_START,
+ VerifiedGood = 0x0500_0000,
/**
* Verified as invalid.
*/
- VerifiedBad = OperationStatusRange.DORMANT_START + 1,
+ VerifiedBad = 0x0501_0000,
}
export interface DenomFees {
@@ -302,11 +421,14 @@ export interface DenomFees {
* Denomination record as stored in the wallet's database.
*/
export interface DenominationRecord {
+ /**
+ * Currency of the denomination.
+ *
+ * Stored separately as we have an index on it.
+ */
currency: string;
- amountVal: number;
-
- amountFrac: number;
+ value: AmountString;
/**
* The denomination public key.
@@ -324,22 +446,22 @@ export interface DenominationRecord {
/**
* Validity start date of the denomination.
*/
- stampStart: TalerProtocolTimestamp;
+ stampStart: DbProtocolTimestamp;
/**
* Date after which the currency can't be withdrawn anymore.
*/
- stampExpireWithdraw: TalerProtocolTimestamp;
+ stampExpireWithdraw: DbProtocolTimestamp;
/**
* Date after the denomination officially doesn't exist anymore.
*/
- stampExpireLegal: TalerProtocolTimestamp;
+ stampExpireLegal: DbProtocolTimestamp;
/**
* Data after which coins of this denomination can't be deposited anymore.
*/
- stampExpireDeposit: TalerProtocolTimestamp;
+ stampExpireDeposit: DbProtocolTimestamp;
/**
* Signature by the exchange's master key over the denomination
@@ -367,6 +489,13 @@ export interface DenominationRecord {
isRevoked: boolean;
/**
+ * If set to true, the exchange announced that the private key for this
+ * denomination is lost. Thus it can't be used to sign new coins
+ * during withdrawal/refresh/..., but the coins can still be spent.
+ */
+ isLost?: boolean;
+
+ /**
* Base URL of the exchange.
*/
exchangeBaseUrl: string;
@@ -376,23 +505,9 @@ export interface DenominationRecord {
* on the denomination.
*/
exchangeMasterPub: string;
-
- /**
- * Latest list issue date of the "/keys" response
- * that includes this denomination.
- */
- listIssueDate: TalerProtocolTimestamp;
}
export namespace DenominationRecord {
- export function getValue(d: DenominationRecord): AmountJson {
- return {
- currency: d.currency,
- fraction: d.amountFrac,
- value: d.amountVal,
- };
- }
-
export function toDenomInfo(d: DenominationRecord): DenominationInfo {
return {
denomPub: d.denomPub,
@@ -401,20 +516,20 @@ export namespace DenominationRecord {
feeRefresh: Amounts.stringify(d.fees.feeRefresh),
feeRefund: Amounts.stringify(d.fees.feeRefund),
feeWithdraw: Amounts.stringify(d.fees.feeWithdraw),
- stampExpireDeposit: d.stampExpireDeposit,
- stampExpireLegal: d.stampExpireLegal,
- stampExpireWithdraw: d.stampExpireWithdraw,
- stampStart: d.stampStart,
- value: Amounts.stringify(DenominationRecord.getValue(d)),
+ stampExpireDeposit: timestampProtocolFromDb(d.stampExpireDeposit),
+ stampExpireLegal: timestampProtocolFromDb(d.stampExpireLegal),
+ stampExpireWithdraw: timestampProtocolFromDb(d.stampExpireWithdraw),
+ stampStart: timestampProtocolFromDb(d.stampStart),
+ value: Amounts.stringify(d.value),
exchangeBaseUrl: d.exchangeBaseUrl,
};
}
}
export interface ExchangeSignkeysRecord {
- stampStart: TalerProtocolTimestamp;
- stampExpire: TalerProtocolTimestamp;
- stampEnd: TalerProtocolTimestamp;
+ stampStart: DbProtocolTimestamp;
+ stampExpire: DbProtocolTimestamp;
+ stampEnd: DbProtocolTimestamp;
signkeyPub: EddsaPublicKeyString;
masterSig: EddsaSignatureString;
@@ -460,21 +575,6 @@ export interface ExchangeDetailsRecord {
*/
globalFees: ExchangeGlobalFees[];
- /**
- * Etag of the current ToS of the exchange.
- */
- tosCurrentEtag: string;
-
- /**
- * Information about ToS acceptance from the user.
- */
- tosAccepted:
- | {
- etag: string;
- timestamp: TalerProtocolTimestamp;
- }
- | undefined;
-
wireInfo: WireInfo;
/**
@@ -483,25 +583,6 @@ export interface ExchangeDetailsRecord {
ageMask?: number;
}
-export interface ExchangeTosRecord {
- exchangeBaseUrl: string;
-
- etag: string;
-
- /**
- * Terms of service text or undefined if not downloaded yet.
- *
- * This is just used as a cache of the last downloaded ToS.
- *
- */
- termsOfServiceText: string | undefined;
-
- /**
- * Content-type of the last downloaded termsOfServiceText.
- */
- termsOfServiceContentType: string | undefined;
-}
-
export interface ExchangeDetailsPointer {
masterPublicKey: string;
@@ -511,19 +592,49 @@ export interface ExchangeDetailsPointer {
* Timestamp when the (masterPublicKey, currency) pointer
* has been updated.
*/
- updateClock: TalerProtocolTimestamp;
+ updateClock: DbPreciseTimestamp;
+}
+
+export enum ExchangeEntryDbRecordStatus {
+ Preset = 1,
+ Ephemeral = 2,
+ Used = 3,
+}
+
+// FIXME: Use status ranges for this as well?
+export enum ExchangeEntryDbUpdateStatus {
+ Initial = 1,
+ InitialUpdate = 2,
+ Suspended = 3,
+ UnavailableUpdate = 4,
+ // Reserved 5 for backwards compatibility.
+ Ready = 6,
+ ReadyUpdate = 7,
}
/**
* Exchange record as stored in the wallet's database.
*/
-export interface ExchangeRecord {
+export interface ExchangeEntryRecord {
/**
* Base url of the exchange.
*/
baseUrl: string;
/**
+ * Currency hint for a preset exchange, relevant
+ * when we didn't contact a preset exchange yet.
+ */
+ presetCurrencyHint?: string;
+
+ /**
+ * When did we confirm the last withdrawal from this exchange?
+ *
+ * Used mostly in the UI to suggest exchanges.
+ */
+ lastWithdrawal?: DbPreciseTimestamp;
+
+ /**
* Pointer to the current exchange details.
*
* Should usually not change. Only changes when the
@@ -535,26 +646,38 @@ export interface ExchangeRecord {
*/
detailsPointer: ExchangeDetailsPointer | undefined;
+ entryStatus: ExchangeEntryDbRecordStatus;
+
+ updateStatus: ExchangeEntryDbUpdateStatus;
+
/**
- * Is this a permanent or temporary exchange record?
+ * If set to true, the next update to the exchange
+ * status will request /keys with no-cache headers set.
+ */
+ cachebreakNextUpdate?: boolean;
+
+ /**
+ * Etag of the current ToS of the exchange.
*/
- permanent: boolean;
+ tosCurrentEtag: string | undefined;
+
+ tosAcceptedEtag: string | undefined;
+
+ tosAcceptedTimestamp: DbPreciseTimestamp | undefined;
/**
- * Last time when the exchange was updated (both /keys and /wire).
+ * Last time when the exchange /keys info was updated.
*/
- lastUpdate: TalerProtocolTimestamp | undefined;
+ lastUpdate: DbPreciseTimestamp | undefined;
/**
* Next scheduled update for the exchange.
- *
- * (This field must always be present, so we can index on the timestamp.)
*/
- nextUpdate: TalerProtocolTimestamp;
+ nextUpdateStamp: DbPreciseTimestamp;
- lastKeysEtag: string | undefined;
+ updateRetryCounter?: number;
- lastWireEtag: string | undefined;
+ lastKeysEtag: string | undefined;
/**
* Next time that we should check if coins need to be refreshed.
@@ -562,19 +685,30 @@ export interface ExchangeRecord {
* Updated whenever the exchange's denominations are updated or when
* the refresh check has been done.
*/
- nextRefreshCheck: TalerProtocolTimestamp;
+ nextRefreshCheckStamp: DbPreciseTimestamp;
/**
* Public key of the reserve that we're currently using for
* receiving P2P payments.
*/
currentMergeReserveRowId?: number;
+
+ /**
+ * Defaults to false.
+ */
+ peerPaymentsDisabled?: boolean;
+
+ /**
+ * Defaults to false.
+ */
+ noFees?: boolean;
}
export enum PlanchetStatus {
- Pending = 10 /* ACTIVE_START */,
- KycRequired = 11 /* ACTIVE_START + 1 */,
- WithdrawalDone = 50 /* DORMANT_START */,
+ Pending = 0x0100_0000,
+ KycRequired = 0x0100_0001,
+ WithdrawalDone = 0x0500_000,
+ AbortedReplaced = 0x0503_0001,
}
/**
@@ -622,7 +756,7 @@ export interface PlanchetRecord {
export enum CoinSourceType {
Withdraw = "withdraw",
Refresh = "refresh",
- Tip = "tip",
+ Reward = "reward",
}
export interface WithdrawCoinSource {
@@ -650,13 +784,16 @@ export interface RefreshCoinSource {
oldCoinPub: string;
}
-export interface TipCoinSource {
- type: CoinSourceType.Tip;
- walletTipId: string;
+export interface RewardCoinSource {
+ type: CoinSourceType.Reward;
+ walletRewardId: string;
coinIndex: number;
}
-export type CoinSource = WithdrawCoinSource | RefreshCoinSource | TipCoinSource;
+export type CoinSource =
+ | WithdrawCoinSource
+ | RefreshCoinSource
+ | RewardCoinSource;
/**
* CoinRecord as stored in the "coins" data store
@@ -669,6 +806,14 @@ export interface CoinRecord {
coinSource: CoinSource;
/**
+ * Source transaction ID of the coin.
+ *
+ * Used to make the coin visible after the transaction
+ * has entered a final state.
+ */
+ sourceTransactionId?: string;
+
+ /**
* Public key of the coin.
*/
coinPub: string;
@@ -714,6 +859,14 @@ export interface CoinRecord {
status: CoinStatus;
/**
+ * Non-zero for visible.
+ *
+ * A coin is visible when it is fresh and the
+ * source transaction is in a final state.
+ */
+ visible?: number;
+
+ /**
* Information about what the coin has been allocated for.
*
* Used for:
@@ -744,29 +897,29 @@ export interface CoinAllocation {
}
/**
- * Status of a tip we got from a merchant.
+ * Status of a reward we got from a merchant.
*/
-export interface TipRecord {
+export interface RewardRecord {
/**
* Has the user accepted the tip? Only after the tip has been accepted coins
* withdrawn from the tip may be used.
*/
- acceptedTimestamp: TalerProtocolTimestamp | undefined;
+ acceptedTimestamp: DbPreciseTimestamp | undefined;
/**
* The tipped amount.
*/
- tipAmountRaw: AmountString;
+ rewardAmountRaw: AmountString;
/**
* Effect on the balance (including fees etc).
*/
- tipAmountEffective: AmountString;
+ rewardAmountEffective: AmountString;
/**
* Timestamp, the tip can't be picked up anymore after this deadline.
*/
- tipExpiration: TalerProtocolTimestamp;
+ rewardExpiration: DbProtocolTimestamp;
/**
* The exchange that will sign our coins, chosen by the merchant.
@@ -792,7 +945,7 @@ export interface TipRecord {
/**
* Tip ID chosen by the wallet.
*/
- walletTipId: string;
+ walletRewardId: string;
/**
* Secret seed used to derive planchets for this tip.
@@ -800,47 +953,84 @@ export interface TipRecord {
secretSeed: string;
/**
- * The merchant's identifier for this tip.
+ * The merchant's identifier for this reward.
*/
- merchantTipId: string;
+ merchantRewardId: string;
+
+ createdTimestamp: DbPreciseTimestamp;
- createdTimestamp: TalerProtocolTimestamp;
+ /**
+ * The url to be redirected after the tip is accepted.
+ */
+ next_url: string | undefined;
/**
* Timestamp for when the wallet finished picking up the tip
* from the merchant.
*/
- pickedUpTimestamp: TalerProtocolTimestamp | undefined;
+ pickedUpTimestamp: DbPreciseTimestamp | undefined;
+
+ status: RewardRecordStatus;
+}
+
+export enum RewardRecordStatus {
+ PendingPickup = 0x0100_0000,
+ SuspendedPickup = 0x0110_0000,
+ DialogAccept = 0x0101_0000,
+ Done = 0x0500_0000,
+ Aborted = 0x0500_0000,
+ Failed = 0x0501_000,
}
export enum RefreshCoinStatus {
- Pending = OperationStatusRange.ACTIVE_START,
- Finished = OperationStatusRange.DORMANT_START,
+ Pending = 0x0100_0000,
+ Finished = 0x0500_0000,
/**
* The refresh for this coin has been frozen, because of a permanent error.
* More info in lastErrorPerCoin.
*/
- Frozen = OperationStatusRange.DORMANT_START + 1,
+ Failed = 0x0501_000,
}
-export enum OperationStatus {
- Finished = OperationStatusRange.DORMANT_START,
- Pending = OperationStatusRange.ACTIVE_START,
+export enum RefreshOperationStatus {
+ Pending = 0x0100_0000,
+ Suspended = 0x0110_0000,
+
+ Finished = 0x0500_000,
+ Failed = 0x0501_000,
}
-export enum RefreshOperationStatus {
- Pending = 10 /* ACTIVE_START */,
- Finished = 50 /* DORMANT_START */,
- FinishedWithError = 51 /* DORMANT_START + 1 */,
+/**
+ * Status of a single element of a deposit group.
+ */
+export enum DepositElementStatus {
+ DepositPending = 0x0100_0000,
+ /**
+ * Accepted, but tracking.
+ */
+ Tracking = 0x0100_0001,
+ KycRequired = 0x0100_0002,
+ Wired = 0x0500_0000,
+ RefundSuccess = 0x0503_0000,
+ RefundFailed = 0x0501_0000,
}
+export interface RefreshGroupPerExchangeInfo {
+ /**
+ * (Expected) output once the refresh group succeeded.
+ */
+ outputEffective: AmountString;
+}
+
+/**
+ * Group of refresh operations. The refreshed coins do not
+ * have to belong to the same exchange, but must have the same
+ * currency.
+ */
export interface RefreshGroupRecord {
operationStatus: RefreshOperationStatus;
- // FIXME: Put this into a different object store?
- lastErrorPerCoin: { [coinIndex: number]: TalerErrorDetail };
-
/**
* Unique, randomly generated identifier for this group of
* refresh operations.
@@ -848,19 +1038,24 @@ export interface RefreshGroupRecord {
refreshGroupId: string;
/**
+ * Currency of this refresh group.
+ */
+ currency: string;
+
+ /**
* Reason why this refresh group has been created.
*/
reason: RefreshReason;
- oldCoinPubs: string[];
+ originatingTransactionId?: string;
- // FIXME: Should this go into a separate
- // object store for faster updates?
- refreshSessionPerCoin: (RefreshSessionRecord | undefined)[];
+ oldCoinPubs: string[];
inputPerCoin: AmountString[];
- estimatedOutputPerCoin: AmountString[];
+ expectedOutputPerCoin: AmountString[];
+
+ infoPerExchange?: Record<string, RefreshGroupPerExchangeInfo>;
/**
* Flag for each coin whether refreshing finished.
@@ -870,18 +1065,25 @@ export interface RefreshGroupRecord {
*/
statusPerCoin: RefreshCoinStatus[];
- timestampCreated: TalerProtocolTimestamp;
+ timestampCreated: DbPreciseTimestamp;
/**
* Timestamp when the refresh session finished.
*/
- timestampFinished: TalerProtocolTimestamp | undefined;
+ timestampFinished: DbPreciseTimestamp | undefined;
}
/**
* Ongoing refresh
*/
export interface RefreshSessionRecord {
+ refreshGroupId: string;
+
+ /**
+ * Index of the coin in the refresh group.
+ */
+ coinIndex: number;
+
/**
* 512-bit secret that can be used to derive
* the other cryptographic material for the refresh session.
@@ -906,63 +1108,8 @@ export interface RefreshSessionRecord {
* The no-reveal-index after we've done the melting.
*/
norevealIndex?: number;
-}
-
-export enum RefundState {
- Failed = "failed",
- Applied = "applied",
- Pending = "pending",
-}
-
-/**
- * State of one refund from the merchant, maintained by the wallet.
- */
-export type WalletRefundItem =
- | WalletRefundFailedItem
- | WalletRefundPendingItem
- | WalletRefundAppliedItem;
-
-export interface WalletRefundItemCommon {
- // Execution time as claimed by the merchant
- executionTime: TalerProtocolTimestamp;
- /**
- * Time when the wallet became aware of the refund.
- */
- obtainedTime: TalerProtocolTimestamp;
-
- refundAmount: AmountString;
-
- refundFee: AmountString;
-
- /**
- * Upper bound on the refresh cost incurred by
- * applying this refund.
- *
- * Might be lower in practice when two refunds on the same
- * coin are refreshed in the same refresh operation.
- */
- totalRefreshCostBound: AmountString;
-
- coinPub: string;
-
- rtransactionId: number;
-}
-
-/**
- * Failed refund, either because the merchant did
- * something wrong or it expired.
- */
-export interface WalletRefundFailedItem extends WalletRefundItemCommon {
- type: RefundState.Failed;
-}
-
-export interface WalletRefundPendingItem extends WalletRefundItemCommon {
- type: RefundState.Pending;
-}
-
-export interface WalletRefundAppliedItem extends WalletRefundItemCommon {
- type: RefundState.Applied;
+ lastError?: TalerErrorDetail;
}
export enum RefundReason {
@@ -976,117 +1123,92 @@ export enum RefundReason {
AbortRefund = "abort-pay-refund",
}
-export interface AllowedAuditorInfo {
- auditorBaseUrl: string;
- auditorPub: string;
-}
-
-export interface AllowedExchangeInfo {
- exchangeBaseUrl: string;
- exchangePub: string;
-}
-
-/**
- * Data extracted from the contract terms that is relevant for payment
- * processing in the wallet.
- */
-export interface WalletContractData {
- products?: Product[];
- summaryI18n: { [lang_tag: string]: string } | undefined;
-
- /**
- * Fulfillment URL, or the empty string if the order has no fulfillment URL.
- *
- * Stored as a non-nullable string as we use this field for IndexedDB indexing.
- */
- fulfillmentUrl: string;
-
- contractTermsHash: string;
- fulfillmentMessage?: string;
- fulfillmentMessageI18n?: InternationalizedString;
- merchantSig: string;
- merchantPub: string;
- merchant: MerchantInfo;
- amount: AmountString;
- orderId: string;
- merchantBaseUrl: string;
- summary: string;
- autoRefund: TalerProtocolDuration | undefined;
- maxWireFee: AmountString;
- wireFeeAmortization: number;
- payDeadline: TalerProtocolTimestamp;
- refundDeadline: TalerProtocolTimestamp;
- allowedAuditors: AllowedAuditorInfo[];
- allowedExchanges: AllowedExchangeInfo[];
- timestamp: TalerProtocolTimestamp;
- wireMethod: string;
- wireInfoHash: string;
- maxDepositFee: AmountString;
- minimumAge?: number;
- deliveryDate: TalerProtocolTimestamp | undefined;
- deliveryLocation: Location | undefined;
-}
-
export enum PurchaseStatus {
/**
* Not downloaded yet.
*/
- DownloadingProposal = 10,
+ PendingDownloadingProposal = 0x0100_0000,
+ SuspendedDownloadingProposal = 0x0110_0000,
/**
* The user has accepted the proposal.
*/
- Paying = 11,
+ PendingPaying = 0x0100_0001,
+ SuspendedPaying = 0x0110_0001,
/**
* Currently in the process of aborting with a refund.
*/
- AbortingWithRefund = 12,
+ AbortingWithRefund = 0x0103_0000,
+ SuspendedAbortingWithRefund = 0x0113_0000,
/**
* Paying a second time, likely with different session ID
*/
- PayingReplay = 13,
+ PendingPayingReplay = 0x0100_0002,
+ SuspendedPayingReplay = 0x0110_0002,
/**
* Query for refunds (until query succeeds).
*/
- QueryingRefund = 14,
+ PendingQueryingRefund = 0x0100_0003,
+ SuspendedQueryingRefund = 0x0110_0003,
/**
* Query for refund (until auto-refund deadline is reached).
*/
- QueryingAutoRefund = 15,
+ PendingQueryingAutoRefund = 0x0100_0004,
+ SuspendedQueryingAutoRefund = 0x0110_0004,
+
+ PendingAcceptRefund = 0x0100_0005,
+ SuspendedPendingAcceptRefund = 0x0110_0005,
/**
* Proposal downloaded, but the user needs to accept/reject it.
*/
- Proposed = 30,
+ DialogProposed = 0x0101_0000,
+
+ /**
+ * Proposal shared to other wallet or read from other wallet
+ * the user needs to accept/reject it.
+ */
+ DialogShared = 0x0101_0001,
/**
* The user has rejected the proposal.
*/
- ProposalRefused = 50,
+ AbortedProposalRefused = 0x0503_0000,
/**
* Downloading or processing the proposal has failed permanently.
*/
- ProposalDownloadFailed = 51,
+ FailedClaim = 0x0501_0000,
/**
- * Downloaded proposal was detected as a re-purchase.
+ * Tried to abort, but aborting failed or was cancelled.
*/
- RepurchaseDetected = 52,
+ FailedAbort = 0x0501_0001,
+
+ FailedPaidByOther = 0x0501_0002,
/**
- * The payment has been aborted.
+ * Payment was successful.
*/
- PaymentAbortFinished = 53,
+ Done = 0x0500_0000,
/**
- * Payment was successful.
+ * Downloaded proposal was detected as a re-purchase.
+ */
+ DoneRepurchaseDetected = 0x0500_0001,
+
+ /**
+ * The payment has been aborted.
*/
- Paid = 54,
+ AbortedIncompletePayment = 0x0503_0000,
+
+ AbortedRefunded = 0x0503_0001,
+
+ AbortedOrderDeleted = 0x0503_0002,
}
/**
@@ -1101,10 +1223,21 @@ export interface ProposalDownloadInfo {
contractTermsMerchantSig: string;
}
+export interface DbCoinSelection {
+ coinPubs: string[];
+ coinContributions: AmountString[];
+}
+
export interface PurchasePayInfo {
- payCoinSelection: PayCoinSelection;
+ /**
+ * Undefined if payment is blocked by a pending refund.
+ */
+ payCoinSelection?: DbCoinSelection;
+ /**
+ * Undefined if payment is blocked by a pending refund.
+ */
+ payCoinSelectionUid?: string;
totalPayCost: AmountString;
- payCoinSelectionUid: string;
}
/**
@@ -1177,34 +1310,35 @@ export interface PurchaseRecord {
* Timestamp of the first time that sending a payment to the merchant
* for this purchase was successful.
*/
- timestampFirstSuccessfulPay: TalerProtocolTimestamp | undefined;
+ timestampFirstSuccessfulPay: DbPreciseTimestamp | undefined;
merchantPaySig: string | undefined;
+ posConfirmation: string | undefined;
+
/**
- * When was the purchase record created?
+ * This purchase was created by reading
+ * a payment share or the wallet
+ * the nonce public by a payment share
*/
- timestamp: TalerProtocolTimestamp;
+ shared: boolean;
/**
- * When was the purchase made?
- * Refers to the time that the user accepted.
+ * When was the purchase record created?
*/
- timestampAccept: TalerProtocolTimestamp | undefined;
+ timestamp: DbPreciseTimestamp;
/**
- * Pending refunds for the purchase. A refund is pending
- * when the merchant reports a transient error from the exchange.
- *
- * FIXME: Put this into a separate object store?
+ * When was the purchase made?
+ * Refers to the time that the user accepted.
*/
- refunds: { [refundKey: string]: WalletRefundItem };
+ timestampAccept: DbPreciseTimestamp | undefined;
/**
* When was the last refund made?
* Set to 0 if no refund was made on the purchase.
*/
- timestampLastRefundStatus: TalerProtocolTimestamp | undefined;
+ timestampLastRefundStatus: DbPreciseTimestamp | undefined;
/**
* Last session signature that we submitted to /pay (if any).
@@ -1214,7 +1348,7 @@ export interface PurchaseRecord {
/**
* Continue querying the refund status until this deadline has expired.
*/
- autoRefundDeadline: TalerProtocolTimestamp | undefined;
+ autoRefundDeadline: DbProtocolTimestamp | undefined;
/**
* How much merchant has refund to be taken but the wallet
@@ -1227,6 +1361,9 @@ export enum ConfigRecordKey {
WalletBackupState = "walletBackupState",
CurrencyDefaultsApplied = "currencyDefaultsApplied",
DevMode = "devMode",
+ // Only for testing, do not use!
+ TestLoopTx = "testTxLoop",
+ LastInitInfo = "lastInitInfo",
}
/**
@@ -1239,7 +1376,8 @@ export type ConfigRecord =
value: WalletBackupConfState;
}
| { key: ConfigRecordKey.CurrencyDefaultsApplied; value: boolean }
- | { key: ConfigRecordKey.DevMode; value: boolean };
+ | { key: ConfigRecordKey.TestLoopTx; value: number }
+ | { key: ConfigRecordKey.LastInitInfo; value: DbProtocolTimestamp };
export interface WalletBackupConfState {
deviceId: string;
@@ -1254,15 +1392,16 @@ export interface WalletBackupConfState {
/**
* Timestamp stored in the last backup.
*/
- lastBackupTimestamp?: TalerProtocolTimestamp;
+ lastBackupTimestamp?: DbPreciseTimestamp;
/**
* Last time we tried to do a backup.
*/
- lastBackupCheckTimestamp?: TalerProtocolTimestamp;
+ lastBackupCheckTimestamp?: DbPreciseTimestamp;
lastBackupNonce?: string;
}
+// FIXME: Should these be numeric codes?
export const enum WithdrawalRecordType {
BankManual = "bank-manual",
BankIntegrated = "bank-integrated",
@@ -1278,16 +1417,26 @@ export interface WgInfoBankIntegrated {
* a Taler-integrated bank.
*/
bankInfo: ReserveBankInfo;
+ /**
+ * Info about withdrawal accounts, possibly including currency conversion.
+ */
+ exchangeCreditAccounts?: WithdrawalExchangeAccountDetails[];
}
export interface WgInfoBankManual {
withdrawalType: WithdrawalRecordType.BankManual;
+
+ /**
+ * Info about withdrawal accounts, possibly including currency conversion.
+ */
+ exchangeCreditAccounts?: WithdrawalExchangeAccountDetails[];
}
export interface WgInfoBankPeerPull {
withdrawalType: WithdrawalRecordType.PeerPullCredit;
- contractTerms: any;
+ // FIXME: include a transaction ID here?
+
/**
* Needed to quickly construct the taler:// URI for the counterparty
* without a join.
@@ -1298,7 +1447,7 @@ export interface WgInfoBankPeerPull {
export interface WgInfoBankPeerPush {
withdrawalType: WithdrawalRecordType.PeerPushCredit;
- contractTerms: any;
+ // FIXME: include a transaction ID here?
}
export interface WgInfoBankRecoup {
@@ -1312,9 +1461,15 @@ export type WgInfo =
| WgInfoBankPeerPush
| WgInfoBankRecoup;
+export type KycUserType = "individual" | "business";
+
+export interface KycPendingInfo {
+ paytoHash: string;
+ requirementRow: number;
+}
/**
* Group of withdrawal operations that need to be executed.
- * (Either for a normal withdrawal or from a tip.)
+ * (Either for a normal withdrawal or from a reward.)
*
* The withdrawal group record is only created after we know
* the coin selection we want to withdraw.
@@ -1327,6 +1482,10 @@ export interface WithdrawalGroupRecord {
wgInfo: WgInfo;
+ kycPending?: KycPendingInfo;
+
+ kycUrl?: string;
+
/**
* Secret seed used to derive planchets.
* Stored since planchets are created lazily.
@@ -1355,12 +1514,12 @@ export interface WithdrawalGroupRecord {
* When was the withdrawal operation started started?
* Timestamp in milliseconds.
*/
- timestampStart: TalerProtocolTimestamp;
+ timestampStart: DbPreciseTimestamp;
/**
* When was the withdrawal operation completed?
*/
- timestampFinish?: TalerProtocolTimestamp;
+ timestampFinish?: DbPreciseTimestamp;
/**
* Current status of the reserve.
@@ -1437,6 +1596,14 @@ export interface BankWithdrawUriRecord {
reservePub: string;
}
+export enum RecoupOperationStatus {
+ Pending = 0x0100_0000,
+ Suspended = 0x0110_0000,
+
+ Finished = 0x0500_000,
+ Failed = 0x0501_000,
+}
+
/**
* Status of recoup operations that were grouped together.
*
@@ -1451,9 +1618,11 @@ export interface RecoupGroupRecord {
exchangeBaseUrl: string;
- timestampStarted: TalerProtocolTimestamp;
+ operationStatus: RecoupOperationStatus;
+
+ timestampStarted: DbPreciseTimestamp;
- timestampFinished: TalerProtocolTimestamp | undefined;
+ timestampFinished: DbPreciseTimestamp | undefined;
/**
* Public keys that identify the coins being recouped
@@ -1487,18 +1656,12 @@ export type BackupProviderState =
}
| {
tag: BackupProviderStateTag.Ready;
- nextBackupTimestamp: TalerProtocolTimestamp;
+ nextBackupTimestamp: DbPreciseTimestamp;
}
| {
tag: BackupProviderStateTag.Retrying;
};
-export interface BackupProviderTerms {
- supportedProtocolVersion: string;
- annualFee: AmountString;
- storageLimitInMegabytes: number;
-}
-
export interface BackupProviderRecord {
/**
* Base URL of the provider.
@@ -1532,7 +1695,7 @@ export interface BackupProviderRecord {
* Does NOT correspond to the timestamp of the backup,
* which only changes when the backup content changes.
*/
- lastBackupCycleTimestamp?: TalerProtocolTimestamp;
+ lastBackupCycleTimestamp?: DbPreciseTimestamp;
/**
* Proposal that we're currently trying to pay for.
@@ -1561,12 +1724,60 @@ export interface BackupProviderRecord {
uids: string[];
}
+export enum DepositOperationStatus {
+ PendingDeposit = 0x0100_0000,
+ PendingTrack = 0x0100_0001,
+ PendingKyc = 0x0100_0002,
+
+ Aborting = 0x0103_0000,
+
+ SuspendedDeposit = 0x0110_0000,
+ SuspendedTrack = 0x0110_0001,
+ SuspendedKyc = 0x0110_0002,
+
+ SuspendedAborting = 0x0113_0000,
+
+ Finished = 0x0500_0000,
+ Failed = 0x0501_0000,
+ Aborted = 0x0503_0000,
+}
+
+export interface DepositTrackingInfo {
+ // Raw wire transfer identifier of the deposit.
+ wireTransferId: string;
+ // When was the wire transfer given to the bank.
+ timestampExecuted: DbProtocolTimestamp;
+ // Total amount transfer for this wtid (including fees)
+ amountRaw: AmountString;
+ // Wire fee amount for this exchange
+ wireFee: AmountString;
+
+ exchangePub: string;
+}
+
+export interface DepositInfoPerExchange {
+ /**
+ * Expected effective amount that will be deposited
+ * from coins of this exchange.
+ */
+ amountEffective: AmountString;
+}
+
/**
* Group of deposits made by the wallet.
*/
export interface DepositGroupRecord {
depositGroupId: string;
+ currency: string;
+
+ /**
+ * Instructed amount.
+ */
+ amount: AmountString;
+
+ wireTransferDeadline: DbProtocolTimestamp;
+
merchantPub: string;
merchantPriv: string;
@@ -1582,53 +1793,49 @@ export interface DepositGroupRecord {
salt: string;
};
- /**
- * Verbatim contract terms.
- */
- contractTermsRaw: MerchantContractTerms;
-
contractTermsHash: string;
- payCoinSelection: PayCoinSelection;
+ payCoinSelection?: DbCoinSelection;
- payCoinSelectionUid: string;
+ payCoinSelectionUid?: string;
totalPayCost: AmountString;
- effectiveDepositAmount: AmountString;
+ /**
+ * The counterparty effective deposit amount.
+ */
+ counterpartyEffectiveDepositAmount: AmountString;
- depositedPerCoin: boolean[];
+ timestampCreated: DbPreciseTimestamp;
- timestampCreated: TalerProtocolTimestamp;
+ timestampFinished: DbPreciseTimestamp | undefined;
- timestampFinished: TalerProtocolTimestamp | undefined;
+ operationStatus: DepositOperationStatus;
- operationStatus: OperationStatus;
-}
+ statusPerCoin?: DepositElementStatus[];
+
+ infoPerExchange?: Record<string, DepositInfoPerExchange>;
-/**
- * Record for a deposits that the wallet observed
- * as a result of double spending, but which is not
- * present in the wallet's own database otherwise.
- */
-export interface GhostDepositGroupRecord {
/**
- * When multiple deposits for the same contract terms hash
- * have a different timestamp, we choose the earliest one.
+ * When the deposit transaction was aborted and
+ * refreshes were tried, we create a refresh
+ * group and store the ID here.
*/
- timestamp: TalerProtocolTimestamp;
+ abortRefreshGroupId?: string;
- contractTermsHash: string;
+ kycInfo?: DepositKycInfo;
- deposits: {
- coinPub: string;
- amount: AmountString;
- timestamp: TalerProtocolTimestamp;
- depositFee: AmountString;
- merchantPub: string;
- coinSig: string;
- wireHash: string;
- }[];
+ // FIXME: Do we need this and should it be in this object store?
+ trackingState?: {
+ [signature: string]: DepositTrackingInfo;
+ };
+}
+
+export interface DepositKycInfo {
+ kycUrl: string;
+ requirementRow: number;
+ paytoHash: string;
+ exchangeBaseUrl: string;
}
export interface TombstoneRecord {
@@ -1638,25 +1845,57 @@ export interface TombstoneRecord {
id: string;
}
-export enum PeerPushPaymentInitiationStatus {
+export enum PeerPushDebitStatus {
/**
* Initiated, but no purse created yet.
*/
- Initiated = 10 /* ACTIVE_START */,
- PurseCreated = 50 /* DORMANT_START */,
+ PendingCreatePurse = 0x0100_0000 /* ACTIVE_START */,
+ PendingReady = 0x0100_0001,
+ AbortingDeletePurse = 0x0103_0000,
+ /**
+ * Refresh after the purse got deleted by the wallet.
+ */
+ AbortingRefreshDeleted = 0x0103_0001,
+ /**
+ * Refresh after the purse expired.
+ */
+ AbortingRefreshExpired = 0x0103_0002,
+
+ SuspendedCreatePurse = 0x0110_0000,
+ SuspendedReady = 0x0110_0001,
+ SuspendedAbortingDeletePurse = 0x0113_0000,
+ SuspendedAbortingRefreshDeleted = 0x0113_0001,
+ SuspendedAbortingRefreshExpired = 0x0113_0002,
+
+ Done = 0x0500_0000,
+ Aborted = 0x0503_0000,
+ Failed = 0x0501_0000,
+ Expired = 0x0502_0000,
+}
+
+export interface DbPeerPushPaymentCoinSelection {
+ contributions: AmountString[];
+ coinPubs: CoinPublicKeyString[];
}
/**
* Record for a push P2P payment that this wallet initiated.
*/
-export interface PeerPushPaymentInitiationRecord {
+export interface PeerPushDebitRecord {
/**
* What exchange are funds coming from?
*/
exchangeBaseUrl: string;
+ /**
+ * Instructed amount.
+ */
amount: AmountString;
+ totalCost: AmountString;
+
+ coinSel?: DbPeerPushPaymentCoinSelection;
+
contractTermsHash: HashCodeString;
/**
@@ -1681,18 +1920,51 @@ export interface PeerPushPaymentInitiationRecord {
mergePriv: string;
contractPriv: string;
+ contractPub: string;
+
+ /**
+ * 24 byte nonce.
+ */
+ contractEncNonce: string;
+
+ purseExpiration: DbProtocolTimestamp;
- purseExpiration: TalerProtocolTimestamp;
+ timestampCreated: DbPreciseTimestamp;
- timestampCreated: TalerProtocolTimestamp;
+ abortRefreshGroupId?: string;
/**
* Status of the peer push payment initiation.
*/
- status: PeerPushPaymentInitiationStatus;
+ status: PeerPushDebitStatus;
+}
+
+export enum PeerPullPaymentCreditStatus {
+ PendingCreatePurse = 0x0100_0000,
+ /**
+ * Purse created, waiting for the other party to accept the
+ * invoice and deposit money into it.
+ */
+ PendingReady = 0x0100_0001,
+ PendingMergeKycRequired = 0x0100_0002,
+ PendingWithdrawing = 0x0100_0003,
+
+ AbortingDeletePurse = 0x0103_0000,
+
+ SuspendedCreatePurse = 0x0110_0000,
+ SuspendedReady = 0x0110_0001,
+ SuspendedMergeKycRequired = 0x0110_0002,
+ SuspendedWithdrawing = 0x0110_0000,
+
+ SuspendedAbortingDeletePurse = 0x0113_0000,
+
+ Done = 0x0500_0000,
+ Failed = 0x0501_0000,
+ Expired = 0x0502_0000,
+ Aborted = 0x0503_0000,
}
-export interface PeerPullPaymentInitiationRecord {
+export interface PeerPullCreditRecord {
/**
* What exchange are we using for the payment request?
*/
@@ -1700,9 +1972,12 @@ export interface PeerPullPaymentInitiationRecord {
/**
* Amount requested.
+ * FIXME: What type of instructed amount is i?
*/
amount: AmountString;
+ estimatedAmountEffective: AmountString;
+
/**
* Purse public key. Used as the primary key to look
* up this record.
@@ -1720,10 +1995,48 @@ export interface PeerPullPaymentInitiationRecord {
*/
contractTermsHash: string;
+ mergePub: string;
+ mergePriv: string;
+
+ contractPub: string;
+ contractPriv: string;
+
+ contractEncNonce: string;
+
+ mergeTimestamp: DbPreciseTimestamp;
+
+ mergeReserveRowId: number;
+
/**
* Status of the peer pull payment initiation.
*/
- status: OperationStatus;
+ status: PeerPullPaymentCreditStatus;
+
+ kycInfo?: KycPendingInfo;
+
+ kycUrl?: string;
+
+ withdrawalGroupId: string | undefined;
+}
+
+export enum PeerPushCreditStatus {
+ PendingMerge = 0x0100_0000,
+ PendingMergeKycRequired = 0x0100_0001,
+ /**
+ * Merge was successful and withdrawal group has been created, now
+ * everything is in the hand of the withdrawal group.
+ */
+ PendingWithdrawing = 0x0100_0002,
+
+ SuspendedMerge = 0x0110_0000,
+ SuspendedMergeKycRequired = 0x0110_0001,
+ SuspendedWithdrawing = 0x0110_0002,
+
+ DialogProposed = 0x0101_0000,
+
+ Done = 0x0500_0000,
+ Aborted = 0x0503_0000,
+ Failed = 0x0501_0000,
}
/**
@@ -1732,7 +2045,7 @@ export interface PeerPullPaymentInitiationRecord {
* Unique: (exchangeBaseUrl, pursePub)
*/
export interface PeerPushPaymentIncomingRecord {
- peerPushPaymentIncomingId: string;
+ peerPushCreditId: string;
exchangeBaseUrl: string;
@@ -1742,7 +2055,9 @@ export interface PeerPushPaymentIncomingRecord {
contractPriv: string;
- timestamp: TalerProtocolTimestamp;
+ timestamp: DbPreciseTimestamp;
+
+ estimatedAmountEffective: AmountString;
/**
* Hash of the contract terms. Also
@@ -1753,32 +2068,85 @@ export interface PeerPushPaymentIncomingRecord {
/**
* Status of the peer push payment incoming initiation.
*/
- status: OperationStatus;
+ status: PeerPushCreditStatus;
+
+ /**
+ * Associated withdrawal group.
+ */
+ withdrawalGroupId: string | undefined;
+
+ /**
+ * Currency of the peer push payment credit transaction.
+ *
+ * Mandatory in current schema version, optional for compatibility
+ * with older (ver_minor<4) DB versions.
+ */
+ currency: string | undefined;
+
+ kycInfo?: KycPendingInfo;
+
+ kycUrl?: string;
}
-export enum PeerPullPaymentIncomingStatus {
- Proposed = 30 /* USER_ATTENTION_START */,
- Accepted = 10 /* ACTIVE_START */,
- Paid = 50 /* DORMANT_START */,
+export enum PeerPullDebitRecordStatus {
+ PendingDeposit = 0x0100_0001,
+ AbortingRefresh = 0x0103_0001,
+
+ SuspendedDeposit = 0x0110_0001,
+ SuspendedAbortingRefresh = 0x0113_0001,
+
+ DialogProposed = 0x0101_0001,
+
+ Done = 0x0500_0000,
+ Aborted = 0x0503_0000,
+ Failed = 0x0501_0000,
+}
+
+export interface PeerPullPaymentCoinSelection {
+ contributions: AmountString[];
+ coinPubs: CoinPublicKeyString[];
+
+ /**
+ * Total cost based on the coin selection.
+ * Non undefined after status === "Accepted"
+ */
+ totalCost: AmountString | undefined;
}
+/**
+ * AKA PeerPullDebit.
+ */
export interface PeerPullPaymentIncomingRecord {
- peerPullPaymentIncomingId: string;
+ peerPullDebitId: string;
pursePub: string;
exchangeBaseUrl: string;
- contractTerms: PeerContractTerms;
+ amount: AmountString;
+
+ contractTermsHash: string;
- timestampCreated: TalerProtocolTimestamp;
+ timestampCreated: DbPreciseTimestamp;
+ /**
+ * Contract priv that we got from the other party.
+ */
contractPriv: string;
/**
* Status of the peer push payment incoming initiation.
*/
- status: PeerPullPaymentIncomingStatus;
+ status: PeerPullDebitRecordStatus;
+
+ /**
+ * Estimated total cost when the record was created.
+ */
+ totalCostEstimated: AmountString;
+
+ abortRefreshGroupId?: string;
+
+ coinSel?: PeerPullPaymentCoinSelection;
}
/**
@@ -1800,13 +2168,13 @@ export interface OperationRetryRecord {
* Unique identifier for the operation. Typically of
* the format `${opType}-${opUniqueKey}`
*
- * @see {@link RetryTags}
+ * @see {@link TaskIdentifiers}
*/
id: string;
lastError?: TalerErrorDetail;
- retryInfo: RetryInfo;
+ retryInfo: DbRetryInfo;
}
/**
@@ -1817,8 +2185,7 @@ export interface OperationRetryRecord {
*/
export interface CoinAvailabilityRecord {
currency: string;
- amountVal: number;
- amountFrac: number;
+ value: AmountString;
denomPubHash: string;
exchangeBaseUrl: string;
@@ -1832,6 +2199,18 @@ export interface CoinAvailabilityRecord {
* Number of fresh coins of this denomination that are available.
*/
freshCoinCount: number;
+
+ /**
+ * Number of fresh coins that are available
+ * and visible, i.e. the source transaction is in
+ * a final state.
+ */
+ visibleCoinCount: number;
+
+ /**
+ * Number of coins that we expect to obtain via a pending refresh.
+ */
+ pendingRefreshOutputCount?: number;
}
export interface ContractTermsRecord {
@@ -1850,15 +2229,167 @@ export interface UserAttentionRecord {
info: AttentionInfo;
entityId: string;
+
/**
* When the notification was created.
*/
- createdMs: number;
+ created: DbPreciseTimestamp;
/**
* When the user mark this notification as read.
*/
- read: TalerProtocolTimestamp | undefined;
+ read: DbPreciseTimestamp | undefined;
+}
+
+export interface DbExchangeHandle {
+ url: string;
+ exchangeMasterPub: string;
+}
+
+export interface DbAuditorHandle {
+ url: string;
+ auditorPub: string;
+}
+
+export enum RefundGroupStatus {
+ Pending = 0x0100_0000,
+ Done = 0x0500_0000,
+ Failed = 0x0501_0000,
+ Aborted = 0x0503_0000,
+ Expired = 0x0502_0000,
+}
+
+/**
+ * Metadata about a group of refunds with the merchant.
+ */
+export interface RefundGroupRecord {
+ status: RefundGroupStatus;
+
+ /**
+ * Timestamp when the refund group was created.
+ */
+ timestampCreated: DbPreciseTimestamp;
+
+ proposalId: string;
+
+ refundGroupId: string;
+
+ refreshGroupId?: string;
+
+ amountRaw: AmountString;
+
+ /**
+ * Estimated effective amount, based on
+ * refund fees and refresh costs.
+ */
+ amountEffective: AmountString;
+}
+
+export enum RefundItemStatus {
+ /**
+ * Intermittent error that the merchant is
+ * reporting from the exchange.
+ *
+ * We'll try again!
+ */
+ Pending = 0x0100_0000,
+ /**
+ * Refund was obtained successfully.
+ */
+ Done = 0x0500_0000,
+ /**
+ * Permanent error reported by the exchange
+ * for the refund.
+ */
+ Failed = 0x0501_0000,
+}
+
+/**
+ * Refund for a single coin in a payment with a merchant.
+ */
+export interface RefundItemRecord {
+ /**
+ * Auto-increment DB record ID.
+ */
+ id?: number;
+
+ status: RefundItemStatus;
+
+ refundGroupId: string;
+
+ /**
+ * Execution time as claimed by the merchant
+ */
+ executionTime: DbProtocolTimestamp;
+
+ /**
+ * Time when the wallet became aware of the refund.
+ */
+ obtainedTime: DbPreciseTimestamp;
+
+ refundAmount: AmountString;
+
+ coinPub: string;
+
+ rtxid: number;
+}
+
+export function passthroughCodec<T>(): Codec<T> {
+ return codecForAny();
+}
+
+export interface GlobalCurrencyAuditorRecord {
+ id?: number;
+ currency: string;
+ auditorBaseUrl: string;
+ auditorPub: string;
+}
+
+export interface GlobalCurrencyExchangeRecord {
+ id?: number;
+ currency: string;
+ exchangeBaseUrl: string;
+ exchangeMasterPub: string;
+}
+
+/**
+ * Primary key: transactionItem.transactionId
+ */
+export interface TransactionRecord {
+ /**
+ * Transaction item returned to the client.
+ */
+ transactionItem: Transaction;
+
+ /**
+ * Exchanges involved in the transaction.
+ */
+ exchanges: string[];
+
+ currency: string;
+}
+
+export enum DenomLossStatus {
+ /**
+ * Done indicates that the loss happened.
+ */
+ Done = 0x0500_0000,
+
+ /**
+ * Aborted in the sense that the loss was reversed.
+ */
+ Aborted = 0x0503_0001,
+}
+
+export interface DenomLossEventRecord {
+ denomLossEventId: string;
+ currency: string;
+ denomPubHashes: string[];
+ status: DenomLossStatus;
+ timestampCreated: DbPreciseTimestamp;
+ amount: string;
+ eventType: DenomLossEventType;
+ exchangeBaseUrl: string;
}
/**
@@ -1866,6 +2397,69 @@ export interface UserAttentionRecord {
* wallet database.
*/
export const WalletStoresV1 = {
+ denomLossEvents: describeStoreV2({
+ recordCodec: passthroughCodec<DenomLossEventRecord>(),
+ storeName: "denomLossEvents",
+ keyPath: "denomLossEventId",
+ versionAdded: 9,
+ indexes: {
+ byCurrency: describeIndex("byCurrency", "currency", {
+ versionAdded: 9,
+ }),
+ byStatus: describeIndex("byStatus", "status", {
+ versionAdded: 10,
+ }),
+ },
+ }),
+ transactions: describeStoreV2({
+ recordCodec: passthroughCodec<TransactionRecord>(),
+ storeName: "transactions",
+ keyPath: "transactionItem.transactionId",
+ versionAdded: 7,
+ indexes: {
+ byCurrency: describeIndex("byCurrency", "currency", {
+ versionAdded: 7,
+ }),
+ byExchange: describeIndex("byExchange", "exchanges", {
+ versionAdded: 7,
+ multiEntry: true,
+ }),
+ },
+ }),
+ globalCurrencyAuditors: describeStoreV2({
+ recordCodec: passthroughCodec<GlobalCurrencyAuditorRecord>(),
+ storeName: "globalCurrencyAuditors",
+ keyPath: "id",
+ autoIncrement: true,
+ versionAdded: 3,
+ indexes: {
+ byCurrencyAndUrlAndPub: describeIndex(
+ "byCurrencyAndUrlAndPub",
+ ["currency", "auditorBaseUrl", "auditorPub"],
+ {
+ unique: true,
+ versionAdded: 4,
+ },
+ ),
+ },
+ }),
+ globalCurrencyExchanges: describeStoreV2({
+ recordCodec: passthroughCodec<GlobalCurrencyExchangeRecord>(),
+ storeName: "globalCurrencyExchanges",
+ keyPath: "id",
+ autoIncrement: true,
+ versionAdded: 3,
+ indexes: {
+ byCurrencyAndUrlAndPub: describeIndex(
+ "byCurrencyAndUrlAndPub",
+ ["currency", "exchangeBaseUrl", "exchangeMasterPub"],
+ {
+ unique: true,
+ versionAdded: 4,
+ },
+ ),
+ },
+ }),
coinAvailability: describeStore(
"coinAvailability",
describeContents<CoinAvailabilityRecord>({
@@ -1877,6 +2471,9 @@ export const WalletStoresV1 = {
"maxAge",
"freshCoinCount",
]),
+ byExchangeBaseUrl: describeIndex("byExchangeBaseUrl", "exchangeBaseUrl", {
+ versionAdded: 8,
+ }),
},
),
coins: describeStore(
@@ -1892,6 +2489,13 @@ export const WalletStoresV1 = {
["exchangeBaseUrl", "denomPubHash", "maxAge", "status"],
),
byCoinEvHash: describeIndex("byCoinEvHash", "coinEvHash"),
+ bySourceTransactionId: describeIndex(
+ "bySourceTransactionId",
+ "sourceTransactionId",
+ {
+ versionAdded: 9,
+ },
+ ),
},
),
reserves: describeStore(
@@ -1904,42 +2508,11 @@ export const WalletStoresV1 = {
byReservePub: describeIndex("byReservePub", "reservePub", {}),
},
),
- exchangeTos: describeStore(
- "exchangeTos",
- describeContents<ExchangeTosRecord>({
- keyPath: ["exchangeBaseUrl", "etag"],
- }),
- {},
- ),
config: describeStore(
"config",
describeContents<ConfigRecord>({ keyPath: "key" }),
{},
),
- auditorTrust: describeStore(
- "auditorTrust",
- describeContents<AuditorTrustRecord>({
- keyPath: ["currency", "auditorBaseUrl"],
- }),
- {
- byAuditorPub: describeIndex("byAuditorPub", "auditorPub"),
- byUid: describeIndex("byUid", "uids", {
- multiEntry: true,
- }),
- },
- ),
- exchangeTrust: describeStore(
- "exchangeTrust",
- describeContents<ExchangeTrustRecord>({
- keyPath: ["currency", "exchangeBaseUrl"],
- }),
- {
- byExchangeMasterPub: describeIndex(
- "byExchangeMasterPub",
- "exchangeMasterPub",
- ),
- },
- ),
denominations: describeStore(
"denominations",
describeContents<DenominationRecord>({
@@ -1951,7 +2524,7 @@ export const WalletStoresV1 = {
),
exchanges: describeStore(
"exchanges",
- describeContents<ExchangeRecord>({
+ describeContents<ExchangeEntryRecord>({
keyPath: "baseUrl",
}),
{},
@@ -1963,6 +2536,9 @@ export const WalletStoresV1 = {
autoIncrement: true,
}),
{
+ byExchangeBaseUrl: describeIndex("byExchangeBaseUrl", "exchangeBaseUrl", {
+ versionAdded: 2,
+ }),
byPointer: describeIndex(
"byDetailsPointer",
["exchangeBaseUrl", "currency", "masterPublicKey"],
@@ -1972,7 +2548,7 @@ export const WalletStoresV1 = {
),
},
),
- exchangeSignkeys: describeStore(
+ exchangeSignKeys: describeStore(
"exchangeSignKeys",
describeContents<ExchangeSignkeysRecord>({
keyPath: ["exchangeDetailsRowId", "signkeyPub"],
@@ -1990,14 +2566,32 @@ export const WalletStoresV1 = {
}),
{
byStatus: describeIndex("byStatus", "operationStatus"),
+ byOriginatingTransactionId: describeIndex(
+ "byOriginatingTransactionId",
+ "originatingTransactionId",
+ {
+ versionAdded: 5,
+ },
+ ),
},
),
+ refreshSessions: describeStore(
+ "refreshSessions",
+ describeContents<RefreshSessionRecord>({
+ keyPath: ["refreshGroupId", "coinIndex"],
+ }),
+ {},
+ ),
recoupGroups: describeStore(
"recoupGroups",
describeContents<RecoupGroupRecord>({
keyPath: "recoupGroupId",
}),
- {},
+ {
+ byStatus: describeIndex("byStatus", "operationStatus", {
+ versionAdded: 6,
+ }),
+ },
),
purchases: describeStore(
"purchases",
@@ -2014,14 +2608,17 @@ export const WalletStoresV1 = {
]),
},
),
- tips: describeStore(
- "tips",
- describeContents<TipRecord>({ keyPath: "walletTipId" }),
+ rewards: describeStore(
+ "rewards",
+ describeContents<RewardRecord>({ keyPath: "walletRewardId" }),
{
- byMerchantTipIdAndBaseUrl: describeIndex("byMerchantTipIdAndBaseUrl", [
- "merchantTipId",
+ byMerchantTipIdAndBaseUrl: describeIndex("byMerchantRewardIdAndBaseUrl", [
+ "merchantRewardId",
"merchantBaseUrl",
]),
+ byStatus: describeIndex("byStatus", "status", {
+ versionAdded: 8,
+ }),
},
),
withdrawalGroups: describeStore(
@@ -2031,6 +2628,9 @@ export const WalletStoresV1 = {
}),
{
byStatus: describeIndex("byStatus", "status"),
+ byExchangeBaseUrl: describeIndex("byExchangeBaseUrl", "exchangeBaseUrl", {
+ versionAdded: 2,
+ }),
byTalerWithdrawUri: describeIndex(
"byTalerWithdrawUri",
"wgInfo.bankInfo.talerWithdrawUri",
@@ -2041,10 +2641,13 @@ export const WalletStoresV1 = {
"planchets",
describeContents<PlanchetRecord>({ keyPath: "coinPub" }),
{
- byGroupAndIndex: describeIndex("byGroupAndIndex", [
- "withdrawalGroupId",
- "coinIdx",
- ]),
+ byGroupAndIndex: describeIndex(
+ "byGroupAndIndex",
+ ["withdrawalGroupId", "coinIdx"],
+ {
+ unique: true,
+ },
+ ),
byGroup: describeIndex("byGroup", "withdrawalGroupId"),
byCoinEvHash: describeIndex("byCoinEv", "coinEvHash"),
},
@@ -2092,51 +2695,68 @@ export const WalletStoresV1 = {
}),
{},
),
- ghostDepositGroups: describeStore(
- "ghostDepositGroups",
- describeContents<GhostDepositGroupRecord>({
- keyPath: "contractTermsHash",
- }),
- {},
- ),
- peerPushPaymentIncoming: describeStore(
- "peerPushPaymentIncoming",
+ peerPushCredit: describeStore(
+ "peerPushCredit",
describeContents<PeerPushPaymentIncomingRecord>({
- keyPath: "peerPushPaymentIncomingId",
+ keyPath: "peerPushCreditId",
}),
{
byExchangeAndPurse: describeIndex("byExchangeAndPurse", [
"exchangeBaseUrl",
"pursePub",
]),
+ byExchangeAndContractPriv: describeIndex(
+ "byExchangeAndContractPriv",
+ ["exchangeBaseUrl", "contractPriv"],
+ {
+ unique: true,
+ },
+ ),
+ byWithdrawalGroupId: describeIndex(
+ "byWithdrawalGroupId",
+ "withdrawalGroupId",
+ {},
+ ),
byStatus: describeIndex("byStatus", "status"),
},
),
- peerPullPaymentIncoming: describeStore(
- "peerPullPaymentIncoming",
+ peerPullDebit: describeStore(
+ "peerPullDebit",
describeContents<PeerPullPaymentIncomingRecord>({
- keyPath: "peerPullPaymentIncomingId",
+ keyPath: "peerPullDebitId",
}),
{
byExchangeAndPurse: describeIndex("byExchangeAndPurse", [
"exchangeBaseUrl",
"pursePub",
]),
+ byExchangeAndContractPriv: describeIndex(
+ "byExchangeAndContractPriv",
+ ["exchangeBaseUrl", "contractPriv"],
+ {
+ unique: true,
+ },
+ ),
byStatus: describeIndex("byStatus", "status"),
},
),
- peerPullPaymentInitiations: describeStore(
- "peerPullPaymentInitiations",
- describeContents<PeerPullPaymentInitiationRecord>({
+ peerPullCredit: describeStore(
+ "peerPullCredit",
+ describeContents<PeerPullCreditRecord>({
keyPath: "pursePub",
}),
{
byStatus: describeIndex("byStatus", "status"),
+ byWithdrawalGroupId: describeIndex(
+ "byWithdrawalGroupId",
+ "withdrawalGroupId",
+ {},
+ ),
},
),
- peerPushPaymentInitiations: describeStore(
- "peerPushPaymentInitiations",
- describeContents<PeerPushPaymentInitiationRecord>({
+ peerPushDebit: describeStore(
+ "peerPushDebit",
+ describeContents<PeerPushDebitRecord>({
keyPath: "pursePub",
}),
{
@@ -2164,8 +2784,65 @@ export const WalletStoresV1 = {
}),
{},
),
+ refundGroups: describeStore(
+ "refundGroups",
+ describeContents<RefundGroupRecord>({
+ keyPath: "refundGroupId",
+ }),
+ {
+ byProposalId: describeIndex("byProposalId", "proposalId"),
+ byStatus: describeIndex("byStatus", "status", {}),
+ },
+ ),
+ refundItems: describeStore(
+ "refundItems",
+ describeContents<RefundItemRecord>({
+ keyPath: "id",
+ autoIncrement: true,
+ }),
+ {
+ byCoinPubAndRtxid: describeIndex("byCoinPubAndRtxid", [
+ "coinPub",
+ "rtxid",
+ ]),
+ // FIXME: Why is this a list of index keys? Confusing!
+ byRefundGroupId: describeIndex("byRefundGroupId", ["refundGroupId"]),
+ },
+ ),
+ fixups: describeStore(
+ "fixups",
+ describeContents<FixupRecord>({
+ keyPath: "fixupName",
+ }),
+ {},
+ ),
};
+export type WalletDbStoresArr = Array<StoreNames<typeof WalletStoresV1>>;
+
+export type WalletDbReadWriteTransaction<StoresArr extends WalletDbStoresArr> =
+ DbReadWriteTransaction<typeof WalletStoresV1, StoresArr>;
+
+export type WalletDbReadOnlyTransaction<StoresArr extends WalletDbStoresArr> =
+ DbReadOnlyTransaction<typeof WalletStoresV1, StoresArr>;
+
+export type WalletDbAllStoresReadOnlyTransaction<> = DbReadOnlyTransaction<
+ typeof WalletStoresV1,
+ WalletDbStoresArr
+>;
+
+export type WalletDbAllStoresReadWriteTransaction<> = DbReadWriteTransaction<
+ typeof WalletStoresV1,
+ WalletDbStoresArr
+>;
+
+/**
+ * An applied migration.
+ */
+export interface FixupRecord {
+ fixupName: string;
+}
+
/**
* User accounts
*/
@@ -2189,112 +2866,512 @@ export const walletMetadataStore = {
),
};
-export function exportDb(db: IDBDatabase): Promise<any> {
- const dump = {
- name: db.name,
- stores: {} as { [s: string]: any },
- version: db.version,
+export interface StoredBackupMeta {
+ name: string;
+}
+
+export const StoredBackupStores = {
+ backupMeta: describeStore(
+ "backupMeta",
+ describeContents<StoredBackupMeta>({ keyPath: "name" }),
+ {},
+ ),
+ backupData: describeStore("backupData", describeContents<any>({}), {}),
+};
+
+export interface DbDumpRecord {
+ /**
+ * Key, serialized with structuredEncapsulated.
+ *
+ * Only present for out-of-line keys (i.e. no key path).
+ */
+ key?: any;
+ /**
+ * Value, serialized with structuredEncapsulated.
+ */
+ value: any;
+}
+
+export interface DbIndexDump {
+ keyPath: string | string[];
+ multiEntry: boolean;
+ unique: boolean;
+}
+
+export interface DbStoreDump {
+ keyPath?: string | string[];
+ autoIncrement: boolean;
+ indexes: { [indexName: string]: DbIndexDump };
+ records: DbDumpRecord[];
+}
+
+export interface DbDumpDatabase {
+ version: number;
+ stores: { [storeName: string]: DbStoreDump };
+}
+
+export interface DbDump {
+ databases: {
+ [name: string]: DbDumpDatabase;
+ };
+}
+
+export async function exportSingleDb(
+ idb: IDBFactory,
+ dbName: string,
+): Promise<DbDumpDatabase> {
+ const myDb = await openDatabase(
+ idb,
+ dbName,
+ undefined,
+ () => {
+ logger.info(`unexpected onversionchange in exportSingleDb of ${dbName}`);
+ },
+ () => {
+ logger.info(`unexpected onupgradeneeded in exportSingleDb of ${dbName}`);
+ },
+ );
+
+ const singleDbDump: DbDumpDatabase = {
+ version: myDb.version,
+ stores: {},
};
return new Promise((resolve, reject) => {
- const tx = db.transaction(Array.from(db.objectStoreNames));
+ const tx = myDb.transaction(Array.from(myDb.objectStoreNames));
tx.addEventListener("complete", () => {
- resolve(dump);
+ //myDb.close();
+ resolve(singleDbDump);
});
// tslint:disable-next-line:prefer-for-of
- for (let i = 0; i < db.objectStoreNames.length; i++) {
- const name = db.objectStoreNames[i];
- const storeDump = {} as { [s: string]: any };
- dump.stores[name] = storeDump;
- tx.objectStore(name)
- .openCursor()
- .addEventListener("success", (e: Event) => {
- const cursor = (e.target as any).result;
- if (cursor) {
- storeDump[cursor.key] = cursor.value;
- cursor.continue();
+ for (let i = 0; i < myDb.objectStoreNames.length; i++) {
+ const name = myDb.objectStoreNames[i];
+ const store = tx.objectStore(name);
+ const storeDump: DbStoreDump = {
+ autoIncrement: store.autoIncrement,
+ keyPath: store.keyPath,
+ indexes: {},
+ records: [],
+ };
+ const indexNames = store.indexNames;
+ for (let j = 0; j < indexNames.length; j++) {
+ const idxName = indexNames[j];
+ const index = store.index(idxName);
+ storeDump.indexes[idxName] = {
+ keyPath: index.keyPath,
+ multiEntry: index.multiEntry,
+ unique: index.unique,
+ };
+ }
+ singleDbDump.stores[name] = storeDump;
+ store.openCursor().addEventListener("success", (e: Event) => {
+ const cursor = (e.target as any).result;
+ if (cursor) {
+ const rec: DbDumpRecord = {
+ value: structuredEncapsulate(cursor.value),
+ };
+ // Only store key if necessary, i.e. when
+ // the key is not stored as part of the object via
+ // a key path.
+ if (store.keyPath == null) {
+ rec.key = structuredEncapsulate(cursor.key);
}
- });
+ storeDump.records.push(rec);
+ cursor.continue();
+ }
+ });
}
});
}
-export interface DatabaseDump {
- name: string;
- stores: { [s: string]: any };
- version: string;
+export async function exportDb(idb: IDBFactory): Promise<DbDump> {
+ const dbDump: DbDump = {
+ databases: {},
+ };
+
+ dbDump.databases[TALER_WALLET_META_DB_NAME] = await exportSingleDb(
+ idb,
+ TALER_WALLET_META_DB_NAME,
+ );
+ dbDump.databases[TALER_WALLET_MAIN_DB_NAME] = await exportSingleDb(
+ idb,
+ TALER_WALLET_MAIN_DB_NAME,
+ );
+
+ return dbDump;
}
async function recoverFromDump(
db: IDBDatabase,
- dump: DatabaseDump,
+ dbDump: DbDumpDatabase,
): Promise<void> {
- return new Promise((resolve, reject) => {
- const tx = db.transaction(Array.from(db.objectStoreNames), "readwrite");
- tx.addEventListener("complete", () => {
- resolve();
- });
- for (let i = 0; i < db.objectStoreNames.length; i++) {
- const name = db.objectStoreNames[i];
- const storeDump = dump.stores[name];
- if (!storeDump) continue;
- Object.keys(storeDump).forEach(async (key) => {
- const value = storeDump[key];
- if (!value) return;
- tx.objectStore(name).put(value);
+ const tx = db.transaction(Array.from(db.objectStoreNames), "readwrite");
+ const txProm = promiseFromTransaction(tx);
+ const storeNames = db.objectStoreNames;
+ for (let i = 0; i < storeNames.length; i++) {
+ const name = db.objectStoreNames[i];
+ const storeDump = dbDump.stores[name];
+ if (!storeDump) continue;
+ await promiseFromRequest(tx.objectStore(name).clear());
+ logger.info(`importing ${storeDump.records.length} records into ${name}`);
+ for (let rec of storeDump.records) {
+ await promiseFromRequest(tx.objectStore(name).put(rec.value, rec.key));
+ logger.info("importing record done");
+ }
+ }
+ tx.commit();
+ return await txProm;
+}
+
+function checkDbDump(x: any): x is DbDump {
+ return "databases" in x;
+}
+
+export async function importDb(db: IDBDatabase, dumpJson: any): Promise<void> {
+ const d = structuredRevive(dumpJson);
+ if (checkDbDump(d)) {
+ const walletDb = d.databases[TALER_WALLET_MAIN_DB_NAME];
+ if (!walletDb) {
+ throw Error(
+ `unable to import, main wallet database (${TALER_WALLET_MAIN_DB_NAME}) not found`,
+ );
+ }
+ await recoverFromDump(db, walletDb);
+ } else {
+ throw Error("unable to import, doesn't look like a valid DB dump");
+ }
+}
+
+export interface FixupDescription {
+ name: string;
+ fn(
+ tx: DbReadWriteTransaction<
+ typeof WalletStoresV1,
+ Array<StoreNames<typeof WalletStoresV1>>
+ >,
+ ): Promise<void>;
+}
+
+/**
+ * Manual migrations between minor versions of the DB schema.
+ */
+export const walletDbFixups: FixupDescription[] = [];
+
+const logger = new Logger("db.ts");
+
+export async function applyFixups(
+ db: DbAccess<typeof WalletStoresV1>,
+): Promise<void> {
+ logger.trace("applying fixups");
+ await db.runAllStoresReadWriteTx({}, async (tx) => {
+ for (const fixupInstruction of walletDbFixups) {
+ logger.trace(`checking fixup ${fixupInstruction.name}`);
+ const fixupRecord = await tx.fixups.get(fixupInstruction.name);
+ if (fixupRecord) {
+ continue;
+ }
+ logger.info(`applying DB fixup ${fixupInstruction.name}`);
+ await fixupInstruction.fn(tx);
+ await tx.fixups.put({
+ fixupName: fixupInstruction.name,
});
}
- tx.commit();
});
}
-export async function importDb(db: IDBDatabase, object: any): Promise<void> {
- if ("name" in object && "stores" in object && "version" in object) {
- // looks like a database dump
- const dump = object as DatabaseDump;
- return recoverFromDump(db, dump);
+/**
+ * Upgrade an IndexedDB in an upgrade transaction.
+ *
+ * The upgrade is made based on a store map, i.e. the metadata
+ * structure that describes all the object stores and indexes.
+ */
+function upgradeFromStoreMap(
+ storeMap: any, // FIXME: nail down type
+ db: IDBDatabase,
+ oldVersion: number,
+ newVersion: number,
+ upgradeTransaction: IDBTransaction,
+): void {
+ if (oldVersion === 0) {
+ for (const n in storeMap) {
+ const swi: StoreWithIndexes<
+ any,
+ StoreDescriptor<unknown>,
+ any
+ > = storeMap[n];
+ const storeDesc: StoreDescriptor<unknown> = swi.store;
+ const s = db.createObjectStore(swi.storeName, {
+ autoIncrement: storeDesc.autoIncrement,
+ keyPath: storeDesc.keyPath,
+ });
+ for (const indexName in swi.indexMap as any) {
+ const indexDesc: IndexDescriptor = swi.indexMap[indexName];
+ s.createIndex(indexDesc.name, indexDesc.keyPath, {
+ multiEntry: indexDesc.multiEntry,
+ unique: indexDesc.unique,
+ });
+ }
+ }
+ return;
}
+ if (oldVersion === newVersion) {
+ return;
+ }
+ logger.info(`upgrading database from ${oldVersion} to ${newVersion}`);
+ for (const n in storeMap) {
+ const swi: StoreWithIndexes<any, StoreDescriptor<unknown>, any> = storeMap[
+ n
+ ];
+ const storeDesc: StoreDescriptor<unknown> = swi.store;
+ const storeAddedVersion = storeDesc.versionAdded ?? 0;
+ let s: IDBObjectStore;
+ if (storeAddedVersion > oldVersion) {
+ // Be tolerant if object store already exists.
+ // Probably means somebody deployed without
+ // adding the "addedInVersion" attribute.
+ if (!upgradeTransaction.objectStoreNames.contains(swi.storeName)) {
+ try {
+ s = db.createObjectStore(swi.storeName, {
+ autoIncrement: storeDesc.autoIncrement,
+ keyPath: storeDesc.keyPath,
+ });
+ } catch (e) {
+ const moreInfo = e instanceof Error ? ` Reason: ${e.message}` : "";
+ throw new Error(
+ `Migration failed. Could not create store ${swi.storeName}.${moreInfo}`,
+ // @ts-expect-error no support for options.cause yet
+ { cause: e },
+ );
+ }
+ }
+ }
- if ("databases" in object && "$types" in object) {
- // looks like a IDBDatabase
- const someDatabase = object.databases;
-
- if (TALER_META_DB_NAME in someDatabase) {
- //looks like a taler database
- const currentMainDbValue =
- someDatabase[TALER_META_DB_NAME].objectStores.metaConfig.records[0]
- .value.value;
+ s = upgradeTransaction.objectStore(swi.storeName);
- if (currentMainDbValue !== TALER_DB_NAME) {
- console.log("not the current database version");
+ for (const indexName in swi.indexMap as any) {
+ const indexDesc: IndexDescriptor = swi.indexMap[indexName];
+ const indexAddedVersion = indexDesc.versionAdded ?? 0;
+ if (indexAddedVersion <= oldVersion) {
+ continue;
+ }
+ // Be tolerant if index already exists.
+ // Probably means somebody deployed without
+ // adding the "addedInVersion" attribute.
+ if (!s.indexNames.contains(indexDesc.name)) {
+ try {
+ s.createIndex(indexDesc.name, indexDesc.keyPath, {
+ multiEntry: indexDesc.multiEntry,
+ unique: indexDesc.unique,
+ });
+ } catch (e) {
+ const moreInfo = e instanceof Error ? ` Reason: ${e.message}` : "";
+ throw Error(
+ `Migration failed. Could not create index ${indexDesc.name}/${indexDesc.keyPath}. ${moreInfo}`,
+ // @ts-expect-error no support for options.cause yet
+ { cause: e },
+ );
+ }
}
+ }
+ }
+}
- const talerDb = someDatabase[currentMainDbValue];
+function promiseFromTransaction(transaction: IDBTransaction): Promise<void> {
+ return new Promise<void>((resolve, reject) => {
+ transaction.oncomplete = () => {
+ resolve();
+ };
+ transaction.onerror = () => {
+ reject();
+ };
+ });
+}
- const objectStoreNames = Object.keys(talerDb.objectStores);
+export function promiseFromRequest(request: IDBRequest): Promise<any> {
+ return new Promise((resolve, reject) => {
+ request.onsuccess = () => {
+ resolve(request.result);
+ };
+ request.onerror = () => {
+ reject(request.error);
+ };
+ });
+}
- const dump: DatabaseDump = {
- name: talerDb.schema.databaseName,
- version: talerDb.schema.databaseVersion,
- stores: {},
- };
+/**
+ * Purge all data in the given database.
+ */
+export function clearDatabase(db: IDBDatabase): Promise<void> {
+ // db.objectStoreNames is a DOMStringList, so we need to convert
+ let stores: string[] = [];
+ for (let i = 0; i < db.objectStoreNames.length; i++) {
+ stores.push(db.objectStoreNames[i]);
+ }
+ const tx = db.transaction(stores, "readwrite");
+ for (const store of stores) {
+ tx.objectStore(store).clear();
+ }
+ return promiseFromTransaction(tx);
+}
- for (let i = 0; i < objectStoreNames.length; i++) {
- const name = objectStoreNames[i];
- const storeDump = {} as { [s: string]: any };
- dump.stores[name] = storeDump;
- talerDb.objectStores[name].records.map((r: any) => {
- const pkey = r.primaryKey;
- const key =
- typeof pkey === "string" || typeof pkey === "number"
- ? pkey
- : pkey.join(",");
- storeDump[key] = r.value;
- });
- }
+function onTalerDbUpgradeNeeded(
+ db: IDBDatabase,
+ oldVersion: number,
+ newVersion: number,
+ upgradeTransaction: IDBTransaction,
+) {
+ upgradeFromStoreMap(
+ WalletStoresV1,
+ db,
+ oldVersion,
+ newVersion,
+ upgradeTransaction,
+ );
+}
+
+function onMetaDbUpgradeNeeded(
+ db: IDBDatabase,
+ oldVersion: number,
+ newVersion: number,
+ upgradeTransaction: IDBTransaction,
+) {
+ upgradeFromStoreMap(
+ walletMetadataStore,
+ db,
+ oldVersion,
+ newVersion,
+ upgradeTransaction,
+ );
+}
+
+function onStoredBackupsDbUpgradeNeeded(
+ db: IDBDatabase,
+ oldVersion: number,
+ newVersion: number,
+ upgradeTransaction: IDBTransaction,
+) {
+ upgradeFromStoreMap(
+ StoredBackupStores,
+ db,
+ oldVersion,
+ newVersion,
+ upgradeTransaction,
+ );
+}
+
+export async function openStoredBackupsDatabase(
+ idbFactory: IDBFactory,
+): Promise<DbAccess<typeof StoredBackupStores>> {
+ const backupsDbHandle = await openDatabase(
+ idbFactory,
+ TALER_WALLET_STORED_BACKUPS_DB_NAME,
+ 1,
+ () => {},
+ onStoredBackupsDbUpgradeNeeded,
+ );
+
+ const handle = new DbAccessImpl(
+ backupsDbHandle,
+ StoredBackupStores,
+ {},
+ CancellationToken.CONTINUE,
+ );
+ return handle;
+}
+
+/**
+ * Return a promise that resolves
+ * to the taler wallet db.
+ *
+ * @param onVersionChange Called when another client concurrenctly connects to the database
+ * with a higher version.
+ */
+export async function openTalerDatabase(
+ idbFactory: IDBFactory,
+ onVersionChange: () => void,
+): Promise<IDBDatabase> {
+ const metaDbHandle = await openDatabase(
+ idbFactory,
+ TALER_WALLET_META_DB_NAME,
+ 1,
+ () => {},
+ onMetaDbUpgradeNeeded,
+ );
+
+ const metaDb = new DbAccessImpl(
+ metaDbHandle,
+ walletMetadataStore,
+ {},
+ CancellationToken.CONTINUE,
+ );
+ let currentMainVersion: string | undefined;
+ await metaDb.runReadWriteTx({ storeNames: ["metaConfig"] }, async (tx) => {
+ const dbVersionRecord = await tx.metaConfig.get(CURRENT_DB_CONFIG_KEY);
+ if (!dbVersionRecord) {
+ currentMainVersion = TALER_WALLET_MAIN_DB_NAME;
+ await tx.metaConfig.put({
+ key: CURRENT_DB_CONFIG_KEY,
+ value: TALER_WALLET_MAIN_DB_NAME,
+ });
+ } else {
+ currentMainVersion = dbVersionRecord.value;
+ }
+ });
- return recoverFromDump(db, dump);
+ if (currentMainVersion !== TALER_WALLET_MAIN_DB_NAME) {
+ switch (currentMainVersion) {
+ case "taler-wallet-main-v2":
+ case "taler-wallet-main-v3":
+ case "taler-wallet-main-v4": // temporary, we might migrate v4 later
+ case "taler-wallet-main-v5":
+ case "taler-wallet-main-v6":
+ case "taler-wallet-main-v7":
+ case "taler-wallet-main-v8":
+ case "taler-wallet-main-v9":
+ // We consider this a pre-release
+ // development version, no migration is done.
+ await metaDb.runReadWriteTx(
+ { storeNames: ["metaConfig"] },
+ async (tx) => {
+ await tx.metaConfig.put({
+ key: CURRENT_DB_CONFIG_KEY,
+ value: TALER_WALLET_MAIN_DB_NAME,
+ });
+ },
+ );
+ break;
+ default:
+ throw Error(
+ `major migration from database major=${currentMainVersion} not supported`,
+ );
}
}
- throw Error("could not import database");
+
+ const mainDbHandle = await openDatabase(
+ idbFactory,
+ TALER_WALLET_MAIN_DB_NAME,
+ WALLET_DB_MINOR_VERSION,
+ onVersionChange,
+ onTalerDbUpgradeNeeded,
+ );
+
+ const mainDbAccess = new DbAccessImpl(
+ mainDbHandle,
+ WalletStoresV1,
+ {},
+ CancellationToken.CONTINUE,
+ );
+ await applyFixups(mainDbAccess);
+
+ return mainDbHandle;
+}
+
+export async function deleteTalerDatabase(
+ idbFactory: IDBFactory,
+): Promise<void> {
+ return new Promise((resolve, reject) => {
+ const req = idbFactory.deleteDatabase(TALER_WALLET_MAIN_DB_NAME);
+ req.onerror = () => reject(req.error);
+ req.onsuccess = () => resolve();
+ });
}
diff --git a/packages/taler-wallet-core/src/dbless.ts b/packages/taler-wallet-core/src/dbless.ts
index 544e2d458..dfefe6ef5 100644
--- a/packages/taler-wallet-core/src/dbless.ts
+++ b/packages/taler-wallet-core/src/dbless.ts
@@ -29,43 +29,39 @@ import {
AbsoluteTime,
AgeRestriction,
AmountJson,
- Amounts,
AmountString,
+ Amounts,
+ DenominationPubKey,
+ ExchangeBatchDepositRequest,
+ ExchangeBatchWithdrawRequest,
+ ExchangeMeltRequest,
+ ExchangeProtocolVersion,
+ Logger,
+ TalerCorebankApiClient,
+ UnblindedSignature,
codecForAny,
codecForBankWithdrawalOperationPostResponse,
- codecForDepositSuccess,
+ codecForBatchDepositSuccess,
codecForExchangeMeltResponse,
codecForExchangeRevealResponse,
- codecForWithdrawResponse,
- DenominationPubKey,
+ codecForExchangeWithdrawBatchResponse,
encodeCrock,
- ExchangeMeltRequest,
- ExchangeProtocolVersion,
- ExchangeWithdrawRequest,
getRandomBytes,
hashWire,
- Logger,
parsePaytoUri,
- UnblindedSignature,
} from "@gnu-taler/taler-util";
-import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js";
-import { DenominationRecord } from "./db.js";
-import {
- BankAccessApi,
- BankApi,
- BankServiceHandle,
-} from "./bank-api-client.js";
import {
HttpRequestLibrary,
readSuccessResponseJsonOrThrow,
-} from "./util/http.js";
-import {
- getBankStatusUrl,
- getBankWithdrawalInfo,
- isWithdrawableDenom,
-} from "./operations/withdraw.js";
-import { ExchangeInfo } from "./operations/exchanges.js";
-import { assembleRefreshRevealRequest } from "./operations/refresh.js";
+} from "@gnu-taler/taler-util/http";
+import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js";
+import { DenominationRecord } from "./db.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 };
const logger = new Logger("dbless.ts");
@@ -103,29 +99,28 @@ export async function checkReserve(
if (longpollTimeoutMs) {
reqUrl.searchParams.set("timeout_ms", `${longpollTimeoutMs}`);
}
- const resp = await http.get(reqUrl.href);
+ const resp = await http.fetch(reqUrl.href, { method: "GET" });
if (resp.status !== 200) {
throw new Error("reserve not okay");
}
}
-export async function topupReserveWithDemobank(
- http: HttpRequestLibrary,
- reservePub: string,
- bankBaseUrl: string,
- bankAccessApiBaseUrl: string,
- exchangeInfo: ExchangeInfo,
- amount: AmountString,
+export interface TopupReserveWithBankArgs {
+ http: HttpRequestLibrary;
+ reservePub: string;
+ corebankApiBaseUrl: string;
+ exchangeInfo: ExchangeInfo;
+ amount: AmountString;
+}
+
+export async function topupReserveWithBank(
+ args: TopupReserveWithBankArgs,
) {
- const bankHandle: BankServiceHandle = {
- baseUrl: bankBaseUrl,
- bankAccessApiBaseUrl: bankAccessApiBaseUrl,
- http,
- };
- const bankUser = await BankApi.createRandomBankUser(bankHandle);
- const wopi = await BankAccessApi.createWithdrawalOperation(
- bankHandle,
- bankUser,
+ const { http, corebankApiBaseUrl, amount, exchangeInfo, reservePub } = args;
+ const bankClient = new TalerCorebankApiClient(corebankApiBaseUrl);
+ const bankUser = await bankClient.createRandomBankUser();
+ const wopi = await bankClient.createWithdrawalOperation(
+ bankUser.username,
amount,
);
const bankInfo = await getBankWithdrawalInfo(http, wopi.taler_withdraw_uri);
@@ -134,19 +129,24 @@ export async function topupReserveWithDemobank(
throw Error("no suggested exchange");
}
const plainPaytoUris =
- exchangeInfo.wire.accounts.map((x) => x.payto_uri) ?? [];
+ exchangeInfo.keys.accounts.map((x) => x.payto_uri) ?? [];
if (plainPaytoUris.length <= 0) {
throw new Error();
}
- const httpResp = await http.postJson(bankStatusUrl, {
- reserve_pub: reservePub,
- selected_exchange: plainPaytoUris[0],
+ const httpResp = await http.fetch(bankStatusUrl, {
+ method: "POST",
+ body: {
+ reserve_pub: reservePub,
+ selected_exchange: plainPaytoUris[0],
+ },
});
await readSuccessResponseJsonOrThrow(
httpResp,
codecForBankWithdrawalOperationPostResponse(),
);
- await BankApi.confirmWithdrawalOperation(bankHandle, bankUser, wopi);
+ await bankClient.confirmWithdrawalOperation(bankUser.username, {
+ withdrawalOperationId: wopi.withdrawal_id,
+ });
}
export async function withdrawCoin(args: {
@@ -164,32 +164,32 @@ export async function withdrawCoin(args: {
reservePriv: reserveKeyPair.reservePriv,
reservePub: reserveKeyPair.reservePub,
secretSeed: encodeCrock(getRandomBytes(32)),
- value: {
- currency: denom.currency,
- fraction: denom.amountFrac,
- value: denom.amountVal,
- },
+ value: Amounts.parseOrThrow(denom.value),
});
- const reqBody: ExchangeWithdrawRequest = {
- denom_pub_hash: planchet.denomPubHash,
- reserve_sig: planchet.withdrawSig,
- coin_ev: planchet.coinEv,
+ const reqBody: ExchangeBatchWithdrawRequest = {
+ planchets: [
+ {
+ denom_pub_hash: planchet.denomPubHash,
+ reserve_sig: planchet.withdrawSig,
+ coin_ev: planchet.coinEv,
+ },
+ ],
};
const reqUrl = new URL(
- `reserves/${planchet.reservePub}/withdraw`,
+ `reserves/${planchet.reservePub}/batch-withdraw`,
exchangeBaseUrl,
).href;
- const resp = await http.postJson(reqUrl, reqBody);
- const r = await readSuccessResponseJsonOrThrow(
+ const resp = await http.fetch(reqUrl, { method: "POST", body: reqBody });
+ const rBatch = await readSuccessResponseJsonOrThrow(
resp,
- codecForWithdrawResponse(),
+ codecForExchangeWithdrawBatchResponse(),
);
const ubSig = await cryptoApi.unblindDenominationSignature({
planchet,
- evSig: r.ev_sig,
+ evSig: rBatch.ev_sigs[0].ev_sig,
});
return {
@@ -205,17 +205,22 @@ export async function withdrawCoin(args: {
};
}
+export interface FindDenomOptions {
+ denomselAllowLate?: boolean;
+}
+
export function findDenomOrThrow(
exchangeInfo: ExchangeInfo,
amount: AmountString,
+ options: FindDenomOptions = {},
): DenominationRecord {
+ const denomselAllowLate = options.denomselAllowLate ?? false;
for (const d of exchangeInfo.keys.currentDenominations) {
- const value: AmountJson = {
- currency: d.currency,
- fraction: d.amountFrac,
- value: d.amountVal,
- };
- if (Amounts.cmp(value, amount) === 0 && isWithdrawableDenom(d)) {
+ const value: AmountJson = Amounts.parseOrThrow(d.value);
+ if (
+ Amounts.cmp(value, amount) === 0 &&
+ isWithdrawableDenom(d, denomselAllowLate)
+ ) {
return d;
}
}
@@ -229,17 +234,22 @@ export async function depositCoin(args: {
coin: CoinInfo;
amount: AmountString;
depositPayto?: string;
-}) {
+ merchantPub?: string;
+ contractTermsHash?: string;
+ // 16 bytes, crockford encoded
+ wireSalt?: string;
+}): Promise<void> {
const { coin, http, cryptoApi } = args;
const depositPayto =
- args.depositPayto ?? "payto://x-taler-bank/localhost/foo";
- const wireSalt = encodeCrock(getRandomBytes(16));
- const timestampNow = AbsoluteTime.toTimestamp(AbsoluteTime.now());
- const contractTermsHash = encodeCrock(getRandomBytes(64));
+ args.depositPayto ?? "payto://x-taler-bank/localhost/foo?receiver-name=foo";
+ const wireSalt = args.wireSalt ?? encodeCrock(getRandomBytes(16));
+ const timestampNow = AbsoluteTime.toProtocolTimestamp(AbsoluteTime.now());
+ const contractTermsHash =
+ args.contractTermsHash ?? encodeCrock(getRandomBytes(64));
const depositTimestamp = timestampNow;
const refundDeadline = timestampNow;
const wireTransferDeadline = timestampNow;
- const merchantPub = encodeCrock(getRandomBytes(32));
+ const merchantPub = args.merchantPub ?? encodeCrock(getRandomBytes(32));
const dp = await cryptoApi.signDepositPermission({
coinPriv: coin.coinPriv,
coinPub: coin.coinPub,
@@ -255,22 +265,30 @@ export async function depositCoin(args: {
refundDeadline: refundDeadline,
wireInfoHash: hashWire(depositPayto, wireSalt),
});
- const requestBody = {
- contribution: Amounts.stringify(dp.contribution),
+ const requestBody: ExchangeBatchDepositRequest = {
+ coins: [
+ {
+ contribution: Amounts.stringify(dp.contribution),
+ coin_pub: dp.coin_pub,
+ coin_sig: dp.coin_sig,
+ denom_pub_hash: dp.h_denom,
+ ub_sig: dp.ub_sig,
+ },
+ ],
merchant_payto_uri: depositPayto,
wire_salt: wireSalt,
h_contract_terms: contractTermsHash,
- ub_sig: coin.denomSig,
timestamp: depositTimestamp,
wire_transfer_deadline: wireTransferDeadline,
refund_deadline: refundDeadline,
- coin_sig: dp.coin_sig,
- denom_pub_hash: dp.h_denom,
merchant_pub: merchantPub,
};
- const url = new URL(`coins/${dp.coin_pub}/deposit`, dp.exchange_url);
- const httpResp = await http.postJson(url.href, requestBody);
- await readSuccessResponseJsonOrThrow(httpResp, codecForDepositSuccess());
+ const url = new URL(`batch-deposit`, dp.exchange_url);
+ const httpResp = await http.fetch(url.href, {
+ method: "POST",
+ body: requestBody,
+ });
+ await readSuccessResponseJsonOrThrow(httpResp, codecForBatchDepositSuccess());
}
export async function refreshCoin(req: {
@@ -294,11 +312,7 @@ export async function refreshCoin(req: {
denomPub: x.denomPub,
denomPubHash: x.denomPubHash,
feeWithdraw: x.fees.feeWithdraw,
- value: Amounts.stringify({
- currency: x.currency,
- fraction: x.amountFrac,
- value: x.amountVal,
- }),
+ value: x.value,
})),
meltCoinMaxAge: oldCoin.maxAge,
});
@@ -321,7 +335,10 @@ export async function refreshCoin(req: {
logger.info("requesting melt done");
- const meltHttpResp = await http.postJson(meltReqUrl.href, meltReqBody);
+ const meltHttpResp = await http.fetch(meltReqUrl.href, {
+ method: "POST",
+ body: meltReqBody,
+ });
const meltResponse = await readSuccessResponseJsonOrThrow(
meltHttpResp,
@@ -348,7 +365,10 @@ export async function refreshCoin(req: {
oldCoin.exchangeBaseUrl,
);
- const revealResp = await http.postJson(reqUrl.href, revealRequest);
+ const revealResp = await http.fetch(reqUrl.href, {
+ method: "POST",
+ body: revealRequest,
+ });
logger.info("requesting reveal done");
@@ -361,28 +381,39 @@ export async function refreshCoin(req: {
// benchmark the exchange.
}
-export async function createFakebankReserve(args: {
+/**
+ * Create a reserve for testing withdrawals.
+ *
+ * The reserve is created using the test-only API "/admin/add-incoming".
+ */
+export async function createTestingReserve(args: {
http: HttpRequestLibrary;
- fakebankBaseUrl: string;
+ corebankApiBaseUrl: string;
amount: string;
reservePub: string;
exchangeInfo: ExchangeInfo;
}): Promise<void> {
- const { http, fakebankBaseUrl, amount, reservePub } = args;
- const paytoUri = args.exchangeInfo.wire.accounts[0].payto_uri;
+ const { http, corebankApiBaseUrl, amount, reservePub } = args;
+ const paytoUri = args.exchangeInfo.keys.accounts[0].payto_uri;
const pt = parsePaytoUri(paytoUri);
if (!pt) {
throw Error("failed to parse payto URI");
}
const components = pt.targetPath.split("/");
const creditorAcct = components[components.length - 1];
- const fbReq = await http.postJson(
- new URL(`${creditorAcct}/admin/add-incoming`, fakebankBaseUrl).href,
+ const fbReq = await http.fetch(
+ new URL(
+ `accounts/${creditorAcct}/taler-wire-gateway/admin/add-incoming`,
+ corebankApiBaseUrl,
+ ).href,
{
- amount,
- reserve_pub: reservePub,
- debit_account: "payto://x-taler-bank/localhost/testdebtor",
+ method: "POST",
+ body: {
+ amount,
+ reserve_pub: reservePub,
+ debit_account: "payto://x-taler-bank/localhost/testdebtor",
+ },
},
);
- const fbResp = await readSuccessResponseJsonOrThrow(fbReq, codecForAny());
+ await readSuccessResponseJsonOrThrow(fbReq, codecForAny());
}
diff --git a/packages/taler-wallet-core/src/denomSelection.ts b/packages/taler-wallet-core/src/denomSelection.ts
new file mode 100644
index 000000000..ecc1fa881
--- /dev/null
+++ b/packages/taler-wallet-core/src/denomSelection.ts
@@ -0,0 +1,199 @@
+/*
+ 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/>
+ */
+
+/**
+ * Selection of denominations for withdrawals.
+ *
+ * @author Florian Dold
+ */
+
+/**
+ * Imports.
+ */
+import {
+ AbsoluteTime,
+ AmountJson,
+ Amounts,
+ DenomSelectionState,
+ ForcedDenomSel,
+ Logger,
+} from "@gnu-taler/taler-util";
+import { DenominationRecord, timestampAbsoluteFromDb } from "./db.js";
+import { isWithdrawableDenom } from "./denominations.js";
+
+const logger = new Logger("denomSelection.ts");
+
+/**
+ * Get a list of denominations (with repetitions possible)
+ * whose total value is as close as possible to the available
+ * amount, but never larger.
+ */
+export function selectWithdrawalDenominations(
+ amountAvailable: AmountJson,
+ denoms: DenominationRecord[],
+ denomselAllowLate: boolean = false,
+): DenomSelectionState {
+ let remaining = Amounts.copy(amountAvailable);
+
+ const selectedDenoms: {
+ count: number;
+ denomPubHash: string;
+ }[] = [];
+
+ let totalCoinValue = Amounts.zeroOfCurrency(amountAvailable.currency);
+ let totalWithdrawCost = Amounts.zeroOfCurrency(amountAvailable.currency);
+ let earliestDepositExpiration: AbsoluteTime | undefined;
+ let hasDenomWithAgeRestriction = false;
+
+ denoms = denoms.filter((d) => isWithdrawableDenom(d, denomselAllowLate));
+ denoms.sort((d1, d2) => Amounts.cmp(d2.value, d1.value));
+
+ if (logger.shouldLogTrace()) {
+ logger.trace(
+ `selecting withdrawal denoms for ${Amounts.stringify(amountAvailable)}`,
+ );
+ }
+
+ for (const d of denoms) {
+ const cost = Amounts.add(d.value, d.fees.feeWithdraw).amount;
+ const res = Amounts.divmod(remaining, cost);
+ const count = res.quotient;
+ remaining = Amounts.sub(remaining, Amounts.mult(cost, count).amount).amount;
+ if (count > 0) {
+ totalCoinValue = Amounts.add(
+ totalCoinValue,
+ Amounts.mult(d.value, count).amount,
+ ).amount;
+ totalWithdrawCost = Amounts.add(
+ totalWithdrawCost,
+ Amounts.mult(cost, count).amount,
+ ).amount;
+ selectedDenoms.push({
+ count,
+ denomPubHash: d.denomPubHash,
+ });
+ hasDenomWithAgeRestriction =
+ hasDenomWithAgeRestriction || d.denomPub.age_mask > 0;
+ const expireDeposit = timestampAbsoluteFromDb(d.stampExpireDeposit);
+ if (!earliestDepositExpiration) {
+ earliestDepositExpiration = expireDeposit;
+ } else {
+ earliestDepositExpiration = AbsoluteTime.min(
+ expireDeposit,
+ earliestDepositExpiration,
+ );
+ }
+ }
+
+ if (logger.shouldLogTrace()) {
+ logger.trace(
+ `denom_pub_hash=${
+ d.denomPubHash
+ }, count=${count}, val=${Amounts.stringify(
+ d.value,
+ )}, wdfee=${Amounts.stringify(d.fees.feeWithdraw)}`,
+ );
+ }
+
+ if (Amounts.isZero(remaining)) {
+ break;
+ }
+ }
+
+ if (logger.shouldLogTrace()) {
+ logger.trace("(end of denom selection)");
+ }
+
+ earliestDepositExpiration ??= AbsoluteTime.never();
+
+ return {
+ selectedDenoms,
+ totalCoinValue: Amounts.stringify(totalCoinValue),
+ totalWithdrawCost: Amounts.stringify(totalWithdrawCost),
+ earliestDepositExpiration: AbsoluteTime.toProtocolTimestamp(
+ earliestDepositExpiration,
+ ),
+ hasDenomWithAgeRestriction,
+ };
+}
+
+export function selectForcedWithdrawalDenominations(
+ amountAvailable: AmountJson,
+ denoms: DenominationRecord[],
+ forcedDenomSel: ForcedDenomSel,
+ denomselAllowLate: boolean,
+): DenomSelectionState {
+ const selectedDenoms: {
+ count: number;
+ denomPubHash: string;
+ }[] = [];
+
+ let totalCoinValue = Amounts.zeroOfCurrency(amountAvailable.currency);
+ let totalWithdrawCost = Amounts.zeroOfCurrency(amountAvailable.currency);
+ let earliestDepositExpiration: AbsoluteTime | undefined;
+ let hasDenomWithAgeRestriction = false;
+
+ denoms = denoms.filter((d) => isWithdrawableDenom(d, denomselAllowLate));
+ denoms.sort((d1, d2) => Amounts.cmp(d2.value, d1.value));
+
+ for (const fds of forcedDenomSel.denoms) {
+ const count = fds.count;
+ const denom = denoms.find((x) => {
+ return Amounts.cmp(x.value, fds.value) == 0;
+ });
+ if (!denom) {
+ throw Error(
+ `unable to find denom for forced selection (value ${fds.value})`,
+ );
+ }
+ const cost = Amounts.add(denom.value, denom.fees.feeWithdraw).amount;
+ totalCoinValue = Amounts.add(
+ totalCoinValue,
+ Amounts.mult(denom.value, count).amount,
+ ).amount;
+ totalWithdrawCost = Amounts.add(
+ totalWithdrawCost,
+ Amounts.mult(cost, count).amount,
+ ).amount;
+ selectedDenoms.push({
+ count,
+ denomPubHash: denom.denomPubHash,
+ });
+ hasDenomWithAgeRestriction =
+ hasDenomWithAgeRestriction || denom.denomPub.age_mask > 0;
+ const expireDeposit = timestampAbsoluteFromDb(denom.stampExpireDeposit);
+ if (!earliestDepositExpiration) {
+ earliestDepositExpiration = expireDeposit;
+ } else {
+ earliestDepositExpiration = AbsoluteTime.min(
+ expireDeposit,
+ earliestDepositExpiration,
+ );
+ }
+ }
+
+ earliestDepositExpiration ??= AbsoluteTime.never();
+
+ return {
+ selectedDenoms,
+ totalCoinValue: Amounts.stringify(totalCoinValue),
+ totalWithdrawCost: Amounts.stringify(totalWithdrawCost),
+ earliestDepositExpiration: AbsoluteTime.toProtocolTimestamp(
+ earliestDepositExpiration,
+ ),
+ hasDenomWithAgeRestriction,
+ };
+}
diff --git a/packages/taler-wallet-core/src/util/denominations.test.ts b/packages/taler-wallet-core/src/denominations.test.ts
index 551e06a33..98af5d1a4 100644
--- a/packages/taler-wallet-core/src/util/denominations.test.ts
+++ b/packages/taler-wallet-core/src/denominations.test.ts
@@ -39,10 +39,10 @@ import test, { ExecutionContext } from "ava";
* Create some constants to be used as reference in the tests
*/
const VALUES: AmountString[] = Array.from({ length: 10 }).map(
- (undef, t) => `USD:${t}`,
+ (undef, t) => `USD:${t}` as AmountString,
);
const TIMESTAMPS = Array.from({ length: 20 }).map((undef, t_s) => ({ t_s }));
-const ABS_TIME = TIMESTAMPS.map((m) => AbsoluteTime.fromTimestamp(m));
+const ABS_TIME = TIMESTAMPS.map((m) => AbsoluteTime.fromProtocolTimestamp(m));
function normalize(
list: DenominationInfo[],
diff --git a/packages/taler-wallet-core/src/util/denominations.ts b/packages/taler-wallet-core/src/denominations.ts
index ef35fe198..d41307d5d 100644
--- a/packages/taler-wallet-core/src/util/denominations.ts
+++ b/packages/taler-wallet-core/src/denominations.ts
@@ -14,18 +14,22 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+/**
+ * Imports.
+ */
import {
AbsoluteTime,
AmountJson,
Amounts,
AmountString,
DenominationInfo,
+ Duration,
FeeDescription,
FeeDescriptionPair,
TalerProtocolTimestamp,
TimePoint,
- WireFee,
} from "@gnu-taler/taler-util";
+import { DenominationRecord, timestampProtocolFromDb } from "./db.js";
/**
* Given a list of denominations with the same value and same period of time:
@@ -128,10 +132,10 @@ export function createPairTimeline(
//check which start after, add gap so both list starts at the same time
// one list may be empty
const leftStartTime: AbsoluteTime = leftGroupIsEmpty
- ? { t_ms: "never" }
+ ? AbsoluteTime.never()
: left[li].from;
const rightStartTime: AbsoluteTime = rightGroupIsEmpty
- ? { t_ms: "never" }
+ ? AbsoluteTime.never()
: right[ri].from;
//first time cut is the smallest time
@@ -322,7 +326,7 @@ export function createTimeline<Type extends object>(
fee: Amounts.stringify(fee),
group,
id,
- moment: AbsoluteTime.fromTimestamp(stampStart),
+ moment: AbsoluteTime.fromProtocolTimestamp(stampStart),
denom,
});
ps.push({
@@ -330,7 +334,7 @@ export function createTimeline<Type extends object>(
fee: Amounts.stringify(fee),
group,
id,
- moment: AbsoluteTime.fromTimestamp(stampEnd),
+ moment: AbsoluteTime.fromProtocolTimestamp(stampEnd),
denom,
});
return ps;
@@ -443,3 +447,33 @@ export function createTimeline<Type extends object>(
return result;
}, [] as FeeDescription[]);
}
+
+/**
+ * Check if a denom is withdrawable based on the expiration time,
+ * revocation and offered state.
+ */
+export function isWithdrawableDenom(
+ d: DenominationRecord,
+ denomselAllowLate?: boolean,
+): boolean {
+ const now = AbsoluteTime.now();
+ const start = AbsoluteTime.fromProtocolTimestamp(
+ timestampProtocolFromDb(d.stampStart),
+ );
+ const withdrawExpire = AbsoluteTime.fromProtocolTimestamp(
+ timestampProtocolFromDb(d.stampExpireWithdraw),
+ );
+ const started = AbsoluteTime.cmp(now, start) >= 0;
+ let lastPossibleWithdraw: AbsoluteTime;
+ if (denomselAllowLate) {
+ lastPossibleWithdraw = start;
+ } else {
+ lastPossibleWithdraw = AbsoluteTime.subtractDuraction(
+ withdrawExpire,
+ Duration.fromSpec({ minutes: 5 }),
+ );
+ }
+ const remaining = Duration.getRemaining(lastPossibleWithdraw, now);
+ const stillOkay = remaining.d_ms !== 0;
+ return started && stillOkay && !d.isRevoked && d.isOffered && !d.isLost;
+}
diff --git a/packages/taler-wallet-core/src/deposits.ts b/packages/taler-wallet-core/src/deposits.ts
new file mode 100644
index 000000000..dbba55247
--- /dev/null
+++ b/packages/taler-wallet-core/src/deposits.ts
@@ -0,0 +1,1775 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Implementation of the deposit transaction.
+ */
+
+/**
+ * Imports.
+ */
+import {
+ AbsoluteTime,
+ AmountJson,
+ Amounts,
+ BatchDepositRequestCoin,
+ CancellationToken,
+ CoinRefreshRequest,
+ CreateDepositGroupRequest,
+ CreateDepositGroupResponse,
+ DepositGroupFees,
+ Duration,
+ ExchangeBatchDepositRequest,
+ ExchangeHandle,
+ ExchangeRefundRequest,
+ HttpStatusCode,
+ Logger,
+ MerchantContractTerms,
+ NotificationType,
+ PrepareDepositRequest,
+ PrepareDepositResponse,
+ RefreshReason,
+ SelectedProspectiveCoin,
+ TalerError,
+ TalerErrorCode,
+ TalerPreciseTimestamp,
+ TalerProtocolTimestamp,
+ TrackTransaction,
+ TransactionAction,
+ TransactionIdStr,
+ TransactionMajorState,
+ TransactionMinorState,
+ TransactionState,
+ TransactionType,
+ URL,
+ WireFee,
+ assertUnreachable,
+ canonicalJson,
+ checkDbInvariant,
+ checkLogicInvariant,
+ codecForBatchDepositSuccess,
+ codecForTackTransactionAccepted,
+ codecForTackTransactionWired,
+ encodeCrock,
+ getRandomBytes,
+ hashTruncate32,
+ hashWire,
+ j2s,
+ parsePaytoUri,
+ stringToBytes,
+} from "@gnu-taler/taler-util";
+import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
+import { selectPayCoins } from "./coinSelection.js";
+import {
+ PendingTaskType,
+ TaskIdStr,
+ TaskRunResult,
+ TombstoneTag,
+ TransactionContext,
+ constructTaskIdentifier,
+ spendCoins,
+} from "./common.js";
+import {
+ DepositElementStatus,
+ DepositGroupRecord,
+ DepositInfoPerExchange,
+ DepositOperationStatus,
+ DepositTrackingInfo,
+ KycPendingInfo,
+ RefreshOperationStatus,
+ timestampPreciseToDb,
+ timestampProtocolToDb,
+} from "./db.js";
+import { getExchangeWireDetailsInTx } from "./exchanges.js";
+import {
+ extractContractData,
+ generateDepositPermissions,
+ getTotalPaymentCost,
+} from "./pay-merchant.js";
+import {
+ CreateRefreshGroupResult,
+ createRefreshGroup,
+ getTotalRefreshCost,
+} from "./refresh.js";
+import {
+ constructTransactionIdentifier,
+ notifyTransition,
+ parseTransactionIdentifier,
+} from "./transactions.js";
+import { WalletExecutionContext, getDenomInfo } from "./wallet.js";
+
+/**
+ * Logger.
+ */
+const logger = new Logger("deposits.ts");
+
+export class DepositTransactionContext implements TransactionContext {
+ readonly transactionId: TransactionIdStr;
+ readonly taskId: TaskIdStr;
+
+ constructor(
+ public wex: WalletExecutionContext,
+ public depositGroupId: string,
+ ) {
+ this.transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Deposit,
+ depositGroupId,
+ });
+ this.taskId = constructTaskIdentifier({
+ tag: PendingTaskType.Deposit,
+ depositGroupId,
+ });
+ }
+
+ async deleteTransaction(): Promise<void> {
+ const depositGroupId = this.depositGroupId;
+ const ws = this.wex;
+ // FIXME: We should check first if we are in a final state
+ // where deletion is allowed.
+ await ws.db.runReadWriteTx(
+ { storeNames: ["depositGroups", "tombstones"] },
+ async (tx) => {
+ const tipRecord = await tx.depositGroups.get(depositGroupId);
+ if (tipRecord) {
+ await tx.depositGroups.delete(depositGroupId);
+ await tx.tombstones.put({
+ id: TombstoneTag.DeleteDepositGroup + ":" + depositGroupId,
+ });
+ }
+ },
+ );
+ return;
+ }
+
+ async suspendTransaction(): Promise<void> {
+ const { wex, depositGroupId, transactionId, taskId: retryTag } = this;
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["depositGroups"] },
+ async (tx) => {
+ const dg = await tx.depositGroups.get(depositGroupId);
+ if (!dg) {
+ logger.warn(
+ `can't suspend deposit group, depositGroupId=${depositGroupId} not found`,
+ );
+ return undefined;
+ }
+ const oldState = computeDepositTransactionStatus(dg);
+ let newOpStatus: DepositOperationStatus | undefined;
+ switch (dg.operationStatus) {
+ case DepositOperationStatus.PendingDeposit:
+ newOpStatus = DepositOperationStatus.SuspendedDeposit;
+ break;
+ case DepositOperationStatus.PendingKyc:
+ newOpStatus = DepositOperationStatus.SuspendedKyc;
+ break;
+ case DepositOperationStatus.PendingTrack:
+ newOpStatus = DepositOperationStatus.SuspendedTrack;
+ break;
+ case DepositOperationStatus.Aborting:
+ newOpStatus = DepositOperationStatus.SuspendedAborting;
+ break;
+ }
+ if (!newOpStatus) {
+ return undefined;
+ }
+ dg.operationStatus = newOpStatus;
+ await tx.depositGroups.put(dg);
+ return {
+ oldTxState: oldState,
+ newTxState: computeDepositTransactionStatus(dg),
+ };
+ },
+ );
+ wex.taskScheduler.stopShepherdTask(retryTag);
+ notifyTransition(wex, transactionId, transitionInfo);
+ }
+
+ async abortTransaction(): Promise<void> {
+ const { wex, depositGroupId, transactionId, taskId: retryTag } = this;
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["depositGroups"] },
+ async (tx) => {
+ const dg = await tx.depositGroups.get(depositGroupId);
+ if (!dg) {
+ logger.warn(
+ `can't suspend deposit group, depositGroupId=${depositGroupId} not found`,
+ );
+ return undefined;
+ }
+ const oldState = computeDepositTransactionStatus(dg);
+ switch (dg.operationStatus) {
+ case DepositOperationStatus.Finished:
+ return undefined;
+ case DepositOperationStatus.PendingDeposit:
+ case DepositOperationStatus.SuspendedDeposit: {
+ dg.operationStatus = DepositOperationStatus.Aborting;
+ await tx.depositGroups.put(dg);
+ return {
+ oldTxState: oldState,
+ newTxState: computeDepositTransactionStatus(dg),
+ };
+ }
+ }
+ return undefined;
+ },
+ );
+ wex.taskScheduler.stopShepherdTask(retryTag);
+ notifyTransition(wex, transactionId, transitionInfo);
+ wex.taskScheduler.startShepherdTask(retryTag);
+ wex.ws.notify({
+ type: NotificationType.BalanceChange,
+ hintTransactionId: transactionId,
+ });
+ }
+
+ async resumeTransaction(): Promise<void> {
+ const { wex, depositGroupId, transactionId, taskId: retryTag } = this;
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["depositGroups"] },
+ async (tx) => {
+ const dg = await tx.depositGroups.get(depositGroupId);
+ if (!dg) {
+ logger.warn(
+ `can't resume deposit group, depositGroupId=${depositGroupId} not found`,
+ );
+ return;
+ }
+ const oldState = computeDepositTransactionStatus(dg);
+ let newOpStatus: DepositOperationStatus | undefined;
+ switch (dg.operationStatus) {
+ case DepositOperationStatus.SuspendedDeposit:
+ newOpStatus = DepositOperationStatus.PendingDeposit;
+ break;
+ case DepositOperationStatus.SuspendedAborting:
+ newOpStatus = DepositOperationStatus.Aborting;
+ break;
+ case DepositOperationStatus.SuspendedKyc:
+ newOpStatus = DepositOperationStatus.PendingKyc;
+ break;
+ case DepositOperationStatus.SuspendedTrack:
+ newOpStatus = DepositOperationStatus.PendingTrack;
+ break;
+ }
+ if (!newOpStatus) {
+ return undefined;
+ }
+ dg.operationStatus = newOpStatus;
+ await tx.depositGroups.put(dg);
+ return {
+ oldTxState: oldState,
+ newTxState: computeDepositTransactionStatus(dg),
+ };
+ },
+ );
+ notifyTransition(wex, transactionId, transitionInfo);
+ wex.taskScheduler.startShepherdTask(retryTag);
+ }
+
+ async failTransaction(): Promise<void> {
+ const { wex, depositGroupId, transactionId, taskId } = this;
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["depositGroups"] },
+ async (tx) => {
+ const dg = await tx.depositGroups.get(depositGroupId);
+ if (!dg) {
+ logger.warn(
+ `can't cancel aborting deposit group, depositGroupId=${depositGroupId} not found`,
+ );
+ return undefined;
+ }
+ const oldState = computeDepositTransactionStatus(dg);
+ switch (dg.operationStatus) {
+ case DepositOperationStatus.SuspendedAborting:
+ case DepositOperationStatus.Aborting: {
+ dg.operationStatus = DepositOperationStatus.Failed;
+ await tx.depositGroups.put(dg);
+ return {
+ oldTxState: oldState,
+ newTxState: computeDepositTransactionStatus(dg),
+ };
+ }
+ }
+ return undefined;
+ },
+ );
+ wex.taskScheduler.stopShepherdTask(taskId);
+ notifyTransition(wex, transactionId, transitionInfo);
+ wex.ws.notify({
+ type: NotificationType.BalanceChange,
+ hintTransactionId: transactionId,
+ });
+ }
+}
+
+/**
+ * Get the (DD37-style) transaction status based on the
+ * database record of a deposit group.
+ */
+export function computeDepositTransactionStatus(
+ dg: DepositGroupRecord,
+): TransactionState {
+ switch (dg.operationStatus) {
+ case DepositOperationStatus.Finished:
+ return {
+ major: TransactionMajorState.Done,
+ };
+ case DepositOperationStatus.PendingDeposit:
+ return {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.Deposit,
+ };
+ case DepositOperationStatus.PendingKyc:
+ return {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.KycRequired,
+ };
+ case DepositOperationStatus.PendingTrack:
+ return {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.Track,
+ };
+ case DepositOperationStatus.SuspendedKyc:
+ return {
+ major: TransactionMajorState.Suspended,
+ minor: TransactionMinorState.KycRequired,
+ };
+ case DepositOperationStatus.SuspendedTrack:
+ return {
+ major: TransactionMajorState.Suspended,
+ minor: TransactionMinorState.Track,
+ };
+ case DepositOperationStatus.SuspendedDeposit:
+ return {
+ major: TransactionMajorState.Suspended,
+ };
+ case DepositOperationStatus.Aborting:
+ return {
+ major: TransactionMajorState.Aborting,
+ };
+ case DepositOperationStatus.Aborted:
+ return {
+ major: TransactionMajorState.Aborted,
+ };
+ case DepositOperationStatus.Failed:
+ return {
+ major: TransactionMajorState.Failed,
+ };
+ case DepositOperationStatus.SuspendedAborting:
+ return {
+ major: TransactionMajorState.SuspendedAborting,
+ };
+ default:
+ assertUnreachable(dg.operationStatus);
+ }
+}
+
+/**
+ * Compute the possible actions possible on a deposit transaction
+ * based on the current transaction state.
+ */
+export function computeDepositTransactionActions(
+ dg: DepositGroupRecord,
+): TransactionAction[] {
+ switch (dg.operationStatus) {
+ case DepositOperationStatus.Finished:
+ return [TransactionAction.Delete];
+ case DepositOperationStatus.PendingDeposit:
+ return [TransactionAction.Suspend, TransactionAction.Abort];
+ case DepositOperationStatus.SuspendedDeposit:
+ return [TransactionAction.Resume];
+ case DepositOperationStatus.Aborting:
+ return [TransactionAction.Fail, TransactionAction.Suspend];
+ case DepositOperationStatus.Aborted:
+ return [TransactionAction.Delete];
+ case DepositOperationStatus.Failed:
+ return [TransactionAction.Delete];
+ case DepositOperationStatus.SuspendedAborting:
+ return [TransactionAction.Resume, TransactionAction.Fail];
+ case DepositOperationStatus.PendingKyc:
+ return [TransactionAction.Suspend, TransactionAction.Fail];
+ case DepositOperationStatus.PendingTrack:
+ return [TransactionAction.Suspend, TransactionAction.Abort];
+ case DepositOperationStatus.SuspendedKyc:
+ return [TransactionAction.Resume, TransactionAction.Fail];
+ case DepositOperationStatus.SuspendedTrack:
+ return [TransactionAction.Resume, TransactionAction.Abort];
+ default:
+ assertUnreachable(dg.operationStatus);
+ }
+}
+
+async function refundDepositGroup(
+ wex: WalletExecutionContext,
+ depositGroup: DepositGroupRecord,
+): Promise<TaskRunResult> {
+ const statusPerCoin = depositGroup.statusPerCoin;
+ const payCoinSelection = depositGroup.payCoinSelection;
+ if (!statusPerCoin) {
+ throw Error(
+ "unable to refund deposit group without coin selection (status missing)",
+ );
+ }
+ if (!payCoinSelection) {
+ throw Error(
+ "unable to refund deposit group without coin selection (selection missing)",
+ );
+ }
+ const newTxPerCoin = [...statusPerCoin];
+ logger.info(`status per coin: ${j2s(depositGroup.statusPerCoin)}`);
+ for (let i = 0; i < statusPerCoin.length; i++) {
+ const st = statusPerCoin[i];
+ switch (st) {
+ case DepositElementStatus.RefundFailed:
+ case DepositElementStatus.RefundSuccess:
+ break;
+ default: {
+ const coinPub = payCoinSelection.coinPubs[i];
+ const coinExchange = await wex.db.runReadOnlyTx(
+ { storeNames: ["coins"] },
+ async (tx) => {
+ const coinRecord = await tx.coins.get(coinPub);
+ checkDbInvariant(!!coinRecord);
+ return coinRecord.exchangeBaseUrl;
+ },
+ );
+ const refundAmount = payCoinSelection.coinContributions[i];
+ // We use a constant refund transaction ID, since there can
+ // only be one refund.
+ const rtid = 1;
+ const sig = await wex.cryptoApi.signRefund({
+ coinPub,
+ contractTermsHash: depositGroup.contractTermsHash,
+ merchantPriv: depositGroup.merchantPriv,
+ merchantPub: depositGroup.merchantPub,
+ refundAmount: refundAmount,
+ rtransactionId: rtid,
+ });
+ const refundReq: ExchangeRefundRequest = {
+ h_contract_terms: depositGroup.contractTermsHash,
+ merchant_pub: depositGroup.merchantPub,
+ merchant_sig: sig.sig,
+ refund_amount: refundAmount,
+ rtransaction_id: rtid,
+ };
+ const refundUrl = new URL(`coins/${coinPub}/refund`, coinExchange);
+ const httpResp = await wex.http.fetch(refundUrl.href, {
+ method: "POST",
+ body: refundReq,
+ cancellationToken: wex.cancellationToken,
+ });
+ logger.info(
+ `coin ${i} refund HTTP status for coin: ${httpResp.status}`,
+ );
+ let newStatus: DepositElementStatus;
+ if (httpResp.status === 200) {
+ // FIXME: validate response
+ newStatus = DepositElementStatus.RefundSuccess;
+ } else {
+ // FIXME: Store problem somewhere!
+ newStatus = DepositElementStatus.RefundFailed;
+ }
+ // FIXME: Handle case where refund request needs to be tried again
+ newTxPerCoin[i] = newStatus;
+ break;
+ }
+ }
+ }
+ let isDone = true;
+ for (let i = 0; i < newTxPerCoin.length; i++) {
+ if (
+ newTxPerCoin[i] != DepositElementStatus.RefundFailed &&
+ newTxPerCoin[i] != DepositElementStatus.RefundSuccess
+ ) {
+ isDone = false;
+ }
+ }
+
+ const currency = Amounts.currencyOf(depositGroup.totalPayCost);
+
+ const res = await wex.db.runReadWriteTx(
+ {
+ storeNames: [
+ "depositGroups",
+ "refreshGroups",
+ "refreshSessions",
+ "coins",
+ "denominations",
+ "coinAvailability",
+ ],
+ },
+ async (tx) => {
+ const newDg = await tx.depositGroups.get(depositGroup.depositGroupId);
+ if (!newDg) {
+ return;
+ }
+ newDg.statusPerCoin = newTxPerCoin;
+ const refreshCoins: CoinRefreshRequest[] = [];
+ for (let i = 0; i < newTxPerCoin.length; i++) {
+ refreshCoins.push({
+ amount: payCoinSelection.coinContributions[i],
+ coinPub: payCoinSelection.coinPubs[i],
+ });
+ }
+ let refreshRes: CreateRefreshGroupResult | undefined = undefined;
+ if (isDone) {
+ refreshRes = await createRefreshGroup(
+ wex,
+ tx,
+ currency,
+ refreshCoins,
+ RefreshReason.AbortDeposit,
+ constructTransactionIdentifier({
+ tag: TransactionType.Deposit,
+ depositGroupId: newDg.depositGroupId,
+ }),
+ );
+ newDg.abortRefreshGroupId = refreshRes.refreshGroupId;
+ }
+ await tx.depositGroups.put(newDg);
+ return { refreshRes };
+ },
+ );
+
+ if (res?.refreshRes) {
+ for (const notif of res.refreshRes.notifications) {
+ wex.ws.notify(notif);
+ }
+ }
+
+ return TaskRunResult.backoff();
+}
+
+/**
+ * Check whether the refresh associated with the
+ * aborting deposit group is done.
+ *
+ * If done, mark the deposit transaction as aborted.
+ *
+ * Otherwise continue waiting.
+ *
+ * FIXME: Wait for the refresh group notifications instead of periodically
+ * checking the refresh group status.
+ * FIXME: This is just one transaction, can't we do this in the initial
+ * transaction of processDepositGroup?
+ */
+async function waitForRefreshOnDepositGroup(
+ wex: WalletExecutionContext,
+ depositGroup: DepositGroupRecord,
+): Promise<TaskRunResult> {
+ const abortRefreshGroupId = depositGroup.abortRefreshGroupId;
+ checkLogicInvariant(!!abortRefreshGroupId);
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Deposit,
+ depositGroupId: depositGroup.depositGroupId,
+ });
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["depositGroups", "refreshGroups"] },
+ async (tx) => {
+ const refreshGroup = await tx.refreshGroups.get(abortRefreshGroupId);
+ let newOpState: DepositOperationStatus | undefined;
+ if (!refreshGroup) {
+ // Maybe it got manually deleted? Means that we should
+ // just go into aborted.
+ logger.warn("no aborting refresh group found for deposit group");
+ newOpState = DepositOperationStatus.Aborted;
+ } else {
+ if (refreshGroup.operationStatus === RefreshOperationStatus.Finished) {
+ newOpState = DepositOperationStatus.Aborted;
+ } else if (
+ refreshGroup.operationStatus === RefreshOperationStatus.Failed
+ ) {
+ newOpState = DepositOperationStatus.Aborted;
+ }
+ }
+ if (newOpState) {
+ const newDg = await tx.depositGroups.get(depositGroup.depositGroupId);
+ if (!newDg) {
+ return;
+ }
+ const oldTxState = computeDepositTransactionStatus(newDg);
+ newDg.operationStatus = newOpState;
+ const newTxState = computeDepositTransactionStatus(newDg);
+ await tx.depositGroups.put(newDg);
+ return { oldTxState, newTxState };
+ }
+ return undefined;
+ },
+ );
+
+ notifyTransition(wex, transactionId, transitionInfo);
+ wex.ws.notify({
+ type: NotificationType.BalanceChange,
+ hintTransactionId: transactionId,
+ });
+ return TaskRunResult.backoff();
+}
+
+async function processDepositGroupAborting(
+ wex: WalletExecutionContext,
+ depositGroup: DepositGroupRecord,
+): Promise<TaskRunResult> {
+ logger.info("processing deposit tx in 'aborting'");
+ const abortRefreshGroupId = depositGroup.abortRefreshGroupId;
+ if (!abortRefreshGroupId) {
+ logger.info("refunding deposit group");
+ return refundDepositGroup(wex, depositGroup);
+ }
+ logger.info("waiting for refresh");
+ return waitForRefreshOnDepositGroup(wex, depositGroup);
+}
+
+async function processDepositGroupPendingKyc(
+ wex: WalletExecutionContext,
+ depositGroup: DepositGroupRecord,
+): Promise<TaskRunResult> {
+ const { depositGroupId } = depositGroup;
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Deposit,
+ depositGroupId,
+ });
+
+ const kycInfo = depositGroup.kycInfo;
+ const userType = "individual";
+
+ if (!kycInfo) {
+ throw Error("invalid DB state, in pending(kyc), but no kycInfo present");
+ }
+
+ const url = new URL(
+ `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,
+ });
+ if (
+ kycStatusRes.status === HttpStatusCode.Ok ||
+ //FIXME: NoContent is not expected https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-merge
+ // remove after the exchange is fixed or clarified
+ kycStatusRes.status === HttpStatusCode.NoContent
+ ) {
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["depositGroups"] },
+ async (tx) => {
+ const newDg = await tx.depositGroups.get(depositGroupId);
+ if (!newDg) {
+ return;
+ }
+ if (newDg.operationStatus !== DepositOperationStatus.PendingKyc) {
+ return;
+ }
+ const oldTxState = computeDepositTransactionStatus(newDg);
+ newDg.operationStatus = DepositOperationStatus.PendingTrack;
+ const newTxState = computeDepositTransactionStatus(newDg);
+ await tx.depositGroups.put(newDg);
+ return { oldTxState, newTxState };
+ },
+ );
+ notifyTransition(wex, transactionId, transitionInfo);
+ } else if (kycStatusRes.status === HttpStatusCode.Accepted) {
+ // FIXME: Do we have to update the URL here?
+ } else {
+ throw Error(`unexpected response from kyc-check (${kycStatusRes.status})`);
+ }
+ return TaskRunResult.backoff();
+}
+
+/**
+ * Tracking information from the exchange indicated that
+ * KYC is required. We need to check the KYC info
+ * and transition the transaction to the KYC required state.
+ */
+async function transitionToKycRequired(
+ wex: WalletExecutionContext,
+ depositGroup: DepositGroupRecord,
+ kycInfo: KycPendingInfo,
+ exchangeUrl: string,
+): Promise<TaskRunResult> {
+ const { depositGroupId } = depositGroup;
+ const userType = "individual";
+
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Deposit,
+ depositGroupId,
+ });
+
+ const url = new URL(
+ `kyc-check/${kycInfo.requirementRow}/${kycInfo.paytoHash}/${userType}`,
+ exchangeUrl,
+ );
+ logger.info(`kyc url ${url.href}`);
+ const kycStatusReq = await wex.http.fetch(url.href, {
+ method: "GET",
+ });
+ if (kycStatusReq.status === HttpStatusCode.Ok) {
+ logger.warn("kyc requested, but already fulfilled");
+ return TaskRunResult.backoff();
+ } else if (kycStatusReq.status === HttpStatusCode.Accepted) {
+ const kycStatus = await kycStatusReq.json();
+ logger.info(`kyc status: ${j2s(kycStatus)}`);
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["depositGroups"] },
+ async (tx) => {
+ const dg = await tx.depositGroups.get(depositGroupId);
+ if (!dg) {
+ return undefined;
+ }
+ if (dg.operationStatus !== DepositOperationStatus.PendingTrack) {
+ return undefined;
+ }
+ const oldTxState = computeDepositTransactionStatus(dg);
+ dg.kycInfo = {
+ exchangeBaseUrl: exchangeUrl,
+ kycUrl: kycStatus.kyc_url,
+ paytoHash: kycInfo.paytoHash,
+ requirementRow: kycInfo.requirementRow,
+ };
+ await tx.depositGroups.put(dg);
+ const newTxState = computeDepositTransactionStatus(dg);
+ return { oldTxState, newTxState };
+ },
+ );
+ notifyTransition(wex, transactionId, transitionInfo);
+ return TaskRunResult.finished();
+ } else {
+ throw Error(`unexpected response from kyc-check (${kycStatusReq.status})`);
+ }
+}
+
+async function processDepositGroupPendingTrack(
+ wex: WalletExecutionContext,
+ depositGroup: DepositGroupRecord,
+): Promise<TaskRunResult> {
+ const statusPerCoin = depositGroup.statusPerCoin;
+ const payCoinSelection = depositGroup.payCoinSelection;
+ if (!statusPerCoin) {
+ throw Error(
+ "unable to refund deposit group without coin selection (status missing)",
+ );
+ }
+ if (!payCoinSelection) {
+ throw Error(
+ "unable to refund deposit group without coin selection (selection missing)",
+ );
+ }
+ const { depositGroupId } = depositGroup;
+ for (let i = 0; i < statusPerCoin.length; i++) {
+ const coinPub = payCoinSelection.coinPubs[i];
+ // FIXME: Make the URL part of the coin selection?
+ const exchangeBaseUrl = await wex.db.runReadWriteTx(
+ { storeNames: ["coins"] },
+ async (tx) => {
+ const coinRecord = await tx.coins.get(coinPub);
+ checkDbInvariant(!!coinRecord);
+ return coinRecord.exchangeBaseUrl;
+ },
+ );
+
+ let updatedTxStatus: DepositElementStatus | undefined = undefined;
+ let newWiredCoin:
+ | {
+ id: string;
+ value: DepositTrackingInfo;
+ }
+ | undefined;
+
+ if (statusPerCoin[i] !== DepositElementStatus.Wired) {
+ const track = await trackDeposit(
+ wex,
+ depositGroup,
+ coinPub,
+ exchangeBaseUrl,
+ );
+
+ if (track.type === "accepted") {
+ if (!track.kyc_ok && track.requirement_row !== undefined) {
+ const paytoHash = encodeCrock(
+ hashTruncate32(stringToBytes(depositGroup.wire.payto_uri + "\0")),
+ );
+ const { requirement_row: requirementRow } = track;
+ const kycInfo: KycPendingInfo = {
+ paytoHash,
+ requirementRow,
+ };
+ return transitionToKycRequired(
+ wex,
+ depositGroup,
+ kycInfo,
+ exchangeBaseUrl,
+ );
+ } else {
+ updatedTxStatus = DepositElementStatus.Tracking;
+ }
+ } else if (track.type === "wired") {
+ updatedTxStatus = DepositElementStatus.Wired;
+
+ const payto = parsePaytoUri(depositGroup.wire.payto_uri);
+ if (!payto) {
+ throw Error(`unparsable payto: ${depositGroup.wire.payto_uri}`);
+ }
+
+ const fee = await getExchangeWireFee(
+ wex,
+ payto.targetType,
+ exchangeBaseUrl,
+ track.execution_time,
+ );
+ const raw = Amounts.parseOrThrow(track.coin_contribution);
+ const wireFee = Amounts.parseOrThrow(fee.wireFee);
+
+ newWiredCoin = {
+ value: {
+ amountRaw: Amounts.stringify(raw),
+ wireFee: Amounts.stringify(wireFee),
+ exchangePub: track.exchange_pub,
+ timestampExecuted: timestampProtocolToDb(track.execution_time),
+ wireTransferId: track.wtid,
+ },
+ id: track.exchange_sig,
+ };
+ } else {
+ updatedTxStatus = DepositElementStatus.DepositPending;
+ }
+ }
+
+ if (updatedTxStatus !== undefined) {
+ await wex.db.runReadWriteTx(
+ { storeNames: ["depositGroups"] },
+ async (tx) => {
+ const dg = await tx.depositGroups.get(depositGroupId);
+ if (!dg) {
+ return;
+ }
+ if (!dg.statusPerCoin) {
+ return;
+ }
+ if (updatedTxStatus !== undefined) {
+ dg.statusPerCoin[i] = updatedTxStatus;
+ }
+ if (newWiredCoin) {
+ /**
+ * FIXME: if there is a new wire information from the exchange
+ * it should add up to the previous tracking states.
+ *
+ * This may loose information by overriding prev state.
+ *
+ * And: add checks to integration tests
+ */
+ if (!dg.trackingState) {
+ dg.trackingState = {};
+ }
+
+ dg.trackingState[newWiredCoin.id] = newWiredCoin.value;
+ }
+ await tx.depositGroups.put(dg);
+ },
+ );
+ }
+ }
+
+ let allWired = true;
+
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["depositGroups"] },
+ async (tx) => {
+ const dg = await tx.depositGroups.get(depositGroupId);
+ if (!dg) {
+ return undefined;
+ }
+ if (!dg.statusPerCoin) {
+ return undefined;
+ }
+ const oldTxState = computeDepositTransactionStatus(dg);
+ for (let i = 0; i < dg.statusPerCoin.length; i++) {
+ if (dg.statusPerCoin[i] !== DepositElementStatus.Wired) {
+ allWired = false;
+ break;
+ }
+ }
+ if (allWired) {
+ dg.timestampFinished = timestampPreciseToDb(
+ TalerPreciseTimestamp.now(),
+ );
+ dg.operationStatus = DepositOperationStatus.Finished;
+ await tx.depositGroups.put(dg);
+ }
+ const newTxState = computeDepositTransactionStatus(dg);
+ return { oldTxState, newTxState };
+ },
+ );
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Deposit,
+ depositGroupId,
+ });
+ notifyTransition(wex, transactionId, transitionInfo);
+ if (allWired) {
+ wex.ws.notify({
+ type: NotificationType.BalanceChange,
+ hintTransactionId: transactionId,
+ });
+ return TaskRunResult.finished();
+ } else {
+ return TaskRunResult.longpollReturnedPending();
+ }
+}
+
+async function processDepositGroupPendingDeposit(
+ wex: WalletExecutionContext,
+ depositGroup: DepositGroupRecord,
+ cancellationToken?: CancellationToken,
+): Promise<TaskRunResult> {
+ logger.info("processing deposit group in pending(deposit)");
+ const depositGroupId = depositGroup.depositGroupId;
+ const contractTermsRec = await wex.db.runReadOnlyTx(
+ { storeNames: ["contractTerms"] },
+ async (tx) => {
+ return tx.contractTerms.get(depositGroup.contractTermsHash);
+ },
+ );
+ if (!contractTermsRec) {
+ throw Error("contract terms for deposit not found in database");
+ }
+ const contractTerms: MerchantContractTerms =
+ contractTermsRec.contractTermsRaw;
+ const contractData = extractContractData(
+ contractTermsRec.contractTermsRaw,
+ depositGroup.contractTermsHash,
+ "",
+ );
+
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Deposit,
+ depositGroupId,
+ });
+
+ // Check for cancellation before expensive operations.
+ cancellationToken?.throwIfCancelled();
+
+ if (!depositGroup.payCoinSelection) {
+ logger.info("missing coin selection for deposit group, selecting now");
+ // FIXME: Consider doing the coin selection inside the txn
+ const payCoinSel = await selectPayCoins(wex, {
+ restrictExchanges: {
+ auditors: [],
+ exchanges: contractData.allowedExchanges,
+ },
+ restrictWireMethod: contractData.wireMethod,
+ contractTermsAmount: Amounts.parseOrThrow(contractData.amount),
+ depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee),
+ prevPayCoins: [],
+ });
+
+ switch (payCoinSel.type) {
+ case "success":
+ logger.info("coin selection success");
+ break;
+ case "failure":
+ logger.info("coin selection failure");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE,
+ {
+ insufficientBalanceDetails: payCoinSel.insufficientBalanceDetails,
+ },
+ );
+ case "prospective":
+ logger.info("coin selection prospective");
+ throw Error("insufficient balance (waiting on pending refresh)");
+ default:
+ assertUnreachable(payCoinSel);
+ }
+
+ const transitionDone = await wex.db.runReadWriteTx(
+ {
+ storeNames: [
+ "depositGroups",
+ "coins",
+ "coinAvailability",
+ "refreshGroups",
+ "refreshSessions",
+ "denominations",
+ ],
+ },
+ async (tx) => {
+ const dg = await tx.depositGroups.get(depositGroupId);
+ if (!dg) {
+ return false;
+ }
+ if (dg.statusPerCoin) {
+ return false;
+ }
+ dg.payCoinSelection = {
+ coinContributions: payCoinSel.coinSel.coins.map(
+ (x) => x.contribution,
+ ),
+ coinPubs: payCoinSel.coinSel.coins.map((x) => x.coinPub),
+ };
+ dg.payCoinSelectionUid = encodeCrock(getRandomBytes(32));
+ dg.statusPerCoin = payCoinSel.coinSel.coins.map(
+ () => DepositElementStatus.DepositPending,
+ );
+ await tx.depositGroups.put(dg);
+ await spendCoins(wex, tx, {
+ allocationId: transactionId,
+ coinPubs: dg.payCoinSelection.coinPubs,
+ contributions: dg.payCoinSelection.coinContributions.map((x) =>
+ Amounts.parseOrThrow(x),
+ ),
+ refreshReason: RefreshReason.PayDeposit,
+ });
+ return true;
+ },
+ );
+
+ if (transitionDone) {
+ return TaskRunResult.progress();
+ } else {
+ return TaskRunResult.backoff();
+ }
+ }
+
+ // FIXME: Cache these!
+ const depositPermissions = await generateDepositPermissions(
+ wex,
+ depositGroup.payCoinSelection,
+ contractData,
+ );
+
+ // Exchanges involved in the deposit
+ const exchanges: Set<string> = new Set();
+
+ for (const dp of depositPermissions) {
+ exchanges.add(dp.exchange_url);
+ }
+
+ // We need to do one batch per exchange.
+ for (const exchangeUrl of exchanges.values()) {
+ const coins: BatchDepositRequestCoin[] = [];
+ const batchIndexes: number[] = [];
+
+ const batchReq: ExchangeBatchDepositRequest = {
+ coins,
+ h_contract_terms: depositGroup.contractTermsHash,
+ merchant_payto_uri: depositGroup.wire.payto_uri,
+ merchant_pub: contractTerms.merchant_pub,
+ timestamp: contractTerms.timestamp,
+ wire_salt: depositGroup.wire.salt,
+ wire_transfer_deadline: contractTerms.wire_transfer_deadline,
+ refund_deadline: contractTerms.refund_deadline,
+ };
+
+ for (let i = 0; i < depositPermissions.length; i++) {
+ const perm = depositPermissions[i];
+ if (perm.exchange_url != exchangeUrl) {
+ continue;
+ }
+ coins.push({
+ coin_pub: perm.coin_pub,
+ coin_sig: perm.coin_sig,
+ contribution: Amounts.stringify(perm.contribution),
+ denom_pub_hash: perm.h_denom,
+ ub_sig: perm.ub_sig,
+ h_age_commitment: perm.h_age_commitment,
+ });
+ batchIndexes.push(i);
+ }
+
+ // Check for cancellation before making network request.
+ cancellationToken?.throwIfCancelled();
+ const url = new URL(`batch-deposit`, exchangeUrl);
+ logger.info(`depositing to ${url.href}`);
+ logger.trace(`deposit request: ${j2s(batchReq)}`);
+ const httpResp = await wex.http.fetch(url.href, {
+ method: "POST",
+ body: batchReq,
+ cancellationToken: cancellationToken,
+ });
+ await readSuccessResponseJsonOrThrow(
+ httpResp,
+ codecForBatchDepositSuccess(),
+ );
+
+ await wex.db.runReadWriteTx(
+ { storeNames: ["depositGroups"] },
+ async (tx) => {
+ const dg = await tx.depositGroups.get(depositGroupId);
+ if (!dg) {
+ return;
+ }
+ if (!dg.statusPerCoin) {
+ return;
+ }
+ for (const batchIndex of batchIndexes) {
+ const coinStatus = dg.statusPerCoin[batchIndex];
+ switch (coinStatus) {
+ case DepositElementStatus.DepositPending:
+ dg.statusPerCoin[batchIndex] = DepositElementStatus.Tracking;
+ await tx.depositGroups.put(dg);
+ }
+ }
+ },
+ );
+ }
+
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["depositGroups"] },
+ async (tx) => {
+ const dg = await tx.depositGroups.get(depositGroupId);
+ if (!dg) {
+ return undefined;
+ }
+ const oldTxState = computeDepositTransactionStatus(dg);
+ dg.operationStatus = DepositOperationStatus.PendingTrack;
+ await tx.depositGroups.put(dg);
+ const newTxState = computeDepositTransactionStatus(dg);
+ return { oldTxState, newTxState };
+ },
+ );
+
+ notifyTransition(wex, transactionId, transitionInfo);
+ return TaskRunResult.progress();
+}
+
+/**
+ * Process a deposit group that is not in its final state yet.
+ */
+export async function processDepositGroup(
+ wex: WalletExecutionContext,
+ depositGroupId: string,
+): Promise<TaskRunResult> {
+ const depositGroup = await wex.db.runReadOnlyTx(
+ { storeNames: ["depositGroups"] },
+ async (tx) => {
+ return tx.depositGroups.get(depositGroupId);
+ },
+ );
+ if (!depositGroup) {
+ logger.warn(`deposit group ${depositGroupId} not found`);
+ return TaskRunResult.finished();
+ }
+
+ switch (depositGroup.operationStatus) {
+ case DepositOperationStatus.PendingTrack:
+ return processDepositGroupPendingTrack(wex, depositGroup);
+ case DepositOperationStatus.PendingKyc:
+ return processDepositGroupPendingKyc(wex, depositGroup);
+ case DepositOperationStatus.PendingDeposit:
+ return processDepositGroupPendingDeposit(wex, depositGroup);
+ case DepositOperationStatus.Aborting:
+ return processDepositGroupAborting(wex, depositGroup);
+ }
+
+ return TaskRunResult.finished();
+}
+
+/**
+ * FIXME: Consider moving this to exchanges.ts.
+ */
+async function getExchangeWireFee(
+ wex: WalletExecutionContext,
+ wireType: string,
+ baseUrl: string,
+ time: TalerProtocolTimestamp,
+): Promise<WireFee> {
+ const exchangeDetails = await wex.db.runReadOnlyTx(
+ { storeNames: ["exchangeDetails", "exchanges"] },
+ async (tx) => {
+ const ex = await tx.exchanges.get(baseUrl);
+ if (!ex || !ex.detailsPointer) return undefined;
+ return await tx.exchangeDetails.indexes.byPointer.get([
+ baseUrl,
+ ex.detailsPointer.currency,
+ ex.detailsPointer.masterPublicKey,
+ ]);
+ },
+ );
+
+ if (!exchangeDetails) {
+ throw Error(`exchange missing: ${baseUrl}`);
+ }
+
+ const fees = exchangeDetails.wireInfo.feesForType[wireType];
+ if (!fees || fees.length === 0) {
+ throw Error(
+ `exchange ${baseUrl} doesn't have fees for wire type ${wireType}`,
+ );
+ }
+ const fee = fees.find((x) => {
+ return AbsoluteTime.isBetween(
+ AbsoluteTime.fromProtocolTimestamp(time),
+ AbsoluteTime.fromProtocolTimestamp(x.startStamp),
+ AbsoluteTime.fromProtocolTimestamp(x.endStamp),
+ );
+ });
+ if (!fee) {
+ throw Error(
+ `exchange ${exchangeDetails.exchangeBaseUrl} doesn't have fees for wire type ${wireType} at ${time.t_s}`,
+ );
+ }
+
+ return fee;
+}
+
+async function trackDeposit(
+ wex: WalletExecutionContext,
+ depositGroup: DepositGroupRecord,
+ coinPub: string,
+ exchangeUrl: string,
+): Promise<TrackTransaction> {
+ const wireHash = hashWire(
+ depositGroup.wire.payto_uri,
+ depositGroup.wire.salt,
+ );
+
+ const url = new URL(
+ `deposits/${wireHash}/${depositGroup.merchantPub}/${depositGroup.contractTermsHash}/${coinPub}`,
+ exchangeUrl,
+ );
+ const sigResp = await wex.cryptoApi.signTrackTransaction({
+ coinPub,
+ contractTermsHash: depositGroup.contractTermsHash,
+ merchantPriv: depositGroup.merchantPriv,
+ merchantPub: depositGroup.merchantPub,
+ 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,
+ });
+ logger.trace(`deposits response status: ${httpResp.status}`);
+ switch (httpResp.status) {
+ case HttpStatusCode.Accepted: {
+ const accepted = await readSuccessResponseJsonOrThrow(
+ httpResp,
+ codecForTackTransactionAccepted(),
+ );
+ return { type: "accepted", ...accepted };
+ }
+ case HttpStatusCode.Ok: {
+ const wired = await readSuccessResponseJsonOrThrow(
+ httpResp,
+ codecForTackTransactionWired(),
+ );
+ return { type: "wired", ...wired };
+ }
+ default: {
+ throw Error(
+ `unexpected response from track-transaction (${httpResp.status})`,
+ );
+ }
+ }
+}
+
+/**
+ * Check if creating a deposit group is possible and calculate
+ * the associated fees.
+ */
+export async function checkDepositGroup(
+ wex: WalletExecutionContext,
+ req: PrepareDepositRequest,
+): Promise<PrepareDepositResponse> {
+ const p = parsePaytoUri(req.depositPaytoUri);
+ if (!p) {
+ throw Error("invalid payto URI");
+ }
+ const amount = Amounts.parseOrThrow(req.amount);
+ const currency = Amounts.currencyOf(amount);
+
+ const exchangeInfos: ExchangeHandle[] = [];
+
+ await wex.db.runReadOnlyTx(
+ { storeNames: ["exchangeDetails", "exchanges"] },
+ async (tx) => {
+ const allExchanges = await tx.exchanges.iter().toArray();
+ for (const e of allExchanges) {
+ const details = await getExchangeWireDetailsInTx(tx, e.baseUrl);
+ if (!details || amount.currency !== details.currency) {
+ continue;
+ }
+ exchangeInfos.push({
+ master_pub: details.masterPublicKey,
+ url: e.baseUrl,
+ });
+ }
+ },
+ );
+
+ const now = AbsoluteTime.now();
+ const nowRounded = AbsoluteTime.toProtocolTimestamp(now);
+ const contractTerms: MerchantContractTerms = {
+ exchanges: exchangeInfos,
+ amount: req.amount,
+ max_fee: Amounts.stringify(amount),
+ wire_method: p.targetType,
+ timestamp: nowRounded,
+ merchant_base_url: "",
+ summary: "",
+ nonce: "",
+ wire_transfer_deadline: nowRounded,
+ order_id: "",
+ h_wire: "",
+ pay_deadline: AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.addDuration(now, Duration.fromSpec({ hours: 1 })),
+ ),
+ merchant: {
+ name: "(wallet)",
+ },
+ merchant_pub: "",
+ refund_deadline: TalerProtocolTimestamp.zero(),
+ };
+
+ const { h: contractTermsHash } = await wex.cryptoApi.hashString({
+ str: canonicalJson(contractTerms),
+ });
+
+ const contractData = extractContractData(
+ contractTerms,
+ contractTermsHash,
+ "",
+ );
+
+ const payCoinSel = await selectPayCoins(wex, {
+ restrictExchanges: {
+ auditors: [],
+ exchanges: contractData.allowedExchanges,
+ },
+ restrictWireMethod: contractData.wireMethod,
+ contractTermsAmount: Amounts.parseOrThrow(contractData.amount),
+ depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee),
+ prevPayCoins: [],
+ });
+
+ let selCoins: SelectedProspectiveCoin[] | undefined = undefined;
+
+ switch (payCoinSel.type) {
+ case "failure":
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE,
+ {
+ insufficientBalanceDetails: payCoinSel.insufficientBalanceDetails,
+ },
+ );
+ case "prospective":
+ selCoins = payCoinSel.result.prospectiveCoins;
+ break;
+ case "success":
+ selCoins = payCoinSel.coinSel.coins;
+ break;
+ default:
+ assertUnreachable(payCoinSel);
+ }
+
+ const totalDepositCost = await getTotalPaymentCost(wex, currency, selCoins);
+
+ const effectiveDepositAmount = await getCounterpartyEffectiveDepositAmount(
+ wex,
+ p.targetType,
+ selCoins,
+ );
+
+ const fees = await getTotalFeesForDepositAmount(
+ wex,
+ p.targetType,
+ amount,
+ selCoins,
+ );
+
+ return {
+ totalDepositCost: Amounts.stringify(totalDepositCost),
+ effectiveDepositAmount: Amounts.stringify(effectiveDepositAmount),
+ fees,
+ };
+}
+
+export function generateDepositGroupTxId(): string {
+ const depositGroupId = encodeCrock(getRandomBytes(32));
+ return constructTransactionIdentifier({
+ tag: TransactionType.Deposit,
+ depositGroupId: depositGroupId,
+ });
+}
+
+export async function createDepositGroup(
+ wex: WalletExecutionContext,
+ req: CreateDepositGroupRequest,
+): Promise<CreateDepositGroupResponse> {
+ const p = parsePaytoUri(req.depositPaytoUri);
+ if (!p) {
+ throw Error("invalid payto URI");
+ }
+
+ const amount = Amounts.parseOrThrow(req.amount);
+ const currency = amount.currency;
+
+ const exchangeInfos: { url: string; master_pub: string }[] = [];
+
+ await wex.db.runReadOnlyTx(
+ { storeNames: ["exchanges", "exchangeDetails"] },
+ async (tx) => {
+ const allExchanges = await tx.exchanges.iter().toArray();
+ for (const e of allExchanges) {
+ const details = await getExchangeWireDetailsInTx(tx, e.baseUrl);
+ if (!details || amount.currency !== details.currency) {
+ continue;
+ }
+ exchangeInfos.push({
+ master_pub: details.masterPublicKey,
+ url: e.baseUrl,
+ });
+ }
+ },
+ );
+
+ const now = AbsoluteTime.now();
+ const wireDeadline = AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.addDuration(now, Duration.fromSpec({ minutes: 5 })),
+ );
+ const nowRounded = AbsoluteTime.toProtocolTimestamp(now);
+ const noncePair = await wex.cryptoApi.createEddsaKeypair({});
+ const merchantPair = await wex.cryptoApi.createEddsaKeypair({});
+ const wireSalt = encodeCrock(getRandomBytes(16));
+ const wireHash = hashWire(req.depositPaytoUri, wireSalt);
+ const contractTerms: MerchantContractTerms = {
+ exchanges: exchangeInfos,
+ amount: req.amount,
+ max_fee: Amounts.stringify(amount),
+ wire_method: p.targetType,
+ timestamp: nowRounded,
+ merchant_base_url: "",
+ summary: "",
+ nonce: noncePair.pub,
+ wire_transfer_deadline: wireDeadline,
+ order_id: "",
+ h_wire: wireHash,
+ pay_deadline: AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.addDuration(now, Duration.fromSpec({ hours: 1 })),
+ ),
+ merchant: {
+ name: "(wallet)",
+ },
+ merchant_pub: merchantPair.pub,
+ refund_deadline: TalerProtocolTimestamp.zero(),
+ };
+
+ const { h: contractTermsHash } = await wex.cryptoApi.hashString({
+ str: canonicalJson(contractTerms),
+ });
+
+ const contractData = extractContractData(
+ contractTerms,
+ contractTermsHash,
+ "",
+ );
+
+ const payCoinSel = await selectPayCoins(wex, {
+ restrictExchanges: {
+ auditors: [],
+ exchanges: contractData.allowedExchanges,
+ },
+ restrictWireMethod: contractData.wireMethod,
+ contractTermsAmount: Amounts.parseOrThrow(contractData.amount),
+ depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee),
+ prevPayCoins: [],
+ });
+
+ let coins: SelectedProspectiveCoin[] | undefined = undefined;
+
+ switch (payCoinSel.type) {
+ case "success":
+ coins = payCoinSel.coinSel.coins;
+ break;
+ case "failure":
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE,
+ {
+ insufficientBalanceDetails: payCoinSel.insufficientBalanceDetails,
+ },
+ );
+ case "prospective":
+ coins = payCoinSel.result.prospectiveCoins;
+ break;
+ default:
+ assertUnreachable(payCoinSel);
+ }
+
+ const totalDepositCost = await getTotalPaymentCost(wex, currency, coins);
+
+ let depositGroupId: string;
+ if (req.transactionId) {
+ const txId = parseTransactionIdentifier(req.transactionId);
+ if (!txId || txId.tag !== TransactionType.Deposit) {
+ throw Error("invalid transaction ID");
+ }
+ depositGroupId = txId.depositGroupId;
+ } else {
+ depositGroupId = encodeCrock(getRandomBytes(32));
+ }
+
+ const infoPerExchange: Record<string, DepositInfoPerExchange> = {};
+
+ for (let i = 0; i < coins.length; i++) {
+ let depPerExchange = infoPerExchange[coins[i].exchangeBaseUrl];
+ if (!depPerExchange) {
+ infoPerExchange[coins[i].exchangeBaseUrl] = depPerExchange = {
+ amountEffective: Amounts.stringify(
+ Amounts.zeroOfAmount(totalDepositCost),
+ ),
+ };
+ }
+ const contrib = coins[i].contribution;
+ depPerExchange.amountEffective = Amounts.stringify(
+ Amounts.add(depPerExchange.amountEffective, contrib).amount,
+ );
+ }
+
+ const counterpartyEffectiveDepositAmount =
+ await getCounterpartyEffectiveDepositAmount(wex, p.targetType, coins);
+
+ const depositGroup: DepositGroupRecord = {
+ contractTermsHash,
+ depositGroupId,
+ currency: Amounts.currencyOf(totalDepositCost),
+ amount: contractData.amount,
+ noncePriv: noncePair.priv,
+ noncePub: noncePair.pub,
+ timestampCreated: timestampPreciseToDb(
+ AbsoluteTime.toPreciseTimestamp(now),
+ ),
+ timestampFinished: undefined,
+ statusPerCoin: undefined,
+ payCoinSelection: undefined,
+ payCoinSelectionUid: undefined,
+ merchantPriv: merchantPair.priv,
+ merchantPub: merchantPair.pub,
+ totalPayCost: Amounts.stringify(totalDepositCost),
+ counterpartyEffectiveDepositAmount: Amounts.stringify(
+ counterpartyEffectiveDepositAmount,
+ ),
+ wireTransferDeadline: timestampProtocolToDb(
+ contractTerms.wire_transfer_deadline,
+ ),
+ wire: {
+ payto_uri: req.depositPaytoUri,
+ salt: wireSalt,
+ },
+ operationStatus: DepositOperationStatus.PendingDeposit,
+ infoPerExchange,
+ };
+
+ if (payCoinSel.type === "success") {
+ depositGroup.payCoinSelection = {
+ coinContributions: payCoinSel.coinSel.coins.map((x) => x.contribution),
+ coinPubs: payCoinSel.coinSel.coins.map((x) => x.coinPub),
+ };
+ depositGroup.payCoinSelectionUid = encodeCrock(getRandomBytes(32));
+ depositGroup.statusPerCoin = payCoinSel.coinSel.coins.map(
+ () => DepositElementStatus.DepositPending,
+ );
+ }
+
+ const ctx = new DepositTransactionContext(wex, depositGroupId);
+ const transactionId = ctx.transactionId;
+
+ const newTxState = await wex.db.runReadWriteTx(
+ {
+ storeNames: [
+ "depositGroups",
+ "coins",
+ "recoupGroups",
+ "denominations",
+ "refreshGroups",
+ "refreshSessions",
+ "coinAvailability",
+ "contractTerms",
+ ],
+ },
+ async (tx) => {
+ if (depositGroup.payCoinSelection) {
+ await spendCoins(wex, tx, {
+ allocationId: transactionId,
+ coinPubs: depositGroup.payCoinSelection.coinPubs,
+ contributions: depositGroup.payCoinSelection.coinContributions.map(
+ (x) => Amounts.parseOrThrow(x),
+ ),
+ refreshReason: RefreshReason.PayDeposit,
+ });
+ }
+ await tx.depositGroups.put(depositGroup);
+ await tx.contractTerms.put({
+ contractTermsRaw: contractTerms,
+ h: contractTermsHash,
+ });
+ return computeDepositTransactionStatus(depositGroup);
+ },
+ );
+
+ wex.ws.notify({
+ type: NotificationType.TransactionStateTransition,
+ transactionId,
+ oldTxState: {
+ major: TransactionMajorState.None,
+ },
+ newTxState,
+ });
+
+ wex.ws.notify({
+ type: NotificationType.BalanceChange,
+ hintTransactionId: transactionId,
+ });
+
+ wex.taskScheduler.startShepherdTask(ctx.taskId);
+
+ return {
+ depositGroupId,
+ transactionId,
+ };
+}
+
+/**
+ * Get the amount that will be deposited on the users bank
+ * account after depositing, not considering aggregation.
+ */
+export async function getCounterpartyEffectiveDepositAmount(
+ wex: WalletExecutionContext,
+ wireType: string,
+ pcs: SelectedProspectiveCoin[],
+): Promise<AmountJson> {
+ const amt: AmountJson[] = [];
+ const fees: AmountJson[] = [];
+ const exchangeSet: Set<string> = new Set();
+
+ await wex.db.runReadOnlyTx(
+ { storeNames: ["coins", "denominations", "exchangeDetails", "exchanges"] },
+ async (tx) => {
+ for (let i = 0; i < pcs.length; i++) {
+ const denom = await getDenomInfo(
+ wex,
+ tx,
+ pcs[i].exchangeBaseUrl,
+ pcs[i].denomPubHash,
+ );
+ if (!denom) {
+ throw Error("can't find denomination to calculate deposit amount");
+ }
+ amt.push(Amounts.parseOrThrow(pcs[i].contribution));
+ fees.push(Amounts.parseOrThrow(denom.feeDeposit));
+ exchangeSet.add(pcs[i].exchangeBaseUrl);
+ }
+
+ for (const exchangeUrl of exchangeSet.values()) {
+ const exchangeDetails = await getExchangeWireDetailsInTx(
+ tx,
+ exchangeUrl,
+ );
+ if (!exchangeDetails) {
+ continue;
+ }
+
+ // FIXME/NOTE: the line below _likely_ throws exception
+ // about "find method not found on undefined" when the wireType
+ // is not supported by the Exchange.
+ const fee = exchangeDetails.wireInfo.feesForType[wireType].find((x) => {
+ return AbsoluteTime.isBetween(
+ AbsoluteTime.now(),
+ AbsoluteTime.fromProtocolTimestamp(x.startStamp),
+ AbsoluteTime.fromProtocolTimestamp(x.endStamp),
+ );
+ })?.wireFee;
+ if (fee) {
+ fees.push(Amounts.parseOrThrow(fee));
+ }
+ }
+ },
+ );
+ return Amounts.sub(Amounts.sum(amt).amount, Amounts.sum(fees).amount).amount;
+}
+
+/**
+ * Get the fee amount that will be charged when trying to deposit the
+ * specified amount using the selected coins and the wire method.
+ */
+async function getTotalFeesForDepositAmount(
+ wex: WalletExecutionContext,
+ wireType: string,
+ total: AmountJson,
+ pcs: SelectedProspectiveCoin[],
+): Promise<DepositGroupFees> {
+ const wireFee: AmountJson[] = [];
+ const coinFee: AmountJson[] = [];
+ const refreshFee: AmountJson[] = [];
+ const exchangeSet: Set<string> = new Set();
+
+ await wex.db.runReadOnlyTx(
+ { storeNames: ["coins", "denominations", "exchanges", "exchangeDetails"] },
+ async (tx) => {
+ for (let i = 0; i < pcs.length; i++) {
+ const denom = await getDenomInfo(
+ wex,
+ tx,
+ pcs[i].exchangeBaseUrl,
+ pcs[i].denomPubHash,
+ );
+ if (!denom) {
+ throw Error("can't find denomination to calculate deposit amount");
+ }
+ coinFee.push(Amounts.parseOrThrow(denom.feeDeposit));
+ exchangeSet.add(pcs[i].exchangeBaseUrl);
+ const amountLeft = Amounts.sub(denom.value, pcs[i].contribution).amount;
+ const refreshCost = await getTotalRefreshCost(
+ wex,
+ tx,
+ denom,
+ amountLeft,
+ );
+ refreshFee.push(refreshCost);
+ }
+
+ for (const exchangeUrl of exchangeSet.values()) {
+ const exchangeDetails = await getExchangeWireDetailsInTx(
+ tx,
+ exchangeUrl,
+ );
+ if (!exchangeDetails) {
+ continue;
+ }
+ const fee = exchangeDetails.wireInfo.feesForType[wireType]?.find(
+ (x) => {
+ return AbsoluteTime.isBetween(
+ AbsoluteTime.now(),
+ AbsoluteTime.fromProtocolTimestamp(x.startStamp),
+ AbsoluteTime.fromProtocolTimestamp(x.endStamp),
+ );
+ },
+ )?.wireFee;
+ if (fee) {
+ wireFee.push(Amounts.parseOrThrow(fee));
+ }
+ }
+ },
+ );
+
+ return {
+ coin: Amounts.stringify(Amounts.sumOrZero(total.currency, coinFee).amount),
+ wire: Amounts.stringify(Amounts.sumOrZero(total.currency, wireFee).amount),
+ refresh: Amounts.stringify(
+ Amounts.sumOrZero(total.currency, refreshFee).amount,
+ ),
+ };
+}
diff --git a/packages/taler-wallet-core/src/dev-experiments.ts b/packages/taler-wallet-core/src/dev-experiments.ts
index e1de7dbf1..5cb9400be 100644
--- a/packages/taler-wallet-core/src/dev-experiments.ts
+++ b/packages/taler-wallet-core/src/dev-experiments.ts
@@ -25,51 +25,37 @@
* Imports.
*/
-import { Logger, parseDevExperimentUri } from "@gnu-taler/taler-util";
-import { ConfigRecordKey } from "./db.js";
-import { InternalWalletState } from "./internal-wallet-state.js";
+import {
+ DenomLossEventType,
+ Logger,
+ RefreshReason,
+ TalerPreciseTimestamp,
+ encodeCrock,
+ getRandomBytes,
+ parseDevExperimentUri,
+} from "@gnu-taler/taler-util";
import {
HttpRequestLibrary,
HttpRequestOptions,
HttpResponse,
-} from "./util/http.js";
+} from "@gnu-taler/taler-util/http";
+import { PendingTaskType, constructTaskIdentifier } from "./common.js";
+import {
+ DenomLossEventRecord,
+ DenomLossStatus,
+ RefreshGroupRecord,
+ RefreshOperationStatus,
+ timestampPreciseToDb,
+} from "./db.js";
+import { WalletExecutionContext } from "./wallet.js";
const logger = new Logger("dev-experiments.ts");
-export async function setDevMode(
- ws: InternalWalletState,
- enabled: boolean,
-): Promise<void> {
- if (enabled) {
- logger.info("enabling devmode");
- await ws.db
- .mktx((x) => [x.config])
- .runReadWrite(async (tx) => {
- tx.config.put({
- key: ConfigRecordKey.DevMode,
- value: true,
- });
- });
- await maybeInitDevMode(ws);
- } else {
- logger.info("disabling devmode");
- await ws.db
- .mktx((x) => [x.config])
- .runReadWrite(async (tx) => {
- tx.config.put({
- key: ConfigRecordKey.DevMode,
- value: false,
- });
- });
- await leaveDevMode(ws);
- }
-}
-
/**
* Apply a dev experiment to the wallet database / state.
*/
export async function applyDevExperiment(
- ws: InternalWalletState,
+ wex: WalletExecutionContext,
uri: string,
): Promise<void> {
logger.info(`applying dev experiment ${uri}`);
@@ -78,43 +64,75 @@ export async function applyDevExperiment(
logger.info("unable to parse dev experiment URI");
return;
}
- if (!ws.devModeActive) {
- throw Error(
- "can't handle devmode URI (other than enable-devmode) unless devmode is active",
- );
+ if (!wex.ws.config.testing.devModeActive) {
+ throw Error("can't handle devmode URI unless devmode is active");
}
- throw Error(`dev-experiment id not understood ${parsedUri.devExperimentId}`);
-}
-/**
- * Enter dev mode, if the wallet's config entry in the DB demands it.
- */
-export async function maybeInitDevMode(ws: InternalWalletState): Promise<void> {
- const devMode = await ws.db
- .mktx((x) => [x.config])
- .runReadOnly(async (tx) => {
- const rec = await tx.config.get(ConfigRecordKey.DevMode);
- if (!rec || rec.key !== ConfigRecordKey.DevMode) {
- return false;
- }
- return rec.value;
- });
- if (!devMode) {
- ws.devModeActive = false;
- return;
- }
- ws.devModeActive = true;
- if (ws.http instanceof DevExperimentHttpLib) {
- return;
+ switch (parsedUri.devExperimentId) {
+ case "start-block-refresh": {
+ wex.ws.devExperimentState.blockRefreshes = true;
+ return;
+ }
+ case "stop-block-refresh": {
+ wex.ws.devExperimentState.blockRefreshes = false;
+ return;
+ }
+ case "insert-pending-refresh": {
+ const refreshGroupId = encodeCrock(getRandomBytes(32));
+ await wex.db.runReadWriteTx(
+ { storeNames: ["refreshGroups"] },
+ async (tx) => {
+ const newRg: RefreshGroupRecord = {
+ currency: "TESTKUDOS",
+ expectedOutputPerCoin: [],
+ inputPerCoin: [],
+ oldCoinPubs: [],
+ operationStatus: RefreshOperationStatus.Pending,
+ reason: RefreshReason.Manual,
+ refreshGroupId,
+ statusPerCoin: [],
+ timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()),
+ timestampFinished: undefined,
+ originatingTransactionId: undefined,
+ infoPerExchange: {},
+ };
+ await tx.refreshGroups.put(newRg);
+ },
+ );
+ wex.taskScheduler.startShepherdTask(
+ constructTaskIdentifier({
+ tag: PendingTaskType.Refresh,
+ refreshGroupId,
+ }),
+ );
+ return;
+ }
+ case "insert-denom-loss": {
+ await wex.db.runReadWriteTx(
+ { storeNames: ["denomLossEvents"] },
+ async (tx) => {
+ const eventId = encodeCrock(getRandomBytes(32));
+ const newRg: DenomLossEventRecord = {
+ amount: "TESTKUDOS:42",
+ currency: "TESTKUDOS",
+ exchangeBaseUrl: "https://exchange.test.taler.net/",
+ denomLossEventId: eventId,
+ denomPubHashes: [
+ encodeCrock(getRandomBytes(64)),
+ encodeCrock(getRandomBytes(64)),
+ ],
+ eventType: DenomLossEventType.DenomExpired,
+ status: DenomLossStatus.Done,
+ timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()),
+ };
+ await tx.denomLossEvents.put(newRg);
+ },
+ );
+ return;
+ }
}
- ws.http = new DevExperimentHttpLib(ws.http);
-}
-export async function leaveDevMode(ws: InternalWalletState): Promise<void> {
- if (ws.http instanceof DevExperimentHttpLib) {
- ws.http = ws.http.underlyingLib;
- }
- ws.devModeActive = false;
+ throw Error(`dev-experiment id not understood ${parsedUri.devExperimentId}`);
}
export class DevExperimentHttpLib implements HttpRequestLibrary {
@@ -125,28 +143,11 @@ export class DevExperimentHttpLib implements HttpRequestLibrary {
this.underlyingLib = lib;
}
- get(
- url: string,
- opt?: HttpRequestOptions | undefined,
- ): Promise<HttpResponse> {
- logger.info(`devexperiment httplib ${url}`);
- return this.underlyingLib.get(url, opt);
- }
-
- postJson(
- url: string,
- body: any,
- opt?: HttpRequestOptions | undefined,
- ): Promise<HttpResponse> {
- logger.info(`devexperiment httplib ${url}`);
- return this.underlyingLib.postJson(url, body, opt);
- }
-
fetch(
url: string,
opt?: HttpRequestOptions | undefined,
): Promise<HttpResponse> {
- logger.info(`devexperiment httplib ${url}`);
+ logger.trace(`devexperiment httplib ${url}`);
return this.underlyingLib.fetch(url, opt);
}
}
diff --git a/packages/taler-wallet-core/src/exchanges.ts b/packages/taler-wallet-core/src/exchanges.ts
new file mode 100644
index 000000000..d5ca7abbf
--- /dev/null
+++ b/packages/taler-wallet-core/src/exchanges.ts
@@ -0,0 +1,2591 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 GNUnet e.V.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * @fileoverview
+ * Implementation of exchange entry management in wallet-core.
+ * The details of exchange entry management are specified in DD48.
+ */
+
+/**
+ * Imports.
+ */
+import {
+ AbsoluteTime,
+ AgeRestriction,
+ Amount,
+ Amounts,
+ AsyncFlag,
+ CancellationToken,
+ CoinRefreshRequest,
+ CoinStatus,
+ DeleteExchangeRequest,
+ DenomKeyType,
+ DenomLossEventType,
+ DenomOperationMap,
+ DenominationInfo,
+ DenominationPubKey,
+ Duration,
+ EddsaPublicKeyString,
+ ExchangeAuditor,
+ ExchangeDetailedResponse,
+ ExchangeGlobalFees,
+ ExchangeListItem,
+ ExchangeSignKeyJson,
+ ExchangeTosStatus,
+ ExchangeWireAccount,
+ ExchangesListResponse,
+ FeeDescription,
+ GetExchangeEntryByUrlRequest,
+ GetExchangeResourcesResponse,
+ GetExchangeTosResult,
+ GlobalFees,
+ LibtoolVersion,
+ Logger,
+ NotificationType,
+ OperationErrorInfo,
+ Recoup,
+ RefreshReason,
+ ScopeInfo,
+ ScopeType,
+ TalerError,
+ TalerErrorCode,
+ TalerErrorDetail,
+ TalerPreciseTimestamp,
+ TalerProtocolDuration,
+ TalerProtocolTimestamp,
+ TransactionIdStr,
+ TransactionMajorState,
+ TransactionState,
+ TransactionType,
+ URL,
+ WalletNotification,
+ WireFee,
+ WireFeeMap,
+ WireFeesJson,
+ WireInfo,
+ assertUnreachable,
+ canonicalizeBaseUrl,
+ checkDbInvariant,
+ codecForExchangeKeysJson,
+ durationMul,
+ encodeCrock,
+ getRandomBytes,
+ hashDenomPub,
+ j2s,
+ makeErrorDetail,
+ parsePaytoUri,
+} from "@gnu-taler/taler-util";
+import {
+ HttpRequestLibrary,
+ getExpiry,
+ readSuccessResponseJsonOrThrow,
+ readSuccessResponseTextOrThrow,
+} from "@gnu-taler/taler-util/http";
+import {
+ PendingTaskType,
+ TaskIdStr,
+ TaskIdentifiers,
+ TaskRunResult,
+ TaskRunResultType,
+ TransactionContext,
+ computeDbBackoff,
+ constructTaskIdentifier,
+ getAutoRefreshExecuteThreshold,
+ getExchangeEntryStatusFromRecord,
+ getExchangeState,
+ getExchangeTosStatusFromRecord,
+ getExchangeUpdateStatusFromRecord,
+} from "./common.js";
+import {
+ DenomLossEventRecord,
+ DenomLossStatus,
+ DenominationRecord,
+ DenominationVerificationStatus,
+ ExchangeDetailsRecord,
+ ExchangeEntryDbRecordStatus,
+ ExchangeEntryDbUpdateStatus,
+ ExchangeEntryRecord,
+ WalletDbReadOnlyTransaction,
+ WalletDbReadWriteTransaction,
+ WalletStoresV1,
+ timestampAbsoluteFromDb,
+ timestampOptionalPreciseFromDb,
+ timestampPreciseFromDb,
+ timestampPreciseToDb,
+ timestampProtocolFromDb,
+ timestampProtocolToDb,
+} from "./db.js";
+import {
+ createTimeline,
+ isWithdrawableDenom,
+ selectBestForOverlappingDenominations,
+ selectMinimumFee,
+} from "./denominations.js";
+import { DbReadOnlyTransaction } from "./query.js";
+import { createRecoupGroup } from "./recoup.js";
+import { createRefreshGroup } from "./refresh.js";
+import {
+ constructTransactionIdentifier,
+ notifyTransition,
+} from "./transactions.js";
+import { WALLET_EXCHANGE_PROTOCOL_VERSION } from "./versions.js";
+import { InternalWalletState, WalletExecutionContext } from "./wallet.js";
+
+const logger = new Logger("exchanges.ts");
+
+function getExchangeRequestTimeout(): Duration {
+ return Duration.fromSpec({
+ seconds: 15,
+ });
+}
+
+interface ExchangeTosDownloadResult {
+ tosText: string;
+ tosEtag: string;
+ tosContentType: string;
+ tosContentLanguage: string | undefined;
+ tosAvailableLanguages: string[];
+}
+
+async function downloadExchangeWithTermsOfService(
+ wex: WalletExecutionContext,
+ exchangeBaseUrl: string,
+ http: HttpRequestLibrary,
+ timeout: Duration,
+ acceptFormat: string,
+ acceptLanguage: string | undefined,
+): Promise<ExchangeTosDownloadResult> {
+ logger.trace(`downloading exchange tos (type ${acceptFormat})`);
+ const reqUrl = new URL("terms", exchangeBaseUrl);
+ const headers: {
+ Accept: string;
+ "Accept-Language"?: string;
+ } = {
+ Accept: acceptFormat,
+ };
+
+ if (acceptLanguage) {
+ headers["Accept-Language"] = acceptLanguage;
+ }
+
+ const resp = await http.fetch(reqUrl.href, {
+ headers,
+ timeout,
+ cancellationToken: wex.cancellationToken,
+ });
+ const tosText = await readSuccessResponseTextOrThrow(resp);
+ const tosEtag = resp.headers.get("etag") || "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") || "";
+ // Work around exchange bug that reports the same language multiple times.
+ const availLangSet = new Set<string>(
+ availLangStr.split(",").map((x) => x.trim()),
+ );
+ const tosAvailableLanguages = [...availLangSet];
+
+ return {
+ tosText,
+ tosEtag,
+ tosContentType,
+ tosContentLanguage,
+ tosAvailableLanguages,
+ };
+}
+
+/**
+ * Get exchange details from the database.
+ */
+async function getExchangeRecordsInternal(
+ tx: WalletDbReadOnlyTransaction<["exchanges", "exchangeDetails"]>,
+ exchangeBaseUrl: string,
+): Promise<ExchangeDetailsRecord | undefined> {
+ const r = await tx.exchanges.get(exchangeBaseUrl);
+ if (!r) {
+ logger.warn(`no exchange found for ${exchangeBaseUrl}`);
+ return;
+ }
+ const dp = r.detailsPointer;
+ if (!dp) {
+ logger.warn(`no exchange details pointer for ${exchangeBaseUrl}`);
+ return;
+ }
+ const { currency, masterPublicKey } = dp;
+ const details = await tx.exchangeDetails.indexes.byPointer.get([
+ r.baseUrl,
+ currency,
+ masterPublicKey,
+ ]);
+ if (!details) {
+ logger.warn(
+ `no exchange details with pointer ${j2s(dp)} for ${exchangeBaseUrl}`,
+ );
+ }
+ return details;
+}
+
+export async function getExchangeScopeInfo(
+ tx: WalletDbReadOnlyTransaction<
+ [
+ "exchanges",
+ "exchangeDetails",
+ "globalCurrencyExchanges",
+ "globalCurrencyAuditors",
+ ]
+ >,
+ exchangeBaseUrl: string,
+ currency: string,
+): Promise<ScopeInfo> {
+ const det = await getExchangeRecordsInternal(tx, exchangeBaseUrl);
+ if (!det) {
+ return {
+ type: ScopeType.Exchange,
+ currency: currency,
+ url: exchangeBaseUrl,
+ };
+ }
+ return internalGetExchangeScopeInfo(tx, det);
+}
+
+async function internalGetExchangeScopeInfo(
+ tx: WalletDbReadOnlyTransaction<
+ ["globalCurrencyExchanges", "globalCurrencyAuditors"]
+ >,
+ exchangeDetails: ExchangeDetailsRecord,
+): Promise<ScopeInfo> {
+ const globalExchangeRec =
+ await tx.globalCurrencyExchanges.indexes.byCurrencyAndUrlAndPub.get([
+ exchangeDetails.currency,
+ exchangeDetails.exchangeBaseUrl,
+ exchangeDetails.masterPublicKey,
+ ]);
+ if (globalExchangeRec) {
+ return {
+ currency: exchangeDetails.currency,
+ type: ScopeType.Global,
+ };
+ } else {
+ for (const aud of exchangeDetails.auditors) {
+ const globalAuditorRec =
+ await tx.globalCurrencyAuditors.indexes.byCurrencyAndUrlAndPub.get([
+ exchangeDetails.currency,
+ aud.auditor_url,
+ aud.auditor_pub,
+ ]);
+ if (globalAuditorRec) {
+ return {
+ currency: exchangeDetails.currency,
+ type: ScopeType.Auditor,
+ url: aud.auditor_url,
+ };
+ }
+ }
+ }
+ return {
+ currency: exchangeDetails.currency,
+ type: ScopeType.Exchange,
+ url: exchangeDetails.exchangeBaseUrl,
+ };
+}
+
+async function makeExchangeListItem(
+ tx: WalletDbReadOnlyTransaction<
+ ["globalCurrencyExchanges", "globalCurrencyAuditors"]
+ >,
+ r: ExchangeEntryRecord,
+ exchangeDetails: ExchangeDetailsRecord | undefined,
+ lastError: TalerErrorDetail | undefined,
+): Promise<ExchangeListItem> {
+ const lastUpdateErrorInfo: OperationErrorInfo | undefined = lastError
+ ? {
+ error: lastError,
+ }
+ : undefined;
+
+ let scopeInfo: ScopeInfo | undefined = undefined;
+
+ if (exchangeDetails) {
+ scopeInfo = await internalGetExchangeScopeInfo(tx, exchangeDetails);
+ }
+
+ return {
+ exchangeBaseUrl: r.baseUrl,
+ masterPub: exchangeDetails?.masterPublicKey,
+ noFees: r.noFees ?? false,
+ peerPaymentsDisabled: r.peerPaymentsDisabled ?? false,
+ currency: exchangeDetails?.currency ?? r.presetCurrencyHint ?? "UNKNOWN",
+ exchangeUpdateStatus: getExchangeUpdateStatusFromRecord(r),
+ exchangeEntryStatus: getExchangeEntryStatusFromRecord(r),
+ tosStatus: getExchangeTosStatusFromRecord(r),
+ ageRestrictionOptions: exchangeDetails?.ageMask
+ ? AgeRestriction.getAgeGroupsFromMask(exchangeDetails.ageMask)
+ : [],
+ paytoUris: exchangeDetails?.wireInfo.accounts.map((x) => x.payto_uri) ?? [],
+ lastUpdateTimestamp: timestampOptionalPreciseFromDb(r.lastUpdate),
+ lastUpdateErrorInfo,
+ scopeInfo: scopeInfo ?? {
+ type: ScopeType.Exchange,
+ currency: "UNKNOWN",
+ url: r.baseUrl,
+ },
+ };
+}
+
+export interface ExchangeWireDetails {
+ currency: string;
+ masterPublicKey: EddsaPublicKeyString;
+ wireInfo: WireInfo;
+ exchangeBaseUrl: string;
+ auditors: ExchangeAuditor[];
+ globalFees: ExchangeGlobalFees[];
+}
+
+export async function getExchangeWireDetailsInTx(
+ tx: WalletDbReadOnlyTransaction<["exchanges", "exchangeDetails"]>,
+ exchangeBaseUrl: string,
+): Promise<ExchangeWireDetails | undefined> {
+ const det = await getExchangeRecordsInternal(tx, exchangeBaseUrl);
+ if (!det) {
+ return undefined;
+ }
+ return {
+ currency: det.currency,
+ masterPublicKey: det.masterPublicKey,
+ wireInfo: det.wireInfo,
+ exchangeBaseUrl: det.exchangeBaseUrl,
+ auditors: det.auditors,
+ globalFees: det.globalFees,
+ };
+}
+
+export async function lookupExchangeByUri(
+ wex: WalletExecutionContext,
+ req: GetExchangeEntryByUrlRequest,
+): Promise<ExchangeListItem> {
+ return await wex.db.runReadOnlyTx(
+ {
+ storeNames: [
+ "exchanges",
+ "exchangeDetails",
+ "operationRetries",
+ "globalCurrencyAuditors",
+ "globalCurrencyExchanges",
+ ],
+ },
+ async (tx) => {
+ const exchangeRec = await tx.exchanges.get(req.exchangeBaseUrl);
+ if (!exchangeRec) {
+ throw Error("exchange not found");
+ }
+ const exchangeDetails = await getExchangeRecordsInternal(
+ tx,
+ exchangeRec.baseUrl,
+ );
+ const opRetryRecord = await tx.operationRetries.get(
+ TaskIdentifiers.forExchangeUpdate(exchangeRec),
+ );
+ return await makeExchangeListItem(
+ tx,
+ exchangeRec,
+ exchangeDetails,
+ opRetryRecord?.lastError,
+ );
+ },
+ );
+}
+
+/**
+ * Mark the current ToS version as accepted by the user.
+ */
+export async function acceptExchangeTermsOfService(
+ wex: WalletExecutionContext,
+ exchangeBaseUrl: string,
+): Promise<void> {
+ const notif = await wex.db.runReadWriteTx(
+ { storeNames: ["exchangeDetails", "exchanges"] },
+ async (tx) => {
+ const exch = await tx.exchanges.get(exchangeBaseUrl);
+ if (exch && exch.tosCurrentEtag) {
+ const oldExchangeState = getExchangeState(exch);
+ exch.tosAcceptedEtag = exch.tosCurrentEtag;
+ exch.tosAcceptedTimestamp = timestampPreciseToDb(
+ TalerPreciseTimestamp.now(),
+ );
+ await tx.exchanges.put(exch);
+ const newExchangeState = getExchangeState(exch);
+ wex.ws.exchangeCache.clear();
+ return {
+ type: NotificationType.ExchangeStateTransition,
+ exchangeBaseUrl,
+ newExchangeState: newExchangeState,
+ oldExchangeState: oldExchangeState,
+ } satisfies WalletNotification;
+ }
+ return undefined;
+ },
+ );
+ if (notif) {
+ wex.ws.notify(notif);
+ }
+}
+
+/**
+ * Mark the current ToS version as accepted by the user.
+ */
+export async function forgetExchangeTermsOfService(
+ wex: WalletExecutionContext,
+ exchangeBaseUrl: string,
+): Promise<void> {
+ const notif = await wex.db.runReadWriteTx(
+ { storeNames: ["exchangeDetails", "exchanges"] },
+ async (tx) => {
+ const exch = await tx.exchanges.get(exchangeBaseUrl);
+ if (exch) {
+ const oldExchangeState = getExchangeState(exch);
+ exch.tosAcceptedEtag = undefined;
+ exch.tosAcceptedTimestamp = undefined;
+ await tx.exchanges.put(exch);
+ const newExchangeState = getExchangeState(exch);
+ wex.ws.exchangeCache.clear();
+ return {
+ type: NotificationType.ExchangeStateTransition,
+ exchangeBaseUrl,
+ newExchangeState: newExchangeState,
+ oldExchangeState: oldExchangeState,
+ } satisfies WalletNotification;
+ }
+ return undefined;
+ },
+ );
+ if (notif) {
+ wex.ws.notify(notif);
+ }
+}
+
+/**
+ * Validate wire fees and wire accounts.
+ *
+ * Throw an exception if they are invalid.
+ */
+async function validateWireInfo(
+ wex: WalletExecutionContext,
+ versionCurrent: number,
+ wireInfo: ExchangeKeysDownloadResult,
+ masterPublicKey: string,
+): Promise<WireInfo> {
+ for (const a of wireInfo.accounts) {
+ logger.trace("validating exchange acct");
+ let isValid = false;
+ if (wex.ws.config.testing.insecureTrustExchange) {
+ isValid = true;
+ } else {
+ const { valid: v } = await wex.ws.cryptoApi.isValidWireAccount({
+ masterPub: masterPublicKey,
+ paytoUri: a.payto_uri,
+ sig: a.master_sig,
+ versionCurrent,
+ conversionUrl: a.conversion_url,
+ creditRestrictions: a.credit_restrictions,
+ debitRestrictions: a.debit_restrictions,
+ });
+ isValid = v;
+ }
+ if (!isValid) {
+ throw Error("exchange acct signature invalid");
+ }
+ }
+ logger.trace("account validation done");
+ const feesForType: WireFeeMap = {};
+ for (const wireMethod of Object.keys(wireInfo.wireFees)) {
+ const feeList: WireFee[] = [];
+ for (const x of wireInfo.wireFees[wireMethod]) {
+ const startStamp = x.start_date;
+ const endStamp = x.end_date;
+ const fee: WireFee = {
+ closingFee: Amounts.stringify(x.closing_fee),
+ endStamp,
+ sig: x.sig,
+ startStamp,
+ wireFee: Amounts.stringify(x.wire_fee),
+ };
+ let isValid = false;
+ if (wex.ws.config.testing.insecureTrustExchange) {
+ isValid = true;
+ } else {
+ const { valid: v } = await wex.ws.cryptoApi.isValidWireFee({
+ masterPub: masterPublicKey,
+ type: wireMethod,
+ wf: fee,
+ });
+ isValid = v;
+ }
+ if (!isValid) {
+ throw Error("exchange wire fee signature invalid");
+ }
+ feeList.push(fee);
+ }
+ feesForType[wireMethod] = feeList;
+ }
+
+ return {
+ accounts: wireInfo.accounts,
+ feesForType,
+ };
+}
+
+/**
+ * Validate global fees.
+ *
+ * Throw an exception if they are invalid.
+ */
+async function validateGlobalFees(
+ wex: WalletExecutionContext,
+ fees: GlobalFees[],
+ masterPub: string,
+): Promise<ExchangeGlobalFees[]> {
+ const egf: ExchangeGlobalFees[] = [];
+ for (const gf of fees) {
+ logger.trace("validating exchange global fees");
+ let isValid = false;
+ if (wex.ws.config.testing.insecureTrustExchange) {
+ isValid = true;
+ } else {
+ const { valid: v } = await wex.cryptoApi.isValidGlobalFees({
+ masterPub,
+ gf,
+ });
+ isValid = v;
+ }
+
+ if (!isValid) {
+ throw Error("exchange global fees signature invalid: " + gf.master_sig);
+ }
+ egf.push({
+ accountFee: Amounts.stringify(gf.account_fee),
+ historyFee: Amounts.stringify(gf.history_fee),
+ purseFee: Amounts.stringify(gf.purse_fee),
+ startDate: gf.start_date,
+ endDate: gf.end_date,
+ signature: gf.master_sig,
+ historyTimeout: gf.history_expiration,
+ purseLimit: gf.purse_account_limit,
+ purseTimeout: gf.purse_timeout,
+ });
+ }
+
+ return egf;
+}
+
+/**
+ * Add an exchange entry to the wallet database in the
+ * entry state "preset".
+ *
+ * Returns the notification to the caller that should be emitted
+ * if the DB transaction succeeds.
+ */
+export async function addPresetExchangeEntry(
+ tx: WalletDbReadWriteTransaction<["exchanges"]>,
+ exchangeBaseUrl: string,
+ currencyHint?: string,
+): Promise<{ notification?: WalletNotification }> {
+ let exchange = await tx.exchanges.get(exchangeBaseUrl);
+ if (!exchange) {
+ const r: ExchangeEntryRecord = {
+ entryStatus: ExchangeEntryDbRecordStatus.Preset,
+ updateStatus: ExchangeEntryDbUpdateStatus.Initial,
+ baseUrl: exchangeBaseUrl,
+ presetCurrencyHint: currencyHint,
+ detailsPointer: undefined,
+ lastUpdate: undefined,
+ lastKeysEtag: undefined,
+ nextRefreshCheckStamp: timestampPreciseToDb(
+ AbsoluteTime.toPreciseTimestamp(AbsoluteTime.never()),
+ ),
+ nextUpdateStamp: timestampPreciseToDb(
+ AbsoluteTime.toPreciseTimestamp(AbsoluteTime.never()),
+ ),
+ tosAcceptedEtag: undefined,
+ tosAcceptedTimestamp: undefined,
+ tosCurrentEtag: undefined,
+ };
+ await tx.exchanges.put(r);
+ return {
+ notification: {
+ type: NotificationType.ExchangeStateTransition,
+ exchangeBaseUrl: exchangeBaseUrl,
+ // Exchange did not exist yet
+ oldExchangeState: undefined,
+ newExchangeState: getExchangeState(r),
+ },
+ };
+ }
+ return {};
+}
+
+async function provideExchangeRecordInTx(
+ ws: InternalWalletState,
+ tx: WalletDbReadWriteTransaction<["exchanges", "exchangeDetails"]>,
+ baseUrl: string,
+): Promise<{
+ exchange: ExchangeEntryRecord;
+ exchangeDetails: ExchangeDetailsRecord | undefined;
+ notification?: WalletNotification;
+}> {
+ let notification: WalletNotification | undefined = undefined;
+ let exchange = await tx.exchanges.get(baseUrl);
+ if (!exchange) {
+ const r: ExchangeEntryRecord = {
+ entryStatus: ExchangeEntryDbRecordStatus.Ephemeral,
+ updateStatus: ExchangeEntryDbUpdateStatus.InitialUpdate,
+ baseUrl: baseUrl,
+ detailsPointer: undefined,
+ lastUpdate: undefined,
+ nextUpdateStamp: timestampPreciseToDb(
+ AbsoluteTime.toPreciseTimestamp(AbsoluteTime.never()),
+ ),
+ nextRefreshCheckStamp: timestampPreciseToDb(
+ AbsoluteTime.toPreciseTimestamp(AbsoluteTime.never()),
+ ),
+ // The first update should always be done in a way that ignores the cache,
+ // so that removing and re-adding an exchange works properly, even
+ // if /keys is cached in the browser.
+ cachebreakNextUpdate: true,
+ lastKeysEtag: undefined,
+ tosAcceptedEtag: undefined,
+ tosAcceptedTimestamp: undefined,
+ tosCurrentEtag: undefined,
+ };
+ await tx.exchanges.put(r);
+ exchange = r;
+ notification = {
+ type: NotificationType.ExchangeStateTransition,
+ exchangeBaseUrl: r.baseUrl,
+ oldExchangeState: undefined,
+ newExchangeState: getExchangeState(r),
+ };
+ }
+ const exchangeDetails = await getExchangeRecordsInternal(tx, baseUrl);
+ return { exchange, exchangeDetails, notification };
+}
+
+export interface ExchangeKeysDownloadResult {
+ baseUrl: string;
+ masterPublicKey: string;
+ currency: string;
+ auditors: ExchangeAuditor[];
+ currentDenominations: DenominationRecord[];
+ protocolVersion: string;
+ signingKeys: ExchangeSignKeyJson[];
+ reserveClosingDelay: TalerProtocolDuration;
+ expiry: TalerProtocolTimestamp;
+ recoup: Recoup[];
+ listIssueDate: TalerProtocolTimestamp;
+ globalFees: GlobalFees[];
+ accounts: ExchangeWireAccount[];
+ wireFees: { [methodName: string]: WireFeesJson[] };
+}
+
+/**
+ * Download and validate an exchange's /keys data.
+ */
+async function downloadExchangeKeysInfo(
+ baseUrl: string,
+ http: HttpRequestLibrary,
+ timeout: Duration,
+ cancellationToken: CancellationToken,
+ noCache: boolean,
+): Promise<ExchangeKeysDownloadResult> {
+ const keysUrl = new URL("keys", baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (noCache) {
+ headers["cache-control"] = "no-cache";
+ }
+ const resp = await http.fetch(keysUrl.href, {
+ timeout,
+ cancellationToken,
+ headers,
+ });
+
+ logger.info("got response to /keys request");
+
+ // We must make sure to parse out the protocol version
+ // before we validate the body.
+ // Otherwise the parser might complain with a hard to understand
+ // message about some other field, when it is just a version
+ // incompatibility.
+
+ const keysJson = await resp.json();
+
+ const protocolVersion = keysJson.version;
+ if (typeof protocolVersion !== "string") {
+ throw Error("bad exchange, does not even specify protocol version");
+ }
+
+ const versionRes = LibtoolVersion.compare(
+ WALLET_EXCHANGE_PROTOCOL_VERSION,
+ protocolVersion,
+ );
+ if (!versionRes) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ {
+ requestUrl: resp.requestUrl,
+ httpStatusCode: resp.status,
+ requestMethod: resp.requestMethod,
+ },
+ "exchange protocol version malformed",
+ );
+ }
+ if (!versionRes.compatible) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_EXCHANGE_PROTOCOL_VERSION_INCOMPATIBLE,
+ {
+ exchangeProtocolVersion: protocolVersion,
+ walletProtocolVersion: WALLET_EXCHANGE_PROTOCOL_VERSION,
+ },
+ "exchange protocol version not compatible with wallet",
+ );
+ }
+
+ const exchangeKeysJsonUnchecked = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForExchangeKeysJson(),
+ );
+
+ if (exchangeKeysJsonUnchecked.denominations.length === 0) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_EXCHANGE_DENOMINATIONS_INSUFFICIENT,
+ {
+ exchangeBaseUrl: baseUrl,
+ },
+ "exchange doesn't offer any denominations",
+ );
+ }
+
+ const currency = exchangeKeysJsonUnchecked.currency;
+
+ const currentDenominations: DenominationRecord[] = [];
+
+ for (const denomGroup of exchangeKeysJsonUnchecked.denominations) {
+ switch (denomGroup.cipher) {
+ case "RSA":
+ case "RSA+age_restricted": {
+ let ageMask = 0;
+ if (denomGroup.cipher === "RSA+age_restricted") {
+ ageMask = denomGroup.age_mask;
+ }
+ for (const denomIn of denomGroup.denoms) {
+ const denomPub: DenominationPubKey = {
+ age_mask: ageMask,
+ cipher: DenomKeyType.Rsa,
+ rsa_public_key: denomIn.rsa_pub,
+ };
+ const denomPubHash = encodeCrock(hashDenomPub(denomPub));
+ const value = Amounts.parseOrThrow(denomGroup.value);
+ const rec: DenominationRecord = {
+ denomPub,
+ denomPubHash,
+ exchangeBaseUrl: baseUrl,
+ exchangeMasterPub: exchangeKeysJsonUnchecked.master_public_key,
+ isOffered: true,
+ isRevoked: false,
+ isLost: denomIn.lost ?? false,
+ value: Amounts.stringify(value),
+ currency: value.currency,
+ stampExpireDeposit: timestampProtocolToDb(
+ denomIn.stamp_expire_deposit,
+ ),
+ stampExpireLegal: timestampProtocolToDb(denomIn.stamp_expire_legal),
+ stampExpireWithdraw: timestampProtocolToDb(
+ denomIn.stamp_expire_withdraw,
+ ),
+ stampStart: timestampProtocolToDb(denomIn.stamp_start),
+ verificationStatus: DenominationVerificationStatus.Unverified,
+ masterSig: denomIn.master_sig,
+ fees: {
+ feeDeposit: Amounts.stringify(denomGroup.fee_deposit),
+ feeRefresh: Amounts.stringify(denomGroup.fee_refresh),
+ feeRefund: Amounts.stringify(denomGroup.fee_refund),
+ feeWithdraw: Amounts.stringify(denomGroup.fee_withdraw),
+ },
+ };
+ currentDenominations.push(rec);
+ }
+ break;
+ }
+ case "CS+age_restricted":
+ case "CS":
+ logger.warn("Clause-Schnorr denominations not supported");
+ continue;
+ default:
+ logger.warn(
+ `denomination type ${(denomGroup as any).cipher} not supported`,
+ );
+ continue;
+ }
+ }
+
+ return {
+ masterPublicKey: exchangeKeysJsonUnchecked.master_public_key,
+ currency,
+ baseUrl: exchangeKeysJsonUnchecked.base_url,
+ auditors: exchangeKeysJsonUnchecked.auditors,
+ currentDenominations,
+ protocolVersion: exchangeKeysJsonUnchecked.version,
+ signingKeys: exchangeKeysJsonUnchecked.signkeys,
+ reserveClosingDelay: exchangeKeysJsonUnchecked.reserve_closing_delay,
+ expiry: AbsoluteTime.toProtocolTimestamp(
+ getExpiry(resp, {
+ minDuration: Duration.fromSpec({ hours: 1 }),
+ }),
+ ),
+ recoup: exchangeKeysJsonUnchecked.recoup ?? [],
+ listIssueDate: exchangeKeysJsonUnchecked.list_issue_date,
+ globalFees: exchangeKeysJsonUnchecked.global_fees,
+ accounts: exchangeKeysJsonUnchecked.accounts,
+ wireFees: exchangeKeysJsonUnchecked.wire_fees,
+ };
+}
+
+async function downloadTosFromAcceptedFormat(
+ wex: WalletExecutionContext,
+ baseUrl: string,
+ timeout: Duration,
+ acceptedFormat?: string[],
+ acceptLanguage?: string,
+): Promise<ExchangeTosDownloadResult> {
+ let tosFound: ExchangeTosDownloadResult | undefined;
+ // Remove this when exchange supports multiple content-type in accept header
+ if (acceptedFormat)
+ for (const format of acceptedFormat) {
+ const resp = await downloadExchangeWithTermsOfService(
+ wex,
+ baseUrl,
+ wex.http,
+ timeout,
+ format,
+ acceptLanguage,
+ );
+ if (resp.tosContentType === format) {
+ tosFound = resp;
+ break;
+ }
+ }
+ if (tosFound !== undefined) {
+ return tosFound;
+ }
+ // If none of the specified format was found try text/plain
+ return await downloadExchangeWithTermsOfService(
+ wex,
+ baseUrl,
+ wex.http,
+ timeout,
+ "text/plain",
+ acceptLanguage,
+ );
+}
+
+/**
+ * Transition an exchange into an updating state.
+ *
+ * If the update is forced, the exchange is put into an updating state
+ * even if the old information should still be up to date.
+ *
+ * If the exchange entry doesn't exist,
+ * a new ephemeral entry is created.
+ */
+async function startUpdateExchangeEntry(
+ wex: WalletExecutionContext,
+ exchangeBaseUrl: string,
+ options: { forceUpdate?: boolean } = {},
+): Promise<void> {
+ const canonBaseUrl = canonicalizeBaseUrl(exchangeBaseUrl);
+
+ logger.info(
+ `starting update of exchange entry ${canonBaseUrl}, forced=${
+ options.forceUpdate ?? false
+ }`,
+ );
+
+ const { notification } = await wex.db.runReadWriteTx(
+ { storeNames: ["exchanges", "exchangeDetails"] },
+ async (tx) => {
+ wex.ws.exchangeCache.clear();
+ return provideExchangeRecordInTx(wex.ws, tx, exchangeBaseUrl);
+ },
+ );
+
+ logger.trace("created exchange record");
+
+ if (notification) {
+ wex.ws.notify(notification);
+ }
+
+ const { oldExchangeState, newExchangeState, taskId } =
+ await wex.db.runReadWriteTx(
+ { storeNames: ["exchanges", "operationRetries"] },
+ async (tx) => {
+ const r = await tx.exchanges.get(canonBaseUrl);
+ if (!r) {
+ throw Error("exchange not found");
+ }
+ const oldExchangeState = getExchangeState(r);
+ switch (r.updateStatus) {
+ case ExchangeEntryDbUpdateStatus.UnavailableUpdate:
+ r.cachebreakNextUpdate = options.forceUpdate;
+ break;
+ case ExchangeEntryDbUpdateStatus.Suspended:
+ r.cachebreakNextUpdate = options.forceUpdate;
+ break;
+ case ExchangeEntryDbUpdateStatus.ReadyUpdate:
+ r.cachebreakNextUpdate = options.forceUpdate;
+ break;
+ case ExchangeEntryDbUpdateStatus.Ready: {
+ const nextUpdateTimestamp = AbsoluteTime.fromPreciseTimestamp(
+ timestampPreciseFromDb(r.nextUpdateStamp),
+ );
+ // Only update if entry is outdated or update is forced.
+ if (
+ options.forceUpdate ||
+ AbsoluteTime.isExpired(nextUpdateTimestamp)
+ ) {
+ r.updateStatus = ExchangeEntryDbUpdateStatus.ReadyUpdate;
+ r.cachebreakNextUpdate = options.forceUpdate;
+ }
+ break;
+ }
+ case ExchangeEntryDbUpdateStatus.Initial:
+ r.cachebreakNextUpdate = options.forceUpdate;
+ r.updateStatus = ExchangeEntryDbUpdateStatus.InitialUpdate;
+ break;
+ case ExchangeEntryDbUpdateStatus.InitialUpdate:
+ r.cachebreakNextUpdate = options.forceUpdate;
+ break;
+ }
+ wex.ws.exchangeCache.clear();
+ await tx.exchanges.put(r);
+ const newExchangeState = getExchangeState(r);
+ // Reset retries for updating the exchange entry.
+ const taskId = TaskIdentifiers.forExchangeUpdate(r);
+ await tx.operationRetries.delete(taskId);
+ return { oldExchangeState, newExchangeState, taskId };
+ },
+ );
+ wex.ws.notify({
+ type: NotificationType.ExchangeStateTransition,
+ exchangeBaseUrl: canonBaseUrl,
+ newExchangeState: newExchangeState,
+ oldExchangeState: oldExchangeState,
+ });
+ await wex.taskScheduler.resetTaskRetries(taskId);
+}
+
+/**
+ * Basic information about an exchange in a ready state.
+ */
+export interface ReadyExchangeSummary {
+ exchangeBaseUrl: string;
+ currency: string;
+ masterPub: string;
+ tosStatus: ExchangeTosStatus;
+ tosAcceptedEtag: string | undefined;
+ tosCurrentEtag: string | undefined;
+ wireInfo: WireInfo;
+ protocolVersionRange: string;
+ tosAcceptedTimestamp: TalerPreciseTimestamp | undefined;
+ scopeInfo: ScopeInfo;
+}
+
+async function internalWaitReadyExchange(
+ wex: WalletExecutionContext,
+ canonUrl: string,
+ exchangeNotifFlag: AsyncFlag,
+ options: {
+ cancellationToken?: CancellationToken;
+ forceUpdate?: boolean;
+ expectedMasterPub?: string;
+ } = {},
+): Promise<ReadyExchangeSummary> {
+ const operationId = constructTaskIdentifier({
+ tag: PendingTaskType.ExchangeUpdate,
+ exchangeBaseUrl: canonUrl,
+ });
+ while (true) {
+ if (wex.cancellationToken.isCancelled) {
+ throw Error("cancelled");
+ }
+ logger.info(`waiting for ready exchange ${canonUrl}`);
+ const { exchange, exchangeDetails, retryInfo, scopeInfo } =
+ await wex.db.runReadOnlyTx(
+ {
+ storeNames: [
+ "exchanges",
+ "exchangeDetails",
+ "operationRetries",
+ "globalCurrencyAuditors",
+ "globalCurrencyExchanges",
+ ],
+ },
+ async (tx) => {
+ const exchange = await tx.exchanges.get(canonUrl);
+ const exchangeDetails = await getExchangeRecordsInternal(
+ tx,
+ canonUrl,
+ );
+ const retryInfo = await tx.operationRetries.get(operationId);
+ let scopeInfo: ScopeInfo | undefined = undefined;
+ if (exchange && exchangeDetails) {
+ scopeInfo = await internalGetExchangeScopeInfo(tx, exchangeDetails);
+ }
+ return { exchange, exchangeDetails, retryInfo, scopeInfo };
+ },
+ );
+
+ if (!exchange) {
+ throw Error("exchange entry does not exist anymore");
+ }
+
+ let ready = false;
+
+ switch (exchange.updateStatus) {
+ case ExchangeEntryDbUpdateStatus.Ready:
+ ready = true;
+ break;
+ case ExchangeEntryDbUpdateStatus.ReadyUpdate:
+ // If the update is forced,
+ // we wait until we're in a full "ready" state,
+ // as we're not happy with the stale information.
+ if (!options.forceUpdate) {
+ ready = true;
+ }
+ break;
+ case ExchangeEntryDbUpdateStatus.UnavailableUpdate:
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_EXCHANGE_UNAVAILABLE,
+ {
+ exchangeBaseUrl: canonUrl,
+ innerError: retryInfo?.lastError,
+ },
+ );
+ default: {
+ if (retryInfo) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_EXCHANGE_UNAVAILABLE,
+ {
+ exchangeBaseUrl: canonUrl,
+ innerError: retryInfo?.lastError,
+ },
+ );
+ }
+ }
+ }
+
+ if (!ready) {
+ logger.info("waiting for exchange update notification");
+ await exchangeNotifFlag.wait();
+ logger.info("done waiting for exchange update notification");
+ exchangeNotifFlag.reset();
+ continue;
+ }
+
+ if (!exchangeDetails) {
+ throw Error("invariant failed");
+ }
+
+ if (!scopeInfo) {
+ throw Error("invariant failed");
+ }
+
+ const res: ReadyExchangeSummary = {
+ currency: exchangeDetails.currency,
+ exchangeBaseUrl: canonUrl,
+ masterPub: exchangeDetails.masterPublicKey,
+ tosStatus: getExchangeTosStatusFromRecord(exchange),
+ tosAcceptedEtag: exchange.tosAcceptedEtag,
+ wireInfo: exchangeDetails.wireInfo,
+ protocolVersionRange: exchangeDetails.protocolVersionRange,
+ tosCurrentEtag: exchange.tosCurrentEtag,
+ tosAcceptedTimestamp: timestampOptionalPreciseFromDb(
+ exchange.tosAcceptedTimestamp,
+ ),
+ scopeInfo,
+ };
+
+ if (options.expectedMasterPub) {
+ if (res.masterPub !== options.expectedMasterPub) {
+ throw Error(
+ "public key of the exchange does not match expected public key",
+ );
+ }
+ }
+ return res;
+ }
+}
+
+/**
+ * Ensure that a fresh exchange entry exists for the given
+ * exchange base URL.
+ *
+ * The cancellation token can be used to abort waiting for the
+ * updated exchange entry.
+ *
+ * If an exchange entry for the database doesn't exist in the
+ * DB, it will be added ephemerally.
+ *
+ * If the expectedMasterPub is given and does not match the actual
+ * master pub, an exception will be thrown. However, the exchange
+ * will still have been added as an ephemeral exchange entry.
+ */
+export async function fetchFreshExchange(
+ wex: WalletExecutionContext,
+ baseUrl: string,
+ options: {
+ cancellationToken?: CancellationToken;
+ forceUpdate?: boolean;
+ expectedMasterPub?: string;
+ } = {},
+): Promise<ReadyExchangeSummary> {
+ const canonUrl = canonicalizeBaseUrl(baseUrl);
+
+ if (!options.forceUpdate) {
+ const cachedResp = wex.ws.exchangeCache.get(canonUrl);
+ if (cachedResp) {
+ return cachedResp;
+ }
+ } else {
+ wex.ws.exchangeCache.clear();
+ }
+
+ await wex.taskScheduler.ensureRunning();
+
+ await startUpdateExchangeEntry(wex, canonUrl, {
+ forceUpdate: options.forceUpdate,
+ });
+
+ const resp = await waitReadyExchange(wex, canonUrl, options);
+ wex.ws.exchangeCache.put(canonUrl, resp);
+ return resp;
+}
+
+async function waitReadyExchange(
+ wex: WalletExecutionContext,
+ canonUrl: string,
+ options: {
+ forceUpdate?: boolean;
+ expectedMasterPub?: string;
+ } = {},
+): 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 unregisterOnCancelled = wex.cancellationToken.onCancelled(() => {
+ cancelNotif();
+ exchangeNotifFlag.raise();
+ });
+
+ try {
+ const res = await internalWaitReadyExchange(
+ wex,
+ canonUrl,
+ exchangeNotifFlag,
+ options,
+ );
+ logger.info("done waiting for ready exchange");
+ return res;
+ } finally {
+ unregisterOnCancelled();
+ cancelNotif();
+ }
+}
+
+function checkPeerPaymentsDisabled(
+ keysInfo: ExchangeKeysDownloadResult,
+): boolean {
+ const now = AbsoluteTime.now();
+ for (let gf of keysInfo.globalFees) {
+ const isActive = AbsoluteTime.isBetween(
+ now,
+ AbsoluteTime.fromProtocolTimestamp(gf.start_date),
+ AbsoluteTime.fromProtocolTimestamp(gf.end_date),
+ );
+ if (!isActive) {
+ continue;
+ }
+ return false;
+ }
+ // No global fees, we can't do p2p payments!
+ return true;
+}
+
+function checkNoFees(keysInfo: ExchangeKeysDownloadResult): boolean {
+ for (const gf of keysInfo.globalFees) {
+ if (!Amounts.isZero(gf.account_fee)) {
+ return false;
+ }
+ if (!Amounts.isZero(gf.history_fee)) {
+ return false;
+ }
+ if (!Amounts.isZero(gf.purse_fee)) {
+ return false;
+ }
+ }
+ for (const denom of keysInfo.currentDenominations) {
+ if (!Amounts.isZero(denom.fees.feeWithdraw)) {
+ return false;
+ }
+ if (!Amounts.isZero(denom.fees.feeDeposit)) {
+ return false;
+ }
+ if (!Amounts.isZero(denom.fees.feeRefund)) {
+ return false;
+ }
+ if (!Amounts.isZero(denom.fees.feeRefresh)) {
+ return false;
+ }
+ }
+ for (const wft of Object.values(keysInfo.wireFees)) {
+ for (const wf of wft) {
+ if (!Amounts.isZero(wf.wire_fee)) {
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+/**
+ * Update an exchange entry in the wallet's database
+ * by fetching the /keys and /wire information.
+ * Optionally link the reserve entry to the new or existing
+ * exchange entry in then DB.
+ */
+export async function updateExchangeFromUrlHandler(
+ wex: WalletExecutionContext,
+ exchangeBaseUrl: string,
+): Promise<TaskRunResult> {
+ logger.trace(`updating exchange info for ${exchangeBaseUrl}`);
+ exchangeBaseUrl = canonicalizeBaseUrl(exchangeBaseUrl);
+
+ const oldExchangeRec = await wex.db.runReadOnlyTx(
+ { storeNames: ["exchanges"] },
+ async (tx) => {
+ return tx.exchanges.get(exchangeBaseUrl);
+ },
+ );
+
+ if (!oldExchangeRec) {
+ logger.info(`not updating exchange ${exchangeBaseUrl}, no record in DB`);
+ return TaskRunResult.finished();
+ }
+
+ let updateRequestedExplicitly = false;
+
+ switch (oldExchangeRec.updateStatus) {
+ case ExchangeEntryDbUpdateStatus.Suspended:
+ logger.info(`not updating exchange in status "suspended"`);
+ return TaskRunResult.finished();
+ case ExchangeEntryDbUpdateStatus.Initial:
+ logger.info(`not updating exchange in status "initial"`);
+ return TaskRunResult.finished();
+ case ExchangeEntryDbUpdateStatus.InitialUpdate:
+ case ExchangeEntryDbUpdateStatus.ReadyUpdate:
+ updateRequestedExplicitly = true;
+ break;
+ case ExchangeEntryDbUpdateStatus.UnavailableUpdate:
+ // Only retry when scheduled to respect backoff
+ break;
+ case ExchangeEntryDbUpdateStatus.Ready:
+ break;
+ default:
+ assertUnreachable(oldExchangeRec.updateStatus);
+ }
+
+ let refreshCheckNecessary = true;
+
+ if (!updateRequestedExplicitly) {
+ // If the update wasn't requested explicitly,
+ // check if we really need to update.
+
+ let nextUpdateStamp = timestampAbsoluteFromDb(
+ oldExchangeRec.nextUpdateStamp,
+ );
+
+ let nextRefreshCheckStamp = timestampAbsoluteFromDb(
+ oldExchangeRec.nextRefreshCheckStamp,
+ );
+
+ let updateNecessary = true;
+
+ if (
+ !AbsoluteTime.isNever(nextUpdateStamp) &&
+ !AbsoluteTime.isExpired(nextUpdateStamp)
+ ) {
+ logger.info(
+ `exchange update for ${exchangeBaseUrl} not necessary, scheduled for ${AbsoluteTime.toIsoString(
+ nextUpdateStamp,
+ )}`,
+ );
+ updateNecessary = false;
+ }
+
+ if (
+ !AbsoluteTime.isNever(nextRefreshCheckStamp) &&
+ !AbsoluteTime.isExpired(nextRefreshCheckStamp)
+ ) {
+ logger.info(
+ `exchange refresh check for ${exchangeBaseUrl} not necessary, scheduled for ${AbsoluteTime.toIsoString(
+ nextRefreshCheckStamp,
+ )}`,
+ );
+ refreshCheckNecessary = false;
+ }
+
+ if (!(updateNecessary || refreshCheckNecessary)) {
+ logger.trace("update not necessary, running again later");
+ return TaskRunResult.runAgainAt(
+ AbsoluteTime.min(nextUpdateStamp, nextRefreshCheckStamp),
+ );
+ }
+ }
+
+ // When doing the auto-refresh check, we always update
+ // the key info before that.
+
+ logger.trace("updating exchange /keys info");
+
+ const timeout = getExchangeRequestTimeout();
+
+ const keysInfo = await downloadExchangeKeysInfo(
+ exchangeBaseUrl,
+ wex.http,
+ timeout,
+ wex.cancellationToken,
+ oldExchangeRec.cachebreakNextUpdate ?? false,
+ );
+
+ logger.trace("validating exchange wire info");
+
+ const version = LibtoolVersion.parseVersion(keysInfo.protocolVersion);
+ if (!version) {
+ // Should have been validated earlier.
+ throw Error("unexpected invalid version");
+ }
+
+ const wireInfo = await validateWireInfo(
+ wex,
+ version.current,
+ keysInfo,
+ keysInfo.masterPublicKey,
+ );
+
+ const globalFees = await validateGlobalFees(
+ wex,
+ keysInfo.globalFees,
+ keysInfo.masterPublicKey,
+ );
+
+ if (keysInfo.baseUrl != exchangeBaseUrl) {
+ logger.warn("exchange base URL mismatch");
+ const errorDetail: TalerErrorDetail = makeErrorDetail(
+ TalerErrorCode.WALLET_EXCHANGE_BASE_URL_MISMATCH,
+ {
+ urlWallet: exchangeBaseUrl,
+ urlExchange: keysInfo.baseUrl,
+ },
+ );
+ return {
+ type: TaskRunResultType.Error,
+ errorDetail,
+ };
+ }
+
+ 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"],
+ );
+
+ logger.trace("updating exchange info in database");
+
+ let ageMask = 0;
+ for (const x of keysInfo.currentDenominations) {
+ if (
+ isWithdrawableDenom(x, wex.ws.config.testing.denomselAllowLate) &&
+ x.denomPub.age_mask != 0
+ ) {
+ ageMask = x.denomPub.age_mask;
+ break;
+ }
+ }
+ let noFees = checkNoFees(keysInfo);
+ let peerPaymentsDisabled = checkPeerPaymentsDisabled(keysInfo);
+
+ const updated = await wex.db.runReadWriteTx(
+ {
+ storeNames: [
+ "exchanges",
+ "exchangeDetails",
+ "exchangeSignKeys",
+ "denominations",
+ "coins",
+ "refreshGroups",
+ "recoupGroups",
+ "coinAvailability",
+ "denomLossEvents",
+ ],
+ },
+ async (tx) => {
+ const r = await tx.exchanges.get(exchangeBaseUrl);
+ if (!r) {
+ logger.warn(`exchange ${exchangeBaseUrl} no longer present`);
+ return;
+ }
+
+ wex.ws.refreshCostCache.clear();
+ wex.ws.exchangeCache.clear();
+ wex.ws.denomInfoCache.clear();
+
+ const oldExchangeState = getExchangeState(r);
+ const existingDetails = await getExchangeRecordsInternal(tx, r.baseUrl);
+ let detailsPointerChanged = false;
+ if (!existingDetails) {
+ detailsPointerChanged = true;
+ }
+ let detailsIncompatible = false;
+ if (existingDetails) {
+ if (existingDetails.masterPublicKey !== keysInfo.masterPublicKey) {
+ detailsIncompatible = true;
+ detailsPointerChanged = true;
+ }
+ if (existingDetails.currency !== keysInfo.currency) {
+ detailsIncompatible = true;
+ detailsPointerChanged = true;
+ }
+ // FIXME: We need to do some consistency checks!
+ }
+ if (detailsIncompatible) {
+ logger.warn(
+ `exchange ${r.baseUrl} has incompatible data in /keys, not updating`,
+ );
+ // We don't support this gracefully right now.
+ // See https://bugs.taler.net/n/8576
+ r.updateStatus = ExchangeEntryDbUpdateStatus.UnavailableUpdate;
+ r.updateRetryCounter = (r.updateRetryCounter ?? 0) + 1;
+ r.nextUpdateStamp = computeDbBackoff(r.updateRetryCounter);
+ r.nextRefreshCheckStamp = timestampPreciseToDb(
+ AbsoluteTime.toPreciseTimestamp(AbsoluteTime.never()),
+ );
+ r.cachebreakNextUpdate = true;
+ await tx.exchanges.put(r);
+ return {
+ oldExchangeState,
+ newExchangeState: getExchangeState(r),
+ };
+ }
+ r.updateRetryCounter = 0;
+ const newDetails: ExchangeDetailsRecord = {
+ auditors: keysInfo.auditors,
+ currency: keysInfo.currency,
+ masterPublicKey: keysInfo.masterPublicKey,
+ protocolVersionRange: keysInfo.protocolVersion,
+ reserveClosingDelay: keysInfo.reserveClosingDelay,
+ globalFees,
+ exchangeBaseUrl: r.baseUrl,
+ wireInfo,
+ ageMask,
+ };
+ r.noFees = noFees;
+ r.peerPaymentsDisabled = peerPaymentsDisabled;
+ r.tosCurrentEtag = tosDownload.tosEtag;
+ if (existingDetails?.rowId) {
+ newDetails.rowId = existingDetails.rowId;
+ }
+ r.lastUpdate = timestampPreciseToDb(TalerPreciseTimestamp.now());
+ r.nextUpdateStamp = timestampPreciseToDb(
+ AbsoluteTime.toPreciseTimestamp(
+ AbsoluteTime.fromProtocolTimestamp(keysInfo.expiry),
+ ),
+ );
+ // New denominations might be available.
+ r.nextRefreshCheckStamp = timestampPreciseToDb(
+ TalerPreciseTimestamp.now(),
+ );
+ if (detailsPointerChanged) {
+ r.detailsPointer = {
+ currency: newDetails.currency,
+ masterPublicKey: newDetails.masterPublicKey,
+ updateClock: timestampPreciseToDb(TalerPreciseTimestamp.now()),
+ };
+ }
+
+ r.updateStatus = ExchangeEntryDbUpdateStatus.Ready;
+ r.cachebreakNextUpdate = false;
+ await tx.exchanges.put(r);
+ logger.info(`putting new exchange details in DB: ${j2s(newDetails)}`);
+ const drRowId = await tx.exchangeDetails.put(newDetails);
+ checkDbInvariant(typeof drRowId.key === "number");
+
+ for (const sk of keysInfo.signingKeys) {
+ // FIXME: validate signing keys before inserting them
+ await tx.exchangeSignKeys.put({
+ exchangeDetailsRowId: drRowId.key,
+ masterSig: sk.master_sig,
+ signkeyPub: sk.key,
+ stampEnd: timestampProtocolToDb(sk.stamp_end),
+ stampExpire: timestampProtocolToDb(sk.stamp_expire),
+ stampStart: timestampProtocolToDb(sk.stamp_start),
+ });
+ }
+
+ // In the future: Filter out old denominations by index
+ const allOldDenoms =
+ await tx.denominations.indexes.byExchangeBaseUrl.getAll(
+ exchangeBaseUrl,
+ );
+ const oldDenomByDph = new Map<string, DenominationRecord>();
+ for (const denom of allOldDenoms) {
+ oldDenomByDph.set(denom.denomPubHash, denom);
+ }
+
+ logger.trace("updating denominations in database");
+ const currentDenomSet = new Set<string>(
+ keysInfo.currentDenominations.map((x) => x.denomPubHash),
+ );
+
+ for (const currentDenom of keysInfo.currentDenominations) {
+ const oldDenom = oldDenomByDph.get(currentDenom.denomPubHash);
+ if (oldDenom) {
+ // FIXME: Do consistency check, report to auditor if necessary.
+ // See https://bugs.taler.net/n/8594
+
+ // Mark lost denominations as lost.
+ if (currentDenom.isLost && !oldDenom.isLost) {
+ logger.warn(
+ `marking denomination ${currentDenom.denomPubHash} of ${exchangeBaseUrl} as lost`,
+ );
+ oldDenom.isLost = true;
+ await tx.denominations.put(currentDenom);
+ }
+ } else {
+ await tx.denominations.put(currentDenom);
+ }
+ }
+
+ // Update list issue date for all denominations,
+ // and mark non-offered denominations as such.
+ for (const x of allOldDenoms) {
+ if (!currentDenomSet.has(x.denomPubHash)) {
+ // FIXME: Here, an auditor report should be created, unless
+ // the denomination is really legally expired.
+ if (x.isOffered) {
+ x.isOffered = false;
+ logger.info(
+ `setting denomination ${x.denomPubHash} to offered=false`,
+ );
+ }
+ } else {
+ if (!x.isOffered) {
+ x.isOffered = true;
+ logger.info(
+ `setting denomination ${x.denomPubHash} to offered=true`,
+ );
+ }
+ }
+ await tx.denominations.put(x);
+ }
+
+ logger.trace("done updating denominations in database");
+
+ const denomLossResult = await handleDenomLoss(
+ wex,
+ tx,
+ newDetails.currency,
+ exchangeBaseUrl,
+ );
+
+ await handleRecoup(wex, tx, exchangeBaseUrl, keysInfo.recoup);
+
+ const newExchangeState = getExchangeState(r);
+
+ return {
+ exchange: r,
+ exchangeDetails: newDetails,
+ oldExchangeState,
+ newExchangeState,
+ denomLossResult,
+ };
+ },
+ );
+
+ if (!updated) {
+ throw Error("something went wrong with updating the exchange");
+ }
+
+ if (updated.denomLossResult) {
+ for (const notif of updated.denomLossResult.notifications) {
+ wex.ws.notify(notif);
+ }
+ }
+
+ logger.trace("done updating exchange info in database");
+
+ logger.trace(`doing auto-refresh check for '${exchangeBaseUrl}'`);
+
+ let minCheckThreshold = AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ days: 1 }),
+ );
+
+ if (refreshCheckNecessary) {
+ // Do auto-refresh.
+ await wex.db.runReadWriteTx(
+ {
+ storeNames: [
+ "coins",
+ "denominations",
+ "coinAvailability",
+ "refreshGroups",
+ "refreshSessions",
+ "exchanges",
+ ],
+ },
+ async (tx) => {
+ const exchange = await tx.exchanges.get(exchangeBaseUrl);
+ if (!exchange || !exchange.detailsPointer) {
+ return;
+ }
+ const coins = await tx.coins.indexes.byBaseUrl
+ .iter(exchangeBaseUrl)
+ .toArray();
+ const refreshCoins: CoinRefreshRequest[] = [];
+ for (const coin of coins) {
+ if (coin.status !== CoinStatus.Fresh) {
+ continue;
+ }
+ const denom = await tx.denominations.get([
+ exchangeBaseUrl,
+ coin.denomPubHash,
+ ]);
+ if (!denom) {
+ logger.warn("denomination not in database");
+ continue;
+ }
+ const executeThreshold =
+ getAutoRefreshExecuteThresholdForDenom(denom);
+ if (AbsoluteTime.isExpired(executeThreshold)) {
+ refreshCoins.push({
+ coinPub: coin.coinPub,
+ amount: denom.value,
+ });
+ } else {
+ const checkThreshold = getAutoRefreshCheckThreshold(denom);
+ minCheckThreshold = AbsoluteTime.min(
+ minCheckThreshold,
+ checkThreshold,
+ );
+ }
+ }
+ if (refreshCoins.length > 0) {
+ const res = await createRefreshGroup(
+ wex,
+ tx,
+ exchange.detailsPointer?.currency,
+ refreshCoins,
+ RefreshReason.Scheduled,
+ undefined,
+ );
+ logger.trace(
+ `created refresh group for auto-refresh (${res.refreshGroupId})`,
+ );
+ }
+ logger.trace(
+ `next refresh check at ${AbsoluteTime.toIsoString(
+ minCheckThreshold,
+ )}`,
+ );
+ exchange.nextRefreshCheckStamp = timestampPreciseToDb(
+ AbsoluteTime.toPreciseTimestamp(minCheckThreshold),
+ );
+ wex.ws.exchangeCache.clear();
+ await tx.exchanges.put(exchange);
+ },
+ );
+ }
+
+ wex.ws.notify({
+ type: NotificationType.ExchangeStateTransition,
+ exchangeBaseUrl,
+ newExchangeState: updated.newExchangeState,
+ oldExchangeState: updated.oldExchangeState,
+ });
+
+ // Next invocation will cause the task to be run again
+ // at the necessary time.
+ return TaskRunResult.progress();
+}
+
+interface DenomLossResult {
+ notifications: WalletNotification[];
+}
+
+async function handleDenomLoss(
+ wex: WalletExecutionContext,
+ tx: WalletDbReadWriteTransaction<
+ ["coinAvailability", "denominations", "denomLossEvents", "coins"]
+ >,
+ currency: string,
+ exchangeBaseUrl: string,
+): Promise<DenomLossResult> {
+ const coinAvailabilityRecs =
+ await tx.coinAvailability.indexes.byExchangeBaseUrl.getAll(exchangeBaseUrl);
+ const denomsVanished: string[] = [];
+ const denomsUnoffered: string[] = [];
+ const denomsExpired: string[] = [];
+ let amountVanished = Amount.zeroOfCurrency(currency);
+ let amountExpired = Amount.zeroOfCurrency(currency);
+ let amountUnoffered = Amount.zeroOfCurrency(currency);
+
+ const result: DenomLossResult = {
+ notifications: [],
+ };
+
+ for (const coinAv of coinAvailabilityRecs) {
+ if (coinAv.freshCoinCount <= 0) {
+ continue;
+ }
+ const n = coinAv.freshCoinCount;
+ const denom = await tx.denominations.get([
+ coinAv.exchangeBaseUrl,
+ coinAv.denomPubHash,
+ ]);
+ const timestampExpireDeposit = !denom
+ ? undefined
+ : timestampAbsoluteFromDb(denom.stampExpireDeposit);
+ if (!denom) {
+ // Remove availability
+ coinAv.freshCoinCount = 0;
+ coinAv.visibleCoinCount = 0;
+ await tx.coinAvailability.put(coinAv);
+ denomsVanished.push(coinAv.denomPubHash);
+ const total = Amount.from(coinAv.value).mult(n);
+ amountVanished = amountVanished.add(total);
+ } else if (!denom.isOffered) {
+ // Remove availability
+ coinAv.freshCoinCount = 0;
+ coinAv.visibleCoinCount = 0;
+ await tx.coinAvailability.put(coinAv);
+ denomsUnoffered.push(coinAv.denomPubHash);
+ const total = Amount.from(coinAv.value).mult(n);
+ amountUnoffered = amountUnoffered.add(total);
+ } else if (
+ timestampExpireDeposit &&
+ AbsoluteTime.isExpired(timestampExpireDeposit)
+ ) {
+ // Remove availability
+ coinAv.freshCoinCount = 0;
+ coinAv.visibleCoinCount = 0;
+ await tx.coinAvailability.put(coinAv);
+ denomsExpired.push(coinAv.denomPubHash);
+ const total = Amount.from(coinAv.value).mult(n);
+ amountExpired = amountExpired.add(total);
+ } else {
+ // Denomination is still fine!
+ continue;
+ }
+
+ logger.warn(`denomination ${coinAv.denomPubHash} is a loss`);
+
+ const coins = await tx.coins.indexes.byDenomPubHash.getAll(
+ coinAv.denomPubHash,
+ );
+ for (const coin of coins) {
+ switch (coin.status) {
+ case CoinStatus.Fresh:
+ case CoinStatus.FreshSuspended: {
+ coin.status = CoinStatus.DenomLoss;
+ await tx.coins.put(coin);
+ break;
+ }
+ }
+ }
+ }
+
+ if (denomsVanished.length > 0) {
+ const denomLossEventId = encodeCrock(getRandomBytes(32));
+ await tx.denomLossEvents.add({
+ denomLossEventId,
+ amount: amountVanished.toString(),
+ currency,
+ exchangeBaseUrl,
+ denomPubHashes: denomsVanished,
+ eventType: DenomLossEventType.DenomVanished,
+ status: DenomLossStatus.Done,
+ timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()),
+ });
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.DenomLoss,
+ denomLossEventId,
+ });
+ result.notifications.push({
+ type: NotificationType.TransactionStateTransition,
+ transactionId,
+ oldTxState: {
+ major: TransactionMajorState.None,
+ },
+ newTxState: {
+ major: TransactionMajorState.Done,
+ },
+ });
+ result.notifications.push({
+ type: NotificationType.BalanceChange,
+ hintTransactionId: transactionId,
+ });
+ }
+
+ if (denomsUnoffered.length > 0) {
+ const denomLossEventId = encodeCrock(getRandomBytes(32));
+ await tx.denomLossEvents.add({
+ denomLossEventId,
+ amount: amountUnoffered.toString(),
+ currency,
+ exchangeBaseUrl,
+ denomPubHashes: denomsUnoffered,
+ eventType: DenomLossEventType.DenomUnoffered,
+ status: DenomLossStatus.Done,
+ timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()),
+ });
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.DenomLoss,
+ denomLossEventId,
+ });
+ result.notifications.push({
+ type: NotificationType.TransactionStateTransition,
+ transactionId,
+ oldTxState: {
+ major: TransactionMajorState.None,
+ },
+ newTxState: {
+ major: TransactionMajorState.Done,
+ },
+ });
+ result.notifications.push({
+ type: NotificationType.BalanceChange,
+ hintTransactionId: transactionId,
+ });
+ }
+
+ if (denomsExpired.length > 0) {
+ const denomLossEventId = encodeCrock(getRandomBytes(32));
+ await tx.denomLossEvents.add({
+ denomLossEventId,
+ amount: amountExpired.toString(),
+ currency,
+ exchangeBaseUrl,
+ denomPubHashes: denomsUnoffered,
+ eventType: DenomLossEventType.DenomExpired,
+ status: DenomLossStatus.Done,
+ timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()),
+ });
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.DenomLoss,
+ denomLossEventId,
+ });
+ result.notifications.push({
+ type: NotificationType.TransactionStateTransition,
+ transactionId,
+ oldTxState: {
+ major: TransactionMajorState.None,
+ },
+ newTxState: {
+ major: TransactionMajorState.Done,
+ },
+ });
+ result.notifications.push({
+ type: NotificationType.BalanceChange,
+ hintTransactionId: transactionId,
+ });
+ }
+
+ return result;
+}
+
+export function computeDenomLossTransactionStatus(
+ rec: DenomLossEventRecord,
+): TransactionState {
+ switch (rec.status) {
+ case DenomLossStatus.Aborted:
+ return {
+ major: TransactionMajorState.Aborted,
+ };
+ case DenomLossStatus.Done:
+ return {
+ major: TransactionMajorState.Done,
+ };
+ }
+}
+
+export class DenomLossTransactionContext implements TransactionContext {
+ get taskId(): TaskIdStr | undefined {
+ return undefined;
+ }
+ transactionId: TransactionIdStr;
+
+ abortTransaction(): Promise<void> {
+ throw new Error("Method not implemented.");
+ }
+ suspendTransaction(): Promise<void> {
+ throw new Error("Method not implemented.");
+ }
+ resumeTransaction(): Promise<void> {
+ throw new Error("Method not implemented.");
+ }
+ failTransaction(): Promise<void> {
+ throw new Error("Method not implemented.");
+ }
+ async deleteTransaction(): Promise<void> {
+ const transitionInfo = await this.wex.db.runReadWriteTx(
+ { storeNames: ["denomLossEvents"] },
+ async (tx) => {
+ const rec = await tx.denomLossEvents.get(this.denomLossEventId);
+ if (rec) {
+ const oldTxState = computeDenomLossTransactionStatus(rec);
+ await tx.denomLossEvents.delete(this.denomLossEventId);
+ return {
+ oldTxState,
+ newTxState: {
+ major: TransactionMajorState.Deleted,
+ },
+ };
+ }
+ return undefined;
+ },
+ );
+ notifyTransition(this.wex, this.transactionId, transitionInfo);
+ }
+
+ constructor(
+ private wex: WalletExecutionContext,
+ public denomLossEventId: string,
+ ) {
+ this.transactionId = constructTransactionIdentifier({
+ tag: TransactionType.DenomLoss,
+ denomLossEventId,
+ });
+ }
+}
+
+async function handleRecoup(
+ wex: WalletExecutionContext,
+ tx: WalletDbReadWriteTransaction<
+ ["denominations", "coins", "recoupGroups", "refreshGroups"]
+ >,
+ exchangeBaseUrl: string,
+ recoup: Recoup[],
+): Promise<void> {
+ // Handle recoup
+ const recoupDenomList = recoup;
+ const newlyRevokedCoinPubs: string[] = [];
+ logger.trace("recoup list from exchange", recoupDenomList);
+ for (const recoupInfo of recoupDenomList) {
+ const oldDenom = await tx.denominations.get([
+ exchangeBaseUrl,
+ recoupInfo.h_denom_pub,
+ ]);
+ if (!oldDenom) {
+ // We never even knew about the revoked denomination, all good.
+ continue;
+ }
+ if (oldDenom.isRevoked) {
+ // We already marked the denomination as revoked,
+ // this implies we revoked all coins
+ logger.trace("denom already revoked");
+ continue;
+ }
+ logger.info("revoking denom", recoupInfo.h_denom_pub);
+ oldDenom.isRevoked = true;
+ await tx.denominations.put(oldDenom);
+ const affectedCoins = await tx.coins.indexes.byDenomPubHash.getAll(
+ recoupInfo.h_denom_pub,
+ );
+ for (const ac of affectedCoins) {
+ newlyRevokedCoinPubs.push(ac.coinPub);
+ }
+ }
+ if (newlyRevokedCoinPubs.length != 0) {
+ logger.info("recouping coins", newlyRevokedCoinPubs);
+ await createRecoupGroup(wex, tx, exchangeBaseUrl, newlyRevokedCoinPubs);
+ }
+}
+
+function getAutoRefreshExecuteThresholdForDenom(
+ d: DenominationRecord,
+): AbsoluteTime {
+ return getAutoRefreshExecuteThreshold({
+ stampExpireWithdraw: timestampProtocolFromDb(d.stampExpireWithdraw),
+ stampExpireDeposit: timestampProtocolFromDb(d.stampExpireDeposit),
+ });
+}
+
+/**
+ * Timestamp after which the wallet would do the next check for an auto-refresh.
+ */
+function getAutoRefreshCheckThreshold(d: DenominationRecord): AbsoluteTime {
+ const expireWithdraw = AbsoluteTime.fromProtocolTimestamp(
+ timestampProtocolFromDb(d.stampExpireWithdraw),
+ );
+ const expireDeposit = AbsoluteTime.fromProtocolTimestamp(
+ timestampProtocolFromDb(d.stampExpireDeposit),
+ );
+ const delta = AbsoluteTime.difference(expireWithdraw, expireDeposit);
+ const deltaDiv = durationMul(delta, 0.75);
+ return AbsoluteTime.addDuration(expireWithdraw, deltaDiv);
+}
+
+/**
+ * Find a payto:// URI of the exchange that is of one
+ * of the given target types.
+ *
+ * Throws if no matching account was found.
+ */
+export async function getExchangePaytoUri(
+ wex: WalletExecutionContext,
+ exchangeBaseUrl: string,
+ supportedTargetTypes: string[],
+): Promise<string> {
+ // We do the update here, since the exchange might not even exist
+ // yet in our database.
+ const details = await wex.db.runReadOnlyTx(
+ { storeNames: ["exchanges", "exchangeDetails"] },
+ async (tx) => {
+ return getExchangeRecordsInternal(tx, exchangeBaseUrl);
+ },
+ );
+ const accounts = details?.wireInfo.accounts ?? [];
+ for (const account of accounts) {
+ const res = parsePaytoUri(account.payto_uri);
+ if (!res) {
+ continue;
+ }
+ if (supportedTargetTypes.includes(res.targetType)) {
+ return account.payto_uri;
+ }
+ }
+ throw Error(
+ `no matching account found at exchange ${exchangeBaseUrl} for wire types ${j2s(
+ supportedTargetTypes,
+ )}`,
+ );
+}
+
+/**
+ * Get the exchange ToS in the requested format.
+ * Try to download in the accepted format not cached.
+ */
+export async function getExchangeTos(
+ wex: WalletExecutionContext,
+ exchangeBaseUrl: string,
+ acceptedFormat?: string[],
+ acceptLanguage?: string,
+): Promise<GetExchangeTosResult> {
+ const exch = await fetchFreshExchange(wex, exchangeBaseUrl);
+
+ const tosDownload = await downloadTosFromAcceptedFormat(
+ wex,
+ exchangeBaseUrl,
+ getExchangeRequestTimeout(),
+ acceptedFormat,
+ acceptLanguage,
+ );
+
+ await wex.db.runReadWriteTx({ storeNames: ["exchanges"] }, async (tx) => {
+ const updateExchangeEntry = await tx.exchanges.get(exchangeBaseUrl);
+ if (updateExchangeEntry) {
+ updateExchangeEntry.tosCurrentEtag = tosDownload.tosEtag;
+ wex.ws.exchangeCache.clear();
+ await tx.exchanges.put(updateExchangeEntry);
+ }
+ });
+
+ return {
+ acceptedEtag: exch.tosAcceptedEtag,
+ currentEtag: tosDownload.tosEtag,
+ content: tosDownload.tosText,
+ contentType: tosDownload.tosContentType,
+ contentLanguage: tosDownload.tosContentLanguage,
+ tosStatus: exch.tosStatus,
+ tosAvailableLanguages: tosDownload.tosAvailableLanguages,
+ };
+}
+
+/**
+ * Parsed information about an exchange,
+ * obtained by requesting /keys.
+ */
+export interface ExchangeInfo {
+ keys: ExchangeKeysDownloadResult;
+}
+
+/**
+ * Helper function to download the exchange /keys info.
+ *
+ * Only used for testing / dbless wallet.
+ */
+export async function downloadExchangeInfo(
+ exchangeBaseUrl: string,
+ http: HttpRequestLibrary,
+): Promise<ExchangeInfo> {
+ const keysInfo = await downloadExchangeKeysInfo(
+ exchangeBaseUrl,
+ http,
+ Duration.getForever(),
+ CancellationToken.CONTINUE,
+ false,
+ );
+ return {
+ keys: keysInfo,
+ };
+}
+
+/**
+ * List all exchange entries known to the wallet.
+ */
+export async function listExchanges(
+ wex: WalletExecutionContext,
+): Promise<ExchangesListResponse> {
+ const exchanges: ExchangeListItem[] = [];
+ await wex.db.runReadOnlyTx(
+ {
+ storeNames: [
+ "exchanges",
+ "operationRetries",
+ "exchangeDetails",
+ "globalCurrencyAuditors",
+ "globalCurrencyExchanges",
+ ],
+ },
+ async (tx) => {
+ const exchangeRecords = await tx.exchanges.iter().toArray();
+ for (const r of exchangeRecords) {
+ const taskId = constructTaskIdentifier({
+ tag: PendingTaskType.ExchangeUpdate,
+ exchangeBaseUrl: r.baseUrl,
+ });
+ const exchangeDetails = await getExchangeRecordsInternal(tx, r.baseUrl);
+ const opRetryRecord = await tx.operationRetries.get(taskId);
+ exchanges.push(
+ await makeExchangeListItem(
+ tx,
+ r,
+ exchangeDetails,
+ opRetryRecord?.lastError,
+ ),
+ );
+ }
+ },
+ );
+ return { exchanges };
+}
+
+/**
+ * Transition an exchange to the "used" entry state if necessary.
+ *
+ * Should be called whenever the exchange is actively used by the client (for withdrawals etc.).
+ *
+ * The caller should emit the returned notification iff the current transaction
+ * succeeded.
+ */
+export async function markExchangeUsed(
+ wex: WalletExecutionContext,
+ tx: WalletDbReadWriteTransaction<["exchanges"]>,
+ exchangeBaseUrl: string,
+): Promise<{ notif: WalletNotification | undefined }> {
+ exchangeBaseUrl = canonicalizeBaseUrl(exchangeBaseUrl);
+ logger.info(`marking exchange ${exchangeBaseUrl} as used`);
+ const exch = await tx.exchanges.get(exchangeBaseUrl);
+ if (!exch) {
+ return {
+ notif: undefined,
+ };
+ }
+ const oldExchangeState = getExchangeState(exch);
+ switch (exch.entryStatus) {
+ case ExchangeEntryDbRecordStatus.Ephemeral:
+ case ExchangeEntryDbRecordStatus.Preset: {
+ exch.entryStatus = ExchangeEntryDbRecordStatus.Used;
+ await tx.exchanges.put(exch);
+ const newExchangeState = getExchangeState(exch);
+ return {
+ notif: {
+ type: NotificationType.ExchangeStateTransition,
+ exchangeBaseUrl,
+ newExchangeState: newExchangeState,
+ oldExchangeState: oldExchangeState,
+ } satisfies WalletNotification,
+ };
+ }
+ default:
+ return {
+ notif: undefined,
+ };
+ }
+}
+
+/**
+ * Get detailed information about the exchange including a timeline
+ * for the fees charged by the exchange.
+ */
+export async function getExchangeDetailedInfo(
+ wex: WalletExecutionContext,
+ exchangeBaseurl: string,
+): Promise<ExchangeDetailedResponse> {
+ const exchange = await wex.db.runReadOnlyTx(
+ { storeNames: ["exchanges", "exchangeDetails", "denominations"] },
+ async (tx) => {
+ const ex = await tx.exchanges.get(exchangeBaseurl);
+ const dp = ex?.detailsPointer;
+ if (!dp) {
+ return;
+ }
+ const { currency } = dp;
+ const exchangeDetails = await getExchangeRecordsInternal(tx, ex.baseUrl);
+ if (!exchangeDetails) {
+ return;
+ }
+ const denominationRecords =
+ await tx.denominations.indexes.byExchangeBaseUrl.getAll(ex.baseUrl);
+
+ if (!denominationRecords) {
+ return;
+ }
+
+ const denominations: DenominationInfo[] = denominationRecords.map((x) =>
+ DenominationRecord.toDenomInfo(x),
+ );
+
+ return {
+ info: {
+ exchangeBaseUrl: ex.baseUrl,
+ currency,
+ paytoUris: exchangeDetails.wireInfo.accounts.map((x) => x.payto_uri),
+ auditors: exchangeDetails.auditors,
+ wireInfo: exchangeDetails.wireInfo,
+ globalFees: exchangeDetails.globalFees,
+ },
+ denominations,
+ };
+ },
+ );
+
+ if (!exchange) {
+ throw Error(`exchange with base url "${exchangeBaseurl}" not found`);
+ }
+
+ const denoms = exchange.denominations.map((d) => ({
+ ...d,
+ group: Amounts.stringifyValue(d.value),
+ }));
+ const denomFees: DenomOperationMap<FeeDescription[]> = {
+ deposit: createTimeline(
+ denoms,
+ "denomPubHash",
+ "stampStart",
+ "stampExpireDeposit",
+ "feeDeposit",
+ "group",
+ selectBestForOverlappingDenominations,
+ ),
+ refresh: createTimeline(
+ denoms,
+ "denomPubHash",
+ "stampStart",
+ "stampExpireWithdraw",
+ "feeRefresh",
+ "group",
+ selectBestForOverlappingDenominations,
+ ),
+ refund: createTimeline(
+ denoms,
+ "denomPubHash",
+ "stampStart",
+ "stampExpireWithdraw",
+ "feeRefund",
+ "group",
+ selectBestForOverlappingDenominations,
+ ),
+ withdraw: createTimeline(
+ denoms,
+ "denomPubHash",
+ "stampStart",
+ "stampExpireWithdraw",
+ "feeWithdraw",
+ "group",
+ selectBestForOverlappingDenominations,
+ ),
+ };
+
+ const transferFees = Object.entries(
+ exchange.info.wireInfo.feesForType,
+ ).reduce(
+ (prev, [wireType, infoForType]) => {
+ const feesByGroup = [
+ ...infoForType.map((w) => ({
+ ...w,
+ fee: Amounts.stringify(w.closingFee),
+ group: "closing",
+ })),
+ ...infoForType.map((w) => ({ ...w, fee: w.wireFee, group: "wire" })),
+ ];
+ prev[wireType] = createTimeline(
+ feesByGroup,
+ "sig",
+ "startStamp",
+ "endStamp",
+ "fee",
+ "group",
+ selectMinimumFee,
+ );
+ return prev;
+ },
+ {} as Record<string, FeeDescription[]>,
+ );
+
+ const globalFeesByGroup = [
+ ...exchange.info.globalFees.map((w) => ({
+ ...w,
+ fee: w.accountFee,
+ group: "account",
+ })),
+ ...exchange.info.globalFees.map((w) => ({
+ ...w,
+ fee: w.historyFee,
+ group: "history",
+ })),
+ ...exchange.info.globalFees.map((w) => ({
+ ...w,
+ fee: w.purseFee,
+ group: "purse",
+ })),
+ ];
+
+ const globalFees = createTimeline(
+ globalFeesByGroup,
+ "signature",
+ "startDate",
+ "endDate",
+ "fee",
+ "group",
+ selectMinimumFee,
+ );
+
+ return {
+ exchange: {
+ ...exchange.info,
+ denomFees,
+ transferFees,
+ globalFees,
+ },
+ };
+}
+
+async function internalGetExchangeResources(
+ wex: WalletExecutionContext,
+ tx: DbReadOnlyTransaction<
+ typeof WalletStoresV1,
+ ["exchanges", "coins", "withdrawalGroups"]
+ >,
+ exchangeBaseUrl: string,
+): Promise<GetExchangeResourcesResponse> {
+ let numWithdrawals = 0;
+ let numCoins = 0;
+ numCoins = await tx.coins.indexes.byBaseUrl.count(exchangeBaseUrl);
+ numWithdrawals =
+ await tx.withdrawalGroups.indexes.byExchangeBaseUrl.count(exchangeBaseUrl);
+ const total = numWithdrawals + numCoins;
+ return {
+ hasResources: total != 0,
+ };
+}
+
+/**
+ * Purge information in the database associated with the exchange.
+ *
+ * Deletes information specific to the exchange and withdrawals,
+ * but keeps some transactions (payments, p2p, refreshes) around.
+ */
+async function purgeExchange(
+ tx: WalletDbReadWriteTransaction<
+ [
+ "exchanges",
+ "exchangeDetails",
+ "transactions",
+ "coinAvailability",
+ "coins",
+ "denominations",
+ "exchangeSignKeys",
+ "withdrawalGroups",
+ "planchets",
+ ]
+ >,
+ exchangeBaseUrl: string,
+): Promise<void> {
+ const detRecs = await tx.exchangeDetails.indexes.byExchangeBaseUrl.getAll();
+ for (const r of detRecs) {
+ if (r.rowId == null) {
+ // Should never happen, as rowId is the primary key.
+ continue;
+ }
+ await tx.exchangeDetails.delete(r.rowId);
+ const signkeyRecs =
+ await tx.exchangeSignKeys.indexes.byExchangeDetailsRowId.getAll(r.rowId);
+ for (const rec of signkeyRecs) {
+ await tx.exchangeSignKeys.delete([r.rowId, rec.signkeyPub]);
+ }
+ }
+ // FIXME: Also remove records related to transactions?
+ await tx.exchanges.delete(exchangeBaseUrl);
+
+ {
+ const coinAvailabilityRecs =
+ await tx.coinAvailability.indexes.byExchangeBaseUrl.getAll(
+ exchangeBaseUrl,
+ );
+ for (const rec of coinAvailabilityRecs) {
+ await tx.coinAvailability.delete([
+ exchangeBaseUrl,
+ rec.denomPubHash,
+ rec.maxAge,
+ ]);
+ }
+ }
+
+ {
+ const coinRecs = await tx.coins.indexes.byBaseUrl.getAll(exchangeBaseUrl);
+ for (const rec of coinRecs) {
+ await tx.coins.delete(rec.coinPub);
+ }
+ }
+
+ {
+ const denomRecs =
+ await tx.denominations.indexes.byExchangeBaseUrl.getAll(exchangeBaseUrl);
+ for (const rec of denomRecs) {
+ await tx.denominations.delete(rec.denomPubHash);
+ }
+ }
+
+ {
+ const withdrawalGroupRecs =
+ await tx.withdrawalGroups.indexes.byExchangeBaseUrl.getAll(
+ exchangeBaseUrl,
+ );
+ for (const wg of withdrawalGroupRecs) {
+ await tx.withdrawalGroups.delete(wg.withdrawalGroupId);
+ const planchets = await tx.planchets.indexes.byGroup.getAll(
+ wg.withdrawalGroupId,
+ );
+ for (const p of planchets) {
+ await tx.planchets.delete(p.coinPub);
+ }
+ }
+ }
+}
+
+export async function deleteExchange(
+ wex: WalletExecutionContext,
+ req: DeleteExchangeRequest,
+): Promise<void> {
+ let inUse: boolean = false;
+ const exchangeBaseUrl = canonicalizeBaseUrl(req.exchangeBaseUrl);
+ await wex.db.runReadWriteTx(
+ {
+ storeNames: [
+ "exchanges",
+ "exchangeDetails",
+ "transactions",
+ "coinAvailability",
+ "coins",
+ "denominations",
+ "exchangeSignKeys",
+ "withdrawalGroups",
+ "planchets",
+ ],
+ },
+ async (tx) => {
+ const exchangeRec = await tx.exchanges.get(exchangeBaseUrl);
+ if (!exchangeRec) {
+ // Nothing to delete!
+ logger.info("no exchange found to delete");
+ return;
+ }
+ const res = await internalGetExchangeResources(wex, tx, exchangeBaseUrl);
+ if (res.hasResources && !req.purge) {
+ inUse = true;
+ return;
+ }
+ await purgeExchange(tx, exchangeBaseUrl);
+ wex.ws.exchangeCache.clear();
+ },
+ );
+
+ if (inUse) {
+ throw TalerError.fromUncheckedDetail({
+ code: TalerErrorCode.WALLET_EXCHANGE_ENTRY_USED,
+ hint: "Exchange in use.",
+ });
+ }
+}
+
+export async function getExchangeResources(
+ wex: WalletExecutionContext,
+ exchangeBaseUrl: string,
+): Promise<GetExchangeResourcesResponse> {
+ // Withdrawals include internal withdrawals from peer transactions
+ const res = await wex.db.runReadOnlyTx(
+ { storeNames: ["exchanges", "withdrawalGroups", "coins"] },
+ async (tx) => {
+ const exchangeRecord = await tx.exchanges.get(exchangeBaseUrl);
+ if (!exchangeRecord) {
+ return undefined;
+ }
+ return internalGetExchangeResources(wex, tx, exchangeBaseUrl);
+ },
+ );
+ if (!res) {
+ throw Error("exchange not found");
+ }
+ return res;
+}
diff --git a/packages/taler-wallet-core/src/headless/NodeHttpLib.ts b/packages/taler-wallet-core/src/headless/NodeHttpLib.ts
deleted file mode 100644
index c1d42796d..000000000
--- a/packages/taler-wallet-core/src/headless/NodeHttpLib.ts
+++ /dev/null
@@ -1,183 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2019 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
-
- SPDX-License-Identifier: AGPL3.0-or-later
-*/
-
-/**
- * Imports.
- */
-import {
- DEFAULT_REQUEST_TIMEOUT_MS,
- Headers,
- HttpRequestLibrary,
- HttpRequestOptions,
- HttpResponse,
-} from "../util/http.js";
-import { RequestThrottler } from "@gnu-taler/taler-util";
-import axios, { AxiosResponse } from "axios";
-import { TalerError } from "../errors.js";
-import { Logger, bytesToString } from "@gnu-taler/taler-util";
-import { TalerErrorCode, URL } from "@gnu-taler/taler-util";
-
-const logger = new Logger("NodeHttpLib.ts");
-
-/**
- * Implementation of the HTTP request library interface for node.
- */
-export class NodeHttpLib implements HttpRequestLibrary {
- private throttle = new RequestThrottler();
- private throttlingEnabled = true;
-
- /**
- * Set whether requests should be throttled.
- */
- setThrottling(enabled: boolean): void {
- this.throttlingEnabled = enabled;
- }
-
- async fetch(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> {
- const method = opt?.method ?? "GET";
- let body = opt?.body;
-
- logger.trace(`Requesting ${method} ${url}`);
-
- const parsedUrl = new URL(url);
- if (this.throttlingEnabled && this.throttle.applyThrottle(url)) {
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED,
- {
- requestMethod: method,
- requestUrl: url,
- throttleStats: this.throttle.getThrottleStats(url),
- },
- `request to origin ${parsedUrl.origin} was throttled`,
- );
- }
- let timeoutMs: number | undefined;
- if (typeof opt?.timeout?.d_ms === "number") {
- timeoutMs = opt.timeout.d_ms;
- } else {
- timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS;
- }
- // FIXME: Use AbortController / etc. to handle cancellation
- let resp: AxiosResponse;
- try {
- let respPromise = axios.default({
- method,
- url: url,
- responseType: "arraybuffer",
- headers: opt?.headers,
- validateStatus: () => true,
- transformResponse: (x) => x,
- data: body,
- timeout: timeoutMs,
- maxRedirects: 0,
- });
- if (opt?.cancellationToken) {
- respPromise = opt.cancellationToken.racePromise(respPromise);
- }
- resp = await respPromise;
- } catch (e: any) {
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_NETWORK_ERROR,
- {
- requestUrl: url,
- requestMethod: method,
- },
- `${e.message}`,
- );
- }
-
- const makeText = async (): Promise<string> => {
- opt?.cancellationToken?.throwIfCancelled();
- const respText = new Uint8Array(resp.data);
- return bytesToString(respText);
- };
-
- const makeJson = async (): Promise<any> => {
- opt?.cancellationToken?.throwIfCancelled();
- let responseJson;
- const respText = await makeText();
- try {
- responseJson = JSON.parse(respText);
- } catch (e) {
- logger.trace(`invalid json: '${resp.data}'`);
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
- {
- httpStatusCode: resp.status,
- requestUrl: url,
- requestMethod: method,
- },
- "Could not parse response body as JSON",
- );
- }
- if (responseJson === null || typeof responseJson !== "object") {
- logger.trace(`invalid json (not an object): '${respText}'`);
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
- {
- httpStatusCode: resp.status,
- requestUrl: url,
- requestMethod: method,
- },
- `invalid JSON`,
- );
- }
- return responseJson;
- };
- const makeBytes = async () => {
- opt?.cancellationToken?.throwIfCancelled();
- if (typeof resp.data.byteLength !== "number") {
- throw Error("expected array buffer");
- }
- const buf = resp.data;
- return buf;
- };
- const headers = new Headers();
- for (const hn of Object.keys(resp.headers)) {
- headers.set(hn, resp.headers[hn]);
- }
- return {
- requestUrl: url,
- requestMethod: method,
- headers,
- status: resp.status,
- text: makeText,
- json: makeJson,
- bytes: makeBytes,
- };
- }
-
- async get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> {
- return this.fetch(url, {
- method: "GET",
- ...opt,
- });
- }
-
- async postJson(
- url: string,
- body: any,
- opt?: HttpRequestOptions,
- ): Promise<HttpResponse> {
- return this.fetch(url, {
- method: "POST",
- body,
- ...opt,
- });
- }
-}
diff --git a/packages/taler-wallet-core/src/host-common.ts b/packages/taler-wallet-core/src/host-common.ts
new file mode 100644
index 000000000..7651e5a12
--- /dev/null
+++ b/packages/taler-wallet-core/src/host-common.ts
@@ -0,0 +1,60 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received 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 { WalletNotification } from "@gnu-taler/taler-util";
+import { HttpRequestLibrary } from "@gnu-taler/taler-util/http";
+
+/**
+ * Helpers to initiate a wallet in a host environment.
+ */
+
+/**
+ */
+export interface DefaultNodeWalletArgs {
+ /**
+ * Location of the wallet database.
+ *
+ * If not specified, the wallet starts out with an empty database and
+ * the wallet database is stored only in memory.
+ */
+ persistentStoragePath?: string;
+
+ /**
+ * Handler for asynchronous notifications from the wallet.
+ */
+ notifyHandler?: (n: WalletNotification) => void;
+
+ /**
+ * If specified, use this as HTTP request library instead
+ * of the default one.
+ */
+ httpLib?: HttpRequestLibrary;
+
+ cryptoWorkerType?: "sync" | "node-worker-thread";
+}
+
+/**
+ * Generate a random alphanumeric ID. Does *not* use cryptographically
+ * secure randomness.
+ */
+export function makeTempfileId(length: number): string {
+ let result = "";
+ const characters = "abcdefghijklmnopqrstuvwxyz0123456789";
+ for (let i = 0; i < length; i++) {
+ result += characters.charAt(Math.floor(Math.random() * characters.length));
+ }
+ return result;
+}
diff --git a/packages/taler-wallet-core/src/crypto/workers/synchronousWorkerFactoryNode.ts b/packages/taler-wallet-core/src/host-impl.missing.ts
index 46cf12915..464a5af15 100644
--- a/packages/taler-wallet-core/src/crypto/workers/synchronousWorkerFactoryNode.ts
+++ b/packages/taler-wallet-core/src/host-impl.missing.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2019 GNUnet e.V.
+ (C) 2019 Taler Systems S.A.
GNU Taler is free 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,22 +15,27 @@
*/
/**
- * Imports.
+ * Helpers to create headless wallets.
+ * @author Florian Dold <dold@taler.net>
*/
-import { CryptoWorkerFactory } from "./cryptoDispatcher.js";
-import { CryptoWorker } from "./cryptoWorkerInterface.js";
-import { SynchronousCryptoWorkerNode } from "./synchronousWorkerNode.js";
/**
- * The synchronous crypto worker produced by this factory doesn't run in the
- * background, but actually blocks the caller until the operation is done.
+ * Imports.
*/
-export class SynchronousCryptoWorkerFactoryNode implements CryptoWorkerFactory {
- startWorker(): CryptoWorker {
- return new SynchronousCryptoWorkerNode();
- }
+import type { AccessStats, IDBFactory } from "@gnu-taler/idb-bridge";
+import { DefaultNodeWalletArgs } from "./host-common.js";
+import { Wallet } from "./index.js";
- getConcurrency(): number {
- return 1;
- }
+/**
+ * Get a wallet instance with default settings for node.
+ *
+ * Extended version that allows getting DB stats.
+ */
+export async function createNativeWalletHost2(
+ args: DefaultNodeWalletArgs = {},
+): Promise<{
+ wallet: Wallet;
+ getDbStats: () => AccessStats;
+}> {
+ throw Error("not implemented");
}
diff --git a/packages/taler-wallet-core/src/headless/helpers.ts b/packages/taler-wallet-core/src/host-impl.node.ts
index 64edf8fb0..ec026b296 100644
--- a/packages/taler-wallet-core/src/headless/helpers.ts
+++ b/packages/taler-wallet-core/src/host-impl.node.ts
@@ -25,84 +25,37 @@
import type { IDBFactory } from "@gnu-taler/idb-bridge";
// eslint-disable-next-line no-duplicate-imports
import {
+ AccessStats,
BridgeIDBFactory,
MemoryBackend,
+ createSqliteBackend,
shimIndexedDB,
} from "@gnu-taler/idb-bridge";
-import { AccessStats } from "@gnu-taler/idb-bridge";
-import { Logger, WalletNotification } from "@gnu-taler/taler-util";
+import { createNodeSqlite3Impl } from "@gnu-taler/idb-bridge/node-sqlite3-bindings";
+import {
+ Logger,
+ SetTimeoutTimerAPI,
+ WalletRunConfig,
+} from "@gnu-taler/taler-util";
+import { createPlatformHttpLib } from "@gnu-taler/taler-util/http";
import * as fs from "fs";
-import { NodeThreadCryptoWorkerFactory } from "../crypto/workers/nodeThreadWorker.js";
-import { SynchronousCryptoWorkerFactoryNode } from "../crypto/workers/synchronousWorkerFactoryNode.js";
-import { openTalerDatabase } from "../db-utils.js";
-import { HttpRequestLibrary } from "../util/http.js";
-import { SetTimeoutTimerAPI } from "../util/timer.js";
-import { Wallet } from "../wallet.js";
-import { NodeHttpLib } from "./NodeHttpLib.js";
-
-const logger = new Logger("headless/helpers.ts");
-
-export interface DefaultNodeWalletArgs {
- /**
- * Location of the wallet database.
- *
- * If not specified, the wallet starts out with an empty database and
- * the wallet database is stored only in memory.
- */
- persistentStoragePath?: string;
-
- /**
- * Handler for asynchronous notifications from the wallet.
- */
- notifyHandler?: (n: WalletNotification) => void;
-
- /**
- * If specified, use this as HTTP request library instead
- * of the default one.
- */
- httpLib?: HttpRequestLibrary;
-
- cryptoWorkerType?: "sync" | "node-worker-thread";
-}
+import { NodeThreadCryptoWorkerFactory } from "./crypto/workers/nodeThreadWorker.js";
+import { SynchronousCryptoWorkerFactoryPlain } from "./crypto/workers/synchronousWorkerFactoryPlain.js";
+import { DefaultNodeWalletArgs, makeTempfileId } from "./host-common.js";
+import { Wallet } from "./wallet.js";
-/**
- * Generate a random alphanumeric ID. Does *not* use cryptographically
- * secure randomness.
- */
-function makeId(length: number): string {
- let result = "";
- const characters = "abcdefghijklmnopqrstuvwxyz0123456789";
- for (let i = 0; i < length; i++) {
- result += characters.charAt(Math.floor(Math.random() * characters.length));
- }
- return result;
-}
+const logger = new Logger("host-impl.node.ts");
-/**
- * Get a wallet instance with default settings for node.
- */
-export async function getDefaultNodeWallet(
- args: DefaultNodeWalletArgs = {},
-): Promise<Wallet> {
- const res = await getDefaultNodeWallet2(args);
- return res.wallet;
+interface MakeDbResult {
+ idbFactory: BridgeIDBFactory;
+ getStats: () => AccessStats;
}
-/**
- * Get a wallet instance with default settings for node.
- *
- * Extended version that allows getting DB stats.
- */
-export async function getDefaultNodeWallet2(
+async function makeFileDb(
args: DefaultNodeWalletArgs = {},
-): Promise<{
- wallet: Wallet;
- getDbStats: () => AccessStats;
-}> {
- BridgeIDBFactory.enableTracing = false;
+): Promise<MakeDbResult> {
const myBackend = new MemoryBackend();
myBackend.enableTracing = false;
-
const storagePath = args.persistentStoragePath;
if (storagePath) {
try {
@@ -117,7 +70,11 @@ export async function getDefaultNodeWallet2(
logger.trace("wallet file doesn't exist yet");
} else {
logger.error("could not open wallet database file");
- throw e;
+ throw Error(
+ "could not open wallet database file",
+ // @ts-expect-error no support for options.cause yet
+ { cause: e },
+ );
}
}
@@ -127,7 +84,8 @@ export async function getDefaultNodeWallet2(
if (args.persistentStoragePath === undefined) {
return;
}
- const tmpPath = `${args.persistentStoragePath}-${makeId(5)}.tmp`;
+ const tmpPath = `${args.persistentStoragePath}-${makeTempfileId(5)}.tmp`;
+ logger.trace("exported DB dump");
const dbContent = myBackend.exportDump();
fs.writeFileSync(tmpPath, JSON.stringify(dbContent, undefined, 2), {
encoding: "utf-8",
@@ -141,31 +99,81 @@ export async function getDefaultNodeWallet2(
BridgeIDBFactory.enableTracing = false;
const myBridgeIdbFactory = new BridgeIDBFactory(myBackend);
- const myIdbFactory: IDBFactory = myBridgeIdbFactory as any as IDBFactory;
+ return {
+ idbFactory: myBridgeIdbFactory,
+ getStats: () => myBackend.accessStats,
+ };
+}
- let myHttpLib;
- if (args.httpLib) {
- myHttpLib = args.httpLib;
- } else {
- myHttpLib = new NodeHttpLib();
+async function makeSqliteDb(
+ args: DefaultNodeWalletArgs,
+): Promise<MakeDbResult> {
+ BridgeIDBFactory.enableTracing = false;
+ const imp = await createNodeSqlite3Impl();
+ const dbFilename = args.persistentStoragePath ?? ":memory:";
+ logger.info(`using database ${dbFilename}`);
+ const myBackend = await createSqliteBackend(imp, {
+ filename: dbFilename,
+ });
+ myBackend.enableTracing = false;
+ if (process.env.TALER_WALLET_DBSTATS) {
+ myBackend.trackStats = true;
}
+ const myBridgeIdbFactory = new BridgeIDBFactory(myBackend);
+ return {
+ getStats() {
+ return myBackend.accessStats;
+ },
+ idbFactory: myBridgeIdbFactory,
+ };
+}
- const myVersionChange = (): Promise<void> => {
- logger.error("version change requested, should not happen");
- throw Error(
- "BUG: wallet DB version change event can't happen with memory IDB",
- );
+/**
+ * Get a wallet instance with default settings for node.
+ *
+ * Extended version that allows getting DB stats.
+ */
+export async function createNativeWalletHost2(
+ args: DefaultNodeWalletArgs = {},
+): Promise<{
+ wallet: Wallet;
+ getDbStats: () => AccessStats;
+}> {
+ const myHttpFactory = (config: WalletRunConfig) => {
+ let myHttpLib;
+ if (args.httpLib) {
+ myHttpLib = args.httpLib;
+ } else {
+ myHttpLib = createPlatformHttpLib({
+ enableThrottling: true,
+ requireTls: !config.features.allowHttp,
+ });
+ }
+ return myHttpLib;
};
- shimIndexedDB(myBridgeIdbFactory);
+ let dbResp: MakeDbResult;
+
+ if (
+ args.persistentStoragePath &&
+ args.persistentStoragePath.endsWith(".json")
+ ) {
+ logger.info("using JSON file DB backend (slow, only use for testing)");
+ dbResp = await makeFileDb(args);
+ } else {
+ logger.info(`using sqlite3 DB backend`);
+ dbResp = await makeSqliteDb(args);
+ }
+
+ const myIdbFactory: IDBFactory = dbResp.idbFactory as any as IDBFactory;
- const myDb = await openTalerDatabase(myIdbFactory, myVersionChange);
+ shimIndexedDB(dbResp.idbFactory);
let workerFactory;
const cryptoWorkerType = args.cryptoWorkerType ?? "node-worker-thread";
if (cryptoWorkerType === "sync") {
logger.info("using synchronous crypto worker");
- workerFactory = new SynchronousCryptoWorkerFactoryNode();
+ workerFactory = new SynchronousCryptoWorkerFactoryPlain();
} else if (cryptoWorkerType === "node-worker-thread") {
try {
// Try if we have worker threads available, fails in older node versions.
@@ -179,7 +187,7 @@ export async function getDefaultNodeWallet2(
logger.warn(
"worker threads not available, falling back to synchronous workers",
);
- workerFactory = new SynchronousCryptoWorkerFactoryNode();
+ workerFactory = new SynchronousCryptoWorkerFactoryPlain();
}
} else {
throw Error(`unsupported crypto worker type '${cryptoWorkerType}'`);
@@ -187,13 +195,18 @@ export async function getDefaultNodeWallet2(
const timer = new SetTimeoutTimerAPI();
- const w = await Wallet.create(myDb, myHttpLib, timer, workerFactory);
+ const w = await Wallet.create(
+ myIdbFactory,
+ myHttpFactory,
+ timer,
+ workerFactory,
+ );
if (args.notifyHandler) {
w.addNotificationListener(args.notifyHandler);
}
return {
wallet: w,
- getDbStats: () => myBackend.accessStats,
+ getDbStats: dbResp.getStats,
};
}
diff --git a/packages/taler-wallet-core/src/host-impl.qtart.ts b/packages/taler-wallet-core/src/host-impl.qtart.ts
new file mode 100644
index 000000000..9c985d0c1
--- /dev/null
+++ b/packages/taler-wallet-core/src/host-impl.qtart.ts
@@ -0,0 +1,219 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Helpers to create headless wallets.
+ * @author Florian Dold <dold@taler.net>
+ */
+
+/**
+ * Imports.
+ */
+import type {
+ IDBFactory,
+ ResultRow,
+ Sqlite3Interface,
+ Sqlite3Statement,
+} from "@gnu-taler/idb-bridge";
+// eslint-disable-next-line no-duplicate-imports
+import {
+ AccessStats,
+ BridgeIDBFactory,
+ MemoryBackend,
+ createSqliteBackend,
+ shimIndexedDB,
+} from "@gnu-taler/idb-bridge";
+import {
+ Logger,
+ SetTimeoutTimerAPI,
+ WalletRunConfig,
+} from "@gnu-taler/taler-util";
+import { createPlatformHttpLib } from "@gnu-taler/taler-util/http";
+import { qjsOs, qjsStd } from "@gnu-taler/taler-util/qtart";
+import { SynchronousCryptoWorkerFactoryPlain } from "./crypto/workers/synchronousWorkerFactoryPlain.js";
+import { DefaultNodeWalletArgs, makeTempfileId } from "./host-common.js";
+import { Wallet } from "./wallet.js";
+
+const logger = new Logger("host-impl.qtart.ts");
+
+interface MakeDbResult {
+ idbFactory: BridgeIDBFactory;
+ getStats: () => AccessStats;
+}
+
+let numStmt = 0;
+
+export async function createQtartSqlite3Impl(): Promise<Sqlite3Interface> {
+ const tart: any = (globalThis as any)._tart;
+ if (!tart) {
+ throw Error("globalThis._qtart not defined");
+ }
+ return {
+ open(filename: string) {
+ const internalDbHandle = tart.sqlite3Open(filename);
+ return {
+ internalDbHandle,
+ close() {
+ tart.sqlite3Close(internalDbHandle);
+ },
+ prepare(stmtStr): Sqlite3Statement {
+ const stmtHandle = tart.sqlite3Prepare(internalDbHandle, stmtStr);
+ return {
+ internalStatement: stmtHandle,
+ getAll(params): ResultRow[] {
+ numStmt++;
+ return tart.sqlite3StmtGetAll(stmtHandle, params);
+ },
+ getFirst(params): ResultRow | undefined {
+ numStmt++;
+ return tart.sqlite3StmtGetFirst(stmtHandle, params);
+ },
+ run(params) {
+ numStmt++;
+ return tart.sqlite3StmtRun(stmtHandle, params);
+ },
+ };
+ },
+ exec(sqlStr): void {
+ numStmt++;
+ tart.sqlite3Exec(internalDbHandle, sqlStr);
+ },
+ };
+ },
+ };
+}
+
+async function makeSqliteDb(
+ args: DefaultNodeWalletArgs,
+): Promise<MakeDbResult> {
+ BridgeIDBFactory.enableTracing = false;
+ const imp = await createQtartSqlite3Impl();
+ const myBackend = await createSqliteBackend(imp, {
+ filename: args.persistentStoragePath ?? ":memory:",
+ });
+ myBackend.trackStats = true;
+ myBackend.enableTracing = false;
+ const myBridgeIdbFactory = new BridgeIDBFactory(myBackend);
+ return {
+ getStats() {
+ return {
+ ...myBackend.accessStats,
+ primitiveStatements: numStmt,
+ };
+ },
+ idbFactory: myBridgeIdbFactory,
+ };
+}
+
+async function makeFileDb(
+ args: DefaultNodeWalletArgs = {},
+): Promise<MakeDbResult> {
+ BridgeIDBFactory.enableTracing = false;
+ const myBackend = new MemoryBackend();
+ myBackend.enableTracing = false;
+
+ const storagePath = args.persistentStoragePath;
+ if (storagePath) {
+ const dbContentStr = qjsStd.loadFile(storagePath);
+ if (dbContentStr != null) {
+ const dbContent = JSON.parse(dbContentStr);
+ myBackend.importDump(dbContent);
+ }
+
+ myBackend.afterCommitCallback = async () => {
+ logger.trace("committing database");
+ // Allow caller to stop persisting the wallet.
+ if (args.persistentStoragePath === undefined) {
+ return;
+ }
+ const tmpPath = `${args.persistentStoragePath}-${makeTempfileId(5)}.tmp`;
+ const dbContent = myBackend.exportDump();
+ logger.trace("exported DB dump");
+ qjsStd.writeFile(tmpPath, JSON.stringify(dbContent, undefined, 2));
+ // Atomically move the temporary file onto the DB path.
+ const res = qjsOs.rename(tmpPath, args.persistentStoragePath);
+ if (res != 0) {
+ throw Error("db commit failed at rename");
+ }
+ logger.trace("committing database done");
+ };
+ }
+
+ const myBridgeIdbFactory = new BridgeIDBFactory(myBackend);
+ return {
+ idbFactory: myBridgeIdbFactory,
+ getStats: () => myBackend.accessStats,
+ };
+}
+
+export async function createNativeWalletHost2(
+ args: DefaultNodeWalletArgs = {},
+): Promise<{
+ wallet: Wallet;
+ getDbStats: () => AccessStats;
+}> {
+ BridgeIDBFactory.enableTracing = false;
+
+ let dbResp: MakeDbResult;
+
+ if (
+ args.persistentStoragePath &&
+ args.persistentStoragePath.endsWith(".json")
+ ) {
+ logger.info("using JSON file DB backend (slow, only use for testing)");
+ dbResp = await makeFileDb(args);
+ } else {
+ logger.info("using sqlite3 DB backend");
+ dbResp = await makeSqliteDb(args);
+ }
+
+ const myIdbFactory: IDBFactory = dbResp.idbFactory as any as IDBFactory;
+
+ shimIndexedDB(dbResp.idbFactory);
+
+ const myHttpFactory = (config: WalletRunConfig) => {
+ let myHttpLib;
+ if (args.httpLib) {
+ myHttpLib = args.httpLib;
+ } else {
+ myHttpLib = createPlatformHttpLib({
+ enableThrottling: true,
+ requireTls: !config.features.allowHttp,
+ });
+ }
+ return myHttpLib;
+ };
+
+ let workerFactory;
+ workerFactory = new SynchronousCryptoWorkerFactoryPlain();
+
+ const timer = new SetTimeoutTimerAPI();
+
+ const w = await Wallet.create(
+ myIdbFactory,
+ myHttpFactory,
+ timer,
+ workerFactory,
+ );
+
+ if (args.notifyHandler) {
+ w.addNotificationListener(args.notifyHandler);
+ }
+ return {
+ wallet: w,
+ getDbStats: dbResp.getStats,
+ };
+}
diff --git a/packages/taler-wallet-core/src/host.ts b/packages/taler-wallet-core/src/host.ts
new file mode 100644
index 000000000..feccf42a6
--- /dev/null
+++ b/packages/taler-wallet-core/src/host.ts
@@ -0,0 +1,46 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received 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 { DefaultNodeWalletArgs } from "./host-common.js";
+import { Wallet } from "./index.js";
+import * as hostImpl from "#host-impl";
+import { AccessStats } from "@gnu-taler/idb-bridge";
+
+/**
+ * Helpers to initiate a wallet in a host environment.
+ */
+
+/**
+ * Get a wallet instance.
+ */
+export async function createNativeWalletHost2(
+ args: DefaultNodeWalletArgs = {},
+): Promise<{
+ wallet: Wallet;
+ getDbStats: () => AccessStats;
+}> {
+ return hostImpl.createNativeWalletHost2(args);
+}
+
+/**
+ * Get a wallet instance.
+ */
+export async function createNativeWalletHost(
+ args: DefaultNodeWalletArgs = {},
+): Promise<Wallet> {
+ const res = await hostImpl.createNativeWalletHost2(args);
+ return res.wallet;
+}
diff --git a/packages/taler-wallet-core/src/index.browser.ts b/packages/taler-wallet-core/src/index.browser.ts
index 02d3665c2..9409673a0 100644
--- a/packages/taler-wallet-core/src/index.browser.ts
+++ b/packages/taler-wallet-core/src/index.browser.ts
@@ -15,4 +15,4 @@
*/
export * from "./index.js";
-export { SynchronousCryptoWorkerPlain as SynchronousCryptoWorker } from "./crypto/workers/synchronousWorkerPlain.js";
+export { SynchronousCryptoWorkerPlain } from "./crypto/workers/synchronousWorkerPlain.js";
diff --git a/packages/taler-wallet-core/src/index.node.ts b/packages/taler-wallet-core/src/index.node.ts
index 8567d13ac..13392d39c 100644
--- a/packages/taler-wallet-core/src/index.node.ts
+++ b/packages/taler-wallet-core/src/index.node.ts
@@ -16,15 +16,8 @@
export * from "./index.js";
-// Utils for using the wallet under node
-export { NodeHttpLib } from "./headless/NodeHttpLib.js";
-export {
- getDefaultNodeWallet,
- getDefaultNodeWallet2,
- DefaultNodeWalletArgs,
-} from "./headless/helpers.js";
export * from "./crypto/workers/nodeThreadWorker.js";
-export { SynchronousCryptoWorkerNode as SynchronousCryptoWorker } from "./crypto/workers/synchronousWorkerNode.js";
+export { SynchronousCryptoWorkerPlain } from "./crypto/workers/synchronousWorkerPlain.js";
export type { AccessStats } from "@gnu-taler/idb-bridge";
-export * from "./crypto/workers/synchronousWorkerFactoryNode.js";
+export * from "./crypto/workers/synchronousWorkerFactoryPlain.js";
diff --git a/packages/taler-wallet-core/src/index.ts b/packages/taler-wallet-core/src/index.ts
index 7cc23aa88..fe2d3af15 100644
--- a/packages/taler-wallet-core/src/index.ts
+++ b/packages/taler-wallet-core/src/index.ts
@@ -18,53 +18,29 @@
* Module entry point for the wallet when used as a node module.
*/
-// Errors
-export * from "./errors.js";
-
-// Util functionality
-export * from "./util/promiseUtils.js";
-export * from "./util/query.js";
-export * from "./util/http.js";
-
-export * from "./versions.js";
-
-export * from "./db.js";
-export * from "./db-utils.js";
-
-// Crypto and crypto workers
-// export * from "./crypto/workers/nodeThreadWorker.js";
-export type { CryptoWorker } from "./crypto/workers/cryptoWorkerInterface.js";
+export * from "./crypto/cryptoImplementation.js";
+export * from "./crypto/cryptoTypes.js";
export {
- CryptoWorkerFactory,
CryptoDispatcher,
-} from "./crypto/workers/cryptoDispatcher.js";
-
-export * from "./pending-types.js";
-
-export * from "./util/debugFlags.js";
-export { InternalWalletState } from "./internal-wallet-state.js";
+ CryptoWorkerFactory,
+} from "./crypto/workers/crypto-dispatcher.js";
+export type { CryptoWorker } from "./crypto/workers/cryptoWorkerInterface.js";
+export { SynchronousCryptoWorkerFactoryPlain } from "./crypto/workers/synchronousWorkerFactoryPlain.js";
+export * from "./host-common.js";
+export * from "./host.js";
+export * from "./versions.js";
export * from "./wallet-api-types.js";
export * from "./wallet.js";
-export * from "./operations/backup/index.js";
-
-export * from "./operations/exchanges.js";
-
-export * from "./bank-api-client.js";
+export { parseTransactionIdentifier } from "./transactions.js";
-export * from "./operations/withdraw.js";
-export * from "./operations/refresh.js";
-
-export * from "./dbless.js";
+export { createPairTimeline } from "./denominations.js";
+// FIXME: Should these really be exported?!
export {
- nativeCryptoR,
- nativeCrypto,
- nullCrypto,
- TalerCryptoInterface,
-} from "./crypto/cryptoImplementation.js";
-
-export * from "./util/timer.js";
-export * from "./util/denominations.js";
-
-export { SynchronousCryptoWorkerFactoryPlain } from "./crypto/workers/synchronousWorkerFactoryPlain.js";
+ WalletStoresV1,
+ deleteTalerDatabase,
+ exportDb,
+ importDb,
+} from "./db.js";
+export { DbAccess } from "./query.js";
diff --git a/packages/taler-wallet-core/src/instructedAmountConversion.test.ts b/packages/taler-wallet-core/src/instructedAmountConversion.test.ts
new file mode 100644
index 000000000..03e702568
--- /dev/null
+++ b/packages/taler-wallet-core/src/instructedAmountConversion.test.ts
@@ -0,0 +1,767 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ AbsoluteTime,
+ AgeRestriction,
+ AmountJson,
+ Amounts,
+ Duration,
+ TransactionAmountMode,
+} from "@gnu-taler/taler-util";
+import test, { ExecutionContext } from "ava";
+import {
+ CoinInfo,
+ convertDepositAmountForAvailableCoins,
+ convertWithdrawalAmountFromAvailableCoins,
+ getMaxDepositAmountForAvailableCoins,
+} from "./instructedAmountConversion.js";
+
+function makeCurrencyHelper(currency: string) {
+ return (sx: TemplateStringsArray, ...vx: any[]) => {
+ const s = String.raw({ raw: sx }, ...vx);
+ return Amounts.parseOrThrow(`${currency}:${s}`);
+ };
+}
+
+const kudos = makeCurrencyHelper("kudos");
+
+function defaultFeeConfig(value: AmountJson, totalAvailable: number): CoinInfo {
+ return {
+ id: Amounts.stringify(value),
+ denomDeposit: kudos`0.01`,
+ denomRefresh: kudos`0.01`,
+ denomWithdraw: kudos`0.01`,
+ exchangeBaseUrl: "1",
+ duration: Duration.getForever(),
+ exchangePurse: undefined,
+ exchangeWire: undefined,
+ maxAge: AgeRestriction.AGE_UNRESTRICTED,
+ totalAvailable,
+ value,
+ };
+}
+type Coin = [AmountJson, number];
+
+/**
+ * Making a deposit with effective amount
+ *
+ */
+
+test("deposit effective 2", (t) => {
+ const coinList: Coin[] = [
+ [kudos`2`, 5],
+ [kudos`5`, 5],
+ ];
+ const result = convertDepositAmountForAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {},
+ },
+ kudos`2`,
+ TransactionAmountMode.Effective,
+ );
+ t.is(Amounts.stringifyValue(result.effective), "2");
+ t.is(Amounts.stringifyValue(result.raw), "1.99");
+});
+
+test("deposit effective 10", (t) => {
+ const coinList: Coin[] = [
+ [kudos`2`, 5],
+ [kudos`5`, 5],
+ ];
+ const result = convertDepositAmountForAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {},
+ },
+ kudos`10`,
+ TransactionAmountMode.Effective,
+ );
+ t.is(Amounts.stringifyValue(result.effective), "10");
+ t.is(Amounts.stringifyValue(result.raw), "9.98");
+});
+
+test("deposit effective 24", (t) => {
+ const coinList: Coin[] = [
+ [kudos`2`, 5],
+ [kudos`5`, 5],
+ ];
+ const result = convertDepositAmountForAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {},
+ },
+ kudos`24`,
+ TransactionAmountMode.Effective,
+ );
+ t.is(Amounts.stringifyValue(result.effective), "24");
+ t.is(Amounts.stringifyValue(result.raw), "23.94");
+});
+
+test("deposit effective 40", (t) => {
+ const coinList: Coin[] = [
+ [kudos`2`, 5],
+ [kudos`5`, 5],
+ ];
+ const result = convertDepositAmountForAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {},
+ },
+ kudos`40`,
+ TransactionAmountMode.Effective,
+ );
+ t.is(Amounts.stringifyValue(result.effective), "35");
+ t.is(Amounts.stringifyValue(result.raw), "34.9");
+});
+
+test("deposit with wire fee effective 2", (t) => {
+ const coinList: Coin[] = [
+ [kudos`2`, 5],
+ [kudos`5`, 5],
+ ];
+ const result = convertDepositAmountForAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {
+ one: {
+ wireFee: kudos`0.1`,
+ purseFee: kudos`0.00`,
+ creditDeadline: AbsoluteTime.never(),
+ debitDeadline: AbsoluteTime.never(),
+ },
+ },
+ },
+ kudos`2`,
+ TransactionAmountMode.Effective,
+ );
+ t.is(Amounts.stringifyValue(result.effective), "2");
+ t.is(Amounts.stringifyValue(result.raw), "1.89");
+});
+
+/**
+ * Making a deposit with raw amount, using the result from effective
+ *
+ */
+
+test("deposit raw 1.99 (effective 2)", (t) => {
+ const coinList: Coin[] = [
+ [kudos`2`, 5],
+ [kudos`5`, 5],
+ ];
+ const result = convertDepositAmountForAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {},
+ },
+ kudos`1.99`,
+ TransactionAmountMode.Raw,
+ );
+ t.is(Amounts.stringifyValue(result.effective), "2");
+ t.is(Amounts.stringifyValue(result.raw), "1.99");
+});
+
+test("deposit raw 9.98 (effective 10)", (t) => {
+ const coinList: Coin[] = [
+ [kudos`2`, 5],
+ [kudos`5`, 5],
+ ];
+ const result = convertDepositAmountForAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {},
+ },
+ kudos`9.98`,
+ TransactionAmountMode.Raw,
+ );
+ t.is(Amounts.stringifyValue(result.effective), "10");
+ t.is(Amounts.stringifyValue(result.raw), "9.98");
+});
+
+test("deposit raw 23.94 (effective 24)", (t) => {
+ const coinList: Coin[] = [
+ [kudos`2`, 5],
+ [kudos`5`, 5],
+ ];
+ const result = convertDepositAmountForAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {},
+ },
+ kudos`23.94`,
+ TransactionAmountMode.Raw,
+ );
+ t.is(Amounts.stringifyValue(result.effective), "24");
+ t.is(Amounts.stringifyValue(result.raw), "23.94");
+});
+
+test("deposit raw 34.9 (effective 40)", (t) => {
+ const coinList: Coin[] = [
+ [kudos`2`, 5],
+ [kudos`5`, 5],
+ ];
+ const result = convertDepositAmountForAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {},
+ },
+ kudos`34.9`,
+ TransactionAmountMode.Raw,
+ );
+ t.is(Amounts.stringifyValue(result.effective), "35");
+ t.is(Amounts.stringifyValue(result.raw), "34.9");
+});
+
+test("deposit with wire fee raw 2", (t) => {
+ const coinList: Coin[] = [
+ [kudos`2`, 5],
+ [kudos`5`, 5],
+ ];
+ const result = convertDepositAmountForAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {
+ one: {
+ wireFee: kudos`0.1`,
+ purseFee: kudos`0.00`,
+ creditDeadline: AbsoluteTime.never(),
+ debitDeadline: AbsoluteTime.never(),
+ },
+ },
+ },
+ kudos`2`,
+ TransactionAmountMode.Effective,
+ );
+ t.is(Amounts.stringifyValue(result.effective), "2");
+ t.is(Amounts.stringifyValue(result.raw), "1.89");
+});
+
+/**
+ * Calculating the max amount possible to deposit
+ *
+ */
+
+test("deposit max 35", (t) => {
+ const coinList: Coin[] = [
+ [kudos`2`, 5],
+ [kudos`5`, 5],
+ ];
+ const result = getMaxDepositAmountForAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {
+ "2": {
+ wireFee: kudos`0.00`,
+ purseFee: kudos`0.00`,
+ creditDeadline: AbsoluteTime.never(),
+ debitDeadline: AbsoluteTime.never(),
+ },
+ },
+ },
+ "KUDOS",
+ );
+ t.is(Amounts.stringifyValue(result.raw), "34.9");
+ t.is(Amounts.stringifyValue(result.effective), "35");
+});
+
+test("deposit max 35 with wirefee", (t) => {
+ const coinList: Coin[] = [
+ [kudos`2`, 5],
+ [kudos`5`, 5],
+ ];
+ const result = getMaxDepositAmountForAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {
+ "2": {
+ wireFee: kudos`1`,
+ purseFee: kudos`0.00`,
+ creditDeadline: AbsoluteTime.never(),
+ debitDeadline: AbsoluteTime.never(),
+ },
+ },
+ },
+ "KUDOS",
+ );
+ t.is(Amounts.stringifyValue(result.raw), "33.9");
+ t.is(Amounts.stringifyValue(result.effective), "35");
+});
+
+test("deposit max repeated denom", (t) => {
+ const coinList: Coin[] = [
+ [kudos`2`, 1],
+ [kudos`2`, 1],
+ [kudos`5`, 1],
+ ];
+ const result = getMaxDepositAmountForAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {
+ "2": {
+ wireFee: kudos`0.00`,
+ purseFee: kudos`0.00`,
+ creditDeadline: AbsoluteTime.never(),
+ debitDeadline: AbsoluteTime.never(),
+ },
+ },
+ },
+ "KUDOS",
+ );
+ t.is(Amounts.stringifyValue(result.raw), "8.97");
+ t.is(Amounts.stringifyValue(result.effective), "9");
+});
+
+/**
+ * Making a withdrawal with effective amount
+ *
+ */
+
+test("withdraw effective 2", (t) => {
+ const coinList: Coin[] = [
+ [kudos`2`, 5],
+ [kudos`5`, 5],
+ ];
+ const result = convertWithdrawalAmountFromAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {},
+ },
+ kudos`2`,
+ TransactionAmountMode.Effective,
+ );
+ t.is(Amounts.stringifyValue(result.effective), "2");
+ t.is(Amounts.stringifyValue(result.raw), "2.01");
+});
+
+test("withdraw effective 10", (t) => {
+ const coinList: Coin[] = [
+ [kudos`2`, 5],
+ [kudos`5`, 5],
+ ];
+ const result = convertWithdrawalAmountFromAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {},
+ },
+ kudos`10`,
+ TransactionAmountMode.Effective,
+ );
+ t.is(Amounts.stringifyValue(result.effective), "10");
+ t.is(Amounts.stringifyValue(result.raw), "10.02");
+});
+
+test("withdraw effective 24", (t) => {
+ const coinList: Coin[] = [
+ [kudos`2`, 5],
+ [kudos`5`, 5],
+ ];
+ const result = convertWithdrawalAmountFromAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {},
+ },
+ kudos`24`,
+ TransactionAmountMode.Effective,
+ );
+ t.is(Amounts.stringifyValue(result.effective), "24");
+ t.is(Amounts.stringifyValue(result.raw), "24.06");
+});
+
+test("withdraw effective 40", (t) => {
+ const coinList: Coin[] = [
+ [kudos`2`, 5],
+ [kudos`5`, 5],
+ ];
+ const result = convertWithdrawalAmountFromAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {},
+ },
+ kudos`40`,
+ TransactionAmountMode.Effective,
+ );
+ t.is(Amounts.stringifyValue(result.effective), "40");
+ t.is(Amounts.stringifyValue(result.raw), "40.08");
+});
+
+/**
+ * Making a deposit with raw amount, using the result from effective
+ *
+ */
+
+test("withdraw raw 2.01 (effective 2)", (t) => {
+ const coinList: Coin[] = [
+ [kudos`2`, 5],
+ [kudos`5`, 5],
+ ];
+ const result = convertWithdrawalAmountFromAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {},
+ },
+ kudos`2.01`,
+ TransactionAmountMode.Raw,
+ );
+ t.is(Amounts.stringifyValue(result.effective), "2");
+ t.is(Amounts.stringifyValue(result.raw), "2.01");
+});
+
+test("withdraw raw 10.02 (effective 10)", (t) => {
+ const coinList: Coin[] = [
+ [kudos`2`, 5],
+ [kudos`5`, 5],
+ ];
+ const result = convertWithdrawalAmountFromAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {},
+ },
+ kudos`10.02`,
+ TransactionAmountMode.Raw,
+ );
+ t.is(Amounts.stringifyValue(result.effective), "10");
+ t.is(Amounts.stringifyValue(result.raw), "10.02");
+});
+
+test("withdraw raw 24.06 (effective 24)", (t) => {
+ const coinList: Coin[] = [
+ [kudos`2`, 5],
+ [kudos`5`, 5],
+ ];
+ const result = convertWithdrawalAmountFromAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {},
+ },
+ kudos`24.06`,
+ TransactionAmountMode.Raw,
+ );
+ t.is(Amounts.stringifyValue(result.effective), "24");
+ t.is(Amounts.stringifyValue(result.raw), "24.06");
+});
+
+test("withdraw raw 40.08 (effective 40)", (t) => {
+ const coinList: Coin[] = [
+ [kudos`2`, 5],
+ [kudos`5`, 5],
+ ];
+ const result = convertWithdrawalAmountFromAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {},
+ },
+ kudos`40.08`,
+ TransactionAmountMode.Raw,
+ );
+ t.is(Amounts.stringifyValue(result.effective), "40");
+ t.is(Amounts.stringifyValue(result.raw), "40.08");
+});
+
+test("withdraw raw 25", (t) => {
+ const coinList: Coin[] = [
+ [kudos`0.1`, 0],
+ [kudos`1`, 0],
+ [kudos`2`, 0],
+ [kudos`5`, 0],
+ ];
+ const result = convertWithdrawalAmountFromAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {},
+ },
+ kudos`25`,
+ TransactionAmountMode.Raw,
+ );
+ t.is(Amounts.stringifyValue(result.effective), "24.8");
+ t.is(Amounts.stringifyValue(result.raw), "24.94");
+});
+
+test("withdraw effective 24.8 (raw 25)", (t) => {
+ const coinList: Coin[] = [
+ [kudos`0.1`, 0],
+ [kudos`1`, 0],
+ [kudos`2`, 0],
+ [kudos`5`, 0],
+ ];
+ const result = convertWithdrawalAmountFromAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {},
+ },
+ kudos`24.8`,
+ TransactionAmountMode.Effective,
+ );
+ t.is(Amounts.stringifyValue(result.effective), "24.8");
+ t.is(Amounts.stringifyValue(result.raw), "24.94");
+});
+
+/**
+ * Making a deposit with refresh
+ *
+ */
+
+test("deposit with refresh: effective 3", (t) => {
+ const coinList: Coin[] = [
+ [kudos`0.1`, 0],
+ [kudos`1`, 0],
+ [kudos`2`, 5],
+ [kudos`5`, 5],
+ ];
+ const result = convertDepositAmountForAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {},
+ },
+ kudos`3`,
+ TransactionAmountMode.Effective,
+ );
+ t.is(Amounts.stringifyValue(result.effective), "3.1");
+ t.is(Amounts.stringifyValue(result.raw), "2.98");
+ expectDefined(t, result.refresh);
+ //FEES
+ //deposit 2 x 0.01
+ //refresh 1 x 0.01
+ //withdraw 9 x 0.01
+ //-----------------
+ //op 0.12
+
+ //coins sent 2 x 2.0
+ //coins recv 9 x 0.1
+ //-------------------
+ //effective 3.10
+ //raw 2.98
+ t.is(Amounts.stringifyValue(result.refresh.selected.id), "2");
+ t.deepEqual(asCoinList(result.refresh.coins), [[kudos`0.1`, 9]]);
+});
+
+test("deposit with refresh: raw 2.98 (effective 3)", (t) => {
+ const coinList: Coin[] = [
+ [kudos`0.1`, 0],
+ [kudos`1`, 0],
+ [kudos`2`, 5],
+ [kudos`5`, 5],
+ ];
+ const result = convertDepositAmountForAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {},
+ },
+ kudos`2.98`,
+ TransactionAmountMode.Raw,
+ );
+ t.is(Amounts.stringifyValue(result.effective), "3.2");
+ t.is(Amounts.stringifyValue(result.raw), "3.09");
+ expectDefined(t, result.refresh);
+ //FEES
+ //deposit 1 x 0.01
+ //refresh 1 x 0.01
+ //withdraw 8 x 0.01
+ //-----------------
+ //op 0.10
+
+ //coins sent 1 x 2.0
+ //coins recv 8 x 0.1
+ //-------------------
+ //effective 3.20
+ //raw 3.09
+ t.is(Amounts.stringifyValue(result.refresh.selected.id), "2");
+ t.deepEqual(asCoinList(result.refresh.coins), [[kudos`0.1`, 8]]);
+});
+
+test("deposit with refresh: effective 3.2 (raw 2.98)", (t) => {
+ const coinList: Coin[] = [
+ [kudos`0.1`, 0],
+ [kudos`1`, 0],
+ [kudos`2`, 5],
+ [kudos`5`, 5],
+ ];
+ const result = convertDepositAmountForAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {},
+ },
+ kudos`3.2`,
+ TransactionAmountMode.Effective,
+ );
+ t.is(Amounts.stringifyValue(result.effective), "3.3");
+ t.is(Amounts.stringifyValue(result.raw), "3.2");
+ expectDefined(t, result.refresh);
+ //FEES
+ //deposit 2 x 0.01
+ //refresh 1 x 0.01
+ //withdraw 7 x 0.01
+ //-----------------
+ //op 0.10
+
+ //coins sent 2 x 2.0
+ //coins recv 7 x 0.1
+ //-------------------
+ //effective 3.30
+ //raw 3.20
+ t.is(Amounts.stringifyValue(result.refresh.selected.id), "2");
+ t.deepEqual(asCoinList(result.refresh.coins), [[kudos`0.1`, 7]]);
+});
+
+function expectDefined<T>(
+ t: ExecutionContext,
+ v: T | undefined,
+): asserts v is T {
+ t.assert(v !== undefined);
+}
+
+function asCoinList(v: { info: CoinInfo; size: number }[]): any {
+ return v.map((c) => {
+ return [c.info.value, c.size];
+ });
+}
+
+/**
+ * regression tests
+ */
+
+test("demo: withdraw raw 25", (t) => {
+ const coinList: Coin[] = [
+ [kudos`0.1`, 0],
+ [kudos`1`, 0],
+ [kudos`2`, 0],
+ [kudos`5`, 0],
+ [kudos`10`, 0],
+ ];
+ const result = convertWithdrawalAmountFromAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {},
+ },
+ kudos`25`,
+ TransactionAmountMode.Raw,
+ );
+ t.is(Amounts.stringifyValue(result.effective), "24.8");
+ t.is(Amounts.stringifyValue(result.raw), "24.92");
+ // coins received
+ // 8 x 0.1
+ // 2 x 0.2
+ // 2 x 10.0
+ // total effective 24.8
+ // fee 12 x 0.01 = 0.12
+ // total raw 24.92
+ // left in reserve 25 - 24.92 == 0.08
+
+ //current wallet impl: hides the left in reserve fee
+ //shows fee = 0.2
+});
+
+test("demo: deposit max after withdraw raw 25", (t) => {
+ const coinList: Coin[] = [
+ [kudos`0.1`, 8],
+ [kudos`1`, 0],
+ [kudos`2`, 2],
+ [kudos`5`, 0],
+ [kudos`10`, 2],
+ ];
+ const result = getMaxDepositAmountForAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {
+ one: {
+ wireFee: kudos`0.01`,
+ purseFee: kudos`0.00`,
+ creditDeadline: AbsoluteTime.never(),
+ debitDeadline: AbsoluteTime.never(),
+ },
+ },
+ },
+ "KUDOS",
+ );
+ t.is(Amounts.stringifyValue(result.effective), "24.8");
+ t.is(Amounts.stringifyValue(result.raw), "24.67");
+
+ // 8 x 0.1
+ // 2 x 0.2
+ // 2 x 10.0
+ // total effective 24.8
+ // deposit fee 12 x 0.01 = 0.12
+ // wire fee 0.01
+ // total raw: 24.8 - 0.13 = 24.67
+
+ // current wallet impl fee 0.14
+});
+
+test("demo: withdraw raw 13", (t) => {
+ const coinList: Coin[] = [
+ [kudos`0.1`, 0],
+ [kudos`1`, 0],
+ [kudos`2`, 0],
+ [kudos`5`, 0],
+ [kudos`10`, 0],
+ ];
+ const result = convertWithdrawalAmountFromAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {},
+ },
+ kudos`13`,
+ TransactionAmountMode.Raw,
+ );
+ t.is(Amounts.stringifyValue(result.effective), "12.8");
+ t.is(Amounts.stringifyValue(result.raw), "12.9");
+ // coins received
+ // 8 x 0.1
+ // 1 x 0.2
+ // 1 x 10.0
+ // total effective 12.8
+ // fee 10 x 0.01 = 0.10
+ // total raw 12.9
+ // left in reserve 13 - 12.9 == 0.1
+
+ //current wallet impl: hides the left in reserve fee
+ //shows fee = 0.2
+});
+
+test("demo: deposit max after withdraw raw 13", (t) => {
+ const coinList: Coin[] = [
+ [kudos`0.1`, 8],
+ [kudos`1`, 0],
+ [kudos`2`, 1],
+ [kudos`5`, 0],
+ [kudos`10`, 1],
+ ];
+ const result = getMaxDepositAmountForAvailableCoins(
+ {
+ list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ exchanges: {
+ one: {
+ wireFee: kudos`0.01`,
+ purseFee: kudos`0.00`,
+ creditDeadline: AbsoluteTime.never(),
+ debitDeadline: AbsoluteTime.never(),
+ },
+ },
+ },
+ "KUDOS",
+ );
+ t.is(Amounts.stringifyValue(result.effective), "12.8");
+ t.is(Amounts.stringifyValue(result.raw), "12.69");
+
+ // 8 x 0.1
+ // 1 x 0.2
+ // 1 x 10.0
+ // total effective 12.8
+ // deposit fee 10 x 0.01 = 0.10
+ // wire fee 0.01
+ // total raw: 12.8 - 0.11 = 12.69
+
+ // current wallet impl fee 0.14
+});
diff --git a/packages/taler-wallet-core/src/instructedAmountConversion.ts b/packages/taler-wallet-core/src/instructedAmountConversion.ts
new file mode 100644
index 000000000..1f7d95959
--- /dev/null
+++ b/packages/taler-wallet-core/src/instructedAmountConversion.ts
@@ -0,0 +1,872 @@
+/*
+ This file is part of GNU Taler
+ (C) 2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { GlobalIDB } from "@gnu-taler/idb-bridge";
+import {
+ AbsoluteTime,
+ AgeRestriction,
+ AmountJson,
+ AmountResponse,
+ Amounts,
+ ConvertAmountRequest,
+ Duration,
+ GetAmountRequest,
+ GetPlanForOperationRequest,
+ TransactionAmountMode,
+ TransactionType,
+ checkDbInvariant,
+ parsePaytoUri,
+ strcmp,
+} from "@gnu-taler/taler-util";
+import { DenominationRecord, timestampProtocolFromDb } from "./db.js";
+import { getExchangeWireDetailsInTx } from "./exchanges.js";
+import { WalletExecutionContext } from "./wallet.js";
+
+export interface CoinInfo {
+ id: string;
+ value: AmountJson;
+ denomDeposit: AmountJson;
+ denomWithdraw: AmountJson;
+ denomRefresh: AmountJson;
+ totalAvailable: number | undefined;
+ exchangeWire: AmountJson | undefined;
+ exchangePurse: AmountJson | undefined;
+ duration: Duration;
+ exchangeBaseUrl: string;
+ maxAge: number;
+}
+
+/**
+ * If the operation going to be plan subtracts
+ * or adds amount in the wallet db
+ */
+export enum OperationType {
+ Credit = "credit",
+ Debit = "debit",
+}
+
+// FIXME: Name conflict ...
+interface ExchangeInfo {
+ wireFee: AmountJson | undefined;
+ purseFee: AmountJson | undefined;
+ creditDeadline: AbsoluteTime;
+ debitDeadline: AbsoluteTime;
+}
+
+function getOperationType(txType: TransactionType): OperationType {
+ const operationType =
+ txType === TransactionType.Withdrawal
+ ? OperationType.Credit
+ : txType === TransactionType.Deposit
+ ? OperationType.Debit
+ : undefined;
+ if (!operationType) {
+ throw Error(`operation type ${txType} not yet supported`);
+ }
+ return operationType;
+}
+
+interface SelectedCoins {
+ totalValue: AmountJson;
+ coins: { info: CoinInfo; size: number }[];
+ refresh?: RefreshChoice;
+}
+
+function getCoinsFilter(req: GetPlanForOperationRequest): CoinsFilter {
+ switch (req.type) {
+ case TransactionType.Withdrawal: {
+ return {
+ exchanges:
+ req.exchangeUrl === undefined ? undefined : [req.exchangeUrl],
+ };
+ }
+ case TransactionType.Deposit: {
+ const payto = parsePaytoUri(req.account);
+ if (!payto) {
+ throw Error(`wrong payto ${req.account}`);
+ }
+ return {
+ wireMethod: payto.targetType,
+ };
+ }
+ }
+}
+
+interface RefreshChoice {
+ /**
+ * Amount that need to be covered
+ */
+ gap: AmountJson;
+ totalFee: AmountJson;
+ selected: CoinInfo;
+ totalChangeValue: AmountJson;
+ refreshEffective: AmountJson;
+ coins: { info: CoinInfo; size: number }[];
+
+ // totalValue: AmountJson;
+ // totalDepositFee: AmountJson;
+ // totalRefreshFee: AmountJson;
+ // totalChangeContribution: AmountJson;
+ // totalChangeWithdrawalFee: AmountJson;
+}
+
+interface CoinsFilter {
+ shouldCalculatePurseFee?: boolean;
+ exchanges?: string[];
+ wireMethod?: string;
+ ageRestricted?: number;
+}
+
+interface AvailableCoins {
+ list: CoinInfo[];
+ exchanges: Record<string, ExchangeInfo>;
+}
+
+/**
+ * Get all the denoms that can be used for a operation that is limited
+ * by the following restrictions.
+ * This function is costly (by the database access) but with high chances
+ * of being cached
+ */
+async function getAvailableDenoms(
+ wex: WalletExecutionContext,
+ op: TransactionType,
+ currency: string,
+ filters: CoinsFilter = {},
+): Promise<AvailableCoins> {
+ const operationType = getOperationType(TransactionType.Deposit);
+
+ return await wex.db.runReadOnlyTx(
+ {
+ storeNames: [
+ "exchanges",
+ "exchangeDetails",
+ "denominations",
+ "coinAvailability",
+ ],
+ },
+ async (tx) => {
+ const list: CoinInfo[] = [];
+ const exchanges: Record<string, ExchangeInfo> = {};
+
+ const databaseExchanges = await tx.exchanges.iter().toArray();
+ const filteredExchanges =
+ filters.exchanges ?? databaseExchanges.map((e) => e.baseUrl);
+
+ for (const exchangeBaseUrl of filteredExchanges) {
+ const exchangeDetails = await getExchangeWireDetailsInTx(
+ tx,
+ exchangeBaseUrl,
+ );
+ // 1.- exchange has same currency
+ if (exchangeDetails?.currency !== currency) {
+ continue;
+ }
+
+ let deadline = AbsoluteTime.never();
+ // 2.- exchange supports wire method
+ let wireFee: AmountJson | undefined;
+ if (filters.wireMethod) {
+ const wireMethodWithDates =
+ exchangeDetails.wireInfo.feesForType[filters.wireMethod];
+
+ if (!wireMethodWithDates) {
+ throw Error(
+ `exchange ${exchangeBaseUrl} doesn't have wire method ${filters.wireMethod}`,
+ );
+ }
+ const wireMethodFee = wireMethodWithDates.find((x) => {
+ return AbsoluteTime.isBetween(
+ AbsoluteTime.now(),
+ AbsoluteTime.fromProtocolTimestamp(x.startStamp),
+ AbsoluteTime.fromProtocolTimestamp(x.endStamp),
+ );
+ });
+
+ if (!wireMethodFee) {
+ throw Error(
+ `exchange ${exchangeBaseUrl} doesn't have wire fee defined for this period`,
+ );
+ }
+ wireFee = Amounts.parseOrThrow(wireMethodFee.wireFee);
+ deadline = AbsoluteTime.min(
+ deadline,
+ AbsoluteTime.fromProtocolTimestamp(wireMethodFee.endStamp),
+ );
+ }
+ // exchanges[exchangeBaseUrl].wireFee = wireMethodFee;
+
+ // 3.- exchange supports wire method
+ let purseFee: AmountJson | undefined;
+ if (filters.shouldCalculatePurseFee) {
+ const purseFeeFound = exchangeDetails.globalFees.find((x) => {
+ return AbsoluteTime.isBetween(
+ AbsoluteTime.now(),
+ AbsoluteTime.fromProtocolTimestamp(x.startDate),
+ AbsoluteTime.fromProtocolTimestamp(x.endDate),
+ );
+ });
+ if (!purseFeeFound) {
+ throw Error(
+ `exchange ${exchangeBaseUrl} doesn't have purse fee defined for this period`,
+ );
+ }
+ purseFee = Amounts.parseOrThrow(purseFeeFound.purseFee);
+ deadline = AbsoluteTime.min(
+ deadline,
+ AbsoluteTime.fromProtocolTimestamp(purseFeeFound.endDate),
+ );
+ }
+
+ let creditDeadline = AbsoluteTime.never();
+ let debitDeadline = AbsoluteTime.never();
+ //4.- filter coins restricted by age
+ if (operationType === OperationType.Credit) {
+ // FIXME: Use denom groups instead of querying all denominations!
+ const ds =
+ await tx.denominations.indexes.byExchangeBaseUrl.getAll(
+ exchangeBaseUrl,
+ );
+ for (const denom of ds) {
+ const expiresWithdraw = AbsoluteTime.fromProtocolTimestamp(
+ timestampProtocolFromDb(denom.stampExpireWithdraw),
+ );
+ const expiresDeposit = AbsoluteTime.fromProtocolTimestamp(
+ timestampProtocolFromDb(denom.stampExpireDeposit),
+ );
+ creditDeadline = AbsoluteTime.min(deadline, expiresWithdraw);
+ debitDeadline = AbsoluteTime.min(deadline, expiresDeposit);
+ list.push(
+ buildCoinInfoFromDenom(
+ denom,
+ purseFee,
+ wireFee,
+ AgeRestriction.AGE_UNRESTRICTED,
+ Number.MAX_SAFE_INTEGER, // Max withdrawable from single denom
+ ),
+ );
+ }
+ } else {
+ const ageLower = filters.ageRestricted ?? 0;
+ const ageUpper = AgeRestriction.AGE_UNRESTRICTED;
+
+ const myExchangeCoins =
+ await tx.coinAvailability.indexes.byExchangeAgeAvailability.getAll(
+ GlobalIDB.KeyRange.bound(
+ [exchangeDetails.exchangeBaseUrl, ageLower, 1],
+ [
+ exchangeDetails.exchangeBaseUrl,
+ ageUpper,
+ Number.MAX_SAFE_INTEGER,
+ ],
+ ),
+ );
+ //5.- save denoms with how many coins are available
+ // FIXME: Check that the individual denomination is audited!
+ // FIXME: Should we exclude denominations that are
+ // not spendable anymore?
+ for (const coinAvail of myExchangeCoins) {
+ const denom = await tx.denominations.get([
+ coinAvail.exchangeBaseUrl,
+ coinAvail.denomPubHash,
+ ]);
+ checkDbInvariant(!!denom);
+ if (denom.isRevoked || !denom.isOffered) {
+ continue;
+ }
+ const expiresWithdraw = AbsoluteTime.fromProtocolTimestamp(
+ timestampProtocolFromDb(denom.stampExpireWithdraw),
+ );
+ const expiresDeposit = AbsoluteTime.fromProtocolTimestamp(
+ timestampProtocolFromDb(denom.stampExpireDeposit),
+ );
+ creditDeadline = AbsoluteTime.min(deadline, expiresWithdraw);
+ debitDeadline = AbsoluteTime.min(deadline, expiresDeposit);
+ list.push(
+ buildCoinInfoFromDenom(
+ denom,
+ purseFee,
+ wireFee,
+ coinAvail.maxAge,
+ coinAvail.freshCoinCount,
+ ),
+ );
+ }
+ }
+
+ exchanges[exchangeBaseUrl] = {
+ purseFee,
+ wireFee,
+ debitDeadline,
+ creditDeadline,
+ };
+ }
+
+ return { list, exchanges };
+ },
+ );
+}
+
+function buildCoinInfoFromDenom(
+ denom: DenominationRecord,
+ purseFee: AmountJson | undefined,
+ wireFee: AmountJson | undefined,
+ maxAge: number,
+ total: number,
+): CoinInfo {
+ return {
+ id: denom.denomPubHash,
+ denomWithdraw: Amounts.parseOrThrow(denom.fees.feeWithdraw),
+ denomDeposit: Amounts.parseOrThrow(denom.fees.feeDeposit),
+ denomRefresh: Amounts.parseOrThrow(denom.fees.feeRefresh),
+ exchangePurse: purseFee,
+ exchangeWire: wireFee,
+ exchangeBaseUrl: denom.exchangeBaseUrl,
+ duration: AbsoluteTime.difference(
+ AbsoluteTime.now(),
+ AbsoluteTime.fromProtocolTimestamp(
+ timestampProtocolFromDb(denom.stampExpireDeposit),
+ ),
+ ),
+ totalAvailable: total,
+ value: Amounts.parseOrThrow(denom.value),
+ maxAge,
+ };
+}
+
+export async function convertDepositAmount(
+ wex: WalletExecutionContext,
+ req: ConvertAmountRequest,
+): Promise<AmountResponse> {
+ const amount = Amounts.parseOrThrow(req.amount);
+ // const filter = getCoinsFilter(req);
+
+ const denoms = await getAvailableDenoms(
+ wex,
+ TransactionType.Deposit,
+ amount.currency,
+ {},
+ );
+ const result = convertDepositAmountForAvailableCoins(
+ denoms,
+ amount,
+ req.type,
+ );
+
+ return {
+ effectiveAmount: Amounts.stringify(result.effective),
+ rawAmount: Amounts.stringify(result.raw),
+ };
+}
+
+const LOG_REFRESH = false;
+const LOG_DEPOSIT = false;
+export function convertDepositAmountForAvailableCoins(
+ denoms: AvailableCoins,
+ amount: AmountJson,
+ mode: TransactionAmountMode,
+): AmountAndRefresh {
+ const zero = Amounts.zeroOfCurrency(amount.currency);
+ if (!denoms.list.length) {
+ // no coins in the database
+ return { effective: zero, raw: zero };
+ }
+ const depositDenoms = rankDenominationForDeposit(denoms.list, mode);
+
+ //FIXME: we are not taking into account
+ // * exchanges with multiple accounts
+ // * wallet with multiple exchanges
+ const wireFee = Object.values(denoms.exchanges)[0]?.wireFee ?? zero;
+ const adjustedAmount = Amounts.add(amount, wireFee).amount;
+
+ const selected = selectGreedyCoins(depositDenoms, adjustedAmount);
+
+ const gap = Amounts.sub(amount, selected.totalValue).amount;
+
+ const result = getTotalEffectiveAndRawForDeposit(
+ selected.coins,
+ amount.currency,
+ );
+ result.raw = Amounts.sub(result.raw, wireFee).amount;
+
+ if (Amounts.isZero(gap)) {
+ // exact amount founds
+ return result;
+ }
+
+ if (LOG_DEPOSIT) {
+ const logInfo = selected.coins.map((c) => {
+ return `${Amounts.stringifyValue(c.info.id)} x ${c.size}`;
+ });
+ console.log(
+ "deposit used:",
+ logInfo.join(", "),
+ "gap:",
+ Amounts.stringifyValue(gap),
+ );
+ }
+
+ const refreshDenoms = rankDenominationForRefresh(denoms.list);
+ /**
+ * FIXME: looking for refresh AFTER selecting greedy is not optimal
+ */
+ const refreshCoin = searchBestRefreshCoin(
+ depositDenoms,
+ refreshDenoms,
+ gap,
+ mode,
+ );
+
+ if (refreshCoin) {
+ const fee = Amounts.sub(result.effective, result.raw).amount;
+ const effective = Amounts.add(
+ result.effective,
+ refreshCoin.refreshEffective,
+ ).amount;
+ const raw = Amounts.sub(effective, fee, refreshCoin.totalFee).amount;
+ //found with change
+ return {
+ effective,
+ raw,
+ refresh: refreshCoin,
+ };
+ }
+
+ // there is a gap, but no refresh coin was found
+ return result;
+}
+
+export async function getMaxDepositAmount(
+ wex: WalletExecutionContext,
+ req: GetAmountRequest,
+): Promise<AmountResponse> {
+ // const filter = getCoinsFilter(req);
+
+ const denoms = await getAvailableDenoms(
+ wex,
+ TransactionType.Deposit,
+ req.currency,
+ {},
+ );
+
+ const result = getMaxDepositAmountForAvailableCoins(denoms, req.currency);
+ return {
+ effectiveAmount: Amounts.stringify(result.effective),
+ rawAmount: Amounts.stringify(result.raw),
+ };
+}
+
+export function getMaxDepositAmountForAvailableCoins(
+ denoms: AvailableCoins,
+ currency: string,
+) {
+ const zero = Amounts.zeroOfCurrency(currency);
+ if (!denoms.list.length) {
+ // no coins in the database
+ return { effective: zero, raw: zero };
+ }
+
+ const result = getTotalEffectiveAndRawForDeposit(
+ denoms.list.map((info) => {
+ return { info, size: info.totalAvailable ?? 0 };
+ }),
+ currency,
+ );
+
+ const wireFee = Object.values(denoms.exchanges)[0]?.wireFee ?? zero;
+ result.raw = Amounts.sub(result.raw, wireFee).amount;
+
+ return result;
+}
+
+export async function convertPeerPushAmount(
+ wex: WalletExecutionContext,
+ req: ConvertAmountRequest,
+): Promise<AmountResponse> {
+ throw Error("to be implemented after 1.0");
+}
+
+export async function getMaxPeerPushAmount(
+ wex: WalletExecutionContext,
+ req: GetAmountRequest,
+): Promise<AmountResponse> {
+ throw Error("to be implemented after 1.0");
+}
+
+export async function convertWithdrawalAmount(
+ wex: WalletExecutionContext,
+ req: ConvertAmountRequest,
+): Promise<AmountResponse> {
+ const amount = Amounts.parseOrThrow(req.amount);
+
+ const denoms = await getAvailableDenoms(
+ wex,
+ TransactionType.Withdrawal,
+ amount.currency,
+ {},
+ );
+
+ const result = convertWithdrawalAmountFromAvailableCoins(
+ denoms,
+ amount,
+ req.type,
+ );
+
+ return {
+ effectiveAmount: Amounts.stringify(result.effective),
+ rawAmount: Amounts.stringify(result.raw),
+ };
+}
+
+export function convertWithdrawalAmountFromAvailableCoins(
+ denoms: AvailableCoins,
+ amount: AmountJson,
+ mode: TransactionAmountMode,
+) {
+ const zero = Amounts.zeroOfCurrency(amount.currency);
+ if (!denoms.list.length) {
+ // no coins in the database
+ return { effective: zero, raw: zero };
+ }
+ const withdrawDenoms = rankDenominationForWithdrawals(denoms.list, mode);
+
+ const selected = selectGreedyCoins(withdrawDenoms, amount);
+
+ return getTotalEffectiveAndRawForWithdrawal(selected.coins, amount.currency);
+}
+
+/** *****************************************************
+ * HELPERS
+ * *****************************************************
+ */
+
+/**
+ *
+ * @param depositDenoms
+ * @param refreshDenoms
+ * @param amount
+ * @param mode
+ * @returns
+ */
+function searchBestRefreshCoin(
+ depositDenoms: SelectableElement[],
+ refreshDenoms: Record<string, SelectableElement[]>,
+ amount: AmountJson,
+ mode: TransactionAmountMode,
+): RefreshChoice | undefined {
+ let choice: RefreshChoice | undefined = undefined;
+ let refreshIdx = 0;
+ refreshIteration: while (refreshIdx < depositDenoms.length) {
+ const d = depositDenoms[refreshIdx];
+
+ const denomContribution =
+ mode === TransactionAmountMode.Effective
+ ? d.value
+ : Amounts.sub(d.value, d.info.denomRefresh, d.info.denomDeposit).amount;
+
+ const changeAfterDeposit = Amounts.sub(denomContribution, amount).amount;
+ if (Amounts.isZero(changeAfterDeposit)) {
+ //this coin is not big enough to use for refresh
+ //since the list is sorted, we can break here
+ break refreshIteration;
+ }
+
+ const withdrawDenoms = refreshDenoms[d.info.exchangeBaseUrl];
+ const change = selectGreedyCoins(withdrawDenoms, changeAfterDeposit);
+
+ const zero = Amounts.zeroOfCurrency(amount.currency);
+ const withdrawChangeFee = change.coins.reduce((cur, prev) => {
+ return Amounts.add(
+ cur,
+ Amounts.mult(prev.info.denomWithdraw, prev.size).amount,
+ ).amount;
+ }, zero);
+
+ const withdrawChangeValue = change.coins.reduce((cur, prev) => {
+ return Amounts.add(cur, Amounts.mult(prev.info.value, prev.size).amount)
+ .amount;
+ }, zero);
+
+ const totalFee = Amounts.add(
+ d.info.denomDeposit,
+ d.info.denomRefresh,
+ withdrawChangeFee,
+ ).amount;
+
+ if (!choice || Amounts.cmp(totalFee, choice.totalFee) === -1) {
+ //found cheaper change
+ choice = {
+ gap: amount,
+ totalFee: totalFee,
+ totalChangeValue: change.totalValue, //change after refresh
+ refreshEffective: Amounts.sub(d.info.value, withdrawChangeValue).amount, // what of the denom used is not recovered
+ selected: d.info,
+ coins: change.coins,
+ };
+ }
+ refreshIdx++;
+ }
+ if (choice) {
+ if (LOG_REFRESH) {
+ const logInfo = choice.coins.map((c) => {
+ return `${Amounts.stringifyValue(c.info.id)} x ${c.size}`;
+ });
+ console.log(
+ "refresh used:",
+ Amounts.stringifyValue(choice.selected.value),
+ "change:",
+ logInfo.join(", "),
+ "fee:",
+ Amounts.stringifyValue(choice.totalFee),
+ "refreshEffective:",
+ Amounts.stringifyValue(choice.refreshEffective),
+ "totalChangeValue:",
+ Amounts.stringifyValue(choice.totalChangeValue),
+ );
+ }
+ }
+ return choice;
+}
+
+/**
+ * Returns a copy of the list sorted for the best denom to withdraw first
+ *
+ * @param denoms
+ * @returns
+ */
+function rankDenominationForWithdrawals(
+ denoms: CoinInfo[],
+ mode: TransactionAmountMode,
+): SelectableElement[] {
+ const copyList = [...denoms];
+ /**
+ * Rank coins
+ */
+ copyList.sort((d1, d2) => {
+ // the best coin to use is
+ // 1.- the one that contrib more and pay less fee
+ // 2.- it takes more time before expires
+
+ //different exchanges may have different wireFee
+ //ranking should take the relative contribution in the exchange
+ //which is (value - denomFee / fixedFee)
+ const rate1 = Amounts.divmod(d1.value, d1.denomWithdraw).quotient;
+ const rate2 = Amounts.divmod(d2.value, d2.denomWithdraw).quotient;
+ const contribCmp = rate1 === rate2 ? 0 : rate1 < rate2 ? 1 : -1;
+ return (
+ contribCmp ||
+ Duration.cmp(d1.duration, d2.duration) ||
+ strcmp(d1.id, d2.id)
+ );
+ });
+
+ return copyList.map((info) => {
+ switch (mode) {
+ case TransactionAmountMode.Effective: {
+ //if the user instructed "effective" then we need to selected
+ //greedy total coin value
+ return {
+ info,
+ value: info.value,
+ total: Number.MAX_SAFE_INTEGER,
+ };
+ }
+ case TransactionAmountMode.Raw: {
+ //if the user instructed "raw" then we need to selected
+ //greedy total coin raw amount (without fee)
+ return {
+ info,
+ value: Amounts.add(info.value, info.denomWithdraw).amount,
+ total: Number.MAX_SAFE_INTEGER,
+ };
+ }
+ }
+ });
+}
+
+/**
+ * Returns a copy of the list sorted for the best denom to deposit first
+ *
+ * @param denoms
+ * @returns
+ */
+function rankDenominationForDeposit(
+ denoms: CoinInfo[],
+ mode: TransactionAmountMode,
+): SelectableElement[] {
+ const copyList = [...denoms];
+ /**
+ * Rank coins
+ */
+ copyList.sort((d1, d2) => {
+ // the best coin to use is
+ // 1.- the one that contrib more and pay less fee
+ // 2.- it takes more time before expires
+
+ //different exchanges may have different wireFee
+ //ranking should take the relative contribution in the exchange
+ //which is (value - denomFee / fixedFee)
+ const rate1 = Amounts.divmod(d1.value, d1.denomDeposit).quotient;
+ const rate2 = Amounts.divmod(d2.value, d2.denomDeposit).quotient;
+ const contribCmp = rate1 === rate2 ? 0 : rate1 < rate2 ? 1 : -1;
+ return (
+ contribCmp ||
+ Duration.cmp(d1.duration, d2.duration) ||
+ strcmp(d1.id, d2.id)
+ );
+ });
+
+ return copyList.map((info) => {
+ switch (mode) {
+ case TransactionAmountMode.Effective: {
+ //if the user instructed "effective" then we need to selected
+ //greedy total coin value
+ return {
+ info,
+ value: info.value,
+ total: info.totalAvailable ?? 0,
+ };
+ }
+ case TransactionAmountMode.Raw: {
+ //if the user instructed "raw" then we need to selected
+ //greedy total coin raw amount (without fee)
+ return {
+ info,
+ value: Amounts.sub(info.value, info.denomDeposit).amount,
+ total: info.totalAvailable ?? 0,
+ };
+ }
+ }
+ });
+}
+
+/**
+ * Returns a copy of the list sorted for the best denom to withdraw first
+ *
+ * @param denoms
+ * @returns
+ */
+function rankDenominationForRefresh(
+ denoms: CoinInfo[],
+): Record<string, SelectableElement[]> {
+ const groupByExchange: Record<string, CoinInfo[]> = {};
+ for (const d of denoms) {
+ if (!groupByExchange[d.exchangeBaseUrl]) {
+ groupByExchange[d.exchangeBaseUrl] = [];
+ }
+ groupByExchange[d.exchangeBaseUrl].push(d);
+ }
+
+ const result: Record<string, SelectableElement[]> = {};
+ for (const d of denoms) {
+ result[d.exchangeBaseUrl] = rankDenominationForWithdrawals(
+ groupByExchange[d.exchangeBaseUrl],
+ TransactionAmountMode.Raw,
+ );
+ }
+ return result;
+}
+
+interface SelectableElement {
+ total: number;
+ value: AmountJson;
+ info: CoinInfo;
+}
+
+function selectGreedyCoins(
+ coins: SelectableElement[],
+ limit: AmountJson,
+): SelectedCoins {
+ const result: SelectedCoins = {
+ totalValue: Amounts.zeroOfCurrency(limit.currency),
+ coins: [],
+ };
+ if (!coins.length) return result;
+
+ let denomIdx = 0;
+ iterateDenoms: while (denomIdx < coins.length) {
+ const denom = coins[denomIdx];
+ // let total = denom.total;
+ const left = Amounts.sub(limit, result.totalValue).amount;
+
+ if (Amounts.isZero(denom.value)) {
+ // 0 contribution denoms should be the last
+ break iterateDenoms;
+ }
+
+ //use Amounts.divmod instead of iterate
+ const div = Amounts.divmod(left, denom.value);
+ const size = Math.min(div.quotient, denom.total);
+ if (size > 0) {
+ const mul = Amounts.mult(denom.value, size).amount;
+ const progress = Amounts.add(result.totalValue, mul).amount;
+
+ result.totalValue = progress;
+ result.coins.push({ info: denom.info, size });
+ denom.total = denom.total - size;
+ }
+
+ //go next denom
+ denomIdx++;
+ }
+
+ return result;
+}
+
+type AmountWithFee = { raw: AmountJson; effective: AmountJson };
+type AmountAndRefresh = AmountWithFee & { refresh?: RefreshChoice };
+
+export function getTotalEffectiveAndRawForDeposit(
+ list: { info: CoinInfo; size: number }[],
+ currency: string,
+): AmountWithFee {
+ const init = {
+ raw: Amounts.zeroOfCurrency(currency),
+ effective: Amounts.zeroOfCurrency(currency),
+ };
+ return list.reduce((prev, cur) => {
+ const ef = Amounts.mult(cur.info.value, cur.size).amount;
+ const rw = Amounts.mult(
+ Amounts.sub(cur.info.value, cur.info.denomDeposit).amount,
+ cur.size,
+ ).amount;
+
+ prev.effective = Amounts.add(prev.effective, ef).amount;
+ prev.raw = Amounts.add(prev.raw, rw).amount;
+ return prev;
+ }, init);
+}
+
+function getTotalEffectiveAndRawForWithdrawal(
+ list: { info: CoinInfo; size: number }[],
+ currency: string,
+): AmountWithFee {
+ const init = {
+ raw: Amounts.zeroOfCurrency(currency),
+ effective: Amounts.zeroOfCurrency(currency),
+ };
+ return list.reduce((prev, cur) => {
+ const ef = Amounts.mult(cur.info.value, cur.size).amount;
+ const rw = Amounts.mult(
+ Amounts.add(cur.info.value, cur.info.denomWithdraw).amount,
+ cur.size,
+ ).amount;
+
+ prev.effective = Amounts.add(prev.effective, ef).amount;
+ prev.raw = Amounts.add(prev.raw, rw).amount;
+ return prev;
+ }, init);
+}
diff --git a/packages/taler-wallet-core/src/internal-wallet-state.ts b/packages/taler-wallet-core/src/internal-wallet-state.ts
deleted file mode 100644
index ebb9cdb9b..000000000
--- a/packages/taler-wallet-core/src/internal-wallet-state.ts
+++ /dev/null
@@ -1,224 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2019 GNUnet e.V.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- * Common interface of the internal wallet state. This object is passed
- * to the various operations (exchange management, withdrawal, refresh, reserve
- * management, etc.).
- *
- * Some operations can be accessed via this state object. This allows mutual
- * recursion between operations, without having cyclic dependencies between
- * the respective TypeScript files.
- *
- * (You can think of this as a "header file" for the wallet implementation.)
- */
-
-/**
- * Imports.
- */
-import {
- WalletNotification,
- BalancesResponse,
- AmountJson,
- DenominationPubKey,
- TalerProtocolTimestamp,
- CancellationToken,
- DenominationInfo,
- RefreshGroupId,
- CoinRefreshRequest,
- RefreshReason,
-} from "@gnu-taler/taler-util";
-import { CryptoDispatcher } from "./crypto/workers/cryptoDispatcher.js";
-import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js";
-import { ExchangeDetailsRecord, ExchangeRecord, WalletStoresV1 } from "./db.js";
-import { PendingOperationsResponse } from "./pending-types.js";
-import { AsyncOpMemoMap, AsyncOpMemoSingle } from "./util/asyncMemo.js";
-import { HttpRequestLibrary } from "./util/http.js";
-import { AsyncCondition } from "./util/promiseUtils.js";
-import {
- DbAccess,
- GetReadOnlyAccess,
- GetReadWriteAccess,
-} from "./util/query.js";
-import { TimerGroup } from "./util/timer.js";
-
-export const EXCHANGE_COINS_LOCK = "exchange-coins-lock";
-export const EXCHANGE_RESERVES_LOCK = "exchange-reserves-lock";
-
-export interface TrustInfo {
- isTrusted: boolean;
- isAudited: boolean;
-}
-
-export interface MerchantInfo {
- protocolVersionCurrent: number;
-}
-
-/**
- * Interface for merchant-related operations.
- */
-export interface MerchantOperations {
- getMerchantInfo(
- ws: InternalWalletState,
- merchantBaseUrl: string,
- ): Promise<MerchantInfo>;
-}
-
-export interface RefreshOperations {
- createRefreshGroup(
- ws: InternalWalletState,
- tx: GetReadWriteAccess<{
- denominations: typeof WalletStoresV1.denominations;
- coins: typeof WalletStoresV1.coins;
- refreshGroups: typeof WalletStoresV1.refreshGroups;
- coinAvailability: typeof WalletStoresV1.coinAvailability;
- }>,
- oldCoinPubs: CoinRefreshRequest[],
- reason: RefreshReason,
- ): Promise<RefreshGroupId>;
-}
-
-/**
- * Interface for exchange-related operations.
- */
-export interface ExchangeOperations {
- // FIXME: Should other operations maybe always use
- // updateExchangeFromUrl?
- getExchangeDetails(
- tx: GetReadOnlyAccess<{
- exchanges: typeof WalletStoresV1.exchanges;
- exchangeDetails: typeof WalletStoresV1.exchangeDetails;
- }>,
- exchangeBaseUrl: string,
- ): Promise<ExchangeDetailsRecord | undefined>;
- getExchangeTrust(
- ws: InternalWalletState,
- exchangeInfo: ExchangeRecord,
- ): Promise<TrustInfo>;
- updateExchangeFromUrl(
- ws: InternalWalletState,
- baseUrl: string,
- options?: {
- forceNow?: boolean;
- cancellationToken?: CancellationToken;
- },
- ): Promise<{
- exchange: ExchangeRecord;
- exchangeDetails: ExchangeDetailsRecord;
- }>;
-}
-
-export interface RecoupOperations {
- createRecoupGroup(
- ws: InternalWalletState,
- tx: GetReadWriteAccess<{
- recoupGroups: typeof WalletStoresV1.recoupGroups;
- denominations: typeof WalletStoresV1.denominations;
- refreshGroups: typeof WalletStoresV1.refreshGroups;
- coins: typeof WalletStoresV1.coins;
- }>,
- exchangeBaseUrl: string,
- coinPubs: string[],
- ): Promise<string>;
- processRecoupGroup(
- ws: InternalWalletState,
- recoupGroupId: string,
- options?: {
- forceNow?: boolean;
- },
- ): Promise<void>;
-}
-
-export type NotificationListener = (n: WalletNotification) => void;
-
-export interface ActiveLongpollInfo {
- [opId: string]: {
- cancel: () => void;
- };
-}
-
-/**
- * Internal, shard wallet state that is used by the implementation
- * of wallet operations.
- *
- * FIXME: This should not be exported anywhere from the taler-wallet-core package,
- * as it's an opaque implementation detail.
- */
-export interface InternalWalletState {
- /**
- * Active longpoll operations.
- */
- activeLongpoll: ActiveLongpollInfo;
-
- cryptoApi: TalerCryptoInterface;
-
- timerGroup: TimerGroup;
- stopped: boolean;
-
- insecureTrustExchange: boolean;
-
- batchWithdrawal: boolean;
-
- /**
- * Asynchronous condition to interrupt the sleep of the
- * retry loop.
- *
- * Used to allow processing of new work faster.
- */
- latch: AsyncCondition;
-
- listeners: NotificationListener[];
-
- initCalled: boolean;
-
- merchantInfoCache: Record<string, MerchantInfo>;
-
- exchangeOps: ExchangeOperations;
- recoupOps: RecoupOperations;
- merchantOps: MerchantOperations;
- refreshOps: RefreshOperations;
-
- devModeActive: boolean;
-
- getDenomInfo(
- ws: InternalWalletState,
- tx: GetReadOnlyAccess<{
- denominations: typeof WalletStoresV1.denominations;
- }>,
- exchangeBaseUrl: string,
- denomPubHash: string,
- ): Promise<DenominationInfo | undefined>;
-
- db: DbAccess<typeof WalletStoresV1>;
- http: HttpRequestLibrary;
-
- notify(n: WalletNotification): void;
-
- addNotificationListener(f: (n: WalletNotification) => void): void;
-
- /**
- * Stop ongoing processing.
- */
- stop(): void;
-
- /**
- * Run an async function after acquiring a list of locks, identified
- * by string tokens.
- */
- runSequentialized<T>(tokens: string[], f: () => Promise<T>): Promise<T>;
-
- runUntilDone(req?: { maxRetries?: number }): Promise<void>;
-}
diff --git a/packages/taler-wallet-core/src/observable-wrappers.ts b/packages/taler-wallet-core/src/observable-wrappers.ts
new file mode 100644
index 000000000..f09498d95
--- /dev/null
+++ b/packages/taler-wallet-core/src/observable-wrappers.ts
@@ -0,0 +1,291 @@
+/*
+ This file is part of GNU Taler
+ (C) 2024 Taler Systems SA
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * @fileoverview Wrappers/proxies to make various interfaces observable.
+ */
+
+/**
+ * Imports.
+ */
+import { IDBDatabase } from "@gnu-taler/idb-bridge";
+import {
+ ObservabilityContext,
+ ObservabilityEventType,
+} from "@gnu-taler/taler-util";
+import { TaskIdStr } from "./common.js";
+import { TalerCryptoInterface } from "./index.js";
+import {
+ DbAccess,
+ DbReadOnlyTransaction,
+ DbReadWriteTransaction,
+ StoreNames,
+} from "./query.js";
+import { TaskScheduler } from "./shepherd.js";
+
+/**
+ * Task scheduler with extra observability events.
+ */
+export class ObservableTaskScheduler implements TaskScheduler {
+ constructor(
+ private impl: TaskScheduler,
+ private oc: ObservabilityContext,
+ ) {}
+
+ private taskDepCache = new Set<string>();
+
+ private declareDep(taskId: TaskIdStr): void {
+ if (this.taskDepCache.size > 500) {
+ this.taskDepCache.clear();
+ }
+ if (!this.taskDepCache.has(taskId)) {
+ this.taskDepCache.add(taskId);
+ this.oc.observe({
+ type: ObservabilityEventType.DeclareTaskDependency,
+ taskId,
+ });
+ }
+ }
+
+ getActiveTasks(): TaskIdStr[] {
+ return this.impl.getActiveTasks();
+ }
+
+ isIdle(): boolean {
+ return this.impl.isIdle();
+ }
+
+ ensureRunning(): Promise<void> {
+ return this.impl.ensureRunning();
+ }
+
+ startShepherdTask(taskId: TaskIdStr): void {
+ this.declareDep(taskId);
+ this.oc.observe({
+ type: ObservabilityEventType.TaskStart,
+ taskId,
+ });
+ return this.impl.startShepherdTask(taskId);
+ }
+
+ stopShepherdTask(taskId: TaskIdStr): void {
+ this.declareDep(taskId);
+ this.oc.observe({
+ type: ObservabilityEventType.TaskStop,
+ taskId,
+ });
+ return this.impl.stopShepherdTask(taskId);
+ }
+
+ resetTaskRetries(taskId: TaskIdStr): Promise<void> {
+ this.declareDep(taskId);
+ if (this.taskDepCache.size > 500) {
+ this.taskDepCache.clear();
+ }
+ this.oc.observe({
+ type: ObservabilityEventType.TaskReset,
+ taskId,
+ });
+ return this.impl.resetTaskRetries(taskId);
+ }
+
+ async reload(): Promise<void> {
+ return this.impl.reload();
+ }
+}
+
+const locRegex = /\s*at\s*([a-zA-Z0-9_.!]*)\s*/;
+
+export function getCallerInfo(up: number = 2): string {
+ const stack = new Error().stack ?? "";
+ const identifies: string[] = [];
+ for (const line of stack.split("\n")) {
+ let l = line.match(locRegex);
+ if (l) {
+ identifies.push(l[1]);
+ }
+ }
+ return identifies.slice(up, up + 2).join("/");
+}
+
+export class ObservableDbAccess<StoreMap> implements DbAccess<StoreMap> {
+ constructor(
+ private impl: DbAccess<StoreMap>,
+ private oc: ObservabilityContext,
+ ) {}
+ idbHandle(): IDBDatabase {
+ return this.impl.idbHandle();
+ }
+
+ async runAllStoresReadWriteTx<T>(
+ options: {
+ label?: string;
+ },
+ txf: (
+ tx: DbReadWriteTransaction<StoreMap, StoreNames<StoreMap>[]>,
+ ) => Promise<T>,
+ ): Promise<T> {
+ const location = getCallerInfo();
+ this.oc.observe({
+ type: ObservabilityEventType.DbQueryStart,
+ name: "<unknown>",
+ location,
+ });
+ try {
+ const ret = await this.impl.runAllStoresReadWriteTx(options, txf);
+ this.oc.observe({
+ type: ObservabilityEventType.DbQueryFinishSuccess,
+ name: "<unknown>",
+ location,
+ });
+ return ret;
+ } catch (e) {
+ this.oc.observe({
+ type: ObservabilityEventType.DbQueryFinishError,
+ name: "<unknown>",
+ location,
+ });
+ throw e;
+ }
+ }
+
+ async runAllStoresReadOnlyTx<T>(
+ options: {
+ label?: string;
+ },
+ txf: (
+ tx: DbReadOnlyTransaction<StoreMap, StoreNames<StoreMap>[]>,
+ ) => Promise<T>,
+ ): Promise<T> {
+ const location = getCallerInfo();
+ this.oc.observe({
+ type: ObservabilityEventType.DbQueryStart,
+ name: options.label ?? "<unknown>",
+ location,
+ });
+ try {
+ const ret = await this.impl.runAllStoresReadOnlyTx(options, txf);
+ this.oc.observe({
+ type: ObservabilityEventType.DbQueryFinishSuccess,
+ name: options.label ?? "<unknown>",
+ location,
+ });
+ return ret;
+ } catch (e) {
+ this.oc.observe({
+ type: ObservabilityEventType.DbQueryFinishError,
+ name: options.label ?? "<unknown>",
+ location,
+ });
+ throw e;
+ }
+ }
+
+ async runReadWriteTx<T, StoreNameArray extends StoreNames<StoreMap>[]>(
+ opts: {
+ storeNames: StoreNameArray;
+ label?: string;
+ },
+ txf: (tx: DbReadWriteTransaction<StoreMap, StoreNameArray>) => Promise<T>,
+ ): Promise<T> {
+ const location = getCallerInfo();
+ this.oc.observe({
+ type: ObservabilityEventType.DbQueryStart,
+ name: opts.label ?? "<unknown>",
+ location,
+ });
+ try {
+ const ret = await this.impl.runReadWriteTx(opts, txf);
+ this.oc.observe({
+ type: ObservabilityEventType.DbQueryFinishSuccess,
+ name: opts.label ?? "<unknown>",
+ location,
+ });
+ return ret;
+ } catch (e) {
+ this.oc.observe({
+ type: ObservabilityEventType.DbQueryFinishError,
+ name: opts.label ?? "<unknown>",
+ location,
+ });
+ throw e;
+ }
+ }
+
+ async runReadOnlyTx<T, StoreNameArray extends StoreNames<StoreMap>[]>(
+ opts: {
+ storeNames: StoreNameArray;
+ label?: string;
+ },
+ txf: (tx: DbReadOnlyTransaction<StoreMap, StoreNameArray>) => Promise<T>,
+ ): Promise<T> {
+ const location = getCallerInfo();
+ try {
+ this.oc.observe({
+ type: ObservabilityEventType.DbQueryStart,
+ name: opts.label ?? "<unknown>",
+ location,
+ });
+ const ret = await this.impl.runReadOnlyTx(opts, txf);
+ this.oc.observe({
+ type: ObservabilityEventType.DbQueryFinishSuccess,
+ name: opts.label ?? "<unknown>",
+ location,
+ });
+ return ret;
+ } catch (e) {
+ this.oc.observe({
+ type: ObservabilityEventType.DbQueryFinishError,
+ name: opts.label ?? "<unknown>",
+ location,
+ });
+ throw e;
+ }
+ }
+}
+
+export function observeTalerCrypto(
+ impl: TalerCryptoInterface,
+ oc: ObservabilityContext,
+): TalerCryptoInterface {
+ return Object.fromEntries(
+ Object.keys(impl).map((name) => {
+ return [
+ name,
+ async (req: any) => {
+ oc.observe({
+ type: ObservabilityEventType.CryptoStart,
+ operation: name,
+ });
+ try {
+ const res = await (impl as any)[name](req);
+ oc.observe({
+ type: ObservabilityEventType.CryptoFinishSuccess,
+ operation: name,
+ });
+ return res;
+ } catch (e) {
+ oc.observe({
+ type: ObservabilityEventType.CryptoFinishError,
+ operation: name,
+ });
+ throw e;
+ }
+ },
+ ];
+ }),
+ ) as any;
+}
diff --git a/packages/taler-wallet-core/src/operations/README.md b/packages/taler-wallet-core/src/operations/README.md
deleted file mode 100644
index a40349d37..000000000
--- a/packages/taler-wallet-core/src/operations/README.md
+++ /dev/null
@@ -1,7 +0,0 @@
-# Wallet Operations
-
-This folder contains the implementations for all wallet operations that operate on the wallet state.
-
-To avoid cyclic dependencies, these files must **not** reference each other. Instead, other operations should only be accessed via injected dependencies.
-
-Avoiding cyclic dependencies is important for module bundlers.
diff --git a/packages/taler-wallet-core/src/operations/backup/export.ts b/packages/taler-wallet-core/src/operations/backup/export.ts
deleted file mode 100644
index 965d51776..000000000
--- a/packages/taler-wallet-core/src/operations/backup/export.ts
+++ /dev/null
@@ -1,580 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2020 Taler Systems SA
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- * Implementation of wallet backups (export/import/upload) and sync
- * server management.
- *
- * @author Florian Dold <dold@taler.net>
- */
-
-/**
- * Imports.
- */
-import {
- AbsoluteTime,
- Amounts,
- BackupBackupProvider,
- BackupBackupProviderTerms,
- BackupCoin,
- BackupCoinSource,
- BackupCoinSourceType,
- BackupDenomination,
- BackupExchange,
- BackupExchangeDetails,
- BackupExchangeSignKey,
- BackupExchangeWireFee,
- BackupOperationStatus,
- BackupPayInfo,
- BackupProposalStatus,
- BackupPurchase,
- BackupRecoupGroup,
- BackupRefreshGroup,
- BackupRefreshOldCoin,
- BackupRefreshSession,
- BackupRefundItem,
- BackupRefundState,
- BackupTip,
- BackupWgInfo,
- BackupWgType,
- BackupWithdrawalGroup,
- BACKUP_VERSION_MAJOR,
- BACKUP_VERSION_MINOR,
- canonicalizeBaseUrl,
- canonicalJson,
- CoinStatus,
- encodeCrock,
- getRandomBytes,
- hash,
- Logger,
- stringToBytes,
- WalletBackupContentV1,
-} from "@gnu-taler/taler-util";
-import {
- CoinSourceType,
- ConfigRecordKey,
- DenominationRecord,
- PurchaseStatus,
- RefreshCoinStatus,
- RefundState,
- WithdrawalGroupStatus,
- WithdrawalRecordType,
-} from "../../db.js";
-import { InternalWalletState } from "../../internal-wallet-state.js";
-import { assertUnreachable } from "../../util/assertUnreachable.js";
-import { checkDbInvariant } from "../../util/invariants.js";
-import { getWalletBackupState, provideBackupState } from "./state.js";
-
-const logger = new Logger("backup/export.ts");
-
-export async function exportBackup(
- ws: InternalWalletState,
-): Promise<WalletBackupContentV1> {
- await provideBackupState(ws);
- return ws.db
- .mktx((x) => [
- x.config,
- x.exchanges,
- x.exchangeDetails,
- x.exchangeSignkeys,
- x.coins,
- x.contractTerms,
- x.denominations,
- x.purchases,
- x.refreshGroups,
- x.backupProviders,
- x.tips,
- x.recoupGroups,
- x.withdrawalGroups,
- ])
- .runReadWrite(async (tx) => {
- const bs = await getWalletBackupState(ws, tx);
-
- const backupExchangeDetails: BackupExchangeDetails[] = [];
- const backupExchanges: BackupExchange[] = [];
- const backupCoinsByDenom: { [dph: string]: BackupCoin[] } = {};
- const backupDenominationsByExchange: {
- [url: string]: BackupDenomination[];
- } = {};
- const backupPurchases: BackupPurchase[] = [];
- const backupRefreshGroups: BackupRefreshGroup[] = [];
- const backupBackupProviders: BackupBackupProvider[] = [];
- const backupTips: BackupTip[] = [];
- const backupRecoupGroups: BackupRecoupGroup[] = [];
- const backupWithdrawalGroups: BackupWithdrawalGroup[] = [];
-
- await tx.withdrawalGroups.iter().forEachAsync(async (wg) => {
- let info: BackupWgInfo;
- switch (wg.wgInfo.withdrawalType) {
- case WithdrawalRecordType.BankIntegrated:
- info = {
- type: BackupWgType.BankIntegrated,
- exchange_payto_uri: wg.wgInfo.bankInfo.exchangePaytoUri,
- taler_withdraw_uri: wg.wgInfo.bankInfo.talerWithdrawUri,
- confirm_url: wg.wgInfo.bankInfo.confirmUrl,
- timestamp_bank_confirmed:
- wg.wgInfo.bankInfo.timestampBankConfirmed,
- timestamp_reserve_info_posted:
- wg.wgInfo.bankInfo.timestampReserveInfoPosted,
- };
- break;
- case WithdrawalRecordType.BankManual:
- info = {
- type: BackupWgType.BankManual,
- };
- break;
- case WithdrawalRecordType.PeerPullCredit:
- info = {
- type: BackupWgType.PeerPullCredit,
- contract_priv: wg.wgInfo.contractPriv,
- contract_terms: wg.wgInfo.contractTerms,
- };
- break;
- case WithdrawalRecordType.PeerPushCredit:
- info = {
- type: BackupWgType.PeerPushCredit,
- contract_terms: wg.wgInfo.contractTerms,
- };
- break;
- case WithdrawalRecordType.Recoup:
- info = {
- type: BackupWgType.Recoup,
- };
- break;
- default:
- assertUnreachable(wg.wgInfo);
- }
- backupWithdrawalGroups.push({
- raw_withdrawal_amount: Amounts.stringify(wg.rawWithdrawalAmount),
- info,
- timestamp_created: wg.timestampStart,
- timestamp_finish: wg.timestampFinish,
- withdrawal_group_id: wg.withdrawalGroupId,
- secret_seed: wg.secretSeed,
- exchange_base_url: wg.exchangeBaseUrl,
- instructed_amount: Amounts.stringify(wg.instructedAmount),
- effective_withdrawal_amount: Amounts.stringify(
- wg.effectiveWithdrawalAmount,
- ),
- reserve_priv: wg.reservePriv,
- restrict_age: wg.restrictAge,
- // FIXME: proper status conversion!
- operation_status:
- wg.status == WithdrawalGroupStatus.Finished
- ? BackupOperationStatus.Finished
- : BackupOperationStatus.Pending,
- selected_denoms_uid: wg.denomSelUid,
- selected_denoms: wg.denomsSel.selectedDenoms.map((x) => ({
- count: x.count,
- denom_pub_hash: x.denomPubHash,
- })),
- });
- });
-
- await tx.tips.iter().forEach((tip) => {
- backupTips.push({
- exchange_base_url: tip.exchangeBaseUrl,
- merchant_base_url: tip.merchantBaseUrl,
- merchant_tip_id: tip.merchantTipId,
- wallet_tip_id: tip.walletTipId,
- secret_seed: tip.secretSeed,
- selected_denoms: tip.denomsSel.selectedDenoms.map((x) => ({
- count: x.count,
- denom_pub_hash: x.denomPubHash,
- })),
- timestamp_finished: tip.pickedUpTimestamp,
- timestamp_accepted: tip.acceptedTimestamp,
- timestamp_created: tip.createdTimestamp,
- timestamp_expiration: tip.tipExpiration,
- tip_amount_raw: Amounts.stringify(tip.tipAmountRaw),
- selected_denoms_uid: tip.denomSelUid,
- });
- });
-
- await tx.recoupGroups.iter().forEach((recoupGroup) => {
- backupRecoupGroups.push({
- recoup_group_id: recoupGroup.recoupGroupId,
- timestamp_created: recoupGroup.timestampStarted,
- timestamp_finish: recoupGroup.timestampFinished,
- coins: recoupGroup.coinPubs.map((x, i) => ({
- coin_pub: x,
- recoup_finished: recoupGroup.recoupFinishedPerCoin[i],
- })),
- });
- });
-
- await tx.backupProviders.iter().forEach((bp) => {
- let terms: BackupBackupProviderTerms | undefined;
- if (bp.terms) {
- terms = {
- annual_fee: Amounts.stringify(bp.terms.annualFee),
- storage_limit_in_megabytes: bp.terms.storageLimitInMegabytes,
- supported_protocol_version: bp.terms.supportedProtocolVersion,
- };
- }
- backupBackupProviders.push({
- terms,
- base_url: canonicalizeBaseUrl(bp.baseUrl),
- pay_proposal_ids: bp.paymentProposalIds,
- uids: bp.uids,
- });
- });
-
- await tx.coins.iter().forEach((coin) => {
- let bcs: BackupCoinSource;
- switch (coin.coinSource.type) {
- case CoinSourceType.Refresh:
- bcs = {
- type: BackupCoinSourceType.Refresh,
- old_coin_pub: coin.coinSource.oldCoinPub,
- refresh_group_id: coin.coinSource.refreshGroupId,
- };
- break;
- case CoinSourceType.Tip:
- bcs = {
- type: BackupCoinSourceType.Tip,
- coin_index: coin.coinSource.coinIndex,
- wallet_tip_id: coin.coinSource.walletTipId,
- };
- break;
- case CoinSourceType.Withdraw:
- bcs = {
- type: BackupCoinSourceType.Withdraw,
- coin_index: coin.coinSource.coinIndex,
- reserve_pub: coin.coinSource.reservePub,
- withdrawal_group_id: coin.coinSource.withdrawalGroupId,
- };
- break;
- }
-
- const coins = (backupCoinsByDenom[coin.denomPubHash] ??= []);
- coins.push({
- blinding_key: coin.blindingKey,
- coin_priv: coin.coinPriv,
- coin_source: bcs,
- fresh: coin.status === CoinStatus.Fresh,
- spend_allocation: coin.spendAllocation
- ? {
- amount: coin.spendAllocation.amount,
- id: coin.spendAllocation.id,
- }
- : undefined,
- denom_sig: coin.denomSig,
- });
- });
-
- await tx.denominations.iter().forEach((denom) => {
- const backupDenoms = (backupDenominationsByExchange[
- denom.exchangeBaseUrl
- ] ??= []);
- backupDenoms.push({
- coins: backupCoinsByDenom[denom.denomPubHash] ?? [],
- denom_pub: denom.denomPub,
- fee_deposit: Amounts.stringify(denom.fees.feeDeposit),
- fee_refresh: Amounts.stringify(denom.fees.feeRefresh),
- fee_refund: Amounts.stringify(denom.fees.feeRefund),
- fee_withdraw: Amounts.stringify(denom.fees.feeWithdraw),
- is_offered: denom.isOffered,
- is_revoked: denom.isRevoked,
- master_sig: denom.masterSig,
- stamp_expire_deposit: denom.stampExpireDeposit,
- stamp_expire_legal: denom.stampExpireLegal,
- stamp_expire_withdraw: denom.stampExpireWithdraw,
- stamp_start: denom.stampStart,
- value: Amounts.stringify(DenominationRecord.getValue(denom)),
- list_issue_date: denom.listIssueDate,
- });
- });
-
- await tx.exchanges.iter().forEachAsync(async (ex) => {
- const dp = ex.detailsPointer;
- if (!dp) {
- return;
- }
- backupExchanges.push({
- base_url: ex.baseUrl,
- currency: dp.currency,
- master_public_key: dp.masterPublicKey,
- update_clock: dp.updateClock,
- });
- });
-
- await tx.exchangeDetails.iter().forEachAsync(async (ex) => {
- // Only back up permanently added exchanges.
-
- const wi = ex.wireInfo;
- const wireFees: BackupExchangeWireFee[] = [];
-
- Object.keys(wi.feesForType).forEach((x) => {
- for (const f of wi.feesForType[x]) {
- wireFees.push({
- wire_type: x,
- closing_fee: Amounts.stringify(f.closingFee),
- end_stamp: f.endStamp,
- sig: f.sig,
- start_stamp: f.startStamp,
- wire_fee: Amounts.stringify(f.wireFee),
- });
- }
- });
- checkDbInvariant(ex.rowId != null);
- const exchangeSk =
- await tx.exchangeSignKeys.indexes.byExchangeDetailsRowId.getAll(
- ex.rowId,
- );
- let signingKeys: BackupExchangeSignKey[] = exchangeSk.map((x) => ({
- key: x.signkeyPub,
- master_sig: x.masterSig,
- stamp_end: x.stampEnd,
- stamp_expire: x.stampExpire,
- stamp_start: x.stampStart,
- }));
-
- backupExchangeDetails.push({
- base_url: ex.exchangeBaseUrl,
- reserve_closing_delay: ex.reserveClosingDelay,
- accounts: ex.wireInfo.accounts.map((x) => ({
- payto_uri: x.payto_uri,
- master_sig: x.master_sig,
- })),
- auditors: ex.auditors.map((x) => ({
- auditor_pub: x.auditor_pub,
- auditor_url: x.auditor_url,
- denomination_keys: x.denomination_keys,
- })),
- master_public_key: ex.masterPublicKey,
- currency: ex.currency,
- protocol_version: ex.protocolVersionRange,
- wire_fees: wireFees,
- signing_keys: signingKeys,
- global_fees: ex.globalFees.map((x) => ({
- accountFee: Amounts.stringify(x.accountFee),
- historyFee: Amounts.stringify(x.historyFee),
- purseFee: Amounts.stringify(x.purseFee),
- endDate: x.endDate,
- historyTimeout: x.historyTimeout,
- signature: x.signature,
- purseLimit: x.purseLimit,
- purseTimeout: x.purseTimeout,
- startDate: x.startDate,
- })),
- tos_accepted_etag: ex.tosAccepted?.etag,
- tos_accepted_timestamp: ex.tosAccepted?.timestamp,
- denominations:
- backupDenominationsByExchange[ex.exchangeBaseUrl] ?? [],
- });
- });
-
- const purchaseProposalIdSet = new Set<string>();
-
- await tx.purchases.iter().forEachAsync(async (purch) => {
- const refunds: BackupRefundItem[] = [];
- purchaseProposalIdSet.add(purch.proposalId);
- for (const refundKey of Object.keys(purch.refunds)) {
- const ri = purch.refunds[refundKey];
- const common = {
- coin_pub: ri.coinPub,
- execution_time: ri.executionTime,
- obtained_time: ri.obtainedTime,
- refund_amount: Amounts.stringify(ri.refundAmount),
- rtransaction_id: ri.rtransactionId,
- total_refresh_cost_bound: Amounts.stringify(
- ri.totalRefreshCostBound,
- ),
- };
- switch (ri.type) {
- case RefundState.Applied:
- refunds.push({ type: BackupRefundState.Applied, ...common });
- break;
- case RefundState.Failed:
- refunds.push({ type: BackupRefundState.Failed, ...common });
- break;
- case RefundState.Pending:
- refunds.push({ type: BackupRefundState.Pending, ...common });
- break;
- }
- }
-
- let propStatus: BackupProposalStatus;
- switch (purch.purchaseStatus) {
- case PurchaseStatus.Paid:
- case PurchaseStatus.QueryingAutoRefund:
- case PurchaseStatus.QueryingRefund:
- propStatus = BackupProposalStatus.Paid;
- break;
- case PurchaseStatus.PayingReplay:
- case PurchaseStatus.DownloadingProposal:
- case PurchaseStatus.Proposed:
- case PurchaseStatus.Paying:
- propStatus = BackupProposalStatus.Proposed;
- break;
- case PurchaseStatus.ProposalDownloadFailed:
- case PurchaseStatus.PaymentAbortFinished:
- propStatus = BackupProposalStatus.PermanentlyFailed;
- break;
- case PurchaseStatus.AbortingWithRefund:
- case PurchaseStatus.ProposalRefused:
- propStatus = BackupProposalStatus.Refused;
- break;
- case PurchaseStatus.RepurchaseDetected:
- propStatus = BackupProposalStatus.Repurchase;
- break;
- default: {
- const error: never = purch.purchaseStatus;
- throw Error(`purchase status ${error} is not handled`);
- }
- }
-
- const payInfo = purch.payInfo;
- let backupPayInfo: BackupPayInfo | undefined = undefined;
- if (payInfo) {
- backupPayInfo = {
- pay_coins: payInfo.payCoinSelection.coinPubs.map((x, i) => ({
- coin_pub: x,
- contribution: Amounts.stringify(
- payInfo.payCoinSelection.coinContributions[i],
- ),
- })),
- total_pay_cost: Amounts.stringify(payInfo.totalPayCost),
- pay_coins_uid: payInfo.payCoinSelectionUid,
- };
- }
-
- let contractTermsRaw = undefined;
- if (purch.download) {
- const contractTermsRecord = await tx.contractTerms.get(
- purch.download.contractTermsHash,
- );
- if (contractTermsRecord) {
- contractTermsRaw = contractTermsRecord.contractTermsRaw;
- }
- }
-
- backupPurchases.push({
- contract_terms_raw: contractTermsRaw,
- auto_refund_deadline: purch.autoRefundDeadline,
- merchant_pay_sig: purch.merchantPaySig,
- pay_info: backupPayInfo,
- proposal_id: purch.proposalId,
- refunds,
- timestamp_accepted: purch.timestampAccept,
- timestamp_first_successful_pay: purch.timestampFirstSuccessfulPay,
- nonce_priv: purch.noncePriv,
- merchant_sig: purch.download?.contractTermsMerchantSig,
- claim_token: purch.claimToken,
- merchant_base_url: purch.merchantBaseUrl,
- order_id: purch.orderId,
- proposal_status: propStatus,
- repurchase_proposal_id: purch.repurchaseProposalId,
- download_session_id: purch.downloadSessionId,
- timestamp_proposed: purch.timestamp,
- });
- });
-
- await tx.refreshGroups.iter().forEach((rg) => {
- const oldCoins: BackupRefreshOldCoin[] = [];
-
- for (let i = 0; i < rg.oldCoinPubs.length; i++) {
- let refreshSession: BackupRefreshSession | undefined;
- const s = rg.refreshSessionPerCoin[i];
- if (s) {
- refreshSession = {
- new_denoms: s.newDenoms.map((x) => ({
- count: x.count,
- denom_pub_hash: x.denomPubHash,
- })),
- session_secret_seed: s.sessionSecretSeed,
- noreveal_index: s.norevealIndex,
- };
- }
- oldCoins.push({
- coin_pub: rg.oldCoinPubs[i],
- estimated_output_amount: Amounts.stringify(
- rg.estimatedOutputPerCoin[i],
- ),
- finished: rg.statusPerCoin[i] === RefreshCoinStatus.Finished,
- input_amount: Amounts.stringify(rg.inputPerCoin[i]),
- refresh_session: refreshSession,
- });
- }
-
- backupRefreshGroups.push({
- reason: rg.reason as any,
- refresh_group_id: rg.refreshGroupId,
- timestamp_created: rg.timestampCreated,
- timestamp_finish: rg.timestampFinished,
- old_coins: oldCoins,
- });
- });
-
- const ts = AbsoluteTime.toTimestamp(AbsoluteTime.now());
-
- if (!bs.lastBackupTimestamp) {
- bs.lastBackupTimestamp = ts;
- }
-
- const backupBlob: WalletBackupContentV1 = {
- schema_id: "gnu-taler-wallet-backup-content",
- schema_version: BACKUP_VERSION_MAJOR,
- minor_version: BACKUP_VERSION_MINOR,
- exchanges: backupExchanges,
- exchange_details: backupExchangeDetails,
- wallet_root_pub: bs.walletRootPub,
- backup_providers: backupBackupProviders,
- current_device_id: bs.deviceId,
- purchases: backupPurchases,
- recoup_groups: backupRecoupGroups,
- refresh_groups: backupRefreshGroups,
- tips: backupTips,
- timestamp: bs.lastBackupTimestamp,
- trusted_auditors: {},
- trusted_exchanges: {},
- intern_table: {},
- error_reports: [],
- tombstones: [],
- // FIXME!
- withdrawal_groups: backupWithdrawalGroups,
- };
-
- // If the backup changed, we change our nonce and timestamp.
-
- let h = encodeCrock(hash(stringToBytes(canonicalJson(backupBlob))));
- if (h !== bs.lastBackupPlainHash) {
- logger.trace(
- `plain backup hash changed (from ${bs.lastBackupPlainHash}to ${h})`,
- );
- bs.lastBackupTimestamp = ts;
- backupBlob.timestamp = ts;
- bs.lastBackupPlainHash = encodeCrock(
- hash(stringToBytes(canonicalJson(backupBlob))),
- );
- bs.lastBackupNonce = encodeCrock(getRandomBytes(32));
- logger.trace(
- `setting timestamp to ${AbsoluteTime.toIsoString(
- AbsoluteTime.fromTimestamp(ts),
- )} and nonce to ${bs.lastBackupNonce}`,
- );
- await tx.config.put({
- key: ConfigRecordKey.WalletBackupState,
- value: bs,
- });
- } else {
- logger.trace("backup hash did not change");
- }
-
- return backupBlob;
- });
-}
diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts b/packages/taler-wallet-core/src/operations/backup/import.ts
deleted file mode 100644
index 5fd220113..000000000
--- a/packages/taler-wallet-core/src/operations/backup/import.ts
+++ /dev/null
@@ -1,863 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2020 Taler Systems SA
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received 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 {
- AgeRestriction,
- AmountJson,
- Amounts,
- BackupCoin,
- BackupCoinSourceType,
- BackupDenomSel,
- BackupPayInfo,
- BackupProposalStatus,
- BackupRefreshReason,
- BackupRefundState,
- BackupWgType,
- codecForMerchantContractTerms,
- CoinStatus,
- DenomKeyType,
- DenomSelectionState,
- j2s,
- Logger,
- PayCoinSelection,
- RefreshReason,
- TalerProtocolTimestamp,
- WalletBackupContentV1,
- WireInfo,
-} from "@gnu-taler/taler-util";
-import {
- CoinRecord,
- CoinSource,
- CoinSourceType,
- DenominationRecord,
- DenominationVerificationStatus,
- ProposalDownloadInfo,
- PurchaseStatus,
- PurchasePayInfo,
- RefreshCoinStatus,
- RefreshSessionRecord,
- RefundState,
- WalletContractData,
- WalletRefundItem,
- WalletStoresV1,
- WgInfo,
- WithdrawalGroupStatus,
- WithdrawalRecordType,
- RefreshOperationStatus,
-} from "../../db.js";
-import { InternalWalletState } from "../../internal-wallet-state.js";
-import { assertUnreachable } from "../../util/assertUnreachable.js";
-import { checkLogicInvariant } from "../../util/invariants.js";
-import { GetReadOnlyAccess, GetReadWriteAccess } from "../../util/query.js";
-import {
- makeCoinAvailable,
- makeTombstoneId,
- makeTransactionId,
- TombstoneTag,
-} from "../common.js";
-import { getExchangeDetails } from "../exchanges.js";
-import { extractContractData } from "../pay-merchant.js";
-import { provideBackupState } from "./state.js";
-
-const logger = new Logger("operations/backup/import.ts");
-
-function checkBackupInvariant(b: boolean, m?: string): asserts b {
- if (!b) {
- if (m) {
- throw Error(`BUG: backup invariant failed (${m})`);
- } else {
- throw Error("BUG: backup invariant failed");
- }
- }
-}
-
-/**
- * Re-compute information about the coin selection for a payment.
- */
-async function recoverPayCoinSelection(
- tx: GetReadWriteAccess<{
- exchanges: typeof WalletStoresV1.exchanges;
- exchangeDetails: typeof WalletStoresV1.exchangeDetails;
- coins: typeof WalletStoresV1.coins;
- denominations: typeof WalletStoresV1.denominations;
- }>,
- contractData: WalletContractData,
- payInfo: BackupPayInfo,
-): Promise<PayCoinSelection> {
- const coinPubs: string[] = payInfo.pay_coins.map((x) => x.coin_pub);
- const coinContributions: AmountJson[] = payInfo.pay_coins.map((x) =>
- Amounts.parseOrThrow(x.contribution),
- );
-
- const coveredExchanges: Set<string> = new Set();
-
- let totalWireFee: AmountJson = Amounts.zeroOfAmount(contractData.amount);
- let totalDepositFees: AmountJson = Amounts.zeroOfAmount(contractData.amount);
-
- for (const coinPub of coinPubs) {
- const coinRecord = await tx.coins.get(coinPub);
- checkBackupInvariant(!!coinRecord);
- const denom = await tx.denominations.get([
- coinRecord.exchangeBaseUrl,
- coinRecord.denomPubHash,
- ]);
- checkBackupInvariant(!!denom);
- totalDepositFees = Amounts.add(
- totalDepositFees,
- denom.fees.feeDeposit,
- ).amount;
-
- if (!coveredExchanges.has(coinRecord.exchangeBaseUrl)) {
- const exchangeDetails = await getExchangeDetails(
- tx,
- coinRecord.exchangeBaseUrl,
- );
- checkBackupInvariant(!!exchangeDetails);
- let wireFee: AmountJson | undefined;
- const feesForType = exchangeDetails.wireInfo.feesForType;
- checkBackupInvariant(!!feesForType);
- for (const fee of feesForType[contractData.wireMethod] || []) {
- if (
- fee.startStamp <= contractData.timestamp &&
- fee.endStamp >= contractData.timestamp
- ) {
- wireFee = Amounts.parseOrThrow(fee.wireFee);
- break;
- }
- }
- if (wireFee) {
- totalWireFee = Amounts.add(totalWireFee, wireFee).amount;
- }
- coveredExchanges.add(coinRecord.exchangeBaseUrl);
- }
- }
-
- let customerWireFee: AmountJson;
-
- const amortizedWireFee = Amounts.divide(
- totalWireFee,
- contractData.wireFeeAmortization,
- );
- if (Amounts.cmp(contractData.maxWireFee, amortizedWireFee) < 0) {
- customerWireFee = amortizedWireFee;
- } else {
- customerWireFee = Amounts.zeroOfAmount(contractData.amount);
- }
-
- const customerDepositFees = Amounts.sub(
- totalDepositFees,
- contractData.maxDepositFee,
- ).amount;
-
- return {
- coinPubs,
- coinContributions: coinContributions.map((x) => Amounts.stringify(x)),
- paymentAmount: Amounts.stringify(contractData.amount),
- customerWireFees: Amounts.stringify(customerWireFee),
- customerDepositFees: Amounts.stringify(customerDepositFees),
- };
-}
-
-async function getDenomSelStateFromBackup(
- tx: GetReadOnlyAccess<{ denominations: typeof WalletStoresV1.denominations }>,
- currency: string,
- exchangeBaseUrl: string,
- sel: BackupDenomSel,
-): Promise<DenomSelectionState> {
- const selectedDenoms: {
- denomPubHash: string;
- count: number;
- }[] = [];
- let totalCoinValue = Amounts.zeroOfCurrency(currency);
- let totalWithdrawCost = Amounts.zeroOfCurrency(currency);
- for (const s of sel) {
- const d = await tx.denominations.get([exchangeBaseUrl, s.denom_pub_hash]);
- checkBackupInvariant(!!d);
- totalCoinValue = Amounts.add(
- totalCoinValue,
- DenominationRecord.getValue(d),
- ).amount;
- totalWithdrawCost = Amounts.add(
- totalWithdrawCost,
- DenominationRecord.getValue(d),
- d.fees.feeWithdraw,
- ).amount;
- }
- return {
- selectedDenoms,
- totalCoinValue: Amounts.stringify(totalCoinValue),
- totalWithdrawCost: Amounts.stringify(totalCoinValue),
- };
-}
-
-export interface CompletedCoin {
- coinPub: string;
- coinEvHash: string;
-}
-
-/**
- * Precomputed cryptographic material for a backup import.
- *
- * We separate this data from the backup blob as we want the backup
- * blob to be small, and we can't compute it during the database transaction,
- * as the async crypto worker communication would auto-close the database transaction.
- */
-export interface BackupCryptoPrecomputedData {
- rsaDenomPubToHash: Record<string, string>;
- coinPrivToCompletedCoin: Record<string, CompletedCoin>;
- proposalNoncePrivToPub: { [priv: string]: string };
- proposalIdToContractTermsHash: { [proposalId: string]: string };
- reservePrivToPub: Record<string, string>;
-}
-
-export async function importCoin(
- ws: InternalWalletState,
- tx: GetReadWriteAccess<{
- coins: typeof WalletStoresV1.coins;
- coinAvailability: typeof WalletStoresV1.coinAvailability;
- denominations: typeof WalletStoresV1.denominations;
- }>,
- cryptoComp: BackupCryptoPrecomputedData,
- args: {
- backupCoin: BackupCoin;
- exchangeBaseUrl: string;
- denomPubHash: string;
- },
-): Promise<void> {
- const { backupCoin, exchangeBaseUrl, denomPubHash } = args;
- const compCoin = cryptoComp.coinPrivToCompletedCoin[backupCoin.coin_priv];
- checkLogicInvariant(!!compCoin);
- const existingCoin = await tx.coins.get(compCoin.coinPub);
- if (!existingCoin) {
- let coinSource: CoinSource;
- switch (backupCoin.coin_source.type) {
- case BackupCoinSourceType.Refresh:
- coinSource = {
- type: CoinSourceType.Refresh,
- oldCoinPub: backupCoin.coin_source.old_coin_pub,
- refreshGroupId: backupCoin.coin_source.refresh_group_id,
- };
- break;
- case BackupCoinSourceType.Tip:
- coinSource = {
- type: CoinSourceType.Tip,
- coinIndex: backupCoin.coin_source.coin_index,
- walletTipId: backupCoin.coin_source.wallet_tip_id,
- };
- break;
- case BackupCoinSourceType.Withdraw:
- coinSource = {
- type: CoinSourceType.Withdraw,
- coinIndex: backupCoin.coin_source.coin_index,
- reservePub: backupCoin.coin_source.reserve_pub,
- withdrawalGroupId: backupCoin.coin_source.withdrawal_group_id,
- };
- break;
- }
- const coinRecord: CoinRecord = {
- blindingKey: backupCoin.blinding_key,
- coinEvHash: compCoin.coinEvHash,
- coinPriv: backupCoin.coin_priv,
- denomSig: backupCoin.denom_sig,
- coinPub: compCoin.coinPub,
- exchangeBaseUrl,
- denomPubHash,
- status: backupCoin.fresh ? CoinStatus.Fresh : CoinStatus.Dormant,
- coinSource,
- // FIXME!
- maxAge: AgeRestriction.AGE_UNRESTRICTED,
- // FIXME!
- ageCommitmentProof: undefined,
- // FIXME!
- spendAllocation: undefined,
- };
- if (coinRecord.status === CoinStatus.Fresh) {
- await makeCoinAvailable(ws, tx, coinRecord);
- } else {
- await tx.coins.put(coinRecord);
- }
- }
-}
-
-export async function importBackup(
- ws: InternalWalletState,
- backupBlobArg: any,
- cryptoComp: BackupCryptoPrecomputedData,
-): Promise<void> {
- await provideBackupState(ws);
-
- logger.info(`importing backup ${j2s(backupBlobArg)}`);
-
- return ws.db
- .mktx((x) => [
- x.config,
- x.exchangeDetails,
- x.exchanges,
- x.coins,
- x.coinAvailability,
- x.denominations,
- x.purchases,
- x.refreshGroups,
- x.backupProviders,
- x.tips,
- x.recoupGroups,
- x.withdrawalGroups,
- x.tombstones,
- x.depositGroups,
- ])
- .runReadWrite(async (tx) => {
- // FIXME: validate schema!
- const backupBlob = backupBlobArg as WalletBackupContentV1;
-
- // FIXME: validate version
-
- for (const tombstone of backupBlob.tombstones) {
- await tx.tombstones.put({
- id: tombstone,
- });
- }
-
- const tombstoneSet = new Set(
- (await tx.tombstones.iter().toArray()).map((x) => x.id),
- );
-
- // FIXME: Validate that the "details pointer" is correct
-
- for (const backupExchange of backupBlob.exchanges) {
- const existingExchange = await tx.exchanges.get(
- backupExchange.base_url,
- );
- if (existingExchange) {
- continue;
- }
- await tx.exchanges.put({
- baseUrl: backupExchange.base_url,
- detailsPointer: {
- currency: backupExchange.currency,
- masterPublicKey: backupExchange.master_public_key,
- updateClock: backupExchange.update_clock,
- },
- permanent: true,
- lastUpdate: undefined,
- nextUpdate: TalerProtocolTimestamp.now(),
- nextRefreshCheck: TalerProtocolTimestamp.now(),
- lastKeysEtag: undefined,
- lastWireEtag: undefined,
- });
- }
-
- for (const backupExchangeDetails of backupBlob.exchange_details) {
- const existingExchangeDetails =
- await tx.exchangeDetails.indexes.byPointer.get([
- backupExchangeDetails.base_url,
- backupExchangeDetails.currency,
- backupExchangeDetails.master_public_key,
- ]);
-
- if (!existingExchangeDetails) {
- const wireInfo: WireInfo = {
- accounts: backupExchangeDetails.accounts.map((x) => ({
- master_sig: x.master_sig,
- payto_uri: x.payto_uri,
- })),
- feesForType: {},
- };
- for (const fee of backupExchangeDetails.wire_fees) {
- const w = (wireInfo.feesForType[fee.wire_type] ??= []);
- w.push({
- closingFee: Amounts.stringify(fee.closing_fee),
- endStamp: fee.end_stamp,
- sig: fee.sig,
- startStamp: fee.start_stamp,
- wireFee: Amounts.stringify(fee.wire_fee),
- });
- }
- let tosAccepted = undefined;
- if (
- backupExchangeDetails.tos_accepted_etag &&
- backupExchangeDetails.tos_accepted_timestamp
- ) {
- tosAccepted = {
- etag: backupExchangeDetails.tos_accepted_etag,
- timestamp: backupExchangeDetails.tos_accepted_timestamp,
- };
- }
- await tx.exchangeDetails.put({
- exchangeBaseUrl: backupExchangeDetails.base_url,
- wireInfo,
- currency: backupExchangeDetails.currency,
- auditors: backupExchangeDetails.auditors.map((x) => ({
- auditor_pub: x.auditor_pub,
- auditor_url: x.auditor_url,
- denomination_keys: x.denomination_keys,
- })),
- masterPublicKey: backupExchangeDetails.master_public_key,
- protocolVersionRange: backupExchangeDetails.protocol_version,
- reserveClosingDelay: backupExchangeDetails.reserve_closing_delay,
- tosCurrentEtag: backupExchangeDetails.tos_accepted_etag || "",
- tosAccepted,
- globalFees: backupExchangeDetails.global_fees.map((x) => ({
- accountFee: Amounts.stringify(x.accountFee),
- historyFee: Amounts.stringify(x.historyFee),
- purseFee: Amounts.stringify(x.purseFee),
- endDate: x.endDate,
- historyTimeout: x.historyTimeout,
- signature: x.signature,
- purseLimit: x.purseLimit,
- purseTimeout: x.purseTimeout,
- startDate: x.startDate,
- })),
- });
- }
-
- for (const backupDenomination of backupExchangeDetails.denominations) {
- if (backupDenomination.denom_pub.cipher !== DenomKeyType.Rsa) {
- throw Error("unsupported cipher");
- }
- const denomPubHash =
- cryptoComp.rsaDenomPubToHash[
- backupDenomination.denom_pub.rsa_public_key
- ];
- checkLogicInvariant(!!denomPubHash);
- const existingDenom = await tx.denominations.get([
- backupExchangeDetails.base_url,
- denomPubHash,
- ]);
- if (!existingDenom) {
- const value = Amounts.parseOrThrow(backupDenomination.value);
-
- await tx.denominations.put({
- denomPub: backupDenomination.denom_pub,
- denomPubHash: denomPubHash,
- exchangeBaseUrl: backupExchangeDetails.base_url,
- exchangeMasterPub: backupExchangeDetails.master_public_key,
- fees: {
- feeDeposit: Amounts.stringify(backupDenomination.fee_deposit),
- feeRefresh: Amounts.stringify(backupDenomination.fee_refresh),
- feeRefund: Amounts.stringify(backupDenomination.fee_refund),
- feeWithdraw: Amounts.stringify(backupDenomination.fee_withdraw),
- },
- isOffered: backupDenomination.is_offered,
- isRevoked: backupDenomination.is_revoked,
- masterSig: backupDenomination.master_sig,
- stampExpireDeposit: backupDenomination.stamp_expire_deposit,
- stampExpireLegal: backupDenomination.stamp_expire_legal,
- stampExpireWithdraw: backupDenomination.stamp_expire_withdraw,
- stampStart: backupDenomination.stamp_start,
- verificationStatus: DenominationVerificationStatus.VerifiedGood,
- currency: value.currency,
- amountFrac: value.fraction,
- amountVal: value.value,
- listIssueDate: backupDenomination.list_issue_date,
- });
- }
- for (const backupCoin of backupDenomination.coins) {
- await importCoin(ws, tx, cryptoComp, {
- backupCoin,
- denomPubHash,
- exchangeBaseUrl: backupExchangeDetails.base_url,
- });
- }
- }
- }
-
- for (const backupWg of backupBlob.withdrawal_groups) {
- const reservePub = cryptoComp.reservePrivToPub[backupWg.reserve_priv];
- checkLogicInvariant(!!reservePub);
- const ts = makeTombstoneId(TombstoneTag.DeleteReserve, reservePub);
- if (tombstoneSet.has(ts)) {
- continue;
- }
- const existingWg = await tx.withdrawalGroups.get(
- backupWg.withdrawal_group_id,
- );
- if (existingWg) {
- continue;
- }
- let wgInfo: WgInfo;
- switch (backupWg.info.type) {
- case BackupWgType.BankIntegrated:
- wgInfo = {
- withdrawalType: WithdrawalRecordType.BankIntegrated,
- bankInfo: {
- exchangePaytoUri: backupWg.info.exchange_payto_uri,
- talerWithdrawUri: backupWg.info.taler_withdraw_uri,
- confirmUrl: backupWg.info.confirm_url,
- timestampBankConfirmed: backupWg.info.timestamp_bank_confirmed,
- timestampReserveInfoPosted:
- backupWg.info.timestamp_reserve_info_posted,
- },
- };
- break;
- case BackupWgType.BankManual:
- wgInfo = {
- withdrawalType: WithdrawalRecordType.BankManual,
- };
- break;
- case BackupWgType.PeerPullCredit:
- wgInfo = {
- withdrawalType: WithdrawalRecordType.PeerPullCredit,
- contractTerms: backupWg.info.contract_terms,
- contractPriv: backupWg.info.contract_priv,
- };
- break;
- case BackupWgType.PeerPushCredit:
- wgInfo = {
- withdrawalType: WithdrawalRecordType.PeerPushCredit,
- contractTerms: backupWg.info.contract_terms,
- };
- break;
- case BackupWgType.Recoup:
- wgInfo = {
- withdrawalType: WithdrawalRecordType.Recoup,
- };
- break;
- default:
- assertUnreachable(backupWg.info);
- }
- const instructedAmount = Amounts.parseOrThrow(
- backupWg.instructed_amount,
- );
- await tx.withdrawalGroups.put({
- withdrawalGroupId: backupWg.withdrawal_group_id,
- exchangeBaseUrl: backupWg.exchange_base_url,
- instructedAmount: Amounts.stringify(instructedAmount),
- secretSeed: backupWg.secret_seed,
- denomsSel: await getDenomSelStateFromBackup(
- tx,
- instructedAmount.currency,
- backupWg.exchange_base_url,
- backupWg.selected_denoms,
- ),
- denomSelUid: backupWg.selected_denoms_uid,
- rawWithdrawalAmount: Amounts.stringify(
- backupWg.raw_withdrawal_amount,
- ),
- effectiveWithdrawalAmount: Amounts.stringify(
- backupWg.effective_withdrawal_amount,
- ),
- reservePriv: backupWg.reserve_priv,
- reservePub,
- status: backupWg.timestamp_finish
- ? WithdrawalGroupStatus.Finished
- : WithdrawalGroupStatus.QueryingStatus, // FIXME!
- timestampStart: backupWg.timestamp_created,
- wgInfo,
- restrictAge: backupWg.restrict_age,
- senderWire: undefined, // FIXME!
- timestampFinish: backupWg.timestamp_finish,
- });
- }
-
- for (const backupPurchase of backupBlob.purchases) {
- const ts = makeTombstoneId(
- TombstoneTag.DeletePayment,
- backupPurchase.proposal_id,
- );
- if (tombstoneSet.has(ts)) {
- continue;
- }
- const existingPurchase = await tx.purchases.get(
- backupPurchase.proposal_id,
- );
- let proposalStatus: PurchaseStatus;
- switch (backupPurchase.proposal_status) {
- case BackupProposalStatus.Paid:
- proposalStatus = PurchaseStatus.Paid;
- break;
- case BackupProposalStatus.Proposed:
- proposalStatus = PurchaseStatus.Proposed;
- break;
- case BackupProposalStatus.PermanentlyFailed:
- proposalStatus = PurchaseStatus.PaymentAbortFinished;
- break;
- case BackupProposalStatus.Refused:
- proposalStatus = PurchaseStatus.ProposalRefused;
- break;
- case BackupProposalStatus.Repurchase:
- proposalStatus = PurchaseStatus.RepurchaseDetected;
- break;
- default: {
- const error: never = backupPurchase.proposal_status;
- throw Error(`backup status ${error} is not handled`);
- }
- }
- if (!existingPurchase) {
- const refunds: { [refundKey: string]: WalletRefundItem } = {};
- for (const backupRefund of backupPurchase.refunds) {
- const key = `${backupRefund.coin_pub}-${backupRefund.rtransaction_id}`;
- const coin = await tx.coins.get(backupRefund.coin_pub);
- checkBackupInvariant(!!coin);
- const denom = await tx.denominations.get([
- coin.exchangeBaseUrl,
- coin.denomPubHash,
- ]);
- checkBackupInvariant(!!denom);
- const common = {
- coinPub: backupRefund.coin_pub,
- executionTime: backupRefund.execution_time,
- obtainedTime: backupRefund.obtained_time,
- refundAmount: Amounts.stringify(backupRefund.refund_amount),
- refundFee: Amounts.stringify(denom.fees.feeRefund),
- rtransactionId: backupRefund.rtransaction_id,
- totalRefreshCostBound: Amounts.stringify(
- backupRefund.total_refresh_cost_bound,
- ),
- };
- switch (backupRefund.type) {
- case BackupRefundState.Applied:
- refunds[key] = {
- type: RefundState.Applied,
- ...common,
- };
- break;
- case BackupRefundState.Failed:
- refunds[key] = {
- type: RefundState.Failed,
- ...common,
- };
- break;
- case BackupRefundState.Pending:
- refunds[key] = {
- type: RefundState.Pending,
- ...common,
- };
- break;
- }
- }
- const parsedContractTerms = codecForMerchantContractTerms().decode(
- backupPurchase.contract_terms_raw,
- );
- const amount = Amounts.parseOrThrow(parsedContractTerms.amount);
- const contractTermsHash =
- cryptoComp.proposalIdToContractTermsHash[
- backupPurchase.proposal_id
- ];
- let maxWireFee: AmountJson;
- if (parsedContractTerms.max_wire_fee) {
- maxWireFee = Amounts.parseOrThrow(parsedContractTerms.max_wire_fee);
- } else {
- maxWireFee = Amounts.zeroOfCurrency(amount.currency);
- }
- const download: ProposalDownloadInfo = {
- contractTermsHash,
- contractTermsMerchantSig: backupPurchase.merchant_sig!,
- currency: amount.currency,
- fulfillmentUrl: backupPurchase.contract_terms_raw.fulfillment_url,
- };
-
- const contractData = extractContractData(
- backupPurchase.contract_terms_raw,
- contractTermsHash,
- download.contractTermsMerchantSig,
- );
-
- let payInfo: PurchasePayInfo | undefined = undefined;
- if (backupPurchase.pay_info) {
- payInfo = {
- payCoinSelection: await recoverPayCoinSelection(
- tx,
- contractData,
- backupPurchase.pay_info,
- ),
- payCoinSelectionUid: backupPurchase.pay_info.pay_coins_uid,
- totalPayCost: Amounts.stringify(
- backupPurchase.pay_info.total_pay_cost,
- ),
- };
- }
-
- await tx.purchases.put({
- proposalId: backupPurchase.proposal_id,
- noncePriv: backupPurchase.nonce_priv,
- noncePub:
- cryptoComp.proposalNoncePrivToPub[backupPurchase.nonce_priv],
- autoRefundDeadline: TalerProtocolTimestamp.never(),
- timestampAccept: backupPurchase.timestamp_accepted,
- timestampFirstSuccessfulPay:
- backupPurchase.timestamp_first_successful_pay,
- timestampLastRefundStatus: undefined,
- merchantPaySig: backupPurchase.merchant_pay_sig,
- lastSessionId: undefined,
- download,
- refunds,
- claimToken: backupPurchase.claim_token,
- downloadSessionId: backupPurchase.download_session_id,
- merchantBaseUrl: backupPurchase.merchant_base_url,
- orderId: backupPurchase.order_id,
- payInfo,
- refundAmountAwaiting: undefined,
- repurchaseProposalId: backupPurchase.repurchase_proposal_id,
- purchaseStatus: proposalStatus,
- timestamp: backupPurchase.timestamp_proposed,
- });
- }
- }
-
- for (const backupRefreshGroup of backupBlob.refresh_groups) {
- const ts = makeTombstoneId(
- TombstoneTag.DeleteRefreshGroup,
- backupRefreshGroup.refresh_group_id,
- );
- if (tombstoneSet.has(ts)) {
- continue;
- }
- const existingRg = await tx.refreshGroups.get(
- backupRefreshGroup.refresh_group_id,
- );
- if (!existingRg) {
- let reason: RefreshReason;
- switch (backupRefreshGroup.reason) {
- case BackupRefreshReason.AbortPay:
- reason = RefreshReason.AbortPay;
- break;
- case BackupRefreshReason.BackupRestored:
- reason = RefreshReason.BackupRestored;
- break;
- case BackupRefreshReason.Manual:
- reason = RefreshReason.Manual;
- break;
- case BackupRefreshReason.Pay:
- reason = RefreshReason.PayMerchant;
- break;
- case BackupRefreshReason.Recoup:
- reason = RefreshReason.Recoup;
- break;
- case BackupRefreshReason.Refund:
- reason = RefreshReason.Refund;
- break;
- case BackupRefreshReason.Scheduled:
- reason = RefreshReason.Scheduled;
- break;
- }
- const refreshSessionPerCoin: (RefreshSessionRecord | undefined)[] =
- [];
- for (const oldCoin of backupRefreshGroup.old_coins) {
- const c = await tx.coins.get(oldCoin.coin_pub);
- checkBackupInvariant(!!c);
- const d = await tx.denominations.get([
- c.exchangeBaseUrl,
- c.denomPubHash,
- ]);
- checkBackupInvariant(!!d);
-
- if (oldCoin.refresh_session) {
- const denomSel = await getDenomSelStateFromBackup(
- tx,
- d.currency,
- c.exchangeBaseUrl,
- oldCoin.refresh_session.new_denoms,
- );
- refreshSessionPerCoin.push({
- sessionSecretSeed: oldCoin.refresh_session.session_secret_seed,
- norevealIndex: oldCoin.refresh_session.noreveal_index,
- newDenoms: oldCoin.refresh_session.new_denoms.map((x) => ({
- count: x.count,
- denomPubHash: x.denom_pub_hash,
- })),
- amountRefreshOutput: Amounts.stringify(denomSel.totalCoinValue),
- });
- } else {
- refreshSessionPerCoin.push(undefined);
- }
- }
- await tx.refreshGroups.put({
- timestampFinished: backupRefreshGroup.timestamp_finish,
- timestampCreated: backupRefreshGroup.timestamp_created,
- refreshGroupId: backupRefreshGroup.refresh_group_id,
- reason,
- lastErrorPerCoin: {},
- oldCoinPubs: backupRefreshGroup.old_coins.map((x) => x.coin_pub),
- statusPerCoin: backupRefreshGroup.old_coins.map((x) =>
- x.finished
- ? RefreshCoinStatus.Finished
- : RefreshCoinStatus.Pending,
- ),
- operationStatus: backupRefreshGroup.timestamp_finish
- ? RefreshOperationStatus.Finished
- : RefreshOperationStatus.Pending,
- inputPerCoin: backupRefreshGroup.old_coins.map(
- (x) => x.input_amount,
- ),
- estimatedOutputPerCoin: backupRefreshGroup.old_coins.map(
- (x) => x.estimated_output_amount,
- ),
- refreshSessionPerCoin,
- });
- }
- }
-
- for (const backupTip of backupBlob.tips) {
- const ts = makeTombstoneId(
- TombstoneTag.DeleteTip,
- backupTip.wallet_tip_id,
- );
- if (tombstoneSet.has(ts)) {
- continue;
- }
- const existingTip = await tx.tips.get(backupTip.wallet_tip_id);
- if (!existingTip) {
- const tipAmountRaw = Amounts.parseOrThrow(backupTip.tip_amount_raw);
- const denomsSel = await getDenomSelStateFromBackup(
- tx,
- tipAmountRaw.currency,
- backupTip.exchange_base_url,
- backupTip.selected_denoms,
- );
- await tx.tips.put({
- acceptedTimestamp: backupTip.timestamp_accepted,
- createdTimestamp: backupTip.timestamp_created,
- denomsSel,
- exchangeBaseUrl: backupTip.exchange_base_url,
- merchantBaseUrl: backupTip.exchange_base_url,
- merchantTipId: backupTip.merchant_tip_id,
- pickedUpTimestamp: backupTip.timestamp_finished,
- secretSeed: backupTip.secret_seed,
- tipAmountEffective: Amounts.stringify(denomsSel.totalCoinValue),
- tipAmountRaw: Amounts.stringify(tipAmountRaw),
- tipExpiration: backupTip.timestamp_expiration,
- walletTipId: backupTip.wallet_tip_id,
- denomSelUid: backupTip.selected_denoms_uid,
- });
- }
- }
-
- // We now process tombstones.
- // The import code above should already prevent
- // importing things that are tombstoned,
- // but we do tombstone processing last just to be sure.
-
- for (const tombstone of tombstoneSet) {
- const [type, ...rest] = tombstone.split(":");
- if (type === TombstoneTag.DeleteDepositGroup) {
- await tx.depositGroups.delete(rest[0]);
- } else if (type === TombstoneTag.DeletePayment) {
- await tx.purchases.delete(rest[0]);
- } else if (type === TombstoneTag.DeleteRefreshGroup) {
- await tx.refreshGroups.delete(rest[0]);
- } else if (type === TombstoneTag.DeleteRefund) {
- // Nothing required, will just prevent display
- // in the transactions list
- } else if (type === TombstoneTag.DeleteTip) {
- await tx.tips.delete(rest[0]);
- } else if (type === TombstoneTag.DeleteWithdrawalGroup) {
- await tx.withdrawalGroups.delete(rest[0]);
- } else {
- logger.warn(`unable to process tombstone of type '${type}'`);
- }
- }
- });
-}
diff --git a/packages/taler-wallet-core/src/operations/backup/state.ts b/packages/taler-wallet-core/src/operations/backup/state.ts
deleted file mode 100644
index fa632f44c..000000000
--- a/packages/taler-wallet-core/src/operations/backup/state.ts
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2020 Taler Systems SA
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received 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 { encodeCrock, getRandomBytes } from "@gnu-taler/taler-util";
-import {
- ConfigRecord,
- ConfigRecordKey,
- WalletBackupConfState,
- WalletStoresV1,
-} from "../../db.js";
-import { checkDbInvariant } from "../../util/invariants.js";
-import { GetReadOnlyAccess } from "../../util/query.js";
-import { InternalWalletState } from "../../internal-wallet-state.js";
-
-export async function provideBackupState(
- ws: InternalWalletState,
-): Promise<WalletBackupConfState> {
- const bs: ConfigRecord | undefined = await ws.db
- .mktx((stores) => [stores.config])
- .runReadOnly(async (tx) => {
- return await tx.config.get(ConfigRecordKey.WalletBackupState);
- });
- if (bs) {
- checkDbInvariant(bs.key === ConfigRecordKey.WalletBackupState);
- return bs.value;
- }
- // We need to generate the key outside of the transaction
- // due to how IndexedDB works.
- const k = await ws.cryptoApi.createEddsaKeypair({});
- const d = getRandomBytes(5);
- // FIXME: device ID should be configured when wallet is initialized
- // and be based on hostname
- const deviceId = `wallet-core-${encodeCrock(d)}`;
- return await ws.db
- .mktx((x) => [x.config])
- .runReadWrite(async (tx) => {
- let backupStateEntry: ConfigRecord | undefined = await tx.config.get(
- ConfigRecordKey.WalletBackupState,
- );
- if (!backupStateEntry) {
- backupStateEntry = {
- key: ConfigRecordKey.WalletBackupState,
- value: {
- deviceId,
- walletRootPub: k.pub,
- walletRootPriv: k.priv,
- lastBackupPlainHash: undefined,
- },
- };
- await tx.config.put(backupStateEntry);
- }
- checkDbInvariant(
- backupStateEntry.key === ConfigRecordKey.WalletBackupState,
- );
- return backupStateEntry.value;
- });
-}
-
-export async function getWalletBackupState(
- ws: InternalWalletState,
- tx: GetReadOnlyAccess<{ config: typeof WalletStoresV1.config }>,
-): Promise<WalletBackupConfState> {
- const bs = await tx.config.get(ConfigRecordKey.WalletBackupState);
- checkDbInvariant(!!bs, "wallet backup state should be in DB");
- checkDbInvariant(bs.key === ConfigRecordKey.WalletBackupState);
- return bs.value;
-}
-
-export async function setWalletDeviceId(
- ws: InternalWalletState,
- deviceId: string,
-): Promise<void> {
- await provideBackupState(ws);
- await ws.db
- .mktx((x) => [x.config])
- .runReadWrite(async (tx) => {
- let backupStateEntry: ConfigRecord | undefined = await tx.config.get(
- ConfigRecordKey.WalletBackupState,
- );
- if (
- !backupStateEntry ||
- backupStateEntry.key !== ConfigRecordKey.WalletBackupState
- ) {
- return;
- }
- backupStateEntry.value.deviceId = deviceId;
- await tx.config.put(backupStateEntry);
- });
-}
-
-export async function getWalletDeviceId(
- ws: InternalWalletState,
-): Promise<string> {
- const bs = await provideBackupState(ws);
- return bs.deviceId;
-}
diff --git a/packages/taler-wallet-core/src/operations/balance.ts b/packages/taler-wallet-core/src/operations/balance.ts
deleted file mode 100644
index cd78b0360..000000000
--- a/packages/taler-wallet-core/src/operations/balance.ts
+++ /dev/null
@@ -1,163 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2019 GNUnet e.V.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- * Imports.
- */
-import {
- AmountJson,
- BalancesResponse,
- Amounts,
- Logger,
-} from "@gnu-taler/taler-util";
-import { WalletStoresV1 } from "../db.js";
-import { GetReadOnlyAccess } from "../util/query.js";
-import { InternalWalletState } from "../internal-wallet-state.js";
-
-const logger = new Logger("operations/balance.ts");
-
-interface WalletBalance {
- available: AmountJson;
- pendingIncoming: AmountJson;
- pendingOutgoing: AmountJson;
-}
-
-/**
- * Get balance information.
- */
-export async function getBalancesInsideTransaction(
- ws: InternalWalletState,
- tx: GetReadOnlyAccess<{
- coins: typeof WalletStoresV1.coins;
- coinAvailability: typeof WalletStoresV1.coinAvailability;
- refreshGroups: typeof WalletStoresV1.refreshGroups;
- withdrawalGroups: typeof WalletStoresV1.withdrawalGroups;
- }>,
-): Promise<BalancesResponse> {
- const balanceStore: Record<string, WalletBalance> = {};
-
- /**
- * Add amount to a balance field, both for
- * the slicing by exchange and currency.
- */
- const initBalance = (currency: string): WalletBalance => {
- const b = balanceStore[currency];
- if (!b) {
- balanceStore[currency] = {
- available: Amounts.zeroOfCurrency(currency),
- pendingIncoming: Amounts.zeroOfCurrency(currency),
- pendingOutgoing: Amounts.zeroOfCurrency(currency),
- };
- }
- return balanceStore[currency];
- };
-
- await tx.coinAvailability.iter().forEach((ca) => {
- const b = initBalance(ca.currency);
- for (let i = 0; i < ca.freshCoinCount; i++) {
- b.available = Amounts.add(b.available, {
- currency: ca.currency,
- fraction: ca.amountFrac,
- value: ca.amountVal,
- }).amount;
- }
- });
-
- await tx.refreshGroups.iter().forEach((r) => {
- // Don't count finished refreshes, since the refresh already resulted
- // in coins being added to the wallet.
- if (r.timestampFinished) {
- return;
- }
- for (let i = 0; i < r.oldCoinPubs.length; i++) {
- const session = r.refreshSessionPerCoin[i];
- if (session) {
- const currency = Amounts.parseOrThrow(
- session.amountRefreshOutput,
- ).currency;
- const b = initBalance(currency);
- // We are always assuming the refresh will succeed, thus we
- // report the output as available balance.
- b.available = Amounts.add(
- b.available,
- session.amountRefreshOutput,
- ).amount;
- } else {
- const currency = Amounts.parseOrThrow(r.inputPerCoin[i]).currency;
- const b = initBalance(currency);
- b.available = Amounts.add(
- b.available,
- r.estimatedOutputPerCoin[i],
- ).amount;
- }
- }
- });
-
- await tx.withdrawalGroups.iter().forEach((wds) => {
- if (wds.timestampFinish) {
- return;
- }
- const b = initBalance(Amounts.currencyOf(wds.denomsSel.totalWithdrawCost));
- b.pendingIncoming = Amounts.add(
- b.pendingIncoming,
- wds.denomsSel.totalCoinValue,
- ).amount;
- });
-
- const balancesResponse: BalancesResponse = {
- balances: [],
- };
-
- Object.keys(balanceStore)
- .sort()
- .forEach((c) => {
- const v = balanceStore[c];
- balancesResponse.balances.push({
- available: Amounts.stringify(v.available),
- pendingIncoming: Amounts.stringify(v.pendingIncoming),
- pendingOutgoing: Amounts.stringify(v.pendingOutgoing),
- hasPendingTransactions: false,
- requiresUserInput: false,
- });
- });
-
- return balancesResponse;
-}
-
-/**
- * Get detailed balance information, sliced by exchange and by currency.
- */
-export async function getBalances(
- ws: InternalWalletState,
-): Promise<BalancesResponse> {
- logger.trace("starting to compute balance");
-
- const wbal = await ws.db
- .mktx((x) => [
- x.coins,
- x.coinAvailability,
- x.refreshGroups,
- x.purchases,
- x.withdrawalGroups,
- ])
- .runReadOnly(async (tx) => {
- return getBalancesInsideTransaction(ws, tx);
- });
-
- logger.trace("finished computing wallet balance");
-
- return wbal;
-}
diff --git a/packages/taler-wallet-core/src/operations/common.ts b/packages/taler-wallet-core/src/operations/common.ts
deleted file mode 100644
index 2323cb82c..000000000
--- a/packages/taler-wallet-core/src/operations/common.ts
+++ /dev/null
@@ -1,411 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022 GNUnet e.V.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- * Imports.
- */
-import {
- AgeRestriction,
- AmountJson,
- Amounts,
- CoinRefreshRequest,
- CoinStatus,
- ExchangeEntryStatus,
- ExchangeListItem,
- ExchangeTosStatus,
- j2s,
- Logger,
- OperationErrorInfo,
- RefreshReason,
- TalerErrorCode,
- TalerErrorDetail,
- TombstoneIdStr,
- TransactionIdStr,
- TransactionType,
-} from "@gnu-taler/taler-util";
-import {
- WalletStoresV1,
- CoinRecord,
- ExchangeDetailsRecord,
- ExchangeRecord,
-} from "../db.js";
-import { makeErrorDetail, TalerError } from "../errors.js";
-import { InternalWalletState } from "../internal-wallet-state.js";
-import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
-import { GetReadWriteAccess } from "../util/query.js";
-import {
- OperationAttemptResult,
- OperationAttemptResultType,
- RetryInfo,
-} from "../util/retries.js";
-
-const logger = new Logger("operations/common.ts");
-
-export interface CoinsSpendInfo {
- coinPubs: string[];
- contributions: AmountJson[];
- refreshReason: RefreshReason;
- /**
- * Identifier for what the coin has been spent for.
- */
- allocationId: TransactionIdStr;
-}
-
-export async function makeCoinAvailable(
- ws: InternalWalletState,
- tx: GetReadWriteAccess<{
- coins: typeof WalletStoresV1.coins;
- coinAvailability: typeof WalletStoresV1.coinAvailability;
- denominations: typeof WalletStoresV1.denominations;
- }>,
- coinRecord: CoinRecord,
-): Promise<void> {
- checkLogicInvariant(coinRecord.status === CoinStatus.Fresh);
- const existingCoin = await tx.coins.get(coinRecord.coinPub);
- if (existingCoin) {
- return;
- }
- const denom = await tx.denominations.get([
- coinRecord.exchangeBaseUrl,
- coinRecord.denomPubHash,
- ]);
- checkDbInvariant(!!denom);
- const ageRestriction = coinRecord.maxAge;
- let car = await tx.coinAvailability.get([
- coinRecord.exchangeBaseUrl,
- coinRecord.denomPubHash,
- ageRestriction,
- ]);
- if (!car) {
- car = {
- maxAge: ageRestriction,
- amountFrac: denom.amountFrac,
- amountVal: denom.amountVal,
- currency: denom.currency,
- denomPubHash: denom.denomPubHash,
- exchangeBaseUrl: denom.exchangeBaseUrl,
- freshCoinCount: 0,
- };
- }
- car.freshCoinCount++;
- await tx.coins.put(coinRecord);
- await tx.coinAvailability.put(car);
-}
-
-export async function spendCoins(
- ws: InternalWalletState,
- tx: GetReadWriteAccess<{
- coins: typeof WalletStoresV1.coins;
- coinAvailability: typeof WalletStoresV1.coinAvailability;
- refreshGroups: typeof WalletStoresV1.refreshGroups;
- denominations: typeof WalletStoresV1.denominations;
- }>,
- csi: CoinsSpendInfo,
-): Promise<void> {
- let refreshCoinPubs: CoinRefreshRequest[] = [];
- for (let i = 0; i < csi.coinPubs.length; i++) {
- const coin = await tx.coins.get(csi.coinPubs[i]);
- if (!coin) {
- throw Error("coin allocated for payment doesn't exist anymore");
- }
- const denom = await ws.getDenomInfo(
- ws,
- tx,
- coin.exchangeBaseUrl,
- coin.denomPubHash,
- );
- checkDbInvariant(!!denom);
- const coinAvailability = await tx.coinAvailability.get([
- coin.exchangeBaseUrl,
- coin.denomPubHash,
- coin.maxAge,
- ]);
- checkDbInvariant(!!coinAvailability);
- 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");
- }
- refreshCoinPubs.push({
- amount: Amounts.stringify(remaining.amount),
- coinPub: coin.coinPub,
- });
- checkDbInvariant(!!coinAvailability);
- if (coinAvailability.freshCoinCount === 0) {
- throw Error(
- `invalid coin count ${coinAvailability.freshCoinCount} in DB`,
- );
- }
- coinAvailability.freshCoinCount--;
- await tx.coins.put(coin);
- await tx.coinAvailability.put(coinAvailability);
- }
- await ws.refreshOps.createRefreshGroup(
- ws,
- tx,
- refreshCoinPubs,
- RefreshReason.PayMerchant,
- );
-}
-
-export async function storeOperationError(
- ws: InternalWalletState,
- pendingTaskId: string,
- e: TalerErrorDetail,
-): Promise<void> {
- await ws.db
- .mktx((x) => [x.operationRetries])
- .runReadWrite(async (tx) => {
- let retryRecord = await tx.operationRetries.get(pendingTaskId);
- if (!retryRecord) {
- retryRecord = {
- id: pendingTaskId,
- lastError: e,
- retryInfo: RetryInfo.reset(),
- };
- } else {
- retryRecord.lastError = e;
- retryRecord.retryInfo = RetryInfo.increment(retryRecord.retryInfo);
- }
- await tx.operationRetries.put(retryRecord);
- });
-}
-
-export async function storeOperationPending(
- ws: InternalWalletState,
- pendingTaskId: string,
-): Promise<void> {
- await ws.db
- .mktx((x) => [x.operationRetries])
- .runReadWrite(async (tx) => {
- let retryRecord = await tx.operationRetries.get(pendingTaskId);
- if (!retryRecord) {
- retryRecord = {
- id: pendingTaskId,
- retryInfo: RetryInfo.reset(),
- };
- } else {
- delete retryRecord.lastError;
- retryRecord.retryInfo = RetryInfo.increment(retryRecord.retryInfo);
- }
- await tx.operationRetries.put(retryRecord);
- });
-}
-
-export async function runOperationWithErrorReporting<T1, T2>(
- ws: InternalWalletState,
- opId: string,
- f: () => Promise<OperationAttemptResult<T1, T2>>,
-): Promise<OperationAttemptResult<T1, T2>> {
- let maybeError: TalerErrorDetail | undefined;
- try {
- const resp = await f();
- switch (resp.type) {
- case OperationAttemptResultType.Error:
- await storeOperationError(ws, opId, resp.errorDetail);
- return resp;
- case OperationAttemptResultType.Finished:
- await storeOperationFinished(ws, opId);
- return resp;
- case OperationAttemptResultType.Pending:
- await storeOperationPending(ws, opId);
- return resp;
- case OperationAttemptResultType.Longpoll:
- return resp;
- }
- } catch (e) {
- if (e instanceof TalerError) {
- logger.warn("operation processed resulted in error");
- logger.warn(`error was: ${j2s(e.errorDetail)}`);
- maybeError = e.errorDetail;
- await storeOperationError(ws, opId, maybeError!);
- return {
- type: OperationAttemptResultType.Error,
- errorDetail: e.errorDetail,
- };
- } else if (e instanceof Error) {
- // This is a bug, as we expect pending operations to always
- // do their own error handling and only throw WALLET_PENDING_OPERATION_FAILED
- // or return something.
- logger.error(`Uncaught exception: ${e.message}`);
- logger.error(`Stack: ${e.stack}`);
- maybeError = makeErrorDetail(
- TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
- {
- stack: e.stack,
- },
- `unexpected exception (message: ${e.message})`,
- );
- await storeOperationError(ws, opId, maybeError);
- return {
- type: OperationAttemptResultType.Error,
- errorDetail: maybeError,
- };
- } else {
- logger.error("Uncaught exception, value is not even an error.");
- maybeError = makeErrorDetail(
- TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
- {},
- `unexpected exception (not even an error)`,
- );
- await storeOperationError(ws, opId, maybeError);
- return {
- type: OperationAttemptResultType.Error,
- errorDetail: maybeError,
- };
- }
- }
-}
-
-export async function storeOperationFinished(
- ws: InternalWalletState,
- pendingTaskId: string,
-): Promise<void> {
- await ws.db
- .mktx((x) => [x.operationRetries])
- .runReadWrite(async (tx) => {
- await tx.operationRetries.delete(pendingTaskId);
- });
-}
-
-export enum TombstoneTag {
- DeleteWithdrawalGroup = "delete-withdrawal-group",
- DeleteReserve = "delete-reserve",
- DeletePayment = "delete-payment",
- DeleteTip = "delete-tip",
- DeleteRefreshGroup = "delete-refresh-group",
- DeleteDepositGroup = "delete-deposit-group",
- DeleteRefund = "delete-refund",
- DeletePeerPullDebit = "delete-peer-pull-debit",
- DeletePeerPushDebit = "delete-peer-push-debit",
-}
-
-/**
- * Create an event ID from the type and the primary key for the event.
- */
-export function makeTransactionId(
- type: TransactionType,
- ...args: string[]
-): TransactionIdStr {
- return `txn:${type}:${args.map((x) => encodeURIComponent(x)).join(":")}`;
-}
-
-export function parseId(
- idType: "txn" | "tmb" | "any",
- txId: string,
-): {
- type: TransactionType;
- args: string[];
-} {
- const txnParts = txId.split(":");
- if (txnParts.length < 3) {
- throw Error("id should have al least 3 parts separated by ':'");
- }
- const [prefix, typeStr, ...args] = txnParts;
- const type = typeStr as TransactionType;
-
- if (idType != "any" && prefix !== idType) {
- throw Error(`id should start with ${idType}`);
- }
-
- if (args.length === 0) {
- throw Error("id should have one or more arguments");
- }
-
- return { type, args };
-}
-
-/**
- * Create an event ID from the type and the primary key for the event.
- */
-export function makeTombstoneId(
- type: TombstoneTag,
- ...args: string[]
-): TombstoneIdStr {
- return `tmb:${type}:${args.map((x) => encodeURIComponent(x)).join(":")}`;
-}
-
-export function getExchangeTosStatus(
- exchangeDetails: ExchangeDetailsRecord,
-): ExchangeTosStatus {
- if (!exchangeDetails.tosAccepted) {
- return ExchangeTosStatus.New;
- }
- if (exchangeDetails.tosAccepted?.etag == exchangeDetails.tosCurrentEtag) {
- return ExchangeTosStatus.Accepted;
- }
- return ExchangeTosStatus.Changed;
-}
-
-export function makeExchangeListItem(
- r: ExchangeRecord,
- exchangeDetails: ExchangeDetailsRecord | undefined,
- lastError: TalerErrorDetail | undefined,
-): ExchangeListItem {
- const lastUpdateErrorInfo: OperationErrorInfo | undefined = lastError
- ? {
- error: lastError,
- }
- : undefined;
- if (!exchangeDetails) {
- return {
- exchangeBaseUrl: r.baseUrl,
- currency: undefined,
- tosStatus: ExchangeTosStatus.Unknown,
- paytoUris: [],
- exchangeStatus: ExchangeEntryStatus.Unknown,
- permanent: r.permanent,
- ageRestrictionOptions: [],
- lastUpdateErrorInfo,
- };
- }
- let exchangeStatus;
- exchangeStatus = ExchangeEntryStatus.Ok;
- return {
- exchangeBaseUrl: r.baseUrl,
- currency: exchangeDetails.currency,
- tosStatus: getExchangeTosStatus(exchangeDetails),
- paytoUris: exchangeDetails.wireInfo.accounts.map((x) => x.payto_uri),
- exchangeStatus,
- permanent: r.permanent,
- ageRestrictionOptions: exchangeDetails.ageMask
- ? AgeRestriction.getAgeGroupsFromMask(exchangeDetails.ageMask)
- : [],
- lastUpdateErrorInfo,
- };
-}
diff --git a/packages/taler-wallet-core/src/operations/deposits.ts b/packages/taler-wallet-core/src/operations/deposits.ts
deleted file mode 100644
index 406d658af..000000000
--- a/packages/taler-wallet-core/src/operations/deposits.ts
+++ /dev/null
@@ -1,655 +0,0 @@
-/*
- 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 {
- AbsoluteTime,
- AmountJson,
- Amounts,
- CancellationToken,
- canonicalJson,
- codecForDepositSuccess,
- MerchantContractTerms,
- CreateDepositGroupRequest,
- CreateDepositGroupResponse,
- DepositGroupFees,
- durationFromSpec,
- encodeCrock,
- ExchangeDepositRequest,
- GetFeeForDepositRequest,
- getRandomBytes,
- hashWire,
- Logger,
- parsePaytoUri,
- PayCoinSelection,
- PrepareDepositRequest,
- PrepareDepositResponse,
- RefreshReason,
- TalerProtocolTimestamp,
- TrackDepositGroupRequest,
- TrackDepositGroupResponse,
- TransactionType,
- URL,
-} from "@gnu-taler/taler-util";
-import {
- DenominationRecord,
- DepositGroupRecord,
- OperationStatus,
-} from "../db.js";
-import { InternalWalletState } from "../internal-wallet-state.js";
-import { readSuccessResponseJsonOrThrow } from "../util/http.js";
-import { OperationAttemptResult } from "../util/retries.js";
-import { makeTransactionId, spendCoins } from "./common.js";
-import { getExchangeDetails } from "./exchanges.js";
-import {
- extractContractData,
- generateDepositPermissions,
- getTotalPaymentCost,
- selectPayCoinsNew,
-} from "./pay-merchant.js";
-import { getTotalRefreshCost } from "./refresh.js";
-
-/**
- * Logger.
- */
-const logger = new Logger("deposits.ts");
-
-/**
- * @see {processDepositGroup}
- */
-export async function processDepositGroup(
- ws: InternalWalletState,
- depositGroupId: string,
- options: {
- forceNow?: boolean;
- cancellationToken?: CancellationToken;
- } = {},
-): Promise<OperationAttemptResult> {
- const depositGroup = await ws.db
- .mktx((x) => [x.depositGroups])
- .runReadOnly(async (tx) => {
- return tx.depositGroups.get(depositGroupId);
- });
- if (!depositGroup) {
- logger.warn(`deposit group ${depositGroupId} not found`);
- return OperationAttemptResult.finishedEmpty();
- }
- if (depositGroup.timestampFinished) {
- logger.trace(`deposit group ${depositGroupId} already finished`);
- return OperationAttemptResult.finishedEmpty();
- }
-
- const contractData = extractContractData(
- depositGroup.contractTermsRaw,
- depositGroup.contractTermsHash,
- "",
- );
-
- // Check for cancellation before expensive operations.
- options.cancellationToken?.throwIfCancelled();
- const depositPermissions = await generateDepositPermissions(
- ws,
- depositGroup.payCoinSelection,
- contractData,
- );
-
- for (let i = 0; i < depositPermissions.length; i++) {
- if (depositGroup.depositedPerCoin[i]) {
- continue;
- }
- const perm = depositPermissions[i];
- const requestBody: ExchangeDepositRequest = {
- contribution: Amounts.stringify(perm.contribution),
- merchant_payto_uri: depositGroup.wire.payto_uri,
- wire_salt: depositGroup.wire.salt,
- h_contract_terms: depositGroup.contractTermsHash,
- ub_sig: perm.ub_sig,
- timestamp: depositGroup.contractTermsRaw.timestamp,
- wire_transfer_deadline:
- depositGroup.contractTermsRaw.wire_transfer_deadline,
- refund_deadline: depositGroup.contractTermsRaw.refund_deadline,
- coin_sig: perm.coin_sig,
- denom_pub_hash: perm.h_denom,
- merchant_pub: depositGroup.merchantPub,
- h_age_commitment: perm.h_age_commitment,
- };
- // Check for cancellation before making network request.
- options.cancellationToken?.throwIfCancelled();
- const url = new URL(`coins/${perm.coin_pub}/deposit`, perm.exchange_url);
- logger.info(`depositing to ${url}`);
- const httpResp = await ws.http.postJson(url.href, requestBody, {
- cancellationToken: options.cancellationToken,
- });
- await readSuccessResponseJsonOrThrow(httpResp, codecForDepositSuccess());
- await ws.db
- .mktx((x) => [x.depositGroups])
- .runReadWrite(async (tx) => {
- const dg = await tx.depositGroups.get(depositGroupId);
- if (!dg) {
- return;
- }
- dg.depositedPerCoin[i] = true;
- await tx.depositGroups.put(dg);
- });
- }
-
- await ws.db
- .mktx((x) => [x.depositGroups])
- .runReadWrite(async (tx) => {
- const dg = await tx.depositGroups.get(depositGroupId);
- if (!dg) {
- return;
- }
- let allDeposited = true;
- for (const d of depositGroup.depositedPerCoin) {
- if (!d) {
- allDeposited = false;
- }
- }
- if (allDeposited) {
- dg.timestampFinished = TalerProtocolTimestamp.now();
- dg.operationStatus = OperationStatus.Finished;
- await tx.depositGroups.put(dg);
- }
- });
- return OperationAttemptResult.finishedEmpty();
-}
-
-export async function trackDepositGroup(
- ws: InternalWalletState,
- req: TrackDepositGroupRequest,
-): Promise<TrackDepositGroupResponse> {
- const responses: {
- status: number;
- body: any;
- }[] = [];
- const depositGroup = await ws.db
- .mktx((x) => [x.depositGroups])
- .runReadOnly(async (tx) => {
- return tx.depositGroups.get(req.depositGroupId);
- });
- if (!depositGroup) {
- throw Error("deposit group not found");
- }
- const contractData = extractContractData(
- depositGroup.contractTermsRaw,
- depositGroup.contractTermsHash,
- "",
- );
-
- const depositPermissions = await generateDepositPermissions(
- ws,
- depositGroup.payCoinSelection,
- contractData,
- );
-
- const wireHash = depositGroup.contractTermsRaw.h_wire;
-
- for (const dp of depositPermissions) {
- const url = new URL(
- `deposits/${wireHash}/${depositGroup.merchantPub}/${depositGroup.contractTermsHash}/${dp.coin_pub}`,
- dp.exchange_url,
- );
- const sigResp = await ws.cryptoApi.signTrackTransaction({
- coinPub: dp.coin_pub,
- contractTermsHash: depositGroup.contractTermsHash,
- merchantPriv: depositGroup.merchantPriv,
- merchantPub: depositGroup.merchantPub,
- wireHash,
- });
- url.searchParams.set("merchant_sig", sigResp.sig);
- const httpResp = await ws.http.get(url.href);
- const body = await httpResp.json();
- responses.push({
- body,
- status: httpResp.status,
- });
- }
- return {
- responses,
- };
-}
-
-export async function getFeeForDeposit(
- ws: InternalWalletState,
- req: GetFeeForDepositRequest,
-): Promise<DepositGroupFees> {
- const p = parsePaytoUri(req.depositPaytoUri);
- if (!p) {
- throw Error("invalid payto URI");
- }
-
- const amount = Amounts.parseOrThrow(req.amount);
-
- const exchangeInfos: { url: string; master_pub: string }[] = [];
-
- await ws.db
- .mktx((x) => [x.exchanges, x.exchangeDetails])
- .runReadOnly(async (tx) => {
- const allExchanges = await tx.exchanges.iter().toArray();
- for (const e of allExchanges) {
- const details = await getExchangeDetails(tx, e.baseUrl);
- if (!details || amount.currency !== details.currency) {
- continue;
- }
- exchangeInfos.push({
- master_pub: details.masterPublicKey,
- url: e.baseUrl,
- });
- }
- });
-
- const payCoinSel = await selectPayCoinsNew(ws, {
- auditors: [],
- exchanges: Object.values(exchangeInfos).map((v) => ({
- exchangeBaseUrl: v.url,
- exchangePub: v.master_pub,
- })),
- wireMethod: p.targetType,
- contractTermsAmount: Amounts.parseOrThrow(req.amount),
- depositFeeLimit: Amounts.parseOrThrow(req.amount),
- wireFeeAmortization: 1,
- wireFeeLimit: Amounts.parseOrThrow(req.amount),
- prevPayCoins: [],
- });
-
- if (!payCoinSel) {
- throw Error("insufficient funds");
- }
-
- return await getTotalFeesForDepositAmount(
- ws,
- p.targetType,
- amount,
- payCoinSel,
- );
-}
-
-export async function prepareDepositGroup(
- ws: InternalWalletState,
- req: PrepareDepositRequest,
-): Promise<PrepareDepositResponse> {
- const p = parsePaytoUri(req.depositPaytoUri);
- if (!p) {
- throw Error("invalid payto URI");
- }
- const amount = Amounts.parseOrThrow(req.amount);
-
- const exchangeInfos: { url: string; master_pub: string }[] = [];
-
- await ws.db
- .mktx((x) => [x.exchanges, x.exchangeDetails])
- .runReadOnly(async (tx) => {
- const allExchanges = await tx.exchanges.iter().toArray();
- for (const e of allExchanges) {
- const details = await getExchangeDetails(tx, e.baseUrl);
- if (!details || amount.currency !== details.currency) {
- continue;
- }
- exchangeInfos.push({
- master_pub: details.masterPublicKey,
- url: e.baseUrl,
- });
- }
- });
-
- const now = AbsoluteTime.now();
- const nowRounded = AbsoluteTime.toTimestamp(now);
- const contractTerms: MerchantContractTerms = {
- auditors: [],
- exchanges: exchangeInfos,
- amount: req.amount,
- max_fee: Amounts.stringify(amount),
- max_wire_fee: Amounts.stringify(amount),
- wire_method: p.targetType,
- timestamp: nowRounded,
- merchant_base_url: "",
- summary: "",
- nonce: "",
- wire_transfer_deadline: nowRounded,
- order_id: "",
- h_wire: "",
- pay_deadline: AbsoluteTime.toTimestamp(
- AbsoluteTime.addDuration(now, durationFromSpec({ hours: 1 })),
- ),
- merchant: {
- name: "(wallet)",
- },
- merchant_pub: "",
- refund_deadline: TalerProtocolTimestamp.zero(),
- };
-
- const { h: contractTermsHash } = await ws.cryptoApi.hashString({
- str: canonicalJson(contractTerms),
- });
-
- const contractData = extractContractData(
- contractTerms,
- contractTermsHash,
- "",
- );
-
- const payCoinSel = await selectPayCoinsNew(ws, {
- auditors: contractData.allowedAuditors,
- exchanges: contractData.allowedExchanges,
- wireMethod: contractData.wireMethod,
- contractTermsAmount: Amounts.parseOrThrow(contractData.amount),
- depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee),
- wireFeeAmortization: contractData.wireFeeAmortization ?? 1,
- wireFeeLimit: Amounts.parseOrThrow(contractData.maxWireFee),
- prevPayCoins: [],
- });
-
- if (!payCoinSel) {
- throw Error("insufficient funds");
- }
-
- const totalDepositCost = await getTotalPaymentCost(ws, payCoinSel);
-
- const effectiveDepositAmount = await getEffectiveDepositAmount(
- ws,
- p.targetType,
- payCoinSel,
- );
-
- return {
- totalDepositCost: Amounts.stringify(totalDepositCost),
- effectiveDepositAmount: Amounts.stringify(effectiveDepositAmount),
- };
-}
-export async function createDepositGroup(
- ws: InternalWalletState,
- req: CreateDepositGroupRequest,
-): Promise<CreateDepositGroupResponse> {
- const p = parsePaytoUri(req.depositPaytoUri);
- if (!p) {
- throw Error("invalid payto URI");
- }
-
- const amount = Amounts.parseOrThrow(req.amount);
-
- const exchangeInfos: { url: string; master_pub: string }[] = [];
-
- await ws.db
- .mktx((x) => [x.exchanges, x.exchangeDetails])
- .runReadOnly(async (tx) => {
- const allExchanges = await tx.exchanges.iter().toArray();
- for (const e of allExchanges) {
- const details = await getExchangeDetails(tx, e.baseUrl);
- if (!details || amount.currency !== details.currency) {
- continue;
- }
- exchangeInfos.push({
- master_pub: details.masterPublicKey,
- url: e.baseUrl,
- });
- }
- });
-
- const now = AbsoluteTime.now();
- const nowRounded = AbsoluteTime.toTimestamp(now);
- const noncePair = await ws.cryptoApi.createEddsaKeypair({});
- const merchantPair = await ws.cryptoApi.createEddsaKeypair({});
- const wireSalt = encodeCrock(getRandomBytes(16));
- const wireHash = hashWire(req.depositPaytoUri, wireSalt);
- const contractTerms: MerchantContractTerms = {
- auditors: [],
- exchanges: exchangeInfos,
- amount: req.amount,
- max_fee: Amounts.stringify(amount),
- max_wire_fee: Amounts.stringify(amount),
- wire_method: p.targetType,
- timestamp: nowRounded,
- merchant_base_url: "",
- summary: "",
- nonce: noncePair.pub,
- wire_transfer_deadline: nowRounded,
- order_id: "",
- h_wire: wireHash,
- pay_deadline: AbsoluteTime.toTimestamp(
- AbsoluteTime.addDuration(now, durationFromSpec({ hours: 1 })),
- ),
- merchant: {
- name: "(wallet)",
- },
- merchant_pub: merchantPair.pub,
- refund_deadline: TalerProtocolTimestamp.zero(),
- };
-
- const { h: contractTermsHash } = await ws.cryptoApi.hashString({
- str: canonicalJson(contractTerms),
- });
-
- const contractData = extractContractData(
- contractTerms,
- contractTermsHash,
- "",
- );
-
- const payCoinSel = await selectPayCoinsNew(ws, {
- auditors: contractData.allowedAuditors,
- exchanges: contractData.allowedExchanges,
- wireMethod: contractData.wireMethod,
- contractTermsAmount: Amounts.parseOrThrow(contractData.amount),
- depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee),
- wireFeeAmortization: contractData.wireFeeAmortization ?? 1,
- wireFeeLimit: Amounts.parseOrThrow(contractData.maxWireFee),
- prevPayCoins: [],
- });
-
- if (!payCoinSel) {
- throw Error("insufficient funds");
- }
-
- const totalDepositCost = await getTotalPaymentCost(ws, payCoinSel);
-
- const depositGroupId = encodeCrock(getRandomBytes(32));
-
- const effectiveDepositAmount = await getEffectiveDepositAmount(
- ws,
- p.targetType,
- payCoinSel,
- );
-
- const depositGroup: DepositGroupRecord = {
- contractTermsHash,
- contractTermsRaw: contractTerms,
- depositGroupId,
- noncePriv: noncePair.priv,
- noncePub: noncePair.pub,
- timestampCreated: AbsoluteTime.toTimestamp(now),
- timestampFinished: undefined,
- payCoinSelection: payCoinSel,
- payCoinSelectionUid: encodeCrock(getRandomBytes(32)),
- depositedPerCoin: payCoinSel.coinPubs.map(() => false),
- merchantPriv: merchantPair.priv,
- merchantPub: merchantPair.pub,
- totalPayCost: Amounts.stringify(totalDepositCost),
- effectiveDepositAmount: Amounts.stringify(effectiveDepositAmount),
- wire: {
- payto_uri: req.depositPaytoUri,
- salt: wireSalt,
- },
- operationStatus: OperationStatus.Pending,
- };
-
- await ws.db
- .mktx((x) => [
- x.depositGroups,
- x.coins,
- x.recoupGroups,
- x.denominations,
- x.refreshGroups,
- x.coinAvailability,
- ])
- .runReadWrite(async (tx) => {
- await spendCoins(ws, tx, {
- allocationId: `txn:deposit:${depositGroup.depositGroupId}`,
- coinPubs: payCoinSel.coinPubs,
- contributions: payCoinSel.coinContributions.map((x) =>
- Amounts.parseOrThrow(x),
- ),
- refreshReason: RefreshReason.PayDeposit,
- });
- await tx.depositGroups.put(depositGroup);
- });
-
- return {
- depositGroupId: depositGroupId,
- transactionId: makeTransactionId(TransactionType.Deposit, depositGroupId),
- };
-}
-
-/**
- * Get the amount that will be deposited on the merchant's bank
- * account, not considering aggregation.
- */
-export async function getEffectiveDepositAmount(
- ws: InternalWalletState,
- wireType: string,
- pcs: PayCoinSelection,
-): Promise<AmountJson> {
- const amt: AmountJson[] = [];
- const fees: AmountJson[] = [];
- const exchangeSet: Set<string> = new Set();
-
- await ws.db
- .mktx((x) => [x.coins, x.denominations, x.exchanges, x.exchangeDetails])
- .runReadOnly(async (tx) => {
- for (let i = 0; i < pcs.coinPubs.length; i++) {
- const coin = await tx.coins.get(pcs.coinPubs[i]);
- if (!coin) {
- throw Error("can't calculate deposit amount, coin not found");
- }
- const denom = await ws.getDenomInfo(
- ws,
- tx,
- coin.exchangeBaseUrl,
- coin.denomPubHash,
- );
- if (!denom) {
- throw Error("can't find denomination to calculate deposit amount");
- }
- amt.push(Amounts.parseOrThrow(pcs.coinContributions[i]));
- fees.push(Amounts.parseOrThrow(denom.feeDeposit));
- exchangeSet.add(coin.exchangeBaseUrl);
- }
-
- for (const exchangeUrl of exchangeSet.values()) {
- const exchangeDetails = await getExchangeDetails(tx, exchangeUrl);
- if (!exchangeDetails) {
- continue;
- }
-
- // FIXME/NOTE: the line below _likely_ throws exception
- // about "find method not found on undefined" when the wireType
- // is not supported by the Exchange.
- const fee = exchangeDetails.wireInfo.feesForType[wireType].find((x) => {
- return AbsoluteTime.isBetween(
- AbsoluteTime.now(),
- AbsoluteTime.fromTimestamp(x.startStamp),
- AbsoluteTime.fromTimestamp(x.endStamp),
- );
- })?.wireFee;
- if (fee) {
- fees.push(Amounts.parseOrThrow(fee));
- }
- }
- });
- return Amounts.sub(Amounts.sum(amt).amount, Amounts.sum(fees).amount).amount;
-}
-
-/**
- * Get the fee amount that will be charged when trying to deposit the
- * specified amount using the selected coins and the wire method.
- */
-export async function getTotalFeesForDepositAmount(
- ws: InternalWalletState,
- wireType: string,
- total: AmountJson,
- pcs: PayCoinSelection,
-): Promise<DepositGroupFees> {
- const wireFee: AmountJson[] = [];
- const coinFee: AmountJson[] = [];
- const refreshFee: AmountJson[] = [];
- const exchangeSet: Set<string> = new Set();
-
- await ws.db
- .mktx((x) => [x.coins, x.denominations, x.exchanges, x.exchangeDetails])
- .runReadOnly(async (tx) => {
- for (let i = 0; i < pcs.coinPubs.length; i++) {
- const coin = await tx.coins.get(pcs.coinPubs[i]);
- if (!coin) {
- throw Error("can't calculate deposit amount, coin not found");
- }
- const denom = await ws.getDenomInfo(
- ws,
- tx,
- coin.exchangeBaseUrl,
- coin.denomPubHash,
- );
- if (!denom) {
- throw Error("can't find denomination to calculate deposit amount");
- }
- coinFee.push(Amounts.parseOrThrow(denom.feeDeposit));
- exchangeSet.add(coin.exchangeBaseUrl);
-
- const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl
- .iter(coin.exchangeBaseUrl)
- .filter((x) =>
- Amounts.isSameCurrency(
- DenominationRecord.getValue(x),
- pcs.coinContributions[i],
- ),
- );
- const amountLeft = Amounts.sub(
- denom.value,
- pcs.coinContributions[i],
- ).amount;
- const refreshCost = getTotalRefreshCost(allDenoms, denom, amountLeft);
- refreshFee.push(refreshCost);
- }
-
- for (const exchangeUrl of exchangeSet.values()) {
- const exchangeDetails = await getExchangeDetails(tx, exchangeUrl);
- if (!exchangeDetails) {
- continue;
- }
- const fee = exchangeDetails.wireInfo.feesForType[wireType]?.find(
- (x) => {
- return AbsoluteTime.isBetween(
- AbsoluteTime.now(),
- AbsoluteTime.fromTimestamp(x.startStamp),
- AbsoluteTime.fromTimestamp(x.endStamp),
- );
- },
- )?.wireFee;
- if (fee) {
- wireFee.push(Amounts.parseOrThrow(fee));
- }
- }
- });
-
- return {
- coin: Amounts.stringify(Amounts.sumOrZero(total.currency, coinFee).amount),
- wire: Amounts.stringify(Amounts.sumOrZero(total.currency, wireFee).amount),
- refresh: Amounts.stringify(
- Amounts.sumOrZero(total.currency, refreshFee).amount,
- ),
- };
-}
diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts
deleted file mode 100644
index b6e2a9d73..000000000
--- a/packages/taler-wallet-core/src/operations/exchanges.ts
+++ /dev/null
@@ -1,964 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2019 GNUnet e.V.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- * Imports.
- */
-import {
- AbsoluteTime,
- Amounts,
- CancellationToken,
- canonicalizeBaseUrl,
- codecForExchangeKeysJson,
- codecForExchangeWireJson,
- DenominationPubKey,
- Duration,
- durationFromSpec,
- encodeCrock,
- ExchangeAuditor,
- ExchangeDenomination,
- ExchangeGlobalFees,
- ExchangeSignKeyJson,
- ExchangeWireJson,
- GlobalFees,
- hashDenomPub,
- j2s,
- LibtoolVersion,
- Logger,
- NotificationType,
- parsePaytoUri,
- Recoup,
- TalerErrorCode,
- TalerProtocolDuration,
- TalerProtocolTimestamp,
- URL,
- WireFee,
- WireFeeMap,
- WireInfo,
-} from "@gnu-taler/taler-util";
-import {
- DenominationRecord,
- DenominationVerificationStatus,
- ExchangeDetailsRecord,
- ExchangeRecord,
- WalletStoresV1,
-} from "../db.js";
-import { TalerError } from "../errors.js";
-import { InternalWalletState, TrustInfo } from "../internal-wallet-state.js";
-import {
- getExpiry,
- HttpRequestLibrary,
- readSuccessResponseJsonOrThrow,
- readSuccessResponseTextOrThrow,
-} from "../util/http.js";
-import { checkDbInvariant } from "../util/invariants.js";
-import {
- DbAccess,
- GetReadOnlyAccess,
- GetReadWriteAccess,
-} from "../util/query.js";
-import {
- OperationAttemptResult,
- OperationAttemptResultType,
- RetryTags,
- unwrapOperationHandlerResultOrThrow,
-} from "../util/retries.js";
-import { WALLET_EXCHANGE_PROTOCOL_VERSION } from "../versions.js";
-import { runOperationWithErrorReporting } from "./common.js";
-import { isWithdrawableDenom } from "./withdraw.js";
-
-const logger = new Logger("exchanges.ts");
-
-function denominationRecordFromKeys(
- exchangeBaseUrl: string,
- exchangeMasterPub: string,
- listIssueDate: TalerProtocolTimestamp,
- denomIn: ExchangeDenomination,
-): DenominationRecord {
- let denomPub: DenominationPubKey;
- denomPub = denomIn.denom_pub;
- const denomPubHash = encodeCrock(hashDenomPub(denomPub));
- const value = Amounts.parseOrThrow(denomIn.value);
- const d: DenominationRecord = {
- denomPub,
- denomPubHash,
- exchangeBaseUrl,
- exchangeMasterPub,
- fees: {
- feeDeposit: Amounts.stringify(denomIn.fee_deposit),
- feeRefresh: Amounts.stringify(denomIn.fee_refresh),
- feeRefund: Amounts.stringify(denomIn.fee_refund),
- feeWithdraw: Amounts.stringify(denomIn.fee_withdraw),
- },
- isOffered: true,
- isRevoked: false,
- masterSig: denomIn.master_sig,
- stampExpireDeposit: denomIn.stamp_expire_deposit,
- stampExpireLegal: denomIn.stamp_expire_legal,
- stampExpireWithdraw: denomIn.stamp_expire_withdraw,
- stampStart: denomIn.stamp_start,
- verificationStatus: DenominationVerificationStatus.Unverified,
- amountFrac: value.fraction,
- amountVal: value.value,
- currency: value.currency,
- listIssueDate,
- };
- return d;
-}
-
-export function getExchangeRequestTimeout(): Duration {
- return Duration.fromSpec({
- seconds: 5,
- });
-}
-
-export interface ExchangeTosDownloadResult {
- tosText: string;
- tosEtag: string;
- tosContentType: string;
-}
-
-export async function downloadExchangeWithTermsOfService(
- exchangeBaseUrl: string,
- http: HttpRequestLibrary,
- timeout: Duration,
- contentType: string,
-): Promise<ExchangeTosDownloadResult> {
- logger.info(`downloading exchange tos (type ${contentType})`);
- const reqUrl = new URL("terms", exchangeBaseUrl);
- const headers = {
- Accept: contentType,
- };
-
- const resp = await http.get(reqUrl.href, {
- headers,
- timeout,
- });
- const tosText = await readSuccessResponseTextOrThrow(resp);
- const tosEtag = resp.headers.get("etag") || "unknown";
- const tosContentType = resp.headers.get("content-type") || "text/plain";
-
- return { tosText, tosEtag, tosContentType };
-}
-
-/**
- * Get exchange details from the database.
- */
-export async function getExchangeDetails(
- tx: GetReadOnlyAccess<{
- exchanges: typeof WalletStoresV1.exchanges;
- exchangeDetails: typeof WalletStoresV1.exchangeDetails;
- }>,
- exchangeBaseUrl: string,
-): Promise<ExchangeDetailsRecord | undefined> {
- const r = await tx.exchanges.get(exchangeBaseUrl);
- if (!r) {
- return;
- }
- const dp = r.detailsPointer;
- if (!dp) {
- return;
- }
- const { currency, masterPublicKey } = dp;
- return await tx.exchangeDetails.indexes.byPointer.get([
- r.baseUrl,
- currency,
- masterPublicKey,
- ]);
-}
-
-getExchangeDetails.makeContext = (db: DbAccess<typeof WalletStoresV1>) =>
- db.mktx((x) => [x.exchanges, x.exchangeDetails]);
-
-/**
- * Update the database based on the download of the terms of service.
- */
-export async function updateExchangeTermsOfService(
- ws: InternalWalletState,
- exchangeBaseUrl: string,
- tos: ExchangeTosDownloadResult,
-): Promise<void> {
- await ws.db
- .mktx((x) => [x.exchanges, x.exchangeTos, x.exchangeDetails])
- .runReadWrite(async (tx) => {
- const d = await getExchangeDetails(tx, exchangeBaseUrl);
- let tosRecord = await tx.exchangeTos.get([exchangeBaseUrl, tos.tosEtag]);
- if (!tosRecord) {
- tosRecord = {
- etag: tos.tosEtag,
- exchangeBaseUrl,
- termsOfServiceContentType: tos.tosContentType,
- termsOfServiceText: tos.tosText,
- };
- await tx.exchangeTos.put(tosRecord);
- }
- if (d) {
- d.tosCurrentEtag = tos.tosEtag;
- await tx.exchangeDetails.put(d);
- }
- });
-}
-
-/**
- * Mark a ToS version as accepted by the user.
- *
- * @param etag version of the ToS to accept, or current ToS version of not given
- */
-export async function acceptExchangeTermsOfService(
- ws: InternalWalletState,
- exchangeBaseUrl: string,
- etag: string | undefined,
-): Promise<void> {
- await ws.db
- .mktx((x) => [x.exchanges, x.exchangeDetails])
- .runReadWrite(async (tx) => {
- const d = await getExchangeDetails(tx, exchangeBaseUrl);
- if (d) {
- d.tosAccepted = {
- etag: etag || d.tosCurrentEtag,
- timestamp: TalerProtocolTimestamp.now(),
- };
- await tx.exchangeDetails.put(d);
- }
- });
-}
-
-async function validateWireInfo(
- ws: InternalWalletState,
- versionCurrent: number,
- wireInfo: ExchangeWireJson,
- masterPublicKey: string,
-): Promise<WireInfo> {
- for (const a of wireInfo.accounts) {
- logger.trace("validating exchange acct");
- let isValid = false;
- if (ws.insecureTrustExchange) {
- isValid = true;
- } else {
- const { valid: v } = await ws.cryptoApi.isValidWireAccount({
- masterPub: masterPublicKey,
- paytoUri: a.payto_uri,
- sig: a.master_sig,
- versionCurrent,
- });
- isValid = v;
- }
- if (!isValid) {
- throw Error("exchange acct signature invalid");
- }
- }
- const feesForType: WireFeeMap = {};
- for (const wireMethod of Object.keys(wireInfo.fees)) {
- const feeList: WireFee[] = [];
- for (const x of wireInfo.fees[wireMethod]) {
- const startStamp = x.start_date;
- const endStamp = x.end_date;
- const fee: WireFee = {
- closingFee: Amounts.stringify(x.closing_fee),
- endStamp,
- sig: x.sig,
- startStamp,
- wireFee: Amounts.stringify(x.wire_fee),
- };
- let isValid = false;
- if (ws.insecureTrustExchange) {
- isValid = true;
- } else {
- const { valid: v } = await ws.cryptoApi.isValidWireFee({
- masterPub: masterPublicKey,
- type: wireMethod,
- wf: fee,
- });
- isValid = v;
- }
- if (!isValid) {
- throw Error("exchange wire fee signature invalid");
- }
- feeList.push(fee);
- }
- feesForType[wireMethod] = feeList;
- }
-
- return {
- accounts: wireInfo.accounts,
- feesForType,
- };
-}
-
-async function validateGlobalFees(
- ws: InternalWalletState,
- fees: GlobalFees[],
- masterPub: string,
-): Promise<ExchangeGlobalFees[]> {
- const egf: ExchangeGlobalFees[] = [];
- for (const gf of fees) {
- logger.trace("validating exchange global fees");
- let isValid = false;
- if (ws.insecureTrustExchange) {
- isValid = true;
- } else {
- const { valid: v } = await ws.cryptoApi.isValidGlobalFees({
- masterPub,
- gf,
- });
- isValid = v;
- }
-
- if (!isValid) {
- throw Error("exchange global fees signature invalid: " + gf.master_sig);
- }
- egf.push({
- accountFee: Amounts.stringify(gf.account_fee),
- historyFee: Amounts.stringify(gf.history_fee),
- purseFee: Amounts.stringify(gf.purse_fee),
- startDate: gf.start_date,
- endDate: gf.end_date,
- signature: gf.master_sig,
- historyTimeout: gf.history_expiration,
- purseLimit: gf.purse_account_limit,
- purseTimeout: gf.purse_timeout,
- });
- }
-
- return egf;
-}
-
-export interface ExchangeInfo {
- wire: ExchangeWireJson;
- keys: ExchangeKeysDownloadResult;
-}
-
-export async function downloadExchangeInfo(
- exchangeBaseUrl: string,
- http: HttpRequestLibrary,
-): Promise<ExchangeInfo> {
- const wireInfo = await downloadExchangeWireInfo(
- exchangeBaseUrl,
- http,
- Duration.getForever(),
- );
- const keysInfo = await downloadExchangeKeysInfo(
- exchangeBaseUrl,
- http,
- Duration.getForever(),
- );
- return {
- keys: keysInfo,
- wire: wireInfo,
- };
-}
-
-/**
- * Fetch wire information for an exchange.
- *
- * @param exchangeBaseUrl Exchange base URL, assumed to be already normalized.
- */
-async function downloadExchangeWireInfo(
- exchangeBaseUrl: string,
- http: HttpRequestLibrary,
- timeout: Duration,
-): Promise<ExchangeWireJson> {
- const reqUrl = new URL("wire", exchangeBaseUrl);
-
- const resp = await http.get(reqUrl.href, {
- timeout,
- });
- const wireInfo = await readSuccessResponseJsonOrThrow(
- resp,
- codecForExchangeWireJson(),
- );
-
- return wireInfo;
-}
-
-export async function provideExchangeRecordInTx(
- ws: InternalWalletState,
- tx: GetReadWriteAccess<{
- exchanges: typeof WalletStoresV1.exchanges;
- exchangeDetails: typeof WalletStoresV1.exchangeDetails;
- }>,
- baseUrl: string,
- now: AbsoluteTime,
-): Promise<{
- exchange: ExchangeRecord;
- exchangeDetails: ExchangeDetailsRecord | undefined;
-}> {
- let exchange = await tx.exchanges.get(baseUrl);
- if (!exchange) {
- const r: ExchangeRecord = {
- permanent: true,
- baseUrl: baseUrl,
- detailsPointer: undefined,
- lastUpdate: undefined,
- nextUpdate: AbsoluteTime.toTimestamp(now),
- nextRefreshCheck: AbsoluteTime.toTimestamp(now),
- lastKeysEtag: undefined,
- lastWireEtag: undefined,
- };
- await tx.exchanges.put(r);
- exchange = r;
- }
- const exchangeDetails = await getExchangeDetails(tx, baseUrl);
- return { exchange, exchangeDetails };
-}
-
-interface ExchangeKeysDownloadResult {
- masterPublicKey: string;
- currency: string;
- auditors: ExchangeAuditor[];
- currentDenominations: DenominationRecord[];
- protocolVersion: string;
- signingKeys: ExchangeSignKeyJson[];
- reserveClosingDelay: TalerProtocolDuration;
- expiry: TalerProtocolTimestamp;
- recoup: Recoup[];
- listIssueDate: TalerProtocolTimestamp;
- globalFees: GlobalFees[];
-}
-
-/**
- * Download and validate an exchange's /keys data.
- */
-async function downloadExchangeKeysInfo(
- baseUrl: string,
- http: HttpRequestLibrary,
- timeout: Duration,
-): Promise<ExchangeKeysDownloadResult> {
- const keysUrl = new URL("keys", baseUrl);
-
- const resp = await http.get(keysUrl.href, {
- timeout,
- });
- const exchangeKeysJsonUnchecked = await readSuccessResponseJsonOrThrow(
- resp,
- codecForExchangeKeysJson(),
- );
-
- if (exchangeKeysJsonUnchecked.denoms.length === 0) {
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_EXCHANGE_DENOMINATIONS_INSUFFICIENT,
- {
- exchangeBaseUrl: baseUrl,
- },
- "exchange doesn't offer any denominations",
- );
- }
-
- const protocolVersion = exchangeKeysJsonUnchecked.version;
-
- const versionRes = LibtoolVersion.compare(
- WALLET_EXCHANGE_PROTOCOL_VERSION,
- protocolVersion,
- );
- if (versionRes?.compatible != true) {
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_EXCHANGE_PROTOCOL_VERSION_INCOMPATIBLE,
- {
- exchangeProtocolVersion: protocolVersion,
- walletProtocolVersion: WALLET_EXCHANGE_PROTOCOL_VERSION,
- },
- "exchange protocol version not compatible with wallet",
- );
- }
-
- const currency = Amounts.parseOrThrow(
- exchangeKeysJsonUnchecked.denoms[0].value,
- ).currency.toUpperCase();
-
- return {
- masterPublicKey: exchangeKeysJsonUnchecked.master_public_key,
- currency,
- auditors: exchangeKeysJsonUnchecked.auditors,
- currentDenominations: exchangeKeysJsonUnchecked.denoms.map((d) =>
- denominationRecordFromKeys(
- baseUrl,
- exchangeKeysJsonUnchecked.master_public_key,
- exchangeKeysJsonUnchecked.list_issue_date,
- d,
- ),
- ),
- protocolVersion: exchangeKeysJsonUnchecked.version,
- signingKeys: exchangeKeysJsonUnchecked.signkeys,
- reserveClosingDelay: exchangeKeysJsonUnchecked.reserve_closing_delay,
- expiry: AbsoluteTime.toTimestamp(
- getExpiry(resp, {
- minDuration: durationFromSpec({ hours: 1 }),
- }),
- ),
- recoup: exchangeKeysJsonUnchecked.recoup ?? [],
- listIssueDate: exchangeKeysJsonUnchecked.list_issue_date,
- globalFees: exchangeKeysJsonUnchecked.global_fees,
- };
-}
-
-export async function downloadTosFromAcceptedFormat(
- ws: InternalWalletState,
- baseUrl: string,
- timeout: Duration,
- acceptedFormat?: string[],
-): Promise<ExchangeTosDownloadResult> {
- let tosFound: ExchangeTosDownloadResult | undefined;
- //Remove this when exchange supports multiple content-type in accept header
- if (acceptedFormat)
- for (const format of acceptedFormat) {
- const resp = await downloadExchangeWithTermsOfService(
- baseUrl,
- ws.http,
- timeout,
- format,
- );
- if (resp.tosContentType === format) {
- tosFound = resp;
- break;
- }
- }
- if (tosFound !== undefined) {
- return tosFound;
- }
- // If none of the specified format was found try text/plain
- return await downloadExchangeWithTermsOfService(
- baseUrl,
- ws.http,
- timeout,
- "text/plain",
- );
-}
-
-export async function updateExchangeFromUrl(
- ws: InternalWalletState,
- baseUrl: string,
- options: {
- forceNow?: boolean;
- cancellationToken?: CancellationToken;
- } = {},
-): Promise<{
- exchange: ExchangeRecord;
- exchangeDetails: ExchangeDetailsRecord;
-}> {
- const canonUrl = canonicalizeBaseUrl(baseUrl);
- return unwrapOperationHandlerResultOrThrow(
- await runOperationWithErrorReporting(
- ws,
- RetryTags.forExchangeUpdateFromUrl(canonUrl),
- () => updateExchangeFromUrlHandler(ws, canonUrl, options),
- ),
- );
-}
-
-/**
- * Update or add exchange DB entry by fetching the /keys and /wire information.
- * Optionally link the reserve entry to the new or existing
- * exchange entry in then DB.
- */
-export async function updateExchangeFromUrlHandler(
- ws: InternalWalletState,
- exchangeBaseUrl: string,
- options: {
- forceNow?: boolean;
- cancellationToken?: CancellationToken;
- } = {},
-): Promise<
- OperationAttemptResult<{
- exchange: ExchangeRecord;
- exchangeDetails: ExchangeDetailsRecord;
- }>
-> {
- const forceNow = options.forceNow ?? false;
- logger.info(
- `updating exchange info for ${exchangeBaseUrl}, forced: ${forceNow}`,
- );
-
- const now = AbsoluteTime.now();
- exchangeBaseUrl = canonicalizeBaseUrl(exchangeBaseUrl);
- let isNewExchange = true;
- const { exchange, exchangeDetails } = await ws.db
- .mktx((x) => [x.exchanges, x.exchangeDetails])
- .runReadWrite(async (tx) => {
- let oldExch = await tx.exchanges.get(exchangeBaseUrl);
- if (oldExch) {
- isNewExchange = false;
- }
- return provideExchangeRecordInTx(ws, tx, exchangeBaseUrl, now);
- });
-
- if (
- !forceNow &&
- exchangeDetails !== undefined &&
- !AbsoluteTime.isExpired(AbsoluteTime.fromTimestamp(exchange.nextUpdate))
- ) {
- logger.info("using existing exchange info");
- return {
- type: OperationAttemptResultType.Finished,
- result: { exchange, exchangeDetails },
- };
- }
-
- logger.info("updating exchange /keys info");
-
- const timeout = getExchangeRequestTimeout();
-
- const keysInfo = await downloadExchangeKeysInfo(
- exchangeBaseUrl,
- ws.http,
- timeout,
- );
-
- logger.info("updating exchange /wire info");
- const wireInfoDownload = await downloadExchangeWireInfo(
- exchangeBaseUrl,
- ws.http,
- timeout,
- );
-
- logger.info("validating exchange /wire info");
-
- const version = LibtoolVersion.parseVersion(keysInfo.protocolVersion);
- if (!version) {
- // Should have been validated earlier.
- throw Error("unexpected invalid version");
- }
-
- const wireInfo = await validateWireInfo(
- ws,
- version.current,
- wireInfoDownload,
- keysInfo.masterPublicKey,
- );
-
- const globalFees = await validateGlobalFees(
- ws,
- keysInfo.globalFees,
- keysInfo.masterPublicKey,
- );
-
- logger.info("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(
- ws,
- exchangeBaseUrl,
- timeout,
- ["text/plain"],
- );
-
- let recoupGroupId: string | undefined;
-
- logger.trace("updating exchange info in database");
-
- let detailsPointerChanged = false;
-
- let ageMask = 0;
- for (const x of keysInfo.currentDenominations) {
- if (isWithdrawableDenom(x) && x.denomPub.age_mask != 0) {
- ageMask = x.denomPub.age_mask;
- break;
- }
- }
-
- const updated = await ws.db
- .mktx((x) => [
- x.exchanges,
- x.exchangeTos,
- x.exchangeDetails,
- x.exchangeSignkeys,
- x.denominations,
- x.coins,
- x.refreshGroups,
- x.recoupGroups,
- ])
- .runReadWrite(async (tx) => {
- const r = await tx.exchanges.get(exchangeBaseUrl);
- if (!r) {
- logger.warn(`exchange ${exchangeBaseUrl} no longer present`);
- return;
- }
- const existingDetails = await getExchangeDetails(tx, r.baseUrl);
- if (!existingDetails) {
- detailsPointerChanged = true;
- }
- if (existingDetails) {
- if (existingDetails.masterPublicKey !== keysInfo.masterPublicKey) {
- detailsPointerChanged = true;
- }
- if (existingDetails.currency !== keysInfo.currency) {
- detailsPointerChanged = true;
- }
- // FIXME: We need to do some consistency checks!
- }
- const existingTosAccepted = existingDetails?.tosAccepted;
- const newDetails: ExchangeDetailsRecord = {
- auditors: keysInfo.auditors,
- currency: keysInfo.currency,
- masterPublicKey: keysInfo.masterPublicKey,
- protocolVersionRange: keysInfo.protocolVersion,
- reserveClosingDelay: keysInfo.reserveClosingDelay,
- globalFees,
- exchangeBaseUrl: r.baseUrl,
- wireInfo,
- tosCurrentEtag: tosDownload.tosEtag,
- tosAccepted: existingTosAccepted,
- ageMask,
- };
- if (existingDetails?.rowId) {
- newDetails.rowId = existingDetails.rowId;
- }
- r.lastUpdate = TalerProtocolTimestamp.now();
- r.nextUpdate = keysInfo.expiry;
- // New denominations might be available.
- r.nextRefreshCheck = TalerProtocolTimestamp.now();
- if (detailsPointerChanged) {
- r.detailsPointer = {
- currency: newDetails.currency,
- masterPublicKey: newDetails.masterPublicKey,
- updateClock: TalerProtocolTimestamp.now(),
- };
- }
- await tx.exchanges.put(r);
- const drRowId = await tx.exchangeDetails.put(newDetails);
- checkDbInvariant(typeof drRowId.key === "number");
-
- let tosRecord = await tx.exchangeTos.get([
- exchangeBaseUrl,
- tosDownload.tosEtag,
- ]);
-
- if (!tosRecord || tosRecord.etag !== existingTosAccepted?.etag) {
- tosRecord = {
- etag: tosDownload.tosEtag,
- exchangeBaseUrl,
- termsOfServiceContentType: tosDownload.tosContentType,
- termsOfServiceText: tosDownload.tosText,
- };
- await tx.exchangeTos.put(tosRecord);
- }
-
- for (const sk of keysInfo.signingKeys) {
- // FIXME: validate signing keys before inserting them
- await tx.exchangeSignKeys.put({
- exchangeDetailsRowId: drRowId.key,
- masterSig: sk.master_sig,
- signkeyPub: sk.key,
- stampEnd: sk.stamp_end,
- stampExpire: sk.stamp_expire,
- stampStart: sk.stamp_start,
- });
- }
-
- logger.info("updating denominations in database");
- const currentDenomSet = new Set<string>(
- keysInfo.currentDenominations.map((x) => x.denomPubHash),
- );
- for (const currentDenom of keysInfo.currentDenominations) {
- const oldDenom = await tx.denominations.get([
- exchangeBaseUrl,
- currentDenom.denomPubHash,
- ]);
- if (oldDenom) {
- // FIXME: Do consistency check, report to auditor if necessary.
- } else {
- await tx.denominations.put(currentDenom);
- }
- }
-
- // Update list issue date for all denominations,
- // and mark non-offered denominations as such.
- await tx.denominations.indexes.byExchangeBaseUrl
- .iter(r.baseUrl)
- .forEachAsync(async (x) => {
- if (!currentDenomSet.has(x.denomPubHash)) {
- // FIXME: Here, an auditor report should be created, unless
- // the denomination is really legally expired.
- if (x.isOffered) {
- x.isOffered = false;
- logger.info(
- `setting denomination ${x.denomPubHash} to offered=false`,
- );
- }
- } else {
- x.listIssueDate = keysInfo.listIssueDate;
- if (!x.isOffered) {
- x.isOffered = true;
- logger.info(
- `setting denomination ${x.denomPubHash} to offered=true`,
- );
- }
- }
- await tx.denominations.put(x);
- });
-
- logger.trace("done updating denominations in database");
-
- // Handle recoup
- const recoupDenomList = keysInfo.recoup;
- const newlyRevokedCoinPubs: string[] = [];
- logger.trace("recoup list from exchange", recoupDenomList);
- for (const recoupInfo of recoupDenomList) {
- const oldDenom = await tx.denominations.get([
- r.baseUrl,
- recoupInfo.h_denom_pub,
- ]);
- if (!oldDenom) {
- // We never even knew about the revoked denomination, all good.
- continue;
- }
- if (oldDenom.isRevoked) {
- // We already marked the denomination as revoked,
- // this implies we revoked all coins
- logger.trace("denom already revoked");
- continue;
- }
- logger.info("revoking denom", recoupInfo.h_denom_pub);
- oldDenom.isRevoked = true;
- await tx.denominations.put(oldDenom);
- const affectedCoins = await tx.coins.indexes.byDenomPubHash
- .iter(recoupInfo.h_denom_pub)
- .toArray();
- for (const ac of affectedCoins) {
- newlyRevokedCoinPubs.push(ac.coinPub);
- }
- }
- if (newlyRevokedCoinPubs.length != 0) {
- logger.info("recouping coins", newlyRevokedCoinPubs);
- recoupGroupId = await ws.recoupOps.createRecoupGroup(
- ws,
- tx,
- exchange.baseUrl,
- newlyRevokedCoinPubs,
- );
- }
-
- return {
- exchange: r,
- exchangeDetails: newDetails,
- };
- });
-
- if (recoupGroupId) {
- // Asynchronously start recoup. This doesn't need to finish
- // for the exchange update to be considered finished.
- ws.recoupOps.processRecoupGroup(ws, recoupGroupId).catch((e) => {
- logger.error("error while recouping coins:", e);
- });
- }
-
- if (!updated) {
- throw Error("something went wrong with updating the exchange");
- }
-
- logger.trace("done updating exchange info in database");
-
- if (isNewExchange) {
- ws.notify({
- type: NotificationType.ExchangeAdded,
- });
- }
-
- return {
- type: OperationAttemptResultType.Finished,
- result: {
- exchange: updated.exchange,
- exchangeDetails: updated.exchangeDetails,
- },
- };
-}
-
-/**
- * Find a payto:// URI of the exchange that is of one
- * of the given target types.
- *
- * Throws if no matching account was found.
- */
-export async function getExchangePaytoUri(
- ws: InternalWalletState,
- exchangeBaseUrl: string,
- supportedTargetTypes: string[],
-): Promise<string> {
- // We do the update here, since the exchange might not even exist
- // yet in our database.
- const details = await getExchangeDetails
- .makeContext(ws.db)
- .runReadOnly(async (tx) => {
- return getExchangeDetails(tx, exchangeBaseUrl);
- });
- const accounts = details?.wireInfo.accounts ?? [];
- for (const account of accounts) {
- const res = parsePaytoUri(account.payto_uri);
- if (!res) {
- continue;
- }
- if (supportedTargetTypes.includes(res.targetType)) {
- return account.payto_uri;
- }
- }
- throw Error(
- `no matching account found at exchange ${exchangeBaseUrl} for wire types ${j2s(
- supportedTargetTypes,
- )}`,
- );
-}
-
-/**
- * Check if and how an exchange is trusted and/or audited.
- */
-export async function getExchangeTrust(
- ws: InternalWalletState,
- exchangeInfo: ExchangeRecord,
-): Promise<TrustInfo> {
- let isTrusted = false;
- let isAudited = false;
-
- return await ws.db
- .mktx((x) => [
- x.exchanges,
- x.exchangeDetails,
- x.exchangeTrust,
- x.auditorTrust,
- ])
- .runReadOnly(async (tx) => {
- const exchangeDetails = await getExchangeDetails(
- tx,
- exchangeInfo.baseUrl,
- );
-
- if (!exchangeDetails) {
- throw Error(`exchange ${exchangeInfo.baseUrl} details not available`);
- }
- const exchangeTrustRecord =
- await tx.exchangeTrust.indexes.byExchangeMasterPub.get(
- exchangeDetails.masterPublicKey,
- );
- if (
- exchangeTrustRecord &&
- exchangeTrustRecord.uids.length > 0 &&
- exchangeTrustRecord.currency === exchangeDetails.currency
- ) {
- isTrusted = true;
- }
-
- for (const auditor of exchangeDetails.auditors) {
- const auditorTrustRecord =
- await tx.auditorTrust.indexes.byAuditorPub.get(auditor.auditor_pub);
- if (auditorTrustRecord && auditorTrustRecord.uids.length > 0) {
- isAudited = true;
- break;
- }
- }
-
- return { isTrusted, isAudited };
- });
-}
diff --git a/packages/taler-wallet-core/src/operations/merchants.ts b/packages/taler-wallet-core/src/operations/merchants.ts
deleted file mode 100644
index eeefc0f79..000000000
--- a/packages/taler-wallet-core/src/operations/merchants.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- 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 {
- canonicalizeBaseUrl,
- Logger,
- URL,
- codecForMerchantConfigResponse,
- LibtoolVersion,
-} from "@gnu-taler/taler-util";
-import { InternalWalletState, MerchantInfo } from "../internal-wallet-state.js";
-import { readSuccessResponseJsonOrThrow } from "../util/http.js";
-
-const logger = new Logger("taler-wallet-core:merchants.ts");
-
-export async function getMerchantInfo(
- ws: InternalWalletState,
- merchantBaseUrl: string,
-): Promise<MerchantInfo> {
- const canonBaseUrl = canonicalizeBaseUrl(merchantBaseUrl);
-
- const existingInfo = ws.merchantInfoCache[canonBaseUrl];
- if (existingInfo) {
- return existingInfo;
- }
-
- const configUrl = new URL("config", canonBaseUrl);
- const resp = await ws.http.get(configUrl.href);
-
- const configResp = await readSuccessResponseJsonOrThrow(
- resp,
- codecForMerchantConfigResponse(),
- );
-
- logger.info(
- `merchant "${canonBaseUrl}" reports protocol ${configResp.version}"`,
- );
-
- const parsedVersion = LibtoolVersion.parseVersion(configResp.version);
- if (!parsedVersion) {
- throw Error("invalid merchant version");
- }
-
- const merchantInfo: MerchantInfo = {
- protocolVersionCurrent: parsedVersion.current,
- };
-
- ws.merchantInfoCache[canonBaseUrl] = merchantInfo;
- return merchantInfo;
-}
diff --git a/packages/taler-wallet-core/src/operations/pay-merchant.ts b/packages/taler-wallet-core/src/operations/pay-merchant.ts
deleted file mode 100644
index ed7f17a18..000000000
--- a/packages/taler-wallet-core/src/operations/pay-merchant.ts
+++ /dev/null
@@ -1,2836 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2019-2022 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- * Implementation of the payment operation, including downloading and
- * claiming of proposals.
- *
- * @author Florian Dold
- */
-
-/**
- * Imports.
- */
-import { GlobalIDB } from "@gnu-taler/idb-bridge";
-import {
- AbortingCoin,
- AbortRequest,
- AbsoluteTime,
- AgeRestriction,
- AmountJson,
- Amounts,
- ApplyRefundResponse,
- codecForAbortResponse,
- codecForMerchantContractTerms,
- codecForMerchantOrderRefundPickupResponse,
- codecForMerchantOrderStatusPaid,
- codecForMerchantPayResponse,
- codecForProposal,
- CoinDepositPermission,
- CoinRefreshRequest,
- CoinStatus,
- ConfirmPayResult,
- ConfirmPayResultType,
- MerchantContractTerms,
- ContractTermsUtil,
- DenominationInfo,
- Duration,
- encodeCrock,
- ForcedCoinSel,
- getRandomBytes,
- HttpStatusCode,
- j2s,
- Logger,
- MerchantCoinRefundFailureStatus,
- MerchantCoinRefundStatus,
- MerchantCoinRefundSuccessStatus,
- NotificationType,
- parsePaytoUri,
- parsePayUri,
- parseRefundUri,
- PayCoinSelection,
- PreparePayResult,
- PreparePayResultType,
- PrepareRefundResult,
- RefreshReason,
- strcmp,
- TalerErrorCode,
- TalerErrorDetail,
- TalerProtocolTimestamp,
- TransactionType,
- URL,
- constructPayUri,
-} from "@gnu-taler/taler-util";
-import { EddsaKeypair } from "../crypto/cryptoImplementation.js";
-import {
- AllowedAuditorInfo,
- AllowedExchangeInfo,
- BackupProviderStateTag,
- CoinRecord,
- DenominationRecord,
- PurchaseRecord,
- PurchaseStatus,
- RefundReason,
- RefundState,
- WalletContractData,
- WalletStoresV1,
-} from "../db.js";
-import {
- makeErrorDetail,
- makePendingOperationFailedError,
- TalerError,
- TalerProtocolViolationError,
-} from "../errors.js";
-import { GetReadWriteAccess } from "../index.browser.js";
-import {
- EXCHANGE_COINS_LOCK,
- InternalWalletState,
-} from "../internal-wallet-state.js";
-import { PendingTaskType } from "../pending-types.js";
-import { assertUnreachable } from "../util/assertUnreachable.js";
-import {
- CoinSelectionTally,
- PreviousPayCoins,
- tallyFees,
-} from "../util/coinSelection.js";
-import {
- getHttpResponseErrorDetails,
- readSuccessResponseJsonOrErrorCode,
- readSuccessResponseJsonOrThrow,
- readTalerErrorResponse,
- readUnexpectedResponseDetails,
- throwUnexpectedRequestError,
-} from "../util/http.js";
-import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
-import {
- OperationAttemptResult,
- OperationAttemptResultType,
- RetryInfo,
- RetryTags,
- scheduleRetry,
-} from "../util/retries.js";
-import {
- makeTransactionId,
- spendCoins,
- storeOperationError,
- storeOperationPending,
-} from "./common.js";
-import { getExchangeDetails } from "./exchanges.js";
-import { createRefreshGroup, getTotalRefreshCost } from "./refresh.js";
-import { GetReadOnlyAccess } from "../util/query.js";
-
-/**
- * Logger.
- */
-const logger = new Logger("pay.ts");
-
-/**
- * Compute the total cost of a payment to the customer.
- *
- * This includes the amount taken by the merchant, fees (wire/deposit) contributed
- * by the customer, refreshing fees, fees for withdraw-after-refresh and "trimmings"
- * of coins that are too small to spend.
- */
-export async function getTotalPaymentCost(
- ws: InternalWalletState,
- pcs: PayCoinSelection,
-): Promise<AmountJson> {
- return ws.db
- .mktx((x) => [x.coins, x.denominations])
- .runReadOnly(async (tx) => {
- const costs: AmountJson[] = [];
- for (let i = 0; i < pcs.coinPubs.length; i++) {
- const coin = await tx.coins.get(pcs.coinPubs[i]);
- if (!coin) {
- throw Error("can't calculate payment cost, coin not found");
- }
- const denom = await tx.denominations.get([
- coin.exchangeBaseUrl,
- coin.denomPubHash,
- ]);
- if (!denom) {
- throw Error(
- "can't calculate payment cost, denomination for coin not found",
- );
- }
- const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl
- .iter(coin.exchangeBaseUrl)
- .filter((x) =>
- Amounts.isSameCurrency(
- DenominationRecord.getValue(x),
- pcs.coinContributions[i],
- ),
- );
- const amountLeft = Amounts.sub(
- DenominationRecord.getValue(denom),
- pcs.coinContributions[i],
- ).amount;
- const refreshCost = getTotalRefreshCost(
- allDenoms,
- DenominationRecord.toDenomInfo(denom),
- amountLeft,
- );
- costs.push(Amounts.parseOrThrow(pcs.coinContributions[i]));
- costs.push(refreshCost);
- }
- const zero = Amounts.zeroOfAmount(pcs.paymentAmount);
- return Amounts.sum([zero, ...costs]).amount;
- });
-}
-
-export interface CoinSelectionRequest {
- amount: AmountJson;
-
- allowedAuditors: AllowedAuditorInfo[];
- allowedExchanges: AllowedExchangeInfo[];
-
- /**
- * Timestamp of the contract.
- */
- timestamp: TalerProtocolTimestamp;
-
- wireMethod: string;
-
- wireFeeAmortization: number;
-
- maxWireFee: AmountJson;
-
- maxDepositFee: AmountJson;
-
- /**
- * Minimum age requirement for the coin selection.
- *
- * When present, only select coins with either no age restriction
- * or coins with an age commitment that matches the minimum age.
- */
- minimumAge?: number;
-}
-
-async function failProposalPermanently(
- ws: InternalWalletState,
- proposalId: string,
- err: TalerErrorDetail,
-): Promise<void> {
- await ws.db
- .mktx((x) => [x.purchases])
- .runReadWrite(async (tx) => {
- const p = await tx.purchases.get(proposalId);
- if (!p) {
- return;
- }
- p.purchaseStatus = PurchaseStatus.ProposalDownloadFailed;
- await tx.purchases.put(p);
- });
-}
-
-function getProposalRequestTimeout(retryInfo?: RetryInfo): Duration {
- return Duration.clamp({
- lower: Duration.fromSpec({ seconds: 1 }),
- upper: Duration.fromSpec({ seconds: 60 }),
- value: retryInfo ? RetryInfo.getDuration(retryInfo) : Duration.fromSpec({}),
- });
-}
-
-function getPayRequestTimeout(purchase: PurchaseRecord): Duration {
- return Duration.multiply(
- { d_ms: 15000 },
- 1 + (purchase.payInfo?.payCoinSelection.coinPubs.length ?? 0) / 5,
- );
-}
-
-/**
- * Return the proposal download data for a purchase, throw if not available.
- *
- * (Async since in the future this will query the DB.)
- */
-export async function expectProposalDownload(
- ws: InternalWalletState,
- p: PurchaseRecord,
- parentTx?: GetReadOnlyAccess<{
- contractTerms: typeof WalletStoresV1.contractTerms;
- }>,
-): Promise<{
- contractData: WalletContractData;
- contractTermsRaw: any;
-}> {
- if (!p.download) {
- throw Error("expected proposal to be downloaded");
- }
- 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,
- };
- }
-
- if (parentTx) {
- return getFromTransaction(parentTx);
- }
- return await ws.db
- .mktx((x) => [x.contractTerms])
- .runReadOnly(getFromTransaction);
-}
-
-export function extractContractData(
- parsedContractTerms: MerchantContractTerms,
- contractTermsHash: string,
- merchantSig: string,
-): WalletContractData {
- const amount = Amounts.parseOrThrow(parsedContractTerms.amount);
- let maxWireFee: AmountJson;
- if (parsedContractTerms.max_wire_fee) {
- maxWireFee = Amounts.parseOrThrow(parsedContractTerms.max_wire_fee);
- } else {
- maxWireFee = Amounts.zeroOfCurrency(amount.currency);
- }
- return {
- amount: Amounts.stringify(amount),
- contractTermsHash: contractTermsHash,
- fulfillmentUrl: parsedContractTerms.fulfillment_url ?? "",
- merchantBaseUrl: parsedContractTerms.merchant_base_url,
- merchantPub: parsedContractTerms.merchant_pub,
- merchantSig,
- orderId: parsedContractTerms.order_id,
- summary: parsedContractTerms.summary,
- autoRefund: parsedContractTerms.auto_refund,
- maxWireFee: Amounts.stringify(maxWireFee),
- payDeadline: parsedContractTerms.pay_deadline,
- refundDeadline: parsedContractTerms.refund_deadline,
- wireFeeAmortization: parsedContractTerms.wire_fee_amortization || 1,
- allowedAuditors: parsedContractTerms.auditors.map((x) => ({
- auditorBaseUrl: x.url,
- auditorPub: x.auditor_pub,
- })),
- allowedExchanges: parsedContractTerms.exchanges.map((x) => ({
- exchangeBaseUrl: x.url,
- exchangePub: x.master_pub,
- })),
- timestamp: parsedContractTerms.timestamp,
- wireMethod: parsedContractTerms.wire_method,
- wireInfoHash: parsedContractTerms.h_wire,
- maxDepositFee: Amounts.stringify(parsedContractTerms.max_fee),
- merchant: parsedContractTerms.merchant,
- products: parsedContractTerms.products,
- summaryI18n: parsedContractTerms.summary_i18n,
- minimumAge: parsedContractTerms.minimum_age,
- deliveryDate: parsedContractTerms.delivery_date,
- deliveryLocation: parsedContractTerms.delivery_location,
- };
-}
-
-export async function processDownloadProposal(
- ws: InternalWalletState,
- proposalId: string,
- options: object = {},
-): Promise<OperationAttemptResult> {
- const proposal = await ws.db
- .mktx((x) => [x.purchases])
- .runReadOnly(async (tx) => {
- return await tx.purchases.get(proposalId);
- });
-
- if (!proposal) {
- return {
- type: OperationAttemptResultType.Finished,
- result: undefined,
- };
- }
-
- if (proposal.purchaseStatus != PurchaseStatus.DownloadingProposal) {
- return {
- type: OperationAttemptResultType.Finished,
- result: undefined,
- };
- }
-
- const orderClaimUrl = new URL(
- `orders/${proposal.orderId}/claim`,
- proposal.merchantBaseUrl,
- ).href;
- logger.trace("downloading contract from '" + orderClaimUrl + "'");
-
- const requestBody: {
- nonce: string;
- token?: string;
- } = {
- nonce: proposal.noncePub,
- };
- if (proposal.claimToken) {
- requestBody.token = proposal.claimToken;
- }
-
- const opId = RetryTags.forPay(proposal);
- const retryRecord = await ws.db
- .mktx((x) => [x.operationRetries])
- .runReadOnly(async (tx) => {
- return tx.operationRetries.get(opId);
- });
-
- // FIXME: Do this in the background using the new return value
- const httpResponse = await ws.http.postJson(orderClaimUrl, requestBody, {
- timeout: getProposalRequestTimeout(retryRecord?.retryInfo),
- });
- const r = await readSuccessResponseJsonOrErrorCode(
- httpResponse,
- codecForProposal(),
- );
- if (r.isError) {
- switch (r.talerErrorResponse.code) {
- case TalerErrorCode.MERCHANT_POST_ORDERS_ID_CLAIM_ALREADY_CLAIMED:
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_ORDER_ALREADY_CLAIMED,
- {
- orderId: proposal.orderId,
- claimUrl: orderClaimUrl,
- },
- "order already claimed (likely by other wallet)",
- );
- default:
- throwUnexpectedRequestError(httpResponse, r.talerErrorResponse);
- }
- }
- const proposalResp = r.response;
-
- // The proposalResp contains the contract terms as raw JSON,
- // as the coded to parse them doesn't necessarily round-trip.
- // We need this raw JSON to compute the contract terms hash.
-
- // FIXME: Do better error handling, check if the
- // contract terms have all their forgettable information still
- // present. The wallet should never accept contract terms
- // with missing information from the merchant.
-
- const isWellFormed = ContractTermsUtil.validateForgettable(
- proposalResp.contract_terms,
- );
-
- if (!isWellFormed) {
- logger.trace(
- `malformed contract terms: ${j2s(proposalResp.contract_terms)}`,
- );
- const err = makeErrorDetail(
- TalerErrorCode.WALLET_CONTRACT_TERMS_MALFORMED,
- {},
- "validation for well-formedness failed",
- );
- await failProposalPermanently(ws, proposalId, err);
- throw makePendingOperationFailedError(
- err,
- TransactionType.Payment,
- proposalId,
- );
- }
-
- const contractTermsHash = ContractTermsUtil.hashContractTerms(
- proposalResp.contract_terms,
- );
-
- logger.info(`Contract terms hash: ${contractTermsHash}`);
-
- let parsedContractTerms: MerchantContractTerms;
-
- try {
- parsedContractTerms = codecForMerchantContractTerms().decode(
- proposalResp.contract_terms,
- );
- } catch (e) {
- const err = makeErrorDetail(
- TalerErrorCode.WALLET_CONTRACT_TERMS_MALFORMED,
- {},
- `schema validation failed: ${e}`,
- );
- await failProposalPermanently(ws, proposalId, err);
- throw makePendingOperationFailedError(
- err,
- TransactionType.Payment,
- proposalId,
- );
- }
-
- const sigValid = await ws.cryptoApi.isValidContractTermsSignature({
- contractTermsHash,
- merchantPub: parsedContractTerms.merchant_pub,
- sig: proposalResp.sig,
- });
-
- if (!sigValid) {
- const err = makeErrorDetail(
- TalerErrorCode.WALLET_CONTRACT_TERMS_SIGNATURE_INVALID,
- {
- merchantPub: parsedContractTerms.merchant_pub,
- orderId: parsedContractTerms.order_id,
- },
- "merchant's signature on contract terms is invalid",
- );
- await failProposalPermanently(ws, proposalId, err);
- throw makePendingOperationFailedError(
- err,
- TransactionType.Payment,
- proposalId,
- );
- }
-
- const fulfillmentUrl = parsedContractTerms.fulfillment_url;
-
- const baseUrlForDownload = proposal.merchantBaseUrl;
- const baseUrlFromContractTerms = parsedContractTerms.merchant_base_url;
-
- if (baseUrlForDownload !== baseUrlFromContractTerms) {
- const err = makeErrorDetail(
- TalerErrorCode.WALLET_CONTRACT_TERMS_BASE_URL_MISMATCH,
- {
- baseUrlForDownload,
- baseUrlFromContractTerms,
- },
- "merchant base URL mismatch",
- );
- await failProposalPermanently(ws, proposalId, err);
- throw makePendingOperationFailedError(
- err,
- TransactionType.Payment,
- proposalId,
- );
- }
-
- const contractData = extractContractData(
- parsedContractTerms,
- contractTermsHash,
- proposalResp.sig,
- );
-
- logger.trace(`extracted contract data: ${j2s(contractData)}`);
-
- await ws.db
- .mktx((x) => [x.purchases, x.contractTerms])
- .runReadWrite(async (tx) => {
- const p = await tx.purchases.get(proposalId);
- if (!p) {
- return;
- }
- if (p.purchaseStatus !== PurchaseStatus.DownloadingProposal) {
- return;
- }
- p.download = {
- contractTermsHash,
- contractTermsMerchantSig: contractData.merchantSig,
- currency: Amounts.currencyOf(contractData.amount),
- fulfillmentUrl: contractData.fulfillmentUrl,
- };
- await tx.contractTerms.put({
- h: contractTermsHash,
- contractTermsRaw: proposalResp.contract_terms,
- });
- if (
- fulfillmentUrl &&
- (fulfillmentUrl.startsWith("http://") ||
- fulfillmentUrl.startsWith("https://"))
- ) {
- const differentPurchase =
- await tx.purchases.indexes.byFulfillmentUrl.get(fulfillmentUrl);
- if (differentPurchase) {
- logger.warn("repurchase detected");
- p.purchaseStatus = PurchaseStatus.RepurchaseDetected;
- p.repurchaseProposalId = differentPurchase.proposalId;
- await tx.purchases.put(p);
- return;
- }
- }
- p.purchaseStatus = PurchaseStatus.Proposed;
- await tx.purchases.put(p);
- });
-
- ws.notify({
- type: NotificationType.ProposalDownloaded,
- proposalId: proposal.proposalId,
- });
-
- return {
- type: OperationAttemptResultType.Finished,
- result: undefined,
- };
-}
-
-/**
- * Download a proposal and store it in the database.
- * Returns an id for it to retrieve it later.
- *
- * @param sessionId Current session ID, if the proposal is being
- * downloaded in the context of a session ID.
- */
-async function startDownloadProposal(
- ws: InternalWalletState,
- merchantBaseUrl: string,
- orderId: string,
- sessionId: string | undefined,
- claimToken: string | undefined,
- noncePriv: string | undefined,
-): Promise<string> {
- const oldProposal = await ws.db
- .mktx((x) => [x.purchases])
- .runReadOnly(async (tx) => {
- return tx.purchases.indexes.byUrlAndOrderId.get([
- merchantBaseUrl,
- orderId,
- ]);
- });
-
- /* If we have already claimed this proposal with the same sessionId
- * nonce and claim token, reuse it. */
- if (
- oldProposal &&
- oldProposal.downloadSessionId === sessionId &&
- (!noncePriv || oldProposal.noncePriv === noncePriv) &&
- oldProposal.claimToken === claimToken
- ) {
- await processDownloadProposal(ws, oldProposal.proposalId);
- return oldProposal.proposalId;
- }
-
- let noncePair: EddsaKeypair;
- if (noncePriv) {
- noncePair = {
- priv: noncePriv,
- pub: (await ws.cryptoApi.eddsaGetPublic({ priv: noncePriv })).pub,
- };
- } else {
- noncePair = await ws.cryptoApi.createEddsaKeypair({});
- }
-
- const { priv, pub } = noncePair;
- const proposalId = encodeCrock(getRandomBytes(32));
-
- const proposalRecord: PurchaseRecord = {
- download: undefined,
- noncePriv: priv,
- noncePub: pub,
- claimToken,
- timestamp: AbsoluteTime.toTimestamp(AbsoluteTime.now()),
- merchantBaseUrl,
- orderId,
- proposalId: proposalId,
- purchaseStatus: PurchaseStatus.DownloadingProposal,
- repurchaseProposalId: undefined,
- downloadSessionId: sessionId,
- autoRefundDeadline: undefined,
- lastSessionId: undefined,
- merchantPaySig: undefined,
- payInfo: undefined,
- refundAmountAwaiting: undefined,
- refunds: {},
- timestampAccept: undefined,
- timestampFirstSuccessfulPay: undefined,
- timestampLastRefundStatus: undefined,
- pendingRemovedCoinPubs: undefined,
- };
-
- await ws.db
- .mktx((x) => [x.purchases])
- .runReadWrite(async (tx) => {
- const existingRecord = await tx.purchases.indexes.byUrlAndOrderId.get([
- merchantBaseUrl,
- orderId,
- ]);
- if (existingRecord) {
- // Created concurrently
- return;
- }
- await tx.purchases.put(proposalRecord);
- });
-
- await processDownloadProposal(ws, proposalId);
- return proposalId;
-}
-
-async function storeFirstPaySuccess(
- ws: InternalWalletState,
- proposalId: string,
- sessionId: string | undefined,
- paySig: string,
-): Promise<void> {
- const now = AbsoluteTime.toTimestamp(AbsoluteTime.now());
- await ws.db
- .mktx((x) => [x.purchases, x.contractTerms])
- .runReadWrite(async (tx) => {
- const purchase = await tx.purchases.get(proposalId);
-
- if (!purchase) {
- logger.warn("purchase does not exist anymore");
- return;
- }
- const isFirst = purchase.timestampFirstSuccessfulPay === undefined;
- if (!isFirst) {
- logger.warn("payment success already stored");
- return;
- }
- if (purchase.purchaseStatus === PurchaseStatus.Paying) {
- purchase.purchaseStatus = PurchaseStatus.Paid;
- }
- purchase.timestampFirstSuccessfulPay = now;
- purchase.lastSessionId = sessionId;
- purchase.merchantPaySig = paySig;
- const dl = purchase.download;
- checkDbInvariant(!!dl);
- const contractTermsRecord = await tx.contractTerms.get(
- dl.contractTermsHash,
- );
- checkDbInvariant(!!contractTermsRecord);
- const contractData = extractContractData(
- contractTermsRecord.contractTermsRaw,
- dl.contractTermsHash,
- dl.contractTermsMerchantSig,
- );
- const protoAr = contractData.autoRefund;
- if (protoAr) {
- const ar = Duration.fromTalerProtocolDuration(protoAr);
- logger.info("auto_refund present");
- purchase.purchaseStatus = PurchaseStatus.QueryingAutoRefund;
- purchase.autoRefundDeadline = AbsoluteTime.toTimestamp(
- AbsoluteTime.addDuration(AbsoluteTime.now(), ar),
- );
- }
- await tx.purchases.put(purchase);
- });
-}
-
-async function storePayReplaySuccess(
- ws: InternalWalletState,
- proposalId: string,
- sessionId: string | undefined,
-): Promise<void> {
- await ws.db
- .mktx((x) => [x.purchases])
- .runReadWrite(async (tx) => {
- const purchase = await tx.purchases.get(proposalId);
-
- if (!purchase) {
- logger.warn("purchase does not exist anymore");
- return;
- }
- const isFirst = purchase.timestampFirstSuccessfulPay === undefined;
- if (isFirst) {
- throw Error("invalid payment state");
- }
- if (
- purchase.purchaseStatus === PurchaseStatus.Paying ||
- purchase.purchaseStatus === PurchaseStatus.PayingReplay
- ) {
- purchase.purchaseStatus = PurchaseStatus.Paid;
- }
- purchase.lastSessionId = sessionId;
- await tx.purchases.put(purchase);
- });
-}
-
-/**
- * Handle a 409 Conflict response from the merchant.
- *
- * We do this by going through the coin history provided by the exchange and
- * (1) verifying the signatures from the exchange
- * (2) adjusting the remaining coin value and refreshing it
- * (3) re-do coin selection with the bad coin removed
- */
-async function handleInsufficientFunds(
- ws: InternalWalletState,
- proposalId: string,
- err: TalerErrorDetail,
-): Promise<void> {
- logger.trace("handling insufficient funds, trying to re-select coins");
-
- const proposal = await ws.db
- .mktx((x) => [x.purchases])
- .runReadOnly(async (tx) => {
- return tx.purchases.get(proposalId);
- });
- if (!proposal) {
- return;
- }
-
- logger.trace(`got error details: ${j2s(err)}`);
-
- const exchangeReply = (err as any).exchange_reply;
- if (
- exchangeReply.code !== TalerErrorCode.EXCHANGE_GENERIC_INSUFFICIENT_FUNDS
- ) {
- // FIXME: set as failed
- if (logger.shouldLogTrace()) {
- logger.trace("got exchange error reply (see below)");
- logger.trace(j2s(exchangeReply));
- }
- throw Error(`unable to handle /pay error response (${exchangeReply.code})`);
- }
-
- const brokenCoinPub = (exchangeReply as any).coin_pub;
- logger.trace(`excluded broken coin pub=${brokenCoinPub}`);
-
- if (!brokenCoinPub) {
- throw new TalerProtocolViolationError();
- }
-
- const { contractData } = await expectProposalDownload(ws, proposal);
-
- const prevPayCoins: PreviousPayCoins = [];
-
- const payInfo = proposal.payInfo;
- if (!payInfo) {
- return;
- }
-
- const payCoinSelection = payInfo.payCoinSelection;
-
- await ws.db
- .mktx((x) => [x.coins, x.denominations])
- .runReadOnly(async (tx) => {
- for (let i = 0; i < payCoinSelection.coinPubs.length; i++) {
- const coinPub = payCoinSelection.coinPubs[i];
- if (coinPub === brokenCoinPub) {
- continue;
- }
- const contrib = payCoinSelection.coinContributions[i];
- const coin = await tx.coins.get(coinPub);
- if (!coin) {
- continue;
- }
- const denom = await tx.denominations.get([
- coin.exchangeBaseUrl,
- coin.denomPubHash,
- ]);
- if (!denom) {
- continue;
- }
- prevPayCoins.push({
- coinPub,
- contribution: Amounts.parseOrThrow(contrib),
- exchangeBaseUrl: coin.exchangeBaseUrl,
- feeDeposit: Amounts.parseOrThrow(denom.fees.feeDeposit),
- });
- }
- });
-
- const res = await selectPayCoinsNew(ws, {
- auditors: contractData.allowedAuditors,
- exchanges: contractData.allowedExchanges,
- wireMethod: contractData.wireMethod,
- contractTermsAmount: Amounts.parseOrThrow(contractData.amount),
- depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee),
- wireFeeAmortization: contractData.wireFeeAmortization ?? 1,
- wireFeeLimit: Amounts.parseOrThrow(contractData.maxWireFee),
- prevPayCoins,
- requiredMinimumAge: contractData.minimumAge,
- });
-
- if (!res) {
- logger.trace("insufficient funds for coin re-selection");
- return;
- }
-
- logger.trace("re-selected coins");
-
- await ws.db
- .mktx((x) => [
- x.purchases,
- x.coins,
- x.coinAvailability,
- x.denominations,
- x.refreshGroups,
- ])
- .runReadWrite(async (tx) => {
- const p = await tx.purchases.get(proposalId);
- if (!p) {
- return;
- }
- const payInfo = p.payInfo;
- if (!payInfo) {
- return;
- }
- payInfo.payCoinSelection = res;
- payInfo.payCoinSelection = res;
- payInfo.payCoinSelectionUid = encodeCrock(getRandomBytes(32));
- await tx.purchases.put(p);
- await spendCoins(ws, tx, {
- allocationId: `txn:proposal:${p.proposalId}`,
- coinPubs: payInfo.payCoinSelection.coinPubs,
- contributions: payInfo.payCoinSelection.coinContributions.map((x) =>
- Amounts.parseOrThrow(x),
- ),
- refreshReason: RefreshReason.PayMerchant,
- });
- });
-}
-
-async function unblockBackup(
- ws: InternalWalletState,
- proposalId: string,
-): Promise<void> {
- await ws.db
- .mktx((x) => [x.backupProviders])
- .runReadWrite(async (tx) => {
- await tx.backupProviders.indexes.byPaymentProposalId
- .iter(proposalId)
- .forEachAsync(async (bp) => {
- bp.state = {
- tag: BackupProviderStateTag.Ready,
- nextBackupTimestamp: TalerProtocolTimestamp.now(),
- };
- tx.backupProviders.put(bp);
- });
- });
-}
-
-export interface SelectPayCoinRequestNg {
- exchanges: AllowedExchangeInfo[];
- auditors: AllowedAuditorInfo[];
- wireMethod: string;
- contractTermsAmount: AmountJson;
- depositFeeLimit: AmountJson;
- wireFeeLimit: AmountJson;
- wireFeeAmortization: number;
- prevPayCoins?: PreviousPayCoins;
- requiredMinimumAge?: number;
- forcedSelection?: ForcedCoinSel;
-}
-
-export type AvailableDenom = DenominationInfo & {
- maxAge: number;
- numAvailable: number;
-};
-
-export async function selectCandidates(
- ws: InternalWalletState,
- req: SelectPayCoinRequestNg,
-): Promise<[AvailableDenom[], Record<string, AmountJson>]> {
- return await ws.db
- .mktx((x) => [
- x.exchanges,
- x.exchangeDetails,
- x.denominations,
- x.coinAvailability,
- ])
- .runReadOnly(async (tx) => {
- const denoms: AvailableDenom[] = [];
- const exchanges = await tx.exchanges.iter().toArray();
- const wfPerExchange: Record<string, AmountJson> = {};
- for (const exchange of exchanges) {
- const exchangeDetails = await getExchangeDetails(tx, exchange.baseUrl);
- if (exchangeDetails?.currency !== req.contractTermsAmount.currency) {
- continue;
- }
- let wireMethodSupported = false;
- for (const acc of exchangeDetails.wireInfo.accounts) {
- const pp = parsePaytoUri(acc.payto_uri);
- checkLogicInvariant(!!pp);
- if (pp.targetType === req.wireMethod) {
- wireMethodSupported = true;
- break;
- }
- }
- if (!wireMethodSupported) {
- break;
- }
- exchangeDetails.wireInfo.accounts;
- let accepted = false;
- for (const allowedExchange of req.exchanges) {
- if (allowedExchange.exchangePub === exchangeDetails.masterPublicKey) {
- accepted = true;
- break;
- }
- }
- for (const allowedAuditor of req.auditors) {
- for (const providedAuditor of exchangeDetails.auditors) {
- if (allowedAuditor.auditorPub === providedAuditor.auditor_pub) {
- accepted = true;
- break;
- }
- }
- }
- if (!accepted) {
- continue;
- }
- let ageLower = 0;
- let ageUpper = AgeRestriction.AGE_UNRESTRICTED;
- if (req.requiredMinimumAge) {
- ageLower = req.requiredMinimumAge;
- }
- const myExchangeDenoms =
- await tx.coinAvailability.indexes.byExchangeAgeAvailability.getAll(
- GlobalIDB.KeyRange.bound(
- [exchangeDetails.exchangeBaseUrl, ageLower, 1],
- [
- exchangeDetails.exchangeBaseUrl,
- ageUpper,
- Number.MAX_SAFE_INTEGER,
- ],
- ),
- );
- // FIXME: Check that the individual denomination is audited!
- // FIXME: Should we exclude denominations that are
- // not spendable anymore?
- for (const denomAvail of myExchangeDenoms) {
- const denom = await tx.denominations.get([
- denomAvail.exchangeBaseUrl,
- denomAvail.denomPubHash,
- ]);
- checkDbInvariant(!!denom);
- if (denom.isRevoked || !denom.isOffered) {
- continue;
- }
- denoms.push({
- ...DenominationRecord.toDenomInfo(denom),
- numAvailable: denomAvail.freshCoinCount ?? 0,
- maxAge: denomAvail.maxAge,
- });
- }
- }
- // Sort by available amount (descending), deposit fee (ascending) and
- // denomPub (ascending) if deposit fee is the same
- // (to guarantee deterministic results)
- denoms.sort(
- (o1, o2) =>
- -Amounts.cmp(o1.value, o2.value) ||
- Amounts.cmp(o1.feeDeposit, o2.feeDeposit) ||
- strcmp(o1.denomPubHash, o2.denomPubHash),
- );
- return [denoms, wfPerExchange];
- });
-}
-
-function makeAvailabilityKey(
- exchangeBaseUrl: string,
- denomPubHash: string,
- maxAge: number,
-): string {
- return `${denomPubHash};${maxAge};${exchangeBaseUrl}`;
-}
-
-/**
- * Selection result.
- */
-interface SelResult {
- /**
- * Map from an availability key
- * to an array of contributions.
- */
- [avKey: string]: {
- exchangeBaseUrl: string;
- denomPubHash: string;
- maxAge: number;
- contributions: AmountJson[];
- };
-}
-
-export function selectGreedy(
- req: SelectPayCoinRequestNg,
- candidateDenoms: AvailableDenom[],
- wireFeesPerExchange: Record<string, AmountJson>,
- tally: CoinSelectionTally,
-): SelResult | undefined {
- const { wireFeeAmortization } = req;
- const selectedDenom: SelResult = {};
- for (const aci of candidateDenoms) {
- const contributions: AmountJson[] = [];
- for (let i = 0; i < aci.numAvailable; i++) {
- // Don't use this coin if depositing it is more expensive than
- // the amount it would give the merchant.
- if (Amounts.cmp(aci.feeDeposit, aci.value) > 0) {
- continue;
- }
-
- if (Amounts.isZero(tally.amountPayRemaining)) {
- // We have spent enough!
- break;
- }
-
- tally = tallyFees(
- tally,
- wireFeesPerExchange,
- wireFeeAmortization,
- aci.exchangeBaseUrl,
- Amounts.parseOrThrow(aci.feeDeposit),
- );
-
- let coinSpend = Amounts.max(
- Amounts.min(tally.amountPayRemaining, aci.value),
- aci.feeDeposit,
- );
-
- tally.amountPayRemaining = Amounts.sub(
- tally.amountPayRemaining,
- coinSpend,
- ).amount;
- contributions.push(coinSpend);
- }
-
- if (contributions.length) {
- const avKey = makeAvailabilityKey(
- aci.exchangeBaseUrl,
- aci.denomPubHash,
- aci.maxAge,
- );
- let sd = selectedDenom[avKey];
- if (!sd) {
- sd = {
- contributions: [],
- denomPubHash: aci.denomPubHash,
- exchangeBaseUrl: aci.exchangeBaseUrl,
- maxAge: aci.maxAge,
- };
- }
- sd.contributions.push(...contributions);
- selectedDenom[avKey] = sd;
- }
-
- if (Amounts.isZero(tally.amountPayRemaining)) {
- return selectedDenom;
- }
- }
- return undefined;
-}
-
-export function selectForced(
- req: SelectPayCoinRequestNg,
- candidateDenoms: AvailableDenom[],
-): SelResult | undefined {
- const selectedDenom: SelResult = {};
-
- const forcedSelection = req.forcedSelection;
- checkLogicInvariant(!!forcedSelection);
-
- for (const forcedCoin of forcedSelection.coins) {
- let found = false;
- for (const aci of candidateDenoms) {
- if (aci.numAvailable <= 0) {
- continue;
- }
- if (Amounts.cmp(aci.value, forcedCoin.value) === 0) {
- aci.numAvailable--;
- const avKey = makeAvailabilityKey(
- aci.exchangeBaseUrl,
- aci.denomPubHash,
- aci.maxAge,
- );
- let sd = selectedDenom[avKey];
- if (!sd) {
- sd = {
- contributions: [],
- denomPubHash: aci.denomPubHash,
- exchangeBaseUrl: aci.exchangeBaseUrl,
- maxAge: aci.maxAge,
- };
- }
- sd.contributions.push(Amounts.parseOrThrow(forcedCoin.value));
- selectedDenom[avKey] = sd;
- found = true;
- break;
- }
- }
- if (!found) {
- throw Error("can't find coin for forced coin selection");
- }
- }
-
- return selectedDenom;
-}
-
-/**
- * Given a list of candidate coins, select coins to spend under the merchant's
- * constraints.
- *
- * The prevPayCoins can be specified to "repair" a coin selection
- * by adding additional coins, after a broken (e.g. double-spent) coin
- * has been removed from the selection.
- *
- * This function is only exported for the sake of unit tests.
- */
-export async function selectPayCoinsNew(
- ws: InternalWalletState,
- req: SelectPayCoinRequestNg,
-): Promise<PayCoinSelection | undefined> {
- const {
- contractTermsAmount,
- depositFeeLimit,
- wireFeeLimit,
- wireFeeAmortization,
- } = req;
-
- const [candidateDenoms, wireFeesPerExchange] = await selectCandidates(
- ws,
- req,
- );
-
- // logger.trace(`candidate denoms: ${j2s(candidateDenoms)}`);
-
- const coinPubs: string[] = [];
- const coinContributions: AmountJson[] = [];
- const currency = contractTermsAmount.currency;
-
- let tally: CoinSelectionTally = {
- amountPayRemaining: contractTermsAmount,
- amountWireFeeLimitRemaining: wireFeeLimit,
- amountDepositFeeLimitRemaining: depositFeeLimit,
- customerDepositFees: Amounts.zeroOfCurrency(currency),
- customerWireFees: Amounts.zeroOfCurrency(currency),
- wireFeeCoveredForExchange: new Set(),
- };
-
- const prevPayCoins = req.prevPayCoins ?? [];
-
- // Look at existing pay coin selection and tally up
- for (const prev of prevPayCoins) {
- tally = tallyFees(
- tally,
- wireFeesPerExchange,
- wireFeeAmortization,
- prev.exchangeBaseUrl,
- prev.feeDeposit,
- );
- tally.amountPayRemaining = Amounts.sub(
- tally.amountPayRemaining,
- prev.contribution,
- ).amount;
-
- coinPubs.push(prev.coinPub);
- coinContributions.push(prev.contribution);
- }
-
- let selectedDenom: SelResult | undefined;
- if (req.forcedSelection) {
- selectedDenom = selectForced(req, candidateDenoms);
- } else {
- // FIXME: Here, we should select coins in a smarter way.
- // Instead of always spending the next-largest coin,
- // we should try to find the smallest coin that covers the
- // amount.
- selectedDenom = selectGreedy(
- req,
- candidateDenoms,
- wireFeesPerExchange,
- tally,
- );
- }
-
- if (!selectedDenom) {
- return undefined;
- }
-
- const finalSel = selectedDenom;
-
- logger.trace(`coin selection request ${j2s(req)}`);
- logger.trace(`selected coins (via denoms) for payment: ${j2s(finalSel)}`);
-
- await ws.db
- .mktx((x) => [x.coins, x.denominations])
- .runReadOnly(async (tx) => {
- for (const dph of Object.keys(finalSel)) {
- const selInfo = finalSel[dph];
- const numRequested = selInfo.contributions.length;
- const query = [
- selInfo.exchangeBaseUrl,
- selInfo.denomPubHash,
- selInfo.maxAge,
- CoinStatus.Fresh,
- ];
- logger.info(`query: ${j2s(query)}`);
- const coins =
- await tx.coins.indexes.byExchangeDenomPubHashAndAgeAndStatus.getAll(
- query,
- numRequested,
- );
- if (coins.length != numRequested) {
- throw Error(
- `coin selection failed (not available anymore, got only ${coins.length}/${numRequested})`,
- );
- }
- coinPubs.push(...coins.map((x) => x.coinPub));
- coinContributions.push(...selInfo.contributions);
- }
- });
-
- return {
- paymentAmount: Amounts.stringify(contractTermsAmount),
- coinContributions: coinContributions.map((x) => Amounts.stringify(x)),
- coinPubs,
- customerDepositFees: Amounts.stringify(tally.customerDepositFees),
- customerWireFees: Amounts.stringify(tally.customerWireFees),
- };
-}
-
-export async function checkPaymentByProposalId(
- ws: InternalWalletState,
- proposalId: string,
- sessionId?: string,
-): Promise<PreparePayResult> {
- let proposal = await ws.db
- .mktx((x) => [x.purchases])
- .runReadOnly(async (tx) => {
- return tx.purchases.get(proposalId);
- });
- if (!proposal) {
- throw Error(`could not get proposal ${proposalId}`);
- }
- if (proposal.purchaseStatus === PurchaseStatus.RepurchaseDetected) {
- const existingProposalId = proposal.repurchaseProposalId;
- if (!existingProposalId) {
- throw Error("invalid proposal state");
- }
- logger.trace("using existing purchase for same product");
- proposal = await ws.db
- .mktx((x) => [x.purchases])
- .runReadOnly(async (tx) => {
- return tx.purchases.get(existingProposalId);
- });
- if (!proposal) {
- throw Error("existing proposal is in wrong state");
- }
- }
- const d = await expectProposalDownload(ws, proposal);
- const contractData = d.contractData;
- const merchantSig = d.contractData.merchantSig;
- if (!merchantSig) {
- throw Error("BUG: proposal is in invalid state");
- }
-
- proposalId = proposal.proposalId;
-
- const talerUri = constructPayUri(
- proposal.merchantBaseUrl,
- proposal.orderId,
- proposal.lastSessionId ?? proposal.downloadSessionId ?? "",
- proposal.claimToken,
- proposal.noncePriv,
- );
-
- // First check if we already paid for it.
- const purchase = await ws.db
- .mktx((x) => [x.purchases])
- .runReadOnly(async (tx) => {
- return tx.purchases.get(proposalId);
- });
-
- if (!purchase || purchase.purchaseStatus === PurchaseStatus.Proposed) {
- // If not already paid, check if we could pay for it.
- const res = await selectPayCoinsNew(ws, {
- auditors: contractData.allowedAuditors,
- exchanges: contractData.allowedExchanges,
- contractTermsAmount: Amounts.parseOrThrow(contractData.amount),
- depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee),
- wireFeeAmortization: contractData.wireFeeAmortization ?? 1,
- wireFeeLimit: Amounts.parseOrThrow(contractData.maxWireFee),
- prevPayCoins: [],
- requiredMinimumAge: contractData.minimumAge,
- wireMethod: contractData.wireMethod,
- });
-
- if (!res) {
- logger.info("not allowing payment, insufficient coins");
- return {
- status: PreparePayResultType.InsufficientBalance,
- contractTerms: d.contractTermsRaw,
- proposalId: proposal.proposalId,
- noncePriv: proposal.noncePriv,
- amountRaw: Amounts.stringify(d.contractData.amount),
- talerUri,
- };
- }
-
- const totalCost = await getTotalPaymentCost(ws, res);
- logger.trace("costInfo", totalCost);
- logger.trace("coinsForPayment", res);
-
- return {
- status: PreparePayResultType.PaymentPossible,
- contractTerms: d.contractTermsRaw,
- proposalId: proposal.proposalId,
- noncePriv: proposal.noncePriv,
- amountEffective: Amounts.stringify(totalCost),
- amountRaw: Amounts.stringify(res.paymentAmount),
- contractTermsHash: d.contractData.contractTermsHash,
- talerUri,
- };
- }
-
- if (
- purchase.purchaseStatus === PurchaseStatus.Paid &&
- purchase.lastSessionId !== sessionId
- ) {
- logger.trace(
- "automatically re-submitting payment with different session ID",
- );
- logger.trace(`last: ${purchase.lastSessionId}, current: ${sessionId}`);
- await ws.db
- .mktx((x) => [x.purchases])
- .runReadWrite(async (tx) => {
- const p = await tx.purchases.get(proposalId);
- if (!p) {
- return;
- }
- p.lastSessionId = sessionId;
- p.purchaseStatus = PurchaseStatus.PayingReplay;
- await tx.purchases.put(p);
- });
- const r = await processPurchasePay(ws, proposalId, { forceNow: true });
- if (r.type !== OperationAttemptResultType.Finished) {
- // FIXME: This does not surface the original error
- throw Error("submitting pay failed");
- }
- const download = await expectProposalDownload(ws, purchase);
- return {
- status: PreparePayResultType.AlreadyConfirmed,
- contractTerms: download.contractTermsRaw,
- contractTermsHash: download.contractData.contractTermsHash,
- paid: true,
- amountRaw: Amounts.stringify(download.contractData.amount),
- amountEffective: Amounts.stringify(purchase.payInfo?.totalPayCost!),
- proposalId,
- talerUri,
- };
- } else if (!purchase.timestampFirstSuccessfulPay) {
- const download = await expectProposalDownload(ws, purchase);
- return {
- status: PreparePayResultType.AlreadyConfirmed,
- contractTerms: download.contractTermsRaw,
- contractTermsHash: download.contractData.contractTermsHash,
- paid: false,
- amountRaw: Amounts.stringify(download.contractData.amount),
- amountEffective: Amounts.stringify(purchase.payInfo?.totalPayCost!),
- proposalId,
- talerUri,
- };
- } else {
- const paid =
- purchase.purchaseStatus === PurchaseStatus.Paid ||
- purchase.purchaseStatus === PurchaseStatus.QueryingRefund ||
- purchase.purchaseStatus === PurchaseStatus.QueryingAutoRefund;
- const download = await expectProposalDownload(ws, purchase);
- return {
- status: PreparePayResultType.AlreadyConfirmed,
- contractTerms: download.contractTermsRaw,
- contractTermsHash: download.contractData.contractTermsHash,
- paid,
- amountRaw: Amounts.stringify(download.contractData.amount),
- amountEffective: Amounts.stringify(purchase.payInfo?.totalPayCost!),
- ...(paid ? { nextUrl: download.contractData.orderId } : {}),
- proposalId,
- talerUri,
- };
- }
-}
-
-export async function getContractTermsDetails(
- ws: InternalWalletState,
- proposalId: string,
-): Promise<WalletContractData> {
- const proposal = await ws.db
- .mktx((x) => [x.purchases])
- .runReadOnly(async (tx) => {
- return tx.purchases.get(proposalId);
- });
-
- if (!proposal) {
- throw Error(`proposal with id ${proposalId} not found`);
- }
-
- const d = await expectProposalDownload(ws, proposal);
-
- return d.contractData;
-}
-
-/**
- * Check if a payment for the given taler://pay/ URI is possible.
- *
- * If the payment is possible, the signature are already generated but not
- * yet send to the merchant.
- */
-export async function preparePayForUri(
- ws: InternalWalletState,
- talerPayUri: string,
-): Promise<PreparePayResult> {
- const uriResult = parsePayUri(talerPayUri);
-
- if (!uriResult) {
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_INVALID_TALER_PAY_URI,
- {
- talerPayUri,
- },
- `invalid taler://pay URI (${talerPayUri})`,
- );
- }
-
- const proposalId = await startDownloadProposal(
- ws,
- uriResult.merchantBaseUrl,
- uriResult.orderId,
- uriResult.sessionId,
- uriResult.claimToken,
- uriResult.noncePriv,
- );
-
- return checkPaymentByProposalId(ws, proposalId, uriResult.sessionId);
-}
-
-/**
- * Generate deposit permissions for a purchase.
- *
- * Accesses the database and the crypto worker.
- */
-export async function generateDepositPermissions(
- ws: InternalWalletState,
- payCoinSel: PayCoinSelection,
- contractData: WalletContractData,
-): Promise<CoinDepositPermission[]> {
- const depositPermissions: CoinDepositPermission[] = [];
- const coinWithDenom: Array<{
- coin: CoinRecord;
- denom: DenominationRecord;
- }> = [];
- await ws.db
- .mktx((x) => [x.coins, x.denominations])
- .runReadOnly(async (tx) => {
- for (let i = 0; i < payCoinSel.coinPubs.length; i++) {
- const coin = await tx.coins.get(payCoinSel.coinPubs[i]);
- if (!coin) {
- throw Error("can't pay, allocated coin not found anymore");
- }
- const denom = await tx.denominations.get([
- coin.exchangeBaseUrl,
- coin.denomPubHash,
- ]);
- if (!denom) {
- throw Error(
- "can't pay, denomination of allocated coin not found anymore",
- );
- }
- coinWithDenom.push({ coin, denom });
- }
- });
-
- for (let i = 0; i < payCoinSel.coinPubs.length; i++) {
- const { coin, denom } = coinWithDenom[i];
- let wireInfoHash: string;
- wireInfoHash = contractData.wireInfoHash;
- logger.trace(
- `signing deposit permission for coin with ageRestriction=${j2s(
- coin.ageCommitmentProof,
- )}`,
- );
- const dp = await ws.cryptoApi.signDepositPermission({
- coinPriv: coin.coinPriv,
- coinPub: coin.coinPub,
- contractTermsHash: contractData.contractTermsHash,
- denomPubHash: coin.denomPubHash,
- denomKeyType: denom.denomPub.cipher,
- denomSig: coin.denomSig,
- exchangeBaseUrl: coin.exchangeBaseUrl,
- feeDeposit: Amounts.parseOrThrow(denom.fees.feeDeposit),
- merchantPub: contractData.merchantPub,
- refundDeadline: contractData.refundDeadline,
- spendAmount: Amounts.parseOrThrow(payCoinSel.coinContributions[i]),
- timestamp: contractData.timestamp,
- wireInfoHash,
- ageCommitmentProof: coin.ageCommitmentProof,
- requiredMinimumAge: contractData.minimumAge,
- });
- depositPermissions.push(dp);
- }
- return depositPermissions;
-}
-
-/**
- * Run the operation handler for a payment
- * and return the result as a {@link ConfirmPayResult}.
- */
-export async function runPayForConfirmPay(
- ws: InternalWalletState,
- proposalId: string,
-): Promise<ConfirmPayResult> {
- const res = await processPurchasePay(ws, proposalId, { forceNow: true });
- switch (res.type) {
- case OperationAttemptResultType.Finished: {
- const purchase = await ws.db
- .mktx((x) => [x.purchases])
- .runReadOnly(async (tx) => {
- return tx.purchases.get(proposalId);
- });
- if (!purchase) {
- throw Error("purchase record not available anymore");
- }
- const d = await expectProposalDownload(ws, purchase);
- return {
- type: ConfirmPayResultType.Done,
- contractTerms: d.contractTermsRaw,
- transactionId: makeTransactionId(TransactionType.Payment, proposalId),
- };
- }
- case OperationAttemptResultType.Error: {
- // We hide transient errors from the caller.
- const opRetry = await ws.db
- .mktx((x) => [x.operationRetries])
- .runReadOnly(async (tx) =>
- tx.operationRetries.get(RetryTags.byPaymentProposalId(proposalId)),
- );
- const maxRetry = 3;
- const numRetry = opRetry?.retryInfo.retryCounter ?? 0;
- if (
- res.errorDetail.code ===
- TalerErrorCode.WALLET_PAY_MERCHANT_SERVER_ERROR &&
- numRetry < maxRetry
- ) {
- // Pretend the operation is pending instead of reporting
- // an error, but only up to maxRetry attempts.
- await storeOperationPending(
- ws,
- RetryTags.byPaymentProposalId(proposalId),
- );
- return {
- type: ConfirmPayResultType.Pending,
- lastError: opRetry?.lastError,
- transactionId: makeTransactionId(TransactionType.Payment, proposalId),
- };
- } else {
- // FIXME: allocate error code!
- await storeOperationError(
- ws,
- RetryTags.byPaymentProposalId(proposalId),
- res.errorDetail,
- );
- throw Error("payment failed");
- }
- }
- case OperationAttemptResultType.Pending:
- await storeOperationPending(
- ws,
- `${PendingTaskType.Purchase}:${proposalId}`,
- );
- return {
- type: ConfirmPayResultType.Pending,
- transactionId: makeTransactionId(TransactionType.Payment, proposalId),
- lastError: undefined,
- };
- case OperationAttemptResultType.Longpoll:
- throw Error("unexpected processPurchasePay result (longpoll)");
- default:
- assertUnreachable(res);
- }
-}
-
-/**
- * Confirm payment for a proposal previously claimed by the wallet.
- */
-export async function confirmPay(
- ws: InternalWalletState,
- proposalId: string,
- sessionIdOverride?: string,
- forcedCoinSel?: ForcedCoinSel,
-): Promise<ConfirmPayResult> {
- logger.trace(
- `executing confirmPay with proposalId ${proposalId} and sessionIdOverride ${sessionIdOverride}`,
- );
- const proposal = await ws.db
- .mktx((x) => [x.purchases])
- .runReadOnly(async (tx) => {
- return tx.purchases.get(proposalId);
- });
-
- if (!proposal) {
- throw Error(`proposal with id ${proposalId} not found`);
- }
-
- const d = await expectProposalDownload(ws, proposal);
- if (!d) {
- throw Error("proposal is in invalid state");
- }
-
- const existingPurchase = await ws.db
- .mktx((x) => [x.purchases])
- .runReadWrite(async (tx) => {
- const purchase = await tx.purchases.get(proposalId);
- if (
- purchase &&
- sessionIdOverride !== undefined &&
- sessionIdOverride != purchase.lastSessionId
- ) {
- logger.trace(`changing session ID to ${sessionIdOverride}`);
- purchase.lastSessionId = sessionIdOverride;
- if (purchase.purchaseStatus === PurchaseStatus.Paid) {
- purchase.purchaseStatus = PurchaseStatus.PayingReplay;
- }
- await tx.purchases.put(purchase);
- }
- return purchase;
- });
-
- if (existingPurchase && existingPurchase.payInfo) {
- logger.trace("confirmPay: submitting payment for existing purchase");
- return runPayForConfirmPay(ws, proposalId);
- }
-
- logger.trace("confirmPay: purchase record does not exist yet");
-
- const contractData = d.contractData;
-
- let maybeCoinSelection: PayCoinSelection | undefined = undefined;
-
- maybeCoinSelection = await selectPayCoinsNew(ws, {
- auditors: contractData.allowedAuditors,
- exchanges: contractData.allowedExchanges,
- wireMethod: contractData.wireMethod,
- contractTermsAmount: Amounts.parseOrThrow(contractData.amount),
- depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee),
- wireFeeAmortization: contractData.wireFeeAmortization ?? 1,
- wireFeeLimit: Amounts.parseOrThrow(contractData.maxWireFee),
- prevPayCoins: [],
- requiredMinimumAge: contractData.minimumAge,
- forcedSelection: forcedCoinSel,
- });
-
- logger.trace("coin selection result", maybeCoinSelection);
-
- if (!maybeCoinSelection) {
- // 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");
- }
-
- const coinSelection = maybeCoinSelection;
-
- const depositPermissions = await generateDepositPermissions(
- ws,
- coinSelection,
- d.contractData,
- );
-
- const payCostInfo = await getTotalPaymentCost(ws, coinSelection);
-
- let sessionId: string | undefined;
- if (sessionIdOverride) {
- sessionId = sessionIdOverride;
- } else {
- sessionId = proposal.downloadSessionId;
- }
-
- logger.trace(
- `recording payment on ${proposal.orderId} with session ID ${sessionId}`,
- );
-
- await ws.db
- .mktx((x) => [
- x.purchases,
- x.coins,
- x.refreshGroups,
- x.denominations,
- x.coinAvailability,
- ])
- .runReadWrite(async (tx) => {
- const p = await tx.purchases.get(proposal.proposalId);
- if (!p) {
- return;
- }
- switch (p.purchaseStatus) {
- case PurchaseStatus.Proposed:
- p.payInfo = {
- payCoinSelection: coinSelection,
- payCoinSelectionUid: encodeCrock(getRandomBytes(16)),
- totalPayCost: Amounts.stringify(payCostInfo),
- };
- p.lastSessionId = sessionId;
- p.timestampAccept = TalerProtocolTimestamp.now();
- p.purchaseStatus = PurchaseStatus.Paying;
- await tx.purchases.put(p);
- await spendCoins(ws, tx, {
- allocationId: `txn:proposal:${p.proposalId}`,
- coinPubs: coinSelection.coinPubs,
- contributions: coinSelection.coinContributions.map((x) =>
- Amounts.parseOrThrow(x),
- ),
- refreshReason: RefreshReason.PayMerchant,
- });
- break;
- case PurchaseStatus.Paid:
- case PurchaseStatus.Paying:
- default:
- break;
- }
- });
-
- ws.notify({
- type: NotificationType.ProposalAccepted,
- proposalId: proposal.proposalId,
- });
-
- return runPayForConfirmPay(ws, proposalId);
-}
-
-export async function processPurchase(
- ws: InternalWalletState,
- proposalId: string,
- options: {
- forceNow?: boolean;
- } = {},
-): Promise<OperationAttemptResult> {
- const purchase = await ws.db
- .mktx((x) => [x.purchases])
- .runReadOnly(async (tx) => {
- return tx.purchases.get(proposalId);
- });
- if (!purchase) {
- return {
- type: OperationAttemptResultType.Error,
- errorDetail: {
- // FIXME: allocate more specific error code
- code: TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
- hint: `trying to pay for purchase that is not in the database`,
- proposalId: proposalId,
- },
- };
- }
-
- switch (purchase.purchaseStatus) {
- case PurchaseStatus.DownloadingProposal:
- return processDownloadProposal(ws, proposalId, options);
- case PurchaseStatus.Paying:
- case PurchaseStatus.PayingReplay:
- return processPurchasePay(ws, proposalId, options);
- case PurchaseStatus.QueryingRefund:
- case PurchaseStatus.QueryingAutoRefund:
- case PurchaseStatus.AbortingWithRefund:
- return processPurchaseQueryRefund(ws, proposalId, options);
- case PurchaseStatus.ProposalDownloadFailed:
- case PurchaseStatus.Paid:
- case PurchaseStatus.RepurchaseDetected:
- case PurchaseStatus.Proposed:
- case PurchaseStatus.ProposalRefused:
- case PurchaseStatus.PaymentAbortFinished:
- return {
- type: OperationAttemptResultType.Finished,
- result: undefined,
- };
- default:
- assertUnreachable(purchase.purchaseStatus);
- // throw Error(`unexpected purchase status (${purchase.purchaseStatus})`);
- }
-}
-
-export async function processPurchasePay(
- ws: InternalWalletState,
- proposalId: string,
- options: {
- forceNow?: boolean;
- } = {},
-): Promise<OperationAttemptResult> {
- const purchase = await ws.db
- .mktx((x) => [x.purchases])
- .runReadOnly(async (tx) => {
- return tx.purchases.get(proposalId);
- });
- if (!purchase) {
- return {
- type: OperationAttemptResultType.Error,
- errorDetail: {
- // FIXME: allocate more specific error code
- code: TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
- hint: `trying to pay for purchase that is not in the database`,
- proposalId: proposalId,
- },
- };
- }
- switch (purchase.purchaseStatus) {
- case PurchaseStatus.Paying:
- case PurchaseStatus.PayingReplay:
- break;
- default:
- return OperationAttemptResult.finishedEmpty();
- }
- logger.trace(`processing purchase pay ${proposalId}`);
-
- const sessionId = purchase.lastSessionId;
-
- logger.trace(`paying with session ID ${sessionId}`);
- const payInfo = purchase.payInfo;
- checkDbInvariant(!!payInfo, "payInfo");
-
- const download = await expectProposalDownload(ws, purchase);
- if (!purchase.merchantPaySig) {
- const payUrl = new URL(
- `orders/${download.contractData.orderId}/pay`,
- download.contractData.merchantBaseUrl,
- ).href;
-
- let depositPermissions: CoinDepositPermission[];
- // FIXME: Cache!
- depositPermissions = await generateDepositPermissions(
- ws,
- payInfo.payCoinSelection,
- download.contractData,
- );
-
- const reqBody = {
- coins: depositPermissions,
- session_id: purchase.lastSessionId,
- };
-
- logger.trace(
- "making pay request ... ",
- JSON.stringify(reqBody, undefined, 2),
- );
-
- const resp = await ws.runSequentialized([EXCHANGE_COINS_LOCK], () =>
- ws.http.postJson(payUrl, reqBody, {
- timeout: getPayRequestTimeout(purchase),
- }),
- );
-
- logger.trace(`got resp ${JSON.stringify(resp)}`);
-
- if (resp.status >= 500 && resp.status <= 599) {
- const errDetails = await readUnexpectedResponseDetails(resp);
- return {
- type: OperationAttemptResultType.Error,
- errorDetail: makeErrorDetail(
- TalerErrorCode.WALLET_PAY_MERCHANT_SERVER_ERROR,
- {
- requestError: errDetails,
- },
- ),
- };
- }
-
- if (resp.status === HttpStatusCode.BadRequest) {
- const errDetails = await readUnexpectedResponseDetails(resp);
- logger.warn("unexpected 400 response for /pay");
- logger.warn(j2s(errDetails));
- await ws.db
- .mktx((x) => [x.purchases])
- .runReadWrite(async (tx) => {
- const purch = await tx.purchases.get(proposalId);
- if (!purch) {
- return;
- }
- // FIXME: Should be some "PayPermanentlyFailed" and error info should be stored
- purch.purchaseStatus = PurchaseStatus.PaymentAbortFinished;
- await tx.purchases.put(purch);
- });
- throw makePendingOperationFailedError(
- errDetails,
- TransactionType.Payment,
- proposalId,
- );
- }
-
- if (resp.status === HttpStatusCode.Gone) {
- const errDetails = await readUnexpectedResponseDetails(resp);
- logger.warn("unexpected 410 response for /pay");
- logger.warn(j2s(errDetails));
- await ws.db
- .mktx((x) => [x.purchases])
- .runReadWrite(async (tx) => {
- const purch = await tx.purchases.get(proposalId);
- if (!purch) {
- return;
- }
- // FIXME: Should be some "PayPermanentlyFailed" and error info should be stored
- purch.purchaseStatus = PurchaseStatus.PaymentAbortFinished;
- await tx.purchases.put(purch);
- });
- throw makePendingOperationFailedError(
- errDetails,
- TransactionType.Payment,
- proposalId,
- );
- }
-
- if (resp.status === HttpStatusCode.Conflict) {
- const err = await readTalerErrorResponse(resp);
- if (
- err.code ===
- TalerErrorCode.MERCHANT_POST_ORDERS_ID_PAY_INSUFFICIENT_FUNDS
- ) {
- // Do this in the background, as it might take some time
- handleInsufficientFunds(ws, proposalId, err).catch(async (e) => {
- console.log("handling insufficient funds failed");
-
- await scheduleRetry(ws, RetryTags.forPay(purchase), {
- code: TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
- message: "unexpected exception",
- hint: "unexpected exception",
- details: {
- exception: e.toString(),
- },
- });
- });
-
- return {
- type: OperationAttemptResultType.Pending,
- result: undefined,
- };
- }
- }
-
- const merchantResp = await readSuccessResponseJsonOrThrow(
- resp,
- codecForMerchantPayResponse(),
- );
-
- logger.trace("got success from pay URL", merchantResp);
-
- const merchantPub = download.contractData.merchantPub;
- const { valid } = await ws.cryptoApi.isValidPaymentSignature({
- contractHash: download.contractData.contractTermsHash,
- merchantPub,
- sig: merchantResp.sig,
- });
-
- if (!valid) {
- logger.error("merchant payment signature invalid");
- // FIXME: properly display error
- throw Error("merchant payment signature invalid");
- }
-
- await storeFirstPaySuccess(ws, proposalId, sessionId, merchantResp.sig);
- await unblockBackup(ws, proposalId);
- } else {
- const payAgainUrl = new URL(
- `orders/${download.contractData.orderId}/paid`,
- download.contractData.merchantBaseUrl,
- ).href;
- const reqBody = {
- sig: purchase.merchantPaySig,
- h_contract: download.contractData.contractTermsHash,
- session_id: sessionId ?? "",
- };
- logger.trace(`/paid request body: ${j2s(reqBody)}`);
- const resp = await ws.runSequentialized([EXCHANGE_COINS_LOCK], () =>
- ws.http.postJson(payAgainUrl, reqBody),
- );
- logger.trace(`/paid response status: ${resp.status}`);
- if (resp.status !== 204) {
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
- getHttpResponseErrorDetails(resp),
- "/paid failed",
- );
- }
- await storePayReplaySuccess(ws, proposalId, sessionId);
- await unblockBackup(ws, proposalId);
- }
-
- ws.notify({
- type: NotificationType.PayOperationSuccess,
- proposalId: purchase.proposalId,
- });
-
- return OperationAttemptResult.finishedEmpty();
-}
-
-export async function refuseProposal(
- ws: InternalWalletState,
- proposalId: string,
-): Promise<void> {
- const success = await ws.db
- .mktx((x) => [x.purchases])
- .runReadWrite(async (tx) => {
- const proposal = await tx.purchases.get(proposalId);
- if (!proposal) {
- logger.trace(`proposal ${proposalId} not found, won't refuse proposal`);
- return false;
- }
- if (proposal.purchaseStatus !== PurchaseStatus.Proposed) {
- return false;
- }
- proposal.purchaseStatus = PurchaseStatus.ProposalRefused;
- await tx.purchases.put(proposal);
- return true;
- });
- if (success) {
- ws.notify({
- type: NotificationType.ProposalRefused,
- });
- }
-}
-
-export async function prepareRefund(
- ws: InternalWalletState,
- talerRefundUri: string,
-): Promise<PrepareRefundResult> {
- const parseResult = parseRefundUri(talerRefundUri);
-
- logger.trace("preparing refund offer", parseResult);
-
- if (!parseResult) {
- throw Error("invalid refund URI");
- }
-
- const purchase = await ws.db
- .mktx((x) => [x.purchases])
- .runReadOnly(async (tx) => {
- return tx.purchases.indexes.byUrlAndOrderId.get([
- parseResult.merchantBaseUrl,
- parseResult.orderId,
- ]);
- });
-
- if (!purchase) {
- throw Error(
- `no purchase for the taler://refund/ URI (${talerRefundUri}) was found`,
- );
- }
-
- const awaiting = await queryAndSaveAwaitingRefund(ws, purchase);
- const summary = await calculateRefundSummary(ws, purchase);
- const proposalId = purchase.proposalId;
-
- const { contractData: c } = await expectProposalDownload(ws, purchase);
-
- return {
- proposalId,
- effectivePaid: Amounts.stringify(summary.amountEffectivePaid),
- gone: Amounts.stringify(summary.amountRefundGone),
- granted: Amounts.stringify(summary.amountRefundGranted),
- pending: summary.pendingAtExchange,
- awaiting: Amounts.stringify(awaiting),
- info: {
- contractTermsHash: c.contractTermsHash,
- merchant: c.merchant,
- orderId: c.orderId,
- products: c.products,
- summary: c.summary,
- fulfillmentMessage: c.fulfillmentMessage,
- summary_i18n: c.summaryI18n,
- fulfillmentMessage_i18n: c.fulfillmentMessageI18n,
- },
- };
-}
-
-function getRefundKey(d: MerchantCoinRefundStatus): string {
- return `${d.coin_pub}-${d.rtransaction_id}`;
-}
-
-async function applySuccessfulRefund(
- tx: GetReadWriteAccess<{
- coins: typeof WalletStoresV1.coins;
- denominations: typeof WalletStoresV1.denominations;
- }>,
- p: PurchaseRecord,
- refreshCoinsMap: Record<string, CoinRefreshRequest>,
- r: MerchantCoinRefundSuccessStatus,
-): Promise<void> {
- // FIXME: check signature before storing it as valid!
-
- const refundKey = getRefundKey(r);
- const coin = await tx.coins.get(r.coin_pub);
- if (!coin) {
- logger.warn("coin not found, can't apply refund");
- return;
- }
- const denom = await tx.denominations.get([
- coin.exchangeBaseUrl,
- coin.denomPubHash,
- ]);
- if (!denom) {
- throw Error("inconsistent database");
- }
- const refundAmount = Amounts.parseOrThrow(r.refund_amount);
- const refundFee = denom.fees.feeRefund;
- const amountLeft = Amounts.sub(refundAmount, refundFee).amount;
- coin.status = CoinStatus.Dormant;
- await tx.coins.put(coin);
-
- const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl
- .iter(coin.exchangeBaseUrl)
- .toArray();
- const totalRefreshCostBound = getTotalRefreshCost(
- allDenoms,
- DenominationRecord.toDenomInfo(denom),
- amountLeft,
- );
-
- refreshCoinsMap[coin.coinPub] = {
- coinPub: coin.coinPub,
- amount: Amounts.stringify(amountLeft),
- };
-
- p.refunds[refundKey] = {
- type: RefundState.Applied,
- obtainedTime: AbsoluteTime.toTimestamp(AbsoluteTime.now()),
- executionTime: r.execution_time,
- refundAmount: Amounts.stringify(r.refund_amount),
- refundFee: Amounts.stringify(denom.fees.feeRefund),
- totalRefreshCostBound: Amounts.stringify(totalRefreshCostBound),
- coinPub: r.coin_pub,
- rtransactionId: r.rtransaction_id,
- };
-}
-
-async function storePendingRefund(
- tx: GetReadWriteAccess<{
- denominations: typeof WalletStoresV1.denominations;
- coins: typeof WalletStoresV1.coins;
- }>,
- p: PurchaseRecord,
- r: MerchantCoinRefundFailureStatus,
-): Promise<void> {
- const refundKey = getRefundKey(r);
-
- const coin = await tx.coins.get(r.coin_pub);
- if (!coin) {
- logger.warn("coin not found, can't apply refund");
- return;
- }
- const denom = await tx.denominations.get([
- coin.exchangeBaseUrl,
- coin.denomPubHash,
- ]);
-
- if (!denom) {
- throw Error("inconsistent database");
- }
-
- const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl
- .iter(coin.exchangeBaseUrl)
- .toArray();
-
- // Refunded amount after fees.
- const amountLeft = Amounts.sub(
- Amounts.parseOrThrow(r.refund_amount),
- denom.fees.feeRefund,
- ).amount;
-
- const totalRefreshCostBound = getTotalRefreshCost(
- allDenoms,
- DenominationRecord.toDenomInfo(denom),
- amountLeft,
- );
-
- p.refunds[refundKey] = {
- type: RefundState.Pending,
- obtainedTime: AbsoluteTime.toTimestamp(AbsoluteTime.now()),
- executionTime: r.execution_time,
- refundAmount: Amounts.stringify(r.refund_amount),
- refundFee: Amounts.stringify(denom.fees.feeRefund),
- totalRefreshCostBound: Amounts.stringify(totalRefreshCostBound),
- coinPub: r.coin_pub,
- rtransactionId: r.rtransaction_id,
- };
-}
-
-async function storeFailedRefund(
- tx: GetReadWriteAccess<{
- coins: typeof WalletStoresV1.coins;
- denominations: typeof WalletStoresV1.denominations;
- }>,
- p: PurchaseRecord,
- refreshCoinsMap: Record<string, CoinRefreshRequest>,
- r: MerchantCoinRefundFailureStatus,
-): Promise<void> {
- const refundKey = getRefundKey(r);
-
- const coin = await tx.coins.get(r.coin_pub);
- if (!coin) {
- logger.warn("coin not found, can't apply refund");
- return;
- }
- const denom = await tx.denominations.get([
- coin.exchangeBaseUrl,
- coin.denomPubHash,
- ]);
-
- if (!denom) {
- throw Error("inconsistent database");
- }
-
- const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl
- .iter(coin.exchangeBaseUrl)
- .toArray();
-
- const amountLeft = Amounts.sub(
- Amounts.parseOrThrow(r.refund_amount),
- denom.fees.feeRefund,
- ).amount;
-
- const totalRefreshCostBound = getTotalRefreshCost(
- allDenoms,
- DenominationRecord.toDenomInfo(denom),
- amountLeft,
- );
-
- p.refunds[refundKey] = {
- type: RefundState.Failed,
- obtainedTime: TalerProtocolTimestamp.now(),
- executionTime: r.execution_time,
- refundAmount: Amounts.stringify(r.refund_amount),
- refundFee: Amounts.stringify(denom.fees.feeRefund),
- totalRefreshCostBound: Amounts.stringify(totalRefreshCostBound),
- coinPub: r.coin_pub,
- rtransactionId: r.rtransaction_id,
- };
-
- if (p.purchaseStatus === PurchaseStatus.AbortingWithRefund) {
- // Refund failed because the merchant didn't even try to deposit
- // the coin yet, so we try to refresh.
- // FIXME: Is this case tested?!
- if (r.exchange_code === TalerErrorCode.EXCHANGE_REFUND_DEPOSIT_NOT_FOUND) {
- const coin = await tx.coins.get(r.coin_pub);
- if (!coin) {
- logger.warn("coin not found, can't apply refund");
- return;
- }
- const denom = await tx.denominations.get([
- coin.exchangeBaseUrl,
- coin.denomPubHash,
- ]);
- if (!denom) {
- logger.warn("denomination for coin missing");
- return;
- }
- const payCoinSelection = p.payInfo?.payCoinSelection;
- if (!payCoinSelection) {
- logger.warn("no pay coin selection, can't apply refund");
- return;
- }
- let contrib: AmountJson | undefined;
- for (let i = 0; i < payCoinSelection.coinPubs.length; i++) {
- if (payCoinSelection.coinPubs[i] === r.coin_pub) {
- contrib = Amounts.parseOrThrow(payCoinSelection.coinContributions[i]);
- }
- }
- // FIXME: Is this case tested?!
- refreshCoinsMap[coin.coinPub] = {
- coinPub: coin.coinPub,
- amount: Amounts.stringify(amountLeft),
- };
- await tx.coins.put(coin);
- }
- }
-}
-
-async function acceptRefunds(
- ws: InternalWalletState,
- proposalId: string,
- refunds: MerchantCoinRefundStatus[],
- reason: RefundReason,
-): Promise<void> {
- logger.trace("handling refunds", refunds);
- const now = TalerProtocolTimestamp.now();
-
- await ws.db
- .mktx((x) => [
- x.purchases,
- x.coins,
- x.coinAvailability,
- x.denominations,
- x.refreshGroups,
- ])
- .runReadWrite(async (tx) => {
- const p = await tx.purchases.get(proposalId);
- if (!p) {
- logger.error("purchase not found, not adding refunds");
- return;
- }
-
- const refreshCoinsMap: Record<string, CoinRefreshRequest> = {};
-
- for (const refundStatus of refunds) {
- const refundKey = getRefundKey(refundStatus);
- const existingRefundInfo = p.refunds[refundKey];
-
- const isPermanentFailure =
- refundStatus.type === "failure" &&
- refundStatus.exchange_status >= 400 &&
- refundStatus.exchange_status < 500;
-
- // Already failed.
- if (existingRefundInfo?.type === RefundState.Failed) {
- continue;
- }
-
- // Already applied.
- if (existingRefundInfo?.type === RefundState.Applied) {
- continue;
- }
-
- // Still pending.
- if (
- refundStatus.type === "failure" &&
- !isPermanentFailure &&
- existingRefundInfo?.type === RefundState.Pending
- ) {
- continue;
- }
-
- // Invariant: (!existingRefundInfo) || (existingRefundInfo === Pending)
-
- if (refundStatus.type === "success") {
- await applySuccessfulRefund(tx, p, refreshCoinsMap, refundStatus);
- } else if (isPermanentFailure) {
- await storeFailedRefund(tx, p, refreshCoinsMap, refundStatus);
- } else {
- await storePendingRefund(tx, p, refundStatus);
- }
- }
-
- const refreshCoinsPubs = Object.values(refreshCoinsMap);
- logger.info(`refreshCoinMap ${j2s(refreshCoinsMap)}`);
- if (refreshCoinsPubs.length > 0) {
- await createRefreshGroup(
- ws,
- tx,
- refreshCoinsPubs,
- RefreshReason.Refund,
- );
- }
-
- // Are we done with querying yet, or do we need to do another round
- // after a retry delay?
- let queryDone = true;
-
- let numPendingRefunds = 0;
- for (const ri of Object.values(p.refunds)) {
- switch (ri.type) {
- case RefundState.Pending:
- numPendingRefunds++;
- break;
- }
- }
-
- if (numPendingRefunds > 0) {
- queryDone = false;
- }
-
- if (queryDone) {
- p.timestampLastRefundStatus = now;
- if (p.purchaseStatus === PurchaseStatus.AbortingWithRefund) {
- p.purchaseStatus = PurchaseStatus.PaymentAbortFinished;
- } else if (p.purchaseStatus === PurchaseStatus.QueryingAutoRefund) {
- const autoRefundDeadline = p.autoRefundDeadline;
- checkDbInvariant(!!autoRefundDeadline);
- if (
- AbsoluteTime.isExpired(
- AbsoluteTime.fromTimestamp(autoRefundDeadline),
- )
- ) {
- p.purchaseStatus = PurchaseStatus.Paid;
- }
- } else if (p.purchaseStatus === PurchaseStatus.QueryingRefund) {
- p.purchaseStatus = PurchaseStatus.Paid;
- }
- logger.trace("refund query done");
- } else {
- // No error, but we need to try again!
- p.timestampLastRefundStatus = now;
- logger.trace("refund query not done");
- }
-
- await tx.purchases.put(p);
- });
-
- ws.notify({
- type: NotificationType.RefundQueried,
- });
-}
-
-async function calculateRefundSummary(
- ws: InternalWalletState,
- p: PurchaseRecord,
-): Promise<RefundSummary> {
- const download = await expectProposalDownload(ws, p);
- let amountRefundGranted = Amounts.zeroOfAmount(download.contractData.amount);
- let amountRefundGone = Amounts.zeroOfAmount(download.contractData.amount);
-
- let pendingAtExchange = false;
-
- const payInfo = p.payInfo;
- if (!payInfo) {
- throw Error("can't calculate refund summary without payInfo");
- }
-
- Object.keys(p.refunds).forEach((rk) => {
- const refund = p.refunds[rk];
- if (refund.type === RefundState.Pending) {
- pendingAtExchange = true;
- }
- if (
- refund.type === RefundState.Applied ||
- refund.type === RefundState.Pending
- ) {
- amountRefundGranted = Amounts.add(
- amountRefundGranted,
- Amounts.sub(
- refund.refundAmount,
- refund.refundFee,
- refund.totalRefreshCostBound,
- ).amount,
- ).amount;
- } else {
- amountRefundGone = Amounts.add(
- amountRefundGone,
- refund.refundAmount,
- ).amount;
- }
- });
- return {
- amountEffectivePaid: Amounts.parseOrThrow(payInfo.totalPayCost),
- amountRefundGone,
- amountRefundGranted,
- pendingAtExchange,
- };
-}
-
-/**
- * Summary of the refund status of a purchase.
- */
-export interface RefundSummary {
- pendingAtExchange: boolean;
- amountEffectivePaid: AmountJson;
- amountRefundGranted: AmountJson;
- amountRefundGone: AmountJson;
-}
-
-/**
- * Accept a refund, return the contract hash for the contract
- * that was involved in the refund.
- */
-export async function applyRefund(
- ws: InternalWalletState,
- talerRefundUri: string,
-): Promise<ApplyRefundResponse> {
- const parseResult = parseRefundUri(talerRefundUri);
-
- logger.trace("applying refund", parseResult);
-
- if (!parseResult) {
- throw Error("invalid refund URI");
- }
-
- const purchase = await ws.db
- .mktx((x) => [x.purchases])
- .runReadOnly(async (tx) => {
- return tx.purchases.indexes.byUrlAndOrderId.get([
- parseResult.merchantBaseUrl,
- parseResult.orderId,
- ]);
- });
-
- if (!purchase) {
- throw Error(
- `no purchase for the taler://refund/ URI (${talerRefundUri}) was found`,
- );
- }
-
- return applyRefundFromPurchaseId(ws, purchase.proposalId);
-}
-
-export async function applyRefundFromPurchaseId(
- ws: InternalWalletState,
- proposalId: string,
-): Promise<ApplyRefundResponse> {
- logger.trace("applying refund for purchase", proposalId);
-
- logger.info("processing purchase for refund");
- const success = await ws.db
- .mktx((x) => [x.purchases])
- .runReadWrite(async (tx) => {
- const p = await tx.purchases.get(proposalId);
- if (!p) {
- logger.error("no purchase found for refund URL");
- return false;
- }
- if (p.purchaseStatus === PurchaseStatus.Paid) {
- p.purchaseStatus = PurchaseStatus.QueryingRefund;
- }
- await tx.purchases.put(p);
- return true;
- });
-
- if (success) {
- ws.notify({
- type: NotificationType.RefundStarted,
- });
- await processPurchaseQueryRefund(ws, proposalId, {
- forceNow: true,
- waitForAutoRefund: false,
- });
- }
-
- const purchase = await ws.db
- .mktx((x) => [x.purchases])
- .runReadOnly(async (tx) => {
- return tx.purchases.get(proposalId);
- });
-
- if (!purchase) {
- throw Error("purchase no longer exists");
- }
-
- const summary = await calculateRefundSummary(ws, purchase);
- const download = await expectProposalDownload(ws, purchase);
-
- return {
- contractTermsHash: download.contractData.contractTermsHash,
- proposalId: purchase.proposalId,
- transactionId: makeTransactionId(TransactionType.Payment, proposalId), //FIXME: can we have the tx id of the refund
- amountEffectivePaid: Amounts.stringify(summary.amountEffectivePaid),
- amountRefundGone: Amounts.stringify(summary.amountRefundGone),
- amountRefundGranted: Amounts.stringify(summary.amountRefundGranted),
- pendingAtExchange: summary.pendingAtExchange,
- info: {
- contractTermsHash: download.contractData.contractTermsHash,
- merchant: download.contractData.merchant,
- orderId: download.contractData.orderId,
- products: download.contractData.products,
- summary: download.contractData.summary,
- fulfillmentMessage: download.contractData.fulfillmentMessage,
- summary_i18n: download.contractData.summaryI18n,
- fulfillmentMessage_i18n: download.contractData.fulfillmentMessageI18n,
- },
- };
-}
-
-async function queryAndSaveAwaitingRefund(
- ws: InternalWalletState,
- purchase: PurchaseRecord,
- waitForAutoRefund?: boolean,
-): Promise<AmountJson> {
- const download = await expectProposalDownload(ws, purchase);
- const requestUrl = new URL(
- `orders/${download.contractData.orderId}`,
- download.contractData.merchantBaseUrl,
- );
- requestUrl.searchParams.set(
- "h_contract",
- download.contractData.contractTermsHash,
- );
- // Long-poll for one second
- if (waitForAutoRefund) {
- requestUrl.searchParams.set("timeout_ms", "1000");
- requestUrl.searchParams.set("await_refund_obtained", "yes");
- logger.trace("making long-polling request for auto-refund");
- }
- const resp = await ws.http.get(requestUrl.href);
- const orderStatus = await readSuccessResponseJsonOrThrow(
- resp,
- codecForMerchantOrderStatusPaid(),
- );
- if (!orderStatus.refunded) {
- // Wait for retry ...
- return Amounts.zeroOfAmount(download.contractData.amount);
- }
-
- const refundAwaiting = Amounts.sub(
- Amounts.parseOrThrow(orderStatus.refund_amount),
- Amounts.parseOrThrow(orderStatus.refund_taken),
- ).amount;
-
- if (
- purchase.refundAmountAwaiting === undefined ||
- Amounts.cmp(refundAwaiting, purchase.refundAmountAwaiting) !== 0
- ) {
- await ws.db
- .mktx((x) => [x.purchases])
- .runReadWrite(async (tx) => {
- const p = await tx.purchases.get(purchase.proposalId);
- if (!p) {
- logger.warn("purchase does not exist anymore");
- return;
- }
- p.refundAmountAwaiting = Amounts.stringify(refundAwaiting);
- await tx.purchases.put(p);
- });
- }
-
- return refundAwaiting;
-}
-
-export async function processPurchaseQueryRefund(
- ws: InternalWalletState,
- proposalId: string,
- options: {
- forceNow?: boolean;
- waitForAutoRefund?: boolean;
- } = {},
-): Promise<OperationAttemptResult> {
- logger.trace(`processing refund query for proposal ${proposalId}`);
- const waitForAutoRefund = options.waitForAutoRefund ?? false;
- const purchase = await ws.db
- .mktx((x) => [x.purchases])
- .runReadOnly(async (tx) => {
- return tx.purchases.get(proposalId);
- });
- if (!purchase) {
- return OperationAttemptResult.finishedEmpty();
- }
-
- if (
- !(
- purchase.purchaseStatus === PurchaseStatus.QueryingAutoRefund ||
- purchase.purchaseStatus === PurchaseStatus.QueryingRefund ||
- purchase.purchaseStatus === PurchaseStatus.AbortingWithRefund
- )
- ) {
- return OperationAttemptResult.finishedEmpty();
- }
-
- const download = await expectProposalDownload(ws, purchase);
-
- if (purchase.timestampFirstSuccessfulPay) {
- if (
- !purchase.autoRefundDeadline ||
- !AbsoluteTime.isExpired(
- AbsoluteTime.fromTimestamp(purchase.autoRefundDeadline),
- )
- ) {
- const awaitingAmount = await queryAndSaveAwaitingRefund(
- ws,
- purchase,
- waitForAutoRefund,
- );
- if (Amounts.isZero(awaitingAmount)) {
- return OperationAttemptResult.finishedEmpty();
- }
- }
-
- const requestUrl = new URL(
- `orders/${download.contractData.orderId}/refund`,
- download.contractData.merchantBaseUrl,
- );
-
- logger.trace(`making refund request to ${requestUrl.href}`);
-
- const request = await ws.http.postJson(requestUrl.href, {
- h_contract: download.contractData.contractTermsHash,
- });
-
- const refundResponse = await readSuccessResponseJsonOrThrow(
- request,
- codecForMerchantOrderRefundPickupResponse(),
- );
-
- await acceptRefunds(
- ws,
- proposalId,
- refundResponse.refunds,
- RefundReason.NormalRefund,
- );
- } else if (purchase.purchaseStatus === PurchaseStatus.AbortingWithRefund) {
- const requestUrl = new URL(
- `orders/${download.contractData.orderId}/abort`,
- download.contractData.merchantBaseUrl,
- );
-
- const abortingCoins: AbortingCoin[] = [];
-
- const payCoinSelection = purchase.payInfo?.payCoinSelection;
- if (!payCoinSelection) {
- throw Error("can't abort, no coins selected");
- }
-
- await ws.db
- .mktx((x) => [x.coins])
- .runReadOnly(async (tx) => {
- for (let i = 0; i < payCoinSelection.coinPubs.length; i++) {
- const coinPub = payCoinSelection.coinPubs[i];
- const coin = await tx.coins.get(coinPub);
- checkDbInvariant(!!coin, "expected coin to be present");
- abortingCoins.push({
- coin_pub: coinPub,
- contribution: Amounts.stringify(
- payCoinSelection.coinContributions[i],
- ),
- exchange_url: coin.exchangeBaseUrl,
- });
- }
- });
-
- const abortReq: AbortRequest = {
- h_contract: download.contractData.contractTermsHash,
- coins: abortingCoins,
- };
-
- logger.trace(`making order abort request to ${requestUrl.href}`);
-
- const request = await ws.http.postJson(requestUrl.href, abortReq);
- const abortResp = await readSuccessResponseJsonOrThrow(
- request,
- codecForAbortResponse(),
- );
-
- const refunds: MerchantCoinRefundStatus[] = [];
-
- if (abortResp.refunds.length != abortingCoins.length) {
- // FIXME: define error code!
- throw Error("invalid order abort response");
- }
-
- for (let i = 0; i < abortResp.refunds.length; i++) {
- const r = abortResp.refunds[i];
- refunds.push({
- ...r,
- coin_pub: payCoinSelection.coinPubs[i],
- refund_amount: Amounts.stringify(payCoinSelection.coinContributions[i]),
- rtransaction_id: 0,
- execution_time: AbsoluteTime.toTimestamp(
- AbsoluteTime.addDuration(
- AbsoluteTime.fromTimestamp(download.contractData.timestamp),
- Duration.fromSpec({ seconds: 1 }),
- ),
- ),
- });
- }
- await acceptRefunds(ws, proposalId, refunds, RefundReason.AbortRefund);
- }
- return OperationAttemptResult.finishedEmpty();
-}
-
-export async function abortFailedPayWithRefund(
- ws: InternalWalletState,
- proposalId: string,
-): Promise<void> {
- await ws.db
- .mktx((x) => [x.purchases])
- .runReadWrite(async (tx) => {
- const purchase = await tx.purchases.get(proposalId);
- if (!purchase) {
- throw Error("purchase not found");
- }
- if (purchase.timestampFirstSuccessfulPay) {
- // No point in aborting it. We don't even report an error.
- logger.warn(`tried to abort successful payment`);
- return;
- }
- if (purchase.purchaseStatus === PurchaseStatus.Paying) {
- purchase.purchaseStatus = PurchaseStatus.AbortingWithRefund;
- }
- await tx.purchases.put(purchase);
- });
- processPurchaseQueryRefund(ws, proposalId, {
- forceNow: true,
- }).catch((e) => {
- logger.trace(`error during refund processing after abort pay: ${e}`);
- });
-}
diff --git a/packages/taler-wallet-core/src/operations/pay-peer.ts b/packages/taler-wallet-core/src/operations/pay-peer.ts
deleted file mode 100644
index cc859f243..000000000
--- a/packages/taler-wallet-core/src/operations/pay-peer.ts
+++ /dev/null
@@ -1,901 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022 GNUnet e.V.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- * Imports.
- */
-import {
- AbsoluteTime,
- AcceptPeerPullPaymentRequest,
- AcceptPeerPullPaymentResponse,
- AcceptPeerPushPaymentRequest,
- AcceptPeerPushPaymentResponse,
- AgeCommitmentProof,
- AmountJson,
- Amounts,
- AmountString,
- buildCodecForObject,
- CheckPeerPullPaymentRequest,
- CheckPeerPullPaymentResponse,
- CheckPeerPushPaymentRequest,
- CheckPeerPushPaymentResponse,
- Codec,
- codecForAmountString,
- codecForAny,
- codecForExchangeGetContractResponse,
- CoinStatus,
- constructPayPullUri,
- constructPayPushUri,
- ContractTermsUtil,
- decodeCrock,
- Duration,
- eddsaGetPublic,
- encodeCrock,
- ExchangePurseDeposits,
- ExchangePurseMergeRequest,
- ExchangeReservePurseRequest,
- getRandomBytes,
- InitiatePeerPullPaymentRequest,
- InitiatePeerPullPaymentResponse,
- InitiatePeerPushPaymentRequest,
- InitiatePeerPushPaymentResponse,
- j2s,
- Logger,
- parsePayPullUri,
- parsePayPushUri,
- PeerContractTerms,
- PreparePeerPullPaymentRequest,
- PreparePeerPullPaymentResponse,
- PreparePeerPushPaymentRequest,
- PreparePeerPushPaymentResponse,
- RefreshReason,
- strcmp,
- TalerProtocolTimestamp,
- TransactionType,
- UnblindedSignature,
- WalletAccountMergeFlags,
-} from "@gnu-taler/taler-util";
-import {
- OperationStatus,
- PeerPullPaymentIncomingStatus,
- PeerPushPaymentIncomingRecord,
- PeerPushPaymentInitiationStatus,
- ReserveRecord,
- WalletStoresV1,
- WithdrawalGroupStatus,
- WithdrawalRecordType,
-} from "../db.js";
-import { InternalWalletState } from "../internal-wallet-state.js";
-import { makeTransactionId, spendCoins } from "../operations/common.js";
-import { readSuccessResponseJsonOrThrow } from "../util/http.js";
-import { checkDbInvariant } from "../util/invariants.js";
-import { GetReadOnlyAccess } from "../util/query.js";
-import { updateExchangeFromUrl } from "./exchanges.js";
-import { internalCreateWithdrawalGroup } from "./withdraw.js";
-
-const logger = new Logger("operations/peer-to-peer.ts");
-
-export interface PeerCoinSelection {
- exchangeBaseUrl: string;
-
- /**
- * Info of Coins that were selected.
- */
- coins: {
- coinPub: string;
- coinPriv: string;
- contribution: AmountString;
- denomPubHash: string;
- denomSig: UnblindedSignature;
- ageCommitmentProof: AgeCommitmentProof | undefined;
- }[];
-
- /**
- * How much of the deposit fees is the customer paying?
- */
- depositFees: AmountJson;
-}
-
-interface CoinInfo {
- /**
- * Public key of the coin.
- */
- coinPub: string;
-
- coinPriv: string;
-
- /**
- * Deposit fee for the coin.
- */
- feeDeposit: AmountJson;
-
- value: AmountJson;
-
- denomPubHash: string;
-
- denomSig: UnblindedSignature;
-
- maxAge: number;
- ageCommitmentProof?: AgeCommitmentProof;
-}
-
-export async function selectPeerCoins(
- ws: InternalWalletState,
- tx: GetReadOnlyAccess<{
- exchanges: typeof WalletStoresV1.exchanges;
- denominations: typeof WalletStoresV1.denominations;
- coins: typeof WalletStoresV1.coins;
- }>,
- instructedAmount: AmountJson,
-): Promise<PeerCoinSelection | undefined> {
- const exchanges = await tx.exchanges.iter().toArray();
- for (const exch of exchanges) {
- if (exch.detailsPointer?.currency !== instructedAmount.currency) {
- continue;
- }
- const coins = (
- await tx.coins.indexes.byBaseUrl.getAll(exch.baseUrl)
- ).filter((x) => x.status === CoinStatus.Fresh);
- const coinInfos: CoinInfo[] = [];
- for (const coin of coins) {
- const denom = await ws.getDenomInfo(
- ws,
- tx,
- coin.exchangeBaseUrl,
- coin.denomPubHash,
- );
- if (!denom) {
- throw Error("denom not found");
- }
- coinInfos.push({
- coinPub: coin.coinPub,
- feeDeposit: Amounts.parseOrThrow(denom.feeDeposit),
- value: Amounts.parseOrThrow(denom.value),
- denomPubHash: denom.denomPubHash,
- coinPriv: coin.coinPriv,
- denomSig: coin.denomSig,
- maxAge: coin.maxAge,
- ageCommitmentProof: coin.ageCommitmentProof,
- });
- }
- if (coinInfos.length === 0) {
- continue;
- }
- coinInfos.sort(
- (o1, o2) =>
- -Amounts.cmp(o1.value, o2.value) ||
- strcmp(o1.denomPubHash, o2.denomPubHash),
- );
- let amountAcc = Amounts.zeroOfCurrency(instructedAmount.currency);
- let depositFeesAcc = Amounts.zeroOfCurrency(instructedAmount.currency);
- const resCoins: {
- coinPub: string;
- coinPriv: string;
- contribution: AmountString;
- denomPubHash: string;
- denomSig: UnblindedSignature;
- ageCommitmentProof: AgeCommitmentProof | undefined;
- }[] = [];
- for (const coin of coinInfos) {
- if (Amounts.cmp(amountAcc, instructedAmount) >= 0) {
- break;
- }
- const gap = Amounts.add(
- coin.feeDeposit,
- Amounts.sub(instructedAmount, amountAcc).amount,
- ).amount;
- const contrib = Amounts.min(gap, coin.value);
- amountAcc = Amounts.add(
- amountAcc,
- Amounts.sub(contrib, coin.feeDeposit).amount,
- ).amount;
- depositFeesAcc = Amounts.add(depositFeesAcc, coin.feeDeposit).amount;
- resCoins.push({
- coinPriv: coin.coinPriv,
- coinPub: coin.coinPub,
- contribution: Amounts.stringify(contrib),
- denomPubHash: coin.denomPubHash,
- denomSig: coin.denomSig,
- ageCommitmentProof: coin.ageCommitmentProof,
- });
- }
- if (Amounts.cmp(amountAcc, instructedAmount) >= 0) {
- const res: PeerCoinSelection = {
- exchangeBaseUrl: exch.baseUrl,
- coins: resCoins,
- depositFees: depositFeesAcc,
- };
- return res;
- }
- continue;
- }
- return undefined;
-}
-
-export async function preparePeerPushPayment(
- ws: InternalWalletState,
- req: PreparePeerPushPaymentRequest,
-): Promise<PreparePeerPushPaymentResponse> {
- // FIXME: look up for the exchange and calculate fee
- return {
- amountEffective: req.amount,
- amountRaw: req.amount,
- };
-}
-
-export async function initiatePeerToPeerPush(
- ws: InternalWalletState,
- req: InitiatePeerPushPaymentRequest,
-): Promise<InitiatePeerPushPaymentResponse> {
- const instructedAmount = Amounts.parseOrThrow(
- req.partialContractTerms.amount,
- );
- const purseExpiration = req.partialContractTerms.purse_expiration;
- const contractTerms = req.partialContractTerms;
-
- const pursePair = await ws.cryptoApi.createEddsaKeypair({});
- const mergePair = await ws.cryptoApi.createEddsaKeypair({});
-
- const hContractTerms = ContractTermsUtil.hashContractTerms(contractTerms);
-
- const econtractResp = await ws.cryptoApi.encryptContractForMerge({
- contractTerms,
- mergePriv: mergePair.priv,
- pursePriv: pursePair.priv,
- pursePub: pursePair.pub,
- });
-
- const coinSelRes: PeerCoinSelection | undefined = await ws.db
- .mktx((x) => [
- x.exchanges,
- x.contractTerms,
- x.coins,
- x.coinAvailability,
- x.denominations,
- x.refreshGroups,
- x.peerPullPaymentInitiations,
- x.peerPushPaymentInitiations,
- ])
- .runReadWrite(async (tx) => {
- const sel = await selectPeerCoins(ws, tx, instructedAmount);
- if (!sel) {
- return undefined;
- }
-
- await spendCoins(ws, tx, {
- allocationId: `txn:peer-push-debit:${pursePair.pub}`,
- coinPubs: sel.coins.map((x) => x.coinPub),
- contributions: sel.coins.map((x) =>
- Amounts.parseOrThrow(x.contribution),
- ),
- refreshReason: RefreshReason.PayPeerPush,
- });
-
- await tx.peerPushPaymentInitiations.add({
- amount: Amounts.stringify(instructedAmount),
- contractPriv: econtractResp.contractPriv,
- contractTermsHash: hContractTerms,
- exchangeBaseUrl: sel.exchangeBaseUrl,
- mergePriv: mergePair.priv,
- mergePub: mergePair.pub,
- purseExpiration: purseExpiration,
- pursePriv: pursePair.priv,
- pursePub: pursePair.pub,
- timestampCreated: TalerProtocolTimestamp.now(),
- // FIXME: Only set the later when the purse is actually created!
- status: PeerPushPaymentInitiationStatus.PurseCreated,
- });
-
- await tx.contractTerms.put({
- h: hContractTerms,
- contractTermsRaw: contractTerms,
- });
-
- return sel;
- });
- logger.info(`selected p2p coins (push): ${j2s(coinSelRes)}`);
-
- if (!coinSelRes) {
- throw Error("insufficient balance");
- }
-
- const purseSigResp = await ws.cryptoApi.signPurseCreation({
- hContractTerms,
- mergePub: mergePair.pub,
- minAge: 0,
- purseAmount: Amounts.stringify(instructedAmount),
- purseExpiration,
- pursePriv: pursePair.priv,
- });
-
- const depositSigsResp = await ws.cryptoApi.signPurseDeposits({
- exchangeBaseUrl: coinSelRes.exchangeBaseUrl,
- pursePub: pursePair.pub,
- coins: coinSelRes.coins,
- });
-
- const createPurseUrl = new URL(
- `purses/${pursePair.pub}/create`,
- coinSelRes.exchangeBaseUrl,
- );
-
- const httpResp = await ws.http.postJson(createPurseUrl.href, {
- amount: Amounts.stringify(instructedAmount),
- merge_pub: mergePair.pub,
- purse_sig: purseSigResp.sig,
- h_contract_terms: hContractTerms,
- purse_expiration: purseExpiration,
- deposits: depositSigsResp.deposits,
- min_age: 0,
- econtract: econtractResp.econtract,
- });
-
- const resp = await httpResp.json();
-
- logger.info(`resp: ${j2s(resp)}`);
-
- if (httpResp.status !== 200) {
- throw Error("got error response from exchange");
- }
-
- return {
- contractPriv: econtractResp.contractPriv,
- mergePriv: mergePair.priv,
- pursePub: pursePair.pub,
- exchangeBaseUrl: coinSelRes.exchangeBaseUrl,
- talerUri: constructPayPushUri({
- exchangeBaseUrl: coinSelRes.exchangeBaseUrl,
- contractPriv: econtractResp.contractPriv,
- }),
- transactionId: makeTransactionId(
- TransactionType.PeerPushDebit,
- pursePair.pub,
- ),
- };
-}
-
-interface ExchangePurseStatus {
- balance: AmountString;
-}
-
-export const codecForExchangePurseStatus = (): Codec<ExchangePurseStatus> =>
- buildCodecForObject<ExchangePurseStatus>()
- .property("balance", codecForAmountString())
- .build("ExchangePurseStatus");
-
-export async function checkPeerPushPayment(
- ws: InternalWalletState,
- req: CheckPeerPushPaymentRequest,
-): Promise<CheckPeerPushPaymentResponse> {
- // FIXME: Check if existing record exists!
-
- const uri = parsePayPushUri(req.talerUri);
-
- if (!uri) {
- throw Error("got invalid taler://pay-push URI");
- }
-
- const exchangeBaseUrl = uri.exchangeBaseUrl;
-
- await updateExchangeFromUrl(ws, exchangeBaseUrl);
-
- const contractPriv = uri.contractPriv;
- const contractPub = encodeCrock(eddsaGetPublic(decodeCrock(contractPriv)));
-
- const getContractUrl = new URL(`contracts/${contractPub}`, exchangeBaseUrl);
-
- const contractHttpResp = await ws.http.get(getContractUrl.href);
-
- const contractResp = await readSuccessResponseJsonOrThrow(
- contractHttpResp,
- codecForExchangeGetContractResponse(),
- );
-
- const pursePub = contractResp.purse_pub;
-
- const dec = await ws.cryptoApi.decryptContractForMerge({
- ciphertext: contractResp.econtract,
- contractPriv: contractPriv,
- pursePub: pursePub,
- });
-
- const getPurseUrl = new URL(`purses/${pursePub}/deposit`, exchangeBaseUrl);
-
- const purseHttpResp = await ws.http.get(getPurseUrl.href);
-
- const purseStatus = await readSuccessResponseJsonOrThrow(
- purseHttpResp,
- codecForExchangePurseStatus(),
- );
-
- const peerPushPaymentIncomingId = encodeCrock(getRandomBytes(32));
-
- const contractTermsHash = ContractTermsUtil.hashContractTerms(
- dec.contractTerms,
- );
-
- await ws.db
- .mktx((x) => [x.contractTerms, x.peerPushPaymentIncoming])
- .runReadWrite(async (tx) => {
- await tx.peerPushPaymentIncoming.add({
- peerPushPaymentIncomingId,
- contractPriv: contractPriv,
- exchangeBaseUrl: exchangeBaseUrl,
- mergePriv: dec.mergePriv,
- pursePub: pursePub,
- timestamp: TalerProtocolTimestamp.now(),
- contractTermsHash,
- status: OperationStatus.Finished,
- });
-
- await tx.contractTerms.put({
- h: contractTermsHash,
- contractTermsRaw: dec.contractTerms,
- });
- });
-
- return {
- amount: purseStatus.balance,
- contractTerms: dec.contractTerms,
- peerPushPaymentIncomingId,
- };
-}
-
-export function talerPaytoFromExchangeReserve(
- exchangeBaseUrl: string,
- reservePub: string,
-): string {
- const url = new URL(exchangeBaseUrl);
- let proto: string;
- if (url.protocol === "http:") {
- proto = "taler-reserve-http";
- } else if (url.protocol === "https:") {
- proto = "taler-reserve";
- } else {
- throw Error(`unsupported exchange base URL protocol (${url.protocol})`);
- }
-
- let path = url.pathname;
- if (!path.endsWith("/")) {
- path = path + "/";
- }
-
- return `payto://${proto}/${url.host}${url.pathname}${reservePub}`;
-}
-
-async function getMergeReserveInfo(
- ws: InternalWalletState,
- req: {
- exchangeBaseUrl: string;
- },
-): Promise<ReserveRecord> {
- // We have to eagerly create the key pair outside of the transaction,
- // due to the async crypto API.
- const newReservePair = await ws.cryptoApi.createEddsaKeypair({});
-
- const mergeReserveRecord: ReserveRecord = await ws.db
- .mktx((x) => [x.exchanges, x.reserves, x.withdrawalGroups])
- .runReadWrite(async (tx) => {
- const ex = await tx.exchanges.get(req.exchangeBaseUrl);
- checkDbInvariant(!!ex);
- if (ex.currentMergeReserveRowId != null) {
- const reserve = await tx.reserves.get(ex.currentMergeReserveRowId);
- checkDbInvariant(!!reserve);
- return reserve;
- }
- const reserve: ReserveRecord = {
- reservePriv: newReservePair.priv,
- reservePub: newReservePair.pub,
- };
- const insertResp = await tx.reserves.put(reserve);
- checkDbInvariant(typeof insertResp.key === "number");
- reserve.rowId = insertResp.key;
- ex.currentMergeReserveRowId = reserve.rowId;
- await tx.exchanges.put(ex);
- return reserve;
- });
-
- return mergeReserveRecord;
-}
-
-export async function acceptPeerPushPayment(
- ws: InternalWalletState,
- req: AcceptPeerPushPaymentRequest,
-): Promise<AcceptPeerPushPaymentResponse> {
- let peerInc: PeerPushPaymentIncomingRecord | undefined;
- let contractTerms: PeerContractTerms | undefined;
- await ws.db
- .mktx((x) => [x.contractTerms, x.peerPushPaymentIncoming])
- .runReadOnly(async (tx) => {
- peerInc = await tx.peerPushPaymentIncoming.get(
- req.peerPushPaymentIncomingId,
- );
- if (!peerInc) {
- return;
- }
- const ctRec = await tx.contractTerms.get(peerInc.contractTermsHash);
- if (ctRec) {
- contractTerms = ctRec.contractTermsRaw;
- }
- });
-
- if (!peerInc) {
- throw Error(
- `can't accept unknown incoming p2p push payment (${req.peerPushPaymentIncomingId})`,
- );
- }
-
- checkDbInvariant(!!contractTerms);
-
- await updateExchangeFromUrl(ws, peerInc.exchangeBaseUrl);
-
- const amount = Amounts.parseOrThrow(contractTerms.amount);
-
- const mergeReserveInfo = await getMergeReserveInfo(ws, {
- exchangeBaseUrl: peerInc.exchangeBaseUrl,
- });
-
- const mergeTimestamp = TalerProtocolTimestamp.now();
-
- const reservePayto = talerPaytoFromExchangeReserve(
- peerInc.exchangeBaseUrl,
- mergeReserveInfo.reservePub,
- );
-
- const sigRes = await ws.cryptoApi.signPurseMerge({
- contractTermsHash: ContractTermsUtil.hashContractTerms(contractTerms),
- flags: WalletAccountMergeFlags.MergeFullyPaidPurse,
- mergePriv: peerInc.mergePriv,
- mergeTimestamp: mergeTimestamp,
- purseAmount: Amounts.stringify(amount),
- purseExpiration: contractTerms.purse_expiration,
- purseFee: Amounts.stringify(Amounts.zeroOfCurrency(amount.currency)),
- pursePub: peerInc.pursePub,
- reservePayto,
- reservePriv: mergeReserveInfo.reservePriv,
- });
-
- const mergePurseUrl = new URL(
- `purses/${peerInc.pursePub}/merge`,
- peerInc.exchangeBaseUrl,
- );
-
- const mergeReq: ExchangePurseMergeRequest = {
- payto_uri: reservePayto,
- merge_timestamp: mergeTimestamp,
- merge_sig: sigRes.mergeSig,
- reserve_sig: sigRes.accountSig,
- };
-
- const mergeHttpReq = await ws.http.postJson(mergePurseUrl.href, mergeReq);
-
- logger.info(`merge request: ${j2s(mergeReq)}`);
- const res = await readSuccessResponseJsonOrThrow(mergeHttpReq, codecForAny());
- logger.info(`merge response: ${j2s(res)}`);
-
- const wg = await internalCreateWithdrawalGroup(ws, {
- amount,
- wgInfo: {
- withdrawalType: WithdrawalRecordType.PeerPushCredit,
- contractTerms,
- },
- exchangeBaseUrl: peerInc.exchangeBaseUrl,
- reserveStatus: WithdrawalGroupStatus.QueryingStatus,
- reserveKeyPair: {
- priv: mergeReserveInfo.reservePriv,
- pub: mergeReserveInfo.reservePub,
- },
- });
-
- return {
- transactionId: makeTransactionId(
- TransactionType.PeerPushCredit,
- wg.withdrawalGroupId,
- ),
- };
-}
-
-export async function acceptPeerPullPayment(
- ws: InternalWalletState,
- req: AcceptPeerPullPaymentRequest,
-): Promise<AcceptPeerPullPaymentResponse> {
- const peerPullInc = await ws.db
- .mktx((x) => [x.peerPullPaymentIncoming])
- .runReadOnly(async (tx) => {
- return tx.peerPullPaymentIncoming.get(req.peerPullPaymentIncomingId);
- });
-
- if (!peerPullInc) {
- throw Error(
- `can't accept unknown incoming p2p pull payment (${req.peerPullPaymentIncomingId})`,
- );
- }
-
- const instructedAmount = Amounts.parseOrThrow(
- peerPullInc.contractTerms.amount,
- );
- const coinSelRes: PeerCoinSelection | undefined = await ws.db
- .mktx((x) => [
- x.exchanges,
- x.coins,
- x.denominations,
- x.refreshGroups,
- x.peerPullPaymentIncoming,
- x.coinAvailability,
- ])
- .runReadWrite(async (tx) => {
- const sel = await selectPeerCoins(ws, tx, instructedAmount);
- if (!sel) {
- return undefined;
- }
-
- await spendCoins(ws, tx, {
- allocationId: `txn:peer-pull-debit:${req.peerPullPaymentIncomingId}`,
- coinPubs: sel.coins.map((x) => x.coinPub),
- contributions: sel.coins.map((x) =>
- Amounts.parseOrThrow(x.contribution),
- ),
- refreshReason: RefreshReason.PayPeerPull,
- });
-
- const pi = await tx.peerPullPaymentIncoming.get(
- req.peerPullPaymentIncomingId,
- );
- if (!pi) {
- throw Error();
- }
- pi.status = PeerPullPaymentIncomingStatus.Accepted;
- await tx.peerPullPaymentIncoming.put(pi);
-
- return sel;
- });
- logger.info(`selected p2p coins (pull): ${j2s(coinSelRes)}`);
-
- if (!coinSelRes) {
- throw Error("insufficient balance");
- }
-
- const pursePub = peerPullInc.pursePub;
-
- const depositSigsResp = await ws.cryptoApi.signPurseDeposits({
- exchangeBaseUrl: coinSelRes.exchangeBaseUrl,
- pursePub,
- coins: coinSelRes.coins,
- });
-
- const purseDepositUrl = new URL(
- `purses/${pursePub}/deposit`,
- coinSelRes.exchangeBaseUrl,
- );
-
- const depositPayload: ExchangePurseDeposits = {
- deposits: depositSigsResp.deposits,
- };
-
- const httpResp = await ws.http.postJson(purseDepositUrl.href, depositPayload);
- const resp = await readSuccessResponseJsonOrThrow(httpResp, codecForAny());
- logger.trace(`purse deposit response: ${j2s(resp)}`);
-
- return {
- transactionId: makeTransactionId(
- TransactionType.PeerPullDebit,
- req.peerPullPaymentIncomingId,
- ),
- };
-}
-
-export async function checkPeerPullPayment(
- ws: InternalWalletState,
- req: CheckPeerPullPaymentRequest,
-): Promise<CheckPeerPullPaymentResponse> {
- const uri = parsePayPullUri(req.talerUri);
-
- if (!uri) {
- throw Error("got invalid taler://pay-push URI");
- }
-
- const exchangeBaseUrl = uri.exchangeBaseUrl;
- const contractPriv = uri.contractPriv;
- const contractPub = encodeCrock(eddsaGetPublic(decodeCrock(contractPriv)));
-
- const getContractUrl = new URL(`contracts/${contractPub}`, exchangeBaseUrl);
-
- const contractHttpResp = await ws.http.get(getContractUrl.href);
-
- const contractResp = await readSuccessResponseJsonOrThrow(
- contractHttpResp,
- codecForExchangeGetContractResponse(),
- );
-
- const pursePub = contractResp.purse_pub;
-
- const dec = await ws.cryptoApi.decryptContractForDeposit({
- ciphertext: contractResp.econtract,
- contractPriv: contractPriv,
- pursePub: pursePub,
- });
-
- const getPurseUrl = new URL(`purses/${pursePub}/merge`, exchangeBaseUrl);
-
- const purseHttpResp = await ws.http.get(getPurseUrl.href);
-
- const purseStatus = await readSuccessResponseJsonOrThrow(
- purseHttpResp,
- codecForExchangePurseStatus(),
- );
-
- const peerPullPaymentIncomingId = encodeCrock(getRandomBytes(32));
-
- await ws.db
- .mktx((x) => [x.peerPullPaymentIncoming])
- .runReadWrite(async (tx) => {
- await tx.peerPullPaymentIncoming.add({
- peerPullPaymentIncomingId,
- contractPriv: contractPriv,
- exchangeBaseUrl: exchangeBaseUrl,
- pursePub: pursePub,
- timestampCreated: TalerProtocolTimestamp.now(),
- contractTerms: dec.contractTerms,
- status: PeerPullPaymentIncomingStatus.Proposed,
- });
- });
-
- return {
- amount: purseStatus.balance,
- contractTerms: dec.contractTerms,
- peerPullPaymentIncomingId,
- };
-}
-
-export async function preparePeerPullPayment(
- ws: InternalWalletState,
- req: PreparePeerPullPaymentRequest,
-): Promise<PreparePeerPullPaymentResponse> {
- //FIXME: look up for exchange details and use purse fee
- return {
- amountEffective: req.amount,
- amountRaw: req.amount,
- };
-}
-/**
- * Initiate a peer pull payment.
- */
-export async function initiatePeerPullPayment(
- ws: InternalWalletState,
- req: InitiatePeerPullPaymentRequest,
-): Promise<InitiatePeerPullPaymentResponse> {
- await updateExchangeFromUrl(ws, req.exchangeBaseUrl);
-
- const mergeReserveInfo = await getMergeReserveInfo(ws, {
- exchangeBaseUrl: req.exchangeBaseUrl,
- });
-
- const mergeTimestamp = TalerProtocolTimestamp.now();
-
- const pursePair = await ws.cryptoApi.createEddsaKeypair({});
- const mergePair = await ws.cryptoApi.createEddsaKeypair({});
-
- const instructedAmount = Amounts.parseOrThrow(
- req.partialContractTerms.amount,
- );
- const purseExpiration = req.partialContractTerms.purse_expiration;
- const contractTerms = req.partialContractTerms;
-
- const reservePayto = talerPaytoFromExchangeReserve(
- req.exchangeBaseUrl,
- mergeReserveInfo.reservePub,
- );
-
- const econtractResp = await ws.cryptoApi.encryptContractForDeposit({
- contractTerms,
- pursePriv: pursePair.priv,
- pursePub: pursePair.pub,
- });
-
- const hContractTerms = ContractTermsUtil.hashContractTerms(contractTerms);
-
- const purseFee = Amounts.stringify(
- Amounts.zeroOfCurrency(instructedAmount.currency),
- );
-
- const sigRes = await ws.cryptoApi.signReservePurseCreate({
- contractTermsHash: hContractTerms,
- flags: WalletAccountMergeFlags.CreateWithPurseFee,
- mergePriv: mergePair.priv,
- mergeTimestamp: mergeTimestamp,
- purseAmount: req.partialContractTerms.amount,
- purseExpiration: purseExpiration,
- purseFee: purseFee,
- pursePriv: pursePair.priv,
- pursePub: pursePair.pub,
- reservePayto,
- reservePriv: mergeReserveInfo.reservePriv,
- });
-
- await ws.db
- .mktx((x) => [x.peerPullPaymentInitiations, x.contractTerms])
- .runReadWrite(async (tx) => {
- await tx.peerPullPaymentInitiations.put({
- amount: req.partialContractTerms.amount,
- contractTermsHash: hContractTerms,
- exchangeBaseUrl: req.exchangeBaseUrl,
- pursePriv: pursePair.priv,
- pursePub: pursePair.pub,
- status: OperationStatus.Finished,
- });
- await tx.contractTerms.put({
- contractTermsRaw: contractTerms,
- h: hContractTerms,
- });
- });
-
- const reservePurseReqBody: ExchangeReservePurseRequest = {
- merge_sig: sigRes.mergeSig,
- merge_timestamp: mergeTimestamp,
- h_contract_terms: hContractTerms,
- merge_pub: mergePair.pub,
- min_age: 0,
- purse_expiration: purseExpiration,
- purse_fee: purseFee,
- purse_pub: pursePair.pub,
- purse_sig: sigRes.purseSig,
- purse_value: req.partialContractTerms.amount,
- reserve_sig: sigRes.accountSig,
- econtract: econtractResp.econtract,
- };
-
- logger.info(`reserve purse request: ${j2s(reservePurseReqBody)}`);
-
- const reservePurseMergeUrl = new URL(
- `reserves/${mergeReserveInfo.reservePub}/purse`,
- req.exchangeBaseUrl,
- );
-
- const httpResp = await ws.http.postJson(
- reservePurseMergeUrl.href,
- reservePurseReqBody,
- );
-
- const resp = await readSuccessResponseJsonOrThrow(httpResp, codecForAny());
-
- logger.info(`reserve merge response: ${j2s(resp)}`);
-
- const wg = await internalCreateWithdrawalGroup(ws, {
- amount: instructedAmount,
- wgInfo: {
- withdrawalType: WithdrawalRecordType.PeerPullCredit,
- contractTerms,
- contractPriv: econtractResp.contractPriv,
- },
- exchangeBaseUrl: req.exchangeBaseUrl,
- reserveStatus: WithdrawalGroupStatus.QueryingStatus,
- reserveKeyPair: {
- priv: mergeReserveInfo.reservePriv,
- pub: mergeReserveInfo.reservePub,
- },
- });
-
- return {
- talerUri: constructPayPullUri({
- exchangeBaseUrl: req.exchangeBaseUrl,
- contractPriv: econtractResp.contractPriv,
- }),
- transactionId: makeTransactionId(
- TransactionType.PeerPullCredit,
- wg.withdrawalGroupId,
- ),
- };
-}
diff --git a/packages/taler-wallet-core/src/operations/pending.ts b/packages/taler-wallet-core/src/operations/pending.ts
deleted file mode 100644
index d2066d4fc..000000000
--- a/packages/taler-wallet-core/src/operations/pending.ts
+++ /dev/null
@@ -1,377 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2019 GNUnet e.V.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- * Derive pending tasks from the wallet database.
- */
-
-/**
- * Imports.
- */
-import {
- PurchaseStatus,
- WalletStoresV1,
- BackupProviderStateTag,
- RefreshCoinStatus,
- OperationStatus,
- OperationStatusRange,
-} from "../db.js";
-import {
- PendingOperationsResponse,
- PendingTaskType,
-} from "../pending-types.js";
-import { AbsoluteTime } from "@gnu-taler/taler-util";
-import { InternalWalletState } from "../internal-wallet-state.js";
-import { GetReadOnlyAccess } from "../util/query.js";
-import { RetryTags } from "../util/retries.js";
-import { GlobalIDB } from "@gnu-taler/idb-bridge";
-
-function getPendingCommon(
- ws: InternalWalletState,
- opTag: string,
- timestampDue: AbsoluteTime,
-): {
- id: string;
- isDue: boolean;
- timestampDue: AbsoluteTime;
- isLongpolling: boolean;
-} {
- const isDue =
- AbsoluteTime.isExpired(timestampDue) && !ws.activeLongpoll[opTag];
- return {
- id: opTag,
- isDue,
- timestampDue,
- isLongpolling: !!ws.activeLongpoll[opTag],
- };
-}
-
-async function gatherExchangePending(
- ws: InternalWalletState,
- tx: GetReadOnlyAccess<{
- exchanges: typeof WalletStoresV1.exchanges;
- exchangeDetails: typeof WalletStoresV1.exchangeDetails;
- operationRetries: typeof WalletStoresV1.operationRetries;
- }>,
- now: AbsoluteTime,
- resp: PendingOperationsResponse,
-): Promise<void> {
- // FIXME: We should do a range query here based on the update time.
- await tx.exchanges.iter().forEachAsync(async (exch) => {
- const opTag = RetryTags.forExchangeUpdate(exch);
- let opr = await tx.operationRetries.get(opTag);
- const timestampDue =
- opr?.retryInfo.nextRetry ?? AbsoluteTime.fromTimestamp(exch.nextUpdate);
- resp.pendingOperations.push({
- type: PendingTaskType.ExchangeUpdate,
- ...getPendingCommon(ws, opTag, timestampDue),
- givesLifeness: false,
- exchangeBaseUrl: exch.baseUrl,
- lastError: opr?.lastError,
- });
-
- // We only schedule a check for auto-refresh if the exchange update
- // was successful.
- if (!opr?.lastError) {
- resp.pendingOperations.push({
- type: PendingTaskType.ExchangeCheckRefresh,
- ...getPendingCommon(ws, opTag, timestampDue),
- timestampDue: AbsoluteTime.fromTimestamp(exch.nextRefreshCheck),
- givesLifeness: false,
- exchangeBaseUrl: exch.baseUrl,
- });
- }
- });
-}
-
-async function gatherRefreshPending(
- ws: InternalWalletState,
- tx: GetReadOnlyAccess<{
- refreshGroups: typeof WalletStoresV1.refreshGroups;
- operationRetries: typeof WalletStoresV1.operationRetries;
- }>,
- now: AbsoluteTime,
- resp: PendingOperationsResponse,
-): Promise<void> {
- const keyRange = GlobalIDB.KeyRange.bound(
- OperationStatusRange.ACTIVE_START,
- OperationStatusRange.ACTIVE_END,
- );
- const refreshGroups = await tx.refreshGroups.indexes.byStatus.getAll(
- keyRange,
- );
- for (const r of refreshGroups) {
- if (r.timestampFinished) {
- return;
- }
- const opId = RetryTags.forRefresh(r);
- const retryRecord = await tx.operationRetries.get(opId);
-
- const timestampDue = retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now();
-
- resp.pendingOperations.push({
- type: PendingTaskType.Refresh,
- ...getPendingCommon(ws, opId, timestampDue),
- givesLifeness: true,
- refreshGroupId: r.refreshGroupId,
- finishedPerCoin: r.statusPerCoin.map(
- (x) => x === RefreshCoinStatus.Finished,
- ),
- retryInfo: retryRecord?.retryInfo,
- });
- }
-}
-
-async function gatherWithdrawalPending(
- ws: InternalWalletState,
- tx: GetReadOnlyAccess<{
- withdrawalGroups: typeof WalletStoresV1.withdrawalGroups;
- planchets: typeof WalletStoresV1.planchets;
- operationRetries: typeof WalletStoresV1.operationRetries;
- }>,
- now: AbsoluteTime,
- resp: PendingOperationsResponse,
-): Promise<void> {
- const wsrs = await tx.withdrawalGroups.indexes.byStatus.getAll(
- GlobalIDB.KeyRange.bound(
- OperationStatusRange.ACTIVE_START,
- OperationStatusRange.ACTIVE_END,
- ),
- );
- for (const wsr of wsrs) {
- if (wsr.timestampFinish) {
- return;
- }
- const opTag = RetryTags.forWithdrawal(wsr);
- let opr = await tx.operationRetries.get(opTag);
- const now = AbsoluteTime.now();
- if (!opr) {
- opr = {
- id: opTag,
- retryInfo: {
- firstTry: now,
- nextRetry: now,
- retryCounter: 0,
- },
- };
- }
- resp.pendingOperations.push({
- type: PendingTaskType.Withdraw,
- ...getPendingCommon(
- ws,
- opTag,
- opr.retryInfo?.nextRetry ?? AbsoluteTime.now(),
- ),
- givesLifeness: true,
- withdrawalGroupId: wsr.withdrawalGroupId,
- lastError: opr.lastError,
- retryInfo: opr.retryInfo,
- });
- }
-}
-
-async function gatherDepositPending(
- ws: InternalWalletState,
- tx: GetReadOnlyAccess<{
- depositGroups: typeof WalletStoresV1.depositGroups;
- operationRetries: typeof WalletStoresV1.operationRetries;
- }>,
- now: AbsoluteTime,
- resp: PendingOperationsResponse,
-): Promise<void> {
- const dgs = await tx.depositGroups.indexes.byStatus.getAll(
- OperationStatus.Pending,
- );
- for (const dg of dgs) {
- if (dg.timestampFinished) {
- return;
- }
- const opId = RetryTags.forDeposit(dg);
- const retryRecord = await tx.operationRetries.get(opId);
- const timestampDue = retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now();
- resp.pendingOperations.push({
- type: PendingTaskType.Deposit,
- ...getPendingCommon(ws, opId, timestampDue),
- givesLifeness: true,
- depositGroupId: dg.depositGroupId,
- lastError: retryRecord?.lastError,
- retryInfo: retryRecord?.retryInfo,
- });
- }
-}
-
-async function gatherTipPending(
- ws: InternalWalletState,
- tx: GetReadOnlyAccess<{
- tips: typeof WalletStoresV1.tips;
- operationRetries: typeof WalletStoresV1.operationRetries;
- }>,
- now: AbsoluteTime,
- resp: PendingOperationsResponse,
-): Promise<void> {
- await tx.tips.iter().forEachAsync(async (tip) => {
- // FIXME: The tip record needs a proper status field!
- if (tip.pickedUpTimestamp) {
- return;
- }
- const opId = RetryTags.forTipPickup(tip);
- const retryRecord = await tx.operationRetries.get(opId);
- const timestampDue = retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now();
- if (tip.acceptedTimestamp) {
- resp.pendingOperations.push({
- type: PendingTaskType.TipPickup,
- ...getPendingCommon(ws, opId, timestampDue),
- givesLifeness: true,
- timestampDue: retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now(),
- merchantBaseUrl: tip.merchantBaseUrl,
- tipId: tip.walletTipId,
- merchantTipId: tip.merchantTipId,
- });
- }
- });
-}
-
-async function gatherPurchasePending(
- ws: InternalWalletState,
- tx: GetReadOnlyAccess<{
- purchases: typeof WalletStoresV1.purchases;
- operationRetries: typeof WalletStoresV1.operationRetries;
- }>,
- now: AbsoluteTime,
- resp: PendingOperationsResponse,
-): Promise<void> {
- const keyRange = GlobalIDB.KeyRange.bound(
- OperationStatusRange.ACTIVE_START,
- OperationStatusRange.ACTIVE_END,
- );
- await tx.purchases.indexes.byStatus
- .iter(keyRange)
- .forEachAsync(async (pr) => {
- const opId = RetryTags.forPay(pr);
- const retryRecord = await tx.operationRetries.get(opId);
- const timestampDue =
- retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now();
- resp.pendingOperations.push({
- type: PendingTaskType.Purchase,
- ...getPendingCommon(ws, opId, timestampDue),
- givesLifeness: true,
- statusStr: PurchaseStatus[pr.purchaseStatus],
- proposalId: pr.proposalId,
- retryInfo: retryRecord?.retryInfo,
- lastError: retryRecord?.lastError,
- });
- });
-}
-
-async function gatherRecoupPending(
- ws: InternalWalletState,
- tx: GetReadOnlyAccess<{
- recoupGroups: typeof WalletStoresV1.recoupGroups;
- operationRetries: typeof WalletStoresV1.operationRetries;
- }>,
- now: AbsoluteTime,
- resp: PendingOperationsResponse,
-): Promise<void> {
- await tx.recoupGroups.iter().forEachAsync(async (rg) => {
- if (rg.timestampFinished) {
- return;
- }
- const opId = RetryTags.forRecoup(rg);
- const retryRecord = await tx.operationRetries.get(opId);
- const timestampDue = retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now();
- resp.pendingOperations.push({
- type: PendingTaskType.Recoup,
- ...getPendingCommon(ws, opId, timestampDue),
- givesLifeness: true,
- recoupGroupId: rg.recoupGroupId,
- retryInfo: retryRecord?.retryInfo,
- lastError: retryRecord?.lastError,
- });
- });
-}
-
-async function gatherBackupPending(
- ws: InternalWalletState,
- tx: GetReadOnlyAccess<{
- backupProviders: typeof WalletStoresV1.backupProviders;
- operationRetries: typeof WalletStoresV1.operationRetries;
- }>,
- now: AbsoluteTime,
- resp: PendingOperationsResponse,
-): Promise<void> {
- await tx.backupProviders.iter().forEachAsync(async (bp) => {
- const opId = RetryTags.forBackup(bp);
- const retryRecord = await tx.operationRetries.get(opId);
- if (bp.state.tag === BackupProviderStateTag.Ready) {
- const timestampDue = AbsoluteTime.fromTimestamp(
- bp.state.nextBackupTimestamp,
- );
- resp.pendingOperations.push({
- type: PendingTaskType.Backup,
- ...getPendingCommon(ws, opId, timestampDue),
- givesLifeness: false,
- backupProviderBaseUrl: bp.baseUrl,
- lastError: undefined,
- });
- } else if (bp.state.tag === BackupProviderStateTag.Retrying) {
- const timestampDue =
- retryRecord?.retryInfo?.nextRetry ?? AbsoluteTime.now();
- resp.pendingOperations.push({
- type: PendingTaskType.Backup,
- ...getPendingCommon(ws, opId, timestampDue),
- givesLifeness: false,
- backupProviderBaseUrl: bp.baseUrl,
- retryInfo: retryRecord?.retryInfo,
- lastError: retryRecord?.lastError,
- });
- }
- });
-}
-
-export async function getPendingOperations(
- ws: InternalWalletState,
-): Promise<PendingOperationsResponse> {
- const now = AbsoluteTime.now();
- return await ws.db
- .mktx((x) => [
- x.backupProviders,
- x.exchanges,
- x.exchangeDetails,
- x.refreshGroups,
- x.coins,
- x.withdrawalGroups,
- x.tips,
- x.purchases,
- x.planchets,
- x.depositGroups,
- x.recoupGroups,
- x.operationRetries,
- ])
- .runReadWrite(async (tx) => {
- const resp: PendingOperationsResponse = {
- pendingOperations: [],
- };
- await gatherExchangePending(ws, tx, now, resp);
- await gatherRefreshPending(ws, tx, now, resp);
- await gatherWithdrawalPending(ws, tx, now, resp);
- await gatherDepositPending(ws, tx, now, resp);
- await gatherTipPending(ws, tx, now, resp);
- await gatherPurchasePending(ws, tx, now, resp);
- await gatherRecoupPending(ws, tx, now, resp);
- await gatherBackupPending(ws, tx, now, resp);
- return resp;
- });
-}
diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts
deleted file mode 100644
index 806e4a246..000000000
--- a/packages/taler-wallet-core/src/operations/refresh.ts
+++ /dev/null
@@ -1,1079 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2019 GNUnet e.V.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-import {
- AbsoluteTime,
- AgeCommitment,
- AgeRestriction,
- AmountJson,
- Amounts,
- amountToPretty,
- codecForExchangeMeltResponse,
- codecForExchangeRevealResponse,
- CoinPublicKeyString,
- CoinRefreshRequest,
- CoinStatus,
- DenominationInfo,
- DenomKeyType,
- Duration,
- durationFromSpec,
- durationMul,
- encodeCrock,
- ExchangeMeltRequest,
- ExchangeProtocolVersion,
- ExchangeRefreshRevealRequest,
- fnutil,
- getRandomBytes,
- HashCodeString,
- HttpStatusCode,
- j2s,
- Logger,
- NotificationType,
- RefreshGroupId,
- RefreshReason,
- TalerProtocolTimestamp,
- URL,
-} from "@gnu-taler/taler-util";
-import { TalerCryptoInterface } from "../crypto/cryptoImplementation.js";
-import {
- DerivedRefreshSession,
- RefreshNewDenomInfo,
-} from "../crypto/cryptoTypes.js";
-import { CryptoApiStoppedError } from "../crypto/workers/cryptoDispatcher.js";
-import {
- CoinRecord,
- CoinSourceType,
- DenominationRecord,
- RefreshCoinStatus,
- RefreshGroupRecord,
- RefreshOperationStatus,
- WalletStoresV1,
-} from "../db.js";
-import { TalerError } from "../errors.js";
-import {
- EXCHANGE_COINS_LOCK,
- InternalWalletState,
-} from "../internal-wallet-state.js";
-import { assertUnreachable } from "../util/assertUnreachable.js";
-import {
- readSuccessResponseJsonOrThrow,
- readUnexpectedResponseDetails,
-} from "../util/http.js";
-import { checkDbInvariant } from "../util/invariants.js";
-import { GetReadWriteAccess } from "../util/query.js";
-import {
- OperationAttemptResult,
- OperationAttemptResultType,
-} from "../util/retries.js";
-import { makeCoinAvailable } from "./common.js";
-import { updateExchangeFromUrl } from "./exchanges.js";
-import {
- isWithdrawableDenom,
- selectWithdrawalDenominations,
-} from "./withdraw.js";
-
-const logger = new Logger("refresh.ts");
-
-/**
- * Get the amount that we lose when refreshing a coin of the given denomination
- * with a certain amount left.
- *
- * If the amount left is zero, then the refresh cost
- * is also considered to be zero. If a refresh isn't possible (e.g. due to lack of
- * the right denominations), then the cost is the full amount left.
- *
- * Considers refresh fees, withdrawal fees after refresh and amounts too small
- * to refresh.
- */
-export function getTotalRefreshCost(
- denoms: DenominationRecord[],
- refreshedDenom: DenominationInfo,
- amountLeft: AmountJson,
-): AmountJson {
- const withdrawAmount = Amounts.sub(
- amountLeft,
- refreshedDenom.feeRefresh,
- ).amount;
- const denomMap = Object.fromEntries(denoms.map((x) => [x.denomPubHash, x]));
- const withdrawDenoms = selectWithdrawalDenominations(withdrawAmount, denoms);
- const resultingAmount = Amounts.add(
- Amounts.zeroOfCurrency(withdrawAmount.currency),
- ...withdrawDenoms.selectedDenoms.map(
- (d) =>
- Amounts.mult(
- DenominationRecord.getValue(denomMap[d.denomPubHash]),
- d.count,
- ).amount,
- ),
- ).amount;
- const totalCost = Amounts.sub(amountLeft, resultingAmount).amount;
- logger.trace(
- `total refresh cost for ${amountToPretty(amountLeft)} is ${amountToPretty(
- totalCost,
- )}`,
- );
- return totalCost;
-}
-
-function updateGroupStatus(rg: RefreshGroupRecord): void {
- const allDone = fnutil.all(
- rg.statusPerCoin,
- (x) => x === RefreshCoinStatus.Finished || x === RefreshCoinStatus.Frozen,
- );
- const anyFrozen = fnutil.any(
- rg.statusPerCoin,
- (x) => x === RefreshCoinStatus.Frozen,
- );
- if (allDone) {
- if (anyFrozen) {
- rg.timestampFinished = AbsoluteTime.toTimestamp(AbsoluteTime.now());
- rg.operationStatus = RefreshOperationStatus.FinishedWithError;
- } else {
- rg.timestampFinished = AbsoluteTime.toTimestamp(AbsoluteTime.now());
- rg.operationStatus = RefreshOperationStatus.Finished;
- }
- }
-}
-
-/**
- * Create a refresh session for one particular coin inside a refresh group.
- */
-async function refreshCreateSession(
- ws: InternalWalletState,
- refreshGroupId: string,
- coinIndex: number,
-): Promise<void> {
- logger.trace(
- `creating refresh session for coin ${coinIndex} in refresh group ${refreshGroupId}`,
- );
-
- const d = await ws.db
- .mktx((x) => [x.refreshGroups, x.coins])
- .runReadWrite(async (tx) => {
- const refreshGroup = await tx.refreshGroups.get(refreshGroupId);
- if (!refreshGroup) {
- return;
- }
- if (
- refreshGroup.statusPerCoin[coinIndex] === RefreshCoinStatus.Finished
- ) {
- return;
- }
- const existingRefreshSession =
- refreshGroup.refreshSessionPerCoin[coinIndex];
- if (existingRefreshSession) {
- return;
- }
- const oldCoinPub = refreshGroup.oldCoinPubs[coinIndex];
- const coin = await tx.coins.get(oldCoinPub);
- if (!coin) {
- throw Error("Can't refresh, coin not found");
- }
- return { refreshGroup, coin };
- });
-
- if (!d) {
- return;
- }
-
- const { refreshGroup, coin } = d;
-
- const { exchange } = await updateExchangeFromUrl(ws, coin.exchangeBaseUrl);
- if (!exchange) {
- throw Error("db inconsistent: exchange of coin not found");
- }
-
- // FIXME: use helper functions from withdraw.ts
- // to update and filter withdrawable denoms.
-
- const { availableAmount, availableDenoms } = await ws.db
- .mktx((x) => [x.denominations])
- .runReadOnly(async (tx) => {
- const oldDenom = await ws.getDenomInfo(
- ws,
- tx,
- exchange.baseUrl,
- coin.denomPubHash,
- );
-
- if (!oldDenom) {
- throw Error("db inconsistent: denomination for coin not found");
- }
-
- // FIXME: use an index here, based on the withdrawal expiration time.
- const availableDenoms: DenominationRecord[] =
- await tx.denominations.indexes.byExchangeBaseUrl
- .iter(exchange.baseUrl)
- .toArray();
-
- const availableAmount = Amounts.sub(
- refreshGroup.inputPerCoin[coinIndex],
- oldDenom.feeRefresh,
- ).amount;
- return { availableAmount, availableDenoms };
- });
-
- const newCoinDenoms = selectWithdrawalDenominations(
- availableAmount,
- availableDenoms,
- );
-
- if (newCoinDenoms.selectedDenoms.length === 0) {
- logger.trace(
- `not refreshing, available amount ${amountToPretty(
- availableAmount,
- )} too small`,
- );
- await ws.db
- .mktx((x) => [x.coins, x.refreshGroups])
- .runReadWrite(async (tx) => {
- const rg = await tx.refreshGroups.get(refreshGroupId);
- if (!rg) {
- return;
- }
- rg.statusPerCoin[coinIndex] = RefreshCoinStatus.Finished;
- updateGroupStatus(rg);
-
- await tx.refreshGroups.put(rg);
- });
- ws.notify({ type: NotificationType.RefreshUnwarranted });
- return;
- }
-
- const sessionSecretSeed = encodeCrock(getRandomBytes(64));
-
- // Store refresh session for this coin in the database.
- await ws.db
- .mktx((x) => [x.refreshGroups, x.coins])
- .runReadWrite(async (tx) => {
- const rg = await tx.refreshGroups.get(refreshGroupId);
- if (!rg) {
- return;
- }
- if (rg.refreshSessionPerCoin[coinIndex]) {
- return;
- }
- rg.refreshSessionPerCoin[coinIndex] = {
- norevealIndex: undefined,
- sessionSecretSeed: sessionSecretSeed,
- newDenoms: newCoinDenoms.selectedDenoms.map((x) => ({
- count: x.count,
- denomPubHash: x.denomPubHash,
- })),
- amountRefreshOutput: Amounts.stringify(newCoinDenoms.totalCoinValue),
- };
- await tx.refreshGroups.put(rg);
- });
- logger.info(
- `created refresh session for coin #${coinIndex} in ${refreshGroupId}`,
- );
- ws.notify({ type: NotificationType.RefreshStarted });
-}
-
-function getRefreshRequestTimeout(rg: RefreshGroupRecord): Duration {
- return Duration.fromSpec({
- seconds: 5,
- });
-}
-
-async function refreshMelt(
- ws: InternalWalletState,
- refreshGroupId: string,
- coinIndex: number,
-): Promise<void> {
- const d = await ws.db
- .mktx((x) => [x.refreshGroups, x.coins, x.denominations])
- .runReadWrite(async (tx) => {
- const refreshGroup = await tx.refreshGroups.get(refreshGroupId);
- if (!refreshGroup) {
- return;
- }
- const refreshSession = refreshGroup.refreshSessionPerCoin[coinIndex];
- if (!refreshSession) {
- return;
- }
- if (refreshSession.norevealIndex !== undefined) {
- return;
- }
-
- const oldCoin = await tx.coins.get(refreshGroup.oldCoinPubs[coinIndex]);
- checkDbInvariant(!!oldCoin, "melt coin doesn't exist");
- const oldDenom = await ws.getDenomInfo(
- ws,
- tx,
- oldCoin.exchangeBaseUrl,
- oldCoin.denomPubHash,
- );
- checkDbInvariant(
- !!oldDenom,
- "denomination for melted coin doesn't exist",
- );
-
- const newCoinDenoms: RefreshNewDenomInfo[] = [];
-
- for (const dh of refreshSession.newDenoms) {
- const newDenom = await ws.getDenomInfo(
- ws,
- tx,
- oldCoin.exchangeBaseUrl,
- dh.denomPubHash,
- );
- checkDbInvariant(
- !!newDenom,
- "new denomination for refresh not in database",
- );
- newCoinDenoms.push({
- count: dh.count,
- denomPub: newDenom.denomPub,
- denomPubHash: newDenom.denomPubHash,
- feeWithdraw: newDenom.feeWithdraw,
- value: Amounts.stringify(newDenom.value),
- });
- }
- return { newCoinDenoms, oldCoin, oldDenom, refreshGroup, refreshSession };
- });
-
- if (!d) {
- return;
- }
-
- const { newCoinDenoms, oldCoin, oldDenom, refreshGroup, refreshSession } = d;
-
- let exchangeProtocolVersion: ExchangeProtocolVersion;
- switch (d.oldDenom.denomPub.cipher) {
- case DenomKeyType.Rsa: {
- exchangeProtocolVersion = ExchangeProtocolVersion.V12;
- break;
- }
- default:
- throw Error("unsupported key type");
- }
-
- const derived = await ws.cryptoApi.deriveRefreshSession({
- exchangeProtocolVersion,
- kappa: 3,
- meltCoinDenomPubHash: oldCoin.denomPubHash,
- meltCoinPriv: oldCoin.coinPriv,
- meltCoinPub: oldCoin.coinPub,
- feeRefresh: Amounts.parseOrThrow(oldDenom.feeRefresh),
- meltCoinMaxAge: oldCoin.maxAge,
- meltCoinAgeCommitmentProof: oldCoin.ageCommitmentProof,
- newCoinDenoms,
- sessionSecretSeed: refreshSession.sessionSecretSeed,
- });
-
- const reqUrl = new URL(
- `coins/${oldCoin.coinPub}/melt`,
- oldCoin.exchangeBaseUrl,
- );
-
- let maybeAch: HashCodeString | undefined;
- if (oldCoin.ageCommitmentProof) {
- maybeAch = AgeRestriction.hashCommitment(
- oldCoin.ageCommitmentProof.commitment,
- );
- }
-
- const meltReqBody: ExchangeMeltRequest = {
- coin_pub: oldCoin.coinPub,
- confirm_sig: derived.confirmSig,
- denom_pub_hash: oldCoin.denomPubHash,
- denom_sig: oldCoin.denomSig,
- rc: derived.hash,
- value_with_fee: Amounts.stringify(derived.meltValueWithFee),
- age_commitment_hash: maybeAch,
- };
-
- const resp = await ws.runSequentialized([EXCHANGE_COINS_LOCK], async () => {
- return await ws.http.postJson(reqUrl.href, meltReqBody, {
- timeout: getRefreshRequestTimeout(refreshGroup),
- });
- });
-
- if (resp.status === HttpStatusCode.NotFound) {
- const errDetails = await readUnexpectedResponseDetails(resp);
- await ws.db
- .mktx((x) => [x.refreshGroups])
- .runReadWrite(async (tx) => {
- const rg = await tx.refreshGroups.get(refreshGroupId);
- if (!rg) {
- return;
- }
- if (rg.timestampFinished) {
- return;
- }
- if (rg.statusPerCoin[coinIndex] !== RefreshCoinStatus.Pending) {
- return;
- }
- rg.statusPerCoin[coinIndex] = RefreshCoinStatus.Frozen;
- rg.lastErrorPerCoin[coinIndex] = errDetails;
- updateGroupStatus(rg);
- await tx.refreshGroups.put(rg);
- });
- return;
- }
-
- if (resp.status === HttpStatusCode.Conflict) {
- // Just log for better diagnostics here, error status
- // will be handled later.
- logger.error(
- `melt request for ${Amounts.stringify(
- derived.meltValueWithFee,
- )} failed in refresh group ${refreshGroupId} due to conflict`,
- );
- }
-
- const meltResponse = await readSuccessResponseJsonOrThrow(
- resp,
- codecForExchangeMeltResponse(),
- );
-
- const norevealIndex = meltResponse.noreveal_index;
-
- refreshSession.norevealIndex = norevealIndex;
-
- await ws.db
- .mktx((x) => [x.refreshGroups])
- .runReadWrite(async (tx) => {
- const rg = await tx.refreshGroups.get(refreshGroupId);
- if (!rg) {
- return;
- }
- if (rg.timestampFinished) {
- return;
- }
- const rs = rg.refreshSessionPerCoin[coinIndex];
- if (!rs) {
- return;
- }
- if (rs.norevealIndex !== undefined) {
- return;
- }
- rs.norevealIndex = norevealIndex;
- await tx.refreshGroups.put(rg);
- });
-
- ws.notify({
- type: NotificationType.RefreshMelted,
- });
-}
-
-export async function assembleRefreshRevealRequest(args: {
- cryptoApi: TalerCryptoInterface;
- derived: DerivedRefreshSession;
- norevealIndex: number;
- oldCoinPub: CoinPublicKeyString;
- oldCoinPriv: string;
- newDenoms: {
- denomPubHash: string;
- count: number;
- }[];
- oldAgeCommitment?: AgeCommitment;
-}): Promise<ExchangeRefreshRevealRequest> {
- const {
- derived,
- norevealIndex,
- cryptoApi,
- oldCoinPriv,
- oldCoinPub,
- newDenoms,
- } = args;
- const privs = Array.from(derived.transferPrivs);
- privs.splice(norevealIndex, 1);
-
- const planchets = derived.planchetsForGammas[norevealIndex];
- if (!planchets) {
- throw Error("refresh index error");
- }
-
- const newDenomsFlat: string[] = [];
- const linkSigs: string[] = [];
-
- for (let i = 0; i < newDenoms.length; i++) {
- const dsel = newDenoms[i];
- for (let j = 0; j < dsel.count; j++) {
- const newCoinIndex = linkSigs.length;
- const linkSig = await cryptoApi.signCoinLink({
- coinEv: planchets[newCoinIndex].coinEv,
- newDenomHash: dsel.denomPubHash,
- oldCoinPriv: oldCoinPriv,
- oldCoinPub: oldCoinPub,
- transferPub: derived.transferPubs[norevealIndex],
- });
- linkSigs.push(linkSig.sig);
- newDenomsFlat.push(dsel.denomPubHash);
- }
- }
-
- const req: ExchangeRefreshRevealRequest = {
- coin_evs: planchets.map((x) => x.coinEv),
- new_denoms_h: newDenomsFlat,
- transfer_privs: privs,
- transfer_pub: derived.transferPubs[norevealIndex],
- link_sigs: linkSigs,
- old_age_commitment: args.oldAgeCommitment?.publicKeys,
- };
- return req;
-}
-
-async function refreshReveal(
- ws: InternalWalletState,
- refreshGroupId: string,
- coinIndex: number,
-): Promise<void> {
- logger.info(
- `doing refresh reveal for ${refreshGroupId} (old coin ${coinIndex})`,
- );
- const d = await ws.db
- .mktx((x) => [x.refreshGroups, x.coins, x.denominations])
- .runReadOnly(async (tx) => {
- const refreshGroup = await tx.refreshGroups.get(refreshGroupId);
- if (!refreshGroup) {
- return;
- }
- const refreshSession = refreshGroup.refreshSessionPerCoin[coinIndex];
- if (!refreshSession) {
- return;
- }
- const norevealIndex = refreshSession.norevealIndex;
- if (norevealIndex === undefined) {
- throw Error("can't reveal without melting first");
- }
-
- const oldCoin = await tx.coins.get(refreshGroup.oldCoinPubs[coinIndex]);
- checkDbInvariant(!!oldCoin, "melt coin doesn't exist");
- const oldDenom = await ws.getDenomInfo(
- ws,
- tx,
- oldCoin.exchangeBaseUrl,
- oldCoin.denomPubHash,
- );
- checkDbInvariant(
- !!oldDenom,
- "denomination for melted coin doesn't exist",
- );
-
- const newCoinDenoms: RefreshNewDenomInfo[] = [];
-
- for (const dh of refreshSession.newDenoms) {
- const newDenom = await ws.getDenomInfo(
- ws,
- tx,
- oldCoin.exchangeBaseUrl,
- dh.denomPubHash,
- );
- checkDbInvariant(
- !!newDenom,
- "new denomination for refresh not in database",
- );
- newCoinDenoms.push({
- count: dh.count,
- denomPub: newDenom.denomPub,
- denomPubHash: newDenom.denomPubHash,
- feeWithdraw: newDenom.feeWithdraw,
- value: Amounts.stringify(newDenom.value),
- });
- }
- return {
- oldCoin,
- oldDenom,
- newCoinDenoms,
- refreshSession,
- refreshGroup,
- norevealIndex,
- };
- });
-
- if (!d) {
- return;
- }
-
- const {
- oldCoin,
- oldDenom,
- newCoinDenoms,
- refreshSession,
- refreshGroup,
- norevealIndex,
- } = d;
-
- let exchangeProtocolVersion: ExchangeProtocolVersion;
- switch (d.oldDenom.denomPub.cipher) {
- case DenomKeyType.Rsa: {
- exchangeProtocolVersion = ExchangeProtocolVersion.V12;
- break;
- }
- default:
- throw Error("unsupported key type");
- }
-
- const derived = await ws.cryptoApi.deriveRefreshSession({
- exchangeProtocolVersion,
- kappa: 3,
- meltCoinDenomPubHash: oldCoin.denomPubHash,
- meltCoinPriv: oldCoin.coinPriv,
- meltCoinPub: oldCoin.coinPub,
- feeRefresh: Amounts.parseOrThrow(oldDenom.feeRefresh),
- newCoinDenoms,
- meltCoinMaxAge: oldCoin.maxAge,
- meltCoinAgeCommitmentProof: oldCoin.ageCommitmentProof,
- sessionSecretSeed: refreshSession.sessionSecretSeed,
- });
-
- const reqUrl = new URL(
- `refreshes/${derived.hash}/reveal`,
- oldCoin.exchangeBaseUrl,
- );
-
- const req = await assembleRefreshRevealRequest({
- cryptoApi: ws.cryptoApi,
- derived,
- newDenoms: newCoinDenoms,
- norevealIndex: norevealIndex,
- oldCoinPriv: oldCoin.coinPriv,
- oldCoinPub: oldCoin.coinPub,
- oldAgeCommitment: oldCoin.ageCommitmentProof?.commitment,
- });
-
- const resp = await ws.runSequentialized([EXCHANGE_COINS_LOCK], async () => {
- return await ws.http.postJson(reqUrl.href, req, {
- timeout: getRefreshRequestTimeout(refreshGroup),
- });
- });
-
- const reveal = await readSuccessResponseJsonOrThrow(
- resp,
- codecForExchangeRevealResponse(),
- );
-
- const coins: CoinRecord[] = [];
-
- for (let i = 0; i < refreshSession.newDenoms.length; i++) {
- const ncd = newCoinDenoms[i];
- for (let j = 0; j < refreshSession.newDenoms[i].count; j++) {
- const newCoinIndex = coins.length;
- const pc = derived.planchetsForGammas[norevealIndex][newCoinIndex];
- if (ncd.denomPub.cipher !== DenomKeyType.Rsa) {
- throw Error("cipher unsupported");
- }
- const evSig = reveal.ev_sigs[newCoinIndex].ev_sig;
- const denomSig = await ws.cryptoApi.unblindDenominationSignature({
- planchet: {
- blindingKey: pc.blindingKey,
- denomPub: ncd.denomPub,
- },
- evSig,
- });
- const coin: CoinRecord = {
- blindingKey: pc.blindingKey,
- coinPriv: pc.coinPriv,
- coinPub: pc.coinPub,
- denomPubHash: ncd.denomPubHash,
- denomSig,
- exchangeBaseUrl: oldCoin.exchangeBaseUrl,
- status: CoinStatus.Fresh,
- coinSource: {
- type: CoinSourceType.Refresh,
- refreshGroupId,
- oldCoinPub: refreshGroup.oldCoinPubs[coinIndex],
- },
- coinEvHash: pc.coinEvHash,
- maxAge: pc.maxAge,
- ageCommitmentProof: pc.ageCommitmentProof,
- spendAllocation: undefined,
- };
-
- coins.push(coin);
- }
- }
-
- await ws.db
- .mktx((x) => [
- x.coins,
- x.denominations,
- x.coinAvailability,
- x.refreshGroups,
- ])
- .runReadWrite(async (tx) => {
- const rg = await tx.refreshGroups.get(refreshGroupId);
- if (!rg) {
- logger.warn("no refresh session found");
- return;
- }
- const rs = rg.refreshSessionPerCoin[coinIndex];
- if (!rs) {
- return;
- }
- rg.statusPerCoin[coinIndex] = RefreshCoinStatus.Finished;
- updateGroupStatus(rg);
- for (const coin of coins) {
- await makeCoinAvailable(ws, tx, coin);
- }
- await tx.refreshGroups.put(rg);
- });
- logger.trace("refresh finished (end of reveal)");
- ws.notify({
- type: NotificationType.RefreshRevealed,
- });
-}
-
-export async function processRefreshGroup(
- ws: InternalWalletState,
- refreshGroupId: string,
- options: {
- forceNow?: boolean;
- } = {},
-): Promise<OperationAttemptResult> {
- logger.info(`processing refresh group ${refreshGroupId}`);
-
- const refreshGroup = await ws.db
- .mktx((x) => [x.refreshGroups])
- .runReadOnly(async (tx) => tx.refreshGroups.get(refreshGroupId));
- if (!refreshGroup) {
- return {
- type: OperationAttemptResultType.Finished,
- result: undefined,
- };
- }
- if (refreshGroup.timestampFinished) {
- return {
- type: OperationAttemptResultType.Finished,
- result: undefined,
- };
- }
- // Process refresh sessions of the group in parallel.
- logger.trace("processing refresh sessions for old coins");
- const ps = refreshGroup.oldCoinPubs.map((x, i) =>
- processRefreshSession(ws, refreshGroupId, i).catch((x) => {
- if (x instanceof CryptoApiStoppedError) {
- logger.info(
- "crypto API stopped while processing refresh group, probably the wallet is currently shutting down.",
- );
- } else 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}`);
- }
- }),
- );
- try {
- logger.trace("waiting for refreshes");
- await Promise.all(ps);
- logger.trace("refresh finished");
- } catch (e) {
- logger.warn("process refresh sessions got exception");
- logger.warn(`exception: ${e}`);
- }
- return {
- type: OperationAttemptResultType.Finished,
- result: undefined,
- };
-}
-
-async function processRefreshSession(
- ws: InternalWalletState,
- refreshGroupId: string,
- coinIndex: number,
-): Promise<void> {
- logger.info(
- `processing refresh session for coin ${coinIndex} of group ${refreshGroupId}`,
- );
- let refreshGroup = await ws.db
- .mktx((x) => [x.refreshGroups])
- .runReadOnly(async (tx) => {
- return tx.refreshGroups.get(refreshGroupId);
- });
- if (!refreshGroup) {
- return;
- }
- if (refreshGroup.statusPerCoin[coinIndex] === RefreshCoinStatus.Finished) {
- return;
- }
- if (!refreshGroup.refreshSessionPerCoin[coinIndex]) {
- await refreshCreateSession(ws, refreshGroupId, coinIndex);
- refreshGroup = await ws.db
- .mktx((x) => [x.refreshGroups])
- .runReadOnly(async (tx) => {
- return tx.refreshGroups.get(refreshGroupId);
- });
- if (!refreshGroup) {
- return;
- }
- }
- const refreshSession = refreshGroup.refreshSessionPerCoin[coinIndex];
- if (!refreshSession) {
- if (refreshGroup.statusPerCoin[coinIndex] !== RefreshCoinStatus.Finished) {
- throw Error(
- "BUG: refresh session was not created and coin not marked as finished",
- );
- }
- return;
- }
- if (refreshSession.norevealIndex === undefined) {
- await refreshMelt(ws, refreshGroupId, coinIndex);
- }
- await refreshReveal(ws, refreshGroupId, coinIndex);
-}
-
-/**
- * Create a refresh group for a list of coins.
- *
- * Refreshes the remaining amount on the coin, effectively capturing the remaining
- * value in the refresh group.
- *
- * The caller must also ensure that the coins that should be refreshed exist
- * in the current database transaction.
- */
-export async function createRefreshGroup(
- ws: InternalWalletState,
- tx: GetReadWriteAccess<{
- denominations: typeof WalletStoresV1.denominations;
- coins: typeof WalletStoresV1.coins;
- refreshGroups: typeof WalletStoresV1.refreshGroups;
- coinAvailability: typeof WalletStoresV1.coinAvailability;
- }>,
- oldCoinPubs: CoinRefreshRequest[],
- reason: RefreshReason,
-): Promise<RefreshGroupId> {
- const refreshGroupId = encodeCrock(getRandomBytes(32));
-
- const inputPerCoin: AmountJson[] = [];
- const estimatedOutputPerCoin: AmountJson[] = [];
-
- const denomsPerExchange: Record<string, DenominationRecord[]> = {};
-
- const getDenoms = async (
- exchangeBaseUrl: string,
- ): Promise<DenominationRecord[]> => {
- if (denomsPerExchange[exchangeBaseUrl]) {
- return denomsPerExchange[exchangeBaseUrl];
- }
- const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl
- .iter(exchangeBaseUrl)
- .filter((x) => {
- return isWithdrawableDenom(x);
- });
- denomsPerExchange[exchangeBaseUrl] = allDenoms;
- return allDenoms;
- };
-
- for (const ocp of oldCoinPubs) {
- const coin = await tx.coins.get(ocp.coinPub);
- checkDbInvariant(!!coin, "coin must be in database");
- const denom = await ws.getDenomInfo(
- ws,
- tx,
- coin.exchangeBaseUrl,
- coin.denomPubHash,
- );
- checkDbInvariant(
- !!denom,
- "denomination for existing coin must be in database",
- );
- switch (coin.status) {
- case CoinStatus.Dormant:
- break;
- case CoinStatus.Fresh: {
- coin.status = CoinStatus.Dormant;
- const coinAv = await tx.coinAvailability.get([
- coin.exchangeBaseUrl,
- coin.denomPubHash,
- coin.maxAge,
- ]);
- checkDbInvariant(!!coinAv);
- checkDbInvariant(coinAv.freshCoinCount > 0);
- coinAv.freshCoinCount--;
- await tx.coinAvailability.put(coinAv);
- break;
- }
- case CoinStatus.FreshSuspended: {
- // For suspended coins, we don't have to adjust coin
- // availability, as they are not counted as available.
- coin.status = CoinStatus.Dormant;
- break;
- }
- default:
- assertUnreachable(coin.status);
- }
- if (!coin.spendAllocation) {
- coin.spendAllocation = {
- amount: Amounts.stringify(ocp.amount),
- id: `txn:refresh:${refreshGroupId}`,
- };
- }
- const refreshAmount = ocp.amount;
- inputPerCoin.push(Amounts.parseOrThrow(refreshAmount));
- await tx.coins.put(coin);
- const denoms = await getDenoms(coin.exchangeBaseUrl);
- const cost = getTotalRefreshCost(
- denoms,
- denom,
- Amounts.parseOrThrow(refreshAmount),
- );
- const output = Amounts.sub(refreshAmount, cost).amount;
- estimatedOutputPerCoin.push(output);
- }
-
- const refreshGroup: RefreshGroupRecord = {
- operationStatus: RefreshOperationStatus.Pending,
- timestampFinished: undefined,
- statusPerCoin: oldCoinPubs.map(() => RefreshCoinStatus.Pending),
- oldCoinPubs: oldCoinPubs.map((x) => x.coinPub),
- lastErrorPerCoin: {},
- reason,
- refreshGroupId,
- refreshSessionPerCoin: oldCoinPubs.map(() => undefined),
- inputPerCoin: inputPerCoin.map((x) => Amounts.stringify(x)),
- estimatedOutputPerCoin: estimatedOutputPerCoin.map((x) =>
- Amounts.stringify(x),
- ),
- timestampCreated: TalerProtocolTimestamp.now(),
- };
-
- if (oldCoinPubs.length == 0) {
- logger.warn("created refresh group with zero coins");
- refreshGroup.timestampFinished = TalerProtocolTimestamp.now();
- refreshGroup.operationStatus = RefreshOperationStatus.Finished;
- }
-
- await tx.refreshGroups.put(refreshGroup);
-
- logger.info(`created refresh group ${refreshGroupId}`);
-
- processRefreshGroup(ws, refreshGroupId).catch((e) => {
- if (e instanceof CryptoApiStoppedError) {
- return;
- }
- logger.warn(`processing refresh group ${refreshGroupId} failed: ${e}`);
- });
-
- return {
- refreshGroupId,
- };
-}
-
-/**
- * Timestamp after which the wallet would do the next check for an auto-refresh.
- */
-function getAutoRefreshCheckThreshold(d: DenominationRecord): AbsoluteTime {
- const expireWithdraw = AbsoluteTime.fromTimestamp(d.stampExpireWithdraw);
- const expireDeposit = AbsoluteTime.fromTimestamp(d.stampExpireDeposit);
- const delta = AbsoluteTime.difference(expireWithdraw, expireDeposit);
- const deltaDiv = durationMul(delta, 0.75);
- return AbsoluteTime.addDuration(expireWithdraw, deltaDiv);
-}
-
-/**
- * Timestamp after which the wallet would do an auto-refresh.
- */
-function getAutoRefreshExecuteThreshold(d: DenominationRecord): AbsoluteTime {
- const expireWithdraw = AbsoluteTime.fromTimestamp(d.stampExpireWithdraw);
- const expireDeposit = AbsoluteTime.fromTimestamp(d.stampExpireDeposit);
- const delta = AbsoluteTime.difference(expireWithdraw, expireDeposit);
- const deltaDiv = durationMul(delta, 0.5);
- return AbsoluteTime.addDuration(expireWithdraw, deltaDiv);
-}
-
-export async function autoRefresh(
- ws: InternalWalletState,
- exchangeBaseUrl: string,
-): Promise<OperationAttemptResult> {
- logger.info(`doing auto-refresh check for '${exchangeBaseUrl}'`);
-
- // We must make sure that the exchange is up-to-date so that
- // can refresh into new denominations.
- await updateExchangeFromUrl(ws, exchangeBaseUrl, {
- forceNow: true,
- });
-
- let minCheckThreshold = AbsoluteTime.addDuration(
- AbsoluteTime.now(),
- durationFromSpec({ days: 1 }),
- );
- await ws.db
- .mktx((x) => [
- x.coins,
- x.denominations,
- x.coinAvailability,
- x.refreshGroups,
- x.exchanges,
- ])
- .runReadWrite(async (tx) => {
- const exchange = await tx.exchanges.get(exchangeBaseUrl);
- if (!exchange) {
- return;
- }
- const coins = await tx.coins.indexes.byBaseUrl
- .iter(exchangeBaseUrl)
- .toArray();
- const refreshCoins: CoinRefreshRequest[] = [];
- for (const coin of coins) {
- if (coin.status !== CoinStatus.Fresh) {
- continue;
- }
- const denom = await tx.denominations.get([
- exchangeBaseUrl,
- coin.denomPubHash,
- ]);
- if (!denom) {
- logger.warn("denomination not in database");
- continue;
- }
- const executeThreshold = getAutoRefreshExecuteThreshold(denom);
- if (AbsoluteTime.isExpired(executeThreshold)) {
- refreshCoins.push({
- coinPub: coin.coinPub,
- amount: Amounts.stringify({
- value: denom.amountVal,
- fraction: denom.amountFrac,
- currency: denom.currency,
- }),
- });
- } else {
- const checkThreshold = getAutoRefreshCheckThreshold(denom);
- minCheckThreshold = AbsoluteTime.min(
- minCheckThreshold,
- checkThreshold,
- );
- }
- }
- if (refreshCoins.length > 0) {
- const res = await createRefreshGroup(
- ws,
- tx,
- refreshCoins,
- RefreshReason.Scheduled,
- );
- logger.info(
- `created refresh group for auto-refresh (${res.refreshGroupId})`,
- );
- }
- logger.info(
- `current wallet time: ${AbsoluteTime.toIsoString(AbsoluteTime.now())}`,
- );
- logger.info(
- `next refresh check at ${AbsoluteTime.toIsoString(minCheckThreshold)}`,
- );
- exchange.nextRefreshCheck = AbsoluteTime.toTimestamp(minCheckThreshold);
- await tx.exchanges.put(exchange);
- });
- return OperationAttemptResult.finishedEmpty();
-}
diff --git a/packages/taler-wallet-core/src/operations/testing.ts b/packages/taler-wallet-core/src/operations/testing.ts
deleted file mode 100644
index 50454a920..000000000
--- a/packages/taler-wallet-core/src/operations/testing.ts
+++ /dev/null
@@ -1,478 +0,0 @@
-/*
- 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 {
- base64FromArrayBuffer,
- ConfirmPayResultType,
- Logger,
- stringToBytes,
- TestPayResult,
- WithdrawTestBalanceRequest,
-} from "@gnu-taler/taler-util";
-import {
- HttpRequestLibrary,
- readSuccessResponseJsonOrThrow,
- checkSuccessResponseOrThrow,
-} from "../util/http.js";
-import {
- AmountString,
- codecForAny,
- CheckPaymentResponse,
- codecForCheckPaymentResponse,
- IntegrationTestArgs,
- Amounts,
- TestPayArgs,
- URL,
- PreparePayResultType,
-} from "@gnu-taler/taler-util";
-import { InternalWalletState } from "../internal-wallet-state.js";
-import { applyRefund, confirmPay, preparePayForUri } from "./pay-merchant.js";
-import { getBalances } from "./balance.js";
-import { checkLogicInvariant } from "../util/invariants.js";
-import { acceptWithdrawalFromUri } from "./withdraw.js";
-
-const logger = new Logger("operations/testing.ts");
-
-interface BankUser {
- username: string;
- password: string;
-}
-
-interface BankWithdrawalResponse {
- taler_withdraw_uri: string;
- withdrawal_id: string;
-}
-
-interface MerchantBackendInfo {
- baseUrl: string;
- authToken?: string;
-}
-
-/**
- * Generate a random alphanumeric ID. Does *not* use cryptographically
- * secure randomness.
- */
-function makeId(length: number): string {
- let result = "";
- const characters =
- "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
- for (let i = 0; i < length; i++) {
- result += characters.charAt(Math.floor(Math.random() * characters.length));
- }
- return result;
-}
-
-/**
- * Helper function to generate the "Authorization" HTTP header.
- * FIXME: redundant, put in taler-util
- */
-function makeBasicAuthHeader(username: string, password: string): string {
- const auth = `${username}:${password}`;
- const authEncoded: string = base64FromArrayBuffer(stringToBytes(auth));
- return `Basic ${authEncoded}`;
-}
-
-export async function withdrawTestBalance(
- ws: InternalWalletState,
- req: WithdrawTestBalanceRequest,
-): Promise<void> {
- const amount = req.amount;
- const exchangeBaseUrl = req.exchangeBaseUrl;
- const bankAccessApiBaseUrl = req.bankAccessApiBaseUrl ?? req.bankBaseUrl;
-
- logger.trace(
- `Registered bank user, bank access base url ${bankAccessApiBaseUrl}`,
- );
- const bankUser = await registerRandomBankUser(ws.http, bankAccessApiBaseUrl);
- logger.trace(`Registered bank user ${JSON.stringify(bankUser)}`);
-
- const wresp = await createDemoBankWithdrawalUri(
- ws.http,
- bankAccessApiBaseUrl,
- bankUser,
- amount,
- );
-
- await acceptWithdrawalFromUri(ws, {
- talerWithdrawUri: wresp.taler_withdraw_uri,
- selectedExchange: exchangeBaseUrl,
- forcedDenomSel: req.forcedDenomSel,
- });
-
- await confirmBankWithdrawalUri(
- ws.http,
- bankAccessApiBaseUrl,
- bankUser,
- wresp.withdrawal_id,
- );
-}
-
-function getMerchantAuthHeader(m: MerchantBackendInfo): Record<string, string> {
- if (m.authToken) {
- return {
- Authorization: `Bearer ${m.authToken}`,
- };
- }
- return {};
-}
-
-/**
- * Use the testing API of a demobank to create a taler://withdraw URI
- * that the wallet can then use to make a withdrawal.
- */
-export async function createDemoBankWithdrawalUri(
- http: HttpRequestLibrary,
- bankAccessApiBaseUrl: string,
- bankUser: BankUser,
- amount: AmountString,
-): Promise<BankWithdrawalResponse> {
- const reqUrl = new URL(
- `accounts/${bankUser.username}/withdrawals`,
- bankAccessApiBaseUrl,
- ).href;
- const resp = await http.postJson(
- reqUrl,
- {
- amount,
- },
- {
- headers: {
- Authorization: makeBasicAuthHeader(
- bankUser.username,
- bankUser.password,
- ),
- },
- },
- );
- const respJson = await readSuccessResponseJsonOrThrow(resp, codecForAny());
- return respJson;
-}
-
-async function confirmBankWithdrawalUri(
- http: HttpRequestLibrary,
- bankAccessApiBaseUrl: string,
- bankUser: BankUser,
- withdrawalId: string,
-): Promise<void> {
- const reqUrl = new URL(
- `accounts/${bankUser.username}/withdrawals/${withdrawalId}/confirm`,
- bankAccessApiBaseUrl,
- ).href;
- const resp = await http.postJson(
- reqUrl,
- {},
- {
- headers: {
- Authorization: makeBasicAuthHeader(
- bankUser.username,
- bankUser.password,
- ),
- },
- },
- );
- await readSuccessResponseJsonOrThrow(resp, codecForAny());
- return;
-}
-
-async function registerRandomBankUser(
- http: HttpRequestLibrary,
- bankAccessApiBaseUrl: string,
-): Promise<BankUser> {
- const reqUrl = new URL("testing/register", bankAccessApiBaseUrl).href;
- const randId = makeId(8);
- const bankUser: BankUser = {
- // euFin doesn't allow resource names to have upper case letters.
- username: `testuser-${randId.toLowerCase()}`,
- password: `testpw-${randId}`,
- };
-
- const resp = await http.postJson(reqUrl, bankUser);
- await checkSuccessResponseOrThrow(resp);
- return bankUser;
-}
-
-async function refund(
- http: HttpRequestLibrary,
- merchantBackend: MerchantBackendInfo,
- orderId: string,
- reason: string,
- refundAmount: string,
-): Promise<string> {
- const reqUrl = new URL(
- `private/orders/${orderId}/refund`,
- merchantBackend.baseUrl,
- );
- const refundReq = {
- order_id: orderId,
- reason,
- refund: refundAmount,
- };
- const resp = await http.postJson(reqUrl.href, refundReq, {
- headers: getMerchantAuthHeader(merchantBackend),
- });
- const r = await readSuccessResponseJsonOrThrow(resp, codecForAny());
- const refundUri = r.taler_refund_uri;
- if (!refundUri) {
- throw Error("no refund URI in response");
- }
- return refundUri;
-}
-
-async function createOrder(
- http: HttpRequestLibrary,
- merchantBackend: MerchantBackendInfo,
- amount: string,
- summary: string,
- fulfillmentUrl: string,
-): Promise<{ orderId: string }> {
- const t = Math.floor(new Date().getTime() / 1000) + 15 * 60;
- const reqUrl = new URL("private/orders", merchantBackend.baseUrl).href;
- const orderReq = {
- order: {
- amount,
- summary,
- fulfillment_url: fulfillmentUrl,
- refund_deadline: { t_s: t },
- wire_transfer_deadline: { t_s: t },
- },
- };
- const resp = await http.postJson(reqUrl, orderReq, {
- headers: getMerchantAuthHeader(merchantBackend),
- });
- const r = await readSuccessResponseJsonOrThrow(resp, codecForAny());
- const orderId = r.order_id;
- if (!orderId) {
- throw Error("no order id in response");
- }
- return { orderId };
-}
-
-async function checkPayment(
- http: HttpRequestLibrary,
- merchantBackend: MerchantBackendInfo,
- orderId: string,
-): Promise<CheckPaymentResponse> {
- const reqUrl = new URL(`private/orders/${orderId}`, merchantBackend.baseUrl);
- reqUrl.searchParams.set("order_id", orderId);
- const resp = await http.get(reqUrl.href, {
- headers: getMerchantAuthHeader(merchantBackend),
- });
- return readSuccessResponseJsonOrThrow(resp, codecForCheckPaymentResponse());
-}
-
-interface BankUser {
- username: string;
- password: string;
-}
-
-interface BankWithdrawalResponse {
- taler_withdraw_uri: string;
- withdrawal_id: string;
-}
-
-async function makePayment(
- ws: InternalWalletState,
- merchant: MerchantBackendInfo,
- amount: string,
- summary: string,
-): Promise<{ orderId: string }> {
- const orderResp = await createOrder(
- ws.http,
- merchant,
- amount,
- summary,
- "taler://fulfillment-success/thx",
- );
-
- logger.trace("created order with orderId", orderResp.orderId);
-
- let paymentStatus = await checkPayment(ws.http, merchant, orderResp.orderId);
-
- logger.trace("payment status", paymentStatus);
-
- const talerPayUri = paymentStatus.taler_pay_uri;
- if (!talerPayUri) {
- throw Error("no taler://pay/ URI in payment response");
- }
-
- const preparePayResult = await preparePayForUri(ws, talerPayUri);
-
- logger.trace("prepare pay result", preparePayResult);
-
- if (preparePayResult.status != "payment-possible") {
- throw Error("payment not possible");
- }
-
- const confirmPayResult = await confirmPay(
- ws,
- preparePayResult.proposalId,
- undefined,
- );
-
- logger.trace("confirmPayResult", confirmPayResult);
-
- paymentStatus = await checkPayment(ws.http, merchant, orderResp.orderId);
-
- logger.trace("payment status after wallet payment:", paymentStatus);
-
- if (paymentStatus.order_status !== "paid") {
- throw Error("payment did not succeed");
- }
-
- return {
- orderId: orderResp.orderId,
- };
-}
-
-export async function runIntegrationTest(
- ws: InternalWalletState,
- args: IntegrationTestArgs,
-): Promise<void> {
- logger.info("running test with arguments", args);
-
- const parsedSpendAmount = Amounts.parseOrThrow(args.amountToSpend);
- const currency = parsedSpendAmount.currency;
-
- logger.info("withdrawing test balance");
- await withdrawTestBalance(ws, {
- amount: args.amountToWithdraw,
- bankBaseUrl: args.bankBaseUrl,
- bankAccessApiBaseUrl: args.bankAccessApiBaseUrl ?? args.bankBaseUrl,
- exchangeBaseUrl: args.exchangeBaseUrl,
- });
- await ws.runUntilDone();
- logger.info("done withdrawing test balance");
-
- const balance = await getBalances(ws);
-
- logger.trace(JSON.stringify(balance, null, 2));
-
- const myMerchant: MerchantBackendInfo = {
- baseUrl: args.merchantBaseUrl,
- authToken: args.merchantAuthToken,
- };
-
- await makePayment(ws, myMerchant, args.amountToSpend, "hello world");
-
- // Wait until the refresh is done
- await ws.runUntilDone();
-
- logger.trace("withdrawing test balance for refund");
- const withdrawAmountTwo = Amounts.parseOrThrow(`${currency}:18`);
- const spendAmountTwo = Amounts.parseOrThrow(`${currency}:7`);
- const refundAmount = Amounts.parseOrThrow(`${currency}:6`);
- const spendAmountThree = Amounts.parseOrThrow(`${currency}:3`);
-
- await withdrawTestBalance(ws, {
- amount: Amounts.stringify(withdrawAmountTwo),
- bankBaseUrl: args.bankBaseUrl,
- bankAccessApiBaseUrl: args.bankAccessApiBaseUrl ?? args.bankBaseUrl,
- exchangeBaseUrl: args.exchangeBaseUrl,
- });
-
- // Wait until the withdraw is done
- await ws.runUntilDone();
-
- const { orderId: refundOrderId } = await makePayment(
- ws,
- myMerchant,
- Amounts.stringify(spendAmountTwo),
- "order that will be refunded",
- );
-
- const refundUri = await refund(
- ws.http,
- myMerchant,
- refundOrderId,
- "test refund",
- Amounts.stringify(refundAmount),
- );
-
- logger.trace("refund URI", refundUri);
-
- await applyRefund(ws, refundUri);
-
- logger.trace("integration test: applied refund");
-
- // Wait until the refund is done
- await ws.runUntilDone();
-
- logger.trace("integration test: making payment after refund");
-
- await makePayment(
- ws,
- myMerchant,
- Amounts.stringify(spendAmountThree),
- "payment after refund",
- );
-
- logger.trace("integration test: make payment done");
-
- await ws.runUntilDone();
-
- logger.trace("integration test: all done!");
-}
-
-export async function testPay(
- ws: InternalWalletState,
- args: TestPayArgs,
-): Promise<TestPayResult> {
- logger.trace("creating order");
- const merchant = {
- authToken: args.merchantAuthToken,
- baseUrl: args.merchantBaseUrl,
- };
- const orderResp = await createOrder(
- ws.http,
- merchant,
- args.amount,
- args.summary,
- "taler://fulfillment-success/thank+you",
- );
- logger.trace("created new order with order ID", orderResp.orderId);
- const checkPayResp = await checkPayment(ws.http, merchant, orderResp.orderId);
- const talerPayUri = checkPayResp.taler_pay_uri;
- if (!talerPayUri) {
- console.error("fatal: no taler pay URI received from backend");
- process.exit(1);
- }
- logger.trace("taler pay URI:", talerPayUri);
- const result = await preparePayForUri(ws, talerPayUri);
- if (result.status !== PreparePayResultType.PaymentPossible) {
- throw Error(`unexpected prepare pay status: ${result.status}`);
- }
- const r = await confirmPay(
- ws,
- result.proposalId,
- undefined,
- args.forcedCoinSel,
- );
- if (r.type != ConfirmPayResultType.Done) {
- throw Error("payment not done");
- }
- const purchase = await ws.db
- .mktx((x) => [x.purchases])
- .runReadOnly(async (tx) => {
- return tx.purchases.get(result.proposalId);
- });
- checkLogicInvariant(!!purchase);
- return {
- payCoinSelection: purchase.payInfo?.payCoinSelection!,
- };
-}
diff --git a/packages/taler-wallet-core/src/operations/tip.ts b/packages/taler-wallet-core/src/operations/tip.ts
deleted file mode 100644
index f9d20fa03..000000000
--- a/packages/taler-wallet-core/src/operations/tip.ts
+++ /dev/null
@@ -1,370 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2019 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received 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 {
- AcceptTipResponse,
- AgeRestriction,
- Amounts,
- BlindedDenominationSignature,
- codecForMerchantTipResponseV2,
- codecForTipPickupGetResponse,
- CoinStatus,
- DenomKeyType,
- encodeCrock,
- getRandomBytes,
- j2s,
- Logger,
- parseTipUri,
- PrepareTipResult,
- TalerErrorCode,
- TalerProtocolTimestamp,
- TipPlanchetDetail,
- TransactionType,
- URL,
-} from "@gnu-taler/taler-util";
-import { DerivedTipPlanchet } from "../crypto/cryptoTypes.js";
-import {
- CoinRecord,
- CoinSourceType,
- DenominationRecord,
- TipRecord,
-} from "../db.js";
-import { makeErrorDetail } from "../errors.js";
-import { InternalWalletState } from "../internal-wallet-state.js";
-import {
- getHttpResponseErrorDetails,
- readSuccessResponseJsonOrThrow,
-} from "../util/http.js";
-import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
-import {
- OperationAttemptResult,
- OperationAttemptResultType,
-} from "../util/retries.js";
-import { makeCoinAvailable, makeTransactionId } from "./common.js";
-import { updateExchangeFromUrl } from "./exchanges.js";
-import {
- getCandidateWithdrawalDenoms,
- getExchangeWithdrawalInfo,
- selectWithdrawalDenominations,
- updateWithdrawalDenoms,
-} from "./withdraw.js";
-
-const logger = new Logger("operations/tip.ts");
-
-export async function prepareTip(
- ws: InternalWalletState,
- talerTipUri: string,
-): Promise<PrepareTipResult> {
- const res = parseTipUri(talerTipUri);
- if (!res) {
- throw Error("invalid taler://tip URI");
- }
-
- let tipRecord = await ws.db
- .mktx((x) => [x.tips])
- .runReadOnly(async (tx) => {
- return tx.tips.indexes.byMerchantTipIdAndBaseUrl.get([
- res.merchantTipId,
- res.merchantBaseUrl,
- ]);
- });
-
- if (!tipRecord) {
- const tipStatusUrl = new URL(
- `tips/${res.merchantTipId}`,
- res.merchantBaseUrl,
- );
- logger.trace("checking tip status from", tipStatusUrl.href);
- const merchantResp = await ws.http.get(tipStatusUrl.href);
- const tipPickupStatus = await readSuccessResponseJsonOrThrow(
- merchantResp,
- codecForTipPickupGetResponse(),
- );
- logger.trace(`status ${j2s(tipPickupStatus)}`);
-
- const amount = Amounts.parseOrThrow(tipPickupStatus.tip_amount);
-
- logger.trace("new tip, creating tip record");
- await updateExchangeFromUrl(ws, tipPickupStatus.exchange_url);
-
- //FIXME: is this needed? withdrawDetails is not used
- // * if the intention is to update the exchange information in the database
- // maybe we can use another name. `get` seems like a pure-function
- const withdrawDetails = await getExchangeWithdrawalInfo(
- ws,
- tipPickupStatus.exchange_url,
- amount,
- undefined,
- );
-
- const walletTipId = encodeCrock(getRandomBytes(32));
- await updateWithdrawalDenoms(ws, tipPickupStatus.exchange_url);
- const denoms = await getCandidateWithdrawalDenoms(
- ws,
- tipPickupStatus.exchange_url,
- );
- const selectedDenoms = selectWithdrawalDenominations(amount, denoms);
-
- const secretSeed = encodeCrock(getRandomBytes(64));
- const denomSelUid = encodeCrock(getRandomBytes(32));
-
- const newTipRecord: TipRecord = {
- walletTipId: walletTipId,
- acceptedTimestamp: undefined,
- tipAmountRaw: Amounts.stringify(amount),
- tipExpiration: tipPickupStatus.expiration,
- exchangeBaseUrl: tipPickupStatus.exchange_url,
- merchantBaseUrl: res.merchantBaseUrl,
- createdTimestamp: TalerProtocolTimestamp.now(),
- merchantTipId: res.merchantTipId,
- tipAmountEffective: Amounts.stringify(selectedDenoms.totalCoinValue),
- denomsSel: selectedDenoms,
- pickedUpTimestamp: undefined,
- secretSeed,
- denomSelUid,
- };
- await ws.db
- .mktx((x) => [x.tips])
- .runReadWrite(async (tx) => {
- await tx.tips.put(newTipRecord);
- });
- tipRecord = newTipRecord;
- }
-
- const tipStatus: PrepareTipResult = {
- accepted: !!tipRecord && !!tipRecord.acceptedTimestamp,
- tipAmountRaw: Amounts.stringify(tipRecord.tipAmountRaw),
- exchangeBaseUrl: tipRecord.exchangeBaseUrl,
- merchantBaseUrl: tipRecord.merchantBaseUrl,
- expirationTimestamp: tipRecord.tipExpiration,
- tipAmountEffective: Amounts.stringify(tipRecord.tipAmountEffective),
- walletTipId: tipRecord.walletTipId,
- };
-
- return tipStatus;
-}
-
-export async function processTip(
- ws: InternalWalletState,
- walletTipId: string,
- options: {
- forceNow?: boolean;
- } = {},
-): Promise<OperationAttemptResult> {
- const tipRecord = await ws.db
- .mktx((x) => [x.tips])
- .runReadOnly(async (tx) => {
- return tx.tips.get(walletTipId);
- });
- if (!tipRecord) {
- return {
- type: OperationAttemptResultType.Finished,
- result: undefined,
- };
- }
-
- if (tipRecord.pickedUpTimestamp) {
- logger.warn("tip already picked up");
- return {
- type: OperationAttemptResultType.Finished,
- result: undefined,
- };
- }
-
- const denomsForWithdraw = tipRecord.denomsSel;
-
- const planchets: DerivedTipPlanchet[] = [];
- // Planchets in the form that the merchant expects
- const planchetsDetail: TipPlanchetDetail[] = [];
- const denomForPlanchet: { [index: number]: DenominationRecord } = [];
-
- for (const dh of denomsForWithdraw.selectedDenoms) {
- const denom = await ws.db
- .mktx((x) => [x.denominations])
- .runReadOnly(async (tx) => {
- return tx.denominations.get([
- tipRecord.exchangeBaseUrl,
- dh.denomPubHash,
- ]);
- });
- checkDbInvariant(!!denom, "denomination should be in database");
- for (let i = 0; i < dh.count; i++) {
- const deriveReq = {
- denomPub: denom.denomPub,
- planchetIndex: planchets.length,
- secretSeed: tipRecord.secretSeed,
- };
- logger.trace(`deriving tip planchet: ${j2s(deriveReq)}`);
- const p = await ws.cryptoApi.createTipPlanchet(deriveReq);
- logger.trace(`derive result: ${j2s(p)}`);
- denomForPlanchet[planchets.length] = denom;
- planchets.push(p);
- planchetsDetail.push({
- coin_ev: p.coinEv,
- denom_pub_hash: denom.denomPubHash,
- });
- }
- }
-
- const tipStatusUrl = new URL(
- `tips/${tipRecord.merchantTipId}/pickup`,
- tipRecord.merchantBaseUrl,
- );
-
- const req = { planchets: planchetsDetail };
- logger.trace(`sending tip request: ${j2s(req)}`);
- const merchantResp = await ws.http.postJson(tipStatusUrl.href, req);
-
- logger.trace(`got tip response, status ${merchantResp.status}`);
-
- // FIXME: Why do we do this?
- if (
- (merchantResp.status >= 500 && merchantResp.status <= 599) ||
- merchantResp.status === 424
- ) {
- logger.trace(`got transient tip error`);
- // FIXME: wrap in another error code that indicates a transient error
- return {
- type: OperationAttemptResultType.Error,
- errorDetail: makeErrorDetail(
- TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
- getHttpResponseErrorDetails(merchantResp),
- "tip pickup failed (transient)",
- ),
- };
- }
- let blindedSigs: BlindedDenominationSignature[] = [];
-
- const response = await readSuccessResponseJsonOrThrow(
- merchantResp,
- codecForMerchantTipResponseV2(),
- );
- blindedSigs = response.blind_sigs.map((x) => x.blind_sig);
-
- if (blindedSigs.length !== planchets.length) {
- throw Error("number of tip responses does not match requested planchets");
- }
-
- const newCoinRecords: CoinRecord[] = [];
-
- for (let i = 0; i < blindedSigs.length; i++) {
- const blindedSig = blindedSigs[i];
-
- const denom = denomForPlanchet[i];
- checkLogicInvariant(!!denom);
- const planchet = planchets[i];
- checkLogicInvariant(!!planchet);
-
- if (denom.denomPub.cipher !== DenomKeyType.Rsa) {
- throw Error("unsupported cipher");
- }
-
- if (blindedSig.cipher !== DenomKeyType.Rsa) {
- throw Error("unsupported cipher");
- }
-
- const denomSigRsa = await ws.cryptoApi.rsaUnblind({
- bk: planchet.blindingKey,
- blindedSig: blindedSig.blinded_rsa_signature,
- pk: denom.denomPub.rsa_public_key,
- });
-
- const isValid = await ws.cryptoApi.rsaVerify({
- hm: planchet.coinPub,
- pk: denom.denomPub.rsa_public_key,
- sig: denomSigRsa.sig,
- });
-
- if (!isValid) {
- return {
- type: OperationAttemptResultType.Error,
- errorDetail: makeErrorDetail(
- TalerErrorCode.WALLET_TIPPING_COIN_SIGNATURE_INVALID,
- {},
- "invalid signature from the exchange (via merchant tip) after unblinding",
- ),
- };
- }
-
- newCoinRecords.push({
- blindingKey: planchet.blindingKey,
- coinPriv: planchet.coinPriv,
- coinPub: planchet.coinPub,
- coinSource: {
- type: CoinSourceType.Tip,
- coinIndex: i,
- walletTipId: walletTipId,
- },
- denomPubHash: denom.denomPubHash,
- denomSig: { cipher: DenomKeyType.Rsa, rsa_signature: denomSigRsa.sig },
- exchangeBaseUrl: tipRecord.exchangeBaseUrl,
- status: CoinStatus.Fresh,
- coinEvHash: planchet.coinEvHash,
- maxAge: AgeRestriction.AGE_UNRESTRICTED,
- ageCommitmentProof: planchet.ageCommitmentProof,
- spendAllocation: undefined,
- });
- }
-
- await ws.db
- .mktx((x) => [x.coins, x.coinAvailability, x.denominations, x.tips])
- .runReadWrite(async (tx) => {
- const tr = await tx.tips.get(walletTipId);
- if (!tr) {
- return;
- }
- if (tr.pickedUpTimestamp) {
- return;
- }
- tr.pickedUpTimestamp = TalerProtocolTimestamp.now();
- await tx.tips.put(tr);
- for (const cr of newCoinRecords) {
- await makeCoinAvailable(ws, tx, cr);
- }
- });
-
- return {
- type: OperationAttemptResultType.Finished,
- result: undefined,
- };
-}
-
-export async function acceptTip(
- ws: InternalWalletState,
- tipId: string,
-): Promise<AcceptTipResponse> {
- const found = await ws.db
- .mktx((x) => [x.tips])
- .runReadWrite(async (tx) => {
- const tipRecord = await tx.tips.get(tipId);
- if (!tipRecord) {
- logger.error("tip not found");
- return false;
- }
- tipRecord.acceptedTimestamp = TalerProtocolTimestamp.now();
- await tx.tips.put(tipRecord);
- return true;
- });
- if (found) {
- await processTip(ws, tipId);
- }
- return {
- transactionId: makeTransactionId(TransactionType.Tip, tipId),
- };
-}
diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts
deleted file mode 100644
index 3fb13d2a0..000000000
--- a/packages/taler-wallet-core/src/operations/transactions.ts
+++ /dev/null
@@ -1,1165 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2019 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- * Imports.
- */
-import {
- AbsoluteTime,
- AmountJson,
- Amounts,
- constructPayPullUri,
- constructPayPushUri,
- Logger,
- OrderShortInfo,
- PaymentStatus,
- PeerContractTerms,
- RefundInfoShort,
- TalerProtocolTimestamp,
- Transaction,
- TransactionByIdRequest,
- TransactionsRequest,
- TransactionsResponse,
- TransactionType,
- WithdrawalType,
-} from "@gnu-taler/taler-util";
-import {
- DepositGroupRecord,
- ExchangeDetailsRecord,
- OperationRetryRecord,
- PeerPullPaymentIncomingRecord,
- PeerPushPaymentInitiationRecord,
- PurchaseStatus,
- PurchaseRecord,
- RefundState,
- TipRecord,
- WalletRefundItem,
- WithdrawalGroupRecord,
- WithdrawalRecordType,
- WalletContractData,
- PeerPushPaymentInitiationStatus,
- PeerPullPaymentIncomingStatus,
-} from "../db.js";
-import { InternalWalletState } from "../internal-wallet-state.js";
-import { assertUnreachable } from "../util/assertUnreachable.js";
-import { checkDbInvariant } from "../util/invariants.js";
-import { RetryTags } from "../util/retries.js";
-import {
- makeTombstoneId,
- makeTransactionId,
- parseId,
- TombstoneTag,
-} from "./common.js";
-import { processDepositGroup } from "./deposits.js";
-import { getExchangeDetails } from "./exchanges.js";
-import {
- expectProposalDownload,
- extractContractData,
- processPurchasePay,
-} from "./pay-merchant.js";
-import { processRefreshGroup } from "./refresh.js";
-import { processTip } from "./tip.js";
-import {
- augmentPaytoUrisForWithdrawal,
- processWithdrawalGroup,
-} from "./withdraw.js";
-
-const logger = new Logger("taler-wallet-core:transactions.ts");
-
-function shouldSkipCurrency(
- transactionsRequest: TransactionsRequest | undefined,
- currency: string,
-): boolean {
- if (!transactionsRequest?.currency) {
- return false;
- }
- return transactionsRequest.currency.toLowerCase() !== currency.toLowerCase();
-}
-
-function shouldSkipSearch(
- transactionsRequest: TransactionsRequest | undefined,
- fields: string[],
-): boolean {
- if (!transactionsRequest?.search) {
- return false;
- }
- const needle = transactionsRequest.search.trim();
- for (const f of fields) {
- if (f.indexOf(needle) >= 0) {
- return false;
- }
- }
- return true;
-}
-
-/**
- * Fallback order of transactions that have the same timestamp.
- */
-const txOrder: { [t in TransactionType]: number } = {
- [TransactionType.Withdrawal]: 1,
- [TransactionType.Tip]: 2,
- [TransactionType.Payment]: 3,
- [TransactionType.PeerPullCredit]: 4,
- [TransactionType.PeerPullDebit]: 5,
- [TransactionType.PeerPushCredit]: 6,
- [TransactionType.PeerPushDebit]: 7,
- [TransactionType.Refund]: 8,
- [TransactionType.Deposit]: 9,
- [TransactionType.Refresh]: 10,
- [TransactionType.Tip]: 11,
-};
-
-export async function getTransactionById(
- ws: InternalWalletState,
- req: TransactionByIdRequest,
-): Promise<Transaction> {
- const { type, args: rest } = parseId("txn", req.transactionId);
- if (
- type === TransactionType.Withdrawal ||
- type === TransactionType.PeerPullCredit ||
- type === TransactionType.PeerPushCredit
- ) {
- const withdrawalGroupId = rest[0];
- return await ws.db
- .mktx((x) => [
- x.withdrawalGroups,
- x.exchangeDetails,
- x.exchanges,
- x.operationRetries,
- ])
- .runReadWrite(async (tx) => {
- const withdrawalGroupRecord = await tx.withdrawalGroups.get(
- withdrawalGroupId,
- );
-
- if (!withdrawalGroupRecord) throw Error("not found");
-
- const opId = RetryTags.forWithdrawal(withdrawalGroupRecord);
- const ort = await tx.operationRetries.get(opId);
-
- if (
- withdrawalGroupRecord.wgInfo.withdrawalType ===
- WithdrawalRecordType.BankIntegrated
- ) {
- return buildTransactionForBankIntegratedWithdraw(
- withdrawalGroupRecord,
- ort,
- );
- }
- if (
- withdrawalGroupRecord.wgInfo.withdrawalType ===
- WithdrawalRecordType.PeerPullCredit
- ) {
- return buildTransactionForPullPaymentCredit(
- withdrawalGroupRecord,
- ort,
- );
- }
- if (
- withdrawalGroupRecord.wgInfo.withdrawalType ===
- WithdrawalRecordType.PeerPushCredit
- ) {
- return buildTransactionForPushPaymentCredit(
- withdrawalGroupRecord,
- ort,
- );
- }
- const exchangeDetails = await getExchangeDetails(
- tx,
- withdrawalGroupRecord.exchangeBaseUrl,
- );
- if (!exchangeDetails) throw Error("not exchange details");
-
- return buildTransactionForManualWithdraw(
- withdrawalGroupRecord,
- exchangeDetails,
- ort,
- );
- });
- } else if (type === TransactionType.Payment) {
- const proposalId = rest[0];
- return await ws.db
- .mktx((x) => [
- x.purchases,
- x.tombstones,
- x.operationRetries,
- x.contractTerms,
- ])
- .runReadWrite(async (tx) => {
- const purchase = await tx.purchases.get(proposalId);
- if (!purchase) throw Error("not found");
-
- const filteredRefunds = await Promise.all(
- Object.values(purchase.refunds).map(async (r) => {
- const t = await tx.tombstones.get(
- makeTombstoneId(
- TombstoneTag.DeleteRefund,
- purchase.proposalId,
- `${r.executionTime.t_s}`,
- ),
- );
- if (!t) return r;
- return undefined;
- }),
- );
-
- const download = await expectProposalDownload(ws, purchase, tx);
-
- const cleanRefunds = filteredRefunds.filter(
- (x): x is WalletRefundItem => !!x,
- );
-
- const contractData = download.contractData;
- const refunds = mergeRefundByExecutionTime(
- cleanRefunds,
- Amounts.zeroOfAmount(contractData.amount),
- );
-
- const payOpId = RetryTags.forPay(purchase);
- const payRetryRecord = await tx.operationRetries.get(payOpId);
-
- return buildTransactionForPurchase(
- purchase,
- contractData,
- refunds,
- payRetryRecord,
- );
- });
- } else if (type === TransactionType.Refresh) {
- const refreshGroupId = rest[0];
- throw Error(`no tx for refresh`);
- } else if (type === TransactionType.Tip) {
- const tipId = rest[0];
- return await ws.db
- .mktx((x) => [x.tips, x.operationRetries])
- .runReadWrite(async (tx) => {
- const tipRecord = await tx.tips.get(tipId);
- if (!tipRecord) throw Error("not found");
-
- const retries = await tx.operationRetries.get(
- RetryTags.forTipPickup(tipRecord),
- );
- return buildTransactionForTip(tipRecord, retries);
- });
- } else if (type === TransactionType.Deposit) {
- const depositGroupId = rest[0];
- return await ws.db
- .mktx((x) => [x.depositGroups, x.operationRetries])
- .runReadWrite(async (tx) => {
- const depositRecord = await tx.depositGroups.get(depositGroupId);
- if (!depositRecord) throw Error("not found");
-
- const retries = await tx.operationRetries.get(
- RetryTags.forDeposit(depositRecord),
- );
- return buildTransactionForDeposit(depositRecord, retries);
- });
- } else if (type === TransactionType.Refund) {
- const proposalId = rest[0];
- const executionTimeStr = rest[1];
-
- return await ws.db
- .mktx((x) => [
- x.operationRetries,
- x.purchases,
- x.tombstones,
- x.contractTerms,
- ])
- .runReadWrite(async (tx) => {
- const purchase = await tx.purchases.get(proposalId);
- if (!purchase) throw Error("not found");
-
- const theRefund = Object.values(purchase.refunds).find(
- (r) => `${r.executionTime.t_s}` === executionTimeStr,
- );
- if (!theRefund) throw Error("not found");
-
- const t = await tx.tombstones.get(
- makeTombstoneId(
- TombstoneTag.DeleteRefund,
- purchase.proposalId,
- executionTimeStr,
- ),
- );
- if (t) throw Error("deleted");
- const download = await expectProposalDownload(ws, purchase, tx);
- const contractData = download.contractData;
- const refunds = mergeRefundByExecutionTime(
- [theRefund],
- Amounts.zeroOfAmount(contractData.amount),
- );
-
- return buildTransactionForRefund(
- purchase,
- contractData,
- refunds[0],
- undefined,
- );
- });
- } else if (type === TransactionType.PeerPullDebit) {
- const peerPullPaymentIncomingId = rest[0];
- return await ws.db
- .mktx((x) => [x.peerPullPaymentIncoming])
- .runReadWrite(async (tx) => {
- const debit = await tx.peerPullPaymentIncoming.get(
- peerPullPaymentIncomingId,
- );
- if (!debit) throw Error("not found");
- return buildTransactionForPullPaymentDebit(debit);
- });
- } else if (type === TransactionType.PeerPushDebit) {
- const pursePub = rest[0];
- return await ws.db
- .mktx((x) => [x.peerPushPaymentInitiations, x.contractTerms])
- .runReadWrite(async (tx) => {
- const debit = await tx.peerPushPaymentInitiations.get(pursePub);
- if (!debit) throw Error("not found");
- const ct = await tx.contractTerms.get(debit.contractTermsHash);
- checkDbInvariant(!!ct);
- return buildTransactionForPushPaymentDebit(debit, ct.contractTermsRaw);
- });
- } else {
- const unknownTxType: never = type;
- throw Error(`can't delete a '${unknownTxType}' transaction`);
- }
-}
-
-function buildTransactionForPushPaymentDebit(
- pi: PeerPushPaymentInitiationRecord,
- contractTerms: PeerContractTerms,
- ort?: OperationRetryRecord,
-): Transaction {
- return {
- type: TransactionType.PeerPushDebit,
- amountEffective: pi.amount,
- amountRaw: pi.amount,
- exchangeBaseUrl: pi.exchangeBaseUrl,
- info: {
- expiration: contractTerms.purse_expiration,
- summary: contractTerms.summary,
- },
- frozen: false,
- pending: pi.status != PeerPushPaymentInitiationStatus.PurseCreated,
- timestamp: pi.timestampCreated,
- talerUri: constructPayPushUri({
- exchangeBaseUrl: pi.exchangeBaseUrl,
- contractPriv: pi.contractPriv,
- }),
- transactionId: makeTransactionId(
- TransactionType.PeerPushDebit,
- pi.pursePub,
- ),
- ...(ort?.lastError ? { error: ort.lastError } : {}),
- };
-}
-
-function buildTransactionForPullPaymentDebit(
- pi: PeerPullPaymentIncomingRecord,
- ort?: OperationRetryRecord,
-): Transaction {
- return {
- type: TransactionType.PeerPullDebit,
- amountEffective: Amounts.stringify(pi.contractTerms.amount),
- amountRaw: Amounts.stringify(pi.contractTerms.amount),
- exchangeBaseUrl: pi.exchangeBaseUrl,
- frozen: false,
- pending: false,
- info: {
- expiration: pi.contractTerms.purse_expiration,
- summary: pi.contractTerms.summary,
- },
- timestamp: pi.timestampCreated,
- transactionId: makeTransactionId(
- TransactionType.PeerPullDebit,
- pi.peerPullPaymentIncomingId,
- ),
- ...(ort?.lastError ? { error: ort.lastError } : {}),
- };
-}
-
-function buildTransactionForPullPaymentCredit(
- wsr: WithdrawalGroupRecord,
- ort?: OperationRetryRecord,
-): Transaction {
- if (wsr.wgInfo.withdrawalType !== WithdrawalRecordType.PeerPullCredit)
- throw Error("");
- return {
- type: TransactionType.PeerPullCredit,
- amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue),
- amountRaw: Amounts.stringify(wsr.instructedAmount),
- exchangeBaseUrl: wsr.exchangeBaseUrl,
- pending: !wsr.timestampFinish,
- timestamp: wsr.timestampStart,
- info: {
- expiration: wsr.wgInfo.contractTerms.purse_expiration,
- summary: wsr.wgInfo.contractTerms.summary,
- },
- talerUri: constructPayPullUri({
- exchangeBaseUrl: wsr.exchangeBaseUrl,
- contractPriv: wsr.wgInfo.contractPriv,
- }),
- transactionId: makeTransactionId(
- TransactionType.PeerPullCredit,
- wsr.withdrawalGroupId,
- ),
- frozen: false,
- ...(ort?.lastError ? { error: ort.lastError } : {}),
- };
-}
-
-function buildTransactionForPushPaymentCredit(
- wsr: WithdrawalGroupRecord,
- ort?: OperationRetryRecord,
-): Transaction {
- if (wsr.wgInfo.withdrawalType !== WithdrawalRecordType.PeerPushCredit)
- throw Error("");
- return {
- type: TransactionType.PeerPushCredit,
- amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue),
- amountRaw: Amounts.stringify(wsr.instructedAmount),
- exchangeBaseUrl: wsr.exchangeBaseUrl,
- info: {
- expiration: wsr.wgInfo.contractTerms.purse_expiration,
- summary: wsr.wgInfo.contractTerms.summary,
- },
- pending: !wsr.timestampFinish,
- timestamp: wsr.timestampStart,
- transactionId: makeTransactionId(
- TransactionType.PeerPushCredit,
- wsr.withdrawalGroupId,
- ),
- frozen: false,
- ...(ort?.lastError ? { error: ort.lastError } : {}),
- };
-}
-
-function buildTransactionForBankIntegratedWithdraw(
- wsr: WithdrawalGroupRecord,
- ort?: OperationRetryRecord,
-): Transaction {
- if (wsr.wgInfo.withdrawalType !== WithdrawalRecordType.BankIntegrated)
- throw Error("");
-
- return {
- type: TransactionType.Withdrawal,
- amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue),
- amountRaw: Amounts.stringify(wsr.instructedAmount),
- withdrawalDetails: {
- type: WithdrawalType.TalerBankIntegrationApi,
- confirmed: wsr.wgInfo.bankInfo.timestampBankConfirmed ? true : false,
- reservePub: wsr.reservePub,
- bankConfirmationUrl: wsr.wgInfo.bankInfo.confirmUrl,
- },
- exchangeBaseUrl: wsr.exchangeBaseUrl,
- pending: !wsr.timestampFinish,
- timestamp: wsr.timestampStart,
- transactionId: makeTransactionId(
- TransactionType.Withdrawal,
- wsr.withdrawalGroupId,
- ),
- frozen: false,
- ...(ort?.lastError ? { error: ort.lastError } : {}),
- };
-}
-
-function buildTransactionForManualWithdraw(
- withdrawalGroup: WithdrawalGroupRecord,
- exchangeDetails: ExchangeDetailsRecord,
- ort?: OperationRetryRecord,
-): Transaction {
- if (withdrawalGroup.wgInfo.withdrawalType !== WithdrawalRecordType.BankManual)
- throw Error("");
-
- const plainPaytoUris =
- exchangeDetails.wireInfo?.accounts.map((x) => x.payto_uri) ?? [];
-
- const exchangePaytoUris = augmentPaytoUrisForWithdrawal(
- plainPaytoUris,
- withdrawalGroup.reservePub,
- withdrawalGroup.instructedAmount,
- );
-
- return {
- type: TransactionType.Withdrawal,
- amountEffective: Amounts.stringify(
- withdrawalGroup.denomsSel.totalCoinValue,
- ),
- amountRaw: Amounts.stringify(withdrawalGroup.instructedAmount),
- withdrawalDetails: {
- type: WithdrawalType.ManualTransfer,
- reservePub: withdrawalGroup.reservePub,
- exchangePaytoUris,
- },
- exchangeBaseUrl: withdrawalGroup.exchangeBaseUrl,
- pending: !withdrawalGroup.timestampFinish,
- timestamp: withdrawalGroup.timestampStart,
- transactionId: makeTransactionId(
- TransactionType.Withdrawal,
- withdrawalGroup.withdrawalGroupId,
- ),
- frozen: false,
- ...(ort?.lastError ? { error: ort.lastError } : {}),
- };
-}
-
-function buildTransactionForDeposit(
- dg: DepositGroupRecord,
- ort?: OperationRetryRecord,
-): Transaction {
- return {
- type: TransactionType.Deposit,
- amountRaw: Amounts.stringify(dg.effectiveDepositAmount),
- amountEffective: Amounts.stringify(dg.totalPayCost),
- pending: !dg.timestampFinished,
- frozen: false,
- timestamp: dg.timestampCreated,
- targetPaytoUri: dg.wire.payto_uri,
- wireTransferDeadline: dg.contractTermsRaw.wire_transfer_deadline,
- transactionId: makeTransactionId(
- TransactionType.Deposit,
- dg.depositGroupId,
- ),
- depositGroupId: dg.depositGroupId,
- ...(ort?.lastError ? { error: ort.lastError } : {}),
- };
-}
-
-function buildTransactionForTip(
- tipRecord: TipRecord,
- ort?: OperationRetryRecord,
-): Transaction {
- if (!tipRecord.acceptedTimestamp) throw Error("");
-
- return {
- type: TransactionType.Tip,
- amountEffective: Amounts.stringify(tipRecord.tipAmountEffective),
- amountRaw: Amounts.stringify(tipRecord.tipAmountRaw),
- pending: !tipRecord.pickedUpTimestamp,
- frozen: false,
- timestamp: tipRecord.acceptedTimestamp,
- transactionId: makeTransactionId(
- TransactionType.Tip,
- tipRecord.walletTipId,
- ),
- merchantBaseUrl: tipRecord.merchantBaseUrl,
- ...(ort?.lastError ? { error: ort.lastError } : {}),
- };
-}
-
-/**
- * For a set of refund with the same executionTime.
- */
-interface MergedRefundInfo {
- executionTime: TalerProtocolTimestamp;
- amountAppliedRaw: AmountJson;
- amountAppliedEffective: AmountJson;
- firstTimestamp: TalerProtocolTimestamp;
-}
-
-function mergeRefundByExecutionTime(
- rs: WalletRefundItem[],
- zero: AmountJson,
-): MergedRefundInfo[] {
- const refundByExecTime = rs.reduce((prev, refund) => {
- const key = `${refund.executionTime.t_s}`;
-
- // refunds count if applied
- const effective =
- refund.type === RefundState.Applied
- ? Amounts.sub(
- refund.refundAmount,
- refund.refundFee,
- refund.totalRefreshCostBound,
- ).amount
- : zero;
- const raw =
- refund.type === RefundState.Applied ? refund.refundAmount : zero;
-
- const v = prev.get(key);
- if (!v) {
- prev.set(key, {
- executionTime: refund.executionTime,
- amountAppliedEffective: effective,
- amountAppliedRaw: Amounts.parseOrThrow(raw),
- firstTimestamp: refund.obtainedTime,
- });
- } else {
- //v.executionTime is the same
- v.amountAppliedEffective = Amounts.add(
- v.amountAppliedEffective,
- effective,
- ).amount;
- v.amountAppliedRaw = Amounts.add(
- v.amountAppliedRaw,
- refund.refundAmount,
- ).amount;
- v.firstTimestamp = TalerProtocolTimestamp.min(
- v.firstTimestamp,
- refund.obtainedTime,
- );
- }
- return prev;
- }, new Map<string, MergedRefundInfo>());
-
- return Array.from(refundByExecTime.values());
-}
-
-async function buildTransactionForRefund(
- purchaseRecord: PurchaseRecord,
- contractData: WalletContractData,
- refundInfo: MergedRefundInfo,
- ort?: OperationRetryRecord,
-): Promise<Transaction> {
- const info: OrderShortInfo = {
- merchant: contractData.merchant,
- orderId: contractData.orderId,
- products: contractData.products,
- summary: contractData.summary,
- summary_i18n: contractData.summaryI18n,
- contractTermsHash: contractData.contractTermsHash,
- };
- if (contractData.fulfillmentUrl !== "") {
- info.fulfillmentUrl = contractData.fulfillmentUrl;
- }
-
- return {
- type: TransactionType.Refund,
- info,
- refundedTransactionId: makeTransactionId(
- TransactionType.Payment,
- purchaseRecord.proposalId,
- ),
- transactionId: makeTransactionId(
- TransactionType.Refund,
- purchaseRecord.proposalId,
- `${refundInfo.executionTime.t_s}`,
- ),
- timestamp: refundInfo.firstTimestamp,
- amountEffective: Amounts.stringify(refundInfo.amountAppliedEffective),
- amountRaw: Amounts.stringify(refundInfo.amountAppliedRaw),
- refundPending:
- purchaseRecord.refundAmountAwaiting === undefined
- ? undefined
- : Amounts.stringify(purchaseRecord.refundAmountAwaiting),
- pending: false,
- frozen: false,
- ...(ort?.lastError ? { error: ort.lastError } : {}),
- };
-}
-
-async function buildTransactionForPurchase(
- purchaseRecord: PurchaseRecord,
- contractData: WalletContractData,
- refundsInfo: MergedRefundInfo[],
- ort?: OperationRetryRecord,
-): Promise<Transaction> {
- const zero = Amounts.zeroOfAmount(contractData.amount);
-
- const info: OrderShortInfo = {
- merchant: contractData.merchant,
- orderId: contractData.orderId,
- products: contractData.products,
- summary: contractData.summary,
- summary_i18n: contractData.summaryI18n,
- contractTermsHash: contractData.contractTermsHash,
- };
-
- if (contractData.fulfillmentUrl !== "") {
- info.fulfillmentUrl = contractData.fulfillmentUrl;
- }
-
- const totalRefund = refundsInfo.reduce(
- (prev, cur) => {
- return {
- raw: Amounts.add(prev.raw, cur.amountAppliedRaw).amount,
- effective: Amounts.add(prev.effective, cur.amountAppliedEffective)
- .amount,
- };
- },
- {
- raw: zero,
- effective: zero,
- } as { raw: AmountJson; effective: AmountJson },
- );
-
- const refunds: RefundInfoShort[] = refundsInfo.map((r) => ({
- amountEffective: Amounts.stringify(r.amountAppliedEffective),
- amountRaw: Amounts.stringify(r.amountAppliedRaw),
- timestamp: r.executionTime,
- transactionId: makeTransactionId(
- TransactionType.Refund,
- purchaseRecord.proposalId,
- `${r.executionTime.t_s}`,
- ),
- }));
-
- const timestamp = purchaseRecord.timestampAccept;
- checkDbInvariant(!!timestamp);
- checkDbInvariant(!!purchaseRecord.payInfo);
-
- return {
- type: TransactionType.Payment,
- amountRaw: Amounts.stringify(contractData.amount),
- amountEffective: Amounts.stringify(purchaseRecord.payInfo.totalPayCost),
- totalRefundRaw: Amounts.stringify(totalRefund.raw),
- totalRefundEffective: Amounts.stringify(totalRefund.effective),
- refundPending:
- purchaseRecord.refundAmountAwaiting === undefined
- ? undefined
- : Amounts.stringify(purchaseRecord.refundAmountAwaiting),
- status: purchaseRecord.timestampFirstSuccessfulPay
- ? PaymentStatus.Paid
- : PaymentStatus.Accepted,
- pending: purchaseRecord.purchaseStatus === PurchaseStatus.Paying,
- refunds,
- timestamp,
- transactionId: makeTransactionId(
- TransactionType.Payment,
- purchaseRecord.proposalId,
- ),
- proposalId: purchaseRecord.proposalId,
- info,
- frozen:
- purchaseRecord.purchaseStatus === PurchaseStatus.PaymentAbortFinished ??
- false,
- ...(ort?.lastError ? { error: ort.lastError } : {}),
- };
-}
-
-/**
- * Retrieve the full event history for this wallet.
- */
-export async function getTransactions(
- ws: InternalWalletState,
- transactionsRequest?: TransactionsRequest,
-): Promise<TransactionsResponse> {
- const transactions: Transaction[] = [];
-
- await ws.db
- .mktx((x) => [
- x.coins,
- x.denominations,
- x.depositGroups,
- x.exchangeDetails,
- x.exchanges,
- x.operationRetries,
- x.peerPullPaymentIncoming,
- x.peerPushPaymentInitiations,
- x.planchets,
- x.purchases,
- x.contractTerms,
- x.recoupGroups,
- x.tips,
- x.tombstones,
- x.withdrawalGroups,
- ])
- .runReadOnly(async (tx) => {
- tx.peerPushPaymentInitiations.iter().forEachAsync(async (pi) => {
- const amount = Amounts.parseOrThrow(pi.amount);
-
- if (shouldSkipCurrency(transactionsRequest, amount.currency)) {
- return;
- }
- if (shouldSkipSearch(transactionsRequest, [])) {
- return;
- }
- const ct = await tx.contractTerms.get(pi.contractTermsHash);
- checkDbInvariant(!!ct);
- transactions.push(
- buildTransactionForPushPaymentDebit(pi, ct.contractTermsRaw),
- );
- });
-
- tx.peerPullPaymentIncoming.iter().forEachAsync(async (pi) => {
- const amount = Amounts.parseOrThrow(pi.contractTerms.amount);
- if (shouldSkipCurrency(transactionsRequest, amount.currency)) {
- return;
- }
- if (shouldSkipSearch(transactionsRequest, [])) {
- return;
- }
- if (
- pi.status !== PeerPullPaymentIncomingStatus.Accepted &&
- pi.status !== PeerPullPaymentIncomingStatus.Paid
- ) {
- return;
- }
-
- transactions.push(buildTransactionForPullPaymentDebit(pi));
- });
-
- tx.withdrawalGroups.iter().forEachAsync(async (wsr) => {
- if (
- shouldSkipCurrency(
- transactionsRequest,
- Amounts.currencyOf(wsr.rawWithdrawalAmount),
- )
- ) {
- return;
- }
-
- if (shouldSkipSearch(transactionsRequest, [])) {
- return;
- }
-
- const opId = RetryTags.forWithdrawal(wsr);
- const ort = await tx.operationRetries.get(opId);
-
- switch (wsr.wgInfo.withdrawalType) {
- case WithdrawalRecordType.PeerPullCredit:
- transactions.push(buildTransactionForPullPaymentCredit(wsr, ort));
- return;
- case WithdrawalRecordType.PeerPushCredit:
- transactions.push(buildTransactionForPushPaymentCredit(wsr, ort));
- return;
- case WithdrawalRecordType.BankIntegrated:
- transactions.push(
- buildTransactionForBankIntegratedWithdraw(wsr, ort),
- );
- return;
- case WithdrawalRecordType.BankManual: {
- const exchangeDetails = await getExchangeDetails(
- tx,
- wsr.exchangeBaseUrl,
- );
- if (!exchangeDetails) {
- // FIXME: report somehow
- return;
- }
-
- transactions.push(
- buildTransactionForManualWithdraw(wsr, exchangeDetails, ort),
- );
- return;
- }
- case WithdrawalRecordType.Recoup:
- // FIXME: Do we also report a transaction here?
- return;
- }
- });
-
- tx.depositGroups.iter().forEachAsync(async (dg) => {
- const amount = Amounts.parseOrThrow(dg.contractTermsRaw.amount);
- if (shouldSkipCurrency(transactionsRequest, amount.currency)) {
- return;
- }
- const opId = RetryTags.forDeposit(dg);
- const retryRecord = await tx.operationRetries.get(opId);
-
- transactions.push(buildTransactionForDeposit(dg, retryRecord));
- });
-
- tx.purchases.iter().forEachAsync(async (purchase) => {
- const download = purchase.download;
- if (!download) {
- return;
- }
- if (!purchase.payInfo) {
- return;
- }
- if (shouldSkipCurrency(transactionsRequest, download.currency)) {
- return;
- }
- const contractTermsRecord = await tx.contractTerms.get(
- download.contractTermsHash,
- );
- if (!contractTermsRecord) {
- return;
- }
- if (
- shouldSkipSearch(transactionsRequest, [
- contractTermsRecord?.contractTermsRaw?.summary || "",
- ])
- ) {
- return;
- }
-
- const contractData = extractContractData(
- contractTermsRecord?.contractTermsRaw,
- download.contractTermsHash,
- download.contractTermsMerchantSig,
- );
-
- const filteredRefunds = await Promise.all(
- Object.values(purchase.refunds).map(async (r) => {
- const t = await tx.tombstones.get(
- makeTombstoneId(
- TombstoneTag.DeleteRefund,
- purchase.proposalId,
- `${r.executionTime.t_s}`,
- ),
- );
- if (!t) return r;
- return undefined;
- }),
- );
-
- const cleanRefunds = filteredRefunds.filter(
- (x): x is WalletRefundItem => !!x,
- );
-
- const refunds = mergeRefundByExecutionTime(
- cleanRefunds,
- Amounts.zeroOfCurrency(download.currency),
- );
-
- refunds.forEach(async (refundInfo) => {
- transactions.push(
- await buildTransactionForRefund(
- purchase,
- contractData,
- refundInfo,
- undefined,
- ),
- );
- });
-
- const payOpId = RetryTags.forPay(purchase);
- const payRetryRecord = await tx.operationRetries.get(payOpId);
- transactions.push(
- await buildTransactionForPurchase(
- purchase,
- contractData,
- refunds,
- payRetryRecord,
- ),
- );
- });
-
- tx.tips.iter().forEachAsync(async (tipRecord) => {
- if (
- shouldSkipCurrency(
- transactionsRequest,
- Amounts.parseOrThrow(tipRecord.tipAmountRaw).currency,
- )
- ) {
- return;
- }
- if (!tipRecord.acceptedTimestamp) {
- return;
- }
- const opId = RetryTags.forTipPickup(tipRecord);
- const retryRecord = await tx.operationRetries.get(opId);
- transactions.push(buildTransactionForTip(tipRecord, retryRecord));
- });
- });
-
- const txPending = transactions.filter((x) => x.pending);
- const txNotPending = transactions.filter((x) => !x.pending);
-
- const txCmp = (h1: Transaction, h2: Transaction) => {
- const tsCmp = AbsoluteTime.cmp(
- AbsoluteTime.fromTimestamp(h1.timestamp),
- AbsoluteTime.fromTimestamp(h2.timestamp),
- );
- if (tsCmp === 0) {
- return Math.sign(txOrder[h1.type] - txOrder[h2.type]);
- }
- return tsCmp;
- };
-
- txPending.sort(txCmp);
- txNotPending.sort(txCmp);
-
- return { transactions: [...txNotPending, ...txPending] };
-}
-
-/**
- * Immediately retry the underlying operation
- * of a transaction.
- */
-export async function retryTransaction(
- ws: InternalWalletState,
- transactionId: string,
-): Promise<void> {
- logger.info(`retrying transaction ${transactionId}`);
-
- const { type, args: rest } = parseId("any", transactionId);
-
- switch (type) {
- case TransactionType.Deposit: {
- const depositGroupId = rest[0];
- processDepositGroup(ws, depositGroupId, {
- forceNow: true,
- });
- break;
- }
- case TransactionType.Withdrawal: {
- const withdrawalGroupId = rest[0];
- await processWithdrawalGroup(ws, withdrawalGroupId, { forceNow: true });
- break;
- }
- case TransactionType.Payment: {
- const proposalId = rest[0];
- await processPurchasePay(ws, proposalId, { forceNow: true });
- break;
- }
- case TransactionType.Tip: {
- const walletTipId = rest[0];
- await processTip(ws, walletTipId, { forceNow: true });
- break;
- }
- case TransactionType.Refresh: {
- const refreshGroupId = rest[0];
- await processRefreshGroup(ws, refreshGroupId, { forceNow: true });
- break;
- }
- default:
- break;
- }
-}
-
-/**
- * Permanently delete a transaction based on the transaction ID.
- */
-export async function deleteTransaction(
- ws: InternalWalletState,
- transactionId: string,
-): Promise<void> {
- const { type, args: rest } = parseId("txn", transactionId);
-
- if (
- type === TransactionType.Withdrawal ||
- type === TransactionType.PeerPullCredit ||
- type === TransactionType.PeerPushCredit
- ) {
- const withdrawalGroupId = rest[0];
- await ws.db
- .mktx((x) => [x.withdrawalGroups, x.tombstones])
- .runReadWrite(async (tx) => {
- const withdrawalGroupRecord = await tx.withdrawalGroups.get(
- withdrawalGroupId,
- );
- if (withdrawalGroupRecord) {
- await tx.withdrawalGroups.delete(withdrawalGroupId);
- await tx.tombstones.put({
- id: TombstoneTag.DeleteWithdrawalGroup + ":" + withdrawalGroupId,
- });
- return;
- }
- });
- } else if (type === TransactionType.Payment) {
- const proposalId = rest[0];
- await ws.db
- .mktx((x) => [x.purchases, x.tombstones])
- .runReadWrite(async (tx) => {
- let found = false;
- const purchase = await tx.purchases.get(proposalId);
- if (purchase) {
- found = true;
- await tx.purchases.delete(proposalId);
- }
- if (found) {
- await tx.tombstones.put({
- id: TombstoneTag.DeletePayment + ":" + proposalId,
- });
- }
- });
- } else if (type === TransactionType.Refresh) {
- const refreshGroupId = rest[0];
- await ws.db
- .mktx((x) => [x.refreshGroups, x.tombstones])
- .runReadWrite(async (tx) => {
- const rg = await tx.refreshGroups.get(refreshGroupId);
- if (rg) {
- await tx.refreshGroups.delete(refreshGroupId);
- await tx.tombstones.put({
- id: TombstoneTag.DeleteRefreshGroup + ":" + refreshGroupId,
- });
- }
- });
- } else if (type === TransactionType.Tip) {
- const tipId = rest[0];
- await ws.db
- .mktx((x) => [x.tips, x.tombstones])
- .runReadWrite(async (tx) => {
- const tipRecord = await tx.tips.get(tipId);
- if (tipRecord) {
- await tx.tips.delete(tipId);
- await tx.tombstones.put({
- id: TombstoneTag.DeleteTip + ":" + tipId,
- });
- }
- });
- } else if (type === TransactionType.Deposit) {
- const depositGroupId = rest[0];
- await ws.db
- .mktx((x) => [x.depositGroups, x.tombstones])
- .runReadWrite(async (tx) => {
- const tipRecord = await tx.depositGroups.get(depositGroupId);
- if (tipRecord) {
- await tx.depositGroups.delete(depositGroupId);
- await tx.tombstones.put({
- id: TombstoneTag.DeleteDepositGroup + ":" + depositGroupId,
- });
- }
- });
- } else if (type === TransactionType.Refund) {
- const proposalId = rest[0];
- const executionTimeStr = rest[1];
-
- await ws.db
- .mktx((x) => [x.purchases, x.tombstones])
- .runReadWrite(async (tx) => {
- const purchase = await tx.purchases.get(proposalId);
- if (purchase) {
- // This should just influence the history view,
- // but won't delete any actual refund information.
- await tx.tombstones.put({
- id: makeTombstoneId(
- TombstoneTag.DeleteRefund,
- proposalId,
- executionTimeStr,
- ),
- });
- }
- });
- } else if (type === TransactionType.PeerPullDebit) {
- const peerPullPaymentIncomingId = rest[0];
- await ws.db
- .mktx((x) => [x.peerPullPaymentIncoming, x.tombstones])
- .runReadWrite(async (tx) => {
- const debit = await tx.peerPullPaymentIncoming.get(
- peerPullPaymentIncomingId,
- );
- if (debit) {
- await tx.peerPullPaymentIncoming.delete(peerPullPaymentIncomingId);
- await tx.tombstones.put({
- id: makeTombstoneId(
- TombstoneTag.DeletePeerPullDebit,
- peerPullPaymentIncomingId,
- ),
- });
- }
- });
- } else if (type === TransactionType.PeerPushDebit) {
- const pursePub = rest[0];
- await ws.db
- .mktx((x) => [x.peerPushPaymentInitiations, x.tombstones])
- .runReadWrite(async (tx) => {
- const debit = await tx.peerPushPaymentInitiations.get(pursePub);
- if (debit) {
- await tx.peerPushPaymentInitiations.delete(pursePub);
- await tx.tombstones.put({
- id: makeTombstoneId(TombstoneTag.DeletePeerPushDebit, pursePub),
- });
- }
- });
- } else {
- const unknownTxType: never = type;
- throw Error(`can't delete a '${unknownTxType}' transaction`);
- }
-}
diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts
deleted file mode 100644
index 76bbec416..000000000
--- a/packages/taler-wallet-core/src/operations/withdraw.ts
+++ /dev/null
@@ -1,1951 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2019-2021 Taler Systems SA
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- * Imports.
- */
-import {
- AbsoluteTime,
- AcceptManualWithdrawalResult,
- AcceptWithdrawalResponse,
- addPaytoQueryParams,
- AgeRestriction,
- AmountJson,
- AmountLike,
- Amounts,
- AmountString,
- BankWithdrawDetails,
- CancellationToken,
- canonicalizeBaseUrl,
- codecForBankWithdrawalOperationPostResponse,
- codecForReserveStatus,
- codecForTalerConfigResponse,
- codecForWithdrawBatchResponse,
- codecForWithdrawOperationStatusResponse,
- codecForWithdrawResponse,
- CoinStatus,
- DenomKeyType,
- DenomSelectionState,
- Duration,
- durationFromSpec,
- encodeCrock,
- ExchangeListItem,
- ExchangeWithdrawalDetails,
- ExchangeWithdrawRequest,
- ForcedDenomSel,
- getRandomBytes,
- HttpStatusCode,
- j2s,
- LibtoolVersion,
- Logger,
- NotificationType,
- parseWithdrawUri,
- TalerErrorCode,
- TalerErrorDetail,
- TalerProtocolTimestamp,
- TransactionType,
- UnblindedSignature,
- URL,
- WithdrawBatchResponse,
- WithdrawResponse,
- WithdrawUriInfoResponse,
-} from "@gnu-taler/taler-util";
-import { EddsaKeypair } from "../crypto/cryptoImplementation.js";
-import {
- CoinRecord,
- CoinSourceType,
- DenominationRecord,
- DenominationVerificationStatus,
- PlanchetRecord,
- PlanchetStatus,
- WalletStoresV1,
- WgInfo,
- WithdrawalGroupRecord,
- WithdrawalGroupStatus,
- WithdrawalRecordType,
-} from "../db.js";
-import {
- getErrorDetailFromException,
- makeErrorDetail,
- TalerError,
-} from "../errors.js";
-import { InternalWalletState } from "../internal-wallet-state.js";
-import {
- getExchangeTosStatus,
- makeCoinAvailable,
- makeExchangeListItem,
- runOperationWithErrorReporting,
-} from "../operations/common.js";
-import { walletCoreDebugFlags } from "../util/debugFlags.js";
-import {
- HttpRequestLibrary,
- readSuccessResponseJsonOrErrorCode,
- readSuccessResponseJsonOrThrow,
- throwUnexpectedRequestError,
-} from "../util/http.js";
-import {
- checkDbInvariant,
- checkLogicInvariant,
- InvariantViolatedError,
-} from "../util/invariants.js";
-import { DbAccess, GetReadOnlyAccess } from "../util/query.js";
-import {
- OperationAttemptResult,
- OperationAttemptResultType,
- RetryTags,
-} from "../util/retries.js";
-import {
- WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
- WALLET_EXCHANGE_PROTOCOL_VERSION,
-} from "../versions.js";
-import {
- makeTransactionId,
- storeOperationError,
- storeOperationPending,
-} from "./common.js";
-import {
- getExchangeDetails,
- getExchangePaytoUri,
- getExchangeTrust,
- updateExchangeFromUrl,
-} from "./exchanges.js";
-
-/**
- * Logger for this file.
- */
-const logger = new Logger("operations/withdraw.ts");
-
-/**
- * Check if a denom is withdrawable based on the expiration time,
- * revocation and offered state.
- */
-export function isWithdrawableDenom(d: DenominationRecord): boolean {
- const now = AbsoluteTime.now();
- const start = AbsoluteTime.fromTimestamp(d.stampStart);
- const withdrawExpire = AbsoluteTime.fromTimestamp(d.stampExpireWithdraw);
- const started = AbsoluteTime.cmp(now, start) >= 0;
- let lastPossibleWithdraw: AbsoluteTime;
- if (walletCoreDebugFlags.denomselAllowLate) {
- lastPossibleWithdraw = start;
- } else {
- lastPossibleWithdraw = AbsoluteTime.subtractDuraction(
- withdrawExpire,
- durationFromSpec({ minutes: 5 }),
- );
- }
- const remaining = Duration.getRemaining(lastPossibleWithdraw, now);
- const stillOkay = remaining.d_ms !== 0;
- return started && stillOkay && !d.isRevoked && d.isOffered;
-}
-
-/**
- * Get a list of denominations (with repetitions possible)
- * whose total value is as close as possible to the available
- * amount, but never larger.
- */
-export function selectWithdrawalDenominations(
- amountAvailable: AmountJson,
- denoms: DenominationRecord[],
-): DenomSelectionState {
- let remaining = Amounts.copy(amountAvailable);
-
- const selectedDenoms: {
- count: number;
- denomPubHash: string;
- }[] = [];
-
- let totalCoinValue = Amounts.zeroOfCurrency(amountAvailable.currency);
- let totalWithdrawCost = Amounts.zeroOfCurrency(amountAvailable.currency);
-
- denoms = denoms.filter(isWithdrawableDenom);
- denoms.sort((d1, d2) =>
- Amounts.cmp(
- DenominationRecord.getValue(d2),
- DenominationRecord.getValue(d1),
- ),
- );
-
- for (const d of denoms) {
- let count = 0;
- const cost = Amounts.add(
- DenominationRecord.getValue(d),
- d.fees.feeWithdraw,
- ).amount;
- for (;;) {
- if (Amounts.cmp(remaining, cost) < 0) {
- break;
- }
- remaining = Amounts.sub(remaining, cost).amount;
- count++;
- }
- if (count > 0) {
- totalCoinValue = Amounts.add(
- totalCoinValue,
- Amounts.mult(DenominationRecord.getValue(d), count).amount,
- ).amount;
- totalWithdrawCost = Amounts.add(
- totalWithdrawCost,
- Amounts.mult(cost, count).amount,
- ).amount;
- selectedDenoms.push({
- count,
- denomPubHash: d.denomPubHash,
- });
- }
-
- if (Amounts.isZero(remaining)) {
- break;
- }
- }
-
- if (logger.shouldLogTrace()) {
- logger.trace(
- `selected withdrawal denoms for ${Amounts.stringify(totalCoinValue)}`,
- );
- for (const sd of selectedDenoms) {
- logger.trace(`denom_pub_hash=${sd.denomPubHash}, count=${sd.count}`);
- }
- logger.trace("(end of withdrawal denom list)");
- }
-
- return {
- selectedDenoms,
- totalCoinValue: Amounts.stringify(totalCoinValue),
- totalWithdrawCost: Amounts.stringify(totalCoinValue),
- };
-}
-
-export function selectForcedWithdrawalDenominations(
- amountAvailable: AmountJson,
- denoms: DenominationRecord[],
- forcedDenomSel: ForcedDenomSel,
-): DenomSelectionState {
- const selectedDenoms: {
- count: number;
- denomPubHash: string;
- }[] = [];
-
- let totalCoinValue = Amounts.zeroOfCurrency(amountAvailable.currency);
- let totalWithdrawCost = Amounts.zeroOfCurrency(amountAvailable.currency);
-
- denoms = denoms.filter(isWithdrawableDenom);
- denoms.sort((d1, d2) =>
- Amounts.cmp(
- DenominationRecord.getValue(d2),
- DenominationRecord.getValue(d1),
- ),
- );
-
- for (const fds of forcedDenomSel.denoms) {
- const count = fds.count;
- const denom = denoms.find((x) => {
- return Amounts.cmp(DenominationRecord.getValue(x), fds.value) == 0;
- });
- if (!denom) {
- throw Error(
- `unable to find denom for forced selection (value ${fds.value})`,
- );
- }
- const cost = Amounts.add(
- DenominationRecord.getValue(denom),
- denom.fees.feeWithdraw,
- ).amount;
- totalCoinValue = Amounts.add(
- totalCoinValue,
- Amounts.mult(DenominationRecord.getValue(denom), count).amount,
- ).amount;
- totalWithdrawCost = Amounts.add(
- totalWithdrawCost,
- Amounts.mult(cost, count).amount,
- ).amount;
- selectedDenoms.push({
- count,
- denomPubHash: denom.denomPubHash,
- });
- }
-
- return {
- selectedDenoms,
- totalCoinValue: Amounts.stringify(totalCoinValue),
- totalWithdrawCost: Amounts.stringify(totalWithdrawCost),
- };
-}
-
-/**
- * Get information about a withdrawal from
- * a taler://withdraw URI by asking the bank.
- *
- * FIXME: Move into bank client.
- */
-export async function getBankWithdrawalInfo(
- http: HttpRequestLibrary,
- talerWithdrawUri: string,
-): Promise<BankWithdrawDetails> {
- const uriResult = parseWithdrawUri(talerWithdrawUri);
- if (!uriResult) {
- throw Error(`can't parse URL ${talerWithdrawUri}`);
- }
-
- const configReqUrl = new URL("config", uriResult.bankIntegrationApiBaseUrl);
-
- const configResp = await http.get(configReqUrl.href);
- const config = await readSuccessResponseJsonOrThrow(
- configResp,
- codecForTalerConfigResponse(),
- );
-
- const versionRes = LibtoolVersion.compare(
- WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
- config.version,
- );
- if (versionRes?.compatible != true) {
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_BANK_INTEGRATION_PROTOCOL_VERSION_INCOMPATIBLE,
- {
- exchangeProtocolVersion: config.version,
- walletProtocolVersion: WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
- },
- "bank integration protocol version not compatible with wallet",
- );
- }
-
- const reqUrl = new URL(
- `withdrawal-operation/${uriResult.withdrawalOperationId}`,
- uriResult.bankIntegrationApiBaseUrl,
- );
-
- logger.info(`bank withdrawal status URL: ${reqUrl.href}}`);
-
- const resp = await http.get(reqUrl.href);
- const status = await readSuccessResponseJsonOrThrow(
- resp,
- codecForWithdrawOperationStatusResponse(),
- );
-
- logger.info(`bank withdrawal operation status: ${j2s(status)}`);
-
- return {
- amount: Amounts.parseOrThrow(status.amount),
- confirmTransferUrl: status.confirm_transfer_url,
- selectionDone: status.selection_done,
- senderWire: status.sender_wire,
- suggestedExchange: status.suggested_exchange,
- transferDone: status.transfer_done,
- wireTypes: status.wire_types,
- };
-}
-
-/**
- * Return denominations that can potentially used for a withdrawal.
- */
-export async function getCandidateWithdrawalDenoms(
- ws: InternalWalletState,
- exchangeBaseUrl: string,
-): Promise<DenominationRecord[]> {
- return await ws.db
- .mktx((x) => [x.denominations])
- .runReadOnly(async (tx) => {
- const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl.getAll(
- exchangeBaseUrl,
- );
- return allDenoms.filter(isWithdrawableDenom);
- });
-}
-
-/**
- * Generate a planchet for a coin index in a withdrawal group.
- * Does not actually withdraw the coin yet.
- *
- * Split up so that we can parallelize the crypto, but serialize
- * the exchange requests per reserve.
- */
-async function processPlanchetGenerate(
- ws: InternalWalletState,
- withdrawalGroup: WithdrawalGroupRecord,
- coinIdx: number,
-): Promise<void> {
- let planchet = await ws.db
- .mktx((x) => [x.planchets])
- .runReadOnly(async (tx) => {
- return tx.planchets.indexes.byGroupAndIndex.get([
- withdrawalGroup.withdrawalGroupId,
- coinIdx,
- ]);
- });
- if (planchet) {
- return;
- }
- let ci = 0;
- let maybeDenomPubHash: string | undefined;
- for (let di = 0; di < withdrawalGroup.denomsSel.selectedDenoms.length; di++) {
- const d = withdrawalGroup.denomsSel.selectedDenoms[di];
- if (coinIdx >= ci && coinIdx < ci + d.count) {
- maybeDenomPubHash = d.denomPubHash;
- break;
- }
- ci += d.count;
- }
- if (!maybeDenomPubHash) {
- throw Error("invariant violated");
- }
- const denomPubHash = maybeDenomPubHash;
-
- const denom = await ws.db
- .mktx((x) => [x.denominations])
- .runReadOnly(async (tx) => {
- return ws.getDenomInfo(
- ws,
- tx,
- withdrawalGroup.exchangeBaseUrl,
- denomPubHash,
- );
- });
- checkDbInvariant(!!denom);
- const r = await ws.cryptoApi.createPlanchet({
- denomPub: denom.denomPub,
- feeWithdraw: Amounts.parseOrThrow(denom.feeWithdraw),
- reservePriv: withdrawalGroup.reservePriv,
- reservePub: withdrawalGroup.reservePub,
- value: Amounts.parseOrThrow(denom.value),
- coinIndex: coinIdx,
- secretSeed: withdrawalGroup.secretSeed,
- restrictAge: withdrawalGroup.restrictAge,
- });
- const newPlanchet: PlanchetRecord = {
- blindingKey: r.blindingKey,
- coinEv: r.coinEv,
- coinEvHash: r.coinEvHash,
- coinIdx,
- coinPriv: r.coinPriv,
- coinPub: r.coinPub,
- denomPubHash: r.denomPubHash,
- planchetStatus: PlanchetStatus.Pending,
- withdrawSig: r.withdrawSig,
- withdrawalGroupId: withdrawalGroup.withdrawalGroupId,
- ageCommitmentProof: r.ageCommitmentProof,
- lastError: undefined,
- };
- await ws.db
- .mktx((x) => [x.planchets])
- .runReadWrite(async (tx) => {
- const p = await tx.planchets.indexes.byGroupAndIndex.get([
- withdrawalGroup.withdrawalGroupId,
- coinIdx,
- ]);
- if (p) {
- planchet = p;
- return;
- }
- await tx.planchets.put(newPlanchet);
- planchet = newPlanchet;
- });
-}
-
-/**
- * Send the withdrawal request for a generated planchet to the exchange.
- *
- * The verification of the response is done asynchronously to enable parallelism.
- */
-async function processPlanchetExchangeRequest(
- ws: InternalWalletState,
- withdrawalGroup: WithdrawalGroupRecord,
- coinIdx: number,
-): Promise<WithdrawResponse | undefined> {
- logger.info(
- `processing planchet exchange request ${withdrawalGroup.withdrawalGroupId}/${coinIdx}`,
- );
- const d = await ws.db
- .mktx((x) => [
- x.withdrawalGroups,
- x.planchets,
- x.exchanges,
- x.denominations,
- ])
- .runReadOnly(async (tx) => {
- let planchet = await tx.planchets.indexes.byGroupAndIndex.get([
- withdrawalGroup.withdrawalGroupId,
- coinIdx,
- ]);
- if (!planchet) {
- return;
- }
- if (planchet.planchetStatus === PlanchetStatus.WithdrawalDone) {
- logger.warn("processPlanchet: planchet already withdrawn");
- return;
- }
- const exchange = await tx.exchanges.get(withdrawalGroup.exchangeBaseUrl);
- if (!exchange) {
- logger.error("db inconsistent: exchange for planchet not found");
- return;
- }
-
- const denom = await ws.getDenomInfo(
- ws,
- tx,
- withdrawalGroup.exchangeBaseUrl,
- planchet.denomPubHash,
- );
-
- if (!denom) {
- logger.error("db inconsistent: denom for planchet not found");
- return;
- }
-
- logger.trace(
- `processing planchet #${coinIdx} in withdrawal ${withdrawalGroup.withdrawalGroupId}`,
- );
-
- const reqBody: ExchangeWithdrawRequest = {
- denom_pub_hash: planchet.denomPubHash,
- reserve_sig: planchet.withdrawSig,
- coin_ev: planchet.coinEv,
- };
- const reqUrl = new URL(
- `reserves/${withdrawalGroup.reservePub}/withdraw`,
- exchange.baseUrl,
- ).href;
-
- return { reqUrl, reqBody };
- });
-
- if (!d) {
- return;
- }
- const { reqUrl, reqBody } = d;
-
- try {
- const resp = await ws.http.postJson(reqUrl, reqBody);
- if (resp.status === HttpStatusCode.UnavailableForLegalReasons) {
- logger.info("withdrawal requires KYC");
- await ws.db
- .mktx((x) => [x.planchets])
- .runReadWrite(async (tx) => {
- let planchet = await tx.planchets.indexes.byGroupAndIndex.get([
- withdrawalGroup.withdrawalGroupId,
- coinIdx,
- ]);
- if (!planchet) {
- return;
- }
- planchet.planchetStatus = PlanchetStatus.KycRequired;
- await tx.planchets.put(planchet);
- });
- return;
- }
- const r = await readSuccessResponseJsonOrThrow(
- resp,
- codecForWithdrawResponse(),
- );
- return r;
- } catch (e) {
- const errDetail = getErrorDetailFromException(e);
- logger.trace("withdrawal request failed", e);
- logger.trace(e);
- await ws.db
- .mktx((x) => [x.planchets])
- .runReadWrite(async (tx) => {
- let planchet = await tx.planchets.indexes.byGroupAndIndex.get([
- withdrawalGroup.withdrawalGroupId,
- coinIdx,
- ]);
- if (!planchet) {
- return;
- }
- planchet.lastError = errDetail;
- await tx.planchets.put(planchet);
- });
- return;
- }
-}
-
-/**
- * Send the withdrawal request for a generated planchet to the exchange.
- *
- * The verification of the response is done asynchronously to enable parallelism.
- */
-async function processPlanchetExchangeBatchRequest(
- ws: InternalWalletState,
- withdrawalGroup: WithdrawalGroupRecord,
-): Promise<WithdrawBatchResponse | undefined> {
- logger.info(
- `processing planchet exchange batch request ${withdrawalGroup.withdrawalGroupId}`,
- );
- const numTotalCoins = withdrawalGroup.denomsSel.selectedDenoms
- .map((x) => x.count)
- .reduce((a, b) => a + b);
- const d = await ws.db
- .mktx((x) => [
- x.withdrawalGroups,
- x.planchets,
- x.exchanges,
- x.denominations,
- ])
- .runReadOnly(async (tx) => {
- const reqBody: { planchets: ExchangeWithdrawRequest[] } = {
- planchets: [],
- };
- const exchange = await tx.exchanges.get(withdrawalGroup.exchangeBaseUrl);
- if (!exchange) {
- logger.error("db inconsistent: exchange for planchet not found");
- return;
- }
-
- for (let coinIdx = 0; coinIdx < numTotalCoins; coinIdx++) {
- let planchet = await tx.planchets.indexes.byGroupAndIndex.get([
- withdrawalGroup.withdrawalGroupId,
- coinIdx,
- ]);
- if (!planchet) {
- return;
- }
- if (planchet.planchetStatus === PlanchetStatus.WithdrawalDone) {
- logger.warn("processPlanchet: planchet already withdrawn");
- return;
- }
- const denom = await ws.getDenomInfo(
- ws,
- tx,
- withdrawalGroup.exchangeBaseUrl,
- planchet.denomPubHash,
- );
-
- if (!denom) {
- logger.error("db inconsistent: denom for planchet not found");
- return;
- }
-
- const planchetReq: ExchangeWithdrawRequest = {
- denom_pub_hash: planchet.denomPubHash,
- reserve_sig: planchet.withdrawSig,
- coin_ev: planchet.coinEv,
- };
- reqBody.planchets.push(planchetReq);
- }
- return reqBody;
- });
-
- if (!d) {
- return;
- }
-
- const reqUrl = new URL(
- `reserves/${withdrawalGroup.reservePub}/batch-withdraw`,
- withdrawalGroup.exchangeBaseUrl,
- ).href;
-
- const resp = await ws.http.postJson(reqUrl, d);
- const r = await readSuccessResponseJsonOrThrow(
- resp,
- codecForWithdrawBatchResponse(),
- );
- return r;
-}
-
-async function processPlanchetVerifyAndStoreCoin(
- ws: InternalWalletState,
- withdrawalGroup: WithdrawalGroupRecord,
- coinIdx: number,
- resp: WithdrawResponse,
-): Promise<void> {
- const d = await ws.db
- .mktx((x) => [x.withdrawalGroups, x.planchets, x.denominations])
- .runReadOnly(async (tx) => {
- let planchet = await tx.planchets.indexes.byGroupAndIndex.get([
- withdrawalGroup.withdrawalGroupId,
- coinIdx,
- ]);
- if (!planchet) {
- return;
- }
- if (planchet.planchetStatus === PlanchetStatus.WithdrawalDone) {
- logger.warn("processPlanchet: planchet already withdrawn");
- return;
- }
- const denomInfo = await ws.getDenomInfo(
- ws,
- tx,
- withdrawalGroup.exchangeBaseUrl,
- planchet.denomPubHash,
- );
- if (!denomInfo) {
- return;
- }
- return {
- planchet,
- denomInfo,
- exchangeBaseUrl: withdrawalGroup.exchangeBaseUrl,
- };
- });
-
- if (!d) {
- return;
- }
-
- const { planchet, denomInfo } = d;
-
- const planchetDenomPub = denomInfo.denomPub;
- if (planchetDenomPub.cipher !== DenomKeyType.Rsa) {
- throw Error(`cipher (${planchetDenomPub.cipher}) not supported`);
- }
-
- let evSig = resp.ev_sig;
- if (!(evSig.cipher === DenomKeyType.Rsa)) {
- throw Error("unsupported cipher");
- }
-
- const denomSigRsa = await ws.cryptoApi.rsaUnblind({
- bk: planchet.blindingKey,
- blindedSig: evSig.blinded_rsa_signature,
- pk: planchetDenomPub.rsa_public_key,
- });
-
- const isValid = await ws.cryptoApi.rsaVerify({
- hm: planchet.coinPub,
- pk: planchetDenomPub.rsa_public_key,
- sig: denomSigRsa.sig,
- });
-
- if (!isValid) {
- await ws.db
- .mktx((x) => [x.planchets])
- .runReadWrite(async (tx) => {
- let planchet = await tx.planchets.indexes.byGroupAndIndex.get([
- withdrawalGroup.withdrawalGroupId,
- coinIdx,
- ]);
- if (!planchet) {
- return;
- }
- planchet.lastError = makeErrorDetail(
- TalerErrorCode.WALLET_EXCHANGE_COIN_SIGNATURE_INVALID,
- {},
- "invalid signature from the exchange after unblinding",
- );
- await tx.planchets.put(planchet);
- });
- return;
- }
-
- let denomSig: UnblindedSignature;
- if (planchetDenomPub.cipher === DenomKeyType.Rsa) {
- denomSig = {
- cipher: planchetDenomPub.cipher,
- rsa_signature: denomSigRsa.sig,
- };
- } else {
- throw Error("unsupported cipher");
- }
-
- const coin: CoinRecord = {
- blindingKey: planchet.blindingKey,
- coinPriv: planchet.coinPriv,
- coinPub: planchet.coinPub,
- denomPubHash: planchet.denomPubHash,
- denomSig,
- coinEvHash: planchet.coinEvHash,
- exchangeBaseUrl: d.exchangeBaseUrl,
- status: CoinStatus.Fresh,
- coinSource: {
- type: CoinSourceType.Withdraw,
- coinIndex: coinIdx,
- reservePub: withdrawalGroup.reservePub,
- withdrawalGroupId: withdrawalGroup.withdrawalGroupId,
- },
- maxAge: withdrawalGroup.restrictAge ?? AgeRestriction.AGE_UNRESTRICTED,
- ageCommitmentProof: planchet.ageCommitmentProof,
- spendAllocation: undefined,
- };
-
- const planchetCoinPub = planchet.coinPub;
-
- // Check if this is the first time that the whole
- // withdrawal succeeded. If so, mark the withdrawal
- // group as finished.
- const firstSuccess = await ws.db
- .mktx((x) => [
- x.coins,
- x.denominations,
- x.coinAvailability,
- x.withdrawalGroups,
- x.planchets,
- ])
- .runReadWrite(async (tx) => {
- const p = await tx.planchets.get(planchetCoinPub);
- if (!p || p.planchetStatus === PlanchetStatus.WithdrawalDone) {
- return false;
- }
- p.planchetStatus = PlanchetStatus.WithdrawalDone;
- await tx.planchets.put(p);
- await makeCoinAvailable(ws, tx, coin);
- return true;
- });
-
- if (firstSuccess) {
- ws.notify({
- type: NotificationType.CoinWithdrawn,
- });
- }
-}
-
-/**
- * Make sure that denominations that currently can be used for withdrawal
- * are validated, and the result of validation is stored in the database.
- */
-export async function updateWithdrawalDenoms(
- ws: InternalWalletState,
- exchangeBaseUrl: string,
-): Promise<void> {
- logger.trace(
- `updating denominations used for withdrawal for ${exchangeBaseUrl}`,
- );
- const exchangeDetails = await ws.db
- .mktx((x) => [x.exchanges, x.exchangeDetails])
- .runReadOnly(async (tx) => {
- return ws.exchangeOps.getExchangeDetails(tx, exchangeBaseUrl);
- });
- if (!exchangeDetails) {
- logger.error("exchange details not available");
- throw Error(`exchange ${exchangeBaseUrl} details not available`);
- }
- // First do a pass where the validity of candidate denominations
- // is checked and the result is stored in the database.
- logger.trace("getting candidate denominations");
- const denominations = await getCandidateWithdrawalDenoms(ws, exchangeBaseUrl);
- logger.trace(`got ${denominations.length} candidate denominations`);
- const batchSize = 500;
- let current = 0;
-
- while (current < denominations.length) {
- const updatedDenominations: DenominationRecord[] = [];
- // Do a batch of batchSize
- for (
- let batchIdx = 0;
- batchIdx < batchSize && current < denominations.length;
- batchIdx++, current++
- ) {
- const denom = denominations[current];
- if (
- denom.verificationStatus === DenominationVerificationStatus.Unverified
- ) {
- logger.trace(
- `Validating denomination (${current + 1}/${
- denominations.length
- }) signature of ${denom.denomPubHash}`,
- );
- let valid = false;
- if (ws.insecureTrustExchange) {
- valid = true;
- } else {
- const res = await ws.cryptoApi.isValidDenom({
- denom,
- masterPub: exchangeDetails.masterPublicKey,
- });
- valid = res.valid;
- }
- logger.trace(`Done validating ${denom.denomPubHash}`);
- if (!valid) {
- logger.warn(
- `Signature check for denomination h=${denom.denomPubHash} failed`,
- );
- denom.verificationStatus = DenominationVerificationStatus.VerifiedBad;
- } else {
- denom.verificationStatus =
- DenominationVerificationStatus.VerifiedGood;
- }
- updatedDenominations.push(denom);
- }
- }
- if (updatedDenominations.length > 0) {
- logger.trace("writing denomination batch to db");
- await ws.db
- .mktx((x) => [x.denominations])
- .runReadWrite(async (tx) => {
- for (let i = 0; i < updatedDenominations.length; i++) {
- const denom = updatedDenominations[i];
- await tx.denominations.put(denom);
- }
- });
- logger.trace("done with DB write");
- }
- }
-}
-
-/**
- * Update the information about a reserve that is stored in the wallet
- * by querying the reserve's exchange.
- *
- * If the reserve have funds that are not allocated in a withdrawal group yet
- * and are big enough to withdraw with available denominations,
- * create a new withdrawal group for the remaining amount.
- */
-async function queryReserve(
- ws: InternalWalletState,
- withdrawalGroupId: string,
- cancellationToken: CancellationToken,
-): Promise<{ ready: boolean }> {
- const withdrawalGroup = await getWithdrawalGroupRecordTx(ws.db, {
- withdrawalGroupId,
- });
- checkDbInvariant(!!withdrawalGroup);
- if (withdrawalGroup.status !== WithdrawalGroupStatus.QueryingStatus) {
- return { ready: true };
- }
- const reservePub = withdrawalGroup.reservePub;
-
- const reserveUrl = new URL(
- `reserves/${reservePub}`,
- withdrawalGroup.exchangeBaseUrl,
- );
- reserveUrl.searchParams.set("timeout_ms", "30000");
-
- logger.info(`querying reserve status via ${reserveUrl}`);
-
- const resp = await ws.http.get(reserveUrl.href, {
- timeout: getReserveRequestTimeout(withdrawalGroup),
- cancellationToken,
- });
-
- const result = await readSuccessResponseJsonOrErrorCode(
- resp,
- codecForReserveStatus(),
- );
-
- if (result.isError) {
- if (
- resp.status === 404 &&
- result.talerErrorResponse.code ===
- TalerErrorCode.EXCHANGE_RESERVES_STATUS_UNKNOWN
- ) {
- ws.notify({
- type: NotificationType.ReserveNotYetFound,
- reservePub,
- });
- return { ready: false };
- } else {
- throwUnexpectedRequestError(resp, result.talerErrorResponse);
- }
- }
-
- logger.trace(`got reserve status ${j2s(result.response)}`);
-
- await ws.db
- .mktx((x) => [x.withdrawalGroups])
- .runReadWrite(async (tx) => {
- const wg = await tx.withdrawalGroups.get(withdrawalGroupId);
- if (!wg) {
- logger.warn(`withdrawal group ${withdrawalGroupId} not found`);
- return;
- }
- wg.status = WithdrawalGroupStatus.Ready;
- wg.reserveBalanceAmount = Amounts.stringify(result.response.balance);
- await tx.withdrawalGroups.put(wg);
- });
-
- return { ready: true };
-}
-
-enum BankStatusResultCode {
- Done = "done",
- Waiting = "waiting",
- Aborted = "aborted",
-}
-
-export async function processWithdrawalGroup(
- ws: InternalWalletState,
- withdrawalGroupId: string,
- options: object = {},
-): Promise<OperationAttemptResult> {
- logger.trace("processing withdrawal group", withdrawalGroupId);
- const withdrawalGroup = await ws.db
- .mktx((x) => [x.withdrawalGroups])
- .runReadOnly(async (tx) => {
- return tx.withdrawalGroups.get(withdrawalGroupId);
- });
-
- if (!withdrawalGroup) {
- throw Error(`withdrawal group ${withdrawalGroupId} not found`);
- }
-
- const retryTag = RetryTags.forWithdrawal(withdrawalGroup);
-
- // We're already running!
- if (ws.activeLongpoll[retryTag]) {
- logger.info("withdrawal group already in long-polling, returning!");
- return {
- type: OperationAttemptResultType.Longpoll,
- };
- }
-
- switch (withdrawalGroup.status) {
- case WithdrawalGroupStatus.RegisteringBank:
- await processReserveBankStatus(ws, withdrawalGroupId);
- return await processWithdrawalGroup(ws, withdrawalGroupId, {
- forceNow: true,
- });
- case WithdrawalGroupStatus.QueryingStatus: {
- const doQueryAsync = async () => {
- if (ws.stopped) {
- logger.trace("not long-polling reserve, wallet already stopped");
- await storeOperationPending(ws, retryTag);
- return;
- }
- const cts = CancellationToken.create();
- let res: { ready: boolean } | undefined = undefined;
- try {
- ws.activeLongpoll[retryTag] = {
- cancel: () => {
- logger.trace("cancel of reserve longpoll requested");
- cts.cancel();
- },
- };
- res = await queryReserve(ws, withdrawalGroupId, cts.token);
- } catch (e) {
- await storeOperationError(
- ws,
- retryTag,
- getErrorDetailFromException(e),
- );
- return;
- }
- delete ws.activeLongpoll[retryTag];
- if (!res.ready) {
- await storeOperationPending(ws, retryTag);
- }
- ws.latch.trigger();
- };
- doQueryAsync();
- logger.trace(
- "returning early from withdrawal for long-polling in background",
- );
- return {
- type: OperationAttemptResultType.Longpoll,
- };
- }
- case WithdrawalGroupStatus.WaitConfirmBank: {
- const res = await processReserveBankStatus(ws, withdrawalGroupId);
- switch (res.status) {
- case BankStatusResultCode.Aborted:
- case BankStatusResultCode.Done:
- return {
- type: OperationAttemptResultType.Finished,
- result: undefined,
- };
- case BankStatusResultCode.Waiting: {
- return {
- type: OperationAttemptResultType.Pending,
- result: undefined,
- };
- }
- }
- break;
- }
- case WithdrawalGroupStatus.BankAborted: {
- // FIXME
- return {
- type: OperationAttemptResultType.Pending,
- result: undefined,
- };
- }
- case WithdrawalGroupStatus.Finished:
- // We can try to withdraw, nothing needs to be done with the reserve.
- break;
- case WithdrawalGroupStatus.Ready:
- // Continue with the actual withdrawal!
- break;
- default:
- throw new InvariantViolatedError(
- `unknown reserve record status: ${withdrawalGroup.status}`,
- );
- }
-
- await ws.exchangeOps.updateExchangeFromUrl(
- ws,
- withdrawalGroup.exchangeBaseUrl,
- );
-
- if (withdrawalGroup.denomsSel.selectedDenoms.length === 0) {
- logger.warn("Finishing empty withdrawal group (no denoms)");
- await ws.db
- .mktx((x) => [x.withdrawalGroups])
- .runReadWrite(async (tx) => {
- const wg = await tx.withdrawalGroups.get(withdrawalGroupId);
- if (!wg) {
- return;
- }
- wg.status = WithdrawalGroupStatus.Finished;
- wg.timestampFinish = TalerProtocolTimestamp.now();
- await tx.withdrawalGroups.put(wg);
- });
- return {
- type: OperationAttemptResultType.Finished,
- result: undefined,
- };
- }
-
- const numTotalCoins = withdrawalGroup.denomsSel.selectedDenoms
- .map((x) => x.count)
- .reduce((a, b) => a + b);
-
- let work: Promise<void>[] = [];
-
- for (let i = 0; i < numTotalCoins; i++) {
- work.push(processPlanchetGenerate(ws, withdrawalGroup, i));
- }
-
- // Generate coins concurrently (parallelism only happens in the crypto API workers)
- await Promise.all(work);
-
- work = [];
-
- if (ws.batchWithdrawal) {
- const resp = await processPlanchetExchangeBatchRequest(ws, withdrawalGroup);
- if (!resp) {
- throw Error("unable to do batch withdrawal");
- }
- for (let coinIdx = 0; coinIdx < numTotalCoins; coinIdx++) {
- work.push(
- processPlanchetVerifyAndStoreCoin(
- ws,
- withdrawalGroup,
- coinIdx,
- resp.ev_sigs[coinIdx],
- ),
- );
- }
- } else {
- for (let coinIdx = 0; coinIdx < numTotalCoins; coinIdx++) {
- const resp = await processPlanchetExchangeRequest(
- ws,
- withdrawalGroup,
- coinIdx,
- );
- if (!resp) {
- continue;
- }
- work.push(
- processPlanchetVerifyAndStoreCoin(ws, withdrawalGroup, coinIdx, resp),
- );
- }
- }
-
- await Promise.all(work);
-
- let numFinished = 0;
- let numKycRequired = 0;
- let finishedForFirstTime = false;
- let errorsPerCoin: Record<number, TalerErrorDetail> = {};
-
- await ws.db
- .mktx((x) => [x.coins, x.withdrawalGroups, x.planchets])
- .runReadWrite(async (tx) => {
- const wg = await tx.withdrawalGroups.get(withdrawalGroupId);
- if (!wg) {
- return;
- }
-
- await tx.planchets.indexes.byGroup
- .iter(withdrawalGroupId)
- .forEach((x) => {
- if (x.planchetStatus === PlanchetStatus.WithdrawalDone) {
- numFinished++;
- }
- if (x.planchetStatus === PlanchetStatus.KycRequired) {
- numKycRequired++;
- }
- if (x.lastError) {
- errorsPerCoin[x.coinIdx] = x.lastError;
- }
- });
- logger.info(`now withdrawn ${numFinished} of ${numTotalCoins} coins`);
- if (wg.timestampFinish === undefined && numFinished === numTotalCoins) {
- finishedForFirstTime = true;
- wg.timestampFinish = TalerProtocolTimestamp.now();
- wg.status = WithdrawalGroupStatus.Finished;
- }
-
- await tx.withdrawalGroups.put(wg);
- });
- if (numKycRequired > 0) {
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED,
- {},
- `KYC check required for withdrawal (not yet implemented in wallet-core)`,
- );
- }
- if (numFinished != numTotalCoins) {
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_WITHDRAWAL_GROUP_INCOMPLETE,
- {
- errorsPerCoin,
- },
- `withdrawal did not finish (${numFinished} / ${numTotalCoins} coins withdrawn)`,
- );
- }
-
- if (finishedForFirstTime) {
- ws.notify({
- type: NotificationType.WithdrawGroupFinished,
- reservePub: withdrawalGroup.reservePub,
- });
- }
-
- return {
- type: OperationAttemptResultType.Finished,
- result: undefined,
- };
-}
-
-const AGE_MASK_GROUPS = "8:10:12:14:16:18"
- .split(":")
- .map((n) => parseInt(n, 10));
-
-export async function getExchangeWithdrawalInfo(
- ws: InternalWalletState,
- exchangeBaseUrl: string,
- instructedAmount: AmountJson,
- ageRestricted: number | undefined,
-): Promise<ExchangeWithdrawalDetails> {
- const { exchange, exchangeDetails } =
- await ws.exchangeOps.updateExchangeFromUrl(ws, exchangeBaseUrl);
- await updateWithdrawalDenoms(ws, exchangeBaseUrl);
- const denoms = await getCandidateWithdrawalDenoms(ws, exchangeBaseUrl);
- const selectedDenoms = selectWithdrawalDenominations(
- instructedAmount,
- denoms,
- );
-
- 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 exchangeDetails.wireInfo.accounts) {
- exchangeWireAccounts.push(account.payto_uri);
- }
-
- const { isTrusted, isAudited } = await ws.exchangeOps.getExchangeTrust(
- ws,
- exchange,
- );
-
- let hasDenomWithAgeRestriction = false;
-
- let earliestDepositExpiration: TalerProtocolTimestamp | undefined;
- for (let i = 0; i < selectedDenoms.selectedDenoms.length; i++) {
- const ds = selectedDenoms.selectedDenoms[i];
- // FIXME: Do in one transaction!
- const denom = await ws.db
- .mktx((x) => [x.denominations])
- .runReadOnly(async (tx) => {
- return ws.getDenomInfo(ws, tx, exchangeBaseUrl, ds.denomPubHash);
- });
- checkDbInvariant(!!denom);
- hasDenomWithAgeRestriction =
- hasDenomWithAgeRestriction || denom.denomPub.age_mask > 0;
- const expireDeposit = denom.stampExpireDeposit;
- if (!earliestDepositExpiration) {
- earliestDepositExpiration = expireDeposit;
- continue;
- }
- if (
- AbsoluteTime.cmp(
- AbsoluteTime.fromTimestamp(expireDeposit),
- AbsoluteTime.fromTimestamp(earliestDepositExpiration),
- ) < 0
- ) {
- earliestDepositExpiration = expireDeposit;
- }
- }
-
- checkLogicInvariant(!!earliestDepositExpiration);
-
- const possibleDenoms = await ws.db
- .mktx((x) => [x.denominations])
- .runReadOnly(async (tx) => {
- const ds = await tx.denominations.indexes.byExchangeBaseUrl.getAll(
- exchangeBaseUrl,
- );
- return ds.filter((x) => x.isOffered);
- });
-
- let versionMatch;
- if (exchangeDetails.protocolVersionRange) {
- versionMatch = LibtoolVersion.compare(
- WALLET_EXCHANGE_PROTOCOL_VERSION,
- exchangeDetails.protocolVersionRange,
- );
-
- if (
- versionMatch &&
- !versionMatch.compatible &&
- versionMatch.currentCmp === -1
- ) {
- logger.warn(
- `wallet's support for exchange protocol version ${WALLET_EXCHANGE_PROTOCOL_VERSION} might be outdated ` +
- `(exchange has ${exchangeDetails.protocolVersionRange}), checking for updates`,
- );
- }
- }
-
- let tosAccepted = false;
- if (exchangeDetails.tosAccepted?.timestamp) {
- if (exchangeDetails.tosAccepted.etag === exchangeDetails.tosCurrentEtag) {
- tosAccepted = true;
- }
- }
-
- const paytoUris = exchangeDetails.wireInfo.accounts.map((x) => x.payto_uri);
- if (!paytoUris) {
- throw Error("exchange is in invalid state");
- }
-
- const ret: ExchangeWithdrawalDetails = {
- earliestDepositExpiration,
- exchangePaytoUris: paytoUris,
- exchangeWireAccounts,
- exchangeVersion: exchangeDetails.protocolVersionRange || "unknown",
- isAudited,
- isTrusted,
- numOfferedDenoms: possibleDenoms.length,
- selectedDenoms,
- // FIXME: delete this field / replace by something we can display to the user
- trustedAuditorPubs: [],
- versionMatch,
- walletVersion: WALLET_EXCHANGE_PROTOCOL_VERSION,
- termsOfServiceAccepted: tosAccepted,
- withdrawalAmountEffective: Amounts.stringify(selectedDenoms.totalCoinValue),
- withdrawalAmountRaw: Amounts.stringify(instructedAmount),
- // TODO: remove hardcoding, this should be calculated from the denominations info
- // force enabled for testing
- ageRestrictionOptions: hasDenomWithAgeRestriction
- ? AGE_MASK_GROUPS
- : undefined,
- };
- return ret;
-}
-
-export interface GetWithdrawalDetailsForUriOpts {
- restrictAge?: 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 permanently added
- * to the wallet's list of known exchanges.
- */
-export async function getWithdrawalDetailsForUri(
- ws: InternalWalletState,
- talerWithdrawUri: string,
- opts: GetWithdrawalDetailsForUriOpts = {},
-): Promise<WithdrawUriInfoResponse> {
- logger.trace(`getting withdrawal details for URI ${talerWithdrawUri}`);
- const info = await getBankWithdrawalInfo(ws.http, talerWithdrawUri);
- logger.trace(`got bank info`);
- if (info.suggestedExchange) {
- // FIXME: right now the exchange gets permanently added,
- // we might want to only temporarily add it.
- try {
- await ws.exchangeOps.updateExchangeFromUrl(ws, info.suggestedExchange);
- } catch (e) {
- // We still continued if it failed, as other exchanges might be available.
- // We don't want to fail if the bank-suggested exchange is broken/offline.
- logger.trace(
- `querying bank-suggested exchange (${info.suggestedExchange}) failed`,
- );
- }
- }
-
- // Extract information about possible exchanges for the withdrawal
- // operation from the database.
-
- const exchanges: ExchangeListItem[] = [];
-
- await ws.db
- .mktx((x) => [
- x.exchanges,
- x.exchangeDetails,
- x.exchangeTos,
- x.denominations,
- x.operationRetries,
- ])
- .runReadOnly(async (tx) => {
- const exchangeRecords = await tx.exchanges.iter().toArray();
- for (const r of exchangeRecords) {
- const exchangeDetails = await ws.exchangeOps.getExchangeDetails(
- tx,
- r.baseUrl,
- );
- const denominations = await tx.denominations.indexes.byExchangeBaseUrl
- .iter(r.baseUrl)
- .toArray();
- const retryRecord = await tx.operationRetries.get(
- RetryTags.forExchangeUpdate(r),
- );
- if (exchangeDetails && denominations) {
- exchanges.push(
- makeExchangeListItem(r, exchangeDetails, retryRecord?.lastError),
- );
- }
- }
- });
-
- return {
- amount: Amounts.stringify(info.amount),
- defaultExchangeBaseUrl: info.suggestedExchange,
- possibleExchanges: exchanges,
- };
-}
-
-export async function getFundingPaytoUrisTx(
- ws: InternalWalletState,
- withdrawalGroupId: string,
-): Promise<string[]> {
- return await ws.db
- .mktx((x) => [x.exchanges, x.exchangeDetails, x.withdrawalGroups])
- .runReadWrite((tx) => getFundingPaytoUris(tx, withdrawalGroupId));
-}
-
-export function augmentPaytoUrisForWithdrawal(
- plainPaytoUris: string[],
- reservePub: string,
- instructedAmount: AmountLike,
-): string[] {
- return plainPaytoUris.map((x) =>
- addPaytoQueryParams(x, {
- amount: Amounts.stringify(instructedAmount),
- message: `Taler Withdrawal ${reservePub}`,
- }),
- );
-}
-
-/**
- * Get payto URIs that can be used to fund a withdrawal operation.
- */
-export async function getFundingPaytoUris(
- tx: GetReadOnlyAccess<{
- withdrawalGroups: typeof WalletStoresV1.withdrawalGroups;
- exchanges: typeof WalletStoresV1.exchanges;
- exchangeDetails: typeof WalletStoresV1.exchangeDetails;
- }>,
- withdrawalGroupId: string,
-): Promise<string[]> {
- const withdrawalGroup = await tx.withdrawalGroups.get(withdrawalGroupId);
- checkDbInvariant(!!withdrawalGroup);
- const exchangeDetails = await getExchangeDetails(
- tx,
- withdrawalGroup.exchangeBaseUrl,
- );
- if (!exchangeDetails) {
- logger.error(`exchange ${withdrawalGroup.exchangeBaseUrl} not found`);
- return [];
- }
- const plainPaytoUris =
- exchangeDetails.wireInfo?.accounts.map((x) => x.payto_uri) ?? [];
- if (!plainPaytoUris) {
- logger.error(
- `exchange ${withdrawalGroup.exchangeBaseUrl} has no wire info`,
- );
- return [];
- }
- return augmentPaytoUrisForWithdrawal(
- plainPaytoUris,
- withdrawalGroup.reservePub,
- withdrawalGroup.instructedAmount,
- );
-}
-
-async function getWithdrawalGroupRecordTx(
- db: DbAccess<typeof WalletStoresV1>,
- req: {
- withdrawalGroupId: string;
- },
-): Promise<WithdrawalGroupRecord | undefined> {
- return await db
- .mktx((x) => [x.withdrawalGroups])
- .runReadOnly(async (tx) => {
- return tx.withdrawalGroups.get(req.withdrawalGroupId);
- });
-}
-
-export function getReserveRequestTimeout(r: WithdrawalGroupRecord): Duration {
- return { d_ms: 60000 };
-}
-
-export function getBankStatusUrl(talerWithdrawUri: string): string {
- const uriResult = parseWithdrawUri(talerWithdrawUri);
- if (!uriResult) {
- throw Error(`can't parse withdrawal URL ${talerWithdrawUri}`);
- }
- const url = new URL(
- `withdrawal-operation/${uriResult.withdrawalOperationId}`,
- uriResult.bankIntegrationApiBaseUrl,
- );
- return url.href;
-}
-
-async function registerReserveWithBank(
- ws: InternalWalletState,
- withdrawalGroupId: string,
-): Promise<void> {
- const withdrawalGroup = await ws.db
- .mktx((x) => [x.withdrawalGroups])
- .runReadOnly(async (tx) => {
- return await tx.withdrawalGroups.get(withdrawalGroupId);
- });
- switch (withdrawalGroup?.status) {
- case WithdrawalGroupStatus.WaitConfirmBank:
- case WithdrawalGroupStatus.RegisteringBank:
- break;
- default:
- return;
- }
- if (
- withdrawalGroup.wgInfo.withdrawalType != WithdrawalRecordType.BankIntegrated
- ) {
- throw Error();
- }
- const bankInfo = withdrawalGroup.wgInfo.bankInfo;
- if (!bankInfo) {
- return;
- }
- const bankStatusUrl = getBankStatusUrl(bankInfo.talerWithdrawUri);
- const reqBody = {
- reserve_pub: withdrawalGroup.reservePub,
- selected_exchange: bankInfo.exchangePaytoUri,
- };
- logger.info(`registering reserve with bank: ${j2s(reqBody)}`);
- const httpResp = await ws.http.postJson(bankStatusUrl, reqBody, {
- timeout: getReserveRequestTimeout(withdrawalGroup),
- });
- await readSuccessResponseJsonOrThrow(
- httpResp,
- codecForBankWithdrawalOperationPostResponse(),
- );
- await ws.db
- .mktx((x) => [x.withdrawalGroups])
- .runReadWrite(async (tx) => {
- const r = await tx.withdrawalGroups.get(withdrawalGroupId);
- if (!r) {
- return;
- }
- switch (r.status) {
- case WithdrawalGroupStatus.RegisteringBank:
- case WithdrawalGroupStatus.WaitConfirmBank:
- break;
- default:
- return;
- }
- if (r.wgInfo.withdrawalType !== WithdrawalRecordType.BankIntegrated) {
- throw Error("invariant failed");
- }
- r.wgInfo.bankInfo.timestampReserveInfoPosted = AbsoluteTime.toTimestamp(
- AbsoluteTime.now(),
- );
- r.status = WithdrawalGroupStatus.WaitConfirmBank;
- await tx.withdrawalGroups.put(r);
- });
- ws.notify({ type: NotificationType.ReserveRegisteredWithBank });
-}
-
-interface BankStatusResult {
- status: BankStatusResultCode;
-}
-
-async function processReserveBankStatus(
- ws: InternalWalletState,
- withdrawalGroupId: string,
-): Promise<BankStatusResult> {
- const withdrawalGroup = await getWithdrawalGroupRecordTx(ws.db, {
- withdrawalGroupId,
- });
- switch (withdrawalGroup?.status) {
- case WithdrawalGroupStatus.WaitConfirmBank:
- case WithdrawalGroupStatus.RegisteringBank:
- break;
- default:
- return {
- status: BankStatusResultCode.Done,
- };
- }
-
- if (
- withdrawalGroup.wgInfo.withdrawalType != WithdrawalRecordType.BankIntegrated
- ) {
- throw Error("wrong withdrawal record type");
- }
- const bankInfo = withdrawalGroup.wgInfo.bankInfo;
- if (!bankInfo) {
- return {
- status: BankStatusResultCode.Done,
- };
- }
-
- const bankStatusUrl = getBankStatusUrl(bankInfo.talerWithdrawUri);
-
- const statusResp = await ws.http.get(bankStatusUrl, {
- timeout: getReserveRequestTimeout(withdrawalGroup),
- });
- const status = await readSuccessResponseJsonOrThrow(
- statusResp,
- codecForWithdrawOperationStatusResponse(),
- );
-
- if (status.aborted) {
- logger.info("bank aborted the withdrawal");
- await ws.db
- .mktx((x) => [x.withdrawalGroups])
- .runReadWrite(async (tx) => {
- const r = await tx.withdrawalGroups.get(withdrawalGroupId);
- if (!r) {
- return;
- }
- switch (r.status) {
- case WithdrawalGroupStatus.RegisteringBank:
- case WithdrawalGroupStatus.WaitConfirmBank:
- break;
- default:
- return;
- }
- if (r.wgInfo.withdrawalType !== WithdrawalRecordType.BankIntegrated) {
- throw Error("invariant failed");
- }
- const now = AbsoluteTime.toTimestamp(AbsoluteTime.now());
- r.wgInfo.bankInfo.timestampBankConfirmed = now;
- r.status = WithdrawalGroupStatus.BankAborted;
- await tx.withdrawalGroups.put(r);
- });
- return {
- status: BankStatusResultCode.Aborted,
- };
- }
-
- // Bank still needs to know our reserve info
- if (!status.selection_done) {
- await registerReserveWithBank(ws, withdrawalGroupId);
- return await processReserveBankStatus(ws, withdrawalGroupId);
- }
-
- // FIXME: Why do we do this?!
- if (withdrawalGroup.status === WithdrawalGroupStatus.RegisteringBank) {
- await registerReserveWithBank(ws, withdrawalGroupId);
- return await processReserveBankStatus(ws, withdrawalGroupId);
- }
-
- await ws.db
- .mktx((x) => [x.withdrawalGroups])
- .runReadWrite(async (tx) => {
- const r = await tx.withdrawalGroups.get(withdrawalGroupId);
- if (!r) {
- return;
- }
- // Re-check reserve status within transaction
- switch (r.status) {
- case WithdrawalGroupStatus.RegisteringBank:
- case WithdrawalGroupStatus.WaitConfirmBank:
- break;
- default:
- return;
- }
- if (r.wgInfo.withdrawalType !== WithdrawalRecordType.BankIntegrated) {
- throw Error("invariant failed");
- }
- if (status.transfer_done) {
- logger.info("withdrawal: transfer confirmed by bank.");
- const now = AbsoluteTime.toTimestamp(AbsoluteTime.now());
- r.wgInfo.bankInfo.timestampBankConfirmed = now;
- r.status = WithdrawalGroupStatus.QueryingStatus;
- } else {
- logger.info("withdrawal: transfer not yet confirmed by bank");
- r.wgInfo.bankInfo.confirmUrl = status.confirm_transfer_url;
- r.senderWire = status.sender_wire;
- }
- await tx.withdrawalGroups.put(r);
- });
-
- if (status.transfer_done) {
- return {
- status: BankStatusResultCode.Done,
- };
- } else {
- return {
- status: BankStatusResultCode.Waiting,
- };
- }
-}
-
-export async function internalCreateWithdrawalGroup(
- ws: InternalWalletState,
- args: {
- reserveStatus: WithdrawalGroupStatus;
- amount: AmountJson;
- exchangeBaseUrl: string;
- forcedDenomSel?: ForcedDenomSel;
- reserveKeyPair?: EddsaKeypair;
- restrictAge?: number;
- wgInfo: WgInfo;
- },
-): Promise<WithdrawalGroupRecord> {
- const reserveKeyPair =
- args.reserveKeyPair ?? (await ws.cryptoApi.createEddsaKeypair({}));
- const now = AbsoluteTime.toTimestamp(AbsoluteTime.now());
- const secretSeed = encodeCrock(getRandomBytes(32));
- const canonExchange = canonicalizeBaseUrl(args.exchangeBaseUrl);
- const withdrawalGroupId = encodeCrock(getRandomBytes(32));
- const amount = args.amount;
-
- await updateWithdrawalDenoms(ws, canonExchange);
- const denoms = await getCandidateWithdrawalDenoms(ws, canonExchange);
-
- let initialDenomSel: DenomSelectionState;
- const denomSelUid = encodeCrock(getRandomBytes(16));
- if (args.forcedDenomSel) {
- logger.warn("using forced denom selection");
- initialDenomSel = selectForcedWithdrawalDenominations(
- amount,
- denoms,
- args.forcedDenomSel,
- );
- } else {
- initialDenomSel = selectWithdrawalDenominations(amount, denoms);
- }
-
- const withdrawalGroup: WithdrawalGroupRecord = {
- denomSelUid,
- denomsSel: initialDenomSel,
- exchangeBaseUrl: canonExchange,
- instructedAmount: Amounts.stringify(amount),
- timestampStart: now,
- rawWithdrawalAmount: initialDenomSel.totalWithdrawCost,
- effectiveWithdrawalAmount: initialDenomSel.totalCoinValue,
- secretSeed,
- reservePriv: reserveKeyPair.priv,
- reservePub: reserveKeyPair.pub,
- status: args.reserveStatus,
- withdrawalGroupId,
- restrictAge: args.restrictAge,
- senderWire: undefined,
- timestampFinish: undefined,
- wgInfo: args.wgInfo,
- };
-
- const exchangeInfo = await updateExchangeFromUrl(ws, canonExchange);
- const exchangeDetails = exchangeInfo.exchangeDetails;
- if (!exchangeDetails) {
- logger.trace(exchangeDetails);
- throw Error("exchange not updated");
- }
- const { isAudited, isTrusted } = await getExchangeTrust(
- ws,
- exchangeInfo.exchange,
- );
-
- await ws.db
- .mktx((x) => [
- x.withdrawalGroups,
- x.reserves,
- x.exchanges,
- x.exchangeDetails,
- x.exchangeTrust,
- ])
- .runReadWrite(async (tx) => {
- await tx.withdrawalGroups.add(withdrawalGroup);
- await tx.reserves.put({
- reservePub: withdrawalGroup.reservePub,
- reservePriv: withdrawalGroup.reservePriv,
- });
-
- if (!isAudited && !isTrusted) {
- await tx.exchangeTrust.put({
- currency: amount.currency,
- exchangeBaseUrl: canonExchange,
- exchangeMasterPub: exchangeDetails.masterPublicKey,
- uids: [encodeCrock(getRandomBytes(32))],
- });
- }
- });
-
- return withdrawalGroup;
-}
-
-export async function acceptWithdrawalFromUri(
- ws: InternalWalletState,
- req: {
- talerWithdrawUri: string;
- selectedExchange: string;
- forcedDenomSel?: ForcedDenomSel;
- restrictAge?: number;
- },
-): Promise<AcceptWithdrawalResponse> {
- const selectedExchange = canonicalizeBaseUrl(req.selectedExchange);
- logger.info(
- `accepting withdrawal via ${req.talerWithdrawUri}, canonicalized selected exchange ${selectedExchange}`,
- );
- const existingWithdrawalGroup = await ws.db
- .mktx((x) => [x.withdrawalGroups])
- .runReadOnly(async (tx) => {
- return await tx.withdrawalGroups.indexes.byTalerWithdrawUri.get(
- req.talerWithdrawUri,
- );
- });
-
- if (existingWithdrawalGroup) {
- let url: string | undefined;
- if (
- existingWithdrawalGroup.wgInfo.withdrawalType ===
- WithdrawalRecordType.BankIntegrated
- ) {
- url = existingWithdrawalGroup.wgInfo.bankInfo.confirmUrl;
- }
- return {
- reservePub: existingWithdrawalGroup.reservePub,
- confirmTransferUrl: url,
- transactionId: makeTransactionId(
- TransactionType.Withdrawal,
- existingWithdrawalGroup.withdrawalGroupId,
- ),
- };
- }
-
- await updateExchangeFromUrl(ws, selectedExchange);
- const withdrawInfo = await getBankWithdrawalInfo(
- ws.http,
- req.talerWithdrawUri,
- );
- const exchangePaytoUri = await getExchangePaytoUri(
- ws,
- selectedExchange,
- withdrawInfo.wireTypes,
- );
-
- const withdrawalGroup = await internalCreateWithdrawalGroup(ws, {
- amount: withdrawInfo.amount,
- exchangeBaseUrl: req.selectedExchange,
- wgInfo: {
- withdrawalType: WithdrawalRecordType.BankIntegrated,
- bankInfo: {
- exchangePaytoUri,
- talerWithdrawUri: req.talerWithdrawUri,
- confirmUrl: withdrawInfo.confirmTransferUrl,
- timestampBankConfirmed: undefined,
- timestampReserveInfoPosted: undefined,
- },
- },
- restrictAge: req.restrictAge,
- forcedDenomSel: req.forcedDenomSel,
- reserveStatus: WithdrawalGroupStatus.RegisteringBank,
- });
-
- const withdrawalGroupId = withdrawalGroup.withdrawalGroupId;
-
- // We do this here, as the reserve should be registered before we return,
- // so that we can redirect the user to the bank's status page.
- await processReserveBankStatus(ws, withdrawalGroupId);
- const processedWithdrawalGroup = await getWithdrawalGroupRecordTx(ws.db, {
- withdrawalGroupId,
- });
- if (processedWithdrawalGroup?.status === WithdrawalGroupStatus.BankAborted) {
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK,
- {},
- );
- }
-
- // Start withdrawal in the background
- processWithdrawalGroup(ws, withdrawalGroupId, {
- forceNow: true,
- }).catch((err) => {
- logger.error("Processing withdrawal (after creation) failed:", err);
- });
-
- return {
- reservePub: withdrawalGroup.reservePub,
- confirmTransferUrl: withdrawInfo.confirmTransferUrl,
- transactionId: makeTransactionId(
- TransactionType.Withdrawal,
- withdrawalGroupId,
- ),
- };
-}
-
-/**
- * Create a manual withdrawal operation.
- *
- * Adds the corresponding exchange as a trusted exchange if it is neither
- * audited nor trusted already.
- *
- * Asynchronously starts the withdrawal.
- */
-export async function createManualWithdrawal(
- ws: InternalWalletState,
- req: {
- exchangeBaseUrl: string;
- amount: AmountLike;
- restrictAge?: number;
- forcedDenomSel?: ForcedDenomSel;
- },
-): Promise<AcceptManualWithdrawalResult> {
- const withdrawalGroup = await internalCreateWithdrawalGroup(ws, {
- amount: Amounts.jsonifyAmount(req.amount),
- wgInfo: {
- withdrawalType: WithdrawalRecordType.BankManual,
- },
- exchangeBaseUrl: req.exchangeBaseUrl,
- forcedDenomSel: req.forcedDenomSel,
- restrictAge: req.restrictAge,
- reserveStatus: WithdrawalGroupStatus.QueryingStatus,
- });
-
- const withdrawalGroupId = withdrawalGroup.withdrawalGroupId;
-
- const exchangePaytoUris = await ws.db
- .mktx((x) => [
- x.withdrawalGroups,
- x.exchanges,
- x.exchangeDetails,
- x.exchangeTrust,
- ])
- .runReadWrite(async (tx) => {
- return await getFundingPaytoUris(tx, withdrawalGroup.withdrawalGroupId);
- });
-
- // Start withdrawal in the background (do not await!)
- // FIXME: We could also interrupt the task look if it is waiting and
- // rely on retry handling to re-process the withdrawal group.
- runOperationWithErrorReporting(
- ws,
- RetryTags.forWithdrawal(withdrawalGroup),
- async () => {
- return await processWithdrawalGroup(ws, withdrawalGroupId, {
- forceNow: true,
- });
- },
- );
-
- return {
- reservePub: withdrawalGroup.reservePub,
- exchangePaytoUris: exchangePaytoUris,
- transactionId: makeTransactionId(
- TransactionType.Withdrawal,
- withdrawalGroupId,
- ),
- };
-}
diff --git a/packages/taler-wallet-core/src/pay-merchant.ts b/packages/taler-wallet-core/src/pay-merchant.ts
new file mode 100644
index 000000000..49ebc282e
--- /dev/null
+++ b/packages/taler-wallet-core/src/pay-merchant.ts
@@ -0,0 +1,3503 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Implementation of the payment operation, including downloading and
+ * claiming of proposals.
+ *
+ * @author Florian Dold
+ */
+
+/**
+ * Imports.
+ */
+import {
+ AbortingCoin,
+ AbortRequest,
+ AbsoluteTime,
+ AmountJson,
+ Amounts,
+ AmountString,
+ assertUnreachable,
+ AsyncFlag,
+ checkDbInvariant,
+ codecForAbortResponse,
+ codecForMerchantContractTerms,
+ codecForMerchantOrderStatusPaid,
+ codecForMerchantPayResponse,
+ codecForMerchantPostOrderResponse,
+ codecForProposal,
+ codecForWalletRefundResponse,
+ CoinDepositPermission,
+ CoinRefreshRequest,
+ ConfirmPayResult,
+ ConfirmPayResultType,
+ ContractTermsUtil,
+ Duration,
+ encodeCrock,
+ ForcedCoinSel,
+ getRandomBytes,
+ HttpStatusCode,
+ j2s,
+ Logger,
+ makeErrorDetail,
+ makePendingOperationFailedError,
+ MerchantCoinRefundStatus,
+ MerchantContractTerms,
+ MerchantPayResponse,
+ MerchantUsingTemplateDetails,
+ NotificationType,
+ parsePayTemplateUri,
+ parsePayUri,
+ parseTalerUri,
+ PreparePayResult,
+ PreparePayResultType,
+ PreparePayTemplateRequest,
+ randomBytes,
+ RefreshReason,
+ SelectedProspectiveCoin,
+ SharePaymentResult,
+ StartRefundQueryForUriResponse,
+ stringifyPayUri,
+ stringifyTalerUri,
+ TalerError,
+ TalerErrorCode,
+ TalerErrorDetail,
+ TalerPreciseTimestamp,
+ TalerProtocolViolationError,
+ TalerUriAction,
+ TransactionAction,
+ TransactionIdStr,
+ TransactionMajorState,
+ TransactionMinorState,
+ TransactionState,
+ TransactionType,
+ URL,
+ WalletContractData,
+} from "@gnu-taler/taler-util";
+import {
+ getHttpResponseErrorDetails,
+ readSuccessResponseJsonOrErrorCode,
+ readSuccessResponseJsonOrThrow,
+ readTalerErrorResponse,
+ readUnexpectedResponseDetails,
+ throwUnexpectedRequestError,
+} from "@gnu-taler/taler-util/http";
+import { PreviousPayCoins, selectPayCoins } from "./coinSelection.js";
+import {
+ constructTaskIdentifier,
+ PendingTaskType,
+ spendCoins,
+ TaskIdStr,
+ TaskRunResult,
+ TaskRunResultType,
+ TombstoneTag,
+ TransactionContext,
+ TransitionResultType,
+} from "./common.js";
+import { EddsaKeypair } from "./crypto/cryptoImplementation.js";
+import {
+ CoinRecord,
+ DbCoinSelection,
+ DenominationRecord,
+ PurchaseRecord,
+ PurchaseStatus,
+ RefundGroupRecord,
+ RefundGroupStatus,
+ RefundItemRecord,
+ RefundItemStatus,
+ RefundReason,
+ timestampPreciseToDb,
+ timestampProtocolFromDb,
+ timestampProtocolToDb,
+ WalletDbReadOnlyTransaction,
+ WalletDbReadWriteTransaction,
+ WalletStoresV1,
+} from "./db.js";
+import { DbReadWriteTransaction, StoreNames } from "./query.js";
+import {
+ calculateRefreshOutput,
+ createRefreshGroup,
+ getTotalRefreshCost,
+} from "./refresh.js";
+import {
+ constructTransactionIdentifier,
+ notifyTransition,
+ parseTransactionIdentifier,
+} from "./transactions.js";
+import {
+ EXCHANGE_COINS_LOCK,
+ getDenomInfo,
+ WalletExecutionContext,
+} from "./wallet.js";
+
+/**
+ * Logger.
+ */
+const logger = new Logger("pay-merchant.ts");
+
+export class PayMerchantTransactionContext implements TransactionContext {
+ readonly transactionId: TransactionIdStr;
+ readonly taskId: TaskIdStr;
+
+ constructor(
+ public wex: WalletExecutionContext,
+ public proposalId: string,
+ ) {
+ this.transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Payment,
+ proposalId,
+ });
+ this.taskId = constructTaskIdentifier({
+ tag: PendingTaskType.Purchase,
+ proposalId,
+ });
+ }
+
+ /**
+ * Transition a payment transition.
+ */
+ async transition(
+ f: (rec: PurchaseRecord) => Promise<TransitionResultType>,
+ ): Promise<void> {
+ return this.transitionExtra(
+ {
+ extraStores: [],
+ },
+ f,
+ );
+ }
+
+ /**
+ * Transition a payment transition.
+ * Extra object stores may be accessed during the transition.
+ */
+ async transitionExtra<
+ StoreNameArray extends Array<StoreNames<typeof WalletStoresV1>> = [],
+ >(
+ opts: { extraStores: StoreNameArray },
+ f: (
+ rec: PurchaseRecord,
+ tx: DbReadWriteTransaction<
+ typeof WalletStoresV1,
+ ["purchases", ...StoreNameArray]
+ >,
+ ) => Promise<TransitionResultType>,
+ ): Promise<void> {
+ const ws = this.wex;
+ const extraStores = opts.extraStores ?? [];
+ const transitionInfo = await ws.db.runReadWriteTx(
+ { storeNames: ["purchases", ...extraStores] },
+ async (tx) => {
+ const purchaseRec = await tx.purchases.get(this.proposalId);
+ if (!purchaseRec) {
+ throw Error("purchase not found anymore");
+ }
+ const oldTxState = computePayMerchantTransactionState(purchaseRec);
+ const res = await f(purchaseRec, tx);
+ switch (res) {
+ case TransitionResultType.Transition: {
+ await tx.purchases.put(purchaseRec);
+ const newTxState = computePayMerchantTransactionState(purchaseRec);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ default:
+ return undefined;
+ }
+ },
+ );
+ notifyTransition(ws, this.transactionId, transitionInfo);
+ }
+
+ async deleteTransaction(): Promise<void> {
+ const { wex: ws, proposalId } = this;
+ await ws.db.runReadWriteTx(
+ { storeNames: ["purchases", "tombstones"] },
+ async (tx) => {
+ let found = false;
+ const purchase = await tx.purchases.get(proposalId);
+ if (purchase) {
+ found = true;
+ await tx.purchases.delete(proposalId);
+ }
+ if (found) {
+ await tx.tombstones.put({
+ id: TombstoneTag.DeletePayment + ":" + proposalId,
+ });
+ }
+ },
+ );
+ }
+
+ async suspendTransaction(): Promise<void> {
+ const { wex, proposalId, transactionId } = this;
+ wex.taskScheduler.stopShepherdTask(this.taskId);
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["purchases"] },
+ async (tx) => {
+ const purchase = await tx.purchases.get(proposalId);
+ if (!purchase) {
+ throw Error("purchase not found");
+ }
+ const oldTxState = computePayMerchantTransactionState(purchase);
+ let newStatus = transitionSuspend[purchase.purchaseStatus];
+ if (!newStatus) {
+ return undefined;
+ }
+ await tx.purchases.put(purchase);
+ const newTxState = computePayMerchantTransactionState(purchase);
+ return { oldTxState, newTxState };
+ },
+ );
+ notifyTransition(wex, transactionId, transitionInfo);
+ }
+
+ async abortTransaction(): Promise<void> {
+ const { wex, proposalId, transactionId } = this;
+ const transitionInfo = await wex.db.runReadWriteTx(
+ {
+ storeNames: [
+ "purchases",
+ "refreshGroups",
+ "refreshSessions",
+ "denominations",
+ "coinAvailability",
+ "coins",
+ "operationRetries",
+ ],
+ },
+ async (tx) => {
+ const purchase = await tx.purchases.get(proposalId);
+ if (!purchase) {
+ throw Error("purchase not found");
+ }
+ const oldTxState = computePayMerchantTransactionState(purchase);
+ const oldStatus = purchase.purchaseStatus;
+ switch (oldStatus) {
+ case PurchaseStatus.Done:
+ return;
+ case PurchaseStatus.PendingPaying:
+ case PurchaseStatus.SuspendedPaying: {
+ purchase.purchaseStatus = PurchaseStatus.AbortingWithRefund;
+ if (purchase.payInfo && purchase.payInfo.payCoinSelection) {
+ const coinSel = purchase.payInfo.payCoinSelection;
+ const currency = Amounts.currencyOf(
+ purchase.payInfo.totalPayCost,
+ );
+ const refreshCoins: CoinRefreshRequest[] = [];
+ for (let i = 0; i < coinSel.coinPubs.length; i++) {
+ refreshCoins.push({
+ amount: coinSel.coinContributions[i],
+ coinPub: coinSel.coinPubs[i],
+ });
+ }
+ await createRefreshGroup(
+ wex,
+ tx,
+ currency,
+ refreshCoins,
+ RefreshReason.AbortPay,
+ this.transactionId,
+ );
+ }
+ break;
+ }
+ case PurchaseStatus.PendingQueryingAutoRefund:
+ case PurchaseStatus.SuspendedQueryingAutoRefund:
+ case PurchaseStatus.PendingAcceptRefund:
+ case PurchaseStatus.SuspendedPendingAcceptRefund:
+ case PurchaseStatus.PendingQueryingRefund:
+ case PurchaseStatus.SuspendedQueryingRefund:
+ if (!purchase.timestampFirstSuccessfulPay) {
+ throw Error("invalid state");
+ }
+ purchase.purchaseStatus = PurchaseStatus.Done;
+ break;
+ case PurchaseStatus.DialogProposed:
+ purchase.purchaseStatus = PurchaseStatus.AbortedProposalRefused;
+ break;
+ default:
+ return;
+ }
+ await tx.purchases.put(purchase);
+ await tx.operationRetries.delete(this.taskId);
+ const newTxState = computePayMerchantTransactionState(purchase);
+ return { oldTxState, newTxState };
+ },
+ );
+ wex.taskScheduler.stopShepherdTask(this.taskId);
+ notifyTransition(wex, transactionId, transitionInfo);
+ wex.taskScheduler.startShepherdTask(this.taskId);
+ }
+
+ async resumeTransaction(): Promise<void> {
+ const { wex, proposalId, transactionId, taskId: retryTag } = this;
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["purchases"] },
+ async (tx) => {
+ const purchase = await tx.purchases.get(proposalId);
+ if (!purchase) {
+ throw Error("purchase not found");
+ }
+ const oldTxState = computePayMerchantTransactionState(purchase);
+ let newStatus = transitionResume[purchase.purchaseStatus];
+ if (!newStatus) {
+ return undefined;
+ }
+ await tx.purchases.put(purchase);
+ const newTxState = computePayMerchantTransactionState(purchase);
+ return { oldTxState, newTxState };
+ },
+ );
+ notifyTransition(wex, transactionId, transitionInfo);
+ wex.taskScheduler.startShepherdTask(this.taskId);
+ }
+
+ async failTransaction(): Promise<void> {
+ const { wex, proposalId, transactionId } = this;
+ const transitionInfo = await wex.db.runReadWriteTx(
+ {
+ storeNames: [
+ "purchases",
+ "refreshGroups",
+ "denominations",
+ "coinAvailability",
+ "coins",
+ "operationRetries",
+ ],
+ },
+ async (tx) => {
+ const purchase = await tx.purchases.get(proposalId);
+ if (!purchase) {
+ throw Error("purchase not found");
+ }
+ const oldTxState = computePayMerchantTransactionState(purchase);
+ let newState: PurchaseStatus | undefined = undefined;
+ switch (purchase.purchaseStatus) {
+ case PurchaseStatus.AbortingWithRefund:
+ newState = PurchaseStatus.FailedAbort;
+ break;
+ }
+ if (newState) {
+ purchase.purchaseStatus = newState;
+ await tx.purchases.put(purchase);
+ }
+ const newTxState = computePayMerchantTransactionState(purchase);
+ return { oldTxState, newTxState };
+ },
+ );
+ notifyTransition(wex, transactionId, transitionInfo);
+ wex.taskScheduler.stopShepherdTask(this.taskId);
+ }
+}
+
+export class RefundTransactionContext implements TransactionContext {
+ public transactionId: TransactionIdStr;
+ public taskId: TaskIdStr | undefined = undefined;
+ constructor(
+ public wex: WalletExecutionContext,
+ public refundGroupId: string,
+ ) {
+ this.transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Refund,
+ refundGroupId,
+ });
+ }
+
+ async deleteTransaction(): Promise<void> {
+ const { wex, refundGroupId, transactionId } = this;
+ await wex.db.runReadWriteTx(
+ { storeNames: ["refundGroups", "tombstones"] },
+ async (tx) => {
+ const refundRecord = await tx.refundGroups.get(refundGroupId);
+ if (!refundRecord) {
+ return;
+ }
+ await tx.refundGroups.delete(refundGroupId);
+ await tx.tombstones.put({ id: transactionId });
+ // FIXME: Also tombstone the refund items, so that they won't reappear.
+ },
+ );
+ }
+
+ suspendTransaction(): Promise<void> {
+ throw new Error("Unsupported operation");
+ }
+
+ abortTransaction(): Promise<void> {
+ throw new Error("Unsupported operation");
+ }
+
+ resumeTransaction(): Promise<void> {
+ throw new Error("Unsupported operation");
+ }
+
+ failTransaction(): Promise<void> {
+ throw new Error("Unsupported operation");
+ }
+}
+
+/**
+ * Compute the total cost of a payment to the customer.
+ *
+ * This includes the amount taken by the merchant, fees (wire/deposit) contributed
+ * by the customer, refreshing fees, fees for withdraw-after-refresh and "trimmings"
+ * of coins that are too small to spend.
+ */
+export async function getTotalPaymentCost(
+ wex: WalletExecutionContext,
+ currency: string,
+ pcs: SelectedProspectiveCoin[],
+): Promise<AmountJson> {
+ 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;
+ },
+ );
+}
+
+async function failProposalPermanently(
+ wex: WalletExecutionContext,
+ proposalId: string,
+ err: TalerErrorDetail,
+): Promise<void> {
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Payment,
+ proposalId,
+ });
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["purchases"] },
+ async (tx) => {
+ const p = await tx.purchases.get(proposalId);
+ if (!p) {
+ return;
+ }
+ // FIXME: We don't store the error detail here?!
+ const oldTxState = computePayMerchantTransactionState(p);
+ p.purchaseStatus = PurchaseStatus.FailedClaim;
+ const newTxState = computePayMerchantTransactionState(p);
+ await tx.purchases.put(p);
+ return { oldTxState, newTxState };
+ },
+ );
+ notifyTransition(wex, transactionId, transitionInfo);
+}
+
+function getPayRequestTimeout(purchase: PurchaseRecord): Duration {
+ return Duration.multiply(
+ { d_ms: 15000 },
+ 1 + (purchase.payInfo?.payCoinSelection?.coinPubs.length ?? 0) / 5,
+ );
+}
+
+/**
+ * Return the proposal download data for a purchase, throw if not available.
+ */
+export async function expectProposalDownload(
+ wex: WalletExecutionContext,
+ p: PurchaseRecord,
+ parentTx?: WalletDbReadOnlyTransaction<["contractTerms"]>,
+): Promise<{
+ contractData: WalletContractData;
+ contractTermsRaw: any;
+}> {
+ if (!p.download) {
+ throw Error("expected proposal to be downloaded");
+ }
+ 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,
+ };
+ }
+
+ if (parentTx) {
+ return getFromTransaction(parentTx);
+ }
+ return await wex.db.runReadOnlyTx(
+ { storeNames: ["contractTerms"] },
+ getFromTransaction,
+ );
+}
+
+export function extractContractData(
+ parsedContractTerms: MerchantContractTerms,
+ contractTermsHash: string,
+ merchantSig: string,
+): WalletContractData {
+ const amount = Amounts.parseOrThrow(parsedContractTerms.amount);
+ return {
+ amount: Amounts.stringify(amount),
+ contractTermsHash: contractTermsHash,
+ fulfillmentUrl: parsedContractTerms.fulfillment_url ?? "",
+ merchantBaseUrl: parsedContractTerms.merchant_base_url,
+ merchantPub: parsedContractTerms.merchant_pub,
+ merchantSig,
+ orderId: parsedContractTerms.order_id,
+ summary: parsedContractTerms.summary,
+ autoRefund: parsedContractTerms.auto_refund,
+ payDeadline: parsedContractTerms.pay_deadline,
+ refundDeadline: parsedContractTerms.refund_deadline,
+ allowedExchanges: parsedContractTerms.exchanges.map((x) => ({
+ exchangeBaseUrl: x.url,
+ exchangePub: x.master_pub,
+ })),
+ timestamp: parsedContractTerms.timestamp,
+ wireMethod: parsedContractTerms.wire_method,
+ wireInfoHash: parsedContractTerms.h_wire,
+ maxDepositFee: Amounts.stringify(parsedContractTerms.max_fee),
+ merchant: parsedContractTerms.merchant,
+ summaryI18n: parsedContractTerms.summary_i18n,
+ minimumAge: parsedContractTerms.minimum_age,
+ };
+}
+
+async function processDownloadProposal(
+ wex: WalletExecutionContext,
+ proposalId: string,
+): Promise<TaskRunResult> {
+ const proposal = await wex.db.runReadOnlyTx(
+ { storeNames: ["purchases"] },
+ async (tx) => {
+ return await tx.purchases.get(proposalId);
+ },
+ );
+
+ if (!proposal) {
+ return TaskRunResult.finished();
+ }
+
+ const ctx = new PayMerchantTransactionContext(wex, proposalId);
+
+ if (proposal.purchaseStatus != PurchaseStatus.PendingDownloadingProposal) {
+ logger.error(
+ `unexpected state ${proposal.purchaseStatus}/${
+ PurchaseStatus[proposal.purchaseStatus]
+ } for ${ctx.transactionId} in processDownloadProposal`,
+ );
+ return TaskRunResult.finished();
+ }
+
+ const transactionId = ctx.transactionId;
+
+ const orderClaimUrl = new URL(
+ `orders/${proposal.orderId}/claim`,
+ proposal.merchantBaseUrl,
+ ).href;
+ logger.trace("downloading contract from '" + orderClaimUrl + "'");
+
+ const requestBody: {
+ nonce: string;
+ token?: string;
+ } = {
+ nonce: proposal.noncePub,
+ };
+ if (proposal.claimToken) {
+ requestBody.token = proposal.claimToken;
+ }
+
+ const httpResponse = await wex.http.fetch(orderClaimUrl, {
+ method: "POST",
+ body: requestBody,
+ cancellationToken: wex.cancellationToken,
+ });
+ const r = await readSuccessResponseJsonOrErrorCode(
+ httpResponse,
+ codecForProposal(),
+ );
+ if (r.isError) {
+ switch (r.talerErrorResponse.code) {
+ case TalerErrorCode.MERCHANT_POST_ORDERS_ID_CLAIM_ALREADY_CLAIMED:
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_ORDER_ALREADY_CLAIMED,
+ {
+ orderId: proposal.orderId,
+ claimUrl: orderClaimUrl,
+ },
+ "order already claimed (likely by other wallet)",
+ );
+ default:
+ throwUnexpectedRequestError(httpResponse, r.talerErrorResponse);
+ }
+ }
+ const proposalResp = r.response;
+
+ // The proposalResp contains the contract terms as raw JSON,
+ // as the code to parse them doesn't necessarily round-trip.
+ // We need this raw JSON to compute the contract terms hash.
+
+ // FIXME: Do better error handling, check if the
+ // contract terms have all their forgettable information still
+ // present. The wallet should never accept contract terms
+ // with missing information from the merchant.
+
+ const isWellFormed = ContractTermsUtil.validateForgettable(
+ proposalResp.contract_terms,
+ );
+
+ if (!isWellFormed) {
+ logger.trace(
+ `malformed contract terms: ${j2s(proposalResp.contract_terms)}`,
+ );
+ const err = makeErrorDetail(
+ TalerErrorCode.WALLET_CONTRACT_TERMS_MALFORMED,
+ {},
+ "validation for well-formedness failed",
+ );
+ await failProposalPermanently(wex, proposalId, err);
+ throw makePendingOperationFailedError(
+ err,
+ TransactionType.Payment,
+ proposalId,
+ );
+ }
+
+ const contractTermsHash = ContractTermsUtil.hashContractTerms(
+ proposalResp.contract_terms,
+ );
+
+ logger.info(`Contract terms hash: ${contractTermsHash}`);
+
+ let parsedContractTerms: MerchantContractTerms;
+
+ try {
+ parsedContractTerms = codecForMerchantContractTerms().decode(
+ proposalResp.contract_terms,
+ );
+ } catch (e) {
+ const err = makeErrorDetail(
+ TalerErrorCode.WALLET_CONTRACT_TERMS_MALFORMED,
+ {},
+ `schema validation failed: ${e}`,
+ );
+ await failProposalPermanently(wex, proposalId, err);
+ throw makePendingOperationFailedError(
+ err,
+ TransactionType.Payment,
+ proposalId,
+ );
+ }
+
+ const sigValid = await wex.cryptoApi.isValidContractTermsSignature({
+ contractTermsHash,
+ merchantPub: parsedContractTerms.merchant_pub,
+ sig: proposalResp.sig,
+ });
+
+ if (!sigValid) {
+ const err = makeErrorDetail(
+ TalerErrorCode.WALLET_CONTRACT_TERMS_SIGNATURE_INVALID,
+ {
+ merchantPub: parsedContractTerms.merchant_pub,
+ orderId: parsedContractTerms.order_id,
+ },
+ "merchant's signature on contract terms is invalid",
+ );
+ await failProposalPermanently(wex, proposalId, err);
+ throw makePendingOperationFailedError(
+ err,
+ TransactionType.Payment,
+ proposalId,
+ );
+ }
+
+ const fulfillmentUrl = parsedContractTerms.fulfillment_url;
+
+ const baseUrlForDownload = proposal.merchantBaseUrl;
+ const baseUrlFromContractTerms = parsedContractTerms.merchant_base_url;
+
+ if (baseUrlForDownload !== baseUrlFromContractTerms) {
+ const err = makeErrorDetail(
+ TalerErrorCode.WALLET_CONTRACT_TERMS_BASE_URL_MISMATCH,
+ {
+ baseUrlForDownload,
+ baseUrlFromContractTerms,
+ },
+ "merchant base URL mismatch",
+ );
+ await failProposalPermanently(wex, proposalId, err);
+ throw makePendingOperationFailedError(
+ err,
+ TransactionType.Payment,
+ proposalId,
+ );
+ }
+
+ const contractData = extractContractData(
+ parsedContractTerms,
+ contractTermsHash,
+ proposalResp.sig,
+ );
+
+ logger.trace(`extracted contract data: ${j2s(contractData)}`);
+
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["purchases", "contractTerms"] },
+ async (tx) => {
+ const p = await tx.purchases.get(proposalId);
+ if (!p) {
+ return;
+ }
+ if (p.purchaseStatus !== PurchaseStatus.PendingDownloadingProposal) {
+ return;
+ }
+ const oldTxState = computePayMerchantTransactionState(p);
+ p.download = {
+ contractTermsHash,
+ contractTermsMerchantSig: contractData.merchantSig,
+ currency: Amounts.currencyOf(contractData.amount),
+ fulfillmentUrl: contractData.fulfillmentUrl,
+ };
+ await tx.contractTerms.put({
+ h: contractTermsHash,
+ contractTermsRaw: proposalResp.contract_terms,
+ });
+ const isResourceFulfillmentUrl =
+ fulfillmentUrl &&
+ (fulfillmentUrl.startsWith("http://") ||
+ fulfillmentUrl.startsWith("https://"));
+ let otherPurchase: PurchaseRecord | undefined;
+ if (isResourceFulfillmentUrl) {
+ otherPurchase =
+ await tx.purchases.indexes.byFulfillmentUrl.get(fulfillmentUrl);
+ }
+ // FIXME: Adjust this to account for refunds, don't count as repurchase
+ // if original order is refunded.
+ if (
+ otherPurchase &&
+ (otherPurchase.purchaseStatus == PurchaseStatus.Done ||
+ otherPurchase.purchaseStatus == PurchaseStatus.PendingPaying ||
+ otherPurchase.purchaseStatus == PurchaseStatus.PendingPayingReplay)
+ ) {
+ logger.warn("repurchase detected");
+ p.purchaseStatus = PurchaseStatus.DoneRepurchaseDetected;
+ p.repurchaseProposalId = otherPurchase.proposalId;
+ await tx.purchases.put(p);
+ } else {
+ p.purchaseStatus = p.shared
+ ? PurchaseStatus.DialogShared
+ : PurchaseStatus.DialogProposed;
+ await tx.purchases.put(p);
+ }
+ const newTxState = computePayMerchantTransactionState(p);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ },
+ );
+
+ notifyTransition(wex, transactionId, transitionInfo);
+
+ return TaskRunResult.progress();
+}
+
+/**
+ * Create a new purchase transaction if necessary. If a purchase
+ * record for the provided arguments already exists,
+ * return the old proposal ID.
+ */
+async function createOrReusePurchase(
+ wex: WalletExecutionContext,
+ merchantBaseUrl: string,
+ orderId: string,
+ sessionId: string | undefined,
+ claimToken: string | undefined,
+ noncePriv: string | undefined,
+): Promise<string> {
+ const oldProposals = await wex.db.runReadOnlyTx(
+ { storeNames: ["purchases"] },
+ async (tx) => {
+ return tx.purchases.indexes.byUrlAndOrderId.getAll([
+ merchantBaseUrl,
+ orderId,
+ ]);
+ },
+ );
+
+ const oldProposal = oldProposals.find((p) => {
+ return (
+ p.downloadSessionId === sessionId &&
+ (!noncePriv || p.noncePriv === noncePriv) &&
+ p.claimToken === claimToken
+ );
+ });
+ // If we have already claimed this proposal with the same sessionId
+ // nonce and claim token, reuse it. */
+ if (
+ oldProposal &&
+ oldProposal.downloadSessionId === sessionId &&
+ (!noncePriv || oldProposal.noncePriv === noncePriv) &&
+ oldProposal.claimToken === claimToken
+ ) {
+ logger.info(
+ `Found old proposal (status=${
+ PurchaseStatus[oldProposal.purchaseStatus]
+ }) for order ${orderId} at ${merchantBaseUrl}`,
+ );
+ if (oldProposal.purchaseStatus === PurchaseStatus.DialogShared) {
+ const download = await expectProposalDownload(wex, oldProposal);
+ const paid = await checkIfOrderIsAlreadyPaid(
+ wex,
+ download.contractData,
+ false,
+ );
+ logger.info(`old proposal paid: ${paid}`);
+ if (paid) {
+ // if this transaction was shared and the order is paid then it
+ // means that another wallet already paid the proposal
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["purchases"] },
+ async (tx) => {
+ const p = await tx.purchases.get(oldProposal.proposalId);
+ if (!p) {
+ logger.warn("purchase does not exist anymore");
+ return;
+ }
+ const oldTxState = computePayMerchantTransactionState(p);
+ p.purchaseStatus = PurchaseStatus.FailedPaidByOther;
+ const newTxState = computePayMerchantTransactionState(p);
+ await tx.purchases.put(p);
+ return { oldTxState, newTxState };
+ },
+ );
+
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Payment,
+ proposalId: oldProposal.proposalId,
+ });
+ notifyTransition(wex, transactionId, transitionInfo);
+ }
+ }
+ return oldProposal.proposalId;
+ }
+
+ let noncePair: EddsaKeypair;
+ let shared = false;
+ if (noncePriv) {
+ shared = true;
+ noncePair = {
+ priv: noncePriv,
+ pub: (await wex.cryptoApi.eddsaGetPublic({ priv: noncePriv })).pub,
+ };
+ } else {
+ noncePair = await wex.cryptoApi.createEddsaKeypair({});
+ }
+
+ const { priv, pub } = noncePair;
+ const proposalId = encodeCrock(getRandomBytes(32));
+
+ const proposalRecord: PurchaseRecord = {
+ download: undefined,
+ noncePriv: priv,
+ noncePub: pub,
+ claimToken,
+ timestamp: timestampPreciseToDb(TalerPreciseTimestamp.now()),
+ merchantBaseUrl,
+ orderId,
+ proposalId: proposalId,
+ purchaseStatus: PurchaseStatus.PendingDownloadingProposal,
+ repurchaseProposalId: undefined,
+ downloadSessionId: sessionId,
+ autoRefundDeadline: undefined,
+ lastSessionId: undefined,
+ merchantPaySig: undefined,
+ payInfo: undefined,
+ refundAmountAwaiting: undefined,
+ timestampAccept: undefined,
+ timestampFirstSuccessfulPay: undefined,
+ timestampLastRefundStatus: undefined,
+ pendingRemovedCoinPubs: undefined,
+ posConfirmation: undefined,
+ shared: shared,
+ };
+
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["purchases"] },
+ async (tx) => {
+ await tx.purchases.put(proposalRecord);
+ const oldTxState: TransactionState = {
+ major: TransactionMajorState.None,
+ };
+ const newTxState = computePayMerchantTransactionState(proposalRecord);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ },
+ );
+
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Payment,
+ proposalId,
+ });
+ notifyTransition(wex, transactionId, transitionInfo);
+ return proposalId;
+}
+
+async function storeFirstPaySuccess(
+ wex: WalletExecutionContext,
+ proposalId: string,
+ sessionId: string | undefined,
+ payResponse: MerchantPayResponse,
+): Promise<void> {
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Payment,
+ proposalId,
+ });
+ const now = AbsoluteTime.toPreciseTimestamp(AbsoluteTime.now());
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["contractTerms", "purchases"] },
+ async (tx) => {
+ const purchase = await tx.purchases.get(proposalId);
+
+ if (!purchase) {
+ logger.warn("purchase does not exist anymore");
+ return;
+ }
+ const isFirst = purchase.timestampFirstSuccessfulPay === undefined;
+ if (!isFirst) {
+ logger.warn("payment success already stored");
+ return;
+ }
+ const oldTxState = computePayMerchantTransactionState(purchase);
+ if (purchase.purchaseStatus === PurchaseStatus.PendingPaying) {
+ purchase.purchaseStatus = PurchaseStatus.Done;
+ }
+ purchase.timestampFirstSuccessfulPay = timestampPreciseToDb(now);
+ purchase.lastSessionId = sessionId;
+ purchase.merchantPaySig = payResponse.sig;
+ purchase.posConfirmation = payResponse.pos_confirmation;
+ const dl = purchase.download;
+ checkDbInvariant(!!dl);
+ const contractTermsRecord = await tx.contractTerms.get(
+ dl.contractTermsHash,
+ );
+ checkDbInvariant(!!contractTermsRecord);
+ const contractData = extractContractData(
+ contractTermsRecord.contractTermsRaw,
+ dl.contractTermsHash,
+ dl.contractTermsMerchantSig,
+ );
+ const protoAr = contractData.autoRefund;
+ if (protoAr) {
+ const ar = Duration.fromTalerProtocolDuration(protoAr);
+ logger.info("auto_refund present");
+ purchase.purchaseStatus = PurchaseStatus.PendingQueryingAutoRefund;
+ purchase.autoRefundDeadline = timestampProtocolToDb(
+ AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.addDuration(AbsoluteTime.now(), ar),
+ ),
+ );
+ }
+ await tx.purchases.put(purchase);
+ const newTxState = computePayMerchantTransactionState(purchase);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ },
+ );
+ notifyTransition(wex, transactionId, transitionInfo);
+}
+
+async function storePayReplaySuccess(
+ wex: WalletExecutionContext,
+ proposalId: string,
+ sessionId: string | undefined,
+): Promise<void> {
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Payment,
+ proposalId,
+ });
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["purchases"] },
+ async (tx) => {
+ const purchase = await tx.purchases.get(proposalId);
+
+ if (!purchase) {
+ logger.warn("purchase does not exist anymore");
+ return;
+ }
+ const isFirst = purchase.timestampFirstSuccessfulPay === undefined;
+ if (isFirst) {
+ throw Error("invalid payment state");
+ }
+ const oldTxState = computePayMerchantTransactionState(purchase);
+ if (
+ purchase.purchaseStatus === PurchaseStatus.PendingPaying ||
+ purchase.purchaseStatus === PurchaseStatus.PendingPayingReplay
+ ) {
+ purchase.purchaseStatus = PurchaseStatus.Done;
+ }
+ purchase.lastSessionId = sessionId;
+ await tx.purchases.put(purchase);
+ const newTxState = computePayMerchantTransactionState(purchase);
+ return { oldTxState, newTxState };
+ },
+ );
+ notifyTransition(wex, transactionId, transitionInfo);
+}
+
+/**
+ * Handle a 409 Conflict response from the merchant.
+ *
+ * We do this by going through the coin history provided by the exchange and
+ * (1) verifying the signatures from the exchange
+ * (2) adjusting the remaining coin value and refreshing it
+ * (3) re-do coin selection with the bad coin removed
+ */
+async function handleInsufficientFunds(
+ wex: WalletExecutionContext,
+ proposalId: string,
+ err: TalerErrorDetail,
+): Promise<void> {
+ logger.trace("handling insufficient funds, trying to re-select coins");
+
+ const proposal = await wex.db.runReadOnlyTx(
+ { storeNames: ["purchases"] },
+ async (tx) => {
+ return tx.purchases.get(proposalId);
+ },
+ );
+ if (!proposal) {
+ return;
+ }
+
+ logger.trace(`got error details: ${j2s(err)}`);
+
+ const exchangeReply = (err as any).exchange_reply;
+ if (
+ exchangeReply.code !== TalerErrorCode.EXCHANGE_GENERIC_INSUFFICIENT_FUNDS
+ ) {
+ // FIXME: set as failed
+ if (logger.shouldLogTrace()) {
+ logger.trace("got exchange error reply (see below)");
+ logger.trace(j2s(exchangeReply));
+ }
+ throw Error(`unable to handle /pay error response (${exchangeReply.code})`);
+ }
+
+ const brokenCoinPub = (exchangeReply as any).coin_pub;
+ logger.trace(`excluded broken coin pub=${brokenCoinPub}`);
+
+ if (!brokenCoinPub) {
+ throw new TalerProtocolViolationError();
+ }
+
+ const { contractData } = await expectProposalDownload(wex, proposal);
+
+ const prevPayCoins: PreviousPayCoins = [];
+
+ const payInfo = proposal.payInfo;
+ if (!payInfo) {
+ return;
+ }
+
+ const payCoinSelection = payInfo.payCoinSelection;
+ if (!payCoinSelection) {
+ 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");
+
+ await wex.db.runReadWriteTx(
+ {
+ storeNames: [
+ "purchases",
+ "coins",
+ "coinAvailability",
+ "denominations",
+ "refreshGroups",
+ "refreshSessions",
+ ],
+ },
+ async (tx) => {
+ const p = await tx.purchases.get(proposalId);
+ if (!p) {
+ return;
+ }
+ const payInfo = p.payInfo;
+ if (!payInfo) {
+ return;
+ }
+ // Convert to DB format
+ payInfo.payCoinSelection = {
+ coinContributions: res.coinSel.coins.map((x) => x.contribution),
+ coinPubs: res.coinSel.coins.map((x) => x.coinPub),
+ };
+ 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,
+ }),
+ coinPubs: payInfo.payCoinSelection.coinPubs,
+ contributions: payInfo.payCoinSelection.coinContributions.map((x) =>
+ Amounts.parseOrThrow(x),
+ ),
+ refreshReason: RefreshReason.PayMerchant,
+ });
+ },
+ );
+
+ wex.ws.notify({
+ type: NotificationType.BalanceChange,
+ hintTransactionId: constructTransactionIdentifier({
+ tag: TransactionType.Payment,
+ proposalId,
+ }),
+ });
+}
+
+// FIXME: Should take a transaction ID instead of a proposal ID
+// FIXME: Does way more than checking the payment
+// FIXME: Should return immediately.
+async function checkPaymentByProposalId(
+ wex: WalletExecutionContext,
+ proposalId: string,
+ sessionId?: string,
+): Promise<PreparePayResult> {
+ let proposal = await wex.db.runReadOnlyTx(
+ { storeNames: ["purchases"] },
+ async (tx) => {
+ return tx.purchases.get(proposalId);
+ },
+ );
+ if (!proposal) {
+ throw Error(`could not get proposal ${proposalId}`);
+ }
+ if (proposal.purchaseStatus === PurchaseStatus.DoneRepurchaseDetected) {
+ const existingProposalId = proposal.repurchaseProposalId;
+ if (existingProposalId) {
+ logger.trace("using existing purchase for same product");
+ const oldProposal = await wex.db.runReadOnlyTx(
+ { storeNames: ["purchases"] },
+ async (tx) => {
+ return tx.purchases.get(existingProposalId);
+ },
+ );
+ if (oldProposal) {
+ proposal = oldProposal;
+ }
+ }
+ }
+ const d = await expectProposalDownload(wex, proposal);
+ const contractData = d.contractData;
+ const merchantSig = d.contractData.merchantSig;
+ if (!merchantSig) {
+ throw Error("BUG: proposal is in invalid state");
+ }
+
+ proposalId = proposal.proposalId;
+
+ const currency = Amounts.currencyOf(contractData.amount);
+
+ const ctx = new PayMerchantTransactionContext(wex, proposalId);
+
+ const transactionId = ctx.transactionId;
+
+ const talerUri = stringifyTalerUri({
+ type: TalerUriAction.Pay,
+ merchantBaseUrl: proposal.merchantBaseUrl,
+ orderId: proposal.orderId,
+ sessionId: proposal.lastSessionId ?? proposal.downloadSessionId ?? "",
+ claimToken: proposal.claimToken,
+ });
+
+ // First check if we already paid for it.
+ const purchase = await wex.db.runReadOnlyTx(
+ { storeNames: ["purchases"] },
+ async (tx) => {
+ return tx.purchases.get(proposalId);
+ },
+ );
+
+ if (
+ !purchase ||
+ purchase.purchaseStatus === PurchaseStatus.DialogProposed ||
+ purchase.purchaseStatus === PurchaseStatus.DialogShared
+ ) {
+ const instructedAmount = Amounts.parseOrThrow(contractData.amount);
+ // If not already paid, check if we could pay for it.
+ const res = await selectPayCoins(wex, {
+ restrictExchanges: {
+ auditors: [],
+ exchanges: contractData.allowedExchanges,
+ },
+ contractTermsAmount: instructedAmount,
+ depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee),
+ prevPayCoins: [],
+ requiredMinimumAge: contractData.minimumAge,
+ restrictWireMethod: contractData.wireMethod,
+ });
+
+ let coins: SelectedProspectiveCoin[] | undefined = undefined;
+
+ switch (res.type) {
+ case "failure": {
+ logger.info("not allowing payment, insufficient coins");
+ logger.info(
+ `insufficient balance details: ${j2s(
+ res.insufficientBalanceDetails,
+ )}`,
+ );
+ return {
+ status: PreparePayResultType.InsufficientBalance,
+ contractTerms: d.contractTermsRaw,
+ proposalId: proposal.proposalId,
+ transactionId,
+ amountRaw: Amounts.stringify(d.contractData.amount),
+ talerUri,
+ balanceDetails: res.insufficientBalanceDetails,
+ };
+ }
+ case "prospective":
+ coins = res.result.prospectiveCoins;
+ break;
+ case "success":
+ coins = res.coinSel.coins;
+ break;
+ default:
+ assertUnreachable(res);
+ }
+
+ const totalCost = await getTotalPaymentCost(wex, currency, coins);
+ logger.trace("costInfo", totalCost);
+ logger.trace("coinsForPayment", res);
+
+ return {
+ status: PreparePayResultType.PaymentPossible,
+ contractTerms: d.contractTermsRaw,
+ transactionId,
+ proposalId: proposal.proposalId,
+ amountEffective: Amounts.stringify(totalCost),
+ amountRaw: Amounts.stringify(instructedAmount),
+ contractTermsHash: d.contractData.contractTermsHash,
+ talerUri,
+ };
+ }
+
+ if (
+ purchase.purchaseStatus === PurchaseStatus.Done &&
+ purchase.lastSessionId !== sessionId
+ ) {
+ logger.trace(
+ "automatically re-submitting payment with different session ID",
+ );
+ logger.trace(`last: ${purchase.lastSessionId}, current: ${sessionId}`);
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["purchases"] },
+ async (tx) => {
+ const p = await tx.purchases.get(proposalId);
+ if (!p) {
+ return;
+ }
+ const oldTxState = computePayMerchantTransactionState(p);
+ p.lastSessionId = sessionId;
+ p.purchaseStatus = PurchaseStatus.PendingPayingReplay;
+ await tx.purchases.put(p);
+ const newTxState = computePayMerchantTransactionState(p);
+ return { oldTxState, newTxState };
+ },
+ );
+ notifyTransition(wex, transactionId, transitionInfo);
+ wex.taskScheduler.startShepherdTask(ctx.taskId);
+
+ // FIXME: Consider changing the API here so that we don't have to
+ // wait inline for the repurchase.
+
+ await waitPaymentResult(wex, proposalId, sessionId);
+ const download = await expectProposalDownload(wex, purchase);
+ return {
+ status: PreparePayResultType.AlreadyConfirmed,
+ contractTerms: download.contractTermsRaw,
+ contractTermsHash: download.contractData.contractTermsHash,
+ paid: true,
+ amountRaw: Amounts.stringify(download.contractData.amount),
+ amountEffective: purchase.payInfo
+ ? Amounts.stringify(purchase.payInfo.totalPayCost)
+ : undefined,
+ transactionId,
+ proposalId,
+ talerUri,
+ };
+ } else if (!purchase.timestampFirstSuccessfulPay) {
+ const download = await expectProposalDownload(wex, purchase);
+ return {
+ status: PreparePayResultType.AlreadyConfirmed,
+ contractTerms: download.contractTermsRaw,
+ contractTermsHash: download.contractData.contractTermsHash,
+ paid: purchase.purchaseStatus === PurchaseStatus.FailedPaidByOther,
+ amountRaw: Amounts.stringify(download.contractData.amount),
+ amountEffective: purchase.payInfo
+ ? Amounts.stringify(purchase.payInfo.totalPayCost)
+ : undefined,
+ transactionId,
+ proposalId,
+ talerUri,
+ };
+ } else {
+ const paid =
+ purchase.purchaseStatus === PurchaseStatus.Done ||
+ purchase.purchaseStatus === PurchaseStatus.PendingQueryingRefund ||
+ purchase.purchaseStatus === PurchaseStatus.PendingQueryingAutoRefund;
+ const download = await expectProposalDownload(wex, purchase);
+ return {
+ status: PreparePayResultType.AlreadyConfirmed,
+ contractTerms: download.contractTermsRaw,
+ contractTermsHash: download.contractData.contractTermsHash,
+ paid,
+ amountRaw: Amounts.stringify(download.contractData.amount),
+ amountEffective: purchase.payInfo
+ ? Amounts.stringify(purchase.payInfo.totalPayCost)
+ : undefined,
+ ...(paid ? { nextUrl: download.contractData.orderId } : {}),
+ transactionId,
+ proposalId,
+ talerUri,
+ };
+ }
+}
+
+export async function getContractTermsDetails(
+ wex: WalletExecutionContext,
+ proposalId: string,
+): Promise<WalletContractData> {
+ const proposal = await wex.db.runReadOnlyTx(
+ { storeNames: ["purchases"] },
+ async (tx) => {
+ return tx.purchases.get(proposalId);
+ },
+ );
+
+ if (!proposal) {
+ throw Error(`proposal with id ${proposalId} not found`);
+ }
+
+ const d = await expectProposalDownload(wex, proposal);
+
+ return d.contractData;
+}
+
+/**
+ * Check if a payment for the given taler://pay/ URI is possible.
+ *
+ * If the payment is possible, the signature are already generated but not
+ * yet send to the merchant.
+ */
+export async function preparePayForUri(
+ wex: WalletExecutionContext,
+ talerPayUri: string,
+): Promise<PreparePayResult> {
+ const uriResult = parsePayUri(talerPayUri);
+
+ if (!uriResult) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_INVALID_TALER_PAY_URI,
+ {
+ talerPayUri,
+ },
+ `invalid taler://pay URI (${talerPayUri})`,
+ );
+ }
+
+ const proposalId = await createOrReusePurchase(
+ wex,
+ uriResult.merchantBaseUrl,
+ uriResult.orderId,
+ uriResult.sessionId,
+ uriResult.claimToken,
+ uriResult.noncePriv,
+ );
+
+ await waitProposalDownloaded(wex, proposalId);
+
+ return checkPaymentByProposalId(wex, proposalId, uriResult.sessionId);
+}
+
+/**
+ * Wait until a proposal is at least downloaded.
+ */
+async function waitProposalDownloaded(
+ wex: WalletExecutionContext,
+ proposalId: string,
+): Promise<void> {
+ // FIXME: This doesn't support cancellation yet
+ const ctx = new PayMerchantTransactionContext(wex, proposalId);
+
+ logger.info(`waiting for ${ctx.transactionId} to be downloaded`);
+
+ wex.taskScheduler.startShepherdTask(ctx.taskId);
+
+ // FIXME: We should use Symbol.dispose magic here for cleanup!
+
+ const payNotifFlag = new AsyncFlag();
+ // Raise exchangeNotifFlag whenever we get a notification
+ // about our exchange.
+ const cancelNotif = wex.ws.addNotificationListener((notif) => {
+ if (
+ notif.type === NotificationType.TransactionStateTransition &&
+ notif.transactionId === ctx.transactionId
+ ) {
+ logger.info(`raising update notification: ${j2s(notif)}`);
+ payNotifFlag.raise();
+ }
+ });
+
+ try {
+ await internalWaitProposalDownloaded(ctx, payNotifFlag);
+ logger.info(`done waiting for ${ctx.transactionId} to be downloaded`);
+ } finally {
+ cancelNotif();
+ }
+}
+
+async function internalWaitProposalDownloaded(
+ ctx: PayMerchantTransactionContext,
+ payNotifFlag: AsyncFlag,
+): Promise<void> {
+ while (true) {
+ const { purchase, retryInfo } = await ctx.wex.db.runReadOnlyTx(
+ { storeNames: ["purchases", "operationRetries"] },
+ async (tx) => {
+ return {
+ purchase: await tx.purchases.get(ctx.proposalId),
+ retryInfo: await tx.operationRetries.get(ctx.taskId),
+ };
+ },
+ );
+ if (!purchase) {
+ throw Error("purchase does not exist anymore");
+ }
+ if (purchase.download) {
+ return;
+ }
+ if (retryInfo) {
+ if (retryInfo.lastError) {
+ throw TalerError.fromUncheckedDetail(retryInfo.lastError);
+ } else {
+ throw Error("transient error while waiting for proposal download");
+ }
+ }
+ await payNotifFlag.wait();
+ payNotifFlag.reset();
+ }
+}
+
+export async function preparePayForTemplate(
+ wex: WalletExecutionContext,
+ req: PreparePayTemplateRequest,
+): Promise<PreparePayResult> {
+ const parsedUri = parsePayTemplateUri(req.talerPayTemplateUri);
+ const templateDetails: MerchantUsingTemplateDetails = {};
+ if (!parsedUri) {
+ throw Error("invalid taler-template URI");
+ }
+ logger.trace(`parsed URI: ${j2s(parsedUri)}`);
+
+ const amountFromUri = parsedUri.templateParams.amount;
+ if (amountFromUri != null) {
+ const templateParamsAmount = req.templateParams?.amount;
+ if (templateParamsAmount != null) {
+ templateDetails.amount = templateParamsAmount as AmountString;
+ } else {
+ if (Amounts.isCurrency(amountFromUri)) {
+ throw Error(
+ "Amount from template URI only has a currency without value. The value must be provided in the templateParams.",
+ );
+ } else {
+ templateDetails.amount = amountFromUri as AmountString;
+ }
+ }
+ }
+ if (
+ parsedUri.templateParams.summary !== undefined &&
+ typeof parsedUri.templateParams.summary === "string"
+ ) {
+ templateDetails.summary =
+ req.templateParams?.summary ?? parsedUri.templateParams.summary;
+ }
+ const reqUrl = new URL(
+ `templates/${parsedUri.templateId}`,
+ parsedUri.merchantBaseUrl,
+ );
+ const httpReq = await wex.http.fetch(reqUrl.href, {
+ method: "POST",
+ body: templateDetails,
+ });
+ const resp = await readSuccessResponseJsonOrThrow(
+ httpReq,
+ codecForMerchantPostOrderResponse(),
+ );
+
+ const payUri = stringifyPayUri({
+ merchantBaseUrl: parsedUri.merchantBaseUrl,
+ orderId: resp.order_id,
+ sessionId: "",
+ claimToken: resp.token,
+ });
+
+ return await preparePayForUri(wex, payUri);
+}
+
+/**
+ * Generate deposit permissions for a purchase.
+ *
+ * Accesses the database and the crypto worker.
+ */
+export async function generateDepositPermissions(
+ wex: WalletExecutionContext,
+ payCoinSel: DbCoinSelection,
+ contractData: WalletContractData,
+): Promise<CoinDepositPermission[]> {
+ const depositPermissions: CoinDepositPermission[] = [];
+ const coinWithDenom: Array<{
+ coin: CoinRecord;
+ denom: DenominationRecord;
+ }> = [];
+ await wex.db.runReadOnlyTx(
+ { storeNames: ["coins", "denominations"] },
+ async (tx) => {
+ for (let i = 0; i < payCoinSel.coinContributions.length; i++) {
+ const coin = await tx.coins.get(payCoinSel.coinPubs[i]);
+ if (!coin) {
+ throw Error("can't pay, allocated coin not found anymore");
+ }
+ const denom = await tx.denominations.get([
+ coin.exchangeBaseUrl,
+ coin.denomPubHash,
+ ]);
+ if (!denom) {
+ throw Error(
+ "can't pay, denomination of allocated coin not found anymore",
+ );
+ }
+ coinWithDenom.push({ coin, denom });
+ }
+ },
+ );
+
+ for (let i = 0; i < payCoinSel.coinContributions.length; i++) {
+ const { coin, denom } = coinWithDenom[i];
+ let wireInfoHash: string;
+ wireInfoHash = contractData.wireInfoHash;
+ const dp = await wex.cryptoApi.signDepositPermission({
+ coinPriv: coin.coinPriv,
+ coinPub: coin.coinPub,
+ contractTermsHash: contractData.contractTermsHash,
+ denomPubHash: coin.denomPubHash,
+ denomKeyType: denom.denomPub.cipher,
+ denomSig: coin.denomSig,
+ exchangeBaseUrl: coin.exchangeBaseUrl,
+ feeDeposit: Amounts.parseOrThrow(denom.fees.feeDeposit),
+ merchantPub: contractData.merchantPub,
+ refundDeadline: contractData.refundDeadline,
+ spendAmount: Amounts.parseOrThrow(payCoinSel.coinContributions[i]),
+ timestamp: contractData.timestamp,
+ wireInfoHash,
+ ageCommitmentProof: coin.ageCommitmentProof,
+ requiredMinimumAge: contractData.minimumAge,
+ });
+ depositPermissions.push(dp);
+ }
+ return depositPermissions;
+}
+
+async function internalWaitPaymentResult(
+ ctx: PayMerchantTransactionContext,
+ purchaseNotifFlag: AsyncFlag,
+ waitSessionId?: string,
+): Promise<ConfirmPayResult> {
+ while (true) {
+ const txRes = await ctx.wex.db.runReadOnlyTx(
+ { storeNames: ["purchases", "operationRetries"] },
+ async (tx) => {
+ const purchase = await tx.purchases.get(ctx.proposalId);
+ const retryRecord = await tx.operationRetries.get(ctx.taskId);
+ return { purchase, retryRecord };
+ },
+ );
+
+ if (!txRes.purchase) {
+ throw Error("purchase gone");
+ }
+
+ const purchase = txRes.purchase;
+
+ logger.info(
+ `purchase is in state ${PurchaseStatus[purchase.purchaseStatus]}`,
+ );
+
+ const d = await expectProposalDownload(ctx.wex, purchase);
+
+ if (txRes.purchase.timestampFirstSuccessfulPay) {
+ if (
+ waitSessionId == null ||
+ txRes.purchase.lastSessionId === waitSessionId
+ ) {
+ return {
+ type: ConfirmPayResultType.Done,
+ contractTerms: d.contractTermsRaw,
+ transactionId: ctx.transactionId,
+ };
+ }
+ }
+
+ if (txRes.retryRecord) {
+ return {
+ type: ConfirmPayResultType.Pending,
+ lastError: txRes.retryRecord.lastError,
+ transactionId: ctx.transactionId,
+ };
+ }
+
+ if (txRes.purchase.purchaseStatus >= PurchaseStatus.Done) {
+ return {
+ type: ConfirmPayResultType.Done,
+ contractTerms: d.contractTermsRaw,
+ transactionId: ctx.transactionId,
+ };
+ }
+
+ await purchaseNotifFlag.wait();
+ purchaseNotifFlag.reset();
+ }
+}
+
+/**
+ * Wait until either:
+ * a) the payment succeeded (if provided under the {@param waitSessionId}), or
+ * b) the attempt to pay failed (merchant unavailable, etc.)
+ */
+async function waitPaymentResult(
+ wex: WalletExecutionContext,
+ proposalId: string,
+ waitSessionId?: string,
+): Promise<ConfirmPayResult> {
+ // FIXME: We don't support cancelletion yet!
+ const ctx = new PayMerchantTransactionContext(wex, proposalId);
+ wex.taskScheduler.startShepherdTask(ctx.taskId);
+
+ // FIXME: Clean up using the new JS "using" / Symbol.dispose syntax.
+ const purchaseNotifFlag = new AsyncFlag();
+ // Raise purchaseNotifFlag whenever we get a notification
+ // about our purchase.
+ const cancelNotif = wex.ws.addNotificationListener((notif) => {
+ if (
+ notif.type === NotificationType.TransactionStateTransition &&
+ notif.transactionId === ctx.transactionId
+ ) {
+ purchaseNotifFlag.raise();
+ }
+ });
+
+ try {
+ logger.info(`waiting for first payment success on ${ctx.transactionId}`);
+ const res = await internalWaitPaymentResult(
+ ctx,
+ purchaseNotifFlag,
+ waitSessionId,
+ );
+ logger.info(
+ `done waiting for first payment success on ${ctx.transactionId}, result ${res.type}`,
+ );
+ return res;
+ } finally {
+ cancelNotif();
+ }
+}
+
+/**
+ * Confirm payment for a proposal previously claimed by the wallet.
+ */
+export async function confirmPay(
+ wex: WalletExecutionContext,
+ transactionId: string,
+ sessionIdOverride?: string,
+ forcedCoinSel?: ForcedCoinSel,
+): Promise<ConfirmPayResult> {
+ const parsedTx = parseTransactionIdentifier(transactionId);
+ if (parsedTx?.tag !== TransactionType.Payment) {
+ throw Error("expected payment transaction ID");
+ }
+ const proposalId = parsedTx.proposalId;
+ logger.trace(
+ `executing confirmPay with proposalId ${proposalId} and sessionIdOverride ${sessionIdOverride}`,
+ );
+ const proposal = await wex.db.runReadOnlyTx(
+ { storeNames: ["purchases"] },
+ async (tx) => {
+ return tx.purchases.get(proposalId);
+ },
+ );
+
+ if (!proposal) {
+ throw Error(`proposal with id ${proposalId} not found`);
+ }
+
+ const d = await expectProposalDownload(wex, proposal);
+ if (!d) {
+ throw Error("proposal is in invalid state");
+ }
+
+ const existingPurchase = await wex.db.runReadWriteTx(
+ { storeNames: ["purchases"] },
+ async (tx) => {
+ const purchase = await tx.purchases.get(proposalId);
+ if (
+ purchase &&
+ sessionIdOverride !== undefined &&
+ sessionIdOverride != purchase.lastSessionId
+ ) {
+ logger.trace(`changing session ID to ${sessionIdOverride}`);
+ purchase.lastSessionId = sessionIdOverride;
+ if (purchase.purchaseStatus === PurchaseStatus.Done) {
+ purchase.purchaseStatus = PurchaseStatus.PendingPayingReplay;
+ }
+ await tx.purchases.put(purchase);
+ }
+ return purchase;
+ },
+ );
+
+ if (existingPurchase && existingPurchase.payInfo) {
+ logger.trace("confirmPay: submitting payment for existing purchase");
+ const ctx = new PayMerchantTransactionContext(
+ wex,
+ existingPurchase.proposalId,
+ );
+ await wex.taskScheduler.resetTaskRetries(ctx.taskId);
+ return waitPaymentResult(wex, proposalId);
+ }
+
+ logger.trace("confirmPay: purchase record does not exist yet");
+
+ const contractData = d.contractData;
+
+ 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;
+ } else {
+ sessionId = proposal.downloadSessionId;
+ }
+
+ logger.trace(
+ `recording payment on ${proposal.orderId} with session ID ${sessionId}`,
+ );
+
+ const transitionInfo = await wex.db.runReadWriteTx(
+ {
+ storeNames: [
+ "purchases",
+ "coins",
+ "refreshGroups",
+ "refreshSessions",
+ "denominations",
+ "coinAvailability",
+ ],
+ },
+ async (tx) => {
+ const p = await tx.purchases.get(proposal.proposalId);
+ if (!p) {
+ return;
+ }
+ const oldTxState = computePayMerchantTransactionState(p);
+ switch (p.purchaseStatus) {
+ case PurchaseStatus.DialogShared:
+ case PurchaseStatus.DialogProposed:
+ p.payInfo = {
+ totalPayCost: Amounts.stringify(payCostInfo),
+ };
+ if (selectCoinsResult.type === "success") {
+ p.payInfo.payCoinSelection = {
+ coinContributions: selectCoinsResult.coinSel.coins.map(
+ (x) => x.contribution,
+ ),
+ coinPubs: selectCoinsResult.coinSel.coins.map((x) => x.coinPub),
+ };
+ p.payInfo.payCoinSelectionUid = encodeCrock(getRandomBytes(16));
+ }
+ p.lastSessionId = sessionId;
+ p.timestampAccept = timestampPreciseToDb(TalerPreciseTimestamp.now());
+ p.purchaseStatus = PurchaseStatus.PendingPaying;
+ await tx.purchases.put(p);
+ if (p.payInfo.payCoinSelection) {
+ const sel = p.payInfo.payCoinSelection;
+ await spendCoins(wex, tx, {
+ //`txn:proposal:${p.proposalId}`
+ allocationId: constructTransactionIdentifier({
+ tag: TransactionType.Payment,
+ proposalId: proposalId,
+ }),
+ coinPubs: sel.coinPubs,
+ contributions: sel.coinContributions.map((x) =>
+ Amounts.parseOrThrow(x),
+ ),
+ refreshReason: RefreshReason.PayMerchant,
+ });
+ }
+
+ break;
+ case PurchaseStatus.Done:
+ case PurchaseStatus.PendingPaying:
+ default:
+ break;
+ }
+ const newTxState = computePayMerchantTransactionState(p);
+ return { oldTxState, newTxState };
+ },
+ );
+
+ notifyTransition(wex, transactionId, transitionInfo);
+ wex.ws.notify({
+ type: NotificationType.BalanceChange,
+ hintTransactionId: transactionId,
+ });
+
+ const ctx = new PayMerchantTransactionContext(wex, proposalId);
+
+ // In case we're sharing the payment and we're long-polling
+ wex.taskScheduler.stopShepherdTask(ctx.taskId);
+
+ // Wait until we have completed the first attempt to pay.
+ return waitPaymentResult(wex, proposalId);
+}
+
+export async function processPurchase(
+ wex: WalletExecutionContext,
+ proposalId: string,
+): Promise<TaskRunResult> {
+ const purchase = await wex.db.runReadOnlyTx(
+ { storeNames: ["purchases"] },
+ async (tx) => {
+ return tx.purchases.get(proposalId);
+ },
+ );
+ if (!purchase) {
+ return {
+ type: TaskRunResultType.Error,
+ errorDetail: {
+ // FIXME: allocate more specific error code
+ code: TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
+ when: AbsoluteTime.now(),
+ hint: `trying to pay for purchase that is not in the database`,
+ proposalId: proposalId,
+ },
+ };
+ }
+
+ switch (purchase.purchaseStatus) {
+ case PurchaseStatus.PendingDownloadingProposal:
+ return processDownloadProposal(wex, proposalId);
+ case PurchaseStatus.PendingPaying:
+ case PurchaseStatus.PendingPayingReplay:
+ return processPurchasePay(wex, proposalId);
+ case PurchaseStatus.PendingQueryingRefund:
+ return processPurchaseQueryRefund(wex, purchase);
+ case PurchaseStatus.PendingQueryingAutoRefund:
+ return processPurchaseAutoRefund(wex, purchase);
+ case PurchaseStatus.AbortingWithRefund:
+ return processPurchaseAbortingRefund(wex, purchase);
+ case PurchaseStatus.PendingAcceptRefund:
+ return processPurchaseAcceptRefund(wex, purchase);
+ case PurchaseStatus.DialogShared:
+ return processPurchaseDialogShared(wex, purchase);
+ case PurchaseStatus.FailedClaim:
+ case PurchaseStatus.Done:
+ case PurchaseStatus.DoneRepurchaseDetected:
+ case PurchaseStatus.DialogProposed:
+ case PurchaseStatus.AbortedProposalRefused:
+ case PurchaseStatus.AbortedIncompletePayment:
+ case PurchaseStatus.AbortedOrderDeleted:
+ case PurchaseStatus.AbortedRefunded:
+ case PurchaseStatus.SuspendedAbortingWithRefund:
+ case PurchaseStatus.SuspendedDownloadingProposal:
+ case PurchaseStatus.SuspendedPaying:
+ case PurchaseStatus.SuspendedPayingReplay:
+ case PurchaseStatus.SuspendedPendingAcceptRefund:
+ case PurchaseStatus.SuspendedQueryingAutoRefund:
+ case PurchaseStatus.SuspendedQueryingRefund:
+ case PurchaseStatus.FailedAbort:
+ case PurchaseStatus.FailedPaidByOther:
+ return TaskRunResult.finished();
+ default:
+ assertUnreachable(purchase.purchaseStatus);
+ }
+}
+
+async function processPurchasePay(
+ wex: WalletExecutionContext,
+ proposalId: string,
+): Promise<TaskRunResult> {
+ const purchase = await wex.db.runReadOnlyTx(
+ { storeNames: ["purchases"] },
+ async (tx) => {
+ return tx.purchases.get(proposalId);
+ },
+ );
+ if (!purchase) {
+ return {
+ type: TaskRunResultType.Error,
+ errorDetail: {
+ // FIXME: allocate more specific error code
+ code: TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
+ when: AbsoluteTime.now(),
+ hint: `trying to pay for purchase that is not in the database`,
+ proposalId: proposalId,
+ },
+ };
+ }
+ switch (purchase.purchaseStatus) {
+ case PurchaseStatus.PendingPaying:
+ case PurchaseStatus.PendingPayingReplay:
+ break;
+ default:
+ return TaskRunResult.finished();
+ }
+ logger.trace(`processing purchase pay ${proposalId}`);
+
+ const ctx = new PayMerchantTransactionContext(wex, proposalId);
+
+ const sessionId = purchase.lastSessionId;
+
+ logger.trace(`paying with session ID ${sessionId}`);
+ const payInfo = purchase.payInfo;
+ checkDbInvariant(!!payInfo, "payInfo");
+
+ const download = await expectProposalDownload(wex, purchase);
+
+ if (purchase.shared) {
+ const paid = await checkIfOrderIsAlreadyPaid(
+ wex,
+ download.contractData,
+ false,
+ );
+
+ if (paid) {
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["purchases"] },
+ async (tx) => {
+ const p = await tx.purchases.get(purchase.proposalId);
+ if (!p) {
+ logger.warn("purchase does not exist anymore");
+ return;
+ }
+ const oldTxState = computePayMerchantTransactionState(p);
+ p.purchaseStatus = PurchaseStatus.FailedPaidByOther;
+ const newTxState = computePayMerchantTransactionState(p);
+ await tx.purchases.put(p);
+ return { oldTxState, newTxState };
+ },
+ );
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Payment,
+ proposalId,
+ });
+
+ notifyTransition(wex, transactionId, transitionInfo);
+
+ return {
+ type: TaskRunResultType.Error,
+ errorDetail: makeErrorDetail(TalerErrorCode.WALLET_ORDER_ALREADY_PAID, {
+ orderId: purchase.orderId,
+ fulfillmentUrl: download.contractData.fulfillmentUrl,
+ }),
+ };
+ }
+ }
+
+ const contractData = download.contractData;
+ const currency = Amounts.currencyOf(download.contractData.amount);
+
+ if (!payInfo.payCoinSelection) {
+ 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,
+ });
+ 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": {
+ throw Error("insufficient balance (pending refresh)");
+ }
+ case "success":
+ break;
+ default:
+ assertUnreachable(selectCoinsResult);
+ }
+
+ logger.trace("coin selection result", selectCoinsResult);
+
+ const payCostInfo = await getTotalPaymentCost(
+ wex,
+ currency,
+ selectCoinsResult.coinSel.coins,
+ );
+
+ const transitionDone = await wex.db.runReadWriteTx(
+ {
+ storeNames: [
+ "purchases",
+ "coins",
+ "refreshGroups",
+ "refreshSessions",
+ "denominations",
+ "coinAvailability",
+ ],
+ },
+ async (tx) => {
+ const p = await tx.purchases.get(proposalId);
+ if (!p) {
+ return false;
+ }
+ if (p.payInfo?.payCoinSelection) {
+ return false;
+ }
+ switch (p.purchaseStatus) {
+ case PurchaseStatus.DialogShared:
+ case PurchaseStatus.DialogProposed:
+ p.payInfo = {
+ totalPayCost: Amounts.stringify(payCostInfo),
+ payCoinSelection: {
+ coinContributions: selectCoinsResult.coinSel.coins.map(
+ (x) => x.contribution,
+ ),
+ coinPubs: selectCoinsResult.coinSel.coins.map((x) => x.coinPub),
+ },
+ };
+ p.payInfo.payCoinSelectionUid = encodeCrock(getRandomBytes(16));
+ p.purchaseStatus = PurchaseStatus.PendingPaying;
+ await tx.purchases.put(p);
+
+ await spendCoins(wex, tx, {
+ //`txn:proposal:${p.proposalId}`
+ allocationId: constructTransactionIdentifier({
+ tag: TransactionType.Payment,
+ proposalId: proposalId,
+ }),
+ coinPubs: selectCoinsResult.coinSel.coins.map((x) => x.coinPub),
+ contributions: selectCoinsResult.coinSel.coins.map((x) =>
+ Amounts.parseOrThrow(x.contribution),
+ ),
+ refreshReason: RefreshReason.PayMerchant,
+ });
+ return true;
+ case PurchaseStatus.Done:
+ case PurchaseStatus.PendingPaying:
+ default:
+ break;
+ }
+ return false;
+ },
+ );
+
+ if (transitionDone) {
+ return TaskRunResult.progress();
+ } else {
+ return TaskRunResult.backoff();
+ }
+ }
+
+ if (!purchase.merchantPaySig) {
+ const payUrl = new URL(
+ `orders/${download.contractData.orderId}/pay`,
+ download.contractData.merchantBaseUrl,
+ ).href;
+
+ let depositPermissions: CoinDepositPermission[];
+ // FIXME: Cache!
+ depositPermissions = await generateDepositPermissions(
+ wex,
+ payInfo.payCoinSelection,
+ download.contractData,
+ );
+
+ const reqBody = {
+ coins: depositPermissions,
+ session_id: purchase.lastSessionId,
+ };
+
+ if (logger.shouldLogTrace()) {
+ logger.trace(`making pay request ... ${j2s(reqBody)}`);
+ }
+
+ const resp = await wex.ws.runSequentialized([EXCHANGE_COINS_LOCK], () =>
+ wex.http.fetch(payUrl, {
+ method: "POST",
+ body: reqBody,
+ timeout: getPayRequestTimeout(purchase),
+ cancellationToken: wex.cancellationToken,
+ }),
+ );
+
+ logger.trace(`got resp ${JSON.stringify(resp)}`);
+
+ if (resp.status >= 500 && resp.status <= 599) {
+ const errDetails = await readUnexpectedResponseDetails(resp);
+ return {
+ type: TaskRunResultType.Error,
+ errorDetail: makeErrorDetail(
+ TalerErrorCode.WALLET_PAY_MERCHANT_SERVER_ERROR,
+ {
+ requestError: errDetails,
+ },
+ ),
+ };
+ }
+
+ if (resp.status === HttpStatusCode.Conflict) {
+ const err = await readTalerErrorResponse(resp);
+ if (
+ err.code ===
+ TalerErrorCode.MERCHANT_POST_ORDERS_ID_PAY_INSUFFICIENT_FUNDS
+ ) {
+ // Do this in the background, as it might take some time
+ // FIXME: Why? We're already in a (background) task!
+ handleInsufficientFunds(wex, proposalId, err).catch(async (e) => {
+ logger.error("handling insufficient funds failed");
+ logger.error(`${e.toString()}`);
+ });
+
+ // FIXME: Should we really consider this to be pending?
+
+ return TaskRunResult.backoff();
+ }
+ }
+
+ if (resp.status >= 400 && resp.status <= 499) {
+ logger.trace("got generic 4xx from merchant");
+ const err = await readTalerErrorResponse(resp);
+ if (logger.shouldLogTrace()) {
+ logger.trace(`error body: ${j2s(err)}`);
+ }
+ throwUnexpectedRequestError(resp, err);
+ }
+
+ const merchantResp = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForMerchantPayResponse(),
+ );
+
+ logger.trace("got success from pay URL", merchantResp);
+
+ const merchantPub = download.contractData.merchantPub;
+ const { valid } = await wex.cryptoApi.isValidPaymentSignature({
+ contractHash: download.contractData.contractTermsHash,
+ merchantPub,
+ sig: merchantResp.sig,
+ });
+
+ if (!valid) {
+ logger.error("merchant payment signature invalid");
+ // FIXME: properly display error
+ throw Error("merchant payment signature invalid");
+ }
+
+ await storeFirstPaySuccess(wex, proposalId, sessionId, merchantResp);
+ } else {
+ const payAgainUrl = new URL(
+ `orders/${download.contractData.orderId}/paid`,
+ download.contractData.merchantBaseUrl,
+ ).href;
+ const reqBody = {
+ sig: purchase.merchantPaySig,
+ h_contract: download.contractData.contractTermsHash,
+ session_id: sessionId ?? "",
+ };
+ logger.trace(`/paid request body: ${j2s(reqBody)}`);
+ const resp = await wex.ws.runSequentialized([EXCHANGE_COINS_LOCK], () =>
+ wex.http.fetch(payAgainUrl, {
+ method: "POST",
+ body: reqBody,
+ cancellationToken: wex.cancellationToken,
+ }),
+ );
+ logger.trace(`/paid response status: ${resp.status}`);
+ if (
+ resp.status !== HttpStatusCode.NoContent &&
+ resp.status != HttpStatusCode.Ok
+ ) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
+ getHttpResponseErrorDetails(resp),
+ "/paid failed",
+ );
+ }
+ await storePayReplaySuccess(wex, proposalId, sessionId);
+ }
+
+ return TaskRunResult.progress();
+}
+
+export async function refuseProposal(
+ wex: WalletExecutionContext,
+ proposalId: string,
+): Promise<void> {
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Payment,
+ proposalId,
+ });
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["purchases"] },
+ async (tx) => {
+ const proposal = await tx.purchases.get(proposalId);
+ if (!proposal) {
+ logger.trace(`proposal ${proposalId} not found, won't refuse proposal`);
+ return undefined;
+ }
+ if (
+ proposal.purchaseStatus !== PurchaseStatus.DialogProposed &&
+ proposal.purchaseStatus !== PurchaseStatus.DialogShared
+ ) {
+ return undefined;
+ }
+ const oldTxState = computePayMerchantTransactionState(proposal);
+ proposal.purchaseStatus = PurchaseStatus.AbortedProposalRefused;
+ const newTxState = computePayMerchantTransactionState(proposal);
+ await tx.purchases.put(proposal);
+ return { oldTxState, newTxState };
+ },
+ );
+
+ notifyTransition(wex, transactionId, transitionInfo);
+}
+
+const transitionSuspend: {
+ [x in PurchaseStatus]?: {
+ next: PurchaseStatus | undefined;
+ };
+} = {
+ [PurchaseStatus.PendingDownloadingProposal]: {
+ next: PurchaseStatus.SuspendedDownloadingProposal,
+ },
+ [PurchaseStatus.AbortingWithRefund]: {
+ next: PurchaseStatus.SuspendedAbortingWithRefund,
+ },
+ [PurchaseStatus.PendingPaying]: {
+ next: PurchaseStatus.SuspendedPaying,
+ },
+ [PurchaseStatus.PendingPayingReplay]: {
+ next: PurchaseStatus.SuspendedPayingReplay,
+ },
+ [PurchaseStatus.PendingQueryingAutoRefund]: {
+ next: PurchaseStatus.SuspendedQueryingAutoRefund,
+ },
+};
+
+const transitionResume: {
+ [x in PurchaseStatus]?: {
+ next: PurchaseStatus | undefined;
+ };
+} = {
+ [PurchaseStatus.SuspendedDownloadingProposal]: {
+ next: PurchaseStatus.PendingDownloadingProposal,
+ },
+ [PurchaseStatus.SuspendedAbortingWithRefund]: {
+ next: PurchaseStatus.AbortingWithRefund,
+ },
+ [PurchaseStatus.SuspendedPaying]: {
+ next: PurchaseStatus.PendingPaying,
+ },
+ [PurchaseStatus.SuspendedPayingReplay]: {
+ next: PurchaseStatus.PendingPayingReplay,
+ },
+ [PurchaseStatus.SuspendedQueryingAutoRefund]: {
+ next: PurchaseStatus.PendingQueryingAutoRefund,
+ },
+};
+
+export function computePayMerchantTransactionState(
+ purchaseRecord: PurchaseRecord,
+): TransactionState {
+ switch (purchaseRecord.purchaseStatus) {
+ // Pending States
+ case PurchaseStatus.PendingDownloadingProposal:
+ return {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.ClaimProposal,
+ };
+ case PurchaseStatus.PendingPaying:
+ return {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.SubmitPayment,
+ };
+ case PurchaseStatus.PendingPayingReplay:
+ return {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.RebindSession,
+ };
+ case PurchaseStatus.PendingQueryingAutoRefund:
+ return {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.AutoRefund,
+ };
+ case PurchaseStatus.PendingQueryingRefund:
+ return {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.CheckRefund,
+ };
+ case PurchaseStatus.PendingAcceptRefund:
+ return {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.AcceptRefund,
+ };
+ // Suspended Pending States
+ case PurchaseStatus.SuspendedDownloadingProposal:
+ return {
+ major: TransactionMajorState.Suspended,
+ minor: TransactionMinorState.ClaimProposal,
+ };
+ case PurchaseStatus.SuspendedPaying:
+ return {
+ major: TransactionMajorState.Suspended,
+ minor: TransactionMinorState.SubmitPayment,
+ };
+ case PurchaseStatus.SuspendedPayingReplay:
+ return {
+ major: TransactionMajorState.Suspended,
+ minor: TransactionMinorState.RebindSession,
+ };
+ case PurchaseStatus.SuspendedQueryingAutoRefund:
+ return {
+ major: TransactionMajorState.Suspended,
+ minor: TransactionMinorState.AutoRefund,
+ };
+ case PurchaseStatus.SuspendedQueryingRefund:
+ return {
+ major: TransactionMajorState.Suspended,
+ minor: TransactionMinorState.CheckRefund,
+ };
+ case PurchaseStatus.SuspendedPendingAcceptRefund:
+ return {
+ major: TransactionMajorState.Suspended,
+ minor: TransactionMinorState.AcceptRefund,
+ };
+ // Aborting States
+ case PurchaseStatus.AbortingWithRefund:
+ return {
+ major: TransactionMajorState.Aborting,
+ };
+ // Suspended Aborting States
+ case PurchaseStatus.SuspendedAbortingWithRefund:
+ return {
+ major: TransactionMajorState.SuspendedAborting,
+ };
+ // Dialog States
+ case PurchaseStatus.DialogProposed:
+ return {
+ major: TransactionMajorState.Dialog,
+ minor: TransactionMinorState.MerchantOrderProposed,
+ };
+ case PurchaseStatus.DialogShared:
+ return {
+ major: TransactionMajorState.Dialog,
+ minor: TransactionMinorState.MerchantOrderProposed,
+ };
+ // Final States
+ case PurchaseStatus.AbortedProposalRefused:
+ return {
+ major: TransactionMajorState.Failed,
+ minor: TransactionMinorState.Refused,
+ };
+ case PurchaseStatus.AbortedOrderDeleted:
+ case PurchaseStatus.AbortedRefunded:
+ return {
+ major: TransactionMajorState.Aborted,
+ };
+ case PurchaseStatus.Done:
+ return {
+ major: TransactionMajorState.Done,
+ };
+ case PurchaseStatus.DoneRepurchaseDetected:
+ return {
+ major: TransactionMajorState.Failed,
+ minor: TransactionMinorState.Repurchase,
+ };
+ case PurchaseStatus.AbortedIncompletePayment:
+ return {
+ major: TransactionMajorState.Aborted,
+ };
+ case PurchaseStatus.FailedClaim:
+ return {
+ major: TransactionMajorState.Failed,
+ minor: TransactionMinorState.ClaimProposal,
+ };
+ case PurchaseStatus.FailedAbort:
+ return {
+ major: TransactionMajorState.Failed,
+ minor: TransactionMinorState.AbortingBank,
+ };
+ case PurchaseStatus.FailedPaidByOther:
+ return {
+ major: TransactionMajorState.Failed,
+ minor: TransactionMinorState.PaidByOther,
+ };
+ default:
+ assertUnreachable(purchaseRecord.purchaseStatus);
+ }
+}
+
+export function computePayMerchantTransactionActions(
+ purchaseRecord: PurchaseRecord,
+): TransactionAction[] {
+ switch (purchaseRecord.purchaseStatus) {
+ // Pending States
+ case PurchaseStatus.PendingDownloadingProposal:
+ return [TransactionAction.Suspend, TransactionAction.Abort];
+ case PurchaseStatus.PendingPaying:
+ return [TransactionAction.Suspend, TransactionAction.Abort];
+ case PurchaseStatus.PendingPayingReplay:
+ // Special "abort" since it goes back to "done".
+ return [TransactionAction.Suspend, TransactionAction.Abort];
+ case PurchaseStatus.PendingQueryingAutoRefund:
+ // Special "abort" since it goes back to "done".
+ return [TransactionAction.Suspend, TransactionAction.Abort];
+ case PurchaseStatus.PendingQueryingRefund:
+ // Special "abort" since it goes back to "done".
+ return [TransactionAction.Suspend, TransactionAction.Abort];
+ case PurchaseStatus.PendingAcceptRefund:
+ // Special "abort" since it goes back to "done".
+ return [TransactionAction.Suspend, TransactionAction.Abort];
+ // Suspended Pending States
+ case PurchaseStatus.SuspendedDownloadingProposal:
+ return [TransactionAction.Resume, TransactionAction.Abort];
+ case PurchaseStatus.SuspendedPaying:
+ return [TransactionAction.Resume, TransactionAction.Abort];
+ case PurchaseStatus.SuspendedPayingReplay:
+ // Special "abort" since it goes back to "done".
+ return [TransactionAction.Resume, TransactionAction.Abort];
+ case PurchaseStatus.SuspendedQueryingAutoRefund:
+ // Special "abort" since it goes back to "done".
+ return [TransactionAction.Resume, TransactionAction.Abort];
+ case PurchaseStatus.SuspendedQueryingRefund:
+ // Special "abort" since it goes back to "done".
+ return [TransactionAction.Resume, TransactionAction.Abort];
+ case PurchaseStatus.SuspendedPendingAcceptRefund:
+ // Special "abort" since it goes back to "done".
+ return [TransactionAction.Resume, TransactionAction.Abort];
+ // Aborting States
+ case PurchaseStatus.AbortingWithRefund:
+ return [TransactionAction.Fail, TransactionAction.Suspend];
+ case PurchaseStatus.SuspendedAbortingWithRefund:
+ return [TransactionAction.Fail, TransactionAction.Resume];
+ // Dialog States
+ case PurchaseStatus.DialogProposed:
+ return [];
+ case PurchaseStatus.DialogShared:
+ return [];
+ // Final States
+ case PurchaseStatus.AbortedProposalRefused:
+ case PurchaseStatus.AbortedOrderDeleted:
+ case PurchaseStatus.AbortedRefunded:
+ return [TransactionAction.Delete];
+ case PurchaseStatus.Done:
+ return [TransactionAction.Delete];
+ case PurchaseStatus.DoneRepurchaseDetected:
+ return [TransactionAction.Delete];
+ case PurchaseStatus.AbortedIncompletePayment:
+ return [TransactionAction.Delete];
+ case PurchaseStatus.FailedClaim:
+ return [TransactionAction.Delete];
+ case PurchaseStatus.FailedAbort:
+ return [TransactionAction.Delete];
+ case PurchaseStatus.FailedPaidByOther:
+ return [TransactionAction.Delete];
+ default:
+ assertUnreachable(purchaseRecord.purchaseStatus);
+ }
+}
+
+export async function sharePayment(
+ wex: WalletExecutionContext,
+ merchantBaseUrl: string,
+ orderId: string,
+): Promise<SharePaymentResult> {
+ const result = await wex.db.runReadWriteTx(
+ { storeNames: ["purchases"] },
+ async (tx) => {
+ const p = await tx.purchases.indexes.byUrlAndOrderId.get([
+ merchantBaseUrl,
+ orderId,
+ ]);
+ if (!p) {
+ logger.warn("purchase does not exist anymore");
+ return undefined;
+ }
+ if (
+ p.purchaseStatus !== PurchaseStatus.DialogProposed &&
+ p.purchaseStatus !== PurchaseStatus.DialogShared
+ ) {
+ // FIXME: purchase can be shared before being paid
+ return undefined;
+ }
+ const oldTxState = computePayMerchantTransactionState(p);
+ if (p.purchaseStatus === PurchaseStatus.DialogProposed) {
+ p.purchaseStatus = PurchaseStatus.DialogShared;
+ p.shared = true;
+ await tx.purchases.put(p);
+ }
+
+ const newTxState = computePayMerchantTransactionState(p);
+
+ return {
+ proposalId: p.proposalId,
+ nonce: p.noncePriv,
+ session: p.lastSessionId ?? p.downloadSessionId,
+ token: p.claimToken,
+ transitionInfo: {
+ oldTxState,
+ newTxState,
+ },
+ };
+ },
+ );
+
+ if (result === undefined) {
+ throw Error("This purchase can't be shared");
+ }
+
+ const ctx = new PayMerchantTransactionContext(wex, result.proposalId);
+
+ notifyTransition(wex, ctx.transactionId, result.transitionInfo);
+
+ // schedule a task to watch for the status
+ wex.taskScheduler.startShepherdTask(ctx.taskId);
+
+ const privatePayUri = stringifyPayUri({
+ merchantBaseUrl,
+ orderId,
+ sessionId: result.session ?? "",
+ noncePriv: result.nonce,
+ claimToken: result.token,
+ });
+
+ return { privatePayUri };
+}
+
+async function checkIfOrderIsAlreadyPaid(
+ wex: WalletExecutionContext,
+ contract: WalletContractData,
+ doLongPolling: boolean,
+) {
+ const requestUrl = new URL(
+ `orders/${contract.orderId}`,
+ contract.merchantBaseUrl,
+ );
+ requestUrl.searchParams.set("h_contract", contract.contractTermsHash);
+
+ if (doLongPolling) {
+ requestUrl.searchParams.set("timeout_ms", "30000");
+ }
+
+ const resp = await wex.http.fetch(requestUrl.href, {
+ cancellationToken: wex.cancellationToken,
+ });
+
+ if (
+ resp.status === HttpStatusCode.Ok ||
+ resp.status === HttpStatusCode.Accepted ||
+ resp.status === HttpStatusCode.Found
+ ) {
+ return true;
+ } else if (resp.status === HttpStatusCode.PaymentRequired) {
+ return false;
+ }
+ // forbidden, not found, not acceptable
+ throw Error(`this order cant be paid: ${resp.status}`);
+}
+
+async function processPurchaseDialogShared(
+ wex: WalletExecutionContext,
+ purchase: PurchaseRecord,
+): Promise<TaskRunResult> {
+ const proposalId = purchase.proposalId;
+ logger.trace(`processing dialog-shared for proposal ${proposalId}`);
+ const download = await expectProposalDownload(wex, purchase);
+
+ if (purchase.purchaseStatus !== PurchaseStatus.DialogShared) {
+ return TaskRunResult.finished();
+ }
+
+ const paid = await checkIfOrderIsAlreadyPaid(
+ wex,
+ download.contractData,
+ true,
+ );
+ if (paid) {
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["purchases"] },
+ async (tx) => {
+ const p = await tx.purchases.get(purchase.proposalId);
+ if (!p) {
+ logger.warn("purchase does not exist anymore");
+ return;
+ }
+ const oldTxState = computePayMerchantTransactionState(p);
+ p.purchaseStatus = PurchaseStatus.FailedPaidByOther;
+ const newTxState = computePayMerchantTransactionState(p);
+ await tx.purchases.put(p);
+ return { oldTxState, newTxState };
+ },
+ );
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Payment,
+ proposalId,
+ });
+
+ notifyTransition(wex, transactionId, transitionInfo);
+ }
+
+ return TaskRunResult.backoff();
+}
+
+async function processPurchaseAutoRefund(
+ wex: WalletExecutionContext,
+ purchase: PurchaseRecord,
+): Promise<TaskRunResult> {
+ const proposalId = purchase.proposalId;
+ logger.trace(`processing auto-refund for proposal ${proposalId}`);
+
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Payment,
+ proposalId,
+ });
+
+ const download = await expectProposalDownload(wex, purchase);
+
+ const noAutoRefundOrExpired =
+ !purchase.autoRefundDeadline ||
+ AbsoluteTime.isExpired(
+ AbsoluteTime.fromProtocolTimestamp(
+ timestampProtocolFromDb(purchase.autoRefundDeadline),
+ ),
+ );
+
+ const totalKnownRefund = await wex.db.runReadOnlyTx(
+ { storeNames: ["refundGroups"] },
+ async (tx) => {
+ const refunds = await tx.refundGroups.indexes.byProposalId.getAll(
+ purchase.proposalId,
+ );
+ const am = Amounts.parseOrThrow(download.contractData.amount);
+ return refunds.reduce((prev, cur) => {
+ if (
+ cur.status === RefundGroupStatus.Done ||
+ cur.status === RefundGroupStatus.Pending
+ ) {
+ return Amounts.add(prev, cur.amountEffective).amount;
+ }
+ return prev;
+ }, Amounts.zeroOfAmount(am));
+ },
+ );
+
+ const refundedIsLessThanPrice =
+ Amounts.cmp(download.contractData.amount, totalKnownRefund) === +1;
+ const nothingMoreToRefund = !refundedIsLessThanPrice;
+
+ if (noAutoRefundOrExpired || nothingMoreToRefund) {
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["purchases"] },
+ async (tx) => {
+ const p = await tx.purchases.get(purchase.proposalId);
+ if (!p) {
+ logger.warn("purchase does not exist anymore");
+ return;
+ }
+ if (p.purchaseStatus !== PurchaseStatus.PendingQueryingAutoRefund) {
+ return;
+ }
+ const oldTxState = computePayMerchantTransactionState(p);
+ p.purchaseStatus = PurchaseStatus.Done;
+ p.refundAmountAwaiting = undefined;
+ const newTxState = computePayMerchantTransactionState(p);
+ await tx.purchases.put(p);
+ return { oldTxState, newTxState };
+ },
+ );
+ notifyTransition(wex, transactionId, transitionInfo);
+ return TaskRunResult.finished();
+ }
+
+ const requestUrl = new URL(
+ `orders/${download.contractData.orderId}`,
+ download.contractData.merchantBaseUrl,
+ );
+ requestUrl.searchParams.set(
+ "h_contract",
+ download.contractData.contractTermsHash,
+ );
+
+ requestUrl.searchParams.set("timeout_ms", "10000");
+ requestUrl.searchParams.set("await_refund_obtained", "yes");
+ requestUrl.searchParams.set("refund", Amounts.stringify(totalKnownRefund));
+
+ const resp = await wex.http.fetch(requestUrl.href, {
+ cancellationToken: wex.cancellationToken,
+ });
+
+ // FIXME: Check other status codes!
+
+ const orderStatus = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForMerchantOrderStatusPaid(),
+ );
+
+ if (orderStatus.refund_pending) {
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["purchases"] },
+ async (tx) => {
+ const p = await tx.purchases.get(purchase.proposalId);
+ if (!p) {
+ logger.warn("purchase does not exist anymore");
+ return;
+ }
+ if (p.purchaseStatus !== PurchaseStatus.PendingQueryingAutoRefund) {
+ return;
+ }
+ const oldTxState = computePayMerchantTransactionState(p);
+ p.purchaseStatus = PurchaseStatus.PendingAcceptRefund;
+ const newTxState = computePayMerchantTransactionState(p);
+ await tx.purchases.put(p);
+ return { oldTxState, newTxState };
+ },
+ );
+ notifyTransition(wex, transactionId, transitionInfo);
+ return TaskRunResult.progress();
+ }
+
+ return TaskRunResult.longpollReturnedPending();
+}
+
+async function processPurchaseAbortingRefund(
+ wex: WalletExecutionContext,
+ purchase: PurchaseRecord,
+): Promise<TaskRunResult> {
+ const proposalId = purchase.proposalId;
+ const download = await expectProposalDownload(wex, purchase);
+ logger.trace(`processing aborting-refund for proposal ${proposalId}`);
+
+ const requestUrl = new URL(
+ `orders/${download.contractData.orderId}/abort`,
+ download.contractData.merchantBaseUrl,
+ );
+
+ const abortingCoins: AbortingCoin[] = [];
+
+ const payCoinSelection = purchase.payInfo?.payCoinSelection;
+ if (!payCoinSelection) {
+ throw Error("can't abort, no coins selected");
+ }
+
+ await wex.db.runReadOnlyTx({ storeNames: ["coins"] }, async (tx) => {
+ for (let i = 0; i < payCoinSelection.coinPubs.length; i++) {
+ const coinPub = payCoinSelection.coinPubs[i];
+ const coin = await tx.coins.get(coinPub);
+ checkDbInvariant(!!coin, "expected coin to be present");
+ abortingCoins.push({
+ coin_pub: coinPub,
+ contribution: Amounts.stringify(payCoinSelection.coinContributions[i]),
+ exchange_url: coin.exchangeBaseUrl,
+ });
+ }
+ });
+
+ const abortReq: AbortRequest = {
+ h_contract: download.contractData.contractTermsHash,
+ coins: abortingCoins,
+ };
+
+ logger.trace(`making order abort request to ${requestUrl.href}`);
+
+ const abortHttpResp = await wex.http.fetch(requestUrl.href, {
+ method: "POST",
+ body: abortReq,
+ cancellationToken: wex.cancellationToken,
+ });
+
+ if (abortHttpResp.status === HttpStatusCode.NotFound) {
+ const err = await readTalerErrorResponse(abortHttpResp);
+ if (
+ err.code ===
+ TalerErrorCode.MERCHANT_POST_ORDERS_ID_ABORT_CONTRACT_NOT_FOUND
+ ) {
+ const ctx = new PayMerchantTransactionContext(wex, proposalId);
+ await ctx.transition(async (rec) => {
+ if (rec.purchaseStatus === PurchaseStatus.AbortingWithRefund) {
+ rec.purchaseStatus = PurchaseStatus.AbortedOrderDeleted;
+ return TransitionResultType.Transition;
+ }
+ return TransitionResultType.Stay;
+ });
+ }
+ }
+
+ const abortResp = await readSuccessResponseJsonOrThrow(
+ abortHttpResp,
+ codecForAbortResponse(),
+ );
+
+ const refunds: MerchantCoinRefundStatus[] = [];
+
+ if (abortResp.refunds.length != abortingCoins.length) {
+ // FIXME: define error code!
+ throw Error("invalid order abort response");
+ }
+
+ for (let i = 0; i < abortResp.refunds.length; i++) {
+ const r = abortResp.refunds[i];
+ refunds.push({
+ ...r,
+ coin_pub: payCoinSelection.coinPubs[i],
+ refund_amount: Amounts.stringify(payCoinSelection.coinContributions[i]),
+ rtransaction_id: 0,
+ execution_time: AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.addDuration(
+ AbsoluteTime.fromProtocolTimestamp(download.contractData.timestamp),
+ Duration.fromSpec({ seconds: 1 }),
+ ),
+ ),
+ });
+ }
+ return await storeRefunds(wex, purchase, refunds, RefundReason.AbortRefund);
+}
+
+async function processPurchaseQueryRefund(
+ wex: WalletExecutionContext,
+ purchase: PurchaseRecord,
+): Promise<TaskRunResult> {
+ const proposalId = purchase.proposalId;
+ logger.trace(`processing query-refund for proposal ${proposalId}`);
+
+ const download = await expectProposalDownload(wex, purchase);
+
+ const requestUrl = new URL(
+ `orders/${download.contractData.orderId}`,
+ download.contractData.merchantBaseUrl,
+ );
+ requestUrl.searchParams.set(
+ "h_contract",
+ download.contractData.contractTermsHash,
+ );
+
+ const resp = await wex.http.fetch(requestUrl.href, {
+ cancellationToken: wex.cancellationToken,
+ });
+ const orderStatus = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForMerchantOrderStatusPaid(),
+ );
+
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Payment,
+ proposalId,
+ });
+
+ if (!orderStatus.refund_pending) {
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["purchases"] },
+ async (tx) => {
+ const p = await tx.purchases.get(purchase.proposalId);
+ if (!p) {
+ logger.warn("purchase does not exist anymore");
+ return undefined;
+ }
+ if (p.purchaseStatus !== PurchaseStatus.PendingQueryingRefund) {
+ return undefined;
+ }
+ const oldTxState = computePayMerchantTransactionState(p);
+ p.purchaseStatus = PurchaseStatus.Done;
+ p.refundAmountAwaiting = undefined;
+ const newTxState = computePayMerchantTransactionState(p);
+ await tx.purchases.put(p);
+ return { oldTxState, newTxState };
+ },
+ );
+ notifyTransition(wex, transactionId, transitionInfo);
+ return TaskRunResult.progress();
+ } else {
+ const refundAwaiting = Amounts.sub(
+ Amounts.parseOrThrow(orderStatus.refund_amount),
+ Amounts.parseOrThrow(orderStatus.refund_taken),
+ ).amount;
+
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["purchases"] },
+ async (tx) => {
+ const p = await tx.purchases.get(purchase.proposalId);
+ if (!p) {
+ logger.warn("purchase does not exist anymore");
+ return;
+ }
+ if (p.purchaseStatus !== PurchaseStatus.PendingQueryingRefund) {
+ return;
+ }
+ const oldTxState = computePayMerchantTransactionState(p);
+ p.refundAmountAwaiting = Amounts.stringify(refundAwaiting);
+ p.purchaseStatus = PurchaseStatus.PendingAcceptRefund;
+ const newTxState = computePayMerchantTransactionState(p);
+ await tx.purchases.put(p);
+ return { oldTxState, newTxState };
+ },
+ );
+ notifyTransition(wex, transactionId, transitionInfo);
+ return TaskRunResult.progress();
+ }
+}
+
+async function processPurchaseAcceptRefund(
+ wex: WalletExecutionContext,
+ purchase: PurchaseRecord,
+): Promise<TaskRunResult> {
+ const download = await expectProposalDownload(wex, purchase);
+
+ const requestUrl = new URL(
+ `orders/${download.contractData.orderId}/refund`,
+ download.contractData.merchantBaseUrl,
+ );
+
+ logger.trace(`making refund request to ${requestUrl.href}`);
+
+ const request = await wex.http.fetch(requestUrl.href, {
+ method: "POST",
+ body: {
+ h_contract: download.contractData.contractTermsHash,
+ },
+ cancellationToken: wex.cancellationToken,
+ });
+
+ const refundResponse = await readSuccessResponseJsonOrThrow(
+ request,
+ codecForWalletRefundResponse(),
+ );
+ return await storeRefunds(
+ wex,
+ purchase,
+ refundResponse.refunds,
+ RefundReason.AbortRefund,
+ );
+}
+
+export async function startRefundQueryForUri(
+ wex: WalletExecutionContext,
+ talerUri: string,
+): Promise<StartRefundQueryForUriResponse> {
+ const parsedUri = parseTalerUri(talerUri);
+ if (!parsedUri) {
+ throw Error("invalid taler:// URI");
+ }
+ if (parsedUri.type !== TalerUriAction.Refund) {
+ throw Error("expected taler://refund URI");
+ }
+ const purchaseRecord = await wex.db.runReadOnlyTx(
+ { storeNames: ["purchases"] },
+ async (tx) => {
+ return tx.purchases.indexes.byUrlAndOrderId.get([
+ parsedUri.merchantBaseUrl,
+ parsedUri.orderId,
+ ]);
+ },
+ );
+ if (!purchaseRecord) {
+ logger.error(
+ `no purchase for order ID "${parsedUri.orderId}" from merchant "${parsedUri.merchantBaseUrl}" when processing "${talerUri}"`,
+ );
+ throw Error("no purchase found, can't refund");
+ }
+ const proposalId = purchaseRecord.proposalId;
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Payment,
+ proposalId,
+ });
+ await startQueryRefund(wex, proposalId);
+ return {
+ transactionId,
+ };
+}
+
+export async function startQueryRefund(
+ wex: WalletExecutionContext,
+ proposalId: string,
+): Promise<void> {
+ const ctx = new PayMerchantTransactionContext(wex, proposalId);
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["purchases"] },
+ async (tx) => {
+ const p = await tx.purchases.get(proposalId);
+ if (!p) {
+ logger.warn(`purchase ${proposalId} does not exist anymore`);
+ return;
+ }
+ if (p.purchaseStatus !== PurchaseStatus.Done) {
+ return;
+ }
+ const oldTxState = computePayMerchantTransactionState(p);
+ p.purchaseStatus = PurchaseStatus.PendingQueryingRefund;
+ const newTxState = computePayMerchantTransactionState(p);
+ await tx.purchases.put(p);
+ return { oldTxState, newTxState };
+ },
+ );
+ notifyTransition(wex, ctx.transactionId, transitionInfo);
+ wex.taskScheduler.startShepherdTask(ctx.taskId);
+}
+
+async function computeRefreshRequest(
+ wex: WalletExecutionContext,
+ tx: WalletDbReadWriteTransaction<["coins", "denominations"]>,
+ items: RefundItemRecord[],
+): Promise<CoinRefreshRequest[]> {
+ const refreshCoins: CoinRefreshRequest[] = [];
+ for (const item of items) {
+ const coin = await tx.coins.get(item.coinPub);
+ if (!coin) {
+ throw Error("coin not found");
+ }
+ const denomInfo = await getDenomInfo(
+ wex,
+ tx,
+ coin.exchangeBaseUrl,
+ coin.denomPubHash,
+ );
+ if (!denomInfo) {
+ throw Error("denom not found");
+ }
+ if (item.status === RefundItemStatus.Done) {
+ const refundedAmount = Amounts.sub(
+ item.refundAmount,
+ denomInfo.feeRefund,
+ ).amount;
+ refreshCoins.push({
+ amount: Amounts.stringify(refundedAmount),
+ coinPub: item.coinPub,
+ });
+ }
+ }
+ return refreshCoins;
+}
+
+/**
+ * Compute the refund item status based on the merchant's response.
+ */
+function getItemStatus(rf: MerchantCoinRefundStatus): RefundItemStatus {
+ if (rf.type === "success") {
+ return RefundItemStatus.Done;
+ } else {
+ if (rf.exchange_status >= 500 && rf.exchange_status <= 599) {
+ return RefundItemStatus.Pending;
+ } else {
+ return RefundItemStatus.Failed;
+ }
+ }
+}
+
+/**
+ * Store refunds, possibly creating a new refund group.
+ */
+async function storeRefunds(
+ wex: WalletExecutionContext,
+ purchase: PurchaseRecord,
+ refunds: MerchantCoinRefundStatus[],
+ reason: RefundReason,
+): Promise<TaskRunResult> {
+ logger.info(`storing refunds: ${j2s(refunds)}`);
+
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Payment,
+ proposalId: purchase.proposalId,
+ });
+
+ const newRefundGroupId = encodeCrock(randomBytes(32));
+ const now = TalerPreciseTimestamp.now();
+
+ const download = await expectProposalDownload(wex, purchase);
+ const currency = Amounts.currencyOf(download.contractData.amount);
+
+ const result = await wex.db.runReadWriteTx(
+ {
+ storeNames: [
+ "coins",
+ "denominations",
+ "purchases",
+ "refundItems",
+ "refundGroups",
+ "denominations",
+ "coins",
+ "coinAvailability",
+ "refreshGroups",
+ "refreshSessions",
+ ],
+ },
+ async (tx) => {
+ const myPurchase = await tx.purchases.get(purchase.proposalId);
+ if (!myPurchase) {
+ logger.warn("purchase group not found anymore");
+ return;
+ }
+ let isAborting: boolean;
+ switch (myPurchase.purchaseStatus) {
+ case PurchaseStatus.PendingAcceptRefund:
+ isAborting = false;
+ break;
+ case PurchaseStatus.AbortingWithRefund:
+ isAborting = true;
+ break;
+ default:
+ logger.warn("wrong state, not accepting refund");
+ return;
+ }
+
+ let newGroup: RefundGroupRecord | undefined = undefined;
+ // Pending, but not part of an aborted refund group.
+ let numPendingItemsTotal = 0;
+ const newGroupRefunds: RefundItemRecord[] = [];
+
+ for (const rf of refunds) {
+ const oldItem = await tx.refundItems.indexes.byCoinPubAndRtxid.get([
+ rf.coin_pub,
+ rf.rtransaction_id,
+ ]);
+ if (oldItem) {
+ logger.info("already have refund in database");
+ if (oldItem.status === RefundItemStatus.Done) {
+ continue;
+ }
+ if (rf.type === "success") {
+ oldItem.status = RefundItemStatus.Done;
+ } else {
+ if (rf.exchange_status >= 500 && rf.exchange_status <= 599) {
+ oldItem.status = RefundItemStatus.Pending;
+ numPendingItemsTotal += 1;
+ } else {
+ oldItem.status = RefundItemStatus.Failed;
+ }
+ }
+ await tx.refundItems.put(oldItem);
+ } else {
+ // Put refund item into a new group!
+ if (!newGroup) {
+ newGroup = {
+ proposalId: purchase.proposalId,
+ refundGroupId: newRefundGroupId,
+ status: RefundGroupStatus.Pending,
+ timestampCreated: timestampPreciseToDb(now),
+ amountEffective: Amounts.stringify(
+ Amounts.zeroOfCurrency(currency),
+ ),
+ amountRaw: Amounts.stringify(Amounts.zeroOfCurrency(currency)),
+ };
+ }
+ const status: RefundItemStatus = getItemStatus(rf);
+ const newItem: RefundItemRecord = {
+ coinPub: rf.coin_pub,
+ executionTime: timestampProtocolToDb(rf.execution_time),
+ obtainedTime: timestampPreciseToDb(now),
+ refundAmount: rf.refund_amount,
+ refundGroupId: newGroup.refundGroupId,
+ rtxid: rf.rtransaction_id,
+ status,
+ };
+ if (status === RefundItemStatus.Pending) {
+ numPendingItemsTotal += 1;
+ }
+ newGroupRefunds.push(newItem);
+ await tx.refundItems.put(newItem);
+ }
+ }
+
+ // Now that we know all the refunds for the new refund group,
+ // we can compute the raw/effective amounts.
+ if (newGroup) {
+ const amountsRaw = newGroupRefunds.map((x) => x.refundAmount);
+ const refreshCoins = await computeRefreshRequest(
+ wex,
+ tx,
+ newGroupRefunds,
+ );
+ const outInfo = await calculateRefreshOutput(
+ wex,
+ tx,
+ currency,
+ refreshCoins,
+ );
+ newGroup.amountEffective = Amounts.stringify(
+ Amounts.sumOrZero(currency, outInfo.outputPerCoin).amount,
+ );
+ newGroup.amountRaw = Amounts.stringify(
+ Amounts.sumOrZero(currency, amountsRaw).amount,
+ );
+ await tx.refundGroups.put(newGroup);
+ }
+
+ const refundGroups = await tx.refundGroups.indexes.byProposalId.getAll(
+ myPurchase.proposalId,
+ );
+
+ for (const refundGroup of refundGroups) {
+ switch (refundGroup.status) {
+ case RefundGroupStatus.Aborted:
+ case RefundGroupStatus.Expired:
+ case RefundGroupStatus.Failed:
+ case RefundGroupStatus.Done:
+ continue;
+ case RefundGroupStatus.Pending:
+ break;
+ default:
+ assertUnreachable(refundGroup.status);
+ }
+ const items = await tx.refundItems.indexes.byRefundGroupId.getAll([
+ refundGroup.refundGroupId,
+ ]);
+ let numPending = 0;
+ let numFailed = 0;
+ for (const item of items) {
+ if (item.status === RefundItemStatus.Pending) {
+ numPending++;
+ }
+ if (item.status === RefundItemStatus.Failed) {
+ numFailed++;
+ }
+ }
+ if (numPending === 0) {
+ // We're done for this refund group!
+ if (numFailed === 0) {
+ refundGroup.status = RefundGroupStatus.Done;
+ } else {
+ refundGroup.status = RefundGroupStatus.Failed;
+ }
+ await tx.refundGroups.put(refundGroup);
+ const refreshCoins = await computeRefreshRequest(wex, tx, items);
+ await createRefreshGroup(
+ wex,
+ tx,
+ Amounts.currencyOf(download.contractData.amount),
+ refreshCoins,
+ RefreshReason.Refund,
+ // Since refunds are really just pseudo-transactions,
+ // the originating transaction for the refresh is the payment transaction.
+ constructTransactionIdentifier({
+ tag: TransactionType.Payment,
+ proposalId: myPurchase.proposalId,
+ }),
+ );
+ }
+ }
+
+ const oldTxState = computePayMerchantTransactionState(myPurchase);
+
+ const shouldCheckAutoRefund =
+ myPurchase.autoRefundDeadline &&
+ !AbsoluteTime.isExpired(
+ AbsoluteTime.fromProtocolTimestamp(
+ timestampProtocolFromDb(myPurchase.autoRefundDeadline),
+ ),
+ );
+
+ if (numPendingItemsTotal === 0) {
+ if (isAborting) {
+ myPurchase.purchaseStatus = PurchaseStatus.AbortedRefunded;
+ } else if (shouldCheckAutoRefund) {
+ myPurchase.purchaseStatus = PurchaseStatus.PendingQueryingAutoRefund;
+ } else {
+ myPurchase.purchaseStatus = PurchaseStatus.Done;
+ }
+ myPurchase.refundAmountAwaiting = undefined;
+ }
+ await tx.purchases.put(myPurchase);
+ const newTxState = computePayMerchantTransactionState(myPurchase);
+
+ return {
+ numPendingItemsTotal,
+ transitionInfo: {
+ oldTxState,
+ newTxState,
+ },
+ };
+ },
+ );
+
+ if (!result) {
+ return TaskRunResult.finished();
+ }
+
+ notifyTransition(wex, transactionId, result.transitionInfo);
+
+ if (result.numPendingItemsTotal > 0) {
+ return TaskRunResult.backoff();
+ } else {
+ return TaskRunResult.progress();
+ }
+}
+
+export function computeRefundTransactionState(
+ refundGroupRecord: RefundGroupRecord,
+): TransactionState {
+ switch (refundGroupRecord.status) {
+ case RefundGroupStatus.Aborted:
+ return {
+ major: TransactionMajorState.Aborted,
+ };
+ case RefundGroupStatus.Done:
+ return {
+ major: TransactionMajorState.Done,
+ };
+ case RefundGroupStatus.Failed:
+ return {
+ major: TransactionMajorState.Failed,
+ };
+ case RefundGroupStatus.Pending:
+ return {
+ major: TransactionMajorState.Pending,
+ };
+ case RefundGroupStatus.Expired:
+ return {
+ major: TransactionMajorState.Expired,
+ };
+ }
+}
diff --git a/packages/taler-wallet-core/src/pay-peer-common.ts b/packages/taler-wallet-core/src/pay-peer-common.ts
new file mode 100644
index 000000000..bfd39b657
--- /dev/null
+++ b/packages/taler-wallet-core/src/pay-peer-common.ts
@@ -0,0 +1,163 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 GNUnet e.V.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ AmountJson,
+ AmountString,
+ Amounts,
+ Codec,
+ SelectedProspectiveCoin,
+ TalerProtocolTimestamp,
+ buildCodecForObject,
+ checkDbInvariant,
+ codecForAmountString,
+ codecForTimestamp,
+ codecOptional,
+} from "@gnu-taler/taler-util";
+import { SpendCoinDetails } from "./crypto/cryptoImplementation.js";
+import { DbPeerPushPaymentCoinSelection, ReserveRecord } from "./db.js";
+import { getTotalRefreshCost } from "./refresh.js";
+import { WalletExecutionContext, getDenomInfo } from "./wallet.js";
+
+/**
+ * Get information about the coin selected for signatures.
+ */
+export async function queryCoinInfosForSelection(
+ wex: WalletExecutionContext,
+ csel: DbPeerPushPaymentCoinSelection,
+): Promise<SpendCoinDetails[]> {
+ let infos: SpendCoinDetails[] = [];
+ await wex.db.runReadOnlyTx(
+ { storeNames: ["coins", "denominations"] },
+ async (tx) => {
+ for (let i = 0; i < csel.coinPubs.length; i++) {
+ const coin = await tx.coins.get(csel.coinPubs[i]);
+ if (!coin) {
+ throw Error("coin not found anymore");
+ }
+ const denom = await getDenomInfo(
+ wex,
+ tx,
+ coin.exchangeBaseUrl,
+ coin.denomPubHash,
+ );
+ if (!denom) {
+ throw Error("denom for coin not found anymore");
+ }
+ infos.push({
+ coinPriv: coin.coinPriv,
+ coinPub: coin.coinPub,
+ denomPubHash: coin.denomPubHash,
+ denomSig: coin.denomSig,
+ ageCommitmentProof: coin.ageCommitmentProof,
+ contribution: csel.contributions[i],
+ });
+ }
+ },
+ );
+ return infos;
+}
+
+export async function getTotalPeerPaymentCost(
+ wex: WalletExecutionContext,
+ pcs: SelectedProspectiveCoin[],
+): Promise<AmountJson> {
+ 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;
+ },
+ );
+}
+
+interface ExchangePurseStatus {
+ balance: AmountString;
+ deposit_timestamp?: TalerProtocolTimestamp;
+ merge_timestamp?: TalerProtocolTimestamp;
+}
+
+export const codecForExchangePurseStatus = (): Codec<ExchangePurseStatus> =>
+ buildCodecForObject<ExchangePurseStatus>()
+ .property("balance", codecForAmountString())
+ .property("deposit_timestamp", codecOptional(codecForTimestamp))
+ .property("merge_timestamp", codecOptional(codecForTimestamp))
+ .build("ExchangePurseStatus");
+
+export async function getMergeReserveInfo(
+ wex: WalletExecutionContext,
+ req: {
+ exchangeBaseUrl: string;
+ },
+): Promise<ReserveRecord> {
+ // We have to eagerly create the key pair outside of the transaction,
+ // due to the async crypto API.
+ const newReservePair = await wex.cryptoApi.createEddsaKeypair({});
+
+ const mergeReserveRecord: ReserveRecord = await wex.db.runReadWriteTx(
+ { storeNames: ["exchanges", "reserves"] },
+ async (tx) => {
+ const ex = await tx.exchanges.get(req.exchangeBaseUrl);
+ checkDbInvariant(!!ex);
+ if (ex.currentMergeReserveRowId != null) {
+ const reserve = await tx.reserves.get(ex.currentMergeReserveRowId);
+ checkDbInvariant(!!reserve);
+ return reserve;
+ }
+ const reserve: ReserveRecord = {
+ reservePriv: newReservePair.priv,
+ reservePub: newReservePair.pub,
+ };
+ const insertResp = await tx.reserves.put(reserve);
+ checkDbInvariant(typeof insertResp.key === "number");
+ reserve.rowId = insertResp.key;
+ ex.currentMergeReserveRowId = reserve.rowId;
+ await tx.exchanges.put(ex);
+ return reserve;
+ },
+ );
+
+ return mergeReserveRecord;
+}
diff --git a/packages/taler-wallet-core/src/pay-peer-pull-credit.ts b/packages/taler-wallet-core/src/pay-peer-pull-credit.ts
new file mode 100644
index 000000000..840c244d0
--- /dev/null
+++ b/packages/taler-wallet-core/src/pay-peer-pull-credit.ts
@@ -0,0 +1,1215 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ AbsoluteTime,
+ Amounts,
+ CheckPeerPullCreditRequest,
+ CheckPeerPullCreditResponse,
+ ContractTermsUtil,
+ ExchangeReservePurseRequest,
+ HttpStatusCode,
+ InitiatePeerPullCreditRequest,
+ InitiatePeerPullCreditResponse,
+ Logger,
+ NotificationType,
+ PeerContractTerms,
+ TalerErrorCode,
+ TalerPreciseTimestamp,
+ TalerProtocolTimestamp,
+ TalerUriAction,
+ TransactionAction,
+ TransactionIdStr,
+ TransactionMajorState,
+ TransactionMinorState,
+ TransactionState,
+ TransactionType,
+ WalletAccountMergeFlags,
+ WalletKycUuid,
+ assertUnreachable,
+ checkDbInvariant,
+ codecForAny,
+ codecForWalletKycUuid,
+ encodeCrock,
+ getRandomBytes,
+ j2s,
+ makeErrorDetail,
+ stringifyTalerUri,
+ talerPaytoFromExchangeReserve,
+} from "@gnu-taler/taler-util";
+import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
+import {
+ PendingTaskType,
+ TaskIdStr,
+ TaskRunResult,
+ TaskRunResultType,
+ TombstoneTag,
+ TransactionContext,
+ constructTaskIdentifier,
+} from "./common.js";
+import {
+ KycPendingInfo,
+ KycUserType,
+ PeerPullCreditRecord,
+ PeerPullPaymentCreditStatus,
+ WithdrawalGroupStatus,
+ WithdrawalRecordType,
+ timestampOptionalPreciseFromDb,
+ timestampPreciseFromDb,
+ timestampPreciseToDb,
+} from "./db.js";
+import { fetchFreshExchange } from "./exchanges.js";
+import {
+ codecForExchangePurseStatus,
+ getMergeReserveInfo,
+} from "./pay-peer-common.js";
+import {
+ constructTransactionIdentifier,
+ notifyTransition,
+} from "./transactions.js";
+import { WalletExecutionContext } from "./wallet.js";
+import {
+ getExchangeWithdrawalInfo,
+ internalCreateWithdrawalGroup,
+ waitWithdrawalFinal,
+} from "./withdraw.js";
+
+const logger = new Logger("pay-peer-pull-credit.ts");
+
+export class PeerPullCreditTransactionContext implements TransactionContext {
+ readonly transactionId: TransactionIdStr;
+ readonly taskId: TaskIdStr;
+
+ constructor(
+ public wex: WalletExecutionContext,
+ public pursePub: string,
+ ) {
+ this.taskId = constructTaskIdentifier({
+ tag: PendingTaskType.PeerPullCredit,
+ pursePub,
+ });
+ this.transactionId = constructTransactionIdentifier({
+ tag: TransactionType.PeerPullCredit,
+ pursePub,
+ });
+ }
+
+ async deleteTransaction(): Promise<void> {
+ const { wex: ws, pursePub } = this;
+ await ws.db.runReadWriteTx(
+ { storeNames: ["withdrawalGroups", "peerPullCredit", "tombstones"] },
+ async (tx) => {
+ const pullIni = await tx.peerPullCredit.get(pursePub);
+ if (!pullIni) {
+ return;
+ }
+ if (pullIni.withdrawalGroupId) {
+ const withdrawalGroupId = pullIni.withdrawalGroupId;
+ const withdrawalGroupRecord =
+ await tx.withdrawalGroups.get(withdrawalGroupId);
+ if (withdrawalGroupRecord) {
+ await tx.withdrawalGroups.delete(withdrawalGroupId);
+ await tx.tombstones.put({
+ id: TombstoneTag.DeleteWithdrawalGroup + ":" + withdrawalGroupId,
+ });
+ }
+ }
+ await tx.peerPullCredit.delete(pursePub);
+ await tx.tombstones.put({
+ id: TombstoneTag.DeletePeerPullCredit + ":" + pursePub,
+ });
+ },
+ );
+
+ return;
+ }
+
+ async suspendTransaction(): Promise<void> {
+ const { wex, pursePub, taskId: retryTag, transactionId } = this;
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["peerPullCredit"] },
+ async (tx) => {
+ const pullCreditRec = await tx.peerPullCredit.get(pursePub);
+ if (!pullCreditRec) {
+ logger.warn(`peer pull credit ${pursePub} not found`);
+ return;
+ }
+ let newStatus: PeerPullPaymentCreditStatus | undefined = undefined;
+ switch (pullCreditRec.status) {
+ case PeerPullPaymentCreditStatus.PendingCreatePurse:
+ newStatus = PeerPullPaymentCreditStatus.SuspendedCreatePurse;
+ break;
+ case PeerPullPaymentCreditStatus.PendingMergeKycRequired:
+ newStatus = PeerPullPaymentCreditStatus.SuspendedMergeKycRequired;
+ break;
+ case PeerPullPaymentCreditStatus.PendingWithdrawing:
+ newStatus = PeerPullPaymentCreditStatus.SuspendedWithdrawing;
+ break;
+ case PeerPullPaymentCreditStatus.PendingReady:
+ newStatus = PeerPullPaymentCreditStatus.SuspendedReady;
+ break;
+ case PeerPullPaymentCreditStatus.AbortingDeletePurse:
+ newStatus =
+ PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse;
+ break;
+ case PeerPullPaymentCreditStatus.Done:
+ case PeerPullPaymentCreditStatus.SuspendedCreatePurse:
+ case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired:
+ case PeerPullPaymentCreditStatus.SuspendedReady:
+ case PeerPullPaymentCreditStatus.SuspendedWithdrawing:
+ case PeerPullPaymentCreditStatus.Aborted:
+ case PeerPullPaymentCreditStatus.Failed:
+ case PeerPullPaymentCreditStatus.Expired:
+ case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse:
+ break;
+ default:
+ assertUnreachable(pullCreditRec.status);
+ }
+ if (newStatus != null) {
+ const oldTxState =
+ computePeerPullCreditTransactionState(pullCreditRec);
+ pullCreditRec.status = newStatus;
+ const newTxState =
+ computePeerPullCreditTransactionState(pullCreditRec);
+ await tx.peerPullCredit.put(pullCreditRec);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ return undefined;
+ },
+ );
+ wex.taskScheduler.stopShepherdTask(retryTag);
+ notifyTransition(wex, transactionId, transitionInfo);
+ }
+
+ async failTransaction(): Promise<void> {
+ const { wex, pursePub, taskId: retryTag, transactionId } = this;
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["peerPullCredit"] },
+ async (tx) => {
+ const pullCreditRec = await tx.peerPullCredit.get(pursePub);
+ if (!pullCreditRec) {
+ logger.warn(`peer pull credit ${pursePub} not found`);
+ return;
+ }
+ let newStatus: PeerPullPaymentCreditStatus | undefined = undefined;
+ switch (pullCreditRec.status) {
+ case PeerPullPaymentCreditStatus.PendingCreatePurse:
+ case PeerPullPaymentCreditStatus.PendingMergeKycRequired:
+ case PeerPullPaymentCreditStatus.PendingWithdrawing:
+ case PeerPullPaymentCreditStatus.PendingReady:
+ case PeerPullPaymentCreditStatus.Done:
+ case PeerPullPaymentCreditStatus.SuspendedCreatePurse:
+ case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired:
+ case PeerPullPaymentCreditStatus.SuspendedReady:
+ case PeerPullPaymentCreditStatus.SuspendedWithdrawing:
+ case PeerPullPaymentCreditStatus.Aborted:
+ case PeerPullPaymentCreditStatus.Failed:
+ case PeerPullPaymentCreditStatus.Expired:
+ break;
+ case PeerPullPaymentCreditStatus.AbortingDeletePurse:
+ case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse:
+ newStatus = PeerPullPaymentCreditStatus.Failed;
+ break;
+ default:
+ assertUnreachable(pullCreditRec.status);
+ }
+ if (newStatus != null) {
+ const oldTxState =
+ computePeerPullCreditTransactionState(pullCreditRec);
+ pullCreditRec.status = newStatus;
+ const newTxState =
+ computePeerPullCreditTransactionState(pullCreditRec);
+ await tx.peerPullCredit.put(pullCreditRec);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ return undefined;
+ },
+ );
+ notifyTransition(wex, transactionId, transitionInfo);
+ wex.taskScheduler.stopShepherdTask(retryTag);
+ }
+
+ async resumeTransaction(): Promise<void> {
+ const { wex, pursePub, taskId: retryTag, transactionId } = this;
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["peerPullCredit"] },
+ async (tx) => {
+ const pullCreditRec = await tx.peerPullCredit.get(pursePub);
+ if (!pullCreditRec) {
+ logger.warn(`peer pull credit ${pursePub} not found`);
+ return;
+ }
+ let newStatus: PeerPullPaymentCreditStatus | undefined = undefined;
+ switch (pullCreditRec.status) {
+ case PeerPullPaymentCreditStatus.PendingCreatePurse:
+ case PeerPullPaymentCreditStatus.PendingMergeKycRequired:
+ case PeerPullPaymentCreditStatus.PendingWithdrawing:
+ case PeerPullPaymentCreditStatus.PendingReady:
+ case PeerPullPaymentCreditStatus.AbortingDeletePurse:
+ case PeerPullPaymentCreditStatus.Done:
+ case PeerPullPaymentCreditStatus.Failed:
+ case PeerPullPaymentCreditStatus.Expired:
+ case PeerPullPaymentCreditStatus.Aborted:
+ break;
+ case PeerPullPaymentCreditStatus.SuspendedCreatePurse:
+ newStatus = PeerPullPaymentCreditStatus.PendingCreatePurse;
+ break;
+ case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired:
+ newStatus = PeerPullPaymentCreditStatus.PendingMergeKycRequired;
+ break;
+ case PeerPullPaymentCreditStatus.SuspendedReady:
+ newStatus = PeerPullPaymentCreditStatus.PendingReady;
+ break;
+ case PeerPullPaymentCreditStatus.SuspendedWithdrawing:
+ newStatus = PeerPullPaymentCreditStatus.PendingWithdrawing;
+ break;
+ case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse:
+ newStatus = PeerPullPaymentCreditStatus.AbortingDeletePurse;
+ break;
+ default:
+ assertUnreachable(pullCreditRec.status);
+ }
+ if (newStatus != null) {
+ const oldTxState =
+ computePeerPullCreditTransactionState(pullCreditRec);
+ pullCreditRec.status = newStatus;
+ const newTxState =
+ computePeerPullCreditTransactionState(pullCreditRec);
+ await tx.peerPullCredit.put(pullCreditRec);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ return undefined;
+ },
+ );
+ notifyTransition(wex, transactionId, transitionInfo);
+ wex.taskScheduler.startShepherdTask(retryTag);
+ }
+
+ async abortTransaction(): Promise<void> {
+ const { wex, pursePub, taskId: retryTag, transactionId } = this;
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["peerPullCredit"] },
+ async (tx) => {
+ const pullCreditRec = await tx.peerPullCredit.get(pursePub);
+ if (!pullCreditRec) {
+ logger.warn(`peer pull credit ${pursePub} not found`);
+ return;
+ }
+ let newStatus: PeerPullPaymentCreditStatus | undefined = undefined;
+ switch (pullCreditRec.status) {
+ case PeerPullPaymentCreditStatus.PendingCreatePurse:
+ case PeerPullPaymentCreditStatus.PendingMergeKycRequired:
+ newStatus = PeerPullPaymentCreditStatus.AbortingDeletePurse;
+ break;
+ case PeerPullPaymentCreditStatus.PendingWithdrawing:
+ throw Error("can't abort anymore");
+ case PeerPullPaymentCreditStatus.PendingReady:
+ newStatus = PeerPullPaymentCreditStatus.AbortingDeletePurse;
+ break;
+ case PeerPullPaymentCreditStatus.Done:
+ case PeerPullPaymentCreditStatus.SuspendedCreatePurse:
+ case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired:
+ case PeerPullPaymentCreditStatus.SuspendedReady:
+ case PeerPullPaymentCreditStatus.SuspendedWithdrawing:
+ case PeerPullPaymentCreditStatus.Aborted:
+ case PeerPullPaymentCreditStatus.AbortingDeletePurse:
+ case PeerPullPaymentCreditStatus.Failed:
+ case PeerPullPaymentCreditStatus.Expired:
+ case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse:
+ break;
+ default:
+ assertUnreachable(pullCreditRec.status);
+ }
+ if (newStatus != null) {
+ const oldTxState =
+ computePeerPullCreditTransactionState(pullCreditRec);
+ pullCreditRec.status = newStatus;
+ const newTxState =
+ computePeerPullCreditTransactionState(pullCreditRec);
+ await tx.peerPullCredit.put(pullCreditRec);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ return undefined;
+ },
+ );
+ wex.taskScheduler.stopShepherdTask(retryTag);
+ notifyTransition(wex, transactionId, transitionInfo);
+ wex.taskScheduler.startShepherdTask(retryTag);
+ }
+}
+
+async function queryPurseForPeerPullCredit(
+ wex: WalletExecutionContext,
+ pullIni: PeerPullCreditRecord,
+): Promise<TaskRunResult> {
+ const purseDepositUrl = new URL(
+ `purses/${pullIni.pursePub}/deposit`,
+ pullIni.exchangeBaseUrl,
+ );
+ purseDepositUrl.searchParams.set("timeout_ms", "30000");
+ logger.info(`querying purse status via ${purseDepositUrl.href}`);
+ const resp = await wex.http.fetch(purseDepositUrl.href, {
+ timeout: { d_ms: 60000 },
+ cancellationToken: wex.cancellationToken,
+ });
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.PeerPullCredit,
+ pursePub: pullIni.pursePub,
+ });
+
+ logger.info(`purse status code: HTTP ${resp.status}`);
+
+ switch (resp.status) {
+ case HttpStatusCode.Gone: {
+ // Exchange says that purse doesn't exist anymore => expired!
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["peerPullCredit"] },
+ async (tx) => {
+ const finPi = await tx.peerPullCredit.get(pullIni.pursePub);
+ if (!finPi) {
+ logger.warn("peerPullCredit not found anymore");
+ return;
+ }
+ const oldTxState = computePeerPullCreditTransactionState(finPi);
+ if (finPi.status === PeerPullPaymentCreditStatus.PendingReady) {
+ finPi.status = PeerPullPaymentCreditStatus.Expired;
+ }
+ await tx.peerPullCredit.put(finPi);
+ const newTxState = computePeerPullCreditTransactionState(finPi);
+ return { oldTxState, newTxState };
+ },
+ );
+ notifyTransition(wex, transactionId, transitionInfo);
+ return TaskRunResult.backoff();
+ }
+ case HttpStatusCode.NotFound:
+ // FIXME: Maybe check error code? 404 could also mean something else.
+ return TaskRunResult.longpollReturnedPending();
+ }
+
+ const result = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForExchangePurseStatus(),
+ );
+
+ logger.trace(`purse status: ${j2s(result)}`);
+
+ const depositTimestamp = result.deposit_timestamp;
+
+ if (!depositTimestamp || TalerProtocolTimestamp.isNever(depositTimestamp)) {
+ logger.info("purse not ready yet (no deposit)");
+ return TaskRunResult.backoff();
+ }
+
+ const reserve = await wex.db.runReadOnlyTx(
+ { storeNames: ["reserves"] },
+ async (tx) => {
+ return await tx.reserves.get(pullIni.mergeReserveRowId);
+ },
+ );
+
+ if (!reserve) {
+ throw Error("reserve for peer pull credit not found in wallet DB");
+ }
+
+ await internalCreateWithdrawalGroup(wex, {
+ amount: Amounts.parseOrThrow(pullIni.amount),
+ wgInfo: {
+ withdrawalType: WithdrawalRecordType.PeerPullCredit,
+ contractPriv: pullIni.contractPriv,
+ },
+ forcedWithdrawalGroupId: pullIni.withdrawalGroupId,
+ exchangeBaseUrl: pullIni.exchangeBaseUrl,
+ reserveStatus: WithdrawalGroupStatus.PendingQueryingStatus,
+ reserveKeyPair: {
+ priv: reserve.reservePriv,
+ pub: reserve.reservePub,
+ },
+ });
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["peerPullCredit"] },
+ async (tx) => {
+ const finPi = await tx.peerPullCredit.get(pullIni.pursePub);
+ if (!finPi) {
+ logger.warn("peerPullCredit not found anymore");
+ return;
+ }
+ const oldTxState = computePeerPullCreditTransactionState(finPi);
+ if (finPi.status === PeerPullPaymentCreditStatus.PendingReady) {
+ finPi.status = PeerPullPaymentCreditStatus.PendingWithdrawing;
+ }
+ await tx.peerPullCredit.put(finPi);
+ const newTxState = computePeerPullCreditTransactionState(finPi);
+ return { oldTxState, newTxState };
+ },
+ );
+ notifyTransition(wex, transactionId, transitionInfo);
+ return TaskRunResult.backoff();
+}
+
+async function longpollKycStatus(
+ wex: WalletExecutionContext,
+ pursePub: string,
+ exchangeUrl: string,
+ kycInfo: KycPendingInfo,
+ userType: KycUserType,
+): Promise<TaskRunResult> {
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.PeerPullCredit,
+ pursePub,
+ });
+ const url = new URL(
+ `kyc-check/${kycInfo.requirementRow}/${kycInfo.paytoHash}/${userType}`,
+ exchangeUrl,
+ );
+ url.searchParams.set("timeout_ms", "10000");
+ logger.info(`kyc url ${url.href}`);
+ const kycStatusRes = await wex.http.fetch(url.href, {
+ method: "GET",
+ cancellationToken: wex.cancellationToken,
+ });
+ if (
+ kycStatusRes.status === HttpStatusCode.Ok ||
+ // FIXME: NoContent is not expected https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-merge
+ // remove after the exchange is fixed or clarified
+ kycStatusRes.status === HttpStatusCode.NoContent
+ ) {
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["peerPullCredit"] },
+ async (tx) => {
+ const peerIni = await tx.peerPullCredit.get(pursePub);
+ if (!peerIni) {
+ return;
+ }
+ if (
+ peerIni.status !== PeerPullPaymentCreditStatus.PendingMergeKycRequired
+ ) {
+ return;
+ }
+ const oldTxState = computePeerPullCreditTransactionState(peerIni);
+ peerIni.status = PeerPullPaymentCreditStatus.PendingCreatePurse;
+ const newTxState = computePeerPullCreditTransactionState(peerIni);
+ await tx.peerPullCredit.put(peerIni);
+ return { oldTxState, newTxState };
+ },
+ );
+ notifyTransition(wex, transactionId, transitionInfo);
+ return TaskRunResult.progress();
+ } else if (kycStatusRes.status === HttpStatusCode.Accepted) {
+ return TaskRunResult.longpollReturnedPending();
+ } else {
+ throw Error(`unexpected response from kyc-check (${kycStatusRes.status})`);
+ }
+}
+
+async function processPeerPullCreditAbortingDeletePurse(
+ wex: WalletExecutionContext,
+ peerPullIni: PeerPullCreditRecord,
+): Promise<TaskRunResult> {
+ const { pursePub, pursePriv } = peerPullIni;
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.PeerPullCredit,
+ pursePub,
+ });
+
+ const sigResp = await wex.cryptoApi.signDeletePurse({
+ pursePriv,
+ });
+ const purseUrl = new URL(`purses/${pursePub}`, peerPullIni.exchangeBaseUrl);
+ const resp = await wex.http.fetch(purseUrl.href, {
+ method: "DELETE",
+ headers: {
+ "taler-purse-signature": sigResp.sig,
+ },
+ cancellationToken: wex.cancellationToken,
+ });
+ logger.info(`deleted purse with response status ${resp.status}`);
+
+ const transitionInfo = await wex.db.runReadWriteTx(
+ {
+ storeNames: [
+ "peerPullCredit",
+ "refreshGroups",
+ "denominations",
+ "coinAvailability",
+ "coins",
+ ],
+ },
+ async (tx) => {
+ const ppiRec = await tx.peerPullCredit.get(pursePub);
+ if (!ppiRec) {
+ return undefined;
+ }
+ if (ppiRec.status !== PeerPullPaymentCreditStatus.AbortingDeletePurse) {
+ return undefined;
+ }
+ const oldTxState = computePeerPullCreditTransactionState(ppiRec);
+ ppiRec.status = PeerPullPaymentCreditStatus.Aborted;
+ await tx.peerPullCredit.put(ppiRec);
+ const newTxState = computePeerPullCreditTransactionState(ppiRec);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ },
+ );
+ notifyTransition(wex, transactionId, transitionInfo);
+
+ return TaskRunResult.backoff();
+}
+
+async function handlePeerPullCreditWithdrawing(
+ wex: WalletExecutionContext,
+ pullIni: PeerPullCreditRecord,
+): Promise<TaskRunResult> {
+ if (!pullIni.withdrawalGroupId) {
+ throw Error("invalid db state (withdrawing, but no withdrawal group ID");
+ }
+ await waitWithdrawalFinal(wex, pullIni.withdrawalGroupId);
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.PeerPullCredit,
+ pursePub: pullIni.pursePub,
+ });
+ const wgId = pullIni.withdrawalGroupId;
+ let finished: boolean = false;
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["peerPullCredit", "withdrawalGroups"] },
+ async (tx) => {
+ const ppi = await tx.peerPullCredit.get(pullIni.pursePub);
+ if (!ppi) {
+ finished = true;
+ return;
+ }
+ if (ppi.status !== PeerPullPaymentCreditStatus.PendingWithdrawing) {
+ finished = true;
+ return;
+ }
+ const oldTxState = computePeerPullCreditTransactionState(ppi);
+ const wg = await tx.withdrawalGroups.get(wgId);
+ if (!wg) {
+ // FIXME: Fail the operation instead?
+ return undefined;
+ }
+ switch (wg.status) {
+ case WithdrawalGroupStatus.Done:
+ finished = true;
+ ppi.status = PeerPullPaymentCreditStatus.Done;
+ break;
+ // FIXME: Also handle other final states!
+ }
+ await tx.peerPullCredit.put(ppi);
+ const newTxState = computePeerPullCreditTransactionState(ppi);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ },
+ );
+ notifyTransition(wex, transactionId, transitionInfo);
+ if (finished) {
+ return TaskRunResult.finished();
+ } else {
+ // FIXME: Return indicator that we depend on the other operation!
+ return TaskRunResult.backoff();
+ }
+}
+
+async function handlePeerPullCreditCreatePurse(
+ wex: WalletExecutionContext,
+ pullIni: PeerPullCreditRecord,
+): Promise<TaskRunResult> {
+ const purseFee = Amounts.stringify(Amounts.zeroOfAmount(pullIni.amount));
+ const pursePub = pullIni.pursePub;
+ const mergeReserve = await wex.db.runReadOnlyTx(
+ { storeNames: ["reserves"] },
+ async (tx) => {
+ return tx.reserves.get(pullIni.mergeReserveRowId);
+ },
+ );
+
+ if (!mergeReserve) {
+ throw Error("merge reserve for peer pull payment not found in database");
+ }
+
+ const contractTermsRecord = await wex.db.runReadOnlyTx(
+ { storeNames: ["contractTerms"] },
+ async (tx) => {
+ return tx.contractTerms.get(pullIni.contractTermsHash);
+ },
+ );
+
+ if (!contractTermsRecord) {
+ throw Error("contract terms for peer pull payment not found in database");
+ }
+
+ const contractTerms: PeerContractTerms = contractTermsRecord.contractTermsRaw;
+
+ const reservePayto = talerPaytoFromExchangeReserve(
+ pullIni.exchangeBaseUrl,
+ mergeReserve.reservePub,
+ );
+
+ const econtractResp = await wex.cryptoApi.encryptContractForDeposit({
+ contractPriv: pullIni.contractPriv,
+ contractPub: pullIni.contractPub,
+ contractTerms: contractTermsRecord.contractTermsRaw,
+ pursePriv: pullIni.pursePriv,
+ pursePub: pullIni.pursePub,
+ nonce: pullIni.contractEncNonce,
+ });
+
+ const mergeTimestamp = timestampPreciseFromDb(pullIni.mergeTimestamp);
+
+ const purseExpiration = contractTerms.purse_expiration;
+ const sigRes = await wex.cryptoApi.signReservePurseCreate({
+ contractTermsHash: pullIni.contractTermsHash,
+ flags: WalletAccountMergeFlags.CreateWithPurseFee,
+ mergePriv: pullIni.mergePriv,
+ mergeTimestamp: TalerPreciseTimestamp.round(mergeTimestamp),
+ purseAmount: pullIni.amount,
+ purseExpiration: purseExpiration,
+ purseFee: purseFee,
+ pursePriv: pullIni.pursePriv,
+ pursePub: pullIni.pursePub,
+ reservePayto,
+ reservePriv: mergeReserve.reservePriv,
+ });
+
+ const reservePurseReqBody: ExchangeReservePurseRequest = {
+ merge_sig: sigRes.mergeSig,
+ merge_timestamp: TalerPreciseTimestamp.round(mergeTimestamp),
+ h_contract_terms: pullIni.contractTermsHash,
+ merge_pub: pullIni.mergePub,
+ min_age: 0,
+ purse_expiration: purseExpiration,
+ purse_fee: purseFee,
+ purse_pub: pullIni.pursePub,
+ purse_sig: sigRes.purseSig,
+ purse_value: pullIni.amount,
+ reserve_sig: sigRes.accountSig,
+ econtract: econtractResp.econtract,
+ };
+
+ logger.info(`reserve purse request: ${j2s(reservePurseReqBody)}`);
+
+ const reservePurseMergeUrl = new URL(
+ `reserves/${mergeReserve.reservePub}/purse`,
+ pullIni.exchangeBaseUrl,
+ );
+
+ const httpResp = await wex.http.fetch(reservePurseMergeUrl.href, {
+ method: "POST",
+ body: reservePurseReqBody,
+ cancellationToken: wex.cancellationToken,
+ });
+
+ if (httpResp.status === HttpStatusCode.UnavailableForLegalReasons) {
+ const respJson = await httpResp.json();
+ const kycPending = codecForWalletKycUuid().decode(respJson);
+ logger.info(`kyc uuid response: ${j2s(kycPending)}`);
+ return processPeerPullCreditKycRequired(wex, pullIni, kycPending);
+ }
+
+ const resp = await readSuccessResponseJsonOrThrow(httpResp, codecForAny());
+
+ logger.info(`reserve merge response: ${j2s(resp)}`);
+
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.PeerPullCredit,
+ pursePub: pullIni.pursePub,
+ });
+
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["peerPullCredit"] },
+ async (tx) => {
+ const pi2 = await tx.peerPullCredit.get(pursePub);
+ if (!pi2) {
+ return;
+ }
+ const oldTxState = computePeerPullCreditTransactionState(pi2);
+ pi2.status = PeerPullPaymentCreditStatus.PendingReady;
+ await tx.peerPullCredit.put(pi2);
+ const newTxState = computePeerPullCreditTransactionState(pi2);
+ return { oldTxState, newTxState };
+ },
+ );
+ notifyTransition(wex, transactionId, transitionInfo);
+ return TaskRunResult.backoff();
+}
+
+export async function processPeerPullCredit(
+ wex: WalletExecutionContext,
+ pursePub: string,
+): Promise<TaskRunResult> {
+ const pullIni = await wex.db.runReadOnlyTx(
+ { storeNames: ["peerPullCredit"] },
+ async (tx) => {
+ return tx.peerPullCredit.get(pursePub);
+ },
+ );
+ if (!pullIni) {
+ throw Error("peer pull payment initiation not found in database");
+ }
+
+ const retryTag = constructTaskIdentifier({
+ tag: PendingTaskType.PeerPullCredit,
+ pursePub,
+ });
+
+ logger.trace(`processing ${retryTag}, status=${pullIni.status}`);
+
+ switch (pullIni.status) {
+ case PeerPullPaymentCreditStatus.Done: {
+ return TaskRunResult.finished();
+ }
+ case PeerPullPaymentCreditStatus.PendingReady:
+ return queryPurseForPeerPullCredit(wex, pullIni);
+ case PeerPullPaymentCreditStatus.PendingMergeKycRequired: {
+ if (!pullIni.kycInfo) {
+ throw Error("invalid state, kycInfo required");
+ }
+ return await longpollKycStatus(
+ wex,
+ pursePub,
+ pullIni.exchangeBaseUrl,
+ pullIni.kycInfo,
+ "individual",
+ );
+ }
+ case PeerPullPaymentCreditStatus.PendingCreatePurse:
+ return handlePeerPullCreditCreatePurse(wex, pullIni);
+ case PeerPullPaymentCreditStatus.AbortingDeletePurse:
+ return await processPeerPullCreditAbortingDeletePurse(wex, pullIni);
+ case PeerPullPaymentCreditStatus.PendingWithdrawing:
+ return handlePeerPullCreditWithdrawing(wex, pullIni);
+ case PeerPullPaymentCreditStatus.Aborted:
+ case PeerPullPaymentCreditStatus.Failed:
+ case PeerPullPaymentCreditStatus.Expired:
+ case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse:
+ case PeerPullPaymentCreditStatus.SuspendedCreatePurse:
+ case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired:
+ case PeerPullPaymentCreditStatus.SuspendedReady:
+ case PeerPullPaymentCreditStatus.SuspendedWithdrawing:
+ break;
+ default:
+ assertUnreachable(pullIni.status);
+ }
+
+ return TaskRunResult.finished();
+}
+
+async function processPeerPullCreditKycRequired(
+ wex: WalletExecutionContext,
+ peerIni: PeerPullCreditRecord,
+ kycPending: WalletKycUuid,
+): Promise<TaskRunResult> {
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.PeerPullCredit,
+ pursePub: peerIni.pursePub,
+ });
+ const { pursePub } = peerIni;
+
+ const userType = "individual";
+ const url = new URL(
+ `kyc-check/${kycPending.requirement_row}/${kycPending.h_payto}/${userType}`,
+ peerIni.exchangeBaseUrl,
+ );
+
+ logger.info(`kyc url ${url.href}`);
+ const kycStatusRes = 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
+ // remove after the exchange is fixed or clarified
+ kycStatusRes.status === HttpStatusCode.NoContent
+ ) {
+ logger.warn("kyc requested, but already fulfilled");
+ return TaskRunResult.backoff();
+ } else if (kycStatusRes.status === HttpStatusCode.Accepted) {
+ const kycStatus = await kycStatusRes.json();
+ logger.info(`kyc status: ${j2s(kycStatus)}`);
+ const { transitionInfo, result } = await wex.db.runReadWriteTx(
+ { storeNames: ["peerPullCredit"] },
+ async (tx) => {
+ const peerInc = await tx.peerPullCredit.get(pursePub);
+ if (!peerInc) {
+ return {
+ transitionInfo: undefined,
+ result: TaskRunResult.finished(),
+ };
+ }
+ const oldTxState = computePeerPullCreditTransactionState(peerInc);
+ peerInc.kycInfo = {
+ paytoHash: kycPending.h_payto,
+ requirementRow: kycPending.requirement_row,
+ };
+ peerInc.kycUrl = kycStatus.kyc_url;
+ peerInc.status = PeerPullPaymentCreditStatus.PendingMergeKycRequired;
+ const newTxState = computePeerPullCreditTransactionState(peerInc);
+ await tx.peerPullCredit.put(peerInc);
+ // We'll remove this eventually! New clients should rely on the
+ // kycUrl field of the transaction, not the error code.
+ const res: TaskRunResult = {
+ type: TaskRunResultType.Error,
+ errorDetail: makeErrorDetail(
+ TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED,
+ {
+ kycUrl: kycStatus.kyc_url,
+ },
+ ),
+ };
+ return {
+ transitionInfo: { oldTxState, newTxState },
+ result: res,
+ };
+ },
+ );
+ notifyTransition(wex, transactionId, transitionInfo);
+ return TaskRunResult.backoff();
+ } else {
+ throw Error(`unexpected response from kyc-check (${kycStatusRes.status})`);
+ }
+}
+
+/**
+ * Check fees and available exchanges for a peer push payment initiation.
+ */
+export async function checkPeerPullPaymentInitiation(
+ wex: WalletExecutionContext,
+ req: CheckPeerPullCreditRequest,
+): Promise<CheckPeerPullCreditResponse> {
+ // FIXME: We don't support exchanges with purse fees yet.
+ // Select an exchange where we have money in the specified currency
+ // FIXME: How do we handle regional currency scopes here? Is it an additional input?
+
+ logger.trace("checking peer-pull-credit fees");
+
+ const currency = Amounts.currencyOf(req.amount);
+ let exchangeUrl;
+ if (req.exchangeBaseUrl) {
+ exchangeUrl = req.exchangeBaseUrl;
+ } else {
+ exchangeUrl = await getPreferredExchangeForCurrency(wex, currency);
+ }
+
+ if (!exchangeUrl) {
+ throw Error("no exchange found for initiating a peer pull payment");
+ }
+
+ logger.trace(`found ${exchangeUrl} as preferred exchange`);
+
+ const wi = await getExchangeWithdrawalInfo(
+ wex,
+ exchangeUrl,
+ Amounts.parseOrThrow(req.amount),
+ undefined,
+ );
+
+ logger.trace(`got withdrawal info`);
+
+ let numCoins = 0;
+ for (let i = 0; i < wi.selectedDenoms.selectedDenoms.length; i++) {
+ numCoins += wi.selectedDenoms.selectedDenoms[i].count;
+ }
+
+ return {
+ exchangeBaseUrl: exchangeUrl,
+ amountEffective: wi.withdrawalAmountEffective,
+ amountRaw: req.amount,
+ numCoins,
+ };
+}
+
+/**
+ * Find a preferred exchange based on when we withdrew last from this exchange.
+ */
+async function getPreferredExchangeForCurrency(
+ wex: WalletExecutionContext,
+ currency: string,
+): Promise<string | undefined> {
+ // Find an exchange with the matching currency.
+ // Prefer exchanges with the most recent withdrawal.
+ const url = await wex.db.runReadOnlyTx(
+ { storeNames: ["exchanges"] },
+ async (tx) => {
+ const exchanges = await tx.exchanges.iter().toArray();
+ let candidate = undefined;
+ for (const e of exchanges) {
+ if (e.detailsPointer?.currency !== currency) {
+ continue;
+ }
+ if (!candidate) {
+ candidate = e;
+ continue;
+ }
+ if (candidate.lastWithdrawal && !e.lastWithdrawal) {
+ continue;
+ }
+ const exchangeLastWithdrawal = timestampOptionalPreciseFromDb(
+ e.lastWithdrawal,
+ );
+ const candidateLastWithdrawal = timestampOptionalPreciseFromDb(
+ candidate.lastWithdrawal,
+ );
+ if (exchangeLastWithdrawal && candidateLastWithdrawal) {
+ if (
+ AbsoluteTime.cmp(
+ AbsoluteTime.fromPreciseTimestamp(exchangeLastWithdrawal),
+ AbsoluteTime.fromPreciseTimestamp(candidateLastWithdrawal),
+ ) > 0
+ ) {
+ candidate = e;
+ }
+ }
+ }
+ if (candidate) {
+ return candidate.baseUrl;
+ }
+ return undefined;
+ },
+ );
+ return url;
+}
+
+/**
+ * Initiate a peer pull payment.
+ */
+export async function initiatePeerPullPayment(
+ wex: WalletExecutionContext,
+ req: InitiatePeerPullCreditRequest,
+): Promise<InitiatePeerPullCreditResponse> {
+ const currency = Amounts.currencyOf(req.partialContractTerms.amount);
+ let maybeExchangeBaseUrl: string | undefined;
+ if (req.exchangeBaseUrl) {
+ maybeExchangeBaseUrl = req.exchangeBaseUrl;
+ } else {
+ maybeExchangeBaseUrl = await getPreferredExchangeForCurrency(wex, currency);
+ }
+
+ if (!maybeExchangeBaseUrl) {
+ throw Error("no exchange found for initiating a peer pull payment");
+ }
+
+ const exchangeBaseUrl = maybeExchangeBaseUrl;
+
+ await fetchFreshExchange(wex, exchangeBaseUrl);
+
+ const mergeReserveInfo = await getMergeReserveInfo(wex, {
+ exchangeBaseUrl: exchangeBaseUrl,
+ });
+
+ const pursePair = await wex.cryptoApi.createEddsaKeypair({});
+ const mergePair = await wex.cryptoApi.createEddsaKeypair({});
+
+ const contractTerms = req.partialContractTerms;
+
+ const hContractTerms = ContractTermsUtil.hashContractTerms(contractTerms);
+
+ const contractKeyPair = await wex.cryptoApi.createEddsaKeypair({});
+
+ const withdrawalGroupId = encodeCrock(getRandomBytes(32));
+
+ const mergeReserveRowId = mergeReserveInfo.rowId;
+ checkDbInvariant(!!mergeReserveRowId);
+
+ const contractEncNonce = encodeCrock(getRandomBytes(24));
+
+ const wi = await getExchangeWithdrawalInfo(
+ wex,
+ exchangeBaseUrl,
+ Amounts.parseOrThrow(req.partialContractTerms.amount),
+ undefined,
+ );
+
+ const mergeTimestamp = TalerPreciseTimestamp.now();
+
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["peerPullCredit", "contractTerms"] },
+ async (tx) => {
+ const ppi: PeerPullCreditRecord = {
+ amount: req.partialContractTerms.amount,
+ contractTermsHash: hContractTerms,
+ exchangeBaseUrl: exchangeBaseUrl,
+ pursePriv: pursePair.priv,
+ pursePub: pursePair.pub,
+ mergePriv: mergePair.priv,
+ mergePub: mergePair.pub,
+ status: PeerPullPaymentCreditStatus.PendingCreatePurse,
+ mergeTimestamp: timestampPreciseToDb(mergeTimestamp),
+ contractEncNonce,
+ mergeReserveRowId: mergeReserveRowId,
+ contractPriv: contractKeyPair.priv,
+ contractPub: contractKeyPair.pub,
+ withdrawalGroupId,
+ estimatedAmountEffective: wi.withdrawalAmountEffective,
+ };
+ await tx.peerPullCredit.put(ppi);
+ const oldTxState: TransactionState = {
+ major: TransactionMajorState.None,
+ };
+ const newTxState = computePeerPullCreditTransactionState(ppi);
+ await tx.contractTerms.put({
+ contractTermsRaw: contractTerms,
+ h: hContractTerms,
+ });
+ return { oldTxState, newTxState };
+ },
+ );
+
+ const ctx = new PeerPullCreditTransactionContext(wex, pursePair.pub);
+
+ notifyTransition(wex, ctx.transactionId, transitionInfo);
+ wex.taskScheduler.startShepherdTask(ctx.taskId);
+
+ // The pending-incoming balance has changed.
+ wex.ws.notify({
+ type: NotificationType.BalanceChange,
+ hintTransactionId: ctx.transactionId,
+ });
+
+ return {
+ talerUri: stringifyTalerUri({
+ type: TalerUriAction.PayPull,
+ exchangeBaseUrl: exchangeBaseUrl,
+ contractPriv: contractKeyPair.priv,
+ }),
+ transactionId: ctx.transactionId,
+ };
+}
+
+export function computePeerPullCreditTransactionState(
+ pullCreditRecord: PeerPullCreditRecord,
+): TransactionState {
+ switch (pullCreditRecord.status) {
+ case PeerPullPaymentCreditStatus.PendingCreatePurse:
+ return {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.CreatePurse,
+ };
+ case PeerPullPaymentCreditStatus.PendingMergeKycRequired:
+ return {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.MergeKycRequired,
+ };
+ case PeerPullPaymentCreditStatus.PendingReady:
+ return {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.Ready,
+ };
+ case PeerPullPaymentCreditStatus.Done:
+ return {
+ major: TransactionMajorState.Done,
+ };
+ case PeerPullPaymentCreditStatus.PendingWithdrawing:
+ return {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.Withdraw,
+ };
+ case PeerPullPaymentCreditStatus.SuspendedCreatePurse:
+ return {
+ major: TransactionMajorState.Suspended,
+ minor: TransactionMinorState.CreatePurse,
+ };
+ case PeerPullPaymentCreditStatus.SuspendedReady:
+ return {
+ major: TransactionMajorState.Suspended,
+ minor: TransactionMinorState.Ready,
+ };
+ case PeerPullPaymentCreditStatus.SuspendedWithdrawing:
+ return {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.Withdraw,
+ };
+ case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired:
+ return {
+ major: TransactionMajorState.Suspended,
+ minor: TransactionMinorState.MergeKycRequired,
+ };
+ case PeerPullPaymentCreditStatus.Aborted:
+ return {
+ major: TransactionMajorState.Aborted,
+ };
+ case PeerPullPaymentCreditStatus.AbortingDeletePurse:
+ return {
+ major: TransactionMajorState.Aborting,
+ minor: TransactionMinorState.DeletePurse,
+ };
+ case PeerPullPaymentCreditStatus.Failed:
+ return {
+ major: TransactionMajorState.Failed,
+ };
+ case PeerPullPaymentCreditStatus.Expired:
+ return {
+ major: TransactionMajorState.Expired,
+ };
+ case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse:
+ return {
+ major: TransactionMajorState.Aborting,
+ minor: TransactionMinorState.DeletePurse,
+ };
+ }
+}
+
+export function computePeerPullCreditTransactionActions(
+ pullCreditRecord: PeerPullCreditRecord,
+): TransactionAction[] {
+ switch (pullCreditRecord.status) {
+ case PeerPullPaymentCreditStatus.PendingCreatePurse:
+ return [TransactionAction.Abort, TransactionAction.Suspend];
+ case PeerPullPaymentCreditStatus.PendingMergeKycRequired:
+ return [TransactionAction.Abort, TransactionAction.Suspend];
+ case PeerPullPaymentCreditStatus.PendingReady:
+ return [TransactionAction.Abort, TransactionAction.Suspend];
+ case PeerPullPaymentCreditStatus.Done:
+ return [TransactionAction.Delete];
+ case PeerPullPaymentCreditStatus.PendingWithdrawing:
+ return [TransactionAction.Abort, TransactionAction.Suspend];
+ case PeerPullPaymentCreditStatus.SuspendedCreatePurse:
+ return [TransactionAction.Resume, TransactionAction.Abort];
+ case PeerPullPaymentCreditStatus.SuspendedReady:
+ return [TransactionAction.Abort, TransactionAction.Resume];
+ case PeerPullPaymentCreditStatus.SuspendedWithdrawing:
+ return [TransactionAction.Resume, TransactionAction.Fail];
+ case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired:
+ return [TransactionAction.Resume, TransactionAction.Fail];
+ case PeerPullPaymentCreditStatus.Aborted:
+ return [TransactionAction.Delete];
+ case PeerPullPaymentCreditStatus.AbortingDeletePurse:
+ return [TransactionAction.Suspend, TransactionAction.Fail];
+ case PeerPullPaymentCreditStatus.Failed:
+ return [TransactionAction.Delete];
+ case PeerPullPaymentCreditStatus.Expired:
+ return [TransactionAction.Delete];
+ case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse:
+ return [TransactionAction.Resume, TransactionAction.Fail];
+ }
+}
diff --git a/packages/taler-wallet-core/src/pay-peer-pull-debit.ts b/packages/taler-wallet-core/src/pay-peer-pull-debit.ts
new file mode 100644
index 000000000..0355b58ad
--- /dev/null
+++ b/packages/taler-wallet-core/src/pay-peer-pull-debit.ts
@@ -0,0 +1,1019 @@
+/*
+ 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/>
+ */
+
+/**
+ * @fileoverview
+ * Implementation of the peer-pull-debit transaction, i.e.
+ * paying for an invoice the wallet received from another wallet.
+ */
+
+/**
+ * Imports.
+ */
+import {
+ AcceptPeerPullPaymentResponse,
+ Amounts,
+ CoinRefreshRequest,
+ ConfirmPeerPullDebitRequest,
+ ContractTermsUtil,
+ ExchangePurseDeposits,
+ HttpStatusCode,
+ Logger,
+ NotificationType,
+ ObservabilityEventType,
+ PeerContractTerms,
+ PreparePeerPullDebitRequest,
+ PreparePeerPullDebitResponse,
+ RefreshReason,
+ SelectedProspectiveCoin,
+ TalerError,
+ TalerErrorCode,
+ TalerPreciseTimestamp,
+ TalerProtocolViolationError,
+ TransactionAction,
+ TransactionIdStr,
+ TransactionMajorState,
+ TransactionMinorState,
+ TransactionState,
+ TransactionType,
+ assertUnreachable,
+ checkLogicInvariant,
+ codecForAny,
+ codecForExchangeGetContractResponse,
+ codecForPeerContractTerms,
+ decodeCrock,
+ eddsaGetPublic,
+ encodeCrock,
+ getRandomBytes,
+ j2s,
+ parsePayPullUri,
+} from "@gnu-taler/taler-util";
+import {
+ HttpResponse,
+ readSuccessResponseJsonOrThrow,
+ readTalerErrorResponse,
+} from "@gnu-taler/taler-util/http";
+import { PreviousPayCoins, selectPeerCoins } from "./coinSelection.js";
+import {
+ PendingTaskType,
+ TaskIdStr,
+ TaskRunResult,
+ TaskRunResultType,
+ TransactionContext,
+ TransitionResultType,
+ constructTaskIdentifier,
+ spendCoins,
+} from "./common.js";
+import {
+ PeerPullDebitRecordStatus,
+ PeerPullPaymentIncomingRecord,
+ RefreshOperationStatus,
+ WalletStoresV1,
+ timestampPreciseToDb,
+} from "./db.js";
+import {
+ codecForExchangePurseStatus,
+ getTotalPeerPaymentCost,
+ queryCoinInfosForSelection,
+} from "./pay-peer-common.js";
+import { DbReadWriteTransaction, StoreNames } from "./query.js";
+import { createRefreshGroup } from "./refresh.js";
+import {
+ constructTransactionIdentifier,
+ notifyTransition,
+ parseTransactionIdentifier,
+} from "./transactions.js";
+import { WalletExecutionContext } from "./wallet.js";
+
+const logger = new Logger("pay-peer-pull-debit.ts");
+
+/**
+ * Common context for a peer-pull-debit transaction.
+ */
+export class PeerPullDebitTransactionContext implements TransactionContext {
+ wex: WalletExecutionContext;
+ readonly transactionId: TransactionIdStr;
+ readonly taskId: TaskIdStr;
+ peerPullDebitId: string;
+
+ constructor(wex: WalletExecutionContext, peerPullDebitId: string) {
+ this.wex = wex;
+ this.transactionId = constructTransactionIdentifier({
+ tag: TransactionType.PeerPullDebit,
+ peerPullDebitId,
+ });
+ this.taskId = constructTaskIdentifier({
+ tag: PendingTaskType.PeerPullDebit,
+ peerPullDebitId,
+ });
+ this.peerPullDebitId = peerPullDebitId;
+ }
+
+ async deleteTransaction(): Promise<void> {
+ const transactionId = this.transactionId;
+ const ws = this.wex;
+ const peerPullDebitId = this.peerPullDebitId;
+ await ws.db.runReadWriteTx(
+ { storeNames: ["peerPullDebit", "tombstones"] },
+ async (tx) => {
+ const debit = await tx.peerPullDebit.get(peerPullDebitId);
+ if (debit) {
+ await tx.peerPullDebit.delete(peerPullDebitId);
+ await tx.tombstones.put({ id: transactionId });
+ }
+ },
+ );
+ }
+
+ async suspendTransaction(): Promise<void> {
+ const taskId = this.taskId;
+ const transactionId = this.transactionId;
+ const wex = this.wex;
+ const peerPullDebitId = this.peerPullDebitId;
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["peerPullDebit"] },
+ async (tx) => {
+ const pullDebitRec = await tx.peerPullDebit.get(peerPullDebitId);
+ if (!pullDebitRec) {
+ logger.warn(`peer pull debit ${peerPullDebitId} not found`);
+ return;
+ }
+ let newStatus: PeerPullDebitRecordStatus | undefined = undefined;
+ switch (pullDebitRec.status) {
+ case PeerPullDebitRecordStatus.DialogProposed:
+ break;
+ case PeerPullDebitRecordStatus.Done:
+ break;
+ case PeerPullDebitRecordStatus.PendingDeposit:
+ newStatus = PeerPullDebitRecordStatus.SuspendedDeposit;
+ break;
+ case PeerPullDebitRecordStatus.SuspendedDeposit:
+ break;
+ case PeerPullDebitRecordStatus.Aborted:
+ break;
+ case PeerPullDebitRecordStatus.AbortingRefresh:
+ newStatus = PeerPullDebitRecordStatus.SuspendedAbortingRefresh;
+ break;
+ case PeerPullDebitRecordStatus.Failed:
+ break;
+ case PeerPullDebitRecordStatus.SuspendedAbortingRefresh:
+ break;
+ default:
+ assertUnreachable(pullDebitRec.status);
+ }
+ if (newStatus != null) {
+ const oldTxState = computePeerPullDebitTransactionState(pullDebitRec);
+ pullDebitRec.status = newStatus;
+ const newTxState = computePeerPullDebitTransactionState(pullDebitRec);
+ await tx.peerPullDebit.put(pullDebitRec);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ return undefined;
+ },
+ );
+ notifyTransition(wex, transactionId, transitionInfo);
+ wex.taskScheduler.stopShepherdTask(taskId);
+ }
+
+ async resumeTransaction(): Promise<void> {
+ const ctx = this;
+ await ctx.transition(async (pi) => {
+ switch (pi.status) {
+ case PeerPullDebitRecordStatus.SuspendedDeposit:
+ pi.status = PeerPullDebitRecordStatus.PendingDeposit;
+ return TransitionResultType.Transition;
+ case PeerPullDebitRecordStatus.SuspendedAbortingRefresh:
+ pi.status = PeerPullDebitRecordStatus.AbortingRefresh;
+ return TransitionResultType.Transition;
+ case PeerPullDebitRecordStatus.Aborted:
+ case PeerPullDebitRecordStatus.AbortingRefresh:
+ case PeerPullDebitRecordStatus.Failed:
+ case PeerPullDebitRecordStatus.DialogProposed:
+ case PeerPullDebitRecordStatus.Done:
+ case PeerPullDebitRecordStatus.PendingDeposit:
+ return TransitionResultType.Stay;
+ }
+ });
+ this.wex.taskScheduler.startShepherdTask(this.taskId);
+ }
+
+ async failTransaction(): Promise<void> {
+ const ctx = this;
+ await ctx.transition(async (pi) => {
+ switch (pi.status) {
+ case PeerPullDebitRecordStatus.SuspendedDeposit:
+ case PeerPullDebitRecordStatus.PendingDeposit:
+ case PeerPullDebitRecordStatus.AbortingRefresh:
+ case PeerPullDebitRecordStatus.SuspendedAbortingRefresh:
+ // FIXME: Should we also abort the corresponding refresh session?!
+ pi.status = PeerPullDebitRecordStatus.Failed;
+ return TransitionResultType.Transition;
+ default:
+ return TransitionResultType.Stay;
+ }
+ });
+ this.wex.taskScheduler.stopShepherdTask(this.taskId);
+ }
+
+ async abortTransaction(): Promise<void> {
+ const ctx = this;
+ await ctx.transitionExtra(
+ {
+ extraStores: [
+ "coinAvailability",
+ "denominations",
+ "refreshGroups",
+ "refreshSessions",
+ "coins",
+ "coinAvailability",
+ ],
+ },
+ async (pi, tx) => {
+ switch (pi.status) {
+ case PeerPullDebitRecordStatus.SuspendedDeposit:
+ case PeerPullDebitRecordStatus.PendingDeposit:
+ break;
+ default:
+ return TransitionResultType.Stay;
+ }
+ const currency = Amounts.currencyOf(pi.totalCostEstimated);
+ const coinPubs: CoinRefreshRequest[] = [];
+
+ if (!pi.coinSel) {
+ throw Error("invalid db state");
+ }
+
+ for (let i = 0; i < pi.coinSel.coinPubs.length; i++) {
+ coinPubs.push({
+ amount: pi.coinSel.contributions[i],
+ coinPub: pi.coinSel.coinPubs[i],
+ });
+ }
+
+ const refresh = await createRefreshGroup(
+ ctx.wex,
+ tx,
+ currency,
+ coinPubs,
+ RefreshReason.AbortPeerPullDebit,
+ this.transactionId,
+ );
+
+ pi.status = PeerPullDebitRecordStatus.AbortingRefresh;
+ pi.abortRefreshGroupId = refresh.refreshGroupId;
+ return TransitionResultType.Transition;
+ },
+ );
+ }
+
+ async transition(
+ f: (rec: PeerPullPaymentIncomingRecord) => Promise<TransitionResultType>,
+ ): Promise<void> {
+ return this.transitionExtra(
+ {
+ extraStores: [],
+ },
+ f,
+ );
+ }
+
+ async transitionExtra<
+ StoreNameArray extends Array<StoreNames<typeof WalletStoresV1>> = [],
+ >(
+ opts: { extraStores: StoreNameArray },
+ f: (
+ rec: PeerPullPaymentIncomingRecord,
+ tx: DbReadWriteTransaction<
+ typeof WalletStoresV1,
+ ["peerPullDebit", ...StoreNameArray]
+ >,
+ ) => Promise<TransitionResultType>,
+ ): Promise<void> {
+ const wex = this.wex;
+ const extraStores = opts.extraStores ?? [];
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["peerPullDebit", ...extraStores] },
+ async (tx) => {
+ const pi = await tx.peerPullDebit.get(this.peerPullDebitId);
+ if (!pi) {
+ throw Error("peer pull payment not found anymore");
+ }
+ const oldTxState = computePeerPullDebitTransactionState(pi);
+ const res = await f(pi, tx);
+ switch (res) {
+ case TransitionResultType.Transition: {
+ await tx.peerPullDebit.put(pi);
+ const newTxState = computePeerPullDebitTransactionState(pi);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ default:
+ return undefined;
+ }
+ },
+ );
+ wex.taskScheduler.stopShepherdTask(this.taskId);
+ notifyTransition(wex, this.transactionId, transitionInfo);
+ wex.taskScheduler.startShepherdTask(this.taskId);
+ }
+}
+
+async function handlePurseCreationConflict(
+ ctx: PeerPullDebitTransactionContext,
+ peerPullInc: PeerPullPaymentIncomingRecord,
+ resp: HttpResponse,
+): Promise<TaskRunResult> {
+ const ws = ctx.wex;
+ const errResp = await readTalerErrorResponse(resp);
+ if (errResp.code !== TalerErrorCode.EXCHANGE_GENERIC_INSUFFICIENT_FUNDS) {
+ await ctx.failTransaction();
+ return TaskRunResult.finished();
+ }
+
+ // FIXME: Properly parse!
+ const brokenCoinPub = (errResp as any).coin_pub;
+ logger.trace(`excluded broken coin pub=${brokenCoinPub}`);
+
+ if (!brokenCoinPub) {
+ // FIXME: Details!
+ throw new TalerProtocolViolationError();
+ }
+
+ const instructedAmount = Amounts.parseOrThrow(peerPullInc.amount);
+
+ const sel = peerPullInc.coinSel;
+ if (!sel) {
+ throw Error("invalid state (coin selection expected)");
+ }
+
+ const repair: PreviousPayCoins = [];
+
+ for (let i = 0; i < sel.coinPubs.length; i++) {
+ if (sel.coinPubs[i] != brokenCoinPub) {
+ repair.push({
+ coinPub: sel.coinPubs[i],
+ contribution: Amounts.parseOrThrow(sel.contributions[i]),
+ });
+ }
+ }
+
+ const coinSelRes = await selectPeerCoins(ws, {
+ instructedAmount,
+ repair,
+ });
+
+ switch (coinSelRes.type) {
+ case "failure":
+ // FIXME: Details!
+ throw Error(
+ "insufficient balance to re-select coins to repair double spending",
+ );
+ case "prospective":
+ throw Error(
+ "insufficient balance to re-select coins to repair double spending (blocked on refresh)",
+ );
+ case "success":
+ break;
+ default:
+ assertUnreachable(coinSelRes);
+ }
+
+ const totalAmount = await getTotalPeerPaymentCost(
+ ws,
+ coinSelRes.result.coins,
+ );
+
+ await ws.db.runReadWriteTx({ storeNames: ["peerPullDebit"] }, async (tx) => {
+ const myPpi = await tx.peerPullDebit.get(peerPullInc.peerPullDebitId);
+ if (!myPpi) {
+ return;
+ }
+ switch (myPpi.status) {
+ case PeerPullDebitRecordStatus.PendingDeposit:
+ case PeerPullDebitRecordStatus.SuspendedDeposit: {
+ const sel = coinSelRes.result;
+ myPpi.coinSel = {
+ coinPubs: sel.coins.map((x) => x.coinPub),
+ contributions: sel.coins.map((x) => x.contribution),
+ totalCost: Amounts.stringify(totalAmount),
+ };
+ break;
+ }
+ default:
+ return;
+ }
+ await tx.peerPullDebit.put(myPpi);
+ });
+ return TaskRunResult.backoff();
+}
+
+async function processPeerPullDebitPendingDeposit(
+ wex: WalletExecutionContext,
+ peerPullInc: PeerPullPaymentIncomingRecord,
+): Promise<TaskRunResult> {
+ const ctx = new PeerPullDebitTransactionContext(
+ wex,
+ peerPullInc.peerPullDebitId,
+ );
+
+ const pursePub = peerPullInc.pursePub;
+
+ const coinSel = peerPullInc.coinSel;
+
+ if (!coinSel) {
+ const instructedAmount = Amounts.parseOrThrow(peerPullInc.amount);
+
+ const coinSelRes = await selectPeerCoins(wex, {
+ instructedAmount,
+ });
+ if (logger.shouldLogTrace()) {
+ logger.trace(`selected p2p coins (pull): ${j2s(coinSelRes)}`);
+ }
+
+ 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":
+ throw Error("insufficient balance (locked behind refresh)");
+ case "success":
+ coins = coinSelRes.result.coins;
+ break;
+ default:
+ assertUnreachable(coinSelRes);
+ }
+
+ const peerPullDebitId = peerPullInc.peerPullDebitId;
+ const totalAmount = await getTotalPeerPaymentCost(wex, coins);
+
+ // FIXME: Missing notification here!
+
+ const transitionDone = await wex.db.runReadWriteTx(
+ {
+ storeNames: [
+ "exchanges",
+ "coins",
+ "denominations",
+ "refreshGroups",
+ "refreshSessions",
+ "peerPullDebit",
+ "coinAvailability",
+ ],
+ },
+ async (tx) => {
+ const pi = await tx.peerPullDebit.get(peerPullDebitId);
+ if (!pi) {
+ return false;
+ }
+ if (pi.status !== PeerPullDebitRecordStatus.PendingDeposit) {
+ return false;
+ }
+ if (pi.coinSel) {
+ return false;
+ }
+ await spendCoins(wex, tx, {
+ // allocationId: `txn:peer-pull-debit:${req.peerPullDebitId}`,
+ allocationId: constructTransactionIdentifier({
+ tag: TransactionType.PeerPullDebit,
+ peerPullDebitId,
+ }),
+ coinPubs: coinSelRes.result.coins.map((x) => x.coinPub),
+ contributions: coinSelRes.result.coins.map((x) =>
+ Amounts.parseOrThrow(x.contribution),
+ ),
+ refreshReason: RefreshReason.PayPeerPull,
+ });
+ pi.coinSel = {
+ coinPubs: coinSelRes.result.coins.map((x) => x.coinPub),
+ contributions: coinSelRes.result.coins.map((x) => x.contribution),
+ totalCost: Amounts.stringify(totalAmount),
+ };
+ await tx.peerPullDebit.put(pi);
+ return true;
+ },
+ );
+ if (transitionDone) {
+ return TaskRunResult.progress();
+ } else {
+ return TaskRunResult.backoff();
+ }
+ }
+
+ const purseDepositUrl = new URL(
+ `purses/${pursePub}/deposit`,
+ peerPullInc.exchangeBaseUrl,
+ );
+
+ // FIXME: We could skip batches that we've already submitted.
+
+ const coins = await queryCoinInfosForSelection(wex, coinSel);
+
+ const maxBatchSize = 100;
+
+ for (let i = 0; i < coins.length; i += maxBatchSize) {
+ const batchSize = Math.min(maxBatchSize, coins.length - i);
+
+ wex.oc.observe({
+ type: ObservabilityEventType.Message,
+ contents: `Depositing batch at ${i}/${coins.length} of size ${batchSize}`,
+ });
+
+ const batchCoins = coins.slice(i, i + batchSize);
+ const depositSigsResp = await wex.cryptoApi.signPurseDeposits({
+ exchangeBaseUrl: peerPullInc.exchangeBaseUrl,
+ pursePub: peerPullInc.pursePub,
+ coins: batchCoins,
+ });
+
+ const depositPayload: ExchangePurseDeposits = {
+ deposits: depositSigsResp.deposits,
+ };
+
+ if (logger.shouldLogTrace()) {
+ logger.trace(`purse deposit payload: ${j2s(depositPayload)}`);
+ }
+
+ const httpResp = await wex.http.fetch(purseDepositUrl.href, {
+ method: "POST",
+ body: depositPayload,
+ cancellationToken: wex.cancellationToken,
+ });
+
+ switch (httpResp.status) {
+ case HttpStatusCode.Ok: {
+ const resp = await readSuccessResponseJsonOrThrow(
+ httpResp,
+ codecForAny(),
+ );
+ logger.trace(`purse deposit response: ${j2s(resp)}`);
+ continue;
+ }
+ case HttpStatusCode.Gone: {
+ await ctx.abortTransaction();
+ return TaskRunResult.backoff();
+ }
+ case HttpStatusCode.Conflict: {
+ return handlePurseCreationConflict(ctx, peerPullInc, httpResp);
+ }
+ default: {
+ const errResp = await readTalerErrorResponse(httpResp);
+ return {
+ type: TaskRunResultType.Error,
+ errorDetail: errResp,
+ };
+ }
+ }
+ }
+
+ // All batches succeeded, we can transition!
+
+ await ctx.transition(async (r) => {
+ if (r.status !== PeerPullDebitRecordStatus.PendingDeposit) {
+ return TransitionResultType.Stay;
+ }
+ r.status = PeerPullDebitRecordStatus.Done;
+ return TransitionResultType.Transition;
+ });
+ return TaskRunResult.finished();
+}
+
+async function processPeerPullDebitAbortingRefresh(
+ wex: WalletExecutionContext,
+ peerPullInc: PeerPullPaymentIncomingRecord,
+): Promise<TaskRunResult> {
+ const peerPullDebitId = peerPullInc.peerPullDebitId;
+ const abortRefreshGroupId = peerPullInc.abortRefreshGroupId;
+ checkLogicInvariant(!!abortRefreshGroupId);
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.PeerPullDebit,
+ peerPullDebitId,
+ });
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["peerPullDebit", "refreshGroups"] },
+ async (tx) => {
+ const refreshGroup = await tx.refreshGroups.get(abortRefreshGroupId);
+ let newOpState: PeerPullDebitRecordStatus | undefined;
+ if (!refreshGroup) {
+ // Maybe it got manually deleted? Means that we should
+ // just go into failed.
+ logger.warn("no aborting refresh group found for deposit group");
+ newOpState = PeerPullDebitRecordStatus.Failed;
+ } else {
+ if (refreshGroup.operationStatus === RefreshOperationStatus.Finished) {
+ newOpState = PeerPullDebitRecordStatus.Aborted;
+ } else if (
+ refreshGroup.operationStatus === RefreshOperationStatus.Failed
+ ) {
+ newOpState = PeerPullDebitRecordStatus.Failed;
+ }
+ }
+ if (newOpState) {
+ const newDg = await tx.peerPullDebit.get(peerPullDebitId);
+ if (!newDg) {
+ return;
+ }
+ const oldTxState = computePeerPullDebitTransactionState(newDg);
+ newDg.status = newOpState;
+ const newTxState = computePeerPullDebitTransactionState(newDg);
+ await tx.peerPullDebit.put(newDg);
+ return { oldTxState, newTxState };
+ }
+ return undefined;
+ },
+ );
+ notifyTransition(wex, transactionId, transitionInfo);
+ // FIXME: Shouldn't this be finished in some cases?!
+ return TaskRunResult.backoff();
+}
+
+export async function processPeerPullDebit(
+ wex: WalletExecutionContext,
+ peerPullDebitId: string,
+): Promise<TaskRunResult> {
+ const peerPullInc = await wex.db.runReadOnlyTx(
+ { storeNames: ["peerPullDebit"] },
+ async (tx) => {
+ return tx.peerPullDebit.get(peerPullDebitId);
+ },
+ );
+ if (!peerPullInc) {
+ throw Error("peer pull debit not found");
+ }
+
+ switch (peerPullInc.status) {
+ case PeerPullDebitRecordStatus.PendingDeposit:
+ return await processPeerPullDebitPendingDeposit(wex, peerPullInc);
+ case PeerPullDebitRecordStatus.AbortingRefresh:
+ return await processPeerPullDebitAbortingRefresh(wex, peerPullInc);
+ }
+ return TaskRunResult.finished();
+}
+
+export async function confirmPeerPullDebit(
+ wex: WalletExecutionContext,
+ req: ConfirmPeerPullDebitRequest,
+): Promise<AcceptPeerPullPaymentResponse> {
+ let peerPullDebitId: string;
+ const parsedTx = parseTransactionIdentifier(req.transactionId);
+ if (!parsedTx || parsedTx.tag !== TransactionType.PeerPullDebit) {
+ throw Error("invalid peer-pull-debit transaction identifier");
+ }
+ peerPullDebitId = parsedTx.peerPullDebitId;
+
+ const peerPullInc = await wex.db.runReadOnlyTx(
+ { storeNames: ["peerPullDebit"] },
+ async (tx) => {
+ return tx.peerPullDebit.get(peerPullDebitId);
+ },
+ );
+
+ if (!peerPullInc) {
+ throw Error(
+ `can't accept unknown incoming p2p pull payment (${req.transactionId})`,
+ );
+ }
+
+ const instructedAmount = Amounts.parseOrThrow(peerPullInc.amount);
+
+ const coinSelRes = await selectPeerCoins(wex, {
+ instructedAmount,
+ });
+ if (logger.shouldLogTrace()) {
+ logger.trace(`selected p2p coins (pull): ${j2s(coinSelRes)}`);
+ }
+
+ 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 totalAmount = await getTotalPeerPaymentCost(wex, coins);
+
+ // FIXME: Missing notification here!
+
+ await wex.db.runReadWriteTx(
+ {
+ storeNames: [
+ "exchanges",
+ "coins",
+ "denominations",
+ "refreshGroups",
+ "refreshSessions",
+ "peerPullDebit",
+ "coinAvailability",
+ ],
+ },
+ async (tx) => {
+ const pi = await tx.peerPullDebit.get(peerPullDebitId);
+ if (!pi) {
+ throw Error();
+ }
+ if (pi.status !== PeerPullDebitRecordStatus.DialogProposed) {
+ return;
+ }
+ if (coinSelRes.type == "success") {
+ await spendCoins(wex, tx, {
+ // allocationId: `txn:peer-pull-debit:${req.peerPullDebitId}`,
+ allocationId: constructTransactionIdentifier({
+ tag: TransactionType.PeerPullDebit,
+ peerPullDebitId,
+ }),
+ coinPubs: coinSelRes.result.coins.map((x) => x.coinPub),
+ contributions: coinSelRes.result.coins.map((x) =>
+ Amounts.parseOrThrow(x.contribution),
+ ),
+ refreshReason: RefreshReason.PayPeerPull,
+ });
+ pi.coinSel = {
+ coinPubs: coinSelRes.result.coins.map((x) => x.coinPub),
+ contributions: coinSelRes.result.coins.map((x) => x.contribution),
+ totalCost: Amounts.stringify(totalAmount),
+ };
+ }
+ pi.status = PeerPullDebitRecordStatus.PendingDeposit;
+ await tx.peerPullDebit.put(pi);
+ },
+ );
+
+ const ctx = new PeerPullDebitTransactionContext(wex, peerPullDebitId);
+
+ const transactionId = ctx.transactionId;
+
+ wex.ws.notify({
+ type: NotificationType.BalanceChange,
+ hintTransactionId: transactionId,
+ });
+
+ wex.taskScheduler.startShepherdTask(ctx.taskId);
+
+ return {
+ transactionId,
+ };
+}
+
+/**
+ * Look up information about an incoming peer pull payment.
+ * Store the results in the wallet DB.
+ */
+export async function preparePeerPullDebit(
+ wex: WalletExecutionContext,
+ req: PreparePeerPullDebitRequest,
+): Promise<PreparePeerPullDebitResponse> {
+ const uri = parsePayPullUri(req.talerUri);
+
+ if (!uri) {
+ throw Error("got invalid taler://pay-pull URI");
+ }
+
+ const existing = await wex.db.runReadOnlyTx(
+ { storeNames: ["peerPullDebit", "contractTerms"] },
+ async (tx) => {
+ const peerPullDebitRecord =
+ await tx.peerPullDebit.indexes.byExchangeAndContractPriv.get([
+ uri.exchangeBaseUrl,
+ uri.contractPriv,
+ ]);
+ if (!peerPullDebitRecord) {
+ return;
+ }
+ const contractTerms = await tx.contractTerms.get(
+ peerPullDebitRecord.contractTermsHash,
+ );
+ if (!contractTerms) {
+ return;
+ }
+ return { peerPullDebitRecord, contractTerms };
+ },
+ );
+
+ if (existing) {
+ return {
+ amount: existing.peerPullDebitRecord.amount,
+ amountRaw: existing.peerPullDebitRecord.amount,
+ amountEffective: existing.peerPullDebitRecord.totalCostEstimated,
+ contractTerms: existing.contractTerms.contractTermsRaw,
+ peerPullDebitId: existing.peerPullDebitRecord.peerPullDebitId,
+ transactionId: constructTransactionIdentifier({
+ tag: TransactionType.PeerPullDebit,
+ peerPullDebitId: existing.peerPullDebitRecord.peerPullDebitId,
+ }),
+ };
+ }
+
+ const exchangeBaseUrl = uri.exchangeBaseUrl;
+ const contractPriv = uri.contractPriv;
+ const contractPub = encodeCrock(eddsaGetPublic(decodeCrock(contractPriv)));
+
+ const getContractUrl = new URL(`contracts/${contractPub}`, exchangeBaseUrl);
+
+ const contractHttpResp = await wex.http.fetch(getContractUrl.href);
+
+ const contractResp = await readSuccessResponseJsonOrThrow(
+ contractHttpResp,
+ codecForExchangeGetContractResponse(),
+ );
+
+ const pursePub = contractResp.purse_pub;
+
+ const dec = await wex.cryptoApi.decryptContractForDeposit({
+ ciphertext: contractResp.econtract,
+ contractPriv: contractPriv,
+ pursePub: pursePub,
+ });
+
+ const getPurseUrl = new URL(`purses/${pursePub}/merge`, exchangeBaseUrl);
+
+ const purseHttpResp = await wex.http.fetch(getPurseUrl.href);
+
+ const purseStatus = await readSuccessResponseJsonOrThrow(
+ purseHttpResp,
+ codecForExchangePurseStatus(),
+ );
+
+ const peerPullDebitId = encodeCrock(getRandomBytes(32));
+
+ let contractTerms: PeerContractTerms;
+
+ if (dec.contractTerms) {
+ contractTerms = codecForPeerContractTerms().decode(dec.contractTerms);
+ // FIXME: Check that the purseStatus balance matches contract terms amount
+ } else {
+ // FIXME: In this case, where do we get the purse expiration from?!
+ // https://bugs.gnunet.org/view.php?id=7706
+ throw Error("pull payments without contract terms not supported yet");
+ }
+
+ const contractTermsHash = ContractTermsUtil.hashContractTerms(contractTerms);
+
+ // FIXME: Why don't we compute the totalCost here?!
+
+ const instructedAmount = Amounts.parseOrThrow(contractTerms.amount);
+
+ const coinSelRes = await selectPeerCoins(wex, {
+ instructedAmount,
+ });
+ if (logger.shouldLogTrace()) {
+ logger.trace(`selected p2p coins (pull): ${j2s(coinSelRes)}`);
+ }
+
+ 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 totalAmount = await getTotalPeerPaymentCost(wex, coins);
+
+ await wex.db.runReadWriteTx(
+ { storeNames: ["peerPullDebit", "contractTerms"] },
+ async (tx) => {
+ await tx.contractTerms.put({
+ h: contractTermsHash,
+ contractTermsRaw: contractTerms,
+ }),
+ await tx.peerPullDebit.add({
+ peerPullDebitId,
+ contractPriv: contractPriv,
+ exchangeBaseUrl: exchangeBaseUrl,
+ pursePub: pursePub,
+ timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()),
+ contractTermsHash,
+ amount: contractTerms.amount,
+ status: PeerPullDebitRecordStatus.DialogProposed,
+ totalCostEstimated: Amounts.stringify(totalAmount),
+ });
+ },
+ );
+
+ return {
+ amount: contractTerms.amount,
+ amountEffective: Amounts.stringify(totalAmount),
+ amountRaw: contractTerms.amount,
+ contractTerms: contractTerms,
+ peerPullDebitId,
+ transactionId: constructTransactionIdentifier({
+ tag: TransactionType.PeerPullDebit,
+ peerPullDebitId: peerPullDebitId,
+ }),
+ };
+}
+
+export function computePeerPullDebitTransactionState(
+ pullDebitRecord: PeerPullPaymentIncomingRecord,
+): TransactionState {
+ switch (pullDebitRecord.status) {
+ case PeerPullDebitRecordStatus.DialogProposed:
+ return {
+ major: TransactionMajorState.Dialog,
+ minor: TransactionMinorState.Proposed,
+ };
+ case PeerPullDebitRecordStatus.PendingDeposit:
+ return {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.Deposit,
+ };
+ case PeerPullDebitRecordStatus.Done:
+ return {
+ major: TransactionMajorState.Done,
+ };
+ case PeerPullDebitRecordStatus.SuspendedDeposit:
+ return {
+ major: TransactionMajorState.Suspended,
+ minor: TransactionMinorState.Deposit,
+ };
+ case PeerPullDebitRecordStatus.Aborted:
+ return {
+ major: TransactionMajorState.Aborted,
+ };
+ case PeerPullDebitRecordStatus.AbortingRefresh:
+ return {
+ major: TransactionMajorState.Aborting,
+ minor: TransactionMinorState.Refresh,
+ };
+ case PeerPullDebitRecordStatus.Failed:
+ return {
+ major: TransactionMajorState.Failed,
+ };
+ case PeerPullDebitRecordStatus.SuspendedAbortingRefresh:
+ return {
+ major: TransactionMajorState.SuspendedAborting,
+ minor: TransactionMinorState.Refresh,
+ };
+ }
+}
+
+export function computePeerPullDebitTransactionActions(
+ pullDebitRecord: PeerPullPaymentIncomingRecord,
+): TransactionAction[] {
+ switch (pullDebitRecord.status) {
+ case PeerPullDebitRecordStatus.DialogProposed:
+ return [];
+ case PeerPullDebitRecordStatus.PendingDeposit:
+ return [TransactionAction.Abort, TransactionAction.Suspend];
+ case PeerPullDebitRecordStatus.Done:
+ return [TransactionAction.Delete];
+ case PeerPullDebitRecordStatus.SuspendedDeposit:
+ return [TransactionAction.Resume, TransactionAction.Abort];
+ case PeerPullDebitRecordStatus.Aborted:
+ return [TransactionAction.Delete];
+ case PeerPullDebitRecordStatus.AbortingRefresh:
+ return [TransactionAction.Fail, TransactionAction.Suspend];
+ case PeerPullDebitRecordStatus.Failed:
+ return [TransactionAction.Delete];
+ case PeerPullDebitRecordStatus.SuspendedAbortingRefresh:
+ return [TransactionAction.Resume, TransactionAction.Fail];
+ }
+}
diff --git a/packages/taler-wallet-core/src/pay-peer-push-credit.ts b/packages/taler-wallet-core/src/pay-peer-push-credit.ts
new file mode 100644
index 000000000..93f1a63a7
--- /dev/null
+++ b/packages/taler-wallet-core/src/pay-peer-push-credit.ts
@@ -0,0 +1,1036 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received 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 {
+ AcceptPeerPushPaymentResponse,
+ Amounts,
+ ConfirmPeerPushCreditRequest,
+ ContractTermsUtil,
+ ExchangePurseMergeRequest,
+ HttpStatusCode,
+ Logger,
+ NotificationType,
+ PeerContractTerms,
+ PreparePeerPushCreditRequest,
+ PreparePeerPushCreditResponse,
+ TalerErrorCode,
+ TalerPreciseTimestamp,
+ TalerProtocolTimestamp,
+ TransactionAction,
+ TransactionIdStr,
+ TransactionMajorState,
+ TransactionMinorState,
+ TransactionState,
+ TransactionType,
+ WalletAccountMergeFlags,
+ WalletKycUuid,
+ assertUnreachable,
+ checkDbInvariant,
+ codecForAny,
+ codecForExchangeGetContractResponse,
+ codecForPeerContractTerms,
+ codecForWalletKycUuid,
+ decodeCrock,
+ eddsaGetPublic,
+ encodeCrock,
+ getRandomBytes,
+ j2s,
+ makeErrorDetail,
+ parsePayPushUri,
+ talerPaytoFromExchangeReserve,
+} from "@gnu-taler/taler-util";
+import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
+import {
+ PendingTaskType,
+ TaskIdStr,
+ TaskRunResult,
+ TaskRunResultType,
+ TombstoneTag,
+ TransactionContext,
+ constructTaskIdentifier,
+} from "./common.js";
+import {
+ KycPendingInfo,
+ KycUserType,
+ PeerPushCreditStatus,
+ PeerPushPaymentIncomingRecord,
+ WithdrawalGroupStatus,
+ WithdrawalRecordType,
+ timestampPreciseToDb,
+} from "./db.js";
+import { fetchFreshExchange } from "./exchanges.js";
+import {
+ codecForExchangePurseStatus,
+ getMergeReserveInfo,
+} from "./pay-peer-common.js";
+import {
+ TransitionInfo,
+ constructTransactionIdentifier,
+ notifyTransition,
+ parseTransactionIdentifier,
+} from "./transactions.js";
+import { WalletExecutionContext } from "./wallet.js";
+import {
+ PerformCreateWithdrawalGroupResult,
+ getExchangeWithdrawalInfo,
+ internalPerformCreateWithdrawalGroup,
+ internalPrepareCreateWithdrawalGroup,
+ waitWithdrawalFinal,
+} from "./withdraw.js";
+
+const logger = new Logger("pay-peer-push-credit.ts");
+
+export class PeerPushCreditTransactionContext implements TransactionContext {
+ readonly transactionId: TransactionIdStr;
+ readonly taskId: TaskIdStr;
+
+ constructor(
+ public wex: WalletExecutionContext,
+ public peerPushCreditId: string,
+ ) {
+ this.transactionId = constructTransactionIdentifier({
+ tag: TransactionType.PeerPushCredit,
+ peerPushCreditId,
+ });
+ this.taskId = constructTaskIdentifier({
+ tag: PendingTaskType.PeerPushCredit,
+ peerPushCreditId,
+ });
+ }
+
+ async deleteTransaction(): Promise<void> {
+ const { wex, peerPushCreditId } = this;
+ await wex.db.runReadWriteTx(
+ { storeNames: ["withdrawalGroups", "peerPushCredit", "tombstones"] },
+ async (tx) => {
+ const pushInc = await tx.peerPushCredit.get(peerPushCreditId);
+ if (!pushInc) {
+ return;
+ }
+ if (pushInc.withdrawalGroupId) {
+ const withdrawalGroupId = pushInc.withdrawalGroupId;
+ const withdrawalGroupRecord =
+ await tx.withdrawalGroups.get(withdrawalGroupId);
+ if (withdrawalGroupRecord) {
+ await tx.withdrawalGroups.delete(withdrawalGroupId);
+ await tx.tombstones.put({
+ id: TombstoneTag.DeleteWithdrawalGroup + ":" + withdrawalGroupId,
+ });
+ }
+ }
+ await tx.peerPushCredit.delete(peerPushCreditId);
+ await tx.tombstones.put({
+ id: TombstoneTag.DeletePeerPushCredit + ":" + peerPushCreditId,
+ });
+ },
+ );
+ return;
+ }
+
+ async suspendTransaction(): Promise<void> {
+ const { wex, peerPushCreditId, taskId: retryTag, transactionId } = this;
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["peerPushCredit"] },
+ async (tx) => {
+ const pushCreditRec = await tx.peerPushCredit.get(peerPushCreditId);
+ if (!pushCreditRec) {
+ logger.warn(`peer push credit ${peerPushCreditId} not found`);
+ return;
+ }
+ let newStatus: PeerPushCreditStatus | undefined = undefined;
+ switch (pushCreditRec.status) {
+ case PeerPushCreditStatus.DialogProposed:
+ case PeerPushCreditStatus.Done:
+ case PeerPushCreditStatus.SuspendedMerge:
+ case PeerPushCreditStatus.SuspendedMergeKycRequired:
+ case PeerPushCreditStatus.SuspendedWithdrawing:
+ break;
+ case PeerPushCreditStatus.PendingMergeKycRequired:
+ newStatus = PeerPushCreditStatus.SuspendedMergeKycRequired;
+ break;
+ case PeerPushCreditStatus.PendingMerge:
+ newStatus = PeerPushCreditStatus.SuspendedMerge;
+ break;
+ case PeerPushCreditStatus.PendingWithdrawing:
+ // FIXME: Suspend internal withdrawal transaction!
+ newStatus = PeerPushCreditStatus.SuspendedWithdrawing;
+ break;
+ case PeerPushCreditStatus.Aborted:
+ break;
+ case PeerPushCreditStatus.Failed:
+ break;
+ default:
+ assertUnreachable(pushCreditRec.status);
+ }
+ if (newStatus != null) {
+ const oldTxState =
+ computePeerPushCreditTransactionState(pushCreditRec);
+ pushCreditRec.status = newStatus;
+ const newTxState =
+ computePeerPushCreditTransactionState(pushCreditRec);
+ await tx.peerPushCredit.put(pushCreditRec);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ return undefined;
+ },
+ );
+ notifyTransition(wex, transactionId, transitionInfo);
+ wex.taskScheduler.stopShepherdTask(retryTag);
+ }
+
+ async abortTransaction(): Promise<void> {
+ const { wex, peerPushCreditId, taskId: retryTag, transactionId } = this;
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["peerPushCredit"] },
+ async (tx) => {
+ const pushCreditRec = await tx.peerPushCredit.get(peerPushCreditId);
+ if (!pushCreditRec) {
+ logger.warn(`peer push credit ${peerPushCreditId} not found`);
+ return;
+ }
+ let newStatus: PeerPushCreditStatus | undefined = undefined;
+ switch (pushCreditRec.status) {
+ case PeerPushCreditStatus.DialogProposed:
+ newStatus = PeerPushCreditStatus.Aborted;
+ break;
+ case PeerPushCreditStatus.Done:
+ break;
+ case PeerPushCreditStatus.SuspendedMerge:
+ case PeerPushCreditStatus.SuspendedMergeKycRequired:
+ case PeerPushCreditStatus.SuspendedWithdrawing:
+ newStatus = PeerPushCreditStatus.Aborted;
+ break;
+ case PeerPushCreditStatus.PendingMergeKycRequired:
+ newStatus = PeerPushCreditStatus.Aborted;
+ break;
+ case PeerPushCreditStatus.PendingMerge:
+ newStatus = PeerPushCreditStatus.Aborted;
+ break;
+ case PeerPushCreditStatus.PendingWithdrawing:
+ newStatus = PeerPushCreditStatus.Aborted;
+ break;
+ case PeerPushCreditStatus.Aborted:
+ break;
+ case PeerPushCreditStatus.Failed:
+ break;
+ default:
+ assertUnreachable(pushCreditRec.status);
+ }
+ if (newStatus != null) {
+ const oldTxState =
+ computePeerPushCreditTransactionState(pushCreditRec);
+ pushCreditRec.status = newStatus;
+ const newTxState =
+ computePeerPushCreditTransactionState(pushCreditRec);
+ await tx.peerPushCredit.put(pushCreditRec);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ return undefined;
+ },
+ );
+ notifyTransition(wex, transactionId, transitionInfo);
+ wex.taskScheduler.startShepherdTask(retryTag);
+ }
+
+ async resumeTransaction(): Promise<void> {
+ const { wex, peerPushCreditId, taskId: retryTag, transactionId } = this;
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["peerPushCredit"] },
+ async (tx) => {
+ const pushCreditRec = await tx.peerPushCredit.get(peerPushCreditId);
+ if (!pushCreditRec) {
+ logger.warn(`peer push credit ${peerPushCreditId} not found`);
+ return;
+ }
+ let newStatus: PeerPushCreditStatus | undefined = undefined;
+ switch (pushCreditRec.status) {
+ case PeerPushCreditStatus.DialogProposed:
+ case PeerPushCreditStatus.Done:
+ case PeerPushCreditStatus.PendingMergeKycRequired:
+ case PeerPushCreditStatus.PendingMerge:
+ case PeerPushCreditStatus.PendingWithdrawing:
+ case PeerPushCreditStatus.SuspendedMerge:
+ newStatus = PeerPushCreditStatus.PendingMerge;
+ break;
+ case PeerPushCreditStatus.SuspendedMergeKycRequired:
+ newStatus = PeerPushCreditStatus.PendingMergeKycRequired;
+ break;
+ case PeerPushCreditStatus.SuspendedWithdrawing:
+ // FIXME: resume underlying "internal-withdrawal" transaction.
+ newStatus = PeerPushCreditStatus.PendingWithdrawing;
+ break;
+ case PeerPushCreditStatus.Aborted:
+ break;
+ case PeerPushCreditStatus.Failed:
+ break;
+ default:
+ assertUnreachable(pushCreditRec.status);
+ }
+ if (newStatus != null) {
+ const oldTxState =
+ computePeerPushCreditTransactionState(pushCreditRec);
+ pushCreditRec.status = newStatus;
+ const newTxState =
+ computePeerPushCreditTransactionState(pushCreditRec);
+ await tx.peerPushCredit.put(pushCreditRec);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ return undefined;
+ },
+ );
+ notifyTransition(wex, transactionId, transitionInfo);
+ wex.taskScheduler.startShepherdTask(retryTag);
+ }
+
+ async failTransaction(): Promise<void> {
+ const { wex, peerPushCreditId, taskId: retryTag, transactionId } = this;
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["peerPushCredit"] },
+ async (tx) => {
+ const pushCreditRec = await tx.peerPushCredit.get(peerPushCreditId);
+ if (!pushCreditRec) {
+ logger.warn(`peer push credit ${peerPushCreditId} not found`);
+ return;
+ }
+ let newStatus: PeerPushCreditStatus | undefined = undefined;
+ switch (pushCreditRec.status) {
+ case PeerPushCreditStatus.Done:
+ case PeerPushCreditStatus.Aborted:
+ case PeerPushCreditStatus.Failed:
+ // Already in a final state.
+ return;
+ case PeerPushCreditStatus.DialogProposed:
+ case PeerPushCreditStatus.PendingMergeKycRequired:
+ case PeerPushCreditStatus.PendingMerge:
+ case PeerPushCreditStatus.PendingWithdrawing:
+ case PeerPushCreditStatus.SuspendedMerge:
+ case PeerPushCreditStatus.SuspendedMergeKycRequired:
+ case PeerPushCreditStatus.SuspendedWithdrawing:
+ newStatus = PeerPushCreditStatus.Failed;
+ break;
+ default:
+ assertUnreachable(pushCreditRec.status);
+ }
+ if (newStatus != null) {
+ const oldTxState =
+ computePeerPushCreditTransactionState(pushCreditRec);
+ pushCreditRec.status = newStatus;
+ const newTxState =
+ computePeerPushCreditTransactionState(pushCreditRec);
+ await tx.peerPushCredit.put(pushCreditRec);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ return undefined;
+ },
+ );
+ wex.taskScheduler.stopShepherdTask(retryTag);
+ notifyTransition(wex, transactionId, transitionInfo);
+ wex.taskScheduler.startShepherdTask(retryTag);
+ }
+}
+
+export async function preparePeerPushCredit(
+ wex: WalletExecutionContext,
+ req: PreparePeerPushCreditRequest,
+): Promise<PreparePeerPushCreditResponse> {
+ const uri = parsePayPushUri(req.talerUri);
+
+ if (!uri) {
+ throw Error("got invalid taler://pay-push URI");
+ }
+
+ const existing = await wex.db.runReadOnlyTx(
+ { storeNames: ["contractTerms", "peerPushCredit"] },
+ async (tx) => {
+ const existingPushInc =
+ await tx.peerPushCredit.indexes.byExchangeAndContractPriv.get([
+ uri.exchangeBaseUrl,
+ uri.contractPriv,
+ ]);
+ if (!existingPushInc) {
+ return;
+ }
+ const existingContractTermsRec = await tx.contractTerms.get(
+ existingPushInc.contractTermsHash,
+ );
+ if (!existingContractTermsRec) {
+ throw Error(
+ "contract terms for peer push payment credit not found in database",
+ );
+ }
+ const existingContractTerms = codecForPeerContractTerms().decode(
+ existingContractTermsRec.contractTermsRaw,
+ );
+ return { existingPushInc, existingContractTerms };
+ },
+ );
+
+ if (existing) {
+ return {
+ amount: existing.existingContractTerms.amount,
+ amountEffective: existing.existingPushInc.estimatedAmountEffective,
+ amountRaw: existing.existingContractTerms.amount,
+ contractTerms: existing.existingContractTerms,
+ peerPushCreditId: existing.existingPushInc.peerPushCreditId,
+ transactionId: constructTransactionIdentifier({
+ tag: TransactionType.PeerPushCredit,
+ peerPushCreditId: existing.existingPushInc.peerPushCreditId,
+ }),
+ exchangeBaseUrl: existing.existingPushInc.exchangeBaseUrl,
+ };
+ }
+
+ const exchangeBaseUrl = uri.exchangeBaseUrl;
+
+ await fetchFreshExchange(wex, exchangeBaseUrl);
+
+ const contractPriv = uri.contractPriv;
+ const contractPub = encodeCrock(eddsaGetPublic(decodeCrock(contractPriv)));
+
+ const getContractUrl = new URL(`contracts/${contractPub}`, exchangeBaseUrl);
+
+ const contractHttpResp = await wex.http.fetch(getContractUrl.href);
+
+ const contractResp = await readSuccessResponseJsonOrThrow(
+ contractHttpResp,
+ codecForExchangeGetContractResponse(),
+ );
+
+ const pursePub = contractResp.purse_pub;
+
+ const dec = await wex.cryptoApi.decryptContractForMerge({
+ ciphertext: contractResp.econtract,
+ contractPriv: contractPriv,
+ pursePub: pursePub,
+ });
+
+ const getPurseUrl = new URL(`purses/${pursePub}/deposit`, exchangeBaseUrl);
+
+ const purseHttpResp = await wex.http.fetch(getPurseUrl.href);
+
+ const contractTerms = codecForPeerContractTerms().decode(dec.contractTerms);
+
+ const purseStatus = await readSuccessResponseJsonOrThrow(
+ purseHttpResp,
+ codecForExchangePurseStatus(),
+ );
+
+ logger.info(
+ `peer push credit, purse balance ${purseStatus.balance}, contract amount ${contractTerms.amount}`,
+ );
+
+ const peerPushCreditId = encodeCrock(getRandomBytes(32));
+
+ const contractTermsHash = ContractTermsUtil.hashContractTerms(
+ dec.contractTerms,
+ );
+
+ const withdrawalGroupId = encodeCrock(getRandomBytes(32));
+
+ const wi = await getExchangeWithdrawalInfo(
+ wex,
+ exchangeBaseUrl,
+ Amounts.parseOrThrow(purseStatus.balance),
+ undefined,
+ );
+
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["contractTerms", "peerPushCredit"] },
+ async (tx) => {
+ const rec: PeerPushPaymentIncomingRecord = {
+ peerPushCreditId,
+ contractPriv: contractPriv,
+ exchangeBaseUrl: exchangeBaseUrl,
+ mergePriv: dec.mergePriv,
+ pursePub: pursePub,
+ timestamp: timestampPreciseToDb(TalerPreciseTimestamp.now()),
+ contractTermsHash,
+ status: PeerPushCreditStatus.DialogProposed,
+ withdrawalGroupId,
+ currency: Amounts.currencyOf(purseStatus.balance),
+ estimatedAmountEffective: Amounts.stringify(
+ wi.withdrawalAmountEffective,
+ ),
+ };
+ await tx.peerPushCredit.add(rec);
+ await tx.contractTerms.put({
+ h: contractTermsHash,
+ contractTermsRaw: dec.contractTerms,
+ });
+
+ const newTxState = computePeerPushCreditTransactionState(rec);
+
+ return {
+ oldTxState: {
+ major: TransactionMajorState.None,
+ },
+ newTxState,
+ } satisfies TransitionInfo;
+ },
+ );
+
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.PeerPushCredit,
+ peerPushCreditId,
+ });
+
+ notifyTransition(wex, transactionId, transitionInfo);
+
+ wex.ws.notify({
+ type: NotificationType.BalanceChange,
+ hintTransactionId: transactionId,
+ });
+
+ return {
+ amount: purseStatus.balance,
+ amountEffective: wi.withdrawalAmountEffective,
+ amountRaw: purseStatus.balance,
+ contractTerms: dec.contractTerms,
+ peerPushCreditId,
+ transactionId,
+ exchangeBaseUrl,
+ };
+}
+
+async function longpollKycStatus(
+ wex: WalletExecutionContext,
+ peerPushCreditId: string,
+ exchangeUrl: string,
+ kycInfo: KycPendingInfo,
+ userType: KycUserType,
+): Promise<TaskRunResult> {
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.PeerPushCredit,
+ peerPushCreditId,
+ });
+ const url = new URL(
+ `kyc-check/${kycInfo.requirementRow}/${kycInfo.paytoHash}/${userType}`,
+ exchangeUrl,
+ );
+ url.searchParams.set("timeout_ms", "30000");
+ logger.info(`kyc url ${url.href}`);
+ const kycStatusRes = await wex.http.fetch(url.href, {
+ method: "GET",
+ cancellationToken: wex.cancellationToken,
+ });
+ if (
+ kycStatusRes.status === HttpStatusCode.Ok ||
+ //FIXME: NoContent is not expected https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-merge
+ // remove after the exchange is fixed or clarified
+ kycStatusRes.status === HttpStatusCode.NoContent
+ ) {
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["peerPushCredit"] },
+ async (tx) => {
+ const peerInc = await tx.peerPushCredit.get(peerPushCreditId);
+ if (!peerInc) {
+ return;
+ }
+ if (peerInc.status !== PeerPushCreditStatus.PendingMergeKycRequired) {
+ return;
+ }
+ const oldTxState = computePeerPushCreditTransactionState(peerInc);
+ peerInc.status = PeerPushCreditStatus.PendingMerge;
+ const newTxState = computePeerPushCreditTransactionState(peerInc);
+ await tx.peerPushCredit.put(peerInc);
+ return { oldTxState, newTxState };
+ },
+ );
+ notifyTransition(wex, transactionId, transitionInfo);
+ return TaskRunResult.progress();
+ } else if (kycStatusRes.status === HttpStatusCode.Accepted) {
+ // FIXME: Do we have to update the URL here?
+ return TaskRunResult.longpollReturnedPending();
+ } else {
+ throw Error(`unexpected response from kyc-check (${kycStatusRes.status})`);
+ }
+}
+
+async function processPeerPushCreditKycRequired(
+ wex: WalletExecutionContext,
+ peerInc: PeerPushPaymentIncomingRecord,
+ kycPending: WalletKycUuid,
+): Promise<TaskRunResult> {
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.PeerPushCredit,
+ peerPushCreditId: peerInc.peerPushCreditId,
+ });
+ const { peerPushCreditId } = peerInc;
+
+ const userType = "individual";
+ const url = new URL(
+ `kyc-check/${kycPending.requirement_row}/${kycPending.h_payto}/${userType}`,
+ peerInc.exchangeBaseUrl,
+ );
+
+ logger.info(`kyc url ${url.href}`);
+ const kycStatusRes = 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
+ // remove after the exchange is fixed or clarified
+ kycStatusRes.status === HttpStatusCode.NoContent
+ ) {
+ logger.warn("kyc requested, but already fulfilled");
+ return TaskRunResult.finished();
+ } else if (kycStatusRes.status === HttpStatusCode.Accepted) {
+ const kycStatus = await kycStatusRes.json();
+ logger.info(`kyc status: ${j2s(kycStatus)}`);
+ const { transitionInfo, result } = await wex.db.runReadWriteTx(
+ { storeNames: ["peerPushCredit"] },
+ async (tx) => {
+ const peerInc = await tx.peerPushCredit.get(peerPushCreditId);
+ if (!peerInc) {
+ return {
+ transitionInfo: undefined,
+ result: TaskRunResult.finished(),
+ };
+ }
+ const oldTxState = computePeerPushCreditTransactionState(peerInc);
+ peerInc.kycInfo = {
+ paytoHash: kycPending.h_payto,
+ requirementRow: kycPending.requirement_row,
+ };
+ peerInc.kycUrl = kycStatus.kyc_url;
+ peerInc.status = PeerPushCreditStatus.PendingMergeKycRequired;
+ const newTxState = computePeerPushCreditTransactionState(peerInc);
+ await tx.peerPushCredit.put(peerInc);
+ // We'll remove this eventually! New clients should rely on the
+ // kycUrl field of the transaction, not the error code.
+ const res: TaskRunResult = {
+ type: TaskRunResultType.Error,
+ errorDetail: makeErrorDetail(
+ TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED,
+ {
+ kycUrl: kycStatus.kyc_url,
+ },
+ ),
+ };
+ return {
+ transitionInfo: { oldTxState, newTxState },
+ result: res,
+ };
+ },
+ );
+ notifyTransition(wex, transactionId, transitionInfo);
+ return result;
+ } else {
+ throw Error(`unexpected response from kyc-check (${kycStatusRes.status})`);
+ }
+}
+
+async function handlePendingMerge(
+ wex: WalletExecutionContext,
+ peerInc: PeerPushPaymentIncomingRecord,
+ contractTerms: PeerContractTerms,
+): Promise<TaskRunResult> {
+ const { peerPushCreditId } = peerInc;
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.PeerPushCredit,
+ peerPushCreditId,
+ });
+
+ const amount = Amounts.parseOrThrow(contractTerms.amount);
+
+ const mergeReserveInfo = await getMergeReserveInfo(wex, {
+ exchangeBaseUrl: peerInc.exchangeBaseUrl,
+ });
+
+ const mergeTimestamp = TalerProtocolTimestamp.now();
+
+ const reservePayto = talerPaytoFromExchangeReserve(
+ peerInc.exchangeBaseUrl,
+ mergeReserveInfo.reservePub,
+ );
+
+ const sigRes = await wex.cryptoApi.signPurseMerge({
+ contractTermsHash: ContractTermsUtil.hashContractTerms(contractTerms),
+ flags: WalletAccountMergeFlags.MergeFullyPaidPurse,
+ mergePriv: peerInc.mergePriv,
+ mergeTimestamp: mergeTimestamp,
+ purseAmount: Amounts.stringify(amount),
+ purseExpiration: contractTerms.purse_expiration,
+ purseFee: Amounts.stringify(Amounts.zeroOfCurrency(amount.currency)),
+ pursePub: peerInc.pursePub,
+ reservePayto,
+ reservePriv: mergeReserveInfo.reservePriv,
+ });
+
+ const mergePurseUrl = new URL(
+ `purses/${peerInc.pursePub}/merge`,
+ peerInc.exchangeBaseUrl,
+ );
+
+ const mergeReq: ExchangePurseMergeRequest = {
+ payto_uri: reservePayto,
+ merge_timestamp: mergeTimestamp,
+ merge_sig: sigRes.mergeSig,
+ reserve_sig: sigRes.accountSig,
+ };
+
+ const mergeHttpResp = await wex.http.fetch(mergePurseUrl.href, {
+ method: "POST",
+ body: mergeReq,
+ });
+
+ if (mergeHttpResp.status === HttpStatusCode.UnavailableForLegalReasons) {
+ const respJson = await mergeHttpResp.json();
+ const kycPending = codecForWalletKycUuid().decode(respJson);
+ logger.info(`kyc uuid response: ${j2s(kycPending)}`);
+ return processPeerPushCreditKycRequired(wex, peerInc, kycPending);
+ }
+
+ logger.trace(`merge request: ${j2s(mergeReq)}`);
+ const res = await readSuccessResponseJsonOrThrow(
+ mergeHttpResp,
+ codecForAny(),
+ );
+ logger.trace(`merge response: ${j2s(res)}`);
+
+ const withdrawalGroupPrep = await internalPrepareCreateWithdrawalGroup(wex, {
+ amount,
+ wgInfo: {
+ withdrawalType: WithdrawalRecordType.PeerPushCredit,
+ },
+ forcedWithdrawalGroupId: peerInc.withdrawalGroupId,
+ exchangeBaseUrl: peerInc.exchangeBaseUrl,
+ reserveStatus: WithdrawalGroupStatus.PendingQueryingStatus,
+ reserveKeyPair: {
+ priv: mergeReserveInfo.reservePriv,
+ pub: mergeReserveInfo.reservePub,
+ },
+ });
+
+ const txRes = await wex.db.runReadWriteTx(
+ {
+ storeNames: [
+ "contractTerms",
+ "peerPushCredit",
+ "withdrawalGroups",
+ "reserves",
+ "exchanges",
+ "exchangeDetails",
+ ],
+ },
+ async (tx) => {
+ const peerInc = await tx.peerPushCredit.get(peerPushCreditId);
+ if (!peerInc) {
+ return undefined;
+ }
+ const oldTxState = computePeerPushCreditTransactionState(peerInc);
+ let wgCreateRes: PerformCreateWithdrawalGroupResult | undefined =
+ undefined;
+ switch (peerInc.status) {
+ case PeerPushCreditStatus.PendingMerge:
+ case PeerPushCreditStatus.PendingMergeKycRequired: {
+ peerInc.status = PeerPushCreditStatus.PendingWithdrawing;
+ wgCreateRes = await internalPerformCreateWithdrawalGroup(
+ wex,
+ tx,
+ withdrawalGroupPrep,
+ );
+ peerInc.withdrawalGroupId =
+ wgCreateRes.withdrawalGroup.withdrawalGroupId;
+ break;
+ }
+ }
+ await tx.peerPushCredit.put(peerInc);
+ const newTxState = computePeerPushCreditTransactionState(peerInc);
+ return {
+ peerPushCreditTransition: { oldTxState, newTxState },
+ wgCreateRes,
+ };
+ },
+ );
+ // Transaction was committed, now we can emit notifications.
+ if (txRes?.wgCreateRes?.exchangeNotif) {
+ wex.ws.notify(txRes.wgCreateRes.exchangeNotif);
+ }
+ notifyTransition(
+ wex,
+ withdrawalGroupPrep.transactionId,
+ txRes?.wgCreateRes?.transitionInfo,
+ );
+ notifyTransition(wex, transactionId, txRes?.peerPushCreditTransition);
+
+ return TaskRunResult.backoff();
+}
+
+async function handlePendingWithdrawing(
+ wex: WalletExecutionContext,
+ peerInc: PeerPushPaymentIncomingRecord,
+): Promise<TaskRunResult> {
+ if (!peerInc.withdrawalGroupId) {
+ throw Error("invalid db state (withdrawing, but no withdrawal group ID");
+ }
+ await waitWithdrawalFinal(wex, peerInc.withdrawalGroupId);
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.PeerPushCredit,
+ peerPushCreditId: peerInc.peerPushCreditId,
+ });
+ const wgId = peerInc.withdrawalGroupId;
+ let finished: boolean = false;
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["peerPushCredit", "withdrawalGroups"] },
+ async (tx) => {
+ const ppi = await tx.peerPushCredit.get(peerInc.peerPushCreditId);
+ if (!ppi) {
+ finished = true;
+ return;
+ }
+ if (ppi.status !== PeerPushCreditStatus.PendingWithdrawing) {
+ finished = true;
+ return;
+ }
+ const oldTxState = computePeerPushCreditTransactionState(ppi);
+ const wg = await tx.withdrawalGroups.get(wgId);
+ if (!wg) {
+ // FIXME: Fail the operation instead?
+ return undefined;
+ }
+ switch (wg.status) {
+ case WithdrawalGroupStatus.Done:
+ finished = true;
+ ppi.status = PeerPushCreditStatus.Done;
+ break;
+ // FIXME: Also handle other final states!
+ }
+ await tx.peerPushCredit.put(ppi);
+ const newTxState = computePeerPushCreditTransactionState(ppi);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ },
+ );
+ notifyTransition(wex, transactionId, transitionInfo);
+ if (finished) {
+ return TaskRunResult.finished();
+ } else {
+ // FIXME: Return indicator that we depend on the other operation!
+ return TaskRunResult.backoff();
+ }
+}
+
+export async function processPeerPushCredit(
+ wex: WalletExecutionContext,
+ peerPushCreditId: string,
+): Promise<TaskRunResult> {
+ let peerInc: PeerPushPaymentIncomingRecord | undefined;
+ let contractTerms: PeerContractTerms | undefined;
+ await wex.db.runReadWriteTx(
+ { storeNames: ["contractTerms", "peerPushCredit"] },
+ async (tx) => {
+ peerInc = await tx.peerPushCredit.get(peerPushCreditId);
+ if (!peerInc) {
+ return;
+ }
+ const ctRec = await tx.contractTerms.get(peerInc.contractTermsHash);
+ if (ctRec) {
+ contractTerms = ctRec.contractTermsRaw;
+ }
+ await tx.peerPushCredit.put(peerInc);
+ },
+ );
+
+ if (!peerInc) {
+ throw Error(
+ `can't accept unknown incoming p2p push payment (${peerPushCreditId})`,
+ );
+ }
+
+ logger.info(
+ `processing peerPushCredit in state ${peerInc.status.toString(16)}`,
+ );
+
+ checkDbInvariant(!!contractTerms);
+
+ switch (peerInc.status) {
+ case PeerPushCreditStatus.PendingMergeKycRequired: {
+ if (!peerInc.kycInfo) {
+ throw Error("invalid state, kycInfo required");
+ }
+ return await longpollKycStatus(
+ wex,
+ peerPushCreditId,
+ peerInc.exchangeBaseUrl,
+ peerInc.kycInfo,
+ "individual",
+ );
+ }
+
+ case PeerPushCreditStatus.PendingMerge:
+ return handlePendingMerge(wex, peerInc, contractTerms);
+
+ case PeerPushCreditStatus.PendingWithdrawing:
+ return handlePendingWithdrawing(wex, peerInc);
+
+ default:
+ return TaskRunResult.finished();
+ }
+}
+
+export async function confirmPeerPushCredit(
+ wex: WalletExecutionContext,
+ req: ConfirmPeerPushCreditRequest,
+): Promise<AcceptPeerPushPaymentResponse> {
+ let peerInc: PeerPushPaymentIncomingRecord | undefined;
+ let peerPushCreditId: string;
+ const parsedTx = parseTransactionIdentifier(req.transactionId);
+ if (!parsedTx) {
+ throw Error("invalid transaction ID");
+ }
+ if (parsedTx.tag !== TransactionType.PeerPushCredit) {
+ throw Error("invalid transaction ID type");
+ }
+ peerPushCreditId = parsedTx.peerPushCreditId;
+
+ logger.trace(`confirming peer-push-credit ${peerPushCreditId}`);
+
+ await wex.db.runReadWriteTx(
+ { storeNames: ["contractTerms", "peerPushCredit"] },
+ async (tx) => {
+ peerInc = await tx.peerPushCredit.get(peerPushCreditId);
+ if (!peerInc) {
+ return;
+ }
+ if (peerInc.status === PeerPushCreditStatus.DialogProposed) {
+ peerInc.status = PeerPushCreditStatus.PendingMerge;
+ }
+ await tx.peerPushCredit.put(peerInc);
+ },
+ );
+
+ if (!peerInc) {
+ throw Error(
+ `can't accept unknown incoming p2p push payment (${req.transactionId})`,
+ );
+ }
+
+ const ctx = new PeerPushCreditTransactionContext(wex, peerPushCreditId);
+
+ wex.taskScheduler.startShepherdTask(ctx.taskId);
+
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.PeerPushCredit,
+ peerPushCreditId,
+ });
+
+ return {
+ transactionId,
+ };
+}
+
+export function computePeerPushCreditTransactionState(
+ pushCreditRecord: PeerPushPaymentIncomingRecord,
+): TransactionState {
+ switch (pushCreditRecord.status) {
+ case PeerPushCreditStatus.DialogProposed:
+ return {
+ major: TransactionMajorState.Dialog,
+ minor: TransactionMinorState.Proposed,
+ };
+ case PeerPushCreditStatus.PendingMerge:
+ return {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.Merge,
+ };
+ case PeerPushCreditStatus.Done:
+ return {
+ major: TransactionMajorState.Done,
+ };
+ case PeerPushCreditStatus.PendingMergeKycRequired:
+ return {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.KycRequired,
+ };
+ case PeerPushCreditStatus.PendingWithdrawing:
+ return {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.Withdraw,
+ };
+ case PeerPushCreditStatus.SuspendedMerge:
+ return {
+ major: TransactionMajorState.Suspended,
+ minor: TransactionMinorState.Merge,
+ };
+ case PeerPushCreditStatus.SuspendedMergeKycRequired:
+ return {
+ major: TransactionMajorState.Suspended,
+ minor: TransactionMinorState.MergeKycRequired,
+ };
+ case PeerPushCreditStatus.SuspendedWithdrawing:
+ return {
+ major: TransactionMajorState.Suspended,
+ minor: TransactionMinorState.Withdraw,
+ };
+ case PeerPushCreditStatus.Aborted:
+ return {
+ major: TransactionMajorState.Aborted,
+ };
+ case PeerPushCreditStatus.Failed:
+ return {
+ major: TransactionMajorState.Failed,
+ };
+ default:
+ assertUnreachable(pushCreditRecord.status);
+ }
+}
+
+export function computePeerPushCreditTransactionActions(
+ pushCreditRecord: PeerPushPaymentIncomingRecord,
+): TransactionAction[] {
+ switch (pushCreditRecord.status) {
+ case PeerPushCreditStatus.DialogProposed:
+ return [TransactionAction.Delete];
+ case PeerPushCreditStatus.PendingMerge:
+ return [TransactionAction.Abort, TransactionAction.Suspend];
+ case PeerPushCreditStatus.Done:
+ return [TransactionAction.Delete];
+ case PeerPushCreditStatus.PendingMergeKycRequired:
+ return [TransactionAction.Abort, TransactionAction.Suspend];
+ case PeerPushCreditStatus.PendingWithdrawing:
+ return [TransactionAction.Suspend, TransactionAction.Fail];
+ case PeerPushCreditStatus.SuspendedMerge:
+ return [TransactionAction.Resume, TransactionAction.Abort];
+ case PeerPushCreditStatus.SuspendedMergeKycRequired:
+ return [TransactionAction.Resume, TransactionAction.Abort];
+ case PeerPushCreditStatus.SuspendedWithdrawing:
+ return [TransactionAction.Resume, TransactionAction.Fail];
+ case PeerPushCreditStatus.Aborted:
+ return [TransactionAction.Delete];
+ case PeerPushCreditStatus.Failed:
+ return [TransactionAction.Delete];
+ default:
+ assertUnreachable(pushCreditRecord.status);
+ }
+}
diff --git a/packages/taler-wallet-core/src/pay-peer-push-debit.ts b/packages/taler-wallet-core/src/pay-peer-push-debit.ts
new file mode 100644
index 000000000..6452407ff
--- /dev/null
+++ b/packages/taler-wallet-core/src/pay-peer-push-debit.ts
@@ -0,0 +1,1322 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received 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,
+ CheckPeerPushDebitRequest,
+ CheckPeerPushDebitResponse,
+ CoinRefreshRequest,
+ ContractTermsUtil,
+ ExchangePurseDeposits,
+ HttpStatusCode,
+ InitiatePeerPushDebitRequest,
+ InitiatePeerPushDebitResponse,
+ Logger,
+ NotificationType,
+ RefreshReason,
+ SelectedProspectiveCoin,
+ TalerError,
+ TalerErrorCode,
+ TalerPreciseTimestamp,
+ TalerProtocolTimestamp,
+ TalerProtocolViolationError,
+ TransactionAction,
+ TransactionIdStr,
+ TransactionMajorState,
+ TransactionMinorState,
+ TransactionState,
+ TransactionType,
+ assertUnreachable,
+ checkDbInvariant,
+ checkLogicInvariant,
+ encodeCrock,
+ getRandomBytes,
+ j2s,
+} from "@gnu-taler/taler-util";
+import {
+ HttpResponse,
+ readSuccessResponseJsonOrThrow,
+ readTalerErrorResponse,
+} from "@gnu-taler/taler-util/http";
+import { PreviousPayCoins, selectPeerCoins } from "./coinSelection.js";
+import {
+ PendingTaskType,
+ TaskIdStr,
+ TaskRunResult,
+ TaskRunResultType,
+ TransactionContext,
+ constructTaskIdentifier,
+ spendCoins,
+} from "./common.js";
+import { EncryptContractRequest } from "./crypto/cryptoTypes.js";
+import {
+ PeerPushDebitRecord,
+ PeerPushDebitStatus,
+ RefreshOperationStatus,
+ timestampPreciseToDb,
+ timestampProtocolFromDb,
+ timestampProtocolToDb,
+} from "./db.js";
+import {
+ codecForExchangePurseStatus,
+ getTotalPeerPaymentCost,
+ queryCoinInfosForSelection,
+} from "./pay-peer-common.js";
+import { createRefreshGroup, waitRefreshFinal } from "./refresh.js";
+import {
+ constructTransactionIdentifier,
+ notifyTransition,
+} from "./transactions.js";
+import { WalletExecutionContext } from "./wallet.js";
+
+const logger = new Logger("pay-peer-push-debit.ts");
+
+export class PeerPushDebitTransactionContext implements TransactionContext {
+ readonly transactionId: TransactionIdStr;
+ readonly taskId: TaskIdStr;
+
+ constructor(
+ public wex: WalletExecutionContext,
+ public pursePub: string,
+ ) {
+ this.transactionId = constructTransactionIdentifier({
+ tag: TransactionType.PeerPushDebit,
+ pursePub,
+ });
+ this.taskId = constructTaskIdentifier({
+ tag: PendingTaskType.PeerPushDebit,
+ pursePub,
+ });
+ }
+
+ async deleteTransaction(): Promise<void> {
+ const { wex, pursePub, transactionId } = this;
+ await wex.db.runReadWriteTx(
+ { storeNames: ["peerPushDebit", "tombstones"] },
+ async (tx) => {
+ const debit = await tx.peerPushDebit.get(pursePub);
+ if (debit) {
+ await tx.peerPushDebit.delete(pursePub);
+ await tx.tombstones.put({ id: transactionId });
+ }
+ },
+ );
+ }
+
+ async suspendTransaction(): Promise<void> {
+ const { wex, pursePub, transactionId, taskId: retryTag } = this;
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["peerPushDebit"] },
+ async (tx) => {
+ const pushDebitRec = await tx.peerPushDebit.get(pursePub);
+ if (!pushDebitRec) {
+ logger.warn(`peer push debit ${pursePub} not found`);
+ return;
+ }
+ let newStatus: PeerPushDebitStatus | undefined = undefined;
+ switch (pushDebitRec.status) {
+ case PeerPushDebitStatus.PendingCreatePurse:
+ newStatus = PeerPushDebitStatus.SuspendedCreatePurse;
+ break;
+ case PeerPushDebitStatus.AbortingRefreshDeleted:
+ newStatus = PeerPushDebitStatus.SuspendedAbortingRefreshDeleted;
+ break;
+ case PeerPushDebitStatus.AbortingRefreshExpired:
+ newStatus = PeerPushDebitStatus.SuspendedAbortingRefreshExpired;
+ break;
+ case PeerPushDebitStatus.AbortingDeletePurse:
+ newStatus = PeerPushDebitStatus.SuspendedAbortingDeletePurse;
+ break;
+ case PeerPushDebitStatus.PendingReady:
+ newStatus = PeerPushDebitStatus.SuspendedReady;
+ break;
+ case PeerPushDebitStatus.SuspendedAbortingDeletePurse:
+ case PeerPushDebitStatus.SuspendedAbortingRefreshDeleted:
+ case PeerPushDebitStatus.SuspendedAbortingRefreshExpired:
+ case PeerPushDebitStatus.SuspendedReady:
+ case PeerPushDebitStatus.SuspendedCreatePurse:
+ case PeerPushDebitStatus.Done:
+ case PeerPushDebitStatus.Aborted:
+ case PeerPushDebitStatus.Failed:
+ case PeerPushDebitStatus.Expired:
+ // Do nothing
+ break;
+ default:
+ assertUnreachable(pushDebitRec.status);
+ }
+ if (newStatus != null) {
+ const oldTxState = computePeerPushDebitTransactionState(pushDebitRec);
+ pushDebitRec.status = newStatus;
+ const newTxState = computePeerPushDebitTransactionState(pushDebitRec);
+ await tx.peerPushDebit.put(pushDebitRec);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ return undefined;
+ },
+ );
+ wex.taskScheduler.stopShepherdTask(retryTag);
+ notifyTransition(wex, transactionId, transitionInfo);
+ }
+
+ async abortTransaction(): Promise<void> {
+ const { wex, pursePub, transactionId, taskId: retryTag } = this;
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["peerPushDebit"] },
+ async (tx) => {
+ const pushDebitRec = await tx.peerPushDebit.get(pursePub);
+ if (!pushDebitRec) {
+ logger.warn(`peer push debit ${pursePub} not found`);
+ return;
+ }
+ let newStatus: PeerPushDebitStatus | undefined = undefined;
+ switch (pushDebitRec.status) {
+ case PeerPushDebitStatus.PendingReady:
+ case PeerPushDebitStatus.SuspendedReady:
+ newStatus = PeerPushDebitStatus.AbortingDeletePurse;
+ break;
+ case PeerPushDebitStatus.SuspendedCreatePurse:
+ case PeerPushDebitStatus.PendingCreatePurse:
+ // Network request might already be in-flight!
+ newStatus = PeerPushDebitStatus.AbortingDeletePurse;
+ break;
+ case PeerPushDebitStatus.SuspendedAbortingRefreshDeleted:
+ case PeerPushDebitStatus.SuspendedAbortingDeletePurse:
+ case PeerPushDebitStatus.SuspendedAbortingRefreshExpired:
+ case PeerPushDebitStatus.AbortingRefreshDeleted:
+ case PeerPushDebitStatus.AbortingRefreshExpired:
+ case PeerPushDebitStatus.Done:
+ case PeerPushDebitStatus.AbortingDeletePurse:
+ case PeerPushDebitStatus.Aborted:
+ case PeerPushDebitStatus.Expired:
+ case PeerPushDebitStatus.Failed:
+ // Do nothing
+ break;
+ default:
+ assertUnreachable(pushDebitRec.status);
+ }
+ if (newStatus != null) {
+ const oldTxState = computePeerPushDebitTransactionState(pushDebitRec);
+ pushDebitRec.status = newStatus;
+ const newTxState = computePeerPushDebitTransactionState(pushDebitRec);
+ await tx.peerPushDebit.put(pushDebitRec);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ return undefined;
+ },
+ );
+ wex.taskScheduler.stopShepherdTask(retryTag);
+ notifyTransition(wex, transactionId, transitionInfo);
+ wex.taskScheduler.startShepherdTask(retryTag);
+ }
+
+ async resumeTransaction(): Promise<void> {
+ const { wex, pursePub, transactionId, taskId: retryTag } = this;
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["peerPushDebit"] },
+ async (tx) => {
+ const pushDebitRec = await tx.peerPushDebit.get(pursePub);
+ if (!pushDebitRec) {
+ logger.warn(`peer push debit ${pursePub} not found`);
+ return;
+ }
+ let newStatus: PeerPushDebitStatus | undefined = undefined;
+ switch (pushDebitRec.status) {
+ case PeerPushDebitStatus.SuspendedAbortingDeletePurse:
+ newStatus = PeerPushDebitStatus.AbortingDeletePurse;
+ break;
+ case PeerPushDebitStatus.SuspendedAbortingRefreshDeleted:
+ newStatus = PeerPushDebitStatus.AbortingRefreshDeleted;
+ break;
+ case PeerPushDebitStatus.SuspendedAbortingRefreshExpired:
+ newStatus = PeerPushDebitStatus.AbortingRefreshExpired;
+ break;
+ case PeerPushDebitStatus.SuspendedReady:
+ newStatus = PeerPushDebitStatus.PendingReady;
+ break;
+ case PeerPushDebitStatus.SuspendedCreatePurse:
+ newStatus = PeerPushDebitStatus.PendingCreatePurse;
+ break;
+ case PeerPushDebitStatus.PendingCreatePurse:
+ case PeerPushDebitStatus.AbortingRefreshDeleted:
+ case PeerPushDebitStatus.AbortingRefreshExpired:
+ case PeerPushDebitStatus.AbortingDeletePurse:
+ case PeerPushDebitStatus.PendingReady:
+ case PeerPushDebitStatus.Done:
+ case PeerPushDebitStatus.Aborted:
+ case PeerPushDebitStatus.Failed:
+ case PeerPushDebitStatus.Expired:
+ // Do nothing
+ break;
+ default:
+ assertUnreachable(pushDebitRec.status);
+ }
+ if (newStatus != null) {
+ const oldTxState = computePeerPushDebitTransactionState(pushDebitRec);
+ pushDebitRec.status = newStatus;
+ const newTxState = computePeerPushDebitTransactionState(pushDebitRec);
+ await tx.peerPushDebit.put(pushDebitRec);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ return undefined;
+ },
+ );
+ wex.taskScheduler.startShepherdTask(retryTag);
+ notifyTransition(wex, transactionId, transitionInfo);
+ }
+
+ async failTransaction(): Promise<void> {
+ const { wex, pursePub, transactionId, taskId: retryTag } = this;
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["peerPushDebit"] },
+ async (tx) => {
+ const pushDebitRec = await tx.peerPushDebit.get(pursePub);
+ if (!pushDebitRec) {
+ logger.warn(`peer push debit ${pursePub} not found`);
+ return;
+ }
+ let newStatus: PeerPushDebitStatus | undefined = undefined;
+ switch (pushDebitRec.status) {
+ case PeerPushDebitStatus.AbortingRefreshDeleted:
+ case PeerPushDebitStatus.SuspendedAbortingRefreshDeleted:
+ // FIXME: What to do about the refresh group?
+ newStatus = PeerPushDebitStatus.Failed;
+ break;
+ case PeerPushDebitStatus.AbortingDeletePurse:
+ case PeerPushDebitStatus.SuspendedAbortingDeletePurse:
+ case PeerPushDebitStatus.AbortingRefreshExpired:
+ case PeerPushDebitStatus.SuspendedAbortingRefreshExpired:
+ case PeerPushDebitStatus.PendingReady:
+ case PeerPushDebitStatus.SuspendedReady:
+ case PeerPushDebitStatus.SuspendedCreatePurse:
+ case PeerPushDebitStatus.PendingCreatePurse:
+ newStatus = PeerPushDebitStatus.Failed;
+ break;
+ case PeerPushDebitStatus.Done:
+ case PeerPushDebitStatus.Aborted:
+ case PeerPushDebitStatus.Failed:
+ case PeerPushDebitStatus.Expired:
+ // Do nothing
+ break;
+ default:
+ assertUnreachable(pushDebitRec.status);
+ }
+ if (newStatus != null) {
+ const oldTxState = computePeerPushDebitTransactionState(pushDebitRec);
+ pushDebitRec.status = newStatus;
+ const newTxState = computePeerPushDebitTransactionState(pushDebitRec);
+ await tx.peerPushDebit.put(pushDebitRec);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ return undefined;
+ },
+ );
+ wex.taskScheduler.stopShepherdTask(retryTag);
+ notifyTransition(wex, transactionId, transitionInfo);
+ wex.taskScheduler.startShepherdTask(retryTag);
+ }
+}
+
+export async function checkPeerPushDebit(
+ wex: WalletExecutionContext,
+ req: CheckPeerPushDebitRequest,
+): Promise<CheckPeerPushDebitResponse> {
+ const instructedAmount = Amounts.parseOrThrow(req.amount);
+ logger.trace(
+ `checking peer push debit for ${Amounts.stringify(instructedAmount)}`,
+ );
+ const coinSelRes = await selectPeerCoins(wex, {
+ instructedAmount,
+ });
+ let coins: SelectedProspectiveCoin[] | undefined = undefined;
+ switch (coinSelRes.type) {
+ case "failure":
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
+ {
+ insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
+ },
+ );
+ case "prospective":
+ coins = coinSelRes.result.prospectiveCoins;
+ break;
+ case "success":
+ coins = coinSelRes.result.coins;
+ break;
+ default:
+ assertUnreachable(coinSelRes);
+ }
+ logger.trace(`selected peer coins (len=${coins.length})`);
+ const totalAmount = await getTotalPeerPaymentCost(wex, coins);
+ logger.trace("computed total peer payment cost");
+ return {
+ exchangeBaseUrl: coinSelRes.result.exchangeBaseUrl,
+ amountEffective: Amounts.stringify(totalAmount),
+ amountRaw: req.amount,
+ maxExpirationDate: coinSelRes.result.maxExpirationDate,
+ };
+}
+
+async function handlePurseCreationConflict(
+ wex: WalletExecutionContext,
+ peerPushInitiation: PeerPushDebitRecord,
+ resp: HttpResponse,
+): Promise<TaskRunResult> {
+ const pursePub = peerPushInitiation.pursePub;
+ const errResp = await readTalerErrorResponse(resp);
+ const ctx = new PeerPushDebitTransactionContext(wex, pursePub);
+ if (errResp.code !== TalerErrorCode.EXCHANGE_GENERIC_INSUFFICIENT_FUNDS) {
+ await ctx.failTransaction();
+ return TaskRunResult.finished();
+ }
+
+ // FIXME: Properly parse!
+ const brokenCoinPub = (errResp as any).coin_pub;
+ logger.trace(`excluded broken coin pub=${brokenCoinPub}`);
+
+ if (!brokenCoinPub) {
+ // FIXME: Details!
+ throw new TalerProtocolViolationError();
+ }
+
+ const instructedAmount = Amounts.parseOrThrow(peerPushInitiation.amount);
+ const sel = peerPushInitiation.coinSel;
+
+ checkDbInvariant(!!sel);
+
+ const repair: PreviousPayCoins = [];
+
+ for (let i = 0; i < sel.coinPubs.length; i++) {
+ if (sel.coinPubs[i] != brokenCoinPub) {
+ repair.push({
+ coinPub: sel.coinPubs[i],
+ contribution: Amounts.parseOrThrow(sel.contributions[i]),
+ });
+ }
+ }
+
+ const coinSelRes = await selectPeerCoins(wex, {
+ instructedAmount,
+ repair,
+ });
+
+ switch (coinSelRes.type) {
+ case "failure":
+ case "prospective":
+ // FIXME: Details!
+ throw Error(
+ "insufficient balance to re-select coins to repair double spending",
+ );
+ case "success":
+ break;
+ default:
+ assertUnreachable(coinSelRes);
+ }
+
+ await wex.db.runReadWriteTx({ storeNames: ["peerPushDebit"] }, async (tx) => {
+ const myPpi = await tx.peerPushDebit.get(peerPushInitiation.pursePub);
+ if (!myPpi) {
+ return;
+ }
+ switch (myPpi.status) {
+ case PeerPushDebitStatus.PendingCreatePurse:
+ case PeerPushDebitStatus.SuspendedCreatePurse: {
+ const sel = coinSelRes.result;
+ myPpi.coinSel = {
+ coinPubs: sel.coins.map((x) => x.coinPub),
+ contributions: sel.coins.map((x) => x.contribution),
+ };
+ break;
+ }
+ default:
+ return;
+ }
+ await tx.peerPushDebit.put(myPpi);
+ });
+ return TaskRunResult.progress();
+}
+
+async function processPeerPushDebitCreateReserve(
+ wex: WalletExecutionContext,
+ peerPushInitiation: PeerPushDebitRecord,
+): Promise<TaskRunResult> {
+ const pursePub = peerPushInitiation.pursePub;
+ const purseExpiration = peerPushInitiation.purseExpiration;
+ const hContractTerms = peerPushInitiation.contractTermsHash;
+ const ctx = new PeerPushDebitTransactionContext(wex, pursePub);
+ const transactionId = ctx.transactionId;
+
+ logger.trace(`processing ${transactionId} pending(create-reserve)`);
+
+ const contractTermsRecord = await wex.db.runReadOnlyTx(
+ { storeNames: ["contractTerms"] },
+ async (tx) => {
+ return tx.contractTerms.get(hContractTerms);
+ },
+ );
+
+ if (!contractTermsRecord) {
+ throw Error(
+ `db invariant failed, contract terms for ${transactionId} missing`,
+ );
+ }
+
+ if (!peerPushInitiation.coinSel) {
+ const coinSelRes = await selectPeerCoins(wex, {
+ instructedAmount: Amounts.parseOrThrow(peerPushInitiation.amount),
+ });
+
+ switch (coinSelRes.type) {
+ case "failure":
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
+ {
+ insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
+ },
+ );
+ case "prospective":
+ throw Error("insufficient funds (blocked on refresh)");
+ case "success":
+ break;
+ default:
+ assertUnreachable(coinSelRes);
+ }
+ const transitionDone = await wex.db.runReadWriteTx(
+ {
+ storeNames: [
+ "exchanges",
+ "contractTerms",
+ "coins",
+ "coinAvailability",
+ "denominations",
+ "refreshGroups",
+ "refreshSessions",
+ "peerPushDebit",
+ ],
+ },
+ async (tx) => {
+ const ppi = await tx.peerPushDebit.get(pursePub);
+ if (!ppi) {
+ return false;
+ }
+ if (ppi.coinSel) {
+ return false;
+ }
+
+ ppi.coinSel = {
+ coinPubs: coinSelRes.result.coins.map((x) => x.coinPub),
+ contributions: coinSelRes.result.coins.map((x) => x.contribution),
+ };
+ // FIXME: Instead of directly doing a spendCoin here,
+ // 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,
+ }),
+ coinPubs: coinSelRes.result.coins.map((x) => x.coinPub),
+ contributions: coinSelRes.result.coins.map((x) =>
+ Amounts.parseOrThrow(x.contribution),
+ ),
+ refreshReason: RefreshReason.PayPeerPush,
+ });
+
+ await tx.peerPushDebit.put(ppi);
+ return true;
+ },
+ );
+ if (transitionDone) {
+ return TaskRunResult.progress();
+ }
+ return TaskRunResult.backoff();
+ }
+
+ const purseSigResp = await wex.cryptoApi.signPurseCreation({
+ hContractTerms,
+ mergePub: peerPushInitiation.mergePub,
+ minAge: 0,
+ purseAmount: peerPushInitiation.amount,
+ purseExpiration: timestampProtocolFromDb(purseExpiration),
+ pursePriv: peerPushInitiation.pursePriv,
+ });
+
+ const coins = await queryCoinInfosForSelection(
+ wex,
+ peerPushInitiation.coinSel,
+ );
+
+ const encryptContractRequest: EncryptContractRequest = {
+ contractTerms: contractTermsRecord.contractTermsRaw,
+ mergePriv: peerPushInitiation.mergePriv,
+ pursePriv: peerPushInitiation.pursePriv,
+ pursePub: peerPushInitiation.pursePub,
+ contractPriv: peerPushInitiation.contractPriv,
+ contractPub: peerPushInitiation.contractPub,
+ nonce: peerPushInitiation.contractEncNonce,
+ };
+
+ const econtractResp = await wex.cryptoApi.encryptContractForMerge(
+ encryptContractRequest,
+ );
+
+ const maxBatchSize = 100;
+
+ for (let i = 0; i < coins.length; i += maxBatchSize) {
+ const batchSize = Math.min(maxBatchSize, coins.length - i);
+ const batchCoins = coins.slice(i, i + batchSize);
+
+ const depositSigsResp = await wex.cryptoApi.signPurseDeposits({
+ exchangeBaseUrl: peerPushInitiation.exchangeBaseUrl,
+ pursePub: peerPushInitiation.pursePub,
+ coins: batchCoins,
+ });
+
+ if (i == 0) {
+ // First batch creates the purse!
+
+ logger.trace(`encrypt contract request: ${j2s(encryptContractRequest)}`);
+
+ const createPurseUrl = new URL(
+ `purses/${peerPushInitiation.pursePub}/create`,
+ peerPushInitiation.exchangeBaseUrl,
+ );
+
+ const reqBody = {
+ amount: peerPushInitiation.amount,
+ merge_pub: peerPushInitiation.mergePub,
+ purse_sig: purseSigResp.sig,
+ h_contract_terms: hContractTerms,
+ purse_expiration: timestampProtocolFromDb(purseExpiration),
+ deposits: depositSigsResp.deposits,
+ min_age: 0,
+ econtract: econtractResp.econtract,
+ };
+
+ if (logger.shouldLogTrace()) {
+ logger.trace(`request body: ${j2s(reqBody)}`);
+ }
+
+ const httpResp = await wex.http.fetch(createPurseUrl.href, {
+ method: "POST",
+ body: reqBody,
+ cancellationToken: wex.cancellationToken,
+ });
+
+ switch (httpResp.status) {
+ case HttpStatusCode.Ok:
+ // Possibly on to the next batch.
+ continue;
+ case HttpStatusCode.Forbidden: {
+ // FIXME: Store this error!
+ await ctx.failTransaction();
+ return TaskRunResult.finished();
+ }
+ case HttpStatusCode.Conflict: {
+ // Handle double-spending
+ return handlePurseCreationConflict(wex, peerPushInitiation, httpResp);
+ }
+ default: {
+ const errResp = await readTalerErrorResponse(httpResp);
+ return {
+ type: TaskRunResultType.Error,
+ errorDetail: errResp,
+ };
+ }
+ }
+ } else {
+ const purseDepositUrl = new URL(
+ `purses/${pursePub}/deposit`,
+ peerPushInitiation.exchangeBaseUrl,
+ );
+
+ const depositPayload: ExchangePurseDeposits = {
+ deposits: depositSigsResp.deposits,
+ };
+
+ const httpResp = await wex.http.fetch(purseDepositUrl.href, {
+ method: "POST",
+ body: depositPayload,
+ cancellationToken: wex.cancellationToken,
+ });
+
+ switch (httpResp.status) {
+ case HttpStatusCode.Ok:
+ // Possibly on to the next batch.
+ continue;
+ case HttpStatusCode.Forbidden: {
+ // FIXME: Store this error!
+ await ctx.failTransaction();
+ return TaskRunResult.finished();
+ }
+ case HttpStatusCode.Conflict: {
+ // Handle double-spending
+ return handlePurseCreationConflict(wex, peerPushInitiation, httpResp);
+ }
+ default: {
+ const errResp = await readTalerErrorResponse(httpResp);
+ return {
+ type: TaskRunResultType.Error,
+ errorDetail: errResp,
+ };
+ }
+ }
+ }
+ }
+
+ // All batches done!
+
+ await transitionPeerPushDebitTransaction(wex, pursePub, {
+ stFrom: PeerPushDebitStatus.PendingCreatePurse,
+ stTo: PeerPushDebitStatus.PendingReady,
+ });
+
+ return TaskRunResult.backoff();
+}
+
+async function processPeerPushDebitAbortingDeletePurse(
+ wex: WalletExecutionContext,
+ peerPushInitiation: PeerPushDebitRecord,
+): Promise<TaskRunResult> {
+ const { pursePub, pursePriv } = peerPushInitiation;
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.PeerPushDebit,
+ pursePub,
+ });
+
+ const sigResp = await wex.cryptoApi.signDeletePurse({
+ pursePriv,
+ });
+ const purseUrl = new URL(
+ `purses/${pursePub}`,
+ peerPushInitiation.exchangeBaseUrl,
+ );
+ const resp = await wex.http.fetch(purseUrl.href, {
+ method: "DELETE",
+ headers: {
+ "taler-purse-signature": sigResp.sig,
+ },
+ cancellationToken: wex.cancellationToken,
+ });
+ logger.info(`deleted purse with response status ${resp.status}`);
+
+ const transitionInfo = await wex.db.runReadWriteTx(
+ {
+ storeNames: [
+ "peerPushDebit",
+ "refreshGroups",
+ "refreshSessions",
+ "denominations",
+ "coinAvailability",
+ "coins",
+ ],
+ },
+ async (tx) => {
+ const ppiRec = await tx.peerPushDebit.get(pursePub);
+ if (!ppiRec) {
+ return undefined;
+ }
+ if (ppiRec.status !== PeerPushDebitStatus.AbortingDeletePurse) {
+ return undefined;
+ }
+ const currency = Amounts.currencyOf(ppiRec.amount);
+ const oldTxState = computePeerPushDebitTransactionState(ppiRec);
+ const coinPubs: CoinRefreshRequest[] = [];
+
+ if (!ppiRec.coinSel) {
+ return undefined;
+ }
+
+ for (let i = 0; i < ppiRec.coinSel.coinPubs.length; i++) {
+ coinPubs.push({
+ amount: ppiRec.coinSel.contributions[i],
+ coinPub: ppiRec.coinSel.coinPubs[i],
+ });
+ }
+
+ const refresh = await createRefreshGroup(
+ wex,
+ tx,
+ currency,
+ coinPubs,
+ RefreshReason.AbortPeerPushDebit,
+ transactionId,
+ );
+ ppiRec.status = PeerPushDebitStatus.AbortingRefreshDeleted;
+ ppiRec.abortRefreshGroupId = refresh.refreshGroupId;
+ await tx.peerPushDebit.put(ppiRec);
+ const newTxState = computePeerPushDebitTransactionState(ppiRec);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ },
+ );
+ notifyTransition(wex, transactionId, transitionInfo);
+
+ return TaskRunResult.backoff();
+}
+
+interface SimpleTransition {
+ stFrom: PeerPushDebitStatus;
+ stTo: PeerPushDebitStatus;
+}
+
+// FIXME: This should be a transition on the peer push debit transaction context!
+async function transitionPeerPushDebitTransaction(
+ wex: WalletExecutionContext,
+ pursePub: string,
+ transitionSpec: SimpleTransition,
+): Promise<void> {
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.PeerPushDebit,
+ pursePub,
+ });
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["peerPushDebit"] },
+ async (tx) => {
+ const ppiRec = await tx.peerPushDebit.get(pursePub);
+ if (!ppiRec) {
+ return undefined;
+ }
+ if (ppiRec.status !== transitionSpec.stFrom) {
+ return undefined;
+ }
+ const oldTxState = computePeerPushDebitTransactionState(ppiRec);
+ ppiRec.status = transitionSpec.stTo;
+ await tx.peerPushDebit.put(ppiRec);
+ const newTxState = computePeerPushDebitTransactionState(ppiRec);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ },
+ );
+ notifyTransition(wex, transactionId, transitionInfo);
+}
+
+async function processPeerPushDebitAbortingRefreshDeleted(
+ wex: WalletExecutionContext,
+ peerPushInitiation: PeerPushDebitRecord,
+): Promise<TaskRunResult> {
+ const pursePub = peerPushInitiation.pursePub;
+ const abortRefreshGroupId = peerPushInitiation.abortRefreshGroupId;
+ checkLogicInvariant(!!abortRefreshGroupId);
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.PeerPushDebit,
+ pursePub: peerPushInitiation.pursePub,
+ });
+ if (peerPushInitiation.abortRefreshGroupId) {
+ await waitRefreshFinal(wex, peerPushInitiation.abortRefreshGroupId);
+ }
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["refreshGroups", "peerPushDebit"] },
+ async (tx) => {
+ const refreshGroup = await tx.refreshGroups.get(abortRefreshGroupId);
+ let newOpState: PeerPushDebitStatus | undefined;
+ if (!refreshGroup) {
+ // Maybe it got manually deleted? Means that we should
+ // just go into failed.
+ logger.warn("no aborting refresh group found for deposit group");
+ newOpState = PeerPushDebitStatus.Failed;
+ } else {
+ if (refreshGroup.operationStatus === RefreshOperationStatus.Finished) {
+ newOpState = PeerPushDebitStatus.Aborted;
+ } else if (
+ refreshGroup.operationStatus === RefreshOperationStatus.Failed
+ ) {
+ newOpState = PeerPushDebitStatus.Failed;
+ }
+ }
+ if (newOpState) {
+ const newDg = await tx.peerPushDebit.get(pursePub);
+ if (!newDg) {
+ return;
+ }
+ const oldTxState = computePeerPushDebitTransactionState(newDg);
+ newDg.status = newOpState;
+ const newTxState = computePeerPushDebitTransactionState(newDg);
+ await tx.peerPushDebit.put(newDg);
+ return { oldTxState, newTxState };
+ }
+ return undefined;
+ },
+ );
+ notifyTransition(wex, transactionId, transitionInfo);
+ // FIXME: Shouldn't this be finished in some cases?!
+ return TaskRunResult.backoff();
+}
+
+async function processPeerPushDebitAbortingRefreshExpired(
+ wex: WalletExecutionContext,
+ peerPushInitiation: PeerPushDebitRecord,
+): Promise<TaskRunResult> {
+ const pursePub = peerPushInitiation.pursePub;
+ const abortRefreshGroupId = peerPushInitiation.abortRefreshGroupId;
+ checkLogicInvariant(!!abortRefreshGroupId);
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.PeerPushDebit,
+ pursePub: peerPushInitiation.pursePub,
+ });
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["peerPushDebit", "refreshGroups"] },
+ async (tx) => {
+ const refreshGroup = await tx.refreshGroups.get(abortRefreshGroupId);
+ let newOpState: PeerPushDebitStatus | undefined;
+ if (!refreshGroup) {
+ // Maybe it got manually deleted? Means that we should
+ // just go into failed.
+ logger.warn("no aborting refresh group found for deposit group");
+ newOpState = PeerPushDebitStatus.Failed;
+ } else {
+ if (refreshGroup.operationStatus === RefreshOperationStatus.Finished) {
+ newOpState = PeerPushDebitStatus.Expired;
+ } else if (
+ refreshGroup.operationStatus === RefreshOperationStatus.Failed
+ ) {
+ newOpState = PeerPushDebitStatus.Failed;
+ }
+ }
+ if (newOpState) {
+ const newDg = await tx.peerPushDebit.get(pursePub);
+ if (!newDg) {
+ return;
+ }
+ const oldTxState = computePeerPushDebitTransactionState(newDg);
+ newDg.status = newOpState;
+ const newTxState = computePeerPushDebitTransactionState(newDg);
+ await tx.peerPushDebit.put(newDg);
+ return { oldTxState, newTxState };
+ }
+ return undefined;
+ },
+ );
+ notifyTransition(wex, transactionId, transitionInfo);
+ // FIXME: Shouldn't this be finished in some cases?!
+ return TaskRunResult.backoff();
+}
+
+/**
+ * Process the "pending(ready)" state of a peer-push-debit transaction.
+ */
+async function processPeerPushDebitReady(
+ wex: WalletExecutionContext,
+ peerPushInitiation: PeerPushDebitRecord,
+): Promise<TaskRunResult> {
+ logger.trace("processing peer-push-debit pending(ready)");
+ const pursePub = peerPushInitiation.pursePub;
+ const transactionId = constructTaskIdentifier({
+ tag: PendingTaskType.PeerPushDebit,
+ pursePub,
+ });
+ const mergeUrl = new URL(
+ `purses/${pursePub}/merge`,
+ peerPushInitiation.exchangeBaseUrl,
+ );
+ mergeUrl.searchParams.set("timeout_ms", "30000");
+ logger.info(`long-polling on purse status at ${mergeUrl.href}`);
+ const resp = await wex.http.fetch(mergeUrl.href, {
+ // timeout: getReserveRequestTimeout(withdrawalGroup),
+ cancellationToken: wex.cancellationToken,
+ });
+ if (resp.status === HttpStatusCode.Ok) {
+ const purseStatus = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForExchangePurseStatus(),
+ );
+ const mergeTimestamp = purseStatus.merge_timestamp;
+ logger.info(`got purse status ${j2s(purseStatus)}`);
+ if (!mergeTimestamp || TalerProtocolTimestamp.isNever(mergeTimestamp)) {
+ return TaskRunResult.backoff();
+ } else {
+ await transitionPeerPushDebitTransaction(
+ wex,
+ peerPushInitiation.pursePub,
+ {
+ stFrom: PeerPushDebitStatus.PendingReady,
+ stTo: PeerPushDebitStatus.Done,
+ },
+ );
+ return TaskRunResult.progress();
+ }
+ } else if (resp.status === HttpStatusCode.Gone) {
+ logger.info(`purse ${pursePub} is gone, aborting peer-push-debit`);
+ const transitionInfo = await wex.db.runReadWriteTx(
+ {
+ storeNames: [
+ "peerPushDebit",
+ "refreshGroups",
+ "refreshSessions",
+ "denominations",
+ "coinAvailability",
+ "coins",
+ ],
+ },
+ async (tx) => {
+ const ppiRec = await tx.peerPushDebit.get(pursePub);
+ if (!ppiRec) {
+ return undefined;
+ }
+ if (ppiRec.status !== PeerPushDebitStatus.PendingReady) {
+ return undefined;
+ }
+ const currency = Amounts.currencyOf(ppiRec.amount);
+ const oldTxState = computePeerPushDebitTransactionState(ppiRec);
+ const coinPubs: CoinRefreshRequest[] = [];
+
+ if (ppiRec.coinSel) {
+ for (let i = 0; i < ppiRec.coinSel.coinPubs.length; i++) {
+ coinPubs.push({
+ amount: ppiRec.coinSel.contributions[i],
+ coinPub: ppiRec.coinSel.coinPubs[i],
+ });
+ }
+
+ const refresh = await createRefreshGroup(
+ wex,
+ tx,
+ currency,
+ coinPubs,
+ RefreshReason.AbortPeerPushDebit,
+ transactionId,
+ );
+
+ ppiRec.abortRefreshGroupId = refresh.refreshGroupId;
+ }
+ ppiRec.status = PeerPushDebitStatus.AbortingRefreshExpired;
+ await tx.peerPushDebit.put(ppiRec);
+ const newTxState = computePeerPushDebitTransactionState(ppiRec);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ },
+ );
+ notifyTransition(wex, transactionId, transitionInfo);
+ return TaskRunResult.backoff();
+ } else {
+ logger.warn(`unexpected HTTP status for purse: ${resp.status}`);
+ return TaskRunResult.longpollReturnedPending();
+ }
+}
+
+export async function processPeerPushDebit(
+ wex: WalletExecutionContext,
+ pursePub: string,
+): Promise<TaskRunResult> {
+ const peerPushInitiation = await wex.db.runReadOnlyTx(
+ { storeNames: ["peerPushDebit"] },
+ async (tx) => {
+ return tx.peerPushDebit.get(pursePub);
+ },
+ );
+ if (!peerPushInitiation) {
+ throw Error("peer push payment not found");
+ }
+
+ switch (peerPushInitiation.status) {
+ case PeerPushDebitStatus.PendingCreatePurse:
+ return processPeerPushDebitCreateReserve(wex, peerPushInitiation);
+ case PeerPushDebitStatus.PendingReady:
+ return processPeerPushDebitReady(wex, peerPushInitiation);
+ case PeerPushDebitStatus.AbortingDeletePurse:
+ return processPeerPushDebitAbortingDeletePurse(wex, peerPushInitiation);
+ case PeerPushDebitStatus.AbortingRefreshDeleted:
+ return processPeerPushDebitAbortingRefreshDeleted(
+ wex,
+ peerPushInitiation,
+ );
+ case PeerPushDebitStatus.AbortingRefreshExpired:
+ return processPeerPushDebitAbortingRefreshExpired(
+ wex,
+ peerPushInitiation,
+ );
+ default: {
+ const txState = computePeerPushDebitTransactionState(peerPushInitiation);
+ logger.warn(
+ `not processing peer-push-debit transaction in state ${j2s(txState)}`,
+ );
+ }
+ }
+
+ return TaskRunResult.finished();
+}
+
+/**
+ * Initiate sending a peer-to-peer push payment.
+ */
+export async function initiatePeerPushDebit(
+ wex: WalletExecutionContext,
+ req: InitiatePeerPushDebitRequest,
+): Promise<InitiatePeerPushDebitResponse> {
+ const instructedAmount = Amounts.parseOrThrow(
+ req.partialContractTerms.amount,
+ );
+ const purseExpiration = req.partialContractTerms.purse_expiration;
+ const contractTerms = req.partialContractTerms;
+
+ const pursePair = await wex.cryptoApi.createEddsaKeypair({});
+ const mergePair = await wex.cryptoApi.createEddsaKeypair({});
+
+ const hContractTerms = ContractTermsUtil.hashContractTerms(contractTerms);
+
+ const contractKeyPair = await wex.cryptoApi.createEddsaKeypair({});
+
+ const coinSelRes = await selectPeerCoins(wex, {
+ instructedAmount,
+ });
+
+ let coins: SelectedProspectiveCoin[] | undefined = undefined;
+
+ switch (coinSelRes.type) {
+ case "failure":
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
+ {
+ insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
+ },
+ );
+ case "prospective":
+ coins = coinSelRes.result.prospectiveCoins;
+ break;
+ case "success":
+ coins = coinSelRes.result.coins;
+ break;
+ default:
+ assertUnreachable(coinSelRes);
+ }
+
+ const sel = coinSelRes.result;
+
+ logger.info(`selected p2p coins (push):`);
+ logger.trace(`${j2s(coinSelRes)}`);
+
+ const totalAmount = await getTotalPeerPaymentCost(wex, coins);
+
+ logger.info(`computed total peer payment cost`);
+
+ const pursePub = pursePair.pub;
+
+ const ctx = new PeerPushDebitTransactionContext(wex, pursePub);
+
+ const transactionId = ctx.transactionId;
+
+ const contractEncNonce = encodeCrock(getRandomBytes(24));
+
+ const transitionInfo = await wex.db.runReadWriteTx(
+ {
+ storeNames: [
+ "exchanges",
+ "contractTerms",
+ "coins",
+ "coinAvailability",
+ "denominations",
+ "refreshGroups",
+ "refreshSessions",
+ "peerPushDebit",
+ ],
+ },
+ async (tx) => {
+ const ppi: PeerPushDebitRecord = {
+ amount: Amounts.stringify(instructedAmount),
+ contractPriv: contractKeyPair.priv,
+ contractPub: contractKeyPair.pub,
+ contractTermsHash: hContractTerms,
+ exchangeBaseUrl: sel.exchangeBaseUrl,
+ mergePriv: mergePair.priv,
+ mergePub: mergePair.pub,
+ purseExpiration: timestampProtocolToDb(purseExpiration),
+ pursePriv: pursePair.priv,
+ pursePub: pursePair.pub,
+ timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()),
+ status: PeerPushDebitStatus.PendingCreatePurse,
+ contractEncNonce,
+ totalCost: Amounts.stringify(totalAmount),
+ };
+
+ if (coinSelRes.type === "success") {
+ ppi.coinSel = {
+ coinPubs: coinSelRes.result.coins.map((x) => x.coinPub),
+ contributions: coinSelRes.result.coins.map((x) => x.contribution),
+ };
+ // FIXME: Instead of directly doing a spendCoin here,
+ // 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,
+ }),
+ coinPubs: coinSelRes.result.coins.map((x) => x.coinPub),
+ contributions: coinSelRes.result.coins.map((x) =>
+ Amounts.parseOrThrow(x.contribution),
+ ),
+ refreshReason: RefreshReason.PayPeerPush,
+ });
+ }
+
+ await tx.peerPushDebit.add(ppi);
+
+ await tx.contractTerms.put({
+ h: hContractTerms,
+ contractTermsRaw: contractTerms,
+ });
+
+ const newTxState = computePeerPushDebitTransactionState(ppi);
+ return {
+ oldTxState: { major: TransactionMajorState.None },
+ newTxState,
+ };
+ },
+ );
+ notifyTransition(wex, transactionId, transitionInfo);
+ wex.ws.notify({
+ type: NotificationType.BalanceChange,
+ hintTransactionId: transactionId,
+ });
+
+ wex.taskScheduler.startShepherdTask(ctx.taskId);
+
+ return {
+ contractPriv: contractKeyPair.priv,
+ mergePriv: mergePair.priv,
+ pursePub: pursePair.pub,
+ exchangeBaseUrl: coinSelRes.result.exchangeBaseUrl,
+ transactionId: constructTransactionIdentifier({
+ tag: TransactionType.PeerPushDebit,
+ pursePub: pursePair.pub,
+ }),
+ };
+}
+
+export function computePeerPushDebitTransactionActions(
+ ppiRecord: PeerPushDebitRecord,
+): TransactionAction[] {
+ switch (ppiRecord.status) {
+ case PeerPushDebitStatus.PendingCreatePurse:
+ return [TransactionAction.Abort, TransactionAction.Suspend];
+ case PeerPushDebitStatus.PendingReady:
+ return [TransactionAction.Abort, TransactionAction.Suspend];
+ case PeerPushDebitStatus.Aborted:
+ return [TransactionAction.Delete];
+ case PeerPushDebitStatus.AbortingDeletePurse:
+ return [TransactionAction.Suspend, TransactionAction.Fail];
+ case PeerPushDebitStatus.AbortingRefreshDeleted:
+ return [TransactionAction.Suspend, TransactionAction.Fail];
+ case PeerPushDebitStatus.AbortingRefreshExpired:
+ return [TransactionAction.Suspend, TransactionAction.Fail];
+ case PeerPushDebitStatus.SuspendedAbortingRefreshExpired:
+ return [TransactionAction.Resume, TransactionAction.Fail];
+ case PeerPushDebitStatus.SuspendedAbortingDeletePurse:
+ return [TransactionAction.Resume, TransactionAction.Fail];
+ case PeerPushDebitStatus.SuspendedAbortingRefreshDeleted:
+ return [TransactionAction.Resume, TransactionAction.Fail];
+ case PeerPushDebitStatus.SuspendedCreatePurse:
+ return [TransactionAction.Resume, TransactionAction.Abort];
+ case PeerPushDebitStatus.SuspendedReady:
+ return [TransactionAction.Resume, TransactionAction.Abort];
+ case PeerPushDebitStatus.Done:
+ return [TransactionAction.Delete];
+ case PeerPushDebitStatus.Expired:
+ return [TransactionAction.Delete];
+ case PeerPushDebitStatus.Failed:
+ return [TransactionAction.Delete];
+ }
+}
+
+export function computePeerPushDebitTransactionState(
+ ppiRecord: PeerPushDebitRecord,
+): TransactionState {
+ switch (ppiRecord.status) {
+ case PeerPushDebitStatus.PendingCreatePurse:
+ return {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.CreatePurse,
+ };
+ case PeerPushDebitStatus.PendingReady:
+ return {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.Ready,
+ };
+ case PeerPushDebitStatus.Aborted:
+ return {
+ major: TransactionMajorState.Aborted,
+ };
+ case PeerPushDebitStatus.AbortingDeletePurse:
+ return {
+ major: TransactionMajorState.Aborting,
+ minor: TransactionMinorState.DeletePurse,
+ };
+ case PeerPushDebitStatus.AbortingRefreshDeleted:
+ return {
+ major: TransactionMajorState.Aborting,
+ minor: TransactionMinorState.Refresh,
+ };
+ case PeerPushDebitStatus.AbortingRefreshExpired:
+ return {
+ major: TransactionMajorState.Aborting,
+ minor: TransactionMinorState.RefreshExpired,
+ };
+ case PeerPushDebitStatus.SuspendedAbortingDeletePurse:
+ return {
+ major: TransactionMajorState.SuspendedAborting,
+ minor: TransactionMinorState.DeletePurse,
+ };
+ case PeerPushDebitStatus.SuspendedAbortingRefreshExpired:
+ return {
+ major: TransactionMajorState.SuspendedAborting,
+ minor: TransactionMinorState.RefreshExpired,
+ };
+ case PeerPushDebitStatus.SuspendedAbortingRefreshDeleted:
+ return {
+ major: TransactionMajorState.SuspendedAborting,
+ minor: TransactionMinorState.Refresh,
+ };
+ case PeerPushDebitStatus.SuspendedCreatePurse:
+ return {
+ major: TransactionMajorState.Suspended,
+ minor: TransactionMinorState.CreatePurse,
+ };
+ case PeerPushDebitStatus.SuspendedReady:
+ return {
+ major: TransactionMajorState.Suspended,
+ minor: TransactionMinorState.Ready,
+ };
+ case PeerPushDebitStatus.Done:
+ return {
+ major: TransactionMajorState.Done,
+ };
+ case PeerPushDebitStatus.Failed:
+ return {
+ major: TransactionMajorState.Failed,
+ };
+ case PeerPushDebitStatus.Expired:
+ return {
+ major: TransactionMajorState.Expired,
+ };
+ }
+}
diff --git a/packages/taler-wallet-core/src/pending-types.ts b/packages/taler-wallet-core/src/pending-types.ts
deleted file mode 100644
index 862bbf4f9..000000000
--- a/packages/taler-wallet-core/src/pending-types.ts
+++ /dev/null
@@ -1,209 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2019 GNUnet e.V.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- * Type and schema definitions for pending tasks in the wallet.
- *
- * These are only used internally, and are not part of the stable public
- * interface to the wallet.
- */
-
-/**
- * Imports.
- */
-import {
- TalerErrorDetail,
- AbsoluteTime,
- TalerProtocolTimestamp,
-} from "@gnu-taler/taler-util";
-import { RetryInfo } from "./util/retries.js";
-
-export enum PendingTaskType {
- ExchangeUpdate = "exchange-update",
- ExchangeCheckRefresh = "exchange-check-refresh",
- Purchase = "purchase",
- Refresh = "refresh",
- Recoup = "recoup",
- TipPickup = "tip-pickup",
- Withdraw = "withdraw",
- Deposit = "deposit",
- Backup = "backup",
-}
-
-/**
- * Information about a pending operation.
- */
-export type PendingTaskInfo = PendingTaskInfoCommon &
- (
- | PendingExchangeUpdateTask
- | PendingExchangeCheckRefreshTask
- | PendingPurchaseTask
- | PendingRefreshTask
- | PendingTipPickupTask
- | PendingWithdrawTask
- | PendingRecoupTask
- | PendingDepositTask
- | PendingBackupTask
- );
-
-export interface PendingBackupTask {
- type: PendingTaskType.Backup;
- backupProviderBaseUrl: string;
- lastError: TalerErrorDetail | undefined;
-}
-
-/**
- * The wallet is currently updating information about an exchange.
- */
-export interface PendingExchangeUpdateTask {
- type: PendingTaskType.ExchangeUpdate;
- exchangeBaseUrl: string;
- lastError: TalerErrorDetail | undefined;
-}
-
-/**
- * The wallet should check whether coins from this exchange
- * need to be auto-refreshed.
- */
-export interface PendingExchangeCheckRefreshTask {
- type: PendingTaskType.ExchangeCheckRefresh;
- exchangeBaseUrl: string;
-}
-
-export enum ReserveType {
- /**
- * Manually created.
- */
- Manual = "manual",
- /**
- * Withdrawn from a bank that has "tight" Taler integration
- */
- TalerBankWithdraw = "taler-bank-withdraw",
-}
-
-/**
- * Status of an ongoing withdrawal operation.
- */
-export interface PendingRefreshTask {
- type: PendingTaskType.Refresh;
- lastError?: TalerErrorDetail;
- refreshGroupId: string;
- finishedPerCoin: boolean[];
- retryInfo?: RetryInfo;
-}
-
-/**
- * The wallet is picking up a tip that the user has accepted.
- */
-export interface PendingTipPickupTask {
- type: PendingTaskType.TipPickup;
- tipId: string;
- merchantBaseUrl: string;
- merchantTipId: string;
-}
-
-/**
- * A purchase needs to be processed (i.e. for download / payment / refund).
- */
-export interface PendingPurchaseTask {
- type: PendingTaskType.Purchase;
- proposalId: string;
- retryInfo?: RetryInfo;
- /**
- * Status of the payment as string, used only for debugging.
- */
- statusStr: string;
- lastError: TalerErrorDetail | undefined;
-}
-
-export interface PendingRecoupTask {
- type: PendingTaskType.Recoup;
- recoupGroupId: string;
- retryInfo?: RetryInfo;
- lastError: TalerErrorDetail | undefined;
-}
-
-/**
- * Status of an ongoing withdrawal operation.
- */
-export interface PendingWithdrawTask {
- type: PendingTaskType.Withdraw;
- lastError: TalerErrorDetail | undefined;
- retryInfo?: RetryInfo;
- withdrawalGroupId: string;
-}
-
-/**
- * Status of an ongoing deposit operation.
- */
-export interface PendingDepositTask {
- type: PendingTaskType.Deposit;
- lastError: TalerErrorDetail | undefined;
- retryInfo: RetryInfo | undefined;
- depositGroupId: string;
-}
-
-/**
- * Fields that are present in every pending operation.
- */
-export interface PendingTaskInfoCommon {
- /**
- * Type of the pending operation.
- */
- type: PendingTaskType;
-
- /**
- * Unique identifier for the pending task.
- */
- id: string;
-
- /**
- * Set to true if the operation indicates that something is really in progress,
- * as opposed to some regular scheduled operation that can be tried later.
- */
- givesLifeness: boolean;
-
- /**
- * Operation is active and waiting for a longpoll result.
- */
- isLongpolling: boolean;
-
- /**
- * Operation is waiting to be executed.
- */
- isDue: boolean;
-
- /**
- * Timestamp when the pending operation should be executed next.
- */
- timestampDue: AbsoluteTime;
-
- /**
- * Retry info. Currently used to stop the wallet after any operation
- * exceeds a number of retries.
- */
- retryInfo?: RetryInfo;
-}
-
-/**
- * Response returned from the pending operations API.
- */
-export interface PendingOperationsResponse {
- /**
- * List of pending operations.
- */
- pendingOperations: PendingTaskInfo[];
-}
diff --git a/packages/taler-wallet-core/src/util/query.ts b/packages/taler-wallet-core/src/query.ts
index 9e960821d..dc15bbdd1 100644
--- a/packages/taler-wallet-core/src/util/query.ts
+++ b/packages/taler-wallet-core/src/query.ts
@@ -15,27 +15,33 @@
*/
/**
- * Database query abstractions.
- * @module Query
+ * @fileoverview
+ * Query helpers for IndexedDB databases.
+ *
* @author Florian Dold
*/
/**
* Imports.
*/
-import { openPromise } from "./promiseUtils.js";
import {
- IDBRequest,
- IDBTransaction,
- IDBValidKey,
+ IDBCursor,
IDBDatabase,
IDBFactory,
- IDBVersionChangeEvent,
- IDBCursor,
IDBKeyPath,
IDBKeyRange,
+ IDBRequest,
+ IDBTransaction,
+ IDBTransactionMode,
+ IDBValidKey,
+ IDBVersionChangeEvent,
} from "@gnu-taler/idb-bridge";
-import { Logger } from "@gnu-taler/taler-util";
+import {
+ CancellationToken,
+ Codec,
+ Logger,
+ openPromise,
+} from "@gnu-taler/taler-util";
const logger = new Logger("query.ts");
@@ -239,7 +245,7 @@ class ResultStream<T> {
export function openDatabase(
idbFactory: IDBFactory,
databaseName: string,
- databaseVersion: number,
+ databaseVersion: number | undefined,
onVersionChange: () => void,
onUpgradeNeeded: (
db: IDBDatabase,
@@ -250,14 +256,14 @@ export function openDatabase(
): Promise<IDBDatabase> {
return new Promise<IDBDatabase>((resolve, reject) => {
const req = idbFactory.open(databaseName, databaseVersion);
- req.onerror = (e) => {
- logger.error("database error", e);
- reject(new Error("database error"));
+ req.onerror = (event) => {
+ // @ts-expect-error
+ reject(new Error(`database opening error`, { cause: req.error }));
};
req.onsuccess = (e) => {
req.result.onversionchange = (evt: IDBVersionChangeEvent) => {
logger.info(
- `handling live db version change from ${evt.oldVersion} to ${evt.newVersion}`,
+ `handling versionchange on ${databaseName} from ${evt.oldVersion} to ${evt.newVersion}`,
);
req.result.close();
onVersionChange();
@@ -268,12 +274,21 @@ export function openDatabase(
const db = req.result;
const newVersion = e.newVersion;
if (!newVersion) {
- throw Error("upgrade needed, but new version unknown");
+ // @ts-expect-error
+ throw Error("upgrade needed, but new version unknown", {
+ cause: req.error,
+ });
}
const transaction = req.transaction;
if (!transaction) {
- throw Error("no transaction handle available in upgrade handler");
+ // @ts-expect-error
+ throw Error("no transaction handle available in upgrade handler", {
+ cause: req.error,
+ });
}
+ logger.info(
+ `handling upgradeneeded event on ${databaseName} from ${e.oldVersion} to ${e.newVersion}`,
+ );
onUpgradeNeeded(db, e.oldVersion, newVersion, transaction);
};
});
@@ -303,7 +318,7 @@ export interface StoreOptions {
autoIncrement?: boolean;
/**
- * Database version that this store was added in, or
+ * First minor database version that this store was added in, or
* undefined if added in the first version.
*/
versionAdded?: number;
@@ -338,9 +353,14 @@ interface IndexReadOnlyAccessor<RecordType> {
iter(query?: IDBKeyRange | IDBValidKey): ResultStream<RecordType>;
get(query: IDBValidKey): Promise<RecordType | undefined>;
getAll(
- query: IDBKeyRange | IDBValidKey,
+ query?: IDBKeyRange | IDBValidKey,
count?: number,
): Promise<RecordType[]>;
+ getAllKeys(
+ query?: IDBKeyRange | IDBValidKey,
+ count?: number,
+ ): Promise<IDBValidKey[]>;
+ count(query?: IDBValidKey): Promise<number>;
}
type GetIndexReadOnlyAccess<RecordType, IndexMap> = {
@@ -351,9 +371,14 @@ interface IndexReadWriteAccessor<RecordType> {
iter(query: IDBKeyRange | IDBValidKey): ResultStream<RecordType>;
get(query: IDBValidKey): Promise<RecordType | undefined>;
getAll(
- query: IDBKeyRange | IDBValidKey,
+ query?: IDBKeyRange | IDBValidKey,
count?: number,
): Promise<RecordType[]>;
+ getAllKeys(
+ query?: IDBKeyRange | IDBValidKey,
+ count?: number,
+ ): Promise<IDBValidKey[]>;
+ count(query?: IDBValidKey): Promise<number>;
}
type GetIndexReadWriteAccess<RecordType, IndexMap> = {
@@ -362,6 +387,10 @@ type GetIndexReadWriteAccess<RecordType, IndexMap> = {
export interface StoreReadOnlyAccessor<RecordType, IndexMap> {
get(key: IDBValidKey): Promise<RecordType | undefined>;
+ getAll(
+ query?: IDBKeyRange | IDBValidKey,
+ count?: number,
+ ): Promise<RecordType[]>;
iter(query?: IDBValidKey): ResultStream<RecordType>;
indexes: GetIndexReadOnlyAccess<RecordType, IndexMap>;
}
@@ -375,20 +404,24 @@ export interface InsertResponse {
export interface StoreReadWriteAccessor<RecordType, IndexMap> {
get(key: IDBValidKey): Promise<RecordType | undefined>;
+ getAll(
+ query?: IDBKeyRange | IDBValidKey,
+ count?: number,
+ ): Promise<RecordType[]>;
iter(query?: IDBValidKey): ResultStream<RecordType>;
- put(r: RecordType): Promise<InsertResponse>;
- add(r: RecordType): Promise<InsertResponse>;
+ put(r: RecordType, key?: IDBValidKey): Promise<InsertResponse>;
+ add(r: RecordType, key?: IDBValidKey): Promise<InsertResponse>;
delete(key: IDBValidKey): Promise<void>;
indexes: GetIndexReadWriteAccess<RecordType, IndexMap>;
}
export interface StoreWithIndexes<
StoreName extends string,
- SD extends StoreDescriptor<unknown>,
+ RecordType,
IndexMap,
> {
storeName: StoreName;
- store: SD;
+ store: StoreDescriptor<RecordType>;
indexMap: IndexMap;
/**
@@ -398,19 +431,13 @@ export interface StoreWithIndexes<
mark: Symbol;
}
-export type GetRecordType<T> = T extends StoreDescriptor<infer X> ? X : unknown;
-
const storeWithIndexesSymbol = Symbol("StoreWithIndexesMark");
-export function describeStore<
- StoreName extends string,
- SD extends StoreDescriptor<unknown>,
- IndexMap,
->(
+export function describeStore<StoreName extends string, RecordType, IndexMap>(
name: StoreName,
- s: SD,
+ s: StoreDescriptor<RecordType>,
m: IndexMap,
-): StoreWithIndexes<StoreName, SD, IndexMap> {
+): StoreWithIndexes<StoreName, RecordType, IndexMap> {
return {
storeName: name,
store: s,
@@ -419,45 +446,125 @@ export function describeStore<
};
}
-export type GetReadOnlyAccess<BoundStores> = {
- [P in keyof BoundStores]: BoundStores[P] extends StoreWithIndexes<
- infer SN,
- infer SD,
- infer IM
- >
- ? StoreReadOnlyAccessor<GetRecordType<SD>, IM>
- : unknown;
-};
+export function describeStoreV2<
+ StoreName extends string,
+ RecordType,
+ IndexMap extends { [x: string]: IndexDescriptor } = {},
+>(args: {
+ storeName: StoreName;
+ recordCodec: Codec<RecordType>;
+ keyPath?: IDBKeyPath | IDBKeyPath[];
+ autoIncrement?: boolean;
+ /**
+ * Database version that this store was added in, or
+ * undefined if added in the first version.
+ */
+ versionAdded?: number;
+ indexes?: IndexMap;
+}): StoreWithIndexes<StoreName, RecordType, IndexMap> {
+ return {
+ storeName: args.storeName,
+ store: {
+ _dummy: undefined as any,
+ autoIncrement: args.autoIncrement,
+ keyPath: args.keyPath,
+ versionAdded: args.versionAdded,
+ },
+ indexMap: args.indexes ?? ({} as IndexMap),
+ mark: storeWithIndexesSymbol,
+ };
+}
+
+type KeyPathComponents = string | number;
-export type GetReadWriteAccess<BoundStores> = {
- [P in keyof BoundStores]: BoundStores[P] extends StoreWithIndexes<
- infer SN,
- infer SD,
- infer IM
- >
- ? StoreReadWriteAccessor<GetRecordType<SD>, IM>
+/**
+ * Follow a key path (dot-separated) in an object.
+ */
+type DerefKeyPath<T, P> = P extends `${infer PX extends keyof T &
+ KeyPathComponents}`
+ ? T[PX]
+ : P extends `${infer P0 extends keyof T & KeyPathComponents}.${infer Rest}`
+ ? DerefKeyPath<T[P0], Rest>
: unknown;
-};
-type ReadOnlyTransactionFunction<BoundStores, T> = (
- t: GetReadOnlyAccess<BoundStores>,
- rawTx: IDBTransaction,
-) => Promise<T>;
+/**
+ * Return a path if it is a valid dot-separate path to an object.
+ * Otherwise, return "never".
+ */
+type ValidateKeyPath<T, P> = P extends `${infer PX extends keyof T &
+ KeyPathComponents}`
+ ? PX
+ : P extends `${infer P0 extends keyof T & KeyPathComponents}.${infer Rest}`
+ ? `${P0}.${ValidateKeyPath<T[P0], Rest>}`
+ : never;
+
+// function foo<T, P>(
+// x: T,
+// p: P extends ValidateKeyPath<T, P> ? P : never,
+// ): void {}
+
+// foo({x: [0,1,2]}, "x.0");
+
+export type StoreNames<StoreMap> = StoreMap extends {
+ [P in keyof StoreMap]: StoreWithIndexes<infer SN1, infer SD1, infer IM1>;
+}
+ ? keyof StoreMap
+ : unknown;
+
+export type DbReadWriteTransaction<
+ StoreMap,
+ StoresArr extends Array<StoreNames<StoreMap>>,
+> = StoreMap extends {
+ [P in string]: StoreWithIndexes<infer _SN1, infer _SD1, infer _IM1>;
+}
+ ? {
+ [X in StoresArr[number] &
+ keyof StoreMap]: StoreMap[X] extends StoreWithIndexes<
+ infer _StoreName,
+ infer RecordType,
+ infer IndexMap
+ >
+ ? StoreReadWriteAccessor<RecordType, IndexMap>
+ : unknown;
+ }
+ : never;
-type ReadWriteTransactionFunction<BoundStores, T> = (
- t: GetReadWriteAccess<BoundStores>,
- rawTx: IDBTransaction,
-) => Promise<T>;
+export type DbReadOnlyTransaction<
+ StoreMap,
+ StoresArr extends Array<StoreNames<StoreMap>>,
+> = StoreMap extends {
+ [P in string]: StoreWithIndexes<infer _SN1, infer _SD1, infer _IM1>;
+}
+ ? {
+ [X in StoresArr[number] &
+ keyof StoreMap]: StoreMap[X] extends StoreWithIndexes<
+ infer _StoreName,
+ infer RecordType,
+ infer IndexMap
+ >
+ ? StoreReadOnlyAccessor<RecordType, IndexMap>
+ : unknown;
+ }
+ : never;
-export interface TransactionContext<BoundStores> {
- runReadWrite<T>(f: ReadWriteTransactionFunction<BoundStores, T>): Promise<T>;
- runReadOnly<T>(f: ReadOnlyTransactionFunction<BoundStores, T>): Promise<T>;
+/**
+ * Convert the type of an array to a union of the contents.
+ *
+ * Example:
+ * Input ["foo", "bar"]
+ * Output "foo" | "bar"
+ */
+export type UnionFromArray<Arr> = Arr extends {
+ [X in keyof Arr]: Arr[X] & string;
}
+ ? Arr[keyof Arr & number]
+ : unknown;
function runTx<Arg, Res>(
tx: IDBTransaction,
arg: Arg,
f: (t: Arg, t2: IDBTransaction) => Promise<Res>,
+ triggerContext: InternalTriggerContext,
): Promise<Res> {
const stack = Error("Failed transaction was started here.");
return new Promise((resolve, reject) => {
@@ -478,6 +585,7 @@ function runTx<Arg, Res>(
logger.error(`${stack.stack}`);
reject(Error(msg));
}
+ triggerContext.handleAfterCommit();
resolve(funResult);
};
tx.onerror = () => {
@@ -522,6 +630,7 @@ function runTx<Arg, Res>(
function makeReadContext(
tx: IDBTransaction,
storePick: { [n: string]: StoreWithIndexes<any, any, any> },
+ triggerContext: InternalTriggerContext,
): any {
const ctx: { [s: string]: StoreReadOnlyAccessor<any, any> } = {};
for (const storeAlias in storePick) {
@@ -534,10 +643,12 @@ function makeReadContext(
const indexName = indexDescriptor.name;
indexes[indexAlias] = {
get(key) {
+ triggerContext.storesAccessed.add(storeName);
const req = tx.objectStore(storeName).index(indexName).get(key);
return requestToPromise(req);
},
iter(query) {
+ triggerContext.storesAccessed.add(storeName);
const req = tx
.objectStore(storeName)
.index(indexName)
@@ -545,21 +656,42 @@ function makeReadContext(
return new ResultStream<any>(req);
},
getAll(query, count) {
+ triggerContext.storesAccessed.add(storeName);
const req = tx
.objectStore(storeName)
.index(indexName)
.getAll(query, count);
return requestToPromise(req);
},
+ getAllKeys(query, count) {
+ triggerContext.storesAccessed.add(storeName);
+ const req = tx
+ .objectStore(storeName)
+ .index(indexName)
+ .getAllKeys(query, count);
+ return requestToPromise(req);
+ },
+ count(query) {
+ triggerContext.storesAccessed.add(storeName);
+ const req = tx.objectStore(storeName).index(indexName).count(query);
+ return requestToPromise(req);
+ },
};
}
ctx[storeAlias] = {
indexes,
get(key) {
+ triggerContext.storesAccessed.add(storeName);
const req = tx.objectStore(storeName).get(key);
return requestToPromise(req);
},
+ getAll(query, count) {
+ triggerContext.storesAccessed.add(storeName);
+ const req = tx.objectStore(storeName).getAll(query, count);
+ return requestToPromise(req);
+ },
iter(query) {
+ triggerContext.storesAccessed.add(storeName);
const req = tx.objectStore(storeName).openCursor(query);
return new ResultStream<any>(req);
},
@@ -571,6 +703,7 @@ function makeReadContext(
function makeWriteContext(
tx: IDBTransaction,
storePick: { [n: string]: StoreWithIndexes<any, any, any> },
+ triggerContext: InternalTriggerContext,
): any {
const ctx: { [s: string]: StoreReadWriteAccessor<any, any> } = {};
for (const storeAlias in storePick) {
@@ -583,10 +716,12 @@ function makeWriteContext(
const indexName = indexDescriptor.name;
indexes[indexAlias] = {
get(key) {
+ triggerContext.storesAccessed.add(storeName);
const req = tx.objectStore(storeName).index(indexName).get(key);
return requestToPromise(req);
},
iter(query) {
+ triggerContext.storesAccessed.add(storeName);
const req = tx
.objectStore(storeName)
.index(indexName)
@@ -594,39 +729,66 @@ function makeWriteContext(
return new ResultStream<any>(req);
},
getAll(query, count) {
+ triggerContext.storesAccessed.add(storeName);
const req = tx
.objectStore(storeName)
.index(indexName)
.getAll(query, count);
return requestToPromise(req);
},
+ getAllKeys(query, count) {
+ triggerContext.storesAccessed.add(storeName);
+ const req = tx
+ .objectStore(storeName)
+ .index(indexName)
+ .getAllKeys(query, count);
+ return requestToPromise(req);
+ },
+ count(query) {
+ triggerContext.storesAccessed.add(storeName);
+ const req = tx.objectStore(storeName).index(indexName).count(query);
+ return requestToPromise(req);
+ },
};
}
ctx[storeAlias] = {
indexes,
get(key) {
+ triggerContext.storesAccessed.add(storeName);
const req = tx.objectStore(storeName).get(key);
return requestToPromise(req);
},
+ getAll(query, count) {
+ triggerContext.storesAccessed.add(storeName);
+ const req = tx.objectStore(storeName).getAll(query, count);
+ return requestToPromise(req);
+ },
iter(query) {
+ triggerContext.storesAccessed.add(storeName);
const req = tx.objectStore(storeName).openCursor(query);
return new ResultStream<any>(req);
},
- async add(r) {
- const req = tx.objectStore(storeName).add(r);
+ async add(r, k) {
+ triggerContext.storesAccessed.add(storeName);
+ triggerContext.storesModified.add(storeName);
+ const req = tx.objectStore(storeName).add(r, k);
const key = await requestToPromise(req);
return {
key: key,
};
},
- async put(r) {
- const req = tx.objectStore(storeName).put(r);
+ async put(r, k) {
+ triggerContext.storesAccessed.add(storeName);
+ triggerContext.storesModified.add(storeName);
+ const req = tx.objectStore(storeName).put(r, k);
const key = await requestToPromise(req);
return {
key: key,
};
},
delete(k) {
+ triggerContext.storesAccessed.add(storeName);
+ triggerContext.storesModified.add(storeName);
const req = tx.objectStore(storeName).delete(k);
return requestToPromise(req);
},
@@ -635,114 +797,208 @@ function makeWriteContext(
return ctx;
}
-type StoreNamesOf<X> = X extends { [x: number]: infer F }
- ? F extends { storeName: infer I }
- ? I
- : never
- : never;
+export interface DbAccess<StoreMap> {
+ idbHandle(): IDBDatabase;
+
+ runAllStoresReadWriteTx<T>(
+ options: {
+ label?: string;
+ },
+ txf: (
+ tx: DbReadWriteTransaction<StoreMap, Array<StoreNames<StoreMap>>>,
+ ) => Promise<T>,
+ ): Promise<T>;
+
+ runAllStoresReadOnlyTx<T>(
+ options: {
+ label?: string;
+ },
+ txf: (
+ tx: DbReadOnlyTransaction<StoreMap, Array<StoreNames<StoreMap>>>,
+ ) => Promise<T>,
+ ): Promise<T>;
+
+ runReadWriteTx<T, StoreNameArray extends Array<StoreNames<StoreMap>>>(
+ opts: {
+ storeNames: StoreNameArray;
+ label?: string;
+ },
+ txf: (tx: DbReadWriteTransaction<StoreMap, StoreNameArray>) => Promise<T>,
+ ): Promise<T>;
+
+ runReadOnlyTx<T, StoreNameArray extends Array<StoreNames<StoreMap>>>(
+ opts: {
+ storeNames: StoreNameArray;
+ label?: string;
+ },
+ txf: (tx: DbReadOnlyTransaction<StoreMap, StoreNameArray>) => Promise<T>,
+ ): Promise<T>;
+}
+
+export interface AfterCommitInfo {
+ mode: IDBTransactionMode;
+ scope: Set<string>;
+ accessedStores: Set<string>;
+ modifiedStores: Set<string>;
+}
+
+export interface TriggerSpec {
+ /**
+ * Trigger run after every successful commit, run outside of the transaction.
+ */
+ afterCommit?: (info: AfterCommitInfo) => void;
+
+ // onRead(store, value)
+ // initState<State> () => State
+ // beforeCommit<State>? (tx: Transaction, s: State | undefined) => Promise<void>;
+}
+
+class InternalTriggerContext {
+ storesScope: Set<string>;
+ storesAccessed: Set<string> = new Set();
+ storesModified: Set<string> = new Set();
+
+ constructor(
+ private triggerSpec: TriggerSpec,
+ private mode: IDBTransactionMode,
+ scope: string[],
+ ) {
+ this.storesScope = new Set(scope);
+ }
+
+ handleAfterCommit() {
+ if (this.triggerSpec.afterCommit) {
+ this.triggerSpec.afterCommit({
+ mode: this.mode,
+ accessedStores: this.storesAccessed,
+ modifiedStores: this.storesModified,
+ scope: this.storesScope,
+ });
+ }
+ }
+}
/**
* Type-safe access to a database with a particular store map.
*
* A store map is the metadata that describes the store.
*/
-export class DbAccess<StoreMap> {
- constructor(private db: IDBDatabase, private stores: StoreMap) {}
+export class DbAccessImpl<StoreMap> implements DbAccess<StoreMap> {
+ constructor(
+ private db: IDBDatabase,
+ private stores: StoreMap,
+ private triggers: TriggerSpec = {},
+ private cancellationToken: CancellationToken,
+ ) {}
idbHandle(): IDBDatabase {
return this.db;
}
- /**
- * Run a transaction with all object stores.
- */
- mktxAll(): TransactionContext<StoreMap> {
- const storeNames: string[] = [];
+ runAllStoresReadWriteTx<T>(
+ options: {
+ label?: string;
+ },
+ txf: (
+ tx: DbReadWriteTransaction<StoreMap, Array<StoreNames<StoreMap>>>,
+ ) => Promise<T>,
+ ): Promise<T> {
const accessibleStores: { [x: string]: StoreWithIndexes<any, any, any> } =
{};
-
- for (let i = 0; i < this.db.objectStoreNames.length; i++) {
- const sn = this.db.objectStoreNames[i];
+ const strStoreNames: string[] = [];
+ for (const sn of Object.keys(this.stores as any)) {
const swi = (this.stores as any)[sn] as StoreWithIndexes<any, any, any>;
- if (!swi) {
- throw Error(`store metadata not available (${sn})`);
- }
- storeNames.push(sn);
- accessibleStores[sn] = swi;
+ strStoreNames.push(swi.storeName);
+ accessibleStores[swi.storeName] = swi;
}
-
- const runReadOnly = <T>(
- txf: ReadOnlyTransactionFunction<StoreMap, T>,
- ): Promise<T> => {
- const tx = this.db.transaction(storeNames, "readonly");
- const readContext = makeReadContext(tx, accessibleStores);
- return runTx(tx, readContext, txf);
- };
-
- const runReadWrite = <T>(
- txf: ReadWriteTransactionFunction<StoreMap, T>,
- ): Promise<T> => {
- const tx = this.db.transaction(storeNames, "readwrite");
- const writeContext = makeWriteContext(tx, accessibleStores);
- return runTx(tx, writeContext, txf);
- };
-
- return {
- runReadOnly,
- runReadWrite,
- };
+ const mode = "readwrite";
+ const triggerContext = new InternalTriggerContext(
+ this.triggers,
+ mode,
+ strStoreNames,
+ );
+ const tx = this.db.transaction(strStoreNames, mode);
+ const writeContext = makeWriteContext(tx, accessibleStores, triggerContext);
+ return runTx(tx, writeContext, txf, triggerContext);
}
- /**
- * Run a transaction with selected object stores.
- *
- * The {@link namePicker} must be a function that selects a list of object
- * stores from all available object stores.
- */
- mktx<
- StoreNames extends keyof StoreMap,
- Stores extends StoreMap[StoreNames],
- StoreList extends Stores[],
- BoundStores extends {
- [X in StoreNamesOf<StoreList>]: StoreList[number] & { storeName: X };
+ async runAllStoresReadOnlyTx<T>(
+ options: {
+ label?: string;
},
- >(namePicker: (x: StoreMap) => StoreList): TransactionContext<BoundStores> {
- const storeNames: string[] = [];
+ txf: (
+ tx: DbReadOnlyTransaction<StoreMap, Array<StoreNames<StoreMap>>>,
+ ) => Promise<T>,
+ ): Promise<T> {
const accessibleStores: { [x: string]: StoreWithIndexes<any, any, any> } =
{};
-
- const storePick = namePicker(this.stores) as any;
- if (typeof storePick !== "object" || storePick === null) {
- throw Error();
- }
- for (const swiPicked of storePick) {
- const swi = swiPicked as StoreWithIndexes<any, any, any>;
- if (swi.mark !== storeWithIndexesSymbol) {
- throw Error("invalid store descriptor returned from selector function");
- }
- storeNames.push(swi.storeName);
+ const strStoreNames: string[] = [];
+ for (const sn of Object.keys(this.stores as any)) {
+ const swi = (this.stores as any)[sn] as StoreWithIndexes<any, any, any>;
+ strStoreNames.push(swi.storeName);
accessibleStores[swi.storeName] = swi;
}
+ const mode = "readonly";
+ const triggerContext = new InternalTriggerContext(
+ this.triggers,
+ mode,
+ strStoreNames,
+ );
+ const tx = this.db.transaction(strStoreNames, mode);
+ const writeContext = makeReadContext(tx, accessibleStores, triggerContext);
+ const res = await runTx(tx, writeContext, txf, triggerContext);
+ return res;
+ }
- const runReadOnly = <T>(
- txf: ReadOnlyTransactionFunction<BoundStores, T>,
- ): Promise<T> => {
- const tx = this.db.transaction(storeNames, "readonly");
- const readContext = makeReadContext(tx, accessibleStores);
- return runTx(tx, readContext, txf);
- };
-
- const runReadWrite = <T>(
- txf: ReadWriteTransactionFunction<BoundStores, T>,
- ): Promise<T> => {
- const tx = this.db.transaction(storeNames, "readwrite");
- const writeContext = makeWriteContext(tx, accessibleStores);
- return runTx(tx, writeContext, txf);
- };
+ async runReadWriteTx<T, StoreNameArray extends Array<StoreNames<StoreMap>>>(
+ opts: {
+ storeNames: StoreNameArray;
+ },
+ txf: (tx: DbReadWriteTransaction<StoreMap, StoreNameArray>) => Promise<T>,
+ ): Promise<T> {
+ const accessibleStores: { [x: string]: StoreWithIndexes<any, any, any> } =
+ {};
+ const strStoreNames: string[] = [];
+ for (const sn of opts.storeNames) {
+ const swi = (this.stores as any)[sn] as StoreWithIndexes<any, any, any>;
+ strStoreNames.push(swi.storeName);
+ accessibleStores[swi.storeName] = swi;
+ }
+ const mode = "readwrite";
+ const triggerContext = new InternalTriggerContext(
+ this.triggers,
+ mode,
+ strStoreNames,
+ );
+ const tx = this.db.transaction(strStoreNames, mode);
+ const writeContext = makeWriteContext(tx, accessibleStores, triggerContext);
+ const res = await runTx(tx, writeContext, txf, triggerContext);
+ return res;
+ }
- return {
- runReadOnly,
- runReadWrite,
- };
+ runReadOnlyTx<T, StoreNameArray extends Array<StoreNames<StoreMap>>>(
+ opts: {
+ storeNames: StoreNameArray;
+ },
+ txf: (tx: DbReadOnlyTransaction<StoreMap, StoreNameArray>) => Promise<T>,
+ ): Promise<T> {
+ const accessibleStores: { [x: string]: StoreWithIndexes<any, any, any> } =
+ {};
+ const strStoreNames: string[] = [];
+ for (const sn of opts.storeNames) {
+ const swi = (this.stores as any)[sn] as StoreWithIndexes<any, any, any>;
+ strStoreNames.push(swi.storeName);
+ accessibleStores[swi.storeName] = swi;
+ }
+ const mode = "readonly";
+ const triggerContext = new InternalTriggerContext(
+ this.triggers,
+ mode,
+ strStoreNames,
+ );
+ const tx = this.db.transaction(strStoreNames, mode);
+ const readContext = makeReadContext(tx, accessibleStores, triggerContext);
+ const res = runTx(tx, readContext, txf, triggerContext);
+ return res;
}
}
diff --git a/packages/taler-wallet-core/src/operations/recoup.ts b/packages/taler-wallet-core/src/recoup.ts
index 4feb4430d..6a09f9a0e 100644
--- a/packages/taler-wallet-core/src/operations/recoup.ts
+++ b/packages/taler-wallet-core/src/recoup.ts
@@ -26,53 +26,56 @@
*/
import {
Amounts,
+ CoinStatus,
+ Logger,
+ RefreshReason,
+ TalerPreciseTimestamp,
+ TransactionIdStr,
+ TransactionType,
+ URL,
+ checkDbInvariant,
codecForRecoupConfirmation,
codecForReserveStatus,
- CoinStatus,
encodeCrock,
getRandomBytes,
j2s,
- Logger,
- NotificationType,
- RefreshReason,
- TalerProtocolTimestamp,
- URL,
} from "@gnu-taler/taler-util";
+import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
+import {
+ PendingTaskType,
+ TaskIdStr,
+ TaskRunResult,
+ TransactionContext,
+ constructTaskIdentifier,
+} from "./common.js";
import {
CoinRecord,
CoinSourceType,
RecoupGroupRecord,
+ RecoupOperationStatus,
RefreshCoinSource,
- WalletStoresV1,
+ WalletDbReadWriteTransaction,
+ WithdrawCoinSource,
WithdrawalGroupStatus,
WithdrawalRecordType,
- WithdrawCoinSource,
-} from "../db.js";
-import { InternalWalletState } from "../internal-wallet-state.js";
-import { readSuccessResponseJsonOrThrow } from "../util/http.js";
-import { checkDbInvariant } from "../util/invariants.js";
-import { GetReadWriteAccess } from "../util/query.js";
-import {
- OperationAttemptResult,
- unwrapOperationHandlerResultOrThrow,
-} from "../util/retries.js";
-import { createRefreshGroup, processRefreshGroup } from "./refresh.js";
+ timestampPreciseToDb,
+} from "./db.js";
+import { createRefreshGroup } from "./refresh.js";
+import { constructTransactionIdentifier } from "./transactions.js";
+import { WalletExecutionContext, getDenomInfo } from "./wallet.js";
import { internalCreateWithdrawalGroup } from "./withdraw.js";
-const logger = new Logger("operations/recoup.ts");
+export const logger = new Logger("operations/recoup.ts");
/**
* Store a recoup group record in the database after marking
* a coin in the group as finished.
*/
-async function putGroupAsFinished(
- ws: InternalWalletState,
- tx: GetReadWriteAccess<{
- recoupGroups: typeof WalletStoresV1.recoupGroups;
- denominations: typeof WalletStoresV1.denominations;
- refreshGroups: typeof WalletStoresV1.refreshGroups;
- coins: typeof WalletStoresV1.coins;
- }>,
+export async function putGroupAsFinished(
+ wex: WalletExecutionContext,
+ tx: WalletDbReadWriteTransaction<
+ ["recoupGroups", "denominations", "refreshGroups", "coins"]
+ >,
recoupGroup: RecoupGroupRecord,
coinIdx: number,
): Promise<void> {
@@ -86,8 +89,8 @@ async function putGroupAsFinished(
await tx.recoupGroups.put(recoupGroup);
}
-async function recoupTipCoin(
- ws: InternalWalletState,
+async function recoupRewardCoin(
+ wex: WalletExecutionContext,
recoupGroupId: string,
coinIdx: number,
coin: CoinRecord,
@@ -95,14 +98,9 @@ async function recoupTipCoin(
// We can't really recoup a coin we got via tipping.
// Thus we just put the coin to sleep.
// FIXME: somehow report this to the user
- await ws.db
- .mktx((stores) => [
- stores.recoupGroups,
- stores.denominations,
- stores.refreshGroups,
- stores.coins,
- ])
- .runReadWrite(async (tx) => {
+ await wex.db.runReadWriteTx(
+ { storeNames: ["recoupGroups", "denominations", "refreshGroups", "coins"] },
+ async (tx) => {
const recoupGroup = await tx.recoupGroups.get(recoupGroupId);
if (!recoupGroup) {
return;
@@ -110,98 +108,23 @@ async function recoupTipCoin(
if (recoupGroup.recoupFinishedPerCoin[coinIdx]) {
return;
}
- await putGroupAsFinished(ws, tx, recoupGroup, coinIdx);
- });
-}
-
-async function recoupWithdrawCoin(
- ws: InternalWalletState,
- recoupGroupId: string,
- coinIdx: number,
- coin: CoinRecord,
- cs: WithdrawCoinSource,
-): Promise<void> {
- const reservePub = cs.reservePub;
- const denomInfo = await ws.db
- .mktx((x) => [x.denominations])
- .runReadOnly(async (tx) => {
- const denomInfo = await ws.getDenomInfo(
- ws,
- tx,
- coin.exchangeBaseUrl,
- coin.denomPubHash,
- );
- return denomInfo;
- });
- if (!denomInfo) {
- // FIXME: We should at least emit some pending operation / warning for this?
- return;
- }
-
- ws.notify({
- type: NotificationType.RecoupStarted,
- });
-
- const recoupRequest = await ws.cryptoApi.createRecoupRequest({
- blindingKey: coin.blindingKey,
- coinPriv: coin.coinPriv,
- coinPub: coin.coinPub,
- denomPub: denomInfo.denomPub,
- denomPubHash: coin.denomPubHash,
- denomSig: coin.denomSig,
- });
- const reqUrl = new URL(`/coins/${coin.coinPub}/recoup`, coin.exchangeBaseUrl);
- logger.trace(`requesting recoup via ${reqUrl.href}`);
- const resp = await ws.http.postJson(reqUrl.href, recoupRequest);
- const recoupConfirmation = await readSuccessResponseJsonOrThrow(
- resp,
- codecForRecoupConfirmation(),
+ await putGroupAsFinished(wex, tx, recoupGroup, coinIdx);
+ },
);
-
- logger.trace(`got recoup confirmation ${j2s(recoupConfirmation)}`);
-
- if (recoupConfirmation.reserve_pub !== reservePub) {
- throw Error(`Coin's reserve doesn't match reserve on recoup`);
- }
-
- // FIXME: verify that our expectations about the amount match
-
- await ws.db
- .mktx((x) => [x.coins, x.denominations, x.recoupGroups, x.refreshGroups])
- .runReadWrite(async (tx) => {
- const recoupGroup = await tx.recoupGroups.get(recoupGroupId);
- if (!recoupGroup) {
- return;
- }
- if (recoupGroup.recoupFinishedPerCoin[coinIdx]) {
- return;
- }
- const updatedCoin = await tx.coins.get(coin.coinPub);
- if (!updatedCoin) {
- return;
- }
- updatedCoin.status = CoinStatus.Dormant;
- await tx.coins.put(updatedCoin);
- await putGroupAsFinished(ws, tx, recoupGroup, coinIdx);
- });
-
- ws.notify({
- type: NotificationType.RecoupFinished,
- });
}
async function recoupRefreshCoin(
- ws: InternalWalletState,
+ wex: WalletExecutionContext,
recoupGroupId: string,
coinIdx: number,
coin: CoinRecord,
cs: RefreshCoinSource,
): Promise<void> {
- const d = await ws.db
- .mktx((x) => [x.coins, x.denominations])
- .runReadOnly(async (tx) => {
- const denomInfo = await ws.getDenomInfo(
- ws,
+ const d = await wex.db.runReadOnlyTx(
+ { storeNames: ["coins", "denominations"] },
+ async (tx) => {
+ const denomInfo = await getDenomInfo(
+ wex,
tx,
coin.exchangeBaseUrl,
coin.denomPubHash,
@@ -210,17 +133,14 @@ async function recoupRefreshCoin(
return;
}
return { denomInfo };
- });
+ },
+ );
if (!d) {
// FIXME: We should at least emit some pending operation / warning for this?
return;
}
- ws.notify({
- type: NotificationType.RecoupStarted,
- });
-
- const recoupRequest = await ws.cryptoApi.createRecoupRefreshRequest({
+ const recoupRequest = await wex.cryptoApi.createRecoupRefreshRequest({
blindingKey: coin.blindingKey,
coinPriv: coin.coinPriv,
coinPub: coin.coinPub,
@@ -234,7 +154,10 @@ async function recoupRefreshCoin(
);
logger.trace(`making recoup request for ${coin.coinPub}`);
- const resp = await ws.http.postJson(reqUrl.href, recoupRequest);
+ const resp = await wex.http.fetch(reqUrl.href, {
+ method: "POST",
+ body: recoupRequest,
+ });
const recoupConfirmation = await readSuccessResponseJsonOrThrow(
resp,
codecForRecoupConfirmation(),
@@ -244,9 +167,9 @@ async function recoupRefreshCoin(
throw Error(`Coin's oldCoinPub doesn't match reserve on recoup`);
}
- await ws.db
- .mktx((x) => [x.coins, x.denominations, x.recoupGroups, x.refreshGroups])
- .runReadWrite(async (tx) => {
+ await wex.db.runReadWriteTx(
+ { storeNames: ["coins", "denominations", "recoupGroups", "refreshGroups"] },
+ async (tx) => {
const recoupGroup = await tx.recoupGroups.get(recoupGroupId);
if (!recoupGroup) {
return;
@@ -264,14 +187,14 @@ async function recoupRefreshCoin(
logger.warn("refresh old coin for recoup not found");
return;
}
- const oldCoinDenom = await ws.getDenomInfo(
- ws,
+ const oldCoinDenom = await getDenomInfo(
+ wex,
tx,
oldCoin.exchangeBaseUrl,
oldCoin.denomPubHash,
);
- const revokedCoinDenom = await ws.getDenomInfo(
- ws,
+ const revokedCoinDenom = await getDenomInfo(
+ wex,
tx,
revokedCoin.exchangeBaseUrl,
revokedCoin.denomPubHash,
@@ -296,46 +219,103 @@ async function recoupRefreshCoin(
}
await tx.coins.put(revokedCoin);
await tx.coins.put(oldCoin);
- await putGroupAsFinished(ws, tx, recoupGroup, coinIdx);
- });
+ await putGroupAsFinished(wex, tx, recoupGroup, coinIdx);
+ },
+ );
}
-export async function processRecoupGroup(
- ws: InternalWalletState,
+export async function recoupWithdrawCoin(
+ wex: WalletExecutionContext,
recoupGroupId: string,
- options: {
- forceNow?: boolean;
- } = {},
+ coinIdx: number,
+ coin: CoinRecord,
+ cs: WithdrawCoinSource,
): Promise<void> {
- await unwrapOperationHandlerResultOrThrow(
- await processRecoupGroupHandler(ws, recoupGroupId, options),
+ const reservePub = cs.reservePub;
+ const denomInfo = await wex.db.runReadOnlyTx(
+ { storeNames: ["denominations"] },
+ async (tx) => {
+ const denomInfo = await getDenomInfo(
+ wex,
+ tx,
+ coin.exchangeBaseUrl,
+ coin.denomPubHash,
+ );
+ return denomInfo;
+ },
+ );
+ if (!denomInfo) {
+ // FIXME: We should at least emit some pending operation / warning for this?
+ return;
+ }
+
+ const recoupRequest = await wex.cryptoApi.createRecoupRequest({
+ blindingKey: coin.blindingKey,
+ coinPriv: coin.coinPriv,
+ coinPub: coin.coinPub,
+ denomPub: denomInfo.denomPub,
+ denomPubHash: coin.denomPubHash,
+ denomSig: coin.denomSig,
+ });
+ const reqUrl = new URL(`/coins/${coin.coinPub}/recoup`, coin.exchangeBaseUrl);
+ logger.trace(`requesting recoup via ${reqUrl.href}`);
+ const resp = await wex.http.fetch(reqUrl.href, {
+ method: "POST",
+ body: recoupRequest,
+ });
+ const recoupConfirmation = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForRecoupConfirmation(),
+ );
+
+ logger.trace(`got recoup confirmation ${j2s(recoupConfirmation)}`);
+
+ if (recoupConfirmation.reserve_pub !== reservePub) {
+ throw Error(`Coin's reserve doesn't match reserve on recoup`);
+ }
+
+ // FIXME: verify that our expectations about the amount match
+ await wex.db.runReadWriteTx(
+ { storeNames: ["coins", "denominations", "recoupGroups", "refreshGroups"] },
+ async (tx) => {
+ const recoupGroup = await tx.recoupGroups.get(recoupGroupId);
+ if (!recoupGroup) {
+ return;
+ }
+ if (recoupGroup.recoupFinishedPerCoin[coinIdx]) {
+ return;
+ }
+ const updatedCoin = await tx.coins.get(coin.coinPub);
+ if (!updatedCoin) {
+ return;
+ }
+ updatedCoin.status = CoinStatus.Dormant;
+ await tx.coins.put(updatedCoin);
+ await putGroupAsFinished(wex, tx, recoupGroup, coinIdx);
+ },
);
- return;
}
-export async function processRecoupGroupHandler(
- ws: InternalWalletState,
+export async function processRecoupGroup(
+ wex: WalletExecutionContext,
recoupGroupId: string,
- options: {
- forceNow?: boolean;
- } = {},
-): Promise<OperationAttemptResult> {
- const forceNow = options.forceNow ?? false;
- let recoupGroup = await ws.db
- .mktx((x) => [x.recoupGroups])
- .runReadOnly(async (tx) => {
+): Promise<TaskRunResult> {
+ let recoupGroup = await wex.db.runReadOnlyTx(
+ { storeNames: ["recoupGroups"] },
+ async (tx) => {
return tx.recoupGroups.get(recoupGroupId);
- });
+ },
+ );
if (!recoupGroup) {
- return OperationAttemptResult.finishedEmpty();
+ return TaskRunResult.finished();
}
if (recoupGroup.timestampFinished) {
logger.trace("recoup group finished");
- return OperationAttemptResult.finishedEmpty();
+ return TaskRunResult.finished();
}
const ps = recoupGroup.coinPubs.map(async (x, i) => {
try {
- await processRecoup(ws, recoupGroupId, i);
+ await processRecoupForCoin(wex, recoupGroupId, i);
} catch (e) {
logger.warn(`processRecoup failed: ${e}`);
throw e;
@@ -343,18 +323,19 @@ export async function processRecoupGroupHandler(
});
await Promise.all(ps);
- recoupGroup = await ws.db
- .mktx((x) => [x.recoupGroups])
- .runReadOnly(async (tx) => {
+ recoupGroup = await wex.db.runReadOnlyTx(
+ { storeNames: ["recoupGroups"] },
+ async (tx) => {
return tx.recoupGroups.get(recoupGroupId);
- });
+ },
+ );
if (!recoupGroup) {
- return OperationAttemptResult.finishedEmpty();
+ return TaskRunResult.finished();
}
for (const b of recoupGroup.recoupFinishedPerCoin) {
if (!b) {
- return OperationAttemptResult.finishedEmpty();
+ return TaskRunResult.finished();
}
}
@@ -364,9 +345,9 @@ export async function processRecoupGroupHandler(
const reservePrivMap: Record<string, string> = {};
for (let i = 0; i < recoupGroup.coinPubs.length; i++) {
const coinPub = recoupGroup.coinPubs[i];
- await ws.db
- .mktx((x) => [x.coins, x.reserves])
- .runReadOnly(async (tx) => {
+ await wex.db.runReadOnlyTx(
+ { storeNames: ["coins", "reserves"] },
+ async (tx) => {
const coin = await tx.coins.get(coinPub);
if (!coin) {
throw Error(`Coin ${coinPub} not found, can't request recoup`);
@@ -381,7 +362,8 @@ export async function processRecoupGroupHandler(
reserveSet.add(coin.coinSource.reservePub);
reservePrivMap[coin.coinSource.reservePub] = reserve.reservePriv;
}
- });
+ },
+ );
}
for (const reservePub of reserveSet) {
@@ -391,16 +373,16 @@ export async function processRecoupGroupHandler(
);
logger.info(`querying reserve status for recoup via ${reserveUrl}`);
- const resp = await ws.http.get(reserveUrl.href);
+ const resp = await wex.http.fetch(reserveUrl.href);
const result = await readSuccessResponseJsonOrThrow(
resp,
codecForReserveStatus(),
);
- await internalCreateWithdrawalGroup(ws, {
+ await internalCreateWithdrawalGroup(wex, {
amount: Amounts.parseOrThrow(result.balance),
exchangeBaseUrl: recoupGroup.exchangeBaseUrl,
- reserveStatus: WithdrawalGroupStatus.QueryingStatus,
+ reserveStatus: WithdrawalGroupStatus.PendingQueryingStatus,
reserveKeyPair: {
pub: reservePub,
priv: reservePrivMap[reservePub],
@@ -411,44 +393,82 @@ export async function processRecoupGroupHandler(
});
}
- await ws.db
- .mktx((x) => [
- x.recoupGroups,
- x.coinAvailability,
- x.denominations,
- x.refreshGroups,
- x.coins,
- ])
- .runReadWrite(async (tx) => {
+ await wex.db.runReadWriteTx(
+ {
+ storeNames: [
+ "recoupGroups",
+ "coinAvailability",
+ "denominations",
+ "refreshGroups",
+ "refreshSessions",
+ "coins",
+ ],
+ },
+ async (tx) => {
const rg2 = await tx.recoupGroups.get(recoupGroupId);
if (!rg2) {
return;
}
- rg2.timestampFinished = TalerProtocolTimestamp.now();
+ rg2.timestampFinished = timestampPreciseToDb(TalerPreciseTimestamp.now());
+ rg2.operationStatus = RecoupOperationStatus.Finished;
if (rg2.scheduleRefreshCoins.length > 0) {
- const refreshGroupId = await createRefreshGroup(
- ws,
+ await createRefreshGroup(
+ wex,
tx,
+ Amounts.currencyOf(rg2.scheduleRefreshCoins[0].amount),
rg2.scheduleRefreshCoins,
RefreshReason.Recoup,
+ constructTransactionIdentifier({
+ tag: TransactionType.Recoup,
+ recoupGroupId: rg2.recoupGroupId,
+ }),
);
- processRefreshGroup(ws, refreshGroupId.refreshGroupId).catch((e) => {
- logger.error(`error while refreshing after recoup ${e}`);
- });
}
await tx.recoupGroups.put(rg2);
+ },
+ );
+ return TaskRunResult.finished();
+}
+
+export class RecoupTransactionContext implements TransactionContext {
+ abortTransaction(): Promise<void> {
+ throw new Error("Method not implemented.");
+ }
+ suspendTransaction(): Promise<void> {
+ throw new Error("Method not implemented.");
+ }
+ resumeTransaction(): Promise<void> {
+ throw new Error("Method not implemented.");
+ }
+ failTransaction(): Promise<void> {
+ throw new Error("Method not implemented.");
+ }
+ deleteTransaction(): Promise<void> {
+ throw new Error("Method not implemented.");
+ }
+ public transactionId: TransactionIdStr;
+ public taskId: TaskIdStr;
+
+ constructor(
+ public wex: WalletExecutionContext,
+ private recoupGroupId: string,
+ ) {
+ this.transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Recoup,
+ recoupGroupId,
});
- return OperationAttemptResult.finishedEmpty();
+ this.taskId = constructTaskIdentifier({
+ tag: PendingTaskType.Recoup,
+ recoupGroupId,
+ });
+ }
}
export async function createRecoupGroup(
- ws: InternalWalletState,
- tx: GetReadWriteAccess<{
- recoupGroups: typeof WalletStoresV1.recoupGroups;
- denominations: typeof WalletStoresV1.denominations;
- refreshGroups: typeof WalletStoresV1.refreshGroups;
- coins: typeof WalletStoresV1.coins;
- }>,
+ wex: WalletExecutionContext,
+ tx: WalletDbReadWriteTransaction<
+ ["recoupGroups", "denominations", "refreshGroups", "coins"]
+ >,
exchangeBaseUrl: string,
coinPubs: string[],
): Promise<string> {
@@ -459,16 +479,17 @@ export async function createRecoupGroup(
exchangeBaseUrl: exchangeBaseUrl,
coinPubs: coinPubs,
timestampFinished: undefined,
- timestampStarted: TalerProtocolTimestamp.now(),
+ timestampStarted: timestampPreciseToDb(TalerPreciseTimestamp.now()),
recoupFinishedPerCoin: coinPubs.map(() => false),
scheduleRefreshCoins: [],
+ operationStatus: RecoupOperationStatus.Pending,
};
for (let coinIdx = 0; coinIdx < coinPubs.length; coinIdx++) {
const coinPub = coinPubs[coinIdx];
const coin = await tx.coins.get(coinPub);
if (!coin) {
- await putGroupAsFinished(ws, tx, recoupGroup, coinIdx);
+ await putGroupAsFinished(wex, tx, recoupGroup, coinIdx);
continue;
}
await tx.coins.put(coin);
@@ -476,20 +497,24 @@ export async function createRecoupGroup(
await tx.recoupGroups.put(recoupGroup);
+ const ctx = new RecoupTransactionContext(wex, recoupGroupId);
+
+ wex.taskScheduler.startShepherdTask(ctx.taskId);
+
return recoupGroupId;
}
/**
* Run the recoup protocol for a single coin in a recoup group.
*/
-async function processRecoup(
- ws: InternalWalletState,
+async function processRecoupForCoin(
+ wex: WalletExecutionContext,
recoupGroupId: string,
coinIdx: number,
): Promise<void> {
- const coin = await ws.db
- .mktx((x) => [x.recoupGroups, x.coins])
- .runReadOnly(async (tx) => {
+ const coin = await wex.db.runReadOnlyTx(
+ { storeNames: ["coins", "recoupGroups"] },
+ async (tx) => {
const recoupGroup = await tx.recoupGroups.get(recoupGroupId);
if (!recoupGroup) {
return;
@@ -508,7 +533,8 @@ async function processRecoup(
throw Error(`Coin ${coinPub} not found, can't request recoup`);
}
return coin;
- });
+ },
+ );
if (!coin) {
return;
@@ -517,12 +543,12 @@ async function processRecoup(
const cs = coin.coinSource;
switch (cs.type) {
- case CoinSourceType.Tip:
- return recoupTipCoin(ws, recoupGroupId, coinIdx, coin);
+ case CoinSourceType.Reward:
+ return recoupRewardCoin(wex, recoupGroupId, coinIdx, coin);
case CoinSourceType.Refresh:
- return recoupRefreshCoin(ws, recoupGroupId, coinIdx, coin, cs);
+ return recoupRefreshCoin(wex, recoupGroupId, coinIdx, coin, cs);
case CoinSourceType.Withdraw:
- return recoupWithdrawCoin(ws, recoupGroupId, coinIdx, coin, cs);
+ return recoupWithdrawCoin(wex, recoupGroupId, coinIdx, coin, cs);
default:
throw Error("unknown coin source type");
}
diff --git a/packages/taler-wallet-core/src/refresh.ts b/packages/taler-wallet-core/src/refresh.ts
new file mode 100644
index 000000000..7800967e6
--- /dev/null
+++ b/packages/taler-wallet-core/src/refresh.ts
@@ -0,0 +1,1883 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * @fileoverview
+ * Implementation of the refresh transaction.
+ */
+
+/**
+ * Imports.
+ */
+import {
+ AgeCommitment,
+ AgeRestriction,
+ AmountJson,
+ Amounts,
+ amountToPretty,
+ assertUnreachable,
+ AsyncFlag,
+ checkDbInvariant,
+ codecForCoinHistoryResponse,
+ codecForExchangeMeltResponse,
+ codecForExchangeRevealResponse,
+ CoinPublicKeyString,
+ CoinRefreshRequest,
+ CoinStatus,
+ DenominationInfo,
+ DenomKeyType,
+ Duration,
+ encodeCrock,
+ ExchangeMeltRequest,
+ ExchangeProtocolVersion,
+ ExchangeRefreshRevealRequest,
+ fnutil,
+ ForceRefreshRequest,
+ getErrorDetailFromException,
+ getRandomBytes,
+ HashCodeString,
+ HttpStatusCode,
+ j2s,
+ Logger,
+ makeErrorDetail,
+ NotificationType,
+ RefreshReason,
+ TalerError,
+ TalerErrorCode,
+ TalerErrorDetail,
+ TalerPreciseTimestamp,
+ TransactionAction,
+ TransactionIdStr,
+ TransactionMajorState,
+ TransactionState,
+ TransactionType,
+ URL,
+ WalletNotification,
+} from "@gnu-taler/taler-util";
+import {
+ readSuccessResponseJsonOrThrow,
+ readTalerErrorResponse,
+ throwUnexpectedRequestError,
+} from "@gnu-taler/taler-util/http";
+import {
+ constructTaskIdentifier,
+ makeCoinsVisible,
+ PendingTaskType,
+ TaskIdStr,
+ TaskRunResult,
+ TaskRunResultType,
+ TombstoneTag,
+ TransactionContext,
+ TransitionResult,
+ TransitionResultType,
+} from "./common.js";
+import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js";
+import {
+ DerivedRefreshSession,
+ RefreshNewDenomInfo,
+} from "./crypto/cryptoTypes.js";
+import { CryptoApiStoppedError } from "./crypto/workers/crypto-dispatcher.js";
+import {
+ CoinAvailabilityRecord,
+ CoinRecord,
+ CoinSourceType,
+ DenominationRecord,
+ RefreshCoinStatus,
+ RefreshGroupPerExchangeInfo,
+ RefreshGroupRecord,
+ RefreshOperationStatus,
+ RefreshSessionRecord,
+ timestampPreciseToDb,
+ WalletDbReadOnlyTransaction,
+ WalletDbReadWriteTransaction,
+ WalletDbStoresArr,
+} from "./db.js";
+import { selectWithdrawalDenominations } from "./denomSelection.js";
+import {
+ constructTransactionIdentifier,
+ notifyTransition,
+ TransitionInfo,
+} from "./transactions.js";
+import {
+ EXCHANGE_COINS_LOCK,
+ getDenomInfo,
+ WalletExecutionContext,
+} from "./wallet.js";
+import { getCandidateWithdrawalDenomsTx } from "./withdraw.js";
+
+const logger = new Logger("refresh.ts");
+
+/**
+ * Update the materialized refresh transaction based
+ * on the refresh group record.
+ */
+async function updateRefreshTransaction(
+ ctx: RefreshTransactionContext,
+ tx: WalletDbReadWriteTransaction<
+ [
+ "refreshGroups",
+ "transactions",
+ "operationRetries",
+ "exchanges",
+ "exchangeDetails",
+ ]
+ >,
+): Promise<void> {}
+
+export class RefreshTransactionContext implements TransactionContext {
+ readonly transactionId: TransactionIdStr;
+ readonly taskId: TaskIdStr;
+
+ constructor(
+ public wex: WalletExecutionContext,
+ public refreshGroupId: string,
+ ) {
+ this.transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Refresh,
+ refreshGroupId,
+ });
+ this.taskId = constructTaskIdentifier({
+ tag: PendingTaskType.Refresh,
+ refreshGroupId,
+ });
+ }
+
+ /**
+ * Transition a withdrawal transaction.
+ * Extra object stores may be accessed during the transition.
+ */
+ async transition<StoreNameArray extends WalletDbStoresArr = []>(
+ opts: { extraStores?: StoreNameArray; transactionLabel?: string },
+ f: (
+ rec: RefreshGroupRecord | undefined,
+ tx: WalletDbReadWriteTransaction<
+ [
+ "refreshGroups",
+ "transactions",
+ "operationRetries",
+ "exchanges",
+ "exchangeDetails",
+ ...StoreNameArray,
+ ]
+ >,
+ ) => Promise<TransitionResult<RefreshGroupRecord>>,
+ ): Promise<TransitionInfo | undefined> {
+ const baseStores = [
+ "refreshGroups" as const,
+ "transactions" as const,
+ "operationRetries" as const,
+ "exchanges" as const,
+ "exchangeDetails" as const,
+ ];
+ let stores = opts.extraStores
+ ? [...baseStores, ...opts.extraStores]
+ : baseStores;
+ const transitionInfo = await this.wex.db.runReadWriteTx(
+ { storeNames: stores },
+ async (tx) => {
+ const wgRec = await tx.refreshGroups.get(this.refreshGroupId);
+ let oldTxState: TransactionState;
+ if (wgRec) {
+ oldTxState = computeRefreshTransactionState(wgRec);
+ } else {
+ oldTxState = {
+ major: TransactionMajorState.None,
+ };
+ }
+ const res = await f(wgRec, tx);
+ switch (res.type) {
+ case TransitionResultType.Transition: {
+ await tx.refreshGroups.put(res.rec);
+ await updateRefreshTransaction(this, tx);
+ const newTxState = computeRefreshTransactionState(res.rec);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ case TransitionResultType.Delete:
+ await tx.refreshGroups.delete(this.refreshGroupId);
+ await updateRefreshTransaction(this, tx);
+ return {
+ oldTxState,
+ newTxState: {
+ major: TransactionMajorState.None,
+ },
+ };
+ default:
+ return undefined;
+ }
+ },
+ );
+ notifyTransition(this.wex, this.transactionId, transitionInfo);
+ return transitionInfo;
+ }
+
+ async deleteTransaction(): Promise<void> {
+ await this.transition(
+ {
+ extraStores: ["tombstones"],
+ },
+ async (rec, tx) => {
+ if (!rec) {
+ return TransitionResult.stay();
+ }
+ await tx.tombstones.put({
+ id: TombstoneTag.DeleteRefreshGroup + ":" + this.refreshGroupId,
+ });
+ return TransitionResult.delete();
+ },
+ );
+ }
+
+ async suspendTransaction(): Promise<void> {
+ await this.transition({}, async (rec, tx) => {
+ if (!rec) {
+ return TransitionResult.stay();
+ }
+ switch (rec.operationStatus) {
+ case RefreshOperationStatus.Finished:
+ case RefreshOperationStatus.Suspended:
+ case RefreshOperationStatus.Failed:
+ return TransitionResult.stay();
+ case RefreshOperationStatus.Pending: {
+ rec.operationStatus = RefreshOperationStatus.Suspended;
+ return TransitionResult.transition(rec);
+ }
+ default:
+ assertUnreachable(rec.operationStatus);
+ }
+ });
+ }
+
+ async abortTransaction(): Promise<void> {
+ // Refresh transactions only support fail, not abort.
+ throw new Error("refresh transactions cannot be aborted");
+ }
+
+ async resumeTransaction(): Promise<void> {
+ await this.transition({}, async (rec, tx) => {
+ if (!rec) {
+ return TransitionResult.stay();
+ }
+ switch (rec.operationStatus) {
+ case RefreshOperationStatus.Finished:
+ case RefreshOperationStatus.Failed:
+ case RefreshOperationStatus.Pending:
+ return TransitionResult.stay();
+ case RefreshOperationStatus.Suspended: {
+ rec.operationStatus = RefreshOperationStatus.Pending;
+ return TransitionResult.transition(rec);
+ }
+ default:
+ assertUnreachable(rec.operationStatus);
+ }
+ });
+ }
+
+ async failTransaction(): Promise<void> {
+ await this.transition({}, async (rec, tx) => {
+ if (!rec) {
+ return TransitionResult.stay();
+ }
+ switch (rec.operationStatus) {
+ case RefreshOperationStatus.Finished:
+ case RefreshOperationStatus.Failed:
+ return TransitionResult.stay();
+ case RefreshOperationStatus.Pending:
+ case RefreshOperationStatus.Suspended: {
+ rec.operationStatus = RefreshOperationStatus.Failed;
+ return TransitionResult.transition(rec);
+ }
+ default:
+ assertUnreachable(rec.operationStatus);
+ }
+ });
+ }
+}
+
+export async function getTotalRefreshCost(
+ wex: WalletExecutionContext,
+ tx: WalletDbReadOnlyTransaction<["denominations"]>,
+ refreshedDenom: DenominationInfo,
+ amountLeft: AmountJson,
+): Promise<AmountJson> {
+ const cacheKey = `denom=${refreshedDenom.exchangeBaseUrl}/${
+ refreshedDenom.denomPubHash
+ };left=${Amounts.stringify(amountLeft)}`;
+ const cacheRes = wex.ws.refreshCostCache.get(cacheKey);
+ if (cacheRes) {
+ return cacheRes;
+ }
+ const allDenoms = await getCandidateWithdrawalDenomsTx(
+ wex,
+ tx,
+ refreshedDenom.exchangeBaseUrl,
+ Amounts.currencyOf(amountLeft),
+ );
+ const res = getTotalRefreshCostInternal(
+ allDenoms,
+ refreshedDenom,
+ amountLeft,
+ );
+ wex.ws.refreshCostCache.put(cacheKey, res);
+ return res;
+}
+
+/**
+ * Get the amount that we lose when refreshing a coin of the given denomination
+ * with a certain amount left.
+ *
+ * If the amount left is zero, then the refresh cost
+ * is also considered to be zero. If a refresh isn't possible (e.g. due to lack of
+ * the right denominations), then the cost is the full amount left.
+ *
+ * Considers refresh fees, withdrawal fees after refresh and amounts too small
+ * to refresh.
+ */
+export function getTotalRefreshCostInternal(
+ denoms: DenominationRecord[],
+ refreshedDenom: DenominationInfo,
+ amountLeft: AmountJson,
+): AmountJson {
+ const withdrawAmount = Amounts.sub(
+ amountLeft,
+ refreshedDenom.feeRefresh,
+ ).amount;
+ const denomMap = Object.fromEntries(denoms.map((x) => [x.denomPubHash, x]));
+ const withdrawDenoms = selectWithdrawalDenominations(
+ withdrawAmount,
+ denoms,
+ false,
+ );
+ const resultingAmount = Amounts.add(
+ Amounts.zeroOfCurrency(withdrawAmount.currency),
+ ...withdrawDenoms.selectedDenoms.map(
+ (d) => Amounts.mult(denomMap[d.denomPubHash].value, d.count).amount,
+ ),
+ ).amount;
+ const totalCost = Amounts.sub(amountLeft, resultingAmount).amount;
+ logger.trace(
+ `total refresh cost for ${amountToPretty(amountLeft)} is ${amountToPretty(
+ totalCost,
+ )}`,
+ );
+ return totalCost;
+}
+
+async function getCoinAvailabilityForDenom(
+ wex: WalletExecutionContext,
+ tx: WalletDbReadWriteTransaction<
+ ["coins", "coinAvailability", "denominations"]
+ >,
+ denom: DenominationInfo,
+ ageRestriction: number,
+): Promise<CoinAvailabilityRecord> {
+ checkDbInvariant(!!denom);
+ let car = await tx.coinAvailability.get([
+ denom.exchangeBaseUrl,
+ denom.denomPubHash,
+ ageRestriction,
+ ]);
+ if (!car) {
+ car = {
+ maxAge: ageRestriction,
+ value: denom.value,
+ currency: Amounts.currencyOf(denom.value),
+ denomPubHash: denom.denomPubHash,
+ exchangeBaseUrl: denom.exchangeBaseUrl,
+ freshCoinCount: 0,
+ visibleCoinCount: 0,
+ };
+ }
+ return car;
+}
+
+/**
+ * Create a refresh session for one particular coin inside a refresh group.
+ */
+async function initRefreshSession(
+ wex: WalletExecutionContext,
+ tx: WalletDbReadWriteTransaction<
+ ["refreshSessions", "coinAvailability", "coins", "denominations"]
+ >,
+ refreshGroup: RefreshGroupRecord,
+ coinIndex: number,
+): Promise<void> {
+ const refreshGroupId = refreshGroup.refreshGroupId;
+ logger.trace(
+ `creating refresh session for coin ${coinIndex} in refresh group ${refreshGroupId}`,
+ );
+ const oldCoinPub = refreshGroup.oldCoinPubs[coinIndex];
+ const oldCoin = await tx.coins.get(oldCoinPub);
+ if (!oldCoin) {
+ throw Error("Can't refresh, coin not found");
+ }
+
+ const exchangeBaseUrl = oldCoin.exchangeBaseUrl;
+
+ const sessionSecretSeed = encodeCrock(getRandomBytes(64));
+
+ const oldDenom = await getDenomInfo(
+ wex,
+ tx,
+ exchangeBaseUrl,
+ oldCoin.denomPubHash,
+ );
+
+ if (!oldDenom) {
+ throw Error("db inconsistent: denomination for coin not found");
+ }
+
+ const currency = refreshGroup.currency;
+
+ const availableDenoms = await getCandidateWithdrawalDenomsTx(
+ wex,
+ tx,
+ exchangeBaseUrl,
+ currency,
+ );
+
+ const availableAmount = Amounts.sub(
+ refreshGroup.inputPerCoin[coinIndex],
+ oldDenom.feeRefresh,
+ ).amount;
+
+ const newCoinDenoms = selectWithdrawalDenominations(
+ availableAmount,
+ availableDenoms,
+ wex.ws.config.testing.denomselAllowLate,
+ );
+
+ if (newCoinDenoms.selectedDenoms.length === 0) {
+ logger.trace(
+ `not refreshing, available amount ${amountToPretty(
+ availableAmount,
+ )} too small`,
+ );
+ refreshGroup.statusPerCoin[coinIndex] = RefreshCoinStatus.Finished;
+ return;
+ }
+
+ for (let i = 0; i < newCoinDenoms.selectedDenoms.length; i++) {
+ const dph = newCoinDenoms.selectedDenoms[i].denomPubHash;
+ const denom = await getDenomInfo(wex, tx, oldDenom.exchangeBaseUrl, dph);
+ if (!denom) {
+ logger.error(`denom ${dph} not in DB`);
+ continue;
+ }
+ const car = await getCoinAvailabilityForDenom(
+ wex,
+ tx,
+ denom,
+ oldCoin.maxAge,
+ );
+ car.pendingRefreshOutputCount =
+ (car.pendingRefreshOutputCount ?? 0) +
+ newCoinDenoms.selectedDenoms[i].count;
+ await tx.coinAvailability.put(car);
+ }
+
+ const newSession: RefreshSessionRecord = {
+ coinIndex,
+ refreshGroupId,
+ norevealIndex: undefined,
+ sessionSecretSeed: sessionSecretSeed,
+ newDenoms: newCoinDenoms.selectedDenoms.map((x) => ({
+ count: x.count,
+ denomPubHash: x.denomPubHash,
+ })),
+ amountRefreshOutput: Amounts.stringify(newCoinDenoms.totalCoinValue),
+ };
+ await tx.refreshSessions.put(newSession);
+}
+
+/**
+ * Uninitialize a refresh session.
+ *
+ * Adjust the coin availability of involved coins.
+ */
+async function destroyRefreshSession(
+ wex: WalletExecutionContext,
+ tx: WalletDbReadWriteTransaction<
+ ["denominations", "coinAvailability", "coins"]
+ >,
+ refreshGroup: RefreshGroupRecord,
+ refreshSession: RefreshSessionRecord,
+): Promise<void> {
+ for (let i = 0; i < refreshSession.newDenoms.length; i++) {
+ const oldCoin = await tx.coins.get(
+ refreshGroup.oldCoinPubs[refreshSession.coinIndex],
+ );
+ if (!oldCoin) {
+ continue;
+ }
+ const dph = refreshSession.newDenoms[i].denomPubHash;
+ const denom = await getDenomInfo(wex, tx, oldCoin.exchangeBaseUrl, dph);
+ if (!denom) {
+ logger.error(`denom ${dph} not in DB`);
+ continue;
+ }
+ const car = await getCoinAvailabilityForDenom(
+ wex,
+ tx,
+ denom,
+ oldCoin.maxAge,
+ );
+ checkDbInvariant(car.pendingRefreshOutputCount != null);
+ car.pendingRefreshOutputCount =
+ car.pendingRefreshOutputCount - refreshSession.newDenoms[i].count;
+ await tx.coinAvailability.put(car);
+ }
+}
+
+function getRefreshRequestTimeout(rg: RefreshGroupRecord): Duration {
+ return Duration.fromSpec({
+ seconds: 5,
+ });
+}
+
+/**
+ * Run the melt step of a refresh session.
+ *
+ * If the melt step succeeds or fails permanently,
+ * the status in the refresh group is updated.
+ *
+ * When a transient error occurs, an exception is thrown.
+ */
+async function refreshMelt(
+ wex: WalletExecutionContext,
+ refreshGroupId: string,
+ coinIndex: number,
+): Promise<void> {
+ const ctx = new RefreshTransactionContext(wex, refreshGroupId);
+ const d = await wex.db.runReadWriteTx(
+ {
+ storeNames: [
+ "refreshGroups",
+ "refreshSessions",
+ "coins",
+ "denominations",
+ ],
+ },
+ async (tx) => {
+ const refreshGroup = await tx.refreshGroups.get(refreshGroupId);
+ if (!refreshGroup) {
+ return;
+ }
+ const refreshSession = await tx.refreshSessions.get([
+ refreshGroupId,
+ coinIndex,
+ ]);
+ if (!refreshSession) {
+ return;
+ }
+ if (refreshSession.norevealIndex !== undefined) {
+ return;
+ }
+
+ const oldCoin = await tx.coins.get(refreshGroup.oldCoinPubs[coinIndex]);
+ checkDbInvariant(!!oldCoin, "melt coin doesn't exist");
+ const oldDenom = await getDenomInfo(
+ wex,
+ tx,
+ oldCoin.exchangeBaseUrl,
+ oldCoin.denomPubHash,
+ );
+ checkDbInvariant(
+ !!oldDenom,
+ "denomination for melted coin doesn't exist",
+ );
+
+ const newCoinDenoms: RefreshNewDenomInfo[] = [];
+
+ for (const dh of refreshSession.newDenoms) {
+ const newDenom = await getDenomInfo(
+ wex,
+ tx,
+ oldCoin.exchangeBaseUrl,
+ dh.denomPubHash,
+ );
+ checkDbInvariant(
+ !!newDenom,
+ "new denomination for refresh not in database",
+ );
+ newCoinDenoms.push({
+ count: dh.count,
+ denomPub: newDenom.denomPub,
+ denomPubHash: newDenom.denomPubHash,
+ feeWithdraw: newDenom.feeWithdraw,
+ value: Amounts.stringify(newDenom.value),
+ });
+ }
+ return { newCoinDenoms, oldCoin, oldDenom, refreshGroup, refreshSession };
+ },
+ );
+
+ if (!d) {
+ return;
+ }
+
+ const { newCoinDenoms, oldCoin, oldDenom, refreshGroup, refreshSession } = d;
+
+ let exchangeProtocolVersion: ExchangeProtocolVersion;
+ switch (d.oldDenom.denomPub.cipher) {
+ case DenomKeyType.Rsa: {
+ exchangeProtocolVersion = ExchangeProtocolVersion.V12;
+ break;
+ }
+ default:
+ throw Error("unsupported key type");
+ }
+
+ const derived = await wex.cryptoApi.deriveRefreshSession({
+ exchangeProtocolVersion,
+ kappa: 3,
+ meltCoinDenomPubHash: oldCoin.denomPubHash,
+ meltCoinPriv: oldCoin.coinPriv,
+ meltCoinPub: oldCoin.coinPub,
+ feeRefresh: Amounts.parseOrThrow(oldDenom.feeRefresh),
+ meltCoinMaxAge: oldCoin.maxAge,
+ meltCoinAgeCommitmentProof: oldCoin.ageCommitmentProof,
+ newCoinDenoms,
+ sessionSecretSeed: refreshSession.sessionSecretSeed,
+ });
+
+ const reqUrl = new URL(
+ `coins/${oldCoin.coinPub}/melt`,
+ oldCoin.exchangeBaseUrl,
+ );
+
+ let maybeAch: HashCodeString | undefined;
+ if (oldCoin.ageCommitmentProof) {
+ maybeAch = AgeRestriction.hashCommitment(
+ oldCoin.ageCommitmentProof.commitment,
+ );
+ }
+
+ const meltReqBody: ExchangeMeltRequest = {
+ coin_pub: oldCoin.coinPub,
+ confirm_sig: derived.confirmSig,
+ denom_pub_hash: oldCoin.denomPubHash,
+ denom_sig: oldCoin.denomSig,
+ rc: derived.hash,
+ value_with_fee: Amounts.stringify(derived.meltValueWithFee),
+ age_commitment_hash: maybeAch,
+ };
+
+ const resp = await wex.ws.runSequentialized(
+ [EXCHANGE_COINS_LOCK],
+ async () => {
+ return await wex.http.fetch(reqUrl.href, {
+ method: "POST",
+ body: meltReqBody,
+ timeout: getRefreshRequestTimeout(refreshGroup),
+ cancellationToken: wex.cancellationToken,
+ });
+ },
+ );
+
+ switch (resp.status) {
+ case HttpStatusCode.NotFound: {
+ const errDetail = await readTalerErrorResponse(resp);
+ await handleRefreshMeltNotFound(ctx, coinIndex, errDetail);
+ return;
+ }
+ case HttpStatusCode.Gone: {
+ const errDetail = await readTalerErrorResponse(resp);
+ await handleRefreshMeltGone(ctx, coinIndex, errDetail);
+ return;
+ }
+ case HttpStatusCode.Conflict: {
+ const errDetail = await readTalerErrorResponse(resp);
+ await handleRefreshMeltConflict(
+ ctx,
+ coinIndex,
+ errDetail,
+ derived,
+ oldCoin,
+ );
+ return;
+ }
+ case HttpStatusCode.Ok:
+ break;
+ default: {
+ const errDetail = await readTalerErrorResponse(resp);
+ throwUnexpectedRequestError(resp, errDetail);
+ }
+ }
+
+ const meltResponse = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForExchangeMeltResponse(),
+ );
+
+ const norevealIndex = meltResponse.noreveal_index;
+
+ refreshSession.norevealIndex = norevealIndex;
+
+ await wex.db.runReadWriteTx(
+ { storeNames: ["refreshGroups", "refreshSessions"] },
+ async (tx) => {
+ const rg = await tx.refreshGroups.get(refreshGroupId);
+ if (!rg) {
+ return;
+ }
+ if (rg.timestampFinished) {
+ return;
+ }
+ const rs = await tx.refreshSessions.get([refreshGroupId, coinIndex]);
+ if (!rs) {
+ return;
+ }
+ if (rs.norevealIndex !== undefined) {
+ return;
+ }
+ rs.norevealIndex = norevealIndex;
+ await tx.refreshSessions.put(rs);
+ },
+ );
+}
+
+async function handleRefreshMeltGone(
+ ctx: RefreshTransactionContext,
+ coinIndex: number,
+ errDetails: TalerErrorDetail,
+): Promise<void> {
+ // const expiredMsg = codecForDenominationExpiredMessage().decode(errDetails);
+
+ // FIXME: Validate signature.
+
+ await ctx.wex.db.runReadWriteTx(
+ {
+ storeNames: [
+ "refreshGroups",
+ "refreshSessions",
+ "coins",
+ "denominations",
+ "coinAvailability",
+ ],
+ },
+ async (tx) => {
+ const rg = await tx.refreshGroups.get(ctx.refreshGroupId);
+ if (!rg) {
+ return;
+ }
+ if (rg.timestampFinished) {
+ return;
+ }
+ if (rg.statusPerCoin[coinIndex] !== RefreshCoinStatus.Pending) {
+ return;
+ }
+ rg.statusPerCoin[coinIndex] = RefreshCoinStatus.Failed;
+ const refreshSession = await tx.refreshSessions.get([
+ ctx.refreshGroupId,
+ coinIndex,
+ ]);
+ if (!refreshSession) {
+ throw Error("db invariant failed: missing refresh session in database");
+ }
+ refreshSession.lastError = errDetails;
+ await destroyRefreshSession(ctx.wex, tx, rg, refreshSession);
+ await tx.refreshGroups.put(rg);
+ await tx.refreshSessions.put(refreshSession);
+ },
+ );
+}
+
+async function handleRefreshMeltConflict(
+ ctx: RefreshTransactionContext,
+ coinIndex: number,
+ errDetails: TalerErrorDetail,
+ derived: DerivedRefreshSession,
+ oldCoin: CoinRecord,
+): Promise<void> {
+ // Just log for better diagnostics here, error status
+ // will be handled later.
+ logger.error(
+ `melt request for ${Amounts.stringify(
+ derived.meltValueWithFee,
+ )} failed in refresh group ${ctx.refreshGroupId} due to conflict`,
+ );
+
+ const historySig = await ctx.wex.cryptoApi.signCoinHistoryRequest({
+ coinPriv: oldCoin.coinPriv,
+ coinPub: oldCoin.coinPub,
+ startOffset: 0,
+ });
+
+ const historyUrl = new URL(
+ `coins/${oldCoin.coinPub}/history`,
+ oldCoin.exchangeBaseUrl,
+ );
+
+ const historyResp = await ctx.wex.http.fetch(historyUrl.href, {
+ method: "GET",
+ headers: {
+ "Taler-Coin-History-Signature": historySig.sig,
+ },
+ cancellationToken: ctx.wex.cancellationToken,
+ });
+
+ const historyJson = await readSuccessResponseJsonOrThrow(
+ historyResp,
+ codecForCoinHistoryResponse(),
+ );
+ logger.info(`coin history: ${j2s(historyJson)}`);
+
+ // FIXME: If response seems wrong, report to auditor (in the future!);
+
+ await ctx.wex.db.runReadWriteTx(
+ {
+ storeNames: [
+ "refreshGroups",
+ "refreshSessions",
+ "denominations",
+ "coins",
+ "coinAvailability",
+ ],
+ },
+ async (tx) => {
+ const rg = await tx.refreshGroups.get(ctx.refreshGroupId);
+ if (!rg) {
+ return;
+ }
+ if (rg.timestampFinished) {
+ return;
+ }
+ if (rg.statusPerCoin[coinIndex] !== RefreshCoinStatus.Pending) {
+ return;
+ }
+ if (Amounts.isZero(historyJson.balance)) {
+ rg.statusPerCoin[coinIndex] = RefreshCoinStatus.Failed;
+ const refreshSession = await tx.refreshSessions.get([
+ ctx.refreshGroupId,
+ coinIndex,
+ ]);
+ if (!refreshSession) {
+ throw Error(
+ "db invariant failed: missing refresh session in database",
+ );
+ }
+ refreshSession.lastError = errDetails;
+ await tx.refreshGroups.put(rg);
+ await tx.refreshSessions.put(refreshSession);
+ } else {
+ // Try again with new denoms!
+ rg.inputPerCoin[coinIndex] = historyJson.balance;
+ const refreshSession = await tx.refreshSessions.get([
+ ctx.refreshGroupId,
+ coinIndex,
+ ]);
+ if (!refreshSession) {
+ throw Error(
+ "db invariant failed: missing refresh session in database",
+ );
+ }
+ await destroyRefreshSession(ctx.wex, tx, rg, refreshSession);
+ await tx.refreshSessions.delete([ctx.refreshGroupId, coinIndex]);
+ await initRefreshSession(ctx.wex, tx, rg, coinIndex);
+ }
+ },
+ );
+}
+
+async function handleRefreshMeltNotFound(
+ ctx: RefreshTransactionContext,
+ coinIndex: number,
+ errDetails: TalerErrorDetail,
+): Promise<void> {
+ // FIXME: Validate the exchange's error response
+ await ctx.wex.db.runReadWriteTx(
+ {
+ storeNames: [
+ "refreshGroups",
+ "refreshSessions",
+ "coins",
+ "denominations",
+ "coinAvailability",
+ ],
+ },
+ async (tx) => {
+ const rg = await tx.refreshGroups.get(ctx.refreshGroupId);
+ if (!rg) {
+ return;
+ }
+ if (rg.timestampFinished) {
+ return;
+ }
+ if (rg.statusPerCoin[coinIndex] !== RefreshCoinStatus.Pending) {
+ return;
+ }
+ rg.statusPerCoin[coinIndex] = RefreshCoinStatus.Failed;
+ const refreshSession = await tx.refreshSessions.get([
+ ctx.refreshGroupId,
+ coinIndex,
+ ]);
+ if (!refreshSession) {
+ throw Error("db invariant failed: missing refresh session in database");
+ }
+ await destroyRefreshSession(ctx.wex, tx, rg, refreshSession);
+ refreshSession.lastError = errDetails;
+ await tx.refreshGroups.put(rg);
+ await tx.refreshSessions.put(refreshSession);
+ },
+ );
+}
+
+export async function assembleRefreshRevealRequest(args: {
+ cryptoApi: TalerCryptoInterface;
+ derived: DerivedRefreshSession;
+ norevealIndex: number;
+ oldCoinPub: CoinPublicKeyString;
+ oldCoinPriv: string;
+ newDenoms: {
+ denomPubHash: string;
+ count: number;
+ }[];
+ oldAgeCommitment?: AgeCommitment;
+}): Promise<ExchangeRefreshRevealRequest> {
+ const {
+ derived,
+ norevealIndex,
+ cryptoApi,
+ oldCoinPriv,
+ oldCoinPub,
+ newDenoms,
+ } = args;
+ const privs = Array.from(derived.transferPrivs);
+ privs.splice(norevealIndex, 1);
+
+ const planchets = derived.planchetsForGammas[norevealIndex];
+ if (!planchets) {
+ throw Error("refresh index error");
+ }
+
+ const newDenomsFlat: string[] = [];
+ const linkSigs: string[] = [];
+
+ for (let i = 0; i < newDenoms.length; i++) {
+ const dsel = newDenoms[i];
+ for (let j = 0; j < dsel.count; j++) {
+ const newCoinIndex = linkSigs.length;
+ const linkSig = await cryptoApi.signCoinLink({
+ coinEv: planchets[newCoinIndex].coinEv,
+ newDenomHash: dsel.denomPubHash,
+ oldCoinPriv: oldCoinPriv,
+ oldCoinPub: oldCoinPub,
+ transferPub: derived.transferPubs[norevealIndex],
+ });
+ linkSigs.push(linkSig.sig);
+ newDenomsFlat.push(dsel.denomPubHash);
+ }
+ }
+
+ const req: ExchangeRefreshRevealRequest = {
+ coin_evs: planchets.map((x) => x.coinEv),
+ new_denoms_h: newDenomsFlat,
+ transfer_privs: privs,
+ transfer_pub: derived.transferPubs[norevealIndex],
+ link_sigs: linkSigs,
+ old_age_commitment: args.oldAgeCommitment?.publicKeys,
+ };
+ return req;
+}
+
+async function refreshReveal(
+ wex: WalletExecutionContext,
+ refreshGroupId: string,
+ coinIndex: number,
+): Promise<void> {
+ logger.trace(
+ `doing refresh reveal for ${refreshGroupId} (old coin ${coinIndex})`,
+ );
+ const ctx = new RefreshTransactionContext(wex, refreshGroupId);
+ const d = await wex.db.runReadOnlyTx(
+ {
+ storeNames: [
+ "refreshGroups",
+ "refreshSessions",
+ "coins",
+ "denominations",
+ ],
+ },
+ async (tx) => {
+ const refreshGroup = await tx.refreshGroups.get(refreshGroupId);
+ if (!refreshGroup) {
+ return;
+ }
+ const refreshSession = await tx.refreshSessions.get([
+ refreshGroupId,
+ coinIndex,
+ ]);
+ if (!refreshSession) {
+ return;
+ }
+ const norevealIndex = refreshSession.norevealIndex;
+ if (norevealIndex === undefined) {
+ throw Error("can't reveal without melting first");
+ }
+
+ const oldCoin = await tx.coins.get(refreshGroup.oldCoinPubs[coinIndex]);
+ checkDbInvariant(!!oldCoin, "melt coin doesn't exist");
+ const oldDenom = await getDenomInfo(
+ wex,
+ tx,
+ oldCoin.exchangeBaseUrl,
+ oldCoin.denomPubHash,
+ );
+ checkDbInvariant(
+ !!oldDenom,
+ "denomination for melted coin doesn't exist",
+ );
+
+ const newCoinDenoms: RefreshNewDenomInfo[] = [];
+
+ for (const dh of refreshSession.newDenoms) {
+ const newDenom = await getDenomInfo(
+ wex,
+ tx,
+ oldCoin.exchangeBaseUrl,
+ dh.denomPubHash,
+ );
+ checkDbInvariant(
+ !!newDenom,
+ "new denomination for refresh not in database",
+ );
+ newCoinDenoms.push({
+ count: dh.count,
+ denomPub: newDenom.denomPub,
+ denomPubHash: newDenom.denomPubHash,
+ feeWithdraw: newDenom.feeWithdraw,
+ value: Amounts.stringify(newDenom.value),
+ });
+ }
+ return {
+ oldCoin,
+ oldDenom,
+ newCoinDenoms,
+ refreshSession,
+ refreshGroup,
+ norevealIndex,
+ };
+ },
+ );
+
+ if (!d) {
+ return;
+ }
+
+ const {
+ oldCoin,
+ oldDenom,
+ newCoinDenoms,
+ refreshSession,
+ refreshGroup,
+ norevealIndex,
+ } = d;
+
+ let exchangeProtocolVersion: ExchangeProtocolVersion;
+ switch (d.oldDenom.denomPub.cipher) {
+ case DenomKeyType.Rsa: {
+ exchangeProtocolVersion = ExchangeProtocolVersion.V12;
+ break;
+ }
+ default:
+ throw Error("unsupported key type");
+ }
+
+ const derived = await wex.cryptoApi.deriveRefreshSession({
+ exchangeProtocolVersion,
+ kappa: 3,
+ meltCoinDenomPubHash: oldCoin.denomPubHash,
+ meltCoinPriv: oldCoin.coinPriv,
+ meltCoinPub: oldCoin.coinPub,
+ feeRefresh: Amounts.parseOrThrow(oldDenom.feeRefresh),
+ newCoinDenoms,
+ meltCoinMaxAge: oldCoin.maxAge,
+ meltCoinAgeCommitmentProof: oldCoin.ageCommitmentProof,
+ sessionSecretSeed: refreshSession.sessionSecretSeed,
+ });
+
+ const reqUrl = new URL(
+ `refreshes/${derived.hash}/reveal`,
+ oldCoin.exchangeBaseUrl,
+ );
+
+ const req = await assembleRefreshRevealRequest({
+ cryptoApi: wex.cryptoApi,
+ derived,
+ newDenoms: newCoinDenoms,
+ norevealIndex: norevealIndex,
+ oldCoinPriv: oldCoin.coinPriv,
+ oldCoinPub: oldCoin.coinPub,
+ oldAgeCommitment: oldCoin.ageCommitmentProof?.commitment,
+ });
+
+ const resp = await wex.ws.runSequentialized(
+ [EXCHANGE_COINS_LOCK],
+ async () => {
+ return await wex.http.fetch(reqUrl.href, {
+ body: req,
+ method: "POST",
+ timeout: getRefreshRequestTimeout(refreshGroup),
+ cancellationToken: wex.cancellationToken,
+ });
+ },
+ );
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ break;
+ case HttpStatusCode.Conflict:
+ case HttpStatusCode.Gone: {
+ const errDetail = await readTalerErrorResponse(resp);
+ await handleRefreshRevealError(ctx, coinIndex, errDetail);
+ return;
+ }
+ default: {
+ const errDetail = await readTalerErrorResponse(resp);
+ throwUnexpectedRequestError(resp, errDetail);
+ }
+ }
+
+ const reveal = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForExchangeRevealResponse(),
+ );
+
+ const coins: CoinRecord[] = [];
+
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Refresh,
+ refreshGroupId,
+ });
+
+ for (let i = 0; i < refreshSession.newDenoms.length; i++) {
+ const ncd = newCoinDenoms[i];
+ for (let j = 0; j < refreshSession.newDenoms[i].count; j++) {
+ const newCoinIndex = coins.length;
+ const pc = derived.planchetsForGammas[norevealIndex][newCoinIndex];
+ if (ncd.denomPub.cipher !== DenomKeyType.Rsa) {
+ throw Error("cipher unsupported");
+ }
+ const evSig = reveal.ev_sigs[newCoinIndex].ev_sig;
+ const denomSig = await wex.cryptoApi.unblindDenominationSignature({
+ planchet: {
+ blindingKey: pc.blindingKey,
+ denomPub: ncd.denomPub,
+ },
+ evSig,
+ });
+ const coin: CoinRecord = {
+ blindingKey: pc.blindingKey,
+ coinPriv: pc.coinPriv,
+ coinPub: pc.coinPub,
+ denomPubHash: ncd.denomPubHash,
+ denomSig,
+ exchangeBaseUrl: oldCoin.exchangeBaseUrl,
+ status: CoinStatus.Fresh,
+ coinSource: {
+ type: CoinSourceType.Refresh,
+ refreshGroupId,
+ oldCoinPub: refreshGroup.oldCoinPubs[coinIndex],
+ },
+ sourceTransactionId: transactionId,
+ coinEvHash: pc.coinEvHash,
+ maxAge: pc.maxAge,
+ ageCommitmentProof: pc.ageCommitmentProof,
+ spendAllocation: undefined,
+ };
+
+ coins.push(coin);
+ }
+ }
+
+ await wex.db.runReadWriteTx(
+ {
+ storeNames: [
+ "coins",
+ "denominations",
+ "coinAvailability",
+ "refreshGroups",
+ "refreshSessions",
+ ],
+ },
+ async (tx) => {
+ const rg = await tx.refreshGroups.get(refreshGroupId);
+ if (!rg) {
+ logger.warn("no refresh session found");
+ return;
+ }
+ if (rg.statusPerCoin[coinIndex] !== RefreshCoinStatus.Pending) {
+ return;
+ }
+ const rs = await tx.refreshSessions.get([refreshGroupId, coinIndex]);
+ if (!rs) {
+ return;
+ }
+ rg.statusPerCoin[coinIndex] = RefreshCoinStatus.Finished;
+ for (const coin of coins) {
+ const existingCoin = await tx.coins.get(coin.coinPub);
+ if (existingCoin) {
+ continue;
+ }
+ await tx.coins.add(coin);
+ const denomInfo = await getDenomInfo(
+ wex,
+ tx,
+ coin.exchangeBaseUrl,
+ coin.denomPubHash,
+ );
+ checkDbInvariant(!!denomInfo);
+ const car = await getCoinAvailabilityForDenom(
+ wex,
+ tx,
+ denomInfo,
+ coin.maxAge,
+ );
+ checkDbInvariant(
+ car.pendingRefreshOutputCount != null &&
+ car.pendingRefreshOutputCount > 0,
+ );
+ car.pendingRefreshOutputCount--;
+ car.freshCoinCount++;
+ await tx.coinAvailability.put(car);
+ }
+ await tx.refreshGroups.put(rg);
+ },
+ );
+ logger.trace("refresh finished (end of reveal)");
+}
+
+async function handleRefreshRevealError(
+ ctx: RefreshTransactionContext,
+ coinIndex: number,
+ errDetails: TalerErrorDetail,
+): Promise<void> {
+ await ctx.wex.db.runReadWriteTx(
+ {
+ storeNames: [
+ "refreshGroups",
+ "refreshSessions",
+ "coins",
+ "denominations",
+ "coinAvailability",
+ ],
+ },
+ async (tx) => {
+ const rg = await tx.refreshGroups.get(ctx.refreshGroupId);
+ if (!rg) {
+ return;
+ }
+ if (rg.timestampFinished) {
+ return;
+ }
+ if (rg.statusPerCoin[coinIndex] !== RefreshCoinStatus.Pending) {
+ return;
+ }
+ rg.statusPerCoin[coinIndex] = RefreshCoinStatus.Failed;
+ const refreshSession = await tx.refreshSessions.get([
+ ctx.refreshGroupId,
+ coinIndex,
+ ]);
+ if (!refreshSession) {
+ throw Error("db invariant failed: missing refresh session in database");
+ }
+ refreshSession.lastError = errDetails;
+ await destroyRefreshSession(ctx.wex, tx, rg, refreshSession);
+ await tx.refreshGroups.put(rg);
+ await tx.refreshSessions.put(refreshSession);
+ },
+ );
+}
+
+export async function processRefreshGroup(
+ wex: WalletExecutionContext,
+ refreshGroupId: string,
+): Promise<TaskRunResult> {
+ logger.trace(`processing refresh group ${refreshGroupId}`);
+
+ const refreshGroup = await wex.db.runReadOnlyTx(
+ { storeNames: ["refreshGroups"] },
+ async (tx) => tx.refreshGroups.get(refreshGroupId),
+ );
+ if (!refreshGroup) {
+ return TaskRunResult.finished();
+ }
+ if (refreshGroup.timestampFinished) {
+ return TaskRunResult.finished();
+ }
+
+ if (
+ wex.ws.config.testing.devModeActive &&
+ wex.ws.devExperimentState.blockRefreshes
+ ) {
+ 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) => {
+ 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}`);
+ }
+ errors.push(getErrorDetailFromException(x));
+ }),
+ );
+ await Promise.all(ps);
+ if (inShutdown) {
+ return TaskRunResult.finished();
+ }
+
+ const ctx = new RefreshTransactionContext(wex, refreshGroupId);
+
+ // We've processed all refresh session and can now update the
+ // status of the whole refresh group.
+
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["coins", "coinAvailability", "refreshGroups"] },
+ async (tx) => {
+ const rg = await tx.refreshGroups.get(refreshGroupId);
+ if (!rg) {
+ return;
+ }
+ switch (rg.operationStatus) {
+ case RefreshOperationStatus.Pending:
+ break;
+ default:
+ return undefined;
+ }
+ const oldTxState = computeRefreshTransactionState(rg);
+ const allFinal = fnutil.all(
+ rg.statusPerCoin,
+ (x) =>
+ x === RefreshCoinStatus.Finished || x === RefreshCoinStatus.Failed,
+ );
+ const anyFailed = fnutil.any(
+ rg.statusPerCoin,
+ (x) => x === RefreshCoinStatus.Failed,
+ );
+ if (allFinal) {
+ if (anyFailed) {
+ rg.timestampFinished = timestampPreciseToDb(
+ TalerPreciseTimestamp.now(),
+ );
+ rg.operationStatus = RefreshOperationStatus.Failed;
+ } else {
+ rg.timestampFinished = timestampPreciseToDb(
+ TalerPreciseTimestamp.now(),
+ );
+ rg.operationStatus = RefreshOperationStatus.Finished;
+ }
+ await makeCoinsVisible(wex, tx, ctx.transactionId);
+ await tx.refreshGroups.put(rg);
+ const newTxState = computeRefreshTransactionState(rg);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ return undefined;
+ },
+ );
+
+ if (transitionInfo) {
+ notifyTransition(wex, ctx.transactionId, transitionInfo);
+ return TaskRunResult.progress();
+ }
+
+ if (errors.length > 0) {
+ return {
+ type: TaskRunResultType.Error,
+ errorDetail: makeErrorDetail(
+ TalerErrorCode.WALLET_REFRESH_GROUP_INCOMPLETE,
+ {
+ numErrors: errors.length,
+ errors: errors.slice(0, 5),
+ },
+ ),
+ };
+ }
+
+ return TaskRunResult.backoff();
+}
+
+async function processRefreshSession(
+ wex: WalletExecutionContext,
+ refreshGroupId: string,
+ coinIndex: number,
+): Promise<void> {
+ logger.trace(
+ `processing refresh session for coin ${coinIndex} of group ${refreshGroupId}`,
+ );
+ let { refreshGroup, refreshSession } = await wex.db.runReadOnlyTx(
+ { storeNames: ["refreshGroups", "refreshSessions"] },
+ async (tx) => {
+ const rg = await tx.refreshGroups.get(refreshGroupId);
+ const rs = await tx.refreshSessions.get([refreshGroupId, coinIndex]);
+ return {
+ refreshGroup: rg,
+ refreshSession: rs,
+ };
+ },
+ );
+ if (!refreshGroup) {
+ return;
+ }
+ if (refreshGroup.statusPerCoin[coinIndex] === RefreshCoinStatus.Finished) {
+ return;
+ }
+ if (!refreshSession) {
+ // No refresh session for that coin.
+ return;
+ }
+ if (refreshSession.norevealIndex === undefined) {
+ await refreshMelt(wex, refreshGroupId, coinIndex);
+ }
+ await refreshReveal(wex, refreshGroupId, coinIndex);
+}
+
+export interface RefreshOutputInfo {
+ outputPerCoin: AmountJson[];
+ perExchangeInfo: Record<string, RefreshGroupPerExchangeInfo>;
+}
+
+export async function calculateRefreshOutput(
+ wex: WalletExecutionContext,
+ tx: WalletDbReadOnlyTransaction<
+ ["denominations", "coins", "refreshGroups", "coinAvailability"]
+ >,
+ currency: string,
+ oldCoinPubs: CoinRefreshRequest[],
+): Promise<RefreshOutputInfo> {
+ const estimatedOutputPerCoin: AmountJson[] = [];
+
+ const denomsPerExchange: Record<string, DenominationRecord[]> = {};
+
+ const infoPerExchange: Record<string, RefreshGroupPerExchangeInfo> = {};
+
+ for (const ocp of oldCoinPubs) {
+ const coin = await tx.coins.get(ocp.coinPub);
+ checkDbInvariant(!!coin, "coin must be in database");
+ const denom = await getDenomInfo(
+ wex,
+ tx,
+ coin.exchangeBaseUrl,
+ coin.denomPubHash,
+ );
+ checkDbInvariant(
+ !!denom,
+ "denomination for existing coin must be in database",
+ );
+ const refreshAmount = ocp.amount;
+ const cost = await getTotalRefreshCost(
+ wex,
+ tx,
+ denom,
+ Amounts.parseOrThrow(refreshAmount),
+ );
+ const output = Amounts.sub(refreshAmount, cost).amount;
+ let exchInfo = infoPerExchange[coin.exchangeBaseUrl];
+ if (!exchInfo) {
+ infoPerExchange[coin.exchangeBaseUrl] = exchInfo = {
+ outputEffective: Amounts.stringify(Amounts.zeroOfAmount(cost)),
+ };
+ }
+ exchInfo.outputEffective = Amounts.stringify(
+ Amounts.add(exchInfo.outputEffective, output).amount,
+ );
+ estimatedOutputPerCoin.push(output);
+ }
+
+ return {
+ outputPerCoin: estimatedOutputPerCoin,
+ perExchangeInfo: infoPerExchange,
+ };
+}
+
+async function applyRefreshToOldCoins(
+ wex: WalletExecutionContext,
+ tx: WalletDbReadWriteTransaction<
+ ["denominations", "coins", "refreshGroups", "coinAvailability"]
+ >,
+ oldCoinPubs: CoinRefreshRequest[],
+ refreshGroupId: string,
+): Promise<void> {
+ for (const ocp of oldCoinPubs) {
+ const coin = await tx.coins.get(ocp.coinPub);
+ checkDbInvariant(!!coin, "coin must be in database");
+ const denom = await getDenomInfo(
+ wex,
+ tx,
+ coin.exchangeBaseUrl,
+ coin.denomPubHash,
+ );
+ checkDbInvariant(
+ !!denom,
+ "denomination for existing coin must be in database",
+ );
+ switch (coin.status) {
+ case CoinStatus.Dormant:
+ break;
+ case CoinStatus.Fresh: {
+ coin.status = CoinStatus.Dormant;
+ const coinAv = await tx.coinAvailability.get([
+ coin.exchangeBaseUrl,
+ coin.denomPubHash,
+ coin.maxAge,
+ ]);
+ checkDbInvariant(!!coinAv);
+ checkDbInvariant(coinAv.freshCoinCount > 0);
+ coinAv.freshCoinCount--;
+ await tx.coinAvailability.put(coinAv);
+ break;
+ }
+ case CoinStatus.FreshSuspended: {
+ // For suspended coins, we don't have to adjust coin
+ // availability, as they are not counted as available.
+ coin.status = CoinStatus.Dormant;
+ break;
+ }
+ case CoinStatus.DenomLoss:
+ break;
+ default:
+ assertUnreachable(coin.status);
+ }
+ if (!coin.spendAllocation) {
+ coin.spendAllocation = {
+ amount: Amounts.stringify(ocp.amount),
+ // id: `txn:refresh:${refreshGroupId}`,
+ id: constructTransactionIdentifier({
+ tag: TransactionType.Refresh,
+ refreshGroupId,
+ }),
+ };
+ }
+ await tx.coins.put(coin);
+ }
+}
+
+export interface CreateRefreshGroupResult {
+ refreshGroupId: string;
+ notifications: WalletNotification[];
+}
+
+/**
+ * Create a refresh group for a list of coins.
+ *
+ * Refreshes the remaining amount on the coin, effectively capturing the remaining
+ * value in the refresh group.
+ *
+ * The caller must also ensure that the coins that should be refreshed exist
+ * in the current database transaction.
+ */
+export async function createRefreshGroup(
+ wex: WalletExecutionContext,
+ tx: WalletDbReadWriteTransaction<
+ [
+ "denominations",
+ "coins",
+ "refreshGroups",
+ "refreshSessions",
+ "coinAvailability",
+ ]
+ >,
+ currency: string,
+ oldCoinPubs: CoinRefreshRequest[],
+ refreshReason: RefreshReason,
+ originatingTransactionId: string | undefined,
+): Promise<CreateRefreshGroupResult> {
+ // FIXME: Check that involved exchanges are reasonably up-to-date.
+ // Otherwise, error out.
+
+ const refreshGroupId = encodeCrock(getRandomBytes(32));
+
+ const outInfo = await calculateRefreshOutput(wex, tx, currency, oldCoinPubs);
+
+ const estimatedOutputPerCoin = outInfo.outputPerCoin;
+
+ await applyRefreshToOldCoins(wex, tx, oldCoinPubs, refreshGroupId);
+
+ const refreshGroup: RefreshGroupRecord = {
+ operationStatus: RefreshOperationStatus.Pending,
+ currency,
+ timestampFinished: undefined,
+ statusPerCoin: oldCoinPubs.map(() => RefreshCoinStatus.Pending),
+ oldCoinPubs: oldCoinPubs.map((x) => x.coinPub),
+ originatingTransactionId,
+ reason: refreshReason,
+ refreshGroupId,
+ inputPerCoin: oldCoinPubs.map((x) => x.amount),
+ expectedOutputPerCoin: estimatedOutputPerCoin.map((x) =>
+ Amounts.stringify(x),
+ ),
+ infoPerExchange: outInfo.perExchangeInfo,
+ timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()),
+ };
+
+ if (oldCoinPubs.length == 0) {
+ logger.warn("created refresh group with zero coins");
+ refreshGroup.timestampFinished = timestampPreciseToDb(
+ TalerPreciseTimestamp.now(),
+ );
+ refreshGroup.operationStatus = RefreshOperationStatus.Finished;
+ }
+
+ for (let i = 0; i < oldCoinPubs.length; i++) {
+ await initRefreshSession(wex, tx, refreshGroup, i);
+ }
+
+ await tx.refreshGroups.put(refreshGroup);
+
+ const newTxState = computeRefreshTransactionState(refreshGroup);
+
+ logger.trace(`created refresh group ${refreshGroupId}`);
+
+ const ctx = new RefreshTransactionContext(wex, refreshGroupId);
+
+ // Shepherd the task.
+ // If the current transaction fails to commit the refresh
+ // group to the DB, the shepherd will give up.
+ wex.taskScheduler.startShepherdTask(ctx.taskId);
+
+ return {
+ refreshGroupId,
+ notifications: [
+ {
+ type: NotificationType.TransactionStateTransition,
+ transactionId: ctx.transactionId,
+ oldTxState: {
+ major: TransactionMajorState.None,
+ },
+ newTxState,
+ },
+ ],
+ };
+}
+
+export function computeRefreshTransactionState(
+ rg: RefreshGroupRecord,
+): TransactionState {
+ switch (rg.operationStatus) {
+ case RefreshOperationStatus.Finished:
+ return {
+ major: TransactionMajorState.Done,
+ };
+ case RefreshOperationStatus.Failed:
+ return {
+ major: TransactionMajorState.Failed,
+ };
+ case RefreshOperationStatus.Pending:
+ return {
+ major: TransactionMajorState.Pending,
+ };
+ case RefreshOperationStatus.Suspended:
+ return {
+ major: TransactionMajorState.Suspended,
+ };
+ }
+}
+
+export function computeRefreshTransactionActions(
+ rg: RefreshGroupRecord,
+): TransactionAction[] {
+ switch (rg.operationStatus) {
+ case RefreshOperationStatus.Finished:
+ return [TransactionAction.Delete];
+ case RefreshOperationStatus.Failed:
+ return [TransactionAction.Delete];
+ case RefreshOperationStatus.Pending:
+ return [
+ TransactionAction.Retry,
+ TransactionAction.Suspend,
+ TransactionAction.Fail,
+ ];
+ case RefreshOperationStatus.Suspended:
+ return [TransactionAction.Resume, TransactionAction.Fail];
+ }
+}
+
+export function getRefreshesForTransaction(
+ wex: WalletExecutionContext,
+ transactionId: string,
+): Promise<string[]> {
+ return wex.db.runReadOnlyTx({ storeNames: ["refreshGroups"] }, async (tx) => {
+ const groups =
+ await tx.refreshGroups.indexes.byOriginatingTransactionId.getAll(
+ transactionId,
+ );
+ return groups.map((x) =>
+ constructTransactionIdentifier({
+ tag: TransactionType.Refresh,
+ refreshGroupId: x.refreshGroupId,
+ }),
+ );
+ });
+}
+
+export interface ForceRefreshResult {
+ refreshGroupId: string;
+}
+
+export async function forceRefresh(
+ wex: WalletExecutionContext,
+ req: ForceRefreshRequest,
+): Promise<ForceRefreshResult> {
+ if (req.refreshCoinSpecs.length == 0) {
+ throw Error("refusing to create empty refresh group");
+ }
+ const res = await wex.db.runReadWriteTx(
+ {
+ storeNames: [
+ "refreshGroups",
+ "coinAvailability",
+ "refreshSessions",
+ "denominations",
+ "coins",
+ ],
+ },
+ async (tx) => {
+ let coinPubs: CoinRefreshRequest[] = [];
+ for (const c of req.refreshCoinSpecs) {
+ const coin = await tx.coins.get(c.coinPub);
+ if (!coin) {
+ throw Error(`coin (pubkey ${c}) not found`);
+ }
+ const denom = await getDenomInfo(
+ wex,
+ tx,
+ coin.exchangeBaseUrl,
+ coin.denomPubHash,
+ );
+ checkDbInvariant(!!denom);
+ coinPubs.push({
+ coinPub: c.coinPub,
+ amount: c.amount ?? denom.value,
+ });
+ }
+ return await createRefreshGroup(
+ wex,
+ tx,
+ Amounts.currencyOf(coinPubs[0].amount),
+ coinPubs,
+ RefreshReason.Manual,
+ undefined,
+ );
+ },
+ );
+
+ for (const notif of res.notifications) {
+ wex.ws.notify(notif);
+ }
+
+ return {
+ refreshGroupId: res.refreshGroupId,
+ };
+}
+
+/**
+ * Wait until a refresh operation is final.
+ */
+export async function waitRefreshFinal(
+ wex: WalletExecutionContext,
+ refreshGroupId: string,
+): Promise<void> {
+ const ctx = new RefreshTransactionContext(wex, refreshGroupId);
+ wex.taskScheduler.startShepherdTask(ctx.taskId);
+
+ // FIXME: Clean up using the new JS "using" / Symbol.dispose syntax.
+ const refreshNotifFlag = new AsyncFlag();
+ // Raise purchaseNotifFlag whenever we get a notification
+ // about our refresh.
+ const cancelNotif = wex.ws.addNotificationListener((notif) => {
+ if (
+ notif.type === NotificationType.TransactionStateTransition &&
+ notif.transactionId === ctx.transactionId
+ ) {
+ refreshNotifFlag.raise();
+ }
+ });
+ const unregisterOnCancelled = wex.cancellationToken.onCancelled(() => {
+ cancelNotif();
+ refreshNotifFlag.raise();
+ });
+
+ try {
+ await internalWaitRefreshFinal(ctx, refreshNotifFlag);
+ } catch (e) {
+ unregisterOnCancelled();
+ cancelNotif();
+ }
+}
+
+async function internalWaitRefreshFinal(
+ ctx: RefreshTransactionContext,
+ flag: AsyncFlag,
+): Promise<void> {
+ while (true) {
+ if (ctx.wex.cancellationToken.isCancelled) {
+ throw Error("cancelled");
+ }
+
+ // Check if refresh is final
+ const res = await ctx.wex.db.runReadOnlyTx(
+ { storeNames: ["refreshGroups", "operationRetries"] },
+ async (tx) => {
+ return {
+ rg: await tx.refreshGroups.get(ctx.refreshGroupId),
+ };
+ },
+ );
+ const { rg } = res;
+ if (!rg) {
+ // Must've been deleted, we consider that final.
+ return;
+ }
+ switch (rg.operationStatus) {
+ case RefreshOperationStatus.Failed:
+ case RefreshOperationStatus.Finished:
+ // Transaction is final
+ return;
+ case RefreshOperationStatus.Pending:
+ case RefreshOperationStatus.Suspended:
+ break;
+ }
+
+ // Wait for the next transition
+ await flag.wait();
+ flag.reset();
+ }
+}
diff --git a/packages/taler-wallet-core/src/remote.ts b/packages/taler-wallet-core/src/remote.ts
new file mode 100644
index 000000000..d7623baab
--- /dev/null
+++ b/packages/taler-wallet-core/src/remote.ts
@@ -0,0 +1,191 @@
+/*
+ This file is part of GNU Taler
+ (C) 2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ CoreApiRequestEnvelope,
+ CoreApiResponse,
+ Logger,
+ OpenedPromise,
+ openPromise,
+ TalerError,
+ WalletNotification,
+} from "@gnu-taler/taler-util";
+import { connectRpc, JsonMessage } from "@gnu-taler/taler-util/twrpc";
+import { WalletCoreApiClient } from "./wallet-api-types.js";
+
+const logger = new Logger("remote.ts");
+
+export interface RemoteWallet {
+ /**
+ * Low-level interface for making API requests to wallet-core.
+ */
+ makeCoreApiRequest(
+ operation: string,
+ payload: unknown,
+ ): Promise<CoreApiResponse>;
+
+ /**
+ * Close the connection to the remote wallet.
+ */
+ close(): void;
+}
+
+export interface RemoteWalletConnectArgs {
+ name?: string;
+ socketFilename: string;
+ notificationHandler?: (n: WalletNotification) => void;
+}
+
+export async function createRemoteWallet(
+ args: RemoteWalletConnectArgs,
+): Promise<RemoteWallet> {
+ let nextRequestId = 1;
+ let requestMap: Map<
+ string,
+ {
+ promiseCapability: OpenedPromise<CoreApiResponse>;
+ }
+ > = new Map();
+
+ const ctx = await connectRpc<RemoteWallet>({
+ socketFilename: args.socketFilename,
+ onEstablished(connection) {
+ const ctx: RemoteWallet = {
+ makeCoreApiRequest(operation, payload) {
+ const id = `req-${nextRequestId}`;
+ nextRequestId += 1;
+ const req: CoreApiRequestEnvelope = {
+ operation,
+ id,
+ args: payload,
+ };
+ const promiseCap = openPromise<CoreApiResponse>();
+ requestMap.set(id, {
+ promiseCapability: promiseCap,
+ });
+ connection.sendMessage(req as unknown as JsonMessage);
+ return promiseCap.promise;
+ },
+ close() {
+ connection.close();
+ },
+ };
+ return {
+ result: ctx,
+ onDisconnect() {
+ logger.info(`${args.name}: remote wallet disconnected`);
+ },
+ onMessage(m) {
+ // FIXME: use a codec for parsing the response envelope!
+
+ if (typeof m !== "object" || m == null) {
+ logger.warn(`${args.name}: message not understood (wrong type)`);
+ return;
+ }
+ const type = (m as any).type;
+ if (type === "response" || type === "error") {
+ const id = (m as any).id;
+ if (typeof id !== "string") {
+ logger.warn(
+ `${args.name}: message not understood (no id in response)`,
+ );
+ return;
+ }
+ const h = requestMap.get(id);
+ if (!h) {
+ logger.warn(
+ `${args.name}: no handler registered for response id ${id}`,
+ );
+ return;
+ }
+ h.promiseCapability.resolve(m as any);
+ } else if (type === "notification") {
+ if (args.notificationHandler) {
+ args.notificationHandler((m as any).payload);
+ }
+ } else {
+ logger.warn(`${args.name}: message not understood`);
+ }
+ },
+ };
+ },
+ });
+ return ctx;
+}
+
+/**
+ * Get a high-level API client from a remove wallet.
+ */
+export function getClientFromRemoteWallet(
+ w: RemoteWallet,
+): WalletCoreApiClient {
+ const client: WalletCoreApiClient = {
+ async call(op, payload): Promise<any> {
+ const res = await w.makeCoreApiRequest(op, payload);
+ switch (res.type) {
+ case "error":
+ throw TalerError.fromUncheckedDetail(res.error);
+ case "response":
+ return res.result;
+ }
+ },
+ };
+ return client;
+}
+
+export interface WalletNotificationWaiter {
+ notify(wn: WalletNotification): void;
+ waitForNotificationCond<T>(
+ cond: (n: WalletNotification) => T | false | undefined,
+ ): Promise<T>;
+}
+
+interface NotificationCondEntry<T> {
+ condition: (n: WalletNotification) => T | false | undefined;
+ promiseCapability: OpenedPromise<T>;
+}
+
+/**
+ * Helper that allows creating a promise that resolves when the
+ * wallet
+ */
+export function makeNotificationWaiter(): WalletNotificationWaiter {
+ // Bookkeeping for waiting on notification conditions
+ let nextCondIndex = 1;
+ const condMap: Map<number, NotificationCondEntry<any>> = new Map();
+ function onNotification(n: WalletNotification) {
+ condMap.forEach((cond, condKey) => {
+ const res = cond.condition(n);
+ if (res) {
+ cond.promiseCapability.resolve(res);
+ }
+ });
+ }
+ function waitForNotificationCond<T>(
+ cond: (n: WalletNotification) => T | false | undefined,
+ ) {
+ const promCap = openPromise<T>();
+ condMap.set(nextCondIndex++, {
+ condition: cond,
+ promiseCapability: promCap,
+ });
+ return promCap.promise;
+ }
+ return {
+ waitForNotificationCond,
+ notify: onNotification,
+ };
+}
diff --git a/packages/taler-wallet-core/src/shepherd.ts b/packages/taler-wallet-core/src/shepherd.ts
new file mode 100644
index 000000000..5e2d23fd9
--- /dev/null
+++ b/packages/taler-wallet-core/src/shepherd.ts
@@ -0,0 +1,1119 @@
+/*
+ This file is part of GNU Taler
+ (C) 2024 Taler Systems SA
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received 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 { GlobalIDB } from "@gnu-taler/idb-bridge";
+import {
+ AbsoluteTime,
+ AsyncCondition,
+ CancellationToken,
+ Duration,
+ Logger,
+ NotificationType,
+ ObservabilityContext,
+ ObservabilityEventType,
+ TalerErrorDetail,
+ TaskThrottler,
+ TransactionIdStr,
+ TransactionState,
+ TransactionType,
+ WalletNotification,
+ assertUnreachable,
+ getErrorDetailFromException,
+ j2s,
+ safeStringifyException,
+} from "@gnu-taler/taler-util";
+import { processBackupForProvider } from "./backup/index.js";
+import {
+ DbRetryInfo,
+ PendingTaskType,
+ TaskIdStr,
+ TaskRunResult,
+ TaskRunResultType,
+ constructTaskIdentifier,
+ getExchangeState,
+ parseTaskIdentifier,
+} from "./common.js";
+import {
+ OPERATION_STATUS_ACTIVE_FIRST,
+ OPERATION_STATUS_ACTIVE_LAST,
+ OperationRetryRecord,
+ WalletDbAllStoresReadOnlyTransaction,
+ WalletDbReadOnlyTransaction,
+ timestampAbsoluteFromDb,
+} from "./db.js";
+import {
+ computeDepositTransactionStatus,
+ processDepositGroup,
+} from "./deposits.js";
+import {
+ computeDenomLossTransactionStatus,
+ updateExchangeFromUrlHandler,
+} from "./exchanges.js";
+import {
+ computePayMerchantTransactionState,
+ computeRefundTransactionState,
+ processPurchase,
+} from "./pay-merchant.js";
+import {
+ computePeerPullCreditTransactionState,
+ processPeerPullCredit,
+} from "./pay-peer-pull-credit.js";
+import {
+ computePeerPullDebitTransactionState,
+ processPeerPullDebit,
+} from "./pay-peer-pull-debit.js";
+import {
+ computePeerPushCreditTransactionState,
+ processPeerPushCredit,
+} from "./pay-peer-push-credit.js";
+import {
+ computePeerPushDebitTransactionState,
+ processPeerPushDebit,
+} from "./pay-peer-push-debit.js";
+import { processRecoupGroup } from "./recoup.js";
+import {
+ computeRefreshTransactionState,
+ processRefreshGroup,
+} from "./refresh.js";
+import {
+ constructTransactionIdentifier,
+ parseTransactionIdentifier,
+} from "./transactions.js";
+import {
+ InternalWalletState,
+ WalletExecutionContext,
+ getNormalWalletExecutionContext,
+ getObservedWalletExecutionContext,
+} from "./wallet.js";
+import {
+ computeWithdrawalTransactionStatus,
+ processWithdrawalGroup,
+} from "./withdraw.js";
+
+const logger = new Logger("shepherd.ts");
+
+/**
+ * Info about one task being shepherded.
+ */
+interface ShepherdInfo {
+ cts: CancellationToken.Source;
+}
+
+/**
+ * Check if a task is alive, i.e. whether it prevents
+ * the main task loop from exiting.
+ */
+function taskGivesLiveness(taskId: string): boolean {
+ const parsedTaskId = parseTaskIdentifier(taskId);
+ switch (parsedTaskId.tag) {
+ case PendingTaskType.Backup:
+ case PendingTaskType.ExchangeUpdate:
+ return false;
+ case PendingTaskType.Deposit:
+ case PendingTaskType.PeerPullCredit:
+ case PendingTaskType.PeerPullDebit:
+ case PendingTaskType.PeerPushCredit:
+ case PendingTaskType.Refresh:
+ case PendingTaskType.Recoup:
+ case PendingTaskType.RewardPickup:
+ case PendingTaskType.Withdraw:
+ case PendingTaskType.PeerPushDebit:
+ case PendingTaskType.Purchase:
+ return true;
+ default:
+ assertUnreachable(parsedTaskId);
+ }
+}
+
+export interface TaskScheduler {
+ ensureRunning(): Promise<void>;
+ startShepherdTask(taskId: TaskIdStr): void;
+ stopShepherdTask(taskId: TaskIdStr): void;
+ resetTaskRetries(taskId: TaskIdStr): Promise<void>;
+ reload(): Promise<void>;
+ getActiveTasks(): TaskIdStr[];
+ isIdle(): boolean;
+}
+
+export class TaskSchedulerImpl implements TaskScheduler {
+ private sheps: Map<TaskIdStr, ShepherdInfo> = new Map();
+
+ private iterCond = new AsyncCondition();
+
+ private throttler = new TaskThrottler();
+
+ isRunning: boolean = false;
+
+ constructor(private ws: InternalWalletState) {}
+
+ private async loadTasksFromDb(): Promise<void> {
+ const activeTasks = await getActiveTaskIds(this.ws);
+
+ logger.info(`active tasks from DB: ${j2s(activeTasks)}`);
+
+ for (const tid of activeTasks.taskIds) {
+ this.startShepherdTask(tid);
+ }
+ }
+
+ getActiveTasks(): TaskIdStr[] {
+ return [...this.sheps.keys()];
+ }
+
+ async ensureRunning(): Promise<void> {
+ if (this.isRunning) {
+ return;
+ }
+ this.isRunning = true;
+ try {
+ await this.loadTasksFromDb();
+ } catch (e) {
+ this.isRunning = false;
+ throw e;
+ }
+ this.run()
+ .catch((e) => {
+ logger.error("error running task loop");
+ logger.error(`err: ${e}`);
+ })
+ .then(() => {
+ logger.info("done running task loop");
+ this.isRunning = false;
+ });
+ }
+
+ isIdle(): boolean {
+ let alive = false;
+ const taskIds = [...this.sheps.keys()];
+ for (const taskId of taskIds) {
+ if (taskGivesLiveness(taskId)) {
+ alive = true;
+ break;
+ }
+ }
+ // We're idle if no task is alive anymore.
+ return !alive;
+ }
+
+ private async run(): Promise<void> {
+ logger.info("Running task loop.");
+ logger.info(`sheps: ${this.sheps.size}`);
+ while (true) {
+ if (this.ws.stopped) {
+ logger.info("Breaking out of task loop (wallet stopped).");
+ break;
+ }
+
+ if (this.isIdle()) {
+ this.ws.notify({
+ type: NotificationType.Idle,
+ });
+ }
+
+ await this.iterCond.wait();
+ }
+ logger.info("Done with task loop.");
+ }
+
+ startShepherdTask(taskId: TaskIdStr): void {
+ this.ensureRunning().catch((e) => {
+ logger.error(`error running scheduler: ${safeStringifyException(e)}`);
+ });
+ // Run in the background, no await!
+ this.internalStartShepherdTask(taskId);
+ }
+
+ /**
+ * Stop and re-load all existing tasks.
+ *
+ * Mostly useful to interrupt all waits when time-travelling.
+ */
+ async reload(): Promise<void> {
+ await this.ensureRunning();
+ const tasksIds = [...this.sheps.keys()];
+ logger.info(`reloading sheperd with ${tasksIds.length} tasks`);
+ for (const taskId of tasksIds) {
+ this.stopShepherdTask(taskId);
+ }
+ for (const taskId of tasksIds) {
+ this.startShepherdTask(taskId);
+ }
+ }
+
+ private async internalStartShepherdTask(taskId: TaskIdStr): Promise<void> {
+ logger.trace(`Starting to shepherd task ${taskId}`);
+ const oldShep = this.sheps.get(taskId);
+ if (oldShep) {
+ logger.trace(`Already have a shepherd for ${taskId}`);
+ return;
+ }
+ logger.trace(`Creating new shepherd for ${taskId}`);
+ const newShep: ShepherdInfo = {
+ cts: CancellationToken.create(),
+ };
+ this.sheps.set(taskId, newShep);
+ try {
+ await this.internalShepherdTask(taskId, newShep);
+ } finally {
+ logger.trace(`Done shepherding ${taskId}`);
+ this.sheps.delete(taskId);
+ this.iterCond.trigger();
+ }
+ }
+
+ stopShepherdTask(taskId: TaskIdStr): void {
+ logger.trace(`Stopping shepherding of ${taskId}`);
+ const oldShep = this.sheps.get(taskId);
+ if (oldShep) {
+ logger.trace(`Cancelling old shepherd for ${taskId}`);
+ oldShep.cts.cancel();
+ this.sheps.delete(taskId);
+ this.iterCond.trigger();
+ }
+ }
+
+ restartShepherdTask(taskId: TaskIdStr): void {
+ this.stopShepherdTask(taskId);
+ this.startShepherdTask(taskId);
+ }
+
+ async resetTaskRetries(taskId: TaskIdStr): Promise<void> {
+ const maybeNotification = await this.ws.db.runAllStoresReadWriteTx(
+ {},
+ async (tx) => {
+ await tx.operationRetries.delete(taskId);
+ return taskToRetryNotification(this.ws, tx, taskId, undefined);
+ },
+ );
+ this.stopShepherdTask(taskId);
+ if (maybeNotification) {
+ this.ws.notify(maybeNotification);
+ }
+ this.startShepherdTask(taskId);
+ }
+
+ private async wait(
+ taskId: TaskIdStr,
+ info: ShepherdInfo,
+ delay: Duration,
+ ): Promise<void> {
+ try {
+ await info.cts.token.racePromise(this.ws.timerGroup.resolveAfter(delay));
+ } catch (e) {
+ logger.info(`waiting for ${taskId} interrupted`);
+ }
+ }
+
+ private async internalShepherdTask(
+ taskId: TaskIdStr,
+ info: ShepherdInfo,
+ ): Promise<void> {
+ while (true) {
+ if (this.ws.stopped) {
+ logger.trace(`Shepherd for ${taskId} stopping as wallet is stopped`);
+ return;
+ }
+ if (info.cts.token.isCancelled) {
+ logger.trace(`Shepherd for ${taskId} got cancelled`);
+ return;
+ }
+ const isThrottled = this.throttler.applyThrottle(taskId);
+ if (isThrottled) {
+ logger.warn(
+ `task ${taskId} throttled, this is very likely a bug in wallet-core, please report`,
+ );
+ logger.warn("waiting for 60 seconds");
+ await this.ws.timerGroup.resolveAfter(
+ Duration.fromSpec({ seconds: 60 }),
+ );
+ }
+ const wex = getWalletExecutionContextForTask(
+ this.ws,
+ taskId,
+ info.cts.token,
+ );
+ const startTime = AbsoluteTime.now();
+ logger.trace(`Shepherd for ${taskId} will call handler`);
+ let res: TaskRunResult;
+ try {
+ res = await callOperationHandlerForTaskId(wex, taskId);
+ } catch (e) {
+ res = {
+ type: TaskRunResultType.Error,
+ errorDetail: getErrorDetailFromException(e),
+ };
+ }
+ if (info.cts.token.isCancelled) {
+ logger.info("task cancelled, not processing result");
+ return;
+ }
+ if (this.ws.stopped) {
+ logger.info("wallet stopped, not processing result");
+ return;
+ }
+ wex.oc.observe({
+ type: ObservabilityEventType.ShepherdTaskResult,
+ resultType: res.type,
+ });
+ switch (res.type) {
+ case TaskRunResultType.Error: {
+ logger.trace(`Shepherd for ${taskId} got error result.`);
+ const retryRecord = await storePendingTaskError(
+ this.ws,
+ taskId,
+ res.errorDetail,
+ );
+ const t = timestampAbsoluteFromDb(retryRecord.retryInfo.nextRetry);
+ const delay = AbsoluteTime.remaining(t);
+ logger.trace(`Waiting for ${delay.d_ms} ms`);
+ await this.wait(taskId, info, delay);
+ break;
+ }
+ case TaskRunResultType.Backoff: {
+ logger.trace(`Shepherd for ${taskId} got backoff result.`);
+ const retryRecord = await storePendingTaskPending(this.ws, taskId);
+ const t = timestampAbsoluteFromDb(retryRecord.retryInfo.nextRetry);
+ const delay = AbsoluteTime.remaining(t);
+ logger.trace(`Waiting for ${delay.d_ms} ms`);
+ await this.wait(taskId, info, delay);
+ break;
+ }
+ case TaskRunResultType.Progress: {
+ logger.trace(
+ `Shepherd for ${taskId} got progress result, re-running immediately.`,
+ );
+ await storeTaskProgress(this.ws, taskId);
+ break;
+ }
+ case TaskRunResultType.ScheduleLater: {
+ logger.trace(`Shepherd for ${taskId} got schedule-later result.`);
+ await storeTaskProgress(this.ws, taskId);
+ const delay = AbsoluteTime.remaining(res.runAt);
+ logger.trace(`Waiting for ${delay.d_ms} ms`);
+ await this.wait(taskId, info, delay);
+ break;
+ }
+ case TaskRunResultType.Finished:
+ logger.trace(`Shepherd for ${taskId} got finished result.`);
+ await storePendingTaskFinished(this.ws, taskId);
+ return;
+ case TaskRunResultType.LongpollReturnedPending: {
+ await storeTaskProgress(this.ws, taskId);
+ // Make sure that we are waiting a bit if long-polling returned too early.
+ const endTime = AbsoluteTime.now();
+ const taskDuration = AbsoluteTime.difference(endTime, startTime);
+ if (
+ Duration.cmp(taskDuration, Duration.fromSpec({ seconds: 20 })) < 0
+ ) {
+ logger.info(
+ `long-poller for ${taskId} returned unexpectedly early (${taskDuration.d_ms} ms), waiting 10 seconds`,
+ );
+ await this.wait(taskId, info, Duration.fromSpec({ seconds: 10 }));
+ } else {
+ logger.info(`task ${taskId} will long-poll again`);
+ }
+ break;
+ }
+ default:
+ assertUnreachable(res);
+ }
+ }
+ }
+}
+
+async function storePendingTaskError(
+ ws: InternalWalletState,
+ pendingTaskId: string,
+ e: TalerErrorDetail,
+): Promise<OperationRetryRecord> {
+ logger.info(`storing pending task error for ${pendingTaskId}`);
+ const res = await ws.db.runAllStoresReadWriteTx({}, async (tx) => {
+ let retryRecord = await tx.operationRetries.get(pendingTaskId);
+ if (!retryRecord) {
+ retryRecord = {
+ id: pendingTaskId,
+ lastError: e,
+ retryInfo: DbRetryInfo.reset(),
+ };
+ } else {
+ retryRecord.lastError = e;
+ retryRecord.retryInfo = DbRetryInfo.increment(retryRecord.retryInfo);
+ }
+ await tx.operationRetries.put(retryRecord);
+ return {
+ notification: await taskToRetryNotification(ws, tx, pendingTaskId, e),
+ retryRecord,
+ };
+ });
+ if (res?.notification) {
+ ws.notify(res.notification);
+ }
+ return res.retryRecord;
+}
+
+/**
+ * Task made progress, clear error.
+ */
+async function storeTaskProgress(
+ ws: InternalWalletState,
+ pendingTaskId: string,
+): Promise<void> {
+ await ws.db.runReadWriteTx(
+ { storeNames: ["operationRetries"] },
+ async (tx) => {
+ await tx.operationRetries.delete(pendingTaskId);
+ },
+ );
+}
+
+async function storePendingTaskPending(
+ ws: InternalWalletState,
+ pendingTaskId: string,
+): Promise<OperationRetryRecord> {
+ const res = await ws.db.runAllStoresReadWriteTx({}, async (tx) => {
+ let retryRecord = await tx.operationRetries.get(pendingTaskId);
+ let hadError = false;
+ if (!retryRecord) {
+ retryRecord = {
+ id: pendingTaskId,
+ retryInfo: DbRetryInfo.reset(),
+ };
+ } else {
+ if (retryRecord.lastError) {
+ hadError = true;
+ }
+ delete retryRecord.lastError;
+ retryRecord.retryInfo = DbRetryInfo.increment(retryRecord.retryInfo);
+ }
+ await tx.operationRetries.put(retryRecord);
+ let notification: WalletNotification | undefined = undefined;
+ if (hadError) {
+ notification = await taskToRetryNotification(
+ ws,
+ tx,
+ pendingTaskId,
+ undefined,
+ );
+ }
+ return {
+ notification,
+ retryRecord,
+ };
+ });
+ if (res.notification) {
+ ws.notify(res.notification);
+ }
+ return res.retryRecord;
+}
+
+async function storePendingTaskFinished(
+ ws: InternalWalletState,
+ pendingTaskId: string,
+): Promise<void> {
+ await ws.db.runReadWriteTx(
+ { storeNames: ["operationRetries"] },
+ async (tx) => {
+ await tx.operationRetries.delete(pendingTaskId);
+ },
+ );
+}
+
+function getWalletExecutionContextForTask(
+ ws: InternalWalletState,
+ taskId: TaskIdStr,
+ cancellationToken: CancellationToken,
+): WalletExecutionContext {
+ let oc: ObservabilityContext;
+ let wex: WalletExecutionContext;
+
+ if (ws.config.testing.emitObservabilityEvents) {
+ oc = {
+ observe(evt) {
+ if (ws.config.testing.emitObservabilityEvents) {
+ ws.notify({
+ type: NotificationType.TaskObservabilityEvent,
+ taskId,
+ event: evt,
+ });
+ }
+ },
+ };
+
+ wex = getObservedWalletExecutionContext(ws, cancellationToken, oc);
+ } else {
+ oc = {
+ observe(evt) {},
+ };
+ wex = getNormalWalletExecutionContext(ws, cancellationToken, oc);
+ }
+ return wex;
+}
+
+async function callOperationHandlerForTaskId(
+ wex: WalletExecutionContext,
+ taskId: TaskIdStr,
+): Promise<TaskRunResult> {
+ const pending = parseTaskIdentifier(taskId);
+ switch (pending.tag) {
+ case PendingTaskType.ExchangeUpdate:
+ return await updateExchangeFromUrlHandler(wex, pending.exchangeBaseUrl);
+ case PendingTaskType.Refresh:
+ return await processRefreshGroup(wex, pending.refreshGroupId);
+ case PendingTaskType.Withdraw:
+ return await processWithdrawalGroup(wex, pending.withdrawalGroupId);
+ case PendingTaskType.Purchase:
+ return await processPurchase(wex, pending.proposalId);
+ case PendingTaskType.Recoup:
+ return await processRecoupGroup(wex, pending.recoupGroupId);
+ case PendingTaskType.Deposit:
+ return await processDepositGroup(wex, pending.depositGroupId);
+ case PendingTaskType.Backup:
+ return await processBackupForProvider(wex, pending.backupProviderBaseUrl);
+ case PendingTaskType.PeerPushDebit:
+ return await processPeerPushDebit(wex, pending.pursePub);
+ case PendingTaskType.PeerPullCredit:
+ return await processPeerPullCredit(wex, pending.pursePub);
+ case PendingTaskType.PeerPullDebit:
+ return await processPeerPullDebit(wex, pending.peerPullDebitId);
+ case PendingTaskType.PeerPushCredit:
+ return await processPeerPushCredit(wex, pending.peerPushCreditId);
+ case PendingTaskType.RewardPickup:
+ throw Error("not supported anymore");
+ default:
+ return assertUnreachable(pending);
+ }
+ throw Error(`not reached ${pending.tag}`);
+}
+
+/**
+ * Generate an appropriate error transition notification
+ * for applicable tasks.
+ *
+ * Namely, transition notifications are generated for:
+ * - exchange update errors
+ * - transactions
+ */
+async function taskToRetryNotification(
+ ws: InternalWalletState,
+ tx: WalletDbAllStoresReadOnlyTransaction,
+ pendingTaskId: string,
+ e: TalerErrorDetail | undefined,
+): Promise<WalletNotification | undefined> {
+ const parsedTaskId = parseTaskIdentifier(pendingTaskId);
+
+ switch (parsedTaskId.tag) {
+ case PendingTaskType.ExchangeUpdate:
+ return makeExchangeRetryNotification(ws, tx, pendingTaskId, e);
+ case PendingTaskType.PeerPullCredit:
+ case PendingTaskType.PeerPullDebit:
+ case PendingTaskType.Withdraw:
+ case PendingTaskType.PeerPushCredit:
+ case PendingTaskType.Deposit:
+ case PendingTaskType.Refresh:
+ case PendingTaskType.RewardPickup:
+ case PendingTaskType.PeerPushDebit:
+ case PendingTaskType.Purchase:
+ return makeTransactionRetryNotification(ws, tx, pendingTaskId, e);
+ case PendingTaskType.Backup:
+ case PendingTaskType.Recoup:
+ return undefined;
+ }
+}
+
+async function getTransactionState(
+ ws: InternalWalletState,
+ tx: WalletDbReadOnlyTransaction<
+ [
+ "depositGroups",
+ "withdrawalGroups",
+ "purchases",
+ "refundGroups",
+ "peerPullCredit",
+ "peerPullDebit",
+ "peerPushDebit",
+ "peerPushCredit",
+ "rewards",
+ "refreshGroups",
+ "denomLossEvents",
+ ]
+ >,
+ transactionId: string,
+): Promise<TransactionState | undefined> {
+ const parsedTxId = parseTransactionIdentifier(transactionId);
+ if (!parsedTxId) {
+ throw Error("invalid tx identifier");
+ }
+ switch (parsedTxId.tag) {
+ case TransactionType.Deposit: {
+ const rec = await tx.depositGroups.get(parsedTxId.depositGroupId);
+ if (!rec) {
+ return undefined;
+ }
+ return computeDepositTransactionStatus(rec);
+ }
+ case TransactionType.InternalWithdrawal:
+ case TransactionType.Withdrawal: {
+ const rec = await tx.withdrawalGroups.get(parsedTxId.withdrawalGroupId);
+ if (!rec) {
+ return undefined;
+ }
+ return computeWithdrawalTransactionStatus(rec);
+ }
+ case TransactionType.Payment: {
+ const rec = await tx.purchases.get(parsedTxId.proposalId);
+ if (!rec) {
+ return;
+ }
+ return computePayMerchantTransactionState(rec);
+ }
+ case TransactionType.Refund: {
+ const rec = await tx.refundGroups.get(parsedTxId.refundGroupId);
+ if (!rec) {
+ return undefined;
+ }
+ return computeRefundTransactionState(rec);
+ }
+ case TransactionType.PeerPullCredit: {
+ const rec = await tx.peerPullCredit.get(parsedTxId.pursePub);
+ if (!rec) {
+ return undefined;
+ }
+ return computePeerPullCreditTransactionState(rec);
+ }
+ case TransactionType.PeerPullDebit: {
+ const rec = await tx.peerPullDebit.get(parsedTxId.peerPullDebitId);
+ if (!rec) {
+ return undefined;
+ }
+ return computePeerPullDebitTransactionState(rec);
+ }
+ case TransactionType.PeerPushCredit: {
+ const rec = await tx.peerPushCredit.get(parsedTxId.peerPushCreditId);
+ if (!rec) {
+ return undefined;
+ }
+ return computePeerPushCreditTransactionState(rec);
+ }
+ case TransactionType.PeerPushDebit: {
+ const rec = await tx.peerPushDebit.get(parsedTxId.pursePub);
+ if (!rec) {
+ return undefined;
+ }
+ return computePeerPushDebitTransactionState(rec);
+ }
+ case TransactionType.Refresh: {
+ const rec = await tx.refreshGroups.get(parsedTxId.refreshGroupId);
+ if (!rec) {
+ return undefined;
+ }
+ return computeRefreshTransactionState(rec);
+ }
+ case TransactionType.Recoup:
+ throw Error("not yet supported");
+ case TransactionType.DenomLoss: {
+ const rec = await tx.denomLossEvents.get(parsedTxId.denomLossEventId);
+ if (!rec) {
+ return undefined;
+ }
+ return computeDenomLossTransactionStatus(rec);
+ }
+ default:
+ assertUnreachable(parsedTxId);
+ }
+}
+
+async function makeTransactionRetryNotification(
+ ws: InternalWalletState,
+ tx: WalletDbAllStoresReadOnlyTransaction,
+ pendingTaskId: string,
+ e: TalerErrorDetail | undefined,
+): Promise<WalletNotification | undefined> {
+ const txId = convertTaskToTransactionId(pendingTaskId);
+ if (!txId) {
+ return undefined;
+ }
+ const txState = await getTransactionState(ws, tx, txId);
+ if (!txState) {
+ return undefined;
+ }
+ const notif: WalletNotification = {
+ type: NotificationType.TransactionStateTransition,
+ transactionId: txId,
+ oldTxState: txState,
+ newTxState: txState,
+ };
+ if (e) {
+ notif.errorInfo = {
+ code: e.code as number,
+ hint: e.hint,
+ };
+ }
+ return notif;
+}
+
+async function makeExchangeRetryNotification(
+ ws: InternalWalletState,
+ tx: WalletDbAllStoresReadOnlyTransaction,
+ pendingTaskId: string,
+ e: TalerErrorDetail | undefined,
+): Promise<WalletNotification | undefined> {
+ logger.info("making exchange retry notification");
+ const parsedTaskId = parseTaskIdentifier(pendingTaskId);
+ if (parsedTaskId.tag !== PendingTaskType.ExchangeUpdate) {
+ throw Error("invalid task identifier");
+ }
+ const rec = await tx.exchanges.get(parsedTaskId.exchangeBaseUrl);
+
+ if (!rec) {
+ logger.info(`exchange ${parsedTaskId.exchangeBaseUrl} not found`);
+ return undefined;
+ }
+
+ const notif: WalletNotification = {
+ type: NotificationType.ExchangeStateTransition,
+ exchangeBaseUrl: parsedTaskId.exchangeBaseUrl,
+ oldExchangeState: getExchangeState(rec),
+ newExchangeState: getExchangeState(rec),
+ };
+ if (e) {
+ notif.errorInfo = {
+ code: e.code as number,
+ hint: e.hint,
+ };
+ }
+ 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.
+ */
+export function convertTaskToTransactionId(
+ taskId: string,
+): TransactionIdStr | undefined {
+ const parsedTaskId = parseTaskIdentifier(taskId);
+ switch (parsedTaskId.tag) {
+ case PendingTaskType.PeerPullCredit:
+ return constructTransactionIdentifier({
+ tag: TransactionType.PeerPullCredit,
+ pursePub: parsedTaskId.pursePub,
+ });
+ case PendingTaskType.PeerPullDebit:
+ return constructTransactionIdentifier({
+ tag: TransactionType.PeerPullDebit,
+ peerPullDebitId: parsedTaskId.peerPullDebitId,
+ });
+ // FIXME: This doesn't distinguish internal-withdrawal.
+ // Maybe we should have a different task type for that as well?
+ // Or maybe transaction IDs should be valid task identifiers?
+ case PendingTaskType.Withdraw:
+ return constructTransactionIdentifier({
+ tag: TransactionType.Withdrawal,
+ withdrawalGroupId: parsedTaskId.withdrawalGroupId,
+ });
+ case PendingTaskType.PeerPushCredit:
+ return constructTransactionIdentifier({
+ tag: TransactionType.PeerPushCredit,
+ peerPushCreditId: parsedTaskId.peerPushCreditId,
+ });
+ case PendingTaskType.Deposit:
+ return constructTransactionIdentifier({
+ tag: TransactionType.Deposit,
+ depositGroupId: parsedTaskId.depositGroupId,
+ });
+ case PendingTaskType.Refresh:
+ return constructTransactionIdentifier({
+ tag: TransactionType.Refresh,
+ refreshGroupId: parsedTaskId.refreshGroupId,
+ });
+ case PendingTaskType.PeerPushDebit:
+ return constructTransactionIdentifier({
+ tag: TransactionType.PeerPushDebit,
+ pursePub: parsedTaskId.pursePub,
+ });
+ case PendingTaskType.Purchase:
+ return constructTransactionIdentifier({
+ tag: TransactionType.Payment,
+ proposalId: parsedTaskId.proposalId,
+ });
+ default:
+ return undefined;
+ }
+}
+
+export interface ActiveTaskIdsResult {
+ taskIds: TaskIdStr[];
+}
+
+export async function getActiveTaskIds(
+ ws: InternalWalletState,
+): Promise<ActiveTaskIdsResult> {
+ const res: ActiveTaskIdsResult = {
+ taskIds: [],
+ };
+ await ws.db.runReadWriteTx(
+ {
+ storeNames: [
+ "exchanges",
+ "refreshGroups",
+ "withdrawalGroups",
+ "purchases",
+ "depositGroups",
+ "recoupGroups",
+ "peerPullCredit",
+ "peerPushDebit",
+ "peerPullDebit",
+ "peerPushCredit",
+ ],
+ },
+ async (tx) => {
+ const active = GlobalIDB.KeyRange.bound(
+ OPERATION_STATUS_ACTIVE_FIRST,
+ OPERATION_STATUS_ACTIVE_LAST,
+ );
+
+ // Withdrawals
+
+ {
+ const activeRecs =
+ await tx.withdrawalGroups.indexes.byStatus.getAll(active);
+ for (const rec of activeRecs) {
+ const taskId = constructTaskIdentifier({
+ tag: PendingTaskType.Withdraw,
+ withdrawalGroupId: rec.withdrawalGroupId,
+ });
+ res.taskIds.push(taskId);
+ }
+ }
+
+ // Deposits
+
+ {
+ const activeRecs =
+ await tx.depositGroups.indexes.byStatus.getAll(active);
+ for (const rec of activeRecs) {
+ const taskId = constructTaskIdentifier({
+ tag: PendingTaskType.Deposit,
+ depositGroupId: rec.depositGroupId,
+ });
+ res.taskIds.push(taskId);
+ }
+ }
+
+ // Refreshes
+
+ {
+ const activeRecs =
+ await tx.refreshGroups.indexes.byStatus.getAll(active);
+ for (const rec of activeRecs) {
+ const taskId = constructTaskIdentifier({
+ tag: PendingTaskType.Refresh,
+ refreshGroupId: rec.refreshGroupId,
+ });
+ res.taskIds.push(taskId);
+ }
+ }
+
+ // Purchases
+
+ {
+ const activeRecs = await tx.purchases.indexes.byStatus.getAll(active);
+ for (const rec of activeRecs) {
+ const taskId = constructTaskIdentifier({
+ tag: PendingTaskType.Purchase,
+ proposalId: rec.proposalId,
+ });
+ res.taskIds.push(taskId);
+ }
+ }
+
+ // peer-push-debit
+
+ {
+ const activeRecs =
+ await tx.peerPushDebit.indexes.byStatus.getAll(active);
+ for (const rec of activeRecs) {
+ const taskId = constructTaskIdentifier({
+ tag: PendingTaskType.PeerPushDebit,
+ pursePub: rec.pursePub,
+ });
+ res.taskIds.push(taskId);
+ }
+ }
+
+ // peer-push-credit
+
+ {
+ const activeRecs =
+ await tx.peerPushCredit.indexes.byStatus.getAll(active);
+ for (const rec of activeRecs) {
+ const taskId = constructTaskIdentifier({
+ tag: PendingTaskType.PeerPushCredit,
+ peerPushCreditId: rec.peerPushCreditId,
+ });
+ res.taskIds.push(taskId);
+ }
+ }
+
+ // peer-pull-debit
+
+ {
+ const activeRecs =
+ await tx.peerPullDebit.indexes.byStatus.getAll(active);
+ for (const rec of activeRecs) {
+ const taskId = constructTaskIdentifier({
+ tag: PendingTaskType.PeerPullDebit,
+ peerPullDebitId: rec.peerPullDebitId,
+ });
+ res.taskIds.push(taskId);
+ }
+ }
+
+ // peer-pull-credit
+
+ {
+ const activeRecs =
+ await tx.peerPullCredit.indexes.byStatus.getAll(active);
+ for (const rec of activeRecs) {
+ const taskId = constructTaskIdentifier({
+ tag: PendingTaskType.PeerPullCredit,
+ pursePub: rec.pursePub,
+ });
+ res.taskIds.push(taskId);
+ }
+ }
+
+ // recoup
+
+ {
+ const activeRecs =
+ await tx.recoupGroups.indexes.byStatus.getAll(active);
+ for (const rec of activeRecs) {
+ const taskId = constructTaskIdentifier({
+ tag: PendingTaskType.Recoup,
+ recoupGroupId: rec.recoupGroupId,
+ });
+ res.taskIds.push(taskId);
+ }
+ }
+
+ // exchange update
+
+ {
+ const exchanges = await tx.exchanges.getAll();
+ for (const rec of exchanges) {
+ const taskIdUpdate = constructTaskIdentifier({
+ tag: PendingTaskType.ExchangeUpdate,
+ exchangeBaseUrl: rec.baseUrl,
+ });
+ res.taskIds.push(taskIdUpdate);
+ }
+ }
+
+ // FIXME: Recoup!
+ },
+ );
+
+ return res;
+}
diff --git a/packages/taler-wallet-core/src/testing.ts b/packages/taler-wallet-core/src/testing.ts
new file mode 100644
index 000000000..899c4a8b2
--- /dev/null
+++ b/packages/taler-wallet-core/src/testing.ts
@@ -0,0 +1,871 @@
+/*
+ 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/>
+ */
+
+/**
+ * @file
+ * Implementation of wallet-core operations that are used for testing,
+ * but typically not in the production wallet.
+ */
+
+/**
+ * Imports.
+ */
+import {
+ AbsoluteTime,
+ addPaytoQueryParams,
+ Amounts,
+ AmountString,
+ checkLogicInvariant,
+ CheckPaymentResponse,
+ codecForAny,
+ codecForCheckPaymentResponse,
+ ConfirmPayResultType,
+ Duration,
+ IntegrationTestArgs,
+ IntegrationTestV2Args,
+ j2s,
+ Logger,
+ NotificationType,
+ parsePaytoUri,
+ PreparePayResultType,
+ TalerCorebankApiClient,
+ TestPayArgs,
+ TestPayResult,
+ TransactionMajorState,
+ TransactionMinorState,
+ TransactionState,
+ TransactionType,
+ URL,
+ WithdrawTestBalanceRequest,
+} from "@gnu-taler/taler-util";
+import {
+ HttpRequestLibrary,
+ readSuccessResponseJsonOrThrow,
+} from "@gnu-taler/taler-util/http";
+import { getBalances } from "./balance.js";
+import { genericWaitForState } from "./common.js";
+import { createDepositGroup } from "./deposits.js";
+import { fetchFreshExchange } from "./exchanges.js";
+import {
+ confirmPay,
+ preparePayForUri,
+ startRefundQueryForUri,
+} from "./pay-merchant.js";
+import { initiatePeerPullPayment } from "./pay-peer-pull-credit.js";
+import {
+ confirmPeerPullDebit,
+ preparePeerPullDebit,
+} from "./pay-peer-pull-debit.js";
+import {
+ confirmPeerPushCredit,
+ preparePeerPushCredit,
+} from "./pay-peer-push-credit.js";
+import { initiatePeerPushDebit } from "./pay-peer-push-debit.js";
+import { getRefreshesForTransaction } from "./refresh.js";
+import { getTransactionById, getTransactions } from "./transactions.js";
+import type { WalletExecutionContext } from "./wallet.js";
+import { acceptWithdrawalFromUri } from "./withdraw.js";
+
+const logger = new Logger("operations/testing.ts");
+
+interface MerchantBackendInfo {
+ baseUrl: string;
+ authToken?: string;
+}
+
+export interface WithdrawTestBalanceResult {
+ /**
+ * Transaction ID of the newly created withdrawal transaction.
+ */
+ transactionId: string;
+
+ /**
+ * Account of the user registered for the withdrawal.
+ */
+ accountPaytoUri: string;
+}
+
+export async function withdrawTestBalance(
+ wex: WalletExecutionContext,
+ req: WithdrawTestBalanceRequest,
+): Promise<WithdrawTestBalanceResult> {
+ const amount = req.amount;
+ const exchangeBaseUrl = req.exchangeBaseUrl;
+ const corebankApiBaseUrl = req.corebankApiBaseUrl;
+
+ logger.trace(
+ `Registering bank user, bank access base url ${corebankApiBaseUrl}`,
+ );
+
+ const corebankClient = new TalerCorebankApiClient(corebankApiBaseUrl);
+
+ const bankUser = await corebankClient.createRandomBankUser();
+ logger.trace(`Registered bank user ${JSON.stringify(bankUser)}`);
+
+ corebankClient.setAuth(bankUser);
+
+ const wresp = await corebankClient.createWithdrawalOperation(
+ bankUser.username,
+ amount,
+ );
+
+ const acceptResp = await acceptWithdrawalFromUri(wex, {
+ talerWithdrawUri: wresp.taler_withdraw_uri,
+ selectedExchange: exchangeBaseUrl,
+ forcedDenomSel: req.forcedDenomSel,
+ });
+
+ await corebankClient.confirmWithdrawalOperation(bankUser.username, {
+ withdrawalOperationId: wresp.withdrawal_id,
+ });
+
+ return {
+ transactionId: acceptResp.transactionId,
+ accountPaytoUri: bankUser.accountPaytoUri,
+ };
+}
+
+/**
+ * FIXME: User MerchantApiClient instead.
+ */
+function getMerchantAuthHeader(m: MerchantBackendInfo): Record<string, string> {
+ if (m.authToken) {
+ return {
+ Authorization: `Bearer ${m.authToken}`,
+ };
+ }
+ return {};
+}
+
+/**
+ * FIXME: User MerchantApiClient instead.
+ */
+async function refund(
+ http: HttpRequestLibrary,
+ merchantBackend: MerchantBackendInfo,
+ orderId: string,
+ reason: string,
+ refundAmount: string,
+): Promise<string> {
+ const reqUrl = new URL(
+ `private/orders/${orderId}/refund`,
+ merchantBackend.baseUrl,
+ );
+ const refundReq = {
+ order_id: orderId,
+ reason,
+ refund: refundAmount,
+ };
+ const resp = await http.fetch(reqUrl.href, {
+ method: "POST",
+ body: refundReq,
+ headers: getMerchantAuthHeader(merchantBackend),
+ });
+ const r = await readSuccessResponseJsonOrThrow(resp, codecForAny());
+ const refundUri = r.taler_refund_uri;
+ if (!refundUri) {
+ throw Error("no refund URI in response");
+ }
+ return refundUri;
+}
+
+/**
+ * FIXME: User MerchantApiClient instead.
+ */
+async function createOrder(
+ http: HttpRequestLibrary,
+ merchantBackend: MerchantBackendInfo,
+ amount: string,
+ summary: string,
+ fulfillmentUrl: string,
+): Promise<{ orderId: string }> {
+ const t = Math.floor(new Date().getTime() / 1000) + 15 * 60;
+ const reqUrl = new URL("private/orders", merchantBackend.baseUrl).href;
+ const orderReq = {
+ order: {
+ amount,
+ summary,
+ fulfillment_url: fulfillmentUrl,
+ refund_deadline: { t_s: t },
+ wire_transfer_deadline: { t_s: t },
+ },
+ };
+ const resp = await http.fetch(reqUrl, {
+ method: "POST",
+ body: orderReq,
+ headers: getMerchantAuthHeader(merchantBackend),
+ });
+ const r = await readSuccessResponseJsonOrThrow(resp, codecForAny());
+ const orderId = r.order_id;
+ if (!orderId) {
+ throw Error("no order id in response");
+ }
+ return { orderId };
+}
+
+/**
+ * FIXME: User MerchantApiClient instead.
+ */
+async function checkPayment(
+ http: HttpRequestLibrary,
+ merchantBackend: MerchantBackendInfo,
+ orderId: string,
+): Promise<CheckPaymentResponse> {
+ const reqUrl = new URL(`private/orders/${orderId}`, merchantBackend.baseUrl);
+ reqUrl.searchParams.set("order_id", orderId);
+ const resp = await http.fetch(reqUrl.href, {
+ headers: getMerchantAuthHeader(merchantBackend),
+ });
+ return readSuccessResponseJsonOrThrow(resp, codecForCheckPaymentResponse());
+}
+
+interface MakePaymentResult {
+ orderId: string;
+ paymentTransactionId: string;
+}
+
+async function makePayment(
+ wex: WalletExecutionContext,
+ merchant: MerchantBackendInfo,
+ amount: string,
+ summary: string,
+): Promise<MakePaymentResult> {
+ const orderResp = await createOrder(
+ wex.http,
+ merchant,
+ amount,
+ summary,
+ "taler://fulfillment-success/thx",
+ );
+
+ logger.trace("created order with orderId", orderResp.orderId);
+
+ let paymentStatus = await checkPayment(wex.http, merchant, orderResp.orderId);
+
+ logger.trace("payment status", paymentStatus);
+
+ const talerPayUri = paymentStatus.taler_pay_uri;
+ if (!talerPayUri) {
+ throw Error("no taler://pay/ URI in payment response");
+ }
+
+ const preparePayResult = await preparePayForUri(wex, talerPayUri);
+
+ logger.trace("prepare pay result", preparePayResult);
+
+ if (preparePayResult.status != "payment-possible") {
+ throw Error("payment not possible");
+ }
+
+ const confirmPayResult = await confirmPay(
+ wex,
+ preparePayResult.transactionId,
+ undefined,
+ );
+
+ logger.trace("confirmPayResult", confirmPayResult);
+
+ paymentStatus = await checkPayment(wex.http, merchant, orderResp.orderId);
+
+ logger.trace("payment status after wallet payment:", paymentStatus);
+
+ if (paymentStatus.order_status !== "paid") {
+ throw Error("payment did not succeed");
+ }
+
+ return {
+ orderId: orderResp.orderId,
+ paymentTransactionId: preparePayResult.transactionId,
+ };
+}
+
+export async function runIntegrationTest(
+ wex: WalletExecutionContext,
+ args: IntegrationTestArgs,
+): Promise<void> {
+ logger.info("running test with arguments", args);
+
+ const parsedSpendAmount = Amounts.parseOrThrow(args.amountToSpend);
+ const currency = parsedSpendAmount.currency;
+
+ logger.info("withdrawing test balance");
+ const withdrawRes1 = await withdrawTestBalance(wex, {
+ amount: args.amountToWithdraw,
+ corebankApiBaseUrl: args.corebankApiBaseUrl,
+ exchangeBaseUrl: args.exchangeBaseUrl,
+ });
+ await waitUntilGivenTransactionsFinal(wex, [withdrawRes1.transactionId]);
+ logger.info("done withdrawing test balance");
+
+ const balance = await getBalances(wex);
+
+ logger.trace(JSON.stringify(balance, null, 2));
+
+ const myMerchant: MerchantBackendInfo = {
+ baseUrl: args.merchantBaseUrl,
+ authToken: args.merchantAuthToken,
+ };
+
+ const makePaymentRes = await makePayment(
+ wex,
+ myMerchant,
+ args.amountToSpend,
+ "hello world",
+ );
+
+ await waitUntilTransactionWithAssociatedRefreshesFinal(
+ wex,
+ makePaymentRes.paymentTransactionId,
+ );
+
+ logger.trace("withdrawing test balance for refund");
+ const withdrawAmountTwo = Amounts.parseOrThrow(`${currency}:18`);
+ const spendAmountTwo = Amounts.parseOrThrow(`${currency}:7`);
+ const refundAmount = Amounts.parseOrThrow(`${currency}:6`);
+ const spendAmountThree = Amounts.parseOrThrow(`${currency}:3`);
+
+ const withdrawRes2 = await withdrawTestBalance(wex, {
+ amount: Amounts.stringify(withdrawAmountTwo),
+ corebankApiBaseUrl: args.corebankApiBaseUrl,
+ exchangeBaseUrl: args.exchangeBaseUrl,
+ });
+
+ await waitUntilGivenTransactionsFinal(wex, [withdrawRes2.transactionId]);
+
+ const { orderId: refundOrderId } = await makePayment(
+ wex,
+ myMerchant,
+ Amounts.stringify(spendAmountTwo),
+ "order that will be refunded",
+ );
+
+ const refundUri = await refund(
+ wex.http,
+ myMerchant,
+ refundOrderId,
+ "test refund",
+ Amounts.stringify(refundAmount),
+ );
+
+ logger.trace("refund URI", refundUri);
+
+ const refundResp = await startRefundQueryForUri(wex, refundUri);
+
+ logger.trace("integration test: applied refund");
+
+ // Wait until the refund is done
+ await waitUntilTransactionWithAssociatedRefreshesFinal(
+ wex,
+ refundResp.transactionId,
+ );
+
+ logger.trace("integration test: making payment after refund");
+
+ const paymentResp2 = await makePayment(
+ wex,
+ myMerchant,
+ Amounts.stringify(spendAmountThree),
+ "payment after refund",
+ );
+
+ logger.trace("integration test: make payment done");
+
+ await waitUntilGivenTransactionsFinal(wex, [
+ paymentResp2.paymentTransactionId,
+ ]);
+ await waitUntilGivenTransactionsFinal(
+ wex,
+ await getRefreshesForTransaction(wex, paymentResp2.paymentTransactionId),
+ );
+
+ logger.trace("integration test: all done!");
+}
+
+/**
+ * Wait until all transactions are in a final state.
+ */
+export async function waitUntilAllTransactionsFinal(
+ wex: WalletExecutionContext,
+): Promise<void> {
+ logger.info("waiting until all transactions are in a final state");
+ await wex.taskScheduler.ensureRunning();
+ await genericWaitForState(wex, {
+ filterNotification(notif) {
+ if (notif.type !== NotificationType.TransactionStateTransition) {
+ return false;
+ }
+ switch (notif.newTxState.major) {
+ case TransactionMajorState.Pending:
+ case TransactionMajorState.Aborting:
+ return false;
+ default:
+ return true;
+ }
+ },
+ async checkState() {
+ const txs = await getTransactions(wex, {
+ includeRefreshes: true,
+ filterByState: "nonfinal",
+ });
+ for (const tx of txs.transactions) {
+ switch (tx.txState.major) {
+ case TransactionMajorState.Pending:
+ case TransactionMajorState.Aborting:
+ case TransactionMajorState.Suspended:
+ case TransactionMajorState.SuspendedAborting:
+ logger.info(
+ `continuing waiting, ${tx.transactionId} in ${tx.txState.major}(${tx.txState.minor})`,
+ );
+ return false;
+ }
+ }
+ return true;
+ },
+ });
+ logger.info("done waiting until all transactions are in a final state");
+}
+
+export async function waitTasksDone(
+ wex: WalletExecutionContext,
+): Promise<void> {
+ await genericWaitForState(wex, {
+ async checkState() {
+ return wex.taskScheduler.isIdle();
+ },
+ filterNotification(notif) {
+ return notif.type === NotificationType.Idle;
+ },
+ });
+}
+
+/**
+ * Wait until all chosen transactions are in a final state.
+ */
+export async function waitUntilGivenTransactionsFinal(
+ wex: WalletExecutionContext,
+ transactionIds: string[],
+): Promise<void> {
+ logger.info(
+ `waiting until given ${transactionIds.length} transactions are in a final state`,
+ );
+ logger.info(`transaction IDs are: ${j2s(transactionIds)}`);
+ if (transactionIds.length === 0) {
+ return;
+ }
+
+ const txIdSet = new Set(transactionIds);
+
+ await genericWaitForState(wex, {
+ filterNotification(notif) {
+ if (notif.type !== NotificationType.TransactionStateTransition) {
+ return false;
+ }
+ if (!txIdSet.has(notif.transactionId)) {
+ return false;
+ }
+ switch (notif.newTxState.major) {
+ case TransactionMajorState.Pending:
+ case TransactionMajorState.Aborting:
+ case TransactionMajorState.Suspended:
+ case TransactionMajorState.SuspendedAborting:
+ return false;
+ }
+ return true;
+ },
+ async checkState() {
+ const txs = await getTransactions(wex, {
+ includeRefreshes: true,
+ filterByState: "nonfinal",
+ });
+ for (const tx of txs.transactions) {
+ if (!txIdSet.has(tx.transactionId)) {
+ // Don't look at this transaction, we're not interested in it.
+ continue;
+ }
+ switch (tx.txState.major) {
+ case TransactionMajorState.Pending:
+ case TransactionMajorState.Aborting:
+ case TransactionMajorState.Suspended:
+ case TransactionMajorState.SuspendedAborting:
+ logger.info(
+ `continuing waiting, ${tx.transactionId} in ${tx.txState.major}(${tx.txState.minor})`,
+ );
+ return false;
+ }
+ }
+ // No transaction is pending, we're done waiting!
+ return true;
+ },
+ });
+ logger.info("done waiting until given transactions are in a final state");
+}
+
+export async function waitUntilRefreshesDone(
+ wex: WalletExecutionContext,
+): Promise<void> {
+ logger.info("waiting until all refresh transactions are in a final state");
+
+ await genericWaitForState(wex, {
+ filterNotification(notif) {
+ if (notif.type !== NotificationType.TransactionStateTransition) {
+ return false;
+ }
+ switch (notif.newTxState.major) {
+ case TransactionMajorState.Pending:
+ case TransactionMajorState.Aborting:
+ return false;
+ default:
+ return true;
+ }
+ },
+ async checkState() {
+ const txs = await getTransactions(wex, {
+ includeRefreshes: true,
+ filterByState: "nonfinal",
+ });
+ for (const tx of txs.transactions) {
+ if (tx.type !== TransactionType.Refresh) {
+ continue;
+ }
+ switch (tx.txState.major) {
+ case TransactionMajorState.Pending:
+ case TransactionMajorState.Aborting:
+ case TransactionMajorState.Suspended:
+ case TransactionMajorState.SuspendedAborting:
+ logger.info(
+ `continuing waiting, ${tx.transactionId} in ${tx.txState.major}(${tx.txState.minor})`,
+ );
+ return false;
+ }
+ }
+ return true;
+ },
+ });
+ logger.info("done waiting until all refreshes are in a final state");
+}
+
+async function waitUntilTransactionPendingReady(
+ wex: WalletExecutionContext,
+ transactionId: string,
+): Promise<void> {
+ return await waitTransactionState(wex, transactionId, {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.Ready,
+ });
+}
+
+/**
+ * Wait until a transaction is in a particular state.
+ */
+export async function waitTransactionState(
+ wex: WalletExecutionContext,
+ transactionId: string,
+ txState: TransactionState,
+): Promise<void> {
+ logger.info(
+ `starting waiting for ${transactionId} to be in ${JSON.stringify(
+ txState,
+ )})`,
+ );
+ await genericWaitForState(wex, {
+ async checkState() {
+ const tx = await getTransactionById(wex, {
+ transactionId,
+ });
+ return (
+ tx.txState.major === txState.major && tx.txState.minor === txState.minor
+ );
+ },
+ filterNotification(notif) {
+ return notif.type === NotificationType.TransactionStateTransition;
+ },
+ });
+ logger.info(
+ `done waiting for ${transactionId} to be in ${JSON.stringify(txState)}`,
+ );
+}
+
+export async function waitUntilTransactionWithAssociatedRefreshesFinal(
+ wex: WalletExecutionContext,
+ transactionId: string,
+): Promise<void> {
+ await waitUntilGivenTransactionsFinal(wex, [transactionId]);
+ await waitUntilGivenTransactionsFinal(
+ wex,
+ await getRefreshesForTransaction(wex, transactionId),
+ );
+}
+
+export async function waitUntilTransactionFinal(
+ wex: WalletExecutionContext,
+ transactionId: string,
+): Promise<void> {
+ await waitUntilGivenTransactionsFinal(wex, [transactionId]);
+}
+
+export async function runIntegrationTest2(
+ wex: WalletExecutionContext,
+ args: IntegrationTestV2Args,
+): Promise<void> {
+ await wex.taskScheduler.ensureRunning();
+ logger.info("running test with arguments", args);
+
+ const exchangeInfo = await fetchFreshExchange(wex, args.exchangeBaseUrl);
+
+ const currency = exchangeInfo.currency;
+
+ const amountToWithdraw = Amounts.parseOrThrow(`${currency}:10`);
+ const amountToSpend = Amounts.parseOrThrow(`${currency}:2`);
+
+ logger.info("withdrawing test balance");
+ const withdrawalRes = await withdrawTestBalance(wex, {
+ amount: Amounts.stringify(amountToWithdraw),
+ corebankApiBaseUrl: args.corebankApiBaseUrl,
+ exchangeBaseUrl: args.exchangeBaseUrl,
+ });
+ await waitUntilTransactionFinal(wex, withdrawalRes.transactionId);
+ logger.info("done withdrawing test balance");
+
+ const balance = await getBalances(wex);
+
+ logger.trace(JSON.stringify(balance, null, 2));
+
+ const myMerchant: MerchantBackendInfo = {
+ baseUrl: args.merchantBaseUrl,
+ authToken: args.merchantAuthToken,
+ };
+
+ const makePaymentRes = await makePayment(
+ wex,
+ myMerchant,
+ Amounts.stringify(amountToSpend),
+ "hello world",
+ );
+
+ await waitUntilTransactionWithAssociatedRefreshesFinal(
+ wex,
+ makePaymentRes.paymentTransactionId,
+ );
+
+ logger.trace("withdrawing test balance for refund");
+ const withdrawAmountTwo = Amounts.parseOrThrow(`${currency}:18`);
+ const spendAmountTwo = Amounts.parseOrThrow(`${currency}:7`);
+ const refundAmount = Amounts.parseOrThrow(`${currency}:6`);
+ const spendAmountThree = Amounts.parseOrThrow(`${currency}:3`);
+
+ const withdrawalRes2 = await withdrawTestBalance(wex, {
+ amount: Amounts.stringify(withdrawAmountTwo),
+ corebankApiBaseUrl: args.corebankApiBaseUrl,
+ exchangeBaseUrl: args.exchangeBaseUrl,
+ });
+
+ // Wait until the withdraw is done
+ await waitUntilTransactionFinal(wex, withdrawalRes2.transactionId);
+
+ const { orderId: refundOrderId } = await makePayment(
+ wex,
+ myMerchant,
+ Amounts.stringify(spendAmountTwo),
+ "order that will be refunded",
+ );
+
+ const refundUri = await refund(
+ wex.http,
+ myMerchant,
+ refundOrderId,
+ "test refund",
+ Amounts.stringify(refundAmount),
+ );
+
+ logger.trace("refund URI", refundUri);
+
+ const refundResp = await startRefundQueryForUri(wex, refundUri);
+
+ logger.trace("integration test: applied refund");
+
+ // Wait until the refund is done
+ await waitUntilTransactionWithAssociatedRefreshesFinal(
+ wex,
+ refundResp.transactionId,
+ );
+
+ logger.trace("integration test: making payment after refund");
+
+ const makePaymentRes2 = await makePayment(
+ wex,
+ myMerchant,
+ Amounts.stringify(spendAmountThree),
+ "payment after refund",
+ );
+
+ await waitUntilTransactionWithAssociatedRefreshesFinal(
+ wex,
+ makePaymentRes2.paymentTransactionId,
+ );
+
+ logger.trace("integration test: make payment done");
+
+ const peerPushInit = await initiatePeerPushDebit(wex, {
+ partialContractTerms: {
+ amount: `${currency}:1` as AmountString,
+ summary: "Payment Peer Push Test",
+ purse_expiration: AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ hours: 1 }),
+ ),
+ ),
+ },
+ });
+
+ await waitUntilTransactionPendingReady(wex, peerPushInit.transactionId);
+ const txDetails = await getTransactionById(wex, {
+ transactionId: peerPushInit.transactionId,
+ });
+
+ if (txDetails.type !== TransactionType.PeerPushDebit) {
+ throw Error("internal invariant failed");
+ }
+
+ if (!txDetails.talerUri) {
+ throw Error("internal invariant failed");
+ }
+
+ const peerPushCredit = await preparePeerPushCredit(wex, {
+ talerUri: txDetails.talerUri,
+ });
+
+ await confirmPeerPushCredit(wex, {
+ transactionId: peerPushCredit.transactionId,
+ });
+
+ const peerPullInit = await initiatePeerPullPayment(wex, {
+ partialContractTerms: {
+ amount: `${currency}:1` as AmountString,
+ summary: "Payment Peer Pull Test",
+ purse_expiration: AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ hours: 1 }),
+ ),
+ ),
+ },
+ });
+
+ await waitUntilTransactionPendingReady(wex, peerPullInit.transactionId);
+
+ const peerPullInc = await preparePeerPullDebit(wex, {
+ talerUri: peerPullInit.talerUri,
+ });
+
+ await confirmPeerPullDebit(wex, {
+ transactionId: peerPullInc.transactionId,
+ });
+
+ await waitUntilTransactionWithAssociatedRefreshesFinal(
+ wex,
+ peerPullInc.transactionId,
+ );
+
+ await waitUntilTransactionWithAssociatedRefreshesFinal(
+ wex,
+ peerPullInit.transactionId,
+ );
+
+ await waitUntilTransactionWithAssociatedRefreshesFinal(
+ wex,
+ peerPushCredit.transactionId,
+ );
+
+ await waitUntilTransactionWithAssociatedRefreshesFinal(
+ wex,
+ peerPushInit.transactionId,
+ );
+
+ let depositPayto = withdrawalRes.accountPaytoUri;
+
+ const parsedPayto = parsePaytoUri(depositPayto);
+ if (!parsedPayto) {
+ throw Error("invalid payto");
+ }
+
+ // Work around libeufin-bank bug where receiver-name is missing
+ if (!parsedPayto.params["receiver-name"]) {
+ depositPayto = addPaytoQueryParams(depositPayto, {
+ "receiver-name": "Test",
+ });
+ }
+
+ await createDepositGroup(wex, {
+ amount: `${currency}:5` as AmountString,
+ depositPaytoUri: depositPayto,
+ });
+
+ logger.trace("integration test: all done!");
+}
+
+export async function testPay(
+ wex: WalletExecutionContext,
+ args: TestPayArgs,
+): Promise<TestPayResult> {
+ logger.trace("creating order");
+ const merchant = {
+ authToken: args.merchantAuthToken,
+ baseUrl: args.merchantBaseUrl,
+ };
+ const orderResp = await createOrder(
+ wex.http,
+ merchant,
+ args.amount,
+ args.summary,
+ "taler://fulfillment-success/thank+you",
+ );
+ logger.trace("created new order with order ID", orderResp.orderId);
+ const checkPayResp = await checkPayment(
+ wex.http,
+ merchant,
+ orderResp.orderId,
+ );
+ const talerPayUri = checkPayResp.taler_pay_uri;
+ if (!talerPayUri) {
+ console.error("fatal: no taler pay URI received from backend");
+ process.exit(1);
+ }
+ logger.trace("taler pay URI:", talerPayUri);
+ const result = await preparePayForUri(wex, talerPayUri);
+ if (result.status !== PreparePayResultType.PaymentPossible) {
+ throw Error(`unexpected prepare pay status: ${result.status}`);
+ }
+ const r = await confirmPay(
+ wex,
+ result.transactionId,
+ undefined,
+ args.forcedCoinSel,
+ );
+ if (r.type != ConfirmPayResultType.Done) {
+ throw Error("payment not done");
+ }
+ const purchase = await wex.db.runReadOnlyTx(
+ { storeNames: ["purchases"] },
+ async (tx) => {
+ return tx.purchases.get(result.proposalId);
+ },
+ );
+ checkLogicInvariant(!!purchase);
+ return {
+ numCoins: purchase.payInfo?.payCoinSelection?.coinContributions.length ?? 0,
+ };
+}
diff --git a/packages/taler-wallet-core/src/transactions.ts b/packages/taler-wallet-core/src/transactions.ts
new file mode 100644
index 000000000..f6216d641
--- /dev/null
+++ b/packages/taler-wallet-core/src/transactions.ts
@@ -0,0 +1,2039 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received 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 { GlobalIDB } from "@gnu-taler/idb-bridge";
+import {
+ AbsoluteTime,
+ Amounts,
+ assertUnreachable,
+ checkDbInvariant,
+ DepositTransactionTrackingState,
+ j2s,
+ Logger,
+ NotificationType,
+ OrderShortInfo,
+ PeerContractTerms,
+ RefundInfoShort,
+ RefundPaymentInfo,
+ ScopeType,
+ stringifyPayPullUri,
+ stringifyPayPushUri,
+ TalerErrorCode,
+ TalerPreciseTimestamp,
+ Transaction,
+ TransactionAction,
+ TransactionByIdRequest,
+ TransactionIdStr,
+ TransactionMajorState,
+ TransactionRecordFilter,
+ TransactionsRequest,
+ TransactionsResponse,
+ TransactionState,
+ TransactionType,
+ TransactionWithdrawal,
+ WalletContractData,
+ WithdrawalTransactionByURIRequest,
+ WithdrawalType,
+} from "@gnu-taler/taler-util";
+import {
+ constructTaskIdentifier,
+ PendingTaskType,
+ TaskIdentifiers,
+ TaskIdStr,
+ TransactionContext,
+} from "./common.js";
+import {
+ DenomLossEventRecord,
+ DepositElementStatus,
+ DepositGroupRecord,
+ OPERATION_STATUS_ACTIVE_FIRST,
+ OPERATION_STATUS_ACTIVE_LAST,
+ OperationRetryRecord,
+ PeerPullCreditRecord,
+ PeerPullDebitRecordStatus,
+ PeerPullPaymentIncomingRecord,
+ PeerPushCreditStatus,
+ PeerPushDebitRecord,
+ PeerPushDebitStatus,
+ PeerPushPaymentIncomingRecord,
+ PurchaseRecord,
+ PurchaseStatus,
+ RefreshGroupRecord,
+ RefreshOperationStatus,
+ RefundGroupRecord,
+ timestampPreciseFromDb,
+ timestampProtocolFromDb,
+ WalletDbReadOnlyTransaction,
+ WithdrawalGroupRecord,
+ WithdrawalGroupStatus,
+ WithdrawalRecordType,
+} from "./db.js";
+import {
+ computeDepositTransactionActions,
+ computeDepositTransactionStatus,
+ DepositTransactionContext,
+} from "./deposits.js";
+import {
+ computeDenomLossTransactionStatus,
+ DenomLossTransactionContext,
+ ExchangeWireDetails,
+ getExchangeWireDetailsInTx,
+} from "./exchanges.js";
+import {
+ computePayMerchantTransactionActions,
+ computePayMerchantTransactionState,
+ computeRefundTransactionState,
+ expectProposalDownload,
+ extractContractData,
+ PayMerchantTransactionContext,
+ RefundTransactionContext,
+} from "./pay-merchant.js";
+import {
+ computePeerPullCreditTransactionActions,
+ computePeerPullCreditTransactionState,
+ PeerPullCreditTransactionContext,
+} from "./pay-peer-pull-credit.js";
+import {
+ computePeerPullDebitTransactionActions,
+ computePeerPullDebitTransactionState,
+ PeerPullDebitTransactionContext,
+} from "./pay-peer-pull-debit.js";
+import {
+ computePeerPushCreditTransactionActions,
+ computePeerPushCreditTransactionState,
+ PeerPushCreditTransactionContext,
+} from "./pay-peer-push-credit.js";
+import {
+ computePeerPushDebitTransactionActions,
+ computePeerPushDebitTransactionState,
+ PeerPushDebitTransactionContext,
+} from "./pay-peer-push-debit.js";
+import {
+ computeRefreshTransactionActions,
+ computeRefreshTransactionState,
+ RefreshTransactionContext,
+} from "./refresh.js";
+import type { WalletExecutionContext } from "./wallet.js";
+import {
+ augmentPaytoUrisForWithdrawal,
+ computeWithdrawalTransactionActions,
+ computeWithdrawalTransactionStatus,
+ WithdrawTransactionContext,
+} from "./withdraw.js";
+
+const logger = new Logger("taler-wallet-core:transactions.ts");
+
+function shouldSkipCurrency(
+ transactionsRequest: TransactionsRequest | undefined,
+ currency: string,
+ exchangesInTransaction: string[],
+): boolean {
+ if (transactionsRequest?.scopeInfo) {
+ const sameCurrency = Amounts.isSameCurrency(
+ currency,
+ transactionsRequest.scopeInfo.currency,
+ );
+ switch (transactionsRequest.scopeInfo.type) {
+ case ScopeType.Global: {
+ return !sameCurrency;
+ }
+ case ScopeType.Exchange: {
+ return (
+ !sameCurrency ||
+ (exchangesInTransaction.length > 0 &&
+ !exchangesInTransaction.includes(transactionsRequest.scopeInfo.url))
+ );
+ }
+ case ScopeType.Auditor: {
+ // same currency and same auditor
+ throw Error("filering balance in auditor scope is not implemented");
+ }
+ default:
+ assertUnreachable(transactionsRequest.scopeInfo);
+ }
+ }
+ // FIXME: remove next release
+ if (transactionsRequest?.currency) {
+ return (
+ transactionsRequest.currency.toLowerCase() !== currency.toLowerCase()
+ );
+ }
+ return false;
+}
+
+function shouldSkipSearch(
+ transactionsRequest: TransactionsRequest | undefined,
+ fields: string[],
+): boolean {
+ if (!transactionsRequest?.search) {
+ return false;
+ }
+ const needle = transactionsRequest.search.trim();
+ for (const f of fields) {
+ if (f.indexOf(needle) >= 0) {
+ return false;
+ }
+ }
+ return true;
+}
+
+/**
+ * Fallback order of transactions that have the same timestamp.
+ */
+const txOrder: { [t in TransactionType]: number } = {
+ [TransactionType.Withdrawal]: 1,
+ [TransactionType.Payment]: 3,
+ [TransactionType.PeerPullCredit]: 4,
+ [TransactionType.PeerPullDebit]: 5,
+ [TransactionType.PeerPushCredit]: 6,
+ [TransactionType.PeerPushDebit]: 7,
+ [TransactionType.Refund]: 8,
+ [TransactionType.Deposit]: 9,
+ [TransactionType.Refresh]: 10,
+ [TransactionType.Recoup]: 11,
+ [TransactionType.InternalWithdrawal]: 12,
+ [TransactionType.DenomLoss]: 13,
+};
+
+export async function getTransactionById(
+ wex: WalletExecutionContext,
+ req: TransactionByIdRequest,
+): Promise<Transaction> {
+ const parsedTx = parseTransactionIdentifier(req.transactionId);
+
+ if (!parsedTx) {
+ throw Error("invalid transaction ID");
+ }
+
+ switch (parsedTx.tag) {
+ case TransactionType.InternalWithdrawal:
+ case TransactionType.Withdrawal: {
+ const withdrawalGroupId = parsedTx.withdrawalGroupId;
+ return await wex.db.runReadWriteTx(
+ {
+ storeNames: [
+ "withdrawalGroups",
+ "exchangeDetails",
+ "exchanges",
+ "operationRetries",
+ ],
+ },
+ async (tx) => {
+ const withdrawalGroupRecord =
+ await tx.withdrawalGroups.get(withdrawalGroupId);
+
+ if (!withdrawalGroupRecord) throw Error("not found");
+
+ const opId = TaskIdentifiers.forWithdrawal(withdrawalGroupRecord);
+ const ort = await tx.operationRetries.get(opId);
+
+ if (
+ withdrawalGroupRecord.wgInfo.withdrawalType ===
+ WithdrawalRecordType.BankIntegrated
+ ) {
+ return buildTransactionForBankIntegratedWithdraw(
+ withdrawalGroupRecord,
+ ort,
+ );
+ }
+ const exchangeDetails = await getExchangeWireDetailsInTx(
+ tx,
+ withdrawalGroupRecord.exchangeBaseUrl,
+ );
+ if (!exchangeDetails) throw Error("not exchange details");
+
+ return buildTransactionForManualWithdraw(
+ withdrawalGroupRecord,
+ exchangeDetails,
+ ort,
+ );
+ },
+ );
+ }
+
+ case TransactionType.DenomLoss: {
+ const rec = await wex.db.runReadOnlyTx(
+ { storeNames: ["denomLossEvents"] },
+ async (tx) => {
+ return tx.denomLossEvents.get(parsedTx.denomLossEventId);
+ },
+ );
+ if (!rec) {
+ throw Error("denom loss record not found");
+ }
+ return buildTransactionForDenomLoss(rec);
+ }
+
+ case TransactionType.Recoup:
+ throw new Error("not yet supported");
+
+ case TransactionType.Payment: {
+ const proposalId = parsedTx.proposalId;
+ return await wex.db.runReadWriteTx(
+ {
+ storeNames: [
+ "purchases",
+ "tombstones",
+ "operationRetries",
+ "contractTerms",
+ "refundGroups",
+ ],
+ },
+ async (tx) => {
+ const purchase = await tx.purchases.get(proposalId);
+ if (!purchase) throw Error("not found");
+ const download = await expectProposalDownload(wex, purchase, tx);
+ const contractData = download.contractData;
+ const payOpId = TaskIdentifiers.forPay(purchase);
+ const payRetryRecord = await tx.operationRetries.get(payOpId);
+
+ const refunds = await tx.refundGroups.indexes.byProposalId.getAll(
+ purchase.proposalId,
+ );
+
+ return buildTransactionForPurchase(
+ purchase,
+ contractData,
+ refunds,
+ payRetryRecord,
+ );
+ },
+ );
+ }
+
+ case TransactionType.Refresh: {
+ // FIXME: We should return info about the refresh here!;
+ const refreshGroupId = parsedTx.refreshGroupId;
+ return await wex.db.runReadOnlyTx(
+ { storeNames: ["refreshGroups", "operationRetries"] },
+ async (tx) => {
+ const refreshGroupRec = await tx.refreshGroups.get(refreshGroupId);
+ if (!refreshGroupRec) {
+ throw Error("not found");
+ }
+ const retries = await tx.operationRetries.get(
+ TaskIdentifiers.forRefresh(refreshGroupRec),
+ );
+ return buildTransactionForRefresh(refreshGroupRec, retries);
+ },
+ );
+ }
+
+ case TransactionType.Deposit: {
+ const depositGroupId = parsedTx.depositGroupId;
+ return await wex.db.runReadWriteTx(
+ { storeNames: ["depositGroups", "operationRetries"] },
+ async (tx) => {
+ const depositRecord = await tx.depositGroups.get(depositGroupId);
+ if (!depositRecord) throw Error("not found");
+
+ const retries = await tx.operationRetries.get(
+ TaskIdentifiers.forDeposit(depositRecord),
+ );
+ return buildTransactionForDeposit(depositRecord, retries);
+ },
+ );
+ }
+
+ case TransactionType.Refund: {
+ return await wex.db.runReadOnlyTx(
+ {
+ storeNames: [
+ "refundGroups",
+ "purchases",
+ "operationRetries",
+ "contractTerms",
+ ],
+ },
+ async (tx) => {
+ const refundRecord = await tx.refundGroups.get(
+ parsedTx.refundGroupId,
+ );
+ if (!refundRecord) {
+ throw Error("not found");
+ }
+ const contractData = await lookupMaybeContractData(
+ tx,
+ refundRecord?.proposalId,
+ );
+ return buildTransactionForRefund(refundRecord, contractData);
+ },
+ );
+ }
+ case TransactionType.PeerPullDebit: {
+ return await wex.db.runReadWriteTx(
+ { storeNames: ["peerPullDebit", "contractTerms"] },
+ async (tx) => {
+ const debit = await tx.peerPullDebit.get(parsedTx.peerPullDebitId);
+ if (!debit) throw Error("not found");
+ const contractTermsRec = await tx.contractTerms.get(
+ debit.contractTermsHash,
+ );
+ if (!contractTermsRec)
+ throw Error("contract terms for peer-pull-debit not found");
+ return buildTransactionForPullPaymentDebit(
+ debit,
+ contractTermsRec.contractTermsRaw,
+ );
+ },
+ );
+ }
+
+ case TransactionType.PeerPushDebit: {
+ return await wex.db.runReadWriteTx(
+ { storeNames: ["peerPushDebit", "contractTerms"] },
+ async (tx) => {
+ const debit = await tx.peerPushDebit.get(parsedTx.pursePub);
+ if (!debit) throw Error("not found");
+ const ct = await tx.contractTerms.get(debit.contractTermsHash);
+ checkDbInvariant(!!ct);
+ return buildTransactionForPushPaymentDebit(
+ debit,
+ ct.contractTermsRaw,
+ );
+ },
+ );
+ }
+
+ case TransactionType.PeerPushCredit: {
+ const peerPushCreditId = parsedTx.peerPushCreditId;
+ return await wex.db.runReadWriteTx(
+ {
+ storeNames: [
+ "peerPushCredit",
+ "contractTerms",
+ "withdrawalGroups",
+ "operationRetries",
+ ],
+ },
+ async (tx) => {
+ const pushInc = await tx.peerPushCredit.get(peerPushCreditId);
+ if (!pushInc) throw Error("not found");
+ const ct = await tx.contractTerms.get(pushInc.contractTermsHash);
+ checkDbInvariant(!!ct);
+
+ let wg: WithdrawalGroupRecord | undefined = undefined;
+ let wgOrt: OperationRetryRecord | undefined = undefined;
+ if (pushInc.withdrawalGroupId) {
+ wg = await tx.withdrawalGroups.get(pushInc.withdrawalGroupId);
+ if (wg) {
+ const withdrawalOpId = TaskIdentifiers.forWithdrawal(wg);
+ wgOrt = await tx.operationRetries.get(withdrawalOpId);
+ }
+ }
+ const pushIncOpId = TaskIdentifiers.forPeerPushCredit(pushInc);
+ let pushIncOrt = await tx.operationRetries.get(pushIncOpId);
+
+ return buildTransactionForPeerPushCredit(
+ pushInc,
+ pushIncOrt,
+ ct.contractTermsRaw,
+ wg,
+ wgOrt,
+ );
+ },
+ );
+ }
+
+ case TransactionType.PeerPullCredit: {
+ const pursePub = parsedTx.pursePub;
+ return await wex.db.runReadWriteTx(
+ {
+ storeNames: [
+ "peerPullCredit",
+ "contractTerms",
+ "withdrawalGroups",
+ "operationRetries",
+ ],
+ },
+ async (tx) => {
+ const pushInc = await tx.peerPullCredit.get(pursePub);
+ if (!pushInc) throw Error("not found");
+ const ct = await tx.contractTerms.get(pushInc.contractTermsHash);
+ checkDbInvariant(!!ct);
+
+ let wg: WithdrawalGroupRecord | undefined = undefined;
+ let wgOrt: OperationRetryRecord | undefined = undefined;
+ if (pushInc.withdrawalGroupId) {
+ wg = await tx.withdrawalGroups.get(pushInc.withdrawalGroupId);
+ if (wg) {
+ const withdrawalOpId = TaskIdentifiers.forWithdrawal(wg);
+ wgOrt = await tx.operationRetries.get(withdrawalOpId);
+ }
+ }
+ const pushIncOpId =
+ TaskIdentifiers.forPeerPullPaymentInitiation(pushInc);
+ let pushIncOrt = await tx.operationRetries.get(pushIncOpId);
+
+ return buildTransactionForPeerPullCredit(
+ pushInc,
+ pushIncOrt,
+ ct.contractTermsRaw,
+ wg,
+ wgOrt,
+ );
+ },
+ );
+ }
+ }
+}
+
+function buildTransactionForPushPaymentDebit(
+ pi: PeerPushDebitRecord,
+ contractTerms: PeerContractTerms,
+ ort?: OperationRetryRecord,
+): Transaction {
+ let talerUri: string | undefined = undefined;
+ switch (pi.status) {
+ case PeerPushDebitStatus.PendingReady:
+ case PeerPushDebitStatus.SuspendedReady:
+ talerUri = stringifyPayPushUri({
+ exchangeBaseUrl: pi.exchangeBaseUrl,
+ contractPriv: pi.contractPriv,
+ });
+ }
+ const txState = computePeerPushDebitTransactionState(pi);
+ return {
+ type: TransactionType.PeerPushDebit,
+ txState,
+ txActions: computePeerPushDebitTransactionActions(pi),
+ amountEffective: isUnsuccessfulTransaction(txState)
+ ? Amounts.stringify(Amounts.zeroOfAmount(pi.totalCost))
+ : pi.totalCost,
+ amountRaw: pi.amount,
+ exchangeBaseUrl: pi.exchangeBaseUrl,
+ info: {
+ expiration: contractTerms.purse_expiration,
+ summary: contractTerms.summary,
+ },
+ timestamp: timestampPreciseFromDb(pi.timestampCreated),
+ talerUri,
+ transactionId: constructTransactionIdentifier({
+ tag: TransactionType.PeerPushDebit,
+ pursePub: pi.pursePub,
+ }),
+ ...(ort?.lastError ? { error: ort.lastError } : {}),
+ };
+}
+
+function buildTransactionForPullPaymentDebit(
+ pi: PeerPullPaymentIncomingRecord,
+ contractTerms: PeerContractTerms,
+ ort?: OperationRetryRecord,
+): Transaction {
+ const txState = computePeerPullDebitTransactionState(pi);
+ return {
+ type: TransactionType.PeerPullDebit,
+ txState,
+ txActions: computePeerPullDebitTransactionActions(pi),
+ amountEffective: isUnsuccessfulTransaction(txState)
+ ? Amounts.stringify(Amounts.zeroOfAmount(pi.amount))
+ : pi.coinSel?.totalCost
+ ? pi.coinSel?.totalCost
+ : Amounts.stringify(pi.amount),
+ amountRaw: Amounts.stringify(pi.amount),
+ exchangeBaseUrl: pi.exchangeBaseUrl,
+ info: {
+ expiration: contractTerms.purse_expiration,
+ summary: contractTerms.summary,
+ },
+ timestamp: timestampPreciseFromDb(pi.timestampCreated),
+ transactionId: constructTransactionIdentifier({
+ tag: TransactionType.PeerPullDebit,
+ peerPullDebitId: pi.peerPullDebitId,
+ }),
+ ...(ort?.lastError ? { error: ort.lastError } : {}),
+ };
+}
+
+function buildTransactionForPeerPullCredit(
+ pullCredit: PeerPullCreditRecord,
+ pullCreditOrt: OperationRetryRecord | undefined,
+ peerContractTerms: PeerContractTerms,
+ wsr: WithdrawalGroupRecord | undefined,
+ wsrOrt: OperationRetryRecord | undefined,
+): Transaction {
+ if (wsr) {
+ if (wsr.wgInfo.withdrawalType !== WithdrawalRecordType.PeerPullCredit) {
+ throw Error(`Unexpected withdrawalType: ${wsr.wgInfo.withdrawalType}`);
+ }
+ /**
+ * FIXME: this should be handled in the withdrawal process.
+ * PeerPull withdrawal fails until reserve have funds but it is not
+ * an error from the user perspective.
+ */
+ const silentWithdrawalErrorForInvoice =
+ wsrOrt?.lastError &&
+ wsrOrt.lastError.code ===
+ TalerErrorCode.WALLET_WITHDRAWAL_GROUP_INCOMPLETE &&
+ Object.values(wsrOrt.lastError.errorsPerCoin ?? {}).every((e) => {
+ return (
+ e.code === TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR &&
+ e.httpStatusCode === 409
+ );
+ });
+ const txState = computePeerPullCreditTransactionState(pullCredit);
+ return {
+ type: TransactionType.PeerPullCredit,
+ txState,
+ txActions: computePeerPullCreditTransactionActions(pullCredit),
+ amountEffective: isUnsuccessfulTransaction(txState)
+ ? Amounts.stringify(Amounts.zeroOfAmount(wsr.instructedAmount))
+ : Amounts.stringify(wsr.denomsSel.totalCoinValue),
+ amountRaw: Amounts.stringify(wsr.instructedAmount),
+ exchangeBaseUrl: wsr.exchangeBaseUrl,
+ timestamp: timestampPreciseFromDb(pullCredit.mergeTimestamp),
+ info: {
+ expiration: peerContractTerms.purse_expiration,
+ summary: peerContractTerms.summary,
+ },
+ talerUri: stringifyPayPullUri({
+ exchangeBaseUrl: wsr.exchangeBaseUrl,
+ contractPriv: wsr.wgInfo.contractPriv,
+ }),
+ transactionId: constructTransactionIdentifier({
+ tag: TransactionType.PeerPullCredit,
+ pursePub: pullCredit.pursePub,
+ }),
+ kycUrl: pullCredit.kycUrl,
+ ...(wsrOrt?.lastError
+ ? {
+ error: silentWithdrawalErrorForInvoice
+ ? undefined
+ : wsrOrt.lastError,
+ }
+ : {}),
+ };
+ }
+
+ const txState = computePeerPullCreditTransactionState(pullCredit);
+ return {
+ type: TransactionType.PeerPullCredit,
+ txState,
+ txActions: computePeerPullCreditTransactionActions(pullCredit),
+ amountEffective: isUnsuccessfulTransaction(txState)
+ ? Amounts.stringify(Amounts.zeroOfAmount(peerContractTerms.amount))
+ : Amounts.stringify(pullCredit.estimatedAmountEffective),
+ amountRaw: Amounts.stringify(peerContractTerms.amount),
+ exchangeBaseUrl: pullCredit.exchangeBaseUrl,
+ timestamp: timestampPreciseFromDb(pullCredit.mergeTimestamp),
+ info: {
+ expiration: peerContractTerms.purse_expiration,
+ summary: peerContractTerms.summary,
+ },
+ talerUri: stringifyPayPullUri({
+ exchangeBaseUrl: pullCredit.exchangeBaseUrl,
+ contractPriv: pullCredit.contractPriv,
+ }),
+ transactionId: constructTransactionIdentifier({
+ tag: TransactionType.PeerPullCredit,
+ pursePub: pullCredit.pursePub,
+ }),
+ kycUrl: pullCredit.kycUrl,
+ ...(pullCreditOrt?.lastError ? { error: pullCreditOrt.lastError } : {}),
+ };
+}
+
+function buildTransactionForPeerPushCredit(
+ pushInc: PeerPushPaymentIncomingRecord,
+ pushOrt: OperationRetryRecord | undefined,
+ peerContractTerms: PeerContractTerms,
+ wsr: WithdrawalGroupRecord | undefined,
+ wsrOrt: OperationRetryRecord | undefined,
+): Transaction {
+ if (wsr) {
+ if (wsr.wgInfo.withdrawalType !== WithdrawalRecordType.PeerPushCredit) {
+ throw Error("invalid withdrawal group type for push payment credit");
+ }
+
+ const txState = computePeerPushCreditTransactionState(pushInc);
+ return {
+ type: TransactionType.PeerPushCredit,
+ txState,
+ txActions: computePeerPushCreditTransactionActions(pushInc),
+ amountEffective: isUnsuccessfulTransaction(txState)
+ ? Amounts.stringify(Amounts.zeroOfAmount(wsr.instructedAmount))
+ : Amounts.stringify(wsr.denomsSel.totalCoinValue),
+ amountRaw: Amounts.stringify(wsr.instructedAmount),
+ exchangeBaseUrl: wsr.exchangeBaseUrl,
+ info: {
+ expiration: peerContractTerms.purse_expiration,
+ summary: peerContractTerms.summary,
+ },
+ timestamp: timestampPreciseFromDb(wsr.timestampStart),
+ transactionId: constructTransactionIdentifier({
+ tag: TransactionType.PeerPushCredit,
+ peerPushCreditId: pushInc.peerPushCreditId,
+ }),
+ kycUrl: pushInc.kycUrl,
+ ...(wsrOrt?.lastError ? { error: wsrOrt.lastError } : {}),
+ };
+ }
+
+ const txState = computePeerPushCreditTransactionState(pushInc);
+ return {
+ type: TransactionType.PeerPushCredit,
+ txState,
+ txActions: computePeerPushCreditTransactionActions(pushInc),
+ amountEffective: isUnsuccessfulTransaction(txState)
+ ? Amounts.stringify(Amounts.zeroOfAmount(peerContractTerms.amount))
+ : // FIXME: This is wrong, needs to consider fees!
+ Amounts.stringify(peerContractTerms.amount),
+ amountRaw: Amounts.stringify(peerContractTerms.amount),
+ exchangeBaseUrl: pushInc.exchangeBaseUrl,
+ info: {
+ expiration: peerContractTerms.purse_expiration,
+ summary: peerContractTerms.summary,
+ },
+ kycUrl: pushInc.kycUrl,
+ timestamp: timestampPreciseFromDb(pushInc.timestamp),
+ transactionId: constructTransactionIdentifier({
+ tag: TransactionType.PeerPushCredit,
+ peerPushCreditId: pushInc.peerPushCreditId,
+ }),
+ ...(pushOrt?.lastError ? { error: pushOrt.lastError } : {}),
+ };
+}
+
+function buildTransactionForBankIntegratedWithdraw(
+ wgRecord: WithdrawalGroupRecord,
+ ort?: OperationRetryRecord,
+): TransactionWithdrawal {
+ if (wgRecord.wgInfo.withdrawalType !== WithdrawalRecordType.BankIntegrated)
+ throw Error("");
+
+ const txState = computeWithdrawalTransactionStatus(wgRecord);
+ return {
+ type: TransactionType.Withdrawal,
+ txState,
+ txActions: computeWithdrawalTransactionActions(wgRecord),
+ amountEffective: isUnsuccessfulTransaction(txState)
+ ? Amounts.stringify(Amounts.zeroOfAmount(wgRecord.instructedAmount))
+ : Amounts.stringify(wgRecord.denomsSel.totalCoinValue),
+ amountRaw: Amounts.stringify(wgRecord.instructedAmount),
+ withdrawalDetails: {
+ type: WithdrawalType.TalerBankIntegrationApi,
+ confirmed: wgRecord.wgInfo.bankInfo.timestampBankConfirmed ? true : false,
+ exchangeCreditAccountDetails: wgRecord.wgInfo.exchangeCreditAccounts,
+ reservePub: wgRecord.reservePub,
+ bankConfirmationUrl: wgRecord.wgInfo.bankInfo.confirmUrl,
+ reserveIsReady:
+ wgRecord.status === WithdrawalGroupStatus.Done ||
+ wgRecord.status === WithdrawalGroupStatus.PendingReady,
+ },
+ kycUrl: wgRecord.kycUrl,
+ exchangeBaseUrl: wgRecord.exchangeBaseUrl,
+ timestamp: timestampPreciseFromDb(wgRecord.timestampStart),
+ transactionId: constructTransactionIdentifier({
+ tag: TransactionType.Withdrawal,
+ withdrawalGroupId: wgRecord.withdrawalGroupId,
+ }),
+ ...(ort?.lastError ? { error: ort.lastError } : {}),
+ };
+}
+
+export function isUnsuccessfulTransaction(state: TransactionState): boolean {
+ return (
+ state.major === TransactionMajorState.Aborted ||
+ state.major === TransactionMajorState.Expired ||
+ state.major === TransactionMajorState.Aborting ||
+ state.major === TransactionMajorState.Deleted ||
+ state.major === TransactionMajorState.Failed
+ );
+}
+
+function buildTransactionForManualWithdraw(
+ withdrawalGroup: WithdrawalGroupRecord,
+ exchangeDetails: ExchangeWireDetails,
+ ort?: OperationRetryRecord,
+): TransactionWithdrawal {
+ if (withdrawalGroup.wgInfo.withdrawalType !== WithdrawalRecordType.BankManual)
+ throw Error("");
+
+ const plainPaytoUris =
+ exchangeDetails.wireInfo?.accounts.map((x) => x.payto_uri) ?? [];
+
+ const exchangePaytoUris = augmentPaytoUrisForWithdrawal(
+ plainPaytoUris,
+ withdrawalGroup.reservePub,
+ withdrawalGroup.instructedAmount,
+ );
+
+ const txState = computeWithdrawalTransactionStatus(withdrawalGroup);
+
+ return {
+ type: TransactionType.Withdrawal,
+ txState,
+ txActions: computeWithdrawalTransactionActions(withdrawalGroup),
+ amountEffective: isUnsuccessfulTransaction(txState)
+ ? Amounts.stringify(
+ Amounts.zeroOfAmount(withdrawalGroup.instructedAmount),
+ )
+ : Amounts.stringify(withdrawalGroup.denomsSel.totalCoinValue),
+ amountRaw: Amounts.stringify(withdrawalGroup.instructedAmount),
+ withdrawalDetails: {
+ type: WithdrawalType.ManualTransfer,
+ reservePub: withdrawalGroup.reservePub,
+ exchangePaytoUris,
+ exchangeCreditAccountDetails:
+ withdrawalGroup.wgInfo.exchangeCreditAccounts,
+ reserveIsReady:
+ withdrawalGroup.status === WithdrawalGroupStatus.Done ||
+ withdrawalGroup.status === WithdrawalGroupStatus.PendingReady,
+ },
+ kycUrl: withdrawalGroup.kycUrl,
+ exchangeBaseUrl: withdrawalGroup.exchangeBaseUrl,
+ timestamp: timestampPreciseFromDb(withdrawalGroup.timestampStart),
+ transactionId: constructTransactionIdentifier({
+ tag: TransactionType.Withdrawal,
+ withdrawalGroupId: withdrawalGroup.withdrawalGroupId,
+ }),
+ ...(ort?.lastError ? { error: ort.lastError } : {}),
+ };
+}
+
+function buildTransactionForRefund(
+ refundRecord: RefundGroupRecord,
+ maybeContractData: WalletContractData | undefined,
+): Transaction {
+ let paymentInfo: RefundPaymentInfo | undefined = undefined;
+
+ if (maybeContractData) {
+ paymentInfo = {
+ merchant: maybeContractData.merchant,
+ summary: maybeContractData.summary,
+ summary_i18n: maybeContractData.summaryI18n,
+ };
+ }
+
+ const txState = computeRefundTransactionState(refundRecord);
+ return {
+ type: TransactionType.Refund,
+ amountEffective: isUnsuccessfulTransaction(txState)
+ ? Amounts.stringify(Amounts.zeroOfAmount(refundRecord.amountEffective))
+ : refundRecord.amountEffective,
+ amountRaw: refundRecord.amountRaw,
+ refundedTransactionId: constructTransactionIdentifier({
+ tag: TransactionType.Payment,
+ proposalId: refundRecord.proposalId,
+ }),
+ timestamp: timestampPreciseFromDb(refundRecord.timestampCreated),
+ transactionId: constructTransactionIdentifier({
+ tag: TransactionType.Refund,
+ refundGroupId: refundRecord.refundGroupId,
+ }),
+ txState,
+ txActions: [],
+ paymentInfo,
+ };
+}
+
+function buildTransactionForRefresh(
+ refreshGroupRecord: RefreshGroupRecord,
+ ort?: OperationRetryRecord,
+): Transaction {
+ const inputAmount = Amounts.sumOrZero(
+ refreshGroupRecord.currency,
+ refreshGroupRecord.inputPerCoin,
+ ).amount;
+ const outputAmount = Amounts.sumOrZero(
+ refreshGroupRecord.currency,
+ refreshGroupRecord.expectedOutputPerCoin,
+ ).amount;
+ const txState = computeRefreshTransactionState(refreshGroupRecord);
+ return {
+ type: TransactionType.Refresh,
+ txState,
+ txActions: computeRefreshTransactionActions(refreshGroupRecord),
+ refreshReason: refreshGroupRecord.reason,
+ amountEffective: isUnsuccessfulTransaction(txState)
+ ? Amounts.stringify(Amounts.zeroOfAmount(inputAmount))
+ : Amounts.stringify(Amounts.sub(outputAmount, inputAmount).amount),
+ amountRaw: Amounts.stringify(
+ Amounts.zeroOfCurrency(refreshGroupRecord.currency),
+ ),
+ refreshInputAmount: Amounts.stringify(inputAmount),
+ refreshOutputAmount: Amounts.stringify(outputAmount),
+ originatingTransactionId: refreshGroupRecord.originatingTransactionId,
+ timestamp: timestampPreciseFromDb(refreshGroupRecord.timestampCreated),
+ transactionId: constructTransactionIdentifier({
+ tag: TransactionType.Refresh,
+ refreshGroupId: refreshGroupRecord.refreshGroupId,
+ }),
+ ...(ort?.lastError ? { error: ort.lastError } : {}),
+ };
+}
+
+function buildTransactionForDenomLoss(rec: DenomLossEventRecord): Transaction {
+ const txState = computeDenomLossTransactionStatus(rec);
+ return {
+ type: TransactionType.DenomLoss,
+ txState,
+ txActions: [TransactionAction.Delete],
+ amountRaw: Amounts.stringify(rec.amount),
+ amountEffective: Amounts.stringify(rec.amount),
+ timestamp: timestampPreciseFromDb(rec.timestampCreated),
+ transactionId: constructTransactionIdentifier({
+ tag: TransactionType.DenomLoss,
+ denomLossEventId: rec.denomLossEventId,
+ }),
+ lossEventType: rec.eventType,
+ exchangeBaseUrl: rec.exchangeBaseUrl,
+ };
+}
+
+function buildTransactionForDeposit(
+ dg: DepositGroupRecord,
+ ort?: OperationRetryRecord,
+): Transaction {
+ let deposited = true;
+ if (dg.statusPerCoin) {
+ for (const d of dg.statusPerCoin) {
+ if (d == DepositElementStatus.DepositPending) {
+ deposited = false;
+ }
+ }
+ } else {
+ deposited = false;
+ }
+
+ const trackingState: DepositTransactionTrackingState[] = [];
+
+ for (const ts of Object.values(dg.trackingState ?? {})) {
+ trackingState.push({
+ amountRaw: ts.amountRaw,
+ timestampExecuted: timestampProtocolFromDb(ts.timestampExecuted),
+ wireFee: ts.wireFee,
+ wireTransferId: ts.wireTransferId,
+ });
+ }
+
+ let wireTransferProgress = 0;
+ if (dg.statusPerCoin) {
+ wireTransferProgress =
+ (100 *
+ dg.statusPerCoin.reduce(
+ (prev, cur) => prev + (cur === DepositElementStatus.Wired ? 1 : 0),
+ 0,
+ )) /
+ dg.statusPerCoin.length;
+ }
+
+ const txState = computeDepositTransactionStatus(dg);
+ return {
+ type: TransactionType.Deposit,
+ txState,
+ txActions: computeDepositTransactionActions(dg),
+ amountRaw: Amounts.stringify(dg.counterpartyEffectiveDepositAmount),
+ amountEffective: isUnsuccessfulTransaction(txState)
+ ? Amounts.stringify(Amounts.zeroOfAmount(dg.totalPayCost))
+ : Amounts.stringify(dg.totalPayCost),
+ timestamp: timestampPreciseFromDb(dg.timestampCreated),
+ targetPaytoUri: dg.wire.payto_uri,
+ wireTransferDeadline: timestampProtocolFromDb(dg.wireTransferDeadline),
+ transactionId: constructTransactionIdentifier({
+ tag: TransactionType.Deposit,
+ depositGroupId: dg.depositGroupId,
+ }),
+ wireTransferProgress,
+ depositGroupId: dg.depositGroupId,
+ trackingState,
+ deposited,
+ ...(ort?.lastError ? { error: ort.lastError } : {}),
+ };
+}
+
+async function lookupMaybeContractData(
+ tx: WalletDbReadOnlyTransaction<["purchases", "contractTerms"]>,
+ proposalId: string,
+): Promise<WalletContractData | undefined> {
+ let contractData: WalletContractData | undefined = undefined;
+ const purchaseTx = await tx.purchases.get(proposalId);
+ if (purchaseTx && purchaseTx.download) {
+ const download = purchaseTx.download;
+ const contractTermsRecord = await tx.contractTerms.get(
+ download.contractTermsHash,
+ );
+ if (!contractTermsRecord) {
+ return;
+ }
+ contractData = extractContractData(
+ contractTermsRecord?.contractTermsRaw,
+ download.contractTermsHash,
+ download.contractTermsMerchantSig,
+ );
+ }
+
+ return contractData;
+}
+
+async function buildTransactionForPurchase(
+ purchaseRecord: PurchaseRecord,
+ contractData: WalletContractData,
+ refundsInfo: RefundGroupRecord[],
+ ort?: OperationRetryRecord,
+): Promise<Transaction> {
+ const zero = Amounts.zeroOfAmount(contractData.amount);
+
+ const info: OrderShortInfo = {
+ merchant: contractData.merchant,
+ orderId: contractData.orderId,
+ summary: contractData.summary,
+ summary_i18n: contractData.summaryI18n,
+ contractTermsHash: contractData.contractTermsHash,
+ };
+
+ if (contractData.fulfillmentUrl !== "") {
+ info.fulfillmentUrl = contractData.fulfillmentUrl;
+ }
+
+ const refunds: RefundInfoShort[] = refundsInfo.map((r) => ({
+ amountEffective: r.amountEffective,
+ amountRaw: r.amountRaw,
+ timestamp: TalerPreciseTimestamp.round(
+ timestampPreciseFromDb(r.timestampCreated),
+ ),
+ transactionId: constructTransactionIdentifier({
+ tag: TransactionType.Refund,
+ refundGroupId: r.refundGroupId,
+ }),
+ }));
+
+ const timestamp = purchaseRecord.timestampAccept;
+ checkDbInvariant(!!timestamp);
+ checkDbInvariant(!!purchaseRecord.payInfo);
+
+ const txState = computePayMerchantTransactionState(purchaseRecord);
+ return {
+ type: TransactionType.Payment,
+ txState,
+ txActions: computePayMerchantTransactionActions(purchaseRecord),
+ amountRaw: Amounts.stringify(contractData.amount),
+ amountEffective: isUnsuccessfulTransaction(txState)
+ ? Amounts.stringify(zero)
+ : Amounts.stringify(purchaseRecord.payInfo.totalPayCost),
+ totalRefundRaw: Amounts.stringify(zero), // FIXME!
+ totalRefundEffective: Amounts.stringify(zero), // FIXME!
+ refundPending:
+ purchaseRecord.refundAmountAwaiting === undefined
+ ? undefined
+ : Amounts.stringify(purchaseRecord.refundAmountAwaiting),
+ refunds,
+ posConfirmation: purchaseRecord.posConfirmation,
+ timestamp: timestampPreciseFromDb(timestamp),
+ transactionId: constructTransactionIdentifier({
+ tag: TransactionType.Payment,
+ proposalId: purchaseRecord.proposalId,
+ }),
+ proposalId: purchaseRecord.proposalId,
+ info,
+ refundQueryActive:
+ purchaseRecord.purchaseStatus === PurchaseStatus.PendingQueryingRefund,
+ ...(ort?.lastError ? { error: ort.lastError } : {}),
+ };
+}
+
+export async function getWithdrawalTransactionByUri(
+ wex: WalletExecutionContext,
+ request: WithdrawalTransactionByURIRequest,
+): Promise<TransactionWithdrawal | undefined> {
+ return await wex.db.runReadWriteTx(
+ {
+ storeNames: [
+ "withdrawalGroups",
+ "exchangeDetails",
+ "exchanges",
+ "operationRetries",
+ ],
+ },
+ async (tx) => {
+ const withdrawalGroupRecord =
+ await tx.withdrawalGroups.indexes.byTalerWithdrawUri.get(
+ request.talerWithdrawUri,
+ );
+
+ if (!withdrawalGroupRecord) {
+ return undefined;
+ }
+
+ const opId = TaskIdentifiers.forWithdrawal(withdrawalGroupRecord);
+ const ort = await tx.operationRetries.get(opId);
+
+ if (
+ withdrawalGroupRecord.wgInfo.withdrawalType ===
+ WithdrawalRecordType.BankIntegrated
+ ) {
+ return buildTransactionForBankIntegratedWithdraw(
+ withdrawalGroupRecord,
+ ort,
+ );
+ }
+ const exchangeDetails = await getExchangeWireDetailsInTx(
+ tx,
+ withdrawalGroupRecord.exchangeBaseUrl,
+ );
+ if (!exchangeDetails) throw Error("not exchange details");
+
+ return buildTransactionForManualWithdraw(
+ withdrawalGroupRecord,
+ exchangeDetails,
+ ort,
+ );
+ },
+ );
+}
+
+/**
+ * Retrieve the full event history for this wallet.
+ */
+export async function getTransactions(
+ wex: WalletExecutionContext,
+ transactionsRequest?: TransactionsRequest,
+): Promise<TransactionsResponse> {
+ const transactions: Transaction[] = [];
+
+ const filter: TransactionRecordFilter = {};
+ if (transactionsRequest?.filterByState) {
+ filter.onlyState = transactionsRequest.filterByState;
+ }
+
+ await wex.db.runReadOnlyTx(
+ {
+ storeNames: [
+ "coins",
+ "denominations",
+ "depositGroups",
+ "exchangeDetails",
+ "exchanges",
+ "operationRetries",
+ "peerPullDebit",
+ "peerPushDebit",
+ "peerPushCredit",
+ "peerPullCredit",
+ "planchets",
+ "purchases",
+ "contractTerms",
+ "recoupGroups",
+ "rewards",
+ "tombstones",
+ "withdrawalGroups",
+ "refreshGroups",
+ "refundGroups",
+ "denomLossEvents",
+ ],
+ },
+ async (tx) => {
+ await iterRecordsForPeerPushDebit(tx, filter, async (pi) => {
+ const amount = Amounts.parseOrThrow(pi.amount);
+ const exchangesInTx = [pi.exchangeBaseUrl];
+ if (
+ shouldSkipCurrency(
+ transactionsRequest,
+ amount.currency,
+ exchangesInTx,
+ )
+ ) {
+ return;
+ }
+ if (shouldSkipSearch(transactionsRequest, [])) {
+ return;
+ }
+ const ct = await tx.contractTerms.get(pi.contractTermsHash);
+ checkDbInvariant(!!ct);
+ transactions.push(
+ buildTransactionForPushPaymentDebit(pi, ct.contractTermsRaw),
+ );
+ });
+
+ await iterRecordsForPeerPullDebit(tx, filter, async (pi) => {
+ const amount = Amounts.parseOrThrow(pi.amount);
+ const exchangesInTx = [pi.exchangeBaseUrl];
+ if (
+ shouldSkipCurrency(
+ transactionsRequest,
+ amount.currency,
+ exchangesInTx,
+ )
+ ) {
+ return;
+ }
+ if (shouldSkipSearch(transactionsRequest, [])) {
+ return;
+ }
+ if (
+ pi.status !== PeerPullDebitRecordStatus.PendingDeposit &&
+ pi.status !== PeerPullDebitRecordStatus.Done
+ ) {
+ // FIXME: Why?!
+ return;
+ }
+
+ const contractTermsRec = await tx.contractTerms.get(
+ pi.contractTermsHash,
+ );
+ if (!contractTermsRec) {
+ return;
+ }
+
+ transactions.push(
+ buildTransactionForPullPaymentDebit(
+ pi,
+ contractTermsRec.contractTermsRaw,
+ ),
+ );
+ });
+
+ await iterRecordsForPeerPushCredit(tx, filter, async (pi) => {
+ if (!pi.currency) {
+ // Legacy transaction
+ return;
+ }
+ const exchangesInTx = [pi.exchangeBaseUrl];
+ if (
+ shouldSkipCurrency(transactionsRequest, pi.currency, exchangesInTx)
+ ) {
+ return;
+ }
+ if (shouldSkipSearch(transactionsRequest, [])) {
+ return;
+ }
+ if (pi.status === PeerPushCreditStatus.DialogProposed) {
+ // We don't report proposed push credit transactions, user needs
+ // to scan URI again and confirm to see it.
+ return;
+ }
+ const ct = await tx.contractTerms.get(pi.contractTermsHash);
+ let wg: WithdrawalGroupRecord | undefined = undefined;
+ let wgOrt: OperationRetryRecord | undefined = undefined;
+ if (pi.withdrawalGroupId) {
+ wg = await tx.withdrawalGroups.get(pi.withdrawalGroupId);
+ if (wg) {
+ const withdrawalOpId = TaskIdentifiers.forWithdrawal(wg);
+ wgOrt = await tx.operationRetries.get(withdrawalOpId);
+ }
+ }
+ const pushIncOpId = TaskIdentifiers.forPeerPushCredit(pi);
+ let pushIncOrt = await tx.operationRetries.get(pushIncOpId);
+
+ checkDbInvariant(!!ct);
+ transactions.push(
+ buildTransactionForPeerPushCredit(
+ pi,
+ pushIncOrt,
+ ct.contractTermsRaw,
+ wg,
+ wgOrt,
+ ),
+ );
+ });
+
+ await iterRecordsForPeerPullCredit(tx, filter, async (pi) => {
+ const currency = Amounts.currencyOf(pi.amount);
+ const exchangesInTx = [pi.exchangeBaseUrl];
+ if (shouldSkipCurrency(transactionsRequest, currency, exchangesInTx)) {
+ return;
+ }
+ if (shouldSkipSearch(transactionsRequest, [])) {
+ return;
+ }
+ const ct = await tx.contractTerms.get(pi.contractTermsHash);
+ let wg: WithdrawalGroupRecord | undefined = undefined;
+ let wgOrt: OperationRetryRecord | undefined = undefined;
+ if (pi.withdrawalGroupId) {
+ wg = await tx.withdrawalGroups.get(pi.withdrawalGroupId);
+ if (wg) {
+ const withdrawalOpId = TaskIdentifiers.forWithdrawal(wg);
+ wgOrt = await tx.operationRetries.get(withdrawalOpId);
+ }
+ }
+ const pushIncOpId = TaskIdentifiers.forPeerPullPaymentInitiation(pi);
+ let pushIncOrt = await tx.operationRetries.get(pushIncOpId);
+
+ checkDbInvariant(!!ct);
+ transactions.push(
+ buildTransactionForPeerPullCredit(
+ pi,
+ pushIncOrt,
+ ct.contractTermsRaw,
+ wg,
+ wgOrt,
+ ),
+ );
+ });
+
+ await iterRecordsForRefund(tx, filter, async (refundGroup) => {
+ const currency = Amounts.currencyOf(refundGroup.amountRaw);
+
+ const exchangesInTx: string[] = [];
+ const p = await tx.purchases.get(refundGroup.proposalId);
+ if (!p || !p.payInfo || !p.payInfo.payCoinSelection) {
+ //refund with no payment
+ return;
+ }
+
+ // FIXME: This is very slow, should become obsolete with materialized transactions.
+ for (const cp of p.payInfo.payCoinSelection.coinPubs) {
+ const c = await tx.coins.get(cp);
+ if (c?.exchangeBaseUrl) {
+ exchangesInTx.push(c.exchangeBaseUrl);
+ }
+ }
+
+ if (shouldSkipCurrency(transactionsRequest, currency, exchangesInTx)) {
+ return;
+ }
+ const contractData = await lookupMaybeContractData(
+ tx,
+ refundGroup.proposalId,
+ );
+ transactions.push(buildTransactionForRefund(refundGroup, contractData));
+ });
+
+ await iterRecordsForRefresh(tx, filter, async (rg) => {
+ const exchangesInTx = rg.infoPerExchange
+ ? Object.keys(rg.infoPerExchange)
+ : [];
+ if (
+ shouldSkipCurrency(transactionsRequest, rg.currency, exchangesInTx)
+ ) {
+ return;
+ }
+ let required = false;
+ const opId = TaskIdentifiers.forRefresh(rg);
+ if (transactionsRequest?.includeRefreshes) {
+ required = true;
+ } else if (rg.operationStatus !== RefreshOperationStatus.Finished) {
+ const ort = await tx.operationRetries.get(opId);
+ if (ort) {
+ required = true;
+ }
+ }
+ if (required) {
+ const ort = await tx.operationRetries.get(opId);
+ transactions.push(buildTransactionForRefresh(rg, ort));
+ }
+ });
+
+ await iterRecordsForWithdrawal(tx, filter, async (wsr) => {
+ const exchangesInTx = [wsr.exchangeBaseUrl];
+ if (
+ shouldSkipCurrency(
+ transactionsRequest,
+ Amounts.currencyOf(wsr.rawWithdrawalAmount),
+ exchangesInTx,
+ )
+ ) {
+ return;
+ }
+
+ if (shouldSkipSearch(transactionsRequest, [])) {
+ return;
+ }
+
+ const opId = TaskIdentifiers.forWithdrawal(wsr);
+ const ort = await tx.operationRetries.get(opId);
+
+ switch (wsr.wgInfo.withdrawalType) {
+ case WithdrawalRecordType.PeerPullCredit:
+ // Will be reported by the corresponding p2p transaction.
+ // FIXME: If this is an orphan withdrawal, still report it as a withdrawal!
+ // FIXME: Still report if requested with verbose option?
+ return;
+ case WithdrawalRecordType.PeerPushCredit:
+ // Will be reported by the corresponding p2p transaction.
+ // FIXME: If this is an orphan withdrawal, still report it as a withdrawal!
+ // FIXME: Still report if requested with verbose option?
+ return;
+ case WithdrawalRecordType.BankIntegrated:
+ transactions.push(
+ buildTransactionForBankIntegratedWithdraw(wsr, ort),
+ );
+ return;
+ case WithdrawalRecordType.BankManual: {
+ const exchangeDetails = await getExchangeWireDetailsInTx(
+ tx,
+ wsr.exchangeBaseUrl,
+ );
+ if (!exchangeDetails) {
+ // FIXME: report somehow
+ return;
+ }
+
+ transactions.push(
+ buildTransactionForManualWithdraw(wsr, exchangeDetails, ort),
+ );
+ return;
+ }
+ case WithdrawalRecordType.Recoup:
+ // FIXME: Do we also report a transaction here?
+ return;
+ }
+ });
+
+ await iterRecordsForDenomLoss(tx, filter, async (rec) => {
+ const amount = Amounts.parseOrThrow(rec.amount);
+ const exchangesInTx = [rec.exchangeBaseUrl];
+ if (
+ shouldSkipCurrency(
+ transactionsRequest,
+ amount.currency,
+ exchangesInTx,
+ )
+ ) {
+ return;
+ }
+ transactions.push(buildTransactionForDenomLoss(rec));
+ });
+
+ await iterRecordsForDeposit(tx, filter, async (dg) => {
+ const amount = Amounts.parseOrThrow(dg.amount);
+ const exchangesInTx = dg.infoPerExchange
+ ? Object.keys(dg.infoPerExchange)
+ : [];
+ if (
+ shouldSkipCurrency(
+ transactionsRequest,
+ amount.currency,
+ exchangesInTx,
+ )
+ ) {
+ return;
+ }
+ const opId = TaskIdentifiers.forDeposit(dg);
+ const retryRecord = await tx.operationRetries.get(opId);
+
+ transactions.push(buildTransactionForDeposit(dg, retryRecord));
+ });
+
+ await iterRecordsForPurchase(tx, filter, async (purchase) => {
+ const download = purchase.download;
+ if (!download) {
+ return;
+ }
+ if (!purchase.payInfo) {
+ return;
+ }
+
+ const exchangesInTx: string[] = [];
+ for (const cp of purchase.payInfo.payCoinSelection?.coinPubs ?? []) {
+ const c = await tx.coins.get(cp);
+ if (c?.exchangeBaseUrl) {
+ exchangesInTx.push(c.exchangeBaseUrl);
+ }
+ }
+
+ if (
+ shouldSkipCurrency(
+ transactionsRequest,
+ download.currency,
+ exchangesInTx,
+ )
+ ) {
+ return;
+ }
+ const contractTermsRecord = await tx.contractTerms.get(
+ download.contractTermsHash,
+ );
+ if (!contractTermsRecord) {
+ return;
+ }
+ if (
+ shouldSkipSearch(transactionsRequest, [
+ contractTermsRecord?.contractTermsRaw?.summary || "",
+ ])
+ ) {
+ return;
+ }
+
+ const contractData = extractContractData(
+ contractTermsRecord?.contractTermsRaw,
+ download.contractTermsHash,
+ download.contractTermsMerchantSig,
+ );
+
+ const payOpId = TaskIdentifiers.forPay(purchase);
+ const payRetryRecord = await tx.operationRetries.get(payOpId);
+
+ const refunds = await tx.refundGroups.indexes.byProposalId.getAll(
+ purchase.proposalId,
+ );
+
+ transactions.push(
+ await buildTransactionForPurchase(
+ purchase,
+ contractData,
+ refunds,
+ payRetryRecord,
+ ),
+ );
+ });
+ },
+ );
+
+ // One-off checks, because of a bug where the wallet previously
+ // did not migrate the DB correctly and caused these amounts
+ // to be missing sometimes.
+ for (let tx of transactions) {
+ if (!tx.amountEffective) {
+ logger.warn(`missing amountEffective in ${j2s(tx)}`);
+ }
+ if (!tx.amountRaw) {
+ logger.warn(`missing amountRaw in ${j2s(tx)}`);
+ }
+ if (!tx.timestamp) {
+ logger.warn(`missing timestamp in ${j2s(tx)}`);
+ }
+ }
+
+ const isPending = (x: Transaction) =>
+ x.txState.major === TransactionMajorState.Pending ||
+ x.txState.major === TransactionMajorState.Aborting ||
+ x.txState.major === TransactionMajorState.Dialog;
+
+ let sortSign: number;
+ if (transactionsRequest?.sort == "descending") {
+ sortSign = -1;
+ } else {
+ sortSign = 1;
+ }
+
+ const txCmp = (h1: Transaction, h2: Transaction) => {
+ // Order transactions by timestamp. Newest transactions come first.
+ const tsCmp = AbsoluteTime.cmp(
+ AbsoluteTime.fromPreciseTimestamp(h1.timestamp),
+ AbsoluteTime.fromPreciseTimestamp(h2.timestamp),
+ );
+ // If the timestamp is exactly the same, order by transaction type.
+ if (tsCmp === 0) {
+ return Math.sign(txOrder[h1.type] - txOrder[h2.type]);
+ }
+ return sortSign * tsCmp;
+ };
+
+ if (transactionsRequest?.sort === "stable-ascending") {
+ transactions.sort(txCmp);
+ return { transactions };
+ }
+
+ const txPending = transactions.filter((x) => isPending(x));
+ const txNotPending = transactions.filter((x) => !isPending(x));
+
+ txPending.sort(txCmp);
+ txNotPending.sort(txCmp);
+
+ return { transactions: [...txPending, ...txNotPending] };
+}
+
+export type ParsedTransactionIdentifier =
+ | { tag: TransactionType.Deposit; depositGroupId: string }
+ | { tag: TransactionType.Payment; proposalId: string }
+ | { tag: TransactionType.PeerPullDebit; peerPullDebitId: string }
+ | { tag: TransactionType.PeerPullCredit; pursePub: string }
+ | { tag: TransactionType.PeerPushCredit; peerPushCreditId: string }
+ | { tag: TransactionType.PeerPushDebit; pursePub: string }
+ | { tag: TransactionType.Refresh; refreshGroupId: string }
+ | { tag: TransactionType.Refund; refundGroupId: string }
+ | { tag: TransactionType.Withdrawal; withdrawalGroupId: string }
+ | { tag: TransactionType.InternalWithdrawal; withdrawalGroupId: string }
+ | { tag: TransactionType.Recoup; recoupGroupId: string }
+ | { tag: TransactionType.DenomLoss; denomLossEventId: string };
+
+export function constructTransactionIdentifier(
+ pTxId: ParsedTransactionIdentifier,
+): TransactionIdStr {
+ switch (pTxId.tag) {
+ case TransactionType.Deposit:
+ return `txn:${pTxId.tag}:${pTxId.depositGroupId}` as TransactionIdStr;
+ case TransactionType.Payment:
+ return `txn:${pTxId.tag}:${pTxId.proposalId}` as TransactionIdStr;
+ case TransactionType.PeerPullCredit:
+ return `txn:${pTxId.tag}:${pTxId.pursePub}` as TransactionIdStr;
+ case TransactionType.PeerPullDebit:
+ return `txn:${pTxId.tag}:${pTxId.peerPullDebitId}` as TransactionIdStr;
+ case TransactionType.PeerPushCredit:
+ return `txn:${pTxId.tag}:${pTxId.peerPushCreditId}` as TransactionIdStr;
+ case TransactionType.PeerPushDebit:
+ return `txn:${pTxId.tag}:${pTxId.pursePub}` as TransactionIdStr;
+ case TransactionType.Refresh:
+ return `txn:${pTxId.tag}:${pTxId.refreshGroupId}` as TransactionIdStr;
+ case TransactionType.Refund:
+ return `txn:${pTxId.tag}:${pTxId.refundGroupId}` as TransactionIdStr;
+ case TransactionType.Withdrawal:
+ return `txn:${pTxId.tag}:${pTxId.withdrawalGroupId}` as TransactionIdStr;
+ case TransactionType.InternalWithdrawal:
+ return `txn:${pTxId.tag}:${pTxId.withdrawalGroupId}` as TransactionIdStr;
+ case TransactionType.Recoup:
+ return `txn:${pTxId.tag}:${pTxId.recoupGroupId}` as TransactionIdStr;
+ case TransactionType.DenomLoss:
+ return `txn:${pTxId.tag}:${pTxId.denomLossEventId}` as TransactionIdStr;
+ default:
+ assertUnreachable(pTxId);
+ }
+}
+
+/**
+ * Parse a transaction identifier string into a typed, structured representation.
+ */
+export function parseTransactionIdentifier(
+ transactionId: string,
+): ParsedTransactionIdentifier | undefined {
+ const txnParts = transactionId.split(":");
+
+ if (txnParts.length < 3) {
+ throw Error("id should have al least 3 parts separated by ':'");
+ }
+
+ const [prefix, type, ...rest] = txnParts;
+
+ if (prefix != "txn") {
+ throw Error("invalid transaction identifier");
+ }
+
+ switch (type) {
+ case TransactionType.Deposit:
+ return { tag: TransactionType.Deposit, depositGroupId: rest[0] };
+ case TransactionType.Payment:
+ return { tag: TransactionType.Payment, proposalId: rest[0] };
+ case TransactionType.PeerPullCredit:
+ return { tag: TransactionType.PeerPullCredit, pursePub: rest[0] };
+ case TransactionType.PeerPullDebit:
+ return {
+ tag: TransactionType.PeerPullDebit,
+ peerPullDebitId: rest[0],
+ };
+ case TransactionType.PeerPushCredit:
+ return {
+ tag: TransactionType.PeerPushCredit,
+ peerPushCreditId: rest[0],
+ };
+ case TransactionType.PeerPushDebit:
+ return { tag: TransactionType.PeerPushDebit, pursePub: rest[0] };
+ case TransactionType.Refresh:
+ return { tag: TransactionType.Refresh, refreshGroupId: rest[0] };
+ case TransactionType.Refund:
+ return {
+ tag: TransactionType.Refund,
+ refundGroupId: rest[0],
+ };
+ case TransactionType.Withdrawal:
+ return {
+ tag: TransactionType.Withdrawal,
+ withdrawalGroupId: rest[0],
+ };
+ case TransactionType.DenomLoss:
+ return {
+ tag: TransactionType.DenomLoss,
+ denomLossEventId: rest[0],
+ };
+ default:
+ return undefined;
+ }
+}
+
+function maybeTaskFromTransaction(
+ transactionId: string,
+): TaskIdStr | undefined {
+ const parsedTx = parseTransactionIdentifier(transactionId);
+
+ if (!parsedTx) {
+ throw Error("invalid transaction identifier");
+ }
+
+ // FIXME: We currently don't cancel active long-polling tasks here.
+
+ switch (parsedTx.tag) {
+ case TransactionType.PeerPullCredit:
+ return constructTaskIdentifier({
+ tag: PendingTaskType.PeerPullCredit,
+ pursePub: parsedTx.pursePub,
+ });
+ case TransactionType.Deposit:
+ return constructTaskIdentifier({
+ tag: PendingTaskType.Deposit,
+ depositGroupId: parsedTx.depositGroupId,
+ });
+ case TransactionType.InternalWithdrawal:
+ case TransactionType.Withdrawal:
+ return constructTaskIdentifier({
+ tag: PendingTaskType.Withdraw,
+ withdrawalGroupId: parsedTx.withdrawalGroupId,
+ });
+ case TransactionType.Payment:
+ return constructTaskIdentifier({
+ tag: PendingTaskType.Purchase,
+ proposalId: parsedTx.proposalId,
+ });
+ case TransactionType.Refresh:
+ return constructTaskIdentifier({
+ tag: PendingTaskType.Refresh,
+ refreshGroupId: parsedTx.refreshGroupId,
+ });
+ case TransactionType.PeerPullDebit:
+ return constructTaskIdentifier({
+ tag: PendingTaskType.PeerPullDebit,
+ peerPullDebitId: parsedTx.peerPullDebitId,
+ });
+ case TransactionType.PeerPushCredit:
+ return constructTaskIdentifier({
+ tag: PendingTaskType.PeerPushCredit,
+ peerPushCreditId: parsedTx.peerPushCreditId,
+ });
+ case TransactionType.PeerPushDebit:
+ return constructTaskIdentifier({
+ tag: PendingTaskType.PeerPushDebit,
+ pursePub: parsedTx.pursePub,
+ });
+ case TransactionType.Refund:
+ // Nothing to do for a refund transaction.
+ return undefined;
+ case TransactionType.Recoup:
+ return constructTaskIdentifier({
+ tag: PendingTaskType.Recoup,
+ recoupGroupId: parsedTx.recoupGroupId,
+ });
+ case TransactionType.DenomLoss:
+ // Nothing to do for denom loss
+ return undefined;
+ default:
+ assertUnreachable(parsedTx);
+ }
+}
+
+/**
+ * Immediately retry the underlying operation
+ * of a transaction.
+ */
+export async function retryTransaction(
+ wex: WalletExecutionContext,
+ transactionId: string,
+): Promise<void> {
+ logger.info(`resetting retry timeout for ${transactionId}`);
+ const taskId = maybeTaskFromTransaction(transactionId);
+ if (taskId) {
+ wex.taskScheduler.resetTaskRetries(taskId);
+ }
+}
+
+async function getContextForTransaction(
+ wex: WalletExecutionContext,
+ transactionId: string,
+): Promise<TransactionContext> {
+ const tx = parseTransactionIdentifier(transactionId);
+ if (!tx) {
+ throw Error("invalid transaction ID");
+ }
+ switch (tx.tag) {
+ case TransactionType.Deposit:
+ return new DepositTransactionContext(wex, tx.depositGroupId);
+ case TransactionType.Refresh:
+ return new RefreshTransactionContext(wex, tx.refreshGroupId);
+ case TransactionType.InternalWithdrawal:
+ case TransactionType.Withdrawal:
+ return new WithdrawTransactionContext(wex, tx.withdrawalGroupId);
+ case TransactionType.Payment:
+ return new PayMerchantTransactionContext(wex, tx.proposalId);
+ case TransactionType.PeerPullCredit:
+ return new PeerPullCreditTransactionContext(wex, tx.pursePub);
+ case TransactionType.PeerPushDebit:
+ return new PeerPushDebitTransactionContext(wex, tx.pursePub);
+ case TransactionType.PeerPullDebit:
+ return new PeerPullDebitTransactionContext(wex, tx.peerPullDebitId);
+ case TransactionType.PeerPushCredit:
+ return new PeerPushCreditTransactionContext(wex, tx.peerPushCreditId);
+ case TransactionType.Refund:
+ return new RefundTransactionContext(wex, tx.refundGroupId);
+ case TransactionType.Recoup:
+ //return new RecoupTransactionContext(ws, tx.recoupGroupId);
+ throw new Error("not yet supported");
+ case TransactionType.DenomLoss:
+ return new DenomLossTransactionContext(wex, tx.denomLossEventId);
+ default:
+ assertUnreachable(tx);
+ }
+}
+
+/**
+ * Suspends a pending transaction, stopping any associated network activities,
+ * but with a chance of trying again at a later time. This could be useful if
+ * a user needs to save battery power or bandwidth and an operation is expected
+ * to take longer (such as a backup, recovery or very large withdrawal operation).
+ */
+export async function suspendTransaction(
+ wex: WalletExecutionContext,
+ transactionId: string,
+): Promise<void> {
+ const ctx = await getContextForTransaction(wex, transactionId);
+ await ctx.suspendTransaction();
+}
+
+export async function failTransaction(
+ wex: WalletExecutionContext,
+ transactionId: string,
+): Promise<void> {
+ const ctx = await getContextForTransaction(wex, transactionId);
+ await ctx.failTransaction();
+}
+
+/**
+ * Resume a suspended transaction.
+ */
+export async function resumeTransaction(
+ wex: WalletExecutionContext,
+ transactionId: string,
+): Promise<void> {
+ const ctx = await getContextForTransaction(wex, transactionId);
+ await ctx.resumeTransaction();
+}
+
+/**
+ * Permanently delete a transaction based on the transaction ID.
+ */
+export async function deleteTransaction(
+ wex: WalletExecutionContext,
+ transactionId: string,
+): Promise<void> {
+ const ctx = await getContextForTransaction(wex, transactionId);
+ await ctx.deleteTransaction();
+ if (ctx.taskId) {
+ wex.taskScheduler.stopShepherdTask(ctx.taskId);
+ }
+}
+
+export async function abortTransaction(
+ wex: WalletExecutionContext,
+ transactionId: string,
+): Promise<void> {
+ const ctx = await getContextForTransaction(wex, transactionId);
+ await ctx.abortTransaction();
+}
+
+export interface TransitionInfo {
+ oldTxState: TransactionState;
+ newTxState: TransactionState;
+}
+
+/**
+ * Notify of a state transition if necessary.
+ */
+export function notifyTransition(
+ wex: WalletExecutionContext,
+ transactionId: string,
+ transitionInfo: TransitionInfo | undefined,
+ experimentalUserData: any = undefined,
+): void {
+ if (
+ transitionInfo &&
+ !(
+ transitionInfo.oldTxState.major === transitionInfo.newTxState.major &&
+ transitionInfo.oldTxState.minor === transitionInfo.newTxState.minor
+ )
+ ) {
+ wex.ws.notify({
+ type: NotificationType.TransactionStateTransition,
+ oldTxState: transitionInfo.oldTxState,
+ newTxState: transitionInfo.newTxState,
+ transactionId,
+ experimentalUserData,
+ });
+ }
+}
+
+/**
+ * Iterate refresh records based on a filter.
+ */
+async function iterRecordsForRefresh(
+ tx: WalletDbReadOnlyTransaction<["refreshGroups"]>,
+ filter: TransactionRecordFilter,
+ f: (r: RefreshGroupRecord) => Promise<void>,
+): Promise<void> {
+ let refreshGroups: RefreshGroupRecord[];
+ if (filter.onlyState === "nonfinal") {
+ const keyRange = GlobalIDB.KeyRange.bound(
+ RefreshOperationStatus.Pending,
+ RefreshOperationStatus.Suspended,
+ );
+ refreshGroups = await tx.refreshGroups.indexes.byStatus.getAll(keyRange);
+ } else {
+ refreshGroups = await tx.refreshGroups.indexes.byStatus.getAll();
+ }
+
+ for (const r of refreshGroups) {
+ await f(r);
+ }
+}
+
+async function iterRecordsForWithdrawal(
+ tx: WalletDbReadOnlyTransaction<["withdrawalGroups"]>,
+ filter: TransactionRecordFilter,
+ f: (r: WithdrawalGroupRecord) => Promise<void>,
+): Promise<void> {
+ let withdrawalGroupRecords: WithdrawalGroupRecord[];
+ if (filter.onlyState === "nonfinal") {
+ const keyRange = GlobalIDB.KeyRange.bound(
+ OPERATION_STATUS_ACTIVE_FIRST,
+ OPERATION_STATUS_ACTIVE_LAST,
+ );
+ withdrawalGroupRecords =
+ await tx.withdrawalGroups.indexes.byStatus.getAll(keyRange);
+ } else {
+ withdrawalGroupRecords =
+ await tx.withdrawalGroups.indexes.byStatus.getAll();
+ }
+ for (const wgr of withdrawalGroupRecords) {
+ await f(wgr);
+ }
+}
+
+async function iterRecordsForDeposit(
+ tx: WalletDbReadOnlyTransaction<["depositGroups"]>,
+ filter: TransactionRecordFilter,
+ f: (r: DepositGroupRecord) => Promise<void>,
+): Promise<void> {
+ let dgs: DepositGroupRecord[];
+ if (filter.onlyState === "nonfinal") {
+ const keyRange = GlobalIDB.KeyRange.bound(
+ OPERATION_STATUS_ACTIVE_FIRST,
+ OPERATION_STATUS_ACTIVE_LAST,
+ );
+ dgs = await tx.depositGroups.indexes.byStatus.getAll(keyRange);
+ } else {
+ dgs = await tx.depositGroups.indexes.byStatus.getAll();
+ }
+
+ for (const dg of dgs) {
+ await f(dg);
+ }
+}
+
+async function iterRecordsForDenomLoss(
+ tx: WalletDbReadOnlyTransaction<["denomLossEvents"]>,
+ filter: TransactionRecordFilter,
+ f: (r: DenomLossEventRecord) => Promise<void>,
+): Promise<void> {
+ let dgs: DenomLossEventRecord[];
+ if (filter.onlyState === "nonfinal") {
+ const keyRange = GlobalIDB.KeyRange.bound(
+ OPERATION_STATUS_ACTIVE_FIRST,
+ OPERATION_STATUS_ACTIVE_LAST,
+ );
+ dgs = await tx.denomLossEvents.indexes.byStatus.getAll(keyRange);
+ } else {
+ dgs = await tx.denomLossEvents.indexes.byStatus.getAll();
+ }
+
+ for (const dg of dgs) {
+ await f(dg);
+ }
+}
+
+async function iterRecordsForRefund(
+ tx: WalletDbReadOnlyTransaction<["refundGroups"]>,
+ filter: TransactionRecordFilter,
+ f: (r: RefundGroupRecord) => Promise<void>,
+): Promise<void> {
+ if (filter.onlyState === "nonfinal") {
+ const keyRange = GlobalIDB.KeyRange.bound(
+ OPERATION_STATUS_ACTIVE_FIRST,
+ OPERATION_STATUS_ACTIVE_LAST,
+ );
+ await tx.refundGroups.indexes.byStatus.iter(keyRange).forEachAsync(f);
+ } else {
+ await tx.refundGroups.iter().forEachAsync(f);
+ }
+}
+
+async function iterRecordsForPurchase(
+ tx: WalletDbReadOnlyTransaction<["purchases"]>,
+ filter: TransactionRecordFilter,
+ f: (r: PurchaseRecord) => Promise<void>,
+): Promise<void> {
+ if (filter.onlyState === "nonfinal") {
+ const keyRange = GlobalIDB.KeyRange.bound(
+ OPERATION_STATUS_ACTIVE_FIRST,
+ OPERATION_STATUS_ACTIVE_LAST,
+ );
+ await tx.purchases.indexes.byStatus.iter(keyRange).forEachAsync(f);
+ } else {
+ await tx.purchases.indexes.byStatus.iter().forEachAsync(f);
+ }
+}
+
+async function iterRecordsForPeerPullCredit(
+ tx: WalletDbReadOnlyTransaction<["peerPullCredit"]>,
+ filter: TransactionRecordFilter,
+ f: (r: PeerPullCreditRecord) => Promise<void>,
+): Promise<void> {
+ if (filter.onlyState === "nonfinal") {
+ const keyRange = GlobalIDB.KeyRange.bound(
+ OPERATION_STATUS_ACTIVE_FIRST,
+ OPERATION_STATUS_ACTIVE_LAST,
+ );
+ await tx.peerPullCredit.indexes.byStatus.iter(keyRange).forEachAsync(f);
+ } else {
+ await tx.peerPullCredit.indexes.byStatus.iter().forEachAsync(f);
+ }
+}
+
+async function iterRecordsForPeerPullDebit(
+ tx: WalletDbReadOnlyTransaction<["peerPullDebit"]>,
+ filter: TransactionRecordFilter,
+ f: (r: PeerPullPaymentIncomingRecord) => Promise<void>,
+): Promise<void> {
+ if (filter.onlyState === "nonfinal") {
+ const keyRange = GlobalIDB.KeyRange.bound(
+ OPERATION_STATUS_ACTIVE_FIRST,
+ OPERATION_STATUS_ACTIVE_LAST,
+ );
+ await tx.peerPullDebit.indexes.byStatus.iter(keyRange).forEachAsync(f);
+ } else {
+ await tx.peerPullDebit.indexes.byStatus.iter().forEachAsync(f);
+ }
+}
+
+async function iterRecordsForPeerPushDebit(
+ tx: WalletDbReadOnlyTransaction<["peerPushDebit"]>,
+ filter: TransactionRecordFilter,
+ f: (r: PeerPushDebitRecord) => Promise<void>,
+): Promise<void> {
+ if (filter.onlyState === "nonfinal") {
+ const keyRange = GlobalIDB.KeyRange.bound(
+ OPERATION_STATUS_ACTIVE_FIRST,
+ OPERATION_STATUS_ACTIVE_LAST,
+ );
+ await tx.peerPushDebit.indexes.byStatus.iter(keyRange).forEachAsync(f);
+ } else {
+ await tx.peerPushDebit.indexes.byStatus.iter().forEachAsync(f);
+ }
+}
+
+async function iterRecordsForPeerPushCredit(
+ tx: WalletDbReadOnlyTransaction<["peerPushCredit"]>,
+ filter: TransactionRecordFilter,
+ f: (r: PeerPushPaymentIncomingRecord) => Promise<void>,
+): Promise<void> {
+ if (filter.onlyState === "nonfinal") {
+ const keyRange = GlobalIDB.KeyRange.bound(
+ OPERATION_STATUS_ACTIVE_FIRST,
+ OPERATION_STATUS_ACTIVE_LAST,
+ );
+ await tx.peerPushCredit.indexes.byStatus.iter(keyRange).forEachAsync(f);
+ } else {
+ await tx.peerPushCredit.indexes.byStatus.iter().forEachAsync(f);
+ }
+}
diff --git a/packages/taler-wallet-core/src/util/asyncMemo.ts b/packages/taler-wallet-core/src/util/asyncMemo.ts
deleted file mode 100644
index 6e88081b6..000000000
--- a/packages/taler-wallet-core/src/util/asyncMemo.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2019 GNUnet e.V.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-interface MemoEntry<T> {
- p: Promise<T>;
- t: number;
- n: number;
-}
-
-export class AsyncOpMemoMap<T> {
- private n = 0;
- private memoMap: { [k: string]: MemoEntry<T> } = {};
-
- private cleanUp(key: string, n: number): void {
- const r = this.memoMap[key];
- if (r && r.n === n) {
- delete this.memoMap[key];
- }
- }
-
- memo(key: string, pg: () => Promise<T>): Promise<T> {
- const res = this.memoMap[key];
- if (res) {
- return res.p;
- }
- const n = this.n++;
- // Wrap the operation in case it immediately throws
- const p = Promise.resolve().then(() => pg());
- this.memoMap[key] = {
- p,
- n,
- t: new Date().getTime(),
- };
- return p.finally(() => {
- this.cleanUp(key, n);
- });
- }
- clear(): void {
- this.memoMap = {};
- }
-}
-
-export class AsyncOpMemoSingle<T> {
- private n = 0;
- private memoEntry: MemoEntry<T> | undefined;
-
- private cleanUp(n: number): void {
- if (this.memoEntry && this.memoEntry.n === n) {
- this.memoEntry = undefined;
- }
- }
-
- memo(pg: () => Promise<T>): Promise<T> {
- const res = this.memoEntry;
- if (res) {
- return res.p;
- }
- const n = this.n++;
- // Wrap the operation in case it immediately throws
- const p = Promise.resolve().then(() => pg());
- p.finally(() => {
- this.cleanUp(n);
- });
- this.memoEntry = {
- p,
- n,
- t: new Date().getTime(),
- };
- return p;
- }
- clear(): void {
- this.memoEntry = undefined;
- }
-}
diff --git a/packages/taler-wallet-core/src/util/coinSelection.ts b/packages/taler-wallet-core/src/util/coinSelection.ts
deleted file mode 100644
index cadf8d829..000000000
--- a/packages/taler-wallet-core/src/util/coinSelection.ts
+++ /dev/null
@@ -1,192 +0,0 @@
-/*
- 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/>
- */
-
-/**
- * Selection of coins for payments.
- *
- * @author Florian Dold
- */
-
-/**
- * Imports.
- */
-import {
- AgeCommitmentProof,
- AmountJson,
- Amounts,
- DenominationPubKey,
- Logger,
-} from "@gnu-taler/taler-util";
-
-const logger = new Logger("coinSelection.ts");
-
-/**
- * Structure to describe a coin that is available to be
- * used in a payment.
- */
-export interface AvailableCoinInfo {
- /**
- * Public key of the coin.
- */
- coinPub: string;
-
- /**
- * Coin's denomination public key.
- *
- * FIXME: We should only need the denomPubHash here, if at all.
- */
- denomPub: DenominationPubKey;
-
- /**
- * Full value of the coin.
- */
- value: AmountJson;
-
- /**
- * Amount still remaining (typically the full amount,
- * as coins are always refreshed after use.)
- */
- availableAmount: AmountJson;
-
- /**
- * Deposit fee for the coin.
- */
- feeDeposit: AmountJson;
-
- exchangeBaseUrl: string;
-
- maxAge: number;
- ageCommitmentProof?: AgeCommitmentProof;
-}
-
-export type PreviousPayCoins = {
- coinPub: string;
- contribution: AmountJson;
- feeDeposit: AmountJson;
- exchangeBaseUrl: string;
-}[];
-
-export interface CoinCandidateSelection {
- candidateCoins: AvailableCoinInfo[];
- wireFeesPerExchange: Record<string, AmountJson>;
-}
-
-export interface SelectPayCoinRequest {
- candidates: CoinCandidateSelection;
- contractTermsAmount: AmountJson;
- depositFeeLimit: AmountJson;
- wireFeeLimit: AmountJson;
- wireFeeAmortization: number;
- prevPayCoins?: PreviousPayCoins;
- requiredMinimumAge?: number;
-}
-
-export interface CoinSelectionTally {
- /**
- * Amount that still needs to be paid.
- * May increase during the computation when fees need to be covered.
- */
- amountPayRemaining: AmountJson;
-
- /**
- * Allowance given by the merchant towards wire fees
- */
- amountWireFeeLimitRemaining: AmountJson;
-
- /**
- * Allowance given by the merchant towards deposit fees
- * (and wire fees after wire fee limit is exhausted)
- */
- amountDepositFeeLimitRemaining: AmountJson;
-
- customerDepositFees: AmountJson;
-
- customerWireFees: AmountJson;
-
- wireFeeCoveredForExchange: Set<string>;
-}
-
-/**
- * Account for the fees of spending a coin.
- */
-export function tallyFees(
- tally: CoinSelectionTally,
- wireFeesPerExchange: Record<string, AmountJson>,
- wireFeeAmortization: number,
- exchangeBaseUrl: string,
- feeDeposit: AmountJson,
-): CoinSelectionTally {
- const currency = tally.amountPayRemaining.currency;
- let amountWireFeeLimitRemaining = tally.amountWireFeeLimitRemaining;
- let amountDepositFeeLimitRemaining = tally.amountDepositFeeLimitRemaining;
- let customerDepositFees = tally.customerDepositFees;
- let customerWireFees = tally.customerWireFees;
- let amountPayRemaining = tally.amountPayRemaining;
- const wireFeeCoveredForExchange = new Set(tally.wireFeeCoveredForExchange);
-
- if (!tally.wireFeeCoveredForExchange.has(exchangeBaseUrl)) {
- const wf =
- wireFeesPerExchange[exchangeBaseUrl] ?? Amounts.zeroOfCurrency(currency);
- const wfForgiven = Amounts.min(amountWireFeeLimitRemaining, wf);
- amountWireFeeLimitRemaining = Amounts.sub(
- amountWireFeeLimitRemaining,
- wfForgiven,
- ).amount;
- // The remaining, amortized amount needs to be paid by the
- // wallet or covered by the deposit fee allowance.
- let wfRemaining = Amounts.divide(
- Amounts.sub(wf, wfForgiven).amount,
- wireFeeAmortization,
- );
-
- // This is the amount forgiven via the deposit fee allowance.
- const wfDepositForgiven = Amounts.min(
- amountDepositFeeLimitRemaining,
- wfRemaining,
- );
- amountDepositFeeLimitRemaining = Amounts.sub(
- amountDepositFeeLimitRemaining,
- wfDepositForgiven,
- ).amount;
-
- wfRemaining = Amounts.sub(wfRemaining, wfDepositForgiven).amount;
- customerWireFees = Amounts.add(customerWireFees, wfRemaining).amount;
- amountPayRemaining = Amounts.add(amountPayRemaining, wfRemaining).amount;
-
- wireFeeCoveredForExchange.add(exchangeBaseUrl);
- }
-
- const dfForgiven = Amounts.min(feeDeposit, amountDepositFeeLimitRemaining);
-
- amountDepositFeeLimitRemaining = Amounts.sub(
- amountDepositFeeLimitRemaining,
- dfForgiven,
- ).amount;
-
- // How much does the user spend on deposit fees for this coin?
- const dfRemaining = Amounts.sub(feeDeposit, dfForgiven).amount;
- customerDepositFees = Amounts.add(customerDepositFees, dfRemaining).amount;
- amountPayRemaining = Amounts.add(amountPayRemaining, dfRemaining).amount;
-
- return {
- amountDepositFeeLimitRemaining,
- amountPayRemaining,
- amountWireFeeLimitRemaining,
- customerDepositFees,
- customerWireFees,
- wireFeeCoveredForExchange,
- };
-}
diff --git a/packages/taler-wallet-core/src/util/promiseUtils.ts b/packages/taler-wallet-core/src/util/promiseUtils.ts
deleted file mode 100644
index d409686d9..000000000
--- a/packages/taler-wallet-core/src/util/promiseUtils.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2019 GNUnet e.V.
-
- TALER is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-export interface OpenedPromise<T> {
- promise: Promise<T>;
- resolve: (val: T) => void;
- reject: (err: any) => void;
-}
-
-/**
- * Get an unresolved promise together with its extracted resolve / reject
- * function.
- */
-export function openPromise<T>(): OpenedPromise<T> {
- let resolve: ((x?: any) => void) | null = null;
- let reject: ((reason?: any) => void) | null = null;
- const promise = new Promise<T>((res, rej) => {
- resolve = res;
- reject = rej;
- });
- if (!(resolve && reject)) {
- // Never happens, unless JS implementation is broken
- throw Error();
- }
- return { resolve, reject, promise };
-}
-
-export class AsyncCondition {
- private _waitPromise: Promise<void>;
- private _resolveWaitPromise: (val: void) => void;
- constructor() {
- const op = openPromise<void>();
- this._waitPromise = op.promise;
- this._resolveWaitPromise = op.resolve;
- }
-
- wait(): Promise<void> {
- return this._waitPromise;
- }
-
- trigger(): void {
- this._resolveWaitPromise();
- const op = openPromise<void>();
- this._waitPromise = op.promise;
- this._resolveWaitPromise = op.resolve;
- }
-}
diff --git a/packages/taler-wallet-core/src/util/retries.ts b/packages/taler-wallet-core/src/util/retries.ts
deleted file mode 100644
index 8861d4d1e..000000000
--- a/packages/taler-wallet-core/src/util/retries.ts
+++ /dev/null
@@ -1,263 +0,0 @@
-/*
- 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/>
- */
-
-/**
- * Helpers for dealing with retry timeouts.
- */
-
-/**
- * Imports.
- */
-import {
- AbsoluteTime,
- Duration,
- TalerErrorDetail,
-} from "@gnu-taler/taler-util";
-import {
- BackupProviderRecord,
- DepositGroupRecord,
- ExchangeRecord,
- PurchaseRecord,
- RecoupGroupRecord,
- RefreshGroupRecord,
- TipRecord,
- WalletStoresV1,
- WithdrawalGroupRecord,
-} from "../db.js";
-import { TalerError } from "../errors.js";
-import { InternalWalletState } from "../internal-wallet-state.js";
-import { PendingTaskType } from "../pending-types.js";
-import { GetReadWriteAccess } from "./query.js";
-
-export enum OperationAttemptResultType {
- Finished = "finished",
- Pending = "pending",
- Error = "error",
- Longpoll = "longpoll",
-}
-
-export type OperationAttemptResult<TSuccess = unknown, TPending = unknown> =
- | OperationAttemptFinishedResult<TSuccess>
- | OperationAttemptErrorResult
- | OperationAttemptLongpollResult
- | OperationAttemptPendingResult<TPending>;
-
-export namespace OperationAttemptResult {
- export function finishedEmpty(): OperationAttemptResult<unknown, unknown> {
- return {
- type: OperationAttemptResultType.Finished,
- result: undefined,
- };
- }
-}
-
-export interface OperationAttemptFinishedResult<T> {
- type: OperationAttemptResultType.Finished;
- result: T;
-}
-
-export interface OperationAttemptPendingResult<T> {
- type: OperationAttemptResultType.Pending;
- result: T;
-}
-
-export interface OperationAttemptErrorResult {
- type: OperationAttemptResultType.Error;
- errorDetail: TalerErrorDetail;
-}
-
-export interface OperationAttemptLongpollResult {
- type: OperationAttemptResultType.Longpoll;
-}
-
-export interface RetryInfo {
- firstTry: AbsoluteTime;
- nextRetry: AbsoluteTime;
- retryCounter: number;
-}
-
-export interface RetryPolicy {
- readonly backoffDelta: Duration;
- readonly backoffBase: number;
- readonly maxTimeout: Duration;
-}
-
-const defaultRetryPolicy: RetryPolicy = {
- backoffBase: 1.5,
- backoffDelta: Duration.fromSpec({ seconds: 1 }),
- maxTimeout: Duration.fromSpec({ minutes: 2 }),
-};
-
-function updateTimeout(
- r: RetryInfo,
- p: RetryPolicy = defaultRetryPolicy,
-): void {
- const now = AbsoluteTime.now();
- if (now.t_ms === "never") {
- throw Error("assertion failed");
- }
- if (p.backoffDelta.d_ms === "forever") {
- r.nextRetry = { t_ms: "never" };
- return;
- }
-
- const nextIncrement =
- p.backoffDelta.d_ms * Math.pow(p.backoffBase, r.retryCounter);
-
- const t =
- now.t_ms +
- (p.maxTimeout.d_ms === "forever"
- ? nextIncrement
- : Math.min(p.maxTimeout.d_ms, nextIncrement));
- r.nextRetry = { t_ms: t };
-}
-
-export namespace RetryInfo {
- export function getDuration(
- r: RetryInfo | undefined,
- p: RetryPolicy = defaultRetryPolicy,
- ): Duration {
- if (!r) {
- // If we don't have any retry info, run immediately.
- return { d_ms: 0 };
- }
- if (p.backoffDelta.d_ms === "forever") {
- return { d_ms: "forever" };
- }
- const t = p.backoffDelta.d_ms * Math.pow(p.backoffBase, r.retryCounter);
- return {
- d_ms:
- p.maxTimeout.d_ms === "forever" ? t : Math.min(p.maxTimeout.d_ms, t),
- };
- }
-
- export function reset(p: RetryPolicy = defaultRetryPolicy): RetryInfo {
- const now = AbsoluteTime.now();
- const info = {
- firstTry: now,
- nextRetry: now,
- retryCounter: 0,
- };
- updateTimeout(info, p);
- return info;
- }
-
- export function increment(
- r: RetryInfo | undefined,
- p: RetryPolicy = defaultRetryPolicy,
- ): RetryInfo {
- if (!r) {
- return reset(p);
- }
- const r2 = { ...r };
- r2.retryCounter++;
- updateTimeout(r2, p);
- return r2;
- }
-}
-
-export namespace RetryTags {
- export function forWithdrawal(wg: WithdrawalGroupRecord): string {
- return `${PendingTaskType.Withdraw}:${wg.withdrawalGroupId}`;
- }
- export function forExchangeUpdate(exch: ExchangeRecord): string {
- return `${PendingTaskType.ExchangeUpdate}:${exch.baseUrl}`;
- }
- export function forExchangeUpdateFromUrl(exchBaseUrl: string): string {
- return `${PendingTaskType.ExchangeUpdate}:${exchBaseUrl}`;
- }
- export function forExchangeCheckRefresh(exch: ExchangeRecord): string {
- return `${PendingTaskType.ExchangeCheckRefresh}:${exch.baseUrl}`;
- }
- export function forTipPickup(tipRecord: TipRecord): string {
- return `${PendingTaskType.TipPickup}:${tipRecord.walletTipId}`;
- }
- export function forRefresh(refreshGroupRecord: RefreshGroupRecord): string {
- return `${PendingTaskType.Refresh}:${refreshGroupRecord.refreshGroupId}`;
- }
- export function forPay(purchaseRecord: PurchaseRecord): string {
- return `${PendingTaskType.Purchase}:${purchaseRecord.proposalId}`;
- }
- export function forRecoup(recoupRecord: RecoupGroupRecord): string {
- return `${PendingTaskType.Recoup}:${recoupRecord.recoupGroupId}`;
- }
- export function forDeposit(depositRecord: DepositGroupRecord): string {
- return `${PendingTaskType.Deposit}:${depositRecord.depositGroupId}`;
- }
- export function forBackup(backupRecord: BackupProviderRecord): string {
- return `${PendingTaskType.Backup}:${backupRecord.baseUrl}`;
- }
- export function byPaymentProposalId(proposalId: string): string {
- return `${PendingTaskType.Purchase}:${proposalId}`;
- }
-}
-
-export async function scheduleRetryInTx(
- ws: InternalWalletState,
- tx: GetReadWriteAccess<{
- operationRetries: typeof WalletStoresV1.operationRetries;
- }>,
- opId: string,
- errorDetail?: TalerErrorDetail,
-): Promise<void> {
- let retryRecord = await tx.operationRetries.get(opId);
- if (!retryRecord) {
- retryRecord = {
- id: opId,
- retryInfo: RetryInfo.reset(),
- };
- if (errorDetail) {
- retryRecord.lastError = errorDetail;
- }
- } else {
- retryRecord.retryInfo = RetryInfo.increment(retryRecord.retryInfo);
- if (errorDetail) {
- retryRecord.lastError = errorDetail;
- } else {
- delete retryRecord.lastError;
- }
- }
- await tx.operationRetries.put(retryRecord);
-}
-
-export async function scheduleRetry(
- ws: InternalWalletState,
- opId: string,
- errorDetail?: TalerErrorDetail,
-): Promise<void> {
- return await ws.db
- .mktx((x) => [x.operationRetries])
- .runReadWrite(async (tx) => {
- tx.operationRetries;
- scheduleRetryInTx(ws, tx, opId, errorDetail);
- });
-}
-
-/**
- * Run an operation handler, expect a success result and extract the success value.
- */
-export async function unwrapOperationHandlerResultOrThrow<T>(
- res: OperationAttemptResult<T>,
-): Promise<T> {
- switch (res.type) {
- case OperationAttemptResultType.Finished:
- return res.result;
- case OperationAttemptResultType.Error:
- throw TalerError.fromUncheckedDetail(res.errorDetail);
- default:
- throw Error(`unexpected operation result (${res.type})`);
- }
-}
diff --git a/packages/taler-wallet-core/src/versions.ts b/packages/taler-wallet-core/src/versions.ts
index c3bc142f0..ad58a66ec 100644
--- a/packages/taler-wallet-core/src/versions.ts
+++ b/packages/taler-wallet-core/src/versions.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2019 Taler Systems S.A.
+ (C) 2019-2023 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -19,18 +19,66 @@
*
* Uses libtool's current:revision:age versioning.
*/
-export const WALLET_EXCHANGE_PROTOCOL_VERSION = "12:0:0";
+export const WALLET_EXCHANGE_PROTOCOL_VERSION = "17:0:0";
/**
* Protocol version spoken with the merchant.
*
* Uses libtool's current:revision:age versioning.
*/
-export const WALLET_MERCHANT_PROTOCOL_VERSION = "2:0:1";
+export const WALLET_MERCHANT_PROTOCOL_VERSION = "5:0:1";
/**
- * Protocol version spoken with the merchant.
+ * 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.
+ */
+export const WALLET_COREBANK_API_PROTOCOL_VERSION = "2:0:0";
+
+/**
+ * Protocol version spoken with the bank (conversion API).
*
* Uses libtool's current:revision:age versioning.
*/
-export const WALLET_BANK_INTEGRATION_PROTOCOL_VERSION = "0:0:0";
+export const WALLET_BANK_CONVERSION_API_PROTOCOL_VERSION = "2:0:0";
+
+/**
+ * Libtool version of the wallet-core API.
+ */
+export const WALLET_CORE_API_PROTOCOL_VERSION = "4:0:0";
+
+/**
+ * Libtool rules:
+ *
+ * If the library source code has changed at all since the last update,
+ * then increment revision (‘c:r:a’ becomes ‘c:r+1:a’).
+ * If any interfaces have been added, removed, or changed since the last
+ * update, increment current, and set revision to 0.
+ * If any interfaces have been added since the last public release, then
+ * increment age.
+ * If any interfaces have been removed or changed since the last public
+ * release, then set age to 0.
+ */
+
+// Provided either by bundler or in the next lines.
+declare global {
+ const walletCoreBuildInfo: {
+ implementationSemver: string;
+ implementationGitHash: string;
+ };
+}
+
+// Provide walletCoreBuildInfo if the bundler does not override it.
+if (!("walletCoreBuildInfo" in globalThis)) {
+ (globalThis as any).walletCoreBuildInfo = {
+ implementationSemver: "unknown",
+ implementationGitHash: "unknown",
+ } satisfies typeof walletCoreBuildInfo;
+}
diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts
index f4fb16e80..ba28c009a 100644
--- a/packages/taler-wallet-core/src/wallet-api-types.ts
+++ b/packages/taler-wallet-core/src/wallet-api-types.ts
@@ -24,112 +24,154 @@
* Imports.
*/
import {
- AbortPayWithRefundRequest,
+ AbortTransactionRequest,
AcceptBankIntegratedWithdrawalRequest,
AcceptExchangeTosRequest,
AcceptManualWithdrawalRequest,
AcceptManualWithdrawalResult,
- AcceptPeerPullPaymentRequest,
- AcceptPeerPushPaymentRequest,
- AcceptTipRequest,
AcceptWithdrawalResponse,
AddExchangeRequest,
+ AddGlobalCurrencyAuditorRequest,
+ AddGlobalCurrencyExchangeRequest,
AddKnownBankAccountsRequest,
+ AmountResponse,
ApplyDevExperimentRequest,
- ApplyRefundFromPurchaseIdRequest,
- ApplyRefundRequest,
- ApplyRefundResponse,
BackupRecovery,
BalancesResponse,
- CheckPeerPullPaymentRequest,
- CheckPeerPullPaymentResponse,
- CheckPeerPushPaymentRequest,
- CheckPeerPushPaymentResponse,
+ CheckPeerPullCreditRequest,
+ CheckPeerPullCreditResponse,
+ CheckPeerPushDebitRequest,
+ CheckPeerPushDebitResponse,
CoinDumpJson,
ConfirmPayRequest,
ConfirmPayResult,
+ ConfirmPeerPullDebitRequest,
+ ConfirmPeerPushCreditRequest,
+ ConvertAmountRequest,
CreateDepositGroupRequest,
CreateDepositGroupResponse,
+ CreateStoredBackupResponse,
+ DeleteExchangeRequest,
+ DeleteStoredBackupRequest,
DeleteTransactionRequest,
- DepositGroupFees,
ExchangeDetailedResponse,
ExchangesListResponse,
+ ExchangesShortListResponse,
+ FailTransactionRequest,
ForceRefreshRequest,
ForgetKnownBankAccountsRequest,
+ GetActiveTasksResponse,
+ GetAmountRequest,
+ GetBalanceDetailRequest,
GetContractTermsDetailsRequest,
+ GetCurrencySpecificationRequest,
+ GetCurrencySpecificationResponse,
+ GetExchangeEntryByUrlRequest,
+ GetExchangeEntryByUrlResponse,
+ GetExchangeResourcesRequest,
+ GetExchangeResourcesResponse,
GetExchangeTosRequest,
GetExchangeTosResult,
- GetFeeForDepositRequest,
+ GetPlanForOperationRequest,
+ GetPlanForOperationResponse,
GetWithdrawalDetailsForAmountRequest,
GetWithdrawalDetailsForUriRequest,
- InitiatePeerPullPaymentRequest,
- InitiatePeerPullPaymentResponse,
- InitiatePeerPushPaymentRequest,
- InitiatePeerPushPaymentResponse,
+ ImportDbRequest,
+ InitRequest,
InitResponse,
+ InitiatePeerPullCreditRequest,
+ InitiatePeerPullCreditResponse,
+ InitiatePeerPushDebitRequest,
+ InitiatePeerPushDebitResponse,
IntegrationTestArgs,
KnownBankAccounts,
+ ListAssociatedRefreshesRequest,
+ ListAssociatedRefreshesResponse,
+ ListExchangesForScopedCurrencyRequest,
+ ListGlobalCurrencyAuditorsResponse,
+ ListGlobalCurrencyExchangesResponse,
ListKnownBankAccountsRequest,
- ManualWithdrawalDetails,
- UserAttentionsCountResponse,
- UserAttentionsRequest,
- UserAttentionsResponse,
PrepareDepositRequest,
PrepareDepositResponse,
PreparePayRequest,
PreparePayResult,
- PreparePeerPullPaymentRequest,
- PreparePeerPullPaymentResponse,
- PreparePeerPushPaymentRequest,
- PreparePeerPushPaymentResponse,
+ PreparePayTemplateRequest,
+ PreparePeerPullDebitRequest,
+ PreparePeerPullDebitResponse,
+ PreparePeerPushCreditRequest,
+ PreparePeerPushCreditResponse,
PrepareRefundRequest,
- PrepareRefundResult,
- PrepareTipRequest,
- PrepareTipResult,
+ PrepareWithdrawExchangeRequest,
+ PrepareWithdrawExchangeResponse,
+ RecoverStoredBackupRequest,
RecoveryLoadRequest,
+ RemoveGlobalCurrencyAuditorRequest,
+ RemoveGlobalCurrencyExchangeRequest,
RetryTransactionRequest,
SetCoinSuspendedRequest,
- SetDevModeRequest,
SetWalletDeviceIdRequest,
+ SharePaymentRequest,
+ SharePaymentResult,
+ StartRefundQueryForUriResponse,
+ StartRefundQueryRequest,
+ StoredBackupList,
TestPayArgs,
TestPayResult,
- TrackDepositGroupRequest,
- TrackDepositGroupResponse,
+ TestingGetDenomStatsRequest,
+ TestingGetDenomStatsResponse,
+ TestingListTasksForTransactionRequest,
+ TestingListTasksForTransactionsResponse,
+ TestingSetTimetravelRequest,
+ TestingWaitTransactionRequest,
Transaction,
TransactionByIdRequest,
+ TransactionWithdrawal,
TransactionsRequest,
TransactionsResponse,
- WalletBackupContentV1,
+ TxIdResponse,
+ UpdateExchangeEntryRequest,
+ UserAttentionByIdRequest,
+ UserAttentionsCountResponse,
+ UserAttentionsRequest,
+ UserAttentionsResponse,
+ ValidateIbanRequest,
+ ValidateIbanResponse,
+ WalletContractData,
WalletCoreVersion,
- WalletCurrencyInfo,
- WithdrawFakebankRequest,
WithdrawTestBalanceRequest,
WithdrawUriInfoResponse,
- UserAttentionByIdRequest,
+ WithdrawalDetailsForAmount,
+ WithdrawalTransactionByURIRequest,
} from "@gnu-taler/taler-util";
-import { WalletContractData } from "./db.js";
import {
AddBackupProviderRequest,
AddBackupProviderResponse,
BackupInfo,
RemoveBackupProviderRequest,
RunBackupCycleRequest,
-} from "./operations/backup/index.js";
-import { PendingOperationsResponse as PendingTasksResponse } from "./pending-types.js";
+} from "./backup/index.js";
+import { PaymentBalanceDetails } from "./balance.js";
export enum WalletApiOperation {
InitWallet = "initWallet",
+ SetWalletRunConfig = "setWalletRunConfig",
WithdrawTestkudos = "withdrawTestkudos",
WithdrawTestBalance = "withdrawTestBalance",
PreparePayForUri = "preparePayForUri",
+ SharePayment = "sharePayment",
+ PreparePayForTemplate = "preparePayForTemplate",
GetContractTermsDetails = "getContractTermsDetails",
RunIntegrationTest = "runIntegrationTest",
+ RunIntegrationTestV2 = "runIntegrationTestV2",
TestCrypto = "testCrypto",
TestPay = "testPay",
AddExchange = "addExchange",
GetTransactions = "getTransactions",
GetTransactionById = "getTransactionById",
+ GetWithdrawalTransactionByUri = "getWithdrawalTransactionByUri",
+ TestingGetSampleTransactions = "testingGetSampleTransactions",
ListExchanges = "listExchanges",
+ GetExchangeEntryByUrl = "getExchangeEntryByUrl",
ListKnownBankAccounts = "listKnownBankAccounts",
AddKnownBankAccounts = "addKnownBankAccounts",
ForgetKnownBankAccounts = "forgetKnownBankAccounts",
@@ -137,25 +179,36 @@ export enum WalletApiOperation {
GetWithdrawalDetailsForAmount = "getWithdrawalDetailsForAmount",
AcceptManualWithdrawal = "acceptManualWithdrawal",
GetBalances = "getBalances",
+ GetBalanceDetail = "getBalanceDetail",
+ GetPlanForOperation = "getPlanForOperation",
+ ConvertDepositAmount = "ConvertDepositAmount",
+ GetMaxDepositAmount = "GetMaxDepositAmount",
+ ConvertPeerPushAmount = "ConvertPeerPushAmount",
+ GetMaxPeerPushAmount = "GetMaxPeerPushAmount",
+ ConvertWithdrawalAmount = "ConvertWithdrawalAmount",
GetUserAttentionRequests = "getUserAttentionRequests",
GetUserAttentionUnreadCount = "getUserAttentionUnreadCount",
MarkAttentionRequestAsRead = "markAttentionRequestAsRead",
GetPendingOperations = "getPendingOperations",
+ GetActiveTasks = "getActiveTasks",
SetExchangeTosAccepted = "setExchangeTosAccepted",
- ApplyRefund = "applyRefund",
- ApplyRefundFromPurchaseId = "applyRefundFromPurchaseId",
- PrepareRefund = "prepareRefund",
+ SetExchangeTosForgotten = "SetExchangeTosForgotten",
+ StartRefundQueryForUri = "startRefundQueryForUri",
+ StartRefundQuery = "startRefundQuery",
AcceptBankIntegratedWithdrawal = "acceptBankIntegratedWithdrawal",
GetExchangeTos = "getExchangeTos",
GetExchangeDetailedInfo = "getExchangeDetailedInfo",
RetryPendingNow = "retryPendingNow",
- AbortFailedPayWithRefund = "abortFailedPayWithRefund",
+ AbortTransaction = "abortTransaction",
+ FailTransaction = "failTransaction",
+ SuspendTransaction = "suspendTransaction",
+ ResumeTransaction = "resumeTransaction",
+ DeleteTransaction = "deleteTransaction",
+ RetryTransaction = "retryTransaction",
ConfirmPay = "confirmPay",
DumpCoins = "dumpCoins",
SetCoinSuspended = "setCoinSuspended",
ForceRefresh = "forceRefresh",
- PrepareTip = "prepareTip",
- AcceptTip = "acceptTip",
ExportBackup = "exportBackup",
AddBackupProvider = "addBackupProvider",
RemoveBackupProvider = "removeBackupProvider",
@@ -163,44 +216,76 @@ export enum WalletApiOperation {
ExportBackupRecovery = "exportBackupRecovery",
ImportBackupRecovery = "importBackupRecovery",
GetBackupInfo = "getBackupInfo",
- TrackDepositGroup = "trackDepositGroup",
- GetFeeForDeposit = "getFeeForDeposit",
PrepareDeposit = "prepareDeposit",
GetVersion = "getVersion",
- DeleteTransaction = "deleteTransaction",
- RetryTransaction = "retryTransaction",
- ListCurrencies = "listCurrencies",
+ GenerateDepositGroupTxId = "generateDepositGroupTxId",
CreateDepositGroup = "createDepositGroup",
SetWalletDeviceId = "setWalletDeviceId",
- ExportBackupPlain = "exportBackupPlain",
- WithdrawFakebank = "withdrawFakebank",
ImportDb = "importDb",
ExportDb = "exportDb",
- PreparePeerPushPayment = "preparePeerPushPayment",
- InitiatePeerPushPayment = "initiatePeerPushPayment",
- CheckPeerPushPayment = "checkPeerPushPayment",
- AcceptPeerPushPayment = "acceptPeerPushPayment",
- PreparePeerPullPayment = "preparePeerPullPayment",
- InitiatePeerPullPayment = "initiatePeerPullPayment",
- CheckPeerPullPayment = "checkPeerPullPayment",
- AcceptPeerPullPayment = "acceptPeerPullPayment",
+ PreparePeerPushCredit = "preparePeerPushCredit",
+ CheckPeerPushDebit = "checkPeerPushDebit",
+ InitiatePeerPushDebit = "initiatePeerPushDebit",
+ ConfirmPeerPushCredit = "confirmPeerPushCredit",
+ CheckPeerPullCredit = "checkPeerPullCredit",
+ InitiatePeerPullCredit = "initiatePeerPullCredit",
+ PreparePeerPullDebit = "preparePeerPullDebit",
+ ConfirmPeerPullDebit = "confirmPeerPullDebit",
ClearDb = "clearDb",
Recycle = "recycle",
- SetDevMode = "setDevMode",
ApplyDevExperiment = "applyDevExperiment",
+ ValidateIban = "validateIban",
+ GetCurrencySpecification = "getCurrencySpecification",
+ ListStoredBackups = "listStoredBackups",
+ CreateStoredBackup = "createStoredBackup",
+ DeleteStoredBackup = "deleteStoredBackup",
+ RecoverStoredBackup = "recoverStoredBackup",
+ UpdateExchangeEntry = "updateExchangeEntry",
+ ListExchangesForScopedCurrency = "listExchangesForScopedCurrency",
+ PrepareWithdrawExchange = "prepareWithdrawExchange",
+ GetExchangeResources = "getExchangeResources",
+ DeleteExchange = "deleteExchange",
+ ListGlobalCurrencyExchanges = "listGlobalCurrencyExchanges",
+ ListGlobalCurrencyAuditors = "listGlobalCurrencyAuditors",
+ AddGlobalCurrencyExchange = "addGlobalCurrencyExchange",
+ RemoveGlobalCurrencyExchange = "removeGlobalCurrencyExchange",
+ AddGlobalCurrencyAuditor = "addGlobalCurrencyAuditor",
+ RemoveGlobalCurrencyAuditor = "removeGlobalCurrencyAuditor",
+ ListAssociatedRefreshes = "listAssociatedRefreshes",
+ TestingWaitTransactionsFinal = "testingWaitTransactionsFinal",
+ TestingWaitRefreshesFinal = "testingWaitRefreshesFinal",
+ TestingWaitTransactionState = "testingWaitTransactionState",
+ TestingWaitTasksDone = "testingWaitTasksDone",
+ TestingSetTimetravel = "testingSetTimetravel",
+ TestingInfiniteTransactionLoop = "testingInfiniteTransactionLoop",
+ TestingListTaskForTransaction = "testingListTasksForTransaction",
+ TestingGetDenomStats = "testingGetDenomStats",
+ TestingPing = "testingPing",
}
// group: Initialization
type EmptyObject = Record<string, never>;
+
/**
* Initialize wallet-core.
*
- * Must be the request before any other operations.
+ * Must be the first request made to wallet-core.
*/
export type InitWalletOp = {
op: WalletApiOperation.InitWallet;
- request: EmptyObject;
+ request: InitRequest;
+ response: InitResponse;
+};
+
+/**
+ * Change the configuration of wallet-core.
+ *
+ * Currently an alias for the initWallet request.
+ */
+export type SetWalletRunConfigOp = {
+ op: WalletApiOperation.SetWalletRunConfig;
+ request: InitRequest;
response: InitResponse;
};
@@ -220,6 +305,43 @@ export type GetBalancesOp = {
request: EmptyObject;
response: BalancesResponse;
};
+export type GetBalancesDetailOp = {
+ op: WalletApiOperation.GetBalanceDetail;
+ request: GetBalanceDetailRequest;
+ response: PaymentBalanceDetails;
+};
+
+export type GetPlanForOperationOp = {
+ op: WalletApiOperation.GetPlanForOperation;
+ request: GetPlanForOperationRequest;
+ response: GetPlanForOperationResponse;
+};
+
+export type ConvertDepositAmountOp = {
+ op: WalletApiOperation.ConvertDepositAmount;
+ request: ConvertAmountRequest;
+ response: AmountResponse;
+};
+export type GetMaxDepositAmountOp = {
+ op: WalletApiOperation.GetMaxDepositAmount;
+ request: GetAmountRequest;
+ response: AmountResponse;
+};
+export type ConvertPeerPushAmountOp = {
+ op: WalletApiOperation.ConvertPeerPushAmount;
+ request: ConvertAmountRequest;
+ response: AmountResponse;
+};
+export type GetMaxPeerPushAmountOp = {
+ op: WalletApiOperation.GetMaxPeerPushAmount;
+ request: GetAmountRequest;
+ response: AmountResponse;
+};
+export type ConvertWithdrawalAmountOp = {
+ op: WalletApiOperation.ConvertWithdrawalAmount;
+ request: ConvertAmountRequest;
+ response: AmountResponse;
+};
// group: Managing Transactions
@@ -232,12 +354,36 @@ export type GetTransactionsOp = {
response: TransactionsResponse;
};
+/**
+ * List refresh transactions associated with another transaction.
+ */
+export type ListAssociatedRefreshesOp = {
+ op: WalletApiOperation.ListAssociatedRefreshes;
+ request: ListAssociatedRefreshesRequest;
+ response: ListAssociatedRefreshesResponse;
+};
+
+/**
+ * Get sample transactions.
+ */
+export type TestingGetSampleTransactionsOp = {
+ op: WalletApiOperation.TestingGetSampleTransactions;
+ request: EmptyObject;
+ response: TransactionsResponse;
+};
+
export type GetTransactionByIdOp = {
op: WalletApiOperation.GetTransactionById;
request: TransactionByIdRequest;
response: Transaction;
};
+export type GetWithdrawalTransactionByUriOp = {
+ op: WalletApiOperation.GetWithdrawalTransactionByUri;
+ request: WithdrawalTransactionByURIRequest;
+ response: TransactionWithdrawal | undefined;
+};
+
export type RetryPendingNowOp = {
op: WalletApiOperation.RetryPendingNow;
request: EmptyObject;
@@ -262,6 +408,46 @@ export type RetryTransactionOp = {
response: EmptyObject;
};
+/**
+ * Abort a transaction
+ *
+ * For payment transactions, it puts the payment into an "aborting" state.
+ */
+export type AbortTransactionOp = {
+ op: WalletApiOperation.AbortTransaction;
+ request: AbortTransactionRequest;
+ response: EmptyObject;
+};
+
+/**
+ * Cancel aborting a transaction
+ *
+ * For payment transactions, it puts the payment into an "aborting" state.
+ */
+export type FailTransactionOp = {
+ op: WalletApiOperation.FailTransaction;
+ request: FailTransactionRequest;
+ response: EmptyObject;
+};
+
+/**
+ * Suspend a transaction
+ */
+export type SuspendTransactionOp = {
+ op: WalletApiOperation.SuspendTransaction;
+ request: AbortTransactionRequest;
+ response: EmptyObject;
+};
+
+/**
+ * Resume a transaction
+ */
+export type ResumeTransactionOp = {
+ op: WalletApiOperation.ResumeTransaction;
+ request: AbortTransactionRequest;
+ response: EmptyObject;
+};
+
// group: Withdrawals
/**
@@ -270,7 +456,7 @@ export type RetryTransactionOp = {
export type GetWithdrawalDetailsForAmountOp = {
op: WalletApiOperation.GetWithdrawalDetailsForAmount;
request: GetWithdrawalDetailsForAmountRequest;
- response: ManualWithdrawalDetails;
+ response: WithdrawalDetailsForAmount;
};
/**
@@ -303,7 +489,7 @@ export type AcceptManualWithdrawalOp = {
// group: Merchant Payments
/**
- * Prepare to make a payment
+ * Prepare to make a payment based on a taler://pay/ URI.
*/
export type PreparePayForUriOp = {
op: WalletApiOperation.PreparePayForUri;
@@ -311,6 +497,21 @@ export type PreparePayForUriOp = {
response: PreparePayResult;
};
+export type SharePaymentOp = {
+ op: WalletApiOperation.SharePayment;
+ request: SharePaymentRequest;
+ response: SharePaymentResult;
+};
+
+/**
+ * Prepare to make a payment based on a taler://pay-template/ URI.
+ */
+export type PreparePayForTemplateOp = {
+ op: WalletApiOperation.PreparePayForTemplate;
+ request: PreparePayTemplateRequest;
+ response: PreparePayResult;
+};
+
export type GetContractTermsDetailsOp = {
op: WalletApiOperation.GetContractTermsDetails;
request: GetContractTermsDetailsRequest;
@@ -328,52 +529,55 @@ export type ConfirmPayOp = {
};
/**
- * Abort a pending payment with a refund.
+ * Check for a refund based on a taler://refund URI.
*/
-export type AbortPayWithRefundOp = {
- op: WalletApiOperation.AbortFailedPayWithRefund;
- request: AbortPayWithRefundRequest;
+export type StartRefundQueryForUriOp = {
+ op: WalletApiOperation.StartRefundQueryForUri;
+ request: PrepareRefundRequest;
+ response: StartRefundQueryForUriResponse;
+};
+
+export type StartRefundQueryOp = {
+ op: WalletApiOperation.StartRefundQuery;
+ request: StartRefundQueryRequest;
response: EmptyObject;
};
-/**
- * Check for a refund based on a taler://refund URI.
- */
-export type ApplyRefundOp = {
- op: WalletApiOperation.ApplyRefund;
- request: ApplyRefundRequest;
- response: ApplyRefundResponse;
+// group: Global Currency management
+
+export type ListGlobalCurrencyAuditorsOp = {
+ op: WalletApiOperation.ListGlobalCurrencyAuditors;
+ request: EmptyObject;
+ response: ListGlobalCurrencyAuditorsResponse;
};
-export type ApplyRefundFromPurchaseIdOp = {
- op: WalletApiOperation.ApplyRefundFromPurchaseId;
- request: ApplyRefundFromPurchaseIdRequest;
- response: ApplyRefundResponse;
+export type ListGlobalCurrencyExchangesOp = {
+ op: WalletApiOperation.ListGlobalCurrencyExchanges;
+ request: EmptyObject;
+ response: ListGlobalCurrencyExchangesResponse;
};
-export type PrepareRefundOp = {
- op: WalletApiOperation.PrepareRefund;
- request: PrepareRefundRequest;
- response: PrepareRefundResult;
+export type AddGlobalCurrencyExchangeOp = {
+ op: WalletApiOperation.AddGlobalCurrencyExchange;
+ request: AddGlobalCurrencyExchangeRequest;
+ response: EmptyObject;
};
-// group: Tipping
+export type AddGlobalCurrencyAuditorOp = {
+ op: WalletApiOperation.AddGlobalCurrencyAuditor;
+ request: AddGlobalCurrencyAuditorRequest;
+ response: EmptyObject;
+};
-/**
- * Query and store information about a tip.
- */
-export type PrepareTipOp = {
- op: WalletApiOperation.PrepareTip;
- request: PrepareTipRequest;
- response: PrepareTipResult;
+export type RemoveGlobalCurrencyExchangeOp = {
+ op: WalletApiOperation.RemoveGlobalCurrencyExchange;
+ request: RemoveGlobalCurrencyExchangeRequest;
+ response: EmptyObject;
};
-/**
- * Accept a tip.
- */
-export type AcceptTipOp = {
- op: WalletApiOperation.AcceptTip;
- request: AcceptTipRequest;
+export type RemoveGlobalCurrencyAuditorOp = {
+ op: WalletApiOperation.RemoveGlobalCurrencyAuditor;
+ request: RemoveGlobalCurrencyAuditorRequest;
response: EmptyObject;
};
@@ -389,6 +593,25 @@ export type ListExchangesOp = {
};
/**
+ * List exchanges that are available for withdrawing a particular
+ * scoped currency.
+ */
+export type ListExchangesForScopedCurrencyOp = {
+ op: WalletApiOperation.ListExchangesForScopedCurrency;
+ request: ListExchangesForScopedCurrencyRequest;
+ response: ExchangesShortListResponse;
+};
+
+/**
+ * Prepare for withdrawing via a taler://withdraw-exchange URI.
+ */
+export type PrepareWithdrawExchangeOp = {
+ op: WalletApiOperation.PrepareWithdrawExchange;
+ request: PrepareWithdrawExchangeRequest;
+ response: PrepareWithdrawExchangeResponse;
+};
+
+/**
* Add / force-update an exchange.
*/
export type AddExchangeOp = {
@@ -397,6 +620,15 @@ export type AddExchangeOp = {
response: EmptyObject;
};
+/**
+ * Update an exchange entry.
+ */
+export type UpdateExchangeEntryOp = {
+ op: WalletApiOperation.UpdateExchangeEntry;
+ request: UpdateExchangeEntryRequest;
+ response: EmptyObject;
+};
+
export type ListKnownBankAccountsOp = {
op: WalletApiOperation.ListKnownBankAccounts;
request: ListKnownBankAccountsRequest;
@@ -425,6 +657,15 @@ export type SetExchangeTosAcceptedOp = {
};
/**
+ * Accept a particular version of the exchange terms of service.
+ */
+export type SetExchangeTosForgottenOp = {
+ op: WalletApiOperation.SetExchangeTosForgotten;
+ request: AcceptExchangeTosRequest;
+ response: EmptyObject;
+};
+
+/**
* Get the current terms of a service of an exchange.
*/
export type GetExchangeTosOp = {
@@ -443,17 +684,54 @@ export type GetExchangeDetailedInfoOp = {
};
/**
- * List currencies known to the wallet.
+ * Get the current terms of a service of an exchange.
*/
-export type ListCurrenciesOp = {
- op: WalletApiOperation.ListCurrencies;
- request: EmptyObject;
- response: WalletCurrencyInfo;
+export type GetExchangeEntryByUrlOp = {
+ op: WalletApiOperation.GetExchangeEntryByUrl;
+ request: GetExchangeEntryByUrlRequest;
+ response: GetExchangeEntryByUrlResponse;
+};
+
+/**
+ * Get resources associated with an exchange.
+ */
+export type GetExchangeResourcesOp = {
+ op: WalletApiOperation.GetExchangeResources;
+ request: GetExchangeResourcesRequest;
+ response: GetExchangeResourcesResponse;
+};
+
+/**
+ * Get resources associated with an exchange.
+ */
+export type DeleteExchangeOp = {
+ op: WalletApiOperation.GetExchangeResources;
+ request: DeleteExchangeRequest;
+ response: EmptyObject;
+};
+
+export type GetCurrencySpecificationOp = {
+ op: WalletApiOperation.GetCurrencySpecification;
+ request: GetCurrencySpecificationRequest;
+ response: GetCurrencySpecificationResponse;
};
// group: Deposits
/**
+ * Generate a fresh transaction ID for a deposit group.
+ *
+ * The resulting transaction ID can be specified when creating
+ * a deposit group, so that the client can already start waiting for notifications
+ * on that specific deposit group before the GreateDepositGroup request returns.
+ */
+export type GenerateDepositGroupTxIdOp = {
+ op: WalletApiOperation.GenerateDepositGroupTxId;
+ request: EmptyObject;
+ response: TxIdResponse;
+};
+
+/**
* Create a new deposit group.
*
* Deposit groups are used to deposit multiple coins to a bank
@@ -465,21 +743,6 @@ export type CreateDepositGroupOp = {
response: CreateDepositGroupResponse;
};
-/**
- * Track the status of a deposit group by querying the exchange.
- */
-export type TrackDepositGroupOp = {
- op: WalletApiOperation.TrackDepositGroup;
- request: TrackDepositGroupRequest;
- response: TrackDepositGroupResponse;
-};
-
-export type GetFeeForDepositOp = {
- op: WalletApiOperation.GetFeeForDeposit;
- request: GetFeeForDepositRequest;
- response: DepositGroupFees;
-};
-
export type PrepareDepositOp = {
op: WalletApiOperation.PrepareDeposit;
request: PrepareDepositRequest;
@@ -556,89 +819,113 @@ export type SetWalletDeviceIdOp = {
response: EmptyObject;
};
-/**
- * Export a backup JSON, mostly useful for testing.
- */
-export type ExportBackupPlainOp = {
- op: WalletApiOperation.ExportBackupPlain;
+export type ListStoredBackupsOp = {
+ op: WalletApiOperation.ListStoredBackups;
request: EmptyObject;
- response: WalletBackupContentV1;
+ response: StoredBackupList;
+};
+
+export type CreateStoredBackupsOp = {
+ op: WalletApiOperation.CreateStoredBackup;
+ request: EmptyObject;
+ response: CreateStoredBackupResponse;
+};
+
+export type RecoverStoredBackupsOp = {
+ op: WalletApiOperation.RecoverStoredBackup;
+ request: RecoverStoredBackupRequest;
+ response: EmptyObject;
+};
+
+export type DeleteStoredBackupOp = {
+ op: WalletApiOperation.DeleteStoredBackup;
+ request: DeleteStoredBackupRequest;
+ response: EmptyObject;
};
// group: Peer Payments
/**
- * Initiate an outgoing peer push payment.
+ * Check if initiating a peer push payment is possible
+ * based on the funds in the wallet.
*/
-export type PreparePeerPushPaymentOp = {
- op: WalletApiOperation.PreparePeerPushPayment;
- request: PreparePeerPushPaymentRequest;
- response: PreparePeerPushPaymentResponse;
+export type CheckPeerPushDebitOp = {
+ op: WalletApiOperation.CheckPeerPushDebit;
+ request: CheckPeerPushDebitRequest;
+ response: CheckPeerPushDebitResponse;
};
/**
* Initiate an outgoing peer push payment.
*/
-export type InitiatePeerPushPaymentOp = {
- op: WalletApiOperation.InitiatePeerPushPayment;
- request: InitiatePeerPushPaymentRequest;
- response: InitiatePeerPushPaymentResponse;
+export type InitiatePeerPushDebitOp = {
+ op: WalletApiOperation.InitiatePeerPushDebit;
+ request: InitiatePeerPushDebitRequest;
+ response: InitiatePeerPushDebitResponse;
};
/**
* Check an incoming peer push payment.
*/
-export type CheckPeerPushPaymentOp = {
- op: WalletApiOperation.CheckPeerPushPayment;
- request: CheckPeerPushPaymentRequest;
- response: CheckPeerPushPaymentResponse;
+export type PreparePeerPushCreditOp = {
+ op: WalletApiOperation.PreparePeerPushCredit;
+ request: PreparePeerPushCreditRequest;
+ response: PreparePeerPushCreditResponse;
};
/**
* Accept an incoming peer push payment.
*/
-export type AcceptPeerPushPaymentOp = {
- op: WalletApiOperation.AcceptPeerPushPayment;
- request: AcceptPeerPushPaymentRequest;
+export type ConfirmPeerPushCreditOp = {
+ op: WalletApiOperation.ConfirmPeerPushCredit;
+ request: ConfirmPeerPushCreditRequest;
response: EmptyObject;
};
/**
- * Initiate an outgoing peer pull payment.
+ * Check fees for an outgoing peer pull payment.
*/
-export type PreparePeerPullPaymentOp = {
- op: WalletApiOperation.PreparePeerPullPayment;
- request: PreparePeerPullPaymentRequest;
- response: PreparePeerPullPaymentResponse;
+export type CheckPeerPullCreditOp = {
+ op: WalletApiOperation.CheckPeerPullCredit;
+ request: CheckPeerPullCreditRequest;
+ response: CheckPeerPullCreditResponse;
};
/**
* Initiate an outgoing peer pull payment.
*/
-export type InitiatePeerPullPaymentOp = {
- op: WalletApiOperation.InitiatePeerPullPayment;
- request: InitiatePeerPullPaymentRequest;
- response: InitiatePeerPullPaymentResponse;
+export type InitiatePeerPullCreditOp = {
+ op: WalletApiOperation.InitiatePeerPullCredit;
+ request: InitiatePeerPullCreditRequest;
+ response: InitiatePeerPullCreditResponse;
};
/**
* Prepare for an incoming peer pull payment.
*/
-export type CheckPeerPullPaymentOp = {
- op: WalletApiOperation.CheckPeerPullPayment;
- request: CheckPeerPullPaymentRequest;
- response: CheckPeerPullPaymentResponse;
+export type PreparePeerPullDebitOp = {
+ op: WalletApiOperation.PreparePeerPullDebit;
+ request: PreparePeerPullDebitRequest;
+ response: PreparePeerPullDebitResponse;
};
/**
- * Accept an incoming peer pull payment.
+ * Accept an incoming peer pull payment (i.e. pay the other party).
*/
-export type AcceptPeerPullPaymentOp = {
- op: WalletApiOperation.AcceptPeerPullPayment;
- request: AcceptPeerPullPaymentRequest;
+export type ConfirmPeerPullDebitOp = {
+ op: WalletApiOperation.ConfirmPeerPullDebit;
+ request: ConfirmPeerPullDebitRequest;
response: EmptyObject;
};
+// group: Data Validation
+
+export type ValidateIbanOp = {
+ op: WalletApiOperation.ValidateIban;
+ request: ValidateIbanRequest;
+ response: ValidateIbanResponse;
+};
+
// group: Database Management
/**
@@ -652,8 +939,8 @@ export type ExportDbOp = {
export type ImportDbOp = {
op: WalletApiOperation.ImportDb;
- request: any;
- response: any;
+ request: ImportDbRequest;
+ response: EmptyObject;
};
/**
@@ -688,9 +975,13 @@ export type ApplyDevExperimentOp = {
response: EmptyObject;
};
-export type SetDevModeOp = {
- op: WalletApiOperation.SetDevMode;
- request: SetDevModeRequest;
+/**
+ * Run a simple integration test on a test deployment
+ * of the exchange and merchant.
+ */
+export type RunIntegrationTestOp = {
+ op: WalletApiOperation.RunIntegrationTest;
+ request: IntegrationTestArgs;
response: EmptyObject;
};
@@ -698,8 +989,8 @@ export type SetDevModeOp = {
* Run a simple integration test on a test deployment
* of the exchange and merchant.
*/
-export type RunIntegrationTestOp = {
- op: WalletApiOperation.RunIntegrationTest;
+export type RunIntegrationTestV2Op = {
+ op: WalletApiOperation.RunIntegrationTestV2;
request: IntegrationTestArgs;
response: EmptyObject;
};
@@ -743,17 +1034,6 @@ export type TestPayOp = {
};
/**
- * Make a withdrawal from a fakebank, i.e.
- * a bank where test users can be registered freely
- * and testing APIs are available.
- */
-export type WithdrawFakebankOp = {
- op: WalletApiOperation.WithdrawFakebank;
- request: WithdrawFakebankRequest;
- response: EmptyObject;
-};
-
-/**
* Get wallet-internal pending tasks.
*/
export type GetUserAttentionRequests = {
@@ -782,11 +1062,19 @@ export type GetUserAttentionsUnreadCount = {
/**
* Get wallet-internal pending tasks.
+ *
+ * @deprecated
*/
export type GetPendingTasksOp = {
op: WalletApiOperation.GetPendingOperations;
request: EmptyObject;
- response: PendingTasksResponse;
+ response: any;
+};
+
+export type GetActiveTasksOp = {
+ op: WalletApiOperation.GetActiveTasks;
+ request: EmptyObject;
+ response: GetActiveTasksResponse;
};
/**
@@ -799,6 +1087,75 @@ export type DumpCoinsOp = {
};
/**
+ * Add an offset to the wallet's internal time.
+ */
+export type TestingSetTimetravelOp = {
+ op: WalletApiOperation.TestingSetTimetravel;
+ request: TestingSetTimetravelRequest;
+ response: EmptyObject;
+};
+
+/**
+ * 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 = {
+ op: WalletApiOperation.TestingWaitTransactionsFinal;
+ request: EmptyObject;
+ response: EmptyObject;
+};
+
+/**
+ * Wait until all transactions are in a final state.
+ */
+export type TestingWaitTasksDoneOp = {
+ op: WalletApiOperation.TestingWaitTasksDone;
+ request: EmptyObject;
+ response: EmptyObject;
+};
+
+/**
+ * Wait until all refresh transactions are in a final state.
+ */
+export type TestingWaitRefreshesFinalOp = {
+ op: WalletApiOperation.TestingWaitRefreshesFinal;
+ request: EmptyObject;
+ response: EmptyObject;
+};
+
+/**
+ * Wait until a transaction is in a particular state.
+ */
+export type TestingWaitTransactionStateOp = {
+ op: WalletApiOperation.TestingWaitTransactionState;
+ request: TestingWaitTransactionRequest;
+ response: EmptyObject;
+};
+
+export type TestingPingOp = {
+ op: WalletApiOperation.TestingPing;
+ request: EmptyObject;
+ response: EmptyObject;
+};
+
+/**
+ * Get stats about an exchange denomination.
+ */
+export type TestingGetDenomStatsOp = {
+ op: WalletApiOperation.TestingGetDenomStats;
+ request: TestingGetDenomStatsRequest;
+ response: TestingGetDenomStatsResponse;
+};
+
+/**
* Set a coin as (un-)suspended.
* Suspended coins won't be used for payments.
*/
@@ -820,18 +1177,33 @@ export type ForceRefreshOp = {
export type WalletOperations = {
[WalletApiOperation.InitWallet]: InitWalletOp;
+ [WalletApiOperation.SetWalletRunConfig]: SetWalletRunConfigOp;
[WalletApiOperation.GetVersion]: GetVersionOp;
- [WalletApiOperation.WithdrawFakebank]: WithdrawFakebankOp;
[WalletApiOperation.PreparePayForUri]: PreparePayForUriOp;
+ [WalletApiOperation.SharePayment]: SharePaymentOp;
+ [WalletApiOperation.PreparePayForTemplate]: PreparePayForTemplateOp;
[WalletApiOperation.GetContractTermsDetails]: GetContractTermsDetailsOp;
[WalletApiOperation.WithdrawTestkudos]: WithdrawTestkudosOp;
[WalletApiOperation.ConfirmPay]: ConfirmPayOp;
- [WalletApiOperation.AbortFailedPayWithRefund]: AbortPayWithRefundOp;
+ [WalletApiOperation.AbortTransaction]: AbortTransactionOp;
+ [WalletApiOperation.FailTransaction]: FailTransactionOp;
+ [WalletApiOperation.SuspendTransaction]: SuspendTransactionOp;
+ [WalletApiOperation.ResumeTransaction]: ResumeTransactionOp;
[WalletApiOperation.GetBalances]: GetBalancesOp;
+ [WalletApiOperation.ConvertDepositAmount]: ConvertDepositAmountOp;
+ [WalletApiOperation.GetMaxDepositAmount]: GetMaxDepositAmountOp;
+ [WalletApiOperation.ConvertPeerPushAmount]: ConvertPeerPushAmountOp;
+ [WalletApiOperation.GetMaxPeerPushAmount]: GetMaxPeerPushAmountOp;
+ [WalletApiOperation.ConvertWithdrawalAmount]: ConvertWithdrawalAmountOp;
+ [WalletApiOperation.GetPlanForOperation]: GetPlanForOperationOp;
+ [WalletApiOperation.GetBalanceDetail]: GetBalancesDetailOp;
[WalletApiOperation.GetTransactions]: GetTransactionsOp;
+ [WalletApiOperation.TestingGetSampleTransactions]: TestingGetSampleTransactionsOp;
[WalletApiOperation.GetTransactionById]: GetTransactionByIdOp;
+ [WalletApiOperation.GetWithdrawalTransactionByUri]: GetWithdrawalTransactionByUriOp;
[WalletApiOperation.RetryPendingNow]: RetryPendingNowOp;
[WalletApiOperation.GetPendingOperations]: GetPendingTasksOp;
+ [WalletApiOperation.GetActiveTasks]: GetActiveTasksOp;
[WalletApiOperation.GetUserAttentionRequests]: GetUserAttentionRequests;
[WalletApiOperation.GetUserAttentionUnreadCount]: GetUserAttentionsUnreadCount;
[WalletApiOperation.MarkAttentionRequestAsRead]: MarkAttentionRequestAsRead;
@@ -840,30 +1212,27 @@ export type WalletOperations = {
[WalletApiOperation.ForceRefresh]: ForceRefreshOp;
[WalletApiOperation.DeleteTransaction]: DeleteTransactionOp;
[WalletApiOperation.RetryTransaction]: RetryTransactionOp;
- [WalletApiOperation.PrepareTip]: PrepareTipOp;
- [WalletApiOperation.AcceptTip]: AcceptTipOp;
- [WalletApiOperation.ApplyRefund]: ApplyRefundOp;
- [WalletApiOperation.ApplyRefundFromPurchaseId]: ApplyRefundFromPurchaseIdOp;
- [WalletApiOperation.PrepareRefund]: PrepareRefundOp;
- [WalletApiOperation.ListCurrencies]: ListCurrenciesOp;
+ [WalletApiOperation.StartRefundQueryForUri]: StartRefundQueryForUriOp;
+ [WalletApiOperation.StartRefundQuery]: StartRefundQueryOp;
[WalletApiOperation.GetWithdrawalDetailsForAmount]: GetWithdrawalDetailsForAmountOp;
[WalletApiOperation.GetWithdrawalDetailsForUri]: GetWithdrawalDetailsForUriOp;
[WalletApiOperation.AcceptBankIntegratedWithdrawal]: AcceptBankIntegratedWithdrawalOp;
[WalletApiOperation.AcceptManualWithdrawal]: AcceptManualWithdrawalOp;
[WalletApiOperation.ListExchanges]: ListExchangesOp;
+ [WalletApiOperation.ListExchangesForScopedCurrency]: ListExchangesForScopedCurrencyOp;
[WalletApiOperation.AddExchange]: AddExchangeOp;
[WalletApiOperation.ListKnownBankAccounts]: ListKnownBankAccountsOp;
[WalletApiOperation.AddKnownBankAccounts]: AddKnownBankAccountsOp;
[WalletApiOperation.ForgetKnownBankAccounts]: ForgetKnownBankAccountsOp;
[WalletApiOperation.SetExchangeTosAccepted]: SetExchangeTosAcceptedOp;
+ [WalletApiOperation.SetExchangeTosForgotten]: SetExchangeTosForgottenOp;
[WalletApiOperation.GetExchangeTos]: GetExchangeTosOp;
[WalletApiOperation.GetExchangeDetailedInfo]: GetExchangeDetailedInfoOp;
- [WalletApiOperation.TrackDepositGroup]: TrackDepositGroupOp;
- [WalletApiOperation.GetFeeForDeposit]: GetFeeForDepositOp;
+ [WalletApiOperation.GetExchangeEntryByUrl]: GetExchangeEntryByUrlOp;
[WalletApiOperation.PrepareDeposit]: PrepareDepositOp;
+ [WalletApiOperation.GenerateDepositGroupTxId]: GenerateDepositGroupTxIdOp;
[WalletApiOperation.CreateDepositGroup]: CreateDepositGroupOp;
[WalletApiOperation.SetWalletDeviceId]: SetWalletDeviceIdOp;
- [WalletApiOperation.ExportBackupPlain]: ExportBackupPlainOp;
[WalletApiOperation.ExportBackupRecovery]: ExportBackupRecoveryOp;
[WalletApiOperation.ImportBackupRecovery]: ImportBackupRecoveryOp;
[WalletApiOperation.RunBackupCycle]: RunBackupCycleOp;
@@ -872,23 +1241,49 @@ export type WalletOperations = {
[WalletApiOperation.RemoveBackupProvider]: RemoveBackupProviderOp;
[WalletApiOperation.GetBackupInfo]: GetBackupInfoOp;
[WalletApiOperation.RunIntegrationTest]: RunIntegrationTestOp;
+ [WalletApiOperation.RunIntegrationTestV2]: RunIntegrationTestV2Op;
[WalletApiOperation.TestCrypto]: TestCryptoOp;
[WalletApiOperation.WithdrawTestBalance]: WithdrawTestBalanceOp;
[WalletApiOperation.TestPay]: TestPayOp;
[WalletApiOperation.ExportDb]: ExportDbOp;
[WalletApiOperation.ImportDb]: ImportDbOp;
- [WalletApiOperation.PreparePeerPushPayment]: PreparePeerPushPaymentOp;
- [WalletApiOperation.InitiatePeerPushPayment]: InitiatePeerPushPaymentOp;
- [WalletApiOperation.CheckPeerPushPayment]: CheckPeerPushPaymentOp;
- [WalletApiOperation.AcceptPeerPushPayment]: AcceptPeerPushPaymentOp;
- [WalletApiOperation.PreparePeerPullPayment]: PreparePeerPullPaymentOp;
- [WalletApiOperation.InitiatePeerPullPayment]: InitiatePeerPullPaymentOp;
- [WalletApiOperation.CheckPeerPullPayment]: CheckPeerPullPaymentOp;
- [WalletApiOperation.AcceptPeerPullPayment]: AcceptPeerPullPaymentOp;
+ [WalletApiOperation.CheckPeerPushDebit]: CheckPeerPushDebitOp;
+ [WalletApiOperation.InitiatePeerPushDebit]: InitiatePeerPushDebitOp;
+ [WalletApiOperation.PreparePeerPushCredit]: PreparePeerPushCreditOp;
+ [WalletApiOperation.ConfirmPeerPushCredit]: ConfirmPeerPushCreditOp;
+ [WalletApiOperation.CheckPeerPullCredit]: CheckPeerPullCreditOp;
+ [WalletApiOperation.InitiatePeerPullCredit]: InitiatePeerPullCreditOp;
+ [WalletApiOperation.PreparePeerPullDebit]: PreparePeerPullDebitOp;
+ [WalletApiOperation.ConfirmPeerPullDebit]: ConfirmPeerPullDebitOp;
[WalletApiOperation.ClearDb]: ClearDbOp;
[WalletApiOperation.Recycle]: RecycleOp;
[WalletApiOperation.ApplyDevExperiment]: ApplyDevExperimentOp;
- [WalletApiOperation.SetDevMode]: SetDevModeOp;
+ [WalletApiOperation.ValidateIban]: ValidateIbanOp;
+ [WalletApiOperation.TestingWaitTransactionsFinal]: TestingWaitTransactionsFinalOp;
+ [WalletApiOperation.TestingWaitRefreshesFinal]: TestingWaitRefreshesFinalOp;
+ [WalletApiOperation.TestingSetTimetravel]: TestingSetTimetravelOp;
+ [WalletApiOperation.TestingWaitTransactionState]: TestingWaitTransactionStateOp;
+ [WalletApiOperation.TestingWaitTasksDone]: TestingWaitTasksDoneOp;
+ [WalletApiOperation.GetCurrencySpecification]: GetCurrencySpecificationOp;
+ [WalletApiOperation.CreateStoredBackup]: CreateStoredBackupsOp;
+ [WalletApiOperation.ListStoredBackups]: ListStoredBackupsOp;
+ [WalletApiOperation.DeleteStoredBackup]: DeleteStoredBackupOp;
+ [WalletApiOperation.RecoverStoredBackup]: RecoverStoredBackupsOp;
+ [WalletApiOperation.UpdateExchangeEntry]: UpdateExchangeEntryOp;
+ [WalletApiOperation.PrepareWithdrawExchange]: PrepareWithdrawExchangeOp;
+ [WalletApiOperation.TestingInfiniteTransactionLoop]: any;
+ [WalletApiOperation.DeleteExchange]: DeleteExchangeOp;
+ [WalletApiOperation.GetExchangeResources]: GetExchangeResourcesOp;
+ [WalletApiOperation.ListGlobalCurrencyAuditors]: ListGlobalCurrencyAuditorsOp;
+ [WalletApiOperation.ListGlobalCurrencyExchanges]: ListGlobalCurrencyExchangesOp;
+ [WalletApiOperation.AddGlobalCurrencyAuditor]: AddGlobalCurrencyAuditorOp;
+ [WalletApiOperation.RemoveGlobalCurrencyAuditor]: RemoveGlobalCurrencyAuditorOp;
+ [WalletApiOperation.AddGlobalCurrencyExchange]: AddGlobalCurrencyExchangeOp;
+ [WalletApiOperation.RemoveGlobalCurrencyExchange]: RemoveGlobalCurrencyExchangeOp;
+ [WalletApiOperation.ListAssociatedRefreshes]: ListAssociatedRefreshesOp;
+ [WalletApiOperation.TestingListTaskForTransaction]: TestingListTasksForTransactionOp;
+ [WalletApiOperation.TestingGetDenomStats]: TestingGetDenomStatsOp;
+ [WalletApiOperation.TestingPing]: TestingPingOp;
};
export type WalletCoreRequestType<
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
index 2167de534..b59f52840 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -15,126 +15,146 @@
*/
/**
- * High-level wallet operations that should be indepentent from the underlying
+ * High-level wallet operations that should be independent from the underlying
* browser extension interface.
*/
/**
* Imports.
*/
+import { IDBDatabase, IDBFactory } from "@gnu-taler/idb-bridge";
import {
AbsoluteTime,
+ ActiveTask,
+ AmountJson,
+ AmountString,
Amounts,
- codecForAbortPayWithRefundRequest,
+ AsyncCondition,
+ CancellationToken,
+ CoinDumpJson,
+ CoinStatus,
+ CoreApiResponse,
+ CreateStoredBackupResponse,
+ DeleteStoredBackupRequest,
+ DenominationInfo,
+ Duration,
+ ExchangesShortListResponse,
+ GetCurrencySpecificationResponse,
+ InitResponse,
+ KnownBankAccounts,
+ KnownBankAccountsInfo,
+ ListGlobalCurrencyAuditorsResponse,
+ ListGlobalCurrencyExchangesResponse,
+ Logger,
+ NotificationType,
+ ObservabilityContext,
+ ObservabilityEventType,
+ ObservableHttpClientLibrary,
+ OpenedPromise,
+ PartialWalletRunConfig,
+ PrepareWithdrawExchangeRequest,
+ PrepareWithdrawExchangeResponse,
+ RecoverStoredBackupRequest,
+ StoredBackupList,
+ TalerError,
+ TalerErrorCode,
+ TalerProtocolTimestamp,
+ TalerUriAction,
+ TestingGetDenomStatsResponse,
+ TestingListTasksForTransactionsResponse,
+ TestingWaitTransactionRequest,
+ TimerAPI,
+ TimerGroup,
+ TransactionType,
+ ValidateIbanResponse,
+ WalletCoreVersion,
+ WalletNotification,
+ WalletRunConfig,
+ checkDbInvariant,
+ codecForAbortTransaction,
codecForAcceptBankIntegratedWithdrawalRequest,
codecForAcceptExchangeTosRequest,
- codecForAcceptManualWithdrawalRequet,
+ codecForAcceptManualWithdrawalRequest,
codecForAcceptPeerPullPaymentRequest,
- codecForAcceptPeerPushPaymentRequest,
- codecForAcceptTipRequest,
codecForAddExchangeRequest,
+ codecForAddGlobalCurrencyAuditorRequest,
+ codecForAddGlobalCurrencyExchangeRequest,
codecForAddKnownBankAccounts,
codecForAny,
codecForApplyDevExperiment,
- codecForApplyRefundFromPurchaseIdRequest,
- codecForApplyRefundRequest,
codecForCheckPeerPullPaymentRequest,
- codecForCheckPeerPushPaymentRequest,
+ codecForCheckPeerPushDebitRequest,
codecForConfirmPayRequest,
+ codecForConfirmPeerPushPaymentRequest,
+ codecForConvertAmountRequest,
codecForCreateDepositGroupRequest,
+ codecForDeleteExchangeRequest,
+ codecForDeleteStoredBackupRequest,
codecForDeleteTransactionRequest,
+ codecForFailTransactionRequest,
codecForForceRefreshRequest,
codecForForgetKnownBankAccounts,
+ codecForGetAmountRequest,
+ codecForGetBalanceDetailRequest,
codecForGetContractTermsDetails,
+ codecForGetCurrencyInfoRequest,
+ codecForGetExchangeEntryByUrlRequest,
+ codecForGetExchangeResourcesRequest,
codecForGetExchangeTosRequest,
- codecForGetFeeForDeposit,
codecForGetWithdrawalDetailsForAmountRequest,
codecForGetWithdrawalDetailsForUri,
codecForImportDbRequest,
+ codecForInitRequest,
codecForInitiatePeerPullPaymentRequest,
- codecForInitiatePeerPushPaymentRequest,
+ codecForInitiatePeerPushDebitRequest,
codecForIntegrationTestArgs,
+ codecForIntegrationTestV2Args,
+ codecForListExchangesForScopedCurrencyRequest,
codecForListKnownBankAccounts,
- codecForUserAttentionsRequest,
codecForPrepareDepositRequest,
codecForPreparePayRequest,
+ codecForPreparePayTemplateRequest,
codecForPreparePeerPullPaymentRequest,
- codecForPreparePeerPushPaymentRequest,
+ codecForPreparePeerPushCreditRequest,
codecForPrepareRefundRequest,
- codecForPrepareTipRequest,
+ codecForPrepareWithdrawExchangeRequest,
+ codecForRecoverStoredBackupRequest,
+ codecForRemoveGlobalCurrencyAuditorRequest,
+ codecForRemoveGlobalCurrencyExchangeRequest,
+ codecForResumeTransaction,
codecForRetryTransactionRequest,
codecForSetCoinSuspendedRequest,
- codecForSetDevModeRequest,
codecForSetWalletDeviceIdRequest,
+ codecForSharePaymentRequest,
+ codecForStartRefundQueryRequest,
+ codecForSuspendTransaction,
codecForTestPayArgs,
- codecForTrackDepositGroupRequest,
+ codecForTestingGetDenomStatsRequest,
+ codecForTestingListTasksForTransactionRequest,
+ codecForTestingSetTimetravelRequest,
codecForTransactionByIdRequest,
codecForTransactionsRequest,
- codecForWithdrawFakebankRequest,
+ codecForUpdateExchangeEntryRequest,
+ codecForUserAttentionByIdRequest,
+ codecForUserAttentionsRequest,
+ codecForValidateIbanRequest,
codecForWithdrawTestBalance,
- CoinDumpJson,
- CoinRefreshRequest,
- CoinStatus,
- CoreApiResponse,
- DenominationInfo,
- DenomOperationMap,
- Duration,
- durationFromSpec,
- durationMin,
- ExchangeDetailedResponse,
- ExchangeListItem,
- ExchangesListResponse,
- ExchangeTosStatusDetails,
- FeeDescription,
- GetExchangeTosResult,
- InitResponse,
+ getErrorDetailFromException,
j2s,
- KnownBankAccounts,
- KnownBankAccountsInfo,
- Logger,
- NotificationType,
+ openPromise,
parsePaytoUri,
- RefreshReason,
- TalerErrorCode,
- URL,
- WalletCoreVersion,
- WalletNotification,
- codecForUserAttentionByIdRequest,
- ManualWithdrawalDetails,
+ parseTalerUri,
+ performanceNow,
+ sampleWalletCoreTransactions,
+ setDangerousTimetravel,
+ validateIban,
} from "@gnu-taler/taler-util";
-import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js";
-import {
- CryptoDispatcher,
- CryptoWorkerFactory,
-} from "./crypto/workers/cryptoDispatcher.js";
-import { clearDatabase } from "./db-utils.js";
-import {
- AuditorTrustRecord,
- CoinSourceType,
- ConfigRecordKey,
- DenominationRecord,
- ExchangeDetailsRecord,
- exportDb,
- importDb,
- WalletStoresV1,
-} from "./db.js";
+import type { HttpRequestLibrary } from "@gnu-taler/taler-util/http";
import {
- applyDevExperiment,
- maybeInitDevMode,
- setDevMode,
-} from "./dev-experiments.js";
-import { getErrorDetailFromException, TalerError } from "./errors.js";
-import {
- ActiveLongpollInfo,
- ExchangeOperations,
- InternalWalletState,
- MerchantInfo,
- MerchantOperations,
- NotificationListener,
- RecoupOperations,
- RefreshOperations,
-} from "./internal-wallet-state.js";
-import { exportBackup } from "./operations/backup/export.js";
+ getUserAttentions,
+ getUserAttentionsUnreadCount,
+ markAttentionRequestAsRead,
+} from "./attention.js";
import {
addBackupProvider,
codecForAddBackupProviderRequest,
@@ -142,119 +162,128 @@ import {
codecForRunBackupCycle,
getBackupInfo,
getBackupRecovery,
- importBackupPlain,
loadBackupRecovery,
- processBackupForProvider,
removeBackupProvider,
runBackupCycle,
-} from "./operations/backup/index.js";
-import { setWalletDeviceId } from "./operations/backup/state.js";
-import { getBalances } from "./operations/balance.js";
+ setWalletDeviceId,
+} from "./backup/index.js";
+import { getBalanceDetail, getBalances } from "./balance.js";
+import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js";
import {
- getUserAttentions,
- getUserAttentionsUnreadCount,
- markAttentionRequestAsRead,
-} from "./operations/attention.js";
+ CryptoDispatcher,
+ CryptoWorkerFactory,
+} from "./crypto/workers/crypto-dispatcher.js";
import {
- getExchangeTosStatus,
- makeExchangeListItem,
- runOperationWithErrorReporting,
-} from "./operations/common.js";
+ CoinSourceType,
+ ConfigRecordKey,
+ DenominationRecord,
+ WalletDbReadOnlyTransaction,
+ WalletStoresV1,
+ clearDatabase,
+ exportDb,
+ importDb,
+ openStoredBackupsDatabase,
+ openTalerDatabase,
+ timestampAbsoluteFromDb,
+ timestampProtocolToDb,
+} from "./db.js";
import {
+ checkDepositGroup,
createDepositGroup,
- getFeeForDeposit,
- prepareDepositGroup,
- processDepositGroup,
- trackDepositGroup,
-} from "./operations/deposits.js";
+ generateDepositGroupTxId,
+} from "./deposits.js";
+import { DevExperimentHttpLib, applyDevExperiment } from "./dev-experiments.js";
import {
+ ReadyExchangeSummary,
acceptExchangeTermsOfService,
- downloadTosFromAcceptedFormat,
- getExchangeDetails,
- getExchangeRequestTimeout,
- getExchangeTrust,
- provideExchangeRecordInTx,
- updateExchangeFromUrl,
- updateExchangeFromUrlHandler,
- updateExchangeTermsOfService,
-} from "./operations/exchanges.js";
-import { getMerchantInfo } from "./operations/merchants.js";
+ addPresetExchangeEntry,
+ deleteExchange,
+ fetchFreshExchange,
+ forgetExchangeTermsOfService,
+ getExchangeDetailedInfo,
+ getExchangeResources,
+ getExchangeTos,
+ listExchanges,
+ lookupExchangeByUri,
+} from "./exchanges.js";
+import {
+ convertDepositAmount,
+ convertPeerPushAmount,
+ convertWithdrawalAmount,
+ getMaxDepositAmount,
+ getMaxPeerPushAmount,
+} from "./instructedAmountConversion.js";
+import {
+ ObservableDbAccess,
+ ObservableTaskScheduler,
+ observeTalerCrypto,
+} from "./observable-wrappers.js";
import {
- abortFailedPayWithRefund,
- applyRefund,
- applyRefundFromPurchaseId,
confirmPay,
getContractTermsDetails,
+ preparePayForTemplate,
preparePayForUri,
- prepareRefund,
- processPurchase,
-} from "./operations/pay-merchant.js";
+ sharePayment,
+ startQueryRefund,
+ startRefundQueryForUri,
+} from "./pay-merchant.js";
import {
- acceptPeerPullPayment,
- acceptPeerPushPayment,
- checkPeerPullPayment,
- checkPeerPushPayment,
+ checkPeerPullPaymentInitiation,
initiatePeerPullPayment,
- initiatePeerToPeerPush,
- preparePeerPullPayment,
- preparePeerPushPayment,
-} from "./operations/pay-peer.js";
-import { getPendingOperations } from "./operations/pending.js";
+} from "./pay-peer-pull-credit.js";
+import {
+ confirmPeerPullDebit,
+ preparePeerPullDebit,
+} from "./pay-peer-pull-debit.js";
+import {
+ confirmPeerPushCredit,
+ preparePeerPushCredit,
+} from "./pay-peer-push-credit.js";
import {
- createRecoupGroup,
- processRecoupGroup,
- processRecoupGroupHandler,
-} from "./operations/recoup.js";
+ checkPeerPushDebit,
+ initiatePeerPushDebit,
+} from "./pay-peer-push-debit.js";
+import {
+ AfterCommitInfo,
+ DbAccess,
+ DbAccessImpl,
+ TriggerSpec,
+} from "./query.js";
+import { forceRefresh } from "./refresh.js";
import {
- autoRefresh,
- createRefreshGroup,
- processRefreshGroup,
-} from "./operations/refresh.js";
+ TaskScheduler,
+ TaskSchedulerImpl,
+ convertTaskToTransactionId,
+ listTaskForTransactionId,
+} from "./shepherd.js";
import {
runIntegrationTest,
+ runIntegrationTest2,
testPay,
+ waitTasksDone,
+ waitTransactionState,
+ waitUntilAllTransactionsFinal,
+ waitUntilRefreshesDone,
withdrawTestBalance,
-} from "./operations/testing.js";
-import { acceptTip, prepareTip, processTip } from "./operations/tip.js";
+} from "./testing.js";
import {
+ abortTransaction,
+ constructTransactionIdentifier,
deleteTransaction,
+ failTransaction,
getTransactionById,
getTransactions,
+ getWithdrawalTransactionByUri,
+ parseTransactionIdentifier,
+ resumeTransaction,
retryTransaction,
-} from "./operations/transactions.js";
-import {
- acceptWithdrawalFromUri,
- createManualWithdrawal,
- getExchangeWithdrawalInfo,
- getWithdrawalDetailsForUri,
- processWithdrawalGroup,
-} from "./operations/withdraw.js";
-import { PendingTaskInfo, PendingTaskType } from "./pending-types.js";
-import { assertUnreachable } from "./util/assertUnreachable.js";
-import {
- createTimeline,
- selectBestForOverlappingDenominations,
- selectMinimumFee,
-} from "./util/denominations.js";
-import {
- HttpRequestLibrary,
- readSuccessResponseJsonOrThrow,
-} from "./util/http.js";
-import { checkDbInvariant } from "./util/invariants.js";
-import {
- AsyncCondition,
- OpenedPromise,
- openPromise,
-} from "./util/promiseUtils.js";
-import {
- DbAccess,
- GetReadOnlyAccess,
- GetReadWriteAccess,
-} from "./util/query.js";
-import { OperationAttemptResult, RetryTags } from "./util/retries.js";
-import { TimerAPI, TimerGroup } from "./util/timer.js";
+ suspendTransaction,
+} 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,
WALLET_MERCHANT_PROTOCOL_VERSION,
} from "./versions.js";
@@ -263,297 +292,93 @@ import {
WalletCoreApiClient,
WalletCoreResponseType,
} from "./wallet-api-types.js";
-
-const builtinAuditors: AuditorTrustRecord[] = [
- {
- currency: "KUDOS",
- auditorPub: "BW9DC48PHQY4NH011SHHX36DZZ3Q22Y6X7FZ1VD1CMZ2PTFZ6PN0",
- auditorBaseUrl: "https://auditor.demo.taler.net/",
- uids: ["5P25XF8TVQP9AW6VYGY2KV47WT5Y3ZXFSJAA570GJPX5SVJXKBVG"],
- },
-];
-
-const builtinExchanges: string[] = ["https://exchange.demo.taler.net/"];
+import {
+ acceptWithdrawalFromUri,
+ createManualWithdrawal,
+ getWithdrawalDetailsForAmount,
+ getWithdrawalDetailsForUri,
+} from "./withdraw.js";
const logger = new Logger("wallet.ts");
/**
- * Call the right handler for a pending operation without doing
- * any special error handling.
- */
-async function callOperationHandler(
- ws: InternalWalletState,
- pending: PendingTaskInfo,
- forceNow = false,
-): Promise<OperationAttemptResult> {
- switch (pending.type) {
- case PendingTaskType.ExchangeUpdate:
- return await updateExchangeFromUrlHandler(ws, pending.exchangeBaseUrl, {
- forceNow,
- });
- case PendingTaskType.Refresh:
- return await processRefreshGroup(ws, pending.refreshGroupId, {
- forceNow,
- });
- case PendingTaskType.Withdraw:
- return await processWithdrawalGroup(ws, pending.withdrawalGroupId, {
- forceNow,
- });
- case PendingTaskType.TipPickup:
- return await processTip(ws, pending.tipId, { forceNow });
- case PendingTaskType.Purchase:
- return await processPurchase(ws, pending.proposalId, { forceNow });
- case PendingTaskType.Recoup:
- return await processRecoupGroupHandler(ws, pending.recoupGroupId, {
- forceNow,
- });
- case PendingTaskType.ExchangeCheckRefresh:
- return await autoRefresh(ws, pending.exchangeBaseUrl);
- case PendingTaskType.Deposit: {
- return await processDepositGroup(ws, pending.depositGroupId, {
- forceNow,
- });
- }
- case PendingTaskType.Backup:
- return await processBackupForProvider(ws, pending.backupProviderBaseUrl);
- default:
- return assertUnreachable(pending);
- }
- throw Error(`not reached ${pending.type}`);
-}
-
-/**
- * Process pending operations.
- */
-export async function runPending(
- ws: InternalWalletState,
- forceNow = false,
-): Promise<void> {
- const pendingOpsResponse = await getPendingOperations(ws);
- for (const p of pendingOpsResponse.pendingOperations) {
- if (!forceNow && !AbsoluteTime.isExpired(p.timestampDue)) {
- continue;
- }
- await runOperationWithErrorReporting(ws, p.id, async () => {
- logger.trace(`running pending ${JSON.stringify(p, undefined, 2)}`);
- return await callOperationHandler(ws, p, forceNow);
- });
- }
-}
-
-export interface RetryLoopOpts {
- /**
- * Stop when the number of retries is exceeded for any pending
- * operation.
- */
- maxRetries?: number;
-
- /**
- * Stop the retry loop when all lifeness-giving pending operations
- * are done.
- *
- * Defaults to false.
- */
- stopWhenDone?: boolean;
-}
-
-export interface TaskLoopResult {
- /**
- * Was the maximum number of retries exceeded in a task?
- */
- retriesExceeded: boolean;
-}
-
-/**
- * Main retry loop of the wallet.
+ * Execution context for code that is run in the wallet.
*
- * Looks up pending operations from the wallet, runs them, repeat.
+ * Typically the execution context is either for a wallet-core
+ * request handler or for a shepherded task.
*/
-async function runTaskLoop(
- ws: InternalWalletState,
- opts: RetryLoopOpts = {},
-): Promise<TaskLoopResult> {
- logger.info(`running task loop opts=${j2s(opts)}`);
- let retriesExceeded = false;
- for (let iteration = 0; !ws.stopped; iteration++) {
- const pending = await getPendingOperations(ws);
- logger.trace(`pending operations: ${j2s(pending)}`);
- let numGivingLiveness = 0;
- let numDue = 0;
- let minDue: AbsoluteTime = AbsoluteTime.never();
-
- for (const p of pending.pendingOperations) {
- const maxRetries = opts.maxRetries;
-
- if (maxRetries && p.retryInfo && p.retryInfo.retryCounter > maxRetries) {
- retriesExceeded = true;
- logger.warn(
- `skipping, as ${maxRetries} retries are exceeded in an operation of type ${p.type}`,
- );
- continue;
- }
- if (p.givesLifeness) {
- numGivingLiveness++;
- }
- if (!p.isDue) {
- continue;
- }
- minDue = AbsoluteTime.min(minDue, p.timestampDue);
- numDue++;
- }
+export interface WalletExecutionContext {
+ readonly ws: InternalWalletState;
+ readonly cryptoApi: TalerCryptoInterface;
+ readonly cancellationToken: CancellationToken;
+ readonly http: HttpRequestLibrary;
+ readonly db: DbAccess<typeof WalletStoresV1>;
+ readonly oc: ObservabilityContext;
+ readonly taskScheduler: TaskScheduler;
+}
- if (opts.stopWhenDone && numGivingLiveness === 0 && iteration !== 0) {
- logger.warn(`stopping, as no pending operations have lifeness`);
- return {
- retriesExceeded,
- };
- }
+export const EXCHANGE_COINS_LOCK = "exchange-coins-lock";
+export const EXCHANGE_RESERVES_LOCK = "exchange-reserves-lock";
- // Make sure that we run tasks that don't give lifeness at least
- // one time.
- if (iteration !== 0 && numDue === 0) {
- // We've executed pending, due operations at least one.
- // Now we don't have any more operations available,
- // and need to wait.
+export type NotificationListener = (n: WalletNotification) => void;
- // Wait for at most 5 seconds to the next check.
- const dt = durationMin(
- durationFromSpec({
- seconds: 5,
- }),
- Duration.getRemaining(minDue),
- );
- logger.trace(`waiting for at most ${dt.d_ms} ms`);
- const timeout = ws.timerGroup.resolveAfter(dt);
- ws.notify({
- type: NotificationType.WaitingForRetry,
- numGivingLiveness,
- numDue,
- numPending: pending.pendingOperations.length,
- });
- // Wait until either the timeout, or we are notified (via the latch)
- // that more work might be available.
- await Promise.race([timeout, ws.latch.wait()]);
- } else {
- logger.trace(
- `running ${pending.pendingOperations.length} pending operations`,
- );
- for (const p of pending.pendingOperations) {
- if (!AbsoluteTime.isExpired(p.timestampDue)) {
- continue;
- }
- await runOperationWithErrorReporting(ws, p.id, async () => {
- logger.trace(`running pending ${JSON.stringify(p, undefined, 2)}`);
- return await callOperationHandler(ws, p);
- });
- ws.notify({
- type: NotificationType.PendingOperationProcessed,
- id: p.id,
- });
- }
- }
- }
- logger.trace("exiting wallet retry loop");
- return {
- retriesExceeded,
- };
-}
+type CancelFn = () => void;
/**
* Insert the hard-coded defaults for exchanges, coins and
* auditors into the database, unless these defaults have
* already been applied.
*/
-async function fillDefaults(ws: InternalWalletState): Promise<void> {
- await ws.db
- .mktx((x) => [x.config, x.auditorTrust, x.exchanges, x.exchangeDetails])
- .runReadWrite(async (tx) => {
+async function fillDefaults(wex: WalletExecutionContext): Promise<void> {
+ const notifications: WalletNotification[] = [];
+ await wex.db.runReadWriteTx(
+ { storeNames: ["config", "exchanges"] },
+ async (tx) => {
const appliedRec = await tx.config.get("currencyDefaultsApplied");
let alreadyApplied = appliedRec ? !!appliedRec.value : false;
if (alreadyApplied) {
logger.trace("defaults already applied");
return;
}
- logger.info("importing default exchanges and auditors");
- for (const c of builtinAuditors) {
- await tx.auditorTrust.put(c);
- }
- for (const baseUrl of builtinExchanges) {
- const now = AbsoluteTime.now();
- provideExchangeRecordInTx(ws, tx, baseUrl, now);
+ for (const exch of wex.ws.config.builtin.exchanges) {
+ const resp = await addPresetExchangeEntry(
+ tx,
+ exch.exchangeBaseUrl,
+ exch.currencyHint,
+ );
+ if (resp.notification) {
+ notifications.push(resp.notification);
+ }
}
await tx.config.put({
key: ConfigRecordKey.CurrencyDefaultsApplied,
value: true,
});
- });
+ },
+ );
+ for (const notif of notifications) {
+ wex.ws.notify(notif);
+ }
}
-/**
- * Get the exchange ToS in the requested format.
- * Try to download in the accepted format not cached.
- */
-async function getExchangeTos(
- ws: InternalWalletState,
+export async function getDenomInfo(
+ wex: WalletExecutionContext,
+ tx: WalletDbReadOnlyTransaction<["denominations"]>,
exchangeBaseUrl: string,
- acceptedFormat?: string[],
-): Promise<GetExchangeTosResult> {
- // FIXME: download ToS in acceptable format if passed!
- const { exchangeDetails } = await updateExchangeFromUrl(ws, exchangeBaseUrl);
- const tosDetails = await ws.db
- .mktx((x) => [x.exchangeTos])
- .runReadOnly(async (tx) => {
- return await getExchangeTosStatusDetails(tx, exchangeDetails);
- });
- const content = tosDetails.content;
- const currentEtag = tosDetails.currentVersion;
- const contentType = tosDetails.contentType;
- if (
- content === undefined ||
- currentEtag === undefined ||
- contentType === undefined
- ) {
- throw Error("exchange is in invalid state");
- }
- if (
- acceptedFormat &&
- acceptedFormat.findIndex((f) => f === contentType) !== -1
- ) {
- return {
- acceptedEtag: exchangeDetails.tosAccepted?.etag,
- currentEtag,
- content,
- contentType,
- tosStatus: getExchangeTosStatus(exchangeDetails),
- };
+ denomPubHash: string,
+): Promise<DenominationInfo | undefined> {
+ const cacheKey = `${exchangeBaseUrl}:${denomPubHash}`;
+ const cached = wex.ws.denomInfoCache.get(cacheKey);
+ if (cached) {
+ return cached;
}
-
- const tosDownload = await downloadTosFromAcceptedFormat(
- ws,
- exchangeBaseUrl,
- getExchangeRequestTimeout(),
- acceptedFormat,
- );
-
- if (tosDownload.tosContentType === contentType) {
- return {
- acceptedEtag: exchangeDetails.tosAccepted?.etag,
- currentEtag,
- content,
- contentType,
- tosStatus: getExchangeTosStatus(exchangeDetails),
- };
+ const d = await tx.denominations.get([exchangeBaseUrl, denomPubHash]);
+ if (d) {
+ const denomInfo = DenominationRecord.toDenomInfo(d);
+ wex.ws.denomInfoCache.put(cacheKey, denomInfo);
+ return denomInfo;
}
-
- await updateExchangeTermsOfService(ws, exchangeBaseUrl, tosDownload);
-
- return {
- acceptedEtag: exchangeDetails.tosAccepted?.etag,
- currentEtag: tosDownload.tosEtag,
- content: tosDownload.tosText,
- contentType: tosDownload.tosContentType,
- tosStatus: getExchangeTosStatus(exchangeDetails),
- };
+ return undefined;
}
/**
@@ -561,293 +386,73 @@ async function getExchangeTos(
* previous withdrawals.
*/
async function listKnownBankAccounts(
- ws: InternalWalletState,
+ wex: WalletExecutionContext,
currency?: string,
): Promise<KnownBankAccounts> {
const accounts: KnownBankAccountsInfo[] = [];
- await ws.db
- .mktx((x) => [x.bankAccounts])
- .runReadOnly(async (tx) => {
- const knownAccounts = await tx.bankAccounts.iter().toArray();
- for (const r of knownAccounts) {
- if (currency && currency !== r.currency) {
- continue;
- }
- const payto = parsePaytoUri(r.uri);
- if (payto) {
- accounts.push({
- uri: payto,
- alias: r.alias,
- kyc_completed: r.kycCompleted,
- currency: r.currency,
- });
- }
+ await wex.db.runReadOnlyTx({ storeNames: ["bankAccounts"] }, async (tx) => {
+ const knownAccounts = await tx.bankAccounts.iter().toArray();
+ for (const r of knownAccounts) {
+ if (currency && currency !== r.currency) {
+ continue;
}
- });
+ const payto = parsePaytoUri(r.uri);
+ if (payto) {
+ accounts.push({
+ uri: payto,
+ alias: r.alias,
+ kyc_completed: r.kycCompleted,
+ currency: r.currency,
+ });
+ }
+ }
+ });
return { accounts };
}
/**
*/
async function addKnownBankAccounts(
- ws: InternalWalletState,
+ wex: WalletExecutionContext,
payto: string,
alias: string,
currency: string,
): Promise<void> {
- await ws.db
- .mktx((x) => [x.bankAccounts])
- .runReadWrite(async (tx) => {
- tx.bankAccounts.put({
- uri: payto,
- alias: alias,
- currency: currency,
- kycCompleted: false,
- });
+ await wex.db.runReadWriteTx({ storeNames: ["bankAccounts"] }, async (tx) => {
+ tx.bankAccounts.put({
+ uri: payto,
+ alias: alias,
+ currency: currency,
+ kycCompleted: false,
});
+ });
return;
}
/**
*/
async function forgetKnownBankAccounts(
- ws: InternalWalletState,
+ wex: WalletExecutionContext,
payto: string,
): Promise<void> {
- await ws.db
- .mktx((x) => [x.bankAccounts])
- .runReadWrite(async (tx) => {
- const account = await tx.bankAccounts.get(payto);
- if (!account) {
- throw Error(`account not found: ${payto}`);
- }
- tx.bankAccounts.delete(account.uri);
- });
+ await wex.db.runReadWriteTx({ storeNames: ["bankAccounts"] }, async (tx) => {
+ const account = await tx.bankAccounts.get(payto);
+ if (!account) {
+ throw Error(`account not found: ${payto}`);
+ }
+ tx.bankAccounts.delete(account.uri);
+ });
return;
}
-async function getExchangeTosStatusDetails(
- tx: GetReadOnlyAccess<{ exchangeTos: typeof WalletStoresV1.exchangeTos }>,
- exchangeDetails: ExchangeDetailsRecord,
-): Promise<ExchangeTosStatusDetails> {
- let exchangeTos = await tx.exchangeTos.get([
- exchangeDetails.exchangeBaseUrl,
- exchangeDetails.tosCurrentEtag,
- ]);
-
- if (!exchangeTos) {
- exchangeTos = {
- etag: "not-available",
- termsOfServiceContentType: "text/plain",
- termsOfServiceText: "terms of service unavailable",
- exchangeBaseUrl: exchangeDetails.exchangeBaseUrl,
- };
- }
-
- return {
- acceptedVersion: exchangeDetails.tosAccepted?.etag,
- content: exchangeTos.termsOfServiceText,
- contentType: exchangeTos.termsOfServiceContentType,
- currentVersion: exchangeTos.etag,
- };
-}
-
-async function getExchanges(
- ws: InternalWalletState,
-): Promise<ExchangesListResponse> {
- const exchanges: ExchangeListItem[] = [];
- await ws.db
- .mktx((x) => [
- x.exchanges,
- x.exchangeDetails,
- x.exchangeTos,
- x.denominations,
- x.operationRetries,
- ])
- .runReadOnly(async (tx) => {
- const exchangeRecords = await tx.exchanges.iter().toArray();
- for (const r of exchangeRecords) {
- const exchangeDetails = await getExchangeDetails(tx, r.baseUrl);
- const opRetryRecord = await tx.operationRetries.get(
- RetryTags.forExchangeUpdate(r),
- );
- exchanges.push(
- makeExchangeListItem(r, exchangeDetails, opRetryRecord?.lastError),
- );
- }
- });
- return { exchanges };
-}
-
-async function getExchangeDetailedInfo(
- ws: InternalWalletState,
- exchangeBaseurl: string,
-): Promise<ExchangeDetailedResponse> {
- //TODO: should we use the forceUpdate parameter?
- const exchange = await ws.db
- .mktx((x) => [
- x.exchanges,
- x.exchangeTos,
- x.exchangeDetails,
- x.denominations,
- ])
- .runReadOnly(async (tx) => {
- const ex = await tx.exchanges.get(exchangeBaseurl);
- const dp = ex?.detailsPointer;
- if (!dp) {
- return;
- }
- const { currency } = dp;
- const exchangeDetails = await getExchangeDetails(tx, ex.baseUrl);
- if (!exchangeDetails) {
- return;
- }
-
- const denominationRecords =
- await tx.denominations.indexes.byExchangeBaseUrl
- .iter(ex.baseUrl)
- .toArray();
-
- if (!denominationRecords) {
- return;
- }
-
- const tos = await getExchangeTosStatusDetails(tx, exchangeDetails);
-
- const denominations: DenominationInfo[] = denominationRecords.map((x) =>
- DenominationRecord.toDenomInfo(x),
- );
-
- return {
- info: {
- exchangeBaseUrl: ex.baseUrl,
- currency,
- tos,
- paytoUris: exchangeDetails.wireInfo.accounts.map((x) => x.payto_uri),
- auditors: exchangeDetails.auditors,
- wireInfo: exchangeDetails.wireInfo,
- globalFees: exchangeDetails.globalFees,
- },
- denominations,
- };
- });
-
- if (!exchange) {
- throw Error(`exchange with base url "${exchangeBaseurl}" not found`);
- }
-
- const denoms = exchange.denominations.map((d) => ({
- ...d,
- group: Amounts.stringifyValue(d.value),
- }));
- const denomFees: DenomOperationMap<FeeDescription[]> = {
- deposit: createTimeline(
- denoms,
- "denomPubHash",
- "stampStart",
- "stampExpireDeposit",
- "feeDeposit",
- "group",
- selectBestForOverlappingDenominations,
- ),
- refresh: createTimeline(
- denoms,
- "denomPubHash",
- "stampStart",
- "stampExpireWithdraw",
- "feeRefresh",
- "group",
- selectBestForOverlappingDenominations,
- ),
- refund: createTimeline(
- denoms,
- "denomPubHash",
- "stampStart",
- "stampExpireWithdraw",
- "feeRefund",
- "group",
- selectBestForOverlappingDenominations,
- ),
- withdraw: createTimeline(
- denoms,
- "denomPubHash",
- "stampStart",
- "stampExpireWithdraw",
- "feeWithdraw",
- "group",
- selectBestForOverlappingDenominations,
- ),
- };
-
- const transferFees = Object.entries(
- exchange.info.wireInfo.feesForType,
- ).reduce((prev, [wireType, infoForType]) => {
- const feesByGroup = [
- ...infoForType.map((w) => ({
- ...w,
- fee: Amounts.stringify(w.closingFee),
- group: "closing",
- })),
- ...infoForType.map((w) => ({ ...w, fee: w.wireFee, group: "wire" })),
- ];
- prev[wireType] = createTimeline(
- feesByGroup,
- "sig",
- "startStamp",
- "endStamp",
- "fee",
- "group",
- selectMinimumFee,
- );
- return prev;
- }, {} as Record<string, FeeDescription[]>);
-
- const globalFeesByGroup = [
- ...exchange.info.globalFees.map((w) => ({
- ...w,
- fee: w.accountFee,
- group: "account",
- })),
- ...exchange.info.globalFees.map((w) => ({
- ...w,
- fee: w.historyFee,
- group: "history",
- })),
- ...exchange.info.globalFees.map((w) => ({
- ...w,
- fee: w.purseFee,
- group: "purse",
- })),
- ];
-
- const globalFees = createTimeline(
- globalFeesByGroup,
- "signature",
- "startDate",
- "endDate",
- "fee",
- "group",
- selectMinimumFee,
- );
-
- return {
- exchange: {
- ...exchange.info,
- denomFees,
- transferFees,
- globalFees,
- },
- };
-}
-
async function setCoinSuspended(
- ws: InternalWalletState,
+ wex: WalletExecutionContext,
coinPub: string,
suspended: boolean,
): Promise<void> {
- await ws.db
- .mktx((x) => [x.coins, x.coinAvailability])
- .runReadWrite(async (tx) => {
+ await wex.db.runReadWriteTx(
+ { storeNames: ["coins", "coinAvailability"] },
+ async (tx) => {
const c = await tx.coins.get(coinPub);
if (!c) {
logger.warn(`coin ${coinPub} not found, won't suspend`);
@@ -879,18 +484,19 @@ async function setCoinSuspended(
}
await tx.coins.put(c);
await tx.coinAvailability.put(coinAvailability);
- });
+ },
+ );
}
/**
* Dump the public information of coins we have in an easy-to-process format.
*/
-async function dumpCoins(ws: InternalWalletState): Promise<CoinDumpJson> {
+async function dumpCoins(wex: WalletExecutionContext): Promise<CoinDumpJson> {
const coinsJson: CoinDumpJson = { coins: [] };
logger.info("dumping coins");
- await ws.db
- .mktx((x) => [x.coins, x.denominations, x.withdrawalGroups])
- .runReadOnly(async (tx) => {
+ await wex.db.runReadOnlyTx(
+ { storeNames: ["coins", "denominations"] },
+ async (tx) => {
const coins = await tx.coins.iter().toArray();
for (const c of coins) {
const denom = await tx.denominations.get([
@@ -898,7 +504,7 @@ async function dumpCoins(ws: InternalWalletState): Promise<CoinDumpJson> {
c.denomPubHash,
]);
if (!denom) {
- console.error("no denom session found for coin");
+ logger.warn("no denom found for coin");
continue;
}
const cs = c.coinSource;
@@ -908,32 +514,23 @@ async function dumpCoins(ws: InternalWalletState): Promise<CoinDumpJson> {
}
let withdrawalReservePub: string | undefined;
if (cs.type == CoinSourceType.Withdraw) {
- const ws = await tx.withdrawalGroups.get(cs.withdrawalGroupId);
- if (!ws) {
- console.error("no withdrawal session found for coin");
- continue;
- }
- withdrawalReservePub = ws.reservePub;
+ withdrawalReservePub = cs.reservePub;
}
- const denomInfo = await ws.getDenomInfo(
- ws,
+ const denomInfo = await getDenomInfo(
+ wex,
tx,
c.exchangeBaseUrl,
c.denomPubHash,
);
if (!denomInfo) {
- console.error("no denomination found for coin");
+ logger.warn("no denomination found for coin");
continue;
}
coinsJson.coins.push({
coin_pub: c.coinPub,
denom_pub: denomInfo.denomPub,
denom_pub_hash: c.denomPubHash,
- denom_value: Amounts.stringify({
- value: denom.amountVal,
- currency: denom.currency,
- fraction: denom.amountFrac,
- }),
+ denom_value: denom.value,
exchange_base_url: c.exchangeBaseUrl,
refresh_parent_coin_pub: refreshParentCoinPub,
withdrawal_reserve_pub: withdrawalReservePub,
@@ -947,20 +544,22 @@ async function dumpCoins(ws: InternalWalletState): Promise<CoinDumpJson> {
: undefined,
});
}
- });
+ },
+ );
return coinsJson;
}
/**
* Get an API client from an internal wallet state object.
*/
-export async function getClientFromWalletState(
+let id = 0;
+async function getClientFromWalletState(
ws: InternalWalletState,
): Promise<WalletCoreApiClient> {
- let id = 0;
const client: WalletCoreApiClient = {
async call(op, payload): Promise<any> {
- const res = await handleCoreApiRequest(ws, op, `${id++}`, payload);
+ id = (id + 1) % (Number.MAX_SAFE_INTEGER - 100);
+ const res = await handleCoreApiRequest(ws, op, String(id), payload);
switch (res.type) {
case "error":
throw TalerError.fromUncheckedDetail(res.error);
@@ -972,21 +571,121 @@ export async function getClientFromWalletState(
return client;
}
-declare const __VERSION__: string;
-declare const __GIT_HASH__: string;
+async function createStoredBackup(
+ wex: WalletExecutionContext,
+): Promise<CreateStoredBackupResponse> {
+ const backup = await exportDb(wex.ws.idb);
+ const backupsDb = await openStoredBackupsDatabase(wex.ws.idb);
+ const name = `backup-${new Date().getTime()}`;
+ await backupsDb.runAllStoresReadWriteTx({}, async (tx) => {
+ await tx.backupMeta.add({
+ name,
+ });
+ await tx.backupData.add(backup, name);
+ });
+ return {
+ name,
+ };
+}
+
+async function listStoredBackups(
+ wex: WalletExecutionContext,
+): Promise<StoredBackupList> {
+ const storedBackups: StoredBackupList = {
+ storedBackups: [],
+ };
+ const backupsDb = await openStoredBackupsDatabase(wex.ws.idb);
+ await backupsDb.runAllStoresReadWriteTx({}, async (tx) => {
+ await tx.backupMeta.iter().forEach((x) => {
+ storedBackups.storedBackups.push({
+ name: x.name,
+ });
+ });
+ });
+ return storedBackups;
+}
-const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : "dev";
-const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined;
+async function deleteStoredBackup(
+ wex: WalletExecutionContext,
+ req: DeleteStoredBackupRequest,
+): Promise<void> {
+ const backupsDb = await openStoredBackupsDatabase(wex.ws.idb);
+ await backupsDb.runAllStoresReadWriteTx({}, async (tx) => {
+ await tx.backupData.delete(req.name);
+ await tx.backupMeta.delete(req.name);
+ });
+}
+
+async function recoverStoredBackup(
+ wex: WalletExecutionContext,
+ req: RecoverStoredBackupRequest,
+): Promise<void> {
+ logger.info(`Recovering stored backup ${req.name}`);
+ const { name } = req;
+ const backupsDb = await openStoredBackupsDatabase(wex.ws.idb);
+ const bd = await backupsDb.runAllStoresReadWriteTx({}, async (tx) => {
+ const backupMeta = tx.backupMeta.get(name);
+ if (!backupMeta) {
+ throw Error("backup not found");
+ }
+ const backupData = await tx.backupData.get(name);
+ if (!backupData) {
+ throw Error("no backup data (DB corrupt)");
+ }
+ return backupData;
+ });
+ logger.info(`backup found, now importing`);
+ await importDb(wex.db.idbHandle(), bd);
+ logger.info(`import done`);
+}
+
+async function handlePrepareWithdrawExchange(
+ wex: WalletExecutionContext,
+ req: PrepareWithdrawExchangeRequest,
+): Promise<PrepareWithdrawExchangeResponse> {
+ const parsedUri = parseTalerUri(req.talerUri);
+ if (parsedUri?.type !== TalerUriAction.WithdrawExchange) {
+ throw Error("expected a taler://withdraw-exchange URI");
+ }
+ const exchangeBaseUrl = parsedUri.exchangeBaseUrl;
+ const exchange = await fetchFreshExchange(wex, exchangeBaseUrl);
+ if (parsedUri.exchangePub && exchange.masterPub != parsedUri.exchangePub) {
+ throw Error("mismatch of exchange master public key (URI vs actual)");
+ }
+ if (parsedUri.amount) {
+ const amt = Amounts.parseOrThrow(parsedUri.amount);
+ if (amt.currency !== exchange.currency) {
+ throw Error("mismatch of currency (URI vs exchange)");
+ }
+ }
+ return {
+ exchangeBaseUrl,
+ amount: parsedUri.amount,
+ };
+}
+
+/**
+ * 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[];
+}
/**
* Implementation of the "wallet-core" API.
*/
-async function dispatchRequestInternal<Op extends WalletApiOperation>(
- ws: InternalWalletState,
+async function dispatchRequestInternal(
+ wex: WalletExecutionContext,
+ cts: CancellationToken.Source,
operation: WalletApiOperation,
payload: unknown,
): Promise<WalletCoreResponseType<typeof operation>> {
- if (!ws.initCalled && operation !== WalletApiOperation.InitWallet) {
+ if (!wex.ws.initCalled && operation !== WalletApiOperation.InitWallet) {
throw Error(
`wallet must be initialized before running operation ${operation}`,
);
@@ -994,89 +693,219 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
// FIXME: Can we make this more type-safe by using the request/response type
// definitions we already have?
switch (operation) {
+ case WalletApiOperation.CreateStoredBackup:
+ return createStoredBackup(wex);
+ case WalletApiOperation.DeleteStoredBackup: {
+ const req = codecForDeleteStoredBackupRequest().decode(payload);
+ await deleteStoredBackup(wex, req);
+ return {};
+ }
+ case WalletApiOperation.ListStoredBackups:
+ return listStoredBackups(wex);
+ case WalletApiOperation.RecoverStoredBackup: {
+ const req = codecForRecoverStoredBackupRequest().decode(payload);
+ await recoverStoredBackup(wex, req);
+ return {};
+ }
+ case WalletApiOperation.SetWalletRunConfig:
case WalletApiOperation.InitWallet: {
- logger.trace("initializing wallet");
- ws.initCalled = true;
- if (typeof payload === "object" && (payload as any).skipDefaults) {
+ const req = codecForInitRequest().decode(payload);
+
+ logger.info(`init request: ${j2s(req)}`);
+
+ if (wex.ws.initCalled) {
+ logger.info("initializing wallet (repeat initialization)");
+ } else {
+ logger.info("initializing wallet (first initialization)");
+ }
+
+ // 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(ws);
+ await fillDefaults(wex);
}
- await maybeInitDevMode(ws);
const resp: InitResponse = {
- versionInfo: getVersion(ws),
+ versionInfo: getVersion(wex),
};
+
+ // After initialization, task loop should run.
+ await wex.taskScheduler.ensureRunning();
+
+ wex.ws.initCalled = true;
return resp;
}
case WalletApiOperation.WithdrawTestkudos: {
- await withdrawTestBalance(ws, {
- amount: "TESTKUDOS:10",
- bankBaseUrl: "https://bank.test.taler.net/",
- bankAccessApiBaseUrl: "https://bank.test.taler.net/",
+ await withdrawTestBalance(wex, {
+ amount: "TESTKUDOS:10" as AmountString,
+ corebankApiBaseUrl: "https://bank.test.taler.net/",
exchangeBaseUrl: "https://exchange.test.taler.net/",
});
return {
- versionInfo: getVersion(ws),
+ versionInfo: getVersion(wex),
};
}
case WalletApiOperation.WithdrawTestBalance: {
const req = codecForWithdrawTestBalance().decode(payload);
- await withdrawTestBalance(ws, req);
+ await withdrawTestBalance(wex, req);
return {};
}
+ case WalletApiOperation.TestingListTaskForTransaction: {
+ const req =
+ codecForTestingListTasksForTransactionRequest().decode(payload);
+ return {
+ taskIdList: listTaskForTransactionId(req.transactionId),
+ } satisfies TestingListTasksForTransactionsResponse;
+ }
case WalletApiOperation.RunIntegrationTest: {
const req = codecForIntegrationTestArgs().decode(payload);
- await runIntegrationTest(ws, req);
+ await runIntegrationTest(wex, req);
return {};
}
+ case WalletApiOperation.RunIntegrationTestV2: {
+ const req = codecForIntegrationTestV2Args().decode(payload);
+ await runIntegrationTest2(wex, req);
+ return {};
+ }
+ case WalletApiOperation.ValidateIban: {
+ const req = codecForValidateIbanRequest().decode(payload);
+ const valRes = validateIban(req.iban);
+ const resp: ValidateIbanResponse = {
+ valid: valRes.type === "valid",
+ };
+ return resp;
+ }
case WalletApiOperation.TestPay: {
const req = codecForTestPayArgs().decode(payload);
- return await testPay(ws, req);
+ return await testPay(wex, req);
}
case WalletApiOperation.GetTransactions: {
const req = codecForTransactionsRequest().decode(payload);
- return await getTransactions(ws, req);
+ return await getTransactions(wex, req);
}
case WalletApiOperation.GetTransactionById: {
const req = codecForTransactionByIdRequest().decode(payload);
- return await getTransactionById(ws, req);
+ return await getTransactionById(wex, req);
+ }
+ case WalletApiOperation.GetWithdrawalTransactionByUri: {
+ const req = codecForGetWithdrawalDetailsForUri().decode(payload);
+ return await getWithdrawalTransactionByUri(wex, req);
}
case WalletApiOperation.AddExchange: {
const req = codecForAddExchangeRequest().decode(payload);
- await updateExchangeFromUrl(ws, req.exchangeBaseUrl, {
- forceNow: req.forceUpdate,
+ await fetchFreshExchange(wex, req.exchangeBaseUrl, {
+ expectedMasterPub: req.masterPub,
});
return {};
}
+ case WalletApiOperation.TestingPing: {
+ return {};
+ }
+ case WalletApiOperation.UpdateExchangeEntry: {
+ const req = codecForUpdateExchangeEntryRequest().decode(payload);
+ await fetchFreshExchange(wex, req.exchangeBaseUrl, {
+ forceUpdate: !!req.force,
+ });
+ return {};
+ }
+ case WalletApiOperation.TestingGetDenomStats: {
+ const req = codecForTestingGetDenomStatsRequest().decode(payload);
+ const denomStats: TestingGetDenomStatsResponse = {
+ numKnown: 0,
+ numLost: 0,
+ numOffered: 0,
+ };
+ 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;
+ }
case WalletApiOperation.ListExchanges: {
- return await getExchanges(ws);
+ return await listExchanges(wex);
+ }
+ case WalletApiOperation.GetExchangeEntryByUrl: {
+ const req = codecForGetExchangeEntryByUrlRequest().decode(payload);
+ return lookupExchangeByUri(wex, req);
+ }
+ case WalletApiOperation.ListExchangesForScopedCurrency: {
+ const req =
+ codecForListExchangesForScopedCurrencyRequest().decode(payload);
+ const exchangesResp = await listExchanges(wex);
+ const result: ExchangesShortListResponse = {
+ exchanges: [],
+ };
+ // Right now we only filter on the currency, as wallet-core doesn't
+ // fully support scoped currencies yet.
+ for (const exch of exchangesResp.exchanges) {
+ if (exch.currency === req.scope.currency) {
+ result.exchanges.push({
+ exchangeBaseUrl: exch.exchangeBaseUrl,
+ });
+ }
+ }
+ return result;
}
case WalletApiOperation.GetExchangeDetailedInfo: {
const req = codecForAddExchangeRequest().decode(payload);
- return await getExchangeDetailedInfo(ws, req.exchangeBaseUrl);
+ return await getExchangeDetailedInfo(wex, req.exchangeBaseUrl);
}
case WalletApiOperation.ListKnownBankAccounts: {
const req = codecForListKnownBankAccounts().decode(payload);
- return await listKnownBankAccounts(ws, req.currency);
+ return await listKnownBankAccounts(wex, req.currency);
}
case WalletApiOperation.AddKnownBankAccounts: {
const req = codecForAddKnownBankAccounts().decode(payload);
- await addKnownBankAccounts(ws, req.payto, req.alias, req.currency);
+ await addKnownBankAccounts(wex, req.payto, req.alias, req.currency);
return {};
}
case WalletApiOperation.ForgetKnownBankAccounts: {
const req = codecForForgetKnownBankAccounts().decode(payload);
- await forgetKnownBankAccounts(ws, req.payto);
+ await forgetKnownBankAccounts(wex, req.payto);
return {};
}
case WalletApiOperation.GetWithdrawalDetailsForUri: {
const req = codecForGetWithdrawalDetailsForUri().decode(payload);
- return await getWithdrawalDetailsForUri(ws, req.talerWithdrawUri);
+ return await getWithdrawalDetailsForUri(wex, req.talerWithdrawUri, {
+ notifyChangeFromPendingTimeoutMs: req.notifyChangeFromPendingTimeoutMs,
+ restrictAge: req.restrictAge,
+ });
}
case WalletApiOperation.AcceptManualWithdrawal: {
- const req = codecForAcceptManualWithdrawalRequet().decode(payload);
- const res = await createManualWithdrawal(ws, {
+ const req = codecForAcceptManualWithdrawalRequest().decode(payload);
+ const res = await createManualWithdrawal(wex, {
amount: Amounts.parseOrThrow(req.amount),
exchangeBaseUrl: req.exchangeBaseUrl,
restrictAge: req.restrictAge,
@@ -1086,56 +915,48 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
case WalletApiOperation.GetWithdrawalDetailsForAmount: {
const req =
codecForGetWithdrawalDetailsForAmountRequest().decode(payload);
- const wi = await getExchangeWithdrawalInfo(
- ws,
- req.exchangeBaseUrl,
- Amounts.parseOrThrow(req.amount),
- req.restrictAge,
- );
- const resp: ManualWithdrawalDetails = {
- amountRaw: req.amount,
- amountEffective: Amounts.stringify(wi.selectedDenoms.totalCoinValue),
- paytoUris: wi.exchangePaytoUris,
- tosAccepted: wi.termsOfServiceAccepted,
- ageRestrictionOptions: wi.ageRestrictionOptions,
- };
+ const resp = await getWithdrawalDetailsForAmount(wex, cts, req);
return resp;
}
case WalletApiOperation.GetBalances: {
- return await getBalances(ws);
+ return await getBalances(wex);
+ }
+ case WalletApiOperation.GetBalanceDetail: {
+ const req = codecForGetBalanceDetailRequest().decode(payload);
+ return await getBalanceDetail(wex, req);
}
case WalletApiOperation.GetUserAttentionRequests: {
const req = codecForUserAttentionsRequest().decode(payload);
- return await getUserAttentions(ws, req);
+ return await getUserAttentions(wex, req);
}
case WalletApiOperation.MarkAttentionRequestAsRead: {
const req = codecForUserAttentionByIdRequest().decode(payload);
- return await markAttentionRequestAsRead(ws, req);
+ return await markAttentionRequestAsRead(wex, req);
}
case WalletApiOperation.GetUserAttentionUnreadCount: {
const req = codecForUserAttentionsRequest().decode(payload);
- return await getUserAttentionsUnreadCount(ws, req);
+ return await getUserAttentionsUnreadCount(wex, req);
}
case WalletApiOperation.GetPendingOperations: {
- return await getPendingOperations(ws);
+ // FIXME: Eventually remove the handler after deprecation period.
+ return {
+ pendingOperations: [],
+ } satisfies PendingOperationsResponse;
}
case WalletApiOperation.SetExchangeTosAccepted: {
const req = codecForAcceptExchangeTosRequest().decode(payload);
- await acceptExchangeTermsOfService(ws, req.exchangeBaseUrl, req.etag);
+ await acceptExchangeTermsOfService(wex, req.exchangeBaseUrl);
return {};
}
- case WalletApiOperation.ApplyRefund: {
- const req = codecForApplyRefundRequest().decode(payload);
- return await applyRefund(ws, req.talerRefundUri);
- }
- case WalletApiOperation.ApplyRefundFromPurchaseId: {
- const req = codecForApplyRefundFromPurchaseIdRequest().decode(payload);
- return await applyRefundFromPurchaseId(ws, req.purchaseId);
+ case WalletApiOperation.SetExchangeTosForgotten: {
+ const req = codecForAcceptExchangeTosRequest().decode(payload);
+ await forgetExchangeTermsOfService(wex, req.exchangeBaseUrl);
+ return {};
}
case WalletApiOperation.AcceptBankIntegratedWithdrawal: {
const req =
codecForAcceptBankIntegratedWithdrawalRequest().decode(payload);
- return await acceptWithdrawalFromUri(ws, {
+ return await acceptWithdrawalFromUri(wex, {
selectedExchange: req.exchangeBaseUrl,
talerWithdrawUri: req.talerWithdrawUri,
forcedDenomSel: req.forcedDenomSel,
@@ -1144,274 +965,530 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
}
case WalletApiOperation.GetExchangeTos: {
const req = codecForGetExchangeTosRequest().decode(payload);
- return getExchangeTos(ws, req.exchangeBaseUrl, req.acceptedFormat);
+ return getExchangeTos(
+ wex,
+ req.exchangeBaseUrl,
+ req.acceptedFormat,
+ req.acceptLanguage,
+ );
}
case WalletApiOperation.GetContractTermsDetails: {
const req = codecForGetContractTermsDetails().decode(payload);
- return getContractTermsDetails(ws, req.proposalId);
+ 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");
}
case WalletApiOperation.RetryPendingNow: {
- await runPending(ws, true);
+ logger.error("retryPendingNow currently not implemented");
return {};
}
- // FIXME: Deprecate one of the aliases!
+ case WalletApiOperation.SharePayment: {
+ const req = codecForSharePaymentRequest().decode(payload);
+ return await sharePayment(wex, req.merchantBaseUrl, req.orderId);
+ }
+ case WalletApiOperation.PrepareWithdrawExchange: {
+ const req = codecForPrepareWithdrawExchangeRequest().decode(payload);
+ return handlePrepareWithdrawExchange(wex, req);
+ }
case WalletApiOperation.PreparePayForUri: {
const req = codecForPreparePayRequest().decode(payload);
- return await preparePayForUri(ws, req.talerPayUri);
+ return await preparePayForUri(wex, req.talerPayUri);
+ }
+ case WalletApiOperation.PreparePayForTemplate: {
+ const req = codecForPreparePayTemplateRequest().decode(payload);
+ return preparePayForTemplate(wex, req);
}
case WalletApiOperation.ConfirmPay: {
const req = codecForConfirmPayRequest().decode(payload);
- return await confirmPay(ws, req.proposalId, req.sessionId);
+ 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);
}
- case WalletApiOperation.AbortFailedPayWithRefund: {
- const req = codecForAbortPayWithRefundRequest().decode(payload);
- await abortFailedPayWithRefund(ws, req.proposalId);
+ case WalletApiOperation.AbortTransaction: {
+ const req = codecForAbortTransaction().decode(payload);
+ await abortTransaction(wex, req.transactionId);
+ return {};
+ }
+ case WalletApiOperation.SuspendTransaction: {
+ const req = codecForSuspendTransaction().decode(payload);
+ await suspendTransaction(wex, req.transactionId);
+ return {};
+ }
+ case WalletApiOperation.GetActiveTasks: {
+ const allTasksId = wex.taskScheduler.getActiveTasks();
+
+ const tasksInfo = await Promise.all(
+ allTasksId.map(async (id) => {
+ return await wex.db.runReadOnlyTx(
+ { storeNames: ["operationRetries"] },
+ async (tx) => {
+ return tx.operationRetries.get(id);
+ },
+ );
+ }),
+ );
+
+ const tasks = allTasksId.map((taskId, i): ActiveTask => {
+ const transaction = convertTaskToTransactionId(taskId);
+ const d = tasksInfo[i];
+
+ const firstTry = !d
+ ? undefined
+ : timestampAbsoluteFromDb(d.retryInfo.firstTry);
+ const nextTry = !d
+ ? undefined
+ : timestampAbsoluteFromDb(d.retryInfo.nextRetry);
+ const counter = d?.retryInfo.retryCounter;
+ const lastError = d?.lastError;
+
+ return {
+ taskId: taskId,
+ retryCounter: counter,
+ firstTry,
+ nextTry,
+ lastError,
+ transaction,
+ };
+ });
+ return { tasks };
+ }
+ case WalletApiOperation.FailTransaction: {
+ const req = codecForFailTransactionRequest().decode(payload);
+ await failTransaction(wex, req.transactionId);
+ return {};
+ }
+ case WalletApiOperation.ResumeTransaction: {
+ const req = codecForResumeTransaction().decode(payload);
+ await resumeTransaction(wex, req.transactionId);
return {};
}
case WalletApiOperation.DumpCoins: {
- return await dumpCoins(ws);
+ return await dumpCoins(wex);
}
case WalletApiOperation.SetCoinSuspended: {
const req = codecForSetCoinSuspendedRequest().decode(payload);
- await setCoinSuspended(ws, req.coinPub, req.suspended);
+ await setCoinSuspended(wex, req.coinPub, req.suspended);
return {};
}
+ case WalletApiOperation.TestingGetSampleTransactions:
+ return { transactions: sampleWalletCoreTransactions };
case WalletApiOperation.ForceRefresh: {
const req = codecForForceRefreshRequest().decode(payload);
- const refreshGroupId = await ws.db
- .mktx((x) => [
- x.refreshGroups,
- x.coinAvailability,
- x.denominations,
- x.coins,
- ])
- .runReadWrite(async (tx) => {
- let coinPubs: CoinRefreshRequest[] = [];
- for (const c of req.coinPubList) {
- const coin = await tx.coins.get(c);
- if (!coin) {
- throw Error(`coin (pubkey ${c}) not found`);
- }
- const denom = await ws.getDenomInfo(
- ws,
- tx,
- coin.exchangeBaseUrl,
- coin.denomPubHash,
- );
- checkDbInvariant(!!denom);
- coinPubs.push({
- coinPub: c,
- amount: denom?.value,
- });
- }
- return await createRefreshGroup(
- ws,
- tx,
- coinPubs,
- RefreshReason.Manual,
- );
- });
- processRefreshGroup(ws, refreshGroupId.refreshGroupId, {
- forceNow: true,
- }).catch((x) => {
- logger.error(x);
- });
- return {
- refreshGroupId,
- };
- }
- case WalletApiOperation.PrepareTip: {
- const req = codecForPrepareTipRequest().decode(payload);
- return await prepareTip(ws, req.talerTipUri);
+ return await forceRefresh(wex, req);
}
- case WalletApiOperation.PrepareRefund: {
+ case WalletApiOperation.StartRefundQueryForUri: {
const req = codecForPrepareRefundRequest().decode(payload);
- return await prepareRefund(ws, req.talerRefundUri);
+ return await startRefundQueryForUri(wex, req.talerRefundUri);
}
- case WalletApiOperation.AcceptTip: {
- const req = codecForAcceptTipRequest().decode(payload);
- return await acceptTip(ws, req.walletTipId);
- }
- case WalletApiOperation.ExportBackupPlain: {
- return exportBackup(ws);
+ case WalletApiOperation.StartRefundQuery: {
+ const req = codecForStartRefundQueryRequest().decode(payload);
+ const txIdParsed = parseTransactionIdentifier(req.transactionId);
+ if (!txIdParsed) {
+ throw Error("invalid transaction ID");
+ }
+ if (txIdParsed.tag !== TransactionType.Payment) {
+ throw Error("expected payment transaction ID");
+ }
+ await startQueryRefund(wex, txIdParsed.proposalId);
+ return {};
}
case WalletApiOperation.AddBackupProvider: {
const req = codecForAddBackupProviderRequest().decode(payload);
- return await addBackupProvider(ws, req);
+ return await addBackupProvider(wex, req);
}
case WalletApiOperation.RunBackupCycle: {
const req = codecForRunBackupCycle().decode(payload);
- await runBackupCycle(ws, req);
+ await runBackupCycle(wex, req);
return {};
}
case WalletApiOperation.RemoveBackupProvider: {
const req = codecForRemoveBackupProvider().decode(payload);
- await removeBackupProvider(ws, req);
+ await removeBackupProvider(wex, req);
return {};
}
case WalletApiOperation.ExportBackupRecovery: {
- const resp = await getBackupRecovery(ws);
+ const resp = await getBackupRecovery(wex);
return resp;
}
+ case WalletApiOperation.TestingWaitTransactionState: {
+ const req = payload as TestingWaitTransactionRequest;
+ await waitTransactionState(wex, req.transactionId, req.txState);
+ return {};
+ }
+ case WalletApiOperation.GetCurrencySpecification: {
+ // Ignore result, just validate in this mock implementation
+ const req = codecForGetCurrencyInfoRequest().decode(payload);
+ // Hard-coded mock for KUDOS and TESTKUDOS
+ if (req.scope.currency === "KUDOS") {
+ const kudosResp: GetCurrencySpecificationResponse = {
+ currencySpecification: {
+ name: "Kudos (Taler Demonstrator)",
+ num_fractional_input_digits: 2,
+ num_fractional_normal_digits: 2,
+ num_fractional_trailing_zero_digits: 2,
+ alt_unit_names: {
+ "0": "ク",
+ },
+ },
+ };
+ return kudosResp;
+ } else if (req.scope.currency === "TESTKUDOS") {
+ const testkudosResp: GetCurrencySpecificationResponse = {
+ currencySpecification: {
+ name: "Test (Taler Unstable Demonstrator)",
+ num_fractional_input_digits: 0,
+ num_fractional_normal_digits: 0,
+ num_fractional_trailing_zero_digits: 0,
+ alt_unit_names: {
+ "0": "テ",
+ },
+ },
+ };
+ return testkudosResp;
+ }
+ const defaultResp: GetCurrencySpecificationResponse = {
+ currencySpecification: {
+ name: req.scope.currency,
+ num_fractional_input_digits: 2,
+ num_fractional_normal_digits: 2,
+ num_fractional_trailing_zero_digits: 2,
+ alt_unit_names: {
+ "0": req.scope.currency,
+ },
+ },
+ };
+ return defaultResp;
+ }
case WalletApiOperation.ImportBackupRecovery: {
const req = codecForAny().decode(payload);
- await loadBackupRecovery(ws, req);
+ await loadBackupRecovery(wex, req);
return {};
}
+ // case WalletApiOperation.GetPlanForOperation: {
+ // const req = codecForGetPlanForOperationRequest().decode(payload);
+ // return await getPlanForOperation(ws, req);
+ // }
+ case WalletApiOperation.ConvertDepositAmount: {
+ const req = codecForConvertAmountRequest.decode(payload);
+ return await convertDepositAmount(wex, req);
+ }
+ case WalletApiOperation.GetMaxDepositAmount: {
+ const req = codecForGetAmountRequest.decode(payload);
+ return await getMaxDepositAmount(wex, req);
+ }
+ case WalletApiOperation.ConvertPeerPushAmount: {
+ const req = codecForConvertAmountRequest.decode(payload);
+ return await convertPeerPushAmount(wex, req);
+ }
+ case WalletApiOperation.GetMaxPeerPushAmount: {
+ const req = codecForGetAmountRequest.decode(payload);
+ return await getMaxPeerPushAmount(wex, req);
+ }
+ case WalletApiOperation.ConvertWithdrawalAmount: {
+ const req = codecForConvertAmountRequest.decode(payload);
+ return await convertWithdrawalAmount(wex, req);
+ }
case WalletApiOperation.GetBackupInfo: {
- const resp = await getBackupInfo(ws);
+ const resp = await getBackupInfo(wex);
return resp;
}
- case WalletApiOperation.GetFeeForDeposit: {
- const req = codecForGetFeeForDeposit().decode(payload);
- return await getFeeForDeposit(ws, req);
- }
case WalletApiOperation.PrepareDeposit: {
const req = codecForPrepareDepositRequest().decode(payload);
- return await prepareDepositGroup(ws, req);
+ return await checkDepositGroup(wex, req);
}
+ case WalletApiOperation.GenerateDepositGroupTxId:
+ return {
+ transactionId: generateDepositGroupTxId(),
+ };
case WalletApiOperation.CreateDepositGroup: {
const req = codecForCreateDepositGroupRequest().decode(payload);
- return await createDepositGroup(ws, req);
- }
- case WalletApiOperation.TrackDepositGroup: {
- const req = codecForTrackDepositGroupRequest().decode(payload);
- return trackDepositGroup(ws, req);
+ return await createDepositGroup(wex, req);
}
case WalletApiOperation.DeleteTransaction: {
const req = codecForDeleteTransactionRequest().decode(payload);
- await deleteTransaction(ws, req.transactionId);
+ await deleteTransaction(wex, req.transactionId);
return {};
}
case WalletApiOperation.RetryTransaction: {
const req = codecForRetryTransactionRequest().decode(payload);
- await retryTransaction(ws, req.transactionId);
+ await retryTransaction(wex, req.transactionId);
return {};
}
case WalletApiOperation.SetWalletDeviceId: {
const req = codecForSetWalletDeviceIdRequest().decode(payload);
- await setWalletDeviceId(ws, req.walletDeviceId);
+ await setWalletDeviceId(wex, req.walletDeviceId);
return {};
}
- case WalletApiOperation.ListCurrencies: {
- return await ws.db
- .mktx((x) => [x.auditorTrust, x.exchangeTrust])
- .runReadOnly(async (tx) => {
- const trustedAuditors = await tx.auditorTrust.iter().toArray();
- const trustedExchanges = await tx.exchangeTrust.iter().toArray();
- return {
- trustedAuditors: trustedAuditors.map((x) => ({
- currency: x.currency,
- auditorBaseUrl: x.auditorBaseUrl,
- auditorPub: x.auditorPub,
- })),
- trustedExchanges: trustedExchanges.map((x) => ({
- currency: x.currency,
- exchangeBaseUrl: x.exchangeBaseUrl,
- exchangeMasterPub: x.exchangeMasterPub,
- })),
- };
- });
+ case WalletApiOperation.TestCrypto: {
+ return await wex.cryptoApi.hashString({ str: "hello world" });
+ }
+ case WalletApiOperation.ClearDb: {
+ wex.ws.clearAllCaches();
+ await clearDatabase(wex.db.idbHandle());
+ return {};
}
- case WalletApiOperation.WithdrawFakebank: {
- const req = codecForWithdrawFakebankRequest().decode(payload);
- const amount = Amounts.parseOrThrow(req.amount);
- const details = await getExchangeWithdrawalInfo(
- ws,
- req.exchange,
- amount,
- undefined,
+ case WalletApiOperation.Recycle: {
+ throw Error("not implemented");
+ return {};
+ }
+ case WalletApiOperation.ExportDb: {
+ const dbDump = await exportDb(wex.ws.idb);
+ return dbDump;
+ }
+ case WalletApiOperation.ListGlobalCurrencyExchanges: {
+ const resp: ListGlobalCurrencyExchangesResponse = {
+ exchanges: [],
+ };
+ await wex.db.runReadOnlyTx(
+ { storeNames: ["globalCurrencyExchanges"] },
+ async (tx) => {
+ const gceList = await tx.globalCurrencyExchanges.iter().toArray();
+ for (const gce of gceList) {
+ resp.exchanges.push({
+ currency: gce.currency,
+ exchangeBaseUrl: gce.exchangeBaseUrl,
+ exchangeMasterPub: gce.exchangeMasterPub,
+ });
+ }
+ },
);
- const wres = await createManualWithdrawal(ws, {
- amount: amount,
- exchangeBaseUrl: req.exchange,
- });
- const paytoUri = details.exchangePaytoUris[0];
- const pt = parsePaytoUri(paytoUri);
- if (!pt) {
- throw Error("failed to parse payto URI");
- }
- const components = pt.targetPath.split("/");
- const creditorAcct = components[components.length - 1];
- logger.info(`making testbank transfer to '${creditorAcct}'`);
- const fbReq = await ws.http.postJson(
- new URL(`${creditorAcct}/admin/add-incoming`, req.bank).href,
- {
- amount: Amounts.stringify(amount),
- reserve_pub: wres.reservePub,
- debit_account: "payto://x-taler-bank/localhost/testdebtor",
+ return resp;
+ }
+ case WalletApiOperation.ListGlobalCurrencyAuditors: {
+ const resp: ListGlobalCurrencyAuditorsResponse = {
+ auditors: [],
+ };
+ await wex.db.runReadOnlyTx(
+ { storeNames: ["globalCurrencyAuditors"] },
+ async (tx) => {
+ const gcaList = await tx.globalCurrencyAuditors.iter().toArray();
+ for (const gca of gcaList) {
+ resp.auditors.push({
+ currency: gca.currency,
+ auditorBaseUrl: gca.auditorBaseUrl,
+ auditorPub: gca.auditorPub,
+ });
+ }
+ },
+ );
+ return resp;
+ }
+ case WalletApiOperation.AddGlobalCurrencyExchange: {
+ const req = codecForAddGlobalCurrencyExchangeRequest().decode(payload);
+ await wex.db.runReadWriteTx(
+ { storeNames: ["globalCurrencyExchanges"] },
+ async (tx) => {
+ const key = [
+ req.currency,
+ req.exchangeBaseUrl,
+ req.exchangeMasterPub,
+ ];
+ const existingRec =
+ await tx.globalCurrencyExchanges.indexes.byCurrencyAndUrlAndPub.get(
+ key,
+ );
+ if (existingRec) {
+ return;
+ }
+ wex.ws.exchangeCache.clear();
+ await tx.globalCurrencyExchanges.add({
+ currency: req.currency,
+ exchangeBaseUrl: req.exchangeBaseUrl,
+ exchangeMasterPub: req.exchangeMasterPub,
+ });
},
);
- const fbResp = await readSuccessResponseJsonOrThrow(fbReq, codecForAny());
- logger.info(`started fakebank withdrawal: ${j2s(fbResp)}`);
return {};
}
- case WalletApiOperation.TestCrypto: {
- return await ws.cryptoApi.hashString({ str: "hello world" });
+ 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);
+ await tx.globalCurrencyExchanges.delete(existingRec.id);
+ },
+ );
+ return {};
}
- case WalletApiOperation.ClearDb:
- await clearDatabase(ws.db.idbHandle());
+ case WalletApiOperation.AddGlobalCurrencyAuditor: {
+ const req = codecForAddGlobalCurrencyAuditorRequest().decode(payload);
+ await wex.db.runReadWriteTx(
+ { storeNames: ["globalCurrencyAuditors"] },
+ async (tx) => {
+ const key = [req.currency, req.auditorBaseUrl, req.auditorPub];
+ const existingRec =
+ await tx.globalCurrencyAuditors.indexes.byCurrencyAndUrlAndPub.get(
+ key,
+ );
+ if (existingRec) {
+ return;
+ }
+ await tx.globalCurrencyAuditors.add({
+ currency: req.currency,
+ auditorBaseUrl: req.auditorBaseUrl,
+ auditorPub: req.auditorPub,
+ });
+ wex.ws.exchangeCache.clear();
+ },
+ );
return {};
- case WalletApiOperation.Recycle: {
- const backup = await exportBackup(ws);
- await clearDatabase(ws.db.idbHandle());
- await importBackupPlain(ws, backup);
+ }
+ case WalletApiOperation.TestingWaitTasksDone: {
+ await waitTasksDone(wex);
return {};
}
- case WalletApiOperation.ExportDb: {
- const dbDump = await exportDb(ws.db.idbHandle());
- return dbDump;
+ 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);
+ await tx.globalCurrencyAuditors.delete(existingRec.id);
+ wex.ws.exchangeCache.clear();
+ },
+ );
+ return {};
}
case WalletApiOperation.ImportDb: {
const req = codecForImportDbRequest().decode(payload);
- await importDb(ws.db.idbHandle(), req.dump);
+ await importDb(wex.db.idbHandle(), req.dump);
return [];
}
- case WalletApiOperation.PreparePeerPushPayment: {
- const req = codecForPreparePeerPushPaymentRequest().decode(payload);
- return await preparePeerPushPayment(ws, req);
+ case WalletApiOperation.CheckPeerPushDebit: {
+ const req = codecForCheckPeerPushDebitRequest().decode(payload);
+ return await checkPeerPushDebit(wex, req);
}
- case WalletApiOperation.InitiatePeerPushPayment: {
- const req = codecForInitiatePeerPushPaymentRequest().decode(payload);
- return await initiatePeerToPeerPush(ws, req);
+ case WalletApiOperation.InitiatePeerPushDebit: {
+ const req = codecForInitiatePeerPushDebitRequest().decode(payload);
+ return await initiatePeerPushDebit(wex, req);
}
- case WalletApiOperation.CheckPeerPushPayment: {
- const req = codecForCheckPeerPushPaymentRequest().decode(payload);
- return await checkPeerPushPayment(ws, req);
+ case WalletApiOperation.PreparePeerPushCredit: {
+ const req = codecForPreparePeerPushCreditRequest().decode(payload);
+ return await preparePeerPushCredit(wex, req);
}
- case WalletApiOperation.AcceptPeerPushPayment: {
- const req = codecForAcceptPeerPushPaymentRequest().decode(payload);
- return await acceptPeerPushPayment(ws, req);
+ case WalletApiOperation.ConfirmPeerPushCredit: {
+ const req = codecForConfirmPeerPushPaymentRequest().decode(payload);
+ return await confirmPeerPushCredit(wex, req);
}
- case WalletApiOperation.PreparePeerPullPayment: {
+ case WalletApiOperation.CheckPeerPullCredit: {
const req = codecForPreparePeerPullPaymentRequest().decode(payload);
- return await preparePeerPullPayment(ws, req);
+ return await checkPeerPullPaymentInitiation(wex, req);
}
- case WalletApiOperation.InitiatePeerPullPayment: {
+ case WalletApiOperation.InitiatePeerPullCredit: {
const req = codecForInitiatePeerPullPaymentRequest().decode(payload);
- return await initiatePeerPullPayment(ws, req);
+ return await initiatePeerPullPayment(wex, req);
}
- case WalletApiOperation.CheckPeerPullPayment: {
+ case WalletApiOperation.PreparePeerPullDebit: {
const req = codecForCheckPeerPullPaymentRequest().decode(payload);
- return await checkPeerPullPayment(ws, req);
+ return await preparePeerPullDebit(wex, req);
}
- case WalletApiOperation.AcceptPeerPullPayment: {
+ case WalletApiOperation.ConfirmPeerPullDebit: {
const req = codecForAcceptPeerPullPaymentRequest().decode(payload);
- return await acceptPeerPullPayment(ws, req);
+ return await confirmPeerPullDebit(wex, req);
}
case WalletApiOperation.ApplyDevExperiment: {
const req = codecForApplyDevExperiment().decode(payload);
- await applyDevExperiment(ws, req.devExperimentUri);
+ await applyDevExperiment(wex, req.devExperimentUri);
return {};
}
- case WalletApiOperation.SetDevMode: {
- const req = codecForSetDevModeRequest().decode(payload);
- await setDevMode(ws, req.devModeEnabled);
+ case WalletApiOperation.GetVersion: {
+ return getVersion(wex);
+ }
+ case WalletApiOperation.TestingWaitTransactionsFinal:
+ return await waitUntilAllTransactionsFinal(wex);
+ case WalletApiOperation.TestingWaitRefreshesFinal:
+ return await waitUntilRefreshesDone(wex);
+ case WalletApiOperation.TestingSetTimetravel: {
+ const req = codecForTestingSetTimetravelRequest().decode(payload);
+ setDangerousTimetravel(req.offsetMs);
+ await wex.taskScheduler.reload();
return {};
}
- case WalletApiOperation.GetVersion: {
- return getVersion(ws);
+ case WalletApiOperation.DeleteExchange: {
+ const req = codecForDeleteExchangeRequest().decode(payload);
+ await deleteExchange(wex, req);
+ return {};
+ }
+ case WalletApiOperation.GetExchangeResources: {
+ const req = codecForGetExchangeResourcesRequest().decode(payload);
+ return await getExchangeResources(wex, req.exchangeBaseUrl);
+ }
+ case WalletApiOperation.TestingInfiniteTransactionLoop: {
+ const myDelayMs = (payload as any).delayMs ?? 5;
+ const shouldFetch = !!(payload as any).shouldFetch;
+ const doFetch = async () => {
+ while (1) {
+ const url =
+ "https://exchange.demo.taler.net/reserves/01PMMB9PJN0QBWAFBXV6R0KNJJMAKXCV4D6FDG0GJFDJQXGYP32G?timeout_ms=30000";
+ logger.info(`fetching ${url}`);
+ const res = await wex.http.fetch(url);
+ logger.info(`fetch result ${res.status}`);
+ }
+ };
+ if (shouldFetch) {
+ // In the background!
+ doFetch();
+ }
+ let loopCount = 0;
+ while (true) {
+ logger.info(`looping test write tx, iteration ${loopCount}`);
+ await wex.db.runReadWriteTx({ storeNames: ["config"] }, async (tx) => {
+ await tx.config.put({
+ key: ConfigRecordKey.TestLoopTx,
+ value: loopCount,
+ });
+ });
+ if (myDelayMs != 0) {
+ await new Promise<void>((resolve, reject) => {
+ setTimeout(() => resolve(), myDelayMs);
+ });
+ }
+ loopCount = (loopCount + 1) % (Number.MAX_SAFE_INTEGER - 1);
+ }
}
+ // default:
+ // assertUnreachable(operation);
}
throw TalerError.fromDetail(
TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN,
@@ -1422,29 +1499,113 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
);
}
-export function getVersion(ws: InternalWalletState): WalletCoreVersion {
- const version: WalletCoreVersion = {
- hash: GIT_HASH,
- version: VERSION,
+export function getVersion(wex: WalletExecutionContext): WalletCoreVersion {
+ const result: WalletCoreVersion = {
+ implementationSemver: walletCoreBuildInfo.implementationSemver,
+ implementationGitHash: walletCoreBuildInfo.implementationGitHash,
+ hash: undefined,
+ version: WALLET_CORE_API_PROTOCOL_VERSION,
exchange: WALLET_EXCHANGE_PROTOCOL_VERSION,
merchant: WALLET_MERCHANT_PROTOCOL_VERSION,
+ bankConversionApiRange: WALLET_BANK_CONVERSION_API_PROTOCOL_VERSION,
+ bankIntegrationApiRange: WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
+ corebankApiRange: WALLET_COREBANK_API_PROTOCOL_VERSION,
bank: WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
- devMode: ws.devModeActive,
+ devMode: wex.ws.config.testing.devModeActive,
+ };
+ return result;
+}
+
+export function getObservedWalletExecutionContext(
+ ws: InternalWalletState,
+ cancellationToken: CancellationToken,
+ oc: ObservabilityContext,
+): WalletExecutionContext {
+ const wex: WalletExecutionContext = {
+ ws,
+ cancellationToken,
+ cryptoApi: observeTalerCrypto(ws.cryptoApi, oc),
+ db: new ObservableDbAccess(ws.db, oc),
+ http: new ObservableHttpClientLibrary(ws.http, oc),
+ taskScheduler: new ObservableTaskScheduler(ws.taskScheduler, oc),
+ oc,
};
- return version;
+ return wex;
+}
+
+export function getNormalWalletExecutionContext(
+ ws: InternalWalletState,
+ cancellationToken: CancellationToken,
+ oc: ObservabilityContext,
+): WalletExecutionContext {
+ const wex: WalletExecutionContext = {
+ ws,
+ cancellationToken,
+ cryptoApi: ws.cryptoApi,
+ db: ws.db,
+ get http() {
+ if (ws.initCalled) {
+ return ws.http;
+ }
+ throw Error("wallet not initialized");
+ },
+ taskScheduler: ws.taskScheduler,
+ oc,
+ };
+ return wex;
}
/**
* Handle a request to the wallet-core API.
*/
-export async function handleCoreApiRequest(
+async function handleCoreApiRequest(
ws: InternalWalletState,
operation: string,
id: string,
payload: unknown,
): Promise<CoreApiResponse> {
+ let wex: WalletExecutionContext;
+ let oc: ObservabilityContext;
+
+ const cts = CancellationToken.create();
+
+ if (ws.initCalled && ws.config.testing.emitObservabilityEvents) {
+ oc = {
+ observe(evt) {
+ ws.notify({
+ type: NotificationType.RequestObservabilityEvent,
+ operation,
+ requestId: id,
+ event: evt,
+ });
+ },
+ };
+
+ wex = getObservedWalletExecutionContext(ws, cts.token, oc);
+ } else {
+ oc = {
+ observe(evt) {},
+ };
+ wex = getNormalWalletExecutionContext(ws, cts.token, oc);
+ }
+
try {
- const result = await dispatchRequestInternal(ws, operation as any, payload);
+ const start = performanceNow();
+ await ws.ensureWalletDbOpen();
+ oc.observe({
+ type: ObservabilityEventType.RequestStart,
+ });
+ const result = await dispatchRequestInternal(
+ wex,
+ cts,
+ operation as any,
+ payload,
+ );
+ const end = performanceNow();
+ oc.observe({
+ type: ObservabilityEventType.RequestFinishSuccess,
+ durationMs: Number((end - start) / 1000n / 1000n),
+ });
return {
type: "response",
operation,
@@ -1453,7 +1614,12 @@ export async function handleCoreApiRequest(
};
} catch (e: any) {
const err = getErrorDetailFromException(e);
- logger.info(`finished wallet core request with error: ${j2s(err)}`);
+ logger.info(
+ `finished wallet core request ${operation} with error: ${j2s(err)}`,
+ );
+ oc.observe({
+ type: ObservabilityEventType.RequestFinishError,
+ });
return {
type: "error",
operation,
@@ -1463,6 +1629,34 @@ export async function handleCoreApiRequest(
}
}
+export function applyRunConfigDefaults(
+ wcp?: PartialWalletRunConfig,
+): WalletRunConfig {
+ return {
+ builtin: {
+ exchanges: wcp?.builtin?.exchanges ?? [
+ {
+ exchangeBaseUrl: "https://exchange.demo.taler.net/",
+ currencyHint: "KUDOS",
+ },
+ ],
+ },
+ features: {
+ allowHttp: wcp?.features?.allowHttp ?? false,
+ },
+ testing: {
+ denomselAllowLate: wcp?.testing?.denomselAllowLate ?? false,
+ devModeActive: wcp?.testing?.devModeActive ?? false,
+ insecureTrustExchange: wcp?.testing?.insecureTrustExchange ?? false,
+ preventThrottling: wcp?.testing?.preventThrottling ?? false,
+ skipDefaults: wcp?.testing?.skipDefaults ?? false,
+ emitObservabilityEvents: wcp?.testing?.emitObservabilityEvents ?? false,
+ },
+ };
+}
+
+export type HttpFactory = (config: WalletRunConfig) => HttpRequestLibrary;
+
/**
* Public handle to a running wallet.
*/
@@ -1471,12 +1665,17 @@ export class Wallet {
private _client: WalletCoreApiClient | undefined;
private constructor(
- db: DbAccess<typeof WalletStoresV1>,
- http: HttpRequestLibrary,
+ idb: IDBFactory,
+ httpFactory: HttpFactory,
timer: TimerAPI,
cryptoWorkerFactory: CryptoWorkerFactory,
) {
- this.ws = new InternalWalletStateImpl(db, http, timer, cryptoWorkerFactory);
+ this.ws = new InternalWalletState(
+ idb,
+ httpFactory,
+ timer,
+ cryptoWorkerFactory,
+ );
}
get client(): WalletCoreApiClient {
@@ -1486,30 +1685,18 @@ export class Wallet {
return this._client;
}
- /**
- * Trust the exchange, do not validate signatures.
- * Only used to benchmark the exchange.
- */
- setInsecureTrustExchange(): void {
- this.ws.insecureTrustExchange = true;
- }
-
- setBatchWithdrawal(enable: boolean): void {
- this.ws.batchWithdrawal = enable;
- }
-
static async create(
- db: DbAccess<typeof WalletStoresV1>,
- http: HttpRequestLibrary,
+ idb: IDBFactory,
+ httpFactory: HttpFactory,
timer: TimerAPI,
cryptoWorkerFactory: CryptoWorkerFactory,
): Promise<Wallet> {
- const w = new Wallet(db, http, timer, cryptoWorkerFactory);
+ const w = new Wallet(idb, httpFactory, timer, cryptoWorkerFactory);
w._client = await getClientFromWalletState(w.ws);
return w;
}
- addNotificationListener(f: (n: WalletNotification) => void): void {
+ addNotificationListener(f: (n: WalletNotification) => void): CancelFn {
return this.ws.addNotificationListener(f);
}
@@ -1517,74 +1704,117 @@ export class Wallet {
this.ws.stop();
}
- runPending(forceNow = false): Promise<void> {
- return runPending(this.ws, forceNow);
- }
-
- runTaskLoop(opts?: RetryLoopOpts): Promise<TaskLoopResult> {
- return runTaskLoop(this.ws, opts);
- }
-
- handleCoreApiRequest(
+ async handleCoreApiRequest(
operation: string,
id: string,
payload: unknown,
): Promise<CoreApiResponse> {
+ await this.ws.ensureWalletDbOpen();
return handleCoreApiRequest(this.ws, operation, id, payload);
}
}
+export interface DevExperimentState {
+ blockRefreshes?: boolean;
+}
+
+export class Cache<T> {
+ private map: Map<string, [AbsoluteTime, T]> = new Map();
+
+ constructor(
+ private maxCapacity: number,
+ private cacheDuration: Duration,
+ ) {}
+
+ get(key: string): T | undefined {
+ const r = this.map.get(key);
+ if (!r) {
+ return undefined;
+ }
+
+ if (AbsoluteTime.isExpired(r[0])) {
+ this.map.delete(key);
+ return undefined;
+ }
+
+ return r[1];
+ }
+
+ clear(): void {
+ this.map.clear();
+ }
+
+ put(key: string, value: T): void {
+ if (this.map.size > this.maxCapacity) {
+ this.map.clear();
+ }
+ const expiry = AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ this.cacheDuration,
+ );
+ this.map.set(key, [expiry, value]);
+ }
+}
+
+/**
+ * Implementation of triggers for the wallet DB.
+ */
+class WalletDbTriggerSpec implements TriggerSpec {
+ constructor(public ws: InternalWalletState) {}
+
+ afterCommit(info: AfterCommitInfo): void {
+ if (info.mode !== "readwrite") {
+ return;
+ }
+ logger.info(
+ `in after commit callback for readwrite, modified ${j2s([
+ ...info.modifiedStores,
+ ])}`,
+ );
+ const modified = info.accessedStores;
+ if (
+ modified.has(WalletStoresV1.exchanges.storeName) ||
+ modified.has(WalletStoresV1.exchangeDetails.storeName) ||
+ modified.has(WalletStoresV1.denominations.storeName) ||
+ modified.has(WalletStoresV1.globalCurrencyAuditors.storeName) ||
+ modified.has(WalletStoresV1.globalCurrencyExchanges.storeName)
+ ) {
+ this.ws.clearAllCaches();
+ }
+ }
+}
+
/**
* Internal state of the wallet.
*
* This ties together all the operation implementations.
*/
-class InternalWalletStateImpl implements InternalWalletState {
- /**
- * @see {@link InternalWalletState.activeLongpoll}
- */
- activeLongpoll: ActiveLongpollInfo = {};
-
+export class InternalWalletState {
cryptoApi: TalerCryptoInterface;
cryptoDispatcher: CryptoDispatcher;
- merchantInfoCache: Record<string, MerchantInfo> = {};
-
- insecureTrustExchange = false;
-
- batchWithdrawal = false;
-
readonly timerGroup: TimerGroup;
- latch = new AsyncCondition();
+ workAvailable = new AsyncCondition();
stopped = false;
- listeners: NotificationListener[] = [];
+ private listeners: NotificationListener[] = [];
initCalled = false;
- devModeActive = false;
-
- exchangeOps: ExchangeOperations = {
- getExchangeDetails,
- getExchangeTrust,
- updateExchangeFromUrl,
- };
-
- recoupOps: RecoupOperations = {
- createRecoupGroup,
- processRecoupGroup,
- };
-
- merchantOps: MerchantOperations = {
- getMerchantInfo,
- };
+ refreshCostCache: Cache<AmountJson> = new Cache(
+ 1000,
+ Duration.fromSpec({ minutes: 1 }),
+ );
- refreshOps: RefreshOperations = {
- createRefreshGroup,
- };
+ denomInfoCache: Cache<DenominationInfo> = new Cache(
+ 1000,
+ Duration.fromSpec({ minutes: 1 }),
+ );
- // FIXME: Use an LRU cache here.
- private denomCache: Record<string, DenominationInfo> = {};
+ exchangeCache: Cache<ReadyExchangeSummary> = new Cache(
+ 1000,
+ Duration.fromSpec({ minutes: 1 }),
+ );
/**
* Promises that are waiting for a particular resource.
@@ -1596,13 +1826,78 @@ class InternalWalletStateImpl implements InternalWalletState {
*/
private resourceLocks: Set<string> = new Set();
+ taskScheduler: TaskScheduler = new TaskSchedulerImpl(this);
+
+ private _config: Readonly<WalletRunConfig> | undefined;
+
+ private _indexedDbHandle: IDBDatabase | undefined = undefined;
+
+ private _dbAccessHandle: DbAccess<typeof WalletStoresV1> | undefined;
+
+ private _http: HttpRequestLibrary | undefined = undefined;
+
+ get db(): DbAccess<typeof WalletStoresV1> {
+ if (!this._dbAccessHandle) {
+ this._dbAccessHandle = this.createDbAccessHandle(
+ CancellationToken.CONTINUE,
+ );
+ }
+ return this._dbAccessHandle;
+ }
+
+ devExperimentState: DevExperimentState = {};
+
+ clientCancellationMap: Map<string, CancellationToken.Source> = new Map();
+
+ clearAllCaches(): void {
+ this.exchangeCache.clear();
+ this.denomInfoCache.clear();
+ this.refreshCostCache.clear();
+ }
+
+ initWithConfig(newConfig: WalletRunConfig): void {
+ this._config = newConfig;
+
+ logger.info(`setting new config to ${j2s(newConfig)}`);
+
+ this._http = this.httpFactory(newConfig);
+
+ if (this.config.testing.devModeActive) {
+ this._http = new DevExperimentHttpLib(this.http);
+ }
+ }
+
+ createDbAccessHandle(
+ cancellationToken: CancellationToken,
+ ): DbAccess<typeof WalletStoresV1> {
+ if (!this._indexedDbHandle) {
+ throw Error("db not initialized");
+ }
+ return new DbAccessImpl(
+ this._indexedDbHandle,
+ WalletStoresV1,
+ new WalletDbTriggerSpec(this),
+ cancellationToken,
+ );
+ }
+
+ get config(): WalletRunConfig {
+ if (!this._config) {
+ throw Error("config not initialized");
+ }
+ return this._config;
+ }
+
+ get http(): HttpRequestLibrary {
+ if (!this._http) {
+ throw Error("wallet not initialized");
+ }
+ return this._http;
+ }
+
constructor(
- // FIXME: Make this a getter and make
- // the actual value nullable.
- // Check if we are in a DB migration / garbage collection
- // and throw an error in that case.
- public db: DbAccess<typeof WalletStoresV1>,
- public http: HttpRequestLibrary,
+ public idb: IDBFactory,
+ private httpFactory: HttpFactory,
public timer: TimerAPI,
cryptoWorkerFactory: CryptoWorkerFactory,
) {
@@ -1611,28 +1906,26 @@ class InternalWalletStateImpl implements InternalWalletState {
this.timerGroup = new TimerGroup(timer);
}
- async getDenomInfo(
- ws: InternalWalletState,
- tx: GetReadWriteAccess<{
- denominations: typeof WalletStoresV1.denominations;
- }>,
- exchangeBaseUrl: string,
- denomPubHash: string,
- ): Promise<DenominationInfo | undefined> {
- const key = `${exchangeBaseUrl}:${denomPubHash}`;
- const cached = this.denomCache[key];
- if (cached) {
- return cached;
- }
- const d = await tx.denominations.get([exchangeBaseUrl, denomPubHash]);
- if (d) {
- return DenominationRecord.toDenomInfo(d);
- }
- return undefined;
+ async ensureWalletDbOpen(): Promise<void> {
+ if (this._indexedDbHandle) {
+ return;
+ }
+ const myVersionChange = async (): Promise<void> => {
+ logger.info("version change requested for Taler DB");
+ };
+ try {
+ const myDb = await openTalerDatabase(this.idb, myVersionChange);
+ this._indexedDbHandle = myDb;
+ } catch (e) {
+ logger.error("error writing to database during initialization");
+ throw TalerError.fromDetail(TalerErrorCode.WALLET_DB_UNAVAILABLE, {
+ innerError: getErrorDetailFromException(e),
+ });
+ }
}
notify(n: WalletNotification): void {
- logger.trace("Notification", n);
+ logger.trace(`Notification: ${j2s(n)}`);
for (const l of this.listeners) {
const nc = JSON.parse(JSON.stringify(n));
setTimeout(() => {
@@ -1641,8 +1934,14 @@ class InternalWalletStateImpl implements InternalWalletState {
}
}
- addNotificationListener(f: (n: WalletNotification) => void): void {
+ addNotificationListener(f: (n: WalletNotification) => void): CancelFn {
this.listeners.push(f);
+ return () => {
+ const idx = this.listeners.indexOf(f);
+ if (idx >= 0) {
+ this.listeners.splice(idx, 1);
+ }
+ };
}
/**
@@ -1653,18 +1952,6 @@ class InternalWalletStateImpl implements InternalWalletState {
this.stopped = true;
this.timerGroup.stopCurrentAndFutureTimers();
this.cryptoDispatcher.stop();
- for (const key of Object.keys(this.activeLongpoll)) {
- logger.trace(`cancelling active longpoll ${key}`);
- this.activeLongpoll[key].cancel();
- }
- }
-
- async runUntilDone(
- req: {
- maxRetries?: number;
- } = {},
- ): Promise<void> {
- await runTaskLoop(this, { ...req, stopWhenDone: true });
}
/**
@@ -1699,7 +1986,7 @@ class InternalWalletStateImpl implements InternalWalletState {
} finally {
for (const token of tokens) {
this.resourceLocks.delete(token);
- let waiter = (this.resourceWaiters[token] ?? []).shift();
+ const waiter = (this.resourceWaiters[token] ?? []).shift();
if (waiter) {
waiter.resolve();
}
diff --git a/packages/taler-wallet-core/src/operations/withdraw.test.ts b/packages/taler-wallet-core/src/withdraw.test.ts
index c77f75b9d..2a081b481 100644
--- a/packages/taler-wallet-core/src/operations/withdraw.test.ts
+++ b/packages/taler-wallet-core/src/withdraw.test.ts
@@ -14,10 +14,14 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { Amounts, DenomKeyType } from "@gnu-taler/taler-util";
+import { AmountString, Amounts, DenomKeyType } from "@gnu-taler/taler-util";
import test from "ava";
-import { DenominationRecord, DenominationVerificationStatus } from "../db.js";
-import { selectWithdrawalDenominations } from "./withdraw.js";
+import {
+ DenominationRecord,
+ DenominationVerificationStatus,
+ timestampProtocolToDb,
+} from "./db.js";
+import { selectWithdrawalDenominations } from "./denomSelection.js";
test("withdrawal selection bug repro", (t) => {
const amount = {
@@ -64,23 +68,21 @@ test("withdrawal selection bug repro", (t) => {
isRevoked: false,
masterSig:
"4F0P456CNNTTWK8BFJHGM3JTD6FVVNZY8EP077GYAHDJ5Y81S5RQ3SMS925NXMDVG9A88JAAP0E2GDZBC21PP5NHFFVWHAW3AVT8J3R",
- stampExpireDeposit: {
+ stampExpireDeposit: timestampProtocolToDb({
t_s: 1742909388,
- },
- stampExpireLegal: {
+ }),
+ stampExpireLegal: timestampProtocolToDb({
t_s: 1900589388,
- },
- stampExpireWithdraw: {
+ }),
+ stampExpireWithdraw: timestampProtocolToDb({
t_s: 1679837388,
- },
- stampStart: {
+ }),
+ stampStart: timestampProtocolToDb({
t_s: 1585229388,
- },
+ }),
verificationStatus: DenominationVerificationStatus.Unverified,
currency: "KUDOS",
- amountFrac: 0,
- amountVal: 1000,
- listIssueDate: { t_s: 0 },
+ value: "KUDOS:1000" as AmountString,
},
{
denomPub: {
@@ -120,23 +122,21 @@ test("withdrawal selection bug repro", (t) => {
isRevoked: false,
masterSig:
"P99AW82W46MZ0AKW7Z58VQPXFNTJQM9DVTYPBDF6KVYF38PPVDAZTV7JQ8TY7HGEC7JJJAY4E7AY7J3W1WV10DAZZQHHKTAVTSRAC20",
- stampExpireDeposit: {
+ stampExpireDeposit: timestampProtocolToDb({
t_s: 1742909388,
- },
- stampExpireLegal: {
+ }),
+ stampExpireLegal: timestampProtocolToDb({
t_s: 1900589388,
- },
- stampExpireWithdraw: {
+ }),
+ stampExpireWithdraw: timestampProtocolToDb({
t_s: 1679837388,
- },
- stampStart: {
+ }),
+ stampStart: timestampProtocolToDb({
t_s: 1585229388,
- },
+ }),
verificationStatus: DenominationVerificationStatus.Unverified,
- amountFrac: 0,
- amountVal: 10,
+ value: "KUDOS:10" as AmountString,
currency: "KUDOS",
- listIssueDate: { t_s: 0 },
},
{
denomPub: {
@@ -175,23 +175,21 @@ test("withdrawal selection bug repro", (t) => {
isRevoked: false,
masterSig:
"8S4VZGHE5WE0N5ZVCHYW9KZZR4YAKK15S46MV1HR1QB9AAMH3NWPW4DCR4NYGJK33Q8YNFY80SWNS6XKAP5DEVK933TM894FJ2VGE3G",
- stampExpireDeposit: {
+ stampExpireDeposit: timestampProtocolToDb({
t_s: 1742909388,
- },
- stampExpireLegal: {
+ }),
+ stampExpireLegal: timestampProtocolToDb({
t_s: 1900589388,
- },
- stampExpireWithdraw: {
+ }),
+ stampExpireWithdraw: timestampProtocolToDb({
t_s: 1679837388,
- },
- stampStart: {
+ }),
+ stampStart: timestampProtocolToDb({
t_s: 1585229388,
- },
+ }),
verificationStatus: DenominationVerificationStatus.Unverified,
- amountFrac: 0,
- amountVal: 5,
+ value: "KUDOS:5" as AmountString,
currency: "KUDOS",
- listIssueDate: { t_s: 0 },
},
{
denomPub: {
@@ -231,23 +229,21 @@ test("withdrawal selection bug repro", (t) => {
isRevoked: false,
masterSig:
"E3AWGAG8VB42P3KXM8B04Z6M483SX59R3Y4T53C3NXCA2NPB6C7HVCMVX05DC6S58E9X40NGEBQNYXKYMYCF3ASY2C4WP1WCZ4ME610",
- stampExpireDeposit: {
+ stampExpireDeposit: timestampProtocolToDb({
t_s: 1742909388,
- },
- stampExpireLegal: {
+ }),
+ stampExpireLegal: timestampProtocolToDb({
t_s: 1900589388,
- },
- stampExpireWithdraw: {
+ }),
+ stampExpireWithdraw: timestampProtocolToDb({
t_s: 1679837388,
- },
- stampStart: {
+ }),
+ stampStart: timestampProtocolToDb({
t_s: 1585229388,
- },
+ }),
verificationStatus: DenominationVerificationStatus.Unverified,
- amountFrac: 0,
- amountVal: 1,
+ value: "KUDOS:1" as AmountString,
currency: "KUDOS",
- listIssueDate: { t_s: 0 },
},
{
denomPub: {
@@ -286,23 +282,25 @@ test("withdrawal selection bug repro", (t) => {
isRevoked: false,
masterSig:
"0ES1RKV002XB4YP21SN0QB7RSDHGYT0XAE65JYN8AVJAA6H7JZFN7JADXT521DJS89XMGPZGR8GCXF1516Y0Q9QDV00E6NMFA6CF838",
- stampExpireDeposit: {
+ stampExpireDeposit: timestampProtocolToDb({
t_s: 1742909388,
- },
- stampExpireLegal: {
+ }),
+ stampExpireLegal: timestampProtocolToDb({
t_s: 1900589388,
- },
- stampExpireWithdraw: {
+ }),
+ stampExpireWithdraw: timestampProtocolToDb({
t_s: 1679837388,
- },
- stampStart: {
+ }),
+ stampStart: timestampProtocolToDb({
t_s: 1585229388,
- },
+ }),
verificationStatus: DenominationVerificationStatus.Unverified,
- amountFrac: 10000000,
- amountVal: 0,
+ value: Amounts.stringify({
+ currency: "KUDOS",
+ fraction: 10000000,
+ value: 0,
+ }),
currency: "KUDOS",
- listIssueDate: { t_s: 0 },
},
{
denomPub: {
@@ -341,23 +339,21 @@ test("withdrawal selection bug repro", (t) => {
isRevoked: false,
masterSig:
"58QEB6C6N7602E572E3JYANVVJ9BRW0V9E2ZFDW940N47YVQDK9SAFPWBN5YGT3G1742AFKQ0CYR4DM2VWV0Z0T1XMEKWN6X2EZ9M0R",
- stampExpireDeposit: {
+ stampExpireDeposit: timestampProtocolToDb({
t_s: 1742909388,
- },
- stampExpireLegal: {
+ }),
+ stampExpireLegal: timestampProtocolToDb({
t_s: 1900589388,
- },
- stampExpireWithdraw: {
+ }),
+ stampExpireWithdraw: timestampProtocolToDb({
t_s: 1679837388,
- },
- stampStart: {
+ }),
+ stampStart: timestampProtocolToDb({
t_s: 1585229388,
- },
+ }),
verificationStatus: DenominationVerificationStatus.Unverified,
- amountFrac: 0,
- amountVal: 2,
+ value: "KUDOS:2" as AmountString,
currency: "KUDOS",
- listIssueDate: { t_s: 0 },
},
];
diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts
new file mode 100644
index 000000000..4936135bd
--- /dev/null
+++ b/packages/taler-wallet-core/src/withdraw.ts
@@ -0,0 +1,3258 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019-2024 Taler Systems SA
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * @fileoverview Implementation of Taler withdrawals, both
+ * bank-integrated and manual.
+ */
+
+/**
+ * Imports.
+ */
+import {
+ AbsoluteTime,
+ AcceptManualWithdrawalResult,
+ AcceptWithdrawalResponse,
+ AgeRestriction,
+ Amount,
+ AmountJson,
+ AmountLike,
+ AmountString,
+ Amounts,
+ AsyncFlag,
+ BankWithdrawDetails,
+ CancellationToken,
+ CoinStatus,
+ CurrencySpecification,
+ DenomKeyType,
+ DenomSelItem,
+ DenomSelectionState,
+ Duration,
+ ExchangeBatchWithdrawRequest,
+ ExchangeUpdateStatus,
+ ExchangeWireAccount,
+ ExchangeWithdrawBatchResponse,
+ ExchangeWithdrawRequest,
+ ExchangeWithdrawResponse,
+ ExchangeWithdrawalDetails,
+ ForcedDenomSel,
+ GetWithdrawalDetailsForAmountRequest,
+ HttpStatusCode,
+ LibtoolVersion,
+ Logger,
+ NotificationType,
+ ObservabilityEventType,
+ TalerBankIntegrationHttpClient,
+ TalerError,
+ TalerErrorCode,
+ TalerErrorDetail,
+ TalerPreciseTimestamp,
+ Transaction,
+ TransactionAction,
+ TransactionIdStr,
+ TransactionMajorState,
+ TransactionMinorState,
+ TransactionState,
+ TransactionType,
+ URL,
+ UnblindedSignature,
+ WalletNotification,
+ WithdrawUriInfoResponse,
+ WithdrawalDetailsForAmount,
+ WithdrawalExchangeAccountDetails,
+ WithdrawalType,
+ addPaytoQueryParams,
+ assertUnreachable,
+ canonicalizeBaseUrl,
+ checkDbInvariant,
+ codeForBankWithdrawalOperationPostResponse,
+ codecForCashinConversionResponse,
+ codecForConversionBankConfig,
+ codecForExchangeWithdrawBatchResponse,
+ codecForReserveStatus,
+ codecForWalletKycUuid,
+ codecForWithdrawOperationStatusResponse,
+ encodeCrock,
+ getErrorDetailFromException,
+ getRandomBytes,
+ j2s,
+ makeErrorDetail,
+ parseWithdrawUri,
+} from "@gnu-taler/taler-util";
+import {
+ HttpRequestLibrary,
+ HttpResponse,
+ readSuccessResponseJsonOrErrorCode,
+ readSuccessResponseJsonOrThrow,
+ readTalerErrorResponse,
+ throwUnexpectedRequestError,
+} from "@gnu-taler/taler-util/http";
+import {
+ PendingTaskType,
+ TaskIdStr,
+ TaskRunResult,
+ TaskRunResultType,
+ TombstoneTag,
+ TransactionContext,
+ TransitionResult,
+ TransitionResultType,
+ constructTaskIdentifier,
+ makeCoinAvailable,
+ makeCoinsVisible,
+} from "./common.js";
+import { EddsaKeypair } from "./crypto/cryptoImplementation.js";
+import {
+ CoinRecord,
+ CoinSourceType,
+ DenominationRecord,
+ DenominationVerificationStatus,
+ KycPendingInfo,
+ PlanchetRecord,
+ PlanchetStatus,
+ WalletDbReadOnlyTransaction,
+ WalletDbReadWriteTransaction,
+ WalletDbStoresArr,
+ WalletStoresV1,
+ WgInfo,
+ WithdrawalGroupRecord,
+ WithdrawalGroupStatus,
+ WithdrawalRecordType,
+ timestampAbsoluteFromDb,
+ timestampPreciseFromDb,
+ timestampPreciseToDb,
+} from "./db.js";
+import {
+ selectForcedWithdrawalDenominations,
+ selectWithdrawalDenominations,
+} from "./denomSelection.js";
+import { isWithdrawableDenom } from "./denominations.js";
+import {
+ ReadyExchangeSummary,
+ fetchFreshExchange,
+ getExchangePaytoUri,
+ getExchangeWireDetailsInTx,
+ listExchanges,
+ markExchangeUsed,
+} from "./exchanges.js";
+import { DbAccess } from "./query.js";
+import {
+ TransitionInfo,
+ constructTransactionIdentifier,
+ isUnsuccessfulTransaction,
+ notifyTransition,
+} from "./transactions.js";
+import {
+ WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
+ WALLET_EXCHANGE_PROTOCOL_VERSION,
+} from "./versions.js";
+import { WalletExecutionContext, getDenomInfo } from "./wallet.js";
+
+/**
+ * Logger for this file.
+ */
+const logger = new Logger("operations/withdraw.ts");
+
+/**
+ * Update the materialized withdrawal transaction based
+ * on the withdrawal group record.
+ */
+async function updateWithdrawalTransaction(
+ ctx: WithdrawTransactionContext,
+ tx: WalletDbReadWriteTransaction<
+ [
+ "withdrawalGroups",
+ "transactions",
+ "operationRetries",
+ "exchanges",
+ "exchangeDetails",
+ ]
+ >,
+): Promise<void> {
+ const wgRecord = await tx.withdrawalGroups.get(ctx.withdrawalGroupId);
+ if (!wgRecord) {
+ await tx.transactions.delete(ctx.transactionId);
+ return;
+ }
+ const retryRecord = await tx.operationRetries.get(ctx.taskId);
+
+ let transactionItem: Transaction;
+
+ if (wgRecord.wgInfo.withdrawalType === WithdrawalRecordType.BankIntegrated) {
+ const txState = computeWithdrawalTransactionStatus(wgRecord);
+ transactionItem = {
+ type: TransactionType.Withdrawal,
+ txState,
+ txActions: computeWithdrawalTransactionActions(wgRecord),
+ amountEffective: isUnsuccessfulTransaction(txState)
+ ? Amounts.stringify(Amounts.zeroOfAmount(wgRecord.instructedAmount))
+ : Amounts.stringify(wgRecord.denomsSel.totalCoinValue),
+ amountRaw: Amounts.stringify(wgRecord.instructedAmount),
+ withdrawalDetails: {
+ type: WithdrawalType.TalerBankIntegrationApi,
+ confirmed: wgRecord.wgInfo.bankInfo.timestampBankConfirmed
+ ? true
+ : false,
+ exchangeCreditAccountDetails: wgRecord.wgInfo.exchangeCreditAccounts,
+ reservePub: wgRecord.reservePub,
+ bankConfirmationUrl: wgRecord.wgInfo.bankInfo.confirmUrl,
+ reserveIsReady:
+ wgRecord.status === WithdrawalGroupStatus.Done ||
+ wgRecord.status === WithdrawalGroupStatus.PendingReady,
+ },
+ kycUrl: wgRecord.kycUrl,
+ exchangeBaseUrl: wgRecord.exchangeBaseUrl,
+ timestamp: timestampPreciseFromDb(wgRecord.timestampStart),
+ transactionId: ctx.transactionId,
+ };
+ } else if (
+ wgRecord.wgInfo.withdrawalType === WithdrawalRecordType.BankManual
+ ) {
+ const exchangeDetails = await getExchangeWireDetailsInTx(
+ tx,
+ wgRecord.exchangeBaseUrl,
+ );
+ const plainPaytoUris =
+ exchangeDetails?.wireInfo?.accounts.map((x) => x.payto_uri) ?? [];
+
+ const exchangePaytoUris = augmentPaytoUrisForWithdrawal(
+ plainPaytoUris,
+ wgRecord.reservePub,
+ wgRecord.instructedAmount,
+ );
+
+ const txState = computeWithdrawalTransactionStatus(wgRecord);
+
+ transactionItem = {
+ type: TransactionType.Withdrawal,
+ txState,
+ txActions: computeWithdrawalTransactionActions(wgRecord),
+ amountEffective: isUnsuccessfulTransaction(txState)
+ ? Amounts.stringify(Amounts.zeroOfAmount(wgRecord.instructedAmount))
+ : Amounts.stringify(wgRecord.denomsSel.totalCoinValue),
+ amountRaw: Amounts.stringify(wgRecord.instructedAmount),
+ withdrawalDetails: {
+ type: WithdrawalType.ManualTransfer,
+ reservePub: wgRecord.reservePub,
+ exchangePaytoUris,
+ exchangeCreditAccountDetails: wgRecord.wgInfo.exchangeCreditAccounts,
+ reserveIsReady:
+ wgRecord.status === WithdrawalGroupStatus.Done ||
+ wgRecord.status === WithdrawalGroupStatus.PendingReady,
+ },
+ kycUrl: wgRecord.kycUrl,
+ exchangeBaseUrl: wgRecord.exchangeBaseUrl,
+ timestamp: timestampPreciseFromDb(wgRecord.timestampStart),
+ transactionId: ctx.transactionId,
+ };
+ } else {
+ // FIXME: If this is an orphaned withdrawal for a p2p transaction, we
+ // still might want to report the withdrawal.
+ return;
+ }
+
+ if (retryRecord?.lastError) {
+ transactionItem.error = retryRecord.lastError;
+ }
+
+ await tx.transactions.put({
+ currency: Amounts.currencyOf(wgRecord.instructedAmount),
+ transactionItem,
+ exchanges: [wgRecord.exchangeBaseUrl],
+ });
+
+ // FIXME: Handle orphaned withdrawals where the p2p or recoup tx was deleted?
+}
+
+export class WithdrawTransactionContext implements TransactionContext {
+ readonly transactionId: TransactionIdStr;
+ readonly taskId: TaskIdStr;
+
+ constructor(
+ public wex: WalletExecutionContext,
+ public withdrawalGroupId: string,
+ ) {
+ this.transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Withdrawal,
+ withdrawalGroupId,
+ });
+ this.taskId = constructTaskIdentifier({
+ tag: PendingTaskType.Withdraw,
+ withdrawalGroupId,
+ });
+ }
+
+ /**
+ * Transition a withdrawal transaction.
+ * Extra object stores may be accessed during the transition.
+ */
+ async transition<StoreNameArray extends WalletDbStoresArr = []>(
+ opts: { extraStores?: StoreNameArray; transactionLabel?: string },
+ f: (
+ rec: WithdrawalGroupRecord | undefined,
+ tx: WalletDbReadWriteTransaction<
+ [
+ "withdrawalGroups",
+ "transactions",
+ "operationRetries",
+ "exchanges",
+ "exchangeDetails",
+ ...StoreNameArray,
+ ]
+ >,
+ ) => Promise<TransitionResult<WithdrawalGroupRecord>>,
+ ): Promise<TransitionInfo | undefined> {
+ const baseStores = [
+ "withdrawalGroups" as const,
+ "transactions" as const,
+ "operationRetries" as const,
+ "exchanges" as const,
+ "exchangeDetails" as const,
+ ];
+ let stores = opts.extraStores
+ ? [...baseStores, ...opts.extraStores]
+ : baseStores;
+ const transitionInfo = await this.wex.db.runReadWriteTx(
+ { storeNames: stores },
+ async (tx) => {
+ const wgRec = await tx.withdrawalGroups.get(this.withdrawalGroupId);
+ let oldTxState: TransactionState;
+ if (wgRec) {
+ oldTxState = computeWithdrawalTransactionStatus(wgRec);
+ } else {
+ oldTxState = {
+ major: TransactionMajorState.None,
+ };
+ }
+ const res = await f(wgRec, tx);
+ switch (res.type) {
+ case TransitionResultType.Transition: {
+ await tx.withdrawalGroups.put(res.rec);
+ await updateWithdrawalTransaction(this, tx);
+ const newTxState = computeWithdrawalTransactionStatus(res.rec);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ case TransitionResultType.Delete:
+ await tx.withdrawalGroups.delete(this.withdrawalGroupId);
+ await updateWithdrawalTransaction(this, tx);
+ return {
+ oldTxState,
+ newTxState: {
+ major: TransactionMajorState.None,
+ },
+ };
+ default:
+ return undefined;
+ }
+ },
+ );
+ notifyTransition(this.wex, this.transactionId, transitionInfo);
+ return transitionInfo;
+ }
+
+ async deleteTransaction(): Promise<void> {
+ await this.transition(
+ {
+ extraStores: ["tombstones"],
+ transactionLabel: "delete-transaction-withdraw",
+ },
+ async (rec, tx) => {
+ if (!rec) {
+ return TransitionResult.stay();
+ }
+ if (rec) {
+ await tx.tombstones.put({
+ id:
+ TombstoneTag.DeleteWithdrawalGroup + ":" + rec.withdrawalGroupId,
+ });
+ }
+ return TransitionResult.delete();
+ },
+ );
+ }
+
+ async suspendTransaction(): Promise<void> {
+ const { withdrawalGroupId } = this;
+ await this.transition(
+ {
+ transactionLabel: "suspend-transaction-withdraw",
+ },
+ async (wg, _tx) => {
+ if (!wg) {
+ logger.warn(`withdrawal group ${withdrawalGroupId} not found`);
+ return TransitionResult.stay();
+ }
+ let newStatus: WithdrawalGroupStatus | undefined = undefined;
+ switch (wg.status) {
+ case WithdrawalGroupStatus.PendingReady:
+ newStatus = WithdrawalGroupStatus.SuspendedReady;
+ break;
+ case WithdrawalGroupStatus.AbortingBank:
+ newStatus = WithdrawalGroupStatus.SuspendedAbortingBank;
+ break;
+ case WithdrawalGroupStatus.PendingWaitConfirmBank:
+ newStatus = WithdrawalGroupStatus.SuspendedWaitConfirmBank;
+ break;
+ case WithdrawalGroupStatus.PendingRegisteringBank:
+ newStatus = WithdrawalGroupStatus.SuspendedRegisteringBank;
+ break;
+ case WithdrawalGroupStatus.PendingQueryingStatus:
+ newStatus = WithdrawalGroupStatus.SuspendedQueryingStatus;
+ break;
+ case WithdrawalGroupStatus.PendingKyc:
+ newStatus = WithdrawalGroupStatus.SuspendedKyc;
+ break;
+ case WithdrawalGroupStatus.PendingAml:
+ newStatus = WithdrawalGroupStatus.SuspendedAml;
+ break;
+ default:
+ logger.warn(
+ `Unsupported 'suspend' on withdrawal transaction in status ${wg.status}`,
+ );
+ return TransitionResult.stay();
+ }
+ wg.status = newStatus;
+ return TransitionResult.transition(wg);
+ },
+ );
+ }
+
+ async abortTransaction(): Promise<void> {
+ const { withdrawalGroupId } = this;
+ await this.transition(
+ {
+ transactionLabel: "abort-transaction-withdraw",
+ },
+ async (wg, _tx) => {
+ if (!wg) {
+ logger.warn(`withdrawal group ${withdrawalGroupId} not found`);
+ return TransitionResult.stay();
+ }
+ let newStatus: WithdrawalGroupStatus | undefined = undefined;
+ switch (wg.status) {
+ case WithdrawalGroupStatus.SuspendedRegisteringBank:
+ case WithdrawalGroupStatus.SuspendedWaitConfirmBank:
+ case WithdrawalGroupStatus.PendingWaitConfirmBank:
+ case WithdrawalGroupStatus.PendingRegisteringBank:
+ newStatus = WithdrawalGroupStatus.AbortingBank;
+ break;
+ case WithdrawalGroupStatus.SuspendedAml:
+ case WithdrawalGroupStatus.SuspendedKyc:
+ case WithdrawalGroupStatus.SuspendedQueryingStatus:
+ case WithdrawalGroupStatus.SuspendedReady:
+ case WithdrawalGroupStatus.PendingAml:
+ case WithdrawalGroupStatus.PendingKyc:
+ case WithdrawalGroupStatus.PendingQueryingStatus:
+ newStatus = WithdrawalGroupStatus.AbortedExchange;
+ break;
+ case WithdrawalGroupStatus.PendingReady:
+ newStatus = WithdrawalGroupStatus.SuspendedReady;
+ break;
+ case WithdrawalGroupStatus.SuspendedAbortingBank:
+ case WithdrawalGroupStatus.AbortingBank:
+ // No transition needed, but not an error
+ return TransitionResult.stay();
+ case WithdrawalGroupStatus.Done:
+ case WithdrawalGroupStatus.FailedBankAborted:
+ case WithdrawalGroupStatus.AbortedExchange:
+ case WithdrawalGroupStatus.AbortedBank:
+ case WithdrawalGroupStatus.FailedAbortingBank:
+ // Not allowed
+ throw Error("abort not allowed in current state");
+ default:
+ assertUnreachable(wg.status);
+ }
+ wg.status = newStatus;
+ return TransitionResult.transition(wg);
+ },
+ );
+ }
+
+ async resumeTransaction(): Promise<void> {
+ const { withdrawalGroupId } = this;
+ await this.transition(
+ {
+ transactionLabel: "resume-transaction-withdraw",
+ },
+ async (wg, _tx) => {
+ if (!wg) {
+ logger.warn(`withdrawal group ${withdrawalGroupId} not found`);
+ return TransitionResult.stay();
+ }
+ let newStatus: WithdrawalGroupStatus | undefined = undefined;
+ switch (wg.status) {
+ case WithdrawalGroupStatus.SuspendedReady:
+ newStatus = WithdrawalGroupStatus.PendingReady;
+ break;
+ case WithdrawalGroupStatus.SuspendedAbortingBank:
+ newStatus = WithdrawalGroupStatus.AbortingBank;
+ break;
+ case WithdrawalGroupStatus.SuspendedWaitConfirmBank:
+ newStatus = WithdrawalGroupStatus.PendingWaitConfirmBank;
+ break;
+ case WithdrawalGroupStatus.SuspendedQueryingStatus:
+ newStatus = WithdrawalGroupStatus.PendingQueryingStatus;
+ break;
+ case WithdrawalGroupStatus.SuspendedRegisteringBank:
+ newStatus = WithdrawalGroupStatus.PendingRegisteringBank;
+ break;
+ case WithdrawalGroupStatus.SuspendedAml:
+ newStatus = WithdrawalGroupStatus.PendingAml;
+ break;
+ case WithdrawalGroupStatus.SuspendedKyc:
+ newStatus = WithdrawalGroupStatus.PendingKyc;
+ break;
+ default:
+ logger.warn(
+ `Unsupported 'resume' on withdrawal transaction in status ${wg.status}`,
+ );
+ return TransitionResult.stay();
+ }
+ wg.status = newStatus;
+ return TransitionResult.transition(wg);
+ },
+ );
+ }
+
+ async failTransaction(): Promise<void> {
+ const { withdrawalGroupId } = this;
+ await this.transition(
+ {
+ transactionLabel: "fail-transaction-withdraw",
+ },
+ async (wg, _tx) => {
+ if (!wg) {
+ logger.warn(`withdrawal group ${withdrawalGroupId} not found`);
+ return TransitionResult.stay();
+ }
+ let newStatus: WithdrawalGroupStatus | undefined = undefined;
+ switch (wg.status) {
+ case WithdrawalGroupStatus.SuspendedAbortingBank:
+ case WithdrawalGroupStatus.AbortingBank:
+ newStatus = WithdrawalGroupStatus.FailedAbortingBank;
+ break;
+ default:
+ return TransitionResult.stay();
+ }
+ wg.status = newStatus;
+ return TransitionResult.transition(wg);
+ },
+ );
+ }
+}
+
+/**
+ * Compute the DD37 transaction state of a withdrawal transaction
+ * from the database's withdrawal group record.
+ */
+export function computeWithdrawalTransactionStatus(
+ wgRecord: WithdrawalGroupRecord,
+): TransactionState {
+ switch (wgRecord.status) {
+ case WithdrawalGroupStatus.FailedBankAborted:
+ return {
+ major: TransactionMajorState.Aborted,
+ };
+ case WithdrawalGroupStatus.Done:
+ return {
+ major: TransactionMajorState.Done,
+ };
+ case WithdrawalGroupStatus.PendingRegisteringBank:
+ return {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.BankRegisterReserve,
+ };
+ case WithdrawalGroupStatus.PendingReady:
+ return {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.WithdrawCoins,
+ };
+ case WithdrawalGroupStatus.PendingQueryingStatus:
+ return {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.ExchangeWaitReserve,
+ };
+ case WithdrawalGroupStatus.PendingWaitConfirmBank:
+ return {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.BankConfirmTransfer,
+ };
+ case WithdrawalGroupStatus.AbortingBank:
+ return {
+ major: TransactionMajorState.Aborting,
+ minor: TransactionMinorState.Bank,
+ };
+ case WithdrawalGroupStatus.SuspendedAbortingBank:
+ return {
+ major: TransactionMajorState.SuspendedAborting,
+ minor: TransactionMinorState.Bank,
+ };
+ case WithdrawalGroupStatus.SuspendedQueryingStatus:
+ return {
+ major: TransactionMajorState.Suspended,
+ minor: TransactionMinorState.ExchangeWaitReserve,
+ };
+ case WithdrawalGroupStatus.SuspendedRegisteringBank:
+ return {
+ major: TransactionMajorState.Suspended,
+ minor: TransactionMinorState.BankRegisterReserve,
+ };
+ case WithdrawalGroupStatus.SuspendedWaitConfirmBank:
+ return {
+ major: TransactionMajorState.Suspended,
+ minor: TransactionMinorState.BankConfirmTransfer,
+ };
+ case WithdrawalGroupStatus.SuspendedReady:
+ return {
+ major: TransactionMajorState.Suspended,
+ minor: TransactionMinorState.WithdrawCoins,
+ };
+ case WithdrawalGroupStatus.PendingAml:
+ return {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.AmlRequired,
+ };
+ case WithdrawalGroupStatus.PendingKyc:
+ return {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.KycRequired,
+ };
+ case WithdrawalGroupStatus.SuspendedAml:
+ return {
+ major: TransactionMajorState.Suspended,
+ minor: TransactionMinorState.AmlRequired,
+ };
+ case WithdrawalGroupStatus.SuspendedKyc:
+ return {
+ major: TransactionMajorState.Suspended,
+ minor: TransactionMinorState.KycRequired,
+ };
+ case WithdrawalGroupStatus.FailedAbortingBank:
+ return {
+ major: TransactionMajorState.Failed,
+ minor: TransactionMinorState.AbortingBank,
+ };
+ case WithdrawalGroupStatus.AbortedExchange:
+ return {
+ major: TransactionMajorState.Aborted,
+ minor: TransactionMinorState.Exchange,
+ };
+ case WithdrawalGroupStatus.AbortedBank:
+ return {
+ major: TransactionMajorState.Aborted,
+ minor: TransactionMinorState.Bank,
+ };
+ }
+}
+
+/**
+ * Compute DD37 transaction actions for a withdrawal transaction
+ * based on the database's withdrawal group record.
+ */
+export function computeWithdrawalTransactionActions(
+ wgRecord: WithdrawalGroupRecord,
+): TransactionAction[] {
+ switch (wgRecord.status) {
+ case WithdrawalGroupStatus.FailedBankAborted:
+ return [TransactionAction.Delete];
+ case WithdrawalGroupStatus.Done:
+ return [TransactionAction.Delete];
+ case WithdrawalGroupStatus.PendingRegisteringBank:
+ return [TransactionAction.Suspend, TransactionAction.Abort];
+ case WithdrawalGroupStatus.PendingReady:
+ return [TransactionAction.Suspend, TransactionAction.Abort];
+ case WithdrawalGroupStatus.PendingQueryingStatus:
+ return [TransactionAction.Suspend, TransactionAction.Abort];
+ case WithdrawalGroupStatus.PendingWaitConfirmBank:
+ return [TransactionAction.Suspend, TransactionAction.Abort];
+ case WithdrawalGroupStatus.AbortingBank:
+ return [TransactionAction.Suspend, TransactionAction.Fail];
+ case WithdrawalGroupStatus.SuspendedAbortingBank:
+ return [TransactionAction.Resume, TransactionAction.Fail];
+ case WithdrawalGroupStatus.SuspendedQueryingStatus:
+ return [TransactionAction.Resume, TransactionAction.Abort];
+ case WithdrawalGroupStatus.SuspendedRegisteringBank:
+ return [TransactionAction.Resume, TransactionAction.Abort];
+ case WithdrawalGroupStatus.SuspendedWaitConfirmBank:
+ return [TransactionAction.Resume, TransactionAction.Abort];
+ case WithdrawalGroupStatus.SuspendedReady:
+ return [TransactionAction.Resume, TransactionAction.Abort];
+ case WithdrawalGroupStatus.PendingAml:
+ return [TransactionAction.Resume, TransactionAction.Abort];
+ case WithdrawalGroupStatus.PendingKyc:
+ return [TransactionAction.Resume, TransactionAction.Abort];
+ case WithdrawalGroupStatus.SuspendedAml:
+ return [TransactionAction.Resume, TransactionAction.Abort];
+ case WithdrawalGroupStatus.SuspendedKyc:
+ return [TransactionAction.Resume, TransactionAction.Abort];
+ case WithdrawalGroupStatus.FailedAbortingBank:
+ return [TransactionAction.Delete];
+ case WithdrawalGroupStatus.AbortedExchange:
+ return [TransactionAction.Delete];
+ case WithdrawalGroupStatus.AbortedBank:
+ return [TransactionAction.Delete];
+ }
+}
+
+/**
+ * Get information about a withdrawal from
+ * a taler://withdraw URI by asking the bank.
+ *
+ * FIXME: Move into bank client.
+ */
+export async function getBankWithdrawalInfo(
+ http: HttpRequestLibrary,
+ talerWithdrawUri: string,
+): Promise<BankWithdrawDetails> {
+ const uriResult = parseWithdrawUri(talerWithdrawUri);
+ if (!uriResult) {
+ throw Error(`can't parse URL ${talerWithdrawUri}`);
+ }
+
+ const bankApi = new TalerBankIntegrationHttpClient(
+ uriResult.bankIntegrationApiBaseUrl,
+ http,
+ );
+
+ const { body: config } = await bankApi.getConfig();
+
+ if (!bankApi.isCompatible(config.version)) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_BANK_INTEGRATION_PROTOCOL_VERSION_INCOMPATIBLE,
+ {
+ bankProtocolVersion: config.version,
+ walletProtocolVersion: WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
+ },
+ "bank integration protocol version not compatible with wallet",
+ );
+ }
+
+ const resp = await bankApi.getWithdrawalOperationById(
+ uriResult.withdrawalOperationId,
+ );
+
+ if (resp.type === "fail") {
+ throw TalerError.fromUncheckedDetail(resp.detail);
+ }
+ const { body: status } = resp;
+
+ logger.info(`bank withdrawal operation status: ${j2s(status)}`);
+
+ return {
+ operationId: uriResult.withdrawalOperationId,
+ apiBaseUrl: uriResult.bankIntegrationApiBaseUrl,
+ amount: Amounts.parseOrThrow(status.amount),
+ confirmTransferUrl: status.confirm_transfer_url,
+ senderWire: status.sender_wire,
+ suggestedExchange: status.suggested_exchange,
+ wireTypes: status.wire_types,
+ status: status.status,
+ };
+}
+
+/**
+ * Return denominations that can potentially used for a withdrawal.
+ */
+async function getCandidateWithdrawalDenoms(
+ wex: WalletExecutionContext,
+ exchangeBaseUrl: string,
+ currency: string,
+): Promise<DenominationRecord[]> {
+ return await wex.db.runReadOnlyTx(
+ { storeNames: ["denominations"] },
+ async (tx) => {
+ return getCandidateWithdrawalDenomsTx(wex, tx, exchangeBaseUrl, currency);
+ },
+ );
+}
+
+export async function getCandidateWithdrawalDenomsTx(
+ wex: WalletExecutionContext,
+ tx: WalletDbReadOnlyTransaction<["denominations"]>,
+ exchangeBaseUrl: string,
+ currency: string,
+): Promise<DenominationRecord[]> {
+ // FIXME(https://bugs.taler.net/n/8446): Use denom groups instead of querying all denominations!
+ const allDenoms =
+ await tx.denominations.indexes.byExchangeBaseUrl.getAll(exchangeBaseUrl);
+ return allDenoms
+ .filter((d) => d.currency === currency)
+ .filter((d) =>
+ isWithdrawableDenom(d, wex.ws.config.testing.denomselAllowLate),
+ );
+}
+
+/**
+ * Generate a planchet for a coin index in a withdrawal group.
+ * Does not actually withdraw the coin yet.
+ *
+ * Split up so that we can parallelize the crypto, but serialize
+ * the exchange requests per reserve.
+ */
+async function processPlanchetGenerate(
+ wex: WalletExecutionContext,
+ withdrawalGroup: WithdrawalGroupRecord,
+ coinIdx: number,
+): Promise<void> {
+ let planchet = await wex.db.runReadOnlyTx(
+ { storeNames: ["planchets"] },
+ async (tx) => {
+ return tx.planchets.indexes.byGroupAndIndex.get([
+ withdrawalGroup.withdrawalGroupId,
+ coinIdx,
+ ]);
+ },
+ );
+ if (planchet) {
+ return;
+ }
+ let ci = 0;
+ let isSkipped = false;
+ let maybeDenomPubHash: string | undefined;
+ for (let di = 0; di < withdrawalGroup.denomsSel.selectedDenoms.length; di++) {
+ const d = withdrawalGroup.denomsSel.selectedDenoms[di];
+ if (coinIdx >= ci && coinIdx < ci + d.count) {
+ maybeDenomPubHash = d.denomPubHash;
+ if (coinIdx >= ci + d.count - (d.skip ?? 0)) {
+ isSkipped = true;
+ }
+ break;
+ }
+ ci += d.count;
+ }
+ if (isSkipped) {
+ return;
+ }
+ if (!maybeDenomPubHash) {
+ throw Error("invariant violated");
+ }
+ const denomPubHash = maybeDenomPubHash;
+
+ const denom = await wex.db.runReadOnlyTx(
+ { storeNames: ["denominations"] },
+ async (tx) => {
+ return getDenomInfo(
+ wex,
+ tx,
+ withdrawalGroup.exchangeBaseUrl,
+ denomPubHash,
+ );
+ },
+ );
+ checkDbInvariant(!!denom);
+ const r = await wex.cryptoApi.createPlanchet({
+ denomPub: denom.denomPub,
+ feeWithdraw: Amounts.parseOrThrow(denom.feeWithdraw),
+ reservePriv: withdrawalGroup.reservePriv,
+ reservePub: withdrawalGroup.reservePub,
+ value: Amounts.parseOrThrow(denom.value),
+ coinIndex: coinIdx,
+ secretSeed: withdrawalGroup.secretSeed,
+ restrictAge: withdrawalGroup.restrictAge,
+ });
+ const newPlanchet: PlanchetRecord = {
+ blindingKey: r.blindingKey,
+ coinEv: r.coinEv,
+ coinEvHash: r.coinEvHash,
+ coinIdx,
+ coinPriv: r.coinPriv,
+ coinPub: r.coinPub,
+ denomPubHash: r.denomPubHash,
+ planchetStatus: PlanchetStatus.Pending,
+ withdrawSig: r.withdrawSig,
+ withdrawalGroupId: withdrawalGroup.withdrawalGroupId,
+ ageCommitmentProof: r.ageCommitmentProof,
+ lastError: undefined,
+ };
+ await wex.db.runReadWriteTx({ storeNames: ["planchets"] }, async (tx) => {
+ const p = await tx.planchets.indexes.byGroupAndIndex.get([
+ withdrawalGroup.withdrawalGroupId,
+ coinIdx,
+ ]);
+ if (p) {
+ planchet = p;
+ return;
+ }
+ await tx.planchets.put(newPlanchet);
+ planchet = newPlanchet;
+ });
+}
+
+interface WithdrawalRequestBatchArgs {
+ coinStartIndex: number;
+
+ batchSize: number;
+}
+
+interface WithdrawalBatchResult {
+ coinIdxs: number[];
+ batchResp: ExchangeWithdrawBatchResponse;
+}
+
+// FIXME: Move to exchange API types
+enum ExchangeAmlStatus {
+ Normal = 0,
+ Pending = 1,
+ Frozen = 2,
+}
+
+async function handleKycRequired(
+ wex: WalletExecutionContext,
+ withdrawalGroup: WithdrawalGroupRecord,
+ resp: HttpResponse,
+ startIdx: number,
+ requestCoinIdxs: number[],
+): Promise<void> {
+ logger.info("withdrawal requires KYC");
+ const respJson = await resp.json();
+ const uuidResp = codecForWalletKycUuid().decode(respJson);
+ const withdrawalGroupId = withdrawalGroup.withdrawalGroupId;
+ const ctx = new WithdrawTransactionContext(wex, withdrawalGroupId);
+ logger.info(`kyc uuid response: ${j2s(uuidResp)}`);
+ const exchangeUrl = withdrawalGroup.exchangeBaseUrl;
+ const userType = "individual";
+ const kycInfo: KycPendingInfo = {
+ paytoHash: uuidResp.h_payto,
+ requirementRow: uuidResp.requirement_row,
+ };
+ const url = new URL(
+ `kyc-check/${kycInfo.requirementRow}/${kycInfo.paytoHash}/${userType}`,
+ exchangeUrl,
+ );
+ logger.info(`kyc url ${url.href}`);
+ const kycStatusRes = await wex.http.fetch(url.href, {
+ method: "GET",
+ cancellationToken: wex.cancellationToken,
+ });
+ let kycUrl: string;
+ let amlStatus: ExchangeAmlStatus | undefined;
+ if (
+ kycStatusRes.status === HttpStatusCode.Ok ||
+ // FIXME: NoContent is not expected https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-merge
+ // remove after the exchange is fixed or clarified
+ kycStatusRes.status === HttpStatusCode.NoContent
+ ) {
+ logger.warn("kyc requested, but already fulfilled");
+ return;
+ } else if (kycStatusRes.status === HttpStatusCode.Accepted) {
+ const kycStatus = await kycStatusRes.json();
+ logger.info(`kyc status: ${j2s(kycStatus)}`);
+ kycUrl = kycStatus.kyc_url;
+ } else if (
+ kycStatusRes.status === HttpStatusCode.UnavailableForLegalReasons
+ ) {
+ const kycStatus = await kycStatusRes.json();
+ logger.info(`aml status: ${j2s(kycStatus)}`);
+ amlStatus = kycStatus.aml_status;
+ } else {
+ throw Error(`unexpected response from kyc-check (${kycStatusRes.status})`);
+ }
+
+ await ctx.transition(
+ {
+ extraStores: ["planchets"],
+ },
+ async (wg2, tx) => {
+ if (!wg2) {
+ return TransitionResult.stay();
+ }
+ for (let i = startIdx; i < requestCoinIdxs.length; i++) {
+ let planchet = await tx.planchets.indexes.byGroupAndIndex.get([
+ withdrawalGroup.withdrawalGroupId,
+ requestCoinIdxs[i],
+ ]);
+ if (!planchet) {
+ continue;
+ }
+ planchet.planchetStatus = PlanchetStatus.KycRequired;
+ await tx.planchets.put(planchet);
+ }
+ if (wg2.status !== WithdrawalGroupStatus.PendingReady) {
+ return TransitionResult.stay();
+ }
+ wg2.kycPending = {
+ paytoHash: uuidResp.h_payto,
+ requirementRow: uuidResp.requirement_row,
+ };
+ wg2.kycUrl = kycUrl;
+ wg2.status =
+ amlStatus === ExchangeAmlStatus.Normal || amlStatus === undefined
+ ? WithdrawalGroupStatus.PendingKyc
+ : amlStatus === ExchangeAmlStatus.Pending
+ ? WithdrawalGroupStatus.PendingAml
+ : amlStatus === ExchangeAmlStatus.Frozen
+ ? WithdrawalGroupStatus.SuspendedAml
+ : assertUnreachable(amlStatus);
+ return TransitionResult.transition(wg2);
+ },
+ );
+}
+
+/**
+ * Send the withdrawal request for a generated planchet to the exchange.
+ *
+ * The verification of the response is done asynchronously to enable parallelism.
+ */
+async function processPlanchetExchangeBatchRequest(
+ wex: WalletExecutionContext,
+ wgContext: WithdrawalGroupStatusInfo,
+ args: WithdrawalRequestBatchArgs,
+): Promise<WithdrawalBatchResult> {
+ const withdrawalGroup: WithdrawalGroupRecord = wgContext.wgRecord;
+ logger.info(
+ `processing planchet exchange batch request ${withdrawalGroup.withdrawalGroupId}, start=${args.coinStartIndex}, len=${args.batchSize}`,
+ );
+
+ const batchReq: ExchangeBatchWithdrawRequest = { planchets: [] };
+ // Indices of coins that are included in the batch request
+ const requestCoinIdxs: number[] = [];
+
+ await wex.db.runReadOnlyTx(
+ { storeNames: ["planchets", "denominations"] },
+ async (tx) => {
+ for (
+ let coinIdx = args.coinStartIndex;
+ coinIdx < args.coinStartIndex + args.batchSize &&
+ coinIdx < wgContext.numPlanchets;
+ coinIdx++
+ ) {
+ let planchet = await tx.planchets.indexes.byGroupAndIndex.get([
+ withdrawalGroup.withdrawalGroupId,
+ coinIdx,
+ ]);
+ if (!planchet) {
+ continue;
+ }
+ if (planchet.planchetStatus === PlanchetStatus.WithdrawalDone) {
+ logger.warn("processPlanchet: planchet already withdrawn");
+ continue;
+ }
+ if (planchet.planchetStatus === PlanchetStatus.AbortedReplaced) {
+ continue;
+ }
+ const denom = await getDenomInfo(
+ wex,
+ tx,
+ withdrawalGroup.exchangeBaseUrl,
+ planchet.denomPubHash,
+ );
+
+ if (!denom) {
+ logger.error("db inconsistent: denom for planchet not found");
+ continue;
+ }
+
+ const planchetReq: ExchangeWithdrawRequest = {
+ denom_pub_hash: planchet.denomPubHash,
+ reserve_sig: planchet.withdrawSig,
+ coin_ev: planchet.coinEv,
+ };
+ batchReq.planchets.push(planchetReq);
+ requestCoinIdxs.push(coinIdx);
+ }
+ },
+ );
+
+ if (batchReq.planchets.length == 0) {
+ logger.warn("empty withdrawal batch");
+ return {
+ batchResp: { ev_sigs: [] },
+ coinIdxs: [],
+ };
+ }
+
+ async function storeCoinError(
+ errDetail: TalerErrorDetail,
+ coinIdx: number,
+ ): Promise<void> {
+ logger.trace(`withdrawal request failed: ${j2s(errDetail)}`);
+ await wex.db.runReadWriteTx({ storeNames: ["planchets"] }, async (tx) => {
+ let planchet = await tx.planchets.indexes.byGroupAndIndex.get([
+ withdrawalGroup.withdrawalGroupId,
+ coinIdx,
+ ]);
+ if (!planchet) {
+ return;
+ }
+ planchet.lastError = errDetail;
+ await tx.planchets.put(planchet);
+ });
+ }
+
+ // FIXME: handle individual error codes better!
+
+ const reqUrl = new URL(
+ `reserves/${withdrawalGroup.reservePub}/batch-withdraw`,
+ withdrawalGroup.exchangeBaseUrl,
+ ).href;
+
+ try {
+ const resp = await wex.http.fetch(reqUrl, {
+ method: "POST",
+ body: batchReq,
+ cancellationToken: wex.cancellationToken,
+ timeout: Duration.fromSpec({ seconds: 40 }),
+ });
+ if (resp.status === HttpStatusCode.UnavailableForLegalReasons) {
+ await handleKycRequired(wex, withdrawalGroup, resp, 0, requestCoinIdxs);
+ return {
+ batchResp: { ev_sigs: [] },
+ coinIdxs: [],
+ };
+ }
+ if (resp.status === HttpStatusCode.Gone) {
+ const e = await readTalerErrorResponse(resp);
+ // FIXME: Store in place of the planchet that is actually affected!
+ await storeCoinError(e, requestCoinIdxs[0]);
+ return {
+ batchResp: { ev_sigs: [] },
+ coinIdxs: [],
+ };
+ }
+ const r = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForExchangeWithdrawBatchResponse(),
+ );
+ return {
+ coinIdxs: requestCoinIdxs,
+ batchResp: r,
+ };
+ } catch (e) {
+ const errDetail = getErrorDetailFromException(e);
+ // We don't know which coin is affected, so we store the error
+ // with the first coin of the batch.
+ await storeCoinError(errDetail, requestCoinIdxs[0]);
+ return {
+ batchResp: { ev_sigs: [] },
+ coinIdxs: [],
+ };
+ }
+}
+
+async function processPlanchetVerifyAndStoreCoin(
+ wex: WalletExecutionContext,
+ wgContext: WithdrawalGroupStatusInfo,
+ coinIdx: number,
+ resp: ExchangeWithdrawResponse,
+): Promise<void> {
+ const withdrawalGroup = wgContext.wgRecord;
+ logger.trace(`checking and storing planchet idx=${coinIdx}`);
+ const d = await wex.db.runReadOnlyTx(
+ { storeNames: ["planchets", "denominations"] },
+ async (tx) => {
+ let planchet = await tx.planchets.indexes.byGroupAndIndex.get([
+ withdrawalGroup.withdrawalGroupId,
+ coinIdx,
+ ]);
+ if (!planchet) {
+ return;
+ }
+ if (planchet.planchetStatus === PlanchetStatus.WithdrawalDone) {
+ logger.warn("processPlanchet: planchet already withdrawn");
+ return;
+ }
+ const denomInfo = await getDenomInfo(
+ wex,
+ tx,
+ withdrawalGroup.exchangeBaseUrl,
+ planchet.denomPubHash,
+ );
+ if (!denomInfo) {
+ return;
+ }
+ return {
+ planchet,
+ denomInfo,
+ exchangeBaseUrl: withdrawalGroup.exchangeBaseUrl,
+ };
+ },
+ );
+
+ if (!d) {
+ return;
+ }
+
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Withdrawal,
+ withdrawalGroupId: wgContext.wgRecord.withdrawalGroupId,
+ });
+
+ const { planchet, denomInfo } = d;
+
+ const planchetDenomPub = denomInfo.denomPub;
+ if (planchetDenomPub.cipher !== DenomKeyType.Rsa) {
+ throw Error(`cipher (${planchetDenomPub.cipher}) not supported`);
+ }
+
+ let evSig = resp.ev_sig;
+ if (!(evSig.cipher === DenomKeyType.Rsa)) {
+ throw Error("unsupported cipher");
+ }
+
+ const denomSigRsa = await wex.cryptoApi.rsaUnblind({
+ bk: planchet.blindingKey,
+ blindedSig: evSig.blinded_rsa_signature,
+ pk: planchetDenomPub.rsa_public_key,
+ });
+
+ const isValid = await wex.cryptoApi.rsaVerify({
+ hm: planchet.coinPub,
+ pk: planchetDenomPub.rsa_public_key,
+ sig: denomSigRsa.sig,
+ });
+
+ if (!isValid) {
+ await wex.db.runReadWriteTx({ storeNames: ["planchets"] }, async (tx) => {
+ let planchet = await tx.planchets.indexes.byGroupAndIndex.get([
+ withdrawalGroup.withdrawalGroupId,
+ coinIdx,
+ ]);
+ if (!planchet) {
+ return;
+ }
+ planchet.lastError = makeErrorDetail(
+ TalerErrorCode.WALLET_EXCHANGE_COIN_SIGNATURE_INVALID,
+ {},
+ "invalid signature from the exchange after unblinding",
+ );
+ await tx.planchets.put(planchet);
+ });
+ return;
+ }
+
+ let denomSig: UnblindedSignature;
+ if (planchetDenomPub.cipher === DenomKeyType.Rsa) {
+ denomSig = {
+ cipher: planchetDenomPub.cipher,
+ rsa_signature: denomSigRsa.sig,
+ };
+ } else {
+ throw Error("unsupported cipher");
+ }
+
+ const coin: CoinRecord = {
+ blindingKey: planchet.blindingKey,
+ coinPriv: planchet.coinPriv,
+ coinPub: planchet.coinPub,
+ denomPubHash: planchet.denomPubHash,
+ denomSig,
+ coinEvHash: planchet.coinEvHash,
+ exchangeBaseUrl: d.exchangeBaseUrl,
+ status: CoinStatus.Fresh,
+ coinSource: {
+ type: CoinSourceType.Withdraw,
+ coinIndex: coinIdx,
+ reservePub: withdrawalGroup.reservePub,
+ withdrawalGroupId: withdrawalGroup.withdrawalGroupId,
+ },
+ sourceTransactionId: transactionId,
+ maxAge: withdrawalGroup.restrictAge ?? AgeRestriction.AGE_UNRESTRICTED,
+ ageCommitmentProof: planchet.ageCommitmentProof,
+ spendAllocation: undefined,
+ };
+
+ const planchetCoinPub = planchet.coinPub;
+
+ wgContext.planchetsFinished.add(planchet.coinPub);
+
+ await wex.db.runReadWriteTx(
+ { storeNames: ["planchets", "coins", "coinAvailability", "denominations"] },
+ async (tx) => {
+ const p = await tx.planchets.get(planchetCoinPub);
+ if (!p || p.planchetStatus === PlanchetStatus.WithdrawalDone) {
+ return;
+ }
+ p.planchetStatus = PlanchetStatus.WithdrawalDone;
+ p.lastError = undefined;
+ await tx.planchets.put(p);
+ await makeCoinAvailable(wex, tx, coin);
+ },
+ );
+}
+
+/**
+ * Make sure that denominations that currently can be used for withdrawal
+ * are validated, and the result of validation is stored in the database.
+ */
+export async function updateWithdrawalDenoms(
+ wex: WalletExecutionContext,
+ exchangeBaseUrl: string,
+): Promise<void> {
+ logger.trace(
+ `updating denominations used for withdrawal for ${exchangeBaseUrl}`,
+ );
+ const exchangeDetails = await wex.db.runReadOnlyTx(
+ { storeNames: ["exchanges", "exchangeDetails"] },
+ async (tx) => {
+ return getExchangeWireDetailsInTx(tx, exchangeBaseUrl);
+ },
+ );
+ if (!exchangeDetails) {
+ logger.error("exchange details not available");
+ throw Error(`exchange ${exchangeBaseUrl} details not available`);
+ }
+ // First do a pass where the validity of candidate denominations
+ // is checked and the result is stored in the database.
+ logger.trace("getting candidate denominations");
+ const denominations = await getCandidateWithdrawalDenoms(
+ wex,
+ exchangeBaseUrl,
+ exchangeDetails.currency,
+ );
+ logger.trace(`got ${denominations.length} candidate denominations`);
+ const batchSize = 500;
+ let current = 0;
+
+ while (current < denominations.length) {
+ const updatedDenominations: DenominationRecord[] = [];
+ // Do a batch of batchSize
+ for (
+ let batchIdx = 0;
+ batchIdx < batchSize && current < denominations.length;
+ batchIdx++, current++
+ ) {
+ const denom = denominations[current];
+ if (
+ denom.verificationStatus === DenominationVerificationStatus.Unverified
+ ) {
+ logger.trace(
+ `Validating denomination (${current + 1}/${
+ denominations.length
+ }) signature of ${denom.denomPubHash}`,
+ );
+ let valid = false;
+ if (wex.ws.config.testing.insecureTrustExchange) {
+ valid = true;
+ } else {
+ const res = await wex.cryptoApi.isValidDenom({
+ denom,
+ masterPub: exchangeDetails.masterPublicKey,
+ });
+ valid = res.valid;
+ }
+ logger.trace(`Done validating ${denom.denomPubHash}`);
+ if (!valid) {
+ logger.warn(
+ `Signature check for denomination h=${denom.denomPubHash} failed`,
+ );
+ denom.verificationStatus = DenominationVerificationStatus.VerifiedBad;
+ } else {
+ denom.verificationStatus =
+ DenominationVerificationStatus.VerifiedGood;
+ }
+ updatedDenominations.push(denom);
+ }
+ }
+ if (updatedDenominations.length > 0) {
+ logger.trace("writing denomination batch to db");
+ await wex.db.runReadWriteTx(
+ { storeNames: ["denominations"] },
+ async (tx) => {
+ for (let i = 0; i < updatedDenominations.length; i++) {
+ const denom = updatedDenominations[i];
+ await tx.denominations.put(denom);
+ }
+ },
+ );
+ wex.ws.denomInfoCache.clear();
+ logger.trace("done with DB write");
+ }
+ }
+}
+
+/**
+ * Update the information about a reserve that is stored in the wallet
+ * by querying the reserve's exchange.
+ *
+ * If the reserve have funds that are not allocated in a withdrawal group yet
+ * and are big enough to withdraw with available denominations,
+ * create a new withdrawal group for the remaining amount.
+ */
+async function processQueryReserve(
+ wex: WalletExecutionContext,
+ withdrawalGroupId: string,
+): Promise<TaskRunResult> {
+ const ctx = new WithdrawTransactionContext(wex, withdrawalGroupId);
+ const withdrawalGroup = await getWithdrawalGroupRecordTx(wex.db, {
+ withdrawalGroupId,
+ });
+ if (!withdrawalGroup) {
+ return TaskRunResult.finished();
+ }
+ if (withdrawalGroup.status !== WithdrawalGroupStatus.PendingQueryingStatus) {
+ return TaskRunResult.backoff();
+ }
+ const reservePub = withdrawalGroup.reservePub;
+
+ const reserveUrl = new URL(
+ `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,
+ });
+
+ logger.trace(`reserve status code: HTTP ${resp.status}`);
+
+ const result = await readSuccessResponseJsonOrErrorCode(
+ resp,
+ codecForReserveStatus(),
+ );
+
+ if (result.isError) {
+ logger.trace(
+ `got reserve status error, EC=${result.talerErrorResponse.code}`,
+ );
+ if (resp.status === HttpStatusCode.NotFound) {
+ return TaskRunResult.longpollReturnedPending();
+ } else {
+ throwUnexpectedRequestError(resp, result.talerErrorResponse);
+ }
+ }
+
+ logger.trace(`got reserve status ${j2s(result.response)}`);
+
+ const transitionResult = await ctx.transition({}, async (wg) => {
+ if (!wg) {
+ logger.warn(`withdrawal group ${withdrawalGroupId} not found`);
+ return TransitionResult.stay();
+ }
+ wg.status = WithdrawalGroupStatus.PendingReady;
+ wg.reserveBalanceAmount = Amounts.stringify(result.response.balance);
+ return TransitionResult.transition(wg);
+ });
+
+ if (transitionResult) {
+ return TaskRunResult.progress();
+ } else {
+ return TaskRunResult.backoff();
+ }
+}
+
+/**
+ * Withdrawal context that is kept in-memory.
+ *
+ * Used to store some cached info during a withdrawal operation.
+ */
+interface WithdrawalGroupStatusInfo {
+ numPlanchets: number;
+ planchetsFinished: Set<string>;
+
+ /**
+ * Cached withdrawal group record from the database.
+ */
+ wgRecord: WithdrawalGroupRecord;
+}
+
+async function processWithdrawalGroupAbortingBank(
+ wex: WalletExecutionContext,
+ withdrawalGroup: WithdrawalGroupRecord,
+): Promise<TaskRunResult> {
+ const { withdrawalGroupId } = withdrawalGroup;
+ const ctx = new WithdrawTransactionContext(wex, withdrawalGroupId);
+ const wgInfo = withdrawalGroup.wgInfo;
+ if (wgInfo.withdrawalType != WithdrawalRecordType.BankIntegrated) {
+ throw Error("invalid state (aborting(bank) without bank info");
+ }
+ const abortUrl = getBankAbortUrl(wgInfo.bankInfo.talerWithdrawUri);
+ logger.info(`aborting withdrawal at ${abortUrl}`);
+ const abortResp = await wex.http.fetch(abortUrl, {
+ method: "POST",
+ body: {},
+ cancellationToken: wex.cancellationToken,
+ });
+ logger.info(`abort response status: ${abortResp.status}`);
+
+ await ctx.transition({}, async (wg) => {
+ if (!wg) {
+ return TransitionResult.stay();
+ }
+ wg.status = WithdrawalGroupStatus.AbortedBank;
+ wg.timestampFinish = timestampPreciseToDb(TalerPreciseTimestamp.now());
+ return TransitionResult.transition(wg);
+ });
+ return TaskRunResult.finished();
+}
+
+async function processWithdrawalGroupPendingKyc(
+ wex: WalletExecutionContext,
+ withdrawalGroup: WithdrawalGroupRecord,
+): Promise<TaskRunResult> {
+ const ctx = new WithdrawTransactionContext(
+ wex,
+ withdrawalGroup.withdrawalGroupId,
+ );
+ const userType = "individual";
+ const kycInfo = withdrawalGroup.kycPending;
+ if (!kycInfo) {
+ throw Error("no kyc info available in pending(kyc)");
+ }
+ const exchangeUrl = withdrawalGroup.exchangeBaseUrl;
+ const url = new URL(
+ `kyc-check/${kycInfo.requirementRow}/${kycInfo.paytoHash}/${userType}`,
+ exchangeUrl,
+ );
+ url.searchParams.set("timeout_ms", "30000");
+ logger.info(`long-polling for withdrawal KYC status via ${url.href}`);
+ const kycStatusRes = await wex.http.fetch(url.href, {
+ method: "GET",
+ cancellationToken: wex.cancellationToken,
+ });
+ logger.info(`kyc long-polling response status: HTTP ${kycStatusRes.status}`);
+ if (
+ kycStatusRes.status === HttpStatusCode.Ok ||
+ // FIXME: NoContent is not expected https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-merge
+ // remove after the exchange is fixed or clarified
+ kycStatusRes.status === HttpStatusCode.NoContent
+ ) {
+ await ctx.transition({}, async (rec) => {
+ if (!rec) {
+ return TransitionResult.stay();
+ }
+ switch (rec.status) {
+ case WithdrawalGroupStatus.PendingKyc: {
+ delete rec.kycPending;
+ delete rec.kycUrl;
+ rec.status = WithdrawalGroupStatus.PendingReady;
+ return TransitionResult.transition(rec);
+ }
+ default:
+ return TransitionResult.stay();
+ }
+ });
+ } else if (kycStatusRes.status === HttpStatusCode.Accepted) {
+ const kycStatus = await kycStatusRes.json();
+ logger.info(`kyc status: ${j2s(kycStatus)}`);
+ const kycUrl = kycStatus.kyc_url;
+ if (typeof kycUrl === "string") {
+ await ctx.transition({}, async (rec) => {
+ if (!rec) {
+ return TransitionResult.stay();
+ }
+ switch (rec.status) {
+ case WithdrawalGroupStatus.PendingReady: {
+ rec.kycUrl = kycUrl;
+ return TransitionResult.transition(rec);
+ }
+ }
+ return TransitionResult.stay();
+ });
+ }
+ } else if (
+ kycStatusRes.status === HttpStatusCode.UnavailableForLegalReasons
+ ) {
+ const kycStatus = await kycStatusRes.json();
+ logger.info(`aml status: ${j2s(kycStatus)}`);
+ } else {
+ throw Error(`unexpected response from kyc-check (${kycStatusRes.status})`);
+ }
+ return TaskRunResult.backoff();
+}
+
+/**
+ * Select new denominations for a withdrawal group.
+ * Necessary when denominations expired or got revoked
+ * before the withdrawal could complete.
+ */
+async function redenominateWithdrawal(
+ wex: WalletExecutionContext,
+ withdrawalGroupId: string,
+): Promise<void> {
+ logger.trace(`redenominating withdrawal group ${withdrawalGroupId}`);
+ await wex.db.runReadWriteTx(
+ { storeNames: ["withdrawalGroups", "planchets", "denominations"] },
+ async (tx) => {
+ const wg = await tx.withdrawalGroups.get(withdrawalGroupId);
+ if (!wg) {
+ return;
+ }
+ const currency = Amounts.currencyOf(wg.denomsSel.totalWithdrawCost);
+ const exchangeBaseUrl = wg.exchangeBaseUrl;
+
+ const candidates = await getCandidateWithdrawalDenomsTx(
+ wex,
+ tx,
+ exchangeBaseUrl,
+ currency,
+ );
+
+ const oldSel = wg.denomsSel;
+
+ if (logger.shouldLogTrace()) {
+ logger.trace(`old denom sel: ${j2s(oldSel)}`);
+ }
+
+ let zero = Amount.zeroOfCurrency(currency);
+ let amountRemaining = zero;
+ let prevTotalCoinValue = zero;
+ let prevTotalWithdrawalCost = zero;
+ let prevHasDenomWithAgeRestriction = false;
+ let prevEarliestDepositExpiration = AbsoluteTime.never();
+ let prevDenoms: DenomSelItem[] = [];
+ let coinIndex = 0;
+ for (let i = 0; i < oldSel.selectedDenoms.length; i++) {
+ const sel = wg.denomsSel.selectedDenoms[i];
+ const denom = await tx.denominations.get([
+ exchangeBaseUrl,
+ sel.denomPubHash,
+ ]);
+ if (!denom) {
+ throw Error("denom in use but not not found");
+ }
+ // FIXME: Also check planchet if there was a different error or planchet already withdrawn
+ let denomOkay = isWithdrawableDenom(
+ denom,
+ wex.ws.config.testing.denomselAllowLate,
+ );
+ const numCoins = sel.count - (sel.skip ?? 0);
+ const denomValue = Amount.from(denom.value).mult(numCoins);
+ const denomFeeWithdraw = Amount.from(denom.fees.feeWithdraw).mult(
+ numCoins,
+ );
+ if (denomOkay) {
+ prevTotalCoinValue = prevTotalCoinValue.add(denomValue);
+ prevTotalWithdrawalCost = prevTotalWithdrawalCost.add(
+ denomValue,
+ denomFeeWithdraw,
+ );
+ prevDenoms.push({
+ count: sel.count,
+ denomPubHash: sel.denomPubHash,
+ skip: sel.skip,
+ });
+ prevHasDenomWithAgeRestriction =
+ prevHasDenomWithAgeRestriction || denom.denomPub.age_mask > 0;
+ prevEarliestDepositExpiration = AbsoluteTime.min(
+ prevEarliestDepositExpiration,
+ timestampAbsoluteFromDb(denom.stampExpireDeposit),
+ );
+ } else {
+ amountRemaining = amountRemaining.add(denomValue, denomFeeWithdraw);
+ prevDenoms.push({
+ count: sel.count,
+ denomPubHash: sel.denomPubHash,
+ skip: (sel.skip ?? 0) + numCoins,
+ });
+
+ for (let j = 0; j < sel.count; j++) {
+ const ci = coinIndex + j;
+ const p = await tx.planchets.indexes.byGroupAndIndex.get([
+ withdrawalGroupId,
+ ci,
+ ]);
+ if (!p) {
+ // Maybe planchet wasn't yet generated.
+ // No problem!
+ logger.info(
+ `not aborting planchet #${coinIndex}, planchet not found`,
+ );
+ continue;
+ }
+ logger.info(`aborting planchet #${coinIndex}`);
+ p.planchetStatus = PlanchetStatus.AbortedReplaced;
+ await tx.planchets.put(p);
+ }
+ }
+
+ coinIndex += sel.count;
+ }
+
+ const newSel = selectWithdrawalDenominations(
+ amountRemaining.toJson(),
+ candidates,
+ );
+
+ if (logger.shouldLogTrace()) {
+ logger.trace(`new denom sel: ${j2s(newSel)}`);
+ }
+
+ const mergedSel: DenomSelectionState = {
+ selectedDenoms: [...prevDenoms, ...newSel.selectedDenoms],
+ totalCoinValue: zero
+ .add(prevTotalCoinValue, newSel.totalCoinValue)
+ .toString(),
+ totalWithdrawCost: zero
+ .add(prevTotalWithdrawalCost, newSel.totalWithdrawCost)
+ .toString(),
+ hasDenomWithAgeRestriction:
+ prevHasDenomWithAgeRestriction || newSel.hasDenomWithAgeRestriction,
+ earliestDepositExpiration: AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.min(
+ prevEarliestDepositExpiration,
+ AbsoluteTime.fromProtocolTimestamp(
+ newSel.earliestDepositExpiration,
+ ),
+ ),
+ ),
+ };
+ wg.denomsSel = mergedSel;
+ if (logger.shouldLogTrace()) {
+ logger.trace(`merged denom sel: ${j2s(mergedSel)}`);
+ }
+ await tx.withdrawalGroups.put(wg);
+ },
+ );
+}
+
+async function processWithdrawalGroupPendingReady(
+ wex: WalletExecutionContext,
+ withdrawalGroup: WithdrawalGroupRecord,
+): Promise<TaskRunResult> {
+ const { withdrawalGroupId } = withdrawalGroup;
+ const ctx = new WithdrawTransactionContext(wex, withdrawalGroupId);
+
+ const exchangeBaseUrl = withdrawalGroup.exchangeBaseUrl;
+
+ await fetchFreshExchange(wex, withdrawalGroup.exchangeBaseUrl);
+
+ if (withdrawalGroup.denomsSel.selectedDenoms.length === 0) {
+ logger.warn("Finishing empty withdrawal group (no denoms)");
+ await ctx.transition({}, async (wg) => {
+ if (!wg) {
+ return TransitionResult.stay();
+ }
+ wg.status = WithdrawalGroupStatus.Done;
+ wg.timestampFinish = timestampPreciseToDb(TalerPreciseTimestamp.now());
+ return TransitionResult.transition(wg);
+ });
+ return TaskRunResult.finished();
+ }
+
+ const numTotalCoins = withdrawalGroup.denomsSel.selectedDenoms
+ .map((x) => x.count)
+ .reduce((a, b) => a + b);
+
+ const wgContext: WithdrawalGroupStatusInfo = {
+ numPlanchets: numTotalCoins,
+ planchetsFinished: new Set<string>(),
+ wgRecord: withdrawalGroup,
+ };
+
+ await wex.db.runReadOnlyTx({ storeNames: ["planchets"] }, async (tx) => {
+ const planchets =
+ await tx.planchets.indexes.byGroup.getAll(withdrawalGroupId);
+ for (const p of planchets) {
+ if (p.planchetStatus === PlanchetStatus.WithdrawalDone) {
+ wgContext.planchetsFinished.add(p.coinPub);
+ }
+ }
+ });
+
+ // We sequentially generate planchets, so that
+ // large withdrawal groups don't make the wallet unresponsive.
+ for (let i = 0; i < numTotalCoins; i++) {
+ await processPlanchetGenerate(wex, withdrawalGroup, i);
+ }
+
+ const maxBatchSize = 100;
+
+ for (let i = 0; i < numTotalCoins; i += maxBatchSize) {
+ const resp = await processPlanchetExchangeBatchRequest(wex, wgContext, {
+ batchSize: maxBatchSize,
+ coinStartIndex: i,
+ });
+ let work: Promise<void>[] = [];
+ work = [];
+ for (let j = 0; j < resp.coinIdxs.length; j++) {
+ if (!resp.batchResp.ev_sigs[j]) {
+ // response may not be available when there is kyc needed
+ continue;
+ }
+ work.push(
+ processPlanchetVerifyAndStoreCoin(
+ wex,
+ wgContext,
+ resp.coinIdxs[j],
+ resp.batchResp.ev_sigs[j],
+ ),
+ );
+ }
+ await Promise.all(work);
+ }
+
+ let redenomRequired = false;
+
+ await wex.db.runReadOnlyTx({ storeNames: ["planchets"] }, async (tx) => {
+ const planchets =
+ await tx.planchets.indexes.byGroup.getAll(withdrawalGroupId);
+ for (const p of planchets) {
+ if (p.planchetStatus !== PlanchetStatus.Pending) {
+ continue;
+ }
+ if (!p.lastError) {
+ continue;
+ }
+ switch (p.lastError.code) {
+ case TalerErrorCode.EXCHANGE_GENERIC_DENOMINATION_EXPIRED:
+ case TalerErrorCode.EXCHANGE_GENERIC_DENOMINATION_REVOKED:
+ redenomRequired = true;
+ return;
+ }
+ }
+ });
+
+ if (redenomRequired) {
+ logger.warn(`withdrawal ${withdrawalGroupId} requires redenomination`);
+ await fetchFreshExchange(wex, exchangeBaseUrl, {
+ forceUpdate: true,
+ });
+ await updateWithdrawalDenoms(wex, exchangeBaseUrl);
+ await redenominateWithdrawal(wex, withdrawalGroupId);
+ return TaskRunResult.backoff();
+ }
+
+ const errorsPerCoin: Record<number, TalerErrorDetail> = {};
+ let numPlanchetErrors = 0;
+ let numActive = 0;
+ let numDone = 0;
+ const maxReportedErrors = 5;
+
+ const res = await ctx.transition(
+ {
+ extraStores: ["coins", "coinAvailability", "planchets"],
+ },
+ async (wg, tx) => {
+ if (!wg) {
+ return TransitionResult.stay();
+ }
+
+ const groupPlanchets =
+ await tx.planchets.indexes.byGroup.getAll(withdrawalGroupId);
+ for (const x of groupPlanchets) {
+ switch (x.planchetStatus) {
+ case PlanchetStatus.KycRequired:
+ case PlanchetStatus.Pending:
+ numActive++;
+ break;
+ case PlanchetStatus.WithdrawalDone:
+ numDone++;
+ break;
+ }
+ if (x.lastError) {
+ numPlanchetErrors++;
+ if (numPlanchetErrors < maxReportedErrors) {
+ errorsPerCoin[x.coinIdx] = x.lastError;
+ }
+ }
+ }
+
+ if (wg.timestampFinish === undefined && numActive === 0) {
+ wg.timestampFinish = timestampPreciseToDb(TalerPreciseTimestamp.now());
+ wg.status = WithdrawalGroupStatus.Done;
+ await makeCoinsVisible(wex, tx, ctx.transactionId);
+ }
+ return TransitionResult.transition(wg);
+ },
+ );
+
+ if (!res) {
+ throw Error("withdrawal group does not exist anymore");
+ }
+
+ wex.ws.notify({
+ type: NotificationType.BalanceChange,
+ hintTransactionId: ctx.transactionId,
+ });
+
+ if (numPlanchetErrors > 0) {
+ return {
+ type: TaskRunResultType.Error,
+ errorDetail: makeErrorDetail(
+ TalerErrorCode.WALLET_WITHDRAWAL_GROUP_INCOMPLETE,
+ {
+ errorsPerCoin,
+ numErrors: numPlanchetErrors,
+ },
+ ),
+ };
+ }
+
+ return TaskRunResult.backoff();
+}
+
+export async function processWithdrawalGroup(
+ wex: WalletExecutionContext,
+ withdrawalGroupId: string,
+): Promise<TaskRunResult> {
+ logger.trace("processing withdrawal group", withdrawalGroupId);
+ const withdrawalGroup = await wex.db.runReadOnlyTx(
+ { storeNames: ["withdrawalGroups"] },
+ async (tx) => {
+ return tx.withdrawalGroups.get(withdrawalGroupId);
+ },
+ );
+
+ if (!withdrawalGroup) {
+ throw Error(`withdrawal group ${withdrawalGroupId} not found`);
+ }
+
+ switch (withdrawalGroup.status) {
+ case WithdrawalGroupStatus.PendingRegisteringBank:
+ return await processBankRegisterReserve(wex, withdrawalGroupId);
+ case WithdrawalGroupStatus.PendingQueryingStatus:
+ return processQueryReserve(wex, withdrawalGroupId);
+ case WithdrawalGroupStatus.PendingWaitConfirmBank:
+ return await processReserveBankStatus(wex, withdrawalGroupId);
+ case WithdrawalGroupStatus.PendingAml:
+ // FIXME: Handle this case, withdrawal doesn't support AML yet.
+ return TaskRunResult.backoff();
+ case WithdrawalGroupStatus.PendingKyc:
+ return processWithdrawalGroupPendingKyc(wex, withdrawalGroup);
+ case WithdrawalGroupStatus.PendingReady:
+ // Continue with the actual withdrawal!
+ return await processWithdrawalGroupPendingReady(wex, withdrawalGroup);
+ case WithdrawalGroupStatus.AbortingBank:
+ return await processWithdrawalGroupAbortingBank(wex, withdrawalGroup);
+ case WithdrawalGroupStatus.AbortedBank:
+ case WithdrawalGroupStatus.AbortedExchange:
+ case WithdrawalGroupStatus.FailedAbortingBank:
+ case WithdrawalGroupStatus.SuspendedAbortingBank:
+ case WithdrawalGroupStatus.SuspendedAml:
+ case WithdrawalGroupStatus.SuspendedKyc:
+ case WithdrawalGroupStatus.SuspendedQueryingStatus:
+ case WithdrawalGroupStatus.SuspendedReady:
+ case WithdrawalGroupStatus.SuspendedRegisteringBank:
+ case WithdrawalGroupStatus.SuspendedWaitConfirmBank:
+ case WithdrawalGroupStatus.Done:
+ case WithdrawalGroupStatus.FailedBankAborted:
+ // Nothing to do.
+ return TaskRunResult.finished();
+ default:
+ assertUnreachable(withdrawalGroup.status);
+ }
+}
+
+const AGE_MASK_GROUPS = "8:10:12:14:16:18"
+ .split(":")
+ .map((n) => parseInt(n, 10));
+
+export async function getExchangeWithdrawalInfo(
+ wex: WalletExecutionContext,
+ exchangeBaseUrl: string,
+ instructedAmount: AmountJson,
+ ageRestricted: number | undefined,
+): Promise<ExchangeWithdrawalDetails> {
+ logger.trace("updating exchange");
+ const exchange = await fetchFreshExchange(wex, exchangeBaseUrl, {
+ cancellationToken: wex.cancellationToken,
+ });
+
+ wex.cancellationToken.throwIfCancelled();
+
+ if (exchange.currency != instructedAmount.currency) {
+ // Specifying the amount in the conversion input currency is not yet supported.
+ // We might add support for it later.
+ throw new Error(
+ `withdrawal only supported when specifying target currency ${exchange.currency}`,
+ );
+ }
+
+ const withdrawalAccountsList = await fetchWithdrawalAccountInfo(
+ wex,
+ {
+ exchange,
+ instructedAmount,
+ },
+ wex.cancellationToken,
+ );
+
+ logger.trace("updating withdrawal denoms");
+ await updateWithdrawalDenoms(wex, exchangeBaseUrl);
+
+ wex.cancellationToken.throwIfCancelled();
+
+ logger.trace("getting candidate denoms");
+ const candidateDenoms = await getCandidateWithdrawalDenoms(
+ wex,
+ exchangeBaseUrl,
+ instructedAmount.currency,
+ );
+
+ wex.cancellationToken.throwIfCancelled();
+
+ logger.trace("selecting withdrawal denoms");
+ // FIXME: Why not in a transaction?
+ const selectedDenoms = selectWithdrawalDenominations(
+ instructedAmount,
+ candidateDenoms,
+ wex.ws.config.testing.denomselAllowLate,
+ );
+
+ 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) {
+ exchangeWireAccounts.push(account.payto_uri);
+ }
+
+ let versionMatch;
+ if (exchange.protocolVersionRange) {
+ versionMatch = LibtoolVersion.compare(
+ WALLET_EXCHANGE_PROTOCOL_VERSION,
+ exchange.protocolVersionRange,
+ );
+
+ if (
+ versionMatch &&
+ !versionMatch.compatible &&
+ versionMatch.currentCmp === -1
+ ) {
+ logger.warn(
+ `wallet's support for exchange protocol version ${WALLET_EXCHANGE_PROTOCOL_VERSION} might be outdated ` +
+ `(exchange has ${exchange.protocolVersionRange}), checking for updates`,
+ );
+ }
+ }
+
+ let tosAccepted = false;
+ if (exchange.tosAcceptedTimestamp) {
+ if (exchange.tosAcceptedEtag === exchange.tosCurrentEtag) {
+ tosAccepted = true;
+ }
+ }
+
+ const paytoUris = exchange.wireInfo.accounts.map((x) => x.payto_uri);
+ if (!paytoUris) {
+ throw Error("exchange is in invalid state");
+ }
+
+ const ret: ExchangeWithdrawalDetails = {
+ earliestDepositExpiration: selectedDenoms.earliestDepositExpiration,
+ exchangePaytoUris: paytoUris,
+ exchangeWireAccounts,
+ exchangeCreditAccountDetails: withdrawalAccountsList,
+ exchangeVersion: exchange.protocolVersionRange || "unknown",
+ selectedDenoms,
+ versionMatch,
+ walletVersion: WALLET_EXCHANGE_PROTOCOL_VERSION,
+ termsOfServiceAccepted: tosAccepted,
+ withdrawalAmountEffective: Amounts.stringify(selectedDenoms.totalCoinValue),
+ withdrawalAmountRaw: Amounts.stringify(instructedAmount),
+ // TODO: remove hardcoding, this should be calculated from the denominations info
+ // force enabled for testing
+ ageRestrictionOptions: selectedDenoms.hasDenomWithAgeRestriction
+ ? AGE_MASK_GROUPS
+ : undefined,
+ scopeInfo: exchange.scopeInfo,
+ };
+ return ret;
+}
+
+export interface GetWithdrawalDetailsForUriOpts {
+ restrictAge?: number;
+ notifyChangeFromPendingTimeoutMs?: number;
+}
+
+type WithdrawalOperationMemoryMap = {
+ [uri: string]: boolean | undefined;
+};
+
+const ongoingChecks: WithdrawalOperationMemoryMap = {};
+
+/**
+ * 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,
+ opts: GetWithdrawalDetailsForUriOpts = {},
+): Promise<WithdrawUriInfoResponse> {
+ logger.trace(`getting withdrawal details for URI ${talerWithdrawUri}`);
+ const info = await getBankWithdrawalInfo(wex.http, talerWithdrawUri);
+ logger.trace(`got bank info`);
+ if (info.suggestedExchange) {
+ try {
+ // If the exchange entry doesn't exist yet,
+ // it'll be created as an ephemeral entry.
+ await fetchFreshExchange(wex, info.suggestedExchange);
+ } catch (e) {
+ // We still continued if it failed, as other exchanges might be available.
+ // We don't want to fail if the bank-suggested exchange is broken/offline.
+ logger.trace(
+ `querying bank-suggested exchange (${info.suggestedExchange}) failed`,
+ );
+ }
+ }
+
+ const currency = Amounts.currencyOf(info.amount);
+
+ const listExchangesResp = await listExchanges(wex);
+ const possibleExchanges = listExchangesResp.exchanges.filter((x) => {
+ return (
+ x.currency === currency &&
+ (x.exchangeUpdateStatus === ExchangeUpdateStatus.Ready ||
+ x.exchangeUpdateStatus === ExchangeUpdateStatus.ReadyUpdate)
+ );
+ });
+
+ // FIXME: this should be removed after the extended version of
+ // withdrawal state machine. issue #8099
+ if (
+ info.status === "pending" &&
+ opts.notifyChangeFromPendingTimeoutMs !== undefined &&
+ !ongoingChecks[talerWithdrawUri]
+ ) {
+ ongoingChecks[talerWithdrawUri] = true;
+ const bankApi = new TalerBankIntegrationHttpClient(
+ info.apiBaseUrl,
+ wex.http,
+ );
+
+ bankApi
+ .getWithdrawalOperationById(info.operationId, {
+ old_state: "pending",
+ timeoutMs: opts.notifyChangeFromPendingTimeoutMs,
+ })
+ .then((resp) => {
+ if (resp.type === "ok" && resp.body.status !== "pending") {
+ wex.ws.notify({
+ type: NotificationType.WithdrawalOperationTransition,
+ uri: talerWithdrawUri,
+ });
+ }
+ })
+ .finally(() => {
+ ongoingChecks[talerWithdrawUri] = false;
+ });
+ }
+
+ return {
+ operationId: info.operationId,
+ confirmTransferUrl: info.confirmTransferUrl,
+ status: info.status,
+ amount: Amounts.stringify(info.amount),
+ defaultExchangeBaseUrl: info.suggestedExchange,
+ possibleExchanges,
+ };
+}
+
+export function augmentPaytoUrisForWithdrawal(
+ plainPaytoUris: string[],
+ reservePub: string,
+ instructedAmount: AmountLike,
+): string[] {
+ return plainPaytoUris.map((x) =>
+ addPaytoQueryParams(x, {
+ amount: Amounts.stringify(instructedAmount),
+ message: `Taler Withdrawal ${reservePub}`,
+ }),
+ );
+}
+
+/**
+ * Get payto URIs that can be used to fund a withdrawal operation.
+ */
+export async function getFundingPaytoUris(
+ tx: WalletDbReadOnlyTransaction<
+ ["withdrawalGroups", "exchanges", "exchangeDetails"]
+ >,
+ withdrawalGroupId: string,
+): Promise<string[]> {
+ const withdrawalGroup = await tx.withdrawalGroups.get(withdrawalGroupId);
+ checkDbInvariant(!!withdrawalGroup);
+ const exchangeDetails = await getExchangeWireDetailsInTx(
+ tx,
+ withdrawalGroup.exchangeBaseUrl,
+ );
+ if (!exchangeDetails) {
+ logger.error(`exchange ${withdrawalGroup.exchangeBaseUrl} not found`);
+ return [];
+ }
+ const plainPaytoUris =
+ exchangeDetails.wireInfo?.accounts.map((x) => x.payto_uri) ?? [];
+ if (!plainPaytoUris) {
+ logger.error(
+ `exchange ${withdrawalGroup.exchangeBaseUrl} has no wire info`,
+ );
+ return [];
+ }
+ return augmentPaytoUrisForWithdrawal(
+ plainPaytoUris,
+ withdrawalGroup.reservePub,
+ withdrawalGroup.instructedAmount,
+ );
+}
+
+async function getWithdrawalGroupRecordTx(
+ db: DbAccess<typeof WalletStoresV1>,
+ req: {
+ withdrawalGroupId: string;
+ },
+): Promise<WithdrawalGroupRecord | undefined> {
+ return await db.runReadOnlyTx(
+ { storeNames: ["withdrawalGroups"] },
+ async (tx) => {
+ return tx.withdrawalGroups.get(req.withdrawalGroupId);
+ },
+ );
+}
+
+export function getReserveRequestTimeout(r: WithdrawalGroupRecord): Duration {
+ return { d_ms: 60000 };
+}
+
+export function getBankStatusUrl(talerWithdrawUri: string): string {
+ const uriResult = parseWithdrawUri(talerWithdrawUri);
+ if (!uriResult) {
+ throw Error(`can't parse withdrawal URL ${talerWithdrawUri}`);
+ }
+ const url = new URL(
+ `withdrawal-operation/${uriResult.withdrawalOperationId}`,
+ uriResult.bankIntegrationApiBaseUrl,
+ );
+ return url.href;
+}
+
+export function getBankAbortUrl(talerWithdrawUri: string): string {
+ const uriResult = parseWithdrawUri(talerWithdrawUri);
+ if (!uriResult) {
+ throw Error(`can't parse withdrawal URL ${talerWithdrawUri}`);
+ }
+ const url = new URL(
+ `withdrawal-operation/${uriResult.withdrawalOperationId}/abort`,
+ uriResult.bankIntegrationApiBaseUrl,
+ );
+ return url.href;
+}
+
+async function registerReserveWithBank(
+ wex: WalletExecutionContext,
+ withdrawalGroupId: string,
+): Promise<void> {
+ const withdrawalGroup = await wex.db.runReadOnlyTx(
+ { storeNames: ["withdrawalGroups"] },
+ async (tx) => {
+ return await tx.withdrawalGroups.get(withdrawalGroupId);
+ },
+ );
+ const ctx = new WithdrawTransactionContext(wex, withdrawalGroupId);
+ switch (withdrawalGroup?.status) {
+ case WithdrawalGroupStatus.PendingWaitConfirmBank:
+ case WithdrawalGroupStatus.PendingRegisteringBank:
+ break;
+ default:
+ return;
+ }
+ if (
+ withdrawalGroup.wgInfo.withdrawalType != WithdrawalRecordType.BankIntegrated
+ ) {
+ throw Error("expecting withdrawal type = bank integrated");
+ }
+ const bankInfo = withdrawalGroup.wgInfo.bankInfo;
+ if (!bankInfo) {
+ return;
+ }
+ const bankStatusUrl = getBankStatusUrl(bankInfo.talerWithdrawUri);
+ const reqBody = {
+ reserve_pub: withdrawalGroup.reservePub,
+ selected_exchange: bankInfo.exchangePaytoUri,
+ };
+ logger.info(`registering reserve with bank: ${j2s(reqBody)}`);
+ const httpResp = await wex.http.fetch(bankStatusUrl, {
+ method: "POST",
+ body: reqBody,
+ timeout: getReserveRequestTimeout(withdrawalGroup),
+ cancellationToken: wex.cancellationToken,
+ });
+ const status = await readSuccessResponseJsonOrThrow(
+ httpResp,
+ codeForBankWithdrawalOperationPostResponse(),
+ );
+
+ await ctx.transition({}, async (r) => {
+ if (!r) {
+ return TransitionResult.stay();
+ }
+ switch (r.status) {
+ case WithdrawalGroupStatus.PendingRegisteringBank:
+ case WithdrawalGroupStatus.PendingWaitConfirmBank:
+ break;
+ default:
+ return TransitionResult.stay();
+ }
+ if (r.wgInfo.withdrawalType !== WithdrawalRecordType.BankIntegrated) {
+ throw Error("invariant failed");
+ }
+ r.wgInfo.bankInfo.timestampReserveInfoPosted = timestampPreciseToDb(
+ AbsoluteTime.toPreciseTimestamp(AbsoluteTime.now()),
+ );
+ r.status = WithdrawalGroupStatus.PendingWaitConfirmBank;
+ r.wgInfo.bankInfo.confirmUrl = status.confirm_transfer_url;
+ return TransitionResult.transition(r);
+ });
+}
+
+async function transitionBankAborted(
+ ctx: WithdrawTransactionContext,
+): Promise<TaskRunResult> {
+ logger.info("bank aborted the withdrawal");
+ await ctx.transition({}, async (r) => {
+ if (!r) {
+ return TransitionResult.stay();
+ }
+ switch (r.status) {
+ case WithdrawalGroupStatus.PendingRegisteringBank:
+ case WithdrawalGroupStatus.PendingWaitConfirmBank:
+ break;
+ default:
+ return TransitionResult.stay();
+ }
+ if (r.wgInfo.withdrawalType !== WithdrawalRecordType.BankIntegrated) {
+ throw Error("invariant failed");
+ }
+ const now = AbsoluteTime.toPreciseTimestamp(AbsoluteTime.now());
+ r.wgInfo.bankInfo.timestampBankConfirmed = timestampPreciseToDb(now);
+ r.status = WithdrawalGroupStatus.FailedBankAborted;
+ return TransitionResult.transition(r);
+ });
+ return TaskRunResult.progress();
+}
+
+async function processBankRegisterReserve(
+ wex: WalletExecutionContext,
+ withdrawalGroupId: string,
+): Promise<TaskRunResult> {
+ const ctx = new WithdrawTransactionContext(wex, withdrawalGroupId);
+ const withdrawalGroup = await getWithdrawalGroupRecordTx(wex.db, {
+ withdrawalGroupId,
+ });
+ if (!withdrawalGroup) {
+ return TaskRunResult.finished();
+ }
+
+ if (
+ withdrawalGroup.wgInfo.withdrawalType != WithdrawalRecordType.BankIntegrated
+ ) {
+ throw Error("wrong withdrawal record type");
+ }
+ const bankInfo = withdrawalGroup.wgInfo.bankInfo;
+ if (!bankInfo) {
+ throw Error("no bank info in bank-integrated withdrawal");
+ }
+
+ const uriResult = parseWithdrawUri(bankInfo.talerWithdrawUri);
+ if (!uriResult) {
+ throw Error(`can't parse withdrawal URL ${bankInfo.talerWithdrawUri}`);
+ }
+ const url = new URL(
+ `withdrawal-operation/${uriResult.withdrawalOperationId}`,
+ uriResult.bankIntegrationApiBaseUrl,
+ );
+
+ const statusResp = await wex.http.fetch(url.href, {
+ timeout: getReserveRequestTimeout(withdrawalGroup),
+ cancellationToken: wex.cancellationToken,
+ });
+
+ const status = await readSuccessResponseJsonOrThrow(
+ statusResp,
+ codecForWithdrawOperationStatusResponse(),
+ );
+
+ if (status.aborted) {
+ return transitionBankAborted(ctx);
+ }
+
+ // FIXME: Put confirm transfer URL in the DB!
+
+ await registerReserveWithBank(wex, withdrawalGroupId);
+ return TaskRunResult.progress();
+}
+
+async function processReserveBankStatus(
+ wex: WalletExecutionContext,
+ withdrawalGroupId: string,
+): Promise<TaskRunResult> {
+ const withdrawalGroup = await getWithdrawalGroupRecordTx(wex.db, {
+ withdrawalGroupId,
+ });
+
+ if (!withdrawalGroup) {
+ return TaskRunResult.finished();
+ }
+
+ const ctx = new WithdrawTransactionContext(wex, withdrawalGroupId);
+
+ if (
+ withdrawalGroup.wgInfo.withdrawalType != WithdrawalRecordType.BankIntegrated
+ ) {
+ throw Error("wrong withdrawal record type");
+ }
+ const bankInfo = withdrawalGroup.wgInfo.bankInfo;
+ if (!bankInfo) {
+ throw Error("no bank info in bank-integrated withdrawal");
+ }
+
+ const uriResult = parseWithdrawUri(bankInfo.talerWithdrawUri);
+ if (!uriResult) {
+ throw Error(`can't parse withdrawal URL ${bankInfo.talerWithdrawUri}`);
+ }
+ const bankStatusUrl = new URL(
+ `withdrawal-operation/${uriResult.withdrawalOperationId}`,
+ uriResult.bankIntegrationApiBaseUrl,
+ );
+ bankStatusUrl.searchParams.set("long_poll_ms", "30000");
+
+ logger.info(`long-polling for withdrawal operation at ${bankStatusUrl.href}`);
+ const statusResp = await wex.http.fetch(bankStatusUrl.href, {
+ timeout: getReserveRequestTimeout(withdrawalGroup),
+ cancellationToken: wex.cancellationToken,
+ });
+ logger.info(
+ `long-polling for withdrawal operation returned status ${statusResp.status}`,
+ );
+
+ const status = await readSuccessResponseJsonOrThrow(
+ statusResp,
+ codecForWithdrawOperationStatusResponse(),
+ );
+
+ if (logger.shouldLogTrace()) {
+ logger.trace(`response body: ${j2s(status)}`);
+ }
+
+ if (status.aborted) {
+ return transitionBankAborted(ctx);
+ }
+
+ if (!status.transfer_done) {
+ return TaskRunResult.longpollReturnedPending();
+ }
+
+ const transitionInfo = await ctx.transition({}, async (r) => {
+ if (!r) {
+ return TransitionResult.stay();
+ }
+ // Re-check reserve status within transaction
+ switch (r.status) {
+ case WithdrawalGroupStatus.PendingWaitConfirmBank:
+ break;
+ default:
+ return TransitionResult.stay();
+ }
+ if (r.wgInfo.withdrawalType !== WithdrawalRecordType.BankIntegrated) {
+ throw Error("invariant failed");
+ }
+ if (status.transfer_done) {
+ logger.info("withdrawal: transfer confirmed by bank.");
+ const now = AbsoluteTime.toPreciseTimestamp(AbsoluteTime.now());
+ r.wgInfo.bankInfo.timestampBankConfirmed = timestampPreciseToDb(now);
+ r.status = WithdrawalGroupStatus.PendingQueryingStatus;
+ return TransitionResult.transition(r);
+ } else {
+ return TransitionResult.stay();
+ }
+ });
+
+ if (transitionInfo) {
+ return TaskRunResult.progress();
+ } else {
+ return TaskRunResult.backoff();
+ }
+}
+
+export interface PrepareCreateWithdrawalGroupResult {
+ withdrawalGroup: WithdrawalGroupRecord;
+ transactionId: string;
+ creationInfo?: {
+ amount: AmountJson;
+ canonExchange: string;
+ };
+}
+
+export async function internalPrepareCreateWithdrawalGroup(
+ wex: WalletExecutionContext,
+ args: {
+ reserveStatus: WithdrawalGroupStatus;
+ amount: AmountJson;
+ exchangeBaseUrl: string;
+ forcedWithdrawalGroupId?: string;
+ forcedDenomSel?: ForcedDenomSel;
+ reserveKeyPair?: EddsaKeypair;
+ restrictAge?: number;
+ wgInfo: WgInfo;
+ },
+): Promise<PrepareCreateWithdrawalGroupResult> {
+ const reserveKeyPair =
+ args.reserveKeyPair ?? (await wex.cryptoApi.createEddsaKeypair({}));
+ const now = AbsoluteTime.toPreciseTimestamp(AbsoluteTime.now());
+ const secretSeed = encodeCrock(getRandomBytes(32));
+ const canonExchange = canonicalizeBaseUrl(args.exchangeBaseUrl);
+ const amount = args.amount;
+ const currency = Amounts.currencyOf(amount);
+
+ let withdrawalGroupId;
+
+ if (args.forcedWithdrawalGroupId) {
+ withdrawalGroupId = args.forcedWithdrawalGroupId;
+ const wgId = withdrawalGroupId;
+ const existingWg = await wex.db.runReadOnlyTx(
+ { storeNames: ["withdrawalGroups"] },
+ async (tx) => {
+ return tx.withdrawalGroups.get(wgId);
+ },
+ );
+
+ if (existingWg) {
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Withdrawal,
+ withdrawalGroupId: existingWg.withdrawalGroupId,
+ });
+ return { withdrawalGroup: existingWg, transactionId };
+ }
+ } else {
+ withdrawalGroupId = encodeCrock(getRandomBytes(32));
+ }
+
+ await updateWithdrawalDenoms(wex, canonExchange);
+ const denoms = await getCandidateWithdrawalDenoms(
+ wex,
+ canonExchange,
+ currency,
+ );
+
+ let initialDenomSel: DenomSelectionState;
+ const denomSelUid = encodeCrock(getRandomBytes(16));
+ if (args.forcedDenomSel) {
+ logger.warn("using forced denom selection");
+ initialDenomSel = selectForcedWithdrawalDenominations(
+ amount,
+ denoms,
+ args.forcedDenomSel,
+ wex.ws.config.testing.denomselAllowLate,
+ );
+ } else {
+ initialDenomSel = selectWithdrawalDenominations(
+ amount,
+ denoms,
+ wex.ws.config.testing.denomselAllowLate,
+ );
+ }
+
+ const withdrawalGroup: WithdrawalGroupRecord = {
+ denomSelUid,
+ denomsSel: initialDenomSel,
+ exchangeBaseUrl: canonExchange,
+ instructedAmount: Amounts.stringify(amount),
+ timestampStart: timestampPreciseToDb(now),
+ rawWithdrawalAmount: initialDenomSel.totalWithdrawCost,
+ effectiveWithdrawalAmount: initialDenomSel.totalCoinValue,
+ secretSeed,
+ reservePriv: reserveKeyPair.priv,
+ reservePub: reserveKeyPair.pub,
+ status: args.reserveStatus,
+ withdrawalGroupId,
+ restrictAge: args.restrictAge,
+ senderWire: undefined,
+ timestampFinish: undefined,
+ wgInfo: args.wgInfo,
+ };
+
+ await fetchFreshExchange(wex, canonExchange);
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Withdrawal,
+ withdrawalGroupId: withdrawalGroup.withdrawalGroupId,
+ });
+
+ return {
+ withdrawalGroup,
+ transactionId,
+ creationInfo: {
+ canonExchange,
+ amount,
+ },
+ };
+}
+
+export interface PerformCreateWithdrawalGroupResult {
+ withdrawalGroup: WithdrawalGroupRecord;
+ transitionInfo: TransitionInfo | undefined;
+
+ /**
+ * Notification for the exchange state transition.
+ *
+ * Should be emitted after the transaction has succeeded.
+ */
+ exchangeNotif: WalletNotification | undefined;
+}
+
+export async function internalPerformCreateWithdrawalGroup(
+ wex: WalletExecutionContext,
+ tx: WalletDbReadWriteTransaction<
+ ["withdrawalGroups", "reserves", "exchanges"]
+ >,
+ prep: PrepareCreateWithdrawalGroupResult,
+): Promise<PerformCreateWithdrawalGroupResult> {
+ const { withdrawalGroup } = prep;
+ if (!prep.creationInfo) {
+ return {
+ withdrawalGroup,
+ transitionInfo: undefined,
+ exchangeNotif: undefined,
+ };
+ }
+ const existingWg = await tx.withdrawalGroups.get(
+ withdrawalGroup.withdrawalGroupId,
+ );
+ if (existingWg) {
+ return {
+ withdrawalGroup: existingWg,
+ exchangeNotif: undefined,
+ transitionInfo: undefined,
+ };
+ }
+ await tx.withdrawalGroups.add(withdrawalGroup);
+ await tx.reserves.put({
+ reservePub: withdrawalGroup.reservePub,
+ reservePriv: withdrawalGroup.reservePriv,
+ });
+
+ const exchange = await tx.exchanges.get(withdrawalGroup.exchangeBaseUrl);
+ if (exchange) {
+ exchange.lastWithdrawal = timestampPreciseToDb(TalerPreciseTimestamp.now());
+ await tx.exchanges.put(exchange);
+ }
+
+ const oldTxState = {
+ major: TransactionMajorState.None,
+ minor: undefined,
+ };
+ const newTxState = computeWithdrawalTransactionStatus(withdrawalGroup);
+ const transitionInfo = {
+ oldTxState,
+ newTxState,
+ };
+
+ const exchangeUsedRes = await markExchangeUsed(
+ wex,
+ tx,
+ prep.withdrawalGroup.exchangeBaseUrl,
+ );
+
+ const ctx = new WithdrawTransactionContext(
+ wex,
+ withdrawalGroup.withdrawalGroupId,
+ );
+
+ wex.taskScheduler.startShepherdTask(ctx.taskId);
+
+ return {
+ withdrawalGroup,
+ transitionInfo,
+ exchangeNotif: exchangeUsedRes.notif,
+ };
+}
+
+/**
+ * Create a withdrawal group.
+ *
+ * If a forcedWithdrawalGroupId is given and a
+ * withdrawal group with this ID already exists,
+ * the existing one is returned. No conflict checking
+ * of the other arguments is done in that case.
+ */
+export async function internalCreateWithdrawalGroup(
+ wex: WalletExecutionContext,
+ args: {
+ reserveStatus: WithdrawalGroupStatus;
+ amount: AmountJson;
+ exchangeBaseUrl: string;
+ forcedWithdrawalGroupId?: string;
+ forcedDenomSel?: ForcedDenomSel;
+ reserveKeyPair?: EddsaKeypair;
+ restrictAge?: number;
+ wgInfo: WgInfo;
+ },
+): Promise<WithdrawalGroupRecord> {
+ const prep = await internalPrepareCreateWithdrawalGroup(wex, args);
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Withdrawal,
+ withdrawalGroupId: prep.withdrawalGroup.withdrawalGroupId,
+ });
+ const ctx = new WithdrawTransactionContext(
+ wex,
+ prep.withdrawalGroup.withdrawalGroupId,
+ );
+ const res = await wex.db.runReadWriteTx(
+ {
+ storeNames: [
+ "withdrawalGroups",
+ "reserves",
+ "exchanges",
+ "exchangeDetails",
+ "transactions",
+ "operationRetries",
+ ],
+ },
+ async (tx) => {
+ const res = await internalPerformCreateWithdrawalGroup(wex, tx, prep);
+ await updateWithdrawalTransaction(ctx, tx);
+ return res;
+ },
+ );
+ if (res.exchangeNotif) {
+ wex.ws.notify(res.exchangeNotif);
+ }
+ notifyTransition(wex, transactionId, res.transitionInfo);
+ return res.withdrawalGroup;
+}
+
+/**
+ * Accept a bank-integrated withdrawal.
+ *
+ * Before returning, the wallet tries to register the reserve with the bank.
+ *
+ * Thus after this call returns, the withdrawal operation can be confirmed
+ * with the bank.
+ */
+export async function acceptWithdrawalFromUri(
+ wex: WalletExecutionContext,
+ req: {
+ talerWithdrawUri: string;
+ selectedExchange: string;
+ forcedDenomSel?: ForcedDenomSel;
+ restrictAge?: number;
+ },
+): Promise<AcceptWithdrawalResponse> {
+ const selectedExchange = canonicalizeBaseUrl(req.selectedExchange);
+ logger.info(
+ `accepting withdrawal via ${req.talerWithdrawUri}, canonicalized selected exchange ${selectedExchange}`,
+ );
+ const existingWithdrawalGroup = await wex.db.runReadOnlyTx(
+ { storeNames: ["withdrawalGroups"] },
+ async (tx) => {
+ return await tx.withdrawalGroups.indexes.byTalerWithdrawUri.get(
+ req.talerWithdrawUri,
+ );
+ },
+ );
+
+ if (existingWithdrawalGroup) {
+ let url: string | undefined;
+ if (
+ existingWithdrawalGroup.wgInfo.withdrawalType ===
+ WithdrawalRecordType.BankIntegrated
+ ) {
+ url = existingWithdrawalGroup.wgInfo.bankInfo.confirmUrl;
+ }
+ return {
+ reservePub: existingWithdrawalGroup.reservePub,
+ confirmTransferUrl: url,
+ transactionId: constructTransactionIdentifier({
+ tag: TransactionType.Withdrawal,
+ withdrawalGroupId: existingWithdrawalGroup.withdrawalGroupId,
+ }),
+ };
+ }
+
+ await fetchFreshExchange(wex, selectedExchange);
+ const withdrawInfo = await getBankWithdrawalInfo(
+ wex.http,
+ req.talerWithdrawUri,
+ );
+ const exchangePaytoUri = await getExchangePaytoUri(
+ wex,
+ selectedExchange,
+ withdrawInfo.wireTypes,
+ );
+
+ const exchange = await fetchFreshExchange(wex, selectedExchange);
+
+ const withdrawalAccountList = await fetchWithdrawalAccountInfo(
+ wex,
+ {
+ exchange,
+ instructedAmount: withdrawInfo.amount,
+ },
+ CancellationToken.CONTINUE,
+ );
+
+ const withdrawalGroup = await internalCreateWithdrawalGroup(wex, {
+ amount: withdrawInfo.amount,
+ exchangeBaseUrl: req.selectedExchange,
+ wgInfo: {
+ withdrawalType: WithdrawalRecordType.BankIntegrated,
+ exchangeCreditAccounts: withdrawalAccountList,
+ bankInfo: {
+ exchangePaytoUri,
+ talerWithdrawUri: req.talerWithdrawUri,
+ confirmUrl: withdrawInfo.confirmTransferUrl,
+ timestampBankConfirmed: undefined,
+ timestampReserveInfoPosted: undefined,
+ },
+ },
+ restrictAge: req.restrictAge,
+ forcedDenomSel: req.forcedDenomSel,
+ reserveStatus: WithdrawalGroupStatus.PendingRegisteringBank,
+ });
+
+ const withdrawalGroupId = withdrawalGroup.withdrawalGroupId;
+
+ const ctx = new WithdrawTransactionContext(wex, withdrawalGroupId);
+
+ wex.ws.notify({
+ type: NotificationType.BalanceChange,
+ hintTransactionId: ctx.transactionId,
+ });
+
+ await waitWithdrawalRegistered(wex, ctx);
+
+ wex.taskScheduler.startShepherdTask(ctx.taskId);
+
+ return {
+ reservePub: withdrawalGroup.reservePub,
+ confirmTransferUrl: withdrawInfo.confirmTransferUrl,
+ transactionId: ctx.transactionId,
+ };
+}
+
+async function internalWaitWithdrawalRegistered(
+ wex: WalletExecutionContext,
+ ctx: WithdrawTransactionContext,
+ withdrawalNotifFlag: AsyncFlag,
+): Promise<void> {
+ while (true) {
+ const { withdrawalRec, retryRec } = await wex.db.runReadOnlyTx(
+ { storeNames: ["withdrawalGroups", "operationRetries"] },
+ async (tx) => {
+ return {
+ withdrawalRec: await tx.withdrawalGroups.get(ctx.withdrawalGroupId),
+ retryRec: await tx.operationRetries.get(ctx.taskId),
+ };
+ },
+ );
+
+ if (!withdrawalRec) {
+ throw Error("withdrawal not found anymore");
+ }
+
+ switch (withdrawalRec.status) {
+ case WithdrawalGroupStatus.FailedBankAborted:
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK,
+ {},
+ );
+ case WithdrawalGroupStatus.PendingKyc:
+ case WithdrawalGroupStatus.PendingAml:
+ case WithdrawalGroupStatus.PendingQueryingStatus:
+ case WithdrawalGroupStatus.PendingReady:
+ case WithdrawalGroupStatus.Done:
+ case WithdrawalGroupStatus.PendingWaitConfirmBank:
+ return;
+ case WithdrawalGroupStatus.PendingRegisteringBank:
+ break;
+ default: {
+ if (retryRec) {
+ if (retryRec.lastError) {
+ throw TalerError.fromUncheckedDetail(retryRec.lastError);
+ } else {
+ throw Error("withdrawal unexpectedly pending");
+ }
+ }
+ }
+ }
+
+ await withdrawalNotifFlag.wait();
+ withdrawalNotifFlag.reset();
+ }
+}
+
+async function waitWithdrawalRegistered(
+ wex: WalletExecutionContext,
+ ctx: WithdrawTransactionContext,
+): Promise<void> {
+ // FIXME: Doesn't support cancellation yet
+ // FIXME: We should use Symbol.dispose magic here for cleanup!
+
+ const withdrawalNotifFlag = new AsyncFlag();
+ // Raise exchangeNotifFlag whenever we get a notification
+ // about our exchange.
+ const cancelNotif = wex.ws.addNotificationListener((notif) => {
+ if (
+ notif.type === NotificationType.TransactionStateTransition &&
+ notif.transactionId === ctx.transactionId
+ ) {
+ logger.info(`raising update notification: ${j2s(notif)}`);
+ withdrawalNotifFlag.raise();
+ }
+ });
+
+ try {
+ const res = await internalWaitWithdrawalRegistered(
+ wex,
+ ctx,
+ withdrawalNotifFlag,
+ );
+ logger.info("done waiting for ready exchange");
+ return res;
+ } finally {
+ cancelNotif();
+ }
+}
+
+async function fetchAccount(
+ wex: WalletExecutionContext,
+ instructedAmount: AmountJson,
+ acct: ExchangeWireAccount,
+ reservePub: string | undefined,
+ cancellationToken: CancellationToken,
+): Promise<WithdrawalExchangeAccountDetails> {
+ let paytoUri: string;
+ let transferAmount: AmountString | undefined = undefined;
+ let currencySpecification: CurrencySpecification | undefined = undefined;
+ if (acct.conversion_url != null) {
+ const reqUrl = new URL("cashin-rate", acct.conversion_url);
+ reqUrl.searchParams.set(
+ "amount_credit",
+ Amounts.stringify(instructedAmount),
+ );
+ const httpResp = await wex.http.fetch(reqUrl.href, {
+ cancellationToken,
+ });
+ const respOrErr = await readSuccessResponseJsonOrErrorCode(
+ httpResp,
+ codecForCashinConversionResponse(),
+ );
+ if (respOrErr.isError) {
+ return {
+ status: "error",
+ paytoUri: acct.payto_uri,
+ conversionError: respOrErr.talerErrorResponse,
+ };
+ }
+ const resp = respOrErr.response;
+ paytoUri = acct.payto_uri;
+ transferAmount = resp.amount_debit;
+ const configUrl = new URL("config", acct.conversion_url);
+ const configResp = await wex.http.fetch(configUrl.href, {
+ cancellationToken,
+ });
+ const configRespOrError = await readSuccessResponseJsonOrErrorCode(
+ configResp,
+ codecForConversionBankConfig(),
+ );
+ if (configRespOrError.isError) {
+ return {
+ status: "error",
+ paytoUri: acct.payto_uri,
+ conversionError: configRespOrError.talerErrorResponse,
+ };
+ }
+ const configParsed = configRespOrError.response;
+ currencySpecification = configParsed.fiat_currency_specification;
+ } else {
+ paytoUri = acct.payto_uri;
+ transferAmount = Amounts.stringify(instructedAmount);
+ }
+ paytoUri = addPaytoQueryParams(paytoUri, {
+ amount: Amounts.stringify(transferAmount),
+ });
+ if (reservePub != null) {
+ paytoUri = addPaytoQueryParams(paytoUri, {
+ message: `Taler Withdrawal ${reservePub}`,
+ });
+ }
+ const acctInfo: WithdrawalExchangeAccountDetails = {
+ status: "ok",
+ paytoUri,
+ transferAmount,
+ bankLabel: acct.bank_label,
+ priority: acct.priority,
+ currencySpecification,
+ creditRestrictions: acct.credit_restrictions,
+ };
+ if (transferAmount != null) {
+ acctInfo.transferAmount = transferAmount;
+ }
+ return acctInfo;
+}
+
+/**
+ * Gather information about bank accounts that can be used for
+ * withdrawals. This includes accounts that are in a different
+ * currency and require conversion.
+ */
+async function fetchWithdrawalAccountInfo(
+ wex: WalletExecutionContext,
+ req: {
+ exchange: ReadyExchangeSummary;
+ instructedAmount: AmountJson;
+ reservePub?: string;
+ },
+ cancellationToken: CancellationToken,
+): Promise<WithdrawalExchangeAccountDetails[]> {
+ const { exchange } = req;
+ const withdrawalAccounts: WithdrawalExchangeAccountDetails[] = [];
+ for (let acct of exchange.wireInfo.accounts) {
+ const acctInfo = await fetchAccount(
+ wex,
+ req.instructedAmount,
+ acct,
+ req.reservePub,
+ cancellationToken,
+ );
+ withdrawalAccounts.push(acctInfo);
+ }
+ withdrawalAccounts.sort((x1, x2) => {
+ // Accounts without explicit priority have prio 0.
+ const n1 = x1.priority ?? 0;
+ const n2 = x2.priority ?? 0;
+ return Math.sign(n2 - n1);
+ });
+ return withdrawalAccounts;
+}
+
+/**
+ * Create a manual withdrawal operation.
+ *
+ * Adds the corresponding exchange as a trusted exchange if it is neither
+ * audited nor trusted already.
+ *
+ * Asynchronously starts the withdrawal.
+ */
+export async function createManualWithdrawal(
+ wex: WalletExecutionContext,
+ req: {
+ exchangeBaseUrl: string;
+ amount: AmountLike;
+ restrictAge?: number;
+ forcedDenomSel?: ForcedDenomSel;
+ },
+): Promise<AcceptManualWithdrawalResult> {
+ const { exchangeBaseUrl } = req;
+ const amount = Amounts.parseOrThrow(req.amount);
+ const exchange = await fetchFreshExchange(wex, exchangeBaseUrl);
+
+ if (exchange.currency != amount.currency) {
+ throw Error(
+ "manual withdrawal with conversion from foreign currency is not yet supported",
+ );
+ }
+ const reserveKeyPair: EddsaKeypair = await wex.cryptoApi.createEddsaKeypair(
+ {},
+ );
+
+ const withdrawalAccountsList = await fetchWithdrawalAccountInfo(
+ wex,
+ {
+ exchange,
+ instructedAmount: amount,
+ reservePub: reserveKeyPair.pub,
+ },
+ CancellationToken.CONTINUE,
+ );
+
+ const withdrawalGroup = await internalCreateWithdrawalGroup(wex, {
+ amount: Amounts.jsonifyAmount(req.amount),
+ wgInfo: {
+ withdrawalType: WithdrawalRecordType.BankManual,
+ exchangeCreditAccounts: withdrawalAccountsList,
+ },
+ exchangeBaseUrl: req.exchangeBaseUrl,
+ forcedDenomSel: req.forcedDenomSel,
+ restrictAge: req.restrictAge,
+ reserveStatus: WithdrawalGroupStatus.PendingQueryingStatus,
+ reserveKeyPair,
+ });
+
+ const ctx = new WithdrawTransactionContext(
+ wex,
+ withdrawalGroup.withdrawalGroupId,
+ );
+
+ const exchangePaytoUris = await wex.db.runReadOnlyTx(
+ { storeNames: ["withdrawalGroups", "exchanges", "exchangeDetails"] },
+ async (tx) => {
+ return await getFundingPaytoUris(tx, withdrawalGroup.withdrawalGroupId);
+ },
+ );
+
+ wex.ws.notify({
+ type: NotificationType.BalanceChange,
+ hintTransactionId: ctx.transactionId,
+ });
+
+ wex.taskScheduler.startShepherdTask(ctx.taskId);
+
+ return {
+ reservePub: withdrawalGroup.reservePub,
+ exchangePaytoUris: exchangePaytoUris,
+ withdrawalAccountsList: withdrawalAccountsList,
+ transactionId: ctx.transactionId,
+ };
+}
+
+/**
+ * Wait until a refresh operation is final.
+ */
+export async function waitWithdrawalFinal(
+ wex: WalletExecutionContext,
+ withdrawalGroupId: string,
+): Promise<void> {
+ const ctx = new WithdrawTransactionContext(wex, withdrawalGroupId);
+ wex.taskScheduler.startShepherdTask(ctx.taskId);
+
+ // FIXME: Clean up using the new JS "using" / Symbol.dispose syntax.
+ const withdrawalNotifFlag = new AsyncFlag();
+ // Raise purchaseNotifFlag whenever we get a notification
+ // about our refresh.
+ const cancelNotif = wex.ws.addNotificationListener((notif) => {
+ if (
+ notif.type === NotificationType.TransactionStateTransition &&
+ notif.transactionId === ctx.transactionId
+ ) {
+ withdrawalNotifFlag.raise();
+ }
+ });
+ const unregisterOnCancelled = wex.cancellationToken.onCancelled(() => {
+ cancelNotif();
+ withdrawalNotifFlag.raise();
+ });
+
+ try {
+ await internalWaitWithdrawalFinal(ctx, withdrawalNotifFlag);
+ } catch (e) {
+ unregisterOnCancelled();
+ cancelNotif();
+ }
+}
+
+async function internalWaitWithdrawalFinal(
+ ctx: WithdrawTransactionContext,
+ flag: AsyncFlag,
+): Promise<void> {
+ while (true) {
+ if (ctx.wex.cancellationToken.isCancelled) {
+ throw Error("cancelled");
+ }
+
+ // Check if refresh is final
+ const res = await ctx.wex.db.runReadOnlyTx(
+ { storeNames: ["withdrawalGroups", "operationRetries"] },
+ async (tx) => {
+ return {
+ wg: await tx.withdrawalGroups.get(ctx.withdrawalGroupId),
+ };
+ },
+ );
+ const { wg } = res;
+ if (!wg) {
+ // Must've been deleted, we consider that final.
+ return;
+ }
+ switch (wg.status) {
+ case WithdrawalGroupStatus.AbortedBank:
+ case WithdrawalGroupStatus.AbortedExchange:
+ case WithdrawalGroupStatus.Done:
+ case WithdrawalGroupStatus.FailedAbortingBank:
+ case WithdrawalGroupStatus.FailedBankAborted:
+ // Transaction is final
+ return;
+ }
+
+ // Wait for the next transition
+ await flag.wait();
+ flag.reset();
+ }
+}
+
+export async function getWithdrawalDetailsForAmount(
+ wex: WalletExecutionContext,
+ cts: CancellationToken.Source,
+ req: GetWithdrawalDetailsForAmountRequest,
+): Promise<WithdrawalDetailsForAmount> {
+ const clientCancelKey = req.clientCancellationId
+ ? `ccid:getWithdrawalDetailsForAmount:${req.clientCancellationId}`
+ : undefined;
+ if (clientCancelKey) {
+ const prevCts = wex.ws.clientCancellationMap.get(clientCancelKey);
+ if (prevCts) {
+ wex.oc.observe({
+ type: ObservabilityEventType.Message,
+ contents: `Cancelling previous key ${clientCancelKey}`,
+ });
+ prevCts.cancel();
+ } else {
+ wex.oc.observe({
+ type: ObservabilityEventType.Message,
+ contents: `No previous key ${clientCancelKey}`,
+ });
+ }
+ wex.oc.observe({
+ type: ObservabilityEventType.Message,
+ contents: `Setting clientCancelKey ${clientCancelKey} to ${cts}`,
+ });
+ wex.ws.clientCancellationMap.set(clientCancelKey, cts);
+ }
+ try {
+ return await internalGetWithdrawalDetailsForAmount(wex, req);
+ } finally {
+ wex.oc.observe({
+ type: ObservabilityEventType.Message,
+ contents: `Deleting clientCancelKey ${clientCancelKey} to ${cts}`,
+ });
+ if (clientCancelKey && !cts.token.isCancelled) {
+ wex.ws.clientCancellationMap.delete(clientCancelKey);
+ }
+ }
+}
+
+async function internalGetWithdrawalDetailsForAmount(
+ wex: WalletExecutionContext,
+ req: GetWithdrawalDetailsForAmountRequest,
+): Promise<WithdrawalDetailsForAmount> {
+ const wi = await getExchangeWithdrawalInfo(
+ wex,
+ req.exchangeBaseUrl,
+ Amounts.parseOrThrow(req.amount),
+ req.restrictAge,
+ );
+ let numCoins = 0;
+ for (const x of wi.selectedDenoms.selectedDenoms) {
+ numCoins += x.count;
+ }
+ const resp: WithdrawalDetailsForAmount = {
+ amountRaw: req.amount,
+ amountEffective: Amounts.stringify(wi.selectedDenoms.totalCoinValue),
+ paytoUris: wi.exchangePaytoUris,
+ tosAccepted: wi.termsOfServiceAccepted,
+ ageRestrictionOptions: wi.ageRestrictionOptions,
+ withdrawalAccountsList: wi.exchangeCreditAccountDetails,
+ numCoins,
+ scopeInfo: wi.scopeInfo,
+ };
+ return resp;
+}
diff --git a/packages/taler-wallet-core/tsconfig.json b/packages/taler-wallet-core/tsconfig.json
index 5b74121a2..7a1a0fcce 100644
--- a/packages/taler-wallet-core/tsconfig.json
+++ b/packages/taler-wallet-core/tsconfig.json
@@ -4,16 +4,18 @@
"composite": true,
"declaration": true,
"declarationMap": false,
- "target": "ES2017",
- "module": "ESNext",
+ "target": "ES2020",
+ "module": "Node16",
"moduleResolution": "Node16",
+ "resolveJsonModule": true,
"sourceMap": true,
- "lib": ["es6"],
+ "lib": ["ES2020"],
+ "resolvePackageJsonImports": true,
"types": ["node"],
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"strict": true,
- "strictPropertyInitialization": false,
+ "strictPropertyInitialization": true,
"outDir": "lib",
"noImplicitAny": true,
"noImplicitThis": true,
@@ -31,5 +33,5 @@
"path": "../taler-util/"
}
],
- "include": ["src/**/*"]
+ "include": ["src/**/*", "src/*.json"]
}
diff --git a/packages/taler-wallet-core/watch_test.sh b/packages/taler-wallet-core/watch_test.sh
new file mode 100755
index 000000000..124e18e21
--- /dev/null
+++ b/packages/taler-wallet-core/watch_test.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+./node_modules/typescript/bin/tsc --watch &
+./node_modules/ava/entrypoints/cli.mjs -w "$@"
diff --git a/packages/taler-wallet-embedded/README.md b/packages/taler-wallet-embedded/README.md
new file mode 100644
index 000000000..b59e79e9a
--- /dev/null
+++ b/packages/taler-wallet-embedded/README.md
@@ -0,0 +1,4 @@
+# taler-wallet-embedded
+
+Minimal wrapper and build system code to make `taler-wallet-core` run
+on embedded interpreters (currently `quickjs-tart`).
diff --git a/packages/taler-wallet-embedded/build.mjs b/packages/taler-wallet-embedded/build.mjs
index 0f12ef2c6..f2bf7b986 100755
--- a/packages/taler-wallet-embedded/build.mjs
+++ b/packages/taler-wallet-embedded/build.mjs
@@ -15,31 +15,38 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import esbuild from 'esbuild'
-import path from "path"
-import fs from "fs"
+import esbuild from "esbuild";
+import path from "path";
+import fs from "fs";
-const BASE = process.cwd()
+const BASE = process.cwd();
-let GIT_ROOT = BASE
-while (!fs.existsSync(path.join(GIT_ROOT, '.git')) && GIT_ROOT !== '/') {
- GIT_ROOT = path.join(GIT_ROOT, '../')
+let GIT_ROOT = BASE;
+while (!fs.existsSync(path.join(GIT_ROOT, ".git")) && GIT_ROOT !== "/") {
+ GIT_ROOT = path.join(GIT_ROOT, "../");
}
-if (GIT_ROOT === '/') {
- console.log("not found")
+if (GIT_ROOT === "/") {
+ console.log("not found");
process.exit(1);
}
-const GIT_HASH = GIT_ROOT === '/' ? undefined : git_hash()
+const GIT_HASH = GIT_ROOT === "/" ? undefined : git_hash();
-
-let _package = JSON.parse(fs.readFileSync(path.join(BASE, 'package.json')));
+let _package = JSON.parse(fs.readFileSync(path.join(BASE, "package.json")));
function git_hash() {
- const rev = fs.readFileSync(path.join(GIT_ROOT, '.git', 'HEAD')).toString().trim().split(/.*[: ]/).slice(-1)[0];
- if (rev.indexOf('/') === -1) {
+ const rev = fs
+ .readFileSync(path.join(GIT_ROOT, ".git", "HEAD"))
+ .toString()
+ .trim()
+ .split(/.*[: ]/)
+ .slice(-1)[0];
+ if (rev.indexOf("/") === -1) {
return rev;
} else {
- return fs.readFileSync(path.join(GIT_ROOT, '.git', rev)).toString().trim();
+ return fs
+ .readFileSync(path.join(GIT_ROOT, ".git", rev))
+ .toString()
+ .trim();
}
}
@@ -48,25 +55,22 @@ export const buildConfig = {
outfile: "dist/taler-wallet-core-qjs.mjs",
bundle: true,
minify: false,
- target: [
- 'es2020'
- ],
- external: ["os"],
- format: 'esm',
- platform: 'neutral',
+ target: ["es2020"],
+ external: ["os", "std", "better-sqlite3"],
+ format: "esm",
+ platform: "neutral",
mainFields: ["module", "main"],
+ conditions: ["qtart"],
sourcemap: true,
define: {
- '__VERSION__': `"${_package.version}"`,
- '__GIT_HASH__': `"${GIT_HASH}"`,
+ __VERSION__: `"${_package.version}"`,
+ __GIT_HASH__: `"${GIT_HASH}"`,
+ "walletCoreBuildInfo.implementationSemver": `"${_package.version}"`,
+ "walletCoreBuildInfo.implementationGitHash": `"${GIT_HASH}"`,
},
-}
-
-esbuild
- .build(buildConfig)
- .catch((e) => {
- console.log(e)
- process.exit(1)
- });
-
+};
+esbuild.build(buildConfig).catch((e) => {
+ console.log(e);
+ process.exit(1);
+});
diff --git a/packages/taler-wallet-embedded/package.json b/packages/taler-wallet-embedded/package.json
index 5d1c501a6..ee9efafdd 100644
--- a/packages/taler-wallet-embedded/package.json
+++ b/packages/taler-wallet-embedded/package.json
@@ -1,23 +1,23 @@
{
"name": "@gnu-taler/taler-wallet-embedded",
- "version": "0.9.0",
+ "version": "0.10.7",
"description": "",
"engines": {
- "node": ">=0.12.0"
+ "node": ">=0.18.0"
},
"repository": {
"type": "git",
"url": "git://git.taler.net/wallet-core.git"
},
- "main": "dist/taler-wallet-embedded.cjs",
"author": "Florian Dold",
"license": "GPL-3.0",
"type": "module",
"scripts": {
- "compile": "tsc && rollup -c",
+ "compile": "./build.mjs",
"pretty": "prettier --write src",
- "coverage": "tsc && nyc ava",
- "clean": "rimraf lib dist tsconfig.tsbuildinfo"
+ "typedoc": "typedoc --out dist/typedoc ./src/wallet-qjs.ts",
+ "clean": "rm -rf lib dist tsconfig.tsbuildinfo",
+ "deps": "pnpm install --frozen-lockfile --filter @gnu-taler/taler-wallet-embedded..."
},
"files": [
"AUTHORS",
@@ -28,22 +28,15 @@
"src/"
],
"devDependencies": {
- "@rollup/plugin-commonjs": "^22.0.2",
- "@rollup/plugin-json": "^4.1.0",
- "@rollup/plugin-node-resolve": "^13.3.0",
- "@rollup/plugin-replace": "^4.0.0",
- "@types/node": "^18.8.5",
- "prettier": "^2.5.1",
- "rimraf": "^3.0.2",
- "rollup": "^2.79.0",
- "rollup-plugin-sourcemaps": "^0.6.3",
- "rollup-plugin-terser": "^7.0.2",
- "typescript": "^4.8.4"
+ "@types/node": "^18.11.17",
+ "esbuild": "^0.19.9",
+ "prettier": "^3.1.1"
},
"dependencies": {
- "@gnu-taler/taler-util": "workspace:*",
+ "@gnu-taler/anastasis-core": "workspace:*",
"@gnu-taler/idb-bridge": "workspace:*",
+ "@gnu-taler/taler-util": "workspace:*",
"@gnu-taler/taler-wallet-core": "workspace:*",
- "tslib": "^2.4.0"
+ "tslib": "^2.6.2"
}
}
diff --git a/packages/taler-wallet-embedded/rollup.config.js b/packages/taler-wallet-embedded/rollup.config.js
deleted file mode 100644
index 1ebefae9a..000000000
--- a/packages/taler-wallet-embedded/rollup.config.js
+++ /dev/null
@@ -1,63 +0,0 @@
-// rollup.config.js
-import commonjs from "@rollup/plugin-commonjs";
-import nodeResolve from "@rollup/plugin-node-resolve";
-import json from "@rollup/plugin-json";
-import builtins from "builtin-modules";
-import pkg from "./package.json";
-import walletCorePkg from "../taler-wallet-core/package.json";
-import replace from "@rollup/plugin-replace";
-import fs from "fs";
-import path from "path";
-
-function git_hash() {
- const rev = fs
- .readFileSync(path.join(GIT_ROOT, ".git", "HEAD"))
- .toString()
- .trim()
- .split(/.*[: ]/)
- .slice(-1)[0];
- if (rev.indexOf("/") === -1) {
- return rev;
- } else {
- return fs.readFileSync(path.join(GIT_ROOT, ".git", rev)).toString().trim();
- }
-}
-
-const BASE = process.cwd();
-let GIT_ROOT = BASE;
-while (!fs.existsSync(path.join(GIT_ROOT, ".git")) && GIT_ROOT !== "/") {
- GIT_ROOT = path.join(GIT_ROOT, "../");
-}
-const GIT_HASH = GIT_ROOT === "/" ? undefined : git_hash();
-
-export default {
- input: "lib/index.js",
- output: {
- file: pkg.main,
- format: "cjs",
- },
- external: builtins,
- plugins: [
- nodeResolve({
- preferBuiltins: true,
- exportConditions: ["node"],
- }),
-
- replace({
- values: {
- __VERSION__: `"${walletCorePkg.version}"`,
- __GIT_HASH__: `"${GIT_HASH}"`,
- },
- preventAssignment: false,
- }),
-
- commonjs({
- include: [/node_modules/, /dist/],
- extensions: [".js", ".ts"],
- ignoreGlobal: false,
- sourceMap: false,
- }),
-
- json(),
- ],
-};
diff --git a/packages/taler-wallet-embedded/src/index.ts b/packages/taler-wallet-embedded/src/index.ts
deleted file mode 100644
index ab8fdd32b..000000000
--- a/packages/taler-wallet-embedded/src/index.ts
+++ /dev/null
@@ -1,294 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2019 GNUnet e.V.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- * Imports.
- */
-import {
- DefaultNodeWalletArgs,
- getDefaultNodeWallet,
- getErrorDetailFromException,
- handleWorkerError,
- handleWorkerMessage,
- Headers,
- HttpRequestLibrary,
- HttpRequestOptions,
- HttpResponse,
- NodeHttpLib,
- OpenedPromise,
- openPromise,
- Wallet,
- WALLET_EXCHANGE_PROTOCOL_VERSION,
- WALLET_MERCHANT_PROTOCOL_VERSION,
-} from "@gnu-taler/taler-wallet-core";
-
-import {
- CoreApiEnvelope,
- CoreApiResponse,
- CoreApiResponseSuccess,
- Logger,
- WalletNotification,
-} from "@gnu-taler/taler-util";
-import fs from "fs";
-
-export { handleWorkerError, handleWorkerMessage };
-
-const logger = new Logger("taler-wallet-embedded/index.ts");
-
-export class NativeHttpLib implements HttpRequestLibrary {
- useNfcTunnel = false;
-
- private nodeHttpLib: HttpRequestLibrary = new NodeHttpLib();
-
- private requestId = 1;
-
- private requestMap: {
- [id: number]: OpenedPromise<HttpResponse>;
- } = {};
-
- constructor(private sendMessage: (m: string) => void) {}
-
- fetch(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> {
- return this.nodeHttpLib.fetch(url, opt);
- }
-
- get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> {
- if (this.useNfcTunnel) {
- const myId = this.requestId++;
- const p = openPromise<HttpResponse>();
- this.requestMap[myId] = p;
- const request = {
- method: "get",
- url,
- };
- this.sendMessage(
- JSON.stringify({
- type: "tunnelHttp",
- request,
- id: myId,
- }),
- );
- return p.promise;
- } else {
- return this.nodeHttpLib.get(url, opt);
- }
- }
-
- postJson(
- url: string,
- body: any,
- opt?: HttpRequestOptions,
- ): Promise<HttpResponse> {
- if (this.useNfcTunnel) {
- const myId = this.requestId++;
- const p = openPromise<HttpResponse>();
- this.requestMap[myId] = p;
- const request = {
- method: "postJson",
- url,
- body,
- };
- this.sendMessage(
- JSON.stringify({ type: "tunnelHttp", request, id: myId }),
- );
- return p.promise;
- } else {
- return this.nodeHttpLib.postJson(url, body, opt);
- }
- }
-
- handleTunnelResponse(msg: any): void {
- const myId = msg.id;
- const p = this.requestMap[myId];
- if (!p) {
- logger.error(
- `no matching request for tunneled HTTP response, id=${myId}`,
- );
- }
- const headers = new Headers();
- if (msg.status != 0) {
- const resp: HttpResponse = {
- // FIXME: pass through this URL
- requestUrl: "",
- headers,
- status: msg.status,
- requestMethod: "FIXME",
- json: async () => JSON.parse(msg.responseText),
- text: async () => msg.responseText,
- bytes: async () => {
- throw Error("bytes() not supported for tunnel response");
- },
- };
- p.resolve(resp);
- } else {
- p.reject(new Error(`unexpected HTTP status code ${msg.status}`));
- }
- delete this.requestMap[myId];
- }
-}
-
-function sendNativeMessage(ev: CoreApiEnvelope): void {
- // @ts-ignore
- const sendMessage = globalThis.__native_sendMessage;
- if (typeof sendMessage !== "function") {
- const errMsg =
- "FATAL: cannot install native wallet listener: native functions missing";
- logger.error(errMsg);
- throw new Error(errMsg);
- }
- const m = JSON.stringify(ev);
- // @ts-ignore
- sendMessage(m);
-}
-
-class NativeWalletMessageHandler {
- walletArgs: DefaultNodeWalletArgs | undefined;
- maybeWallet: Wallet | undefined;
- wp = openPromise<Wallet>();
- httpLib = new NodeHttpLib();
-
- /**
- * Handle a request from the native wallet.
- */
- async handleMessage(
- operation: string,
- id: string,
- args: any,
- ): Promise<CoreApiResponse> {
- const wrapResponse = (result: unknown): CoreApiResponseSuccess => {
- return {
- type: "response",
- id,
- operation,
- result,
- };
- };
-
- let initResponse: any = {};
-
- const reinit = async () => {
- logger.info("in reinit");
- const w = await getDefaultNodeWallet(this.walletArgs);
- this.maybeWallet = w;
- const resp = await w.handleCoreApiRequest(
- "initWallet",
- "native-init",
- {},
- );
- initResponse = resp.type == "response" ? resp.result : resp.error;
- w.runTaskLoop().catch((e) => {
- logger.error(
- `Error during wallet retry loop: ${e.stack ?? e.toString()}`,
- );
- });
- this.wp.resolve(w);
- };
-
- switch (operation) {
- case "init": {
- this.walletArgs = {
- notifyHandler: async (notification: WalletNotification) => {
- sendNativeMessage({ type: "notification", payload: notification });
- },
- persistentStoragePath: args.persistentStoragePath,
- httpLib: this.httpLib,
- cryptoWorkerType: args.cryptoWorkerType,
- };
- await reinit();
- return wrapResponse({
- ...initResponse,
- });
- }
- case "startTunnel": {
- // this.httpLib.useNfcTunnel = true;
- throw Error("not implemented");
- }
- case "stopTunnel": {
- // this.httpLib.useNfcTunnel = false;
- throw Error("not implemented");
- }
- case "tunnelResponse": {
- // httpLib.handleTunnelResponse(msg.args);
- throw Error("not implemented");
- }
- case "reset": {
- const oldArgs = this.walletArgs;
- this.walletArgs = { ...oldArgs };
- if (oldArgs && oldArgs.persistentStoragePath) {
- try {
- fs.unlinkSync(oldArgs.persistentStoragePath);
- } catch (e) {
- logger.error("Error while deleting the wallet db:", e);
- }
- // Prevent further storage!
- this.walletArgs.persistentStoragePath = undefined;
- }
- const wallet = await this.wp.promise;
- wallet.stop();
- this.wp = openPromise<Wallet>();
- this.maybeWallet = undefined;
- await reinit();
- return wrapResponse({});
- }
- default: {
- const wallet = await this.wp.promise;
- return await wallet.handleCoreApiRequest(operation, id, args);
- }
- }
- }
-}
-
-export function installNativeWalletListener(): void {
- const handler = new NativeWalletMessageHandler();
- const onMessage = async (msgStr: any): Promise<void> => {
- if (typeof msgStr !== "string") {
- logger.error("expected string as message");
- return;
- }
- const msg = JSON.parse(msgStr);
- const operation = msg.operation;
- if (typeof operation !== "string") {
- logger.error(
- "message to native wallet helper must contain operation of type string",
- );
- return;
- }
- const id = msg.id;
- logger.info(`native listener: got request for ${operation} (${id})`);
-
- try {
- const respMsg = await handler.handleMessage(operation, id, msg.args);
- logger.info(
- `native listener: sending success response for ${operation} (${id})`,
- );
- sendNativeMessage(respMsg);
- } catch (e) {
- const respMsg: CoreApiResponse = {
- type: "error",
- id,
- operation,
- error: getErrorDetailFromException(e),
- };
- sendNativeMessage(respMsg);
- return;
- }
- };
-
- // @ts-ignore
- globalThis.__native_onMessage = onMessage;
-
- logger.info("native wallet listener installed");
-}
diff --git a/packages/taler-wallet-embedded/src/wallet-qjs.ts b/packages/taler-wallet-embedded/src/wallet-qjs.ts
index 8cad653a8..4441ac8c1 100644
--- a/packages/taler-wallet-embedded/src/wallet-qjs.ts
+++ b/packages/taler-wallet-embedded/src/wallet-qjs.ts
@@ -15,255 +15,64 @@
*/
/**
+ * Entry-point for the wallet under qtart, the QuickJS-based GNU Taler runtime.
+ */
+
+/**
* Imports.
*/
import {
- AccessStats,
- DefaultNodeWalletArgs,
- getErrorDetailFromException,
- handleWorkerError,
- handleWorkerMessage,
- Headers,
- HttpRequestLibrary,
- HttpRequestOptions,
- HttpResponse,
- openPromise,
- openTalerDatabase,
- SetTimeoutTimerAPI,
- SynchronousCryptoWorkerFactoryPlain,
- Wallet,
- WalletApiOperation,
-} from "@gnu-taler/taler-wallet-core";
-
+ discoverPolicies,
+ getBackupStartState,
+ getRecoveryStartState,
+ mergeDiscoveryAggregate,
+ reduceAction,
+} from "@gnu-taler/anastasis-core";
+import { userIdentifierDerive } from "@gnu-taler/anastasis-core/lib/crypto.js";
import {
- CoreApiEnvelope,
+ AmountString,
+ CoreApiMessageEnvelope,
CoreApiResponse,
CoreApiResponseSuccess,
- j2s,
Logger,
- setGlobalLogLevelFromString,
- setPRNG,
+ PartialWalletRunConfig,
WalletNotification,
+ enableNativeLogging,
+ getErrorDetailFromException,
+ j2s,
+ openPromise,
+ performanceNow,
+ setGlobalLogLevelFromString,
} from "@gnu-taler/taler-util";
-import { BridgeIDBFactory } from "@gnu-taler/idb-bridge";
-import { MemoryBackend } from "@gnu-taler/idb-bridge";
-import { shimIndexedDB } from "@gnu-taler/idb-bridge";
-import { IDBFactory } from "@gnu-taler/idb-bridge";
-
-import * as _qjsOsImp from "os";
-
-const textDecoder = new TextDecoder();
-const textEncoder = new TextEncoder();
+import { createPlatformHttpLib } from "@gnu-taler/taler-util/http";
+import { qjsOs } from "@gnu-taler/taler-util/qtart";
+import {
+ DefaultNodeWalletArgs,
+ Wallet,
+ WalletApiOperation,
+ createNativeWalletHost2,
+} from "@gnu-taler/taler-wallet-core";
setGlobalLogLevelFromString("trace");
-setPRNG(function (x: Uint8Array, n: number) {
- // @ts-ignore
- const va = globalThis._randomBytes(n);
- const v = new Uint8Array(va);
- for (let i = 0; i < n; i++) x[i] = v[i];
- for (let i = 0; i < v.length; i++) v[i] = 0;
-});
-
-export interface QjsHttpResp {
- status: number;
- data: ArrayBuffer;
-}
-
-export interface QjsHttpOptions {
- method: string;
- debug?: boolean;
- data?: ArrayBuffer;
- headers?: string[];
-}
-
-export interface QjsOsLib {
- // Not async!
- fetchHttp(url: string, options?: QjsHttpOptions): QjsHttpResp;
-}
-
-// This is not the nodejs "os" module, but the qjs "os" module.
-const qjsOs: QjsOsLib = _qjsOsImp as any;
-
-export { handleWorkerError, handleWorkerMessage };
-
const logger = new Logger("taler-wallet-embedded/index.ts");
-export class NativeHttpLib implements HttpRequestLibrary {
- get(
- url: string,
- opt?: HttpRequestOptions | undefined,
- ): Promise<HttpResponse> {
- return this.fetch(url, {
- method: "GET",
- ...opt,
- });
- }
- postJson(
- url: string,
- body: any,
- opt?: HttpRequestOptions | undefined,
- ): Promise<HttpResponse> {
- return this.fetch(url, {
- method: "POST",
- body,
- ...opt,
- });
- }
- async fetch(
- url: string,
- opt?: HttpRequestOptions | undefined,
- ): Promise<HttpResponse> {
- const method = opt?.method ?? "GET";
- let data: ArrayBuffer | undefined = undefined;
- let headers: string[] = [];
- if (opt?.headers) {
- for (let headerName of Object.keys(opt.headers)) {
- headers.push(`${headerName}: ${opt.headers[headerName]}`);
- }
- }
- if (method.toUpperCase() === "POST") {
- if (opt?.body) {
- if (typeof opt.body === "string") {
- data = textEncoder.encode(opt.body).buffer;
- } else if (ArrayBuffer.isView(opt.body)) {
- data = opt.body.buffer;
- } else if (opt.body instanceof ArrayBuffer) {
- data = opt.body;
- } else if (typeof opt.body === "object") {
- data = textEncoder.encode(JSON.stringify(opt.body)).buffer;
- }
- } else {
- data = new ArrayBuffer(0);
- }
- }
- const res = qjsOs.fetchHttp(url, {
- method,
- data,
- headers,
- });
- return {
- requestMethod: method,
- headers: new Headers(),
- async bytes() {
- return res.data;
- },
- json() {
- const text = textDecoder.decode(res.data);
- return JSON.parse(text);
- },
- async text() {
- const text = textDecoder.decode(res.data);
- return text;
- },
- requestUrl: url,
- status: res.status,
- };
- }
-}
-
-function sendNativeMessage(ev: CoreApiEnvelope): void {
- // @ts-ignore
- const sendMessage = globalThis.__native_sendMessage;
- if (typeof sendMessage !== "function") {
- const errMsg =
- "FATAL: cannot install native wallet listener: native functions missing";
- logger.error(errMsg);
- throw new Error(errMsg);
- }
+/**
+ * Sends JSON to the host application, i.e. the process that
+ * runs the JavaScript interpreter (quickjs / qtart) to run
+ * the embedded wallet.
+ */
+function sendNativeMessage(ev: CoreApiMessageEnvelope): void {
const m = JSON.stringify(ev);
- // @ts-ignore
- sendMessage(m);
-}
-
-export async function getWallet(args: DefaultNodeWalletArgs = {}): Promise<{
- wallet: Wallet;
- getDbStats: () => AccessStats;
-}> {
- BridgeIDBFactory.enableTracing = false;
- const myBackend = new MemoryBackend();
- myBackend.enableTracing = false;
-
- const storagePath = args.persistentStoragePath;
- if (storagePath) {
- // try {
- // const dbContentStr: string = fs.readFileSync(storagePath, {
- // encoding: "utf-8",
- // });
- // const dbContent = JSON.parse(dbContentStr);
- // myBackend.importDump(dbContent);
- // } catch (e: any) {
- // const code: string = e.code;
- // if (code === "ENOENT") {
- // logger.trace("wallet file doesn't exist yet");
- // } else {
- // logger.error("could not open wallet database file");
- // throw e;
- // }
- // }
-
- myBackend.afterCommitCallback = async () => {
- logger.error("DB commit not implemented");
- // logger.trace("committing database");
- // // Allow caller to stop persisting the wallet.
- // if (args.persistentStoragePath === undefined) {
- // return;
- // }
- // const tmpPath = `${args.persistentStoragePath}-${makeId(5)}.tmp`;
- // const dbContent = myBackend.exportDump();
- // fs.writeFileSync(tmpPath, JSON.stringify(dbContent, undefined, 2), {
- // encoding: "utf-8",
- // });
- // // Atomically move the temporary file onto the DB path.
- // fs.renameSync(tmpPath, args.persistentStoragePath);
- // logger.trace("committing database done");
- };
- }
-
- BridgeIDBFactory.enableTracing = false;
-
- const myBridgeIdbFactory = new BridgeIDBFactory(myBackend);
- const myIdbFactory: IDBFactory = myBridgeIdbFactory as any as IDBFactory;
-
- let myHttpLib;
- if (args.httpLib) {
- myHttpLib = args.httpLib;
- } else {
- myHttpLib = new NativeHttpLib();
- }
-
- const myVersionChange = (): Promise<void> => {
- logger.error("version change requested, should not happen");
- throw Error(
- "BUG: wallet DB version change event can't happen with memory IDB",
- );
- };
-
- shimIndexedDB(myBridgeIdbFactory);
-
- const myDb = await openTalerDatabase(myIdbFactory, myVersionChange);
-
- let workerFactory;
- workerFactory = new SynchronousCryptoWorkerFactoryPlain();
-
- const timer = new SetTimeoutTimerAPI();
-
- const w = await Wallet.create(myDb, myHttpLib, timer, workerFactory);
-
- if (args.notifyHandler) {
- w.addNotificationListener(args.notifyHandler);
- }
- return {
- wallet: w,
- getDbStats: () => myBackend.accessStats,
- };
+ qjsOs.postMessageToHost(m);
}
class NativeWalletMessageHandler {
walletArgs: DefaultNodeWalletArgs | undefined;
+ walletConfig: PartialWalletRunConfig | undefined;
maybeWallet: Wallet | undefined;
wp = openPromise<Wallet>();
- httpLib = new NativeHttpLib();
+ httpLib = createPlatformHttpLib();
/**
* Handle a request from the native wallet.
@@ -273,7 +82,7 @@ class NativeWalletMessageHandler {
id: string,
args: any,
): Promise<CoreApiResponse> {
- const wrapResponse = (result: unknown): CoreApiResponseSuccess => {
+ const wrapSuccessResponse = (result: unknown): CoreApiResponseSuccess => {
return {
type: "response",
id,
@@ -286,20 +95,17 @@ class NativeWalletMessageHandler {
const reinit = async () => {
logger.info("in reinit");
- const wR = await getWallet(this.walletArgs);
+ const wR = await createNativeWalletHost2(this.walletArgs);
const w = wR.wallet;
this.maybeWallet = w;
const resp = await w.handleCoreApiRequest(
"initWallet",
"native-init",
- {},
+ {
+ config: this.walletConfig
+ },
);
initResponse = resp.type == "response" ? resp.result : resp.error;
- w.runTaskLoop().catch((e) => {
- logger.error(
- `Error during wallet retry loop: ${e.stack ?? e.toString()}`,
- );
- });
this.wp.resolve(w);
};
@@ -312,9 +118,19 @@ class NativeWalletMessageHandler {
persistentStoragePath: args.persistentStoragePath,
httpLib: this.httpLib,
cryptoWorkerType: args.cryptoWorkerType,
+ ...args,
};
+ this.walletConfig = args.config ?? {};
+ const logLevel = args.logLevel;
+ if (logLevel) {
+ setGlobalLogLevelFromString(logLevel);
+ }
+ const nativeLogging = args.useNativeLogging ?? false;
+ if (nativeLogging) {
+ enableNativeLogging();
+ }
await reinit();
- return wrapResponse({
+ return wrapSuccessResponse({
...initResponse,
});
}
@@ -331,24 +147,9 @@ class NativeWalletMessageHandler {
throw Error("not implemented");
}
case "reset": {
- const oldArgs = this.walletArgs;
- this.walletArgs = { ...oldArgs };
- if (oldArgs && oldArgs.persistentStoragePath) {
- try {
- logger.error("FIXME: reset not implemented");
- // fs.unlinkSync(oldArgs.persistentStoragePath);
- } catch (e) {
- logger.error("Error while deleting the wallet db:", e);
- }
- // Prevent further storage!
- this.walletArgs.persistentStoragePath = undefined;
- }
- const wallet = await this.wp.promise;
- wallet.stop();
- this.wp = openPromise<Wallet>();
- this.maybeWallet = undefined;
- await reinit();
- return wrapResponse({});
+ throw Error(
+ "reset not supported anymore, please use the clearDb wallet-core request",
+ );
}
default: {
const wallet = await this.wp.promise;
@@ -358,6 +159,55 @@ class NativeWalletMessageHandler {
}
}
+/**
+ * Handle an Anastasis request from the native app.
+ */
+async function handleAnastasisRequest(
+ operation: string,
+ id: string,
+ args: any,
+): Promise<CoreApiResponse> {
+ const wrapSuccessResponse = (result: unknown): CoreApiResponseSuccess => {
+ return {
+ type: "response",
+ id,
+ operation,
+ result,
+ };
+ };
+
+ let req = args ?? {};
+
+ switch (operation) {
+ case "anastasisReduce":
+ // TODO: do some input validation here
+ let reduceRes = await reduceAction(req.state, req.action, req.args ?? {});
+ // For now, this will return "success" even if the wrapped Anastasis
+ // response is a ReducerStateError.
+ return wrapSuccessResponse(reduceRes);
+ case "anastasisStartBackup":
+ return wrapSuccessResponse(await getBackupStartState());
+ case "anastasisStartRecovery":
+ return wrapSuccessResponse(await getRecoveryStartState());
+ case "anastasisDiscoverPolicies":
+ let discoverRes = await discoverPolicies(req.state, req.cursor);
+ let aggregatedPolicies = mergeDiscoveryAggregate(
+ discoverRes.policies ?? [],
+ req.state.discoveryState?.aggregatedPolicies ?? [],
+ );
+ return wrapSuccessResponse({
+ ...req.state,
+ discoveryState: {
+ state: "finished",
+ aggregatedPolicies,
+ cursor: discoverRes.cursor,
+ },
+ });
+ default:
+ throw Error("unsupported anastasis operation");
+ }
+}
+
export function installNativeWalletListener(): void {
setGlobalLogLevelFromString("trace");
const handler = new NativeWalletMessageHandler();
@@ -377,26 +227,46 @@ export function installNativeWalletListener(): void {
const id = msg.id;
logger.info(`native listener: got request for ${operation} (${id})`);
+ const startTimeNs = performanceNow();
+
+ let respMsg: CoreApiResponse;
try {
- const respMsg = await handler.handleMessage(operation, id, msg.args);
- logger.info(
- `native listener: sending success response for ${operation} (${id})`,
- );
- sendNativeMessage(respMsg);
+ if (msg.operation.startsWith("anastasis")) {
+ respMsg = await handleAnastasisRequest(operation, id, msg.args ?? {});
+ } else if (msg.operation === "testing-dangerously-eval") {
+ // Eval code, used only for testing. No client may rely on this.
+ logger.info(`evaluating ${msg.args.jscode}`);
+ const f = new Function(msg.args.jscode);
+ f();
+ respMsg = {
+ type: "response",
+ result: {},
+ operation: "testing-dangerously-eval",
+ id: msg.id,
+ };
+ }
+ {
+ respMsg = await handler.handleMessage(operation, id, msg.args ?? {});
+ }
} catch (e) {
- const respMsg: CoreApiResponse = {
+ respMsg = {
type: "error",
id,
operation,
error: getErrorDetailFromException(e),
};
- sendNativeMessage(respMsg);
- return;
}
+ const endTimeNs = performanceNow();
+ const requestDurationMs = Math.round(
+ Number((endTimeNs - startTimeNs) / 1000n / 1000n),
+ );
+ logger.info(
+ `native listener: sending back ${respMsg.type} message for operation ${operation} (${id}) after ${requestDurationMs} ms`,
+ );
+ sendNativeMessage(respMsg);
};
- // @ts-ignore
- globalThis.__native_onMessage = onMessage;
+ qjsOs.setMessageFromHostHandler((m) => onMessage(m));
logger.info("native wallet listener installed");
}
@@ -404,41 +274,110 @@ export function installNativeWalletListener(): void {
// @ts-ignore
globalThis.installNativeWalletListener = installNativeWalletListener;
-// @ts-ignore
-globalThis.makeWallet = getWallet;
-
export async function testWithGv() {
- const w = await getWallet();
- await w.wallet.client.call(WalletApiOperation.InitWallet, {});
+ const w = await createNativeWalletHost2({});
+ await w.wallet.client.call(WalletApiOperation.InitWallet, {
+ config: {
+ features: {
+ allowHttp: true,
+ },
+ },
+ });
await w.wallet.client.call(WalletApiOperation.RunIntegrationTest, {
- amountToSpend: "KUDOS:1",
- amountToWithdraw: "KUDOS:3",
- bankBaseUrl: "https://bank.demo.taler.net/demobanks/default/access-api/",
+ amountToSpend: "KUDOS:1" as AmountString,
+ amountToWithdraw: "KUDOS:3" as AmountString,
+ corebankApiBaseUrl: "https://bank.demo.taler.net/",
exchangeBaseUrl: "https://exchange.demo.taler.net/",
merchantBaseUrl: "https://backend.demo.taler.net/",
+ merchantAuthToken: "secret-token:sandbox",
});
- await w.wallet.runTaskLoop({
- stopWhenDone: true,
+ await w.wallet.client.call(WalletApiOperation.TestingWaitTasksDone, {});
+ w.wallet.stop();
+}
+
+export async function testWithFdold() {
+ const w = await createNativeWalletHost2({});
+ await w.wallet.client.call(WalletApiOperation.InitWallet, {
+ config: {
+ features: {
+ allowHttp: true,
+ },
+ },
+ });
+ await w.wallet.client.call(WalletApiOperation.RunIntegrationTest, {
+ amountToSpend: "TESTKUDOS:1" as AmountString,
+ amountToWithdraw: "TESTKUDOS:3" as AmountString,
+ corebankApiBaseUrl: "https://bank.taler.fdold.eu/",
+ exchangeBaseUrl: "https://exchange.taler.fdold.eu/",
+ merchantBaseUrl: "https://merchant.taler.fdold.eu/",
});
+ await w.wallet.client.call(WalletApiOperation.TestingWaitTasksDone, {});
+ w.wallet.stop();
}
-export async function testWithLocal() {
- const w = await getWallet();
- await w.wallet.client.call(WalletApiOperation.InitWallet, {});
+export async function testWithLocal(path: string) {
+ console.log("running local test");
+ const w = await createNativeWalletHost2({
+ persistentStoragePath: path ?? "walletdb.json",
+ });
+ console.log("created wallet");
+ await w.wallet.client.call(WalletApiOperation.InitWallet, {
+ config: {
+ features: {
+ allowHttp: true,
+ },
+ testing: {
+ skipDefaults: true,
+ },
+ },
+ });
+ console.log("initialized wallet");
await w.wallet.client.call(WalletApiOperation.RunIntegrationTest, {
- amountToSpend: "TESTKUDOS:1",
- amountToWithdraw: "TESTKUDOS:3",
- bankBaseUrl: "http://localhost:8082/",
- bankAccessApiBaseUrl: "http://localhost:8082/taler-bank-access/",
+ amountToSpend: "TESTKUDOS:1" as AmountString,
+ amountToWithdraw: "TESTKUDOS:3" as AmountString,
+ corebankApiBaseUrl: "http://localhost:8082/taler-bank-access/",
exchangeBaseUrl: "http://localhost:8081/",
- merchantBaseUrl: "http://backend.demo.taler.net:8083/",
- });
- await w.wallet.runTaskLoop({
- stopWhenDone: true,
+ merchantBaseUrl: "http://localhost:8083/",
});
+ console.log("started integration test");
+ await w.wallet.client.call(WalletApiOperation.TestingWaitTasksDone, {});
+ console.log("done with task loop");
+ w.wallet.stop();
+ console.log("DB stats:", j2s(w.getDbStats()));
+}
+
+export async function testArgon2id() {
+ const userIdVector = {
+ input_id_data: {
+ name: "Fleabag",
+ ssn: "AB123",
+ },
+ input_server_salt: "FZ48EFS7WS3R2ZR4V53A3GFFY4",
+ output_id:
+ "YS45R6CGJV84K1NN7T14ZBCPVTZ6H15XJSM1FV0R748MHPV82SM0126EBZKBAAGCR34Q9AFKPEW1HRT2Q9GQ5JRA3642AB571DKZS18",
+ };
+
+ if (
+ (await userIdentifierDerive(
+ userIdVector.input_id_data,
+ userIdVector.input_server_salt,
+ )) != userIdVector.output_id
+ ) {
+ throw Error("argon2id is not working!");
+ }
+
+ console.log("argon2id is working!");
}
// @ts-ignore
globalThis.testWithGv = testWithGv;
// @ts-ignore
globalThis.testWithLocal = testWithLocal;
+// @ts-ignore
+globalThis.testArgon2id = testArgon2id;
+// @ts-ignore
+globalThis.testReduceAction = reduceAction;
+// @ts-ignore
+globalThis.testDiscoverPolicies = discoverPolicies;
+// @ts-ignore
+globalThis.testWithFdold = testWithFdold;
diff --git a/packages/taler-wallet-embedded/test-embedded.cjs b/packages/taler-wallet-embedded/test-embedded.cjs
deleted file mode 100644
index bc5bf9086..000000000
--- a/packages/taler-wallet-embedded/test-embedded.cjs
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-// This file demonstrates how to use the single-file embedded wallet.
-
-// Load the embedded wallet
-const embedded = require("./dist/taler-wallet-embedded.cjs");
-
-// Some bookkeeping to correlate requests to responses.
-const requestMap = {};
-let requestCounter = 1;
-
-// Install __native_onMessage in the global namespace.
-// The __native_onMessage handles messages from the host,
-// i.e. it handles wallet-core requests from the host application (UI etc.).
-embedded.installNativeWalletListener();
-
-// The host application must the __native_sendMessage callback
-// to allow wallet-core to respond.
-globalThis.__native_sendMessage = (msgStr) => {
- const message = JSON.parse(msgStr);
- if (message.type === "notification") {
- console.log("got notification:", JSON.stringify(message.payload));
- return;
- }
- if (message.type === "response") {
- console.log("got response", JSON.parse(msgStr));
- const msgId = message.id;
- requestMap[msgId](message);
- delete requestMap[msgId];
- return;
- }
- throw Error("not reached");
-};
-
-async function makeRequest(operation, payload = {}) {
- return new Promise((resolve, reject) => {
- const reqId = `req-${requestCounter++}`;
- requestMap[reqId] = (x) => resolve(x);
- __native_onMessage(
- JSON.stringify({
- operation,
- args: payload,
- id: reqId,
- }),
- );
- });
-}
-
-async function testMain() {
- const resp = await makeRequest("init");
- console.log("response from init", JSON.stringify(resp));
-}
-
-testMain();
diff --git a/packages/taler-wallet-embedded/tsconfig.json b/packages/taler-wallet-embedded/tsconfig.json
index 7b27ca6b7..3dd8cdcb2 100644
--- a/packages/taler-wallet-embedded/tsconfig.json
+++ b/packages/taler-wallet-embedded/tsconfig.json
@@ -4,11 +4,11 @@
"composite": true,
"declaration": true,
"declarationMap": true,
- "target": "ES6",
- "module": "ESNext",
+ "target": "ES2020",
+ "module": "Node16",
"moduleResolution": "Node16",
"sourceMap": true,
- "lib": ["es6"],
+ "lib": ["ES2020"],
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"strict": true,
diff --git a/packages/taler-wallet-webextension/.eslintrc.cjs b/packages/taler-wallet-webextension/.eslintrc.cjs
new file mode 100644
index 000000000..05618b499
--- /dev/null
+++ b/packages/taler-wallet-webextension/.eslintrc.cjs
@@ -0,0 +1,28 @@
+module.exports = {
+ extends: [
+ 'eslint:recommended',
+ 'plugin:@typescript-eslint/recommended',
+ 'plugin:react/recommended',
+ ],
+ parser: '@typescript-eslint/parser',
+ plugins: ['@typescript-eslint', 'header'],
+ root: true,
+ rules: {
+ "react/no-unknown-property": 0,
+ "react/no-unescaped-entities": 0,
+ "@typescript-eslint/no-namespace": 0,
+ "@typescript-eslint/no-unused-vars": [2,{argsIgnorePattern:"^_"}],
+ "header/header": [2,"copyleft-header.js"]
+ },
+ parserOptions: {
+ ecmaVersion: 6,
+ sourceType: 'module',
+ jsx: true,
+ },
+ settings: {
+ react: {
+ version: "18",
+ pragma: "h",
+ }
+ },
+};
diff --git a/packages/taler-wallet-webextension/README.md b/packages/taler-wallet-webextension/README.md
new file mode 100644
index 000000000..a29147c0d
--- /dev/null
+++ b/packages/taler-wallet-webextension/README.md
@@ -0,0 +1,4 @@
+# taler-wallet-webextension
+
+This package contains the implementation of the GNU Taler wallet browser extension,
+using the cross-browser WebExtension APIs.
diff --git a/packages/taler-wallet-webextension/build-fast-with-linaria.mjs b/packages/taler-wallet-webextension/build-fast-with-linaria.mjs
deleted file mode 100755
index 1232eac98..000000000
--- a/packages/taler-wallet-webextension/build-fast-with-linaria.mjs
+++ /dev/null
@@ -1,132 +0,0 @@
-#!/usr/bin/env node
-/*
- This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-import linaria from '@linaria/esbuild'
-import esbuild from 'esbuild'
-import path from "path"
-import fs from "fs"
-
-function getFilesInDirectory(startPath, regex) {
- if (!fs.existsSync(startPath)) {
- return;
- }
- const files = fs.readdirSync(startPath);
- const result = files.flatMap(file => {
- const filename = path.join(startPath, file);
-
- const stat = fs.lstatSync(filename);
- if (stat.isDirectory()) {
- return getFilesInDirectory(filename, regex);
- }
- else if (regex.test(filename)) {
- return filename
- }
- }).filter(x => !!x)
-
- return result
-}
-
-// eslint-disable-next-line no-undef
-const BASE = process.cwd()
-const allTestFiles = getFilesInDirectory(path.join(BASE, 'src'), /.test.ts$/)
-
-const preact = path.join(BASE, "node_modules", "preact", "compat", "dist", "compat.module.js");
-const preactCompatPlugin = {
- name: "preact-compat",
- setup(build) {
- build.onResolve({ filter: /^(react-dom|react)$/ }, args => ({ path: preact }));
- }
-}
-
-const entryPoints = [
- 'src/popupEntryPoint.tsx',
- 'src/popupEntryPoint.dev.tsx',
- 'src/walletEntryPoint.tsx',
- 'src/walletEntryPoint.dev.tsx',
- 'src/background.ts',
- 'src/stories.tsx',
- 'src/background.dev.ts',
- 'src/browserWorkerEntry.ts'
-]
-
-let GIT_ROOT = BASE
-while (!fs.existsSync(path.join(GIT_ROOT, '.git')) && GIT_ROOT !== '/') {
- GIT_ROOT = path.join(GIT_ROOT, '../')
-}
-if (GIT_ROOT === '/') {
- // eslint-disable-next-line no-undef
- console.log("not found")
- // eslint-disable-next-line no-undef
- process.exit(1);
-}
-const GIT_HASH = GIT_ROOT === '/' ? undefined : git_hash()
-
-
-let _package = JSON.parse(fs.readFileSync(path.join(BASE, 'package.json')));
-
-function git_hash() {
- const rev = fs.readFileSync(path.join(GIT_ROOT, '.git', 'HEAD')).toString().trim().split(/.*[: ]/).slice(-1)[0];
- if (rev.indexOf('/') === -1) {
- return rev;
- } else {
- return fs.readFileSync(path.join(GIT_ROOT, '.git', rev)).toString().trim();
- }
-}
-
-export const buildConfig = {
- entryPoints: [...entryPoints, ...allTestFiles],
- bundle: true,
- outdir: 'dist',
- minify: false,
- loader: {
- '.svg': 'text',
- '.png': 'dataurl',
- '.jpeg': 'dataurl',
- },
- target: [
- 'es6'
- ],
- format: 'iife',
- platform: 'browser',
- sourcemap: true,
- jsxFactory: 'h',
- jsxFragment: 'Fragment',
- define: {
- '__VERSION__': `"${_package.version}"`,
- '__GIT_HASH__': `"${GIT_HASH}"`,
- },
- plugins: [
- preactCompatPlugin,
- linaria.default({
- babelOptions: {
- babelrc: false,
- configFile: './babel.config-linaria.json',
- },
- sourceMap: true,
- }),
- ],
-}
-
-await esbuild
- .build(buildConfig)
- .catch((e) => {
- // eslint-disable-next-line no-undef
- console.log(e)
- // eslint-disable-next-line no-undef
- process.exit(1)
- });
-
diff --git a/packages/taler-wallet-webextension/build.mjs b/packages/taler-wallet-webextension/build.mjs
new file mode 100755
index 000000000..e1fd5c0e1
--- /dev/null
+++ b/packages/taler-wallet-webextension/build.mjs
@@ -0,0 +1,49 @@
+#!/usr/bin/env node
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { build } from "@gnu-taler/web-util/build";
+import linaria from "@linaria/esbuild";
+import { shakerPlugin } from "@linaria/shaker";
+
+await build({
+ type: "production",
+ source: {
+ js: [
+ "src/popupEntryPoint.tsx",
+ "src/walletEntryPoint.tsx",
+ "src/background.ts",
+ "src/taler-wallet-interaction-loader.ts",
+ "src/taler-wallet-interaction-support.ts",
+ "src/browserWorkerEntry.ts",
+ ],
+ },
+ destination: "./dist/prod",
+ css: "linaria",
+ linariaPlugin: () => {
+ // linaria has a bug if this run in web-util library
+ return linaria({
+ babelOptions: {
+ presets: [
+ "@babel/preset-typescript",
+ "@babel/preset-react",
+ "@linaria",
+ ],
+ },
+ // sourceMap: true,
+ });
+ },
+});
diff --git a/packages/taler-wallet-webextension/clean_and_build.sh b/packages/taler-wallet-webextension/clean_and_build.sh
index 1c01e3a4a..fa8d514f2 100755
--- a/packages/taler-wallet-webextension/clean_and_build.sh
+++ b/packages/taler-wallet-webextension/clean_and_build.sh
@@ -5,7 +5,7 @@ set -e
rm -rf dist lib tsconfig.tsbuildinfo .linaria-cache
echo typecheck and bundle...
-node build-fast-with-linaria.mjs &
+node build.mjs &
pnpm tsc --noEmit &
wait -n
wait -n
diff --git a/packages/taler-wallet-webextension/dev-html/.gitignore b/packages/taler-wallet-webextension/dev-html/.gitignore
deleted file mode 100644
index c4f051f4f..000000000
--- a/packages/taler-wallet-webextension/dev-html/.gitignore
+++ /dev/null
@@ -1,4 +0,0 @@
-/mocha.css
-/mocha.js
-/mocha.js.map
-/manifest.json
diff --git a/packages/taler-wallet-webextension/dev-html/index.html b/packages/taler-wallet-webextension/dev-html/index.html
deleted file mode 100644
index 4b7fe34e8..000000000
--- a/packages/taler-wallet-webextension/dev-html/index.html
+++ /dev/null
@@ -1,68 +0,0 @@
-<html>
- <head>
- <meta charset="utf-8" />
- <link rel="manifest" href="./manifest.json" />
- </head>
- <body>
- <script>
- function openPopup() {
- window.frames["popup"].location = "/popup.html";
- }
- function openWallet() {
- window.frames["wallet"].location = "/wallet.html";
- }
- function closeWallet() {
- window.frames["wallet"].location = "about:blank";
- }
- function openPage() {
- window.frames["other"].location =
- document.getElementById("page-url").value;
- }
- </script>
- <input id="page-url" type="text" />
- <button onclick="openPage()">open</button>
- <a
- href='javascript:void(window.frames["other"].location = "http://bank.taler:5882")'
- >open local bank</a
- >
- <hr />
- <iframe
- id="other-window"
- name="other"
- src="http://bank.taler:5882"
- width="100%"
- height="325"
- >
- </iframe>
- <hr />
- <button value="asd" onclick="openPopup()">open popup</button><br />
- <iframe
- id="popup-window"
- name="popup"
- src="about:blank"
- width="500"
- height="325"
- >
- </iframe>
- <hr />
- <button value="asd" onclick="closeWallet();openWallet()">
- reload wallet page
- </button>
- <br />
- <iframe
- id="wallet-window"
- name="wallet"
- src="/wallet.html"
- width="800"
- height="100%"
- >
- </iframe>
- <hr />
- <iframe src="/tests.html" name="wallet" width="800" height="100%"> </iframe>
- <hr />
- <iframe src="/stories.html" name="wallet" width="800" height="100%">
- </iframe>
- <hr />
- <script src="/dist/background.dev.js"></script>
- </body>
-</html>
diff --git a/packages/taler-wallet-webextension/dev-html/static/font/import.css b/packages/taler-wallet-webextension/dev-html/static/font/import.css
deleted file mode 100644
index 05edddb51..000000000
--- a/packages/taler-wallet-webextension/dev-html/static/font/import.css
+++ /dev/null
@@ -1,35 +0,0 @@
-@font-face {
- font-family: 'Roboto';
- font-style: italic;
- font-weight: 400;
- font-display: swap;
- src: url(/static/font/roboto-italic-400.ttf) format('truetype');
-}
-@font-face {
- font-family: 'Roboto';
- font-style: normal;
- font-weight: 300;
- font-display: swap;
- src: url(/static/font/roboto-normal-300.ttf) format('truetype');
-}
-@font-face {
- font-family: 'Roboto';
- font-style: normal;
- font-weight: 400;
- font-display: swap;
- src: url(/static/font/roboto-normal-400.ttf) format('truetype');
-}
-@font-face {
- font-family: 'Roboto';
- font-style: normal;
- font-weight: 500;
- font-display: swap;
- src: url(/static/font/roboto-normal-500.ttf) format('truetype');
-}
-@font-face {
- font-family: 'Roboto';
- font-style: normal;
- font-weight: 700;
- font-display: swap;
- src: url(/static/font/roboto-normal-700.ttf) format('truetype');
-}
diff --git a/packages/taler-wallet-webextension/dev.mjs b/packages/taler-wallet-webextension/dev.mjs
index fb5661aa5..dc597c248 100755
--- a/packages/taler-wallet-webextension/dev.mjs
+++ b/packages/taler-wallet-webextension/dev.mjs
@@ -15,19 +15,52 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { serve } from "@gnu-taler/web-util/lib/index.node";
-import esbuild from 'esbuild';
-import { buildConfig } from "./build-fast-with-linaria.mjs";
+import { getFilesInDirectory, initializeDev } from "@gnu-taler/web-util/build";
+import { serve } from "@gnu-taler/web-util/node";
+import linaria from "@linaria/esbuild";
-buildConfig.inject = ['./node_modules/@gnu-taler/web-util/lib/live-reload.mjs']
+const allStaticFiles = getFilesInDirectory("src/pwa");
+
+const devEntryPoints = [
+ "src/popupEntryPoint.dev.tsx",
+ "src/walletEntryPoint.dev.tsx",
+ "src/background.dev.ts",
+ "src/browserWorkerEntry.ts",
+ "src/stories.tsx",
+];
+
+const build = initializeDev({
+ type: "development",
+ source: {
+ js: devEntryPoints,
+ assets: allStaticFiles,
+ },
+ destination: "./dist/dev",
+ public: "/app",
+ css: "linaria",
+ linariaPlugin: () => {
+ // linaria has a bug if this run in web-util library
+ return linaria({
+ babelOptions: {
+ presets: [
+ "@babel/preset-typescript",
+ "@babel/preset-react",
+ "@linaria",
+ ],
+ },
+ sourceMap: true,
+ });
+ },
+});
+
+await build();
serve({
- folder: './dist',
+ folder: "./dist/dev",
port: 8080,
- source: './src',
- development: true,
- onUpdate: async () => esbuild.build(buildConfig)
-})
+ source: "./src",
+ onSourceUpdate: build,
+});
// FIXME: create a mocha test in the browser as it was before
@@ -35,4 +68,3 @@ serve({
// fs.writeFileSync("dev-html/mocha.css", fs.readFileSync("node_modules/mocha/mocha.css"))
// fs.writeFileSync("dev-html/mocha.js", fs.readFileSync("node_modules/mocha/mocha.js"))
// fs.writeFileSync("dev-html/mocha.js.map", fs.readFileSync("node_modules/mocha/mocha.js.map"))
-
diff --git a/packages/taler-wallet-webextension/manifest-common.json b/packages/taler-wallet-webextension/manifest-common.json
index 2dac9a015..32bd5267f 100644
--- a/packages/taler-wallet-webextension/manifest-common.json
+++ b/packages/taler-wallet-webextension/manifest-common.json
@@ -2,8 +2,7 @@
"name": "GNU Taler Wallet (git)",
"description": "Privacy preserving and transparent payments",
"author": "GNU Taler Developers",
- "version": "0.9.0.34",
- "version_name": "0.9.0",
+ "version": "0.10.7",
"icons": {
"16": "static/img/taler-logo-16.png",
"19": "static/img/taler-logo-19.png",
@@ -14,5 +13,6 @@
"128": "static/img/taler-logo-128.png",
"256": "static/img/taler-logo-256.png",
"512": "static/img/taler-logo-512.png"
- }
+ },
+ "version_name": "0.10.7"
}
diff --git a/packages/taler-wallet-webextension/manifest-v2.json b/packages/taler-wallet-webextension/manifest-v2.json
index 74f103feb..6f2096b05 100644
--- a/packages/taler-wallet-webextension/manifest-v2.json
+++ b/packages/taler-wallet-webextension/manifest-v2.json
@@ -17,15 +17,51 @@
},
"permissions": [
"unlimitedStorage",
- "http://*/*",
- "https://*/*",
+ "storage",
+ "<all_urls>",
"activeTab"
],
- "optional_permissions": [
- "http://*/*",
- "https://*/*",
- "webRequest",
- "clipboardRead"
+ "web_accessible_resources": [
+ "static/wallet.html",
+ "dist/taler-wallet-interaction-loader.js.map",
+ "dist/taler-wallet-interaction-loader.js",
+ "dist/taler-wallet-interaction-support.js.map",
+ "dist/taler-wallet-interaction-support.js"
+ ],
+ "content_scripts": [
+ {
+ "matches": [
+ "file://*/*",
+ "http://*/*",
+ "https://*/*"
+ ],
+ "js": [
+ "dist/taler-wallet-interaction-loader.js"
+ ],
+ "run_at": "document_start"
+ }
+ ],
+ "protocol_handlers": [
+ {
+ "protocol": "ext+taler+http",
+ "name": "Taler Wallet WebExtension",
+ "uriTemplate": "/static/wallet.html#/cta/taler-uri/%s"
+ },
+ {
+ "protocol": "web+taler+http",
+ "name": "Taler Wallet WebExtension",
+ "uriTemplate": "/static/wallet.html#/cta/taler-uri/%s"
+ },
+ {
+ "protocol": "ext+taler",
+ "name": "Taler Wallet WebExtension",
+ "uriTemplate": "/static/wallet.html#/cta/taler-uri/%s"
+ },
+ {
+ "protocol": "web+taler",
+ "name": "Taler Wallet WebExtension",
+ "uriTemplate": "/static/wallet.html#/cta/taler-uri/%s"
+ }
],
"browser_action": {
"default_icon": {
diff --git a/packages/taler-wallet-webextension/manifest-v3.json b/packages/taler-wallet-webextension/manifest-v3.json
index 78dcac623..65a75824b 100644
--- a/packages/taler-wallet-webextension/manifest-v3.json
+++ b/packages/taler-wallet-webextension/manifest-v3.json
@@ -14,11 +14,15 @@
},
"permissions": [
"unlimitedStorage",
+ "storage",
"activeTab",
"scripting",
"declarativeContent",
"alarms"
],
+ "host_permissions": [
+ "<all_urls>"
+ ],
"commands": {
"_execute_action": {
"suggested_key": {
@@ -26,13 +30,33 @@
}
}
},
- "optional_permissions": [
- "webRequest",
- "clipboardRead"
+ "content_scripts": [
+ {
+ "id": "taler-wallet-interaction",
+ "matches": [
+ "http://*/*",
+ "https://*/*"
+ ],
+ "js": [
+ "dist/taler-wallet-interaction-loader.js"
+ ],
+ "run_at": "document_start"
+ }
],
- "host_permissions": [
- "http://*/*",
- "https://*/*"
+ "web_accessible_resources": [
+ {
+ "resources": [
+ "static/wallet.html",
+ "dist/taler-wallet-interaction-loader.js.map",
+ "dist/taler-wallet-interaction-loader.js",
+ "dist/taler-wallet-interaction-support.js.map",
+ "dist/taler-wallet-interaction-support.js"
+ ],
+ "matches": [
+ "https://*/*",
+ "http://*/*"
+ ]
+ }
],
"action": {
"default_icon": {
diff --git a/packages/taler-wallet-webextension/pack.sh b/packages/taler-wallet-webextension/pack.sh
index ca05f36de..f83948a4d 100755
--- a/packages/taler-wallet-webextension/pack.sh
+++ b/packages/taler-wallet-webextension/pack.sh
@@ -13,11 +13,14 @@ fi
vers_manifest=$(jq -r '.version' manifest-common.json)
+
+# Create version form Manifest v2
zipfile="taler-wallet-webextension-${vers_manifest}.zip"
TEMP_DIR=$(mktemp -d)
jq -s 'add | .name = "GNU Taler Wallet" ' manifest-common.json manifest-v2.json > $TEMP_DIR/manifest.json
-cp -r dist static $TEMP_DIR
+cp -r static $TEMP_DIR
+cp -r dist/prod $TEMP_DIR/dist
find $TEMP_DIR/dist \( -name "test.*" -o -name "*.test.*" -o -name "stories.*" -o -name "*.dev.*" \) -delete
[[ "$ENV" == "prod" ]] && find $TEMP_DIR/dist \( -name "*.map" \) -delete
@@ -35,11 +38,14 @@ mkdir -p extension/v2/unpacked
echo "Packed webextension: extension/v2/$zipfile"
cp -rf src extension/v2/unpacked
+# Create version form Manifest v3
zipfile="taler-wallet-webextension-${vers_manifest}.zip"
TEMP_DIR=$(mktemp -d)
jq -s 'add | .name = "GNU Taler Wallet" ' manifest-common.json manifest-v3.json > $TEMP_DIR/manifest.json
-cp -r dist static service_worker.js $TEMP_DIR
+cp -r static $TEMP_DIR
+cp -r dist/prod $TEMP_DIR/dist
+cp -r service_worker.js $TEMP_DIR
find $TEMP_DIR/dist \( -name "test.*" -o -name "*.test.*" -o -name "stories.*" -o -name "*.dev.*" \) -delete
[[ "$ENV" == "prod" ]] && find $TEMP_DIR/dist \( -name "*.map" \) -delete
diff --git a/packages/taler-wallet-webextension/package.json b/packages/taler-wallet-webextension/package.json
index 0deca26cc..bf063d76e 100644
--- a/packages/taler-wallet-webextension/package.json
+++ b/packages/taler-wallet-webextension/package.json
@@ -1,18 +1,22 @@
{
"name": "@gnu-taler/taler-wallet-webextension",
- "version": "0.9.0",
+ "version": "0.10.7",
"description": "GNU Taler Wallet browser extension",
"main": "./build/index.js",
"types": "./build/index.d.ts",
+ "type": "module",
"author": "Florian Dold",
"license": "AGPL-3.0-or-later",
"private": false,
"scripts": {
- "clean": "rimraf dist lib tsconfig.tsbuildinfo",
- "test": "pnpm compile && mocha 'dist/**/*.test.js' 'dist/**/test.js'",
+ "clean": "rm -rf dist lib tsconfig.tsbuildinfo extension",
+ "test": "./test.mjs && mocha --require source-map-support/register 'dist/test/**/*.test.js' 'dist/test/**/test.js'",
"test:coverage": "nyc pnpm test",
- "compile": "tsc && ./build-fast-with-linaria.mjs",
- "prepare": "pnpm compile",
+ "test:firefox": "web-ext run --source-dir extension/v2/unpacked --verbose --firefox-profile $(mktemp -d) --browser-console --devtools -f $FIREFOX_PATH/firefox-bin",
+ "test:firefox-private": "web-ext run --source-dir extension/v2/unpacked --verbose --firefox-profile $(mktemp -d) --browser-console --devtools -f $FIREFOX_PATH/firefox-bin --pref browser.privatebrowsing.autostart=true",
+ "compile": "tsc && ./build.mjs",
+ "typedoc": "typedoc --out dist/typedoc ./src/ --entryPointStrategy expand",
+ "lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
"dev": "./dev.mjs",
"pretty": "prettier --write src",
"i18n:extract": "pogen extract",
@@ -25,51 +29,41 @@
"@gnu-taler/taler-wallet-core": "workspace:*",
"date-fns": "^2.29.2",
"history": "4.10.1",
+ "jsqr": "^1.4.0",
"preact": "10.11.3",
"preact-router": "3.2.1",
- "qr-scanner": "^1.4.1",
"qrcode-generator": "^1.4.4",
- "tslib": "^2.4.0"
- },
- "eslintConfig": {
- "plugins": [
- "header"
- ],
- "rules": {
- "header/header": [
- 2,
- "copyleft-header.js"
- ]
- }
+ "tslib": "^2.6.2"
},
"devDependencies": {
- "@gnu-taler/web-util": "workspace:*",
- "@babel/core": "7.18.9",
- "@babel/plugin-transform-modules-commonjs": "7.18.6",
- "@babel/plugin-transform-react-jsx-source": "7.18.6",
+ "eslint": "^8.56.0",
+ "@typescript-eslint/eslint-plugin": "^6.19.0",
+ "@typescript-eslint/parser": "^6.19.0",
+ "eslint-config-prettier": "^9.1.0",
+ "eslint-plugin-react": "^7.33.2",
+ "@babel/preset-react": "^7.22.3",
"@babel/preset-typescript": "7.18.6",
- "@babel/runtime": "7.18.9",
"@gnu-taler/pogen": "workspace:*",
- "@linaria/babel-preset": "3.0.0-beta.22",
- "@linaria/core": "3.0.0-beta.22",
- "@linaria/react": "3.0.0-beta.22",
- "@linaria/webpack-loader": "3.0.0-beta.22",
+ "@gnu-taler/web-util": "workspace:*",
+ "@linaria/babel-preset": "5.0.4",
+ "@linaria/core": "5.0.2",
+ "@linaria/esbuild": "5.0.4",
+ "@linaria/react": "5.0.3",
+ "@linaria/shaker": "5.0.3",
"@types/chai": "^4.3.0",
"@types/chrome": "0.0.197",
"@types/history": "^4.7.8",
"@types/mocha": "^9.0.0",
- "@types/node": "^18.8.5",
- "babel-loader": "^8.2.3",
- "babel-plugin-transform-react-jsx": "^6.24.1",
+ "@types/node": "^18.11.17",
"chai": "^4.3.6",
- "esbuild": "^0.15.13",
+ "esbuild": "^0.19.9",
"mocha": "^9.2.0",
"nyc": "^15.1.0",
"polished": "^4.1.4",
"preact-cli": "^3.3.5",
"preact-render-to-string": "^5.1.19",
- "rimraf": "^3.0.2",
- "typescript": "^4.8.4"
+ "typescript": "5.3.3",
+ "web-ext": "^7.11.0"
},
"nyc": {
"include": [
@@ -80,4 +74,4 @@
"pogen": {
"domain": "taler-wallet-webex"
}
-} \ No newline at end of file
+}
diff --git a/packages/taler-wallet-webextension/src/NavigationBar.tsx b/packages/taler-wallet-webextension/src/NavigationBar.tsx
index 8fb289aa6..fe348f7fb 100644
--- a/packages/taler-wallet-webextension/src/NavigationBar.tsx
+++ b/packages/taler-wallet-webextension/src/NavigationBar.tsx
@@ -24,20 +24,22 @@
/**
* Imports.
*/
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { Fragment, h, VNode } from "preact";
+import { EnabledBySettings } from "./components/EnabledBySettings.js";
import {
NavigationHeader,
NavigationHeaderHolder,
SvgIcon,
} from "./components/styled/index.js";
-import { useTranslationContext } from "./context/translation.js";
-import settingsIcon from "./svg/settings_black_24dp.svg";
-import qrIcon from "./svg/qr_code_24px.svg";
-import warningIcon from "./svg/warning_24px.svg";
+import { useBackendContext } from "./context/backend.js";
import { useAsyncAsHook } from "./hooks/useAsyncAsHook.js";
-import { wxApi } from "./wxApi.js";
-import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-import { JustInDevMode } from "./components/JustInDevMode.js";
+import searchIcon from "./svg/search_24px.inline.svg";
+import qrIcon from "./svg/qr_code_24px.inline.svg";
+import settingsIcon from "./svg/settings_black_24dp.inline.svg";
+import warningIcon from "./svg/warning_24px.inline.svg";
+import { parseTalerUri, TalerUriAction } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
/**
* List of pages used by the wallet
@@ -45,6 +47,7 @@ import { JustInDevMode } from "./components/JustInDevMode.js";
* @author sebasjm
*/
+// eslint-disable-next-line @typescript-eslint/ban-types
type PageLocation<DynamicPart extends object> = {
pattern: string;
(params: DynamicPart): string;
@@ -53,15 +56,19 @@ type PageLocation<DynamicPart extends object> = {
function replaceAll(
pattern: string,
vars: Record<string, string>,
- values: Record<string, any>,
+ values: Record<string, string>,
): string {
let result = pattern;
for (const v in vars) {
- result = result.replace(vars[v], !values[v] ? "" : values[v]);
+ result = result.replace(
+ vars[v],
+ !values[v] ? "" : encodeURIComponent(values[v]),
+ );
}
return result;
}
+// eslint-disable-next-line @typescript-eslint/ban-types
function pageDefinition<T extends object>(pattern: string): PageLocation<T> {
const patternParams = pattern.match(/(:[\w?]*)/g);
if (!patternParams)
@@ -69,16 +76,20 @@ function pageDefinition<T extends object>(pattern: string): PageLocation<T> {
`page definition pattern ${pattern} doesn't have any parameter`,
);
- const vars = patternParams.reduce((prev, cur) => {
- const pName = cur.match(/(\w+)/g);
+ const vars = patternParams.reduce(
+ (prev, cur) => {
+ const pName = cur.match(/(\w+)/g);
- //skip things like :? in the path pattern
- if (!pName || !pName[0]) return prev;
- const name = pName[0];
- return { ...prev, [name]: cur };
- }, {} as Record<string, string>);
+ //skip things like :? in the path pattern
+ if (!pName || !pName[0]) return prev;
+ const name = pName[0];
+ return { ...prev, [name]: cur };
+ },
+ {} as Record<string, string>,
+ );
- const f = (values: T): string => replaceAll(pattern, vars, values);
+ const f = (values: T): string =>
+ replaceAll(pattern, vars, (values ?? {}) as Record<string, string>);
f.pattern = pattern;
return f;
}
@@ -89,6 +100,9 @@ export const Pages = {
balanceHistory: pageDefinition<{ currency?: string }>(
"/balance/history/:currency?",
),
+ searchHistory: pageDefinition<{ currency?: string }>(
+ "/search/history/:currency?",
+ ),
balanceDeposit: pageDefinition<{ amount: string }>(
"/balance/deposit/:amount",
),
@@ -113,13 +127,16 @@ export const Pages = {
"/settings/exchange/add/:currency?",
),
+ defaultCta: pageDefinition<{ uri: string }>("/taler-uri/:uri"),
cta: pageDefinition<{ action: string }>("/cta/:action"),
ctaPay: "/cta/pay",
+ ctaPayTemplate: "/cta/pay/template",
ctaRecovery: "/cta/recovery",
ctaRefund: "/cta/refund",
- ctaTips: "/cta/tip",
ctaWithdraw: "/cta/withdraw",
ctaDeposit: "/cta/deposit",
+ ctaExperiment: "/cta/experiment",
+ ctaAddExchange: "/cta/add/exchange",
ctaInvoiceCreate: pageDefinition<{ amount?: string }>(
"/cta/invoice/create/:amount?",
),
@@ -133,13 +150,40 @@ export const Pages = {
),
};
-export function PopupNavBar({
- path = "",
-}: {
- path?: string;
-}): // api: typeof wxApi,
-VNode {
- const api = wxApi; //FIXME: as parameter
+const talerUriActionToPageName: {
+ [t in TalerUriAction]: keyof typeof Pages | undefined;
+} = {
+ [TalerUriAction.Withdraw]: "ctaWithdraw",
+ [TalerUriAction.Pay]: "ctaPay",
+ [TalerUriAction.Refund]: "ctaRefund",
+ [TalerUriAction.PayPull]: "ctaInvoicePay",
+ [TalerUriAction.PayPush]: "ctaTransferPickup",
+ [TalerUriAction.Restore]: "ctaRecovery",
+ [TalerUriAction.PayTemplate]: "ctaPayTemplate",
+ [TalerUriAction.WithdrawExchange]: "ctaWithdrawManual",
+ [TalerUriAction.DevExperiment]: "ctaExperiment",
+ [TalerUriAction.AddExchange]: "ctaAddExchange",
+};
+
+export function getPathnameForTalerURI(talerUri: string): string | undefined {
+ const uri = parseTalerUri(talerUri);
+ if (!uri) {
+ return undefined;
+ }
+ const pageName = talerUriActionToPageName[uri.type];
+ if (!pageName) {
+ return undefined;
+ }
+ const pageString: string =
+ typeof Pages[pageName] === "function"
+ ? (Pages[pageName] as any)()
+ : Pages[pageName];
+ return `${pageString}?talerUri=${encodeURIComponent(talerUri)}`;
+}
+
+export type PopupNavBarOptions = "balance" | "backup" | "dev";
+export function PopupNavBar({ path }: { path?: PopupNavBarOptions }): VNode {
+ const api = useBackendContext();
const hook = useAsyncAsHook(async () => {
return await api.wallet.call(
WalletApiOperation.GetUserAttentionUnreadCount,
@@ -151,15 +195,14 @@ VNode {
const { i18n } = useTranslationContext();
return (
<NavigationHeader>
- <a
- href={Pages.balance}
- class={path.startsWith("/balance") ? "active" : ""}
- >
+ <a href={Pages.balance} class={path === "balance" ? "active" : ""}>
<i18n.Translate>Balance</i18n.Translate>
</a>
- <a href={Pages.backup} class={path.startsWith("/backup") ? "active" : ""}>
- <i18n.Translate>Backup</i18n.Translate>
- </a>
+ <EnabledBySettings name="backup">
+ <a href={Pages.backup} class={path === "backup" ? "active" : ""}>
+ <i18n.Translate>Backup</i18n.Translate>
+ </a>
+ </EnabledBySettings>
<div style={{ display: "flex", paddingTop: 4, justifyContent: "right" }}>
{attentionCount > 0 ? (
<a href={Pages.notifications}>
@@ -190,34 +233,31 @@ VNode {
</NavigationHeader>
);
}
-
-export function WalletNavBar({ path = "" }: { path?: string }): VNode {
+export type WalletNavBarOptions = "balance" | "backup" | "dev";
+export function WalletNavBar({ path }: { path?: WalletNavBarOptions }): VNode {
const { i18n } = useTranslationContext();
- const api = wxApi; //FIXME: as parameter
+ const api = useBackendContext();
const hook = useAsyncAsHook(async () => {
return await api.wallet.call(
WalletApiOperation.GetUserAttentionUnreadCount,
{},
);
});
- const attentionCount = !hook || hook.hasError ? 0 : hook.response.total;
+ const attentionCount =
+ (!hook || hook.hasError ? 0 : hook.response?.total) ?? 0;
return (
<NavigationHeaderHolder>
<NavigationHeader>
- <a
- href={Pages.balance}
- class={path.startsWith("/balance") ? "active" : ""}
- >
+ <a href={Pages.balance} class={path === "balance" ? "active" : ""}>
<i18n.Translate>Balance</i18n.Translate>
</a>
- <a
- href={Pages.backup}
- class={path.startsWith("/backup") ? "active" : ""}
- >
- <i18n.Translate>Backup</i18n.Translate>
- </a>
+ <EnabledBySettings name="backup">
+ <a href={Pages.backup} class={path === "backup" ? "active" : ""}>
+ <i18n.Translate>Backup</i18n.Translate>
+ </a>
+ </EnabledBySettings>
{attentionCount > 0 ? (
<a href={Pages.notifications}>
@@ -227,15 +267,22 @@ export function WalletNavBar({ path = "" }: { path?: string }): VNode {
<Fragment />
)}
- <JustInDevMode>
- <a href={Pages.dev} class={path.startsWith("/dev") ? "active" : ""}>
- <i18n.Translate>Dev</i18n.Translate>
+ <EnabledBySettings name="advancedMode">
+ <a href={Pages.dev} class={path === "dev" ? "active" : ""}>
+ <i18n.Translate>Dev tools</i18n.Translate>
</a>
- </JustInDevMode>
+ </EnabledBySettings>
<div
style={{ display: "flex", paddingTop: 4, justifyContent: "right" }}
>
+ <a href={Pages.searchHistory({})}>
+ <SvgIcon
+ title={i18n.str`Search transactions`}
+ dangerouslySetInnerHTML={{ __html: searchIcon }}
+ color="white"
+ />
+ </a>
<a href={Pages.qr}>
<SvgIcon
title={i18n.str`QR Reader and Taler URI`}
diff --git a/packages/taler-wallet-webextension/src/background.dev.ts b/packages/taler-wallet-webextension/src/background.dev.ts
index d81df11df..96cf63409 100644
--- a/packages/taler-wallet-webextension/src/background.dev.ts
+++ b/packages/taler-wallet-webextension/src/background.dev.ts
@@ -23,21 +23,14 @@
/**
* Imports.
*/
-import { platform, setupPlatform } from "./platform/api.js";
+import { platform, setupPlatform } from "./platform/background.js";
import devAPI from "./platform/dev.js";
import { wxMain } from "./wxBackend.js";
-console.log("Wallet setup for Dev API");
setupPlatform(devAPI);
-try {
- platform.registerOnInstalled(() => {
- platform.openWalletPage("/welcome");
- });
-} catch (e) {
- console.error(e);
+async function start() {
+ await platform.notifyWhenAppIsReady();
+ await wxMain();
}
-
-platform.notifyWhenAppIsReady(() => {
- wxMain();
-});
+start();
diff --git a/packages/taler-wallet-webextension/src/background.ts b/packages/taler-wallet-webextension/src/background.ts
index 0e2ea3f3a..7df66eff8 100644
--- a/packages/taler-wallet-webextension/src/background.ts
+++ b/packages/taler-wallet-webextension/src/background.ts
@@ -23,7 +23,7 @@
/**
* Imports.
*/
-import { platform, setupPlatform } from "./platform/api.js";
+import { platform, setupPlatform } from "./platform/background.js";
import chromeAPI from "./platform/chrome.js";
import firefoxAPI from "./platform/firefox.js";
import { wxMain } from "./wxBackend.js";
@@ -35,14 +35,15 @@ const isFirefox =
// FIXME: create different entry point for any platform instead of
// switching in runtime
if (isFirefox) {
- console.log("Wallet setup for Firefox API");
setupPlatform(firefoxAPI);
} else {
- console.log("Wallet setup for Chrome API");
setupPlatform(chromeAPI);
}
// setGlobalLogLevelFromString("trace")
-platform.notifyWhenAppIsReady(() => {
- wxMain();
-});
+
+async function start() {
+ await platform.notifyWhenAppIsReady();
+ await wxMain();
+}
+start();
diff --git a/packages/taler-wallet-webextension/src/browserWorkerEntry.ts b/packages/taler-wallet-webextension/src/browserWorkerEntry.ts
index 2f1a26e36..bb1794e56 100644
--- a/packages/taler-wallet-webextension/src/browserWorkerEntry.ts
+++ b/packages/taler-wallet-webextension/src/browserWorkerEntry.ts
@@ -22,11 +22,12 @@
* Imports.
*/
-import { j2s, Logger } from "@gnu-taler/taler-util";
import {
+ j2s,
+ Logger,
getErrorDetailFromException,
- nativeCrypto,
-} from "@gnu-taler/taler-wallet-core";
+} from "@gnu-taler/taler-util";
+import { nativeCrypto } from "@gnu-taler/taler-wallet-core";
const logger = new Logger("browserWorkerEntry.ts");
diff --git a/packages/taler-wallet-webextension/src/chromeBadge.ts b/packages/taler-wallet-webextension/src/chromeBadge.ts
index 350920e93..63d0372b6 100644
--- a/packages/taler-wallet-webextension/src/chromeBadge.ts
+++ b/packages/taler-wallet-webextension/src/chromeBadge.ts
@@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { platform } from "./platform/api.js";
+import { platform } from "./platform/background.js";
/**
* Polyfill for requestAnimationFrame, which
diff --git a/packages/taler-wallet-webextension/src/components/Amount.stories.tsx b/packages/taler-wallet-webextension/src/components/Amount.stories.tsx
index 095c9be24..fa28088eb 100644
--- a/packages/taler-wallet-webextension/src/components/Amount.stories.tsx
+++ b/packages/taler-wallet-webextension/src/components/Amount.stories.tsx
@@ -22,6 +22,7 @@
import { styled } from "@linaria/react";
import { Fragment, h, VNode } from "preact";
import { Amount } from "./Amount.js";
+import { AmountString } from "@gnu-taler/taler-util";
export default {
title: "amount",
@@ -39,7 +40,7 @@ const Table = styled.table`
function ProductTable(
prods: string[],
- AmountRender: (p: { value: string; index: number }) => VNode = Amount,
+ AmountRender: (p: { value: AmountString; index: number }) => VNode = Amount,
): VNode {
return (
<Table>
@@ -52,7 +53,7 @@ function ProductTable(
<tr key={i}>
<td>p{i}</td>
<td>
- <AmountRender value={value} index={i} />
+ <AmountRender value={value as AmountString} index={i} />
{/* <Amount value={value} fracSize={fracSize} /> */}
</td>
</tr>
diff --git a/packages/taler-wallet-webextension/src/components/AmountField.stories.tsx b/packages/taler-wallet-webextension/src/components/AmountField.stories.tsx
index 9a1d96014..daa06fa65 100644
--- a/packages/taler-wallet-webextension/src/components/AmountField.stories.tsx
+++ b/packages/taler-wallet-webextension/src/components/AmountField.stories.tsx
@@ -20,12 +20,10 @@
*/
import { AmountJson, Amounts } from "@gnu-taler/taler-util";
-import { styled } from "@linaria/react";
import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
-import { useTranslationContext } from "../context/translation.js";
-import { Grid } from "../mui/Grid.js";
-import { AmountFieldHandler, TextFieldHandler } from "../mui/handlers.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { AmountFieldHandler, nullFunction, withSafe } from "../mui/handlers.js";
import { AmountField } from "./AmountField.js";
export default {
@@ -33,15 +31,19 @@ export default {
};
function RenderAmount(): VNode {
- const [value, setValue] = useState<AmountJson | undefined>(undefined);
+ const [value, setValue] = useState<AmountJson | undefined>({
+ currency: "USD",
+ value: 1,
+ fraction: 0,
+ });
const error = value === undefined ? undefined : undefined;
const handler: AmountFieldHandler = {
value: value ?? Amounts.zeroOfCurrency("USD"),
- onInput: async (e) => {
+ onInput: withSafe(async (e) => {
setValue(e);
- },
+ }, nullFunction),
error,
};
const { i18n } = useTranslationContext();
@@ -49,13 +51,16 @@ function RenderAmount(): VNode {
<Fragment>
<AmountField
required
- label={<i18n.Translate>Amount</i18n.Translate>}
+ label={i18n.str`Amount`}
highestDenom={2000000}
lowestDenom={0.01}
handler={handler}
/>
<p>
- <pre>{JSON.stringify(value, undefined, 2)}</pre>
+ <pre>
+ value : {value?.value} <br />
+ fraction : {value?.fraction}
+ </pre>
</p>
</Fragment>
);
diff --git a/packages/taler-wallet-webextension/src/components/AmountField.tsx b/packages/taler-wallet-webextension/src/components/AmountField.tsx
index 2e8942f0d..c330c72b5 100644
--- a/packages/taler-wallet-webextension/src/components/AmountField.tsx
+++ b/packages/taler-wallet-webextension/src/components/AmountField.tsx
@@ -21,15 +21,20 @@ import {
amountMaxValue,
Amounts,
Result,
+ TranslatedString,
} from "@gnu-taler/taler-util";
import { Fragment, h, VNode } from "preact";
-import { useState } from "preact/hooks";
+import { useEffect, useState } from "preact/hooks";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { AmountFieldHandler } from "../mui/handlers.js";
import { TextField } from "../mui/TextField.js";
const HIGH_DENOM_SYMBOL = ["", "K", "M", "G", "T", "P"];
const LOW_DENOM_SYMBOL = ["", "m", "mm", "n", "p", "f"];
+/**
+ * Show normalized value based on the currency unit
+ */
export function AmountField({
label,
handler,
@@ -37,86 +42,58 @@ export function AmountField({
highestDenom = 1,
required,
}: {
- label: VNode;
+ label: TranslatedString;
lowestDenom?: number;
highestDenom?: number;
required?: boolean;
handler: AmountFieldHandler;
}): VNode {
+ const { i18n } = useTranslationContext();
const [unit, setUnit] = useState(1);
- const [decimalPlaces, setDecimalPlaces] = useState<number | undefined>(
- undefined,
- );
- const currency = handler.value.currency;
+ const [error, setError] = useState<string>("");
- let hd = Math.floor(Math.log10(highestDenom || 1) / 3);
- let ld = Math.ceil((-1 * Math.log10(lowestDenom || 1)) / 3);
+ const normal = normalize(handler.value, unit);
+ const previousValue = Amounts.stringifyValue(normal);
- const currencyLabels: Array<{ name: string; unit: number }> = [
- {
- name: currency,
- unit: 1,
- },
- ];
+ const [textValue, setTextValue] = useState<string>(previousValue);
+ useEffect(() => {
+ setTextValue(previousValue);
+ }, [previousValue]);
- while (hd > 0) {
- currencyLabels.push({
- name: `${HIGH_DENOM_SYMBOL[hd]}${currency}`,
- unit: Math.pow(10, hd * 3),
- });
- hd--;
- }
- while (ld > 0) {
- currencyLabels.push({
- name: `${LOW_DENOM_SYMBOL[ld]}${currency}`,
- unit: Math.pow(10, -1 * ld * 3),
- });
- ld--;
+ function updateUnit(newUnit: number) {
+ setUnit(newUnit);
+ const newNorm = normalize(handler.value, newUnit);
+ setTextValue(Amounts.stringifyValue(newNorm));
}
- const previousValue = Amounts.stringifyValue(handler.value, decimalPlaces);
-
- const normal = denormalize(handler.value, unit) ?? handler.value;
+ const currency = handler.value.currency;
- let textValue = Amounts.stringifyValue(normal, decimalPlaces);
- if (decimalPlaces === 0) {
- textValue += ".";
- }
+ const currencyLabels = buildLabelsForCurrency(
+ currency,
+ lowestDenom,
+ highestDenom,
+ );
function positiveAmount(value: string): string {
- // setDotAtTheEnd(value.endsWith("."));
- // const dotAtTheEnd = value.endsWith(".");
if (!value) {
if (handler.onInput) {
handler.onInput(Amounts.zeroOfCurrency(currency));
}
- return "";
- }
- try {
- //remove all but last dot
- const parsed = value.replace(/(\.)(?=.*\1)/g, "");
- const parts = parsed.split(".");
- setDecimalPlaces(parts.length === 1 ? undefined : parts[1].length);
-
- //FIXME: should normalize before parsing
- //parsing first add some restriction on the rage of the values
- const real = parseValue(currency, parsed);
-
- if (!real || real.value < 0) {
- return previousValue;
- }
+ } else
+ try {
+ const parsed = Amounts.parseOrThrow(`${currency}:${value.trim()}`);
- const realNormalized = normalize(real, unit);
+ const realValue = denormalize(parsed, unit);
- // console.log(real, unit, normal);
- if (realNormalized && handler.onInput) {
- handler.onInput(realNormalized);
+ if (handler.onInput) {
+ handler.onInput(realValue);
+ }
+ setError("");
+ } catch (e) {
+ setError(i18n.str`Amount is not valid`);
}
- return parsed;
- } catch (e) {
- // do nothing
- }
- return previousValue;
+ setTextValue(value);
+ return value;
}
return (
@@ -145,7 +122,7 @@ export function AmountField({
disabled={!handler.onInput}
onChange={(e) => {
const unit = Number.parseFloat(e.currentTarget.value);
- setUnit(unit);
+ updateUnit(unit);
}}
value={String(unit)}
style={{
@@ -167,30 +144,19 @@ export function AmountField({
disabled={!handler.onInput}
onInput={positiveAmount}
/>
+ {error && <div style={{ color: "red" }}>{error}</div>}
</Fragment>
);
}
-function parseValue(currency: string, s: string): AmountJson | undefined {
- const [intPart, fractPart] = s.split(".");
- const tailPart = !fractPart
- ? "0"
- : fractPart.substring(0, amountFractionalLength);
-
- const value = Number.parseInt(intPart, 10);
- const parsedTail = Number.parseFloat(`.${tailPart}`);
- if (Number.isNaN(value) || Number.isNaN(parsedTail)) {
- return undefined;
- }
- if (value > amountMaxValue) {
- return undefined;
- }
-
- const fraction = Math.round(amountFractionalBase * parsedTail);
- return { currency, fraction, value };
-}
-
-function normalize(amount: AmountJson, unit: number): AmountJson | undefined {
+/**
+ * Return the real value of a normalized unit
+ * If the value is 20 and the unit is kilo == 1000 the returned value will be amount * 1000
+ * @param amount
+ * @param unit
+ * @returns
+ */
+function denormalize(amount: AmountJson, unit: number): AmountJson {
if (unit === 1 || Amounts.isZero(amount)) return amount;
const result =
unit < 1
@@ -199,7 +165,15 @@ function normalize(amount: AmountJson, unit: number): AmountJson | undefined {
return result;
}
-function denormalize(amount: AmountJson, unit: number): AmountJson | undefined {
+/**
+ * Return the amount in the current unit.
+ * If the value is 20000 and the unit is kilo == 1000 and the returned value will be amount / unit
+ *
+ * @param amount
+ * @param unit
+ * @returns
+ */
+function normalize(amount: AmountJson, unit: number): AmountJson {
if (unit === 1 || Amounts.isZero(amount)) return amount;
const result =
unit < 1
@@ -207,3 +181,43 @@ function denormalize(amount: AmountJson, unit: number): AmountJson | undefined {
: Amounts.divide(amount, unit);
return result;
}
+
+/**
+ * Take every label in HIGH_DENOM_SYMBOL and LOW_DENOM_SYMBOL and create
+ * which create the corresponding unit multiplier
+ * @param currency
+ * @param lowestDenom
+ * @param highestDenom
+ * @returns
+ */
+function buildLabelsForCurrency(
+ currency: string,
+ lowestDenom: number,
+ highestDenom: number,
+): Array<{ name: string; unit: number }> {
+ let hd = Math.floor(Math.log10(highestDenom || 1) / 3);
+ let ld = Math.ceil((-1 * Math.log10(lowestDenom || 1)) / 3);
+
+ const result: Array<{ name: string; unit: number }> = [
+ {
+ name: currency,
+ unit: 1,
+ },
+ ];
+
+ while (hd > 0) {
+ result.push({
+ name: `${HIGH_DENOM_SYMBOL[hd]}${currency}`,
+ unit: Math.pow(10, hd * 3),
+ });
+ hd--;
+ }
+ while (ld > 0) {
+ result.push({
+ name: `${LOW_DENOM_SYMBOL[ld]}${currency}`,
+ unit: Math.pow(10, -1 * ld * 3),
+ });
+ ld--;
+ }
+ return result;
+}
diff --git a/packages/taler-wallet-webextension/src/components/BalanceTable.tsx b/packages/taler-wallet-webextension/src/components/BalanceTable.tsx
index c2cef451b..6dd577b88 100644
--- a/packages/taler-wallet-webextension/src/components/BalanceTable.tsx
+++ b/packages/taler-wallet-webextension/src/components/BalanceTable.tsx
@@ -14,41 +14,51 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { Amounts, Balance } from "@gnu-taler/taler-util";
-import { h, VNode } from "preact";
-import { TableWithRoundRows as TableWithRoundedRows } from "./styled/index.js";
+import { Amounts, ScopeType, WalletBalance } from "@gnu-taler/taler-util";
+import { Fragment, VNode, h } from "preact";
+import {
+ TableWithRoundRows as TableWithRoundedRows
+} from "./styled/index.js";
export function BalanceTable({
balances,
goToWalletHistory,
}: {
- balances: Balance[];
+ balances: WalletBalance[];
goToWalletHistory: (currency: string) => void;
}): VNode {
return (
- <TableWithRoundedRows>
- {balances.map((entry, idx) => {
- const av = Amounts.parseOrThrow(entry.available);
+ <Fragment>
+ <TableWithRoundedRows>
+ {balances.map((entry, idx) => {
+ const av = Amounts.parseOrThrow(entry.available);
- return (
- <tr
- key={idx}
- onClick={() => goToWalletHistory(av.currency)}
- style={{ cursor: "pointer" }}
- >
- <td>{av.currency}</td>
- <td
- style={{
- fontSize: "2em",
- textAlign: "right",
- width: "100%",
- }}
+ return (
+ <tr
+ key={idx}
+ onClick={() => goToWalletHistory(av.currency)}
+ style={{ cursor: "pointer" }}
>
- {Amounts.stringifyValue(av, 2)}
- </td>
- </tr>
- );
- })}
- </TableWithRoundedRows>
+ <td>{av.currency}</td>
+ <td
+ style={{
+ fontSize: "2em",
+ textAlign: "right",
+ width: "100%",
+ }}
+ >
+ {Amounts.stringifyValue(av, 2)}
+ <div style={{ fontSize: "small", color: "grey" }}>
+ {entry.scopeInfo.type === ScopeType.Exchange ||
+ entry.scopeInfo.type === ScopeType.Auditor
+ ? entry.scopeInfo.url
+ : undefined}
+ </div>
+ </td>
+ </tr>
+ );
+ })}
+ </TableWithRoundedRows>
+ </Fragment>
);
}
diff --git a/packages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx b/packages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx
index 6f4980aff..8b6377fc5 100644
--- a/packages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx
+++ b/packages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx
@@ -17,39 +17,60 @@
import {
AmountJson,
Amounts,
- PaytoUri,
+ parsePaytoUri,
segwitMinAmount,
+ stringifyPaytoUri,
+ TranslatedString,
+ WithdrawalExchangeAccountDetails,
} from "@gnu-taler/taler-util";
-import { Fragment, h, VNode } from "preact";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { ComponentChildren, Fragment, h, VNode } from "preact";
import { useEffect, useRef, useState } from "preact/hooks";
-import { useTranslationContext } from "../context/translation.js";
import { CopiedIcon, CopyIcon } from "../svg/index.js";
import { Amount } from "./Amount.js";
-import { ButtonBox, TooltipLeft } from "./styled/index.js";
+import { ButtonBox, TooltipLeft, WarningBox } from "./styled/index.js";
+import { Button } from "../mui/Button.js";
export interface BankDetailsProps {
- payto: PaytoUri | undefined;
- exchangeBaseUrl: string;
subject: string;
amount: AmountJson;
+ accounts: WithdrawalExchangeAccountDetails[];
}
export function BankDetailsByPaytoType({
- payto,
subject,
- exchangeBaseUrl,
amount,
+ accounts: unsortedAccounts,
}: BankDetailsProps): VNode {
const { i18n } = useTranslationContext();
+ const [index, setIndex] = useState(0);
+
+ if (!unsortedAccounts.length) {
+ return <div>the exchange account list is empty</div>;
+ }
+
+ const accounts = unsortedAccounts.sort((a, b) => {
+ return (b.priority ?? 0) - (a.priority ?? 0);
+ });
+
+ const selectedAccount = accounts[index];
+ const altCurrency = selectedAccount.currencySpecification?.name;
+
+ const payto = parsePaytoUri(selectedAccount.paytoUri);
+
if (!payto) return <Fragment />;
+ payto.params["amount"] = altCurrency
+ ? selectedAccount.transferAmount!
+ : Amounts.stringify(amount);
+ payto.params["message"] = subject;
- if (payto.isKnown && payto.targetType === "bitcoin") {
- const min = segwitMinAmount(amount.currency);
- const addrs = payto.segwitAddrs.map(
- (a) => `${a} ${Amounts.stringifyValue(min)}`,
- );
- addrs.unshift(`${payto.targetPath} ${Amounts.stringifyValue(amount)}`);
- const copyContent = addrs.join("\n");
+ function Frame({
+ title,
+ children,
+ }: {
+ title: TranslatedString;
+ children: ComponentChildren;
+ }): VNode {
return (
<section
style={{
@@ -59,9 +80,59 @@ export function BankDetailsByPaytoType({
borderRadius: 4,
}}
>
- <p style={{ marginTop: 0 }}>
- <i18n.Translate>Bitcoin transfer details</i18n.Translate>
- </p>
+ <div
+ style={{
+ display: "flex",
+ width: "100%",
+ justifyContent: "space-between",
+ }}
+ >
+ <p style={{ marginTop: 0 }}>{title}</p>
+ <div></div>
+ </div>
+
+ {children}
+
+ {accounts.length > 1 ? (
+ <Fragment>
+ {accounts.map((ac, acIdx) => {
+ const accountLabel = ac.bankLabel ?? `Account #${acIdx + 1}`;
+ return (
+ <Button
+ key={acIdx}
+ variant={acIdx === index ? "contained" : "outlined"}
+ onClick={async () => {
+ setIndex(acIdx);
+ }}
+ >
+ {accountLabel} (
+ {ac.currencySpecification?.name ?? amount.currency})
+ </Button>
+ );
+ })}
+
+ {/* <Button variant={currency === altCurrency ? "contained" : "outlined"}
+ onClick={async () => {
+ setCurrency(altCurrency)
+ }}
+ >
+ <i18n.Translate>{altCurrency}</i18n.Translate>
+ </Button> */}
+ </Fragment>
+ ) : undefined}
+ </section>
+ );
+ }
+
+ if (payto.isKnown && payto.targetType === "bitcoin") {
+ const min = segwitMinAmount(amount.currency);
+ const addrs = payto.segwitAddrs.map(
+ (a) => `${a} ${Amounts.stringifyValue(min)}`,
+ );
+ addrs.unshift(`${payto.targetPath} ${Amounts.stringifyValue(amount)}`);
+ const copyContent = addrs.join("\n");
+ return (
+ <Frame title={i18n.str`Bitcoin transfer details`}>
<p>
<i18n.Translate>
The exchange need a transaction with 3 output, one output is the
@@ -101,67 +172,120 @@ export function BankDetailsByPaytoType({
BTC, else you have to change the base unit to BTC
</i18n.Translate>
</p>
- </section>
+ </Frame>
);
}
const accountPart = !payto.isKnown ? (
- <Row
- name={<i18n.Translate>Account</i18n.Translate>}
- value={payto.targetPath}
- />
+ <Fragment>
+ <Row name={i18n.str`Account`} value={payto.targetPath} />
+ </Fragment>
) : payto.targetType === "x-taler-bank" ? (
<Fragment>
- <Row
- name={<i18n.Translate>Bank host</i18n.Translate>}
- value={payto.host}
- />
- <Row
- name={<i18n.Translate>Bank account</i18n.Translate>}
- value={payto.account}
- />
+ <Row name={i18n.str`Bank host`} value={payto.host} />
+ <Row name={i18n.str`Bank account`} value={payto.account} />
</Fragment>
) : payto.targetType === "iban" ? (
<Fragment>
{payto.bic !== undefined ? (
- <Row name={<i18n.Translate>BIC</i18n.Translate>} value={payto.bic} />
+ <Row name={i18n.str`BIC`} value={payto.bic} />
) : undefined}
- <Row name={<i18n.Translate>IBAN</i18n.Translate>} value={payto.iban} />
+ <Row name={i18n.str`IBAN`} value={payto.iban} />
</Fragment>
) : undefined;
- const receiver = payto.params["receiver"] || undefined;
+ const receiver =
+ payto.params["receiver-name"] || payto.params["receiver"] || undefined;
return (
- <div
- style={{
- textAlign: "left",
- border: "solid 1px black",
- padding: 8,
- borderRadius: 4,
- }}
- >
- <p style={{ marginTop: 0 }}>
- <i18n.Translate>Bank transfer details</i18n.Translate>
- </p>
+ <Frame title={i18n.str`Bank transfer details`}>
<table>
- {accountPart}
- <Row
- name={<i18n.Translate>Amount</i18n.Translate>}
- value={<Amount value={amount} hideCurrency />}
- />
- <Row
- name={<i18n.Translate>Subject</i18n.Translate>}
- value={subject}
- literal
- />
- {receiver ? (
+ <tbody>
+ <tr>
+ <td colSpan={3}>
+ <i18n.Translate>Step 1:</i18n.Translate>
+ &nbsp;
+ <i18n.Translate>
+ Copy this code and paste it into the subject/purpose field in
+ your banking app or bank website
+ </i18n.Translate>
+ </td>
+ </tr>
+ <Row name={i18n.str`Subject`} value={subject} literal />
+
+ <tr>
+ <td colSpan={3}>
+ <i18n.Translate>Step 2:</i18n.Translate>
+ &nbsp;
+ <i18n.Translate>
+ If you don't already have it in your banking favourites list,
+ then copy and paste this IBAN and the name into the receiver
+ fields in your banking app or website
+ </i18n.Translate>
+ </td>
+ </tr>
+ {accountPart}
+ {receiver ? (
+ <Row name={i18n.str`Receiver name`} value={receiver} />
+ ) : undefined}
+
+ <tr>
+ <td colSpan={3}>
+ <i18n.Translate>Step 3:</i18n.Translate>
+ &nbsp;
+ <i18n.Translate>
+ Finish the wire transfer setting the amount in your banking app
+ or website, then this withdrawal will proceed automatically.
+ </i18n.Translate>
+ </td>
+ </tr>
<Row
- name={<i18n.Translate>Receiver name</i18n.Translate>}
- value={receiver}
+ name={i18n.str`Amount`}
+ value={
+ <Amount
+ value={altCurrency ? selectedAccount.transferAmount! : amount}
+ hideCurrency
+ />
+ }
/>
- ) : undefined}
+
+ <tr>
+ <td colSpan={3}>
+ <WarningBox style={{ margin: 0 }}>
+ <span>
+ <i18n.Translate>
+ Make sure ALL data is correct, including the subject;
+ otherwise, the money will not arrive in this wallet. You can
+ use the copy buttons (<CopyIcon />) to prevent typing errors
+ or the "payto://" URI below to copy just one value.
+ </i18n.Translate>
+ </span>
+ </WarningBox>
+ </td>
+ </tr>
+
+ <tr>
+ <td colSpan={2} width="100%" style={{ wordBreak: "break-all" }}>
+ <i18n.Translate>
+ Alternative if your bank already supports PayTo URI, you can use
+ this{" "}
+ <a
+ target="_bank"
+ rel="noreferrer"
+ title="RFC 8905 for designating targets for payments"
+ href="https://tools.ietf.org/html/rfc8905"
+ >
+ PayTo URI
+ </a>{" "}
+ link instead
+ </i18n.Translate>
+ </td>
+ <td>
+ <CopyButton getContent={() => stringifyPaytoUri(payto)} />
+ </td>
+ </tr>
+ </tbody>
</table>
- </div>
+ </Frame>
);
}
@@ -200,7 +324,7 @@ function Row({
value,
literal,
}: {
- name: VNode;
+ name: TranslatedString;
value: string | VNode;
literal?: boolean;
}): VNode {
diff --git a/packages/taler-wallet-webextension/src/components/Banner.stories.tsx b/packages/taler-wallet-webextension/src/components/Banner.stories.tsx
index 39012480b..ee2dbfc69 100644
--- a/packages/taler-wallet-webextension/src/components/Banner.stories.tsx
+++ b/packages/taler-wallet-webextension/src/components/Banner.stories.tsx
@@ -24,7 +24,7 @@ import { Avatar } from "../mui/Avatar.js";
import { Typography } from "../mui/Typography.js";
import { Banner } from "./Banner.js";
import { SvgIcon } from "./styled/index.js";
-import wifiIcon from "../svg/wifi.svg";
+import wifiIcon from "../svg/wifi.inline.svg";
export default {
title: "banner",
component: Banner,
@@ -65,23 +65,25 @@ export const BasicExample = (): VNode => (
</a>
</p>
<Banner
- elements={[
- {
- icon: <SignalWifiOffIcon color="gray" />,
- description: (
- <Typography>
- You have lost connection to the internet. This app is offline.
- </Typography>
- ),
- },
- ]}
+ // elements={[
+ // {
+ // icon: <SignalWifiOffIcon color="gray" />,
+ // description: (
+ // <Typography>
+ // You have lost connection to the internet. This app is offline.
+ // </Typography>
+ // ),
+ // },
+ // ]}
confirm={{
label: "turn on wifi",
action: async () => {
return;
},
}}
- />
+ >
+ <div />
+ </Banner>
</Wrapper>
</Fragment>
);
@@ -92,31 +94,33 @@ export const PendingOperation = (): VNode => (
<Banner
title="PENDING TRANSACTIONS"
style={{ backgroundColor: "lightcyan", padding: 8 }}
- elements={[
- {
- icon: (
- <Avatar
- style={{
- border: "solid blue 1px",
- color: "blue",
- boxSizing: "border-box",
- }}
- >
- P
- </Avatar>
- ),
- description: (
- <Fragment>
- <Typography inline bold>
- EUR 37.95
- </Typography>
- &nbsp;
- <Typography inline>- 5 feb 2022</Typography>
- </Fragment>
- ),
- },
- ]}
- />
+ // elements={[
+ // {
+ // icon: (
+ // <Avatar
+ // style={{
+ // border: "solid blue 1px",
+ // color: "blue",
+ // boxSizing: "border-box",
+ // }}
+ // >
+ // P
+ // </Avatar>
+ // ),
+ // description: (
+ // <Fragment>
+ // <Typography inline bold>
+ // EUR 37.95
+ // </Typography>
+ // &nbsp;
+ // <Typography inline>- 5 feb 2022</Typography>
+ // </Fragment>
+ // ),
+ // },
+ // ]}
+ >
+ asd
+ </Banner>
</Wrapper>
</Fragment>
);
diff --git a/packages/taler-wallet-webextension/src/components/Banner.tsx b/packages/taler-wallet-webextension/src/components/Banner.tsx
index f95647d42..40a4847b8 100644
--- a/packages/taler-wallet-webextension/src/components/Banner.tsx
+++ b/packages/taler-wallet-webextension/src/components/Banner.tsx
@@ -13,21 +13,21 @@
You should have received 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, Fragment, VNode, JSX } from "preact";
-import { Divider } from "../mui/Divider.js";
+import { TranslatedString } from "@gnu-taler/taler-util";
+import { ComponentChildren, Fragment, h, JSX, VNode } from "preact";
import { Button } from "../mui/Button.js";
-import { Typography } from "../mui/Typography.js";
-import { Avatar } from "../mui/Avatar.js";
+import { Divider } from "../mui/Divider.js";
import { Grid } from "../mui/Grid.js";
import { Paper } from "../mui/Paper.js";
interface Props extends JSX.HTMLAttributes<HTMLDivElement> {
- titleHead?: VNode;
- elements: {
- icon?: VNode;
- description: VNode;
- action?: () => void;
- }[];
+ titleHead?: VNode | TranslatedString;
+ children: ComponentChildren;
+ // elements: {
+ // icon?: VNode;
+ // description: VNode;
+ // action?: () => void;
+ // }[];
confirm?: {
label: string;
action: () => Promise<void>;
@@ -36,8 +36,9 @@ interface Props extends JSX.HTMLAttributes<HTMLDivElement> {
export function Banner({
titleHead,
- elements,
+ children,
confirm,
+ href,
...rest
}: Props): VNode {
return (
@@ -49,25 +50,7 @@ export function Banner({
</Grid>
)}
<Grid container columns={1}>
- {elements.map((e, i) => (
- <Grid
- container
- item
- xs={1}
- key={i}
- wrap="nowrap"
- spacing={1}
- alignItems="center"
- onClick={e.action}
- >
- {e.icon && (
- <Grid item xs={"auto"}>
- <Avatar>{e.icon}</Avatar>
- </Grid>
- )}
- <Grid item>{e.description}</Grid>
- </Grid>
- ))}
+ {children}
</Grid>
{confirm && (
<Grid container justifyContent="flex-end" spacing={8}>
diff --git a/packages/taler-wallet-webextension/src/components/Checkbox.tsx b/packages/taler-wallet-webextension/src/components/Checkbox.tsx
index b6fa8b663..ec1b93a01 100644
--- a/packages/taler-wallet-webextension/src/components/Checkbox.tsx
+++ b/packages/taler-wallet-webextension/src/components/Checkbox.tsx
@@ -14,14 +14,15 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+import { TranslatedString } from "@gnu-taler/taler-util";
import { h, VNode } from "preact";
interface Props {
enabled?: boolean;
onToggle?: () => Promise<void>;
- label: VNode;
+ label: TranslatedString;
name: string;
- description?: VNode;
+ description?: VNode | TranslatedString;
}
export function Checkbox({
name,
@@ -30,6 +31,7 @@ export function Checkbox({
label,
description,
}: Props): VNode {
+
return (
<div>
<input
diff --git a/packages/taler-wallet-webextension/src/components/CurrentAlerts.tsx b/packages/taler-wallet-webextension/src/components/CurrentAlerts.tsx
new file mode 100644
index 000000000..b1ed3b02c
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/components/CurrentAlerts.tsx
@@ -0,0 +1,147 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { ComponentChildren, Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import {
+ Alert as AlertNotification,
+ useAlertContext,
+} from "../context/alert.js";
+import { Alert } from "../mui/Alert.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+
+/**
+ *
+ * @author sebasjm
+ */
+
+function AlertContext({
+ context,
+ cause,
+}: {
+ cause: unknown;
+ context: undefined | object;
+}): VNode {
+ const [more, setMore] = useState(false);
+ const [wrap, setWrap] = useState(false);
+ const { i18n } = useTranslationContext();
+ if (!more) {
+ return (
+ <div style={{ display: "flex", justifyContent: "right" }}>
+ <a
+ onClick={() => setMore(true)}
+ style={{ cursor: "pointer", textDecoration: "underline" }}
+ >
+ <i18n.Translate>more info</i18n.Translate>
+ </a>
+ </div>
+ );
+ }
+ const errorInfo = JSON.stringify(
+ context === undefined ? { cause } : { context, cause },
+ undefined,
+ 2,
+ );
+ return (
+ <Fragment>
+ <div style={{ display: "flex", justifyContent: "right" }}>
+ <a
+ onClick={() => setWrap(!wrap)}
+ style={{ cursor: "pointer", textDecoration: "underline" }}
+ >
+ <i18n.Translate>wrap text</i18n.Translate>
+ </a>
+ &nbsp;&nbsp;
+ <a
+ onClick={() => navigator.clipboard.writeText(errorInfo)}
+ style={{ cursor: "pointer", textDecoration: "underline" }}
+ >
+ <i18n.Translate>copy content</i18n.Translate>
+ </a>
+ &nbsp;&nbsp;
+ <a
+ onClick={() => setMore(false)}
+ style={{ cursor: "pointer", textDecoration: "underline" }}
+ >
+ <i18n.Translate>less info</i18n.Translate>
+ </a>
+ </div>
+ <pre
+ style={
+ wrap
+ ? {
+ whiteSpace: "pre-wrap",
+ overflowWrap: "anywhere",
+ }
+ : {
+ overflow: "overlay",
+ }
+ }
+ >
+ {errorInfo}
+ </pre>
+ </Fragment>
+ );
+}
+
+export function ErrorAlertView({
+ error,
+ onClose,
+}: {
+ error: AlertNotification;
+ onClose?: () => Promise<void>;
+}): VNode {
+ return (
+ <Wrapper>
+ <AlertView alert={error} onClose={onClose} />
+ </Wrapper>
+ );
+}
+
+export function AlertView({
+ alert,
+ onClose,
+}: {
+ alert: AlertNotification;
+ onClose?: () => Promise<void>;
+}): VNode {
+ return (
+ <Alert title={alert.message} severity={alert.type} onClose={onClose}>
+ <div style={{ display: "flex", flexDirection: "column" }}>
+ <div>{alert.description}</div>
+ {alert.type === "error" && alert.cause !== undefined ? (
+ <AlertContext context={alert.context} cause={alert.cause} />
+ ) : undefined}
+ </div>
+ </Alert>
+ );
+}
+
+export function CurrentAlerts(): VNode {
+ const { alerts, removeAlert } = useAlertContext();
+ if (alerts.length === 0) return <Fragment />;
+ return (
+ <Wrapper>
+ {alerts.map((n, i) => (
+ <AlertView key={i} alert={n} onClose={async () => removeAlert(n)} />
+ ))}
+ </Wrapper>
+ );
+}
+
+function Wrapper({ children }: { children: ComponentChildren }): VNode {
+ return <div style={{ margin: "1em" }}>{children}</div>;
+}
diff --git a/packages/taler-wallet-webextension/src/components/Diagnostics.tsx b/packages/taler-wallet-webextension/src/components/Diagnostics.tsx
index 886a44752..8bd0abcaf 100644
--- a/packages/taler-wallet-webextension/src/components/Diagnostics.tsx
+++ b/packages/taler-wallet-webextension/src/components/Diagnostics.tsx
@@ -16,7 +16,7 @@
import { WalletDiagnostics } from "@gnu-taler/taler-util";
import { Fragment, h, VNode } from "preact";
-import { useTranslationContext } from "../context/translation.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
interface Props {
timedOut: boolean;
diff --git a/packages/taler-wallet-webextension/src/components/EditableText.tsx b/packages/taler-wallet-webextension/src/components/EditableText.tsx
index c32ec158d..1da090492 100644
--- a/packages/taler-wallet-webextension/src/components/EditableText.tsx
+++ b/packages/taler-wallet-webextension/src/components/EditableText.tsx
@@ -16,7 +16,7 @@
import { h, VNode } from "preact";
import { useRef, useState } from "preact/hooks";
-import { useTranslationContext } from "../context/translation.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
interface Props {
value: string;
diff --git a/packages/taler-wallet-webextension/src/components/EnabledBySettings.tsx b/packages/taler-wallet-webextension/src/components/EnabledBySettings.tsx
new file mode 100644
index 000000000..6f666d301
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/components/EnabledBySettings.tsx
@@ -0,0 +1,38 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { ComponentChildren, Fragment, h, VNode } from "preact";
+import { useSettings } from "../hooks/useSettings.js";
+import { Settings } from "../platform/api.js";
+
+export function EnabledBySettings<K extends keyof Settings>({
+ children,
+ value,
+ name,
+}: {
+ name: K;
+ value?: Settings[K];
+ children: ComponentChildren;
+}): VNode {
+ const [settings] = useSettings();
+ if (value === undefined) {
+ if (!settings[name]) return <Fragment />;
+ return <Fragment>{children}</Fragment>;
+ }
+ if (settings[name] !== value) {
+ return <Fragment />;
+ }
+ return <Fragment>{children}</Fragment>;
+}
diff --git a/packages/taler-wallet-webextension/src/components/ErrorMessage.tsx b/packages/taler-wallet-webextension/src/components/ErrorMessage.tsx
index ce8dc0ad1..06c8a81ef 100644
--- a/packages/taler-wallet-webextension/src/components/ErrorMessage.tsx
+++ b/packages/taler-wallet-webextension/src/components/ErrorMessage.tsx
@@ -13,19 +13,23 @@
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+import { TranslatedString } from "@gnu-taler/taler-util";
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
-import arrowDown from "../svg/chevron-down.svg";
+import arrowDown from "../svg/chevron-down.inline.svg";
import { ErrorBox } from "./styled/index.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
export function ErrorMessage({
title,
description,
}: {
- title: VNode;
- description?: string | VNode;
+ title: TranslatedString;
+ description?: string | VNode | Error;
}): VNode | null {
const [showErrorDetail, setShowErrorDetail] = useState(false);
+ const [showMore, setShowMore] = useState(false);
+ const { i18n } = useTranslationContext();
return (
<ErrorBox style={{ paddingTop: 0, paddingBottom: 0 }}>
<div>
@@ -43,7 +47,14 @@ export function ErrorMessage({
</button>
)}
</div>
- {showErrorDetail && <p>{description}</p>}
+ {showErrorDetail && description && <p>
+ {description instanceof Error && !showMore ? description.message : description.toString()}
+ {description instanceof Error && <div>
+ <a href="#" onClick={(e) => {
+ setShowMore(!showMore)
+ e.preventDefault()
+ }}>{showMore ? i18n.str`show less` : i18n.str`show more`} </a> </div>}
+ </p>}
</ErrorBox>
);
}
diff --git a/packages/taler-wallet-webextension/src/components/ErrorTalerOperation.tsx b/packages/taler-wallet-webextension/src/components/ErrorTalerOperation.tsx
index a7223d2db..3298840e2 100644
--- a/packages/taler-wallet-webextension/src/components/ErrorTalerOperation.tsx
+++ b/packages/taler-wallet-webextension/src/components/ErrorTalerOperation.tsx
@@ -13,21 +13,20 @@
You should have received 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 { TalerErrorDetail } from "@gnu-taler/taler-util";
+import { TalerErrorDetail, TranslatedString } from "@gnu-taler/taler-util";
import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
-import arrowDown from "../svg/chevron-down.svg";
-import { useDevContext } from "../context/devContext.js";
+import arrowDown from "../svg/chevron-down.inline.svg";
import { ErrorBox } from "./styled/index.js";
+import { EnabledBySettings } from "./EnabledBySettings.js";
export function ErrorTalerOperation({
title,
error,
}: {
- title?: VNode;
+ title?: TranslatedString;
error?: TalerErrorDetail;
}): VNode | null {
- const { devMode } = useDevContext();
const [showErrorDetail, setShowErrorDetail] = useState(false);
if (!title || !error) return null;
@@ -62,11 +61,11 @@ export function ErrorTalerOperation({
<b>{error.hint}</b> {!errorHint ? "" : `: ${errorHint}`}{" "}
</div>
</div>
- {devMode && (
+ <EnabledBySettings name="showJsonOnError">
<div style={{ textAlign: "left", overflowX: "auto" }}>
<pre>{JSON.stringify(error, undefined, 2)}</pre>
</div>
- )}
+ </EnabledBySettings>
</Fragment>
)}
</ErrorBox>
diff --git a/packages/taler-wallet-webextension/src/components/HistoryItem.tsx b/packages/taler-wallet-webextension/src/components/HistoryItem.tsx
new file mode 100644
index 000000000..9be9326b2
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/components/HistoryItem.tsx
@@ -0,0 +1,432 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ AmountJson,
+ Amounts,
+ AmountString,
+ AbsoluteTime,
+ Transaction,
+ TransactionType,
+ WithdrawalType,
+ TransactionMajorState,
+ DenomLossEventType,
+ parsePaytoUri,
+} from "@gnu-taler/taler-util";
+import { h, VNode } from "preact";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Avatar } from "../mui/Avatar.js";
+import { Pages } from "../NavigationBar.js";
+import { assertUnreachable } from "../utils/index.js";
+import {
+ Column,
+ ExtraLargeText,
+ HistoryRow,
+ LargeText,
+ LightText,
+ SmallLightText,
+} from "./styled/index.js";
+import { Time } from "./Time.js";
+
+export function HistoryItem(props: { tx: Transaction }): VNode {
+ const tx = props.tx;
+ const { i18n } = useTranslationContext();
+ /**
+ *
+ */
+ switch (tx.type) {
+ case TransactionType.Withdrawal:
+ return (
+ <Layout
+ id={tx.transactionId}
+ amount={tx.amountEffective}
+ debitCreditIndicator={"credit"}
+ title={new URL(tx.exchangeBaseUrl).hostname}
+ timestamp={AbsoluteTime.fromPreciseTimestamp(tx.timestamp)}
+ iconPath={"W"}
+ currentState={tx.txState.major}
+ description={
+ tx.txState.major === TransactionMajorState.Pending
+ ? tx.withdrawalDetails.type ===
+ WithdrawalType.TalerBankIntegrationApi
+ ? !tx.withdrawalDetails.confirmed
+ ? i18n.str`Need approval in the Bank`
+ : i18n.str`Waiting for wire transfer to complete`
+ : tx.withdrawalDetails.type === WithdrawalType.ManualTransfer
+ ? i18n.str`Waiting for wire transfer to complete`
+ : "" //pending but no message
+ : undefined
+ }
+ />
+ );
+ case TransactionType.InternalWithdrawal:
+ return (
+ <Layout
+ id={tx.transactionId}
+ amount={tx.amountEffective}
+ debitCreditIndicator={"credit"}
+ title={new URL(tx.exchangeBaseUrl).hostname}
+ timestamp={AbsoluteTime.fromPreciseTimestamp(tx.timestamp)}
+ iconPath={"I"}
+ currentState={tx.txState.major}
+ description={
+ tx.txState.major === TransactionMajorState.Pending
+ ? tx.withdrawalDetails.type ===
+ WithdrawalType.TalerBankIntegrationApi
+ ? !tx.withdrawalDetails.confirmed
+ ? i18n.str`Need approval in the Bank`
+ : i18n.str`Exchange is waiting the wire transfer`
+ : tx.withdrawalDetails.type === WithdrawalType.ManualTransfer
+ ? i18n.str`Exchange is waiting the wire transfer`
+ : "" //pending but no message
+ : undefined
+ }
+ />
+ );
+ case TransactionType.Payment:
+ return (
+ <Layout
+ id={tx.transactionId}
+ amount={tx.amountEffective}
+ debitCreditIndicator={"debit"}
+ title={tx.info.merchant.name}
+ subtitle={tx.info.summary}
+ timestamp={AbsoluteTime.fromPreciseTimestamp(tx.timestamp)}
+ iconPath={"P"}
+ currentState={tx.txState.major}
+ description={
+ tx.txState.major === TransactionMajorState.Pending
+ ? i18n.str`Payment in progress`
+ : undefined
+ }
+ />
+ );
+ case TransactionType.Refund:
+ return (
+ <Layout
+ id={tx.transactionId}
+ amount={tx.amountEffective}
+ debitCreditIndicator={"credit"}
+ subtitle={tx.paymentInfo ? tx.paymentInfo.summary : undefined} //FIXME: DD37 wallet-core is not returning this value
+ title={
+ tx.paymentInfo
+ ? tx.paymentInfo.merchant.name
+ : "--unknown merchant--"
+ } //FIXME: DD37 wallet-core is not returning this value
+ timestamp={AbsoluteTime.fromPreciseTimestamp(tx.timestamp)}
+ iconPath={"R"}
+ currentState={tx.txState.major}
+ description={
+ tx.txState.major === TransactionMajorState.Pending
+ ? i18n.str`Executing refund...`
+ : undefined
+ }
+ />
+ );
+ case TransactionType.Refresh:
+ return (
+ <Layout
+ id={tx.transactionId}
+ amount={tx.amountEffective}
+ debitCreditIndicator={"credit"}
+ title={"Refresh"}
+ timestamp={AbsoluteTime.fromPreciseTimestamp(tx.timestamp)}
+ iconPath={"R"}
+ currentState={tx.txState.major}
+ description={
+ tx.txState.major === TransactionMajorState.Pending
+ ? i18n.str`Refreshing coins...`
+ : undefined
+ }
+ />
+ );
+ case TransactionType.Deposit:{
+ const payto = parsePaytoUri(tx.targetPaytoUri);
+ const title = payto === undefined || !payto.isKnown ? tx.targetPaytoUri :
+ payto.params["receiver-name"] ;
+ return (
+ <Layout
+ id={tx.transactionId}
+ amount={tx.amountEffective}
+ debitCreditIndicator={"debit"}
+ title={title}
+ timestamp={AbsoluteTime.fromPreciseTimestamp(tx.timestamp)}
+ iconPath={"D"}
+ currentState={tx.txState.major}
+ description={
+ tx.txState.major === TransactionMajorState.Pending
+ ? i18n.str`Deposit in progress`
+ : undefined
+ }
+ />
+ );
+ }
+ case TransactionType.PeerPullCredit:
+ return (
+ <Layout
+ id={tx.transactionId}
+ amount={tx.amountEffective}
+ debitCreditIndicator={"credit"}
+ title={tx.info.summary || "Invoice"}
+ timestamp={AbsoluteTime.fromPreciseTimestamp(tx.timestamp)}
+ iconPath={"I"}
+ currentState={tx.txState.major}
+ description={
+ tx.txState.major === TransactionMajorState.Pending
+ ? i18n.str`Waiting to be paid`
+ : undefined
+ }
+ />
+ );
+ case TransactionType.PeerPullDebit:
+ return (
+ <Layout
+ id={tx.transactionId}
+ amount={tx.amountEffective}
+ debitCreditIndicator={"debit"}
+ title={tx.info.summary || "Invoice"}
+ timestamp={AbsoluteTime.fromPreciseTimestamp(tx.timestamp)}
+ iconPath={"I"}
+ currentState={tx.txState.major}
+ description={
+ tx.txState.major === TransactionMajorState.Pending
+ ? i18n.str`Payment in progress`
+ : undefined
+ }
+ />
+ );
+ case TransactionType.PeerPushCredit:
+ return (
+ <Layout
+ id={tx.transactionId}
+ amount={tx.amountEffective}
+ debitCreditIndicator={"credit"}
+ title={tx.info.summary || "Transfer"}
+ timestamp={AbsoluteTime.fromPreciseTimestamp(tx.timestamp)}
+ iconPath={"T"}
+ currentState={tx.txState.major}
+ description={
+ tx.txState.major === TransactionMajorState.Pending
+ ? i18n.str`Receiving the transfer`
+ : undefined
+ }
+ />
+ );
+ case TransactionType.PeerPushDebit:
+ return (
+ <Layout
+ id={tx.transactionId}
+ amount={tx.amountEffective}
+ debitCreditIndicator={"debit"}
+ title={tx.info.summary || "Transfer"}
+ timestamp={AbsoluteTime.fromPreciseTimestamp(tx.timestamp)}
+ iconPath={"T"}
+ currentState={tx.txState.major}
+ description={
+ tx.txState.major === TransactionMajorState.Pending
+ ? i18n.str`Waiting to be received`
+ : undefined
+ }
+ />
+ );
+ case TransactionType.DenomLoss: {
+ switch (tx.lossEventType) {
+ case DenomLossEventType.DenomExpired: {
+ return (
+ <Layout
+ id={tx.transactionId}
+ amount={tx.amountEffective}
+ debitCreditIndicator={"debit"}
+ title={i18n.str`Denomination expired`}
+ timestamp={AbsoluteTime.fromPreciseTimestamp(tx.timestamp)}
+ iconPath={"L"}
+ currentState={tx.txState.major}
+ description={undefined}
+ />
+ );
+ }
+ case DenomLossEventType.DenomVanished: {
+ return (
+ <Layout
+ id={tx.transactionId}
+ amount={tx.amountEffective}
+ debitCreditIndicator={"debit"}
+ title={i18n.str`Denomination vanished`}
+ timestamp={AbsoluteTime.fromPreciseTimestamp(tx.timestamp)}
+ iconPath={"L"}
+ currentState={tx.txState.major}
+ description={undefined}
+ />
+ );
+ }
+ case DenomLossEventType.DenomUnoffered: {
+ return (
+ <Layout
+ id={tx.transactionId}
+ amount={tx.amountEffective}
+ debitCreditIndicator={"debit"}
+ title={i18n.str`Denomination unoffered`}
+ timestamp={AbsoluteTime.fromPreciseTimestamp(tx.timestamp)}
+ iconPath={"L"}
+ currentState={tx.txState.major}
+ description={undefined}
+ />
+ );
+ }
+ default: {
+ assertUnreachable(tx.lossEventType);
+ }
+ }
+ break;
+ }
+ case TransactionType.Recoup:
+ throw Error("recoup transaction not implemented");
+ default: {
+ assertUnreachable(tx);
+ }
+ }
+}
+
+function Layout(props: LayoutProps): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <HistoryRow
+ href={Pages.balanceTransaction({ tid: props.id })}
+ style={{
+ backgroundColor:
+ props.currentState === TransactionMajorState.Pending ||
+ props.currentState === TransactionMajorState.Dialog
+ ? "lightcyan"
+ : props.currentState === TransactionMajorState.Failed
+ ? "#ff000040"
+ : props.currentState === TransactionMajorState.Aborted ||
+ props.currentState === TransactionMajorState.Aborting
+ ? "#00000010"
+ : "inherit",
+ alignItems: "center",
+ }}
+ >
+ <Avatar
+ style={{
+ border: "solid gray 1px",
+ color: "gray",
+ boxSizing: "border-box",
+ }}
+ >
+ {props.iconPath}
+ </Avatar>
+ <Column>
+ <LargeText>
+ <div>{props.title}</div>
+ {props.subtitle && (
+ <div style={{ color: "gray", fontSize: "medium", marginTop: 5 }}>
+ {props.subtitle}
+ </div>
+ )}
+ </LargeText>
+ {props.description && (
+ <LightText style={{ marginTop: 5, marginBottom: 5 }}>
+ <i18n.Translate>{props.description}</i18n.Translate>
+ </LightText>
+ )}
+ <SmallLightText style={{ marginTop: 5 }}>
+ <Time timestamp={props.timestamp} format="HH:mm" />
+ </SmallLightText>
+ </Column>
+ <TransactionAmount
+ currentState={props.currentState}
+ amount={Amounts.parseOrThrow(props.amount)}
+ debitCreditIndicator={props.debitCreditIndicator}
+ />
+ </HistoryRow>
+ );
+}
+
+interface LayoutProps {
+ debitCreditIndicator: "debit" | "credit" | "unknown";
+ amount: AmountString | "unknown";
+ timestamp: AbsoluteTime;
+ title: string;
+ subtitle?: string;
+ id: string;
+ iconPath: string;
+ currentState: TransactionMajorState;
+ description?: string;
+}
+
+interface TransactionAmountProps {
+ debitCreditIndicator: "debit" | "credit" | "unknown";
+ amount: AmountJson;
+ currentState: TransactionMajorState;
+}
+
+function TransactionAmount(props: TransactionAmountProps): VNode {
+ const { i18n } = useTranslationContext();
+ let sign: string;
+ switch (props.debitCreditIndicator) {
+ case "credit":
+ sign = "+";
+ break;
+ case "debit":
+ sign = "-";
+ break;
+ case "unknown":
+ sign = "";
+ }
+ return (
+ <Column
+ style={{
+ textAlign: "center",
+ color:
+ props.currentState !== TransactionMajorState.Done
+ ? "gray"
+ : sign === "+"
+ ? "darkgreen"
+ : sign === "-"
+ ? "darkred"
+ : undefined,
+ }}
+ >
+ <ExtraLargeText>
+ {sign}
+ {Amounts.stringifyValue(props.amount, 2)}
+ </ExtraLargeText>
+ {props.currentState === TransactionMajorState.Aborted ? (
+ <div
+ style={{
+ color: "black",
+ border: "1px black solid",
+ borderRadius: 8,
+ padding: 4,
+ }}
+ >
+ <i18n.Translate>ABORTED</i18n.Translate>
+ </div>
+ ) : props.currentState === TransactionMajorState.Failed ? (
+ <div
+ style={{
+ color: "red",
+ border: "1px darkred solid",
+ borderRadius: 8,
+ padding: 4,
+ }}
+ >
+ <i18n.Translate>FAILED</i18n.Translate>
+ </div>
+ ) : undefined}
+ </Column>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/components/Loading.tsx b/packages/taler-wallet-webextension/src/components/Loading.tsx
index f2195b646..b0209f855 100644
--- a/packages/taler-wallet-webextension/src/components/Loading.tsx
+++ b/packages/taler-wallet-webextension/src/components/Loading.tsx
@@ -13,30 +13,88 @@
You should have received 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 { css } from "@linaria/core";
import { Fragment, h, VNode } from "preact";
import { useEffect, useState } from "preact/hooks";
-import { useTranslationContext } from "../context/translation.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import ProgressIcon from "../svg/progress.inline.svg";
import { CenteredText } from "./styled/index.js";
+const fadeIn = css`
+ & {
+ animation: fadein 3s;
+ }
+ @keyframes fadein {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+ }
+`;
+
export function Loading(): VNode {
const { i18n } = useTranslationContext();
- const [tooLong, setTooLong] = useState(false);
- useEffect(() => {
- const id = setTimeout(() => {
- setTooLong(true);
- }, 500);
- return () => {
- clearTimeout(id);
- };
- });
- if (tooLong) {
- return (
- <section style={{ margin: "auto" }}>
- <CenteredText>
- <i18n.Translate>Loading</i18n.Translate>...
- </CenteredText>
- </section>
- );
- }
- return <Fragment />;
+ return (
+ <section style={{ margin: "auto" }}>
+ <CenteredText class={fadeIn}>
+ <i18n.Translate>Loading</i18n.Translate>...
+ </CenteredText>
+ {/* <div class={ripple} style={{ "--size": "250px" }}>
+ <div></div>
+ <div></div>
+ </div> */}
+ <div class={fadeIn} dangerouslySetInnerHTML={{ __html: ProgressIcon }} />
+ </section>
+ );
}
+
+const ripple = css`
+ & {
+ display: inline-block;
+ position: relative;
+ width: var(--size);
+ height: var(--size);
+ }
+ & div {
+ position: absolute;
+ border: 4px solid black;
+ opacity: 1;
+ border-radius: 50%;
+ animation: lds-ripple 1s cubic-bezier(0, 0.2, 0.8, 1) infinite;
+ }
+ & div:nth-child(2) {
+ animation-delay: -0.3s;
+ }
+ @keyframes lds-ripple {
+ 0% {
+ top: calc(var(--size) / 2);
+ left: calc(var(--size) / 2);
+ width: 0;
+ height: 0;
+ opacity: 0;
+ }
+ 14.9% {
+ top: calc(var(--size) / 2);
+ left: calc(var(--size) / 2);
+ width: 0;
+ height: 0;
+ opacity: 0;
+ }
+ 15% {
+ top: calc(var(--size) / 2);
+ left: calc(var(--size) / 2);
+ width: 0;
+ height: 0;
+ opacity: 1;
+ }
+ 100% {
+ top: 0px;
+ left: 0px;
+ width: var(--size);
+ height: var(--size);
+ opacity: 0;
+ }
+ }
+`;
diff --git a/packages/taler-wallet-webextension/src/components/LogoHeader.tsx b/packages/taler-wallet-webextension/src/components/LogoHeader.tsx
index 7b9701336..2330b1b95 100644
--- a/packages/taler-wallet-webextension/src/components/LogoHeader.tsx
+++ b/packages/taler-wallet-webextension/src/components/LogoHeader.tsx
@@ -15,7 +15,7 @@
*/
import { h, VNode } from "preact";
-import logo from "../svg/logo-2021.svg";
+import logo from "../svg/logo-2021.inline.svg";
export function LogoHeader(): VNode {
return (
diff --git a/packages/taler-wallet-webextension/src/components/Modal.tsx b/packages/taler-wallet-webextension/src/components/Modal.tsx
index 3fea063d3..f8c0f1651 100644
--- a/packages/taler-wallet-webextension/src/components/Modal.tsx
+++ b/packages/taler-wallet-webextension/src/components/Modal.tsx
@@ -17,8 +17,8 @@
import { styled } from "@linaria/react";
import { ComponentChildren, h, VNode } from "preact";
import { ButtonHandler } from "../mui/handlers.js";
-import closeIcon from "../svg/close_24px.svg";
-import { Link, LinkPrimary, LinkWarning } from "./styled/index.js";
+import closeIcon from "../svg/close_24px.inline.svg";
+import { Link } from "./styled/index.js";
interface Props {
children: ComponentChildren;
@@ -52,40 +52,44 @@ const Body = styled.div`
export function Modal({ title, children, onClose }: Props): VNode {
return (
- <FullSize onClick={onClose?.onClick}>
- <div
- onClick={(e) => e.stopPropagation()}
- style={{
- background: "white",
- width: 600,
- height: "80%",
- margin: "auto",
- borderRadius: 8,
- padding: 8,
- // overflow: "scroll",
- }}
- >
- <Header>
- <div>
- <h2>{title}</h2>
- </div>
- <Link onClick={onClose?.onClick}>
- <div
- style={{
- height: 24,
- width: 24,
- marginLeft: 4,
- marginRight: 4,
- // fill: "white",
- }}
- dangerouslySetInnerHTML={{ __html: closeIcon }}
- />
- </Link>
- </Header>
- <hr />
+ <div style={{ top: 0, width: "100%", height: "100%" }}>
- <Body onClick={(e: any) => e.stopPropagation()}>{children}</Body>
- </div>
- </FullSize>
+ <FullSize onClick={onClose?.onClick}>
+ <div
+ onClick={(e) => e.stopPropagation()}
+ style={{
+ background: "white",
+ width: 600,
+ height: "80%",
+ margin: "auto",
+ borderRadius: 8,
+ padding: 8,
+ zIndex: 100,
+ // overflow: "scroll",
+ }}
+ >
+ <Header>
+ <div>
+ <h2>{title}</h2>
+ </div>
+ <Link onClick={onClose?.onClick}>
+ <div
+ style={{
+ height: 24,
+ width: 24,
+ marginLeft: 4,
+ marginRight: 4,
+ // fill: "white",
+ }}
+ dangerouslySetInnerHTML={{ __html: closeIcon }}
+ />
+ </Link>
+ </Header>
+ <hr />
+
+ <Body onClick={(e: any) => e.stopPropagation()}>{children}</Body>
+ </div>
+ </FullSize>
+ </div>
);
}
diff --git a/packages/taler-wallet-webextension/src/components/MultiActionButton.tsx b/packages/taler-wallet-webextension/src/components/MultiActionButton.tsx
index 673ff3dc2..7d3cf3f57 100644
--- a/packages/taler-wallet-webextension/src/components/MultiActionButton.tsx
+++ b/packages/taler-wallet-webextension/src/components/MultiActionButton.tsx
@@ -13,15 +13,15 @@
You should have received 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 { getUnpackedSettings } from "http2";
+import { TranslatedString } from "@gnu-taler/taler-util";
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
import { Button } from "../mui/Button.js";
-import arrowDown from "../svg/chevron-down.svg";
+import arrowDown from "../svg/chevron-down.inline.svg";
import { ParagraphClickable } from "./styled/index.js";
export interface Props {
- label: (s: string) => VNode;
+ label: (s: string) => TranslatedString;
actions: string[];
onClick: (s: string) => Promise<void>;
}
diff --git a/packages/taler-wallet-webextension/src/components/Part.tsx b/packages/taler-wallet-webextension/src/components/Part.tsx
index a488ca4dc..b95bbf3b7 100644
--- a/packages/taler-wallet-webextension/src/components/Part.tsx
+++ b/packages/taler-wallet-webextension/src/components/Part.tsx
@@ -14,7 +14,11 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { PaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util";
+import {
+ PaytoUri,
+ stringifyPaytoUri,
+ TranslatedString,
+} from "@gnu-taler/taler-util";
import { styled } from "@linaria/react";
import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
@@ -27,8 +31,8 @@ import {
export type Kind = "positive" | "negative" | "neutral";
interface Props {
- title: VNode | string;
- text: VNode | string;
+ title: VNode | TranslatedString;
+ text: VNode | TranslatedString;
kind?: Kind;
big?: boolean;
showSign?: boolean;
@@ -92,8 +96,8 @@ const CollasibleBox = styled.div`
}
}
`;
-import arrowDown from "../svg/chevron-down.svg";
-import { useTranslationContext } from "../context/translation.js";
+import arrowDown from "../svg/chevron-down.inline.svg";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
export function PartCollapsible({ text, title, big, showSign }: Props): VNode {
const Text = big ? ExtraLargeText : LargeText;
diff --git a/packages/taler-wallet-webextension/src/components/PaymentButtons.tsx b/packages/taler-wallet-webextension/src/components/PaymentButtons.tsx
new file mode 100644
index 000000000..7fa0376c9
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/components/PaymentButtons.tsx
@@ -0,0 +1,239 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ AmountJson,
+ Amounts,
+ PaymentInsufficientBalanceDetails,
+ PreparePayResult,
+ PreparePayResultType,
+ TranslatedString,
+ parsePayUri,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { useBackendContext } from "../context/backend.js";
+import { Button } from "../mui/Button.js";
+import { ButtonHandler } from "../mui/handlers.js";
+import { assertUnreachable } from "../utils/index.js";
+import { Amount } from "./Amount.js";
+import { Part } from "./Part.js";
+import { QR } from "./QR.js";
+import { LinkSuccess, WarningBox } from "./styled/index.js";
+
+interface Props {
+ payStatus: PreparePayResult;
+ payHandler: ButtonHandler | undefined;
+ uri: string;
+ amount: AmountJson;
+ goToWalletManualWithdraw: (currency: string) => Promise<void>;
+}
+
+export function PaymentButtons({
+ payStatus,
+ uri,
+ payHandler,
+ amount,
+ goToWalletManualWithdraw,
+}: Props): VNode {
+ const { i18n } = useTranslationContext();
+ if (payStatus.status === PreparePayResultType.PaymentPossible) {
+ return (
+ <Fragment>
+ <section>
+ <Button
+ variant="contained"
+ color="success"
+ onClick={payHandler?.onClick}
+ >
+ <i18n.Translate>
+ Pay &nbsp;
+ {<Amount value={amount} />}
+ </i18n.Translate>
+ </Button>
+ </section>
+ <PayWithMobile uri={uri} />
+ </Fragment>
+ );
+ }
+
+ if (payStatus.status === PreparePayResultType.InsufficientBalance) {
+ const reason = getReason(payStatus.balanceDetails);
+
+ let BalanceMessage = "";
+ switch (reason) {
+ case "age-acceptable": {
+ BalanceMessage = i18n.str`Balance is not enough because you have ${Amounts.stringifyValue(
+ payStatus.balanceDetails.balanceAgeAcceptable,
+ )} ${amount.currency} to pay for this contract which is restricted.`;
+ break;
+ }
+ case "available": {
+ BalanceMessage = i18n.str`Balance is not enough because you have ${Amounts.stringifyValue(
+ payStatus.balanceDetails.balanceAvailable,
+ )} ${amount.currency} available.`;
+ break;
+ }
+ case "merchant-acceptable": {
+ BalanceMessage = i18n.str`Balance is not enough because merchant will just accept ${Amounts.stringifyValue(
+ payStatus.balanceDetails.balanceReceiverAcceptable,
+ )} ${amount.currency
+ } . To know more you can check which exchange and auditors the merchant trust.`;
+ break;
+ }
+ case "merchant-depositable": {
+ BalanceMessage = i18n.str`Balance is not enough because merchant will just accept ${Amounts.stringifyValue(
+ payStatus.balanceDetails.balanceReceiverDepositable,
+ )} ${amount.currency
+ } . To know more you can check which wire methods the merchant accepts.`;
+ break;
+ }
+ case "material": {
+ BalanceMessage = i18n.str`Balance is not enough because you have ${Amounts.stringifyValue(
+ payStatus.balanceDetails.balanceMaterial,
+ )} ${amount.currency
+ } to spend right know. There are some coins that need to be refreshed.`;
+ break;
+ }
+ case "fee-gap": {
+ BalanceMessage = i18n.str`Balance looks like it should be enough, but doesn't cover all fees requested by the merchant and payment processor. Please ensure there is at least ${Amounts.stringifyValue(
+ Amounts.stringify(
+ Amounts.sub(
+ amount,
+ payStatus.balanceDetails.maxEffectiveSpendAmount,
+ ).amount,
+ ),
+ )} ${amount.currency
+ } more balance in your wallet or ask your merchant to cover more of the fees.`;
+ break;
+ }
+ default:
+ assertUnreachable(reason);
+ }
+
+ return (
+ <Fragment>
+ <section>
+ <WarningBox>{BalanceMessage}</WarningBox>
+ </section>
+ <section>
+ <Button
+ variant="contained"
+ color="success"
+ onClick={() => goToWalletManualWithdraw(Amounts.stringify(amount))}
+ >
+ <i18n.Translate>Get digital cash</i18n.Translate>
+ </Button>
+ </section>
+ <PayWithMobile uri={uri} />
+ </Fragment>
+ );
+ }
+ if (payStatus.status === PreparePayResultType.AlreadyConfirmed) {
+ return (
+ <Fragment>
+ <section>
+ {payStatus.paid && payStatus.contractTerms.fulfillment_message && (
+ <Part
+ title={i18n.str`Merchant message`}
+ text={
+ payStatus.contractTerms.fulfillment_message as TranslatedString
+ }
+ kind="neutral"
+ />
+ )}
+ </section>
+ </Fragment>
+ );
+ }
+
+ assertUnreachable(payStatus);
+}
+
+function PayWithMobile({ uri }: { uri: string }): VNode {
+ const { i18n } = useTranslationContext();
+ const api = useBackendContext();
+
+ const payUri = parsePayUri(uri);
+
+ const [showQR, setShowQR] = useState<string | undefined>(undefined);
+ async function sharePrivatePaymentURI() {
+ if (!payUri) {
+ return;
+ }
+ if (!showQR) {
+ const result = await api.wallet.call(WalletApiOperation.SharePayment, {
+ merchantBaseUrl: payUri.merchantBaseUrl,
+ orderId: payUri.orderId,
+ });
+ setShowQR(result.privatePayUri);
+ } else {
+ setShowQR(undefined);
+ }
+ }
+ if (!payUri) {
+ return <Fragment />
+ }
+ return (
+ <section>
+ <LinkSuccess upperCased onClick={sharePrivatePaymentURI}>
+ {!showQR ? i18n.str`Pay with a mobile phone` : i18n.str`Hide QR`}
+ </LinkSuccess>
+ {showQR && (
+ <div>
+ <QR text={showQR} />
+ <i18n.Translate>
+ Scan the QR code or &nbsp;
+ <a href={showQR}>
+ <i18n.Translate>click here</i18n.Translate>
+ </a>
+ </i18n.Translate>
+ </div>
+ )}
+ </section>
+ );
+}
+
+type NoEnoughBalanceReason =
+ | "available"
+ | "material"
+ | "age-acceptable"
+ | "merchant-acceptable"
+ | "merchant-depositable"
+ | "fee-gap";
+
+function getReason(
+ info: PaymentInsufficientBalanceDetails,
+): NoEnoughBalanceReason {
+ if (Amounts.cmp(info.amountRequested, info.balanceAvailable) > 0) {
+ return "available";
+ }
+ if (Amounts.cmp(info.amountRequested, info.balanceMaterial) > 0) {
+ return "material";
+ }
+ if (Amounts.cmp(info.amountRequested, info.balanceAgeAcceptable) > 0) {
+ return "age-acceptable";
+ }
+ if (Amounts.cmp(info.amountRequested, info.balanceReceiverAcceptable) > 0) {
+ return "merchant-acceptable";
+ }
+ if (Amounts.cmp(info.amountRequested, info.balanceReceiverDepositable) > 0) {
+ return "merchant-depositable";
+ }
+ return "fee-gap";
+}
diff --git a/packages/taler-wallet-webextension/src/components/PendingTransactions.stories.tsx b/packages/taler-wallet-webextension/src/components/PendingTransactions.stories.tsx
index 2155c7aa6..d1c49aea2 100644
--- a/packages/taler-wallet-webextension/src/components/PendingTransactions.stories.tsx
+++ b/packages/taler-wallet-webextension/src/components/PendingTransactions.stories.tsx
@@ -24,7 +24,7 @@ import {
Transaction,
TransactionType,
} from "@gnu-taler/taler-util";
-import { createExample } from "../test-utils.js";
+import * as tests from "@gnu-taler/web-util/testing";
import { PendingTransactionsView as TestedComponent } from "./PendingTransactions.js";
export default {
@@ -32,7 +32,7 @@ export default {
component: TestedComponent,
};
-export const OnePendingTransaction = createExample(TestedComponent, {
+export const OnePendingTransaction = tests.createExample(TestedComponent, {
transactions: [
{
amountEffective: "USD:10",
@@ -42,7 +42,7 @@ export const OnePendingTransaction = createExample(TestedComponent, {
],
});
-export const ThreePendingTransactions = createExample(TestedComponent, {
+export const ThreePendingTransactions = tests.createExample(TestedComponent, {
transactions: [
{
amountEffective: "USD:10",
@@ -62,7 +62,7 @@ export const ThreePendingTransactions = createExample(TestedComponent, {
],
});
-export const TenPendingTransactions = createExample(TestedComponent, {
+export const TenPendingTransactions = tests.createExample(TestedComponent, {
transactions: [
{
amountEffective: "USD:10",
diff --git a/packages/taler-wallet-webextension/src/components/PendingTransactions.tsx b/packages/taler-wallet-webextension/src/components/PendingTransactions.tsx
index 5d5dae092..c94010ede 100644
--- a/packages/taler-wallet-webextension/src/components/PendingTransactions.tsx
+++ b/packages/taler-wallet-webextension/src/components/PendingTransactions.tsx
@@ -18,45 +18,63 @@ import {
Amounts,
NotificationType,
Transaction,
+ TransactionMajorState,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { Fragment, h, JSX, VNode } from "preact";
import { useEffect } from "preact/hooks";
-import { useTranslationContext } from "../context/translation.js";
+import { useBackendContext } from "../context/backend.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
import { Avatar } from "../mui/Avatar.js";
+import { Grid } from "../mui/Grid.js";
import { Typography } from "../mui/Typography.js";
-import { wxApi } from "../wxApi.js";
import Banner from "./Banner.js";
import { Time } from "./Time.js";
interface Props extends JSX.HTMLAttributes {
- goToTransaction: (id: string) => Promise<void>;
+ goToTransaction?: (id: string) => Promise<void>;
+ goToURL: (url: string) => void;
}
-export function PendingTransactions({ goToTransaction }: Props): VNode {
+/**
+ * this cache will save the tx from the previous render
+ */
+const cache = { tx: [] as Transaction[] };
+
+export function PendingTransactions({
+ goToTransaction,
+ goToURL,
+}: Props): VNode {
+ const api = useBackendContext();
const state = useAsyncAsHook(() =>
- wxApi.wallet.call(WalletApiOperation.GetTransactions, {}),
+ api.wallet.call(WalletApiOperation.GetTransactions, {}),
);
useEffect(() => {
- return wxApi.listener.onUpdateNotification(
- [NotificationType.WithdrawGroupFinished],
+ return api.listener.onUpdateNotification(
+ [NotificationType.TransactionStateTransition],
state?.retry,
);
});
const transactions =
!state || state.hasError
- ? []
- : state.response.transactions.filter((t) => t.pending);
+ ? cache.tx
+ : state.response.transactions.filter(
+ (t) => t.txState.major === TransactionMajorState.Pending,
+ );
- if (!state || state.hasError || !transactions.length) {
+ if (state && !state.hasError) {
+ cache.tx = transactions;
+ }
+ if (!transactions.length) {
return <Fragment />;
}
return (
<PendingTransactionsView
goToTransaction={goToTransaction}
+ goToURL={goToURL}
transactions={transactions}
/>
);
@@ -65,52 +83,122 @@ export function PendingTransactions({ goToTransaction }: Props): VNode {
export function PendingTransactionsView({
transactions,
goToTransaction,
+ goToURL,
}: {
- goToTransaction: (id: string) => Promise<void>;
+ goToTransaction?: (id: string) => Promise<void>;
+ goToURL: (id: string) => void;
transactions: Transaction[];
}): VNode {
const { i18n } = useTranslationContext();
+ const kycTransaction = transactions.find((tx) => tx.kycUrl);
+ if (kycTransaction) {
+ return (
+ <div
+ style={{
+ backgroundColor: "#fff3cd",
+ color: "#664d03",
+ display: "flex",
+ justifyContent: "center",
+ }}
+ >
+ <Banner
+ titleHead={i18n.str`KYC requirement`}
+ style={{
+ backgroundColor: "lightred",
+ maxHeight: 150,
+ padding: 8,
+ flexGrow: 1, //#fff3cd //#ffecb5
+ maxWidth: 500,
+ overflowY: transactions.length > 3 ? "scroll" : "hidden",
+ }}
+ >
+ <Grid
+ container
+ item
+ xs={1}
+ wrap="nowrap"
+ role="button"
+ spacing={1}
+ alignItems="center"
+ onClick={() => {
+ goToURL(kycTransaction.kycUrl ?? "#");
+ }}
+ >
+ <Grid item>
+ <Typography inline bold>
+ One or more transaction require a KYC step to complete
+ </Typography>
+ </Grid>
+ </Grid>
+ </Banner>
+ </div>
+ );
+ }
+
+ if (!goToTransaction) return <Fragment />;
+
return (
- <Banner
- titleHead={<i18n.Translate>PENDING OPERATIONS</i18n.Translate>}
+ <div
style={{
backgroundColor: "lightcyan",
- maxHeight: 150,
- padding: 8,
- flexGrow: 1,
- maxWidth: 500,
- overflowY: transactions.length > 3 ? "scroll" : "hidden",
+ display: "flex",
+ justifyContent: "center",
}}
- elements={transactions.map((t) => {
- const amount = Amounts.parseOrThrow(t.amountEffective);
- return {
- icon: (
- <Avatar
- style={{
- border: "solid blue 1px",
- color: "blue",
- boxSizing: "border-box",
+ >
+ <Banner
+ titleHead={i18n.str`PENDING OPERATIONS`}
+ style={{
+ backgroundColor: "lightcyan",
+ maxHeight: 150,
+ padding: 8,
+ flexGrow: 1,
+ maxWidth: 500,
+ overflowY: transactions.length > 3 ? "scroll" : "hidden",
+ }}
+ >
+ {transactions.map((t, i) => {
+ const amount = Amounts.parseOrThrow(t.amountEffective);
+ return (
+ <Grid
+ container
+ item
+ xs={1}
+ key={i}
+ wrap="nowrap"
+ role="button"
+ spacing={1}
+ alignItems="center"
+ onClick={() => {
+ goToTransaction(t.transactionId);
}}
>
- {t.type.substring(0, 1)}
- </Avatar>
- ),
- action: () => goToTransaction(t.transactionId),
- description: (
- <Fragment>
- <Typography inline bold>
- {amount.currency} {Amounts.stringifyValue(amount)}
- </Typography>
- &nbsp;-&nbsp;
- <Time
- timestamp={AbsoluteTime.fromTimestamp(t.timestamp)}
- format="dd MMMM yyyy"
- />
- </Fragment>
- ),
- };
- })}
- />
+ <Grid item xs={"auto"}>
+ <Avatar
+ style={{
+ border: "solid blue 1px",
+ color: "blue",
+ boxSizing: "border-box",
+ }}
+ >
+ {t.type.substring(0, 1)}
+ </Avatar>
+ </Grid>
+
+ <Grid item>
+ <Typography inline bold>
+ {amount.currency} {Amounts.stringifyValue(amount)}
+ </Typography>
+ &nbsp;-&nbsp;
+ <Time
+ timestamp={AbsoluteTime.fromPreciseTimestamp(t.timestamp)}
+ format="dd MMMM yyyy"
+ />
+ </Grid>
+ </Grid>
+ );
+ })}
+ </Banner>
+ </div>
);
}
diff --git a/packages/taler-wallet-webextension/src/components/ProductList.tsx b/packages/taler-wallet-webextension/src/components/ProductList.tsx
new file mode 100644
index 000000000..748935dff
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/components/ProductList.tsx
@@ -0,0 +1,89 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { Amounts, Product } from "@gnu-taler/taler-util";
+import { Fragment, h, VNode } from "preact";
+import { SmallLightText } from "./styled/index.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+
+export function ProductList({ products }: { products: Product[] }): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <Fragment>
+ <SmallLightText style={{ margin: ".5em" }}>
+ <i18n.Translate>List of products</i18n.Translate>
+ </SmallLightText>
+ <dl>
+ {products.map((p, i) => {
+ if (p.price) {
+ const pPrice = Amounts.parseOrThrow(p.price);
+ return (
+ <div key={i} style={{ display: "flex", textAlign: "left" }}>
+ <div>
+ <img
+ src={p.image ? p.image : undefined}
+ style={{ width: 32, height: 32 }}
+ />
+ </div>
+ <div>
+ <dt>
+ {p.quantity ?? 1} x {p.description}{" "}
+ <span style={{ color: "gray" }}>
+ {Amounts.stringify(pPrice)}
+ </span>
+ </dt>
+ <dd>
+ <b>
+ {Amounts.stringify(
+ Amounts.mult(pPrice, p.quantity ?? 1).amount,
+ )}
+ </b>
+ </dd>
+ </div>
+ </div>
+ );
+ }
+ return (
+ <div key={i} style={{ display: "flex", textAlign: "left" }}>
+ <div>
+ <img src={p.image} style={{ width: 32, height: 32 }} />
+ </div>
+ <div>
+ <dt>
+ {p.quantity ?? 1} x {p.description}
+ </dt>
+ <dd>
+ <i18n.Translate>Total</i18n.Translate>
+ {` `}
+ {p.price ? (
+ `${Amounts.stringifyValue(
+ Amounts.mult(
+ Amounts.parseOrThrow(p.price),
+ p.quantity ?? 1,
+ ).amount,
+ )} ${p}`
+ ) : (
+ <i18n.Translate>free</i18n.Translate>
+ )}
+ </dd>
+ </div>
+ </div>
+ );
+ })}
+ </dl>
+ </Fragment>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/components/QR.stories.tsx b/packages/taler-wallet-webextension/src/components/QR.stories.tsx
index 83365670e..1d1f15b69 100644
--- a/packages/taler-wallet-webextension/src/components/QR.stories.tsx
+++ b/packages/taler-wallet-webextension/src/components/QR.stories.tsx
@@ -19,13 +19,13 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { createExample } from "../test-utils.js";
+import * as tests from "@gnu-taler/web-util/testing";
import { QR } from "./QR.js";
export default {
title: "qr",
};
-export const Restore = createExample(QR, {
+export const Restore = tests.createExample(QR, {
text: "taler://restore/6J0RZTJC6AV21WXK87BTE67WTHE9P2QSHF2BZXTP7PDZY2ARYBPG@sync1.demo.taler.net,sync2.demo.taler.net,sync1.demo.taler.net,sync3.demo.taler.net",
});
diff --git a/packages/taler-wallet-webextension/src/components/SelectList.tsx b/packages/taler-wallet-webextension/src/components/SelectList.tsx
index 3ceac752e..6eb72a266 100644
--- a/packages/taler-wallet-webextension/src/components/SelectList.tsx
+++ b/packages/taler-wallet-webextension/src/components/SelectList.tsx
@@ -14,14 +14,15 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+import { TranslatedString } from "@gnu-taler/taler-util";
import { Fragment, h, VNode } from "preact";
-import { useTranslationContext } from "../context/translation.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { NiceSelect } from "./styled/index.js";
interface Props {
value?: string;
onChange?: (s: string) => void;
- label: VNode;
+ label: VNode | TranslatedString;
list: {
[label: string]: string;
};
diff --git a/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.stories.tsx b/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.stories.tsx
index 841583113..0e23d5850 100644
--- a/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.stories.tsx
+++ b/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.stories.tsx
@@ -18,22 +18,21 @@
*
* @author Sebastian Javier Marchano (sebasjm)
*/
-
-import { WalletContractData } from "@gnu-taler/taler-wallet-core";
-import { createExample } from "../test-utils.js";
+import * as tests from "@gnu-taler/web-util/testing";
import {
ErrorView,
HiddenView,
LoadingView,
ShowView,
} from "./ShowFullContractTermPopup.js";
+import { AmountString, WalletContractData } from "@gnu-taler/taler-util";
export default {
title: "ShowFullContractTermPopup",
};
const cd: WalletContractData = {
- amount: "ARS:2",
+ amount: "ARS:2" as AmountString,
contractTermsHash:
"92X0KSJPZ8XS2XECCGFWTCGW8XMFCXTT2S6WHZDP6H9Y3TSKMTHY94WXEWDERTNN5XWCYGW4VN5CF2D4846HXTW7P06J4CZMHCWKC9G",
fulfillmentUrl: "",
@@ -43,20 +42,12 @@ const cd: WalletContractData = {
"0YA1WETV15R6K8QKS79QA3QMT16010F42Q49VSKYQ71HVQKAG0A4ZJCA4YTKHE9EA5SP156TJSKZEJJJ87305N6PS80PC48RNKYZE08",
orderId: "2022.220-0281XKKB8W7YE",
summary: "w",
- maxWireFee: "ARS:1",
payDeadline: {
t_s: 1660002673,
},
refundDeadline: {
t_s: 1660002673,
},
- wireFeeAmortization: 1,
- allowedAuditors: [
- {
- auditorBaseUrl: "https://auditor.taler.ar/",
- auditorPub: "0000000000000000000000000000000000000000000000000000",
- },
- ],
allowedExchanges: [
{
exchangeBaseUrl: "https://exchange.taler.ar/",
@@ -69,7 +60,7 @@ const cd: WalletContractData = {
wireMethod: "x-taler-bank",
wireInfoHash:
"QDT28374ZHYJ59WQFZ3TW1D5WKJVDYHQT86VHED3TNMB15ANJSKXDYPPNX01348KDYCX6T4WXA5A8FJJ8YWNEB1JW726C1JPKHM89DR",
- maxDepositFee: "ARS:1",
+ maxDepositFee: "ARS:1" as AmountString,
merchant: {
name: "Default",
address: {
@@ -79,26 +70,29 @@ const cd: WalletContractData = {
country: "ar",
},
},
- products: [],
+ // products: [],
autoRefund: undefined,
summaryI18n: undefined,
- deliveryDate: undefined,
- deliveryLocation: undefined,
+ // deliveryDate: undefined,
+ // deliveryLocation: undefined,
};
-export const ShowingSimpleOrder = createExample(ShowView, {
+export const ShowingSimpleOrder = tests.createExample(ShowView, {
contractTerms: cd,
});
-export const Error = createExample(ErrorView, {
- proposalId: "asd",
+export const Error = tests.createExample(ErrorView, {
+ transactionId: "asd",
error: {
hasError: true,
message: "message",
- operational: false,
+ // details: {
+ // co
+ // },
+ type: "error",
// details: {
// code: 123,
// },
},
});
-export const Loading = createExample(LoadingView, {});
-export const Hidden = createExample(HiddenView, {});
+export const Loading = tests.createExample(LoadingView, {});
+export const Hidden = tests.createExample(HiddenView, {});
diff --git a/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.tsx b/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.tsx
index 6461f76e3..e655def39 100644
--- a/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.tsx
+++ b/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.tsx
@@ -13,24 +13,28 @@
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { AbsoluteTime, Duration, Location } from "@gnu-taler/taler-util";
import {
- WalletApiOperation,
+ AbsoluteTime,
+ Duration,
+ Location,
+ TransactionIdStr,
WalletContractData,
-} from "@gnu-taler/taler-wallet-core";
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { styled } from "@linaria/react";
import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
import { Loading } from "../components/Loading.js";
-import { LoadingError } from "../components/LoadingError.js";
import { Modal } from "../components/Modal.js";
import { Time } from "../components/Time.js";
-import { useTranslationContext } from "../context/translation.js";
+import { alertFromError, useAlertContext } from "../context/alert.js";
+import { useBackendContext } from "../context/backend.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { HookError, useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
import { ButtonHandler } from "../mui/handlers.js";
import { compose, StateViewMap } from "../utils/index.js";
-import { wxApi } from "../wxApi.js";
import { Amount } from "./Amount.js";
+import { ErrorAlertView } from "./CurrentAlerts.js";
import { Link } from "./styled/index.js";
const ContractTermsTable = styled.table`
@@ -41,6 +45,7 @@ const ContractTermsTable = styled.table`
}
& > tr > td:nth-child(2n) {
text-align: right;
+ overflow-wrap: anywhere;
}
& > tr:nth-child(2n) {
background: #ebebeb;
@@ -72,14 +77,14 @@ function locationAsText(l: Location | undefined): VNode {
type State = States.Loading | States.Error | States.Hidden | States.Show;
-namespace States {
+export namespace States {
export interface Loading {
status: "loading";
hideHandler: ButtonHandler;
}
export interface Error {
status: "error";
- proposalId: string;
+ transactionId: string;
error: HookError;
hideHandler: ButtonHandler;
}
@@ -95,23 +100,25 @@ namespace States {
}
interface Props {
- proposalId: string;
+ transactionId: TransactionIdStr;
}
-function useComponentState({ proposalId }: Props, api: typeof wxApi): State {
+function useComponentState({ transactionId }: Props): State {
+ const api = useBackendContext();
const [show, setShow] = useState(false);
+ const { pushAlertOnError } = useAlertContext();
const hook = useAsyncAsHook(async () => {
if (!show) return undefined;
return await api.wallet.call(WalletApiOperation.GetContractTermsDetails, {
- proposalId,
+ transactionId,
});
}, [show]);
const hideHandler = {
- onClick: async () => setShow(false),
+ onClick: pushAlertOnError(async () => setShow(false)),
};
const showHandler = {
- onClick: async () => setShow(true),
+ onClick: pushAlertOnError(async () => setShow(true)),
};
if (!show) {
return {
@@ -121,7 +128,7 @@ function useComponentState({ proposalId }: Props, api: typeof wxApi): State {
}
if (!hook) return { status: "loading", hideHandler };
if (hook.hasError)
- return { status: "error", proposalId, error: hook, hideHandler };
+ return { status: "error", transactionId, error: hook, hideHandler };
if (!hook.response) return { status: "loading", hideHandler };
return {
status: "show",
@@ -139,7 +146,7 @@ const viewMapping: StateViewMap<State> = {
export const ShowFullContractTermPopup = compose(
"ShowFullContractTermPopup",
- (p: Props) => useComponentState(p, wxApi),
+ (p: Props) => useComponentState(p),
viewMapping,
);
@@ -154,18 +161,18 @@ export function LoadingView({ hideHandler }: States.Loading): VNode {
export function ErrorView({
hideHandler,
error,
- proposalId,
+ transactionId,
}: States.Error): VNode {
const { i18n } = useTranslationContext();
return (
<Modal title="Full detail" onClose={hideHandler}>
- <LoadingError
- title={
- <i18n.Translate>
- Could not load purchase proposal details
- </i18n.Translate>
- }
- error={error}
+ <ErrorAlertView
+ error={alertFromError(
+ i18n,
+ i18n.str`Could not load purchase proposal details`,
+ error,
+ { transactionId },
+ )}
/>
</Modal>
);
@@ -176,7 +183,7 @@ export function HiddenView({ showHandler }: States.Hidden): VNode {
}
export function ShowView({ contractTerms, hideHandler }: States.Show): VNode {
- const createdAt = AbsoluteTime.fromTimestamp(contractTerms.timestamp);
+ const createdAt = AbsoluteTime.fromProtocolTimestamp(contractTerms.timestamp);
const { i18n } = useTranslationContext();
return (
@@ -256,14 +263,14 @@ export function ShowView({ contractTerms, hideHandler }: States.Show): VNode {
</span>
</td>
</tr>
- <tr>
+ {/* <tr>
<td>
<i18n.Translate>Delivery date</i18n.Translate>
</td>
<td>
{contractTerms.deliveryDate && (
<Time
- timestamp={AbsoluteTime.fromTimestamp(
+ timestamp={AbsoluteTime.fromProtocolTimestamp(
contractTerms.deliveryDate,
)}
format="dd MMMM yyyy, HH:mm"
@@ -288,7 +295,7 @@ export function ShowView({ contractTerms, hideHandler }: States.Show): VNode {
.map((p) => `${p.description} x ${p.quantity}`)
.join(", ")}
</td>
- </tr>
+ </tr> */}
<tr>
<td>
<i18n.Translate>Created at</i18n.Translate>
@@ -296,7 +303,7 @@ export function ShowView({ contractTerms, hideHandler }: States.Show): VNode {
<td>
{contractTerms.timestamp && (
<Time
- timestamp={AbsoluteTime.fromTimestamp(
+ timestamp={AbsoluteTime.fromProtocolTimestamp(
contractTerms.timestamp,
)}
format="dd MMMM yyyy, HH:mm"
@@ -311,7 +318,7 @@ export function ShowView({ contractTerms, hideHandler }: States.Show): VNode {
<td>
{
<Time
- timestamp={AbsoluteTime.fromTimestamp(
+ timestamp={AbsoluteTime.fromProtocolTimestamp(
contractTerms.refundDeadline,
)}
format="dd MMMM yyyy, HH:mm"
@@ -331,8 +338,8 @@ export function ShowView({ contractTerms, hideHandler }: States.Show): VNode {
!contractTerms.autoRefund
? Duration.getZero()
: Duration.fromTalerProtocolDuration(
- contractTerms.autoRefund,
- ),
+ contractTerms.autoRefund,
+ ),
)}
format="dd MMMM yyyy, HH:mm"
/>
@@ -346,7 +353,7 @@ export function ShowView({ contractTerms, hideHandler }: States.Show): VNode {
<td>
{
<Time
- timestamp={AbsoluteTime.fromTimestamp(
+ timestamp={AbsoluteTime.fromProtocolTimestamp(
contractTerms.payDeadline,
)}
format="dd MMMM yyyy, HH:mm"
@@ -378,20 +385,6 @@ export function ShowView({ contractTerms, hideHandler }: States.Show): VNode {
<Amount value={contractTerms.maxDepositFee} />
</td>
</tr>
- <tr>
- <td>
- <i18n.Translate>Max fee</i18n.Translate>
- </td>
- <td>
- <Amount value={contractTerms.maxWireFee} />
- </td>
- </tr>
- <tr>
- <td>
- <i18n.Translate>Minimum age</i18n.Translate>
- </td>
- <td>{contractTerms.minimumAge}</td>
- </tr>
{/* <tr>
<td>Extra</td>
<td>
@@ -400,27 +393,6 @@ export function ShowView({ contractTerms, hideHandler }: States.Show): VNode {
</tr> */}
<tr>
<td>
- <i18n.Translate>Wire fee amortization</i18n.Translate>
- </td>
- <td>{contractTerms.wireFeeAmortization}</td>
- </tr>
- <tr>
- <td>
- <i18n.Translate>Auditors</i18n.Translate>
- </td>
- <td>
- {(contractTerms.allowedAuditors || []).map((e) => (
- <Fragment key={e.auditorPub}>
- <a href={e.auditorBaseUrl} title={e.auditorPub}>
- {e.auditorPub.substring(0, 6)}...
- </a>
- &nbsp;
- </Fragment>
- ))}
- </td>
- </tr>
- <tr>
- <td>
<i18n.Translate>Exchanges</i18n.Translate>
</td>
<td>
diff --git a/packages/taler-wallet-webextension/src/components/TermsOfService/index.ts b/packages/taler-wallet-webextension/src/components/TermsOfService/index.ts
index 4d2c4346e..1585e3992 100644
--- a/packages/taler-wallet-webextension/src/components/TermsOfService/index.ts
+++ b/packages/taler-wallet-webextension/src/components/TermsOfService/index.ts
@@ -14,16 +14,15 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+import { ComponentChildren } from "preact";
import { Loading } from "../../components/Loading.js";
-import { HookError } from "../../hooks/useAsyncAsHook.js";
-import { ToggleHandler } from "../../mui/handlers.js";
-import { compose, StateViewMap } from "../../utils/index.js";
-import { wxApi } from "../../wxApi.js";
+import { ErrorAlert } from "../../context/alert.js";
+import { SelectFieldHandler, ToggleHandler } from "../../mui/handlers.js";
+import { StateViewMap, compose } from "../../utils/index.js";
+import { ErrorAlertView } from "../CurrentAlerts.js";
import { useComponentState } from "./state.js";
import { TermsState } from "./utils.js";
import {
- ErrorAcceptingView,
- LoadingUriView,
ShowButtonsAcceptedTosView,
ShowButtonsNonAcceptedTosView,
ShowTosContentView,
@@ -31,13 +30,14 @@ import {
export interface Props {
exchangeUrl: string;
- onChange?: (v: boolean) => void;
+ readOnly?: boolean;
+ showEvenIfaccepted?: boolean;
+ children: ComponentChildren;
}
export type State =
| State.Loading
- | State.LoadingUriError
- | State.ErrorAccepting
+ | State.Error
| State.ShowButtonsAccepted
| State.ShowButtonsNotAccepted
| State.ShowContent;
@@ -48,14 +48,9 @@ export namespace State {
error: undefined;
}
- export interface LoadingUriError {
- status: "loading-error";
- error: HookError;
- }
-
- export interface ErrorAccepting {
- status: "error-accepting";
- error: HookError;
+ export interface Error {
+ status: "error";
+ error: ErrorAlert;
}
export interface BaseInfo {
@@ -64,13 +59,16 @@ export namespace State {
}
export interface ShowContent extends BaseInfo {
status: "show-content";
- termsAccepted?: ToggleHandler;
+ termsAccepted: ToggleHandler;
showingTermsOfService?: ToggleHandler;
+ tosLang: SelectFieldHandler;
+ tosFormat: SelectFieldHandler;
}
export interface ShowButtonsAccepted extends BaseInfo {
status: "show-buttons-accepted";
termsAccepted: ToggleHandler;
showingTermsOfService: ToggleHandler;
+ children: ComponentChildren,
}
export interface ShowButtonsNotAccepted extends BaseInfo {
status: "show-buttons-not-accepted";
@@ -80,15 +78,14 @@ export namespace State {
const viewMapping: StateViewMap<State> = {
loading: Loading,
- "loading-error": LoadingUriView,
+ error: ErrorAlertView,
"show-content": ShowTosContentView,
"show-buttons-accepted": ShowButtonsAcceptedTosView,
"show-buttons-not-accepted": ShowButtonsNonAcceptedTosView,
- "error-accepting": ErrorAcceptingView,
};
export const TermsOfService = compose(
"TermsOfService",
- (p: Props) => useComponentState(p, wxApi),
+ (p: Props) => useComponentState(p),
viewMapping,
);
diff --git a/packages/taler-wallet-webextension/src/components/TermsOfService/state.ts b/packages/taler-wallet-webextension/src/components/TermsOfService/state.ts
index c5be71ef0..76524f0f4 100644
--- a/packages/taler-wallet-webextension/src/components/TermsOfService/state.ts
+++ b/packages/taler-wallet-webextension/src/components/TermsOfService/state.ts
@@ -15,22 +15,32 @@
*/
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { useState } from "preact/hooks";
+import { alertFromError, useAlertContext } from "../../context/alert.js";
+import { useBackendContext } from "../../context/backend.js";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
-import { wxApi } from "../../wxApi.js";
import { Props, State } from "./index.js";
import { buildTermsOfServiceState } from "./utils.js";
-export function useComponentState(
- { exchangeUrl, onChange }: Props,
- api: typeof wxApi,
-): State {
- const readOnly = !onChange;
- const [showContent, setShowContent] = useState<boolean>(readOnly);
- const [errorAccepting, setErrorAccepting] = useState<Error | undefined>(
- undefined,
- );
+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 {
+ const api = useBackendContext();
+ const [showContent, setShowContent] = useState<boolean>(!!readOnly);
+ const { i18n, lang } = useTranslationContext();
+ const [tosLang, setTosLang] = useState<string>()
+ const { pushAlertOnError } = useAlertContext();
+
+ const [format, setFormat] = useState("text/html")
+ const acceptedLang = tosLang ?? lang
/**
* For the exchange selected, bring the status of the terms of service
*/
@@ -39,14 +49,20 @@ export function useComponentState(
WalletApiOperation.GetExchangeTos,
{
exchangeBaseUrl: exchangeUrl,
- acceptedFormat: ["text/xml"],
+ acceptedFormat: [format],
+ acceptLanguage: acceptedLang,
},
);
+ const supportedLangs = exchangeTos.tosAvailableLanguages.reduce((prev, cur) => {
+ prev[cur] = cur
+ return prev;
+ }, {} as Record<string, string>)
+
const state = buildTermsOfServiceState(exchangeTos);
- return { state };
- }, []);
+ return { state, supportedLangs };
+ }, [acceptedLang, format]);
if (!terms) {
return {
@@ -56,49 +72,27 @@ export function useComponentState(
}
if (terms.hasError) {
return {
- status: "loading-error",
- error: terms,
- };
- }
-
- if (errorAccepting) {
- return {
- status: "error-accepting",
- error: {
- hasError: true,
- operational: false,
- message: errorAccepting.message,
- },
+ status: "error",
+ error: alertFromError(
+ i18n,
+ i18n.str`Could not load the status of the term of service`,
+ terms,
+ ),
};
}
-
- const { state } = terms.response;
+ const { state, supportedLangs } = terms.response;
async function onUpdate(accepted: boolean): Promise<void> {
if (!state) return;
- try {
- if (accepted) {
- api.wallet.call(WalletApiOperation.SetExchangeTosAccepted, {
- exchangeBaseUrl: exchangeUrl,
- etag: state.version,
- });
- } else {
- // mark as not accepted
- api.wallet.call(WalletApiOperation.SetExchangeTosAccepted, {
- exchangeBaseUrl: exchangeUrl,
- etag: undefined,
- });
- }
- // setAccepted(accepted);
- if (!readOnly) onChange(accepted); //external update
- } catch (e) {
- if (e instanceof Error) {
- //FIXME: uncomment this and display error
- // setErrorAccepting(e.message);
- setErrorAccepting(e);
- }
+ if (accepted) {
+ await api.wallet.call(WalletApiOperation.SetExchangeTosAccepted, {
+ exchangeBaseUrl: exchangeUrl,
+ });
+ } else {
+ // mark as not accepted
}
+ terms?.retry()
}
const accepted = state.status === "accepted";
@@ -106,45 +100,61 @@ export function useComponentState(
const base = {
error: undefined,
showingTermsOfService: {
- value: showContent,
+ value: showContent && (!accepted || showEvenIfaccepted),
button: {
- onClick: async () => {
+ onClick: accepted && !showEvenIfaccepted ? undefined : pushAlertOnError(async () => {
setShowContent(!showContent);
- },
+ }),
},
},
terms: state,
termsAccepted: {
value: accepted,
button: {
- onClick: async () => {
+ onClick: readOnly ? undefined : pushAlertOnError(async () => {
const newValue = !accepted; //toggle
- onUpdate(newValue);
+ await onUpdate(newValue);
setShowContent(false);
- },
+ }),
},
},
};
- if (showContent) {
- return {
- status: "show-content",
- error: undefined,
- terms: state,
- showingTermsOfService: readOnly ? undefined : base.showingTermsOfService,
- termsAccepted: readOnly ? undefined : base.termsAccepted,
- };
- }
- //showing buttons
if (accepted) {
return {
status: "show-buttons-accepted",
...base,
+ children,
};
- } else {
+ }
+
+ if ((accepted && showEvenIfaccepted) || showContent) {
return {
- status: "show-buttons-not-accepted",
- ...base,
+ status: "show-content",
+ error: undefined,
+ terms: state,
+ showingTermsOfService: readOnly ? undefined : base.showingTermsOfService,
+ termsAccepted: base.termsAccepted,
+ tosFormat: {
+ onChange: pushAlertOnError(async (s) => {
+ setFormat(s)
+ }),
+ list: supportedFormats,
+ value: format ?? ""
+ },
+ tosLang: {
+ onChange: pushAlertOnError(async (s) => {
+ setTosLang(s)
+ }),
+ list: supportedLangs,
+ value: tosLang ?? lang
+ }
};
}
+ //showing buttons
+ return {
+ status: "show-buttons-not-accepted",
+ ...base,
+ };
+
}
diff --git a/packages/taler-wallet-webextension/src/components/TermsOfService/stories.tsx b/packages/taler-wallet-webextension/src/components/TermsOfService/stories.tsx
index 2479274cb..a28729eae 100644
--- a/packages/taler-wallet-webextension/src/components/TermsOfService/stories.tsx
+++ b/packages/taler-wallet-webextension/src/components/TermsOfService/stories.tsx
@@ -19,11 +19,41 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { createExample } from "../../test-utils.js";
-// import { ReadyView } from "./views.js";
+import * as tests from "@gnu-taler/web-util/testing";
+import { ShowTosContentView } from "./views.js";
+import { ExchangeTosStatus } from "@gnu-taler/taler-util";
export default {
title: "TermsOfService",
};
-// export const Ready = createExample(ReadyView, {});
+export const Ready = tests.createExample(ShowTosContentView, {
+ tosLang: {
+ list: {
+ es: "es",
+ en: "en",
+ },
+ value: "es",
+ onChange: (() => { }) as any
+ },
+ tosFormat: {
+ list: {
+ es: "es",
+ en: "en",
+ },
+ value: "es",
+ onChange: (() => { }) as any
+ },
+ terms: {
+ content: {
+ type: "plain",
+ content: "hola"
+ },
+ status: ExchangeTosStatus.Accepted,
+ version: "1"
+ },
+ status: "show-content",
+ termsAccepted: {
+ button: {},
+ }
+});
diff --git a/packages/taler-wallet-webextension/src/components/TermsOfService/utils.ts b/packages/taler-wallet-webextension/src/components/TermsOfService/utils.ts
index a106c3d85..96e268689 100644
--- a/packages/taler-wallet-webextension/src/components/TermsOfService/utils.ts
+++ b/packages/taler-wallet-webextension/src/components/TermsOfService/utils.ts
@@ -14,7 +14,11 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { ExchangeTosStatus, GetExchangeTosResult } from "@gnu-taler/taler-util";
+import {
+ ExchangeTosStatus,
+ GetExchangeTosResult,
+ Logger,
+} from "@gnu-taler/taler-util";
export function buildTermsOfServiceState(
tos: GetExchangeTosResult,
@@ -27,6 +31,8 @@ export function buildTermsOfServiceState(
return { content, status: tos.tosStatus, version: tos.currentEtag };
}
+const logger = new Logger("termsofservice");
+
function parseTermsOfServiceContent(
type: string,
text: string,
@@ -36,38 +42,31 @@ function parseTermsOfServiceContent(
const document = new DOMParser().parseFromString(text, "text/xml");
return { type: "xml", document };
} catch (e) {
- console.log(e);
+ logger.error("error parsing xml", e);
}
} else if (type === "text/html") {
try {
- const href = new URL(text);
- return { type: "html", href };
+ return { type: "html", html: text };
} catch (e) {
- console.log(e);
+ logger.error("error parsing url", e);
}
} else if (type === "text/json") {
try {
const data = JSON.parse(text);
return { type: "json", data };
} catch (e) {
- console.log(e);
+ logger.error("error parsing json", e);
}
} else if (type === "text/pdf") {
try {
const location = new URL(text);
return { type: "pdf", location };
} catch (e) {
- console.log(e);
- }
- } else if (type === "text/plain") {
- try {
- const content = text;
- return { type: "plain", content };
- } catch (e) {
- console.log(e);
+ logger.error("error parsing url", e);
}
}
- return undefined;
+ const content = text;
+ return { type: "plain", content };
}
export type TermsState = {
@@ -90,7 +89,7 @@ export interface TermsDocumentXml {
export interface TermsDocumentHtml {
type: "html";
- href: URL;
+ html: string;
}
export interface TermsDocumentPlain {
diff --git a/packages/taler-wallet-webextension/src/components/TermsOfService/views.tsx b/packages/taler-wallet-webextension/src/components/TermsOfService/views.tsx
index a7e03fd01..40cfba3bc 100644
--- a/packages/taler-wallet-webextension/src/components/TermsOfService/views.tsx
+++ b/packages/taler-wallet-webextension/src/components/TermsOfService/views.tsx
@@ -14,78 +14,58 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+import { ExchangeTosStatus } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, h, VNode } from "preact";
-import { LoadingError } from "../../components/LoadingError.js";
-import { useTranslationContext } from "../../context/translation.js";
-import { TermsDocument, TermsState } from "./utils.js";
-import { State } from "./index.js";
import { CheckboxOutlined } from "../../components/CheckboxOutlined.js";
+import { ExchangeXmlTos } from "../../components/ExchangeToS.js";
import {
+ Input,
LinkSuccess,
- TermsOfService,
- WarningBox,
- WarningText,
+ TermsOfServiceStyle,
+ WarningBox
} from "../../components/styled/index.js";
-import { ExchangeXmlTos } from "../../components/ExchangeToS.js";
-import { ToggleHandler } from "../../mui/handlers.js";
import { Button } from "../../mui/Button.js";
-import { ExchangeTosStatus } from "@gnu-taler/taler-util";
-
-export function LoadingUriView({ error }: State.LoadingUriError): VNode {
- const { i18n } = useTranslationContext();
-
- return (
- <LoadingError
- title={<i18n.Translate>Could not load</i18n.Translate>}
- error={error}
- />
- );
-}
-
-export function ErrorAcceptingView({ error }: State.ErrorAccepting): VNode {
- const { i18n } = useTranslationContext();
-
- return (
- <LoadingError
- title={<i18n.Translate>Could not load</i18n.Translate>}
- error={error}
- />
- );
-}
+import { State } from "./index.js";
+import { SelectList } from "../SelectList.js";
+import { EnabledBySettings } from "../EnabledBySettings.js";
export function ShowButtonsAcceptedTosView({
termsAccepted,
showingTermsOfService,
- terms,
+ children,
}: State.ShowButtonsAccepted): VNode {
const { i18n } = useTranslationContext();
- const ableToReviewTermsOfService =
- showingTermsOfService.button.onClick !== undefined;
return (
<Fragment>
- {ableToReviewTermsOfService && (
- <section style={{ justifyContent: "space-around", display: "flex" }}>
- <LinkSuccess
- upperCased
- onClick={showingTermsOfService.button.onClick}
- >
- <i18n.Translate>Show terms of service</i18n.Translate>
- </LinkSuccess>
- </section>
+ {showingTermsOfService.button.onClick !== undefined && (
+ <Fragment>
+ <section style={{ justifyContent: "space-around", display: "flex" }}>
+ <LinkSuccess
+ upperCased
+ onClick={showingTermsOfService.button.onClick}
+ >
+ <i18n.Translate>Show terms of service</i18n.Translate>
+ </LinkSuccess>
+ </section>
+ {termsAccepted.button.onClick !== undefined && (
+ <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>
+ )}
+ </Fragment>
)}
- <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>
+ {children}
</Fragment>
);
}
@@ -95,28 +75,28 @@ export function ShowButtonsNonAcceptedTosView({
terms,
}: State.ShowButtonsNotAccepted): VNode {
const { i18n } = useTranslationContext();
- const ableToReviewTermsOfService =
- showingTermsOfService.button.onClick !== undefined;
+ // const ableToReviewTermsOfService =
+ // showingTermsOfService.button.onClick !== undefined;
- if (!ableToReviewTermsOfService) {
- 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>
- )}
- </Fragment>
- );
- }
+ // 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 && (
+ {/* {terms.status === ExchangeTosStatus.NotFound && (
<section style={{ justifyContent: "space-around", display: "flex" }}>
<WarningText>
<i18n.Translate>
@@ -124,31 +104,16 @@ export function ShowButtonsNonAcceptedTosView({
</i18n.Translate>
</WarningText>
</section>
- )}
- {terms.status === "new" && (
- <section style={{ justifyContent: "space-around", display: "flex" }}>
- <Button
- variant="contained"
- color="success"
- onClick={showingTermsOfService.button.onClick}
- >
- <i18n.Translate>Review exchange terms of service</i18n.Translate>
- </Button>
- </section>
- )}
- {terms.status === "changed" && (
- <section style={{ justifyContent: "space-around", display: "flex" }}>
- <Button
- variant="contained"
- color="success"
- onClick={showingTermsOfService.button.onClick}
- >
- <i18n.Translate>
- Review new version of terms of service
- </i18n.Translate>
- </Button>
- </section>
- )}
+ )} */}
+ <section style={{ justifyContent: "space-around", display: "flex" }}>
+ <Button
+ variant="contained"
+ color="success"
+ onClick={showingTermsOfService.button.onClick}
+ >
+ <i18n.Translate>Review exchange terms of service</i18n.Translate>
+ </Button>
+ </section>
</Fragment>
);
}
@@ -157,36 +122,72 @@ export function ShowTosContentView({
termsAccepted,
showingTermsOfService,
terms,
+ tosLang,
+ tosFormat,
}: State.ShowContent): VNode {
const { i18n } = useTranslationContext();
const ableToReviewTermsOfService =
- showingTermsOfService?.button.onClick !== undefined;
+ termsAccepted.button.onClick !== undefined;
return (
- <Fragment>
- {terms.status !== ExchangeTosStatus.NotFound && !terms.content && (
+ <section>
+ <Input style={{ display: "flex", justifyContent: "end" }}>
+ <EnabledBySettings name="selectTosFormat">
+ <SelectList
+ label={i18n.str`Format`}
+ list={tosFormat.list}
+ name="format"
+ value={tosFormat.value}
+ onChange={tosFormat.onChange}
+ />
+ </EnabledBySettings>
+ <SelectList
+ label={i18n.str`Language`}
+ list={tosLang.list}
+ name="lang"
+ value={tosLang.value}
+ onChange={tosLang.onChange}
+ />
+ </Input>
+
+ {!terms.content && (
<section style={{ justifyContent: "space-around", display: "flex" }}>
<WarningBox>
<i18n.Translate>
- The exchange reply with a empty terms of service
+ The exchange replied with a empty terms of service
</i18n.Translate>
</WarningBox>
</section>
)}
{terms.content && (
<section style={{ justifyContent: "space-around", display: "flex" }}>
- {terms.content.type === "xml" && (
- <TermsOfService>
- <ExchangeXmlTos doc={terms.content.document} />
- </TermsOfService>
- )}
- {terms.content.type === "plain" && (
- <div style={{ textAlign: "left" }}>
- <pre>{terms.content.content}</pre>
- </div>
- )}
+ {terms.content.type === "xml" &&
+ (!terms.content.document ? (
+ <WarningBox>
+ <i18n.Translate>
+ No terms of service. The exchange replied with a empty
+ document
+ </i18n.Translate>
+ </WarningBox>
+ ) : (
+ <TermsOfServiceStyle>
+ <ExchangeXmlTos doc={terms.content.document} />
+ </TermsOfServiceStyle>
+ ))}
+ {terms.content.type === "plain" &&
+ (!terms.content.content ? (
+ <WarningBox>
+ <i18n.Translate>
+ No terms of service. The exchange replied with a empty text
+ </i18n.Translate>
+ </WarningBox>
+ ) : (
+ <div style={{ textAlign: "left" }}>
+ <pre>{terms.content.content}</pre>
+ </div>
+ ))}
{terms.content.type === "html" && (
- <iframe src={terms.content.href.toString()} />
+ <iframe style={{ width: "100%" }} srcDoc={terms.content.html} />
)}
{terms.content.type === "pdf" && (
<a href={terms.content.location.toString()} download="tos.pdf">
@@ -205,7 +206,7 @@ export function ShowTosContentView({
</LinkSuccess>
</section>
)}
- {termsAccepted && terms.status !== ExchangeTosStatus.NotFound && (
+ {termsAccepted.button.onClick && terms.status !== ExchangeTosStatus.Accepted && (
<section style={{ justifyContent: "space-around", display: "flex" }}>
<CheckboxOutlined
name="terms"
@@ -219,6 +220,6 @@ export function ShowTosContentView({
/>
</section>
)}
- </Fragment>
+ </section>
);
}
diff --git a/packages/taler-wallet-webextension/src/components/Time.tsx b/packages/taler-wallet-webextension/src/components/Time.tsx
index 7ec91d56c..eee295756 100644
--- a/packages/taler-wallet-webextension/src/components/Time.tsx
+++ b/packages/taler-wallet-webextension/src/components/Time.tsx
@@ -18,6 +18,11 @@ import { AbsoluteTime } from "@gnu-taler/taler-util";
import { formatISO, format } from "date-fns";
import { h, VNode } from "preact";
+/**
+ *
+ * @deprecated use web-util
+ * @returns
+ */
export function Time({
timestamp,
format: formatString,
diff --git a/packages/taler-wallet-webextension/src/components/TransactionItem.tsx b/packages/taler-wallet-webextension/src/components/TransactionItem.tsx
deleted file mode 100644
index c2c4b52e3..000000000
--- a/packages/taler-wallet-webextension/src/components/TransactionItem.tsx
+++ /dev/null
@@ -1,284 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-import {
- AmountJson,
- Amounts,
- AmountString,
- AbsoluteTime,
- Transaction,
- TransactionType,
- WithdrawalType,
-} from "@gnu-taler/taler-util";
-import { h, VNode } from "preact";
-import { useTranslationContext } from "../context/translation.js";
-import { Avatar } from "../mui/Avatar.js";
-import { Pages } from "../NavigationBar.js";
-import { assertUnreachable } from "../utils/index.js";
-import {
- Column,
- ExtraLargeText,
- HistoryRow,
- LargeText,
- LightText,
- SmallLightText,
-} from "./styled/index.js";
-import { Time } from "./Time.js";
-
-export function TransactionItem(props: { tx: Transaction }): VNode {
- const tx = props.tx;
- const { i18n } = useTranslationContext();
- switch (tx.type) {
- case TransactionType.Withdrawal:
- return (
- <TransactionLayout
- id={tx.transactionId}
- amount={tx.amountEffective}
- debitCreditIndicator={"credit"}
- title={new URL(tx.exchangeBaseUrl).hostname}
- timestamp={AbsoluteTime.fromTimestamp(tx.timestamp)}
- iconPath={"W"}
- pending={
- tx.pending
- ? tx.withdrawalDetails.type ===
- WithdrawalType.TalerBankIntegrationApi
- ? !tx.withdrawalDetails.confirmed
- ? i18n.str`Need approval in the Bank`
- : i18n.str`Exchange is waiting the wire transfer`
- : tx.withdrawalDetails.type === WithdrawalType.ManualTransfer
- ? i18n.str`Exchange is waiting the wire transfer`
- : "" //pending but no message
- : undefined
- }
- />
- );
- case TransactionType.Payment:
- return (
- <TransactionLayout
- id={tx.transactionId}
- amount={tx.amountEffective}
- debitCreditIndicator={"debit"}
- title={tx.info.merchant.name}
- subtitle={tx.info.summary}
- timestamp={AbsoluteTime.fromTimestamp(tx.timestamp)}
- iconPath={"P"}
- // pending={tx.pending}
- />
- );
- case TransactionType.Refund:
- return (
- <TransactionLayout
- id={tx.transactionId}
- amount={tx.amountEffective}
- debitCreditIndicator={"credit"}
- subtitle={tx.info.summary}
- title={tx.info.merchant.name}
- timestamp={AbsoluteTime.fromTimestamp(tx.timestamp)}
- iconPath={"R"}
- // pending={tx.pending}
- />
- );
- case TransactionType.Tip:
- return (
- <TransactionLayout
- id={tx.transactionId}
- amount={tx.amountEffective}
- debitCreditIndicator={"credit"}
- title={new URL(tx.merchantBaseUrl).hostname}
- timestamp={AbsoluteTime.fromTimestamp(tx.timestamp)}
- iconPath={"T"}
- // pending={tx.pending}
- />
- );
- case TransactionType.Refresh:
- return (
- <TransactionLayout
- id={tx.transactionId}
- amount={tx.amountEffective}
- debitCreditIndicator={"credit"}
- title={new URL(tx.exchangeBaseUrl).hostname}
- timestamp={AbsoluteTime.fromTimestamp(tx.timestamp)}
- iconPath={"R"}
- // pending={tx.pending}
- />
- );
- case TransactionType.Deposit:
- return (
- <TransactionLayout
- id={tx.transactionId}
- amount={tx.amountEffective}
- debitCreditIndicator={"debit"}
- title={tx.targetPaytoUri}
- timestamp={AbsoluteTime.fromTimestamp(tx.timestamp)}
- iconPath={"D"}
- // pending={tx.pending}
- />
- );
- case TransactionType.PeerPullCredit:
- return (
- <TransactionLayout
- id={tx.transactionId}
- amount={tx.amountEffective}
- debitCreditIndicator={"credit"}
- title={tx.info.summary || "Invoice"}
- timestamp={AbsoluteTime.fromTimestamp(tx.timestamp)}
- iconPath={"I"}
- // pending={tx.pending}
- />
- );
- case TransactionType.PeerPullDebit:
- return (
- <TransactionLayout
- id={tx.transactionId}
- amount={tx.amountEffective}
- debitCreditIndicator={"debit"}
- title={tx.info.summary || "Invoice"}
- timestamp={AbsoluteTime.fromTimestamp(tx.timestamp)}
- iconPath={"I"}
- // pending={tx.pending}
- />
- );
- case TransactionType.PeerPushCredit:
- return (
- <TransactionLayout
- id={tx.transactionId}
- amount={tx.amountEffective}
- debitCreditIndicator={"credit"}
- title={tx.info.summary || "Transfer"}
- timestamp={AbsoluteTime.fromTimestamp(tx.timestamp)}
- iconPath={"T"}
- // pending={tx.pending}
- />
- );
- case TransactionType.PeerPushDebit:
- return (
- <TransactionLayout
- id={tx.transactionId}
- amount={tx.amountEffective}
- debitCreditIndicator={"debit"}
- title={tx.info.summary || "Transfer"}
- timestamp={AbsoluteTime.fromTimestamp(tx.timestamp)}
- iconPath={"T"}
- // pending={tx.pending}
- />
- );
- default: {
- assertUnreachable(tx);
- }
- }
-}
-
-function TransactionLayout(props: TransactionLayoutProps): VNode {
- const { i18n } = useTranslationContext();
- return (
- <HistoryRow
- href={Pages.balanceTransaction({ tid: props.id })}
- style={{
- backgroundColor: props.pending ? "lightcyan" : "inherit",
- alignItems: "center",
- }}
- >
- <Avatar
- style={{
- border: "solid gray 1px",
- color: "gray",
- boxSizing: "border-box",
- }}
- >
- {props.iconPath}
- </Avatar>
- <Column>
- <LargeText>
- <div>{props.title}</div>
- {props.subtitle && (
- <div style={{ color: "gray", fontSize: "medium", marginTop: 5 }}>
- {props.subtitle}
- </div>
- )}
- </LargeText>
- {props.pending && (
- <LightText style={{ marginTop: 5, marginBottom: 5 }}>
- <i18n.Translate>{props.pending}</i18n.Translate>
- </LightText>
- )}
- <SmallLightText style={{ marginTop: 5 }}>
- <Time timestamp={props.timestamp} format="HH:mm" />
- </SmallLightText>
- </Column>
- <TransactionAmount
- pending={props.pending !== undefined}
- amount={Amounts.parseOrThrow(props.amount)}
- debitCreditIndicator={props.debitCreditIndicator}
- />
- </HistoryRow>
- );
-}
-
-interface TransactionLayoutProps {
- debitCreditIndicator: "debit" | "credit" | "unknown";
- amount: AmountString | "unknown";
- timestamp: AbsoluteTime;
- title: string;
- subtitle?: string;
- id: string;
- iconPath: string;
- pending?: string;
-}
-
-interface TransactionAmountProps {
- debitCreditIndicator: "debit" | "credit" | "unknown";
- amount: AmountJson;
- pending: boolean;
-}
-
-function TransactionAmount(props: TransactionAmountProps): VNode {
- const { i18n } = useTranslationContext();
- let sign: string;
- switch (props.debitCreditIndicator) {
- case "credit":
- sign = "+";
- break;
- case "debit":
- sign = "-";
- break;
- case "unknown":
- sign = "";
- }
- return (
- <Column
- style={{
- textAlign: "center",
- color: props.pending
- ? "gray"
- : sign === "+"
- ? "darkgreen"
- : sign === "-"
- ? "darkred"
- : undefined,
- }}
- >
- <ExtraLargeText>
- {sign}
- {Amounts.stringifyValue(props.amount, 2)}
- </ExtraLargeText>
- {props.pending && (
- <div>
- <i18n.Translate>PENDING</i18n.Translate>
- </div>
- )}
- </Column>
- );
-}
diff --git a/packages/taler-wallet-webextension/src/components/WalletActivity.tsx b/packages/taler-wallet-webextension/src/components/WalletActivity.tsx
new file mode 100644
index 000000000..69a2c0675
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/components/WalletActivity.tsx
@@ -0,0 +1,1100 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ AbsoluteTime,
+ NotificationType,
+ ObservabilityEventType,
+ RequestProgressNotification,
+ TalerErrorCode,
+ TalerErrorDetail,
+ TaskProgressNotification,
+ WalletNotification,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, JSX, VNode, h } from "preact";
+import { useEffect, useState } from "preact/hooks";
+import { Pages } from "../NavigationBar.js";
+import { useBackendContext } from "../context/backend.js";
+import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
+import { useSettings } from "../hooks/useSettings.js";
+import { Button } from "../mui/Button.js";
+import { WxApiType } from "../wxApi.js";
+import { Modal } from "./Modal.js";
+import { Time } from "./Time.js";
+
+interface Props extends JSX.HTMLAttributes {}
+
+export function WalletActivity({}: Props): VNode {
+ const { i18n } = useTranslationContext();
+ const [settings, updateSettings] = useSettings();
+ const api = useBackendContext();
+ useEffect(() => {
+ document.body.style.marginBottom = "250px";
+ return () => {
+ document.body.style.marginBottom = "0px";
+ };
+ });
+ const [table, setTable] = useState<"tasks" | "events">("tasks");
+ return (
+ <div
+ style={{
+ position: "fixed",
+ bottom: 0,
+ background: "white",
+ zIndex: 1,
+ height: 250,
+ overflowY: "scroll",
+ width: "100%",
+ }}
+ >
+ <div
+ style={{
+ display: "flex",
+ justifyContent: "space-between",
+ float: "right",
+ }}
+ >
+ <div />
+ <div>
+ <div
+ style={{ padding: 4, margin: 2, border: "solid 1px black" }}
+ onClick={() => {
+ updateSettings("showWalletActivity", false);
+ }}
+ >
+ close
+ </div>
+ </div>
+ </div>
+ <div style={{ display: "flex", justifyContent: "space-around" }}>
+ <Button
+ variant={table === "tasks" ? "contained" : "outlined"}
+ style={{ margin: 4 }}
+ onClick={async () => {
+ setTable("tasks");
+ }}
+ >
+ <i18n.Translate>Tasks</i18n.Translate>
+ </Button>
+ <Button
+ variant={table === "events" ? "contained" : "outlined"}
+ style={{ margin: 4 }}
+ onClick={async () => {
+ setTable("events");
+ }}
+ >
+ <i18n.Translate>Events</i18n.Translate>
+ </Button>
+ </div>
+ {(function (): VNode {
+ switch (table) {
+ case "events": {
+ return <ObservabilityEventsTable />;
+ }
+ case "tasks": {
+ return <ActiveTasksTable />;
+ }
+ default: {
+ assertUnreachable(table);
+ }
+ }
+ })()}
+ </div>
+ );
+}
+
+interface MoreInfoPRops {
+ events: (WalletNotification & { when: AbsoluteTime })[];
+ onClick: (content: VNode) => void;
+}
+type Notif = {
+ id: string;
+ events: (WalletNotification & { when: AbsoluteTime })[];
+ description: string;
+ start: AbsoluteTime;
+ end: AbsoluteTime;
+ reference:
+ | {
+ eventType: NotificationType;
+ referenceType: "task" | "transaction" | "operation" | "exchange";
+ id: string;
+ }
+ | undefined;
+ MoreInfo: (p: MoreInfoPRops) => VNode;
+};
+
+function ShowBalanceChange({ events }: MoreInfoPRops): VNode {
+ if (!events.length) return <Fragment />;
+ const not = events[0];
+ if (not.type !== NotificationType.BalanceChange) return <Fragment />;
+ return (
+ <Fragment>
+ <dt>Transaction</dt>
+ <dd>
+ <a
+ title={not.hintTransactionId}
+ href={Pages.balanceTransaction({ tid: not.hintTransactionId })}
+ >
+ {not.hintTransactionId.substring(0, 10)}
+ </a>
+ </dd>
+ </Fragment>
+ );
+}
+
+function ShowBackupOperationError({ events, onClick }: MoreInfoPRops): VNode {
+ if (!events.length) return <Fragment />;
+ const not = events[0];
+ if (not.type !== NotificationType.BackupOperationError) return <Fragment />;
+ return (
+ <Fragment>
+ <dt>Error</dt>
+ <dd>
+ <a
+ href="#"
+ onClick={(e) => {
+ e.preventDefault();
+ const error = not.error;
+ onClick(
+ <Fragment>
+ <dl>
+ <dt>Code</dt>
+ <dd>
+ {TalerErrorCode[error.code]} ({error.code})
+ </dd>
+ <dt>Hint</dt>
+ <dd>{error.hint ?? "--"}</dd>
+ <dt>Time</dt>
+ <dd>
+ <Time timestamp={error.when} format="yyyy/MM/dd HH:mm:ss" />
+ </dd>
+ </dl>
+ <pre
+ style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}
+ >
+ {JSON.stringify(error, undefined, 2)}
+ </pre>
+ </Fragment>,
+ );
+ }}
+ >
+ {TalerErrorCode[not.error.code]}
+ </a>
+ </dd>
+ </Fragment>
+ );
+}
+
+function ShowTransactionStateTransition({
+ events,
+ onClick,
+}: MoreInfoPRops): VNode {
+ if (!events.length) return <Fragment />;
+ const not = events[0];
+ if (not.type !== NotificationType.TransactionStateTransition)
+ return <Fragment />;
+ return (
+ <Fragment>
+ <dt>Old state</dt>
+ <dd>
+ {not.oldTxState.major} - {not.oldTxState.minor ?? ""}
+ </dd>
+ <dt>New state</dt>
+ <dd>
+ {not.newTxState.major} - {not.newTxState.minor ?? ""}
+ </dd>
+ <dt>Transaction</dt>
+ <dd>
+ <a
+ title={not.transactionId}
+ href={Pages.balanceTransaction({ tid: not.transactionId })}
+ >
+ {not.transactionId.substring(0, 10)}
+ </a>
+ </dd>
+ {not.errorInfo ? (
+ <Fragment>
+ <dt>Error</dt>
+ <dd>
+ <a
+ href="#"
+ onClick={(e) => {
+ if (!not.errorInfo) return;
+ e.preventDefault();
+ const error = not.errorInfo;
+ onClick(
+ <Fragment>
+ <dl>
+ <dt>Code</dt>
+ <dd>
+ {TalerErrorCode[error.code]} ({error.code})
+ </dd>
+ <dt>Hint</dt>
+ <dd>{error.hint ?? "--"}</dd>
+ <dt>Message</dt>
+ <dd>{error.message ?? "--"}</dd>
+ </dl>
+ </Fragment>,
+ );
+ }}
+ >
+ {TalerErrorCode[not.errorInfo.code]}
+ </a>
+ </dd>
+ </Fragment>
+ ) : undefined}
+ <dt>Experimental</dt>
+ <dd>
+ <pre style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}>
+ {JSON.stringify(not.experimentalUserData, undefined, 2)}
+ </pre>
+ </dd>
+ </Fragment>
+ );
+}
+function ShowExchangeStateTransition({
+ events,
+ onClick,
+}: MoreInfoPRops): VNode {
+ if (!events.length) return <Fragment />;
+ const not = events[0];
+ if (not.type !== NotificationType.ExchangeStateTransition)
+ return <Fragment />;
+ return (
+ <Fragment>
+ <dt>Exchange</dt>
+ <dd>{not.exchangeBaseUrl}</dd>
+ {not.oldExchangeState &&
+ not.newExchangeState.exchangeEntryStatus !==
+ not.oldExchangeState?.exchangeEntryStatus && (
+ <Fragment>
+ <dt>Entry status</dt>
+ <dd>
+ from {not.oldExchangeState.exchangeEntryStatus} to{" "}
+ {not.newExchangeState.exchangeEntryStatus}
+ </dd>
+ </Fragment>
+ )}
+ {not.oldExchangeState &&
+ not.newExchangeState.exchangeUpdateStatus !==
+ not.oldExchangeState?.exchangeUpdateStatus && (
+ <Fragment>
+ <dt>Update status</dt>
+ <dd>
+ from {not.oldExchangeState.exchangeUpdateStatus} to{" "}
+ {not.newExchangeState.exchangeUpdateStatus}
+ </dd>
+ </Fragment>
+ )}
+ {not.oldExchangeState &&
+ not.newExchangeState.tosStatus !== not.oldExchangeState?.tosStatus && (
+ <Fragment>
+ <dt>Tos status</dt>
+ <dd>
+ from {not.oldExchangeState.tosStatus} to{" "}
+ {not.newExchangeState.tosStatus}
+ </dd>
+ </Fragment>
+ )}
+ </Fragment>
+ );
+}
+
+type ObservaNotifWithTime = (
+ | TaskProgressNotification
+ | RequestProgressNotification
+) & {
+ when: AbsoluteTime;
+};
+function ShowObservabilityEvent({ events, onClick }: MoreInfoPRops): VNode {
+ // let prev: ObservaNotifWithTime;
+ const asd = events.map((not) => {
+ if (
+ not.type !== NotificationType.RequestObservabilityEvent &&
+ not.type !== NotificationType.TaskObservabilityEvent
+ )
+ return <Fragment />;
+
+ 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.DbQueryStart:
+ return "Database";
+ case ObservabilityEventType.RequestFinishSuccess:
+ case ObservabilityEventType.RequestFinishError:
+ case ObservabilityEventType.RequestStart:
+ return "Wallet";
+ case ObservabilityEventType.CryptoFinishSuccess:
+ case ObservabilityEventType.CryptoFinishError:
+ case ObservabilityEventType.CryptoStart:
+ return "Crypto";
+ case ObservabilityEventType.TaskStart:
+ return "Task start";
+ case ObservabilityEventType.TaskStop:
+ return "Task stop";
+ case ObservabilityEventType.TaskReset:
+ return "Task reset";
+ case ObservabilityEventType.ShepherdTaskResult:
+ return "Schedule";
+ case ObservabilityEventType.DeclareTaskDependency:
+ return "Task dependency";
+ case ObservabilityEventType.Message:
+ return "Message";
+ }
+ })();
+
+ return (
+ <ShowObervavilityDetails title={title} notif={not} onClick={onClick} />
+ );
+ });
+ return (
+ <table>
+ <thead>
+ <td>Event</td>
+ <td>Info</td>
+ <td>Start</td>
+ <td>End</td>
+ </thead>
+ <tbody>{asd}</tbody>
+ </table>
+ );
+}
+
+function ShowObervavilityDetails({
+ title,
+ notif,
+ onClick,
+ prev,
+}: {
+ title: string;
+ notif: ObservaNotifWithTime;
+ prev?: ObservaNotifWithTime;
+ onClick: (content: VNode) => void;
+}): VNode {
+ switch (notif.event.type) {
+ case ObservabilityEventType.HttpFetchStart:
+ case ObservabilityEventType.HttpFetchFinishError:
+ case ObservabilityEventType.HttpFetchFinishSuccess: {
+ return (
+ <tr>
+ <td>
+ <a
+ href="#"
+ onClick={(e) => {
+ e.preventDefault();
+ onClick(
+ <Fragment>
+ <pre
+ style={{
+ whiteSpace: "pre-wrap",
+ wordBreak: "break-word",
+ }}
+ >
+ {JSON.stringify({ event: notif, prev }, undefined, 2)}
+ </pre>
+ </Fragment>,
+ );
+ }}
+ >
+ {title}
+ </a>
+ </td>
+ <td>
+ {notif.event.url}{" "}
+ {prev?.event.type ===
+ ObservabilityEventType.HttpFetchFinishSuccess ? (
+ `(${prev.event.status})`
+ ) : prev?.event.type ===
+ ObservabilityEventType.HttpFetchFinishError ? (
+ <a
+ href="#"
+ onClick={(e) => {
+ e.preventDefault();
+ if (
+ prev.event.type !==
+ ObservabilityEventType.HttpFetchFinishError
+ )
+ return;
+ const error = prev.event.error;
+ onClick(
+ <Fragment>
+ <dl>
+ <dt>Code</dt>
+ <dd>
+ {TalerErrorCode[error.code]} ({error.code})
+ </dd>
+ <dt>Hint</dt>
+ <dd>{error.hint ?? "--"}</dd>
+ <dt>Time</dt>
+ <dd>
+ <Time
+ timestamp={error.when}
+ format="yyyy/MM/dd HH:mm:ss"
+ />
+ </dd>
+ </dl>
+ <pre
+ style={{
+ whiteSpace: "pre-wrap",
+ wordBreak: "break-word",
+ }}
+ >
+ {JSON.stringify(error, undefined, 2)}
+ </pre>
+ </Fragment>,
+ );
+ }}
+ >
+ fail
+ </a>
+ ) : undefined}
+ </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" />
+ </td>
+ </tr>
+ );
+ }
+ case ObservabilityEventType.DbQueryStart:
+ case ObservabilityEventType.DbQueryFinishSuccess:
+ case ObservabilityEventType.DbQueryFinishError: {
+ return (
+ <tr>
+ <td>
+ <a
+ href="#"
+ onClick={(e) => {
+ e.preventDefault();
+ onClick(
+ <Fragment>
+ <pre
+ style={{
+ whiteSpace: "pre-wrap",
+ wordBreak: "break-word",
+ }}
+ >
+ {JSON.stringify({ event: notif, prev }, undefined, 2)}
+ </pre>
+ </Fragment>,
+ );
+ }}
+ >
+ {title}
+ </a>
+ </td>
+ <td>
+ {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" />
+ </td>
+ </tr>
+ );
+ }
+
+ case ObservabilityEventType.TaskStart:
+ case ObservabilityEventType.TaskStop:
+ case ObservabilityEventType.DeclareTaskDependency:
+ case ObservabilityEventType.TaskReset: {
+ return (
+ <tr>
+ <td>
+ <a
+ href="#"
+ onClick={(e) => {
+ e.preventDefault();
+ onClick(
+ <Fragment>
+ <pre
+ style={{
+ whiteSpace: "pre-wrap",
+ wordBreak: "break-word",
+ }}
+ >
+ {JSON.stringify({ event: notif, prev }, undefined, 2)}
+ </pre>
+ </Fragment>,
+ );
+ }}
+ >
+ {title}
+ </a>
+ </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" />
+ </td>
+ </tr>
+ );
+ }
+ case ObservabilityEventType.ShepherdTaskResult: {
+ return (
+ <tr>
+ <td>
+ <a
+ href="#"
+ onClick={(e) => {
+ e.preventDefault();
+ onClick(
+ <Fragment>
+ <pre
+ style={{
+ whiteSpace: "pre-wrap",
+ wordBreak: "break-word",
+ }}
+ >
+ {JSON.stringify({ event: notif, prev }, undefined, 2)}
+ </pre>
+ </Fragment>,
+ );
+ }}
+ >
+ {title}
+ </a>
+ </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" />
+ </td>
+ </tr>
+ );
+ }
+ case ObservabilityEventType.CryptoStart:
+ case ObservabilityEventType.CryptoFinishSuccess:
+ case ObservabilityEventType.CryptoFinishError: {
+ return (
+ <tr>
+ <td>
+ <a
+ href="#"
+ onClick={(e) => {
+ e.preventDefault();
+ onClick(
+ <Fragment>
+ <pre
+ style={{
+ whiteSpace: "pre-wrap",
+ wordBreak: "break-word",
+ }}
+ >
+ {JSON.stringify({ event: notif, prev }, undefined, 2)}
+ </pre>
+ </Fragment>,
+ );
+ }}
+ >
+ {title}
+ </a>
+ </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" />
+ </td>
+ </tr>
+ );
+ }
+ case ObservabilityEventType.RequestStart:
+ case ObservabilityEventType.RequestFinishSuccess:
+ case ObservabilityEventType.RequestFinishError: {
+ return (
+ <tr>
+ <td>
+ <a
+ href="#"
+ onClick={(e) => {
+ e.preventDefault();
+ onClick(
+ <Fragment>
+ <pre
+ style={{
+ whiteSpace: "pre-wrap",
+ wordBreak: "break-word",
+ }}
+ >
+ {JSON.stringify({ event: notif, prev }, undefined, 2)}
+ </pre>
+ </Fragment>,
+ );
+ }}
+ >
+ {title}
+ </a>
+ </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" />
+ </td>
+ </tr>
+ );
+ }
+ case ObservabilityEventType.Message:
+ // FIXME
+ return <></>;
+ }
+}
+
+function getNotificationFor(
+ id: string,
+ event: WalletNotification,
+ start: AbsoluteTime,
+ list: Notif[],
+): Notif | undefined {
+ const eventWithTime = { ...event, when: start };
+ switch (event.type) {
+ case NotificationType.BalanceChange: {
+ return {
+ id,
+ events: [eventWithTime],
+ reference: {
+ eventType: event.type,
+ referenceType: "transaction",
+ id: event.hintTransactionId,
+ },
+ description: "Balance change",
+ start,
+ end: AbsoluteTime.never(),
+ MoreInfo: ShowBalanceChange,
+ };
+ }
+ case NotificationType.BackupOperationError: {
+ return {
+ id,
+ events: [eventWithTime],
+ reference: undefined,
+ description: "Backup error",
+ start,
+ end: AbsoluteTime.never(),
+ MoreInfo: ShowBackupOperationError,
+ };
+ }
+ case NotificationType.TransactionStateTransition: {
+ const found = list.find(
+ (a) =>
+ a.reference?.eventType === event.type &&
+ a.reference.id === event.transactionId,
+ );
+ if (found) {
+ found.end = start;
+ found.events.unshift(eventWithTime);
+ return undefined;
+ }
+ return {
+ id,
+ events: [eventWithTime],
+ reference: {
+ eventType: event.type,
+ referenceType: "transaction",
+ id: event.transactionId,
+ },
+ description: event.type,
+ start,
+ end: AbsoluteTime.never(),
+ MoreInfo: ShowTransactionStateTransition,
+ };
+ }
+ case NotificationType.ExchangeStateTransition: {
+ const found = list.find(
+ (a) =>
+ a.reference?.eventType === event.type &&
+ a.reference.id === event.exchangeBaseUrl,
+ );
+ if (found) {
+ found.end = start;
+ found.events.unshift(eventWithTime);
+ return undefined;
+ }
+ return {
+ id,
+ events: [eventWithTime],
+ description: "Exchange update",
+ reference: {
+ eventType: event.type,
+ referenceType: "exchange",
+ id: event.exchangeBaseUrl,
+ },
+ start,
+ end: AbsoluteTime.never(),
+ MoreInfo: ShowExchangeStateTransition,
+ };
+ }
+ case NotificationType.TaskObservabilityEvent: {
+ const found = list.find(
+ (a) =>
+ a.reference?.eventType === event.type &&
+ a.reference.id === event.taskId,
+ );
+ if (found) {
+ found.end = start;
+ found.events.unshift(eventWithTime);
+ return undefined;
+ }
+ return {
+ id,
+ events: [eventWithTime],
+ reference: {
+ eventType: event.type,
+ referenceType: "task",
+ id: event.taskId,
+ },
+ description: `Task update ${event.taskId}`,
+ start,
+ end: AbsoluteTime.never(),
+ MoreInfo: ShowObservabilityEvent,
+ };
+ }
+ case NotificationType.WithdrawalOperationTransition: {
+ const found = list.find(
+ (a) =>
+ a.reference?.eventType === event.type && a.reference.id === event.uri,
+ );
+ if (found) {
+ found.end = start;
+ found.events.unshift(eventWithTime);
+ return undefined;
+ }
+ return {
+ id,
+ events: [eventWithTime],
+ reference: {
+ eventType: event.type,
+ referenceType: "task",
+ id: event.uri,
+ },
+ description: `Withdrawal operation updated`,
+ start,
+ end: AbsoluteTime.never(),
+ MoreInfo: ShowObservabilityEvent,
+ };
+ }
+ case NotificationType.RequestObservabilityEvent: {
+ const found = list.find(
+ (a) =>
+ a.reference?.eventType === event.type &&
+ a.reference.id === event.requestId,
+ );
+ if (found) {
+ found.end = start;
+ found.events.unshift(eventWithTime);
+ return undefined;
+ }
+ return {
+ id,
+ events: [eventWithTime],
+ reference: {
+ eventType: event.type,
+ referenceType: "operation",
+ id: event.requestId,
+ },
+ description: `wallet.${event.operation}(${event.requestId})`,
+ start,
+ end: AbsoluteTime.never(),
+ MoreInfo: ShowObservabilityEvent,
+ };
+ }
+ case NotificationType.Idle:
+ return undefined;
+ default: {
+ assertUnreachable(event);
+ }
+ }
+}
+
+function refresh(api: WxApiType, onUpdate: (list: Notif[]) => void) {
+ api.background
+ .call("getNotifications", undefined)
+ .then((notif) => {
+ const list: Notif[] = [];
+ for (const n of notif) {
+ if (
+ n.notification.type === NotificationType.RequestObservabilityEvent &&
+ n.notification.operation === "getActiveTasks"
+ ) {
+ //ignore monitor request
+ continue;
+ }
+ const event = getNotificationFor(
+ String(list.length),
+ n.notification,
+ n.when,
+ list,
+ );
+ // pepe.
+ if (event) {
+ list.unshift(event);
+ }
+ }
+ onUpdate(list);
+ })
+ .catch((error) => {
+ console.log(error);
+ });
+}
+
+export function ObservabilityEventsTable({}: {}): VNode {
+ const { i18n } = useTranslationContext();
+ const api = useBackendContext();
+
+ const [notifications, setNotifications] = useState<Notif[]>([]);
+ const [showDetails, setShowDetails] = useState<VNode>();
+
+ useEffect(() => {
+ let lastTimeout: ReturnType<typeof setTimeout>;
+ function periodicRefresh() {
+ refresh(api, setNotifications);
+
+ lastTimeout = setTimeout(() => {
+ periodicRefresh();
+ }, 1000);
+
+ //clear on unload
+ return () => {
+ clearTimeout(lastTimeout);
+ };
+ }
+ return periodicRefresh();
+ }, [1]);
+
+ return (
+ <div>
+ <div style={{ display: "flex", justifyContent: "space-between" }}>
+ <div
+ style={{ padding: 4, margin: 2, border: "solid 1px black" }}
+ onClick={() => {
+ api.background.call("clearNotifications", undefined).then((d) => {
+ refresh(api, setNotifications);
+ });
+ }}
+ >
+ clear
+ </div>
+ </div>
+ {showDetails && (
+ <Modal
+ title="event details"
+ onClose={{
+ onClick: (async () => {
+ setShowDetails(undefined);
+ }) as any,
+ }}
+ >
+ {showDetails}
+ </Modal>
+ )}
+ {notifications.map((not) => {
+ return (
+ <details key={not.id}>
+ <summary>
+ <div
+ style={{
+ width: "90%",
+ display: "inline-flex",
+ justifyContent: "space-between",
+ padding: 4,
+ }}
+ >
+ <div style={{ padding: 4 }}>{not.description}</div>
+ <div style={{ padding: 4 }}>
+ <Time timestamp={not.start} format="yyyy/MM/dd HH:mm:ss" />
+ </div>
+ <div style={{ padding: 4 }}>
+ <Time timestamp={not.end} format="yyyy/MM/dd HH:mm:ss" />
+ </div>
+ </div>
+ </summary>
+ <not.MoreInfo
+ events={not.events}
+ onClick={(details) => {
+ setShowDetails(details);
+ }}
+ />
+ </details>
+ );
+ })}
+ </div>
+ );
+}
+
+function ErroDetailModal({
+ error,
+ onClose,
+}: {
+ error: TalerErrorDetail;
+ onClose: () => void;
+}): VNode {
+ return (
+ <Modal
+ title="Full detail"
+ onClose={{
+ onClick: onClose as any,
+ }}
+ >
+ <dl>
+ <dt>Code</dt>
+ <dd>
+ {TalerErrorCode[error.code]} ({error.code})
+ </dd>
+ <dt>Hint</dt>
+ <dd>{error.hint ?? "--"}</dd>
+ <dt>Time</dt>
+ <dd>
+ <Time timestamp={error.when} format="yyyy/MM/dd HH:mm:ss" />
+ </dd>
+ </dl>
+ <pre style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}>
+ {JSON.stringify(error, undefined, 2)}
+ </pre>
+ </Modal>
+ );
+}
+
+export function ActiveTasksTable({}: {}): VNode {
+ const { i18n } = useTranslationContext();
+ const api = useBackendContext();
+ const state = useAsyncAsHook(() => {
+ return api.wallet.call(WalletApiOperation.GetActiveTasks, {});
+ });
+ const [showError, setShowError] = useState<TalerErrorDetail>();
+ const tasks = state && !state.hasError ? state.response.tasks : [];
+
+ useEffect(() => {
+ if (!state || state.hasError) return;
+ const lastTimeout = setTimeout(() => {
+ state.retry();
+ }, 1000);
+ return () => {
+ clearTimeout(lastTimeout);
+ };
+ }, [tasks]);
+
+ // const listenAllEvents = Array.from<NotificationType>({ length: 1 });
+ // listenAllEvents.includes = () => true
+ // useEffect(() => {
+ // return api.listener.onUpdateNotification(listenAllEvents, (notif) => {
+ // state?.retry()
+ // });
+ // });
+ return (
+ <Fragment>
+ {showError && (
+ <ErroDetailModal
+ error={showError}
+ onClose={async () => {
+ setShowError(undefined);
+ }}
+ />
+ )}
+
+ <table style={{ width: "100%" }}>
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>Type</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Id</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Since</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Next try</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Error</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Transaction</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ {tasks.map((task) => {
+ const [type, id] = task.taskId.split(":");
+ return (
+ <tr>
+ <td>{type}</td>
+ <td title={id}>{id.substring(0, 10)}</td>
+ <td>
+ <Time
+ timestamp={task.firstTry}
+ format="yyyy/MM/dd HH:mm:ss"
+ />
+ </td>
+ <td>
+ <Time timestamp={task.nextTry} format="yyyy/MM/dd HH:mm:ss" />
+ </td>
+ <td>
+ {!task.lastError?.code ? (
+ ""
+ ) : (
+ <a
+ href="#"
+ onClick={(e) => {
+ e.preventDefault();
+ setShowError(task.lastError);
+ }}
+ >
+ {TalerErrorCode[task.lastError.code]}
+ </a>
+ )}
+ </td>
+ <td>
+ {task.transaction ? (
+ <a
+ title={task.transaction}
+ href={Pages.balanceTransaction({ tid: task.transaction })}
+ >
+ {task.transaction.substring(0, 10)}
+ </a>
+ ) : (
+ "--"
+ )}
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </Fragment>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/components/index.stories.tsx b/packages/taler-wallet-webextension/src/components/index.stories.tsx
index 469ed82fa..4a7a068d3 100644
--- a/packages/taler-wallet-webextension/src/components/index.stories.tsx
+++ b/packages/taler-wallet-webextension/src/components/index.stories.tsx
@@ -24,5 +24,5 @@ export * as a2 from "./PendingTransactions.stories.js";
export * as a3 from "./Amount.stories.js";
export * as a4 from "./ShowFullContractTermPopup.stories.js";
export * as a5 from "./TermsOfService/stories.js";
-export * as a6 from "./QR.stories";
+export * as a6 from "./QR.stories.js";
export * as a7 from "./AmountField.stories.js";
diff --git a/packages/taler-wallet-webextension/src/components/styled/index.tsx b/packages/taler-wallet-webextension/src/components/styled/index.tsx
index 7a3c27c73..89678c74a 100644
--- a/packages/taler-wallet-webextension/src/components/styled/index.tsx
+++ b/packages/taler-wallet-webextension/src/components/styled/index.tsx
@@ -16,7 +16,7 @@
// need to import linaria types, otherwise compiler will complain
// eslint-disable-next-line @typescript-eslint/no-unused-vars
-import type * as Linaria from "@linaria/core";
+// import type * as Linaria from "@linaria/core";
import { styled } from "@linaria/react";
@@ -24,7 +24,7 @@ export const PaymentStatus = styled.div<{ color: string }>`
padding: 5px;
border-radius: 5px;
color: white;
- background-color: ${(p) => p.color};
+ background-color: ${(p: any) => p.color};
`;
export const WalletAction = styled.div`
@@ -35,7 +35,6 @@ export const WalletAction = styled.div`
align-items: center;
margin: auto;
- height: 100%;
& h1:first-child {
margin-top: 0;
@@ -100,7 +99,7 @@ export const WalletBox = styled.div<{ noPadding?: boolean }>`
width: 800px;
}
& > section {
- padding: ${({ noPadding }) => (noPadding ? "0px" : "8px")};
+ padding: ${({ noPadding }: any) => (noPadding ? "0px" : "8px")};
margin-bottom: auto;
overflow: auto;
@@ -159,7 +158,7 @@ export const Middle = styled.div`
height: 100%;
`;
-export const PopupBox = styled.div<{ noPadding?: boolean; devMode?: boolean }>`
+export const PopupBox = styled.div<{ noPadding?: boolean }>`
height: 290px;
width: 500px;
overflow-y: visible;
@@ -168,7 +167,7 @@ export const PopupBox = styled.div<{ noPadding?: boolean; devMode?: boolean }>`
justify-content: space-between;
& > section {
- padding: ${({ noPadding }) => (noPadding ? "0px" : "8px")};
+ padding: ${({ noPadding }: any) => (noPadding ? "0px" : "8px")};
// this margin will send the section up when used with a header
margin-bottom: auto;
overflow-y: auto;
@@ -411,7 +410,8 @@ export const Button = styled.button<{ upperCased?: boolean }>`
cursor: pointer;
user-select: none;
box-sizing: border-box;
- text-transform: ${({ upperCased }) => (upperCased ? "uppercase" : "none")};
+ text-transform: ${({ upperCased }: any) =>
+ upperCased ? "uppercase" : "none"};
font-family: inherit;
font-size: 100%;
@@ -461,7 +461,8 @@ export const Link = styled.a<{ upperCased?: boolean }>`
cursor: pointer;
user-select: none;
box-sizing: border-box;
- text-transform: ${({ upperCased }) => (upperCased ? "uppercase" : "none")};
+ text-transform: ${({ upperCased }: any) =>
+ upperCased ? "uppercase" : "none"};
font-family: inherit;
font-size: 100%;
@@ -535,11 +536,11 @@ export const LinkDestructive = styled(Link)`
`;
export const LinkPrimary = styled(Link)`
- color: rgb(66, 184, 221);
+ color: black;
`;
-export const ButtonPrimary = styled(ButtonVariant)<{ small?: boolean }>`
- font-size: ${({ small }) => (small ? "small" : "inherit")};
+export const ButtonPrimary = styled(ButtonVariant) <{ small?: boolean }>`
+ font-size: ${({ small }: any) => (small ? "small" : "inherit")};
background-color: #0042b2;
border-color: #0042b2;
`;
@@ -717,13 +718,13 @@ export const Input = styled.div<{ invalid?: boolean }>`
& label {
display: block;
padding: 5px;
- color: ${({ invalid }) => (!invalid ? "inherit" : "red")};
+ color: ${({ invalid }: any) => (!invalid ? "inherit" : "red")};
}
& input {
display: block;
padding: 5px;
width: calc(100% - 4px - 10px);
- border-color: ${({ invalid }) => (!invalid ? "inherit" : "red")};
+ border-color: ${({ invalid }: any) => (!invalid ? "inherit" : "red")};
}
`;
@@ -735,7 +736,7 @@ export const InputWithLabel = styled.div<{ invalid?: boolean }>`
font-weight: bold;
margin-left: 0.5em;
padding: 5px;
- color: ${({ invalid }) => (!invalid ? "inherit" : "red")};
+ color: ${({ invalid }: any) => (!invalid ? "inherit" : "red")};
}
& div {
@@ -761,7 +762,7 @@ export const InputWithLabel = styled.div<{ invalid?: boolean }>`
/* border-color: lightgray; */
border-bottom-right-radius: 0.25em;
border-top-right-radius: 0.25em;
- border-color: ${({ invalid }) => (!invalid ? "lightgray" : "red")};
+ border-color: ${({ invalid }: any) => (!invalid ? "lightgray" : "red")};
}
margin-bottom: 16px;
`;
@@ -797,6 +798,17 @@ export const ErrorBox = styled.div`
}
`;
+export const RedBanner = styled.div`
+ width: 80%;
+ padding: 4px;
+ text-align: center;
+ background: red;
+ border: 1px solid #df3a3a;
+ border-radius: 4px;
+ font-weight: bold;
+ margin: 4px;
+`;
+
export const InfoBox = styled(ErrorBox)`
color: black;
background-color: #d1e7dd;
@@ -859,8 +871,8 @@ interface SvgIconProps {
}
export const SvgIcon = styled.div<SvgIconProps>`
& > svg {
- fill: ${({ color }) => color};
- transform: ${({ transform }) => (transform ? transform : "")};
+ fill: ${({ color }: any) => color};
+ transform: ${({ transform }: any) => (transform ? transform : "")};
}
/* width: 24px;
height: 24px; */
@@ -868,7 +880,7 @@ export const SvgIcon = styled.div<SvgIconProps>`
margin-right: 8px;
display: inline;
padding: 4px;
- cursor: ${({ onClick }) => (onClick ? "pointer" : "inherit")};
+ cursor: ${({ onClick }: any) => (onClick ? "pointer" : "inherit")};
`;
export const Icon = styled.div`
@@ -959,7 +971,7 @@ export const TermsSection = styled.a`
}
`;
-export const TermsOfService = styled.div`
+export const TermsOfServiceStyle = styled.div`
display: flex;
flex-direction: column;
text-align: left;
diff --git a/packages/taler-wallet-webextension/src/context/alert.ts b/packages/taler-wallet-webextension/src/context/alert.ts
new file mode 100644
index 000000000..e30fdd72c
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/context/alert.ts
@@ -0,0 +1,277 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import {
+ TalerError,
+ TalerErrorCode,
+ TalerErrorDetail,
+ TranslatedString,
+} from "@gnu-taler/taler-util";
+import { ComponentChildren, createContext, h, VNode } from "preact";
+import { useContext, useState } from "preact/hooks";
+import { HookError } from "../hooks/useAsyncAsHook.js";
+import { SafeHandler, withSafe } from "../mui/handlers.js";
+import { BackgroundError } from "../wxApi.js";
+import {
+ InternationalizationAPI,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { platform } from "../platform/foreground.js";
+
+export type AlertType = "info" | "warning" | "error" | "success";
+
+export interface InfoAlert {
+ message: TranslatedString;
+ description: TranslatedString | VNode;
+ type: "info" | "warning" | "success";
+}
+
+export type Alert = InfoAlert | ErrorAlert;
+
+export interface ErrorAlert {
+ message: TranslatedString;
+ description: TranslatedString | VNode;
+ type: "error";
+ context: object | undefined;
+ cause: any | undefined;
+}
+
+type Type = {
+ alerts: Alert[];
+ pushAlert: (n: Alert) => void;
+ removeAlert: (n: Alert) => void;
+ /**
+ *
+ * @param h
+ * @returns
+ * @deprecated use safely
+ */
+ pushAlertOnError: <T>(h: (p: T) => Promise<void>) => SafeHandler<T>;
+ safely: <T>(name: string, h: (p: T) => Promise<void>) => SafeHandler<T>;
+};
+
+const initial: Type = {
+ alerts: [],
+ pushAlertOnError: () => {
+ throw Error("alert context not initialized");
+ },
+ safely: () => {
+ throw Error("alert context not initialized");
+ },
+ pushAlert: () => {
+ null;
+ },
+ removeAlert: () => {
+ null;
+ },
+};
+
+const Context = createContext<Type>(initial);
+
+type AlertWithDate = Alert & { since: Date };
+
+type Props = Partial<Type> & {
+ children: ComponentChildren;
+};
+
+export const AlertProvider = ({ children }: Props): VNode => {
+ const timeout = 3000;
+
+ const [alerts, setAlerts] = useState<AlertWithDate[]>([]);
+
+ const pushAlert = (n: Alert): void => {
+ const entry = { ...n, since: new Date() };
+ setAlerts((ns) => [...ns, entry]);
+ if (n.type !== "error") {
+ setTimeout(() => {
+ setAlerts((ns) => ns.filter((x) => x.since !== entry.since));
+ }, timeout);
+ }
+ };
+
+ const removeAlert = (alert: Alert): void => {
+ setAlerts((ns: AlertWithDate[]) => ns.filter((n) => n !== alert));
+ };
+
+ const { i18n } = useTranslationContext();
+
+ function pushAlertOnError<T>(
+ handler: (p: T) => Promise<void>,
+ ): SafeHandler<T> {
+ return withSafe(handler, (e) => {
+ const a = alertFromError(i18n, e.message as TranslatedString, e);
+ pushAlert(a);
+ });
+ }
+
+ function safely<T>(
+ name: string,
+ handler: (p: T) => Promise<void>,
+ ): SafeHandler<T> {
+ const message = i18n.str`Error was thrown trying to: "${name}"`;
+ return withSafe(handler, (e) => {
+ const a = alertFromError(i18n, message, e);
+ pushAlert(a);
+ });
+ }
+
+ return h(Context.Provider, {
+ value: { alerts, pushAlert, removeAlert, pushAlertOnError, safely },
+ children,
+ });
+};
+
+export const useAlertContext = (): Type => useContext(Context);
+
+export function alertFromError(
+ i18n: InternationalizationAPI,
+ message: TranslatedString,
+ error: HookError,
+ ...context: any[]
+): ErrorAlert;
+
+export function alertFromError(
+ i18n: InternationalizationAPI,
+ message: TranslatedString,
+ error: Error,
+ ...context: any[]
+): ErrorAlert;
+
+export function alertFromError(
+ i18n: InternationalizationAPI,
+ message: TranslatedString,
+ error: TalerErrorDetail,
+ ...context: any[]
+): ErrorAlert;
+
+export function alertFromError(
+ i18n: InternationalizationAPI,
+ message: TranslatedString,
+ error: HookError | TalerErrorDetail | Error,
+ ...context: any[]
+): ErrorAlert {
+ let description: TranslatedString;
+ let cause: any;
+
+ if (typeof error === "object" && error !== null) {
+ if ("code" in error) {
+ //TalerErrorDetail
+ description = (error.hint ??
+ `Error code: ${error.code}`) as TranslatedString;
+ cause = {
+ details: error,
+ };
+ } else if ("hasError" in error) {
+ //HookError
+ description = error.message as TranslatedString;
+ if (error.type === "taler") {
+ const msg = isWalletNotAvailable(i18n, error.details);
+ if (msg) {
+ description = msg;
+ } else {
+ const msg2 = isHttpError(i18n, error.details);
+ if (msg2) {
+ description = msg2;
+ }
+ }
+ cause = {
+ details: error.details,
+ };
+ }
+ } else {
+ if (error instanceof BackgroundError) {
+ const msg = isWalletNotAvailable(i18n, error.errorDetail);
+ if (msg) {
+ description = msg;
+ } else {
+ const msg2 = isHttpError(i18n, error.errorDetail);
+ if (msg2) {
+ description = msg2;
+ } else {
+ description = (error.errorDetail.hint ??
+ `Error code: ${error.errorDetail.code}`) as TranslatedString;
+ }
+ }
+ cause = {
+ details: error.errorDetail,
+ stack: error.stack,
+ };
+ } else {
+ description = error.message as TranslatedString;
+ cause = {
+ stack: error.stack,
+ };
+ }
+ }
+ } else {
+ description = "" as TranslatedString;
+ cause = error;
+ }
+
+ return {
+ type: "error",
+ message,
+ description,
+ cause,
+ context,
+ };
+}
+
+function isWalletNotAvailable(
+ i18n: InternationalizationAPI,
+ detail: TalerErrorDetail,
+): TranslatedString | undefined {
+ if (
+ detail.code === TalerErrorCode.WALLET_CORE_NOT_AVAILABLE &&
+ detail.lastError
+ ) {
+ const le = detail.lastError as TalerErrorDetail;
+ if (le.code === TalerErrorCode.WALLET_DB_UNAVAILABLE) {
+ if (platform.isFirefox() && platform.runningOnPrivateMode()) {
+ return i18n.str`Could not open the wallet database. Firefox is known to run into this problem under "permanent private mode".`;
+ } else {
+ return i18n.str`Could not open the wallet database.`;
+ }
+ } else {
+ return (detail.hint ?? `Error code: ${detail.code}`) as TranslatedString;
+ }
+ }
+ return undefined;
+}
+
+function isHttpError(
+ i18n: InternationalizationAPI,
+ detail: TalerErrorDetail,
+): TranslatedString | undefined {
+ if (
+ detail.code === TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR &&
+ detail.errorResponse
+ ) {
+ const er = detail.errorResponse as TalerErrorDetail;
+ return (
+ (er.hint as TranslatedString) ??
+ detail.hint ??
+ i18n.str`Unexpected request error, code: ${er.code}`
+ );
+ }
+ return undefined;
+}
+//
diff --git a/packages/demobank-ui/src/context/backend.ts b/packages/taler-wallet-webextension/src/context/backend.ts
index 1c40506c9..280fb266d 100644
--- a/packages/demobank-ui/src/context/backend.ts
+++ b/packages/taler-wallet-webextension/src/context/backend.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -14,43 +14,39 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { ComponentChildren, createContext, h, VNode } from "preact";
-import { useContext } from "preact/hooks";
-import {
- BackendStateHandler,
- defaultState,
- useBackendState,
-} from "../hooks/backend.js";
-
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
-export type Type = BackendStateHandler;
+import { ComponentChildren, createContext, h, VNode } from "preact";
+import { useContext } from "preact/hooks";
+import { wxApi, WxApiType } from "../wxApi.js";
-const initial: Type = {
- state: defaultState,
- clear() {
- null;
- },
- save(info) {
- null;
- },
-};
-const Context = createContext<Type>(initial);
+type Type = WxApiType;
-export const useBackendContext = (): Type => useContext(Context);
+const initial = wxApi;
-export const BackendStateProvider = ({
- children,
-}: {
+const Context = createContext<Type>(initial);
+
+type Props = Partial<Type> & {
children: ComponentChildren;
-}): VNode => {
- const value = useBackendState();
+};
+export const BackendProvider = ({
+ wallet,
+ background,
+ listener,
+ children,
+}: Props): VNode => {
return h(Context.Provider, {
- value,
+ value: {
+ wallet: wallet ?? initial.wallet,
+ background: background ?? initial.background,
+ listener: listener ?? initial.listener,
+ },
children,
});
};
+
+export const useBackendContext = (): Type => useContext(Context);
diff --git a/packages/taler-wallet-webextension/src/context/devContext.ts b/packages/taler-wallet-webextension/src/context/devContext.ts
deleted file mode 100644
index 99301df52..000000000
--- a/packages/taler-wallet-webextension/src/context/devContext.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import { createContext, h, VNode } from "preact";
-import { useContext } from "preact/hooks";
-import { useWalletDevMode } from "../hooks/useWalletDevMode.js";
-import { ToggleHandler } from "../mui/handlers.js";
-
-interface Type {
- devMode: boolean;
- devModeToggle: ToggleHandler;
-}
-const Context = createContext<Type>({
- devMode: false,
- devModeToggle: {
- button: {},
- },
-});
-
-export const useDevContext = (): Type => useContext(Context);
-
-export const DevContextProviderForTesting = ({
- value,
- children,
-}: {
- value?: boolean;
- children: any;
-}): VNode => {
- return h(Context.Provider, {
- value: {
- devMode: !!value,
- devModeToggle: {
- value,
- button: {},
- },
- },
- children,
- });
-};
-
-export const DevContextProvider = ({ children }: { children: any }): VNode => {
- const devModeToggle = useWalletDevMode();
- const value: Type = { devMode: !!devModeToggle.value, devModeToggle };
- //support for function as children, useful for getting the value right away
- children =
- children.length === 1 && typeof children === "function"
- ? children(value)
- : children;
-
- return h(Context.Provider, { value, children });
-};
diff --git a/packages/taler-wallet-webextension/src/context/iocContext.ts b/packages/taler-wallet-webextension/src/context/iocContext.ts
index c7fee0bc0..89f984f2f 100644
--- a/packages/taler-wallet-webextension/src/context/iocContext.ts
+++ b/packages/taler-wallet-webextension/src/context/iocContext.ts
@@ -21,7 +21,7 @@
import { createContext, h, VNode } from "preact";
import { useContext } from "preact/hooks";
-import { platform } from "../platform/api.js";
+import { platform } from "../platform/foreground.js";
interface Type {
findTalerUriInActiveTab: () => Promise<string | undefined>;
diff --git a/packages/taler-wallet-webextension/src/context/translation.ts b/packages/taler-wallet-webextension/src/context/translation.ts
deleted file mode 100644
index bc7e4bee2..000000000
--- a/packages/taler-wallet-webextension/src/context/translation.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import { i18n, setupI18n } from "@gnu-taler/taler-util";
-import { createContext, h, VNode } from "preact";
-import { useContext, useEffect } from "preact/hooks";
-import { useLang } from "../hooks/useLang.js";
-import { strings } from "../i18n/strings.js";
-
-interface Type {
- lang: string;
- supportedLang: { [id in keyof typeof supportedLang]: string };
- changeLanguage: (l: string) => void;
- i18n: typeof i18n;
- isSaved: boolean;
-}
-
-const supportedLang = {
- es: "Español [es]",
- ja: "日本語 [ja]",
- en: "English [en]",
- fr: "Français [fr]",
- de: "Deutsch [de]",
- sv: "Svenska [sv]",
- it: "Italiano [it]",
- // ko: "한국어 [ko]",
- // ru: "Ру́сский язы́к [ru]",
- tr: "Türk [tr]",
- navigator: "Defined by navigator",
-};
-
-const initial = {
- lang: "en",
- supportedLang,
- changeLanguage: () => {
- // do not change anything
- },
- i18n,
- isSaved: false,
-};
-const Context = createContext<Type>(initial);
-
-interface Props {
- initial?: string;
- children: any;
- forceLang?: string;
-}
-
-export const TranslationProvider = ({
- initial,
- children,
- forceLang,
-}: Props): VNode => {
- const [lang, changeLanguage, isSaved] = useLang(initial);
- useEffect(() => {
- if (forceLang) {
- changeLanguage(forceLang);
- }
- });
- useEffect(() => {
- setupI18n(lang, strings);
- }, [lang]);
- if (forceLang) {
- setupI18n(forceLang, strings);
- } else {
- setupI18n(lang, strings);
- }
- return h(Context.Provider, {
- value: { lang, changeLanguage, supportedLang, i18n, isSaved },
- children,
- });
-};
-
-export const useTranslationContext = (): Type => useContext(Context);
diff --git a/packages/taler-wallet-webextension/src/cta/Deposit/index.ts b/packages/taler-wallet-webextension/src/cta/Deposit/index.ts
index 539709821..6b228188b 100644
--- a/packages/taler-wallet-webextension/src/cta/Deposit/index.ts
+++ b/packages/taler-wallet-webextension/src/cta/Deposit/index.ts
@@ -15,13 +15,13 @@
*/
import { AmountJson, AmountString } from "@gnu-taler/taler-util";
+import { ErrorAlertView } from "../../components/CurrentAlerts.js";
import { Loading } from "../../components/Loading.js";
-import { HookError } from "../../hooks/useAsyncAsHook.js";
+import { ErrorAlert } from "../../context/alert.js";
import { ButtonHandler } from "../../mui/handlers.js";
import { compose, StateViewMap } from "../../utils/index.js";
-import { wxApi } from "../../wxApi.js";
import { useComponentState } from "./state.js";
-import { LoadingUriView, ReadyView } from "./views.js";
+import { ReadyView } from "./views.js";
export interface Props {
talerDepositUri: string | undefined;
@@ -38,8 +38,8 @@ export namespace State {
error: undefined;
}
export interface LoadingUriError {
- status: "loading-uri";
- error: HookError;
+ status: "error";
+ error: ErrorAlert;
}
export interface Ready {
status: "ready";
@@ -58,12 +58,12 @@ export namespace State {
const viewMapping: StateViewMap<State> = {
loading: Loading,
- "loading-uri": LoadingUriView,
+ error: ErrorAlertView,
ready: ReadyView,
};
export const DepositPage = compose(
"Deposit",
- (p: Props) => useComponentState(p, wxApi),
+ (p: Props) => useComponentState(p),
viewMapping,
);
diff --git a/packages/taler-wallet-webextension/src/cta/Deposit/state.ts b/packages/taler-wallet-webextension/src/cta/Deposit/state.ts
index 77e918ca9..efcef8c28 100644
--- a/packages/taler-wallet-webextension/src/cta/Deposit/state.ts
+++ b/packages/taler-wallet-webextension/src/cta/Deposit/state.ts
@@ -16,14 +16,20 @@
import { Amounts } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { alertFromError, useAlertContext } from "../../context/alert.js";
+import { useBackendContext } from "../../context/backend.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
-import { wxApi } from "../../wxApi.js";
import { Props, State } from "./index.js";
-export function useComponentState(
- { talerDepositUri, amountStr, cancel, onSuccess }: Props,
- api: typeof wxApi,
-): State {
+export function useComponentState({
+ talerDepositUri,
+ amountStr,
+ cancel,
+ onSuccess,
+}: Props): State {
+ const api = useBackendContext();
+ const { pushAlertOnError } = useAlertContext();
const info = useAsyncAsHook(async () => {
if (!talerDepositUri) throw Error("ERROR_NO-URI-FOR-DEPOSIT");
if (!amountStr) throw Error("ERROR_NO-AMOUNT-FOR-DEPOSIT");
@@ -35,12 +41,17 @@ export function useComponentState(
});
return { deposit, uri: talerDepositUri, amount };
});
+ const { i18n } = useTranslationContext();
if (!info) return { status: "loading", error: undefined };
if (info.hasError) {
return {
- status: "loading-uri",
- error: info,
+ status: "error",
+ error: alertFromError(
+ i18n,
+ i18n.str`Could not load the status of deposit`,
+ info,
+ ),
};
}
@@ -57,7 +68,7 @@ export function useComponentState(
status: "ready",
error: undefined,
confirm: {
- onClick: doDeposit,
+ onClick: pushAlertOnError(doDeposit),
},
fee: Amounts.sub(deposit.totalDepositCost, deposit.effectiveDepositAmount)
.amount,
diff --git a/packages/taler-wallet-webextension/src/cta/Deposit/stories.tsx b/packages/taler-wallet-webextension/src/cta/Deposit/stories.tsx
index 6d1535953..cd65ce8e1 100644
--- a/packages/taler-wallet-webextension/src/cta/Deposit/stories.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Deposit/stories.tsx
@@ -20,14 +20,14 @@
*/
import { Amounts } from "@gnu-taler/taler-util";
-import { createExample } from "../../test-utils.js";
+import * as tests from "@gnu-taler/web-util/testing";
import { ReadyView } from "./views.js";
export default {
title: "deposit",
};
-export const Ready = createExample(ReadyView, {
+export const Ready = tests.createExample(ReadyView, {
status: "ready",
confirm: {},
cost: Amounts.parseOrThrow("EUR:1.2"),
diff --git a/packages/taler-wallet-webextension/src/cta/Deposit/test.ts b/packages/taler-wallet-webextension/src/cta/Deposit/test.ts
index a5bfed4a8..100929918 100644
--- a/packages/taler-wallet-webextension/src/cta/Deposit/test.ts
+++ b/packages/taler-wallet-webextension/src/cta/Deposit/test.ts
@@ -19,17 +19,19 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { Amounts } from "@gnu-taler/taler-util";
-import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { AmountString, Amounts } from "@gnu-taler/taler-util";
import { expect } from "chai";
-import { mountHook } from "../../test-utils.js";
import { createWalletApiMock } from "../../test-utils.js";
import { useComponentState } from "./state.js";
+import * as tests from "@gnu-taler/web-util/testing";
+import { Props } from "./index.js";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
describe("Deposit CTA states", () => {
it("should tell the user that the URI is missing", async () => {
- const { handler, mock } = createWalletApiMock();
- const props = {
+ const { handler, TestingContext } = createWalletApiMock();
+
+ const props: Props = {
talerDepositUri: undefined,
amountStr: undefined,
cancel: async () => {
@@ -39,43 +41,50 @@ describe("Deposit CTA states", () => {
null;
},
};
- const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
- mountHook(() => useComponentState(props, mock));
- {
- const { status } = pullLastResultOrThrow();
- expect(status).equals("loading");
- }
-
- expect(await waitForStateUpdate()).true;
-
- {
- const { status, error } = pullLastResultOrThrow();
-
- expect(status).equals("loading-uri");
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentState,
+ props,
+ [
+ ({ status }) => {
+ expect(status).equals("loading");
+ },
+ ({ status, error }) => {
+ expect(status).equals("error");
+
+ if (!error) expect.fail();
+ // if (!error.hasError) expect.fail();
+ // if (error.operational) expect.fail();
+ expect(error.description).eq("ERROR_NO-URI-FOR-DEPOSIT");
+ },
+ ],
+ TestingContext,
+ );
- if (!error) expect.fail();
- if (!error.hasError) expect.fail();
- if (error.operational) expect.fail();
- expect(error.message).eq("ERROR_NO-URI-FOR-DEPOSIT");
- }
- await assertNoPendingUpdate();
+ expect(hookBehavior).deep.equal({ result: "ok" });
expect(handler.getCallingQueueState()).eq("empty");
});
it("should be ready after loading", async () => {
- const { handler, mock } = createWalletApiMock();
+ const { handler, TestingContext } = createWalletApiMock();
+
handler.addWalletCallResponse(
WalletApiOperation.PrepareDeposit,
undefined,
{
- effectiveDepositAmount: "EUR:1",
- totalDepositCost: "EUR:1.2",
+ effectiveDepositAmount: "EUR:1" as AmountString,
+ totalDepositCost: "EUR:1.2" as AmountString,
+ fees: {
+ coin: "EUR:0" as AmountString,
+ refresh: "EUR:0.2" as AmountString,
+ wire: "EUR:0" as AmountString,
+ },
},
);
+
const props = {
talerDepositUri: "payto://refund/asdasdas",
- amountStr: "EUR:1",
+ amountStr: "EUR:1" as AmountString,
cancel: async () => {
null;
},
@@ -84,28 +93,26 @@ describe("Deposit CTA states", () => {
},
};
- const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
- mountHook(() => useComponentState(props, mock));
-
- {
- const { status } = pullLastResultOrThrow();
- expect(status).equals("loading");
- }
-
- expect(await waitForStateUpdate()).true;
-
- {
- const state = pullLastResultOrThrow();
-
- if (state.status !== "ready") expect.fail();
- if (state.error) expect.fail();
- expect(state.confirm.onClick).not.undefined;
- expect(state.cost).deep.eq(Amounts.parseOrThrow("EUR:1.2"));
- expect(state.fee).deep.eq(Amounts.parseOrThrow("EUR:0.2"));
- expect(state.effective).deep.eq(Amounts.parseOrThrow("EUR:1"));
- }
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentState,
+ props,
+ [
+ ({ status }) => {
+ expect(status).equals("loading");
+ },
+ (state) => {
+ if (state.status !== "ready") expect.fail();
+ if (state.error) expect.fail();
+ expect(state.confirm.onClick).not.undefined;
+ expect(state.cost).deep.eq(Amounts.parseOrThrow("EUR:1.2"));
+ expect(state.fee).deep.eq(Amounts.parseOrThrow("EUR:0.2"));
+ expect(state.effective).deep.eq(Amounts.parseOrThrow("EUR:1"));
+ },
+ ],
+ TestingContext,
+ );
- await assertNoPendingUpdate();
+ expect(hookBehavior).deep.equal({ result: "ok" });
expect(handler.getCallingQueueState()).eq("empty");
});
});
diff --git a/packages/taler-wallet-webextension/src/cta/Deposit/views.tsx b/packages/taler-wallet-webextension/src/cta/Deposit/views.tsx
index 2ec305de5..c683a755c 100644
--- a/packages/taler-wallet-webextension/src/cta/Deposit/views.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Deposit/views.tsx
@@ -15,13 +15,10 @@
*/
import { Amounts } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, h, VNode } from "preact";
import { Amount } from "../../components/Amount.js";
-import { LoadingError } from "../../components/LoadingError.js";
-import { LogoHeader } from "../../components/LogoHeader.js";
import { Part } from "../../components/Part.js";
-import { SubTitle, WalletAction } from "../../components/styled/index.js";
-import { useTranslationContext } from "../../context/translation.js";
import { Button } from "../../mui/Button.js";
import { State } from "./index.js";
@@ -30,32 +27,16 @@ import { State } from "./index.js";
* @author sebasjm
*/
-export function LoadingUriView({ error }: State.LoadingUriError): VNode {
- const { i18n } = useTranslationContext();
-
- return (
- <LoadingError
- title={<i18n.Translate>Could not load deposit status</i18n.Translate>}
- error={error}
- />
- );
-}
-
export function ReadyView(state: State.Ready): VNode {
const { i18n } = useTranslationContext();
return (
- <WalletAction>
- <LogoHeader />
-
- <SubTitle>
- <i18n.Translate>Digital cash deposit</i18n.Translate>
- </SubTitle>
+ <Fragment>
<section>
{Amounts.isNonZero(state.cost) && (
<Part
big
- title={<i18n.Translate>Cost</i18n.Translate>}
+ title={i18n.str`Cost`}
text={<Amount value={state.cost} />}
kind="negative"
/>
@@ -63,14 +44,14 @@ export function ReadyView(state: State.Ready): VNode {
{Amounts.isNonZero(state.fee) && (
<Part
big
- title={<i18n.Translate>Fee</i18n.Translate>}
+ title={i18n.str`Fee`}
text={<Amount value={state.fee} />}
kind="negative"
/>
)}
<Part
big
- title={<i18n.Translate>To be received</i18n.Translate>}
+ title={i18n.str`To be received`}
text={<Amount value={state.effective} />}
kind="positive"
/>
@@ -86,6 +67,6 @@ export function ReadyView(state: State.Ready): VNode {
</i18n.Translate>
</Button>
</section>
- </WalletAction>
+ </Fragment>
);
}
diff --git a/packages/taler-wallet-webextension/src/cta/DevExperiment/index.ts b/packages/taler-wallet-webextension/src/cta/DevExperiment/index.ts
new file mode 100644
index 000000000..ec09fd9f1
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/DevExperiment/index.ts
@@ -0,0 +1,73 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { ErrorAlertView } from "../../components/CurrentAlerts.js";
+import { Loading } from "../../components/Loading.js";
+import { ErrorAlert } from "../../context/alert.js";
+import { ButtonHandler } from "../../mui/handlers.js";
+import { StateViewMap, compose } from "../../utils/index.js";
+import { useComponentState } from "./state.js";
+import { InsertLostView, InsertPendingRefreshView, UnknownView } from "./views.js";
+
+export interface Props {
+ talerExperimentUri: string | undefined;
+ onCancel: () => Promise<void>;
+ onSuccess: () => Promise<void>;
+}
+
+export type State = State.Loading | State.LoadingUriError | State.Unknown | State.InsertLost | State.PendingRefresh;
+
+export namespace State {
+ export interface Loading {
+ status: "loading";
+ error: undefined;
+ }
+ export interface LoadingUriError {
+ status: "error";
+ error: ErrorAlert;
+ }
+ export interface InsertLost {
+ status: "insertLost";
+ error: undefined;
+ confirm: ButtonHandler;
+ cancel: () => Promise<void>;
+ }
+ export interface PendingRefresh {
+ status: "pendingRefresh";
+ error: undefined;
+ confirm: ButtonHandler;
+ cancel: () => Promise<void>;
+ }
+ export interface Unknown {
+ status: "unknown";
+ experimentId: string;
+ error: undefined;
+ }
+}
+
+const viewMapping: StateViewMap<State> = {
+ loading: Loading,
+ error: ErrorAlertView,
+ pendingRefresh: InsertPendingRefreshView,
+ insertLost: InsertLostView,
+ unknown: UnknownView,
+};
+
+export const DevExperimentPage = compose(
+ "DevExperiment",
+ (p: Props) => useComponentState(p),
+ viewMapping,
+);
diff --git a/packages/taler-wallet-webextension/src/cta/DevExperiment/state.ts b/packages/taler-wallet-webextension/src/cta/DevExperiment/state.ts
new file mode 100644
index 000000000..774a1129d
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/DevExperiment/state.ts
@@ -0,0 +1,83 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { parseDevExperimentUri } from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { useAlertContext } from "../../context/alert.js";
+import { useBackendContext } from "../../context/backend.js";
+import { Props, State } from "./index.js";
+
+export function useComponentState({
+ talerExperimentUri,
+ onCancel,
+ onSuccess,
+}: Props): State {
+ const api = useBackendContext();
+ const { pushAlertOnError } = useAlertContext();
+ const { i18n } = useTranslationContext();
+
+ async function doApply(): Promise<void> {
+ if (!talerExperimentUri) return;
+ await api.wallet.call(WalletApiOperation.ApplyDevExperiment, {
+ devExperimentUri: talerExperimentUri
+ })
+ // const resp = await api.wallet.call(WalletApiOperation.CreateDepositGroup, {
+ // amount: Amounts.stringify(amount),
+ // depositPaytoUri: uri,
+ // });
+ onSuccess();
+ }
+ const uri = talerExperimentUri === undefined ? undefined : parseDevExperimentUri(talerExperimentUri);
+
+ if (!uri) {
+ return {
+ status: "error",
+ error: {
+ type: "error",
+ message: i18n.str`Invalid dev experiment URI.`,
+ description: i18n.str`URI: ${talerExperimentUri}`,
+ cause: {},
+ context: {},
+ },
+ };
+ }
+ if (uri.devExperimentId === "insert-denom-loss") {
+ return {
+ status: "insertLost",
+ error: undefined,
+ confirm: {
+ onClick: pushAlertOnError(doApply),
+ },
+ cancel: onCancel,
+ };
+ }
+ if (uri.devExperimentId === "insert-pending-refresh") {
+ return {
+ status: "pendingRefresh",
+ error: undefined,
+ confirm: {
+ onClick: pushAlertOnError(doApply),
+ },
+ cancel: onCancel,
+ };
+ }
+ return {
+ status: "unknown",
+ error: undefined,
+ experimentId: uri.devExperimentId,
+ }
+}
diff --git a/packages/demobank-ui/src/pages/home/QrCodeSection.stories.tsx b/packages/taler-wallet-webextension/src/cta/DevExperiment/stories.tsx
index 521d4255e..c9851495f 100644
--- a/packages/demobank-ui/src/pages/home/QrCodeSection.stories.tsx
+++ b/packages/taler-wallet-webextension/src/cta/DevExperiment/stories.tsx
@@ -19,15 +19,15 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { QrCodeSection } from "./QrCodeSection.js";
+import * as tests from "@gnu-taler/web-util/testing";
+import { InsertLostView } from "./views.js";
export default {
- title: "Qr Code Selection",
+ title: "dev-experiment",
};
-export const SimpleExample = {
- component: QrCodeSection,
- props: {
- talerWithdrawUri: "taler://withdraw/asdasdasd",
- },
-};
+export const Ready = tests.createExample(InsertLostView, {
+ status: "insertLost",
+ confirm: {},
+ error: undefined,
+});
diff --git a/packages/taler-wallet-webextension/src/cta/DevExperiment/test.ts b/packages/taler-wallet-webextension/src/cta/DevExperiment/test.ts
new file mode 100644
index 000000000..d4f2ca8b1
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/DevExperiment/test.ts
@@ -0,0 +1,65 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import * as tests from "@gnu-taler/web-util/testing";
+import { expect } from "chai";
+import { createWalletApiMock } from "../../test-utils.js";
+import { Props } from "./index.js";
+import { useComponentState } from "./state.js";
+
+describe("DevExperiment CTA states", () => {
+ it("should tell the user that the URI is missing", async () => {
+ const { handler, TestingContext } = createWalletApiMock();
+
+ const props: Props = {
+ talerExperimentUri: undefined,
+ onCancel: async () => {
+ null;
+ },
+ onSuccess: async () => {
+ null;
+ },
+ };
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentState,
+ props,
+ [
+ ({ status }) => {
+ expect(status).equals("error");
+ },
+ ({ status, error }) => {
+ expect(status).equals("error");
+
+ if (!error) expect.fail();
+ // if (!error.hasError) expect.fail();
+ // if (error.operational) expect.fail();
+ // expect(error.description).eq("ERROR_NO-URI-FOR-DEPOSIT");
+ },
+ ],
+ TestingContext,
+ );
+
+ expect(hookBehavior).deep.equal({ result: "ok" });
+ expect(handler.getCallingQueueState()).eq("empty");
+ });
+
+});
diff --git a/packages/taler-wallet-webextension/src/cta/DevExperiment/views.tsx b/packages/taler-wallet-webextension/src/cta/DevExperiment/views.tsx
new file mode 100644
index 000000000..afad17ad1
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/DevExperiment/views.tsx
@@ -0,0 +1,74 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { Amounts } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { Amount } from "../../components/Amount.js";
+import { Part } from "../../components/Part.js";
+import { Button } from "../../mui/Button.js";
+import { State } from "./index.js";
+
+/**
+ *
+ * @author sebasjm
+ */
+
+export function InsertLostView(state: State.InsertLost): VNode {
+ const { i18n } = useTranslationContext();
+ return <Fragment>
+ <section>
+ <Part
+ title={i18n.str`Experiment`}
+ text={i18n.str`Insert lost denomination`}
+ />
+ </section>
+ <section>
+ <Button
+ variant="contained"
+ color="success"
+ onClick={state.confirm.onClick}
+ >
+ <i18n.Translate>Apply</i18n.Translate>
+ </Button>
+ </section>
+ </Fragment>
+}
+
+export function InsertPendingRefreshView(state: State.PendingRefresh): VNode {
+ const { i18n } = useTranslationContext();
+ return <Fragment>
+ <section>
+ <Part
+ title={i18n.str`Experiment`}
+ text={i18n.str`Pending refresh`}
+ />
+ </section>
+ <section>
+ <Button
+ variant="contained"
+ color="success"
+ onClick={state.confirm.onClick}
+ >
+ <i18n.Translate>Apply</i18n.Translate>
+ </Button>
+ </section>
+ </Fragment>
+}
+
+export function UnknownView(state: State.Unknown): VNode {
+ return <div>unknown experiment "{state.experimentId}"</div>
+}
diff --git a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/index.ts b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/index.ts
index 01dbb6d6d..fd3fb52f8 100644
--- a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/index.ts
+++ b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/index.ts
@@ -14,20 +14,20 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { AmountJson, TalerErrorDetail } from "@gnu-taler/taler-util";
+import { AmountJson, AmountString } from "@gnu-taler/taler-util";
+import { ErrorAlertView } from "../../components/CurrentAlerts.js";
import { Loading } from "../../components/Loading.js";
-import { HookError } from "../../hooks/useAsyncAsHook.js";
+import { ErrorAlert } from "../../context/alert.js";
import { State as SelectExchangeState } from "../../hooks/useSelectedExchange.js";
import { ButtonHandler, TextFieldHandler } from "../../mui/handlers.js";
import { compose, StateViewMap } from "../../utils/index.js";
import { ExchangeSelectionPage } from "../../wallet/ExchangeSelection/index.js";
import { NoExchangesView } from "../../wallet/ExchangeSelection/views.js";
-import { wxApi } from "../../wxApi.js";
import { useComponentState } from "./state.js";
-import { LoadingUriView, ReadyView } from "./views.js";
+import { ReadyView } from "./views.js";
export interface Props {
- amount: string;
+ amount: AmountString;
onClose: () => Promise<void>;
onSuccess: (tx: string) => Promise<void>;
}
@@ -37,7 +37,7 @@ export type State =
| State.LoadingUriError
| State.Ready
| SelectExchangeState.Selecting
- | SelectExchangeState.NoExchange;
+ | SelectExchangeState.NoExchangeFound;
export namespace State {
export interface Loading {
@@ -46,8 +46,8 @@ export namespace State {
}
export interface LoadingUriError {
- status: "loading-uri";
- error: HookError;
+ status: "error";
+ error: ErrorAlert;
}
export interface BaseInfo {
@@ -64,20 +64,19 @@ export namespace State {
requestAmount: AmountJson;
exchangeUrl: string;
error: undefined;
- operationError?: TalerErrorDetail;
}
}
const viewMapping: StateViewMap<State> = {
loading: Loading,
- "loading-uri": LoadingUriView,
- "no-exchange": NoExchangesView,
+ error: ErrorAlertView,
+ "no-exchange-found": NoExchangesView,
"selecting-exchange": ExchangeSelectionPage,
ready: ReadyView,
};
export const InvoiceCreatePage = compose(
"InvoiceCreatePage",
- (p: Props) => useComponentState(p, wxApi),
+ (p: Props) => useComponentState(p),
viewMapping,
);
diff --git a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/state.ts b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/state.ts
index 6007b5193..daa3ee76d 100644
--- a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/state.ts
+++ b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/state.ts
@@ -15,29 +15,30 @@
*/
/* eslint-disable react-hooks/rules-of-hooks */
-import {
- Amounts,
- TalerErrorDetail,
- TalerProtocolTimestamp,
-} from "@gnu-taler/taler-util";
-import { TalerError, WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { Amounts, TalerProtocolTimestamp } from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { isFuture, parse } from "date-fns";
import { useState } from "preact/hooks";
+import { alertFromError, useAlertContext } from "../../context/alert.js";
+import { useBackendContext } from "../../context/backend.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import { useSelectedExchange } from "../../hooks/useSelectedExchange.js";
import { RecursiveState } from "../../utils/index.js";
-import { wxApi } from "../../wxApi.js";
import { Props, State } from "./index.js";
-export function useComponentState(
- { amount: amountStr, onClose, onSuccess }: Props,
- api: typeof wxApi,
-): RecursiveState<State> {
+export function useComponentState({
+ amount: amountStr,
+ onClose,
+ onSuccess,
+}: Props): RecursiveState<State> {
const amount = Amounts.parseOrThrow(amountStr);
+ const api = useBackendContext();
const hook = useAsyncAsHook(() =>
api.wallet.call(WalletApiOperation.ListExchanges, {}),
);
+ const { i18n } = useTranslationContext();
if (!hook) {
return {
@@ -47,20 +48,27 @@ export function useComponentState(
}
if (hook.hasError) {
return {
- status: "loading-uri",
- error: hook,
+ status: "error",
+ error: alertFromError(
+ i18n,
+ i18n.str`Could not load the list of exchanges`,
+ hook,
+ ),
};
}
+ // if (hook.hasError) {
+ // return {
+ // status: "loading-uri",
+ // error: hook,
+ // };
+ // }
const exchangeList = hook.response.exchanges;
return () => {
const [subject, setSubject] = useState<string | undefined>();
const [timestamp, setTimestamp] = useState<string | undefined>();
-
- const [operationError, setOperationError] = useState<
- TalerErrorDetail | undefined
- >(undefined);
+ const { pushAlertOnError } = useAlertContext();
const selectedExchange = useSelectedExchange({
currency: amount.currency,
@@ -76,7 +84,7 @@ export function useComponentState(
const hook = useAsyncAsHook(async () => {
const resp = await api.wallet.call(
- WalletApiOperation.PreparePeerPullPayment,
+ WalletApiOperation.CheckPeerPullCredit,
{
amount: amountStr,
exchangeBaseUrl: exchange.exchangeBaseUrl,
@@ -91,11 +99,20 @@ export function useComponentState(
error: undefined,
};
}
+
if (hook.hasError) {
return {
- status: "loading-uri",
- error: hook,
+ status: "error",
+ error: alertFromError(
+ i18n,
+ i18n.str`Could not load the invoice status`,
+ hook,
+ ),
};
+ // return {
+ // status: "loading-uri",
+ // error: hook,
+ // };
}
const { amountEffective, amountRaw } = hook.response;
@@ -126,27 +143,20 @@ export function useComponentState(
async function accept(): Promise<void> {
if (!subject || !purse_expiration) return;
- try {
- const resp = await api.wallet.call(
- WalletApiOperation.InitiatePeerPullPayment,
- {
- exchangeBaseUrl: exchange.exchangeBaseUrl,
- partialContractTerms: {
- amount: Amounts.stringify(amount),
- summary: subject,
- purse_expiration,
- },
+
+ const resp = await api.wallet.call(
+ WalletApiOperation.InitiatePeerPullCredit,
+ {
+ exchangeBaseUrl: exchange.exchangeBaseUrl,
+ partialContractTerms: {
+ amount: Amounts.stringify(amount),
+ summary: subject,
+ purse_expiration,
},
- );
+ },
+ );
- onSuccess(resp.transactionId);
- } catch (e) {
- if (e instanceof TalerError) {
- setOperationError(e.errorDetail);
- }
- console.error(e);
- throw Error("error trying to accept");
- }
+ onSuccess(resp.transactionId);
}
const unableToCreate =
!subject || Amounts.isZero(amount) || !purse_expiration;
@@ -158,30 +168,29 @@ export function useComponentState(
subject === undefined
? undefined
: !subject
- ? "Can't be empty"
- : undefined,
+ ? "Can't be empty"
+ : undefined,
value: subject ?? "",
- onInput: async (e) => setSubject(e),
+ onInput: pushAlertOnError(async (e) => setSubject(e)),
},
expiration: {
error: timestampError,
value: timestamp === undefined ? "" : timestamp,
- onInput: async (e) => {
+ onInput: pushAlertOnError(async (e) => {
setTimestamp(e);
- },
+ }),
},
doSelectExchange: selectedExchange.doSelect,
exchangeUrl: exchange.exchangeBaseUrl,
create: {
- onClick: unableToCreate ? undefined : accept,
+ onClick: unableToCreate ? undefined : pushAlertOnError(accept),
},
cancel: {
- onClick: onClose,
+ onClick: pushAlertOnError(onClose),
},
requestAmount,
toBeReceived,
error: undefined,
- operationError,
};
};
}
diff --git a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/stories.tsx b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/stories.tsx
index 05b923c9e..779f130aa 100644
--- a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/stories.tsx
+++ b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/stories.tsx
@@ -19,14 +19,15 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { createExample } from "../../test-utils.js";
+import * as tests from "@gnu-taler/web-util/testing";
+import { nullFunction } from "../../mui/handlers.js";
import { ReadyView } from "./views.js";
export default {
title: "invoice create",
};
-export const Ready = createExample(ReadyView, {
+export const Ready = tests.createExample(ReadyView, {
requestAmount: {
currency: "ARS",
value: 1,
@@ -45,9 +46,7 @@ export const Ready = createExample(ReadyView, {
exchangeUrl: "https://exchange.taler.ar",
subject: {
value: "some subject",
- onInput: async () => {
- null;
- },
+ onInput: nullFunction,
},
create: {},
});
diff --git a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/views.tsx b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/views.tsx
index 0ef5c697e..e2c37fbba 100644
--- a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/views.tsx
+++ b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/views.tsx
@@ -14,52 +14,32 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { format } from "date-fns";
-import { h, VNode } from "preact";
-import { ErrorTalerOperation } from "../../components/ErrorTalerOperation.js";
-import { LoadingError } from "../../components/LoadingError.js";
-import { LogoHeader } from "../../components/LogoHeader.js";
+import { Fragment, h, VNode } from "preact";
import { Part } from "../../components/Part.js";
-import { QR } from "../../components/QR.js";
-import {
- Link,
- SubTitle,
- SvgIcon,
- WalletAction,
-} from "../../components/styled/index.js";
-import { useTranslationContext } from "../../context/translation.js";
+import { TermsOfService } from "../../components/TermsOfService/index.js";
import { Button } from "../../mui/Button.js";
-import { Grid } from "../../mui/Grid.js";
import { TextField } from "../../mui/TextField.js";
-import editIcon from "../../svg/edit_24px.svg";
-import { ExchangeDetails, InvoiceDetails } from "../../wallet/Transaction.js";
+import {
+ ExchangeDetails,
+ getAmountWithFee,
+ InvoiceCreationDetails,
+} from "../../wallet/Transaction.js";
import { State } from "./index.js";
-export function LoadingUriView({ error }: State.LoadingUriError): VNode {
- const { i18n } = useTranslationContext();
-
- return (
- <LoadingError
- title={<i18n.Translate>Could not load</i18n.Translate>}
- error={error}
- />
- );
-}
-
export function ReadyView({
exchangeUrl,
subject,
expiration,
- cancel,
- operationError,
create,
toBeReceived,
requestAmount,
- doSelectExchange,
+ doSelectExchange: _doSelectExchange,
}: State.Ready): VNode {
const { i18n } = useTranslationContext();
- async function oneDayExpiration() {
+ async function oneDayExpiration(): Promise<void> {
if (expiration.onInput) {
expiration.onInput(
format(new Date().getTime() + 1000 * 60 * 60 * 24, "dd/MM/yyyy"),
@@ -67,36 +47,22 @@ export function ReadyView({
}
}
- async function oneWeekExpiration() {
+ async function oneWeekExpiration(): Promise<void> {
if (expiration.onInput) {
expiration.onInput(
format(new Date().getTime() + 1000 * 60 * 60 * 24 * 7, "dd/MM/yyyy"),
);
}
}
- async function _20DaysExpiration() {
+ async function _30DaysExpiration(): Promise<void> {
if (expiration.onInput) {
expiration.onInput(
- format(new Date().getTime() + 1000 * 60 * 60 * 24 * 20, "dd/MM/yyyy"),
+ format(new Date().getTime() + 1000 * 60 * 60 * 24 * 30, "dd/MM/yyyy"),
);
}
}
return (
- <WalletAction>
- <LogoHeader />
- <SubTitle>
- <i18n.Translate>Digital invoice</i18n.Translate>
- </SubTitle>
- {operationError && (
- <ErrorTalerOperation
- title={
- <i18n.Translate>
- Could not finish the invoice creation
- </i18n.Translate>
- }
- error={operationError}
- />
- )}
+ <Fragment>
<section style={{ textAlign: "left" }}>
<Part
title={
@@ -107,13 +73,13 @@ export function ReadyView({
}}
>
<i18n.Translate>Exchange</i18n.Translate>
- <Button onClick={doSelectExchange.onClick} variant="text">
+ {/* <Button onClick={doSelectExchange.onClick} variant="text">
<SvgIcon
title="Edit"
dangerouslySetInnerHTML={{ __html: editIcon }}
color="black"
/>
- </Button>
+ </Button> */}
</div>
}
text={<ExchangeDetails exchange={exchangeUrl} />}
@@ -125,9 +91,7 @@ export function ReadyView({
label="Subject"
variant="filled"
error={subject.error}
- helperText={
- <i18n.Translate>Short description of the invoice</i18n.Translate>
- }
+ helperText={i18n.str`Short description of the invoice`}
required
fullWidth
value={subject.value}
@@ -163,35 +127,29 @@ export function ReadyView({
<Button
variant="outlined"
disabled={!expiration.onInput}
- onClick={_20DaysExpiration}
+ onClick={_30DaysExpiration}
>
- 20 days
+ 30 days
</Button>
</p>
</p>
<Part
- title={<i18n.Translate>Details</i18n.Translate>}
+ title={i18n.str`Details`}
text={
- <InvoiceDetails
- amount={{
- effective: toBeReceived,
- raw: requestAmount,
- }}
+ <InvoiceCreationDetails
+ amount={getAmountWithFee(toBeReceived, requestAmount, "credit")}
/>
}
/>
</section>
<section>
- <Button onClick={create.onClick} variant="contained" color="success">
- <i18n.Translate>Create</i18n.Translate>
- </Button>
- </section>
- <section>
- <Link upperCased onClick={cancel.onClick}>
- <i18n.Translate>Cancel</i18n.Translate>
- </Link>
+ <TermsOfService key="terms" exchangeUrl={exchangeUrl} >
+ <Button onClick={create.onClick} variant="contained" color="success">
+ <i18n.Translate>Create</i18n.Translate>
+ </Button>
+ </TermsOfService>
</section>
- </WalletAction>
+ </Fragment>
);
}
diff --git a/packages/taler-wallet-webextension/src/cta/InvoicePay/index.ts b/packages/taler-wallet-webextension/src/cta/InvoicePay/index.ts
index 6e16b528c..f0cd63fbe 100644
--- a/packages/taler-wallet-webextension/src/cta/InvoicePay/index.ts
+++ b/packages/taler-wallet-webextension/src/cta/InvoicePay/index.ts
@@ -20,13 +20,13 @@ import {
PreparePayResult,
TalerErrorDetail,
} from "@gnu-taler/taler-util";
+import { ErrorAlertView } from "../../components/CurrentAlerts.js";
import { Loading } from "../../components/Loading.js";
-import { HookError } from "../../hooks/useAsyncAsHook.js";
+import { ErrorAlert } from "../../context/alert.js";
import { ButtonHandler } from "../../mui/handlers.js";
import { compose, StateViewMap } from "../../utils/index.js";
-import { wxApi } from "../../wxApi.js";
import { useComponentState } from "./state.js";
-import { LoadingUriView, ReadyView } from "./views.js";
+import { ReadyView } from "./views.js";
export interface Props {
talerPayPullUri: string;
@@ -49,19 +49,19 @@ export namespace State {
}
export interface LoadingUriError {
- status: "loading-uri";
- error: HookError;
+ status: "error";
+ error: ErrorAlert;
}
export interface BaseInfo {
error: undefined;
uri: string;
cancel: ButtonHandler;
- amount: AmountJson;
+ effective: AmountJson;
+ raw: AmountJson;
goToWalletManualWithdraw: (currency: string) => Promise<void>;
summary: string | undefined;
expiration: AbsoluteTime | undefined;
- operationError?: TalerErrorDetail;
payStatus: PreparePayResult;
}
@@ -84,7 +84,7 @@ export namespace State {
const viewMapping: StateViewMap<State> = {
loading: Loading,
- "loading-uri": LoadingUriView,
+ error: ErrorAlertView,
"no-balance-for-currency": ReadyView,
"no-enough-balance": ReadyView,
ready: ReadyView,
@@ -92,6 +92,6 @@ const viewMapping: StateViewMap<State> = {
export const InvoicePayPage = compose(
"InvoicePayPage",
- (p: Props) => useComponentState(p, wxApi),
+ (p: Props) => useComponentState(p),
viewMapping,
);
diff --git a/packages/taler-wallet-webextension/src/cta/InvoicePay/state.ts b/packages/taler-wallet-webextension/src/cta/InvoicePay/state.ts
index c7fb48958..99de03d2d 100644
--- a/packages/taler-wallet-webextension/src/cta/InvoicePay/state.ts
+++ b/packages/taler-wallet-webextension/src/cta/InvoicePay/state.ts
@@ -20,21 +20,27 @@ import {
NotificationType,
PreparePayResult,
PreparePayResultType,
- TalerErrorDetail,
TalerProtocolTimestamp,
} from "@gnu-taler/taler-util";
-import { TalerError, WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-import { useEffect, useState } from "preact/hooks";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { useEffect } from "preact/hooks";
+import { alertFromError, useAlertContext } from "../../context/alert.js";
+import { useBackendContext } from "../../context/backend.js";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
-import { wxApi } from "../../wxApi.js";
import { Props, State } from "./index.js";
-export function useComponentState(
- { talerPayPullUri, onClose, goToWalletManualWithdraw, onSuccess }: Props,
- api: typeof wxApi,
-): State {
+export function useComponentState({
+ talerPayPullUri,
+ onClose,
+ goToWalletManualWithdraw,
+ onSuccess,
+}: Props): State {
+ const api = useBackendContext();
+ const { i18n } = useTranslationContext();
+ const { pushAlertOnError } = useAlertContext();
const hook = useAsyncAsHook(async () => {
- const p2p = await api.wallet.call(WalletApiOperation.CheckPeerPullPayment, {
+ const p2p = await api.wallet.call(WalletApiOperation.PreparePeerPullDebit, {
talerUri: talerPayPullUri,
});
const balance = await api.wallet.call(WalletApiOperation.GetBalances, {});
@@ -43,15 +49,11 @@ export function useComponentState(
useEffect(() =>
api.listener.onUpdateNotification(
- [NotificationType.CoinWithdrawn],
+ [NotificationType.TransactionStateTransition],
hook?.retry,
),
);
- const [operationError, setOperationError] = useState<
- TalerErrorDetail | undefined
- >(undefined);
-
if (!hook) {
return {
status: "loading",
@@ -60,18 +62,31 @@ export function useComponentState(
}
if (hook.hasError) {
return {
- status: "loading-uri",
- error: hook,
+ status: "error",
+ error: alertFromError(
+ i18n,
+ i18n.str`Could not load the transfer payment status`,
+ hook,
+ ),
};
}
+ // if (hook.hasError) {
+ // return {
+ // status: "loading-uri",
+ // error: hook,
+ // };
+ // }
- const { contractTerms, peerPullPaymentIncomingId } = hook.response.p2p;
+ const { contractTerms, transactionId, amountEffective, amountRaw } =
+ hook.response.p2p;
- const amountStr: string = contractTerms?.amount;
+ const amountStr: string = contractTerms.amount;
const amount = Amounts.parseOrThrow(amountStr);
- const summary: string | undefined = contractTerms?.summary;
+ const effective = Amounts.parseOrThrow(amountEffective);
+ const raw = Amounts.parseOrThrow(amountRaw);
+ const summary: string | undefined = contractTerms.summary;
const expiration: TalerProtocolTimestamp | undefined =
- contractTerms?.purse_expiration;
+ contractTerms.purse_expiration;
const foundBalance = hook.response.balance.balances.find(
(b) => Amounts.parseOrThrow(b.available).currency === amount.currency,
@@ -84,7 +99,6 @@ export function useComponentState(
contractTermsHash: "asd",
amountRaw: hook.response.p2p.amount,
amountEffective: hook.response.p2p.amount,
- noncePriv: "",
} as PreparePayResult;
const insufficientBalance: PreparePayResult = {
@@ -94,18 +108,20 @@ export function useComponentState(
contractTerms: {} as any,
amountRaw: hook.response.p2p.amount,
noncePriv: "",
- };
+ } as any; //FIXME: check this interface with new values
const baseResult = {
uri: talerPayPullUri,
cancel: {
- onClick: onClose,
+ onClick: pushAlertOnError(onClose),
},
- amount,
+ effective,
+ raw,
goToWalletManualWithdraw,
summary,
- expiration: expiration ? AbsoluteTime.fromTimestamp(expiration) : undefined,
- operationError,
+ expiration: expiration
+ ? AbsoluteTime.fromProtocolTimestamp(expiration)
+ : undefined,
};
if (!foundBalance) {
@@ -133,21 +149,13 @@ export function useComponentState(
}
async function accept(): Promise<void> {
- try {
- const resp = await api.wallet.call(
- WalletApiOperation.AcceptPeerPullPayment,
- {
- peerPullPaymentIncomingId,
- },
- );
- onSuccess(resp.transactionId);
- } catch (e) {
- if (e instanceof TalerError) {
- setOperationError(e.errorDetail);
- }
- console.error(e);
- throw Error("error trying to accept");
- }
+ const resp = await api.wallet.call(
+ WalletApiOperation.ConfirmPeerPullDebit,
+ {
+ transactionId,
+ },
+ );
+ onSuccess(resp.transactionId);
}
return {
@@ -157,7 +165,7 @@ export function useComponentState(
payStatus: paymentPossible,
balance: foundAmount,
accept: {
- onClick: accept,
+ onClick: pushAlertOnError(accept),
},
};
}
diff --git a/packages/taler-wallet-webextension/src/cta/InvoicePay/stories.tsx b/packages/taler-wallet-webextension/src/cta/InvoicePay/stories.tsx
index 749cd78fc..8993476ea 100644
--- a/packages/taler-wallet-webextension/src/cta/InvoicePay/stories.tsx
+++ b/packages/taler-wallet-webextension/src/cta/InvoicePay/stories.tsx
@@ -19,28 +19,38 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { PreparePayResult, PreparePayResultType } from "@gnu-taler/taler-util";
-import { createExample } from "../../test-utils.js";
+import {
+ AbsoluteTime,
+ PreparePayResult,
+ PreparePayResultType,
+} from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
import { ReadyView } from "./views.js";
export default {
title: "invoice payment",
};
-export const Ready = createExample(ReadyView, {
- amount: {
+export const Ready = tests.createExample(ReadyView, {
+ effective: {
+ currency: "ARS",
+ value: 1,
+ fraction: 0,
+ },
+ raw: {
currency: "ARS",
value: 1,
fraction: 0,
},
summary: "some subject",
+ uri: "taler://pay/merchant.ar/123",
payStatus: {
status: PreparePayResultType.PaymentPossible,
amountEffective: "ARS:1",
} as PreparePayResult,
- expiration: {
- t_ms: new Date().getTime() + 1000 * 60 * 60,
- },
+ expiration: AbsoluteTime.fromMilliseconds(
+ new Date().getTime() + 1000 * 60 * 60,
+ ),
accept: {},
cancel: {},
});
diff --git a/packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx b/packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx
index 8484680bf..547d5ac9a 100644
--- a/packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx
+++ b/packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx
@@ -14,88 +14,47 @@
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 { Amount } from "../../components/Amount.js";
-import { ErrorTalerOperation } from "../../components/ErrorTalerOperation.js";
-import { LoadingError } from "../../components/LoadingError.js";
-import { LogoHeader } from "../../components/LogoHeader.js";
import { Part } from "../../components/Part.js";
-import { Link, SubTitle, WalletAction } from "../../components/styled/index.js";
+import { PaymentButtons } from "../../components/PaymentButtons.js";
import { Time } from "../../components/Time.js";
-import { useTranslationContext } from "../../context/translation.js";
-import { ButtonsSection } from "../Payment/views.js";
+import {
+ getAmountWithFee,
+ InvoicePaymentDetails,
+} from "../../wallet/Transaction.js";
import { State } from "./index.js";
-export function LoadingUriView({ error }: State.LoadingUriError): VNode {
- const { i18n } = useTranslationContext();
-
- return (
- <LoadingError
- title={<i18n.Translate>Could not load</i18n.Translate>}
- error={error}
- />
- );
-}
-
export function ReadyView(
state: State.Ready | State.NoBalanceForCurrency | State.NoEnoughBalance,
): VNode {
const { i18n } = useTranslationContext();
- const {
- operationError,
- summary,
- amount,
- expiration,
- uri,
- status,
- balance,
- payStatus,
- cancel,
- } = state;
+ const { summary, effective, raw, expiration, uri, status, payStatus } = state;
return (
- <WalletAction>
- <LogoHeader />
- <SubTitle>
- <i18n.Translate>Digital invoice</i18n.Translate>
- </SubTitle>
- {operationError && (
- <ErrorTalerOperation
- title={
- <i18n.Translate>
- Could not finish the payment operation
- </i18n.Translate>
- }
- error={operationError}
- />
- )}
+ <Fragment>
<section style={{ textAlign: "left" }}>
+ <Part title={i18n.str`Subject`} text={<div>{summary}</div>} />
<Part
- title={<i18n.Translate>Subject</i18n.Translate>}
- text={<div>{summary}</div>}
- />
- <Part
- title={<i18n.Translate>Amount</i18n.Translate>}
- text={<Amount value={amount} />}
+ title={i18n.str`Details`}
+ text={
+ <InvoicePaymentDetails
+ amount={getAmountWithFee(effective, raw, "debit")}
+ />
+ }
/>
<Part
- title={<i18n.Translate>Valid until</i18n.Translate>}
+ title={i18n.str`Valid until`}
text={<Time timestamp={expiration} format="dd MMMM yyyy, HH:mm" />}
kind="neutral"
/>
</section>
- <ButtonsSection
- amount={amount}
- balance={balance}
+ <PaymentButtons
+ amount={effective}
payStatus={payStatus}
uri={uri}
payHandler={status === "ready" ? state.accept : undefined}
goToWalletManualWithdraw={state.goToWalletManualWithdraw}
/>
- <section>
- <Link upperCased onClick={cancel.onClick}>
- <i18n.Translate>Cancel</i18n.Translate>
- </Link>
- </section>
- </WalletAction>
+ </Fragment>
);
}
diff --git a/packages/taler-wallet-webextension/src/cta/Payment/index.ts b/packages/taler-wallet-webextension/src/cta/Payment/index.ts
index 9bca8f74f..c9bead89c 100644
--- a/packages/taler-wallet-webextension/src/cta/Payment/index.ts
+++ b/packages/taler-wallet-webextension/src/cta/Payment/index.ts
@@ -21,16 +21,16 @@ import {
PreparePayResultInsufficientBalance,
PreparePayResultPaymentPossible,
} from "@gnu-taler/taler-util";
+import { ErrorAlertView } from "../../components/CurrentAlerts.js";
import { Loading } from "../../components/Loading.js";
-import { HookError } from "../../hooks/useAsyncAsHook.js";
+import { ErrorAlert } from "../../context/alert.js";
import { ButtonHandler } from "../../mui/handlers.js";
import { compose, StateViewMap } from "../../utils/index.js";
-import { wxApi } from "../../wxApi.js";
import { useComponentState } from "./state.js";
-import { BaseView, LoadingUriView } from "./views.js";
+import { BaseView } from "./views.js";
export interface Props {
- talerPayUri?: string;
+ talerPayUri: string;
goToWalletManualWithdraw: (amount?: string) => Promise<void>;
cancel: () => Promise<void>;
onSuccess: (tx: string) => Promise<void>;
@@ -50,8 +50,8 @@ export namespace State {
error: undefined;
}
export interface LoadingUriError {
- status: "loading-uri";
- error: HookError;
+ status: "error";
+ error: ErrorAlert;
}
interface BaseInfo {
@@ -87,7 +87,7 @@ export namespace State {
const viewMapping: StateViewMap<State> = {
loading: Loading,
- "loading-uri": LoadingUriView,
+ error: ErrorAlertView,
"no-balance-for-currency": BaseView,
"no-enough-balance": BaseView,
confirmed: BaseView,
@@ -96,6 +96,6 @@ const viewMapping: StateViewMap<State> = {
export const PaymentPage = compose(
"Payment",
- (p: Props) => useComponentState(p, wxApi),
+ (p: Props) => useComponentState(p),
viewMapping,
);
diff --git a/packages/taler-wallet-webextension/src/cta/Payment/state.ts b/packages/taler-wallet-webextension/src/cta/Payment/state.ts
index 970af5b81..4733e5aee 100644
--- a/packages/taler-wallet-webextension/src/cta/Payment/state.ts
+++ b/packages/taler-wallet-webextension/src/cta/Payment/state.ts
@@ -19,20 +19,25 @@ import {
ConfirmPayResultType,
NotificationType,
PreparePayResultType,
- TalerErrorCode,
} from "@gnu-taler/taler-util";
-import { TalerError, WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-import { useEffect, useState } from "preact/hooks";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useEffect } from "preact/hooks";
+import { alertFromError, useAlertContext } from "../../context/alert.js";
+import { useBackendContext } from "../../context/backend.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import { ButtonHandler } from "../../mui/handlers.js";
-import { wxApi } from "../../wxApi.js";
import { Props, State } from "./index.js";
-export function useComponentState(
- { talerPayUri, cancel, goToWalletManualWithdraw, onSuccess }: Props,
- api: typeof wxApi,
-): State {
- const [payErrMsg, setPayErrMsg] = useState<TalerError | undefined>(undefined);
+export function useComponentState({
+ talerPayUri,
+ cancel,
+ goToWalletManualWithdraw,
+ onSuccess,
+}: Props): State {
+ const { pushAlertOnError } = useAlertContext();
+ const api = useBackendContext();
+ const { i18n } = useTranslationContext();
const hook = useAsyncAsHook(async () => {
if (!talerPayUri) throw Error("ERROR_NO-URI-FOR-PAYMENT");
@@ -49,7 +54,7 @@ export function useComponentState(
useEffect(
() =>
api.listener.onUpdateNotification(
- [NotificationType.CoinWithdrawn],
+ [NotificationType.TransactionStateTransition],
hook?.retry,
),
[hook],
@@ -77,10 +82,20 @@ export function useComponentState(
if (!hook) return { status: "loading", error: undefined };
if (hook.hasError) {
return {
- status: "loading-uri",
- error: hook,
+ status: "error",
+ error: alertFromError(
+ i18n,
+ i18n.str`Could not load the payment and balance status`,
+ hook,
+ ),
};
}
+ // if (hook.hasError) {
+ // return {
+ // status: "loading-uri",
+ // error: hook,
+ // };
+ // }
const { payStatus } = hook.response;
const amount = Amounts.parseOrThrow(payStatus.amountRaw);
@@ -127,43 +142,25 @@ export function useComponentState(
}
async function doPayment(): Promise<void> {
- try {
- if (payStatus.status !== "payment-possible") {
- throw TalerError.fromUncheckedDetail({
- code: TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR,
- hint: `payment is not possible: ${payStatus.status}`,
- });
- }
- const res = await api.wallet.call(WalletApiOperation.ConfirmPay, {
- proposalId: payStatus.proposalId,
- });
- // handle confirm pay
- if (res.type !== ConfirmPayResultType.Done) {
- throw TalerError.fromUncheckedDetail({
- code: TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR,
- hint: `could not confirm payment`,
- payResult: res,
- });
- }
- const fu = res.contractTerms.fulfillment_url;
- if (fu) {
- if (typeof window !== "undefined") {
- document.location.href = fu;
- } else {
- console.log(`should d to ${fu}`);
- }
- }
+ const res = await api.wallet.call(WalletApiOperation.ConfirmPay, {
+ proposalId: payStatus.proposalId,
+ });
+ // handle confirm pay
+ if (res.type !== ConfirmPayResultType.Done) {
onSuccess(res.transactionId);
- } catch (e) {
- if (e instanceof TalerError) {
- setPayErrMsg(e);
+ return;
+ }
+ const fu = res.contractTerms.fulfillment_url;
+ if (fu) {
+ if (typeof window !== "undefined") {
+ document.location.href = fu;
}
}
+ onSuccess(res.transactionId);
}
const payHandler: ButtonHandler = {
- onClick: payErrMsg ? undefined : doPayment,
- error: payErrMsg,
+ onClick: pushAlertOnError(doPayment),
};
// (payStatus.status === PreparePayResultType.PaymentPossible)
diff --git a/packages/taler-wallet-webextension/src/cta/Payment/stories.tsx b/packages/taler-wallet-webextension/src/cta/Payment/stories.tsx
index 28fcd8db7..d03f48746 100644
--- a/packages/taler-wallet-webextension/src/cta/Payment/stories.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Payment/stories.tsx
@@ -20,14 +20,17 @@
*/
import {
+ AmountString,
Amounts,
MerchantContractTerms as ContractTerms,
PreparePayResultType,
+ TransactionIdStr,
} from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
+import beer from "../../../static-dev/beer.png";
import merchantIcon from "../../../static-dev/merchant-icon.jpeg";
-import { createExample } from "../../test-utils.js";
+import { nullFunction } from "../../mui/handlers.js";
import { BaseView } from "./views.js";
-import beer from "../../../static-dev/beer.png";
export default {
title: "payment",
@@ -35,17 +38,33 @@ export default {
argTypes: {},
};
-export const NoBalance = createExample(BaseView, {
- status: "no-balance-for-currency",
+export const NoEnoughBalanceAvailable = tests.createExample(BaseView, {
+ status: "no-enough-balance",
error: undefined,
amount: Amounts.parseOrThrow("USD:10"),
- balance: undefined,
+ balance: {
+ currency: "USD",
+ fraction: 40000000,
+ value: 12,
+ },
uri: "",
payStatus: {
+ transactionId: " " as TransactionIdStr,
status: PreparePayResultType.InsufficientBalance,
+ balanceDetails: {
+ amountRequested: "USD:10" as AmountString,
+ balanceAvailable: "USD:9" as AmountString,
+ balanceMaterial: "USD:9" as AmountString,
+ balanceAgeAcceptable: "USD:9" as AmountString,
+ balanceReceiverAcceptable: "USD:9" as AmountString,
+ balanceReceiverDepositable: "USD:9" as AmountString,
+ maxEffectiveSpendAmount: "USD:9.5" as AmountString,
+ balanceExchangeDepositable: "USD:9.5" as AmountString,
+ perExchange: {},
+ },
talerUri: "taler://pay/..",
- noncePriv: "",
+
proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
contractTerms: {
merchant: {
@@ -57,25 +76,37 @@ export const NoBalance = createExample(BaseView, {
summary: "some beers",
amount: "USD:10",
} as Partial<ContractTerms> as any,
- amountRaw: "USD:10",
+ amountRaw: "USD:10" as AmountString,
},
});
-export const NoEnoughBalance = createExample(BaseView, {
+export const NoEnoughBalanceMaterial = tests.createExample(BaseView, {
status: "no-enough-balance",
error: undefined,
amount: Amounts.parseOrThrow("USD:10"),
balance: {
currency: "USD",
fraction: 40000000,
- value: 9,
+ value: 12,
},
uri: "",
payStatus: {
+ transactionId: " " as TransactionIdStr,
status: PreparePayResultType.InsufficientBalance,
+ balanceDetails: {
+ amountRequested: "USD:10" as AmountString,
+ balanceAvailable: "USD:10" as AmountString,
+ balanceMaterial: "USD:9" as AmountString,
+ balanceAgeAcceptable: "USD:9" as AmountString,
+ balanceReceiverAcceptable: "USD:9" as AmountString,
+ balanceReceiverDepositable: "USD:0" as AmountString,
+ maxEffectiveSpendAmount: "USD:9.5" as AmountString,
+ balanceExchangeDepositable: "USD:9.5" as AmountString,
+ perExchange: {},
+ },
talerUri: "taler://pay/..",
- noncePriv: "",
+
proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
contractTerms: {
merchant: {
@@ -85,27 +116,39 @@ export const NoEnoughBalance = createExample(BaseView, {
email: "contact@merchant.taler",
},
summary: "some beers",
- amount: "USD:10",
+ amount: "USD:10" as AmountString,
} as Partial<ContractTerms> as any,
- amountRaw: "USD:10",
+ amountRaw: "USD:10" as AmountString,
},
});
-export const EnoughBalanceButRestricted = createExample(BaseView, {
+export const NoEnoughBalanceAgeAcceptable = tests.createExample(BaseView, {
status: "no-enough-balance",
error: undefined,
amount: Amounts.parseOrThrow("USD:10"),
balance: {
currency: "USD",
fraction: 40000000,
- value: 19,
+ value: 12,
},
uri: "",
payStatus: {
+ transactionId: " " as TransactionIdStr,
status: PreparePayResultType.InsufficientBalance,
+ balanceDetails: {
+ amountRequested: "USD:10" as AmountString,
+ balanceAvailable: "USD:10" as AmountString,
+ balanceMaterial: "USD:10" as AmountString,
+ balanceAgeAcceptable: "USD:9" as AmountString,
+ balanceReceiverAcceptable: "USD:9" as AmountString,
+ balanceReceiverDepositable: "USD:9" as AmountString,
+ maxEffectiveSpendAmount: "USD:9.5" as AmountString,
+ balanceExchangeDepositable: "USD:9.5" as AmountString,
+ perExchange: {},
+ },
talerUri: "taler://pay/..",
- noncePriv: "",
+
proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
contractTerms: {
merchant: {
@@ -114,14 +157,145 @@ export const EnoughBalanceButRestricted = createExample(BaseView, {
website: "https://www.themerchant.taler",
email: "contact@merchant.taler",
},
+ minimum_age: 18,
summary: "some beers",
amount: "USD:10",
} as Partial<ContractTerms> as any,
- amountRaw: "USD:10",
+ amountRaw: "USD:10" as AmountString,
+ },
+});
+
+export const NoEnoughBalanceMerchantAcceptable = tests.createExample(BaseView, {
+ status: "no-enough-balance",
+ error: undefined,
+ amount: Amounts.parseOrThrow("USD:10"),
+ balance: {
+ currency: "USD",
+ fraction: 40000000,
+ value: 12,
+ },
+
+ uri: "",
+ payStatus: {
+ transactionId: " " as TransactionIdStr,
+ status: PreparePayResultType.InsufficientBalance,
+ balanceDetails: {
+ amountRequested: "USD:10" as AmountString,
+ balanceAvailable: "USD:10" as AmountString,
+ balanceMaterial: "USD:10" as AmountString,
+ balanceAgeAcceptable: "USD:10" as AmountString,
+ balanceReceiverAcceptable: "USD:9" as AmountString,
+ balanceReceiverDepositable: "USD:9" as AmountString,
+ maxEffectiveSpendAmount: "USD:9.5" as AmountString,
+ balanceExchangeDepositable: "USD:9.5" as AmountString,
+ perExchange: {},
+ },
+ talerUri: "taler://pay/..",
+
+ proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
+ contractTerms: {
+ merchant: {
+ name: "the merchant",
+ logo: merchantIcon,
+ website: "https://www.themerchant.taler",
+ email: "contact@merchant.taler",
+ },
+ summary: "some beers",
+ amount: "USD:10" as AmountString,
+ } as Partial<ContractTerms> as any,
+ amountRaw: "USD:10" as AmountString,
+ },
+});
+
+export const NoEnoughBalanceMerchantDepositable = tests.createExample(
+ BaseView,
+ {
+ status: "no-enough-balance",
+ error: undefined,
+ amount: Amounts.parseOrThrow("USD:10"),
+ balance: {
+ currency: "USD",
+ fraction: 40000000,
+ value: 12,
+ },
+
+ uri: "",
+ payStatus: {
+ transactionId: " " as TransactionIdStr,
+ status: PreparePayResultType.InsufficientBalance,
+ balanceDetails: {
+ amountRequested: "USD:10" as AmountString,
+ balanceAvailable: "USD:10" as AmountString,
+ balanceMaterial: "USD:10" as AmountString,
+ balanceAgeAcceptable: "USD:10" as AmountString,
+ balanceReceiverAcceptable: "USD:10" as AmountString,
+ balanceReceiverDepositable: "USD:9" as AmountString,
+ maxEffectiveSpendAmount: "USD:9.5" as AmountString,
+ balanceExchangeDepositable: "USD:9.5" as AmountString,
+ perExchange: {},
+ },
+ talerUri: "taler://pay/..",
+
+ proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
+ contractTerms: {
+ merchant: {
+ name: "the merchant",
+ logo: merchantIcon,
+ website: "https://www.themerchant.taler",
+ email: "contact@merchant.taler",
+ },
+ summary: "some beers",
+ amount: "USD:10" as AmountString,
+ } as Partial<ContractTerms> as any,
+ amountRaw: "USD:10" as AmountString,
+ },
+ },
+);
+
+export const NoEnoughBalanceFeeGap = tests.createExample(BaseView, {
+ status: "no-enough-balance",
+ error: undefined,
+ amount: Amounts.parseOrThrow("USD:10"),
+ balance: {
+ currency: "USD",
+ fraction: 40000000,
+ value: 12,
+ },
+
+ uri: "",
+ payStatus: {
+ transactionId: " " as TransactionIdStr,
+ status: PreparePayResultType.InsufficientBalance,
+ balanceDetails: {
+ amountRequested: "USD:10" as AmountString,
+ balanceAvailable: "USD:10" as AmountString,
+ balanceMaterial: "USD:10" as AmountString,
+ balanceAgeAcceptable: "USD:10" as AmountString,
+ balanceReceiverAcceptable: "USD:10" as AmountString,
+ balanceReceiverDepositable: "USD:10" as AmountString,
+ maxEffectiveSpendAmount: "USD:9.5" as AmountString,
+ balanceExchangeDepositable: "USD:9.5" as AmountString,
+ perExchange: {},
+ },
+ talerUri: "taler://pay/..",
+
+ proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
+ contractTerms: {
+ merchant: {
+ name: "the merchant",
+ logo: merchantIcon,
+ website: "https://www.themerchant.taler",
+ email: "contact@merchant.taler",
+ },
+ minimum_age: 18,
+ summary: "some beers",
+ amount: "USD:10" as AmountString,
+ } as Partial<ContractTerms> as any,
+ amountRaw: "USD:10" as AmountString,
},
});
-export const PaymentPossible = createExample(BaseView, {
+export const PaymentPossible = tests.createExample(BaseView, {
status: "ready",
error: undefined,
amount: Amounts.parseOrThrow("USD:10"),
@@ -131,18 +305,17 @@ export const PaymentPossible = createExample(BaseView, {
value: 11,
},
payHandler: {
- onClick: async () => {
- null;
- },
+ onClick: nullFunction,
},
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
payStatus: {
+ transactionId: " " as TransactionIdStr,
status: PreparePayResultType.PaymentPossible,
talerUri: "taler://pay/..",
- amountEffective: "USD:10",
- amountRaw: "USD:10",
- noncePriv: "",
+ amountEffective: "USD:10" as AmountString,
+ amountRaw: "USD:10" as AmountString,
+
contractTerms: {
nonce: "123213123",
merchant: {
@@ -154,7 +327,7 @@ export const PaymentPossible = createExample(BaseView, {
pay_deadline: {
t_s: new Date().getTime() / 1000 + 60 * 60 * 3,
},
- amount: "USD:10",
+ amount: "USD:10" as AmountString,
summary: "some beers",
} as Partial<ContractTerms> as any,
contractTermsHash: "123456",
@@ -162,7 +335,7 @@ export const PaymentPossible = createExample(BaseView, {
},
});
-export const PaymentPossibleWithFee = createExample(BaseView, {
+export const PaymentPossibleWithFee = tests.createExample(BaseView, {
status: "ready",
error: undefined,
amount: Amounts.parseOrThrow("USD:10"),
@@ -172,18 +345,17 @@ export const PaymentPossibleWithFee = createExample(BaseView, {
value: 11,
},
payHandler: {
- onClick: async () => {
- null;
- },
+ onClick: nullFunction,
},
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
payStatus: {
+ transactionId: " " as TransactionIdStr,
status: PreparePayResultType.PaymentPossible,
talerUri: "taler://pay/..",
- amountEffective: "USD:10.20",
- amountRaw: "USD:10",
- noncePriv: "",
+ amountEffective: "USD:10.20" as AmountString,
+ amountRaw: "USD:10" as AmountString,
+
contractTerms: {
nonce: "123213123",
merchant: {
@@ -192,7 +364,7 @@ export const PaymentPossibleWithFee = createExample(BaseView, {
website: "https://www.themerchant.taler",
email: "contact@merchant.taler",
},
- amount: "USD:10",
+ amount: "USD:10" as AmountString,
summary: "some beers",
} as Partial<ContractTerms> as any,
contractTermsHash: "123456",
@@ -200,7 +372,7 @@ export const PaymentPossibleWithFee = createExample(BaseView, {
},
});
-export const TicketWithAProductList = createExample(BaseView, {
+export const TicketWithAProductList = tests.createExample(BaseView, {
status: "ready",
error: undefined,
amount: Amounts.parseOrThrow("USD:10"),
@@ -210,18 +382,17 @@ export const TicketWithAProductList = createExample(BaseView, {
value: 11,
},
payHandler: {
- onClick: async () => {
- null;
- },
+ onClick: nullFunction,
},
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
payStatus: {
+ transactionId: " " as TransactionIdStr,
status: PreparePayResultType.PaymentPossible,
talerUri: "taler://pay/..",
- amountEffective: "USD:10.20",
- amountRaw: "USD:10",
- noncePriv: "",
+ amountEffective: "USD:10.20" as AmountString,
+ amountRaw: "USD:10" as AmountString,
+
contractTerms: {
nonce: "123213123",
merchant: {
@@ -257,7 +428,7 @@ export const TicketWithAProductList = createExample(BaseView, {
},
});
-export const TicketWithShipping = createExample(BaseView, {
+export const TicketWithShipping = tests.createExample(BaseView, {
status: "ready",
error: undefined,
amount: Amounts.parseOrThrow("USD:10"),
@@ -267,18 +438,17 @@ export const TicketWithShipping = createExample(BaseView, {
value: 11,
},
payHandler: {
- onClick: async () => {
- null;
- },
+ onClick: nullFunction,
},
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
payStatus: {
+ transactionId: " " as TransactionIdStr,
status: PreparePayResultType.PaymentPossible,
talerUri: "taler://pay/..",
- amountEffective: "USD:10.20",
- amountRaw: "USD:10",
- noncePriv: "",
+ amountEffective: "USD:10.20" as AmountString,
+ amountRaw: "USD:10" as AmountString,
+
contractTerms: {
nonce: "123213123",
merchant: {
@@ -309,7 +479,7 @@ export const TicketWithShipping = createExample(BaseView, {
},
});
-export const AlreadyConfirmedByOther = createExample(BaseView, {
+export const AlreadyConfirmedByOther = tests.createExample(BaseView, {
status: "confirmed",
error: undefined,
amount: Amounts.parseOrThrow("USD:10"),
@@ -321,10 +491,11 @@ export const AlreadyConfirmedByOther = createExample(BaseView, {
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
payStatus: {
+ transactionId: " " as TransactionIdStr,
status: PreparePayResultType.AlreadyConfirmed,
talerUri: "taler://pay/..",
- amountEffective: "USD:10",
- amountRaw: "USD:10",
+ amountEffective: "USD:10" as AmountString,
+ amountRaw: "USD:10" as AmountString,
contractTerms: {
merchant: {
name: "the merchant",
diff --git a/packages/taler-wallet-webextension/src/cta/Payment/test.ts b/packages/taler-wallet-webextension/src/cta/Payment/test.ts
index b02ac6274..5847cc833 100644
--- a/packages/taler-wallet-webextension/src/cta/Payment/test.ts
+++ b/packages/taler-wallet-webextension/src/cta/Payment/test.ts
@@ -20,6 +20,7 @@
*/
import {
+ AmountString,
Amounts,
ConfirmPayResult,
ConfirmPayResultType,
@@ -27,57 +28,56 @@ import {
PreparePayResultInsufficientBalance,
PreparePayResultPaymentPossible,
PreparePayResultType,
+ ScopeType,
+ TransactionMajorState,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { expect } from "chai";
-import { mountHook, nullFunction } from "../../test-utils.js";
+import * as tests from "@gnu-taler/web-util/testing";
+import { ErrorAlert, useAlertContext } from "../../context/alert.js";
+import { nullFunction } from "../../mui/handlers.js";
import { createWalletApiMock } from "../../test-utils.js";
import { useComponentState } from "./state.js";
describe("Payment CTA states", () => {
it("should tell the user that the URI is missing", async () => {
- const { handler, mock } = createWalletApiMock();
+ const { handler, TestingContext } = createWalletApiMock();
const props = {
- talerPayUri: undefined,
+ talerPayUri: "",
cancel: nullFunction,
goToWalletManualWithdraw: nullFunction,
- onSuccess: async () => {
- null;
- },
+ onSuccess: nullFunction,
};
- const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
- mountHook(() => useComponentState(props, mock));
-
- {
- const { status, error } = pullLastResultOrThrow();
- expect(status).equals("loading");
- expect(error).undefined;
- }
- expect(await waitForStateUpdate()).true;
-
- {
- const { status, error } = pullLastResultOrThrow();
-
- expect(status).equals("loading-uri");
- if (error === undefined) expect.fail();
- expect(error.hasError).true;
- expect(error.operational).false;
- }
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentState,
+ props,
+ [
+ ({ status, error }) => {
+ expect(status).equals("loading");
+ expect(error).undefined;
+ },
+ ({ status, error }) => {
+ expect(status).equals("error");
+ if (error === undefined) expect.fail();
+ // expect(error.hasError).true;
+ // expect(error.operational).false;
+ },
+ ],
+ TestingContext,
+ );
- await assertNoPendingUpdate();
+ expect(hookBehavior).deep.equal({ result: "ok" });
expect(handler.getCallingQueueState()).eq("empty");
});
it("should response with no balance", async () => {
- const { handler, mock } = createWalletApiMock();
+ const { handler, TestingContext } = createWalletApiMock();
const props = {
talerPayUri: "taller://pay",
cancel: nullFunction,
goToWalletManualWithdraw: nullFunction,
- onSuccess: async () => {
- null;
- },
+ onSuccess: nullFunction,
};
handler.addWalletCallResponse(
@@ -94,41 +94,39 @@ describe("Payment CTA states", () => {
{ balances: [] },
);
- const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
- mountHook(() => useComponentState(props, mock));
-
- {
- const { status, error } = pullLastResultOrThrow();
- expect(status).equals("loading");
- expect(error).undefined;
- }
-
- expect(await waitForStateUpdate()).true;
-
- {
- const r = pullLastResultOrThrow();
- if (r.status !== "no-balance-for-currency") {
- expect(r).eq({});
- return;
- }
- expect(r.balance).undefined;
- expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:10"));
- }
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentState,
+ props,
+ [
+ ({ status, error }) => {
+ expect(status).equals("loading");
+ expect(error).undefined;
+ },
+ (state) => {
+ if (state.status !== "no-balance-for-currency") {
+ expect(state).eq({});
+ return;
+ }
+ expect(state.balance).undefined;
+ expect(state.amount).deep.equal(Amounts.parseOrThrow("USD:10"));
+ },
+ ],
+ TestingContext,
+ );
- await assertNoPendingUpdate();
+ expect(hookBehavior).deep.equal({ result: "ok" });
expect(handler.getCallingQueueState()).eq("empty");
});
it("should not be able to pay if there is no enough balance", async () => {
- const { handler, mock } = createWalletApiMock();
+ const { handler, TestingContext } = createWalletApiMock();
const props = {
talerPayUri: "taller://pay",
cancel: nullFunction,
goToWalletManualWithdraw: nullFunction,
- onSuccess: async () => {
- null;
- },
+ onSuccess: nullFunction,
};
+
handler.addWalletCallResponse(
WalletApiOperation.PreparePayForUri,
undefined,
@@ -143,48 +141,52 @@ describe("Payment CTA states", () => {
{
balances: [
{
- available: "USD:5",
+ flags: [],
+ available: "USD:5" as AmountString,
hasPendingTransactions: false,
- pendingIncoming: "USD:0",
- pendingOutgoing: "USD:0",
+ pendingIncoming: "USD:0" as AmountString,
+ pendingOutgoing: "USD:0" as AmountString,
requiresUserInput: false,
+ scopeInfo: {
+ currency: "USD",
+ type: ScopeType.Auditor,
+ url: "",
+ },
},
],
},
);
- const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
- mountHook(() => useComponentState(props, mock));
-
- {
- const { status, error } = pullLastResultOrThrow();
- expect(status).equals("loading");
- expect(error).undefined;
- }
-
- expect(await waitForStateUpdate()).true;
-
- {
- const r = pullLastResultOrThrow();
- if (r.status !== "no-enough-balance") expect.fail();
- expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:5"));
- expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:10"));
- }
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentState,
+ props,
+ [
+ ({ status, error }) => {
+ expect(status).equals("loading");
+ expect(error).undefined;
+ },
+ (state) => {
+ if (state.status !== "no-enough-balance") expect.fail();
+ expect(state.balance).deep.equal(Amounts.parseOrThrow("USD:5"));
+ expect(state.amount).deep.equal(Amounts.parseOrThrow("USD:10"));
+ },
+ ],
+ TestingContext,
+ );
- await assertNoPendingUpdate();
+ expect(hookBehavior).deep.equal({ result: "ok" });
expect(handler.getCallingQueueState()).eq("empty");
});
it("should be able to pay (without fee)", async () => {
- const { handler, mock } = createWalletApiMock();
+ const { handler, TestingContext } = createWalletApiMock();
const props = {
talerPayUri: "taller://pay",
cancel: nullFunction,
goToWalletManualWithdraw: nullFunction,
- onSuccess: async () => {
- null;
- },
+ onSuccess: nullFunction,
};
+
handler.addWalletCallResponse(
WalletApiOperation.PreparePayForUri,
undefined,
@@ -200,51 +202,55 @@ describe("Payment CTA states", () => {
{
balances: [
{
- available: "USD:15",
+ flags: [],
+ available: "USD:15" as AmountString,
hasPendingTransactions: false,
- pendingIncoming: "USD:0",
- pendingOutgoing: "USD:0",
+ pendingIncoming: "USD:0" as AmountString,
+ pendingOutgoing: "USD:0" as AmountString,
requiresUserInput: false,
+ scopeInfo: {
+ currency: "USD",
+ type: ScopeType.Auditor,
+ url: "",
+ },
},
],
},
);
- const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
- mountHook(() => useComponentState(props, mock));
-
- {
- const { status, error } = pullLastResultOrThrow();
- expect(status).equals("loading");
- expect(error).undefined;
- }
-
- expect(await waitForStateUpdate()).true;
-
- {
- const r = pullLastResultOrThrow();
- if (r.status !== "ready") {
- expect(r).eq({});
- return;
- }
- expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
- expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:10"));
- expect(r.payHandler.onClick).not.undefined;
- }
-
- await assertNoPendingUpdate();
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentState,
+ props,
+ [
+ ({ status, error }) => {
+ expect(status).equals("loading");
+ expect(error).undefined;
+ },
+ (state) => {
+ if (state.status !== "ready") {
+ expect(state).eq({});
+ return;
+ }
+ expect(state.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
+ expect(state.amount).deep.equal(Amounts.parseOrThrow("USD:10"));
+ expect(state.payHandler.onClick).not.undefined;
+ },
+ ],
+ TestingContext,
+ );
+
+ expect(hookBehavior).deep.equal({ result: "ok" });
expect(handler.getCallingQueueState()).eq("empty");
});
it("should be able to pay (with fee)", async () => {
- const { handler, mock } = createWalletApiMock();
+ const { handler, TestingContext } = createWalletApiMock();
const props = {
talerPayUri: "taller://pay",
cancel: nullFunction,
goToWalletManualWithdraw: nullFunction,
- onSuccess: async () => {
- null;
- },
+ onSuccess: nullFunction,
};
+
handler.addWalletCallResponse(
WalletApiOperation.PreparePayForUri,
undefined,
@@ -260,48 +266,52 @@ describe("Payment CTA states", () => {
{
balances: [
{
- available: "USD:15",
+ flags: [],
+ available: "USD:15" as AmountString,
hasPendingTransactions: false,
- pendingIncoming: "USD:0",
- pendingOutgoing: "USD:0",
+ pendingIncoming: "USD:0" as AmountString,
+ pendingOutgoing: "USD:0" as AmountString,
requiresUserInput: false,
+ scopeInfo: {
+ currency: "USD",
+ type: ScopeType.Auditor,
+ url: "",
+ },
},
],
},
);
- const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
- mountHook(() => useComponentState(props, mock));
-
- {
- const { status, error } = pullLastResultOrThrow();
- expect(status).equals("loading");
- expect(error).undefined;
- }
-
- expect(await waitForStateUpdate()).true;
-
- {
- const r = pullLastResultOrThrow();
- if (r.status !== "ready") expect.fail();
- expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
- expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
- expect(r.payHandler.onClick).not.undefined;
- }
-
- await assertNoPendingUpdate();
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentState,
+ props,
+ [
+ ({ status, error }) => {
+ expect(status).equals("loading");
+ expect(error).undefined;
+ },
+ (state) => {
+ if (state.status !== "ready") expect.fail();
+ expect(state.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
+ expect(state.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
+ expect(state.payHandler.onClick).not.undefined;
+ },
+ ],
+ TestingContext,
+ );
+
+ expect(hookBehavior).deep.equal({ result: "ok" });
expect(handler.getCallingQueueState()).eq("empty");
});
it("should get confirmation done after pay successfully", async () => {
- const { handler, mock } = createWalletApiMock();
+ const { handler, TestingContext } = createWalletApiMock();
const props = {
talerPayUri: "taller://pay",
cancel: nullFunction,
goToWalletManualWithdraw: nullFunction,
- onSuccess: async () => {
- null;
- },
+ onSuccess: nullFunction,
};
+
handler.addWalletCallResponse(
WalletApiOperation.PreparePayForUri,
undefined,
@@ -318,11 +328,17 @@ describe("Payment CTA states", () => {
{
balances: [
{
- available: "USD:15",
+ flags: [],
+ available: "USD:15" as AmountString,
hasPendingTransactions: false,
- pendingIncoming: "USD:0",
- pendingOutgoing: "USD:0",
+ pendingIncoming: "USD:0" as AmountString,
+ pendingOutgoing: "USD:0" as AmountString,
requiresUserInput: false,
+ scopeInfo: {
+ currency: "USD",
+ type: ScopeType.Auditor,
+ url: "",
+ },
},
],
},
@@ -332,35 +348,34 @@ describe("Payment CTA states", () => {
contractTerms: {},
} as ConfirmPayResult);
- const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
- mountHook(() => useComponentState(props, mock));
-
- {
- const { status, error } = pullLastResultOrThrow();
- expect(status).equals("loading");
- expect(error).undefined;
- }
-
- expect(await waitForStateUpdate()).true;
-
- {
- const r = pullLastResultOrThrow();
- if (r.status !== "ready") {
- expect(r).eq({});
- return;
- }
- expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
- expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
- if (r.payHandler.onClick === undefined) expect.fail();
- r.payHandler.onClick();
- }
-
- await assertNoPendingUpdate();
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentState,
+ props,
+ [
+ ({ status, error }) => {
+ expect(status).equals("loading");
+ expect(error).undefined;
+ },
+ (state) => {
+ if (state.status !== "ready") {
+ expect(state).eq({});
+ return;
+ }
+ expect(state.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
+ expect(state.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
+ if (state.payHandler.onClick === undefined) expect.fail();
+ state.payHandler.onClick();
+ },
+ ],
+ TestingContext,
+ );
+
+ expect(hookBehavior).deep.equal({ result: "ok" });
expect(handler.getCallingQueueState()).eq("empty");
});
it("should not stay in ready state after pay with error", async () => {
- const { handler, mock } = createWalletApiMock();
+ const { handler, TestingContext } = createWalletApiMock();
const props = {
talerPayUri: "taller://pay",
cancel: nullFunction,
@@ -383,11 +398,17 @@ describe("Payment CTA states", () => {
{
balances: [
{
- available: "USD:15",
+ flags: [],
+ available: "USD:15" as AmountString,
hasPendingTransactions: false,
- pendingIncoming: "USD:0",
- pendingOutgoing: "USD:0",
+ pendingIncoming: "USD:0" as AmountString,
+ pendingOutgoing: "USD:0" as AmountString,
requiresUserInput: false,
+ scopeInfo: {
+ currency: "USD",
+ type: ScopeType.Auditor,
+ url: "",
+ },
},
],
},
@@ -397,62 +418,57 @@ describe("Payment CTA states", () => {
lastError: { code: 1 },
} as ConfirmPayResult);
- const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
- mountHook(() => useComponentState(props, mock));
-
- {
- const { status, error } = pullLastResultOrThrow();
- expect(status).equals("loading");
- expect(error).undefined;
- }
-
- expect(await waitForStateUpdate()).true;
-
- {
- const r = pullLastResultOrThrow();
- if (r.status !== "ready") expect.fail();
- expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
- expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
- // expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
- if (r.payHandler.onClick === undefined) expect.fail();
- r.payHandler.onClick();
- }
-
- expect(await waitForStateUpdate()).true;
-
- {
- const r = pullLastResultOrThrow();
- if (r.status !== "ready") expect.fail();
- expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
- expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
- // expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
- expect(r.payHandler.onClick).undefined;
- if (r.payHandler.error === undefined) expect.fail();
- //FIXME: error message here is bad
- expect(r.payHandler.error.errorDetail.hint).eq(
- "could not confirm payment",
- );
- expect(r.payHandler.error.errorDetail.payResult).deep.equal({
- type: ConfirmPayResultType.Pending,
- lastError: { code: 1 },
- });
- }
-
- await assertNoPendingUpdate();
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const state = useComponentState(props);
+ // const { alerts } = useAlertContext();
+ return { ...state, alerts: {} };
+ },
+ {},
+ [
+ ({ status, error }) => {
+ expect(status).equals("loading");
+ expect(error).undefined;
+ },
+ (state) => {
+ if (state.status !== "ready") expect.fail();
+ expect(state.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
+ expect(state.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
+ // expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
+ if (state.payHandler.onClick === undefined) expect.fail();
+ state.payHandler.onClick();
+ },
+ // (state) => {
+ // if (state.status !== "ready") expect.fail();
+ // expect(state.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
+ // expect(state.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
+
+ // // FIXME: check that the error is pushed to the alertContext
+ // // expect(state.alerts.length).eq(1);
+ // // const alert = state.alerts[0]
+ // // if (alert.type !== "error") expect.fail();
+
+ // // expect(alert.cause.errorDetail.payResult).deep.equal({
+ // // type: ConfirmPayResultType.Pending,
+ // // lastError: { code: 1 },
+ // // });
+ // },
+ ],
+ TestingContext,
+ );
+ expect(hookBehavior).deep.equal({ result: "ok" });
expect(handler.getCallingQueueState()).eq("empty");
});
it("should update balance if a coins is withdraw", async () => {
- const { handler, mock } = createWalletApiMock();
+ const { handler, TestingContext } = createWalletApiMock();
const props = {
talerPayUri: "taller://pay",
cancel: nullFunction,
goToWalletManualWithdraw: nullFunction,
- onSuccess: async () => {
- null;
- },
+ onSuccess: nullFunction,
};
handler.addWalletCallResponse(
@@ -471,11 +487,17 @@ describe("Payment CTA states", () => {
{
balances: [
{
- available: "USD:10",
+ flags: [],
+ available: "USD:10" as AmountString,
hasPendingTransactions: false,
- pendingIncoming: "USD:0",
- pendingOutgoing: "USD:0",
+ pendingIncoming: "USD:0" as AmountString,
+ pendingOutgoing: "USD:0" as AmountString,
requiresUserInput: false,
+ scopeInfo: {
+ currency: "USD",
+ type: ScopeType.Auditor,
+ url: "",
+ },
},
],
},
@@ -497,56 +519,58 @@ describe("Payment CTA states", () => {
{
balances: [
{
- available: "USD:15",
+ flags: [],
+ available: "USD:15" as AmountString,
hasPendingTransactions: false,
- pendingIncoming: "USD:0",
- pendingOutgoing: "USD:0",
+ pendingIncoming: "USD:0" as AmountString,
+ pendingOutgoing: "USD:0" as AmountString,
requiresUserInput: false,
+ scopeInfo: {
+ currency: "USD",
+ type: ScopeType.Auditor,
+ url: "",
+ },
},
],
},
);
- const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
- mountHook(() => useComponentState(props, mock));
-
- {
- const { status, error } = pullLastResultOrThrow();
- expect(status).equals("loading");
- expect(error).undefined;
- }
-
- expect(await waitForStateUpdate()).true;
-
- {
- const r = pullLastResultOrThrow();
- if (r.status !== "ready") {
- expect(r).eq({});
- return;
- }
- expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:10"));
- expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
- // expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
- expect(r.payHandler.onClick).not.undefined;
-
- handler.notifyEventFromWallet(NotificationType.CoinWithdrawn);
- }
-
- expect(await waitForStateUpdate()).true;
-
- {
- const r = pullLastResultOrThrow();
- if (r.status !== "ready") {
- expect(r).eq({});
- return;
- }
- expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
- expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
- // expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
- expect(r.payHandler.onClick).not.undefined;
- }
-
- await assertNoPendingUpdate();
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentState,
+ props,
+ [
+ ({ status, error }) => {
+ expect(status).equals("loading");
+ expect(error).undefined;
+ },
+ (state) => {
+ if (state.status !== "ready") expect.fail();
+ expect(state.balance).deep.equal(Amounts.parseOrThrow("USD:10"));
+ expect(state.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
+ // expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
+ expect(state.payHandler.onClick).not.undefined;
+
+ handler.notifyEventFromWallet({
+ type: NotificationType.TransactionStateTransition,
+ newTxState: {} as any,
+ oldTxState: {} as any,
+ transactionId: "123",
+ }
+
+ );
+ },
+ (state) => {
+ if (state.status !== "ready") expect.fail();
+ expect(state.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
+ expect(state.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
+ // expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
+ expect(state.payHandler.onClick).not.undefined;
+ },
+ ],
+ TestingContext,
+ );
+
+ expect(hookBehavior).deep.equal({ result: "ok" });
expect(handler.getCallingQueueState()).eq("empty");
});
});
diff --git a/packages/taler-wallet-webextension/src/cta/Payment/views.tsx b/packages/taler-wallet-webextension/src/cta/Payment/views.tsx
index 0f6cb5c28..8bbb8dac2 100644
--- a/packages/taler-wallet-webextension/src/cta/Payment/views.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Payment/views.tsx
@@ -16,48 +16,21 @@
import {
AbsoluteTime,
- AmountJson,
Amounts,
MerchantContractTerms as ContractTerms,
- PreparePayResult,
PreparePayResultType,
- Product,
+ TranslatedString,
} from "@gnu-taler/taler-util";
-import { Fragment, h, VNode } from "preact";
-import { useState } from "preact/hooks";
-import { Amount } from "../../components/Amount.js";
-import { ErrorMessage } from "../../components/ErrorMessage.js";
-import { LoadingError } from "../../components/LoadingError.js";
-import { LogoHeader } from "../../components/LogoHeader.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
import { Part } from "../../components/Part.js";
-import { QR } from "../../components/QR.js";
-import {
- Link,
- LinkSuccess,
- SmallLightText,
- SubTitle,
- SuccessBox,
- WalletAction,
- WarningBox,
-} from "../../components/styled/index.js";
+import { PaymentButtons } from "../../components/PaymentButtons.js";
+import { ShowFullContractTermPopup } from "../../components/ShowFullContractTermPopup.js";
import { Time } from "../../components/Time.js";
-import { useTranslationContext } from "../../context/translation.js";
-import { Button } from "../../mui/Button.js";
-import { ButtonHandler } from "../../mui/handlers.js";
-import { assertUnreachable } from "../../utils/index.js";
-import { MerchantDetails, PurchaseDetails } from "../../wallet/Transaction.js";
+import { SuccessBox, WarningBox } from "../../components/styled/index.js";
+import { MerchantDetails } from "../../wallet/Transaction.js";
import { State } from "./index.js";
-
-export function LoadingUriView({ error }: State.LoadingUriError): VNode {
- const { i18n } = useTranslationContext();
-
- return (
- <LoadingError
- title={<i18n.Translate>Could not load pay status</i18n.Translate>}
- error={error}
- />
- );
-}
+import { EnabledBySettings } from "../../components/EnabledBySettings.js";
type SupportedStates =
| State.Ready
@@ -70,93 +43,34 @@ export function BaseView(state: SupportedStates): VNode {
const contractTerms: ContractTerms = state.payStatus.contractTerms;
- const price = {
- raw: state.amount,
- effective:
- "amountEffective" in state.payStatus
+ const effective =
+ "amountEffective" in state.payStatus
+ ? state.payStatus.amountEffective
? Amounts.parseOrThrow(state.payStatus.amountEffective)
- : state.amount,
- };
- // const totalFees = Amounts.sub(price.effective, price.raw).amount;
+ : Amounts.zeroOfCurrency(state.amount.currency)
+ : state.amount;
return (
- <WalletAction>
- <LogoHeader />
-
- <SubTitle>
- <i18n.Translate>Digital cash payment</i18n.Translate>
- </SubTitle>
-
+ <Fragment>
<ShowImportantMessage state={state} />
<section style={{ textAlign: "left" }}>
- {/* {state.payStatus.status !== PreparePayResultType.InsufficientBalance &&
- Amounts.isNonZero(totalFees) && (
- <Part
- big
- title={<i18n.Translate>Total to pay</i18n.Translate>}
- text={<Amount value={state.payStatus.amountEffective} />}
- kind="negative"
- />
- )}
- <Part
- big
- title={<i18n.Translate>Purchase amount</i18n.Translate>}
- text={<Amount value={state.payStatus.amountRaw} />}
- kind="neutral"
- />
- {Amounts.isNonZero(totalFees) && (
- <Fragment>
- <Part
- big
- title={<i18n.Translate>Fee</i18n.Translate>}
- text={<Amount value={totalFees} />}
- kind="negative"
- />
- </Fragment>
- )} */}
<Part
- title={<i18n.Translate>Purchase</i18n.Translate>}
- text={contractTerms.summary}
+ title={i18n.str`Purchase`}
+ text={contractTerms.summary as TranslatedString}
kind="neutral"
/>
<Part
- title={<i18n.Translate>Merchant</i18n.Translate>}
+ title={i18n.str`Merchant`}
text={<MerchantDetails merchant={contractTerms.merchant} />}
kind="neutral"
/>
- {/* <pre>{JSON.stringify(price)}</pre>
- <hr />
- <pre>{JSON.stringify(state.payStatus, undefined, 2)}</pre> */}
- <Part
- title={<i18n.Translate>Details</i18n.Translate>}
- text={
- <PurchaseDetails
- price={price}
- info={{
- ...contractTerms,
- orderId: contractTerms.order_id,
- contractTermsHash: "",
- products: contractTerms.products!,
- }}
- proposalId={state.payStatus.proposalId}
- />
- }
- kind="neutral"
- />
- {contractTerms.order_id && (
- <Part
- title={<i18n.Translate>Receipt</i18n.Translate>}
- text={`#${contractTerms.order_id}`}
- kind="neutral"
- />
- )}
{contractTerms.pay_deadline && (
<Part
- title={<i18n.Translate>Valid until</i18n.Translate>}
+ title={i18n.str`Valid until`}
text={
<Time
- timestamp={AbsoluteTime.fromTimestamp(
+ timestamp={AbsoluteTime.fromProtocolTimestamp(
contractTerms.pay_deadline,
)}
format="dd MMMM yyyy, HH:mm"
@@ -166,88 +80,20 @@ export function BaseView(state: SupportedStates): VNode {
/>
)}
</section>
- <ButtonsSection
- amount={state.amount}
- balance={state.balance}
+ <EnabledBySettings name="advancedMode">
+ <section style={{ textAlign: "left" }}>
+ <ShowFullContractTermPopup
+ transactionId={state.payStatus.transactionId}
+ />
+ </section>
+ </EnabledBySettings>
+ <PaymentButtons
+ amount={effective}
payStatus={state.payStatus}
uri={state.uri}
payHandler={state.status === "ready" ? state.payHandler : undefined}
goToWalletManualWithdraw={state.goToWalletManualWithdraw}
/>
- <section>
- <Link upperCased onClick={state.cancel}>
- <i18n.Translate>Cancel</i18n.Translate>
- </Link>
- </section>
- </WalletAction>
- );
-}
-
-export function ProductList({ products }: { products: Product[] }): VNode {
- const { i18n } = useTranslationContext();
- return (
- <Fragment>
- <SmallLightText style={{ margin: ".5em" }}>
- <i18n.Translate>List of products</i18n.Translate>
- </SmallLightText>
- <dl>
- {products.map((p, i) => {
- if (p.price) {
- const pPrice = Amounts.parseOrThrow(p.price);
- return (
- <div key={i} style={{ display: "flex", textAlign: "left" }}>
- <div>
- <img
- src={p.image ? p.image : undefined}
- style={{ width: 32, height: 32 }}
- />
- </div>
- <div>
- <dt>
- {p.quantity ?? 1} x {p.description}{" "}
- <span style={{ color: "gray" }}>
- {Amounts.stringify(pPrice)}
- </span>
- </dt>
- <dd>
- <b>
- {Amounts.stringify(
- Amounts.mult(pPrice, p.quantity ?? 1).amount,
- )}
- </b>
- </dd>
- </div>
- </div>
- );
- }
- return (
- <div key={i} style={{ display: "flex", textAlign: "left" }}>
- <div>
- <img src={p.image} style={{ width: 32, height: 32 }} />
- </div>
- <div>
- <dt>
- {p.quantity ?? 1} x {p.description}
- </dt>
- <dd>
- <i18n.Translate>Total</i18n.Translate>
- {` `}
- {p.price ? (
- `${Amounts.stringifyValue(
- Amounts.mult(
- Amounts.parseOrThrow(p.price),
- p.quantity ?? 1,
- ).amount,
- )} ${p}`
- ) : (
- <i18n.Translate>free</i18n.Translate>
- )}
- </dd>
- </div>
- </div>
- );
- })}
- </dl>
</Fragment>
);
}
@@ -284,124 +130,3 @@ function ShowImportantMessage({ state }: { state: SupportedStates }): VNode {
return <Fragment />;
}
-
-export function PayWithMobile({ uri }: { uri: string }): VNode {
- const { i18n } = useTranslationContext();
-
- const [showQR, setShowQR] = useState<boolean>(false);
-
- return (
- <section>
- <LinkSuccess upperCased onClick={() => setShowQR((qr) => !qr)}>
- {!showQR ? (
- <i18n.Translate>Pay with a mobile phone</i18n.Translate>
- ) : (
- <i18n.Translate>Hide QR</i18n.Translate>
- )}
- </LinkSuccess>
- {showQR && (
- <div>
- <QR text={uri} />
- <i18n.Translate>
- Scan the QR code or &nbsp;
- <a href={uri}>
- <i18n.Translate>click here</i18n.Translate>
- </a>
- </i18n.Translate>
- </div>
- )}
- </section>
- );
-}
-
-interface ButtonSectionProps {
- payStatus: PreparePayResult;
- payHandler: ButtonHandler | undefined;
- balance: AmountJson | undefined;
- uri: string;
- amount: AmountJson;
- goToWalletManualWithdraw: (currency: string) => Promise<void>;
-}
-
-export function ButtonsSection({
- payStatus,
- uri,
- payHandler,
- balance,
- amount,
- goToWalletManualWithdraw,
-}: ButtonSectionProps): VNode {
- const { i18n } = useTranslationContext();
- if (payStatus.status === PreparePayResultType.PaymentPossible) {
- const privateUri = `${uri}&n=${payStatus.noncePriv}`;
-
- return (
- <Fragment>
- <section>
- <Button
- variant="contained"
- color="success"
- onClick={payHandler?.onClick}
- >
- <i18n.Translate>
- Pay &nbsp;
- {<Amount value={amount} />}
- </i18n.Translate>
- </Button>
- </section>
- <PayWithMobile uri={privateUri} />
- </Fragment>
- );
- }
-
- if (payStatus.status === PreparePayResultType.InsufficientBalance) {
- let BalanceMessage = "";
- if (!balance) {
- BalanceMessage = i18n.str`You have no balance for this currency. Withdraw digital cash first.`;
- } else {
- const balanceShouldBeEnough = Amounts.cmp(balance, amount) !== -1;
- if (balanceShouldBeEnough) {
- BalanceMessage = i18n.str`Could not find enough coins to pay. Even if you have enough ${balance.currency} some restriction may apply.`;
- } else {
- BalanceMessage = i18n.str`Your current balance is not enough.`;
- }
- }
- const uriPrivate = `${uri}&n=${payStatus.noncePriv}`;
-
- return (
- <Fragment>
- <section>
- <WarningBox>{BalanceMessage}</WarningBox>
- </section>
- <section>
- <Button
- variant="contained"
- color="success"
- onClick={() => goToWalletManualWithdraw(Amounts.stringify(amount))}
- >
- <i18n.Translate>Get digital cash</i18n.Translate>
- </Button>
- </section>
- <PayWithMobile uri={uriPrivate} />
- </Fragment>
- );
- }
- if (payStatus.status === PreparePayResultType.AlreadyConfirmed) {
- return (
- <Fragment>
- <section>
- {payStatus.paid && payStatus.contractTerms.fulfillment_message && (
- <Part
- title={<i18n.Translate>Merchant message</i18n.Translate>}
- text={payStatus.contractTerms.fulfillment_message}
- kind="neutral"
- />
- )}
- </section>
- {!payStatus.paid && <PayWithMobile uri={uri} />}
- </Fragment>
- );
- }
-
- assertUnreachable(payStatus);
-}
diff --git a/packages/taler-wallet-webextension/src/cta/Tip/index.ts b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/index.ts
index ff917008f..f5a8c8814 100644
--- a/packages/taler-wallet-webextension/src/cta/Tip/index.ts
+++ b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/index.ts
@@ -14,76 +14,70 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { AmountJson } from "@gnu-taler/taler-util";
+import { ErrorAlertView } from "../../components/CurrentAlerts.js";
import { Loading } from "../../components/Loading.js";
-import { HookError } from "../../hooks/useAsyncAsHook.js";
-import { ButtonHandler } from "../../mui/handlers.js";
+import { ErrorAlert } from "../../context/alert.js";
import { compose, StateViewMap } from "../../utils/index.js";
-import { wxApi } from "../../wxApi.js";
import { useComponentState } from "./state.js";
+import { ReadyView } from "./views.js";
+import { PaymentPage } from "../Payment/index.js";
import {
- AcceptedView,
- IgnoredView,
- LoadingUriView,
- ReadyView,
-} from "./views.js";
+ AmountFieldHandler,
+ ButtonHandler,
+ TextFieldHandler,
+} from "../../mui/handlers.js";
export interface Props {
- talerTipUri?: string;
- onCancel: () => Promise<void>;
+ talerTemplateUri: string;
+ goToWalletManualWithdraw: (amount?: string) => Promise<void>;
+ cancel: () => Promise<void>;
onSuccess: (tx: string) => Promise<void>;
}
export type State =
| State.Loading
| State.LoadingUriError
- | State.Ignored
- | State.Accepted
- | State.Ready
- | State.Ignored;
+ | State.OrderReady
+ | State.FillTemplate;
export namespace State {
export interface Loading {
status: "loading";
error: undefined;
}
-
export interface LoadingUriError {
- status: "loading-uri";
- error: HookError;
+ status: "error";
+ error: ErrorAlert;
}
- export interface BaseInfo {
- merchantBaseUrl: string;
- amount: AmountJson;
- exchangeBaseUrl: string;
+ export interface FillTemplate {
+ status: "fill-template";
error: undefined;
- cancel: ButtonHandler;
- }
-
- export interface Ignored extends BaseInfo {
- status: "ignored";
+ currency: string;
+ amount?: AmountFieldHandler;
+ summary?: TextFieldHandler;
+ onCreate: ButtonHandler;
}
- export interface Accepted extends BaseInfo {
- status: "accepted";
- }
- export interface Ready extends BaseInfo {
- status: "ready";
- accept: ButtonHandler;
+ export interface OrderReady {
+ status: "order-ready";
+ error: undefined;
+ talerPayUri: string;
+ onSuccess: (tx: string) => Promise<void>;
+ cancel: () => Promise<void>;
+ goToWalletManualWithdraw: () => Promise<void>;
}
}
const viewMapping: StateViewMap<State> = {
loading: Loading,
- "loading-uri": LoadingUriView,
- accepted: AcceptedView,
- ignored: IgnoredView,
- ready: ReadyView,
+ error: ErrorAlertView,
+ "fill-template": ReadyView,
+ "order-ready": PaymentPage,
};
-export const TipPage = compose(
- "Tip",
- (p: Props) => useComponentState(p, wxApi),
+export const PaymentTemplatePage = compose(
+ "PaymentTemplate",
+ (p: Props) => useComponentState(p),
viewMapping,
);
diff --git a/packages/taler-wallet-webextension/src/cta/PaymentTemplate/state.ts b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/state.ts
new file mode 100644
index 000000000..6b4584fea
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/state.ts
@@ -0,0 +1,174 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { Amounts, PreparePayResult } from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useState } from "preact/hooks";
+import { alertFromError, useAlertContext } from "../../context/alert.js";
+import { useBackendContext } from "../../context/backend.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
+import { AmountFieldHandler, TextFieldHandler } from "../../mui/handlers.js";
+import { Props, State } from "./index.js";
+
+export function useComponentState({
+ talerTemplateUri,
+ cancel,
+ goToWalletManualWithdraw,
+ onSuccess,
+}: Props): State {
+ const api = useBackendContext();
+ const { i18n } = useTranslationContext();
+ const { safely } = useAlertContext();
+
+ const url = talerTemplateUri ? new URL(talerTemplateUri) : undefined;
+
+ const amountParam = !url
+ ? undefined
+ : url.searchParams.get("amount") ?? undefined;
+ const summaryParam = !url
+ ? undefined
+ : url.searchParams.get("summary") ?? undefined;
+
+ const parsedAmount = !amountParam ? undefined : Amounts.parse(amountParam);
+ const currency = parsedAmount ? parsedAmount.currency : amountParam;
+
+ const initialAmount =
+ parsedAmount ?? (currency ? Amounts.zeroOfCurrency(currency) : undefined);
+ const [amount, setAmount] = useState(initialAmount);
+ const [summary, setSummary] = useState(summaryParam);
+ const [newOrder, setNewOrder] = useState("");
+
+ const hook = useAsyncAsHook(async () => {
+ if (!talerTemplateUri) throw Error("ERROR_NO-URI-FOR-PAYMENT-TEMPLATE");
+ let payStatus: PreparePayResult | undefined = undefined;
+ if (!amountParam && !summaryParam) {
+ payStatus = await api.wallet.call(
+ WalletApiOperation.PreparePayForTemplate,
+ {
+ talerPayTemplateUri: talerTemplateUri,
+ templateParams: {},
+ },
+ );
+ }
+ const balance = await api.wallet.call(WalletApiOperation.GetBalances, {});
+ return { payStatus, balance, uri: talerTemplateUri };
+ }, []);
+
+ if (!hook) {
+ return {
+ status: "loading",
+ error: undefined,
+ };
+ }
+
+ if (hook.hasError) {
+ return {
+ status: "error",
+ error: alertFromError(
+ i18n,
+ i18n.str`Could not load the status of the order template`,
+ hook,
+ ),
+ };
+ }
+
+ if (hook.response.payStatus) {
+ return {
+ status: "order-ready",
+ error: undefined,
+ cancel,
+ goToWalletManualWithdraw,
+ onSuccess,
+ talerPayUri: hook.response.payStatus.talerUri!,
+ };
+ }
+
+ if (newOrder) {
+ return {
+ status: "order-ready",
+ error: undefined,
+ cancel,
+ goToWalletManualWithdraw,
+ onSuccess,
+ talerPayUri: newOrder,
+ };
+ }
+
+ async function createOrder() {
+ try {
+ const templateParams: Record<string, string> = {};
+ if (amount) {
+ templateParams["amount"] = Amounts.stringify(amount);
+ }
+ if (summary) {
+ templateParams["summary"] = summary;
+ }
+ const payStatus = await api.wallet.call(
+ WalletApiOperation.PreparePayForTemplate,
+ {
+ talerPayTemplateUri: talerTemplateUri,
+ templateParams,
+ },
+ );
+ setNewOrder(payStatus.talerUri!);
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ const errors = undefinedIfEmpty({
+ amount: amount && Amounts.isZero(amount) ? i18n.str`required` : undefined,
+ summary: summary !== undefined && !summary ? i18n.str`required` : undefined,
+ });
+ return {
+ status: "fill-template",
+ error: undefined,
+ currency: currency!, //currency is always not null
+ amount:
+ amount !== undefined
+ ? ({
+ onInput: (a) => {
+ setAmount(a);
+ },
+ value: amount,
+ error: errors?.amount,
+ } as AmountFieldHandler)
+ : undefined,
+ summary:
+ summary !== undefined
+ ? ({
+ onInput: (t) => {
+ setSummary(t);
+ },
+ value: summary,
+ error: errors?.summary,
+ } as TextFieldHandler)
+ : undefined,
+ onCreate: {
+ onClick: errors
+ ? undefined
+ : safely("create order for pay template", createOrder),
+ },
+ };
+}
+
+function undefinedIfEmpty<T extends object>(obj: T): T | undefined {
+ return Object.keys(obj).some(
+ (k) => (obj as Record<string, unknown>)[k] !== undefined,
+ )
+ ? obj
+ : undefined;
+}
diff --git a/packages/taler-wallet-webextension/src/cta/PaymentTemplate/stories.tsx b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/stories.tsx
new file mode 100644
index 000000000..02607fa30
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/stories.tsx
@@ -0,0 +1,34 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import * as tests from "@gnu-taler/web-util/testing";
+import { ReadyView } from "./views.js";
+
+export default {
+ title: "payment template",
+ component: ReadyView,
+ argTypes: {},
+};
+
+export const PaymentPossible = tests.createExample(ReadyView, {
+ status: "fill-template",
+ error: undefined,
+});
diff --git a/packages/taler-wallet-webextension/src/cta/PaymentTemplate/test.ts b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/test.ts
new file mode 100644
index 000000000..d15761eae
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/test.ts
@@ -0,0 +1,59 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { expect } from "chai";
+import * as tests from "@gnu-taler/web-util/testing";
+import { nullFunction } from "../../mui/handlers.js";
+import { createWalletApiMock } from "../../test-utils.js";
+import { useComponentState } from "./state.js";
+
+describe("Order template CTA states", () => {
+ it("should tell the user that the URI is missing", async () => {
+ const { handler, TestingContext } = createWalletApiMock();
+ const props = {
+ talerTemplateUri: "",
+ cancel: nullFunction,
+ goToWalletManualWithdraw: nullFunction,
+ onSuccess: nullFunction,
+ };
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentState,
+ props,
+ [
+ ({ status, error }) => {
+ expect(status).equals("loading");
+ expect(error).undefined;
+ },
+ ({ status, error }) => {
+ expect(status).equals("error");
+ if (error === undefined) expect.fail();
+ // expect(error.hasError).true;
+ // expect(error.operational).false;
+ },
+ ],
+ TestingContext,
+ );
+
+ expect(hookBehavior).deep.equal({ result: "ok" });
+ expect(handler.getCallingQueueState()).eq("empty");
+ });
+});
diff --git a/packages/taler-wallet-webextension/src/cta/PaymentTemplate/views.tsx b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/views.tsx
new file mode 100644
index 000000000..88658b5e1
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/views.tsx
@@ -0,0 +1,77 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { Fragment, h, VNode } from "preact";
+import { AmountField } from "../../components/AmountField.js";
+import { Part } from "../../components/Part.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Button } from "../../mui/Button.js";
+import { TextField } from "../../mui/TextField.js";
+import { State } from "./index.js";
+
+export function ReadyView({
+ currency,
+ amount,
+ summary,
+ onCreate,
+}: State.FillTemplate): VNode {
+ const { i18n } = useTranslationContext();
+
+ return (
+ <Fragment>
+ <section style={{ textAlign: "left" }}>
+ {/* <Part
+ title={
+ <div
+ style={{
+ display: "flex",
+ alignItems: "center",
+ }}
+ >
+ <i18n.Translate>Merchant</i18n.Translate>
+ </div>
+ }
+ text={<ExchangeDetails exchange={exchangeUrl} />}
+ kind="neutral"
+ big
+ /> */}
+ {!amount ? undefined : (
+ <p>
+ <AmountField label={i18n.str`Amount`} handler={amount} />
+ </p>
+ )}
+ {!summary ? undefined : (
+ <p>
+ <TextField
+ label="Summary"
+ variant="filled"
+ required
+ fullWidth
+ error={summary.error}
+ value={summary.value}
+ onChange={summary.onInput}
+ />
+ </p>
+ )}
+ </section>
+ <section>
+ <Button onClick={onCreate.onClick} variant="contained" color="success">
+ <i18n.Translate>Review order</i18n.Translate>
+ </Button>
+ </section>
+ </Fragment>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/cta/Recovery/index.ts b/packages/taler-wallet-webextension/src/cta/Recovery/index.ts
index 4a65c571b..79056c15b 100644
--- a/packages/taler-wallet-webextension/src/cta/Recovery/index.ts
+++ b/packages/taler-wallet-webextension/src/cta/Recovery/index.ts
@@ -14,13 +14,13 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+import { ErrorAlertView } from "../../components/CurrentAlerts.js";
import { Loading } from "../../components/Loading.js";
-import { HookError } from "../../hooks/useAsyncAsHook.js";
+import { ErrorAlert } from "../../context/alert.js";
import { ButtonHandler } from "../../mui/handlers.js";
import { compose, StateViewMap } from "../../utils/index.js";
-import { wxApi } from "../../wxApi.js";
import { useComponentState } from "./state.js";
-import { LoadingUriView, ReadyView } from "./views.js";
+import { ReadyView } from "./views.js";
export interface Props {
talerRecoveryUri?: string;
@@ -37,8 +37,8 @@ export namespace State {
}
export interface LoadingUriError {
- status: "loading-uri";
- error: HookError;
+ status: "error";
+ error: ErrorAlert;
}
export interface BaseInfo {
@@ -54,12 +54,12 @@ export namespace State {
const viewMapping: StateViewMap<State> = {
loading: Loading,
- "loading-uri": LoadingUriView,
+ error: ErrorAlertView,
ready: ReadyView,
};
export const RecoveryPage = compose(
"Recovery",
- (p: Props) => useComponentState(p, wxApi),
+ (p: Props) => useComponentState(p),
viewMapping,
);
diff --git a/packages/taler-wallet-webextension/src/cta/Recovery/state.ts b/packages/taler-wallet-webextension/src/cta/Recovery/state.ts
index 3a5d94e2e..5399c5bfc 100644
--- a/packages/taler-wallet-webextension/src/cta/Recovery/state.ts
+++ b/packages/taler-wallet-webextension/src/cta/Recovery/state.ts
@@ -14,42 +14,58 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { parseRecoveryUri } from "@gnu-taler/taler-util";
+import { parseRestoreUri } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-import { wxApi } from "../../wxApi.js";
+import { useAlertContext } from "../../context/alert.js";
+import { useBackendContext } from "../../context/backend.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Props, State } from "./index.js";
-export function useComponentState(
- { talerRecoveryUri, onCancel, onSuccess }: Props,
- api: typeof wxApi,
-): State {
+export function useComponentState({
+ talerRecoveryUri,
+ onCancel,
+ onSuccess,
+}: Props): State {
+ const api = useBackendContext();
+ const { pushAlertOnError } = useAlertContext();
+ const { i18n } = useTranslationContext();
if (!talerRecoveryUri) {
return {
- status: "loading-uri",
+ status: "error",
error: {
- operational: false,
- hasError: true,
- message: "Missing URI",
+ type: "error",
+ message: i18n.str`Missing URI`,
+ description: i18n.str``,
+ cause: new Error("something"),
+ context: {},
},
};
}
- const info = parseRecoveryUri(talerRecoveryUri);
+ const info = parseRestoreUri(talerRecoveryUri);
if (!info) {
return {
- status: "loading-uri",
+ status: "error",
error: {
- operational: false,
- hasError: true,
- message: "Could not be read",
+ type: "error",
+ message: i18n.str`Could not parse the recovery URI`,
+ description: i18n.str``,
+ cause: new Error("something"),
+ context: {},
},
};
}
const recovery = info;
async function recoverBackup(): Promise<void> {
- await wxApi.wallet.call(WalletApiOperation.ImportBackupRecovery, {
- recovery,
+ await api.wallet.call(WalletApiOperation.ImportBackupRecovery, {
+ recovery: {
+ walletRootPriv: recovery.walletRootPriv,
+ providers: recovery.providers.map((url) => ({
+ name: new URL(url).hostname,
+ url,
+ })),
+ },
});
onSuccess();
}
@@ -58,10 +74,10 @@ export function useComponentState(
status: "ready",
accept: {
- onClick: recoverBackup,
+ onClick: pushAlertOnError(recoverBackup),
},
cancel: {
- onClick: onCancel,
+ onClick: pushAlertOnError(onCancel),
},
error: undefined,
};
diff --git a/packages/taler-wallet-webextension/src/cta/Recovery/stories.tsx b/packages/taler-wallet-webextension/src/cta/Recovery/stories.tsx
index 9243cc015..4d8dc3737 100644
--- a/packages/taler-wallet-webextension/src/cta/Recovery/stories.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Recovery/stories.tsx
@@ -20,7 +20,7 @@
*/
import { Amounts } from "@gnu-taler/taler-util";
-import { createExample } from "../../test-utils.js";
+import * as tests from "@gnu-taler/web-util/testing";
import { ReadyView } from "./views.js";
export default {
diff --git a/packages/taler-wallet-webextension/src/cta/Recovery/views.tsx b/packages/taler-wallet-webextension/src/cta/Recovery/views.tsx
index 371516932..5a3a00daa 100644
--- a/packages/taler-wallet-webextension/src/cta/Recovery/views.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Recovery/views.tsx
@@ -15,38 +15,16 @@
*/
import { Fragment, h, VNode } from "preact";
-import { LoadingError } from "../../components/LoadingError.js";
import { LogoHeader } from "../../components/LogoHeader.js";
import { SubTitle, WalletAction } from "../../components/styled/index.js";
-import { useTranslationContext } from "../../context/translation.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Button } from "../../mui/Button.js";
import { State } from "./index.js";
-export function LoadingUriView({ error }: State.LoadingUriError): VNode {
- const { i18n } = useTranslationContext();
-
- return (
- <LoadingError
- title={
- <i18n.Translate>
- Could not load backup recovery information
- </i18n.Translate>
- }
- error={error}
- />
- );
-}
-
export function ReadyView({ accept, cancel }: State.Ready): VNode {
const { i18n } = useTranslationContext();
return (
- <WalletAction>
- <LogoHeader />
-
- <SubTitle>
- <i18n.Translate>Digital wallet recovery</i18n.Translate>
- </SubTitle>
-
+ <Fragment>
<section>
<p>
<i18n.Translate>Import backup, show info</i18n.Translate>
@@ -58,6 +36,6 @@ export function ReadyView({ accept, cancel }: State.Ready): VNode {
Cancel
</Button>
</section>
- </WalletAction>
+ </Fragment>
);
}
diff --git a/packages/taler-wallet-webextension/src/cta/Refund/index.ts b/packages/taler-wallet-webextension/src/cta/Refund/index.ts
index 099f72919..42e9cc534 100644
--- a/packages/taler-wallet-webextension/src/cta/Refund/index.ts
+++ b/packages/taler-wallet-webextension/src/cta/Refund/index.ts
@@ -15,18 +15,13 @@
*/
import { AmountJson, Product } from "@gnu-taler/taler-util";
+import { ErrorAlertView } from "../../components/CurrentAlerts.js";
import { Loading } from "../../components/Loading.js";
-import { HookError } from "../../hooks/useAsyncAsHook.js";
+import { ErrorAlert } from "../../context/alert.js";
import { ButtonHandler } from "../../mui/handlers.js";
import { compose, StateViewMap } from "../../utils/index.js";
-import { wxApi } from "../../wxApi.js";
import { useComponentState } from "./state.js";
-import {
- IgnoredView,
- InProgressView,
- LoadingUriView,
- ReadyView,
-} from "./views.js";
+import { IgnoredView, ReadyView } from "./views.js";
export interface Props {
talerRefundUri?: string;
@@ -38,8 +33,8 @@ export type State =
| State.Loading
| State.LoadingUriError
| State.Ready
- | State.Ignored
- | State.InProgress;
+ // | State.InProgress
+ | State.Ignored;
export namespace State {
export interface Loading {
@@ -48,16 +43,16 @@ export namespace State {
}
export interface LoadingUriError {
- status: "loading-uri";
- error: HookError;
+ status: "error";
+ error: ErrorAlert;
}
interface BaseInfo {
merchantName: string;
- products: Product[] | undefined;
+ // products: Product[] | undefined;
amount: AmountJson;
- awaitingAmount: AmountJson;
- granted: AmountJson;
+ // awaitingAmount: AmountJson;
+ // granted: AmountJson;
}
export interface Ready extends BaseInfo {
@@ -74,22 +69,22 @@ export namespace State {
status: "ignored";
error: undefined;
}
- export interface InProgress extends BaseInfo {
- status: "in-progress";
- error: undefined;
- }
+ // export interface InProgress extends BaseInfo {
+ // status: "in-progress";
+ // error: undefined;
+ // }
}
const viewMapping: StateViewMap<State> = {
loading: Loading,
- "loading-uri": LoadingUriView,
- "in-progress": InProgressView,
+ error: ErrorAlertView,
+ // "in-progress": InProgressView,
ignored: IgnoredView,
ready: ReadyView,
};
export const RefundPage = compose(
"Refund",
- (p: Props) => useComponentState(p, wxApi),
+ (p: Props) => useComponentState(p),
viewMapping,
);
diff --git a/packages/taler-wallet-webextension/src/cta/Refund/state.ts b/packages/taler-wallet-webextension/src/cta/Refund/state.ts
index 94c5567d6..6f0a98151 100644
--- a/packages/taler-wallet-webextension/src/cta/Refund/state.ts
+++ b/packages/taler-wallet-webextension/src/cta/Refund/state.ts
@@ -14,30 +14,53 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { Amounts, NotificationType } from "@gnu-taler/taler-util";
+import {
+ Amounts,
+ NotificationType,
+ TransactionPayment,
+ TransactionType,
+} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useEffect, useState } from "preact/hooks";
+import { alertFromError, useAlertContext } from "../../context/alert.js";
+import { useBackendContext } from "../../context/backend.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
-import { wxApi } from "../../wxApi.js";
import { Props, State } from "./index.js";
-export function useComponentState(
- { talerRefundUri, cancel, onSuccess }: Props,
- api: typeof wxApi,
-): State {
+export function useComponentState({
+ talerRefundUri,
+ cancel,
+ onSuccess,
+}: Props): State {
+ const api = useBackendContext();
+ const { i18n } = useTranslationContext();
const [ignored, setIgnored] = useState(false);
+ const { pushAlertOnError } = useAlertContext();
const info = useAsyncAsHook(async () => {
if (!talerRefundUri) throw Error("ERROR_NO-URI-FOR-REFUND");
- const refund = await api.wallet.call(WalletApiOperation.PrepareRefund, {
- talerRefundUri,
- });
- return { refund, uri: talerRefundUri };
+ const refund = await api.wallet.call(
+ WalletApiOperation.StartRefundQueryForUri,
+ {
+ talerRefundUri,
+ },
+ );
+ const purchase = await api.wallet.call(
+ WalletApiOperation.GetTransactionById,
+ {
+ transactionId: refund.transactionId,
+ },
+ );
+ if (purchase.type !== TransactionType.Payment) {
+ throw Error("Refund of non purchase transaction is not handled");
+ }
+ return { refund, purchase, uri: talerRefundUri };
});
useEffect(() =>
api.listener.onUpdateNotification(
- [NotificationType.RefreshMelted],
+ [NotificationType.TransactionStateTransition],
info?.retry,
),
);
@@ -47,17 +70,30 @@ export function useComponentState(
}
if (info.hasError) {
return {
- status: "loading-uri",
- error: info,
+ status: "error",
+ error: alertFromError(
+ i18n,
+ i18n.str`Could not load the refund status`,
+ info,
+ ),
};
}
+ // if (info.hasError) {
+ // return {
+ // status: "loading-uri",
+ // error: info,
+ // };
+ // }
- const { refund, uri } = info.response;
+ const { refund, purchase, uri } = info.response;
const doAccept = async (): Promise<void> => {
- const res = await api.wallet.call(WalletApiOperation.ApplyRefund, {
- talerRefundUri: uri,
- });
+ const res = await api.wallet.call(
+ WalletApiOperation.StartRefundQueryForUri,
+ {
+ talerRefundUri: uri,
+ },
+ );
onSuccess(res.transactionId);
};
@@ -67,11 +103,11 @@ export function useComponentState(
};
const baseInfo = {
- amount: Amounts.parseOrThrow(info.response.refund.effectivePaid),
- granted: Amounts.parseOrThrow(info.response.refund.granted),
- merchantName: info.response.refund.info.merchant.name,
- products: info.response.refund.info.products,
- awaitingAmount: Amounts.parseOrThrow(refund.awaiting),
+ amount: Amounts.parseOrThrow(purchase.amountEffective),
+ // granted: Amounts.parseOrThrow(info.response.refund.granted),
+ // awaitingAmount: Amounts.parseOrThrow(refund.awaiting),
+ merchantName: purchase.info.merchant.name,
+ // products: purchase.info.products,
error: undefined,
};
@@ -82,22 +118,23 @@ export function useComponentState(
};
}
- if (refund.pending) {
- return {
- status: "in-progress",
- ...baseInfo,
- };
- }
+ //FIXME: DD37 wallet-core is not returning this value
+ // if (refund.pending) {
+ // return {
+ // status: "in-progress",
+ // ...baseInfo,
+ // };
+ // }
return {
status: "ready",
...baseInfo,
- orderId: info.response.refund.info.orderId,
+ orderId: purchase.info.orderId,
accept: {
- onClick: doAccept,
+ onClick: pushAlertOnError(doAccept),
},
ignore: {
- onClick: doIgnore,
+ onClick: pushAlertOnError(doIgnore),
},
cancel,
};
diff --git a/packages/taler-wallet-webextension/src/cta/Refund/stories.tsx b/packages/taler-wallet-webextension/src/cta/Refund/stories.tsx
index 921cf77e6..03d55ee91 100644
--- a/packages/taler-wallet-webextension/src/cta/Refund/stories.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Refund/stories.tsx
@@ -21,61 +21,61 @@
import { Amounts } from "@gnu-taler/taler-util";
import beer from "../../../static-dev/beer.png";
-import { createExample } from "../../test-utils.js";
-import { IgnoredView, InProgressView, ReadyView } from "./views.js";
+import * as tests from "@gnu-taler/web-util/testing";
+import { IgnoredView, ReadyView } from "./views.js";
export default {
title: "refund",
};
-export const InProgress = createExample(InProgressView, {
- status: "in-progress",
- error: undefined,
- amount: Amounts.parseOrThrow("USD:1"),
- awaitingAmount: Amounts.parseOrThrow("USD:1"),
- granted: Amounts.parseOrThrow("USD:0"),
- merchantName: "the merchant",
- products: undefined,
-});
+// export const InProgress = tests.createExample(InProgressView, {
+// status: "in-progress",
+// error: undefined,
+// amount: Amounts.parseOrThrow("USD:1"),
+// awaitingAmount: Amounts.parseOrThrow("USD:1"),
+// granted: Amounts.parseOrThrow("USD:0"),
+// merchantName: "the merchant",
+// products: undefined,
+// });
-export const Ready = createExample(ReadyView, {
+export const Ready = tests.createExample(ReadyView, {
status: "ready",
error: undefined,
accept: {},
ignore: {},
amount: Amounts.parseOrThrow("USD:1"),
- awaitingAmount: Amounts.parseOrThrow("USD:1"),
- granted: Amounts.parseOrThrow("USD:0"),
+ // awaitingAmount: Amounts.parseOrThrow("USD:1"),
+ // granted: Amounts.parseOrThrow("USD:0"),
merchantName: "the merchant",
- products: [],
+ // products: [],
orderId: "abcdef",
});
-export const WithAProductList = createExample(ReadyView, {
+export const WithAProductList = tests.createExample(ReadyView, {
status: "ready",
error: undefined,
accept: {},
ignore: {},
amount: Amounts.parseOrThrow("USD:1"),
- awaitingAmount: Amounts.parseOrThrow("USD:1"),
- granted: Amounts.parseOrThrow("USD:0"),
+ // awaitingAmount: Amounts.parseOrThrow("USD:1"),
+ // granted: Amounts.parseOrThrow("USD:0"),
merchantName: "the merchant",
- products: [
- {
- description: "beer",
- image: beer,
- quantity: 2,
- },
- {
- description: "t-shirt",
- price: "EUR:1",
- quantity: 5,
- },
- ],
+ // products: [
+ // {
+ // description: "beer",
+ // image: beer,
+ // quantity: 2,
+ // },
+ // {
+ // description: "t-shirt",
+ // price: "EUR:1",
+ // quantity: 5,
+ // },
+ // ],
orderId: "abcdef",
});
-export const Ignored = createExample(IgnoredView, {
+export const Ignored = tests.createExample(IgnoredView, {
status: "ignored",
error: undefined,
merchantName: "the merchant",
diff --git a/packages/taler-wallet-webextension/src/cta/Refund/test.ts b/packages/taler-wallet-webextension/src/cta/Refund/test.ts
index 927c45981..bc0e61fcb 100644
--- a/packages/taler-wallet-webextension/src/cta/Refund/test.ts
+++ b/packages/taler-wallet-webextension/src/cta/Refund/test.ts
@@ -20,391 +20,268 @@
*/
import {
- AmountJson,
Amounts,
NotificationType,
OrderShortInfo,
- PrepareRefundResult,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { expect } from "chai";
-import { mountHook } from "../../test-utils.js";
+import * as tests from "@gnu-taler/web-util/testing";
+import { nullFunction } from "../../mui/handlers.js";
import { createWalletApiMock } from "../../test-utils.js";
import { useComponentState } from "./state.js";
-describe("Refund CTA states", () => {
- it("should tell the user that the URI is missing", async () => {
- const { handler, mock } = createWalletApiMock();
-
- const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
- mountHook(() =>
- useComponentState(
- {
- talerRefundUri: undefined,
- cancel: async () => {
- null;
- },
- onSuccess: async () => {
- null;
- },
- },
- mock,
- // {
- // prepareRefund: async () => ({}),
- // applyRefund: async () => ({}),
- // onUpdateNotification: async () => ({}),
- // } as any,
- ),
- );
-
- {
- const { status, error } = pullLastResultOrThrow();
- expect(status).equals("loading");
- expect(error).undefined;
- }
-
- expect(await waitForStateUpdate()).true;
-
- {
- const { status, error } = pullLastResultOrThrow();
-
- expect(status).equals("loading-uri");
- if (!error) expect.fail();
- if (!error.hasError) expect.fail();
- if (error.operational) expect.fail();
- expect(error.message).eq("ERROR_NO-URI-FOR-REFUND");
- }
-
- await assertNoPendingUpdate();
- expect(handler.getCallingQueueState()).eq("empty");
- });
-
- it("should be ready after loading", async () => {
- const { handler, mock } = createWalletApiMock();
- const props = {
- talerRefundUri: "taler://refund/asdasdas",
- cancel: async () => {
- null;
- },
- onSuccess: async () => {
- null;
- },
- };
-
- handler.addWalletCallResponse(WalletApiOperation.PrepareRefund, undefined, {
- awaiting: "EUR:2",
- effectivePaid: "EUR:2",
- gone: "EUR:0",
- granted: "EUR:0",
- pending: false,
- proposalId: "1",
- info: {
- contractTermsHash: "123",
- merchant: {
- name: "the merchant name",
- },
- orderId: "orderId1",
- summary: "the summary",
- } as OrderShortInfo,
- });
-
- const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
- mountHook(() =>
- useComponentState(
- props,
- mock,
- // {
- // prepareRefund: async () =>
- // ({
- // effectivePaid: "EUR:2",
- // awaiting: "EUR:2",
- // gone: "EUR:0",
- // granted: "EUR:0",
- // pending: false,
- // proposalId: "1",
- // info: {
- // contractTermsHash: "123",
- // merchant: {
- // name: "the merchant name",
- // },
- // orderId: "orderId1",
- // summary: "the summary",
- // },
- // } as PrepareRefundResult as any),
- // applyRefund: async () => ({}),
- // onUpdateNotification: async () => ({}),
- // } as any,
- ),
- );
-
- {
- const { status, error } = pullLastResultOrThrow();
- expect(status).equals("loading");
- expect(error).undefined;
- }
-
- expect(await waitForStateUpdate()).true;
-
- {
- const state = pullLastResultOrThrow();
-
- if (state.status !== "ready") expect.fail();
- if (state.error) expect.fail();
- expect(state.accept.onClick).not.undefined;
- expect(state.ignore.onClick).not.undefined;
- expect(state.merchantName).eq("the merchant name");
- expect(state.orderId).eq("orderId1");
- expect(state.products).undefined;
- }
-
- await assertNoPendingUpdate();
- expect(handler.getCallingQueueState()).eq("empty");
- });
-
- it("should be ignored after clicking the ignore button", async () => {
- const { handler, mock } = createWalletApiMock();
- const props = {
- talerRefundUri: "taler://refund/asdasdas",
- cancel: async () => {
- null;
- },
- onSuccess: async () => {
- null;
- },
- };
-
- handler.addWalletCallResponse(WalletApiOperation.PrepareRefund, undefined, {
- awaiting: "EUR:2",
- effectivePaid: "EUR:2",
- gone: "EUR:0",
- granted: "EUR:0",
- pending: false,
- proposalId: "1",
- info: {
- contractTermsHash: "123",
- merchant: {
- name: "the merchant name",
- },
- orderId: "orderId1",
- summary: "the summary",
- } as OrderShortInfo,
- });
- // handler.addWalletCall(WalletApiOperation.ApplyRefund)
- // handler.addWalletCall(WalletApiOperation.PrepareRefund, undefined, {
- // awaiting: "EUR:1",
- // effectivePaid: "EUR:2",
- // gone: "EUR:0",
- // granted: "EUR:1",
- // pending: true,
- // proposalId: "1",
- // info: {
- // contractTermsHash: "123",
- // merchant: {
- // name: "the merchant name",
- // },
- // orderId: "orderId1",
- // summary: "the summary",
- // } as OrderShortInfo,
- // })
- const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
- mountHook(() =>
- useComponentState(
- props,
- mock,
- // {
- // prepareRefund: async () =>
- // ({
- // effectivePaid: "EUR:2",
- // awaiting: "EUR:2",
- // gone: "EUR:0",
- // granted: "EUR:0",
- // pending: false,
- // proposalId: "1",
- // info: {
- // contractTermsHash: "123",
- // merchant: {
- // name: "the merchant name",
- // },
- // orderId: "orderId1",
- // summary: "the summary",
- // },
- // } as PrepareRefundResult as any),
- // applyRefund: async () => ({}),
- // onUpdateNotification: async () => ({}),
- // } as any,
- ),
- );
-
- {
- const { status, error } = pullLastResultOrThrow();
- expect(status).equals("loading");
- expect(error).undefined;
- }
-
- expect(await waitForStateUpdate()).true;
-
- {
- const state = pullLastResultOrThrow();
-
- if (state.status !== "ready") {
- expect(state).eq({});
- return;
- }
- if (state.error) {
- expect(state).eq({});
- return;
- }
- expect(state.accept.onClick).not.undefined;
- expect(state.merchantName).eq("the merchant name");
- expect(state.orderId).eq("orderId1");
- expect(state.products).undefined;
-
- if (state.ignore.onClick === undefined) expect.fail();
- state.ignore.onClick();
- }
-
- expect(await waitForStateUpdate()).true;
-
- {
- const state = pullLastResultOrThrow();
-
- if (state.status !== "ignored") {
- expect(state).eq({});
- return;
- }
- if (state.error) {
- expect(state).eq({});
- return;
- }
- expect(state.merchantName).eq("the merchant name");
- }
-
- await assertNoPendingUpdate();
- expect(handler.getCallingQueueState()).eq("empty");
- });
-
- it("should be in progress when doing refresh", async () => {
- const { handler, mock } = createWalletApiMock();
- const props = {
- talerRefundUri: "taler://refund/asdasdas",
- cancel: async () => {
- null;
- },
- onSuccess: async () => {
- null;
- },
- };
-
- handler.addWalletCallResponse(WalletApiOperation.PrepareRefund, undefined, {
- awaiting: "EUR:2",
- effectivePaid: "EUR:2",
- gone: "EUR:0",
- granted: "EUR:0",
- pending: true,
- proposalId: "1",
- info: {
- contractTermsHash: "123",
- merchant: {
- name: "the merchant name",
- },
- orderId: "orderId1",
- summary: "the summary",
- } as OrderShortInfo,
- });
- handler.addWalletCallResponse(WalletApiOperation.PrepareRefund, undefined, {
- awaiting: "EUR:1",
- effectivePaid: "EUR:2",
- gone: "EUR:0",
- granted: "EUR:1",
- pending: true,
- proposalId: "1",
- info: {
- contractTermsHash: "123",
- merchant: {
- name: "the merchant name",
- },
- orderId: "orderId1",
- summary: "the summary",
- } as OrderShortInfo,
- });
- handler.addWalletCallResponse(WalletApiOperation.PrepareRefund, undefined, {
- awaiting: "EUR:0",
- effectivePaid: "EUR:2",
- gone: "EUR:0",
- granted: "EUR:2",
- pending: false,
- proposalId: "1",
- info: {
- contractTermsHash: "123",
- merchant: {
- name: "the merchant name",
- },
- orderId: "orderId1",
- summary: "the summary",
- } as OrderShortInfo,
- });
-
- const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
- mountHook(() => useComponentState(props, mock));
-
- {
- const { status, error } = pullLastResultOrThrow();
- expect(status).equals("loading");
- expect(error).undefined;
- }
-
- expect(await waitForStateUpdate()).true;
-
- {
- const state = pullLastResultOrThrow();
-
- if (state.status !== "in-progress") {
- expect(state).eq({});
- return;
- }
- if (state.error) expect.fail();
- expect(state.merchantName).eq("the merchant name");
- expect(state.products).undefined;
- expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:2"));
- // expect(state.progress).closeTo(1 / 3, 0.01)
-
- handler.notifyEventFromWallet(NotificationType.RefreshMelted);
- }
-
- expect(await waitForStateUpdate()).true;
-
- {
- const state = pullLastResultOrThrow();
-
- if (state.status !== "in-progress") {
- expect(state).eq({});
- return;
- }
- if (state.error) expect.fail();
- expect(state.merchantName).eq("the merchant name");
- expect(state.products).undefined;
- expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:2"));
- // expect(state.progress).closeTo(2 / 3, 0.01)
-
- handler.notifyEventFromWallet(NotificationType.RefreshMelted);
- }
-
- expect(await waitForStateUpdate()).true;
-
- {
- const state = pullLastResultOrThrow();
-
- if (state.status !== "ready") {
- expect(state).eq({});
- return;
- }
- if (state.error) expect.fail();
- expect(state.merchantName).eq("the merchant name");
- expect(state.products).undefined;
- expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:2"));
- }
+/**
+ * Commenting this tests out since the behavior
+ */
- await assertNoPendingUpdate();
- expect(handler.getCallingQueueState()).eq("empty");
- });
+describe("Refund CTA states", () => {
+ // it("should tell the user that the URI is missing", async () => {
+ // const { handler, TestingContext } = createWalletApiMock();
+ // const props = {
+ // talerRefundUri: undefined,
+ // cancel: nullFunction,
+ // onSuccess: nullFunction,
+ // };
+ // const hookBehavior = await tests.hookBehaveLikeThis(
+ // useComponentState,
+ // props,
+ // [
+ // ({ status, error }) => {
+ // expect(status).equals("loading");
+ // expect(error).undefined;
+ // },
+ // ({ status, error }) => {
+ // expect(status).equals("error");
+ // if (!error) expect.fail();
+ // // if (!error.hasError) expect.fail();
+ // // if (error.operational) expect.fail();
+ // expect(error.description).eq("ERROR_NO-URI-FOR-REFUND");
+ // },
+ // ],
+ // TestingContext,
+ // );
+ // expect(hookBehavior).deep.equal({ result: "ok" });
+ // expect(handler.getCallingQueueState()).eq("empty");
+ // });
+ // it("should be ready after loading", async () => {
+ // const { handler, TestingContext } = createWalletApiMock();
+ // const props = {
+ // talerRefundUri: "taler://refund/asdasdas",
+ // cancel: nullFunction,
+ // onSuccess: nullFunction,
+ // };
+ // handler.addWalletCallResponse(
+ // WalletApiOperation.StartRefundQueryForUri,
+ // undefined,
+ // {
+ // // awaiting: "EUR:2",
+ // // effectivePaid: "EUR:2",
+ // // gone: "EUR:0",
+ // // granted: "EUR:0",
+ // // pending: false,
+ // // proposalId: "1",
+ // // info: {
+ // // contractTermsHash: "123",
+ // // merchant: {
+ // // name: "the merchant name",
+ // // },
+ // // orderId: "orderId1",
+ // // summary: "the summary",
+ // // } as OrderShortInfo,
+ // },
+ // );
+ // const hookBehavior = await tests.hookBehaveLikeThis(
+ // useComponentState,
+ // props,
+ // [
+ // ({ status, error }) => {
+ // expect(status).equals("loading");
+ // expect(error).undefined;
+ // },
+ // (state) => {
+ // if (state.status !== "ready") expect.fail();
+ // if (state.error) expect.fail();
+ // expect(state.accept.onClick).not.undefined;
+ // expect(state.ignore.onClick).not.undefined;
+ // expect(state.merchantName).eq("the merchant name");
+ // expect(state.orderId).eq("orderId1");
+ // expect(state.products).undefined;
+ // },
+ // ],
+ // TestingContext,
+ // );
+ // expect(hookBehavior).deep.equal({ result: "ok" });
+ // expect(handler.getCallingQueueState()).eq("empty");
+ // });
+ // it("should be ignored after clicking the ignore button", async () => {
+ // const { handler, TestingContext } = createWalletApiMock();
+ // const props = {
+ // talerRefundUri: "taler://refund/asdasdas",
+ // cancel: async () => {
+ // null;
+ // },
+ // onSuccess: async () => {
+ // null;
+ // },
+ // };
+ // handler.addWalletCallResponse(
+ // WalletApiOperation.StartRefundQueryForUri,
+ // undefined,
+ // {
+ // // awaiting: "EUR:2",
+ // // effectivePaid: "EUR:2",
+ // // gone: "EUR:0",
+ // // granted: "EUR:0",
+ // // pending: false,
+ // // proposalId: "1",
+ // // info: {
+ // // contractTermsHash: "123",
+ // // merchant: {
+ // // name: "the merchant name",
+ // // },
+ // // orderId: "orderId1",
+ // // summary: "the summary",
+ // // } as OrderShortInfo,
+ // },
+ // );
+ // const hookBehavior = await tests.hookBehaveLikeThis(
+ // useComponentState,
+ // props,
+ // [
+ // ({ status, error }) => {
+ // expect(status).equals("loading");
+ // expect(error).undefined;
+ // },
+ // (state) => {
+ // if (state.status !== "ready") expect.fail();
+ // if (state.error) expect.fail();
+ // expect(state.accept.onClick).not.undefined;
+ // expect(state.merchantName).eq("the merchant name");
+ // expect(state.orderId).eq("orderId1");
+ // expect(state.products).undefined;
+ // if (state.ignore.onClick === undefined) expect.fail();
+ // state.ignore.onClick();
+ // },
+ // (state) => {
+ // if (state.status !== "ignored") expect.fail();
+ // if (state.error) expect.fail();
+ // expect(state.merchantName).eq("the merchant name");
+ // },
+ // ],
+ // TestingContext,
+ // );
+ // expect(hookBehavior).deep.equal({ result: "ok" });
+ // expect(handler.getCallingQueueState()).eq("empty");
+ // });
+ // it("should be in progress when doing refresh", async () => {
+ // const { handler, TestingContext } = createWalletApiMock();
+ // const props = {
+ // talerRefundUri: "taler://refund/asdasdas",
+ // cancel: async () => {
+ // null;
+ // },
+ // onSuccess: async () => {
+ // null;
+ // },
+ // };
+ // handler.addWalletCallResponse(
+ // WalletApiOperation.StartRefundQueryForUri,
+ // undefined,
+ // {
+ // // awaiting: "EUR:2",
+ // // effectivePaid: "EUR:2",
+ // // gone: "EUR:0",
+ // // granted: "EUR:0",
+ // // pending: true,
+ // // proposalId: "1",
+ // // info: {
+ // // contractTermsHash: "123",
+ // // merchant: {
+ // // name: "the merchant name",
+ // // },
+ // // orderId: "orderId1",
+ // // summary: "the summary",
+ // // } as OrderShortInfo,
+ // },
+ // );
+ // handler.addWalletCallResponse(
+ // WalletApiOperation.StartRefundQueryForUri,
+ // undefined,
+ // {
+ // // awaiting: "EUR:1",
+ // // effectivePaid: "EUR:2",
+ // // gone: "EUR:0",
+ // // granted: "EUR:1",
+ // // pending: true,
+ // // proposalId: "1",
+ // // info: {
+ // // contractTermsHash: "123",
+ // // merchant: {
+ // // name: "the merchant name",
+ // // },
+ // // orderId: "orderId1",
+ // // summary: "the summary",
+ // // } as OrderShortInfo,
+ // },
+ // );
+ // handler.addWalletCallResponse(
+ // WalletApiOperation.StartRefundQueryForUri,
+ // undefined,
+ // {
+ // // awaiting: "EUR:0",
+ // // effectivePaid: "EUR:2",
+ // // gone: "EUR:0",
+ // // granted: "EUR:2",
+ // // pending: false,
+ // // proposalId: "1",
+ // // info: {
+ // // contractTermsHash: "123",
+ // // merchant: {
+ // // name: "the merchant name",
+ // // },
+ // // orderId: "orderId1",
+ // // summary: "the summary",
+ // // } as OrderShortInfo,
+ // },
+ // );
+ // const hookBehavior = await tests.hookBehaveLikeThis(
+ // useComponentState,
+ // props,
+ // [
+ // ({ status, error }) => {
+ // expect(status).equals("loading");
+ // expect(error).undefined;
+ // },
+ // (state) => {
+ // if (state.status !== "in-progress") expect.fail();
+ // if (state.error) expect.fail();
+ // expect(state.merchantName).eq("the merchant name");
+ // expect(state.products).undefined;
+ // expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:2"));
+ // // expect(state.progress).closeTo(1 / 3, 0.01)
+ // handler.notifyEventFromWallet(NotificationType.TransactionStateTransition);
+ // },
+ // (state) => {
+ // if (state.status !== "in-progress") expect.fail();
+ // if (state.error) expect.fail();
+ // expect(state.merchantName).eq("the merchant name");
+ // expect(state.products).undefined;
+ // expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:2"));
+ // // expect(state.progress).closeTo(2 / 3, 0.01)
+ // handler.notifyEventFromWallet(NotificationType.TransactionStateTransition);
+ // },
+ // (state) => {
+ // if (state.status !== "ready") expect.fail();
+ // if (state.error) expect.fail();
+ // expect(state.merchantName).eq("the merchant name");
+ // expect(state.products).undefined;
+ // expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:2"));
+ // },
+ // ],
+ // TestingContext,
+ // );
+ // expect(hookBehavior).deep.equal({ result: "ok" });
+ // expect(handler.getCallingQueueState()).eq("empty");
+ // });
});
diff --git a/packages/taler-wallet-webextension/src/cta/Refund/views.tsx b/packages/taler-wallet-webextension/src/cta/Refund/views.tsx
index 4b5ff70dd..ae4d728f3 100644
--- a/packages/taler-wallet-webextension/src/cta/Refund/views.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Refund/views.tsx
@@ -14,93 +14,63 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { Amounts } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, h, VNode } from "preact";
import { Amount } from "../../components/Amount.js";
-import { LoadingError } from "../../components/LoadingError.js";
-import { LogoHeader } from "../../components/LogoHeader.js";
import { Part } from "../../components/Part.js";
-import { Link, SubTitle, WalletAction } from "../../components/styled/index.js";
-import { useTranslationContext } from "../../context/translation.js";
import { Button } from "../../mui/Button.js";
-import { ProductList } from "../Payment/views.js";
import { State } from "./index.js";
-
-export function LoadingUriView({ error }: State.LoadingUriError): VNode {
- const { i18n } = useTranslationContext();
-
- return (
- <LoadingError
- title={<i18n.Translate>Could not load refund status</i18n.Translate>}
- error={error}
- />
- );
-}
+import { TermsOfService } from "../../components/TermsOfService/index.js";
export function IgnoredView(state: State.Ignored): VNode {
const { i18n } = useTranslationContext();
return (
- <WalletAction>
- <LogoHeader />
-
- <SubTitle>
- <i18n.Translate>Digital cash refund</i18n.Translate>
- </SubTitle>
+ <Fragment>
<section>
<p>
- <i18n.Translate>You&apos;ve ignored the tip.</i18n.Translate>
+ <i18n.Translate>You&apos;ve ignored the refund.</i18n.Translate>
</p>
</section>
- </WalletAction>
+ </Fragment>
);
}
-export function InProgressView(state: State.InProgress): VNode {
- const { i18n } = useTranslationContext();
+// export function InProgressView(state: State.InProgress): VNode {
+// const { i18n } = useTranslationContext();
- return (
- <WalletAction>
- <LogoHeader />
-
- <SubTitle>
- <i18n.Translate>Digital cash refund</i18n.Translate>
- </SubTitle>
- <section>
- <p>
- <i18n.Translate>The refund is in progress.</i18n.Translate>
- </p>
- </section>
- <section>
- <Part
- big
- title={<i18n.Translate>Total to refund</i18n.Translate>}
- text={<Amount value={state.awaitingAmount} />}
- kind="negative"
- />
- <Part
- big
- title={<i18n.Translate>Refunded</i18n.Translate>}
- text={<Amount value={state.amount} />}
- kind="negative"
- />
- </section>
- {state.products && state.products.length ? (
- <section>
- <ProductList products={state.products} />
- </section>
- ) : undefined}
- </WalletAction>
- );
-}
+// return (
+// <Fragment>
+// <section>
+// <p>
+// <i18n.Translate>The refund is in progress.</i18n.Translate>
+// </p>
+// </section>
+// <section>
+// <Part
+// big
+// title={i18n.str`Total to refund`}
+// text={<Amount value={state.awaitingAmount} />}
+// kind="negative"
+// />
+// <Part
+// big
+// title={i18n.str`Refunded`}
+// text={<Amount value={state.amount} />}
+// kind="negative"
+// />
+// </section>
+// {state.products && state.products.length ? (
+// <section>
+// <ProductList products={state.products} />
+// </section>
+// ) : undefined}
+// </Fragment>
+// );
+// }
export function ReadyView(state: State.Ready): VNode {
const { i18n } = useTranslationContext();
return (
- <WalletAction>
- <LogoHeader />
-
- <SubTitle>
- <i18n.Translate>Digital cash refund</i18n.Translate>
- </SubTitle>
+ <Fragment>
<section>
<p>
<i18n.Translate>
@@ -112,30 +82,30 @@ export function ReadyView(state: State.Ready): VNode {
<section>
<Part
big
- title={<i18n.Translate>Order amount</i18n.Translate>}
+ title={i18n.str`Order amount`}
text={<Amount value={state.amount} />}
kind="neutral"
/>
- {Amounts.isNonZero(state.granted) && (
+ {/* {Amounts.isNonZero(state.granted) && (
<Part
big
- title={<i18n.Translate>Already refunded</i18n.Translate>}
+ title={i18n.str`Already refunded`}
text={<Amount value={state.granted} />}
kind="neutral"
/>
)}
<Part
big
- title={<i18n.Translate>Refund offered</i18n.Translate>}
+ title={i18n.str`Refund offered (without fee)`}
text={<Amount value={state.awaitingAmount} />}
kind="positive"
- />
+ /> */}
</section>
- {state.products && state.products.length ? (
+ {/* {state.products && state.products.length ? (
<section>
<ProductList products={state.products} />
</section>
- ) : undefined}
+ ) : undefined} */}
<section>
<Button
variant="contained"
@@ -143,15 +113,11 @@ export function ReadyView(state: State.Ready): VNode {
onClick={state.accept.onClick}
>
<i18n.Translate>
- Accept &nbsp; <Amount value={state.amount} />
+ {/* Accept &nbsp; <Amount value={state.awaitingAmount} /> */}
+ Accept
</i18n.Translate>
</Button>
</section>
- <section>
- <Link upperCased onClick={state.cancel}>
- <i18n.Translate>Cancel</i18n.Translate>
- </Link>
- </section>
- </WalletAction>
+ </Fragment>
);
}
diff --git a/packages/taler-wallet-webextension/src/cta/Tip/state.ts b/packages/taler-wallet-webextension/src/cta/Tip/state.ts
deleted file mode 100644
index ea9ba1b37..000000000
--- a/packages/taler-wallet-webextension/src/cta/Tip/state.ts
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-import { Amounts } from "@gnu-taler/taler-util";
-import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
-import { wxApi } from "../../wxApi.js";
-import { Props, State } from "./index.js";
-
-export function useComponentState(
- { talerTipUri, onCancel, onSuccess }: Props,
- api: typeof wxApi,
-): State {
- const tipInfo = useAsyncAsHook(async () => {
- if (!talerTipUri) throw Error("ERROR_NO-URI-FOR-TIP");
- const tip = await api.wallet.call(WalletApiOperation.PrepareTip, {
- talerTipUri,
- });
- return { tip };
- });
-
- if (!tipInfo) {
- return {
- status: "loading",
- error: undefined,
- };
- }
- if (tipInfo.hasError) {
- return {
- status: "loading-uri",
- error: tipInfo,
- };
- }
-
- const { tip } = tipInfo.response;
-
- const doAccept = async (): Promise<void> => {
- const res = await api.wallet.call(WalletApiOperation.AcceptTip, {
- walletTipId: tip.walletTipId,
- });
-
- //FIX: this may not be seen since we are moving to the success also
- tipInfo.retry();
- onSuccess(res.transactionId);
- };
-
- const baseInfo = {
- merchantBaseUrl: tip.merchantBaseUrl,
- exchangeBaseUrl: tip.exchangeBaseUrl,
- amount: Amounts.parseOrThrow(tip.tipAmountEffective),
- error: undefined,
- cancel: {
- onClick: onCancel,
- },
- };
-
- if (tip.accepted) {
- return {
- status: "accepted",
- ...baseInfo,
- };
- }
-
- return {
- status: "ready",
- ...baseInfo,
- accept: {
- onClick: doAccept,
- },
- };
-}
diff --git a/packages/taler-wallet-webextension/src/cta/Tip/test.ts b/packages/taler-wallet-webextension/src/cta/Tip/test.ts
deleted file mode 100644
index e57b9ec4d..000000000
--- a/packages/taler-wallet-webextension/src/cta/Tip/test.ts
+++ /dev/null
@@ -1,259 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import { Amounts } from "@gnu-taler/taler-util";
-import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-import { expect } from "chai";
-import { mountHook } from "../../test-utils.js";
-import { createWalletApiMock } from "../../test-utils.js";
-import { useComponentState } from "./state.js";
-
-describe("Tip CTA states", () => {
- it("should tell the user that the URI is missing", async () => {
- const { handler, mock } = createWalletApiMock();
-
- const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
- mountHook(() =>
- useComponentState(
- {
- talerTipUri: undefined,
- onCancel: async () => {
- null;
- },
- onSuccess: async () => {
- null;
- },
- },
- mock,
- ),
- );
-
- {
- const { status, error } = pullLastResultOrThrow();
- expect(status).equals("loading");
- expect(error).undefined;
- }
-
- expect(await waitForStateUpdate()).true;
-
- {
- const { status, error } = pullLastResultOrThrow();
-
- expect(status).equals("loading-uri");
- if (!error) expect.fail();
- if (!error.hasError) expect.fail();
- if (error.operational) expect.fail();
- expect(error.message).eq("ERROR_NO-URI-FOR-TIP");
- }
-
- await assertNoPendingUpdate();
- expect(handler.getCallingQueueState()).eq("empty");
- });
-
- it("should be ready for accepting the tip", async () => {
- const { handler, mock } = createWalletApiMock();
-
- handler.addWalletCallResponse(WalletApiOperation.PrepareTip, undefined, {
- accepted: false,
- exchangeBaseUrl: "exchange url",
- merchantBaseUrl: "merchant url",
- tipAmountEffective: "EUR:1",
- walletTipId: "tip_id",
- expirationTimestamp: {
- t_s: 1,
- },
- tipAmountRaw: "",
- });
-
- const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
- mountHook(() =>
- useComponentState(
- {
- talerTipUri: "taler://tip/asd",
- onCancel: async () => {
- null;
- },
- onSuccess: async () => {
- null;
- },
- },
- mock,
- ),
- );
-
- {
- const { status, error } = pullLastResultOrThrow();
- expect(status).equals("loading");
- expect(error).undefined;
- }
-
- expect(await waitForStateUpdate()).true;
-
- {
- const state = pullLastResultOrThrow();
-
- if (state.status !== "ready") {
- expect(state).eq({ status: "ready" });
- return;
- }
- if (state.error) expect.fail();
- expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:1"));
- expect(state.merchantBaseUrl).eq("merchant url");
- expect(state.exchangeBaseUrl).eq("exchange url");
- if (state.accept.onClick === undefined) expect.fail();
-
- handler.addWalletCallResponse(WalletApiOperation.AcceptTip);
- state.accept.onClick();
- }
-
- handler.addWalletCallResponse(WalletApiOperation.PrepareTip, undefined, {
- accepted: true,
- exchangeBaseUrl: "exchange url",
- merchantBaseUrl: "merchant url",
- tipAmountEffective: "EUR:1",
- walletTipId: "tip_id",
- expirationTimestamp: {
- t_s: 1,
- },
- tipAmountRaw: "",
- });
- expect(await waitForStateUpdate()).true;
-
- {
- const state = pullLastResultOrThrow();
-
- if (state.status !== "accepted") {
- expect(state).eq({ status: "accepted" });
- return;
- }
- if (state.error) expect.fail();
- expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:1"));
- expect(state.merchantBaseUrl).eq("merchant url");
- expect(state.exchangeBaseUrl).eq("exchange url");
- }
- await assertNoPendingUpdate();
- expect(handler.getCallingQueueState()).eq("empty");
- });
-
- it("should be ignored after clicking the ignore button", async () => {
- const { handler, mock } = createWalletApiMock();
- handler.addWalletCallResponse(WalletApiOperation.PrepareTip, undefined, {
- exchangeBaseUrl: "exchange url",
- merchantBaseUrl: "merchant url",
- tipAmountEffective: "EUR:1",
- walletTipId: "tip_id",
- accepted: false,
- expirationTimestamp: {
- t_s: 1,
- },
- tipAmountRaw: "",
- });
-
- const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
- mountHook(() =>
- useComponentState(
- {
- talerTipUri: "taler://tip/asd",
- onCancel: async () => {
- null;
- },
- onSuccess: async () => {
- null;
- },
- },
- mock,
- ),
- );
-
- {
- const { status, error } = pullLastResultOrThrow();
- expect(status).equals("loading");
- expect(error).undefined;
- }
-
- expect(await waitForStateUpdate()).true;
-
- {
- const state = pullLastResultOrThrow();
-
- if (state.status !== "ready") expect.fail();
- if (state.error) expect.fail();
- expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:1"));
- expect(state.merchantBaseUrl).eq("merchant url");
- expect(state.exchangeBaseUrl).eq("exchange url");
- }
-
- await assertNoPendingUpdate();
- expect(handler.getCallingQueueState()).eq("empty");
- });
-
- it("should render accepted if the tip has been used previously", async () => {
- const { handler, mock } = createWalletApiMock();
-
- handler.addWalletCallResponse(WalletApiOperation.PrepareTip, undefined, {
- accepted: true,
- exchangeBaseUrl: "exchange url",
- merchantBaseUrl: "merchant url",
- tipAmountEffective: "EUR:1",
- walletTipId: "tip_id",
- expirationTimestamp: {
- t_s: 1,
- },
- tipAmountRaw: "",
- });
-
- const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
- mountHook(() =>
- useComponentState(
- {
- talerTipUri: "taler://tip/asd",
- onCancel: async () => {
- null;
- },
- onSuccess: async () => {
- null;
- },
- },
- mock,
- ),
- );
-
- {
- const { status, error } = pullLastResultOrThrow();
- expect(status).equals("loading");
- expect(error).undefined;
- }
-
- expect(await waitForStateUpdate()).true;
-
- {
- const state = pullLastResultOrThrow();
-
- if (state.status !== "accepted") expect.fail();
- if (state.error) expect.fail();
- expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:1"));
- expect(state.merchantBaseUrl).eq("merchant url");
- expect(state.exchangeBaseUrl).eq("exchange url");
- }
- await assertNoPendingUpdate();
- expect(handler.getCallingQueueState()).eq("empty");
- });
-});
diff --git a/packages/taler-wallet-webextension/src/cta/Tip/views.tsx b/packages/taler-wallet-webextension/src/cta/Tip/views.tsx
deleted file mode 100644
index fbc93c5ab..000000000
--- a/packages/taler-wallet-webextension/src/cta/Tip/views.tsx
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-import { Fragment, h, VNode } from "preact";
-import { Amount } from "../../components/Amount.js";
-import { LoadingError } from "../../components/LoadingError.js";
-import { LogoHeader } from "../../components/LogoHeader.js";
-import { Part } from "../../components/Part.js";
-import { Link, SubTitle, WalletAction } from "../../components/styled/index.js";
-import { useTranslationContext } from "../../context/translation.js";
-import { Button } from "../../mui/Button.js";
-import { State } from "./index.js";
-
-export function LoadingUriView({ error }: State.LoadingUriError): VNode {
- const { i18n } = useTranslationContext();
-
- return (
- <LoadingError
- title={<i18n.Translate>Could not load tip status</i18n.Translate>}
- error={error}
- />
- );
-}
-
-export function IgnoredView(state: State.Ignored): VNode {
- const { i18n } = useTranslationContext();
- return (
- <WalletAction>
- <LogoHeader />
-
- <SubTitle>
- <i18n.Translate>Digital cash tip</i18n.Translate>
- </SubTitle>
- <span>
- <i18n.Translate>You&apos;ve ignored the tip.</i18n.Translate>
- </span>
- </WalletAction>
- );
-}
-
-export function ReadyView(state: State.Ready): VNode {
- const { i18n } = useTranslationContext();
- return (
- <WalletAction>
- <LogoHeader />
-
- <SubTitle>
- <i18n.Translate>Digital cash tip</i18n.Translate>
- </SubTitle>
-
- <section>
- <p>
- <i18n.Translate>The merchant is offering you a tip</i18n.Translate>
- </p>
- <Part
- title={<i18n.Translate>Amount</i18n.Translate>}
- text={<Amount value={state.amount} />}
- kind="positive"
- />
- <Part
- title={<i18n.Translate>Merchant URL</i18n.Translate>}
- text={state.merchantBaseUrl}
- kind="neutral"
- />
- <Part
- title={<i18n.Translate>Exchange</i18n.Translate>}
- text={state.exchangeBaseUrl}
- kind="neutral"
- />
- </section>
- <section>
- <Button
- variant="contained"
- color="success"
- onClick={state.accept.onClick}
- >
- <i18n.Translate>
- Receive &nbsp; {<Amount value={state.amount} />}
- </i18n.Translate>
- </Button>
- </section>
- <section>
- <Link upperCased onClick={state.cancel.onClick}>
- <i18n.Translate>Cancel</i18n.Translate>
- </Link>
- </section>
- </WalletAction>
- );
-}
-
-export function AcceptedView(state: State.Accepted): VNode {
- const { i18n } = useTranslationContext();
- return (
- <WalletAction>
- <LogoHeader />
-
- <SubTitle>
- <i18n.Translate>Digital cash tip</i18n.Translate>
- </SubTitle>
- <section>
- <i18n.Translate>
- Tip from <code>{state.merchantBaseUrl}</code> accepted. Check your
- transactions list for more details.
- </i18n.Translate>
- </section>
- </WalletAction>
- );
-}
diff --git a/packages/taler-wallet-webextension/src/cta/TransferCreate/index.ts b/packages/taler-wallet-webextension/src/cta/TransferCreate/index.ts
index 8d51ff3e0..794d2ad1c 100644
--- a/packages/taler-wallet-webextension/src/cta/TransferCreate/index.ts
+++ b/packages/taler-wallet-webextension/src/cta/TransferCreate/index.ts
@@ -14,17 +14,17 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { AmountJson, 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 { HookError } from "../../hooks/useAsyncAsHook.js";
+import { ErrorAlert } from "../../context/alert.js";
import { ButtonHandler, TextFieldHandler } from "../../mui/handlers.js";
import { compose, StateViewMap } from "../../utils/index.js";
-import { wxApi } from "../../wxApi.js";
import { useComponentState } from "./state.js";
-import { LoadingUriView, ReadyView } from "./views.js";
+import { ReadyView } from "./views.js";
export interface Props {
- amount: string;
+ amount: AmountString;
onClose: () => Promise<void>;
onSuccess: (tx: string) => Promise<void>;
}
@@ -38,8 +38,8 @@ export namespace State {
}
export interface LoadingUriError {
- status: "loading-uri";
- error: HookError;
+ status: "error";
+ error: ErrorAlert;
}
export interface BaseInfo {
@@ -54,18 +54,17 @@ export namespace State {
subject: TextFieldHandler;
expiration: TextFieldHandler;
error: undefined;
- operationError?: TalerErrorDetail;
}
}
const viewMapping: StateViewMap<State> = {
loading: Loading,
- "loading-uri": LoadingUriView,
+ error: ErrorAlertView,
ready: ReadyView,
};
export const TransferCreatePage = compose(
"TransferCreatePage",
- (p: Props) => useComponentState(p, wxApi),
+ (p: Props) => useComponentState(p),
viewMapping,
);
diff --git a/packages/taler-wallet-webextension/src/cta/TransferCreate/state.ts b/packages/taler-wallet-webextension/src/cta/TransferCreate/state.ts
index c5e143f42..f092801ed 100644
--- a/packages/taler-wallet-webextension/src/cta/TransferCreate/state.ts
+++ b/packages/taler-wallet-webextension/src/cta/TransferCreate/state.ts
@@ -15,37 +15,36 @@
*/
import {
+ AmountString,
Amounts,
- TalerErrorDetail,
+ TalerErrorCode,
TalerProtocolTimestamp,
} from "@gnu-taler/taler-util";
-import { TalerError, WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-import { format, isFuture, parse } from "date-fns";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { isFuture, parse } from "date-fns";
import { useState } from "preact/hooks";
+import { alertFromError, useAlertContext } from "../../context/alert.js";
+import { useBackendContext } from "../../context/backend.js";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
-import { wxApi } from "../../wxApi.js";
+import { BackgroundError, WxApiType } from "../../wxApi.js";
import { Props, State } from "./index.js";
-export function useComponentState(
- { amount: amountStr, onClose, onSuccess }: Props,
- api: typeof wxApi,
-): State {
+export function useComponentState({
+ amount: amountStr,
+ onClose,
+ onSuccess,
+}: Props): State {
+ const api = useBackendContext();
+ const { pushAlertOnError } = useAlertContext();
const amount = Amounts.parseOrThrow(amountStr);
+ const { i18n } = useTranslationContext();
const [subject, setSubject] = useState<string | undefined>();
const [timestamp, setTimestamp] = useState<string | undefined>();
- const [operationError, setOperationError] = useState<
- TalerErrorDetail | undefined
- >(undefined);
-
const hook = useAsyncAsHook(async () => {
- const resp = await api.wallet.call(
- WalletApiOperation.PreparePeerPushPayment,
- {
- amount: amountStr,
- },
- );
+ const resp = await checkPeerPushDebitAndCheckMax(api, amountStr);
return resp;
});
@@ -57,14 +56,18 @@ export function useComponentState(
}
if (hook.hasError) {
return {
- status: "loading-uri",
- error: hook,
+ status: "error",
+ error: alertFromError(
+ i18n,
+ i18n.str`Could not load the max amount to transfer`,
+ hook,
+ ),
};
}
const { amountEffective, amountRaw } = hook.response;
- const debitAmount = Amounts.parseOrThrow(amountRaw);
- const toBeReceived = Amounts.parseOrThrow(amountEffective);
+ const debitAmount = Amounts.parseOrThrow(amountEffective);
+ const toBeReceived = Amounts.parseOrThrow(amountRaw);
let purse_expiration: TalerProtocolTimestamp | undefined = undefined;
let timestampError: string | undefined = undefined;
@@ -90,25 +93,17 @@ export function useComponentState(
async function accept(): Promise<void> {
if (!subject || !purse_expiration) return;
- try {
- const resp = await api.wallet.call(
- WalletApiOperation.InitiatePeerPushPayment,
- {
- partialContractTerms: {
- summary: subject,
- amount: amountStr,
- purse_expiration,
- },
+ const resp = await api.wallet.call(
+ WalletApiOperation.InitiatePeerPushDebit,
+ {
+ partialContractTerms: {
+ summary: subject,
+ amount: amountStr,
+ purse_expiration,
},
- );
- onSuccess(resp.transactionId);
- } catch (e) {
- if (e instanceof TalerError) {
- setOperationError(e.errorDetail);
- }
- console.error(e);
- throw Error("error trying to accept");
- }
+ },
+ );
+ onSuccess(resp.transactionId);
}
const unableToCreate =
@@ -117,31 +112,74 @@ export function useComponentState(
return {
status: "ready",
cancel: {
- onClick: onClose,
+ onClick: pushAlertOnError(onClose),
},
subject: {
error:
subject === undefined
? undefined
: !subject
- ? "Can't be empty"
- : undefined,
+ ? "Can't be empty"
+ : undefined,
value: subject ?? "",
- onInput: async (e) => setSubject(e),
+ onInput: pushAlertOnError(async (e) => setSubject(e)),
},
expiration: {
error: timestampError,
value: timestamp === undefined ? "" : timestamp,
- onInput: async (e) => {
+ onInput: pushAlertOnError(async (e) => {
setTimestamp(e);
- },
+ }),
},
create: {
- onClick: unableToCreate ? undefined : accept,
+ onClick: unableToCreate ? undefined : pushAlertOnError(accept),
},
debitAmount,
toBeReceived,
error: undefined,
- operationError,
};
}
+
+async function checkPeerPushDebitAndCheckMax(
+ api: WxApiType,
+ amountState: AmountString,
+) {
+ // FIXME : https://bugs.gnunet.org/view.php?id=7872
+ try {
+ return await api.wallet.call(WalletApiOperation.CheckPeerPushDebit, {
+ amount: amountState,
+ });
+ } catch (e) {
+ if (!(e instanceof BackgroundError)) {
+ throw e;
+ }
+ if (
+ !e.hasErrorCode(
+ TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
+ )
+ ) {
+ throw e;
+ }
+ const material = Amounts.parseOrThrow(
+ e.errorDetail.insufficientBalanceDetails.balanceMaterial,
+ );
+ const amount = Amounts.parseOrThrow(amountState);
+ const gap = Amounts.sub(
+ amount,
+ Amounts.parseOrThrow(
+ e.errorDetail.insufficientBalanceDetails.maxEffectiveSpendAmount,
+ ),
+ ).amount;
+ const newAmount = Amounts.sub(material, gap).amount;
+ if (Amounts.cmp(newAmount, amount) === 0) {
+ //insufficient balance and the exception didn't give
+ //a good response that allow us to try again
+ throw e;
+ }
+ if (Amounts.cmp(newAmount, amount) === 1) {
+ //how can this happen?
+ throw e;
+ }
+ return checkPeerPushDebitAndCheckMax(api, Amounts.stringify(newAmount));
+ }
+}
diff --git a/packages/taler-wallet-webextension/src/cta/TransferCreate/stories.tsx b/packages/taler-wallet-webextension/src/cta/TransferCreate/stories.tsx
index d0650f562..8e9fbbe63 100644
--- a/packages/taler-wallet-webextension/src/cta/TransferCreate/stories.tsx
+++ b/packages/taler-wallet-webextension/src/cta/TransferCreate/stories.tsx
@@ -19,14 +19,15 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { createExample } from "../../test-utils.js";
+import * as tests from "@gnu-taler/web-util/testing";
+import { nullFunction } from "../../mui/handlers.js";
import { ReadyView } from "./views.js";
export default {
title: "transfer create",
};
-export const Ready = createExample(ReadyView, {
+export const Ready = tests.createExample(ReadyView, {
debitAmount: {
currency: "ARS",
value: 1,
@@ -44,8 +45,6 @@ export const Ready = createExample(ReadyView, {
},
subject: {
value: "the subject",
- onInput: async () => {
- null;
- },
+ onInput: nullFunction,
},
});
diff --git a/packages/taler-wallet-webextension/src/cta/TransferCreate/views.tsx b/packages/taler-wallet-webextension/src/cta/TransferCreate/views.tsx
index 0b034e3fb..bc855f33d 100644
--- a/packages/taler-wallet-webextension/src/cta/TransferCreate/views.tsx
+++ b/packages/taler-wallet-webextension/src/cta/TransferCreate/views.tsx
@@ -14,39 +14,24 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { format } from "date-fns";
-import { h, VNode } from "preact";
-import { ErrorTalerOperation } from "../../components/ErrorTalerOperation.js";
-import { LoadingError } from "../../components/LoadingError.js";
-import { LogoHeader } from "../../components/LogoHeader.js";
+import { Fragment, h, VNode } from "preact";
import { Part } from "../../components/Part.js";
-import { QR } from "../../components/QR.js";
-import { Link, SubTitle, WalletAction } from "../../components/styled/index.js";
-import { useTranslationContext } from "../../context/translation.js";
import { Button } from "../../mui/Button.js";
import { TextField } from "../../mui/TextField.js";
-import { TransferDetails } from "../../wallet/Transaction.js";
+import {
+ getAmountWithFee,
+ TransferCreationDetails,
+} from "../../wallet/Transaction.js";
import { State } from "./index.js";
-export function LoadingUriView({ error }: State.LoadingUriError): VNode {
- const { i18n } = useTranslationContext();
-
- return (
- <LoadingError
- title={<i18n.Translate>Could not load</i18n.Translate>}
- error={error}
- />
- );
-}
-
export function ReadyView({
subject,
expiration,
toBeReceived,
debitAmount,
create,
- operationError,
- cancel,
}: State.Ready): VNode {
const { i18n } = useTranslationContext();
@@ -65,37 +50,21 @@ export function ReadyView({
);
}
}
- async function _20DaysExpiration() {
+ async function _30DaysExpiration() {
if (expiration.onInput) {
expiration.onInput(
- format(new Date().getTime() + 1000 * 60 * 60 * 24 * 20, "dd/MM/yyyy"),
+ format(new Date().getTime() + 1000 * 60 * 60 * 24 * 30, "dd/MM/yyyy"),
);
}
}
return (
- <WalletAction>
- <LogoHeader />
- <SubTitle>
- <i18n.Translate>Digital cash transfer</i18n.Translate>
- </SubTitle>
- {operationError && (
- <ErrorTalerOperation
- title={
- <i18n.Translate>
- Could not finish the transfer creation
- </i18n.Translate>
- }
- error={operationError}
- />
- )}
+ <Fragment>
<section style={{ textAlign: "left" }}>
<p>
<TextField
label="Subject"
variant="filled"
- helperText={
- <i18n.Translate>Short description of the transfer</i18n.Translate>
- }
+ helperText={i18n.str`Short description of the transfer`}
error={subject.error}
required
fullWidth
@@ -131,20 +100,17 @@ export function ReadyView({
<Button
variant="outlined"
disabled={!expiration.onInput}
- onClick={_20DaysExpiration}
+ onClick={_30DaysExpiration}
>
- 20 days
+ 30 days
</Button>
</p>
</p>
<Part
- title={<i18n.Translate>Details</i18n.Translate>}
+ title={i18n.str`Details`}
text={
- <TransferDetails
- amount={{
- effective: toBeReceived,
- raw: debitAmount,
- }}
+ <TransferCreationDetails
+ amount={getAmountWithFee(debitAmount, toBeReceived, "debit")}
/>
}
/>
@@ -154,13 +120,6 @@ export function ReadyView({
<i18n.Translate>Create</i18n.Translate>
</Button>
</section>
- <section>
- <section>
- <Link upperCased onClick={cancel.onClick}>
- <i18n.Translate>Cancel</i18n.Translate>
- </Link>
- </section>
- </section>
- </WalletAction>
+ </Fragment>
);
}
diff --git a/packages/taler-wallet-webextension/src/cta/TransferPickup/index.ts b/packages/taler-wallet-webextension/src/cta/TransferPickup/index.ts
index 399f1e290..4e1301d6a 100644
--- a/packages/taler-wallet-webextension/src/cta/TransferPickup/index.ts
+++ b/packages/taler-wallet-webextension/src/cta/TransferPickup/index.ts
@@ -19,13 +19,13 @@ import {
AmountJson,
TalerErrorDetail,
} from "@gnu-taler/taler-util";
+import { ErrorAlertView } from "../../components/CurrentAlerts.js";
import { Loading } from "../../components/Loading.js";
-import { HookError } from "../../hooks/useAsyncAsHook.js";
+import { ErrorAlert } from "../../context/alert.js";
import { ButtonHandler } from "../../mui/handlers.js";
import { compose, StateViewMap } from "../../utils/index.js";
-import { wxApi } from "../../wxApi.js";
import { useComponentState } from "./state.js";
-import { LoadingUriView, ReadyView } from "./views.js";
+import { ReadyView } from "./views.js";
export interface Props {
talerPayPushUri: string;
@@ -42,8 +42,8 @@ export namespace State {
}
export interface LoadingUriError {
- status: "loading-uri";
- error: HookError;
+ status: "error";
+ error: ErrorAlert;
}
export interface BaseInfo {
@@ -52,23 +52,24 @@ export namespace State {
}
export interface Ready extends BaseInfo {
status: "ready";
- amount: AmountJson;
+ effective: AmountJson;
+ exchangeBaseUrl: string;
+ raw: AmountJson;
summary: string | undefined;
expiration: AbsoluteTime | undefined;
error: undefined;
accept: ButtonHandler;
- operationError?: TalerErrorDetail;
}
}
const viewMapping: StateViewMap<State> = {
loading: Loading,
- "loading-uri": LoadingUriView,
+ error: ErrorAlertView,
ready: ReadyView,
};
export const TransferPickupPage = compose(
"TransferPickupPage",
- (p: Props) => useComponentState(p, wxApi),
+ (p: Props) => useComponentState(p),
viewMapping,
);
diff --git a/packages/taler-wallet-webextension/src/cta/TransferPickup/state.ts b/packages/taler-wallet-webextension/src/cta/TransferPickup/state.ts
index e8fb99ab7..67f6d9113 100644
--- a/packages/taler-wallet-webextension/src/cta/TransferPickup/state.ts
+++ b/packages/taler-wallet-webextension/src/cta/TransferPickup/state.ts
@@ -17,27 +17,28 @@
import {
AbsoluteTime,
Amounts,
- TalerErrorDetail,
TalerProtocolTimestamp,
} from "@gnu-taler/taler-util";
-import { TalerError, WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-import { useState } from "preact/hooks";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { alertFromError, useAlertContext } from "../../context/alert.js";
+import { useBackendContext } from "../../context/backend.js";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
-import { wxApi } from "../../wxApi.js";
import { Props, State } from "./index.js";
-export function useComponentState(
- { talerPayPushUri, onClose, onSuccess }: Props,
- api: typeof wxApi,
-): State {
+export function useComponentState({
+ talerPayPushUri,
+ onClose,
+ onSuccess,
+}: Props): State {
+ const api = useBackendContext();
+ const { pushAlertOnError } = useAlertContext();
+ const { i18n } = useTranslationContext();
const hook = useAsyncAsHook(async () => {
- return await api.wallet.call(WalletApiOperation.CheckPeerPushPayment, {
+ return await api.wallet.call(WalletApiOperation.PreparePeerPushCredit, {
talerUri: talerPayPushUri,
});
}, []);
- const [operationError, setOperationError] = useState<
- TalerErrorDetail | undefined
- >(undefined);
if (!hook) {
return {
@@ -47,47 +48,52 @@ export function useComponentState(
}
if (hook.hasError) {
return {
- status: "loading-uri",
- error: hook,
+ status: "error",
+ error: alertFromError(
+ i18n,
+ i18n.str`Could not load the invoice payment status`,
+ hook,
+ ),
};
}
- const { contractTerms, peerPushPaymentIncomingId } = hook.response;
+ const {
+ contractTerms,
+ transactionId,
+ amountEffective,
+ amountRaw,
+ exchangeBaseUrl,
+ } = hook.response;
- const amount: string = contractTerms?.amount;
- const summary: string | undefined = contractTerms?.summary;
- const expiration: TalerProtocolTimestamp | undefined =
- contractTerms?.purse_expiration;
+ const effective = Amounts.parseOrThrow(amountEffective);
+ const raw = Amounts.parseOrThrow(amountRaw);
+ const summary: string = contractTerms.summary;
+ const expiration: TalerProtocolTimestamp = contractTerms.purse_expiration;
async function accept(): Promise<void> {
- try {
- const resp = await api.wallet.call(
- WalletApiOperation.AcceptPeerPushPayment,
- {
- peerPushPaymentIncomingId,
- },
- );
- onSuccess(resp.transactionId);
- } catch (e) {
- if (e instanceof TalerError) {
- setOperationError(e.errorDetail);
- }
- console.error(e);
- throw Error("error trying to accept");
- }
+ const resp = await api.wallet.call(
+ WalletApiOperation.ConfirmPeerPushCredit,
+ {
+ transactionId,
+ },
+ );
+ onSuccess(resp.transactionId);
}
return {
status: "ready",
- amount: Amounts.parseOrThrow(amount),
+ effective,
+ exchangeBaseUrl,
+ raw,
error: undefined,
accept: {
- onClick: accept,
+ onClick: pushAlertOnError(accept),
},
summary,
- expiration: expiration ? AbsoluteTime.fromTimestamp(expiration) : undefined,
+ expiration: expiration
+ ? AbsoluteTime.fromProtocolTimestamp(expiration)
+ : undefined,
cancel: {
- onClick: onClose,
+ onClick: pushAlertOnError(onClose),
},
- operationError,
};
}
diff --git a/packages/taler-wallet-webextension/src/cta/TransferPickup/stories.tsx b/packages/taler-wallet-webextension/src/cta/TransferPickup/stories.tsx
index 250e99ae1..4fb230cd9 100644
--- a/packages/taler-wallet-webextension/src/cta/TransferPickup/stories.tsx
+++ b/packages/taler-wallet-webextension/src/cta/TransferPickup/stories.tsx
@@ -19,23 +19,29 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { createExample } from "../../test-utils.js";
+import * as tests from "@gnu-taler/web-util/testing";
import { ReadyView } from "./views.js";
+import { AbsoluteTime } from "@gnu-taler/taler-util";
export default {
title: "transfer pickup",
};
-export const Ready = createExample(ReadyView, {
- amount: {
+export const Ready = tests.createExample(ReadyView, {
+ effective: {
currency: "ARS",
value: 1,
fraction: 0,
},
- summary: "some subject",
- expiration: {
- t_ms: new Date().getTime() + 1000 * 60 * 60,
+ raw: {
+ currency: "ARS",
+ value: 1,
+ fraction: 0,
},
+ summary: "some subject",
+ expiration: AbsoluteTime.fromMilliseconds(
+ new Date().getTime() + 1000 * 60 * 60,
+ ),
accept: {},
cancel: {},
});
diff --git a/packages/taler-wallet-webextension/src/cta/TransferPickup/views.tsx b/packages/taler-wallet-webextension/src/cta/TransferPickup/views.tsx
index c43b0ff52..caa1b485a 100644
--- a/packages/taler-wallet-webextension/src/cta/TransferPickup/views.tsx
+++ b/packages/taler-wallet-webextension/src/cta/TransferPickup/views.tsx
@@ -14,81 +14,57 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { h, VNode } from "preact";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
import { Amount } from "../../components/Amount.js";
-import { ErrorTalerOperation } from "../../components/ErrorTalerOperation.js";
-import { LoadingError } from "../../components/LoadingError.js";
-import { LogoHeader } from "../../components/LogoHeader.js";
import { Part } from "../../components/Part.js";
-import { Link, SubTitle, WalletAction } from "../../components/styled/index.js";
import { Time } from "../../components/Time.js";
-import { useTranslationContext } from "../../context/translation.js";
import { Button } from "../../mui/Button.js";
+import {
+ getAmountWithFee,
+ TransferPickupDetails,
+} from "../../wallet/Transaction.js";
import { State } from "./index.js";
-
-export function LoadingUriView({ error }: State.LoadingUriError): VNode {
- const { i18n } = useTranslationContext();
-
- return (
- <LoadingError
- title={<i18n.Translate>Could not load</i18n.Translate>}
- error={error}
- />
- );
-}
+import { TermsOfService } from "../../components/TermsOfService/index.js";
export function ReadyView({
accept,
summary,
expiration,
- amount,
- cancel,
- operationError,
+ effective,
+ exchangeBaseUrl,
+ raw,
}: State.Ready): VNode {
const { i18n } = useTranslationContext();
return (
- <WalletAction>
- <LogoHeader />
- <SubTitle>
- <i18n.Translate>Digital cash transfer</i18n.Translate>
- </SubTitle>
- {operationError && (
- <ErrorTalerOperation
- title={
- <i18n.Translate>
- Could not finish the pickup operation
- </i18n.Translate>
- }
- error={operationError}
- />
- )}
+ <Fragment>
<section style={{ textAlign: "left" }}>
+ <Part title={i18n.str`Subject`} text={<div>{summary}</div>} />
+ <Part title={i18n.str`Amount`} text={<Amount value={raw} />} />
<Part
- title={<i18n.Translate>Subject</i18n.Translate>}
- text={<div>{summary}</div>}
- />
- <Part
- title={<i18n.Translate>Amount</i18n.Translate>}
- text={<Amount value={amount} />}
+ title={i18n.str`Details`}
+ text={
+ <TransferPickupDetails
+ amount={getAmountWithFee(effective, raw, "credit")}
+ />
+ }
/>
+
<Part
- title={<i18n.Translate>Valid until</i18n.Translate>}
+ title={i18n.str`Valid until`}
text={<Time timestamp={expiration} format="dd MMMM yyyy, HH:mm" />}
kind="neutral"
/>
</section>
<section>
- <Button variant="contained" color="success" onClick={accept.onClick}>
- <i18n.Translate>
- Receive &nbsp; {<Amount value={amount} />}
- </i18n.Translate>
- </Button>
- </section>
- <section>
- <Link upperCased onClick={cancel.onClick}>
- <i18n.Translate>Cancel</i18n.Translate>
- </Link>
+ <TermsOfService key="terms" exchangeUrl={exchangeBaseUrl} >
+ <Button variant="contained" color="success" onClick={accept.onClick}>
+ <i18n.Translate>
+ Receive &nbsp; {<Amount value={effective} />}
+ </i18n.Translate>
+ </Button>
+ </TermsOfService>
</section>
- </WalletAction>
+ </Fragment>
);
}
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts b/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts
index 68b314c07..1f8745a5d 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts
@@ -14,21 +14,31 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { AmountJson, ExchangeListItem } from "@gnu-taler/taler-util";
+import {
+ AmountJson,
+ AmountString,
+ CurrencySpecification,
+ ExchangeListItem,
+ WithdrawalExchangeAccountDetails,
+} from "@gnu-taler/taler-util";
import { Loading } from "../../components/Loading.js";
-import { HookError } from "../../hooks/useAsyncAsHook.js";
import { State as SelectExchangeState } from "../../hooks/useSelectedExchange.js";
-import { ButtonHandler, SelectFieldHandler } from "../../mui/handlers.js";
-import { compose, StateViewMap } from "../../utils/index.js";
-import { wxApi } from "../../wxApi.js";
+import {
+ AmountFieldHandler,
+ ButtonHandler,
+ SelectFieldHandler,
+} from "../../mui/handlers.js";
+import { StateViewMap, compose } from "../../utils/index.js";
import {
useComponentStateFromParams,
useComponentStateFromURI,
} from "./state.js";
+import { ErrorAlertView } from "../../components/CurrentAlerts.js";
+import { ErrorAlert } from "../../context/alert.js";
import { ExchangeSelectionPage } from "../../wallet/ExchangeSelection/index.js";
import { NoExchangesView } from "../../wallet/ExchangeSelection/views.js";
-import { LoadingInfoView, LoadingUriView, SuccessView } from "./views.js";
+import { FinalStateOperation, SelectAmountView, SuccessView } from "./views.js";
export interface PropsFromURI {
talerWithdrawUri: string | undefined;
@@ -37,17 +47,20 @@ export interface PropsFromURI {
}
export interface PropsFromParams {
- amount: string;
+ talerExchangeWithdrawUri: string | undefined;
+ amount: string | undefined;
cancel: () => Promise<void>;
onSuccess: (txid: string) => Promise<void>;
+ onAmountChanged: (amount: AmountString) => Promise<void>;
}
export type State =
| State.Loading
| State.LoadingUriError
- | State.LoadingInfoError
- | SelectExchangeState.NoExchange
+ | SelectExchangeState.NoExchangeFound
| SelectExchangeState.Selecting
+ | State.SelectAmount
+ | State.AlreadyCompleted
| State.Success;
export namespace State {
@@ -56,12 +69,23 @@ export namespace State {
error: undefined;
}
export interface LoadingUriError {
- status: "uri-error";
- error: HookError;
+ status: "error";
+ error: ErrorAlert;
}
- export interface LoadingInfoError {
- status: "amount-error";
- error: HookError;
+
+ export interface SelectAmount {
+ status: "select-amount";
+ error: undefined;
+ exchangeBaseUrl: string;
+ confirm: ButtonHandler;
+ amount: AmountFieldHandler;
+ currency: string;
+ }
+ export interface AlreadyCompleted {
+ status: "already-completed";
+ operationState: "confirmed" | "aborted" | "selected";
+ confirmTransferUrl?: string,
+ error: undefined;
}
export type Success = {
@@ -77,30 +101,38 @@ export namespace State {
doWithdrawal: ButtonHandler;
doSelectExchange: ButtonHandler;
+ chooseCurrencies: string[];
+ selectedCurrency: string;
+ changeCurrency: (s: string) => void;
+ conversionInfo: {
+ spec: CurrencySpecification,
+ amount: AmountJson,
+ } | undefined;
+
ageRestriction?: SelectFieldHandler;
talerWithdrawUri?: string;
cancel: () => Promise<void>;
- onTosUpdate: () => void;
};
}
const viewMapping: StateViewMap<State> = {
loading: Loading,
- "uri-error": LoadingUriView,
- "amount-error": LoadingInfoView,
- "no-exchange": NoExchangesView,
+ error: ErrorAlertView,
+ "select-amount": SelectAmountView,
+ "no-exchange-found": NoExchangesView,
"selecting-exchange": ExchangeSelectionPage,
success: SuccessView,
+ "already-completed": FinalStateOperation,
};
export const WithdrawPageFromURI = compose(
- "WithdrawPageFromURI",
- (p: PropsFromURI) => useComponentStateFromURI(p, wxApi),
+ "WithdrawPageFromURI_Withdraw",
+ (p: PropsFromURI) => useComponentStateFromURI(p),
viewMapping,
);
export const WithdrawPageFromParams = compose(
"WithdrawPageFromParams",
- (p: PropsFromParams) => useComponentStateFromParams(p, wxApi),
+ (p: PropsFromParams) => useComponentStateFromParams(p),
viewMapping,
);
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts b/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts
index 9bb29fbd6..f2fa04902 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts
@@ -14,45 +14,146 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-/* eslint-disable react-hooks/rules-of-hooks */
import {
AmountJson,
Amounts,
+ ExchangeFullDetails,
ExchangeListItem,
- ExchangeTosStatus,
+ NotificationType,
+ parseWithdrawExchangeUri
} from "@gnu-taler/taler-util";
-import { TalerError, WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-import { useState } from "preact/hooks";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { useCallback, useEffect, useState } from "preact/hooks";
+import { alertFromError, useAlertContext } from "../../context/alert.js";
+import { useBackendContext } from "../../context/backend.js";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import { useSelectedExchange } from "../../hooks/useSelectedExchange.js";
import { RecursiveState } from "../../utils/index.js";
-import { wxApi } from "../../wxApi.js";
import { PropsFromParams, PropsFromURI, State } from "./index.js";
-export function useComponentStateFromParams(
- { amount, cancel, onSuccess }: PropsFromParams,
- api: typeof wxApi,
-): RecursiveState<State> {
+export function useComponentStateFromParams({
+ talerExchangeWithdrawUri: maybeTalerUri,
+ amount,
+ cancel,
+ onAmountChanged,
+ onSuccess,
+}: PropsFromParams): RecursiveState<State> {
+ const api = useBackendContext();
+ const { i18n } = useTranslationContext();
+ const paramsAmount = amount ? Amounts.parse(amount) : undefined;
const uriInfoHook = useAsyncAsHook(async () => {
const exchanges = await api.wallet.call(
WalletApiOperation.ListExchanges,
{},
);
- return { amount: Amounts.parseOrThrow(amount), exchanges };
+ const uri = maybeTalerUri
+ ? parseWithdrawExchangeUri(maybeTalerUri)
+ : undefined;
+ const exchangeByTalerUri = uri?.exchangeBaseUrl;
+ let ex: ExchangeFullDetails | undefined;
+ if (exchangeByTalerUri) {
+ await api.wallet.call(WalletApiOperation.AddExchange, {
+ exchangeBaseUrl: exchangeByTalerUri,
+ masterPub: uri.exchangePub,
+ });
+ const info = await api.wallet.call(
+ WalletApiOperation.GetExchangeDetailedInfo,
+ {
+ exchangeBaseUrl: exchangeByTalerUri,
+ },
+ );
+
+ ex = info.exchange;
+ }
+ const chosenAmount =
+ !uri || !uri.amount ? undefined : Amounts.parse(uri.amount);
+ return { amount: chosenAmount, exchanges, exchange: ex };
});
if (!uriInfoHook) return { status: "loading", error: undefined };
if (uriInfoHook.hasError) {
return {
- status: "uri-error",
- error: uriInfoHook,
+ status: "error",
+ error: alertFromError(
+ i18n,
+ i18n.str`Could not load the list of exchanges`,
+ uriInfoHook,
+ ),
};
}
- const chosenAmount = uriInfoHook.response.amount;
+ useEffect(() => {
+ uriInfoHook?.retry();
+ }, [amount]);
+
+ const exchangeByTalerUri = uriInfoHook.response.exchange?.exchangeBaseUrl;
const exchangeList = uriInfoHook.response.exchanges.exchanges;
+ const maybeAmount = uriInfoHook.response.amount ?? paramsAmount;
+
+ if (!maybeAmount) {
+ const exchangeBaseUrl =
+ uriInfoHook.response.exchange?.exchangeBaseUrl ??
+ (exchangeList.length > 0 ? exchangeList[0].exchangeBaseUrl : undefined);
+ const currency =
+ uriInfoHook.response.exchange?.currency ??
+ (exchangeList.length > 0 ? exchangeList[0].currency : undefined);
+
+ if (!exchangeBaseUrl) {
+ return {
+ status: "error",
+ error: {
+ message: i18n.str`Can't withdraw from exchange`,
+ description: i18n.str`Missing base URL`,
+ cause: undefined,
+ context: {},
+ type: "error",
+ },
+ };
+ }
+ if (!currency) {
+ return {
+ status: "error",
+ error: {
+ message: i18n.str`Can't withdraw from exchange`,
+ description: i18n.str`Missing unknown currency`,
+ cause: undefined,
+ context: {},
+ type: "error",
+ },
+ };
+ }
+ return () => {
+ const { pushAlertOnError } = useAlertContext();
+ const [amount, setAmount] = useState<AmountJson>(
+ Amounts.zeroOfCurrency(currency),
+ );
+ const isValid = Amounts.isNonZero(amount);
+ return {
+ status: "select-amount",
+ currency,
+ exchangeBaseUrl,
+ error: undefined,
+ confirm: {
+ onClick: isValid
+ ? pushAlertOnError(async () => {
+ onAmountChanged(Amounts.stringify(amount));
+ })
+ : undefined,
+ },
+ amount: {
+ value: amount,
+ onInput: pushAlertOnError(async (e) => {
+ setAmount(e);
+ }),
+ },
+ };
+ };
+ }
+ const chosenAmount = maybeAmount;
+
async function doManualWithdraw(
exchange: string,
ageRestricted: number | undefined,
@@ -76,49 +177,85 @@ export function useComponentStateFromParams(
return () =>
exchangeSelectionState(
- uriInfoHook.retry,
doManualWithdraw,
cancel,
onSuccess,
undefined,
chosenAmount,
exchangeList,
- undefined,
- api,
+ exchangeByTalerUri,
);
}
-export function useComponentStateFromURI(
- { talerWithdrawUri, cancel, onSuccess }: PropsFromURI,
- api: typeof wxApi,
-): RecursiveState<State> {
+export function useComponentStateFromURI({
+ talerWithdrawUri: maybeTalerUri,
+ cancel,
+ onSuccess,
+}: PropsFromURI): RecursiveState<State> {
+ const api = useBackendContext();
+ const { i18n } = useTranslationContext();
/**
* Ask the wallet about the withdraw URI
*/
const uriInfoHook = useAsyncAsHook(async () => {
- if (!talerWithdrawUri) throw Error("ERROR_NO-URI-FOR-WITHDRAWAL");
+ if (!maybeTalerUri) throw Error("ERROR_NO-URI-FOR-WITHDRAWAL");
+ const talerWithdrawUri = maybeTalerUri.startsWith("ext+")
+ ? maybeTalerUri.substring(4)
+ : maybeTalerUri;
const uriInfo = await api.wallet.call(
WalletApiOperation.GetWithdrawalDetailsForUri,
{
talerWithdrawUri,
+ notifyChangeFromPendingTimeoutMs: 30 * 1000,
},
);
- const { amount, defaultExchangeBaseUrl } = uriInfo;
+ const {
+ amount,
+ defaultExchangeBaseUrl,
+ possibleExchanges,
+ operationId,
+ confirmTransferUrl,
+ status,
+ } = uriInfo;
+ const transaction = await api.wallet.call(
+ WalletApiOperation.GetWithdrawalTransactionByUri,
+ { talerWithdrawUri },
+ );
return {
talerWithdrawUri,
+ operationId,
+ status,
+ transaction,
+ confirmTransferUrl,
amount: Amounts.parseOrThrow(amount),
thisExchange: defaultExchangeBaseUrl,
- exchanges: uriInfo.possibleExchanges,
+ exchanges: possibleExchanges,
};
});
+ const readyToListen = uriInfoHook && !uriInfoHook.hasError;
+
+ useEffect(() => {
+ if (!uriInfoHook) {
+ return;
+ }
+ return api.listener.onUpdateNotification(
+ [NotificationType.WithdrawalOperationTransition],
+ uriInfoHook.retry,
+ );
+ }, [readyToListen]);
+
if (!uriInfoHook) return { status: "loading", error: undefined };
if (uriInfoHook.hasError) {
return {
- status: "uri-error",
- error: uriInfoHook,
+ status: "error",
+ error: alertFromError(
+ i18n,
+ i18n.str`Could not load info from URI`,
+ uriInfoHook,
+ ),
};
}
@@ -148,9 +285,20 @@ export function useComponentStateFromURI(
};
}
- return () =>
- exchangeSelectionState(
- uriInfoHook.retry,
+ if (uriInfoHook.response.status !== "pending") {
+ if (uriInfoHook.response.transaction) {
+ onSuccess(uriInfoHook.response.transaction.transactionId);
+ }
+ return {
+ status: "already-completed",
+ operationState: uriInfoHook.response.status,
+ confirmTransferUrl: uriInfoHook.response.confirmTransferUrl,
+ error: undefined,
+ };
+ }
+
+ return useCallback(() => {
+ return exchangeSelectionState(
doManagedWithdraw,
cancel,
onSuccess,
@@ -158,8 +306,8 @@ export function useComponentStateFromURI(
chosenAmount,
exchangeList,
defaultExchange,
- api,
);
+ }, []);
}
type ManualOrManagedWithdrawFunction = (
@@ -168,19 +316,18 @@ type ManualOrManagedWithdrawFunction = (
) => Promise<{ transactionId: string; confirmTransferUrl: string | undefined }>;
function exchangeSelectionState(
- onTosUpdate: () => void,
doWithdraw: ManualOrManagedWithdrawFunction,
cancel: () => Promise<void>,
onSuccess: (txid: string) => Promise<void>,
talerWithdrawUri: string | undefined,
chosenAmount: AmountJson,
exchangeList: ExchangeListItem[],
- defaultExchange: string | undefined,
- api: typeof wxApi,
+ exchangeSuggestedByTheBank: string | undefined,
): RecursiveState<State> {
+ const api = useBackendContext();
const selectedExchange = useSelectedExchange({
currency: chosenAmount.currency,
- defaultExchange,
+ defaultExchange: exchangeSuggestedByTheBank,
list: exchangeList,
});
@@ -188,13 +335,18 @@ function exchangeSelectionState(
return selectedExchange;
}
- return () => {
+ return useCallback(():
+ | State.Success
+ | State.LoadingUriError
+ | State.Loading => {
+ const { i18n } = useTranslationContext();
+ const { pushAlertOnError } = useAlertContext();
const [ageRestricted, setAgeRestricted] = useState(0);
const currentExchange = selectedExchange.selected;
- const tosNeedToBeAccepted =
- currentExchange.tosStatus == ExchangeTosStatus.New ||
- currentExchange.tosStatus == ExchangeTosStatus.Changed;
+ const [selectedCurrency, setSelectedCurrency] = useState<string>(
+ chosenAmount.currency,
+ );
/**
* With the exchange and amount, ask the wallet the information
* about the withdrawal
@@ -217,12 +369,10 @@ function exchangeSelectionState(
return {
amount: withdrawAmount,
ageRestrictionOptions: info.ageRestrictionOptions,
+ accounts: info.withdrawalAccountsList,
};
}, []);
- const [withdrawError, setWithdrawError] = useState<TalerError | undefined>(
- undefined,
- );
const [doingWithdraw, setDoingWithdraw] = useState<boolean>(false);
async function doWithdrawAndCheckError(): Promise<void> {
@@ -238,9 +388,9 @@ function exchangeSelectionState(
onSuccess(res.transactionId);
}
} catch (e) {
- if (e instanceof TalerError) {
- setWithdrawError(e);
- }
+ console.error(e);
+ // if (e instanceof TalerError) {
+ // }
}
setDoingWithdraw(false);
}
@@ -250,8 +400,12 @@ function exchangeSelectionState(
}
if (amountHook.hasError) {
return {
- status: "amount-error",
- error: amountHook,
+ status: "error",
+ error: alertFromError(
+ i18n,
+ i18n.str`Could not load the withdrawal details`,
+ amountHook,
+ ),
};
}
if (!amountHook.response) {
@@ -278,31 +432,57 @@ function exchangeSelectionState(
//TODO: calculate based on exchange info
const ageRestriction = ageRestrictionEnabled
? {
- list: ageRestrictionOptions,
- value: String(ageRestricted),
- onChange: async (v: string) => setAgeRestricted(parseInt(v, 10)),
- }
+ list: ageRestrictionOptions,
+ value: String(ageRestricted),
+ onChange: pushAlertOnError(async (v: string) =>
+ setAgeRestricted(parseInt(v, 10)),
+ ),
+ }
: undefined;
+ const altCurrencies = amountHook.response.accounts
+ .filter((a) => !!a.currencySpecification)
+ .map((a) => a.currencySpecification!.name);
+ const chooseCurrencies =
+ altCurrencies.length === 0
+ ? []
+ : [toBeReceived.currency, ...altCurrencies];
+
+ const convAccount = amountHook.response.accounts.find((c) => {
+ return (
+ c.currencySpecification &&
+ c.currencySpecification.name === selectedCurrency
+ );
+ });
+ const conversionInfo = !convAccount
+ ? undefined
+ : {
+ spec: convAccount.currencySpecification!,
+ amount: Amounts.parseOrThrow(convAccount.transferAmount!),
+ };
+
return {
status: "success",
error: undefined,
doSelectExchange: selectedExchange.doSelect,
currentExchange,
toBeReceived,
+ chooseCurrencies,
+ selectedCurrency,
+ changeCurrency: (s) => {
+ setSelectedCurrency(s);
+ },
+ conversionInfo,
withdrawalFee,
chosenAmount,
talerWithdrawUri,
ageRestriction,
doWithdrawal: {
- onClick:
- doingWithdraw || tosNeedToBeAccepted
- ? undefined
- : doWithdrawAndCheckError,
- error: withdrawError,
+ onClick: doingWithdraw
+ ? undefined
+ : pushAlertOnError(doWithdrawAndCheckError),
},
- onTosUpdate,
cancel,
};
- };
+ }, []);
}
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx b/packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx
index a8031223b..29f39054f 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx
@@ -19,37 +19,16 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { ExchangeListItem } from "@gnu-taler/taler-util";
-import { createExample } from "../../test-utils.js";
+import { CurrencySpecification, ExchangeListItem } from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
+import { nullFunction } from "../../mui/handlers.js";
// import { TermsState } from "../../utils/index.js";
-import { SuccessView } from "./views.js";
+import { SuccessView, FinalStateOperation } from "./views.js";
export default {
title: "withdraw",
};
-const exchangeList = {
- "exchange.demo.taler.net": "http://exchange.demo.taler.net (USD)",
- "exchange.test.taler.net": "http://exchange.test.taler.net (KUDOS)",
-};
-
-const nullHandler = {
- onClick: async (): Promise<void> => {
- null;
- },
-};
-
-// const normalTosState = {
-// terms: {
-// status: "accepted",
-// version: "",
-// } as TermsState,
-// onAccept: () => null,
-// onReview: () => null,
-// reviewed: false,
-// reviewing: false,
-// };
-
const ageRestrictionOptions: Record<string, string> = "6:12:18"
.split(":")
.reduce((p, c) => ({ ...p, [c]: `under ${c}` }), {});
@@ -61,7 +40,7 @@ const ageRestrictionSelectField = {
value: "0",
};
-export const TermsOfServiceNotYetLoaded = createExample(SuccessView, {
+export const TermsOfServiceNotYetLoaded = tests.createExample(SuccessView, {
error: undefined,
status: "success",
chosenAmount: {
@@ -69,7 +48,7 @@ export const TermsOfServiceNotYetLoaded = createExample(SuccessView, {
value: 2,
fraction: 10000000,
},
- doWithdrawal: nullHandler,
+ doWithdrawal: { onClick: nullFunction },
currentExchange: {
exchangeBaseUrl: "https://exchange.demo.taler.net",
tos: {},
@@ -85,9 +64,27 @@ export const TermsOfServiceNotYetLoaded = createExample(SuccessView, {
fraction: 0,
value: 1,
},
+ chooseCurrencies: [],
});
-export const WithSomeFee = createExample(SuccessView, {
+export const AlreadyAborted = tests.createExample(FinalStateOperation, {
+ error: undefined,
+ status: "already-completed",
+ operationState: "aborted"
+});
+export const AlreadySelected = tests.createExample(FinalStateOperation, {
+ error: undefined,
+ status: "already-completed",
+ operationState: "selected"
+});
+export const AlreadyConfirmed = tests.createExample(FinalStateOperation, {
+ error: undefined,
+ status: "already-completed",
+ operationState: "confirmed"
+});
+
+
+export const WithSomeFee = tests.createExample(SuccessView, {
error: undefined,
status: "success",
chosenAmount: {
@@ -95,7 +92,7 @@ export const WithSomeFee = createExample(SuccessView, {
value: 2,
fraction: 10000000,
},
- doWithdrawal: nullHandler,
+ doWithdrawal: { onClick: nullFunction },
currentExchange: {
exchangeBaseUrl: "https://exchange.demo.taler.net",
tos: {},
@@ -111,9 +108,10 @@ export const WithSomeFee = createExample(SuccessView, {
value: 1,
},
doSelectExchange: {},
+ chooseCurrencies: [],
});
-export const WithoutFee = createExample(SuccessView, {
+export const WithoutFee = tests.createExample(SuccessView, {
error: undefined,
status: "success",
chosenAmount: {
@@ -121,7 +119,7 @@ export const WithoutFee = createExample(SuccessView, {
value: 2,
fraction: 0,
},
- doWithdrawal: nullHandler,
+ doWithdrawal: { onClick: nullFunction },
currentExchange: {
exchangeBaseUrl: "https://exchange.demo.taler.net",
tos: {},
@@ -137,9 +135,10 @@ export const WithoutFee = createExample(SuccessView, {
fraction: 0,
value: 2,
},
+ chooseCurrencies: [],
});
-export const EditExchangeUntouched = createExample(SuccessView, {
+export const EditExchangeUntouched = tests.createExample(SuccessView, {
error: undefined,
status: "success",
chosenAmount: {
@@ -147,7 +146,7 @@ export const EditExchangeUntouched = createExample(SuccessView, {
value: 2,
fraction: 10000000,
},
- doWithdrawal: nullHandler,
+ doWithdrawal: { onClick: nullFunction },
currentExchange: {
exchangeBaseUrl: "https://exchange.demo.taler.net",
tos: {},
@@ -163,9 +162,10 @@ export const EditExchangeUntouched = createExample(SuccessView, {
fraction: 0,
value: 2,
},
+ chooseCurrencies: [],
});
-export const EditExchangeModified = createExample(SuccessView, {
+export const EditExchangeModified = tests.createExample(SuccessView, {
error: undefined,
status: "success",
chosenAmount: {
@@ -173,7 +173,7 @@ export const EditExchangeModified = createExample(SuccessView, {
value: 2,
fraction: 10000000,
},
- doWithdrawal: nullHandler,
+ doWithdrawal: { onClick: nullFunction },
currentExchange: {
exchangeBaseUrl: "https://exchange.demo.taler.net",
tos: {},
@@ -189,9 +189,10 @@ export const EditExchangeModified = createExample(SuccessView, {
fraction: 0,
value: 2,
},
+ chooseCurrencies: [],
});
-export const WithAgeRestriction = createExample(SuccessView, {
+export const WithAgeRestriction = tests.createExample(SuccessView, {
error: undefined,
status: "success",
ageRestriction: ageRestrictionSelectField,
@@ -201,7 +202,7 @@ export const WithAgeRestriction = createExample(SuccessView, {
fraction: 10000000,
},
doSelectExchange: {},
- doWithdrawal: nullHandler,
+ doWithdrawal: { onClick: nullFunction },
currentExchange: {
exchangeBaseUrl: "https://exchange.demo.taler.net",
tos: {},
@@ -216,4 +217,111 @@ export const WithAgeRestriction = createExample(SuccessView, {
fraction: 0,
value: 2,
},
+ chooseCurrencies: [],
+});
+
+export const WithAlternateCurrenciesNETZBON = tests.createExample(SuccessView, {
+ error: undefined,
+ status: "success",
+ chosenAmount: {
+ currency: "NETZBON",
+ value: 2,
+ fraction: 10000000,
+ },
+ chooseCurrencies: ["NETZBON", "EUR"],
+ selectedCurrency: "NETZBON",
+ doWithdrawal: { onClick: nullFunction },
+ currentExchange: {
+ exchangeBaseUrl: "https://exchange.netzbon.ch",
+ tos: {},
+ } as Partial<ExchangeListItem> as any,
+ withdrawalFee: {
+ currency: "NETZBON",
+ fraction: 10000000,
+ value: 1,
+ },
+ doSelectExchange: {},
+ toBeReceived: {
+ currency: "NETZBON",
+ fraction: 0,
+ value: 1,
+ },
+});
+
+export const WithAlternateCurrenciesEURO = tests.createExample(SuccessView, {
+ error: undefined,
+ status: "success",
+ chosenAmount: {
+ currency: "NETZBON",
+ value: 2,
+ fraction: 10000000,
+ },
+ chooseCurrencies: ["NETZBON", "EUR"],
+ selectedCurrency: "EUR",
+ changeCurrency: () => { },
+ conversionInfo: {
+ spec: {
+ 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: {
+ currency: "NETZBON",
+ fraction: 10000000,
+ value: 1,
+ },
+ doSelectExchange: {},
+ toBeReceived: {
+ currency: "NETZBON",
+ fraction: 0,
+ value: 1,
+ },
+});
+
+export const WithAlternateCurrenciesEURO11 = tests.createExample(SuccessView, {
+ error: undefined,
+ status: "success",
+ chosenAmount: {
+ currency: "NETZBON",
+ value: 2,
+ fraction: 10000000,
+ },
+ chooseCurrencies: ["NETZBON", "EUR"],
+ selectedCurrency: "EUR",
+ changeCurrency: () => { },
+ conversionInfo: {
+ spec: {
+ 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: {
+ currency: "NETZBON",
+ fraction: 10000000,
+ value: 1,
+ },
+ doSelectExchange: {},
+ toBeReceived: {
+ currency: "NETZBON",
+ fraction: 0,
+ 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 7fd8188ce..f90f7bed7 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts
@@ -20,14 +20,16 @@
*/
import {
+ AmountString,
Amounts,
ExchangeEntryStatus,
ExchangeListItem,
ExchangeTosStatus,
+ ScopeType,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { expect } from "chai";
-import { mountHook } from "../../test-utils.js";
+import * as tests from "@gnu-taler/web-util/testing";
import { createWalletApiMock } from "../../test-utils.js";
import { useComponentStateFromURI } from "./state.js";
@@ -37,7 +39,7 @@ const exchanges: ExchangeListItem[] = [
exchangeBaseUrl: "http://exchange.demo.taler.net",
paytoUris: [],
tosStatus: ExchangeTosStatus.Accepted,
- exchangeStatus: ExchangeEntryStatus.Ok,
+ exchangeStatus: ExchangeEntryStatus.Used,
permanent: true,
auditors: [
{
@@ -61,174 +63,181 @@ const exchanges: ExchangeListItem[] = [
} as Partial<ExchangeListItem> as ExchangeListItem,
];
+const nullFunction = async (): Promise<void> => {
+ null;
+};
+
describe("Withdraw CTA states", () => {
it("should tell the user that the URI is missing", async () => {
- const { handler, mock } = createWalletApiMock();
+ const { handler, TestingContext } = createWalletApiMock();
+
const props = {
talerWithdrawUri: undefined,
- cancel: async () => {
- null;
- },
- onSuccess: async () => {
- null;
- },
+ cancel: nullFunction,
+ onSuccess: nullFunction,
};
- const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
- mountHook(() => useComponentStateFromURI(props, mock));
-
- {
- const { status } = pullLastResultOrThrow();
- expect(status).equals("loading");
- }
-
- expect(await waitForStateUpdate()).true;
-
- {
- const { status, error } = pullLastResultOrThrow();
- if (status != "uri-error") expect.fail();
- if (!error) expect.fail();
- if (!error.hasError) expect.fail();
- if (error.operational) expect.fail();
- expect(error.message).eq("ERROR_NO-URI-FOR-WITHDRAWAL");
- }
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentStateFromURI,
+ props,
+ [
+ ({ status }) => {
+ expect(status).equals("loading");
+ },
+ ({ status, error }) => {
+ if (status != "error") expect.fail();
+ if (!error) expect.fail();
+ // if (!error.hasError) expect.fail();
+ // if (error.operational) expect.fail();
+ expect(error.description).eq("ERROR_NO-URI-FOR-WITHDRAWAL");
+ },
+ ],
+ TestingContext,
+ );
- await assertNoPendingUpdate();
+ expect(hookBehavior).deep.equal({ result: "ok" });
expect(handler.getCallingQueueState()).eq("empty");
});
it("should tell the user that there is not known exchange", async () => {
- const { handler, mock } = createWalletApiMock();
+ const { handler, TestingContext } = createWalletApiMock();
const props = {
talerWithdrawUri: "taler-withdraw://",
- cancel: async () => {
- null;
- },
- onSuccess: async () => {
- null;
- },
+ cancel: nullFunction,
+ onSuccess: nullFunction,
};
+
handler.addWalletCallResponse(
WalletApiOperation.GetWithdrawalDetailsForUri,
undefined,
{
- amount: "EUR:2",
+ status: "pending",
+ operationId: "123",
+ amount: "EUR:2" as AmountString,
possibleExchanges: [],
},
);
+ handler.addWalletCallResponse(
+ WalletApiOperation.GetWithdrawalTransactionByUri,
+ undefined,
+ {
+ transactionId: "123"
+ } as any,
+ );
- const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
- mountHook(() => useComponentStateFromURI(props, mock));
-
- {
- const { status } = pullLastResultOrThrow();
- expect(status).equals("loading", "1");
- }
-
- expect(await waitForStateUpdate()).true;
-
- {
- const { status, error } = pullLastResultOrThrow();
-
- expect(status).equals("no-exchange", "3");
-
- expect(error).undefined;
- }
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentStateFromURI,
+ props,
+ [
+ ({ status }) => {
+ expect(status).equals("loading");
+ },
+ ({ status, error }) => {
+ expect(status).equals("no-exchange-found");
+ expect(error).undefined;
+ },
+ ],
+ TestingContext,
+ );
- await assertNoPendingUpdate();
+ expect(hookBehavior).deep.equal({ result: "ok" });
expect(handler.getCallingQueueState()).eq("empty");
});
it("should be able to withdraw if tos are ok", async () => {
- const { handler, mock } = createWalletApiMock();
+ const { handler, TestingContext } = createWalletApiMock();
const props = {
talerWithdrawUri: "taler-withdraw://",
- cancel: async () => {
- null;
- },
- onSuccess: async () => {
- null;
- },
+ cancel: nullFunction,
+ onSuccess: nullFunction,
};
+
handler.addWalletCallResponse(
WalletApiOperation.GetWithdrawalDetailsForUri,
undefined,
{
- amount: "ARS:2",
+ status: "pending",
+ operationId: "123",
+ amount: "ARS:2" as AmountString,
possibleExchanges: exchanges,
defaultExchangeBaseUrl: exchanges[0].exchangeBaseUrl,
},
);
handler.addWalletCallResponse(
+ WalletApiOperation.GetWithdrawalTransactionByUri,
+ undefined,
+ {
+ transactionId: "123"
+ } as any,
+ );
+ handler.addWalletCallResponse(
WalletApiOperation.GetWithdrawalDetailsForAmount,
undefined,
{
- amountRaw: "ARS:2",
- amountEffective: "ARS:2",
+ amountRaw: "ARS:2" as AmountString,
+ amountEffective: "ARS:2" as AmountString,
paytoUris: ["payto://"],
tosAccepted: true,
+ scopeInfo: {
+ currency: "ARS",
+ type: ScopeType.Exchange,
+ url: "http://asd"
+ },
+ withdrawalAccountsList: [],
ageRestrictionOptions: [],
+ numCoins: 42,
},
);
- const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
- mountHook(() => useComponentStateFromURI(props, mock));
-
- {
- const { status, error } = pullLastResultOrThrow();
- expect(status).equals("loading");
- expect(error).undefined;
- }
-
- expect(await waitForStateUpdate()).true;
-
- {
- const { status, error } = pullLastResultOrThrow();
-
- expect(status).equals("loading");
-
- expect(error).undefined;
- }
-
- expect(await waitForStateUpdate()).true;
-
- {
- const state = pullLastResultOrThrow();
- expect(state.status).equals("success");
- 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.chosenAmount).deep.equal(Amounts.parseOrThrow("ARS:2"));
-
- expect(state.doWithdrawal.onClick).not.undefined;
- }
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentStateFromURI,
+ props,
+ [
+ ({ status }) => {
+ expect(status).equals("loading");
+ },
+ ({ status, error }) => {
+ expect(status).equals("loading");
+ expect(error).undefined;
+ },
+ (state) => {
+ expect(state.status).equals("success");
+ 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.chosenAmount).deep.equal(Amounts.parseOrThrow("ARS:2"));
+
+ expect(state.doWithdrawal.onClick).not.undefined;
+ },
+ ],
+ TestingContext,
+ );
- await assertNoPendingUpdate();
+ expect(hookBehavior).deep.equal({ result: "ok" });
expect(handler.getCallingQueueState()).eq("empty");
});
- it("should accept the tos before withdraw", async () => {
- const { handler, mock } = createWalletApiMock();
+ it.skip("should accept the tos before withdraw", async () => {
+ const { handler, TestingContext } = createWalletApiMock();
const props = {
talerWithdrawUri: "taler-withdraw://",
- cancel: async () => {
- null;
- },
- onSuccess: async () => {
- null;
- },
+ cancel: nullFunction,
+ onSuccess: nullFunction,
};
+
const exchangeWithNewTos = exchanges.map((e) => ({
...e,
- tosStatus: ExchangeTosStatus.New,
+ tosStatus: ExchangeTosStatus.Proposed,
}));
handler.addWalletCallResponse(
WalletApiOperation.GetWithdrawalDetailsForUri,
undefined,
{
- amount: "ARS:2",
+ status: "pending",
+ operationId: "123",
+ amount: "ARS:2" as AmountString,
possibleExchanges: exchangeWithNewTos,
defaultExchangeBaseUrl: exchangeWithNewTos[0].exchangeBaseUrl,
},
@@ -237,11 +246,18 @@ describe("Withdraw CTA states", () => {
WalletApiOperation.GetWithdrawalDetailsForAmount,
undefined,
{
- amountRaw: "ARS:2",
- amountEffective: "ARS:2",
+ amountRaw: "ARS:2" as AmountString,
+ amountEffective: "ARS:2" as AmountString,
paytoUris: ["payto://"],
+ scopeInfo: {
+ currency: "ARS",
+ type: ScopeType.Exchange,
+ url: "http://asd"
+ },
tosAccepted: false,
+ withdrawalAccountsList: [],
ageRestrictionOptions: [],
+ numCoins: 42,
},
);
@@ -249,63 +265,40 @@ describe("Withdraw CTA states", () => {
WalletApiOperation.GetWithdrawalDetailsForUri,
undefined,
{
- amount: "ARS:2",
+ status: "pending",
+ operationId: "123",
+ amount: "ARS:2" as AmountString,
possibleExchanges: exchanges,
defaultExchangeBaseUrl: exchanges[0].exchangeBaseUrl,
},
);
- const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
- mountHook(() => useComponentStateFromURI(props, mock));
-
- {
- const { status, error } = pullLastResultOrThrow();
- expect(status).equals("loading");
- expect(error).undefined;
- }
-
- expect(await waitForStateUpdate()).true;
-
- {
- const { status, error } = pullLastResultOrThrow();
-
- expect(status).equals("loading");
-
- expect(error).undefined;
- }
-
- expect(await waitForStateUpdate()).true;
-
- {
- const state = pullLastResultOrThrow();
- expect(state.status).equals("success");
- 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.chosenAmount).deep.equal(Amounts.parseOrThrow("ARS:2"));
-
- expect(state.doWithdrawal.onClick).undefined;
-
- // updateAcceptedVersionToCurrentVersion();
- state.onTosUpdate();
- }
-
- expect(await waitForStateUpdate()).true;
-
- {
- const state = pullLastResultOrThrow();
- expect(state.status).equals("success");
- 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.chosenAmount).deep.equal(Amounts.parseOrThrow("ARS:2"));
-
- expect(state.doWithdrawal.onClick).not.undefined;
- }
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentStateFromURI,
+ props,
+ [
+ ({ status }) => {
+ expect(status).equals("loading");
+ },
+ ({ status, error }) => {
+ expect(status).equals("loading");
+ expect(error).undefined;
+ },
+ (state) => {
+ expect(state.status).equals("success");
+ 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.chosenAmount).deep.equal(Amounts.parseOrThrow("ARS:2"));
+
+ expect(state.doWithdrawal.onClick).not.undefined;
+ },
+ ],
+ TestingContext,
+ );
- await assertNoPendingUpdate();
+ expect(hookBehavior).deep.equal({ result: "ok" });
expect(handler.getCallingQueueState()).eq("empty");
});
});
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx b/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx
index 5c35151c8..aade67835 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx
@@ -14,77 +14,60 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { Fragment, h, VNode } from "preact";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { Amount } from "../../components/Amount.js";
-import { ErrorTalerOperation } from "../../components/ErrorTalerOperation.js";
-import { LoadingError } from "../../components/LoadingError.js";
-import { LogoHeader } from "../../components/LogoHeader.js";
+import { AmountField } from "../../components/AmountField.js";
import { Part } from "../../components/Part.js";
import { QR } from "../../components/QR.js";
import { SelectList } from "../../components/SelectList.js";
-import {
- Input,
- Link,
- LinkSuccess,
- SubTitle,
- SvgIcon,
- WalletAction,
-} from "../../components/styled/index.js";
-import { useTranslationContext } from "../../context/translation.js";
-import { Button } from "../../mui/Button.js";
-import editIcon from "../../svg/edit_24px.svg";
-import { ExchangeDetails, WithdrawDetails } from "../../wallet/Transaction.js";
import { TermsOfService } from "../../components/TermsOfService/index.js";
+import { Input, LinkSuccess, SvgIcon, WarningBox } from "../../components/styled/index.js";
+import { Button } from "../../mui/Button.js";
+import { Grid } from "../../mui/Grid.js";
+import editIcon from "../../svg/edit_24px.inline.svg";
+import {
+ ExchangeDetails,
+ WithdrawDetails,
+ getAmountWithFee,
+} from "../../wallet/Transaction.js";
import { State } from "./index.js";
-import { ExchangeTosStatus } from "@gnu-taler/taler-util";
-
-export function LoadingUriView({ error }: State.LoadingUriError): VNode {
- const { i18n } = useTranslationContext();
-
- return (
- <LoadingError
- title={
- <i18n.Translate>Could not get the info from the URI</i18n.Translate>
- }
- error={error}
- />
- );
-}
+import { EnabledBySettings } from "../../components/EnabledBySettings.js";
-export function LoadingInfoView({ error }: State.LoadingInfoError): VNode {
+export function FinalStateOperation(state: State.AlreadyCompleted): VNode {
const { i18n } = useTranslationContext();
- return (
- <LoadingError
- title={<i18n.Translate>Could not get info of withdrawal</i18n.Translate>}
- error={error}
- />
- );
+ switch (state.operationState) {
+ case "confirmed": return <WarningBox>
+ <div style={{ justifyContent: "center", lineHeight: "25px" }}>
+ <i18n.Translate>This operation has already been completed by another wallet.</i18n.Translate>
+ </div>
+ </WarningBox>
+ case "aborted": return <WarningBox>
+ <div style={{ justifyContent: "center", lineHeight: "25px" }}>
+ <i18n.Translate>This operation has already been aborted</i18n.Translate>
+ </div>
+ </WarningBox>
+ case "selected": return <WarningBox>
+ <div style={{ justifyContent: "center", lineHeight: "25px" }}>
+ <i18n.Translate>This operation has already been used by another wallet.</i18n.Translate>
+ </div>
+ <div style={{ justifyContent: "center", lineHeight: "25px" }}>
+ <i18n.Translate>It can be confirmed in</i18n.Translate>&nbsp;<a target="_bank" rel="noreferrer" href={state.confirmTransferUrl}>
+ <i18n.Translate>this page</i18n.Translate>
+ </a>
+ </div>
+ </WarningBox>
+ }
}
export function SuccessView(state: State.Success): VNode {
const { i18n } = useTranslationContext();
- const currentTosVersionIsAccepted =
- state.currentExchange.tosStatus === ExchangeTosStatus.Accepted;
+ // const currentTosVersionIsAccepted =
+ // state.currentExchange.tosStatus === ExchangeTosStatus.Accepted;
return (
- <WalletAction>
- <LogoHeader />
- <SubTitle>
- <i18n.Translate>Digital cash withdrawal</i18n.Translate>
- </SubTitle>
-
- {state.doWithdrawal.error && (
- <ErrorTalerOperation
- title={
- <i18n.Translate>
- Could not finish the withdrawal operation
- </i18n.Translate>
- }
- error={state.doWithdrawal.error.errorDetail}
- />
- )}
-
+ <Fragment>
<section style={{ textAlign: "left" }}>
<Part
title={
@@ -95,13 +78,15 @@ export function SuccessView(state: State.Success): VNode {
}}
>
<i18n.Translate>Exchange</i18n.Translate>
- <Button onClick={state.doSelectExchange.onClick} variant="text">
- <SvgIcon
- title="Edit"
- dangerouslySetInnerHTML={{ __html: editIcon }}
- color="black"
- />
- </Button>
+ <EnabledBySettings name="showExchangeManagement">
+ <Button onClick={state.doSelectExchange.onClick} variant="text">
+ <SvgIcon
+ title="Edit"
+ dangerouslySetInnerHTML={{ __html: editIcon }}
+ color="black"
+ />
+ </Button>
+ </EnabledBySettings>
</div>
}
text={
@@ -110,21 +95,39 @@ export function SuccessView(state: State.Success): VNode {
kind="neutral"
big
/>
+ {state.chooseCurrencies.length > 0 ?
+ <Fragment>
+ <p>
+ {state.chooseCurrencies.map(currency => {
+ return <Button variant={currency === state.selectedCurrency ? "contained" : "outlined"}
+ onClick={async () => {
+ state.changeCurrency(currency)
+ }}
+ >
+ {currency}
+ </Button>
+ })}
+ </p>
+ </Fragment>
+ : <Fragment />}
+
<Part
- title={<i18n.Translate>Details</i18n.Translate>}
+ title={i18n.str`Details`}
text={
<WithdrawDetails
- amount={{
- effective: state.toBeReceived,
- raw: state.chosenAmount,
- }}
+ conversion={state.conversionInfo?.amount}
+ amount={getAmountWithFee(
+ state.toBeReceived,
+ state.chosenAmount,
+ "credit",
+ )}
/>
}
/>
{state.ageRestriction && (
<Input>
<SelectList
- label={<i18n.Translate>Age restriction</i18n.Translate>}
+ label={i18n.str`Age restriction`}
list={state.ageRestriction.list}
name="age"
value={state.ageRestriction.value}
@@ -135,7 +138,8 @@ export function SuccessView(state: State.Success): VNode {
</section>
<section>
- {currentTosVersionIsAccepted ? (
+ {/* <div> */}
+ <TermsOfService exchangeUrl={state.currentExchange.exchangeBaseUrl}>
<Button
variant="contained"
color="success"
@@ -146,22 +150,26 @@ export function SuccessView(state: State.Success): VNode {
Withdraw &nbsp; <Amount value={state.toBeReceived} />
</i18n.Translate>
</Button>
- ) : (
- <TermsOfService
- exchangeUrl={state.currentExchange.exchangeBaseUrl}
- onChange={state.onTosUpdate}
- />
- )}
+ </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} />
) : undefined}
- <section>
- <Link upperCased onClick={state.cancel}>
- <i18n.Translate>Cancel</i18n.Translate>
- </Link>
- </section>
- </WalletAction>
+ </Fragment>
);
}
@@ -176,11 +184,7 @@ function WithdrawWithMobile({
return (
<section>
<LinkSuccess upperCased onClick={() => setShowQR((qr) => !qr)}>
- {!showQR ? (
- <i18n.Translate>Withdraw to a mobile phone</i18n.Translate>
- ) : (
- <i18n.Translate>Hide QR</i18n.Translate>
- )}
+ {!showQR ? i18n.str`Withdraw to a mobile phone` : i18n.str`Hide QR`}
</LinkSuccess>
{showQR && (
<div>
@@ -196,3 +200,46 @@ function WithdrawWithMobile({
</section>
);
}
+
+export function SelectAmountView({
+ currency,
+ amount,
+ exchangeBaseUrl,
+ confirm,
+}: State.SelectAmount): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <Fragment>
+ <section style={{ textAlign: "left" }}>
+ <Part
+ title={
+ <div
+ style={{
+ display: "flex",
+ alignItems: "center",
+ }}
+ >
+ <i18n.Translate>Exchange</i18n.Translate>
+ </div>
+ }
+ text={<ExchangeDetails exchange={exchangeBaseUrl} />}
+ kind="neutral"
+ big
+ />
+ <Grid container columns={2} justifyContent="space-between">
+ <AmountField label={i18n.str`Amount`} required handler={amount} />
+ </Grid>
+ </section>
+ <section>
+ <Button
+ variant="contained"
+ color="info"
+ disabled={!confirm.onClick}
+ onClick={confirm.onClick}
+ >
+ <i18n.Translate>See details</i18n.Translate>
+ </Button>
+ </section>
+ </Fragment>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/cta/index.stories.ts b/packages/taler-wallet-webextension/src/cta/index.stories.ts
index 84863f84f..36e9cd1b9 100644
--- a/packages/taler-wallet-webextension/src/cta/index.stories.ts
+++ b/packages/taler-wallet-webextension/src/cta/index.stories.ts
@@ -22,7 +22,6 @@
export * as a1 from "./Deposit/stories.jsx";
export * as a3 from "./Payment/stories.jsx";
export * as a4 from "./Refund/stories.jsx";
-export * as a5 from "./Tip/stories.jsx";
export * as a6 from "./Withdraw/stories.jsx";
export * as a8 from "./InvoiceCreate/stories.js";
export * as a9 from "./InvoicePay/stories.js";
diff --git a/packages/taler-wallet-webextension/src/hooks/useAsyncAsHook.ts b/packages/taler-wallet-webextension/src/hooks/useAsyncAsHook.ts
index 1b2929317..bd430f2ef 100644
--- a/packages/taler-wallet-webextension/src/hooks/useAsyncAsHook.ts
+++ b/packages/taler-wallet-webextension/src/hooks/useAsyncAsHook.ts
@@ -13,9 +13,9 @@
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { TalerErrorDetail } from "@gnu-taler/taler-util";
-import { TalerError } from "@gnu-taler/taler-wallet-core";
+import { TalerErrorDetail, TalerError } from "@gnu-taler/taler-util";
import { useEffect, useMemo, useState } from "preact/hooks";
+import { BackgroundError } from "../wxApi.js";
export interface HookOk<T> {
hasError: false;
@@ -26,13 +26,14 @@ export type HookError = HookGenericError | HookOperationalError;
export interface HookGenericError {
hasError: true;
- operational: false;
+ type: "error";
message: string;
}
export interface HookOperationalError {
hasError: true;
- operational: true;
+ type: "taler";
+ message: string;
details: TalerErrorDetail;
}
@@ -47,14 +48,13 @@ export type HookResponseWithRetry<T> =
export function useAsyncAsHook<T>(
fn: () => Promise<T | false>,
- deps?: any[],
+ deps?: unknown[],
): HookResponseWithRetry<T> {
const [result, setHookResponse] = useState<HookResponse<T>>(undefined);
const args = useMemo(
() => ({
fn,
- // eslint-disable-next-line react-hooks/exhaustive-deps
}),
deps || [],
);
@@ -68,13 +68,21 @@ export function useAsyncAsHook<T>(
if (e instanceof TalerError) {
setHookResponse({
hasError: true,
- operational: true,
+ type: "taler",
+ message: e.message,
+ details: e.errorDetail,
+ });
+ } else if (e instanceof BackgroundError) {
+ setHookResponse({
+ hasError: true,
+ type: "taler",
+ message: e.message,
details: e.errorDetail,
});
} else if (e instanceof Error) {
setHookResponse({
hasError: true,
- operational: false,
+ type: "error",
message: e.message,
});
}
diff --git a/packages/taler-wallet-webextension/src/hooks/useAutoOpenPermissions.ts b/packages/taler-wallet-webextension/src/hooks/useAutoOpenPermissions.ts
deleted file mode 100644
index 3361394a4..000000000
--- a/packages/taler-wallet-webextension/src/hooks/useAutoOpenPermissions.ts
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-import { TalerError } from "@gnu-taler/taler-wallet-core";
-import { useEffect, useState } from "preact/hooks";
-import { ToggleHandler } from "../mui/handlers.js";
-import { platform } from "../platform/api.js";
-import { wxApi } from "../wxApi.js";
-
-export function useAutoOpenPermissions(): ToggleHandler {
- const [enabled, setEnabled] = useState(false);
- const [error, setError] = useState<TalerError | undefined>();
- const toggle = async (): Promise<void> => {
- return handleAutoOpenPerm(enabled, setEnabled).catch((e) => {
- setError(TalerError.fromException(e));
- });
- };
-
- useEffect(() => {
- async function getValue(): Promise<void> {
- const res = await wxApi.background.containsHeaderListener();
- setEnabled(res.newValue);
- }
- getValue();
- }, []);
- return {
- value: enabled,
- button: {
- onClick: toggle,
- error,
- },
- };
-}
-
-async function handleAutoOpenPerm(
- isEnabled: boolean,
- onChange: (value: boolean) => void,
-): Promise<void> {
- if (!isEnabled) {
- // We set permissions here, since apparently FF wants this to be done
- // as the result of an input event ...
- let granted: boolean;
- try {
- granted = await platform.getPermissionsApi().requestHostPermissions();
- } catch (lastError) {
- onChange(false);
- throw lastError;
- }
- const res = await wxApi.background.toggleHeaderListener(granted);
- onChange(res.newValue);
- } else {
- try {
- await wxApi.background
- .toggleHeaderListener(false)
- .then((r) => onChange(r.newValue));
- } catch (e) {
- console.log(e);
- }
- }
- return;
-}
diff --git a/packages/taler-wallet-webextension/src/hooks/useBackupDeviceName.ts b/packages/taler-wallet-webextension/src/hooks/useBackupDeviceName.ts
index be81b7d7d..6288b6986 100644
--- a/packages/taler-wallet-webextension/src/hooks/useBackupDeviceName.ts
+++ b/packages/taler-wallet-webextension/src/hooks/useBackupDeviceName.ts
@@ -16,7 +16,7 @@
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useEffect, useState } from "preact/hooks";
-import { wxApi } from "../wxApi.js";
+import { useBackendContext } from "../context/backend.js";
export interface BackupDeviceName {
name: string;
@@ -28,17 +28,18 @@ export function useBackupDeviceName(): BackupDeviceName {
name: "",
update: () => Promise.resolve(),
});
+ const api = useBackendContext();
useEffect(() => {
async function run(): Promise<void> {
//create a first list of backup info by currency
- const status = await wxApi.wallet.call(
+ const status = await api.wallet.call(
WalletApiOperation.GetBackupInfo,
{},
);
async function update(newName: string): Promise<void> {
- await wxApi.wallet.call(WalletApiOperation.SetWalletDeviceId, {
+ await api.wallet.call(WalletApiOperation.SetWalletDeviceId, {
walletDeviceId: newName,
});
setStatus((old) => ({ ...old, name: newName }));
diff --git a/packages/taler-wallet-webextension/src/hooks/useClipboardPermissions.ts b/packages/taler-wallet-webextension/src/hooks/useClipboardPermissions.ts
index 9b7d46ba7..35b7148cc 100644
--- a/packages/taler-wallet-webextension/src/hooks/useClipboardPermissions.ts
+++ b/packages/taler-wallet-webextension/src/hooks/useClipboardPermissions.ts
@@ -14,64 +14,63 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { TalerError } from "@gnu-taler/taler-wallet-core";
import { useEffect, useState } from "preact/hooks";
+import { useAlertContext } from "../context/alert.js";
+import { useBackendContext } from "../context/backend.js";
import { ToggleHandler } from "../mui/handlers.js";
-import { platform } from "../platform/api.js";
-import { wxApi } from "../wxApi.js";
+import { platform } from "../platform/foreground.js";
+/**
+ * This is not implemented.
+ * Clipboard permission need to get ask the permission to the user
+ * based on user-intention
+ * @returns
+ */
export function useClipboardPermissions(): ToggleHandler {
const [enabled, setEnabled] = useState(false);
- const [error, setError] = useState<TalerError | undefined>();
- const toggle = async (): Promise<void> => {
- return handleClipboardPerm(enabled, setEnabled).catch((e) => {
- setError(TalerError.fromException(e));
- });
- };
+ const api = useBackendContext();
+ const { pushAlertOnError } = useAlertContext();
- useEffect(() => {
- async function getValue(): Promise<void> {
- const res = await wxApi.background.containsHeaderListener();
- setEnabled(res.newValue);
+ async function handleClipboardPerm(): Promise<void> {
+ if (!enabled) {
+ // We set permissions here, since apparently FF wants this to be done
+ // as the result of an input event ...
+ let granted: boolean;
+ try {
+ granted = await platform
+ .getPermissionsApi()
+ .requestClipboardPermissions();
+ } catch (lastError) {
+ setEnabled(false);
+ throw lastError;
+ }
+ setEnabled(granted);
+ } else {
+ // try {
+ // await api.background
+ // .call("toggleHeaderListener", false)
+ // .then((r) => setEnabled(r.newValue));
+ // } catch (e) {
+ // }
}
- getValue();
- }, []);
+ return;
+ }
+
+ // useEffect(() => {
+ // async function getValue(): Promise<void> {
+ // const res = await api.background.call(
+ // "containsHeaderListener",
+ // undefined,
+ // );
+ // setEnabled(res.newValue);
+ // }
+ // getValue();
+ // }, []);
return {
value: enabled,
button: {
- onClick: toggle,
- error,
+ onClick: pushAlertOnError(handleClipboardPerm),
},
};
}
-
-async function handleClipboardPerm(
- isEnabled: boolean,
- onChange: (value: boolean) => void,
-): Promise<void> {
- if (!isEnabled) {
- // We set permissions here, since apparently FF wants this to be done
- // as the result of an input event ...
- let granted: boolean;
- try {
- granted = await platform
- .getPermissionsApi()
- .requestClipboardPermissions();
- } catch (lastError) {
- onChange(false);
- throw lastError;
- }
- // const res = await wxApi.toggleHeaderListener(granted);
- onChange(granted);
- } else {
- try {
- await wxApi.background
- .toggleHeaderListener(false)
- .then((r) => onChange(r.newValue));
- } catch (e) {
- console.log(e);
- }
- }
- return;
-}
diff --git a/packages/taler-wallet-webextension/src/hooks/useDiagnostics.ts b/packages/taler-wallet-webextension/src/hooks/useDiagnostics.ts
deleted file mode 100644
index a8564fddb..000000000
--- a/packages/taler-wallet-webextension/src/hooks/useDiagnostics.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-import { WalletDiagnostics } from "@gnu-taler/taler-util";
-import { useEffect, useState } from "preact/hooks";
-import { wxApi } from "../wxApi.js";
-
-export function useDiagnostics(): [WalletDiagnostics | undefined, boolean] {
- const [timedOut, setTimedOut] = useState(false);
- const [diagnostics, setDiagnostics] = useState<WalletDiagnostics | undefined>(
- undefined,
- );
-
- useEffect(() => {
- let gotDiagnostics = false;
- setTimeout(() => {
- if (!gotDiagnostics) {
- console.error("timed out");
- setTimedOut(true);
- }
- }, 1000);
- const doFetch = async (): Promise<void> => {
- const d = await wxApi.background.getDiagnostics();
- gotDiagnostics = true;
- setDiagnostics(d);
- };
- doFetch();
- }, []);
- return [diagnostics, timedOut];
-}
diff --git a/packages/taler-wallet-webextension/src/hooks/useIsOnline.ts b/packages/taler-wallet-webextension/src/hooks/useIsOnline.ts
new file mode 100644
index 000000000..8d26bf3b6
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/hooks/useIsOnline.ts
@@ -0,0 +1,14 @@
+import { codecForBoolean } from "@gnu-taler/taler-util";
+import { buildStorageKey, useMemoryStorage } from "@gnu-taler/web-util/browser";
+import { platform } from "../platform/foreground.js";
+import { useEffect } from "preact/hooks";
+
+export function useIsOnline(): boolean {
+ const { value, update } = useMemoryStorage("online", true);
+ useEffect(() => {
+ return platform.listenNetworkConnectionState((state) => {
+ update(state === "on");
+ });
+ });
+ return value;
+}
diff --git a/packages/taler-wallet-webextension/src/hooks/useLocalStorage.ts b/packages/taler-wallet-webextension/src/hooks/useLocalStorage.ts
deleted file mode 100644
index 88b7655b6..000000000
--- a/packages/taler-wallet-webextension/src/hooks/useLocalStorage.ts
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import { StateUpdater, useState } from "preact/hooks";
-
-export function useLocalStorage(
- 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),
- ): void => {
- 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];
-}
-
-//TODO: merge with the above function
-export function useNotNullLocalStorage(
- key: string,
- initialValue: string,
-): [string, StateUpdater<string>, boolean] {
- const [storedValue, setStoredValue] = useState<string>((): string => {
- return typeof window !== "undefined"
- ? window.localStorage.getItem(key) || initialValue
- : initialValue;
- });
-
- const setValue = (value: string | ((val: string) => string)): void => {
- const valueToStore = value instanceof Function ? value(storedValue) : value;
- setStoredValue(valueToStore);
- if (typeof window !== "undefined") {
- if (!valueToStore) {
- window.localStorage.removeItem(key);
- } else {
- window.localStorage.setItem(key, valueToStore);
- }
- }
- };
-
- const isSaved = window.localStorage.getItem(key) !== null;
- return [storedValue, setValue, isSaved];
-}
diff --git a/packages/taler-wallet-webextension/src/hooks/useProviderStatus.ts b/packages/taler-wallet-webextension/src/hooks/useProviderStatus.ts
index e6473d1ec..e2ba5b285 100644
--- a/packages/taler-wallet-webextension/src/hooks/useProviderStatus.ts
+++ b/packages/taler-wallet-webextension/src/hooks/useProviderStatus.ts
@@ -14,9 +14,10 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { ProviderInfo, WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { ProviderInfo } from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useEffect, useState } from "preact/hooks";
-import { wxApi } from "../wxApi.js";
+import { useBackendContext } from "../context/backend.js";
export interface ProviderStatus {
info?: ProviderInfo;
@@ -26,11 +27,11 @@ export interface ProviderStatus {
export function useProviderStatus(url: string): ProviderStatus | undefined {
const [status, setStatus] = useState<ProviderStatus | undefined>(undefined);
-
+ const api = useBackendContext();
useEffect(() => {
async function run(): Promise<void> {
//create a first list of backup info by currency
- const status = await wxApi.wallet.call(
+ const status = await api.wallet.call(
WalletApiOperation.GetBackupInfo,
{},
);
@@ -42,7 +43,7 @@ export function useProviderStatus(url: string): ProviderStatus | undefined {
async function sync(): Promise<void> {
if (info) {
- await wxApi.wallet.call(WalletApiOperation.RunBackupCycle, {
+ await api.wallet.call(WalletApiOperation.RunBackupCycle, {
providers: [info.syncProviderBaseUrl],
});
}
@@ -50,7 +51,7 @@ export function useProviderStatus(url: string): ProviderStatus | undefined {
async function remove(): Promise<void> {
if (info) {
- await wxApi.wallet.call(WalletApiOperation.RemoveBackupProvider, {
+ await api.wallet.call(WalletApiOperation.RemoveBackupProvider, {
provider: info.syncProviderBaseUrl,
});
}
diff --git a/packages/taler-wallet-webextension/src/hooks/useSelectedExchange.ts b/packages/taler-wallet-webextension/src/hooks/useSelectedExchange.ts
index c04dcce84..6907a247d 100644
--- a/packages/taler-wallet-webextension/src/hooks/useSelectedExchange.ts
+++ b/packages/taler-wallet-webextension/src/hooks/useSelectedExchange.ts
@@ -16,15 +16,17 @@
import { ExchangeListItem } from "@gnu-taler/taler-util";
import { useState } from "preact/hooks";
+import { useAlertContext } from "../context/alert.js";
import { ButtonHandler } from "../mui/handlers.js";
-type State = State.Ready | State.NoExchange | State.Selecting;
+type State = State.Ready | State.NoExchangeFound | State.Selecting;
export namespace State {
- export interface NoExchange {
- status: "no-exchange";
+ export interface NoExchangeFound {
+ status: "no-exchange-found";
error: undefined;
- currency: string | undefined;
+ currency: string;
+ defaultExchange: string | undefined;
}
export interface Ready {
status: "ready";
@@ -38,7 +40,7 @@ export namespace State {
onCancel: () => Promise<void>;
list: ExchangeListItem[];
currency: string;
- currentExchange: string;
+ initialValue: string;
}
}
@@ -59,34 +61,39 @@ export function useSelectedExchange({
const [selectedExchange, setSelectedExchange] = useState<string | undefined>(
undefined,
);
+ const { pushAlertOnError } = useAlertContext();
if (!list.length) {
return {
- status: "no-exchange",
+ status: "no-exchange-found",
error: undefined,
- currency: undefined,
+ currency,
+ defaultExchange,
};
}
- const listCurrency = list.filter((e) => e.currency === currency);
- if (!listCurrency.length) {
+ const exchangesWithThisCurrency = list.filter((e) => e.currency === currency);
+ if (!exchangesWithThisCurrency.length) {
// there should be at least one exchange for this currency
return {
- status: "no-exchange",
+ status: "no-exchange-found",
error: undefined,
currency,
+ defaultExchange,
};
}
if (isSelecting) {
const currentExchange =
- selectedExchange ?? defaultExchange ?? listCurrency[0].exchangeBaseUrl;
+ selectedExchange ??
+ defaultExchange ??
+ exchangesWithThisCurrency[0].exchangeBaseUrl;
return {
status: "selecting-exchange",
error: undefined,
- list: listCurrency,
+ list: exchangesWithThisCurrency,
currency,
- currentExchange: currentExchange,
+ initialValue: currentExchange,
onSelection: async (exchangeBaseUrl: string) => {
setIsSelecting(false);
setSelectedExchange(exchangeBaseUrl);
@@ -105,7 +112,7 @@ export function useSelectedExchange({
return {
status: "ready",
doSelect: {
- onClick: async () => setIsSelecting(true),
+ onClick: pushAlertOnError(async () => setIsSelecting(true)),
},
selected: found,
};
@@ -118,7 +125,7 @@ export function useSelectedExchange({
return {
status: "ready",
doSelect: {
- onClick: async () => setIsSelecting(true),
+ onClick: pushAlertOnError(async () => setIsSelecting(true)),
},
selected: found,
};
@@ -127,8 +134,8 @@ export function useSelectedExchange({
return {
status: "ready",
doSelect: {
- onClick: async () => setIsSelecting(true),
+ onClick: pushAlertOnError(async () => setIsSelecting(true)),
},
- selected: listCurrency[0],
+ selected: exchangesWithThisCurrency[0],
};
}
diff --git a/packages/taler-wallet-webextension/src/hooks/useSettings.ts b/packages/taler-wallet-webextension/src/hooks/useSettings.ts
new file mode 100644
index 000000000..a79a71087
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/hooks/useSettings.ts
@@ -0,0 +1,64 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser";
+import { Settings, defaultSettings } from "../platform/api.js";
+import {
+ Codec,
+ buildCodecForObject,
+ codecForBoolean,
+} from "@gnu-taler/taler-util";
+
+function parse_json_or_undefined<T>(str: string | undefined): T | undefined {
+ if (str === undefined) return undefined;
+ try {
+ return JSON.parse(str);
+ } catch {
+ return undefined;
+ }
+}
+
+export const codecForSettings = (): Codec<Settings> =>
+ buildCodecForObject<Settings>()
+ .property("walletAllowHttp", codecForBoolean())
+ .property("injectTalerSupport", codecForBoolean())
+ .property("autoOpen", codecForBoolean())
+ .property("advancedMode", codecForBoolean())
+ .property("backup", codecForBoolean())
+ .property("langSelector", codecForBoolean())
+ .property("showJsonOnError", codecForBoolean())
+ .property("extendedAccountTypes", codecForBoolean())
+ .property("suspendIndividualTransaction", codecForBoolean())
+ .property("showRefeshTransactions", codecForBoolean())
+ .property("showExchangeManagement", codecForBoolean())
+ .property("selectTosFormat", codecForBoolean())
+ .property("showWalletActivity", codecForBoolean())
+ .build("Settings");
+
+const SETTINGS_KEY = buildStorageKey("wallet-settings", codecForSettings());
+
+export function useSettings(): [
+ Readonly<Settings>,
+ <T extends keyof Settings>(key: T, value: Settings[T]) => void,
+] {
+ const { value, update } = useLocalStorage(SETTINGS_KEY, defaultSettings);
+
+ function updateField<T extends keyof Settings>(k: T, v: Settings[T]) {
+ update({ ...value, [k]: v });
+ }
+
+ return [value, updateField];
+}
diff --git a/packages/taler-wallet-webextension/src/hooks/useTalerActionURL.test.ts b/packages/taler-wallet-webextension/src/hooks/useTalerActionURL.test.ts
index 8aabb8adf..0bb47530c 100644
--- a/packages/taler-wallet-webextension/src/hooks/useTalerActionURL.test.ts
+++ b/packages/taler-wallet-webextension/src/hooks/useTalerActionURL.test.ts
@@ -13,11 +13,11 @@
You should have received 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 { useTalerActionURL } from "./useTalerActionURL.js";
-import { mountHook } from "../test-utils.js";
-import { IoCProviderForTesting } from "../context/iocContext.js";
-import { h, VNode } from "preact";
import { expect } from "chai";
+import { h, VNode } from "preact";
+import { IoCProviderForTesting } from "../context/iocContext.js";
+import { useTalerActionURL } from "./useTalerActionURL.js";
+import * as tests from "@gnu-taler/web-util/testing";
describe("useTalerActionURL hook", () => {
it("should be set url to undefined when dismiss", async () => {
@@ -31,32 +31,28 @@ describe("useTalerActionURL hook", () => {
});
};
- const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
- mountHook(useTalerActionURL, ctx);
-
- {
- const [url] = pullLastResultOrThrow();
- expect(url).undefined;
- }
-
- expect(await waitForStateUpdate()).true;
-
- {
- const [url, setDismissed] = pullLastResultOrThrow();
- expect(url).deep.equals({
- location: "clipboard",
- uri: "qwe",
- });
- setDismissed(true);
- }
-
- expect(await waitForStateUpdate()).true;
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useTalerActionURL,
+ {},
+ [
+ ([url]) => {
+ expect(url).undefined;
+ },
+ ([url, setDismissed]) => {
+ expect(url).deep.equals({
+ location: "clipboard",
+ uri: "qwe",
+ });
+ setDismissed(true);
+ },
+ ([url]) => {
+ if (url !== undefined) throw Error("invalid");
+ expect(url).undefined;
+ },
+ ],
+ ctx,
+ );
- {
- const [url] = pullLastResultOrThrow();
- if (url !== undefined) throw Error("invalid");
- expect(url).undefined;
- }
- await assertNoPendingUpdate();
+ expect(hookBehavior).deep.equal({ result: "ok" });
});
});
diff --git a/packages/taler-wallet-webextension/src/hooks/useWalletDevMode.ts b/packages/taler-wallet-webextension/src/hooks/useWalletDevMode.ts
deleted file mode 100644
index d5743eb2e..000000000
--- a/packages/taler-wallet-webextension/src/hooks/useWalletDevMode.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-import { useState, useEffect } from "preact/hooks";
-import { wxApi } from "../wxApi.js";
-import { ToggleHandler } from "../mui/handlers.js";
-import { TalerError, WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-
-export function useWalletDevMode(): ToggleHandler {
- const [enabled, setEnabled] = useState<undefined | boolean>(undefined);
- const [error, setError] = useState<TalerError | undefined>();
- const toggle = async (): Promise<void> => {
- return handleOpen(enabled, setEnabled).catch((e) => {
- setError(TalerError.fromException(e));
- });
- };
-
- useEffect(() => {
- async function getValue(): Promise<void> {
- const res = await wxApi.wallet.call(WalletApiOperation.GetVersion, {});
- setEnabled(res.devMode);
- }
- getValue();
- }, []);
- return {
- value: enabled,
- button: {
- onClick: enabled === undefined ? undefined : toggle,
- error,
- },
- };
-}
-
-async function handleOpen(
- currentValue: undefined | boolean,
- onChange: (value: boolean) => void,
-): Promise<void> {
- const nextValue = !currentValue;
- await wxApi.wallet.call(WalletApiOperation.SetDevMode, {
- devModeEnabled: nextValue,
- });
- onChange(nextValue);
- return;
-}
diff --git a/packages/taler-wallet-webextension/src/i18n/de.po b/packages/taler-wallet-webextension/src/i18n/de.po
index daf618ddf..1a285499c 100644
--- a/packages/taler-wallet-webextension/src/i18n/de.po
+++ b/packages/taler-wallet-webextension/src/i18n/de.po
@@ -15,18 +15,18 @@
msgid ""
msgstr ""
"Project-Id-Version: Taler Wallet\n"
-"Report-Msgid-Bugs-To: \n"
+"Report-Msgid-Bugs-To: languages@taler.net\n"
"POT-Creation-Date: 2016-11-23 00:00+0100\n"
-"PO-Revision-Date: 2022-03-05 08:29+0000\n"
+"PO-Revision-Date: 2023-11-25 17:24+0000\n"
"Last-Translator: Stefan Kügel <skuegel@web.de>\n"
-"Language-Team: German <http://weblate.taler.net/projects/gnu-taler/"
+"Language-Team: German <https://weblate.taler.net/projects/gnu-taler/"
"webextensions/de/>\n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
-"X-Generator: Weblate 4.4.2\n"
+"X-Generator: Weblate 5.2.1\n"
#: src/NavigationBar.tsx:139
#, c-format
@@ -154,6 +154,8 @@ msgid ""
"terms has changed, extending the service will imply accepting the new terms "
"of service"
msgstr ""
+"Die AGB sind geändert worden, eine Weiternutzung des Diensts erfordert die "
+"Einwilligung in die neuen Allgemeinen Geschäftsbedingungen (AGB)"
#: src/wallet/ProviderDetailPage.tsx:179
#, c-format
@@ -787,7 +789,7 @@ msgstr ""
#: src/wallet/Transaction.tsx:935
#, c-format
msgid "Date"
-msgstr ""
+msgstr "Datum"
#: src/wallet/Transaction.tsx:990
#, c-format
@@ -812,7 +814,7 @@ msgstr ""
#: src/wallet/Transaction.tsx:1156
#, c-format
msgid "Refunded"
-msgstr ""
+msgstr "Rückerstattet"
#: src/wallet/Transaction.tsx:1220
#, c-format
@@ -1013,42 +1015,43 @@ msgstr "Konnte die Umsatzanzeige nicht laden"
#: src/components/TermsOfService/views.tsx:73
#, c-format
msgid "Show terms of service"
-msgstr ""
+msgstr "Allgemeine Geschäftsbedingungen (AGB) anzeigen"
#: src/components/TermsOfService/views.tsx:81
#, c-format
msgid "I accept the exchange terms of service"
-msgstr ""
+msgstr "Ich akzeptiere die Allgemeinen Geschäftsbedingungen (AGB)"
#: src/components/TermsOfService/views.tsx:107
#, c-format
msgid "Exchange doesn&apos;t have terms of service"
-msgstr ""
+msgstr "Dieser Exchange hat keine Allgemeine Geschäftsbedingungen (AGB)"
#: src/components/TermsOfService/views.tsx:135
#, c-format
msgid "Review exchange terms of service"
-msgstr ""
+msgstr "Allgemeine Geschäftsbedingungen (AGB) ansehen"
#: src/components/TermsOfService/views.tsx:146
#, c-format
msgid "Review new version of terms of service"
-msgstr ""
+msgstr "Neue Version der Allgemeinen Geschäftsbedingungen (AGB) ansehen"
#: src/components/TermsOfService/views.tsx:170
#, c-format
msgid "The exchange reply with a empty terms of service"
msgstr ""
+"Der Exchange wird mit Allgemeinen Geschäftsbedingungen ohne Inhalt antworten"
#: src/components/TermsOfService/views.tsx:193
#, c-format
msgid "Download Terms of Service"
-msgstr ""
+msgstr "Allgemeine Geschäftsbedingungen (AGB) herunterladen"
#: src/components/TermsOfService/views.tsx:204
#, c-format
msgid "Hide terms of service"
-msgstr ""
+msgstr "Allgemeine Geschäftsbedingungen (AGB) verstecken"
#: src/wallet/ExchangeSelection/views.tsx:117
#, fuzzy, c-format
@@ -1357,7 +1360,7 @@ msgstr ""
#: src/wallet/ExchangeAddConfirm.tsx:42
#, c-format
msgid "Review terms of service"
-msgstr ""
+msgstr "Allgemeine Geschäftsbedingungen (AGB) ansehen"
#: src/wallet/ExchangeAddConfirm.tsx:45
#, c-format
@@ -1732,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 ihre %1$s Einstellungen, für die Sie IndexedDB verwenden ("
+"preference name %2$s prüfen)."
#: src/components/Diagnostics.tsx:70
#, c-format
@@ -1855,7 +1858,7 @@ msgstr ""
#: src/wallet/QrReader.tsx:122
#, c-format
msgid "Open"
-msgstr ""
+msgstr "Noch zu vergeben"
#: src/wallet/QrReader.tsx:128
#, c-format
diff --git a/packages/taler-wallet-webextension/src/i18n/es.po b/packages/taler-wallet-webextension/src/i18n/es.po
index 78ca20220..ea1fa9803 100644
--- a/packages/taler-wallet-webextension/src/i18n/es.po
+++ b/packages/taler-wallet-webextension/src/i18n/es.po
@@ -17,8 +17,8 @@ msgstr ""
"Project-Id-Version: Taler Wallet\n"
"Report-Msgid-Bugs-To: languages@taler.net\n"
"POT-Creation-Date: 2016-11-23 00:00+0100\n"
-"PO-Revision-Date: 2022-10-23 20:20+0000\n"
-"Last-Translator: Sebastian Marchano <sebasjm@gmail.com>\n"
+"PO-Revision-Date: 2024-03-07 07:03+0000\n"
+"Last-Translator: Javier Sepulveda <javier.sepulveda@uv.es>\n"
"Language-Team: Spanish <https://weblate.taler.net/projects/gnu-taler/"
"webextensions/es/>\n"
"Language: es\n"
@@ -26,12 +26,12 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
-"X-Generator: Weblate 4.13.1\n"
+"X-Generator: Weblate 5.2.1\n"
#: src/NavigationBar.tsx:139
#, c-format
msgid "Balance"
-msgstr "Balance"
+msgstr "Saldo"
#: src/NavigationBar.tsx:142
#, c-format
@@ -330,7 +330,7 @@ msgstr "Resumen"
#: src/components/ShowFullContractTermPopup.tsx:195
#, c-format
msgid "Amount"
-msgstr "Cantidad"
+msgstr "Monto"
#: src/components/ShowFullContractTermPopup.tsx:203
#, c-format
@@ -390,7 +390,7 @@ msgstr "Creado en"
#: src/components/ShowFullContractTermPopup.tsx:304
#, c-format
msgid "Refund deadline"
-msgstr "Plazo de devolución"
+msgstr "Plazo de reembolso"
#: src/components/ShowFullContractTermPopup.tsx:319
#, c-format
@@ -405,7 +405,7 @@ msgstr "Plazo de pago"
#: src/components/ShowFullContractTermPopup.tsx:354
#, c-format
msgid "Fulfillment URL"
-msgstr "URL de éxito"
+msgstr "URL de cumplimiento"
#: src/components/ShowFullContractTermPopup.tsx:360
#, c-format
@@ -445,7 +445,7 @@ msgstr "Exchanges"
#: src/components/Part.tsx:148
#, c-format
msgid "Bank account"
-msgstr "Cuenta del banco"
+msgstr "Cuenta bancaria"
#: src/components/Part.tsx:160
#, c-format
@@ -564,7 +564,7 @@ msgstr "Esta transacción no está completada"
#: src/wallet/Transaction.tsx:209
#, c-format
msgid "Send"
-msgstr "Enviar"
+msgstr "Envíar"
#: src/wallet/Transaction.tsx:216
#, c-format
@@ -598,7 +598,7 @@ msgstr "Confirmar"
#: src/wallet/Transaction.tsx:267
#, c-format
msgid "Withdrawal"
-msgstr "Retirada"
+msgstr "Extracción"
#: src/wallet/Transaction.tsx:286
#, c-format
@@ -783,7 +783,7 @@ msgstr "Código postal"
#: src/wallet/Transaction.tsx:892
#, c-format
msgid "Town location"
-msgstr "Ubicación de la ciudad"
+msgstr "Ubicación de ciudad"
#: src/wallet/Transaction.tsx:900
#, c-format
@@ -828,7 +828,7 @@ msgstr "Precio"
#: src/wallet/Transaction.tsx:1156
#, c-format
msgid "Refunded"
-msgstr "Devuelto"
+msgstr "Reembolsado"
#: src/wallet/Transaction.tsx:1220
#, c-format
@@ -893,7 +893,7 @@ msgstr "Ya reclamado"
#: src/cta/Payment/views.tsx:296
#, c-format
msgid "Pay with a mobile phone"
-msgstr "Pagar con un teléfono móbil"
+msgstr "Pagar con un teléfono móvil"
#: src/cta/Payment/views.tsx:298
#, c-format
@@ -1113,7 +1113,7 @@ msgstr "No tiene auditores"
#: src/wallet/ExchangeSelection/views.tsx:241
#, c-format
msgid "currency"
-msgstr "Divisa"
+msgstr "divisa"
#: src/wallet/ExchangeSelection/views.tsx:249
#, c-format
@@ -1386,7 +1386,7 @@ msgstr "Revisar los términos de servicio"
#: src/wallet/ExchangeAddConfirm.tsx:45
#, c-format
msgid "Exchange URL"
-msgstr "Exchange URL"
+msgstr "URL del Exchange"
#: src/wallet/ExchangeAddConfirm.tsx:70
#, c-format
@@ -1765,8 +1765,8 @@ msgid ""
"Please check in your %1$s settings that you have IndexedDB enabled (check "
"the preference name %2$s)."
msgstr ""
-"Por favor revisa en tu configuración %1$s que tienes IndexedDB habilitado "
-"(el nombre de la preferencia %2$s)."
+"Por favor revisa en tu configuración %1$s que tienes IndexedDB habilitado ("
+"el nombre de la preferencia %2$s)."
#: src/components/Diagnostics.tsx:70
#, c-format
@@ -1890,7 +1890,7 @@ msgstr "Escanear un código QR o ingresar taler:// URI debajo"
#: src/wallet/QrReader.tsx:122
#, c-format
msgid "Open"
-msgstr "Abrir"
+msgstr "Abierto"
#: src/wallet/QrReader.tsx:128
#, c-format
diff --git a/packages/taler-wallet-webextension/src/i18n/fi.po b/packages/taler-wallet-webextension/src/i18n/fi.po
new file mode 100644
index 000000000..c6196b7f3
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/i18n/fi.po
@@ -0,0 +1,1967 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: languages@taler.net\n"
+"POT-Creation-Date: 2016-11-23 00:00+0100\n"
+"PO-Revision-Date: 2024-03-20 00:10+0000\n"
+"Last-Translator: Sara Korpinen <sara.a.korpinen@gmail.com>\n"
+"Language-Team: Finnish <https://weblate.taler.net/projects/gnu-taler/"
+"webextensions/fi/>\n"
+"Language: fi\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 5.2.1\n"
+
+#: src/NavigationBar.tsx:139
+#, c-format
+msgid "Balance"
+msgstr "Saldo"
+
+#: src/NavigationBar.tsx:142
+#, c-format
+msgid "Backup"
+msgstr "Varmuuskopio"
+
+#: src/NavigationBar.tsx:147
+#, c-format
+msgid "QR Reader and Taler URI"
+msgstr "QR -lukija ja Taler URI"
+
+#: src/NavigationBar.tsx:154
+#, c-format
+msgid "Settings"
+msgstr "Asetukset"
+
+#: src/NavigationBar.tsx:184
+#, c-format
+msgid "Dev"
+msgstr "Kehitys"
+
+#: src/mui/Typography.tsx:122
+#, c-format
+msgid "%1$s"
+msgstr "%1$s"
+
+#: src/components/PendingTransactions.tsx:74
+#, c-format
+msgid "PENDING OPERATIONS"
+msgstr "ODOTTAVAT TOIMINNOT"
+
+#: src/components/Loading.tsx:36
+#, c-format
+msgid "Loading"
+msgstr "Lataa"
+
+#: src/wallet/BackupPage.tsx:123
+#, c-format
+msgid "Could not load backup providers"
+msgstr "Varmuuskopion tarjoajia ei voitu ladata"
+
+#: src/wallet/BackupPage.tsx:202
+#, c-format
+msgid "No backup providers configured"
+msgstr "Varmuuskopion tarjoajia ei ole määritetty"
+
+#: src/wallet/BackupPage.tsx:205
+#, c-format
+msgid "Add provider"
+msgstr "Lisää palveluntarjoaja"
+
+#: src/wallet/BackupPage.tsx:219
+#, c-format
+msgid "Sync all backups"
+msgstr "Synkronoi kaikki varmuuskopiot"
+
+#: src/wallet/BackupPage.tsx:221
+#, c-format
+msgid "Sync now"
+msgstr "Synkronoi nyt"
+
+#: src/wallet/BackupPage.tsx:264
+#, c-format
+msgid "Last synced"
+msgstr "Viimeksi synkronoitu"
+
+#: src/wallet/BackupPage.tsx:269
+#, c-format
+msgid "Not synced"
+msgstr "Ei synkronoitu"
+
+#: src/wallet/BackupPage.tsx:289
+#, c-format
+msgid "Expires in"
+msgstr "Vanhenee"
+
+#: src/wallet/ProviderDetailPage.tsx:60
+#, c-format
+msgid "There was an error loading the provider detail for &quot; %1$s&quot;"
+msgstr "Virhe ladattaessa palveluntarjoajan tietoja kohteelle &quot; %1$s&quot;"
+
+#: src/wallet/ProviderDetailPage.tsx:108
+#, c-format
+msgid "There is not known provider with url &quot;%1$s&quot;."
+msgstr "Ei tunneta palveluntarjoajaa, jonka URL-osoite on &quot;%1$s&quot;."
+
+#: src/wallet/ProviderDetailPage.tsx:115
+#, c-format
+msgid "See providers"
+msgstr "Katso palveluntarjoajat"
+
+#: src/wallet/ProviderDetailPage.tsx:143
+#, c-format
+msgid "Last backup"
+msgstr "Viimeisin varmuuskopio"
+
+#: src/wallet/ProviderDetailPage.tsx:148
+#, c-format
+msgid "Back up"
+msgstr "Varmuuskopioi"
+
+#: src/wallet/ProviderDetailPage.tsx:154
+#, c-format
+msgid "Provider fee"
+msgstr "Palveluntarjoajan maksu"
+
+#: src/wallet/ProviderDetailPage.tsx:157
+#, c-format
+msgid "per year"
+msgstr "vuodessa"
+
+#: src/wallet/ProviderDetailPage.tsx:163
+#, c-format
+msgid "Extend"
+msgstr "Laajenna"
+
+#: src/wallet/ProviderDetailPage.tsx:169
+#, c-format
+msgid ""
+"terms has changed, extending the service will imply accepting the new terms of "
+"service"
+msgstr ""
+"ehdot ovat muuttuneet, palvelun laajentaminen tarkoittaa uusien "
+"käyttöehtojen hyväksymistä"
+
+#: src/wallet/ProviderDetailPage.tsx:179
+#, c-format
+msgid "old"
+msgstr "vanha"
+
+#: src/wallet/ProviderDetailPage.tsx:183
+#, c-format
+msgid "new"
+msgstr "uusi"
+
+#: src/wallet/ProviderDetailPage.tsx:190
+#, c-format
+msgid "fee"
+msgstr "maksu"
+
+#: src/wallet/ProviderDetailPage.tsx:198
+#, c-format
+msgid "storage"
+msgstr "tila"
+
+#: src/wallet/ProviderDetailPage.tsx:215
+#, c-format
+msgid "Remove provider"
+msgstr "Poista palveluntarjoaja"
+
+#: src/wallet/ProviderDetailPage.tsx:228
+#, c-format
+msgid "This provider has reported an error"
+msgstr "Tämä palveluntarjoaja on ilmoittanut virheestä"
+
+#: src/wallet/ProviderDetailPage.tsx:242
+#, c-format
+msgid "There is conflict with another backup from %1$s"
+msgstr "Ristiriita toisen varmuuskopion kanssa kohteesta %1$s"
+
+#: src/wallet/ProviderDetailPage.tsx:253
+#, c-format
+msgid "Backup is not readable"
+msgstr "Varmuuskopiota ei voi lukea"
+
+#: src/wallet/ProviderDetailPage.tsx:261
+#, c-format
+msgid "Unknown backup problem: %1$s"
+msgstr "Tuntematon varmuuskopiointi ongelma: %1$s"
+
+#: src/wallet/ProviderDetailPage.tsx:283
+#, c-format
+msgid "service paid"
+msgstr "palvelu maksettu"
+
+#: src/wallet/ProviderDetailPage.tsx:290
+#, c-format, fuzzy
+msgid "Backup valid until"
+msgstr "Varmuuskopio voimassa"
+
+#: src/wallet/AddNewActionView.tsx:57
+#, c-format
+msgid "Cancel"
+msgstr "Peruuta"
+
+#: src/wallet/AddNewActionView.tsx:68
+#, c-format
+msgid "Open reserve page"
+msgstr "Avaa varaussivu"
+
+#: src/wallet/AddNewActionView.tsx:70
+#, c-format
+msgid "Open pay page"
+msgstr "Avaa maksusivu"
+
+#: src/wallet/AddNewActionView.tsx:72
+#, c-format
+msgid "Open refund page"
+msgstr "Avaa hyvityssivu"
+
+#: src/wallet/AddNewActionView.tsx:74
+#, c-format
+msgid "Open tip page"
+msgstr "Avaa tippi sivu"
+
+#: src/wallet/AddNewActionView.tsx:76
+#, c-format
+msgid "Open withdraw page"
+msgstr "Avaa nostosivu"
+
+#: src/popup/NoBalanceHelp.tsx:43
+#, c-format
+msgid "Get digital cash"
+msgstr "Hanki digitaalista käteistä"
+
+#: src/popup/BalancePage.tsx:138
+#, c-format
+msgid "Could not load balance page"
+msgstr "Ei voitu ladata saldosivua"
+
+#: src/popup/BalancePage.tsx:175
+#, c-format
+msgid "Add"
+msgstr "Lisää"
+
+#: src/popup/BalancePage.tsx:179
+#, c-format
+msgid "Send %1$s"
+msgstr "Lähetä %1$s"
+
+#: src/popup/TalerActionFound.tsx:44
+#, c-format
+msgid "Taler Action"
+msgstr "Taler toiminta"
+
+#: src/popup/TalerActionFound.tsx:49
+#, c-format
+msgid "This page has pay action."
+msgstr "Tällä sivulla on maksutoiminto."
+
+#: src/popup/TalerActionFound.tsx:63
+#, c-format
+msgid "This page has a withdrawal action."
+msgstr "Tällä sivulla on nosto toiminto."
+
+#: src/popup/TalerActionFound.tsx:79
+#, c-format
+msgid "This page has a tip action."
+msgstr "Tällä sivulla on tippaus toiminto."
+
+#: src/popup/TalerActionFound.tsx:93
+#, c-format
+msgid "This page has a notify reserve action."
+msgstr "Tällä sivulla on ilmoitus varaus toiminto."
+
+#: src/popup/TalerActionFound.tsx:102
+#, c-format
+msgid "Notify"
+msgstr "Ilmoita"
+
+#: src/popup/TalerActionFound.tsx:109
+#, c-format
+msgid "This page has a refund action."
+msgstr "Tällä sivulla on hyvitys toiminto."
+
+#: src/popup/TalerActionFound.tsx:123
+#, c-format
+msgid "This page has a malformed taler uri."
+msgstr "Tällä sivulla on väärin muotoiltu taler uri."
+
+#: src/popup/TalerActionFound.tsx:134
+#, c-format
+msgid "Dismiss"
+msgstr "Hylkää"
+
+#: src/popup/Application.tsx:177
+#, c-format
+msgid "this popup is being closed and you are being redirected to %1$s"
+msgstr "tämä ponnahdusikkuna suljetaan ja sinut ohjataan osoitteeseen %1$s"
+
+#: src/components/ShowFullContractTermPopup.tsx:158
+#, c-format
+msgid "Could not load purchase proposal details"
+msgstr "Ostoehdotuksen tietoja ei voitu ladata"
+
+#: src/components/ShowFullContractTermPopup.tsx:183
+#, c-format
+msgid "Order Id"
+msgstr "Tilausnumero"
+
+#: src/components/ShowFullContractTermPopup.tsx:189
+#, c-format
+msgid "Summary"
+msgstr "Yhteenveto"
+
+#: src/components/ShowFullContractTermPopup.tsx:195
+#, c-format
+msgid "Amount"
+msgstr "Summa"
+
+#: src/components/ShowFullContractTermPopup.tsx:203
+#, c-format
+msgid "Merchant name"
+msgstr "Kauppiaan nimi"
+
+#: src/components/ShowFullContractTermPopup.tsx:209
+#, c-format
+msgid "Merchant jurisdiction"
+msgstr "Kauppiaan toimivalta"
+
+#: src/components/ShowFullContractTermPopup.tsx:215
+#, c-format
+msgid "Merchant address"
+msgstr "Kauppiaan osoite"
+
+#: src/components/ShowFullContractTermPopup.tsx:221
+#, c-format
+msgid "Merchant logo"
+msgstr "Kauppiaan logo"
+
+#: src/components/ShowFullContractTermPopup.tsx:234
+#, c-format
+msgid "Merchant website"
+msgstr "Kauppiaan nettisivut"
+
+#: src/components/ShowFullContractTermPopup.tsx:240
+#, c-format
+msgid "Merchant email"
+msgstr "Kauppiaan sähköposti"
+
+#: src/components/ShowFullContractTermPopup.tsx:246
+#, c-format
+msgid "Merchant public key"
+msgstr "Kauppiaan julkinen avain"
+
+#: src/components/ShowFullContractTermPopup.tsx:256
+#, c-format
+msgid "Delivery date"
+msgstr "Toimituspäivä"
+
+#: src/components/ShowFullContractTermPopup.tsx:271
+#, c-format
+msgid "Delivery location"
+msgstr "Toimituspaikka"
+
+#: src/components/ShowFullContractTermPopup.tsx:277
+#, c-format
+msgid "Products"
+msgstr "Tuotteet"
+
+#: src/components/ShowFullContractTermPopup.tsx:289
+#, c-format
+msgid "Created at"
+msgstr "Luotu"
+
+#: src/components/ShowFullContractTermPopup.tsx:304
+#, c-format
+msgid "Refund deadline"
+msgstr "Palautuksen määräaika"
+
+#: src/components/ShowFullContractTermPopup.tsx:319
+#, c-format
+msgid "Auto refund"
+msgstr "Automaattinen palautus"
+
+#: src/components/ShowFullContractTermPopup.tsx:339
+#, c-format
+msgid "Pay deadline"
+msgstr "Maksun määräaika"
+
+#: src/components/ShowFullContractTermPopup.tsx:354
+#, c-format
+msgid "Fulfillment URL"
+msgstr "Toteutus-URL"
+
+#: src/components/ShowFullContractTermPopup.tsx:360
+#, c-format
+msgid "Fulfillment message"
+msgstr "Toteutusviesti"
+
+#: src/components/ShowFullContractTermPopup.tsx:370
+#, c-format
+msgid "Max deposit fee"
+msgstr "Max talletusmaksu"
+
+#: src/components/ShowFullContractTermPopup.tsx:378
+#, c-format
+msgid "Max fee"
+msgstr "Max maksu"
+
+#: src/components/ShowFullContractTermPopup.tsx:386
+#, c-format
+msgid "Minimum age"
+msgstr "Alaikäraja"
+
+#: src/components/ShowFullContractTermPopup.tsx:398
+#, c-format
+msgid "Wire fee amortization"
+msgstr "Pankkimaksun lyhennys"
+
+#: src/components/ShowFullContractTermPopup.tsx:404
+#, c-format
+msgid "Auditors"
+msgstr "Tilintarkastajat"
+
+#: src/components/ShowFullContractTermPopup.tsx:419
+#, c-format
+msgid "Exchanges"
+msgstr "Vaihdot"
+
+#: src/components/Part.tsx:148
+#, c-format
+msgid "Bank account"
+msgstr "Pankkitili"
+
+#: src/components/Part.tsx:160
+#, c-format
+msgid "Bitcoin address"
+msgstr "Bitcoin osoite"
+
+#: src/components/Part.tsx:163
+#, c-format
+msgid "IBAN"
+msgstr "IBAN"
+
+#: src/cta/Deposit/views.tsx:38
+#, c-format
+msgid "Could not load deposit status"
+msgstr "Talletuksen tilaa ei voitu ladata"
+
+#: src/cta/Deposit/views.tsx:52
+#, c-format
+msgid "Digital cash deposit"
+msgstr "Digitaalinen käteistalletus"
+
+#: src/cta/Deposit/views.tsx:58
+#, c-format
+msgid "Cost"
+msgstr "Kustannus"
+
+#: src/cta/Deposit/views.tsx:66
+#, c-format
+msgid "Fee"
+msgstr "Maksu"
+
+#: src/cta/Deposit/views.tsx:73
+#, c-format
+msgid "To be received"
+msgstr "Vastaanotettava"
+
+#: src/cta/Deposit/views.tsx:84
+#, c-format
+msgid "Send &nbsp; %1$s"
+msgstr "Lähetä &nbsp; %1$s"
+
+#: src/components/BankDetailsByPaytoType.tsx:63
+#, c-format
+msgid "Bitcoin transfer details"
+msgstr "Bitcoin -siirron tiedot"
+
+#: src/components/BankDetailsByPaytoType.tsx:66
+#, c-format
+msgid ""
+"The exchange need a transaction with 3 output, one output is the exchange "
+"account and the other two are segwit fake address for metadata with an minimum "
+"amount."
+msgstr ""
+"Pörssi tarvitsee tapahtuman, jossa on 3 lähtöä, joista yksi on vaihtotili ja "
+"kaksi muuta ovat segwit fake -osoitteita metatiedoille vähimmäismäärällä."
+
+#: src/components/BankDetailsByPaytoType.tsx:74
+#, c-format
+msgid ""
+"In bitcoincore wallet use &apos;Add Recipient&apos; button to add two additional "
+"recipient and copy addresses and amounts"
+msgstr ""
+"Käytä bitcoincore-lompakossa &apos;Lisää vastaanottaja&apos; -painiketta "
+"lisätäksesi kaksi muuta vastaanottajaa ja kopioidaksesi osoitteet ja summat"
+
+#: src/components/BankDetailsByPaytoType.tsx:98
+#, c-format
+msgid "Make sure the amount show %1$s BTC, else you have to change the base unit to BTC"
+msgstr ""
+"Varmista, että summa näyttää %1$s BTC, muuten sinun on vaihdettava "
+"perusyksikkö BTC:ksi"
+
+#: src/components/BankDetailsByPaytoType.tsx:110
+#, c-format
+msgid "Account"
+msgstr "Tili"
+
+#: src/components/BankDetailsByPaytoType.tsx:116
+#, c-format
+msgid "Bank host"
+msgstr "Pankin isäntä"
+
+#: src/components/BankDetailsByPaytoType.tsx:139
+#, c-format
+msgid "Bank transfer details"
+msgstr "Pankkisiirtotiedot"
+
+#: src/components/BankDetailsByPaytoType.tsx:148
+#, c-format
+msgid "Subject"
+msgstr "Aihe"
+
+#: src/components/BankDetailsByPaytoType.tsx:154
+#, c-format
+msgid "Receiver name"
+msgstr "Vastaanottajan nimi"
+
+#: src/wallet/Transaction.tsx:98
+#, c-format
+msgid "Could not load the transaction information"
+msgstr "Tapahtumatietoja ei voitu ladata"
+
+#: src/wallet/Transaction.tsx:191
+#, c-format
+msgid "There was an error trying to complete the transaction"
+msgstr "Tapahtuman suorittamisessa tapahtui virhe"
+
+#: src/wallet/Transaction.tsx:200
+#, c-format
+msgid "This transaction is not completed"
+msgstr "Tätä tapahtumaa ei ole suoritettu loppuun"
+
+#: src/wallet/Transaction.tsx:209
+#, c-format
+msgid "Send"
+msgstr "Lähetä"
+
+#: src/wallet/Transaction.tsx:216
+#, c-format
+msgid "Retry"
+msgstr "Yritä uudelleen"
+
+#: src/wallet/Transaction.tsx:224
+#, c-format
+msgid "Forget"
+msgstr "Unohda"
+
+#: src/wallet/Transaction.tsx:241
+#, c-format
+msgid "Caution!"
+msgstr "Varoitus!"
+
+#: src/wallet/Transaction.tsx:244
+#, c-format
+msgid ""
+"If you have already wired money to the exchange you will loose the chance to get "
+"the coins form it."
+msgstr ""
+"Jos olet jo siirtänyt rahaa vaihtoon, menetät mahdollisuuden saada kolikot "
+"siitä."
+
+#: src/wallet/Transaction.tsx:259
+#, c-format
+msgid "Confirm"
+msgstr "Vahvista"
+
+#: src/wallet/Transaction.tsx:267
+#, c-format
+msgid "Withdrawal"
+msgstr "Nosto"
+
+#: src/wallet/Transaction.tsx:286
+#, c-format
+msgid ""
+"Make sure to use the correct subject, otherwise the money will not arrive in "
+"this wallet."
+msgstr ""
+"Varmista, että käytät oikeaa aihetta, muuten rahat eivät tule tähän "
+"lompakkoon."
+
+#: src/wallet/Transaction.tsx:298
+#, c-format
+msgid ""
+"The bank did not yet confirmed the wire transfer. Go to the %1$s %2$s and check "
+"there is no pending step."
+msgstr ""
+"Pankki ei ole vielä vahvistanut pankkisiirtoa. Siirry kohtaan %1$s %2$s ja "
+"tarkista, ettei odottavaa vaihetta ole."
+
+#: src/wallet/Transaction.tsx:316
+#, c-format
+msgid "Bank has confirmed the wire transfer. Waiting for the exchange to send the coins"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:325
+#, c-format
+msgid "Details"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:360
+#, c-format
+msgid "Payment"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:378
+#, c-format
+msgid "Refunds"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:385
+#, c-format
+msgid "%1$s %2$s on %3$s"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:415
+#, c-format
+msgid "Merchant created a refund for this order but was not automatically picked up."
+msgstr ""
+
+#: src/wallet/Transaction.tsx:420
+#, c-format
+msgid "Offer"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:431
+#, c-format
+msgid "Accept"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:438
+#, c-format
+msgid "Merchant"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:443
+#, c-format
+msgid "Invoice ID"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:470
+#, c-format
+msgid "Deposit"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:496
+#, c-format
+msgid "Refresh"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:517
+#, c-format
+msgid "Tip"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:542
+#, c-format
+msgid "Refund"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:555
+#, c-format
+msgid "Original order ID"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:568
+#, c-format
+msgid "Purchase summary"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:593
+#, c-format
+msgid "copy"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:596
+#, c-format
+msgid "hide qr"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:608
+#, c-format
+msgid "show qr"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:620
+#, c-format
+msgid "Credit"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:624
+#, c-format
+msgid "Invoice"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:635
+#, c-format
+msgid "Exchange"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:641
+#, c-format
+msgid "URI"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:667
+#, c-format
+msgid "Debit"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:710
+#, c-format
+msgid "Transfer"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:844
+#, c-format
+msgid "Country"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:852
+#, c-format
+msgid "Address lines"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:860
+#, c-format
+msgid "Building number"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:868
+#, c-format
+msgid "Building name"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:876
+#, c-format
+msgid "Street"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:884
+#, c-format
+msgid "Post code"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:892
+#, c-format
+msgid "Town location"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:900
+#, c-format
+msgid "Town"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:908
+#, c-format
+msgid "District"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:916
+#, c-format
+msgid "Country subdivision"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:935
+#, c-format
+msgid "Date"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:990
+#, c-format
+msgid "Transaction fees"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1004
+#, c-format
+msgid "Total"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1074
+#, c-format
+msgid "Withdraw"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1146
+#, c-format
+msgid "Price"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1156
+#, c-format
+msgid "Refunded"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1220
+#, c-format
+msgid "Delivery"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1335
+#, c-format
+msgid "Total transfer"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:57
+#, c-format
+msgid "Could not load pay status"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:87
+#, c-format
+msgid "Digital cash payment"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:119
+#, c-format
+msgid "Purchase"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:149
+#, c-format
+msgid "Receipt"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:156
+#, c-format
+msgid "Valid until"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:191
+#, c-format
+msgid "List of products"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:242
+#, c-format
+msgid "free"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:263
+#, c-format
+msgid "Already paid, you are going to be redirected to %1$s"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:274
+#, c-format
+msgid "Already paid"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:280
+#, c-format
+msgid "Already claimed"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:296
+#, c-format
+msgid "Pay with a mobile phone"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:298
+#, c-format
+msgid "Hide QR"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:305
+#, c-format
+msgid "Scan the QR code or &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:346
+#, c-format
+msgid "Pay &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:360
+#, c-format
+msgid "You have no balance for this currency. Withdraw digital cash first."
+msgstr ""
+
+#: src/cta/Payment/views.tsx:364
+#, c-format
+msgid ""
+"Could not find enough coins to pay. Even if you have enough %1$s some "
+"restriction may apply."
+msgstr ""
+
+#: src/cta/Payment/views.tsx:366
+#, c-format
+msgid "Your current balance is not enough."
+msgstr ""
+
+#: src/cta/Payment/views.tsx:395
+#, c-format
+msgid "Merchant message"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:34
+#, c-format
+msgid "Could not load refund status"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:48
+#, c-format
+msgid "Digital cash refund"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:52
+#, c-format
+msgid "You&apos;ve ignored the tip."
+msgstr ""
+
+#: src/cta/Refund/views.tsx:70
+#, c-format
+msgid "The refund is in progress."
+msgstr ""
+
+#: src/cta/Refund/views.tsx:76
+#, c-format
+msgid "Total to refund"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:106
+#, c-format
+msgid "The merchant &quot;%1$s&quot; is offering you a refund."
+msgstr ""
+
+#: src/cta/Refund/views.tsx:115
+#, c-format
+msgid "Order amount"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:122
+#, c-format
+msgid "Already refunded"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:129
+#, c-format
+msgid "Refund offered"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:145
+#, c-format
+msgid "Accept &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:32
+#, c-format
+msgid "Could not load tip status"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:45
+#, c-format
+msgid "Digital cash tip"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:66
+#, c-format
+msgid "The merchant is offering you a tip"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:74
+#, c-format
+msgid "Merchant URL"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:90
+#, c-format
+msgid "Receive &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:114
+#, c-format
+msgid "Tip from %1$s accepted. Check your transactions list for more details."
+msgstr ""
+
+#: src/components/SelectList.tsx:66
+#, c-format
+msgid "Select one option"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:39
+#, c-format
+msgid "Could not load"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:73
+#, c-format
+msgid "Show terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:81
+#, c-format
+msgid "I accept the exchange terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:107
+#, c-format
+msgid "Exchange doesn&apos;t have terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:135
+#, c-format
+msgid "Review exchange terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:146
+#, c-format
+msgid "Review new version of terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:170
+#, c-format
+msgid "The exchange reply with a empty terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:193
+#, c-format
+msgid "Download Terms of Service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:204
+#, c-format
+msgid "Hide terms of service"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:117
+#, c-format
+msgid "Could not load exchange fees"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:131
+#, c-format
+msgid "Close"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:160
+#, c-format
+msgid "could not find any exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:166
+#, c-format
+msgid "could not find any exchange for the currency %1$s"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:186
+#, c-format
+msgid "Service fee description"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:201
+#, c-format
+msgid "Select %1$s exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:215
+#, c-format
+msgid "Reset"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:218
+#, c-format
+msgid "Use this exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:230
+#, c-format
+msgid "Doesn&apos;t have auditors"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:241
+#, c-format
+msgid "currency"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:249
+#, c-format
+msgid "Operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:252
+#, c-format
+msgid "Deposits"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:259
+#, c-format
+msgid "Denomination"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:265
+#, c-format
+msgid "Until"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:274
+#, c-format
+msgid "Withdrawals"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:423
+#, c-format
+msgid "Currency"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:433
+#, c-format
+msgid "Coin operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:436
+#, c-format
+msgid ""
+"Every operation in this section may be different by denomination value and is "
+"valid for a period of time. The exchange will charge the indicated amount every "
+"time a coin is used in such operation."
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:545
+#, c-format
+msgid "Transfer operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:548
+#, c-format
+msgid ""
+"Every operation in this section may be different by transfer type and is valid "
+"for a period of time. The exchange will charge the indicated amount every time a "
+"transfer is made."
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:563
+#, c-format
+msgid "Operation"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:583
+#, c-format
+msgid "Wallet operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:597
+#, c-format
+msgid "Feature"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:47
+#, c-format
+msgid "Could not get the info from the URI"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:60
+#, c-format
+msgid "Could not get info of withdrawal"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:74
+#, c-format
+msgid "Digital cash withdrawal"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:79
+#, c-format
+msgid "Could not finish the withdrawal operation"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:127
+#, c-format
+msgid "Age restriction"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:145
+#, c-format
+msgid "Withdraw &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:179
+#, c-format
+msgid "Withdraw to a mobile phone"
+msgstr ""
+
+#: src/cta/InvoiceCreate/views.tsx:65
+#, c-format
+msgid "Digital invoice"
+msgstr ""
+
+#: src/cta/InvoiceCreate/views.tsx:69
+#, c-format
+msgid "Could not finish the invoice creation"
+msgstr ""
+
+#: src/cta/InvoiceCreate/views.tsx:130
+#, c-format
+msgid "Create"
+msgstr ""
+
+#: src/cta/InvoicePay/views.tsx:63
+#, c-format
+msgid "Could not finish the payment operation"
+msgstr ""
+
+#: src/cta/TransferCreate/views.tsx:55
+#, c-format
+msgid "Digital cash transfer"
+msgstr ""
+
+#: src/cta/TransferCreate/views.tsx:59
+#, c-format
+msgid "Could not finish the transfer creation"
+msgstr ""
+
+#: src/cta/TransferPickup/views.tsx:57
+#, c-format
+msgid "Could not finish the pickup operation"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:149
+#, c-format
+msgid "Manual Withdrawal for %1$s"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:154
+#, c-format
+msgid ""
+"Choose a exchange from where the coins will be withdrawn. The exchange will send "
+"the coins to this wallet after receiving a wire transfer with the correct "
+"subject."
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:162
+#, c-format
+msgid "No exchange found for %1$s"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:170
+#, c-format
+msgid "Add Exchange"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:192
+#, c-format
+msgid "No exchange configured"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:210
+#, c-format
+msgid "Can&apos;t create the reserve"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:277
+#, c-format
+msgid "Start withdrawal"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:38
+#, c-format
+msgid "Could not load deposit balance"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:51
+#, c-format
+msgid "A currency or an amount should be indicated"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:67
+#, c-format
+msgid "There is no enough balance to make a deposit for currency %1$s"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:117
+#, c-format
+msgid "Send %1$s to your account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:121
+#, c-format
+msgid "There is no account to make a deposit for currency %1$s"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:127
+#, c-format
+msgid "Add account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:151
+#, c-format
+msgid "Select account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:163
+#, c-format
+msgid "Add another account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:191
+#, c-format
+msgid "Deposit fee"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:205
+#, c-format
+msgid "Total deposit"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:233
+#, c-format
+msgid "Deposit&nbsp;%1$s %2$s"
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:56
+#, c-format
+msgid "Add bank account for %1$s"
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:59
+#, c-format
+msgid "Enter the URL of an exchange you trust."
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:66
+#, c-format
+msgid "Unable add this account"
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:73
+#, c-format
+msgid "Select account type"
+msgstr ""
+
+#: src/wallet/ExchangeAddConfirm.tsx:42
+#, c-format
+msgid "Review terms of service"
+msgstr ""
+
+#: src/wallet/ExchangeAddConfirm.tsx:45
+#, c-format
+msgid "Exchange URL"
+msgstr ""
+
+#: src/wallet/ExchangeAddConfirm.tsx:70
+#, c-format
+msgid "Add exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:112
+#, c-format
+msgid "Add new exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:116
+#, c-format
+msgid "Add exchange for %1$s"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:128
+#, c-format
+msgid "An exchange has been found! Review the information and click next"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:135
+#, c-format
+msgid "This exchange doesn&apos;t match the expected currency %1$s"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:143
+#, c-format
+msgid "Unable to verify this exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:151
+#, c-format
+msgid "Unable to add this exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:167
+#, c-format
+msgid "loading"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:174
+#, c-format
+msgid "Version"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:206
+#, c-format
+msgid "Next"
+msgstr ""
+
+#: src/components/TransactionItem.tsx:201
+#, c-format
+msgid "Waiting for confirmation"
+msgstr ""
+
+#: src/components/TransactionItem.tsx:266
+#, c-format
+msgid "PENDING"
+msgstr ""
+
+#: src/wallet/History.tsx:75
+#, c-format
+msgid "Could not load the list of transactions"
+msgstr ""
+
+#: src/wallet/History.tsx:233
+#, c-format
+msgid "Your transaction history is empty for this currency."
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:127
+#, c-format
+msgid "Add backup provider"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:131
+#, c-format
+msgid "Could not get provider information"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:140
+#, c-format
+msgid "Backup providers may charge for their service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:147
+#, c-format
+msgid "URL"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:158
+#, c-format
+msgid "Name"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:212
+#, c-format
+msgid "Provider URL"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:218
+#, c-format
+msgid "Please review and accept this provider&apos;s terms of service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:223
+#, c-format
+msgid "Pricing"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:226
+#, c-format
+msgid "free of charge"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:228
+#, c-format
+msgid "%1$s per year of service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:235
+#, c-format
+msgid "Storage"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:238
+#, c-format
+msgid "%1$s megabytes of storage per year of service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:244
+#, c-format
+msgid "Accept terms of service"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:44
+#, c-format
+msgid "Could not parse the payto URI"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:45
+#, c-format
+msgid "Please check the uri"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:75
+#, c-format
+msgid "Exchange is ready for withdrawal"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:78
+#, c-format
+msgid "To complete the process you need to wire%1$s %2$s to the exchange bank account"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:87
+#, c-format
+msgid ""
+"Alternative, you can also scan this QR code or open %1$s if you have a banking "
+"app installed that supports RFC 8905"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:98
+#, c-format
+msgid "Cancel withdrawal"
+msgstr ""
+
+#: src/wallet/Settings.tsx:115
+#, c-format
+msgid "Could not toggle auto-open"
+msgstr ""
+
+#: src/wallet/Settings.tsx:121
+#, c-format
+msgid "Could not toggle clipboard"
+msgstr ""
+
+#: src/wallet/Settings.tsx:126
+#, c-format
+msgid "Navigator"
+msgstr ""
+
+#: src/wallet/Settings.tsx:129
+#, c-format
+msgid "Automatically open wallet based on page content"
+msgstr ""
+
+#: src/wallet/Settings.tsx:135
+#, c-format
+msgid ""
+"Enabling this option below will make using the wallet faster, but requires more "
+"permissions from your browser."
+msgstr ""
+
+#: src/wallet/Settings.tsx:145
+#, c-format
+msgid "Automatically check clipboard for Taler URI"
+msgstr ""
+
+#: src/wallet/Settings.tsx:162
+#, c-format
+msgid "Trust"
+msgstr ""
+
+#: src/wallet/Settings.tsx:166
+#, c-format
+msgid "No exchange yet"
+msgstr ""
+
+#: src/wallet/Settings.tsx:180
+#, c-format
+msgid "Term of Service"
+msgstr ""
+
+#: src/wallet/Settings.tsx:191
+#, c-format
+msgid "ok"
+msgstr ""
+
+#: src/wallet/Settings.tsx:197
+#, c-format
+msgid "changed"
+msgstr ""
+
+#: src/wallet/Settings.tsx:204
+#, c-format
+msgid "not accepted"
+msgstr ""
+
+#: src/wallet/Settings.tsx:210
+#, c-format
+msgid "unknown (exchange status should be updated)"
+msgstr ""
+
+#: src/wallet/Settings.tsx:236
+#, c-format
+msgid "Add an exchange"
+msgstr ""
+
+#: src/wallet/Settings.tsx:241
+#, c-format
+msgid "Troubleshooting"
+msgstr ""
+
+#: src/wallet/Settings.tsx:244
+#, c-format
+msgid "Developer mode"
+msgstr ""
+
+#: src/wallet/Settings.tsx:246
+#, c-format
+msgid "More options and information useful for debugging"
+msgstr ""
+
+#: src/wallet/Settings.tsx:257
+#, c-format
+msgid "Display"
+msgstr ""
+
+#: src/wallet/Settings.tsx:261
+#, c-format
+msgid "Current Language"
+msgstr ""
+
+#: src/wallet/Settings.tsx:274
+#, c-format
+msgid "Wallet Core"
+msgstr ""
+
+#: src/wallet/Settings.tsx:284
+#, c-format
+msgid "Web Extension"
+msgstr ""
+
+#: src/wallet/Settings.tsx:295
+#, c-format
+msgid "Exchange compatibility"
+msgstr ""
+
+#: src/wallet/Settings.tsx:299
+#, c-format
+msgid "Merchant compatibility"
+msgstr ""
+
+#: src/wallet/Settings.tsx:303
+#, c-format
+msgid "Bank compatibility"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:59
+#, c-format
+msgid "Browser Extension Installed!"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:63
+#, c-format
+msgid "You can open the GNU Taler Wallet using the combination %1$s ."
+msgstr ""
+
+#: src/wallet/Welcome.tsx:72
+#, c-format
+msgid ""
+"Also pinning the GNU Taler Wallet to your Chrome browser allows you to quick "
+"access without keyboard:"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:79
+#, c-format
+msgid "Click the puzzle icon"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:82
+#, c-format
+msgid "Search for GNU Taler Wallet"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:85
+#, c-format
+msgid "Click the pin icon"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:91
+#, c-format
+msgid "Permissions"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:100
+#, c-format
+msgid ""
+"(Enabling this option below will make using the wallet faster, but requires more "
+"permissions from your browser.)"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:110
+#, c-format
+msgid "Next Steps"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:113
+#, c-format
+msgid "Try the demo"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:116
+#, c-format
+msgid "Learn how to top up your wallet balance"
+msgstr ""
+
+#: src/components/Diagnostics.tsx:31
+#, c-format
+msgid "Diagnostics timed out. Could not talk to the wallet backend."
+msgstr ""
+
+#: src/components/Diagnostics.tsx:52
+#, c-format
+msgid "Problems detected:"
+msgstr ""
+
+#: src/components/Diagnostics.tsx:61
+#, c-format
+msgid ""
+"Please check in your %1$s settings that you have IndexedDB enabled (check the "
+"preference name %2$s)."
+msgstr ""
+
+#: src/components/Diagnostics.tsx:70
+#, c-format
+msgid ""
+"Your wallet database is outdated. Currently automatic migration is not "
+"supported. Please go %1$s to reset the wallet database."
+msgstr ""
+
+#: src/components/Diagnostics.tsx:83
+#, c-format
+msgid "Running diagnostics"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:163
+#, c-format
+msgid "Debug tools"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:170
+#, c-format
+msgid ""
+"Do you want to IRREVOCABLY DESTROY everything inside your wallet and LOSE ALL "
+"YOUR COINS?"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:176
+#, c-format
+msgid "reset"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:183
+#, c-format
+msgid "TESTING: This may delete all your coin, proceed with caution"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:189
+#, c-format
+msgid "run gc"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:197
+#, c-format
+msgid "import database"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:219
+#, c-format
+msgid "export database"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:225
+#, c-format
+msgid "Database exported at %1$s %2$s to download"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:248
+#, c-format
+msgid "Coins"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:282
+#, c-format
+msgid "Pending operations"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:328
+#, c-format
+msgid "usable coins"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:337
+#, c-format
+msgid "id"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:340
+#, c-format
+msgid "denom"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:343
+#, c-format
+msgid "value"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:346
+#, c-format
+msgid "status"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:349
+#, c-format
+msgid "from refresh?"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:352
+#, c-format
+msgid "age key count"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:369
+#, c-format
+msgid "spent coins"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:373
+#, c-format
+msgid "click to show"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:108
+#, c-format
+msgid "Scan a QR code or enter taler:// URI below"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:122
+#, c-format
+msgid "Open"
+msgstr "Avoin"
+
+#: src/wallet/QrReader.tsx:128
+#, c-format
+msgid "URI is not valid. Taler URI should start with `taler://`"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:133
+#, c-format
+msgid "Try another"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:183
+#, c-format
+msgid "Could not load list of exchange"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:209
+#, c-format
+msgid "Choose a currency to proceed or add another exchange"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:217
+#, c-format
+msgid "Known currencies"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:318
+#, c-format
+msgid "Specify the amount and the origin"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:336
+#, c-format
+msgid "Change currency"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:344
+#, c-format
+msgid "Use previous origins:"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:364
+#, c-format
+msgid "Or specify the origin of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:372
+#, c-format
+msgid "Specify the origin of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:380
+#, c-format
+msgid "From my bank account"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:395
+#, c-format
+msgid "From another wallet"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:449
+#, c-format
+msgid "currency not provided"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:459
+#, c-format
+msgid "Specify the amount and the destination"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:483
+#, c-format
+msgid "Use previous destinations:"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:503
+#, c-format
+msgid "Or specify the destination of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:511
+#, c-format
+msgid "Specify the destination of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:521
+#, c-format
+msgid "To my bank account"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:534
+#, c-format
+msgid "To another wallet"
+msgstr ""
+
+#: src/cta/Recovery/views.tsx:30
+#, c-format
+msgid "Could not load backup recovery information"
+msgstr ""
+
+#: src/cta/Recovery/views.tsx:47
+#, c-format
+msgid "Digital wallet recovery"
+msgstr ""
+
+#: src/cta/Recovery/views.tsx:52
+#, c-format
+msgid "Import backup, show info"
+msgstr ""
+
+#: src/wallet/Application.tsx:189
+#, c-format
+msgid "All done, your transaction is in progress"
+msgstr ""
+
+#: src/components/EditableText.tsx:45
+#, c-format
+msgid "Edit"
+msgstr ""
+
+#: src/wallet/ManualWithdrawPage.tsx:102
+#, c-format
+msgid "Could not load the list of known exchanges"
+msgstr ""
diff --git a/packages/taler-wallet-webextension/src/i18n/fr.po b/packages/taler-wallet-webextension/src/i18n/fr.po
index e5b9df860..462eb30f7 100644
--- a/packages/taler-wallet-webextension/src/i18n/fr.po
+++ b/packages/taler-wallet-webextension/src/i18n/fr.po
@@ -15,18 +15,18 @@
msgid ""
msgstr ""
"Project-Id-Version: Taler Wallet\n"
-"Report-Msgid-Bugs-To: \n"
+"Report-Msgid-Bugs-To: languages@taler.net\n"
"POT-Creation-Date: 2016-11-23 00:00+0100\n"
-"PO-Revision-Date: 2022-02-18 19:31+0000\n"
-"Last-Translator: Stefan <eintritt@hotmail.com>\n"
-"Language-Team: French <http://weblate.taler.net/projects/gnu-taler/"
+"PO-Revision-Date: 2024-02-28 08:07+0000\n"
+"Last-Translator: d0p1 <contact@d0p1.eu>\n"
+"Language-Team: French <https://weblate.taler.net/projects/gnu-taler/"
"webextensions/fr/>\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n!=1);\n"
-"X-Generator: Weblate 4.4.2\n"
+"X-Generator: Weblate 5.2.1\n"
#: src/NavigationBar.tsx:139
#, c-format
@@ -213,7 +213,7 @@ msgstr ""
#: src/wallet/AddNewActionView.tsx:57
#, c-format
msgid "Cancel"
-msgstr ""
+msgstr "Annuler"
#: src/wallet/AddNewActionView.tsx:68
#, c-format
@@ -714,7 +714,7 @@ msgstr ""
#: src/wallet/Transaction.tsx:635
#, c-format
msgid "Exchange"
-msgstr ""
+msgstr "Exchange"
#: src/wallet/Transaction.tsx:641
#, c-format
@@ -1051,7 +1051,7 @@ msgstr ""
#: src/wallet/ExchangeSelection/views.tsx:131
#, c-format
msgid "Close"
-msgstr ""
+msgstr "Fermer"
#: src/wallet/ExchangeSelection/views.tsx:160
#, c-format
diff --git a/packages/taler-wallet-webextension/src/i18n/it.po b/packages/taler-wallet-webextension/src/i18n/it.po
index 3d9110ade..e0568b8f8 100644
--- a/packages/taler-wallet-webextension/src/i18n/it.po
+++ b/packages/taler-wallet-webextension/src/i18n/it.po
@@ -15,23 +15,23 @@
msgid ""
msgstr ""
"Project-Id-Version: Taler Wallet\n"
-"Report-Msgid-Bugs-To: \n"
+"Report-Msgid-Bugs-To: languages@taler.net\n"
"POT-Creation-Date: 2016-11-23 00:00+0100\n"
-"PO-Revision-Date: 2022-02-18 19:33+0000\n"
-"Last-Translator: Stefan <eintritt@hotmail.com>\n"
-"Language-Team: Italian <http://weblate.taler.net/projects/gnu-taler/"
+"PO-Revision-Date: 2023-08-16 12:43+0000\n"
+"Last-Translator: Krystian Baran <kiszkot@murena.io>\n"
+"Language-Team: Italian <https://weblate.taler.net/projects/gnu-taler/"
"webextensions/it/>\n"
"Language: it\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
-"X-Generator: Weblate 4.4.2\n"
+"X-Generator: Weblate 4.13.1\n"
#: src/NavigationBar.tsx:139
#, c-format
msgid "Balance"
-msgstr ""
+msgstr "Saldo"
#: src/NavigationBar.tsx:142
#, c-format
@@ -41,12 +41,12 @@ msgstr ""
#: src/NavigationBar.tsx:147
#, c-format
msgid "QR Reader and Taler URI"
-msgstr ""
+msgstr "Lettore QR e Taler URI"
#: src/NavigationBar.tsx:154
#, c-format
msgid "Settings"
-msgstr ""
+msgstr "Impostazioni"
#: src/NavigationBar.tsx:184
#, c-format
@@ -328,7 +328,7 @@ msgstr ""
#: src/components/ShowFullContractTermPopup.tsx:195
#, c-format
msgid "Amount"
-msgstr ""
+msgstr "Importo"
#: src/components/ShowFullContractTermPopup.tsx:203
#, c-format
@@ -530,7 +530,7 @@ msgstr ""
#: src/components/BankDetailsByPaytoType.tsx:148
#, c-format
msgid "Subject"
-msgstr ""
+msgstr "Soggetto"
#: src/components/BankDetailsByPaytoType.tsx:154
#, c-format
@@ -714,7 +714,7 @@ msgstr ""
#: src/wallet/Transaction.tsx:635
#, c-format
msgid "Exchange"
-msgstr ""
+msgstr "Cambio"
#: src/wallet/Transaction.tsx:641
#, c-format
@@ -784,7 +784,7 @@ msgstr ""
#: src/wallet/Transaction.tsx:935
#, c-format
msgid "Date"
-msgstr ""
+msgstr "Data"
#: src/wallet/Transaction.tsx:990
#, c-format
@@ -799,7 +799,7 @@ msgstr ""
#: src/wallet/Transaction.tsx:1074
#, c-format
msgid "Withdraw"
-msgstr ""
+msgstr "Prelevare"
#: src/wallet/Transaction.tsx:1146
#, c-format
@@ -809,7 +809,7 @@ msgstr ""
#: src/wallet/Transaction.tsx:1156
#, c-format
msgid "Refunded"
-msgstr ""
+msgstr "Rimborsato"
#: src/wallet/Transaction.tsx:1220
#, c-format
@@ -1270,7 +1270,7 @@ msgstr ""
#: src/wallet/CreateManualWithdraw.tsx:277
#, c-format
msgid "Start withdrawal"
-msgstr ""
+msgstr "Inizia a prelevare"
#: src/wallet/DepositPage/views.tsx:38
#, c-format
@@ -1576,7 +1576,7 @@ msgstr ""
#: src/wallet/Settings.tsx:191
#, c-format
msgid "ok"
-msgstr ""
+msgstr "ok"
#: src/wallet/Settings.tsx:197
#, c-format
diff --git a/packages/taler-wallet-webextension/src/i18n/ja.po b/packages/taler-wallet-webextension/src/i18n/ja.po
index a82abf4b5..298ad6018 100644
--- a/packages/taler-wallet-webextension/src/i18n/ja.po
+++ b/packages/taler-wallet-webextension/src/i18n/ja.po
@@ -15,18 +15,18 @@
msgid ""
msgstr ""
"Project-Id-Version: Taler Wallet\n"
-"Report-Msgid-Bugs-To: \n"
+"Report-Msgid-Bugs-To: languages@taler.net\n"
"POT-Creation-Date: 2016-11-23 00:00+0100\n"
-"PO-Revision-Date: 2022-02-18 17:20+0000\n"
-"Last-Translator: Stefan <eintritt@hotmail.com>\n"
-"Language-Team: Japanese <http://weblate.taler.net/projects/gnu-taler/"
+"PO-Revision-Date: 2023-03-06 22:06+0000\n"
+"Last-Translator: Anonymous <noreply@weblate.org>\n"
+"Language-Team: Japanese <https://weblate.taler.net/projects/gnu-taler/"
"webextensions/ja/>\n"
"Language: ja\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
-"X-Generator: Weblate 4.4.2\n"
+"X-Generator: Weblate 4.13.1\n"
#: src/NavigationBar.tsx:139
#, c-format
diff --git a/packages/taler-wallet-webextension/src/i18n/nl.po b/packages/taler-wallet-webextension/src/i18n/nl.po
new file mode 100644
index 000000000..4f11592dd
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/i18n/nl.po
@@ -0,0 +1,1953 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: languages@taler.net\n"
+"POT-Creation-Date: 2016-11-23 00:00+0100\n"
+"PO-Revision-Date: 2024-03-02 16:54+0000\n"
+"Last-Translator: Midgard <midgard@users.noreply.weblate.taler.net>\n"
+"Language-Team: Dutch <https://weblate.taler.net/projects/gnu-taler/"
+"webextensions/nl/>\n"
+"Language: nl\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 5.2.1\n"
+
+#: src/NavigationBar.tsx:139
+#, c-format
+msgid "Balance"
+msgstr ""
+
+#: src/NavigationBar.tsx:142
+#, c-format
+msgid "Backup"
+msgstr ""
+
+#: src/NavigationBar.tsx:147
+#, c-format
+msgid "QR Reader and Taler URI"
+msgstr ""
+
+#: src/NavigationBar.tsx:154
+#, c-format
+msgid "Settings"
+msgstr ""
+
+#: src/NavigationBar.tsx:184
+#, c-format
+msgid "Dev"
+msgstr ""
+
+#: src/mui/Typography.tsx:122
+#, c-format
+msgid "%1$s"
+msgstr ""
+
+#: src/components/PendingTransactions.tsx:74
+#, c-format
+msgid "PENDING OPERATIONS"
+msgstr ""
+
+#: src/components/Loading.tsx:36
+#, c-format
+msgid "Loading"
+msgstr ""
+
+#: src/wallet/BackupPage.tsx:123
+#, c-format
+msgid "Could not load backup providers"
+msgstr ""
+
+#: src/wallet/BackupPage.tsx:202
+#, c-format
+msgid "No backup providers configured"
+msgstr ""
+
+#: src/wallet/BackupPage.tsx:205
+#, c-format
+msgid "Add provider"
+msgstr ""
+
+#: src/wallet/BackupPage.tsx:219
+#, c-format
+msgid "Sync all backups"
+msgstr ""
+
+#: src/wallet/BackupPage.tsx:221
+#, c-format
+msgid "Sync now"
+msgstr ""
+
+#: src/wallet/BackupPage.tsx:264
+#, c-format
+msgid "Last synced"
+msgstr ""
+
+#: src/wallet/BackupPage.tsx:269
+#, c-format
+msgid "Not synced"
+msgstr ""
+
+#: src/wallet/BackupPage.tsx:289
+#, c-format
+msgid "Expires in"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:60
+#, c-format
+msgid "There was an error loading the provider detail for &quot; %1$s&quot;"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:108
+#, c-format
+msgid "There is not known provider with url &quot;%1$s&quot;."
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:115
+#, c-format
+msgid "See providers"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:143
+#, c-format
+msgid "Last backup"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:148
+#, c-format
+msgid "Back up"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:154
+#, c-format
+msgid "Provider fee"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:157
+#, c-format
+msgid "per year"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:163
+#, c-format
+msgid "Extend"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:169
+#, c-format
+msgid ""
+"terms has changed, extending the service will imply accepting the new terms of "
+"service"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:179
+#, c-format
+msgid "old"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:183
+#, c-format
+msgid "new"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:190
+#, c-format
+msgid "fee"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:198
+#, c-format
+msgid "storage"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:215
+#, c-format
+msgid "Remove provider"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:228
+#, c-format
+msgid "This provider has reported an error"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:242
+#, c-format
+msgid "There is conflict with another backup from %1$s"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:253
+#, c-format
+msgid "Backup is not readable"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:261
+#, c-format
+msgid "Unknown backup problem: %1$s"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:283
+#, c-format
+msgid "service paid"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:290
+#, c-format
+msgid "Backup valid until"
+msgstr ""
+
+#: src/wallet/AddNewActionView.tsx:57
+#, c-format
+msgid "Cancel"
+msgstr ""
+
+#: src/wallet/AddNewActionView.tsx:68
+#, c-format
+msgid "Open reserve page"
+msgstr ""
+
+#: src/wallet/AddNewActionView.tsx:70
+#, c-format
+msgid "Open pay page"
+msgstr ""
+
+#: src/wallet/AddNewActionView.tsx:72
+#, c-format
+msgid "Open refund page"
+msgstr ""
+
+#: src/wallet/AddNewActionView.tsx:74
+#, c-format
+msgid "Open tip page"
+msgstr ""
+
+#: src/wallet/AddNewActionView.tsx:76
+#, c-format
+msgid "Open withdraw page"
+msgstr ""
+
+#: src/popup/NoBalanceHelp.tsx:43
+#, c-format
+msgid "Get digital cash"
+msgstr ""
+
+#: src/popup/BalancePage.tsx:138
+#, c-format
+msgid "Could not load balance page"
+msgstr ""
+
+#: src/popup/BalancePage.tsx:175
+#, c-format
+msgid "Add"
+msgstr ""
+
+#: src/popup/BalancePage.tsx:179
+#, c-format
+msgid "Send %1$s"
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:44
+#, c-format
+msgid "Taler Action"
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:49
+#, c-format
+msgid "This page has pay action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:63
+#, c-format
+msgid "This page has a withdrawal action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:79
+#, c-format
+msgid "This page has a tip action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:93
+#, c-format
+msgid "This page has a notify reserve action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:102
+#, c-format
+msgid "Notify"
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:109
+#, c-format
+msgid "This page has a refund action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:123
+#, c-format
+msgid "This page has a malformed taler uri."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:134
+#, c-format
+msgid "Dismiss"
+msgstr ""
+
+#: src/popup/Application.tsx:177
+#, c-format
+msgid "this popup is being closed and you are being redirected to %1$s"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:158
+#, c-format
+msgid "Could not load purchase proposal details"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:183
+#, c-format
+msgid "Order Id"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:189
+#, c-format
+msgid "Summary"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:195
+#, c-format
+msgid "Amount"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:203
+#, c-format
+msgid "Merchant name"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:209
+#, c-format
+msgid "Merchant jurisdiction"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:215
+#, c-format
+msgid "Merchant address"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:221
+#, c-format
+msgid "Merchant logo"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:234
+#, c-format
+msgid "Merchant website"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:240
+#, c-format
+msgid "Merchant email"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:246
+#, c-format
+msgid "Merchant public key"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:256
+#, c-format
+msgid "Delivery date"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:271
+#, c-format
+msgid "Delivery location"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:277
+#, c-format
+msgid "Products"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:289
+#, c-format
+msgid "Created at"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:304
+#, c-format
+msgid "Refund deadline"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:319
+#, c-format
+msgid "Auto refund"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:339
+#, c-format
+msgid "Pay deadline"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:354
+#, c-format
+msgid "Fulfillment URL"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:360
+#, c-format
+msgid "Fulfillment message"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:370
+#, c-format
+msgid "Max deposit fee"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:378
+#, c-format
+msgid "Max fee"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:386
+#, c-format
+msgid "Minimum age"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:398
+#, c-format
+msgid "Wire fee amortization"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:404
+#, c-format
+msgid "Auditors"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:419
+#, c-format
+msgid "Exchanges"
+msgstr ""
+
+#: src/components/Part.tsx:148
+#, c-format
+msgid "Bank account"
+msgstr ""
+
+#: src/components/Part.tsx:160
+#, c-format
+msgid "Bitcoin address"
+msgstr ""
+
+#: src/components/Part.tsx:163
+#, c-format
+msgid "IBAN"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:38
+#, c-format
+msgid "Could not load deposit status"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:52
+#, c-format
+msgid "Digital cash deposit"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:58
+#, c-format
+msgid "Cost"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:66
+#, c-format
+msgid "Fee"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:73
+#, c-format
+msgid "To be received"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:84
+#, c-format
+msgid "Send &nbsp; %1$s"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:63
+#, c-format
+msgid "Bitcoin transfer details"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:66
+#, c-format
+msgid ""
+"The exchange need a transaction with 3 output, one output is the exchange "
+"account and the other two are segwit fake address for metadata with an minimum "
+"amount."
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:74
+#, c-format
+msgid ""
+"In bitcoincore wallet use &apos;Add Recipient&apos; button to add two additional "
+"recipient and copy addresses and amounts"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:98
+#, c-format
+msgid "Make sure the amount show %1$s BTC, else you have to change the base unit to BTC"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:110
+#, c-format
+msgid "Account"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:116
+#, c-format
+msgid "Bank host"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:139
+#, c-format
+msgid "Bank transfer details"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:148
+#, c-format
+msgid "Subject"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:154
+#, c-format
+msgid "Receiver name"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:98
+#, c-format
+msgid "Could not load the transaction information"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:191
+#, c-format
+msgid "There was an error trying to complete the transaction"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:200
+#, c-format
+msgid "This transaction is not completed"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:209
+#, c-format
+msgid "Send"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:216
+#, c-format
+msgid "Retry"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:224
+#, c-format
+msgid "Forget"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:241
+#, c-format
+msgid "Caution!"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:244
+#, c-format
+msgid ""
+"If you have already wired money to the exchange you will loose the chance to get "
+"the coins form it."
+msgstr ""
+
+#: src/wallet/Transaction.tsx:259
+#, c-format
+msgid "Confirm"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:267
+#, c-format
+msgid "Withdrawal"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:286
+#, c-format
+msgid ""
+"Make sure to use the correct subject, otherwise the money will not arrive in "
+"this wallet."
+msgstr ""
+
+#: src/wallet/Transaction.tsx:298
+#, c-format
+msgid ""
+"The bank did not yet confirmed the wire transfer. Go to the %1$s %2$s and check "
+"there is no pending step."
+msgstr ""
+
+#: src/wallet/Transaction.tsx:316
+#, c-format
+msgid "Bank has confirmed the wire transfer. Waiting for the exchange to send the coins"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:325
+#, c-format
+msgid "Details"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:360
+#, c-format
+msgid "Payment"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:378
+#, c-format
+msgid "Refunds"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:385
+#, c-format
+msgid "%1$s %2$s on %3$s"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:415
+#, c-format
+msgid "Merchant created a refund for this order but was not automatically picked up."
+msgstr ""
+
+#: src/wallet/Transaction.tsx:420
+#, c-format
+msgid "Offer"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:431
+#, c-format
+msgid "Accept"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:438
+#, c-format
+msgid "Merchant"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:443
+#, c-format
+msgid "Invoice ID"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:470
+#, c-format
+msgid "Deposit"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:496
+#, c-format
+msgid "Refresh"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:517
+#, c-format
+msgid "Tip"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:542
+#, c-format
+msgid "Refund"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:555
+#, c-format
+msgid "Original order ID"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:568
+#, c-format
+msgid "Purchase summary"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:593
+#, c-format
+msgid "copy"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:596
+#, c-format
+msgid "hide qr"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:608
+#, c-format
+msgid "show qr"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:620
+#, c-format
+msgid "Credit"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:624
+#, c-format
+msgid "Invoice"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:635
+#, c-format
+msgid "Exchange"
+msgstr "Beurs"
+
+#: src/wallet/Transaction.tsx:641
+#, c-format
+msgid "URI"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:667
+#, c-format
+msgid "Debit"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:710
+#, c-format
+msgid "Transfer"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:844
+#, c-format
+msgid "Country"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:852
+#, c-format
+msgid "Address lines"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:860
+#, c-format
+msgid "Building number"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:868
+#, c-format
+msgid "Building name"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:876
+#, c-format
+msgid "Street"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:884
+#, c-format
+msgid "Post code"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:892
+#, c-format
+msgid "Town location"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:900
+#, c-format
+msgid "Town"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:908
+#, c-format
+msgid "District"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:916
+#, c-format
+msgid "Country subdivision"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:935
+#, c-format
+msgid "Date"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:990
+#, c-format
+msgid "Transaction fees"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1004
+#, c-format
+msgid "Total"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1074
+#, c-format
+msgid "Withdraw"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1146
+#, c-format
+msgid "Price"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1156
+#, c-format
+msgid "Refunded"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1220
+#, c-format
+msgid "Delivery"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1335
+#, c-format
+msgid "Total transfer"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:57
+#, c-format
+msgid "Could not load pay status"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:87
+#, c-format
+msgid "Digital cash payment"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:119
+#, c-format
+msgid "Purchase"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:149
+#, c-format
+msgid "Receipt"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:156
+#, c-format
+msgid "Valid until"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:191
+#, c-format
+msgid "List of products"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:242
+#, c-format
+msgid "free"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:263
+#, c-format
+msgid "Already paid, you are going to be redirected to %1$s"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:274
+#, c-format
+msgid "Already paid"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:280
+#, c-format
+msgid "Already claimed"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:296
+#, c-format
+msgid "Pay with a mobile phone"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:298
+#, c-format
+msgid "Hide QR"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:305
+#, c-format
+msgid "Scan the QR code or &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:346
+#, c-format
+msgid "Pay &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:360
+#, c-format
+msgid "You have no balance for this currency. Withdraw digital cash first."
+msgstr ""
+
+#: src/cta/Payment/views.tsx:364
+#, c-format
+msgid ""
+"Could not find enough coins to pay. Even if you have enough %1$s some "
+"restriction may apply."
+msgstr ""
+
+#: src/cta/Payment/views.tsx:366
+#, c-format
+msgid "Your current balance is not enough."
+msgstr ""
+
+#: src/cta/Payment/views.tsx:395
+#, c-format
+msgid "Merchant message"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:34
+#, c-format
+msgid "Could not load refund status"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:48
+#, c-format
+msgid "Digital cash refund"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:52
+#, c-format
+msgid "You&apos;ve ignored the tip."
+msgstr ""
+
+#: src/cta/Refund/views.tsx:70
+#, c-format
+msgid "The refund is in progress."
+msgstr ""
+
+#: src/cta/Refund/views.tsx:76
+#, c-format
+msgid "Total to refund"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:106
+#, c-format
+msgid "The merchant &quot;%1$s&quot; is offering you a refund."
+msgstr ""
+
+#: src/cta/Refund/views.tsx:115
+#, c-format
+msgid "Order amount"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:122
+#, c-format
+msgid "Already refunded"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:129
+#, c-format
+msgid "Refund offered"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:145
+#, c-format
+msgid "Accept &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:32
+#, c-format
+msgid "Could not load tip status"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:45
+#, c-format
+msgid "Digital cash tip"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:66
+#, c-format
+msgid "The merchant is offering you a tip"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:74
+#, c-format
+msgid "Merchant URL"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:90
+#, c-format
+msgid "Receive &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:114
+#, c-format
+msgid "Tip from %1$s accepted. Check your transactions list for more details."
+msgstr ""
+
+#: src/components/SelectList.tsx:66
+#, c-format
+msgid "Select one option"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:39
+#, c-format
+msgid "Could not load"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:73
+#, c-format
+msgid "Show terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:81
+#, c-format
+msgid "I accept the exchange terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:107
+#, c-format
+msgid "Exchange doesn&apos;t have terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:135
+#, c-format
+msgid "Review exchange terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:146
+#, c-format
+msgid "Review new version of terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:170
+#, c-format
+msgid "The exchange reply with a empty terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:193
+#, c-format
+msgid "Download Terms of Service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:204
+#, c-format
+msgid "Hide terms of service"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:117
+#, c-format
+msgid "Could not load exchange fees"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:131
+#, c-format
+msgid "Close"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:160
+#, c-format
+msgid "could not find any exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:166
+#, c-format
+msgid "could not find any exchange for the currency %1$s"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:186
+#, c-format
+msgid "Service fee description"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:201
+#, c-format
+msgid "Select %1$s exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:215
+#, c-format
+msgid "Reset"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:218
+#, c-format
+msgid "Use this exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:230
+#, c-format
+msgid "Doesn&apos;t have auditors"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:241
+#, c-format
+msgid "currency"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:249
+#, c-format
+msgid "Operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:252
+#, c-format
+msgid "Deposits"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:259
+#, c-format
+msgid "Denomination"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:265
+#, c-format
+msgid "Until"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:274
+#, c-format
+msgid "Withdrawals"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:423
+#, c-format
+msgid "Currency"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:433
+#, c-format
+msgid "Coin operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:436
+#, c-format
+msgid ""
+"Every operation in this section may be different by denomination value and is "
+"valid for a period of time. The exchange will charge the indicated amount every "
+"time a coin is used in such operation."
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:545
+#, c-format
+msgid "Transfer operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:548
+#, c-format
+msgid ""
+"Every operation in this section may be different by transfer type and is valid "
+"for a period of time. The exchange will charge the indicated amount every time a "
+"transfer is made."
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:563
+#, c-format
+msgid "Operation"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:583
+#, c-format
+msgid "Wallet operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:597
+#, c-format
+msgid "Feature"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:47
+#, c-format
+msgid "Could not get the info from the URI"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:60
+#, c-format
+msgid "Could not get info of withdrawal"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:74
+#, c-format
+msgid "Digital cash withdrawal"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:79
+#, c-format
+msgid "Could not finish the withdrawal operation"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:127
+#, c-format
+msgid "Age restriction"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:145
+#, c-format
+msgid "Withdraw &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:179
+#, c-format
+msgid "Withdraw to a mobile phone"
+msgstr ""
+
+#: src/cta/InvoiceCreate/views.tsx:65
+#, c-format
+msgid "Digital invoice"
+msgstr ""
+
+#: src/cta/InvoiceCreate/views.tsx:69
+#, c-format
+msgid "Could not finish the invoice creation"
+msgstr ""
+
+#: src/cta/InvoiceCreate/views.tsx:130
+#, c-format
+msgid "Create"
+msgstr ""
+
+#: src/cta/InvoicePay/views.tsx:63
+#, c-format
+msgid "Could not finish the payment operation"
+msgstr ""
+
+#: src/cta/TransferCreate/views.tsx:55
+#, c-format
+msgid "Digital cash transfer"
+msgstr ""
+
+#: src/cta/TransferCreate/views.tsx:59
+#, c-format
+msgid "Could not finish the transfer creation"
+msgstr ""
+
+#: src/cta/TransferPickup/views.tsx:57
+#, c-format
+msgid "Could not finish the pickup operation"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:149
+#, c-format
+msgid "Manual Withdrawal for %1$s"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:154
+#, c-format
+msgid ""
+"Choose a exchange from where the coins will be withdrawn. The exchange will send "
+"the coins to this wallet after receiving a wire transfer with the correct "
+"subject."
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:162
+#, c-format
+msgid "No exchange found for %1$s"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:170
+#, c-format
+msgid "Add Exchange"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:192
+#, c-format
+msgid "No exchange configured"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:210
+#, c-format
+msgid "Can&apos;t create the reserve"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:277
+#, c-format
+msgid "Start withdrawal"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:38
+#, c-format
+msgid "Could not load deposit balance"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:51
+#, c-format
+msgid "A currency or an amount should be indicated"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:67
+#, c-format
+msgid "There is no enough balance to make a deposit for currency %1$s"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:117
+#, c-format
+msgid "Send %1$s to your account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:121
+#, c-format
+msgid "There is no account to make a deposit for currency %1$s"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:127
+#, c-format
+msgid "Add account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:151
+#, c-format
+msgid "Select account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:163
+#, c-format
+msgid "Add another account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:191
+#, c-format
+msgid "Deposit fee"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:205
+#, c-format
+msgid "Total deposit"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:233
+#, c-format
+msgid "Deposit&nbsp;%1$s %2$s"
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:56
+#, c-format
+msgid "Add bank account for %1$s"
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:59
+#, c-format
+msgid "Enter the URL of an exchange you trust."
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:66
+#, c-format
+msgid "Unable add this account"
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:73
+#, c-format
+msgid "Select account type"
+msgstr ""
+
+#: src/wallet/ExchangeAddConfirm.tsx:42
+#, c-format
+msgid "Review terms of service"
+msgstr ""
+
+#: src/wallet/ExchangeAddConfirm.tsx:45
+#, c-format
+msgid "Exchange URL"
+msgstr ""
+
+#: src/wallet/ExchangeAddConfirm.tsx:70
+#, c-format
+msgid "Add exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:112
+#, c-format
+msgid "Add new exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:116
+#, c-format
+msgid "Add exchange for %1$s"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:128
+#, c-format
+msgid "An exchange has been found! Review the information and click next"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:135
+#, c-format
+msgid "This exchange doesn&apos;t match the expected currency %1$s"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:143
+#, c-format
+msgid "Unable to verify this exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:151
+#, c-format
+msgid "Unable to add this exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:167
+#, c-format
+msgid "loading"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:174
+#, c-format
+msgid "Version"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:206
+#, c-format
+msgid "Next"
+msgstr ""
+
+#: src/components/TransactionItem.tsx:201
+#, c-format
+msgid "Waiting for confirmation"
+msgstr ""
+
+#: src/components/TransactionItem.tsx:266
+#, c-format
+msgid "PENDING"
+msgstr ""
+
+#: src/wallet/History.tsx:75
+#, c-format
+msgid "Could not load the list of transactions"
+msgstr ""
+
+#: src/wallet/History.tsx:233
+#, c-format
+msgid "Your transaction history is empty for this currency."
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:127
+#, c-format
+msgid "Add backup provider"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:131
+#, c-format
+msgid "Could not get provider information"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:140
+#, c-format
+msgid "Backup providers may charge for their service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:147
+#, c-format
+msgid "URL"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:158
+#, c-format
+msgid "Name"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:212
+#, c-format
+msgid "Provider URL"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:218
+#, c-format
+msgid "Please review and accept this provider&apos;s terms of service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:223
+#, c-format
+msgid "Pricing"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:226
+#, c-format
+msgid "free of charge"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:228
+#, c-format
+msgid "%1$s per year of service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:235
+#, c-format
+msgid "Storage"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:238
+#, c-format
+msgid "%1$s megabytes of storage per year of service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:244
+#, c-format
+msgid "Accept terms of service"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:44
+#, c-format
+msgid "Could not parse the payto URI"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:45
+#, c-format
+msgid "Please check the uri"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:75
+#, c-format
+msgid "Exchange is ready for withdrawal"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:78
+#, c-format
+msgid "To complete the process you need to wire%1$s %2$s to the exchange bank account"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:87
+#, c-format
+msgid ""
+"Alternative, you can also scan this QR code or open %1$s if you have a banking "
+"app installed that supports RFC 8905"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:98
+#, c-format
+msgid "Cancel withdrawal"
+msgstr ""
+
+#: src/wallet/Settings.tsx:115
+#, c-format
+msgid "Could not toggle auto-open"
+msgstr ""
+
+#: src/wallet/Settings.tsx:121
+#, c-format
+msgid "Could not toggle clipboard"
+msgstr ""
+
+#: src/wallet/Settings.tsx:126
+#, c-format
+msgid "Navigator"
+msgstr ""
+
+#: src/wallet/Settings.tsx:129
+#, c-format
+msgid "Automatically open wallet based on page content"
+msgstr ""
+
+#: src/wallet/Settings.tsx:135
+#, c-format
+msgid ""
+"Enabling this option below will make using the wallet faster, but requires more "
+"permissions from your browser."
+msgstr ""
+
+#: src/wallet/Settings.tsx:145
+#, c-format
+msgid "Automatically check clipboard for Taler URI"
+msgstr ""
+
+#: src/wallet/Settings.tsx:162
+#, c-format
+msgid "Trust"
+msgstr ""
+
+#: src/wallet/Settings.tsx:166
+#, c-format
+msgid "No exchange yet"
+msgstr ""
+
+#: src/wallet/Settings.tsx:180
+#, c-format
+msgid "Term of Service"
+msgstr ""
+
+#: src/wallet/Settings.tsx:191
+#, c-format
+msgid "ok"
+msgstr ""
+
+#: src/wallet/Settings.tsx:197
+#, c-format
+msgid "changed"
+msgstr ""
+
+#: src/wallet/Settings.tsx:204
+#, c-format
+msgid "not accepted"
+msgstr ""
+
+#: src/wallet/Settings.tsx:210
+#, c-format
+msgid "unknown (exchange status should be updated)"
+msgstr ""
+
+#: src/wallet/Settings.tsx:236
+#, c-format
+msgid "Add an exchange"
+msgstr ""
+
+#: src/wallet/Settings.tsx:241
+#, c-format
+msgid "Troubleshooting"
+msgstr ""
+
+#: src/wallet/Settings.tsx:244
+#, c-format
+msgid "Developer mode"
+msgstr ""
+
+#: src/wallet/Settings.tsx:246
+#, c-format
+msgid "More options and information useful for debugging"
+msgstr ""
+
+#: src/wallet/Settings.tsx:257
+#, c-format
+msgid "Display"
+msgstr ""
+
+#: src/wallet/Settings.tsx:261
+#, c-format
+msgid "Current Language"
+msgstr ""
+
+#: src/wallet/Settings.tsx:274
+#, c-format
+msgid "Wallet Core"
+msgstr ""
+
+#: src/wallet/Settings.tsx:284
+#, c-format
+msgid "Web Extension"
+msgstr ""
+
+#: src/wallet/Settings.tsx:295
+#, c-format
+msgid "Exchange compatibility"
+msgstr ""
+
+#: src/wallet/Settings.tsx:299
+#, c-format
+msgid "Merchant compatibility"
+msgstr ""
+
+#: src/wallet/Settings.tsx:303
+#, c-format
+msgid "Bank compatibility"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:59
+#, c-format
+msgid "Browser Extension Installed!"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:63
+#, c-format
+msgid "You can open the GNU Taler Wallet using the combination %1$s ."
+msgstr ""
+
+#: src/wallet/Welcome.tsx:72
+#, c-format
+msgid ""
+"Also pinning the GNU Taler Wallet to your Chrome browser allows you to quick "
+"access without keyboard:"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:79
+#, c-format
+msgid "Click the puzzle icon"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:82
+#, c-format
+msgid "Search for GNU Taler Wallet"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:85
+#, c-format
+msgid "Click the pin icon"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:91
+#, c-format
+msgid "Permissions"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:100
+#, c-format
+msgid ""
+"(Enabling this option below will make using the wallet faster, but requires more "
+"permissions from your browser.)"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:110
+#, c-format
+msgid "Next Steps"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:113
+#, c-format
+msgid "Try the demo"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:116
+#, c-format
+msgid "Learn how to top up your wallet balance"
+msgstr ""
+
+#: src/components/Diagnostics.tsx:31
+#, c-format
+msgid "Diagnostics timed out. Could not talk to the wallet backend."
+msgstr ""
+
+#: src/components/Diagnostics.tsx:52
+#, c-format
+msgid "Problems detected:"
+msgstr ""
+
+#: src/components/Diagnostics.tsx:61
+#, c-format
+msgid ""
+"Please check in your %1$s settings that you have IndexedDB enabled (check the "
+"preference name %2$s)."
+msgstr ""
+
+#: src/components/Diagnostics.tsx:70
+#, c-format
+msgid ""
+"Your wallet database is outdated. Currently automatic migration is not "
+"supported. Please go %1$s to reset the wallet database."
+msgstr ""
+
+#: src/components/Diagnostics.tsx:83
+#, c-format
+msgid "Running diagnostics"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:163
+#, c-format
+msgid "Debug tools"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:170
+#, c-format
+msgid ""
+"Do you want to IRREVOCABLY DESTROY everything inside your wallet and LOSE ALL "
+"YOUR COINS?"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:176
+#, c-format
+msgid "reset"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:183
+#, c-format
+msgid "TESTING: This may delete all your coin, proceed with caution"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:189
+#, c-format
+msgid "run gc"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:197
+#, c-format
+msgid "import database"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:219
+#, c-format
+msgid "export database"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:225
+#, c-format
+msgid "Database exported at %1$s %2$s to download"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:248
+#, c-format
+msgid "Coins"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:282
+#, c-format
+msgid "Pending operations"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:328
+#, c-format
+msgid "usable coins"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:337
+#, c-format
+msgid "id"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:340
+#, c-format
+msgid "denom"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:343
+#, c-format
+msgid "value"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:346
+#, c-format
+msgid "status"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:349
+#, c-format
+msgid "from refresh?"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:352
+#, c-format
+msgid "age key count"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:369
+#, c-format
+msgid "spent coins"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:373
+#, c-format
+msgid "click to show"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:108
+#, c-format
+msgid "Scan a QR code or enter taler:// URI below"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:122
+#, c-format
+msgid "Open"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:128
+#, c-format
+msgid "URI is not valid. Taler URI should start with `taler://`"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:133
+#, c-format
+msgid "Try another"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:183
+#, c-format
+msgid "Could not load list of exchange"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:209
+#, c-format
+msgid "Choose a currency to proceed or add another exchange"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:217
+#, c-format
+msgid "Known currencies"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:318
+#, c-format
+msgid "Specify the amount and the origin"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:336
+#, c-format
+msgid "Change currency"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:344
+#, c-format
+msgid "Use previous origins:"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:364
+#, c-format
+msgid "Or specify the origin of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:372
+#, c-format
+msgid "Specify the origin of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:380
+#, c-format
+msgid "From my bank account"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:395
+#, c-format
+msgid "From another wallet"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:449
+#, c-format
+msgid "currency not provided"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:459
+#, c-format
+msgid "Specify the amount and the destination"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:483
+#, c-format
+msgid "Use previous destinations:"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:503
+#, c-format
+msgid "Or specify the destination of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:511
+#, c-format
+msgid "Specify the destination of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:521
+#, c-format
+msgid "To my bank account"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:534
+#, c-format
+msgid "To another wallet"
+msgstr ""
+
+#: src/cta/Recovery/views.tsx:30
+#, c-format
+msgid "Could not load backup recovery information"
+msgstr ""
+
+#: src/cta/Recovery/views.tsx:47
+#, c-format
+msgid "Digital wallet recovery"
+msgstr ""
+
+#: src/cta/Recovery/views.tsx:52
+#, c-format
+msgid "Import backup, show info"
+msgstr ""
+
+#: src/wallet/Application.tsx:189
+#, c-format
+msgid "All done, your transaction is in progress"
+msgstr ""
+
+#: src/components/EditableText.tsx:45
+#, c-format
+msgid "Edit"
+msgstr ""
+
+#: src/wallet/ManualWithdrawPage.tsx:102
+#, c-format
+msgid "Could not load the list of known exchanges"
+msgstr ""
diff --git a/packages/taler-wallet-webextension/src/i18n/sv.po b/packages/taler-wallet-webextension/src/i18n/sv.po
index 6cef9ffe7..bb3caea4b 100644
--- a/packages/taler-wallet-webextension/src/i18n/sv.po
+++ b/packages/taler-wallet-webextension/src/i18n/sv.po
@@ -15,18 +15,18 @@
msgid ""
msgstr ""
"Project-Id-Version: Taler Wallet\n"
-"Report-Msgid-Bugs-To: \n"
+"Report-Msgid-Bugs-To: languages@taler.net\n"
"POT-Creation-Date: 2016-11-23 00:00+0100\n"
-"PO-Revision-Date: 2022-02-18 19:33+0000\n"
-"Last-Translator: Stefan <eintritt@hotmail.com>\n"
-"Language-Team: Swedish <http://weblate.taler.net/projects/gnu-taler/"
+"PO-Revision-Date: 2023-03-06 22:06+0000\n"
+"Last-Translator: Anonymous <noreply@weblate.org>\n"
+"Language-Team: Swedish <https://weblate.taler.net/projects/gnu-taler/"
"webextensions/sv/>\n"
"Language: sv\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
-"X-Generator: Weblate 4.4.2\n"
+"X-Generator: Weblate 4.13.1\n"
#: src/NavigationBar.tsx:139
#, c-format
diff --git a/packages/taler-wallet-webextension/src/i18n/taler-wallet-webextension.pot b/packages/taler-wallet-webextension/src/i18n/taler-wallet-webextension.pot
deleted file mode 100644
index 4fd500d25..000000000
--- a/packages/taler-wallet-webextension/src/i18n/taler-wallet-webextension.pot
+++ /dev/null
@@ -1,366 +0,0 @@
-# SOME DESCRIPTIVE TITLE.
-# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
-# This file is distributed under the same license as the PACKAGE package.
-# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
-#
-#, fuzzy
-msgid ""
-msgstr ""
-"Project-Id-Version: PACKAGE VERSION\n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-11-23 00:00+0100\n"
-"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
-"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
-"Language-Team: LANGUAGE <LL@li.org>\n"
-"Language: \n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/NavigationBar.tsx:86
-#, c-format
-msgid "Balance"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/NavigationBar.tsx:87
-#, c-format
-msgid "Pending"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/NavigationBar.tsx:88
-#, c-format
-msgid "Backup"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/NavigationBar.tsx:89
-#, c-format
-msgid "Settings"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/NavigationBar.tsx:90
-#, c-format
-msgid "Dev"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx:127
-#, c-format
-msgid "Add provider"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx:137
-#, c-format
-msgid "Sync all backups"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx:139
-#, c-format
-msgid "Sync now"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/popup/BalancePage.tsx:79
-#, c-format
-msgid "You have no balance to show. Need some %1$s getting started?"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/ProviderAddPage.tsx:145
-#, c-format
-msgid "&lt; Back"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/ProviderAddPage.tsx:156
-#, c-format
-msgid "Next"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/ProviderAddPage.tsx:210
-#, c-format
-msgid "&lt; Back"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/ProviderAddPage.tsx:213
-#, c-format
-msgid "Add provider"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx:57
-#, c-format
-msgid "Loading..."
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx:64
-#, c-format
-msgid "There was an error loading the provider detail for "%1$s""
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx:75
-#, c-format
-msgid "There is not known provider with url "%1$s". Redirecting back..."
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx:131
-#, c-format
-msgid "Back up"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx:142
-#, c-format
-msgid "Extend"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx:148
-#, c-format
-msgid ""
-"terms has changed, extending the service will imply accepting the new terms of "
-"service"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx:158
-#, c-format
-msgid "old"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx:162
-#, c-format
-msgid "new"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx:169
-#, c-format
-msgid "fee"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx:177
-#, c-format
-msgid "storage"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx:190
-#, c-format
-msgid "&lt; back"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx:194
-#, c-format
-msgid "remove provider"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx:213
-#, c-format
-msgid "There is conflict with another backup from %1$s"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx:228
-#, c-format
-msgid "Unknown backup problem: %1$s"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx:247
-#, c-format
-msgid "service paid"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/popup/Settings.tsx:46
-#, c-format
-msgid "Permissions"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/cta/TermsOfServiceSection.tsx:37
-#, c-format
-msgid "Exchange doesn't have terms of service"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/cta/TermsOfServiceSection.tsx:49
-#, c-format
-msgid "Exchange doesn't have terms of service"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/cta/TermsOfServiceSection.tsx:56
-#, c-format
-msgid "Review exchange terms of service"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/cta/TermsOfServiceSection.tsx:63
-#, c-format
-msgid "Review new version of terms of service"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/cta/TermsOfServiceSection.tsx:75
-#, c-format
-msgid "Show terms of service"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/cta/TermsOfServiceSection.tsx:83
-#, c-format
-msgid "I accept the exchange terms of service"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/cta/TermsOfServiceSection.tsx:127
-#, c-format
-msgid "Hide terms of service"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/cta/TermsOfServiceSection.tsx:136
-#, c-format
-msgid "I accept the exchange terms of service"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/ExchangeAddConfirm.tsx:110
-#, c-format
-msgid "Cancel"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/ExchangeAddConfirm.tsx:114
-#, c-format
-msgid "Loading terms.."
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/ExchangeAddConfirm.tsx:121
-#, c-format
-msgid "Add exchange"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/ExchangeAddConfirm.tsx:126
-#, c-format
-msgid "Add exchange"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/ExchangeAddConfirm.tsx:131
-#, c-format
-msgid "Add exchange anyway"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/ExchangeSetUrl.tsx:133
-#, c-format
-msgid "Cancel"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/ExchangeSetUrl.tsx:149
-#, c-format
-msgid "Next"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/BalancePage.tsx:83
-#, c-format
-msgid "You have no balance to show. Need some %1$s getting started?"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx:104
-#, c-format
-msgid "Add exchange"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx:144
-#, c-format
-msgid "Add exchange"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/Settings.tsx:84
-#, c-format
-msgid "Permissions"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/Settings.tsx:95
-#, c-format
-msgid "Known exchanges"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/Transaction.tsx:154
-#, c-format
-msgid "&lt; Back"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/Transaction.tsx:159
-#, c-format
-msgid "retry"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/Transaction.tsx:163
-#, c-format
-msgid "Forget"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/Transaction.tsx:194
-#, c-format
-msgid "Cancel"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/Transaction.tsx:198
-#, c-format
-msgid "Confirm"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/cta/Pay.tsx:211
-#, c-format
-msgid "Pay with a mobile phone"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/cta/Pay.tsx:211
-#, c-format
-msgid "Hide QR"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/cta/Pay.tsx:241
-#, c-format
-msgid "Pay"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/cta/Pay.tsx:265
-#, c-format
-msgid "Withdraw digital cash"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/cta/Pay.tsx:295
-#, c-format
-msgid "Digital cash payment"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/cta/Withdraw.tsx:101
-#, c-format
-msgid "Digital cash withdrawal"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/cta/Withdraw.tsx:149
-#, c-format
-msgid "Cancel exchange selection"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/cta/Withdraw.tsx:150
-#, c-format
-msgid "Confirm exchange selection"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/cta/Withdraw.tsx:155
-#, c-format
-msgid "Switch exchange"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/cta/Withdraw.tsx:174
-#, c-format
-msgid "Confirm withdrawal"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/cta/Withdraw.tsx:183
-#, c-format
-msgid "Withdraw anyway"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/cta/Withdraw.tsx:310
-#, c-format
-msgid "missing withdraw uri"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/cta/Deposit.tsx:119
-#, c-format
-msgid "Digital cash payment"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/cta/Deposit.tsx:133
-#, c-format
-msgid "Digital cash payment"
-msgstr ""
-
-#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/cta/Deposit.tsx:186
-#, c-format
-msgid "Digital cash deposit"
-msgstr ""
-
diff --git a/packages/taler-wallet-webextension/src/i18n/tr.po b/packages/taler-wallet-webextension/src/i18n/tr.po
index e98ff61cb..5848b9f3a 100644
--- a/packages/taler-wallet-webextension/src/i18n/tr.po
+++ b/packages/taler-wallet-webextension/src/i18n/tr.po
@@ -15,18 +15,18 @@
msgid ""
msgstr ""
"Project-Id-Version: Taler Wallet\n"
-"Report-Msgid-Bugs-To: \n"
+"Report-Msgid-Bugs-To: languages@taler.net\n"
"POT-Creation-Date: 2016-11-23 00:00+0100\n"
-"PO-Revision-Date: 2022-04-13 16:06+0000\n"
+"PO-Revision-Date: 2024-03-08 01:14+0000\n"
"Last-Translator: Alp <berna.alp@digitalekho.com>\n"
-"Language-Team: Turkish <http://weblate.taler.net/projects/gnu-taler/"
+"Language-Team: Turkish <https://weblate.taler.net/projects/gnu-taler/"
"webextensions/tr/>\n"
"Language: tr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
-"X-Generator: Weblate 4.4.2\n"
+"X-Generator: Weblate 5.2.1\n"
#: src/NavigationBar.tsx:139
#, c-format
@@ -198,22 +198,22 @@ msgstr ""
#: src/wallet/ProviderDetailPage.tsx:261
#, c-format
msgid "Unknown backup problem: %1$s"
-msgstr ""
+msgstr "Bilinmeyen yedekleme problemi: %1$s"
#: src/wallet/ProviderDetailPage.tsx:283
#, c-format
msgid "service paid"
-msgstr ""
+msgstr "hizmet ödendi"
#: src/wallet/ProviderDetailPage.tsx:290
#, c-format
msgid "Backup valid until"
-msgstr ""
+msgstr "Yedekleme geçerlilik süresi"
#: src/wallet/AddNewActionView.tsx:57
-#, fuzzy, c-format
+#, c-format
msgid "Cancel"
-msgstr "Bakiye"
+msgstr "İptal et"
#: src/wallet/AddNewActionView.tsx:68
#, c-format
@@ -243,7 +243,7 @@ msgstr "Para çekme sayfasını açın"
#: src/popup/NoBalanceHelp.tsx:43
#, c-format
msgid "Get digital cash"
-msgstr ""
+msgstr "Dijital para alın"
#: src/popup/BalancePage.tsx:138
#, c-format
@@ -253,12 +253,12 @@ msgstr "Bakiye sayfası yüklenemedi"
#: src/popup/BalancePage.tsx:175
#, c-format
msgid "Add"
-msgstr ""
+msgstr "Ekle"
#: src/popup/BalancePage.tsx:179
-#, fuzzy, c-format
+#, c-format
msgid "Send %1$s"
-msgstr "%1$s seçin"
+msgstr "%1$s gönder"
#: src/popup/TalerActionFound.tsx:44
#, c-format
@@ -353,12 +353,12 @@ msgstr ""
#: src/components/ShowFullContractTermPopup.tsx:234
#, c-format
msgid "Merchant website"
-msgstr ""
+msgstr "Satıcı web sitesi"
#: src/components/ShowFullContractTermPopup.tsx:240
#, c-format
msgid "Merchant email"
-msgstr ""
+msgstr "Satıcı e-postası"
#: src/components/ShowFullContractTermPopup.tsx:246
#, c-format
@@ -1863,7 +1863,7 @@ msgstr ""
#: src/wallet/QrReader.tsx:122
#, c-format
msgid "Open"
-msgstr ""
+msgstr "Açık"
#: src/wallet/QrReader.tsx:128
#, c-format
diff --git a/packages/taler-wallet-webextension/src/i18n/uk.po b/packages/taler-wallet-webextension/src/i18n/uk.po
new file mode 100644
index 000000000..c4f5d6537
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/i18n/uk.po
@@ -0,0 +1,1956 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: languages@taler.net\n"
+"POT-Creation-Date: 2016-11-23 00:00+0100\n"
+"PO-Revision-Date: 2024-03-05 13:03+0000\n"
+"Last-Translator: Tim Vutor <flukes.ostrich0p@icloud.com>\n"
+"Language-Team: Ukrainian <https://weblate.taler.net/projects/gnu-taler/"
+"webextensions/uk/>\n"
+"Language: uk\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
+"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
+"X-Generator: Weblate 5.2.1\n"
+
+#: src/NavigationBar.tsx:139
+#, c-format
+msgid "Balance"
+msgstr "Баланс"
+
+#: src/NavigationBar.tsx:142
+#, c-format
+msgid "Backup"
+msgstr "Бекап"
+
+#: src/NavigationBar.tsx:147
+#, c-format
+msgid "QR Reader and Taler URI"
+msgstr "QR-читалка та Taler URI"
+
+#: src/NavigationBar.tsx:154
+#, c-format
+msgid "Settings"
+msgstr "Налаштування"
+
+#: src/NavigationBar.tsx:184
+#, c-format
+msgid "Dev"
+msgstr "Розробка"
+
+#: src/mui/Typography.tsx:122
+#, c-format, fuzzy
+msgid "%1$s"
+msgstr "%1$s"
+
+#: src/components/PendingTransactions.tsx:74
+#, c-format
+msgid "PENDING OPERATIONS"
+msgstr "НЕЗАВЕРШЕНІ ОПЕРАЦІЇ"
+
+#: src/components/Loading.tsx:36
+#, c-format
+msgid "Loading"
+msgstr "Завантаження"
+
+#: src/wallet/BackupPage.tsx:123
+#, c-format
+msgid "Could not load backup providers"
+msgstr "Не вдалося завантажити зберігачів резервних копій"
+
+#: src/wallet/BackupPage.tsx:202
+#, c-format
+msgid "No backup providers configured"
+msgstr "Не налаштовано жодного зберігача резервних копій"
+
+#: src/wallet/BackupPage.tsx:205
+#, c-format
+msgid "Add provider"
+msgstr "Додати зберігача"
+
+#: src/wallet/BackupPage.tsx:219
+#, c-format
+msgid "Sync all backups"
+msgstr "Синхронізувати всі резервні копії"
+
+#: src/wallet/BackupPage.tsx:221
+#, c-format
+msgid "Sync now"
+msgstr "Синхронізувати зараз"
+
+#: src/wallet/BackupPage.tsx:264
+#, c-format
+msgid "Last synced"
+msgstr "Останній раз синхронізовано"
+
+#: src/wallet/BackupPage.tsx:269
+#, c-format
+msgid "Not synced"
+msgstr "Не синхронізовано"
+
+#: src/wallet/BackupPage.tsx:289
+#, c-format
+msgid "Expires in"
+msgstr "Термін дії закінчується в"
+
+#: src/wallet/ProviderDetailPage.tsx:60
+#, c-format
+msgid "There was an error loading the provider detail for &quot; %1$s&quot;"
+msgstr "Виникла помилка при завантаженні інформації зберігача &quot; %1$s&quot;"
+
+#: src/wallet/ProviderDetailPage.tsx:108
+#, c-format
+msgid "There is not known provider with url &quot;%1$s&quot;."
+msgstr "Зберігач з посиланням &quot;%1$s&quot; невідомий."
+
+#: src/wallet/ProviderDetailPage.tsx:115
+#, c-format
+msgid "See providers"
+msgstr "Подивитись зберігачів"
+
+#: src/wallet/ProviderDetailPage.tsx:143
+#, c-format
+msgid "Last backup"
+msgstr "Остання резервна копія"
+
+#: src/wallet/ProviderDetailPage.tsx:148
+#, c-format
+msgid "Back up"
+msgstr "Зробити резервну копію"
+
+#: src/wallet/ProviderDetailPage.tsx:154
+#, c-format
+msgid "Provider fee"
+msgstr "Комісія зберігача"
+
+#: src/wallet/ProviderDetailPage.tsx:157
+#, c-format
+msgid "per year"
+msgstr "на рік"
+
+#: src/wallet/ProviderDetailPage.tsx:163
+#, c-format
+msgid "Extend"
+msgstr "Подовжити"
+
+#: src/wallet/ProviderDetailPage.tsx:169
+#, c-format
+msgid ""
+"terms has changed, extending the service will imply accepting the new terms of "
+"service"
+msgstr ""
+"умови надання послуг змінились, продовження послуги означатиме прийняття "
+"нових умов"
+
+#: src/wallet/ProviderDetailPage.tsx:179
+#, c-format
+msgid "old"
+msgstr "старий"
+
+#: src/wallet/ProviderDetailPage.tsx:183
+#, c-format
+msgid "new"
+msgstr "новий"
+
+#: src/wallet/ProviderDetailPage.tsx:190
+#, c-format
+msgid "fee"
+msgstr "комісія"
+
+#: src/wallet/ProviderDetailPage.tsx:198
+#, c-format
+msgid "storage"
+msgstr "сховище"
+
+#: src/wallet/ProviderDetailPage.tsx:215
+#, c-format
+msgid "Remove provider"
+msgstr "Видалити зберігача"
+
+#: src/wallet/ProviderDetailPage.tsx:228
+#, c-format
+msgid "This provider has reported an error"
+msgstr "Цей постачальник повідомив про помилку"
+
+#: src/wallet/ProviderDetailPage.tsx:242
+#, c-format
+msgid "There is conflict with another backup from %1$s"
+msgstr "Конфлікт з іншою резервною копією з %1$s"
+
+#: src/wallet/ProviderDetailPage.tsx:253
+#, c-format
+msgid "Backup is not readable"
+msgstr "Резервна копія пошкоджена або не може бути прочитана"
+
+#: src/wallet/ProviderDetailPage.tsx:261
+#, c-format
+msgid "Unknown backup problem: %1$s"
+msgstr "Невідома помилка резервного копіювання: %1$s"
+
+#: src/wallet/ProviderDetailPage.tsx:283
+#, c-format
+msgid "service paid"
+msgstr "послуга сплачена"
+
+#: src/wallet/ProviderDetailPage.tsx:290
+#, c-format
+msgid "Backup valid until"
+msgstr "Резервна копія дійсна до"
+
+#: src/wallet/AddNewActionView.tsx:57
+#, c-format
+msgid "Cancel"
+msgstr "Відмінити"
+
+#: src/wallet/AddNewActionView.tsx:68
+#, c-format
+msgid "Open reserve page"
+msgstr "Показати резерв"
+
+#: src/wallet/AddNewActionView.tsx:70
+#, c-format
+msgid "Open pay page"
+msgstr "Показати сторінку оплати"
+
+#: src/wallet/AddNewActionView.tsx:72
+#, c-format
+msgid "Open refund page"
+msgstr "Показати відшкодування"
+
+#: src/wallet/AddNewActionView.tsx:74
+#, c-format
+msgid "Open tip page"
+msgstr "Показати чайові"
+
+#: src/wallet/AddNewActionView.tsx:76
+#, c-format
+msgid "Open withdraw page"
+msgstr "Показати списання"
+
+#: src/popup/NoBalanceHelp.tsx:43
+#, c-format
+msgid "Get digital cash"
+msgstr "Отримати е-готівку"
+
+#: src/popup/BalancePage.tsx:138
+#, c-format
+msgid "Could not load balance page"
+msgstr "Не вдалося показати залишок"
+
+#: src/popup/BalancePage.tsx:175
+#, c-format
+msgid "Add"
+msgstr "Додати"
+
+#: src/popup/BalancePage.tsx:179
+#, c-format
+msgid "Send %1$s"
+msgstr "Переказати %1$s"
+
+#: src/popup/TalerActionFound.tsx:44
+#, c-format
+msgid "Taler Action"
+msgstr "Taler Дія"
+
+#: src/popup/TalerActionFound.tsx:49
+#, c-format
+msgid "This page has pay action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:63
+#, c-format
+msgid "This page has a withdrawal action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:79
+#, c-format
+msgid "This page has a tip action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:93
+#, c-format
+msgid "This page has a notify reserve action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:102
+#, c-format
+msgid "Notify"
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:109
+#, c-format
+msgid "This page has a refund action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:123
+#, c-format
+msgid "This page has a malformed taler uri."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:134
+#, c-format
+msgid "Dismiss"
+msgstr ""
+
+#: src/popup/Application.tsx:177
+#, c-format
+msgid "this popup is being closed and you are being redirected to %1$s"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:158
+#, c-format
+msgid "Could not load purchase proposal details"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:183
+#, c-format
+msgid "Order Id"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:189
+#, c-format
+msgid "Summary"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:195
+#, c-format
+msgid "Amount"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:203
+#, c-format
+msgid "Merchant name"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:209
+#, c-format
+msgid "Merchant jurisdiction"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:215
+#, c-format
+msgid "Merchant address"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:221
+#, c-format
+msgid "Merchant logo"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:234
+#, c-format
+msgid "Merchant website"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:240
+#, c-format
+msgid "Merchant email"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:246
+#, c-format
+msgid "Merchant public key"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:256
+#, c-format
+msgid "Delivery date"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:271
+#, c-format
+msgid "Delivery location"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:277
+#, c-format
+msgid "Products"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:289
+#, c-format
+msgid "Created at"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:304
+#, c-format
+msgid "Refund deadline"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:319
+#, c-format
+msgid "Auto refund"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:339
+#, c-format
+msgid "Pay deadline"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:354
+#, c-format
+msgid "Fulfillment URL"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:360
+#, c-format
+msgid "Fulfillment message"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:370
+#, c-format
+msgid "Max deposit fee"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:378
+#, c-format
+msgid "Max fee"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:386
+#, c-format
+msgid "Minimum age"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:398
+#, c-format
+msgid "Wire fee amortization"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:404
+#, c-format
+msgid "Auditors"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:419
+#, c-format
+msgid "Exchanges"
+msgstr ""
+
+#: src/components/Part.tsx:148
+#, c-format
+msgid "Bank account"
+msgstr ""
+
+#: src/components/Part.tsx:160
+#, c-format
+msgid "Bitcoin address"
+msgstr ""
+
+#: src/components/Part.tsx:163
+#, c-format
+msgid "IBAN"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:38
+#, c-format
+msgid "Could not load deposit status"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:52
+#, c-format
+msgid "Digital cash deposit"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:58
+#, c-format
+msgid "Cost"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:66
+#, c-format
+msgid "Fee"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:73
+#, c-format
+msgid "To be received"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:84
+#, c-format
+msgid "Send &nbsp; %1$s"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:63
+#, c-format
+msgid "Bitcoin transfer details"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:66
+#, c-format
+msgid ""
+"The exchange need a transaction with 3 output, one output is the exchange "
+"account and the other two are segwit fake address for metadata with an minimum "
+"amount."
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:74
+#, c-format
+msgid ""
+"In bitcoincore wallet use &apos;Add Recipient&apos; button to add two additional "
+"recipient and copy addresses and amounts"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:98
+#, c-format
+msgid "Make sure the amount show %1$s BTC, else you have to change the base unit to BTC"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:110
+#, c-format
+msgid "Account"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:116
+#, c-format
+msgid "Bank host"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:139
+#, c-format
+msgid "Bank transfer details"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:148
+#, c-format
+msgid "Subject"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:154
+#, c-format
+msgid "Receiver name"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:98
+#, c-format
+msgid "Could not load the transaction information"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:191
+#, c-format
+msgid "There was an error trying to complete the transaction"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:200
+#, c-format
+msgid "This transaction is not completed"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:209
+#, c-format
+msgid "Send"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:216
+#, c-format
+msgid "Retry"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:224
+#, c-format
+msgid "Forget"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:241
+#, c-format
+msgid "Caution!"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:244
+#, c-format
+msgid ""
+"If you have already wired money to the exchange you will loose the chance to get "
+"the coins form it."
+msgstr ""
+
+#: src/wallet/Transaction.tsx:259
+#, c-format
+msgid "Confirm"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:267
+#, c-format
+msgid "Withdrawal"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:286
+#, c-format
+msgid ""
+"Make sure to use the correct subject, otherwise the money will not arrive in "
+"this wallet."
+msgstr ""
+
+#: src/wallet/Transaction.tsx:298
+#, c-format
+msgid ""
+"The bank did not yet confirmed the wire transfer. Go to the %1$s %2$s and check "
+"there is no pending step."
+msgstr ""
+
+#: src/wallet/Transaction.tsx:316
+#, c-format
+msgid "Bank has confirmed the wire transfer. Waiting for the exchange to send the coins"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:325
+#, c-format
+msgid "Details"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:360
+#, c-format
+msgid "Payment"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:378
+#, c-format
+msgid "Refunds"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:385
+#, c-format
+msgid "%1$s %2$s on %3$s"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:415
+#, c-format
+msgid "Merchant created a refund for this order but was not automatically picked up."
+msgstr ""
+
+#: src/wallet/Transaction.tsx:420
+#, c-format
+msgid "Offer"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:431
+#, c-format
+msgid "Accept"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:438
+#, c-format
+msgid "Merchant"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:443
+#, c-format
+msgid "Invoice ID"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:470
+#, c-format
+msgid "Deposit"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:496
+#, c-format
+msgid "Refresh"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:517
+#, c-format
+msgid "Tip"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:542
+#, c-format
+msgid "Refund"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:555
+#, c-format
+msgid "Original order ID"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:568
+#, c-format
+msgid "Purchase summary"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:593
+#, c-format
+msgid "copy"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:596
+#, c-format
+msgid "hide qr"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:608
+#, c-format
+msgid "show qr"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:620
+#, c-format
+msgid "Credit"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:624
+#, c-format
+msgid "Invoice"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:635
+#, c-format
+msgid "Exchange"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:641
+#, c-format
+msgid "URI"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:667
+#, c-format
+msgid "Debit"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:710
+#, c-format
+msgid "Transfer"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:844
+#, c-format
+msgid "Country"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:852
+#, c-format
+msgid "Address lines"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:860
+#, c-format
+msgid "Building number"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:868
+#, c-format
+msgid "Building name"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:876
+#, c-format
+msgid "Street"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:884
+#, c-format
+msgid "Post code"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:892
+#, c-format
+msgid "Town location"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:900
+#, c-format
+msgid "Town"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:908
+#, c-format
+msgid "District"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:916
+#, c-format
+msgid "Country subdivision"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:935
+#, c-format
+msgid "Date"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:990
+#, c-format
+msgid "Transaction fees"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1004
+#, c-format
+msgid "Total"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1074
+#, c-format
+msgid "Withdraw"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1146
+#, c-format
+msgid "Price"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1156
+#, c-format
+msgid "Refunded"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1220
+#, c-format
+msgid "Delivery"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1335
+#, c-format
+msgid "Total transfer"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:57
+#, c-format
+msgid "Could not load pay status"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:87
+#, c-format
+msgid "Digital cash payment"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:119
+#, c-format
+msgid "Purchase"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:149
+#, c-format
+msgid "Receipt"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:156
+#, c-format
+msgid "Valid until"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:191
+#, c-format
+msgid "List of products"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:242
+#, c-format
+msgid "free"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:263
+#, c-format
+msgid "Already paid, you are going to be redirected to %1$s"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:274
+#, c-format
+msgid "Already paid"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:280
+#, c-format
+msgid "Already claimed"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:296
+#, c-format
+msgid "Pay with a mobile phone"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:298
+#, c-format
+msgid "Hide QR"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:305
+#, c-format
+msgid "Scan the QR code or &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:346
+#, c-format
+msgid "Pay &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:360
+#, c-format
+msgid "You have no balance for this currency. Withdraw digital cash first."
+msgstr ""
+
+#: src/cta/Payment/views.tsx:364
+#, c-format
+msgid ""
+"Could not find enough coins to pay. Even if you have enough %1$s some "
+"restriction may apply."
+msgstr ""
+
+#: src/cta/Payment/views.tsx:366
+#, c-format
+msgid "Your current balance is not enough."
+msgstr ""
+
+#: src/cta/Payment/views.tsx:395
+#, c-format
+msgid "Merchant message"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:34
+#, c-format
+msgid "Could not load refund status"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:48
+#, c-format
+msgid "Digital cash refund"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:52
+#, c-format
+msgid "You&apos;ve ignored the tip."
+msgstr ""
+
+#: src/cta/Refund/views.tsx:70
+#, c-format
+msgid "The refund is in progress."
+msgstr ""
+
+#: src/cta/Refund/views.tsx:76
+#, c-format
+msgid "Total to refund"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:106
+#, c-format
+msgid "The merchant &quot;%1$s&quot; is offering you a refund."
+msgstr ""
+
+#: src/cta/Refund/views.tsx:115
+#, c-format
+msgid "Order amount"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:122
+#, c-format
+msgid "Already refunded"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:129
+#, c-format
+msgid "Refund offered"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:145
+#, c-format
+msgid "Accept &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:32
+#, c-format
+msgid "Could not load tip status"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:45
+#, c-format
+msgid "Digital cash tip"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:66
+#, c-format
+msgid "The merchant is offering you a tip"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:74
+#, c-format
+msgid "Merchant URL"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:90
+#, c-format
+msgid "Receive &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:114
+#, c-format
+msgid "Tip from %1$s accepted. Check your transactions list for more details."
+msgstr ""
+
+#: src/components/SelectList.tsx:66
+#, c-format
+msgid "Select one option"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:39
+#, c-format
+msgid "Could not load"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:73
+#, c-format
+msgid "Show terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:81
+#, c-format
+msgid "I accept the exchange terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:107
+#, c-format
+msgid "Exchange doesn&apos;t have terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:135
+#, c-format
+msgid "Review exchange terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:146
+#, c-format
+msgid "Review new version of terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:170
+#, c-format
+msgid "The exchange reply with a empty terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:193
+#, c-format
+msgid "Download Terms of Service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:204
+#, c-format
+msgid "Hide terms of service"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:117
+#, c-format
+msgid "Could not load exchange fees"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:131
+#, c-format
+msgid "Close"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:160
+#, c-format
+msgid "could not find any exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:166
+#, c-format
+msgid "could not find any exchange for the currency %1$s"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:186
+#, c-format
+msgid "Service fee description"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:201
+#, c-format
+msgid "Select %1$s exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:215
+#, c-format
+msgid "Reset"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:218
+#, c-format
+msgid "Use this exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:230
+#, c-format
+msgid "Doesn&apos;t have auditors"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:241
+#, c-format
+msgid "currency"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:249
+#, c-format
+msgid "Operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:252
+#, c-format
+msgid "Deposits"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:259
+#, c-format
+msgid "Denomination"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:265
+#, c-format
+msgid "Until"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:274
+#, c-format
+msgid "Withdrawals"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:423
+#, c-format
+msgid "Currency"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:433
+#, c-format
+msgid "Coin operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:436
+#, c-format
+msgid ""
+"Every operation in this section may be different by denomination value and is "
+"valid for a period of time. The exchange will charge the indicated amount every "
+"time a coin is used in such operation."
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:545
+#, c-format
+msgid "Transfer operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:548
+#, c-format
+msgid ""
+"Every operation in this section may be different by transfer type and is valid "
+"for a period of time. The exchange will charge the indicated amount every time a "
+"transfer is made."
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:563
+#, c-format
+msgid "Operation"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:583
+#, c-format
+msgid "Wallet operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:597
+#, c-format
+msgid "Feature"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:47
+#, c-format
+msgid "Could not get the info from the URI"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:60
+#, c-format
+msgid "Could not get info of withdrawal"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:74
+#, c-format
+msgid "Digital cash withdrawal"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:79
+#, c-format
+msgid "Could not finish the withdrawal operation"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:127
+#, c-format
+msgid "Age restriction"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:145
+#, c-format
+msgid "Withdraw &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:179
+#, c-format
+msgid "Withdraw to a mobile phone"
+msgstr ""
+
+#: src/cta/InvoiceCreate/views.tsx:65
+#, c-format
+msgid "Digital invoice"
+msgstr ""
+
+#: src/cta/InvoiceCreate/views.tsx:69
+#, c-format
+msgid "Could not finish the invoice creation"
+msgstr ""
+
+#: src/cta/InvoiceCreate/views.tsx:130
+#, c-format
+msgid "Create"
+msgstr ""
+
+#: src/cta/InvoicePay/views.tsx:63
+#, c-format
+msgid "Could not finish the payment operation"
+msgstr ""
+
+#: src/cta/TransferCreate/views.tsx:55
+#, c-format
+msgid "Digital cash transfer"
+msgstr ""
+
+#: src/cta/TransferCreate/views.tsx:59
+#, c-format
+msgid "Could not finish the transfer creation"
+msgstr ""
+
+#: src/cta/TransferPickup/views.tsx:57
+#, c-format
+msgid "Could not finish the pickup operation"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:149
+#, c-format
+msgid "Manual Withdrawal for %1$s"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:154
+#, c-format
+msgid ""
+"Choose a exchange from where the coins will be withdrawn. The exchange will send "
+"the coins to this wallet after receiving a wire transfer with the correct "
+"subject."
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:162
+#, c-format
+msgid "No exchange found for %1$s"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:170
+#, c-format
+msgid "Add Exchange"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:192
+#, c-format
+msgid "No exchange configured"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:210
+#, c-format
+msgid "Can&apos;t create the reserve"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:277
+#, c-format
+msgid "Start withdrawal"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:38
+#, c-format
+msgid "Could not load deposit balance"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:51
+#, c-format
+msgid "A currency or an amount should be indicated"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:67
+#, c-format
+msgid "There is no enough balance to make a deposit for currency %1$s"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:117
+#, c-format
+msgid "Send %1$s to your account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:121
+#, c-format
+msgid "There is no account to make a deposit for currency %1$s"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:127
+#, c-format
+msgid "Add account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:151
+#, c-format
+msgid "Select account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:163
+#, c-format
+msgid "Add another account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:191
+#, c-format
+msgid "Deposit fee"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:205
+#, c-format
+msgid "Total deposit"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:233
+#, c-format
+msgid "Deposit&nbsp;%1$s %2$s"
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:56
+#, c-format
+msgid "Add bank account for %1$s"
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:59
+#, c-format
+msgid "Enter the URL of an exchange you trust."
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:66
+#, c-format
+msgid "Unable add this account"
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:73
+#, c-format
+msgid "Select account type"
+msgstr ""
+
+#: src/wallet/ExchangeAddConfirm.tsx:42
+#, c-format
+msgid "Review terms of service"
+msgstr ""
+
+#: src/wallet/ExchangeAddConfirm.tsx:45
+#, c-format
+msgid "Exchange URL"
+msgstr ""
+
+#: src/wallet/ExchangeAddConfirm.tsx:70
+#, c-format
+msgid "Add exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:112
+#, c-format
+msgid "Add new exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:116
+#, c-format
+msgid "Add exchange for %1$s"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:128
+#, c-format
+msgid "An exchange has been found! Review the information and click next"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:135
+#, c-format
+msgid "This exchange doesn&apos;t match the expected currency %1$s"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:143
+#, c-format
+msgid "Unable to verify this exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:151
+#, c-format
+msgid "Unable to add this exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:167
+#, c-format
+msgid "loading"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:174
+#, c-format
+msgid "Version"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:206
+#, c-format
+msgid "Next"
+msgstr ""
+
+#: src/components/TransactionItem.tsx:201
+#, c-format
+msgid "Waiting for confirmation"
+msgstr ""
+
+#: src/components/TransactionItem.tsx:266
+#, c-format
+msgid "PENDING"
+msgstr ""
+
+#: src/wallet/History.tsx:75
+#, c-format
+msgid "Could not load the list of transactions"
+msgstr ""
+
+#: src/wallet/History.tsx:233
+#, c-format
+msgid "Your transaction history is empty for this currency."
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:127
+#, c-format
+msgid "Add backup provider"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:131
+#, c-format
+msgid "Could not get provider information"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:140
+#, c-format
+msgid "Backup providers may charge for their service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:147
+#, c-format
+msgid "URL"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:158
+#, c-format
+msgid "Name"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:212
+#, c-format
+msgid "Provider URL"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:218
+#, c-format
+msgid "Please review and accept this provider&apos;s terms of service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:223
+#, c-format
+msgid "Pricing"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:226
+#, c-format
+msgid "free of charge"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:228
+#, c-format
+msgid "%1$s per year of service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:235
+#, c-format
+msgid "Storage"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:238
+#, c-format
+msgid "%1$s megabytes of storage per year of service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:244
+#, c-format
+msgid "Accept terms of service"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:44
+#, c-format
+msgid "Could not parse the payto URI"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:45
+#, c-format
+msgid "Please check the uri"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:75
+#, c-format
+msgid "Exchange is ready for withdrawal"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:78
+#, c-format
+msgid "To complete the process you need to wire%1$s %2$s to the exchange bank account"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:87
+#, c-format
+msgid ""
+"Alternative, you can also scan this QR code or open %1$s if you have a banking "
+"app installed that supports RFC 8905"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:98
+#, c-format
+msgid "Cancel withdrawal"
+msgstr ""
+
+#: src/wallet/Settings.tsx:115
+#, c-format
+msgid "Could not toggle auto-open"
+msgstr ""
+
+#: src/wallet/Settings.tsx:121
+#, c-format
+msgid "Could not toggle clipboard"
+msgstr ""
+
+#: src/wallet/Settings.tsx:126
+#, c-format
+msgid "Navigator"
+msgstr ""
+
+#: src/wallet/Settings.tsx:129
+#, c-format
+msgid "Automatically open wallet based on page content"
+msgstr ""
+
+#: src/wallet/Settings.tsx:135
+#, c-format
+msgid ""
+"Enabling this option below will make using the wallet faster, but requires more "
+"permissions from your browser."
+msgstr ""
+
+#: src/wallet/Settings.tsx:145
+#, c-format
+msgid "Automatically check clipboard for Taler URI"
+msgstr ""
+
+#: src/wallet/Settings.tsx:162
+#, c-format
+msgid "Trust"
+msgstr ""
+
+#: src/wallet/Settings.tsx:166
+#, c-format
+msgid "No exchange yet"
+msgstr ""
+
+#: src/wallet/Settings.tsx:180
+#, c-format
+msgid "Term of Service"
+msgstr ""
+
+#: src/wallet/Settings.tsx:191
+#, c-format
+msgid "ok"
+msgstr ""
+
+#: src/wallet/Settings.tsx:197
+#, c-format
+msgid "changed"
+msgstr ""
+
+#: src/wallet/Settings.tsx:204
+#, c-format
+msgid "not accepted"
+msgstr ""
+
+#: src/wallet/Settings.tsx:210
+#, c-format
+msgid "unknown (exchange status should be updated)"
+msgstr ""
+
+#: src/wallet/Settings.tsx:236
+#, c-format
+msgid "Add an exchange"
+msgstr ""
+
+#: src/wallet/Settings.tsx:241
+#, c-format
+msgid "Troubleshooting"
+msgstr ""
+
+#: src/wallet/Settings.tsx:244
+#, c-format
+msgid "Developer mode"
+msgstr ""
+
+#: src/wallet/Settings.tsx:246
+#, c-format
+msgid "More options and information useful for debugging"
+msgstr ""
+
+#: src/wallet/Settings.tsx:257
+#, c-format
+msgid "Display"
+msgstr ""
+
+#: src/wallet/Settings.tsx:261
+#, c-format
+msgid "Current Language"
+msgstr ""
+
+#: src/wallet/Settings.tsx:274
+#, c-format
+msgid "Wallet Core"
+msgstr ""
+
+#: src/wallet/Settings.tsx:284
+#, c-format
+msgid "Web Extension"
+msgstr ""
+
+#: src/wallet/Settings.tsx:295
+#, c-format
+msgid "Exchange compatibility"
+msgstr ""
+
+#: src/wallet/Settings.tsx:299
+#, c-format
+msgid "Merchant compatibility"
+msgstr ""
+
+#: src/wallet/Settings.tsx:303
+#, c-format
+msgid "Bank compatibility"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:59
+#, c-format
+msgid "Browser Extension Installed!"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:63
+#, c-format
+msgid "You can open the GNU Taler Wallet using the combination %1$s ."
+msgstr ""
+
+#: src/wallet/Welcome.tsx:72
+#, c-format
+msgid ""
+"Also pinning the GNU Taler Wallet to your Chrome browser allows you to quick "
+"access without keyboard:"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:79
+#, c-format
+msgid "Click the puzzle icon"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:82
+#, c-format
+msgid "Search for GNU Taler Wallet"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:85
+#, c-format
+msgid "Click the pin icon"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:91
+#, c-format
+msgid "Permissions"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:100
+#, c-format
+msgid ""
+"(Enabling this option below will make using the wallet faster, but requires more "
+"permissions from your browser.)"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:110
+#, c-format
+msgid "Next Steps"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:113
+#, c-format
+msgid "Try the demo"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:116
+#, c-format
+msgid "Learn how to top up your wallet balance"
+msgstr ""
+
+#: src/components/Diagnostics.tsx:31
+#, c-format
+msgid "Diagnostics timed out. Could not talk to the wallet backend."
+msgstr ""
+
+#: src/components/Diagnostics.tsx:52
+#, c-format
+msgid "Problems detected:"
+msgstr ""
+
+#: src/components/Diagnostics.tsx:61
+#, c-format
+msgid ""
+"Please check in your %1$s settings that you have IndexedDB enabled (check the "
+"preference name %2$s)."
+msgstr ""
+
+#: src/components/Diagnostics.tsx:70
+#, c-format
+msgid ""
+"Your wallet database is outdated. Currently automatic migration is not "
+"supported. Please go %1$s to reset the wallet database."
+msgstr ""
+
+#: src/components/Diagnostics.tsx:83
+#, c-format
+msgid "Running diagnostics"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:163
+#, c-format
+msgid "Debug tools"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:170
+#, c-format
+msgid ""
+"Do you want to IRREVOCABLY DESTROY everything inside your wallet and LOSE ALL "
+"YOUR COINS?"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:176
+#, c-format
+msgid "reset"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:183
+#, c-format
+msgid "TESTING: This may delete all your coin, proceed with caution"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:189
+#, c-format
+msgid "run gc"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:197
+#, c-format
+msgid "import database"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:219
+#, c-format
+msgid "export database"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:225
+#, c-format
+msgid "Database exported at %1$s %2$s to download"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:248
+#, c-format
+msgid "Coins"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:282
+#, c-format
+msgid "Pending operations"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:328
+#, c-format
+msgid "usable coins"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:337
+#, c-format
+msgid "id"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:340
+#, c-format
+msgid "denom"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:343
+#, c-format
+msgid "value"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:346
+#, c-format
+msgid "status"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:349
+#, c-format
+msgid "from refresh?"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:352
+#, c-format
+msgid "age key count"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:369
+#, c-format
+msgid "spent coins"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:373
+#, c-format
+msgid "click to show"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:108
+#, c-format
+msgid "Scan a QR code or enter taler:// URI below"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:122
+#, c-format
+msgid "Open"
+msgstr "Доступні"
+
+#: src/wallet/QrReader.tsx:128
+#, c-format
+msgid "URI is not valid. Taler URI should start with `taler://`"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:133
+#, c-format
+msgid "Try another"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:183
+#, c-format
+msgid "Could not load list of exchange"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:209
+#, c-format
+msgid "Choose a currency to proceed or add another exchange"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:217
+#, c-format
+msgid "Known currencies"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:318
+#, c-format
+msgid "Specify the amount and the origin"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:336
+#, c-format
+msgid "Change currency"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:344
+#, c-format
+msgid "Use previous origins:"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:364
+#, c-format
+msgid "Or specify the origin of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:372
+#, c-format
+msgid "Specify the origin of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:380
+#, c-format
+msgid "From my bank account"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:395
+#, c-format
+msgid "From another wallet"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:449
+#, c-format
+msgid "currency not provided"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:459
+#, c-format
+msgid "Specify the amount and the destination"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:483
+#, c-format
+msgid "Use previous destinations:"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:503
+#, c-format
+msgid "Or specify the destination of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:511
+#, c-format
+msgid "Specify the destination of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:521
+#, c-format
+msgid "To my bank account"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:534
+#, c-format
+msgid "To another wallet"
+msgstr ""
+
+#: src/cta/Recovery/views.tsx:30
+#, c-format
+msgid "Could not load backup recovery information"
+msgstr ""
+
+#: src/cta/Recovery/views.tsx:47
+#, c-format
+msgid "Digital wallet recovery"
+msgstr ""
+
+#: src/cta/Recovery/views.tsx:52
+#, c-format
+msgid "Import backup, show info"
+msgstr ""
+
+#: src/wallet/Application.tsx:189
+#, c-format
+msgid "All done, your transaction is in progress"
+msgstr ""
+
+#: src/components/EditableText.tsx:45
+#, c-format
+msgid "Edit"
+msgstr ""
+
+#: src/wallet/ManualWithdrawPage.tsx:102
+#, c-format
+msgid "Could not load the list of known exchanges"
+msgstr ""
diff --git a/packages/taler-wallet-webextension/src/mui/Alert.stories.tsx b/packages/taler-wallet-webextension/src/mui/Alert.stories.tsx
index 62f7a2993..b0c2a2730 100644
--- a/packages/taler-wallet-webextension/src/mui/Alert.stories.tsx
+++ b/packages/taler-wallet-webextension/src/mui/Alert.stories.tsx
@@ -19,6 +19,7 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
+import { TranslatedString } from "@gnu-taler/taler-util";
import { css } from "@linaria/core";
import { ComponentChildren, Fragment, h, VNode } from "preact";
import { Alert } from "./Alert.jsx";
@@ -53,16 +54,16 @@ export const BasicExample = (): VNode => (
export const WithTitle = (): VNode => (
<Wrapper>
- <Alert title="Warning" severity="warning">
+ <Alert title={"Warning" as TranslatedString} severity="warning">
this is an warning
</Alert>
- <Alert title="Error" severity="error">
+ <Alert title={"Error" as TranslatedString} severity="error">
this is an error
</Alert>
- <Alert title="Success" severity="success">
+ <Alert title={"Success" as TranslatedString} severity="success">
this is an success
</Alert>
- <Alert title="Info" severity="info">
+ <Alert title={"Info" as TranslatedString} severity="info">
this is an info
</Alert>
</Wrapper>
@@ -74,16 +75,32 @@ const showSomething = async function (): Promise<void> {
export const WithAction = (): VNode => (
<Wrapper>
- <Alert title="Warning" severity="warning" onClose={showSomething}>
+ <Alert
+ title={"Warning" as TranslatedString}
+ severity="warning"
+ onClose={showSomething}
+ >
this is an warning
</Alert>
- <Alert title="Error" severity="error" onClose={showSomething}>
+ <Alert
+ title={"Error" as TranslatedString}
+ severity="error"
+ onClose={showSomething}
+ >
this is an error
</Alert>
- <Alert title="Success" severity="success" onClose={showSomething}>
+ <Alert
+ title={"Success" as TranslatedString}
+ severity="success"
+ onClose={showSomething}
+ >
this is an success
</Alert>
- <Alert title="Info" severity="info" onClose={showSomething}>
+ <Alert
+ title={"Info" as TranslatedString}
+ severity="info"
+ onClose={showSomething}
+ >
this is an info
</Alert>
</Wrapper>
diff --git a/packages/taler-wallet-webextension/src/mui/Alert.tsx b/packages/taler-wallet-webextension/src/mui/Alert.tsx
index 360c3c3cb..22ea0b8ab 100644
--- a/packages/taler-wallet-webextension/src/mui/Alert.tsx
+++ b/packages/taler-wallet-webextension/src/mui/Alert.tsx
@@ -13,20 +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 { TranslatedString } from "@gnu-taler/taler-util";
import { css } from "@linaria/core";
import { ComponentChildren, h, VNode } from "preact";
// eslint-disable-next-line import/extensions
-import CloseIcon from "../svg/close_24px.svg";
-import ErrorOutlineIcon from "../svg/error_outline_outlined_24px.svg";
-import InfoOutlinedIcon from "../svg/info_outlined_24px.svg";
-import ReportProblemOutlinedIcon from "../svg/report_problem_outlined_24px.svg";
-import SuccessOutlinedIcon from "../svg/success_outlined_24px.svg";
+import CloseIcon from "../svg/close_24px.inline.svg";
+import ErrorOutlineIcon from "../svg/error_outline_outlined_24px.inline.svg";
+import InfoOutlinedIcon from "../svg/info_outlined_24px.inline.svg";
+import ReportProblemOutlinedIcon from "../svg/report_problem_outlined_24px.inline.svg";
+import SuccessOutlinedIcon from "../svg/success_outlined_24px.inline.svg";
import { IconButton } from "./Button.js";
-// eslint-disable-next-line import/extensions
-import { darken, lighten } from "./colors/manipulation";
+import { darken, lighten } from "./colors/manipulation.js";
import { Paper } from "./Paper.js";
-// eslint-disable-next-line import/extensions
-import { theme } from "./style";
+import { theme } from "./style.jsx";
import { Typography } from "./Typography.js";
const defaultIconMapping = {
@@ -61,7 +60,7 @@ const colorVariant = {
};
interface Props {
- title?: string;
+ title?: TranslatedString;
variant?: "filled" | "outlined" | "standard";
role?: string;
onClose?: () => Promise<void>;
@@ -110,20 +109,20 @@ function Message({
title,
children,
}: {
- title?: string;
+ title?: TranslatedString;
children: ComponentChildren;
}): VNode {
return (
<div
class={css`
padding: 8px 0px;
- width: 100%;
+ width: calc(100% - 48px - 36px);
`}
>
{title && (
<Typography
class={css`
- font-weight: ${theme.typography.fontWeightMedium};
+ font-weight: ${theme.typography.fontWeightBold};
`}
gutterBottom
>
@@ -160,6 +159,7 @@ export function Alert({
"--color-main": theme.palette[severity].main,
"--color-light": theme.palette[severity].light,
// ...(style as any),
+ textAlign: "left",
}}
elevation={1}
>
diff --git a/packages/taler-wallet-webextension/src/mui/Avatar.tsx b/packages/taler-wallet-webextension/src/mui/Avatar.tsx
index 14ec1ae9b..b6e37d2ce 100644
--- a/packages/taler-wallet-webextension/src/mui/Avatar.tsx
+++ b/packages/taler-wallet-webextension/src/mui/Avatar.tsx
@@ -16,7 +16,7 @@
import { css } from "@linaria/core";
import { h, JSX, VNode, ComponentChildren } from "preact";
// eslint-disable-next-line import/extensions
-import { theme } from "./style";
+import { theme } from "./style.jsx";
const root = css`
position: relative;
diff --git a/packages/taler-wallet-webextension/src/mui/Button.stories.tsx b/packages/taler-wallet-webextension/src/mui/Button.stories.tsx
index 65af81849..5506caa42 100644
--- a/packages/taler-wallet-webextension/src/mui/Button.stories.tsx
+++ b/packages/taler-wallet-webextension/src/mui/Button.stories.tsx
@@ -21,8 +21,8 @@
import { Button } from "./Button.js";
import { Fragment, h, VNode } from "preact";
-import DeleteIcon from "../svg/delete_24px.svg";
-import SendIcon from "../svg/send_24px.svg";
+import DeleteIcon from "../svg/delete_24px.inline.svg";
+import SendIcon from "../svg/send_24px.inline.svg";
import { styled } from "@linaria/react";
export default {
diff --git a/packages/taler-wallet-webextension/src/mui/Button.tsx b/packages/taler-wallet-webextension/src/mui/Button.tsx
index bca0d6231..1af281d42 100644
--- a/packages/taler-wallet-webextension/src/mui/Button.tsx
+++ b/packages/taler-wallet-webextension/src/mui/Button.tsx
@@ -16,10 +16,16 @@
import { ComponentChildren, h, VNode, JSX } from "preact";
import { css } from "@linaria/core";
// eslint-disable-next-line import/extensions
-import { theme, Colors, rippleEnabled, rippleEnabledOutlined } from "./style";
+import {
+ theme,
+ Colors,
+ rippleEnabled,
+ rippleEnabledOutlined,
+} from "./style.js";
// eslint-disable-next-line import/extensions
-import { alpha } from "./colors/manipulation";
+import { alpha } from "./colors/manipulation.js";
import { useState } from "preact/hooks";
+import { SafeHandler } from "./handlers.js";
export const buttonBaseStyle = css`
display: inline-flex;
@@ -55,6 +61,7 @@ interface Props {
tooltip?: string;
color?: Colors;
onClick?: () => Promise<void>;
+ // onClick?: SafeHandler<void>;
}
const button = css`
diff --git a/packages/taler-wallet-webextension/src/mui/Grid.tsx b/packages/taler-wallet-webextension/src/mui/Grid.tsx
index e9b839b2f..2db439778 100644
--- a/packages/taler-wallet-webextension/src/mui/Grid.tsx
+++ b/packages/taler-wallet-webextension/src/mui/Grid.tsx
@@ -17,7 +17,7 @@ import { css } from "@linaria/core";
import { h, JSX, VNode, ComponentChildren, createContext } from "preact";
import { useContext } from "preact/hooks";
// eslint-disable-next-line import/extensions
-import { theme } from "./style";
+import { theme } from "./style.js";
type ResponsiveKeys = "xs" | "sm" | "md" | "lg" | "xl";
diff --git a/packages/taler-wallet-webextension/src/mui/InputFile.tsx b/packages/taler-wallet-webextension/src/mui/InputFile.tsx
index 2b67dc99b..40e9f9ace 100644
--- a/packages/taler-wallet-webextension/src/mui/InputFile.tsx
+++ b/packages/taler-wallet-webextension/src/mui/InputFile.tsx
@@ -49,7 +49,7 @@ export function InputFile<T>({ onChange, children }: Props): VNode {
ref={image}
style={{ display: "none" }}
type="file"
- name={String(name)}
+ // name={String(name)}
onChange={(e) => {
const f: FileList | null = e.currentTarget.files;
if (!f || f.length != 1) {
diff --git a/packages/taler-wallet-webextension/src/mui/Menu.stories.tsx b/packages/taler-wallet-webextension/src/mui/Menu.stories.tsx
index e2bba2678..200af8f57 100644
--- a/packages/taler-wallet-webextension/src/mui/Menu.stories.tsx
+++ b/packages/taler-wallet-webextension/src/mui/Menu.stories.tsx
@@ -47,8 +47,7 @@ export const BasicExample = (): VNode => {
};
import { styled } from "@linaria/react";
-// eslint-disable-next-line import/extensions
-import { theme } from "./style";
+import { theme } from "./style.js";
import { Typography } from "./Typography.js";
import { Divider } from "./Divider.js";
diff --git a/packages/taler-wallet-webextension/src/mui/Menu.tsx b/packages/taler-wallet-webextension/src/mui/Menu.tsx
index 941abfee4..dd8266931 100644
--- a/packages/taler-wallet-webextension/src/mui/Menu.tsx
+++ b/packages/taler-wallet-webextension/src/mui/Menu.tsx
@@ -19,7 +19,7 @@ import { buttonBaseStyle } from "./Button.js";
import { alpha } from "./colors/manipulation.js";
import { Paper } from "./Paper.js";
// eslint-disable-next-line import/extensions
-import { Colors, ripple, theme } from "./style";
+import { Colors, ripple, theme } from "./style.js";
interface Props {
children: ComponentChildren;
diff --git a/packages/taler-wallet-webextension/src/mui/Modal.tsx b/packages/taler-wallet-webextension/src/mui/Modal.tsx
index 7b1cf3f3a..0ea1372fa 100644
--- a/packages/taler-wallet-webextension/src/mui/Modal.tsx
+++ b/packages/taler-wallet-webextension/src/mui/Modal.tsx
@@ -18,11 +18,11 @@ import { css } from "@linaria/core";
import { h, JSX, VNode, ComponentChildren } from "preact";
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
// eslint-disable-next-line import/extensions
-import { alpha } from "./colors/manipulation";
+import { alpha } from "./colors/manipulation.js";
import { ModalManager } from "./ModalManager.js";
import { Portal } from "./Portal.js";
// eslint-disable-next-line import/extensions
-import { theme } from "./style";
+import { theme } from "./style.js";
const baseStyle = css`
position: fixed;
diff --git a/packages/taler-wallet-webextension/src/mui/Paper.tsx b/packages/taler-wallet-webextension/src/mui/Paper.tsx
index 3b5f24bc1..a44b1be0d 100644
--- a/packages/taler-wallet-webextension/src/mui/Paper.tsx
+++ b/packages/taler-wallet-webextension/src/mui/Paper.tsx
@@ -16,9 +16,9 @@
import { css } from "@linaria/core";
import { h, JSX, VNode, ComponentChildren } from "preact";
// eslint-disable-next-line import/extensions
-import { alpha } from "./colors/manipulation";
+import { alpha } from "./colors/manipulation.js";
// eslint-disable-next-line import/extensions
-import { theme } from "./style";
+import { theme } from "./style.js";
const borderVariant = {
outlined: css`
@@ -29,9 +29,6 @@ const borderVariant = {
`,
};
const baseStyle = css`
- background-color: ${theme.palette.background.paper};
- color: ${theme.palette.text.primary};
-
.theme-dark & {
background-image: var(--gradient-white-elevation);
}
diff --git a/packages/taler-wallet-webextension/src/mui/Popover.tsx b/packages/taler-wallet-webextension/src/mui/Popover.tsx
index 69e0ab10a..da551c65d 100644
--- a/packages/taler-wallet-webextension/src/mui/Popover.tsx
+++ b/packages/taler-wallet-webextension/src/mui/Popover.tsx
@@ -15,11 +15,7 @@
*/
import { css } from "@linaria/core";
-import { h, JSX, VNode, ComponentChildren } from "preact";
-// eslint-disable-next-line import/extensions
-import { alpha } from "./colors/manipulation";
-// eslint-disable-next-line import/extensions
-import { theme } from "./style";
+import { h, VNode, ComponentChildren } from "preact";
const baseStyle = css``;
diff --git a/packages/taler-wallet-webextension/src/mui/Portal.tsx b/packages/taler-wallet-webextension/src/mui/Portal.tsx
index 2026d7e5a..1d835abac 100644
--- a/packages/taler-wallet-webextension/src/mui/Portal.tsx
+++ b/packages/taler-wallet-webextension/src/mui/Portal.tsx
@@ -26,11 +26,9 @@ import {
cloneElement,
Fragment,
} from "preact";
-import { Ref, useEffect, useMemo, useState } from "preact/hooks";
-// eslint-disable-next-line import/extensions
-import { alpha } from "./colors/manipulation";
-// eslint-disable-next-line import/extensions
-import { theme } from "./style";
+import { Ref, useEffect, useState } from "preact/hooks";
+
+import { theme } from "./style.js";
const baseStyle = css`
position: fixed;
diff --git a/packages/taler-wallet-webextension/src/mui/TextField.tsx b/packages/taler-wallet-webextension/src/mui/TextField.tsx
index 42ac49a00..ab29fb78d 100644
--- a/packages/taler-wallet-webextension/src/mui/TextField.tsx
+++ b/packages/taler-wallet-webextension/src/mui/TextField.tsx
@@ -23,14 +23,14 @@ import { SelectFilled } from "./input/SelectFilled.js";
import { SelectOutlined } from "./input/SelectOutlined.js";
import { SelectStandard } from "./input/SelectStandard.js";
// eslint-disable-next-line import/extensions
-import { Colors } from "./style";
+import { Colors } from "./style.js";
export interface Props {
autoComplete?: string;
autoFocus?: boolean;
color?: Colors;
disabled?: boolean;
- error?: string;
+ error?: string | Error;
fullWidth?: boolean;
helperText?: VNode | string;
id?: string;
diff --git a/packages/taler-wallet-webextension/src/mui/Typography.tsx b/packages/taler-wallet-webextension/src/mui/Typography.tsx
index b3a9e0010..c9c811c99 100644
--- a/packages/taler-wallet-webextension/src/mui/Typography.tsx
+++ b/packages/taler-wallet-webextension/src/mui/Typography.tsx
@@ -15,9 +15,9 @@
*/
import { css } from "@linaria/core";
import { ComponentChildren, h, VNode } from "preact";
-import { useTranslationContext } from "../context/translation.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
// eslint-disable-next-line import/extensions
-import { theme } from "./style";
+import { theme } from "./style.js";
type VariantEnum =
| "body1"
diff --git a/packages/taler-wallet-webextension/src/mui/colors/manipulation.ts b/packages/taler-wallet-webextension/src/mui/colors/manipulation.ts
index 226d3c860..f9bf9eb2b 100644
--- a/packages/taler-wallet-webextension/src/mui/colors/manipulation.ts
+++ b/packages/taler-wallet-webextension/src/mui/colors/manipulation.ts
@@ -57,7 +57,7 @@ export function hexToRgb(color: string): string {
let colors = color.match(re);
if (colors && colors[0].length === 1) {
- colors = colors.map((n) => n + n);
+ colors = colors.map((n) => n + n) as RegExpMatchArray;
}
return colors
diff --git a/packages/taler-wallet-webextension/src/mui/handlers.ts b/packages/taler-wallet-webextension/src/mui/handlers.ts
index 655fceef9..a194bd02a 100644
--- a/packages/taler-wallet-webextension/src/mui/handlers.ts
+++ b/packages/taler-wallet-webextension/src/mui/handlers.ts
@@ -14,23 +14,58 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { AmountJson } from "@gnu-taler/taler-util";
-import { TalerError } from "@gnu-taler/taler-wallet-core";
export interface TextFieldHandler {
- onInput?: (value: string) => Promise<void>;
+ onInput?: SafeHandler<string>;
value: string;
- error?: string;
+ error?: string | Error;
}
export interface AmountFieldHandler {
- onInput?: (value: AmountJson) => Promise<void>;
+ onInput?: SafeHandler<AmountJson>;
value: AmountJson;
- error?: string;
+ error?: string | Error;
}
+declare const __safe_handler: unique symbol;
+export type SafeHandler<T> = {
+ <Req extends T>(req: Req): Promise<void>;
+ (): Promise<void>;
+ [__safe_handler]: true;
+};
+
+type UnsafeHandler<T> = ((p: T) => Promise<void>) | ((p: T) => void);
+
+export function withSafe<T>(
+ handler: UnsafeHandler<T>,
+ onError: (e: Error) => void,
+): SafeHandler<T> {
+ const sh = async function (p: T): Promise<void> {
+ try {
+ await handler(p);
+ } catch (e) {
+ if (e instanceof Error) {
+ onError(e);
+ } else {
+ onError(new Error(String(e)));
+ }
+ }
+ };
+ return sh as SafeHandler<T>;
+}
+
+export const nullFunction = async function (): Promise<void> {
+ //do nothing
+} as SafeHandler<void>;
+
+//FIXME: UI button should required SafeHandler but
+//useStateComponent should not be required to create SafeHandlers
+//so this need to be split in two:
+// * ButtonHandlerUI => with i18n
+// * ButtonHandlerLogic => without i18n
export interface ButtonHandler {
- onClick?: () => Promise<void>;
- error?: TalerError;
+ onClick?: SafeHandler<void>;
+ // error?: TalerError;
}
export interface ToggleHandler {
@@ -39,7 +74,7 @@ export interface ToggleHandler {
}
export interface SelectFieldHandler {
- onChange?: (value: string) => Promise<void>;
+ onChange?: SafeHandler<string>;
error?: string;
value: string;
isDirty?: boolean;
diff --git a/packages/taler-wallet-webextension/src/mui/input/FormControl.tsx b/packages/taler-wallet-webextension/src/mui/input/FormControl.tsx
index e80e7f8d8..45f5a81d1 100644
--- a/packages/taler-wallet-webextension/src/mui/input/FormControl.tsx
+++ b/packages/taler-wallet-webextension/src/mui/input/FormControl.tsx
@@ -17,12 +17,12 @@ import { css } from "@linaria/core";
import { ComponentChildren, createContext, h, VNode } from "preact";
import { useContext, useMemo, useState } from "preact/hooks";
// eslint-disable-next-line import/extensions
-import { Colors } from "../style";
+import { Colors } from "../style.js";
export interface Props {
color: Colors;
disabled: boolean;
- error?: string;
+ error?: string | Error;
focused: boolean;
fullWidth: boolean;
hiddenLabel: boolean;
@@ -124,7 +124,7 @@ export interface FCCProps {
// setAdornedStart,
color: Colors;
disabled: boolean;
- error: string | undefined;
+ error: string | undefined | Error;
filled: boolean;
focused: boolean;
fullWidth: boolean;
diff --git a/packages/taler-wallet-webextension/src/mui/input/FormHelperText.tsx b/packages/taler-wallet-webextension/src/mui/input/FormHelperText.tsx
index 5e40ba616..3b80b0f23 100644
--- a/packages/taler-wallet-webextension/src/mui/input/FormHelperText.tsx
+++ b/packages/taler-wallet-webextension/src/mui/input/FormHelperText.tsx
@@ -16,7 +16,7 @@
import { css } from "@linaria/core";
import { ComponentChildren, h, VNode } from "preact";
// eslint-disable-next-line import/extensions
-import { theme } from "../style";
+import { theme } from "../style.js";
import { useFormControl } from "./FormControl.js";
const root = css`
@@ -43,7 +43,7 @@ const containedStyle = css`
interface Props {
disabled?: boolean;
- error?: string;
+ error?: string | Error;
filled?: boolean;
focused?: boolean;
margin?: "dense";
diff --git a/packages/taler-wallet-webextension/src/mui/input/FormLabel.tsx b/packages/taler-wallet-webextension/src/mui/input/FormLabel.tsx
index 11404b5c1..68fbdc38e 100644
--- a/packages/taler-wallet-webextension/src/mui/input/FormLabel.tsx
+++ b/packages/taler-wallet-webextension/src/mui/input/FormLabel.tsx
@@ -16,7 +16,7 @@
import { css } from "@linaria/core";
import { ComponentChildren, h, VNode } from "preact";
// eslint-disable-next-line import/extensions
-import { Colors, theme } from "../style";
+import { Colors, theme } from "../style.js";
import { useFormControl } from "./FormControl.js";
export interface Props {
diff --git a/packages/taler-wallet-webextension/src/mui/input/InputBase.tsx b/packages/taler-wallet-webextension/src/mui/input/InputBase.tsx
index 94304f16b..d811a3dbb 100644
--- a/packages/taler-wallet-webextension/src/mui/input/InputBase.tsx
+++ b/packages/taler-wallet-webextension/src/mui/input/InputBase.tsx
@@ -23,7 +23,7 @@ import {
useState,
} from "preact/hooks";
// eslint-disable-next-line import/extensions
-import { theme } from "../style";
+import { theme } from "../style.js";
import { FormControlContext, useFormControl } from "./FormControl.js";
const rootStyle = css`
diff --git a/packages/taler-wallet-webextension/src/mui/input/InputFilled.tsx b/packages/taler-wallet-webextension/src/mui/input/InputFilled.tsx
index 9ab91e7fd..0707046f3 100644
--- a/packages/taler-wallet-webextension/src/mui/input/InputFilled.tsx
+++ b/packages/taler-wallet-webextension/src/mui/input/InputFilled.tsx
@@ -16,7 +16,7 @@
import { css } from "@linaria/core";
import { h, VNode } from "preact";
// eslint-disable-next-line import/extensions
-import { Colors, theme } from "../style";
+import { Colors, theme } from "../style.js";
import { useFormControl } from "./FormControl.js";
import { InputBase, InputBaseComponent, InputBaseRoot } from "./InputBase.js";
@@ -27,7 +27,7 @@ export interface Props {
defaultValue?: string;
disabled?: boolean;
disableUnderline?: boolean;
- error?: string;
+ error?: string | Error;
fullWidth?: boolean;
id?: string;
margin?: "dense" | "normal" | "none";
@@ -89,9 +89,9 @@ const filledRootStyle = css`
border-top-left-radius: ${theme.shape.borderRadius}px;
border-top-right-radius: ${theme.shape.borderRadius}px;
transition: ${theme.transitions.create("background-color", {
- duration: theme.transitions.duration.shorter,
- easing: theme.transitions.easing.easeOut,
- })};
+ duration: theme.transitions.duration.shorter,
+ easing: theme.transitions.easing.easeOut,
+})};
// when is not disabled underline
&:hover {
background-color: ${backgroundColorHover};
@@ -124,9 +124,9 @@ const underlineStyle = css`
right: 0px;
transform: scaleX(0);
transition: ${theme.transitions.create("transform", {
- duration: theme.transitions.duration.shorter,
- easing: theme.transitions.easing.easeOut,
- })};
+ duration: theme.transitions.duration.shorter,
+ easing: theme.transitions.easing.easeOut,
+})};
pointer-events: none;
}
&[data-focused]:after {
@@ -139,8 +139,8 @@ const underlineStyle = css`
&:before {
border-bottom: 1px solid
${theme.palette.mode === "light"
- ? "rgba(0, 0, 0, 0.42)"
- : "rgba(255, 255, 255, 0.7)"};
+ ? "rgba(0, 0, 0, 0.42)"
+ : "rgba(255, 255, 255, 0.7)"};
left: 0px;
bottom: 0px;
right: 0px;
@@ -156,8 +156,8 @@ const underlineStyle = css`
@media (hover: none) {
border-bottom: 1px solid
${theme.palette.mode === "light"
- ? "rgba(0, 0, 0, 0.42)"
- : "rgba(255, 255, 255, 0.7)"};
+ ? "rgba(0, 0, 0, 0.42)"
+ : "rgba(255, 255, 255, 0.7)"};
}
}
&[data-disabled]:before {
diff --git a/packages/taler-wallet-webextension/src/mui/input/InputLabel.tsx b/packages/taler-wallet-webextension/src/mui/input/InputLabel.tsx
index 35cbd7a41..2d4743e59 100644
--- a/packages/taler-wallet-webextension/src/mui/input/InputLabel.tsx
+++ b/packages/taler-wallet-webextension/src/mui/input/InputLabel.tsx
@@ -16,7 +16,7 @@
import { css } from "@linaria/core";
import { ComponentChildren, h, VNode } from "preact";
// eslint-disable-next-line import/extensions
-import { Colors, theme } from "../style";
+import { Colors, theme } from "../style.js";
import { useFormControl } from "./FormControl.js";
import { FormLabel } from "./FormLabel.js";
diff --git a/packages/taler-wallet-webextension/src/mui/input/InputStandard.tsx b/packages/taler-wallet-webextension/src/mui/input/InputStandard.tsx
index 45614f618..7352c5ec1 100644
--- a/packages/taler-wallet-webextension/src/mui/input/InputStandard.tsx
+++ b/packages/taler-wallet-webextension/src/mui/input/InputStandard.tsx
@@ -15,8 +15,7 @@
*/
import { css } from "@linaria/core";
import { h, VNode } from "preact";
-// eslint-disable-next-line import/extensions
-import { Colors, theme } from "../style";
+import { Colors, theme } from "../style.js";
import { useFormControl } from "./FormControl.js";
import { InputBase, InputBaseComponent, InputBaseRoot } from "./InputBase.js";
@@ -28,7 +27,7 @@ export interface Props {
disabled?: boolean;
disableUnderline?: boolean;
endAdornment?: VNode;
- error?: string;
+ error?: string | Error;
fullWidth?: boolean;
id?: string;
margin?: "dense" | "normal" | "none";
@@ -83,9 +82,9 @@ const underlineStyle = css`
right: 0px;
transform: scaleX(0);
transition: ${theme.transitions.create("transform", {
- duration: theme.transitions.duration.shorter,
- easing: theme.transitions.easing.easeOut,
- })};
+ duration: theme.transitions.duration.shorter,
+ easing: theme.transitions.easing.easeOut,
+})};
pointer-events: none;
}
&[data-focused]:after {
@@ -98,8 +97,8 @@ const underlineStyle = css`
&:before {
border-bottom: 1px solid
${theme.palette.mode === "light"
- ? "rgba(0, 0, 0, 0.42)"
- : "rgba(255, 255, 255, 0.7)"};
+ ? "rgba(0, 0, 0, 0.42)"
+ : "rgba(255, 255, 255, 0.7)"};
left: 0px;
bottom: 0px;
right: 0px;
@@ -115,8 +114,8 @@ const underlineStyle = css`
@media (hover: none) {
border-bottom: 1px solid
${theme.palette.mode === "light"
- ? "rgba(0, 0, 0, 0.42)"
- : "rgba(255, 255, 255, 0.7)"};
+ ? "rgba(0, 0, 0, 0.42)"
+ : "rgba(255, 255, 255, 0.7)"};
}
}
&[data-disabled]:before {
diff --git a/packages/taler-wallet-webextension/src/mui/style.tsx b/packages/taler-wallet-webextension/src/mui/style.tsx
index c3071b314..99adf2a76 100644
--- a/packages/taler-wallet-webextension/src/mui/style.tsx
+++ b/packages/taler-wallet-webextension/src/mui/style.tsx
@@ -26,9 +26,9 @@ import {
purple,
red,
// eslint-disable-next-line import/extensions
-} from "./colors/constants";
+} from "./colors/constants.js";
// eslint-disable-next-line import/extensions
-import { getContrastRatio } from "./colors/manipulation";
+import { getContrastRatio } from "./colors/manipulation.js";
export type Colors =
| "primary"
@@ -56,8 +56,6 @@ export interface Spacing {
(top: number, right: number, bottom: number, left: number): string;
}
-export const theme = createTheme();
-
const zIndex = {
mobileStepper: 1000,
speedDial: 1050,
@@ -68,6 +66,8 @@ const zIndex = {
tooltip: 1500,
};
+export const theme = createTheme();
+
export const ripple = css`
background-position: center;
diff --git a/packages/taler-wallet-webextension/src/platform/api.ts b/packages/taler-wallet-webextension/src/platform/api.ts
index 37546d2df..3c116fab2 100644
--- a/packages/taler-wallet-webextension/src/platform/api.ts
+++ b/packages/taler-wallet-webextension/src/platform/api.ts
@@ -14,7 +14,18 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { CoreApiResponse, NotificationType } from "@gnu-taler/taler-util";
+import {
+ CoreApiResponse,
+ TalerUri,
+ WalletNotification,
+ WalletRunConfig,
+} from "@gnu-taler/taler-util";
+import { WalletOperations } from "@gnu-taler/taler-wallet-core";
+import {
+ ExtensionOperations,
+ MessageFromExtension,
+} from "../taler-wallet-interaction-loader.js";
+import { BackgroundOperations } from "../wxApi.js";
export interface Permissions {
/**
@@ -33,33 +44,115 @@ export interface Permissions {
* Compatibility API that works on multiple browsers.
*/
export interface CrossBrowserPermissionsApi {
- containsHostPermissions(): Promise<boolean>;
- requestHostPermissions(): Promise<boolean>;
- removeHostPermissions(): Promise<boolean>;
-
containsClipboardPermissions(): Promise<boolean>;
requestClipboardPermissions(): Promise<boolean>;
removeClipboardPermissions(): Promise<boolean>;
+}
- addPermissionsListener(
- callback: (p: Permissions, lastError?: string) => void,
- ): void;
+export enum ExtensionNotificationType {
+ SettingsChange = "settings-change",
+ ClearNotifications = "clear-notifications",
}
-export type MessageFromBackend = {
- type: NotificationType;
+export interface SettingsChangeNotification {
+ type: ExtensionNotificationType.SettingsChange;
+
+ currentValue: Settings;
+}
+export interface ClearNotificaitonNotification {
+ type: ExtensionNotificationType.ClearNotifications;
+}
+
+export type ExtensionNotification =
+ | SettingsChangeNotification
+ | ClearNotificaitonNotification;
+
+export type MessageFromBackend =
+ | {
+ type: "wallet";
+ notification: WalletNotification;
+ }
+ | {
+ type: "web-extension";
+ notification: ExtensionNotification;
+ };
+
+export type MessageFromFrontend<
+ Op extends BackgroundOperations | WalletOperations | ExtensionOperations,
+> = Op extends BackgroundOperations
+ ? MessageFromFrontendBackground<keyof BackgroundOperations>
+ : Op extends ExtensionOperations
+ ? MessageFromExtension<keyof ExtensionOperations>
+ : Op extends WalletOperations
+ ? MessageFromFrontendWallet<keyof WalletOperations>
+ : never;
+
+export type MessageFromFrontendBackground<
+ Op extends keyof BackgroundOperations,
+> = {
+ channel: "background";
+ operation: Op;
+ payload: BackgroundOperations[Op]["request"];
+};
+
+export type MessageFromFrontendWallet<Op extends keyof WalletOperations> = {
+ channel: "wallet";
+ operation: Op;
+ payload: WalletOperations[Op]["request"];
};
+export type MessageResponse = CoreApiResponse;
+
export interface WalletWebExVersion {
version_name?: string | undefined;
version: string;
}
+type F = WalletRunConfig["features"];
+type WebexWalletConfig = {
+ [P in keyof F as `wallet${Capitalize<P>}`]: F[P];
+};
+
+export interface Settings extends WebexWalletConfig {
+ injectTalerSupport: boolean;
+ autoOpen: boolean;
+ advancedMode: boolean;
+ backup: boolean;
+ langSelector: boolean;
+ showJsonOnError: boolean;
+ extendedAccountTypes: boolean;
+ showRefeshTransactions: boolean;
+ suspendIndividualTransaction: boolean;
+ showExchangeManagement: boolean;
+ selectTosFormat: boolean;
+ showWalletActivity: boolean;
+}
+
+export const defaultSettings: Settings = {
+ injectTalerSupport: true,
+ autoOpen: true,
+ advancedMode: false,
+ backup: false,
+ langSelector: false,
+ showRefeshTransactions: false,
+ suspendIndividualTransaction: false,
+ showJsonOnError: false,
+ extendedAccountTypes: false,
+ showExchangeManagement: false,
+ walletAllowHttp: false,
+ selectTosFormat: false,
+ showWalletActivity: false,
+};
+
/**
* Compatibility helpers needed for browsers that don't implement
* WebExtension APIs consistently.
*/
-export interface PlatformAPI {
+export interface BackgroundPlatformAPI {
+ /**
+ *
+ */
+ getSettingsFromStorage(): Promise<Settings>;
/**
* Guarantee that the service workers don't die
*/
@@ -71,44 +164,49 @@ export interface PlatformAPI {
*/
isFirefox(): boolean;
+ registerOnInstalled(callback: () => void): void;
+
/**
- * Permission API for checking and add a listener
+ *
+ * Check if background process run as service worker. This is used from the
+ * wallet use different http api and crypto worker.
*/
- getPermissionsApi(): CrossBrowserPermissionsApi;
-
+ useServiceWorkerAsBackgroundProcess(): boolean;
+ /**
+ *
+ * Open a page into the wallet UI
+ * @param page
+ */
+ openWalletPage(page: string): void;
/**
- * Backend API
*
* Register a callback to be called when the wallet is ready to start
* @param callback
*/
- notifyWhenAppIsReady(callback: () => void): void;
+ notifyWhenAppIsReady(): Promise<void>;
/**
- * Popup API
- *
- * Used when an TalerURI is found and open up from the popup UI.
- * Closes the popup and open the URI into the wallet UI.
- *
- * @param talerUri
+ * Get the wallet version from manifest
*/
- openWalletURIFromPopup(talerUri: string): void;
-
+ getWalletWebExVersion(): WalletWebExVersion;
/**
* Backend API
- *
- * Open a page into the wallet UI
- * @param page
*/
- openWalletPage(page: string): void;
+ registerAllIncomingConnections(): void;
+ /**
+ * Backend API
+ */
+ registerReloadOnNewVersion(): void;
/**
- * Popup API
- *
- * Open a page into the wallet UI and closed the popup
- * @param page
+ * Permission API for checking and add a listener
*/
- openWalletPageFromPopup(page: string): void;
+ getPermissionsApi(): CrossBrowserPermissionsApi;
+ /**
+ * Used by the wallet backend to send notification about new information
+ * @param message
+ */
+ sendMessageToAllChannels(message: MessageFromBackend): void;
/**
* Backend API
@@ -120,42 +218,70 @@ export interface PlatformAPI {
* @param page
*/
redirectTabToWalletPage(tabId: number, page: string): void;
-
/**
- * Get the wallet version from manifest
+ * Use by the wallet backend to receive operations from frontend (popup & wallet)
+ * and send a response back.
+ *
+ * @param onNewMessage
*/
- getWalletWebExVersion(): WalletWebExVersion;
+ listenToAllChannels(
+ notifyNewMessage: <Op extends WalletOperations | BackgroundOperations>(
+ message: MessageFromFrontend<Op> & { id: string },
+ ) => Promise<MessageResponse>,
+ ): void;
/**
- * Backend API
+ * Change web extension Icon
*/
- registerAllIncomingConnections(): void;
+ setAlertedIcon(): void;
+ setNormalIcon(): void;
+}
+
+export interface ForegroundPlatformAPI {
/**
- * Backend API
+ * Check if the extension is running under
+ * chrome incognito or firefox private mode.
*/
- registerReloadOnNewVersion(): void;
+ runningOnPrivateMode(): boolean;
/**
- * Backend API
+ * FIXME: should not be needed
+ *
+ * check if the platform is firefox
*/
- registerTalerHeaderListener(
- onHeader: (tabId: number, url: string) => void,
- ): void;
+ isFirefox(): boolean;
+
/**
- * Frontend API
+ * Permission API for checking and add a listener
*/
- containsTalerHeaderListener(): boolean;
+ getPermissionsApi(): CrossBrowserPermissionsApi;
+
/**
- * Backend API
+ * Popup API
+ *
+ * Used when an TalerURI is found and open up from the popup UI.
+ * Closes the popup and open the URI into the wallet UI.
+ *
+ * @param talerUri
*/
- registerOnInstalled(callback: () => void): void;
+ openWalletURIFromPopup(talerUri: TalerUri): void;
/**
- * Backend API
+ * Popup API
*
- * Check if background process run as service worker. This is used from the
- * wallet use different http api and crypto worker.
+ * Open a page into the wallet UI and close the popup
+ * @param page
*/
- useServiceWorkerAsBackgroundProcess(): boolean;
+ openWalletPageFromPopup(page: string): void;
+
+ /**
+ * Open a page and close the popup
+ * @param url
+ */
+ openNewURLFromPopup(url: URL): void;
+ /**
+ * Get the wallet version from manifest
+ */
+ getWalletWebExVersion(): WalletWebExVersion;
/**
* Popup API
@@ -183,10 +309,15 @@ export interface PlatformAPI {
*
* @return response from the backend
*/
- sendMessageToWalletBackground(
- operation: string,
- payload: any,
- ): Promise<CoreApiResponse>;
+ sendMessageToBackground<Op extends WalletOperations | BackgroundOperations>(
+ message: MessageFromFrontend<Op>,
+ ): Promise<MessageResponse>;
+
+ /**
+ * Used by the wallet frontend to send notification about new information
+ * @param message
+ */
+ triggerWalletEvent(message: MessageFromBackend): void;
/**
* Used from the frontend to receive notifications about new information
@@ -198,27 +329,9 @@ export interface PlatformAPI {
): () => void;
/**
- * Use by the wallet backend to receive operations from frontend (popup & wallet)
- * and send a response back.
- *
- * @param onNewMessage
- */
- listenToAllChannels(
- onNewMessage: (
- message: any,
- sender: any,
- sendResponse: (r: CoreApiResponse) => void,
- ) => void,
- ): void;
-
- /**
- * Used by the wallet backend to send notification about new information
- * @param message
+ * Notify when platform went offline
*/
- sendMessageToAllChannels(message: MessageFromBackend): void;
-}
-
-export let platform: PlatformAPI = undefined as any;
-export function setupPlatform(impl: PlatformAPI): void {
- platform = impl;
+ listenNetworkConnectionState(
+ listener: (state: "on" | "off") => void,
+ ): () => void;
}
diff --git a/packages/taler-wallet-webextension/src/platform/background.ts b/packages/taler-wallet-webextension/src/platform/background.ts
new file mode 100644
index 000000000..13808af2b
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/platform/background.ts
@@ -0,0 +1,23 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { BackgroundPlatformAPI } from "./api.js";
+
+// it should never be undefined :)
+export let platform: BackgroundPlatformAPI = undefined!;
+export function setupPlatform(impl: BackgroundPlatformAPI): void {
+ platform = impl;
+}
diff --git a/packages/taler-wallet-webextension/src/platform/chrome.ts b/packages/taler-wallet-webextension/src/platform/chrome.ts
index 4b9caa714..e63040f5c 100644
--- a/packages/taler-wallet-webextension/src/platform/chrome.ts
+++ b/packages/taler-wallet-webextension/src/platform/chrome.ts
@@ -15,24 +15,36 @@
*/
import {
- classifyTalerUri,
- CoreApiResponse,
Logger,
- TalerUriType,
+ TalerError,
+ TalerErrorCode,
+ TalerUri,
+ TalerUriAction,
+ stringifyTalerUri,
} from "@gnu-taler/taler-util";
+import { WalletOperations } from "@gnu-taler/taler-wallet-core";
+import { BackgroundOperations } from "../wxApi.js";
import {
+ BackgroundPlatformAPI,
CrossBrowserPermissionsApi,
+ ExtensionNotificationType,
+ ForegroundPlatformAPI,
MessageFromBackend,
- Permissions,
- PlatformAPI,
+ MessageFromFrontend,
+ MessageResponse,
+ Settings,
+ defaultSettings,
} from "./api.js";
-const api: PlatformAPI = {
+const api: BackgroundPlatformAPI & ForegroundPlatformAPI = {
isFirefox,
+ getSettingsFromStorage,
findTalerUriInActiveTab,
findTalerUriInClipboard,
getPermissionsApi,
+ runningOnPrivateMode,
getWalletWebExVersion,
+ triggerWalletEvent,
listenToWalletBackground,
notifyWhenAppIsReady,
openWalletPage,
@@ -41,21 +53,41 @@ const api: PlatformAPI = {
redirectTabToWalletPage,
registerAllIncomingConnections,
registerOnInstalled,
- listenToAllChannels,
+ listenToAllChannels ,
registerReloadOnNewVersion,
- registerTalerHeaderListener,
sendMessageToAllChannels,
- sendMessageToWalletBackground,
+ openNewURLFromPopup,
+ sendMessageToBackground,
useServiceWorkerAsBackgroundProcess,
- containsTalerHeaderListener,
keepAlive,
+ listenNetworkConnectionState,
+ setAlertedIcon,
+ setNormalIcon,
};
export default api;
const logger = new Logger("chrome.ts");
-function keepAlive(callback: any): void {
+const WALLET_STORAGE_KEY = "wallet-settings";
+
+function jsonParseOrDefault(unparsed: string, def: unknown) {
+ if (!unparsed) return def;
+ try {
+ return JSON.parse(unparsed);
+ } catch (e) {
+ return def;
+ }
+}
+
+async function getSettingsFromStorage(): Promise<Settings> {
+ const data = await chrome.storage.local.get(WALLET_STORAGE_KEY);
+ if (!data) return defaultSettings;
+ const settings = data[WALLET_STORAGE_KEY];
+ return jsonParseOrDefault(settings, defaultSettings);
+}
+
+function keepAlive(callback: () => void): void {
if (extensionIsManifestV3()) {
chrome.alarms.create("wallet-worker", { periodInMinutes: 1 });
@@ -72,142 +104,47 @@ function isFirefox(): boolean {
return false;
}
-const hostPermissions = {
- permissions: ["webRequest"],
- origins: ["http://*/*", "https://*/*"],
-};
-
export function containsClipboardPermissions(): Promise<boolean> {
- return new Promise((res, rej) => {
- chrome.permissions.contains({ permissions: ["clipboardRead"] }, (resp) => {
- const le = chrome.runtime.lastError?.message;
- if (le) {
- rej(le);
- }
- res(resp);
- });
- });
-}
-
-export function containsHostPermissions(): Promise<boolean> {
- return new Promise((res, rej) => {
- chrome.permissions.contains(hostPermissions, (resp) => {
- const le = chrome.runtime.lastError?.message;
- if (le) {
- rej(le);
- }
- res(resp);
- });
+ return new Promise((res) => {
+ res(false);
+ // chrome.permissions.contains({ permissions: ["clipboardRead"] }, (resp) => {
+ // const le = chrome.runtime.lastError?.message;
+ // if (le) {
+ // rej(le);
+ // }
+ // res(resp);
+ // });
});
}
export async function requestClipboardPermissions(): Promise<boolean> {
- return new Promise((res, rej) => {
- chrome.permissions.request({ permissions: ["clipboardRead"] }, (resp) => {
- const le = chrome.runtime.lastError?.message;
- if (le) {
- rej(le);
- }
- res(resp);
- });
- });
-}
-
-export async function requestHostPermissions(): Promise<boolean> {
- return new Promise((res, rej) => {
- chrome.permissions.request(hostPermissions, (resp) => {
- const le = chrome.runtime.lastError?.message;
- if (le) {
- rej(le);
- }
- res(resp);
- });
- });
-}
-
-type HeaderListenerFunc = (
- details: chrome.webRequest.WebResponseHeadersDetails,
-) => void;
-let currentHeaderListener: HeaderListenerFunc | undefined = undefined;
-
-type TabListenerFunc = (tabId: number, info: chrome.tabs.TabChangeInfo) => void;
-let currentTabListener: TabListenerFunc | undefined = undefined;
-
-export function containsTalerHeaderListener(): boolean {
- return (
- currentHeaderListener !== undefined || currentTabListener !== undefined
- );
-}
-
-export async function removeHostPermissions(): Promise<boolean> {
- //if there is a handler already, remove it
- if (
- currentHeaderListener &&
- chrome?.webRequest?.onHeadersReceived?.hasListener(currentHeaderListener)
- ) {
- chrome.webRequest.onHeadersReceived.removeListener(currentHeaderListener);
- }
- if (
- currentTabListener &&
- chrome?.tabs?.onUpdated?.hasListener(currentTabListener)
- ) {
- chrome.tabs.onUpdated.removeListener(currentTabListener);
- }
-
- currentHeaderListener = undefined;
- currentTabListener = undefined;
-
- //notify the browser about this change, this operation is expensive
- if ("webRequest" in chrome) {
- chrome.webRequest.handlerBehaviorChanged(() => {
- if (chrome.runtime.lastError) {
- logger.error(JSON.stringify(chrome.runtime.lastError));
- }
- });
- }
-
- if (extensionIsManifestV3()) {
- // Trying to remove host permissions with manifest >= v3 throws an error
- return true;
- }
- return new Promise((res, rej) => {
- chrome.permissions.remove(hostPermissions, (resp) => {
- const le = chrome.runtime.lastError?.message;
- if (le) {
- rej(le);
- }
- res(resp);
- });
+ return new Promise((res) => {
+ res(false);
+ // chrome.permissions.request({ permissions: ["clipboardRead"] }, (resp) => {
+ // const le = chrome.runtime.lastError?.message;
+ // if (le) {
+ // rej(le);
+ // }
+ // res(resp);
+ // });
});
}
export function removeClipboardPermissions(): Promise<boolean> {
- return new Promise((res, rej) => {
- chrome.permissions.remove({ permissions: ["clipboardRead"] }, (resp) => {
- const le = chrome.runtime.lastError?.message;
- if (le) {
- rej(le);
- }
- res(resp);
- });
- });
-}
-
-function addPermissionsListener(
- callback: (p: Permissions, lastError?: string) => void,
-): void {
- chrome.permissions.onAdded.addListener((perm: Permissions) => {
- const lastError = chrome.runtime.lastError?.message;
- callback(perm, lastError);
+ return new Promise((res) => {
+ res(true);
+ // chrome.permissions.remove({ permissions: ["clipboardRead"] }, (resp) => {
+ // const le = chrome.runtime.lastError?.message;
+ // if (le) {
+ // rej(le);
+ // }
+ // res(resp);
+ // });
});
}
function getPermissionsApi(): CrossBrowserPermissionsApi {
return {
- addPermissionsListener,
- containsHostPermissions,
- requestHostPermissions,
- removeHostPermissions,
requestClipboardPermissions,
removeClipboardPermissions,
containsClipboardPermissions,
@@ -218,64 +155,86 @@ function getPermissionsApi(): CrossBrowserPermissionsApi {
*
* @param callback function to be called
*/
-function notifyWhenAppIsReady(callback: () => void): void {
- if (extensionIsManifestV3()) {
- callback();
- } else {
- window.addEventListener("load", callback);
- }
+function notifyWhenAppIsReady(): Promise<void> {
+ return new Promise((resolve) => {
+ if (extensionIsManifestV3()) {
+ resolve();
+ } else {
+ window.addEventListener("load", () => {
+ resolve();
+ });
+ }
+ });
}
-function openWalletURIFromPopup(talerUri: string): void {
- const uriType = classifyTalerUri(talerUri);
+function openWalletURIFromPopup(uri: TalerUri): void {
+ const talerUri = stringifyTalerUri(uri);
+ //FIXME: this should redirect to just one place
+ // the target pathname should handle what happens if the endpoint is not there
+ // like "trying to open from popup but this uri is not handled"
let url: string | undefined = undefined;
- switch (uriType) {
- case TalerUriType.TalerWithdraw:
+ switch (uri.type) {
+ case TalerUriAction.WithdrawExchange:
+ case TalerUriAction.Withdraw:
url = chrome.runtime.getURL(
- `static/wallet.html#/cta/withdraw?talerWithdrawUri=${talerUri}`,
+ `static/wallet.html#/cta/withdraw?talerUri=${encodeURIComponent(
+ talerUri,
+ )}`,
);
break;
- case TalerUriType.TalerRecovery:
+ case TalerUriAction.Restore:
url = chrome.runtime.getURL(
- `static/wallet.html#/cta/recovery?talerRecoveryUri=${talerUri}`,
+ `static/wallet.html#/cta/recovery?talerUri=${encodeURIComponent(
+ talerUri,
+ )}`,
);
break;
- case TalerUriType.TalerPay:
+ case TalerUriAction.Pay:
url = chrome.runtime.getURL(
- `static/wallet.html#/cta/pay?talerPayUri=${talerUri}`,
+ `static/wallet.html#/cta/pay?talerUri=${encodeURIComponent(talerUri)}`,
);
break;
- case TalerUriType.TalerTip:
+ case TalerUriAction.Refund:
url = chrome.runtime.getURL(
- `static/wallet.html#/cta/tip?talerTipUri=${talerUri}`,
+ `static/wallet.html#/cta/refund?talerUri=${encodeURIComponent(
+ talerUri,
+ )}`,
);
break;
- case TalerUriType.TalerRefund:
+ case TalerUriAction.PayPull:
url = chrome.runtime.getURL(
- `static/wallet.html#/cta/refund?talerRefundUri=${talerUri}`,
+ `static/wallet.html#/cta/invoice/pay?talerUri=${encodeURIComponent(
+ talerUri,
+ )}`,
);
break;
- case TalerUriType.TalerPayPull:
+ case TalerUriAction.PayPush:
url = chrome.runtime.getURL(
- `static/wallet.html#/cta/invoice/pay?talerPayPullUri=${talerUri}`,
+ `static/wallet.html#/cta/transfer/pickup?talerUri=${encodeURIComponent(
+ talerUri,
+ )}`,
);
break;
- case TalerUriType.TalerPayPush:
+ case TalerUriAction.PayTemplate:
url = chrome.runtime.getURL(
- `static/wallet.html#/cta/transfer/pickup?talerPayPushUri=${talerUri}`,
+ `static/wallet.html#/cta/pay/template?talerUri=${encodeURIComponent(
+ talerUri,
+ )}`,
);
break;
- case TalerUriType.Unknown:
- logger.warn(
- `Response with HTTP 402 the Taler header but could not classify ${talerUri}`,
+ case TalerUriAction.AddExchange:
+ url = chrome.runtime.getURL(
+ `static/wallet.html#/cta/add/exchange?talerUri=${encodeURIComponent(
+ talerUri,
+ )}`,
);
- return;
- case TalerUriType.TalerDevExperiment:
+ break;
+ case TalerUriAction.DevExperiment:
logger.warn(`taler://dev-experiment URIs are not allowed in headers`);
return;
default: {
- const error: never = uriType;
+ const error: never = uri;
logger.warn(
`Response with HTTP 402 the Taler header "${error}", but header value is not a taler:// URI.`,
);
@@ -283,7 +242,7 @@ function openWalletURIFromPopup(talerUri: string): void {
}
}
- chrome.tabs.create({ active: true, url }, () => {
+ chrome.tabs.update({ active: true, url }, () => {
window.close();
});
}
@@ -299,35 +258,58 @@ function openWalletPageFromPopup(page: string): void {
window.close();
});
}
+function openNewURLFromPopup(url: URL): void {
+ // const url = chrome.runtime.getURL(`/static/wallet.html#${page}`);
+ chrome.tabs.create({ active: true, url: url.href }, () => {
+ window.close();
+ });
+}
-let i = 0;
-
-async function sendMessageToWalletBackground(
- operation: string,
- payload: any,
-): Promise<any> {
- return new Promise<any>((resolve, reject) => {
- logger.trace("send operation to the wallet background", operation);
- chrome.runtime.sendMessage(
- { operation, payload, id: `id_${i++ % 1000}` },
- (backgroundResponse) => {
- logger.trace("BUG: got response from background", backgroundResponse);
+let nextMessageIndex = 0;
- if (chrome.runtime.lastError) {
- reject(chrome.runtime.lastError.message);
- }
- // const apiResponse = JSON.parse(resp)
+/**
+ * To be used by the foreground
+ * @param message
+ * @returns
+ */
+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}` };
+
+ 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,
+ }));
+ }, 20 * 1000);
+ chrome.runtime.sendMessage(messageWithId, (backgroundResponse) => {
+ if (timedout) {
+ return false; //already rejected
+ }
+ clearTimeout(timerId);
+ if (chrome.runtime.lastError) {
+ reject(chrome.runtime.lastError.message);
+ } else {
resolve(backgroundResponse);
-
- // return true to keep the channel open
- return true;
- },
- );
+ }
+ // return true to keep the channel open
+ return true;
+ });
});
}
+/**
+ * To be used by the foreground
+ */
let notificationPort: chrome.runtime.Port | undefined;
-function listenToWalletBackground(listener: (m: any) => void): () => void {
+function listenToWalletBackground(listener: (message: MessageFromBackend) => void): () => void {
if (notificationPort === undefined) {
notificationPort = chrome.runtime.connect({ name: "notifications" });
}
@@ -342,6 +324,17 @@ function listenToWalletBackground(listener: (m: any) => void): () => void {
const allPorts: chrome.runtime.Port[] = [];
+function triggerWalletEvent(message: MessageFromBackend): void {
+ for (const notif of allPorts) {
+ // const message: MessageFromBackend = { type: msg.type };
+ try {
+ notif.postMessage(message);
+ } catch (e) {
+ logger.error("error posting a message", e);
+ }
+ }
+}
+
function sendMessageToAllChannels(message: MessageFromBackend): void {
for (const notif of allPorts) {
// const message: MessageFromBackend = { type: msg.type };
@@ -355,28 +348,60 @@ function sendMessageToAllChannels(message: MessageFromBackend): void {
function registerAllIncomingConnections(): void {
chrome.runtime.onConnect.addListener((port) => {
- allPorts.push(port);
- port.onDisconnect.addListener((discoPort) => {
- const idx = allPorts.indexOf(discoPort);
- if (idx >= 0) {
- allPorts.splice(idx, 1);
- }
- });
+ try {
+ allPorts.push(port);
+ port.onDisconnect.addListener((discoPort) => {
+ try {
+ const idx = allPorts.indexOf(discoPort);
+ if (idx >= 0) {
+ allPorts.splice(idx, 1);
+ }
+ } catch (e) {
+ logger.error("error trying to remove connection", e);
+ }
+ });
+ } catch (e) {
+ logger.error("error trying to save incoming connection", e);
+ }
+ });
+ chrome.storage.onChanged.addListener((event) => {
+ if (event[WALLET_STORAGE_KEY]) {
+ sendMessageToAllChannels({
+ type: "web-extension",
+ notification: {
+ type: ExtensionNotificationType.SettingsChange,
+ currentValue: jsonParseOrDefault(
+ event[WALLET_STORAGE_KEY].newValue,
+ defaultSettings,
+ ),
+ },
+ });
+ }
});
}
function listenToAllChannels(
- cb: (
- message: any,
- sender: any,
- callback: (r: CoreApiResponse) => void,
- ) => void,
+ notifyNewMessage: <Op extends WalletOperations | BackgroundOperations>(
+ message: MessageFromFrontend<Op> & { id: string },
+ ) => Promise<MessageResponse>,
): void {
- chrome.runtime.onMessage.addListener((m, s, c) => {
- cb(m, s, (apiResponse) => {
- logger.trace("BUG: sending response to client", apiResponse);
- c(apiResponse);
- });
+ chrome.runtime.onMessage.addListener((message, sender, reply) => {
+ notifyNewMessage(message)
+ .then((apiResponse) => {
+ try {
+ reply(apiResponse);
+ } catch (e) {
+ logger.error(
+ "sending response to frontend failed",
+ message,
+ apiResponse,
+ e,
+ );
+ }
+ })
+ .catch((e) => {
+ logger.error("notify to background failed", e);
+ });
// keep the connection open
return true;
@@ -392,10 +417,20 @@ function registerReloadOnNewVersion(): void {
});
}
-function redirectTabToWalletPage(tabId: number, page: string): void {
+// async function redirectCurrentTabToWalletPage(page: string): Promise<void> {
+// let queryOptions = { active: true, lastFocusedWindow: true };
+// let [tab] = await chrome.tabs.query(queryOptions);
+
+// return redirectTabToWalletPage(tab.id!, page);
+// }
+
+async function redirectTabToWalletPage(
+ tabId: number,
+ page: string,
+): Promise<void> {
const url = chrome.runtime.getURL(`/static/wallet.html#${page}`);
logger.trace("redirecting tabId: ", tabId, " to: ", url);
- chrome.tabs.update(tabId, { url });
+ await chrome.tabs.update(tabId, { url });
}
interface WalletVersion {
@@ -408,101 +443,6 @@ function getWalletWebExVersion(): WalletVersion {
return manifestData;
}
-function registerTalerHeaderListener(
- callback: (tabId: number, url: string) => void,
-): void {
- logger.trace("setting up header listener");
-
- function headerListener(
- details: chrome.webRequest.WebResponseHeadersDetails,
- ): void {
- if (chrome.runtime.lastError) {
- logger.error(JSON.stringify(chrome.runtime.lastError));
- return;
- }
- if (
- details.statusCode === 402 ||
- details.statusCode === 202 ||
- details.statusCode === 200
- ) {
- const values = (details.responseHeaders || [])
- .filter((h) => h.name.toLowerCase() === "taler")
- .map((h) => h.value)
- .filter((value): value is string => !!value);
- if (values.length > 0) {
- logger.info(
- `Found a Taler URI in a response header for the request ${details.url} from tab ${details.tabId}`,
- );
- callback(details.tabId, values[0]);
- }
- }
- return;
- }
-
- async function tabListener(
- tabId: number,
- info: chrome.tabs.TabChangeInfo,
- ): Promise<void> {
- if (tabId < 0) return;
- const tabLocationHasBeenUpdated = info.status === "complete"
- const tabTitleHasBeenUpdated = info.title !== undefined
- if (tabLocationHasBeenUpdated || tabTitleHasBeenUpdated) {
- const uri = await findTalerUriInTab(tabId);
- if (!uri) return;
- logger.info(`Found a Taler URI in the tab ${tabId}`);
- callback(tabId, uri);
- }
- }
-
- const prevHeaderListener = currentHeaderListener;
- const prevTabListener = currentTabListener;
-
- getPermissionsApi()
- .containsHostPermissions()
- .then((result) => {
- //if there is a handler already, remove it
- if (
- prevHeaderListener &&
- chrome?.webRequest?.onHeadersReceived?.hasListener(prevHeaderListener)
- ) {
- chrome.webRequest.onHeadersReceived.removeListener(prevHeaderListener);
- }
- if (
- prevTabListener &&
- chrome?.tabs?.onUpdated?.hasListener(prevTabListener)
- ) {
- chrome.tabs.onUpdated.removeListener(prevTabListener);
- }
-
- //if the result was positive, add the headerListener
- if (result) {
- const headersEvent:
- | chrome.webRequest.WebResponseHeadersEvent
- | undefined = chrome?.webRequest?.onHeadersReceived;
- if (headersEvent) {
- headersEvent.addListener(headerListener, { urls: ["<all_urls>"] }, [
- "responseHeaders",
- ]);
- currentHeaderListener = headerListener;
- }
-
- const tabsEvent: chrome.tabs.TabUpdatedEvent | undefined =
- chrome?.tabs?.onUpdated;
- if (tabsEvent) {
- tabsEvent.addListener(tabListener);
- currentTabListener = tabListener;
- }
- }
-
- //notify the browser about this change, this operation is expensive
- chrome?.webRequest?.handlerBehaviorChanged(() => {
- if (chrome.runtime.lastError) {
- logger.error(JSON.stringify(chrome.runtime.lastError));
- }
- });
- });
-}
-
const alertIcons = {
"16": "/static/img/taler-alert-16.png",
"19": "/static/img/taler-alert-19.png",
@@ -680,7 +620,7 @@ function registerOnInstalled(callback: () => void): void {
if (details.reason === chrome.runtime.OnInstalledReason.INSTALL) {
callback();
}
- registerIconChangeOnTalerContent();
+ await registerIconChangeOnTalerContent();
});
}
@@ -728,7 +668,7 @@ async function findTalerUriInTab(tabId: number): Promise<string | undefined> {
return;
}
} else {
- return new Promise((resolve, reject) => {
+ return new Promise((resolve) => {
//manifest v2
chrome.tabs.executeScript(
tabId,
@@ -754,25 +694,27 @@ async function findTalerUriInTab(tabId: number): Promise<string | undefined> {
}
}
-async function timeout(ms: number): Promise<void> {
- return new Promise((resolve) => setTimeout(resolve, ms));
-}
+// async function timeout(ms: number): Promise<void> {
+// return new Promise((resolve) => setTimeout(resolve, ms));
+// }
async function findTalerUriInClipboard(): Promise<string | undefined> {
- try {
- //It looks like clipboard promise does not return, so we need a timeout
- const textInClipboard = await Promise.any([
- timeout(100),
- window.navigator.clipboard.readText(),
- ]);
- if (!textInClipboard) return;
- return textInClipboard.startsWith("taler://") ||
- textInClipboard.startsWith("taler+http://")
- ? textInClipboard
- : undefined;
- } catch (e) {
- logger.error("could not read clipboard", e);
- return undefined;
- }
+ //FIXME: add clipboard feature
+ // try {
+ // //It looks like clipboard promise does not return, so we need a timeout
+ // const textInClipboard = await Promise.any([
+ // timeout(100),
+ // window.navigator.clipboard.readText(),
+ // ]);
+ // if (!textInClipboard) return;
+ // return textInClipboard.startsWith("taler://") ||
+ // textInClipboard.startsWith("taler+http://")
+ // ? textInClipboard
+ // : undefined;
+ // } catch (e) {
+ // logger.error("could not read clipboard", e);
+ // return undefined;
+ // }
+ return undefined;
}
async function findTalerUriInActiveTab(): Promise<string | undefined> {
@@ -780,3 +722,25 @@ async function findTalerUriInActiveTab(): Promise<string | undefined> {
if (!tab || tab.id === undefined) return;
return findTalerUriInTab(tab.id);
}
+
+function listenNetworkConnectionState(
+ notify: (state: "on" | "off") => void,
+): () => void {
+ function notifyOffline() {
+ notify("off");
+ }
+ function notifyOnline() {
+ notify("on");
+ }
+ notify(window.navigator.onLine ? "on" : "off");
+ window.addEventListener("offline", notifyOffline);
+ window.addEventListener("online", notifyOnline);
+ return () => {
+ window.removeEventListener("offline", notifyOffline);
+ window.removeEventListener("online", notifyOnline);
+ };
+}
+
+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 8a410b062..d6e743147 100644
--- a/packages/taler-wallet-webextension/src/platform/dev.ts
+++ b/packages/taler-wallet-webextension/src/platform/dev.ts
@@ -14,79 +14,97 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { CoreApiResponse } from "@gnu-taler/taler-util";
-import { MessageFromBackend, PlatformAPI } from "./api.js";
+import { Logger, TalerUri } from "@gnu-taler/taler-util";
+import { WalletOperations } from "@gnu-taler/taler-wallet-core";
+import { BackgroundOperations } from "../wxApi.js";
+import {
+ BackgroundPlatformAPI,
+ ForegroundPlatformAPI,
+ MessageFromBackend,
+ MessageFromFrontend,
+ MessageResponse,
+ defaultSettings,
+} from "./api.js";
-const frames = ["popup", "wallet"];
+const logger = new Logger("dev.ts");
-const api: PlatformAPI = {
+const api: BackgroundPlatformAPI & ForegroundPlatformAPI = {
+ runningOnPrivateMode: () => false,
isFirefox: () => false,
+ getSettingsFromStorage: () => Promise.resolve(defaultSettings),
keepAlive: (cb: VoidFunction) => cb(),
findTalerUriInActiveTab: async () => undefined,
findTalerUriInClipboard: async () => undefined,
- containsTalerHeaderListener: () => {
- return true;
- },
+ listenNetworkConnectionState,
+ openNewURLFromPopup: () => undefined,
+ triggerWalletEvent: () => undefined,
+ setAlertedIcon: () => undefined,
+ setNormalIcon : () => undefined,
getPermissionsApi: () => ({
- addPermissionsListener: () => undefined,
- containsHostPermissions: async () => true,
- removeHostPermissions: async () => false,
- requestHostPermissions: async () => false,
containsClipboardPermissions: async () => true,
removeClipboardPermissions: async () => false,
requestClipboardPermissions: async () => false,
}),
+
getWalletWebExVersion: () => ({
version: "none",
}),
- notifyWhenAppIsReady: (fn: () => void) => {
- let total = frames.length;
- function waitAndNotify(): void {
- total--;
- if (total < 1) {
- fn();
- }
- }
- frames.forEach((f) => {
- const theFrame = window.frames[f as any];
- if (theFrame.location.href === "about:blank") {
- waitAndNotify();
- } else {
- theFrame.addEventListener("load", waitAndNotify);
+ notifyWhenAppIsReady: () => {
+ const knownFrames = ["popup", "wallet"];
+ let total = knownFrames.length;
+ return new Promise((fn) => {
+ function waitAndNotify(): void {
+ total--;
+ logger.trace(`waitAndNotify ${total}`);
+ if (total < 1) {
+ fn();
+ }
}
+ knownFrames.forEach((f) => {
+ const theFrame = window.frames[f as any];
+ if (theFrame.location.href === "about:blank") {
+ waitAndNotify();
+ } else {
+ theFrame.addEventListener("load", waitAndNotify);
+ }
+ });
});
},
openWalletPage: (page: string) => {
- window.frames["wallet" as any].location = `/wallet.html#${page}`;
+ // @ts-ignore
+ window.parent.redirectWallet(`wallet.html#${page}`);
},
openWalletPageFromPopup: (page: string) => {
- window.parent.frames["wallet" as any].location = `/wallet.html#${page}`;
- window.location.href = "about:blank";
+ // @ts-ignore
+ window.parent.redirectWallet(`wallet.html#${page}`);
+ // close the popup
+ // @ts-ignore
+ window.parent.closePopup();
},
- openWalletURIFromPopup: (page: string) => {
+ openWalletURIFromPopup: (page: TalerUri) => {
alert("openWalletURIFromPopup not implemented yet");
},
redirectTabToWalletPage: (tabId: number, page: string) => {
alert("redirectTabToWalletPage not implemented yet");
},
-
registerAllIncomingConnections: () => undefined,
- registerOnInstalled: (fn: () => void) => undefined,
+ registerOnInstalled: () => Promise.resolve(),
registerReloadOnNewVersion: () => undefined,
- registerTalerHeaderListener: () => undefined,
useServiceWorkerAsBackgroundProcess: () => false,
listenToAllChannels: (
- fn: (m: any, s: any, c: (r: CoreApiResponse) => void) => void,
+ notifyNewMessage: (message: any) => Promise<MessageResponse>,
) => {
window.addEventListener(
"message",
(event: MessageEvent<IframeMessageType>) => {
if (event.data.type !== "command") return;
const sender = event.data.header.replyMe;
- fn(event.data.body, sender, (resp: CoreApiResponse) => {
+
+ notifyNewMessage(event.data.body as any).then((resp) => {
+ logger.trace(`listenToAllChannels: from ${sender}`, event);
if (event.source) {
const msg: IframeMessageResponse = {
type: "response",
@@ -113,6 +131,7 @@ const api: PlatformAPI = {
},
listenToWalletBackground: (onNewMessage: (m: MessageFromBackend) => void) => {
function listener(event: MessageEvent<IframeMessageType>): void {
+ logger.trace(`listenToWalletBackground: `, event);
if (event.data.type !== "notification") return;
onNewMessage(event.data.body);
}
@@ -121,14 +140,20 @@ const api: PlatformAPI = {
window.parent.removeEventListener("message", listener);
};
},
- sendMessageToWalletBackground: async (operation: string, payload: any) => {
+
+ sendMessageToBackground: async <
+ Op extends WalletOperations | BackgroundOperations,
+ >(
+ payload: MessageFromFrontend<Op>,
+ ): Promise<MessageResponse> => {
const replyMe = `reply-${Math.floor(Math.random() * 100000)}`;
const message: IframeMessageCommand = {
type: "command",
header: { replyMe },
- body: { operation, payload, id: "(none)" },
+ body: payload,
};
- window.parent.postMessage(message);
+
+ logger.trace(`sendMessageToBackground: `, message);
return new Promise((res, rej) => {
function listener(event: MessageEvent<IframeMessageType>): void {
@@ -142,6 +167,7 @@ const api: PlatformAPI = {
window.parent.removeEventListener("message", listener);
}
window.parent.addEventListener("message", listener, {});
+ window.parent.postMessage(message);
});
},
};
@@ -150,6 +176,7 @@ type IframeMessageType =
| IframeMessageNotification
| IframeMessageResponse
| IframeMessageCommand;
+
interface IframeMessageNotification {
type: "notification";
header: Record<string, never>;
@@ -160,7 +187,7 @@ interface IframeMessageResponse {
header: {
responseId: string;
};
- body: CoreApiResponse;
+ body: MessageResponse;
}
interface IframeMessageCommand {
@@ -168,11 +195,24 @@ interface IframeMessageCommand {
header: {
replyMe: string;
};
- body: {
- operation: any;
- id: string;
- payload: any;
- };
+ body: MessageFromFrontend<any>;
}
export default api;
+
+function listenNetworkConnectionState(
+ notify: (state: "on" | "off") => void,
+): () => void {
+ function notifyOffline() {
+ notify("off");
+ }
+ function notifyOnline() {
+ notify("on");
+ }
+ window.addEventListener("offline", notifyOffline);
+ window.addEventListener("online", notifyOnline);
+ return () => {
+ window.removeEventListener("offline", notifyOffline);
+ window.removeEventListener("online", notifyOnline);
+ };
+}
diff --git a/packages/taler-wallet-webextension/src/platform/firefox.ts b/packages/taler-wallet-webextension/src/platform/firefox.ts
index 943168956..3d67423fd 100644
--- a/packages/taler-wallet-webextension/src/platform/firefox.ts
+++ b/packages/taler-wallet-webextension/src/platform/firefox.ts
@@ -14,19 +14,24 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { CrossBrowserPermissionsApi, Permissions, PlatformAPI } from "./api.js";
+import {
+ BackgroundPlatformAPI,
+ CrossBrowserPermissionsApi,
+ ForegroundPlatformAPI,
+ Permissions,
+ Settings,
+ defaultSettings,
+} from "./api.js";
import chromePlatform, {
- containsHostPermissions as chromeHostContains,
- removeHostPermissions as chromeHostRemove,
- requestHostPermissions as chromeHostRequest,
containsClipboardPermissions as chromeClipContains,
removeClipboardPermissions as chromeClipRemove,
requestClipboardPermissions as chromeClipRequest,
} from "./chrome.js";
-const api: PlatformAPI = {
+const api: BackgroundPlatformAPI & ForegroundPlatformAPI = {
...chromePlatform,
isFirefox,
+ getSettingsFromStorage,
getPermissionsApi,
notifyWhenAppIsReady,
redirectTabToWalletPage,
@@ -39,32 +44,42 @@ function isFirefox(): boolean {
return true;
}
-function addPermissionsListener(callback: (p: Permissions) => void): void {
- console.log("addPermissionListener is not supported for Firefox");
-}
-
function getPermissionsApi(): CrossBrowserPermissionsApi {
return {
- addPermissionsListener,
- containsHostPermissions: chromeHostContains,
- requestHostPermissions: chromeHostRequest,
- removeHostPermissions: chromeHostRemove,
containsClipboardPermissions: chromeClipContains,
removeClipboardPermissions: chromeClipRemove,
requestClipboardPermissions: chromeClipRequest,
};
}
+async function getSettingsFromStorage(): Promise<Settings> {
+ //@ts-ignore
+ const data = await browser.storage.local.get("wallet-settings");
+ if (!data) return defaultSettings;
+ const settings = data["wallet-settings"];
+ if (!settings) return defaultSettings;
+ try {
+ const parsed = JSON.parse(settings);
+ return parsed;
+ } catch (e) {
+ return defaultSettings;
+ }
+}
+
/**
*
* @param callback function to be called
*/
-function notifyWhenAppIsReady(callback: () => void): void {
- if (chrome.runtime && chrome.runtime.getManifest().manifest_version === 3) {
- callback();
- } else {
- window.addEventListener("load", callback);
- }
+function notifyWhenAppIsReady(): Promise<void> {
+ return new Promise((resolve) => {
+ if (chrome.runtime && chrome.runtime.getManifest().manifest_version === 3) {
+ resolve();
+ } else {
+ window.addEventListener("load", () => {
+ resolve();
+ });
+ }
+ });
}
function redirectTabToWalletPage(tabId: number, page: string): void {
diff --git a/packages/taler-wallet-webextension/src/platform/foreground.ts b/packages/taler-wallet-webextension/src/platform/foreground.ts
new file mode 100644
index 000000000..ae8dc8a95
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/platform/foreground.ts
@@ -0,0 +1,22 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { ForegroundPlatformAPI } from "./api.js";
+
+export let platform: ForegroundPlatformAPI = undefined as any;
+export function setupPlatform(impl: ForegroundPlatformAPI): void {
+ platform = impl;
+}
diff --git a/packages/taler-wallet-webextension/src/popup/Application.tsx b/packages/taler-wallet-webextension/src/popup/Application.tsx
index 8186c6790..cbb9b50b2 100644
--- a/packages/taler-wallet-webextension/src/popup/Application.tsx
+++ b/packages/taler-wallet-webextension/src/popup/Application.tsx
@@ -20,29 +20,40 @@
* @author sebasjm
*/
+import {
+ TranslationProvider,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
import { createHashHistory } from "history";
-import { Fragment, h, VNode } from "preact";
-import Router, { route, Route } from "preact-router";
-import { Match } from "preact-router/match";
+import { ComponentChildren, Fragment, h, VNode } from "preact";
+import { route, Route, Router } from "preact-router";
import { useEffect, useState } from "preact/hooks";
import PendingTransactions from "../components/PendingTransactions.js";
import { PopupBox } from "../components/styled/index.js";
-import { DevContextProvider } from "../context/devContext.js";
+import { AlertProvider } from "../context/alert.js";
import { IoCProviderForRuntime } from "../context/iocContext.js";
-import {
- TranslationProvider,
- useTranslationContext,
-} from "../context/translation.js";
import { useTalerActionURL } from "../hooks/useTalerActionURL.js";
-import { Pages, PopupNavBar } from "../NavigationBar.js";
-import { platform } from "../platform/api.js";
+import { strings } from "../i18n/strings.js";
+import { Pages, PopupNavBar, PopupNavBarOptions } from "../NavigationBar.js";
+import { platform } from "../platform/foreground.js";
import { BackupPage } from "../wallet/BackupPage.js";
import { ProviderDetailPage } from "../wallet/ProviderDetailPage.js";
import { BalancePage } from "./BalancePage.js";
import { TalerActionFound } from "./TalerActionFound.js";
-function CheckTalerActionComponent(): VNode {
- const [action] = useTalerActionURL();
+export function Application(): VNode {
+ return (
+ <TranslationProvider source={strings}>
+ <IoCProviderForRuntime>
+ <ApplicationView />
+ </IoCProviderForRuntime>
+ </TranslationProvider>
+ );
+}
+function ApplicationView(): VNode {
+ const hash_history = createHashHistory();
+
+ const [action, setDismissed] = useTalerActionURL();
const actionUri = action?.uri;
@@ -52,116 +63,115 @@ function CheckTalerActionComponent(): VNode {
}
}, [actionUri]);
- return <Fragment />;
-}
+ async function redirectToTxInfo(tid: string): Promise<void> {
+ redirectTo(Pages.balanceTransaction({ tid }));
+ }
+
+ function redirectToURL(str: string): void {
+ platform.openNewURLFromPopup(new URL(str))
+ }
-export function Application(): VNode {
- const hash_history = createHashHistory();
return (
- <TranslationProvider>
- <DevContextProvider>
- {({ devMode }: { devMode: boolean }) => (
- <IoCProviderForRuntime>
- <PendingTransactions
- goToTransaction={(tid: string) =>
- redirectTo(Pages.balanceTransaction({ tid }))
+ <Router history={hash_history}>
+ <Route
+ path={Pages.balance}
+ component={() => (
+ <PopupTemplate path="balance" goToTransaction={redirectToTxInfo} goToURL={redirectToURL}>
+ <BalancePage
+ goToWalletManualWithdraw={() => redirectTo(Pages.receiveCash({}))}
+ goToWalletDeposit={(currency: string) =>
+ redirectTo(Pages.sendCash({ amount: `${currency}:0` }))
+ }
+ goToWalletHistory={(currency: string) =>
+ redirectTo(Pages.balanceHistory({ currency }))
}
/>
- <Match>
- {({ path }: { path: string }) => <PopupNavBar path={path} />}
- </Match>
- <CheckTalerActionComponent />
- <PopupBox devMode={devMode}>
- <Router history={hash_history}>
- <Route
- path={Pages.balance}
- component={BalancePage}
- goToWalletManualWithdraw={() =>
- redirectTo(Pages.receiveCash({}))
- }
- goToWalletDeposit={(currency: string) =>
- redirectTo(Pages.sendCash({ amount: `${currency}:0` }))
- }
- goToWalletHistory={(currency: string) =>
- redirectTo(Pages.balanceHistory({ currency }))
- }
- />
-
- <Route
- path={Pages.cta.pattern}
- component={function Action({ action }: { action: string }) {
- const [, setDismissed] = useTalerActionURL();
-
- return (
- <TalerActionFound
- url={decodeURIComponent(action)}
- onDismiss={() => {
- setDismissed(true);
- return redirectTo(Pages.balance);
- }}
- />
- );
- }}
- />
-
- <Route
- path={Pages.backup}
- component={BackupPage}
- onAddProvider={() => redirectTo(Pages.backupProviderAdd)}
- />
- <Route
- path={Pages.backupProviderDetail.pattern}
- component={ProviderDetailPage}
- onBack={() => redirectTo(Pages.backup)}
- />
-
- <Route
- path={Pages.balanceTransaction.pattern}
- component={RedirectToWalletPage}
- />
- <Route
- path={Pages.ctaWithdrawManual.pattern}
- component={RedirectToWalletPage}
- />
- <Route
- path={Pages.balanceDeposit.pattern}
- component={RedirectToWalletPage}
- />
- <Route
- path={Pages.balanceHistory.pattern}
- component={RedirectToWalletPage}
- />
- <Route
- path={Pages.backupProviderAdd}
- component={RedirectToWalletPage}
- />
- <Route
- path={Pages.receiveCash.pattern}
- component={RedirectToWalletPage}
- />
- <Route
- path={Pages.sendCash.pattern}
- component={RedirectToWalletPage}
- />
- <Route path={Pages.qr} component={RedirectToWalletPage} />
- <Route path={Pages.settings} component={RedirectToWalletPage} />
- <Route
- path={Pages.settingsExchangeAdd.pattern}
- component={RedirectToWalletPage}
- />
- <Route path={Pages.dev} component={RedirectToWalletPage} />
- <Route
- path={Pages.notifications}
- component={RedirectToWalletPage}
- />
-
- <Route default component={Redirect} to={Pages.balance} />
- </Router>
- </PopupBox>
- </IoCProviderForRuntime>
+ </PopupTemplate>
)}
- </DevContextProvider>
- </TranslationProvider>
+ />
+
+ <Route
+ path={Pages.cta.pattern}
+ component={function Action({ action }: { action: string }) {
+ // const [, setDismissed] = useTalerActionURL();
+
+ return (
+ <PopupTemplate goToURL={redirectToURL}>
+ <TalerActionFound
+ url={decodeURIComponent(action)}
+ onDismiss={() => {
+ setDismissed(true);
+ return redirectTo(Pages.balance);
+ }}
+ />
+ </PopupTemplate>
+ );
+ }}
+ />
+
+ <Route
+ path={Pages.backup}
+ component={() => (
+ <PopupTemplate path="backup" goToTransaction={redirectToTxInfo} goToURL={redirectToURL}>
+ <BackupPage
+ onAddProvider={() => redirectTo(Pages.backupProviderAdd)}
+ />
+ </PopupTemplate>
+ )}
+ />
+ <Route
+ path={Pages.backupProviderDetail.pattern}
+ component={({ pid }: { pid: string }) => (
+ <PopupTemplate path="backup" goToURL={redirectToURL}>
+ <ProviderDetailPage
+ onPayProvider={(uri: string) =>
+ redirectTo(`${Pages.ctaPay}?talerPayUri=${uri}`)
+ }
+ onWithdraw={(amount: string) =>
+ redirectTo(Pages.receiveCash({ amount }))
+ }
+ pid={pid}
+ onBack={() => redirectTo(Pages.backup)}
+ />
+ </PopupTemplate>
+ )}
+ />
+
+ <Route
+ path={Pages.balanceTransaction.pattern}
+ component={RedirectToWalletPage}
+ />
+ <Route
+ path={Pages.ctaWithdrawManual.pattern}
+ component={RedirectToWalletPage}
+ />
+ <Route
+ path={Pages.balanceDeposit.pattern}
+ component={RedirectToWalletPage}
+ />
+ <Route
+ path={Pages.balanceHistory.pattern}
+ component={RedirectToWalletPage}
+ />
+ <Route path={Pages.backupProviderAdd} component={RedirectToWalletPage} />
+ <Route
+ path={Pages.receiveCash.pattern}
+ component={RedirectToWalletPage}
+ />
+ <Route path={Pages.sendCash.pattern} component={RedirectToWalletPage} />
+ <Route path={Pages.ctaPayTemplate} component={RedirectToWalletPage} />
+ <Route path={Pages.ctaPay} component={RedirectToWalletPage} />
+ <Route path={Pages.qr} component={RedirectToWalletPage} />
+ <Route path={Pages.settings} component={RedirectToWalletPage} />
+ <Route
+ path={Pages.settingsExchangeAdd.pattern}
+ component={RedirectToWalletPage}
+ />
+ <Route path={Pages.dev} component={RedirectToWalletPage} />
+ <Route path={Pages.notifications} component={RedirectToWalletPage} />
+
+ <Route default component={Redirect} to={Pages.balance} />
+ </Router>
);
}
@@ -195,3 +205,25 @@ function Redirect({ to }: { to: string }): null {
});
return null;
}
+
+function PopupTemplate({
+ path,
+ children,
+ goToTransaction,
+ goToURL,
+}: {
+ path?: PopupNavBarOptions;
+ children: ComponentChildren;
+ goToTransaction?: (id: string) => Promise<void>;
+ goToURL: (s: string) => void;
+}): VNode {
+ return (
+ <Fragment>
+ <PendingTransactions goToTransaction={goToTransaction} goToURL={goToURL} />
+ <PopupNavBar path={path} />
+ <PopupBox>
+ <AlertProvider>{children}</AlertProvider>
+ </PopupBox>
+ </Fragment>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/popup/Balance.stories.tsx b/packages/taler-wallet-webextension/src/popup/Balance.stories.tsx
index 8f3762c29..626ad4977 100644
--- a/packages/taler-wallet-webextension/src/popup/Balance.stories.tsx
+++ b/packages/taler-wallet-webextension/src/popup/Balance.stories.tsx
@@ -19,138 +19,223 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { createExample } from "../test-utils.js";
+import { AmountString, ScopeType } from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
import { BalanceView as TestedComponent } from "./BalancePage.js";
export default {
title: "balance",
};
-export const EmptyBalance = createExample(TestedComponent, {
+export const EmptyBalance = tests.createExample(TestedComponent, {
balances: [],
goToWalletManualWithdraw: {},
});
-export const SomeCoins = createExample(TestedComponent, {
+export const SomeCoins = tests.createExample(TestedComponent, {
balances: [
{
- available: "USD:10.5",
+ flags: [],
+ available: "USD:10.5" as AmountString,
hasPendingTransactions: false,
- pendingIncoming: "USD:0",
- pendingOutgoing: "USD:0",
+ pendingIncoming: "USD:0" as AmountString,
+ pendingOutgoing: "USD:0" as AmountString,
requiresUserInput: false,
+ scopeInfo: {
+ currency: "TESTKUDOS",
+ type: ScopeType.Auditor,
+ url: "asd",
+ },
},
],
addAction: {},
goToWalletManualWithdraw: {},
});
-export const SomeCoinsInTreeCurrencies = createExample(TestedComponent, {
+export const SomeCoinsInTreeCurrencies = tests.createExample(TestedComponent, {
balances: [
{
- available: "EUR:1",
+ flags: [],
+ available: "EUR:1" as AmountString,
hasPendingTransactions: false,
- pendingIncoming: "USD:0",
- pendingOutgoing: "USD:0",
+ pendingIncoming: "USD:0" as AmountString,
+ pendingOutgoing: "USD:0" as AmountString,
requiresUserInput: false,
+ scopeInfo: {
+ currency: "TESTKUDOS",
+ type: ScopeType.Auditor,
+ url: "asd",
+ },
},
{
- available: "TESTKUDOS:2000",
+ flags: [],
+ available: "TESTKUDOS:2000" as AmountString,
hasPendingTransactions: false,
- pendingIncoming: "USD:0",
- pendingOutgoing: "USD:0",
+ pendingIncoming: "USD:0" as AmountString,
+ pendingOutgoing: "USD:0" as AmountString,
requiresUserInput: false,
+ scopeInfo: {
+ currency: "TESTKUDOS",
+ type: ScopeType.Auditor,
+ url: "asd",
+ },
},
{
- available: "JPY:4",
+ flags: [],
+ available: "JPY:4" as AmountString,
hasPendingTransactions: false,
- pendingIncoming: "EUR:15",
- pendingOutgoing: "EUR:0",
+ pendingIncoming: "EUR:15" as AmountString,
+ pendingOutgoing: "EUR:0" as AmountString,
requiresUserInput: false,
+ scopeInfo: {
+ currency: "TESTKUDOS",
+ type: ScopeType.Auditor,
+ url: "asd",
+ },
},
],
goToWalletManualWithdraw: {},
addAction: {},
});
-export const NoCoinsInTreeCurrencies = createExample(TestedComponent, {
+export const NoCoinsInTreeCurrencies = tests.createExample(TestedComponent, {
balances: [
{
- available: "EUR:3",
+ flags: [],
+ available: "EUR:3" as AmountString,
hasPendingTransactions: false,
- pendingIncoming: "USD:0",
- pendingOutgoing: "USD:0",
+ pendingIncoming: "USD:0" as AmountString,
+ pendingOutgoing: "USD:0" as AmountString,
requiresUserInput: false,
+ scopeInfo: {
+ currency: "TESTKUDOS",
+ type: ScopeType.Auditor,
+ url: "asd",
+ },
},
{
- available: "USD:2",
+ flags: [],
+ available: "USD:2" as AmountString,
hasPendingTransactions: false,
- pendingIncoming: "USD:0",
- pendingOutgoing: "USD:0",
+ pendingIncoming: "USD:0" as AmountString,
+ pendingOutgoing: "USD:0" as AmountString,
requiresUserInput: false,
+ scopeInfo: {
+ currency: "TESTKUDOS",
+ type: ScopeType.Auditor,
+ url: "asd",
+ },
},
{
- available: "ARS:1",
+ flags: [],
+ available: "ARS:1" as AmountString,
hasPendingTransactions: false,
- pendingIncoming: "EUR:15",
- pendingOutgoing: "EUR:0",
+ pendingIncoming: "EUR:15" as AmountString,
+ pendingOutgoing: "EUR:0" as AmountString,
requiresUserInput: false,
+ scopeInfo: {
+ currency: "TESTKUDOS",
+ type: ScopeType.Auditor,
+ url: "asd",
+ },
},
],
goToWalletManualWithdraw: {},
addAction: {},
});
-export const SomeCoinsInFiveCurrencies = createExample(TestedComponent, {
+export const SomeCoinsInFiveCurrencies = tests.createExample(TestedComponent, {
balances: [
{
- available: "USD:0",
+ flags: [],
+ available: "USD:0" as AmountString,
hasPendingTransactions: false,
- pendingIncoming: "USD:0",
- pendingOutgoing: "USD:0",
+ pendingIncoming: "USD:0" as AmountString,
+ pendingOutgoing: "USD:0" as AmountString,
requiresUserInput: false,
+ scopeInfo: {
+ currency: "TESTKUDOS",
+ type: ScopeType.Auditor,
+ url: "asd",
+ },
},
{
- available: "ARS:13451",
+ flags: [],
+ available: "ARS:13451" as AmountString,
hasPendingTransactions: false,
- pendingIncoming: "USD:0",
- pendingOutgoing: "USD:0",
+ pendingIncoming: "USD:0" as AmountString,
+ pendingOutgoing: "USD:0" as AmountString,
requiresUserInput: false,
+ scopeInfo: {
+ currency: "TESTKUDOS",
+ type: ScopeType.Auditor,
+ url: "asd",
+ },
},
{
- available: "EUR:202.02",
+ flags: [],
+ available: "EUR:202.02" as AmountString,
hasPendingTransactions: false,
- pendingIncoming: "EUR:0",
- pendingOutgoing: "EUR:0",
+ pendingIncoming: "EUR:0" as AmountString,
+ pendingOutgoing: "EUR:0" as AmountString,
requiresUserInput: false,
+ scopeInfo: {
+ currency: "TESTKUDOS",
+ type: ScopeType.Auditor,
+ url: "asd",
+ },
},
{
- available: "JPY:0",
+ flags: [],
+ available: "JPY:0" as AmountString,
hasPendingTransactions: false,
- pendingIncoming: "EUR:0",
- pendingOutgoing: "EUR:0",
+ pendingIncoming: "EUR:0" as AmountString,
+ pendingOutgoing: "EUR:0" as AmountString,
requiresUserInput: false,
+ scopeInfo: {
+ currency: "TESTKUDOS",
+ type: ScopeType.Auditor,
+ url: "asd",
+ },
},
{
- available: "JPY:51223233",
+ flags: [],
+ available: "JPY:51223233" as AmountString,
hasPendingTransactions: false,
- pendingIncoming: "EUR:0",
- pendingOutgoing: "EUR:0",
+ pendingIncoming: "EUR:0" as AmountString,
+ pendingOutgoing: "EUR:0" as AmountString,
requiresUserInput: false,
+ scopeInfo: {
+ currency: "TESTKUDOS",
+ type: ScopeType.Auditor,
+ url: "asd",
+ },
},
{
- available: "DEMOKUDOS:6",
+ flags: [],
+ available: "DEMOKUDOS:6" as AmountString,
hasPendingTransactions: false,
- pendingIncoming: "USD:0",
- pendingOutgoing: "USD:0",
+ pendingIncoming: "USD:0" as AmountString,
+ pendingOutgoing: "USD:0" as AmountString,
requiresUserInput: false,
+ scopeInfo: {
+ currency: "TESTKUDOS",
+ type: ScopeType.Auditor,
+ url: "asd",
+ },
},
{
- available: "TESTKUDOS:6",
+ flags: [],
+ available: "TESTKUDOS:6" as AmountString,
hasPendingTransactions: false,
- pendingIncoming: "USD:5",
- pendingOutgoing: "USD:0",
+ pendingIncoming: "USD:5" as AmountString,
+ pendingOutgoing: "USD:0" as AmountString,
requiresUserInput: false,
+ scopeInfo: {
+ currency: "TESTKUDOS",
+ type: ScopeType.Auditor,
+ url: "asd",
+ },
},
],
goToWalletManualWithdraw: {},
diff --git a/packages/taler-wallet-webextension/src/popup/BalancePage.tsx b/packages/taler-wallet-webextension/src/popup/BalancePage.tsx
index 98c6d166c..93770312e 100644
--- a/packages/taler-wallet-webextension/src/popup/BalancePage.tsx
+++ b/packages/taler-wallet-webextension/src/popup/BalancePage.tsx
@@ -14,21 +14,30 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { Amounts, Balance, NotificationType } from "@gnu-taler/taler-util";
+import {
+ Amounts,
+ NotificationType,
+ WalletBalance,
+} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-import { Fragment, h, VNode } from "preact";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
import { useEffect, useState } from "preact/hooks";
import { BalanceTable } from "../components/BalanceTable.js";
+import { ErrorAlertView } from "../components/CurrentAlerts.js";
import { Loading } from "../components/Loading.js";
-import { LoadingError } from "../components/LoadingError.js";
import { MultiActionButton } from "../components/MultiActionButton.js";
-import { useTranslationContext } from "../context/translation.js";
-import { HookError, useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
+import {
+ ErrorAlert,
+ alertFromError,
+ useAlertContext,
+} from "../context/alert.js";
+import { useBackendContext } from "../context/backend.js";
+import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
import { Button } from "../mui/Button.js";
import { ButtonHandler } from "../mui/handlers.js";
-import { compose, StateViewMap } from "../utils/index.js";
+import { StateViewMap, compose } from "../utils/index.js";
import { AddNewActionView } from "../wallet/AddNewActionView.js";
-import { wxApi } from "../wxApi.js";
import { NoBalanceHelp } from "./NoBalanceHelp.js";
export interface Props {
@@ -47,7 +56,7 @@ export namespace State {
export interface Error {
status: "error";
- error: HookError;
+ error: ErrorAlert;
}
export interface Action {
@@ -59,7 +68,7 @@ export namespace State {
export interface Balances {
status: "balance";
error: undefined;
- balances: Balance[];
+ balances: WalletBalance[];
addAction: ButtonHandler;
goToWalletDeposit: (currency: string) => Promise<void>;
goToWalletHistory: (currency: string) => Promise<void>;
@@ -67,10 +76,14 @@ export namespace State {
}
}
-function useComponentState(
- { goToWalletDeposit, goToWalletHistory, goToWalletManualWithdraw }: Props,
- api: typeof wxApi,
-): State {
+function useComponentState({
+ goToWalletDeposit,
+ goToWalletHistory,
+ goToWalletManualWithdraw,
+}: Props): State {
+ const api = useBackendContext();
+ const { i18n } = useTranslationContext();
+ const { pushAlertOnError } = useAlertContext();
const [addingAction, setAddingAction] = useState(false);
const state = useAsyncAsHook(() =>
api.wallet.call(WalletApiOperation.GetBalances, {}),
@@ -78,7 +91,7 @@ function useComponentState(
useEffect(() =>
api.listener.onUpdateNotification(
- [NotificationType.WithdrawGroupFinished],
+ [NotificationType.TransactionStateTransition],
state?.retry,
),
);
@@ -92,7 +105,8 @@ function useComponentState(
if (state.hasError) {
return {
status: "error",
- error: state,
+ error: alertFromError( i18n,
+ i18n.str`Could not load the balance`, state),
};
}
if (addingAction) {
@@ -100,7 +114,7 @@ function useComponentState(
status: "action",
error: undefined,
cancel: {
- onClick: async () => setAddingAction(false),
+ onClick: pushAlertOnError(async () => setAddingAction(false)),
},
};
}
@@ -109,10 +123,10 @@ function useComponentState(
error: undefined,
balances: state.response.balances,
addAction: {
- onClick: async () => setAddingAction(true),
+ onClick: pushAlertOnError(async () => setAddingAction(true)),
},
goToWalletManualWithdraw: {
- onClick: goToWalletManualWithdraw,
+ onClick: pushAlertOnError(goToWalletManualWithdraw),
},
goToWalletDeposit,
goToWalletHistory,
@@ -121,27 +135,17 @@ function useComponentState(
const viewMapping: StateViewMap<State> = {
loading: Loading,
- error: ErrorView,
+ error: ErrorAlertView,
action: ActionView,
balance: BalanceView,
};
export const BalancePage = compose(
"BalancePage",
- (p: Props) => useComponentState(p, wxApi),
+ (p: Props) => useComponentState(p),
viewMapping,
);
-function ErrorView({ error }: State.Error): VNode {
- const { i18n } = useTranslationContext();
- return (
- <LoadingError
- title={<i18n.Translate>Could not load balance page</i18n.Translate>}
- error={error}
- />
- );
-}
-
function ActionView({ cancel }: State.Action): VNode {
return <AddNewActionView onCancel={cancel.onClick!} />;
}
@@ -150,7 +154,10 @@ export function BalanceView(state: State.Balances): VNode {
const { i18n } = useTranslationContext();
const currencyWithNonZeroAmount = state.balances
.filter((b) => !Amounts.isZero(b.available))
- .map((b) => b.available.split(":")[0]);
+ .map((b) => {
+ b.flags
+ return b.available.split(":")[0]
+ });
if (state.balances.length === 0) {
return (
@@ -177,7 +184,7 @@ export function BalanceView(state: State.Balances): VNode {
</Button>
{currencyWithNonZeroAmount.length > 0 && (
<MultiActionButton
- label={(s) => <i18n.Translate>Send {s}</i18n.Translate>}
+ label={(s) => i18n.str`Send ${s}`}
actions={currencyWithNonZeroAmount}
onClick={(c) => state.goToWalletDeposit(c)}
/>
diff --git a/packages/taler-wallet-webextension/src/popup/NoBalanceHelp.tsx b/packages/taler-wallet-webextension/src/popup/NoBalanceHelp.tsx
index 7d2e15726..c698066e7 100644
--- a/packages/taler-wallet-webextension/src/popup/NoBalanceHelp.tsx
+++ b/packages/taler-wallet-webextension/src/popup/NoBalanceHelp.tsx
@@ -15,7 +15,7 @@
*/
import { css } from "@linaria/core";
import { Fragment, h, VNode } from "preact";
-import { useTranslationContext } from "../context/translation.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Alert } from "../mui/Alert.js";
import { Button } from "../mui/Button.js";
import { ButtonHandler } from "../mui/handlers.js";
@@ -31,9 +31,10 @@ export function NoBalanceHelp({
goToWalletManualWithdraw: ButtonHandler;
}): VNode {
const { i18n } = useTranslationContext();
- return (
+ return (<Fragment>
+
<Paper class={margin}>
- <Alert title="Your wallet is empty." severity="info">
+ <Alert title={i18n.str`Your wallet is empty.`} severity="info">
<Button
fullWidth
color="info"
@@ -44,5 +45,9 @@ export function NoBalanceHelp({
</Button>
</Alert>
</Paper>
+ <a target="_bank" rel="noreferrer" href="https://demo.taler.net/" style={{ display: "block" }}>
+ <i18n.Translate>Try the demo bank and withdraw test money.</i18n.Translate> »
+ </a>
+ </Fragment>
);
}
diff --git a/packages/taler-wallet-webextension/src/popup/TalerActionFound.stories.tsx b/packages/taler-wallet-webextension/src/popup/TalerActionFound.stories.tsx
index 00293a690..0388664b3 100644
--- a/packages/taler-wallet-webextension/src/popup/TalerActionFound.stories.tsx
+++ b/packages/taler-wallet-webextension/src/popup/TalerActionFound.stories.tsx
@@ -19,33 +19,29 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { createExample } from "../test-utils.js";
+import * as tests from "@gnu-taler/web-util/testing";
import { TalerActionFound as TestedComponent } from "./TalerActionFound.js";
export default {
title: "TalerActionFound",
};
-export const PayAction = createExample(TestedComponent, {
+export const PayAction = tests.createExample(TestedComponent, {
url: "taler://pay/something",
});
-export const WithdrawalAction = createExample(TestedComponent, {
+export const WithdrawalAction = tests.createExample(TestedComponent, {
url: "taler://withdraw/something",
});
-export const TipAction = createExample(TestedComponent, {
- url: "taler://tip/something",
-});
-
-export const NotifyAction = createExample(TestedComponent, {
+export const NotifyAction = tests.createExample(TestedComponent, {
url: "taler://notify-reserve/something",
});
-export const RefundAction = createExample(TestedComponent, {
+export const RefundAction = tests.createExample(TestedComponent, {
url: "taler://refund/something",
});
-export const InvalidAction = createExample(TestedComponent, {
+export const InvalidAction = tests.createExample(TestedComponent, {
url: "taler://something/asd",
});
diff --git a/packages/taler-wallet-webextension/src/popup/TalerActionFound.tsx b/packages/taler-wallet-webextension/src/popup/TalerActionFound.tsx
index dc2b8bfae..21373c7cd 100644
--- a/packages/taler-wallet-webextension/src/popup/TalerActionFound.tsx
+++ b/packages/taler-wallet-webextension/src/popup/TalerActionFound.tsx
@@ -19,23 +19,93 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { classifyTalerUri, TalerUriType } from "@gnu-taler/taler-util";
+import { parseTalerUri, TalerUri, TalerUriAction } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, h, VNode } from "preact";
import { Title } from "../components/styled/index.js";
-import { useTranslationContext } from "../context/translation.js";
import { Button } from "../mui/Button.js";
-import { platform } from "../platform/api.js";
+import { platform } from "../platform/foreground.js";
export interface Props {
url: string;
onDismiss: () => Promise<void>;
}
+function ContentByUriType({
+ uri,
+ onConfirm,
+}: {
+ uri: TalerUri;
+ onConfirm: () => Promise<void>;
+}) {
+ const { i18n } = useTranslationContext();
+ switch (uri.type) {
+ case TalerUriAction.WithdrawExchange:
+ case TalerUriAction.Withdraw:
+ return (
+ <div>
+ <p>
+ <i18n.Translate>This page has a withdrawal action.</i18n.Translate>
+ </p>
+ <Button variant="contained" color="success" onClick={onConfirm}>
+ <i18n.Translate>Open withdraw page</i18n.Translate>
+ </Button>
+ </div>
+ );
+
+ case TalerUriAction.PayTemplate:
+ case TalerUriAction.Pay:
+ return (
+ <div>
+ <p>
+ <i18n.Translate>This page has pay action.</i18n.Translate>
+ </p>
+ <Button variant="contained" color="success" onClick={onConfirm}>
+ <i18n.Translate>Open pay page</i18n.Translate>
+ </Button>
+ </div>
+ );
+
+ case TalerUriAction.Refund:
+ return (
+ <div>
+ <p>
+ <i18n.Translate>This page has a refund action.</i18n.Translate>
+ </p>
+ <Button variant="contained" color="success" onClick={onConfirm}>
+ <i18n.Translate>Open refund page</i18n.Translate>
+ </Button>
+ </div>
+ );
+ case TalerUriAction.AddExchange:
+ return (
+ <div>
+ <p>
+ <i18n.Translate>This page has a add exchange action.</i18n.Translate>
+ </p>
+ <Button variant="contained" color="success" onClick={onConfirm}>
+ <i18n.Translate>Open add exchange page</i18n.Translate>
+ </Button>
+ </div>
+ );
+
+ case TalerUriAction.DevExperiment:
+ case TalerUriAction.PayPull:
+ case TalerUriAction.PayPush:
+ case TalerUriAction.Restore:
+ return null;
+ default: {
+ const error: never = uri;
+ return null;
+ }
+ }
+}
+
export function TalerActionFound({ url, onDismiss }: Props): VNode {
- const uriType = classifyTalerUri(url);
+ const talerUri = parseTalerUri(url);
const { i18n } = useTranslationContext();
async function redirectToWallet(): Promise<void> {
- platform.openWalletURIFromPopup(url);
+ platform.openWalletURIFromPopup(talerUri!);
}
return (
<Fragment>
@@ -43,73 +113,16 @@ export function TalerActionFound({ url, onDismiss }: Props): VNode {
<Title>
<i18n.Translate>Taler Action</i18n.Translate>
</Title>
- {uriType === TalerUriType.TalerPay && (
- <div>
- <p>
- <i18n.Translate>This page has pay action.</i18n.Translate>
- </p>
- <Button
- variant="contained"
- color="success"
- onClick={redirectToWallet}
- >
- <i18n.Translate>Open pay page</i18n.Translate>
- </Button>
- </div>
- )}
- {uriType === TalerUriType.TalerWithdraw && (
- <div>
- <p>
- <i18n.Translate>
- This page has a withdrawal action.
- </i18n.Translate>
- </p>
- <Button
- variant="contained"
- color="success"
- onClick={redirectToWallet}
- >
- <i18n.Translate>Open withdraw page</i18n.Translate>
- </Button>
- </div>
- )}
- {uriType === TalerUriType.TalerTip && (
- <div>
- <p>
- <i18n.Translate>This page has a tip action.</i18n.Translate>
- </p>
- <Button
- variant="contained"
- color="success"
- onClick={redirectToWallet}
- >
- <i18n.Translate>Open tip page</i18n.Translate>
- </Button>
- </div>
- )}
- {uriType === TalerUriType.TalerRefund && (
- <div>
- <p>
- <i18n.Translate>This page has a refund action.</i18n.Translate>
- </p>
- <Button
- variant="contained"
- color="success"
- onClick={redirectToWallet}
- >
- <i18n.Translate>Open refund page</i18n.Translate>
- </Button>
- </div>
- )}
- {uriType === TalerUriType.Unknown && (
+ {!talerUri ? (
<div>
<p>
<i18n.Translate>
This page has a malformed taler uri.
</i18n.Translate>
</p>
- <p>{url}</p>
</div>
+ ) : (
+ <ContentByUriType uri={talerUri} onConfirm={redirectToWallet} />
)}
</section>
<footer>
diff --git a/packages/taler-wallet-webextension/src/popupEntryPoint.dev.tsx b/packages/taler-wallet-webextension/src/popupEntryPoint.dev.tsx
index e50bb8f49..f0bc81399 100644
--- a/packages/taler-wallet-webextension/src/popupEntryPoint.dev.tsx
+++ b/packages/taler-wallet-webextension/src/popupEntryPoint.dev.tsx
@@ -23,11 +23,10 @@
import { setupI18n } from "@gnu-taler/taler-util";
import { h, render } from "preact";
import { strings } from "./i18n/strings.js";
-import { setupPlatform } from "./platform/api.js";
+import { setupPlatform } from "./platform/foreground.js";
import devAPI from "./platform/dev.js";
import { Application } from "./popup/Application.js";
-console.log("Wallet setup for Dev API");
setupPlatform(devAPI);
function main(): void {
diff --git a/packages/taler-wallet-webextension/src/popupEntryPoint.tsx b/packages/taler-wallet-webextension/src/popupEntryPoint.tsx
index 1c2580310..08915ea96 100644
--- a/packages/taler-wallet-webextension/src/popupEntryPoint.tsx
+++ b/packages/taler-wallet-webextension/src/popupEntryPoint.tsx
@@ -23,7 +23,7 @@
import { setupI18n } from "@gnu-taler/taler-util";
import { h, render } from "preact";
import { strings } from "./i18n/strings.js";
-import { setupPlatform } from "./platform/api.js";
+import { setupPlatform } from "./platform/foreground.js";
import chromeAPI from "./platform/chrome.js";
import firefoxAPI from "./platform/firefox.js";
import { Application } from "./popup/Application.js";
@@ -32,10 +32,8 @@ import { Application } from "./popup/Application.js";
//switching in runtime
const isFirefox = typeof (window as any)["InstallTrigger"] !== "undefined";
if (isFirefox) {
- console.log("Wallet setup for Firefox API");
setupPlatform(firefoxAPI);
} else {
- console.log("Wallet setup for Chrome API");
setupPlatform(chromeAPI);
}
diff --git a/packages/taler-wallet-webextension/src/pwa/index.html b/packages/taler-wallet-webextension/src/pwa/index.html
new file mode 100644
index 000000000..c150ee68d
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pwa/index.html
@@ -0,0 +1,114 @@
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <link rel="manifest" href="./manifest.json" />
+ <style>
+ .overlay {
+ position: absolute;
+ top: 0px;
+ display: none;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(0, 0, 0, 0.5);
+ color: white;
+ justify-content: center;
+ }
+ .overlay > iframe {
+ margin: auto;
+ }
+ </style>
+ </head>
+ <body>
+ <script>
+ function openPopup() {
+ document.getElementById("popup-overlay").style.display = "flex";
+ window.frames["popup"].location = "popup.html";
+ }
+ function closePopup() {
+ document.getElementById("popup-overlay").style.display = "none";
+ }
+ function redirectWallet(url) {
+ window.frames["wallet"].location = url;
+ }
+ function openWallet() {
+ redirectWallet("wallet.html");
+ }
+ function closeWallet() {
+ redirectWallet("about:blank");
+ }
+ function reloadWallet() {
+ window.frames["wallet"].location.reload()
+ }
+ function openPage() {
+ window.frames["other"].location =
+ document.getElementById("page-url").value;
+ }
+ </script>
+ <button value="asd" onclick="openPopup()">open popup</button>
+ <button value="asd" onclick="closeWallet();openWallet()">
+ restart
+ </button>
+ <button value="asd" onclick="reloadWallet()">
+ refresh
+ </button>
+ <br />
+ <iframe
+ id="wallet-window"
+ name="wallet"
+ src="wallet.html"
+ style="height: calc(100% - 30px)"
+ width="850"
+ height="90%"
+ >
+ </iframe>
+ <!-- <input id="page-url" type="text" />
+ <button onclick="openPage()">open</button> -->
+ <!-- <a
+ href='javascript:void(window.frames["other"].location = "http://bank.taler:5882")'
+ >open local bank</a
+ >
+ <hr />
+ <iframe
+ id="other-window"
+ name="other"
+ src="http://bank.taler:5882"
+ width="100%"
+ height="325"
+ >
+ </iframe> -->
+ <div class="overlay" id="popup-overlay" onclick="closePopup()">
+
+ <iframe
+ id="popup-window"
+ name="popup"
+ src="about:blank"
+ width="500"
+ height="325"
+ >
+ </iframe>
+ </div>
+ <!-- <hr />
+ <iframe src="tests.html" name="wallet" width="800" height="100%"> </iframe> -->
+ <!-- <hr />
+ <iframe src="stories.html" name="wallet" width="800" height="100%"> -->
+ <script type="module" src="background.dev.js"></script>
+ <script type="module">
+ if ("serviceWorker" in navigator) {
+ try {
+ const registration = await navigator.serviceWorker.register("sw.js", {
+ scope: "/app/",
+ });
+ if (registration.installing) {
+ console.log("Service worker installing");
+ } else if (registration.waiting) {
+ console.log("Service worker installed");
+ } else if (registration.active) {
+ console.log("Service worker active");
+ }
+ } catch (error) {
+ console.error(`Registration failed with ${error}`);
+ }
+ }
+ </script>
+ </body>
+</html>
diff --git a/packages/taler-wallet-webextension/src/pwa/manifest.json b/packages/taler-wallet-webextension/src/pwa/manifest.json
new file mode 100644
index 000000000..adf27e43f
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pwa/manifest.json
@@ -0,0 +1,35 @@
+{
+ "name": "GNU Taler Wallet",
+ "description": "Privacy preserving and transparent payments",
+ "author": "GNU Taler Developers",
+ "version": "0.9.3.13",
+ "id": "gnu-taler-wallet-web-spa-development",
+ "version_name": "0.9.3-dev.13",
+ "display": "minimal-ui",
+ "start_url": "./",
+ "manifest_version": 3,
+ "minimum_chrome_version": "88",
+ "icons": [
+ {
+ "src": "./static/img/taler-logo-48.png",
+ "type": "image/png",
+ "sizes": "48x48"
+ },
+ {
+ "src": "./static/img/taler-logo-128.png",
+ "type": "image/png",
+ "sizes": "128x128"
+ },
+ {
+ "src": "./static/img/taler-logo-512.png",
+ "type": "image/png",
+ "sizes": "512x512"
+ }
+ ],
+ "protocol_handlers": [
+ {
+ "protocol": "web+taler",
+ "url": "./wallet.html?type=%s"
+ }
+ ]
+}
diff --git a/packages/taler-wallet-webextension/dev-html/popup.html b/packages/taler-wallet-webextension/src/pwa/popup.html
index 93a886d54..34d1d019c 100644
--- a/packages/taler-wallet-webextension/dev-html/popup.html
+++ b/packages/taler-wallet-webextension/src/pwa/popup.html
@@ -29,8 +29,8 @@
}
</style>
- <link rel="stylesheet" type="text/css" href="/dist/popupEntryPoint.css" />
- <script src="/dist/popupEntryPoint.dev.js"></script>
+ <link rel="stylesheet" type="text/css" href="popupEntryPoint.dev.css" />
+ <script type="module" src="popupEntryPoint.dev.js"></script>
</head>
<body>
diff --git a/packages/taler-wallet-webextension/src/pwa/static/font/import.css b/packages/taler-wallet-webextension/src/pwa/static/font/import.css
new file mode 100644
index 000000000..d726ebc5a
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pwa/static/font/import.css
@@ -0,0 +1,35 @@
+@font-face {
+ font-family: "Roboto";
+ font-style: italic;
+ font-weight: 400;
+ font-display: swap;
+ src: url(/static/font/roboto-italic-400.ttf) format("truetype");
+}
+@font-face {
+ font-family: "Roboto";
+ font-style: normal;
+ font-weight: 300;
+ font-display: swap;
+ src: url(/static/font/roboto-normal-300.ttf) format("truetype");
+}
+@font-face {
+ font-family: "Roboto";
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src: url(/static/font/roboto-normal-400.ttf) format("truetype");
+}
+@font-face {
+ font-family: "Roboto";
+ font-style: normal;
+ font-weight: 500;
+ font-display: swap;
+ src: url(/static/font/roboto-normal-500.ttf) format("truetype");
+}
+@font-face {
+ font-family: "Roboto";
+ font-style: normal;
+ font-weight: 700;
+ font-display: swap;
+ src: url(/static/font/roboto-normal-700.ttf) format("truetype");
+}
diff --git a/packages/taler-wallet-webextension/dev-html/static/font/roboto-italic-400.ttf b/packages/taler-wallet-webextension/src/pwa/static/font/roboto-italic-400.ttf
index 1e746d17f..1e746d17f 100644
--- a/packages/taler-wallet-webextension/dev-html/static/font/roboto-italic-400.ttf
+++ b/packages/taler-wallet-webextension/src/pwa/static/font/roboto-italic-400.ttf
Binary files differ
diff --git a/packages/taler-wallet-webextension/dev-html/static/font/roboto-normal-300.tff b/packages/taler-wallet-webextension/src/pwa/static/font/roboto-normal-300.tff
index ec821b577..ec821b577 100644
--- a/packages/taler-wallet-webextension/dev-html/static/font/roboto-normal-300.tff
+++ b/packages/taler-wallet-webextension/src/pwa/static/font/roboto-normal-300.tff
Binary files differ
diff --git a/packages/taler-wallet-webextension/dev-html/static/font/roboto-normal-400.ttf b/packages/taler-wallet-webextension/src/pwa/static/font/roboto-normal-400.ttf
index 9d4b32b47..9d4b32b47 100644
--- a/packages/taler-wallet-webextension/dev-html/static/font/roboto-normal-400.ttf
+++ b/packages/taler-wallet-webextension/src/pwa/static/font/roboto-normal-400.ttf
Binary files differ
diff --git a/packages/taler-wallet-webextension/dev-html/static/font/roboto-normal-500.ttf b/packages/taler-wallet-webextension/src/pwa/static/font/roboto-normal-500.ttf
index 4b4e1c656..4b4e1c656 100644
--- a/packages/taler-wallet-webextension/dev-html/static/font/roboto-normal-500.ttf
+++ b/packages/taler-wallet-webextension/src/pwa/static/font/roboto-normal-500.ttf
Binary files differ
diff --git a/packages/taler-wallet-webextension/dev-html/static/font/roboto-normal-700.ttf b/packages/taler-wallet-webextension/src/pwa/static/font/roboto-normal-700.ttf
index 58d877c58..58d877c58 100644
--- a/packages/taler-wallet-webextension/dev-html/static/font/roboto-normal-700.ttf
+++ b/packages/taler-wallet-webextension/src/pwa/static/font/roboto-normal-700.ttf
Binary files differ
diff --git a/packages/taler-wallet-webextension/src/pwa/static/img/taler-logo-128.png b/packages/taler-wallet-webextension/src/pwa/static/img/taler-logo-128.png
new file mode 100644
index 000000000..a2f0c22eb
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pwa/static/img/taler-logo-128.png
Binary files differ
diff --git a/packages/taler-wallet-webextension/src/pwa/static/img/taler-logo-2022.svg b/packages/taler-wallet-webextension/src/pwa/static/img/taler-logo-2022.svg
new file mode 100644
index 000000000..2ac2785b8
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pwa/static/img/taler-logo-2022.svg
@@ -0,0 +1,468 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="670"
+ height="300"
+ viewBox="0 0 201 90"
+ version="1.1"
+ id="svg8"
+ sodipodi:docname="taler-logo-2023.svg"
+ inkscape:version="0.92.3 (2405546, 2018-03-11)">
+ <metadata
+ id="metadata67">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs854">
+ <clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath20663">
+ <rect
+ style="fill:#00ff00;fill-opacity:1;fill-rule:evenodd;stroke:#df373a;stroke-width:0.103816;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect20665"
+ width="164.73636"
+ height="53.465477"
+ x="12.38413"
+ y="263.48923" />
+ </clipPath>
+ <clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath20667">
+ <rect
+ style="fill:#00ff00;fill-opacity:1;fill-rule:evenodd;stroke:#df373a;stroke-width:0.103816;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect20669"
+ width="164.73636"
+ height="53.465477"
+ x="12.38413"
+ y="263.48923" />
+ </clipPath>
+ <clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath20671">
+ <rect
+ style="fill:#00ff00;fill-opacity:1;fill-rule:evenodd;stroke:#df373a;stroke-width:0.103816;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect20673"
+ width="164.73636"
+ height="53.465477"
+ x="12.38413"
+ y="263.48923" />
+ </clipPath>
+ <clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath20675">
+ <rect
+ style="fill:#00ff00;fill-opacity:1;fill-rule:evenodd;stroke:#df373a;stroke-width:0.103816;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect20677"
+ width="164.73636"
+ height="53.465477"
+ x="12.38413"
+ y="263.48923" />
+ </clipPath>
+ <clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath20679">
+ <rect
+ style="fill:#00ff00;fill-opacity:1;fill-rule:evenodd;stroke:#df373a;stroke-width:0.103816;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect20681"
+ width="164.73636"
+ height="53.465477"
+ x="12.38413"
+ y="263.48923" />
+ </clipPath>
+ <clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath20683">
+ <rect
+ style="fill:#00ff00;fill-opacity:1;fill-rule:evenodd;stroke:#df373a;stroke-width:0.103816;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect20685"
+ width="164.73636"
+ height="53.465477"
+ x="12.38413"
+ y="263.48923" />
+ </clipPath>
+ <clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath20687">
+ <rect
+ style="fill:#00ff00;fill-opacity:1;fill-rule:evenodd;stroke:#df373a;stroke-width:0.103816;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect20689"
+ width="164.73636"
+ height="53.465477"
+ x="12.38413"
+ y="263.48923" />
+ </clipPath>
+ <clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath20691">
+ <rect
+ style="fill:#00ff00;fill-opacity:1;fill-rule:evenodd;stroke:#df373a;stroke-width:0.103816;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect20693"
+ width="164.73636"
+ height="53.465477"
+ x="12.38413"
+ y="263.48923" />
+ </clipPath>
+ <clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath20695">
+ <rect
+ style="fill:#00ff00;fill-opacity:1;fill-rule:evenodd;stroke:#df373a;stroke-width:0.103816;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect20697"
+ width="164.73636"
+ height="53.465477"
+ x="12.38413"
+ y="263.48923" />
+ </clipPath>
+ <clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath20699">
+ <rect
+ style="fill:#00ff00;fill-opacity:1;fill-rule:evenodd;stroke:#df373a;stroke-width:0.103816;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect20701"
+ width="164.73636"
+ height="53.465477"
+ x="12.38413"
+ y="263.48923" />
+ </clipPath>
+ <clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath20703">
+ <rect
+ style="fill:#00ff00;fill-opacity:1;fill-rule:evenodd;stroke:#df373a;stroke-width:0.103816;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect20705"
+ width="164.73636"
+ height="53.465477"
+ x="12.38413"
+ y="263.48923" />
+ </clipPath>
+ <clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath20707">
+ <rect
+ style="opacity:1;fill:#00ff00;fill-opacity:1;stroke:#df373a;stroke-width:0.103816;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect20709"
+ width="164.73636"
+ height="53.465477"
+ x="-16.523348"
+ y="98.188889" />
+ </clipPath>
+ <clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath20711">
+ <rect
+ style="opacity:1;fill:#00ff00;fill-opacity:1;stroke:#df373a;stroke-width:0.103816;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect20713"
+ width="164.73636"
+ height="53.465477"
+ x="-16.523348"
+ y="98.188889" />
+ </clipPath>
+ <clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath20715">
+ <rect
+ style="opacity:1;fill:#00ff00;fill-opacity:1;stroke:#df373a;stroke-width:0.103816;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect20717"
+ width="164.73636"
+ height="53.465477"
+ x="-16.523348"
+ y="98.188889" />
+ </clipPath>
+ <clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath20719">
+ <rect
+ style="opacity:1;fill:#00ff00;fill-opacity:1;stroke:#df373a;stroke-width:0.103816;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect20721"
+ width="164.73636"
+ height="53.465477"
+ x="-16.523348"
+ y="98.188889" />
+ </clipPath>
+ <clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath20723">
+ <rect
+ style="opacity:1;fill:#00ff00;fill-opacity:1;stroke:#df373a;stroke-width:0.103816;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect20725"
+ width="164.73636"
+ height="53.465477"
+ x="-16.523348"
+ y="98.188889" />
+ </clipPath>
+ <clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath20727">
+ <rect
+ style="opacity:1;fill:#00ff00;fill-opacity:1;stroke:#df373a;stroke-width:0.103816;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect20729"
+ width="164.73636"
+ height="53.465477"
+ x="-16.523348"
+ y="98.188889" />
+ </clipPath>
+ <clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath20731">
+ <rect
+ style="opacity:1;fill:#00ff00;fill-opacity:1;stroke:#df373a;stroke-width:0.103816;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect20733"
+ width="164.73636"
+ height="53.465477"
+ x="-16.523348"
+ y="98.188889" />
+ </clipPath>
+ <clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath20735">
+ <rect
+ style="opacity:1;fill:#00ff00;fill-opacity:1;stroke:#df373a;stroke-width:0.103816;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect20737"
+ width="164.73636"
+ height="53.465477"
+ x="-16.523348"
+ y="98.188889" />
+ </clipPath>
+ </defs>
+ <sodipodi:namedview
+ id="namedview852"
+ pagecolor="#000000"
+ bordercolor="#cccccc"
+ borderopacity="1"
+ inkscape:pageshadow="0"
+ inkscape:pageopacity="0"
+ inkscape:pagecheckerboard="false"
+ showgrid="false"
+ inkscape:zoom="0.46315494"
+ inkscape:cx="-659.30808"
+ inkscape:cy="83.54417"
+ inkscape:window-width="1920"
+ inkscape:window-height="1025"
+ inkscape:window-x="0"
+ inkscape:window-y="26"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="logo" />
+ <g
+ id="logo">
+ <g
+ id="circles"
+ style="display:inline;fill:#0042b3;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.327943"
+ transform="translate(180)">
+ <g
+ id="g4645"
+ inkscape:export-xdpi="98.304001"
+ inkscape:export-ydpi="98.304001">
+ <ellipse
+ transform="matrix(-0.99007841,-0.140516,0.16039263,-0.98705329,0,0)"
+ ry="75.234604"
+ rx="74.764656"
+ cy="-29.611343"
+ cx="101.25517"
+ id="path4580"
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.1471165;stroke-opacity:1" />
+ <g
+ transform="rotate(-180,-107.57659,26.234233)"
+ id="g4622">
+ <path
+ id="path1306-7-63-9"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.0330856;stroke-linejoin:round;stroke-opacity:1"
+ d="M 45.48017,110.87571 A 35.545008,38.588202 0 0 0 9.9354536,149.46424 35.545008,38.588202 0 0 0 45.48017,188.05226 35.545008,38.588202 0 0 0 81.025385,149.46424 35.545008,38.588202 0 0 0 45.48017,110.87571 Z m -0.07061,4.90892 a 31.151221,33.78691 0 0 1 0.829048,0.0119 31.151221,33.78691 0 0 1 0.82854,0.0362 31.151221,33.78691 0 0 1 0.827519,0.0595 31.151221,33.78691 0 0 1 0.825964,0.0839 31.151221,33.78691 0 0 1 0.823425,0.10767 31.151221,33.78691 0 0 1 0.820349,0.13097 31.151221,33.78691 0 0 1 0.816775,0.15478 31.151221,33.78691 0 0 1 0.813198,0.17859 31.151221,33.78691 0 0 1 0.808083,0.20188 31.151221,33.78691 0 0 1 0.802953,0.22518 31.151221,33.78691 0 0 1 0.797339,0.24796 31.151221,33.78691 0 0 1 0.790697,0.27125 31.151221,33.78691 0 0 1 0.783536,0.29403 31.151221,33.78691 0 0 1 0.776355,0.31629 31.151221,33.78691 0 0 1 0.768177,0.33854 31.151221,33.78691 0 0 1 0.760022,0.3608 31.151221,33.78691 0 0 1 0.750783,0.38255 31.151221,33.78691 0 0 1 0.740582,0.40429 31.151221,33.78691 0 0 1 0.73085,0.42499 31.151221,33.78691 0 0 1 0.7201,0.44622 31.151221,33.78691 0 0 1 0.70885,0.46692 31.151221,33.78691 0 0 1 0.6971,0.48764 31.151221,33.78691 0 0 1 0.68484,0.50678 31.151221,33.78691 0 0 1 0.67254,0.52697 31.151221,33.78691 0 0 1 0.65872,0.54613 31.151221,33.78691 0 0 1 0.64545,0.56476 31.151221,33.78691 0 0 1 0.63165,0.58339 31.151221,33.78691 0 0 1 0.61678,0.601 31.151221,33.78691 0 0 1 0.60196,0.61911 31.151221,33.78691 0 0 1 0.58611,0.63568 31.151221,33.78691 0 0 1 0.57078,0.65277 31.151221,33.78691 0 0 1 0.5544,0.66932 31.151221,33.78691 0 0 1 0.53751,0.68486 31.151221,33.78691 0 0 1 0.52066,0.69987 31.151221,33.78691 0 0 1 0.50379,0.71488 31.151221,33.78691 0 0 1 0.48534,0.72885 31.151221,33.78691 0 0 1 0.46798,0.74284 31.151221,33.78691 0 0 1 0.44904,0.75629 31.151221,33.78691 0 0 1 0.43065,0.76872 31.151221,33.78691 0 0 1 0.41119,0.78114 31.151221,33.78691 0 0 1 0.39227,0.79253 31.151221,33.78691 0 0 1 0.37284,0.80391 31.151221,33.78691 0 0 1 0.35239,0.81376 31.151221,33.78691 0 0 1 0.33294,0.8241 31.151221,33.78691 0 0 1 0.31198,0.83342 31.151221,33.78691 0 0 1 0.29153,0.84171 31.151221,33.78691 0 0 1 0.27107,0.85051 31.151221,33.78691 0 0 1 0.2501,0.85775 31.151221,33.78691 0 0 1 0.2286,0.86448 31.151221,33.78691 0 0 1 0.20765,0.8707 31.151221,33.78691 0 0 1 0.18616,0.8769 31.151221,33.78691 0 0 1 0.16468,0.88157 31.151221,33.78691 0 0 1 0.14268,0.8857 31.151221,33.78691 0 0 1 0.12073,0.88985 31.151221,33.78691 0 0 1 0.0992,0.89347 31.151221,33.78691 0 0 1 0.0772,0.89554 31.151221,33.78691 0 0 1 0.0553,0.89761 31.151221,33.78691 0 0 1 0.0332,0.89865 31.151221,33.78691 0 0 1 0.0108,0.89917 31.151221,33.78691 0 0 1 -0.0957,2.6509 31.151221,33.78691 0 0 1 -0.28795,2.63486 31.151221,33.78691 0 0 1 -0.47716,2.60173 31.151221,33.78691 0 0 1 -0.66385,2.55359 31.151221,33.78691 0 0 1 -0.84644,2.48888 31.151221,33.78691 0 0 1 -1.0239,2.40916 31.151221,33.78691 0 0 1 -1.19526,2.31444 31.151221,33.78691 0 0 1 -1.3589,2.20624 31.151221,33.78691 0 0 1 -1.51437,2.08304 31.151221,33.78691 0 0 1 -1.66015,1.94845 31.151221,33.78691 0 0 1 -1.79618,1.8004 31.151221,33.78691 0 0 1 -1.92098,1.64252 31.151221,33.78691 0 0 1 -2.034,1.47428 31.151221,33.78691 0 0 1 -2.13375,1.2962 31.151221,33.78691 0 0 1 -2.221695,1.11037 31.151221,33.78691 0 0 1 -2.294328,0.91832 31.151221,33.78691 0 0 1 -2.354157,0.72006 31.151221,33.78691 0 0 1 -2.399184,0.51765 31.151221,33.78691 0 0 1 -2.428831,0.31163 31.151221,33.78691 0 0 1 -2.444189,0.10457 31.151221,33.78691 0 0 1 -2.44418,-0.10457 31.151221,33.78691 0 0 1 -2.428838,-0.31163 31.151221,33.78691 0 0 1 -2.399177,-0.51765 31.151221,33.78691 0 0 1 -2.354163,-0.72006 31.151221,33.78691 0 0 1 -2.29484,-0.91831 31.151221,33.78691 0 0 1 -2.221196,-1.11037 31.151221,33.78691 0 0 1 -2.134249,-1.29621 31.151221,33.78691 0 0 1 -2.033489,-1.47428 31.151221,33.78691 0 0 1 -1.920973,-1.64252 31.151221,33.78691 0 0 1 -1.796194,-1.8004 31.151221,33.78691 0 0 1 -1.660643,-1.94845 31.151221,33.78691 0 0 1 -1.513873,-2.08304 31.151221,33.78691 0 0 1 -1.358905,-2.20624 31.151221,33.78691 0 0 1 -1.195236,-2.31443 31.151221,33.78691 0 0 1 -1.023911,-2.40916 31.151221,33.78691 0 0 1 -0.846946,-2.48888 31.151221,33.78691 0 0 1 -0.663854,-2.55359 31.151221,33.78691 0 0 1 -0.477175,-2.60173 31.151221,33.78691 0 0 1 -0.287434,-2.63486 31.151221,33.78691 0 0 1 -0.09615,-2.65091 31.151221,33.78691 0 0 1 0.09615,-2.65091 31.151221,33.78691 0 0 1 0.287434,-2.63433 31.151221,33.78691 0 0 1 0.477175,-2.60174 31.151221,33.78691 0 0 1 0.663854,-2.55358 31.151221,33.78691 0 0 1 0.846946,-2.48889 31.151221,33.78691 0 0 1 1.023911,-2.40916 31.151221,33.78691 0 0 1 1.195236,-2.31495 31.151221,33.78691 0 0 1 1.358905,-2.20572 31.151221,33.78691 0 0 1 1.513873,-2.08356 31.151221,33.78691 0 0 1 1.660643,-1.94793 31.151221,33.78691 0 0 1 1.796194,-1.80092 31.151221,33.78691 0 0 1 1.920973,-1.64252 31.151221,33.78691 0 0 1 2.033505,-1.47376 31.151221,33.78691 0 0 1 2.13425,-1.29621 31.151221,33.78691 0 0 1 2.221195,-1.11088 31.151221,33.78691 0 0 1 2.294841,-0.9178 31.151221,33.78691 0 0 1 2.354165,-0.72058 31.151221,33.78691 0 0 1 2.399176,-0.51713 31.151221,33.78691 0 0 1 2.428838,-0.31215 31.151221,33.78691 0 0 1 2.44418,-0.10405 z"
+ clip-path="url(#clipPath20735)"
+ transform="translate(-177.74838,-111.36079)"
+ inkscape:export-filename="C:\Users\a116178\Downloads\Taler\taler 512.png"
+ inkscape:export-xdpi="96.231026"
+ inkscape:export-ydpi="96.231026"
+ inkscape:connector-curvature="0" />
+ <path
+ id="path1306-0"
+ style="opacity:1;fill:#0042b3;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.0376767;stroke-linejoin:round"
+ d="m 68.010803,105.31927 a 40.722405,43.678338 0 0 0 -40.72207,43.67871 40.722405,43.678338 0 0 0 40.72207,43.67812 40.722405,43.678338 0 0 0 40.722647,-43.67812 40.722405,43.678338 0 0 0 -40.722647,-43.67871 z m -0.0809,5.55644 a 35.68863,38.243712 0 0 1 0.94981,0.0135 35.68863,38.243712 0 0 1 0.94922,0.041 35.68863,38.243712 0 0 1 0.94805,0.0674 35.68863,38.243712 0 0 1 0.94628,0.0949 35.68863,38.243712 0 0 1 0.94336,0.12188 35.68863,38.243712 0 0 1 0.93984,0.14824 35.68863,38.243712 0 0 1 0.93574,0.1752 35.68863,38.243712 0 0 1 0.93165,0.20214 35.68863,38.243712 0 0 1 0.92578,0.22852 35.68863,38.243712 0 0 1 0.91992,0.25488 35.68863,38.243712 0 0 1 0.91348,0.28067 35.68863,38.243712 0 0 1 0.90586,0.30703 35.68863,38.243712 0 0 1 0.89765,0.33281 35.68863,38.243712 0 0 1 0.88945,0.35801 35.68863,38.243712 0 0 1 0.880067,0.3832 35.68863,38.243712 0 0 1 0.87071,0.4084 35.68863,38.243712 0 0 1 0.86015,0.43301 35.68863,38.243712 0 0 1 0.84844,0.45762 35.68863,38.243712 0 0 1 0.8373,0.48105 35.68863,38.243712 0 0 1 0.825,0.50508 35.68863,38.243712 0 0 1 0.81211,0.52851 35.68863,38.243712 0 0 1 0.79863,0.55196 35.68863,38.243712 0 0 1 0.78458,0.57363 35.68863,38.243712 0 0 1 0.7705,0.59648 35.68863,38.243712 0 0 1 0.75469,0.61817 35.68863,38.243712 0 0 1 0.73946,0.63926 35.68863,38.243712 0 0 1 0.72363,0.66035 35.68863,38.243712 0 0 1 0.70664,0.68027 35.68863,38.243712 0 0 1 0.68965,0.70078 35.68863,38.243712 0 0 1 0.67148,0.71953 35.68863,38.243712 0 0 1 0.65391,0.73887 35.68863,38.243712 0 0 1 0.63515,0.75762 35.68863,38.243712 0 0 1 0.61582,0.77519 35.68863,38.243712 0 0 1 0.59649,0.79219 35.68863,38.243712 0 0 1 0.57715,0.80918 35.68863,38.243712 0 0 1 0.55605,0.825 35.68863,38.243712 0 0 1 0.53613,0.84082 35.68863,38.243712 0 0 1 0.51446,0.85606 35.68863,38.243712 0 0 1 0.493358,0.87011 35.68863,38.243712 0 0 1 0.47109,0.88418 35.68863,38.243712 0 0 1 0.44941,0.89707 35.68863,38.243712 0 0 1 0.427152,0.90996 35.68863,38.243712 0 0 1 0.40372,0.9211 35.68863,38.243712 0 0 1 0.38145,0.93281 35.68863,38.243712 0 0 1 0.35742,0.94336 35.68863,38.243712 0 0 1 0.33399,0.95273 35.68863,38.243712 0 0 1 0.31054,0.9627 35.68863,38.243712 0 0 1 0.28653,0.9709 35.68863,38.243712 0 0 1 0.26191,0.97851 35.68863,38.243712 0 0 1 0.23789,0.98555 35.68863,38.243712 0 0 1 0.21328,0.99258 35.68863,38.243712 0 0 1 0.18867,0.99785 35.68863,38.243712 0 0 1 0.16348,1.00254 35.68863,38.243712 0 0 1 0.13828,1.00723 35.68863,38.243712 0 0 1 0.11367,1.01132 35.68863,38.243712 0 0 1 0.0885,1.01368 35.68863,38.243712 0 0 1 0.0633,1.01601 35.68863,38.243712 0 0 1 0.0381,1.01719 35.68863,38.243712 0 0 1 0.0123,1.01777 35.68863,38.243712 0 0 1 -0.10957,3.00058 35.68863,38.243712 0 0 1 -0.32988,2.98243 35.68863,38.243712 0 0 1 -0.54668,2.94492 35.68863,38.243712 0 0 1 -0.76055,2.89043 35.68863,38.243712 0 0 1 -0.96972,2.81718 35.68863,38.243712 0 0 1 -1.173062,2.72696 35.68863,38.243712 0 0 1 -1.369338,2.61973 35.68863,38.243712 0 0 1 -1.55683,2.49726 35.68863,38.243712 0 0 1 -1.73496,2.35781 35.68863,38.243712 0 0 1 -1.90196,2.20547 35.68863,38.243712 0 0 1 -2.05781,2.03789 35.68863,38.243712 0 0 1 -2.20078,1.85918 35.68863,38.243712 0 0 1 -2.33027,1.66875 35.68863,38.243712 0 0 1 -2.44454,1.46719 35.68863,38.243712 0 0 1 -2.54531,1.25684 35.68863,38.243712 0 0 1 -2.628507,1.03945 35.68863,38.243712 0 0 1 -2.69706,0.81504 35.68863,38.243712 0 0 1 -2.74864,0.58593 35.68863,38.243712 0 0 1 -2.78261,0.35274 35.68863,38.243712 0 0 1 -2.8002,0.11836 35.68863,38.243712 0 0 1 -2.80019,-0.11836 35.68863,38.243712 0 0 1 -2.78262,-0.35274 35.68863,38.243712 0 0 1 -2.74863,-0.58593 35.68863,38.243712 0 0 1 -2.69707,-0.81504 35.68863,38.243712 0 0 1 -2.6291,-1.03945 35.68863,38.243712 0 0 1 -2.54473,-1.25684 35.68863,38.243712 0 0 1 -2.44512,-1.46719 35.68863,38.243712 0 0 1 -2.32968,-1.66875 35.68863,38.243712 0 0 1 -2.20078,-1.85918 35.68863,38.243712 0 0 1 -2.05782,-2.03789 35.68863,38.243712 0 0 1 -1.90253,-2.20547 35.68863,38.243712 0 0 1 -1.73438,-2.35781 35.68863,38.243712 0 0 1 -1.55684,-2.49726 35.68863,38.243712 0 0 1 -1.36933,-2.61973 35.68863,38.243712 0 0 1 -1.17305,-2.72696 35.68863,38.243712 0 0 1 -0.97031,-2.81718 35.68863,38.243712 0 0 1 -0.76055,-2.89043 35.68863,38.243712 0 0 1 -0.54668,-2.94492 35.68863,38.243712 0 0 1 -0.3293,-2.98243 35.68863,38.243712 0 0 1 -0.11015,-3.00058 35.68863,38.243712 0 0 1 0.11015,-3.00058 35.68863,38.243712 0 0 1 0.3293,-2.98184 35.68863,38.243712 0 0 1 0.54668,-2.94492 35.68863,38.243712 0 0 1 0.76055,-2.89043 35.68863,38.243712 0 0 1 0.97031,-2.81719 35.68863,38.243712 0 0 1 1.17305,-2.72695 35.68863,38.243712 0 0 1 1.36933,-2.62031 35.68863,38.243712 0 0 1 1.55684,-2.49668 35.68863,38.243712 0 0 1 1.73438,-2.3584 35.68863,38.243712 0 0 1 1.90253,-2.20489 35.68863,38.243712 0 0 1 2.05782,-2.03847 35.68863,38.243712 0 0 1 2.20078,-1.85918 35.68863,38.243712 0 0 1 2.32968,-1.66817 35.68863,38.243712 0 0 1 2.44512,-1.46718 35.68863,38.243712 0 0 1 2.54473,-1.25743 35.68863,38.243712 0 0 1 2.6291,-1.03886 35.68863,38.243712 0 0 1 2.69707,-0.81563 35.68863,38.243712 0 0 1 2.74863,-0.58535 35.68863,38.243712 0 0 1 2.78262,-0.35332 35.68863,38.243712 0 0 1 2.80019,-0.11777 z"
+ clip-path="url(#clipPath20731)"
+ transform="translate(-177.74838,-111.36079)"
+ inkscape:export-filename="C:\Users\a116178\Downloads\Taler\taler 512.png"
+ inkscape:export-xdpi="96.231026"
+ inkscape:export-ydpi="96.231026"
+ inkscape:connector-curvature="0" />
+ <path
+ id="path1306-5"
+ style="opacity:1;fill:#0042b3;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.0376767;stroke-linejoin:round"
+ d="M 45.56102,105.31927 A 40.722405,43.678338 0 0 0 4.8389507,148.99798 40.722405,43.678338 0 0 0 45.56102,192.6761 40.722405,43.678338 0 0 0 86.283657,148.99798 40.722405,43.678338 0 0 0 45.56102,105.31927 Z m -0.0809,5.55644 a 35.68863,38.243712 0 0 1 0.94981,0.0135 35.68863,38.243712 0 0 1 0.94922,0.041 35.68863,38.243712 0 0 1 0.94805,0.0674 35.68863,38.243712 0 0 1 0.94628,0.0949 35.68863,38.243712 0 0 1 0.94336,0.12188 35.68863,38.243712 0 0 1 0.93984,0.14824 35.68863,38.243712 0 0 1 0.93574,0.1752 35.68863,38.243712 0 0 1 0.93165,0.20214 35.68863,38.243712 0 0 1 0.92578,0.22852 35.68863,38.243712 0 0 1 0.91992,0.25488 35.68863,38.243712 0 0 1 0.91348,0.28067 35.68863,38.243712 0 0 1 0.90586,0.30703 35.68863,38.243712 0 0 1 0.89765,0.33281 35.68863,38.243712 0 0 1 0.88945,0.35801 35.68863,38.243712 0 0 1 0.880067,0.3832 35.68863,38.243712 0 0 1 0.87071,0.4084 35.68863,38.243712 0 0 1 0.86015,0.43301 35.68863,38.243712 0 0 1 0.84844,0.45762 35.68863,38.243712 0 0 1 0.8373,0.48105 35.68863,38.243712 0 0 1 0.825,0.50508 35.68863,38.243712 0 0 1 0.81211,0.52851 35.68863,38.243712 0 0 1 0.79863,0.55196 35.68863,38.243712 0 0 1 0.78458,0.57363 35.68863,38.243712 0 0 1 0.7705,0.59648 35.68863,38.243712 0 0 1 0.75469,0.61817 35.68863,38.243712 0 0 1 0.73946,0.63926 35.68863,38.243712 0 0 1 0.72363,0.66035 35.68863,38.243712 0 0 1 0.70664,0.68027 35.68863,38.243712 0 0 1 0.68965,0.70078 35.68863,38.243712 0 0 1 0.67148,0.71953 35.68863,38.243712 0 0 1 0.65391,0.73887 35.68863,38.243712 0 0 1 0.63515,0.75762 35.68863,38.243712 0 0 1 0.61582,0.77519 35.68863,38.243712 0 0 1 0.59649,0.79219 35.68863,38.243712 0 0 1 0.57715,0.80918 35.68863,38.243712 0 0 1 0.55605,0.825 35.68863,38.243712 0 0 1 0.53613,0.84082 35.68863,38.243712 0 0 1 0.51446,0.85606 35.68863,38.243712 0 0 1 0.49336,0.87011 35.68863,38.243712 0 0 1 0.47109,0.88418 35.68863,38.243712 0 0 1 0.44941,0.89707 35.68863,38.243712 0 0 1 0.42715,0.90996 35.68863,38.243712 0 0 1 0.40371,0.9211 35.68863,38.243712 0 0 1 0.38145,0.93281 35.68863,38.243712 0 0 1 0.35742,0.94336 35.68863,38.243712 0 0 1 0.33399,0.95273 35.68863,38.243712 0 0 1 0.31054,0.9627 35.68863,38.243712 0 0 1 0.28653,0.9709 35.68863,38.243712 0 0 1 0.26191,0.97851 35.68863,38.243712 0 0 1 0.23789,0.98555 35.68863,38.243712 0 0 1 0.21328,0.99258 35.68863,38.243712 0 0 1 0.18867,0.99785 35.68863,38.243712 0 0 1 0.16348,1.00254 35.68863,38.243712 0 0 1 0.13828,1.00723 35.68863,38.243712 0 0 1 0.11367,1.01132 35.68863,38.243712 0 0 1 0.0885,1.01368 35.68863,38.243712 0 0 1 0.0633,1.01601 35.68863,38.243712 0 0 1 0.0381,1.01719 35.68863,38.243712 0 0 1 0.0123,1.01778 35.68863,38.243712 0 0 1 -0.10957,3.00058 35.68863,38.243712 0 0 1 -0.32988,2.98243 35.68863,38.243712 0 0 1 -0.54668,2.94492 35.68863,38.243712 0 0 1 -0.76055,2.89043 35.68863,38.243712 0 0 1 -0.96972,2.81718 35.68863,38.243712 0 0 1 -1.17305,2.72696 35.68863,38.243712 0 0 1 -1.36934,2.61972 35.68863,38.243712 0 0 1 -1.55683,2.49727 35.68863,38.243712 0 0 1 -1.73496,2.35781 35.68863,38.243712 0 0 1 -1.90196,2.20547 35.68863,38.243712 0 0 1 -2.05781,2.03789 35.68863,38.243712 0 0 1 -2.20078,1.85918 35.68863,38.243712 0 0 1 -2.33027,1.66875 35.68863,38.243712 0 0 1 -2.44454,1.46719 35.68863,38.243712 0 0 1 -2.54531,1.25683 35.68863,38.243712 0 0 1 -2.628507,1.03946 35.68863,38.243712 0 0 1 -2.69706,0.81504 35.68863,38.243712 0 0 1 -2.74864,0.58593 35.68863,38.243712 0 0 1 -2.78261,0.35274 35.68863,38.243712 0 0 1 -2.8002,0.11836 35.68863,38.243712 0 0 1 -2.80019,-0.11836 35.68863,38.243712 0 0 1 -2.78262,-0.35274 35.68863,38.243712 0 0 1 -2.74863,-0.58593 35.68863,38.243712 0 0 1 -2.69707,-0.81504 35.68863,38.243712 0 0 1 -2.6291,-1.03946 35.68863,38.243712 0 0 1 -2.54473,-1.25683 35.68863,38.243712 0 0 1 -2.44512,-1.46719 35.68863,38.243712 0 0 1 -2.32968,-1.66875 35.68863,38.243712 0 0 1 -2.20078,-1.85918 35.68863,38.243712 0 0 1 -2.05782,-2.03789 35.68863,38.243712 0 0 1 -1.90253,-2.20547 35.68863,38.243712 0 0 1 -1.73438,-2.35781 35.68863,38.243712 0 0 1 -1.55684,-2.49727 35.68863,38.243712 0 0 1 -1.36933,-2.61972 35.68863,38.243712 0 0 1 -1.17305,-2.72696 35.68863,38.243712 0 0 1 -0.97031,-2.81718 35.68863,38.243712 0 0 1 -0.76055,-2.89043 35.68863,38.243712 0 0 1 -0.546679,-2.94492 35.68863,38.243712 0 0 1 -0.3293004,-2.98243 35.68863,38.243712 0 0 1 -0.11015,-3.00058 35.68863,38.243712 0 0 1 0.11015,-3.00059 35.68863,38.243712 0 0 1 0.3293004,-2.98184 35.68863,38.243712 0 0 1 0.546679,-2.94492 35.68863,38.243712 0 0 1 0.76055,-2.89043 35.68863,38.243712 0 0 1 0.97031,-2.81719 35.68863,38.243712 0 0 1 1.17305,-2.72695 35.68863,38.243712 0 0 1 1.36933,-2.62031 35.68863,38.243712 0 0 1 1.55684,-2.49668 35.68863,38.243712 0 0 1 1.73438,-2.3584 35.68863,38.243712 0 0 1 1.90253,-2.20489 35.68863,38.243712 0 0 1 2.05782,-2.03847 35.68863,38.243712 0 0 1 2.20078,-1.85918 35.68863,38.243712 0 0 1 2.32968,-1.66817 35.68863,38.243712 0 0 1 2.44512,-1.46718 35.68863,38.243712 0 0 1 2.54473,-1.25743 35.68863,38.243712 0 0 1 2.6291,-1.03886 35.68863,38.243712 0 0 1 2.69707,-0.81563 35.68863,38.243712 0 0 1 2.74863,-0.58535 35.68863,38.243712 0 0 1 2.78262,-0.35332 35.68863,38.243712 0 0 1 2.80019,-0.11777 z"
+ clip-path="url(#clipPath20727)"
+ transform="translate(-177.74838,-111.36079)"
+ inkscape:export-filename="C:\Users\a116178\Downloads\Taler\taler 512.png"
+ inkscape:export-xdpi="96.231026"
+ inkscape:export-ydpi="96.231026"
+ inkscape:connector-curvature="0" />
+ <path
+ id="path1306-7-6"
+ style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.0427732;stroke-linejoin:round;stroke-opacity:1"
+ d="M 68.102923,99.029256 A 46.363577,49.444797 0 0 0 21.739728,148.47447 46.363577,49.444797 0 0 0 68.102923,197.91903 46.363577,49.444797 0 0 0 114.46677,148.47447 46.363577,49.444797 0 0 0 68.102923,99.029256 Z m -0.09212,6.290014 a 40.632485,43.292687 0 0 1 1.081384,0.0153 40.632485,43.292687 0 0 1 1.080714,0.0464 40.632485,43.292687 0 0 1 1.079381,0.0763 40.632485,43.292687 0 0 1 1.077365,0.10745 40.632485,43.292687 0 0 1 1.074041,0.13797 40.632485,43.292687 0 0 1 1.070034,0.16781 40.632485,43.292687 0 0 1 1.065366,0.19832 40.632485,43.292687 0 0 1 1.060709,0.22884 40.632485,43.292687 0 0 1 1.054026,0.25869 40.632485,43.292687 0 0 1 1.047355,0.28853 40.632485,43.292687 0 0 1 1.040022,0.31772 40.632485,43.292687 0 0 1 1.031346,0.34756 40.632485,43.292687 0 0 1 1.022009,0.37675 40.632485,43.292687 0 0 1 1.01266,0.40528 40.632485,43.292687 0 0 1 1.00198,0.43379 40.632485,43.292687 0 0 1 0.99134,0.46232 40.632485,43.292687 0 0 1 0.97929,0.49017 40.632485,43.292687 0 0 1 0.96598,0.51803 40.632485,43.292687 0 0 1 0.9533,0.54457 40.632485,43.292687 0 0 1 0.93927,0.57175 40.632485,43.292687 0 0 1 0.92461,0.5983 40.632485,43.292687 0 0 1 0.90926,0.62482 40.632485,43.292687 0 0 1 0.89328,0.64936 40.632485,43.292687 0 0 1 0.87724,0.67524 40.632485,43.292687 0 0 1 0.85922,0.69977 40.632485,43.292687 0 0 1 0.8419,0.72365 40.632485,43.292687 0 0 1 0.82388,0.74754 40.632485,43.292687 0 0 1 0.80452,0.77008 40.632485,43.292687 0 0 1 0.78518,0.7933 40.632485,43.292687 0 0 1 0.7645,0.81452 40.632485,43.292687 0 0 1 0.74449,0.83642 40.632485,43.292687 0 0 1 0.723143,0.85763 40.632485,43.292687 0 0 1 0.70113,0.87754 40.632485,43.292687 0 0 1 0.67912,0.89678 40.632485,43.292687 0 0 1 0.657112,0.916 40.632485,43.292687 0 0 1 0.63307,0.93392 40.632485,43.292687 0 0 1 0.6104,0.95183 40.632485,43.292687 0 0 1 0.58573,0.96907 40.632485,43.292687 0 0 1 0.56171,0.98499 40.632485,43.292687 0 0 1 0.53634,1.00091 40.632485,43.292687 0 0 1 0.51167,1.0155 40.632485,43.292687 0 0 1 0.48631,1.0301 40.632485,43.292687 0 0 1 0.45965,1.04269 40.632485,43.292687 0 0 1 0.43428,1.05597 40.632485,43.292687 0 0 1 0.40694,1.0679 40.632485,43.292687 0 0 1 0.38025,1.07852 40.632485,43.292687 0 0 1 0.35357,1.08979 40.632485,43.292687 0 0 1 0.32622,1.09908 40.632485,43.292687 0 0 1 0.29818,1.10769 40.632485,43.292687 0 0 1 0.27085,1.11566 40.632485,43.292687 0 0 1 0.24283,1.12362 40.632485,43.292687 0 0 1 0.2148,1.12959 40.632485,43.292687 0 0 1 0.18612,1.1349 40.632485,43.292687 0 0 1 0.15745,1.1402 40.632485,43.292687 0 0 1 0.12941,1.14485 40.632485,43.292687 0 0 1 0.10078,1.14749 40.632485,43.292687 0 0 1 0.072,1.15015 40.632485,43.292687 0 0 1 0.0434,1.15148 40.632485,43.292687 0 0 1 0.014,1.15214 40.632485,43.292687 0 0 1 -0.12477,3.39672 40.632485,43.292687 0 0 1 -0.37557,3.37617 40.632485,43.292687 0 0 1 -0.6224,3.33371 40.632485,43.292687 0 0 1 -0.86591,3.27203 40.632485,43.292687 0 0 1 -1.10406,3.18911 40.632485,43.292687 0 0 1 -1.33555,3.08697 40.632485,43.292687 0 0 1 -1.55903,2.96559 40.632485,43.292687 0 0 1 -1.77249,2.82696 40.632485,43.292687 0 0 1 -1.975302,2.66909 40.632485,43.292687 0 0 1 -2.165443,2.49664 40.632485,43.292687 0 0 1 -2.34287,2.30693 40.632485,43.292687 0 0 1 -2.50564,2.10463 40.632485,43.292687 0 0 1 -2.65309,1.88906 40.632485,43.292687 0 0 1 -2.78317,1.66089 40.632485,43.292687 0 0 1 -2.89791,1.42277 40.632485,43.292687 0 0 1 -2.992636,1.17668 40.632485,43.292687 0 0 1 -3.070676,0.92264 40.632485,43.292687 0 0 1 -3.129402,0.66329 40.632485,43.292687 0 0 1 -3.168078,0.3993 40.632485,43.292687 0 0 1 -3.188104,0.13399 40.632485,43.292687 0 0 1 -3.188094,-0.13399 40.632485,43.292687 0 0 1 -3.168088,-0.3993 40.632485,43.292687 0 0 1 -3.12939,-0.6633 40.632485,43.292687 0 0 1 -3.07069,-0.92264 40.632485,43.292687 0 0 1 -2.993303,-1.17668 40.632485,43.292687 0 0 1 -2.897244,-1.42277 40.632485,43.292687 0 0 1 -2.783837,-1.66088 40.632485,43.292687 0 0 1 -2.652404,-1.88906 40.632485,43.292687 0 0 1 -2.505648,-2.10463 40.632485,43.292687 0 0 1 -2.342886,-2.30694 40.632485,43.292687 0 0 1 -2.166082,-2.49664 40.632485,43.292687 0 0 1 -1.974639,-2.66909 40.632485,43.292687 0 0 1 -1.772504,-2.82696 40.632485,43.292687 0 0 1 -1.559022,-2.96558 40.632485,43.292687 0 0 1 -1.335549,-3.08697 40.632485,43.292687 0 0 1 -1.104725,-3.18911 40.632485,43.292687 0 0 1 -0.865908,-3.27203 40.632485,43.292687 0 0 1 -0.622408,-3.33371 40.632485,43.292687 0 0 1 -0.374918,-3.37617 40.632485,43.292687 0 0 1 -0.125409,-3.39672 40.632485,43.292687 0 0 1 0.125409,-3.39673 40.632485,43.292687 0 0 1 0.374918,-3.3755 40.632485,43.292687 0 0 1 0.622408,-3.33371 40.632485,43.292687 0 0 1 0.865908,-3.27203 40.632485,43.292687 0 0 1 1.104725,-3.18912 40.632485,43.292687 0 0 1 1.335549,-3.08696 40.632485,43.292687 0 0 1 1.559022,-2.96625 40.632485,43.292687 0 0 1 1.772504,-2.8263 40.632485,43.292687 0 0 1 1.974639,-2.66975 40.632485,43.292687 0 0 1 2.166082,-2.49598 40.632485,43.292687 0 0 1 2.342886,-2.3076 40.632485,43.292687 0 0 1 2.505648,-2.10463 40.632485,43.292687 0 0 1 2.652426,-1.88839 40.632485,43.292687 0 0 1 2.783836,-1.66089 40.632485,43.292687 0 0 1 2.897245,-1.42343 40.632485,43.292687 0 0 1 2.993303,-1.17602 40.632485,43.292687 0 0 1 3.070689,-0.9233 40.632485,43.292687 0 0 1 3.12939,-0.66263 40.632485,43.292687 0 0 1 3.16809,-0.39997 40.632485,43.292687 0 0 1 3.188093,-0.13332 z"
+ clip-path="url(#clipPath20723)"
+ transform="translate(-177.74838,-111.36079)"
+ inkscape:connector-curvature="0" />
+ <path
+ id="path1306-7-63-6"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.0330856;stroke-linejoin:round;stroke-opacity:1"
+ d="m 67.929953,110.87572 a 35.545008,38.588202 0 0 0 -35.544717,38.58853 35.545008,38.588202 0 0 0 35.544717,38.58801 35.545008,38.588202 0 0 0 35.545217,-38.58801 35.545008,38.588202 0 0 0 -35.545217,-38.58853 z m -0.07061,4.90891 a 31.151221,33.78691 0 0 1 0.829048,0.0119 31.151221,33.78691 0 0 1 0.82854,0.0362 31.151221,33.78691 0 0 1 0.827519,0.0595 31.151221,33.78691 0 0 1 0.825964,0.0839 31.151221,33.78691 0 0 1 0.823425,0.10768 31.151221,33.78691 0 0 1 0.820349,0.13096 31.151221,33.78691 0 0 1 0.816775,0.15478 31.151221,33.78691 0 0 1 0.813198,0.17859 31.151221,33.78691 0 0 1 0.808083,0.20189 31.151221,33.78691 0 0 1 0.802953,0.22518 31.151221,33.78691 0 0 1 0.797339,0.24795 31.151221,33.78691 0 0 1 0.790697,0.27125 31.151221,33.78691 0 0 1 0.783536,0.29403 31.151221,33.78691 0 0 1 0.776355,0.31629 31.151221,33.78691 0 0 1 0.768177,0.33854 31.151221,33.78691 0 0 1 0.760022,0.36081 31.151221,33.78691 0 0 1 0.750783,0.38255 31.151221,33.78691 0 0 1 0.740582,0.40428 31.151221,33.78691 0 0 1 0.73085,0.425 31.151221,33.78691 0 0 1 0.7201,0.44621 31.151221,33.78691 0 0 1 0.70885,0.46693 31.151221,33.78691 0 0 1 0.6971,0.48763 31.151221,33.78691 0 0 1 0.68484,0.50678 31.151221,33.78691 0 0 1 0.67254,0.52697 31.151221,33.78691 0 0 1 0.65872,0.54613 31.151221,33.78691 0 0 1 0.64545,0.56476 31.151221,33.78691 0 0 1 0.63165,0.5834 31.151221,33.78691 0 0 1 0.61678,0.60099 31.151221,33.78691 0 0 1 0.60196,0.61912 31.151221,33.78691 0 0 1 0.58611,0.63568 31.151221,33.78691 0 0 1 0.57078,0.65276 31.151221,33.78691 0 0 1 0.5544,0.66933 31.151221,33.78691 0 0 1 0.53751,0.68485 31.151221,33.78691 0 0 1 0.52066,0.69987 31.151221,33.78691 0 0 1 0.50379,0.71488 31.151221,33.78691 0 0 1 0.48534,0.72886 31.151221,33.78691 0 0 1 0.46798,0.74283 31.151221,33.78691 0 0 1 0.44904,0.75629 31.151221,33.78691 0 0 1 0.43065,0.76872 31.151221,33.78691 0 0 1 0.41119,0.78114 31.151221,33.78691 0 0 1 0.39227,0.79253 31.151221,33.78691 0 0 1 0.37284,0.80392 31.151221,33.78691 0 0 1 0.35239,0.81375 31.151221,33.78691 0 0 1 0.33294,0.82411 31.151221,33.78691 0 0 1 0.31198,0.83342 31.151221,33.78691 0 0 1 0.29153,0.8417 31.151221,33.78691 0 0 1 0.27107,0.85051 31.151221,33.78691 0 0 1 0.2501,0.85775 31.151221,33.78691 0 0 1 0.2286,0.86448 31.151221,33.78691 0 0 1 0.20765,0.8707 31.151221,33.78691 0 0 1 0.18616,0.8769 31.151221,33.78691 0 0 1 0.16468,0.88157 31.151221,33.78691 0 0 1 0.14268,0.8857 31.151221,33.78691 0 0 1 0.12073,0.88986 31.151221,33.78691 0 0 1 0.0992,0.89346 31.151221,33.78691 0 0 1 0.0772,0.89554 31.151221,33.78691 0 0 1 0.0553,0.89761 31.151221,33.78691 0 0 1 0.0332,0.89865 31.151221,33.78691 0 0 1 0.0108,0.89917 31.151221,33.78691 0 0 1 -0.0957,2.65091 31.151221,33.78691 0 0 1 -0.28795,2.63486 31.151221,33.78691 0 0 1 -0.47716,2.60172 31.151221,33.78691 0 0 1 -0.66385,2.55359 31.151221,33.78691 0 0 1 -0.84644,2.48888 31.151221,33.78691 0 0 1 -1.0239,2.40917 31.151221,33.78691 0 0 1 -1.19526,2.31443 31.151221,33.78691 0 0 1 -1.3589,2.20624 31.151221,33.78691 0 0 1 -1.51437,2.08304 31.151221,33.78691 0 0 1 -1.66015,1.94845 31.151221,33.78691 0 0 1 -1.79618,1.8004 31.151221,33.78691 0 0 1 -1.92098,1.64252 31.151221,33.78691 0 0 1 -2.034,1.47428 31.151221,33.78691 0 0 1 -2.13375,1.2962 31.151221,33.78691 0 0 1 -2.221695,1.11038 31.151221,33.78691 0 0 1 -2.294328,0.91831 31.151221,33.78691 0 0 1 -2.354157,0.72006 31.151221,33.78691 0 0 1 -2.399184,0.51765 31.151221,33.78691 0 0 1 -2.428831,0.31163 31.151221,33.78691 0 0 1 -2.444189,0.10457 31.151221,33.78691 0 0 1 -2.44418,-0.10457 31.151221,33.78691 0 0 1 -2.428838,-0.31162 31.151221,33.78691 0 0 1 -2.399177,-0.51765 31.151221,33.78691 0 0 1 -2.354163,-0.72006 31.151221,33.78691 0 0 1 -2.29484,-0.91832 31.151221,33.78691 0 0 1 -2.221196,-1.11037 31.151221,33.78691 0 0 1 -2.134249,-1.29621 31.151221,33.78691 0 0 1 -2.033489,-1.47428 31.151221,33.78691 0 0 1 -1.920973,-1.64251 31.151221,33.78691 0 0 1 -1.796194,-1.8004 31.151221,33.78691 0 0 1 -1.660643,-1.94845 31.151221,33.78691 0 0 1 -1.513873,-2.08304 31.151221,33.78691 0 0 1 -1.358905,-2.20625 31.151221,33.78691 0 0 1 -1.195236,-2.31443 31.151221,33.78691 0 0 1 -1.023911,-2.40916 31.151221,33.78691 0 0 1 -0.846946,-2.48888 31.151221,33.78691 0 0 1 -0.663854,-2.55359 31.151221,33.78691 0 0 1 -0.477175,-2.60173 31.151221,33.78691 0 0 1 -0.287434,-2.63486 31.151221,33.78691 0 0 1 -0.09615,-2.6509 31.151221,33.78691 0 0 1 0.09615,-2.65091 31.151221,33.78691 0 0 1 0.287434,-2.63434 31.151221,33.78691 0 0 1 0.477175,-2.60173 31.151221,33.78691 0 0 1 0.663854,-2.55359 31.151221,33.78691 0 0 1 0.846946,-2.48888 31.151221,33.78691 0 0 1 1.023911,-2.40916 31.151221,33.78691 0 0 1 1.195236,-2.31496 31.151221,33.78691 0 0 1 1.358905,-2.20572 31.151221,33.78691 0 0 1 1.513873,-2.08356 31.151221,33.78691 0 0 1 1.660643,-1.94793 31.151221,33.78691 0 0 1 1.796194,-1.80092 31.151221,33.78691 0 0 1 1.920973,-1.64252 31.151221,33.78691 0 0 1 2.033505,-1.47376 31.151221,33.78691 0 0 1 2.13425,-1.2962 31.151221,33.78691 0 0 1 2.221195,-1.11089 31.151221,33.78691 0 0 1 2.294841,-0.9178 31.151221,33.78691 0 0 1 2.354165,-0.72057 31.151221,33.78691 0 0 1 2.399176,-0.51714 31.151221,33.78691 0 0 1 2.428838,-0.31215 31.151221,33.78691 0 0 1 2.44418,-0.10404 z"
+ clip-path="url(#clipPath20719)"
+ transform="translate(-177.74838,-111.36079)"
+ inkscape:export-filename="C:\Users\a116178\Downloads\Taler\taler 512.png"
+ inkscape:export-xdpi="96.231026"
+ inkscape:export-ydpi="96.231026"
+ inkscape:connector-curvature="0" />
+ <path
+ id="path1306"
+ style="opacity:1;fill:#0042b3;fill-opacity:1;fill-rule:evenodd;stroke:#df373a;stroke-width:0.0376767;stroke-linejoin:round;stroke-opacity:1"
+ d="m 90.379694,105.31927 a 40.722405,43.678338 0 0 0 -40.72207,43.67871 40.722405,43.678338 0 0 0 40.72207,43.67812 40.722405,43.678338 0 0 0 40.722646,-43.67812 40.722405,43.678338 0 0 0 -40.722646,-43.67871 z m -0.0809,5.55644 a 35.68863,38.243712 0 0 1 0.94981,0.0135 35.68863,38.243712 0 0 1 0.94922,0.041 35.68863,38.243712 0 0 1 0.94805,0.0674 35.68863,38.243712 0 0 1 0.94628,0.0949 35.68863,38.243712 0 0 1 0.94336,0.12188 35.68863,38.243712 0 0 1 0.93984,0.14824 35.68863,38.243712 0 0 1 0.93574,0.1752 35.68863,38.243712 0 0 1 0.93165,0.20214 35.68863,38.243712 0 0 1 0.925784,0.22852 35.68863,38.243712 0 0 1 0.91992,0.25488 35.68863,38.243712 0 0 1 0.913482,0.28067 35.68863,38.243712 0 0 1 0.90586,0.30703 35.68863,38.243712 0 0 1 0.89765,0.33281 35.68863,38.243712 0 0 1 0.88945,0.35801 35.68863,38.243712 0 0 1 0.88007,0.3832 35.68863,38.243712 0 0 1 0.87071,0.4084 35.68863,38.243712 0 0 1 0.86015,0.43301 35.68863,38.243712 0 0 1 0.84844,0.45762 35.68863,38.243712 0 0 1 0.8373,0.48105 35.68863,38.243712 0 0 1 0.825,0.50508 35.68863,38.243712 0 0 1 0.81211,0.52851 35.68863,38.243712 0 0 1 0.79863,0.55196 35.68863,38.243712 0 0 1 0.78458,0.57363 35.68863,38.243712 0 0 1 0.7705,0.59648 35.68863,38.243712 0 0 1 0.75469,0.61817 35.68863,38.243712 0 0 1 0.73946,0.63926 35.68863,38.243712 0 0 1 0.72363,0.66035 35.68863,38.243712 0 0 1 0.70664,0.68027 35.68863,38.243712 0 0 1 0.68965,0.70078 35.68863,38.243712 0 0 1 0.67148,0.71953 35.68863,38.243712 0 0 1 0.65391,0.73887 35.68863,38.243712 0 0 1 0.63515,0.75762 35.68863,38.243712 0 0 1 0.61582,0.77519 35.68863,38.243712 0 0 1 0.59649,0.79219 35.68863,38.243712 0 0 1 0.57715,0.80918 35.68863,38.243712 0 0 1 0.55605,0.825 35.68863,38.243712 0 0 1 0.53613,0.84082 35.68863,38.243712 0 0 1 0.51446,0.85606 35.68863,38.243712 0 0 1 0.49336,0.87011 35.68863,38.243712 0 0 1 0.47109,0.88418 35.68863,38.243712 0 0 1 0.44941,0.89707 35.68863,38.243712 0 0 1 0.42715,0.90996 35.68863,38.243712 0 0 1 0.40371,0.9211 35.68863,38.243712 0 0 1 0.38145,0.93281 35.68863,38.243712 0 0 1 0.35742,0.94336 35.68863,38.243712 0 0 1 0.33399,0.95273 35.68863,38.243712 0 0 1 0.31054,0.9627 35.68863,38.243712 0 0 1 0.28653,0.9709 35.68863,38.243712 0 0 1 0.26191,0.97851 35.68863,38.243712 0 0 1 0.23789,0.98555 35.68863,38.243712 0 0 1 0.21328,0.99258 35.68863,38.243712 0 0 1 0.18867,0.99785 35.68863,38.243712 0 0 1 0.16348,1.00254 35.68863,38.243712 0 0 1 0.13828,1.00723 35.68863,38.243712 0 0 1 0.11367,1.01132 35.68863,38.243712 0 0 1 0.0885,1.01368 35.68863,38.243712 0 0 1 0.0633,1.01601 35.68863,38.243712 0 0 1 0.0381,1.01719 35.68863,38.243712 0 0 1 0.0123,1.01778 35.68863,38.243712 0 0 1 -0.10957,3.00058 35.68863,38.243712 0 0 1 -0.32988,2.98243 35.68863,38.243712 0 0 1 -0.54668,2.94492 35.68863,38.243712 0 0 1 -0.76055,2.89043 35.68863,38.243712 0 0 1 -0.96972,2.81718 35.68863,38.243712 0 0 1 -1.17305,2.72696 35.68863,38.243712 0 0 1 -1.36934,2.61972 35.68863,38.243712 0 0 1 -1.55683,2.49727 35.68863,38.243712 0 0 1 -1.73496,2.35781 35.68863,38.243712 0 0 1 -1.90196,2.20547 35.68863,38.243712 0 0 1 -2.05781,2.03789 35.68863,38.243712 0 0 1 -2.20078,1.85918 35.68863,38.243712 0 0 1 -2.33027,1.66875 35.68863,38.243712 0 0 1 -2.44454,1.46719 35.68863,38.243712 0 0 1 -2.54531,1.25683 35.68863,38.243712 0 0 1 -2.62851,1.03946 35.68863,38.243712 0 0 1 -2.697062,0.81504 35.68863,38.243712 0 0 1 -2.748644,0.58593 35.68863,38.243712 0 0 1 -2.78261,0.35274 35.68863,38.243712 0 0 1 -2.8002,0.11836 35.68863,38.243712 0 0 1 -2.80019,-0.11836 35.68863,38.243712 0 0 1 -2.78262,-0.35274 35.68863,38.243712 0 0 1 -2.74863,-0.58593 35.68863,38.243712 0 0 1 -2.69707,-0.81504 35.68863,38.243712 0 0 1 -2.6291,-1.03946 35.68863,38.243712 0 0 1 -2.54473,-1.25683 35.68863,38.243712 0 0 1 -2.44512,-1.46719 35.68863,38.243712 0 0 1 -2.32968,-1.66875 35.68863,38.243712 0 0 1 -2.20078,-1.85918 35.68863,38.243712 0 0 1 -2.05782,-2.03789 35.68863,38.243712 0 0 1 -1.90253,-2.20547 35.68863,38.243712 0 0 1 -1.73438,-2.35781 35.68863,38.243712 0 0 1 -1.55684,-2.49727 35.68863,38.243712 0 0 1 -1.36933,-2.61972 35.68863,38.243712 0 0 1 -1.17305,-2.72696 35.68863,38.243712 0 0 1 -0.97031,-2.81718 35.68863,38.243712 0 0 1 -0.76055,-2.89043 35.68863,38.243712 0 0 1 -0.54668,-2.94492 35.68863,38.243712 0 0 1 -0.3293,-2.98243 35.68863,38.243712 0 0 1 -0.11015,-3.00058 35.68863,38.243712 0 0 1 0.11015,-3.00059 35.68863,38.243712 0 0 1 0.3293,-2.98184 35.68863,38.243712 0 0 1 0.54668,-2.94492 35.68863,38.243712 0 0 1 0.76055,-2.89043 35.68863,38.243712 0 0 1 0.97031,-2.81719 35.68863,38.243712 0 0 1 1.17305,-2.72695 35.68863,38.243712 0 0 1 1.36933,-2.62031 35.68863,38.243712 0 0 1 1.55684,-2.49668 35.68863,38.243712 0 0 1 1.73438,-2.3584 35.68863,38.243712 0 0 1 1.90253,-2.20489 35.68863,38.243712 0 0 1 2.05782,-2.03847 35.68863,38.243712 0 0 1 2.20078,-1.85918 35.68863,38.243712 0 0 1 2.32968,-1.66817 35.68863,38.243712 0 0 1 2.44512,-1.46718 35.68863,38.243712 0 0 1 2.54473,-1.25743 35.68863,38.243712 0 0 1 2.6291,-1.03886 35.68863,38.243712 0 0 1 2.69707,-0.81563 35.68863,38.243712 0 0 1 2.74863,-0.58535 35.68863,38.243712 0 0 1 2.78262,-0.35332 35.68863,38.243712 0 0 1 2.80019,-0.11777 z"
+ clip-path="url(#clipPath20715)"
+ transform="translate(-177.74838,-111.36079)"
+ inkscape:export-filename="C:\Users\a116178\Downloads\Taler\taler 512.png"
+ inkscape:export-xdpi="96.231026"
+ inkscape:export-ydpi="96.231026"
+ inkscape:connector-curvature="0" />
+ <path
+ id="path1306-7"
+ style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.0427732;stroke-linejoin:round;stroke-opacity:1"
+ d="M 90.531339,99.785944 A 46.363577,49.444797 0 0 0 44.168144,149.23116 46.363577,49.444797 0 0 0 90.531339,198.67572 46.363577,49.444797 0 0 0 136.89519,149.23116 46.363577,49.444797 0 0 0 90.531339,99.785944 Z m -0.09212,6.290016 a 40.632485,43.292687 0 0 1 1.081384,0.0153 40.632485,43.292687 0 0 1 1.080714,0.0464 40.632485,43.292687 0 0 1 1.079381,0.0763 40.632485,43.292687 0 0 1 1.077365,0.10745 40.632485,43.292687 0 0 1 1.074041,0.13797 40.632485,43.292687 0 0 1 1.070034,0.16781 40.632485,43.292687 0 0 1 1.065366,0.19832 40.632485,43.292687 0 0 1 1.060704,0.22884 40.632485,43.292687 0 0 1 1.054032,0.25868 40.632485,43.292687 0 0 1 1.04736,0.28854 40.632485,43.292687 0 0 1 1.04002,0.31772 40.632485,43.292687 0 0 1 1.03135,0.34756 40.632485,43.292687 0 0 1 1.02201,0.37675 40.632485,43.292687 0 0 1 1.01266,0.40527 40.632485,43.292687 0 0 1 1.00198,0.4338 40.632485,43.292687 0 0 1 0.99134,0.46231 40.632485,43.292687 0 0 1 0.97929,0.49018 40.632485,43.292687 0 0 1 0.96598,0.51803 40.632485,43.292687 0 0 1 0.9533,0.54456 40.632485,43.292687 0 0 1 0.93927,0.57176 40.632485,43.292687 0 0 1 0.92461,0.59829 40.632485,43.292687 0 0 1 0.90926,0.62483 40.632485,43.292687 0 0 1 0.89328,0.64936 40.632485,43.292687 0 0 1 0.87724,0.67523 40.632485,43.292687 0 0 1 0.85922,0.69978 40.632485,43.292687 0 0 1 0.8419,0.72365 40.632485,43.292687 0 0 1 0.82388,0.74753 40.632485,43.292687 0 0 1 0.80452,0.77009 40.632485,43.292687 0 0 1 0.78518,0.7933 40.632485,43.292687 0 0 1 0.7645,0.81452 40.632485,43.292687 0 0 1 0.74449,0.83642 40.632485,43.292687 0 0 1 0.72314,0.85763 40.632485,43.292687 0 0 1 0.70113,0.87754 40.632485,43.292687 0 0 1 0.67912,0.89677 40.632485,43.292687 0 0 1 0.65711,0.91601 40.632485,43.292687 0 0 1 0.63307,0.93392 40.632485,43.292687 0 0 1 0.6104,0.95183 40.632485,43.292687 0 0 1 0.58573,0.96907 40.632485,43.292687 0 0 1 0.56171,0.98499 40.632485,43.292687 0 0 1 0.53634,1.00091 40.632485,43.292687 0 0 1 0.51167,1.0155 40.632485,43.292687 0 0 1 0.48631,1.0301 40.632485,43.292687 0 0 1 0.45965,1.04269 40.632485,43.292687 0 0 1 0.43428,1.05597 40.632485,43.292687 0 0 1 0.40694,1.0679 40.632485,43.292687 0 0 1 0.38025,1.07851 40.632485,43.292687 0 0 1 0.35357,1.0898 40.632485,43.292687 0 0 1 0.32622,1.09908 40.632485,43.292687 0 0 1 0.29818,1.10769 40.632485,43.292687 0 0 1 0.27085,1.11566 40.632485,43.292687 0 0 1 0.24283,1.12362 40.632485,43.292687 0 0 1 0.2148,1.12959 40.632485,43.292687 0 0 1 0.18612,1.1349 40.632485,43.292687 0 0 1 0.15745,1.1402 40.632485,43.292687 0 0 1 0.12941,1.14484 40.632485,43.292687 0 0 1 0.10078,1.1475 40.632485,43.292687 0 0 1 0.072,1.15015 40.632485,43.292687 0 0 1 0.0434,1.15148 40.632485,43.292687 0 0 1 0.014,1.15214 40.632485,43.292687 0 0 1 -0.12477,3.39673 40.632485,43.292687 0 0 1 -0.37557,3.37616 40.632485,43.292687 0 0 1 -0.6224,3.33372 40.632485,43.292687 0 0 1 -0.86591,3.27203 40.632485,43.292687 0 0 1 -1.10406,3.18911 40.632485,43.292687 0 0 1 -1.33555,3.08697 40.632485,43.292687 0 0 1 -1.55903,2.96558 40.632485,43.292687 0 0 1 -1.77249,2.82696 40.632485,43.292687 0 0 1 -1.9753,2.66909 40.632485,43.292687 0 0 1 -2.16544,2.49664 40.632485,43.292687 0 0 1 -2.34287,2.30693 40.632485,43.292687 0 0 1 -2.50564,2.10464 40.632485,43.292687 0 0 1 -2.65309,1.88906 40.632485,43.292687 0 0 1 -2.78317,1.66088 40.632485,43.292687 0 0 1 -2.89791,1.42277 40.632485,43.292687 0 0 1 -2.99264,1.17668 40.632485,43.292687 0 0 1 -3.070682,0.92264 40.632485,43.292687 0 0 1 -3.129401,0.6633 40.632485,43.292687 0 0 1 -3.168078,0.3993 40.632485,43.292687 0 0 1 -3.188104,0.13399 40.632485,43.292687 0 0 1 -3.188094,-0.13399 40.632485,43.292687 0 0 1 -3.168088,-0.3993 40.632485,43.292687 0 0 1 -3.12939,-0.6633 40.632485,43.292687 0 0 1 -3.07069,-0.92264 40.632485,43.292687 0 0 1 -2.993303,-1.17668 40.632485,43.292687 0 0 1 -2.897244,-1.42277 40.632485,43.292687 0 0 1 -2.783837,-1.66088 40.632485,43.292687 0 0 1 -2.652404,-1.88906 40.632485,43.292687 0 0 1 -2.505648,-2.10464 40.632485,43.292687 0 0 1 -2.342886,-2.30693 40.632485,43.292687 0 0 1 -2.166082,-2.49664 40.632485,43.292687 0 0 1 -1.974639,-2.66909 40.632485,43.292687 0 0 1 -1.772504,-2.82696 40.632485,43.292687 0 0 1 -1.559022,-2.96558 40.632485,43.292687 0 0 1 -1.335549,-3.08697 40.632485,43.292687 0 0 1 -1.104725,-3.18911 40.632485,43.292687 0 0 1 -0.865908,-3.27203 40.632485,43.292687 0 0 1 -0.622408,-3.33372 40.632485,43.292687 0 0 1 -0.374918,-3.37616 40.632485,43.292687 0 0 1 -0.125409,-3.39673 40.632485,43.292687 0 0 1 0.125409,-3.39673 40.632485,43.292687 0 0 1 0.374918,-3.3755 40.632485,43.292687 0 0 1 0.622408,-3.33371 40.632485,43.292687 0 0 1 0.865908,-3.27203 40.632485,43.292687 0 0 1 1.104725,-3.18911 40.632485,43.292687 0 0 1 1.335549,-3.08697 40.632485,43.292687 0 0 1 1.559022,-2.96625 40.632485,43.292687 0 0 1 1.772504,-2.82629 40.632485,43.292687 0 0 1 1.974639,-2.66976 40.632485,43.292687 0 0 1 2.166082,-2.49597 40.632485,43.292687 0 0 1 2.342886,-2.3076 40.632485,43.292687 0 0 1 2.505648,-2.10463 40.632485,43.292687 0 0 1 2.652426,-1.8884 40.632485,43.292687 0 0 1 2.783836,-1.66089 40.632485,43.292687 0 0 1 2.897245,-1.42343 40.632485,43.292687 0 0 1 2.993303,-1.17601 40.632485,43.292687 0 0 1 3.070689,-0.92331 40.632485,43.292687 0 0 1 3.12939,-0.66263 40.632485,43.292687 0 0 1 3.16809,-0.39997 40.632485,43.292687 0 0 1 3.188093,-0.13332 z"
+ clip-path="url(#clipPath20711)"
+ transform="translate(-177.74838,-111.36079)"
+ inkscape:connector-curvature="0" />
+ <path
+ id="path1306-7-63"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.0330856;stroke-linejoin:round;stroke-opacity:1"
+ d="m 90.144156,110.56416 a 35.545008,38.588202 0 0 0 -35.544717,38.58853 35.545008,38.588202 0 0 0 35.544717,38.58802 35.545008,38.588202 0 0 0 35.545224,-38.58802 35.545008,38.588202 0 0 0 -35.545224,-38.58853 z m -0.07061,4.90892 a 31.151221,33.78691 0 0 1 0.829048,0.0119 31.151221,33.78691 0 0 1 0.82854,0.0362 31.151221,33.78691 0 0 1 0.827519,0.0595 31.151221,33.78691 0 0 1 0.825964,0.0839 31.151221,33.78691 0 0 1 0.823425,0.10767 31.151221,33.78691 0 0 1 0.820349,0.13097 31.151221,33.78691 0 0 1 0.816775,0.15478 31.151221,33.78691 0 0 1 0.813198,0.17859 31.151221,33.78691 0 0 1 0.808083,0.20188 31.151221,33.78691 0 0 1 0.802953,0.22518 31.151221,33.78691 0 0 1 0.797338,0.24796 31.151221,33.78691 0 0 1 0.7907,0.27125 31.151221,33.78691 0 0 1 0.783542,0.29403 31.151221,33.78691 0 0 1 0.77635,0.31629 31.151221,33.78691 0 0 1 0.76818,0.33854 31.151221,33.78691 0 0 1 0.76002,0.3608 31.151221,33.78691 0 0 1 0.75079,0.38255 31.151221,33.78691 0 0 1 0.74058,0.40429 31.151221,33.78691 0 0 1 0.73085,0.42499 31.151221,33.78691 0 0 1 0.7201,0.44622 31.151221,33.78691 0 0 1 0.70885,0.46692 31.151221,33.78691 0 0 1 0.6971,0.48764 31.151221,33.78691 0 0 1 0.68484,0.50678 31.151221,33.78691 0 0 1 0.67254,0.52697 31.151221,33.78691 0 0 1 0.65872,0.54613 31.151221,33.78691 0 0 1 0.64545,0.56476 31.151221,33.78691 0 0 1 0.63165,0.58339 31.151221,33.78691 0 0 1 0.61678,0.601 31.151221,33.78691 0 0 1 0.60196,0.61911 31.151221,33.78691 0 0 1 0.58611,0.63568 31.151221,33.78691 0 0 1 0.57078,0.65277 31.151221,33.78691 0 0 1 0.5544,0.66932 31.151221,33.78691 0 0 1 0.53751,0.68486 31.151221,33.78691 0 0 1 0.52066,0.69987 31.151221,33.78691 0 0 1 0.50379,0.71488 31.151221,33.78691 0 0 1 0.48534,0.72885 31.151221,33.78691 0 0 1 0.46798,0.74284 31.151221,33.78691 0 0 1 0.44904,0.75629 31.151221,33.78691 0 0 1 0.43065,0.76872 31.151221,33.78691 0 0 1 0.41119,0.78114 31.151221,33.78691 0 0 1 0.39227,0.79253 31.151221,33.78691 0 0 1 0.37284,0.80391 31.151221,33.78691 0 0 1 0.35239,0.81376 31.151221,33.78691 0 0 1 0.33294,0.8241 31.151221,33.78691 0 0 1 0.31198,0.83342 31.151221,33.78691 0 0 1 0.29153,0.84171 31.151221,33.78691 0 0 1 0.27107,0.85051 31.151221,33.78691 0 0 1 0.2501,0.85775 31.151221,33.78691 0 0 1 0.2286,0.86448 31.151221,33.78691 0 0 1 0.20765,0.8707 31.151221,33.78691 0 0 1 0.18616,0.8769 31.151221,33.78691 0 0 1 0.16468,0.88157 31.151221,33.78691 0 0 1 0.14268,0.8857 31.151221,33.78691 0 0 1 0.12073,0.88985 31.151221,33.78691 0 0 1 0.0992,0.89347 31.151221,33.78691 0 0 1 0.0772,0.89554 31.151221,33.78691 0 0 1 0.0553,0.89761 31.151221,33.78691 0 0 1 0.0332,0.89865 31.151221,33.78691 0 0 1 0.0108,0.89917 31.151221,33.78691 0 0 1 -0.0957,2.6509 31.151221,33.78691 0 0 1 -0.28795,2.63486 31.151221,33.78691 0 0 1 -0.47716,2.60173 31.151221,33.78691 0 0 1 -0.66385,2.55359 31.151221,33.78691 0 0 1 -0.84644,2.48888 31.151221,33.78691 0 0 1 -1.0239,2.40916 31.151221,33.78691 0 0 1 -1.19526,2.31444 31.151221,33.78691 0 0 1 -1.3589,2.20624 31.151221,33.78691 0 0 1 -1.51437,2.08304 31.151221,33.78691 0 0 1 -1.66015,1.94845 31.151221,33.78691 0 0 1 -1.79618,1.8004 31.151221,33.78691 0 0 1 -1.92098,1.64252 31.151221,33.78691 0 0 1 -2.034,1.47428 31.151221,33.78691 0 0 1 -2.13375,1.2962 31.151221,33.78691 0 0 1 -2.2217,1.11037 31.151221,33.78691 0 0 1 -2.294332,0.91832 31.151221,33.78691 0 0 1 -2.354157,0.72006 31.151221,33.78691 0 0 1 -2.399184,0.51765 31.151221,33.78691 0 0 1 -2.428831,0.31163 31.151221,33.78691 0 0 1 -2.444189,0.10457 31.151221,33.78691 0 0 1 -2.44418,-0.10457 31.151221,33.78691 0 0 1 -2.428838,-0.31163 31.151221,33.78691 0 0 1 -2.399177,-0.51765 31.151221,33.78691 0 0 1 -2.354163,-0.72006 31.151221,33.78691 0 0 1 -2.29484,-0.91831 31.151221,33.78691 0 0 1 -2.221196,-1.11037 31.151221,33.78691 0 0 1 -2.134249,-1.29621 31.151221,33.78691 0 0 1 -2.033489,-1.47428 31.151221,33.78691 0 0 1 -1.920973,-1.64252 31.151221,33.78691 0 0 1 -1.796194,-1.8004 31.151221,33.78691 0 0 1 -1.660643,-1.94845 31.151221,33.78691 0 0 1 -1.513873,-2.08304 31.151221,33.78691 0 0 1 -1.358905,-2.20624 31.151221,33.78691 0 0 1 -1.195236,-2.31443 31.151221,33.78691 0 0 1 -1.023911,-2.40916 31.151221,33.78691 0 0 1 -0.846946,-2.48888 31.151221,33.78691 0 0 1 -0.663854,-2.55359 31.151221,33.78691 0 0 1 -0.477175,-2.60173 31.151221,33.78691 0 0 1 -0.287434,-2.63486 31.151221,33.78691 0 0 1 -0.09615,-2.65091 31.151221,33.78691 0 0 1 0.09615,-2.65091 31.151221,33.78691 0 0 1 0.287434,-2.63433 31.151221,33.78691 0 0 1 0.477175,-2.60174 31.151221,33.78691 0 0 1 0.663854,-2.55358 31.151221,33.78691 0 0 1 0.846946,-2.48889 31.151221,33.78691 0 0 1 1.023911,-2.40916 31.151221,33.78691 0 0 1 1.195236,-2.31495 31.151221,33.78691 0 0 1 1.358905,-2.20572 31.151221,33.78691 0 0 1 1.513873,-2.08356 31.151221,33.78691 0 0 1 1.660643,-1.94793 31.151221,33.78691 0 0 1 1.796194,-1.80092 31.151221,33.78691 0 0 1 1.920973,-1.64252 31.151221,33.78691 0 0 1 2.033505,-1.47376 31.151221,33.78691 0 0 1 2.13425,-1.29621 31.151221,33.78691 0 0 1 2.221195,-1.11088 31.151221,33.78691 0 0 1 2.294841,-0.9178 31.151221,33.78691 0 0 1 2.354165,-0.72058 31.151221,33.78691 0 0 1 2.399176,-0.51713 31.151221,33.78691 0 0 1 2.428838,-0.31215 31.151221,33.78691 0 0 1 2.44418,-0.10405 z"
+ clip-path="url(#clipPath20707)"
+ transform="translate(-177.74838,-111.36079)"
+ inkscape:export-filename="C:\Users\a116178\Downloads\Taler\taler 512.png"
+ inkscape:export-xdpi="96.231026"
+ inkscape:export-ydpi="96.231026"
+ inkscape:connector-curvature="0" />
+ <path
+ d="m 119.20127,221.87113 c 15.58969,0 29.12922,9.40117 35.96102,23.20181 h -5.81736 c -6.31922,-10.6997 -17.45681,-17.80491 -30.14366,-17.80491 -19.690574,0 -35.652879,17.112 -35.652879,38.22056 0,10.3318 3.825597,19.70468 10.03957,26.58295 -1.342357,1.12091 -2.771532,2.1279 -4.275488,3.00675 -6.701874,-7.77494 -10.798502,-18.16843 -10.798502,-29.5897 0,-24.08921 18.216325,-43.61746 40.687299,-43.61746 z m 35.852,64.25471 c -6.86645,13.68013 -20.34561,22.98022 -35.852,22.98022 -1.0527,0 -2.0961,-0.0429 -3.12869,-0.12703 3.0522,-1.56117 5.91359,-3.48039 8.53831,-5.70731 10.32096,-1.68438 19.18598,-8.11363 24.60181,-17.14588 z"
+ id="path2350-0"
+ style="fill:#0042b3;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.327943"
+ clip-path="url(#clipPath20703)"
+ transform="translate(-206.48906,-227.91266)"
+ inkscape:export-filename="C:\Users\a116178\Downloads\Taler\taler 512.png"
+ inkscape:export-xdpi="96.231026"
+ inkscape:export-ydpi="96.231026"
+ inkscape:connector-curvature="0" />
+ <path
+ d="m 96.751486,221.87113 c 1.052607,0 2.095998,0.0429 3.128684,0.12706 -3.052192,1.56117 -5.913678,3.48036 -8.538403,5.70731 -17.123111,2.7943 -30.243159,18.646 -30.243159,37.78309 0,14.26457 7.29059,26.70203 18.093843,33.26893 -1.593656,0.26719 -3.226966,0.40695 -4.890748,0.40695 -1.239545,0 -2.46151,-0.0795 -3.663522,-0.22937 -8.907938,-8.00114 -14.573991,-20.01353 -14.573991,-33.44651 0,-24.08921 18.216324,-43.61746 40.687296,-43.61746 z m 5.409714,81.40059 c 10.32112,-1.68438 19.18608,-8.11394 24.60197,-17.14636 h 5.84051 c -6.86635,13.68031 -20.34554,22.9807 -35.852194,22.9807 -1.052703,0 -2.095999,-0.0429 -3.128684,-0.12703 3.052002,-1.56137 5.913836,-3.48022 8.538398,-5.70731 z m 24.7338,-58.19878 c -3.13939,-5.31472 -7.46755,-9.74275 -12.58451,-12.85327 1.59365,-0.26719 3.2269,-0.40695 4.89078,-0.40695 1.23945,0 2.46151,0.0795 3.66352,0.22937 4.01602,3.60724 7.3732,8.03011 9.84905,13.03085 z"
+ id="path2352-7"
+ style="fill:#0042b3;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.327943"
+ clip-path="url(#clipPath20699)"
+ transform="translate(-206.48906,-227.91266)"
+ inkscape:export-filename="C:\Users\a116178\Downloads\Taler\taler 512.png"
+ inkscape:export-xdpi="96.231026"
+ inkscape:export-ydpi="96.231026"
+ inkscape:connector-curvature="0" />
+ <path
+ d="m 74.301703,221.87113 c 1.064296,0 2.118804,0.0444 3.162607,0.13022 -3.046523,1.55896 -5.903162,3.47451 -8.52358,5.69681 -17.146992,2.77237 -30.291903,18.63524 -30.291903,37.79043 0,21.10857 15.962401,38.22057 35.652876,38.22057 12.599746,0 23.672446,-7.00705 30.013747,-17.5838 h 5.83852 c -6.86636,13.68031 -20.345616,22.9807 -35.852267,22.9807 -22.470907,0 -40.6872,-19.52825 -40.6872,-43.61747 0,-24.08921 18.216293,-43.61746 40.6872,-43.61746 z m 30.142787,23.20181 c -1.31192,-2.22057 -2.83098,-4.28705 -4.528878,-6.16651 1.342448,-1.12094 2.771378,-2.12838 4.275138,-3.00723 2.37299,2.75301 4.41888,5.83464 6.07249,9.17374 z"
+ id="path2354-0"
+ style="fill:#0042b3;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.327943"
+ clip-path="url(#clipPath20695)"
+ transform="translate(-206.48906,-227.91266)"
+ inkscape:export-filename="C:\Users\a116178\Downloads\Taler\taler 512.png"
+ inkscape:export-xdpi="96.231026"
+ inkscape:export-ydpi="96.231026"
+ inkscape:connector-curvature="0" />
+ <path
+ id="path1306-9"
+ style="fill:#0042b3;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.0376767;stroke-linejoin:round;stroke-opacity:1"
+ d="m 119.12038,221.87113 a 40.722405,43.678338 0 0 0 -40.722073,43.67871 40.722405,43.678338 0 0 0 40.722073,43.67813 40.722405,43.678338 0 0 0 40.72263,-43.67813 40.722405,43.678338 0 0 0 -40.72263,-43.67871 z m -0.0809,5.55645 a 35.68863,38.243712 0 0 1 0.94981,0.0135 35.68863,38.243712 0 0 1 0.94922,0.041 35.68863,38.243712 0 0 1 0.94805,0.0674 35.68863,38.243712 0 0 1 0.94628,0.0949 35.68863,38.243712 0 0 1 0.94336,0.12187 35.68863,38.243712 0 0 1 0.93984,0.14824 35.68863,38.243712 0 0 1 0.93574,0.1752 35.68863,38.243712 0 0 1 0.93165,0.20215 35.68863,38.243712 0 0 1 0.92578,0.22851 35.68863,38.243712 0 0 1 0.91992,0.25489 35.68863,38.243712 0 0 1 0.91348,0.28066 35.68863,38.243712 0 0 1 0.90586,0.30703 35.68863,38.243712 0 0 1 0.89765,0.33281 35.68863,38.243712 0 0 1 0.88945,0.35801 35.68863,38.243712 0 0 1 0.88006,0.38321 35.68863,38.243712 0 0 1 0.87071,0.40839 35.68863,38.243712 0 0 1 0.86015,0.43301 35.68863,38.243712 0 0 1 0.84844,0.45762 35.68863,38.243712 0 0 1 0.8373,0.48105 35.68863,38.243712 0 0 1 0.825,0.50508 35.68863,38.243712 0 0 1 0.81211,0.52852 35.68863,38.243712 0 0 1 0.79863,0.55195 35.68863,38.243712 0 0 1 0.78458,0.57363 35.68863,38.243712 0 0 1 0.7705,0.59649 35.68863,38.243712 0 0 1 0.75469,0.61816 35.68863,38.243712 0 0 1 0.73946,0.63926 35.68863,38.243712 0 0 1 0.72363,0.66035 35.68863,38.243712 0 0 1 0.70664,0.68027 35.68863,38.243712 0 0 1 0.68965,0.70078 35.68863,38.243712 0 0 1 0.67148,0.71954 35.68863,38.243712 0 0 1 0.65391,0.73886 35.68863,38.243712 0 0 1 0.63515,0.75762 35.68863,38.243712 0 0 1 0.61582,0.7752 35.68863,38.243712 0 0 1 0.59649,0.79218 35.68863,38.243712 0 0 1 0.57715,0.80918 35.68863,38.243712 0 0 1 0.55605,0.825 35.68863,38.243712 0 0 1 0.53613,0.84082 35.68863,38.243712 0 0 1 0.51446,0.85606 35.68863,38.243712 0 0 1 0.49336,0.87012 35.68863,38.243712 0 0 1 0.47109,0.88417 35.68863,38.243712 0 0 1 0.44941,0.89708 35.68863,38.243712 0 0 1 0.42715,0.90996 35.68863,38.243712 0 0 1 0.40371,0.92109 35.68863,38.243712 0 0 1 0.38145,0.93281 35.68863,38.243712 0 0 1 0.35742,0.94336 35.68863,38.243712 0 0 1 0.33399,0.95274 35.68863,38.243712 0 0 1 0.31054,0.96269 35.68863,38.243712 0 0 1 0.28653,0.9709 35.68863,38.243712 0 0 1 0.26191,0.97852 35.68863,38.243712 0 0 1 0.23789,0.98554 35.68863,38.243712 0 0 1 0.21328,0.99258 35.68863,38.243712 0 0 1 0.18867,0.99785 35.68863,38.243712 0 0 1 0.16348,1.00254 35.68863,38.243712 0 0 1 0.13828,1.00723 35.68863,38.243712 0 0 1 0.11367,1.01133 35.68863,38.243712 0 0 1 0.0885,1.01367 35.68863,38.243712 0 0 1 0.0633,1.01601 35.68863,38.243712 0 0 1 0.0381,1.01719 35.68863,38.243712 0 0 1 0.0123,1.01778 35.68863,38.243712 0 0 1 -0.10957,3.00058 35.68863,38.243712 0 0 1 -0.32988,2.98242 35.68863,38.243712 0 0 1 -0.54668,2.94492 35.68863,38.243712 0 0 1 -0.76055,2.89044 35.68863,38.243712 0 0 1 -0.96972,2.81718 35.68863,38.243712 0 0 1 -1.17305,2.72696 35.68863,38.243712 0 0 1 -1.36934,2.61972 35.68863,38.243712 0 0 1 -1.55683,2.49727 35.68863,38.243712 0 0 1 -1.73496,2.35781 35.68863,38.243712 0 0 1 -1.90196,2.20547 35.68863,38.243712 0 0 1 -2.05781,2.03789 35.68863,38.243712 0 0 1 -2.20078,1.85918 35.68863,38.243712 0 0 1 -2.33027,1.66875 35.68863,38.243712 0 0 1 -2.44454,1.46718 35.68863,38.243712 0 0 1 -2.54531,1.25684 35.68863,38.243712 0 0 1 -2.6285,1.03945 35.68863,38.243712 0 0 1 -2.69706,0.81504 35.68863,38.243712 0 0 1 -2.74864,0.58594 35.68863,38.243712 0 0 1 -2.78261,0.35274 35.68863,38.243712 0 0 1 -2.8002,0.11836 35.68863,38.243712 0 0 1 -2.80019,-0.11836 35.68863,38.243712 0 0 1 -2.78262,-0.35274 35.68863,38.243712 0 0 1 -2.74863,-0.58594 35.68863,38.243712 0 0 1 -2.69707,-0.81504 35.68863,38.243712 0 0 1 -2.6291,-1.03945 35.68863,38.243712 0 0 1 -2.54473,-1.25684 35.68863,38.243712 0 0 1 -2.44512,-1.46718 35.68863,38.243712 0 0 1 -2.329683,-1.66875 35.68863,38.243712 0 0 1 -2.20078,-1.85918 35.68863,38.243712 0 0 1 -2.05782,-2.03789 35.68863,38.243712 0 0 1 -1.90253,-2.20547 35.68863,38.243712 0 0 1 -1.73438,-2.35781 35.68863,38.243712 0 0 1 -1.55684,-2.49727 35.68863,38.243712 0 0 1 -1.36933,-2.61972 35.68863,38.243712 0 0 1 -1.17305,-2.72696 35.68863,38.243712 0 0 1 -0.97031,-2.81718 35.68863,38.243712 0 0 1 -0.76055,-2.89044 35.68863,38.243712 0 0 1 -0.54668,-2.94492 35.68863,38.243712 0 0 1 -0.3293,-2.98242 35.68863,38.243712 0 0 1 -0.11015,-3.00058 35.68863,38.243712 0 0 1 0.11015,-3.00059 35.68863,38.243712 0 0 1 0.3293,-2.98184 35.68863,38.243712 0 0 1 0.54668,-2.94492 35.68863,38.243712 0 0 1 0.76055,-2.89043 35.68863,38.243712 0 0 1 0.97031,-2.81719 35.68863,38.243712 0 0 1 1.17305,-2.72695 35.68863,38.243712 0 0 1 1.36933,-2.62031 35.68863,38.243712 0 0 1 1.55684,-2.49668 35.68863,38.243712 0 0 1 1.73438,-2.3584 35.68863,38.243712 0 0 1 1.90253,-2.20488 35.68863,38.243712 0 0 1 2.05782,-2.03848 35.68863,38.243712 0 0 1 2.20078,-1.85918 35.68863,38.243712 0 0 1 2.329683,-1.66816 35.68863,38.243712 0 0 1 2.44512,-1.46719 35.68863,38.243712 0 0 1 2.54473,-1.25742 35.68863,38.243712 0 0 1 2.6291,-1.03887 35.68863,38.243712 0 0 1 2.69707,-0.81562 35.68863,38.243712 0 0 1 2.74863,-0.58536 35.68863,38.243712 0 0 1 2.78262,-0.35332 35.68863,38.243712 0 0 1 2.80019,-0.11777 z"
+ clip-path="url(#clipPath20671)"
+ transform="translate(-206.48906,-227.91266)"
+ inkscape:export-filename="C:\Users\a116178\Downloads\Taler\taler 512.png"
+ inkscape:export-xdpi="96.231026"
+ inkscape:export-ydpi="96.231026"
+ inkscape:connector-curvature="0" />
+ <path
+ id="path1306-7-63-4"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.0330856;stroke-linejoin:round;stroke-opacity:1"
+ d="m 118.88484,227.11603 a 35.545008,38.588202 0 0 0 -35.544718,38.58853 35.545008,38.588202 0 0 0 35.544718,38.58802 35.545008,38.588202 0 0 0 35.54521,-38.58802 35.545008,38.588202 0 0 0 -35.54521,-38.58853 z m -0.0706,4.90891 a 31.151221,33.78691 0 0 1 0.82905,0.0119 31.151221,33.78691 0 0 1 0.82854,0.0362 31.151221,33.78691 0 0 1 0.82752,0.0595 31.151221,33.78691 0 0 1 0.82596,0.0839 31.151221,33.78691 0 0 1 0.82342,0.10768 31.151221,33.78691 0 0 1 0.82035,0.13096 31.151221,33.78691 0 0 1 0.81678,0.15478 31.151221,33.78691 0 0 1 0.8132,0.17859 31.151221,33.78691 0 0 1 0.80808,0.20189 31.151221,33.78691 0 0 1 0.80295,0.22518 31.151221,33.78691 0 0 1 0.79734,0.24795 31.151221,33.78691 0 0 1 0.7907,0.27125 31.151221,33.78691 0 0 1 0.78353,0.29403 31.151221,33.78691 0 0 1 0.77636,0.31629 31.151221,33.78691 0 0 1 0.76818,0.33854 31.151221,33.78691 0 0 1 0.76002,0.36081 31.151221,33.78691 0 0 1 0.75078,0.38255 31.151221,33.78691 0 0 1 0.74058,0.40428 31.151221,33.78691 0 0 1 0.73085,0.425 31.151221,33.78691 0 0 1 0.7201,0.44622 31.151221,33.78691 0 0 1 0.70885,0.46692 31.151221,33.78691 0 0 1 0.6971,0.48763 31.151221,33.78691 0 0 1 0.68484,0.50678 31.151221,33.78691 0 0 1 0.67254,0.52698 31.151221,33.78691 0 0 1 0.65872,0.54612 31.151221,33.78691 0 0 1 0.64545,0.56476 31.151221,33.78691 0 0 1 0.63165,0.5834 31.151221,33.78691 0 0 1 0.61678,0.60099 31.151221,33.78691 0 0 1 0.60196,0.61912 31.151221,33.78691 0 0 1 0.58611,0.63568 31.151221,33.78691 0 0 1 0.57078,0.65276 31.151221,33.78691 0 0 1 0.5544,0.66933 31.151221,33.78691 0 0 1 0.53751,0.68485 31.151221,33.78691 0 0 1 0.52066,0.69987 31.151221,33.78691 0 0 1 0.50379,0.71488 31.151221,33.78691 0 0 1 0.48534,0.72886 31.151221,33.78691 0 0 1 0.46798,0.74284 31.151221,33.78691 0 0 1 0.44904,0.75629 31.151221,33.78691 0 0 1 0.43065,0.76871 31.151221,33.78691 0 0 1 0.41119,0.78114 31.151221,33.78691 0 0 1 0.39227,0.79253 31.151221,33.78691 0 0 1 0.37284,0.80392 31.151221,33.78691 0 0 1 0.35239,0.81375 31.151221,33.78691 0 0 1 0.33294,0.82411 31.151221,33.78691 0 0 1 0.31198,0.83342 31.151221,33.78691 0 0 1 0.29153,0.8417 31.151221,33.78691 0 0 1 0.27107,0.85051 31.151221,33.78691 0 0 1 0.2501,0.85775 31.151221,33.78691 0 0 1 0.2286,0.86448 31.151221,33.78691 0 0 1 0.20765,0.8707 31.151221,33.78691 0 0 1 0.18616,0.87691 31.151221,33.78691 0 0 1 0.16468,0.88156 31.151221,33.78691 0 0 1 0.14268,0.88571 31.151221,33.78691 0 0 1 0.12073,0.88985 31.151221,33.78691 0 0 1 0.0992,0.89347 31.151221,33.78691 0 0 1 0.0772,0.89554 31.151221,33.78691 0 0 1 0.0553,0.89761 31.151221,33.78691 0 0 1 0.0332,0.89865 31.151221,33.78691 0 0 1 0.0108,0.89917 31.151221,33.78691 0 0 1 -0.0957,2.6509 31.151221,33.78691 0 0 1 -0.28795,2.63486 31.151221,33.78691 0 0 1 -0.47716,2.60173 31.151221,33.78691 0 0 1 -0.66385,2.55359 31.151221,33.78691 0 0 1 -0.84644,2.48888 31.151221,33.78691 0 0 1 -1.0239,2.40916 31.151221,33.78691 0 0 1 -1.19526,2.31443 31.151221,33.78691 0 0 1 -1.3589,2.20625 31.151221,33.78691 0 0 1 -1.51437,2.08304 31.151221,33.78691 0 0 1 -1.66015,1.94845 31.151221,33.78691 0 0 1 -1.79618,1.8004 31.151221,33.78691 0 0 1 -1.92098,1.64251 31.151221,33.78691 0 0 1 -2.034,1.47429 31.151221,33.78691 0 0 1 -2.13375,1.2962 31.151221,33.78691 0 0 1 -2.22169,1.11037 31.151221,33.78691 0 0 1 -2.29433,0.91832 31.151221,33.78691 0 0 1 -2.35416,0.72006 31.151221,33.78691 0 0 1 -2.39918,0.51765 31.151221,33.78691 0 0 1 -2.42883,0.31163 31.151221,33.78691 0 0 1 -2.44419,0.10456 31.151221,33.78691 0 0 1 -2.44418,-0.10456 31.151221,33.78691 0 0 1 -2.42884,-0.31163 31.151221,33.78691 0 0 1 -2.39918,-0.51765 31.151221,33.78691 0 0 1 -2.35416,-0.72006 31.151221,33.78691 0 0 1 -2.29484,-0.91831 31.151221,33.78691 0 0 1 -2.22119,-1.11038 31.151221,33.78691 0 0 1 -2.13425,-1.2962 31.151221,33.78691 0 0 1 -2.03349,-1.47428 31.151221,33.78691 0 0 1 -1.920975,-1.64252 31.151221,33.78691 0 0 1 -1.796194,-1.8004 31.151221,33.78691 0 0 1 -1.660643,-1.94845 31.151221,33.78691 0 0 1 -1.513873,-2.08304 31.151221,33.78691 0 0 1 -1.358905,-2.20624 31.151221,33.78691 0 0 1 -1.195236,-2.31443 31.151221,33.78691 0 0 1 -1.023911,-2.40916 31.151221,33.78691 0 0 1 -0.846946,-2.48888 31.151221,33.78691 0 0 1 -0.663854,-2.55359 31.151221,33.78691 0 0 1 -0.477175,-2.60173 31.151221,33.78691 0 0 1 -0.287434,-2.63486 31.151221,33.78691 0 0 1 -0.09615,-2.65091 31.151221,33.78691 0 0 1 0.09615,-2.65091 31.151221,33.78691 0 0 1 0.287434,-2.63434 31.151221,33.78691 0 0 1 0.477175,-2.60173 31.151221,33.78691 0 0 1 0.663854,-2.55359 31.151221,33.78691 0 0 1 0.846946,-2.48888 31.151221,33.78691 0 0 1 1.023911,-2.40916 31.151221,33.78691 0 0 1 1.195236,-2.31495 31.151221,33.78691 0 0 1 1.358905,-2.20573 31.151221,33.78691 0 0 1 1.513873,-2.08355 31.151221,33.78691 0 0 1 1.660643,-1.94794 31.151221,33.78691 0 0 1 1.796194,-1.80092 31.151221,33.78691 0 0 1 1.920975,-1.64251 31.151221,33.78691 0 0 1 2.0335,-1.47377 31.151221,33.78691 0 0 1 2.13425,-1.2962 31.151221,33.78691 0 0 1 2.2212,-1.11089 31.151221,33.78691 0 0 1 2.29484,-0.9178 31.151221,33.78691 0 0 1 2.35416,-0.72057 31.151221,33.78691 0 0 1 2.39918,-0.51714 31.151221,33.78691 0 0 1 2.42884,-0.31215 31.151221,33.78691 0 0 1 2.44418,-0.10404 z"
+ clip-path="url(#clipPath20663)"
+ transform="translate(-206.48906,-227.91266)"
+ inkscape:export-filename="C:\Users\a116178\Downloads\Taler\taler 512.png"
+ inkscape:export-xdpi="96.231026"
+ inkscape:export-ydpi="96.231026"
+ inkscape:connector-curvature="0" />
+ <path
+ id="path1306-0-7"
+ style="fill:#0042b3;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.0376767;stroke-linejoin:round"
+ d="m 96.751486,221.87113 a 40.722405,43.678338 0 0 0 -40.72207,43.67871 40.722405,43.678338 0 0 0 40.72207,43.67813 40.722405,43.678338 0 0 0 40.722634,-43.67813 40.722405,43.678338 0 0 0 -40.722634,-43.67871 z m -0.0809,5.55645 a 35.68863,38.243712 0 0 1 0.94981,0.0135 35.68863,38.243712 0 0 1 0.94922,0.041 35.68863,38.243712 0 0 1 0.94805,0.0674 35.68863,38.243712 0 0 1 0.946284,0.0949 35.68863,38.243712 0 0 1 0.94336,0.12187 35.68863,38.243712 0 0 1 0.93984,0.14825 35.68863,38.243712 0 0 1 0.93574,0.17519 35.68863,38.243712 0 0 1 0.93165,0.20215 35.68863,38.243712 0 0 1 0.92578,0.22851 35.68863,38.243712 0 0 1 0.91992,0.25489 35.68863,38.243712 0 0 1 0.91348,0.28066 35.68863,38.243712 0 0 1 0.90586,0.30703 35.68863,38.243712 0 0 1 0.89765,0.33282 35.68863,38.243712 0 0 1 0.88945,0.358 35.68863,38.243712 0 0 1 0.88006,0.38321 35.68863,38.243712 0 0 1 0.87071,0.40839 35.68863,38.243712 0 0 1 0.86015,0.43301 35.68863,38.243712 0 0 1 0.84844,0.45762 35.68863,38.243712 0 0 1 0.8373,0.48105 35.68863,38.243712 0 0 1 0.825,0.50508 35.68863,38.243712 0 0 1 0.81211,0.52852 35.68863,38.243712 0 0 1 0.79863,0.55195 35.68863,38.243712 0 0 1 0.78458,0.57363 35.68863,38.243712 0 0 1 0.7705,0.59649 35.68863,38.243712 0 0 1 0.75469,0.61816 35.68863,38.243712 0 0 1 0.73946,0.63926 35.68863,38.243712 0 0 1 0.72363,0.66035 35.68863,38.243712 0 0 1 0.70664,0.68028 35.68863,38.243712 0 0 1 0.68965,0.70078 35.68863,38.243712 0 0 1 0.67148,0.71953 35.68863,38.243712 0 0 1 0.65391,0.73887 35.68863,38.243712 0 0 1 0.63515,0.75761 35.68863,38.243712 0 0 1 0.61582,0.7752 35.68863,38.243712 0 0 1 0.59649,0.79219 35.68863,38.243712 0 0 1 0.57715,0.80917 35.68863,38.243712 0 0 1 0.55605,0.825 35.68863,38.243712 0 0 1 0.53613,0.84083 35.68863,38.243712 0 0 1 0.51446,0.85605 35.68863,38.243712 0 0 1 0.49336,0.87012 35.68863,38.243712 0 0 1 0.47109,0.88418 35.68863,38.243712 0 0 1 0.44941,0.89707 35.68863,38.243712 0 0 1 0.42715,0.90996 35.68863,38.243712 0 0 1 0.40371,0.92109 35.68863,38.243712 0 0 1 0.38145,0.93282 35.68863,38.243712 0 0 1 0.35742,0.94335 35.68863,38.243712 0 0 1 0.33399,0.95274 35.68863,38.243712 0 0 1 0.31054,0.96269 35.68863,38.243712 0 0 1 0.28653,0.9709 35.68863,38.243712 0 0 1 0.26191,0.97852 35.68863,38.243712 0 0 1 0.23789,0.98554 35.68863,38.243712 0 0 1 0.21328,0.99258 35.68863,38.243712 0 0 1 0.18867,0.99786 35.68863,38.243712 0 0 1 0.16348,1.00253 35.68863,38.243712 0 0 1 0.13828,1.00723 35.68863,38.243712 0 0 1 0.11367,1.01133 35.68863,38.243712 0 0 1 0.0885,1.01367 35.68863,38.243712 0 0 1 0.0633,1.01601 35.68863,38.243712 0 0 1 0.0381,1.01719 35.68863,38.243712 0 0 1 0.0123,1.01778 35.68863,38.243712 0 0 1 -0.10957,3.00058 35.68863,38.243712 0 0 1 -0.32988,2.98243 35.68863,38.243712 0 0 1 -0.54668,2.94492 35.68863,38.243712 0 0 1 -0.76055,2.89043 35.68863,38.243712 0 0 1 -0.96972,2.81718 35.68863,38.243712 0 0 1 -1.17305,2.72696 35.68863,38.243712 0 0 1 -1.36934,2.61972 35.68863,38.243712 0 0 1 -1.55683,2.49727 35.68863,38.243712 0 0 1 -1.73496,2.35781 35.68863,38.243712 0 0 1 -1.90196,2.20547 35.68863,38.243712 0 0 1 -2.05781,2.03789 35.68863,38.243712 0 0 1 -2.20078,1.85918 35.68863,38.243712 0 0 1 -2.33027,1.66875 35.68863,38.243712 0 0 1 -2.44454,1.46719 35.68863,38.243712 0 0 1 -2.54531,1.25683 35.68863,38.243712 0 0 1 -2.6285,1.03946 35.68863,38.243712 0 0 1 -2.69706,0.81504 35.68863,38.243712 0 0 1 -2.74864,0.58593 35.68863,38.243712 0 0 1 -2.782614,0.35274 35.68863,38.243712 0 0 1 -2.8002,0.11836 35.68863,38.243712 0 0 1 -2.80019,-0.11836 35.68863,38.243712 0 0 1 -2.78262,-0.35274 35.68863,38.243712 0 0 1 -2.74863,-0.58593 35.68863,38.243712 0 0 1 -2.69707,-0.81504 35.68863,38.243712 0 0 1 -2.6291,-1.03946 35.68863,38.243712 0 0 1 -2.54473,-1.25683 35.68863,38.243712 0 0 1 -2.44512,-1.46719 35.68863,38.243712 0 0 1 -2.32968,-1.66875 35.68863,38.243712 0 0 1 -2.20078,-1.85918 35.68863,38.243712 0 0 1 -2.05782,-2.03789 35.68863,38.243712 0 0 1 -1.90253,-2.20547 35.68863,38.243712 0 0 1 -1.73438,-2.35781 35.68863,38.243712 0 0 1 -1.55684,-2.49727 35.68863,38.243712 0 0 1 -1.36933,-2.61972 35.68863,38.243712 0 0 1 -1.17305,-2.72696 35.68863,38.243712 0 0 1 -0.97031,-2.81718 35.68863,38.243712 0 0 1 -0.76055,-2.89043 35.68863,38.243712 0 0 1 -0.54668,-2.94492 35.68863,38.243712 0 0 1 -0.3293,-2.98243 35.68863,38.243712 0 0 1 -0.11015,-3.00058 35.68863,38.243712 0 0 1 0.11015,-3.00059 35.68863,38.243712 0 0 1 0.3293,-2.98184 35.68863,38.243712 0 0 1 0.54668,-2.94492 35.68863,38.243712 0 0 1 0.76055,-2.89043 35.68863,38.243712 0 0 1 0.97031,-2.81718 35.68863,38.243712 0 0 1 1.17305,-2.72696 35.68863,38.243712 0 0 1 1.36933,-2.62031 35.68863,38.243712 0 0 1 1.55684,-2.49668 35.68863,38.243712 0 0 1 1.73438,-2.3584 35.68863,38.243712 0 0 1 1.90253,-2.20488 35.68863,38.243712 0 0 1 2.05782,-2.03848 35.68863,38.243712 0 0 1 2.20078,-1.85918 35.68863,38.243712 0 0 1 2.32968,-1.66816 35.68863,38.243712 0 0 1 2.44512,-1.46719 35.68863,38.243712 0 0 1 2.54473,-1.25742 35.68863,38.243712 0 0 1 2.6291,-1.03887 35.68863,38.243712 0 0 1 2.69707,-0.81562 35.68863,38.243712 0 0 1 2.74863,-0.58535 35.68863,38.243712 0 0 1 2.78262,-0.35333 35.68863,38.243712 0 0 1 2.80019,-0.11777 z"
+ clip-path="url(#clipPath20687)"
+ transform="translate(-206.48906,-227.91266)"
+ inkscape:export-filename="C:\Users\a116178\Downloads\Taler\taler 512.png"
+ inkscape:export-xdpi="96.231026"
+ inkscape:export-ydpi="96.231026"
+ inkscape:connector-curvature="0" />
+ <path
+ id="path1306-7-63-9-8"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.0330856;stroke-linejoin:round;stroke-opacity:1"
+ d="m 74.220853,227.42758 a 35.545008,38.588202 0 0 0 -35.544717,38.58853 35.545008,38.588202 0 0 0 35.544717,38.58802 35.545008,38.588202 0 0 0 35.545217,-38.58802 35.545008,38.588202 0 0 0 -35.545217,-38.58853 z m -0.07061,4.90891 a 31.151221,33.78691 0 0 1 0.829048,0.0119 31.151221,33.78691 0 0 1 0.82854,0.0362 31.151221,33.78691 0 0 1 0.827519,0.0595 31.151221,33.78691 0 0 1 0.825964,0.0839 31.151221,33.78691 0 0 1 0.823425,0.10768 31.151221,33.78691 0 0 1 0.820349,0.13096 31.151221,33.78691 0 0 1 0.816775,0.15478 31.151221,33.78691 0 0 1 0.813198,0.17859 31.151221,33.78691 0 0 1 0.808083,0.20189 31.151221,33.78691 0 0 1 0.802953,0.22518 31.151221,33.78691 0 0 1 0.797339,0.24795 31.151221,33.78691 0 0 1 0.790697,0.27125 31.151221,33.78691 0 0 1 0.783536,0.29403 31.151221,33.78691 0 0 1 0.776355,0.31629 31.151221,33.78691 0 0 1 0.768177,0.33854 31.151221,33.78691 0 0 1 0.760022,0.36081 31.151221,33.78691 0 0 1 0.750783,0.38255 31.151221,33.78691 0 0 1 0.740582,0.40428 31.151221,33.78691 0 0 1 0.73085,0.425 31.151221,33.78691 0 0 1 0.7201,0.44622 31.151221,33.78691 0 0 1 0.70885,0.46692 31.151221,33.78691 0 0 1 0.6971,0.48763 31.151221,33.78691 0 0 1 0.68484,0.50678 31.151221,33.78691 0 0 1 0.67254,0.52698 31.151221,33.78691 0 0 1 0.65872,0.54612 31.151221,33.78691 0 0 1 0.64545,0.56476 31.151221,33.78691 0 0 1 0.63165,0.5834 31.151221,33.78691 0 0 1 0.61678,0.60099 31.151221,33.78691 0 0 1 0.60196,0.61912 31.151221,33.78691 0 0 1 0.58611,0.63568 31.151221,33.78691 0 0 1 0.57078,0.65276 31.151221,33.78691 0 0 1 0.5544,0.66933 31.151221,33.78691 0 0 1 0.53751,0.68485 31.151221,33.78691 0 0 1 0.52066,0.69987 31.151221,33.78691 0 0 1 0.50379,0.71488 31.151221,33.78691 0 0 1 0.48534,0.72886 31.151221,33.78691 0 0 1 0.467982,0.74283 31.151221,33.78691 0 0 1 0.44904,0.7563 31.151221,33.78691 0 0 1 0.43065,0.76871 31.151221,33.78691 0 0 1 0.41119,0.78114 31.151221,33.78691 0 0 1 0.39227,0.79253 31.151221,33.78691 0 0 1 0.37284,0.80392 31.151221,33.78691 0 0 1 0.35239,0.81375 31.151221,33.78691 0 0 1 0.33294,0.82411 31.151221,33.78691 0 0 1 0.31198,0.83342 31.151221,33.78691 0 0 1 0.29153,0.8417 31.151221,33.78691 0 0 1 0.27107,0.85051 31.151221,33.78691 0 0 1 0.2501,0.85776 31.151221,33.78691 0 0 1 0.2286,0.86447 31.151221,33.78691 0 0 1 0.20765,0.8707 31.151221,33.78691 0 0 1 0.18616,0.87691 31.151221,33.78691 0 0 1 0.16468,0.88156 31.151221,33.78691 0 0 1 0.14268,0.88571 31.151221,33.78691 0 0 1 0.12073,0.88985 31.151221,33.78691 0 0 1 0.0992,0.89347 31.151221,33.78691 0 0 1 0.0772,0.89554 31.151221,33.78691 0 0 1 0.0553,0.89761 31.151221,33.78691 0 0 1 0.0332,0.89865 31.151221,33.78691 0 0 1 0.0108,0.89917 31.151221,33.78691 0 0 1 -0.0957,2.6509 31.151221,33.78691 0 0 1 -0.28795,2.63486 31.151221,33.78691 0 0 1 -0.47716,2.60173 31.151221,33.78691 0 0 1 -0.66385,2.55359 31.151221,33.78691 0 0 1 -0.84644,2.48888 31.151221,33.78691 0 0 1 -1.0239,2.40916 31.151221,33.78691 0 0 1 -1.19526,2.31443 31.151221,33.78691 0 0 1 -1.358902,2.20625 31.151221,33.78691 0 0 1 -1.51437,2.08304 31.151221,33.78691 0 0 1 -1.66015,1.94845 31.151221,33.78691 0 0 1 -1.79618,1.8004 31.151221,33.78691 0 0 1 -1.92098,1.64252 31.151221,33.78691 0 0 1 -2.034,1.47428 31.151221,33.78691 0 0 1 -2.13375,1.2962 31.151221,33.78691 0 0 1 -2.221695,1.11037 31.151221,33.78691 0 0 1 -2.294328,0.91832 31.151221,33.78691 0 0 1 -2.354157,0.72006 31.151221,33.78691 0 0 1 -2.399184,0.51765 31.151221,33.78691 0 0 1 -2.428831,0.31163 31.151221,33.78691 0 0 1 -2.444189,0.10456 31.151221,33.78691 0 0 1 -2.44418,-0.10456 31.151221,33.78691 0 0 1 -2.428838,-0.31163 31.151221,33.78691 0 0 1 -2.399177,-0.51765 31.151221,33.78691 0 0 1 -2.354163,-0.72006 31.151221,33.78691 0 0 1 -2.29484,-0.91831 31.151221,33.78691 0 0 1 -2.221196,-1.11038 31.151221,33.78691 0 0 1 -2.134249,-1.2962 31.151221,33.78691 0 0 1 -2.033489,-1.47428 31.151221,33.78691 0 0 1 -1.920973,-1.64252 31.151221,33.78691 0 0 1 -1.796194,-1.8004 31.151221,33.78691 0 0 1 -1.660643,-1.94845 31.151221,33.78691 0 0 1 -1.513873,-2.08304 31.151221,33.78691 0 0 1 -1.358905,-2.20624 31.151221,33.78691 0 0 1 -1.195236,-2.31443 31.151221,33.78691 0 0 1 -1.023911,-2.40916 31.151221,33.78691 0 0 1 -0.846946,-2.48888 31.151221,33.78691 0 0 1 -0.663854,-2.55359 31.151221,33.78691 0 0 1 -0.477175,-2.60173 31.151221,33.78691 0 0 1 -0.287434,-2.63486 31.151221,33.78691 0 0 1 -0.09615,-2.65091 31.151221,33.78691 0 0 1 0.09615,-2.65091 31.151221,33.78691 0 0 1 0.287434,-2.63434 31.151221,33.78691 0 0 1 0.477175,-2.60173 31.151221,33.78691 0 0 1 0.663854,-2.55359 31.151221,33.78691 0 0 1 0.846946,-2.48888 31.151221,33.78691 0 0 1 1.023911,-2.40916 31.151221,33.78691 0 0 1 1.195236,-2.31495 31.151221,33.78691 0 0 1 1.358905,-2.20573 31.151221,33.78691 0 0 1 1.513873,-2.08355 31.151221,33.78691 0 0 1 1.660643,-1.94794 31.151221,33.78691 0 0 1 1.796194,-1.80092 31.151221,33.78691 0 0 1 1.920973,-1.64251 31.151221,33.78691 0 0 1 2.033505,-1.47376 31.151221,33.78691 0 0 1 2.13425,-1.29621 31.151221,33.78691 0 0 1 2.221195,-1.11089 31.151221,33.78691 0 0 1 2.294841,-0.9178 31.151221,33.78691 0 0 1 2.354165,-0.72057 31.151221,33.78691 0 0 1 2.399176,-0.51714 31.151221,33.78691 0 0 1 2.428838,-0.31214 31.151221,33.78691 0 0 1 2.44418,-0.10405 z"
+ clip-path="url(#clipPath20691)"
+ transform="translate(-206.48906,-227.91266)"
+ inkscape:export-filename="C:\Users\a116178\Downloads\Taler\taler 512.png"
+ inkscape:export-xdpi="96.231026"
+ inkscape:export-ydpi="96.231026"
+ inkscape:connector-curvature="0" />
+ <path
+ id="path1306-7-63-6-2"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.0330856;stroke-linejoin:round;stroke-opacity:1"
+ d="m 96.670636,227.42758 a 35.545008,38.588202 0 0 0 -35.544717,38.58854 35.545008,38.588202 0 0 0 35.544717,38.58801 35.545008,38.588202 0 0 0 35.545214,-38.58801 35.545008,38.588202 0 0 0 -35.545214,-38.58854 z m -0.07061,4.90892 a 31.151221,33.78691 0 0 1 0.829048,0.0119 31.151221,33.78691 0 0 1 0.82854,0.0362 31.151221,33.78691 0 0 1 0.827519,0.0595 31.151221,33.78691 0 0 1 0.825964,0.0839 31.151221,33.78691 0 0 1 0.823423,0.10767 31.151221,33.78691 0 0 1 0.82035,0.13097 31.151221,33.78691 0 0 1 0.81678,0.15477 31.151221,33.78691 0 0 1 0.81319,0.1786 31.151221,33.78691 0 0 1 0.80809,0.20188 31.151221,33.78691 0 0 1 0.80295,0.22518 31.151221,33.78691 0 0 1 0.79734,0.24796 31.151221,33.78691 0 0 1 0.7907,0.27125 31.151221,33.78691 0 0 1 0.78353,0.29403 31.151221,33.78691 0 0 1 0.77636,0.31628 31.151221,33.78691 0 0 1 0.76817,0.33855 31.151221,33.78691 0 0 1 0.76003,0.3608 31.151221,33.78691 0 0 1 0.75078,0.38255 31.151221,33.78691 0 0 1 0.74058,0.40429 31.151221,33.78691 0 0 1 0.73085,0.42499 31.151221,33.78691 0 0 1 0.7201,0.44622 31.151221,33.78691 0 0 1 0.70885,0.46692 31.151221,33.78691 0 0 1 0.6971,0.48763 31.151221,33.78691 0 0 1 0.68484,0.50679 31.151221,33.78691 0 0 1 0.67254,0.52697 31.151221,33.78691 0 0 1 0.65872,0.54612 31.151221,33.78691 0 0 1 0.64545,0.56476 31.151221,33.78691 0 0 1 0.63165,0.5834 31.151221,33.78691 0 0 1 0.61678,0.601 31.151221,33.78691 0 0 1 0.60196,0.61911 31.151221,33.78691 0 0 1 0.58611,0.63568 31.151221,33.78691 0 0 1 0.57078,0.65276 31.151221,33.78691 0 0 1 0.5544,0.66933 31.151221,33.78691 0 0 1 0.53751,0.68486 31.151221,33.78691 0 0 1 0.52066,0.69987 31.151221,33.78691 0 0 1 0.50379,0.71487 31.151221,33.78691 0 0 1 0.48534,0.72886 31.151221,33.78691 0 0 1 0.46798,0.74284 31.151221,33.78691 0 0 1 0.44904,0.75629 31.151221,33.78691 0 0 1 0.43065,0.76872 31.151221,33.78691 0 0 1 0.41119,0.78113 31.151221,33.78691 0 0 1 0.39227,0.79253 31.151221,33.78691 0 0 1 0.37284,0.80392 31.151221,33.78691 0 0 1 0.35239,0.81375 31.151221,33.78691 0 0 1 0.33294,0.82411 31.151221,33.78691 0 0 1 0.31198,0.83342 31.151221,33.78691 0 0 1 0.29153,0.84171 31.151221,33.78691 0 0 1 0.27107,0.8505 31.151221,33.78691 0 0 1 0.2501,0.85776 31.151221,33.78691 0 0 1 0.2286,0.86448 31.151221,33.78691 0 0 1 0.20765,0.87069 31.151221,33.78691 0 0 1 0.18616,0.87691 31.151221,33.78691 0 0 1 0.16468,0.88157 31.151221,33.78691 0 0 1 0.14268,0.8857 31.151221,33.78691 0 0 1 0.12073,0.88985 31.151221,33.78691 0 0 1 0.0992,0.89347 31.151221,33.78691 0 0 1 0.0772,0.89554 31.151221,33.78691 0 0 1 0.0553,0.89761 31.151221,33.78691 0 0 1 0.0332,0.89865 31.151221,33.78691 0 0 1 0.0108,0.89917 31.151221,33.78691 0 0 1 -0.0957,2.6509 31.151221,33.78691 0 0 1 -0.28795,2.63486 31.151221,33.78691 0 0 1 -0.47716,2.60173 31.151221,33.78691 0 0 1 -0.66385,2.55359 31.151221,33.78691 0 0 1 -0.84644,2.48888 31.151221,33.78691 0 0 1 -1.0239,2.40917 31.151221,33.78691 0 0 1 -1.19526,2.31443 31.151221,33.78691 0 0 1 -1.3589,2.20624 31.151221,33.78691 0 0 1 -1.51437,2.08304 31.151221,33.78691 0 0 1 -1.66015,1.94845 31.151221,33.78691 0 0 1 -1.79618,1.8004 31.151221,33.78691 0 0 1 -1.92098,1.64252 31.151221,33.78691 0 0 1 -2.034,1.47428 31.151221,33.78691 0 0 1 -2.13375,1.2962 31.151221,33.78691 0 0 1 -2.22169,1.11037 31.151221,33.78691 0 0 1 -2.29433,0.91832 31.151221,33.78691 0 0 1 -2.35416,0.72006 31.151221,33.78691 0 0 1 -2.39918,0.51765 31.151221,33.78691 0 0 1 -2.428834,0.31163 31.151221,33.78691 0 0 1 -2.444189,0.10457 31.151221,33.78691 0 0 1 -2.44418,-0.10457 31.151221,33.78691 0 0 1 -2.428838,-0.31162 31.151221,33.78691 0 0 1 -2.399177,-0.51766 31.151221,33.78691 0 0 1 -2.354163,-0.72006 31.151221,33.78691 0 0 1 -2.29484,-0.91831 31.151221,33.78691 0 0 1 -2.221196,-1.11037 31.151221,33.78691 0 0 1 -2.134249,-1.29621 31.151221,33.78691 0 0 1 -2.033489,-1.47428 31.151221,33.78691 0 0 1 -1.920973,-1.64251 31.151221,33.78691 0 0 1 -1.796194,-1.8004 31.151221,33.78691 0 0 1 -1.660643,-1.94845 31.151221,33.78691 0 0 1 -1.513873,-2.08304 31.151221,33.78691 0 0 1 -1.358905,-2.20625 31.151221,33.78691 0 0 1 -1.195236,-2.31443 31.151221,33.78691 0 0 1 -1.023911,-2.40916 31.151221,33.78691 0 0 1 -0.846946,-2.48888 31.151221,33.78691 0 0 1 -0.663854,-2.55359 31.151221,33.78691 0 0 1 -0.477175,-2.60173 31.151221,33.78691 0 0 1 -0.287434,-2.63486 31.151221,33.78691 0 0 1 -0.09615,-2.65091 31.151221,33.78691 0 0 1 0.09615,-2.6509 31.151221,33.78691 0 0 1 0.287434,-2.63435 31.151221,33.78691 0 0 1 0.477175,-2.60173 31.151221,33.78691 0 0 1 0.663854,-2.55359 31.151221,33.78691 0 0 1 0.846946,-2.48888 31.151221,33.78691 0 0 1 1.023911,-2.40916 31.151221,33.78691 0 0 1 1.195236,-2.31495 31.151221,33.78691 0 0 1 1.358905,-2.20572 31.151221,33.78691 0 0 1 1.513873,-2.08356 31.151221,33.78691 0 0 1 1.660643,-1.94793 31.151221,33.78691 0 0 1 1.796194,-1.80092 31.151221,33.78691 0 0 1 1.920973,-1.64252 31.151221,33.78691 0 0 1 2.033505,-1.47376 31.151221,33.78691 0 0 1 2.13425,-1.29621 31.151221,33.78691 0 0 1 2.221195,-1.11088 31.151221,33.78691 0 0 1 2.294841,-0.9178 31.151221,33.78691 0 0 1 2.354165,-0.72058 31.151221,33.78691 0 0 1 2.399176,-0.51714 31.151221,33.78691 0 0 1 2.428838,-0.31214 31.151221,33.78691 0 0 1 2.44418,-0.10405 z"
+ clip-path="url(#clipPath20675)"
+ transform="translate(-206.48906,-227.91266)"
+ inkscape:export-filename="C:\Users\a116178\Downloads\Taler\taler 512.png"
+ inkscape:export-xdpi="96.231026"
+ inkscape:export-ydpi="96.231026"
+ inkscape:connector-curvature="0" />
+ <path
+ id="path1306-7-6-6"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.0427732;stroke-linejoin:round;stroke-opacity:1"
+ d="M 96.843606,215.58112 A 46.363577,49.444797 0 0 0 50.480411,265.02634 46.363577,49.444797 0 0 0 96.843606,314.4709 46.363577,49.444797 0 0 0 143.20744,265.02634 46.363577,49.444797 0 0 0 96.843606,215.58112 Z m -0.09212,6.29001 a 40.632485,43.292687 0 0 1 1.081384,0.0153 40.632485,43.292687 0 0 1 1.080714,0.0464 40.632485,43.292687 0 0 1 1.079381,0.0763 40.632485,43.292687 0 0 1 1.077365,0.10746 40.632485,43.292687 0 0 1 1.07404,0.13796 40.632485,43.292687 0 0 1 1.07004,0.16781 40.632485,43.292687 0 0 1 1.06536,0.19833 40.632485,43.292687 0 0 1 1.06071,0.22884 40.632485,43.292687 0 0 1 1.05403,0.25868 40.632485,43.292687 0 0 1 1.04735,0.28853 40.632485,43.292687 0 0 1 1.04002,0.31772 40.632485,43.292687 0 0 1 1.03135,0.34757 40.632485,43.292687 0 0 1 1.02201,0.37675 40.632485,43.292687 0 0 1 1.01266,0.40527 40.632485,43.292687 0 0 1 1.00198,0.43379 40.632485,43.292687 0 0 1 0.99134,0.46232 40.632485,43.292687 0 0 1 0.97929,0.49017 40.632485,43.292687 0 0 1 0.96598,0.51804 40.632485,43.292687 0 0 1 0.9533,0.54456 40.632485,43.292687 0 0 1 0.93927,0.57176 40.632485,43.292687 0 0 1 0.92461,0.59829 40.632485,43.292687 0 0 1 0.90926,0.62482 40.632485,43.292687 0 0 1 0.89328,0.64937 40.632485,43.292687 0 0 1 0.87724,0.67523 40.632485,43.292687 0 0 1 0.85922,0.69977 40.632485,43.292687 0 0 1 0.8419,0.72366 40.632485,43.292687 0 0 1 0.82388,0.74753 40.632485,43.292687 0 0 1 0.80452,0.77008 40.632485,43.292687 0 0 1 0.78518,0.7933 40.632485,43.292687 0 0 1 0.7645,0.81453 40.632485,43.292687 0 0 1 0.74449,0.83641 40.632485,43.292687 0 0 1 0.72314,0.85764 40.632485,43.292687 0 0 1 0.70113,0.87754 40.632485,43.292687 0 0 1 0.67912,0.89677 40.632485,43.292687 0 0 1 0.65711,0.91601 40.632485,43.292687 0 0 1 0.63307,0.93391 40.632485,43.292687 0 0 1 0.6104,0.95183 40.632485,43.292687 0 0 1 0.58573,0.96907 40.632485,43.292687 0 0 1 0.56171,0.98499 40.632485,43.292687 0 0 1 0.53633,1.00091 40.632485,43.292687 0 0 1 0.51167,1.01551 40.632485,43.292687 0 0 1 0.48631,1.03009 40.632485,43.292687 0 0 1 0.45965,1.0427 40.632485,43.292687 0 0 1 0.43428,1.05596 40.632485,43.292687 0 0 1 0.40694,1.0679 40.632485,43.292687 0 0 1 0.38025,1.07852 40.632485,43.292687 0 0 1 0.35357,1.08979 40.632485,43.292687 0 0 1 0.32622,1.09908 40.632485,43.292687 0 0 1 0.29818,1.1077 40.632485,43.292687 0 0 1 0.27085,1.11566 40.632485,43.292687 0 0 1 0.24283,1.12362 40.632485,43.292687 0 0 1 0.2148,1.12959 40.632485,43.292687 0 0 1 0.18612,1.13489 40.632485,43.292687 0 0 1 0.15745,1.14021 40.632485,43.292687 0 0 1 0.12941,1.14484 40.632485,43.292687 0 0 1 0.10078,1.1475 40.632485,43.292687 0 0 1 0.072,1.15015 40.632485,43.292687 0 0 1 0.0434,1.15148 40.632485,43.292687 0 0 1 0.014,1.15214 40.632485,43.292687 0 0 1 -0.12477,3.39672 40.632485,43.292687 0 0 1 -0.37557,3.37617 40.632485,43.292687 0 0 1 -0.6224,3.33371 40.632485,43.292687 0 0 1 -0.86591,3.27203 40.632485,43.292687 0 0 1 -1.10406,3.18911 40.632485,43.292687 0 0 1 -1.33555,3.08697 40.632485,43.292687 0 0 1 -1.55902,2.96559 40.632485,43.292687 0 0 1 -1.77249,2.82696 40.632485,43.292687 0 0 1 -1.9753,2.66909 40.632485,43.292687 0 0 1 -2.16544,2.49664 40.632485,43.292687 0 0 1 -2.34287,2.30693 40.632485,43.292687 0 0 1 -2.50564,2.10463 40.632485,43.292687 0 0 1 -2.65309,1.88906 40.632485,43.292687 0 0 1 -2.78317,1.66089 40.632485,43.292687 0 0 1 -2.89791,1.42276 40.632485,43.292687 0 0 1 -2.99264,1.17668 40.632485,43.292687 0 0 1 -3.07067,0.92265 40.632485,43.292687 0 0 1 -3.12941,0.66329 40.632485,43.292687 0 0 1 -3.168074,0.3993 40.632485,43.292687 0 0 1 -3.188104,0.13399 40.632485,43.292687 0 0 1 -3.188094,-0.13399 40.632485,43.292687 0 0 1 -3.168088,-0.3993 40.632485,43.292687 0 0 1 -3.12939,-0.6633 40.632485,43.292687 0 0 1 -3.07069,-0.92264 40.632485,43.292687 0 0 1 -2.993303,-1.17668 40.632485,43.292687 0 0 1 -2.897244,-1.42277 40.632485,43.292687 0 0 1 -2.783837,-1.66088 40.632485,43.292687 0 0 1 -2.652404,-1.88906 40.632485,43.292687 0 0 1 -2.505648,-2.10464 40.632485,43.292687 0 0 1 -2.342886,-2.30693 40.632485,43.292687 0 0 1 -2.166082,-2.49664 40.632485,43.292687 0 0 1 -1.974639,-2.66909 40.632485,43.292687 0 0 1 -1.772504,-2.82696 40.632485,43.292687 0 0 1 -1.559022,-2.96558 40.632485,43.292687 0 0 1 -1.335549,-3.08697 40.632485,43.292687 0 0 1 -1.104725,-3.18912 40.632485,43.292687 0 0 1 -0.865908,-3.27202 40.632485,43.292687 0 0 1 -0.622408,-3.33372 40.632485,43.292687 0 0 1 -0.374918,-3.37616 40.632485,43.292687 0 0 1 -0.125409,-3.39673 40.632485,43.292687 0 0 1 0.125409,-3.39672 40.632485,43.292687 0 0 1 0.374918,-3.37551 40.632485,43.292687 0 0 1 0.622408,-3.33371 40.632485,43.292687 0 0 1 0.865908,-3.27203 40.632485,43.292687 0 0 1 1.104725,-3.18911 40.632485,43.292687 0 0 1 1.335549,-3.08697 40.632485,43.292687 0 0 1 1.559022,-2.96625 40.632485,43.292687 0 0 1 1.772504,-2.82629 40.632485,43.292687 0 0 1 1.974639,-2.66976 40.632485,43.292687 0 0 1 2.166082,-2.49597 40.632485,43.292687 0 0 1 2.342886,-2.3076 40.632485,43.292687 0 0 1 2.505648,-2.10463 40.632485,43.292687 0 0 1 2.652426,-1.8884 40.632485,43.292687 0 0 1 2.783836,-1.66089 40.632485,43.292687 0 0 1 2.897245,-1.42342 40.632485,43.292687 0 0 1 2.993303,-1.17602 40.632485,43.292687 0 0 1 3.070689,-0.92331 40.632485,43.292687 0 0 1 3.12939,-0.66263 40.632485,43.292687 0 0 1 3.16809,-0.39997 40.632485,43.292687 0 0 1 3.188093,-0.13332 z"
+ clip-path="url(#clipPath20679)"
+ transform="translate(-206.48906,-227.91266)"
+ inkscape:export-filename="C:\Users\a116178\Downloads\Taler\taler 512.png"
+ inkscape:export-xdpi="96.231026"
+ inkscape:export-ydpi="96.231026"
+ inkscape:connector-curvature="0" />
+ <path
+ id="path1306-5-7"
+ style="fill:#0042b3;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.0376767;stroke-linejoin:round"
+ d="m 74.301703,221.87113 a 40.722405,43.678338 0 0 0 -40.72207,43.67871 40.722405,43.678338 0 0 0 40.72207,43.67813 40.722405,43.678338 0 0 0 40.722637,-43.67813 40.722405,43.678338 0 0 0 -40.722637,-43.67871 z m -0.0809,5.55645 a 35.68863,38.243712 0 0 1 0.94981,0.0135 35.68863,38.243712 0 0 1 0.94922,0.041 35.68863,38.243712 0 0 1 0.94805,0.0674 35.68863,38.243712 0 0 1 0.94628,0.0949 35.68863,38.243712 0 0 1 0.94336,0.12187 35.68863,38.243712 0 0 1 0.93984,0.14824 35.68863,38.243712 0 0 1 0.93574,0.1752 35.68863,38.243712 0 0 1 0.93165,0.20215 35.68863,38.243712 0 0 1 0.92578,0.22851 35.68863,38.243712 0 0 1 0.91992,0.25489 35.68863,38.243712 0 0 1 0.91348,0.28066 35.68863,38.243712 0 0 1 0.90586,0.30703 35.68863,38.243712 0 0 1 0.89765,0.33281 35.68863,38.243712 0 0 1 0.88945,0.35801 35.68863,38.243712 0 0 1 0.880067,0.3832 35.68863,38.243712 0 0 1 0.87071,0.4084 35.68863,38.243712 0 0 1 0.86015,0.43301 35.68863,38.243712 0 0 1 0.84844,0.45762 35.68863,38.243712 0 0 1 0.8373,0.48105 35.68863,38.243712 0 0 1 0.825,0.50508 35.68863,38.243712 0 0 1 0.81211,0.52852 35.68863,38.243712 0 0 1 0.79863,0.55195 35.68863,38.243712 0 0 1 0.78458,0.57363 35.68863,38.243712 0 0 1 0.7705,0.59649 35.68863,38.243712 0 0 1 0.75469,0.61816 35.68863,38.243712 0 0 1 0.73946,0.63926 35.68863,38.243712 0 0 1 0.72363,0.66035 35.68863,38.243712 0 0 1 0.70664,0.68027 35.68863,38.243712 0 0 1 0.68965,0.70078 35.68863,38.243712 0 0 1 0.67148,0.71954 35.68863,38.243712 0 0 1 0.65391,0.73886 35.68863,38.243712 0 0 1 0.63515,0.75762 35.68863,38.243712 0 0 1 0.61582,0.7752 35.68863,38.243712 0 0 1 0.59649,0.79218 35.68863,38.243712 0 0 1 0.57715,0.80918 35.68863,38.243712 0 0 1 0.55605,0.825 35.68863,38.243712 0 0 1 0.53613,0.84082 35.68863,38.243712 0 0 1 0.51446,0.85606 35.68863,38.243712 0 0 1 0.49336,0.87012 35.68863,38.243712 0 0 1 0.47109,0.88417 35.68863,38.243712 0 0 1 0.44941,0.89708 35.68863,38.243712 0 0 1 0.42715,0.90996 35.68863,38.243712 0 0 1 0.40371,0.92109 35.68863,38.243712 0 0 1 0.38145,0.93281 35.68863,38.243712 0 0 1 0.35742,0.94336 35.68863,38.243712 0 0 1 0.33399,0.95274 35.68863,38.243712 0 0 1 0.31054,0.96269 35.68863,38.243712 0 0 1 0.28653,0.9709 35.68863,38.243712 0 0 1 0.26191,0.97852 35.68863,38.243712 0 0 1 0.23789,0.98554 35.68863,38.243712 0 0 1 0.21328,0.99258 35.68863,38.243712 0 0 1 0.18867,0.99785 35.68863,38.243712 0 0 1 0.16348,1.00254 35.68863,38.243712 0 0 1 0.13828,1.00723 35.68863,38.243712 0 0 1 0.11367,1.01133 35.68863,38.243712 0 0 1 0.0885,1.01367 35.68863,38.243712 0 0 1 0.0633,1.01601 35.68863,38.243712 0 0 1 0.0381,1.01719 35.68863,38.243712 0 0 1 0.0123,1.01778 35.68863,38.243712 0 0 1 -0.10957,3.00058 35.68863,38.243712 0 0 1 -0.32988,2.98242 35.68863,38.243712 0 0 1 -0.54668,2.94492 35.68863,38.243712 0 0 1 -0.76055,2.89044 35.68863,38.243712 0 0 1 -0.96972,2.81718 35.68863,38.243712 0 0 1 -1.17305,2.72696 35.68863,38.243712 0 0 1 -1.36934,2.61972 35.68863,38.243712 0 0 1 -1.55683,2.49727 35.68863,38.243712 0 0 1 -1.73496,2.35781 35.68863,38.243712 0 0 1 -1.90196,2.20547 35.68863,38.243712 0 0 1 -2.05781,2.03789 35.68863,38.243712 0 0 1 -2.20078,1.85918 35.68863,38.243712 0 0 1 -2.33027,1.66875 35.68863,38.243712 0 0 1 -2.44454,1.46718 35.68863,38.243712 0 0 1 -2.54531,1.25684 35.68863,38.243712 0 0 1 -2.628507,1.03945 35.68863,38.243712 0 0 1 -2.69706,0.81504 35.68863,38.243712 0 0 1 -2.74864,0.58594 35.68863,38.243712 0 0 1 -2.78261,0.35274 35.68863,38.243712 0 0 1 -2.8002,0.11836 35.68863,38.243712 0 0 1 -2.80019,-0.11836 35.68863,38.243712 0 0 1 -2.78262,-0.35274 35.68863,38.243712 0 0 1 -2.74863,-0.58594 35.68863,38.243712 0 0 1 -2.69707,-0.81504 35.68863,38.243712 0 0 1 -2.6291,-1.03945 35.68863,38.243712 0 0 1 -2.54473,-1.25684 35.68863,38.243712 0 0 1 -2.44512,-1.46718 35.68863,38.243712 0 0 1 -2.32968,-1.66875 35.68863,38.243712 0 0 1 -2.20078,-1.85918 35.68863,38.243712 0 0 1 -2.05782,-2.03789 35.68863,38.243712 0 0 1 -1.90253,-2.20547 35.68863,38.243712 0 0 1 -1.73438,-2.35781 35.68863,38.243712 0 0 1 -1.55684,-2.49727 35.68863,38.243712 0 0 1 -1.36933,-2.61972 35.68863,38.243712 0 0 1 -1.17305,-2.72696 35.68863,38.243712 0 0 1 -0.97031,-2.81718 35.68863,38.243712 0 0 1 -0.76055,-2.89044 35.68863,38.243712 0 0 1 -0.54668,-2.94492 35.68863,38.243712 0 0 1 -0.3293,-2.98242 35.68863,38.243712 0 0 1 -0.11015,-3.00058 35.68863,38.243712 0 0 1 0.11015,-3.00059 35.68863,38.243712 0 0 1 0.3293,-2.98184 35.68863,38.243712 0 0 1 0.54668,-2.94492 35.68863,38.243712 0 0 1 0.76055,-2.89043 35.68863,38.243712 0 0 1 0.97031,-2.81719 35.68863,38.243712 0 0 1 1.17305,-2.72695 35.68863,38.243712 0 0 1 1.36933,-2.62031 35.68863,38.243712 0 0 1 1.55684,-2.49668 35.68863,38.243712 0 0 1 1.73438,-2.3584 35.68863,38.243712 0 0 1 1.90253,-2.20488 35.68863,38.243712 0 0 1 2.05782,-2.03848 35.68863,38.243712 0 0 1 2.20078,-1.85918 35.68863,38.243712 0 0 1 2.32968,-1.66816 35.68863,38.243712 0 0 1 2.44512,-1.46719 35.68863,38.243712 0 0 1 2.54473,-1.25742 35.68863,38.243712 0 0 1 2.6291,-1.03887 35.68863,38.243712 0 0 1 2.69707,-0.81562 35.68863,38.243712 0 0 1 2.74863,-0.58536 35.68863,38.243712 0 0 1 2.78262,-0.35332 35.68863,38.243712 0 0 1 2.80019,-0.11777 z"
+ clip-path="url(#clipPath20683)"
+ transform="translate(-206.48906,-227.91266)"
+ inkscape:export-filename="C:\Users\a116178\Downloads\Taler\taler 512.png"
+ inkscape:export-xdpi="96.231026"
+ inkscape:export-ydpi="96.231026"
+ inkscape:connector-curvature="0" />
+ <path
+ id="path1306-7-8"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.0427732;stroke-linejoin:round;stroke-opacity:1"
+ d="m 119.27202,216.33781 a 46.363577,49.444797 0 0 0 -46.363193,49.44522 46.363577,49.444797 0 0 0 46.363193,49.44456 46.363577,49.444797 0 0 0 46.36384,-49.44456 46.363577,49.444797 0 0 0 -46.36384,-49.44522 z m -0.0921,6.29001 a 40.632485,43.292687 0 0 1 1.08139,0.0153 40.632485,43.292687 0 0 1 1.08071,0.0464 40.632485,43.292687 0 0 1 1.07938,0.0763 40.632485,43.292687 0 0 1 1.07737,0.10745 40.632485,43.292687 0 0 1 1.07404,0.13797 40.632485,43.292687 0 0 1 1.07003,0.16781 40.632485,43.292687 0 0 1 1.06537,0.19832 40.632485,43.292687 0 0 1 1.06071,0.22884 40.632485,43.292687 0 0 1 1.05402,0.25868 40.632485,43.292687 0 0 1 1.04736,0.28854 40.632485,43.292687 0 0 1 1.04002,0.31771 40.632485,43.292687 0 0 1 1.03134,0.34757 40.632485,43.292687 0 0 1 1.02201,0.37675 40.632485,43.292687 0 0 1 1.01266,0.40527 40.632485,43.292687 0 0 1 1.00198,0.4338 40.632485,43.292687 0 0 1 0.99134,0.46231 40.632485,43.292687 0 0 1 0.97929,0.49018 40.632485,43.292687 0 0 1 0.96598,0.51803 40.632485,43.292687 0 0 1 0.9533,0.54456 40.632485,43.292687 0 0 1 0.93927,0.57176 40.632485,43.292687 0 0 1 0.92461,0.59829 40.632485,43.292687 0 0 1 0.90926,0.62483 40.632485,43.292687 0 0 1 0.89328,0.64936 40.632485,43.292687 0 0 1 0.87724,0.67523 40.632485,43.292687 0 0 1 0.85922,0.69978 40.632485,43.292687 0 0 1 0.8419,0.72365 40.632485,43.292687 0 0 1 0.82388,0.74753 40.632485,43.292687 0 0 1 0.80452,0.77009 40.632485,43.292687 0 0 1 0.78518,0.7933 40.632485,43.292687 0 0 1 0.7645,0.81452 40.632485,43.292687 0 0 1 0.74449,0.83641 40.632485,43.292687 0 0 1 0.72314,0.85764 40.632485,43.292687 0 0 1 0.70113,0.87754 40.632485,43.292687 0 0 1 0.67912,0.89677 40.632485,43.292687 0 0 1 0.65711,0.91601 40.632485,43.292687 0 0 1 0.63307,0.93392 40.632485,43.292687 0 0 1 0.6104,0.95183 40.632485,43.292687 0 0 1 0.58573,0.96907 40.632485,43.292687 0 0 1 0.56171,0.98499 40.632485,43.292687 0 0 1 0.53634,1.00091 40.632485,43.292687 0 0 1 0.51167,1.0155 40.632485,43.292687 0 0 1 0.48631,1.0301 40.632485,43.292687 0 0 1 0.45965,1.04269 40.632485,43.292687 0 0 1 0.43428,1.05597 40.632485,43.292687 0 0 1 0.40694,1.0679 40.632485,43.292687 0 0 1 0.38025,1.07851 40.632485,43.292687 0 0 1 0.35357,1.0898 40.632485,43.292687 0 0 1 0.32622,1.09908 40.632485,43.292687 0 0 1 0.29818,1.10769 40.632485,43.292687 0 0 1 0.27085,1.11566 40.632485,43.292687 0 0 1 0.24283,1.12362 40.632485,43.292687 0 0 1 0.2148,1.12959 40.632485,43.292687 0 0 1 0.18612,1.1349 40.632485,43.292687 0 0 1 0.15745,1.1402 40.632485,43.292687 0 0 1 0.12941,1.14484 40.632485,43.292687 0 0 1 0.10078,1.1475 40.632485,43.292687 0 0 1 0.072,1.15015 40.632485,43.292687 0 0 1 0.0434,1.15148 40.632485,43.292687 0 0 1 0.014,1.15214 40.632485,43.292687 0 0 1 -0.12477,3.39673 40.632485,43.292687 0 0 1 -0.37557,3.37616 40.632485,43.292687 0 0 1 -0.6224,3.33371 40.632485,43.292687 0 0 1 -0.86591,3.27203 40.632485,43.292687 0 0 1 -1.10406,3.18912 40.632485,43.292687 0 0 1 -1.33555,3.08697 40.632485,43.292687 0 0 1 -1.55903,2.96558 40.632485,43.292687 0 0 1 -1.77249,2.82696 40.632485,43.292687 0 0 1 -1.9753,2.66909 40.632485,43.292687 0 0 1 -2.16544,2.49664 40.632485,43.292687 0 0 1 -2.34287,2.30693 40.632485,43.292687 0 0 1 -2.50564,2.10464 40.632485,43.292687 0 0 1 -2.65309,1.88906 40.632485,43.292687 0 0 1 -2.78317,1.66088 40.632485,43.292687 0 0 1 -2.89791,1.42277 40.632485,43.292687 0 0 1 -2.99263,1.17668 40.632485,43.292687 0 0 1 -3.07068,0.92264 40.632485,43.292687 0 0 1 -3.1294,0.66329 40.632485,43.292687 0 0 1 -3.16808,0.39931 40.632485,43.292687 0 0 1 -3.1881,0.13398 40.632485,43.292687 0 0 1 -3.1881,-0.13398 40.632485,43.292687 0 0 1 -3.16808,-0.39931 40.632485,43.292687 0 0 1 -3.12939,-0.66329 40.632485,43.292687 0 0 1 -3.07069,-0.92264 40.632485,43.292687 0 0 1 -2.99331,-1.17668 40.632485,43.292687 0 0 1 -2.89724,-1.42277 40.632485,43.292687 0 0 1 -2.783838,-1.66088 40.632485,43.292687 0 0 1 -2.652404,-1.88906 40.632485,43.292687 0 0 1 -2.505648,-2.10464 40.632485,43.292687 0 0 1 -2.342886,-2.30693 40.632485,43.292687 0 0 1 -2.166082,-2.49664 40.632485,43.292687 0 0 1 -1.974639,-2.66909 40.632485,43.292687 0 0 1 -1.772504,-2.82696 40.632485,43.292687 0 0 1 -1.559022,-2.96558 40.632485,43.292687 0 0 1 -1.335549,-3.08697 40.632485,43.292687 0 0 1 -1.104725,-3.18912 40.632485,43.292687 0 0 1 -0.865908,-3.27203 40.632485,43.292687 0 0 1 -0.622408,-3.33371 40.632485,43.292687 0 0 1 -0.374918,-3.37616 40.632485,43.292687 0 0 1 -0.125409,-3.39673 40.632485,43.292687 0 0 1 0.125409,-3.39673 40.632485,43.292687 0 0 1 0.374918,-3.3755 40.632485,43.292687 0 0 1 0.622408,-3.33371 40.632485,43.292687 0 0 1 0.865908,-3.27203 40.632485,43.292687 0 0 1 1.104725,-3.18911 40.632485,43.292687 0 0 1 1.335549,-3.08697 40.632485,43.292687 0 0 1 1.559022,-2.96625 40.632485,43.292687 0 0 1 1.772504,-2.8263 40.632485,43.292687 0 0 1 1.974639,-2.66975 40.632485,43.292687 0 0 1 2.166082,-2.49597 40.632485,43.292687 0 0 1 2.342886,-2.3076 40.632485,43.292687 0 0 1 2.505648,-2.10463 40.632485,43.292687 0 0 1 2.652426,-1.8884 40.632485,43.292687 0 0 1 2.783836,-1.66089 40.632485,43.292687 0 0 1 2.89724,-1.42343 40.632485,43.292687 0 0 1 2.99331,-1.17602 40.632485,43.292687 0 0 1 3.07069,-0.9233 40.632485,43.292687 0 0 1 3.12939,-0.66263 40.632485,43.292687 0 0 1 3.16809,-0.39997 40.632485,43.292687 0 0 1 3.18809,-0.13332 z"
+ clip-path="url(#clipPath20667)"
+ transform="translate(-251.48906,-227.91266)"
+ inkscape:export-filename="C:\Users\a116178\Downloads\Taler\taler 512.png"
+ inkscape:export-xdpi="96.231026"
+ inkscape:export-ydpi="96.231026"
+ inkscape:connector-curvature="0" />
+ </g>
+ </g>
+ </g>
+ </g>
+</svg>
diff --git a/packages/taler-wallet-webextension/src/pwa/static/img/taler-logo-48.png b/packages/taler-wallet-webextension/src/pwa/static/img/taler-logo-48.png
new file mode 100644
index 000000000..f13a23c85
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pwa/static/img/taler-logo-48.png
Binary files differ
diff --git a/packages/taler-wallet-webextension/src/pwa/static/img/taler-logo-512.png b/packages/taler-wallet-webextension/src/pwa/static/img/taler-logo-512.png
new file mode 100644
index 000000000..be312ef55
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pwa/static/img/taler-logo-512.png
Binary files differ
diff --git a/packages/taler-wallet-webextension/dev-html/stories.html b/packages/taler-wallet-webextension/src/pwa/stories.html
index b07c4bd1c..f18307669 100644
--- a/packages/taler-wallet-webextension/dev-html/stories.html
+++ b/packages/taler-wallet-webextension/src/pwa/stories.html
@@ -2,9 +2,9 @@
<html>
<head>
<title>Stories</title>
- <link rel="stylesheet" type="text/css" href="/dist/stories.css" />
+ <link rel="stylesheet" type="text/css" href="stories.css" />
<link rel="stylesheet" type="text/css" href="/static/font/import.css" />
- <script src="/dist/stories.js"></script>
+ <script src="stories.js"></script>
</head>
<body>
<taler-stories id="container"></taler-stories>
diff --git a/packages/taler-wallet-webextension/src/pwa/sw.js b/packages/taler-wallet-webextension/src/pwa/sw.js
new file mode 100644
index 000000000..2b2219578
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pwa/sw.js
@@ -0,0 +1,6 @@
+console.log("sw: Service worker installed");
+
+self.addEventListener("fetch", (event) => {
+ // console.log("fetch event", event);
+ // event.respondWith(/* custom content goes here */);
+});
diff --git a/packages/taler-wallet-webextension/dev-html/tests.html b/packages/taler-wallet-webextension/src/pwa/tests.html
index 4b3ec93b5..383f13d03 100644
--- a/packages/taler-wallet-webextension/dev-html/tests.html
+++ b/packages/taler-wallet-webextension/src/pwa/tests.html
@@ -12,8 +12,8 @@
</script>
<!-- load code you want to test here -->
- <script src="/dist/stories.test.js"></script>
- <script src="/dist/hooks/useTalerActionURL.test.js"></script>
+ <script src="stories.test.js"></script>
+ <script src="hooks/useTalerActionURL.test.js"></script>
<!-- load your test files here -->
<script>
diff --git a/packages/taler-wallet-webextension/dev-html/wallet.html b/packages/taler-wallet-webextension/src/pwa/wallet.html
index ff8616847..366615dff 100644
--- a/packages/taler-wallet-webextension/dev-html/wallet.html
+++ b/packages/taler-wallet-webextension/src/pwa/wallet.html
@@ -1,7 +1,7 @@
<html>
<head>
<meta charset="utf-8" />
- <link rel="stylesheet" type="text/css" href="/dist/walletEntryPoint.css" />
+ <link rel="stylesheet" type="text/css" href="walletEntryPoint.dev.css" />
<style>
html {
font-family: sans-serif; /* 1 */
@@ -20,7 +20,7 @@
font-family: Arial, Helvetica, sans-serif;
}
</style>
- <script src="/dist/walletEntryPoint.dev.js"></script>
+ <script type="module" src="walletEntryPoint.dev.js"></script>
</head>
<body>
diff --git a/packages/taler-wallet-webextension/src/serviceWorkerHttpLib.ts b/packages/taler-wallet-webextension/src/serviceWorkerHttpLib.ts
deleted file mode 100644
index 74c7f161d..000000000
--- a/packages/taler-wallet-webextension/src/serviceWorkerHttpLib.ts
+++ /dev/null
@@ -1,168 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- * Imports.
- */
-import { RequestThrottler, TalerErrorCode } from "@gnu-taler/taler-util";
-import {
- Headers,
- HttpRequestLibrary,
- HttpRequestOptions,
- HttpResponse,
- TalerError,
-} from "@gnu-taler/taler-wallet-core";
-
-/**
- * An implementation of the [[HttpRequestLibrary]] using the
- * browser's XMLHttpRequest.
- */
-export class ServiceWorkerHttpLib implements HttpRequestLibrary {
- private throttle = new RequestThrottler();
- private throttlingEnabled = true;
-
- async fetch(
- requestUrl: string,
- options?: HttpRequestOptions,
- ): Promise<HttpResponse> {
- const requestMethod = options?.method ?? "GET";
- const requestBody = options?.body;
- const requestHeader = options?.headers;
-
- if (this.throttlingEnabled && this.throttle.applyThrottle(requestUrl)) {
- const parsedUrl = new URL(requestUrl);
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED,
- {
- requestMethod,
- requestUrl,
- throttleStats: this.throttle.getThrottleStats(requestUrl),
- },
- `request to origin ${parsedUrl.origin} was throttled`,
- );
- }
-
- let myBody: BodyInit | undefined = undefined;
- if (requestBody != null) {
- if (typeof requestBody === "string") {
- myBody = requestBody;
- } else if (requestBody instanceof ArrayBuffer) {
- myBody = requestBody;
- } else if (ArrayBuffer.isView(requestBody)) {
- myBody = requestBody;
- } else if (typeof requestBody === "object") {
- myBody = JSON.stringify(myBody);
- } else {
- throw Error("unsupported request body type");
- }
- }
-
- const response = await fetch(requestUrl, {
- headers: requestHeader,
- body: myBody,
- method: requestMethod,
- // timeout: options?.timeout
- });
-
- const headerMap = new Headers();
- response.headers.forEach((value, key) => {
- headerMap.set(key, value);
- });
- return {
- headers: headerMap,
- status: response.status,
- requestMethod,
- requestUrl,
- json: makeJsonHandler(response, requestUrl),
- text: makeTextHandler(response, requestUrl),
- bytes: async () => (await response.blob()).arrayBuffer(),
- };
- }
-
- get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> {
- return this.fetch(url, {
- method: "GET",
- ...opt,
- });
- }
-
- // FIXME: "Content-Type: application/json" goes here,
- // after Sebastian suggestion.
- 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,
- });
- }
-
- stop(): void {
- // Nothing to do
- }
-}
-
-function makeTextHandler(response: Response, requestUrl: string) {
- return async function getJsonFromResponse(): Promise<any> {
- let respText;
- try {
- respText = await response.text();
- } catch (e) {
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
- {
- requestUrl,
- httpStatusCode: response.status,
- },
- "Invalid JSON from HTTP response",
- );
- }
- return respText;
- };
-}
-
-function makeJsonHandler(response: Response, requestUrl: string) {
- return async function getJsonFromResponse(): Promise<any> {
- let responseJson;
- try {
- responseJson = await response.json();
- } catch (e) {
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
- {
- requestUrl,
- httpStatusCode: response.status,
- },
- "Invalid JSON from HTTP response",
- );
- }
- if (responseJson === null || typeof responseJson !== "object") {
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
- {
- requestUrl,
- httpStatusCode: response.status,
- },
- "Invalid JSON from HTTP response",
- );
- }
- return responseJson;
- };
-}
diff --git a/packages/taler-wallet-webextension/src/stories.test.ts b/packages/taler-wallet-webextension/src/stories.test.ts
index 9277530a3..b4af1bc1a 100644
--- a/packages/taler-wallet-webextension/src/stories.test.ts
+++ b/packages/taler-wallet-webextension/src/stories.test.ts
@@ -19,33 +19,47 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
import { setupI18n } from "@gnu-taler/taler-util";
-import { parseGroupImport } from "@gnu-taler/web-util/lib/index.browser";
-import { setupPlatform } from "./platform/api.js";
+import { parseGroupImport } from "@gnu-taler/web-util/browser";
+import * as tests from "@gnu-taler/web-util/testing";
import chromeAPI from "./platform/chrome.js";
-import { renderNodeOrBrowser } from "./test-utils.js";
+import { setupPlatform } from "./platform/foreground.js";
import * as components from "./components/index.stories.js";
import * as cta from "./cta/index.stories.js";
import * as mui from "./mui/index.stories.js";
import * as popup from "./popup/index.stories.js";
import * as wallet from "./wallet/index.stories.js";
+// import { renderNodeOrBrowser } from "./test-utils.js";
+import { h, VNode, ComponentChildren } from "preact";
+import { AlertProvider } from "./context/alert.js";
setupI18n("en", { en: {} });
setupPlatform(chromeAPI);
describe("All the examples:", () => {
- const cms = parseGroupImport({ popup, wallet, cta, mui, components })
- cms.forEach(group => {
+ const cms = parseGroupImport({ popup, wallet, cta, mui, components });
+ cms.forEach((group) => {
describe(`Example for group "${group.title}:"`, () => {
- group.list.forEach(component => {
+ group.list.forEach((component) => {
describe(`Component ${component.name}:`, () => {
- component.examples.forEach(example => {
+ component.examples.forEach((example) => {
it(`should render example: ${example.name}`, () => {
- renderNodeOrBrowser(example.render.component, example.render.props)
- })
- })
- })
- })
- })
- })
+ tests.renderUI(example.render, DefaultTestingContext);
+ });
+ });
+ });
+ });
+ });
+ });
});
+function DefaultTestingContext({
+ children,
+}: {
+ children: ComponentChildren;
+}): VNode {
+ //FIXME:
+ //some components push the alter in the UI function
+ //that's not correct, should be moved into the state function
+ // until then, we ran the tests with the alert provider
+ return h(AlertProvider, { children });
+}
diff --git a/packages/taler-wallet-webextension/src/stories.tsx b/packages/taler-wallet-webextension/src/stories.tsx
index 8834b8084..98ab301a3 100644
--- a/packages/taler-wallet-webextension/src/stories.tsx
+++ b/packages/taler-wallet-webextension/src/stories.tsx
@@ -20,7 +20,11 @@
*/
import { Fragment, FunctionComponent, h } from "preact";
import { LogoHeader } from "./components/LogoHeader.js";
-import { PopupBox, WalletBox } from "./components/styled/index.js";
+import {
+ PopupBox,
+ WalletAction,
+ WalletBox,
+} from "./components/styled/index.js";
import { strings } from "./i18n/strings.js";
import { PopupNavBar, WalletNavBar } from "./NavigationBar.js";
@@ -30,7 +34,8 @@ import * as mui from "./mui/index.stories.js";
import * as popup from "./popup/index.stories.js";
import * as wallet from "./wallet/index.stories.js";
-import { renderStories } from "@gnu-taler/web-util/lib/index.browser";
+import { renderStories } from "@gnu-taler/web-util/browser";
+import { AlertProvider } from "./context/alert.js";
function main(): void {
renderStories(
@@ -52,28 +57,28 @@ function getWrapperForGroup(group: string): FunctionComponent {
case "popup":
return function PopupWrapper({ children }: any) {
return (
- <Fragment>
+ <AlertProvider>
<PopupNavBar />
<PopupBox>{children}</PopupBox>
- </Fragment>
+ </AlertProvider>
);
};
case "wallet":
return function WalletWrapper({ children }: any) {
return (
- <Fragment>
+ <AlertProvider>
<LogoHeader />
<WalletNavBar />
<WalletBox>{children}</WalletBox>
- </Fragment>
+ </AlertProvider>
);
};
case "cta":
return function WalletWrapper({ children }: any) {
return (
- <Fragment>
- <WalletBox>{children}</WalletBox>
- </Fragment>
+ <AlertProvider>
+ <WalletAction>{children}</WalletAction>
+ </AlertProvider>
);
};
default:
diff --git a/packages/taler-wallet-webextension/src/svg/check_24px.svg b/packages/taler-wallet-webextension/src/svg/check_24px.inline.svg
index 522695ef3..522695ef3 100644
--- a/packages/taler-wallet-webextension/src/svg/check_24px.svg
+++ b/packages/taler-wallet-webextension/src/svg/check_24px.inline.svg
diff --git a/packages/taler-wallet-webextension/src/svg/chevron-down.svg b/packages/taler-wallet-webextension/src/svg/chevron-down.inline.svg
index 0e1c0aeda..0e1c0aeda 100644
--- a/packages/taler-wallet-webextension/src/svg/chevron-down.svg
+++ b/packages/taler-wallet-webextension/src/svg/chevron-down.inline.svg
diff --git a/packages/taler-wallet-webextension/src/svg/close_24px.svg b/packages/taler-wallet-webextension/src/svg/close_24px.inline.svg
index 8b7eb6e84..8b7eb6e84 100644
--- a/packages/taler-wallet-webextension/src/svg/close_24px.svg
+++ b/packages/taler-wallet-webextension/src/svg/close_24px.inline.svg
diff --git a/packages/taler-wallet-webextension/src/svg/delete_24px.svg b/packages/taler-wallet-webextension/src/svg/delete_24px.inline.svg
index ead7caa9f..ead7caa9f 100644
--- a/packages/taler-wallet-webextension/src/svg/delete_24px.svg
+++ b/packages/taler-wallet-webextension/src/svg/delete_24px.inline.svg
diff --git a/packages/taler-wallet-webextension/src/svg/download_24px.svg b/packages/taler-wallet-webextension/src/svg/download_24px.inline.svg
index c4ec1c354..c4ec1c354 100644
--- a/packages/taler-wallet-webextension/src/svg/download_24px.svg
+++ b/packages/taler-wallet-webextension/src/svg/download_24px.inline.svg
diff --git a/packages/taler-wallet-webextension/src/svg/edit_24px.svg b/packages/taler-wallet-webextension/src/svg/edit_24px.inline.svg
index a4b3c9f6b..a4b3c9f6b 100644
--- a/packages/taler-wallet-webextension/src/svg/edit_24px.svg
+++ b/packages/taler-wallet-webextension/src/svg/edit_24px.inline.svg
diff --git a/packages/taler-wallet-webextension/src/svg/error_outline_outlined_24px.svg b/packages/taler-wallet-webextension/src/svg/error_outline_outlined_24px.inline.svg
index 4a0daa1e9..4a0daa1e9 100644
--- a/packages/taler-wallet-webextension/src/svg/error_outline_outlined_24px.svg
+++ b/packages/taler-wallet-webextension/src/svg/error_outline_outlined_24px.inline.svg
diff --git a/packages/taler-wallet-webextension/src/svg/info_outlined_24px.svg b/packages/taler-wallet-webextension/src/svg/info_outlined_24px.inline.svg
index 80dad95cc..80dad95cc 100644
--- a/packages/taler-wallet-webextension/src/svg/info_outlined_24px.svg
+++ b/packages/taler-wallet-webextension/src/svg/info_outlined_24px.inline.svg
diff --git a/packages/taler-wallet-webextension/src/svg/logo-2021.inline.svg b/packages/taler-wallet-webextension/src/svg/logo-2021.inline.svg
new file mode 100644
index 000000000..8c5ff3e5b
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/svg/logo-2021.inline.svg
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 201 90">
+ <g fill="#0042b3" fill-rule="evenodd" stroke-width=".3">
+ <path d="M86.7 1.1c15.6 0 29 9.4 36 23.2h-5.9A35.1 35.1 0 0086.7 6.5C67 6.5 51 23.6 51 44.7c0 10.4 3.8 19.7 10 26.6a31.4 31.4 0 01-4.2 3A45.2 45.2 0 0146 44.7c0-24 18.2-43.6 40.7-43.6zm35.8 64.3a40.4 40.4 0 01-39 22.8c3-1.5 6-3.5 8.6-5.7a35.6 35.6 0 0024.6-17.1z" />
+ <path d="M64.2 1.1l3.1.1c-3 1.6-5.9 3.5-8.5 5.8a37.5 37.5 0 00-30.2 37.7c0 14.3 7.3 26.7 18 33.3a29.6 29.6 0 01-8.5.2c-9-8-14.6-20-14.6-33.5 0-24 18.2-43.6 40.7-43.6zm5.4 81.4a35.6 35.6 0 0024.6-17.1h5.9a40.4 40.4 0 01-39 22.8c3-1.5 5.9-3.5 8.5-5.7zm24.8-58.2a37 37 0 00-12.6-12.8 29.6 29.6 0 018.5-.2c4 3.6 7.4 8 9.9 13z" />
+ <path d="M41.8 1.1c1 0 2 0 3.1.2-3 1.5-5.9 3.4-8.5 5.6A37.5 37.5 0 006.1 44.7c0 21.1 16 38.3 35.7 38.3 12.6 0 23.6-7 30-17.6h5.8a40.4 40.4 0 01-35.8 23C19.3 88.4 1 68.8 1 44.7c0-24 18.2-43.6 40.7-43.6zm30.1 23.2a38.1 38.1 0 00-4.5-6.1c1.3-1.2 2.7-2.2 4.3-3 2.3 2.7 4.4 5.8 6 9.1z" />
+ </g>
+ <path d="M76.1 34.4h9.2v-5H61.9v5H71v26h5.1zM92.6 52.9h13.7l3 7.4h5.3l-12.7-31.2h-4.7L84.5 60.3h5.2zm11.8-4.9h-9.9l5-12.4zM123.8 29.4h-4.6v31h20.6v-5h-16zM166.5 29.4H145v31h21.6v-5H150v-8.3h14.5v-4.9h-14.5v-8h16.4zM191.2 39.5c0 1.6-.5 2.8-1.6 3.8s-2.6 1.4-4.4 1.4h-7.4V34.3h7.4c1.9 0 3.4.4 4.4 1.3 1 .9 1.6 2.2 1.6 3.9zm6 20.8l-7.7-11.7c1-.3 1.9-.7 2.7-1.3a8.8 8.8 0 003.6-4.6c.4-1 .5-2.2.5-3.5 0-1.5-.2-2.9-.7-4.1a8.4 8.4 0 00-2.1-3.1c-1-.8-2-1.5-3.4-2-1.3-.4-2.8-.6-4.5-.6h-12.9v31h5V49.4h6.5l7 10.8z" />
+</svg> \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/svg/progress.inline.svg b/packages/taler-wallet-webextension/src/svg/progress.inline.svg
new file mode 100644
index 000000000..c7284a545
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/svg/progress.inline.svg
@@ -0,0 +1,12 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="margin:auto;background:#fff;display:block;" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
+ <defs>
+ <clipPath id="progress-cp" x="0" y="0" width="100" height="100">
+ <rect x="0" y="0" width="0" height="100">
+ <animate attributeName="width" repeatCount="indefinite" dur="2s" values="0;100;100" keyTimes="0;0.5;1"></animate>
+ <animate attributeName="x" repeatCount="indefinite" dur="2s" values="0;0;100" keyTimes="0;0.5;1"></animate>
+ </rect>
+ </clipPath>
+ </defs>
+ <path fill="none" stroke="darkgrey" stroke-width="1.04" d="M10.000000000000004 44.019999999999996L89.99999999999999 44.019999999999996A5.98 5.98 0 0 1 95.97999999999999 50L95.97999999999999 50A5.98 5.98 0 0 1 89.99999999999999 55.980000000000004L10.000000000000004 55.980000000000004A5.98 5.98 0 0 1 4.020000000000003 50L4.020000000000003 50A5.98 5.98 0 0 1 10.000000000000004 44.019999999999996 Z"></path>
+ <path fill="#0042b2" clip-path="url(#progress-cp)" d="M10.000000000000004 45.54L90 45.54A4.460000000000001 4.460000000000001 0 0 1 94.46 50L94.46 50A4.460000000000001 4.460000000000001 0 0 1 90 54.46L10.000000000000004 54.46A4.460000000000001 4.460000000000001 0 0 1 5.540000000000003 50L5.540000000000003 50A4.460000000000001 4.460000000000001 0 0 1 10.000000000000004 45.54 Z"></path>
+</svg> \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/svg/qr_code_24px.svg b/packages/taler-wallet-webextension/src/svg/qr_code_24px.inline.svg
index c0c158359..c0c158359 100644
--- a/packages/taler-wallet-webextension/src/svg/qr_code_24px.svg
+++ b/packages/taler-wallet-webextension/src/svg/qr_code_24px.inline.svg
diff --git a/packages/taler-wallet-webextension/src/svg/refresh_24px.inline.svg b/packages/taler-wallet-webextension/src/svg/refresh_24px.inline.svg
new file mode 100644
index 000000000..b8d69f402
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/svg/refresh_24px.inline.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg> \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/svg/refresh_outlined_24px.inline.svg b/packages/taler-wallet-webextension/src/svg/refresh_outlined_24px.inline.svg
new file mode 100644
index 000000000..1959c1aad
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/svg/refresh_outlined_24px.inline.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg> \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/svg/refresh_rounded_24px.inline.svg b/packages/taler-wallet-webextension/src/svg/refresh_rounded_24px.inline.svg
new file mode 100644
index 000000000..46db14360
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/svg/refresh_rounded_24px.inline.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M17.65 6.35c-1.63-1.63-3.94-2.57-6.48-2.31-3.67.37-6.69 3.35-7.1 7.02C3.52 15.91 7.27 20 12 20c3.19 0 5.93-1.87 7.21-4.56.32-.67-.16-1.44-.9-1.44-.37 0-.72.2-.88.53-1.13 2.43-3.84 3.97-6.8 3.31-2.22-.49-4.01-2.3-4.48-4.52C5.31 9.44 8.26 6 12 6c1.66 0 3.14.69 4.22 1.78l-1.51 1.51c-.63.63-.19 1.71.7 1.71H19c.55 0 1-.45 1-1V6.41c0-.89-1.08-1.34-1.71-.71l-.64.65z"/></svg> \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/svg/refresh_sharp_24px.inline.svg b/packages/taler-wallet-webextension/src/svg/refresh_sharp_24px.inline.svg
new file mode 100644
index 000000000..1959c1aad
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/svg/refresh_sharp_24px.inline.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg> \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/svg/refresh_two_tone_24px.inline.svg b/packages/taler-wallet-webextension/src/svg/refresh_two_tone_24px.inline.svg
new file mode 100644
index 000000000..1959c1aad
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/svg/refresh_two_tone_24px.inline.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg> \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/svg/report_problem_outlined_24px.svg b/packages/taler-wallet-webextension/src/svg/report_problem_outlined_24px.inline.svg
index ed32f6f28..ed32f6f28 100644
--- a/packages/taler-wallet-webextension/src/svg/report_problem_outlined_24px.svg
+++ b/packages/taler-wallet-webextension/src/svg/report_problem_outlined_24px.inline.svg
diff --git a/packages/taler-wallet-webextension/src/svg/ri-bank-line.svg b/packages/taler-wallet-webextension/src/svg/ri-bank-line.inline.svg
index 8d987df79..8d987df79 100644
--- a/packages/taler-wallet-webextension/src/svg/ri-bank-line.svg
+++ b/packages/taler-wallet-webextension/src/svg/ri-bank-line.inline.svg
diff --git a/packages/taler-wallet-webextension/src/svg/search_24px.inline.svg b/packages/taler-wallet-webextension/src/svg/search_24px.inline.svg
new file mode 100644
index 000000000..d880cbf0f
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/svg/search_24px.inline.svg
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" viewBox="0 0 24 24" height="24" width="24">
+ <path fill-rule="evenodd" d="M10.5 3.75a6.75 6.75 0 1 0 0 13.5 6.75 6.75 0 0 0 0-13.5ZM2.25 10.5a8.25 8.25 0 1 1 14.59 5.28l4.69 4.69a.75.75 0 1 1-1.06 1.06l-4.69-4.69A8.25 8.25 0 0 1 2.25 10.5Z" clip-rule="evenodd" />
+</svg>
+
diff --git a/packages/taler-wallet-webextension/src/svg/send_24px.svg b/packages/taler-wallet-webextension/src/svg/send_24px.inline.svg
index 6e41d1c9e..6e41d1c9e 100644
--- a/packages/taler-wallet-webextension/src/svg/send_24px.svg
+++ b/packages/taler-wallet-webextension/src/svg/send_24px.inline.svg
diff --git a/packages/taler-wallet-webextension/src/svg/settings_black_24dp.svg b/packages/taler-wallet-webextension/src/svg/settings_black_24dp.inline.svg
index adcd50405..adcd50405 100644
--- a/packages/taler-wallet-webextension/src/svg/settings_black_24dp.svg
+++ b/packages/taler-wallet-webextension/src/svg/settings_black_24dp.inline.svg
diff --git a/packages/taler-wallet-webextension/src/svg/success_outlined_24px.svg b/packages/taler-wallet-webextension/src/svg/success_outlined_24px.inline.svg
index c6130b495..c6130b495 100644
--- a/packages/taler-wallet-webextension/src/svg/success_outlined_24px.svg
+++ b/packages/taler-wallet-webextension/src/svg/success_outlined_24px.inline.svg
diff --git a/packages/taler-wallet-webextension/src/svg/upload_24px.svg b/packages/taler-wallet-webextension/src/svg/upload_24px.inline.svg
index c604ef78d..c604ef78d 100644
--- a/packages/taler-wallet-webextension/src/svg/upload_24px.svg
+++ b/packages/taler-wallet-webextension/src/svg/upload_24px.inline.svg
diff --git a/packages/taler-wallet-webextension/src/svg/warning_24px.svg b/packages/taler-wallet-webextension/src/svg/warning_24px.inline.svg
index d27c4c6ec..d27c4c6ec 100644
--- a/packages/taler-wallet-webextension/src/svg/warning_24px.svg
+++ b/packages/taler-wallet-webextension/src/svg/warning_24px.inline.svg
diff --git a/packages/taler-wallet-webextension/src/svg/wifi.svg b/packages/taler-wallet-webextension/src/svg/wifi.inline.svg
index ad712435d..ad712435d 100644
--- a/packages/taler-wallet-webextension/src/svg/wifi.svg
+++ b/packages/taler-wallet-webextension/src/svg/wifi.inline.svg
diff --git a/packages/taler-wallet-webextension/src/taler-wallet-interaction-loader.ts b/packages/taler-wallet-webextension/src/taler-wallet-interaction-loader.ts
new file mode 100644
index 000000000..3b7cbcbb7
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/taler-wallet-interaction-loader.ts
@@ -0,0 +1,372 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { CoreApiResponse, TalerError, TalerErrorCode } from "@gnu-taler/taler-util";
+import type { MessageFromBackend } from "./platform/api.js";
+
+/**
+ * This will modify all the pages that the user load when navigating with Web Extension enabled
+ *
+ * Can't do useful integration since it run in ISOLATED (or equivalent) mode.
+ *
+ * If taler support is expected, it will inject a script which will complete the integration.
+ */
+
+// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Chrome_incompatibilities#content_script_environment
+
+// ISOLATED mode in chromium browsers
+// https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/bindings/core/v8/V8BindingDesign.md#world
+// X-Ray vision in Firefox
+// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Sharing_objects_with_page_scripts#xray_vision_in_firefox
+
+// *** IMPORTANT ***
+
+// Content script lifecycle during navigation
+// In Firefox: Content scripts remain injected in a web page after the user has navigated away,
+// however, window object properties are destroyed.
+// In Chrome: Content scripts are destroyed when the user navigates away from a web page.
+
+const documentDocTypeIsHTML =
+ window.document.doctype && window.document.doctype.name === "html";
+const suffixIsNotXMLorPDF =
+ !window.location.pathname.endsWith(".xml") &&
+ !window.location.pathname.endsWith(".pdf");
+const rootElementIsHTML =
+ document.documentElement.nodeName &&
+ document.documentElement.nodeName.toLowerCase() === "html";
+// const pageAcceptsTalerSupport = document.head.querySelector(
+// "meta[name=taler-support]",
+// );
+
+
+
+function validateTalerUri(uri: string): boolean {
+ return (
+ !!uri && (uri.startsWith("taler://") || uri.startsWith("taler+http://"))
+ );
+}
+
+function convertURIToWebExtensionPath(uri: string) {
+ const url = new URL(
+ chrome.runtime.getURL(`static/wallet.html#/taler-uri/${encodeURIComponent(uri)}`),
+ );
+ return url.href;
+}
+
+// safe check, if one of this is true then taler handler is not useful
+// or not expected
+const shouldNotInject =
+ !documentDocTypeIsHTML ||
+ !suffixIsNotXMLorPDF ||
+ // !pageAcceptsTalerSupport ||
+ !rootElementIsHTML;
+
+const logger = {
+ debug: (...msg: any[]) => { },
+ info: (...msg: any[]) =>
+ console.log(`${new Date().toISOString()} TALER`, ...msg),
+ error: (...msg: any[]) =>
+ console.error(`${new Date().toISOString()} TALER`, ...msg),
+};
+
+// logger.debug = logger.info
+
+/**
+ */
+function redirectToTalerActionHandler(element: HTMLMetaElement) {
+ const name = element.getAttribute("name")
+ if (!name) return;
+ if (name !== "taler-uri") return;
+ const uri = element.getAttribute("content");
+ if (!uri) return;
+
+ if (!validateTalerUri(uri)) {
+ logger.error(`taler:// URI is invalid: ${uri}`);
+ return;
+ }
+
+ const walletPage = convertURIToWebExtensionPath(uri)
+ window.location.replace(walletPage)
+}
+
+function injectTalerSupportScript(head: HTMLHeadElement, trusted: boolean) {
+ const meta = head.querySelector("meta[name=taler-support]")
+ if (!meta) return;
+ const content = meta.getAttribute("content");
+ if (!content) return;
+ const features = content.split(",")
+
+ const debugEnabled = meta.getAttribute("debug") === "true";
+ const hijackEnabled = features.indexOf("uri") !== -1
+ const talerApiEnabled = features.indexOf("api") !== -1 && trusted
+
+ const scriptTag = document.createElement("script");
+ scriptTag.setAttribute("async", "false");
+ const url = new URL(
+ chrome.runtime.getURL("/dist/taler-wallet-interaction-support.js"),
+ );
+ url.searchParams.set("id", chrome.runtime.id);
+ if (debugEnabled) {
+ url.searchParams.set("debug", "true");
+ }
+ if (talerApiEnabled) {
+ url.searchParams.set("api", "true");
+ }
+ if (hijackEnabled) {
+ url.searchParams.set("hijack", "true");
+ }
+ scriptTag.src = url.href;
+
+ try {
+ 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;
+ response: boolean;
+ };
+ isDomainTrusted: {
+ request: {
+ domain: string;
+ };
+ response: boolean;
+ };
+}
+
+export type MessageFromExtension<Op extends keyof ExtensionOperations> = {
+ channel: "extension";
+ operation: Op;
+ payload: ExtensionOperations[Op]["request"];
+};
+
+export type MessageResponse = CoreApiResponse;
+
+async function callBackground<Op extends keyof ExtensionOperations>(
+ operation: Op,
+ payload: ExtensionOperations[Op]["request"],
+): Promise<ExtensionOperations[Op]["response"]> {
+ const message: MessageFromExtension<Op> = {
+ channel: "extension",
+ operation,
+ payload,
+ };
+
+ const response = await sendMessageToBackground(message);
+ if (response.type === "error") {
+ throw new Error(`Background operation "${operation}" failed`);
+ }
+ return response.result as any;
+}
+
+
+let nextMessageIndex = 0;
+/**
+ *
+ * @param message
+ * @returns
+ */
+async function sendMessageToBackground<Op extends keyof ExtensionOperations>(
+ message: MessageFromExtension<Op>,
+): Promise<MessageResponse> {
+ const messageWithId = { ...message, id: `id_${nextMessageIndex++ % 1000}` };
+
+ if (!chrome.runtime.id) {
+ 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);
+ let timedout = false;
+ const timerId = setTimeout(() => {
+ timedout = true;
+ reject(TalerError.fromDetail(TalerErrorCode.GENERIC_TIMEOUT, {
+ requestMethod: "wallet",
+ requestUrl: message.operation,
+ timeoutMs: 20 * 1000,
+ }))
+ }, 20 * 1000); //five seconds
+ try {
+ chrome.runtime.sendMessage(messageWithId, (backgroundResponse) => {
+ if (timedout) {
+ return false; //already rejected
+ }
+ clearTimeout(timerId);
+ if (chrome.runtime.lastError) {
+ reject(chrome.runtime.lastError.message);
+ } else {
+ resolve(backgroundResponse);
+ }
+ // return true to keep the channel open
+ return true;
+ });
+ } catch (e) {
+ console.log(e)
+ }
+ });
+}
+
+let notificationPort: chrome.runtime.Port | undefined;
+function listenToWalletBackground(listener: (m: any) => void): () => void {
+ if (notificationPort === undefined) {
+ notificationPort = chrome.runtime.connect({ name: "notifications" });
+ }
+ notificationPort.onMessage.addListener(listener);
+ function removeListener(): void {
+ if (notificationPort !== undefined) {
+ notificationPort.onMessage.removeListener(listener);
+ }
+ }
+ return removeListener;
+}
+
+const loaderSettings = {
+ isAutoOpenEnabled: false,
+ isDomainTrusted: false,
+}
+
+function start(
+ onTalerMetaTagFound: (listener: (el: HTMLMetaElement) => 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 => {
+ loaderSettings.isAutoOpenEnabled = result;
+ return result;
+ })
+ const isDomainTrusted_promise = callBackground("isDomainTrusted", {
+ domain: window.location.origin
+ }).then(result => {
+ loaderSettings.isDomainTrusted = result;
+ return result;
+ })
+
+ onTalerMetaTagFound(async (el) => {
+ await isAutoOpenEnabled_promise;
+ if (!loaderSettings.isAutoOpenEnabled) {
+ return;
+ }
+ redirectToTalerActionHandler(el)
+ })
+
+ onHeadReady(async (el) => {
+ 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
+ }
+ })
+
+}
+
+function isCorrectMetaElement(el: HTMLMetaElement): boolean {
+ 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
+}
+
+/**
+ * Tries to find taler meta tag ASAP and report
+ * @param notify
+ * @returns
+ */
+function notifyWhenTalerUriIsFound(notify: (el: HTMLMetaElement) => void) {
+ if (document.head) {
+ const element = document.head.querySelector("meta[name=taler-uri]")
+ if (!element) return;
+ if (!(element instanceof HTMLMetaElement)) return;
+
+ if (isCorrectMetaElement(element)) {
+ notify(element)
+ }
+ return;
+ }
+ const obs = new MutationObserver(async function (mutations) {
+ try {
+ mutations.forEach((mut) => {
+ if (mut.type === "childList") {
+ mut.addedNodes.forEach((added) => {
+ if (added instanceof HTMLMetaElement) {
+ if (isCorrectMetaElement(added)) {
+ notify(added)
+ obs.disconnect()
+ }
+ }
+ });
+ }
+ });
+ } catch (e) {
+ console.error(e)
+ }
+ })
+
+ obs.observe(document, {
+ childList: true,
+ subtree: true,
+ attributes: false,
+ })
+
+}
+
+/**
+ * Tries to find HEAD tag ASAP and report
+ * @param notify
+ * @returns
+ */
+function notifyWhenHeadIsFound(notify: (el: HTMLHeadElement) => void) {
+ if (document.head) {
+ notify(document.head)
+ return;
+ }
+ const obs = new MutationObserver(async function (mutations) {
+ try {
+ mutations.forEach((mut) => {
+ if (mut.type === "childList") {
+ mut.addedNodes.forEach((added) => {
+ if (added instanceof HTMLHeadElement) {
+ notify(added)
+ obs.disconnect()
+ }
+ });
+ }
+ });
+ } catch (e) {
+ console.error(e)
+ }
+ })
+
+ obs.observe(document, {
+ childList: true,
+ subtree: true,
+ attributes: false,
+ })
+}
+
+start(notifyWhenTalerUriIsFound, notifyWhenHeadIsFound);
diff --git a/packages/taler-wallet-webextension/src/taler-wallet-interaction-support.ts b/packages/taler-wallet-webextension/src/taler-wallet-interaction-support.ts
new file mode 100644
index 000000000..8b15380f9
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/taler-wallet-interaction-support.ts
@@ -0,0 +1,200 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * WARNING
+ *
+ * This script will be loaded and run in every page while the
+ * user us navigating. It must be short, simple and safe.
+ */
+(() => {
+ const logger = {
+ debug: (...msg: any[]) => { },
+ info: (...msg: any[]) =>
+ console.log(`${new Date().toISOString()} TALER`, ...msg),
+ error: (...msg: any[]) =>
+ console.error(`${new Date().toISOString()} TALER`, ...msg),
+ };
+
+ const documentDocTypeIsHTML =
+ window.document.doctype && window.document.doctype.name === "html";
+ const suffixIsNotXMLorPDF =
+ !window.location.pathname.endsWith(".xml") &&
+ !window.location.pathname.endsWith(".pdf");
+ const rootElementIsHTML =
+ document.documentElement.nodeName &&
+ document.documentElement.nodeName.toLowerCase() === "html";
+ const pageAcceptsTalerSupport = document.head.querySelector(
+ "meta[name=taler-support]",
+ );
+
+ // this is also checked by the loader
+ // but a double check will prevent running and breaking user navigation
+ // if loaded from other location
+ const shouldNotRun =
+ !documentDocTypeIsHTML ||
+ !suffixIsNotXMLorPDF ||
+ !pageAcceptsTalerSupport ||
+ !rootElementIsHTML;
+
+ interface Info {
+ extensionId: string;
+ protocol: string;
+ hostname: string;
+ }
+ interface API {
+ convertURIToWebExtensionPath: (uri: string) => string | undefined;
+ anchorOnClick: (ev: MouseEvent) => void;
+ registerProtocolHandler: () => void;
+ }
+ interface TalerSupport {
+ info: Readonly<Info>;
+ __internal: API;
+ }
+
+ function buildApi(config: Readonly<Info>): API {
+ /**
+ * Takes an anchor href that starts with taler:// and
+ * returns the path to the web-extension page
+ */
+ function convertURIToWebExtensionPath(uri: string): string | undefined {
+ if (!validateTalerUri(uri)) {
+ logger.error(`taler:// URI is invalid: ${uri}`);
+ return undefined;
+ }
+ const host = `${config.protocol}//${config.hostname}`;
+ const path = `static/wallet.html#/taler-uri/${encodeURIComponent(uri)}`;
+ return `${host}/${path}`;
+ }
+
+ function anchorOnClick(ev: MouseEvent) {
+ if (!(ev.currentTarget instanceof Element)) {
+ logger.debug(`onclick: registered in a link that is not an HTML element`);
+ return;
+ }
+ const hrefAttr = ev.currentTarget.attributes.getNamedItem("href");
+ if (!hrefAttr) {
+ logger.debug(`onclick: link didn't have href with taler:// uri`);
+ return;
+ }
+ const targetAttr = ev.currentTarget.attributes.getNamedItem("target");
+ const windowTarget =
+ targetAttr && targetAttr.value ? targetAttr.value : "_self";
+ const page = convertURIToWebExtensionPath(hrefAttr.value);
+ if (!page) {
+ logger.debug(`onclick: could not convert "${hrefAttr.value}" into path`);
+ return;
+ }
+ // we can use window.open, but maybe some browser will block it?
+ window.open(page, windowTarget);
+ ev.preventDefault();
+ ev.stopPropagation();
+ ev.stopImmediatePropagation();
+ return false;
+ }
+
+ function overrideAllAnchor(root: HTMLElement) {
+ const allAnchors = root.querySelectorAll("a[href^=taler]");
+ logger.debug(`registering taler protocol in ${allAnchors.length} links`);
+ allAnchors.forEach((link) => {
+ if (link instanceof HTMLElement) {
+ link.addEventListener("click", anchorOnClick);
+ }
+ });
+ }
+
+ function checkForNewAnchors(
+ mutations: MutationRecord[],
+ observer: MutationObserver,
+ ) {
+ mutations.forEach((mut) => {
+ if (mut.type === "childList") {
+ mut.addedNodes.forEach((added) => {
+ if (added instanceof HTMLElement) {
+ logger.debug(`new element`, added);
+ overrideAllAnchor(added);
+ }
+ });
+ }
+ });
+ }
+
+ /**
+ * Check of every anchor and observes for new one.
+ * Register the anchor handler when found
+ */
+ function registerProtocolHandler() {
+ if (document.body) overrideAllAnchor(document.body)
+ new MutationObserver(checkForNewAnchors).observe(document, {
+ childList: true,
+ subtree: true,
+ attributes: false,
+ });
+ }
+
+ return {
+ convertURIToWebExtensionPath,
+ anchorOnClick,
+ registerProtocolHandler,
+ };
+ }
+
+ function start() {
+ if (shouldNotRun) return;
+ if (!(document.currentScript instanceof HTMLScriptElement)) return;
+
+ const url = new URL(document.currentScript.src);
+ const { protocol, searchParams, hostname } = url;
+ const extensionId = searchParams.get("id") ?? "";
+ const debugEnabled = searchParams.get("debug") === "true";
+ const apiEnabled = searchParams.get("api") === "true";
+ const hijackEnabled = searchParams.get("hijack") === "true";
+
+ const info: Info = Object.freeze({
+ extensionId,
+ protocol,
+ hostname,
+ });
+
+ if (debugEnabled) {
+ logger.debug = logger.info;
+ }
+
+ const taler: TalerSupport = {
+ info,
+ __internal: buildApi(info),
+ };
+
+ if (apiEnabled) {
+ //@ts-ignore
+ window.taler = taler;
+ }
+
+ if (hijackEnabled) {
+ taler.__internal.registerProtocolHandler();
+ }
+ }
+
+ // utils functions
+ function validateTalerUri(uri: string): boolean {
+ return (
+ !!uri && (uri.startsWith("taler://") || uri.startsWith("taler+http://"))
+ );
+ }
+
+ start();
+})()
+
diff --git a/packages/taler-wallet-webextension/src/test-utils.ts b/packages/taler-wallet-webextension/src/test-utils.ts
index b4983b4c2..452cc578e 100644
--- a/packages/taler-wallet-webextension/src/test-utils.ts
+++ b/packages/taler-wallet-webextension/src/test-utils.ts
@@ -14,209 +14,27 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { NotificationType } from "@gnu-taler/taler-util";
+import { NotificationType, TalerBankIntegrationHttpClient, TalerCoreBankHttpClient, TalerRevenueHttpClient, TalerWireGatewayHttpClient, WalletNotification } from "@gnu-taler/taler-util";
import {
WalletCoreApiClient,
WalletCoreOpKeys,
WalletCoreRequestType,
WalletCoreResponseType,
} from "@gnu-taler/taler-wallet-core";
+import { ApiContextProvider, TranslationProvider, defaultRequestHandler } from "@gnu-taler/web-util/browser";
import {
ComponentChildren,
- Fragment,
FunctionalComponent,
- h as create,
- options,
- render as renderIntoDom,
VNode,
+ h as create,
} from "preact";
-import { render as renderToString } from "preact-render-to-string";
+import { AlertProvider } from "./context/alert.js";
+import { BackendProvider } from "./context/backend.js";
+import { strings } from "./i18n/strings.js";
+import { nullFunction } from "./mui/handlers.js";
import { BackgroundApiClient, wxApi } from "./wxApi.js";
-// When doing tests we want the requestAnimationFrame to be as fast as possible.
-// without this option the RAF will timeout after 100ms making the tests slower
-options.requestAnimationFrame = (fn: () => void) => {
- // console.log("RAF called")
- return fn();
-};
-
-export function createExample<Props>(
- Component: FunctionalComponent<Props>,
- props: Partial<Props> | (() => Partial<Props>),
-): ComponentChildren {
- //FIXME: props are evaluated on build time
- // in some cases we want to evaluated the props on render time so we can get some relative timestamp
- // check how we can build evaluatedProps in render time
- const evaluatedProps = typeof props === "function" ? props() : props;
- const Render = (args: any): VNode => create(Component, args);
- // Render.args = evaluatedProps;
-
- return {
- component: Render,
- props: evaluatedProps
- };
-}
-
-export function createExampleWithCustomContext<Props, ContextProps>(
- Component: FunctionalComponent<Props>,
- props: Partial<Props> | (() => Partial<Props>),
- ContextProvider: FunctionalComponent<ContextProps>,
- contextProps: Partial<ContextProps>,
-): ComponentChildren {
- const evaluatedProps = typeof props === "function" ? props() : props;
- const Render = (args: any): VNode => create(Component, args);
- const WithContext = (args: any): VNode =>
- create(ContextProvider, {
- ...contextProps,
- children: [Render(args)],
- } as any);
-
- return {
- component: WithContext,
- props: evaluatedProps
- };
-}
-
-export function NullLink({
- children,
-}: {
- children?: ComponentChildren;
-}): VNode {
- return create("a", { children, href: "javascript:void(0);" });
-}
-
-export function renderNodeOrBrowser(Component: any, args: any): void {
- const vdom = create(Component, args);
- if (typeof window === "undefined") {
- renderToString(vdom);
- } else {
- const div = document.createElement("div");
- document.body.appendChild(div);
- renderIntoDom(vdom, div);
- renderIntoDom(null, div);
- document.body.removeChild(div);
- }
-}
-type RecursiveState<S> = S | (() => RecursiveState<S>);
-
-interface Mounted<T> {
- unmount: () => void;
- pullLastResultOrThrow: () => Exclude<T, VoidFunction>;
- assertNoPendingUpdate: () => void;
- // waitNextUpdate: (s?: string) => Promise<void>;
- waitForStateUpdate: () => Promise<boolean>;
-}
-
-const isNode = typeof window === "undefined";
-
-export function mountHook<T extends object>(
- callback: () => RecursiveState<T>,
- Context?: ({ children }: { children: any }) => VNode,
-): Mounted<T> {
- let lastResult: Exclude<T, VoidFunction> | Error | null = null;
-
- const listener: Array<() => void> = [];
-
- // component that's going to hold the hook
- function Component(): VNode {
- try {
- let componentOrResult = callback();
- while (typeof componentOrResult === "function") {
- componentOrResult = componentOrResult();
- }
- //typecheck fails here
- const l: Exclude<T, () => void> = componentOrResult as any;
- lastResult = l;
- } catch (e) {
- if (e instanceof Error) {
- lastResult = e;
- } else {
- lastResult = new Error(`mounting the hook throw an exception: ${e}`);
- }
- }
-
- // notify to everyone waiting for an update and clean the queue
- listener.splice(0, listener.length).forEach((cb) => cb());
- return create(Fragment, {});
- }
-
- // create the vdom with context if required
- const vdom = !Context
- ? create(Component, {})
- : create(Context, { children: [create(Component, {})] });
-
- const customElement = {} as Element;
- const parentElement = isNode ? customElement : document.createElement("div");
- if (!isNode) {
- document.body.appendChild(parentElement);
- }
-
- renderIntoDom(vdom, parentElement);
-
- // clean up callback
- function unmount(): void {
- if (!isNode) {
- document.body.removeChild(parentElement);
- }
- }
-
- function pullLastResult(): Exclude<T | Error | null, VoidFunction> {
- const copy: Exclude<T | Error | null, VoidFunction> = lastResult;
- lastResult = null;
- return copy;
- }
-
- function pullLastResultOrThrow(): Exclude<T, VoidFunction> {
- const r = pullLastResult();
- if (r instanceof Error) throw r;
- if (!r) throw Error("there was no last result");
- return r;
- }
-
- async function assertNoPendingUpdate(): Promise<void> {
- await new Promise((res, rej) => {
- const tid = setTimeout(() => {
- res(undefined);
- }, 10);
-
- listener.push(() => {
- clearTimeout(tid);
- rej(
- Error(`Expecting no pending result but the hook got updated.
- If the update was not intended you need to check the hook dependencies
- (or dependencies of the internal state) but otherwise make
- sure to consume the result before ending the test.`),
- );
- });
- });
-
- const r = pullLastResult();
- if (r)
- throw Error(`There are still pending results.
- This may happen because the hook did a new update but the test didn't consume the result using pullLastResult`);
- }
- async function waitForStateUpdate(): Promise<boolean> {
- return await new Promise((res, rej) => {
- const tid = setTimeout(() => {
- res(false);
- }, 10);
-
- listener.push(() => {
- clearTimeout(tid);
- res(true);
- });
- });
- }
-
- return {
- unmount,
- pullLastResultOrThrow,
- waitForStateUpdate,
- assertNoPendingUpdate,
- };
-}
-
-export const nullFunction: any = () => null;
+// export const nullFunction: any = () => null;
interface MockHandler {
addWalletCallResponse<Op extends WalletCoreOpKeys>(
@@ -228,7 +46,7 @@ interface MockHandler {
getCallingQueueState(): "empty" | string;
- notifyEventFromWallet(event: NotificationType): void;
+ notifyEventFromWallet(notif: WalletNotification): void;
}
type CallRecord = WalletCallRecord | BackgroundCallRecord;
@@ -247,12 +65,12 @@ interface BackgroundCallRecord {
}
type Subscriptions = {
- [key in NotificationType]?: VoidFunction;
+ [key in NotificationType]?: (d: WalletNotification) => void;
};
export function createWalletApiMock(): {
handler: MockHandler;
- mock: typeof wxApi;
+ TestingContext: FunctionalComponent<{ children: ComponentChildren }>;
} {
const calls = new Array<CallRecord>();
const subscriptions: Subscriptions = {};
@@ -297,9 +115,12 @@ export function createWalletApiMock(): {
},
}),
listener: {
+ trigger: () => {
+
+ },
onUpdateNotification(
mTypes: NotificationType[],
- callback: (() => void) | undefined,
+ callback: ((d: WalletNotification) => void) | undefined,
): () => void {
mTypes.forEach((m) => {
subscriptions[m] = callback;
@@ -341,21 +162,53 @@ export function createWalletApiMock(): {
callback: cb
? cb
: () => {
- null;
- },
+ null;
+ },
});
return handler;
},
- notifyEventFromWallet(event: NotificationType): void {
- const callback = subscriptions[event];
+ notifyEventFromWallet(event: WalletNotification): void {
+ const callback = subscriptions[event.type];
if (!callback)
throw Error(`Expected to have a subscription for ${event}`);
- return callback();
+ return callback(event);
},
getCallingQueueState() {
return calls.length === 0 ? "empty" : `${calls.length} left`;
},
};
- return { handler, mock };
+ function TestingContext({
+ children: _cs,
+ }: {
+ children: ComponentChildren;
+ }): VNode {
+ let children = _cs;
+ children = create(AlertProvider, { children }, children);
+ const value = {
+ request: defaultRequestHandler,
+ bankCore: new TalerCoreBankHttpClient("/"),
+ bankIntegration: new TalerBankIntegrationHttpClient("/"),
+ bankWire: new TalerWireGatewayHttpClient("/",""),
+ bankRevenue: new TalerRevenueHttpClient("/"),
+ }
+ children = create(ApiContextProvider, { value, children }, children);
+ children = create(
+ TranslationProvider,
+ { children, source: strings, initial: "en", forceLang: "en" },
+ children,
+ );
+ return create(
+ BackendProvider,
+ {
+ wallet: mock.wallet,
+ background: mock.background,
+ listener: mock.listener,
+ children,
+ },
+ children,
+ );
+ }
+
+ return { handler, TestingContext };
}
diff --git a/packages/taler-wallet-webextension/src/utils/index.ts b/packages/taler-wallet-webextension/src/utils/index.ts
index c2d7c10a8..d83e6f472 100644
--- a/packages/taler-wallet-webextension/src/utils/index.ts
+++ b/packages/taler-wallet-webextension/src/utils/index.ts
@@ -15,6 +15,7 @@
*/
import { createElement, VNode } from "preact";
+import { useCallback, useMemo } from "preact/hooks";
function getJsonIfOk(r: Response): Promise<any> {
if (r.ok) {
@@ -26,8 +27,7 @@ function getJsonIfOk(r: Response): Promise<any> {
}
throw new Error(
- `Try another server: (${r.status}) ${
- r.statusText || "internal server error"
+ `Try another server: (${r.status}) ${r.statusText || "internal server error"
}`,
);
}
@@ -74,7 +74,7 @@ export async function queryToSlashKeys<T>(url: string): Promise<T> {
return timeout(3000, query);
}
-export type StateFunc<S> = (p: S) => VNode;
+export type StateFunc<S> = (p: S) => VNode | null;
export type StateViewMap<StateType extends { status: string }> = {
[S in StateType as S["status"]]: StateFunc<S>;
@@ -89,6 +89,7 @@ export function compose<SType extends { status: string }, PType>(
): (p: PType) => VNode {
function withHook(stateHook: () => RecursiveState<SType>): () => VNode {
function TheComponent(): VNode {
+ //if the function is the same, do not compute
const state = stateHook();
if (typeof state === "function") {
@@ -102,7 +103,9 @@ export function compose<SType extends { status: string }, PType>(
}
// TheComponent.name = `${name}`;
- return TheComponent;
+ return useMemo(() => {
+ return TheComponent
+ }, [stateHook]);
}
return (p: PType) => {
diff --git a/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/index.ts b/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/index.ts
index 3205588af..daa6b425d 100644
--- a/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/index.ts
+++ b/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/index.ts
@@ -15,29 +15,22 @@
*/
import {
- AmountJson,
- BackupBackupProviderTerms,
+ SyncTermsOfServiceResponse,
TalerErrorDetail,
} from "@gnu-taler/taler-util";
-import { SyncTermsOfServiceResponse } from "@gnu-taler/taler-wallet-core";
+import { ErrorAlertView } from "../../components/CurrentAlerts.js";
import { Loading } from "../../components/Loading.js";
-import { HookError } from "../../hooks/useAsyncAsHook.js";
+import { ErrorAlert } from "../../context/alert.js";
import {
ButtonHandler,
TextFieldHandler,
ToggleHandler,
} from "../../mui/handlers.js";
-import { compose, StateViewMap } from "../../utils/index.js";
-import { wxApi } from "../../wxApi.js";
+import { StateViewMap, compose } from "../../utils/index.js";
import { useComponentState } from "./state.js";
-import {
- LoadingUriView,
- SelectProviderView,
- ConfirmProviderView,
-} from "./views.js";
+import { ConfirmProviderView, SelectProviderView } from "./views.js";
export interface Props {
- currency: string;
onBack: () => Promise<void>;
onComplete: (pid: string) => Promise<void>;
onPaymentRequired: (uri: string) => Promise<void>;
@@ -56,13 +49,13 @@ export namespace State {
}
export interface LoadingUriError {
- status: "loading-error";
- error: HookError;
+ status: "error";
+ error: ErrorAlert;
}
export interface ConfirmProvider {
status: "confirm-provider";
- error: undefined | TalerErrorDetail;
+ error: undefined;
url: string;
provider: SyncTermsOfServiceResponse;
tos: ToggleHandler;
@@ -83,13 +76,13 @@ export namespace State {
const viewMapping: StateViewMap<State> = {
loading: Loading,
- "loading-error": LoadingUriView,
+ error: ErrorAlertView,
"select-provider": SelectProviderView,
"confirm-provider": ConfirmProviderView,
};
export const AddBackupProviderPage = compose(
"AddBackupProvider",
- (p: Props) => useComponentState(p, wxApi),
+ (p: Props) => useComponentState(p),
viewMapping,
);
diff --git a/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/state.ts b/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/state.ts
index 504ee4678..75b8e53c0 100644
--- a/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/state.ts
+++ b/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/state.ts
@@ -17,15 +17,13 @@
import {
canonicalizeBaseUrl,
Codec,
- TalerErrorDetail,
-} from "@gnu-taler/taler-util";
-import {
codecForSyncTermsOfServiceResponse,
- WalletApiOperation,
-} from "@gnu-taler/taler-wallet-core";
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useEffect, useState } from "preact/hooks";
+import { useAlertContext } from "../../context/alert.js";
+import { useBackendContext } from "../../context/backend.js";
import { assertUnreachable } from "../../utils/index.js";
-import { wxApi } from "../../wxApi.js";
import { Props, State } from "./index.js";
type UrlState<T> = UrlOk<T> | UrlError;
@@ -101,66 +99,69 @@ function useUrlState<T>(
}
const constHref = href;
- useDebounceEffect(
- 500,
- constHref == undefined
- ? undefined
- : async () => {
- const req = await fetch(constHref).catch((e) => {
- return setState({
- status: "network-error",
- href: constHref,
- });
- });
- if (!req) return;
+ async function checkURL() {
+ if (!constHref) {
+ return;
+ }
+ const req = await fetch(constHref).catch((e) => {
+ return setState({
+ status: "network-error",
+ href: constHref,
+ });
+ });
+ if (!req) return;
- if (req.status >= 400 && req.status < 500) {
- setState({
- status: "client-error",
- code: req.status,
- });
- return;
- }
- if (req.status > 500) {
- setState({
- status: "server-error",
- code: req.status,
- });
- return;
- }
+ if (req.status >= 400 && req.status < 500) {
+ setState({
+ status: "client-error",
+ code: req.status,
+ });
+ return;
+ }
+ if (req.status > 500) {
+ setState({
+ status: "server-error",
+ code: req.status,
+ });
+ return;
+ }
- const json = await req.json();
- try {
- const result = codec.decode(json);
- setState({ status: "ok", result });
- } catch (e: any) {
- setState({ status: "parsing-error", json });
- }
- },
+ const json = await req.json();
+ try {
+ const result = codec.decode(json);
+ setState({ status: "ok", result });
+ } catch (e: any) {
+ setState({ status: "parsing-error", json });
+ }
+ }
+
+ useDebounceEffect(
+ 500,
+ constHref == undefined ? undefined : checkURL,
[host, path],
);
return state;
}
-export function useComponentState(
- { currency, onBack, onComplete, onPaymentRequired }: Props,
- api: typeof wxApi,
-): State {
+export function useComponentState({
+ onBack,
+ onComplete,
+ onPaymentRequired,
+}: Props): State {
+ const api = useBackendContext();
const [url, setHost] = useState<string | undefined>();
const [name, setName] = useState<string | undefined>();
const [tos, setTos] = useState(false);
+ const { pushAlertOnError } = useAlertContext();
const urlState = useUrlState(
url,
"config",
codecForSyncTermsOfServiceResponse(),
);
- const [operationError, setOperationError] = useState<
- TalerErrorDetail | undefined
- >();
const [showConfirm, setShowConfirm] = useState(false);
- async function addBackupProvider() {
+ async function addBackupProvider(): Promise<void> {
if (!url || !name) return;
const resp = await api.wallet.call(WalletApiOperation.AddBackupProvider, {
@@ -176,8 +177,6 @@ export function useComponentState(
} else {
return onComplete(url);
}
- case "error":
- return setOperationError(resp.error);
case "ok":
return onComplete(url);
default:
@@ -188,18 +187,18 @@ export function useComponentState(
if (showConfirm && urlState && urlState.status === "ok") {
return {
status: "confirm-provider",
- error: operationError,
+ error: undefined,
onAccept: {
- onClick: !tos ? undefined : addBackupProvider,
+ onClick: !tos ? undefined : pushAlertOnError(addBackupProvider),
},
onCancel: {
- onClick: onBack,
+ onClick: pushAlertOnError(onBack),
},
provider: urlState.result,
tos: {
value: tos,
button: {
- onClick: async () => setTos(!tos),
+ onClick: pushAlertOnError(async () => setTos(!tos)),
},
},
url: url ?? "",
@@ -211,25 +210,25 @@ export function useComponentState(
error: undefined,
name: {
value: name || "",
- onInput: async (e) => setName(e),
+ onInput: pushAlertOnError(async (e) => setName(e)),
error:
name === undefined ? undefined : !name ? "Can't be empty" : undefined,
},
onCancel: {
- onClick: onBack,
+ onClick: pushAlertOnError(onBack),
},
onConfirm: {
onClick:
!urlState || urlState.status !== "ok" || !name
? undefined
- : async () => {
+ : pushAlertOnError(async () => {
setShowConfirm(true);
- },
+ }),
},
urlOk: urlState?.status === "ok",
url: {
value: url || "",
- onInput: async (e) => setHost(e),
+ onInput: pushAlertOnError(async (e) => setHost(e)),
error: errorString(urlState),
},
};
diff --git a/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/stories.tsx b/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/stories.tsx
index 887ad235e..7ac92c6c9 100644
--- a/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/stories.tsx
@@ -19,17 +19,18 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { createExample } from "../../test-utils.js";
+import * as tests from "@gnu-taler/web-util/testing";
import { ConfirmProviderView, SelectProviderView } from "./views.js";
+import { AmountString } from "@gnu-taler/taler-util";
export default {
title: "add backup provider",
};
-export const DemoService = createExample(ConfirmProviderView, {
+export const DemoService = tests.createExample(ConfirmProviderView, {
url: "https://sync.demo.taler.net/",
provider: {
- annual_fee: "KUDOS:0.1",
+ annual_fee: "KUDOS:0.1" as AmountString,
storage_limit_in_megabytes: 20,
version: "1",
},
@@ -40,10 +41,10 @@ export const DemoService = createExample(ConfirmProviderView, {
onCancel: {},
});
-export const FreeService = createExample(ConfirmProviderView, {
+export const FreeService = tests.createExample(ConfirmProviderView, {
url: "https://sync.taler:9667/",
provider: {
- annual_fee: "ARS:0",
+ annual_fee: "ARS:0" as AmountString,
storage_limit_in_megabytes: 20,
version: "1",
},
@@ -54,14 +55,14 @@ export const FreeService = createExample(ConfirmProviderView, {
onCancel: {},
});
-export const Initial = createExample(SelectProviderView, {
+export const Initial = tests.createExample(SelectProviderView, {
url: { value: "" },
name: { value: "" },
onCancel: {},
onConfirm: {},
});
-export const WithValue = createExample(SelectProviderView, {
+export const WithValue = tests.createExample(SelectProviderView, {
url: {
value: "sync.demo.taler.net",
},
@@ -72,7 +73,7 @@ export const WithValue = createExample(SelectProviderView, {
onConfirm: {},
});
-export const WithConnectionError = createExample(SelectProviderView, {
+export const WithConnectionError = tests.createExample(SelectProviderView, {
url: {
value: "sync.demo.taler.net",
error: "Network error",
@@ -84,7 +85,7 @@ export const WithConnectionError = createExample(SelectProviderView, {
onConfirm: {},
});
-export const WithClientError = createExample(SelectProviderView, {
+export const WithClientError = tests.createExample(SelectProviderView, {
url: {
value: "sync.demo.taler.net",
error: "URL may not be right: (404) Not Found",
@@ -96,7 +97,7 @@ export const WithClientError = createExample(SelectProviderView, {
onConfirm: {},
});
-export const WithServerError = createExample(SelectProviderView, {
+export const WithServerError = tests.createExample(SelectProviderView, {
url: {
value: "sync.demo.taler.net",
error: "Try another server: (500) Internal Server Error",
diff --git a/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/test.ts b/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/test.ts
index 1143853f8..058f4f460 100644
--- a/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/test.ts
+++ b/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/test.ts
@@ -19,61 +19,50 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { expect } from "chai";
-import {
- createWalletApiMock,
- mountHook,
- nullFunction,
-} from "../../test-utils.js";
+import * as tests from "@gnu-taler/web-util/testing";
+import { nullFunction } from "../../mui/handlers.js";
+import { createWalletApiMock } from "../../test-utils.js";
import { Props } from "./index.js";
import { useComponentState } from "./state.js";
const props: Props = {
- currency: "KUDOS",
onBack: nullFunction,
onComplete: nullFunction,
onPaymentRequired: nullFunction,
};
describe("AddBackupProvider states", () => {
- it("should start in 'select-provider' state", async () => {
- const { handler, mock } = createWalletApiMock();
+ /**
+ * FIXME: this test has inconsistent behavior.
+ * it should always expect one state but for some reason
+ * (maybe race condition) it sometime expect 1 update when
+ * it should no update
+ */
+ it.skip("should start in 'select-provider' state", async () => {
+ const { handler, TestingContext } = createWalletApiMock();
- // handler.addWalletCallResponse(
- // WalletApiOperation.ListKnownBankAccounts,
- // undefined,
- // {
- // accounts: [],
- // },
- // );
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentState,
+ props,
+ [
+ (state) => {
+ expect(state.status).equal("select-provider");
+ if (state.status !== "select-provider") return;
+ expect(state.name.value).eq("");
+ expect(state.url.value).eq("");
+ },
+ //FIXME: this shouldn't take 2 updates, just
+ // (state) => {
+ // expect(state.status).equal("select-provider");
+ // if (state.status !== "select-provider") return;
+ // expect(state.name.value).eq("");
+ // expect(state.url.value).eq("");
+ // },
+ ],
+ TestingContext,
+ );
- const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
- mountHook(() => useComponentState(props, mock));
-
- {
- const state = pullLastResultOrThrow();
- expect(state.status).equal("select-provider");
- if (state.status !== "select-provider") return;
- expect(state.name.value).eq("");
- expect(state.url.value).eq("");
- }
-
- //FIXME: this should not make an extra update
- /**
- * this may be due to useUrlState because is using an effect over
- * a dependency with a timeout
- */
- // NOTE: do not remove this comment, keeping as an example
- // await waitForStateUpdate()
- // {
- // const state = pullLastResultOrThrow();
- // expect(state.status).equal("select-provider");
- // if (state.status !== "select-provider") return;
- // expect(state.name.value).eq("")
- // expect(state.url.value).eq("")
- // }
-
- await assertNoPendingUpdate();
+ expect(hookBehavior).deep.equal({ result: "ok" });
expect(handler.getCallingQueueState()).eq("empty");
});
});
diff --git a/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/views.tsx b/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/views.tsx
index b633a595f..c67c288dc 100644
--- a/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/views.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/views.tsx
@@ -17,30 +17,17 @@
import { Amounts } from "@gnu-taler/taler-util";
import { Fragment, h, VNode } from "preact";
import { Checkbox } from "../../components/Checkbox.js";
-import { LoadingError } from "../../components/LoadingError.js";
import {
LightText,
SmallLightText,
SubTitle,
- TermsOfService,
Title,
} from "../../components/styled/index.js";
-import { useTranslationContext } from "../../context/translation.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Button } from "../../mui/Button.js";
import { TextField } from "../../mui/TextField.js";
import { State } from "./index.js";
-export function LoadingUriView({ error }: State.LoadingUriError): VNode {
- const { i18n } = useTranslationContext();
-
- return (
- <LoadingError
- title={<i18n.Translate>Could not load</i18n.Translate>}
- error={error}
- />
- );
-}
-
export function ConfirmProviderView({
url,
provider,
@@ -88,9 +75,8 @@ export function ConfirmProviderView({
of service
</i18n.Translate>
</p>
- {/* replace with <TermsOfService /> */}
<Checkbox
- label={<i18n.Translate>Accept terms of service</i18n.Translate>}
+ label={i18n.str`Accept terms of service`}
name="terms"
onToggle={tos.button.onClick}
enabled={tos.value}
diff --git a/packages/taler-wallet-webextension/src/wallet/AddExchange/index.ts b/packages/taler-wallet-webextension/src/wallet/AddExchange/index.ts
new file mode 100644
index 000000000..94b32c157
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/AddExchange/index.ts
@@ -0,0 +1,92 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { OperationFailWithBody, OperationOk, TalerExchangeApi } from "@gnu-taler/taler-util";
+import { ErrorAlertView } from "../../components/CurrentAlerts.js";
+import { Loading } from "../../components/Loading.js";
+import { ErrorAlert } from "../../context/alert.js";
+import { TextFieldHandler } from "../../mui/handlers.js";
+import { StateViewMap, compose } from "../../utils/index.js";
+import { useComponentState } from "./state.js";
+import { ConfirmAddExchangeView, VerifyView } from "./views.js";
+
+export interface Props {
+ currency?: string;
+ onBack: () => Promise<void>;
+ noDebounce?: boolean;
+}
+
+export type State = State.Loading
+ | State.LoadingUriError
+ | State.Confirm
+ | State.Verify;
+
+export type CheckExchangeErrors = {
+ "invalid-version": string;
+ "invalid-currency": string;
+ "not-found": void;
+ "already-active": void;
+ "invalid-protocol": void;
+}
+
+export namespace State {
+ export interface Loading {
+ status: "loading";
+ error: undefined;
+ }
+
+ export interface LoadingUriError {
+ status: "error";
+ error: ErrorAlert;
+ }
+
+ export interface BaseInfo {
+ error: undefined;
+ }
+ export interface Confirm extends BaseInfo {
+ status: "confirm";
+ url: string;
+ onCancel: () => Promise<void>;
+ onConfirm: () => Promise<void>;
+ error: undefined;
+ }
+ export interface Verify extends BaseInfo {
+ status: "verify";
+ error: undefined;
+
+ onCancel: () => Promise<void>;
+ onAccept: () => Promise<void>;
+
+ url: TextFieldHandler,
+ loading: boolean;
+ knownExchanges: URL[],
+ result: OperationOk<TalerExchangeApi.ExchangeKeysResponse> | OperationFailWithBody<CheckExchangeErrors> | undefined,
+ expectedCurrency: string | undefined,
+ }
+}
+
+const viewMapping: StateViewMap<State> = {
+ loading: Loading,
+ error: ErrorAlertView,
+ confirm: ConfirmAddExchangeView,
+ verify: VerifyView,
+};
+
+export const AddExchange = compose(
+ "AddExchange",
+ (p: Props) => useComponentState(p),
+ viewMapping,
+);
diff --git a/packages/taler-wallet-webextension/src/wallet/AddExchange/state.ts b/packages/taler-wallet-webextension/src/wallet/AddExchange/state.ts
new file mode 100644
index 000000000..4a04f762a
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/AddExchange/state.ts
@@ -0,0 +1,198 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { ExchangeEntryStatus, TalerExchangeHttpClient, canonicalizeBaseUrl, opKnownFailureWithBody } from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { BrowserFetchHttpLib } from "@gnu-taler/web-util/browser";
+import { useCallback, useEffect, useState } from "preact/hooks";
+import { useBackendContext } from "../../context/backend.js";
+import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
+import { withSafe } from "../../mui/handlers.js";
+import { RecursiveState } from "../../utils/index.js";
+import { CheckExchangeErrors, Props, State } from "./index.js";
+
+function urlFromInput(str: string): URL {
+ let result: URL;
+ try {
+ result = new URL(str)
+ } catch (original) {
+ try {
+ result = new URL(`https://${str}`)
+ } catch (e) {
+ throw original
+ }
+ }
+ if (!result.pathname.endsWith("/")) {
+ result.pathname = result.pathname + "/";
+ }
+ result.search = "";
+ result.hash = "";
+ return result;
+}
+
+export function useComponentState({ onBack, currency, noDebounce }: Props): RecursiveState<State> {
+ const [verified, setVerified] = useState<string>();
+
+ const api = useBackendContext();
+ const hook = useAsyncAsHook(() =>
+ api.wallet.call(WalletApiOperation.ListExchanges, {}),
+ );
+ const walletExchanges = !hook ? [] : hook.hasError ? [] : hook.response.exchanges
+ const used = walletExchanges.filter(e => e.exchangeEntryStatus === ExchangeEntryStatus.Used);
+ const preset = walletExchanges.filter(e => e.exchangeEntryStatus === ExchangeEntryStatus.Preset);
+
+ if (!verified) {
+ return (): State => {
+ const checkExchangeBaseUrl_memo = useCallback(async function checkExchangeBaseUrl(str: string) {
+ const baseUrl = urlFromInput(str)
+ if (baseUrl.protocol !== "http:" && baseUrl.protocol !== "https:") {
+ return opKnownFailureWithBody<CheckExchangeErrors>("invalid-protocol", undefined)
+ }
+ const found = used.findIndex((e) => e.exchangeBaseUrl === baseUrl.href);
+ if (found !== -1) {
+ return opKnownFailureWithBody<CheckExchangeErrors>("already-active", undefined);
+ }
+
+ /**
+ * FIXME: For some reason typescript doesn't like the next BrowserFetchHttpLib
+ *
+ * │ src/wallet/AddExchange/state.ts(68,63): error TS2345: Argument of type 'BrowserFetchHttpLib' is not assignable to parameter of ty
+ * │ Types of property 'fetch' are incompatible.
+ * │ Type '(requestUrl: string, options?: HttpRequestOptions | undefined) => Promise<HttpResponse>' is not assignable to type '(ur
+ * │ Types of parameters 'options' and 'opt' are incompatible.
+ * │ Type 'import("$PATH/wallet.git/packages/taler-util/lib/http-common", { wi
+ * │ Type 'import("$PATH/wallet.git/packages/taler-util/lib/http-common", {
+ * │ Types of property 'cancellationToken' are incompatible.
+ * │ Type 'import("$PATH/wallet.git/packages/taler-util/lib/Cancellation
+ * │ Type 'import("$PATH/wallet.git/packages/taler-util/lib/Cancellati
+ * │ Types have separate declarations of a private property '_isCancelled'.
+ *
+ */
+ const api = new TalerExchangeHttpClient(baseUrl.href, new BrowserFetchHttpLib() as any);
+ const config = await api.getConfig()
+ if (config.type === "fail") {
+ return opKnownFailureWithBody<CheckExchangeErrors>("not-found", undefined)
+ }
+ if (!api.isCompatible(config.body.version)) {
+ return opKnownFailureWithBody<CheckExchangeErrors>("invalid-version", config.body.version)
+ }
+ if (currency !== undefined && currency !== config.body.currency) {
+ return opKnownFailureWithBody<CheckExchangeErrors>("invalid-currency", config.body.currency)
+ }
+ const keys = await api.getKeys()
+ return keys
+ }, [used])
+
+ const { result, value: url, loading, update, error: requestError } = useDebounce(checkExchangeBaseUrl_memo, noDebounce ?? false)
+ const [inputError, setInputError] = useState<string>()
+
+ return {
+ status: "verify",
+ error: undefined,
+ onCancel: onBack,
+ expectedCurrency: currency,
+ onAccept: async () => {
+ if (!result || result.type !== "ok") return;
+ setVerified(result.body.base_url)
+ },
+ result,
+ loading,
+ knownExchanges: preset.map(e => new URL(e.exchangeBaseUrl)),
+ url: {
+ value: url ?? "",
+ error: inputError ?? requestError,
+ onInput: withSafe(update, (e) => {
+ setInputError(e.message)
+ })
+ },
+ };
+ }
+ }
+
+ async function onConfirm() {
+ if (!verified) return;
+ await api.wallet.call(WalletApiOperation.AddExchange, {
+ exchangeBaseUrl: canonicalizeBaseUrl(verified),
+ forceUpdate: true,
+ });
+ onBack();
+ }
+
+ return {
+ status: "confirm",
+ error: undefined,
+ onCancel: onBack,
+ onConfirm,
+ url: verified
+ };
+}
+
+
+
+function useDebounce<T>(
+ onTrigger: (v: string) => Promise<T>,
+ disabled: boolean,
+): {
+ loading: boolean;
+ error?: Error;
+ value: string | undefined;
+ result: T | undefined;
+ update: (s: string) => void;
+} {
+ const [value, setValue] = useState<string>();
+ const [dirty, setDirty] = useState(false);
+ const [loading, setLoading] = useState(false);
+ const [result, setResult] = useState<T | undefined>(undefined);
+ const [error, setError] = useState<Error | undefined>(undefined);
+
+ const [handler, setHandler] = useState<number | undefined>(undefined);
+
+ if (!disabled) {
+ useEffect(() => {
+ if (!value) return;
+ clearTimeout(handler);
+ const h = setTimeout(async () => {
+ setDirty(true);
+ setLoading(true);
+ try {
+ const result = await onTrigger(value);
+ setResult(result);
+ setError(undefined);
+ setLoading(false);
+ } catch (er) {
+ if (er instanceof Error) {
+ setError(er);
+ } else {
+ // @ts-expect-error cause still not in typescript
+ setError(new Error('unkown error on debounce', { cause: er }))
+ }
+ setLoading(false);
+ setResult(undefined);
+ }
+ }, 500);
+ setHandler(h as unknown as number);
+ }, [value, setHandler, onTrigger]);
+ }
+
+ return {
+ error: dirty ? error : undefined,
+ loading: loading,
+ result: result,
+ value: value,
+ update: disabled ? onTrigger : setValue,
+ };
+}
+
diff --git a/packages/merchant-backoffice-ui/src/.babelrc b/packages/taler-wallet-webextension/src/wallet/AddExchange/stories.tsx
index 3ec8a6291..f205b6415 100644
--- a/packages/merchant-backoffice-ui/src/.babelrc
+++ b/packages/taler-wallet-webextension/src/wallet/AddExchange/stories.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -14,13 +14,14 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- /**
+/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
-{
- "presets": [
- "preact-cli/babel"
- ]
-}
+
+export default {
+ title: "example",
+};
+
+// export const Ready = tests.createExample(ReadyView, {});
diff --git a/packages/taler-wallet-webextension/src/wallet/AddExchange/test.ts b/packages/taler-wallet-webextension/src/wallet/AddExchange/test.ts
new file mode 100644
index 000000000..d0e78a94e
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/AddExchange/test.ts
@@ -0,0 +1,209 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import {
+ ExchangeEntryStatus,
+ ExchangeTosStatus,
+ ExchangeUpdateStatus,
+ ScopeType,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import * as tests from "@gnu-taler/web-util/testing";
+import { expect } from "chai";
+import { nullFunction } from "../../mui/handlers.js";
+import { createWalletApiMock } from "../../test-utils.js";
+import { Props } from "./index.js";
+import { useComponentState } from "./state.js";
+const props: Props = {
+ onBack: nullFunction,
+ noDebounce: true,
+};
+
+describe("AddExchange states", () => {
+ it("should start in 'verify' state", async () => {
+ const { handler, TestingContext } = createWalletApiMock();
+
+ handler.addWalletCallResponse(
+ WalletApiOperation.ListExchanges,
+ {},
+ {
+ exchanges: [
+ {
+ exchangeBaseUrl: "http://exchange.local/",
+ ageRestrictionOptions: [],
+ scopeInfo: {
+ currency: "ARS",
+ type: ScopeType.Exchange,
+ url: "http://exchange.local/",
+ },
+ masterPub: "123qwe123",
+ currency: "ARS",
+ exchangeEntryStatus: ExchangeEntryStatus.Ephemeral,
+ tosStatus: ExchangeTosStatus.Pending,
+ exchangeUpdateStatus: ExchangeUpdateStatus.UnavailableUpdate,
+ paytoUris: [],
+ lastUpdateTimestamp: undefined,
+ noFees: false,
+ peerPaymentsDisabled: false,
+ },
+ ],
+ },
+ );
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentState,
+ props,
+ [
+ (state) => {
+ expect(state.status).equal("verify");
+ if (state.status !== "verify") return;
+ expect(state.url.value).eq("");
+ expect(state.expectedCurrency).is.undefined;
+ expect(state.result).is.undefined;
+ },
+ (state) => {
+ expect(state.status).equal("verify");
+ if (state.status !== "verify") return;
+ expect(state.url.value).eq("");
+ expect(state.expectedCurrency).is.undefined;
+ expect(state.result).is.undefined;
+ },
+ ],
+ TestingContext,
+ );
+
+ expect(hookBehavior).deep.equal({ result: "ok" });
+ expect(handler.getCallingQueueState()).eq("empty");
+ });
+
+ // it("should not be able to add a known exchange", async () => {
+ // const { handler, TestingContext } = createWalletApiMock();
+
+ // handler.addWalletCallResponse(
+ // WalletApiOperation.ListExchanges,
+ // {},
+ // {
+ // exchanges: [
+ // {
+ // exchangeBaseUrl: "http://exchange.local/",
+ // ageRestrictionOptions: [],
+ // scopeInfo: undefined,
+ // currency: "ARS",
+ // exchangeEntryStatus: ExchangeEntryStatus.Used,
+ // tosStatus: ExchangeTosStatus.Pending,
+ // exchangeUpdateStatus: ExchangeUpdateStatus.Ready,
+ // paytoUris: [],
+ // },
+ // ],
+ // },
+ // );
+
+ // const hookBehavior = await tests.hookBehaveLikeThis(
+ // useComponentState,
+ // props,
+ // [
+ // (state) => {
+ // expect(state.status).equal("verify");
+ // if (state.status !== "verify") return;
+ // expect(state.url.value).eq("");
+ // expect(state.expectedCurrency).is.undefined;
+ // expect(state.result).is.undefined;
+ // },
+ // (state) => {
+ // expect(state.status).equal("verify");
+ // if (state.status !== "verify") return;
+ // expect(state.url.value).eq("");
+ // expect(state.expectedCurrency).is.undefined;
+ // expect(state.result).is.undefined;
+ // expect(state.error).is.undefined;
+ // expect(state.url.onInput).is.not.undefined;
+ // if (!state.url.onInput) return;
+ // state.url.onInput("http://exchange.local/");
+ // },
+ // (state) => {
+ // expect(state.status).equal("verify");
+ // if (state.status !== "verify") return;
+ // expect(state.url.value).eq("");
+ // expect(state.expectedCurrency).is.undefined;
+ // expect(state.result).is.undefined;
+ // expect(state.url.error).eq("This exchange is already active");
+ // expect(state.url.onInput).is.not.undefined;
+ // },
+ // ],
+ // TestingContext,
+ // );
+
+ // expect(hookBehavior).deep.equal({ result: "ok" });
+ // expect(handler.getCallingQueueState()).eq("empty");
+ // });
+
+ // it("should be able to add a preset exchange", async () => {
+ // const { handler, TestingContext } = createWalletApiMock();
+
+ // handler.addWalletCallResponse(
+ // WalletApiOperation.ListExchanges,
+ // {},
+ // {
+ // exchanges: [
+ // {
+ // exchangeBaseUrl: "http://exchange.local/",
+ // ageRestrictionOptions: [],
+ // scopeInfo: undefined,
+ // currency: "ARS",
+ // exchangeEntryStatus: ExchangeEntryStatus.Preset,
+ // tosStatus: ExchangeTosStatus.Pending,
+ // exchangeUpdateStatus: ExchangeUpdateStatus.Ready,
+ // paytoUris: [],
+ // },
+ // ],
+ // },
+ // );
+
+ // const hookBehavior = await tests.hookBehaveLikeThis(
+ // useComponentState,
+ // props,
+ // [
+ // (state) => {
+ // expect(state.status).equal("verify");
+ // if (state.status !== "verify") return;
+ // expect(state.url.value).eq("");
+ // expect(state.expectedCurrency).is.undefined;
+ // expect(state.result).is.undefined;
+ // },
+ // (state) => {
+ // expect(state.status).equal("verify");
+ // if (state.status !== "verify") return;
+ // expect(state.url.value).eq("");
+ // expect(state.expectedCurrency).is.undefined;
+ // expect(state.result).is.undefined;
+ // expect(state.error).is.undefined;
+ // expect(state.url.onInput).is.not.undefined;
+ // if (!state.url.onInput) return;
+ // state.url.onInput("http://exchange.local/");
+ // },
+ // ],
+ // TestingContext,
+ // );
+
+ // expect(hookBehavior).deep.equal({ result: "ok" });
+ // expect(handler.getCallingQueueState()).eq("empty");
+ // });
+});
diff --git a/packages/taler-wallet-webextension/src/wallet/AddExchange/views.tsx b/packages/taler-wallet-webextension/src/wallet/AddExchange/views.tsx
new file mode 100644
index 000000000..f6537bc68
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/AddExchange/views.tsx
@@ -0,0 +1,251 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { ErrorMessage } from "../../components/ErrorMessage.js";
+import {
+ Input,
+ LightText,
+ SubTitle,
+ Title,
+ WarningBox,
+} from "../../components/styled/index.js";
+import { TermsOfService } from "../../components/TermsOfService/index.js";
+import { Button } from "../../mui/Button.js";
+import { State } from "./index.js";
+import { assertUnreachable } from "@gnu-taler/taler-util";
+
+export function VerifyView({
+ expectedCurrency,
+ onCancel,
+ onAccept,
+ result,
+ loading,
+ knownExchanges,
+ url,
+}: State.Verify): VNode {
+ const { i18n } = useTranslationContext();
+
+ return (
+ <Fragment>
+ <section>
+ {!expectedCurrency ? (
+ <Title>
+ <i18n.Translate>Add new exchange</i18n.Translate>
+ </Title>
+ ) : (
+ <SubTitle>
+ <i18n.Translate>Add exchange for {expectedCurrency}</i18n.Translate>
+ </SubTitle>
+ )}
+ {!result && (
+ <LightText>
+ <i18n.Translate>
+ Enter the URL of an exchange you trust.
+ </i18n.Translate>
+ </LightText>
+ )}
+ {(() => {
+ if (!result) return;
+ if (result.type == "ok") {
+ return (
+ <LightText>
+ <i18n.Translate>
+ An exchange has been found! Review the information and click
+ next
+ </i18n.Translate>
+ </LightText>
+ );
+ }
+ switch (result.case) {
+ case "already-active": {
+ return (
+ <WarningBox>
+ <i18n.Translate>
+ This exchange is already in your list.
+ </i18n.Translate>
+ </WarningBox>
+ );
+ }
+ case "invalid-protocol": {
+ return (
+ <WarningBox>
+ <i18n.Translate>
+ Only exchange accessible through "http" and "https" are
+ allowed.
+ </i18n.Translate>
+ </WarningBox>
+ );
+ }
+ case "invalid-version": {
+ return (
+ <WarningBox>
+ <i18n.Translate>
+ This exchange protocol version is not supported: "
+ {result.body}".
+ </i18n.Translate>
+ </WarningBox>
+ );
+ }
+ case "invalid-currency": {
+ return (
+ <WarningBox>
+ <i18n.Translate>
+ This exchange currency "{result.body}" doesn&apos;t match
+ the expected currency {expectedCurrency}.
+ </i18n.Translate>
+ </WarningBox>
+ );
+ }
+ case "not-found": {
+ return (
+ <WarningBox>
+ <i18n.Translate>
+ No exchange found in that URL.
+ </i18n.Translate>
+ </WarningBox>
+ );
+ }
+ default: {
+ assertUnreachable(result.case);
+ }
+ }
+ })()}
+ <p>
+ <Input invalid={result && result.type !== "ok"}>
+ <label>URL</label>
+ <input
+ type="text"
+ placeholder="https://"
+ value={url.value}
+ onInput={(e) => {
+ if (url.onInput) {
+ url.onInput(e.currentTarget.value);
+ }
+ }}
+ />
+ </Input>
+ {loading && (
+ <div>
+ <i18n.Translate>loading</i18n.Translate>...
+ </div>
+ )}
+ {result && result.type === "ok" && (
+ <Fragment>
+ <Input>
+ <label>
+ <i18n.Translate>Version</i18n.Translate>
+ </label>
+ <input type="text" disabled value={result.body.version} />
+ </Input>
+ <Input>
+ <label>
+ <i18n.Translate>Currency</i18n.Translate>
+ </label>
+ <input type="text" disabled value={result.body.currency} />
+ </Input>
+ </Fragment>
+ )}
+ </p>
+ {url.value && url.error && (
+ <ErrorMessage
+ title={i18n.str`Can't use the URL: "${url.value}"`}
+ description={url.error}
+ />
+ )}
+ </section>
+ <footer>
+ <Button variant="contained" color="secondary" onClick={onCancel}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </Button>
+ <Button
+ variant="contained"
+ disabled={!result || result.type !== "ok"}
+ onClick={onAccept}
+ >
+ <i18n.Translate>Next</i18n.Translate>
+ </Button>
+ </footer>
+ <section>
+ <ul>
+ {knownExchanges.map((ex) => {
+ return (
+ <li key={ex.href}>
+ <a
+ href="#"
+ onClick={(e) => {
+ if (url.onInput) {
+ url.onInput(ex.href);
+ }
+ e.preventDefault();
+ }}
+ >
+ {ex.href}
+ </a>
+ </li>
+ );
+ })}
+ </ul>
+ </section>
+ </Fragment>
+ );
+}
+
+export function ConfirmAddExchangeView({
+ url,
+ onCancel,
+ onConfirm,
+}: State.Confirm): VNode {
+ const { i18n } = useTranslationContext();
+
+ return (
+ <Fragment>
+ <section>
+ <Title>
+ <i18n.Translate>Review terms of service</i18n.Translate>
+ </Title>
+ <div>
+ <i18n.Translate>Exchange URL</i18n.Translate>:
+ <a href={url} target="_blank" rel="noreferrer">
+ {url}
+ </a>
+ </div>
+ </section>
+
+ <TermsOfService key="terms" exchangeUrl={url}>
+ <footer>
+ <Button
+ key="cancel"
+ variant="contained"
+ color="secondary"
+ onClick={onCancel}
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </Button>
+ <Button
+ key="add"
+ variant="contained"
+ color="success"
+ onClick={onConfirm}
+ >
+ <i18n.Translate>Add exchange</i18n.Translate>
+ </Button>
+ </footer>
+ </TermsOfService>
+ </Fragment>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/wallet/AddNewActionView.stories.tsx b/packages/taler-wallet-webextension/src/wallet/AddNewActionView.stories.tsx
index f5db3825d..704f9e9a1 100644
--- a/packages/taler-wallet-webextension/src/wallet/AddNewActionView.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/AddNewActionView.stories.tsx
@@ -19,7 +19,7 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { createExample } from "../test-utils.js";
+import * as tests from "@gnu-taler/web-util/testing";
import { AddNewActionView as TestedComponent } from "./AddNewActionView.js";
export default {
@@ -30,4 +30,4 @@ export default {
},
};
-export const Initial = createExample(TestedComponent, {});
+export const Initial = tests.createExample(TestedComponent, {});
diff --git a/packages/taler-wallet-webextension/src/wallet/AddNewActionView.tsx b/packages/taler-wallet-webextension/src/wallet/AddNewActionView.tsx
index c704c8085..dd1777fd1 100644
--- a/packages/taler-wallet-webextension/src/wallet/AddNewActionView.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/AddNewActionView.tsx
@@ -13,13 +13,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 { classifyTalerUri, TalerUriType } from "@gnu-taler/taler-util";
+import { parseTalerUri, TalerUriAction } 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 { platform } from "../platform/api.js";
import { InputWithLabel } from "../components/styled/index.js";
-import { useTranslationContext } from "../context/translation.js";
import { Button } from "../mui/Button.js";
+import { platform } from "../platform/foreground.js";
export interface Props {
onCancel: () => Promise<void>;
@@ -27,19 +27,17 @@ export interface Props {
export function AddNewActionView({ onCancel }: Props): VNode {
const [url, setUrl] = useState("");
- const uriType = classifyTalerUri(url);
+ const uri = parseTalerUri(url);
const { i18n } = useTranslationContext();
async function redirectToWallet(): Promise<void> {
- platform.openWalletURIFromPopup(url);
+ platform.openWalletURIFromPopup(uri!);
}
return (
<Fragment>
<section>
- <InputWithLabel
- invalid={url !== "" && uriType === TalerUriType.Unknown}
- >
+ <InputWithLabel invalid={url !== "" && !uri}>
<label>GNU Taler URI</label>
<div>
<input
@@ -56,21 +54,19 @@ export function AddNewActionView({ onCancel }: Props): VNode {
<Button variant="contained" color="secondary" onClick={onCancel}>
<i18n.Translate>Cancel</i18n.Translate>
</Button>
- {uriType !== TalerUriType.Unknown && (
+ {uri && (
<Button
variant="contained"
color="success"
onClick={redirectToWallet}
>
{(() => {
- switch (uriType) {
- case TalerUriType.TalerPay:
+ switch (uri.type) {
+ case TalerUriAction.Pay:
return <i18n.Translate>Open pay page</i18n.Translate>;
- case TalerUriType.TalerRefund:
+ case TalerUriAction.Refund:
return <i18n.Translate>Open refund page</i18n.Translate>;
- case TalerUriType.TalerTip:
- return <i18n.Translate>Open tip page</i18n.Translate>;
- case TalerUriType.TalerWithdraw:
+ case TalerUriAction.Withdraw:
return <i18n.Translate>Open withdraw page</i18n.Translate>;
}
return <Fragment />;
diff --git a/packages/taler-wallet-webextension/src/wallet/Application.tsx b/packages/taler-wallet-webextension/src/wallet/Application.tsx
index d150ebfaf..884c2eab7 100644
--- a/packages/taler-wallet-webextension/src/wallet/Application.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Application.tsx
@@ -20,354 +20,560 @@
* @author sebasjm
*/
+import {
+ Amounts,
+ TalerUri,
+ TalerUriAction,
+ TranslatedString,
+ parseTalerUri,
+ stringifyTalerUri,
+} from "@gnu-taler/taler-util";
+import {
+ TranslationProvider,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
import { createHashHistory } from "history";
-import { Fragment, h, VNode } from "preact";
-import Router, { route, Route } from "preact-router";
-import Match from "preact-router/match";
-import { useEffect, useState } from "preact/hooks";
+import { ComponentChildren, Fragment, VNode, h } from "preact";
+import { Route, Router, route } from "preact-router";
+import { useEffect } from "preact/hooks";
+import {
+ Pages,
+ WalletNavBar,
+ WalletNavBarOptions,
+ getPathnameForTalerURI,
+} from "../NavigationBar.js";
+import { AlertView, CurrentAlerts } from "../components/CurrentAlerts.js";
import { LogoHeader } from "../components/LogoHeader.js";
import PendingTransactions from "../components/PendingTransactions.js";
-import { SuccessBox, WalletBox } from "../components/styled/index.js";
-import { DevContextProvider } from "../context/devContext.js";
-import { IoCProviderForRuntime } from "../context/iocContext.js";
import {
- TranslationProvider,
- useTranslationContext,
-} from "../context/translation.js";
+ LinkPrimary,
+ RedBanner,
+ SubTitle,
+ WalletAction,
+ WalletBox,
+} from "../components/styled/index.js";
+import { AlertProvider } from "../context/alert.js";
+import { IoCProviderForRuntime } from "../context/iocContext.js";
+import { DepositPage as DepositPageCTA } from "../cta/Deposit/index.js";
+import { InvoiceCreatePage } from "../cta/InvoiceCreate/index.js";
+import { InvoicePayPage } from "../cta/InvoicePay/index.js";
import { PaymentPage } from "../cta/Payment/index.js";
+import { PaymentTemplatePage } from "../cta/PaymentTemplate/index.js";
+import { RecoveryPage } from "../cta/Recovery/index.js";
import { RefundPage } from "../cta/Refund/index.js";
-import { TipPage } from "../cta/Tip/index.js";
+import { TransferCreatePage } from "../cta/TransferCreate/index.js";
+import { TransferPickupPage } from "../cta/TransferPickup/index.js";
import {
WithdrawPageFromParams,
WithdrawPageFromURI,
} from "../cta/Withdraw/index.js";
-import { DepositPage as DepositPageCTA } from "../cta/Deposit/index.js";
-import { Pages, WalletNavBar } from "../NavigationBar.js";
-import { DeveloperPage } from "./DeveloperPage.js";
+import { useIsOnline } from "../hooks/useIsOnline.js";
+import { strings } from "../i18n/strings.js";
+import CloseIcon from "../svg/close_24px.inline.svg";
+import { AddBackupProviderPage } from "./AddBackupProvider/index.js";
+import { AddExchange } from "./AddExchange/index.js";
import { BackupPage } from "./BackupPage.js";
import { DepositPage } from "./DepositPage/index.js";
-import { ExchangeAddPage } from "./ExchangeAddPage.js";
+import { DestinationSelectionPage } from "./DestinationSelection/index.js";
+import { DeveloperPage } from "./DeveloperPage.js";
import { HistoryPage } from "./History.js";
+import { NotificationsPage } from "./Notifications/index.js";
import { ProviderDetailPage } from "./ProviderDetailPage.js";
+import { QrReaderPage } from "./QrReader.js";
import { SettingsPage } from "./Settings.js";
import { TransactionPage } from "./Transaction.js";
import { WelcomePage } from "./Welcome.js";
-import { QrReaderPage } from "./QrReader.js";
-import { platform } from "../platform/api.js";
-import { DestinationSelectionPage } from "./DestinationSelection/index.js";
-import { ExchangeSelectionPage } from "./ExchangeSelection/index.js";
-import { TransferCreatePage } from "../cta/TransferCreate/index.js";
-import { InvoiceCreatePage } from "../cta/InvoiceCreate/index.js";
-import { TransferPickupPage } from "../cta/TransferPickup/index.js";
-import { InvoicePayPage } from "../cta/InvoicePay/index.js";
-import { RecoveryPage } from "../cta/Recovery/index.js";
-import { AddBackupProviderPage } from "./AddBackupProvider/index.js";
-import { NotificationsPage } from "./Notifications/index.js";
+import { WalletActivity } from "../components/WalletActivity.js";
+import { EnabledBySettings } from "../components/EnabledBySettings.js";
+import { DevExperimentPage } from "../cta/DevExperiment/index.js";
+import { ConfirmAddExchangeView } from "./AddExchange/views.js";
export function Application(): VNode {
- const [globalNotification, setGlobalNotification] = useState<
- VNode | undefined
- >(undefined);
+ const { i18n } = useTranslationContext();
const hash_history = createHashHistory();
- function clearNotification(): void {
- setGlobalNotification(undefined);
+
+ async function redirectToTxInfo(tid: string): Promise<void> {
+ redirectTo(Pages.balanceTransaction({ tid }));
}
- function clearNotificationWhenMovingOut(): void {
- // const movingOutFromNotification =
- // globalNotification && e.url !== globalNotification.to;
- if (globalNotification) {
- //&& movingOutFromNotification) {
- setGlobalNotification(undefined);
- }
+ function redirectToURL(str: string): void {
+ window.location.href = new URL(str).href
}
- const { i18n } = useTranslationContext();
return (
- <TranslationProvider>
- <DevContextProvider>
- <IoCProviderForRuntime>
- {/* <Match/> won't work in the first render if <Router /> is not called first */}
- {/* https://github.com/preactjs/preact-router/issues/415 */}
- <Router history={hash_history}>
- <Match default>
- {({ path }: { path: string }) => {
- if (path && path.startsWith("/cta")) return;
- return (
- <Fragment>
- <LogoHeader />
- <WalletNavBar path={path} />
- {shouldShowPendingOperations(path) && (
- <div
- style={{
- backgroundColor: "lightcyan",
- display: "flex",
- justifyContent: "center",
- }}
- >
- <PendingTransactions
- goToTransaction={(tid: string) =>
- redirectTo(Pages.balanceTransaction({ tid }))
- }
- />
- </div>
- )}
- </Fragment>
- );
- }}
- </Match>
- </Router>
- <WalletBox>
- {globalNotification && (
- <SuccessBox onClick={clearNotification}>
- <div>{globalNotification}</div>
- </SuccessBox>
+ <TranslationProvider source={strings}>
+ <IoCProviderForRuntime>
+ <Router history={hash_history}>
+ <Route
+ path={Pages.welcome}
+ component={() => (
+ <WalletTemplate goToURL={redirectToURL}>
+ <WelcomePage />
+ </WalletTemplate>
)}
- <Router
- history={hash_history}
- onChange={clearNotificationWhenMovingOut}
- >
- <Route path={Pages.welcome} component={WelcomePage} />
-
- {/**
- * BALANCE
- */}
+ />
- <Route
- path={Pages.balanceHistory.pattern}
- component={HistoryPage}
- goToWalletDeposit={(currency: string) =>
- redirectTo(Pages.sendCash({ amount: `${currency}:0` }))
- }
- goToWalletManualWithdraw={(currency?: string) =>
- redirectTo(
- Pages.receiveCash({
- amount: !currency ? undefined : `${currency}:0`,
- }),
- )
- }
- />
- <Route path={Pages.exchanges} component={ExchangeSelectionPage} />
- <Route
- path={Pages.sendCash.pattern}
- type="send"
- component={DestinationSelectionPage}
- goToWalletBankDeposit={(amount: string) =>
- redirectTo(Pages.balanceDeposit({ amount }))
- }
- goToWalletWalletSend={(amount: string) =>
- redirectTo(Pages.ctaTransferCreate({ amount }))
- }
- />
- <Route
- path={Pages.receiveCash.pattern}
- type="get"
- component={DestinationSelectionPage}
- goToWalletManualWithdraw={(amount?: string) =>
- redirectTo(Pages.ctaWithdrawManual({ amount }))
- }
- goToWalletWalletInvoice={(amount?: string) =>
- redirectTo(Pages.ctaInvoiceCreate({ amount }))
- }
- />
-
- <Route
- path={Pages.balanceTransaction.pattern}
- component={TransactionPage}
- goToWalletHistory={(currency?: string) =>
- redirectTo(Pages.balanceHistory({ currency }))
- }
- />
-
- <Route
- path={Pages.balanceDeposit.pattern}
- component={DepositPage}
- onCancel={(currency: string) => {
- redirectTo(Pages.balanceHistory({ currency }));
- }}
- onSuccess={(currency: string) => {
- redirectTo(Pages.balanceHistory({ currency }));
- setGlobalNotification(
- <i18n.Translate>
- All done, your transaction is in progress
- </i18n.Translate>,
- );
- }}
- />
- {/**
- * PENDING
- */}
- <Route
- path={Pages.qr}
- component={QrReaderPage}
- onDetected={(talerActionUrl: string) => {
- platform.openWalletURIFromPopup(talerActionUrl);
- }}
- />
+ <Route
+ path={Pages.qr}
+ component={() => (
+ <WalletTemplate goToTransaction={redirectToTxInfo} goToURL={redirectToURL}>
+ <QrReaderPage
+ onDetected={(talerActionUrl: TalerUri) => {
+ redirectTo(
+ Pages.defaultCta({
+ uri: stringifyTalerUri(talerActionUrl),
+ }),
+ );
+ }}
+ />
+ </WalletTemplate>
+ )}
+ />
- <Route path={Pages.settings} component={SettingsPage} />
- <Route path={Pages.notifications} component={NotificationsPage} />
+ <Route
+ path={Pages.settings}
+ component={() => (
+ <WalletTemplate goToTransaction={redirectToTxInfo} goToURL={redirectToURL}>
+ <SettingsPage />
+ </WalletTemplate>
+ )}
+ />
+ <Route
+ path={Pages.notifications}
+ component={() => (
+ <WalletTemplate goToURL={redirectToURL}>
+ <NotificationsPage />
+ </WalletTemplate>
+ )}
+ />
+ {/**
+ * SETTINGS
+ */}
+ <Route
+ path={Pages.settingsExchangeAdd.pattern}
+ component={() => (
+ <WalletTemplate goToURL={redirectToURL}>
+ <AddExchange onBack={() => redirectTo(Pages.balance)} />
+ </WalletTemplate>
+ )}
+ />
- {/**
- * BACKUP
- */}
- <Route
- path={Pages.backup}
- component={BackupPage}
- onAddProvider={() => redirectTo(Pages.backupProviderAdd)}
- />
- <Route
- path={Pages.backupProviderDetail.pattern}
- component={ProviderDetailPage}
- onPayProvider={(uri: string) =>
- redirectTo(`${Pages.ctaPay}?talerPayUri=${uri}`)
- }
- onWithdraw={(amount: string) =>
- redirectTo(Pages.receiveCash({ amount }))
- }
- onBack={() => redirectTo(Pages.backup)}
- />
- <Route
- path={Pages.backupProviderAdd}
- component={AddBackupProviderPage}
- onPaymentRequired={(uri: string) =>
- redirectTo(`${Pages.ctaPay}?talerPayUri=${uri}`)
- }
- onComplete={(pid: string) =>
- redirectTo(Pages.backupProviderDetail({ pid }))
- }
- onBack={() => redirectTo(Pages.backup)}
- />
+<Route
+ path={Pages.balanceHistory.pattern}
+ component={({ currency }: { currency?: string }) => (
+ <WalletTemplate path="balance" goToTransaction={redirectToTxInfo} goToURL={redirectToURL}>
+ <HistoryPage
+ currency={currency}
+ goToWalletDeposit={(currency: string) =>
+ redirectTo(Pages.sendCash({ amount: `${currency}:0` }))
+ }
+ goToWalletManualWithdraw={(currency?: string) =>
+ redirectTo(
+ Pages.receiveCash({
+ amount: !currency ? undefined : `${currency}:0`,
+ }),
+ )
+ }
+ />
+ </WalletTemplate>
+ )}
+ />
+ <Route
+ path={Pages.searchHistory.pattern}
+ component={({ currency }: { currency?: string }) => (
+ <WalletTemplate path="balance" goToTransaction={redirectToTxInfo} goToURL={redirectToURL}>
+ <HistoryPage
+ currency={currency}
+ search
+ goToWalletDeposit={(currency: string) =>
+ redirectTo(Pages.sendCash({ amount: `${currency}:0` }))
+ }
+ goToWalletManualWithdraw={(currency?: string) =>
+ redirectTo(
+ Pages.receiveCash({
+ amount: !currency ? undefined : `${currency}:0`,
+ }),
+ )
+ }
+ />
+ </WalletTemplate>
+ )}
+ />
+ <Route
+ path={Pages.sendCash.pattern}
+ component={({ amount }: { amount?: string }) => (
+ <WalletTemplate path="balance" goToURL={redirectToURL}>
+ <DestinationSelectionPage
+ type="send"
+ amount={amount}
+ goToWalletBankDeposit={(amount: string) =>
+ redirectTo(Pages.balanceDeposit({ amount }))
+ }
+ goToWalletWalletSend={(amount: string) =>
+ redirectTo(Pages.ctaTransferCreate({ amount }))
+ }
+ />
+ </WalletTemplate>
+ )}
+ />
+ <Route
+ path={Pages.receiveCash.pattern}
+ component={({ amount }: { amount?: string }) => (
+ <WalletTemplate path="balance" goToURL={redirectToURL}>
+ <DestinationSelectionPage
+ type="get"
+ amount={amount}
+ goToWalletManualWithdraw={(amount?: string) =>
+ redirectTo(Pages.ctaWithdrawManual({ amount }))
+ }
+ goToWalletWalletInvoice={(amount?: string) =>
+ redirectTo(Pages.ctaInvoiceCreate({ amount }))
+ }
+ />
+ </WalletTemplate>
+ )}
+ />
- {/**
- * SETTINGS
- */}
- <Route
- path={Pages.settingsExchangeAdd.pattern}
- component={ExchangeAddPage}
- onBack={() => redirectTo(Pages.balance)}
- />
+ <Route
+ path={Pages.balanceTransaction.pattern}
+ component={({ tid }: { tid: string }) => (
+ <WalletTemplate path="balance" goToURL={redirectToURL}>
+ <TransactionPage
+ tid={tid}
+ goToWalletHistory={(currency?: string) =>
+ redirectTo(Pages.balanceHistory({ currency }))
+ }
+ />
+ </WalletTemplate>
+ )}
+ />
- {/**
- * DEV
- */}
+ <Route
+ path={Pages.balanceDeposit.pattern}
+ component={({ amount }: { amount: string }) => (
+ <WalletTemplate path="balance" goToURL={redirectToURL}>
+ <DepositPage
+ amount={amount}
+ onCancel={(currency: string) => {
+ redirectTo(Pages.balanceHistory({ currency }));
+ }}
+ onSuccess={(currency: string) => {
+ redirectTo(Pages.balanceHistory({ currency }));
+ }}
+ />
+ </WalletTemplate>
+ )}
+ />
- <Route path={Pages.dev} component={DeveloperPage} />
+ <Route
+ path={Pages.backup}
+ component={() => (
+ <WalletTemplate path="backup" goToTransaction={redirectToTxInfo} goToURL={redirectToURL}>
+ <BackupPage
+ onAddProvider={() => redirectTo(Pages.backupProviderAdd)}
+ />
+ </WalletTemplate>
+ )}
+ />
+ <Route
+ path={Pages.backupProviderDetail.pattern}
+ component={({ pid }: { pid: string }) => (
+ <WalletTemplate goToURL={redirectToURL}>
+ <ProviderDetailPage
+ pid={pid}
+ onPayProvider={(uri: string) =>
+ redirectTo(`${Pages.ctaPay}?talerPayUri=${uri}`)
+ }
+ onWithdraw={(amount: string) =>
+ redirectTo(Pages.receiveCash({ amount }))
+ }
+ onBack={() => redirectTo(Pages.backup)}
+ />
+ </WalletTemplate>
+ )}
+ />
+ <Route
+ path={Pages.backupProviderAdd}
+ component={() => (
+ <WalletTemplate goToURL={redirectToURL}>
+ <AddBackupProviderPage
+ onPaymentRequired={(uri: string) =>
+ redirectTo(`${Pages.ctaPay}?talerPayUri=${uri}`)
+ }
+ onComplete={(pid: string) =>
+ redirectTo(Pages.backupProviderDetail({ pid }))
+ }
+ onBack={() => redirectTo(Pages.backup)}
+ />
+ </WalletTemplate>
+ )}
+ />
- {/**
- * CALL TO ACTION
- */}
- <Route
- path={Pages.ctaPay}
- component={PaymentPage}
- goToWalletManualWithdraw={(amount?: string) =>
- redirectTo(Pages.receiveCash({ amount }))
- }
- cancel={() => redirectTo(Pages.balance)}
- onSuccess={(tid: string) =>
- redirectTo(Pages.balanceTransaction({ tid }))
- }
- />
- <Route
- path={Pages.ctaRefund}
- component={RefundPage}
- cancel={() => redirectTo(Pages.balance)}
- onSuccess={(tid: string) =>
- redirectTo(Pages.balanceTransaction({ tid }))
- }
- />
- <Route
- path={Pages.ctaTips}
- component={TipPage}
- onCancel={() => redirectTo(Pages.balance)}
- onSuccess={(tid: string) =>
- redirectTo(Pages.balanceTransaction({ tid }))
- }
- />
- <Route
- path={Pages.ctaWithdraw}
- component={WithdrawPageFromURI}
- cancel={() => redirectTo(Pages.balance)}
- onSuccess={(tid: string) =>
- redirectTo(Pages.balanceTransaction({ tid }))
- }
- />
- <Route
- path={Pages.ctaWithdrawManual.pattern}
- component={WithdrawPageFromParams}
- cancel={() => redirectTo(Pages.balance)}
- onSuccess={(tid: string) =>
- redirectTo(Pages.balanceTransaction({ tid }))
- }
- />
- <Route
- path={Pages.ctaDeposit}
- component={DepositPageCTA}
- cancel={() => redirectTo(Pages.balance)}
- onSuccess={(tid: string) =>
- redirectTo(Pages.balanceTransaction({ tid }))
- }
- />
- <Route
- path={Pages.ctaInvoiceCreate.pattern}
- component={InvoiceCreatePage}
- onClose={() => redirectTo(Pages.balance)}
- onSuccess={(tid: string) =>
- redirectTo(Pages.balanceTransaction({ tid }))
- }
- />
- <Route
- path={Pages.ctaTransferCreate.pattern}
- component={TransferCreatePage}
- onClose={() => redirectTo(Pages.balance)}
- onSuccess={(tid: string) =>
- redirectTo(Pages.balanceTransaction({ tid }))
- }
- />
- <Route
- path={Pages.ctaInvoicePay}
- component={InvoicePayPage}
- goToWalletManualWithdraw={(amount?: string) =>
- redirectTo(Pages.receiveCash({ amount }))
- }
- onClose={() => redirectTo(Pages.balance)}
- onSuccess={(tid: string) =>
- redirectTo(Pages.balanceTransaction({ tid }))
- }
- />
- <Route
- path={Pages.ctaTransferPickup}
- component={TransferPickupPage}
- onClose={() => redirectTo(Pages.balance)}
- onSuccess={(tid: string) =>
- redirectTo(Pages.balanceTransaction({ tid }))
- }
- />
- <Route
- path={Pages.ctaRecovery}
- component={RecoveryPage}
- onCancel={() => redirectTo(Pages.balance)}
- onSuccess={() => redirectTo(Pages.backup)}
- />
+ {/**
+ * DEV
+ */}
+ <Route
+ path={Pages.dev}
+ component={() => (
+ <WalletTemplate path="dev" goToTransaction={redirectToTxInfo} goToURL={redirectToURL}>
+ <DeveloperPage />
+ </WalletTemplate>
+ )}
+ />
- {/**
- * NOT FOUND
- * all redirects should be at the end
- */}
- <Route
- path={Pages.balance}
- component={Redirect}
- to={Pages.balanceHistory({})}
- />
+ {/**
+ * CALL TO ACTION
+ */}
+ <Route
+ path={Pages.defaultCta.pattern}
+ component={({ uri }: { uri: string }) => {
+ const path = getPathnameForTalerURI(uri);
+ if (!path) {
+ return (
+ <CallToActionTemplate title={i18n.str`Taler URI handler`}>
+ <AlertView
+ alert={{
+ type: "warning",
+ message: i18n.str`Could not found a handler for the Taler URI`,
+ description: i18n.str`The uri read in the path parameter is not valid: "${uri}"`,
+ }}
+ />
+ </CallToActionTemplate>
+ );
+ }
+ return <Redirect to={path} />;
+ }}
+ />
+ <Route
+ path={Pages.ctaPay}
+ component={({ talerUri }: { talerUri: string }) => (
+ <CallToActionTemplate title={i18n.str`Digital cash payment`}>
+ <PaymentPage
+ talerPayUri={decodeURIComponent(talerUri)}
+ goToWalletManualWithdraw={(amount?: string) =>
+ redirectTo(Pages.receiveCash({ amount }))
+ }
+ cancel={() => redirectTo(Pages.balance)}
+ onSuccess={(tid: string) =>
+ redirectTo(Pages.balanceTransaction({ tid }))
+ }
+ />
+ </CallToActionTemplate>
+ )}
+ />
+ <Route
+ path={Pages.ctaPayTemplate}
+ component={({ talerUri }: { talerUri: string }) => (
+ <CallToActionTemplate title={i18n.str`Digital cash payment`}>
+ <PaymentTemplatePage
+ talerTemplateUri={decodeURIComponent(talerUri)}
+ goToWalletManualWithdraw={(amount?: string) =>
+ redirectTo(Pages.receiveCash({ amount }))
+ }
+ cancel={() => redirectTo(Pages.balance)}
+ onSuccess={(tid: string) =>
+ redirectTo(Pages.balanceTransaction({ tid }))
+ }
+ />
+ </CallToActionTemplate>
+ )}
+ />
+ <Route
+ path={Pages.ctaRefund}
+ component={({ talerUri }: { talerUri: string }) => (
+ <CallToActionTemplate title={i18n.str`Digital cash refund`}>
+ <RefundPage
+ talerRefundUri={decodeURIComponent(talerUri)}
+ cancel={() => redirectTo(Pages.balance)}
+ onSuccess={(tid: string) =>
+ redirectTo(Pages.balanceTransaction({ tid }))
+ }
+ />
+ </CallToActionTemplate>
+ )}
+ />
+ <Route
+ path={Pages.ctaWithdraw}
+ component={({ talerUri }: { talerUri: string }) => (
+ <CallToActionTemplate title={i18n.str`Digital cash withdrawal`}>
+ <WithdrawPageFromURI
+ talerWithdrawUri={decodeURIComponent(talerUri)}
+ cancel={() => redirectTo(Pages.balance)}
+ onSuccess={(tid: string) =>
+ redirectTo(Pages.balanceTransaction({ tid }))
+ }
+ />
+ </CallToActionTemplate>
+ )}
+ />
+ <Route
+ path={Pages.ctaWithdrawManual.pattern}
+ component={({
+ amount,
+ talerUri,
+ }: {
+ amount: string;
+ talerUri: string;
+ }) => (
+ <CallToActionTemplate title={i18n.str`Digital cash withdrawal`}>
+ <WithdrawPageFromParams
+ onAmountChanged={async (newamount) => {
+ const page = `${Pages.ctaWithdrawManual({ amount: newamount })}?talerUri=${encodeURIComponent(talerUri)}`;
+ redirectTo(page);
+ }}
+ talerExchangeWithdrawUri={talerUri}
+ amount={amount}
+ cancel={() => redirectTo(Pages.balance)}
+ onSuccess={(tid: string) =>
+ redirectTo(Pages.balanceTransaction({ tid }))
+ }
+ />
+ </CallToActionTemplate>
+ )}
+ />
+ <Route
+ path={Pages.ctaDeposit}
+ component={({
+ amount,
+ talerUri,
+ }: {
+ amount: string;
+ talerUri: string;
+ }) => (
+ <CallToActionTemplate title={i18n.str`Digital cash deposit`}>
+ <DepositPageCTA
+ amountStr={Amounts.stringify(Amounts.parseOrThrow(amount))}
+ talerDepositUri={decodeURIComponent(talerUri)}
+ cancel={() => redirectTo(Pages.balance)}
+ onSuccess={(tid: string) =>
+ redirectTo(Pages.balanceTransaction({ tid }))
+ }
+ />
+ </CallToActionTemplate>
+ )}
+ />
+ <Route
+ path={Pages.ctaInvoiceCreate.pattern}
+ component={({ amount }: { amount: string }) => (
+ <CallToActionTemplate title={i18n.str`Digital cash invoice`}>
+ <InvoiceCreatePage
+ amount={Amounts.stringify(Amounts.parseOrThrow(amount))}
+ onClose={() => redirectTo(Pages.balance)}
+ onSuccess={(tid: string) =>
+ redirectTo(Pages.balanceTransaction({ tid }))
+ }
+ />
+ </CallToActionTemplate>
+ )}
+ />
+ <Route
+ path={Pages.ctaTransferCreate.pattern}
+ component={({ amount }: { amount: string }) => (
+ <CallToActionTemplate title={i18n.str`Digital cash transfer`}>
+ <TransferCreatePage
+ amount={Amounts.stringify(Amounts.parseOrThrow(amount))}
+ onClose={() => redirectTo(Pages.balance)}
+ onSuccess={(tid: string) =>
+ redirectTo(Pages.balanceTransaction({ tid }))
+ }
+ />
+ </CallToActionTemplate>
+ )}
+ />
+ <Route
+ path={Pages.ctaInvoicePay}
+ component={({ talerUri }: { talerUri: string }) => (
+ <CallToActionTemplate title={i18n.str`Digital cash invoice`}>
+ <InvoicePayPage
+ talerPayPullUri={decodeURIComponent(talerUri)}
+ goToWalletManualWithdraw={(amount?: string) =>
+ redirectTo(Pages.receiveCash({ amount }))
+ }
+ onClose={() => redirectTo(Pages.balance)}
+ onSuccess={(tid: string) =>
+ redirectTo(Pages.balanceTransaction({ tid }))
+ }
+ />
+ </CallToActionTemplate>
+ )}
+ />
+ <Route
+ path={Pages.ctaTransferPickup}
+ component={({ talerUri }: { talerUri: string }) => (
+ <CallToActionTemplate title={i18n.str`Digital cash transfer`}>
+ <TransferPickupPage
+ talerPayPushUri={decodeURIComponent(talerUri)}
+ onClose={() => redirectTo(Pages.balance)}
+ onSuccess={(tid: string) =>
+ redirectTo(Pages.balanceTransaction({ tid }))
+ }
+ />
+ </CallToActionTemplate>
+ )}
+ />
+ <Route
+ path={Pages.ctaRecovery}
+ component={({ talerRecoveryUri }: { talerRecoveryUri: string }) => (
+ <CallToActionTemplate title={i18n.str`Digital cash recovery`}>
+ <RecoveryPage
+ talerRecoveryUri={decodeURIComponent(talerRecoveryUri)}
+ onCancel={() => redirectTo(Pages.balance)}
+ onSuccess={() => redirectTo(Pages.backup)}
+ />
+ </CallToActionTemplate>
+ )}
+ />
+ <Route
+ path={Pages.ctaExperiment}
+ component={({ talerUri }: { talerUri: string }) => (
+ <CallToActionTemplate title={i18n.str`Development experiment`}>
+ <DevExperimentPage
+ talerExperimentUri={decodeURIComponent(talerUri)}
+ onCancel={() => redirectTo(Pages.balanceHistory({}))}
+ onSuccess={() => redirectTo(Pages.balanceHistory({}))}
+ />
+ </CallToActionTemplate>
+ )}
+ />
+ <Route
+ path={Pages.ctaAddExchange}
+ component={({ talerUri }: { talerUri: string }) => {
+ const tUri = parseTalerUri(decodeURIComponent(talerUri))
+ const baseUrl = tUri?.type === TalerUriAction.AddExchange ? tUri.exchangeBaseUrl : undefined
+ if (!baseUrl) {
+ redirectTo(Pages.balanceHistory({}))
+ return <div>
+ invalid url {talerUri}
+ </div>
+ }
+ return <CallToActionTemplate title={i18n.str`Add exchange`}>
+ <ConfirmAddExchangeView
+ url={baseUrl}
+ status="confirm"
+ error={undefined}
+ onCancel={() => redirectTo(Pages.balanceHistory({}))}
+ onConfirm={() => redirectTo(Pages.balanceHistory({}))}
+ />
+ </CallToActionTemplate>
+ }}
+ />
+ {/**
+ * NOT FOUND
+ * all redirects should be at the end
+ */}
+ <Route
+ path={Pages.balance}
+ component={() => <Redirect to={Pages.balanceHistory({})} />}
+ />
- <Route
- default
- component={Redirect}
- to={Pages.balanceHistory({})}
- />
- </Router>
- </WalletBox>
- </IoCProviderForRuntime>
- </DevContextProvider>
+ <Route
+ default
+ component={() => <Redirect to={Pages.balanceHistory({})} />}
+ />
+ </Router>
+ <EnabledBySettings name="showWalletActivity">
+ <WalletActivity />
+ </EnabledBySettings>
+ </IoCProviderForRuntime>
</TranslationProvider>
);
}
@@ -383,23 +589,89 @@ function Redirect({ to }: { to: string }): null {
return null;
}
-function matchesRoute(url: string, route: string): boolean {
- type MatcherFunc = (
- url: string,
- route: string,
- opts: any,
- ) => Record<string, string> | false;
+// function matchesRoute(url: string, route: string): boolean {
+// type MatcherFunc = (
+// url: string,
+// route: string,
+// opts: any,
+// ) => Record<string, string> | false;
+
+// const internalPreactMatcher: MatcherFunc = (Router as any).exec;
+// const result = internalPreactMatcher(url, route, {});
+// return !result ? false : true;
+// }
- const internalPreactMatcher: MatcherFunc = (Router as any).exec;
- const result = internalPreactMatcher(url, route, {});
- return !result ? false : true;
+function CallToActionTemplate({
+ title,
+ children,
+}: {
+ title: TranslatedString;
+ children: ComponentChildren;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <WalletAction>
+ <LogoHeader />
+ <section style={{ display: "flex", justifyContent: "right", margin: 0 }}>
+ <LinkPrimary href={Pages.balance}>
+ <div
+ style={{
+ height: 24,
+ width: 24,
+ marginLeft: 4,
+ marginRight: 4,
+ border: "1px solid black",
+ borderRadius: 12,
+ }}
+ dangerouslySetInnerHTML={{ __html: CloseIcon }}
+ />
+ </LinkPrimary>
+ </section>
+ <SubTitle>{title}</SubTitle>
+ <AlertProvider>
+ <CurrentAlerts />
+ {children}
+ </AlertProvider>
+ <section style={{ display: "flex", justifyContent: "right" }}>
+ <LinkPrimary href={Pages.balance}>
+ <i18n.Translate>Return to wallet</i18n.Translate>
+ </LinkPrimary>
+ </section>
+ </WalletAction>
+ );
}
-function shouldShowPendingOperations(url: string): boolean {
- return [
- Pages.balanceHistory.pattern,
- Pages.dev,
- Pages.settings,
- Pages.backup,
- ].some((p) => matchesRoute(url, p));
+function WalletTemplate({
+ path,
+ children,
+ goToTransaction,
+ goToURL,
+}: {
+ path?: WalletNavBarOptions;
+ children: ComponentChildren;
+ goToTransaction?: (id: string) => Promise<void>;
+ goToURL: (url: string) => void;
+}): VNode {
+ const online = useIsOnline();
+ const { i18n } = useTranslationContext();
+ return (
+ <Fragment>
+ {!online && (
+ <div style={{ display: "flex", justifyContent: "center" }}>
+ <RedBanner>{i18n.str`Network is offline`}</RedBanner>
+ </div>
+ )}
+ <LogoHeader />
+ <WalletNavBar path={path} />
+ <PendingTransactions
+ goToTransaction={goToTransaction}
+ goToURL={goToURL} />
+ <WalletBox>
+ <AlertProvider>
+ <CurrentAlerts />
+ {children}
+ </AlertProvider>
+ </WalletBox>
+ </Fragment>
+ );
}
diff --git a/packages/taler-wallet-webextension/src/wallet/Backup.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Backup.stories.tsx
index c3a1ea5d6..cc7c9af67 100644
--- a/packages/taler-wallet-webextension/src/wallet/Backup.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Backup.stories.tsx
@@ -19,38 +19,40 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { ProviderPaymentType } from "@gnu-taler/taler-wallet-core";
+import {
+ AbsoluteTime,
+ AmountString,
+ ProviderPaymentType,
+ TalerPreciseTimestamp,
+} from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
import { addDays } from "date-fns";
import {
- BackupView as TestedComponent,
ShowRecoveryInfo,
+ BackupView as TestedComponent,
} from "./BackupPage.js";
-import { createExample } from "../test-utils.js";
-import { TalerProtocolTimestamp } from "@gnu-taler/taler-util";
export default {
title: "backup",
};
-export const LotOfProviders = createExample(TestedComponent, {
+export const LotOfProviders = tests.createExample(TestedComponent, {
providers: [
{
active: true,
name: "sync.demo",
syncProviderBaseUrl: "http://sync.taler:9967/",
lastSuccessfulBackupTimestamp:
- TalerProtocolTimestamp.fromSeconds(1625063925),
+ TalerPreciseTimestamp.fromSeconds(1625063925),
paymentProposalIds: [
"43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG",
],
paymentStatus: {
type: ProviderPaymentType.Paid,
- paidUntil: {
- t_ms: 1656599921000,
- },
+ paidUntil: AbsoluteTime.fromMilliseconds(1656599921000),
},
terms: {
- annualFee: "ARS:1",
+ annualFee: "ARS:1" as AmountString,
storageLimitInMegabytes: 16,
supportedProtocolVersion: "0.0",
},
@@ -60,18 +62,18 @@ export const LotOfProviders = createExample(TestedComponent, {
name: "sync.demo",
syncProviderBaseUrl: "http://sync.taler:9967/",
lastSuccessfulBackupTimestamp:
- TalerProtocolTimestamp.fromSeconds(1625063925),
+ TalerPreciseTimestamp.fromSeconds(1625063925),
paymentProposalIds: [
"43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG",
],
paymentStatus: {
type: ProviderPaymentType.Paid,
- paidUntil: {
- t_ms: addDays(new Date(), 13).getTime(),
- },
+ paidUntil: AbsoluteTime.fromMilliseconds(
+ addDays(new Date(), 13).getTime(),
+ ),
},
terms: {
- annualFee: "ARS:1",
+ annualFee: "ARS:1" as AmountString,
storageLimitInMegabytes: 16,
supportedProtocolVersion: "0.0",
},
@@ -86,7 +88,7 @@ export const LotOfProviders = createExample(TestedComponent, {
talerUri: "taler://",
},
terms: {
- annualFee: "KUDOS:0.1",
+ annualFee: "KUDOS:0.1" as AmountString,
storageLimitInMegabytes: 16,
supportedProtocolVersion: "0.0",
},
@@ -98,10 +100,10 @@ export const LotOfProviders = createExample(TestedComponent, {
paymentProposalIds: [],
paymentStatus: {
type: ProviderPaymentType.InsufficientBalance,
- amount: "KUDOS:10",
+ amount: "KUDOS:10" as AmountString,
},
terms: {
- annualFee: "KUDOS:0.1",
+ annualFee: "KUDOS:0.1" as AmountString,
storageLimitInMegabytes: 16,
supportedProtocolVersion: "0.0",
},
@@ -114,21 +116,19 @@ export const LotOfProviders = createExample(TestedComponent, {
paymentStatus: {
type: ProviderPaymentType.TermsChanged,
newTerms: {
- annualFee: "USD:2",
+ annualFee: "USD:2" as AmountString,
storageLimitInMegabytes: 8,
supportedProtocolVersion: "2",
},
oldTerms: {
- annualFee: "USD:1",
+ annualFee: "USD:1" as AmountString,
storageLimitInMegabytes: 16,
supportedProtocolVersion: "1",
},
- paidUntil: {
- t_ms: "never",
- },
+ paidUntil: AbsoluteTime.never(),
},
terms: {
- annualFee: "KUDOS:0.1",
+ annualFee: "KUDOS:0.1" as AmountString,
storageLimitInMegabytes: 16,
supportedProtocolVersion: "0.0",
},
@@ -142,7 +142,7 @@ export const LotOfProviders = createExample(TestedComponent, {
type: ProviderPaymentType.Unpaid,
},
terms: {
- annualFee: "KUDOS:0.1",
+ annualFee: "KUDOS:0.1" as AmountString,
storageLimitInMegabytes: 16,
supportedProtocolVersion: "0.0",
},
@@ -156,7 +156,7 @@ export const LotOfProviders = createExample(TestedComponent, {
type: ProviderPaymentType.Unpaid,
},
terms: {
- annualFee: "KUDOS:0.1",
+ annualFee: "KUDOS:0.1" as AmountString,
storageLimitInMegabytes: 16,
supportedProtocolVersion: "0.0",
},
@@ -164,25 +164,23 @@ export const LotOfProviders = createExample(TestedComponent, {
],
});
-export const OneProvider = createExample(TestedComponent, {
+export const OneProvider = tests.createExample(TestedComponent, {
providers: [
{
active: true,
name: "sync.demo",
syncProviderBaseUrl: "http://sync.taler:9967/",
lastSuccessfulBackupTimestamp:
- TalerProtocolTimestamp.fromSeconds(1625063925),
+ TalerPreciseTimestamp.fromSeconds(1625063925),
paymentProposalIds: [
"43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG",
],
paymentStatus: {
type: ProviderPaymentType.Paid,
- paidUntil: {
- t_ms: 1656599921000,
- },
+ paidUntil: AbsoluteTime.fromMilliseconds(1656599921000),
},
terms: {
- annualFee: "ARS:1",
+ annualFee: "ARS:1" as AmountString,
storageLimitInMegabytes: 16,
supportedProtocolVersion: "0.0",
},
@@ -190,10 +188,10 @@ export const OneProvider = createExample(TestedComponent, {
],
});
-export const Empty = createExample(TestedComponent, {
+export const Empty = tests.createExample(TestedComponent, {
providers: [],
});
-export const Recovery = createExample(ShowRecoveryInfo, {
+export const Recovery = tests.createExample(ShowRecoveryInfo, {
info: "taler://recovery/ASLDKJASLKDJASD",
});
diff --git a/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx b/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx
index c9dbfb64d..8a3710f69 100644
--- a/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx
@@ -14,23 +14,26 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { AbsoluteTime, constructRecoveryUri } from "@gnu-taler/taler-util";
import {
+ AbsoluteTime,
ProviderInfo,
ProviderPaymentPaid,
ProviderPaymentStatus,
ProviderPaymentType,
- WalletApiOperation,
-} from "@gnu-taler/taler-wallet-core";
+ stringifyRestoreUri,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import {
differenceInMonths,
formatDuration,
intervalToDuration,
} from "date-fns";
-import { Fragment, h, VNode } from "preact";
+import { Fragment, VNode, h } from "preact";
import { useEffect, useState } from "preact/hooks";
+import { Pages } from "../NavigationBar.js";
+import { ErrorAlertView } from "../components/CurrentAlerts.js";
import { Loading } from "../components/Loading.js";
-import { LoadingError } from "../components/LoadingError.js";
import { QR } from "../components/QR.js";
import {
BoldLight,
@@ -42,11 +45,10 @@ import {
SmallText,
WarningBox,
} from "../components/styled/index.js";
-import { useTranslationContext } from "../context/translation.js";
+import { alertFromError } from "../context/alert.js";
+import { useBackendContext } from "../context/backend.js";
import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
import { Button } from "../mui/Button.js";
-import { Pages } from "../NavigationBar.js";
-import { wxApi } from "../wxApi.js";
interface Props {
onAddProvider: () => Promise<void>;
@@ -107,8 +109,9 @@ export function ShowRecoveryInfo({
export function BackupPage({ onAddProvider }: Props): VNode {
const { i18n } = useTranslationContext();
+ const api = useBackendContext();
const status = useAsyncAsHook(() =>
- wxApi.wallet.call(WalletApiOperation.GetBackupInfo, {}),
+ api.wallet.call(WalletApiOperation.GetBackupInfo, {}),
);
const [recoveryInfo, setRecoveryInfo] = useState<string>("");
if (!status) {
@@ -116,19 +119,25 @@ export function BackupPage({ onAddProvider }: Props): VNode {
}
if (status.hasError) {
return (
- <LoadingError
- title={<i18n.Translate>Could not load backup providers</i18n.Translate>}
- error={status}
+ <ErrorAlertView
+ error={alertFromError(
+ i18n,
+ i18n.str`Could not load backup providers`,
+ status,
+ )}
/>
);
}
async function getRecoveryInfo(): Promise<void> {
- const r = await wxApi.wallet.call(
+ const r = await api.wallet.call(
WalletApiOperation.ExportBackupRecovery,
{},
);
- const str = constructRecoveryUri(r);
+ const str = stringifyRestoreUri({
+ walletRootPriv: r.walletRootPriv,
+ providers: r.providers.map((p) => p.url),
+ });
setRecoveryInfo(str);
}
@@ -158,7 +167,7 @@ export function BackupPage({ onAddProvider }: Props): VNode {
providers={providers}
onAddProvider={onAddProvider}
onSyncAll={async () =>
- wxApi.wallet.call(WalletApiOperation.RunBackupCycle, {}).then()
+ api.wallet.call(WalletApiOperation.RunBackupCycle, {}).then()
}
onShowInfo={getRecoveryInfo}
/>
@@ -188,7 +197,7 @@ export function BackupView({
status={provider.paymentStatus}
timestamp={
provider.lastSuccessfulBackupTimestamp
- ? AbsoluteTime.fromTimestamp(
+ ? AbsoluteTime.fromPreciseTimestamp(
provider.lastSuccessfulBackupTimestamp,
)
: undefined
@@ -218,11 +227,9 @@ export function BackupView({
</div>
<div>
<Button variant="contained" onClick={onSyncAll}>
- {providers.length > 1 ? (
- <i18n.Translate>Sync all backups</i18n.Translate>
- ) : (
- <i18n.Translate>Sync now</i18n.Translate>
- )}
+ {providers.length > 1
+ ? i18n.str`Sync all backups`
+ : i18n.str`Sync now`}
</Button>
<Button variant="contained" color="success" onClick={onAddProvider}>
<i18n.Translate>Add provider</i18n.Translate>
@@ -316,12 +323,12 @@ function daysUntil(d: AbsoluteTime): string {
duration?.years
? "years"
: duration?.months
- ? "months"
- : duration?.days
- ? "days"
- : duration.hours
- ? "hours"
- : "minutes",
+ ? "months"
+ : duration?.days
+ ? "days"
+ : duration.hours
+ ? "hours"
+ : "minutes",
],
});
return `${str}`;
@@ -344,6 +351,6 @@ function getStatusPaidOrder(
return a.paidUntil.t_ms === "never"
? -1
: b.paidUntil.t_ms === "never"
- ? 1
- : a.paidUntil.t_ms - b.paidUntil.t_ms;
+ ? 1
+ : a.paidUntil.t_ms - b.paidUntil.t_ms;
}
diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/index.ts b/packages/taler-wallet-webextension/src/wallet/DepositPage/index.ts
index 3f23515b2..838739ad1 100644
--- a/packages/taler-wallet-webextension/src/wallet/DepositPage/index.ts
+++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/index.ts
@@ -15,21 +15,19 @@
*/
import { AmountJson, PaytoUri } from "@gnu-taler/taler-util";
+import { ErrorAlertView } from "../../components/CurrentAlerts.js";
import { Loading } from "../../components/Loading.js";
-import { HookError } from "../../hooks/useAsyncAsHook.js";
+import { ErrorAlert } from "../../context/alert.js";
import {
AmountFieldHandler,
ButtonHandler,
SelectFieldHandler,
- TextFieldHandler,
} from "../../mui/handlers.js";
import { compose, StateViewMap } from "../../utils/index.js";
-import { wxApi } from "../../wxApi.js";
import { ManageAccountPage } from "../ManageAccount/index.js";
import { useComponentState } from "./state.js";
import {
AmountOrCurrencyErrorView,
- LoadingErrorView,
NoAccountToDepositView,
NoEnoughBalanceView,
ReadyView,
@@ -37,7 +35,6 @@ import {
export interface Props {
amount?: string;
- currency?: string;
onCancel: (currency: string) => void;
onSuccess: (currency: string) => void;
}
@@ -58,8 +55,8 @@ export namespace State {
}
export interface LoadingUriError {
- status: "loading-error";
- error: HookError;
+ status: "error";
+ error: ErrorAlert;
}
export interface AddingAccount {
@@ -109,7 +106,7 @@ export namespace State {
const viewMapping: StateViewMap<State> = {
loading: Loading,
- "loading-error": LoadingErrorView,
+ error: ErrorAlertView,
"amount-or-currency-error": AmountOrCurrencyErrorView,
"no-enough-balance": NoEnoughBalanceView,
"no-accounts": NoAccountToDepositView,
@@ -119,6 +116,6 @@ const viewMapping: StateViewMap<State> = {
export const DepositPage = compose(
"DepositPage",
- (p: Props) => useComponentState(p, wxApi),
+ (p: Props) => useComponentState(p),
viewMapping,
);
diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts b/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts
index bbf2c2771..97b2ab517 100644
--- a/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts
+++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts
@@ -25,16 +25,23 @@ import {
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useState } from "preact/hooks";
+import { alertFromError, useAlertContext } from "../../context/alert.js";
+import { useBackendContext } from "../../context/backend.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
-import { wxApi } from "../../wxApi.js";
+import { RecursiveState } from "../../utils/index.js";
import { Props, State } from "./index.js";
-export function useComponentState(
- { amount: amountStr, currency: currencyStr, onCancel, onSuccess }: Props,
- api: typeof wxApi,
-): State {
+export function useComponentState({
+ amount: amountStr,
+ onCancel,
+ onSuccess,
+}: Props): RecursiveState<State> {
+ const api = useBackendContext();
+ const { i18n } = useTranslationContext();
+ const { pushAlertOnError } = useAlertContext();
const parsed = amountStr === undefined ? undefined : Amounts.parse(amountStr);
- const currency = parsed !== undefined ? parsed.currency : currencyStr;
+ const currency = parsed !== undefined ? parsed.currency : undefined;
const hook = useAsyncAsHook(async () => {
const { balances } = await api.wallet.call(
@@ -43,9 +50,7 @@ export function useComponentState(
);
const { accounts } = await api.wallet.call(
WalletApiOperation.ListKnownBankAccounts,
- {
- currency,
- },
+ { currency },
);
return { accounts, balances };
@@ -55,13 +60,11 @@ export function useComponentState(
parsed !== undefined
? parsed
: currency !== undefined
- ? Amounts.zeroOfCurrency(currency)
- : undefined;
+ ? Amounts.zeroOfCurrency(currency)
+ : undefined;
// const [accountIdx, setAccountIdx] = useState<number>(0);
- const [amount, setAmount] = useState<AmountJson>(initialValue ?? ({} as any));
const [selectedAccount, setSelectedAccount] = useState<PaytoUri>();
- const [fee, setFee] = useState<DepositGroupFees | undefined>(undefined);
const [addingAccount, setAddingAccount] = useState(false);
if (!currency) {
@@ -79,13 +82,19 @@ export function useComponentState(
}
if (hook.hasError) {
return {
- status: "loading-error",
- error: hook,
+ status: "error",
+ error: alertFromError(i18n,
+ i18n.str`Could not load balance information`, hook),
};
}
const { accounts, balances } = hook.response;
- // const parsedAmount = Amounts.parse(`${currency}:${amount}`);
+ async function updateAccountFromList(accountStr: string): Promise<void> {
+ const uri = !accountStr ? undefined : parsePaytoUri(accountStr);
+ if (uri) {
+ setSelectedAccount(uri);
+ }
+ }
if (addingAccount) {
return {
@@ -124,136 +133,123 @@ export function useComponentState(
error: undefined,
currency,
onAddAccount: {
- onClick: async () => {
+ onClick: pushAlertOnError(async () => {
setAddingAccount(true);
- },
+ }),
},
};
}
const firstAccount = accounts[0].uri;
const currentAccount = !selectedAccount ? firstAccount : selectedAccount;
- if (fee === undefined) {
- getFeeForAmount(currentAccount, amount, api).then((initialFee) => {
- setFee(initialFee);
- });
- return {
- status: "loading",
- error: undefined,
- };
- }
+ return () => {
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ const [amount, setAmount] = useState<AmountJson>(
+ initialValue ?? ({} as any),
+ );
+ const amountStr = Amounts.stringify(amount);
+ const depositPaytoUri = stringifyPaytoUri(currentAccount);
- const accountMap = createLabelsForBankAccount(accounts);
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ const hook = useAsyncAsHook(async () => {
+ const fee = await api.wallet.call(WalletApiOperation.PrepareDeposit, {
+ amount: amountStr,
+ depositPaytoUri,
+ });
- async function updateAccountFromList(accountStr: string): Promise<void> {
- const uri = !accountStr ? undefined : parsePaytoUri(accountStr);
- if (uri) {
- try {
- const result = await getFeeForAmount(uri, amount, api);
- setSelectedAccount(uri);
- setFee(result);
- } catch (e) {
- setSelectedAccount(uri);
- setFee(undefined);
- }
- }
- }
+ return { fee };
+ }, [amountStr, depositPaytoUri]);
- async function updateAmount(newAmount: AmountJson): Promise<void> {
- // const parsed = Amounts.parse(`${currency}:${numStr}`);
- try {
- const result = await getFeeForAmount(currentAccount, newAmount, api);
- setAmount(newAmount);
- setFee(result);
- } catch (e) {
- setAmount(newAmount);
- setFee(undefined);
+ if (!hook) {
+ return {
+ status: "loading",
+ error: undefined,
+ };
+ }
+ if (hook.hasError) {
+ return {
+ status: "error",
+ error: alertFromError(
+ i18n,
+ i18n.str`Could not load fee for amount ${amountStr}`,
+ hook,
+ ),
+ };
}
- }
- const totalFee =
- fee !== undefined
- ? Amounts.sum([fee.wire, fee.coin, fee.refresh]).amount
- : Amounts.zeroOfCurrency(currency);
+ const { fee } = hook.response;
- const totalToDeposit =
- fee !== undefined
- ? Amounts.sub(amount, totalFee).amount
- : Amounts.zeroOfCurrency(currency);
+ const accountMap = createLabelsForBankAccount(accounts);
- const isDirty = amount !== initialValue;
- const amountError = !isDirty
- ? undefined
- : Amounts.cmp(balance, amount) === -1
- ? `Too much, your current balance is ${Amounts.stringifyValue(balance)}`
- : undefined;
+ const totalFee =
+ fee !== undefined
+ ? Amounts.sum([fee.fees.wire, fee.fees.coin, fee.fees.refresh]).amount
+ : Amounts.zeroOfCurrency(currency);
- const unableToDeposit =
- Amounts.isZero(totalToDeposit) || //deposit may be zero because of fee
- fee === undefined || //no fee calculated yet
- amountError !== undefined; //amount field may be invalid
+ const totalToDeposit =
+ fee !== undefined
+ ? Amounts.sub(amount, totalFee).amount
+ : Amounts.zeroOfCurrency(currency);
- async function doSend(): Promise<void> {
- if (!currency) return;
+ const isDirty = amount !== initialValue;
+ const amountError = !isDirty
+ ? undefined
+ : Amounts.cmp(balance, amount) === -1
+ ? `Too much, your current balance is ${Amounts.stringifyValue(balance)}`
+ : undefined;
- const depositPaytoUri = stringifyPaytoUri(currentAccount);
- const amountStr = Amounts.stringify(amount);
- await api.wallet.call(WalletApiOperation.CreateDepositGroup, {
- amount: amountStr,
- depositPaytoUri,
- });
- onSuccess(currency);
- }
+ const unableToDeposit =
+ Amounts.isZero(totalToDeposit) || //deposit may be zero because of fee
+ fee === undefined || //no fee calculated yet
+ amountError !== undefined; //amount field may be invalid
- return {
- status: "ready",
- error: undefined,
- currency,
- amount: {
- value: amount,
- onInput: updateAmount,
- error: amountError,
- },
- onAddAccount: {
- onClick: async () => {
- setAddingAccount(true);
+ async function doSend(): Promise<void> {
+ if (!currency) return;
+
+ const depositPaytoUri = stringifyPaytoUri(currentAccount);
+ const amountStr = Amounts.stringify(amount);
+ await api.wallet.call(WalletApiOperation.CreateDepositGroup, {
+ amount: amountStr,
+ depositPaytoUri,
+ });
+ onSuccess(currency);
+ }
+
+ return {
+ status: "ready",
+ error: undefined,
+ currency,
+ amount: {
+ value: amount,
+ onInput: pushAlertOnError(async (a) => setAmount(a)),
+ error: amountError,
+ },
+ onAddAccount: {
+ onClick: pushAlertOnError(async () => {
+ setAddingAccount(true);
+ }),
+ },
+ account: {
+ list: accountMap,
+ value: stringifyPaytoUri(currentAccount),
+ onChange: pushAlertOnError(updateAccountFromList),
},
- },
- account: {
- list: accountMap,
- value: stringifyPaytoUri(currentAccount),
- onChange: updateAccountFromList,
- },
- currentAccount,
- cancelHandler: {
- onClick: async () => {
- onCancel(currency);
+ currentAccount,
+ cancelHandler: {
+ onClick: pushAlertOnError(async () => {
+ onCancel(currency);
+ }),
},
- },
- depositHandler: {
- onClick: unableToDeposit ? undefined : doSend,
- },
- totalFee,
- totalToDeposit,
- // currentAccount,
- // parsedAmount,
+ depositHandler: {
+ onClick: unableToDeposit ? undefined : pushAlertOnError(doSend),
+ },
+ totalFee,
+ totalToDeposit,
+ };
};
}
-async function getFeeForAmount(
- p: PaytoUri,
- a: AmountJson,
- api: typeof wxApi,
-): Promise<DepositGroupFees> {
- const depositPaytoUri = `payto://${p.targetType}/${p.targetPath}`;
- const amount = Amounts.stringify(a);
- return await api.wallet.call(WalletApiOperation.GetFeeForDeposit, {
- amount,
- depositPaytoUri,
- });
-}
-
-export function labelForAccountType(id: string) {
+export function labelForAccountType(id: string): string {
switch (id) {
case "":
return "Choose one";
diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/stories.tsx b/packages/taler-wallet-webextension/src/wallet/DepositPage/stories.tsx
index b4d1060eb..c23f83fdd 100644
--- a/packages/taler-wallet-webextension/src/wallet/DepositPage/stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/stories.tsx
@@ -19,26 +19,21 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { Amounts, DepositGroupFees } from "@gnu-taler/taler-util";
-import { createExample } from "../../test-utils.js";
-import { labelForAccountType } from "./state.js";
+import { Amounts } from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
+import { nullFunction } from "../../mui/handlers.js";
import { ReadyView } from "./views.js";
export default {
title: "deposit",
};
-// const ac = parsePaytoUri("payto://iban/ES8877998399652238")!;
-// const accountMap = createLabelsForBankAccount([ac]);
-
-export const WithNoAccountForIBAN = createExample(ReadyView, {
+export const WithNoAccountForIBAN = tests.createExample(ReadyView, {
status: "ready",
account: {
list: {},
value: "",
- onChange: async () => {
- null;
- },
+ onChange: nullFunction,
},
currentAccount: {
isKnown: true,
@@ -49,31 +44,25 @@ export const WithNoAccountForIBAN = createExample(ReadyView, {
},
currency: "USD",
amount: {
- onInput: async () => {
- null;
- },
+ onInput: nullFunction,
value: Amounts.parseOrThrow("USD:10"),
},
onAddAccount: {},
cancelHandler: {},
depositHandler: {
- onClick: async () => {
- return;
- },
+ onClick: nullFunction,
},
totalFee: Amounts.zeroOfCurrency("USD"),
totalToDeposit: Amounts.parseOrThrow("USD:10"),
// onCalculateFee: alwaysReturnFeeToOne,
});
-export const WithIBANAccountTypeSelected = createExample(ReadyView, {
+export const WithIBANAccountTypeSelected = tests.createExample(ReadyView, {
status: "ready",
account: {
list: { asdlkajsdlk: "asdlkajsdlk", qwerqwer: "qwerqwer" },
value: "asdlkajsdlk",
- onChange: async () => {
- null;
- },
+ onChange: nullFunction,
},
currentAccount: {
isKnown: true,
@@ -84,31 +73,25 @@ export const WithIBANAccountTypeSelected = createExample(ReadyView, {
},
currency: "USD",
amount: {
- onInput: async () => {
- null;
- },
+ onInput: nullFunction,
value: Amounts.parseOrThrow("USD:10"),
},
onAddAccount: {},
cancelHandler: {},
depositHandler: {
- onClick: async () => {
- return;
- },
+ onClick: nullFunction,
},
totalFee: Amounts.zeroOfCurrency("USD"),
totalToDeposit: Amounts.parseOrThrow("USD:10"),
// onCalculateFee: alwaysReturnFeeToOne,
});
-export const NewBitcoinAccountTypeSelected = createExample(ReadyView, {
+export const NewBitcoinAccountTypeSelected = tests.createExample(ReadyView, {
status: "ready",
account: {
list: {},
value: "asdlkajsdlk",
- onChange: async () => {
- null;
- },
+ onChange: nullFunction,
},
currentAccount: {
isKnown: true,
@@ -120,16 +103,12 @@ export const NewBitcoinAccountTypeSelected = createExample(ReadyView, {
onAddAccount: {},
currency: "USD",
amount: {
- onInput: async () => {
- null;
- },
+ onInput: nullFunction,
value: Amounts.parseOrThrow("USD:10"),
},
cancelHandler: {},
depositHandler: {
- onClick: async () => {
- return;
- },
+ onClick: nullFunction,
},
totalFee: Amounts.zeroOfCurrency("USD"),
totalToDeposit: Amounts.parseOrThrow("USD:10"),
diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts b/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts
index 3f08c678c..157cb868a 100644
--- a/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts
+++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts
@@ -21,46 +21,62 @@
import {
Amounts,
+ AmountString,
DepositGroupFees,
parsePaytoUri,
+ PrepareDepositResponse,
+ ScopeType,
stringifyPaytoUri,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { expect } from "chai";
-import {
- createWalletApiMock,
- mountHook,
- nullFunction,
-} from "../../test-utils.js";
+import * as tests from "@gnu-taler/web-util/testing";
+import { nullFunction } from "../../mui/handlers.js";
+import { createWalletApiMock } from "../../test-utils.js";
import { useComponentState } from "./state.js";
const currency = "EUR";
-const withoutFee = (): DepositGroupFees => ({
- coin: Amounts.stringify(`${currency}:0`),
- wire: Amounts.stringify(`${currency}:0`),
- refresh: Amounts.stringify(`${currency}:0`),
+const amount = `${currency}:0`;
+const withoutFee = (): PrepareDepositResponse => ({
+ effectiveDepositAmount: `${currency}:5` as AmountString,
+ totalDepositCost: `${currency}:5` as AmountString,
+ fees: {
+ coin: Amounts.stringify(`${currency}:0`),
+ wire: Amounts.stringify(`${currency}:0`),
+ refresh: Amounts.stringify(`${currency}:0`),
+ },
});
-const withSomeFee = (): DepositGroupFees => ({
- coin: Amounts.stringify(`${currency}:1`),
- wire: Amounts.stringify(`${currency}:1`),
- refresh: Amounts.stringify(`${currency}:1`),
+const withSomeFee = (): PrepareDepositResponse => ({
+ effectiveDepositAmount: `${currency}:5` as AmountString,
+ totalDepositCost: `${currency}:5` as AmountString,
+ fees: {
+ coin: Amounts.stringify(`${currency}:1`),
+ wire: Amounts.stringify(`${currency}:1`),
+ refresh: Amounts.stringify(`${currency}:1`),
+ },
});
describe("DepositPage states", () => {
it("should have status 'no-enough-balance' when balance is empty", async () => {
- const { handler, mock } = createWalletApiMock();
- const props = { currency, onCancel: nullFunction, onSuccess: nullFunction };
+ const { handler, TestingContext } = createWalletApiMock();
+ const props = { amount, onCancel: nullFunction, onSuccess: nullFunction };
handler.addWalletCallResponse(WalletApiOperation.GetBalances, undefined, {
balances: [
{
- available: `${currency}:0`,
+ flags: [],
+ available: `${currency}:0` as AmountString,
hasPendingTransactions: false,
- pendingIncoming: `${currency}:0`,
- pendingOutgoing: `${currency}:0`,
+ pendingIncoming: `${currency}:0` as AmountString,
+ pendingOutgoing: `${currency}:0` as AmountString,
requiresUserInput: false,
+ scopeInfo: {
+ currency,
+ type: ScopeType.Auditor,
+ url: "asd",
+ },
},
],
});
@@ -72,37 +88,42 @@ describe("DepositPage states", () => {
},
);
- const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
- mountHook(() => useComponentState(props, mock));
-
- {
- const { status } = pullLastResultOrThrow();
- expect(status).equal("loading");
- }
-
- expect(await waitForStateUpdate()).true;
-
- {
- const { status } = pullLastResultOrThrow();
- expect(status).equal("no-enough-balance");
- }
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentState,
+ props,
+ [
+ ({ status }) => {
+ expect(status).equal("loading");
+ },
+ ({ status }) => {
+ expect(status).equal("no-enough-balance");
+ },
+ ],
+ TestingContext,
+ );
- await assertNoPendingUpdate();
+ expect(hookBehavior).deep.equal({ result: "ok" });
expect(handler.getCallingQueueState()).eq("empty");
});
it("should have status 'no-accounts' when balance is not empty and accounts is empty", async () => {
- const { handler, mock } = createWalletApiMock();
- const props = { currency, onCancel: nullFunction, onSuccess: nullFunction };
+ const { handler, TestingContext } = createWalletApiMock();
+ const props = { amount, onCancel: nullFunction, onSuccess: nullFunction };
handler.addWalletCallResponse(WalletApiOperation.GetBalances, undefined, {
balances: [
{
- available: `${currency}:1`,
+ flags: [],
+ available: `${currency}:1` as AmountString,
hasPendingTransactions: false,
- pendingIncoming: `${currency}:0`,
- pendingOutgoing: `${currency}:0`,
+ pendingIncoming: `${currency}:0` as AmountString,
+ pendingOutgoing: `${currency}:0` as AmountString,
requiresUserInput: false,
+ scopeInfo: {
+ currency,
+ type: ScopeType.Auditor,
+ url: "asd",
+ },
},
],
});
@@ -113,22 +134,22 @@ describe("DepositPage states", () => {
accounts: [],
},
);
- const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
- mountHook(() => useComponentState(props, mock));
-
- {
- const { status } = pullLastResultOrThrow();
- expect(status).equal("loading");
- }
-
- expect(await waitForStateUpdate()).true;
- {
- const r = pullLastResultOrThrow();
- if (r.status !== "no-accounts") expect.fail();
- // expect(r.cancelHandler.onClick).not.undefined;
- }
-
- await assertNoPendingUpdate();
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentState,
+ props,
+ [
+ ({ status }) => {
+ expect(status).equal("loading");
+ },
+ ({ status }) => {
+ expect(status).equal("no-accounts");
+ },
+ ],
+ TestingContext,
+ );
+
+ expect(hookBehavior).deep.equal({ result: "ok" });
expect(handler.getCallingQueueState()).eq("empty");
});
@@ -146,17 +167,23 @@ describe("DepositPage states", () => {
};
it("should have status 'ready' but unable to deposit ", async () => {
- const { handler, mock } = createWalletApiMock();
- const props = { currency, onCancel: nullFunction, onSuccess: nullFunction };
+ const { handler, TestingContext } = createWalletApiMock();
+ const props = { amount, onCancel: nullFunction, onSuccess: nullFunction };
handler.addWalletCallResponse(WalletApiOperation.GetBalances, undefined, {
balances: [
{
- available: `${currency}:1`,
+ flags: [],
+ available: `${currency}:1` as AmountString,
hasPendingTransactions: false,
- pendingIncoming: `${currency}:0`,
- pendingOutgoing: `${currency}:0`,
+ pendingIncoming: `${currency}:0` as AmountString,
+ pendingOutgoing: `${currency}:0` as AmountString,
requiresUserInput: false,
+ scopeInfo: {
+ currency,
+ type: ScopeType.Auditor,
+ url: "asd",
+ },
},
],
});
@@ -168,52 +195,55 @@ describe("DepositPage states", () => {
},
);
handler.addWalletCallResponse(
- WalletApiOperation.GetFeeForDeposit,
+ WalletApiOperation.PrepareDeposit,
undefined,
withoutFee(),
);
- const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
- mountHook(() => useComponentState(props, mock));
-
- {
- const { status } = pullLastResultOrThrow();
- expect(status).equal("loading");
- }
-
- expect(await waitForStateUpdate()).true;
- {
- const { status } = pullLastResultOrThrow();
- expect(status).equal("loading");
- }
- expect(await waitForStateUpdate()).true;
-
- {
- const r = pullLastResultOrThrow();
- if (r.status !== "ready") expect.fail();
- expect(r.cancelHandler.onClick).not.undefined;
- expect(r.currency).eq(currency);
- expect(r.account.value).eq(stringifyPaytoUri(ibanPayto.uri));
- expect(r.amount.value).deep.eq(Amounts.parseOrThrow("EUR:0"));
- expect(r.depositHandler.onClick).undefined;
- }
-
- await assertNoPendingUpdate();
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentState,
+ props,
+ [
+ ({ status }) => {
+ expect(status).equal("loading");
+ },
+ ({ status }) => {
+ expect(status).equal("loading");
+ },
+ (state) => {
+ if (state.status !== "ready") expect.fail();
+ expect(state.cancelHandler.onClick).not.undefined;
+ expect(state.currency).eq(currency);
+ expect(state.account.value).eq(stringifyPaytoUri(ibanPayto.uri));
+ expect(state.amount.value).deep.eq(Amounts.parseOrThrow("EUR:0"));
+ expect(state.depositHandler.onClick).undefined;
+ },
+ ],
+ TestingContext,
+ );
+
+ expect(hookBehavior).deep.equal({ result: "ok" });
expect(handler.getCallingQueueState()).eq("empty");
});
it("should not be able to deposit more than the balance ", async () => {
- const { handler, mock } = createWalletApiMock();
- const props = { currency, onCancel: nullFunction, onSuccess: nullFunction };
+ const { handler, TestingContext } = createWalletApiMock();
+ const props = { amount, onCancel: nullFunction, onSuccess: nullFunction };
handler.addWalletCallResponse(WalletApiOperation.GetBalances, undefined, {
balances: [
{
- available: `${currency}:5`,
+ flags: [],
+ available: `${currency}:5` as AmountString,
hasPendingTransactions: false,
- pendingIncoming: `${currency}:0`,
- pendingOutgoing: `${currency}:0`,
+ pendingIncoming: `${currency}:0` as AmountString,
+ pendingOutgoing: `${currency}:0` as AmountString,
requiresUserInput: false,
+ scopeInfo: {
+ currency,
+ type: ScopeType.Auditor,
+ url: "asd",
+ },
},
],
});
@@ -225,154 +255,85 @@ describe("DepositPage states", () => {
},
);
handler.addWalletCallResponse(
- WalletApiOperation.GetFeeForDeposit,
+ WalletApiOperation.PrepareDeposit,
undefined,
withoutFee(),
);
handler.addWalletCallResponse(
- WalletApiOperation.GetFeeForDeposit,
- undefined,
- withoutFee(),
- );
- handler.addWalletCallResponse(
- WalletApiOperation.GetFeeForDeposit,
- undefined,
- withoutFee(),
- );
- handler.addWalletCallResponse(
- WalletApiOperation.GetFeeForDeposit,
+ WalletApiOperation.PrepareDeposit,
undefined,
withoutFee(),
);
- const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
- mountHook(() => useComponentState(props, mock));
-
- {
- const { status } = pullLastResultOrThrow();
- expect(status).equal("loading");
- }
-
- expect(await waitForStateUpdate()).true;
- {
- const { status } = pullLastResultOrThrow();
- expect(status).equal("loading");
- }
-
- expect(await waitForStateUpdate()).true;
const accountSelected = stringifyPaytoUri(ibanPayto.uri);
- {
- const r = pullLastResultOrThrow();
- if (r.status !== "ready") expect.fail();
- expect(r.cancelHandler.onClick).not.undefined;
- expect(r.currency).eq(currency);
- expect(r.account.value).eq(stringifyPaytoUri(talerBankPayto.uri));
- expect(r.amount.value).deep.eq(Amounts.parseOrThrow("EUR:0"));
- expect(r.depositHandler.onClick).undefined;
- expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`));
- expect(r.account.onChange).not.undefined;
-
- r.account.onChange!(accountSelected);
- }
-
- expect(await waitForStateUpdate()).true;
-
- {
- const r = pullLastResultOrThrow();
- if (r.status !== "ready") expect.fail();
- expect(r.cancelHandler.onClick).not.undefined;
- expect(r.currency).eq(currency);
- expect(r.account.value).eq(accountSelected);
- expect(r.amount.value).deep.eq(Amounts.parseOrThrow("EUR:0"));
- expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`));
- expect(r.depositHandler.onClick).undefined;
- }
-
- await assertNoPendingUpdate();
- });
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentState,
+ props,
+ [
+ ({ status }) => {
+ expect(status).equal("loading");
+ },
+ ({ status }) => {
+ expect(status).equal("loading");
+ },
+ (state) => {
+ if (state.status !== "ready") expect.fail();
+ expect(state.cancelHandler.onClick).not.undefined;
+ expect(state.currency).eq(currency);
+ expect(state.account.value).eq(stringifyPaytoUri(talerBankPayto.uri));
+ expect(state.amount.value).deep.eq(Amounts.parseOrThrow("EUR:0"));
+ expect(state.depositHandler.onClick).undefined;
+ expect(state.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`));
+ expect(state.account.onChange).not.undefined;
+
+ state.account.onChange!(accountSelected);
+ },
+ (state) => {
+ if (state.status !== "ready") expect.fail();
+ expect(state.cancelHandler.onClick).not.undefined;
+ expect(state.currency).eq(currency);
+ expect(state.account.value).eq(accountSelected);
+ expect(state.amount.value).deep.eq(Amounts.parseOrThrow("EUR:0"));
+ expect(state.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`));
+ expect(state.depositHandler.onClick).undefined;
+ },
+ (state) => {
+ if (state.status !== "ready") expect.fail();
+ expect(state.cancelHandler.onClick).not.undefined;
+ expect(state.currency).eq(currency);
+ expect(state.account.value).eq(accountSelected);
+ expect(state.amount.value).deep.eq(Amounts.parseOrThrow("EUR:0"));
+ expect(state.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`));
+ expect(state.depositHandler.onClick).undefined;
+ },
+ ],
+ TestingContext,
+ );
- // it("should calculate the fee upon entering amount ", async () => {
- // const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
- // mountHook(() =>
- // useComponentState(
- // { currency, onCancel: nullFunction, onSuccess: nullFunction },
- // {
- // getBalance: async () =>
- // ({
- // balances: [{ available: `${currency}:1` }],
- // } as Partial<BalancesResponse>),
- // listKnownBankAccounts: async () => ({ accounts: [ibanPayto] }),
- // getFeeForDeposit: withSomeFee,
- // } as Partial<typeof wxApi> as any,
- // ),
- // );
-
- // {
- // const { status } = getLastResultOrThrow();
- // expect(status).equal("loading");
- // }
-
- // await waitNextUpdate();
-
- // {
- // const r = getLastResultOrThrow();
- // if (r.status !== "ready") expect.fail();
- // expect(r.cancelHandler.onClick).not.undefined;
- // expect(r.currency).eq(currency);
- // expect(r.account.value).eq(accountSelected);
- // expect(r.amount.value).eq("0");
- // expect(r.depositHandler.onClick).undefined;
- // expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`));
-
- // r.amount.onInput("10");
- // }
-
- // expect(await waitForStateUpdate()).true;
-
- // {
- // const r = pullLastResultOrThrow();
- // if (r.status !== "ready") expect.fail();
- // expect(r.cancelHandler.onClick).not.undefined;
- // expect(r.currency).eq(currency);
- // expect(r.account.value).eq(accountSelected);
- // expect(r.amount.value).eq("10");
- // expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`));
- // expect(r.depositHandler.onClick).undefined;
-
- // r.amount.onInput("3");
- // }
-
- // expect(await waitForStateUpdate()).true;
-
- // {
- // const r = pullLastResultOrThrow();
- // if (r.status !== "ready") expect.fail();
- // expect(r.cancelHandler.onClick).not.undefined;
- // expect(r.currency).eq(currency);
- // expect(r.account.value).eq(accountSelected);
- // expect(r.amount.value).eq("3");
- // expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`));
- // expect(r.depositHandler.onClick).not.undefined;
- // }
-
- // await assertNoPendingUpdate();
- // expect(handler.getCallingQueueState()).eq("empty")
- // });
+ expect(hookBehavior).deep.equal({ result: "ok" });
+ expect(handler.getCallingQueueState()).eq("empty");
+ });
it("should calculate the fee upon entering amount ", async () => {
- const { handler, mock } = createWalletApiMock();
- const props = { currency, onCancel: nullFunction, onSuccess: nullFunction };
+ const { handler, TestingContext } = createWalletApiMock();
+ const props = { amount, onCancel: nullFunction, onSuccess: nullFunction };
handler.addWalletCallResponse(WalletApiOperation.GetBalances, undefined, {
balances: [
{
- available: `${currency}:10`,
+ flags: [],
+ available: `${currency}:10` as AmountString,
hasPendingTransactions: false,
- pendingIncoming: `${currency}:0`,
- pendingOutgoing: `${currency}:0`,
+ pendingIncoming: `${currency}:0` as AmountString,
+ pendingOutgoing: `${currency}:0` as AmountString,
requiresUserInput: false,
+ scopeInfo: {
+ currency,
+ type: ScopeType.Auditor,
+ url: "asd",
+ },
},
],
});
@@ -384,85 +345,87 @@ describe("DepositPage states", () => {
},
);
handler.addWalletCallResponse(
- WalletApiOperation.GetFeeForDeposit,
+ WalletApiOperation.PrepareDeposit,
undefined,
withoutFee(),
);
handler.addWalletCallResponse(
- WalletApiOperation.GetFeeForDeposit,
+ WalletApiOperation.PrepareDeposit,
undefined,
withSomeFee(),
);
handler.addWalletCallResponse(
- WalletApiOperation.GetFeeForDeposit,
+ WalletApiOperation.PrepareDeposit,
undefined,
withSomeFee(),
);
- const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
- mountHook(() => useComponentState(props, mock));
-
- {
- const { status } = pullLastResultOrThrow();
- expect(status).equal("loading");
- }
-
- expect(await waitForStateUpdate()).true;
-
- {
- const { status } = pullLastResultOrThrow();
- expect(status).equal("loading");
- }
-
- expect(await waitForStateUpdate()).true;
const accountSelected = stringifyPaytoUri(ibanPayto.uri);
- {
- const r = pullLastResultOrThrow();
- if (r.status !== "ready") expect.fail();
- expect(r.cancelHandler.onClick).not.undefined;
- expect(r.currency).eq(currency);
- expect(r.account.value).eq(stringifyPaytoUri(talerBankPayto.uri));
- expect(r.amount.value).deep.eq(Amounts.parseOrThrow("EUR:0"));
- expect(r.depositHandler.onClick).undefined;
- expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`));
- expect(r.account.onChange).not.undefined;
-
- r.account.onChange!(accountSelected);
- }
-
- expect(await waitForStateUpdate()).true;
-
- {
- const r = pullLastResultOrThrow();
- if (r.status !== "ready") expect.fail();
- expect(r.cancelHandler.onClick).not.undefined;
- expect(r.currency).eq(currency);
- expect(r.account.value).eq(accountSelected);
- expect(r.amount.value).deep.eq(Amounts.parseOrThrow("EUR:0"));
- expect(r.depositHandler.onClick).undefined;
- expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`));
-
- expect(r.amount.onInput).not.undefined;
- if (!r.amount.onInput) return;
- r.amount.onInput(Amounts.parseOrThrow("EUR:10"));
- }
-
- expect(await waitForStateUpdate()).true;
-
- {
- const r = pullLastResultOrThrow();
- if (r.status !== "ready") expect.fail();
- expect(r.cancelHandler.onClick).not.undefined;
- expect(r.currency).eq(currency);
- expect(r.account.value).eq(accountSelected);
- expect(r.amount.value).deep.eq(Amounts.parseOrThrow("EUR:10"));
- expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`));
- expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:7`));
- expect(r.depositHandler.onClick).not.undefined;
- }
-
- await assertNoPendingUpdate();
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentState,
+ props,
+ [
+ ({ status }) => {
+ expect(status).equal("loading");
+ },
+ ({ status }) => {
+ expect(status).equal("loading");
+ },
+ (state) => {
+ if (state.status !== "ready") expect.fail();
+ expect(state.cancelHandler.onClick).not.undefined;
+ expect(state.currency).eq(currency);
+ expect(state.account.value).eq(stringifyPaytoUri(talerBankPayto.uri));
+ expect(state.amount.value).deep.eq(Amounts.parseOrThrow("EUR:0"));
+ expect(state.depositHandler.onClick).undefined;
+ expect(state.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`));
+ expect(state.account.onChange).not.undefined;
+
+ state.account.onChange!(accountSelected);
+ },
+ (state) => {
+ if (state.status !== "ready") expect.fail();
+ expect(state.cancelHandler.onClick).not.undefined;
+ expect(state.currency).eq(currency);
+ expect(state.account.value).eq(accountSelected);
+ expect(state.amount.value).deep.eq(Amounts.parseOrThrow("EUR:0"));
+ expect(state.depositHandler.onClick).undefined;
+ expect(state.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`));
+
+ expect(state.amount.onInput).not.undefined;
+ if (!state.amount.onInput) return;
+ state.amount.onInput(Amounts.parseOrThrow("EUR:10"));
+ },
+ (state) => {
+ if (state.status !== "ready") expect.fail();
+ expect(state.cancelHandler.onClick).not.undefined;
+ expect(state.currency).eq(currency);
+ expect(state.account.value).eq(accountSelected);
+ expect(state.amount.value).deep.eq(Amounts.parseOrThrow("EUR:10"));
+ expect(state.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`));
+ expect(state.totalToDeposit).deep.eq(
+ Amounts.parseOrThrow(`${currency}:7`),
+ );
+ expect(state.depositHandler.onClick).not.undefined;
+ },
+ (state) => {
+ if (state.status !== "ready") expect.fail();
+ expect(state.cancelHandler.onClick).not.undefined;
+ expect(state.currency).eq(currency);
+ expect(state.account.value).eq(accountSelected);
+ expect(state.amount.value).deep.eq(Amounts.parseOrThrow("EUR:10"));
+ expect(state.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`));
+ expect(state.totalToDeposit).deep.eq(
+ Amounts.parseOrThrow(`${currency}:7`),
+ );
+ expect(state.depositHandler.onClick).not.undefined;
+ },
+ ],
+ TestingContext,
+ );
+
+ expect(hookBehavior).deep.equal({ result: "ok" });
expect(handler.getCallingQueueState()).eq("empty");
});
});
diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/views.tsx b/packages/taler-wallet-webextension/src/wallet/DepositPage/views.tsx
index 6a28f31e1..908becb04 100644
--- a/packages/taler-wallet-webextension/src/wallet/DepositPage/views.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/views.tsx
@@ -18,31 +18,13 @@ import { Amounts, PaytoUri } from "@gnu-taler/taler-util";
import { Fragment, h, VNode } from "preact";
import { AmountField } from "../../components/AmountField.js";
import { ErrorMessage } from "../../components/ErrorMessage.js";
-import { LoadingError } from "../../components/LoadingError.js";
import { SelectList } from "../../components/SelectList.js";
-import {
- ErrorText,
- Input,
- InputWithLabel,
- SubTitle,
- WarningBox,
-} from "../../components/styled/index.js";
-import { useTranslationContext } from "../../context/translation.js";
+import { Input, SubTitle, WarningBox } from "../../components/styled/index.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Button } from "../../mui/Button.js";
import { Grid } from "../../mui/Grid.js";
import { State } from "./index.js";
-export function LoadingErrorView({ error }: State.LoadingUriError): VNode {
- const { i18n } = useTranslationContext();
-
- return (
- <LoadingError
- title={<i18n.Translate>Could not load deposit balance</i18n.Translate>}
- error={error}
- />
- );
-}
-
export function AmountOrCurrencyErrorView(
p: State.AmountOrCurrencyError,
): VNode {
@@ -50,11 +32,7 @@ export function AmountOrCurrencyErrorView(
return (
<ErrorMessage
- title={
- <i18n.Translate>
- A currency or an amount should be indicated
- </i18n.Translate>
- }
+ title={i18n.str`A currency or an amount should be indicated`}
/>
);
}
@@ -66,11 +44,7 @@ export function NoEnoughBalanceView({
return (
<ErrorMessage
- title={
- <i18n.Translate>
- There is no enough balance to make a deposit for currency {currency}
- </i18n.Translate>
- }
+ title={i18n.str`There is no enough balance to make a deposit for currency ${currency}`}
/>
);
}
@@ -150,7 +124,7 @@ export function ReadyView(state: State.Ready): VNode {
>
<Input>
<SelectList
- label={<i18n.Translate>Select account</i18n.Translate>}
+ label={i18n.str`Select account`}
list={state.account.list}
name="account"
value={state.account.value}
@@ -171,14 +145,11 @@ export function ReadyView(state: State.Ready): VNode {
</p>
<Grid container spacing={2} columns={1}>
<Grid item xs={1}>
- <AmountField
- label={<i18n.Translate>Amount</i18n.Translate>}
- handler={state.amount}
- />
+ <AmountField label={i18n.str`Amount`} handler={state.amount} />
</Grid>
<Grid item xs={1}>
<AmountField
- label={<i18n.Translate>Deposit fee</i18n.Translate>}
+ label={i18n.str`Deposit fee`}
handler={{
value: state.totalFee,
}}
@@ -186,7 +157,7 @@ export function ReadyView(state: State.Ready): VNode {
</Grid>
<Grid item xs={1}>
<AmountField
- label={<i18n.Translate>Total deposit</i18n.Translate>}
+ label={i18n.str`Total deposit`}
handler={{
value: state.totalToDeposit,
}}
diff --git a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/index.ts b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/index.ts
index 2f066d744..b56fe5523 100644
--- a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/index.ts
+++ b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/index.ts
@@ -14,13 +14,17 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+import { ErrorAlertView } from "../../components/CurrentAlerts.js";
import { Loading } from "../../components/Loading.js";
-import { HookError } from "../../hooks/useAsyncAsHook.js";
-import { AmountFieldHandler, ButtonHandler } from "../../mui/handlers.js";
+import { ErrorAlert } from "../../context/alert.js";
+import {
+ AmountFieldHandler,
+ ButtonHandler,
+ ToggleHandler,
+} from "../../mui/handlers.js";
import { compose, StateViewMap } from "../../utils/index.js";
-import { wxApi } from "../../wxApi.js";
import { useComponentState } from "./state.js";
-import { LoadingUriView, ReadyView, SelectCurrencyView } from "./views.js";
+import { ReadyView, SelectCurrencyView } from "./views.js";
export type Props = PropsGet | PropsSend;
@@ -50,8 +54,8 @@ export namespace State {
}
export interface LoadingUriError {
- status: "loading-error";
- error: HookError;
+ status: "error";
+ error: ErrorAlert;
}
export interface SelectCurrency {
@@ -66,6 +70,7 @@ export namespace State {
error: undefined;
type: Props["type"];
selectCurrency: ButtonHandler;
+ selectMax: ButtonHandler;
previous: Contact[];
goToBank: ButtonHandler;
goToWallet: ButtonHandler;
@@ -81,13 +86,13 @@ export type Contact = {
const viewMapping: StateViewMap<State> = {
loading: Loading,
- "loading-error": LoadingUriView,
+ error: ErrorAlertView,
"select-currency": SelectCurrencyView,
ready: ReadyView,
};
export const DestinationSelectionPage = compose(
"DestinationSelectionPage",
- (p: Props) => useComponentState(p, wxApi),
+ (p: Props) => useComponentState(p),
viewMapping,
);
diff --git a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/state.ts b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/state.ts
index a67f926bc..d4e270a6c 100644
--- a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/state.ts
+++ b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/state.ts
@@ -16,26 +16,37 @@
import { Amounts } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { useState } from "preact/hooks";
+import { alertFromError, useAlertContext } from "../../context/alert.js";
+import { useBackendContext } from "../../context/backend.js";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
-import { assertUnreachable, RecursiveState } from "../../utils/index.js";
-import { wxApi } from "../../wxApi.js";
+import { RecursiveState, assertUnreachable } from "../../utils/index.js";
import { Contact, Props, State } from "./index.js";
-export function useComponentState(
- props: Props,
- api: typeof wxApi,
-): RecursiveState<State> {
+export function useComponentState(props: Props): RecursiveState<State> {
+ const api = useBackendContext();
+ const { pushAlertOnError } = useAlertContext();
+
const parsedInitialAmount = !props.amount
? undefined
: Amounts.parse(props.amount);
+ const hook = useAsyncAsHook(async () => {
+ if (!parsedInitialAmount) return undefined;
+ const balance = await api.wallet.call(WalletApiOperation.GetBalanceDetail, {
+ currency: parsedInitialAmount.currency,
+ });
+ return { balance };
+ });
+
+ const info = hook && !hook.hasError ? hook.response : undefined;
+
// const initialCurrency = parsedInitialAmount?.currency;
const [amount, setAmount] = useState(
!parsedInitialAmount ? undefined : parsedInitialAmount,
);
-
//FIXME: get this information from wallet
// eslint-disable-next-line no-constant-condition
const previous: Contact[] = true
@@ -43,17 +54,17 @@ export function useComponentState(
: [
{
name: "International Bank",
- icon_type: 'bank',
+ icon_type: "bank",
description: "account ending with 3454",
},
{
name: "Max",
- icon_type: 'bank',
+ icon_type: "bank",
description: "account ending with 3454",
},
{
name: "Alex",
- icon_type: 'bank',
+ icon_type: "bank",
description: "account ending with 3454",
},
];
@@ -61,6 +72,8 @@ export function useComponentState(
if (!amount) {
return () => {
// eslint-disable-next-line react-hooks/rules-of-hooks
+ const { i18n } = useTranslationContext();
+ // eslint-disable-next-line react-hooks/rules-of-hooks
const hook = useAsyncAsHook(() =>
api.wallet.call(WalletApiOperation.ListExchanges, {}),
);
@@ -73,8 +86,9 @@ export function useComponentState(
}
if (hook.hasError) {
return {
- status: "loading-error",
- error: hook,
+ status: "error",
+ error: alertFromError(i18n,
+ i18n.str`Could not load exchanges`, hook),
};
}
const currencies: Record<string, string> = {};
@@ -106,26 +120,37 @@ export function useComponentState(
error: undefined,
previous,
selectCurrency: {
- onClick: async () => {
+ onClick: pushAlertOnError(async () => {
setAmount(undefined);
- },
+ }),
},
goToBank: {
onClick: invalid
? undefined
- : async () => {
+ : pushAlertOnError(async () => {
props.goToWalletBankDeposit(currencyAndAmount);
- },
+ }),
+ },
+ selectMax: {
+ onClick: pushAlertOnError(async () => {
+ const resp = await api.wallet.call(
+ WalletApiOperation.GetMaxDepositAmount,
+ {
+ currency: amount.currency,
+ },
+ );
+ setAmount(Amounts.parseOrThrow(resp.effectiveAmount));
+ }),
},
goToWallet: {
onClick: invalid
? undefined
- : async () => {
+ : pushAlertOnError(async () => {
props.goToWalletWalletSend(currencyAndAmount);
- },
+ }),
},
amountHandler: {
- onInput: async (s) => setAmount(s),
+ onInput: pushAlertOnError(async (s) => setAmount(s)),
value: amount,
},
type: props.type,
@@ -136,26 +161,33 @@ export function useComponentState(
error: undefined,
previous,
selectCurrency: {
- onClick: async () => {
+ onClick: pushAlertOnError(async () => {
setAmount(undefined);
- },
+ }),
+ },
+ selectMax: {
+ onClick: invalid
+ ? undefined
+ : pushAlertOnError(async () => {
+ props.goToWalletManualWithdraw(currencyAndAmount);
+ }),
},
goToBank: {
onClick: invalid
? undefined
- : async () => {
+ : pushAlertOnError(async () => {
props.goToWalletManualWithdraw(currencyAndAmount);
- },
+ }),
},
goToWallet: {
onClick: invalid
? undefined
- : async () => {
+ : pushAlertOnError(async () => {
props.goToWalletWalletInvoice(currencyAndAmount);
- },
+ }),
},
amountHandler: {
- onInput: async (s) => setAmount(s),
+ onInput: pushAlertOnError(async (s) => setAmount(s)),
value: amount,
},
type: props.type,
diff --git a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/stories.tsx b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/stories.tsx
index ffec8ba36..e1ac958f7 100644
--- a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/stories.tsx
@@ -19,14 +19,14 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { createExample } from "../../test-utils.js";
+import * as tests from "@gnu-taler/web-util/testing";
import { ReadyView, SelectCurrencyView } from "./views.js";
export default {
title: "destination",
};
-export const GetCash = createExample(ReadyView, {
+export const GetCash = tests.createExample(ReadyView, {
amountHandler: {
value: {
currency: "EUR",
@@ -35,12 +35,13 @@ export const GetCash = createExample(ReadyView, {
},
},
goToBank: {},
+ selectMax: {},
goToWallet: {},
previous: [],
selectCurrency: {},
type: "get",
});
-export const SendCash = createExample(ReadyView, {
+export const SendCash = tests.createExample(ReadyView, {
amountHandler: {
value: {
currency: "EUR",
@@ -48,6 +49,7 @@ export const SendCash = createExample(ReadyView, {
value: 1,
},
},
+ selectMax: {},
goToBank: {},
goToWallet: {},
previous: [],
@@ -55,7 +57,7 @@ export const SendCash = createExample(ReadyView, {
type: "send",
});
-export const SelectCurrency = createExample(SelectCurrencyView, {
+export const SelectCurrency = tests.createExample(SelectCurrencyView, {
currencies: {
"": "Select a currency",
USD: "USD",
diff --git a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/test.ts b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/test.ts
index c2aa04849..683378613 100644
--- a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/test.ts
+++ b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/test.ts
@@ -24,25 +24,38 @@ import {
ExchangeEntryStatus,
ExchangeListItem,
ExchangeTosStatus,
+ ExchangeUpdateStatus,
+ ScopeType,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import * as tests from "@gnu-taler/web-util/testing";
import { expect } from "chai";
-import { createWalletApiMock, mountHook } from "../../test-utils.js";
+import { nullFunction } from "../../mui/handlers.js";
+import { createWalletApiMock } from "../../test-utils.js";
import { useComponentState } from "./state.js";
const exchangeArs: ExchangeListItem = {
currency: "ARS",
exchangeBaseUrl: "http://",
+ masterPub: "123qwe123",
+ scopeInfo: {
+ currency: "ARS",
+ type: ScopeType.Exchange,
+ url: "http://",
+ },
tosStatus: ExchangeTosStatus.Accepted,
- exchangeStatus: ExchangeEntryStatus.Ok,
+ exchangeEntryStatus: ExchangeEntryStatus.Used,
+ exchangeUpdateStatus: ExchangeUpdateStatus.Initial,
paytoUris: [],
- permanent: true,
ageRestrictionOptions: [],
+ lastUpdateTimestamp: undefined,
+ noFees: false,
+ peerPaymentsDisabled: false,
};
describe("Destination selection states", () => {
it("should select currency if no amount specified", async () => {
- const { handler, mock } = createWalletApiMock();
+ const { handler, TestingContext } = createWalletApiMock();
handler.addWalletCallResponse(
WalletApiOperation.ListExchanges,
@@ -54,83 +67,87 @@ describe("Destination selection states", () => {
const props = {
type: "get" as const,
- goToWalletManualWithdraw: () => {
- return null;
- },
- goToWalletWalletInvoice: () => {
- null;
- },
+ goToWalletManualWithdraw: nullFunction,
+ goToWalletWalletInvoice: nullFunction,
};
- const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
- mountHook(() => useComponentState(props, mock));
-
- {
- const state = pullLastResultOrThrow();
-
- if (state.status !== "loading") expect.fail();
- if (state.error) expect.fail();
- }
-
- expect(await waitForStateUpdate()).true;
-
- {
- const state = pullLastResultOrThrow();
-
- if (state.status !== "select-currency") expect.fail();
- if (state.error) expect.fail();
- expect(state.currencies).deep.eq({
- ARS: "ARS",
- "": "Select a currency",
- });
-
- state.onCurrencySelected(exchangeArs.currency!);
- }
-
- expect(await waitForStateUpdate()).true;
- {
- const state = pullLastResultOrThrow();
-
- if (state.status !== "ready") expect.fail();
- if (state.error) expect.fail();
- expect(state.goToBank.onClick).eq(undefined);
- expect(state.goToWallet.onClick).eq(undefined);
-
- expect(state.amountHandler.value).deep.eq(Amounts.parseOrThrow("ARS:0"));
- }
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentState,
+ props,
+ [
+ ({ status }) => {
+ expect(status).equal("loading");
+ },
+ (state) => {
+ if (state.status !== "select-currency") expect.fail();
+ if (state.error) expect.fail();
+ expect(state.currencies).deep.eq({
+ ARS: "ARS",
+ "": "Select a currency",
+ });
+
+ state.onCurrencySelected(exchangeArs.currency!);
+ },
+ (state) => {
+ if (state.status !== "ready") expect.fail();
+ if (state.error) expect.fail();
+ expect(state.goToBank.onClick).eq(undefined);
+ expect(state.goToWallet.onClick).eq(undefined);
+
+ expect(state.amountHandler.value).deep.eq(
+ Amounts.parseOrThrow("ARS:0"),
+ );
+ },
+ ],
+ TestingContext,
+ );
- await assertNoPendingUpdate();
+ expect(hookBehavior).deep.equal({ result: "ok" });
expect(handler.getCallingQueueState()).eq("empty");
});
it("should be possible to start with an amount specified in request params", async () => {
- const { handler, mock } = createWalletApiMock();
+ const { handler, TestingContext } = createWalletApiMock();
const props = {
type: "get" as const,
- goToWalletManualWithdraw: () => {
- return null;
- },
- goToWalletWalletInvoice: () => {
- null;
- },
+ goToWalletManualWithdraw: nullFunction,
+ goToWalletWalletInvoice: nullFunction,
amount: "ARS:2",
};
- const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
- mountHook(() => useComponentState(props, mock));
-
- {
- const state = pullLastResultOrThrow();
- if (state.status !== "ready") expect.fail();
- if (state.error) expect.fail();
- expect(state.goToBank.onClick).not.eq(undefined);
- expect(state.goToWallet.onClick).not.eq(undefined);
-
- expect(state.amountHandler.value).deep.eq(Amounts.parseOrThrow("ARS:2"));
- }
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentState,
+ props,
+ [
+ // ({ status }) => {
+ // expect(status).equal("loading");
+ // },
+ (state) => {
+ if (state.status !== "ready") expect.fail();
+ if (state.error) expect.fail();
+ expect(state.goToBank.onClick).not.eq(undefined);
+ expect(state.goToWallet.onClick).not.eq(undefined);
+
+ expect(state.amountHandler.value).deep.eq(
+ Amounts.parseOrThrow("ARS:2"),
+ );
+ },
+ (state) => {
+ if (state.status !== "ready") expect.fail();
+ if (state.error) expect.fail();
+ expect(state.goToBank.onClick).not.eq(undefined);
+ expect(state.goToWallet.onClick).not.eq(undefined);
+
+ expect(state.amountHandler.value).deep.eq(
+ Amounts.parseOrThrow("ARS:2"),
+ );
+ },
+ ],
+ TestingContext,
+ );
- await assertNoPendingUpdate();
+ expect(hookBehavior).deep.equal({ result: "ok" });
expect(handler.getCallingQueueState()).eq("empty");
});
});
diff --git a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx
index a9a4b2e41..8a74a20f1 100644
--- a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx
@@ -14,9 +14,11 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { styled } from "@linaria/react";
import { Fragment, h, VNode } from "preact";
-import { LoadingError } from "../../components/LoadingError.js";
+import { AmountField } from "../../components/AmountField.js";
+import { EnabledBySettings } from "../../components/EnabledBySettings.js";
import { SelectList } from "../../components/SelectList.js";
import {
Input,
@@ -24,25 +26,14 @@ import {
LinkPrimary,
SvgIcon,
} from "../../components/styled/index.js";
-import { useTranslationContext } from "../../context/translation.js";
-import { Pages } from "../../NavigationBar.js";
-import { Contact, State } from "./index.js";
-import arrowIcon from "../../svg/chevron-down.svg";
-import { AmountField } from "../../components/AmountField.js";
+import { Button } from "../../mui/Button.js";
import { Grid } from "../../mui/Grid.js";
import { Paper } from "../../mui/Paper.js";
-import { Button } from "../../mui/Button.js";
+import { Pages } from "../../NavigationBar.js";
+import arrowIcon from "../../svg/chevron-down.inline.svg";
+import bankIcon from "../../svg/ri-bank-line.inline.svg";
import { assertUnreachable } from "../../utils/index.js";
-
-export function LoadingUriView({ error }: State.LoadingUriError): VNode {
- const { i18n } = useTranslationContext();
- return (
- <LoadingError
- title={<i18n.Translate>Could not load</i18n.Translate>}
- error={error}
- />
- );
-}
+import { Contact, State } from "./index.js";
export function SelectCurrencyView({
currencies,
@@ -61,7 +52,7 @@ export function SelectCurrencyView({
<p>
<Input>
<SelectList
- label={<i18n.Translate>Known currencies</i18n.Translate>}
+ label={i18n.str`Known currencies`}
list={currencies}
name="lang"
value={""}
@@ -178,7 +169,9 @@ const CircleDiv = styled.div`
text-align: center;
text-decoration: none;
text-transform: uppercase;
- transition: background-color 0.15s ease, border-color 0.15s ease,
+ transition:
+ background-color 0.15s ease,
+ border-color 0.15s ease,
color 0.15s ease;
font-size: 16px;
background-color: #86a7bd1a;
@@ -214,10 +207,11 @@ export function ReadyGetView({
</h1>
<Grid container columns={2} justifyContent="space-between">
<AmountField
- label={<i18n.Translate>Amount</i18n.Translate>}
+ label={i18n.str`Amount`}
required
handler={amountHandler}
/>
+
<Button onClick={selectCurrency.onClick}>
<i18n.Translate>Change currency</i18n.Translate>
</Button>
@@ -283,6 +277,16 @@ export function ReadyGetView({
</Button>
</Paper>
</Grid>
+ <Grid item xs={1}>
+ <Paper style={{ padding: 8 }}>
+ <p>
+ <i18n.Translate>From a <pre style={{display:"inline"}}>taler://peer-push-credit</pre> URI</i18n.Translate>
+ </p>
+ <a href={Pages.qr}>
+ <i18n.Translate>Enter URI here</i18n.Translate>
+ </a>
+ </Paper>
+ </Grid>
</Grid>
</Grid>
</Container>
@@ -293,6 +297,7 @@ export function ReadySendView({
goToBank,
goToWallet,
previous,
+ selectMax,
}: State.Ready): VNode {
const { i18n } = useTranslationContext();
@@ -302,13 +307,18 @@ export function ReadySendView({
<i18n.Translate>Specify the amount and the destination</i18n.Translate>
</h1>
- <div>
+ <Grid container columns={2} justifyContent="space-between">
<AmountField
- label={<i18n.Translate>Amount</i18n.Translate>}
+ label={i18n.str`Amount`}
required
handler={amountHandler}
/>
- </div>
+ <EnabledBySettings name="advancedMode">
+ <Button onClick={selectMax.onClick}>
+ <i18n.Translate>Send all</i18n.Translate>
+ </Button>
+ </EnabledBySettings>
+ </Grid>
<Grid container spacing={1} columns={1}>
{previous.length > 0 ? (
@@ -377,7 +387,6 @@ export function ReadySendView({
</Container>
);
}
-import bankIcon from "../../svg/ri-bank-line.svg";
function RowExample({
info,
diff --git a/packages/taler-wallet-webextension/src/wallet/DeveloperPage.stories.tsx b/packages/taler-wallet-webextension/src/wallet/DeveloperPage.stories.tsx
index d9a5a8fd7..e7c9111fd 100644
--- a/packages/taler-wallet-webextension/src/wallet/DeveloperPage.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/DeveloperPage.stories.tsx
@@ -19,9 +19,9 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { PendingTaskType } from "@gnu-taler/taler-wallet-core";
-import { createExample } from "../test-utils.js";
-import { View as TestedComponent } from "./DeveloperPage.js";
+import { AbsoluteTime } from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
+import { DeveloperPage as TestedComponent } from "./DeveloperPage.js";
export default {
title: "developer",
@@ -31,18 +31,16 @@ export default {
},
};
-export const AllOff = createExample(TestedComponent, {
+export const AllOff = tests.createExample(TestedComponent, {
onDownloadDatabase: async () => "this is the content of the database",
operations: [
{
- id: "",
- type: PendingTaskType.ExchangeUpdate,
+ id: " ",
+ type: "exchange-update",
exchangeBaseUrl: "http://exchange.url.",
givesLifeness: false,
lastError: undefined,
- timestampDue: {
- t_ms: 123123213,
- },
+ timestampDue: AbsoluteTime.fromMilliseconds(123123213),
retryInfo: undefined,
isDue: false,
isLongpolling: false,
diff --git a/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx b/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx
index 2333fd3c1..53380e263 100644
--- a/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx
@@ -15,84 +15,44 @@
*/
import {
+ AbsoluteTime,
Amounts,
CoinDumpJson,
CoinStatus,
ExchangeListItem,
+ ExchangeTosStatus,
+ LogLevel,
NotificationType,
+ ScopeType,
+ parseWithdrawUri,
+ stringifyWithdrawExchange,
} from "@gnu-taler/taler-util";
-import {
- PendingTaskInfo,
- WalletApiOperation,
-} from "@gnu-taler/taler-wallet-core";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { format } from "date-fns";
-import { Fragment, h, VNode } from "preact";
+import { Fragment, VNode, h } from "preact";
import { useEffect, useRef, useState } from "preact/hooks";
-import { Diagnostics } from "../components/Diagnostics.js";
-import { NotifyUpdateFadeOut } from "../components/styled/index.js";
+import { Checkbox } from "../components/Checkbox.js";
+import { SelectList } from "../components/SelectList.js";
import { Time } from "../components/Time.js";
-import { useTranslationContext } from "../context/translation.js";
+import { DestructiveText, LinkPrimary, NotifyUpdateFadeOut, SubTitle, SuccessText, WarningText } from "../components/styled/index.js";
+import { useAlertContext } from "../context/alert.js";
+import { useBackendContext } from "../context/backend.js";
import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
-import { useDiagnostics } from "../hooks/useDiagnostics.js";
+import { useSettings } from "../hooks/useSettings.js";
import { Button } from "../mui/Button.js";
import { Grid } from "../mui/Grid.js";
-import { wxApi } from "../wxApi.js";
-
-export function DeveloperPage(): VNode {
- const [status, timedOut] = useDiagnostics();
-
- const listenAllEvents = Array.from<NotificationType>({ length: 1 });
- //FIXME: waiting for retry notification make a always increasing loop of notifications
- listenAllEvents.includes = (e) => e !== "waiting-for-retry"; // includes every event
-
- const response = useAsyncAsHook(async () => {
- const op = await wxApi.wallet.call(
- WalletApiOperation.GetPendingOperations,
- {},
- );
- const c = await wxApi.wallet.call(WalletApiOperation.DumpCoins, {});
- const ex = await wxApi.wallet.call(WalletApiOperation.ListExchanges, {});
- return {
- operations: op.pendingOperations,
- coins: c.coins,
- exchanges: ex.exchanges,
- };
- });
-
- useEffect(() => {
- return wxApi.listener.onUpdateNotification(
- listenAllEvents,
- response?.retry,
- );
- });
-
- const nonResponse = { operations: [], coins: [], exchanges: [] };
- const { operations, coins, exchanges } =
- response === undefined
- ? nonResponse
- : response.hasError
- ? nonResponse
- : response.response;
-
- return (
- <View
- status={status}
- timedOut={timedOut}
- operations={operations}
- coins={coins}
- exchanges={exchanges}
- onDownloadDatabase={async () => {
- const db = await wxApi.wallet.call(WalletApiOperation.ExportDb, {});
- return JSON.stringify(db);
- }}
- />
- );
-}
+import { Paper } from "../mui/Paper.js";
+import { TextField } from "../mui/TextField.js";
+import { Pages } from "../NavigationBar.js";
+import { CoinInfo } from "@gnu-taler/taler-wallet-core/dbless";
+import { ActiveTasksTable } from "../components/WalletActivity.js";
type CoinsInfo = CoinDumpJson["coins"];
type CalculatedCoinfInfo = {
- ageKeysCount: number | undefined;
+ // ageKeysCount: number | undefined;
denom_value: number;
+ denom_fraction: number;
//remain_value: number;
status: string;
from_refresh: boolean;
@@ -105,42 +65,56 @@ type SplitedCoinInfo = {
};
export interface Props {
- status: any;
- timedOut: boolean;
- operations: PendingTaskInfo[];
- coins: CoinsInfo;
- exchanges: ExchangeListItem[];
- onDownloadDatabase: () => Promise<string>;
+ // FIXME: Pending operations don't exist anymore.
}
function hashObjectId(o: any): string {
return JSON.stringify(o);
}
-export function View({
- status,
- timedOut,
- operations,
- coins,
- onDownloadDatabase,
-}: Props): VNode {
+export function DeveloperPage({ }: Props): VNode {
const { i18n } = useTranslationContext();
const [downloadedDatabase, setDownloadedDatabase] = useState<
{ time: Date; content: string } | undefined
>(undefined);
async function onExportDatabase(): Promise<void> {
- const content = await onDownloadDatabase();
+ const db = await api.wallet.call(WalletApiOperation.ExportDb, {});
+ const content = JSON.stringify(db);
setDownloadedDatabase({
time: new Date(),
content,
});
}
+ const api = useBackendContext();
+
const fileRef = useRef<HTMLInputElement>(null);
async function onImportDatabase(str: string): Promise<void> {
- return wxApi.wallet.call(WalletApiOperation.ImportDb, {
+ await api.wallet.call(WalletApiOperation.ImportDb, {
dump: JSON.parse(str),
});
}
+ const [settings, updateSettings] = useSettings();
+ const { safely } = useAlertContext();
+
+ const listenAllEvents = Array.from<NotificationType>({ length: 1 });
+ // listenAllEvents.includes = () => true
+
+ const hook = useAsyncAsHook(async () => {
+ const list = await api.wallet.call(WalletApiOperation.ListExchanges, {});
+ const version = await api.wallet.call(WalletApiOperation.GetVersion, {});
+ const coins = await api.wallet.call(WalletApiOperation.DumpCoins, {});
+ return { exchanges: list.exchanges, version, coins };
+ });
+ const exchangeList = hook && !hook.hasError ? hook.response.exchanges : [];
+ const coins = hook && !hook.hasError ? hook.response.coins.coins : [];
+
+ useEffect(() => {
+ return api.listener.onUpdateNotification(listenAllEvents, (ev) => {
+ console.log("event", ev)
+ return hook?.retry()
+ });
+ });
+
const currencies: { [ex: string]: string } = {};
const money_by_exchange = coins.reduce(
(prev, cur) => {
@@ -150,8 +124,9 @@ export function View({
currencies[cur.exchange_base_url] = denom.currency;
}
prev[cur.exchange_base_url].push({
- ageKeysCount: cur.ageCommitmentProof?.proof.privateKeys.length,
- denom_value: parseFloat(Amounts.stringifyValue(denom)),
+ // ageKeysCount: cur.ageCommitmentProof?.proof.privateKeys.length,
+ denom_value: denom.value,
+ denom_fraction: denom.fraction,
// remain_value: parseFloat(
// Amounts.stringifyValue(Amounts.parseOrThrow(cur.remaining_value)),
// ),
@@ -165,19 +140,22 @@ export function View({
[exchange_name: string]: CalculatedCoinfInfo[];
},
);
+
+ const [tagName, setTagName] = useState("");
+ const [logLevel, setLogLevel] = useState("info");
return (
<div>
<p>
<i18n.Translate>Debug tools</i18n.Translate>:
</p>
- <Grid container justifyContent="space-between" spacing={1}>
+ <Grid container justifyContent="space-between" spacing={1} size={4}>
<Grid item>
<Button
variant="contained"
onClick={() =>
confirmReset(
i18n.str`Do you want to IRREVOCABLY DESTROY everything inside your wallet and LOSE ALL YOUR COINS?`,
- () => wxApi.background.resetDb(),
+ () => api.background.call("resetDb", undefined),
)
}
>
@@ -190,7 +168,7 @@ export function View({
onClick={() =>
confirmReset(
i18n.str`TESTING: This may delete all your coin, proceed with caution`,
- () => wxApi.background.runGarbageCollector(),
+ () => api.background.call("runGarbageCollector", undefined),
)
}
>
@@ -227,15 +205,46 @@ export function View({
<i18n.Translate>export database</i18n.Translate>
</Button>
</Grid>
+ <Grid item>
+ <Button
+ variant="contained"
+ onClick={async () => {
+ const result = await Promise.all(
+ exchangeList.map(async (exchange) => {
+ const url = exchange.exchangeBaseUrl;
+ const oldKeys = JSON.stringify(
+ await (await fetch(`${url}keys`)).json(),
+ );
+ const newKeys = JSON.stringify(
+ await (
+ await fetch(`${url}keys`, { cache: "no-cache" })
+ ).json(),
+ );
+ return oldKeys !== newKeys;
+ }),
+ );
+ const ex = exchangeList.filter((e, i) => result[i]);
+ if (!ex.length) {
+ alert("no exchange was outdated");
+ } else {
+ alert(`found some exchange out of date: ${result.join(", ")}`);
+ }
+ }}
+ >
+ <i18n.Translate>Clear exchange key cache</i18n.Translate>
+ </Button>
+ </Grid>{" "}
</Grid>
{downloadedDatabase && (
<div>
<i18n.Translate>
- Database exported at
+ Database exported at{" "}
<Time
- timestamp={{ t_ms: downloadedDatabase.time.getTime() }}
+ timestamp={AbsoluteTime.fromMilliseconds(
+ downloadedDatabase.time.getTime(),
+ )}
format="yyyy/MM/dd HH:mm:ss"
- />
+ />{" "}
<a
href={`data:text/plain;charset=utf-8;base64,${toBase64(
downloadedDatabase.content,
@@ -246,11 +255,258 @@ export function View({
)}.json`}
>
<i18n.Translate>click here</i18n.Translate>
- </a>
+ </a>{" "}
to download
</i18n.Translate>
</div>
)}
+ <Checkbox
+ label={i18n.str`Inject Taler support in all pages`}
+ name="inject"
+ description={
+ <i18n.Translate>
+ Enabling this option will make `window.taler` be available in all
+ sites
+ </i18n.Translate>
+ }
+ enabled={settings.injectTalerSupport!}
+ onToggle={safely("update support injection", async () => {
+ updateSettings("injectTalerSupport", !settings.injectTalerSupport);
+ })}
+ />
+
+
+ <SubTitle>
+ <i18n.Translate>Exchange Entries</i18n.Translate>
+ </SubTitle>
+ {!exchangeList || !exchangeList.length ? (
+ <div>
+ <i18n.Translate>No exchange yet</i18n.Translate>
+ </div>
+ ) : (
+ <Fragment>
+ <table>
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>Currency</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>URL</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Status</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Terms of Service</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Last Update</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Actions</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ {exchangeList.map((e, idx) => {
+ function TosStatus(): VNode {
+ switch (e.tosStatus) {
+ case ExchangeTosStatus.Accepted:
+ return (
+ <SuccessText>
+ <i18n.Translate>ok</i18n.Translate>
+ </SuccessText>
+ );
+ case ExchangeTosStatus.Pending:
+ return (
+ <WarningText>
+ <i18n.Translate>pending</i18n.Translate>
+ </WarningText>
+ );
+ case ExchangeTosStatus.Proposed:
+ return <i18n.Translate>proposed</i18n.Translate>;
+ default:
+ return (
+ <DestructiveText>
+ <i18n.Translate>
+ unknown (exchange status should be updated)
+ </i18n.Translate>
+ </DestructiveText>
+ );
+ }
+ }
+ const uri = !e.masterPub ? undefined : stringifyWithdrawExchange({
+ exchangeBaseUrl: e.exchangeBaseUrl,
+ exchangePub: e.masterPub,
+ });
+ return (
+ <tr key={idx}>
+ <td>
+ <a href={!uri ? undefined : Pages.defaultCta({ uri })}>
+ {e.scopeInfo ? `${e.scopeInfo.currency} (${e.scopeInfo.type === ScopeType.Global ? "global" : "regional"})` : e.currency}
+ </a>
+ </td>
+ <td>
+ <a href={new URL(`/keys`, e.exchangeBaseUrl).href} target="_blank">{e.exchangeBaseUrl}</a>
+ </td>
+ <td>
+ {e.exchangeEntryStatus} / {e.exchangeUpdateStatus}
+ </td>
+ <td>
+ <TosStatus />
+ </td>
+ <td>
+ {e.lastUpdateTimestamp
+ ? AbsoluteTime.toIsoString(
+ AbsoluteTime.fromPreciseTimestamp(
+ e.lastUpdateTimestamp,
+ ),
+ )
+ : "never"}
+ </td>
+ <td>
+ <button
+ onClick={() => {
+ api.wallet.call(
+ WalletApiOperation.UpdateExchangeEntry,
+ {
+ exchangeBaseUrl: e.exchangeBaseUrl,
+ force: true,
+ },
+ );
+ }}
+ >
+ Reload
+ </button>
+ <button
+ onClick={() => {
+ api.wallet.call(
+ WalletApiOperation.DeleteExchange,
+ {
+ exchangeBaseUrl: e.exchangeBaseUrl,
+ },
+ );
+ }}
+ >
+ Delete
+ </button>
+ <button
+ onClick={() => {
+ api.wallet.call(
+ WalletApiOperation.DeleteExchange,
+ {
+ exchangeBaseUrl: e.exchangeBaseUrl,
+ purge: true,
+ },
+ );
+ }}
+ >
+ Purge
+ </button>
+ {e.scopeInfo && e.masterPub && e.currency ?
+ (e.scopeInfo.type === ScopeType.Global ?
+ <button
+ onClick={() => {
+ api.wallet.call(
+ WalletApiOperation.RemoveGlobalCurrencyExchange,
+ {
+ exchangeBaseUrl: e.exchangeBaseUrl,
+ currency: e.currency!,
+ exchangeMasterPub: e.masterPub!,
+ },
+ );
+ }}
+ >
+
+ Make regional
+ </button>
+ : e.scopeInfo.type === ScopeType.Auditor ?
+ undefined
+
+ : e.scopeInfo.type === ScopeType.Exchange ?
+ <button
+ onClick={() => {
+ api.wallet.call(
+ WalletApiOperation.AddGlobalCurrencyExchange,
+ {
+ exchangeBaseUrl: e.exchangeBaseUrl,
+ currency: e.currency!,
+ exchangeMasterPub: e.masterPub!,
+ },
+ );
+ }}
+ >
+
+ Make global
+ </button>
+ : undefined) : undefined
+ }
+ <button
+ onClick={() => {
+ api.wallet.call(
+ WalletApiOperation.SetExchangeTosForgotten,
+ {
+ exchangeBaseUrl: e.exchangeBaseUrl,
+ },
+ );
+ }}
+ >
+ Forget ToS
+ </button>
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </Fragment>
+ )}
+ <div style={{ display: "flex", justifyContent: "space-between" }}>
+ <div />
+ <LinkPrimary href={Pages.settingsExchangeAdd({})}>
+ <i18n.Translate>Add an exchange</i18n.Translate>
+ </LinkPrimary>
+ </div>
+
+
+ <Paper style={{ padding: 10, margin: 10 }}>
+ <h3>Logging</h3>
+ <div>
+ <TextField
+ label="Tag name"
+ placeholder="wallet.ts"
+ variant="filled"
+ // error={subject.error}
+ required
+ value={tagName}
+ onChange={setTagName}
+ />
+ <SelectList
+ label={i18n.str`Log levels`}
+ list={{
+ trace: "TRACE",
+ info: "INFO",
+ error: "ERROR",
+ }}
+ name="logLevel"
+ value={logLevel}
+ onChange={(v) => setLogLevel(v)}
+ />
+ </div>
+ <Button
+ variant="contained"
+ onClick={async () => {
+ api.background.call("setLoggingLevel", {
+ tag: tagName,
+ level: logLevel as LogLevel,
+ });
+ }}
+ >
+ Set log level
+ </Button>
+ </Paper>
+
<br />
<p>
<i18n.Translate>Coins</i18n.Translate>:
@@ -258,7 +514,10 @@ export function View({
{Object.keys(money_by_exchange).map((ex, idx) => {
const allcoins = money_by_exchange[ex];
allcoins.sort((a, b) => {
- return b.denom_value - a.denom_value;
+ if (b.denom_value !== a.denom_value) {
+ return b.denom_value - a.denom_value;
+ }
+ return b.denom_fraction - a.denom_fraction;
});
const coins = allcoins.reduce(
@@ -283,32 +542,9 @@ export function View({
);
})}
<br />
- <Diagnostics diagnostics={status} timedOut={timedOut} />
- {operations && operations.length > 0 && (
- <Fragment>
- <p>
- <i18n.Translate>Pending operations</i18n.Translate>
- </p>
- <dl>
- {operations.reverse().map((o) => {
- return (
- <NotifyUpdateFadeOut key={hashObjectId(o)}>
- <dt>
- {o.type}{" "}
- <Time
- timestamp={o.timestampDue}
- format="yy/MM/dd HH:mm:ss"
- />
- </dt>
- <dd>
- <pre>{JSON.stringify(o, undefined, 2)}</pre>
- </dd>
- </NotifyUpdateFadeOut>
- );
- })}
- </dl>
- </Fragment>
- )}
+ <NotifyUpdateFadeOut>
+ <ActiveTasksTable />
+ </NotifyUpdateFadeOut>
</div>
);
}
@@ -325,11 +561,31 @@ function ShowAllCoins({
const { i18n } = useTranslationContext();
const [collapsedSpent, setCollapsedSpent] = useState(true);
const [collapsedUnspent, setCollapsedUnspent] = useState(false);
- const total = coins.usable.reduce((prev, cur) => prev + cur.denom_value, 0);
+ const totalUsable = coins.usable.reduce(
+ (prev, cur) =>
+ Amounts.add(prev, {
+ currency: "NONE",
+ fraction: cur.denom_fraction,
+ value: cur.denom_value,
+ }).amount,
+ Amounts.zeroOfCurrency("NONE"),
+ );
+ const totalSpent = coins.spent.reduce(
+ (prev, cur) =>
+ Amounts.add(prev, {
+ currency: "NONE",
+ fraction: cur.denom_fraction,
+ value: cur.denom_value,
+ }).amount,
+ Amounts.zeroOfCurrency("NONE"),
+ );
return (
<Fragment>
<p>
- <b>{ex}</b>: {total} {currencies[ex]}
+ <b>{ex}</b>: {Amounts.stringifyValue(totalUsable)} {currencies[ex]}
+ </p>
+ <p>
+ spent: {Amounts.stringifyValue(totalSpent)} {currencies[ex]}
</p>
<p onClick={() => setCollapsedUnspent(true)}>
<b>
@@ -348,9 +604,6 @@ function ShowAllCoins({
<i18n.Translate>denom</i18n.Translate>
</td>
<td>
- <i18n.Translate>value</i18n.Translate>
- </td>
- <td>
<i18n.Translate>status</i18n.Translate>
</td>
<td>
@@ -364,10 +617,16 @@ function ShowAllCoins({
return (
<tr key={idx}>
<td>{c.id.substring(0, 5)}</td>
- <td>{c.denom_value}</td>
+ <td>
+ {Amounts.stringifyValue({
+ value: c.denom_value,
+ fraction: c.denom_fraction,
+ currency: "ANY",
+ })}
+ </td>
<td>{c.status}</td>
<td>{c.from_refresh ? "true" : "false"}</td>
- <td>{String(c.ageKeysCount)}</td>
+ {/* <td>{String(c.ageKeysCount)}</td> */}
</tr>
);
})}
@@ -390,9 +649,6 @@ function ShowAllCoins({
<i18n.Translate>denom</i18n.Translate>
</td>
<td>
- <i18n.Translate>value</i18n.Translate>
- </td>
- <td>
<i18n.Translate>status</i18n.Translate>
</td>
<td>
diff --git a/packages/taler-wallet-webextension/src/wallet/EmptyComponentExample/index.ts b/packages/taler-wallet-webextension/src/wallet/EmptyComponentExample/index.ts
index 4b7725264..afbaf1945 100644
--- a/packages/taler-wallet-webextension/src/wallet/EmptyComponentExample/index.ts
+++ b/packages/taler-wallet-webextension/src/wallet/EmptyComponentExample/index.ts
@@ -14,12 +14,12 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+import { ErrorAlertView } from "../../components/CurrentAlerts.js";
import { Loading } from "../../components/Loading.js";
-import { HookError } from "../../hooks/useAsyncAsHook.js";
+import { ErrorAlert } from "../../context/alert.js";
import { compose, StateViewMap } from "../../utils/index.js";
-import { wxApi } from "../../wxApi.js";
import { useComponentState } from "./state.js";
-import { LoadingUriView, ReadyView } from "./views.js";
+import { ReadyView } from "./views.js";
export interface Props {
p: string;
@@ -34,8 +34,8 @@ export namespace State {
}
export interface LoadingUriError {
- status: "loading-error";
- error: HookError;
+ status: "error";
+ error: ErrorAlert;
}
export interface BaseInfo {
@@ -49,12 +49,12 @@ export namespace State {
const viewMapping: StateViewMap<State> = {
loading: Loading,
- "loading-error": LoadingUriView,
+ error: ErrorAlertView,
ready: ReadyView,
};
export const ComponentName = compose(
"ComponentName",
- (p: Props) => useComponentState(p, wxApi),
+ (p: Props) => useComponentState(p),
viewMapping,
);
diff --git a/packages/taler-wallet-webextension/src/wallet/EmptyComponentExample/state.ts b/packages/taler-wallet-webextension/src/wallet/EmptyComponentExample/state.ts
index d194b3f97..31a351579 100644
--- a/packages/taler-wallet-webextension/src/wallet/EmptyComponentExample/state.ts
+++ b/packages/taler-wallet-webextension/src/wallet/EmptyComponentExample/state.ts
@@ -14,10 +14,9 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { wxApi } from "../../wxApi.js";
import { Props, State } from "./index.js";
-export function useComponentState({ p }: Props, api: typeof wxApi): State {
+export function useComponentState({ p }: Props): State {
return {
status: "ready",
error: undefined,
diff --git a/packages/taler-wallet-webextension/src/wallet/EmptyComponentExample/stories.tsx b/packages/taler-wallet-webextension/src/wallet/EmptyComponentExample/stories.tsx
index 696e424c4..628e97c02 100644
--- a/packages/taler-wallet-webextension/src/wallet/EmptyComponentExample/stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/EmptyComponentExample/stories.tsx
@@ -19,11 +19,11 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { createExample } from "../../test-utils.js";
+import * as tests from "@gnu-taler/web-util/testing";
import { ReadyView } from "./views.js";
export default {
title: "example",
};
-export const Ready = createExample(ReadyView, {});
+export const Ready = tests.createExample(ReadyView, {});
diff --git a/packages/taler-wallet-webextension/src/wallet/EmptyComponentExample/views.tsx b/packages/taler-wallet-webextension/src/wallet/EmptyComponentExample/views.tsx
index 5784a7db5..a98bfef60 100644
--- a/packages/taler-wallet-webextension/src/wallet/EmptyComponentExample/views.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/EmptyComponentExample/views.tsx
@@ -15,21 +15,9 @@
*/
import { h, VNode } from "preact";
-import { LoadingError } from "../../components/LoadingError.js";
-import { useTranslationContext } from "../../context/translation.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { State } from "./index.js";
-export function LoadingUriView({ error }: State.LoadingUriError): VNode {
- const { i18n } = useTranslationContext();
-
- return (
- <LoadingError
- title={<i18n.Translate>Could not load</i18n.Translate>}
- error={error}
- />
- );
-}
-
export function ReadyView({ error }: State.Ready): VNode {
const { i18n } = useTranslationContext();
diff --git a/packages/taler-wallet-webextension/src/wallet/ExchangeAddConfirm.tsx b/packages/taler-wallet-webextension/src/wallet/ExchangeAddConfirm.tsx
deleted file mode 100644
index 2118c8984..000000000
--- a/packages/taler-wallet-webextension/src/wallet/ExchangeAddConfirm.tsx
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-import { Fragment, h, VNode } from "preact";
-import { useState } from "preact/hooks";
-import { Title } from "../components/styled/index.js";
-import { TermsOfService } from "../components/TermsOfService/index.js";
-import { useTranslationContext } from "../context/translation.js";
-import { Button } from "../mui/Button.js";
-
-export interface Props {
- url: string;
- onCancel: () => Promise<void>;
- onConfirm: () => Promise<void>;
-}
-
-export function ExchangeAddConfirmPage({
- url,
- onCancel,
- onConfirm,
-}: Props): VNode {
- const { i18n } = useTranslationContext();
-
- const [accepted, setAccepted] = useState(false);
-
- return (
- <Fragment>
- <section>
- <Title>
- <i18n.Translate>Review terms of service</i18n.Translate>
- </Title>
- <div>
- <i18n.Translate>Exchange URL</i18n.Translate>:
- <a href={url} target="_blank" rel="noreferrer">
- {url}
- </a>
- </div>
- </section>
-
- <TermsOfService key="terms" exchangeUrl={url} onChange={setAccepted} />
-
- <footer>
- <Button
- key="cancel"
- variant="contained"
- color="secondary"
- onClick={onCancel}
- >
- <i18n.Translate>Cancel</i18n.Translate>
- </Button>
- <Button
- key="add"
- variant="contained"
- color="success"
- disabled={!accepted}
- onClick={onConfirm}
- >
- <i18n.Translate>Add exchange</i18n.Translate>
- </Button>
- </footer>
- </Fragment>
- );
-}
diff --git a/packages/taler-wallet-webextension/src/wallet/ExchangeAddPage.tsx b/packages/taler-wallet-webextension/src/wallet/ExchangeAddPage.tsx
deleted file mode 100644
index a0c62787a..000000000
--- a/packages/taler-wallet-webextension/src/wallet/ExchangeAddPage.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-import {
- canonicalizeBaseUrl,
- TalerConfigResponse,
-} from "@gnu-taler/taler-util";
-import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-import { h, VNode } from "preact";
-import { useState } from "preact/hooks";
-import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
-import { queryToSlashKeys } from "../utils/index.js";
-import { wxApi } from "../wxApi.js";
-import { ExchangeAddConfirmPage } from "./ExchangeAddConfirm.js";
-import { ExchangeSetUrlPage } from "./ExchangeSetUrl.js";
-
-interface Props {
- currency?: string;
- onBack: () => Promise<void>;
-}
-
-export function ExchangeAddPage({ currency, onBack }: Props): VNode {
- const [verifying, setVerifying] = useState<
- { url: string; config: TalerConfigResponse } | undefined
- >(undefined);
-
- const knownExchangesResponse = useAsyncAsHook(() =>
- wxApi.wallet.call(WalletApiOperation.ListExchanges, {}),
- );
- const knownExchanges = !knownExchangesResponse
- ? []
- : knownExchangesResponse.hasError
- ? []
- : knownExchangesResponse.response.exchanges;
-
- if (!verifying) {
- return (
- <ExchangeSetUrlPage
- onCancel={onBack}
- expectedCurrency={currency}
- onVerify={async (url) => {
- const found =
- knownExchanges.findIndex((e) => e.exchangeBaseUrl === url) !== -1;
-
- if (found) {
- throw Error("This exchange is already known");
- }
- return queryToSlashKeys(url);
- }}
- onConfirm={(url) =>
- queryToSlashKeys<TalerConfigResponse>(url)
- .then((config) => {
- setVerifying({ url, config });
- })
- .catch((e) => e.message)
- }
- />
- );
- }
- return (
- <ExchangeAddConfirmPage
- url={verifying.url}
- onCancel={onBack}
- onConfirm={async () => {
- await wxApi.wallet.call(WalletApiOperation.AddExchange, {
- exchangeBaseUrl: canonicalizeBaseUrl(verifying.url),
- forceUpdate: true,
- });
- onBack();
- }}
- />
- );
-}
diff --git a/packages/taler-wallet-webextension/src/wallet/ExchangeAddSetUrl.stories.tsx b/packages/taler-wallet-webextension/src/wallet/ExchangeAddSetUrl.stories.tsx
deleted file mode 100644
index cd86ad8c6..000000000
--- a/packages/taler-wallet-webextension/src/wallet/ExchangeAddSetUrl.stories.tsx
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import { createExample } from "../test-utils.js";
-import { queryToSlashKeys } from "../utils/index.js";
-import { ExchangeSetUrlPage as TestedComponent } from "./ExchangeSetUrl.js";
-
-export default {
- title: "exchange add set url",
-};
-
-export const ExpectedUSD = createExample(TestedComponent, {
- expectedCurrency: "USD",
- onVerify: queryToSlashKeys,
-});
-
-export const ExpectedKUDOS = createExample(TestedComponent, {
- expectedCurrency: "KUDOS",
- onVerify: queryToSlashKeys,
-});
-
-export const InitialState = createExample(TestedComponent, {
- onVerify: queryToSlashKeys,
-});
-
-const knownExchanges = [
- {
- currency: "TESTKUDOS",
- exchangeBaseUrl: "https://exchange.demo.taler.net/",
- tos: {
- currentVersion: "1",
- acceptedVersion: "1",
- content: "content of tos",
- contentType: "text/plain",
- },
- paytoUris: [],
- },
-];
-
-export const WithDemoAsKnownExchange = createExample(TestedComponent, {
- onVerify: async (url) => {
- const found =
- knownExchanges.findIndex((e) => e.exchangeBaseUrl === url) !== -1;
-
- if (found) {
- throw Error("This exchange is already known");
- }
- return queryToSlashKeys(url);
- },
-});
diff --git a/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/index.ts b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/index.ts
index a95830f8e..d711f1ecc 100644
--- a/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/index.ts
+++ b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/index.ts
@@ -20,16 +20,16 @@ import {
ExchangeListItem,
FeeDescriptionPair,
} from "@gnu-taler/taler-util";
+import { ErrorAlertView } from "../../components/CurrentAlerts.js";
import { Loading } from "../../components/Loading.js";
+import { ErrorAlert } from "../../context/alert.js";
import { HookError } from "../../hooks/useAsyncAsHook.js";
import { State as SelectExchangeState } from "../../hooks/useSelectedExchange.js";
import { ButtonHandler, SelectFieldHandler } from "../../mui/handlers.js";
import { compose, StateViewMap } from "../../utils/index.js";
-import { wxApi } from "../../wxApi.js";
import { useComponentState } from "./state.js";
import {
ComparingView,
- ErrorLoadingView,
NoExchangesView,
PrivacyContentView,
ReadyView,
@@ -38,7 +38,7 @@ import {
export interface Props {
list: ExchangeListItem[];
- currentExchange: string;
+ initialValue: string;
onCancel: () => Promise<void>;
onSelection: (exchange: string) => Promise<void>;
}
@@ -50,7 +50,7 @@ export type State =
| State.Comparing
| State.ShowingTos
| State.ShowingPrivacy
- | SelectExchangeState.NoExchange;
+ | SelectExchangeState.NoExchangeFound;
export namespace State {
export interface Loading {
@@ -59,8 +59,8 @@ export namespace State {
}
export interface LoadingUriError {
- status: "error-loading";
- error: HookError;
+ status: "error";
+ error: ErrorAlert;
}
export interface BaseInfo {
@@ -78,7 +78,11 @@ export namespace State {
export interface Comparing extends BaseInfo {
status: "comparing";
- pairTimeline: DenomOperationMap<FeeDescriptionPair[]>;
+ coinOperationTimeline: DenomOperationMap<FeeDescriptionPair[]>;
+ wireFeeTimeline: Record<string, FeeDescriptionPair[]>;
+ globalFeeTimeline: FeeDescriptionPair[];
+ missingWireTYpe: string[];
+ newWireType: string[];
onReset: ButtonHandler;
onSelect: ButtonHandler;
}
@@ -96,9 +100,9 @@ export namespace State {
const viewMapping: StateViewMap<State> = {
loading: Loading,
- "error-loading": ErrorLoadingView,
+ error: ErrorAlertView,
comparing: ComparingView,
- "no-exchange": NoExchangesView,
+ "no-exchange-found": NoExchangesView,
"showing-tos": TosContentView,
"showing-privacy": PrivacyContentView,
ready: ReadyView,
@@ -106,6 +110,6 @@ const viewMapping: StateViewMap<State> = {
export const ExchangeSelectionPage = compose(
"ExchangeSelectionPage",
- (p: Props) => useComponentState(p, wxApi),
+ (p: Props) => useComponentState(p),
viewMapping,
);
diff --git a/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/state.ts b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/state.ts
index 63d545b97..d70b62de0 100644
--- a/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/state.ts
+++ b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/state.ts
@@ -16,44 +16,51 @@
import { DenomOperationMap, FeeDescription } from "@gnu-taler/taler-util";
import {
- createPairTimeline,
WalletApiOperation,
+ createPairTimeline,
} from "@gnu-taler/taler-wallet-core";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { useState } from "preact/hooks";
+import { alertFromError, useAlertContext } from "../../context/alert.js";
+import { useBackendContext } from "../../context/backend.js";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
-import { wxApi } from "../../wxApi.js";
import { Props, State } from "./index.js";
-export function useComponentState(
- { onCancel, onSelection, list: exchanges, currentExchange }: Props,
- api: typeof wxApi,
-): State {
- const initialValue = exchanges.findIndex(
- (e) => e.exchangeBaseUrl === currentExchange,
+export function useComponentState({
+ onCancel,
+ onSelection,
+ list: exchanges,
+ initialValue,
+}: Props): State {
+ const api = useBackendContext();
+ const { pushAlertOnError } = useAlertContext();
+ const { i18n } = useTranslationContext();
+ const initialValueIdx = exchanges.findIndex(
+ (e) => e.exchangeBaseUrl === initialValue,
);
- if (initialValue === -1) {
+ if (initialValueIdx === -1) {
throw Error(
- `wrong usage of ExchangeSelection component, currentExchange '${currentExchange}' is not in the list of exchanges`,
+ `wrong usage of ExchangeSelection component, currentExchange '${initialValue}' is not in the list of exchanges`,
);
}
- const [value, setValue] = useState(String(initialValue));
+ const [value, setValue] = useState(String(initialValueIdx));
const selectedIdx = parseInt(value, 10);
- const selectedExchange =
- exchanges.length == 0 ? undefined : exchanges[selectedIdx];
+ const selectedExchange = exchanges[selectedIdx];
- const comparingExchanges = selectedIdx !== initialValue;
+ const comparingExchanges = selectedIdx !== initialValueIdx;
const initialExchange = comparingExchanges
- ? exchanges[initialValue]
+ ? exchanges[initialValueIdx]
: undefined;
const hook = useAsyncAsHook(async () => {
- const selected = !selectedExchange
- ? undefined
- : await api.wallet.call(WalletApiOperation.GetExchangeDetailedInfo, {
- exchangeBaseUrl: selectedExchange.exchangeBaseUrl,
- });
+ const selected = await api.wallet.call(
+ WalletApiOperation.GetExchangeDetailedInfo,
+ {
+ exchangeBaseUrl: selectedExchange.exchangeBaseUrl,
+ },
+ );
const original = !initialExchange
? undefined
@@ -63,7 +70,7 @@ export function useComponentState(
return {
exchanges,
- selected: selected?.exchange,
+ selected: selected.exchange,
original: original?.exchange,
};
}, [selectedExchange, initialExchange]);
@@ -81,21 +88,17 @@ export function useComponentState(
}
if (hook.hasError) {
return {
- status: "error-loading",
- error: hook,
+ status: "error",
+ error: alertFromError(
+ i18n,
+ i18n.str`Could not load exchange details info`,
+ hook,
+ ),
};
}
const { selected, original } = hook.response;
- if (selectedExchange === undefined || !selected) {
- return {
- status: "no-exchange",
- error: undefined,
- currency: undefined,
- };
- }
-
const exchangeMap = exchanges.reduce(
(prev, cur, idx) => ({ ...prev, [String(idx)]: cur.exchangeBaseUrl }),
{} as Record<string, string>,
@@ -104,9 +107,8 @@ export function useComponentState(
if (showingPrivacy) {
return {
status: "showing-privacy",
- error: undefined,
onClose: {
- onClick: async () => setShowingPrivacy(undefined),
+ onClick: pushAlertOnError(async () => setShowingPrivacy(undefined)),
},
exchangeUrl: showingPrivacy,
};
@@ -114,9 +116,8 @@ export function useComponentState(
if (showingTos) {
return {
status: "showing-tos",
- error: undefined,
onClose: {
- onClick: async () => setShowingTos(undefined),
+ onClick: pushAlertOnError(async () => setShowingTos(undefined)),
},
exchangeUrl: showingTos,
};
@@ -129,30 +130,30 @@ export function useComponentState(
exchanges: {
list: exchangeMap,
value: value,
- onChange: async (v) => {
+ onChange: pushAlertOnError(async (v) => {
setValue(v);
- },
+ }),
},
error: undefined,
onClose: {
- onClick: onCancel,
+ onClick: pushAlertOnError(onCancel),
},
selected,
onShowPrivacy: {
- onClick: async () => {
+ onClick: pushAlertOnError(async () => {
setShowingPrivacy(selected.exchangeBaseUrl);
- },
+ }),
},
onShowTerms: {
- onClick: async () => {
+ onClick: pushAlertOnError(async () => {
setShowingTos(selected.exchangeBaseUrl);
- },
+ }),
},
};
}
- //this may be expensive, useMemo
- const pairTimeline: DenomOperationMap<FeeDescription[]> = {
+ // this may be expensive, useMemo
+ const coinOperationTimeline: DenomOperationMap<FeeDescription[]> = {
deposit: createPairTimeline(
selected.denomFees.deposit,
original.denomFees.deposit,
@@ -171,37 +172,71 @@ export function useComponentState(
),
};
+ const globalFeeTimeline = createPairTimeline(
+ selected.globalFees,
+ original.globalFees,
+ );
+
+ const allWireType = Object.keys(selected.transferFees).concat(
+ Object.keys(original.transferFees),
+ );
+
+ const wireFeeTimeline: Record<string, FeeDescription[]> = {};
+
+ const missingWireTYpe: string[] = [];
+ const newWireType: string[] = [];
+
+ for (const wire of allWireType) {
+ const selectedWire = selected.transferFees[wire];
+ const originalWire = original.transferFees[wire];
+
+ if (!selectedWire) {
+ newWireType.push(wire);
+ continue;
+ }
+ if (!originalWire) {
+ missingWireTYpe.push(wire);
+ continue;
+ }
+
+ wireFeeTimeline[wire] = createPairTimeline(selectedWire, originalWire);
+ }
+
return {
status: "comparing",
exchanges: {
list: exchangeMap,
value: value,
- onChange: async (v) => {
+ onChange: pushAlertOnError(async (v) => {
setValue(v);
- },
+ }),
},
error: undefined,
onReset: {
- onClick: async () => {
+ onClick: pushAlertOnError(async () => {
setValue(String(initialValue));
- },
+ }),
},
onSelect: {
- onClick: async () => {
+ onClick: pushAlertOnError(async () => {
onSelection(selected.exchangeBaseUrl);
- },
+ }),
},
onShowPrivacy: {
- onClick: async () => {
+ onClick: pushAlertOnError(async () => {
setShowingPrivacy(selected.exchangeBaseUrl);
- },
+ }),
},
onShowTerms: {
- onClick: async () => {
+ onClick: pushAlertOnError(async () => {
setShowingTos(selected.exchangeBaseUrl);
- },
+ }),
},
selected,
- pairTimeline,
+ coinOperationTimeline,
+ wireFeeTimeline,
+ globalFeeTimeline,
+ missingWireTYpe,
+ newWireType,
};
}
diff --git a/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/stories.tsx b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/stories.tsx
index 3706359a8..990e2790f 100644
--- a/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/stories.tsx
@@ -19,14 +19,19 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { createExample } from "../../test-utils.js";
-import { ComparingView, ReadyView } from "./views.js";
+import * as tests from "@gnu-taler/web-util/testing";
+import { ComparingView, ReadyView, NoExchangesView } from "./views.js";
export default {
title: "select exchange",
};
-export const Bitcoin1 = createExample(ReadyView, {
+export const NoExchangeFound = tests.createExample(NoExchangesView, {
+ currency: "USD",
+ defaultExchange: "https://exchange.taler.ar",
+});
+
+export const Bitcoin1 = tests.createExample(ReadyView, {
exchanges: {
list: { "0": "https://exchange.taler.ar" },
value: "0",
@@ -43,7 +48,7 @@ export const Bitcoin1 = createExample(ReadyView, {
onShowTerms: {},
onClose: {},
});
-export const Bitcoin2 = createExample(ReadyView, {
+export const Bitcoin2 = tests.createExample(ReadyView, {
exchanges: {
list: {
"https://exchange.taler.ar": "https://exchange.taler.ar",
@@ -64,7 +69,7 @@ export const Bitcoin2 = createExample(ReadyView, {
onClose: {},
});
-export const Kudos1 = createExample(ReadyView, {
+export const Kudos1 = tests.createExample(ReadyView, {
exchanges: {
list: {
"https://exchange-kudos.taler.ar": "https://exchange-kudos.taler.ar",
@@ -83,7 +88,7 @@ export const Kudos1 = createExample(ReadyView, {
onShowTerms: {},
onClose: {},
});
-export const Kudos2 = createExample(ReadyView, {
+export const Kudos2 = tests.createExample(ReadyView, {
exchanges: {
list: {
"https://exchange-kudos.taler.ar": "https://exchange-kudos.taler.ar",
@@ -103,7 +108,7 @@ export const Kudos2 = createExample(ReadyView, {
onShowTerms: {},
onClose: {},
});
-export const ComparingBitcoin = createExample(ComparingView, {
+export const ComparingBitcoin = tests.createExample(ComparingView, {
exchanges: {
list: { "http://exchange": "http://exchange" },
value: "http://exchange",
@@ -120,14 +125,18 @@ export const ComparingBitcoin = createExample(ComparingView, {
onShowTerms: {},
onSelect: {},
error: undefined,
- pairTimeline: {
+ coinOperationTimeline: {
deposit: [],
refresh: [],
refund: [],
withdraw: [],
},
+ globalFeeTimeline: [],
+ newWireType: [],
+ missingWireTYpe: [],
+ wireFeeTimeline: {},
});
-export const ComparingKudos = createExample(ComparingView, {
+export const ComparingKudos = tests.createExample(ComparingView, {
exchanges: {
list: { "http://exchange": "http://exchange" },
value: "http://exchange",
@@ -144,12 +153,16 @@ export const ComparingKudos = createExample(ComparingView, {
onShowTerms: {},
onSelect: {},
error: undefined,
- pairTimeline: {
+ coinOperationTimeline: {
deposit: [],
refresh: [],
refund: [],
withdraw: [],
},
+ globalFeeTimeline: [],
+ newWireType: [],
+ missingWireTYpe: [],
+ wireFeeTimeline: {},
});
function timelineExample() {
diff --git a/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/views.tsx b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/views.tsx
index 95ab55261..6f67d84b7 100644
--- a/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/views.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/views.tsx
@@ -19,16 +19,16 @@ import { styled } from "@linaria/react";
import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
import { Amount } from "../../components/Amount.js";
+import { AlertView } from "../../components/CurrentAlerts.js";
import { ErrorMessage } from "../../components/ErrorMessage.js";
-import { LoadingError } from "../../components/LoadingError.js";
import { SelectList } from "../../components/SelectList.js";
import { Input, SvgIcon } from "../../components/styled/index.js";
import { TermsOfService } from "../../components/TermsOfService/index.js";
import { Time } from "../../components/Time.js";
-import { useTranslationContext } from "../../context/translation.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { State as SelectExchangeState } from "../../hooks/useSelectedExchange.js";
import { Button } from "../../mui/Button.js";
-import arrowDown from "../../svg/chevron-down.svg";
+import arrowDown from "../../svg/chevron-down.inline.svg";
import { State } from "./index.js";
const ButtonGroup = styled.div`
@@ -110,17 +110,6 @@ const Container = styled.div`
}
`;
-export function ErrorLoadingView({ error }: State.LoadingUriError): VNode {
- const { i18n } = useTranslationContext();
-
- return (
- <LoadingError
- title={<i18n.Translate>Could not load exchange fees</i18n.Translate>}
- error={error}
- />
- );
-}
-
export function PrivacyContentView({
exchangeUrl,
onClose,
@@ -146,30 +135,41 @@ export function TosContentView({
<Button variant="outlined" onClick={onClose.onClick}>
<i18n.Translate>Close</i18n.Translate>
</Button>
- <TermsOfService exchangeUrl={exchangeUrl} />
+ <TermsOfService exchangeUrl={exchangeUrl} readOnly >
+ s
+ </TermsOfService>
</div>
);
}
export function NoExchangesView({
+ defaultExchange,
currency,
-}: SelectExchangeState.NoExchange): VNode {
+}: SelectExchangeState.NoExchangeFound): VNode {
const { i18n } = useTranslationContext();
- if (!currency) {
- return (
- <ErrorMessage
- title={<i18n.Translate>Could not find any exchange</i18n.Translate>}
- />
- );
- }
return (
- <ErrorMessage
- title={
- <i18n.Translate>
- Could not find any exchange for the currency {currency}
- </i18n.Translate>
- }
- />
+ <Fragment>
+ <p>
+ <AlertView
+ alert={{
+ type: "error",
+ message: i18n.str`There is no exchange available for currency ${currency}`,
+ description: i18n.str`You can add more exchanges from the settings.`,
+ cause: undefined,
+ context: undefined,
+ }}
+ />
+ </p>
+ {defaultExchange && (
+ <AlertView
+ alert={{
+ type: "warning",
+ message: i18n.str`Exchange ${defaultExchange} is not available`,
+ description: i18n.str`Exchange status can view accessed from the settings.`,
+ }}
+ />
+ )}
+ </Fragment>
);
}
@@ -178,7 +178,11 @@ export function ComparingView({
selected,
onReset,
onSelect,
- pairTimeline,
+ coinOperationTimeline,
+ globalFeeTimeline,
+ wireFeeTimeline,
+ missingWireTYpe,
+ newWireType,
onShowPrivacy,
onShowTerms,
}: State.Comparing): VNode {
@@ -249,9 +253,16 @@ export function ComparingView({
</section>
<section>
<h2>
- <i18n.Translate>Operations</i18n.Translate>
+ <i18n.Translate>Coin operations</i18n.Translate>
</h2>
<p>
+ <i18n.Translate>
+ Every operation in this section may be different by denomination
+ value and is valid for a period of time. The exchange will charge
+ the indicated amount every time a coin is used in such operation.
+ </i18n.Translate>
+ </p>
+ <p>
<i18n.Translate>Deposits</i18n.Translate>
</p>
<FeeDescriptionTable>
@@ -274,7 +285,7 @@ export function ComparingView({
</thead>
<tbody>
<RenderFeePairByValue
- list={pairTimeline.deposit}
+ list={coinOperationTimeline.deposit}
sorting={(a, b) => Number(a) - Number(b)}
/>
</tbody>
@@ -290,7 +301,10 @@ export function ComparingView({
<i18n.Translate>Denomination</i18n.Translate>
</th>
<th class="fee">
- <i18n.Translate>Fee</i18n.Translate>
+ <i18n.Translate>Current</i18n.Translate>
+ </th>
+ <th class="fee">
+ <i18n.Translate>Selected</i18n.Translate>
</th>
<th>
<i18n.Translate>Until</i18n.Translate>
@@ -299,7 +313,7 @@ export function ComparingView({
</thead>
<tbody>
<RenderFeePairByValue
- list={pairTimeline.withdraw}
+ list={coinOperationTimeline.withdraw}
sorting={(a, b) => Number(a) - Number(b)}
/>
</tbody>
@@ -315,7 +329,10 @@ export function ComparingView({
<i18n.Translate>Denomination</i18n.Translate>
</th>
<th class="fee">
- <i18n.Translate>Fee</i18n.Translate>
+ <i18n.Translate>Current</i18n.Translate>
+ </th>
+ <th class="fee">
+ <i18n.Translate>Selected</i18n.Translate>
</th>
<th>
<i18n.Translate>Until</i18n.Translate>
@@ -324,7 +341,7 @@ export function ComparingView({
</thead>
<tbody>
<RenderFeePairByValue
- list={pairTimeline.refund}
+ list={coinOperationTimeline.refund}
sorting={(a, b) => Number(a) - Number(b)}
/>
</tbody>
@@ -340,7 +357,10 @@ export function ComparingView({
<i18n.Translate>Denomination</i18n.Translate>
</th>
<th class="fee">
- <i18n.Translate>Fee</i18n.Translate>
+ <i18n.Translate>Current</i18n.Translate>
+ </th>
+ <th class="fee">
+ <i18n.Translate>Selected</i18n.Translate>
</th>
<th>
<i18n.Translate>Until</i18n.Translate>
@@ -349,13 +369,141 @@ export function ComparingView({
</thead>
<tbody>
<RenderFeePairByValue
- list={pairTimeline.refresh}
+ list={coinOperationTimeline.refresh}
sorting={(a, b) => Number(a) - Number(b)}
/>
</tbody>
</FeeDescriptionTable>{" "}
</section>
<section>
+ <h2>
+ <i18n.Translate>Transfer operations</i18n.Translate>
+ </h2>
+ <p>
+ <i18n.Translate>
+ Every operation in this section may be different by transfer type
+ and is valid for a period of time. The exchange will charge the
+ indicated amount every time a transfer is made.
+ </i18n.Translate>
+ </p>
+ {missingWireTYpe.map((type) => {
+ return (
+ <p key={type}>
+ Wire <b>{type}</b> is not supported for this exchange.
+ </p>
+ );
+ })}
+ {newWireType.map((type) => {
+ return (
+ <Fragment key={type}>
+ <p>
+ Wire <b>{type}</b> is not supported for the previous exchange.
+ </p>
+ <FeeDescriptionTable>
+ <thead>
+ <tr>
+ <th>&nbsp;</th>
+ <th>
+ <i18n.Translate>Operation</i18n.Translate>
+ </th>
+ <th class="fee">
+ <i18n.Translate>Fee</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Until</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <RenderFeeDescriptionByValue
+ list={selected.transferFees[type]}
+ />
+ </tbody>
+ </FeeDescriptionTable>
+ </Fragment>
+ );
+ })}
+ {Object.entries(wireFeeTimeline).map(([type, fees], idx) => {
+ return (
+ <Fragment key={idx}>
+ <p>{type}</p>
+ <FeeDescriptionTable>
+ <thead>
+ <tr>
+ <th>&nbsp;</th>
+ <th>
+ <i18n.Translate>Operation</i18n.Translate>
+ </th>
+ <th class="fee">
+ <i18n.Translate>Current</i18n.Translate>
+ </th>
+ <th class="fee">
+ <i18n.Translate>Selected</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Until</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <RenderFeePairByValue
+ list={fees}
+ sorting={(a, b) => a.localeCompare(b)}
+ />
+ </tbody>
+ </FeeDescriptionTable>
+ </Fragment>
+ );
+ })}
+ </section>
+ <section>
+ <h2>
+ <i18n.Translate>Wallet operations</i18n.Translate>
+ </h2>
+ <p>
+ <i18n.Translate>
+ Every operation in this section may be different by transfer type
+ and is valid for a period of time. The exchange will charge the
+ indicated amount every time a transfer is made.
+ </i18n.Translate>
+ </p>
+ <FeeDescriptionTable>
+ <thead>
+ <tr>
+ <th>&nbsp;</th>
+ <th>
+ <i18n.Translate>Feature</i18n.Translate>
+ </th>
+ <th class="fee">
+ <i18n.Translate>Current</i18n.Translate>
+ </th>
+ <th class="fee">
+ <i18n.Translate>Selected</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Until</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <RenderFeePairByValue
+ list={globalFeeTimeline}
+ sorting={(a, b) => a.localeCompare(b)}
+ />
+ </tbody>
+ </FeeDescriptionTable>
+ </section>
+ <section>
+ <ButtonGroupFooter>
+ <Button onClick={onShowPrivacy.onClick} variant="outlined">
+ Privacy policy
+ </Button>
+ <Button onClick={onShowTerms.onClick} variant="outlined">
+ Terms of service
+ </Button>
+ </ButtonGroupFooter>
+ </section>
+ <section>
<ButtonGroupFooter>
<Button onClick={onShowPrivacy.onClick} variant="outlined">
Privacy policy
@@ -755,35 +903,6 @@ function RenderFeePairByValue({
.sort(sorting)
.map((i, idx) => <FeePairRowsGroup key={idx} infos={grouped[i]} />);
return <Fragment>{p}</Fragment>;
-
- // return (
- // <Fragment>
- // {
- // list.reduce(
- // (prev, info, idx) => {
- // const next = idx >= list.length - 1 ? undefined : list[idx + 1];
-
- // const nextIsMoreInfo =
- // next !== undefined && next.group === info.group;
-
- // prev.rows.push(info);
-
- // if (nextIsMoreInfo) {
- // return prev;
- // }
-
- // // prev.rows = [];
- // prev.views.push(<FeePairRowsGroup infos={prev.rows} />);
- // return prev;
- // },
- // { rows: [], views: [] } as {
- // rows: FeeDescriptionPair[];
- // views: h.JSX.Element[];
- // },
- // ).views
- // }
- // </Fragment>
- // );
}
/**
*
diff --git a/packages/taler-wallet-webextension/src/wallet/ExchangeSetUrl.tsx b/packages/taler-wallet-webextension/src/wallet/ExchangeSetUrl.tsx
deleted file mode 100644
index 8c31d8d95..000000000
--- a/packages/taler-wallet-webextension/src/wallet/ExchangeSetUrl.tsx
+++ /dev/null
@@ -1,211 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-import {
- canonicalizeBaseUrl,
- TalerConfigResponse,
-} from "@gnu-taler/taler-util";
-import { Fragment, h, VNode } from "preact";
-import { useEffect, useState } from "preact/hooks";
-import { ErrorMessage } from "../components/ErrorMessage.js";
-import {
- Input,
- LightText,
- SubTitle,
- Title,
- WarningBox,
-} from "../components/styled/index.js";
-import { useTranslationContext } from "../context/translation.js";
-import { Button } from "../mui/Button.js";
-
-export interface Props {
- initialValue?: string;
- expectedCurrency?: string;
- onCancel: () => Promise<void>;
- onVerify: (s: string) => Promise<TalerConfigResponse | undefined>;
- onConfirm: (url: string) => Promise<string | undefined>;
- withError?: string;
-}
-
-function useEndpointStatus<T>(
- endpoint: string,
- onVerify: (e: string) => Promise<T>,
-): {
- loading: boolean;
- error?: string;
- endpoint: string;
- result: T | undefined;
- updateEndpoint: (s: string) => void;
-} {
- const [value, setValue] = useState<string>(endpoint);
- const [dirty, setDirty] = useState(false);
- const [loading, setLoading] = useState(false);
- const [result, setResult] = useState<T | undefined>(undefined);
- const [error, setError] = useState<string | undefined>(undefined);
-
- const [handler, setHandler] = useState<number | undefined>(undefined);
-
- useEffect(() => {
- if (!value) return;
- window.clearTimeout(handler);
- const h = window.setTimeout(async () => {
- setDirty(true);
- setLoading(true);
- try {
- const url = canonicalizeBaseUrl(value);
- const result = await onVerify(url);
- setResult(result);
- setError(undefined);
- setLoading(false);
- } catch (e) {
- const errorMessage =
- e instanceof Error ? e.message : `unknown error: ${e}`;
- setError(errorMessage);
- setLoading(false);
- setResult(undefined);
- }
- }, 500);
- setHandler(h);
- }, [value, setHandler, onVerify]);
-
- return {
- error: dirty ? error : undefined,
- loading: loading,
- result: result,
- endpoint: value,
- updateEndpoint: setValue,
- };
-}
-
-export function ExchangeSetUrlPage({
- initialValue,
- expectedCurrency,
- onCancel,
- onVerify,
- onConfirm,
-}: Props): VNode {
- const { i18n } = useTranslationContext();
- const { loading, result, endpoint, updateEndpoint, error } =
- useEndpointStatus(initialValue ?? "", onVerify);
-
- const [confirmationError, setConfirmationError] = useState<
- string | undefined
- >(undefined);
-
- return (
- <Fragment>
- <section>
- {!expectedCurrency ? (
- <Title>
- <i18n.Translate>Add new exchange</i18n.Translate>
- </Title>
- ) : (
- <SubTitle>
- <i18n.Translate>Add exchange for {expectedCurrency}</i18n.Translate>
- </SubTitle>
- )}
- {!result && (
- <LightText>
- <i18n.Translate>
- Enter the URL of an exchange you trust.
- </i18n.Translate>
- </LightText>
- )}
- {result && (
- <LightText>
- <i18n.Translate>
- An exchange has been found! Review the information and click next
- </i18n.Translate>
- </LightText>
- )}
- {result && expectedCurrency && expectedCurrency !== result.currency && (
- <WarningBox>
- <i18n.Translate>
- This exchange doesn&apos;t match the expected currency
- <b>{expectedCurrency}</b>
- </i18n.Translate>
- </WarningBox>
- )}
- {error && (
- <ErrorMessage
- title={
- <i18n.Translate>Unable to verify this exchange</i18n.Translate>
- }
- description={error}
- />
- )}
- {confirmationError && (
- <ErrorMessage
- title={<i18n.Translate>Unable to add this exchange</i18n.Translate>}
- description={confirmationError}
- />
- )}
- <p>
- <Input invalid={!!error}>
- <label>URL</label>
- <input
- type="text"
- placeholder="https://"
- value={endpoint}
- onInput={(e) => updateEndpoint(e.currentTarget.value)}
- />
- </Input>
- {loading && (
- <div>
- <i18n.Translate>loading</i18n.Translate>...
- </div>
- )}
- {result && !loading && (
- <Fragment>
- <Input>
- <label>
- <i18n.Translate>Version</i18n.Translate>
- </label>
- <input type="text" disabled value={result.version} />
- </Input>
- <Input>
- <label>
- <i18n.Translate>Currency</i18n.Translate>
- </label>
- <input type="text" disabled value={result.currency} />
- </Input>
- </Fragment>
- )}
- </p>
- </section>
- <footer>
- <Button variant="contained" color="secondary" onClick={onCancel}>
- <i18n.Translate>Cancel</i18n.Translate>
- </Button>
- <Button
- variant="contained"
- disabled={
- !result ||
- !!error ||
- (!!expectedCurrency && expectedCurrency !== result.currency)
- }
- onClick={() => {
- const url = canonicalizeBaseUrl(endpoint);
- return onConfirm(url).then((r) =>
- r ? setConfirmationError(r) : undefined,
- );
- }}
- >
- <i18n.Translate>Next</i18n.Translate>
- </Button>
- </footer>
- </Fragment>
- );
-}
diff --git a/packages/taler-wallet-webextension/src/wallet/History.stories.tsx b/packages/taler-wallet-webextension/src/wallet/History.stories.tsx
index 1674ac135..482b8d698 100644
--- a/packages/taler-wallet-webextension/src/wallet/History.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/History.stories.tsx
@@ -20,10 +20,14 @@
*/
import {
+ AmountString,
PaymentStatus,
+ RefreshReason,
+ ScopeType,
TalerProtocolTimestamp,
TransactionCommon,
TransactionDeposit,
+ TransactionMajorState,
TransactionPayment,
TransactionPeerPullCredit,
TransactionPeerPullDebit,
@@ -31,16 +35,15 @@ import {
TransactionPeerPushDebit,
TransactionRefresh,
TransactionRefund,
- TransactionTip,
TransactionType,
TransactionWithdrawal,
WithdrawalType,
} from "@gnu-taler/taler-util";
import { HistoryView as TestedComponent } from "./History.js";
-import { createExample } from "../test-utils.js";
+import * as tests from "@gnu-taler/web-util/testing";
export default {
- title: "balance",
+ title: "history",
component: TestedComponent,
};
@@ -49,12 +52,14 @@ const commonTransaction = (): TransactionCommon =>
({
amountRaw: "USD:10",
amountEffective: "USD:9",
- pending: false,
+ txState: {
+ major: TransactionMajorState.Done,
+ },
timestamp: TalerProtocolTimestamp.fromSeconds(
new Date().getTime() / 1000 - count++ * 60 * 60 * 7,
),
transactionId: String(count),
- } as TransactionCommon);
+ }) as TransactionCommon;
const exampleData = {
withdraw: {
@@ -66,12 +71,14 @@ const exampleData = {
confirmed: false,
exchangePaytoUris: ["payto://x-taler-bank/bank/account"],
type: WithdrawalType.ManualTransfer,
+ reserveIsReady: false,
},
} as TransactionWithdrawal,
payment: {
...commonTransaction(),
- amountEffective: "USD:11",
+ amountEffective: "USD:11" as AmountString,
type: TransactionType.Payment,
+ posConfirmation: undefined,
info: {
contractTermsHash: "ASDZXCASD",
merchant: {
@@ -84,10 +91,11 @@ const exampleData = {
},
refunds: [],
refundPending: undefined,
- totalRefundEffective: "USD:0",
- totalRefundRaw: "USD:0",
+ totalRefundEffective: "USD:0" as AmountString,
+ totalRefundRaw: "USD:0" as AmountString,
proposalId: "1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0",
status: PaymentStatus.Accepted,
+ refundQueryActive: false,
} as TransactionPayment,
deposit: {
...commonTransaction(),
@@ -98,27 +106,21 @@ const exampleData = {
refresh: {
...commonTransaction(),
type: TransactionType.Refresh,
+ refreshInputAmount: "USD:1" as AmountString,
+ refreshOutputAmount: "USD:0.5" as AmountString,
exchangeBaseUrl: "http://exchange.taler",
+ refreshReason: RefreshReason.PayMerchant,
} as TransactionRefresh,
- tip: {
- ...commonTransaction(),
- type: TransactionType.Tip,
- merchantBaseUrl: "http://ads.merchant.taler.net/",
- } as TransactionTip,
refund: {
...commonTransaction(),
type: TransactionType.Refund,
refundedTransactionId:
"payment:1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0",
- info: {
- contractTermsHash: "ASDZXCASD",
+ paymentInfo: {
merchant: {
name: "the merchant",
},
- orderId: "2021.167-03NPY6MCYMVGT",
- products: [],
summary: "the summary",
- fulfillmentMessage: "",
},
refundPending: undefined,
} as TransactionRefund,
@@ -160,228 +162,461 @@ const exampleData = {
} as TransactionPeerPullDebit,
};
-export const NoBalance = createExample(TestedComponent, {
- transactions: [],
- balances: [],
-});
-
-export const SomeBalanceWithNoTransactions = createExample(TestedComponent, {
- transactions: [],
- balances: [
- {
- available: "TESTKUDOS:10",
- pendingIncoming: "TESTKUDOS:0",
- pendingOutgoing: "TESTKUDOS:0",
- hasPendingTransactions: false,
- requiresUserInput: false,
+export const SomeBalanceWithNoTransactions = tests.createExample(
+ TestedComponent,
+ {
+ transactionsByDate: {
+ "11/11/11": [],
},
- ],
-});
+ balances: [
+ {
+ available: "TESTKUDOS:10" as AmountString,
+ flags: [],
+ pendingIncoming: "TESTKUDOS:0" as AmountString,
+ pendingOutgoing: "TESTKUDOS:0" as AmountString,
+ hasPendingTransactions: false,
+ requiresUserInput: false,
+ scopeInfo: {
+ currency: "Ásd",
+ type: ScopeType.Auditor,
+ url: "",
+ },
+ },
+ ],
+ balanceIndex: 0,
+ },
+);
-export const OneSimpleTransaction = createExample(TestedComponent, {
- transactions: [exampleData.withdraw],
+export const OneSimpleTransaction = tests.createExample(TestedComponent, {
+ transactionsByDate: {
+ "11/11/11": [exampleData.withdraw],
+ },
balances: [
{
- available: "USD:10",
- pendingIncoming: "USD:0",
- pendingOutgoing: "USD:0",
+ flags: [],
+ available: "USD:10" as AmountString,
+ pendingIncoming: "USD:0" as AmountString,
+ pendingOutgoing: "USD:0" as AmountString,
hasPendingTransactions: false,
requiresUserInput: false,
+ scopeInfo: {
+ currency: "Ásd",
+ type: ScopeType.Auditor,
+ url: "",
+ },
},
],
+ balanceIndex: 0,
});
-export const TwoTransactionsAndZeroBalance = createExample(TestedComponent, {
- transactions: [exampleData.withdraw, exampleData.deposit],
- balances: [
- {
- available: "USD:0",
- pendingIncoming: "USD:0",
- pendingOutgoing: "USD:0",
- hasPendingTransactions: false,
- requiresUserInput: false,
+export const TwoTransactionsAndZeroBalance = tests.createExample(
+ TestedComponent,
+ {
+ transactionsByDate: {
+ "11/11/11": [exampleData.withdraw, exampleData.deposit],
},
- ],
-});
+ balances: [
+ {
+ flags: [],
+ available: "USD:0" as AmountString,
+ pendingIncoming: "USD:0" as AmountString,
+ pendingOutgoing: "USD:0" as AmountString,
+ hasPendingTransactions: false,
+ requiresUserInput: false,
+ scopeInfo: {
+ currency: "Ásd",
+ type: ScopeType.Auditor,
+ url: "",
+ },
+ },
+ ],
+ balanceIndex: 0,
+ },
+);
-export const OneTransactionPending = createExample(TestedComponent, {
- transactions: [
- {
- ...exampleData.withdraw,
- pending: true,
- },
- ],
+export const OneTransactionPending = tests.createExample(TestedComponent, {
+ transactionsByDate: {
+ "11/11/11": [
+ {
+ ...exampleData.withdraw,
+ txState: {
+ major: TransactionMajorState.Pending,
+ },
+ },
+ ],
+ },
balances: [
{
- available: "USD:10",
- pendingIncoming: "USD:0",
- pendingOutgoing: "USD:0",
+ flags: [],
+ available: "USD:10" as AmountString,
+ pendingIncoming: "USD:0" as AmountString,
+ pendingOutgoing: "USD:0" as AmountString,
hasPendingTransactions: false,
requiresUserInput: false,
+ scopeInfo: {
+ currency: "Ásd",
+ type: ScopeType.Auditor,
+ url: "",
+ },
},
],
+ balanceIndex: 0,
});
-export const SomeTransactions = createExample(TestedComponent, {
- transactions: [
- exampleData.withdraw,
- exampleData.payment,
- exampleData.withdraw,
- exampleData.payment,
- {
- ...exampleData.payment,
- info: {
- ...exampleData.payment.info,
- summary:
- "this is a long summary that may be cropped because its too long",
+export const SomeTransactions = tests.createExample(TestedComponent, {
+ transactionsByDate: {
+ "11/11/11": [
+ exampleData.withdraw,
+ exampleData.payment,
+ exampleData.withdraw,
+ exampleData.payment,
+ {
+ ...exampleData.payment,
+ info: {
+ ...exampleData.payment.info,
+ summary:
+ "this is a long summary that may be cropped because its too long",
+ },
},
- },
- exampleData.refund,
- exampleData.tip,
- exampleData.deposit,
- ],
+ exampleData.refund,
+ exampleData.deposit,
+ ],
+ },
balances: [
{
- available: "USD:10",
- pendingIncoming: "USD:0",
- pendingOutgoing: "USD:0",
+ flags: [],
+ available: "USD:10" as AmountString,
+ pendingIncoming: "USD:0" as AmountString,
+ pendingOutgoing: "USD:0" as AmountString,
hasPendingTransactions: false,
requiresUserInput: false,
+ scopeInfo: {
+ currency: "Ásd",
+ type: ScopeType.Auditor,
+ url: "",
+ },
},
],
+ balanceIndex: 0,
});
-export const SomeTransactionsWithTwoCurrencies = createExample(
+export const SomeTransactionsInDifferentStates = tests.createExample(
TestedComponent,
{
- transactions: [
- exampleData.withdraw,
- exampleData.payment,
- exampleData.withdraw,
- exampleData.payment,
- exampleData.refresh,
- exampleData.refund,
- exampleData.tip,
- exampleData.deposit,
+ transactionsByDate: {
+ "11/11/11": [
+ exampleData.withdraw,
+ {
+ ...exampleData.withdraw,
+ exchangeBaseUrl: "https://aborted/withdrawal",
+ txState: {
+ major: TransactionMajorState.Aborted,
+ },
+ },
+ {
+ ...exampleData.withdraw,
+ exchangeBaseUrl: "https://pending/withdrawal",
+ txState: {
+ major: TransactionMajorState.Pending,
+ },
+ },
+ {
+ ...exampleData.withdraw,
+ exchangeBaseUrl: "https://failed/withdrawal",
+ txState: {
+ major: TransactionMajorState.Failed,
+ },
+ },
+ {
+ ...exampleData.payment,
+ info: {
+ ...exampleData.payment.info,
+ summary: "normal payment",
+ },
+ },
+ {
+ ...exampleData.payment,
+ info: {
+ ...exampleData.payment.info,
+ summary: "aborting in progress",
+ },
+ txState: {
+ major: TransactionMajorState.Aborting,
+ },
+ },
+ {
+ ...exampleData.payment,
+ info: {
+ ...exampleData.payment.info,
+ summary: "aborted payment",
+ },
+ txState: {
+ major: TransactionMajorState.Aborted,
+ },
+ },
+ {
+ ...exampleData.payment,
+ info: {
+ ...exampleData.payment.info,
+ summary: "pending payment",
+ },
+ txState: {
+ major: TransactionMajorState.Pending,
+ },
+ },
+ {
+ ...exampleData.payment,
+ info: {
+ ...exampleData.payment.info,
+ summary: "failed payment",
+ },
+ txState: {
+ major: TransactionMajorState.Failed,
+ },
+ },
+ exampleData.refund,
+ exampleData.deposit,
+ ],
+ },
+ balances: [
+ {
+ flags: [],
+ available: "USD:10" as AmountString,
+ pendingIncoming: "USD:0" as AmountString,
+ pendingOutgoing: "USD:0" as AmountString,
+ hasPendingTransactions: false,
+ requiresUserInput: false,
+ scopeInfo: {
+ currency: "Ásd",
+ type: ScopeType.Auditor,
+ url: "",
+ },
+ },
],
+ balanceIndex: 0,
+ },
+);
+
+export const SomeTransactionsWithTwoCurrencies = tests.createExample(
+ TestedComponent,
+ {
+ transactionsByDate: {
+ "11/11/11": [
+ exampleData.withdraw,
+ exampleData.payment,
+ exampleData.withdraw,
+ exampleData.payment,
+ exampleData.refresh,
+ exampleData.refund,
+ exampleData.deposit,
+ ],
+ },
balances: [
{
- available: "USD:0",
- pendingIncoming: "USD:0",
- pendingOutgoing: "USD:0",
+ flags: [],
+ available: "USD:0" as AmountString,
+ pendingIncoming: "USD:0" as AmountString,
+ pendingOutgoing: "USD:0" as AmountString,
hasPendingTransactions: false,
requiresUserInput: false,
+ scopeInfo: {
+ currency: "Ásd",
+ type: ScopeType.Auditor,
+ url: "",
+ },
},
{
- available: "TESTKUDOS:10",
- pendingIncoming: "TESTKUDOS:0",
- pendingOutgoing: "TESTKUDOS:0",
+ flags: [],
+ available: "TESTKUDOS:10" as AmountString,
+ pendingIncoming: "TESTKUDOS:0" as AmountString,
+ pendingOutgoing: "TESTKUDOS:0" as AmountString,
hasPendingTransactions: false,
requiresUserInput: false,
+ scopeInfo: {
+ currency: "Ásd",
+ type: ScopeType.Auditor,
+ url: "",
+ },
},
],
+ balanceIndex: 0,
},
);
-export const FiveOfficialCurrencies = createExample(TestedComponent, {
- transactions: [exampleData.withdraw],
+export const FiveOfficialCurrencies = tests.createExample(TestedComponent, {
+ transactionsByDate: {
+ "11/11/11": [exampleData.withdraw],
+ },
balances: [
{
- available: "USD:1000",
- pendingIncoming: "USD:0",
- pendingOutgoing: "USD:0",
+ flags: [],
+ available: "USD:1000" as AmountString,
+ pendingIncoming: "USD:0" as AmountString,
+ pendingOutgoing: "USD:0" as AmountString,
hasPendingTransactions: false,
requiresUserInput: false,
+ scopeInfo: {
+ currency: "Ásd",
+ type: ScopeType.Auditor,
+ url: "",
+ },
},
{
- available: "EUR:881",
- pendingIncoming: "TESTKUDOS:0",
- pendingOutgoing: "TESTKUDOS:0",
+ flags: [],
+ available: "EUR:881" as AmountString,
+ pendingIncoming: "TESTKUDOS:0" as AmountString,
+ pendingOutgoing: "TESTKUDOS:0" as AmountString,
hasPendingTransactions: false,
requiresUserInput: false,
+ scopeInfo: {
+ currency: "Ásd",
+ type: ScopeType.Auditor,
+ url: "",
+ },
},
{
- available: "COL:4043000.5",
- pendingIncoming: "TESTKUDOS:0",
- pendingOutgoing: "TESTKUDOS:0",
+ flags: [],
+ available: "COL:4043000.5" as AmountString,
+ pendingIncoming: "TESTKUDOS:0" as AmountString,
+ pendingOutgoing: "TESTKUDOS:0" as AmountString,
hasPendingTransactions: false,
requiresUserInput: false,
+ scopeInfo: {
+ currency: "Ásd",
+ type: ScopeType.Auditor,
+ url: "",
+ },
},
{
- available: "JPY:11564450.6",
- pendingIncoming: "TESTKUDOS:0",
- pendingOutgoing: "TESTKUDOS:0",
+ flags: [],
+ available: "JPY:11564450.6" as AmountString,
+ pendingIncoming: "TESTKUDOS:0" as AmountString,
+ pendingOutgoing: "TESTKUDOS:0" as AmountString,
hasPendingTransactions: false,
requiresUserInput: false,
+ scopeInfo: {
+ currency: "Ásd",
+ type: ScopeType.Auditor,
+ url: "",
+ },
},
{
- available: "GBP:736",
- pendingIncoming: "TESTKUDOS:0",
- pendingOutgoing: "TESTKUDOS:0",
+ flags: [],
+ available: "GBP:736" as AmountString,
+ pendingIncoming: "TESTKUDOS:0" as AmountString,
+ pendingOutgoing: "TESTKUDOS:0" as AmountString,
hasPendingTransactions: false,
requiresUserInput: false,
+ scopeInfo: {
+ currency: "Ásd",
+ type: ScopeType.Auditor,
+ url: "",
+ },
},
],
+ balanceIndex: 0,
});
-export const FiveOfficialCurrenciesWithHighValue = createExample(
+export const FiveOfficialCurrenciesWithHighValue = tests.createExample(
TestedComponent,
{
- transactions: [exampleData.withdraw],
+ transactionsByDate: {
+ "11/11/11": [exampleData.withdraw],
+ },
balances: [
{
- available: "USD:881001321230000",
- pendingIncoming: "USD:0",
- pendingOutgoing: "USD:0",
+ flags: [],
+ available: "USD:881001321230000" as AmountString,
+ pendingIncoming: "USD:0" as AmountString,
+ pendingOutgoing: "USD:0" as AmountString,
hasPendingTransactions: false,
requiresUserInput: false,
+ scopeInfo: {
+ currency: "Ásd",
+ type: ScopeType.Auditor,
+ url: "",
+ },
},
{
- available: "EUR:10",
- pendingIncoming: "TESTKUDOS:0",
- pendingOutgoing: "TESTKUDOS:0",
+ flags: [],
+ available: "EUR:10" as AmountString,
+ pendingIncoming: "TESTKUDOS:0" as AmountString,
+ pendingOutgoing: "TESTKUDOS:0" as AmountString,
hasPendingTransactions: false,
requiresUserInput: false,
+ scopeInfo: {
+ currency: "Ásd",
+ type: ScopeType.Auditor,
+ url: "",
+ },
},
{
- available: "COL:443000123123000.5123123",
- pendingIncoming: "TESTKUDOS:0",
- pendingOutgoing: "TESTKUDOS:0",
+ flags: [],
+ available: "COL:443000123123000.5123123" as AmountString,
+ pendingIncoming: "TESTKUDOS:0" as AmountString,
+ pendingOutgoing: "TESTKUDOS:0" as AmountString,
hasPendingTransactions: false,
+ scopeInfo: {
+ currency: "Ásd",
+ type: ScopeType.Auditor,
+ url: "",
+ },
requiresUserInput: false,
},
{
- available: "JPY:1564450000000.6123123",
- pendingIncoming: "TESTKUDOS:0",
- pendingOutgoing: "TESTKUDOS:0",
+ flags: [],
+ available: "JPY:1564450000000.6123123" as AmountString,
+ pendingIncoming: "TESTKUDOS:0" as AmountString,
+ pendingOutgoing: "TESTKUDOS:0" as AmountString,
hasPendingTransactions: false,
requiresUserInput: false,
+ scopeInfo: {
+ currency: "Ásd",
+ type: ScopeType.Auditor,
+ url: "",
+ },
},
{
- available: "GBP:736001231231200.23123",
- pendingIncoming: "TESTKUDOS:0",
- pendingOutgoing: "TESTKUDOS:0",
+ flags: [],
+ available: "GBP:736001231231200.23123" as AmountString,
+ pendingIncoming: "TESTKUDOS:0" as AmountString,
+ pendingOutgoing: "TESTKUDOS:0" as AmountString,
hasPendingTransactions: false,
requiresUserInput: false,
+ scopeInfo: {
+ currency: "Ásd",
+ type: ScopeType.Auditor,
+ url: "",
+ },
},
],
+ balanceIndex: 0,
},
);
-export const PeerToPeer = createExample(TestedComponent, {
- transactions: [
- exampleData.pull_credit,
- exampleData.pull_debit,
- exampleData.push_credit,
- exampleData.push_debit,
- ],
+export const PeerToPeer = tests.createExample(TestedComponent, {
+ transactionsByDate: {
+ "11/11/11": [
+ exampleData.pull_credit,
+ exampleData.pull_debit,
+ exampleData.push_credit,
+ exampleData.push_debit,
+ ],
+ },
balances: [
{
- available: "USD:10",
- pendingIncoming: "USD:0",
- pendingOutgoing: "USD:0",
+ flags: [],
+ available: "USD:10" as AmountString,
+ pendingIncoming: "USD:0" as AmountString,
+ pendingOutgoing: "USD:0" as AmountString,
hasPendingTransactions: false,
requiresUserInput: false,
+ scopeInfo: {
+ currency: "Ásd",
+ type: ScopeType.Auditor,
+ url: "",
+ },
},
],
+ balanceIndex: 0,
});
diff --git a/packages/taler-wallet-webextension/src/wallet/History.tsx b/packages/taler-wallet-webextension/src/wallet/History.tsx
index 4b9c5c711..f81e6db9f 100644
--- a/packages/taler-wallet-webextension/src/wallet/History.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/History.tsx
@@ -15,54 +15,77 @@
*/
import {
+ AbsoluteTime,
Amounts,
- Balance,
NotificationType,
+ ScopeType,
Transaction,
+ WalletBalance,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-import { Fragment, h, VNode } from "preact";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { startOfDay } from "date-fns";
+import { Fragment, VNode, h } from "preact";
import { useEffect, useState } from "preact/hooks";
+import { ErrorAlertView } from "../components/CurrentAlerts.js";
+import { HistoryItem } from "../components/HistoryItem.js";
import { Loading } from "../components/Loading.js";
-import { LoadingError } from "../components/LoadingError.js";
+import { Time } from "../components/Time.js";
import {
CenteredBoldText,
CenteredText,
DateSeparator,
NiceSelect,
} from "../components/styled/index.js";
-import { Time } from "../components/Time.js";
-import { TransactionItem } from "../components/TransactionItem.js";
-import { useTranslationContext } from "../context/translation.js";
+import { alertFromError, useAlertContext } from "../context/alert.js";
+import { useBackendContext } from "../context/backend.js";
import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
+import { useSettings } from "../hooks/useSettings.js";
import { Button } from "../mui/Button.js";
import { NoBalanceHelp } from "../popup/NoBalanceHelp.js";
-import DownloadIcon from "../svg/download_24px.svg";
-import UploadIcon from "../svg/upload_24px.svg";
-import { wxApi } from "../wxApi.js";
+import DownloadIcon from "../svg/download_24px.inline.svg";
+import UploadIcon from "../svg/upload_24px.inline.svg";
+import { TextField } from "../mui/TextField.js";
+import { TextFieldHandler } from "../mui/handlers.js";
interface Props {
currency?: string;
+ search?: boolean;
goToWalletDeposit: (currency: string) => Promise<void>;
goToWalletManualWithdraw: (currency?: string) => Promise<void>;
}
export function HistoryPage({
- currency,
+ currency: _c,
+ search: showSearch,
goToWalletManualWithdraw,
goToWalletDeposit,
}: Props): VNode {
const { i18n } = useTranslationContext();
- const state = useAsyncAsHook(async () => ({
- b: await wxApi.wallet.call(WalletApiOperation.GetBalances, {}),
- tx: await wxApi.wallet.call(WalletApiOperation.GetTransactions, {}),
- }));
+ const api = useBackendContext();
+ const [balanceIndex, setBalanceIndex] = useState<number>(0);
+ const [search, setSearch] = useState<string>();
+
+ const [settings] = useSettings();
+ const state = useAsyncAsHook(async () => {
+ const b = await api.wallet.call(WalletApiOperation.GetBalances, {});
+ const balance =
+ b.balances.length > 0 ? b.balances[balanceIndex] : undefined;
+ const tx = await api.wallet.call(WalletApiOperation.GetTransactions, {
+ scopeInfo: showSearch ? undefined : balance?.scopeInfo,
+ sort: "descending",
+ includeRefreshes: settings.showRefeshTransactions,
+ search,
+ });
+ return { b, tx };
+ }, [balanceIndex, search]);
useEffect(() => {
- return wxApi.listener.onUpdateNotification(
- [NotificationType.WithdrawGroupFinished],
+ return api.listener.onUpdateNotification(
+ [NotificationType.TransactionStateTransition],
state?.retry,
);
});
+ const { pushAlertOnError } = useAlertContext();
if (!state) {
return <Loading />;
@@ -70,86 +93,96 @@ export function HistoryPage({
if (state.hasError) {
return (
- <LoadingError
- title={
- <i18n.Translate>
- Could not load the list of transactions
- </i18n.Translate>
+ <ErrorAlertView
+ error={alertFromError(
+ i18n,
+ i18n.str`Could not load the list of transactions`,
+ state,
+ )}
+ />
+ );
+ }
+
+ if (!state.response.b.balances.length) {
+ return (
+ <NoBalanceHelp
+ goToWalletManualWithdraw={{
+ onClick: pushAlertOnError(goToWalletManualWithdraw),
+ }}
+ />
+ );
+ }
+
+ const byDate = state.response.tx.transactions.reduce(
+ (rv, x) => {
+ const startDay =
+ x.timestamp.t_s === "never"
+ ? 0
+ : startOfDay(x.timestamp.t_s * 1000).getTime();
+ if (startDay) {
+ if (!rv[startDay]) {
+ rv[startDay] = [];
+ // datesWithTransaction.push(String(startDay));
}
- error={state}
+ rv[startDay].push(x);
+ }
+
+ return rv;
+ },
+ {} as { [x: string]: Transaction[] },
+ );
+
+ if (showSearch) {
+ return (
+ <FilteredHistoryView
+ search={{
+ value: search ?? "",
+ onInput: pushAlertOnError(async (d: string) => {
+ setSearch(d);
+ }),
+ }}
+ transactionsByDate={byDate}
/>
);
}
return (
<HistoryView
+ balanceIndex={balanceIndex}
+ changeBalanceIndex={(b) => setBalanceIndex(b)}
balances={state.response.b.balances}
- defaultCurrency={currency}
goToWalletManualWithdraw={goToWalletManualWithdraw}
goToWalletDeposit={goToWalletDeposit}
- transactions={[...state.response.tx.transactions].reverse()}
+ transactionsByDate={byDate}
/>
);
}
-const term = 1000 * 60 * 60 * 24;
-function normalizeToDay(x: number): number {
- return Math.round(x / term) * term;
-}
-
export function HistoryView({
- defaultCurrency,
- transactions,
balances,
+ balanceIndex,
+ changeBalanceIndex,
+ transactionsByDate,
goToWalletManualWithdraw,
goToWalletDeposit,
}: {
+ balanceIndex: number;
+ changeBalanceIndex: (s: number) => void;
goToWalletDeposit: (currency: string) => Promise<void>;
goToWalletManualWithdraw: (currency?: string) => Promise<void>;
- defaultCurrency?: string;
- transactions: Transaction[];
- balances: Balance[];
+ transactionsByDate: Record<string, Transaction[]>;
+ balances: WalletBalance[];
}): VNode {
const { i18n } = useTranslationContext();
- const currencies = balances.map((b) => b.available.split(":")[0]);
- const defaultCurrencyIndex = currencies.findIndex(
- (c) => c === defaultCurrency,
- );
- const [currencyIndex, setCurrencyIndex] = useState(
- defaultCurrencyIndex === -1 ? 0 : defaultCurrencyIndex,
- );
- const selectedCurrency =
- currencies.length > 0 ? currencies[currencyIndex] : undefined;
+ const balance = balances[balanceIndex];
- const currencyAmount = balances[currencyIndex]
- ? Amounts.jsonifyAmount(balances[currencyIndex].available)
+ const available = balance
+ ? Amounts.jsonifyAmount(balance.available)
: undefined;
- const byDate = transactions
- .filter((t) => t.amountRaw.split(":")[0] === selectedCurrency)
- .reduce((rv, x) => {
- const theDate =
- x.timestamp.t_s === "never"
- ? 0
- : normalizeToDay(x.timestamp.t_s * 1000);
- if (theDate) {
- (rv[theDate] = rv[theDate] || []).push(x);
- }
+ const datesWithTransaction: string[] = Object.keys(transactionsByDate);
- return rv;
- }, {} as { [x: string]: Transaction[] });
- const datesWithTransaction = Object.keys(byDate);
-
- if (balances.length === 0 || !selectedCurrency) {
- return (
- <NoBalanceHelp
- goToWalletManualWithdraw={{
- onClick: goToWalletManualWithdraw,
- }}
- />
- );
- }
return (
<Fragment>
<section>
@@ -159,72 +192,151 @@ export function HistoryView({
flexWrap: "wrap",
alignItems: "center",
justifyContent: "space-between",
+ marginRight: 20,
}}
>
- <div
- style={{
- width: "fit-content",
- display: "flex",
- }}
- >
- {currencies.length === 1 ? (
- <CenteredText style={{ fontSize: "x-large", margin: 8 }}>
- {selectedCurrency}
- </CenteredText>
- ) : (
- <NiceSelect>
- <select
- style={{
- fontSize: "x-large",
- }}
- value={currencyIndex}
- onChange={(e) => {
- setCurrencyIndex(Number(e.currentTarget.value));
- }}
- >
- {currencies.map((currency, index) => {
- return (
- <option value={index} key={currency}>
- {currency}
- </option>
- );
- })}
- </select>
- </NiceSelect>
- )}
- {currencyAmount && (
- <CenteredBoldText
- style={{
- display: "inline-block",
- fontSize: "x-large",
- margin: 8,
- }}
- >
- {Amounts.stringifyValue(currencyAmount, 2)}
- </CenteredBoldText>
- )}
- </div>
<div>
<Button
tooltip="Transfer money to the wallet"
startIcon={DownloadIcon}
variant="contained"
- onClick={() => goToWalletManualWithdraw(selectedCurrency)}
+ onClick={() =>
+ goToWalletManualWithdraw(balance.scopeInfo.currency)
+ }
>
- <i18n.Translate>Add</i18n.Translate>
+ <i18n.Translate>Receive</i18n.Translate>
</Button>
- {currencyAmount && Amounts.isNonZero(currencyAmount) && (
+ {available && Amounts.isNonZero(available) && (
<Button
tooltip="Transfer money from the wallet"
startIcon={UploadIcon}
variant="outlined"
color="primary"
- onClick={() => goToWalletDeposit(selectedCurrency)}
+ onClick={() => goToWalletDeposit(balance.scopeInfo.currency)}
>
<i18n.Translate>Send</i18n.Translate>
</Button>
)}
</div>
+ <div style={{ display: "flex", flexDirection: "column" }}>
+ <h3 style={{ marginBottom: 0 }}>Balance</h3>
+ <div
+ style={{
+ width: "fit-content",
+ display: "flex",
+ }}
+ >
+ {balances.length === 1 ? (
+ <CenteredText style={{ fontSize: "x-large", margin: 8 }}>
+ {balance.scopeInfo.currency}
+ </CenteredText>
+ ) : (
+ <NiceSelect style={{ flexDirection: "column" }}>
+ <select
+ style={{
+ fontSize: "x-large",
+ }}
+ value={balanceIndex}
+ onChange={(e) => {
+ changeBalanceIndex(
+ Number.parseInt(e.currentTarget.value, 10),
+ );
+ }}
+ >
+ {balances.map((entry, index) => {
+ return (
+ <option value={index} key={entry.scopeInfo.currency}>
+ {entry.scopeInfo.currency}
+ </option>
+ );
+ })}
+ </select>
+ <div style={{ fontSize: "small", color: "grey" }}>
+ {balance.scopeInfo.type === ScopeType.Exchange ||
+ balance.scopeInfo.type === ScopeType.Auditor
+ ? balance.scopeInfo.url
+ : undefined}
+ </div>
+ </NiceSelect>
+ )}
+ {available && (
+ <CenteredBoldText
+ style={{
+ display: "inline-block",
+ fontSize: "x-large",
+ margin: 8,
+ }}
+ >
+ {Amounts.stringifyValue(available, 2)}
+ </CenteredBoldText>
+ )}
+ </div>
+ </div>
+ </div>
+ </section>
+ {datesWithTransaction.length === 0 ? (
+ <section>
+ <i18n.Translate>
+ Your transaction history is empty for this currency.
+ </i18n.Translate>
+ </section>
+ ) : (
+ <section>
+ {datesWithTransaction.map((d, i) => {
+ return (
+ <Fragment key={i}>
+ <DateSeparator>
+ <Time
+ timestamp={AbsoluteTime.fromMilliseconds(
+ Number.parseInt(d, 10),
+ )}
+ format="dd MMMM yyyy"
+ />
+ </DateSeparator>
+ {transactionsByDate[d].map((tx, i) => (
+ <HistoryItem key={i} tx={tx} />
+ ))}
+ </Fragment>
+ );
+ })}
+ </section>
+ )}
+ </Fragment>
+ );
+}
+
+export function FilteredHistoryView({
+ search,
+ transactionsByDate,
+}: {
+ search: TextFieldHandler;
+ transactionsByDate: Record<string, Transaction[]>;
+}): VNode {
+ const { i18n } = useTranslationContext();
+
+ const datesWithTransaction: string[] = Object.keys(transactionsByDate);
+
+ return (
+ <Fragment>
+ <section>
+ <div
+ style={{
+ display: "flex",
+ flexWrap: "wrap",
+ alignItems: "center",
+ justifyContent: "space-between",
+ marginRight: 20,
+ }}
+ >
+ <TextField
+ label="Search"
+ variant="filled"
+ error={search.error}
+ required
+ fullWidth
+ value={search.value}
+ onChange={search.onInput}
+ />
</div>
</section>
{datesWithTransaction.length === 0 ? (
@@ -240,12 +352,14 @@ export function HistoryView({
<Fragment key={i}>
<DateSeparator>
<Time
- timestamp={{ t_ms: Number.parseInt(d, 10) }}
+ timestamp={AbsoluteTime.fromMilliseconds(
+ Number.parseInt(d, 10),
+ )}
format="dd MMMM yyyy"
/>
</DateSeparator>
- {byDate[d].map((tx, i) => (
- <TransactionItem key={i} tx={tx} />
+ {transactionsByDate[d].map((tx, i) => (
+ <HistoryItem key={i} tx={tx} />
))}
</Fragment>
);
diff --git a/packages/taler-wallet-webextension/src/wallet/ManageAccount/index.ts b/packages/taler-wallet-webextension/src/wallet/ManageAccount/index.ts
index df4e7586f..3a00d48ce 100644
--- a/packages/taler-wallet-webextension/src/wallet/ManageAccount/index.ts
+++ b/packages/taler-wallet-webextension/src/wallet/ManageAccount/index.ts
@@ -15,17 +15,17 @@
*/
import { KnownBankAccountsInfo } from "@gnu-taler/taler-util";
+import { ErrorAlertView } from "../../components/CurrentAlerts.js";
import { Loading } from "../../components/Loading.js";
-import { HookError } from "../../hooks/useAsyncAsHook.js";
+import { ErrorAlert } from "../../context/alert.js";
import {
ButtonHandler,
SelectFieldHandler,
TextFieldHandler,
} from "../../mui/handlers.js";
import { compose, StateViewMap } from "../../utils/index.js";
-import { wxApi } from "../../wxApi.js";
import { useComponentState } from "./state.js";
-import { LoadingUriView, ReadyView } from "./views.js";
+import { ReadyView } from "./views.js";
export interface Props {
currency: string;
@@ -42,8 +42,8 @@ export namespace State {
}
export interface LoadingUriError {
- status: "loading-error";
- error: HookError;
+ status: "error";
+ error: ErrorAlert;
}
export interface BaseInfo {
@@ -69,12 +69,12 @@ export type AccountByType = {
const viewMapping: StateViewMap<State> = {
loading: Loading,
- "loading-error": LoadingUriView,
+ error: ErrorAlertView,
ready: ReadyView,
};
export const ManageAccountPage = compose(
"ManageAccountPage",
- (p: Props) => useComponentState(p, wxApi),
+ (p: Props) => useComponentState(p),
viewMapping,
);
diff --git a/packages/taler-wallet-webextension/src/wallet/ManageAccount/state.ts b/packages/taler-wallet-webextension/src/wallet/ManageAccount/state.ts
index 1f920f05f..a7b2fe90f 100644
--- a/packages/taler-wallet-webextension/src/wallet/ManageAccount/state.ts
+++ b/packages/taler-wallet-webextension/src/wallet/ManageAccount/state.ts
@@ -21,21 +21,36 @@ import {
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useState } from "preact/hooks";
+import { alertFromError, useAlertContext } from "../../context/alert.js";
+import { useBackendContext } from "../../context/backend.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
-import { wxApi } from "../../wxApi.js";
import { AccountByType, Props, State } from "./index.js";
-
-export function useComponentState(
- { currency, onAccountAdded, onCancel }: Props,
- api: typeof wxApi,
-): State {
+import { useSettings } from "../../hooks/useSettings.js";
+
+export function useComponentState({
+ currency,
+ onAccountAdded,
+ onCancel,
+}: Props): State {
+ const api = useBackendContext();
+ const { pushAlertOnError } = useAlertContext();
+ const { i18n } = useTranslationContext();
const hook = useAsyncAsHook(() =>
api.wallet.call(WalletApiOperation.ListKnownBankAccounts, { currency }),
);
+ const accountType: Record<string, string> = {
+ iban: "IBAN",
+ };
+ const [settings] = useSettings();
+ if (settings.extendedAccountTypes) {
+ accountType["bitcoin"] = "Bitcoin";
+ accountType["x-taler-bank"] = "Taler Bank";
+ }
const [payto, setPayto] = useState("");
const [alias, setAlias] = useState("");
- const [type, setType] = useState("");
+ const [type, setType] = useState("iban");
if (!hook) {
return {
@@ -45,17 +60,14 @@ export function useComponentState(
}
if (hook.hasError) {
return {
- status: "loading-error",
- error: hook,
+ status: "error",
+ error: alertFromError(
+ i18n,
+ i18n.str`Could not load known bank accounts`,
+ hook),
};
}
- const accountType: Record<string, string> = {
- "": "Choose one account type",
- iban: "IBAN",
- // bitcoin: "Bitcoin",
- // "x-taler-bank": "Taler Bank",
- };
const uri = parsePaytoUri(payto);
const found =
hook.response.accounts.findIndex(
@@ -104,30 +116,30 @@ export function useComponentState(
accountType: {
list: accountType,
value: type,
- onChange: async (v) => {
+ onChange: pushAlertOnError(async (v) => {
setType(v);
- },
+ }),
},
alias: {
value: alias,
- onInput: async (v) => {
+ onInput: pushAlertOnError(async (v) => {
setAlias(v);
- },
+ }),
},
uri: {
value: payto,
error: paytoUriError,
- onInput: async (v) => {
+ onInput: pushAlertOnError(async (v) => {
setPayto(v);
- },
+ }),
},
accountByType,
- deleteAccount,
+ deleteAccount: pushAlertOnError(deleteAccount),
onAccountAdded: {
- onClick: unableToAdd ? undefined : addAccount,
+ onClick: unableToAdd ? undefined : pushAlertOnError(addAccount),
},
onCancel: {
- onClick: async () => onCancel(),
+ onClick: pushAlertOnError(async () => onCancel()),
},
};
}
diff --git a/packages/taler-wallet-webextension/src/wallet/ManageAccount/stories.tsx b/packages/taler-wallet-webextension/src/wallet/ManageAccount/stories.tsx
index ca6db8be9..c01797e31 100644
--- a/packages/taler-wallet-webextension/src/wallet/ManageAccount/stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/ManageAccount/stories.tsx
@@ -19,28 +19,24 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { createExample } from "../../test-utils.js";
+import * as tests from "@gnu-taler/web-util/testing";
+import { nullFunction } from "../../mui/handlers.js";
import { ReadyView } from "./views.js";
export default {
title: "manage account",
};
-const nullFunction = async () => {
- null;
-};
-
-export const JustTwoBitcoinAccounts = createExample(ReadyView, {
+export const JustTwoBitcoinAccounts = tests.createExample(ReadyView, {
status: "ready",
currency: "ARS",
accountType: {
list: {
- "": "Choose one account type",
iban: "IBAN",
- // bitcoin: "Bitcoin",
- // "x-taler-bank": "Taler Bank",
+ bitcoin: "Bitcoin",
+ "x-taler-bank": "Taler Bank",
},
- value: "",
+ value: "bitcoin",
},
alias: {
value: "",
@@ -62,6 +58,7 @@ export const JustTwoBitcoinAccounts = createExample(ReadyView, {
targetType: "bitcoin",
segwitAddrs: [],
isKnown: true,
+ address: "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
targetPath: "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
params: {},
},
@@ -73,6 +70,7 @@ export const JustTwoBitcoinAccounts = createExample(ReadyView, {
uri: {
targetType: "bitcoin",
segwitAddrs: [],
+ address: "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
isKnown: true,
targetPath: "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
params: {},
@@ -84,17 +82,16 @@ export const JustTwoBitcoinAccounts = createExample(ReadyView, {
onCancel: {},
});
-export const WithAllTypeOfAccounts = createExample(ReadyView, {
+export const WithAllTypeOfAccounts = tests.createExample(ReadyView, {
status: "ready",
currency: "ARS",
accountType: {
list: {
- "": "Choose one account type",
iban: "IBAN",
- // bitcoin: "Bitcoin",
- // "x-taler-bank": "Taler Bank",
+ bitcoin: "Bitcoin",
+ "x-taler-bank": "Taler Bank",
},
- value: "",
+ value: "x-taler-bank",
},
alias: {
value: "",
@@ -143,6 +140,7 @@ export const WithAllTypeOfAccounts = createExample(ReadyView, {
targetType: "bitcoin",
segwitAddrs: [],
isKnown: true,
+ address: "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
targetPath: "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
params: {},
},
@@ -155,6 +153,7 @@ export const WithAllTypeOfAccounts = createExample(ReadyView, {
targetType: "bitcoin",
segwitAddrs: [],
isKnown: true,
+ address: "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
targetPath: "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
params: {},
},
@@ -165,12 +164,11 @@ export const WithAllTypeOfAccounts = createExample(ReadyView, {
onCancel: {},
});
-export const AddingIbanAccount = createExample(ReadyView, {
+export const AddingIbanAccount = tests.createExample(ReadyView, {
status: "ready",
currency: "ARS",
accountType: {
list: {
- "": "Choose one account type",
iban: "IBAN",
// bitcoin: "Bitcoin",
// "x-taler-bank": "Taler Bank",
diff --git a/packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx b/packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx
index 3af0d5505..7b80977f3 100644
--- a/packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx
@@ -15,32 +15,27 @@
*/
import {
+ buildPayto,
KnownBankAccountsInfo,
PaytoUriBitcoin,
PaytoUriIBAN,
PaytoUriTalerBank,
+ stringifyPaytoUri,
+ validateIban,
} from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { styled } from "@linaria/react";
import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
-import { LoadingError } from "../../components/LoadingError.js";
-import { SelectList } from "../../components/SelectList.js";
-import {
- Input,
- LightText,
- SubTitle,
- SvgIcon,
- WarningText,
-} from "../../components/styled/index.js";
-import { useTranslationContext } from "../../context/translation.js";
+import { ErrorMessage } from "../../components/ErrorMessage.js";
+import { SubTitle, SvgIcon } from "../../components/styled/index.js";
import { Button } from "../../mui/Button.js";
import { TextFieldHandler } from "../../mui/handlers.js";
import { TextField } from "../../mui/TextField.js";
-import checkIcon from "../../svg/check_24px.svg";
-import warningIcon from "../../svg/warning_24px.svg";
-import deleteIcon from "../../svg/delete_24px.svg";
+import checkIcon from "../../svg/check_24px.inline.svg";
+import deleteIcon from "../../svg/delete_24px.inline.svg";
+import warningIcon from "../../svg/warning_24px.inline.svg";
import { State } from "./index.js";
-import { ErrorMessage } from "../../components/ErrorMessage.js";
type AccountType = "bitcoin" | "x-taler-bank" | "iban";
type ComponentFormByAccountType = {
@@ -80,17 +75,6 @@ const AccountTable = styled.table`
}
`;
-export function LoadingUriView({ error }: State.LoadingUriError): VNode {
- const { i18n } = useTranslationContext();
-
- return (
- <LoadingError
- title={<i18n.Translate>Could not load</i18n.Translate>}
- error={error}
- />
- );
-}
-
export function ReadyView({
currency,
error,
@@ -118,42 +102,53 @@ export function ReadyView({
{error && (
<ErrorMessage
- title={<i18n.Translate>Unable add this account</i18n.Translate>}
+ title={i18n.str`Unable add this account`}
description={error}
/>
)}
- <p>
- <Input>
- <SelectList
- label={<i18n.Translate>Select account type</i18n.Translate>}
- list={accountType.list}
- name="accountType"
- value={accountType.value}
- onChange={accountType.onChange}
+ <div style={{ width: "100%", display: "flex" }}>
+ {Object.entries(accountType.list).map(([key, name], idx) => (
+ <div
+ key={idx}
+ style={{
+ marginLeft: 8,
+ padding: 8,
+ borderTopLeftRadius: 5,
+ borderTopRightRadius: 5,
+ backgroundColor:
+ accountType.value === key ? "#0042b2" : "unset",
+ color: accountType.value === key ? "white" : "unset",
+ }}
+ onClick={() => {
+ if (accountType.onChange) {
+ accountType.onChange(key);
+ }
+ }}
+ >
+ {name}
+ </div>
+ ))}
+ </div>
+ <div style={{ border: "1px solid gray", padding: 8, borderRadius: 5 }}>
+ --- {uri.value} ---
+ <p>
+ <CustomFieldByAccountType
+ type={accountType.value as AccountType}
+ field={uri}
/>
- </Input>
+ </p>
+ </div>
+ <p>
+ <TextField
+ label="Alias"
+ variant="filled"
+ placeholder="Easy to remember description"
+ fullWidth
+ disabled={accountType.value === ""}
+ value={alias.value}
+ onChange={alias.onInput}
+ />
</p>
- {accountType.value === "" ? undefined : (
- <Fragment>
- <p>
- <CustomFieldByAccountType
- type={accountType.value as AccountType}
- field={uri}
- />
- </p>
- <p>
- <TextField
- label="Alias"
- variant="filled"
- placeholder="Easy to remember description"
- fullWidth
- disabled={accountType.value === ""}
- value={alias.value}
- onChange={alias.onInput}
- />
- </p>
- </Fragment>
- )}
</section>
<section>
<Button
@@ -414,6 +409,9 @@ function BitcoinAddressAccount({ field }: { field: TextFieldHandler }): VNode {
});
return (
<Fragment>
+ <h3>
+ <i18n.Translate>Bitcoin Account</i18n.Translate>
+ </h3>
<TextField
label="Bitcoin address"
variant="standard"
@@ -424,7 +422,8 @@ function BitcoinAddressAccount({ field }: { field: TextFieldHandler }): VNode {
onChange={(v) => {
setValue(v);
if (!errors && field.onInput) {
- field.onInput(`payto://bitcoin/${v}`);
+ const p = buildPayto("bitcoin", v, undefined);
+ field.onInput(stringifyPaytoUri(p));
}
}}
/>
@@ -433,7 +432,7 @@ function BitcoinAddressAccount({ field }: { field: TextFieldHandler }): VNode {
}
function undefinedIfEmpty<T extends object>(obj: T): T | undefined {
- return Object.keys(obj).some((k) => (obj as any)[k] !== undefined)
+ return Object.keys(obj).some((k) => (obj as Record<string,unknown>)[k] !== undefined)
? obj
: undefined;
}
@@ -452,6 +451,9 @@ function TalerBankAddressAccount({
});
return (
<Fragment>
+ <h3>
+ <i18n.Translate>Taler Bank</i18n.Translate>
+ </h3>
<TextField
label="Bank host"
variant="standard"
@@ -461,8 +463,9 @@ function TalerBankAddressAccount({
disabled={!field.onInput}
onChange={(v) => {
setHost(v);
- if (!errors && field.onInput) {
- field.onInput(`payto://x-taler-bank/${v}/${account}`);
+ if (!errors && field.onInput && account) {
+ const p = buildPayto("x-taler-bank", v, account);
+ field.onInput(stringifyPaytoUri(p));
}
}}
/>
@@ -475,8 +478,9 @@ function TalerBankAddressAccount({
error={account !== undefined ? errors?.account : undefined}
onChange={(v) => {
setAccount(v || "");
- if (!errors && field.onInput) {
- field.onInput(`payto://x-taler-bank/${host}/${v}`);
+ if (!errors && field.onInput && host) {
+ const p = buildPayto("x-taler-bank", host, v);
+ field.onInput(stringifyPaytoUri(p));
}
}}
/>
@@ -485,43 +489,50 @@ function TalerBankAddressAccount({
}
//Taken from libeufin and libeufin took it from the ISO20022 XSD schema
-const bicRegex = /^[A-Z]{6}[A-Z2-9][A-NP-Z0-9]([A-Z0-9]{3})?$/;
-const ibanRegex = /^[A-Z]{2}[0-9]{2}[a-zA-Z0-9]{1,30}$/;
+// const bicRegex = /^[A-Z]{6}[A-Z2-9][A-NP-Z0-9]([A-Z0-9]{3})?$/;
+// const ibanRegex = /^[A-Z]{2}[0-9]{2}[a-zA-Z0-9]{1,30}$/;
function IbanAddressAccount({ field }: { field: TextFieldHandler }): VNode {
const { i18n } = useTranslationContext();
- const [bic, setBic] = useState<string | undefined>(undefined);
+ // const [bic, setBic] = useState<string | undefined>(undefined);
const [iban, setIban] = useState<string | undefined>(undefined);
const [name, setName] = useState<string | undefined>(undefined);
- const errors = undefinedIfEmpty({
- bic: !bic
- ? undefined
- : !bicRegex.test(bic)
- ? i18n.str`Invalid bic`
- : undefined,
+ const bic = ""
+ const errorsFN = (iban:string | undefined, name: string | undefined) => undefinedIfEmpty({
+ // bic: !bic
+ // ? undefined
+ // : !bicRegex.test(bic)
+ // ? i18n.str`Invalid bic`
+ // : undefined,
iban: !iban
? i18n.str`Can't be empty`
- : !ibanRegex.test(iban)
+ : validateIban(iban).type === "invalid"
? i18n.str`Invalid iban`
: undefined,
name: !name ? i18n.str`Can't be empty` : undefined,
});
+ const errors = errorsFN(iban, name)
function sendUpdateIfNoErrors(
bic: string | undefined,
iban: string,
name: string,
): void {
- if (!errors && field.onInput) {
- const path = bic === undefined ? iban : `${bic}/${iban}`;
- field.onInput(
- `payto://iban/${path}?receiver-name=${encodeURIComponent(name)}`,
- );
+ if (!field.onInput) return;
+ if (!errorsFN(iban, name)) {
+ const p = buildPayto("iban", iban, bic);
+ p.params["receiver-name"] = name;
+ field.onInput(stringifyPaytoUri(p));
+ } else {
+ field.onInput("")
}
}
return (
<Fragment>
- <p>
+ <h3>
+ <i18n.Translate>International Bank Account Number</i18n.Translate>
+ </h3>
+ {/* <p>
<TextField
label="BIC"
variant="filled"
@@ -535,7 +546,7 @@ function IbanAddressAccount({ field }: { field: TextFieldHandler }): VNode {
sendUpdateIfNoErrors(v, iban || "", name || "");
}}
/>
- </p>
+ </p> */}
<p>
<TextField
label="IBAN"
@@ -554,7 +565,7 @@ function IbanAddressAccount({ field }: { field: TextFieldHandler }): VNode {
</p>
<p>
<TextField
- label="Receiver name"
+ label="Account name"
variant="filled"
placeholder="Name of the target bank account owner"
fullWidth
@@ -579,17 +590,12 @@ function CustomFieldByAccountType({
type: AccountType;
field: TextFieldHandler;
}): VNode {
- const { i18n } = useTranslationContext();
+ // const { i18n } = useTranslationContext();
const AccountForm = formComponentByAccountType[type];
return (
<div>
- <WarningText>
- <i18n.Translate>
- We can not validate the account so make sure the value is correct.
- </i18n.Translate>
- </WarningText>
<AccountForm field={field} />
</div>
);
diff --git a/packages/taler-wallet-webextension/src/wallet/Notifications/index.ts b/packages/taler-wallet-webextension/src/wallet/Notifications/index.ts
index 253a0e629..22b3adb0f 100644
--- a/packages/taler-wallet-webextension/src/wallet/Notifications/index.ts
+++ b/packages/taler-wallet-webextension/src/wallet/Notifications/index.ts
@@ -15,14 +15,14 @@
*/
import { UserAttentionUnreadList } from "@gnu-taler/taler-util";
+import { ErrorAlertView } from "../../components/CurrentAlerts.js";
import { Loading } from "../../components/Loading.js";
-import { HookError } from "../../hooks/useAsyncAsHook.js";
+import { ErrorAlert } from "../../context/alert.js";
import { compose, StateViewMap } from "../../utils/index.js";
-import { wxApi } from "../../wxApi.js";
import { useComponentState } from "./state.js";
-import { LoadingUriView, ReadyView } from "./views.js";
+import { ReadyView } from "./views.js";
-export interface Props {}
+export type Props = object;
export type State = State.Loading | State.LoadingUriError | State.Ready;
@@ -33,8 +33,8 @@ export namespace State {
}
export interface LoadingUriError {
- status: "loading-error";
- error: HookError;
+ status: "error";
+ error: ErrorAlert;
}
export interface BaseInfo {
@@ -50,12 +50,12 @@ export namespace State {
const viewMapping: StateViewMap<State> = {
loading: Loading,
- "loading-error": LoadingUriView,
+ error: ErrorAlertView,
ready: ReadyView,
};
export const NotificationsPage = compose(
"NotificationsPage",
- (p: Props) => useComponentState(p, wxApi),
+ (p: Props) => useComponentState(p),
viewMapping,
);
diff --git a/packages/taler-wallet-webextension/src/wallet/Notifications/state.ts b/packages/taler-wallet-webextension/src/wallet/Notifications/state.ts
index 093722cf0..3ef8250ac 100644
--- a/packages/taler-wallet-webextension/src/wallet/Notifications/state.ts
+++ b/packages/taler-wallet-webextension/src/wallet/Notifications/state.ts
@@ -15,11 +15,15 @@
*/
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { alertFromError } from "../../context/alert.js";
+import { useBackendContext } from "../../context/backend.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
-import { wxApi } from "../../wxApi.js";
import { Props, State } from "./index.js";
-export function useComponentState({}: Props, api: typeof wxApi): State {
+export function useComponentState(p: Props): State {
+ const api = useBackendContext();
+ const { i18n } = useTranslationContext();
const hook = useAsyncAsHook(async () => {
return await api.wallet.call(
WalletApiOperation.GetUserAttentionRequests,
@@ -33,10 +37,15 @@ export function useComponentState({}: Props, api: typeof wxApi): State {
error: undefined,
};
}
+
if (hook.hasError) {
return {
- status: "loading-error",
- error: hook,
+ status: "error",
+ error: alertFromError(
+ i18n,
+ i18n.str`Could not load user attention request`,
+ hook,
+ ),
};
}
diff --git a/packages/taler-wallet-webextension/src/wallet/Notifications/stories.tsx b/packages/taler-wallet-webextension/src/wallet/Notifications/stories.tsx
index c4da99909..7344f417c 100644
--- a/packages/taler-wallet-webextension/src/wallet/Notifications/stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Notifications/stories.tsx
@@ -19,34 +19,39 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { AbsoluteTime, AttentionType } from "@gnu-taler/taler-util";
-import { createExample } from "../../test-utils.js";
+import {
+ AbsoluteTime,
+ AttentionType,
+ TalerPreciseTimestamp,
+ TransactionIdStr,
+} from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
import { ReadyView } from "./views.js";
export default {
title: "notifications",
};
-export const Ready = createExample(ReadyView, {
+export const Ready = tests.createExample(ReadyView, {
list: [
{
- when: AbsoluteTime.now(),
+ when: TalerPreciseTimestamp.now(),
read: false,
info: {
type: AttentionType.KycWithdrawal,
- transactionId: "123",
+ transactionId: "123" as TransactionIdStr,
},
},
{
- when: AbsoluteTime.now(),
+ when: TalerPreciseTimestamp.now(),
read: false,
info: {
type: AttentionType.MerchantRefund,
- transactionId: "123",
+ transactionId: "123" as TransactionIdStr,
},
},
{
- when: AbsoluteTime.now(),
+ when: TalerPreciseTimestamp.now(),
read: false,
info: {
type: AttentionType.BackupUnpaid,
diff --git a/packages/taler-wallet-webextension/src/wallet/Notifications/views.tsx b/packages/taler-wallet-webextension/src/wallet/Notifications/views.tsx
index 9146d8837..03a08016a 100644
--- a/packages/taler-wallet-webextension/src/wallet/Notifications/views.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Notifications/views.tsx
@@ -20,7 +20,6 @@ import {
AttentionType,
} from "@gnu-taler/taler-util";
import { Fragment, h, VNode } from "preact";
-import { LoadingError } from "../../components/LoadingError.js";
import {
Column,
DateSeparator,
@@ -29,7 +28,7 @@ import {
SmallLightText,
} from "../../components/styled/index.js";
import { Time } from "../../components/Time.js";
-import { useTranslationContext } from "../../context/translation.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Avatar } from "../../mui/Avatar.js";
import { Button } from "../../mui/Button.js";
import { Grid } from "../../mui/Grid.js";
@@ -37,17 +36,6 @@ import { Pages } from "../../NavigationBar.js";
import { assertUnreachable } from "../../utils/index.js";
import { State } from "./index.js";
-export function LoadingUriView({ error }: State.LoadingUriError): VNode {
- const { i18n } = useTranslationContext();
-
- return (
- <LoadingError
- title={<i18n.Translate>Could not load notifications</i18n.Translate>}
- error={error}
- />
- );
-}
-
const term = 1000 * 60 * 60 * 24;
function normalizeToDay(x: number): number {
return Math.round(x / term) * term;
@@ -64,7 +52,8 @@ export function ReadyView({ list }: State.Ready): VNode {
}
const byDate = list.reduce((rv, x) => {
- const theDate = x.when.t_ms === "never" ? 0 : normalizeToDay(x.when.t_ms);
+ const theDate =
+ x.when.t_s === "never" ? 0 : normalizeToDay(x.when.t_s * 1000);
if (theDate) {
(rv[theDate] = rv[theDate] || []).push(x);
}
@@ -80,7 +69,9 @@ export function ReadyView({ list }: State.Ready): VNode {
<Fragment key={i}>
<DateSeparator>
<Time
- timestamp={{ t_ms: Number.parseInt(d, 10) }}
+ timestamp={AbsoluteTime.fromMilliseconds(
+ Number.parseInt(d, 10),
+ )}
format="dd MMMM yyyy"
/>
</DateSeparator>
@@ -89,7 +80,7 @@ export function ReadyView({ list }: State.Ready): VNode {
key={i}
info={n.info}
isRead={n.read}
- timestamp={n.when}
+ timestamp={AbsoluteTime.fromPreciseTimestamp(n.when)}
/>
))}
</Fragment>
diff --git a/packages/taler-wallet-webextension/src/wallet/ProviderAddConfirmProvider.stories.tsx b/packages/taler-wallet-webextension/src/wallet/ProviderAddConfirmProvider.stories.tsx
index 9ca397302..e5c43e230 100644
--- a/packages/taler-wallet-webextension/src/wallet/ProviderAddConfirmProvider.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/ProviderAddConfirmProvider.stories.tsx
@@ -19,7 +19,7 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { createExample } from "../test-utils.js";
+import * as tests from "@gnu-taler/web-util/testing";
import { ConfirmProviderView as TestedComponent } from "./ProviderAddPage.js";
export default {
@@ -32,7 +32,7 @@ export default {
},
};
-export const DemoService = createExample(TestedComponent, {
+export const DemoService = tests.createExample(TestedComponent, {
url: "https://sync.demo.taler.net/",
provider: {
annual_fee: "KUDOS:0.1",
@@ -41,7 +41,7 @@ export const DemoService = createExample(TestedComponent, {
},
});
-export const FreeService = createExample(TestedComponent, {
+export const FreeService = tests.createExample(TestedComponent, {
url: "https://sync.taler:9667/",
provider: {
annual_fee: "ARS:0",
diff --git a/packages/taler-wallet-webextension/src/wallet/ProviderAddPage.tsx b/packages/taler-wallet-webextension/src/wallet/ProviderAddPage.tsx
index d5f072828..6ade0718a 100644
--- a/packages/taler-wallet-webextension/src/wallet/ProviderAddPage.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/ProviderAddPage.tsx
@@ -16,7 +16,6 @@
import {
Amounts,
- BackupBackupProviderTerms,
canonicalizeBaseUrl,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
@@ -31,22 +30,28 @@ import {
SubTitle,
Title,
} from "../components/styled/index.js";
-import { useTranslationContext } from "../context/translation.js";
+import { useBackendContext } from "../context/backend.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Button } from "../mui/Button.js";
import { queryToSlashConfig } from "../utils/index.js";
-import { wxApi } from "../wxApi.js";
interface Props {
currency: string;
onBack: () => Promise<void>;
}
+interface BackupBackupProviderTerms {
+ annual_fee: string;
+ storage_limit_in_megabytes: number;
+ supported_protocol_version: string;
+}
+
export function ProviderAddPage({ onBack }: Props): VNode {
const [verifying, setVerifying] = useState<
| { url: string; name: string; provider: BackupBackupProviderTerms }
| undefined
>(undefined);
-
+ const api = useBackendContext();
if (!verifying) {
return (
<SetUrlView
@@ -70,7 +75,7 @@ export function ProviderAddPage({ onBack }: Props): VNode {
setVerifying(undefined);
}}
onConfirm={() => {
- return wxApi.wallet
+ return api.wallet
.call(WalletApiOperation.AddBackupProvider, {
backupProviderBaseUrl: verifying.url,
name: verifying.name,
@@ -127,11 +132,7 @@ export function SetUrlView({
</Title>
{error && (
<ErrorMessage
- title={
- <i18n.Translate>
- Could not get provider information
- </i18n.Translate>
- }
+ title={i18n.str`Could not get provider information`}
description={error}
/>
)}
@@ -223,7 +224,7 @@ export function ConfirmProviderView({
</SubTitle>
<p>
{Amounts.isZero(provider.annual_fee) ? (
- <i18n.Translate>free of charge</i18n.Translate>
+ i18n.str`free of charge`
) : (
<i18n.Translate>
{provider.annual_fee} per year of service
@@ -240,7 +241,7 @@ export function ConfirmProviderView({
</i18n.Translate>
</p>
<Checkbox
- label={<i18n.Translate>Accept terms of service</i18n.Translate>}
+ label={i18n.str`Accept terms of service`}
name="terms"
onToggle={async () => setAccepted((old) => !old)}
enabled={accepted}
diff --git a/packages/taler-wallet-webextension/src/wallet/ProviderAddSetUrl.stories.tsx b/packages/taler-wallet-webextension/src/wallet/ProviderAddSetUrl.stories.tsx
index a5528c36b..d35a0ff99 100644
--- a/packages/taler-wallet-webextension/src/wallet/ProviderAddSetUrl.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/ProviderAddSetUrl.stories.tsx
@@ -19,7 +19,7 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { createExample } from "../test-utils.js";
+import * as tests from "@gnu-taler/web-util/testing";
import { SetUrlView as TestedComponent } from "./ProviderAddPage.js";
export default {
@@ -32,20 +32,20 @@ export default {
},
};
-export const Initial = createExample(TestedComponent, {});
+export const Initial = tests.createExample(TestedComponent, {});
-export const WithValue = createExample(TestedComponent, {
+export const WithValue = tests.createExample(TestedComponent, {
initialValue: "sync.demo.taler.net",
});
-export const WithConnectionError = createExample(TestedComponent, {
+export const WithConnectionError = tests.createExample(TestedComponent, {
withError: "Network error",
});
-export const WithClientError = createExample(TestedComponent, {
+export const WithClientError = tests.createExample(TestedComponent, {
withError: "URL may not be right: (404) Not Found",
});
-export const WithServerError = createExample(TestedComponent, {
+export const WithServerError = tests.createExample(TestedComponent, {
withError: "Try another server: (500) Internal Server Error",
});
diff --git a/packages/taler-wallet-webextension/src/wallet/ProviderDetail.stories.tsx b/packages/taler-wallet-webextension/src/wallet/ProviderDetail.stories.tsx
index 98c68e6bd..d4ee09b89 100644
--- a/packages/taler-wallet-webextension/src/wallet/ProviderDetail.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/ProviderDetail.stories.tsx
@@ -19,9 +19,13 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { TalerProtocolTimestamp } from "@gnu-taler/taler-util";
-import { ProviderPaymentType } from "@gnu-taler/taler-wallet-core";
-import { createExample } from "../test-utils.js";
+import {
+ AbsoluteTime,
+ AmountString,
+ ProviderPaymentType,
+ TalerPreciseTimestamp,
+} from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
import { ProviderView as TestedComponent } from "./ProviderDetailPage.js";
export default {
@@ -34,122 +38,116 @@ export default {
},
};
-export const Active = createExample(TestedComponent, {
+export const Active = tests.createExample(TestedComponent, {
info: {
active: true,
name: "sync.demo",
syncProviderBaseUrl: "http://sync.taler:9967/",
lastSuccessfulBackupTimestamp:
- TalerProtocolTimestamp.fromSeconds(1625063925),
+ TalerPreciseTimestamp.fromSeconds(1625063925),
paymentProposalIds: [
"43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG",
],
paymentStatus: {
type: ProviderPaymentType.Paid,
- paidUntil: {
- t_ms: 1656599921000,
- },
+ paidUntil: AbsoluteTime.fromMilliseconds(1656599921000),
},
terms: {
- annualFee: "EUR:1",
+ annualFee: "EUR:1" as AmountString,
storageLimitInMegabytes: 16,
supportedProtocolVersion: "0.0",
},
},
});
-export const ActiveErrorSync = createExample(TestedComponent, {
+export const ActiveErrorSync = tests.createExample(TestedComponent, {
info: {
active: true,
name: "sync.demo",
syncProviderBaseUrl: "http://sync.taler:9967/",
lastSuccessfulBackupTimestamp:
- TalerProtocolTimestamp.fromSeconds(1625063925),
+ TalerPreciseTimestamp.fromSeconds(1625063925),
lastAttemptedBackupTimestamp:
- TalerProtocolTimestamp.fromSeconds(1625063925078),
+ TalerPreciseTimestamp.fromSeconds(1625063925078),
paymentProposalIds: [
"43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG",
],
paymentStatus: {
type: ProviderPaymentType.Paid,
- paidUntil: {
- t_ms: 1656599921000,
- },
+ paidUntil: AbsoluteTime.fromMilliseconds(1656599921000),
},
lastError: {
code: 2002,
details: "details",
+ when: AbsoluteTime.now(),
hint: "error hint from the server",
message: "message",
},
terms: {
- annualFee: "EUR:1",
+ annualFee: "EUR:1" as AmountString,
storageLimitInMegabytes: 16,
supportedProtocolVersion: "0.0",
},
},
});
-export const ActiveBackupProblemUnreadable = createExample(TestedComponent, {
- info: {
- active: true,
- name: "sync.demo",
- syncProviderBaseUrl: "http://sync.taler:9967/",
- lastSuccessfulBackupTimestamp:
- TalerProtocolTimestamp.fromSeconds(1625063925),
- paymentProposalIds: [
- "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG",
- ],
- paymentStatus: {
- type: ProviderPaymentType.Paid,
- paidUntil: {
- t_ms: 1656599921000,
+export const ActiveBackupProblemUnreadable = tests.createExample(
+ TestedComponent,
+ {
+ info: {
+ active: true,
+ name: "sync.demo",
+ syncProviderBaseUrl: "http://sync.taler:9967/",
+ lastSuccessfulBackupTimestamp:
+ TalerPreciseTimestamp.fromSeconds(1625063925),
+ paymentProposalIds: [
+ "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG",
+ ],
+ paymentStatus: {
+ type: ProviderPaymentType.Paid,
+ paidUntil: AbsoluteTime.fromMilliseconds(1656599921000),
+ },
+ backupProblem: {
+ type: "backup-unreadable",
+ },
+ terms: {
+ annualFee: "EUR:1" as AmountString,
+ storageLimitInMegabytes: 16,
+ supportedProtocolVersion: "0.0",
},
- },
- backupProblem: {
- type: "backup-unreadable",
- },
- terms: {
- annualFee: "EUR:1",
- storageLimitInMegabytes: 16,
- supportedProtocolVersion: "0.0",
},
},
-});
+);
-export const ActiveBackupProblemDevice = createExample(TestedComponent, {
+export const ActiveBackupProblemDevice = tests.createExample(TestedComponent, {
info: {
active: true,
name: "sync.demo",
syncProviderBaseUrl: "http://sync.taler:9967/",
lastSuccessfulBackupTimestamp:
- TalerProtocolTimestamp.fromSeconds(1625063925078),
+ TalerPreciseTimestamp.fromSeconds(1625063925078),
paymentProposalIds: [
"43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG",
],
paymentStatus: {
type: ProviderPaymentType.Paid,
- paidUntil: {
- t_ms: 1656599921000,
- },
+ paidUntil: AbsoluteTime.fromMilliseconds(1656599921000),
},
backupProblem: {
type: "backup-conflicting-device",
myDeviceId: "my-device-id",
otherDeviceId: "other-device-id",
- backupTimestamp: {
- t_ms: 1656599921000,
- },
+ backupTimestamp: AbsoluteTime.fromMilliseconds(1656599921000),
},
terms: {
- annualFee: "EUR:1",
+ annualFee: "EUR:1" as AmountString,
storageLimitInMegabytes: 16,
supportedProtocolVersion: "0.0",
},
},
});
-export const InactiveUnpaid = createExample(TestedComponent, {
+export const InactiveUnpaid = tests.createExample(TestedComponent, {
info: {
active: false,
name: "sync.demo",
@@ -159,32 +157,35 @@ export const InactiveUnpaid = createExample(TestedComponent, {
type: ProviderPaymentType.Unpaid,
},
terms: {
- annualFee: "EUR:0.1",
+ annualFee: "EUR:0.1" as AmountString,
storageLimitInMegabytes: 16,
supportedProtocolVersion: "0.0",
},
},
});
-export const InactiveInsufficientBalance = createExample(TestedComponent, {
- info: {
- active: false,
- name: "sync.demo",
- syncProviderBaseUrl: "http://sync.demo.taler.net/",
- paymentProposalIds: [],
- paymentStatus: {
- type: ProviderPaymentType.InsufficientBalance,
- amount: "EUR:123",
- },
- terms: {
- annualFee: "EUR:0.1",
- storageLimitInMegabytes: 16,
- supportedProtocolVersion: "0.0",
+export const InactiveInsufficientBalance = tests.createExample(
+ TestedComponent,
+ {
+ info: {
+ active: false,
+ name: "sync.demo",
+ syncProviderBaseUrl: "http://sync.demo.taler.net/",
+ paymentProposalIds: [],
+ paymentStatus: {
+ type: ProviderPaymentType.InsufficientBalance,
+ amount: "EUR:123" as AmountString,
+ },
+ terms: {
+ annualFee: "EUR:0.1" as AmountString,
+ storageLimitInMegabytes: 16,
+ supportedProtocolVersion: "0.0",
+ },
},
},
-});
+);
-export const InactivePending = createExample(TestedComponent, {
+export const InactivePending = tests.createExample(TestedComponent, {
info: {
active: false,
name: "sync.demo",
@@ -195,14 +196,14 @@ export const InactivePending = createExample(TestedComponent, {
talerUri: "taler://pay/sad",
},
terms: {
- annualFee: "EUR:0.1",
+ annualFee: "EUR:0.1" as AmountString,
storageLimitInMegabytes: 16,
supportedProtocolVersion: "0.0",
},
},
});
-export const ActiveTermsChanged = createExample(TestedComponent, {
+export const ActiveTermsChanged = tests.createExample(TestedComponent, {
info: {
active: true,
name: "sync.demo",
@@ -210,22 +211,20 @@ export const ActiveTermsChanged = createExample(TestedComponent, {
paymentProposalIds: [],
paymentStatus: {
type: ProviderPaymentType.TermsChanged,
- paidUntil: {
- t_ms: 1656599921000,
- },
+ paidUntil: AbsoluteTime.fromMilliseconds(1656599921000),
newTerms: {
- annualFee: "EUR:10",
+ annualFee: "EUR:10" as AmountString,
storageLimitInMegabytes: 8,
supportedProtocolVersion: "0.0",
},
oldTerms: {
- annualFee: "EUR:0.1",
+ annualFee: "EUR:0.1" as AmountString,
storageLimitInMegabytes: 16,
supportedProtocolVersion: "0.0",
},
},
terms: {
- annualFee: "EUR:0.1",
+ annualFee: "EUR:0.1" as AmountString,
storageLimitInMegabytes: 16,
supportedProtocolVersion: "0.0",
},
diff --git a/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx b/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx
index 6dde30b39..d628b68e8 100644
--- a/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx
@@ -15,23 +15,24 @@
*/
import * as utils from "@gnu-taler/taler-util";
-import { AbsoluteTime } from "@gnu-taler/taler-util";
import {
+ AbsoluteTime,
ProviderInfo,
ProviderPaymentStatus,
ProviderPaymentType,
- WalletApiOperation,
-} from "@gnu-taler/taler-wallet-core";
-import { Fragment, h, VNode } from "preact";
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { ErrorAlertView } from "../components/CurrentAlerts.js";
import { ErrorMessage } from "../components/ErrorMessage.js";
import { Loading } from "../components/Loading.js";
-import { LoadingError } from "../components/LoadingError.js";
-import { PaymentStatus, SmallLightText } from "../components/styled/index.js";
import { Time } from "../components/Time.js";
-import { useTranslationContext } from "../context/translation.js";
+import { PaymentStatus, SmallLightText } from "../components/styled/index.js";
+import { alertFromError } from "../context/alert.js";
+import { useBackendContext } from "../context/backend.js";
import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
import { Button } from "../mui/Button.js";
-import { wxApi } from "../wxApi.js";
interface Props {
pid: string;
@@ -47,12 +48,10 @@ export function ProviderDetailPage({
onWithdraw,
}: Props): VNode {
const { i18n } = useTranslationContext();
+ const api = useBackendContext();
async function getProviderInfo(): Promise<ProviderInfo | null> {
//create a first list of backup info by currency
- const status = await wxApi.wallet.call(
- WalletApiOperation.GetBackupInfo,
- {},
- );
+ const status = await api.wallet.call(WalletApiOperation.GetBackupInfo, {});
const providers = status.providers.filter(
(p) => p.syncProviderBaseUrl === providerURL,
@@ -67,14 +66,12 @@ export function ProviderDetailPage({
}
if (state.hasError) {
return (
- <LoadingError
- title={
- <i18n.Translate>
- There was an error loading the provider detail for &quot;
- {providerURL}&quot;
- </i18n.Translate>
- }
- error={state}
+ <ErrorAlertView
+ error={alertFromError(
+ i18n,
+ i18n.str`There was an error loading the provider detail for &quot;${providerURL}&quot;`,
+ state,
+ )}
/>
);
}
@@ -103,7 +100,7 @@ export function ProviderDetailPage({
<ProviderView
info={info}
onSync={async () =>
- wxApi.wallet
+ api.wallet
.call(WalletApiOperation.RunBackupCycle, {
providers: [providerURL],
})
@@ -120,7 +117,7 @@ export function ProviderDetailPage({
onWithdraw(info.paymentStatus.amount);
}}
onDelete={() =>
- wxApi.wallet
+ api.wallet
.call(WalletApiOperation.RemoveBackupProvider, {
provider: providerURL,
})
@@ -155,7 +152,7 @@ export function ProviderView({
}: ViewProps): VNode {
const { i18n } = useTranslationContext();
const lb = info.lastSuccessfulBackupTimestamp
- ? AbsoluteTime.fromTimestamp(info.lastSuccessfulBackupTimestamp)
+ ? AbsoluteTime.fromPreciseTimestamp(info.lastSuccessfulBackupTimestamp)
: undefined;
const isPaid =
info.paymentStatus.type === ProviderPaymentType.Paid ||
@@ -272,9 +269,7 @@ function Error({ info }: { info: ProviderInfo }): VNode {
if (info.lastError) {
return (
<ErrorMessage
- title={
- <i18n.Translate>This provider has reported an error</i18n.Translate>
- }
+ title={i18n.str`This provider has reported an error`}
description={info.lastError.hint}
/>
);
@@ -284,32 +279,17 @@ function Error({ info }: { info: ProviderInfo }): VNode {
case "backup-conflicting-device":
return (
<ErrorMessage
- title={
- <Fragment>
- <i18n.Translate>
- There is conflict with another backup from{" "}
- <b>{info.backupProblem.otherDeviceId}</b>
- </i18n.Translate>
- </Fragment>
- }
+ title={i18n.str`There is conflict with another backup from &quot;${info.backupProblem.otherDeviceId}&quot;`}
/>
);
case "backup-unreadable":
- return (
- <ErrorMessage
- title={<i18n.Translate>Backup is not readable</i18n.Translate>}
- />
- );
+ return <ErrorMessage title={i18n.str`Backup is not readable`} />;
default:
return (
<ErrorMessage
- title={
- <Fragment>
- <i18n.Translate>
- Unknown backup problem: {JSON.stringify(info.backupProblem)}
- </i18n.Translate>
- </Fragment>
- }
+ title={i18n.str`Unknown backup problem: ${JSON.stringify(
+ info.backupProblem,
+ )}`}
/>
);
}
diff --git a/packages/taler-wallet-webextension/src/wallet/QrReader.stories.tsx b/packages/taler-wallet-webextension/src/wallet/QrReader.stories.tsx
index 0fc38e90f..8fc6985b0 100644
--- a/packages/taler-wallet-webextension/src/wallet/QrReader.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/QrReader.stories.tsx
@@ -19,11 +19,11 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { createExample } from "../test-utils.js";
+import * as tests from "@gnu-taler/web-util/testing";
import { QrReaderPage } from "./QrReader.js";
export default {
title: "qr reader",
};
-export const Reading = createExample(QrReaderPage, {});
+export const Reading = tests.createExample(QrReaderPage, {});
diff --git a/packages/taler-wallet-webextension/src/wallet/QrReader.tsx b/packages/taler-wallet-webextension/src/wallet/QrReader.tsx
index 467f8bb7c..a01ea6967 100644
--- a/packages/taler-wallet-webextension/src/wallet/QrReader.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/QrReader.tsx
@@ -14,17 +14,27 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { classifyTalerUri, TalerUriType } from "@gnu-taler/taler-util";
+import {
+ assertUnreachable,
+ parseTalerUri,
+ TalerUri,
+ TalerUriAction,
+ TranslatedString,
+} from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { css } from "@linaria/core";
import { styled } from "@linaria/react";
-import { Fragment, h, VNode } from "preact";
-import { Ref, useEffect, useRef, useState } from "preact/hooks";
-import QrScanner from "qr-scanner";
-import { useTranslationContext } from "../context/translation.js";
+import jsQR, * as pr from "jsqr";
+import { h, VNode } from "preact";
+import { useRef, useState } from "preact/hooks";
+import { EnabledBySettings } from "../components/EnabledBySettings.js";
import { Alert } from "../mui/Alert.js";
import { Button } from "../mui/Button.js";
+import { Grid } from "../mui/Grid.js";
+import { InputFile } from "../mui/InputFile.js";
import { TextField } from "../mui/TextField.js";
-const QrVideo = styled.video`
+const QrCanvas = css`
width: 80%;
margin-left: auto;
margin-right: auto;
@@ -32,6 +42,8 @@ const QrVideo = styled.video`
background-color: black;
`;
+const LINE_COLOR = "#FF3B58";
+
const Container = styled.div`
display: flex;
flex-direction: column;
@@ -40,115 +52,341 @@ const Container = styled.div`
}
`;
-interface Props {
- onDetected: (url: string) => void;
+export interface Props {
+ onDetected: (url: TalerUri) => void;
+}
+
+type XY = { x: number; y: number };
+
+function drawLine(
+ canvas: CanvasRenderingContext2D,
+ begin: XY,
+ end: XY,
+ color: string,
+) {
+ canvas.beginPath();
+ canvas.moveTo(begin.x, begin.y);
+ canvas.lineTo(end.x, end.y);
+ canvas.lineWidth = 4;
+ canvas.strokeStyle = color;
+ canvas.stroke();
+}
+
+function drawBox(context: CanvasRenderingContext2D, code: pr.QRCode) {
+ drawLine(
+ context,
+ code.location.topLeftCorner,
+ code.location.topRightCorner,
+ LINE_COLOR,
+ );
+ drawLine(
+ context,
+ code.location.topRightCorner,
+ code.location.bottomRightCorner,
+ LINE_COLOR,
+ );
+ drawLine(
+ context,
+ code.location.bottomRightCorner,
+ code.location.bottomLeftCorner,
+ LINE_COLOR,
+ );
+ drawLine(
+ context,
+ code.location.bottomLeftCorner,
+ code.location.topLeftCorner,
+ LINE_COLOR,
+ );
+}
+
+const SCAN_PER_SECONDS = 3;
+const TIME_BETWEEN_FRAMES = 1000 / SCAN_PER_SECONDS;
+
+async function delay(ms: number) {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+function drawIntoCanvasAndGetQR(
+ tag: HTMLVideoElement | HTMLImageElement,
+ canvas: HTMLCanvasElement,
+): string | undefined {
+ const context = canvas.getContext("2d");
+ if (!context) {
+ throw Error("no 2d canvas context");
+ }
+ context.clearRect(0, 0, canvas.width, canvas.height);
+ context.drawImage(tag, 0, 0, canvas.width, canvas.height);
+ const imgData = context.getImageData(0, 0, canvas.width, canvas.height);
+ const code = jsQR.default(imgData.data, canvas.width, canvas.height, {
+ inversionAttempts: "attemptBoth",
+ });
+ if (code) {
+ drawBox(context, code);
+ return code.data;
+ }
+ return undefined;
+}
+
+async function readNextFrame(
+ video: HTMLVideoElement,
+ canvas: HTMLCanvasElement,
+): Promise<string | undefined> {
+ const requestFrame =
+ "requestVideoFrameCallback" in video
+ ? video.requestVideoFrameCallback.bind(video)
+ : requestAnimationFrame;
+
+ return new Promise<string | undefined>((ok, bad) => {
+ requestFrame(() => {
+ try {
+ const code = drawIntoCanvasAndGetQR(video, canvas);
+ ok(code);
+ } catch (error) {
+ bad(error);
+ }
+ });
+ });
+}
+
+async function createCanvasFromVideo(
+ video: HTMLVideoElement,
+ canvas: HTMLCanvasElement,
+): Promise<string> {
+ const context = canvas.getContext("2d", {
+ willReadFrequently: true,
+ });
+ if (!context) {
+ throw Error("no 2d canvas context");
+ }
+ canvas.width = video.videoWidth;
+ canvas.height = video.videoHeight;
+
+ let last = Date.now();
+
+ let found: string | undefined = undefined;
+ while (!found) {
+ const timeSinceLast = Date.now() - last;
+ if (timeSinceLast < TIME_BETWEEN_FRAMES) {
+ await delay(TIME_BETWEEN_FRAMES - timeSinceLast);
+ }
+ last = Date.now();
+ found = await readNextFrame(video, canvas);
+ }
+ video.pause();
+ return found;
+}
+
+async function createCanvasFromFile(
+ source: string,
+ canvas: HTMLCanvasElement,
+): Promise<string | undefined> {
+ const img = new Image(300, 300);
+ img.src = source;
+ canvas.width = img.width;
+ canvas.height = img.height;
+ return new Promise<string | undefined>((ok, bad) => {
+ img.addEventListener("load", () => {
+ try {
+ const code = drawIntoCanvasAndGetQR(img, canvas);
+ ok(code);
+ } catch (error) {
+ bad(error);
+ }
+ });
+ });
+}
+
+async function waitUntilReady(video: HTMLVideoElement): Promise<void> {
+ return new Promise((ok, _bad) => {
+ if (video.readyState === video.HAVE_ENOUGH_DATA) {
+ return ok();
+ }
+ setTimeout(waitUntilReady, 100);
+ });
}
export function QrReaderPage({ onDetected }: Props): VNode {
const videoRef = useRef<HTMLVideoElement>(null);
- // const imageRef = useRef<HTMLImageElement>(null);
- const qrScanner = useRef<QrScanner | null>(null);
- const [value, onChange] = useState("");
- const [active, setActive] = useState(false);
+ const canvasRef = useRef<HTMLCanvasElement>(null);
+ const [error, setError] = useState<TranslatedString | undefined>();
+ const [value, setValue] = useState("");
+ const [show, setShow] = useState<"canvas" | "video" | "nothing">("nothing");
+
const { i18n } = useTranslationContext();
- function start(): void {
- qrScanner.current!.start();
- onChange("");
- setActive(true);
+ function onChangeDetect(str: string) {
+ if (str) {
+ const uri = parseTalerUri(str);
+ if (!uri) {
+ setError(
+ i18n.str`URI is not valid. Taler URI should start with "taler://"`,
+ );
+ } else {
+ onDetected(uri);
+ setError(undefined);
+ }
+ } else {
+ setError(undefined);
+ }
+ setValue(str);
}
- function stop(): void {
- qrScanner.current!.stop();
- setActive(false);
+
+ function onChange(str: string) {
+ if (str) {
+ if (!parseTalerUri(str)) {
+ setError(
+ i18n.str`URI is not valid. Taler URI should start with "taler://"`,
+ );
+ } else {
+ setError(undefined);
+ }
+ } else {
+ setError(undefined);
+ }
+ setValue(str);
}
- function check(v: string) {
- return (
- v.startsWith("taler://") && classifyTalerUri(v) !== TalerUriType.Unknown
- );
+ async function startVideo() {
+ if (!videoRef.current || !canvasRef.current) {
+ return;
+ }
+ const video = videoRef.current;
+ if (!video || !video.played) return;
+ const stream = await navigator.mediaDevices.getUserMedia({
+ video: { facingMode: "environment" },
+ audio: false,
+ });
+ setShow("video");
+ setError(undefined);
+ video.srcObject = stream;
+ await video.play();
+ await waitUntilReady(video);
+ try {
+ const code = await createCanvasFromVideo(video, canvasRef.current);
+ if (code) {
+ onChangeDetect(code);
+ setShow("canvas");
+ }
+ stream.getTracks().forEach((e) => {
+ e.stop();
+ });
+ } catch (error) {
+ setError(i18n.str`something unexpected happen: ${error}`);
+ }
}
- useEffect(() => {
- if (!videoRef.current) {
- console.log("vide was not ready");
+ async function onFileRead(fileContent: string) {
+ if (!canvasRef.current) {
return;
}
- const elem = videoRef.current;
- setTimeout(() => {
- qrScanner.current = new QrScanner(
- elem,
- ({ data, cornerPoints }) => {
- if (check(data)) {
- onDetected(data);
- return;
- }
- onChange(data);
- stop();
- },
- {
- maxScansPerSecond: 5, //default 25
- highlightScanRegion: true,
- },
- );
- start();
- }, 1);
- return () => {
- qrScanner.current?.destroy();
- };
- }, []);
-
- const isValid = check(value);
+ setShow("nothing");
+ setError(undefined);
+ try {
+ const code = await createCanvasFromFile(fileContent, canvasRef.current);
+ if (code) {
+ onChangeDetect(code);
+ setShow("canvas");
+ } else {
+ setError(i18n.str`Could not found a QR code in the file`);
+ }
+ } catch (error) {
+ setError(i18n.str`something unexpected happen: ${error}`);
+ }
+ }
+ const uri = parseTalerUri(value);
+
return (
<Container>
- {/* <InputFile onChange={(f) => scanImage(imageRef, f)}>
- Read QR from file
- </InputFile>
- <div ref={imageRef} /> */}
- <h1>
- <i18n.Translate>
- Scan a QR code or enter taler:// URI below
- </i18n.Translate>
- </h1>
- <QrVideo ref={videoRef} />
- <TextField
- label="Taler URI"
- variant="standard"
- fullWidth
- value={value}
- onChange={onChange}
- />
- {isValid && (
- <Button variant="contained" onClick={async () => onDetected(value)}>
- <i18n.Translate>Open</i18n.Translate>
- </Button>
- )}
- {!active && !isValid && (
- <Fragment>
- <Alert severity="error">
- <i18n.Translate>
- URI is not valid. Taler URI should start with `taler://`
- </i18n.Translate>
- </Alert>
- <Button variant="contained" onClick={async () => start()}>
- <i18n.Translate>Try another</i18n.Translate>
- </Button>
- </Fragment>
- )}
+ <section>
+ <h1>
+ <i18n.Translate>
+ Scan a QR code or enter taler:// URI below
+ </i18n.Translate>
+ </h1>
+ <div style={{ justifyContent: "space-between", display: "flex" }}>
+ <div style={{ width: "75%" }}>
+ <TextField
+ label="Taler URI"
+ variant="filled"
+ fullWidth
+ value={value}
+ onChange={onChange}
+ />
+ </div>
+ {uri && (
+ <Button
+ disabled={!!error}
+ variant="contained"
+ color="success"
+ onClick={async () => {
+ if (uri) onDetected(uri);
+ }}
+ >
+ {(function (talerUri: TalerUri): VNode {
+ switch (talerUri.type) {
+ case TalerUriAction.Pay:
+ return <i18n.Translate>Pay invoice</i18n.Translate>;
+ case TalerUriAction.Withdraw:
+ return (
+ <i18n.Translate>Withdrawal from bank</i18n.Translate>
+ );
+ case TalerUriAction.Refund:
+ return <i18n.Translate>Claim refund</i18n.Translate>;
+ case TalerUriAction.PayPull:
+ return <i18n.Translate>Pay invoice</i18n.Translate>;
+ case TalerUriAction.PayPush:
+ return <i18n.Translate>Accept payment</i18n.Translate>;
+ case TalerUriAction.PayTemplate:
+ return <i18n.Translate>Complete order</i18n.Translate>;
+ case TalerUriAction.Restore:
+ return <i18n.Translate>Restore wallet</i18n.Translate>;
+ case TalerUriAction.DevExperiment:
+ return <i18n.Translate>Enable experiment</i18n.Translate>;
+ case TalerUriAction.WithdrawExchange:
+ return (
+ <i18n.Translate>Withdraw from exchange</i18n.Translate>
+ );
+ case TalerUriAction.AddExchange:
+ return <i18n.Translate>Add exchange</i18n.Translate>;
+ default: {
+ assertUnreachable(talerUri);
+ }
+ }
+ })(uri)}
+ </Button>
+ )}
+ </div>
+ <Grid container justifyContent="space-around" columns={2}>
+ <Grid item xs={2}>
+ <p>{error && <Alert severity="error">{error}</Alert>}</p>
+ </Grid>
+ <Grid item xs={2}>
+ <p>
+ <Button variant="contained" onClick={startVideo}>
+ Use Camera
+ </Button>
+ </p>
+ </Grid>
+ <EnabledBySettings name="advancedMode">
+ <Grid item xs={2}>
+ <InputFile onChange={onFileRead}>Read QR from file</InputFile>
+ </Grid>
+ </EnabledBySettings>
+ </Grid>
+ </section>
+ <div>
+ <video
+ ref={videoRef}
+ style={{ display: show === "video" ? "unset" : "none" }}
+ playsInline={true}
+ />
+ <canvas
+ id="este"
+ class={QrCanvas}
+ ref={canvasRef}
+ style={{ display: show === "canvas" ? "unset " : "none" }}
+ />
+ </div>
</Container>
);
}
-
-async function scanImage(
- imageRef: Ref<HTMLImageElement>,
- image: string,
-): Promise<void> {
- const imageEl = new Image();
- imageEl.src = image;
- imageEl.width = 200;
- imageRef.current!.appendChild(imageEl);
- QrScanner.scanImage(image, {
- alsoTryWithoutScanRegion: true,
- })
- .then((result) => console.log(result))
- .catch((error) => console.log(error || "No QR code found."));
-}
diff --git a/packages/taler-wallet-webextension/src/wallet/ReserveCreated.stories.tsx b/packages/taler-wallet-webextension/src/wallet/ReserveCreated.stories.tsx
deleted file mode 100644
index 7ea3b386b..000000000
--- a/packages/taler-wallet-webextension/src/wallet/ReserveCreated.stories.tsx
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import { parsePaytoUri } from "@gnu-taler/taler-util";
-import { createExample } from "../test-utils.js";
-import { ReserveCreated as TestedComponent } from "./ReserveCreated.js";
-
-export default {
- title: "reserve created",
- component: TestedComponent,
- argTypes: {},
-};
-
-export const TalerBank = createExample(TestedComponent, {
- reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG",
- paytoURI: parsePaytoUri(
- "payto://x-taler-bank/bank.taler:5882/exchangeminator?amount=COL%3A1&message=Taler+Withdrawal+A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG",
- ),
- amount: {
- currency: "USD",
- value: 10,
- fraction: 0,
- },
- exchangeBaseUrl: "https://exchange.demo.taler.net",
-});
-
-export const IBAN = createExample(TestedComponent, {
- reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG",
- paytoURI: parsePaytoUri(
- "payto://iban/ES8877998399652238?amount=COL%3A1&message=Taler+Withdrawal+A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG",
- ),
- amount: {
- currency: "USD",
- value: 10,
- fraction: 0,
- },
- exchangeBaseUrl: "https://exchange.demo.taler.net",
-});
-
-export const WithReceiverName = createExample(TestedComponent, {
- reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG",
- paytoURI: parsePaytoUri(
- "payto://iban/ES8877998399652238?amount=COL%3A1&message=Taler+Withdrawal+A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG&receiver=Sebastian",
- ),
- amount: {
- currency: "USD",
- value: 10,
- fraction: 0,
- },
- exchangeBaseUrl: "https://exchange.demo.taler.net",
-});
-
-export const Bitcoin = createExample(TestedComponent, {
- reservePub: "0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00",
- paytoURI: parsePaytoUri(
- "payto://bitcoin/bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4?amount=BTC:0.1&subject=0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00",
- ),
- amount: {
- currency: "BTC",
- value: 0,
- fraction: 14000000,
- },
- exchangeBaseUrl: "https://exchange.demo.taler.net",
-});
-
-export const BitcoinRegTest = createExample(TestedComponent, {
- reservePub: "0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00",
- paytoURI: parsePaytoUri(
- "payto://bitcoin/bcrt1q6ps8qs6v8tkqrnru4xqqqa6rfwcx5ufpdfqht4?amount=BTC:0.1&subject=0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00",
- ),
- amount: {
- currency: "BTC",
- value: 0,
- fraction: 14000000,
- },
- exchangeBaseUrl: "https://exchange.demo.taler.net",
-});
-export const BitcoinTest = createExample(TestedComponent, {
- reservePub: "0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00",
- paytoURI: parsePaytoUri(
- "payto://bitcoin/tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx?amount=BTC:0.1&subject=0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00",
- ),
- amount: {
- currency: "BTC",
- value: 0,
- fraction: 14000000,
- },
- exchangeBaseUrl: "https://exchange.demo.taler.net",
-});
diff --git a/packages/taler-wallet-webextension/src/wallet/ReserveCreated.tsx b/packages/taler-wallet-webextension/src/wallet/ReserveCreated.tsx
deleted file mode 100644
index a259f7c9a..000000000
--- a/packages/taler-wallet-webextension/src/wallet/ReserveCreated.tsx
+++ /dev/null
@@ -1,130 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-import { AmountJson, PaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util";
-import { Fragment, h, VNode } from "preact";
-import { Amount } from "../components/Amount.js";
-import { BankDetailsByPaytoType } from "../components/BankDetailsByPaytoType.js";
-import { CopyButton } from "../components/CopyButton.js";
-import { ErrorMessage } from "../components/ErrorMessage.js";
-import { QR } from "../components/QR.js";
-import { Title, WarningBox } from "../components/styled/index.js";
-import { useTranslationContext } from "../context/translation.js";
-import { Button } from "../mui/Button.js";
-export interface Props {
- reservePub: string;
- paytoURI: PaytoUri | undefined;
- exchangeBaseUrl: string;
- amount: AmountJson;
- onCancel: () => Promise<void>;
-}
-
-export function ReserveCreated({
- reservePub,
- paytoURI,
- onCancel,
- exchangeBaseUrl,
- amount,
-}: Props): VNode {
- const { i18n } = useTranslationContext();
- if (!paytoURI) {
- return (
- <ErrorMessage
- title={<i18n.Translate>Could not parse the payto URI</i18n.Translate>}
- description={<i18n.Translate>Please check the uri</i18n.Translate>}
- />
- );
- }
- function TransferDetails(): VNode {
- if (!paytoURI) return <Fragment />;
- return (
- <section>
- <BankDetailsByPaytoType
- amount={amount}
- exchangeBaseUrl={exchangeBaseUrl}
- payto={paytoURI}
- subject={reservePub}
- />
- <table>
- <tbody>
- <tr>
- <td>
- <pre>
- <b>
- <a
- target="_bank"
- rel="noreferrer"
- title="RFC 8905 for designating targets for payments"
- href="https://tools.ietf.org/html/rfc8905"
- >
- Payto URI
- </a>
- </b>
- </pre>
- </td>
- <td width="100%" style={{ wordBreak: "break-all" }}>
- {stringifyPaytoUri(paytoURI)}
- </td>
- <td>
- <CopyButton getContent={() => stringifyPaytoUri(paytoURI)} />
- </td>
- </tr>
- </tbody>
- </table>
- <p>
- <WarningBox>
- <i18n.Translate>
- Make sure to use the correct subject, otherwise the money will not
- arrive in this wallet.
- </i18n.Translate>
- </WarningBox>
- </p>
- </section>
- );
- }
-
- return (
- <Fragment>
- <section>
- <Title>
- <i18n.Translate>Exchange is ready for withdrawal</i18n.Translate>
- </Title>
- <p>
- <i18n.Translate>
- To complete the process you need to wire{` `}
- <b>{<Amount value={amount} />}</b> to the exchange bank account
- </i18n.Translate>
- </p>
- </section>
- <TransferDetails />
- <section>
- <p>
- <i18n.Translate>
- Alternative, you can also scan this QR code or open{" "}
- <a href={stringifyPaytoUri(paytoURI)}>this link</a> if you have a
- banking app installed that supports RFC 8905
- </i18n.Translate>
- </p>
- <QR text={stringifyPaytoUri(paytoURI)} />
- </section>
- <footer>
- <div />
- <Button variant="contained" color="error" onClick={onCancel}>
- <i18n.Translate>Cancel withdrawal</i18n.Translate>
- </Button>
- </footer>
- </Fragment>
- );
-}
diff --git a/packages/taler-wallet-webextension/src/wallet/Settings.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Settings.stories.tsx
index 04b7f3e09..cd43c4526 100644
--- a/packages/taler-wallet-webextension/src/wallet/Settings.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Settings.stories.tsx
@@ -19,8 +19,9 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { createExample } from "../test-utils.js";
+import * as tests from "@gnu-taler/web-util/testing";
import { SettingsView as TestedComponent } from "./Settings.js";
+import { WalletCoreVersion } from "@gnu-taler/taler-util";
export default {
title: "settings",
@@ -36,94 +37,55 @@ const version = {
merchant: "2:0:1",
bank: "0:0:0",
hash: "d439c3e1bc743f2aa47de4457953dba6ecb0e20f",
- version: "0.9.0-dev.1",
+ version: "1:2:3",
devMode: false,
- },
+ bankConversionApiRange: "0:0:0",
+ bankIntegrationApiRange: "0:0:0",
+ corebankApiRange: "0:0:0",
+ implementationGitHash: "d439c3e1bc743f2aa47de4457953dba6ecb0e20f",
+ implementationSemver: "0.9.0-dev.1",
+ } satisfies WalletCoreVersion,
webexVersion: {
version: "0.9.0.13",
hash: "d439c3e1bc743f2aa47de4457953dba6ecb0e20f",
},
};
-export const AllOff = createExample(TestedComponent, {
+export const AllOff = tests.createExample(TestedComponent, {
deviceName: "this-is-the-device-name",
- devModeToggle: { value: false, button: {} },
+ advanceToggle: { value: false, button: {} },
autoOpenToggle: { value: false, button: {} },
- clipboardToggle: { value: false, button: {} },
+ langToggle: { value: false, button: {} },
setDeviceName: () => Promise.resolve(),
...version,
});
-export const OneChecked = createExample(TestedComponent, {
+export const OneChecked = tests.createExample(TestedComponent, {
deviceName: "this-is-the-device-name",
- devModeToggle: { value: false, button: {} },
+ advanceToggle: { value: false, button: {} },
autoOpenToggle: { value: false, button: {} },
- clipboardToggle: { value: false, button: {} },
+ langToggle: { value: false, button: {} },
setDeviceName: () => Promise.resolve(),
...version,
});
-export const WithOneExchange = createExample(TestedComponent, {
+export const WithOneExchange = tests.createExample(TestedComponent, {
deviceName: "this-is-the-device-name",
- devModeToggle: { value: false, button: {} },
+ advanceToggle: { value: false, button: {} },
autoOpenToggle: { value: false, button: {} },
- clipboardToggle: { value: false, button: {} },
+ langToggle: { value: false, button: {} },
setDeviceName: () => Promise.resolve(),
- knownExchanges: [
- {
- currency: "USD",
- exchangeBaseUrl: "http://exchange.taler",
- tos: {
- currentVersion: "1",
- acceptedVersion: "1",
- content: "content of tos",
- contentType: "text/plain",
- },
- paytoUris: ["payto://x-taler-bank/bank.rpi.sebasjm.com/exchangeminator"],
- } as any, //TODO: complete with auditors, wireInfo and denominations
- ],
...version,
});
-export const WithExchangeInDifferentState = createExample(TestedComponent, {
- deviceName: "this-is-the-device-name",
- devModeToggle: { value: false, button: {} },
- autoOpenToggle: { value: false, button: {} },
- clipboardToggle: { value: false, button: {} },
- setDeviceName: () => Promise.resolve(),
- knownExchanges: [
- {
- currency: "USD",
- exchangeBaseUrl: "http://exchange1.taler",
- tos: {
- currentVersion: "1",
- acceptedVersion: "1",
- content: "content of tos",
- contentType: "text/plain",
- },
- paytoUris: ["payto://x-taler-bank/bank.rpi.sebasjm.com/exchangeminator"],
- },
- {
- currency: "USD",
- exchangeBaseUrl: "http://exchange2.taler",
- tos: {
- currentVersion: "2",
- acceptedVersion: "1",
- content: "content of tos",
- contentType: "text/plain",
- },
- paytoUris: ["payto://x-taler-bank/bank.rpi.sebasjm.com/exchangeminator"],
- } as any, //TODO: complete with auditors, wireInfo and denominations
- {
- currency: "USD",
- exchangeBaseUrl: "http://exchange3.taler",
- tos: {
- currentVersion: "1",
- content: "content of tos",
- contentType: "text/plain",
- },
- paytoUris: ["payto://x-taler-bank/bank.rpi.sebasjm.com/exchangeminator"],
- },
- ],
- ...version,
-});
+export const WithExchangeInDifferentState = tests.createExample(
+ TestedComponent,
+ {
+ deviceName: "this-is-the-device-name",
+ advanceToggle: { value: false, button: {} },
+ autoOpenToggle: { value: false, button: {} },
+ langToggle: { value: false, button: {} },
+ setDeviceName: () => Promise.resolve(),
+ ...version,
+ },
+);
diff --git a/packages/taler-wallet-webextension/src/wallet/Settings.tsx b/packages/taler-wallet-webextension/src/wallet/Settings.tsx
index c0268a1ae..0d0a31a2d 100644
--- a/packages/taler-wallet-webextension/src/wallet/Settings.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Settings.tsx
@@ -15,63 +15,76 @@
*/
import {
- ExchangeListItem,
- ExchangeTosStatus,
- WalletCoreVersion,
+ LibtoolVersion,
+ TranslatedString,
+ WalletCoreVersion
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-import { Fragment, h, VNode } from "preact";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
import { Checkbox } from "../components/Checkbox.js";
-import { ErrorTalerOperation } from "../components/ErrorTalerOperation.js";
-import { JustInDevMode } from "../components/JustInDevMode.js";
+import { EnabledBySettings } from "../components/EnabledBySettings.js";
import { Part } from "../components/Part.js";
import { SelectList } from "../components/SelectList.js";
import {
- DestructiveText,
Input,
- LinkPrimary,
SubTitle,
- SuccessText,
- WarningText,
+ WarningBox
} from "../components/styled/index.js";
-import { useDevContext } from "../context/devContext.js";
-import { useTranslationContext } from "../context/translation.js";
+import { useAlertContext } from "../context/alert.js";
+import { useBackendContext } from "../context/backend.js";
import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
-import { useAutoOpenPermissions } from "../hooks/useAutoOpenPermissions.js";
import { useBackupDeviceName } from "../hooks/useBackupDeviceName.js";
-import { useClipboardPermissions } from "../hooks/useClipboardPermissions.js";
+import { useSettings } from "../hooks/useSettings.js";
import { ToggleHandler } from "../mui/handlers.js";
-import { Pages } from "../NavigationBar.js";
-import { platform } from "../platform/api.js";
-import { wxApi } from "../wxApi.js";
+import { Settings } from "../platform/api.js";
+import { platform } from "../platform/foreground.js";
+import { WALLET_CORE_SUPPORTED_VERSION } from "../wxApi.js";
const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined;
export function SettingsPage(): VNode {
- const autoOpenToggle = useAutoOpenPermissions();
- const clipboardToggle = useClipboardPermissions();
- const { devModeToggle } = useDevContext();
+ const [settings, updateSettings] = useSettings();
+ const { safely } = useAlertContext();
const { name, update } = useBackupDeviceName();
const webex = platform.getWalletWebExVersion();
+ const api = useBackendContext();
- const exchangesHook = useAsyncAsHook(async () => {
- const list = await wxApi.wallet.call(WalletApiOperation.ListExchanges, {});
- const version = await wxApi.wallet.call(WalletApiOperation.GetVersion, {});
- return { exchanges: list.exchanges, version };
+ const hook = useAsyncAsHook(async () => {
+ const version = await api.wallet.call(WalletApiOperation.GetVersion, {});
+ return { version };
});
- const { exchanges, version } =
- !exchangesHook || exchangesHook.hasError
- ? { exchanges: [], version: undefined }
- : exchangesHook.response;
+
+ const version = hook && !hook.hasError ? hook.response.version : undefined
return (
<SettingsView
- knownExchanges={exchanges}
deviceName={name}
setDeviceName={update}
- autoOpenToggle={autoOpenToggle}
- clipboardToggle={clipboardToggle}
- devModeToggle={devModeToggle}
+ autoOpenToggle={{
+ value: settings.autoOpen,
+ button: {
+ onClick: safely("update support injection", async () => {
+ updateSettings("autoOpen", !settings.autoOpen);
+ }),
+ },
+ }}
+ advanceToggle={{
+ value: settings.advancedMode,
+ button: {
+ onClick: safely("update advance mode", async () => {
+ updateSettings("advancedMode", !settings.advancedMode);
+ }),
+ },
+ }}
+ langToggle={{
+ value: settings.langSelector,
+ button: {
+ onClick: safely("update lang selector", async () => {
+ updateSettings("langSelector", !settings.langSelector);
+ }),
+ },
+ }}
webexVersion={{
version: webex.version,
hash: GIT_HASH,
@@ -85,9 +98,8 @@ export interface ViewProps {
deviceName: string;
setDeviceName: (s: string) => Promise<void>;
autoOpenToggle: ToggleHandler;
- clipboardToggle: ToggleHandler;
- devModeToggle: ToggleHandler;
- knownExchanges: Array<ExchangeListItem>;
+ advanceToggle: ToggleHandler;
+ langToggle: ToggleHandler;
coreVersion: WalletCoreVersion | undefined;
webexVersion: {
version: string;
@@ -96,162 +108,95 @@ export interface ViewProps {
}
export function SettingsView({
- knownExchanges,
autoOpenToggle,
- clipboardToggle,
- devModeToggle,
+ advanceToggle,
+ langToggle,
coreVersion,
webexVersion,
}: ViewProps): VNode {
const { i18n, lang, supportedLang, changeLanguage } = useTranslationContext();
+ const api = useBackendContext();
+
return (
<Fragment>
<section>
- {autoOpenToggle.button.error && (
- <ErrorTalerOperation
- title={<i18n.Translate>Could not toggle auto-open</i18n.Translate>}
- error={autoOpenToggle.button.error.errorDetail}
- />
- )}
- {clipboardToggle.button.error && (
- <ErrorTalerOperation
- title={<i18n.Translate>Could not toggle clipboard</i18n.Translate>}
- error={clipboardToggle.button.error.errorDetail}
- />
- )}
<SubTitle>
<i18n.Translate>Navigator</i18n.Translate>
</SubTitle>
<Checkbox
- label={
- <i18n.Translate>
- Automatically open wallet based on page content
- </i18n.Translate>
- }
+ label={i18n.str`Automatically open wallet`}
name="autoOpen"
description={
<i18n.Translate>
- Enabling this option below will make using the wallet faster, but
- requires more permissions from your browser.
+ Open the wallet when a payment action is found.
</i18n.Translate>
}
enabled={autoOpenToggle.value!}
onToggle={autoOpenToggle.button.onClick!}
/>
- <Checkbox
- label={
- <i18n.Translate>
- Automatically check clipboard for Taler URI
- </i18n.Translate>
- }
- name="clipboard"
- description={
- <i18n.Translate>
- Enabling this option below will make using the wallet faster, but
- requires more permissions from your browser.
- </i18n.Translate>
- }
- enabled={clipboardToggle.value!}
- onToggle={clipboardToggle.button.onClick!}
- />
<SubTitle>
- <i18n.Translate>Trust</i18n.Translate>
+ <i18n.Translate>Version Info</i18n.Translate>
</SubTitle>
- {!knownExchanges || !knownExchanges.length ? (
- <div>
- <i18n.Translate>No exchange yet</i18n.Translate>
- </div>
- ) : (
+ <Part
+ title={i18n.str`Web Extension`}
+ text={
+ <span>
+ {webexVersion.version}{" "}
+ <EnabledBySettings name="advancedMode">
+ {webexVersion.hash}
+ </EnabledBySettings>
+ </span>
+ }
+ />
+ {coreVersion && (
<Fragment>
- <table>
- <thead>
- <tr>
- <th>
- <i18n.Translate>Currency</i18n.Translate>
- </th>
- <th>
- <i18n.Translate>URL</i18n.Translate>
- </th>
- <th>
- <i18n.Translate>Term of Service</i18n.Translate>
- </th>
- </tr>
- </thead>
- <tbody>
- {knownExchanges.map((e, idx) => {
- function Status(): VNode {
- switch (e.tosStatus) {
- case ExchangeTosStatus.Accepted:
- return (
- <SuccessText>
- <i18n.Translate>ok</i18n.Translate>
- </SuccessText>
- );
- case ExchangeTosStatus.Changed:
- return (
- <WarningText>
- <i18n.Translate>changed</i18n.Translate>
- </WarningText>
- );
- case ExchangeTosStatus.New:
- case ExchangeTosStatus.NotFound:
- return (
- <DestructiveText>
- <i18n.Translate>not accepted</i18n.Translate>
- </DestructiveText>
- );
- case ExchangeTosStatus.Unknown:
- default:
- return (
- <DestructiveText>
- <i18n.Translate>
- unknown (exchange status should be updated)
- </i18n.Translate>
- </DestructiveText>
- );
- }
- }
- return (
- <tr key={idx}>
- <td>{e.currency}</td>
- <td>
- <a href={e.exchangeBaseUrl}>{e.exchangeBaseUrl}</a>
- </td>
- <td>
- <Status />
- </td>
- </tr>
- );
- })}
- </tbody>
- </table>
+ {LibtoolVersion.compare(
+ coreVersion.version,
+ WALLET_CORE_SUPPORTED_VERSION,
+ )?.compatible ? undefined : (
+ <WarningBox>
+ <i18n.Translate>
+ The version of wallet core is not supported. (supported
+ version: {WALLET_CORE_SUPPORTED_VERSION}, wallet version: {coreVersion.version})
+ </i18n.Translate>
+ </WarningBox>
+ )}
+ <EnabledBySettings name="advancedMode">
+ <Part
+ title={i18n.str`Exchange compatibility`}
+ text={<span>{coreVersion.exchange}</span>}
+ />
+ <Part
+ title={i18n.str`Merchant compatibility`}
+ text={<span>{coreVersion.merchant}</span>}
+ />
+ <Part
+ title={i18n.str`Bank compatibility`}
+ text={<span>{coreVersion.bank}</span>}
+ />
+ <Part
+ title={i18n.str`Wallet Core compatibility`}
+ text={<span>{coreVersion.version}</span>}
+ />
+ </EnabledBySettings>
</Fragment>
)}
- <div style={{ display: "flex", justifyContent: "space-between" }}>
- <div />
- <LinkPrimary href={Pages.settingsExchangeAdd({})}>
- <i18n.Translate>Add an exchange</i18n.Translate>
- </LinkPrimary>
- </div>
-
<SubTitle>
- <i18n.Translate>Troubleshooting</i18n.Translate>
+ <i18n.Translate>Settings</i18n.Translate>
</SubTitle>
<Checkbox
- label={<i18n.Translate>Developer mode</i18n.Translate>}
+ label={i18n.str`Enable developer mode`}
name="devMode"
- description={
- <i18n.Translate>
- More options and information useful for debugging
- </i18n.Translate>
- }
- enabled={devModeToggle.value!}
- onToggle={devModeToggle.button.onClick!}
+ description={i18n.str`Show more information and options in the UI`}
+ enabled={advanceToggle.value!}
+ onToggle={advanceToggle.button.onClick!}
/>
-
- <JustInDevMode>
+ <EnabledBySettings name="advancedMode">
+ <AdvanceSettings />
+ </EnabledBySettings>
+ <EnabledBySettings name="langSelector">
<SubTitle>
<i18n.Translate>Display</i18n.Translate>
</SubTitle>
@@ -264,46 +209,81 @@ export function SettingsView({
onChange={(v) => changeLanguage(v)}
/>
</Input>
- </JustInDevMode>
- <SubTitle>
- <i18n.Translate>Version</i18n.Translate>
- </SubTitle>
- {coreVersion && (
- <Part
- title={<i18n.Translate>Wallet Core</i18n.Translate>}
- text={
- <span>
- {coreVersion.version}{" "}
- <JustInDevMode>{coreVersion.hash}</JustInDevMode>
- </span>
- }
- />
- )}
- <Part
- title={<i18n.Translate>Web Extension</i18n.Translate>}
- text={
- <span>
- {webexVersion.version}{" "}
- <JustInDevMode>{webexVersion.hash}</JustInDevMode>
- </span>
- }
- />
- {coreVersion && (
- <JustInDevMode>
- <Part
- title={<i18n.Translate>Exchange compatibility</i18n.Translate>}
- text={<span>{coreVersion.exchange}</span>}
- />
- <Part
- title={<i18n.Translate>Merchant compatibility</i18n.Translate>}
- text={<span>{coreVersion.merchant}</span>}
- />
- <Part
- title={<i18n.Translate>Bank compatibility</i18n.Translate>}
- text={<span>{coreVersion.bank}</span>}
+ </EnabledBySettings>
+ </section>
+ </Fragment>
+ );
+}
+
+type Info = { label: TranslatedString; description: TranslatedString };
+type Options = {
+ [k in keyof Settings]?: Info;
+};
+function AdvanceSettings(): VNode {
+ const [settings, updateSettings] = useSettings();
+ const api = useBackendContext();
+ const { i18n } = useTranslationContext();
+ const o: Options = {
+ backup: {
+ label: i18n.str`Show backup feature`,
+ description: i18n.str`Backup integration still in beta.`,
+ },
+ suspendIndividualTransaction: {
+ label: i18n.str`Show suspend/resume transaction`,
+ description: i18n.str`Prevent transaction from doing network request.`,
+ },
+ showRefeshTransactions: {
+ label: i18n.str`Show refresh transaction type in the transaction list`,
+ description: i18n.str`Refresh transaction will be hidden by default if the refresh operation doesn't have fee.`,
+ },
+ extendedAccountTypes: {
+ label: i18n.str`Show more account types on deposit`,
+ description: i18n.str`Extends the UI to more payment target types.`,
+ },
+ showJsonOnError: {
+ label: i18n.str`Show JSON on error`,
+ description: i18n.str`Print more information about the error. Useful for debugging.`,
+ },
+ walletAllowHttp: {
+ label: i18n.str`Allow HTTP connections`,
+ description: i18n.str`Using HTTP connection may be faster but unsafe (wallet restart required)`,
+ },
+ langSelector: {
+ label: i18n.str`Lang selector`,
+ description: i18n.str`Allows to manually change the language of the UI. Otherwise it will be automatically selected by your browser configuration.`,
+ },
+ showExchangeManagement: {
+ label: i18n.str`Edit exchange management`,
+ description: i18n.str`Allows to see the list of exchange, remove, add and switch before withdrawal.`,
+ },
+ selectTosFormat: {
+ label: i18n.str`Select terms of service format`,
+ description: i18n.str`Allows to render the terms of service on different format selected by the user.`,
+ },
+ showWalletActivity: {
+ label: i18n.str`Show wallet activity`,
+ description: i18n.str`Show the wallet notification and observability event in the UI.`,
+ },
+ };
+ return (
+ <Fragment>
+ <section>
+ {Object.entries(o).map(([name, { label, description }]) => {
+ const settingsName = name as keyof Settings;
+ return (
+ <Checkbox
+ label={label}
+ name={name}
+ key={name}
+ description={description}
+ enabled={settings[settingsName]}
+ onToggle={async () => {
+ updateSettings(settingsName, !settings[settingsName]);
+ await api.background.call("reinitWallet", undefined);
+ }}
/>
- </JustInDevMode>
- )}
+ );
+ })}
</section>
</Fragment>
);
diff --git a/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx
index 868d3b0e6..194f0e0bb 100644
--- a/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx
@@ -21,10 +21,16 @@
import {
AbsoluteTime,
+ AmountString,
PaymentStatus,
+ RefreshReason,
+ TalerPreciseTimestamp,
TalerProtocolTimestamp,
TransactionCommon,
TransactionDeposit,
+ TransactionIdStr,
+ TransactionMajorState,
+ TransactionMinorState,
TransactionPayment,
TransactionPeerPullCredit,
TransactionPeerPullDebit,
@@ -32,17 +38,13 @@ import {
TransactionPeerPushDebit,
TransactionRefresh,
TransactionRefund,
- TransactionTip,
TransactionType,
TransactionWithdrawal,
WithdrawalDetails,
- WithdrawalType,
+ WithdrawalType
} from "@gnu-taler/taler-util";
-import { DevContextProviderForTesting } from "../context/devContext.js";
-import {
- createExample,
- createExampleWithCustomContext as createExampleInCustomContext,
-} from "../test-utils.js";
+import * as tests from "@gnu-taler/web-util/testing";
+import beer from "../../static-dev/beer.png";
import { TransactionView as TestedComponent } from "./Transaction.js";
export default {
@@ -51,19 +53,26 @@ export default {
argTypes: {
onRetry: { action: "onRetry" },
onDelete: { action: "onDelete" },
+ onCancel: { action: "onCancel" },
onBack: { action: "onBack" },
},
};
-const commonTransaction = {
- amountRaw: "KUDOS:11",
- amountEffective: "KUDOS:9.2",
- pending: false,
+const commonTransaction: TransactionCommon = {
+ error: undefined,
+ amountRaw: "KUDOS:11" as AmountString,
+ amountEffective: "KUDOS:9.2" as AmountString,
+ txState: {
+ major: TransactionMajorState.Done,
+ },
+ txActions: [],
timestamp: TalerProtocolTimestamp.now(),
- transactionId: "txn:deposit:12",
- frozen: false,
+ transactionId: "txn:deposit:12" as TransactionIdStr,
type: TransactionType.Deposit,
-} as TransactionCommon;
+} as Omit<
+ Omit<Omit<TransactionCommon, "extendedStatus">, "frozen">,
+ "pending"
+> as TransactionCommon;
import merchantIcon from "../../static-dev/merchant-icon.jpeg";
@@ -73,6 +82,7 @@ const exampleData = {
type: TransactionType.Withdrawal,
exchangeBaseUrl: "http://exchange.taler",
withdrawalDetails: {
+ reserveIsReady: false,
confirmed: false,
reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG",
exchangePaytoUris: ["payto://x-taler-bank/bank.demo.taler.net/Exchange"],
@@ -81,8 +91,9 @@ const exampleData = {
} as TransactionWithdrawal,
payment: {
...commonTransaction,
- amountEffective: "KUDOS:12",
+ amountEffective: "KUDOS:12" as AmountString,
type: TransactionType.Payment,
+ posConfirmation: undefined,
info: {
contractTermsHash: "ASDZXCASD",
merchant: {
@@ -102,10 +113,11 @@ const exampleData = {
},
refunds: [],
refundPending: undefined,
- totalRefundEffective: "KUDOS:0",
- totalRefundRaw: "KUDOS:0",
+ totalRefundEffective: "KUDOS:0" as AmountString,
+ totalRefundRaw: "KUDOS:0" as AmountString,
proposalId: "1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0",
status: PaymentStatus.Accepted,
+ refundQueryActive: false,
} as TransactionPayment,
deposit: {
...commonTransaction,
@@ -119,33 +131,22 @@ const exampleData = {
refresh: {
...commonTransaction,
type: TransactionType.Refresh,
+ refreshInputAmount: "KUDOS:1" as AmountString,
+ refreshOutputAmount: "KUDOS:0.5" as AmountString,
exchangeBaseUrl: "http://exchange.taler",
+ refreshReason: RefreshReason.Manual,
} as TransactionRefresh,
- tip: {
- ...commonTransaction,
- type: TransactionType.Tip,
- // merchant: {
- // name: "the merchant",
- // logo: merchantIcon,
- // website: "https://www.themerchant.taler",
- // email: "contact@merchant.taler",
- // },
- merchantBaseUrl: "http://merchant.taler",
- } as TransactionTip,
refund: {
...commonTransaction,
type: TransactionType.Refund,
refundedTransactionId:
"payment:1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0",
- info: {
- contractTermsHash: "ASDZXCASD",
+ paymentInfo: {
merchant: {
- name: "the merchant",
+ name: "The merchant",
},
- orderId: "2021.167-03NPY6MCYMVGT",
- products: [],
summary: "Essay: Why the Devil's Advocate Doesn't Help Reach the Truth",
- fulfillmentMessage: "",
+ summary_i18n: {},
},
refundPending: undefined,
} as TransactionRefund,
@@ -159,6 +160,7 @@ const exampleData = {
summary: "take this money",
completed: true,
},
+ kycUrl: undefined,
exchangeBaseUrl: "https://exchange.taler.net",
} as TransactionPeerPushCredit,
push_debit: {
@@ -187,6 +189,7 @@ const exampleData = {
summary: "pay me, please?",
completed: true,
},
+ kycUrl: undefined,
exchangeBaseUrl: "https://exchange.taler.net",
} as TransactionPeerPullCredit,
pull_debit: {
@@ -214,68 +217,85 @@ const transactionError = {
hint: "The payment is too late, the offer has expired.",
},
},
+ when: AbsoluteTime.now(),
hint: "Error: WALLET_UNEXPECTED_REQUEST_ERROR",
message: "Unexpected error code in response",
};
-export const Withdraw = createExample(TestedComponent, {
+export const Withdraw = tests.createExample(TestedComponent, {
transaction: exampleData.withdraw,
});
-export const WithdrawFiveMinutesAgo = createExample(TestedComponent, () => ({
- transaction: {
- ...exampleData.withdraw,
- timestamp: TalerProtocolTimestamp.fromSeconds(
- new Date().getTime() / 1000 - 60 * 5,
- ),
- },
-}));
+export const WithdrawFiveMinutesAgo = tests.createExample(
+ TestedComponent,
+ () => ({
+ transaction: {
+ ...exampleData.withdraw,
+ timestamp: TalerPreciseTimestamp.fromSeconds(
+ new Date().getTime() / 1000 - 60 * 5,
+ ),
+ },
+ }),
+);
-export const WithdrawFiveMinutesAgoAndPending = createExample(
+export const WithdrawFiveMinutesAgoAndPending = tests.createExample(
TestedComponent,
() => ({
transaction: {
...exampleData.withdraw,
- timestamp: TalerProtocolTimestamp.fromSeconds(
+ timestamp: TalerPreciseTimestamp.fromSeconds(
new Date().getTime() / 1000 - 60 * 5,
),
- pending: true,
+ txState: {
+ major: TransactionMajorState.Pending,
+ },
},
}),
);
-export const WithdrawError = createExample(TestedComponent, {
+export const WithdrawError = tests.createExample(TestedComponent, {
transaction: {
...exampleData.withdraw,
error: transactionError,
},
});
-export const WithdrawErrorInDevMode = createExampleInCustomContext(
+export const WithdrawErrorKYC = tests.createExample(TestedComponent, {
+ transaction: {
+ ...exampleData.withdraw,
+ txState: {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.KycRequired,
+ },
+ kycUrl:
+ "http://localhost:6666/oauth/v2/login?client_id=taler-exchange&redirect_uri=http%3A%2F%2Flocalhost%3A8081%2F%2Fkyc-proof%2F59WFS5VXXY3CEE25BM45XPB7ZCDQZNZ46PJCMNXK05P65T9M1X90%2FKYC-PROVIDER-MYPROV%2F1",
+ },
+});
+
+export const WithdrawPendingManual = tests.createExample(
TestedComponent,
- {
+ () => ({
transaction: {
...exampleData.withdraw,
- error: transactionError,
+ withdrawalDetails: {
+ type: WithdrawalType.ManualTransfer,
+ exchangePaytoUris: ["payto://iban/ES8877998399652238"],
+ reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG",
+ exchangeCreditAccountDetails: [{
+ paytoUri: "payto://IBAN/1231231231",
+ },
+ {
+ paytoUri: "payto://IBAN/2342342342",
+ }],
+ } as WithdrawalDetails,
+ txState: {
+ major: TransactionMajorState.Pending,
+ },
},
- },
- DevContextProviderForTesting,
- { value: true },
+ }),
);
-export const WithdrawPendingManual = createExample(TestedComponent, () => ({
- transaction: {
- ...exampleData.withdraw,
- withdrawalDetails: {
- type: WithdrawalType.ManualTransfer,
- exchangePaytoUris: ["payto://iban/ES8877998399652238"],
- reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG",
- } as WithdrawalDetails,
- pending: true,
- },
-}));
-
-export const WithdrawPendingTalerBankUnconfirmed = createExample(
+export const WithdrawPendingTalerBankUnconfirmed = tests.createExample(
TestedComponent,
{
transaction: {
@@ -283,15 +303,18 @@ export const WithdrawPendingTalerBankUnconfirmed = createExample(
withdrawalDetails: {
type: WithdrawalType.TalerBankIntegrationApi,
confirmed: false,
+ reserveIsReady: false,
reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG",
bankConfirmationUrl: "http://bank.demo.taler.net",
},
- pending: true,
+ txState: {
+ major: TransactionMajorState.Pending,
+ },
},
},
);
-export const WithdrawPendingTalerBankConfirmed = createExample(
+export const WithdrawPendingTalerBankConfirmed = tests.createExample(
TestedComponent,
{
transaction: {
@@ -299,131 +322,144 @@ export const WithdrawPendingTalerBankConfirmed = createExample(
withdrawalDetails: {
type: WithdrawalType.TalerBankIntegrationApi,
confirmed: true,
+ reserveIsReady: false,
reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG",
},
- pending: true,
+ txState: {
+ major: TransactionMajorState.Pending,
+ },
},
},
);
-export const Payment = createExample(TestedComponent, {
+export const Payment = tests.createExample(TestedComponent, {
transaction: exampleData.payment,
});
-export const PaymentError = createExample(TestedComponent, {
+export const PaymentWithPosConfirmation = tests.createExample(TestedComponent, {
+ transaction: {
+ ...exampleData.payment,
+ posConfirmation: "123123\n3345345\n567567",
+ },
+});
+
+export const PaymentError = tests.createExample(TestedComponent, {
transaction: {
...exampleData.payment,
error: transactionError,
},
});
-export const PaymentWithRefund = createExample(TestedComponent, {
+export const PaymentWithRefund = tests.createExample(TestedComponent, {
transaction: {
...exampleData.payment,
- amountRaw: "KUDOS:12",
- totalRefundEffective: "KUDOS:1",
- totalRefundRaw: "KUDOS:1",
+ amountRaw: "KUDOS:12" as AmountString,
+ totalRefundEffective: "KUDOS:1" as AmountString,
+ totalRefundRaw: "KUDOS:1" as AmountString,
refunds: [
{
transactionId: "1123123",
- amountRaw: "KUDOS:1",
- amountEffective: "KUDOS:1",
+ amountRaw: "KUDOS:1" as AmountString,
+ amountEffective: "KUDOS:1" as AmountString,
timestamp: TalerProtocolTimestamp.fromSeconds(1546546544),
},
],
},
});
-export const PaymentWithDeliveryDate = createExample(TestedComponent, {
+export const PaymentWithDeliveryDate = tests.createExample(TestedComponent, {
transaction: {
...exampleData.payment,
- amountRaw: "KUDOS:12",
+ amountRaw: "KUDOS:12" as AmountString,
info: {
...exampleData.payment.info,
- delivery_date: {
- t_s: new Date().getTime() / 1000,
- },
+ // delivery_date: {
+ // t_s: new Date().getTime() / 1000,
+ // },
},
},
});
-export const PaymentWithDeliveryAddr = createExample(TestedComponent, {
+export const PaymentWithDeliveryAddr = tests.createExample(TestedComponent, {
transaction: {
...exampleData.payment,
- amountRaw: "KUDOS:12",
+ amountRaw: "KUDOS:12" as AmountString,
info: {
...exampleData.payment.info,
- delivery_location: {
- country: "Argentina",
- street: "Elm Street",
- district: "CABA",
- post_code: "1101",
- },
+ // delivery_location: {
+ // country: "Argentina",
+ // street: "Elm Street",
+ // district: "CABA",
+ // post_code: "1101",
+ // },
},
},
});
-export const PaymentWithDeliveryFull = createExample(TestedComponent, {
+export const PaymentWithDeliveryFull = tests.createExample(TestedComponent, {
transaction: {
...exampleData.payment,
- amountRaw: "KUDOS:12",
+ amountRaw: "KUDOS:12" as AmountString,
info: {
...exampleData.payment.info,
- delivery_date: {
- t_s: new Date().getTime() / 1000,
- },
- delivery_location: {
- country: "Argentina",
- street: "Elm Street",
- district: "CABA",
- post_code: "1101",
- },
+ // delivery_date: {
+ // t_s: new Date().getTime() / 1000,
+ // },
+ // delivery_location: {
+ // country: "Argentina",
+ // street: "Elm Street",
+ // district: "CABA",
+ // post_code: "1101",
+ // },
},
},
});
-export const PaymentWithRefundPending = createExample(TestedComponent, {
+export const PaymentWithRefundPending = tests.createExample(TestedComponent, {
transaction: {
...exampleData.payment,
- amountRaw: "KUDOS:12",
- refundPending: "KUDOS:3",
- totalRefundEffective: "KUDOS:1",
- totalRefundRaw: "KUDOS:1",
+ amountRaw: "KUDOS:12" as AmountString,
+ refundPending: "KUDOS:3" as AmountString,
+ totalRefundEffective: "KUDOS:1" as AmountString,
+ totalRefundRaw: "KUDOS:1" as AmountString,
},
});
-export const PaymentWithFeeAndRefund = createExample(TestedComponent, {
+export const PaymentWithFeeAndRefund = tests.createExample(TestedComponent, {
transaction: {
...exampleData.payment,
- amountRaw: "KUDOS:11",
- totalRefundEffective: "KUDOS:1",
- totalRefundRaw: "KUDOS:1",
+ amountRaw: "KUDOS:11" as AmountString,
+ totalRefundEffective: "KUDOS:1" as AmountString,
+ totalRefundRaw: "KUDOS:1" as AmountString,
},
});
-export const PaymentWithFeeAndRefundFee = createExample(TestedComponent, {
+export const PaymentWithFeeAndRefundFee = tests.createExample(TestedComponent, {
transaction: {
...exampleData.payment,
- amountRaw: "KUDOS:11",
- totalRefundEffective: "KUDOS:1",
- totalRefundRaw: "KUDOS:2",
+ amountRaw: "KUDOS:11" as AmountString,
+ totalRefundEffective: "KUDOS:1" as AmountString,
+ totalRefundRaw: "KUDOS:2" as AmountString,
},
});
-export const PaymentWithoutFee = createExample(TestedComponent, {
+export const PaymentWithoutFee = tests.createExample(TestedComponent, {
transaction: {
...exampleData.payment,
- amountRaw: "KUDOS:12",
+ amountRaw: "KUDOS:12" as AmountString,
},
});
-export const PaymentPending = createExample(TestedComponent, {
- transaction: { ...exampleData.payment, pending: true },
+export const PaymentPending = tests.createExample(TestedComponent, {
+ transaction: {
+ ...exampleData.payment,
+ txState: {
+ major: TransactionMajorState.Pending,
+ },
+ },
});
-import beer from "../../static-dev/beer.png";
-
-export const PaymentWithProducts = createExample(TestedComponent, {
+export const PaymentWithProducts = tests.createExample(TestedComponent, {
transaction: {
...exampleData.payment,
info: {
@@ -460,7 +496,7 @@ export const PaymentWithProducts = createExample(TestedComponent, {
} as TransactionPayment,
});
-export const PaymentWithLongSummary = createExample(TestedComponent, {
+export const PaymentWithLongSummary = tests.createExample(TestedComponent, {
transaction: {
...exampleData.payment,
info: {
@@ -484,108 +520,107 @@ export const PaymentWithLongSummary = createExample(TestedComponent, {
} as TransactionPayment,
});
-export const Deposit = createExample(TestedComponent, {
+export const Deposit = tests.createExample(TestedComponent, {
transaction: exampleData.deposit,
});
-export const DepositTalerBank = createExample(TestedComponent, {
+export const DepositTalerBank = tests.createExample(TestedComponent, {
transaction: {
...exampleData.deposit,
targetPaytoUri: "payto://x-taler-bank/bank.demo.taler.net/Exchange",
},
});
-export const DepositBitcoin = createExample(TestedComponent, {
+export const DepositBitcoin = tests.createExample(TestedComponent, {
transaction: {
...exampleData.deposit,
- amountRaw: "BITCOINBTC:0.0000011",
- amountEffective: "BITCOINBTC:0.00000092",
+ amountRaw: "BITCOINBTC:0.0000011" as AmountString,
+ amountEffective: "BITCOINBTC:0.00000092" as AmountString,
targetPaytoUri:
"payto://bitcoin/bcrt1q6ps8qs6v8tkqrnru4xqqqa6rfwcx5ufpdfqht4?amount=BTC:0.1&subject=0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00",
},
});
-export const DepositIBAN = createExample(TestedComponent, {
+export const DepositIBAN = tests.createExample(TestedComponent, {
transaction: {
...exampleData.deposit,
targetPaytoUri: "payto://iban/ES8877998399652238",
},
});
-export const DepositError = createExample(TestedComponent, {
+export const DepositError = tests.createExample(TestedComponent, {
transaction: {
...exampleData.deposit,
error: transactionError,
},
});
-export const DepositPending = createExample(TestedComponent, {
- transaction: { ...exampleData.deposit, pending: true },
+export const DepositPending = tests.createExample(TestedComponent, {
+ transaction: {
+ ...exampleData.deposit,
+ txState: {
+ major: TransactionMajorState.Pending,
+ },
+ },
});
-export const Refresh = createExample(TestedComponent, {
+export const Refresh = tests.createExample(TestedComponent, {
transaction: exampleData.refresh,
});
-export const RefreshError = createExample(TestedComponent, {
+export const RefreshError = tests.createExample(TestedComponent, {
transaction: {
...exampleData.refresh,
error: transactionError,
},
});
-export const Tip = createExample(TestedComponent, {
- transaction: exampleData.tip,
+export const Refund = tests.createExample(TestedComponent, {
+ transaction: exampleData.refund,
});
-export const TipError = createExample(TestedComponent, {
+export const RefundError = tests.createExample(TestedComponent, {
transaction: {
- ...exampleData.tip,
+ ...exampleData.refund,
error: transactionError,
},
});
-export const TipPending = createExample(TestedComponent, {
- transaction: { ...exampleData.tip, pending: true },
-});
-
-export const Refund = createExample(TestedComponent, {
- transaction: exampleData.refund,
-});
-
-export const RefundError = createExample(TestedComponent, {
+export const RefundPending = tests.createExample(TestedComponent, {
transaction: {
...exampleData.refund,
- error: transactionError,
+ txState: {
+ major: TransactionMajorState.Pending,
+ },
},
});
-export const RefundPending = createExample(TestedComponent, {
- transaction: { ...exampleData.refund, pending: true },
-});
-
-export const InvoiceCreditComplete = createExample(TestedComponent, {
+export const InvoiceCreditComplete = tests.createExample(TestedComponent, {
transaction: { ...exampleData.pull_credit },
});
-export const InvoiceCreditIncomplete = createExample(TestedComponent, {
+export const InvoiceCreditIncomplete = tests.createExample(TestedComponent, {
transaction: {
...exampleData.pull_credit,
- pending: true,
+ txState: {
+ major: TransactionMajorState.Pending,
+ },
},
});
-export const InvoiceDebit = createExample(TestedComponent, {
+export const InvoiceDebit = tests.createExample(TestedComponent, {
transaction: { ...exampleData.pull_debit },
});
-export const TransferCredit = createExample(TestedComponent, {
+export const TransferCredit = tests.createExample(TestedComponent, {
transaction: { ...exampleData.push_credit },
});
-export const TransferDebitComplete = createExample(TestedComponent, {
+export const TransferDebitComplete = tests.createExample(TestedComponent, {
transaction: { ...exampleData.push_debit },
});
-export const TransferDebitIncomplete = createExample(TestedComponent, {
+export const TransferDebitIncomplete = tests.createExample(TestedComponent, {
transaction: {
...exampleData.push_debit,
- pending: true,
+ txState: {
+ major: TransactionMajorState.Pending,
+ },
},
});
diff --git a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx
index e70f5fbd1..1f0293352 100644
--- a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx
@@ -18,76 +18,84 @@ import {
AbsoluteTime,
AmountJson,
Amounts,
- Location,
+ AmountString,
+ DenomLossEventType,
MerchantInfo,
NotificationType,
OrderShortInfo,
parsePaytoUri,
PaytoUri,
stringifyPaytoUri,
- TalerProtocolTimestamp,
+ TalerErrorCode,
+ TalerPreciseTimestamp,
Transaction,
+ TransactionAction,
TransactionDeposit,
- TransactionRefresh,
- TransactionRefund,
- TransactionTip,
+ TransactionIdStr,
+ TransactionInternalWithdrawal,
+ TransactionMajorState,
+ TransactionMinorState,
TransactionType,
+ TransactionWithdrawal,
+ TranslatedString,
WithdrawalType,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { styled } from "@linaria/react";
-import { differenceInSeconds } from "date-fns";
+import { isPast } from "date-fns";
import { ComponentChildren, Fragment, h, VNode } from "preact";
import { useEffect, useState } from "preact/hooks";
-import emptyImg from "../../static/img/empty.png";
import { Amount } from "../components/Amount.js";
import { BankDetailsByPaytoType } from "../components/BankDetailsByPaytoType.js";
-import { CopyButton } from "../components/CopyButton.js";
-import { ErrorTalerOperation } from "../components/ErrorTalerOperation.js";
+import { AlertView, ErrorAlertView } from "../components/CurrentAlerts.js";
+import { EnabledBySettings } from "../components/EnabledBySettings.js";
import { Loading } from "../components/Loading.js";
-import { LoadingError } from "../components/LoadingError.js";
-import { Kind, Part, PartCollapsible, PartPayto } from "../components/Part.js";
+import { Kind, Part, PartPayto } from "../components/Part.js";
import { QR } from "../components/QR.js";
import { ShowFullContractTermPopup } from "../components/ShowFullContractTermPopup.js";
import {
CenteredDialog,
+ ErrorBox,
InfoBox,
- ListOfProducts,
+ Link,
Overlay,
- Row,
SmallLightText,
SubTitle,
+ SvgIcon,
WarningBox,
} from "../components/styled/index.js";
import { Time } from "../components/Time.js";
-import { useTranslationContext } from "../context/translation.js";
+import { alertFromError, useAlertContext } from "../context/alert.js";
+import { useBackendContext } from "../context/backend.js";
import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
+import { useSettings } from "../hooks/useSettings.js";
import { Button } from "../mui/Button.js";
+import { SafeHandler } from "../mui/handlers.js";
import { Pages } from "../NavigationBar.js";
-import { wxApi } from "../wxApi.js";
+import refreshIcon from "../svg/refresh_24px.inline.svg";
+import { assertUnreachable } from "../utils/index.js";
interface Props {
tid: string;
goToWalletHistory: (currency?: string) => Promise<void>;
}
-export function TransactionPage({
- tid: transactionId,
- goToWalletHistory,
-}: Props): VNode {
+export function TransactionPage({ tid, goToWalletHistory }: Props): VNode {
+ const transactionId = tid as TransactionIdStr; //FIXME: validate
const { i18n } = useTranslationContext();
-
+ const api = useBackendContext();
const state = useAsyncAsHook(
() =>
- wxApi.wallet.call(WalletApiOperation.GetTransactionById, {
+ api.wallet.call(WalletApiOperation.GetTransactionById, {
transactionId,
}),
[transactionId],
);
useEffect(() =>
- wxApi.listener.onUpdateNotification(
- [NotificationType.WithdrawGroupFinished],
+ api.listener.onUpdateNotification(
+ [NotificationType.TransactionStateTransition],
state?.retry,
),
);
@@ -98,13 +106,12 @@ export function TransactionPage({
if (state.hasError) {
return (
- <LoadingError
- title={
- <i18n.Translate>
- Could not load the transaction information
- </i18n.Translate>
- }
- error={state}
+ <ErrorAlertView
+ error={alertFromError(
+ i18n,
+ i18n.str`Could not load transaction information`,
+ state,
+ )}
/>
);
}
@@ -114,24 +121,45 @@ export function TransactionPage({
return (
<TransactionView
transaction={state.response}
- onSend={async () => {
- null;
+ onCancel={async () => {
+ await api.wallet.call(WalletApiOperation.FailTransaction, {
+ transactionId,
+ });
+ goToWalletHistory(currency);
}}
- onDelete={async () => {
- await wxApi.wallet.call(WalletApiOperation.DeleteTransaction, {
+ onSuspend={async () => {
+ await api.wallet.call(WalletApiOperation.SuspendTransaction, {
+ transactionId,
+ });
+ goToWalletHistory(currency);
+ }}
+ onResume={async () => {
+ await api.wallet.call(WalletApiOperation.ResumeTransaction, {
+ transactionId,
+ });
+ goToWalletHistory(currency);
+ }}
+ onAbort={async () => {
+ await api.wallet.call(WalletApiOperation.AbortTransaction, {
transactionId,
});
goToWalletHistory(currency);
}}
onRetry={async () => {
- await wxApi.wallet.call(WalletApiOperation.RetryTransaction, {
+ await api.wallet.call(WalletApiOperation.RetryTransaction, {
transactionId,
});
goToWalletHistory(currency);
}}
- onRefund={async (purchaseId) => {
- await wxApi.wallet.call(WalletApiOperation.ApplyRefundFromPurchaseId, {
- purchaseId,
+ onDelete={async () => {
+ await api.wallet.call(WalletApiOperation.DeleteTransaction, {
+ transactionId,
+ });
+ goToWalletHistory(currency);
+ }}
+ onRefund={async (transactionId) => {
+ await api.wallet.call(WalletApiOperation.StartRefundQuery, {
+ transactionId,
});
}}
onBack={() => goToWalletHistory(currency)}
@@ -141,10 +169,13 @@ export function TransactionPage({
export interface WalletTransactionProps {
transaction: Transaction;
- onSend: () => Promise<void>;
+ onCancel: () => Promise<void>;
+ onSuspend: () => Promise<void>;
+ onResume: () => Promise<void>;
+ onAbort: () => Promise<void>;
onDelete: () => Promise<void>;
onRetry: () => Promise<void>;
- onRefund: (id: string) => Promise<void>;
+ onRefund: (id: TransactionIdStr) => Promise<void>;
onBack: () => Promise<void>;
}
@@ -156,18 +187,32 @@ const PurchaseDetailsTable = styled.table`
}
`;
-export function TransactionView({
+type TransactionTemplateProps = Omit<
+ Omit<WalletTransactionProps, "onRefund">,
+ "onBack"
+> & {
+ children: ComponentChildren;
+};
+
+function TransactionTemplate({
transaction,
onDelete,
onRetry,
- onSend,
- onRefund,
-}: WalletTransactionProps): VNode {
+ onAbort,
+ onResume,
+ onSuspend,
+ onCancel,
+ children,
+}: TransactionTemplateProps): VNode {
+ const { i18n } = useTranslationContext();
const [confirmBeforeForget, setConfirmBeforeForget] = useState(false);
+ const [confirmBeforeCancel, setConfirmBeforeCancel] = useState(false);
+ const { safely } = useAlertContext();
+ const [settings] = useSettings();
async function doCheckBeforeForget(): Promise<void> {
if (
- transaction.pending &&
+ transaction.txState.major === TransactionMajorState.Pending &&
transaction.type === TransactionType.Withdrawal
) {
setConfirmBeforeForget(true);
@@ -176,76 +221,87 @@ export function TransactionView({
}
}
- const SHOWING_RETRY_THRESHOLD_SECS = 30;
-
- const { i18n } = useTranslationContext();
+ async function doCheckBeforeCancel(): Promise<void> {
+ setConfirmBeforeCancel(true);
+ }
- function TransactionTemplate({
- children,
- }: {
- children: ComponentChildren;
- }): VNode {
- const showSend = false;
- // (transaction.type === TransactionType.PeerPullCredit ||
- // transaction.type === TransactionType.PeerPushDebit) &&
- // !transaction.info.completed;
- const showRetry =
- transaction.error !== undefined ||
- transaction.timestamp.t_s === "never" ||
- (transaction.pending &&
- differenceInSeconds(new Date(), transaction.timestamp.t_s * 1000) >
- SHOWING_RETRY_THRESHOLD_SECS);
+ const showButton = getShowButtonStates(transaction);
- return (
- <Fragment>
- <section style={{ padding: 8, textAlign: "center" }}>
- <ErrorTalerOperation
- title={
+ return (
+ <Fragment>
+ <section style={{ padding: 8, textAlign: "center" }}>
+ {transaction?.error &&
+ // FIXME: wallet core should stop sending this error on KYC
+ transaction.error.code !==
+ TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED ? (
+ <ErrorAlertView
+ error={alertFromError(
+ i18n,
+ i18n.str`There was an error trying to complete the transaction.`,
+ transaction.error,
+ )}
+ />
+ ) : undefined}
+ {transaction.txState.major === TransactionMajorState.Pending &&
+ (transaction.txState.minor === TransactionMinorState.KycRequired ? (
+ <AlertView
+ alert={{
+ type: "warning",
+ message: i18n.str`KYC check required for the transaction to complete.`,
+ description:
+ transaction.kycUrl &&
+ typeof transaction.kycUrl === "string" ? (
+ <div>
+ <i18n.Translate>
+ Follow this link to the{` `}
+ <a
+ rel="noreferrer"
+ target="_bank"
+ href={transaction.kycUrl}
+ >
+ KYC verifier.
+ </a>
+ </i18n.Translate>
+ </div>
+ ) : (
+ i18n.str`No additional information has been provided.`
+ ),
+ }}
+ />
+ ) : transaction.txState.minor ===
+ TransactionMinorState.AmlRequired ? (
+ <WarningBox>
<i18n.Translate>
- There was an error trying to complete the transaction
+ The transaction has been blocked since the account required an
+ AML check.
</i18n.Translate>
- }
- error={transaction?.error}
- />
- {transaction.pending && (
+ </WarningBox>
+ ) : (
<WarningBox>
- <i18n.Translate>This transaction is not completed</i18n.Translate>
+ <div style={{ justifyContent: "center", lineHeight: "25px" }}>
+ <i18n.Translate>
+ This transaction is not completed
+ </i18n.Translate>
+ <Link onClick={onRetry} style={{ padding: 0 }}>
+ <SvgIcon
+ title={i18n.str`Retry`}
+ dangerouslySetInnerHTML={{ __html: refreshIcon }}
+ color="black"
+ />
+ </Link>
+ </div>
</WarningBox>
- )}
- </section>
- <section>{children}</section>
- <footer>
- <div>
- {showSend ? (
- <Button variant="contained" onClick={onSend}>
- <i18n.Translate>Send</i18n.Translate>
- </Button>
- ) : null}
- </div>
- <div>
- {showRetry ? (
- <Button variant="contained" onClick={onRetry}>
- <i18n.Translate>Retry</i18n.Translate>
- </Button>
- ) : null}
- <Button
- variant="contained"
- color="error"
- onClick={doCheckBeforeForget}
- >
- <i18n.Translate>Forget</i18n.Translate>
- </Button>
- </div>
- </footer>
- </Fragment>
- );
- }
-
- if (transaction.type === TransactionType.Withdrawal) {
- const total = Amounts.parseOrThrow(transaction.amountEffective);
- const chosen = Amounts.parseOrThrow(transaction.amountRaw);
- return (
- <TransactionTemplate>
+ ))}
+ {transaction.txState.major === TransactionMajorState.Aborted && (
+ <InfoBox>
+ <i18n.Translate>This transaction was aborted.</i18n.Translate>
+ </InfoBox>
+ )}
+ {transaction.txState.major === TransactionMajorState.Failed && (
+ <ErrorBox>
+ <i18n.Translate>This transaction failed.</i18n.Translate>
+ </ErrorBox>
+ )}
{confirmBeforeForget ? (
<Overlay>
<CenteredDialog>
@@ -262,118 +318,205 @@ export function TransactionView({
<Button
variant="contained"
color="secondary"
- onClick={async () => setConfirmBeforeForget(false)}
+ onClick={
+ (async () =>
+ setConfirmBeforeForget(false)) as SafeHandler<void>
+ }
>
<i18n.Translate>Cancel</i18n.Translate>
</Button>
- <Button variant="contained" color="error" onClick={onDelete}>
+ <Button
+ variant="contained"
+ color="error"
+ onClick={safely("delete transaction", onDelete)}
+ >
<i18n.Translate>Confirm</i18n.Translate>
</Button>
</footer>
</CenteredDialog>
</Overlay>
) : undefined}
+ {confirmBeforeCancel ? (
+ <Overlay>
+ <CenteredDialog>
+ <header>
+ <i18n.Translate>Caution!</i18n.Translate>
+ </header>
+ <section>
+ <i18n.Translate>
+ Doing a cancellation while the transaction still active might
+ result in lost coins. Do you still want to cancel the
+ transaction?
+ </i18n.Translate>
+ </section>
+ <footer>
+ <Button
+ variant="contained"
+ color="secondary"
+ onClick={
+ (async () =>
+ setConfirmBeforeCancel(false)) as SafeHandler<void>
+ }
+ >
+ <i18n.Translate>No</i18n.Translate>
+ </Button>
+
+ <Button
+ variant="contained"
+ color="error"
+ onClick={safely("cancel active transaction", onCancel)}
+ >
+ <i18n.Translate>Yes</i18n.Translate>
+ </Button>
+ </footer>
+ </CenteredDialog>
+ </Overlay>
+ ) : undefined}
+ </section>
+ <section>{children}</section>
+ <footer>
+ <div />
+ <div>
+ {showButton.abort && (
+ <Button
+ variant="contained"
+ onClick={safely("abort transaction", onAbort)}
+ >
+ <i18n.Translate>Abort</i18n.Translate>
+ </Button>
+ )}
+ {showButton.resume && settings.suspendIndividualTransaction && (
+ <Button
+ variant="contained"
+ onClick={safely("resume transaction", onResume)}
+ >
+ <i18n.Translate>Resume</i18n.Translate>
+ </Button>
+ )}
+ {showButton.suspend && settings.suspendIndividualTransaction && (
+ <Button
+ variant="contained"
+ onClick={safely("suspend transaction", onSuspend)}
+ >
+ <i18n.Translate>Suspend</i18n.Translate>
+ </Button>
+ )}
+ {showButton.fail && (
+ <Button
+ variant="contained"
+ color="error"
+ onClick={doCheckBeforeCancel as SafeHandler<void>}
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </Button>
+ )}
+ {showButton.remove && (
+ <Button
+ variant="contained"
+ color="error"
+ onClick={doCheckBeforeForget as SafeHandler<void>}
+ >
+ <i18n.Translate>Delete</i18n.Translate>
+ </Button>
+ )}
+ </div>
+ </footer>
+ </Fragment>
+ );
+}
+
+export function TransactionView({
+ transaction,
+ onDelete,
+ onAbort,
+ // onBack,
+ onResume,
+ onSuspend,
+ onRetry,
+ onRefund,
+ onCancel,
+}: WalletTransactionProps): VNode {
+ const { i18n } = useTranslationContext();
+ const { safely } = useAlertContext();
+
+ const raw = Amounts.parseOrThrow(transaction.amountRaw);
+ const effective = Amounts.parseOrThrow(transaction.amountEffective);
+
+ if (
+ transaction.type === TransactionType.Withdrawal ||
+ transaction.type === TransactionType.InternalWithdrawal
+ ) {
+ // const conversion =
+ // transaction.withdrawalDetails.type === WithdrawalType.ManualTransfer
+ // ? transaction.withdrawalDetails.exchangeCreditAccountDetails ?? []
+ // : [];
+ const blockedByKycOrAml =
+ transaction.txState.minor === TransactionMinorState.KycRequired ||
+ transaction.txState.minor === TransactionMinorState.AmlRequired;
+ return (
+ <TransactionTemplate
+ transaction={transaction}
+ onDelete={onDelete}
+ onRetry={onRetry}
+ onAbort={onAbort}
+ onResume={onResume}
+ onSuspend={onSuspend}
+ onCancel={onCancel}
+ >
<Header
timestamp={transaction.timestamp}
type={i18n.str`Withdrawal`}
- total={total}
+ total={effective}
kind="positive"
>
{transaction.exchangeBaseUrl}
</Header>
- {!transaction.pending ? undefined : transaction.withdrawalDetails
- .type === WithdrawalType.ManualTransfer ? (
+ {transaction.txState.major !== TransactionMajorState.Pending ||
+ blockedByKycOrAml ? undefined : transaction.withdrawalDetails.type ===
+ WithdrawalType.ManualTransfer &&
+ transaction.withdrawalDetails.exchangeCreditAccountDetails ? (
<Fragment>
- <BankDetailsByPaytoType
- amount={chosen}
- exchangeBaseUrl={transaction.exchangeBaseUrl}
- payto={parsePaytoUri(
- transaction.withdrawalDetails.exchangePaytoUris[0],
+ <InfoBox>
+ {transaction.withdrawalDetails.exchangeCreditAccountDetails
+ .length > 1 ? (
+ <span>
+ <i18n.Translate>
+ Now the payment service provider is waiting for{" "}
+ <Amount value={raw} /> to be transferred. Select one of the
+ accounts and use the information below to complete the
+ operation by making a wire transfer from your bank account.
+ </i18n.Translate>
+ </span>
+ ) : (
+ <span>
+ <i18n.Translate>
+ Now the payment service provider is waiting for{" "}
+ <Amount value={raw} /> to be transferred. Use the
+ information below to complete the operation by making a wire
+ transfer from your bank account.
+ </i18n.Translate>
+ </span>
)}
+ </InfoBox>
+ <BankDetailsByPaytoType
+ amount={raw}
+ accounts={
+ transaction.withdrawalDetails.exchangeCreditAccountDetails ?? []
+ }
subject={transaction.withdrawalDetails.reservePub}
/>
- <table>
- <tbody>
- <tr>
- <td>
- <pre>
- <b>
- <a
- target="_bank"
- rel="noreferrer"
- title="RFC 8905 for designating targets for payments"
- href="https://tools.ietf.org/html/rfc8905"
- >
- Payto URI
- </a>
- </b>
- </pre>
- </td>
- <td width="100%" style={{ wordBreak: "break-all" }}>
- {transaction.withdrawalDetails.exchangePaytoUris[0]}
- </td>
- <td>
- <CopyButton
- getContent={() =>
- transaction.withdrawalDetails.type ===
- WithdrawalType.ManualTransfer
- ? transaction.withdrawalDetails.exchangePaytoUris[0]
- : ""
- }
- />
- </td>
- </tr>
- </tbody>
- </table>
- <WarningBox>
- <i18n.Translate>
- Make sure to use the correct subject, otherwise the money will
- not arrive in this wallet.
- </i18n.Translate>
- </WarningBox>
</Fragment>
) : (
- <Fragment>
- {!transaction.withdrawalDetails.confirmed &&
- transaction.withdrawalDetails.bankConfirmationUrl ? (
- <InfoBox>
- <div style={{ display: "block" }}>
- <i18n.Translate>
- Wire transfer need a confirmation. Go to the
- <a
- href={transaction.withdrawalDetails.bankConfirmationUrl}
- target="_blank"
- rel="noreferrer"
- style={{ display: "inline" }}
- >
- <i18n.Translate>bank site</i18n.Translate>
- </a>{" "}
- and check wire transfer operation to exchange account is
- complete.
- </i18n.Translate>
- </div>
- </InfoBox>
- ) : undefined}
- {transaction.withdrawalDetails.confirmed && (
- <InfoBox>
- <i18n.Translate>
- Bank has confirmed the wire transfer. Waiting for the exchange
- to send the coins
- </i18n.Translate>
- </InfoBox>
- )}
- </Fragment>
+ //integrated bank withdrawal
+ <ShowWithdrawalDetailForBankIntegrated transaction={transaction} />
)}
<Part
- title={<i18n.Translate>Details</i18n.Translate>}
+ title={i18n.str`Details`}
text={
<WithdrawDetails
- amount={{
- effective: Amounts.parseOrThrow(transaction.amountEffective),
- raw: Amounts.parseOrThrow(transaction.amountRaw),
- }}
+ amount={getAmountWithFee(effective, raw, "credit")}
/>
}
/>
@@ -387,21 +530,23 @@ export function TransactionView({
? undefined
: Amounts.parseOrThrow(transaction.refundPending);
- const price = {
- raw: Amounts.parseOrThrow(transaction.amountRaw),
- effective: Amounts.parseOrThrow(transaction.amountEffective),
- };
- const refund = {
- raw: Amounts.parseOrThrow(transaction.totalRefundRaw),
- effective: Amounts.parseOrThrow(transaction.totalRefundEffective),
- };
- const total = Amounts.sub(price.effective, refund.effective).amount;
+ const effectiveRefund = Amounts.parseOrThrow(
+ transaction.totalRefundEffective,
+ );
return (
- <TransactionTemplate>
+ <TransactionTemplate
+ transaction={transaction}
+ onDelete={onDelete}
+ onAbort={onAbort}
+ onResume={onResume}
+ onSuspend={onSuspend}
+ onRetry={onRetry}
+ onCancel={onCancel}
+ >
<Header
timestamp={transaction.timestamp}
- total={total}
+ total={effective}
type={i18n.str`Payment`}
kind="negative"
>
@@ -420,7 +565,7 @@ export function TransactionView({
<br />
{transaction.refunds.length > 0 ? (
<Part
- title={<i18n.Translate>Refunds</i18n.Translate>}
+ title={i18n.str`Refunds`}
text={
<table>
{transaction.refunds.map((r, i) => {
@@ -439,12 +584,13 @@ export function TransactionView({
on{" "}
{
<Time
- timestamp={AbsoluteTime.fromTimestamp(
+ timestamp={AbsoluteTime.fromProtocolTimestamp(
r.timestamp,
)}
format="dd MMMM yyyy"
/>
}
+ .
</i18n.Translate>
</td>
</tr>
@@ -457,225 +603,260 @@ export function TransactionView({
) : undefined}
{pendingRefund !== undefined && Amounts.isNonZero(pendingRefund) && (
<InfoBox>
- <i18n.Translate>
- Merchant created a refund for this order but was not automatically
- picked up.
- </i18n.Translate>
+ {transaction.refundQueryActive ? (
+ <i18n.Translate>Refund is in progress.</i18n.Translate>
+ ) : (
+ <i18n.Translate>
+ Merchant created a refund for this order but was not
+ automatically picked up.
+ </i18n.Translate>
+ )}
<Part
- title={<i18n.Translate>Offer</i18n.Translate>}
+ title={i18n.str`Offer`}
text={<Amount value={pendingRefund} />}
kind="positive"
/>
- <div>
- <div />
+ {transaction.refundQueryActive ? undefined : (
<div>
- <Button
- variant="contained"
- onClick={() => onRefund(transaction.proposalId)}
- >
- <i18n.Translate>Accept</i18n.Translate>
- </Button>
+ <div />
+ <div>
+ <Button
+ variant="contained"
+ onClick={safely("refund transaction", () =>
+ onRefund(transaction.transactionId),
+ )}
+ >
+ <i18n.Translate>Accept</i18n.Translate>
+ </Button>
+ </div>
</div>
- </div>
+ )}
</InfoBox>
)}
+ {transaction.posConfirmation ? (
+ <AlertView
+ alert={{
+ type: "info",
+ message: i18n.str`Confirmation code`,
+ description: <pre>{transaction.posConfirmation}</pre>,
+ }}
+ />
+ ) : undefined}
<Part
- title={<i18n.Translate>Merchant</i18n.Translate>}
+ title={i18n.str`Merchant`}
text={<MerchantDetails merchant={transaction.info.merchant} />}
kind="neutral"
/>
<Part
- title={<i18n.Translate>Invoice ID</i18n.Translate>}
- text={transaction.info.orderId}
+ title={i18n.str`Invoice ID`}
+ text={transaction.info.orderId as TranslatedString}
kind="neutral"
/>
<Part
- title={<i18n.Translate>Details</i18n.Translate>}
+ title={i18n.str`Details`}
text={
<PurchaseDetails
- price={price}
- refund={refund}
+ price={getAmountWithFee(effective, raw, "debit")}
+ effectiveRefund={effectiveRefund}
info={transaction.info}
- proposalId={transaction.proposalId}
/>
}
kind="neutral"
/>
+ <ShowFullContractTermPopup transactionId={transaction.transactionId} />
</TransactionTemplate>
);
}
if (transaction.type === TransactionType.Deposit) {
- const total = Amounts.parseOrThrow(transaction.amountRaw);
const payto = parsePaytoUri(transaction.targetPaytoUri);
+
+ const wireTime = AbsoluteTime.fromProtocolTimestamp(
+ transaction.wireTransferDeadline,
+ );
+ const shouldBeWired = wireTime.t_ms !== "never" && isPast(wireTime.t_ms);
return (
- <TransactionTemplate>
+ <TransactionTemplate
+ transaction={transaction}
+ onDelete={onDelete}
+ onRetry={onRetry}
+ onAbort={onAbort}
+ onResume={onResume}
+ onSuspend={onSuspend}
+ onCancel={onCancel}
+ >
<Header
timestamp={transaction.timestamp}
type={i18n.str`Deposit`}
- total={total}
+ total={effective}
kind="negative"
>
{!payto ? transaction.targetPaytoUri : <NicePayto payto={payto} />}
</Header>
{payto && <PartPayto payto={payto} kind="neutral" />}
<Part
- title={<i18n.Translate>Details</i18n.Translate>}
- text={<DepositDetails transaction={transaction} />}
- kind="neutral"
- />
- <Part
- title={<i18n.Translate>Wire transfer deadline</i18n.Translate>}
+ title={i18n.str`Details`}
text={
- <Time
- timestamp={AbsoluteTime.fromTimestamp(
- transaction.wireTransferDeadline,
- )}
- format="dd MMMM yyyy 'at' HH:mm"
+ <DepositDetails
+ amount={getAmountWithFee(effective, raw, "debit")}
/>
}
kind="neutral"
/>
+ {!shouldBeWired ? (
+ <Part
+ title={i18n.str`Wire transfer deadline.`}
+ text={
+ <Time timestamp={wireTime} format="dd MMMM yyyy 'at' HH:mm" />
+ }
+ kind="neutral"
+ />
+ ) : transaction.wireTransferProgress === 0 ? (
+ <AlertView
+ alert={{
+ type: "warning",
+ message: i18n.str`Wire transfer is not initiated.`,
+ description: i18n.str` `,
+ }}
+ />
+ ) : transaction.wireTransferProgress === 100 ? (
+ <Fragment>
+ <AlertView
+ alert={{
+ type: "success",
+ message: i18n.str`Wire transfer completed.`,
+ description: i18n.str` `,
+ }}
+ />
+ <Part
+ title={i18n.str`Transfer details`}
+ text={
+ <TrackingDepositDetails
+ trackingState={transaction.trackingState}
+ />
+ }
+ kind="neutral"
+ />
+ </Fragment>
+ ) : (
+ <AlertView
+ alert={{
+ type: "info",
+ message: i18n.str`Wire transfer in progress.`,
+ description: i18n.str` `,
+ }}
+ />
+ )}
</TransactionTemplate>
);
}
if (transaction.type === TransactionType.Refresh) {
- const total = Amounts.sub(
- Amounts.parseOrThrow(transaction.amountRaw),
- Amounts.parseOrThrow(transaction.amountEffective),
- ).amount;
-
return (
- <TransactionTemplate>
+ <TransactionTemplate
+ transaction={transaction}
+ onDelete={onDelete}
+ onRetry={onRetry}
+ onAbort={onAbort}
+ onResume={onResume}
+ onSuspend={onSuspend}
+ onCancel={onCancel}
+ >
<Header
timestamp={transaction.timestamp}
type={i18n.str`Refresh`}
- total={total}
+ total={effective}
kind="negative"
>
- {transaction.exchangeBaseUrl}
- </Header>
- <Part
- title={<i18n.Translate>Details</i18n.Translate>}
- text={<RefreshDetails transaction={transaction} />}
- />
- </TransactionTemplate>
- );
- }
-
- if (transaction.type === TransactionType.Tip) {
- const total = Amounts.parseOrThrow(transaction.amountEffective);
-
- return (
- <TransactionTemplate>
- <Header
- timestamp={transaction.timestamp}
- type={i18n.str`Tip`}
- total={total}
- kind="positive"
- >
- {transaction.merchantBaseUrl}
+ {"Refresh"}
</Header>
- {/* <Part
- title={<i18n.Translate>Merchant</i18n.Translate>}
- text={<MerchantDetails merchant={transaction.merchant} />}
- kind="neutral"
- /> */}
<Part
- title={<i18n.Translate>Details</i18n.Translate>}
- text={<TipDetails transaction={transaction} />}
+ title={i18n.str`Details`}
+ text={
+ <RefreshDetails
+ amount={getAmountWithFee(effective, raw, "debit")}
+ />
+ }
/>
</TransactionTemplate>
);
}
if (transaction.type === TransactionType.Refund) {
- const total = Amounts.parseOrThrow(transaction.amountEffective);
return (
- <TransactionTemplate>
+ <TransactionTemplate
+ transaction={transaction}
+ onDelete={onDelete}
+ onRetry={onRetry}
+ onAbort={onAbort}
+ onResume={onResume}
+ onSuspend={onSuspend}
+ onCancel={onCancel}
+ >
<Header
timestamp={transaction.timestamp}
type={i18n.str`Refund`}
- total={total}
+ total={effective}
kind="positive"
>
- {transaction.info.summary}
- </Header>
-
- <Part
- title={<i18n.Translate>Merchant</i18n.Translate>}
- text={transaction.info.merchant.name}
- kind="neutral"
- />
- <Part
- title={<i18n.Translate>Original order ID</i18n.Translate>}
- text={
+ {transaction.paymentInfo ? (
<a
href={Pages.balanceTransaction({
tid: transaction.refundedTransactionId,
})}
>
- {transaction.info.orderId}
+ {transaction.paymentInfo.summary}
</a>
+ ) : (
+ <span style={{ color: "gray" }}>-- deleted --</span>
+ )}
+ </Header>
+
+ <Part
+ title={i18n.str`Merchant`}
+ text={
+ (transaction.paymentInfo
+ ? transaction.paymentInfo.merchant.name
+ : "-- deleted --") as TranslatedString
}
kind="neutral"
/>
<Part
- title={<i18n.Translate>Purchase summary</i18n.Translate>}
- text={transaction.info.summary}
+ title={i18n.str`Purchase summary`}
+ text={
+ (transaction.paymentInfo
+ ? transaction.paymentInfo.summary
+ : "-- deleted --") as TranslatedString
+ }
kind="neutral"
/>
<Part
- title={<i18n.Translate>Details</i18n.Translate>}
- text={<RefundDetails transaction={transaction} />}
+ title={i18n.str`Details`}
+ text={
+ <RefundDetails
+ amount={getAmountWithFee(effective, raw, "credit")}
+ />
+ }
/>
</TransactionTemplate>
);
}
- function ShowQrWithCopy({ text }: { text: string }): VNode {
- const [showing, setShowing] = useState(false);
- async function copy(): Promise<void> {
- navigator.clipboard.writeText(text);
- }
- async function toggle(): Promise<void> {
- setShowing((s) => !s);
- }
- if (showing) {
- return (
- <div>
- <QR text={text} />
- <Button onClick={copy}>
- <i18n.Translate>copy</i18n.Translate>
- </Button>
- <Button onClick={toggle}>
- <i18n.Translate>hide qr</i18n.Translate>
- </Button>
- </div>
- );
- }
- return (
- <div>
- <div>{text.substring(0, 64)}...</div>
- <Button onClick={copy}>
- <i18n.Translate>copy</i18n.Translate>
- </Button>
- <Button onClick={toggle}>
- <i18n.Translate>show qr</i18n.Translate>
- </Button>
- </div>
- );
- }
-
if (transaction.type === TransactionType.PeerPullCredit) {
- const total = Amounts.parseOrThrow(transaction.amountEffective);
return (
- <TransactionTemplate>
+ <TransactionTemplate
+ transaction={transaction}
+ onDelete={onDelete}
+ onRetry={onRetry}
+ onAbort={onAbort}
+ onResume={onResume}
+ onSuspend={onSuspend}
+ onCancel={onCancel}
+ >
<Header
timestamp={transaction.timestamp}
type={i18n.str`Credit`}
- total={total}
+ total={effective}
kind="positive"
>
<i18n.Translate>Invoice</i18n.Translate>
@@ -683,31 +864,31 @@ export function TransactionView({
{transaction.info.summary ? (
<Part
- title={<i18n.Translate>Subject</i18n.Translate>}
- text={transaction.info.summary}
+ title={i18n.str`Subject`}
+ text={transaction.info.summary as TranslatedString}
kind="neutral"
/>
) : undefined}
<Part
- title={<i18n.Translate>Exchange</i18n.Translate>}
- text={transaction.exchangeBaseUrl}
+ title={i18n.str`Exchange`}
+ text={transaction.exchangeBaseUrl as TranslatedString}
kind="neutral"
/>
- {transaction.pending /** pending is not-pay */ && (
- <Part
- title={<i18n.Translate>URI</i18n.Translate>}
- text={<ShowQrWithCopy text={transaction.talerUri} />}
- kind="neutral"
- />
- )}
+ {transaction.txState.major === TransactionMajorState.Pending &&
+ transaction.txState.minor === TransactionMinorState.Ready &&
+ transaction.talerUri &&
+ !transaction.error && (
+ <Part
+ title={i18n.str`URI`}
+ text={<ShowQrWithCopy text={transaction.talerUri} />}
+ kind="neutral"
+ />
+ )}
<Part
- title={<i18n.Translate>Details</i18n.Translate>}
+ title={i18n.str`Details`}
text={
- <InvoiceDetails
- amount={{
- effective: Amounts.parseOrThrow(transaction.amountEffective),
- raw: Amounts.parseOrThrow(transaction.amountRaw),
- }}
+ <InvoiceCreationDetails
+ amount={getAmountWithFee(effective, raw, "credit")}
/>
}
/>
@@ -716,13 +897,20 @@ export function TransactionView({
}
if (transaction.type === TransactionType.PeerPullDebit) {
- const total = Amounts.parseOrThrow(transaction.amountEffective);
return (
- <TransactionTemplate>
+ <TransactionTemplate
+ transaction={transaction}
+ onDelete={onDelete}
+ onRetry={onRetry}
+ onAbort={onAbort}
+ onResume={onResume}
+ onSuspend={onSuspend}
+ onCancel={onCancel}
+ >
<Header
timestamp={transaction.timestamp}
type={i18n.str`Debit`}
- total={total}
+ total={effective}
kind="negative"
>
<i18n.Translate>Invoice</i18n.Translate>
@@ -730,34 +918,40 @@ export function TransactionView({
{transaction.info.summary ? (
<Part
- title={<i18n.Translate>Subject</i18n.Translate>}
- text={transaction.info.summary}
+ title={i18n.str`Subject`}
+ text={transaction.info.summary as TranslatedString}
kind="neutral"
/>
) : undefined}
<Part
- title={<i18n.Translate>Exchange</i18n.Translate>}
- text={transaction.exchangeBaseUrl}
+ title={i18n.str`Exchange`}
+ text={transaction.exchangeBaseUrl as TranslatedString}
kind="neutral"
/>
<Part
- title={<i18n.Translate>Details</i18n.Translate>}
+ title={i18n.str`Details`}
text={
- <InvoiceDetails
- amount={{
- effective: Amounts.parseOrThrow(transaction.amountEffective),
- raw: Amounts.parseOrThrow(transaction.amountRaw),
- }}
+ <InvoicePaymentDetails
+ amount={getAmountWithFee(effective, raw, "debit")}
/>
}
/>
</TransactionTemplate>
);
}
+
if (transaction.type === TransactionType.PeerPushDebit) {
const total = Amounts.parseOrThrow(transaction.amountEffective);
return (
- <TransactionTemplate>
+ <TransactionTemplate
+ transaction={transaction}
+ onDelete={onDelete}
+ onRetry={onRetry}
+ onAbort={onAbort}
+ onResume={onResume}
+ onSuspend={onSuspend}
+ onCancel={onCancel}
+ >
<Header
timestamp={transaction.timestamp}
type={i18n.str`Debit`}
@@ -769,31 +963,28 @@ export function TransactionView({
{transaction.info.summary ? (
<Part
- title={<i18n.Translate>Subject</i18n.Translate>}
- text={transaction.info.summary}
+ title={i18n.str`Subject`}
+ text={transaction.info.summary as TranslatedString}
kind="neutral"
/>
) : undefined}
<Part
- title={<i18n.Translate>Exchange</i18n.Translate>}
- text={transaction.exchangeBaseUrl}
- kind="neutral"
- />
- {/* {transaction.pending && ( //pending is not-received
- )} */}
- <Part
- title={<i18n.Translate>URI</i18n.Translate>}
- text={<ShowQrWithCopy text={transaction.talerUri} />}
+ title={i18n.str`Exchange`}
+ text={transaction.exchangeBaseUrl as TranslatedString}
kind="neutral"
/>
+ {transaction.talerUri && (
+ <Part
+ title={i18n.str`URI`}
+ text={<ShowQrWithCopy text={transaction.talerUri} />}
+ kind="neutral"
+ />
+ )}
<Part
- title={<i18n.Translate>Details</i18n.Translate>}
+ title={i18n.str`Details`}
text={
- <TransferDetails
- amount={{
- effective: Amounts.parseOrThrow(transaction.amountEffective),
- raw: Amounts.parseOrThrow(transaction.amountRaw),
- }}
+ <TransferCreationDetails
+ amount={getAmountWithFee(effective, raw, "debit")}
/>
}
/>
@@ -802,13 +993,20 @@ export function TransactionView({
}
if (transaction.type === TransactionType.PeerPushCredit) {
- const total = Amounts.parseOrThrow(transaction.amountEffective);
return (
- <TransactionTemplate>
+ <TransactionTemplate
+ transaction={transaction}
+ onDelete={onDelete}
+ onRetry={onRetry}
+ onAbort={onAbort}
+ onResume={onResume}
+ onSuspend={onSuspend}
+ onCancel={onCancel}
+ >
<Header
timestamp={transaction.timestamp}
type={i18n.str`Credit`}
- total={total}
+ total={effective}
kind="positive"
>
<i18n.Translate>Transfer</i18n.Translate>
@@ -816,31 +1014,135 @@ export function TransactionView({
{transaction.info.summary ? (
<Part
- title={<i18n.Translate>Subject</i18n.Translate>}
- text={transaction.info.summary}
+ title={i18n.str`Subject`}
+ text={transaction.info.summary as TranslatedString}
kind="neutral"
/>
) : undefined}
<Part
- title={<i18n.Translate>Exchange</i18n.Translate>}
- text={transaction.exchangeBaseUrl}
+ title={i18n.str`Exchange`}
+ text={transaction.exchangeBaseUrl as TranslatedString}
kind="neutral"
/>
<Part
- title={<i18n.Translate>Details</i18n.Translate>}
+ title={i18n.str`Details`}
text={
- <TransferDetails
- amount={{
- effective: Amounts.parseOrThrow(transaction.amountEffective),
- raw: Amounts.parseOrThrow(transaction.amountRaw),
- }}
+ <TransferPickupDetails
+ amount={getAmountWithFee(effective, raw, "credit")}
/>
}
/>
</TransactionTemplate>
);
}
- return <div />;
+
+ if (transaction.type === TransactionType.DenomLoss) {
+ switch (transaction.lossEventType) {
+ case DenomLossEventType.DenomExpired: {
+ return (
+ <TransactionTemplate
+ transaction={transaction}
+ onDelete={onDelete}
+ onRetry={onRetry}
+ onAbort={onAbort}
+ onResume={onResume}
+ onSuspend={onSuspend}
+ onCancel={onCancel}
+ >
+ <Header
+ timestamp={transaction.timestamp}
+ type={i18n.str`Debit`}
+ total={effective}
+ kind="negative"
+ >
+ <i18n.Translate>Lost</i18n.Translate>
+ </Header>
+
+ <Part
+ title={i18n.str`Exchange`}
+ text={transaction.exchangeBaseUrl as TranslatedString}
+ kind="neutral"
+ />
+ <Part
+ title={i18n.str`Reason`}
+ text={i18n.str`Denomination expired.`}
+ />
+ </TransactionTemplate>
+ );
+ }
+ case DenomLossEventType.DenomVanished: {
+ return (
+ <TransactionTemplate
+ transaction={transaction}
+ onDelete={onDelete}
+ onRetry={onRetry}
+ onAbort={onAbort}
+ onResume={onResume}
+ onSuspend={onSuspend}
+ onCancel={onCancel}
+ >
+ <Header
+ timestamp={transaction.timestamp}
+ type={i18n.str`Debit`}
+ total={effective}
+ kind="negative"
+ >
+ <i18n.Translate>Lost</i18n.Translate>
+ </Header>
+
+ <Part
+ title={i18n.str`Exchange`}
+ text={transaction.exchangeBaseUrl as TranslatedString}
+ kind="neutral"
+ />
+ <Part
+ title={i18n.str`Reason`}
+ text={i18n.str`Denomination vanished.`}
+ />
+ </TransactionTemplate>
+ );
+ }
+ case DenomLossEventType.DenomUnoffered: {
+ return (
+ <TransactionTemplate
+ transaction={transaction}
+ onDelete={onDelete}
+ onRetry={onRetry}
+ onAbort={onAbort}
+ onResume={onResume}
+ onSuspend={onSuspend}
+ onCancel={onCancel}
+ >
+ <Header
+ timestamp={transaction.timestamp}
+ type={i18n.str`Debit`}
+ total={effective}
+ kind="negative"
+ >
+ <i18n.Translate>Lost</i18n.Translate>
+ </Header>
+
+ <Part
+ title={i18n.str`Exchange`}
+ text={transaction.exchangeBaseUrl as TranslatedString}
+ kind="neutral"
+ />
+ <Part
+ title={i18n.str`Reason`}
+ text={i18n.str`Denomination is unoffered.`}
+ />
+ </TransactionTemplate>
+ );
+ }
+ default: {
+ assertUnreachable(transaction.lossEventType);
+ }
+ }
+ }
+ if (transaction.type === TransactionType.Recoup) {
+ throw Error("recoup transaction not implemented");
+ }
+ assertUnreachable(transaction);
}
export function MerchantDetails({
@@ -883,127 +1185,6 @@ export function MerchantDetails({
);
}
-function DeliveryDetails({
- date,
- location,
-}: {
- date: TalerProtocolTimestamp | undefined;
- location: Location | undefined;
-}): VNode {
- const { i18n } = useTranslationContext();
- return (
- <PurchaseDetailsTable>
- {location && (
- <Fragment>
- {location.country && (
- <tr>
- <td>
- <i18n.Translate>Country</i18n.Translate>
- </td>
- <td>{location.country}</td>
- </tr>
- )}
- {location.address_lines && (
- <tr>
- <td>
- <i18n.Translate>Address lines</i18n.Translate>
- </td>
- <td>{location.address_lines}</td>
- </tr>
- )}
- {location.building_number && (
- <tr>
- <td>
- <i18n.Translate>Building number</i18n.Translate>
- </td>
- <td>{location.building_number}</td>
- </tr>
- )}
- {location.building_name && (
- <tr>
- <td>
- <i18n.Translate>Building name</i18n.Translate>
- </td>
- <td>{location.building_name}</td>
- </tr>
- )}
- {location.street && (
- <tr>
- <td>
- <i18n.Translate>Street</i18n.Translate>
- </td>
- <td>{location.street}</td>
- </tr>
- )}
- {location.post_code && (
- <tr>
- <td>
- <i18n.Translate>Post code</i18n.Translate>
- </td>
- <td>{location.post_code}</td>
- </tr>
- )}
- {location.town_location && (
- <tr>
- <td>
- <i18n.Translate>Town location</i18n.Translate>
- </td>
- <td>{location.town_location}</td>
- </tr>
- )}
- {location.town && (
- <tr>
- <td>
- <i18n.Translate>Town</i18n.Translate>
- </td>
- <td>{location.town}</td>
- </tr>
- )}
- {location.district && (
- <tr>
- <td>
- <i18n.Translate>District</i18n.Translate>
- </td>
- <td>{location.district}</td>
- </tr>
- )}
- {location.country_subdivision && (
- <tr>
- <td>
- <i18n.Translate>Country subdivision</i18n.Translate>
- </td>
- <td>{location.country_subdivision}</td>
- </tr>
- )}
- </Fragment>
- )}
-
- {!location || !date ? undefined : (
- <tr>
- <td colSpan={2}>
- <hr />
- </td>
- </tr>
- )}
- {date && (
- <Fragment>
- <tr>
- <td>
- <i18n.Translate>Date</i18n.Translate>
- </td>
- <td>
- <Time
- timestamp={AbsoluteTime.fromTimestamp(date)}
- format="dd MMMM yyyy, HH:mm"
- />
- </td>
- </tr>
- </Fragment>
- )}
- </PurchaseDetailsTable>
- );
-}
-
export function ExchangeDetails({ exchange }: { exchange: string }): VNode {
return (
<div>
@@ -1017,19 +1198,40 @@ export function ExchangeDetails({ exchange }: { exchange: string }): VNode {
}
export interface AmountWithFee {
- effective: AmountJson;
- raw: AmountJson;
+ value: AmountJson;
+ fee: AmountJson;
+ total: AmountJson;
+ maxFrac: number;
}
-export function InvoiceDetails({ amount }: { amount: AmountWithFee }): VNode {
- const { i18n } = useTranslationContext();
-
- const fee = Amounts.sub(amount.raw, amount.effective).amount;
+export function getAmountWithFee(
+ effective: AmountJson,
+ raw: AmountJson,
+ direction: "credit" | "debit",
+): AmountWithFee {
+ const total = direction === "credit" ? effective : raw;
+ const value = direction === "debit" ? effective : raw;
+ const fee = Amounts.sub(value, total).amount;
- const maxFrac = [amount.raw, amount.effective, fee]
+ const maxFrac = [effective, raw, fee]
.map((a) => Amounts.maxFractionalDigits(a))
.reduce((c, p) => Math.max(c, p), 0);
+ return {
+ total,
+ value,
+ fee,
+ maxFrac,
+ };
+}
+
+export function InvoiceCreationDetails({
+ amount,
+}: {
+ amount: AmountWithFee;
+}): VNode {
+ const { i18n } = useTranslationContext();
+
return (
<PurchaseDetailsTable>
<tr>
@@ -1037,164 +1239,264 @@ export function InvoiceDetails({ amount }: { amount: AmountWithFee }): VNode {
<i18n.Translate>Invoice</i18n.Translate>
</td>
<td>
- <Amount value={amount.raw} maxFracSize={maxFrac} />
+ <Amount value={amount.value} maxFracSize={amount.maxFrac} />
</td>
</tr>
- {Amounts.isNonZero(fee) && (
- <tr>
- <td>
- <i18n.Translate>Transaction fees</i18n.Translate>
- </td>
- <td>
- <Amount value={fee} negative maxFracSize={maxFrac} />
- </td>
- </tr>
+ {Amounts.isNonZero(amount.fee) && (
+ <Fragment>
+ <tr>
+ <td>
+ <i18n.Translate>Fees</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Total</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.total} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ </Fragment>
)}
- <tr>
- <td colSpan={2}>
- <hr />
- </td>
- </tr>
- <tr>
- <td>
- <i18n.Translate>Total</i18n.Translate>
- </td>
- <td>
- <Amount value={amount.effective} maxFracSize={maxFrac} />
- </td>
- </tr>
</PurchaseDetailsTable>
);
}
-export function TransferDetails({ amount }: { amount: AmountWithFee }): VNode {
+export function InvoicePaymentDetails({
+ amount,
+}: {
+ amount: AmountWithFee;
+}): VNode {
const { i18n } = useTranslationContext();
- const fee = Amounts.sub(amount.raw, amount.effective).amount;
-
- const maxFrac = [amount.raw, amount.effective, fee]
- .map((a) => Amounts.maxFractionalDigits(a))
- .reduce((c, p) => Math.max(c, p), 0);
-
return (
<PurchaseDetailsTable>
<tr>
<td>
- <i18n.Translate>Transfer</i18n.Translate>
+ <i18n.Translate>Invoice</i18n.Translate>
</td>
<td>
- <Amount value={amount.raw} maxFracSize={maxFrac} />
+ <Amount value={amount.total} maxFracSize={amount.maxFrac} />
</td>
</tr>
- {Amounts.isNonZero(fee) && (
- <tr>
- <td>
- <i18n.Translate>Transaction fees</i18n.Translate>
- </td>
- <td>
- <Amount value={fee} negative maxFracSize={maxFrac} />
- </td>
- </tr>
+ {Amounts.isNonZero(amount.fee) && (
+ <Fragment>
+ <tr>
+ <td>
+ <i18n.Translate>Fees</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Total</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.value} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ </Fragment>
)}
- <tr>
- <td colSpan={2}>
- <hr />
- </td>
- </tr>
+ </PurchaseDetailsTable>
+ );
+}
+
+export function TransferCreationDetails({
+ amount,
+}: {
+ amount: AmountWithFee;
+}): VNode {
+ const { i18n } = useTranslationContext();
+
+ return (
+ <PurchaseDetailsTable>
<tr>
<td>
- <i18n.Translate>Total</i18n.Translate>
+ <i18n.Translate>Sent</i18n.Translate>
</td>
<td>
- <Amount value={amount.effective} maxFracSize={maxFrac} />
+ <Amount value={amount.value} maxFracSize={amount.maxFrac} />
</td>
</tr>
+
+ {Amounts.isNonZero(amount.fee) && (
+ <Fragment>
+ <tr>
+ <td>
+ <i18n.Translate>Fees</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Transfer</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.total} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ </Fragment>
+ )}
</PurchaseDetailsTable>
);
}
-export function WithdrawDetails({ amount }: { amount: AmountWithFee }): VNode {
+export function TransferPickupDetails({
+ amount,
+}: {
+ amount: AmountWithFee;
+}): VNode {
const { i18n } = useTranslationContext();
- const fee = Amounts.sub(amount.raw, amount.effective).amount;
-
- const maxFrac = [amount.raw, amount.effective, fee]
- .map((a) => Amounts.maxFractionalDigits(a))
- .reduce((c, p) => Math.max(c, p), 0);
-
return (
<PurchaseDetailsTable>
<tr>
<td>
- <i18n.Translate>Withdraw</i18n.Translate>
+ <i18n.Translate>Transfer</i18n.Translate>
</td>
<td>
- <Amount value={amount.raw} maxFracSize={maxFrac} />
+ <Amount value={amount.value} maxFracSize={amount.maxFrac} />
</td>
</tr>
- {Amounts.isNonZero(fee) && (
+ {Amounts.isNonZero(amount.fee) && (
+ <Fragment>
+ <tr>
+ <td>
+ <i18n.Translate>Fees</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Total</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.total} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ </Fragment>
+ )}
+ </PurchaseDetailsTable>
+ );
+}
+
+export function WithdrawDetails({
+ conversion,
+ amount,
+}: {
+ conversion?: AmountJson;
+ amount: AmountWithFee;
+}): VNode {
+ const { i18n } = useTranslationContext();
+
+ return (
+ <PurchaseDetailsTable>
+ {conversion ? (
+ <Fragment>
+ <tr>
+ <td>
+ <i18n.Translate>Transfer</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={conversion} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ {conversion.fraction === amount.value.fraction &&
+ conversion.value === amount.value.value ? undefined : (
+ <tr>
+ <td>
+ <i18n.Translate>Converted</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.value} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ )}
+ </Fragment>
+ ) : (
<tr>
<td>
- <i18n.Translate>Transaction fees</i18n.Translate>
+ <i18n.Translate>Transfer</i18n.Translate>
</td>
<td>
- <Amount value={fee} negative maxFracSize={maxFrac} />
+ <Amount value={amount.value} maxFracSize={amount.maxFrac} />
</td>
</tr>
)}
- <tr>
- <td colSpan={2}>
- <hr />
- </td>
- </tr>
- <tr>
- <td>
- <i18n.Translate>Total</i18n.Translate>
- </td>
- <td>
- <Amount value={amount.effective} maxFracSize={maxFrac} />
- </td>
- </tr>
+ {Amounts.isNonZero(amount.fee) && (
+ <Fragment>
+ <tr>
+ <td>
+ <i18n.Translate>Fees</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Total</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.total} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ </Fragment>
+ )}
</PurchaseDetailsTable>
);
}
export function PurchaseDetails({
price,
- refund,
- info,
- proposalId,
+ effectiveRefund,
+ info: _info,
}: {
price: AmountWithFee;
- refund?: AmountWithFee;
+ effectiveRefund?: AmountJson;
info: OrderShortInfo;
- proposalId: string;
}): VNode {
const { i18n } = useTranslationContext();
- const partialFee = Amounts.sub(price.effective, price.raw).amount;
-
- const refundFee = !refund
- ? Amounts.zeroOfCurrency(price.effective.currency)
- : Amounts.sub(refund.raw, refund.effective).amount;
-
- const fee = Amounts.sum([partialFee, refundFee]).amount;
-
- const hasProducts = info.products && info.products.length > 0;
-
- const hasShipping =
- info.delivery_date !== undefined || info.delivery_location !== undefined;
-
- const showLargePic = (): void => {
- return;
- };
-
- const total = !refund
- ? price.effective
- : Amounts.sub(price.effective, refund.effective).amount;
+ const total = Amounts.add(price.value, price.fee).amount;
return (
<PurchaseDetailsTable>
@@ -1203,49 +1505,82 @@ export function PurchaseDetails({
<i18n.Translate>Price</i18n.Translate>
</td>
<td>
- <Amount value={price.raw} />
+ <Amount value={price.total} />
</td>
</tr>
-
- {refund && Amounts.isNonZero(refund.raw) && (
- <tr>
- <td>
- <i18n.Translate>Refunded</i18n.Translate>
- </td>
- <td>
- <Amount value={refund.raw} negative />
- </td>
- </tr>
- )}
- {Amounts.isNonZero(fee) && (
- <tr>
- <td>
- <i18n.Translate>Transaction fees</i18n.Translate>
- </td>
- <td>
- <Amount value={fee} />
- </td>
- </tr>
+ {Amounts.isNonZero(price.fee) && (
+ <Fragment>
+ <tr>
+ <td>
+ <i18n.Translate>Fees</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={price.fee} />
+ </td>
+ </tr>
+ {effectiveRefund && Amounts.isNonZero(effectiveRefund) ? (
+ <Fragment>
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Subtotal</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={price.total} />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Refunded</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={effectiveRefund} negative />
+ </td>
+ </tr>
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Total</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={Amounts.sub(total, effectiveRefund).amount} />
+ </td>
+ </tr>
+ </Fragment>
+ ) : (
+ <Fragment>
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Total</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={price.value} />
+ </td>
+ </tr>
+ </Fragment>
+ )}
+ </Fragment>
)}
- <tr>
- <td colSpan={2}>
- <hr />
- </td>
- </tr>
- <tr>
- <td>
- <i18n.Translate>Total</i18n.Translate>
- </td>
- <td>
- <Amount value={total} />
- </td>
- </tr>
- {hasProducts && (
+
+ {/* {hasProducts && (
<tr>
<td colSpan={2}>
<PartCollapsible
big
- title={<i18n.Translate>Products</i18n.Translate>}
+ title={i18n.str`Products`}
text={
<ListOfProducts>
{info.products?.map((p, k) => (
@@ -1268,13 +1603,13 @@ export function PurchaseDetails({
/>
</td>
</tr>
- )}
- {hasShipping && (
+ )} */}
+ {/* {hasShipping && (
<tr>
<td colSpan={2}>
<PartCollapsible
big
- title={<i18n.Translate>Delivery</i18n.Translate>}
+ title={i18n.str`Delivery`}
text={
<DeliveryDetails
date={info.delivery_date}
@@ -1284,202 +1619,185 @@ export function PurchaseDetails({
/>
</td>
</tr>
- )}
- <tr>
- <td>
- <ShowFullContractTermPopup proposalId={proposalId} />
- </td>
- </tr>
+ )} */}
</PurchaseDetailsTable>
);
}
-function RefundDetails({
- transaction,
-}: {
- transaction: TransactionRefund;
-}): VNode {
+function RefundDetails({ amount }: { amount: AmountWithFee }): VNode {
const { i18n } = useTranslationContext();
- const r = Amounts.parseOrThrow(transaction.amountRaw);
- const e = Amounts.parseOrThrow(transaction.amountEffective);
- const fee = Amounts.sub(r, e).amount;
-
- const maxFrac = [r, e, fee]
- .map((a) => Amounts.maxFractionalDigits(a))
- .reduce((c, p) => Math.max(c, p), 0);
-
return (
<PurchaseDetailsTable>
<tr>
<td>
- <i18n.Translate>Amount</i18n.Translate>
+ <i18n.Translate>Refund</i18n.Translate>
</td>
<td>
- <Amount value={transaction.amountRaw} maxFracSize={maxFrac} />
+ <Amount value={amount.value} maxFracSize={amount.maxFrac} />
</td>
</tr>
- {Amounts.isNonZero(fee) && (
- <tr>
- <td>
- <i18n.Translate>Transaction fees</i18n.Translate>
- </td>
- <td>
- <Amount value={fee} negative maxFracSize={maxFrac} />
- </td>
- </tr>
+ {Amounts.isNonZero(amount.fee) && (
+ <Fragment>
+ <tr>
+ <td>
+ <i18n.Translate>Fees</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Total</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.total} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ </Fragment>
)}
- <tr>
- <td colSpan={2}>
- <hr />
- </td>
- </tr>
- <tr>
- <td>
- <i18n.Translate>Total</i18n.Translate>
- </td>
- <td>
- <Amount value={transaction.amountEffective} maxFracSize={maxFrac} />
- </td>
- </tr>
</PurchaseDetailsTable>
);
}
-function DepositDetails({
- transaction,
+type AmountAmountByWireTransferByWire = {
+ id: string;
+ amount: AmountString;
+}[];
+
+function calculateAmountByWireTransfer(
+ state: TransactionDeposit["trackingState"],
+): AmountAmountByWireTransferByWire {
+ const allTracking = Object.values(state ?? {});
+
+ //group tracking by wtid, sum amounts
+ const trackByWtid = allTracking.reduce(
+ (prev, cur) => {
+ const fee = Amounts.parseOrThrow(cur.wireFee);
+ const raw = Amounts.parseOrThrow(cur.amountRaw);
+ const total = !prev[cur.wireTransferId]
+ ? raw
+ : Amounts.add(prev[cur.wireTransferId].total, raw).amount;
+
+ prev[cur.wireTransferId] = {
+ total,
+ fee,
+ };
+ return prev;
+ },
+ {} as Record<string, { total: AmountJson; fee: AmountJson }>,
+ );
+
+ //remove wire fee from total amount
+ return Object.entries(trackByWtid).map(([id, info]) => ({
+ id,
+ amount: Amounts.stringify(Amounts.sub(info.total, info.fee).amount),
+ }));
+}
+
+function TrackingDepositDetails({
+ trackingState,
}: {
- transaction: TransactionDeposit;
+ trackingState: TransactionDeposit["trackingState"];
}): VNode {
const { i18n } = useTranslationContext();
- const r = Amounts.parseOrThrow(transaction.amountRaw);
- const e = Amounts.parseOrThrow(transaction.amountEffective);
- const fee = Amounts.sub(e, r).amount;
- const maxFrac = [r, e, fee]
- .map((a) => Amounts.maxFractionalDigits(a))
- .reduce((c, p) => Math.max(c, p), 0);
+ const wireTransfers = calculateAmountByWireTransfer(trackingState);
return (
<PurchaseDetailsTable>
<tr>
<td>
- <i18n.Translate>Amount</i18n.Translate>
+ <i18n.Translate>Transfer identification</i18n.Translate>
</td>
<td>
- <Amount value={transaction.amountRaw} maxFracSize={maxFrac} />
+ <i18n.Translate>Amount</i18n.Translate>
</td>
</tr>
- {Amounts.isNonZero(fee) && (
- <tr>
+ {wireTransfers.map((wire) => (
+ <tr key={wire.id}>
+ <td>{wire.id}</td>
<td>
- <i18n.Translate>Transaction fees</i18n.Translate>
- </td>
- <td>
- <Amount value={fee} maxFracSize={maxFrac} />
+ <Amount value={wire.amount} />
</td>
</tr>
- )}
- <tr>
- <td colSpan={2}>
- <hr />
- </td>
- </tr>
- <tr>
- <td>
- <i18n.Translate>Total transfer</i18n.Translate>
- </td>
- <td>
- <Amount value={transaction.amountEffective} maxFracSize={maxFrac} />
- </td>
- </tr>
+ ))}
</PurchaseDetailsTable>
);
}
-function RefreshDetails({
- transaction,
-}: {
- transaction: TransactionRefresh;
-}): VNode {
- const { i18n } = useTranslationContext();
-
- const r = Amounts.parseOrThrow(transaction.amountRaw);
- const e = Amounts.parseOrThrow(transaction.amountEffective);
- const fee = Amounts.sub(r, e).amount;
- const maxFrac = [r, e, fee]
- .map((a) => Amounts.maxFractionalDigits(a))
- .reduce((c, p) => Math.max(c, p), 0);
+function DepositDetails({ amount }: { amount: AmountWithFee }): VNode {
+ const { i18n } = useTranslationContext();
return (
<PurchaseDetailsTable>
<tr>
<td>
- <i18n.Translate>Amount</i18n.Translate>
- </td>
- <td>
- <Amount value={transaction.amountRaw} maxFracSize={maxFrac} />
- </td>
- </tr>
- <tr>
- <td>
- <i18n.Translate>Transaction fees</i18n.Translate>
- </td>
- <td>
- <Amount value={fee} negative maxFracSize={maxFrac} />
- </td>
- </tr>
- <tr>
- <td colSpan={2}>
- <hr />
- </td>
- </tr>
- <tr>
- <td>
- <i18n.Translate>Total</i18n.Translate>
+ <i18n.Translate>Sent</i18n.Translate>
</td>
<td>
- <Amount value={transaction.amountEffective} maxFracSize={maxFrac} />
+ <Amount value={amount.value} maxFracSize={amount.maxFrac} />
</td>
</tr>
+
+ {Amounts.isNonZero(amount.fee) && (
+ <Fragment>
+ <tr>
+ <td>
+ <i18n.Translate>Fees</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Total</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.total} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ </Fragment>
+ )}
</PurchaseDetailsTable>
);
}
-function TipDetails({ transaction }: { transaction: TransactionTip }): VNode {
+function RefreshDetails({ amount }: { amount: AmountWithFee }): VNode {
const { i18n } = useTranslationContext();
- const r = Amounts.parseOrThrow(transaction.amountRaw);
- const e = Amounts.parseOrThrow(transaction.amountEffective);
- const fee = Amounts.sub(r, e).amount;
-
- const maxFrac = [r, e, fee]
- .map((a) => Amounts.maxFractionalDigits(a))
- .reduce((c, p) => Math.max(c, p), 0);
-
return (
<PurchaseDetailsTable>
<tr>
<td>
- <i18n.Translate>Amount</i18n.Translate>
+ <i18n.Translate>Refresh</i18n.Translate>
</td>
<td>
- <Amount value={transaction.amountRaw} maxFracSize={maxFrac} />
+ <Amount value={amount.value} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Fees</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
</td>
</tr>
-
- {Amounts.isNonZero(fee) && (
- <tr>
- <td>
- <i18n.Translate>Transaction fees</i18n.Translate>
- </td>
- <td>
- <Amount value={fee} negative maxFracSize={maxFrac} />
- </td>
- </tr>
- )}
<tr>
<td colSpan={2}>
<hr />
@@ -1490,7 +1808,7 @@ function TipDetails({ transaction }: { transaction: TransactionTip }): VNode {
<i18n.Translate>Total</i18n.Translate>
</td>
<td>
- <Amount value={transaction.amountEffective} maxFracSize={maxFrac} />
+ <Amount value={amount.total} maxFracSize={amount.maxFrac} />
</td>
</tr>
</PurchaseDetailsTable>
@@ -1504,11 +1822,11 @@ function Header({
kind,
type,
}: {
- timestamp: TalerProtocolTimestamp;
+ timestamp: TalerPreciseTimestamp;
total: AmountJson;
children: ComponentChildren;
kind: Kind;
- type: string;
+ type: TranslatedString;
}): VNode {
return (
<div
@@ -1521,7 +1839,7 @@ function Header({
<div>
<SubTitle>{children}</SubTitle>
<Time
- timestamp={AbsoluteTime.fromTimestamp(timestamp)}
+ timestamp={AbsoluteTime.fromPreciseTimestamp(timestamp)}
format="dd MMMM yyyy, HH:mm"
/>
</div>
@@ -1548,10 +1866,10 @@ function NicePayto({ payto }: { payto: PaytoUri }): VNode {
const url = new URL("/", `https://${payto.host}`);
return (
<Fragment>
- <div>{payto.account}</div>
+ <div>{"payto.account"}</div>
<SmallLightText>
<a href={url.href} target="_bank" rel="noreferrer">
- {url.toString()}
+ {url.href}
</a>
</SmallLightText>
</Fragment>
@@ -1564,3 +1882,149 @@ function NicePayto({ payto }: { payto: PaytoUri }): VNode {
}
return <Fragment>{stringifyPaytoUri(payto)}</Fragment>;
}
+
+function ShowQrWithCopy({ text }: { text: string }): VNode {
+ const [showing, setShowing] = useState(false);
+ const { i18n } = useTranslationContext();
+ async function copy(): Promise<void> {
+ navigator.clipboard.writeText(text);
+ }
+ async function toggle(): Promise<void> {
+ setShowing((s) => !s);
+ }
+ if (showing) {
+ return (
+ <div>
+ <QR text={text} />
+ <Button onClick={copy as SafeHandler<void>}>
+ <i18n.Translate>copy</i18n.Translate>
+ </Button>
+ <Button onClick={toggle as SafeHandler<void>}>
+ <i18n.Translate>hide qr</i18n.Translate>
+ </Button>
+ </div>
+ );
+ }
+ return (
+ <div>
+ <div>{text.substring(0, 64)}...</div>
+ <Button onClick={copy as SafeHandler<void>}>
+ <i18n.Translate>copy</i18n.Translate>
+ </Button>
+ <Button onClick={toggle as SafeHandler<void>}>
+ <i18n.Translate>show qr</i18n.Translate>
+ </Button>
+ </div>
+ );
+}
+
+function getShowButtonStates(transaction: Transaction) {
+ let abort = false;
+ let fail = false;
+ let resume = false;
+ let remove = false;
+ let suspend = false;
+
+ transaction.txActions.forEach((a) => {
+ switch (a) {
+ case TransactionAction.Delete:
+ remove = true;
+ break;
+ case TransactionAction.Suspend:
+ suspend = true;
+ break;
+ case TransactionAction.Resume:
+ resume = true;
+ break;
+ case TransactionAction.Abort:
+ abort = true;
+ break;
+ case TransactionAction.Fail:
+ fail = true;
+ break;
+ case TransactionAction.Retry:
+ break;
+ default:
+ assertUnreachable(a);
+ break;
+ }
+ });
+ return { abort, fail, resume, remove, suspend };
+}
+
+function ShowWithdrawalDetailForBankIntegrated({
+ transaction,
+}: {
+ transaction: TransactionWithdrawal | TransactionInternalWithdrawal;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const [showDetails, setShowDetails] = useState(false);
+ if (
+ transaction.txState.major !== TransactionMajorState.Pending ||
+ transaction.withdrawalDetails.type === WithdrawalType.ManualTransfer
+ ) {
+ return <Fragment />;
+ }
+ const raw = Amounts.parseOrThrow(transaction.amountRaw);
+ return (
+ <Fragment>
+ <EnabledBySettings name="advancedMode">
+ <a
+ href="#"
+ onClick={(e) => {
+ e.preventDefault();
+ setShowDetails(!showDetails);
+ }}
+ >
+ Show details.
+ </a>
+ </EnabledBySettings>
+
+ {showDetails && (
+ <BankDetailsByPaytoType
+ amount={raw}
+ accounts={
+ transaction.withdrawalDetails.exchangeCreditAccountDetails ?? []
+ }
+ subject={transaction.withdrawalDetails.reservePub}
+ />
+ )}
+ {!transaction.withdrawalDetails.confirmed &&
+ transaction.withdrawalDetails.bankConfirmationUrl ? (
+ <InfoBox>
+ <div style={{ display: "block" }}>
+ <i18n.Translate>
+ Wire transfer need a confirmation. Go to the{" "}
+ <a
+ href={transaction.withdrawalDetails.bankConfirmationUrl}
+ target="_blank"
+ rel="noreferrer"
+ style={{ display: "inline" }}
+ >
+ <i18n.Translate>bank site</i18n.Translate>
+ </a>{" "}
+ and check wire transfer operation to exchange account is complete.
+ </i18n.Translate>
+ </div>
+ </InfoBox>
+ ) : undefined}
+ {transaction.withdrawalDetails.confirmed &&
+ !transaction.withdrawalDetails.reserveIsReady && (
+ <InfoBox>
+ <i18n.Translate>
+ Bank has confirmed the wire transfer. Waiting for the exchange to
+ send the coins.
+ </i18n.Translate>
+ </InfoBox>
+ )}
+ {transaction.withdrawalDetails.confirmed &&
+ transaction.withdrawalDetails.reserveIsReady && (
+ <InfoBox>
+ <i18n.Translate>
+ Exchange is ready to send the coins, withdrawal in progress.
+ </i18n.Translate>
+ </InfoBox>
+ )}
+ </Fragment>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/wallet/Welcome.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Welcome.stories.tsx
index 7e52d4270..dfce1c14b 100644
--- a/packages/taler-wallet-webextension/src/wallet/Welcome.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Welcome.stories.tsx
@@ -19,7 +19,7 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { createExample } from "../test-utils.js";
+import * as tests from "@gnu-taler/web-util/testing";
import { View as TestedComponent } from "./Welcome.js";
export default {
@@ -27,22 +27,14 @@ export default {
component: TestedComponent,
};
-export const Normal = createExample(TestedComponent, {
+export const Normal = tests.createExample(TestedComponent, {
permissionToggle: { value: true, button: {} },
- diagnostics: {
- errors: [],
- walletManifestVersion: "1.0",
- walletManifestDisplayVersion: "1.0",
- firefoxIdbProblem: false,
- dbOutdated: false,
- },
});
-export const TimedoutDiagnostics = createExample(TestedComponent, {
- timedOut: true,
+export const TimedoutDiagnostics = tests.createExample(TestedComponent, {
permissionToggle: { value: true, button: {} },
});
-export const RunningDiagnostics = createExample(TestedComponent, {
+export const RunningDiagnostics = tests.createExample(TestedComponent, {
permissionToggle: { value: true, button: {} },
});
diff --git a/packages/taler-wallet-webextension/src/wallet/Welcome.tsx b/packages/taler-wallet-webextension/src/wallet/Welcome.tsx
index 659a6c2cf..6a57fe18a 100644
--- a/packages/taler-wallet-webextension/src/wallet/Welcome.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Welcome.tsx
@@ -24,100 +24,74 @@ import { WalletDiagnostics } from "@gnu-taler/taler-util";
import { Fragment, h, VNode } from "preact";
import { Checkbox } from "../components/Checkbox.js";
import { SubTitle, Title } from "../components/styled/index.js";
-import { useTranslationContext } from "../context/translation.js";
-import { useDiagnostics } from "../hooks/useDiagnostics.js";
-import { useAutoOpenPermissions } from "../hooks/useAutoOpenPermissions.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { useSettings } from "../hooks/useSettings.js";
import { ToggleHandler } from "../mui/handlers.js";
-import { platform } from "../platform/api.js";
+import { platform } from "../platform/foreground.js";
+import { useAlertContext } from "../context/alert.js";
export function WelcomePage(): VNode {
- const permissionToggle = useAutoOpenPermissions();
- const [diagnostics, timedOut] = useDiagnostics();
+ const [settings, updateSettings] = useSettings();
+ const { safely } = useAlertContext();
return (
<View
- permissionToggle={permissionToggle}
- diagnostics={diagnostics}
- timedOut={timedOut}
+ permissionToggle={{
+ value: settings.injectTalerSupport,
+ button: {
+ onClick: safely("update support injection", async () =>
+ updateSettings("injectTalerSupport", !settings.injectTalerSupport),
+ ),
+ },
+ }}
/>
);
}
export interface ViewProps {
permissionToggle: ToggleHandler;
- diagnostics: WalletDiagnostics | undefined;
- timedOut: boolean;
}
export function View({
permissionToggle,
- diagnostics,
- timedOut,
}: ViewProps): VNode {
const { i18n } = useTranslationContext();
return (
<Fragment>
<Title>
- <i18n.Translate>Browser Extension Installed!</i18n.Translate>
+ <i18n.Translate>GNU Taler Wallet installed!</i18n.Translate>
</Title>
<div>
<p>
<i18n.Translate>
- You can open the GNU Taler Wallet using the combination{" "}
+ You can open the wallet using the combination{" "}
<pre style="font-weight: bold; display: inline;">&lt;ALT+W&gt;</pre>
.
</i18n.Translate>
</p>
- {!platform.isFirefox() && (
- <Fragment>
- <p>
- <i18n.Translate>
- Also pinning the GNU Taler Wallet to your Chrome browser allows
- you to quick access without keyboard:
- </i18n.Translate>
- </p>
- <ol style={{ paddingLeft: 40 }}>
- <li>
- <i18n.Translate>Click the puzzle icon</i18n.Translate>
- </li>
- <li>
- <i18n.Translate>Search for GNU Taler Wallet</i18n.Translate>
- </li>
- <li>
- <i18n.Translate>Click the pin icon</i18n.Translate>
- </li>
- </ol>
- </Fragment>
- )}
- <SubTitle>
- <i18n.Translate>Permissions</i18n.Translate>
- </SubTitle>
- <Checkbox
- label={
- <i18n.Translate>
- Automatically open wallet based on page content
- </i18n.Translate>
- }
- name="perm"
- description={
+ <Fragment>
+ <p>
<i18n.Translate>
- (Enabling this option below will make using the wallet faster, but
- requires more permissions from your browser.)
+ Also pinning the GNU Taler Wallet to your browser allows
+ you to quick access without keyboard:
</i18n.Translate>
- }
- enabled={permissionToggle.value!}
- onToggle={permissionToggle.button.onClick!}
- />
+ </p>
+ <ol style={{ paddingLeft: 40 }}>
+ <li>
+ <i18n.Translate>Click the puzzle icon</i18n.Translate>
+ </li>
+ <li>
+ <i18n.Translate>Search for GNU Taler Wallet</i18n.Translate>
+ </li>
+ <li>
+ <i18n.Translate>Click the pin icon</i18n.Translate>
+ </li>
+ </ol>
+ </Fragment>
<SubTitle>
<i18n.Translate>Next Steps</i18n.Translate>
</SubTitle>
<a href="https://demo.taler.net/" style={{ display: "block" }}>
<i18n.Translate>Try the demo</i18n.Translate> »
</a>
- <a href="https://demo.taler.net/" style={{ display: "block" }}>
- <i18n.Translate>
- Learn how to top up your wallet balance
- </i18n.Translate>{" "}
- »
- </a>
</div>
</Fragment>
);
diff --git a/packages/taler-wallet-webextension/src/wallet/index.stories.tsx b/packages/taler-wallet-webextension/src/wallet/index.stories.tsx
index 05e141dc6..89bb75b29 100644
--- a/packages/taler-wallet-webextension/src/wallet/index.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/index.stories.tsx
@@ -21,12 +21,9 @@
export * as a1 from "./Backup.stories.js";
export * as a4 from "./DepositPage/stories.js";
-export * as a5 from "./ExchangeAddConfirm.stories.js";
-export * as a6 from "./ExchangeAddSetUrl.stories.js";
export * as a7 from "./History.stories.js";
export * as a8 from "./AddBackupProvider/stories.js";
export * as a10 from "./ProviderDetail.stories.js";
-export * as a11 from "./ReserveCreated.stories.js";
export * as a12 from "./Settings.stories.js";
export * as a13 from "./Transaction.stories.js";
export * as a14 from "./Welcome.stories.js";
diff --git a/packages/taler-wallet-webextension/src/walletEntryPoint.dev.tsx b/packages/taler-wallet-webextension/src/walletEntryPoint.dev.tsx
index 4cc217e47..60a5970e4 100644
--- a/packages/taler-wallet-webextension/src/walletEntryPoint.dev.tsx
+++ b/packages/taler-wallet-webextension/src/walletEntryPoint.dev.tsx
@@ -23,11 +23,10 @@
import { setupI18n } from "@gnu-taler/taler-util";
import { h, render } from "preact";
import { strings } from "./i18n/strings.js";
-import { setupPlatform } from "./platform/api.js";
+import { setupPlatform } from "./platform/foreground.js";
import devAPI from "./platform/dev.js";
import { Application } from "./wallet/Application.js";
-console.log("Wallet setup for Dev API");
setupPlatform(devAPI);
function main(): void {
diff --git a/packages/taler-wallet-webextension/src/walletEntryPoint.tsx b/packages/taler-wallet-webextension/src/walletEntryPoint.tsx
index 6f1d6e06b..1bd42796b 100644
--- a/packages/taler-wallet-webextension/src/walletEntryPoint.tsx
+++ b/packages/taler-wallet-webextension/src/walletEntryPoint.tsx
@@ -23,7 +23,7 @@
import { setupI18n } from "@gnu-taler/taler-util";
import { h, render } from "preact";
import { strings } from "./i18n/strings.js";
-import { setupPlatform } from "./platform/api.js";
+import { setupPlatform } from "./platform/foreground.js";
import chromeAPI from "./platform/chrome.js";
import firefoxAPI from "./platform/firefox.js";
import { Application } from "./wallet/Application.js";
@@ -33,10 +33,8 @@ const isFirefox = typeof (window as any)["InstallTrigger"] !== "undefined";
//FIXME: create different entry point for any platform instead of
//switching in runtime
if (isFirefox) {
- console.log("Wallet setup for Firefox API");
setupPlatform(firefoxAPI);
} else {
- console.log("Wallet setup for Chrome API");
setupPlatform(chromeAPI);
}
diff --git a/packages/taler-wallet-webextension/src/wxApi.ts b/packages/taler-wallet-webextension/src/wxApi.ts
index 524eed990..195efecd4 100644
--- a/packages/taler-wallet-webextension/src/wxApi.ts
+++ b/packages/taler-wallet-webextension/src/wxApi.ts
@@ -22,120 +22,179 @@
* Imports.
*/
import {
+ AbsoluteTime,
CoreApiResponse,
+ DetailsMap,
Logger,
+ LogLevel,
NotificationType,
- WalletDiagnostics,
+ TalerError,
+ TalerErrorCode,
+ TalerErrorDetail,
+ WalletNotification
} from "@gnu-taler/taler-util";
import {
- TalerError,
WalletCoreApiClient,
WalletCoreOpKeys,
WalletCoreRequestType,
WalletCoreResponseType,
} from "@gnu-taler/taler-wallet-core";
-import { MessageFromBackend, platform } from "./platform/api.js";
-import { nullFunction } from "./test-utils.js";
+import {
+ ExtensionNotification,
+ MessageFromBackend,
+ MessageFromFrontendBackground,
+ MessageFromFrontendWallet,
+} from "./platform/api.js";
+import { platform } from "./platform/foreground.js";
/**
*
- * @author Florian Dold
* @author sebasjm
*/
+const logger = new Logger("wxApi");
+
+export const WALLET_CORE_SUPPORTED_VERSION = "4:0:0"
+
export interface ExtendedPermissionsResponse {
newValue: boolean;
}
-const logger = new Logger("wxApi");
-/**
- * Response with information about available version upgrades.
- */
-export interface UpgradeResponse {
- /**
- * Is a reset required because of a new DB version
- * that can't be automatically upgraded?
- */
- dbResetRequired: boolean;
-
- /**
- * Current database version.
- */
- currentDbVersion: string;
-
- /**
- * Old db version (if applicable).
- */
- oldDbVersion: string;
+export interface BackgroundOperations {
+ resetDb: {
+ request: void;
+ response: void;
+ };
+ runGarbageCollector: {
+ request: void;
+ response: void;
+ };
+ reinitWallet: {
+ request: void;
+ response: void;
+ };
+ getNotifications: {
+ request: void;
+ response: WalletEvent[];
+ };
+ clearNotifications: {
+ request: void;
+ response: void;
+ };
+ setLoggingLevel: {
+ request: {
+ tag?: string;
+ level: LogLevel;
+ };
+ response: void;
+ };
+}
+
+export type WalletEvent = { notification: WalletNotification, when: AbsoluteTime }
+
+export interface BackgroundApiClient {
+ call<Op extends keyof BackgroundOperations>(
+ operation: Op,
+ payload: BackgroundOperations[Op]["request"],
+ ): Promise<BackgroundOperations[Op]["response"]>;
+}
+
+export class BackgroundError<T = any> extends Error {
+ public readonly errorDetail: TalerErrorDetail & T;
+ public readonly cause: Error;
+
+ constructor(title: string, e: TalerErrorDetail & T, cause: Error) {
+ super(title);
+ this.errorDetail = e;
+ this.cause = cause;
+ }
+
+ hasErrorCode<C extends keyof DetailsMap>(
+ code: C,
+ ): this is BackgroundError<DetailsMap[C]> {
+ return this.errorDetail.code === code;
+ }
}
/**
- * @deprecated Use {@link WxWalletCoreApiClient} instead.
+ * BackgroundApiClient integration with browser platform
*/
-async function callBackend(operation: string, payload: any): Promise<any> {
- let response: CoreApiResponse;
- try {
- response = await platform.sendMessageToWalletBackground(operation, payload);
- } catch (e) {
- console.log("Error calling backend");
- throw new Error(`Error contacting backend: ${e}`);
- }
- logger.info("got response", response);
- if (response.type === "error") {
- throw TalerError.fromUncheckedDetail(response.error);
+class BackgroundApiClientImpl implements BackgroundApiClient {
+ async call<Op extends keyof BackgroundOperations>(
+ operation: Op,
+ payload: BackgroundOperations[Op]["request"],
+ ): Promise<BackgroundOperations[Op]["response"]> {
+ let response: CoreApiResponse;
+
+ const message: MessageFromFrontendBackground<Op> = {
+ channel: "background",
+ operation,
+ payload,
+ };
+
+ try {
+ 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 error;
+ }
+ if (response.type === "error") {
+ throw new BackgroundError(
+ `Background operation "${operation}" failed`,
+ response.error,
+ TalerError.fromUncheckedDetail(response.error),
+ );
+ }
+ logger.trace("response", response);
+ return response.result as any;
}
- return response.result;
}
-export class WxWalletCoreApiClient implements WalletCoreApiClient {
+/**
+ * WalletCoreApiClient integration with browser platform
+ */
+class WalletApiClientImpl implements WalletCoreApiClient {
async call<Op extends WalletCoreOpKeys>(
operation: Op,
payload: WalletCoreRequestType<Op>,
): Promise<WalletCoreResponseType<Op>> {
let response: CoreApiResponse;
try {
- response = await platform.sendMessageToWalletBackground(
+ const message: MessageFromFrontendWallet<Op> = {
+ channel: "wallet",
operation,
payload,
- );
- } catch (e) {
- console.log("Error calling backend");
- throw new Error(`Error contacting backend: ${e}`);
+ };
+ 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 error;
}
- logger.info("got response", response);
if (response.type === "error") {
- throw TalerError.fromUncheckedDetail(response.error);
+ throw new BackgroundError(
+ `Wallet operation "${operation}" failed`,
+ response.error,
+ TalerError.fromUncheckedDetail(response.error)
+ );
}
+ logger.trace("got response", response);
return response.result as any;
}
}
-export class BackgroundApiClient {
- public resetDb(): Promise<void> {
- return callBackend("reset-db", {});
- }
-
- public containsHeaderListener(): Promise<ExtendedPermissionsResponse> {
- return callBackend("containsHeaderListener", {});
- }
-
- public getDiagnostics(): Promise<WalletDiagnostics> {
- return callBackend("wxGetDiagnostics", {});
- }
-
- public toggleHeaderListener(
- value: boolean,
- ): Promise<ExtendedPermissionsResponse> {
- return callBackend("toggleHeaderListener", { value });
- }
-
- public runGarbageCollector(): Promise<void> {
- return callBackend("run-gc", {});
- }
-}
function onUpdateNotification(
messageTypes: Array<NotificationType>,
- doCallback: undefined | (() => void),
+ doCallback: undefined | ((n: WalletNotification) => void),
): () => void {
//if no callback, then ignore
if (!doCallback)
@@ -143,18 +202,35 @@ function onUpdateNotification(
return;
};
const onNewMessage = (message: MessageFromBackend): void => {
- const shouldNotify = messageTypes.includes(message.type);
+ const shouldNotify = message.type === "wallet" && messageTypes.includes(message.notification.type);
if (shouldNotify) {
- doCallback();
+ doCallback(message.notification);
}
};
return platform.listenToWalletBackground(onNewMessage);
}
+export type WxApiType = {
+ wallet: WalletCoreApiClient;
+ background: BackgroundApiClient;
+ listener: {
+ trigger: (d: ExtensionNotification) => void;
+ onUpdateNotification: typeof onUpdateNotification;
+ };
+};
+
+function trigger(w: ExtensionNotification) {
+ platform.triggerWalletEvent({
+ type: "web-extension",
+ notification: w,
+ })
+}
+
export const wxApi = {
- wallet: new WxWalletCoreApiClient(),
- background: new BackgroundApiClient(),
+ wallet: new WalletApiClientImpl(),
+ background: new BackgroundApiClientImpl(),
listener: {
+ trigger,
onUpdateNotification,
},
};
diff --git a/packages/taler-wallet-webextension/src/wxBackend.ts b/packages/taler-wallet-webextension/src/wxBackend.ts
index 28adfa037..bf70f68df 100644
--- a/packages/taler-wallet-webextension/src/wxBackend.ts
+++ b/packages/taler-wallet-webextension/src/wxBackend.ts
@@ -24,32 +24,42 @@
* Imports.
*/
import {
- classifyTalerUri,
- CoreApiResponse,
- CoreApiResponseSuccess,
+ AbsoluteTime,
+ BalanceFlag,
+ LogLevel,
Logger,
+ NotificationType,
+ OpenedPromise,
+ SetTimeoutTimerAPI,
+ TalerError,
TalerErrorCode,
- TalerUriType,
- WalletDiagnostics,
+ TalerErrorDetail,
+ TransactionMajorState,
+ TransactionMinorState,
+ WalletNotification,
+ getErrorDetailFromException,
+ makeErrorDetail,
+ openPromise,
+ setGlobalLogLevelFromString,
+ setLogLevelFromString,
} from "@gnu-taler/taler-util";
+import { HttpRequestLibrary } from "@gnu-taler/taler-util/http";
import {
DbAccess,
+ SynchronousCryptoWorkerFactoryPlain,
+ Wallet,
+ WalletApiOperation,
+ WalletOperations,
+ WalletStoresV1,
deleteTalerDatabase,
exportDb,
importDb,
- makeErrorDetail,
- OpenedPromise,
- openPromise,
- openTalerDatabase,
- Wallet,
- WalletStoresV1,
} from "@gnu-taler/taler-wallet-core";
-import { SetTimeoutTimerAPI } from "@gnu-taler/taler-wallet-core";
-import { BrowserCryptoWorkerFactory } from "./browserCryptoWorkerFactory.js";
-import { BrowserHttpLib } from "./browserHttpLib.js";
-import { MessageFromBackend, platform } from "./platform/api.js";
-import { SynchronousCryptoWorkerFactory } from "./serviceWorkerCryptoWorkerFactory.js";
-import { ServiceWorkerHttpLib } from "./serviceWorkerHttpLib.js";
+import { MessageFromFrontend, MessageResponse } from "./platform/api.js";
+import { platform } from "./platform/background.js";
+import { ExtensionOperations } from "./taler-wallet-interaction-loader.js";
+import { BackgroundOperations, WalletEvent } from "./wxApi.js";
+import { BrowserFetchHttpLib } from "@gnu-taler/web-util/browser";
/**
* Currently active wallet instance. Might be unloaded and
@@ -61,138 +71,206 @@ let currentWallet: Wallet | undefined;
let currentDatabase: DbAccess<typeof WalletStoresV1> | undefined;
-/**
- * Last version of an outdated DB, if applicable.
- */
-let outdatedDbVersion: number | undefined;
-
const walletInit: OpenedPromise<void> = openPromise<void>();
const logger = new Logger("wxBackend.ts");
-async function getDiagnostics(): Promise<WalletDiagnostics> {
- const manifestData = platform.getWalletWebExVersion();
- const errors: string[] = [];
- let firefoxIdbProblem = false;
- let dbOutdated = false;
- try {
- await walletInit.promise;
- } catch (e) {
- errors.push("Error during wallet initialization: " + e);
- if (
- currentDatabase === undefined &&
- outdatedDbVersion === undefined &&
- platform.isFirefox()
- ) {
- firefoxIdbProblem = true;
- }
- }
- if (!currentWallet) {
- errors.push("Could not create wallet backend.");
+type BackendHandlerType = {
+ [Op in keyof BackgroundOperations]: (
+ req: BackgroundOperations[Op]["request"],
+ ) => Promise<BackgroundOperations[Op]["response"]>;
+};
+
+type ExtensionHandlerType = {
+ [Op in keyof ExtensionOperations]: (
+ req: ExtensionOperations[Op]["request"],
+ ) => Promise<ExtensionOperations[Op]["response"]>;
+};
+
+async function resetDb(): Promise<void> {
+ await deleteTalerDatabase(indexedDB as any);
+ await reinitWallet();
+}
+
+//FIXME: maybe circular buffer
+const notifications: WalletEvent[] = [];
+async function getNotifications(): Promise<WalletEvent[]> {
+ return notifications;
+}
+
+async function clearNotifications(): Promise<void> {
+ notifications.splice(0, notifications.length);
+}
+
+async function runGarbageCollector(): Promise<void> {
+ const dbBeforeGc = currentDatabase;
+ if (!dbBeforeGc) {
+ throw Error("no current db before running gc");
}
- if (!currentDatabase) {
- errors.push("Could not open database");
+ const dump = await exportDb(indexedDB as any);
+
+ await deleteTalerDatabase(indexedDB as any);
+ logger.info("cleaned");
+ await reinitWallet();
+ logger.info("init");
+
+ const dbAfterGc = currentDatabase;
+ if (!dbAfterGc) {
+ throw Error("no current db before running gc");
}
- if (outdatedDbVersion !== undefined) {
- errors.push(`Outdated DB version: ${outdatedDbVersion}`);
- dbOutdated = true;
+ await importDb(dbAfterGc.idbHandle(), dump);
+ logger.info("imported");
+}
+
+const extensionHandlers: ExtensionHandlerType = {
+ isAutoOpenEnabled,
+ isDomainTrusted,
+};
+
+async function isAutoOpenEnabled(): Promise<boolean> {
+ const settings = await platform.getSettingsFromStorage();
+ return settings.autoOpen === true;
+}
+
+async function isDomainTrusted(): Promise<boolean> {
+ const settings = await platform.getSettingsFromStorage();
+ return settings.injectTalerSupport === true;
+}
+
+const backendHandlers: BackendHandlerType = {
+ resetDb,
+ runGarbageCollector,
+ getNotifications,
+ clearNotifications,
+ reinitWallet,
+ setLoggingLevel,
+};
+
+async function setLoggingLevel({
+ tag,
+ level,
+}: {
+ tag?: string;
+ level: LogLevel;
+}): Promise<void> {
+ logger.info(`setting ${tag} to ${level}`);
+ if (!tag) {
+ setGlobalLogLevelFromString(level);
+ } else {
+ setLogLevelFromString(tag, level);
}
- const diagnostics: WalletDiagnostics = {
- walletManifestDisplayVersion: manifestData.version_name || "(undefined)",
- walletManifestVersion: manifestData.version,
- errors,
- firefoxIdbProblem,
- dbOutdated,
- };
- return diagnostics;
}
+let nextMessageIndex = 0;
-async function dispatch(
- req: any,
- sender: any,
- sendResponse: any,
-): Promise<void> {
- let r: CoreApiResponse;
-
- const wrapResponse = (result: unknown): CoreApiResponseSuccess => {
- return {
- type: "response",
- id: req.id,
- operation: req.operation,
- result,
- };
- };
+async function dispatch<
+ Op extends WalletOperations | BackgroundOperations | ExtensionOperations,
+>(req: MessageFromFrontend<Op> & { id: string }): Promise<MessageResponse> {
+ nextMessageIndex = (nextMessageIndex + 1) % (Number.MAX_SAFE_INTEGER - 100);
- try {
- switch (req.operation) {
- case "wxGetDiagnostics": {
- r = wrapResponse(await getDiagnostics());
- break;
- }
- case "reset-db": {
- await deleteTalerDatabase(indexedDB as any);
- r = wrapResponse(await reinitWallet());
- break;
+ switch (req.channel) {
+ case "background": {
+ const handler = backendHandlers[req.operation] as (req: any) => any;
+ if (!handler) {
+ return {
+ type: "error",
+ id: req.id,
+ operation: String(req.operation),
+ error: getErrorDetailFromException(
+ Error(`unknown background operation`),
+ ),
+ };
}
- case "run-gc": {
- logger.info("gc");
- const dump = await exportDb(currentDatabase!.idbHandle());
- await deleteTalerDatabase(indexedDB as any);
- logger.info("cleaned");
- await reinitWallet();
- logger.info("init");
- await importDb(currentDatabase!.idbHandle(), dump);
- logger.info("imported");
- r = wrapResponse({ result: true });
- break;
- }
- case "containsHeaderListener": {
- const res = await platform.containsTalerHeaderListener();
- r = wrapResponse({ newValue: res });
- break;
+ try {
+ const result = await handler(req.payload);
+ return {
+ type: "response",
+ id: req.id,
+ operation: String(req.operation),
+ result,
+ };
+ } catch (er) {
+ return {
+ type: "error",
+ id: req.id,
+ error: getErrorDetailFromException(er),
+ operation: String(req.operation),
+ };
}
- //FIXME: implement type checked api like WalletCoreApi
- case "toggleHeaderListener": {
- const newVal = req.payload.value;
- logger.trace("new extended permissions value", newVal);
- if (newVal) {
- platform.registerTalerHeaderListener(parseTalerUriAndRedirect);
- r = wrapResponse({ newValue: true });
- } else {
- const rem = await platform
- .getPermissionsApi()
- .removeHostPermissions();
- logger.trace("permissions removed:", rem);
- r = wrapResponse({ newVal: false });
- }
- break;
+ }
+ case "extension": {
+ const handler = extensionHandlers[req.operation] as (req: any) => any;
+ if (!handler) {
+ return {
+ type: "error",
+ id: req.id,
+ operation: String(req.operation),
+ error: getErrorDetailFromException(
+ Error(`unknown extension operation`),
+ ),
+ };
}
- default: {
- const w = currentWallet;
- if (!w) {
- r = {
- type: "error",
- id: req.id,
- operation: req.operation,
- error: makeErrorDetail(
- TalerErrorCode.WALLET_CORE_NOT_AVAILABLE,
- {},
- "wallet core not available",
- ),
- };
- break;
- }
- r = await w.handleCoreApiRequest(req.operation, req.id, req.payload);
- console.log("response received from wallet", r);
- break;
+ try {
+ const result = await handler(req.payload);
+ return {
+ type: "response",
+ id: req.id,
+ operation: String(req.operation),
+ result,
+ };
+ } catch (er) {
+ return {
+ type: "error",
+ id: req.id,
+ error: getErrorDetailFromException(er),
+ operation: String(req.operation),
+ };
}
}
+ case "wallet": {
+ const w = currentWallet;
+ if (!w) {
+ const lastError: TalerErrorDetail =
+ walletInit.lastError instanceof TalerError
+ ? walletInit.lastError.errorDetail
+ : undefined;
- sendResponse(r);
- } catch (e) {
- logger.error(`Error sending operation: ${req.operation}`, e);
- // might fail if tab disconnected
+ return {
+ type: "error",
+ id: req.id,
+ operation: req.operation,
+ error: makeErrorDetail(
+ TalerErrorCode.WALLET_CORE_NOT_AVAILABLE,
+ { lastError },
+ `wallet core not available${
+ !lastError ? "" : `,last error: ${lastError.hint}`
+ }`,
+ ),
+ };
+ }
+ //multiple client can create the same id, send the wallet an unique key
+ const newId = `${req.id}_${nextMessageIndex}`;
+ const resp = await w.handleCoreApiRequest(
+ req.operation,
+ newId,
+ req.payload,
+ );
+ //return to the client the original id
+ resp.id = req.id;
+ return resp;
+ }
}
+
+ const anyReq = req as any;
+ return {
+ type: "error",
+ id: anyReq.id,
+ operation: String(anyReq.operation),
+ error: getErrorDetailFromException(
+ Error(
+ `unknown channel ${anyReq.channel}, should be "background", "extension" or "wallet"`,
+ ),
+ ),
+ };
}
async function reinitWallet(): Promise<void> {
@@ -202,119 +280,76 @@ async function reinitWallet(): Promise<void> {
}
currentDatabase = undefined;
// setBadgeText({ text: "" });
- try {
- currentDatabase = await openTalerDatabase(indexedDB as any, reinitWallet);
- } catch (e) {
- logger.error("could not open database", e);
- walletInit.reject(e);
- return;
- }
- let httpLib;
let cryptoWorker;
let timer;
+ const httpFactory = (): HttpRequestLibrary => {
+ return new BrowserFetchHttpLib({
+ // enableThrottling: false,
+ });
+ };
+
if (platform.useServiceWorkerAsBackgroundProcess()) {
- httpLib = new ServiceWorkerHttpLib();
- cryptoWorker = new SynchronousCryptoWorkerFactory();
+ cryptoWorker = new SynchronousCryptoWorkerFactoryPlain();
timer = new SetTimeoutTimerAPI();
} else {
- httpLib = new BrowserHttpLib();
// We could (should?) use the BrowserCryptoWorkerFactory here,
// but right now we don't, to have less platform differences.
// cryptoWorker = new BrowserCryptoWorkerFactory();
- cryptoWorker = new SynchronousCryptoWorkerFactory();
+ cryptoWorker = new SynchronousCryptoWorkerFactoryPlain();
timer = new SetTimeoutTimerAPI();
}
+ const settings = await platform.getSettingsFromStorage();
logger.info("Setting up wallet");
const wallet = await Wallet.create(
- currentDatabase,
- httpLib,
+ indexedDB as any,
+ httpFactory as any,
timer,
cryptoWorker,
);
try {
- await wallet.handleCoreApiRequest("initWallet", "native-init", {});
+ await wallet.handleCoreApiRequest("initWallet", "native-init", {
+ config: {
+ testing: {
+ emitObservabilityEvents: settings.showWalletActivity,
+ devModeActive: settings.advancedMode,
+ },
+ features: {
+ allowHttp: settings.walletAllowHttp,
+ },
+ },
+ });
} catch (e) {
logger.error("could not initialize wallet", e);
walletInit.reject(e);
return;
}
- wallet.addNotificationListener((x) => {
- const message: MessageFromBackend = { type: x.type };
- platform.sendMessageToAllChannels(message);
- });
+ wallet.addNotificationListener((message) => {
+ if (settings.showWalletActivity) {
+ notifications.push({
+ notification: message,
+ when: AbsoluteTime.now(),
+ });
+ }
+
+ processWalletNotification(message);
- platform.keepAlive(() => {
- return wallet.runTaskLoop().catch((e) => {
- logger.error("error during wallet task loop", e);
+ platform.sendMessageToAllChannels({
+ type: "wallet",
+ notification: message,
});
});
+
// Useful for debugging in the background page.
if (typeof window !== "undefined") {
(window as any).talerWallet = wallet;
}
currentWallet = wallet;
+ updateIconBasedOnBalance();
return walletInit.resolve();
}
-function parseTalerUriAndRedirect(tabId: number, talerUri: string): void {
- const uriType = classifyTalerUri(talerUri);
- switch (uriType) {
- case TalerUriType.TalerWithdraw:
- return platform.redirectTabToWalletPage(
- tabId,
- `/cta/withdraw?talerWithdrawUri=${talerUri}`,
- );
- case TalerUriType.TalerPay:
- return platform.redirectTabToWalletPage(
- tabId,
- `/cta/pay?talerPayUri=${talerUri}`,
- );
- case TalerUriType.TalerTip:
- return platform.redirectTabToWalletPage(
- tabId,
- `/cta/tip?talerTipUri=${talerUri}`,
- );
- case TalerUriType.TalerRefund:
- return platform.redirectTabToWalletPage(
- tabId,
- `/cta/refund?talerRefundUri=${talerUri}`,
- );
- case TalerUriType.TalerPayPull:
- return platform.redirectTabToWalletPage(
- tabId,
- `/cta/invoice/pay?talerPayPullUri=${talerUri}`,
- );
- case TalerUriType.TalerPayPush:
- return platform.redirectTabToWalletPage(
- tabId,
- `/cta/transfer/pickup?talerPayPushUri=${talerUri}`,
- );
- case TalerUriType.TalerRecovery:
- return platform.redirectTabToWalletPage(
- tabId,
- `/cta/transfer/recovery?talerBackupUri=${talerUri}`,
- );
- case TalerUriType.Unknown:
- logger.warn(
- `Response with HTTP 402 the Taler header but could not classify ${talerUri}`,
- );
- return;
- case TalerUriType.TalerDevExperiment:
- // FIXME: Implement!
- logger.warn("not implemented");
- return;
- default: {
- const error: never = uriType;
- logger.warn(
- `Response with HTTP 402 the Taler header "${error}", but header value is not a taler:// URI.`,
- );
- return;
- }
- }
-}
-
/**
* Main function to run for the WebExtension backend.
*
@@ -324,45 +359,71 @@ export async function wxMain(): Promise<void> {
logger.trace("starting");
const afterWalletIsInitialized = reinitWallet();
+ logger.trace("reload on new version");
platform.registerReloadOnNewVersion();
// Handlers for messages coming directly from the content
// script on the page
- platform.listenToAllChannels((message, sender, callback) => {
- afterWalletIsInitialized.then(() => {
- dispatch(message, sender, (response: CoreApiResponse) => {
- callback(response);
- });
- });
+ logger.trace("listen all channels");
+ platform.listenToAllChannels(async (message) => {
+ //wait until wallet is initialized
+ await afterWalletIsInitialized;
+ const result = await dispatch(message);
+ return result;
});
+ logger.trace("register all incoming connections");
platform.registerAllIncomingConnections();
+ logger.trace("redirect if first start");
try {
platform.registerOnInstalled(() => {
platform.openWalletPage("/welcome");
-
- //
- try {
- platform.registerTalerHeaderListener(parseTalerUriAndRedirect);
- } catch (e) {
- logger.error("could not register header listener", e);
- }
});
} catch (e) {
console.error(e);
}
+}
- // On platforms that support it, also listen to external
- // modification of permissions.
- platform.getPermissionsApi().addPermissionsListener((perm, lastError) => {
- if (lastError) {
- logger.error(
- `there was a problem trying to get permission ${perm}`,
- lastError,
- );
- return;
+async function updateIconBasedOnBalance() {
+ const balance = await currentWallet?.client.call(
+ WalletApiOperation.GetBalances,
+ {},
+ );
+ if (balance) {
+ let showAlert = false;
+ for (const b of balance.balances) {
+ if (b.flags.length > 0) {
+ console.log("b.flags", JSON.stringify(b.flags))
+ showAlert = true;
+ break;
+ }
}
- platform.registerTalerHeaderListener(parseTalerUriAndRedirect);
- });
+
+ if (showAlert) {
+ platform.setAlertedIcon();
+ } else {
+ platform.setNormalIcon();
+ }
+ }
+}
+
+/**
+ * All the actions triggered by notification that need to be
+ * run in the background.
+ *
+ * @param message
+ */
+async function processWalletNotification(message: WalletNotification) {
+ if (
+ message.type === NotificationType.TransactionStateTransition &&
+ (message.newTxState.minor === TransactionMinorState.KycRequired ||
+ message.oldTxState.minor === TransactionMinorState.KycRequired ||
+ message.newTxState.minor === TransactionMinorState.AmlRequired ||
+ message.oldTxState.minor === TransactionMinorState.AmlRequired ||
+ message.newTxState.minor === TransactionMinorState.BankConfirmTransfer ||
+ message.oldTxState.minor === TransactionMinorState.BankConfirmTransfer)
+ ) {
+ await updateIconBasedOnBalance();
+ }
}
diff --git a/packages/taler-wallet-webextension/static/img/icon.png b/packages/taler-wallet-webextension/static/img/icon.png
deleted file mode 100644
index b4733bebc..000000000
--- a/packages/taler-wallet-webextension/static/img/icon.png
+++ /dev/null
Binary files differ
diff --git a/packages/taler-wallet-webextension/static/wallet.html b/packages/taler-wallet-webextension/static/wallet.html
index 3246ac8f8..3025901d8 100644
--- a/packages/taler-wallet-webextension/static/wallet.html
+++ b/packages/taler-wallet-webextension/static/wallet.html
@@ -1,32 +1,41 @@
<!DOCTYPE html>
<html>
- <head>
- <meta charset="utf-8" />
- <link rel="stylesheet" type="text/css" href="/dist/walletEntryPoint.css" />
- <link rel="stylesheet" type="text/css" href="/static/font/import.css" />
- <link rel="icon" href="/static/img/icon.png" />
- <script src="/dist/walletEntryPoint.js"></script>
- <style>
- html {
- font-family: sans-serif; /* 1 */
- }
- h1 {
- font-size: 2em;
- }
- input {
- font: inherit;
- }
- body {
- margin: 0;
- font-size: 100%;
- padding: 0;
- background-color: #f8faf7;
- font-family: Arial, Helvetica, sans-serif;
- }
- </style>
- </head>
- <body>
- <div id="container" class="wallet-container"></div>
- </body>
-</html>
+<head>
+ <title>GNU Taler Wallet - WebExtension</title>
+ <meta charset="utf-8" />
+ <meta name="taler-support" content="uri,api" />
+ <link rel="stylesheet" type="text/css" href="/dist/walletEntryPoint.css" />
+ <link rel="stylesheet" type="text/css" href="/static/font/import.css" />
+ <link rel="icon"
+ href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/Qn3//3ryn/795Tf/WrpP/2LCW/8B6T//w4Nb///////Pn4P+/d0v/9u3n/+7d0v/EhV7//v///+HDr//fxLD/zph2/+TJt//8/Pv/woBX//Lm3f/y5dz/v3hN//bu6f/JjGn/4sW0///////Df1j/8OLZ//v6+P+/elH/+vj1//jy7f+/elL//////+zYzP/Eg13//////967p//MlHT/wn5X///////v4Nb/yY1s///////jw7H/06KG////////////z5t9/+fNvf//////x4pn//Pp4v/8+vn/w39X/8WEX///////5s/A/9CbfP//////27Oc/9y2n////////////9itlf/gu6f//////86Vdf/r2Mz//////8SCXP/Df1j//////+7d0v/KkG7//////+HBrf/VpYr////////////RnoH/5sq6///////Ii2n/8ubf//39/P/Cf1j/xohk/+bNvv//////wn5W//Tq4//58/D/wHxV//7+/f/59fH/v3xU//39/P/w4Nf/xIFb///////hw7H/yo9t/+/f1f/AeU3/+/n2/+nSxP/FhmD//////9qzm//Upon/4MSx/96+qf//////xINc/+3bz//48e3/v3hN//Pn3///////6M+//752S//gw6//06aK/8J+VP/kzLr/zZd1/8OCWv/q18r/17KZ/9Ooi//fv6r/v3dK/+vWyP///////v39///////27un/1aeK/9Opjv/m1cf/1KCC/9a0nP/n08T/0Jx8/82YdP/QnHz/16yR//jx7P///////v39///////+/f3///7+///////+//7//v7+///////+/v7//v/+/////////////////////////v7//v79///////////////////+/v/+/Pv//v39///+/v/+/Pv///7+//7+/f/+/Pv//v39//79/P/+/Pv///7+////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" />
+ <script src="/dist/walletEntryPoint.js"></script>
+ <style>
+ html {
+ font-family: sans-serif;
+ /* 1 */
+ }
+
+ h1 {
+ font-size: 2em;
+ }
+
+ input {
+ font: inherit;
+ }
+
+ body {
+ margin: 0;
+ font-size: 100%;
+ padding: 0;
+ background-color: #f8faf7;
+ font-family: Arial, Helvetica, sans-serif;
+ }
+ </style>
+</head>
+
+<body>
+ <div id="container" class="wallet-container"></div>
+</body>
+
+</html> \ No newline at end of file
diff --git a/packages/demobank-ui/dev.mjs b/packages/taler-wallet-webextension/test.mjs
index 35a9fa16c..2fd007c2a 100755
--- a/packages/demobank-ui/dev.mjs
+++ b/packages/taler-wallet-webextension/test.mjs
@@ -15,16 +15,18 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { serve } from "@gnu-taler/web-util/lib/index.node";
-import esbuild from "esbuild";
-import { buildConfig } from "./build.mjs";
+import { build, getFilesInDirectory } from "@gnu-taler/web-util/build";
-buildConfig.inject = ['./node_modules/@gnu-taler/web-util/lib/live-reload.mjs']
+const allTestFiles = getFilesInDirectory("src", /.test.tsx?$/);
-serve({
- folder: './dist',
- port: 8080,
- source: './src',
- development: true,
- onUpdate: async () => esbuild.build(buildConfig)
-})
+await build({
+ type: "test",
+ source: {
+ js: allTestFiles.files,
+ assets: [],
+ },
+ destination: "./dist/test",
+ css: "linaria",
+ // no need to produce css
+ // linariaPlugin: () => {},
+});
diff --git a/packages/taler-wallet-webextension/trim-extension.cjs b/packages/taler-wallet-webextension/trim-extension.cjs
new file mode 100644
index 000000000..256a9f48d
--- /dev/null
+++ b/packages/taler-wallet-webextension/trim-extension.cjs
@@ -0,0 +1,25 @@
+// Simple plugin to trim extensions from the filename of relative import statements.
+// Required to get linaria to work with `moduleResolution: "Node16"` imports.
+// @author Florian Dold
+//
+
+module.exports = function ({ types: t }) {
+ return {
+ name: "trim-extension",
+ visitor: {
+ ImportDeclaration: (x) => {
+ const src = x.node.source;
+ if (src.value.startsWith(".")) {
+ if (src.value.endsWith(".js")) {
+ const newVal = src.value.replace(/[.]js$/, "");
+ x.node.source = t.stringLiteral(newVal);
+ }
+ }
+ if (src.value.endsWith(".jsx")) {
+ const newVal = src.value.replace(/[.]jsx$/, "");
+ x.node.source = t.stringLiteral(newVal);
+ }
+ },
+ },
+ };
+};
diff --git a/packages/taler-wallet-webextension/tsconfig.json b/packages/taler-wallet-webextension/tsconfig.json
index 5fc45caae..2c34816e6 100644
--- a/packages/taler-wallet-webextension/tsconfig.json
+++ b/packages/taler-wallet-webextension/tsconfig.json
@@ -1,16 +1,13 @@
{
"compilerOptions": {
"composite": true,
- "lib": [
- "es2021",
- "DOM"
- ],
- "jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
- "jsxFactory": "h", /* Specify the JSX factory function to use when targeting react JSX emit, e.g. React.createElement or h. */
+ "lib": ["es2020", "DOM"],
+ "jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */,
+ "jsxFactory": "h" /* Specify the JSX factory function to use when targeting react JSX emit, e.g. React.createElement or h. */,
"jsxFragmentFactory": "Fragment", // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-0.html#custom-jsx-factories
- "moduleResolution": "Node",
- "module": "ESNext",
- "target": "ES6",
+ "moduleResolution": "Node16",
+ "module": "Node16",
+ "target": "ES2020",
"skipLibCheck": true,
"preserveSymlinks": true,
"noImplicitAny": true,
@@ -23,22 +20,15 @@
"esModuleInterop": true,
"importHelpers": true,
"rootDir": "./src",
- "typeRoots": [
- "./node_modules/@types"
- ]
+ "typeRoots": ["./node_modules/@types"]
},
"references": [
{
"path": "../taler-wallet-core/"
},
{
- "path": "../web-util/"
- },
- {
"path": "../taler-util/"
}
],
- "include": [
- "src/**/*"
- ]
-} \ No newline at end of file
+ "include": ["src/**/*"]
+}
diff --git a/packages/web-util/README b/packages/web-util/README
deleted file mode 100644
index e69de29bb..000000000
--- a/packages/web-util/README
+++ /dev/null
diff --git a/packages/web-util/README.md b/packages/web-util/README.md
new file mode 100644
index 000000000..d1465aa71
--- /dev/null
+++ b/packages/web-util/README.md
@@ -0,0 +1,3 @@
+# web-util
+
+Common utilities for other web applications in this repository.
diff --git a/packages/web-util/build.mjs b/packages/web-util/build.mjs
index ba277b666..02d077571 100755
--- a/packages/web-util/build.mjs
+++ b/packages/web-util/build.mjs
@@ -15,89 +15,195 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import esbuild from 'esbuild'
-import path from "path"
-import fs from "fs"
+import esbuild from "esbuild";
+import path from "path";
+import fs from "fs";
// eslint-disable-next-line no-undef
-const BASE = process.cwd()
+const BASE = process.cwd();
-let GIT_ROOT = BASE
-while (!fs.existsSync(path.join(GIT_ROOT, '.git')) && GIT_ROOT !== '/') {
- GIT_ROOT = path.join(GIT_ROOT, '../')
+let GIT_ROOT = BASE;
+while (!fs.existsSync(path.join(GIT_ROOT, ".git")) && GIT_ROOT !== "/") {
+ GIT_ROOT = path.join(GIT_ROOT, "../");
}
-if (GIT_ROOT === '/') {
+if (GIT_ROOT === "/") {
// eslint-disable-next-line no-undef
- console.log("not found")
+ console.log("not found");
// eslint-disable-next-line no-undef
process.exit(1);
}
-const GIT_HASH = GIT_ROOT === '/' ? undefined : git_hash()
+const GIT_HASH = GIT_ROOT === "/" ? undefined : git_hash();
-
-let _package = JSON.parse(fs.readFileSync(path.join(BASE, 'package.json')));
+let _package = JSON.parse(fs.readFileSync(path.join(BASE, "package.json")));
function git_hash() {
- const rev = fs.readFileSync(path.join(GIT_ROOT, '.git', 'HEAD')).toString().trim().split(/.*[: ]/).slice(-1)[0];
- if (rev.indexOf('/') === -1) {
+ const rev = fs
+ .readFileSync(path.join(GIT_ROOT, ".git", "HEAD"))
+ .toString()
+ .trim()
+ .split(/.*[: ]/)
+ .slice(-1)[0];
+ if (rev.indexOf("/") === -1) {
return rev;
} else {
- return fs.readFileSync(path.join(GIT_ROOT, '.git', rev)).toString().trim();
+ return fs.readFileSync(path.join(GIT_ROOT, ".git", rev)).toString().trim();
}
}
+/**
+ * Problem:
+ * No loader is configured for ".node" files: ../../node_modules/.pnpm/fsevents@2.3.3/node_modules/fsevents/fsevents.node
+ *
+ * Reference:
+ * https://github.com/evanw/esbuild/issues/1051#issuecomment-806325487
+ *
+ * kept as reference, need to be tested on MacOs
+ */
+const nativeNodeModulesPlugin = {
+ name: 'native-node-modules',
+ setup(build) {
+ // If a ".node" file is imported within a module in the "file" namespace, resolve
+ // it to an absolute path and put it into the "node-file" virtual namespace.
+ build.onResolve({ filter: /\.node$/, namespace: 'file' }, args => ({
+ path: require.resolve(args.path, { paths: [args.resolveDir] }),
+ namespace: 'node-file',
+ }))
+
+ // Files in the "node-file" virtual namespace call "require()" on the
+ // path from esbuild of the ".node" file in the output directory.
+ build.onLoad({ filter: /.*/, namespace: 'node-file' }, args => ({
+ contents: `
+ import path from ${JSON.stringify(args.path)}
+ try { module.exports = require(path) }
+ catch {}
+ `,
+ }))
+
+ // If a ".node" file is imported within a module in the "node-file" namespace, put
+ // it in the "file" namespace where esbuild's default loading behavior will handle
+ // it. It is already an absolute path since we resolved it to one above.
+ build.onResolve({ filter: /\.node$/, namespace: 'node-file' }, args => ({
+ path: args.path,
+ namespace: 'file',
+ }))
+
+ // Tell esbuild's default loading behavior to use the "file" loader for
+ // these ".node" files.
+ let opts = build.initialOptions
+ opts.loader = opts.loader || {}
+ opts.loader['.node'] = 'file'
+ },
+}
+
const buildConfigBase = {
outdir: "lib",
bundle: true,
minify: false,
- target: [
- 'es6'
- ],
+ target: ["es2020"],
loader: {
- '.key': 'text',
- '.crt': 'text',
- '.html': 'text',
+ ".key": "text",
+ ".crt": "text",
+ ".node": "file",
+ ".html": "text",
+ ".svg": "dataurl",
},
sourcemap: true,
define: {
- '__VERSION__': `"${_package.version}"`,
- '__GIT_HASH__': `"${GIT_HASH}"`,
+ __VERSION__: `"${_package.version}"`,
+ __GIT_HASH__: `"${GIT_HASH}"`,
},
-}
+ //plugins: [nativeNodeModulesPlugin],
+};
+
+/**
+ * Build time libraries, under node runtime
+ */
+const buildConfigBuild = {
+ ...buildConfigBase,
+ entryPoints: ["src/index.build.ts"],
+ outExtension: {
+ ".js": ".mjs",
+ },
+ format: "esm",
+ platform: "node",
+ external: ["esbuild"],
+ // https://github.com/evanw/esbuild/issues/1921
+ // How to fix "Dynamic require of "os" is not supported"
+ // esbuild cannot convert external "static" commonjs require statements to static esm imports
+ banner: {
+ js: `
+ import { fileURLToPath } from 'url';
+ import { createRequire as topLevelCreateRequire } from 'module';
+ const require = topLevelCreateRequire(import.meta.url);
+ const __filename = fileURLToPath(import.meta.url);
+ const __dirname = path.dirname(__filename);
+`,
+ },
+};
+/**
+ * Development libraries, under node runtime
+ */
+const buildConfigTesting = {
+ ...buildConfigBase,
+ entryPoints: ["src/index.testing.ts"],
+ outExtension: {
+ ".js": ".mjs",
+ },
+ format: "esm",
+ platform: "browser",
+ external: ["preact", "@gnu-taler/taler-util", "jed", "swr", "axios"],
+ jsxFactory: "h",
+ jsxFragment: "Fragment",
+};
+
+/**
+ * Testing libraries, under node runtime
+ */
const buildConfigNode = {
...buildConfigBase,
entryPoints: ["src/index.node.ts", "src/cli.ts"],
outExtension: {
- '.js': '.cjs'
+ ".js": ".cjs",
},
- format: 'cjs',
- platform: 'node',
+ format: "cjs",
+ platform: "node",
external: ["preact"],
+
};
+/**
+ * Support libraries, under browser runtime
+ */
const buildConfigBrowser = {
...buildConfigBase,
- entryPoints: ["src/index.browser.ts", "src/live-reload.ts", 'src/stories.tsx'],
+ entryPoints: [
+ "src/tests/mock.ts",
+ "src/tests/swr.ts",
+ "src/index.browser.ts",
+ "src/live-reload.ts",
+ "src/stories.tsx",
+ ],
outExtension: {
- '.js': '.mjs'
+ ".js": ".mjs",
},
- format: 'esm',
- platform: 'browser',
- external: ["preact", "@gnu-taler/taler-util", "jed"],
- jsxFactory: 'h',
- jsxFragment: 'Fragment',
+ format: "esm",
+ platform: "browser",
+ external: ["preact", "@gnu-taler/taler-util", "jed", "swr", "axios"],
+ jsxFactory: "h",
+ jsxFragment: "Fragment",
};
-[buildConfigNode, buildConfigBrowser].forEach((config) => {
- esbuild
- .build(config)
- .catch((e) => {
- // eslint-disable-next-line no-undef
- console.log(e)
- // eslint-disable-next-line no-undef
- process.exit(1)
- });
-
-})
-
+[
+ buildConfigNode,
+ buildConfigBrowser,
+ buildConfigBuild,
+ buildConfigTesting,
+].forEach((config) => {
+ esbuild.build(config).catch((e) => {
+ // eslint-disable-next-line no-undef
+ console.log(e);
+ // eslint-disable-next-line no-undef
+ process.exit(1);
+ });
+});
diff --git a/packages/web-util/package.json b/packages/web-util/package.json
index a4d1c116b..369b872b6 100644
--- a/packages/web-util/package.json
+++ b/packages/web-util/package.json
@@ -1,40 +1,70 @@
{
"name": "@gnu-taler/web-util",
- "version": "0.9.0",
+ "version": "0.10.7",
"description": "Generic helper functionality for GNU Taler Web Apps",
"type": "module",
"types": "./lib/index.node.d.ts",
"main": "./dist/taler-web-cli.cjs",
- "bin": {
- "taler-wallet-cli": "./bin/taler-web-cli.cjs"
- },
"author": "Sebastian Marchano",
"license": "AGPL-3.0-or-later",
"private": false,
"exports": {
- "./lib/index.browser": "./lib/index.browser.mjs",
- "./lib/index.node": "./lib/index.node.cjs"
+ "./browser": {
+ "types": "./lib/index.browser.js",
+ "default": "./lib/index.browser.mjs"
+ },
+ "./build": {
+ "types": "./lib/index.build.js",
+ "default": "./lib/index.build.mjs"
+ },
+ "./node": {
+ "types": "./lib/index.node.js",
+ "default": "./lib/index.node.cjs"
+ },
+ "./testing": {
+ "types": "./lib/index.testing.js",
+ "default": "./lib/index.testing.mjs"
+ }
},
"scripts": {
- "prepare": "tsc && ./build.mjs",
"compile": "tsc && ./build.mjs",
- "clean": "rimraf dist lib tsconfig.tsbuildinfo",
+ "build": "tsc && ./build.mjs",
+ "clean": "rm -rf dist lib tsconfig.tsbuildinfo",
"pretty": "prettier --write src"
},
"devDependencies": {
+ "@babel/preset-react": "^7.22.3",
+ "@babel/preset-typescript": "^7.21.5",
"@gnu-taler/taler-util": "workspace:*",
+ "@heroicons/react": "^2.0.17",
+ "@linaria/babel-preset": "5.0.4",
+ "@linaria/core": "5.0.2",
+ "@linaria/esbuild": "5.0.4",
+ "@linaria/react": "5.0.3",
"@types/express": "^4.17.14",
- "@types/node": "^18.11.9",
+ "@types/node": "^18.11.17",
"@types/web": "^0.0.82",
"@types/ws": "^8.5.3",
+ "autoprefixer": "^10.4.14",
"chokidar": "^3.5.3",
- "esbuild": "^0.14.21",
+ "date-fns": "2.29.3",
+ "esbuild": "^0.19.9",
"express": "^4.18.2",
+ "postcss": "^8.4.23",
+ "postcss-load-config": "^4.0.1",
"preact": "10.11.3",
- "prettier": "^2.5.1",
- "rimraf": "^3.0.2",
- "tslib": "^2.4.0",
- "typescript": "^4.8.4",
+ "preact-render-to-string": "^5.2.6",
+ "prettier": "^3.1.1",
+ "sass": "1.56.1",
+ "swr": "2.0.3",
+ "tslib": "^2.6.2",
+ "typescript": "^5.3.3",
"ws": "7.4.5"
+ },
+ "dependencies": {
+ "@babel/core": "7.18.9",
+ "@babel/helper-compilation-targets": "7.18.9",
+ "@types/chrome": "0.0.197",
+ "tailwindcss": "^3.3.2"
}
}
diff --git a/packages/web-util/src/assets/lang.svg b/packages/web-util/src/assets/lang.svg
new file mode 100644
index 000000000..dd72ce65e
--- /dev/null
+++ b/packages/web-util/src/assets/lang.svg
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 2411.2 2794" style="enable-background:new 0 0 2411.2 2794;" xml:space="preserve">
+<style type="text/css">
+ .st0{fill:#FFFFFF;}
+ .st1{fill-rule:evenodd;clip-rule:evenodd;}
+ .st2{fill-rule:evenodd;clip-rule:evenodd;fill:#FFFFFF;}
+</style>
+<g id="Layer_2">
+</g>
+<g id="Layer_x5F_1_x5F_1">
+ <g>
+ <polygon points="1204.6,359.2 271.8,30 271.8,2060.1 1204.6,1758.3 "/>
+ <polygon class="st0" points="1182.2,358.1 2150.6,29 2150.6,2059 1182.2,1757.3 "/>
+ <polygon class="st0" points="30,2415.4 1182.2,2031.4 1182.2,357.9 30,742 "/>
+ <polygon points="1707.2,2440.7 1870.5,2709.4 1956.6,2459.8 "/>
+ <g>
+ <path d="M421.7,934.8c-6.1-6,8,49.1,27.6,68.9c34.8,35.1,61.9,39.6,76.4,40.2c32,1.3,71.5-8,94.9-17.8
+ c22.7-9.7,62.4-30,77.5-59.6c3.2-6.3,11.9-17,6.4-43.2c-4.2-20.2-17-27.3-32.7-26.2c-15.7,1.1-63.2,13.7-86.1,20.8
+ c-23,7-70.3,21.4-90.9,25.8C474.3,948.2,429,941.7,421.7,934.8z"/>
+ <path d="M1003.1,1593.7c-9.1-3.3-196.9-81.1-223.6-93.9c-21.8-10.5-75.2-33.1-100.4-43.3c70.8-109.2,115.5-191.6,121.5-204.1
+ c11-23,86-169.6,87.7-178.7c1.7-9.1,3.8-42.9,2.2-51c-1.7-8.2-29.1,7.6-66.4,20.2c-37.4,12.6-108.4,58.8-135.8,64.6
+ c-27.5,5.7-115.5,39.1-160.5,54c-45,14.9-130.2,40.9-165.2,50.4c-35.1,9.5-65.7,10.2-85.3,16.2c0,0,2.6,27.5,7.8,35.7
+ c5.2,8.2,23.7,28.4,45.3,34.1c21.6,5.7,57.3,3.4,73.6-0.3c16.3-3.8,44.4-17.5,48.2-23.6c3.8-6.1-2-24.9,4.5-30.6
+ c6.5-5.6,92.2-25.7,124.6-35.4c32.4-10,156.3-52.6,173.1-50.5c-5.3,17.7-105,215.1-137.1,274c-32.1,58.9-218.6,318-258.3,363.6
+ c-30.1,34.7-103.2,123.5-128.5,143.6c6.4,1.8,51.6-2.1,59.9-7.2c51.3-31.6,136.9-138.1,164.4-170.5
+ c81.9-96,153.8-196.8,210.8-283.4h0.1c11.1,4.6,100.9,77.8,124.4,94c23.4,16.2,115.9,67.8,136,76.4c20,8.7,97.1,44.2,100.3,32.2
+ C1029.4,1668,1012.2,1597.1,1003.1,1593.7z"/>
+ </g>
+ <path class="st1" d="M569,2572c18,11,35,20,54,29c38,19,81,39,122,54c56,21,112,38,168,51c31,7,65,13,98,18c3,0,92,11,110,11h90
+ c35-3,68-5,103-10c28-4,59-9,89-16c22-5,45-10,67-17c21-6,45-14,68-22c15-5,31-12,47-18c13-6,29-13,44-19c18-8,39-19,59-29
+ c16-8,34-18,51-28c13-7,43-30,59-30c18,0,30,16,30,30c0,29-39,38-57,51c-19,13-42,23-62,34c-40,21-81,39-120,54
+ c-51,19-107,37-157,49c-19,4-38,9-57,12c-10,2-114,18-143,18h-132c-35-3-72-7-107-12c-31-5-64-11-95-18c-24-5-50-12-73-19
+ c-40-11-79-25-117-40c-69-26-141-60-209-105c-12-8-13-16-13-25c0-15,11-29,29-29C531,2546,563,2569,569,2572z"/>
+ <path class="st1" d="M1151,2009L61,2372V764l1090-363V2009z M1212,354v1680c-1,5-3,10-7,15c-2,3-6,7-9,8c-25,10-1151,388-1166,388
+ c-12,0-23-8-29-21c0-1-1-2-1-4V739c2-5,3-12,7-16c8-11,22-13,31-16c17-6,1126-378,1142-378C1190,329,1212,336,1212,354z"/>
+ <path class="st1" d="M2120,2017l-907-282V380l907-308V2017z M2181,32v2023c-1,23-17,33-32,33c-13,0-107-32-123-37
+ c-126-39-253-78-378-117c-28-9-57-18-84-27c-24-7-50-15-74-23c-107-33-216-66-323-102c-4-1-14-15-14-18V351c2-5,4-11,9-15
+ c8-9,351-123,486-168c36-13,487-168,501-168C2167,0,2181,13,2181,32z"/>
+ <polygon points="2411.2,2440.7 1199.5,2054.5 1204.6,373.2 2411.2,757.2 "/>
+ <g>
+ <path class="st2" d="M1800.3,1124.6L1681.4,1412l218.6,66.3L1800.3,1124.6z M1729,853.2l156.1,47.3l284.4,1025l-160.3-48.7
+ l-57.6-210.4L1620.2,1566l-71.3,171.4l-160.4-48.7L1729,853.2z"/>
+ </g>
+ </g>
+</g>
+</svg>
diff --git a/packages/web-util/src/assets/logo-2021.svg b/packages/web-util/src/assets/logo-2021.svg
new file mode 100644
index 000000000..8c5ff3e5b
--- /dev/null
+++ b/packages/web-util/src/assets/logo-2021.svg
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 201 90">
+ <g fill="#0042b3" fill-rule="evenodd" stroke-width=".3">
+ <path d="M86.7 1.1c15.6 0 29 9.4 36 23.2h-5.9A35.1 35.1 0 0086.7 6.5C67 6.5 51 23.6 51 44.7c0 10.4 3.8 19.7 10 26.6a31.4 31.4 0 01-4.2 3A45.2 45.2 0 0146 44.7c0-24 18.2-43.6 40.7-43.6zm35.8 64.3a40.4 40.4 0 01-39 22.8c3-1.5 6-3.5 8.6-5.7a35.6 35.6 0 0024.6-17.1z" />
+ <path d="M64.2 1.1l3.1.1c-3 1.6-5.9 3.5-8.5 5.8a37.5 37.5 0 00-30.2 37.7c0 14.3 7.3 26.7 18 33.3a29.6 29.6 0 01-8.5.2c-9-8-14.6-20-14.6-33.5 0-24 18.2-43.6 40.7-43.6zm5.4 81.4a35.6 35.6 0 0024.6-17.1h5.9a40.4 40.4 0 01-39 22.8c3-1.5 5.9-3.5 8.5-5.7zm24.8-58.2a37 37 0 00-12.6-12.8 29.6 29.6 0 018.5-.2c4 3.6 7.4 8 9.9 13z" />
+ <path d="M41.8 1.1c1 0 2 0 3.1.2-3 1.5-5.9 3.4-8.5 5.6A37.5 37.5 0 006.1 44.7c0 21.1 16 38.3 35.7 38.3 12.6 0 23.6-7 30-17.6h5.8a40.4 40.4 0 01-35.8 23C19.3 88.4 1 68.8 1 44.7c0-24 18.2-43.6 40.7-43.6zm30.1 23.2a38.1 38.1 0 00-4.5-6.1c1.3-1.2 2.7-2.2 4.3-3 2.3 2.7 4.4 5.8 6 9.1z" />
+ </g>
+ <path d="M76.1 34.4h9.2v-5H61.9v5H71v26h5.1zM92.6 52.9h13.7l3 7.4h5.3l-12.7-31.2h-4.7L84.5 60.3h5.2zm11.8-4.9h-9.9l5-12.4zM123.8 29.4h-4.6v31h20.6v-5h-16zM166.5 29.4H145v31h21.6v-5H150v-8.3h14.5v-4.9h-14.5v-8h16.4zM191.2 39.5c0 1.6-.5 2.8-1.6 3.8s-2.6 1.4-4.4 1.4h-7.4V34.3h7.4c1.9 0 3.4.4 4.4 1.3 1 .9 1.6 2.2 1.6 3.9zm6 20.8l-7.7-11.7c1-.3 1.9-.7 2.7-1.3a8.8 8.8 0 003.6-4.6c.4-1 .5-2.2.5-3.5 0-1.5-.2-2.9-.7-4.1a8.4 8.4 0 00-2.1-3.1c-1-.8-2-1.5-3.4-2-1.3-.4-2.8-.6-4.5-.6h-12.9v31h5V49.4h6.5l7 10.8z" />
+</svg> \ No newline at end of file
diff --git a/packages/web-util/src/assets/logo-white.svg b/packages/web-util/src/assets/logo-white.svg
new file mode 100644
index 000000000..cb1f023c5
--- /dev/null
+++ b/packages/web-util/src/assets/logo-white.svg
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ width="670"
+ height="300"
+ viewBox="0 0 201 90"
+ version="1.1"
+ id="svg8">
+ <g
+ id="logo">
+ <g
+ id="circles"
+ style="fill:#FFF;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.327943">
+ <path
+ d="m 86.662153,1.1211936 c 15.589697,0 29.129227,9.4011664 35.961027,23.2018054 h -5.81736 C 110.4866,13.623304 99.349002,6.5180852 86.662153,6.5180852 c -19.690571,0 -35.652876,17.1120008 -35.652876,38.2205688 0,10.331797 3.825597,19.704678 10.03957,26.582945 -1.342357,1.120912 -2.771532,2.127905 -4.275488,3.006754 C 50.071485,66.553412 45.974857,56.15992 45.974857,44.738654 c 0,-24.089211 18.216325,-43.6174604 40.687296,-43.6174604 z M 122.51416,65.375898 c -6.86645,13.680134 -20.34561,22.980218 -35.852007,22.980218 -1.052702,0 -2.096093,-0.04291 -3.128683,-0.127026 3.052192,-1.561167 5.913582,-3.480387 8.538307,-5.707305 10.320963,-1.684389 19.185983,-8.113638 24.601813,-17.145887 z"
+ id="path2350" />
+ <path
+ d="m 64.212372,1.1211936 c 1.052607,0 2.095998,0.042919 3.128684,0.1270583 C 64.288864,2.8094199 61.427378,4.728606 58.802653,6.9555572 41.679542,9.7498571 28.559494,25.601563 28.559494,44.738654 c 0,14.264563 7.29059,26.702023 18.093843,33.268925 -1.593656,0.26719 -3.226966,0.406948 -4.890748,0.406948 -1.239545,0 -2.46151,-0.07952 -3.663522,-0.229364 C 29.191129,70.184015 23.525076,58.171633 23.525076,44.738654 23.525076,20.649443 41.7414,1.1211936 64.212372,1.1211936 Z M 69.62209,82.521785 C 79.943207,80.837396 88.808164,74.407841 94.224059,65.375422 h 5.840511 c -6.866354,13.680305 -20.345548,22.980694 -35.852198,22.980694 -1.052703,0 -2.095999,-0.04291 -3.128684,-0.127026 3.052002,-1.561371 5.913836,-3.480218 8.538402,-5.707305 z M 94.355885,24.322999 c -3.13939,-5.314721 -7.467551,-9.74275 -12.584511,-12.853269 1.593656,-0.26719 3.226904,-0.406948 4.890779,-0.406948 1.239451,0 2.461512,0.07952 3.663524,0.229364 4.016018,3.607242 7.373195,8.030111 9.849053,13.030853 z"
+ id="path2352" />
+ <path
+ d="m 41.762589,1.1211936 c 1.064296,0 2.118804,0.044379 3.162607,0.1302161 -3.046523,1.558961 -5.903162,3.4745139 -8.52358,5.6968133 C 19.254624,9.7205882 6.1097128,25.583465 6.1097128,44.738654 c 0,21.108568 15.9624012,38.22057 35.6528762,38.22057 12.599746,0 23.672446,-7.007056 30.013748,-17.583802 h 5.838515 C 70.748498,79.055727 57.26924,88.356116 41.762589,88.356116 c -22.470907,0 -40.6871998,-19.52825 -40.6871998,-43.617462 0,-24.089211 18.2162928,-43.6174604 40.6871998,-43.6174604 z M 71.905375,24.322999 c -1.31192,-2.220567 -2.830984,-4.287049 -4.528877,-6.166508 1.342452,-1.120945 2.771374,-2.128381 4.275139,-3.00723 2.372984,2.753011 4.418875,5.834636 6.072489,9.173738 z"
+ id="path2354" />
+ </g>
+ <g
+ id="letters"
+ style="fill:#FFF">
+ <path
+ d="m 76.135411,34.409066 h 9.161042 V 29.36588 H 61.857537 v 5.043186 h 9.161137 v 25.92317 h 5.116737 z"
+ id="path2346" />
+ <path
+ d="m 92.647571,52.856334 h 13.659009 l 2.93009,7.476072 h 5.36461 L 101.89122,29.144903 H 97.187186 L 84.477089,60.332406 h 5.199533 z m 11.802109,-4.822276 h -9.944771 l 4.951718,-12.386462 z"
+ id="path2362" />
+ <path
+ d="m 123.80641,29.366084 h -4.58038 v 30.966322 h 20.54728 v -4.910253 c -5.32227,0 -10.64463,0 -15.9669,0 z"
+ id="path2356" />
+ <path
+ d="m 166.4722,29.366084 h -21.37564 v 30.966322 h 21.58203 v -4.910253 h -16.54771 v -8.27275 h 14.48439 V 42.23925 h -14.48439 v -7.962811 h 16.34132 z"
+ id="path2360" />
+ <path
+ d="m 191.19035,39.474593 c 0,1.59947 -0.53646,2.87535 -1.61628,3.818883 -1.07281,0.95124 -2.52409,1.422837 -4.34678,1.422837 h -7.44851 V 34.276439 h 7.4073 c 1.9051,0 3.38376,0.435027 4.42939,1.312178 1.05226,0.870258 1.57488,2.167734 1.57488,3.885976 z m 6.06602,20.857813 -7.79911,-11.723191 c 1.01771,-0.294794 1.94631,-0.714813 2.78553,-1.260566 0.83885,-0.545619 1.56122,-1.209263 2.16629,-1.990627 0.60541,-0.781738 1.07981,-1.681096 1.42369,-2.698345 0.34378,-1.017553 0.51561,-2.175238 0.51561,-3.472883 0,-1.50409 -0.24743,-2.867948 -0.74267,-4.092048 -0.49515,-1.223794 -1.20344,-2.256186 -2.12499,-3.096734 -0.92173,-0.840446 -2.04957,-1.489252 -3.38375,-1.946452 -1.33447,-0.457267 -2.82692,-0.685476 -4.4774,-0.685476 h -12.87512 v 30.966322 h 5.03433 V 49.538522 h 6.37569 l 7.11829,10.793884 z"
+ id="path2358" />
+ </g>
+ </g>
+</svg>
diff --git a/packages/web-util/src/cli.ts b/packages/web-util/src/cli.ts
index dca4fc664..05a22bc8a 100644
--- a/packages/web-util/src/cli.ts
+++ b/packages/web-util/src/cli.ts
@@ -1,4 +1,5 @@
-import { clk, setGlobalLogLevelFromString } from "@gnu-taler/taler-util";
+import { setGlobalLogLevelFromString } from "@gnu-taler/taler-util";
+import { clk } from "@gnu-taler/taler-util/clk";
import { serve } from "./serve.js";
export const walletCli = clk
@@ -35,7 +36,6 @@ walletCli
return serve({
folder: args.serve.folder || "./dist",
port: args.serve.port || 8000,
- development: args.serve.development,
});
});
diff --git a/packages/web-util/src/components/Attention.tsx b/packages/web-util/src/components/Attention.tsx
new file mode 100644
index 000000000..4172c0c9b
--- /dev/null
+++ b/packages/web-util/src/components/Attention.tsx
@@ -0,0 +1,80 @@
+import { Duration, TranslatedString, assertUnreachable } from "@gnu-taler/taler-util";
+import { ComponentChildren, Fragment, VNode, h } from "preact";
+
+interface Props {
+ type?: "info" | "success" | "warning" | "danger" | "low",
+ onClose?: () => void,
+ title: TranslatedString,
+ children?: ComponentChildren,
+ timeout?: Duration,
+}
+export function Attention({ type = "info", title, children, onClose, timeout = Duration.getForever() }: Props): VNode {
+
+ return <div class={`group attention-${type} mt-2 shadow-lg`}>
+ {timeout.d_ms === "forever" ? undefined : <style>{`
+ .progress {
+ animation: notificationTimeoutBar ${Math.round(timeout.d_ms / 1000)}s ease-in-out;
+ animation-fill-mode:both;
+ }
+
+ @keyframes notificationTimeoutBar {
+ 0% { width: 0; }
+ 100% { width: 100%; }
+ }
+ `}</style>
+ }
+
+ <div data-timed={timeout.d_ms !== "forever"} class="rounded-md data-[timed=true]:rounded-b-none group-[.attention-info]:bg-blue-50 group-[.attention-low]:bg-gray-100 group-[.attention-warning]:bg-yellow-50 group-[.attention-danger]:bg-red-50 group-[.attention-success]:bg-green-50 p-4 shadow">
+ <div class="flex">
+ <div >
+ {type === "low" ? undefined :
+ <svg xmlns="http://www.w3.org/2000/svg" stroke="none" viewBox="0 0 24 24" fill="currentColor" class="w-8 h-8 group-[.attention-info]:text-blue-400 group-[.attention-warning]:text-yellow-400 group-[.attention-danger]:text-red-400 group-[.attention-success]:text-green-400">
+ {(() => {
+ switch (type) {
+ case "info":
+ return <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z" />
+ case "warning":
+ return <path fill-rule="evenodd" d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z" />
+ case "danger":
+ return <path fill-rule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z" />
+ case "success":
+ return <path fill-rule="evenodd" d="M7.493 18.75c-.425 0-.82-.236-.975-.632A7.48 7.48 0 016 15.375c0-1.75.599-3.358 1.602-4.634.151-.192.373-.309.6-.397.473-.183.89-.514 1.212-.924a9.042 9.042 0 012.861-2.4c.723-.384 1.35-.956 1.653-1.715a4.498 4.498 0 00.322-1.672V3a.75.75 0 01.75-.75 2.25 2.25 0 012.25 2.25c0 1.152-.26 2.243-.723 3.218-.266.558.107 1.282.725 1.282h3.126c1.026 0 1.945.694 2.054 1.715.045.422.068.85.068 1.285a11.95 11.95 0 01-2.649 7.521c-.388.482-.987.729-1.605.729H14.23c-.483 0-.964-.078-1.423-.23l-3.114-1.04a4.501 4.501 0 00-1.423-.23h-.777zM2.331 10.977a11.969 11.969 0 00-.831 4.398 12 12 0 00.52 3.507c.26.85 1.084 1.368 1.973 1.368H4.9c.445 0 .72-.498.523-.898a8.963 8.963 0 01-.924-3.977c0-1.708.476-3.305 1.302-4.666.245-.403-.028-.959-.5-.959H4.25c-.832 0-1.612.453-1.918 1.227z" />
+ default:
+ assertUnreachable(type)
+ }
+ })()}
+ </svg>
+ }
+ </div>
+ <div class="ml-3 w-full">
+ <h3 class="text-sm font-bold group-[.attention-info]:text-blue-800 group-[.attention-success]:text-green-800 group-[.attention-warning]:text-yellow-800 group-[.attention-danger]:text-red-800">
+ {title}
+ </h3>
+ <div class="mt-2 text-sm group-[.attention-info]:text-blue-700 group-[.attention-warning]:text-yellow-700 group-[.attention-danger]:text-red-700 group-[.attention-success]:text-green-700">
+ {children}
+ </div>
+ </div>
+ {onClose &&
+ <div>
+ <button type="button" class="font-semibold items-center rounded bg-transparent px-2 py-1 text-xs text-gray-900 hover:bg-gray-50"
+ onClick={(e) => {
+ e.preventDefault();
+ onClose();
+ }}
+ >
+ <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
+ <path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" />
+ </svg>
+ </button>
+ </div>
+ }
+ </div>
+ </div>
+ {timeout.d_ms === "forever" ? undefined :
+ <div class="meter group-[.attention-info]:bg-blue-50 group-[.attention-low]:bg-gray-100 group-[.attention-warning]:bg-yellow-50 group-[.attention-danger]:bg-red-50 group-[.attention-success]:bg-green-50 h-1 relative overflow-hidden -mt-1">
+ <span class="w-full h-full block"><span class="h-full block progress group-[.attention-info]:bg-blue-600 group-[.attention-low]:bg-gray-600 group-[.attention-warning]:bg-yellow-600 group-[.attention-danger]:bg-red-600 group-[.attention-success]:bg-green-600"></span></span>
+ </div>
+ }
+
+ </div>
+}
diff --git a/packages/web-util/src/components/Button.tsx b/packages/web-util/src/components/Button.tsx
new file mode 100644
index 000000000..18cecbdab
--- /dev/null
+++ b/packages/web-util/src/components/Button.tsx
@@ -0,0 +1,167 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ AbsoluteTime,
+ OperationAlternative,
+ OperationFail,
+ OperationOk,
+ OperationResult,
+ TalerError,
+ TranslatedString,
+} from "@gnu-taler/taler-util";
+// import { NotificationMessage, notifyInfo } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { HTMLAttributes, useState } from "preact/compat";
+import {
+ NotificationMessage,
+ buildUnifiedRequestErrorMessage,
+ notifyInfo,
+ useTranslationContext,
+} from "../index.browser.js";
+// import { useBankCoreApiContext } from "../context/config.js";
+
+// function errorMap<T extends OperationFail<unknown>>(resp: T, map: (d: T["case"]) => TranslatedString): void {
+
+export type OnOperationSuccesReturnType<T> = (
+ result: T extends OperationOk<any> ? T : never,
+) => TranslatedString | void;
+export type OnOperationFailReturnType<T> = (
+ (d: (T extends OperationFail<any> ? T : never) | (T extends OperationAlternative<any,any> ? T : never)) => TranslatedString)
+
+export interface ButtonHandler<T extends OperationResult<A, B>, A, B> {
+ onClick: () => Promise<T | undefined>;
+ onNotification: (n: NotificationMessage) => void;
+ onOperationSuccess: OnOperationSuccesReturnType<T>;
+ onOperationFail: OnOperationFailReturnType<T>;
+ onOperationComplete?: () => void;
+}
+
+interface Props<T extends OperationResult<A, B>, A, B>
+ extends HTMLAttributes<HTMLButtonElement> {
+ handler: ButtonHandler<T, A, B> | undefined;
+}
+
+/**
+ * This button accept an async function and report a notification
+ * on error or success.
+ *
+ * When the async function is running the inner text will change into
+ * a "loading" animation.
+ *
+ * @param param0
+ * @returns
+ */
+export function Button<T extends OperationResult<A, B>, A, B>({
+ handler,
+ children,
+ disabled,
+ onClick: clickEvent,
+ ...rest
+}: Props<T, A, B>): VNode {
+ const { i18n } = useTranslationContext();
+ const [running, setRunning] = useState(false);
+ return (
+ <button
+ {...rest}
+ disabled={disabled || running}
+ onClick={(e) => {
+ e.preventDefault();
+ if (!handler) {
+ return;
+ }
+ setRunning(true);
+ handler
+ .onClick()
+ .then((resp) => {
+ if (resp) {
+ if (resp.type === "ok") {
+ const result: OperationOk<any> = resp;
+ // @ts-expect-error this is an operationOk
+ const msg = handler.onOperationSuccess(result);
+ if (msg) {
+ notifyInfo(msg);
+ }
+ }
+ if (resp.type === "fail") {
+ const d = 'detail' in resp ? resp.detail : undefined
+
+ const title = handler.onOperationFail(resp as any);
+ handler.onNotification({
+ title,
+ type: "error",
+ description: d && d.hint ? d.hint as TranslatedString : undefined,
+ debug: d,
+ when: AbsoluteTime.now(),
+ });
+ }
+ }
+ if (handler.onOperationComplete) {
+ handler.onOperationComplete();
+ }
+ setRunning(false);
+ })
+ .catch((error) => {
+ console.error(error);
+
+ if (error instanceof TalerError) {
+ handler.onNotification(
+ buildUnifiedRequestErrorMessage(i18n, error),
+ );
+ } else {
+ const description = (
+ error instanceof Error ? error.message : String(error)
+ ) as TranslatedString;
+
+ handler.onNotification({
+ title: i18n.str`Operation failed`,
+ type: "error",
+ description,
+ when: AbsoluteTime.now(),
+ });
+ }
+
+ if (handler.onOperationComplete) {
+ handler.onOperationComplete();
+ }
+ setRunning(false);
+ });
+ }}
+ >
+ {running ? <Wait /> : children}
+ </button>
+ );
+}
+
+function Wait(): VNode {
+ return (
+ <Fragment>
+ <style>
+ {`
+ #l1 { width: 120px;
+ height: 20px;
+ -webkit-mask: radial-gradient(circle closest-side, currentColor 90%, #0000) left/20% 100%;
+ background: linear-gradient(currentColor 0 0) left/0% 100% no-repeat #ddd;
+ animation: l17 2s infinite steps(6);
+ }
+ @keyframes l17 {
+ 100% {background-size:120% 100%}
+`}
+ </style>
+ <div id="l1" />
+ </Fragment>
+ );
+}
diff --git a/packages/web-util/src/components/CopyButton.tsx b/packages/web-util/src/components/CopyButton.tsx
new file mode 100644
index 000000000..dbb38b474
--- /dev/null
+++ b/packages/web-util/src/components/CopyButton.tsx
@@ -0,0 +1,56 @@
+import { ComponentChildren, h, VNode } from "preact";
+import { useEffect, useState } from "preact/hooks";
+
+export function CopyIcon(): VNode {
+ return (
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75" />
+ </svg>
+ )
+};
+
+export function CopiedIcon(): VNode {
+ return (
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
+ </svg>
+ )
+};
+
+export function CopyButton({ class: clazz, children, getContent }: { children?: ComponentChildren, class: string, getContent: () => string }): VNode {
+ const [copied, setCopied] = useState(false);
+ function copyText(): void {
+ if (!navigator.clipboard && !window.isSecureContext) {
+ alert('clipboard is not available on insecure context (http)')
+ }
+ if (navigator.clipboard) {
+ navigator.clipboard.writeText(getContent() || "");
+ setCopied(true);
+ }
+ }
+ useEffect(() => {
+ if (copied) {
+ setTimeout(() => {
+ setCopied(false);
+ }, 1000);
+ }
+ }, [copied]);
+
+ if (!copied) {
+ return (
+ <button class={clazz} onClick={e => {
+ e.preventDefault()
+ copyText()
+ }} >
+ <CopyIcon />
+ {children}
+ </button>
+ );
+ }
+ return (
+ <button class={clazz} disabled>
+ <CopiedIcon />
+ {children}
+ </button>
+ );
+}
diff --git a/packages/web-util/src/components/ErrorLoading.tsx b/packages/web-util/src/components/ErrorLoading.tsx
new file mode 100644
index 000000000..7089266b9
--- /dev/null
+++ b/packages/web-util/src/components/ErrorLoading.tsx
@@ -0,0 +1,147 @@
+/*
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { TalerError, TalerErrorCode, assertUnreachable } from "@gnu-taler/taler-util";
+import { Fragment, VNode, h } from "preact";
+import { Attention } from "./Attention.js";
+import { useTranslationContext } from "../index.browser.js";
+
+export function ErrorLoading({ error, showDetail }: { error: TalerError, showDetail?: boolean }): VNode {
+ const { i18n } = useTranslationContext()
+ switch (error.errorDetail.code) {
+ //////////////////
+ // Every error that can be produce in a Http Request
+ //////////////////
+ case TalerErrorCode.GENERIC_TIMEOUT: {
+ if (error.hasErrorCode(TalerErrorCode.GENERIC_TIMEOUT)) {
+ const { requestMethod, requestUrl, timeoutMs } = error.errorDetail
+ return <Attention type="danger" title={i18n.str`The request reached a timeout, check your connection.`}>
+ {error.message}
+ {showDetail &&
+ <pre class="whitespace-break-spaces ">
+ {JSON.stringify({ requestMethod, requestUrl, timeoutMs }, undefined, 2)}
+ </pre>
+ }
+ </Attention>
+ }
+ assertUnreachable(1 as never)
+ }
+ case TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR: {
+ if (error.hasErrorCode(TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR)) {
+ const { requestMethod, requestUrl, timeoutMs } = error.errorDetail
+ return <Attention type="danger" title={i18n.str`The request was cancelled.`}>
+ {error.message}
+ {showDetail &&
+ <pre class="whitespace-break-spaces ">
+ {JSON.stringify({ requestMethod, requestUrl, timeoutMs }, undefined, 2)}
+ </pre>
+ }
+ </Attention>
+ }
+ assertUnreachable(1 as never)
+ }
+ case TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT: {
+ if (error.hasErrorCode(TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT)) {
+ const { requestMethod, requestUrl, timeoutMs } = error.errorDetail
+ return <Attention type="danger" title={i18n.str`The request reached a timeout, check your connection.`}>
+ {error.message}
+ {showDetail &&
+ <pre class="whitespace-break-spaces ">
+ {JSON.stringify({ requestMethod, requestUrl, timeoutMs }, undefined, 2)}
+ </pre>
+ }
+ </Attention>
+ }
+ assertUnreachable(1 as never)
+ }
+ case TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED: {
+ if (error.hasErrorCode(TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED)) {
+ const { requestMethod, requestUrl, throttleStats } = error.errorDetail
+ return <Attention type="danger" title={i18n.str`A lot of request were made to the same server and this action was throttled`}>
+ {error.message}
+ {showDetail &&
+ <pre class="whitespace-break-spaces ">
+ {JSON.stringify({ requestMethod, requestUrl, throttleStats }, undefined, 2)}
+ </pre>
+ }
+ </Attention>
+ }
+ assertUnreachable(1 as never)
+ }
+ case TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE: {
+ if (error.hasErrorCode(TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE)) {
+ const { requestMethod, requestUrl, httpStatusCode, validationError } = error.errorDetail
+ return <Attention type="danger" title={i18n.str`The response of the request is malformed.`}>
+ {error.message}
+ {showDetail &&
+ <pre class="whitespace-break-spaces ">
+ {JSON.stringify({ requestMethod, requestUrl, httpStatusCode, validationError }, undefined, 2)}
+ </pre>
+ }
+ </Attention>
+ }
+ assertUnreachable(1 as never)
+ }
+ case TalerErrorCode.WALLET_NETWORK_ERROR: {
+ if (error.hasErrorCode(TalerErrorCode.WALLET_NETWORK_ERROR)) {
+ const { requestMethod, requestUrl } = error.errorDetail
+ return <Attention type="danger" title={i18n.str`Could not complete the request due to a network problem.`}>
+ {error.message}
+ {showDetail &&
+ <pre class="whitespace-break-spaces ">
+ {JSON.stringify({ requestMethod, requestUrl }, undefined, 2)}
+ </pre>
+ }
+ </Attention>
+ }
+ assertUnreachable(1 as never)
+ }
+ case TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR: {
+ if (error.hasErrorCode(TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR)) {
+ const { requestMethod, requestUrl, httpStatusCode, errorResponse } = error.errorDetail
+ return <Attention type="danger" title={i18n.str`Unexpected request error`}>
+ {error.message}
+ {showDetail &&
+ <pre class="whitespace-break-spaces ">
+ {JSON.stringify({ requestMethod, requestUrl, httpStatusCode, errorResponse }, undefined, 2)}
+ </pre>
+ }
+ </Attention>
+ }
+ assertUnreachable(1 as never)
+ }
+ //////////////////
+ // Every other error
+ //////////////////
+ // case TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR: {
+ // return <Attention type="danger" title={i18n.str``}>
+ // </Attention>
+ // }
+ //////////////////
+ // Default message for unhandled case
+ //////////////////
+ default: return <Attention type="danger" title={i18n.str`Unexpected error`}>
+ {error.message}
+ {showDetail &&
+ <pre class="whitespace-break-spaces ">
+ {JSON.stringify(error.errorDetail, undefined, 2)}
+ </pre>
+ }
+ </Attention>
+ }
+}
+
diff --git a/packages/web-util/src/components/ErrorLoadingMerchant.tsx b/packages/web-util/src/components/ErrorLoadingMerchant.tsx
new file mode 100644
index 000000000..7089266b9
--- /dev/null
+++ b/packages/web-util/src/components/ErrorLoadingMerchant.tsx
@@ -0,0 +1,147 @@
+/*
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { TalerError, TalerErrorCode, assertUnreachable } from "@gnu-taler/taler-util";
+import { Fragment, VNode, h } from "preact";
+import { Attention } from "./Attention.js";
+import { useTranslationContext } from "../index.browser.js";
+
+export function ErrorLoading({ error, showDetail }: { error: TalerError, showDetail?: boolean }): VNode {
+ const { i18n } = useTranslationContext()
+ switch (error.errorDetail.code) {
+ //////////////////
+ // Every error that can be produce in a Http Request
+ //////////////////
+ case TalerErrorCode.GENERIC_TIMEOUT: {
+ if (error.hasErrorCode(TalerErrorCode.GENERIC_TIMEOUT)) {
+ const { requestMethod, requestUrl, timeoutMs } = error.errorDetail
+ return <Attention type="danger" title={i18n.str`The request reached a timeout, check your connection.`}>
+ {error.message}
+ {showDetail &&
+ <pre class="whitespace-break-spaces ">
+ {JSON.stringify({ requestMethod, requestUrl, timeoutMs }, undefined, 2)}
+ </pre>
+ }
+ </Attention>
+ }
+ assertUnreachable(1 as never)
+ }
+ case TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR: {
+ if (error.hasErrorCode(TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR)) {
+ const { requestMethod, requestUrl, timeoutMs } = error.errorDetail
+ return <Attention type="danger" title={i18n.str`The request was cancelled.`}>
+ {error.message}
+ {showDetail &&
+ <pre class="whitespace-break-spaces ">
+ {JSON.stringify({ requestMethod, requestUrl, timeoutMs }, undefined, 2)}
+ </pre>
+ }
+ </Attention>
+ }
+ assertUnreachable(1 as never)
+ }
+ case TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT: {
+ if (error.hasErrorCode(TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT)) {
+ const { requestMethod, requestUrl, timeoutMs } = error.errorDetail
+ return <Attention type="danger" title={i18n.str`The request reached a timeout, check your connection.`}>
+ {error.message}
+ {showDetail &&
+ <pre class="whitespace-break-spaces ">
+ {JSON.stringify({ requestMethod, requestUrl, timeoutMs }, undefined, 2)}
+ </pre>
+ }
+ </Attention>
+ }
+ assertUnreachable(1 as never)
+ }
+ case TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED: {
+ if (error.hasErrorCode(TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED)) {
+ const { requestMethod, requestUrl, throttleStats } = error.errorDetail
+ return <Attention type="danger" title={i18n.str`A lot of request were made to the same server and this action was throttled`}>
+ {error.message}
+ {showDetail &&
+ <pre class="whitespace-break-spaces ">
+ {JSON.stringify({ requestMethod, requestUrl, throttleStats }, undefined, 2)}
+ </pre>
+ }
+ </Attention>
+ }
+ assertUnreachable(1 as never)
+ }
+ case TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE: {
+ if (error.hasErrorCode(TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE)) {
+ const { requestMethod, requestUrl, httpStatusCode, validationError } = error.errorDetail
+ return <Attention type="danger" title={i18n.str`The response of the request is malformed.`}>
+ {error.message}
+ {showDetail &&
+ <pre class="whitespace-break-spaces ">
+ {JSON.stringify({ requestMethod, requestUrl, httpStatusCode, validationError }, undefined, 2)}
+ </pre>
+ }
+ </Attention>
+ }
+ assertUnreachable(1 as never)
+ }
+ case TalerErrorCode.WALLET_NETWORK_ERROR: {
+ if (error.hasErrorCode(TalerErrorCode.WALLET_NETWORK_ERROR)) {
+ const { requestMethod, requestUrl } = error.errorDetail
+ return <Attention type="danger" title={i18n.str`Could not complete the request due to a network problem.`}>
+ {error.message}
+ {showDetail &&
+ <pre class="whitespace-break-spaces ">
+ {JSON.stringify({ requestMethod, requestUrl }, undefined, 2)}
+ </pre>
+ }
+ </Attention>
+ }
+ assertUnreachable(1 as never)
+ }
+ case TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR: {
+ if (error.hasErrorCode(TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR)) {
+ const { requestMethod, requestUrl, httpStatusCode, errorResponse } = error.errorDetail
+ return <Attention type="danger" title={i18n.str`Unexpected request error`}>
+ {error.message}
+ {showDetail &&
+ <pre class="whitespace-break-spaces ">
+ {JSON.stringify({ requestMethod, requestUrl, httpStatusCode, errorResponse }, undefined, 2)}
+ </pre>
+ }
+ </Attention>
+ }
+ assertUnreachable(1 as never)
+ }
+ //////////////////
+ // Every other error
+ //////////////////
+ // case TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR: {
+ // return <Attention type="danger" title={i18n.str``}>
+ // </Attention>
+ // }
+ //////////////////
+ // Default message for unhandled case
+ //////////////////
+ default: return <Attention type="danger" title={i18n.str`Unexpected error`}>
+ {error.message}
+ {showDetail &&
+ <pre class="whitespace-break-spaces ">
+ {JSON.stringify(error.errorDetail, undefined, 2)}
+ </pre>
+ }
+ </Attention>
+ }
+}
+
diff --git a/packages/web-util/src/components/Footer.tsx b/packages/web-util/src/components/Footer.tsx
new file mode 100644
index 000000000..58dd2a60a
--- /dev/null
+++ b/packages/web-util/src/components/Footer.tsx
@@ -0,0 +1,48 @@
+import { useTranslationContext } from "../index.browser.js";
+import { h } from "preact";
+
+export function Footer({ testingUrlKey, VERSION, GIT_HASH }: { VERSION?: string, GIT_HASH?: string, testingUrlKey?: string }) {
+ const { i18n } = useTranslationContext()
+
+ const testingUrl = (testingUrlKey && typeof localStorage !== "undefined") && localStorage.getItem(testingUrlKey) ?
+ localStorage.getItem(testingUrlKey) ?? undefined :
+ undefined
+ const versionText = VERSION
+ ? GIT_HASH
+ ? <a href={`https://git.taler.net/wallet-core.git/tree/?id=${GIT_HASH}`} target="_blank" rel="noreferrer noopener">
+ Version {VERSION} ({GIT_HASH.substring(0, 8)})
+ </a>
+ : VERSION
+ : "";
+ return (
+ <footer class="bottom-4 my-4 mx-8 bg-slate-200">
+ <div>
+ <p class="text-xs leading-5 text-gray-400">
+ <i18n.Translate>
+ 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>
+ </i18n.Translate>
+ </p>
+ </div>
+ <div style="flex-grow:1" />
+ <p class="text-xs leading-5 text-gray-400">
+ Copyright &copy; 2014&mdash;2023 Taler Systems SA. {versionText}{" "}
+ </p>
+ {testingUrlKey && testingUrl &&
+
+ <p class="text-xs leading-5 text-gray-300">
+ Testing with {testingUrl}{" "}
+ <a
+ href=""
+ onClick={(e) => {
+ e.preventDefault();
+ localStorage.removeItem(testingUrlKey);
+ window.location.reload();
+ }}
+ >
+ stop testing
+ </a>
+ </p>
+ }
+ </footer>
+ );
+}
diff --git a/packages/web-util/src/components/Header.tsx b/packages/web-util/src/components/Header.tsx
new file mode 100644
index 000000000..29f4a4949
--- /dev/null
+++ b/packages/web-util/src/components/Header.tsx
@@ -0,0 +1,183 @@
+import { useState } from "preact/hooks";
+import { LangSelector, useNotifications, useTranslationContext } from "../index.browser.js";
+import { ComponentChildren, Fragment, VNode, h } from "preact";
+import logo from "../assets/logo-2021.svg";
+
+interface Props {
+ title: string;
+ iconLinkURL: string;
+ profileURL?: string;
+ notificationURL?: string;
+ children?: ComponentChildren;
+ onLogout: (() => void) | undefined;
+ sites: Array<Array<string>>;
+ supportedLangs: string[]
+}
+
+export function Header({ title, profileURL, notificationURL, iconLinkURL, sites, onLogout, children }: Props): VNode {
+ const { i18n } = useTranslationContext();
+ const [open, setOpen] = useState(false)
+ const ns = useNotifications();
+
+ 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={iconLinkURL ?? "#"} name="logo">
+ <img
+ class="h-8 w-auto"
+ src={logo}
+ alt="GNU Taler"
+ style={{ height: "1.5rem", margin: ".5rem" }}
+ />
+ </a>
+ </div>
+ <span class="flex items-center text-white text-lg font-bold ml-4">
+ {title}
+ </span>
+ </div>
+ <div class="flex-1 ml-6 ">
+ <div class="flex flex-1 space-x-4">
+ {sites.map((site) => {
+ if (site.length !== 2) return;
+ const [name, url] = site
+ return <a href={url} name={`site header ${name}`} class="hidden sm:block text-white hover:bg-indigo-500 hover:bg-opacity-75 rounded-md py-2 px-3 text-sm font-medium">{name}</a>
+ })}
+ </div>
+ </div>
+ <div class="flex justify-end">
+ {!notificationURL ? undefined :
+ <a href={notificationURL} name="notifications" class="relative inline-flex items-center justify-center rounded-md bg-indigo-600 p-1 mr-2 text-indigo-200 hover:bg-indigo-500 hover:bg-opacity-75 hover:text-white focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-indigo-600" aria-controls="mobile-menu" aria-expanded="false">
+ <span class="absolute -inset-0.5"></span>
+ <span class="sr-only"><i18n.Translate>Show notifications</i18n.Translate></span>
+ {ns.length > 0 ?
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-10 h-10">
+ <path d="M5.85 3.5a.75.75 0 0 0-1.117-1 9.719 9.719 0 0 0-2.348 4.876.75.75 0 0 0 1.479.248A8.219 8.219 0 0 1 5.85 3.5ZM19.267 2.5a.75.75 0 1 0-1.118 1 8.22 8.22 0 0 1 1.987 4.124.75.75 0 0 0 1.48-.248A9.72 9.72 0 0 0 19.266 2.5Z" />
+ <path fill-rule="evenodd" d="M12 2.25A6.75 6.75 0 0 0 5.25 9v.75a8.217 8.217 0 0 1-2.119 5.52.75.75 0 0 0 .298 1.206c1.544.57 3.16.99 4.831 1.243a3.75 3.75 0 1 0 7.48 0 24.583 24.583 0 0 0 4.83-1.244.75.75 0 0 0 .298-1.205 8.217 8.217 0 0 1-2.118-5.52V9A6.75 6.75 0 0 0 12 2.25ZM9.75 18c0-.034 0-.067.002-.1a25.05 25.05 0 0 0 4.496 0l.002.1a2.25 2.25 0 1 1-4.5 0Z" clip-rule="evenodd" />
+ </svg>
+ :
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-10 h-10">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 0 0 5.454-1.31A8.967 8.967 0 0 1 18 9.75V9A6 6 0 0 0 6 9v.75a8.967 8.967 0 0 1-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 0 1-5.714 0m5.714 0a3 3 0 1 1-5.714 0" />
+ </svg>
+ }
+ </a>
+ }
+ {!profileURL ? undefined :
+ <a href={profileURL} name="profile" class="relative inline-flex items-center justify-center rounded-md bg-indigo-600 p-1 mr-2 text-indigo-200 hover:bg-indigo-500 hover:bg-opacity-75 hover:text-white focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-indigo-600" aria-controls="mobile-menu" aria-expanded="false">
+ <span class="absolute -inset-0.5"></span>
+ <span class="sr-only"><i18n.Translate>Open profile</i18n.Translate></span>
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-10 h-10">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M17.982 18.725A7.488 7.488 0 0 0 12 15.75a7.488 7.488 0 0 0-5.982 2.975m11.963 0a9 9 0 1 0-11.963 0m11.963 0A8.966 8.966 0 0 1 12 21a8.966 8.966 0 0 1-5.982-2.275M15 9.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
+ </svg>
+ </a>
+ }
+ <button type="button" name="toggle sidebar" class="relative inline-flex items-center justify-center rounded-md bg-indigo-600 p-1 text-indigo-200 hover:bg-indigo-500 hover:bg-opacity-75 hover:text-white focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-indigo-600" aria-controls="mobile-menu" aria-expanded="false"
+ onClick={(e) => {
+ setOpen(!open)
+ }}>
+ <span class="absolute -inset-0.5"></span>
+ <span class="sr-only"><i18n.Translate>Open settings</i18n.Translate></span>
+ <svg class="block h-10 w-10" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" aria-hidden="true">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
+ </svg>
+ </button>
+ </div>
+ </div>
+ </header>
+
+ {
+ open &&
+ <div class="relative z-10" name="sidebar overlay" aria-labelledby="slide-over-title" role="dialog" aria-modal="true"
+ onClick={() => {
+ setOpen(false)
+ }}>
+ <div class="fixed inset-0"></div>
+
+ <div class="fixed inset-0 overflow-hidden">
+ <div class="absolute inset-0 overflow-hidden">
+ <div class="pointer-events-none fixed inset-y-0 right-0 flex max-w-full pl-10">
+ <div class="pointer-events-auto w-screen max-w-md" >
+ <div class="flex h-full flex-col overflow-y-scroll bg-white py-6 shadow-xl" onClick={(e) => {
+ //do not trigger close if clicking inside the sidebar
+ e.stopPropagation();
+ }}>
+ <div class="px-4 sm:px-6" >
+ <div class="flex items-start justify-between" >
+ <h2 class="text-base font-semibold leading-6 text-gray-900" id="slide-over-title">
+ <i18n.Translate>Menu</i18n.Translate>
+ </h2>
+ <div class="ml-3 flex h-7 items-center">
+ <button type="button" name="close sidebar" class="relative rounded-md bg-white text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
+ onClick={(e) => {
+ setOpen(false)
+ }}
+
+ >
+ <span class="absolute -inset-2.5"></span>
+ <span class="sr-only">
+ <i18n.Translate>Close panel</i18n.Translate>
+ </span>
+ <svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
+ </svg>
+ </button>
+ </div>
+ </div>
+ </div>
+ <div class="relative mt-6 flex-1 px-4 sm:px-6">
+ <nav class="flex flex-1 flex-col" aria-label="Sidebar">
+ <ul role="list" class="flex flex-1 flex-col gap-y-7">
+ {onLogout ?
+ <li>
+ <a href="#"
+ name="logout"
+ class="text-gray-700 hover:text-indigo-600 hover:bg-gray-100 group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold"
+ onClick={() => {
+ onLogout();
+ setOpen(false)
+ }}
+ >
+ <svg class="h-6 w-6 shrink-0 text-indigo-600" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" />
+ </svg>
+ <i18n.Translate>Log out</i18n.Translate>
+ </a>
+ </li>
+ : undefined}
+ <li>
+ <LangSelector />
+ </li>
+ {/* CHILDREN */}
+ {children}
+ {/* /CHILDREN */}
+ {sites.length > 0 ?
+ <li class="block sm:hidden">
+ <div class="text-xs font-semibold leading-6 text-gray-400">
+ <i18n.Translate>Sites</i18n.Translate>
+ </div>
+ <ul role="list" class="space-y-1">
+ {sites.map(([name, url]) => {
+ return <li>
+ <a href={url} name={`site ${name}`} target="_blank" rel="noopener noreferrer" class="text-gray-700 hover:text-indigo-600 hover:bg-gray-100 group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold">
+ <span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-lg border text-[0.625rem] font-medium bg-white text-gray-400 border-gray-200 group-hover:border-indigo-600 group-hover:text-indigo-600">&gt;</span>
+ <span class="truncate">{name}</span>
+ </a>
+ </li>
+ })}
+ </ul>
+ </li>
+ : undefined
+ }
+ </ul>
+ </nav>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ }
+ </Fragment >
+}
diff --git a/packages/web-util/src/components/LangSelector.tsx b/packages/web-util/src/components/LangSelector.tsx
new file mode 100644
index 000000000..8e5a82f75
--- /dev/null
+++ b/packages/web-util/src/components/LangSelector.tsx
@@ -0,0 +1,115 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { Fragment, h, VNode } from "preact";
+import { useEffect, useState } from "preact/hooks";
+// import { strings as messages } from "../i18n/strings.js";
+import langIcon from "../assets/lang.svg";
+import { useTranslationContext } from "../index.browser.js";
+
+type LangsNames = {
+ [P: string]: string;
+};
+
+const names: LangsNames = {
+ es: "Español [es]",
+ en: "English [en]",
+ fr: "Français [fr]",
+ de: "Deutsch [de]",
+ sv: "Svenska [sv]",
+ it: "Italiano [it]",
+};
+
+function getLangName(s: keyof LangsNames | string): string {
+ if (names[s]) return names[s];
+ return String(s);
+}
+
+export function LangSelector({ }: {}): VNode {
+ const [updatingLang, setUpdatingLang] = useState(false);
+ const { lang, changeLanguage, completeness, supportedLang } = useTranslationContext();
+ const [hidden, setHidden] = useState(true);
+
+ useEffect(() => {
+ function bodyKeyPress(event: KeyboardEvent) {
+ if (event.code === "Escape") setHidden(true);
+ }
+ function bodyOnClick(event: Event) {
+ setHidden(true);
+ }
+ document.body.addEventListener("click", bodyOnClick);
+ document.body.addEventListener("keydown", bodyKeyPress as any);
+ return () => {
+ document.body.removeEventListener("keydown", bodyKeyPress as any);
+ document.body.removeEventListener("click", bodyOnClick);
+ };
+ }, []);
+ return (
+ <div>
+ <div class="relative mt-2">
+ <button type="button" class="relative w-full cursor-default rounded-md bg-white py-1.5 pl-3 pr-10 text-left text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-600 sm:text-sm sm:leading-6" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label"
+ onClick={(e) => {
+ setHidden(!hidden);
+ e.stopPropagation()
+ }}>
+ <span class="flex items-center">
+ <img alt="language" class="h-5 w-5 flex-shrink-0 rounded-full" src={langIcon} />
+ <span class="ml-3 block truncate">{getLangName(lang)}</span>
+ </span>
+ <span class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
+ <svg class="h-5 w-5 text-gray-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
+ <path fill-rule="evenodd" d="M10 3a.75.75 0 01.55.24l3.25 3.5a.75.75 0 11-1.1 1.02L10 4.852 7.3 7.76a.75.75 0 01-1.1-1.02l3.25-3.5A.75.75 0 0110 3zm-3.76 9.2a.75.75 0 011.06.04l2.7 2.908 2.7-2.908a.75.75 0 111.1 1.02l-3.25 3.5a.75.75 0 01-1.1 0l-3.25-3.5a.75.75 0 01.04-1.06z" clip-rule="evenodd" />
+ </svg>
+ </span>
+ </button>
+
+ {!hidden &&
+ <ul class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm" tabIndex={-1} role="listbox" aria-labelledby="listbox-label" aria-activedescendant="listbox-option-3">
+ {Object.keys(supportedLang)
+ .filter((l) => l !== lang)
+ .map((lang) => (
+ <li class="text-gray-900 hover:bg-indigo-600 hover:text-white cursor-pointer relative select-none py-2 pl-3 pr-9" role="option"
+ onClick={() => {
+ changeLanguage(lang);
+ setUpdatingLang(false);
+ setHidden(true)
+ }}
+ >
+ <span class="font-normal truncate flex justify-between ">
+ <span>{getLangName(lang)}</span>
+ <span>{(completeness as any)[lang]}%</span>
+ </span>
+
+ <span class="text-indigo-600 absolute inset-y-0 right-0 flex items-center pr-4">
+ {/* <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
+ <path fill-rule="evenodd" d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z" clip-rule="evenodd" />
+ </svg> */}
+ </span>
+ </li>
+ ))}
+
+ </ul>
+ }
+
+ </div>
+ </div>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/components/LoadingError.tsx b/packages/web-util/src/components/Loading.tsx
index 2cd8bee3b..c5dcd90c1 100644
--- a/packages/taler-wallet-webextension/src/components/LoadingError.tsx
+++ b/packages/web-util/src/components/Loading.tsx
@@ -13,18 +13,33 @@
You should have received 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 { HookError } from "../hooks/useAsyncAsHook.js";
-import { ErrorMessage } from "./ErrorMessage.js";
-import { ErrorTalerOperation } from "./ErrorTalerOperation.js";
-export interface Props {
- title: VNode;
- error: HookError;
+export function Loading(): VNode {
+ return (
+ <div
+ class="columns is-centered is-vcentered"
+ style={{
+ width: "100%",
+ height: "200px",
+ display: "flex",
+ margin: "auto",
+ justifyContent: "center",
+ }}
+ >
+ <Spinner />
+ </div>
+ );
}
-export function LoadingError({ title, error }: Props): VNode {
- if (error.operational) {
- return <ErrorTalerOperation title={title} error={error.details} />;
- }
- return <ErrorMessage title={title} description={error.message} />;
+
+function Spinner(): VNode {
+ return (
+ <div class="lds-ring" style={{ margin: "auto" }}>
+ <div />
+ <div />
+ <div />
+ <div />
+ </div>
+ );
}
diff --git a/packages/web-util/src/components/NotificationBanner.tsx b/packages/web-util/src/components/NotificationBanner.tsx
new file mode 100644
index 000000000..31d5a5d01
--- /dev/null
+++ b/packages/web-util/src/components/NotificationBanner.tsx
@@ -0,0 +1,33 @@
+import { h, Fragment, VNode } from "preact";
+import { Attention } from "./Attention.js";
+import { Notification } from "../index.browser.js";
+
+export function LocalNotificationBanner({ notification, showDebug }: { notification?: Notification, showDebug?: boolean }): VNode {
+ if (!notification) return <Fragment />
+ switch (notification.message.type) {
+ case "error":
+ return <div class="relative">
+ <div class="fixed top-0 left-0 right-0 z-20 w-full p-4">
+ <Attention type="danger" title={notification.message.title} onClose={() => {
+ notification.acknowledge()
+ }}>
+ {notification.message.description &&
+ <div class="mt-2 text-sm text-red-700">
+ {notification.message.description}
+ </div>
+ }
+ {showDebug && <pre class="whitespace-break-spaces ">
+ {notification.message.debug}
+ </pre>}
+ </Attention>
+ </div>
+ </div>
+ case "info":
+ return <div class="relative">
+ <div class="fixed top-0 left-0 right-0 z-20 w-full p-4">
+ <Attention type="success" title={notification.message.title} onClose={() => {
+ notification.acknowledge();
+ }} /></div></div>
+ }
+}
+
diff --git a/packages/demobank-ui/src/pages/home/ShowInputErrorLabel.tsx b/packages/web-util/src/components/ShowInputErrorLabel.tsx
index dacffe20a..c5840cad9 100644
--- a/packages/demobank-ui/src/pages/home/ShowInputErrorLabel.tsx
+++ b/packages/web-util/src/components/ShowInputErrorLabel.tsx
@@ -24,6 +24,6 @@ export function ShowInputErrorLabel({
isDirty: boolean;
}): VNode {
if (message && isDirty)
- return <div style={{ marginTop: 8, color: "red" }}>{message}</div>;
- return <Fragment />;
+ return <div class="text-base" style={{ color: "red" }}>{message}</div>;
+ return <div class="text-base" style={{ }}> </div>;
}
diff --git a/packages/web-util/src/components/ToastBanner.tsx b/packages/web-util/src/components/ToastBanner.tsx
new file mode 100644
index 000000000..ece26285f
--- /dev/null
+++ b/packages/web-util/src/components/ToastBanner.tsx
@@ -0,0 +1,61 @@
+/*
+ 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 { Fragment, VNode, h } from "preact"
+import { Attention, GLOBAL_NOTIFICATION_TIMEOUT as GLOBAL_TOAST_TIMEOUT, Notification, useNotifications } from "../index.browser.js"
+
+/**
+ * Toasts should be considered when displaying these types of information to the user:
+ *
+ * Low attention messages that do not require user action
+ * Singular status updates
+ * Confirmations
+ * Information that does not need to be followed up
+ *
+ * Do not use toasts if the information contains the following:
+ *
+ * High attention and crtitical information
+ * Time-sensitive information
+ * Requires user action or input
+ * Batch updates
+ *
+ * @returns
+ */
+export function ToastBanner(): VNode {
+ const notifs = useNotifications()
+ if (notifs.length === 0) return <Fragment />
+ const show = notifs.filter(e => !e.message.ack && !e.message.timeout)
+ if (show.length === 0) return <Fragment />
+ return <AttentionByType msg={show[0]} />
+}
+
+function AttentionByType({ msg }: { msg: Notification }) {
+ switch (msg.message.type) {
+ case "error":
+ return <Attention type="danger" title={msg.message.title} onClose={() => {
+ msg.acknowledge()
+ }} timeout={GLOBAL_TOAST_TIMEOUT}>
+ {msg.message.description &&
+ <div class="mt-2 text-sm text-red-700">
+ {msg.message.description}
+ </div>
+ }
+ </Attention>
+ case "info":
+ return <Attention type="success" title={msg.message.title} onClose={() => {
+ msg.acknowledge();
+ }} timeout={GLOBAL_TOAST_TIMEOUT} />
+ }
+}
diff --git a/packages/web-util/src/components/index.ts b/packages/web-util/src/components/index.ts
new file mode 100644
index 000000000..d7ea41874
--- /dev/null
+++ b/packages/web-util/src/components/index.ts
@@ -0,0 +1,12 @@
+export * as utils from "./utils.js";
+export * from "./Attention.js";
+export * from "./CopyButton.js";
+export * from "./ErrorLoading.js";
+export * from "./LangSelector.js";
+export * from "./Loading.js";
+export * from "./Header.js";
+export * from "./Footer.js";
+export * from "./Button.js";
+export * from "./ShowInputErrorLabel.js";
+export * from "./NotificationBanner.js";
+export * from "./ToastBanner.js";
diff --git a/packages/web-util/src/components/utils.ts b/packages/web-util/src/components/utils.ts
new file mode 100644
index 000000000..75c3fc0fe
--- /dev/null
+++ b/packages/web-util/src/components/utils.ts
@@ -0,0 +1,111 @@
+import { createElement, VNode } from "preact";
+
+export type StateFunc<S> = (p: S) => VNode;
+
+export type StateViewMap<StateType extends { status: string }> = {
+ [S in StateType as S["status"]]: StateFunc<S>;
+};
+
+export type RecursiveState<S extends object> = S | (() => RecursiveState<S>);
+
+export function compose<SType extends { status: string }, PType>(
+ hook: (p: PType) => RecursiveState<SType>,
+ viewMap: StateViewMap<SType>,
+): (p: PType) => VNode {
+
+ function withHook(stateHook: () => RecursiveState<SType>): () => VNode {
+ function ComposedComponent(): VNode {
+ const state = stateHook();
+
+ if (typeof state === "function") {
+ const subComponent = withHook(state);
+ return createElement(subComponent, {});
+ }
+
+ const statusName = state.status as unknown as SType["status"];
+ const viewComponent = viewMap[statusName] as unknown as StateFunc<SType>;
+ return createElement(viewComponent, state);
+ }
+
+ return ComposedComponent;
+ }
+
+ return (p: PType) => {
+ const h = withHook(() => hook(p));
+ return h();
+ };
+}
+
+export function recursive<PType>(
+ hook: (p: PType) => RecursiveState<VNode>,
+): (p: PType) => VNode {
+
+ function withHook(stateHook: () => RecursiveState<VNode>): () => VNode {
+ function ComposedComponent(): VNode {
+ const state = stateHook();
+
+ if (typeof state === "function") {
+ const subComponent = withHook(state);
+ return createElement(subComponent, {});
+ }
+
+ return state;
+ }
+
+ return ComposedComponent;
+ }
+
+ return (p: PType) => {
+ const h = withHook(() => hook(p));
+ return h();
+ };
+}
+
+
+
+/**
+ *
+ * @param obj VNode
+ * @returns
+ */
+export function saveVNodeForInspection<T>(obj: T): T {
+ // @ts-ignore
+ window["showVNodeInfo"] = function showVNodeInfo() {
+ inspect(obj);
+ };
+ return obj;
+}
+function inspect(obj: any) {
+ if (!obj) return;
+ if (obj.__c && obj.__c.__H) {
+ const componentName = obj.__c.constructor.name;
+ const hookState = obj.__c.__H;
+ const stateList = hookState.__ as Array<any>;
+ console.log("==============", componentName);
+ stateList.forEach((hook) => {
+ const { __: value, c: context, __h: factory, __H: args } = hook;
+ if (typeof context !== "undefined") {
+ const { __c: contextId } = context;
+ console.log("context:", contextId, hook);
+ } else if (typeof factory === "function") {
+ console.log("memo:", value, "deps:", args);
+ } else if (typeof value === "function") {
+ const effectName = value.name;
+ console.log("effect:", effectName, "deps:", args);
+ } else if (typeof value.current !== "undefined") {
+ const ref = value.current;
+ console.log("ref:", ref instanceof Element ? ref.outerHTML : ref);
+ } else if (value instanceof Array) {
+ console.log("state:", value[0]);
+ } else {
+ console.log(hook);
+ }
+ });
+ }
+ const children = obj.__k;
+ if (children instanceof Array) {
+ children.forEach((e) => inspect(e));
+ } else {
+ inspect(children);
+ }
+}
diff --git a/packages/web-util/src/context/activity.ts b/packages/web-util/src/context/activity.ts
new file mode 100644
index 000000000..fd366cbe5
--- /dev/null
+++ b/packages/web-util/src/context/activity.ts
@@ -0,0 +1,72 @@
+/*
+ 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 { ChallengerHttpClient, ObservabilityEvent, TalerAuthenticationHttpClient, TalerBankConversionHttpClient, TalerCoreBankHttpClient, TalerMerchantInstanceHttpClient, TalerMerchantManagementHttpClient } from "@gnu-taler/taler-util";
+
+type Listener<Event> = (e: Event) => void;
+type Unsuscriber = () => void;
+export type Subscriber<Event> = (fn: Listener<Event>) => Unsuscriber;
+
+export class ActiviyTracker<Event> {
+ private observers = new Array<Listener<Event>>();
+ constructor() {
+ this.notify = this.notify.bind(this)
+ this.subscribe = this.subscribe.bind(this)
+ }
+ notify(data: Event): void {
+ this.observers.forEach((observer) => observer(data))
+ }
+ subscribe(func: Listener<Event>): Unsuscriber {
+ this.observers.push(func);
+ return () => {
+ this.observers.forEach((observer, index) => {
+ if (observer === func) {
+ this.observers.splice(index, 1);
+ }
+ });
+ };
+ }
+}
+
+/**
+ * build http client with cache breaker due to SWR
+ * @param url
+ * @returns
+ */
+export interface APIClient<T, C> {
+ getRemoteConfig(): Promise<C>;
+ VERSION: string;
+ lib: T,
+ onActivity: Subscriber<ObservabilityEvent>;
+ cancelRequest(id: string): void;
+}
+
+export interface MerchantLib {
+ instance: TalerMerchantManagementHttpClient;
+ authenticate: TalerAuthenticationHttpClient;
+ subInstanceApi: (instanceId: string) => MerchantLib;
+}
+
+export interface BankLib {
+ bank: TalerCoreBankHttpClient;
+ conversion: TalerBankConversionHttpClient;
+ auth: (user: string) => TalerAuthenticationHttpClient;
+}
+
+export interface ChallengerLib {
+ challenger: ChallengerHttpClient;
+}
+
diff --git a/packages/web-util/src/context/api.ts b/packages/web-util/src/context/api.ts
new file mode 100644
index 000000000..c1eaa37f8
--- /dev/null
+++ b/packages/web-util/src/context/api.ts
@@ -0,0 +1,49 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { TalerBankIntegrationHttpClient, TalerCoreBankHttpClient, TalerRevenueHttpClient, TalerWireGatewayHttpClient } from "@gnu-taler/taler-util";
+import { ComponentChildren, createContext, h, VNode } from "preact";
+import { useContext } from "preact/hooks";
+import { defaultRequestHandler } from "../utils/request.js";
+
+interface Type {
+ /**
+ * @deprecated this show not be used
+ */
+ request: typeof defaultRequestHandler;
+ bankCore: TalerCoreBankHttpClient,
+ bankIntegration: TalerBankIntegrationHttpClient,
+ bankWire: TalerWireGatewayHttpClient,
+ bankRevenue: TalerRevenueHttpClient,
+}
+
+const Context = createContext<Type>({ request: defaultRequestHandler } as any);
+
+export const useApiContext = (): Type => useContext(Context);
+export const ApiContextProvider = ({
+ children,
+ value,
+}: {
+ value: Type;
+ children: ComponentChildren;
+}): VNode => {
+ return h(Context.Provider, { value, children });
+};
diff --git a/packages/web-util/src/context/bank-api.ts b/packages/web-util/src/context/bank-api.ts
new file mode 100644
index 000000000..3f6a32f4b
--- /dev/null
+++ b/packages/web-util/src/context/bank-api.ts
@@ -0,0 +1,224 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ CacheEvictor,
+ LibtoolVersion,
+ ObservabilityEvent,
+ ObservableHttpClientLibrary,
+ TalerAuthenticationHttpClient,
+ TalerBankConversionCacheEviction,
+ TalerBankConversionHttpClient,
+ TalerCoreBankCacheEviction,
+ TalerCoreBankHttpClient,
+ TalerCorebankApi,
+ TalerError,
+} from "@gnu-taler/taler-util";
+import {
+ ComponentChildren,
+ FunctionComponent,
+ VNode,
+ createContext,
+ h,
+} from "preact";
+import { useContext, useEffect, useState } from "preact/hooks";
+import { APIClient, ActiviyTracker, BankLib, Subscriber } from "./activity.js";
+import { useTranslationContext } from "./translation.js";
+import { BrowserFetchHttpLib, ErrorLoading } from "../index.browser.js";
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+export type BankContextType = {
+ url: URL;
+ config: TalerCorebankApi.Config;
+ lib: BankLib;
+ hints: VersionHint[];
+ onActivity: Subscriber<ObservabilityEvent>;
+ cancelRequest: (eventId: string) => void;
+};
+
+// @ts-expect-error default value to undefined, should it be another thing?
+const BankContext = createContext<BankContextType>(undefined);
+
+export const useBankCoreApiContext = (): BankContextType =>
+ useContext(BankContext);
+
+enum VersionHint {
+ NONE,
+}
+
+type Evictors = {
+ conversion?: CacheEvictor<TalerBankConversionCacheEviction>;
+ bank?: CacheEvictor<TalerCoreBankCacheEviction>;
+};
+
+type ConfigResult<T> =
+ | undefined
+ | { type: "ok"; config: T; hints: VersionHint[] }
+ | { type: "incompatible"; result: T; supported: string }
+ | { type: "error"; error: TalerError };
+
+const CONFIG_FAIL_TRY_AGAIN_MS = 5000;
+
+export const BankApiProvider = ({
+ baseUrl,
+ children,
+ frameOnError,
+ evictors = {},
+}: {
+ baseUrl: URL;
+ children: ComponentChildren;
+ evictors?: Evictors;
+ frameOnError: FunctionComponent<{ children: ComponentChildren }>;
+}): VNode => {
+ const [checked, setChecked] =
+ useState<ConfigResult<TalerCorebankApi.Config>>();
+ const { i18n } = useTranslationContext();
+
+ const { getRemoteConfig, VERSION, lib, cancelRequest, onActivity } =
+ buildBankApiClient(baseUrl, evictors);
+
+ useEffect(() => {
+ let keepRetrying = true;
+ async function testConfig(): Promise<void> {
+ try {
+ const config = await getRemoteConfig();
+ if (LibtoolVersion.compare(VERSION, config.version)) {
+ setChecked({ type: "ok", config, hints: [] });
+ } else {
+ setChecked({
+ type: "incompatible",
+ result: config,
+ supported: VERSION,
+ });
+ }
+ } catch (error) {
+ if (error instanceof TalerError) {
+ if (keepRetrying) {
+ setTimeout(() => {
+ testConfig();
+ }, CONFIG_FAIL_TRY_AGAIN_MS);
+ }
+ setChecked({ type: "error", error });
+ } else {
+ setChecked({ type: "error", error: TalerError.fromException(error) });
+ }
+ }
+ }
+ testConfig();
+ return () => {
+ // on unload, stop retry
+ keepRetrying = false;
+ };
+ }, []);
+
+ if (checked === undefined) {
+ return h(frameOnError, {
+ children: h("div", {}, "checking compatibility with server..."),
+ });
+ }
+ if (checked.type === "error") {
+ return h(frameOnError, {
+ children: h(ErrorLoading, { error: checked.error, showDetail: true }),
+ });
+ }
+ if (checked.type === "incompatible") {
+ return h(frameOnError, {
+ children: h(
+ "div",
+ {},
+ i18n.str`The server version is not supported. Supported version "${checked.supported}", server version "${checked.result.version}"`,
+ ),
+ });
+ }
+
+ const value: BankContextType = {
+ url: baseUrl,
+ config: checked.config,
+ onActivity: onActivity,
+ lib,
+ cancelRequest,
+ hints: checked.hints,
+ };
+ return h(BankContext.Provider, {
+ value,
+ children,
+ });
+};
+
+function buildBankApiClient(
+ url: URL,
+ evictors: Evictors,
+): APIClient<BankLib, TalerCorebankApi.Config> {
+ const httpFetch = new BrowserFetchHttpLib({
+ enableThrottling: true,
+ requireTls: false,
+ });
+ const tracker = new ActiviyTracker<ObservabilityEvent>();
+ const httpLib = new ObservableHttpClientLibrary(httpFetch, {
+ observe(ev) {
+ tracker.notify(ev);
+ },
+ });
+
+ const bank = new TalerCoreBankHttpClient(url.href, httpLib, evictors.bank);
+ const conversion = new TalerBankConversionHttpClient(
+ bank.getConversionInfoAPI().href,
+ httpLib,
+ evictors.conversion,
+ );
+ const auth = (user: string) =>
+ new TalerAuthenticationHttpClient(
+ bank.getAuthenticationAPI(user).href,
+ httpLib,
+ );
+
+ async function getRemoteConfig(): Promise<TalerCorebankApi.Config> {
+ const resp = await bank.getConfig();
+ if (resp.type === "fail") {
+ throw TalerError.fromUncheckedDetail(resp.detail);
+ }
+ return resp.body;
+ }
+
+ return {
+ getRemoteConfig,
+ VERSION: bank.PROTOCOL_VERSION,
+ lib: {
+ bank,
+ conversion,
+ auth,
+ },
+ onActivity: tracker.subscribe,
+ cancelRequest: httpLib.cancelRequest,
+ };
+}
+
+export const BankApiProviderTesting = ({
+ children,
+ value,
+}: {
+ value: BankContextType;
+ children: ComponentChildren;
+}): VNode => {
+ return h(BankContext.Provider, {
+ value,
+ children,
+ });
+};
diff --git a/packages/web-util/src/context/challenger-api.ts b/packages/web-util/src/context/challenger-api.ts
new file mode 100644
index 000000000..8748f5f69
--- /dev/null
+++ b/packages/web-util/src/context/challenger-api.ts
@@ -0,0 +1,213 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ CacheEvictor,
+ ChallengerApi,
+ ChallengerCacheEviction,
+ ChallengerHttpClient,
+ LibtoolVersion,
+ ObservabilityEvent,
+ ObservableHttpClientLibrary,
+ TalerError
+} from "@gnu-taler/taler-util";
+import {
+ ComponentChildren,
+ FunctionComponent,
+ VNode,
+ createContext,
+ h,
+} from "preact";
+import { useContext, useEffect, useState } from "preact/hooks";
+import { BrowserFetchHttpLib, ErrorLoading } from "../index.browser.js";
+import {
+ APIClient,
+ ActiviyTracker,
+ ChallengerLib,
+ Subscriber
+} from "./activity.js";
+import { useTranslationContext } from "./translation.js";
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+export type ChallengerContextType = {
+ url: URL;
+ config: ChallengerApi.ChallengerTermsOfServiceResponse;
+ lib: ChallengerLib;
+ hints: VersionHint[];
+ onActivity: Subscriber<ObservabilityEvent>;
+ cancelRequest: (eventId: string) => void;
+};
+
+// @ts-expect-error default value to undefined, should it be another thing?
+const ChallengerContext = createContext<ChallengerContextType>(undefined);
+
+export const useChallengerApiContext = (): ChallengerContextType =>
+ useContext(ChallengerContext);
+
+enum VersionHint {
+ NONE,
+}
+
+type Evictors = {
+ challenger?: CacheEvictor<ChallengerCacheEviction>;
+}
+
+type ConfigResult<T> =
+ | undefined
+ | { type: "ok"; config: T; hints: VersionHint[] }
+ | { type: "incompatible"; result: T; supported: string }
+ | { type: "error"; error: TalerError };
+
+const CONFIG_FAIL_TRY_AGAIN_MS = 5000;
+
+export const ChallengerApiProvider = ({
+ baseUrl,
+ children,
+ frameOnError,
+ evictors = {},
+}: {
+ baseUrl: URL;
+ children: ComponentChildren;
+ evictors?: Evictors;
+ frameOnError: FunctionComponent<{ children: ComponentChildren }>;
+}): VNode => {
+ const [checked, setChecked] =
+ useState<ConfigResult<ChallengerApi.ChallengerTermsOfServiceResponse>>();
+ const { i18n } = useTranslationContext();
+
+ const { getRemoteConfig, VERSION, lib, cancelRequest, onActivity } =
+ buildChallengerApiClient(baseUrl, evictors);
+
+ useEffect(() => {
+ let keepRetrying = true;
+ async function testConfig(): Promise<void> {
+ try {
+ const config = await getRemoteConfig();
+ if (LibtoolVersion.compare(VERSION, config.version)) {
+ setChecked({ type: "ok", config, hints: [] });
+ } else {
+ setChecked({
+ type: "incompatible",
+ result: config,
+ supported: VERSION,
+ });
+ }
+ } catch (error) {
+ if (error instanceof TalerError) {
+ if (keepRetrying) {
+ setTimeout(() => {
+ testConfig();
+ }, CONFIG_FAIL_TRY_AGAIN_MS);
+ }
+ setChecked({ type: "error", error });
+ } else {
+ setChecked({ type: "error", error: TalerError.fromException(error) });
+ }
+ }
+ }
+ testConfig();
+ return () => {
+ // on unload, stop retry
+ keepRetrying = false;
+ };
+ }, []);
+
+ if (checked === undefined) {
+ return h(frameOnError, {
+ children: h("div", {}, "checking compatibility with server..."),
+ });
+ }
+ if (checked.type === "error") {
+ return h(frameOnError, {
+ children: h(ErrorLoading, { error: checked.error, showDetail: true }),
+ });
+ }
+ if (checked.type === "incompatible") {
+ return h(frameOnError, {
+ children: h(
+ "div",
+ {},
+ i18n.str`The server version is not supported. Supported version "${checked.supported}", server version "${checked.result.version}"`,
+ ),
+ });
+ }
+
+ const value: ChallengerContextType = {
+ url: baseUrl,
+ config: checked.config,
+ onActivity: onActivity,
+ lib,
+ cancelRequest,
+ hints: checked.hints,
+ };
+ return h(ChallengerContext.Provider, {
+ value,
+ children,
+ });
+};
+
+function buildChallengerApiClient(
+ url: URL,
+ evictors: Evictors,
+): APIClient<ChallengerLib, ChallengerApi.ChallengerTermsOfServiceResponse> {
+ const httpFetch = new BrowserFetchHttpLib({
+ enableThrottling: true,
+ requireTls: false,
+ });
+ const tracker = new ActiviyTracker<ObservabilityEvent>();
+ const httpLib = new ObservableHttpClientLibrary(httpFetch, {
+ observe(ev) {
+ tracker.notify(ev);
+ },
+ });
+
+ const challenger = new ChallengerHttpClient(url.href, httpLib, evictors.challenger);
+
+ async function getRemoteConfig(): Promise<ChallengerApi.ChallengerTermsOfServiceResponse> {
+ const resp = await challenger.getConfig();
+ if (resp.type === "fail") {
+ throw TalerError.fromUncheckedDetail(resp.detail);
+ }
+ return resp.body;
+ }
+
+ return {
+ getRemoteConfig,
+ VERSION: challenger.PROTOCOL_VERSION,
+ lib: {
+ challenger,
+ },
+ onActivity: tracker.subscribe,
+ cancelRequest: httpLib.cancelRequest,
+ };
+}
+
+export const ChallengerApiProviderTesting = ({
+ children,
+ value,
+}: {
+ value: ChallengerContextType;
+ children: ComponentChildren;
+}): VNode => {
+ return h(ChallengerContext.Provider, {
+ value,
+ children,
+ });
+};
diff --git a/packages/web-util/src/context/index.ts b/packages/web-util/src/context/index.ts
new file mode 100644
index 000000000..8e7f096da
--- /dev/null
+++ b/packages/web-util/src/context/index.ts
@@ -0,0 +1,11 @@
+export { ApiContextProvider, useApiContext } from "./api.js";
+export {
+ InternationalizationAPI,
+ TranslationProvider,
+ useTranslationContext
+} from "./translation.js";
+export * from "./bank-api.js";
+export * from "./challenger-api.js";
+export * from "./merchant-api.js";
+export * from "./navigation.js";
+export * from "./wallet-integration.js";
diff --git a/packages/web-util/src/context/merchant-api.ts b/packages/web-util/src/context/merchant-api.ts
new file mode 100644
index 000000000..03c95d48e
--- /dev/null
+++ b/packages/web-util/src/context/merchant-api.ts
@@ -0,0 +1,228 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ CacheEvictor,
+ LibtoolVersion,
+ ObservabilityEvent,
+ ObservableHttpClientLibrary,
+ TalerAuthenticationHttpClient,
+ TalerError,
+ TalerMerchantApi,
+ TalerMerchantInstanceCacheEviction,
+ TalerMerchantManagementCacheEviction,
+ TalerMerchantManagementHttpClient,
+} from "@gnu-taler/taler-util";
+import {
+ ComponentChildren,
+ FunctionComponent,
+ VNode,
+ createContext,
+ h,
+} from "preact";
+import { useContext, useEffect, useState } from "preact/hooks";
+import { BrowserFetchHttpLib } from "../index.browser.js";
+import {
+ APIClient,
+ ActiviyTracker,
+ MerchantLib,
+ Subscriber,
+} from "./activity.js";
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+export type MerchantContextType = {
+ url: URL;
+ config: TalerMerchantApi.VersionResponse;
+ lib: MerchantLib;
+ hints: VersionHint[];
+ onActivity: Subscriber<ObservabilityEvent>;
+ cancelRequest: (eventId: string) => void;
+ changeBackend: (url: URL) => void;
+};
+
+// FIXME: below
+// @ts-expect-error default value to undefined, should it be another thing?
+const MerchantContext = createContext<MerchantContextType>(undefined);
+
+export const useMerchantApiContext = (): MerchantContextType =>
+ useContext(MerchantContext);
+
+enum VersionHint {
+ NONE,
+}
+
+type Evictors = {
+ management?: CacheEvictor<
+ TalerMerchantManagementCacheEviction | TalerMerchantInstanceCacheEviction
+ >;
+};
+
+type ConfigResult<T> =
+ | undefined
+ | { type: "ok"; config: T; hints: VersionHint[] }
+ | ConfigResultFail<T>;
+
+export type ConfigResultFail<T> =
+ | { type: "incompatible"; result: T; supported: string }
+ | { type: "error"; error: TalerError };
+
+const CONFIG_FAIL_TRY_AGAIN_MS = 5000;
+
+export const MerchantApiProvider = ({
+ baseUrl,
+ children,
+ evictors = {},
+ frameOnError,
+}: {
+ baseUrl: URL;
+ evictors?: Evictors;
+ children: ComponentChildren;
+ frameOnError: FunctionComponent<{
+ state: ConfigResultFail<TalerMerchantApi.VersionResponse> | undefined;
+ }>;
+}): VNode => {
+ const [checked, setChecked] =
+ useState<ConfigResult<TalerMerchantApi.VersionResponse>>();
+
+ const [merchantEndpoint, changeMerchantEndpoint] = useState(baseUrl);
+
+ const { getRemoteConfig, VERSION, lib, cancelRequest, onActivity } =
+ buildMerchantApiClient(merchantEndpoint, evictors);
+
+ useEffect(() => {
+ let keepRetrying = true;
+ async function testConfig(): Promise<void> {
+ try {
+ const config = await getRemoteConfig();
+ if (LibtoolVersion.compare(VERSION, config.version)) {
+ setChecked({ type: "ok", config, hints: [] });
+ } else {
+ setChecked({
+ type: "incompatible",
+ result: config,
+ supported: VERSION,
+ });
+ }
+ } catch (error) {
+ if (error instanceof TalerError) {
+ if (keepRetrying) {
+ setTimeout(() => {
+ testConfig();
+ }, CONFIG_FAIL_TRY_AGAIN_MS);
+ }
+ setChecked({ type: "error", error });
+ } else {
+ setChecked({ type: "error", error: TalerError.fromException(error) });
+ }
+ }
+ }
+ testConfig();
+ return () => {
+ // on unload, stop retry
+ keepRetrying = false;
+ };
+ }, []);
+
+ if (!checked || checked.type !== "ok") {
+ return h(frameOnError, { state: checked }, []);
+ }
+
+ const value: MerchantContextType = {
+ url: merchantEndpoint,
+ config: checked.config,
+ onActivity: onActivity,
+ lib,
+ cancelRequest,
+ changeBackend: changeMerchantEndpoint,
+ hints: checked.hints,
+ };
+ return h(MerchantContext.Provider, {
+ value,
+ children,
+ });
+};
+
+function buildMerchantApiClient(
+ url: URL,
+ evictors: Evictors,
+): APIClient<MerchantLib, TalerMerchantApi.VersionResponse> {
+ const httpFetch = new BrowserFetchHttpLib({
+ enableThrottling: true,
+ requireTls: false,
+ });
+ const tracker = new ActiviyTracker<ObservabilityEvent>();
+
+ const httpLib = new ObservableHttpClientLibrary(httpFetch, {
+ observe(ev) {
+ tracker.notify(ev);
+ },
+ });
+
+ const instance = new TalerMerchantManagementHttpClient(
+ url.href,
+ httpLib,
+ evictors.management,
+ );
+ const authenticate = new TalerAuthenticationHttpClient(
+ instance.getAuthenticationAPI().href,
+ httpLib,
+ );
+
+ function getSubInstanceAPI(instanceId: string): MerchantLib {
+ const api = buildMerchantApiClient(
+ instance.getSubInstanceAPI(instanceId) as URL,
+ evictors,
+ );
+ return api.lib;
+ }
+
+ async function getRemoteConfig(): Promise<TalerMerchantApi.VersionResponse> {
+ const resp = await instance.getConfig();
+ if (resp.type === "fail") {
+ throw TalerError.fromUncheckedDetail(resp.detail);
+ }
+ return resp.body;
+ }
+
+ return {
+ getRemoteConfig,
+ VERSION: instance.PROTOCOL_VERSION,
+ lib: {
+ instance,
+ authenticate,
+ subInstanceApi: getSubInstanceAPI,
+ },
+ onActivity: tracker.subscribe,
+ cancelRequest: httpLib.cancelRequest,
+ };
+}
+
+export const MerchantApiProviderTesting = ({
+ children,
+ value,
+}: {
+ value: MerchantContextType;
+ children: ComponentChildren;
+}): VNode => {
+ return h(MerchantContext.Provider, {
+ value,
+ children,
+ });
+};
diff --git a/packages/web-util/src/context/navigation.ts b/packages/web-util/src/context/navigation.ts
new file mode 100644
index 000000000..c2f2bbbc1
--- /dev/null
+++ b/packages/web-util/src/context/navigation.ts
@@ -0,0 +1,114 @@
+/*
+ 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 { ComponentChildren, createContext, h, VNode } from "preact";
+import { useContext, useEffect, useState } from "preact/hooks";
+import {
+ AppLocation,
+ ObjectOf,
+ Location,
+ findMatch,
+ RouteDefinition,
+} from "../utils/route.js";
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+export type Type = {
+ path: string;
+ params: Record<string, string[]>;
+ navigateTo: (path: AppLocation) => void;
+ // addNavigationListener: (listener: (path: string, params: Record<string, string>) => void) => (() => void);
+};
+
+// @ts-expect-error should not be used without provider
+const Context = createContext<Type>(undefined);
+
+export const useNavigationContext = (): Type => useContext(Context);
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function useCurrentLocation<T extends ObjectOf<RouteDefinition<any>>>(
+ pagesMap: T,
+): Location<T> | undefined {
+ const pageList = Object.keys(pagesMap as object) as Array<keyof T>;
+ const { path, params } = useNavigationContext();
+
+ return findMatch(pagesMap, pageList, path, params);
+}
+
+function getPathAndParamsFromWindow(): {
+ path: string;
+ params: Record<string, string[]>;
+} {
+ const path =
+ typeof window !== "undefined" ? window.location.hash.substring(1) : "/";
+ const params: Record<string, string[]> = {};
+ if (typeof window !== "undefined") {
+ for (const [key, value] of new URLSearchParams(window.location.search)) {
+ if (!params[key]) {
+ params[key] = [];
+ }
+ params[key].push(value);
+ }
+ }
+ return { path, params };
+}
+
+const { path: initialPath, params: initialParams } =
+ getPathAndParamsFromWindow();
+
+// there is a possibility that if the browser does a redirection
+// (which doesn't go through navigatTo function) and that executed
+// too early (before addEventListener runs) it won't be taking
+// into account
+const PopStateEventType = "popstate";
+
+export const BrowserHashNavigationProvider = ({
+ children,
+}: {
+ children: ComponentChildren;
+}): VNode => {
+ const [{ path, params }, setState] = useState({
+ path: initialPath,
+ params: initialParams,
+ });
+ if (typeof window === "undefined") {
+ throw Error(
+ "Can't use BrowserHashNavigationProvider if there is no window object",
+ );
+ }
+ function navigateTo(path: string): void {
+ const { params } = getPathAndParamsFromWindow();
+ setState({ path, params });
+ window.location.href = path;
+ }
+
+ useEffect(() => {
+ function eventListener(): void {
+ setState(getPathAndParamsFromWindow());
+ }
+ window.addEventListener(PopStateEventType, eventListener);
+ return () => {
+ window.removeEventListener(PopStateEventType, eventListener);
+ };
+ }, []);
+ return h(Context.Provider, {
+ value: { path, params, navigateTo },
+ children,
+ });
+};
diff --git a/packages/demobank-ui/src/context/translation.ts b/packages/web-util/src/context/translation.ts
index 0a7e9429d..2725dc7e1 100644
--- a/packages/demobank-ui/src/context/translation.ts
+++ b/packages/web-util/src/context/translation.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -14,47 +14,54 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
import { i18n, setupI18n } from "@gnu-taler/taler-util";
import { ComponentChildren, createContext, h, VNode } from "preact";
import { useContext, useEffect } from "preact/hooks";
-import { hooks } from "@gnu-taler/web-util/lib/index.browser";
-import { strings } from "../i18n/strings.js";
+import { useLang } from "../hooks/index.js";
+import { Locale } from "date-fns";
+import {
+ es as esLocale,
+ enGB as enLocale,
+ fr as frLocale,
+ de as deLocale
+} from "date-fns/locale"
+
+export type InternationalizationAPI = typeof i18n;
interface Type {
lang: string;
supportedLang: { [id in keyof typeof supportedLang]: string };
changeLanguage: (l: string) => void;
- i18n: typeof i18n;
- isSaved: boolean;
+ i18n: InternationalizationAPI;
+ dateLocale: Locale,
+ completeness: { [id in keyof typeof supportedLang]: number }
}
const supportedLang = {
- es: "Español [es]",
- ja: "日本語 [ja]",
+ es: "Espanol [es]",
en: "English [en]",
- fr: "Français [fr]",
+ fr: "Francais [fr]",
de: "Deutsch [de]",
sv: "Svenska [sv]",
- it: "Italiano [it]",
- // ko: "한국어 [ko]",
- // ru: "Ру́сский язы́к [ru]",
- tr: "Türk [tr]",
- navigator: "Defined by navigator",
+ it: "Italiane [it]",
};
-const initial = {
+const initial: Type = {
lang: "en",
supportedLang,
changeLanguage: () => {
// do not change anything
},
i18n,
- isSaved: false,
+ dateLocale: enLocale,
+ completeness: {
+ de: 0,
+ en: 0,
+ es: 0,
+ fr: 0,
+ it: 0,
+ sv: 0,
+ }
};
const Context = createContext<Type>(initial);
@@ -62,6 +69,8 @@ interface Props {
initial?: string;
children: ComponentChildren;
forceLang?: string;
+ source: Record<string, any>;
+ completeness?: Record<string, number>;
}
// Outmost UI wrapper.
@@ -69,24 +78,40 @@ export const TranslationProvider = ({
initial,
children,
forceLang,
+ source,
+ completeness: completenessProp
}: Props): VNode => {
- const [lang, changeLanguage, isSaved] = hooks.useLang(initial);
+ const completeness = {
+ en: 100,
+ de: !completenessProp || !completenessProp["de"] ? 0 : completenessProp["de"],
+ es: !completenessProp || !completenessProp["es"] ? 0 : completenessProp["es"],
+ fr: !completenessProp || !completenessProp["fr"] ? 0 : completenessProp["fr"],
+ it: !completenessProp || !completenessProp["it"] ? 0 : completenessProp["it"],
+ sv: !completenessProp || !completenessProp["sv"] ? 0 : completenessProp["sv"],
+ }
+ const { value: lang, update: changeLanguage } = useLang(initial, completeness);
+
useEffect(() => {
if (forceLang) {
changeLanguage(forceLang);
}
});
useEffect(() => {
- setupI18n(lang, strings);
+ setupI18n(lang, source);
}, [lang]);
if (forceLang) {
- setupI18n(forceLang, strings);
+ setupI18n(forceLang, source);
} else {
- setupI18n(lang, strings);
+ setupI18n(lang, source);
}
+ const dateLocale = lang === "es" ? esLocale :
+ lang === "fr" ? frLocale :
+ lang === "de" ? deLocale :
+ enLocale;
+
return h(Context.Provider, {
- value: { lang, changeLanguage, supportedLang, i18n, isSaved },
+ value: { lang, changeLanguage, supportedLang, i18n, dateLocale, completeness },
children,
});
};
diff --git a/packages/web-util/src/context/wallet-integration.ts b/packages/web-util/src/context/wallet-integration.ts
new file mode 100644
index 000000000..e14988ed1
--- /dev/null
+++ b/packages/web-util/src/context/wallet-integration.ts
@@ -0,0 +1,83 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { stringifyTalerUri, TalerUri } from "@gnu-taler/taler-util";
+import { ComponentChildren, createContext, h, VNode } from "preact";
+import { useContext } from "preact/hooks";
+
+/**
+ * https://docs.taler.net/design-documents/039-taler-browser-integration.html
+ *
+ * @param uri
+ */
+function createHeadMetaTag(uri: TalerUri, onNotFound?: () => void) {
+ const meta = document.createElement("meta");
+ meta.setAttribute("name", "taler-uri");
+ meta.setAttribute("content", stringifyTalerUri(uri));
+
+ document.head.appendChild(meta);
+
+ let walletFound = false;
+ window.addEventListener("beforeunload", () => {
+ walletFound = true;
+ });
+ setTimeout(() => {
+ if (!walletFound && onNotFound) {
+ onNotFound();
+ }
+ }, 10); //very short timeout
+}
+interface Type {
+ /**
+ * Tell the active wallet that an action is found
+ *
+ * @param uri
+ * @returns
+ */
+ publishTalerAction: (uri: TalerUri, onNotFound?: () => void) => void;
+}
+
+// @ts-expect-error default value to undefined, should it be another thing?
+const Context = createContext<Type>(undefined);
+
+export const useTalerWalletIntegrationAPI = (): Type => useContext(Context);
+
+export const TalerWalletIntegrationBrowserProvider = ({
+ children,
+}: {
+ children: ComponentChildren;
+}): VNode => {
+ const value: Type = {
+ publishTalerAction: createHeadMetaTag,
+ };
+ return h(Context.Provider, {
+ value,
+ children,
+ });
+};
+
+export const TalerWalletIntegrationTestingProvider = ({
+ children,
+ value,
+}: {
+ children: ComponentChildren;
+ value: Type;
+}): VNode => {
+ return h(Context.Provider, {
+ value,
+ children,
+ });
+};
diff --git a/packages/web-util/src/declaration.d.ts b/packages/web-util/src/declaration.d.ts
new file mode 100644
index 000000000..c8ba3d576
--- /dev/null
+++ b/packages/web-util/src/declaration.d.ts
@@ -0,0 +1,35 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+declare module "*.css" {
+ const mapping: Record<string, string>;
+ export default mapping;
+}
+declare module "*.svg" {
+ const content: any;
+ export default content;
+}
+declare module "*.jpeg" {
+ const content: any;
+ export default content;
+}
+declare module "*.png" {
+ const content: any;
+ export default content;
+}
+
+declare const __VERSION__: string;
+declare const __GIT_HASH__: string;
diff --git a/packages/web-util/src/forms/Calendar.tsx b/packages/web-util/src/forms/Calendar.tsx
new file mode 100644
index 000000000..a0df639f3
--- /dev/null
+++ b/packages/web-util/src/forms/Calendar.tsx
@@ -0,0 +1,119 @@
+import { AbsoluteTime } from "@gnu-taler/taler-util"
+import { add as dateAdd, sub as dateSub, eachDayOfInterval, endOfMonth, endOfWeek, format, getMonth, getYear, isSameDay, isSameMonth, startOfDay, startOfMonth, startOfWeek } from "date-fns"
+import { VNode, h } from "preact"
+import { useState } from "preact/hooks"
+import { useTranslationContext } from "../index.browser.js"
+
+export function Calendar({ value, onChange }: { value: AbsoluteTime | undefined, onChange: (v: AbsoluteTime) => void }): VNode {
+ const today = startOfDay(new Date())
+ const selected = !value ? today : new Date(AbsoluteTime.toStampMs(value))
+ const [showingDate, setShowingDate] = useState(selected)
+ const month = getMonth(showingDate)
+ const year = getYear(showingDate)
+
+ const start = startOfWeek(startOfMonth(showingDate));
+ const end = endOfWeek(endOfMonth(showingDate));
+ const daysInMonth = eachDayOfInterval({ start, end });
+ const { i18n } = useTranslationContext()
+ const monthNames = [
+ i18n.str`January`,
+ i18n.str`February`,
+ i18n.str`March`,
+ i18n.str`April`,
+ i18n.str`May`,
+ i18n.str`June`,
+ i18n.str`July`,
+ i18n.str`August`,
+ i18n.str`September`,
+ i18n.str`October`,
+ i18n.str`November`,
+ i18n.str`December`,
+ ]
+ return <div class="text-center p-2">
+ <div class="flex items-center text-gray-900">
+ <button type="button" class="flex px-4 flex-none items-center justify-center p-1.5 text-gray-400 hover:text-gray-500 ring-2 round-sm"
+ onClick={() => {
+ setShowingDate(dateSub(showingDate, { years: 1 }))
+ }}>
+ <span class="sr-only">
+ {i18n.str`Previous year`}
+ </span>
+ <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
+ <path fill-rule="evenodd" d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z" clip-rule="evenodd" />
+ </svg>
+ </button>
+ <div class="flex-auto text-sm font-semibold">{year}</div>
+ <button type="button" class="flex px-4 flex-none items-center justify-center p-1.5 text-gray-400 hover:text-gray-500 ring-2 round-sm"
+ onClick={() => {
+ setShowingDate(dateAdd(showingDate, { years: 1 }))
+ }}>
+ <span class="sr-only">
+ {i18n.str`Next year`}
+ </span>
+ <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
+ <path fill-rule="evenodd" d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd" />
+ </svg>
+ </button>
+ </div>
+ <div class="mt-4 flex items-center text-gray-900">
+ <button type="button" class="flex px-4 flex-none items-center justify-center p-1.5 text-gray-400 hover:text-gray-500 ring-2 round-sm"
+ onClick={() => {
+ setShowingDate(dateSub(showingDate, { months: 1 }))
+ }}>
+ <span class="sr-only">
+ {i18n.str`Previous month`}
+ </span>
+ <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
+ <path fill-rule="evenodd" d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z" clip-rule="evenodd" />
+ </svg>
+ </button>
+ <div class="flex-auto text-sm font-semibold">{monthNames[month]}</div>
+ <button type="button" class="flex px-4 flex-none items-center justify-center p-1.5 text-gray-400 hover:text-gray-500 ring-2 rounded-sm "
+ onClick={() => {
+ setShowingDate(dateAdd(showingDate, { months: 1 }))
+ }}>
+ <span class="sr-only">
+ {i18n.str`Next month`}
+ </span>
+ <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
+ <path fill-rule="evenodd" d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd" />
+ </svg>
+ </button>
+ </div>
+ <div class="mt-6 grid grid-cols-7 text-xs leading-6 text-gray-500">
+ <div>M</div>
+ <div>T</div>
+ <div>W</div>
+ <div>T</div>
+ <div>F</div>
+ <div>S</div>
+ <div>S</div>
+ </div>
+ <div class="isolate mt-2">
+ <div class="grid grid-cols-7 gap-px rounded-lg bg-gray-200 text-sm shadow ring-1 ring-gray-200">
+ {daysInMonth.map(current => (
+ <button type="button"
+ data-month={isSameMonth(current, showingDate)}
+ data-today={isSameDay(current, today)}
+ data-selected={isSameDay(current, selected)}
+ onClick={() => {
+ onChange(AbsoluteTime.fromStampMs(current.getTime()))
+ }}
+ class="text-gray-400 hover:bg-gray-700 focus:z-10 py-1.5
+ data-[month=false]:bg-gray-100 data-[month=true]:bg-white
+ data-[today=true]:font-semibold
+ data-[month=true]:text-gray-900
+ data-[today=true]:bg-red-300 data-[today=true]:hover:bg-red-200
+ data-[month=true]:hover:bg-gray-200
+ data-[selected=true]:!bg-blue-400 data-[selected=true]:hover:!bg-blue-300 ">
+ <time dateTime={format(current, "yyyy-MM-dd")}
+ class="mx-auto flex h-7 w-7 py-4 px-5 sm:px-8 items-center justify-center rounded-full">
+ {format(current, "dd")}
+ </time>
+ </button>
+ ))}
+ </div>
+ {daysInMonth.length < 40 ? <div class="w-7 h-7 m-1.5" /> : undefined}
+ </div>
+ </div>
+}
diff --git a/packages/web-util/src/forms/Caption.tsx b/packages/web-util/src/forms/Caption.tsx
new file mode 100644
index 000000000..8facddec3
--- /dev/null
+++ b/packages/web-util/src/forms/Caption.tsx
@@ -0,0 +1,32 @@
+import { TranslatedString } from "@gnu-taler/taler-util";
+import { VNode, h } from "preact";
+import {
+ LabelWithTooltipMaybeRequired
+} from "./InputLine.js";
+
+interface Props {
+ label: TranslatedString;
+ tooltip?: TranslatedString;
+ help?: TranslatedString;
+ before?: VNode;
+ after?: VNode;
+}
+
+export function Caption({ before, after, label, tooltip, help }: Props): VNode {
+ return (
+ <div class="sm:col-span-6 flex">
+ {before !== undefined && (
+ <span class="pointer-events-none flex items-center pr-2">{before}</span>
+ )}
+ <LabelWithTooltipMaybeRequired label={label} tooltip={tooltip} />
+ {after !== undefined && (
+ <span class="pointer-events-none flex items-center pl-2">{after}</span>
+ )}
+ {help && (
+ <p class="mt-2 text-sm text-gray-500" id="email-description">
+ {help}
+ </p>
+ )}
+ </div>
+ );
+}
diff --git a/packages/web-util/src/forms/DefaultForm.tsx b/packages/web-util/src/forms/DefaultForm.tsx
new file mode 100644
index 000000000..1155401f5
--- /dev/null
+++ b/packages/web-util/src/forms/DefaultForm.tsx
@@ -0,0 +1,83 @@
+import { Fragment, h } from "preact";
+import { FormProvider, FormProviderProps, FormState } from "./FormProvider.js";
+import { RenderAllFieldsByUiConfig, UIFormField } from "./forms.js";
+import { TranslatedString } from "@gnu-taler/taler-util";
+
+/**
+ * Flexible form uses a DoubleColumForm for design
+ * and may have a dynamic properties defined by
+ * behavior function.
+ */
+export interface FlexibleForm<T extends object> {
+ design: DoubleColumnForm;
+ behavior?: (form: Partial<T>) => FormState<T>;
+}
+
+/**
+ * Double column form
+ *
+ * Form with sections, every sections have a title and may
+ * have a description.
+ * Every sections contain a set of fields.
+ */
+export type DoubleColumnForm = Array<DoubleColumnFormSection | undefined>;
+
+export type DoubleColumnFormSection = {
+ title: TranslatedString;
+ description?: TranslatedString;
+ fields: UIFormField[];
+};
+
+/**
+ * Form Provider implementation that use FlexibleForm
+ * to defined behavior and fields.
+ */
+export function DefaultForm<T extends object>({
+ initial,
+ onUpdate,
+ form,
+ onSubmit,
+ children,
+ readOnly,
+}: Omit<FormProviderProps<T>, "computeFormState"> & { form: FlexibleForm<T> }) {
+ return (
+ <FormProvider
+ initial={initial}
+ onUpdate={onUpdate}
+ onSubmit={onSubmit}
+ readOnly={readOnly}
+ computeFormState={form.behavior}
+ >
+ <div class="space-y-10 divide-y -mt-5 divide-gray-900/10">
+ {form.design.map((section, i) => {
+ if (!section) return <Fragment />;
+ return (
+ <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-5 md:grid-cols-3">
+ <div class="px-4 sm:px-0">
+ <h2 class="text-base font-semibold leading-7 text-gray-900">
+ {section.title}
+ </h2>
+ {section.description && (
+ <p class="mt-1 text-sm leading-6 text-gray-600">
+ {section.description}
+ </p>
+ )}
+ </div>
+ <div class="bg-white shadow-sm ring-1 ring-gray-900/5 rounded-md md:col-span-2">
+ <div class="p-3">
+ <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
+ <RenderAllFieldsByUiConfig
+ key={i}
+ fields={section.fields}
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+ })}
+ </div>
+ {children}
+ </FormProvider>
+ );
+}
diff --git a/packages/web-util/src/forms/Dialog.tsx b/packages/web-util/src/forms/Dialog.tsx
new file mode 100644
index 000000000..7b41fe487
--- /dev/null
+++ b/packages/web-util/src/forms/Dialog.tsx
@@ -0,0 +1,15 @@
+import { ComponentChildren, VNode, h } from "preact";
+
+export function Dialog({ children, onClose }: { onClose?: () => void; children: ComponentChildren }): VNode {
+ return <div class="relative z-10" aria-labelledby="modal-title" role="dialog" aria-modal="true" onClick={onClose}>
+ <div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"></div>
+
+ <div class="fixed inset-0 z-10 w-screen overflow-y-auto">
+ <div class="flex min-h-full items-center justify-center p-4 text-center ">
+ <div class="relative transform overflow-hidden rounded-lg bg-white p-1 text-left shadow-xl transition-all" onClick={(e) => e.stopPropagation()}>
+ {children}
+ </div>
+ </div>
+ </div>
+ </div>
+}
diff --git a/packages/web-util/src/forms/FormProvider.tsx b/packages/web-util/src/forms/FormProvider.tsx
new file mode 100644
index 000000000..f4616525b
--- /dev/null
+++ b/packages/web-util/src/forms/FormProvider.tsx
@@ -0,0 +1,148 @@
+import {
+ AbsoluteTime,
+ AmountJson,
+ TranslatedString,
+} from "@gnu-taler/taler-util";
+import { ComponentChildren, VNode, createContext, h } from "preact";
+import {
+ MutableRef,
+ useState
+} from "preact/hooks";
+
+export interface FormType<T extends object> {
+ value: MutableRef<Partial<T>>;
+ initial?: Partial<T>;
+ readOnly?: boolean;
+ onUpdate?: (v: Partial<T>) => void;
+ computeFormState?: (v: Partial<T>) => FormState<T>;
+}
+
+//@ts-ignore
+export const FormContext = createContext<FormType<any>>({});
+
+/**
+ * Map of {[field]:FieldUIOptions}
+ * for every field of type
+ * - any native (string, number, etc...)
+ * - absoluteTime
+ * - amountJson
+ *
+ * except for:
+ * - object => recurse into
+ * - array => behavior result and element field
+ */
+export type FormState<T extends object | undefined> = {
+ [field in keyof T]?: T[field] extends AbsoluteTime
+ ? FieldUIOptions
+ : T[field] extends AmountJson
+ ? FieldUIOptions
+ : T[field] extends Array<infer P extends object>
+ ? InputArrayFieldState<P>
+ : T[field] extends (object | undefined)
+ ? FormState<T[field]>
+ : FieldUIOptions;
+};
+
+/**
+ * Properties that can be defined by design or by computing state
+ */
+export type FieldUIOptions = {
+ /* text to be shown next to the field */
+ error?: TranslatedString;
+ /* instruction to be shown in the field */
+ placeholder?: TranslatedString;
+ /* long text help to be shown on demand */
+ tooltip?: TranslatedString;
+ /* short text to be shown next to the field*/
+
+ help?: TranslatedString;
+ /* should show as disabled and readonly */
+ disabled?: boolean;
+ /* should not show */
+ hidden?: boolean;
+
+ /* show a mark as required*/
+ required?: boolean;
+}
+
+/**
+ * properties only to be defined on design time
+ */
+export interface UIFormProps<T extends object, K extends keyof T> extends FieldUIOptions {
+
+ // property name of the object
+ name: K;
+
+ // label if the field
+ label: TranslatedString;
+ before?: Addon;
+ after?: Addon;
+
+ // converter to string and back
+ converter?: StringConverter<T[K]>;
+}
+
+export interface IconAddon {
+ type: "icon";
+ icon: VNode;
+}
+export interface ButtonAddon {
+ type: "button";
+ onClick: () => void;
+ children: ComponentChildren;
+}
+export interface TextAddon {
+ type: "text";
+ text: TranslatedString;
+}
+export type Addon = IconAddon | ButtonAddon | TextAddon;
+
+export interface StringConverter<T> {
+ toStringUI: (v?: T) => string;
+ fromStringUI: (v?: string) => T;
+}
+
+export interface InputArrayFieldState<P extends object> extends FieldUIOptions {
+ elements?: FormState<P>[];
+}
+
+export type FormProviderProps<T extends object> = Omit<FormType<T>, "value"> & {
+ onSubmit?: (v: Partial<T>, s: FormState<T> | undefined) => void;
+ children?: ComponentChildren;
+}
+
+export function FormProvider<T extends object>({
+ children,
+ initial,
+ onUpdate: notify,
+ onSubmit,
+ computeFormState,
+ readOnly,
+}: FormProviderProps<T>): VNode {
+
+ const [state, setState] = useState<Partial<T>>(initial ?? {});
+ const value = { current: state };
+ const onUpdate = (v: typeof state) => {
+ setState(v);
+ if (notify) notify(v);
+ };
+ return (
+ <FormContext.Provider
+ value={{ initial, value, onUpdate, computeFormState, readOnly }}
+ >
+ <form
+ onSubmit={(e) => {
+ e.preventDefault();
+ //@ts-ignore
+ if (onSubmit)
+ onSubmit(
+ value.current,
+ !computeFormState ? undefined : computeFormState(value.current),
+ );
+ }}
+ >
+ {children}
+ </form>
+ </FormContext.Provider>
+ );
+}
diff --git a/packages/web-util/src/forms/Group.tsx b/packages/web-util/src/forms/Group.tsx
new file mode 100644
index 000000000..0645f6d97
--- /dev/null
+++ b/packages/web-util/src/forms/Group.tsx
@@ -0,0 +1,41 @@
+import { TranslatedString } from "@gnu-taler/taler-util";
+import { VNode, h } from "preact";
+import { LabelWithTooltipMaybeRequired } from "./InputLine.js";
+import { RenderAllFieldsByUiConfig, UIFormField } from "./forms.js";
+
+interface Props {
+ before?: TranslatedString;
+ after?: TranslatedString;
+ tooltipBefore?: TranslatedString;
+ tooltipAfter?: TranslatedString;
+ fields: UIFormField[];
+}
+
+export function Group({
+ before,
+ after,
+ tooltipAfter,
+ tooltipBefore,
+ fields,
+}: Props): VNode {
+ return (
+ <div class="sm:col-span-6 p-4 rounded-lg border-r-2 border-2 bg-gray-50">
+ <div class="pb-4">
+ {before && (
+ <LabelWithTooltipMaybeRequired
+ label={before}
+ tooltip={tooltipBefore}
+ />
+ )}
+ </div>
+ <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-2 sm:grid-cols-6">
+ <RenderAllFieldsByUiConfig fields={fields} />
+ </div>
+ <div class="pt-4">
+ {after && (
+ <LabelWithTooltipMaybeRequired label={after} tooltip={tooltipAfter} />
+ )}
+ </div>
+ </div>
+ );
+}
diff --git a/packages/web-util/src/forms/InputAbsoluteTime.stories.tsx b/packages/web-util/src/forms/InputAbsoluteTime.stories.tsx
new file mode 100644
index 000000000..6245cf27c
--- /dev/null
+++ b/packages/web-util/src/forms/InputAbsoluteTime.stories.tsx
@@ -0,0 +1,60 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
+import {
+ FlexibleForm,
+ DefaultForm as TestedComponent,
+} from "./DefaultForm.js";
+
+export default {
+ title: "Input Absolute Time",
+};
+
+export namespace Simplest {
+ export interface Form {
+ comment: string;
+ }
+}
+
+type TargetObject = {
+ today: AbsoluteTime;
+}
+const initial: TargetObject = {
+ today: AbsoluteTime.now()
+}
+
+const form: FlexibleForm<TargetObject> = {
+ design: [{
+ title: "this is a simple form" as TranslatedString,
+ fields: [{
+ type: "absoluteTime",
+ props: {
+ label: "label of the field" as TranslatedString,
+ name: "today",
+ pattern: "dd/MM/yyyy HH:mm"
+ },
+ }]
+ }]
+}
+
+export const SimpleComment = tests.createExample(TestedComponent, { initial, form });
diff --git a/packages/web-util/src/forms/InputAbsoluteTime.tsx b/packages/web-util/src/forms/InputAbsoluteTime.tsx
new file mode 100644
index 000000000..ee18e5592
--- /dev/null
+++ b/packages/web-util/src/forms/InputAbsoluteTime.tsx
@@ -0,0 +1,77 @@
+import { AbsoluteTime } from "@gnu-taler/taler-util";
+import { InputLine } from "./InputLine.js";
+import { Fragment, VNode, h } from "preact";
+import { format, parse } from "date-fns";
+import { Dialog } from "./Dialog.js";
+import { Calendar } from "./Calendar.js";
+import { useState } from "preact/hooks";
+import { useField } from "./useField.js";
+import { UIFormProps } from "./FormProvider.js";
+import { TimePicker } from "./TimePicker.js";
+
+export function InputAbsoluteTime<T extends object, K extends keyof T>(
+ props: { pattern?: string } & UIFormProps<T, K>,
+): VNode {
+ const pattern = props.pattern ?? "dd/MM/yyyy";
+ const [open, setOpen] = useState(false)
+ const { value, onChange } = useField<T, K>(props.name);
+ return (
+ <Fragment>
+
+ <InputLine<T, K>
+ type="text"
+ after={{
+ type: "button",
+ onClick: () => {
+ setOpen(true)
+ },
+ // icon: <CalendarIcon class="h-6 w-6" />,
+ children: (
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5" />
+ </svg>)
+ }}
+ converter={{
+ //@ts-ignore
+ fromStringUI: (v): AbsoluteTime | undefined => {
+ if (!v) return undefined;
+ try {
+ const t_ms = parse(v, pattern, Date.now()).getTime();
+ return AbsoluteTime.fromMilliseconds(t_ms);
+ } catch (e) {
+ return undefined;
+ }
+ },
+ //@ts-ignore
+ toStringUI: (v: AbsoluteTime | undefined) => {
+ return !v || !v.t_ms
+ ? undefined
+ : v.t_ms === "never"
+ ? "never"
+ : format(v.t_ms, pattern);
+ },
+ }}
+ {...props}
+ />
+ {open &&
+ <Dialog onClose={() => setOpen(false)}>
+ <Calendar value={value as AbsoluteTime ?? AbsoluteTime.now()}
+ onChange={(v) => {
+ onChange(v as any)
+ setOpen(false)
+ }} />
+ </Dialog>
+ }
+ {/* {open &&
+ <Dialog onClose={() => setOpen(false)} >
+ <TimePicker value={value as AbsoluteTime ?? AbsoluteTime.now()}
+ onChange={(v) => {
+ onChange(v as any)
+ }}
+ onConfirm={() => {
+ setOpen(false)
+ }} />
+ </Dialog>} */}
+ </Fragment>
+ );
+}
diff --git a/packages/web-util/src/forms/InputAmount.stories.tsx b/packages/web-util/src/forms/InputAmount.stories.tsx
new file mode 100644
index 000000000..c9f12a437
--- /dev/null
+++ b/packages/web-util/src/forms/InputAmount.stories.tsx
@@ -0,0 +1,59 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { AmountJson, Amounts, TranslatedString } from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
+import {
+ FlexibleForm,
+ DefaultForm as TestedComponent,
+} from "./DefaultForm.js";
+
+export default {
+ title: "Input Amount",
+};
+
+export namespace Simplest {
+ export interface Form {
+ comment: string;
+ }
+}
+
+type TargetObject = {
+ amount: AmountJson;
+}
+const initial: TargetObject = {
+ amount: Amounts.parseOrThrow("USD:10")
+}
+
+const form: FlexibleForm<TargetObject> = {
+ design: [{
+ title: "this is a simple form" as TranslatedString,
+ fields: [{
+ type: "amount",
+ props: {
+ label: "label of the field" as TranslatedString,
+ name: "amount",
+ },
+ }]
+ }]
+}
+
+export const SimpleComment = tests.createExample(TestedComponent, { initial, form });
diff --git a/packages/web-util/src/forms/InputAmount.tsx b/packages/web-util/src/forms/InputAmount.tsx
new file mode 100644
index 000000000..7a8c08f76
--- /dev/null
+++ b/packages/web-util/src/forms/InputAmount.tsx
@@ -0,0 +1,36 @@
+import { AmountJson, Amounts, TranslatedString } from "@gnu-taler/taler-util";
+import { VNode, h } from "preact";
+import { UIFormProps } from "./FormProvider.js";
+import { InputLine } from "./InputLine.js";
+import { useField } from "./useField.js";
+
+export function InputAmount<T extends object, K extends keyof T>(
+ props: { currency?: string } & UIFormProps<T, K>,
+): VNode {
+ const { value } = useField<T, K>(props.name);
+ const currency =
+ !value || !(value as any).currency
+ ? props.currency
+ : (value as any).currency;
+ return (
+ <InputLine<T, K>
+ type="text"
+ before={{
+ type: "text",
+ text: currency as TranslatedString,
+ }}
+ converter={{
+ //@ts-ignore
+ fromStringUI: (v): AmountJson => {
+
+ return Amounts.parse(`${currency}:${v}`) ?? Amounts.zeroOfCurrency(currency);
+ },
+ //@ts-ignore
+ toStringUI: (v: AmountJson) => {
+ return v === undefined ? "" : Amounts.stringifyValue(v);
+ },
+ }}
+ {...props}
+ />
+ );
+}
diff --git a/packages/web-util/src/forms/InputArray.stories.tsx b/packages/web-util/src/forms/InputArray.stories.tsx
new file mode 100644
index 000000000..8dbd3ff07
--- /dev/null
+++ b/packages/web-util/src/forms/InputArray.stories.tsx
@@ -0,0 +1,79 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { TranslatedString } from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
+import {
+ FlexibleForm,
+ DefaultForm as TestedComponent,
+} from "./DefaultForm.js";
+
+export default {
+ title: "Input Array",
+};
+
+export namespace Simplest {
+ export interface Form {
+ comment: string;
+ }
+}
+
+type TargetObject = {
+ people: {
+ name: string;
+ age: number;
+ }[];
+}
+const initial: TargetObject = {
+ people: [{
+ name: "me",
+ age: 17,
+ }]
+}
+
+const form: FlexibleForm<TargetObject> = {
+ design: [{
+ title: "this is a simple form" as TranslatedString,
+ fields: [{
+ type: "array",
+ props: {
+ label: "People" as TranslatedString,
+ name: "comment",
+ fields: [{
+ type: "text",
+ props: {
+ label: "the name" as TranslatedString,
+ name: "name",
+ }
+ }, {
+ type: "integer",
+ props: {
+ label: "the age" as TranslatedString,
+ name: "age",
+ }
+ }],
+ labelField: "name"
+ },
+ }]
+ }]
+}
+
+export const SimpleComment = tests.createExample(TestedComponent, { initial, form });
diff --git a/packages/web-util/src/forms/InputArray.tsx b/packages/web-util/src/forms/InputArray.tsx
new file mode 100644
index 000000000..7d9a1b378
--- /dev/null
+++ b/packages/web-util/src/forms/InputArray.tsx
@@ -0,0 +1,186 @@
+import { TranslatedString } from "@gnu-taler/taler-util";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { FormProvider, UIFormProps } from "./FormProvider.js";
+import { LabelWithTooltipMaybeRequired } from "./InputLine.js";
+import { RenderAllFieldsByUiConfig, UIFormField } from "./forms.js";
+import { useField } from "./useField.js";
+
+function Option({
+ label,
+ disabled,
+ isFirst,
+ isLast,
+ isSelected,
+ onClick,
+}: {
+ label: TranslatedString;
+ isFirst?: boolean;
+ isLast?: boolean;
+ isSelected?: boolean;
+ disabled?: boolean;
+ onClick: () => void;
+}): VNode {
+ let clazz = "relative flex border p-4 focus:outline-none disabled:text-grey";
+ if (isFirst) {
+ clazz += " rounded-tl-md rounded-tr-md ";
+ }
+ if (isLast) {
+ clazz += " rounded-bl-md rounded-br-md ";
+ }
+ if (isSelected) {
+ clazz += " z-10 border-indigo-200 bg-indigo-50 ";
+ } else {
+ clazz += " border-gray-200";
+ }
+ if (disabled) {
+ clazz +=
+ " cursor-not-allowed bg-gray-50 text-gray-500 ring-gray-200 text-gray";
+ } else {
+ clazz += " cursor-pointer";
+ }
+ return (
+ <label class={clazz}>
+ <input
+ type="radio"
+ name="privacy-setting"
+ checked={isSelected}
+ disabled={disabled}
+ onClick={onClick}
+ class="mt-0.5 h-4 w-4 shrink-0 text-indigo-600 disabled:cursor-not-allowed disabled:bg-gray-50 disabled:text-gray-500 disabled:ring-gray-200 focus:ring-indigo-600"
+ aria-labelledby="privacy-setting-0-label"
+ aria-describedby="privacy-setting-0-description"
+ />
+ <span class="ml-3 flex flex-col">
+ <span
+ id="privacy-setting-0-label"
+ disabled
+ class="block text-sm font-medium"
+ >
+ {label}
+ </span>
+ {/* <!-- Checked: "text-indigo-700", Not Checked: "text-gray-500" --> */}
+ {/* <span
+ id="privacy-setting-0-description"
+ class="block text-sm"
+ >
+ This project would be available to anyone who has the link
+ </span> */}
+ </span>
+ </label>
+ );
+}
+
+export function InputArray<T extends object, K extends keyof T>(
+ props: {
+ fields: UIFormField[];
+ labelField: string;
+ } & UIFormProps<T, K>,
+): VNode {
+ const { fields, labelField, name, label, required, tooltip } = props;
+ const { value, onChange, state } = useField<T, K>(name);
+ const list = (value ?? []) as Array<Record<string, string | undefined>>;
+ const [selectedIndex, setSelected] = useState<number | undefined>(undefined);
+ const selected =
+ selectedIndex === undefined ? undefined : list[selectedIndex];
+
+ return (
+ <div class="sm:col-span-6">
+ <LabelWithTooltipMaybeRequired
+ label={label}
+ required={required}
+ tooltip={tooltip}
+ />
+
+ <div class="-space-y-px rounded-md bg-white ">
+ {list.map((v, idx) => {
+ return (
+ <Option
+ label={v[labelField] as TranslatedString}
+ isSelected={selectedIndex === idx}
+ isLast={idx === list.length - 1}
+ disabled={selectedIndex !== undefined && selectedIndex !== idx}
+ isFirst={idx === 0}
+ onClick={() => {
+ setSelected(selectedIndex === idx ? undefined : idx);
+ }}
+ />
+ );
+ })}
+ {!state.disabled &&
+ <div class="pt-2">
+ <Option
+ label={"Add..." as TranslatedString}
+ isSelected={selectedIndex === list.length}
+ isLast
+ isFirst
+ disabled={
+ selectedIndex !== undefined && selectedIndex !== list.length
+ }
+ onClick={() => {
+ setSelected(
+ selectedIndex === list.length ? undefined : list.length,
+ );
+ }}
+ />
+ </div>
+ }
+ </div>
+ {selectedIndex !== undefined && (
+ /**
+ * This form provider act as a substate of the parent form
+ * Consider creating an InnerFormProvider since not every feature is expected
+ */
+ <FormProvider
+ initial={selected}
+ readOnly={state.disabled}
+ computeFormState={(v) => {
+ // current state is ignored
+ // the state is defined by the parent form
+
+ // elements should be present in the state object since this is expected to be an array
+ //@ts-ignore
+ return state.elements[selectedIndex];
+ }}
+ onSubmit={(v) => {
+ const newValue = [...list];
+ newValue.splice(selectedIndex, 1, v);
+ onChange(newValue as T[K]);
+ setSelected(undefined);
+ }}
+ onUpdate={(v) => {
+ const newValue = [...list];
+ newValue.splice(selectedIndex, 1, v);
+ onChange(newValue as T[K]);
+ }}
+ >
+ <div class="px-4 py-6">
+ <div class="grid grid-cols-1 gap-y-8 ">
+ <RenderAllFieldsByUiConfig fields={fields} />
+ </div>
+ </div>
+ </FormProvider>
+ )}
+ {selectedIndex !== undefined && (
+ <div class="flex items-center pt-3">
+ <div class="flex-auto">
+ {selected !== undefined && (
+ <button
+ type="button"
+ onClick={() => {
+ const newValue = [...list];
+ newValue.splice(selectedIndex, 1);
+ onChange(newValue as T[K]);
+ setSelected(undefined);
+ }}
+ class="block rounded-md bg-red-600 px-3 py-2 text-center text-sm text-white shadow-sm hover:bg-red-500 "
+ >
+ Remove
+ </button>
+ )}
+ </div>
+ </div>
+ )}
+ </div>
+ );
+}
diff --git a/packages/web-util/src/forms/InputChoiceHorizontal.stories.tsx b/packages/web-util/src/forms/InputChoiceHorizontal.stories.tsx
new file mode 100644
index 000000000..b950d3d02
--- /dev/null
+++ b/packages/web-util/src/forms/InputChoiceHorizontal.stories.tsx
@@ -0,0 +1,69 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { TranslatedString } from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
+import {
+ FlexibleForm,
+ DefaultForm as TestedComponent,
+} from "./DefaultForm.js";
+
+export default {
+ title: "Input Choice Horizontal",
+};
+
+export namespace Simplest {
+ export interface Form {
+ comment: string;
+ }
+}
+
+type TargetObject = {
+ comment: string;
+}
+const initial: TargetObject = {
+ comment: "0"
+}
+
+const form: FlexibleForm<TargetObject> = {
+ design: [{
+ title: "this is a simple form" as TranslatedString,
+ fields: [{
+ type: "choiceHorizontal",
+ props: {
+ label: "label of the field" as TranslatedString,
+ name: "comment",
+ choices: [{
+ label: "first choice" as TranslatedString,
+ value: "1"
+ }, {
+ label: "second choice" as TranslatedString,
+ value: "2"
+ }, {
+ label: "third choice" as TranslatedString,
+ value: "3"
+ },],
+ },
+ }]
+ }]
+}
+
+export const SimpleComment = tests.createExample(TestedComponent, { initial, form });
diff --git a/packages/web-util/src/forms/InputChoiceHorizontal.tsx b/packages/web-util/src/forms/InputChoiceHorizontal.tsx
new file mode 100644
index 000000000..778b73c75
--- /dev/null
+++ b/packages/web-util/src/forms/InputChoiceHorizontal.tsx
@@ -0,0 +1,87 @@
+import { TranslatedString } from "@gnu-taler/taler-util";
+import { Fragment, VNode, h } from "preact";
+import { UIFormProps } from "./FormProvider.js";
+import { LabelWithTooltipMaybeRequired } from "./InputLine.js";
+import { useField } from "./useField.js";
+
+export interface ChoiceH<V> {
+ label: TranslatedString;
+ value: V;
+}
+
+export function InputChoiceHorizontal<T extends object, K extends keyof T>(
+ props: {
+ choices: ChoiceH<T[K]>[];
+ } & UIFormProps<T, K>,
+): VNode {
+ const {
+ choices,
+ name,
+ label,
+ tooltip,
+ help,
+ placeholder,
+ required,
+ before,
+ after,
+ converter,
+ } = props;
+ const { value, onChange, state, isDirty } = useField<T, K>(name);
+ if (state.hidden) {
+ return <Fragment />;
+ }
+
+ return (
+ <div class="sm:col-span-6">
+ <LabelWithTooltipMaybeRequired
+ label={label}
+ required={required}
+ tooltip={tooltip}
+ />
+ <fieldset class="mt-2">
+ <div class="isolate inline-flex rounded-md shadow-sm">
+ {choices.map((choice, idx) => {
+ const isFirst = idx === 0;
+ const isLast = idx === choices.length - 1;
+ let clazz =
+ "relative inline-flex items-center px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 focus:z-10";
+ if (choice.value === value) {
+ clazz +=
+ " text-white bg-indigo-600 hover:bg-indigo-500 ring-2 ring-indigo-600 hover:ring-indigo-500";
+ } else {
+ clazz += " hover:bg-gray-100 border-gray-300";
+ }
+ if (isFirst) {
+ clazz += " rounded-l-md";
+ } else {
+ clazz += " -ml-px";
+ }
+ if (isLast) {
+ clazz += " rounded-r-md";
+ }
+ return (
+ <button
+ type="button"
+ disabled={state.disabled}
+ label={choice.label}
+ class={clazz}
+ onClick={(e) => {
+ onChange(
+ (value === choice.value ? undefined : choice.value) as T[K],
+ );
+ }}
+ >
+ {choice.label}
+ </button>
+ );
+ })}
+ </div>
+ </fieldset>
+ {help && (
+ <p class="mt-2 text-sm text-gray-500" id="email-description">
+ {help}
+ </p>
+ )}
+ </div>
+ );
+}
diff --git a/packages/web-util/src/forms/InputChoiceStacked.stories.tsx b/packages/web-util/src/forms/InputChoiceStacked.stories.tsx
new file mode 100644
index 000000000..ed5170d17
--- /dev/null
+++ b/packages/web-util/src/forms/InputChoiceStacked.stories.tsx
@@ -0,0 +1,69 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { TranslatedString } from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
+import {
+ FlexibleForm,
+ DefaultForm as TestedComponent,
+} from "./DefaultForm.js";
+
+export default {
+ title: "Input Choice Stacked",
+};
+
+export namespace Simplest {
+ export interface Form {
+ comment: string;
+ }
+}
+
+type TargetObject = {
+ comment: string;
+}
+const initial: TargetObject = {
+ comment: "some initial comment"
+}
+
+const form: FlexibleForm<TargetObject> = {
+ design: [{
+ title: "this is a simple form" as TranslatedString,
+ fields: [{
+ type: "choiceStacked",
+ props: {
+ label: "label of the field" as TranslatedString,
+ name: "comment",
+ choices: [{
+ label: "first choice" as TranslatedString,
+ value: "1"
+ }, {
+ label: "second choice" as TranslatedString,
+ value: "2"
+ }, {
+ label: "third choice" as TranslatedString,
+ value: "3"
+ },],
+ },
+ }]
+ }]
+}
+
+export const SimpleComment = tests.createExample(TestedComponent, { initial, form });
diff --git a/packages/web-util/src/forms/InputChoiceStacked.tsx b/packages/web-util/src/forms/InputChoiceStacked.tsx
new file mode 100644
index 000000000..234bb2255
--- /dev/null
+++ b/packages/web-util/src/forms/InputChoiceStacked.tsx
@@ -0,0 +1,113 @@
+import { TranslatedString } from "@gnu-taler/taler-util";
+import { Fragment, VNode, h } from "preact";
+import { UIFormProps } from "./FormProvider.js";
+import { LabelWithTooltipMaybeRequired } from "./InputLine.js";
+import { useField } from "./useField.js";
+
+export interface ChoiceS<V> {
+ label: TranslatedString;
+ description?: TranslatedString;
+ value: V;
+}
+
+export function InputChoiceStacked<T extends object, K extends keyof T>(
+ props: {
+ choices: ChoiceS<T[K]>[];
+ } & UIFormProps<T, K>,
+): VNode {
+ const {
+ choices,
+ name,
+ label,
+ tooltip,
+ help,
+ placeholder,
+ required,
+ before,
+ after,
+ converter,
+ } = props;
+ const { value, onChange, state, isDirty } = useField<T, K>(name);
+ if (state.hidden) {
+ return <Fragment />;
+ }
+
+ return (
+ <div class="sm:col-span-6">
+ <LabelWithTooltipMaybeRequired
+ label={label}
+ required={required}
+ tooltip={tooltip}
+ />
+ <fieldset class="mt-2">
+ <div class="space-y-4">
+ {choices.map((choice) => {
+ // const currentValue = !converter
+ // ? choice.value
+ // : converter.fromStringUI(choice.value) ?? "";
+
+ let clazz =
+ "border relative block cursor-pointer rounded-lg bg-white px-6 py-4 shadow-sm focus:outline-none sm:flex sm:justify-between";
+ if (choice.value === value) {
+ clazz +=
+ " border-transparent border-indigo-600 ring-2 ring-indigo-600";
+ } else {
+ clazz += " border-gray-300";
+ }
+
+ return (
+ <label class={clazz}>
+ <input
+ type="radio"
+ name="server-size"
+ // defaultValue={choice.value}
+ disabled={state.disabled}
+ value={
+ (!converter
+ ? (choice.value as string)
+ : converter?.toStringUI(choice.value)) ?? ""
+ }
+ onClick={(e) => {
+ onChange(
+ (value === choice.value
+ ? undefined
+ : choice.value) as T[K],
+ );
+ }}
+ class="sr-only"
+ aria-labelledby="server-size-0-label"
+ aria-describedby="server-size-0-description-0 server-size-0-description-1"
+ />
+ <span class="flex items-center">
+ <span class="flex flex-col text-sm">
+ <span
+ id="server-size-0-label"
+ class="font-medium text-gray-900"
+ >
+ {choice.label}
+ </span>
+ {choice.description !== undefined && (
+ <span
+ id="server-size-0-description-0"
+ class="text-gray-500"
+ >
+ <span class="block sm:inline">
+ {choice.description}
+ </span>
+ </span>
+ )}
+ </span>
+ </span>
+ </label>
+ );
+ })}
+ </div>
+ </fieldset>
+ {help && (
+ <p class="mt-2 text-sm text-gray-500" id="email-description">
+ {help}
+ </p>
+ )}
+ </div>
+ );
+}
diff --git a/packages/web-util/src/forms/InputFile.stories.tsx b/packages/web-util/src/forms/InputFile.stories.tsx
new file mode 100644
index 000000000..ba06debf9
--- /dev/null
+++ b/packages/web-util/src/forms/InputFile.stories.tsx
@@ -0,0 +1,64 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { TranslatedString } from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
+import {
+ FlexibleForm,
+ DefaultForm as TestedComponent,
+} from "./DefaultForm.js";
+
+export default {
+ title: "Input File",
+};
+
+export namespace Simplest {
+ export interface Form {
+ comment: string;
+ }
+}
+
+type TargetObject = {
+ comment: string;
+}
+const initial: TargetObject = {
+ comment: "some initial comment"
+}
+
+const form: FlexibleForm<TargetObject> = {
+ design: [{
+ title: "this is a simple form" as TranslatedString,
+ fields: [{
+ type: "file",
+ props: {
+ label: "label of the field" as TranslatedString,
+ name: "comment",
+ required: true,
+ maxBites: 2 * 1024 * 1024,
+ accept: ".png",
+ tooltip: "this is a very long tooltip that explain what the field does without being short" as TranslatedString,
+ help: "Max size of 2 mega bytes" as TranslatedString,
+ },
+ }]
+ }]
+}
+
+export const SimpleComment = tests.createExample(TestedComponent, { initial, form });
diff --git a/packages/web-util/src/forms/InputFile.tsx b/packages/web-util/src/forms/InputFile.tsx
new file mode 100644
index 000000000..6337d0902
--- /dev/null
+++ b/packages/web-util/src/forms/InputFile.tsx
@@ -0,0 +1,106 @@
+import { Fragment, VNode, h } from "preact";
+import { UIFormProps } from "./FormProvider.js";
+import { LabelWithTooltipMaybeRequired } from "./InputLine.js";
+import { useField } from "./useField.js";
+
+export function InputFile<T extends object, K extends keyof T>(
+ props: { maxBites: number; accept?: string } & UIFormProps<T, K>,
+): VNode {
+ const {
+ name,
+ label,
+ placeholder,
+ tooltip,
+ required,
+ help: propsHelp,
+ maxBites,
+ accept,
+ } = props;
+ const { value, onChange, state } = useField<T, K>(name);
+ const help = propsHelp ?? state.help
+ if (state.hidden) {
+ return <div />;
+ }
+ return (
+ <div class="col-span-full">
+ <LabelWithTooltipMaybeRequired
+ label={label}
+ tooltip={tooltip}
+ required={required}
+ />
+ {!value || !(value as string).startsWith("data:image/") ? (
+ <div class="mt-2 flex justify-center rounded-lg border border-dashed border-gray-900/25 py-1">
+ <div class="text-center">
+ <svg
+ class="mx-auto h-12 w-12 text-gray-300"
+ viewBox="0 0 24 24"
+ fill="currentColor"
+ aria-hidden="true"
+ >
+ <path
+ fill-rule="evenodd"
+ d="M1.5 6a2.25 2.25 0 012.25-2.25h16.5A2.25 2.25 0 0122.5 6v12a2.25 2.25 0 01-2.25 2.25H3.75A2.25 2.25 0 011.5 18V6zM3 16.06V18c0 .414.336.75.75.75h16.5A.75.75 0 0021 18v-1.94l-2.69-2.689a1.5 1.5 0 00-2.12 0l-.88.879.97.97a.75.75 0 11-1.06 1.06l-5.16-5.159a1.5 1.5 0 00-2.12 0L3 16.061zm10.125-7.81a1.125 1.125 0 112.25 0 1.125 1.125 0 01-2.25 0z"
+ clip-rule="evenodd"
+ />
+ </svg>
+ {!state.disabled &&
+ <div class="my-2 flex text-sm leading-6 text-gray-600">
+ <label
+ for="file-upload"
+ class="relative cursor-pointer rounded-md bg-white font-semibold text-indigo-600 focus-within:outline-none focus-within:ring-2 focus-within:ring-indigo-600 focus-within:ring-offset-2 hover:text-indigo-500"
+ >
+ <span>Upload a file</span>
+ <input
+ id="file-upload"
+ name="file-upload"
+ type="file"
+ class="sr-only"
+ accept={accept}
+ onChange={(e) => {
+ const f: FileList | null = e.currentTarget.files;
+ if (!f || f.length != 1) {
+ return onChange(undefined!);
+ }
+ if (f[0].size > maxBites) {
+ return onChange(undefined!);
+ }
+ 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);
+ });
+ }}
+ />
+ </label>
+ {/* <p class="pl-1">or drag and drop</p> */}
+ </div>
+ }
+ </div>
+ </div>
+ ) : (
+ <div class="mt-2 flex justify-center rounded-lg border border-dashed border-gray-900/25 relative">
+ <img
+ src={value as string}
+ class=" h-24 w-full object-cover relative"
+ />
+
+ {!state.disabled &&
+ <div
+ class="opacity-0 hover:opacity-70 duration-300 absolute rounded-lg border inset-0 z-10 flex justify-center text-xl items-center bg-black text-white cursor-pointer "
+ onClick={() => {
+ onChange(undefined!);
+ }}
+ >
+ Clear
+ </div>
+ }
+ </div>
+ )}
+ {help && <p class="text-xs leading-5 text-gray-600 mt-2">{help}</p>}
+ </div>
+ );
+}
diff --git a/packages/web-util/src/forms/InputInteger.stories.tsx b/packages/web-util/src/forms/InputInteger.stories.tsx
new file mode 100644
index 000000000..bd1a467ab
--- /dev/null
+++ b/packages/web-util/src/forms/InputInteger.stories.tsx
@@ -0,0 +1,55 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { TranslatedString } from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
+import {
+ FlexibleForm,
+ DefaultForm as TestedComponent,
+} from "./DefaultForm.js";
+
+export default {
+ title: "Input Integer",
+};
+
+
+type TargetObject = {
+ age: number;
+}
+const initial: TargetObject = {
+ age: 5,
+}
+
+const form: FlexibleForm<TargetObject> = {
+ design: [{
+ title: "this is a simple form" as TranslatedString,
+ fields: [{
+ type: "integer",
+ props: {
+ label: "label of the field" as TranslatedString,
+ name: "age",
+ tooltip: "just numbers" as TranslatedString,
+ },
+ }]
+ }]
+}
+
+export const SimpleComment = tests.createExample(TestedComponent, { initial, form });
diff --git a/packages/web-util/src/forms/InputInteger.tsx b/packages/web-util/src/forms/InputInteger.tsx
new file mode 100644
index 000000000..a6a02ad43
--- /dev/null
+++ b/packages/web-util/src/forms/InputInteger.tsx
@@ -0,0 +1,24 @@
+import { VNode, h } from "preact";
+import { InputLine } from "./InputLine.js";
+import { UIFormProps } from "./FormProvider.js";
+
+export function InputInteger<T extends object, K extends keyof T>(
+ props: UIFormProps<T, K>,
+): VNode {
+ return (
+ <InputLine
+ type="number"
+ converter={{
+ //@ts-ignore
+ fromStringUI: (v): number => {
+ return !v ? 0 : Number.parseInt(v, 10);
+ },
+ //@ts-ignore
+ toStringUI: (v?: number): string => {
+ return v === undefined ? "" : String(v);
+ },
+ }}
+ {...props}
+ />
+ );
+}
diff --git a/packages/web-util/src/forms/InputLine.stories.tsx b/packages/web-util/src/forms/InputLine.stories.tsx
new file mode 100644
index 000000000..da41a221e
--- /dev/null
+++ b/packages/web-util/src/forms/InputLine.stories.tsx
@@ -0,0 +1,59 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { TranslatedString } from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
+import {
+ FlexibleForm,
+ DefaultForm as TestedComponent,
+} from "./DefaultForm.js";
+
+export default {
+ title: "Input Line",
+};
+
+export namespace Simplest {
+ export interface Form {
+ comment: string;
+ }
+}
+
+type TargetObject = {
+ comment: string;
+}
+const initial: TargetObject = {
+ comment: "some initial comment"
+}
+
+const form: FlexibleForm<TargetObject> = {
+ design: [{
+ title: "this is a simple form" as TranslatedString,
+ fields: [{
+ type: "text",
+ props: {
+ label: "label of the field" as TranslatedString,
+ name: "comment",
+ },
+ }]
+ }]
+}
+
+export const SimpleComment = tests.createExample(TestedComponent, { initial, form });
diff --git a/packages/web-util/src/forms/InputLine.tsx b/packages/web-util/src/forms/InputLine.tsx
new file mode 100644
index 000000000..b8879f9ec
--- /dev/null
+++ b/packages/web-util/src/forms/InputLine.tsx
@@ -0,0 +1,268 @@
+import { TranslatedString } from "@gnu-taler/taler-util";
+import { ComponentChildren, Fragment, VNode, h } from "preact";
+import { useEffect, useState } from "preact/hooks";
+import { UIFormProps } from "./FormProvider.js";
+import { useField } from "./useField.js";
+
+//@ts-ignore
+const TooltipIcon = (
+ <svg
+ class="w-5 h-5"
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="0 0 20 20"
+ fill="currentColor"
+ >
+ <path
+ fill-rule="evenodd"
+ d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-3a1 1 0 00-.867.5 1 1 0 11-1.731-1A3 3 0 0113 8a3.001 3.001 0 01-2 2.83V11a1 1 0 11-2 0v-1a1 1 0 011-1 1 1 0 100-2zm0 8a1 1 0 100-2 1 1 0 000 2z"
+ clip-rule="evenodd"
+ />
+ </svg>
+);
+
+export function LabelWithTooltipMaybeRequired({
+ label,
+ required,
+ tooltip,
+}: {
+ label: TranslatedString;
+ required?: boolean;
+ tooltip?: TranslatedString;
+}): VNode {
+ const Label = (
+ <Fragment>
+ <div class="flex justify-between">
+ <label
+ htmlFor="email"
+ class="block text-sm font-medium leading-6 text-gray-900"
+ >
+ {label}
+ </label>
+ </div>
+ </Fragment>
+ );
+ const WithTooltip = tooltip ? (
+ <div class="relative flex flex-grow items-stretch focus-within:z-10">
+ {Label}
+ <span class="relative flex items-center group pl-2">
+ {TooltipIcon}
+ <div class="absolute bottom-0 -ml-10 hidden flex-col items-center mb-6 group-hover:flex w-28">
+ <div class="relative z-10 p-2 text-xs leading-none text-white whitespace-no-wrap bg-black shadow-lg">
+ {tooltip}
+ </div>
+ <div class="w-3 h-3 -mt-2 rotate-45 bg-black"></div>
+ </div>
+ </span>
+ </div>
+ ) : (
+ Label
+ );
+ if (required) {
+ return (
+ <div class="flex justify-between">
+ {WithTooltip}
+ <span class="text-sm leading-6 text-red-600">*</span>
+ </div>
+ );
+ }
+ return WithTooltip;
+}
+
+function InputWrapper<T extends object, K extends keyof T>({
+ children,
+ label,
+ tooltip,
+ before,
+ after,
+ help,
+ error,
+ disabled,
+ required,
+}: { error?: string; disabled: boolean, children: ComponentChildren } & UIFormProps<T, K>): VNode {
+ return (
+ <div class="sm:col-span-6">
+ <LabelWithTooltipMaybeRequired
+ label={label}
+ required={required}
+ tooltip={tooltip}
+ />
+ <div class="relative mt-2 flex rounded-md shadow-sm">
+ {before &&
+ (before.type === "text" ? (
+ <span class="inline-flex items-center rounded-l-md border border-r-0 border-gray-300 px-3 text-gray-500 sm:text-sm">
+ {before.text}
+ </span>
+ ) : before.type === "icon" ? (
+ <div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
+ {before.icon}
+ </div>
+ ) : before.type === "button" ? (
+ <button
+ type="button"
+ disabled={disabled}
+ onClick={before.onClick}
+ class="relative -ml-px inline-flex items-center gap-x-1.5 rounded-l-md px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
+ >
+ {before.children}
+ </button>
+ ) : undefined)}
+
+ {children}
+
+ {after &&
+ (after.type === "text" ? (
+ <span class="inline-flex items-center rounded-r-md border border-l-0 border-gray-300 px-3 text-gray-500 sm:text-sm">
+ {after.text}
+ </span>
+ ) : after.type === "icon" ? (
+ <div class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
+ {after.icon}
+ </div>
+ ) : after.type === "button" ? (
+ <button
+ type="button"
+ disabled={disabled}
+ onClick={after.onClick}
+ class="relative -ml-px inline-flex items-center gap-x-1.5 rounded-r-md px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
+ >
+ {after.children}
+ </button>
+ ) : undefined)}
+ </div>
+ {error && (
+ <p class="mt-2 text-sm text-red-600" id="email-error">
+ {error}
+ </p>
+ )}
+ {help && (
+ <p class="mt-2 text-sm text-gray-500" id="email-description">
+ {help}
+ </p>
+ )}
+ </div>
+ );
+}
+
+function defaultToString(v: unknown) {
+ return v === undefined ? "" : typeof v !== "object" ? String(v) : "";
+}
+function defaultFromString(v: string) {
+ return v;
+}
+
+type InputType = "text" | "text-area" | "password" | "email" | "number";
+
+export function InputLine<T extends object, K extends keyof T>(
+ props: { type: InputType } & UIFormProps<T, K>,
+): VNode {
+ const { name, placeholder, before, after, converter, type } = props;
+ const { value, onChange, state, isDirty } = useField<T, K>(name);
+
+ const [text, setText] = useState("")
+ const fromString: (s: string) => any =
+ converter?.fromStringUI ?? defaultFromString;
+ const toString: (s: any) => string = converter?.toStringUI ?? defaultToString;
+
+ useEffect(() => {
+ const newValue = toString(value)
+ if (newValue) {
+ setText(newValue)
+ }
+ }, [value])
+
+ if (state.hidden) return <div />;
+
+ let clazz =
+ "block w-full rounded-md border-0 py-1.5 shadow-sm ring-1 ring-inset focus:ring-2 focus:ring-inset sm:text-sm sm:leading-6 disabled:cursor-not-allowed disabled:bg-gray-50 disabled:text-gray-500 disabled:ring-gray-200";
+ if (before) {
+ switch (before.type) {
+ case "icon": {
+ clazz += " pl-10";
+ break;
+ }
+ case "button": {
+ clazz += " rounded-none rounded-r-md ";
+ break;
+ }
+ case "text": {
+ clazz += " min-w-0 flex-1 rounded-r-md rounded-none ";
+ break;
+ }
+ }
+ }
+ if (after) {
+ switch (after.type) {
+ case "icon": {
+ clazz += " pr-10";
+ break;
+ }
+ case "button": {
+ clazz += " rounded-none rounded-l-md";
+ break;
+ }
+ case "text": {
+ clazz += " min-w-0 flex-1 rounded-l-md rounded-none ";
+ break;
+ }
+ }
+ }
+ const showError = isDirty && state.error;
+ if (showError) {
+ clazz +=
+ " text-red-900 ring-red-300 placeholder:text-red-300 focus:ring-red-500";
+ } else {
+ clazz +=
+ " text-gray-900 ring-gray-300 placeholder:text-gray-400 focus:ring-indigo-600";
+ }
+
+ if (type === "text-area") {
+ return (
+ <InputWrapper<T, K>
+ {...props}
+ help={props.help ?? state.help}
+ disabled={state.disabled ?? false}
+ error={showError ? state.error : undefined}
+ >
+ <textarea
+ rows={4}
+ name={String(name)}
+ onChange={(e) => {
+ onChange(fromString(e.currentTarget.value));
+ }}
+ placeholder={placeholder ? placeholder : undefined}
+ value={toString(value) ?? ""}
+ // defaultValue={toString(value)}
+ disabled={state.disabled}
+ aria-invalid={showError}
+ // aria-describedby="email-error"
+ class={clazz}
+ />
+ </InputWrapper>
+ );
+ }
+
+ return (
+ <InputWrapper<T, K> {...props}
+ help={props.help ?? state.help}
+ disabled={state.disabled ?? false} error={showError ? state.error : undefined}
+ >
+ <input
+ name={String(name)}
+ type={type}
+ onChange={(e) => {
+ setText(e.currentTarget.value)
+ }}
+ placeholder={placeholder ? placeholder : undefined}
+ value={text}
+ onBlur={() => {
+ onChange(fromString(text));
+ }}
+ // defaultValue={toString(value)}
+ disabled={state.disabled}
+ aria-invalid={showError}
+ // aria-describedby="email-error"
+ class={clazz}
+ />
+ </InputWrapper>
+ );
+}
diff --git a/packages/web-util/src/forms/InputSelectMultiple.stories.tsx b/packages/web-util/src/forms/InputSelectMultiple.stories.tsx
new file mode 100644
index 000000000..6ce5445c0
--- /dev/null
+++ b/packages/web-util/src/forms/InputSelectMultiple.stories.tsx
@@ -0,0 +1,90 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { TranslatedString } from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
+import {
+ FlexibleForm,
+ DefaultForm as TestedComponent,
+} from "./DefaultForm.js";
+
+export default {
+ title: "Input Select Multiple",
+};
+
+export namespace Simplest {
+ export interface Form {
+ comment: string;
+ }
+}
+
+type TargetObject = {
+ pets: string[];
+ things: string[];
+}
+const initial: TargetObject = {
+ pets: [],
+ things: [],
+}
+
+const form: FlexibleForm<TargetObject> = {
+ design: [{
+ title: "this is a simple form" as TranslatedString,
+ fields: [{
+ type: "selectMultiple",
+ props: {
+ label: "allow diplicates" as TranslatedString,
+ name: "pets",
+ placeholder: "search..." as TranslatedString,
+ choices: [{
+ label: "one label" as TranslatedString,
+ value: "one"
+ }, {
+ label: "two label" as TranslatedString,
+ value: "two"
+ }, {
+ label: "five label" as TranslatedString,
+ value: "five"
+ }]
+ },
+ }, {
+ type: "selectMultiple",
+ props: {
+ label: "unique values" as TranslatedString,
+ name: "things",
+ unique: true,
+ placeholder: "search..." as TranslatedString,
+ choices: [{
+ label: "one label" as TranslatedString,
+ value: "one"
+ }, {
+ label: "two label" as TranslatedString,
+ value: "two"
+ }, {
+ label: "five label" as TranslatedString,
+ value: "five"
+ }]
+ },
+ }]
+ }]
+}
+
+export const SimpleComment = tests.createExample(TestedComponent, { initial, form });
diff --git a/packages/web-util/src/forms/InputSelectMultiple.tsx b/packages/web-util/src/forms/InputSelectMultiple.tsx
new file mode 100644
index 000000000..a67eb23b7
--- /dev/null
+++ b/packages/web-util/src/forms/InputSelectMultiple.tsx
@@ -0,0 +1,154 @@
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { UIFormProps } from "./FormProvider.js";
+import { ChoiceS } from "./InputChoiceStacked.js";
+import { LabelWithTooltipMaybeRequired } from "./InputLine.js";
+import { useField } from "./useField.js";
+
+export function InputSelectMultiple<T extends object, K extends keyof T>(
+ props: {
+ choices: ChoiceS<T[K]>[];
+ unique?: boolean;
+ max?: number;
+ } & UIFormProps<T, K>,
+): VNode {
+ const { name, label, choices, placeholder, tooltip, required, unique, max } =
+ props;
+ const { value, onChange, state } = useField<T, K>(name);
+
+ const [filter, setFilter] = useState<string | undefined>(undefined);
+ const regex = new RegExp(`.*${filter}.*`, "i");
+ const choiceMap = choices.reduce((prev, curr) => {
+ return { ...prev, [curr.value as string]: curr.label };
+ }, {} as Record<string, string>);
+
+ const list = (value ?? []) as string[];
+ const filteredChoices =
+ filter === undefined
+ ? undefined
+ : choices.filter((v) => {
+ return regex.test(v.label);
+ });
+ return (
+ <div class="sm:col-span-6">
+ <LabelWithTooltipMaybeRequired
+ label={label}
+ required={required}
+ tooltip={tooltip}
+ />
+ {list.map((v, idx) => {
+ return (
+ <span class="inline-flex items-center gap-x-0.5 rounded-md bg-gray-100 p-1 mr-2 text-xs font-medium text-gray-600">
+ {choiceMap[v]}
+ <button
+ type="button"
+ disabled={state.disabled}
+ onClick={() => {
+ const newValue = [...list];
+ newValue.splice(idx, 1);
+ onChange(newValue as T[K]);
+ setFilter(undefined);
+ }}
+ class="group relative h-5 w-5 rounded-sm hover:bg-gray-500/20"
+ >
+ <span class="sr-only">Remove</span>
+ <svg
+ viewBox="0 0 14 14"
+ class="h-5 w-5 stroke-gray-700/50 group-hover:stroke-gray-700/75"
+ >
+ <path d="M4 4l6 6m0-6l-6 6" />
+ </svg>
+ <span class="absolute -inset-1"></span>
+ </button>
+ </span>
+ );
+ })}
+
+ {!state.disabled && <div class="relative mt-2">
+ <input
+ id="combobox"
+ type="text"
+ value={filter ?? ""}
+ onChange={(e) => {
+ setFilter(e.currentTarget.value);
+ }}
+ placeholder={placeholder}
+ class="w-full rounded-md border-0 bg-white py-1.5 pl-3 pr-12 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ role="combobox"
+ aria-controls="options"
+ aria-expanded="false"
+ />
+ <button
+ type="button"
+ disabled={state.disabled}
+ onClick={() => {
+ setFilter(filter === undefined ? "" : undefined);
+ }}
+ class="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none"
+ >
+ <svg
+ class="h-5 w-5 text-gray-400"
+ viewBox="0 0 20 20"
+ fill="currentColor"
+ aria-hidden="true"
+ >
+ <path
+ fill-rule="evenodd"
+ d="M10 3a.75.75 0 01.55.24l3.25 3.5a.75.75 0 11-1.1 1.02L10 4.852 7.3 7.76a.75.75 0 01-1.1-1.02l3.25-3.5A.75.75 0 0110 3zm-3.76 9.2a.75.75 0 011.06.04l2.7 2.908 2.7-2.908a.75.75 0 111.1 1.02l-3.25 3.5a.75.75 0 01-1.1 0l-3.25-3.5a.75.75 0 01.04-1.06z"
+ clip-rule="evenodd"
+ />
+ </svg>
+ </button>
+
+ {filteredChoices !== undefined && (
+ <ul
+ class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
+ id="options"
+ role="listbox"
+ >
+ {filteredChoices.map((v, idx) => {
+ return (
+ <li
+ class="relative cursor-pointer select-none py-2 pl-3 pr-9 text-gray-900 hover:text-white hover:bg-indigo-600"
+ id="option-0"
+ role="option"
+ onClick={() => {
+ setFilter(undefined);
+ if (unique && list.indexOf(v.value as string) !== -1) {
+ return;
+ }
+ if (max !== undefined && list.length >= max) {
+ return;
+ }
+ const newValue = [...list];
+ newValue.splice(0, 0, v.value as string);
+ onChange(newValue as T[K]);
+ }}
+
+ // tabindex="-1"
+ >
+ {/* <!-- Selected: "font-semibold" --> */}
+ <span class="block truncate">{v.label}</span>
+
+ {/* <!--
+ Checkmark, only display for selected option.
+
+ Active: "text-white", Not Active: "text-indigo-600"
+ --> */}
+ </li>
+ );
+ })}
+
+ {/* <!--
+ Combobox option, manage highlight styles based on mouseenter/mouseleave and keyboard navigation.
+
+ Active: "text-white bg-indigo-600", Not Active: "text-gray-900"
+ --> */}
+
+ {/* <!-- More items... --> */}
+ </ul>
+ )}
+ </div>}
+ </div>
+ );
+}
diff --git a/packages/web-util/src/forms/InputSelectOne.stories.tsx b/packages/web-util/src/forms/InputSelectOne.stories.tsx
new file mode 100644
index 000000000..9e9029a77
--- /dev/null
+++ b/packages/web-util/src/forms/InputSelectOne.stories.tsx
@@ -0,0 +1,70 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { TranslatedString } from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
+import {
+ FlexibleForm,
+ DefaultForm as TestedComponent,
+} from "./DefaultForm.js";
+
+export default {
+ title: "Input Select One",
+};
+
+export namespace Simplest {
+ export interface Form {
+ comment: string;
+ }
+}
+
+type TargetObject = {
+ things: string;
+}
+const initial: TargetObject = {
+ things: "one"
+}
+
+const form: FlexibleForm<TargetObject> = {
+ design: [{
+ title: "this is a simple form" as TranslatedString,
+ fields: [{
+ type: "selectOne",
+ props: {
+ label: "label of the field" as TranslatedString,
+ name: "things",
+ placeholder: "search..." as TranslatedString,
+ choices: [{
+ label: "one label" as TranslatedString,
+ value: "one"
+ }, {
+ label: "two label" as TranslatedString,
+ value: "two"
+ }, {
+ label: "five label" as TranslatedString,
+ value: "five"
+ }]
+ },
+ }]
+ }]
+}
+
+export const SimpleComment = tests.createExample(TestedComponent, { initial, form });
diff --git a/packages/web-util/src/forms/InputSelectOne.tsx b/packages/web-util/src/forms/InputSelectOne.tsx
new file mode 100644
index 000000000..d100b079d
--- /dev/null
+++ b/packages/web-util/src/forms/InputSelectOne.tsx
@@ -0,0 +1,135 @@
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { UIFormProps } from "./FormProvider.js";
+import { ChoiceS } from "./InputChoiceStacked.js";
+import { LabelWithTooltipMaybeRequired } from "./InputLine.js";
+import { useField } from "./useField.js";
+
+export function InputSelectOne<T extends object, K extends keyof T>(
+ props: {
+ choices: ChoiceS<T[K]>[];
+ } & UIFormProps<T, K>,
+): VNode {
+ const { name, label, choices, placeholder, tooltip, required } = props;
+ const { value, onChange } = useField<T, K>(name);
+
+ const [filter, setFilter] = useState<string | undefined>(undefined);
+ const regex = new RegExp(`.*${filter}.*`, "i");
+ const choiceMap = choices.reduce((prev, curr) => {
+ return { ...prev, [curr.value as string]: curr.label };
+ }, {} as Record<string, string>);
+
+ const filteredChoices =
+ filter === undefined
+ ? undefined
+ : choices.filter((v) => {
+ return regex.test(v.label);
+ });
+ return (
+ <div class="sm:col-span-6">
+ <LabelWithTooltipMaybeRequired
+ label={label}
+ required={required}
+ tooltip={tooltip}
+ />
+ {value ? (
+ <span class="inline-flex items-center gap-x-0.5 rounded-md bg-gray-100 p-1 mr-2 font-medium text-gray-600">
+ {choiceMap[value as string]}
+ <button
+ type="button"
+ onClick={() => {
+ onChange(undefined!);
+ }}
+ class="group relative h-5 w-5 rounded-sm hover:bg-gray-500/20"
+ >
+ <span class="sr-only">Remove</span>
+ <svg
+ viewBox="0 0 14 14"
+ class="h-5 w-5 stroke-gray-700/50 group-hover:stroke-gray-700/75"
+ >
+ <path d="M4 4l6 6m0-6l-6 6" />
+ </svg>
+ <span class="absolute -inset-1"></span>
+ </button>
+ </span>
+ ) : (
+ <div class="relative mt-2">
+ <input
+ id="combobox"
+ type="text"
+ value={filter ?? ""}
+ onChange={(e) => {
+ setFilter(e.currentTarget.value);
+ }}
+ placeholder={placeholder}
+ class="w-full rounded-md border-0 bg-white py-1.5 pl-3 pr-12 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ role="combobox"
+ aria-controls="options"
+ aria-expanded="false"
+ />
+ <button
+ type="button"
+ onClick={() => {
+ setFilter(filter === undefined ? "" : undefined);
+ }}
+ class="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none"
+ >
+ <svg
+ class="h-5 w-5 text-gray-400"
+ viewBox="0 0 20 20"
+ fill="currentColor"
+ aria-hidden="true"
+ >
+ <path
+ fill-rule="evenodd"
+ d="M10 3a.75.75 0 01.55.24l3.25 3.5a.75.75 0 11-1.1 1.02L10 4.852 7.3 7.76a.75.75 0 01-1.1-1.02l3.25-3.5A.75.75 0 0110 3zm-3.76 9.2a.75.75 0 011.06.04l2.7 2.908 2.7-2.908a.75.75 0 111.1 1.02l-3.25 3.5a.75.75 0 01-1.1 0l-3.25-3.5a.75.75 0 01.04-1.06z"
+ clip-rule="evenodd"
+ />
+ </svg>
+ </button>
+
+ {filteredChoices !== undefined && (
+ <ul
+ class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
+ id="options"
+ role="listbox"
+ >
+ {filteredChoices.map((v, idx) => {
+ return (
+ <li
+ class="relative cursor-pointer select-none py-2 pl-3 pr-9 text-gray-900 hover:text-white hover:bg-indigo-600"
+ id="option-0"
+ role="option"
+ onClick={() => {
+ setFilter(undefined);
+ onChange(v.value as T[K]);
+ }}
+
+ // tabindex="-1"
+ >
+ {/* <!-- Selected: "font-semibold" --> */}
+ <span class="block truncate">{v.label}</span>
+
+ {/* <!--
+ Checkmark, only display for selected option.
+
+ Active: "text-white", Not Active: "text-indigo-600"
+ --> */}
+ </li>
+ );
+ })}
+
+ {/* <!--
+ Combobox option, manage highlight styles based on mouseenter/mouseleave and keyboard navigation.
+
+ Active: "text-white bg-indigo-600", Not Active: "text-gray-900"
+ --> */}
+
+ {/* <!-- More items... --> */}
+ </ul>
+ )}
+ </div>
+ )}
+ </div>
+ );
+}
diff --git a/packages/web-util/src/forms/InputText.stories.tsx b/packages/web-util/src/forms/InputText.stories.tsx
new file mode 100644
index 000000000..04ab8a1c6
--- /dev/null
+++ b/packages/web-util/src/forms/InputText.stories.tsx
@@ -0,0 +1,59 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { TranslatedString } from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
+import {
+ FlexibleForm,
+ DefaultForm as TestedComponent,
+} from "./DefaultForm.js";
+
+export default {
+ title: "Input Text",
+};
+
+export namespace Simplest {
+ export interface Form {
+ comment: string;
+ }
+}
+
+type TargetObject = {
+ comment: string;
+}
+const initial: TargetObject = {
+ comment: "some initial comment"
+}
+
+const form: FlexibleForm<TargetObject> = {
+ design: [{
+ title: "this is a simple form" as TranslatedString,
+ fields: [{
+ type: "text",
+ props: {
+ label: "label of the field" as TranslatedString,
+ name: "comment",
+ },
+ }]
+ }]
+}
+
+export const SimpleComment = tests.createExample(TestedComponent, { initial, form });
diff --git a/packages/web-util/src/forms/InputText.tsx b/packages/web-util/src/forms/InputText.tsx
new file mode 100644
index 000000000..1c0c04225
--- /dev/null
+++ b/packages/web-util/src/forms/InputText.tsx
@@ -0,0 +1,9 @@
+import { VNode, h } from "preact";
+import { UIFormProps } from "./FormProvider.js";
+import { InputLine } from "./InputLine.js";
+
+export function InputText<T extends object, K extends keyof T>(
+ props: UIFormProps<T, K>,
+): VNode {
+ return <InputLine type="text" {...props} />;
+}
diff --git a/packages/web-util/src/forms/InputTextArea.stories.tsx b/packages/web-util/src/forms/InputTextArea.stories.tsx
new file mode 100644
index 000000000..c8c3eb088
--- /dev/null
+++ b/packages/web-util/src/forms/InputTextArea.stories.tsx
@@ -0,0 +1,59 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { TranslatedString } from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
+import {
+ DefaultForm as TestedComponent,
+ FlexibleForm,
+} from "./DefaultForm.js";
+
+export default {
+ title: "Input Text Area",
+};
+
+export namespace Simplest {
+ export interface Form {
+ comment: string;
+ }
+}
+
+type TargetObject = {
+ comment: string;
+}
+const initial: TargetObject = {
+ comment: "some initial comment"
+}
+
+const form: FlexibleForm<TargetObject> = {
+ design: [{
+ title: "this is a simple form" as TranslatedString,
+ fields: [{
+ type: "text",
+ props: {
+ label: "label of the field" as TranslatedString,
+ name: "comment",
+ },
+ }]
+ }]
+}
+
+export const SimpleComment = tests.createExample(TestedComponent, { initial, form });
diff --git a/packages/web-util/src/forms/InputTextArea.tsx b/packages/web-util/src/forms/InputTextArea.tsx
new file mode 100644
index 000000000..6b76d8329
--- /dev/null
+++ b/packages/web-util/src/forms/InputTextArea.tsx
@@ -0,0 +1,9 @@
+import { VNode, h } from "preact";
+import { InputLine } from "./InputLine.js";
+import { UIFormProps } from "./FormProvider.js";
+
+export function InputTextArea<T extends object, K extends keyof T>(
+ props: UIFormProps<T, K>,
+): VNode {
+ return <InputLine type="text-area" {...props} />;
+}
diff --git a/packages/web-util/src/forms/InputToggle.stories.tsx b/packages/web-util/src/forms/InputToggle.stories.tsx
new file mode 100644
index 000000000..ca6857618
--- /dev/null
+++ b/packages/web-util/src/forms/InputToggle.stories.tsx
@@ -0,0 +1,59 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { TranslatedString } from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
+import {
+ FlexibleForm,
+ DefaultForm as TestedComponent,
+} from "./DefaultForm.js";
+
+export default {
+ title: "Input Toggle",
+};
+
+export namespace Simplest {
+ export interface Form {
+ comment: string;
+ }
+}
+
+type TargetObject = {
+ comment: string;
+}
+const initial: TargetObject = {
+ comment: "some initial comment"
+}
+
+const form: FlexibleForm<TargetObject> = {
+ design: [{
+ title: "this is a simple form" as TranslatedString,
+ fields: [{
+ type: "toggle",
+ props: {
+ label: "label of the field" as TranslatedString,
+ name: "comment",
+ },
+ }]
+ }]
+}
+
+export const SimpleComment = tests.createExample(TestedComponent, { initial, form });
diff --git a/packages/web-util/src/forms/InputToggle.tsx b/packages/web-util/src/forms/InputToggle.tsx
new file mode 100644
index 000000000..56b29c502
--- /dev/null
+++ b/packages/web-util/src/forms/InputToggle.tsx
@@ -0,0 +1,38 @@
+import { VNode, h } from "preact";
+import { UIFormProps } from "./FormProvider.js";
+import { LabelWithTooltipMaybeRequired } from "./InputLine.js";
+import { useField } from "./useField.js";
+
+export function InputToggle<T extends object, K extends keyof T>(
+ props: UIFormProps<T, K>,
+): VNode {
+ const {
+ name,
+ label,
+ tooltip,
+ help,
+ placeholder,
+ required,
+ before,
+ after,
+ converter,
+ } = props;
+ const { value, onChange, state, isDirty } = useField<T, K>(name);
+
+ const isOn = !!value
+ return <div class="sm:col-span-6">
+ <div class="flex items-center justify-between">
+ <LabelWithTooltipMaybeRequired
+ label={label}
+ required={required}
+ tooltip={tooltip}
+ />
+ <button type="button" 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={() => { onChange(!isOn as any); }}>
+ <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>
+ </div>
+}
diff --git a/packages/web-util/src/forms/TimePicker.tsx b/packages/web-util/src/forms/TimePicker.tsx
new file mode 100644
index 000000000..5e4e7a8fa
--- /dev/null
+++ b/packages/web-util/src/forms/TimePicker.tsx
@@ -0,0 +1,109 @@
+import { AbsoluteTime } from "@gnu-taler/taler-util"
+import { getHours, getMinutes, getSeconds, setHours } from "date-fns"
+import { Fragment, VNode, h } from "preact"
+import { useTranslationContext } from "../index.browser.js"
+
+export function TimePicker({ value, onChange, onConfirm }: { value: AbsoluteTime | undefined, onChange: (v: AbsoluteTime) => void, onConfirm: () => void }): VNode {
+ const date = !value ? new Date() : new Date(AbsoluteTime.toStampMs(value))
+ const hours = getHours(date) % 12
+ const minutes = getMinutes(date)
+ const seconds = getSeconds(date)
+
+ const { i18n } = useTranslationContext()
+
+ return <Fragment>
+ <div class="flex flex-col bg-white rounded-t-sm justify-around" >
+ {/* time selection */}
+ <div id="" class="bg-[#3b71ca] dark:bg-zinc-700 h-24 rounded-t-lg p-12 flex flex-row items-center justify-center">
+ <div class="flex w-full justify-evenly">
+ <div class="">
+ <span class="relative h-full">
+ <button type="button" class="py-1 px-3 text-[3.75rem] font-light leading-[1.2] text-white opacity-[.54] border-none bg-transparent p-0 cursor-pointer hover:bg-[#00000026] hover:outline-none focus:bg-[#00000026] focus:outline-none "
+ style="pointer-events: none;">
+ {new String(hours).padStart(2, "0")}
+ </button>
+ </span>
+ <span type="button" class="font-light leading-[1.2] text-[3.75rem] opacity-[.54] border-none bg-transparent p-0 text-white " >:</span>
+ <span class="relative h-full">
+ <button type="button" class="py-1 px-3 text-[3.75rem] font-light leading-[1.2] text-white opacity-[.54] border-none bg-transparent p-0 cursor-pointer hover:bg-[#00000026] hover:outline-none focus:bg-[#00000026] focus:outline-none " >
+ {new String(minutes).padStart(2, "0")}
+ </button>
+ </span>
+ <span type="button" class="font-light leading-[1.2] text-[3.75rem] opacity-[.54] border-none bg-transparent p-0 text-white " >:</span>
+ <span class="relative h-full">
+ <button type="button" class="py-1 px-3 text-[3.75rem] font-light leading-[1.2] text-white opacity-[.54] border-none bg-transparent p-0 cursor-pointer hover:bg-[#00000026] hover:outline-none focus:bg-[#00000026] focus:outline-none " >
+ {new String(seconds).padStart(2, "0")}
+ </button>
+ </span>
+ </div>
+ <div class="flex flex-col justify-center text-[18px] text-[#ffffff8a] ">
+ <button type="button" class="py-1 px-3 bg-transparent border-none text-white cursor-pointer hover:bg-[#00000026] hover:outline-none focus:bg-[#00000026] focus:outline-none" >
+ AM
+ </button>
+ <button type="button" class="py-1 px-3 bg-transparent border-none text-white cursor-pointer hover:bg-[#00000026] hover:outline-none focus:bg-[#00000026] focus:outline-none" >
+ PM
+ </button>
+ </div>
+ </div>
+ </div>
+ {/* clock */}
+ <div id="" class="mt-2 min-w-[310px] max-w-[325px] min-h-[305px] overflow-x-hidden h-full flex justify-center mx-auto flex-col items-center dark:bg-zinc-500" >
+ <div class="relative rounded-[100%] w-[260px] h-[260px] cursor-default my-0 mx-auto bg-[#00000012] dark:bg-zinc-600/50 animate-[show-up-clock_350ms_linear]" >
+
+ <span class="top-1/2 left-1/2 w-[6px] h-[6px] -translate-y-1/2 -translate-x-1/2 rounded-[50%] bg-[#3b71ca] absolute" ></span>
+ <div class="bg-[#3b71ca] bottom-1/2 h-2/5 left-[calc(50%-1px)] rtl:!left-auto origin-[center_bottom_0] rtl:!origin-[50%_50%_0] w-[2px] absolute" style={{ transform: "rotateZ(60deg)", height: "calc(35% + 1px)" }}>
+ {/* <div class="-top-[21px] -left-[15px] w-[4px] border-[14px] border-solid border-[#3b71ca] h-[4px] box-content rounded-[100%] absolute" style="background-color: rgb(25, 118, 210);"></div> */}
+ </div>
+
+ <span onClick={() => onChange(AbsoluteTime.fromStampMs(setHours(date, 12).getTime()))} class="absolute rounded-[100%] w-[32px] h-[32px] text-center cursor-pointer text-[1.1rem] bg-transparent flex justify-center items-center font-light focus:outline-none selection:bg-transparent data-[selected=true]:text-white data-[selected=true]:bg-[#3b71ca] data-[selected=true]:font-normal" style="left: 114px; bottom: 224px;">
+ <span>0</span>
+ </span>
+ <span onClick={() => onChange(AbsoluteTime.fromStampMs(setHours(date, 1).getTime()))} class="absolute rounded-[100%] w-[32px] h-[32px] text-center cursor-pointer text-[1.1rem] bg-transparent flex justify-center items-center font-light focus:outline-none selection:bg-transparent data-[selected=true]:text-white data-[selected=true]:bg-[#3b71ca] data-[selected=true]:font-normal" style="left: 169px; bottom: 209.263px;">
+ <span >1</span>
+ </span>
+ <span onClick={() => onChange(AbsoluteTime.fromStampMs(setHours(date, 2).getTime()))} class="absolute rounded-[100%] w-[32px] h-[32px] text-center cursor-pointer text-[1.1rem] bg-transparent flex justify-center items-center font-light focus:outline-none selection:bg-transparent data-[selected=true]:text-white data-[selected=true]:bg-[#3b71ca] data-[selected=true]:font-normal" data-selected={true} style="left: 209.263px; bottom: 169px;" >
+ <span >2</span>
+ </span>
+ <span onClick={() => onChange(AbsoluteTime.fromStampMs(setHours(date, 3).getTime()))} class="absolute rounded-[100%] w-[32px] h-[32px] text-center cursor-pointer text-[1.1rem] bg-transparent flex justify-center items-center font-light focus:outline-none selection:bg-transparent data-[selected=true]:text-white data-[selected=true]:bg-[#3b71ca] data-[selected=true]:font-normal" style="left: 224px; bottom: 114px;">
+ <span >3</span>
+ </span>
+ <span onClick={() => onChange(AbsoluteTime.fromStampMs(setHours(date, 4).getTime()))} class="absolute rounded-[100%] w-[32px] h-[32px] text-center cursor-pointer text-[1.1rem] bg-transparent flex justify-center items-center font-light focus:outline-none selection:bg-transparent data-[selected=true]:text-white data-[selected=true]:bg-[#3b71ca] data-[selected=true]:font-normal" style="left: 209.263px; bottom: 59px;">
+ <span >4</span>
+ </span>
+ <span onClick={() => onChange(AbsoluteTime.fromStampMs(setHours(date, 5).getTime()))} class="absolute rounded-[100%] w-[32px] h-[32px] text-center cursor-pointer text-[1.1rem] bg-transparent flex justify-center items-center font-light focus:outline-none selection:bg-transparent data-[selected=true]:text-white data-[selected=true]:bg-[#3b71ca] data-[selected=true]:font-normal" style="left: 169px; bottom: 18.7372px;">
+ <span >5</span>
+ </span>
+ <span onClick={() => onChange(AbsoluteTime.fromStampMs(setHours(date, 6).getTime()))} class="absolute rounded-[100%] w-[32px] h-[32px] text-center cursor-pointer text-[1.1rem] bg-transparent flex justify-center items-center font-light focus:outline-none selection:bg-transparent data-[selected=true]:text-white data-[selected=true]:bg-[#3b71ca] data-[selected=true]:font-normal" style="left: 114px; bottom: 4px;">
+ <span >6</span>
+ </span>
+ <span onClick={() => onChange(AbsoluteTime.fromStampMs(setHours(date, 7).getTime()))} class="absolute rounded-[100%] w-[32px] h-[32px] text-center cursor-pointer text-[1.1rem] bg-transparent flex justify-center items-center font-light focus:outline-none selection:bg-transparent data-[selected=true]:text-white data-[selected=true]:bg-[#3b71ca] data-[selected=true]:font-normal" style="left: 59px; bottom: 18.7372px;">
+ <span >7</span>
+ </span>
+ <span onClick={() => onChange(AbsoluteTime.fromStampMs(setHours(date, 8).getTime()))} class="absolute rounded-[100%] w-[32px] h-[32px] text-center cursor-pointer text-[1.1rem] bg-transparent flex justify-center items-center font-light focus:outline-none selection:bg-transparent data-[selected=true]:text-white data-[selected=true]:bg-[#3b71ca] data-[selected=true]:font-normal" style="left: 18.7372px; bottom: 59px;">
+ <span >8</span>
+ </span>
+ <span onClick={() => onChange(AbsoluteTime.fromStampMs(setHours(date, 9).getTime()))} class="absolute rounded-[100%] w-[32px] h-[32px] text-center cursor-pointer text-[1.1rem] bg-transparent flex justify-center items-center font-light focus:outline-none selection:bg-transparent data-[selected=true]:text-white data-[selected=true]:bg-[#3b71ca] data-[selected=true]:font-normal" style="left: 4px; bottom: 114px;">
+ <span >9</span>
+ </span>
+ <span onClick={() => onChange(AbsoluteTime.fromStampMs(setHours(date, 10).getTime()))} class="absolute rounded-[100%] w-[32px] h-[32px] text-center cursor-pointer text-[1.1rem] bg-transparent flex justify-center items-center font-light focus:outline-none selection:bg-transparent data-[selected=true]:text-white data-[selected=true]:bg-[#3b71ca] data-[selected=true]:font-normal" style="left: 18.7372px; bottom: 169px;">
+ <span >10</span>
+ </span>
+ <span onClick={() => onChange(AbsoluteTime.fromStampMs(setHours(date, 11).getTime()))} class="absolute rounded-[100%] w-[32px] h-[32px] text-center cursor-pointer text-[1.1rem] bg-transparent flex justify-center items-center font-light focus:outline-none selection:bg-transparent data-[selected=true]:text-white data-[selected=true]:bg-[#3b71ca] data-[selected=true]:font-normal" style="left: 59px; bottom: 209.263px;">
+ <span >11</span>
+ </span>
+ </div>
+ </div>
+ </div>
+ <div id="" class="rounded-b-lg flex justify-between items-center w-full h-[56px] px-[12px] bg-white dark:bg-zinc-500">
+ <div class="w-full flex justify-end">
+ <button
+ type="submit"
+ onClick={onConfirm}
+ class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </button>
+ </div>
+ </div>
+ </Fragment>
+}
diff --git a/packages/web-util/src/forms/forms.ts b/packages/web-util/src/forms/forms.ts
new file mode 100644
index 000000000..3b8620bfb
--- /dev/null
+++ b/packages/web-util/src/forms/forms.ts
@@ -0,0 +1,134 @@
+import { h as create, Fragment, VNode } from "preact";
+import { Caption } from "./Caption.js";
+import { FormProvider } from "./FormProvider.js";
+import { Group } from "./Group.js";
+import { InputAbsoluteTime } from "./InputAbsoluteTime.js";
+import { InputAmount } from "./InputAmount.js";
+import { InputArray } from "./InputArray.js";
+import { InputChoiceHorizontal } from "./InputChoiceHorizontal.js";
+import { InputChoiceStacked } from "./InputChoiceStacked.js";
+import { InputFile } from "./InputFile.js";
+import { InputInteger } from "./InputInteger.js";
+import { InputLine } from "./InputLine.js";
+import { InputSelectMultiple } from "./InputSelectMultiple.js";
+import { InputSelectOne } from "./InputSelectOne.js";
+import { InputText } from "./InputText.js";
+import { InputTextArea } from "./InputTextArea.js";
+import { InputToggle } from "./InputToggle.js";
+
+/**
+ * Constrain the type with the ui props
+ */
+type FieldType<T extends object = any, K extends keyof T = any> = {
+ group: Parameters<typeof Group>[0];
+ caption: Parameters<typeof Caption>[0];
+ array: Parameters<typeof InputArray<T, K>>[0];
+ file: Parameters<typeof InputFile<T, K>>[0];
+ selectOne: Parameters<typeof InputSelectOne<T, K>>[0];
+ selectMultiple: Parameters<typeof InputSelectMultiple<T, K>>[0];
+ text: Parameters<typeof InputText<T, K>>[0];
+ textArea: Parameters<typeof InputTextArea<T, K>>[0];
+ choiceStacked: Parameters<typeof InputChoiceStacked<T, K>>[0];
+ choiceHorizontal: Parameters<typeof InputChoiceHorizontal<T, K>>[0];
+ absoluteTime: Parameters<typeof InputAbsoluteTime<T, K>>[0];
+ integer: Parameters<typeof InputInteger<T, K>>[0];
+ toggle: Parameters<typeof InputToggle<T, K>>[0];
+ amount: Parameters<typeof InputAmount<T, K>>[0];
+};
+
+/**
+ * List all the form fields so typescript can type-check the form instance
+ */
+export type UIFormField =
+ | { type: "group"; props: FieldType["group"] }
+ | { type: "caption"; props: FieldType["caption"] }
+ | { type: "array"; props: FieldType["array"] }
+ | { type: "file"; props: FieldType["file"] }
+ | { type: "amount"; props: FieldType["amount"] }
+ | { type: "selectOne"; props: FieldType["selectOne"] }
+ | { type: "selectMultiple"; props: FieldType["selectMultiple"] }
+ | { type: "text"; props: FieldType["text"] }
+ | { type: "textArea"; props: FieldType["textArea"] }
+ | { type: "choiceStacked"; props: FieldType["choiceStacked"] }
+ | { type: "choiceHorizontal"; props: FieldType["choiceHorizontal"] }
+ | { type: "integer"; props: FieldType["integer"] }
+ | { type: "toggle"; props: FieldType["toggle"] }
+ | { type: "absoluteTime"; props: FieldType["absoluteTime"] };
+
+type FieldComponentFunction<key extends keyof FieldType> = (
+ props: FieldType[key],
+) => VNode;
+
+type UIFormFieldMap = {
+ [key in keyof FieldType]: FieldComponentFunction<key>;
+};
+
+/**
+ * Maps input type with component implementation
+ */
+const UIFormConfiguration: UIFormFieldMap = {
+ group: Group,
+ caption: Caption,
+ //@ts-ignore
+ array: InputArray,
+ text: InputText,
+ //@ts-ignore
+ file: InputFile,
+ textArea: InputTextArea,
+ //@ts-ignore
+ absoluteTime: InputAbsoluteTime,
+ //@ts-ignore
+ choiceStacked: InputChoiceStacked,
+ //@ts-ignore
+ choiceHorizontal: InputChoiceHorizontal,
+ integer: InputInteger,
+ //@ts-ignore
+ selectOne: InputSelectOne,
+ //@ts-ignore
+ selectMultiple: InputSelectMultiple,
+ //@ts-ignore
+ toggle: InputToggle,
+ //@ts-ignore
+ amount: InputAmount,
+};
+
+export function RenderAllFieldsByUiConfig({
+ fields,
+}: {
+ fields: UIFormField[];
+}): VNode {
+ return create(
+ Fragment,
+ {},
+ fields.map((field, i) => {
+ const Component = UIFormConfiguration[
+ field.type
+ ] as FieldComponentFunction<any>;
+ return Component(field.props);
+ }),
+ );
+}
+
+type FormSet<T extends object> = {
+ Provider: typeof FormProvider<T>;
+ InputLine: <K extends keyof T>() => typeof InputLine<T, K>;
+ InputChoiceHorizontal: <K extends keyof T>() => typeof InputChoiceHorizontal<T, K>;
+};
+
+/**
+ * Helper function that created a typed object.
+ *
+ * @returns
+ */
+export function createNewForm<T extends object>() {
+ const res: FormSet<T> = {
+ Provider: FormProvider,
+ InputLine: () => InputLine,
+ InputChoiceHorizontal: () => InputChoiceHorizontal,
+ };
+ return {
+ Provider: res.Provider,
+ InputLine: res.InputLine(),
+ InputChoiceHorizontal: res.InputChoiceHorizontal(),
+ };
+}
diff --git a/packages/web-util/src/forms/index.stories.ts b/packages/web-util/src/forms/index.stories.ts
new file mode 100644
index 000000000..55878cb02
--- /dev/null
+++ b/packages/web-util/src/forms/index.stories.ts
@@ -0,0 +1,13 @@
+export * as a1 from "./InputAmount.stories.js";
+export * as a2 from "./InputArray.stories.js";
+export * as a3 from "./InputChoiceHorizontal.stories.js";
+export * as a4 from "./InputChoiceStacked.stories.js";
+export * as a5 from "./InputAbsoluteTime.stories.js";
+export * as a6 from "./InputFile.stories.js";
+export * as a7 from "./InputInteger.stories.js";
+export * as a8 from "./InputLine.stories.js";
+export * as a9 from "./InputSelectMultiple.stories.js";
+export * as a10 from "./InputSelectOne.stories.js";
+export * as a11 from "./InputText.stories.js";
+export * as a12 from "./InputTextArea.stories.js";
+export * as a13 from "./InputToggle.stories.js";
diff --git a/packages/web-util/src/forms/index.ts b/packages/web-util/src/forms/index.ts
new file mode 100644
index 000000000..4ff71f197
--- /dev/null
+++ b/packages/web-util/src/forms/index.ts
@@ -0,0 +1,23 @@
+export * from "./Calendar.js"
+export * from "./Caption.js"
+export * from "./DefaultForm.js"
+export * from "./Dialog.js"
+export * from "./FormProvider.js"
+export * from "./Group.js"
+export * from "./InputAbsoluteTime.js"
+export * from "./InputAmount.js"
+export * from "./InputArray.js"
+export * from "./InputChoiceHorizontal.js"
+export * from "./InputChoiceStacked.js"
+export * from "./InputFile.js"
+export * from "./InputInteger.js"
+export * from "./InputLine.js"
+export * from "./InputSelectMultiple.js"
+export * from "./InputSelectOne.js"
+export * from "./InputText.js"
+export * from "./InputTextArea.js"
+export * from "./InputToggle.js"
+export * from "./TimePicker.js"
+export * from "./forms.js"
+export * from "./useField.js"
+
diff --git a/packages/web-util/src/forms/useField.ts b/packages/web-util/src/forms/useField.ts
new file mode 100644
index 000000000..eed8cebea
--- /dev/null
+++ b/packages/web-util/src/forms/useField.ts
@@ -0,0 +1,93 @@
+import { useContext, useState } from "preact/compat";
+import { FieldUIOptions, FormContext } from "./FormProvider.js";
+
+export interface InputFieldHandler<Type> {
+ value: Type;
+ onChange: (s: Type) => void;
+ state: FieldUIOptions;
+ isDirty: boolean;
+}
+
+export function useField<T extends object, K extends keyof T>(
+ name: K,
+): InputFieldHandler<T[K]> {
+ const {
+ value: formValue,
+ computeFormState,
+ onUpdate: notifyUpdate,
+ readOnly: readOnlyForm,
+ } = useContext(FormContext);
+
+ type P = typeof name;
+ type V = T[P];
+ const formState = computeFormState ? computeFormState(formValue.current) : {};
+
+ const fieldValue = readField(formValue.current, String(name)) as V;
+ // console.log("USE FIELD", String(name), formValue.current, fieldValue);
+ const [currentValue, setCurrentValue] = useState<any | undefined>(fieldValue);
+ const fieldState =
+ readField<Partial<FieldUIOptions>>(formState, String(name)) ?? {};
+
+ //compute default state
+ const state = {
+ disabled: readOnlyForm ? true : (fieldState.disabled ?? false),
+ hidden: fieldState.hidden ?? false,
+ error: fieldState.error,
+ help: fieldState.help,
+ elements: "elements" in fieldState ? fieldState.elements ?? [] : [],
+ };
+
+ function onChange(value: V): void {
+ setCurrentValue(value);
+ formValue.current = setValueDeeper(
+ formValue.current,
+ String(name).split("."),
+ value,
+ );
+ if (notifyUpdate) {
+ notifyUpdate(formValue.current);
+ }
+ }
+
+ return {
+ value: fieldValue,
+ onChange,
+ isDirty: currentValue !== undefined,
+ state,
+ };
+}
+
+/**
+ * read the field of an object an support accessing it using '.'
+ *
+ * @param object
+ * @param name
+ * @returns
+ */
+function readField<T>(
+ object: any,
+ name: string,
+ debug?: boolean,
+): T | undefined {
+ return name.split(".").reduce((prev, current) => {
+ if (debug) {
+ console.log(
+ "READ",
+ name,
+ prev,
+ current,
+ prev ? prev[current] : undefined,
+ );
+ }
+ return prev ? prev[current] : undefined;
+ }, object);
+}
+
+function setValueDeeper(object: any, names: string[], value: any): any {
+ if (names.length === 0) return value;
+ const [head, ...rest] = names;
+ if (object === undefined) {
+ return { [head]: setValueDeeper({}, rest, value) };
+ }
+ return { ...object, [head]: setValueDeeper(object[head] ?? {}, rest, value) };
+}
diff --git a/packages/web-util/src/hooks/index.ts b/packages/web-util/src/hooks/index.ts
index f18d61b9c..ba1b6e222 100644
--- a/packages/web-util/src/hooks/index.ts
+++ b/packages/web-util/src/hooks/index.ts
@@ -1,3 +1,13 @@
-
export { useLang } from "./useLang.js";
-export { useLocalStorage, useNotNullLocalStorage } from "./useLocalStorage.js" \ No newline at end of file
+export { useLocalStorage, buildStorageKey, StorageKey, StorageState } from "./useLocalStorage.js";
+export { useMemoryStorage } from "./useMemoryStorage.js";
+export * from "./useNotifications.js";
+export {
+ useAsyncAsHook,
+ HookError,
+ HookOk,
+ HookResponse,
+ HookResponseWithRetry,
+ HookGenericError,
+ HookOperationalError,
+} from "./useAsyncAsHook.js";
diff --git a/packages/web-util/src/hooks/useAsyncAsHook.ts b/packages/web-util/src/hooks/useAsyncAsHook.ts
new file mode 100644
index 000000000..48d29aa45
--- /dev/null
+++ b/packages/web-util/src/hooks/useAsyncAsHook.ts
@@ -0,0 +1,91 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { TalerErrorDetail } from "@gnu-taler/taler-util";
+// import { TalerError } from "@gnu-taler/taler-wallet-core";
+import { useEffect, useMemo, useState } from "preact/hooks";
+
+export interface HookOk<T> {
+ hasError: false;
+ response: T;
+}
+
+export type HookError = HookGenericError | HookOperationalError;
+
+export interface HookGenericError {
+ hasError: true;
+ operational: false;
+ message: string;
+}
+
+export interface HookOperationalError {
+ hasError: true;
+ operational: true;
+ details: TalerErrorDetail;
+}
+
+interface WithRetry {
+ retry: () => void;
+}
+
+export type HookResponse<T> = HookOk<T> | HookError | undefined;
+export type HookResponseWithRetry<T> =
+ | ((HookOk<T> | HookError) & WithRetry)
+ | undefined;
+
+export function useAsyncAsHook<T>(
+ fn: () => Promise<T | false>,
+ deps?: any[],
+): HookResponseWithRetry<T> {
+ const [result, setHookResponse] = useState<HookResponse<T>>(undefined);
+
+ const args = useMemo(
+ () => ({
+ fn,
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }),
+ deps || [],
+ );
+
+ async function doAsync(): Promise<void> {
+ try {
+ const response = await args.fn();
+ if (response === false) return;
+ setHookResponse({ hasError: false, response });
+ } catch (e) {
+ // if (e instanceof TalerError) {
+ // setHookResponse({
+ // hasError: true,
+ // operational: true,
+ // details: e.errorDetail,
+ // });
+ // } else
+ if (e instanceof Error) {
+ setHookResponse({
+ hasError: true,
+ operational: false,
+ message: e.message,
+ });
+ }
+ }
+ }
+
+ useEffect(() => {
+ doAsync();
+ }, [args]);
+
+ if (!result) return undefined;
+ return { ...result, retry: doAsync };
+}
diff --git a/packages/web-util/src/hooks/useLang.ts b/packages/web-util/src/hooks/useLang.ts
index 5b02c5255..5b1be0309 100644
--- a/packages/web-util/src/hooks/useLang.ts
+++ b/packages/web-util/src/hooks/useLang.ts
@@ -14,17 +14,48 @@
GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { useNotNullLocalStorage } from "./useLocalStorage.js";
+import {
+ StorageState,
+ buildStorageKey,
+ useLocalStorage,
+} from "./useLocalStorage.js";
+
+const MIN_LANG_COVERAGE_THRESHOLD = 90;
+/**
+ * choose the best from the browser config based on the completeness
+ * on the translation
+ */
+function getBrowserLang(completeness: Record<string, number>): string | undefined {
+ if (typeof window === "undefined") return undefined;
+
+ if (window.navigator.language) {
+ if (completeness[window.navigator.language] >= MIN_LANG_COVERAGE_THRESHOLD) {
+ return window.navigator.language
+ }
+ }
+ if (window.navigator.languages) {
+ const match = Object.entries(completeness).filter(([code, value]) => {
+ if (value < MIN_LANG_COVERAGE_THRESHOLD) return false; //do not consider langs below 90%
+ return window.navigator.languages.findIndex(l => l.startsWith(code)) !== -1
+ }).map(([code, value]) => ({ code, value }))
+
+ if (match.length > 0) {
+ let max = match[0]
+ match.forEach(v => {
+ if (v.value > max.value) {
+ max = v
+ }
+ })
+ return max.code
+ }
+ };
-function getBrowserLang(): string | undefined {
- if (window.navigator.languages) return window.navigator.languages[0];
- if (window.navigator.language) return window.navigator.language;
return undefined;
}
-export function useLang(
- initial?: string,
-): [string, (s: string) => void, boolean] {
- const defaultLang = (getBrowserLang() || initial || "en").substring(0, 2);
- return useNotNullLocalStorage("lang-preference", defaultLang);
+const langPreferenceKey = buildStorageKey("lang-preference");
+
+export function useLang(initial: string | undefined, completeness: Record<string, number>): Required<StorageState> {
+ const defaultValue = (getBrowserLang(completeness) || initial || "en").substring(0, 2);
+ return useLocalStorage(langPreferenceKey, defaultValue);
}
diff --git a/packages/web-util/src/hooks/useLocalStorage.ts b/packages/web-util/src/hooks/useLocalStorage.ts
index 25df9dfcd..abd80bacc 100644
--- a/packages/web-util/src/hooks/useLocalStorage.ts
+++ b/packages/web-util/src/hooks/useLocalStorage.ts
@@ -19,90 +19,121 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { StateUpdater, useEffect, useState } from "preact/hooks";
-
-export function useLocalStorage(
- 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;
- },
- );
+import { AbsoluteTime, Codec, codecForString } from "@gnu-taler/taler-util";
+import { useEffect, useState } from "preact/hooks";
+import {
+ ObservableMap,
+ browserStorageMap,
+ localStorageMap,
+ memoryMap,
+} from "../utils/observable.js";
- useEffect(() => {
- const listener = buildListenerForKey(key, (newValue) => {
- setStoredValue(newValue ?? initialValue)
- })
- window.addEventListener('storage', listener)
- return () => {
- window.removeEventListener('storage', listener)
- }
- }, [])
-
- const setValue = (
- value?: string | ((val?: string) => string | undefined),
- ): void => {
- 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;
- });
- };
+declare const opaque_StorageKey: unique symbol;
- return [storedValue, setValue];
+export type StorageKey<Key> = {
+ id: string;
+ [opaque_StorageKey]: true;
+ codec: Codec<Key>;
+};
+
+export function buildStorageKey<Key>(
+ name: string,
+ codec: Codec<Key>,
+): StorageKey<Key>;
+export function buildStorageKey(name: string): StorageKey<string>;
+export function buildStorageKey<Key = string>(
+ name: string,
+ codec?: Codec<Key>,
+): StorageKey<Key> {
+ return {
+ id: name,
+ codec: codec ?? (codecForString() as Codec<Key>),
+ } as StorageKey<Key>;
}
-function buildListenerForKey(key: string, onUpdate: (newValue: string | undefined) => void): () => void {
- return function listenKeyChange() {
- const value = window.localStorage.getItem(key)
- onUpdate(value ?? undefined)
- }
+export interface StorageState<Type = string> {
+ value?: Type;
+ update: (s: Type) => void;
+ reset: () => void;
}
-//TODO: merge with the above function
-export function useNotNullLocalStorage(
- key: string,
- initialValue: string,
-): [string, StateUpdater<string>, boolean] {
- const [storedValue, setStoredValue] = useState<string>((): string => {
- return typeof window !== "undefined"
- ? window.localStorage.getItem(key) || initialValue
- : initialValue;
- });
+const supportLocalStorage = typeof window !== "undefined";
+const supportBrowserStorage =
+ typeof chrome !== "undefined" && typeof chrome.storage !== "undefined";
+
+/**
+ * Build setting storage
+ */
+const storage: ObservableMap<string, string> = (function buildStorage() {
+ if (supportBrowserStorage) {
+ //browser storage is like local storage but
+ //with app sync.
+ //Works for almost every browser
+ if (supportLocalStorage) {
+ return browserStorageMap(localStorageMap());
+ } else {
+ // service worker doesn't have local storage
+ return browserStorageMap(memoryMap<string>());
+ }
+ } else if (supportLocalStorage) {
+ // fallback if browser is too old
+ return localStorageMap();
+ } else {
+ // new need to save settings somewhere
+ return memoryMap<string>();
+ }
+})();
+//with initial value
+export function useLocalStorage<Type = string>(
+ key: StorageKey<Type>,
+ defaultValue: Type,
+): Required<StorageState<Type>>;
+//without initial value
+export function useLocalStorage<Type = string>(
+ key: StorageKey<Type>,
+): StorageState<Type>;
+// impl
+export function useLocalStorage<Type = string>(
+ key: StorageKey<Type>,
+ defaultValue?: Type,
+): StorageState<Type> {
+ const current = convert(storage.get(key.id), key, defaultValue);
+ const [_, setStoredValue] = useState(AbsoluteTime.now().t_ms);
useEffect(() => {
- const listener = buildListenerForKey(key, (newValue) => {
- setStoredValue(newValue ?? initialValue)
- })
- window.localStorage.addEventListener('storage', listener)
- return () => {
- window.localStorage.removeEventListener('storage', listener)
- }
- })
-
- const setValue = (value: string | ((val: string) => string)): void => {
- const valueToStore = value instanceof Function ? value(storedValue) : value;
- setStoredValue(valueToStore);
- if (typeof window !== "undefined") {
- if (!valueToStore) {
- window.localStorage.removeItem(key);
- } else {
- window.localStorage.setItem(key, valueToStore);
- }
+ return storage.onUpdate(key.id, () => {
+ // const newValue = storage.get(key.id);
+ setStoredValue(AbsoluteTime.now().t_ms);
+ });
+ }, [key.id]);
+
+ const setValue = (value?: Type): void => {
+ if (value === undefined) {
+ storage.delete(key.id);
+ } else {
+ storage.set(
+ key.id,
+ key.codec ? JSON.stringify(value) : (value as string),
+ );
}
};
- const isSaved = window.localStorage.getItem(key) !== null;
- return [storedValue, setValue, isSaved];
+ return {
+ value: current,
+ update: setValue,
+ reset: () => {
+ setValue(defaultValue);
+ },
+ };
+}
+
+function convert<Type>(updated: string | undefined, key: StorageKey<Type>, defaultValue?: Type): Type | undefined {
+ if (updated === undefined) return defaultValue; //optional
+ try {
+ return key.codec.decode(JSON.parse(updated));
+ } catch (e) {
+ //decode error
+ return defaultValue;
+ }
}
diff --git a/packages/web-util/src/hooks/useMemoryStorage.ts b/packages/web-util/src/hooks/useMemoryStorage.ts
new file mode 100644
index 000000000..ef186392f
--- /dev/null
+++ b/packages/web-util/src/hooks/useMemoryStorage.ts
@@ -0,0 +1,71 @@
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useEffect, useState } from "preact/hooks";
+import { ObservableMap, memoryMap } from "../utils/observable.js";
+import { StorageKey, StorageState } from "./useLocalStorage.js";
+
+const storage: ObservableMap<string, any> = memoryMap<any>();
+
+//with initial value
+export function useMemoryStorage<Type = string>(
+ key: string,
+ defaultValue: Type,
+): Required<StorageState<Type>>;
+//with initial value
+export function useMemoryStorage<Type = string>(
+ key: string,
+): StorageState<Type>;
+// impl
+export function useMemoryStorage<Type = string>(
+ key: string,
+ defaultValue?: Type,
+): StorageState<Type> {
+ const [storedValue, setStoredValue] = useState<Type | undefined>(
+ (): Type | undefined => {
+ const prev = storage.get(key);
+ return prev === undefined ? defaultValue : prev;
+ },
+ );
+
+ useEffect(() => {
+ return storage.onUpdate(key, () => {
+ const newValue = storage.get(key);
+ setStoredValue(newValue === undefined ? defaultValue : newValue);
+ });
+ }, [key]);
+
+ const setValue = (value?: Type): void => {
+ if (value === undefined) {
+ storage.delete(key);
+ } else {
+ storage.set(key, value);
+ }
+ };
+
+ return {
+ value: storedValue,
+ update: setValue,
+ reset: () => {
+ setValue(defaultValue);
+ },
+ };
+}
diff --git a/packages/web-util/src/hooks/useNotifications.ts b/packages/web-util/src/hooks/useNotifications.ts
new file mode 100644
index 000000000..81a1ae91e
--- /dev/null
+++ b/packages/web-util/src/hooks/useNotifications.ts
@@ -0,0 +1,337 @@
+import {
+ AbsoluteTime,
+ Duration,
+ OperationAlternative,
+ OperationFail,
+ OperationOk,
+ OperationResult,
+ TalerError,
+ TalerErrorCode,
+ TranslatedString,
+} from "@gnu-taler/taler-util";
+import { useEffect, useState } from "preact/hooks";
+import { ButtonHandler, OnOperationFailReturnType, OnOperationSuccesReturnType } from "../components/Button.js";
+import {
+ InternationalizationAPI,
+ memoryMap,
+ useTranslationContext,
+} from "../index.browser.js";
+
+export type NotificationMessage = ErrorNotification | InfoNotification;
+
+export interface ErrorNotification {
+ type: "error";
+ title: TranslatedString;
+ ack?: boolean;
+ timeout?: boolean;
+ description?: TranslatedString;
+ debug?: any;
+ when: AbsoluteTime;
+}
+export interface InfoNotification {
+ type: "info";
+ title: TranslatedString;
+ ack?: boolean;
+ timeout?: boolean;
+ when: AbsoluteTime;
+}
+
+const storage = memoryMap<Map<string, NotificationMessage>>();
+const NOTIFICATION_KEY = "notification";
+
+export const GLOBAL_NOTIFICATION_TIMEOUT = Duration.fromSpec({
+ seconds: 5,
+});
+
+function updateInStorage(n: NotificationMessage) {
+ const h = hash(n);
+ const mem = storage.get(NOTIFICATION_KEY) ?? new Map();
+ const newState = new Map(mem);
+ newState.set(h, n);
+ storage.set(NOTIFICATION_KEY, newState);
+}
+
+export function notify(notif: NotificationMessage): void {
+ const currentState: Map<string, NotificationMessage> =
+ storage.get(NOTIFICATION_KEY) ?? new Map();
+ const newState = currentState.set(hash(notif), notif);
+
+ if (GLOBAL_NOTIFICATION_TIMEOUT.d_ms !== "forever") {
+ setTimeout(() => {
+ notif.timeout = true;
+ updateInStorage(notif);
+ }, GLOBAL_NOTIFICATION_TIMEOUT.d_ms);
+ }
+
+ storage.set(NOTIFICATION_KEY, newState);
+}
+export function notifyError(
+ title: TranslatedString,
+ description: TranslatedString | undefined,
+ debug?: any,
+) {
+ notify({
+ type: "error" as const,
+ title,
+ description,
+ debug,
+ when: AbsoluteTime.now(),
+ });
+}
+export function notifyException(title: TranslatedString, ex: Error) {
+ notify({
+ type: "error" as const,
+ title,
+ description: ex.message as TranslatedString,
+ debug: ex.stack,
+ when: AbsoluteTime.now(),
+ });
+}
+export function notifyInfo(title: TranslatedString) {
+ notify({
+ type: "info" as const,
+ title,
+ when: AbsoluteTime.now(),
+ });
+}
+
+export type Notification = {
+ message: NotificationMessage;
+ acknowledge: () => void;
+};
+
+export function useNotifications(): Notification[] {
+ const [, setLastUpdate] = useState<number>();
+ const value = storage.get(NOTIFICATION_KEY) ?? new Map();
+
+ useEffect(() => {
+ return storage.onUpdate(NOTIFICATION_KEY, () => {
+ setLastUpdate(Date.now())
+ // const mem = storage.get(NOTIFICATION_KEY) ?? new Map();
+ // setter(structuredClone(mem));
+ });
+ });
+
+ return Array.from(value.values()).map((message, idx) => {
+ return {
+ message,
+ acknowledge: () => {
+ message.ack = true;
+ updateInStorage(message);
+ },
+ };
+ });
+}
+
+function hashCode(str: string): string {
+ if (str.length === 0) return "0";
+ let hash = 0;
+ let chr;
+ for (let i = 0; i < str.length; i++) {
+ chr = str.charCodeAt(i);
+ hash = (hash << 5) - hash + chr;
+ hash |= 0; // Convert to 32bit integer
+ }
+ return hash.toString(16);
+}
+
+function hash(msg: NotificationMessage): string {
+ let str = (msg.type + ":" + msg.title) as string;
+ if (msg.type === "error") {
+ if (msg.description) {
+ str += ":" + msg.description;
+ }
+ if (msg.debug) {
+ str += ":" + msg.debug;
+ }
+ }
+ return hashCode(str);
+}
+
+function errorMap<T extends OperationFail<unknown>>(
+ resp: T,
+ map: (d: T["case"]) => TranslatedString,
+): void {
+ notify({
+ type: "error",
+ title: map(resp.case),
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+}
+
+export type ErrorNotificationHandler = (
+ cb: (notify: typeof errorMap) => Promise<void>,
+) => Promise<void>;
+
+/**
+ * @deprecated use useLocalNotificationHandler
+ *
+ * @returns
+ */
+export function useLocalNotification(): [
+ Notification | undefined,
+ (n: NotificationMessage) => void,
+ ErrorNotificationHandler,
+] {
+ const { i18n } = useTranslationContext();
+
+ const [value, setter] = useState<NotificationMessage>();
+ const notif = !value
+ ? undefined
+ : {
+ message: value,
+ acknowledge: () => {
+ setter(undefined);
+ },
+ };
+
+ async function errorHandling(cb: (notify: typeof errorMap) => Promise<void>) {
+ try {
+ return await cb(errorMap);
+ } catch (error: unknown) {
+ if (error instanceof TalerError) {
+ notify(buildUnifiedRequestErrorMessage(i18n, error));
+ } else {
+ notifyError(
+ i18n.str`Operation failed, please report`,
+ (error instanceof Error
+ ? error.message
+ : JSON.stringify(error)) as TranslatedString,
+ );
+ }
+ }
+ }
+ return [notif, setter, errorHandling];
+}
+
+type HandlerMaker = <T extends OperationResult<A, B>, A, B>(
+ onClick: () => Promise<T | undefined>,
+ onOperationSuccess: OnOperationSuccesReturnType<T>,
+ onOperationFail: OnOperationFailReturnType<T>,
+ onOperationComplete?: () => void,
+) => ButtonHandler<T, A, B>;
+
+export function useLocalNotificationHandler(): [
+ Notification | undefined,
+ HandlerMaker,
+ (n: NotificationMessage) => void,
+] {
+ const [value, setter] = useState<NotificationMessage>();
+ const notif = !value
+ ? undefined
+ : {
+ message: value,
+ acknowledge: () => {
+ setter(undefined);
+ },
+ };
+
+ function makeHandler<T extends OperationResult<A, B>, A, B>(
+ onClick: () => Promise<T | undefined>,
+ onOperationSuccess:OnOperationSuccesReturnType<T>,
+ onOperationFail: OnOperationFailReturnType<T>,
+ onOperationComplete?: () => void,
+ ): ButtonHandler<T, A, B> {
+ return {
+ onClick,
+ onNotification: setter,
+ onOperationFail,
+ onOperationSuccess,
+ onOperationComplete,
+ };
+ }
+
+ return [notif, makeHandler, setter];
+}
+
+export function buildUnifiedRequestErrorMessage(
+ i18n: InternationalizationAPI,
+ cause: TalerError,
+): ErrorNotification {
+ let result: ErrorNotification;
+ switch (cause.errorDetail.code) {
+ case TalerErrorCode.GENERIC_TIMEOUT: {
+ result = {
+ type: "error",
+ title: i18n.str`Request timeout`,
+ description: cause.message as TranslatedString,
+ debug: JSON.stringify(cause.errorDetail, undefined, 2),
+ when: AbsoluteTime.now(),
+ };
+ break;
+ }
+ case TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR: {
+ result = {
+ type: "error",
+ title: i18n.str`Request cancelled`,
+ description: cause.message as TranslatedString,
+ debug: JSON.stringify(cause.errorDetail, undefined, 2),
+ when: AbsoluteTime.now(),
+ };
+ break;
+ }
+ case TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT: {
+ result = {
+ type: "error",
+ title: i18n.str`Request timeout`,
+ description: cause.message as TranslatedString,
+ debug: JSON.stringify(cause.errorDetail, undefined, 2),
+ when: AbsoluteTime.now(),
+ };
+ break;
+ }
+ case TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED: {
+ result = {
+ type: "error",
+ title: i18n.str`Request throttled`,
+ description: cause.message as TranslatedString,
+ debug: JSON.stringify(cause.errorDetail, undefined, 2),
+ when: AbsoluteTime.now(),
+ };
+ break;
+ }
+ case TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE: {
+ result = {
+ type: "error",
+ title: i18n.str`Malformed response`,
+ description: cause.message as TranslatedString,
+ debug: JSON.stringify(cause.errorDetail, undefined, 2),
+ when: AbsoluteTime.now(),
+ };
+ break;
+ }
+ case TalerErrorCode.WALLET_NETWORK_ERROR: {
+ result = {
+ type: "error",
+ title: i18n.str`Network error`,
+ description: cause.message as TranslatedString,
+ debug: JSON.stringify(cause.errorDetail, undefined, 2),
+ when: AbsoluteTime.now(),
+ };
+ break;
+ }
+ case TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR: {
+ result = {
+ type: "error",
+ title: i18n.str`Unexpected request error`,
+ description: cause.message as TranslatedString,
+ debug: JSON.stringify(cause.errorDetail, undefined, 2),
+ when: AbsoluteTime.now(),
+ };
+ break;
+ }
+ default: {
+ result = {
+ type: "error",
+ title: i18n.str`Unexpected error`,
+ description: cause.message as TranslatedString,
+ debug: JSON.stringify(cause.errorDetail, undefined, 2),
+ when: AbsoluteTime.now(),
+ };
+ break;
+ }
+ }
+ return result;
+}
diff --git a/packages/web-util/src/index.browser.ts b/packages/web-util/src/index.browser.ts
index 57c97e605..2f3b57b8d 100644
--- a/packages/web-util/src/index.browser.ts
+++ b/packages/web-util/src/index.browser.ts
@@ -1,2 +1,10 @@
-export * as hooks from "./hooks/index.js";
+export * from "./hooks/index.js";
+export * from "./utils/request.js";
+export * from "./utils/http-impl.browser.js";
+export * from "./utils/http-impl.sw.js";
+export * from "./utils/observable.js";
+export * from "./utils/route.js";
+export * from "./context/index.js";
+export * from "./components/index.js";
+export * from "./forms/index.js";
export { renderStories, parseGroupImport } from "./stories.js";
diff --git a/packages/web-util/src/index.build.ts b/packages/web-util/src/index.build.ts
new file mode 100644
index 000000000..c0c5fc179
--- /dev/null
+++ b/packages/web-util/src/index.build.ts
@@ -0,0 +1,327 @@
+import esbuild, { PluginBuild } from "esbuild";
+import linaria from "@linaria/esbuild";
+import fs from "fs";
+import path from "path";
+import postcss from "postcss";
+import sass from "sass";
+import postcssrc from "postcss-load-config";
+
+// this should give us the current directory where
+// the project is being built
+const BASE = process.cwd();
+
+type Assets = {
+ base: string;
+ files: string[];
+};
+
+export function getFilesInDirectory(startPath: string, regex?: RegExp): Assets {
+ if (!fs.existsSync(startPath)) {
+ return {
+ base: startPath,
+ files: [],
+ };
+ }
+ const files = fs.readdirSync(startPath);
+ const result = files
+ .flatMap((file) => {
+ const filename = path.join(startPath, file);
+
+ const stat = fs.lstatSync(filename);
+ if (stat.isDirectory()) {
+ return getFilesInDirectory(filename, regex).files;
+ }
+ if (!regex || regex.test(filename)) {
+ return [filename];
+ }
+ return [];
+ })
+ .filter((x) => !!x);
+
+ return {
+ base: startPath,
+ files: result,
+ };
+}
+
+let GIT_ROOT = BASE;
+while (!fs.existsSync(path.join(GIT_ROOT, ".git")) && GIT_ROOT !== "/") {
+ GIT_ROOT = path.join(GIT_ROOT, "../");
+}
+if (GIT_ROOT === "/") {
+ // eslint-disable-next-line no-undef
+ console.log("not found");
+ // eslint-disable-next-line no-undef
+ process.exit(1);
+}
+const GIT_HASH = git_hash();
+
+const buf = fs.readFileSync(path.join(BASE, "package.json"));
+let _package = JSON.parse(buf.toString("utf-8"));
+
+function git_hash() {
+ const rev = fs
+ .readFileSync(path.join(GIT_ROOT, ".git", "HEAD"))
+ .toString()
+ .trim()
+ .split(/.*[: ]/)
+ .slice(-1)[0];
+ if (rev.indexOf("/") === -1) {
+ return rev;
+ } else {
+ return fs.readFileSync(path.join(GIT_ROOT, ".git", rev)).toString().trim();
+ }
+}
+
+// FIXME: Put this into some helper library.
+function copyFilesPlugin(assets: Assets | Assets[]) {
+ return {
+ name: "copy-files",
+ setup(build: PluginBuild) {
+ if (!assets || (assets instanceof Array && !assets.length)) {
+ return;
+ }
+ const list = assets instanceof Array ? assets : [assets];
+
+ const outDir = build.initialOptions.outdir;
+ if (outDir === undefined) {
+ throw Error("esbuild build options does not specify outdir");
+ }
+ build.onEnd(() => {
+ list.forEach((as) => {
+ for (const file of as.files) {
+ const destination = path.join(outDir, path.relative(as.base, file));
+ const dirname = path.dirname(destination);
+ if (!fs.existsSync(dirname)) {
+ fs.mkdirSync(dirname, { recursive: true });
+ }
+ fs.copyFileSync(file, destination);
+ }
+ });
+ });
+ },
+ };
+}
+
+const DEFAULT_SASS_FILTER = /\.(s[ac]ss|css)$/;
+
+const sassPlugin: esbuild.Plugin = {
+ name: "custom-build-sass",
+ setup(build) {
+ build.onLoad({ filter: DEFAULT_SASS_FILTER }, ({ path: file }) => {
+ const resolveDir = path.dirname(file);
+ const { css: contents } = sass.compile(file, { loadPaths: ["./"] });
+
+ return {
+ resolveDir,
+ loader: "css",
+ contents,
+ };
+ });
+ },
+};
+
+
+/**
+ * Problem:
+ * No loader is configured for ".node" files: ../../node_modules/.pnpm/fsevents@2.3.3/node_modules/fsevents/fsevents.node
+ *
+ * Reference:
+ * https://github.com/evanw/esbuild/issues/1051#issuecomment-806325487
+ */
+const nativeNodeModulesPlugin: esbuild.Plugin = {
+ name: 'native-node-modules',
+ setup(build) {
+ // If a ".node" file is imported within a module in the "file" namespace, resolve
+ // it to an absolute path and put it into the "node-file" virtual namespace.
+ build.onResolve({ filter: /\.node$/, namespace: 'file' }, args => ({
+ path: require.resolve(args.path, { paths: [args.resolveDir] }),
+ namespace: 'node-file',
+ }))
+
+ // Files in the "node-file" virtual namespace call "require()" on the
+ // path from esbuild of the ".node" file in the output directory.
+ build.onLoad({ filter: /.*/, namespace: 'node-file' }, args => ({
+ contents: `
+ import path from ${JSON.stringify(args.path)}
+ try { module.exports = require(path) }
+ catch {}
+ `,
+ }))
+
+ // If a ".node" file is imported within a module in the "node-file" namespace, put
+ // it in the "file" namespace where esbuild's default loading behavior will handle
+ // it. It is already an absolute path since we resolved it to one above.
+ build.onResolve({ filter: /\.node$/, namespace: 'node-file' }, args => ({
+ path: args.path,
+ namespace: 'file',
+ }))
+
+ // Tell esbuild's default loading behavior to use the "file" loader for
+ // these ".node" files.
+ let opts = build.initialOptions
+ opts.loader = opts.loader || {}
+ opts.loader['.node'] = 'file'
+ },
+}
+
+
+const postCssPlugin: esbuild.Plugin = {
+ name: "custom-build-postcss",
+ setup(build) {
+ build.onLoad({ filter: DEFAULT_SASS_FILTER }, async ({ path: file }) => {
+ const resolveDir = path.dirname(file);
+ const sourceBuffer = fs.readFileSync(file);
+ const source = sourceBuffer.toString("utf-8");
+ const postCssConfig = await postcssrc();
+ postCssConfig.options.from = file;
+
+ const { css: contents } = await postcss(postCssConfig.plugins).process(
+ source,
+ postCssConfig.options,
+ );
+
+ return {
+ resolveDir,
+ loader: "css",
+ contents,
+ };
+ });
+ },
+};
+
+/**
+ * This should be able to load the plugin but but some reason it does not work
+ *
+ * text: "Cannot find module '../plugins/preeval'\n" +
+ *
+ */
+function linariaPlugin() {
+ const linariaCssPlugin: esbuild.Plugin = (linaria as any)({
+ babelOptions: {
+ presets: ["@babel/preset-typescript", "@babel/preset-react", "@linaria"],
+ },
+ sourceMap: true,
+ });
+ return linariaCssPlugin;
+}
+
+const defaultEsBuildConfig: esbuild.BuildOptions = {
+ bundle: true,
+ loader: {
+ ".svg": "file",
+ ".inline.svg": "text",
+ ".png": "dataurl",
+ ".jpeg": "dataurl",
+ ".ttf": "file",
+ ".woff": "file",
+ ".woff2": "file",
+ ".eot": "file",
+ },
+ target: ["es2020"],
+ format: "esm",
+ platform: "browser",
+ jsxFactory: "h",
+ jsxFragment: "Fragment",
+ alias: {
+ react: "preact/compat",
+ "react-dom": "preact/compat",
+ },
+ define: {
+ __VERSION__: `"${_package.version}"`,
+ __GIT_HASH__: `"${GIT_HASH}"`,
+ },
+};
+
+export interface BuildParams {
+ type: "development" | "test" | "production";
+ source: {
+ assets: Assets | Assets[];
+ js: string[];
+ };
+ public?: string;
+ destination: string;
+ css: "sass" | "postcss" | "linaria";
+ linariaPlugin?: () => esbuild.Plugin;
+}
+
+export function computeConfig(params: BuildParams): esbuild.BuildOptions {
+ const plugins: Array<esbuild.Plugin> = [
+ copyFilesPlugin(params.source.assets),
+ ];
+
+ if (params.css) {
+ switch (params.css) {
+ case "sass": {
+ plugins.push(sassPlugin);
+ break;
+ }
+ case "postcss": {
+ plugins.push(postCssPlugin);
+ break;
+ }
+
+ case "linaria": {
+ if (params.linariaPlugin) {
+ plugins.push(params.linariaPlugin());
+ }
+ break;
+ }
+
+ default: {
+ const cssType: never = params.css;
+ throw Error(`not supported: ${cssType}`);
+ }
+ }
+ }
+ if (!params.type) {
+ throw Error(
+ `missing build type, it should be "test", "development" or "production"`,
+ );
+ }
+
+ // if (!params.source.js) {
+ // throw Error(`no javascript entry points, nothing to compile?`);
+ // }
+ if (!params.destination) {
+ throw Error(`missing destination folder`);
+ }
+
+ return {
+ ...defaultEsBuildConfig,
+ entryPoints: params.source.js,
+ publicPath: params.public,
+ outdir: params.destination,
+ minify: false, //params.type === "production",
+ sourcemap: true, //params.type !== "production",
+ define: {
+ ...defaultEsBuildConfig.define,
+ "process.env.NODE_ENV": JSON.stringify(params.type),
+ },
+ plugins,
+ };
+}
+
+/**
+ * Build sources for prod environment
+ */
+export function build(config: BuildParams) {
+ return esbuild.build(computeConfig(config));
+}
+
+const LIVE_RELOAD_SCRIPT =
+ "./node_modules/@gnu-taler/web-util/lib/live-reload.mjs";
+
+/**
+ * Do startup for development environment
+ */
+export function initializeDev(
+ config: BuildParams,
+): () => Promise<esbuild.BuildResult> {
+ function buildDevelopment() {
+ const result = computeConfig(config);
+ result.inject = [LIVE_RELOAD_SCRIPT];
+ return esbuild.build(result);
+ }
+ return buildDevelopment;
+}
diff --git a/packages/web-util/src/index.testing.ts b/packages/web-util/src/index.testing.ts
new file mode 100644
index 000000000..2349debc5
--- /dev/null
+++ b/packages/web-util/src/index.testing.ts
@@ -0,0 +1,3 @@
+export * from "./tests/hook.js";
+export * from "./tests/swr.js";
+export * from "./tests/mock.js";
diff --git a/packages/web-util/src/index.ts b/packages/web-util/src/index.ts
deleted file mode 100644
index ff8b4c563..000000000
--- a/packages/web-util/src/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export default {};
diff --git a/packages/web-util/src/live-reload.ts b/packages/web-util/src/live-reload.ts
index 60c7cb565..cd3a7540d 100644
--- a/packages/web-util/src/live-reload.ts
+++ b/packages/web-util/src/live-reload.ts
@@ -1,7 +1,10 @@
/* eslint-disable no-undef */
function setupLiveReload(): void {
- const ws = new WebSocket("wss://localhost:8080/ws");
+ const stopWs = localStorage.getItem("stop-ws")
+ if (!!stopWs) return;
+ const protocol = window.location.protocol === "http:" ? "ws:" : "wss:";
+ const ws = new WebSocket(`${protocol}//${window.location.hostname}:${window.location.port}/ws`);
ws.addEventListener("message", (message) => {
try {
@@ -14,6 +17,31 @@ function setupLiveReload(): void {
window.location.reload();
return;
}
+ if (event.type === "file-updated-failed") {
+ const h1 = document.getElementById("overlay-text");
+ if (h1) {
+ h1.innerHTML = "compilation failed";
+ h1.style.color = "red";
+ h1.style.margin = "";
+ }
+ const div = document.getElementById("overlay");
+ if (div) {
+ const content = JSON.stringify(event.data, undefined, 2);
+ const pre = document.createElement("pre");
+ pre.id = "error-text";
+ pre.style.margin = "";
+ pre.textContent = content;
+ div.style.backgroundColor = "rgba(0,0,0,0.8)";
+ div.style.flexDirection = "column";
+ div.appendChild(pre);
+ }
+ console.error(event.data.error);
+ return;
+ }
+ if (event.type === "file-updated") {
+ window.location.reload();
+ return;
+ }
} catch (e) {
return;
}
@@ -31,14 +59,17 @@ setupLiveReload();
function showReloadOverlay(): void {
const d = document.createElement("div");
+ d.id = "overlay";
d.style.position = "absolute";
d.style.width = "100%";
d.style.height = "100%";
d.style.color = "white";
d.style.backgroundColor = "rgba(0,0,0,0.5)";
d.style.display = "flex";
+ d.style.zIndex = String(Number.MAX_SAFE_INTEGER);
d.style.justifyContent = "center";
const h = document.createElement("h1");
+ h.id = "overlay-text";
h.style.margin = "auto";
h.innerHTML = "reloading...";
d.appendChild(h);
diff --git a/packages/web-util/src/serve.ts b/packages/web-util/src/serve.ts
index 736e57430..1daea15bf 100644
--- a/packages/web-util/src/serve.ts
+++ b/packages/web-util/src/serve.ts
@@ -2,8 +2,9 @@ import { Logger } from "@gnu-taler/taler-util";
import chokidar from "chokidar";
import express from "express";
import https from "https";
+import http from "http";
import { parse } from "url";
-import WebSocket, { Server } from "ws";
+import WebSocket from "ws";
import locahostCrt from "./keys/localhost.crt";
import locahostKey from "./keys/localhost.key";
@@ -20,7 +21,6 @@ const logger = new Logger("serve.ts");
const PATHS = {
WS: "/ws",
- NOTIFY: "/notify",
EXAMPLE: "/examples",
APP: "/app",
};
@@ -29,29 +29,38 @@ export async function serve(opts: {
folder: string;
port: number;
source?: string;
- development?: boolean;
+ tls?: boolean;
examplesLocationJs?: string;
examplesLocationCss?: string;
- onUpdate?: () => Promise<void>;
+ onSourceUpdate?: () => Promise<void>;
}): Promise<void> {
const app = express();
app.use(PATHS.APP, express.static(opts.folder));
- const server = https.createServer(httpServerOptions, app);
- server.listen(opts.port);
- logger.info(`serving ${opts.folder} on ${opts.port}`);
- logger.info(` ${PATHS.APP}: application`);
- logger.info(` ${PATHS.EXAMPLE}: examples`);
- logger.info(` ${PATHS.WS}: websocket`);
- logger.info(` ${PATHS.NOTIFY}: broadcast`);
-
- if (opts.development) {
- const wss = new Server({ noServer: true });
-
- wss.on("connection", function connection(ws) {
- ws.send("welcome");
- });
+ const httpServer = http.createServer(app);
+ const httpPort = opts.port;
+ let httpsServer: typeof httpServer | undefined;
+ let httpsPort: number | undefined;
+ const servers = [httpServer];
+ if (opts.tls) {
+ httpsServer = https.createServer(httpServerOptions, app);
+ httpsPort = opts.port + 1;
+ servers.push(httpsServer)
+ }
+
+ logger.info(`Dev server. Endpoints:`);
+ logger.info(` ${PATHS.APP}: where root application can be tested`);
+ logger.info(` ${PATHS.EXAMPLE}: where examples can be found and browse`);
+ logger.info(` ${PATHS.WS}: websocket for live reloading`);
+
+ const wss = new WebSocket.Server({ noServer: true });
+
+ wss.on("connection", function connection(ws) {
+ ws.send("welcome");
+ });
+
+ servers.forEach(function addWSHandler(server) {
server.on("upgrade", function upgrade(request, socket, head) {
const { pathname } = parse(request.url || "");
if (pathname === PATHS.WS) {
@@ -62,50 +71,63 @@ export async function serve(opts: {
socket.destroy();
}
});
+ });
- const sendToAllClients = function (data: object): void {
- wss.clients.forEach(function each(client) {
- if (client.readyState === WebSocket.OPEN) {
- client.send(JSON.stringify(data));
- }
- });
- };
- const watchingFolder = opts.source ?? opts.folder;
- logger.info(`watching ${watchingFolder} for change`);
+ const sendToAllClients = function (data: object): void {
+ wss.clients.forEach(function each(client) {
+ if (client.readyState === WebSocket.OPEN) {
+ client.send(JSON.stringify(data));
+ }
+ });
+ };
+ const watchingFolder = opts.source ?? opts.folder;
+ logger.info(`watching ${watchingFolder} for changes`);
- chokidar.watch(watchingFolder).on("change", (path, stats) => {
- logger.trace(`changed ${path}`);
+ chokidar.watch(watchingFolder).on("change", (path, stats) => {
+ logger.info(`changed: ${path}`);
+ if (opts.onSourceUpdate) {
sendToAllClients({ type: "file-updated-start", data: { path } });
- if (opts.onUpdate) {
- opts.onUpdate().then((result) => {
+ opts
+ .onSourceUpdate()
+ .then((result) => {
sendToAllClients({
type: "file-updated-done",
data: { path, result },
});
+ })
+ .catch((error) => {
+ sendToAllClients({
+ type: "file-updated-failed",
+ data: { path, error: JSON.stringify(error) },
+ });
});
- } else {
- sendToAllClients({ type: "file-change-done", data: { path } });
- }
- });
+ } else {
+ sendToAllClients({ type: "file-change", data: { path } });
+ }
+ });
- app.get(PATHS.EXAMPLE, function (req: any, res: any) {
- res.set("Content-Type", "text/html");
- res.send(
- storiesHtml
- .replace(
- "__EXAMPLES_JS_FILE_LOCATION__",
- opts.examplesLocationJs ?? `.${PATHS.APP}/stories.js`,
- )
- .replace(
- "__EXAMPLES_CSS_FILE_LOCATION__",
- opts.examplesLocationCss ?? `.${PATHS.APP}/stories.css`,
- ),
- );
- });
+ if (opts.onSourceUpdate) opts.onSourceUpdate();
- app.get(PATHS.NOTIFY, function (req: any, res: any) {
- res.send("ok");
- });
+ app.get(PATHS.EXAMPLE, function (req: any, res: any) {
+ res.set("Content-Type", "text/html");
+ res.send(
+ storiesHtml
+ .replace(
+ "__EXAMPLES_JS_FILE_LOCATION__",
+ opts.examplesLocationJs ?? `.${PATHS.APP}/stories.js`,
+ )
+ .replace(
+ "__EXAMPLES_CSS_FILE_LOCATION__",
+ opts.examplesLocationCss ?? `.${PATHS.APP}/stories.css`,
+ ),
+ );
+ });
+
+ logger.info(`Serving ${opts.folder} on ${httpPort}: plain HTTP`);
+ httpServer.listen(httpPort);
+ if (httpsServer !== undefined) {
+ logger.info(`Serving ${opts.folder} on ${httpsPort}: HTTP + TLS`);
+ httpsServer.listen(httpsPort);
}
}
diff --git a/packages/web-util/src/stories.tsx b/packages/web-util/src/stories.tsx
index a8a9fdf77..d9c2406eb 100644
--- a/packages/web-util/src/stories.tsx
+++ b/packages/web-util/src/stories.tsx
@@ -19,7 +19,6 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
import { setupI18n } from "@gnu-taler/taler-util";
-import e from "express";
import {
ComponentChild,
ComponentChildren,
@@ -32,6 +31,7 @@ import {
VNode,
} from "preact";
import { useEffect, useErrorBoundary, useState } from "preact/hooks";
+import { ExampleItemSetup } from "./tests/hook.js";
const Page: FunctionalComponent = ({ children }): VNode => {
return (
@@ -184,8 +184,8 @@ function ExampleList({
backgroundColor: isSelected
? "green"
: i % 2
- ? "lightgray"
- : "lightblue",
+ ? "lightgray"
+ : "lightblue",
marginLeft: "1em",
padding: 4,
cursor: "pointer",
@@ -323,6 +323,7 @@ function parseExampleImport(
render: {
component: exampleValue as FunctionComponent,
props: {},
+ contextProps: {},
},
};
}
@@ -367,19 +368,16 @@ export interface Group {
list: ComponentItem[];
}
-export interface ComponentItem {
+export interface ComponentItem<Props extends object = {}> {
name: string;
- examples: ExampleItem[];
+ examples: ExampleItem<Props>[];
}
-export interface ExampleItem {
+export interface ExampleItem<Props extends object = {}> {
group: string;
component: string;
name: string;
- render: {
- component: FunctionalComponent;
- props: object;
- };
+ render: ExampleItemSetup<Props>;
}
type ComponentOrFolder = MaybeComponent | MaybeFolder;
@@ -397,10 +395,10 @@ function folder(groupName: string, value: ComponentOrFolder): ComponentItem[] {
try {
title =
typeof value === "object" &&
- typeof value.default === "object" &&
- value.default !== undefined &&
- "title" in value.default &&
- typeof value.default.title === "string"
+ typeof value.default === "object" &&
+ value.default !== undefined &&
+ "title" in value.default &&
+ typeof value.default.title === "string"
? value.default.title
: undefined;
} catch (e) {
@@ -432,12 +430,12 @@ function Application({
examplesInGroups,
getWrapperForGroup,
}: Props): VNode {
+ const url = new URL(window.location.href);
const initialSelection = getSelectionFromLocationHash(
- location.hash,
+ url.hash,
examplesInGroups,
);
- const url = new URL(window.location.href);
const currentLang = url.searchParams.get("lang") || "en";
if (!langs["en"]) {
@@ -450,15 +448,15 @@ function Application({
);
const [sidebarWidth, setSidebarWidth] = useState(200);
useEffect(() => {
- if (location.hash) {
- const hash = location.hash.substring(1);
+ if (url.hash) {
+ const hash = url.hash.substring(1);
const found = document.getElementById(hash);
if (found) {
setTimeout(() => {
found.scrollIntoView({
block: "center",
});
- }, 10);
+ }, 50);
}
}
}, []);
@@ -502,11 +500,11 @@ function Application({
))}
<hr />
</SideBar>
- <ResizeHandle
+ {/* <ResizeHandle
onUpdate={(x) => {
setSidebarWidth((s) => s + x);
}}
- />
+ /> */}
<Content>
<ErrorReport selected={selected}>
<PreventLinkNavigation>
diff --git a/packages/web-util/src/tests/hook.ts b/packages/web-util/src/tests/hook.ts
new file mode 100644
index 000000000..59f17ba8d
--- /dev/null
+++ b/packages/web-util/src/tests/hook.ts
@@ -0,0 +1,325 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ Fragment,
+ FunctionComponent,
+ FunctionalComponent,
+ VNode,
+ h as create,
+ options,
+ render as renderIntoDom,
+} from "preact";
+import { render as renderToString } from "preact-render-to-string";
+
+// This library is expected to be included in testing environment only
+// When doing tests we want the requestAnimationFrame to be as fast as possible.
+// without this option the RAF will timeout after 100ms making the tests slower
+options.requestAnimationFrame = (fn: () => void) => {
+ return fn();
+};
+
+export type ExampleItemSetup<Props extends object = {}> = {
+ component: FunctionalComponent<Props>;
+ props: Props;
+ contextProps: object;
+};
+
+/**
+ *
+ * @param Component component to be tested
+ * @param props allow partial props for easier example setup
+ * @param contextProps if the context requires params for this example
+ * @returns
+ */
+export function createExample<T extends object, Props extends object>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props> | (() => Partial<Props>),
+ contextProps?: T | (() => T),
+): ExampleItemSetup<Props> {
+ const evaluatedProps = typeof props === "function" ? props() : props;
+ const Render = (args: any): VNode => create(Component, args);
+ const evaluatedContextProps =
+ typeof contextProps === "function" ? contextProps() : contextProps;
+ return {
+ component: Render,
+ props: evaluatedProps as Props,
+ contextProps: !evaluatedContextProps ? {} : evaluatedContextProps,
+ };
+}
+
+/**
+ * Should render HTML on node and browser
+ * Browser: mount update and unmount
+ * Node: render to string
+ *
+ * @param Component
+ * @param args
+ */
+export function renderUI(example: ExampleItemSetup<any>, Context?: any): void {
+ const vdom = !Context
+ ? create(example.component, example.props)
+ : create(Context, {
+ ...example.contextProps,
+ children: [create(example.component, example.props)],
+ });
+
+ if (typeof window === "undefined") {
+ renderToString(vdom);
+ } else {
+ const div = document.createElement("div");
+ document.body.appendChild(div);
+ renderIntoDom(vdom, div);
+ renderIntoDom(null, div);
+ document.body.removeChild(div);
+ }
+}
+
+/**
+ * No need to render.
+ * Should mount, update and run effects.
+ *
+ * Browser: mount update and unmount
+ * Node: mount on a mock virtual dom
+ *
+ * Mounting hook doesn't use DOM api so is
+ * safe to use normal mounting api in node
+ *
+ * @param Component
+ * @param props
+ * @param Context
+ */
+function renderHook(
+ Component: FunctionComponent,
+ Context?: ({ children }: { children: any }) => VNode | null,
+): void {
+ const vdom = !Context
+ ? create(Component, {})
+ : create(Context, { children: [create(Component, {})] });
+
+ //use normal mounting API since we expect
+ //useEffect to be called ( and similar APIs )
+ renderIntoDom(vdom, {} as Element);
+}
+
+type RecursiveState<S> = S | (() => RecursiveState<S>);
+
+interface Mounted<T> {
+ pullLastResultOrThrow: () => Exclude<T, VoidFunction>;
+ assertNoPendingUpdate: () => Promise<boolean>;
+ waitForStateUpdate: () => Promise<boolean>;
+}
+
+/**
+ * Manual API mount the hook and return testing API
+ * Consider using hookBehaveLikeThis() function
+ *
+ * @param hookToBeTested
+ * @param Context
+ *
+ * @returns testing API
+ */
+function mountHook<T extends object>(
+ hookToBeTested: () => RecursiveState<T>,
+ Context?: ({ children }: { children: any }) => VNode | null,
+): Mounted<T> {
+ let lastResult: Exclude<T, VoidFunction> | Error | null = null;
+
+ const listener: Array<() => void> = [];
+
+ // component that's going to hold the hook
+ function Component(): VNode {
+ try {
+ let componentOrResult = hookToBeTested();
+
+ // special loop
+ // since Taler use a special type of hook that can return
+ // a function and it will be treated as a composed component
+ // then tests should be aware of it and reproduce the same behavior
+ while (typeof componentOrResult === "function") {
+ componentOrResult = componentOrResult();
+ }
+ //typecheck fails here
+ const l: Exclude<T, () => void> = componentOrResult as any;
+ lastResult = l;
+ } catch (e) {
+ if (e instanceof Error) {
+ lastResult = e;
+ } else {
+ lastResult = new Error(`mounting the hook throw an exception: ${e}`);
+ }
+ }
+
+ // notify to everyone waiting for an update and clean the queue
+ listener.splice(0, listener.length).forEach((cb) => cb());
+ return create(Fragment, {});
+ }
+
+ renderHook(Component, Context);
+
+ function pullLastResult(): Exclude<T | Error | null, VoidFunction> {
+ const copy: Exclude<T | Error | null, VoidFunction> = lastResult;
+ lastResult = null;
+ return copy;
+ }
+
+ function pullLastResultOrThrow(): Exclude<T, VoidFunction> {
+ const r = pullLastResult();
+ if (r instanceof Error) throw r;
+ //sanity check
+ if (!r) throw Error("there was no last result");
+ return r;
+ }
+
+ async function assertNoPendingUpdate(): Promise<boolean> {
+ await new Promise((res, rej) => {
+ const tid = setTimeout(() => {
+ res(true);
+ }, 10);
+
+ listener.push(() => {
+ clearTimeout(tid);
+ res(false);
+ // Error(`Expecting no pending result but the hook got updated.
+ // If the update was not intended you need to check the hook dependencies
+ // (or dependencies of the internal state) but otherwise make
+ // sure to consume the result before ending the test.`),
+ // );
+ });
+ });
+
+ const r = pullLastResult();
+ if (r) {
+ return Promise.resolve(false);
+ }
+ return Promise.resolve(true);
+ // This may happen because the hook did a new update but the test didn't consume the result using pullLastResult`);
+ }
+ async function waitForStateUpdate(): Promise<boolean> {
+ return await new Promise((res, rej) => {
+ const tid = setTimeout(() => {
+ res(false);
+ }, 10);
+
+ listener.push(() => {
+ clearTimeout(tid);
+ res(true);
+ });
+ });
+ }
+
+ return {
+ pullLastResultOrThrow,
+ waitForStateUpdate,
+ assertNoPendingUpdate,
+ };
+}
+
+export const nullFunction = (): void => {
+ null;
+};
+export const nullAsyncFunction = (): Promise<void> => {
+ return Promise.resolve();
+};
+
+type HookTestResult = HookTestResultOk | HookTestResultError;
+
+interface HookTestResultOk {
+ result: "ok";
+}
+interface HookTestResultError {
+ result: "fail";
+ error: string;
+ index: number;
+}
+
+/**
+ * Main testing driver.
+ * It will assert that there are no more and no less hook updates than expected.
+ *
+ * @param hookFunction hook function to be tested
+ * @param props initial props for the hook
+ * @param checks step by step state validation
+ * @param Context additional testing context for overrides
+ *
+ * @returns testing result, should also be checked to be "ok"
+ */
+// eslint-disable-next-line @typescript-eslint/ban-types
+export async function hookBehaveLikeThis<T extends object, PropsType>(
+ hookFunction: (p: PropsType) => RecursiveState<T>,
+ props: PropsType,
+ checks: Array<(state: Exclude<T, VoidFunction>) => void>,
+ Context?: ({ children }: { children: any }) => VNode | null,
+): Promise<HookTestResult> {
+ const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
+ mountHook<T>(() => hookFunction(props), Context);
+
+ const [firstCheck, ...restOfTheChecks] = checks;
+ {
+ const state = pullLastResultOrThrow();
+ const checkError = firstCheck(state);
+ if (checkError !== undefined) {
+ return {
+ result: "fail",
+ index: 0,
+ error: `First check returned with error: ${checkError}`,
+ };
+ }
+ }
+
+ let index = 1;
+ for (const check of restOfTheChecks) {
+ const hasNext = await waitForStateUpdate();
+ if (!hasNext) {
+ return {
+ result: "fail",
+ error: "Component didn't update and the test expected one more state",
+ index,
+ };
+ }
+ const state = pullLastResultOrThrow();
+ const checkError = check(state);
+ if (checkError !== undefined) {
+ return {
+ result: "fail",
+ index,
+ error: `Check returned with error: ${checkError}`,
+ };
+ }
+ index++;
+ }
+
+ const hasNext = await waitForStateUpdate();
+ if (hasNext) {
+ return {
+ result: "fail",
+ index,
+ error: "Component updated and test didn't expect more states",
+ };
+ }
+ const noMoreUpdates = await assertNoPendingUpdate();
+ if (noMoreUpdates === false) {
+ return {
+ result: "fail",
+ index,
+ error: "Component was updated but the test does not cover the update",
+ };
+ }
+
+ return {
+ result: "ok",
+ };
+}
diff --git a/packages/web-util/src/tests/mock.ts b/packages/web-util/src/tests/mock.ts
new file mode 100644
index 000000000..d09e8b4a6
--- /dev/null
+++ b/packages/web-util/src/tests/mock.ts
@@ -0,0 +1,503 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { Logger } from "@gnu-taler/taler-util";
+
+type HttpMethod =
+ | "get"
+ | "GET"
+ | "delete"
+ | "DELETE"
+ | "head"
+ | "HEAD"
+ | "options"
+ | "OPTIONS"
+ | "post"
+ | "POST"
+ | "put"
+ | "PUT"
+ | "patch"
+ | "PATCH"
+ | "purge"
+ | "PURGE"
+ | "link"
+ | "LINK"
+ | "unlink"
+ | "UNLINK";
+
+/**
+ * @deprecated do not use it, it will be removed
+ */
+export type Query<Req, Res> = {
+ method: HttpMethod;
+ url: string;
+ code?: number;
+};
+
+type ExpectationValues = {
+ query: Query<any, any>;
+ auth?: string;
+ params?: {
+ // eslint-disable-next-line @typescript-eslint/ban-types
+ request?: object;
+ qparam?: Record<string, string>;
+ // eslint-disable-next-line @typescript-eslint/ban-types
+ response?: object;
+ };
+};
+
+type TestValues = {
+ currentExpectedQuery: ExpectationValues | undefined;
+ lastQuery: ExpectationValues | undefined;
+};
+
+const logger = new Logger("testing/mock.ts");
+
+type MockedResponse = {
+ queryMade: ExpectationValues;
+ expectedQuery?: ExpectationValues;
+};
+
+/**
+ * @deprecated do not use it, it will be removed
+ */
+export abstract class MockEnvironment {
+ expectations: Array<ExpectationValues> = [];
+ queriesMade: Array<ExpectationValues> = [];
+ index = 0;
+
+ debug: boolean;
+ constructor(debug: boolean) {
+ this.debug = debug;
+ this.saveRequestAndGetMockedResponse.bind(this);
+ }
+
+ public addRequestExpectation<
+ // eslint-disable-next-line @typescript-eslint/ban-types
+ RequestType extends object,
+ // eslint-disable-next-line @typescript-eslint/ban-types
+ ResponseType extends object,
+ >(
+ query: Query<RequestType, ResponseType>,
+ params: {
+ auth?: string;
+ request?: RequestType;
+ qparam?: any;
+ response?: ResponseType;
+ },
+ ): void {
+ const expected = { query, params, auth: params.auth };
+ this.expectations.push(expected);
+ if (this.debug) {
+ logger.info("saving query as expected", expected);
+ }
+ }
+
+ public saveRequestAndGetMockedResponse<
+ // eslint-disable-next-line @typescript-eslint/ban-types
+ RequestType extends object,
+ // eslint-disable-next-line @typescript-eslint/ban-types
+ ResponseType extends object,
+ >(
+ query: Query<RequestType, ResponseType>,
+ params: {
+ auth?: string;
+ request?: RequestType;
+ qparam?: any;
+ response?: ResponseType;
+ },
+ ): MockedResponse {
+ const queryMade = { query, params, auth: params.auth };
+ this.queriesMade.push(queryMade);
+ const expectedQuery = this.expectations[this.index];
+ if (!expectedQuery) {
+ if (this.debug) {
+ logger.info("unexpected query made", queryMade);
+ }
+ return { queryMade };
+ }
+
+ if (this.debug) {
+ logger.info("tracking query made", {
+ queryMade,
+ expectedQuery,
+ });
+ }
+ this.index++;
+ return { queryMade, expectedQuery };
+ }
+
+ public assertJustExpectedRequestWereMade(): AssertStatus {
+ let queryNumber = 0;
+
+ while (queryNumber < this.expectations.length) {
+ const r = this.assertNextRequest(queryNumber);
+ if (r.result !== "ok") return r;
+ queryNumber++;
+ }
+ return this.assertNoMoreRequestWereMade(queryNumber);
+ }
+
+ private getLastTestValues(idx: number): TestValues {
+ const currentExpectedQuery = this.expectations[idx];
+ const lastQuery = this.queriesMade[idx];
+
+ return { currentExpectedQuery, lastQuery };
+ }
+
+ private assertNoMoreRequestWereMade(idx: number): AssertStatus {
+ const { currentExpectedQuery, lastQuery } = this.getLastTestValues(idx);
+
+ if (lastQuery !== undefined) {
+ return {
+ result: "error-did-one-more",
+ made: lastQuery,
+ };
+ }
+ if (currentExpectedQuery !== undefined) {
+ return {
+ result: "error-did-one-less",
+ expected: currentExpectedQuery,
+ };
+ }
+
+ return {
+ result: "ok",
+ };
+ }
+
+ private assertNextRequest(index: number): AssertStatus {
+ const { currentExpectedQuery, lastQuery } = this.getLastTestValues(index);
+
+ if (!currentExpectedQuery) {
+ return {
+ result: "error-query-missing",
+ };
+ }
+
+ if (!lastQuery) {
+ return {
+ result: "error-did-one-less",
+ expected: currentExpectedQuery,
+ };
+ }
+
+ if (lastQuery.query.method) {
+ if (currentExpectedQuery.query.method !== lastQuery.query.method) {
+ return {
+ result: "error-difference",
+ diff: "method",
+ last: lastQuery.query.method,
+ expected: currentExpectedQuery.query.method,
+ index,
+ };
+ }
+ if (currentExpectedQuery.query.url !== lastQuery.query.url) {
+ return {
+ result: "error-difference",
+ diff: "url",
+ last: lastQuery.query.url,
+ expected: currentExpectedQuery.query.url,
+ index,
+ };
+ }
+ }
+ if (
+ !deepEquals(
+ currentExpectedQuery.params?.request,
+ lastQuery.params?.request,
+ )
+ ) {
+ return {
+ result: "error-difference",
+ diff: "query-body",
+ expected: currentExpectedQuery.params?.request,
+ last: lastQuery.params?.request,
+ index,
+ };
+ }
+ if (
+ !deepEquals(currentExpectedQuery.params?.qparam, lastQuery.params?.qparam)
+ ) {
+ return {
+ result: "error-difference",
+ diff: "query-params",
+ expected: currentExpectedQuery.params?.qparam,
+ last: lastQuery.params?.qparam,
+ index,
+ };
+ }
+ if (!deepEquals(currentExpectedQuery.auth, lastQuery.auth)) {
+ return {
+ result: "error-difference",
+ diff: "query-auth",
+ expected: currentExpectedQuery.auth,
+ last: lastQuery.auth,
+ index,
+ };
+ }
+
+ return {
+ result: "ok",
+ };
+ }
+}
+
+type AssertStatus =
+ | AssertOk
+ | AssertQueryNotMadeButExpected
+ | AssertQueryMadeButNotExpected
+ | AssertQueryMissing
+ | AssertExpectedQueryMethodMismatch
+ | AssertExpectedQueryUrlMismatch
+ | AssertExpectedQueryAuthMismatch
+ | AssertExpectedQueryBodyMismatch
+ | AssertExpectedQueryParamsMismatch;
+
+interface AssertOk {
+ result: "ok";
+}
+
+//trying to assert for a expected query but there is
+//no expected query in the queue
+interface AssertQueryMissing {
+ result: "error-query-missing";
+}
+
+//tested component did one more query that expected
+interface AssertQueryNotMadeButExpected {
+ result: "error-did-one-more";
+ made: ExpectationValues;
+}
+
+//tested component didn't make an expected query
+interface AssertQueryMadeButNotExpected {
+ result: "error-did-one-less";
+ expected: ExpectationValues;
+}
+
+interface AssertExpectedQueryMethodMismatch {
+ result: "error-difference";
+ diff: "method";
+ last: string;
+ expected: string;
+ index: number;
+}
+interface AssertExpectedQueryUrlMismatch {
+ result: "error-difference";
+ diff: "url";
+ last: string;
+ expected: string;
+ index: number;
+}
+interface AssertExpectedQueryAuthMismatch {
+ result: "error-difference";
+ diff: "query-auth";
+ last: string | undefined;
+ expected: string | undefined;
+ index: number;
+}
+interface AssertExpectedQueryBodyMismatch {
+ result: "error-difference";
+ diff: "query-body";
+ last: any;
+ expected: any;
+ index: number;
+}
+interface AssertExpectedQueryParamsMismatch {
+ result: "error-difference";
+ diff: "query-params";
+ last: any;
+ expected: any;
+ index: number;
+}
+
+/**
+ * helpers
+ *
+ */
+export type Tester = (a: any, b: any) => boolean | undefined;
+
+function deepEquals(
+ a: unknown,
+ b: unknown,
+ aStack: Array<unknown> = [],
+ bStack: Array<unknown> = [],
+): boolean {
+ //one if the element is null or undefined
+ if (a === null || b === null || b === undefined || a === undefined) {
+ return a === b;
+ }
+ //both are errors
+ if (a instanceof Error && b instanceof Error) {
+ return a.message == b.message;
+ }
+ //is the same object
+ if (Object.is(a, b)) {
+ return true;
+ }
+ //both the same class
+ const name = Object.prototype.toString.call(a);
+ if (name != Object.prototype.toString.call(b)) {
+ return false;
+ }
+ //
+ switch (name) {
+ case "[object Boolean]":
+ case "[object String]":
+ case "[object Number]":
+ if (typeof a !== typeof b) {
+ // One is a primitive, one a `new Primitive()`
+ return false;
+ } else if (typeof a !== "object" && typeof b !== "object") {
+ // both are proper primitives
+ return Object.is(a, b);
+ } else {
+ // both are `new Primitive()`s
+ return Object.is(a.valueOf(), b.valueOf());
+ }
+ case "[object Date]": {
+ const _a = a as Date;
+ const _b = b as Date;
+ return _a == _b;
+ }
+ case "[object RegExp]": {
+ const _a = a as RegExp;
+ const _b = b as RegExp;
+ return _a.source === _b.source && _a.flags === _b.flags;
+ }
+ case "[object Array]": {
+ const _a = a as Array<any>;
+ const _b = b as Array<any>;
+ if (_a.length !== _b.length) {
+ return false;
+ }
+ }
+ }
+ if (typeof a !== "object" || typeof b !== "object") {
+ return false;
+ }
+
+ if (
+ typeof a === "object" &&
+ typeof b === "object" &&
+ !Array.isArray(a) &&
+ !Array.isArray(b) &&
+ hasIterator(a) &&
+ hasIterator(b)
+ ) {
+ return iterable(a, b);
+ }
+
+ // Used to detect circular references.
+ let length = aStack.length;
+ while (length--) {
+ if (aStack[length] === a) {
+ return bStack[length] === b;
+ } else if (bStack[length] === b) {
+ return false;
+ }
+ }
+ aStack.push(a);
+ bStack.push(b);
+
+ const aKeys = allKeysFromObject(a);
+ const bKeys = allKeysFromObject(b);
+ let keySize = aKeys.length;
+
+ //same number of keys
+ if (bKeys.length !== keySize) {
+ return false;
+ }
+
+ let keyIterator: string;
+ while (keySize--) {
+ // eslint-disable-next-line @typescript-eslint/ban-types
+ const _a = a as Record<string, object>;
+ // eslint-disable-next-line @typescript-eslint/ban-types
+ const _b = b as Record<string, object>;
+
+ keyIterator = aKeys[keySize];
+
+ const de = deepEquals(_a[keyIterator], _b[keyIterator], aStack, bStack);
+ if (!de) {
+ return false;
+ }
+ }
+
+ aStack.pop();
+ bStack.pop();
+
+ return true;
+}
+
+// eslint-disable-next-line @typescript-eslint/ban-types
+function allKeysFromObject(obj: object): Array<string> {
+ const keys = [];
+ for (const key in obj) {
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
+ keys.push(key);
+ }
+ }
+ return keys;
+}
+
+const IteratorSymbol = Symbol.iterator;
+
+function hasIterator(object: any): boolean {
+ return !!(object != null && object[IteratorSymbol]);
+}
+
+function iterable(
+ a: unknown,
+ b: unknown,
+ aStack: Array<unknown> = [],
+ bStack: Array<unknown> = [],
+): boolean {
+ if (a === null || b === null || b === undefined || a === undefined) {
+ return a === b;
+ }
+ if (a.constructor !== b.constructor) {
+ return false;
+ }
+ let length = aStack.length;
+ while (length--) {
+ if (aStack[length] === a) {
+ return bStack[length] === b;
+ }
+ }
+ aStack.push(a);
+ bStack.push(b);
+
+ const aIterator = (a as any)[IteratorSymbol]();
+ const bIterator = (b as any)[IteratorSymbol]();
+
+ const nextA = aIterator.next();
+ while (nextA.done) {
+ const nextB = bIterator.next();
+ if (nextB.done || !deepEquals(nextA.value, nextB.value)) {
+ return false;
+ }
+ }
+ if (!bIterator.next().done) {
+ return false;
+ }
+
+ // Remove the first value from the stack of traversed values.
+ aStack.pop();
+ bStack.pop();
+ return true;
+}
diff --git a/packages/web-util/src/tests/swr.ts b/packages/web-util/src/tests/swr.ts
new file mode 100644
index 000000000..d5f4341f3
--- /dev/null
+++ b/packages/web-util/src/tests/swr.ts
@@ -0,0 +1,105 @@
+/*
+ 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/>
+ */
+
+import { ComponentChildren, FunctionalComponent, h, VNode } from "preact";
+import { MockEnvironment } from "./mock.js";
+import { SWRConfig } from "swr";
+import * as swr__internal from "swr/_internal";
+import { Logger } from "@gnu-taler/taler-util";
+import { buildRequestFailed, RequestError } from "../index.browser.js";
+
+const logger = new Logger("tests/swr.ts");
+
+/**
+ * Helper for hook that use SWR inside.
+ *
+ * buildTestingContext() will return a testing context
+ *
+ * @deprecated do not use it, it will be removed
+ */
+export class SwrMockEnvironment extends MockEnvironment {
+ constructor(debug = false) {
+ super(debug);
+ }
+
+ public buildTestingContext(): FunctionalComponent<{
+ children: ComponentChildren;
+ }> {
+ const __SAVE_REQUEST_AND_GET_MOCKED_RESPONSE =
+ this.saveRequestAndGetMockedResponse.bind(this);
+
+ function testingFetcher(params: any): any {
+ const url = JSON.stringify(params);
+ const mocked = __SAVE_REQUEST_AND_GET_MOCKED_RESPONSE<any, any>(
+ {
+ method: "get",
+ url,
+ },
+ {},
+ );
+
+ //unexpected query
+ if (!mocked.expectedQuery) return undefined;
+ const status = mocked.expectedQuery.query.code ?? 200;
+ const requestPayload = mocked.expectedQuery.params?.request;
+ const responsePayload = mocked.expectedQuery.params?.response;
+ //simulated error
+ if (status >= 400) {
+ const error = buildRequestFailed(
+ url,
+ JSON.stringify(responsePayload),
+ status,
+ requestPayload,
+ );
+ //example error handling from https://swr.vercel.app/docs/error-handling
+ throw new RequestError(error);
+ }
+ return responsePayload;
+ }
+
+ const value: Partial<swr__internal.PublicConfiguration> & {
+ provider: () => Map<any, any>;
+ } = {
+ use: [
+ (useSWRNext) => {
+ return (key, fetcher, config) => {
+ //prevent the request
+ //use the testing fetcher instead
+ return useSWRNext(key, testingFetcher, config);
+ };
+ },
+ ],
+ fetcher: testingFetcher,
+ //These options are set for ending the test faster
+ //otherwise SWR will create timeouts that will live after the test finished
+ loadingTimeout: 0,
+ dedupingInterval: 0,
+ shouldRetryOnError: false,
+ errorRetryInterval: 0,
+ errorRetryCount: 0,
+ //clean cache for every test
+ provider: () => new Map(),
+ };
+
+ return function TestingContext({
+ children,
+ }: {
+ children: ComponentChildren;
+ }): VNode {
+ return h(SWRConfig, { value }, children);
+ };
+ }
+}
diff --git a/packages/web-util/src/utils/base64.ts b/packages/web-util/src/utils/base64.ts
new file mode 100644
index 000000000..0e075880f
--- /dev/null
+++ b/packages/web-util/src/utils/base64.ts
@@ -0,0 +1,243 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received 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 function base64encode(str: string): string {
+ return base64EncArr(strToUTF8Arr(str))
+}
+
+export function base64decode(str: string): string {
+ return UTF8ArrToStr(base64DecToArr(str))
+}
+
+// from https://developer.mozilla.org/en-US/docs/Glossary/Base64
+
+// Array of bytes to Base64 string decoding
+function b64ToUint6(nChr: number): number {
+ return nChr > 64 && nChr < 91
+ ? nChr - 65
+ : nChr > 96 && nChr < 123
+ ? nChr - 71
+ : nChr > 47 && nChr < 58
+ ? nChr + 4
+ : nChr === 43
+ ? 62
+ : nChr === 47
+ ? 63
+ : 0;
+}
+
+function base64DecToArr(sBase64: string, nBlocksSize?: number): Uint8Array {
+ const sB64Enc = sBase64.replace(/[^A-Za-z0-9+/]/g, ""); // Only necessary if the base64 includes whitespace such as line breaks.
+ const nInLen = sB64Enc.length;
+ const nOutLen = nBlocksSize
+ ? Math.ceil(((nInLen * 3 + 1) >> 2) / nBlocksSize) * nBlocksSize
+ : (nInLen * 3 + 1) >> 2;
+ const taBytes = new Uint8Array(nOutLen);
+
+ let nMod3;
+ let nMod4;
+ let nUint24 = 0;
+ let nOutIdx = 0;
+ for (let nInIdx = 0; nInIdx < nInLen; nInIdx++) {
+ nMod4 = nInIdx & 3;
+ nUint24 |= b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << (6 * (3 - nMod4));
+ if (nMod4 === 3 || nInLen - nInIdx === 1) {
+ nMod3 = 0;
+ while (nMod3 < 3 && nOutIdx < nOutLen) {
+ taBytes[nOutIdx] = (nUint24 >>> ((16 >>> nMod3) & 24)) & 255;
+ nMod3++;
+ nOutIdx++;
+ }
+ nUint24 = 0;
+ }
+ }
+
+ return taBytes;
+}
+
+/* Base64 string to array encoding */
+function uint6ToB64(nUint6: number): number {
+ return nUint6 < 26
+ ? nUint6 + 65
+ : nUint6 < 52
+ ? nUint6 + 71
+ : nUint6 < 62
+ ? nUint6 - 4
+ : nUint6 === 62
+ ? 43
+ : nUint6 === 63
+ ? 47
+ : 65;
+}
+
+function base64EncArr(aBytes: Uint8Array): string {
+ let nMod3 = 2;
+ let sB64Enc = "";
+
+ const nLen = aBytes.length;
+ let nUint24 = 0;
+ for (let nIdx = 0; nIdx < nLen; nIdx++) {
+ nMod3 = nIdx % 3;
+ // To break your base64 into several 80-character lines, add:
+ // if (nIdx > 0 && ((nIdx * 4) / 3) % 76 === 0) {
+ // sB64Enc += "\r\n";
+ // }
+
+ nUint24 |= aBytes[nIdx] << ((16 >>> nMod3) & 24);
+ if (nMod3 === 2 || aBytes.length - nIdx === 1) {
+ sB64Enc += String.fromCodePoint(
+ uint6ToB64((nUint24 >>> 18) & 63),
+ uint6ToB64((nUint24 >>> 12) & 63),
+ uint6ToB64((nUint24 >>> 6) & 63),
+ uint6ToB64(nUint24 & 63)
+ );
+ nUint24 = 0;
+ }
+ }
+ return (
+ sB64Enc.substring(0, sB64Enc.length - 2 + nMod3) +
+ (nMod3 === 2 ? "" : nMod3 === 1 ? "=" : "==")
+ );
+}
+
+/* UTF-8 array to JS string and vice versa */
+
+function UTF8ArrToStr(aBytes: Uint8Array): string {
+ let sView = "";
+ let nPart;
+ const nLen = aBytes.length;
+ for (let nIdx = 0; nIdx < nLen; nIdx++) {
+ nPart = aBytes[nIdx];
+ sView += String.fromCodePoint(
+ nPart > 251 && nPart < 254 && nIdx + 5 < nLen /* six bytes */
+ ? /* (nPart - 252 << 30) may be not so safe in ECMAScript! So…: */
+ (nPart - 252) * 1073741824 +
+ ((aBytes[++nIdx] - 128) << 24) +
+ ((aBytes[++nIdx] - 128) << 18) +
+ ((aBytes[++nIdx] - 128) << 12) +
+ ((aBytes[++nIdx] - 128) << 6) +
+ aBytes[++nIdx] -
+ 128
+ : nPart > 247 && nPart < 252 && nIdx + 4 < nLen /* five bytes */
+ ? ((nPart - 248) << 24) +
+ ((aBytes[++nIdx] - 128) << 18) +
+ ((aBytes[++nIdx] - 128) << 12) +
+ ((aBytes[++nIdx] - 128) << 6) +
+ aBytes[++nIdx] -
+ 128
+ : nPart > 239 && nPart < 248 && nIdx + 3 < nLen /* four bytes */
+ ? ((nPart - 240) << 18) +
+ ((aBytes[++nIdx] - 128) << 12) +
+ ((aBytes[++nIdx] - 128) << 6) +
+ aBytes[++nIdx] -
+ 128
+ : nPart > 223 && nPart < 240 && nIdx + 2 < nLen /* three bytes */
+ ? ((nPart - 224) << 12) +
+ ((aBytes[++nIdx] - 128) << 6) +
+ aBytes[++nIdx] -
+ 128
+ : nPart > 191 && nPart < 224 && nIdx + 1 < nLen /* two bytes */
+ ? ((nPart - 192) << 6) + aBytes[++nIdx] - 128
+ : /* nPart < 127 ? */ /* one byte */
+ nPart
+ );
+ }
+ return sView;
+}
+
+function strToUTF8Arr(sDOMStr: string): Uint8Array {
+ let nChr;
+ const nStrLen = sDOMStr.length;
+ let nArrLen = 0;
+
+ /* mapping… */
+ for (let nMapIdx = 0; nMapIdx < nStrLen; nMapIdx++) {
+ nChr = sDOMStr.codePointAt(nMapIdx);
+ if (nChr === undefined) {
+ throw Error(`No char at ${nMapIdx} on string with length: ${sDOMStr.length}`)
+ }
+
+ if (nChr >= 0x10000) {
+ nMapIdx++;
+ }
+
+ nArrLen +=
+ nChr < 0x80
+ ? 1
+ : nChr < 0x800
+ ? 2
+ : nChr < 0x10000
+ ? 3
+ : nChr < 0x200000
+ ? 4
+ : nChr < 0x4000000
+ ? 5
+ : 6;
+ }
+
+ const aBytes = new Uint8Array(nArrLen);
+
+ /* transcription… */
+ let nIdx = 0;
+ let nChrIdx = 0;
+ while (nIdx < nArrLen) {
+ nChr = sDOMStr.codePointAt(nChrIdx);
+ if (nChr === undefined) {
+ throw Error(`No char at ${nChrIdx} on string with length: ${sDOMStr.length}`)
+ }
+ if (nChr < 128) {
+ /* one byte */
+ aBytes[nIdx++] = nChr;
+ } else if (nChr < 0x800) {
+ /* two bytes */
+ aBytes[nIdx++] = 192 + (nChr >>> 6);
+ aBytes[nIdx++] = 128 + (nChr & 63);
+ } else if (nChr < 0x10000) {
+ /* three bytes */
+ aBytes[nIdx++] = 224 + (nChr >>> 12);
+ aBytes[nIdx++] = 128 + ((nChr >>> 6) & 63);
+ aBytes[nIdx++] = 128 + (nChr & 63);
+ } else if (nChr < 0x200000) {
+ /* four bytes */
+ aBytes[nIdx++] = 240 + (nChr >>> 18);
+ aBytes[nIdx++] = 128 + ((nChr >>> 12) & 63);
+ aBytes[nIdx++] = 128 + ((nChr >>> 6) & 63);
+ aBytes[nIdx++] = 128 + (nChr & 63);
+ nChrIdx++;
+ } else if (nChr < 0x4000000) {
+ /* five bytes */
+ aBytes[nIdx++] = 248 + (nChr >>> 24);
+ aBytes[nIdx++] = 128 + ((nChr >>> 18) & 63);
+ aBytes[nIdx++] = 128 + ((nChr >>> 12) & 63);
+ aBytes[nIdx++] = 128 + ((nChr >>> 6) & 63);
+ aBytes[nIdx++] = 128 + (nChr & 63);
+ nChrIdx++;
+ } /* if (nChr <= 0x7fffffff) */ else {
+ /* six bytes */
+ aBytes[nIdx++] = 252 + (nChr >>> 30);
+ aBytes[nIdx++] = 128 + ((nChr >>> 24) & 63);
+ aBytes[nIdx++] = 128 + ((nChr >>> 18) & 63);
+ aBytes[nIdx++] = 128 + ((nChr >>> 12) & 63);
+ aBytes[nIdx++] = 128 + ((nChr >>> 6) & 63);
+ aBytes[nIdx++] = 128 + (nChr & 63);
+ nChrIdx++;
+ }
+ nChrIdx++;
+ }
+
+ return aBytes;
+}
diff --git a/packages/taler-wallet-webextension/src/browserHttpLib.ts b/packages/web-util/src/utils/http-impl.browser.ts
index 26fa8eb11..1e5496071 100644
--- a/packages/taler-wallet-webextension/src/browserHttpLib.ts
+++ b/packages/web-util/src/utils/http-impl.browser.ts
@@ -18,28 +18,41 @@
* Imports.
*/
import {
- HttpRequestLibrary,
- HttpRequestOptions,
- HttpResponse,
- Headers,
- TalerError,
-} from "@gnu-taler/taler-wallet-core";
-import {
Logger,
RequestThrottler,
- stringToBytes,
TalerErrorCode,
+ TalerError,
+ Duration,
} from "@gnu-taler/taler-util";
+import {
+ HttpRequestLibrary,
+ HttpRequestOptions,
+ HttpResponse,
+ Headers,
+ getDefaultHeaders,
+ encodeBody,
+ DEFAULT_REQUEST_TIMEOUT_MS,
+ HttpLibArgs,
+} from "@gnu-taler/taler-util/http";
+
const logger = new Logger("browserHttpLib");
/**
* An implementation of the [[HttpRequestLibrary]] using the
* browser's XMLHttpRequest.
+ *
+ * @deprecated use BrowserFetchHttpLib
*/
-export class BrowserHttpLib implements HttpRequestLibrary {
+export class BrowserHttpLibDepreacted implements HttpRequestLibrary {
private throttle = new RequestThrottler();
private throttlingEnabled = true;
+ private requireTls = false;
+
+ constructor(args?: HttpLibArgs) {
+ this.throttlingEnabled = args?.enableThrottling ?? true;
+ this.requireTls = args?.requireTls ?? false;
+ }
fetch(
requestUrl: string,
@@ -47,9 +60,12 @@ export class BrowserHttpLib implements HttpRequestLibrary {
): Promise<HttpResponse> {
const requestMethod = options?.method ?? "GET";
const requestBody = options?.body;
+ const requestHeader = options?.headers;
+ const requestTimeout =
+ options?.timeout ?? Duration.fromMilliseconds(DEFAULT_REQUEST_TIMEOUT_MS);
+ const parsedUrl = new URL(requestUrl);
if (this.throttlingEnabled && this.throttle.applyThrottle(requestUrl)) {
- const parsedUrl = new URL(requestUrl);
throw TalerError.fromDetail(
TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED,
{
@@ -60,29 +76,32 @@ export class BrowserHttpLib implements HttpRequestLibrary {
`request to origin ${parsedUrl.origin} was throttled`,
);
}
+ if (this.requireTls && parsedUrl.protocol !== "https:") {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_NETWORK_ERROR,
+ {
+ requestMethod: requestMethod,
+ requestUrl: requestUrl,
+ },
+ `request to ${parsedUrl.origin} is not possible with protocol ${parsedUrl.protocol}`,
+ );
+ }
+
+ let myBody: ArrayBuffer | undefined =
+ requestMethod === "POST" || requestMethod === "PUT" || requestMethod === "PATCH"
+ ? encodeBody(requestBody)
+ : undefined;
+
+ const requestHeadersMap = getDefaultHeaders(requestMethod);
+ if (requestHeader) {
+ Object.entries(requestHeader).forEach(([key, value]) => {
+ if (value === undefined) return;
+ requestHeadersMap[key] = value
+ })
+ }
return new Promise<HttpResponse>((resolve, reject) => {
const myRequest = new XMLHttpRequest();
- myRequest.open(requestMethod, requestUrl);
- if (options?.headers) {
- for (const headerName in options.headers) {
- myRequest.setRequestHeader(headerName, options.headers[headerName]);
- }
- }
- myRequest.responseType = "arraybuffer";
- if (requestBody) {
- if (requestBody instanceof ArrayBuffer) {
- myRequest.send(requestBody);
- } else if (ArrayBuffer.isView(requestBody)) {
- myRequest.send(requestBody);
- } else if (typeof requestBody === "string") {
- myRequest.send(requestBody);
- } else {
- myRequest.send(JSON.stringify(requestBody));
- }
- } else {
- myRequest.send();
- }
myRequest.onerror = (e) => {
logger.error("http request error");
@@ -90,20 +109,49 @@ export class BrowserHttpLib implements HttpRequestLibrary {
TalerError.fromDetail(
TalerErrorCode.WALLET_NETWORK_ERROR,
{
- requestUrl: requestUrl,
+ requestUrl,
+ requestMethod,
},
"Could not make request",
),
);
};
+ myRequest.open(requestMethod, requestUrl);
+
+ let timeoutId: any | undefined;
+ if (requestTimeout.d_ms !== "forever") {
+ timeoutId = setTimeout(() => {
+ myRequest.abort();
+ reject(
+ TalerError.fromDetail(
+ TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT,
+ {
+ requestUrl,
+ requestMethod,
+ timeoutMs: requestTimeout.d_ms === "forever" ? 0 : requestTimeout.d_ms
+ },
+ `request to ${requestUrl} timed out`,
+ ),
+ );
+ }, requestTimeout.d_ms);
+ }
+
+ Object.keys(requestHeadersMap).forEach((headerName) => {
+ myRequest.setRequestHeader(headerName, requestHeadersMap[headerName]);
+ });
+
+ myRequest.responseType = "arraybuffer";
+ myRequest.send(myBody);
+
myRequest.addEventListener("readystatechange", (e) => {
if (myRequest.readyState === XMLHttpRequest.DONE) {
if (myRequest.status === 0) {
const exc = TalerError.fromDetail(
TalerErrorCode.WALLET_NETWORK_ERROR,
{
- requestUrl: requestUrl,
+ requestUrl,
+ requestMethod,
},
"HTTP request failed (status 0, maybe URI scheme was wrong?)",
);
@@ -114,27 +162,31 @@ export class BrowserHttpLib implements HttpRequestLibrary {
const td = new TextDecoder();
return td.decode(myRequest.response);
};
+ let responseJson: unknown = undefined;
const makeJson = async (): Promise<any> => {
- let responseJson;
- try {
- const td = new TextDecoder();
- const responseString = td.decode(myRequest.response);
- responseJson = JSON.parse(responseString);
- } catch (e) {
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
- {
- requestUrl: requestUrl,
- httpStatusCode: myRequest.status,
- },
- "Invalid JSON from HTTP response",
- );
+ if (responseJson === undefined) {
+ try {
+ const td = new TextDecoder();
+ const responseString = td.decode(myRequest.response);
+ responseJson = JSON.parse(responseString);
+ } catch (e) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ {
+ requestUrl,
+ requestMethod,
+ httpStatusCode: myRequest.status,
+ },
+ "Invalid JSON from HTTP response",
+ );
+ }
}
if (responseJson === null || typeof responseJson !== "object") {
throw TalerError.fromDetail(
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
{
- requestUrl: requestUrl,
+ requestUrl,
+ requestMethod,
httpStatusCode: myRequest.status,
},
"Invalid JSON from HTTP response",
diff --git a/packages/web-util/src/utils/http-impl.sw.ts b/packages/web-util/src/utils/http-impl.sw.ts
new file mode 100644
index 000000000..9c820bb4b
--- /dev/null
+++ b/packages/web-util/src/utils/http-impl.sw.ts
@@ -0,0 +1,217 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ Duration,
+ RequestThrottler,
+ TalerError,
+ TalerErrorCode
+} from "@gnu-taler/taler-util";
+
+import {
+ DEFAULT_REQUEST_TIMEOUT_MS,
+ Headers,
+ HttpLibArgs,
+ HttpRequestLibrary,
+ HttpRequestOptions,
+ HttpResponse,
+ encodeBody,
+ getDefaultHeaders,
+} from "@gnu-taler/taler-util/http";
+
+/**
+ * An implementation of the [[HttpRequestLibrary]] using the
+ * browser's XMLHttpRequest.
+ */
+export class BrowserFetchHttpLib implements HttpRequestLibrary {
+ private throttle = new RequestThrottler();
+ private throttlingEnabled = true;
+ private requireTls = false;
+
+ public constructor(args?: HttpLibArgs) {
+ this.throttlingEnabled = args?.enableThrottling ?? true;
+ this.requireTls = args?.requireTls ?? false;
+ }
+
+ async fetch(
+ requestUrl: string,
+ options?: HttpRequestOptions,
+ ): Promise<HttpResponse> {
+ const requestMethod = options?.method ?? "GET";
+ const requestBody = options?.body;
+ const requestHeader = options?.headers;
+ const requestTimeout =
+ options?.timeout ?? Duration.fromMilliseconds(DEFAULT_REQUEST_TIMEOUT_MS);
+ const requestCancel = options?.cancellationToken;
+ const requestRedirect = options?.redirect;
+
+ const parsedUrl = new URL(requestUrl);
+ if (this.throttlingEnabled && this.throttle.applyThrottle(requestUrl)) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED,
+ {
+ requestMethod,
+ requestUrl,
+ throttleStats: this.throttle.getThrottleStats(requestUrl),
+ },
+ `request to origin ${parsedUrl.origin} was throttled`,
+ );
+ }
+ if (this.requireTls && parsedUrl.protocol !== "https:") {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_NETWORK_ERROR,
+ {
+ requestMethod: requestMethod,
+ requestUrl: requestUrl,
+ },
+ `request to ${parsedUrl.origin} is not possible with protocol ${parsedUrl.protocol}`,
+ );
+ }
+
+ const myBody: ArrayBuffer | undefined =
+ requestMethod === "POST" || requestMethod === "PUT" || requestMethod === "PATCH"
+ ? encodeBody(requestBody)
+ : undefined;
+
+ const requestHeadersMap = getDefaultHeaders(requestMethod);
+ if (requestHeader) {
+ Object.entries(requestHeader).forEach(([key, value]) => {
+ if (value === undefined) return;
+ requestHeadersMap[key] = value
+ })
+ }
+
+ const controller = new AbortController();
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
+ if (requestTimeout.d_ms !== "forever") {
+ timeoutId = setTimeout(() => {
+ controller.abort(TalerErrorCode.GENERIC_TIMEOUT);
+ }, requestTimeout.d_ms);
+ }
+ if (requestCancel) {
+ requestCancel.onCancelled(() => {
+ controller.abort(TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR)
+ });
+ }
+
+ try {
+ const response = await fetch(requestUrl, {
+ headers: requestHeadersMap,
+ body: myBody,
+ method: requestMethod,
+ signal: controller.signal,
+ redirect: requestRedirect
+ });
+
+ if (timeoutId) {
+ clearTimeout(timeoutId);
+ }
+
+ const headerMap = new Headers();
+ response.headers.forEach((value, key) => {
+ headerMap.set(key, value);
+ });
+ return {
+ headers: headerMap,
+ status: response.status,
+ requestMethod,
+ requestUrl,
+ json: makeJsonHandler(response, requestUrl, requestMethod),
+ text: makeTextHandler(response, requestUrl, requestMethod),
+ bytes: async () => (await response.blob()).arrayBuffer(),
+ };
+ } catch (e) {
+ if (controller.signal) {
+ throw TalerError.fromDetail(
+ controller.signal.reason,
+ {
+ requestUrl,
+ requestMethod,
+ timeoutMs: requestTimeout.d_ms === "forever" ? 0 : requestTimeout.d_ms
+ },
+ `HTTP request failed.`,
+ );
+ }
+ throw e;
+ }
+ }
+
+}
+
+function makeTextHandler(
+ response: Response,
+ requestUrl: string,
+ requestMethod: string,
+) {
+ return async function getTextFromResponse(): Promise<any> {
+ let respText;
+ try {
+ respText = await response.text();
+ } catch (e) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ {
+ requestUrl,
+ requestMethod,
+ httpStatusCode: response.status,
+ },
+ "Invalid text from HTTP response",
+ );
+ }
+ return respText;
+ };
+}
+
+function makeJsonHandler(
+ response: Response,
+ requestUrl: string,
+ requestMethod: string,
+) {
+ let responseJson: unknown = undefined;
+ return async function getJsonFromResponse(): Promise<any> {
+ if (responseJson === undefined) {
+ try {
+ responseJson = await response.json();
+ } catch (e) {
+ const message = e instanceof Error ? `Invalid JSON from HTTP response: ${e.message}` : "Invalid JSON from HTTP response"
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ {
+ requestUrl,
+ requestMethod,
+ httpStatusCode: response.status,
+ },
+ message,
+ );
+ }
+ }
+ if (responseJson === null || typeof responseJson !== "object") {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ {
+ requestUrl,
+ requestMethod,
+ httpStatusCode: response.status,
+ },
+ "Invalid JSON from HTTP response: null or not object",
+ );
+ }
+ return responseJson;
+ };
+}
diff --git a/packages/web-util/src/utils/observable.ts b/packages/web-util/src/utils/observable.ts
new file mode 100644
index 000000000..16a33ae72
--- /dev/null
+++ b/packages/web-util/src/utils/observable.ts
@@ -0,0 +1,283 @@
+import { isArrayBufferView } from "util/types";
+
+export type ObservableMap<K, V> = Map<K, V> & {
+ onAnyUpdate: (callback: () => void) => () => void;
+ onUpdate: (key: string, callback: () => void) => () => void;
+};
+
+//FIXME: allow different type for different properties
+export function memoryMap<T>(
+ backend: Map<string, T> = new Map<string, T>(),
+): ObservableMap<string, T> {
+ const obs = new EventTarget();
+ const theMemoryMap: ObservableMap<string, T> = {
+ onAnyUpdate: (handler) => {
+ obs.addEventListener(`update`, handler);
+ obs.addEventListener(`clear`, handler);
+ return () => {
+ obs.removeEventListener(`update`, handler);
+ obs.removeEventListener(`clear`, handler);
+ };
+ },
+ onUpdate: (key, handler) => {
+ obs.addEventListener(`update-${key}`, handler);
+ obs.addEventListener(`clear`, handler);
+ return () => {
+ obs.removeEventListener(`update-${key}`, handler);
+ obs.removeEventListener(`clear`, handler);
+ };
+ },
+ delete: (key: string) => {
+ const result = backend.delete(key);
+ //@ts-ignore
+ theMemoryMap.size = backend.length;
+ obs.dispatchEvent(new Event(`update-${key}`));
+ obs.dispatchEvent(new Event(`update`));
+ return result;
+ },
+ set: (key: string, value: T) => {
+ backend.set(key, value);
+ //@ts-ignore
+ theMemoryMap.size = backend.length;
+ obs.dispatchEvent(new Event(`update-${key}`));
+ obs.dispatchEvent(new Event(`update`));
+ return theMemoryMap;
+ },
+ clear: () => {
+ backend.clear();
+ obs.dispatchEvent(new Event(`clear`));
+ },
+ entries: backend.entries.bind(backend),
+ forEach: backend.forEach.bind(backend),
+ get: backend.get.bind(backend),
+ has: backend.has.bind(backend),
+ keys: backend.keys.bind(backend),
+ size: backend.size,
+ values: backend.values.bind(backend),
+ [Symbol.iterator]: backend[Symbol.iterator],
+ [Symbol.toStringTag]: "theMemoryMap",
+ };
+ return theMemoryMap;
+}
+
+//FIXME: change this implementation to match the
+// browser storage. instead of creating a sync implementation
+// of observable map it should reuse the memoryMap and
+// sync the state with local storage
+export function localStorageMap(): ObservableMap<string, string> {
+ const obs = new EventTarget();
+ const theLocalStorageMap: ObservableMap<string, string> = {
+ onAnyUpdate: (handler) => {
+ obs.addEventListener(`update`, handler);
+ obs.addEventListener(`clear`, handler);
+ window.addEventListener("storage", handler);
+ return () => {
+ window.removeEventListener("storage", handler);
+ obs.removeEventListener(`update`, handler);
+ obs.removeEventListener(`clear`, handler);
+ };
+ },
+ onUpdate: (key, handler) => {
+ obs.addEventListener(`update-${key}`, handler);
+ obs.addEventListener(`clear`, handler);
+ function handleStorageEvent(ev: StorageEvent) {
+ if (ev.key === null || ev.key === key) {
+ handler();
+ }
+ }
+ window.addEventListener("storage", handleStorageEvent);
+ return () => {
+ window.removeEventListener("storage", handleStorageEvent);
+ obs.removeEventListener(`update-${key}`, handler);
+ obs.removeEventListener(`clear`, handler);
+ };
+ },
+ delete: (key: string) => {
+ const exists = localStorage.getItem(key) !== null;
+ localStorage.removeItem(key);
+ //@ts-ignore
+ theLocalStorageMap.size = localStorage.length;
+ obs.dispatchEvent(new Event(`update-${key}`));
+ obs.dispatchEvent(new Event(`update`));
+ return exists;
+ },
+ set: (key: string, v: string) => {
+ localStorage.setItem(key, v);
+ //@ts-ignore
+ theLocalStorageMap.size = localStorage.length;
+ obs.dispatchEvent(new Event(`update-${key}`));
+ obs.dispatchEvent(new Event(`update`));
+ return theLocalStorageMap;
+ },
+ clear: () => {
+ localStorage.clear();
+ obs.dispatchEvent(new Event(`clear`));
+ },
+ entries: (): IterableIterator<[string, string]> => {
+ let index = 0;
+ const total = localStorage.length;
+ return {
+ next() {
+ if (index === total) return { done: true, value: undefined };
+ const key = localStorage.key(index);
+ if (key === null) {
+ //we are going from 0 until last, this should not happen
+ throw Error("key cant be null");
+ }
+ const item = localStorage.getItem(key);
+ if (item === null) {
+ //the key exist, this should not happen
+ throw Error("value cant be null");
+ }
+ index = index + 1;
+ return { done: false, value: [key, item] };
+ },
+ [Symbol.iterator]() {
+ return this;
+ },
+ };
+ },
+ forEach: (cb) => {
+ for (let index = 0; index < localStorage.length; index++) {
+ const key = localStorage.key(index);
+ if (key === null) {
+ //we are going from 0 until last, this should not happen
+ throw Error("key cant be null");
+ }
+ const item = localStorage.getItem(key);
+ if (item === null) {
+ //the key exist, this should not happen
+ throw Error("value cant be null");
+ }
+ cb(key, item, theLocalStorageMap);
+ }
+ },
+ get: (key: string) => {
+ const item = localStorage.getItem(key);
+ if (item === null) return undefined;
+ return item;
+ },
+ has: (key: string) => {
+ return localStorage.getItem(key) === null;
+ },
+ keys: () => {
+ let index = 0;
+ const total = localStorage.length;
+ return {
+ next() {
+ if (index === total) return { done: true, value: undefined };
+ const key = localStorage.key(index);
+ if (key === null) {
+ //we are going from 0 until last, this should not happen
+ throw Error("key cant be null");
+ }
+ index = index + 1;
+ return { done: false, value: key };
+ },
+ [Symbol.iterator]() {
+ return this;
+ },
+ };
+ },
+ size: localStorage.length,
+ values: () => {
+ let index = 0;
+ const total = localStorage.length;
+ return {
+ next() {
+ if (index === total) return { done: true, value: undefined };
+ const key = localStorage.key(index);
+ if (key === null) {
+ //we are going from 0 until last, this should not happen
+ throw Error("key cant be null");
+ }
+ const item = localStorage.getItem(key);
+ if (item === null) {
+ //the key exist, this should not happen
+ throw Error("value cant be null");
+ }
+ index = index + 1;
+ return { done: false, value: item };
+ },
+ [Symbol.iterator]() {
+ return this;
+ },
+ };
+ },
+ [Symbol.iterator]: function (): IterableIterator<[string, string]> {
+ return theLocalStorageMap.entries();
+ },
+ [Symbol.toStringTag]: "theLocalStorageMap",
+ };
+ return theLocalStorageMap;
+}
+
+const isFirefox =
+ typeof (window as any) !== "undefined" &&
+ typeof (window as any)["InstallTrigger"] !== "undefined";
+
+async function getAllContent() {
+ //Firefox and Chrome has different storage api
+ if (isFirefox) {
+ // @ts-ignore
+ return browser.storage.local.get();
+ } else {
+ return chrome.storage.local.get();
+ }
+}
+
+async function updateContent(obj: Record<string, any>) {
+ if (isFirefox) {
+ // @ts-ignore
+ return browser.storage.local.set(obj);
+ } else {
+ return chrome.storage.local.set(obj);
+ }
+}
+type Changes = { [key: string]: { oldValue?: any; newValue?: any } };
+function onBrowserStorageUpdate(cb: (changes: Changes) => void): void {
+ if (isFirefox) {
+ // @ts-ignore
+ browser.storage.local.onChanged.addListener(cb);
+ } else {
+ chrome.storage.local.onChanged.addListener(cb);
+ }
+}
+
+export function browserStorageMap(
+ backend: ObservableMap<string, string>,
+): ObservableMap<string, string> {
+ getAllContent().then(content => {
+ Object.entries(content ?? {}).forEach(([k, v]) => {
+ backend.set(k, v as string);
+ });
+ })
+
+ backend.onAnyUpdate(async () => {
+ const result: Record<string, string> = {};
+ for (const [key, value] of backend.entries()) {
+ result[key] = value;
+ }
+ await updateContent(result);
+ });
+
+ onBrowserStorageUpdate((changes) => {
+ //another chrome instance made the change
+ const changedItems = Object.keys(changes);
+ if (changedItems.length === 0) {
+ backend.clear();
+ } else {
+ for (const key of changedItems) {
+ if (!changes[key].newValue) {
+ backend.delete(key);
+ } else {
+ if (changes[key].newValue !== changes[key].oldValue) {
+ backend.set(key, changes[key].newValue);
+ }
+ }
+ }
+ }
+ });
+
+ return backend;
+}
diff --git a/packages/web-util/src/utils/request.ts b/packages/web-util/src/utils/request.ts
new file mode 100644
index 000000000..23d3af468
--- /dev/null
+++ b/packages/web-util/src/utils/request.ts
@@ -0,0 +1,477 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+import { base64encode } from "./base64.js";
+
+/**
+ * @deprecated do not use it, it will be removed
+ */
+export enum ErrorType {
+ CLIENT,
+ SERVER,
+ UNREADABLE,
+ TIMEOUT,
+ UNEXPECTED,
+}
+
+
+
+/**
+ *
+ * @param baseUrl URL where the service is located
+ * @param endpoint endpoint of the service to be called
+ * @param options auth, method and params
+ * @deprecated do not use it, it will be removed
+ * @returns
+ */
+export async function defaultRequestHandler<T>(
+ baseUrl: string,
+ endpoint: string,
+ options: RequestOptions = {},
+): Promise<HttpResponseOk<T>> {
+ const requestHeaders: Record<string, string> = {};
+ if (options.token) {
+ requestHeaders.Authorization = `Bearer secret-token:${options.token}`;
+ } else if (options.basicAuth) {
+ requestHeaders.Authorization = `Basic ${base64encode(
+ `${options.basicAuth.username}:${options.basicAuth.password}`,
+ )}`;
+ }
+ requestHeaders["Content-Type"] =
+ !options.contentType || options.contentType === "json" ? "application/json" : "text/plain";
+
+ if (options.talerAmlOfficerSignature) {
+ requestHeaders["Taler-AML-Officer-Signature"] =
+ options.talerAmlOfficerSignature;
+ }
+
+ const requestMethod = options?.method ?? "GET";
+ const requestBody = options?.data;
+ const requestTimeout = options?.timeout ?? 5 * 1000;
+ const requestParams = options.params ?? {};
+ const requestPreventCache = options.preventCache ?? false;
+ const requestPreventCors = options.preventCors ?? false;
+
+ const validURL = validateURL(baseUrl, endpoint);
+
+ if (!validURL) {
+ const error: HttpResponseUnexpectedError = {
+ info: {
+ url: `${baseUrl}${endpoint}`,
+ payload: {},
+ hasToken: !!options.token,
+ status: 0,
+ options,
+ },
+ type: ErrorType.UNEXPECTED,
+ exception: undefined,
+ loading: false,
+ message: `invalid URL: "${baseUrl}${endpoint}"`,
+ };
+ throw new RequestError(error)
+ }
+
+ Object.entries(requestParams).forEach(([key, value]) => {
+ validURL.searchParams.set(key, String(value));
+ });
+
+ let payload: BodyInit | undefined = undefined;
+ if (requestBody != null) {
+ if (typeof requestBody === "string") {
+ payload = requestBody;
+ } else if (requestBody instanceof ArrayBuffer) {
+ payload = requestBody;
+ } else if (ArrayBuffer.isView(requestBody)) {
+ payload = requestBody;
+ } else if (typeof requestBody === "object") {
+ payload = JSON.stringify(requestBody);
+ } else {
+ const error: HttpResponseUnexpectedError = {
+ info: {
+ url: validURL.href,
+ payload: {},
+ hasToken: !!options.token,
+ status: 0,
+ options,
+ },
+ type: ErrorType.UNEXPECTED,
+ exception: undefined,
+ loading: false,
+ message: `unsupported request body type: "${typeof requestBody}"`,
+ };
+ throw new RequestError(error)
+ }
+ }
+
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => {
+ controller.abort("HTTP_REQUEST_TIMEOUT");
+ }, requestTimeout);
+
+ let response;
+ try {
+ response = await fetch(validURL.href, {
+ headers: requestHeaders,
+ method: requestMethod,
+ credentials: "omit",
+ mode: requestPreventCors ? "no-cors" : "cors",
+ cache: requestPreventCache ? "no-cache" : "default",
+ body: payload,
+ signal: controller.signal,
+ });
+ } catch (ex) {
+ const info: RequestInfo = {
+ payload,
+ url: validURL.href,
+ hasToken: !!options.token,
+ status: 0,
+ options,
+ };
+
+ if (ex instanceof Error) {
+ if (ex.message === "HTTP_REQUEST_TIMEOUT") {
+ const error: HttpRequestTimeoutError = {
+ info,
+ type: ErrorType.TIMEOUT,
+ message: "request timeout",
+ };
+ throw new RequestError(error);
+ }
+ }
+
+ const error: HttpResponseUnexpectedError = {
+ info,
+ type: ErrorType.UNEXPECTED,
+ exception: ex,
+ loading: false,
+ message: (ex instanceof Error ? ex.message : ""),
+ };
+ throw new RequestError(error);
+ }
+
+ if (timeoutId) {
+ clearTimeout(timeoutId);
+ }
+ const headerMap = new Headers();
+ response.headers.forEach((value, key) => {
+ headerMap.set(key, value);
+ });
+
+ if (response.ok) {
+ const result = await buildRequestOk<T>(
+ response,
+ validURL.href,
+ payload,
+ !!options.token,
+ options,
+ );
+ return result;
+ } else {
+ const dataTxt = await response.text();
+ const error = buildRequestFailed(
+ validURL.href,
+ dataTxt,
+ response.status,
+ payload,
+ options,
+ );
+ throw new RequestError(error);
+ }
+}
+
+/**
+ * @deprecated do not use it, it will be removed
+ */
+export type HttpResponse<T, ErrorDetail> =
+ | HttpResponseOk<T>
+ | HttpResponseLoading<T>
+ | HttpError<ErrorDetail>;
+
+/**
+ * @deprecated do not use it, it will be removed
+ */
+export type HttpResponsePaginated<T, ErrorDetail> =
+ | HttpResponseOkPaginated<T>
+ | HttpResponseLoading<T>
+ | HttpError<ErrorDetail>;
+
+/**
+ * @deprecated do not use it, it will be removed
+ */
+export interface RequestInfo {
+ url: string;
+ hasToken: boolean;
+ payload: any;
+ status: number;
+ options: RequestOptions;
+}
+
+interface HttpResponseLoading<T> {
+ ok?: false;
+ loading: true;
+ clientError?: false;
+ serverError?: false;
+
+ data?: T;
+}
+/**
+ * @deprecated do not use it, it will be removed
+ */
+export interface HttpResponseOk<T> {
+ ok: true;
+ loading?: false;
+ clientError?: false;
+ serverError?: false;
+
+ data: T;
+ info?: RequestInfo;
+}
+
+/**
+ * @deprecated do not use it, it will be removed
+ */
+export type HttpResponseOkPaginated<T> = HttpResponseOk<T> & WithPagination;
+
+/**
+ * @deprecated do not use it, it will be removed
+ */
+export interface WithPagination {
+ loadMore: () => void;
+ loadMorePrev: () => void;
+ isReachingEnd?: boolean;
+ isReachingStart?: boolean;
+}
+
+/**
+ * @deprecated do not use it, it will be removed
+ */
+export type HttpError<ErrorDetail> =
+ | HttpRequestTimeoutError
+ | HttpResponseClientError<ErrorDetail>
+ | HttpResponseServerError<ErrorDetail>
+ | HttpResponseUnreadableError
+ | HttpResponseUnexpectedError;
+
+/**
+ * @deprecated do not use it, it will be removed
+ */
+export interface HttpResponseServerError<ErrorDetail> {
+ ok?: false;
+ loading?: false;
+ type: ErrorType.SERVER;
+ payload: ErrorDetail;
+ status: HttpStatusCode;
+ message: string;
+ info: RequestInfo;
+}
+interface HttpRequestTimeoutError {
+ ok?: false;
+ loading?: false;
+ type: ErrorType.TIMEOUT;
+
+ info: RequestInfo;
+
+ message: string;
+}
+interface HttpResponseClientError<ErrorDetail> {
+ ok?: false;
+ loading?: false;
+ type: ErrorType.CLIENT;
+
+ info: RequestInfo;
+ status: HttpStatusCode;
+ payload: ErrorDetail;
+ message: string;
+}
+
+interface HttpResponseUnexpectedError {
+ ok?: false;
+ loading: false;
+ type: ErrorType.UNEXPECTED;
+
+ info: RequestInfo;
+ status?: HttpStatusCode;
+ exception: unknown;
+ message: string;
+}
+
+interface HttpResponseUnreadableError {
+ ok?: false;
+ loading: false;
+ type: ErrorType.UNREADABLE;
+
+ info: RequestInfo;
+ status: HttpStatusCode;
+ exception: unknown;
+ body: string;
+ message: string;
+}
+/**
+ * @deprecated do not use it, it will be removed
+ */
+export class RequestError<ErrorDetail> extends Error {
+ /**
+ * @deprecated use cause
+ */
+ info: HttpError<ErrorDetail>;
+ cause: HttpError<ErrorDetail>;
+ constructor(d: HttpError<ErrorDetail>) {
+ super(d.message);
+ this.info = d;
+ this.cause = d;
+ }
+}
+
+type Methods = "GET" | "POST" | "PATCH" | "DELETE" | "PUT";
+
+/**
+ * @deprecated do not use it, it will be removed
+ */
+export interface RequestOptions {
+ method?: Methods;
+ token?: string;
+ basicAuth?: {
+ username: string;
+ password: string;
+ };
+ preventCache?: boolean;
+ preventCors?: boolean;
+ data?: any;
+ params?: unknown;
+ timeout?: number;
+ contentType?: "text" | "json";
+ talerAmlOfficerSignature?: string;
+}
+
+/**
+ * @deprecated do not use it, it will be removed
+ */
+async function buildRequestOk<T>(
+ response: Response,
+ url: string,
+ payload: any,
+ hasToken: boolean,
+ options: RequestOptions,
+): Promise<HttpResponseOk<T>> {
+ const dataTxt = await response.text();
+ const data = dataTxt ? JSON.parse(dataTxt) : undefined;
+ return {
+ ok: true,
+ data,
+ info: {
+ payload,
+ url,
+ hasToken,
+ options,
+ status: response.status,
+ },
+ };
+}
+
+/**
+ * @deprecated do not use it, it will be removed
+ */
+export function buildRequestFailed<ErrorDetail>(
+ url: string,
+ dataTxt: string,
+ status: number,
+ payload: any,
+ maybeOptions?: RequestOptions,
+):
+ | HttpResponseClientError<ErrorDetail>
+ | HttpResponseServerError<ErrorDetail>
+ | HttpResponseUnreadableError
+ | HttpResponseUnexpectedError {
+ const options = maybeOptions ?? {};
+ const info: RequestInfo = {
+ payload,
+ url,
+ hasToken: !!options.token,
+ options,
+ status: status || 0,
+ };
+
+ // const dataTxt = await response.text();
+ try {
+ const data = dataTxt ? JSON.parse(dataTxt) : undefined;
+ const errorCode = !data || !data.code ? "" : `(code: ${data.code})`;
+ const errorHint =
+ !data || !data.hint ? "Not hint." : `${data.hint} ${errorCode}`;
+
+ if (status && status >= 400 && status < 500) {
+ const message =
+ data === undefined
+ ? `Client error (${status}) without data.`
+ : errorHint;
+
+ const error: HttpResponseClientError<ErrorDetail> = {
+ type: ErrorType.CLIENT,
+ status,
+ info,
+ message,
+ payload: data,
+ };
+ return error;
+ }
+ if (status && status >= 500 && status < 600) {
+ const message =
+ data === undefined
+ ? `Server error (${status}) without data.`
+ : errorHint;
+ const error: HttpResponseServerError<ErrorDetail> = {
+ type: ErrorType.SERVER,
+ status,
+ info,
+ message,
+ payload: data,
+ };
+ return error;
+ }
+ return {
+ info,
+ loading: false,
+ type: ErrorType.UNEXPECTED,
+ status,
+ exception: undefined,
+ message: `http status code not handled: ${status}`,
+ };
+ } catch (ex) {
+ const error: HttpResponseUnreadableError = {
+ info,
+ loading: false,
+ status,
+ type: ErrorType.UNREADABLE,
+ exception: ex,
+ body: dataTxt,
+ message: "Could not parse body as json",
+ };
+
+ return error;
+ }
+}
+
+/**
+ * @deprecated do not use it, it will be removed
+ */
+function validateURL(baseUrl: string, endpoint: string): URL | undefined {
+ try {
+ return new URL(`${baseUrl}${endpoint}`)
+ } catch (ex) {
+ return undefined
+ }
+
+} \ No newline at end of file
diff --git a/packages/web-util/src/utils/route.ts b/packages/web-util/src/utils/route.ts
new file mode 100644
index 000000000..494a61efa
--- /dev/null
+++ b/packages/web-util/src/utils/route.ts
@@ -0,0 +1,126 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+declare const __location: unique symbol;
+/**
+ * special string that defined a location in the application
+ *
+ * this help to prevent wrong path
+ */
+export type AppLocation = string & {
+ [__location]: true;
+};
+
+export type EmptyObject = Record<string, never>;
+
+export function urlPattern<
+ T extends Record<string, string | undefined> = EmptyObject,
+>(pattern: RegExp, reverse: (p: T) => string): RouteDefinition<T> {
+ const url = reverse as (p: T) => AppLocation;
+ return {
+ pattern: new RegExp(pattern),
+ url,
+ };
+}
+
+/**
+ * defines a location in the app
+ *
+ * pattern: how a string will trigger this location
+ * url(): how a state serialize to a location
+ */
+
+export type ObjectOf<T> = Record<string, T> | EmptyObject;
+
+export type RouteDefinition<
+ T extends ObjectOf<string | undefined> = EmptyObject,
+> = {
+ pattern: RegExp;
+ url: (p: T) => AppLocation;
+};
+
+const nullRountDef = {
+ pattern: new RegExp(/.*/),
+ url: () => "" as AppLocation,
+};
+export function buildNullRoutDefinition<
+ T extends ObjectOf<string>,
+>(): RouteDefinition<T> {
+ return nullRountDef;
+}
+
+/**
+ * Search path in the pageList
+ * get the values from the path found
+ * add params from searchParams
+ *
+ * @param path
+ * @param params
+ */
+export function findMatch<T extends ObjectOf<RouteDefinition>>(
+ pagesMap: T,
+ pageList: Array<keyof T>,
+ path: string,
+ params: Record<string, string[]>,
+): Location<T> | undefined {
+ for (let idx = 0; idx < pageList.length; idx++) {
+ const name = pageList[idx];
+ const found = pagesMap[name].pattern.exec(path);
+ if (found !== null) {
+ const values = {} as Record<string, unknown>;
+
+ if (found.groups !== undefined) {
+ Object.entries(found.groups).forEach(([key, value]) => {
+ values[key] = value;
+ });
+ }
+
+ // @ts-expect-error values is a map string which is equivalent to the RouteParamsType
+ return { name, parent: pagesMap, values, params };
+ }
+ }
+ return undefined;
+}
+
+/**
+ * get the type of the params of a location
+ *
+ */
+type RouteParamsType<
+ RouteType,
+ Key extends keyof RouteType,
+> = RouteType[Key] extends RouteDefinition<infer ParamType> ? ParamType : never;
+
+/**
+ * Helps to create a map of a type with the key
+ */
+type MapKeyValue<Type> = {
+ [Key in keyof Type]: Key extends string
+ ? {
+ parent: Type;
+ name: Key;
+ values: RouteParamsType<Type, Key>;
+ params: Record<string, string[]>;
+ }
+ : never;
+};
+
+/**
+ * create a enumeration of value of a mapped type
+ */
+type EnumerationOf<T> = T[keyof T];
+
+export type Location<T> = EnumerationOf<MapKeyValue<T>>;
diff --git a/packages/web-util/tsconfig.json b/packages/web-util/tsconfig.json
index aede0a0ac..a315dda1c 100644
--- a/packages/web-util/tsconfig.json
+++ b/packages/web-util/tsconfig.json
@@ -1,16 +1,16 @@
{
"compilerOptions": {
"composite": true,
- "target": "ES6",
- "module": "ESNext",
+ "declaration": true,
+ "declarationMap": true,
+ "target": "ES2020",
+ "module": "Node16",
"jsx": "react",
"jsxFactory": "h",
"jsxFragmentFactory": "Fragment",
- "moduleResolution": "Node",
+ "moduleResolution": "Node16",
"sourceMap": true,
- "lib": [
- "es6"
- ],
+ "lib": ["DOM", "ES2020"],
"outDir": "lib",
"preserveSymlinks": true,
"skipLibCheck": true,
@@ -24,11 +24,7 @@
"esModuleInterop": true,
"importHelpers": true,
"rootDir": "./src",
- "typeRoots": [
- "./node_modules/@types"
- ]
+ "typeRoots": ["./node_modules/@types"]
},
- "include": [
- "src/**/*"
- ]
-} \ No newline at end of file
+ "include": ["src/**/*"]
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 541130e78..3b56c5e64 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1,736 +1,1204 @@
-lockfileVersion: 5.4
+lockfileVersion: '6.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
importers:
.:
- specifiers:
- '@babel/core': 7.13.16
- '@linaria/esbuild': ^3.0.0-beta.15
- '@linaria/shaker': ^3.0.0-beta.15
- esbuild: ^0.15.13
- nx: 15.0.1
- prettier: ^2.5.1
devDependencies:
- '@babel/core': 7.13.16
- '@linaria/esbuild': 3.0.0-beta.23
- '@linaria/shaker': 3.0.0-beta.23
- esbuild: 0.15.13
- nx: 15.0.1
- prettier: 2.7.1
+ '@babel/core':
+ specifier: 7.13.16
+ version: 7.13.16
+ '@linaria/esbuild':
+ specifier: ^3.0.0-beta.15
+ version: 3.0.0-beta.23
+ '@linaria/shaker':
+ specifier: ^3.0.0-beta.15
+ version: 3.0.0-beta.23
+ esbuild:
+ specifier: ^0.19.9
+ version: 0.19.9
+ prettier:
+ specifier: ^3.1.1
+ version: 3.1.1
+ typedoc:
+ specifier: ^0.25.4
+ version: 0.25.4(typescript@5.3.3)
+ typescript:
+ specifier: ^5.3.3
+ version: 5.3.3
+
+ packages/aml-backoffice-ui:
+ dependencies:
+ '@gnu-taler/taler-util':
+ specifier: workspace:*
+ version: link:../taler-util
+ '@gnu-taler/web-util':
+ specifier: workspace:*
+ version: link:../web-util
+ '@headlessui/react':
+ specifier: ^1.7.14
+ version: 1.7.14(react-dom@18.2.0)(react@18.2.0)
+ '@heroicons/react':
+ specifier: ^2.0.17
+ version: 2.0.17(react@18.2.0)
+ date-fns:
+ specifier: 2.29.3
+ version: 2.29.3
+ history:
+ specifier: 4.10.1
+ version: 4.10.1
+ jed:
+ specifier: 1.1.1
+ version: 1.1.1
+ preact:
+ specifier: 10.11.3
+ version: 10.11.3
+ swr:
+ specifier: 2.2.2
+ version: 2.2.2(react@18.2.0)
+ devDependencies:
+ '@gnu-taler/pogen':
+ specifier: workspace:*
+ version: link:../pogen
+ '@tailwindcss/forms':
+ specifier: ^0.5.3
+ version: 0.5.3(tailwindcss@3.3.2)
+ '@tailwindcss/typography':
+ specifier: ^0.5.9
+ version: 0.5.9(tailwindcss@3.3.2)
+ '@types/chai':
+ specifier: ^4.3.0
+ version: 4.3.3
+ '@types/history':
+ specifier: ^4.7.8
+ version: 4.7.11
+ '@types/mocha':
+ specifier: ^10.0.1
+ version: 10.0.1
+ '@typescript-eslint/eslint-plugin':
+ specifier: ^6.19.0
+ version: 6.19.0(@typescript-eslint/parser@6.19.0)(eslint@8.56.0)(typescript@5.3.3)
+ '@typescript-eslint/parser':
+ specifier: ^6.19.0
+ version: 6.19.0(eslint@8.56.0)(typescript@5.3.3)
+ autoprefixer:
+ specifier: ^10.4.14
+ version: 10.4.14(postcss@8.4.23)
+ chai:
+ specifier: ^4.3.6
+ version: 4.3.6
+ esbuild:
+ specifier: ^0.19.9
+ version: 0.19.9
+ eslint:
+ specifier: ^8.56.0
+ version: 8.56.0
+ eslint-config-prettier:
+ specifier: ^9.1.0
+ version: 9.1.0(eslint@8.56.0)
+ eslint-plugin-react:
+ specifier: ^7.33.2
+ version: 7.33.2(eslint@8.56.0)
+ mocha:
+ specifier: ^9.2.0
+ version: 9.2.2
+ po2json:
+ specifier: ^0.4.5
+ version: 0.4.5
+ postcss:
+ specifier: ^8.4.23
+ version: 8.4.23
+ postcss-cli:
+ specifier: ^10.1.0
+ version: 10.1.0(postcss@8.4.23)
+ tailwindcss:
+ specifier: ^3.3.2
+ version: 3.3.2
+ typescript:
+ specifier: 5.3.3
+ version: 5.3.3
+
+ packages/anastasis-cli:
+ dependencies:
+ '@gnu-taler/anastasis-core':
+ specifier: workspace:*
+ version: link:../anastasis-core
+ '@gnu-taler/taler-util':
+ specifier: workspace:*
+ version: link:../taler-util
+ tslib:
+ specifier: ^2.6.2
+ version: 2.6.2
+ devDependencies:
+ '@types/node':
+ specifier: ^18.11.17
+ version: 18.11.17
+ prettier:
+ specifier: ^3.1.1
+ version: 3.1.1
+ typedoc:
+ specifier: ^0.25.4
+ version: 0.25.4(typescript@5.3.3)
+ typescript:
+ specifier: ^5.3.3
+ version: 5.3.3
packages/anastasis-core:
- specifiers:
- '@gnu-taler/taler-util': workspace:*
- ava: ^4.3.3
- fetch-ponyfill: ^7.1.0
- fflate: ^0.7.4
- hash-wasm: ^4.9.0
- node-fetch: ^3.2.0
- rimraf: ^3.0.2
- tslib: ^2.4.0
- typescript: ^4.8.4
- dependencies:
- '@gnu-taler/taler-util': link:../taler-util
- fetch-ponyfill: 7.1.0
- fflate: 0.7.4
- hash-wasm: 4.9.0
- node-fetch: 3.2.10
- tslib: 2.4.0
+ dependencies:
+ '@gnu-taler/taler-util':
+ specifier: workspace:*
+ version: link:../taler-util
+ fflate:
+ specifier: ^0.8.1
+ version: 0.8.1
+ tslib:
+ specifier: ^2.6.2
+ version: 2.6.2
devDependencies:
- ava: 4.3.3
- rimraf: 3.0.2
- typescript: 4.8.4
+ ava:
+ specifier: ^6.0.1
+ version: 6.0.1(@ava/typescript@4.1.0)
+ typescript:
+ specifier: ^5.3.3
+ version: 5.3.3
packages/anastasis-webui:
- specifiers:
- '@creativebulma/bulma-tooltip': ^1.2.0
- '@gnu-taler/anastasis-core': workspace:*
- '@gnu-taler/taler-util': workspace:*
- '@gnu-taler/web-util': workspace:*
- '@types/chai': ^4.3.0
- '@types/mocha': ^9.0.0
- bulma: ^0.9.3
- bulma-checkbox: ^1.1.1
- bulma-radio: ^1.1.1
- chai: ^4.3.6
- date-fns: 2.29.2
- jed: 1.1.1
- jssha: ^3.2.0
- mocha: ^9.2.0
- preact: 10.11.3
- preact-render-to-string: ^5.1.19
- preact-router: ^3.2.1
- qrcode-generator: ^1.4.4
- sass: 1.56.1
- typescript: ^4.8.4
- dependencies:
- '@gnu-taler/anastasis-core': link:../anastasis-core
- '@gnu-taler/taler-util': link:../taler-util
- '@gnu-taler/web-util': link:../web-util
- '@types/chai': 4.3.3
- chai: 4.3.6
- date-fns: 2.29.2
- jed: 1.1.1
- preact: 10.11.3
- preact-render-to-string: 5.2.6_preact@10.11.3
- preact-router: 3.2.1_preact@10.11.3
- qrcode-generator: 1.4.4
+ dependencies:
+ '@gnu-taler/anastasis-core':
+ specifier: workspace:*
+ version: link:../anastasis-core
+ '@gnu-taler/taler-util':
+ specifier: workspace:*
+ version: link:../taler-util
+ '@gnu-taler/web-util':
+ specifier: workspace:*
+ version: link:../web-util
+ date-fns:
+ specifier: 2.29.2
+ version: 2.29.2
+ jed:
+ specifier: 1.1.1
+ version: 1.1.1
+ jssha:
+ specifier: ^3.3.0
+ version: 3.3.0
+ preact:
+ specifier: 10.11.3
+ version: 10.11.3
+ preact-router:
+ specifier: ^3.2.1
+ version: 3.2.1(preact@10.11.3)
+ qrcode-generator:
+ specifier: ^1.4.4
+ version: 1.4.4
devDependencies:
- '@creativebulma/bulma-tooltip': 1.2.0
- '@types/mocha': 9.1.1
- bulma: 0.9.4
- bulma-checkbox: 1.2.1
- bulma-radio: 1.2.0
- jssha: 3.3.0
- mocha: 9.2.2
- sass: 1.56.1
- typescript: 4.8.4
-
- packages/demobank-ui:
- specifiers:
- '@creativebulma/bulma-tooltip': ^1.2.0
- '@gnu-taler/pogen': ^0.0.5
- '@gnu-taler/taler-util': workspace:*
- '@gnu-taler/web-util': workspace:*
- '@types/history': ^4.7.8
- '@typescript-eslint/eslint-plugin': ^5.41.0
- '@typescript-eslint/parser': ^5.41.0
- bulma: ^0.9.4
- bulma-checkbox: ^1.1.1
- bulma-radio: ^1.1.1
- date-fns: 2.29.3
- esbuild: ^0.15.12
- eslint: ^8.26.0
- eslint-config-preact: ^1.2.0
- history: 4.10.1
- jed: 1.1.1
- po2json: ^0.4.5
- preact: 10.11.3
- preact-router: 3.2.1
- qrcode-generator: ^1.4.4
- sass: 1.56.1
- swr: 1.3.0
- typescript: ^4.4.4
- dependencies:
- '@gnu-taler/taler-util': link:../taler-util
- '@gnu-taler/web-util': link:../web-util
- date-fns: 2.29.3
- history: 4.10.1
- jed: 1.1.1
- preact: 10.11.3
- preact-router: 3.2.1_preact@10.11.3
- qrcode-generator: 1.4.4
- swr: 1.3.0
+ '@creativebulma/bulma-tooltip':
+ specifier: ^1.2.0
+ version: 1.2.0
+ '@types/chai':
+ specifier: ^4.3.0
+ version: 4.3.3
+ '@types/mocha':
+ specifier: ^9.0.0
+ version: 9.1.1
+ bulma:
+ specifier: ^0.9.3
+ version: 0.9.4
+ bulma-checkbox:
+ specifier: ^1.1.1
+ version: 1.2.1
+ bulma-radio:
+ specifier: ^1.1.1
+ version: 1.2.0
+ chai:
+ specifier: ^4.3.6
+ version: 4.3.6
+ mocha:
+ specifier: ^9.2.0
+ version: 9.2.2
+ sass:
+ specifier: 1.56.1
+ version: 1.56.1
+ typescript:
+ specifier: ^5.3.3
+ version: 5.3.3
+
+ packages/auditor-backoffice-ui:
+ dependencies:
+ '@gnu-taler/taler-util':
+ specifier: workspace:*
+ version: link:../taler-util
+ '@gnu-taler/web-util':
+ specifier: workspace:*
+ version: link:../web-util
+ date-fns:
+ specifier: 2.29.3
+ version: 2.29.3
+ history:
+ specifier: 4.10.1
+ version: 4.10.1
+ jed:
+ specifier: 1.1.1
+ version: 1.1.1
+ preact:
+ specifier: 10.11.3
+ version: 10.11.3
+ preact-router:
+ specifier: 3.2.1
+ version: 3.2.1(preact@10.11.3)
+ qrcode-generator:
+ specifier: 1.4.4
+ version: 1.4.4
+ swr:
+ specifier: 2.2.2
+ version: 2.2.2(react@18.2.0)
+ yup:
+ specifier: ^0.32.9
+ version: 0.32.11
devDependencies:
- '@creativebulma/bulma-tooltip': 1.2.0
- '@gnu-taler/pogen': link:../pogen
- '@types/history': 4.7.11
- '@typescript-eslint/eslint-plugin': 5.41.0_huremdigmcnkianavgfk3x6iou
- '@typescript-eslint/parser': 5.41.0_wyqvi574yv7oiwfeinomdzmc3m
- bulma: 0.9.4
- bulma-checkbox: 1.2.1
- bulma-radio: 1.2.0
- esbuild: 0.15.12
- eslint: 8.26.0
- eslint-config-preact: 1.3.0_fy74h4y2g2kkrxhvsefhiowl74
- po2json: 0.4.5
- sass: 1.56.1
- typescript: 4.8.4
+ '@creativebulma/bulma-tooltip':
+ specifier: ^1.2.0
+ version: 1.2.0
+ '@gnu-taler/pogen':
+ specifier: workspace:*
+ version: link:../pogen
+ '@types/chai':
+ specifier: ^4.3.0
+ version: 4.3.3
+ '@types/history':
+ specifier: ^4.7.8
+ version: 4.7.11
+ '@types/mocha':
+ specifier: ^8.2.3
+ version: 8.2.3
+ '@types/node':
+ specifier: ^18.11.17
+ version: 18.11.17
+ '@typescript-eslint/eslint-plugin':
+ specifier: ^4.22.0
+ version: 4.33.0(@typescript-eslint/parser@4.33.0)(eslint@7.32.0)(typescript@5.3.3)
+ '@typescript-eslint/parser':
+ specifier: ^4.22.0
+ version: 4.33.0(eslint@7.32.0)(typescript@5.3.3)
+ base64-inline-loader:
+ specifier: ^1.1.1
+ version: 1.1.1(webpack@4.47.0)
+ bulma:
+ specifier: ^0.9.2
+ version: 0.9.4
+ bulma-checkbox:
+ specifier: ^1.1.1
+ version: 1.2.1
+ bulma-radio:
+ specifier: ^1.1.1
+ version: 1.2.0
+ bulma-responsive-tables:
+ specifier: ^1.2.3
+ version: 1.2.5
+ bulma-switch-control:
+ specifier: ^1.1.1
+ version: 1.2.2
+ bulma-timeline:
+ specifier: ^3.0.4
+ version: 3.0.5
+ bulma-upload-control:
+ specifier: ^1.2.0
+ version: 1.2.0
+ chai:
+ specifier: ^4.3.6
+ version: 4.3.6
+ dotenv:
+ specifier: ^8.2.0
+ version: 8.6.0
+ eslint:
+ specifier: ^7.25.0
+ version: 7.32.0
+ eslint-config-preact:
+ specifier: ^1.1.4
+ version: 1.3.0(@typescript-eslint/eslint-plugin@4.33.0)(eslint@7.32.0)(typescript@5.3.3)
+ eslint-plugin-header:
+ specifier: ^3.1.1
+ version: 3.1.1(eslint@7.32.0)
+ html-webpack-inline-chunk-plugin:
+ specifier: ^1.1.1
+ version: 1.1.1
+ html-webpack-inline-source-plugin:
+ specifier: 0.0.10
+ version: 0.0.10
+ html-webpack-skip-assets-plugin:
+ specifier: ^1.0.1
+ version: 1.0.3(html-webpack-plugin@5.5.4)(webpack@4.47.0)
+ inline-chunk-html-plugin:
+ specifier: ^1.1.1
+ version: 1.1.1
+ mocha:
+ specifier: ^9.2.0
+ version: 9.2.2
+ preact-render-to-string:
+ specifier: ^5.2.6
+ version: 5.2.6(preact@10.11.3)
+ sass:
+ specifier: 1.56.1
+ version: 1.56.1
+ source-map-support:
+ specifier: ^0.5.21
+ version: 0.5.21
+ typedoc:
+ specifier: ^0.25.4
+ version: 0.25.4(typescript@5.3.3)
+ typescript:
+ specifier: 5.3.3
+ version: 5.3.3
+
+ packages/bank-ui:
+ dependencies:
+ '@gnu-taler/taler-util':
+ specifier: workspace:*
+ version: link:../taler-util
+ '@gnu-taler/web-util':
+ specifier: workspace:*
+ version: link:../web-util
+ date-fns:
+ specifier: 2.29.3
+ version: 2.29.3
+ jed:
+ specifier: 1.1.1
+ version: 1.1.1
+ preact:
+ specifier: 10.11.3
+ version: 10.11.3
+ qrcode-generator:
+ specifier: ^1.4.4
+ version: 1.4.4
+ swr:
+ specifier: 2.0.3
+ version: 2.0.3(react@18.2.0)
+ devDependencies:
+ '@gnu-taler/pogen':
+ specifier: workspace:*
+ version: link:../pogen
+ '@tailwindcss/forms':
+ specifier: ^0.5.3
+ version: 0.5.3(tailwindcss@3.3.2)
+ '@tailwindcss/typography':
+ specifier: ^0.5.9
+ version: 0.5.9(tailwindcss@3.3.2)
+ '@types/chai':
+ specifier: ^4.3.0
+ version: 4.3.3
+ '@types/history':
+ specifier: ^4.7.8
+ version: 4.7.11
+ '@types/mocha':
+ specifier: ^10.0.1
+ version: 10.0.1
+ '@types/node':
+ specifier: ^18.11.17
+ version: 18.11.17
+ '@typescript-eslint/eslint-plugin':
+ specifier: ^6.19.0
+ version: 6.19.0(@typescript-eslint/parser@6.19.0)(eslint@8.56.0)(typescript@5.3.3)
+ '@typescript-eslint/parser':
+ specifier: ^6.19.0
+ version: 6.19.0(eslint@8.56.0)(typescript@5.3.3)
+ autoprefixer:
+ specifier: ^10.4.14
+ version: 10.4.14(postcss@8.4.33)
+ chai:
+ specifier: ^4.3.6
+ version: 4.3.6
+ esbuild:
+ specifier: ^0.19.9
+ version: 0.19.9
+ eslint:
+ specifier: ^8.56.0
+ version: 8.56.0
+ eslint-config-prettier:
+ specifier: ^9.1.0
+ version: 9.1.0(eslint@8.56.0)
+ eslint-plugin-react:
+ specifier: ^7.33.2
+ version: 7.33.2(eslint@8.56.0)
+ mocha:
+ specifier: 9.2.0
+ version: 9.2.0
+ po2json:
+ specifier: ^0.4.5
+ version: 0.4.5
+ tailwindcss:
+ specifier: ^3.3.2
+ version: 3.3.2
+ typescript:
+ specifier: 5.3.3
+ version: 5.3.3
+
+ packages/challenger-ui:
+ dependencies:
+ '@gnu-taler/taler-util':
+ specifier: workspace:*
+ version: link:../taler-util
+ '@gnu-taler/web-util':
+ specifier: workspace:*
+ version: link:../web-util
+ date-fns:
+ specifier: 2.29.3
+ version: 2.29.3
+ jed:
+ specifier: 1.1.1
+ version: 1.1.1
+ preact:
+ specifier: 10.11.3
+ version: 10.11.3
+ qrcode-generator:
+ specifier: ^1.4.4
+ version: 1.4.4
+ swr:
+ specifier: 2.0.3
+ version: 2.0.3(react@18.2.0)
+ devDependencies:
+ '@gnu-taler/pogen':
+ specifier: workspace:*
+ version: link:../pogen
+ '@tailwindcss/forms':
+ specifier: ^0.5.3
+ version: 0.5.3(tailwindcss@3.3.2)
+ '@tailwindcss/typography':
+ specifier: ^0.5.9
+ version: 0.5.9(tailwindcss@3.3.2)
+ '@types/chai':
+ specifier: ^4.3.0
+ version: 4.3.3
+ '@types/history':
+ specifier: ^4.7.8
+ version: 4.7.11
+ '@types/mocha':
+ specifier: ^10.0.1
+ version: 10.0.1
+ '@types/node':
+ specifier: ^18.11.17
+ version: 18.11.17
+ '@typescript-eslint/eslint-plugin':
+ specifier: ^6.19.0
+ version: 6.19.0(@typescript-eslint/parser@6.19.0)(eslint@8.56.0)(typescript@5.3.3)
+ '@typescript-eslint/parser':
+ specifier: ^6.19.0
+ version: 6.19.0(eslint@8.56.0)(typescript@5.3.3)
+ autoprefixer:
+ specifier: ^10.4.14
+ version: 10.4.14(postcss@8.4.33)
+ chai:
+ specifier: ^4.3.6
+ version: 4.3.6
+ esbuild:
+ specifier: ^0.19.9
+ version: 0.19.9
+ eslint:
+ specifier: ^8.56.0
+ version: 8.56.0
+ eslint-config-prettier:
+ specifier: ^9.1.0
+ version: 9.1.0(eslint@8.56.0)
+ eslint-plugin-react:
+ specifier: ^7.33.2
+ version: 7.33.2(eslint@8.56.0)
+ mocha:
+ specifier: 9.2.0
+ version: 9.2.0
+ po2json:
+ specifier: ^0.4.5
+ version: 0.4.5
+ tailwindcss:
+ specifier: ^3.3.2
+ version: 3.3.2
+ typescript:
+ specifier: 5.3.3
+ version: 5.3.3
packages/idb-bridge:
- specifiers:
- '@types/node': ^18.8.5
- ava: ^4.3.3
- esm: ^3.2.25
- prettier: ^2.5.1
- rimraf: ^3.0.2
- tslib: ^2.4.0
- typescript: ^4.8.4
- dependencies:
- tslib: 2.4.0
+ dependencies:
+ tslib:
+ specifier: ^2.6.2
+ version: 2.6.2
+ optionalDependencies:
+ better-sqlite3:
+ specifier: 9.4.0
+ version: 9.4.0
devDependencies:
- '@types/node': 18.11.5
- ava: 4.3.3
- esm: 3.2.25
- prettier: 2.7.1
- rimraf: 3.0.2
- typescript: 4.8.4
+ '@types/better-sqlite3':
+ specifier: ^7.6.8
+ version: 7.6.8
+ '@types/node':
+ specifier: ^20.4.1
+ version: 20.4.1
+ ava:
+ specifier: ^6.0.1
+ version: 6.0.1(@ava/typescript@4.1.0)
+ prettier:
+ specifier: ^3.1.1
+ version: 3.1.1
+ typescript:
+ specifier: ^5.3.3
+ version: 5.3.3
packages/merchant-backend-ui:
- specifiers:
- '@babel/core': 7.18.9
- '@babel/plugin-transform-react-jsx-source': 7.18.6
- '@creativebulma/bulma-tooltip': ^1.2.0
- '@gnu-taler/pogen': ^0.0.5
- '@gnu-taler/taler-util': workspace:*
- '@linaria/babel-preset': 3.0.0-beta.22
- '@linaria/core': 3.0.0-beta.22
- '@linaria/react': 3.0.0-beta.22
- '@linaria/rollup': 3.0.0-beta.22
- '@linaria/shaker': 3.0.0-beta.22
- '@linaria/webpack-loader': 3.0.0-beta.22
- '@rollup/plugin-alias': ^3.1.5
- '@rollup/plugin-babel': ^5.3.0
- '@rollup/plugin-commonjs': ^20.0.0
- '@rollup/plugin-html': ^0.2.3
- '@rollup/plugin-image': ^2.1.1
- '@rollup/plugin-json': ^4.1.0
- '@rollup/plugin-replace': ^3.0.0
- '@rollup/plugin-typescript': ^8.2.5
- '@storybook/addon-a11y': ^6.2.9
- '@storybook/addon-actions': ^6.2.9
- '@storybook/addon-essentials': ^6.2.9
- '@storybook/addon-links': ^6.2.9
- '@storybook/preact': ^6.2.9
- '@storybook/preset-scss': ^1.0.3
- '@testing-library/preact': ^2.0.1
- '@testing-library/preact-hooks': ^1.1.0
- '@types/enzyme': ^3.10.8
- '@types/history': ^4.7.8
- '@types/jest': ^26.0.23
- '@types/mocha': ^8.2.2
- '@types/mustache': ^4.1.2
- '@typescript-eslint/eslint-plugin': ^4.22.0
- '@typescript-eslint/parser': ^4.22.0
- axios: ^0.21.1
- babel-loader: ^8.2.2
- base64-inline-loader: ^1.1.1
- bulma: ^0.9.2
- bulma-checkbox: ^1.1.1
- bulma-radio: ^1.1.1
- bulma-responsive-tables: ^1.2.3
- bulma-switch-control: ^1.1.1
- bulma-timeline: ^3.0.4
- bulma-upload-control: ^1.2.0
- date-fns: ^2.21.1
- dotenv: ^8.2.0
- enzyme: ^3.11.0
- enzyme-adapter-preact-pure: ^3.1.0
- eslint: ^7.25.0
- eslint-config-preact: ^1.1.4
- eslint-plugin-header: ^3.1.1
- history: 4.10.1
- html-webpack-inline-chunk-plugin: ^1.1.1
- html-webpack-inline-source-plugin: 0.0.10
- html-webpack-skip-assets-plugin: ^1.0.1
- inline-chunk-html-plugin: ^1.1.1
- jed: ^1.1.1
- jest: ^26.6.3
- jest-preset-preact: ^4.0.2
- mustache: ^4.2.0
- po2json: ^0.4.5
- preact: ^10.5.13
- preact-cli: ^3.0.5
- preact-render-to-json: ^3.6.6
- preact-render-to-string: ^5.1.19
- preact-router: ^3.2.1
- qrcode-generator: ^1.4.4
- rimraf: ^3.0.2
- rollup: ^2.56.3
- rollup-plugin-bundle-html: ^0.2.2
- rollup-plugin-css-only: ^3.1.0
- sass: ^1.32.13
- sass-loader: 10.1.1
- script-ext-html-webpack-plugin: ^2.1.5
- sirv-cli: ^1.0.11
- swr: ^0.5.5
- tslib: ^2.3.1
- typedoc: ^0.20.36
- typescript: ^4.2.4
- yup: ^0.32.9
- dependencies:
- '@gnu-taler/taler-util': link:../taler-util
- axios: 0.21.4
- date-fns: 2.29.3
- history: 4.10.1
- jed: 1.1.1
- preact: 10.11.2
- preact-router: 3.2.1_preact@10.11.2
- qrcode-generator: 1.4.4
- swr: 0.5.7
- yup: 0.32.11
+ dependencies:
+ date-fns:
+ specifier: ^2.21.1
+ version: 2.29.3
+ preact:
+ specifier: 10.11.3
+ version: 10.11.3
+ qrcode-generator:
+ specifier: ^1.4.4
+ version: 1.4.4
devDependencies:
- '@babel/core': 7.18.9
- '@babel/plugin-transform-react-jsx-source': 7.18.6_@babel+core@7.18.9
- '@creativebulma/bulma-tooltip': 1.2.0
- '@gnu-taler/pogen': link:../pogen
- '@linaria/babel-preset': 3.0.0-beta.22
- '@linaria/core': 3.0.0-beta.22
- '@linaria/react': 3.0.0-beta.22
- '@linaria/rollup': 3.0.0-beta.22
- '@linaria/shaker': 3.0.0-beta.22
- '@linaria/webpack-loader': 3.0.0-beta.22
- '@rollup/plugin-alias': 3.1.9_rollup@2.79.1
- '@rollup/plugin-babel': 5.3.1_cwbsg774jzhqoll5t2xfwyzz54
- '@rollup/plugin-commonjs': 20.0.0_rollup@2.79.1
- '@rollup/plugin-html': 0.2.4_rollup@2.79.1
- '@rollup/plugin-image': 2.1.1_rollup@2.79.1
- '@rollup/plugin-json': 4.1.0_rollup@2.79.1
- '@rollup/plugin-replace': 3.1.0_rollup@2.79.1
- '@rollup/plugin-typescript': 8.5.0_hafrwlgfjmvsm7253l3bfjzhnq
- '@storybook/addon-a11y': 6.5.13
- '@storybook/addon-actions': 6.5.13
- '@storybook/addon-essentials': 6.5.13_dazlt7ye7nu7xsezygxn7bviwy
- '@storybook/addon-links': 6.5.13
- '@storybook/preact': 6.5.13_xeiodxunknjqnq4up5ebnv6gwe
- '@storybook/preset-scss': 1.0.3_sass-loader@10.1.1
- '@testing-library/preact': 2.0.1_preact@10.11.2
- '@testing-library/preact-hooks': 1.1.0_aub6lnx45vk623d66chdvib7ry
- '@types/enzyme': 3.10.12
- '@types/history': 4.7.11
- '@types/jest': 26.0.24
- '@types/mocha': 8.2.3
- '@types/mustache': 4.2.1
- '@typescript-eslint/eslint-plugin': 4.33.0_k4l66av2tbo6kxzw52jzgbfzii
- '@typescript-eslint/parser': 4.33.0_3rubbgt5ekhqrcgx4uwls3neim
- babel-loader: 8.2.5_@babel+core@7.18.9
- base64-inline-loader: 1.1.1
- bulma: 0.9.4
- bulma-checkbox: 1.2.1
- bulma-radio: 1.2.0
- bulma-responsive-tables: 1.2.5
- bulma-switch-control: 1.2.2
- bulma-timeline: 3.0.5
- bulma-upload-control: 1.2.0
- dotenv: 8.6.0
- enzyme: 3.11.0
- enzyme-adapter-preact-pure: 3.4.0_iis6uhuvbdn4qfhp3h7qledc2m
- eslint: 7.32.0
- eslint-config-preact: 1.3.0_nxlzr75jbqkso2fds5zjovs2ii
- eslint-plugin-header: 3.1.1_eslint@7.32.0
- html-webpack-inline-chunk-plugin: 1.1.1
- html-webpack-inline-source-plugin: 0.0.10
- html-webpack-skip-assets-plugin: 1.0.3
- inline-chunk-html-plugin: 1.1.1
- jest: 26.6.3
- jest-preset-preact: 4.0.5_k4rseuq4tu3ktjhgekqzusjwfq
- mustache: 4.2.0
- po2json: 0.4.5
- preact-cli: 3.4.1_rg336vsyl675j3voc2t5224afq
- preact-render-to-json: 3.6.6_preact@10.11.2
- preact-render-to-string: 5.2.6_preact@10.11.2
- rimraf: 3.0.2
- rollup: 2.79.1
- rollup-plugin-bundle-html: 0.2.2
- rollup-plugin-css-only: 3.1.0_rollup@2.79.1
- sass: 1.55.0
- sass-loader: 10.1.1_sass@1.55.0
- script-ext-html-webpack-plugin: 2.1.5
- sirv-cli: 1.0.14
- tslib: 2.4.0
- typedoc: 0.20.37_typescript@4.8.4
- typescript: 4.8.4
+ '@babel/core':
+ specifier: 7.18.9
+ version: 7.18.9
+ '@gnu-taler/pogen':
+ specifier: workspace:*
+ version: link:../pogen
+ '@linaria/babel-preset':
+ specifier: 3.0.0-beta.22
+ version: 3.0.0-beta.22
+ '@linaria/core':
+ specifier: 3.0.0-beta.22
+ version: 3.0.0-beta.22
+ '@linaria/react':
+ specifier: 3.0.0-beta.22
+ version: 3.0.0-beta.22(react@18.2.0)
+ '@linaria/shaker':
+ specifier: 3.0.0-beta.22
+ version: 3.0.0-beta.22
+ '@linaria/webpack-loader':
+ specifier: 3.0.0-beta.22
+ version: 3.0.0-beta.22(webpack@4.47.0)
+ '@types/mocha':
+ specifier: ^8.2.2
+ version: 8.2.3
+ '@types/mustache':
+ specifier: ^4.1.2
+ version: 4.2.1
+ '@types/node':
+ specifier: ^20.11.13
+ version: 20.11.13
+ '@typescript-eslint/eslint-plugin':
+ specifier: ^4.22.0
+ version: 4.33.0(@typescript-eslint/parser@4.33.0)(eslint@7.32.0)(typescript@5.3.3)
+ '@typescript-eslint/parser':
+ specifier: ^4.22.0
+ version: 4.33.0(eslint@7.32.0)(typescript@5.3.3)
+ babel-loader:
+ specifier: ^8.2.2
+ version: 8.2.5(@babel/core@7.18.9)(webpack@4.47.0)
+ base64-inline-loader:
+ specifier: ^1.1.1
+ version: 1.1.1(webpack@4.47.0)
+ eslint:
+ specifier: ^7.25.0
+ version: 7.32.0
+ eslint-config-preact:
+ specifier: ^1.1.4
+ version: 1.3.0(@typescript-eslint/eslint-plugin@4.33.0)(eslint@7.32.0)(typescript@5.3.3)
+ eslint-plugin-header:
+ specifier: ^3.1.1
+ version: 3.1.1(eslint@7.32.0)
+ mustache:
+ specifier: ^4.2.0
+ version: 4.2.0
+ po2json:
+ specifier: ^0.4.5
+ version: 0.4.5
+ preact-render-to-string:
+ specifier: ^5.1.19
+ version: 5.2.6(preact@10.11.3)
+ sirv-cli:
+ specifier: ^1.0.11
+ version: 1.0.14
+ ts-node:
+ specifier: ^10.9.1
+ version: 10.9.1(@types/node@20.11.13)(typescript@5.3.3)
+ tslib:
+ specifier: 2.6.2
+ version: 2.6.2
+ typescript:
+ specifier: 5.3.3
+ version: 5.3.3
packages/merchant-backoffice-ui:
- specifiers:
- '@babel/core': 7.18.9
- '@babel/plugin-transform-react-jsx-source': 7.18.6
- '@creativebulma/bulma-tooltip': ^1.2.0
- '@gnu-taler/pogen': ^0.0.5
- '@gnu-taler/taler-util': workspace:*
- '@gnu-taler/web-util': workspace:*
- '@storybook/addon-a11y': ^6.2.9
- '@storybook/addon-actions': ^6.2.9
- '@storybook/addon-essentials': ^6.2.9
- '@storybook/addon-links': ^6.2.9
- '@storybook/preact': ^6.2.9
- '@storybook/preset-scss': ^1.0.3
- '@testing-library/preact': ^2.0.1
- '@testing-library/preact-hooks': ^1.1.0
- '@types/history': ^4.7.8
- '@types/jest': ^26.0.23
- '@types/mocha': ^8.2.2
- '@types/node': ^18.8.5
- '@typescript-eslint/eslint-plugin': ^4.22.0
- '@typescript-eslint/parser': ^4.22.0
- axios: ^0.21.1
- babel-loader: ^8.2.2
- base64-inline-loader: ^1.1.1
- bulma: ^0.9.2
- bulma-checkbox: ^1.1.1
- bulma-radio: ^1.1.1
- bulma-responsive-tables: ^1.2.3
- bulma-switch-control: ^1.1.1
- bulma-timeline: ^3.0.4
- bulma-upload-control: ^1.2.0
- date-fns: 2.29.3
- dotenv: ^8.2.0
- eslint: ^7.25.0
- eslint-config-preact: ^1.1.4
- eslint-plugin-header: ^3.1.1
- history: 4.10.1
- html-webpack-inline-chunk-plugin: ^1.1.1
- html-webpack-inline-source-plugin: 0.0.10
- html-webpack-skip-assets-plugin: ^1.0.1
- inline-chunk-html-plugin: ^1.1.1
- jed: 1.1.1
- jest: ^26.6.3
- jest-preset-preact: ^4.0.2
- po2json: ^0.4.5
- preact: 10.6.5
- preact-cli: ^3.0.5
- preact-render-to-json: ^3.6.6
- preact-render-to-string: ^5.1.19
- preact-router: 3.2.1
- qrcode-generator: 1.4.4
- react: npm:@preact/compat@^17.1.2
- rimraf: ^3.0.2
- sass: ^1.32.13
- sass-loader: 10.1.1
- script-ext-html-webpack-plugin: ^2.1.5
- sirv-cli: ^1.0.11
- swr: 1.3.0
- typedoc: ^0.20.36
- typescript: 4.4.4
- yup: ^0.32.9
- dependencies:
- '@gnu-taler/taler-util': link:../taler-util
- '@gnu-taler/web-util': link:../web-util
- axios: 0.21.4
- date-fns: 2.29.3
- history: 4.10.1
- jed: 1.1.1
- preact: 10.6.5
- preact-router: 3.2.1_preact@10.6.5
- qrcode-generator: 1.4.4
- react: /@preact/compat/17.1.2_preact@10.6.5
- swr: 1.3.0_@preact+compat@17.1.2
- yup: 0.32.11
+ dependencies:
+ '@gnu-taler/taler-util':
+ specifier: workspace:*
+ version: link:../taler-util
+ '@gnu-taler/web-util':
+ specifier: workspace:*
+ version: link:../web-util
+ date-fns:
+ specifier: 2.29.3
+ version: 2.29.3
+ history:
+ specifier: 4.10.1
+ version: 4.10.1
+ jed:
+ specifier: 1.1.1
+ version: 1.1.1
+ preact:
+ specifier: 10.11.3
+ version: 10.11.3
+ preact-router:
+ specifier: 3.2.1
+ version: 3.2.1(preact@10.11.3)
+ qrcode-generator:
+ specifier: 1.4.4
+ version: 1.4.4
+ swr:
+ specifier: 2.2.2
+ version: 2.2.2(react@18.2.0)
+ yup:
+ specifier: ^0.32.9
+ version: 0.32.11
devDependencies:
- '@babel/core': 7.18.9
- '@babel/plugin-transform-react-jsx-source': 7.18.6_@babel+core@7.18.9
- '@creativebulma/bulma-tooltip': 1.2.0
- '@gnu-taler/pogen': link:../pogen
- '@storybook/addon-a11y': 6.5.13_@preact+compat@17.1.2
- '@storybook/addon-actions': 6.5.13_@preact+compat@17.1.2
- '@storybook/addon-essentials': 6.5.13_e4oandlgwkzlaxn6zc3btvol24
- '@storybook/addon-links': 6.5.13_@preact+compat@17.1.2
- '@storybook/preact': 6.5.13_q57tjbu375p52k2lkxkownwouu
- '@storybook/preset-scss': 1.0.3_sass-loader@10.1.1
- '@testing-library/preact': 2.0.1_preact@10.6.5
- '@testing-library/preact-hooks': 1.1.0_vfcmu6iy7nffpurikpgxo6gwxi
- '@types/history': 4.7.11
- '@types/jest': 26.0.24
- '@types/mocha': 8.2.3
- '@types/node': 18.11.5
- '@typescript-eslint/eslint-plugin': 4.33.0_zrqxgwgitu7trrjeml3nqco3jq
- '@typescript-eslint/parser': 4.33.0_wnilx7boviscikmvsfkd6ljepe
- babel-loader: 8.2.5_@babel+core@7.18.9
- base64-inline-loader: 1.1.1
- bulma: 0.9.4
- bulma-checkbox: 1.2.1
- bulma-radio: 1.2.0
- bulma-responsive-tables: 1.2.5
- bulma-switch-control: 1.2.2
- bulma-timeline: 3.0.5
- bulma-upload-control: 1.2.0
- dotenv: 8.6.0
- eslint: 7.32.0
- eslint-config-preact: 1.3.0_55vw575o5aj4h37h7cossdtfje
- eslint-plugin-header: 3.1.1_eslint@7.32.0
- html-webpack-inline-chunk-plugin: 1.1.1
- html-webpack-inline-source-plugin: 0.0.10
- html-webpack-skip-assets-plugin: 1.0.3
- inline-chunk-html-plugin: 1.1.1
- jest: 26.6.3
- jest-preset-preact: 4.0.5_moqeqtbsr7edkxzj3jgnhqkxsm
- po2json: 0.4.5
- preact-cli: 3.4.1_bbfn3vavr32nbxkp5peg6brs4i
- preact-render-to-json: 3.6.6_preact@10.6.5
- preact-render-to-string: 5.2.6_preact@10.6.5
- rimraf: 3.0.2
- sass: 1.55.0
- sass-loader: 10.1.1_sass@1.55.0
- script-ext-html-webpack-plugin: 2.1.5
- sirv-cli: 1.0.14
- typedoc: 0.20.37_typescript@4.4.4
- typescript: 4.4.4
+ '@creativebulma/bulma-tooltip':
+ specifier: ^1.2.0
+ version: 1.2.0
+ '@gnu-taler/pogen':
+ specifier: workspace:*
+ version: link:../pogen
+ '@types/chai':
+ specifier: ^4.3.0
+ version: 4.3.3
+ '@types/history':
+ specifier: ^4.7.8
+ version: 4.7.11
+ '@types/mocha':
+ specifier: ^8.2.3
+ version: 8.2.3
+ '@types/node':
+ specifier: ^18.11.17
+ version: 18.11.17
+ '@typescript-eslint/eslint-plugin':
+ specifier: ^6.19.0
+ version: 6.19.0(@typescript-eslint/parser@6.19.0)(eslint@8.56.0)(typescript@5.3.3)
+ '@typescript-eslint/parser':
+ specifier: ^6.19.0
+ version: 6.19.0(eslint@8.56.0)(typescript@5.3.3)
+ base64-inline-loader:
+ specifier: ^1.1.1
+ version: 1.1.1(webpack@4.47.0)
+ bulma:
+ specifier: ^0.9.2
+ version: 0.9.4
+ bulma-checkbox:
+ specifier: ^1.1.1
+ version: 1.2.1
+ bulma-radio:
+ specifier: ^1.1.1
+ version: 1.2.0
+ bulma-responsive-tables:
+ specifier: ^1.2.3
+ version: 1.2.5
+ bulma-switch-control:
+ specifier: ^1.1.1
+ version: 1.2.2
+ bulma-timeline:
+ specifier: ^3.0.4
+ version: 3.0.5
+ bulma-upload-control:
+ specifier: ^1.2.0
+ version: 1.2.0
+ chai:
+ specifier: ^4.3.6
+ version: 4.3.6
+ dotenv:
+ specifier: ^8.2.0
+ version: 8.6.0
+ eslint:
+ specifier: ^8.56.0
+ version: 8.56.0
+ eslint-config-prettier:
+ specifier: ^9.1.0
+ version: 9.1.0(eslint@8.56.0)
+ eslint-plugin-react:
+ specifier: ^7.33.2
+ version: 7.33.2(eslint@8.56.0)
+ html-webpack-inline-chunk-plugin:
+ specifier: ^1.1.1
+ version: 1.1.1
+ html-webpack-inline-source-plugin:
+ specifier: 0.0.10
+ version: 0.0.10
+ html-webpack-skip-assets-plugin:
+ specifier: ^1.0.1
+ version: 1.0.3(html-webpack-plugin@5.5.4)(webpack@4.47.0)
+ inline-chunk-html-plugin:
+ specifier: ^1.1.1
+ version: 1.1.1
+ mocha:
+ specifier: ^9.2.0
+ version: 9.2.2
+ preact-render-to-string:
+ specifier: ^5.2.6
+ version: 5.2.6(preact@10.11.3)
+ sass:
+ specifier: 1.56.1
+ version: 1.56.1
+ source-map-support:
+ specifier: ^0.5.21
+ version: 0.5.21
+ typedoc:
+ specifier: ^0.25.4
+ version: 0.25.4(typescript@5.3.3)
+ typescript:
+ specifier: 5.3.3
+ version: 5.3.3
packages/pogen:
- specifiers:
- '@types/node': ^18.8.5
- glob: ^7.2.0
- po2json: ^0.4.5
- typescript: ^4.8.4
dependencies:
- '@types/node': 18.11.5
- glob: 7.2.3
+ '@types/node':
+ specifier: ^18.11.17
+ version: 18.11.17
+ glob:
+ specifier: ^10.3.10
+ version: 10.3.10
devDependencies:
- po2json: 0.4.5
- typescript: 4.8.4
+ po2json:
+ specifier: ^0.4.5
+ version: 0.4.5
+ typescript:
+ specifier: ^5.3.3
+ version: 5.3.3
+
+ packages/taler-harness:
+ dependencies:
+ '@gnu-taler/taler-util':
+ specifier: workspace:*
+ version: link:../taler-util
+ '@gnu-taler/taler-wallet-core':
+ specifier: workspace:*
+ version: link:../taler-wallet-core
+ tslib:
+ specifier: ^2.6.2
+ version: 2.6.2
+ devDependencies:
+ '@types/node':
+ specifier: ^18.11.17
+ version: 18.11.17
+ esbuild:
+ specifier: ^0.19.9
+ version: 0.19.9
+ prettier:
+ specifier: ^3.1.1
+ version: 3.1.1
+ typescript:
+ specifier: ^5.3.3
+ version: 5.3.3
packages/taler-util:
- specifiers:
- '@types/node': ^18.8.5
- ava: ^4.3.3
- big-integer: ^1.6.51
- esbuild: ^0.14.21
- fflate: ^0.7.4
- jed: ^1.1.1
- prettier: ^2.5.1
- rimraf: ^3.0.2
- tslib: ^2.4.0
- typescript: ^4.8.4
- dependencies:
- big-integer: 1.6.51
- fflate: 0.7.4
- jed: 1.1.1
- tslib: 2.4.0
+ dependencies:
+ big-integer:
+ specifier: ^1.6.52
+ version: 1.6.52
+ fflate:
+ specifier: ^0.8.1
+ version: 0.8.1
+ follow-redirects:
+ specifier: ^1.15.5
+ version: 1.15.5
+ hash-wasm:
+ specifier: ^4.11.0
+ version: 4.11.0
+ jed:
+ specifier: ^1.1.1
+ version: 1.1.1
+ tslib:
+ specifier: ^2.6.2
+ version: 2.6.2
devDependencies:
- '@types/node': 18.11.5
- ava: 4.3.3
- esbuild: 0.14.54
- prettier: 2.7.1
- rimraf: 3.0.2
- typescript: 4.8.4
+ '@types/follow-redirects':
+ specifier: ^1.14.4
+ version: 1.14.4
+ '@types/node':
+ specifier: ^18.11.17
+ version: 18.11.17
+ '@typescript-eslint/eslint-plugin':
+ specifier: ^6.19.0
+ version: 6.19.0(@typescript-eslint/parser@6.19.0)(eslint@8.56.0)(typescript@5.3.3)
+ '@typescript-eslint/parser':
+ specifier: ^6.19.0
+ version: 6.19.0(eslint@8.56.0)(typescript@5.3.3)
+ ava:
+ specifier: ^6.0.1
+ version: 6.0.1(@ava/typescript@4.1.0)
+ esbuild:
+ specifier: ^0.19.9
+ version: 0.19.9
+ eslint:
+ specifier: ^8.56.0
+ version: 8.56.0
+ typescript:
+ specifier: ^5.3.3
+ version: 5.3.3
packages/taler-wallet-cli:
- specifiers:
- '@gnu-taler/taler-util': workspace:*
- '@gnu-taler/taler-wallet-core': workspace:*
- '@rollup/plugin-commonjs': ^22.0.2
- '@rollup/plugin-json': ^4.1.0
- '@rollup/plugin-node-resolve': ^13.3.0
- '@rollup/plugin-replace': ^4.0.0
- '@types/node': ^18.8.5
- axios: ^0.27.2
- prettier: ^2.5.1
- rimraf: ^3.0.2
- rollup: ^2.79.0
- rollup-plugin-sourcemaps: ^0.6.3
- rollup-plugin-terser: ^7.0.2
- tslib: ^2.4.0
- typedoc: ^0.23.16
- typescript: ^4.8.4
- dependencies:
- '@gnu-taler/taler-util': link:../taler-util
- '@gnu-taler/taler-wallet-core': link:../taler-wallet-core
- axios: 0.27.2
- tslib: 2.4.0
+ dependencies:
+ '@gnu-taler/taler-util':
+ specifier: workspace:*
+ version: link:../taler-util
+ '@gnu-taler/taler-wallet-core':
+ specifier: workspace:*
+ version: link:../taler-wallet-core
+ tslib:
+ specifier: ^2.6.2
+ version: 2.6.2
devDependencies:
- '@rollup/plugin-commonjs': 22.0.2_rollup@2.79.1
- '@rollup/plugin-json': 4.1.0_rollup@2.79.1
- '@rollup/plugin-node-resolve': 13.3.0_rollup@2.79.1
- '@rollup/plugin-replace': 4.0.0_rollup@2.79.1
- '@types/node': 18.11.5
- prettier: 2.7.1
- rimraf: 3.0.2
- rollup: 2.79.1
- rollup-plugin-sourcemaps: 0.6.3_cxvqwjbwwnthagk6mnrmqfhepa
- rollup-plugin-terser: 7.0.2_rollup@2.79.1
- typedoc: 0.23.18_typescript@4.8.4
- typescript: 4.8.4
+ '@types/node':
+ specifier: ^18.11.17
+ version: 18.11.17
+ prettier:
+ specifier: ^3.1.1
+ version: 3.1.1
+ typedoc:
+ specifier: ^0.25.4
+ version: 0.25.4(typescript@5.3.3)
+ typescript:
+ specifier: ^5.3.3
+ version: 5.3.3
packages/taler-wallet-core:
- specifiers:
- '@ava/typescript': ^3.0.1
- '@gnu-taler/idb-bridge': workspace:*
- '@gnu-taler/pogen': workspace:*
- '@gnu-taler/taler-util': workspace:*
- '@types/node': ^18.8.5
- '@typescript-eslint/eslint-plugin': ^5.36.1
- '@typescript-eslint/parser': ^5.36.1
- ava: ^4.3.3
- axios: ^0.27.2
- big-integer: ^1.6.51
- c8: ^7.11.0
- eslint: ^8.8.0
- eslint-config-airbnb-typescript: ^16.1.0
- eslint-plugin-import: ^2.25.4
- eslint-plugin-jsx-a11y: ^6.5.1
- eslint-plugin-react: ^7.28.0
- eslint-plugin-react-hooks: ^4.3.0
- fflate: ^0.7.4
- jed: ^1.1.1
- po2json: ^0.4.5
- prettier: ^2.5.1
- rimraf: ^3.0.2
- tslib: ^2.4.0
- typedoc: ^0.23.16
- typescript: ^4.8.4
- dependencies:
- '@gnu-taler/idb-bridge': link:../idb-bridge
- '@gnu-taler/taler-util': link:../taler-util
- '@types/node': 18.11.5
- axios: 0.27.2
- big-integer: 1.6.51
- fflate: 0.7.4
- tslib: 2.4.0
+ dependencies:
+ '@gnu-taler/idb-bridge':
+ specifier: workspace:*
+ version: link:../idb-bridge
+ '@gnu-taler/taler-util':
+ specifier: workspace:*
+ version: link:../taler-util
+ '@types/node':
+ specifier: ^18.11.17
+ version: 18.11.17
+ big-integer:
+ specifier: ^1.6.52
+ version: 1.6.52
+ fflate:
+ specifier: ^0.8.1
+ version: 0.8.1
+ tslib:
+ specifier: ^2.6.2
+ version: 2.6.2
devDependencies:
- '@ava/typescript': 3.0.1
- '@gnu-taler/pogen': link:../pogen
- '@typescript-eslint/eslint-plugin': 5.41.0_huremdigmcnkianavgfk3x6iou
- '@typescript-eslint/parser': 5.41.0_wyqvi574yv7oiwfeinomdzmc3m
- ava: 4.3.3_@ava+typescript@3.0.1
- c8: 7.12.0
- eslint: 8.26.0
- eslint-config-airbnb-typescript: 16.2.0_yxexh7lkdp6zshkc5735fso3gu
- eslint-plugin-import: 2.26.0_c2flhriocdzler6lrwbyxxyoca
- eslint-plugin-jsx-a11y: 6.6.1_eslint@8.26.0
- eslint-plugin-react: 7.31.10_eslint@8.26.0
- eslint-plugin-react-hooks: 4.6.0_eslint@8.26.0
- jed: 1.1.1
- po2json: 0.4.5
- prettier: 2.7.1
- rimraf: 3.0.2
- typedoc: 0.23.18_typescript@4.8.4
- typescript: 4.8.4
+ '@ava/typescript':
+ specifier: ^4.1.0
+ version: 4.1.0
+ '@gnu-taler/pogen':
+ specifier: workspace:*
+ version: link:../pogen
+ '@typescript-eslint/eslint-plugin':
+ specifier: ^5.36.1
+ version: 5.41.0(@typescript-eslint/parser@5.41.0)(eslint@8.26.0)(typescript@5.3.3)
+ '@typescript-eslint/parser':
+ specifier: ^5.36.1
+ version: 5.41.0(eslint@8.26.0)(typescript@5.3.3)
+ ava:
+ specifier: ^6.0.1
+ version: 6.0.1(@ava/typescript@4.1.0)
+ c8:
+ specifier: ^8.0.1
+ version: 8.0.1
+ eslint:
+ specifier: ^8.8.0
+ version: 8.26.0
+ eslint-config-airbnb-typescript:
+ specifier: ^17.1.0
+ version: 17.1.0(@typescript-eslint/eslint-plugin@5.41.0)(@typescript-eslint/parser@5.41.0)(eslint-plugin-import@2.29.1)(eslint@8.26.0)
+ eslint-plugin-import:
+ specifier: ^2.29.1
+ version: 2.29.1(@typescript-eslint/parser@5.41.0)(eslint@8.26.0)
+ eslint-plugin-jsx-a11y:
+ specifier: ^6.8.0
+ version: 6.8.0(eslint@8.26.0)
+ eslint-plugin-react:
+ specifier: ^7.33.2
+ version: 7.33.2(eslint@8.26.0)
+ eslint-plugin-react-hooks:
+ specifier: ^4.3.0
+ version: 4.6.0(eslint@8.26.0)
+ jed:
+ specifier: ^1.1.1
+ version: 1.1.1
+ po2json:
+ specifier: ^0.4.5
+ version: 0.4.5
+ prettier:
+ specifier: ^3.1.1
+ version: 3.1.1
+ typedoc:
+ specifier: ^0.25.4
+ version: 0.25.4(typescript@5.3.3)
+ typescript:
+ specifier: ^5.3.3
+ version: 5.3.3
packages/taler-wallet-embedded:
- specifiers:
- '@gnu-taler/idb-bridge': workspace:*
- '@gnu-taler/taler-util': workspace:*
- '@gnu-taler/taler-wallet-core': workspace:*
- '@rollup/plugin-commonjs': ^22.0.2
- '@rollup/plugin-json': ^4.1.0
- '@rollup/plugin-node-resolve': ^13.3.0
- '@rollup/plugin-replace': ^4.0.0
- '@types/node': ^18.8.5
- prettier: ^2.5.1
- rimraf: ^3.0.2
- rollup: ^2.79.0
- rollup-plugin-sourcemaps: ^0.6.3
- rollup-plugin-terser: ^7.0.2
- tslib: ^2.4.0
- typescript: ^4.8.4
- dependencies:
- '@gnu-taler/idb-bridge': link:../idb-bridge
- '@gnu-taler/taler-util': link:../taler-util
- '@gnu-taler/taler-wallet-core': link:../taler-wallet-core
- tslib: 2.4.0
+ dependencies:
+ '@gnu-taler/anastasis-core':
+ specifier: workspace:*
+ version: link:../anastasis-core
+ '@gnu-taler/idb-bridge':
+ specifier: workspace:*
+ version: link:../idb-bridge
+ '@gnu-taler/taler-util':
+ specifier: workspace:*
+ version: link:../taler-util
+ '@gnu-taler/taler-wallet-core':
+ specifier: workspace:*
+ version: link:../taler-wallet-core
+ tslib:
+ specifier: ^2.6.2
+ version: 2.6.2
devDependencies:
- '@rollup/plugin-commonjs': 22.0.2_rollup@2.79.1
- '@rollup/plugin-json': 4.1.0_rollup@2.79.1
- '@rollup/plugin-node-resolve': 13.3.0_rollup@2.79.1
- '@rollup/plugin-replace': 4.0.0_rollup@2.79.1
- '@types/node': 18.11.5
- prettier: 2.7.1
- rimraf: 3.0.2
- rollup: 2.79.1
- rollup-plugin-sourcemaps: 0.6.3_cxvqwjbwwnthagk6mnrmqfhepa
- rollup-plugin-terser: 7.0.2_rollup@2.79.1
- typescript: 4.8.4
+ '@types/node':
+ specifier: ^18.11.17
+ version: 18.11.17
+ esbuild:
+ specifier: ^0.19.9
+ version: 0.19.9
+ prettier:
+ specifier: ^3.1.1
+ version: 3.1.1
packages/taler-wallet-webextension:
- specifiers:
- '@babel/core': 7.18.9
- '@babel/plugin-transform-modules-commonjs': 7.18.6
- '@babel/plugin-transform-react-jsx-source': 7.18.6
- '@babel/preset-typescript': 7.18.6
- '@babel/runtime': 7.18.9
- '@gnu-taler/pogen': workspace:*
- '@gnu-taler/taler-util': workspace:*
- '@gnu-taler/taler-wallet-core': workspace:*
- '@gnu-taler/web-util': workspace:*
- '@linaria/babel-preset': 3.0.0-beta.22
- '@linaria/core': 3.0.0-beta.22
- '@linaria/react': 3.0.0-beta.22
- '@linaria/webpack-loader': 3.0.0-beta.22
- '@types/chai': ^4.3.0
- '@types/chrome': 0.0.197
- '@types/history': ^4.7.8
- '@types/mocha': ^9.0.0
- '@types/node': ^18.8.5
- babel-loader: ^8.2.3
- babel-plugin-transform-react-jsx: ^6.24.1
- chai: ^4.3.6
- date-fns: ^2.29.2
- esbuild: ^0.15.13
- history: 4.10.1
- mocha: ^9.2.0
- nyc: ^15.1.0
- polished: ^4.1.4
- preact: 10.11.3
- preact-cli: ^3.3.5
- preact-render-to-string: ^5.1.19
- preact-router: 3.2.1
- qr-scanner: ^1.4.1
- qrcode-generator: ^1.4.4
- rimraf: ^3.0.2
- tslib: ^2.4.0
- typescript: ^4.8.4
- dependencies:
- '@gnu-taler/taler-util': link:../taler-util
- '@gnu-taler/taler-wallet-core': link:../taler-wallet-core
- date-fns: 2.29.3
- history: 4.10.1
- preact: 10.11.3
- preact-router: 3.2.1_preact@10.11.3
- qr-scanner: 1.4.1
- qrcode-generator: 1.4.4
- tslib: 2.4.0
+ dependencies:
+ '@gnu-taler/taler-util':
+ specifier: workspace:*
+ version: link:../taler-util
+ '@gnu-taler/taler-wallet-core':
+ specifier: workspace:*
+ version: link:../taler-wallet-core
+ date-fns:
+ specifier: ^2.29.2
+ version: 2.29.3
+ history:
+ specifier: 4.10.1
+ version: 4.10.1
+ jsqr:
+ specifier: ^1.4.0
+ version: 1.4.0
+ preact:
+ specifier: 10.11.3
+ version: 10.11.3
+ preact-router:
+ specifier: 3.2.1
+ version: 3.2.1(preact@10.11.3)
+ qrcode-generator:
+ specifier: ^1.4.4
+ version: 1.4.4
+ tslib:
+ specifier: ^2.6.2
+ version: 2.6.2
devDependencies:
- '@babel/core': 7.18.9
- '@babel/plugin-transform-modules-commonjs': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-transform-react-jsx-source': 7.18.6_@babel+core@7.18.9
- '@babel/preset-typescript': 7.18.6_@babel+core@7.18.9
- '@babel/runtime': 7.18.9
- '@gnu-taler/pogen': link:../pogen
- '@gnu-taler/web-util': link:../web-util
- '@linaria/babel-preset': 3.0.0-beta.22
- '@linaria/core': 3.0.0-beta.22
- '@linaria/react': 3.0.0-beta.22
- '@linaria/webpack-loader': 3.0.0-beta.22
- '@types/chai': 4.3.3
- '@types/chrome': 0.0.197
- '@types/history': 4.7.11
- '@types/mocha': 9.1.1
- '@types/node': 18.11.5
- babel-loader: 8.2.5_@babel+core@7.18.9
- babel-plugin-transform-react-jsx: 6.24.1
- chai: 4.3.6
- esbuild: 0.15.13
- mocha: 9.2.2
- nyc: 15.1.0
- polished: 4.2.2
- preact-cli: 3.4.1_i2jslynuqxjzp37vlc24guk7gu
- preact-render-to-string: 5.2.6_preact@10.11.3
- rimraf: 3.0.2
- typescript: 4.8.4
+ '@babel/preset-react':
+ specifier: ^7.22.3
+ version: 7.22.3(@babel/core@7.18.9)
+ '@babel/preset-typescript':
+ specifier: 7.18.6
+ version: 7.18.6(@babel/core@7.18.9)
+ '@gnu-taler/pogen':
+ specifier: workspace:*
+ version: link:../pogen
+ '@gnu-taler/web-util':
+ specifier: workspace:*
+ version: link:../web-util
+ '@linaria/babel-preset':
+ specifier: 5.0.4
+ version: 5.0.4
+ '@linaria/core':
+ specifier: 5.0.2
+ version: 5.0.2
+ '@linaria/esbuild':
+ specifier: 5.0.4
+ version: 5.0.4(esbuild@0.19.9)
+ '@linaria/react':
+ specifier: 5.0.3
+ version: 5.0.3(react@18.2.0)
+ '@linaria/shaker':
+ specifier: 5.0.3
+ version: 5.0.3
+ '@types/chai':
+ specifier: ^4.3.0
+ version: 4.3.3
+ '@types/chrome':
+ specifier: 0.0.197
+ version: 0.0.197
+ '@types/history':
+ specifier: ^4.7.8
+ version: 4.7.11
+ '@types/mocha':
+ specifier: ^9.0.0
+ version: 9.1.1
+ '@types/node':
+ specifier: ^18.11.17
+ version: 18.11.17
+ '@typescript-eslint/eslint-plugin':
+ specifier: ^6.19.0
+ version: 6.19.0(@typescript-eslint/parser@6.19.0)(eslint@8.56.0)(typescript@5.3.3)
+ '@typescript-eslint/parser':
+ specifier: ^6.19.0
+ version: 6.19.0(eslint@8.56.0)(typescript@5.3.3)
+ chai:
+ specifier: ^4.3.6
+ version: 4.3.6
+ esbuild:
+ specifier: ^0.19.9
+ version: 0.19.9
+ eslint:
+ specifier: ^8.56.0
+ version: 8.56.0
+ eslint-config-prettier:
+ specifier: ^9.1.0
+ version: 9.1.0(eslint@8.56.0)
+ eslint-plugin-react:
+ specifier: ^7.33.2
+ version: 7.33.2(eslint@8.56.0)
+ mocha:
+ specifier: ^9.2.0
+ version: 9.2.2
+ nyc:
+ specifier: ^15.1.0
+ version: 15.1.0
+ polished:
+ specifier: ^4.1.4
+ version: 4.2.2
+ preact-cli:
+ specifier: ^3.3.5
+ version: 3.4.1(eslint@8.56.0)(preact-render-to-string@5.2.6)(preact@10.11.3)
+ preact-render-to-string:
+ specifier: ^5.1.19
+ version: 5.2.6(preact@10.11.3)
+ typescript:
+ specifier: 5.3.3
+ version: 5.3.3
+ web-ext:
+ specifier: ^7.11.0
+ version: 7.11.0
packages/web-util:
- specifiers:
- '@gnu-taler/taler-util': workspace:*
- '@types/express': ^4.17.14
- '@types/node': ^18.11.9
- '@types/web': ^0.0.82
- '@types/ws': ^8.5.3
- chokidar: ^3.5.3
- esbuild: ^0.14.21
- express: ^4.18.2
- preact: 10.11.3
- prettier: ^2.5.1
- rimraf: ^3.0.2
- tslib: ^2.4.0
- typescript: ^4.8.4
- ws: 7.4.5
+ dependencies:
+ '@babel/core':
+ specifier: 7.18.9
+ version: 7.18.9
+ '@babel/helper-compilation-targets':
+ specifier: 7.18.9
+ version: 7.18.9(@babel/core@7.18.9)
+ '@types/chrome':
+ specifier: 0.0.197
+ version: 0.0.197
+ tailwindcss:
+ specifier: ^3.3.2
+ version: 3.3.2
devDependencies:
- '@gnu-taler/taler-util': link:../taler-util
- '@types/express': 4.17.14
- '@types/node': 18.11.9
- '@types/web': 0.0.82
- '@types/ws': 8.5.3
- chokidar: 3.5.3
- esbuild: 0.14.54
- express: 4.18.2
- preact: 10.11.3
- prettier: 2.7.1
- rimraf: 3.0.2
- tslib: 2.4.1
- typescript: 4.8.4
- ws: 7.4.5
+ '@babel/preset-react':
+ specifier: ^7.22.3
+ version: 7.22.3(@babel/core@7.18.9)
+ '@babel/preset-typescript':
+ specifier: ^7.21.5
+ version: 7.21.5(@babel/core@7.18.9)
+ '@gnu-taler/taler-util':
+ specifier: workspace:*
+ version: link:../taler-util
+ '@heroicons/react':
+ specifier: ^2.0.17
+ version: 2.0.17(react@18.2.0)
+ '@linaria/babel-preset':
+ specifier: 5.0.4
+ version: 5.0.4
+ '@linaria/core':
+ specifier: 5.0.2
+ version: 5.0.2
+ '@linaria/esbuild':
+ specifier: 5.0.4
+ version: 5.0.4(esbuild@0.19.9)
+ '@linaria/react':
+ specifier: 5.0.3
+ version: 5.0.3(react@18.2.0)
+ '@types/express':
+ specifier: ^4.17.14
+ version: 4.17.14
+ '@types/node':
+ specifier: ^18.11.17
+ version: 18.11.17
+ '@types/web':
+ specifier: ^0.0.82
+ version: 0.0.82
+ '@types/ws':
+ specifier: ^8.5.3
+ version: 8.5.3
+ autoprefixer:
+ specifier: ^10.4.14
+ version: 10.4.14(postcss@8.4.23)
+ chokidar:
+ specifier: ^3.5.3
+ version: 3.5.3
+ date-fns:
+ specifier: 2.29.3
+ version: 2.29.3
+ esbuild:
+ specifier: ^0.19.9
+ version: 0.19.9
+ express:
+ specifier: ^4.18.2
+ version: 4.18.2
+ postcss:
+ specifier: ^8.4.23
+ version: 8.4.23
+ postcss-load-config:
+ specifier: ^4.0.1
+ version: 4.0.1(postcss@8.4.23)
+ preact:
+ specifier: 10.11.3
+ version: 10.11.3
+ preact-render-to-string:
+ specifier: ^5.2.6
+ version: 5.2.6(preact@10.11.3)
+ prettier:
+ specifier: ^3.1.1
+ version: 3.1.1
+ sass:
+ specifier: 1.56.1
+ version: 1.56.1
+ swr:
+ specifier: 2.0.3
+ version: 2.0.3(react@18.2.0)
+ tslib:
+ specifier: ^2.6.2
+ version: 2.6.2
+ typescript:
+ specifier: ^5.3.3
+ version: 5.3.3
+ ws:
+ specifier: 7.4.5
+ version: 7.4.5
packages:
- /@ampproject/remapping/2.2.0:
+ /@aashutoshrathi/word-wrap@1.2.6:
+ resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /@alloc/quick-lru@5.2.0:
+ resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
+ engines: {node: '>=10'}
+
+ /@ampproject/remapping@2.2.0:
resolution: {integrity: sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==}
engines: {node: '>=6.0.0'}
dependencies:
'@jridgewell/gen-mapping': 0.1.1
- '@jridgewell/trace-mapping': 0.3.17
- dev: true
+ '@jridgewell/trace-mapping': 0.3.19
- /@apideck/better-ajv-errors/0.3.6_ajv@8.11.0:
+ /@apideck/better-ajv-errors@0.3.6(ajv@8.11.0):
resolution: {integrity: sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==}
engines: {node: '>=10'}
peerDependencies:
@@ -742,63 +1210,61 @@ packages:
leven: 3.1.0
dev: true
- /@ava/typescript/3.0.1:
- resolution: {integrity: sha512-/JXIUuKsvkaneaiA9ckk3ksFTqvu0mDNlChASrTe2BnDsvMbhQdPWyqQjJ9WRJWVhhs5TWn1/0Pp1G6Rv8Syrw==}
- engines: {node: '>=12.22 <13 || >=14.17 <15 || >=16.4 <17 || >=17'}
+ /@ava/typescript@4.1.0:
+ resolution: {integrity: sha512-1iWZQ/nr9iflhLK9VN8H+1oDZqe93qxNnyYUz+jTzkYPAHc5fdZXBrqmNIgIfFhWYXK5OaQ5YtC7OmLeTNhVEg==}
+ engines: {node: ^14.19 || ^16.15 || ^18 || ^20}
dependencies:
escape-string-regexp: 5.0.0
- execa: 5.1.1
+ execa: 7.2.0
dev: true
- /@babel/code-frame/7.12.11:
+ /@babel/code-frame@7.12.11:
resolution: {integrity: sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==}
dependencies:
- '@babel/highlight': 7.18.6
+ '@babel/highlight': 7.23.4
dev: true
- /@babel/code-frame/7.18.6:
+ /@babel/code-frame@7.18.6:
resolution: {integrity: sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/highlight': 7.18.6
dev: true
- /@babel/compat-data/7.19.4:
+ /@babel/code-frame@7.21.4:
+ resolution: {integrity: sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/highlight': 7.18.6
+
+ /@babel/code-frame@7.23.5:
+ resolution: {integrity: sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/highlight': 7.23.4
+ chalk: 2.4.2
+
+ /@babel/compat-data@7.19.4:
resolution: {integrity: sha512-CHIGpJcUQ5lU9KrPHTjBMhVwQG6CQjxfg36fGXl3qk/Gik1WwWachaXFuo0uCWJT/mStOKtcbFJCaVLihC1CMw==}
engines: {node: '>=6.9.0'}
dev: true
- /@babel/core/7.12.9:
- resolution: {integrity: sha512-gTXYh3M5wb7FRXQy+FErKFAv90BnlOuNn1QkCK2lREoPAjrQCO49+HVSrFoe5uakFAF5eenS75KbO2vQiLrTMQ==}
+ /@babel/compat-data@7.21.7:
+ resolution: {integrity: sha512-KYMqFYTaenzMK4yUtf4EW9wc4N9ef80FsbMtkwool5zpwl4YrT1SdWYSTRcT94KO4hannogdS+LxY7L+arP3gA==}
+ engines: {node: '>=6.9.0'}
+
+ /@babel/compat-data@7.23.5:
+ resolution: {integrity: sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==}
engines: {node: '>=6.9.0'}
- dependencies:
- '@babel/code-frame': 7.18.6
- '@babel/generator': 7.19.6
- '@babel/helper-module-transforms': 7.19.6
- '@babel/helpers': 7.19.4
- '@babel/parser': 7.19.6
- '@babel/template': 7.18.10
- '@babel/traverse': 7.19.6
- '@babel/types': 7.19.4
- convert-source-map: 1.9.0
- debug: 4.3.4
- gensync: 1.0.0-beta.2
- json5: 2.2.1
- lodash: 4.17.21
- resolve: 1.22.1
- semver: 5.7.1
- source-map: 0.5.7
- transitivePeerDependencies:
- - supports-color
dev: true
- /@babel/core/7.13.16:
+ /@babel/core@7.13.16:
resolution: {integrity: sha512-sXHpixBiWWFti0AV2Zq7avpTasr6sIAu7Y396c608541qAU2ui4a193m0KSQmfPSKFZLnQ3cvlKDOm3XkuXm3Q==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/code-frame': 7.18.6
'@babel/generator': 7.19.6
- '@babel/helper-compilation-targets': 7.19.3_@babel+core@7.13.16
+ '@babel/helper-compilation-targets': 7.18.9(@babel/core@7.13.16)
'@babel/helper-module-transforms': 7.19.6
'@babel/helpers': 7.19.4
'@babel/parser': 7.19.6
@@ -815,67 +1281,75 @@ packages:
- supports-color
dev: true
- /@babel/core/7.18.9:
+ /@babel/core@7.18.9:
resolution: {integrity: sha512-1LIb1eL8APMy91/IMW+31ckrfBM4yCoLaVzoDhZUKSM4cu1L1nIidyxkCgzPAgrC5WEz36IPEr/eSeSF9pIn+g==}
engines: {node: '>=6.9.0'}
dependencies:
'@ampproject/remapping': 2.2.0
- '@babel/code-frame': 7.18.6
- '@babel/generator': 7.19.6
- '@babel/helper-compilation-targets': 7.19.3_@babel+core@7.18.9
- '@babel/helper-module-transforms': 7.19.6
- '@babel/helpers': 7.19.4
- '@babel/parser': 7.19.6
- '@babel/template': 7.18.10
- '@babel/traverse': 7.19.6
- '@babel/types': 7.19.4
+ '@babel/code-frame': 7.21.4
+ '@babel/generator': 7.21.5
+ '@babel/helper-compilation-targets': 7.18.9(@babel/core@7.18.9)
+ '@babel/helper-module-transforms': 7.21.5
+ '@babel/helpers': 7.21.5
+ '@babel/parser': 7.21.8
+ '@babel/template': 7.20.7
+ '@babel/traverse': 7.21.5
+ '@babel/types': 7.21.5
convert-source-map: 1.9.0
debug: 4.3.4
gensync: 1.0.0-beta.2
- json5: 2.2.1
+ json5: 2.2.3
semver: 6.3.0
transitivePeerDependencies:
- supports-color
- dev: true
- /@babel/core/7.19.6:
- resolution: {integrity: sha512-D2Ue4KHpc6Ys2+AxpIx1BZ8+UegLLLE2p3KJEuJRKmokHOtl49jQ5ny1773KsGLZs8MQvBidAF6yWUJxRqtKtg==}
+ /@babel/core@7.22.1:
+ resolution: {integrity: sha512-Hkqu7J4ynysSXxmAahpN1jjRwVJ+NdpraFLIWflgjpVob3KNyK3/tIUc7Q7szed8WMp0JNa7Qtd1E9Oo22F9gA==}
engines: {node: '>=6.9.0'}
dependencies:
'@ampproject/remapping': 2.2.0
- '@babel/code-frame': 7.18.6
- '@babel/generator': 7.19.6
- '@babel/helper-compilation-targets': 7.19.3_@babel+core@7.19.6
- '@babel/helper-module-transforms': 7.19.6
- '@babel/helpers': 7.19.4
- '@babel/parser': 7.19.6
- '@babel/template': 7.18.10
- '@babel/traverse': 7.19.6
- '@babel/types': 7.19.4
+ '@babel/code-frame': 7.23.5
+ '@babel/generator': 7.23.5
+ '@babel/helper-compilation-targets': 7.22.15
+ '@babel/helper-module-transforms': 7.23.3(@babel/core@7.22.1)
+ '@babel/helpers': 7.23.5
+ '@babel/parser': 7.23.5
+ '@babel/template': 7.22.15
+ '@babel/traverse': 7.23.5
+ '@babel/types': 7.23.5
convert-source-map: 1.9.0
debug: 4.3.4
gensync: 1.0.0-beta.2
- json5: 2.2.1
- semver: 6.3.0
+ json5: 2.2.3
+ semver: 6.3.1
transitivePeerDependencies:
- supports-color
dev: true
- /@babel/eslint-parser/7.19.1_cjip4hokpjhf4bbkea6jsnplgy:
- resolution: {integrity: sha512-AqNf2QWt1rtu2/1rLswy6CDP7H9Oh3mMhk177Y67Rg8d7RD9WfOLLv8CGn6tisFvS2htm86yIe1yLF6I1UDaGQ==}
- engines: {node: ^10.13.0 || ^12.13.0 || >=14.0.0}
- peerDependencies:
- '@babel/core': '>=7.11.0'
- eslint: ^7.5.0 || ^8.0.0
+ /@babel/core@7.23.5:
+ resolution: {integrity: sha512-Cwc2XjUrG4ilcfOw4wBAK+enbdgwAcAJCfGUItPBKR7Mjw4aEfAFYrLxeRp4jWgtNIKn3n2AlBOfwwafl+42/g==}
+ engines: {node: '>=6.9.0'}
dependencies:
- '@babel/core': 7.18.9
- '@nicolo-ribaudo/eslint-scope-5-internals': 5.1.1-v1
- eslint: 8.26.0
- eslint-visitor-keys: 2.1.0
- semver: 6.3.0
+ '@ampproject/remapping': 2.2.0
+ '@babel/code-frame': 7.23.5
+ '@babel/generator': 7.23.5
+ '@babel/helper-compilation-targets': 7.22.15
+ '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.5)
+ '@babel/helpers': 7.23.5
+ '@babel/parser': 7.23.5
+ '@babel/template': 7.22.15
+ '@babel/traverse': 7.23.5
+ '@babel/types': 7.23.5
+ convert-source-map: 2.0.0
+ debug: 4.3.4
+ gensync: 1.0.0-beta.2
+ json5: 2.2.3
+ semver: 6.3.1
+ transitivePeerDependencies:
+ - supports-color
dev: true
- /@babel/eslint-parser/7.19.1_o5peei4wpze5egwf42u76kwdva:
+ /@babel/eslint-parser@7.19.1(@babel/core@7.18.9)(eslint@7.32.0):
resolution: {integrity: sha512-AqNf2QWt1rtu2/1rLswy6CDP7H9Oh3mMhk177Y67Rg8d7RD9WfOLLv8CGn6tisFvS2htm86yIe1yLF6I1UDaGQ==}
engines: {node: ^10.13.0 || ^12.13.0 || >=14.0.0}
peerDependencies:
@@ -886,222 +1360,324 @@ packages:
'@nicolo-ribaudo/eslint-scope-5-internals': 5.1.1-v1
eslint: 7.32.0
eslint-visitor-keys: 2.1.0
- semver: 6.3.0
+ semver: 6.3.1
dev: true
- /@babel/generator/7.19.6:
+ /@babel/generator@7.19.6:
resolution: {integrity: sha512-oHGRUQeoX1QrKeJIKVe0hwjGqNnVYsM5Nep5zo0uE0m42sLH+Fsd2pStJ5sRM1bNyTUUoz0pe2lTeMJrb/taTA==}
engines: {node: '>=6.9.0'}
dependencies:
- '@babel/types': 7.19.4
+ '@babel/types': 7.22.4
'@jridgewell/gen-mapping': 0.3.2
jsesc: 2.5.2
dev: true
- /@babel/helper-annotate-as-pure/7.18.6:
- resolution: {integrity: sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==}
+ /@babel/generator@7.21.5:
+ resolution: {integrity: sha512-SrKK/sRv8GesIW1bDagf9cCG38IOMYZusoe1dfg0D8aiUe3Amvoj1QtjTPAWcfrZFvIwlleLb0gxzQidL9w14w==}
engines: {node: '>=6.9.0'}
dependencies:
- '@babel/types': 7.19.4
+ '@babel/types': 7.23.5
+ '@jridgewell/gen-mapping': 0.3.3
+ '@jridgewell/trace-mapping': 0.3.19
+ jsesc: 2.5.2
+
+ /@babel/generator@7.22.3:
+ resolution: {integrity: sha512-C17MW4wlk//ES/CJDL51kPNwl+qiBQyN7b9SKyVp11BLGFeSPoVaHrv+MNt8jwQFhQWowW88z1eeBx3pFz9v8A==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/types': 7.23.5
+ '@jridgewell/gen-mapping': 0.3.3
+ '@jridgewell/trace-mapping': 0.3.19
+ jsesc: 2.5.2
dev: true
- /@babel/helper-builder-binary-assignment-operator-visitor/7.18.9:
- resolution: {integrity: sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw==}
+ /@babel/generator@7.23.5:
+ resolution: {integrity: sha512-BPssCHrBD+0YrxviOa3QzpqwhNIXKEtOa2jQrm4FlmkC2apYgRnQcmPWiGZDlGxiNtltnUFolMe8497Esry+jA==}
engines: {node: '>=6.9.0'}
dependencies:
- '@babel/helper-explode-assignable-expression': 7.18.6
- '@babel/types': 7.19.4
+ '@babel/types': 7.23.5
+ '@jridgewell/gen-mapping': 0.3.3
+ '@jridgewell/trace-mapping': 0.3.19
+ jsesc: 2.5.2
+
+ /@babel/helper-annotate-as-pure@7.22.5:
+ resolution: {integrity: sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/types': 7.23.5
+ dev: true
+
+ /@babel/helper-builder-binary-assignment-operator-visitor@7.22.15:
+ resolution: {integrity: sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/types': 7.23.5
dev: true
- /@babel/helper-compilation-targets/7.19.3_@babel+core@7.13.16:
- resolution: {integrity: sha512-65ESqLGyGmLvgR0mst5AdW1FkNlj9rQsCKduzEoEPhBCDFGXvz2jW6bXFG6i0/MrV2s7hhXjjb2yAzcPuQlLwg==}
+ /@babel/helper-compilation-targets@7.18.9(@babel/core@7.13.16):
+ resolution: {integrity: sha512-tzLCyVmqUiFlcFoAPLA/gL9TeYrF61VLNtb+hvkuVaB5SUjW7jcfrglBIX1vUIoT7CLP3bBlIMeyEsIl2eFQNg==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0
dependencies:
- '@babel/compat-data': 7.19.4
+ '@babel/compat-data': 7.21.7
'@babel/core': 7.13.16
- '@babel/helper-validator-option': 7.18.6
- browserslist: 4.21.4
- semver: 6.3.0
+ '@babel/helper-validator-option': 7.21.0
+ browserslist: 4.22.2
+ semver: 6.3.1
dev: true
- /@babel/helper-compilation-targets/7.19.3_@babel+core@7.18.9:
- resolution: {integrity: sha512-65ESqLGyGmLvgR0mst5AdW1FkNlj9rQsCKduzEoEPhBCDFGXvz2jW6bXFG6i0/MrV2s7hhXjjb2yAzcPuQlLwg==}
+ /@babel/helper-compilation-targets@7.18.9(@babel/core@7.18.9):
+ resolution: {integrity: sha512-tzLCyVmqUiFlcFoAPLA/gL9TeYrF61VLNtb+hvkuVaB5SUjW7jcfrglBIX1vUIoT7CLP3bBlIMeyEsIl2eFQNg==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0
dependencies:
- '@babel/compat-data': 7.19.4
+ '@babel/compat-data': 7.21.7
'@babel/core': 7.18.9
- '@babel/helper-validator-option': 7.18.6
- browserslist: 4.21.4
- semver: 6.3.0
+ '@babel/helper-validator-option': 7.21.0
+ browserslist: 4.22.2
+ semver: 6.3.1
+
+ /@babel/helper-compilation-targets@7.18.9(@babel/core@7.22.1):
+ resolution: {integrity: sha512-tzLCyVmqUiFlcFoAPLA/gL9TeYrF61VLNtb+hvkuVaB5SUjW7jcfrglBIX1vUIoT7CLP3bBlIMeyEsIl2eFQNg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+ dependencies:
+ '@babel/compat-data': 7.21.7
+ '@babel/core': 7.22.1
+ '@babel/helper-validator-option': 7.21.0
+ browserslist: 4.22.2
+ semver: 6.3.1
dev: true
- /@babel/helper-compilation-targets/7.19.3_@babel+core@7.19.6:
- resolution: {integrity: sha512-65ESqLGyGmLvgR0mst5AdW1FkNlj9rQsCKduzEoEPhBCDFGXvz2jW6bXFG6i0/MrV2s7hhXjjb2yAzcPuQlLwg==}
+ /@babel/helper-compilation-targets@7.21.5(@babel/core@7.22.1):
+ resolution: {integrity: sha512-1RkbFGUKex4lvsB9yhIfWltJM5cZKUftB2eNajaDv3dCMEp49iBG0K14uH8NnX9IPux2+mK7JGEOB0jn48/J6w==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0
dependencies:
- '@babel/compat-data': 7.19.4
- '@babel/core': 7.19.6
- '@babel/helper-validator-option': 7.18.6
- browserslist: 4.21.4
- semver: 6.3.0
+ '@babel/compat-data': 7.23.5
+ '@babel/core': 7.22.1
+ '@babel/helper-validator-option': 7.23.5
+ browserslist: 4.22.2
+ lru-cache: 5.1.1
+ semver: 6.3.1
+ dev: true
+
+ /@babel/helper-compilation-targets@7.22.15:
+ resolution: {integrity: sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/compat-data': 7.23.5
+ '@babel/helper-validator-option': 7.23.5
+ browserslist: 4.22.2
+ lru-cache: 5.1.1
+ semver: 6.3.1
dev: true
- /@babel/helper-create-class-features-plugin/7.19.0_@babel+core@7.18.9:
- resolution: {integrity: sha512-NRz8DwF4jT3UfrmUoZjd0Uph9HQnP30t7Ash+weACcyNkiYTywpIjDBgReJMKgr+n86sn2nPVVmJ28Dm053Kqw==}
+ /@babel/helper-create-class-features-plugin@7.23.5(@babel/core@7.18.9):
+ resolution: {integrity: sha512-QELlRWxSpgdwdJzSJn4WAhKC+hvw/AtHbbrIoncKHkhKKR/luAlKkgBDcri1EzWAo8f8VvYVryEHN4tax/V67A==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0
dependencies:
'@babel/core': 7.18.9
- '@babel/helper-annotate-as-pure': 7.18.6
- '@babel/helper-environment-visitor': 7.18.9
- '@babel/helper-function-name': 7.19.0
- '@babel/helper-member-expression-to-functions': 7.18.9
- '@babel/helper-optimise-call-expression': 7.18.6
- '@babel/helper-replace-supers': 7.19.1
- '@babel/helper-split-export-declaration': 7.18.6
- transitivePeerDependencies:
- - supports-color
+ '@babel/helper-annotate-as-pure': 7.22.5
+ '@babel/helper-environment-visitor': 7.22.20
+ '@babel/helper-function-name': 7.23.0
+ '@babel/helper-member-expression-to-functions': 7.23.0
+ '@babel/helper-optimise-call-expression': 7.22.5
+ '@babel/helper-replace-supers': 7.22.20(@babel/core@7.18.9)
+ '@babel/helper-skip-transparent-expression-wrappers': 7.22.5
+ '@babel/helper-split-export-declaration': 7.22.6
+ semver: 6.3.1
+ dev: true
+
+ /@babel/helper-create-class-features-plugin@7.23.5(@babel/core@7.22.1):
+ resolution: {integrity: sha512-QELlRWxSpgdwdJzSJn4WAhKC+hvw/AtHbbrIoncKHkhKKR/luAlKkgBDcri1EzWAo8f8VvYVryEHN4tax/V67A==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+ dependencies:
+ '@babel/core': 7.22.1
+ '@babel/helper-annotate-as-pure': 7.22.5
+ '@babel/helper-environment-visitor': 7.22.20
+ '@babel/helper-function-name': 7.23.0
+ '@babel/helper-member-expression-to-functions': 7.23.0
+ '@babel/helper-optimise-call-expression': 7.22.5
+ '@babel/helper-replace-supers': 7.22.20(@babel/core@7.22.1)
+ '@babel/helper-skip-transparent-expression-wrappers': 7.22.5
+ '@babel/helper-split-export-declaration': 7.22.6
+ semver: 6.3.1
dev: true
- /@babel/helper-create-class-features-plugin/7.19.0_@babel+core@7.19.6:
- resolution: {integrity: sha512-NRz8DwF4jT3UfrmUoZjd0Uph9HQnP30t7Ash+weACcyNkiYTywpIjDBgReJMKgr+n86sn2nPVVmJ28Dm053Kqw==}
+ /@babel/helper-create-class-features-plugin@7.23.5(@babel/core@7.23.5):
+ resolution: {integrity: sha512-QELlRWxSpgdwdJzSJn4WAhKC+hvw/AtHbbrIoncKHkhKKR/luAlKkgBDcri1EzWAo8f8VvYVryEHN4tax/V67A==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-annotate-as-pure': 7.18.6
- '@babel/helper-environment-visitor': 7.18.9
- '@babel/helper-function-name': 7.19.0
- '@babel/helper-member-expression-to-functions': 7.18.9
- '@babel/helper-optimise-call-expression': 7.18.6
- '@babel/helper-replace-supers': 7.19.1
- '@babel/helper-split-export-declaration': 7.18.6
- transitivePeerDependencies:
- - supports-color
+ '@babel/core': 7.23.5
+ '@babel/helper-annotate-as-pure': 7.22.5
+ '@babel/helper-environment-visitor': 7.22.20
+ '@babel/helper-function-name': 7.23.0
+ '@babel/helper-member-expression-to-functions': 7.23.0
+ '@babel/helper-optimise-call-expression': 7.22.5
+ '@babel/helper-replace-supers': 7.22.20(@babel/core@7.23.5)
+ '@babel/helper-skip-transparent-expression-wrappers': 7.22.5
+ '@babel/helper-split-export-declaration': 7.22.6
+ semver: 6.3.1
dev: true
- /@babel/helper-create-regexp-features-plugin/7.19.0_@babel+core@7.18.9:
- resolution: {integrity: sha512-htnV+mHX32DF81amCDrwIDr8nrp1PTm+3wfBN9/v8QJOLEioOCOG7qNyq0nHeFiWbT3Eb7gsPwEmV64UCQ1jzw==}
+ /@babel/helper-create-regexp-features-plugin@7.22.15(@babel/core@7.18.9):
+ resolution: {integrity: sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0
dependencies:
'@babel/core': 7.18.9
- '@babel/helper-annotate-as-pure': 7.18.6
- regexpu-core: 5.2.1
+ '@babel/helper-annotate-as-pure': 7.22.5
+ regexpu-core: 5.3.2
+ semver: 6.3.1
dev: true
- /@babel/helper-create-regexp-features-plugin/7.19.0_@babel+core@7.19.6:
- resolution: {integrity: sha512-htnV+mHX32DF81amCDrwIDr8nrp1PTm+3wfBN9/v8QJOLEioOCOG7qNyq0nHeFiWbT3Eb7gsPwEmV64UCQ1jzw==}
+ /@babel/helper-create-regexp-features-plugin@7.22.15(@babel/core@7.22.1):
+ resolution: {integrity: sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-annotate-as-pure': 7.18.6
- regexpu-core: 5.2.1
+ '@babel/core': 7.22.1
+ '@babel/helper-annotate-as-pure': 7.22.5
+ regexpu-core: 5.3.2
+ semver: 6.3.1
dev: true
- /@babel/helper-define-polyfill-provider/0.1.5_@babel+core@7.18.9:
- resolution: {integrity: sha512-nXuzCSwlJ/WKr8qxzW816gwyT6VZgiJG17zR40fou70yfAcqjoNyTLl/DQ+FExw5Hx5KNqshmN8Ldl/r2N7cTg==}
+ /@babel/helper-create-regexp-features-plugin@7.22.15(@babel/core@7.23.5):
+ resolution: {integrity: sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+ dependencies:
+ '@babel/core': 7.23.5
+ '@babel/helper-annotate-as-pure': 7.22.5
+ regexpu-core: 5.3.2
+ semver: 6.3.1
+ dev: true
+
+ /@babel/helper-define-polyfill-provider@0.3.3(@babel/core@7.22.1):
+ resolution: {integrity: sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==}
peerDependencies:
'@babel/core': ^7.4.0-0
dependencies:
- '@babel/core': 7.18.9
- '@babel/helper-compilation-targets': 7.19.3_@babel+core@7.18.9
- '@babel/helper-module-imports': 7.18.6
- '@babel/helper-plugin-utils': 7.19.0
- '@babel/traverse': 7.19.6
+ '@babel/core': 7.22.1
+ '@babel/helper-compilation-targets': 7.18.9(@babel/core@7.22.1)
+ '@babel/helper-plugin-utils': 7.22.5
debug: 4.3.4
lodash.debounce: 4.0.8
- resolve: 1.22.1
- semver: 6.3.0
+ resolve: 1.22.8
+ semver: 6.3.1
transitivePeerDependencies:
- supports-color
dev: true
- /@babel/helper-define-polyfill-provider/0.3.3_@babel+core@7.18.9:
- resolution: {integrity: sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==}
+ /@babel/helper-define-polyfill-provider@0.4.3(@babel/core@7.18.9):
+ resolution: {integrity: sha512-WBrLmuPP47n7PNwsZ57pqam6G/RGo1vw/87b0Blc53tZNGZ4x7YvZ6HgQe2vo1W/FR20OgjeZuGXzudPiXHFug==}
peerDependencies:
- '@babel/core': ^7.4.0-0
+ '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0
dependencies:
'@babel/core': 7.18.9
- '@babel/helper-compilation-targets': 7.19.3_@babel+core@7.18.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/helper-compilation-targets': 7.22.15
+ '@babel/helper-plugin-utils': 7.22.5
debug: 4.3.4
lodash.debounce: 4.0.8
- resolve: 1.22.1
- semver: 6.3.0
+ resolve: 1.22.8
transitivePeerDependencies:
- supports-color
dev: true
- /@babel/helper-define-polyfill-provider/0.3.3_@babel+core@7.19.6:
- resolution: {integrity: sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==}
+ /@babel/helper-define-polyfill-provider@0.4.3(@babel/core@7.23.5):
+ resolution: {integrity: sha512-WBrLmuPP47n7PNwsZ57pqam6G/RGo1vw/87b0Blc53tZNGZ4x7YvZ6HgQe2vo1W/FR20OgjeZuGXzudPiXHFug==}
peerDependencies:
- '@babel/core': ^7.4.0-0
+ '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-compilation-targets': 7.19.3_@babel+core@7.19.6
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.23.5
+ '@babel/helper-compilation-targets': 7.22.15
+ '@babel/helper-plugin-utils': 7.22.5
debug: 4.3.4
lodash.debounce: 4.0.8
- resolve: 1.22.1
- semver: 6.3.0
+ resolve: 1.22.8
transitivePeerDependencies:
- supports-color
dev: true
- /@babel/helper-environment-visitor/7.18.9:
+ /@babel/helper-environment-visitor@7.18.9:
resolution: {integrity: sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==}
engines: {node: '>=6.9.0'}
dev: true
- /@babel/helper-explode-assignable-expression/7.18.6:
- resolution: {integrity: sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg==}
+ /@babel/helper-environment-visitor@7.22.20:
+ resolution: {integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==}
engines: {node: '>=6.9.0'}
- dependencies:
- '@babel/types': 7.19.4
- dev: true
- /@babel/helper-function-name/7.19.0:
+ /@babel/helper-function-name@7.19.0:
resolution: {integrity: sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==}
engines: {node: '>=6.9.0'}
dependencies:
- '@babel/template': 7.18.10
- '@babel/types': 7.19.4
+ '@babel/template': 7.22.15
+ '@babel/types': 7.23.5
dev: true
- /@babel/helper-hoist-variables/7.18.6:
+ /@babel/helper-function-name@7.23.0:
+ resolution: {integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/template': 7.22.15
+ '@babel/types': 7.23.5
+
+ /@babel/helper-hoist-variables@7.18.6:
resolution: {integrity: sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==}
engines: {node: '>=6.9.0'}
dependencies:
- '@babel/types': 7.19.4
+ '@babel/types': 7.23.5
dev: true
- /@babel/helper-member-expression-to-functions/7.18.9:
- resolution: {integrity: sha512-RxifAh2ZoVU67PyKIO4AMi1wTenGfMR/O/ae0CCRqwgBAt5v7xjdtRw7UoSbsreKrQn5t7r89eruK/9JjYHuDg==}
+ /@babel/helper-hoist-variables@7.22.5:
+ resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==}
engines: {node: '>=6.9.0'}
dependencies:
- '@babel/types': 7.19.4
+ '@babel/types': 7.23.5
+
+ /@babel/helper-member-expression-to-functions@7.23.0:
+ resolution: {integrity: sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/types': 7.23.5
dev: true
- /@babel/helper-module-imports/7.18.6:
+ /@babel/helper-module-imports@7.18.6:
resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==}
engines: {node: '>=6.9.0'}
dependencies:
- '@babel/types': 7.19.4
+ '@babel/types': 7.23.5
dev: true
- /@babel/helper-module-transforms/7.19.6:
+ /@babel/helper-module-imports@7.21.4:
+ resolution: {integrity: sha512-orajc5T2PsRYUN3ZryCEFeMDYwyw09c/pZeaQEZPH0MpKzSvn3e0uXsDBu3k03VI+9DBiRo+l22BfKTpKwa/Wg==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/types': 7.23.5
+ dev: true
+
+ /@babel/helper-module-imports@7.22.15:
+ resolution: {integrity: sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/types': 7.23.5
+
+ /@babel/helper-module-transforms@7.19.6:
resolution: {integrity: sha512-fCmcfQo/KYr/VXXDIyd3CBGZ6AFhPFy1TfSEJ+PilGVlQT6jcbqtHAM4C1EciRqMza7/TpOUZliuSH+U6HAhJw==}
engines: {node: '>=6.9.0'}
dependencies:
@@ -1117,2103 +1693,3082 @@ packages:
- supports-color
dev: true
- /@babel/helper-optimise-call-expression/7.18.6:
- resolution: {integrity: sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==}
+ /@babel/helper-module-transforms@7.21.5:
+ resolution: {integrity: sha512-bI2Z9zBGY2q5yMHoBvJ2a9iX3ZOAzJPm7Q8Yz6YeoUjU/Cvhmi2G4QyTNyPBqqXSgTjUxRg3L0xV45HvkNWWBw==}
engines: {node: '>=6.9.0'}
dependencies:
- '@babel/types': 7.19.4
+ '@babel/helper-environment-visitor': 7.22.20
+ '@babel/helper-module-imports': 7.22.15
+ '@babel/helper-simple-access': 7.22.5
+ '@babel/helper-split-export-declaration': 7.22.6
+ '@babel/helper-validator-identifier': 7.22.20
+ '@babel/template': 7.22.15
+ '@babel/traverse': 7.23.5
+ '@babel/types': 7.23.5
+ transitivePeerDependencies:
+ - supports-color
+
+ /@babel/helper-module-transforms@7.23.3(@babel/core@7.18.9):
+ resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+ dependencies:
+ '@babel/core': 7.18.9
+ '@babel/helper-environment-visitor': 7.22.20
+ '@babel/helper-module-imports': 7.22.15
+ '@babel/helper-simple-access': 7.22.5
+ '@babel/helper-split-export-declaration': 7.22.6
+ '@babel/helper-validator-identifier': 7.22.20
dev: true
- /@babel/helper-plugin-utils/7.10.4:
- resolution: {integrity: sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==}
+ /@babel/helper-module-transforms@7.23.3(@babel/core@7.22.1):
+ resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+ dependencies:
+ '@babel/core': 7.22.1
+ '@babel/helper-environment-visitor': 7.22.20
+ '@babel/helper-module-imports': 7.22.15
+ '@babel/helper-simple-access': 7.22.5
+ '@babel/helper-split-export-declaration': 7.22.6
+ '@babel/helper-validator-identifier': 7.22.20
dev: true
- /@babel/helper-plugin-utils/7.19.0:
+ /@babel/helper-module-transforms@7.23.3(@babel/core@7.23.5):
+ resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+ dependencies:
+ '@babel/core': 7.23.5
+ '@babel/helper-environment-visitor': 7.22.20
+ '@babel/helper-module-imports': 7.22.15
+ '@babel/helper-simple-access': 7.22.5
+ '@babel/helper-split-export-declaration': 7.22.6
+ '@babel/helper-validator-identifier': 7.22.20
+ dev: true
+
+ /@babel/helper-optimise-call-expression@7.22.5:
+ resolution: {integrity: sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/types': 7.23.5
+ dev: true
+
+ /@babel/helper-plugin-utils@7.19.0:
resolution: {integrity: sha512-40Ryx7I8mT+0gaNxm8JGTZFUITNqdLAgdg0hXzeVZxVD6nFsdhQvip6v8dqkRHzsz1VFpFAaOCHNn0vKBL7Czw==}
engines: {node: '>=6.9.0'}
dev: true
- /@babel/helper-remap-async-to-generator/7.18.9_@babel+core@7.18.9:
- resolution: {integrity: sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==}
+ /@babel/helper-plugin-utils@7.21.5:
+ resolution: {integrity: sha512-0WDaIlXKOX/3KfBK/dwP1oQGiPh6rjMkT7HIRv7i5RR2VUMwrx5ZL0dwBkKx7+SW1zwNdgjHd34IMk5ZjTeHVg==}
+ engines: {node: '>=6.9.0'}
+ dev: true
+
+ /@babel/helper-plugin-utils@7.22.5:
+ resolution: {integrity: sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==}
+ engines: {node: '>=6.9.0'}
+ dev: true
+
+ /@babel/helper-remap-async-to-generator@7.22.20(@babel/core@7.18.9):
+ resolution: {integrity: sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0
dependencies:
'@babel/core': 7.18.9
- '@babel/helper-annotate-as-pure': 7.18.6
- '@babel/helper-environment-visitor': 7.18.9
- '@babel/helper-wrap-function': 7.19.0
- '@babel/types': 7.19.4
- transitivePeerDependencies:
- - supports-color
+ '@babel/helper-annotate-as-pure': 7.22.5
+ '@babel/helper-environment-visitor': 7.22.20
+ '@babel/helper-wrap-function': 7.22.20
dev: true
- /@babel/helper-remap-async-to-generator/7.18.9_@babel+core@7.19.6:
- resolution: {integrity: sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==}
+ /@babel/helper-remap-async-to-generator@7.22.20(@babel/core@7.22.1):
+ resolution: {integrity: sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-annotate-as-pure': 7.18.6
- '@babel/helper-environment-visitor': 7.18.9
- '@babel/helper-wrap-function': 7.19.0
- '@babel/types': 7.19.4
- transitivePeerDependencies:
- - supports-color
+ '@babel/core': 7.22.1
+ '@babel/helper-annotate-as-pure': 7.22.5
+ '@babel/helper-environment-visitor': 7.22.20
+ '@babel/helper-wrap-function': 7.22.20
dev: true
- /@babel/helper-replace-supers/7.19.1:
- resolution: {integrity: sha512-T7ahH7wV0Hfs46SFh5Jz3s0B6+o8g3c+7TMxu7xKfmHikg7EAZ3I2Qk9LFhjxXq8sL7UkP5JflezNwoZa8WvWw==}
+ /@babel/helper-remap-async-to-generator@7.22.20(@babel/core@7.23.5):
+ resolution: {integrity: sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==}
engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
dependencies:
- '@babel/helper-environment-visitor': 7.18.9
- '@babel/helper-member-expression-to-functions': 7.18.9
- '@babel/helper-optimise-call-expression': 7.18.6
- '@babel/traverse': 7.19.6
- '@babel/types': 7.19.4
- transitivePeerDependencies:
- - supports-color
+ '@babel/core': 7.23.5
+ '@babel/helper-annotate-as-pure': 7.22.5
+ '@babel/helper-environment-visitor': 7.22.20
+ '@babel/helper-wrap-function': 7.22.20
+ dev: true
+
+ /@babel/helper-replace-supers@7.22.20(@babel/core@7.18.9):
+ resolution: {integrity: sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+ dependencies:
+ '@babel/core': 7.18.9
+ '@babel/helper-environment-visitor': 7.22.20
+ '@babel/helper-member-expression-to-functions': 7.23.0
+ '@babel/helper-optimise-call-expression': 7.22.5
+ dev: true
+
+ /@babel/helper-replace-supers@7.22.20(@babel/core@7.22.1):
+ resolution: {integrity: sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+ dependencies:
+ '@babel/core': 7.22.1
+ '@babel/helper-environment-visitor': 7.22.20
+ '@babel/helper-member-expression-to-functions': 7.23.0
+ '@babel/helper-optimise-call-expression': 7.22.5
+ dev: true
+
+ /@babel/helper-replace-supers@7.22.20(@babel/core@7.23.5):
+ resolution: {integrity: sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+ dependencies:
+ '@babel/core': 7.23.5
+ '@babel/helper-environment-visitor': 7.22.20
+ '@babel/helper-member-expression-to-functions': 7.23.0
+ '@babel/helper-optimise-call-expression': 7.22.5
dev: true
- /@babel/helper-simple-access/7.19.4:
+ /@babel/helper-simple-access@7.19.4:
resolution: {integrity: sha512-f9Xq6WqBFqaDfbCzn2w85hwklswz5qsKlh7f08w4Y9yhJHpnNC0QemtSkK5YyOY8kPGvyiwdzZksGUhnGdaUIg==}
engines: {node: '>=6.9.0'}
dependencies:
- '@babel/types': 7.19.4
+ '@babel/types': 7.23.5
dev: true
- /@babel/helper-skip-transparent-expression-wrappers/7.18.9:
- resolution: {integrity: sha512-imytd2gHi3cJPsybLRbmFrF7u5BIEuI2cNheyKi3/iOBC63kNn3q8Crn2xVuESli0aM4KYsyEqKyS7lFL8YVtw==}
+ /@babel/helper-simple-access@7.22.5:
+ resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==}
engines: {node: '>=6.9.0'}
dependencies:
- '@babel/types': 7.19.4
+ '@babel/types': 7.23.5
+
+ /@babel/helper-skip-transparent-expression-wrappers@7.22.5:
+ resolution: {integrity: sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/types': 7.23.5
dev: true
- /@babel/helper-split-export-declaration/7.18.6:
+ /@babel/helper-split-export-declaration@7.18.6:
resolution: {integrity: sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==}
engines: {node: '>=6.9.0'}
dependencies:
- '@babel/types': 7.19.4
+ '@babel/types': 7.23.5
dev: true
- /@babel/helper-string-parser/7.19.4:
+ /@babel/helper-split-export-declaration@7.22.6:
+ resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/types': 7.23.5
+
+ /@babel/helper-string-parser@7.19.4:
resolution: {integrity: sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==}
engines: {node: '>=6.9.0'}
dev: true
- /@babel/helper-validator-identifier/7.19.1:
+ /@babel/helper-string-parser@7.23.4:
+ resolution: {integrity: sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==}
+ engines: {node: '>=6.9.0'}
+
+ /@babel/helper-validator-identifier@7.19.1:
resolution: {integrity: sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==}
engines: {node: '>=6.9.0'}
dev: true
- /@babel/helper-validator-option/7.18.6:
+ /@babel/helper-validator-identifier@7.22.20:
+ resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==}
+ engines: {node: '>=6.9.0'}
+
+ /@babel/helper-validator-option@7.18.6:
resolution: {integrity: sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==}
engines: {node: '>=6.9.0'}
dev: true
- /@babel/helper-wrap-function/7.19.0:
- resolution: {integrity: sha512-txX8aN8CZyYGTwcLhlk87KRqncAzhh5TpQamZUa0/u3an36NtDpUP6bQgBCBcLeBs09R/OwQu3OjK0k/HwfNDg==}
+ /@babel/helper-validator-option@7.21.0:
+ resolution: {integrity: sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ==}
+ engines: {node: '>=6.9.0'}
+
+ /@babel/helper-validator-option@7.23.5:
+ resolution: {integrity: sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==}
+ engines: {node: '>=6.9.0'}
+ dev: true
+
+ /@babel/helper-wrap-function@7.22.20:
+ resolution: {integrity: sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/helper-function-name': 7.23.0
+ '@babel/template': 7.22.15
+ '@babel/types': 7.23.5
+ dev: true
+
+ /@babel/helpers@7.19.4:
+ resolution: {integrity: sha512-G+z3aOx2nfDHwX/kyVii5fJq+bgscg89/dJNWpYeKeBv3v9xX8EIabmx1k6u9LS04H7nROFVRVK+e3k0VHp+sw==}
engines: {node: '>=6.9.0'}
dependencies:
- '@babel/helper-function-name': 7.19.0
'@babel/template': 7.18.10
'@babel/traverse': 7.19.6
- '@babel/types': 7.19.4
+ '@babel/types': 7.22.4
transitivePeerDependencies:
- supports-color
dev: true
- /@babel/helpers/7.19.4:
- resolution: {integrity: sha512-G+z3aOx2nfDHwX/kyVii5fJq+bgscg89/dJNWpYeKeBv3v9xX8EIabmx1k6u9LS04H7nROFVRVK+e3k0VHp+sw==}
+ /@babel/helpers@7.21.5:
+ resolution: {integrity: sha512-BSY+JSlHxOmGsPTydUkPf1MdMQ3M81x5xGCOVgWM3G8XH77sJ292Y2oqcp0CbbgxhqBuI46iUz1tT7hqP7EfgA==}
engines: {node: '>=6.9.0'}
dependencies:
- '@babel/template': 7.18.10
- '@babel/traverse': 7.19.6
- '@babel/types': 7.19.4
+ '@babel/template': 7.22.15
+ '@babel/traverse': 7.23.5
+ '@babel/types': 7.23.5
+ transitivePeerDependencies:
+ - supports-color
+
+ /@babel/helpers@7.23.5:
+ resolution: {integrity: sha512-oO7us8FzTEsG3U6ag9MfdF1iA/7Z6dz+MtFhifZk8C8o453rGJFFWUP1t+ULM9TUIAzC9uxXEiXjOiVMyd7QPg==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/template': 7.22.15
+ '@babel/traverse': 7.23.5
+ '@babel/types': 7.23.5
transitivePeerDependencies:
- supports-color
dev: true
- /@babel/highlight/7.18.6:
+ /@babel/highlight@7.18.6:
resolution: {integrity: sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==}
engines: {node: '>=6.9.0'}
dependencies:
- '@babel/helper-validator-identifier': 7.19.1
+ '@babel/helper-validator-identifier': 7.22.20
chalk: 2.4.2
js-tokens: 4.0.0
- dev: true
- /@babel/parser/7.19.6:
+ /@babel/highlight@7.23.4:
+ resolution: {integrity: sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/helper-validator-identifier': 7.22.20
+ chalk: 2.4.2
+ js-tokens: 4.0.0
+
+ /@babel/parser@7.19.6:
resolution: {integrity: sha512-h1IUp81s2JYJ3mRkdxJgs4UvmSsRvDrx5ICSJbPvtWYv5i1nTBGcBpnog+89rAFMwvvru6E5NUHdBe01UeSzYA==}
engines: {node: '>=6.0.0'}
- hasBin: true
dependencies:
- '@babel/types': 7.19.4
+ '@babel/types': 7.22.4
dev: true
- /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/7.18.6_@babel+core@7.18.9:
+ /@babel/parser@7.21.8:
+ resolution: {integrity: sha512-6zavDGdzG3gUqAdWvlLFfk+36RilI+Pwyuuh7HItyeScCWP3k6i8vKclAQ0bM/0y/Kz/xiwvxhMv9MgTJP5gmA==}
+ engines: {node: '>=6.0.0'}
+ dependencies:
+ '@babel/types': 7.23.5
+
+ /@babel/parser@7.23.5:
+ resolution: {integrity: sha512-hOOqoiNXrmGdFbhgCzu6GiURxUgM27Xwd/aPuu8RfHEZPBzL1Z54okAHAQjXfcQNwvrlkAmAp4SlRTZ45vlthQ==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+ dependencies:
+ '@babel/types': 7.23.5
+
+ /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.18.6(@babel/core@7.22.1):
resolution: {integrity: sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0
dependencies:
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.22.5
+ dev: true
+
+ /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.23.3(@babel/core@7.18.9):
+ resolution: {integrity: sha512-iRkKcCqb7iGnq9+3G6rZ+Ciz5VywC4XNRHe57lKM+jOeYAoR0lVqdeeDRfh0tQcTfw/+vBhHn926FmQhLtlFLQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+ dependencies:
'@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/7.18.6_@babel+core@7.19.6:
- resolution: {integrity: sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==}
+ /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.23.3(@babel/core@7.23.5):
+ resolution: {integrity: sha512-iRkKcCqb7iGnq9+3G6rZ+Ciz5VywC4XNRHe57lKM+jOeYAoR0lVqdeeDRfh0tQcTfw/+vBhHn926FmQhLtlFLQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.23.5
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/7.18.9_@babel+core@7.18.9:
+ /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.18.9(@babel/core@7.22.1):
resolution: {integrity: sha512-AHrP9jadvH7qlOj6PINbgSuphjQUAK7AOT7DPjBo9EHoLhQTnnK5u45e1Hd4DbSQEO9nqPWtQ89r+XEOWFScKg==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.13.0
dependencies:
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/helper-skip-transparent-expression-wrappers': 7.22.5
+ '@babel/plugin-proposal-optional-chaining': 7.18.9(@babel/core@7.22.1)
+ dev: true
+
+ /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.23.3(@babel/core@7.18.9):
+ resolution: {integrity: sha512-WwlxbfMNdVEpQjZmK5mhm7oSwD3dS6eU+Iwsi4Knl9wAletWem7kaRsGOG+8UEbRyqxY4SS5zvtfXwX+jMxUwQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.13.0
+ dependencies:
'@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
- '@babel/helper-skip-transparent-expression-wrappers': 7.18.9
- '@babel/plugin-proposal-optional-chaining': 7.18.9_@babel+core@7.18.9
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/helper-skip-transparent-expression-wrappers': 7.22.5
+ '@babel/plugin-transform-optional-chaining': 7.23.4(@babel/core@7.18.9)
dev: true
- /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/7.18.9_@babel+core@7.19.6:
- resolution: {integrity: sha512-AHrP9jadvH7qlOj6PINbgSuphjQUAK7AOT7DPjBo9EHoLhQTnnK5u45e1Hd4DbSQEO9nqPWtQ89r+XEOWFScKg==}
+ /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.23.3(@babel/core@7.23.5):
+ resolution: {integrity: sha512-WwlxbfMNdVEpQjZmK5mhm7oSwD3dS6eU+Iwsi4Knl9wAletWem7kaRsGOG+8UEbRyqxY4SS5zvtfXwX+jMxUwQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.13.0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
- '@babel/helper-skip-transparent-expression-wrappers': 7.18.9
- '@babel/plugin-proposal-optional-chaining': 7.18.9_@babel+core@7.19.6
+ '@babel/core': 7.23.5
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/helper-skip-transparent-expression-wrappers': 7.22.5
+ '@babel/plugin-transform-optional-chaining': 7.23.4(@babel/core@7.23.5)
dev: true
- /@babel/plugin-proposal-async-generator-functions/7.19.1_@babel+core@7.18.9:
- resolution: {integrity: sha512-0yu8vNATgLy4ivqMNBIwb1HebCelqN7YX8SL3FDXORv/RqT0zEEWUCH4GH44JsSrvCu6GqnAdR5EBFAPeNBB4Q==}
+ /@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.23.3(@babel/core@7.18.9):
+ resolution: {integrity: sha512-XaJak1qcityzrX0/IU5nKHb34VaibwP3saKqG6a/tppelgllOH13LUann4ZCIBcVOeE6H18K4Vx9QKkVww3z/w==}
engines: {node: '>=6.9.0'}
peerDependencies:
- '@babel/core': ^7.0.0-0
+ '@babel/core': ^7.0.0
dependencies:
'@babel/core': 7.18.9
- '@babel/helper-environment-visitor': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
- '@babel/helper-remap-async-to-generator': 7.18.9_@babel+core@7.18.9
- '@babel/plugin-syntax-async-generators': 7.8.4_@babel+core@7.18.9
- transitivePeerDependencies:
- - supports-color
+ '@babel/helper-environment-visitor': 7.22.20
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-proposal-async-generator-functions/7.19.1_@babel+core@7.19.6:
- resolution: {integrity: sha512-0yu8vNATgLy4ivqMNBIwb1HebCelqN7YX8SL3FDXORv/RqT0zEEWUCH4GH44JsSrvCu6GqnAdR5EBFAPeNBB4Q==}
+ /@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.23.3(@babel/core@7.23.5):
+ resolution: {integrity: sha512-XaJak1qcityzrX0/IU5nKHb34VaibwP3saKqG6a/tppelgllOH13LUann4ZCIBcVOeE6H18K4Vx9QKkVww3z/w==}
engines: {node: '>=6.9.0'}
peerDependencies:
- '@babel/core': ^7.0.0-0
+ '@babel/core': ^7.0.0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-environment-visitor': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
- '@babel/helper-remap-async-to-generator': 7.18.9_@babel+core@7.19.6
- '@babel/plugin-syntax-async-generators': 7.8.4_@babel+core@7.19.6
- transitivePeerDependencies:
- - supports-color
+ '@babel/core': 7.23.5
+ '@babel/helper-environment-visitor': 7.22.20
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-proposal-class-properties/7.18.6_@babel+core@7.18.9:
- resolution: {integrity: sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==}
+ /@babel/plugin-proposal-async-generator-functions@7.19.1(@babel/core@7.22.1):
+ resolution: {integrity: sha512-0yu8vNATgLy4ivqMNBIwb1HebCelqN7YX8SL3FDXORv/RqT0zEEWUCH4GH44JsSrvCu6GqnAdR5EBFAPeNBB4Q==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.18.9
- '@babel/helper-create-class-features-plugin': 7.19.0_@babel+core@7.18.9
- '@babel/helper-plugin-utils': 7.19.0
- transitivePeerDependencies:
- - supports-color
+ '@babel/core': 7.22.1
+ '@babel/helper-environment-visitor': 7.22.20
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.22.1)
+ '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.22.1)
dev: true
- /@babel/plugin-proposal-class-properties/7.18.6_@babel+core@7.19.6:
+ /@babel/plugin-proposal-class-properties@7.18.6(@babel/core@7.22.1):
resolution: {integrity: sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-create-class-features-plugin': 7.19.0_@babel+core@7.19.6
- '@babel/helper-plugin-utils': 7.19.0
- transitivePeerDependencies:
- - supports-color
+ '@babel/core': 7.22.1
+ '@babel/helper-create-class-features-plugin': 7.23.5(@babel/core@7.22.1)
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-proposal-class-static-block/7.18.6_@babel+core@7.18.9:
+ /@babel/plugin-proposal-class-static-block@7.18.6(@babel/core@7.22.1):
resolution: {integrity: sha512-+I3oIiNxrCpup3Gi8n5IGMwj0gOCAjcJUSQEcotNnCCPMEnixawOQ+KeJPlgfjzx+FKQ1QSyZOWe7wmoJp7vhw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.12.0
dependencies:
- '@babel/core': 7.18.9
- '@babel/helper-create-class-features-plugin': 7.19.0_@babel+core@7.18.9
- '@babel/helper-plugin-utils': 7.19.0
- '@babel/plugin-syntax-class-static-block': 7.14.5_@babel+core@7.18.9
- transitivePeerDependencies:
- - supports-color
+ '@babel/core': 7.22.1
+ '@babel/helper-create-class-features-plugin': 7.23.5(@babel/core@7.22.1)
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.22.1)
dev: true
- /@babel/plugin-proposal-class-static-block/7.18.6_@babel+core@7.19.6:
- resolution: {integrity: sha512-+I3oIiNxrCpup3Gi8n5IGMwj0gOCAjcJUSQEcotNnCCPMEnixawOQ+KeJPlgfjzx+FKQ1QSyZOWe7wmoJp7vhw==}
+ /@babel/plugin-proposal-decorators@7.19.6(@babel/core@7.22.1):
+ resolution: {integrity: sha512-PKWforYpkVkogpOW0RaPuh7eQ7AoFgBJP+d87tQCRY2LVbvyGtfRM7RtrhCBsNgZb+2EY28SeWB6p2xe1Z5oAw==}
engines: {node: '>=6.9.0'}
peerDependencies:
- '@babel/core': ^7.12.0
+ '@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-create-class-features-plugin': 7.19.0_@babel+core@7.19.6
- '@babel/helper-plugin-utils': 7.19.0
- '@babel/plugin-syntax-class-static-block': 7.14.5_@babel+core@7.19.6
- transitivePeerDependencies:
- - supports-color
+ '@babel/core': 7.22.1
+ '@babel/helper-create-class-features-plugin': 7.23.5(@babel/core@7.22.1)
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/helper-replace-supers': 7.22.20(@babel/core@7.22.1)
+ '@babel/helper-split-export-declaration': 7.22.6
+ '@babel/plugin-syntax-decorators': 7.19.0(@babel/core@7.22.1)
dev: true
- /@babel/plugin-proposal-decorators/7.19.6_@babel+core@7.18.9:
- resolution: {integrity: sha512-PKWforYpkVkogpOW0RaPuh7eQ7AoFgBJP+d87tQCRY2LVbvyGtfRM7RtrhCBsNgZb+2EY28SeWB6p2xe1Z5oAw==}
+ /@babel/plugin-proposal-dynamic-import@7.18.6(@babel/core@7.22.1):
+ resolution: {integrity: sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.18.9
- '@babel/helper-create-class-features-plugin': 7.19.0_@babel+core@7.18.9
- '@babel/helper-plugin-utils': 7.19.0
- '@babel/helper-replace-supers': 7.19.1
- '@babel/helper-split-export-declaration': 7.18.6
- '@babel/plugin-syntax-decorators': 7.19.0_@babel+core@7.18.9
- transitivePeerDependencies:
- - supports-color
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.22.1)
dev: true
- /@babel/plugin-proposal-dynamic-import/7.18.6_@babel+core@7.18.9:
- resolution: {integrity: sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==}
+ /@babel/plugin-proposal-export-namespace-from@7.18.9(@babel/core@7.18.9):
+ resolution: {integrity: sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
- '@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.18.9
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.18.9)
dev: true
- /@babel/plugin-proposal-dynamic-import/7.18.6_@babel+core@7.19.6:
- resolution: {integrity: sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==}
+ /@babel/plugin-proposal-export-namespace-from@7.18.9(@babel/core@7.22.1):
+ resolution: {integrity: sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
- '@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.19.6
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.22.1)
dev: true
- /@babel/plugin-proposal-export-default-from/7.18.10_@babel+core@7.18.9:
- resolution: {integrity: sha512-5H2N3R2aQFxkV4PIBUR/i7PUSwgTZjouJKzI8eKswfIjT0PhvzkPn0t0wIS5zn6maQuvtT0t1oHtMUz61LOuow==}
+ /@babel/plugin-proposal-json-strings@7.18.6(@babel/core@7.22.1):
+ resolution: {integrity: sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
- '@babel/plugin-syntax-export-default-from': 7.18.6_@babel+core@7.18.9
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.22.1)
dev: true
- /@babel/plugin-proposal-export-namespace-from/7.18.9_@babel+core@7.18.9:
- resolution: {integrity: sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==}
+ /@babel/plugin-proposal-logical-assignment-operators@7.18.9(@babel/core@7.22.1):
+ resolution: {integrity: sha512-128YbMpjCrP35IOExw2Fq+x55LMP42DzhOhX2aNNIdI9avSWl2PI0yuBWarr3RYpZBSPtabfadkH2yeRiMD61Q==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
- '@babel/plugin-syntax-export-namespace-from': 7.8.3_@babel+core@7.18.9
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.22.1)
dev: true
- /@babel/plugin-proposal-export-namespace-from/7.18.9_@babel+core@7.19.6:
- resolution: {integrity: sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==}
+ /@babel/plugin-proposal-nullish-coalescing-operator@7.18.6(@babel/core@7.22.1):
+ resolution: {integrity: sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
- '@babel/plugin-syntax-export-namespace-from': 7.8.3_@babel+core@7.19.6
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.22.1)
dev: true
- /@babel/plugin-proposal-json-strings/7.18.6_@babel+core@7.18.9:
- resolution: {integrity: sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==}
+ /@babel/plugin-proposal-numeric-separator@7.18.6(@babel/core@7.22.1):
+ resolution: {integrity: sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
- '@babel/plugin-syntax-json-strings': 7.8.3_@babel+core@7.18.9
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.22.1)
dev: true
- /@babel/plugin-proposal-json-strings/7.18.6_@babel+core@7.19.6:
- resolution: {integrity: sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==}
+ /@babel/plugin-proposal-object-rest-spread@7.19.4(@babel/core@7.22.1):
+ resolution: {integrity: sha512-wHmj6LDxVDnL+3WhXteUBaoM1aVILZODAUjg11kHqG4cOlfgMQGxw6aCgvrXrmaJR3Bn14oZhImyCPZzRpC93Q==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
- '@babel/plugin-syntax-json-strings': 7.8.3_@babel+core@7.19.6
+ '@babel/compat-data': 7.23.5
+ '@babel/core': 7.22.1
+ '@babel/helper-compilation-targets': 7.22.15
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.22.1)
+ '@babel/plugin-transform-parameters': 7.23.3(@babel/core@7.22.1)
dev: true
- /@babel/plugin-proposal-logical-assignment-operators/7.18.9_@babel+core@7.18.9:
- resolution: {integrity: sha512-128YbMpjCrP35IOExw2Fq+x55LMP42DzhOhX2aNNIdI9avSWl2PI0yuBWarr3RYpZBSPtabfadkH2yeRiMD61Q==}
+ /@babel/plugin-proposal-optional-catch-binding@7.18.6(@babel/core@7.22.1):
+ resolution: {integrity: sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
- '@babel/plugin-syntax-logical-assignment-operators': 7.10.4_@babel+core@7.18.9
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.22.1)
dev: true
- /@babel/plugin-proposal-logical-assignment-operators/7.18.9_@babel+core@7.19.6:
- resolution: {integrity: sha512-128YbMpjCrP35IOExw2Fq+x55LMP42DzhOhX2aNNIdI9avSWl2PI0yuBWarr3RYpZBSPtabfadkH2yeRiMD61Q==}
+ /@babel/plugin-proposal-optional-chaining@7.18.9(@babel/core@7.22.1):
+ resolution: {integrity: sha512-v5nwt4IqBXihxGsW2QmCWMDS3B3bzGIk/EQVZz2ei7f3NJl8NzAJVvUmpDW5q1CRNY+Beb/k58UAH1Km1N411w==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
- '@babel/plugin-syntax-logical-assignment-operators': 7.10.4_@babel+core@7.19.6
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/helper-skip-transparent-expression-wrappers': 7.22.5
+ '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.22.1)
dev: true
- /@babel/plugin-proposal-nullish-coalescing-operator/7.18.6_@babel+core@7.18.9:
- resolution: {integrity: sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==}
+ /@babel/plugin-proposal-private-methods@7.18.6(@babel/core@7.22.1):
+ resolution: {integrity: sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
- '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3_@babel+core@7.18.9
+ '@babel/core': 7.22.1
+ '@babel/helper-create-class-features-plugin': 7.23.5(@babel/core@7.22.1)
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-proposal-nullish-coalescing-operator/7.18.6_@babel+core@7.19.6:
- resolution: {integrity: sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==}
+ /@babel/plugin-proposal-private-property-in-object@7.18.6(@babel/core@7.22.1):
+ resolution: {integrity: sha512-9Rysx7FOctvT5ouj5JODjAFAkgGoudQuLPamZb0v1TGLpapdNaftzifU8NTWQm0IRjqoYypdrSmyWgkocDQ8Dw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
- '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3_@babel+core@7.19.6
+ '@babel/core': 7.22.1
+ '@babel/helper-annotate-as-pure': 7.22.5
+ '@babel/helper-create-class-features-plugin': 7.23.5(@babel/core@7.22.1)
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.22.1)
dev: true
- /@babel/plugin-proposal-numeric-separator/7.18.6_@babel+core@7.18.9:
- resolution: {integrity: sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==}
+ /@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.18.9):
+ resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
- '@babel/plugin-syntax-numeric-separator': 7.10.4_@babel+core@7.18.9
dev: true
- /@babel/plugin-proposal-numeric-separator/7.18.6_@babel+core@7.19.6:
- resolution: {integrity: sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==}
+ /@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.23.5):
+ resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
- '@babel/plugin-syntax-numeric-separator': 7.10.4_@babel+core@7.19.6
+ '@babel/core': 7.23.5
dev: true
- /@babel/plugin-proposal-object-rest-spread/7.12.1_@babel+core@7.12.9:
- resolution: {integrity: sha512-s6SowJIjzlhx8o7lsFx5zmY4At6CTtDvgNQDdPzkBQucle58A6b/TTeEBYtyDgmcXjUTM+vE8YOGHZzzbc/ioA==}
+ /@babel/plugin-proposal-unicode-property-regex@7.18.6(@babel/core@7.22.1):
+ resolution: {integrity: sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==}
+ engines: {node: '>=4'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.12.9
- '@babel/helper-plugin-utils': 7.10.4
- '@babel/plugin-syntax-object-rest-spread': 7.8.3_@babel+core@7.12.9
- '@babel/plugin-transform-parameters': 7.18.8_@babel+core@7.12.9
+ '@babel/core': 7.22.1
+ '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.22.1)
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-proposal-object-rest-spread/7.19.4_@babel+core@7.18.9:
- resolution: {integrity: sha512-wHmj6LDxVDnL+3WhXteUBaoM1aVILZODAUjg11kHqG4cOlfgMQGxw6aCgvrXrmaJR3Bn14oZhImyCPZzRpC93Q==}
- engines: {node: '>=6.9.0'}
+ /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.18.9):
+ resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/compat-data': 7.19.4
'@babel/core': 7.18.9
- '@babel/helper-compilation-targets': 7.19.3_@babel+core@7.18.9
- '@babel/helper-plugin-utils': 7.19.0
- '@babel/plugin-syntax-object-rest-spread': 7.8.3_@babel+core@7.18.9
- '@babel/plugin-transform-parameters': 7.18.8_@babel+core@7.18.9
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-proposal-object-rest-spread/7.19.4_@babel+core@7.19.6:
- resolution: {integrity: sha512-wHmj6LDxVDnL+3WhXteUBaoM1aVILZODAUjg11kHqG4cOlfgMQGxw6aCgvrXrmaJR3Bn14oZhImyCPZzRpC93Q==}
- engines: {node: '>=6.9.0'}
+ /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.22.1):
+ resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/compat-data': 7.19.4
- '@babel/core': 7.19.6
- '@babel/helper-compilation-targets': 7.19.3_@babel+core@7.19.6
- '@babel/helper-plugin-utils': 7.19.0
- '@babel/plugin-syntax-object-rest-spread': 7.8.3_@babel+core@7.19.6
- '@babel/plugin-transform-parameters': 7.18.8_@babel+core@7.19.6
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-proposal-optional-catch-binding/7.18.6_@babel+core@7.18.9:
- resolution: {integrity: sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==}
- engines: {node: '>=6.9.0'}
+ /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.23.5):
+ resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.23.5
+ '@babel/helper-plugin-utils': 7.22.5
+ dev: true
+
+ /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.18.9):
+ resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
- '@babel/plugin-syntax-optional-catch-binding': 7.8.3_@babel+core@7.18.9
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-proposal-optional-catch-binding/7.18.6_@babel+core@7.19.6:
- resolution: {integrity: sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==}
- engines: {node: '>=6.9.0'}
+ /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.22.1):
+ resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
- '@babel/plugin-syntax-optional-catch-binding': 7.8.3_@babel+core@7.19.6
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-proposal-optional-chaining/7.18.9_@babel+core@7.18.9:
- resolution: {integrity: sha512-v5nwt4IqBXihxGsW2QmCWMDS3B3bzGIk/EQVZz2ei7f3NJl8NzAJVvUmpDW5q1CRNY+Beb/k58UAH1Km1N411w==}
- engines: {node: '>=6.9.0'}
+ /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.23.5):
+ resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
- '@babel/helper-skip-transparent-expression-wrappers': 7.18.9
- '@babel/plugin-syntax-optional-chaining': 7.8.3_@babel+core@7.18.9
+ '@babel/core': 7.23.5
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-proposal-optional-chaining/7.18.9_@babel+core@7.19.6:
- resolution: {integrity: sha512-v5nwt4IqBXihxGsW2QmCWMDS3B3bzGIk/EQVZz2ei7f3NJl8NzAJVvUmpDW5q1CRNY+Beb/k58UAH1Km1N411w==}
+ /@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.18.9):
+ resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
- '@babel/helper-skip-transparent-expression-wrappers': 7.18.9
- '@babel/plugin-syntax-optional-chaining': 7.8.3_@babel+core@7.19.6
+ '@babel/core': 7.18.9
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-proposal-private-methods/7.18.6_@babel+core@7.18.9:
- resolution: {integrity: sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==}
+ /@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.22.1):
+ resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.18.9
- '@babel/helper-create-class-features-plugin': 7.19.0_@babel+core@7.18.9
- '@babel/helper-plugin-utils': 7.19.0
- transitivePeerDependencies:
- - supports-color
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-proposal-private-methods/7.18.6_@babel+core@7.19.6:
- resolution: {integrity: sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==}
+ /@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.23.5):
+ resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-create-class-features-plugin': 7.19.0_@babel+core@7.19.6
- '@babel/helper-plugin-utils': 7.19.0
- transitivePeerDependencies:
- - supports-color
+ '@babel/core': 7.23.5
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-proposal-private-property-in-object/7.18.6_@babel+core@7.18.9:
- resolution: {integrity: sha512-9Rysx7FOctvT5ouj5JODjAFAkgGoudQuLPamZb0v1TGLpapdNaftzifU8NTWQm0IRjqoYypdrSmyWgkocDQ8Dw==}
+ /@babel/plugin-syntax-decorators@7.19.0(@babel/core@7.18.9):
+ resolution: {integrity: sha512-xaBZUEDntt4faL1yN8oIFlhfXeQAWJW7CLKYsHTUqriCUbj8xOra8bfxxKGi/UwExPFBuPdH4XfHc9rGQhrVkQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.18.9
- '@babel/helper-annotate-as-pure': 7.18.6
- '@babel/helper-create-class-features-plugin': 7.19.0_@babel+core@7.18.9
- '@babel/helper-plugin-utils': 7.19.0
- '@babel/plugin-syntax-private-property-in-object': 7.14.5_@babel+core@7.18.9
- transitivePeerDependencies:
- - supports-color
+ '@babel/helper-plugin-utils': 7.21.5
dev: true
- /@babel/plugin-proposal-private-property-in-object/7.18.6_@babel+core@7.19.6:
- resolution: {integrity: sha512-9Rysx7FOctvT5ouj5JODjAFAkgGoudQuLPamZb0v1TGLpapdNaftzifU8NTWQm0IRjqoYypdrSmyWgkocDQ8Dw==}
+ /@babel/plugin-syntax-decorators@7.19.0(@babel/core@7.22.1):
+ resolution: {integrity: sha512-xaBZUEDntt4faL1yN8oIFlhfXeQAWJW7CLKYsHTUqriCUbj8xOra8bfxxKGi/UwExPFBuPdH4XfHc9rGQhrVkQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-annotate-as-pure': 7.18.6
- '@babel/helper-create-class-features-plugin': 7.19.0_@babel+core@7.19.6
- '@babel/helper-plugin-utils': 7.19.0
- '@babel/plugin-syntax-private-property-in-object': 7.14.5_@babel+core@7.19.6
- transitivePeerDependencies:
- - supports-color
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.21.5
dev: true
- /@babel/plugin-proposal-unicode-property-regex/7.18.6_@babel+core@7.18.9:
- resolution: {integrity: sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==}
- engines: {node: '>=4'}
+ /@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.18.9):
+ resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.18.9
- '@babel/helper-create-regexp-features-plugin': 7.19.0_@babel+core@7.18.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-proposal-unicode-property-regex/7.18.6_@babel+core@7.19.6:
- resolution: {integrity: sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==}
- engines: {node: '>=4'}
+ /@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.22.1):
+ resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-create-regexp-features-plugin': 7.19.0_@babel+core@7.19.6
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-syntax-async-generators/7.8.4_@babel+core@7.18.9:
- resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==}
+ /@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.23.5):
+ resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.23.5
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-syntax-async-generators/7.8.4_@babel+core@7.19.6:
- resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==}
+ /@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.18.9):
+ resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.18.9
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-syntax-bigint/7.8.3_@babel+core@7.18.9:
- resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==}
+ /@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.22.1):
+ resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-syntax-class-properties/7.12.13_@babel+core@7.18.9:
- resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==}
+ /@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.23.5):
+ resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.23.5
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-syntax-class-properties/7.12.13_@babel+core@7.19.6:
- resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==}
+ /@babel/plugin-syntax-import-assertions@7.18.6(@babel/core@7.22.1):
+ resolution: {integrity: sha512-/DU3RXad9+bZwrgWJQKbr39gYbJpLJHezqEzRzi/BHRlJ9zsQb4CK2CA/5apllXNomwA1qHwzvHl+AdEmC5krQ==}
+ engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-syntax-class-static-block/7.14.5_@babel+core@7.18.9:
- resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==}
+ /@babel/plugin-syntax-import-assertions@7.23.3(@babel/core@7.18.9):
+ resolution: {integrity: sha512-lPgDSU+SJLK3xmFDTV2ZRQAiM7UuUjGidwBywFavObCiZc1BeAAcMtHJKUya92hPHO+at63JJPLygilZard8jw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-syntax-class-static-block/7.14.5_@babel+core@7.19.6:
- resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==}
+ /@babel/plugin-syntax-import-assertions@7.23.3(@babel/core@7.23.5):
+ resolution: {integrity: sha512-lPgDSU+SJLK3xmFDTV2ZRQAiM7UuUjGidwBywFavObCiZc1BeAAcMtHJKUya92hPHO+at63JJPLygilZard8jw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.23.5
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-syntax-decorators/7.19.0_@babel+core@7.18.9:
- resolution: {integrity: sha512-xaBZUEDntt4faL1yN8oIFlhfXeQAWJW7CLKYsHTUqriCUbj8xOra8bfxxKGi/UwExPFBuPdH4XfHc9rGQhrVkQ==}
+ /@babel/plugin-syntax-import-attributes@7.23.3(@babel/core@7.18.9):
+ resolution: {integrity: sha512-pawnE0P9g10xgoP7yKr6CK63K2FMsTE+FZidZO/1PwRdzmAPVs+HS1mAURUsgaoxammTJvULUdIkEK0gOcU2tA==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-syntax-dynamic-import/7.8.3_@babel+core@7.18.9:
- resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==}
+ /@babel/plugin-syntax-import-attributes@7.23.3(@babel/core@7.23.5):
+ resolution: {integrity: sha512-pawnE0P9g10xgoP7yKr6CK63K2FMsTE+FZidZO/1PwRdzmAPVs+HS1mAURUsgaoxammTJvULUdIkEK0gOcU2tA==}
+ engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.23.5
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-syntax-dynamic-import/7.8.3_@babel+core@7.19.6:
- resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==}
+ /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.18.9):
+ resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.18.9
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-syntax-export-default-from/7.18.6_@babel+core@7.18.9:
- resolution: {integrity: sha512-Kr//z3ujSVNx6E9z9ih5xXXMqK07VVTuqPmqGe6Mss/zW5XPeLZeSDZoP9ab/hT4wPKqAgjl2PnhPrcpk8Seew==}
- engines: {node: '>=6.9.0'}
+ /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.23.5):
+ resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.23.5
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-syntax-export-namespace-from/7.8.3_@babel+core@7.18.9:
- resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==}
+ /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.18.9):
+ resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-syntax-export-namespace-from/7.8.3_@babel+core@7.19.6:
- resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==}
+ /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.22.1):
+ resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-syntax-import-assertions/7.18.6_@babel+core@7.18.9:
- resolution: {integrity: sha512-/DU3RXad9+bZwrgWJQKbr39gYbJpLJHezqEzRzi/BHRlJ9zsQb4CK2CA/5apllXNomwA1qHwzvHl+AdEmC5krQ==}
- engines: {node: '>=6.9.0'}
+ /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.23.5):
+ resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.23.5
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-syntax-import-assertions/7.18.6_@babel+core@7.19.6:
- resolution: {integrity: sha512-/DU3RXad9+bZwrgWJQKbr39gYbJpLJHezqEzRzi/BHRlJ9zsQb4CK2CA/5apllXNomwA1qHwzvHl+AdEmC5krQ==}
+ /@babel/plugin-syntax-jsx@7.21.4(@babel/core@7.18.9):
+ resolution: {integrity: sha512-5hewiLct5OKyh6PLKEYaFclcqtIgCb6bmELouxjF6up5q3Sov7rOayW4RwhbaBL0dit8rA80GNfY+UuDp2mBbQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.18.9
+ '@babel/helper-plugin-utils': 7.21.5
dev: true
- /@babel/plugin-syntax-import-meta/7.10.4_@babel+core@7.18.9:
- resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==}
+ /@babel/plugin-syntax-jsx@7.21.4(@babel/core@7.22.1):
+ resolution: {integrity: sha512-5hewiLct5OKyh6PLKEYaFclcqtIgCb6bmELouxjF6up5q3Sov7rOayW4RwhbaBL0dit8rA80GNfY+UuDp2mBbQ==}
+ engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.21.5
dev: true
- /@babel/plugin-syntax-json-strings/7.8.3_@babel+core@7.18.9:
- resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==}
+ /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.18.9):
+ resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-syntax-json-strings/7.8.3_@babel+core@7.19.6:
- resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==}
+ /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.22.1):
+ resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-syntax-jsx/7.12.1_@babel+core@7.12.9:
- resolution: {integrity: sha512-1yRi7yAtB0ETgxdY9ti/p2TivUxJkTdhu/ZbF9MshVGqOx1TdB3b7xCXs49Fupgg50N45KcAsRP/ZqWjs9SRjg==}
+ /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.23.5):
+ resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.12.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.23.5
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-syntax-jsx/7.18.6_@babel+core@7.18.9:
- resolution: {integrity: sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==}
- engines: {node: '>=6.9.0'}
+ /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.18.9):
+ resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-syntax-logical-assignment-operators/7.10.4_@babel+core@7.18.9:
- resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==}
+ /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.22.1):
+ resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-syntax-logical-assignment-operators/7.10.4_@babel+core@7.19.6:
- resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==}
+ /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.23.5):
+ resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.23.5
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-syntax-nullish-coalescing-operator/7.8.3_@babel+core@7.18.9:
- resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==}
+ /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.18.9):
+ resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-syntax-nullish-coalescing-operator/7.8.3_@babel+core@7.19.6:
- resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==}
+ /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.22.1):
+ resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-syntax-numeric-separator/7.10.4_@babel+core@7.18.9:
+ /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.23.5):
resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.23.5
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-syntax-numeric-separator/7.10.4_@babel+core@7.19.6:
- resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==}
+ /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.18.9):
+ resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.18.9
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-syntax-object-rest-spread/7.8.3_@babel+core@7.12.9:
+ /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.22.1):
resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.12.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-syntax-object-rest-spread/7.8.3_@babel+core@7.18.9:
+ /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.23.5):
resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.23.5
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-syntax-object-rest-spread/7.8.3_@babel+core@7.19.6:
- resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==}
+ /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.18.9):
+ resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.18.9
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-syntax-optional-catch-binding/7.8.3_@babel+core@7.18.9:
+ /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.22.1):
resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-syntax-optional-catch-binding/7.8.3_@babel+core@7.19.6:
+ /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.23.5):
resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.23.5
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-syntax-optional-chaining/7.8.3_@babel+core@7.18.9:
+ /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.18.9):
resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-syntax-optional-chaining/7.8.3_@babel+core@7.19.6:
+ /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.22.1):
resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.22.5
+ dev: true
+
+ /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.23.5):
+ resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.23.5
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-syntax-private-property-in-object/7.14.5_@babel+core@7.18.9:
+ /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.18.9):
resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-syntax-private-property-in-object/7.14.5_@babel+core@7.19.6:
+ /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.22.1):
resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-syntax-top-level-await/7.14.5_@babel+core@7.18.9:
+ /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.23.5):
+ resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.23.5
+ '@babel/helper-plugin-utils': 7.22.5
+ dev: true
+
+ /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.18.9):
resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-syntax-top-level-await/7.14.5_@babel+core@7.19.6:
+ /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.22.1):
resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-syntax-typescript/7.18.6_@babel+core@7.18.9:
- resolution: {integrity: sha512-mAWAuq4rvOepWCBid55JuRNvpTNf2UGVgoz4JV0fXEKolsVZDzsa4NqCef758WZJj/GDu0gVGItjKFiClTAmZA==}
+ /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.23.5):
+ resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.23.5
+ '@babel/helper-plugin-utils': 7.22.5
+ dev: true
+
+ /@babel/plugin-syntax-typescript@7.20.0(@babel/core@7.18.9):
+ resolution: {integrity: sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-transform-arrow-functions/7.18.6_@babel+core@7.18.9:
- resolution: {integrity: sha512-9S9X9RUefzrsHZmKMbDXxweEH+YlE8JJEuat9FdvW9Qh1cw7W64jELCtWNkPBPX5En45uy28KGvA/AySqUh8CQ==}
+ /@babel/plugin-syntax-typescript@7.20.0(@babel/core@7.22.1):
+ resolution: {integrity: sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.22.5
+ dev: true
+
+ /@babel/plugin-syntax-typescript@7.21.4(@babel/core@7.18.9):
+ resolution: {integrity: sha512-xz0D39NvhQn4t4RNsHmDnnsaQizIlUkdtYvLs8La1BlfjQ6JEwxkJGeqJMW2tAXx+q6H+WFuUTXNdYVpEya0YA==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-transform-arrow-functions/7.18.6_@babel+core@7.19.6:
+ /@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.18.9):
+ resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+ dependencies:
+ '@babel/core': 7.18.9
+ '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.18.9)
+ '@babel/helper-plugin-utils': 7.22.5
+ dev: true
+
+ /@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.23.5):
+ resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+ dependencies:
+ '@babel/core': 7.23.5
+ '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.5)
+ '@babel/helper-plugin-utils': 7.22.5
+ dev: true
+
+ /@babel/plugin-transform-arrow-functions@7.18.6(@babel/core@7.22.1):
resolution: {integrity: sha512-9S9X9RUefzrsHZmKMbDXxweEH+YlE8JJEuat9FdvW9Qh1cw7W64jELCtWNkPBPX5En45uy28KGvA/AySqUh8CQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-transform-async-to-generator/7.18.6_@babel+core@7.18.9:
- resolution: {integrity: sha512-ARE5wZLKnTgPW7/1ftQmSi1CmkqqHo2DNmtztFhvgtOWSDfq0Cq9/9L+KnZNYSNrydBekhW3rwShduf59RoXag==}
+ /@babel/plugin-transform-arrow-functions@7.23.3(@babel/core@7.18.9):
+ resolution: {integrity: sha512-NzQcQrzaQPkaEwoTm4Mhyl8jI1huEL/WWIEvudjTCMJ9aBZNpsJbMASx7EQECtQQPS/DcnFpo0FIh3LvEO9cxQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.18.9
- '@babel/helper-module-imports': 7.18.6
- '@babel/helper-plugin-utils': 7.19.0
- '@babel/helper-remap-async-to-generator': 7.18.9_@babel+core@7.18.9
- transitivePeerDependencies:
- - supports-color
+ '@babel/helper-plugin-utils': 7.22.5
+ dev: true
+
+ /@babel/plugin-transform-arrow-functions@7.23.3(@babel/core@7.23.5):
+ resolution: {integrity: sha512-NzQcQrzaQPkaEwoTm4Mhyl8jI1huEL/WWIEvudjTCMJ9aBZNpsJbMASx7EQECtQQPS/DcnFpo0FIh3LvEO9cxQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.23.5
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-transform-async-to-generator/7.18.6_@babel+core@7.19.6:
+ /@babel/plugin-transform-async-generator-functions@7.23.4(@babel/core@7.18.9):
+ resolution: {integrity: sha512-efdkfPhHYTtn0G6n2ddrESE91fgXxjlqLsnUtPWnJs4a4mZIbUaK7ffqKIIUKXSHwcDvaCVX6GXkaJJFqtX7jw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.18.9
+ '@babel/helper-environment-visitor': 7.22.20
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.18.9)
+ '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.18.9)
+ dev: true
+
+ /@babel/plugin-transform-async-generator-functions@7.23.4(@babel/core@7.23.5):
+ resolution: {integrity: sha512-efdkfPhHYTtn0G6n2ddrESE91fgXxjlqLsnUtPWnJs4a4mZIbUaK7ffqKIIUKXSHwcDvaCVX6GXkaJJFqtX7jw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.23.5
+ '@babel/helper-environment-visitor': 7.22.20
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.23.5)
+ '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.23.5)
+ dev: true
+
+ /@babel/plugin-transform-async-to-generator@7.18.6(@babel/core@7.22.1):
resolution: {integrity: sha512-ARE5wZLKnTgPW7/1ftQmSi1CmkqqHo2DNmtztFhvgtOWSDfq0Cq9/9L+KnZNYSNrydBekhW3rwShduf59RoXag==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-module-imports': 7.18.6
- '@babel/helper-plugin-utils': 7.19.0
- '@babel/helper-remap-async-to-generator': 7.18.9_@babel+core@7.19.6
- transitivePeerDependencies:
- - supports-color
+ '@babel/core': 7.22.1
+ '@babel/helper-module-imports': 7.22.15
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.22.1)
dev: true
- /@babel/plugin-transform-block-scoped-functions/7.18.6_@babel+core@7.18.9:
- resolution: {integrity: sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==}
+ /@babel/plugin-transform-async-to-generator@7.23.3(@babel/core@7.18.9):
+ resolution: {integrity: sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/helper-module-imports': 7.22.15
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.18.9)
+ dev: true
+
+ /@babel/plugin-transform-async-to-generator@7.23.3(@babel/core@7.23.5):
+ resolution: {integrity: sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.23.5
+ '@babel/helper-module-imports': 7.22.15
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.23.5)
dev: true
- /@babel/plugin-transform-block-scoped-functions/7.18.6_@babel+core@7.19.6:
+ /@babel/plugin-transform-block-scoped-functions@7.18.6(@babel/core@7.22.1):
resolution: {integrity: sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-transform-block-scoping/7.19.4_@babel+core@7.18.9:
- resolution: {integrity: sha512-934S2VLLlt2hRJwPf4MczaOr4hYF0z+VKPwqTNxyKX7NthTiPfhuKFWQZHXRM0vh/wo/VyXB3s4bZUNA08l+tQ==}
+ /@babel/plugin-transform-block-scoped-functions@7.23.3(@babel/core@7.18.9):
+ resolution: {integrity: sha512-vI+0sIaPIO6CNuM9Kk5VmXcMVRiOpDh7w2zZt9GXzmE/9KD70CUEVhvPR/etAeNK/FAEkhxQtXOzVF3EuRL41A==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/helper-plugin-utils': 7.22.5
+ dev: true
+
+ /@babel/plugin-transform-block-scoped-functions@7.23.3(@babel/core@7.23.5):
+ resolution: {integrity: sha512-vI+0sIaPIO6CNuM9Kk5VmXcMVRiOpDh7w2zZt9GXzmE/9KD70CUEVhvPR/etAeNK/FAEkhxQtXOzVF3EuRL41A==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.23.5
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-transform-block-scoping/7.19.4_@babel+core@7.19.6:
+ /@babel/plugin-transform-block-scoping@7.19.4(@babel/core@7.22.1):
resolution: {integrity: sha512-934S2VLLlt2hRJwPf4MczaOr4hYF0z+VKPwqTNxyKX7NthTiPfhuKFWQZHXRM0vh/wo/VyXB3s4bZUNA08l+tQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.22.5
+ dev: true
+
+ /@babel/plugin-transform-block-scoping@7.23.4(@babel/core@7.18.9):
+ resolution: {integrity: sha512-0QqbP6B6HOh7/8iNR4CQU2Th/bbRtBp4KS9vcaZd1fZ0wSh5Fyssg0UCIHwxh+ka+pNDREbVLQnHCMHKZfPwfw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.18.9
+ '@babel/helper-plugin-utils': 7.22.5
+ dev: true
+
+ /@babel/plugin-transform-block-scoping@7.23.4(@babel/core@7.23.5):
+ resolution: {integrity: sha512-0QqbP6B6HOh7/8iNR4CQU2Th/bbRtBp4KS9vcaZd1fZ0wSh5Fyssg0UCIHwxh+ka+pNDREbVLQnHCMHKZfPwfw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.23.5
+ '@babel/helper-plugin-utils': 7.22.5
+ dev: true
+
+ /@babel/plugin-transform-class-properties@7.23.3(@babel/core@7.18.9):
+ resolution: {integrity: sha512-uM+AN8yCIjDPccsKGlw271xjJtGii+xQIF/uMPS8H15L12jZTsLfF4o5vNO7d/oUguOyfdikHGc/yi9ge4SGIg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.18.9
+ '@babel/helper-create-class-features-plugin': 7.23.5(@babel/core@7.18.9)
+ '@babel/helper-plugin-utils': 7.22.5
+ dev: true
+
+ /@babel/plugin-transform-class-properties@7.23.3(@babel/core@7.23.5):
+ resolution: {integrity: sha512-uM+AN8yCIjDPccsKGlw271xjJtGii+xQIF/uMPS8H15L12jZTsLfF4o5vNO7d/oUguOyfdikHGc/yi9ge4SGIg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.23.5
+ '@babel/helper-create-class-features-plugin': 7.23.5(@babel/core@7.23.5)
+ '@babel/helper-plugin-utils': 7.22.5
+ dev: true
+
+ /@babel/plugin-transform-class-static-block@7.23.4(@babel/core@7.18.9):
+ resolution: {integrity: sha512-nsWu/1M+ggti1SOALj3hfx5FXzAY06fwPJsUZD4/A5e1bWi46VUIWtD+kOX6/IdhXGsXBWllLFDSnqSCdUNydQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.12.0
+ dependencies:
+ '@babel/core': 7.18.9
+ '@babel/helper-create-class-features-plugin': 7.23.5(@babel/core@7.18.9)
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.18.9)
dev: true
- /@babel/plugin-transform-classes/7.19.0_@babel+core@7.18.9:
+ /@babel/plugin-transform-class-static-block@7.23.4(@babel/core@7.23.5):
+ resolution: {integrity: sha512-nsWu/1M+ggti1SOALj3hfx5FXzAY06fwPJsUZD4/A5e1bWi46VUIWtD+kOX6/IdhXGsXBWllLFDSnqSCdUNydQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.12.0
+ dependencies:
+ '@babel/core': 7.23.5
+ '@babel/helper-create-class-features-plugin': 7.23.5(@babel/core@7.23.5)
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.23.5)
+ dev: true
+
+ /@babel/plugin-transform-classes@7.19.0(@babel/core@7.22.1):
resolution: {integrity: sha512-YfeEE9kCjqTS9IitkgfJuxjcEtLUHMqa8yUJ6zdz8vR7hKuo6mOy2C05P0F1tdMmDCeuyidKnlrw/iTppHcr2A==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
+ '@babel/core': 7.22.1
+ '@babel/helper-annotate-as-pure': 7.22.5
+ '@babel/helper-compilation-targets': 7.22.15
+ '@babel/helper-environment-visitor': 7.22.20
+ '@babel/helper-function-name': 7.23.0
+ '@babel/helper-optimise-call-expression': 7.22.5
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/helper-replace-supers': 7.22.20(@babel/core@7.22.1)
+ '@babel/helper-split-export-declaration': 7.22.6
+ globals: 11.12.0
+ dev: true
+
+ /@babel/plugin-transform-classes@7.23.5(@babel/core@7.18.9):
+ resolution: {integrity: sha512-jvOTR4nicqYC9yzOHIhXG5emiFEOpappSJAl73SDSEDcybD+Puuze8Tnpb9p9qEyYup24tq891gkaygIFvWDqg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
'@babel/core': 7.18.9
- '@babel/helper-annotate-as-pure': 7.18.6
- '@babel/helper-compilation-targets': 7.19.3_@babel+core@7.18.9
- '@babel/helper-environment-visitor': 7.18.9
- '@babel/helper-function-name': 7.19.0
- '@babel/helper-optimise-call-expression': 7.18.6
- '@babel/helper-plugin-utils': 7.19.0
- '@babel/helper-replace-supers': 7.19.1
- '@babel/helper-split-export-declaration': 7.18.6
+ '@babel/helper-annotate-as-pure': 7.22.5
+ '@babel/helper-compilation-targets': 7.22.15
+ '@babel/helper-environment-visitor': 7.22.20
+ '@babel/helper-function-name': 7.23.0
+ '@babel/helper-optimise-call-expression': 7.22.5
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/helper-replace-supers': 7.22.20(@babel/core@7.18.9)
+ '@babel/helper-split-export-declaration': 7.22.6
globals: 11.12.0
- transitivePeerDependencies:
- - supports-color
dev: true
- /@babel/plugin-transform-classes/7.19.0_@babel+core@7.19.6:
- resolution: {integrity: sha512-YfeEE9kCjqTS9IitkgfJuxjcEtLUHMqa8yUJ6zdz8vR7hKuo6mOy2C05P0F1tdMmDCeuyidKnlrw/iTppHcr2A==}
+ /@babel/plugin-transform-classes@7.23.5(@babel/core@7.23.5):
+ resolution: {integrity: sha512-jvOTR4nicqYC9yzOHIhXG5emiFEOpappSJAl73SDSEDcybD+Puuze8Tnpb9p9qEyYup24tq891gkaygIFvWDqg==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-annotate-as-pure': 7.18.6
- '@babel/helper-compilation-targets': 7.19.3_@babel+core@7.19.6
- '@babel/helper-environment-visitor': 7.18.9
- '@babel/helper-function-name': 7.19.0
- '@babel/helper-optimise-call-expression': 7.18.6
- '@babel/helper-plugin-utils': 7.19.0
- '@babel/helper-replace-supers': 7.19.1
- '@babel/helper-split-export-declaration': 7.18.6
+ '@babel/core': 7.23.5
+ '@babel/helper-annotate-as-pure': 7.22.5
+ '@babel/helper-compilation-targets': 7.22.15
+ '@babel/helper-environment-visitor': 7.22.20
+ '@babel/helper-function-name': 7.23.0
+ '@babel/helper-optimise-call-expression': 7.22.5
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/helper-replace-supers': 7.22.20(@babel/core@7.23.5)
+ '@babel/helper-split-export-declaration': 7.22.6
globals: 11.12.0
- transitivePeerDependencies:
- - supports-color
dev: true
- /@babel/plugin-transform-computed-properties/7.18.9_@babel+core@7.18.9:
+ /@babel/plugin-transform-computed-properties@7.18.9(@babel/core@7.22.1):
resolution: {integrity: sha512-+i0ZU1bCDymKakLxn5srGHrsAPRELC2WIbzwjLhHW9SIE1cPYkLCL0NlnXMZaM1vhfgA2+M7hySk42VBvrkBRw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.22.5
+ dev: true
+
+ /@babel/plugin-transform-computed-properties@7.23.3(@babel/core@7.18.9):
+ resolution: {integrity: sha512-dTj83UVTLw/+nbiHqQSFdwO9CbTtwq1DsDqm3CUEtDrZNET5rT5E6bIdTlOftDTDLMYxvxHNEYO4B9SLl8SLZw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
'@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/template': 7.22.15
dev: true
- /@babel/plugin-transform-computed-properties/7.18.9_@babel+core@7.19.6:
- resolution: {integrity: sha512-+i0ZU1bCDymKakLxn5srGHrsAPRELC2WIbzwjLhHW9SIE1cPYkLCL0NlnXMZaM1vhfgA2+M7hySk42VBvrkBRw==}
+ /@babel/plugin-transform-computed-properties@7.23.3(@babel/core@7.23.5):
+ resolution: {integrity: sha512-dTj83UVTLw/+nbiHqQSFdwO9CbTtwq1DsDqm3CUEtDrZNET5rT5E6bIdTlOftDTDLMYxvxHNEYO4B9SLl8SLZw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.23.5
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/template': 7.22.15
dev: true
- /@babel/plugin-transform-destructuring/7.19.4_@babel+core@7.18.9:
+ /@babel/plugin-transform-destructuring@7.19.4(@babel/core@7.22.1):
resolution: {integrity: sha512-t0j0Hgidqf0aM86dF8U+vXYReUgJnlv4bZLsyoPnwZNrGY+7/38o8YjaELrvHeVfTZao15kjR0PVv0nju2iduA==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.22.5
+ dev: true
+
+ /@babel/plugin-transform-destructuring@7.23.3(@babel/core@7.18.9):
+ resolution: {integrity: sha512-n225npDqjDIr967cMScVKHXJs7rout1q+tt50inyBCPkyZ8KxeI6d+GIbSBTT/w/9WdlWDOej3V9HE5Lgk57gw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
'@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-transform-destructuring/7.19.4_@babel+core@7.19.6:
- resolution: {integrity: sha512-t0j0Hgidqf0aM86dF8U+vXYReUgJnlv4bZLsyoPnwZNrGY+7/38o8YjaELrvHeVfTZao15kjR0PVv0nju2iduA==}
+ /@babel/plugin-transform-destructuring@7.23.3(@babel/core@7.23.5):
+ resolution: {integrity: sha512-n225npDqjDIr967cMScVKHXJs7rout1q+tt50inyBCPkyZ8KxeI6d+GIbSBTT/w/9WdlWDOej3V9HE5Lgk57gw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.23.5
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-transform-dotall-regex/7.18.6_@babel+core@7.18.9:
+ /@babel/plugin-transform-dotall-regex@7.18.6(@babel/core@7.22.1):
resolution: {integrity: sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
+ '@babel/core': 7.22.1
+ '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.22.1)
+ '@babel/helper-plugin-utils': 7.22.5
+ dev: true
+
+ /@babel/plugin-transform-dotall-regex@7.23.3(@babel/core@7.18.9):
+ resolution: {integrity: sha512-vgnFYDHAKzFaTVp+mneDsIEbnJ2Np/9ng9iviHw3P/KVcgONxpNULEW/51Z/BaFojG2GI2GwwXck5uV1+1NOYQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
'@babel/core': 7.18.9
- '@babel/helper-create-regexp-features-plugin': 7.19.0_@babel+core@7.18.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.18.9)
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-transform-dotall-regex/7.18.6_@babel+core@7.19.6:
- resolution: {integrity: sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==}
+ /@babel/plugin-transform-dotall-regex@7.23.3(@babel/core@7.22.1):
+ resolution: {integrity: sha512-vgnFYDHAKzFaTVp+mneDsIEbnJ2Np/9ng9iviHw3P/KVcgONxpNULEW/51Z/BaFojG2GI2GwwXck5uV1+1NOYQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-create-regexp-features-plugin': 7.19.0_@babel+core@7.19.6
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.22.1
+ '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.22.1)
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-transform-duplicate-keys/7.18.9_@babel+core@7.18.9:
+ /@babel/plugin-transform-dotall-regex@7.23.3(@babel/core@7.23.5):
+ resolution: {integrity: sha512-vgnFYDHAKzFaTVp+mneDsIEbnJ2Np/9ng9iviHw3P/KVcgONxpNULEW/51Z/BaFojG2GI2GwwXck5uV1+1NOYQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.23.5
+ '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.5)
+ '@babel/helper-plugin-utils': 7.22.5
+ dev: true
+
+ /@babel/plugin-transform-duplicate-keys@7.18.9(@babel/core@7.22.1):
resolution: {integrity: sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.22.5
+ dev: true
+
+ /@babel/plugin-transform-duplicate-keys@7.23.3(@babel/core@7.18.9):
+ resolution: {integrity: sha512-RrqQ+BQmU3Oyav3J+7/myfvRCq7Tbz+kKLLshUmMwNlDHExbGL7ARhajvoBJEvc+fCguPPu887N+3RRXBVKZUA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
'@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-transform-duplicate-keys/7.18.9_@babel+core@7.19.6:
- resolution: {integrity: sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==}
+ /@babel/plugin-transform-duplicate-keys@7.23.3(@babel/core@7.23.5):
+ resolution: {integrity: sha512-RrqQ+BQmU3Oyav3J+7/myfvRCq7Tbz+kKLLshUmMwNlDHExbGL7ARhajvoBJEvc+fCguPPu887N+3RRXBVKZUA==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.23.5
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-transform-exponentiation-operator/7.18.6_@babel+core@7.18.9:
- resolution: {integrity: sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==}
+ /@babel/plugin-transform-dynamic-import@7.23.4(@babel/core@7.18.9):
+ resolution: {integrity: sha512-V6jIbLhdJK86MaLh4Jpghi8ho5fGzt3imHOBu/x0jlBaPYqDoWz4RDXjmMOfnh+JWNaQleEAByZLV0QzBT4YQQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.18.9
- '@babel/helper-builder-binary-assignment-operator-visitor': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.18.9)
+ dev: true
+
+ /@babel/plugin-transform-dynamic-import@7.23.4(@babel/core@7.23.5):
+ resolution: {integrity: sha512-V6jIbLhdJK86MaLh4Jpghi8ho5fGzt3imHOBu/x0jlBaPYqDoWz4RDXjmMOfnh+JWNaQleEAByZLV0QzBT4YQQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.23.5
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.23.5)
dev: true
- /@babel/plugin-transform-exponentiation-operator/7.18.6_@babel+core@7.19.6:
+ /@babel/plugin-transform-exponentiation-operator@7.18.6(@babel/core@7.22.1):
resolution: {integrity: sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-builder-binary-assignment-operator-visitor': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.22.1
+ '@babel/helper-builder-binary-assignment-operator-visitor': 7.22.15
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-transform-for-of/7.18.8_@babel+core@7.18.9:
- resolution: {integrity: sha512-yEfTRnjuskWYo0k1mHUqrVWaZwrdq8AYbfrpqULOJOaucGSp4mNMVps+YtA8byoevxS/urwU75vyhQIxcCgiBQ==}
+ /@babel/plugin-transform-exponentiation-operator@7.23.3(@babel/core@7.18.9):
+ resolution: {integrity: sha512-5fhCsl1odX96u7ILKHBj4/Y8vipoqwsJMh4csSA8qFfxrZDEA4Ssku2DyNvMJSmZNOEBT750LfFPbtrnTP90BQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/helper-builder-binary-assignment-operator-visitor': 7.22.15
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-transform-for-of/7.18.8_@babel+core@7.19.6:
+ /@babel/plugin-transform-exponentiation-operator@7.23.3(@babel/core@7.23.5):
+ resolution: {integrity: sha512-5fhCsl1odX96u7ILKHBj4/Y8vipoqwsJMh4csSA8qFfxrZDEA4Ssku2DyNvMJSmZNOEBT750LfFPbtrnTP90BQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.23.5
+ '@babel/helper-builder-binary-assignment-operator-visitor': 7.22.15
+ '@babel/helper-plugin-utils': 7.22.5
+ dev: true
+
+ /@babel/plugin-transform-export-namespace-from@7.23.4(@babel/core@7.18.9):
+ resolution: {integrity: sha512-GzuSBcKkx62dGzZI1WVgTWvkkz84FZO5TC5T8dl/Tht/rAla6Dg/Mz9Yhypg+ezVACf/rgDuQt3kbWEv7LdUDQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.18.9
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.18.9)
+ dev: true
+
+ /@babel/plugin-transform-export-namespace-from@7.23.4(@babel/core@7.23.5):
+ resolution: {integrity: sha512-GzuSBcKkx62dGzZI1WVgTWvkkz84FZO5TC5T8dl/Tht/rAla6Dg/Mz9Yhypg+ezVACf/rgDuQt3kbWEv7LdUDQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.23.5
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.23.5)
+ dev: true
+
+ /@babel/plugin-transform-for-of@7.18.8(@babel/core@7.22.1):
resolution: {integrity: sha512-yEfTRnjuskWYo0k1mHUqrVWaZwrdq8AYbfrpqULOJOaucGSp4mNMVps+YtA8byoevxS/urwU75vyhQIxcCgiBQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-transform-function-name/7.18.9_@babel+core@7.18.9:
- resolution: {integrity: sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==}
+ /@babel/plugin-transform-for-of@7.23.3(@babel/core@7.18.9):
+ resolution: {integrity: sha512-X8jSm8X1CMwxmK878qsUGJRmbysKNbdpTv/O1/v0LuY/ZkZrng5WYiekYSdg9m09OTmDDUWeEDsTE+17WYbAZw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.18.9
- '@babel/helper-compilation-targets': 7.19.3_@babel+core@7.18.9
- '@babel/helper-function-name': 7.19.0
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/helper-plugin-utils': 7.22.5
+ dev: true
+
+ /@babel/plugin-transform-for-of@7.23.3(@babel/core@7.23.5):
+ resolution: {integrity: sha512-X8jSm8X1CMwxmK878qsUGJRmbysKNbdpTv/O1/v0LuY/ZkZrng5WYiekYSdg9m09OTmDDUWeEDsTE+17WYbAZw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.23.5
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-transform-function-name/7.18.9_@babel+core@7.19.6:
+ /@babel/plugin-transform-function-name@7.18.9(@babel/core@7.22.1):
resolution: {integrity: sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-compilation-targets': 7.19.3_@babel+core@7.19.6
- '@babel/helper-function-name': 7.19.0
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.22.1
+ '@babel/helper-compilation-targets': 7.18.9(@babel/core@7.22.1)
+ '@babel/helper-function-name': 7.23.0
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-transform-literals/7.18.9_@babel+core@7.18.9:
- resolution: {integrity: sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==}
+ /@babel/plugin-transform-function-name@7.23.3(@babel/core@7.18.9):
+ resolution: {integrity: sha512-I1QXp1LxIvt8yLaib49dRW5Okt7Q4oaxao6tFVKS/anCdEOMtYwWVKoiOA1p34GOWIZjUK0E+zCp7+l1pfQyiw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/helper-compilation-targets': 7.22.15
+ '@babel/helper-function-name': 7.23.0
+ '@babel/helper-plugin-utils': 7.22.5
+ dev: true
+
+ /@babel/plugin-transform-function-name@7.23.3(@babel/core@7.23.5):
+ resolution: {integrity: sha512-I1QXp1LxIvt8yLaib49dRW5Okt7Q4oaxao6tFVKS/anCdEOMtYwWVKoiOA1p34GOWIZjUK0E+zCp7+l1pfQyiw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.23.5
+ '@babel/helper-compilation-targets': 7.22.15
+ '@babel/helper-function-name': 7.23.0
+ '@babel/helper-plugin-utils': 7.22.5
+ dev: true
+
+ /@babel/plugin-transform-json-strings@7.23.4(@babel/core@7.18.9):
+ resolution: {integrity: sha512-81nTOqM1dMwZ/aRXQ59zVubN9wHGqk6UtqRK+/q+ciXmRy8fSolhGVvG09HHRGo4l6fr/c4ZhXUQH0uFW7PZbg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.18.9
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.18.9)
dev: true
- /@babel/plugin-transform-literals/7.18.9_@babel+core@7.19.6:
+ /@babel/plugin-transform-json-strings@7.23.4(@babel/core@7.23.5):
+ resolution: {integrity: sha512-81nTOqM1dMwZ/aRXQ59zVubN9wHGqk6UtqRK+/q+ciXmRy8fSolhGVvG09HHRGo4l6fr/c4ZhXUQH0uFW7PZbg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.23.5
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.23.5)
+ dev: true
+
+ /@babel/plugin-transform-literals@7.18.9(@babel/core@7.22.1):
resolution: {integrity: sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-transform-member-expression-literals/7.18.6_@babel+core@7.18.9:
- resolution: {integrity: sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==}
+ /@babel/plugin-transform-literals@7.23.3(@babel/core@7.18.9):
+ resolution: {integrity: sha512-wZ0PIXRxnwZvl9AYpqNUxpZ5BiTGrYt7kueGQ+N5FiQ7RCOD4cm8iShd6S6ggfVIWaJf2EMk8eRzAh52RfP4rQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/helper-plugin-utils': 7.22.5
+ dev: true
+
+ /@babel/plugin-transform-literals@7.23.3(@babel/core@7.23.5):
+ resolution: {integrity: sha512-wZ0PIXRxnwZvl9AYpqNUxpZ5BiTGrYt7kueGQ+N5FiQ7RCOD4cm8iShd6S6ggfVIWaJf2EMk8eRzAh52RfP4rQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.23.5
+ '@babel/helper-plugin-utils': 7.22.5
+ dev: true
+
+ /@babel/plugin-transform-logical-assignment-operators@7.23.4(@babel/core@7.18.9):
+ resolution: {integrity: sha512-Mc/ALf1rmZTP4JKKEhUwiORU+vcfarFVLfcFiolKUo6sewoxSEgl36ak5t+4WamRsNr6nzjZXQjM35WsU+9vbg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.18.9
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.18.9)
dev: true
- /@babel/plugin-transform-member-expression-literals/7.18.6_@babel+core@7.19.6:
+ /@babel/plugin-transform-logical-assignment-operators@7.23.4(@babel/core@7.23.5):
+ resolution: {integrity: sha512-Mc/ALf1rmZTP4JKKEhUwiORU+vcfarFVLfcFiolKUo6sewoxSEgl36ak5t+4WamRsNr6nzjZXQjM35WsU+9vbg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.23.5
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.23.5)
+ dev: true
+
+ /@babel/plugin-transform-member-expression-literals@7.18.6(@babel/core@7.22.1):
resolution: {integrity: sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-transform-modules-amd/7.19.6_@babel+core@7.18.9:
- resolution: {integrity: sha512-uG3od2mXvAtIFQIh0xrpLH6r5fpSQN04gIVovl+ODLdUMANokxQLZnPBHcjmv3GxRjnqwLuHvppjjcelqUFZvg==}
+ /@babel/plugin-transform-member-expression-literals@7.23.3(@babel/core@7.18.9):
+ resolution: {integrity: sha512-sC3LdDBDi5x96LA+Ytekz2ZPk8i/Ck+DEuDbRAll5rknJ5XRTSaPKEYwomLcs1AA8wg9b3KjIQRsnApj+q51Ag==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.18.9
- '@babel/helper-module-transforms': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
- transitivePeerDependencies:
- - supports-color
+ '@babel/helper-plugin-utils': 7.22.5
+ dev: true
+
+ /@babel/plugin-transform-member-expression-literals@7.23.3(@babel/core@7.23.5):
+ resolution: {integrity: sha512-sC3LdDBDi5x96LA+Ytekz2ZPk8i/Ck+DEuDbRAll5rknJ5XRTSaPKEYwomLcs1AA8wg9b3KjIQRsnApj+q51Ag==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.23.5
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-transform-modules-amd/7.19.6_@babel+core@7.19.6:
+ /@babel/plugin-transform-modules-amd@7.19.6(@babel/core@7.22.1):
resolution: {integrity: sha512-uG3od2mXvAtIFQIh0xrpLH6r5fpSQN04gIVovl+ODLdUMANokxQLZnPBHcjmv3GxRjnqwLuHvppjjcelqUFZvg==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-module-transforms': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
- transitivePeerDependencies:
- - supports-color
+ '@babel/core': 7.22.1
+ '@babel/helper-module-transforms': 7.23.3(@babel/core@7.22.1)
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-transform-modules-commonjs/7.18.6_@babel+core@7.18.9:
- resolution: {integrity: sha512-Qfv2ZOWikpvmedXQJDSbxNqy7Xr/j2Y8/KfijM0iJyKkBTmWuvCA1yeH1yDM7NJhBW/2aXxeucLj6i80/LAJ/Q==}
+ /@babel/plugin-transform-modules-amd@7.23.3(@babel/core@7.18.9):
+ resolution: {integrity: sha512-vJYQGxeKM4t8hYCKVBlZX/gtIY2I7mRGFNcm85sgXGMTBcoV3QdVtdpbcWEbzbfUIUZKwvgFT82mRvaQIebZzw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.18.9
- '@babel/helper-module-transforms': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
- '@babel/helper-simple-access': 7.19.4
- babel-plugin-dynamic-import-node: 2.3.3
- transitivePeerDependencies:
- - supports-color
+ '@babel/helper-module-transforms': 7.23.3(@babel/core@7.18.9)
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-transform-modules-commonjs/7.19.6_@babel+core@7.18.9:
+ /@babel/plugin-transform-modules-amd@7.23.3(@babel/core@7.23.5):
+ resolution: {integrity: sha512-vJYQGxeKM4t8hYCKVBlZX/gtIY2I7mRGFNcm85sgXGMTBcoV3QdVtdpbcWEbzbfUIUZKwvgFT82mRvaQIebZzw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.23.5
+ '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.5)
+ '@babel/helper-plugin-utils': 7.22.5
+ dev: true
+
+ /@babel/plugin-transform-modules-commonjs@7.19.6(@babel/core@7.18.9):
resolution: {integrity: sha512-8PIa1ym4XRTKuSsOUXqDG0YaOlEuTVvHMe5JCfgBMOtHvJKw/4NGovEGN33viISshG/rZNVrACiBmPQLvWN8xQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.18.9
- '@babel/helper-module-transforms': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
- '@babel/helper-simple-access': 7.19.4
- transitivePeerDependencies:
- - supports-color
+ '@babel/helper-module-transforms': 7.23.3(@babel/core@7.18.9)
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/helper-simple-access': 7.22.5
dev: true
- /@babel/plugin-transform-modules-commonjs/7.19.6_@babel+core@7.19.6:
+ /@babel/plugin-transform-modules-commonjs@7.19.6(@babel/core@7.22.1):
resolution: {integrity: sha512-8PIa1ym4XRTKuSsOUXqDG0YaOlEuTVvHMe5JCfgBMOtHvJKw/4NGovEGN33viISshG/rZNVrACiBmPQLvWN8xQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-module-transforms': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
- '@babel/helper-simple-access': 7.19.4
- transitivePeerDependencies:
- - supports-color
+ '@babel/core': 7.22.1
+ '@babel/helper-module-transforms': 7.23.3(@babel/core@7.22.1)
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/helper-simple-access': 7.22.5
dev: true
- /@babel/plugin-transform-modules-systemjs/7.19.6_@babel+core@7.18.9:
- resolution: {integrity: sha512-fqGLBepcc3kErfR9R3DnVpURmckXP7gj7bAlrTQyBxrigFqszZCkFkcoxzCp2v32XmwXLvbw+8Yq9/b+QqksjQ==}
+ /@babel/plugin-transform-modules-commonjs@7.21.5(@babel/core@7.18.9):
+ resolution: {integrity: sha512-OVryBEgKUbtqMoB7eG2rs6UFexJi6Zj6FDXx+esBLPTCxCNxAY9o+8Di7IsUGJ+AVhp5ncK0fxWUBd0/1gPhrQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.18.9
- '@babel/helper-hoist-variables': 7.18.6
- '@babel/helper-module-transforms': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
- '@babel/helper-validator-identifier': 7.19.1
- transitivePeerDependencies:
- - supports-color
+ '@babel/helper-module-transforms': 7.23.3(@babel/core@7.18.9)
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/helper-simple-access': 7.22.5
dev: true
- /@babel/plugin-transform-modules-systemjs/7.19.6_@babel+core@7.19.6:
+ /@babel/plugin-transform-modules-commonjs@7.23.3(@babel/core@7.18.9):
+ resolution: {integrity: sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.18.9
+ '@babel/helper-module-transforms': 7.23.3(@babel/core@7.18.9)
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/helper-simple-access': 7.22.5
+ dev: true
+
+ /@babel/plugin-transform-modules-commonjs@7.23.3(@babel/core@7.23.5):
+ resolution: {integrity: sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.23.5
+ '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.5)
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/helper-simple-access': 7.22.5
+ dev: true
+
+ /@babel/plugin-transform-modules-systemjs@7.19.6(@babel/core@7.22.1):
resolution: {integrity: sha512-fqGLBepcc3kErfR9R3DnVpURmckXP7gj7bAlrTQyBxrigFqszZCkFkcoxzCp2v32XmwXLvbw+8Yq9/b+QqksjQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-hoist-variables': 7.18.6
- '@babel/helper-module-transforms': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
- '@babel/helper-validator-identifier': 7.19.1
- transitivePeerDependencies:
- - supports-color
+ '@babel/core': 7.22.1
+ '@babel/helper-hoist-variables': 7.22.5
+ '@babel/helper-module-transforms': 7.23.3(@babel/core@7.22.1)
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/helper-validator-identifier': 7.22.20
dev: true
- /@babel/plugin-transform-modules-umd/7.18.6_@babel+core@7.18.9:
- resolution: {integrity: sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==}
+ /@babel/plugin-transform-modules-systemjs@7.23.3(@babel/core@7.18.9):
+ resolution: {integrity: sha512-ZxyKGTkF9xT9YJuKQRo19ewf3pXpopuYQd8cDXqNzc3mUNbOME0RKMoZxviQk74hwzfQsEe66dE92MaZbdHKNQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.18.9
- '@babel/helper-module-transforms': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
- transitivePeerDependencies:
- - supports-color
+ '@babel/helper-hoist-variables': 7.22.5
+ '@babel/helper-module-transforms': 7.23.3(@babel/core@7.18.9)
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/helper-validator-identifier': 7.22.20
+ dev: true
+
+ /@babel/plugin-transform-modules-systemjs@7.23.3(@babel/core@7.23.5):
+ resolution: {integrity: sha512-ZxyKGTkF9xT9YJuKQRo19ewf3pXpopuYQd8cDXqNzc3mUNbOME0RKMoZxviQk74hwzfQsEe66dE92MaZbdHKNQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.23.5
+ '@babel/helper-hoist-variables': 7.22.5
+ '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.5)
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/helper-validator-identifier': 7.22.20
dev: true
- /@babel/plugin-transform-modules-umd/7.18.6_@babel+core@7.19.6:
+ /@babel/plugin-transform-modules-umd@7.18.6(@babel/core@7.22.1):
resolution: {integrity: sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-module-transforms': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
- transitivePeerDependencies:
- - supports-color
+ '@babel/core': 7.22.1
+ '@babel/helper-module-transforms': 7.23.3(@babel/core@7.22.1)
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-transform-named-capturing-groups-regex/7.19.1_@babel+core@7.18.9:
+ /@babel/plugin-transform-modules-umd@7.23.3(@babel/core@7.18.9):
+ resolution: {integrity: sha512-zHsy9iXX2nIsCBFPud3jKn1IRPWg3Ing1qOZgeKV39m1ZgIdpJqvlWVeiHBZC6ITRG0MfskhYe9cLgntfSFPIg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.18.9
+ '@babel/helper-module-transforms': 7.23.3(@babel/core@7.18.9)
+ '@babel/helper-plugin-utils': 7.22.5
+ dev: true
+
+ /@babel/plugin-transform-modules-umd@7.23.3(@babel/core@7.23.5):
+ resolution: {integrity: sha512-zHsy9iXX2nIsCBFPud3jKn1IRPWg3Ing1qOZgeKV39m1ZgIdpJqvlWVeiHBZC6ITRG0MfskhYe9cLgntfSFPIg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.23.5
+ '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.5)
+ '@babel/helper-plugin-utils': 7.22.5
+ dev: true
+
+ /@babel/plugin-transform-named-capturing-groups-regex@7.19.1(@babel/core@7.22.1):
resolution: {integrity: sha512-oWk9l9WItWBQYS4FgXD4Uyy5kq898lvkXpXQxoJEY1RnvPk4R/Dvu2ebXU9q8lP+rlMwUQTFf2Ok6d78ODa0kw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0
dependencies:
+ '@babel/core': 7.22.1
+ '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.22.1)
+ '@babel/helper-plugin-utils': 7.22.5
+ dev: true
+
+ /@babel/plugin-transform-named-capturing-groups-regex@7.22.5(@babel/core@7.18.9):
+ resolution: {integrity: sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+ dependencies:
'@babel/core': 7.18.9
- '@babel/helper-create-regexp-features-plugin': 7.19.0_@babel+core@7.18.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.18.9)
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-transform-named-capturing-groups-regex/7.19.1_@babel+core@7.19.6:
- resolution: {integrity: sha512-oWk9l9WItWBQYS4FgXD4Uyy5kq898lvkXpXQxoJEY1RnvPk4R/Dvu2ebXU9q8lP+rlMwUQTFf2Ok6d78ODa0kw==}
+ /@babel/plugin-transform-named-capturing-groups-regex@7.22.5(@babel/core@7.23.5):
+ resolution: {integrity: sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-create-regexp-features-plugin': 7.19.0_@babel+core@7.19.6
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.23.5
+ '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.5)
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-transform-new-target/7.18.6_@babel+core@7.18.9:
+ /@babel/plugin-transform-new-target@7.18.6(@babel/core@7.22.1):
resolution: {integrity: sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.22.5
+ dev: true
+
+ /@babel/plugin-transform-new-target@7.23.3(@babel/core@7.18.9):
+ resolution: {integrity: sha512-YJ3xKqtJMAT5/TIZnpAR3I+K+WaDowYbN3xyxI8zxx/Gsypwf9B9h0VB+1Nh6ACAAPRS5NSRje0uVv5i79HYGQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
'@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-transform-new-target/7.18.6_@babel+core@7.19.6:
- resolution: {integrity: sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==}
+ /@babel/plugin-transform-new-target@7.23.3(@babel/core@7.23.5):
+ resolution: {integrity: sha512-YJ3xKqtJMAT5/TIZnpAR3I+K+WaDowYbN3xyxI8zxx/Gsypwf9B9h0VB+1Nh6ACAAPRS5NSRje0uVv5i79HYGQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.23.5
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-transform-object-assign/7.18.6_@babel+core@7.18.9:
+ /@babel/plugin-transform-nullish-coalescing-operator@7.23.4(@babel/core@7.18.9):
+ resolution: {integrity: sha512-jHE9EVVqHKAQx+VePv5LLGHjmHSJR76vawFPTdlxR/LVJPfOEGxREQwQfjuZEOPTwG92X3LINSh3M40Rv4zpVA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.18.9
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.18.9)
+ dev: true
+
+ /@babel/plugin-transform-nullish-coalescing-operator@7.23.4(@babel/core@7.23.5):
+ resolution: {integrity: sha512-jHE9EVVqHKAQx+VePv5LLGHjmHSJR76vawFPTdlxR/LVJPfOEGxREQwQfjuZEOPTwG92X3LINSh3M40Rv4zpVA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.23.5
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.23.5)
+ dev: true
+
+ /@babel/plugin-transform-numeric-separator@7.23.4(@babel/core@7.18.9):
+ resolution: {integrity: sha512-mps6auzgwjRrwKEZA05cOwuDc9FAzoyFS4ZsG/8F43bTLf/TgkJg7QXOrPO1JO599iA3qgK9MXdMGOEC8O1h6Q==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.18.9
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.18.9)
+ dev: true
+
+ /@babel/plugin-transform-numeric-separator@7.23.4(@babel/core@7.23.5):
+ resolution: {integrity: sha512-mps6auzgwjRrwKEZA05cOwuDc9FAzoyFS4ZsG/8F43bTLf/TgkJg7QXOrPO1JO599iA3qgK9MXdMGOEC8O1h6Q==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.23.5
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.23.5)
+ dev: true
+
+ /@babel/plugin-transform-object-assign@7.18.6(@babel/core@7.22.1):
resolution: {integrity: sha512-mQisZ3JfqWh2gVXvfqYCAAyRs6+7oev+myBsTwW5RnPhYXOTuCEw2oe3YgxlXMViXUS53lG8koulI7mJ+8JE+A==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.22.5
+ dev: true
+
+ /@babel/plugin-transform-object-rest-spread@7.23.4(@babel/core@7.18.9):
+ resolution: {integrity: sha512-9x9K1YyeQVw0iOXJlIzwm8ltobIIv7j2iLyP2jIhEbqPRQ7ScNgwQufU2I0Gq11VjyG4gI4yMXt2VFags+1N3g==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/compat-data': 7.23.5
'@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/helper-compilation-targets': 7.22.15
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.18.9)
+ '@babel/plugin-transform-parameters': 7.23.3(@babel/core@7.18.9)
+ dev: true
+
+ /@babel/plugin-transform-object-rest-spread@7.23.4(@babel/core@7.23.5):
+ resolution: {integrity: sha512-9x9K1YyeQVw0iOXJlIzwm8ltobIIv7j2iLyP2jIhEbqPRQ7ScNgwQufU2I0Gq11VjyG4gI4yMXt2VFags+1N3g==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/compat-data': 7.23.5
+ '@babel/core': 7.23.5
+ '@babel/helper-compilation-targets': 7.22.15
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.23.5)
+ '@babel/plugin-transform-parameters': 7.23.3(@babel/core@7.23.5)
dev: true
- /@babel/plugin-transform-object-super/7.18.6_@babel+core@7.18.9:
+ /@babel/plugin-transform-object-super@7.18.6(@babel/core@7.22.1):
resolution: {integrity: sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/helper-replace-supers': 7.22.20(@babel/core@7.22.1)
+ dev: true
+
+ /@babel/plugin-transform-object-super@7.23.3(@babel/core@7.18.9):
+ resolution: {integrity: sha512-BwQ8q0x2JG+3lxCVFohg+KbQM7plfpBwThdW9A6TMtWwLsbDA01Ek2Zb/AgDN39BiZsExm4qrXxjk+P1/fzGrA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
'@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
- '@babel/helper-replace-supers': 7.19.1
- transitivePeerDependencies:
- - supports-color
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/helper-replace-supers': 7.22.20(@babel/core@7.18.9)
dev: true
- /@babel/plugin-transform-object-super/7.18.6_@babel+core@7.19.6:
- resolution: {integrity: sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==}
+ /@babel/plugin-transform-object-super@7.23.3(@babel/core@7.23.5):
+ resolution: {integrity: sha512-BwQ8q0x2JG+3lxCVFohg+KbQM7plfpBwThdW9A6TMtWwLsbDA01Ek2Zb/AgDN39BiZsExm4qrXxjk+P1/fzGrA==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
- '@babel/helper-replace-supers': 7.19.1
- transitivePeerDependencies:
- - supports-color
+ '@babel/core': 7.23.5
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/helper-replace-supers': 7.22.20(@babel/core@7.23.5)
dev: true
- /@babel/plugin-transform-parameters/7.18.8_@babel+core@7.12.9:
- resolution: {integrity: sha512-ivfbE3X2Ss+Fj8nnXvKJS6sjRG4gzwPMsP+taZC+ZzEGjAYlvENixmt1sZ5Ca6tWls+BlKSGKPJ6OOXvXCbkFg==}
+ /@babel/plugin-transform-optional-catch-binding@7.23.4(@babel/core@7.18.9):
+ resolution: {integrity: sha512-XIq8t0rJPHf6Wvmbn9nFxU6ao4c7WhghTR5WyV8SrJfUFzyxhCm4nhC+iAp3HFhbAKLfYpgzhJ6t4XCtVwqO5A==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.12.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.18.9
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.18.9)
dev: true
- /@babel/plugin-transform-parameters/7.18.8_@babel+core@7.18.9:
- resolution: {integrity: sha512-ivfbE3X2Ss+Fj8nnXvKJS6sjRG4gzwPMsP+taZC+ZzEGjAYlvENixmt1sZ5Ca6tWls+BlKSGKPJ6OOXvXCbkFg==}
+ /@babel/plugin-transform-optional-catch-binding@7.23.4(@babel/core@7.23.5):
+ resolution: {integrity: sha512-XIq8t0rJPHf6Wvmbn9nFxU6ao4c7WhghTR5WyV8SrJfUFzyxhCm4nhC+iAp3HFhbAKLfYpgzhJ6t4XCtVwqO5A==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.23.5
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.23.5)
+ dev: true
+
+ /@babel/plugin-transform-optional-chaining@7.23.4(@babel/core@7.18.9):
+ resolution: {integrity: sha512-ZU8y5zWOfjM5vZ+asjgAPwDaBjJzgufjES89Rs4Lpq63O300R/kOz30WCLo6BxxX6QVEilwSlpClnG5cZaikTA==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/helper-skip-transparent-expression-wrappers': 7.22.5
+ '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.18.9)
dev: true
- /@babel/plugin-transform-parameters/7.18.8_@babel+core@7.19.6:
+ /@babel/plugin-transform-optional-chaining@7.23.4(@babel/core@7.23.5):
+ resolution: {integrity: sha512-ZU8y5zWOfjM5vZ+asjgAPwDaBjJzgufjES89Rs4Lpq63O300R/kOz30WCLo6BxxX6QVEilwSlpClnG5cZaikTA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.23.5
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/helper-skip-transparent-expression-wrappers': 7.22.5
+ '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.23.5)
+ dev: true
+
+ /@babel/plugin-transform-parameters@7.18.8(@babel/core@7.22.1):
resolution: {integrity: sha512-ivfbE3X2Ss+Fj8nnXvKJS6sjRG4gzwPMsP+taZC+ZzEGjAYlvENixmt1sZ5Ca6tWls+BlKSGKPJ6OOXvXCbkFg==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-transform-property-literals/7.18.6_@babel+core@7.18.9:
- resolution: {integrity: sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==}
+ /@babel/plugin-transform-parameters@7.23.3(@babel/core@7.18.9):
+ resolution: {integrity: sha512-09lMt6UsUb3/34BbECKVbVwrT9bO6lILWln237z7sLaWnMsTi7Yc9fhX5DLpkJzAGfaReXI22wP41SZmnAA3Vw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/helper-plugin-utils': 7.22.5
+ dev: true
+
+ /@babel/plugin-transform-parameters@7.23.3(@babel/core@7.22.1):
+ resolution: {integrity: sha512-09lMt6UsUb3/34BbECKVbVwrT9bO6lILWln237z7sLaWnMsTi7Yc9fhX5DLpkJzAGfaReXI22wP41SZmnAA3Vw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.22.5
+ dev: true
+
+ /@babel/plugin-transform-parameters@7.23.3(@babel/core@7.23.5):
+ resolution: {integrity: sha512-09lMt6UsUb3/34BbECKVbVwrT9bO6lILWln237z7sLaWnMsTi7Yc9fhX5DLpkJzAGfaReXI22wP41SZmnAA3Vw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.23.5
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-transform-property-literals/7.18.6_@babel+core@7.19.6:
+ /@babel/plugin-transform-private-methods@7.23.3(@babel/core@7.18.9):
+ resolution: {integrity: sha512-UzqRcRtWsDMTLrRWFvUBDwmw06tCQH9Rl1uAjfh6ijMSmGYQ+fpdB+cnqRC8EMh5tuuxSv0/TejGL+7vyj+50g==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.18.9
+ '@babel/helper-create-class-features-plugin': 7.23.5(@babel/core@7.18.9)
+ '@babel/helper-plugin-utils': 7.22.5
+ dev: true
+
+ /@babel/plugin-transform-private-methods@7.23.3(@babel/core@7.23.5):
+ resolution: {integrity: sha512-UzqRcRtWsDMTLrRWFvUBDwmw06tCQH9Rl1uAjfh6ijMSmGYQ+fpdB+cnqRC8EMh5tuuxSv0/TejGL+7vyj+50g==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.23.5
+ '@babel/helper-create-class-features-plugin': 7.23.5(@babel/core@7.23.5)
+ '@babel/helper-plugin-utils': 7.22.5
+ dev: true
+
+ /@babel/plugin-transform-private-property-in-object@7.23.4(@babel/core@7.18.9):
+ resolution: {integrity: sha512-9G3K1YqTq3F4Vt88Djx1UZ79PDyj+yKRnUy7cZGSMe+a7jkwD259uKKuUzQlPkGam7R+8RJwh5z4xO27fA1o2A==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.18.9
+ '@babel/helper-annotate-as-pure': 7.22.5
+ '@babel/helper-create-class-features-plugin': 7.23.5(@babel/core@7.18.9)
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.18.9)
+ dev: true
+
+ /@babel/plugin-transform-private-property-in-object@7.23.4(@babel/core@7.23.5):
+ resolution: {integrity: sha512-9G3K1YqTq3F4Vt88Djx1UZ79PDyj+yKRnUy7cZGSMe+a7jkwD259uKKuUzQlPkGam7R+8RJwh5z4xO27fA1o2A==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.23.5
+ '@babel/helper-annotate-as-pure': 7.22.5
+ '@babel/helper-create-class-features-plugin': 7.23.5(@babel/core@7.23.5)
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.23.5)
+ dev: true
+
+ /@babel/plugin-transform-property-literals@7.18.6(@babel/core@7.22.1):
resolution: {integrity: sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-transform-react-display-name/7.18.6_@babel+core@7.18.9:
- resolution: {integrity: sha512-TV4sQ+T013n61uMoygyMRm+xf04Bd5oqFpv2jAEQwSZ8NwQA7zeRPg1LMVg2PWi3zWBz+CLKD+v5bcpZ/BS0aA==}
+ /@babel/plugin-transform-property-literals@7.23.3(@babel/core@7.18.9):
+ resolution: {integrity: sha512-jR3Jn3y7cZp4oEWPFAlRsSWjxKe4PZILGBSd4nis1TsC5qeSpb+nrtihJuDhNI7QHiVbUaiXa0X2RZY3/TI6Nw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-transform-react-jsx-development/7.18.6_@babel+core@7.18.9:
- resolution: {integrity: sha512-SA6HEjwYFKF7WDjWcMcMGUimmw/nhNRDWxr+KaLSCrkD/LMDBvWRmHAYgE1HDeF8KUuI8OAu+RT6EOtKxSW2qA==}
+ /@babel/plugin-transform-property-literals@7.23.3(@babel/core@7.23.5):
+ resolution: {integrity: sha512-jR3Jn3y7cZp4oEWPFAlRsSWjxKe4PZILGBSd4nis1TsC5qeSpb+nrtihJuDhNI7QHiVbUaiXa0X2RZY3/TI6Nw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.23.5
+ '@babel/helper-plugin-utils': 7.22.5
+ dev: true
+
+ /@babel/plugin-transform-react-display-name@7.18.6(@babel/core@7.18.9):
+ resolution: {integrity: sha512-TV4sQ+T013n61uMoygyMRm+xf04Bd5oqFpv2jAEQwSZ8NwQA7zeRPg1LMVg2PWi3zWBz+CLKD+v5bcpZ/BS0aA==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.18.9
- '@babel/plugin-transform-react-jsx': 7.19.0_@babel+core@7.18.9
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-transform-react-jsx-source/7.18.6_@babel+core@7.18.9:
- resolution: {integrity: sha512-utZmlASneDfdaMh0m/WausbjUjEdGrQJz0vFK93d7wD3xf5wBtX219+q6IlCNZeguIcxS2f/CvLZrlLSvSHQXw==}
+ /@babel/plugin-transform-react-jsx-development@7.18.6(@babel/core@7.18.9):
+ resolution: {integrity: sha512-SA6HEjwYFKF7WDjWcMcMGUimmw/nhNRDWxr+KaLSCrkD/LMDBvWRmHAYgE1HDeF8KUuI8OAu+RT6EOtKxSW2qA==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/plugin-transform-react-jsx': 7.22.3(@babel/core@7.18.9)
dev: true
- /@babel/plugin-transform-react-jsx/7.19.0_@babel+core@7.18.9:
+ /@babel/plugin-transform-react-jsx@7.19.0(@babel/core@7.22.1):
resolution: {integrity: sha512-UVEvX3tXie3Szm3emi1+G63jyw1w5IcMY0FSKM+CRnKRI5Mr1YbCNgsSTwoTwKphQEG9P+QqmuRFneJPZuHNhg==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
+ '@babel/core': 7.22.1
+ '@babel/helper-annotate-as-pure': 7.22.5
+ '@babel/helper-module-imports': 7.22.15
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/plugin-syntax-jsx': 7.21.4(@babel/core@7.22.1)
+ '@babel/types': 7.23.5
+ dev: true
+
+ /@babel/plugin-transform-react-jsx@7.22.3(@babel/core@7.18.9):
+ resolution: {integrity: sha512-JEulRWG2f04a7L8VWaOngWiK6p+JOSpB+DAtwfJgOaej1qdbNxqtK7MwTBHjUA10NeFcszlFNqCdbRcirzh2uQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
'@babel/core': 7.18.9
- '@babel/helper-annotate-as-pure': 7.18.6
- '@babel/helper-module-imports': 7.18.6
- '@babel/helper-plugin-utils': 7.19.0
- '@babel/plugin-syntax-jsx': 7.18.6_@babel+core@7.18.9
- '@babel/types': 7.19.4
+ '@babel/helper-annotate-as-pure': 7.22.5
+ '@babel/helper-module-imports': 7.22.15
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/plugin-syntax-jsx': 7.21.4(@babel/core@7.18.9)
+ '@babel/types': 7.23.5
dev: true
- /@babel/plugin-transform-react-pure-annotations/7.18.6_@babel+core@7.18.9:
+ /@babel/plugin-transform-react-pure-annotations@7.18.6(@babel/core@7.18.9):
resolution: {integrity: sha512-I8VfEPg9r2TRDdvnHgPepTKvuRomzA8+u+nhY7qSI1fR2hRNebasZEETLyM5mAUr0Ku56OkXJ0I7NHJnO6cJiQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.18.9
- '@babel/helper-annotate-as-pure': 7.18.6
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/helper-annotate-as-pure': 7.22.5
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-transform-regenerator/7.18.6_@babel+core@7.18.9:
+ /@babel/plugin-transform-regenerator@7.18.6(@babel/core@7.22.1):
resolution: {integrity: sha512-poqRI2+qiSdeldcz4wTSTXBRryoq3Gc70ye7m7UD5Ww0nE29IXqMl6r7Nd15WBgRd74vloEMlShtH6CKxVzfmQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.22.5
+ regenerator-transform: 0.15.2
+ dev: true
+
+ /@babel/plugin-transform-regenerator@7.23.3(@babel/core@7.18.9):
+ resolution: {integrity: sha512-KP+75h0KghBMcVpuKisx3XTu9Ncut8Q8TuvGO4IhY+9D5DFEckQefOuIsB/gQ2tG71lCke4NMrtIPS8pOj18BQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
'@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
- regenerator-transform: 0.15.0
+ '@babel/helper-plugin-utils': 7.22.5
+ regenerator-transform: 0.15.2
dev: true
- /@babel/plugin-transform-regenerator/7.18.6_@babel+core@7.19.6:
- resolution: {integrity: sha512-poqRI2+qiSdeldcz4wTSTXBRryoq3Gc70ye7m7UD5Ww0nE29IXqMl6r7Nd15WBgRd74vloEMlShtH6CKxVzfmQ==}
+ /@babel/plugin-transform-regenerator@7.23.3(@babel/core@7.23.5):
+ resolution: {integrity: sha512-KP+75h0KghBMcVpuKisx3XTu9Ncut8Q8TuvGO4IhY+9D5DFEckQefOuIsB/gQ2tG71lCke4NMrtIPS8pOj18BQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
- regenerator-transform: 0.15.0
+ '@babel/core': 7.23.5
+ '@babel/helper-plugin-utils': 7.22.5
+ regenerator-transform: 0.15.2
dev: true
- /@babel/plugin-transform-reserved-words/7.18.6_@babel+core@7.18.9:
+ /@babel/plugin-transform-reserved-words@7.18.6(@babel/core@7.22.1):
resolution: {integrity: sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.22.5
+ dev: true
+
+ /@babel/plugin-transform-reserved-words@7.23.3(@babel/core@7.18.9):
+ resolution: {integrity: sha512-QnNTazY54YqgGxwIexMZva9gqbPa15t/x9VS+0fsEFWplwVpXYZivtgl43Z1vMpc1bdPP2PP8siFeVcnFvA3Cg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
'@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-transform-reserved-words/7.18.6_@babel+core@7.19.6:
- resolution: {integrity: sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==}
+ /@babel/plugin-transform-reserved-words@7.23.3(@babel/core@7.23.5):
+ resolution: {integrity: sha512-QnNTazY54YqgGxwIexMZva9gqbPa15t/x9VS+0fsEFWplwVpXYZivtgl43Z1vMpc1bdPP2PP8siFeVcnFvA3Cg==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.23.5
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-transform-runtime/7.19.6_@babel+core@7.18.9:
+ /@babel/plugin-transform-runtime@7.19.6(@babel/core@7.22.1):
resolution: {integrity: sha512-PRH37lz4JU156lYFW1p8OxE5i7d6Sl/zV58ooyr+q1J1lnQPyg5tIiXlIwNVhJaY4W3TmOtdc8jqdXQcB1v5Yw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.18.9
- '@babel/helper-module-imports': 7.18.6
- '@babel/helper-plugin-utils': 7.19.0
- babel-plugin-polyfill-corejs2: 0.3.3_@babel+core@7.18.9
- babel-plugin-polyfill-corejs3: 0.6.0_@babel+core@7.18.9
- babel-plugin-polyfill-regenerator: 0.4.1_@babel+core@7.18.9
+ '@babel/core': 7.22.1
+ '@babel/helper-module-imports': 7.21.4
+ '@babel/helper-plugin-utils': 7.21.5
+ babel-plugin-polyfill-corejs2: 0.3.3(@babel/core@7.22.1)
+ babel-plugin-polyfill-corejs3: 0.6.0(@babel/core@7.22.1)
+ babel-plugin-polyfill-regenerator: 0.4.1(@babel/core@7.22.1)
semver: 6.3.0
transitivePeerDependencies:
- supports-color
dev: true
- /@babel/plugin-transform-runtime/7.19.6_@babel+core@7.19.6:
- resolution: {integrity: sha512-PRH37lz4JU156lYFW1p8OxE5i7d6Sl/zV58ooyr+q1J1lnQPyg5tIiXlIwNVhJaY4W3TmOtdc8jqdXQcB1v5Yw==}
+ /@babel/plugin-transform-runtime@7.23.4(@babel/core@7.18.9):
+ resolution: {integrity: sha512-ITwqpb6V4btwUG0YJR82o2QvmWrLgDnx/p2A3CTPYGaRgULkDiC0DRA2C4jlRB9uXGUEfaSS/IGHfVW+ohzYDw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-module-imports': 7.18.6
- '@babel/helper-plugin-utils': 7.19.0
- babel-plugin-polyfill-corejs2: 0.3.3_@babel+core@7.19.6
- babel-plugin-polyfill-corejs3: 0.6.0_@babel+core@7.19.6
- babel-plugin-polyfill-regenerator: 0.4.1_@babel+core@7.19.6
- semver: 6.3.0
+ '@babel/core': 7.18.9
+ '@babel/helper-module-imports': 7.22.15
+ '@babel/helper-plugin-utils': 7.22.5
+ babel-plugin-polyfill-corejs2: 0.4.6(@babel/core@7.18.9)
+ babel-plugin-polyfill-corejs3: 0.8.6(@babel/core@7.18.9)
+ babel-plugin-polyfill-regenerator: 0.5.3(@babel/core@7.18.9)
+ semver: 6.3.1
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@babel/plugin-transform-runtime@7.23.4(@babel/core@7.23.5):
+ resolution: {integrity: sha512-ITwqpb6V4btwUG0YJR82o2QvmWrLgDnx/p2A3CTPYGaRgULkDiC0DRA2C4jlRB9uXGUEfaSS/IGHfVW+ohzYDw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.23.5
+ '@babel/helper-module-imports': 7.22.15
+ '@babel/helper-plugin-utils': 7.22.5
+ babel-plugin-polyfill-corejs2: 0.4.6(@babel/core@7.23.5)
+ babel-plugin-polyfill-corejs3: 0.8.6(@babel/core@7.23.5)
+ babel-plugin-polyfill-regenerator: 0.5.3(@babel/core@7.23.5)
+ semver: 6.3.1
transitivePeerDependencies:
- supports-color
dev: true
- /@babel/plugin-transform-shorthand-properties/7.18.6_@babel+core@7.18.9:
+ /@babel/plugin-transform-shorthand-properties@7.18.6(@babel/core@7.22.1):
resolution: {integrity: sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.22.5
+ dev: true
+
+ /@babel/plugin-transform-shorthand-properties@7.23.3(@babel/core@7.18.9):
+ resolution: {integrity: sha512-ED2fgqZLmexWiN+YNFX26fx4gh5qHDhn1O2gvEhreLW2iI63Sqm4llRLCXALKrCnbN4Jy0VcMQZl/SAzqug/jg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
'@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-transform-shorthand-properties/7.18.6_@babel+core@7.19.6:
- resolution: {integrity: sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==}
+ /@babel/plugin-transform-shorthand-properties@7.23.3(@babel/core@7.23.5):
+ resolution: {integrity: sha512-ED2fgqZLmexWiN+YNFX26fx4gh5qHDhn1O2gvEhreLW2iI63Sqm4llRLCXALKrCnbN4Jy0VcMQZl/SAzqug/jg==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.23.5
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-transform-spread/7.19.0_@babel+core@7.18.9:
+ /@babel/plugin-transform-spread@7.19.0(@babel/core@7.22.1):
resolution: {integrity: sha512-RsuMk7j6n+r752EtzyScnWkQyuJdli6LdO5Klv8Yx0OfPVTcQkIUfS8clx5e9yHXzlnhOZF3CbQ8C2uP5j074w==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/helper-skip-transparent-expression-wrappers': 7.22.5
+ dev: true
+
+ /@babel/plugin-transform-spread@7.23.3(@babel/core@7.18.9):
+ resolution: {integrity: sha512-VvfVYlrlBVu+77xVTOAoxQ6mZbnIq5FM0aGBSFEcIh03qHf+zNqA4DC/3XMUozTg7bZV3e3mZQ0i13VB6v5yUg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
'@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
- '@babel/helper-skip-transparent-expression-wrappers': 7.18.9
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/helper-skip-transparent-expression-wrappers': 7.22.5
dev: true
- /@babel/plugin-transform-spread/7.19.0_@babel+core@7.19.6:
- resolution: {integrity: sha512-RsuMk7j6n+r752EtzyScnWkQyuJdli6LdO5Klv8Yx0OfPVTcQkIUfS8clx5e9yHXzlnhOZF3CbQ8C2uP5j074w==}
+ /@babel/plugin-transform-spread@7.23.3(@babel/core@7.23.5):
+ resolution: {integrity: sha512-VvfVYlrlBVu+77xVTOAoxQ6mZbnIq5FM0aGBSFEcIh03qHf+zNqA4DC/3XMUozTg7bZV3e3mZQ0i13VB6v5yUg==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
- '@babel/helper-skip-transparent-expression-wrappers': 7.18.9
+ '@babel/core': 7.23.5
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/helper-skip-transparent-expression-wrappers': 7.22.5
dev: true
- /@babel/plugin-transform-sticky-regex/7.18.6_@babel+core@7.18.9:
+ /@babel/plugin-transform-sticky-regex@7.18.6(@babel/core@7.22.1):
resolution: {integrity: sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.22.5
+ dev: true
+
+ /@babel/plugin-transform-sticky-regex@7.23.3(@babel/core@7.18.9):
+ resolution: {integrity: sha512-HZOyN9g+rtvnOU3Yh7kSxXrKbzgrm5X4GncPY1QOquu7epga5MxKHVpYu2hvQnry/H+JjckSYRb93iNfsioAGg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
'@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-transform-sticky-regex/7.18.6_@babel+core@7.19.6:
- resolution: {integrity: sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==}
+ /@babel/plugin-transform-sticky-regex@7.23.3(@babel/core@7.23.5):
+ resolution: {integrity: sha512-HZOyN9g+rtvnOU3Yh7kSxXrKbzgrm5X4GncPY1QOquu7epga5MxKHVpYu2hvQnry/H+JjckSYRb93iNfsioAGg==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.23.5
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-transform-template-literals/7.18.9_@babel+core@7.18.9:
+ /@babel/plugin-transform-template-literals@7.18.9(@babel/core@7.22.1):
resolution: {integrity: sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.21.5
+ dev: true
+
+ /@babel/plugin-transform-template-literals@7.23.3(@babel/core@7.18.9):
+ resolution: {integrity: sha512-Flok06AYNp7GV2oJPZZcP9vZdszev6vPBkHLwxwSpaIqx75wn6mUd3UFWsSsA0l8nXAKkyCmL/sR02m8RYGeHg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
'@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-transform-template-literals/7.18.9_@babel+core@7.19.6:
- resolution: {integrity: sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==}
+ /@babel/plugin-transform-template-literals@7.23.3(@babel/core@7.23.5):
+ resolution: {integrity: sha512-Flok06AYNp7GV2oJPZZcP9vZdszev6vPBkHLwxwSpaIqx75wn6mUd3UFWsSsA0l8nXAKkyCmL/sR02m8RYGeHg==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.23.5
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-transform-typeof-symbol/7.18.9_@babel+core@7.18.9:
+ /@babel/plugin-transform-typeof-symbol@7.18.9(@babel/core@7.22.1):
resolution: {integrity: sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.22.5
+ dev: true
+
+ /@babel/plugin-transform-typeof-symbol@7.23.3(@babel/core@7.18.9):
+ resolution: {integrity: sha512-4t15ViVnaFdrPC74be1gXBSMzXk3B4Us9lP7uLRQHTFpV5Dvt33pn+2MyyNxmN3VTTm3oTrZVMUmuw3oBnQ2oQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
'@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-transform-typeof-symbol/7.18.9_@babel+core@7.19.6:
- resolution: {integrity: sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==}
+ /@babel/plugin-transform-typeof-symbol@7.23.3(@babel/core@7.23.5):
+ resolution: {integrity: sha512-4t15ViVnaFdrPC74be1gXBSMzXk3B4Us9lP7uLRQHTFpV5Dvt33pn+2MyyNxmN3VTTm3oTrZVMUmuw3oBnQ2oQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.23.5
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-transform-typescript/7.19.3_@babel+core@7.18.9:
- resolution: {integrity: sha512-z6fnuK9ve9u/0X0rRvI9MY0xg+DOUaABDYOe+/SQTxtlptaBB/V9JIUxJn6xp3lMBeb9qe8xSFmHU35oZDXD+w==}
+ /@babel/plugin-transform-typescript@7.20.13(@babel/core@7.18.9):
+ resolution: {integrity: sha512-O7I/THxarGcDZxkgWKMUrk7NK1/WbHAg3Xx86gqS6x9MTrNL6AwIluuZ96ms4xeDe6AVx6rjHbWHP7x26EPQBA==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.18.9
- '@babel/helper-create-class-features-plugin': 7.19.0_@babel+core@7.18.9
- '@babel/helper-plugin-utils': 7.19.0
- '@babel/plugin-syntax-typescript': 7.18.6_@babel+core@7.18.9
- transitivePeerDependencies:
- - supports-color
+ '@babel/helper-create-class-features-plugin': 7.23.5(@babel/core@7.18.9)
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/plugin-syntax-typescript': 7.20.0(@babel/core@7.18.9)
dev: true
- /@babel/plugin-transform-unicode-escapes/7.18.10_@babel+core@7.18.9:
- resolution: {integrity: sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==}
+ /@babel/plugin-transform-typescript@7.20.13(@babel/core@7.22.1):
+ resolution: {integrity: sha512-O7I/THxarGcDZxkgWKMUrk7NK1/WbHAg3Xx86gqS6x9MTrNL6AwIluuZ96ms4xeDe6AVx6rjHbWHP7x26EPQBA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.22.1
+ '@babel/helper-create-class-features-plugin': 7.23.5(@babel/core@7.22.1)
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/plugin-syntax-typescript': 7.20.0(@babel/core@7.22.1)
+ dev: true
+
+ /@babel/plugin-transform-typescript@7.22.3(@babel/core@7.18.9):
+ resolution: {integrity: sha512-pyjnCIniO5PNaEuGxT28h0HbMru3qCVrMqVgVOz/krComdIrY9W6FCLBq9NWHY8HDGaUlan+UhmZElDENIfCcw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/helper-annotate-as-pure': 7.22.5
+ '@babel/helper-create-class-features-plugin': 7.23.5(@babel/core@7.18.9)
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/plugin-syntax-typescript': 7.21.4(@babel/core@7.18.9)
dev: true
- /@babel/plugin-transform-unicode-escapes/7.18.10_@babel+core@7.19.6:
+ /@babel/plugin-transform-unicode-escapes@7.18.10(@babel/core@7.22.1):
resolution: {integrity: sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-transform-unicode-regex/7.18.6_@babel+core@7.18.9:
- resolution: {integrity: sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==}
+ /@babel/plugin-transform-unicode-escapes@7.23.3(@babel/core@7.18.9):
+ resolution: {integrity: sha512-OMCUx/bU6ChE3r4+ZdylEqAjaQgHAgipgW8nsCfu5pGqDcFytVd91AwRvUJSBZDz0exPGgnjoqhgRYLRjFZc9Q==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.18.9
- '@babel/helper-create-regexp-features-plugin': 7.19.0_@babel+core@7.18.9
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/plugin-transform-unicode-regex/7.18.6_@babel+core@7.19.6:
+ /@babel/plugin-transform-unicode-escapes@7.23.3(@babel/core@7.23.5):
+ resolution: {integrity: sha512-OMCUx/bU6ChE3r4+ZdylEqAjaQgHAgipgW8nsCfu5pGqDcFytVd91AwRvUJSBZDz0exPGgnjoqhgRYLRjFZc9Q==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.23.5
+ '@babel/helper-plugin-utils': 7.22.5
+ dev: true
+
+ /@babel/plugin-transform-unicode-property-regex@7.23.3(@babel/core@7.18.9):
+ resolution: {integrity: sha512-KcLIm+pDZkWZQAFJ9pdfmh89EwVfmNovFBcXko8szpBeF8z68kWIPeKlmSOkT9BXJxs2C0uk+5LxoxIv62MROA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.18.9
+ '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.18.9)
+ '@babel/helper-plugin-utils': 7.22.5
+ dev: true
+
+ /@babel/plugin-transform-unicode-property-regex@7.23.3(@babel/core@7.23.5):
+ resolution: {integrity: sha512-KcLIm+pDZkWZQAFJ9pdfmh89EwVfmNovFBcXko8szpBeF8z68kWIPeKlmSOkT9BXJxs2C0uk+5LxoxIv62MROA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.23.5
+ '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.5)
+ '@babel/helper-plugin-utils': 7.22.5
+ dev: true
+
+ /@babel/plugin-transform-unicode-regex@7.18.6(@babel/core@7.22.1):
resolution: {integrity: sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-create-regexp-features-plugin': 7.19.0_@babel+core@7.19.6
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/core': 7.22.1
+ '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.22.1)
+ '@babel/helper-plugin-utils': 7.22.5
dev: true
- /@babel/preset-env/7.18.9_@babel+core@7.18.9:
- resolution: {integrity: sha512-75pt/q95cMIHWssYtyfjVlvI+QEZQThQbKvR9xH+F/Agtw/s4Wfc2V9Bwd/P39VtixB7oWxGdH4GteTTwYJWMg==}
+ /@babel/plugin-transform-unicode-regex@7.23.3(@babel/core@7.18.9):
+ resolution: {integrity: sha512-wMHpNA4x2cIA32b/ci3AfwNgheiva2W0WUKWTK7vBHBhDKfPsc5cFGNWm69WBqpwd86u1qwZ9PWevKqm1A3yAw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/compat-data': 7.19.4
'@babel/core': 7.18.9
- '@babel/helper-compilation-targets': 7.19.3_@babel+core@7.18.9
- '@babel/helper-plugin-utils': 7.19.0
- '@babel/helper-validator-option': 7.18.6
- '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.18.9_@babel+core@7.18.9
- '@babel/plugin-proposal-async-generator-functions': 7.19.1_@babel+core@7.18.9
- '@babel/plugin-proposal-class-properties': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-proposal-class-static-block': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-proposal-dynamic-import': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-proposal-export-namespace-from': 7.18.9_@babel+core@7.18.9
- '@babel/plugin-proposal-json-strings': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-proposal-logical-assignment-operators': 7.18.9_@babel+core@7.18.9
- '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-proposal-numeric-separator': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-proposal-object-rest-spread': 7.19.4_@babel+core@7.18.9
- '@babel/plugin-proposal-optional-catch-binding': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-proposal-optional-chaining': 7.18.9_@babel+core@7.18.9
- '@babel/plugin-proposal-private-methods': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-proposal-private-property-in-object': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-proposal-unicode-property-regex': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-syntax-async-generators': 7.8.4_@babel+core@7.18.9
- '@babel/plugin-syntax-class-properties': 7.12.13_@babel+core@7.18.9
- '@babel/plugin-syntax-class-static-block': 7.14.5_@babel+core@7.18.9
- '@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.18.9
- '@babel/plugin-syntax-export-namespace-from': 7.8.3_@babel+core@7.18.9
- '@babel/plugin-syntax-import-assertions': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-syntax-json-strings': 7.8.3_@babel+core@7.18.9
- '@babel/plugin-syntax-logical-assignment-operators': 7.10.4_@babel+core@7.18.9
- '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3_@babel+core@7.18.9
- '@babel/plugin-syntax-numeric-separator': 7.10.4_@babel+core@7.18.9
- '@babel/plugin-syntax-object-rest-spread': 7.8.3_@babel+core@7.18.9
- '@babel/plugin-syntax-optional-catch-binding': 7.8.3_@babel+core@7.18.9
- '@babel/plugin-syntax-optional-chaining': 7.8.3_@babel+core@7.18.9
- '@babel/plugin-syntax-private-property-in-object': 7.14.5_@babel+core@7.18.9
- '@babel/plugin-syntax-top-level-await': 7.14.5_@babel+core@7.18.9
- '@babel/plugin-transform-arrow-functions': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-transform-async-to-generator': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-transform-block-scoped-functions': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-transform-block-scoping': 7.19.4_@babel+core@7.18.9
- '@babel/plugin-transform-classes': 7.19.0_@babel+core@7.18.9
- '@babel/plugin-transform-computed-properties': 7.18.9_@babel+core@7.18.9
- '@babel/plugin-transform-destructuring': 7.19.4_@babel+core@7.18.9
- '@babel/plugin-transform-dotall-regex': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-transform-duplicate-keys': 7.18.9_@babel+core@7.18.9
- '@babel/plugin-transform-exponentiation-operator': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-transform-for-of': 7.18.8_@babel+core@7.18.9
- '@babel/plugin-transform-function-name': 7.18.9_@babel+core@7.18.9
- '@babel/plugin-transform-literals': 7.18.9_@babel+core@7.18.9
- '@babel/plugin-transform-member-expression-literals': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-transform-modules-amd': 7.19.6_@babel+core@7.18.9
- '@babel/plugin-transform-modules-commonjs': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-transform-modules-systemjs': 7.19.6_@babel+core@7.18.9
- '@babel/plugin-transform-modules-umd': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-transform-named-capturing-groups-regex': 7.19.1_@babel+core@7.18.9
- '@babel/plugin-transform-new-target': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-transform-object-super': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-transform-parameters': 7.18.8_@babel+core@7.18.9
- '@babel/plugin-transform-property-literals': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-transform-regenerator': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-transform-reserved-words': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-transform-shorthand-properties': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-transform-spread': 7.19.0_@babel+core@7.18.9
- '@babel/plugin-transform-sticky-regex': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-transform-template-literals': 7.18.9_@babel+core@7.18.9
- '@babel/plugin-transform-typeof-symbol': 7.18.9_@babel+core@7.18.9
- '@babel/plugin-transform-unicode-escapes': 7.18.10_@babel+core@7.18.9
- '@babel/plugin-transform-unicode-regex': 7.18.6_@babel+core@7.18.9
- '@babel/preset-modules': 0.1.5_@babel+core@7.18.9
- '@babel/types': 7.19.4
- babel-plugin-polyfill-corejs2: 0.3.3_@babel+core@7.18.9
- babel-plugin-polyfill-corejs3: 0.5.3_@babel+core@7.18.9
- babel-plugin-polyfill-regenerator: 0.3.1_@babel+core@7.18.9
+ '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.18.9)
+ '@babel/helper-plugin-utils': 7.22.5
+ dev: true
+
+ /@babel/plugin-transform-unicode-regex@7.23.3(@babel/core@7.23.5):
+ resolution: {integrity: sha512-wMHpNA4x2cIA32b/ci3AfwNgheiva2W0WUKWTK7vBHBhDKfPsc5cFGNWm69WBqpwd86u1qwZ9PWevKqm1A3yAw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.23.5
+ '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.5)
+ '@babel/helper-plugin-utils': 7.22.5
+ dev: true
+
+ /@babel/plugin-transform-unicode-sets-regex@7.23.3(@babel/core@7.18.9):
+ resolution: {integrity: sha512-W7lliA/v9bNR83Qc3q1ip9CQMZ09CcHDbHfbLRDNuAhn1Mvkr1ZNF7hPmztMQvtTGVLJ9m8IZqWsTkXOml8dbw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+ dependencies:
+ '@babel/core': 7.18.9
+ '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.18.9)
+ '@babel/helper-plugin-utils': 7.22.5
+ dev: true
+
+ /@babel/plugin-transform-unicode-sets-regex@7.23.3(@babel/core@7.23.5):
+ resolution: {integrity: sha512-W7lliA/v9bNR83Qc3q1ip9CQMZ09CcHDbHfbLRDNuAhn1Mvkr1ZNF7hPmztMQvtTGVLJ9m8IZqWsTkXOml8dbw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+ dependencies:
+ '@babel/core': 7.23.5
+ '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.5)
+ '@babel/helper-plugin-utils': 7.22.5
+ dev: true
+
+ /@babel/preset-env@7.19.4(@babel/core@7.22.1):
+ resolution: {integrity: sha512-5QVOTXUdqTCjQuh2GGtdd7YEhoRXBMVGROAtsBeLGIbIz3obCBIfRMT1I3ZKkMgNzwkyCkftDXSSkHxnfVf4qg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/compat-data': 7.19.4
+ '@babel/core': 7.22.1
+ '@babel/helper-compilation-targets': 7.21.5(@babel/core@7.22.1)
+ '@babel/helper-plugin-utils': 7.21.5
+ '@babel/helper-validator-option': 7.21.0
+ '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.18.6(@babel/core@7.22.1)
+ '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.18.9(@babel/core@7.22.1)
+ '@babel/plugin-proposal-async-generator-functions': 7.19.1(@babel/core@7.22.1)
+ '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.22.1)
+ '@babel/plugin-proposal-class-static-block': 7.18.6(@babel/core@7.22.1)
+ '@babel/plugin-proposal-dynamic-import': 7.18.6(@babel/core@7.22.1)
+ '@babel/plugin-proposal-export-namespace-from': 7.18.9(@babel/core@7.22.1)
+ '@babel/plugin-proposal-json-strings': 7.18.6(@babel/core@7.22.1)
+ '@babel/plugin-proposal-logical-assignment-operators': 7.18.9(@babel/core@7.22.1)
+ '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6(@babel/core@7.22.1)
+ '@babel/plugin-proposal-numeric-separator': 7.18.6(@babel/core@7.22.1)
+ '@babel/plugin-proposal-object-rest-spread': 7.19.4(@babel/core@7.22.1)
+ '@babel/plugin-proposal-optional-catch-binding': 7.18.6(@babel/core@7.22.1)
+ '@babel/plugin-proposal-optional-chaining': 7.18.9(@babel/core@7.22.1)
+ '@babel/plugin-proposal-private-methods': 7.18.6(@babel/core@7.22.1)
+ '@babel/plugin-proposal-private-property-in-object': 7.18.6(@babel/core@7.22.1)
+ '@babel/plugin-proposal-unicode-property-regex': 7.18.6(@babel/core@7.22.1)
+ '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.22.1)
+ '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.22.1)
+ '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.22.1)
+ '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.22.1)
+ '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.22.1)
+ '@babel/plugin-syntax-import-assertions': 7.18.6(@babel/core@7.22.1)
+ '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.22.1)
+ '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.22.1)
+ '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.22.1)
+ '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.22.1)
+ '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.22.1)
+ '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.22.1)
+ '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.22.1)
+ '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.22.1)
+ '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.22.1)
+ '@babel/plugin-transform-arrow-functions': 7.18.6(@babel/core@7.22.1)
+ '@babel/plugin-transform-async-to-generator': 7.18.6(@babel/core@7.22.1)
+ '@babel/plugin-transform-block-scoped-functions': 7.18.6(@babel/core@7.22.1)
+ '@babel/plugin-transform-block-scoping': 7.19.4(@babel/core@7.22.1)
+ '@babel/plugin-transform-classes': 7.19.0(@babel/core@7.22.1)
+ '@babel/plugin-transform-computed-properties': 7.18.9(@babel/core@7.22.1)
+ '@babel/plugin-transform-destructuring': 7.19.4(@babel/core@7.22.1)
+ '@babel/plugin-transform-dotall-regex': 7.18.6(@babel/core@7.22.1)
+ '@babel/plugin-transform-duplicate-keys': 7.18.9(@babel/core@7.22.1)
+ '@babel/plugin-transform-exponentiation-operator': 7.18.6(@babel/core@7.22.1)
+ '@babel/plugin-transform-for-of': 7.18.8(@babel/core@7.22.1)
+ '@babel/plugin-transform-function-name': 7.18.9(@babel/core@7.22.1)
+ '@babel/plugin-transform-literals': 7.18.9(@babel/core@7.22.1)
+ '@babel/plugin-transform-member-expression-literals': 7.18.6(@babel/core@7.22.1)
+ '@babel/plugin-transform-modules-amd': 7.19.6(@babel/core@7.22.1)
+ '@babel/plugin-transform-modules-commonjs': 7.19.6(@babel/core@7.22.1)
+ '@babel/plugin-transform-modules-systemjs': 7.19.6(@babel/core@7.22.1)
+ '@babel/plugin-transform-modules-umd': 7.18.6(@babel/core@7.22.1)
+ '@babel/plugin-transform-named-capturing-groups-regex': 7.19.1(@babel/core@7.22.1)
+ '@babel/plugin-transform-new-target': 7.18.6(@babel/core@7.22.1)
+ '@babel/plugin-transform-object-super': 7.18.6(@babel/core@7.22.1)
+ '@babel/plugin-transform-parameters': 7.18.8(@babel/core@7.22.1)
+ '@babel/plugin-transform-property-literals': 7.18.6(@babel/core@7.22.1)
+ '@babel/plugin-transform-regenerator': 7.18.6(@babel/core@7.22.1)
+ '@babel/plugin-transform-reserved-words': 7.18.6(@babel/core@7.22.1)
+ '@babel/plugin-transform-shorthand-properties': 7.18.6(@babel/core@7.22.1)
+ '@babel/plugin-transform-spread': 7.19.0(@babel/core@7.22.1)
+ '@babel/plugin-transform-sticky-regex': 7.18.6(@babel/core@7.22.1)
+ '@babel/plugin-transform-template-literals': 7.18.9(@babel/core@7.22.1)
+ '@babel/plugin-transform-typeof-symbol': 7.18.9(@babel/core@7.22.1)
+ '@babel/plugin-transform-unicode-escapes': 7.18.10(@babel/core@7.22.1)
+ '@babel/plugin-transform-unicode-regex': 7.18.6(@babel/core@7.22.1)
+ '@babel/preset-modules': 0.1.5(@babel/core@7.22.1)
+ '@babel/types': 7.22.4
+ babel-plugin-polyfill-corejs2: 0.3.3(@babel/core@7.22.1)
+ babel-plugin-polyfill-corejs3: 0.6.0(@babel/core@7.22.1)
+ babel-plugin-polyfill-regenerator: 0.4.1(@babel/core@7.22.1)
core-js-compat: 3.26.0
semver: 6.3.0
transitivePeerDependencies:
- supports-color
dev: true
- /@babel/preset-env/7.19.4_@babel+core@7.18.9:
- resolution: {integrity: sha512-5QVOTXUdqTCjQuh2GGtdd7YEhoRXBMVGROAtsBeLGIbIz3obCBIfRMT1I3ZKkMgNzwkyCkftDXSSkHxnfVf4qg==}
+ /@babel/preset-env@7.23.5(@babel/core@7.18.9):
+ resolution: {integrity: sha512-0d/uxVD6tFGWXGDSfyMD1p2otoaKmu6+GD+NfAx0tMaH+dxORnp7T9TaVQ6mKyya7iBtCIVxHjWT7MuzzM9z+A==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/compat-data': 7.19.4
+ '@babel/compat-data': 7.23.5
'@babel/core': 7.18.9
- '@babel/helper-compilation-targets': 7.19.3_@babel+core@7.18.9
- '@babel/helper-plugin-utils': 7.19.0
- '@babel/helper-validator-option': 7.18.6
- '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.18.9_@babel+core@7.18.9
- '@babel/plugin-proposal-async-generator-functions': 7.19.1_@babel+core@7.18.9
- '@babel/plugin-proposal-class-properties': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-proposal-class-static-block': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-proposal-dynamic-import': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-proposal-export-namespace-from': 7.18.9_@babel+core@7.18.9
- '@babel/plugin-proposal-json-strings': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-proposal-logical-assignment-operators': 7.18.9_@babel+core@7.18.9
- '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-proposal-numeric-separator': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-proposal-object-rest-spread': 7.19.4_@babel+core@7.18.9
- '@babel/plugin-proposal-optional-catch-binding': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-proposal-optional-chaining': 7.18.9_@babel+core@7.18.9
- '@babel/plugin-proposal-private-methods': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-proposal-private-property-in-object': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-proposal-unicode-property-regex': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-syntax-async-generators': 7.8.4_@babel+core@7.18.9
- '@babel/plugin-syntax-class-properties': 7.12.13_@babel+core@7.18.9
- '@babel/plugin-syntax-class-static-block': 7.14.5_@babel+core@7.18.9
- '@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.18.9
- '@babel/plugin-syntax-export-namespace-from': 7.8.3_@babel+core@7.18.9
- '@babel/plugin-syntax-import-assertions': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-syntax-json-strings': 7.8.3_@babel+core@7.18.9
- '@babel/plugin-syntax-logical-assignment-operators': 7.10.4_@babel+core@7.18.9
- '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3_@babel+core@7.18.9
- '@babel/plugin-syntax-numeric-separator': 7.10.4_@babel+core@7.18.9
- '@babel/plugin-syntax-object-rest-spread': 7.8.3_@babel+core@7.18.9
- '@babel/plugin-syntax-optional-catch-binding': 7.8.3_@babel+core@7.18.9
- '@babel/plugin-syntax-optional-chaining': 7.8.3_@babel+core@7.18.9
- '@babel/plugin-syntax-private-property-in-object': 7.14.5_@babel+core@7.18.9
- '@babel/plugin-syntax-top-level-await': 7.14.5_@babel+core@7.18.9
- '@babel/plugin-transform-arrow-functions': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-transform-async-to-generator': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-transform-block-scoped-functions': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-transform-block-scoping': 7.19.4_@babel+core@7.18.9
- '@babel/plugin-transform-classes': 7.19.0_@babel+core@7.18.9
- '@babel/plugin-transform-computed-properties': 7.18.9_@babel+core@7.18.9
- '@babel/plugin-transform-destructuring': 7.19.4_@babel+core@7.18.9
- '@babel/plugin-transform-dotall-regex': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-transform-duplicate-keys': 7.18.9_@babel+core@7.18.9
- '@babel/plugin-transform-exponentiation-operator': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-transform-for-of': 7.18.8_@babel+core@7.18.9
- '@babel/plugin-transform-function-name': 7.18.9_@babel+core@7.18.9
- '@babel/plugin-transform-literals': 7.18.9_@babel+core@7.18.9
- '@babel/plugin-transform-member-expression-literals': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-transform-modules-amd': 7.19.6_@babel+core@7.18.9
- '@babel/plugin-transform-modules-commonjs': 7.19.6_@babel+core@7.18.9
- '@babel/plugin-transform-modules-systemjs': 7.19.6_@babel+core@7.18.9
- '@babel/plugin-transform-modules-umd': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-transform-named-capturing-groups-regex': 7.19.1_@babel+core@7.18.9
- '@babel/plugin-transform-new-target': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-transform-object-super': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-transform-parameters': 7.18.8_@babel+core@7.18.9
- '@babel/plugin-transform-property-literals': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-transform-regenerator': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-transform-reserved-words': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-transform-shorthand-properties': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-transform-spread': 7.19.0_@babel+core@7.18.9
- '@babel/plugin-transform-sticky-regex': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-transform-template-literals': 7.18.9_@babel+core@7.18.9
- '@babel/plugin-transform-typeof-symbol': 7.18.9_@babel+core@7.18.9
- '@babel/plugin-transform-unicode-escapes': 7.18.10_@babel+core@7.18.9
- '@babel/plugin-transform-unicode-regex': 7.18.6_@babel+core@7.18.9
- '@babel/preset-modules': 0.1.5_@babel+core@7.18.9
- '@babel/types': 7.19.4
- babel-plugin-polyfill-corejs2: 0.3.3_@babel+core@7.18.9
- babel-plugin-polyfill-corejs3: 0.6.0_@babel+core@7.18.9
- babel-plugin-polyfill-regenerator: 0.4.1_@babel+core@7.18.9
- core-js-compat: 3.26.0
- semver: 6.3.0
+ '@babel/helper-compilation-targets': 7.22.15
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/helper-validator-option': 7.23.5
+ '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.23.3(@babel/core@7.18.9)
+ '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.23.3(@babel/core@7.18.9)
+ '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.23.3(@babel/core@7.18.9)
+ '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.18.9)
+ '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.18.9)
+ '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.18.9)
+ '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.18.9)
+ '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.18.9)
+ '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.18.9)
+ '@babel/plugin-syntax-import-assertions': 7.23.3(@babel/core@7.18.9)
+ '@babel/plugin-syntax-import-attributes': 7.23.3(@babel/core@7.18.9)
+ '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.18.9)
+ '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.18.9)
+ '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.18.9)
+ '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.18.9)
+ '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.18.9)
+ '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.18.9)
+ '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.18.9)
+ '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.18.9)
+ '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.18.9)
+ '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.18.9)
+ '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.18.9)
+ '@babel/plugin-transform-arrow-functions': 7.23.3(@babel/core@7.18.9)
+ '@babel/plugin-transform-async-generator-functions': 7.23.4(@babel/core@7.18.9)
+ '@babel/plugin-transform-async-to-generator': 7.23.3(@babel/core@7.18.9)
+ '@babel/plugin-transform-block-scoped-functions': 7.23.3(@babel/core@7.18.9)
+ '@babel/plugin-transform-block-scoping': 7.23.4(@babel/core@7.18.9)
+ '@babel/plugin-transform-class-properties': 7.23.3(@babel/core@7.18.9)
+ '@babel/plugin-transform-class-static-block': 7.23.4(@babel/core@7.18.9)
+ '@babel/plugin-transform-classes': 7.23.5(@babel/core@7.18.9)
+ '@babel/plugin-transform-computed-properties': 7.23.3(@babel/core@7.18.9)
+ '@babel/plugin-transform-destructuring': 7.23.3(@babel/core@7.18.9)
+ '@babel/plugin-transform-dotall-regex': 7.23.3(@babel/core@7.18.9)
+ '@babel/plugin-transform-duplicate-keys': 7.23.3(@babel/core@7.18.9)
+ '@babel/plugin-transform-dynamic-import': 7.23.4(@babel/core@7.18.9)
+ '@babel/plugin-transform-exponentiation-operator': 7.23.3(@babel/core@7.18.9)
+ '@babel/plugin-transform-export-namespace-from': 7.23.4(@babel/core@7.18.9)
+ '@babel/plugin-transform-for-of': 7.23.3(@babel/core@7.18.9)
+ '@babel/plugin-transform-function-name': 7.23.3(@babel/core@7.18.9)
+ '@babel/plugin-transform-json-strings': 7.23.4(@babel/core@7.18.9)
+ '@babel/plugin-transform-literals': 7.23.3(@babel/core@7.18.9)
+ '@babel/plugin-transform-logical-assignment-operators': 7.23.4(@babel/core@7.18.9)
+ '@babel/plugin-transform-member-expression-literals': 7.23.3(@babel/core@7.18.9)
+ '@babel/plugin-transform-modules-amd': 7.23.3(@babel/core@7.18.9)
+ '@babel/plugin-transform-modules-commonjs': 7.23.3(@babel/core@7.18.9)
+ '@babel/plugin-transform-modules-systemjs': 7.23.3(@babel/core@7.18.9)
+ '@babel/plugin-transform-modules-umd': 7.23.3(@babel/core@7.18.9)
+ '@babel/plugin-transform-named-capturing-groups-regex': 7.22.5(@babel/core@7.18.9)
+ '@babel/plugin-transform-new-target': 7.23.3(@babel/core@7.18.9)
+ '@babel/plugin-transform-nullish-coalescing-operator': 7.23.4(@babel/core@7.18.9)
+ '@babel/plugin-transform-numeric-separator': 7.23.4(@babel/core@7.18.9)
+ '@babel/plugin-transform-object-rest-spread': 7.23.4(@babel/core@7.18.9)
+ '@babel/plugin-transform-object-super': 7.23.3(@babel/core@7.18.9)
+ '@babel/plugin-transform-optional-catch-binding': 7.23.4(@babel/core@7.18.9)
+ '@babel/plugin-transform-optional-chaining': 7.23.4(@babel/core@7.18.9)
+ '@babel/plugin-transform-parameters': 7.23.3(@babel/core@7.18.9)
+ '@babel/plugin-transform-private-methods': 7.23.3(@babel/core@7.18.9)
+ '@babel/plugin-transform-private-property-in-object': 7.23.4(@babel/core@7.18.9)
+ '@babel/plugin-transform-property-literals': 7.23.3(@babel/core@7.18.9)
+ '@babel/plugin-transform-regenerator': 7.23.3(@babel/core@7.18.9)
+ '@babel/plugin-transform-reserved-words': 7.23.3(@babel/core@7.18.9)
+ '@babel/plugin-transform-shorthand-properties': 7.23.3(@babel/core@7.18.9)
+ '@babel/plugin-transform-spread': 7.23.3(@babel/core@7.18.9)
+ '@babel/plugin-transform-sticky-regex': 7.23.3(@babel/core@7.18.9)
+ '@babel/plugin-transform-template-literals': 7.23.3(@babel/core@7.18.9)
+ '@babel/plugin-transform-typeof-symbol': 7.23.3(@babel/core@7.18.9)
+ '@babel/plugin-transform-unicode-escapes': 7.23.3(@babel/core@7.18.9)
+ '@babel/plugin-transform-unicode-property-regex': 7.23.3(@babel/core@7.18.9)
+ '@babel/plugin-transform-unicode-regex': 7.23.3(@babel/core@7.18.9)
+ '@babel/plugin-transform-unicode-sets-regex': 7.23.3(@babel/core@7.18.9)
+ '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.18.9)
+ babel-plugin-polyfill-corejs2: 0.4.6(@babel/core@7.18.9)
+ babel-plugin-polyfill-corejs3: 0.8.6(@babel/core@7.18.9)
+ babel-plugin-polyfill-regenerator: 0.5.3(@babel/core@7.18.9)
+ core-js-compat: 3.33.3
+ semver: 6.3.1
transitivePeerDependencies:
- supports-color
dev: true
- /@babel/preset-env/7.19.4_@babel+core@7.19.6:
- resolution: {integrity: sha512-5QVOTXUdqTCjQuh2GGtdd7YEhoRXBMVGROAtsBeLGIbIz3obCBIfRMT1I3ZKkMgNzwkyCkftDXSSkHxnfVf4qg==}
+ /@babel/preset-env@7.23.5(@babel/core@7.23.5):
+ resolution: {integrity: sha512-0d/uxVD6tFGWXGDSfyMD1p2otoaKmu6+GD+NfAx0tMaH+dxORnp7T9TaVQ6mKyya7iBtCIVxHjWT7MuzzM9z+A==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/compat-data': 7.19.4
- '@babel/core': 7.19.6
- '@babel/helper-compilation-targets': 7.19.3_@babel+core@7.19.6
- '@babel/helper-plugin-utils': 7.19.0
- '@babel/helper-validator-option': 7.18.6
- '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.18.6_@babel+core@7.19.6
- '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.18.9_@babel+core@7.19.6
- '@babel/plugin-proposal-async-generator-functions': 7.19.1_@babel+core@7.19.6
- '@babel/plugin-proposal-class-properties': 7.18.6_@babel+core@7.19.6
- '@babel/plugin-proposal-class-static-block': 7.18.6_@babel+core@7.19.6
- '@babel/plugin-proposal-dynamic-import': 7.18.6_@babel+core@7.19.6
- '@babel/plugin-proposal-export-namespace-from': 7.18.9_@babel+core@7.19.6
- '@babel/plugin-proposal-json-strings': 7.18.6_@babel+core@7.19.6
- '@babel/plugin-proposal-logical-assignment-operators': 7.18.9_@babel+core@7.19.6
- '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6_@babel+core@7.19.6
- '@babel/plugin-proposal-numeric-separator': 7.18.6_@babel+core@7.19.6
- '@babel/plugin-proposal-object-rest-spread': 7.19.4_@babel+core@7.19.6
- '@babel/plugin-proposal-optional-catch-binding': 7.18.6_@babel+core@7.19.6
- '@babel/plugin-proposal-optional-chaining': 7.18.9_@babel+core@7.19.6
- '@babel/plugin-proposal-private-methods': 7.18.6_@babel+core@7.19.6
- '@babel/plugin-proposal-private-property-in-object': 7.18.6_@babel+core@7.19.6
- '@babel/plugin-proposal-unicode-property-regex': 7.18.6_@babel+core@7.19.6
- '@babel/plugin-syntax-async-generators': 7.8.4_@babel+core@7.19.6
- '@babel/plugin-syntax-class-properties': 7.12.13_@babel+core@7.19.6
- '@babel/plugin-syntax-class-static-block': 7.14.5_@babel+core@7.19.6
- '@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.19.6
- '@babel/plugin-syntax-export-namespace-from': 7.8.3_@babel+core@7.19.6
- '@babel/plugin-syntax-import-assertions': 7.18.6_@babel+core@7.19.6
- '@babel/plugin-syntax-json-strings': 7.8.3_@babel+core@7.19.6
- '@babel/plugin-syntax-logical-assignment-operators': 7.10.4_@babel+core@7.19.6
- '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3_@babel+core@7.19.6
- '@babel/plugin-syntax-numeric-separator': 7.10.4_@babel+core@7.19.6
- '@babel/plugin-syntax-object-rest-spread': 7.8.3_@babel+core@7.19.6
- '@babel/plugin-syntax-optional-catch-binding': 7.8.3_@babel+core@7.19.6
- '@babel/plugin-syntax-optional-chaining': 7.8.3_@babel+core@7.19.6
- '@babel/plugin-syntax-private-property-in-object': 7.14.5_@babel+core@7.19.6
- '@babel/plugin-syntax-top-level-await': 7.14.5_@babel+core@7.19.6
- '@babel/plugin-transform-arrow-functions': 7.18.6_@babel+core@7.19.6
- '@babel/plugin-transform-async-to-generator': 7.18.6_@babel+core@7.19.6
- '@babel/plugin-transform-block-scoped-functions': 7.18.6_@babel+core@7.19.6
- '@babel/plugin-transform-block-scoping': 7.19.4_@babel+core@7.19.6
- '@babel/plugin-transform-classes': 7.19.0_@babel+core@7.19.6
- '@babel/plugin-transform-computed-properties': 7.18.9_@babel+core@7.19.6
- '@babel/plugin-transform-destructuring': 7.19.4_@babel+core@7.19.6
- '@babel/plugin-transform-dotall-regex': 7.18.6_@babel+core@7.19.6
- '@babel/plugin-transform-duplicate-keys': 7.18.9_@babel+core@7.19.6
- '@babel/plugin-transform-exponentiation-operator': 7.18.6_@babel+core@7.19.6
- '@babel/plugin-transform-for-of': 7.18.8_@babel+core@7.19.6
- '@babel/plugin-transform-function-name': 7.18.9_@babel+core@7.19.6
- '@babel/plugin-transform-literals': 7.18.9_@babel+core@7.19.6
- '@babel/plugin-transform-member-expression-literals': 7.18.6_@babel+core@7.19.6
- '@babel/plugin-transform-modules-amd': 7.19.6_@babel+core@7.19.6
- '@babel/plugin-transform-modules-commonjs': 7.19.6_@babel+core@7.19.6
- '@babel/plugin-transform-modules-systemjs': 7.19.6_@babel+core@7.19.6
- '@babel/plugin-transform-modules-umd': 7.18.6_@babel+core@7.19.6
- '@babel/plugin-transform-named-capturing-groups-regex': 7.19.1_@babel+core@7.19.6
- '@babel/plugin-transform-new-target': 7.18.6_@babel+core@7.19.6
- '@babel/plugin-transform-object-super': 7.18.6_@babel+core@7.19.6
- '@babel/plugin-transform-parameters': 7.18.8_@babel+core@7.19.6
- '@babel/plugin-transform-property-literals': 7.18.6_@babel+core@7.19.6
- '@babel/plugin-transform-regenerator': 7.18.6_@babel+core@7.19.6
- '@babel/plugin-transform-reserved-words': 7.18.6_@babel+core@7.19.6
- '@babel/plugin-transform-shorthand-properties': 7.18.6_@babel+core@7.19.6
- '@babel/plugin-transform-spread': 7.19.0_@babel+core@7.19.6
- '@babel/plugin-transform-sticky-regex': 7.18.6_@babel+core@7.19.6
- '@babel/plugin-transform-template-literals': 7.18.9_@babel+core@7.19.6
- '@babel/plugin-transform-typeof-symbol': 7.18.9_@babel+core@7.19.6
- '@babel/plugin-transform-unicode-escapes': 7.18.10_@babel+core@7.19.6
- '@babel/plugin-transform-unicode-regex': 7.18.6_@babel+core@7.19.6
- '@babel/preset-modules': 0.1.5_@babel+core@7.19.6
- '@babel/types': 7.19.4
- babel-plugin-polyfill-corejs2: 0.3.3_@babel+core@7.19.6
- babel-plugin-polyfill-corejs3: 0.6.0_@babel+core@7.19.6
- babel-plugin-polyfill-regenerator: 0.4.1_@babel+core@7.19.6
- core-js-compat: 3.26.0
- semver: 6.3.0
+ '@babel/compat-data': 7.23.5
+ '@babel/core': 7.23.5
+ '@babel/helper-compilation-targets': 7.22.15
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/helper-validator-option': 7.23.5
+ '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.23.3(@babel/core@7.23.5)
+ '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.23.3(@babel/core@7.23.5)
+ '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.23.3(@babel/core@7.23.5)
+ '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.23.5)
+ '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.23.5)
+ '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.23.5)
+ '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.23.5)
+ '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.23.5)
+ '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.23.5)
+ '@babel/plugin-syntax-import-assertions': 7.23.3(@babel/core@7.23.5)
+ '@babel/plugin-syntax-import-attributes': 7.23.3(@babel/core@7.23.5)
+ '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.23.5)
+ '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.23.5)
+ '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.23.5)
+ '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.23.5)
+ '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.23.5)
+ '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.23.5)
+ '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.23.5)
+ '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.23.5)
+ '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.23.5)
+ '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.23.5)
+ '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.23.5)
+ '@babel/plugin-transform-arrow-functions': 7.23.3(@babel/core@7.23.5)
+ '@babel/plugin-transform-async-generator-functions': 7.23.4(@babel/core@7.23.5)
+ '@babel/plugin-transform-async-to-generator': 7.23.3(@babel/core@7.23.5)
+ '@babel/plugin-transform-block-scoped-functions': 7.23.3(@babel/core@7.23.5)
+ '@babel/plugin-transform-block-scoping': 7.23.4(@babel/core@7.23.5)
+ '@babel/plugin-transform-class-properties': 7.23.3(@babel/core@7.23.5)
+ '@babel/plugin-transform-class-static-block': 7.23.4(@babel/core@7.23.5)
+ '@babel/plugin-transform-classes': 7.23.5(@babel/core@7.23.5)
+ '@babel/plugin-transform-computed-properties': 7.23.3(@babel/core@7.23.5)
+ '@babel/plugin-transform-destructuring': 7.23.3(@babel/core@7.23.5)
+ '@babel/plugin-transform-dotall-regex': 7.23.3(@babel/core@7.23.5)
+ '@babel/plugin-transform-duplicate-keys': 7.23.3(@babel/core@7.23.5)
+ '@babel/plugin-transform-dynamic-import': 7.23.4(@babel/core@7.23.5)
+ '@babel/plugin-transform-exponentiation-operator': 7.23.3(@babel/core@7.23.5)
+ '@babel/plugin-transform-export-namespace-from': 7.23.4(@babel/core@7.23.5)
+ '@babel/plugin-transform-for-of': 7.23.3(@babel/core@7.23.5)
+ '@babel/plugin-transform-function-name': 7.23.3(@babel/core@7.23.5)
+ '@babel/plugin-transform-json-strings': 7.23.4(@babel/core@7.23.5)
+ '@babel/plugin-transform-literals': 7.23.3(@babel/core@7.23.5)
+ '@babel/plugin-transform-logical-assignment-operators': 7.23.4(@babel/core@7.23.5)
+ '@babel/plugin-transform-member-expression-literals': 7.23.3(@babel/core@7.23.5)
+ '@babel/plugin-transform-modules-amd': 7.23.3(@babel/core@7.23.5)
+ '@babel/plugin-transform-modules-commonjs': 7.23.3(@babel/core@7.23.5)
+ '@babel/plugin-transform-modules-systemjs': 7.23.3(@babel/core@7.23.5)
+ '@babel/plugin-transform-modules-umd': 7.23.3(@babel/core@7.23.5)
+ '@babel/plugin-transform-named-capturing-groups-regex': 7.22.5(@babel/core@7.23.5)
+ '@babel/plugin-transform-new-target': 7.23.3(@babel/core@7.23.5)
+ '@babel/plugin-transform-nullish-coalescing-operator': 7.23.4(@babel/core@7.23.5)
+ '@babel/plugin-transform-numeric-separator': 7.23.4(@babel/core@7.23.5)
+ '@babel/plugin-transform-object-rest-spread': 7.23.4(@babel/core@7.23.5)
+ '@babel/plugin-transform-object-super': 7.23.3(@babel/core@7.23.5)
+ '@babel/plugin-transform-optional-catch-binding': 7.23.4(@babel/core@7.23.5)
+ '@babel/plugin-transform-optional-chaining': 7.23.4(@babel/core@7.23.5)
+ '@babel/plugin-transform-parameters': 7.23.3(@babel/core@7.23.5)
+ '@babel/plugin-transform-private-methods': 7.23.3(@babel/core@7.23.5)
+ '@babel/plugin-transform-private-property-in-object': 7.23.4(@babel/core@7.23.5)
+ '@babel/plugin-transform-property-literals': 7.23.3(@babel/core@7.23.5)
+ '@babel/plugin-transform-regenerator': 7.23.3(@babel/core@7.23.5)
+ '@babel/plugin-transform-reserved-words': 7.23.3(@babel/core@7.23.5)
+ '@babel/plugin-transform-shorthand-properties': 7.23.3(@babel/core@7.23.5)
+ '@babel/plugin-transform-spread': 7.23.3(@babel/core@7.23.5)
+ '@babel/plugin-transform-sticky-regex': 7.23.3(@babel/core@7.23.5)
+ '@babel/plugin-transform-template-literals': 7.23.3(@babel/core@7.23.5)
+ '@babel/plugin-transform-typeof-symbol': 7.23.3(@babel/core@7.23.5)
+ '@babel/plugin-transform-unicode-escapes': 7.23.3(@babel/core@7.23.5)
+ '@babel/plugin-transform-unicode-property-regex': 7.23.3(@babel/core@7.23.5)
+ '@babel/plugin-transform-unicode-regex': 7.23.3(@babel/core@7.23.5)
+ '@babel/plugin-transform-unicode-sets-regex': 7.23.3(@babel/core@7.23.5)
+ '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.23.5)
+ babel-plugin-polyfill-corejs2: 0.4.6(@babel/core@7.23.5)
+ babel-plugin-polyfill-corejs3: 0.8.6(@babel/core@7.23.5)
+ babel-plugin-polyfill-regenerator: 0.5.3(@babel/core@7.23.5)
+ core-js-compat: 3.33.3
+ semver: 6.3.1
transitivePeerDependencies:
- supports-color
dev: true
- /@babel/preset-modules/0.1.5_@babel+core@7.18.9:
+ /@babel/preset-modules@0.1.5(@babel/core@7.22.1):
resolution: {integrity: sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/plugin-proposal-unicode-property-regex': 7.18.6(@babel/core@7.22.1)
+ '@babel/plugin-transform-dotall-regex': 7.23.3(@babel/core@7.22.1)
+ '@babel/types': 7.23.5
+ esutils: 2.0.3
+ dev: true
+
+ /@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.18.9):
+ resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0
+ dependencies:
'@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
- '@babel/plugin-proposal-unicode-property-regex': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-transform-dotall-regex': 7.18.6_@babel+core@7.18.9
- '@babel/types': 7.19.4
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/types': 7.23.5
esutils: 2.0.3
dev: true
- /@babel/preset-modules/0.1.5_@babel+core@7.19.6:
- resolution: {integrity: sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==}
+ /@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.23.5):
+ resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==}
peerDependencies:
- '@babel/core': ^7.0.0-0
+ '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-plugin-utils': 7.19.0
- '@babel/plugin-proposal-unicode-property-regex': 7.18.6_@babel+core@7.19.6
- '@babel/plugin-transform-dotall-regex': 7.18.6_@babel+core@7.19.6
- '@babel/types': 7.19.4
+ '@babel/core': 7.23.5
+ '@babel/helper-plugin-utils': 7.22.5
+ '@babel/types': 7.23.5
esutils: 2.0.3
dev: true
- /@babel/preset-react/7.18.6_@babel+core@7.18.9:
- resolution: {integrity: sha512-zXr6atUmyYdiWRVLOZahakYmOBHtWc2WGCkP8PYTgZi0iJXDY2CN180TdrIW4OGOAdLc7TifzDIvtx6izaRIzg==}
+ /@babel/preset-react@7.22.3(@babel/core@7.18.9):
+ resolution: {integrity: sha512-lxDz1mnZ9polqClBCVBjIVUypoB4qV3/tZUDb/IlYbW1kiiLaXaX+bInbRjl+lNQ/iUZraQ3+S8daEmoELMWug==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.18.9
- '@babel/helper-plugin-utils': 7.19.0
- '@babel/helper-validator-option': 7.18.6
- '@babel/plugin-transform-react-display-name': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-transform-react-jsx': 7.19.0_@babel+core@7.18.9
- '@babel/plugin-transform-react-jsx-development': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-transform-react-pure-annotations': 7.18.6_@babel+core@7.18.9
+ '@babel/helper-plugin-utils': 7.21.5
+ '@babel/helper-validator-option': 7.21.0
+ '@babel/plugin-transform-react-display-name': 7.18.6(@babel/core@7.18.9)
+ '@babel/plugin-transform-react-jsx': 7.22.3(@babel/core@7.18.9)
+ '@babel/plugin-transform-react-jsx-development': 7.18.6(@babel/core@7.18.9)
+ '@babel/plugin-transform-react-pure-annotations': 7.18.6(@babel/core@7.18.9)
dev: true
- /@babel/preset-typescript/7.18.6_@babel+core@7.18.9:
+ /@babel/preset-typescript@7.18.6(@babel/core@7.18.9):
resolution: {integrity: sha512-s9ik86kXBAnD760aybBucdpnLsAt0jK1xqJn2juOn9lkOvSHV60os5hxoVJsPzMQxvnUJFAlkont2DvvaYEBtQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
@@ -3222,74 +4777,144 @@ packages:
'@babel/core': 7.18.9
'@babel/helper-plugin-utils': 7.19.0
'@babel/helper-validator-option': 7.18.6
- '@babel/plugin-transform-typescript': 7.19.3_@babel+core@7.18.9
- transitivePeerDependencies:
- - supports-color
+ '@babel/plugin-transform-typescript': 7.20.13(@babel/core@7.18.9)
dev: true
- /@babel/register/7.18.9_@babel+core@7.18.9:
- resolution: {integrity: sha512-ZlbnXDcNYHMR25ITwwNKT88JiaukkdVj/nG7r3wnuXkOTHc60Uy05PwMCPre0hSkY68E6zK3xz+vUJSP2jWmcw==}
+ /@babel/preset-typescript@7.18.6(@babel/core@7.22.1):
+ resolution: {integrity: sha512-s9ik86kXBAnD760aybBucdpnLsAt0jK1xqJn2juOn9lkOvSHV60os5hxoVJsPzMQxvnUJFAlkont2DvvaYEBtQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.18.9
- clone-deep: 4.0.1
- find-cache-dir: 2.1.0
- make-dir: 2.1.0
- pirates: 4.0.5
- source-map-support: 0.5.21
+ '@babel/core': 7.22.1
+ '@babel/helper-plugin-utils': 7.19.0
+ '@babel/helper-validator-option': 7.18.6
+ '@babel/plugin-transform-typescript': 7.20.13(@babel/core@7.22.1)
dev: true
- /@babel/runtime-corejs3/7.19.6:
- resolution: {integrity: sha512-oWNn1ZlGde7b4i/3tnixpH9qI0bOAACiUs+KEES4UUCnsPjVWFlWdLV/iwJuPC2qp3EowbAqsm+0XqNwnwYhxA==}
+ /@babel/preset-typescript@7.21.5(@babel/core@7.18.9):
+ resolution: {integrity: sha512-iqe3sETat5EOrORXiQ6rWfoOg2y68Cs75B9wNxdPW4kixJxh7aXQE1KPdWLDniC24T/6dSnguF33W9j/ZZQcmA==}
engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
dependencies:
- core-js-pure: 3.26.0
- regenerator-runtime: 0.13.10
+ '@babel/core': 7.18.9
+ '@babel/helper-plugin-utils': 7.21.5
+ '@babel/helper-validator-option': 7.21.0
+ '@babel/plugin-syntax-jsx': 7.21.4(@babel/core@7.18.9)
+ '@babel/plugin-transform-modules-commonjs': 7.21.5(@babel/core@7.18.9)
+ '@babel/plugin-transform-typescript': 7.22.3(@babel/core@7.18.9)
+ dev: true
+
+ /@babel/regjsgen@0.8.0:
+ resolution: {integrity: sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==}
dev: true
- /@babel/runtime/7.18.9:
+ /@babel/runtime@7.18.9:
resolution: {integrity: sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw==}
engines: {node: '>=6.9.0'}
dependencies:
regenerator-runtime: 0.13.10
dev: true
- /@babel/runtime/7.19.4:
+ /@babel/runtime@7.19.4:
resolution: {integrity: sha512-EXpLCrk55f+cYqmHsSR+yD/0gAIMxxA9QK9lnQWzhMCvt+YmoBN7Zx94s++Kv0+unHk39vxNO8t+CMA2WSS3wA==}
engines: {node: '>=6.9.0'}
dependencies:
regenerator-runtime: 0.13.10
- /@babel/template/7.18.10:
+ /@babel/runtime@7.21.0:
+ resolution: {integrity: sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ regenerator-runtime: 0.13.11
+ dev: true
+
+ /@babel/runtime@7.23.6:
+ resolution: {integrity: sha512-zHd0eUrf5GZoOWVCXp6koAKQTfZV07eit6bGPmJgnZdnSAvvZee6zniW2XMF7Cmc4ISOOnPy3QaSiIJGJkVEDQ==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ regenerator-runtime: 0.14.0
+ dev: true
+
+ /@babel/template@7.18.10:
resolution: {integrity: sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/code-frame': 7.18.6
'@babel/parser': 7.19.6
- '@babel/types': 7.19.4
+ '@babel/types': 7.22.4
dev: true
- /@babel/traverse/7.19.6:
+ /@babel/template@7.20.7:
+ resolution: {integrity: sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/code-frame': 7.23.5
+ '@babel/parser': 7.23.5
+ '@babel/types': 7.23.5
+
+ /@babel/template@7.22.15:
+ resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/code-frame': 7.23.5
+ '@babel/parser': 7.23.5
+ '@babel/types': 7.23.5
+
+ /@babel/traverse@7.19.6:
resolution: {integrity: sha512-6l5HrUCzFM04mfbG09AagtYyR2P0B71B1wN7PfSPiksDPz2k5H9CBC1tcZpz2M8OxbKTPccByoOJ22rUKbpmQQ==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/code-frame': 7.18.6
- '@babel/generator': 7.19.6
+ '@babel/generator': 7.22.3
'@babel/helper-environment-visitor': 7.18.9
'@babel/helper-function-name': 7.19.0
'@babel/helper-hoist-variables': 7.18.6
'@babel/helper-split-export-declaration': 7.18.6
'@babel/parser': 7.19.6
- '@babel/types': 7.19.4
+ '@babel/types': 7.22.4
debug: 4.3.4
globals: 11.12.0
transitivePeerDependencies:
- supports-color
dev: true
- /@babel/types/7.19.4:
+ /@babel/traverse@7.21.5:
+ resolution: {integrity: sha512-AhQoI3YjWi6u/y/ntv7k48mcrCXmus0t79J9qPNlk/lAsFlCiJ047RmbfMOawySTHtywXhbXgpx/8nXMYd+oFw==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/code-frame': 7.23.5
+ '@babel/generator': 7.23.5
+ '@babel/helper-environment-visitor': 7.22.20
+ '@babel/helper-function-name': 7.23.0
+ '@babel/helper-hoist-variables': 7.22.5
+ '@babel/helper-split-export-declaration': 7.22.6
+ '@babel/parser': 7.23.5
+ '@babel/types': 7.23.5
+ debug: 4.3.4
+ globals: 11.12.0
+ transitivePeerDependencies:
+ - supports-color
+
+ /@babel/traverse@7.23.5:
+ resolution: {integrity: sha512-czx7Xy5a6sapWWRx61m1Ke1Ra4vczu1mCTtJam5zRTBOonfdJ+S/B6HYmGYu3fJtr8GGET3si6IhgWVBhJ/m8w==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/code-frame': 7.23.5
+ '@babel/generator': 7.23.5
+ '@babel/helper-environment-visitor': 7.22.20
+ '@babel/helper-function-name': 7.23.0
+ '@babel/helper-hoist-variables': 7.22.5
+ '@babel/helper-split-export-declaration': 7.22.6
+ '@babel/parser': 7.23.5
+ '@babel/types': 7.23.5
+ debug: 4.3.4
+ globals: 11.12.0
+ transitivePeerDependencies:
+ - supports-color
+
+ /@babel/types@7.19.4:
resolution: {integrity: sha512-M5LK7nAeS6+9j7hAq+b3fQs+pNfUtTGq+yFFfHnauFA8zQtLRfmuipmsKDKKLuyG+wC8ABW43A153YNawNTEtw==}
engines: {node: '>=6.9.0'}
dependencies:
@@ -3298,56 +4923,103 @@ packages:
to-fast-properties: 2.0.0
dev: true
- /@bcoe/v8-coverage/0.2.3:
+ /@babel/types@7.21.5:
+ resolution: {integrity: sha512-m4AfNvVF2mVC/F7fDEdH2El3HzUg9It/XsCxZiOTTA3m3qYfcSVSbTfM6Q9xG+hYDniZssYhlXKKUMD5m8tF4Q==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/helper-string-parser': 7.23.4
+ '@babel/helper-validator-identifier': 7.22.20
+ to-fast-properties: 2.0.0
+
+ /@babel/types@7.22.4:
+ resolution: {integrity: sha512-Tx9x3UBHTTsMSW85WB2kphxYQVvrZ/t1FxD88IpSgIjiUJlCm9z+xWIDwyo1vffTwSqteqyznB8ZE9vYYk16zA==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/helper-string-parser': 7.23.4
+ '@babel/helper-validator-identifier': 7.22.20
+ to-fast-properties: 2.0.0
+ dev: true
+
+ /@babel/types@7.23.5:
+ resolution: {integrity: sha512-ON5kSOJwVO6xXVRTvOI0eOnWe7VdUcIpsovGo9U/Br4Ie4UVFQTboO2cYnDhAGU6Fp+UxSiT+pMft0SMHfuq6w==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/helper-string-parser': 7.23.4
+ '@babel/helper-validator-identifier': 7.22.20
+ to-fast-properties: 2.0.0
+
+ /@bcoe/v8-coverage@0.2.3:
resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
dev: true
- /@cnakazawa/watch/1.0.4:
- resolution: {integrity: sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ==}
- engines: {node: '>=0.1.95'}
- hasBin: true
+ /@creativebulma/bulma-tooltip@1.2.0:
+ resolution: {integrity: sha512-ooImbeXEBxf77cttbzA7X5rC5aAWm9UsXIGViFOnsqB+6M944GkB28S5R4UWRqjFd2iW4zGEkEifAU+q43pt2w==}
+ dev: true
+
+ /@cspotcode/source-map-support@0.8.1:
+ resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
+ engines: {node: '>=12'}
dependencies:
- exec-sh: 0.3.6
- minimist: 1.2.7
+ '@jridgewell/trace-mapping': 0.3.9
dev: true
- /@colors/colors/1.5.0:
- resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==}
- engines: {node: '>=0.1.90'}
- requiresBuild: true
+ /@devicefarmer/adbkit-logcat@2.1.3:
+ resolution: {integrity: sha512-yeaGFjNBc/6+svbDeul1tNHtNChw6h8pSHAt5D+JsedUrMTN7tla7B15WLDyekxsuS2XlZHRxpuC6m92wiwCNw==}
+ engines: {node: '>= 4'}
dev: true
- optional: true
- /@creativebulma/bulma-tooltip/1.2.0:
- resolution: {integrity: sha512-ooImbeXEBxf77cttbzA7X5rC5aAWm9UsXIGViFOnsqB+6M944GkB28S5R4UWRqjFd2iW4zGEkEifAU+q43pt2w==}
+ /@devicefarmer/adbkit-monkey@1.2.1:
+ resolution: {integrity: sha512-ZzZY/b66W2Jd6NHbAhLyDWOEIBWC11VizGFk7Wx7M61JZRz7HR9Cq5P+65RKWUU7u6wgsE8Lmh9nE4Mz+U2eTg==}
+ engines: {node: '>= 0.10.4'}
dev: true
- /@discoveryjs/json-ext/0.5.7:
- resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==}
- engines: {node: '>=10.0.0'}
+ /@devicefarmer/adbkit@3.2.3:
+ resolution: {integrity: sha512-wK9rVrabs4QU0oK8Jnwi+HRBEm+s1x/o63kgthUe0y7K1bfcYmgLuQf41/adsj/5enddlSxzkJavl2EwOu+r1g==}
+ engines: {node: '>= 0.10.4'}
+ hasBin: true
+ dependencies:
+ '@devicefarmer/adbkit-logcat': 2.1.3
+ '@devicefarmer/adbkit-monkey': 1.2.1
+ bluebird: 3.7.2
+ commander: 9.5.0
+ debug: 4.3.4
+ node-forge: 1.3.1
+ split: 1.0.1
+ transitivePeerDependencies:
+ - supports-color
dev: true
- /@emotion/is-prop-valid/0.8.8:
+ /@emotion/is-prop-valid@0.8.8:
resolution: {integrity: sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==}
dependencies:
'@emotion/memoize': 0.7.4
dev: true
- /@emotion/memoize/0.7.4:
+ /@emotion/is-prop-valid@1.2.1:
+ resolution: {integrity: sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==}
+ dependencies:
+ '@emotion/memoize': 0.8.1
+ dev: true
+
+ /@emotion/memoize@0.7.4:
resolution: {integrity: sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==}
dev: true
- /@esbuild/android-arm/0.15.12:
- resolution: {integrity: sha512-IC7TqIqiyE0MmvAhWkl/8AEzpOtbhRNDo7aph47We1NbE5w2bt/Q+giAhe0YYeVpYnIhGMcuZY92qDK6dQauvA==}
+ /@emotion/memoize@0.8.1:
+ resolution: {integrity: sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==}
+ dev: true
+
+ /@esbuild/android-arm64@0.19.9:
+ resolution: {integrity: sha512-q4cR+6ZD0938R19MyEW3jEsMzbb/1rulLXiNAJQADD/XYp7pT+rOS5JGxvpRW8dFDEfjW4wLgC/3FXIw4zYglQ==}
engines: {node: '>=12'}
- cpu: [arm]
+ cpu: [arm64]
os: [android]
requiresBuild: true
dev: true
optional: true
- /@esbuild/android-arm/0.15.13:
- resolution: {integrity: sha512-RY2fVI8O0iFUNvZirXaQ1vMvK0xhCcl0gqRj74Z6yEiO1zAUa7hbsdwZM1kzqbxHK7LFyMizipfXT3JME+12Hw==}
+ /@esbuild/android-arm@0.19.9:
+ resolution: {integrity: sha512-jkYjjq7SdsWuNI6b5quymW0oC83NN5FdRPuCbs9HZ02mfVdAP8B8eeqLSYU3gb6OJEaY5CQabtTFbqBf26H3GA==}
engines: {node: '>=12'}
cpu: [arm]
os: [android]
@@ -3355,26 +5027,80 @@ packages:
dev: true
optional: true
- /@esbuild/linux-loong64/0.14.54:
- resolution: {integrity: sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==}
+ /@esbuild/android-x64@0.19.9:
+ resolution: {integrity: sha512-KOqoPntWAH6ZxDwx1D6mRntIgZh9KodzgNOy5Ebt9ghzffOk9X2c1sPwtM9P+0eXbefnDhqYfkh5PLP5ULtWFA==}
engines: {node: '>=12'}
- cpu: [loong64]
+ cpu: [x64]
+ os: [android]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/darwin-arm64@0.19.9:
+ resolution: {integrity: sha512-KBJ9S0AFyLVx2E5D8W0vExqRW01WqRtczUZ8NRu+Pi+87opZn5tL4Y0xT0mA4FtHctd0ZgwNoN639fUUGlNIWw==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [darwin]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/darwin-x64@0.19.9:
+ resolution: {integrity: sha512-vE0VotmNTQaTdX0Q9dOHmMTao6ObjyPm58CHZr1UK7qpNleQyxlFlNCaHsHx6Uqv86VgPmR4o2wdNq3dP1qyDQ==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [darwin]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/freebsd-arm64@0.19.9:
+ resolution: {integrity: sha512-uFQyd/o1IjiEk3rUHSwUKkqZwqdvuD8GevWF065eqgYfexcVkxh+IJgwTaGZVu59XczZGcN/YMh9uF1fWD8j1g==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [freebsd]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/freebsd-x64@0.19.9:
+ resolution: {integrity: sha512-WMLgWAtkdTbTu1AWacY7uoj/YtHthgqrqhf1OaEWnZb7PQgpt8eaA/F3LkV0E6K/Lc0cUr/uaVP/49iE4M4asA==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [freebsd]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/linux-arm64@0.19.9:
+ resolution: {integrity: sha512-PiPblfe1BjK7WDAKR1Cr9O7VVPqVNpwFcPWgfn4xu0eMemzRp442hXyzF/fSwgrufI66FpHOEJk0yYdPInsmyQ==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
os: [linux]
requiresBuild: true
dev: true
optional: true
- /@esbuild/linux-loong64/0.15.12:
- resolution: {integrity: sha512-tZEowDjvU7O7I04GYvWQOS4yyP9E/7YlsB0jjw1Ycukgr2ycEzKyIk5tms5WnLBymaewc6VmRKnn5IJWgK4eFw==}
+ /@esbuild/linux-arm@0.19.9:
+ resolution: {integrity: sha512-C/ChPohUYoyUaqn1h17m/6yt6OB14hbXvT8EgM1ZWaiiTYz7nWZR0SYmMnB5BzQA4GXl3BgBO1l8MYqL/He3qw==}
engines: {node: '>=12'}
- cpu: [loong64]
+ cpu: [arm]
os: [linux]
requiresBuild: true
dev: true
optional: true
- /@esbuild/linux-loong64/0.15.13:
- resolution: {integrity: sha512-+BoyIm4I8uJmH/QDIH0fu7MG0AEx9OXEDXnqptXCwKOlOqZiS4iraH1Nr7/ObLMokW3sOCeBNyD68ATcV9b9Ag==}
+ /@esbuild/linux-ia32@0.19.9:
+ resolution: {integrity: sha512-f37i/0zE0MjDxijkPSQw1CO/7C27Eojqb+r3BbHVxMLkj8GCa78TrBZzvPyA/FNLUMzP3eyHCVkAopkKVja+6Q==}
+ engines: {node: '>=12'}
+ cpu: [ia32]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/linux-loong64@0.19.9:
+ resolution: {integrity: sha512-t6mN147pUIf3t6wUt3FeumoOTPfmv9Cc6DQlsVBpB7eCpLOqQDyWBP1ymXn1lDw4fNUSb/gBcKAmvTP49oIkaA==}
engines: {node: '>=12'}
cpu: [loong64]
os: [linux]
@@ -3382,14 +5108,128 @@ packages:
dev: true
optional: true
- /@eslint/eslintrc/0.4.3:
+ /@esbuild/linux-mips64el@0.19.9:
+ resolution: {integrity: sha512-jg9fujJTNTQBuDXdmAg1eeJUL4Jds7BklOTkkH80ZgQIoCTdQrDaHYgbFZyeTq8zbY+axgptncko3v9p5hLZtw==}
+ engines: {node: '>=12'}
+ cpu: [mips64el]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/linux-ppc64@0.19.9:
+ resolution: {integrity: sha512-tkV0xUX0pUUgY4ha7z5BbDS85uI7ABw3V1d0RNTii7E9lbmV8Z37Pup2tsLV46SQWzjOeyDi1Q7Wx2+QM8WaCQ==}
+ engines: {node: '>=12'}
+ cpu: [ppc64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/linux-riscv64@0.19.9:
+ resolution: {integrity: sha512-DfLp8dj91cufgPZDXr9p3FoR++m3ZJ6uIXsXrIvJdOjXVREtXuQCjfMfvmc3LScAVmLjcfloyVtpn43D56JFHg==}
+ engines: {node: '>=12'}
+ cpu: [riscv64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/linux-s390x@0.19.9:
+ resolution: {integrity: sha512-zHbglfEdC88KMgCWpOl/zc6dDYJvWGLiUtmPRsr1OgCViu3z5GncvNVdf+6/56O2Ca8jUU+t1BW261V6kp8qdw==}
+ engines: {node: '>=12'}
+ cpu: [s390x]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/linux-x64@0.19.9:
+ resolution: {integrity: sha512-JUjpystGFFmNrEHQnIVG8hKwvA2DN5o7RqiO1CVX8EN/F/gkCjkUMgVn6hzScpwnJtl2mPR6I9XV1oW8k9O+0A==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/netbsd-x64@0.19.9:
+ resolution: {integrity: sha512-GThgZPAwOBOsheA2RUlW5UeroRfESwMq/guy8uEe3wJlAOjpOXuSevLRd70NZ37ZrpO6RHGHgEHvPg1h3S1Jug==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [netbsd]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/openbsd-x64@0.19.9:
+ resolution: {integrity: sha512-Ki6PlzppaFVbLnD8PtlVQfsYw4S9n3eQl87cqgeIw+O3sRr9IghpfSKY62mggdt1yCSZ8QWvTZ9jo9fjDSg9uw==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [openbsd]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/sunos-x64@0.19.9:
+ resolution: {integrity: sha512-MLHj7k9hWh4y1ddkBpvRj2b9NCBhfgBt3VpWbHQnXRedVun/hC7sIyTGDGTfsGuXo4ebik2+3ShjcPbhtFwWDw==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [sunos]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/win32-arm64@0.19.9:
+ resolution: {integrity: sha512-GQoa6OrQ8G08guMFgeXPH7yE/8Dt0IfOGWJSfSH4uafwdC7rWwrfE6P9N8AtPGIjUzdo2+7bN8Xo3qC578olhg==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [win32]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/win32-ia32@0.19.9:
+ resolution: {integrity: sha512-UOozV7Ntykvr5tSOlGCrqU3NBr3d8JqPes0QWN2WOXfvkWVGRajC+Ym0/Wj88fUgecUCLDdJPDF0Nna2UK3Qtg==}
+ engines: {node: '>=12'}
+ cpu: [ia32]
+ os: [win32]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/win32-x64@0.19.9:
+ resolution: {integrity: sha512-oxoQgglOP7RH6iasDrhY+R/3cHrfwIDvRlT4CGChflq6twk8iENeVvMJjmvBb94Ik1Z+93iGO27err7w6l54GQ==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [win32]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@eslint-community/eslint-utils@4.4.0(eslint@8.56.0):
+ resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ peerDependencies:
+ eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
+ dependencies:
+ eslint: 8.56.0
+ eslint-visitor-keys: 3.4.3
+ dev: true
+
+ /@eslint-community/regexpp@4.10.0:
+ resolution: {integrity: sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==}
+ engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
+ dev: true
+
+ /@eslint/eslintrc@0.4.3:
resolution: {integrity: sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==}
engines: {node: ^10.12.0 || >=12.0.0}
dependencies:
ajv: 6.12.6
debug: 4.3.4
espree: 7.3.1
- globals: 13.17.0
+ globals: 13.21.0
ignore: 4.0.6
import-fresh: 3.3.0
js-yaml: 3.14.1
@@ -3399,14 +5239,14 @@ packages:
- supports-color
dev: true
- /@eslint/eslintrc/1.3.3:
+ /@eslint/eslintrc@1.3.3:
resolution: {integrity: sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dependencies:
ajv: 6.12.6
debug: 4.3.4
espree: 9.4.0
- globals: 13.17.0
+ globals: 13.21.0
ignore: 5.2.0
import-fresh: 3.3.0
js-yaml: 4.1.0
@@ -3416,360 +5256,219 @@ packages:
- supports-color
dev: true
- /@gar/promisify/1.1.3:
- resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==}
- dev: true
-
- /@humanwhocodes/config-array/0.11.6:
- resolution: {integrity: sha512-jJr+hPTJYKyDILJfhNSHsjiwXYf26Flsz8DvNndOsHs5pwSnpGUEy8yzF0JYhCEvTDdV2vuOK5tt8BVhwO5/hg==}
- engines: {node: '>=10.10.0'}
+ /@eslint/eslintrc@2.1.4:
+ resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dependencies:
- '@humanwhocodes/object-schema': 1.2.1
+ ajv: 6.12.6
debug: 4.3.4
+ espree: 9.6.1
+ globals: 13.24.0
+ ignore: 5.3.0
+ import-fresh: 3.3.0
+ js-yaml: 4.1.0
minimatch: 3.1.2
+ strip-json-comments: 3.1.1
transitivePeerDependencies:
- supports-color
dev: true
- /@humanwhocodes/config-array/0.5.0:
- resolution: {integrity: sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==}
- engines: {node: '>=10.10.0'}
- dependencies:
- '@humanwhocodes/object-schema': 1.2.1
- debug: 4.3.4
- minimatch: 3.1.2
- transitivePeerDependencies:
- - supports-color
+ /@eslint/js@8.56.0:
+ resolution: {integrity: sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dev: true
- /@humanwhocodes/module-importer/1.0.1:
- resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
- engines: {node: '>=12.22'}
+ /@fluent/syntax@0.19.0:
+ resolution: {integrity: sha512-5D2qVpZrgpjtqU4eNOcWGp1gnUCgjfM+vKGE2y03kKN6z5EBhtx0qdRFbg8QuNNj8wXNoX93KJoYb+NqoxswmQ==}
+ engines: {node: '>=14.0.0', npm: '>=7.0.0'}
dev: true
- /@humanwhocodes/object-schema/1.2.1:
- resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==}
+ /@gar/promisify@1.1.3:
+ resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==}
dev: true
- /@istanbuljs/load-nyc-config/1.1.0:
- resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==}
- engines: {node: '>=8'}
+ /@gnu-taler/pogen@0.0.5:
+ resolution: {integrity: sha512-hd+05sHcYySMY3DUKKxw1eyboWhwQpPr0puGqdsepqXfjAwPyyFzVzF1fnPFc5w/jbn5Wm8ByCB2jEiX24fOqg==}
dependencies:
- camelcase: 5.3.1
- find-up: 4.1.0
- get-package-type: 0.1.0
- js-yaml: 3.14.1
- resolve-from: 5.0.0
+ '@types/node': 14.18.63
dev: true
- /@istanbuljs/schema/0.1.3:
- resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==}
- engines: {node: '>=8'}
- dev: true
-
- /@jest/console/26.6.2:
- resolution: {integrity: sha512-IY1R2i2aLsLr7Id3S6p2BA82GNWryt4oSvEXLAKc+L2zdi89dSkE8xC1C+0kpATG4JhBJREnQOH7/zmccM2B0g==}
- engines: {node: '>= 10.14.2'}
+ /@headlessui/react@1.7.14(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-znzdq9PG8rkwcu9oQ2FwIy0ZFtP9Z7ycS+BAqJ3R5EIqC/0bJGvhT7193rFf+45i9nnPsYvCQVW4V/bB9Xc+gA==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ react: ^16 || ^17 || ^18
+ react-dom: ^16 || ^17 || ^18
dependencies:
- '@jest/types': 26.6.2
- '@types/node': 18.11.9
- chalk: 4.1.2
- jest-message-util: 26.6.2
- jest-util: 26.6.2
- slash: 3.0.0
- dev: true
+ client-only: 0.0.1
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ dev: false
- /@jest/console/27.5.1:
- resolution: {integrity: sha512-kZ/tNpS3NXn0mlXXXPNuDZnb4c0oZ20r4K5eemM2k30ZC3G0T02nXUvyhf5YdbXWHPEJLc9qGLxEZ216MdL+Zg==}
- engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
+ /@heroicons/react@2.0.17(react@18.2.0):
+ resolution: {integrity: sha512-90GMZktkA53YbNzHp6asVEDevUQCMtxWH+2UK2S8OpnLEu7qckTJPhNxNQG52xIR1WFTwFqtH6bt7a60ZNcLLA==}
+ peerDependencies:
+ react: '>= 16'
dependencies:
- '@jest/types': 27.5.1
- '@types/node': 18.11.9
- chalk: 4.1.2
- jest-message-util: 27.5.1
- jest-util: 27.5.1
- slash: 3.0.0
- dev: true
+ react: 18.2.0
- /@jest/core/26.6.3:
- resolution: {integrity: sha512-xvV1kKbhfUqFVuZ8Cyo+JPpipAHHAV3kcDBftiduK8EICXmTFddryy3P7NfZt8Pv37rA9nEJBKCCkglCPt/Xjw==}
- engines: {node: '>= 10.14.2'}
+ /@humanwhocodes/config-array@0.11.13:
+ resolution: {integrity: sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==}
+ engines: {node: '>=10.10.0'}
dependencies:
- '@jest/console': 26.6.2
- '@jest/reporters': 26.6.2
- '@jest/test-result': 26.6.2
- '@jest/transform': 26.6.2
- '@jest/types': 26.6.2
- '@types/node': 18.11.9
- ansi-escapes: 4.3.2
- chalk: 4.1.2
- exit: 0.1.2
- graceful-fs: 4.2.10
- jest-changed-files: 26.6.2
- jest-config: 26.6.3
- jest-haste-map: 26.6.2
- jest-message-util: 26.6.2
- jest-regex-util: 26.0.0
- jest-resolve: 26.6.2
- jest-resolve-dependencies: 26.6.3
- jest-runner: 26.6.3
- jest-runtime: 26.6.3
- jest-snapshot: 26.6.2
- jest-util: 26.6.2
- jest-validate: 26.6.2
- jest-watcher: 26.6.2
- micromatch: 4.0.5
- p-each-series: 2.2.0
- rimraf: 3.0.2
- slash: 3.0.0
- strip-ansi: 6.0.1
+ '@humanwhocodes/object-schema': 2.0.1
+ debug: 4.3.4
+ minimatch: 3.1.2
transitivePeerDependencies:
- - bufferutil
- - canvas
- supports-color
- - ts-node
- - utf-8-validate
dev: true
- /@jest/environment/26.6.2:
- resolution: {integrity: sha512-nFy+fHl28zUrRsCeMB61VDThV1pVTtlEokBRgqPrcT1JNq4yRNIyTHfyht6PqtUvY9IsuLGTrbG8kPXjSZIZwA==}
- engines: {node: '>= 10.14.2'}
- dependencies:
- '@jest/fake-timers': 26.6.2
- '@jest/types': 26.6.2
- '@types/node': 18.11.9
- jest-mock: 26.6.2
- dev: true
-
- /@jest/fake-timers/26.6.2:
- resolution: {integrity: sha512-14Uleatt7jdzefLPYM3KLcnUl1ZNikaKq34enpb5XG9i81JpppDb5muZvonvKyrl7ftEHkKS5L5/eB/kxJ+bvA==}
- engines: {node: '>= 10.14.2'}
- dependencies:
- '@jest/types': 26.6.2
- '@sinonjs/fake-timers': 6.0.1
- '@types/node': 18.11.9
- jest-message-util: 26.6.2
- jest-mock: 26.6.2
- jest-util: 26.6.2
- dev: true
-
- /@jest/globals/26.6.2:
- resolution: {integrity: sha512-85Ltnm7HlB/KesBUuALwQ68YTU72w9H2xW9FjZ1eL1U3lhtefjjl5c2MiUbpXt/i6LaPRvoOFJ22yCBSfQ0JIA==}
- engines: {node: '>= 10.14.2'}
- dependencies:
- '@jest/environment': 26.6.2
- '@jest/types': 26.6.2
- expect: 26.6.2
- dev: true
-
- /@jest/reporters/26.6.2:
- resolution: {integrity: sha512-h2bW53APG4HvkOnVMo8q3QXa6pcaNt1HkwVsOPMBV6LD/q9oSpxNSYZQYkAnjdMjrJ86UuYeLo+aEZClV6opnw==}
- engines: {node: '>= 10.14.2'}
+ /@humanwhocodes/config-array@0.11.6:
+ resolution: {integrity: sha512-jJr+hPTJYKyDILJfhNSHsjiwXYf26Flsz8DvNndOsHs5pwSnpGUEy8yzF0JYhCEvTDdV2vuOK5tt8BVhwO5/hg==}
+ engines: {node: '>=10.10.0'}
dependencies:
- '@bcoe/v8-coverage': 0.2.3
- '@jest/console': 26.6.2
- '@jest/test-result': 26.6.2
- '@jest/transform': 26.6.2
- '@jest/types': 26.6.2
- chalk: 4.1.2
- collect-v8-coverage: 1.0.1
- exit: 0.1.2
- glob: 7.2.3
- graceful-fs: 4.2.10
- istanbul-lib-coverage: 3.2.0
- istanbul-lib-instrument: 4.0.3
- istanbul-lib-report: 3.0.0
- istanbul-lib-source-maps: 4.0.1
- istanbul-reports: 3.1.5
- jest-haste-map: 26.6.2
- jest-resolve: 26.6.2
- jest-util: 26.6.2
- jest-worker: 26.6.2
- slash: 3.0.0
- source-map: 0.6.1
- string-length: 4.0.2
- terminal-link: 2.1.1
- v8-to-istanbul: 7.1.2
- optionalDependencies:
- node-notifier: 8.0.2
+ '@humanwhocodes/object-schema': 1.2.1
+ debug: 4.3.4
+ minimatch: 3.1.2
transitivePeerDependencies:
- supports-color
dev: true
- /@jest/source-map/26.6.2:
- resolution: {integrity: sha512-YwYcCwAnNmOVsZ8mr3GfnzdXDAl4LaenZP5z+G0c8bzC9/dugL8zRmxZzdoTl4IaS3CryS1uWnROLPFmb6lVvA==}
- engines: {node: '>= 10.14.2'}
- dependencies:
- callsites: 3.1.0
- graceful-fs: 4.2.10
- source-map: 0.6.1
- dev: true
-
- /@jest/test-result/26.6.2:
- resolution: {integrity: sha512-5O7H5c/7YlojphYNrK02LlDIV2GNPYisKwHm2QTKjNZeEzezCbwYs9swJySv2UfPMyZ0VdsmMv7jIlD/IKYQpQ==}
- engines: {node: '>= 10.14.2'}
+ /@humanwhocodes/config-array@0.5.0:
+ resolution: {integrity: sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==}
+ engines: {node: '>=10.10.0'}
dependencies:
- '@jest/console': 26.6.2
- '@jest/types': 26.6.2
- '@types/istanbul-lib-coverage': 2.0.4
- collect-v8-coverage: 1.0.1
+ '@humanwhocodes/object-schema': 1.2.1
+ debug: 4.3.4
+ minimatch: 3.1.2
+ transitivePeerDependencies:
+ - supports-color
dev: true
- /@jest/test-result/27.5.1:
- resolution: {integrity: sha512-EW35l2RYFUcUQxFJz5Cv5MTOxlJIQs4I7gxzi2zVU7PJhOwfYq1MdC5nhSmYjX1gmMmLPvB3sIaC+BkcHRBfag==}
- engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
- dependencies:
- '@jest/console': 27.5.1
- '@jest/types': 27.5.1
- '@types/istanbul-lib-coverage': 2.0.4
- collect-v8-coverage: 1.0.1
+ /@humanwhocodes/module-importer@1.0.1:
+ resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
+ engines: {node: '>=12.22'}
dev: true
- /@jest/test-sequencer/26.6.3:
- resolution: {integrity: sha512-YHlVIjP5nfEyjlrSr8t/YdNfU/1XEt7c5b4OxcXCjyRhjzLYu/rO69/WHPuYcbCWkz8kAeZVZp2N2+IOLLEPGw==}
- engines: {node: '>= 10.14.2'}
- dependencies:
- '@jest/test-result': 26.6.2
- graceful-fs: 4.2.10
- jest-haste-map: 26.6.2
- jest-runner: 26.6.3
- jest-runtime: 26.6.3
- transitivePeerDependencies:
- - bufferutil
- - canvas
- - supports-color
- - ts-node
- - utf-8-validate
+ /@humanwhocodes/object-schema@1.2.1:
+ resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==}
dev: true
- /@jest/transform/26.6.2:
- resolution: {integrity: sha512-E9JjhUgNzvuQ+vVAL21vlyfy12gP0GhazGgJC4h6qUt1jSdUXGWJ1wfu/X7Sd8etSgxV4ovT1pb9v5D6QW4XgA==}
- engines: {node: '>= 10.14.2'}
- dependencies:
- '@babel/core': 7.18.9
- '@jest/types': 26.6.2
- babel-plugin-istanbul: 6.1.1
- chalk: 4.1.2
- convert-source-map: 1.9.0
- fast-json-stable-stringify: 2.1.0
- graceful-fs: 4.2.10
- jest-haste-map: 26.6.2
- jest-regex-util: 26.0.0
- jest-util: 26.6.2
- micromatch: 4.0.5
- pirates: 4.0.5
- slash: 3.0.0
- source-map: 0.6.1
- write-file-atomic: 3.0.3
- transitivePeerDependencies:
- - supports-color
+ /@humanwhocodes/object-schema@2.0.1:
+ resolution: {integrity: sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==}
dev: true
- /@jest/transform/27.5.1:
- resolution: {integrity: sha512-ipON6WtYgl/1329g5AIJVbUuEh0wZVbdpGwC99Jw4LwuoBNS95MVphU6zOeD9pDkon+LLbFL7lOQRapbB8SCHw==}
- engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
+ /@isaacs/cliui@8.0.2:
+ resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
+ engines: {node: '>=12'}
dependencies:
- '@babel/core': 7.18.9
- '@jest/types': 27.5.1
- babel-plugin-istanbul: 6.1.1
- chalk: 4.1.2
- convert-source-map: 1.9.0
- fast-json-stable-stringify: 2.1.0
- graceful-fs: 4.2.10
- jest-haste-map: 27.5.1
- jest-regex-util: 27.5.1
- jest-util: 27.5.1
- micromatch: 4.0.5
- pirates: 4.0.5
- slash: 3.0.0
- source-map: 0.6.1
- write-file-atomic: 3.0.3
- transitivePeerDependencies:
- - supports-color
- dev: true
+ string-width: 5.1.2
+ string-width-cjs: /string-width@4.2.3
+ strip-ansi: 7.1.0
+ strip-ansi-cjs: /strip-ansi@6.0.1
+ wrap-ansi: 8.1.0
+ wrap-ansi-cjs: /wrap-ansi@7.0.0
- /@jest/types/26.6.2:
- resolution: {integrity: sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==}
- engines: {node: '>= 10.14.2'}
+ /@istanbuljs/load-nyc-config@1.1.0:
+ resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==}
+ engines: {node: '>=8'}
dependencies:
- '@types/istanbul-lib-coverage': 2.0.4
- '@types/istanbul-reports': 3.0.1
- '@types/node': 18.11.9
- '@types/yargs': 15.0.14
- chalk: 4.1.2
+ camelcase: 5.3.1
+ find-up: 4.1.0
+ get-package-type: 0.1.0
+ js-yaml: 3.14.1
+ resolve-from: 5.0.0
dev: true
- /@jest/types/27.5.1:
- resolution: {integrity: sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==}
- engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
- dependencies:
- '@types/istanbul-lib-coverage': 2.0.4
- '@types/istanbul-reports': 3.0.1
- '@types/node': 18.11.9
- '@types/yargs': 16.0.4
- chalk: 4.1.2
+ /@istanbuljs/schema@0.1.3:
+ resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==}
+ engines: {node: '>=8'}
dev: true
- /@jridgewell/gen-mapping/0.1.1:
+ /@jridgewell/gen-mapping@0.1.1:
resolution: {integrity: sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==}
engines: {node: '>=6.0.0'}
dependencies:
'@jridgewell/set-array': 1.1.2
- '@jridgewell/sourcemap-codec': 1.4.14
- dev: true
+ '@jridgewell/sourcemap-codec': 1.4.15
- /@jridgewell/gen-mapping/0.3.2:
+ /@jridgewell/gen-mapping@0.3.2:
resolution: {integrity: sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==}
engines: {node: '>=6.0.0'}
dependencies:
'@jridgewell/set-array': 1.1.2
- '@jridgewell/sourcemap-codec': 1.4.14
- '@jridgewell/trace-mapping': 0.3.17
+ '@jridgewell/sourcemap-codec': 1.4.15
+ '@jridgewell/trace-mapping': 0.3.19
dev: true
- /@jridgewell/resolve-uri/3.1.0:
- resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==}
+ /@jridgewell/gen-mapping@0.3.3:
+ resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==}
+ engines: {node: '>=6.0.0'}
+ dependencies:
+ '@jridgewell/set-array': 1.1.2
+ '@jridgewell/sourcemap-codec': 1.4.15
+ '@jridgewell/trace-mapping': 0.3.19
+
+ /@jridgewell/resolve-uri@3.1.1:
+ resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==}
engines: {node: '>=6.0.0'}
- dev: true
- /@jridgewell/set-array/1.1.2:
+ /@jridgewell/set-array@1.1.2:
resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==}
engines: {node: '>=6.0.0'}
- dev: true
- /@jridgewell/source-map/0.3.2:
+ /@jridgewell/source-map@0.3.2:
resolution: {integrity: sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==}
dependencies:
- '@jridgewell/gen-mapping': 0.3.2
- '@jridgewell/trace-mapping': 0.3.17
+ '@jridgewell/gen-mapping': 0.3.3
+ '@jridgewell/trace-mapping': 0.3.19
dev: true
- /@jridgewell/sourcemap-codec/1.4.14:
- resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==}
+ /@jridgewell/source-map@0.3.5:
+ resolution: {integrity: sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==}
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.3
+ '@jridgewell/trace-mapping': 0.3.20
dev: true
- /@jridgewell/trace-mapping/0.3.17:
- resolution: {integrity: sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==}
+ /@jridgewell/sourcemap-codec@1.4.15:
+ resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==}
+
+ /@jridgewell/trace-mapping@0.3.19:
+ resolution: {integrity: sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==}
dependencies:
- '@jridgewell/resolve-uri': 3.1.0
- '@jridgewell/sourcemap-codec': 1.4.14
+ '@jridgewell/resolve-uri': 3.1.1
+ '@jridgewell/sourcemap-codec': 1.4.15
+
+ /@jridgewell/trace-mapping@0.3.20:
+ resolution: {integrity: sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==}
+ dependencies:
+ '@jridgewell/resolve-uri': 3.1.1
+ '@jridgewell/sourcemap-codec': 1.4.15
dev: true
- /@leichtgewicht/ip-codec/2.0.4:
+ /@jridgewell/trace-mapping@0.3.9:
+ resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
+ dependencies:
+ '@jridgewell/resolve-uri': 3.1.1
+ '@jridgewell/sourcemap-codec': 1.4.15
+ dev: true
+
+ /@leichtgewicht/ip-codec@2.0.4:
resolution: {integrity: sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==}
dev: true
- /@linaria/babel-preset/3.0.0-beta.22:
+ /@linaria/babel-preset@3.0.0-beta.22:
resolution: {integrity: sha512-0Igie3stlsKT+XKYdhW7Yy8MNg2gZ5ShOosgTkpz3Agwtz+Osgb/4JNMnqGlAUaVULtMpKLc7mF/NbbhAEEF3Q==}
engines: {node: ^12.16.0 || >=13.7.0}
dependencies:
'@babel/core': 7.18.9
- '@babel/generator': 7.19.6
- '@babel/plugin-proposal-export-namespace-from': 7.18.9_@babel+core@7.18.9
- '@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.18.9
- '@babel/plugin-transform-modules-commonjs': 7.19.6_@babel+core@7.18.9
- '@babel/template': 7.18.10
- '@babel/traverse': 7.19.6
+ '@babel/generator': 7.21.5
+ '@babel/plugin-proposal-export-namespace-from': 7.18.9(@babel/core@7.18.9)
+ '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.18.9)
+ '@babel/plugin-transform-modules-commonjs': 7.19.6(@babel/core@7.18.9)
+ '@babel/template': 7.20.7
+ '@babel/traverse': 7.21.5
'@linaria/core': 3.0.0-beta.22
'@linaria/logger': 3.0.0-beta.20
'@linaria/utils': 3.0.0-beta.20
@@ -3781,17 +5480,17 @@ packages:
- supports-color
dev: true
- /@linaria/babel-preset/3.0.0-beta.23:
+ /@linaria/babel-preset@3.0.0-beta.23:
resolution: {integrity: sha512-NhxUZokEq12RLpDo4v/f59dB9A/1BbLgGLFotnrDzNBHfylm0qXSIIel68pZOXUB5lVdPJHqZWcT2zxbpGW6fA==}
engines: {node: ^12.16.0 || >=13.7.0}
dependencies:
- '@babel/core': 7.19.6
- '@babel/generator': 7.19.6
- '@babel/plugin-proposal-export-namespace-from': 7.18.9_@babel+core@7.19.6
- '@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.19.6
- '@babel/plugin-transform-modules-commonjs': 7.19.6_@babel+core@7.19.6
- '@babel/template': 7.18.10
- '@babel/traverse': 7.19.6
+ '@babel/core': 7.18.9
+ '@babel/generator': 7.23.5
+ '@babel/plugin-proposal-export-namespace-from': 7.18.9(@babel/core@7.18.9)
+ '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.18.9)
+ '@babel/plugin-transform-modules-commonjs': 7.23.3(@babel/core@7.18.9)
+ '@babel/template': 7.22.15
+ '@babel/traverse': 7.23.5
'@linaria/core': 3.0.0-beta.22
'@linaria/logger': 3.0.0-beta.20
'@linaria/utils': 3.0.0-beta.20
@@ -3803,7 +5502,31 @@ packages:
- supports-color
dev: true
- /@linaria/core/3.0.0-beta.22:
+ /@linaria/babel-preset@5.0.4:
+ resolution: {integrity: sha512-OMhlD6gc/+6DFLkadoavlxCtTIElo/UdDMeQH6I/CAL3hgfmHyIXJPrGObTa7jvQKddUaKvFIHGAVB7pz6J8Qg==}
+ engines: {node: '>=16.0.0'}
+ dependencies:
+ '@babel/core': 7.23.5
+ '@babel/generator': 7.23.5
+ '@babel/helper-module-imports': 7.22.15
+ '@babel/template': 7.22.15
+ '@babel/traverse': 7.23.5
+ '@babel/types': 7.23.5
+ '@linaria/core': 5.0.2
+ '@linaria/logger': 5.0.0
+ '@linaria/shaker': 5.0.3
+ '@linaria/tags': 5.0.2
+ '@linaria/utils': 5.0.2
+ cosmiconfig: 8.1.3
+ happy-dom: 10.8.0
+ source-map: 0.7.4
+ stylis: 3.5.4
+ ts-invariant: 0.10.3
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@linaria/core@3.0.0-beta.22:
resolution: {integrity: sha512-BPSecW8QmhQ0y+5cWXEja+MTmLsuo0T1PjqRlSWsmDgjJFFObqCnPEgbR1KNtQb3Msmx1/9q3dYKpA5Zk3g8KQ==}
engines: {node: ^12.16.0 || >=13.7.0}
dependencies:
@@ -3813,18 +5536,43 @@ packages:
- supports-color
dev: true
- /@linaria/esbuild/3.0.0-beta.23:
+ /@linaria/core@5.0.2:
+ resolution: {integrity: sha512-l5jQq7w9kDvonfr/0MBF47Dagx9Y9f/o5Q8j3zv7GepwG/yHQdbjKr0tq07rx2fSDDX7Nbqlxk6k9Ymir/NGpg==}
+ engines: {node: '>=16.0.0'}
+ dependencies:
+ '@linaria/logger': 5.0.0
+ '@linaria/tags': 5.0.2
+ '@linaria/utils': 5.0.2
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@linaria/esbuild@3.0.0-beta.23:
resolution: {integrity: sha512-5hYMPSXo/dsRwq/UszRa17vL5mARa9e/+qKkG8loju0DNN/73kyVkGRcVHw+EMftXnY2x3y7RIJ3E8fWtFvDfg==}
engines: {node: ^12.16.0 || >=13.7.0}
dependencies:
- '@babel/core': 7.19.6
+ '@babel/core': 7.22.1
'@linaria/babel-preset': 3.0.0-beta.23
esbuild: 0.12.29
transitivePeerDependencies:
- supports-color
dev: true
- /@linaria/logger/3.0.0-beta.20:
+ /@linaria/esbuild@5.0.4(esbuild@0.19.9):
+ resolution: {integrity: sha512-sIPxeH3TQrIfNBz3wCtxTcu/M5dS2SOBSFps+3EVz1LOkIdy5YAOSWL1i1KWUavSg1cs467Ujxq9Nu79k1SayQ==}
+ engines: {node: '>=16.0.0'}
+ peerDependencies:
+ esbuild: '>=0.12.0'
+ dependencies:
+ '@babel/core': 7.23.5
+ '@linaria/babel-preset': 5.0.4
+ '@linaria/utils': 5.0.2
+ esbuild: 0.19.9
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@linaria/logger@3.0.0-beta.20:
resolution: {integrity: sha512-wCxWnldCHf7HXdLG3QtbKyBur+z5V1qZTouSEvcVYDfd4aSRPOi/jLdwsZlsUq2PFGpA3jW6JnreZJ/vxuEl7g==}
engines: {node: ^12.16.0 || >=13.7.0}
dependencies:
@@ -3834,7 +5582,17 @@ packages:
- supports-color
dev: true
- /@linaria/preeval/3.0.0-beta.23:
+ /@linaria/logger@5.0.0:
+ resolution: {integrity: sha512-PZd5H0I4F84U0kXSE+vD75ltIGDxEA6gMDNWS2aDZFitmdlQM5rIXqvKFrp5NsHa7a3AH+I2Hxm0dg60WZF7vg==}
+ engines: {node: '>=16.0.0'}
+ dependencies:
+ debug: 4.3.4
+ picocolors: 1.0.0
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@linaria/preeval@3.0.0-beta.23:
resolution: {integrity: sha512-TAIN6GPFCahoIH3FHsHk5NM+iaBp5Snniqirk8mailjGGprswl14Z7lgGPzzKZiA5HBzWKW4Wpe1N8W2vSjJTw==}
engines: {node: ^12.16.0 || >=13.7.0}
dependencies:
@@ -3843,7 +5601,7 @@ packages:
- supports-color
dev: true
- /@linaria/react/3.0.0-beta.22:
+ /@linaria/react@3.0.0-beta.22(react@18.2.0):
resolution: {integrity: sha512-14rnb/zkzhFhJM3hbBOzLLS0bu01mOg23Rv2nGQUt5CWd+HOhksmqzqBtC/ijeVlY2hRI0rJJcng9r07LGGAPA==}
engines: {node: ^12.16.0 || >=13.7.0}
peerDependencies:
@@ -3851,30 +5609,39 @@ packages:
dependencies:
'@emotion/is-prop-valid': 0.8.8
'@linaria/core': 3.0.0-beta.22
+ react: 18.2.0
ts-invariant: 0.10.3
transitivePeerDependencies:
- supports-color
dev: true
- /@linaria/rollup/3.0.0-beta.22:
- resolution: {integrity: sha512-VSMOvDsuAC/GoIwt111v6T7NlK5xvwVi5sK6OUNBCAZLdBai72uPKQKniye6qLMsI9SUfzPbktjTPVP/Uffweg==}
- engines: {node: ^12.16.0 || >=13.7.0}
+ /@linaria/react@5.0.3(react@18.2.0):
+ resolution: {integrity: sha512-faTQHnUlrAz0Lodu+rr6Yx59rX0nqFOrZ5/IdlfQcTRz9VebyVL4vtA3AOecmn1YFZjMpqjopT0OzNz6GknQSg==}
+ engines: {node: '>=16.0.0'}
+ peerDependencies:
+ react: '>=16'
dependencies:
- '@linaria/babel-preset': 3.0.0-beta.22
- '@rollup/pluginutils': 4.2.1
+ '@emotion/is-prop-valid': 1.2.1
+ '@linaria/core': 5.0.2
+ '@linaria/tags': 5.0.2
+ '@linaria/utils': 5.0.2
+ minimatch: 9.0.3
+ react: 18.2.0
+ react-html-attributes: 1.4.6
+ ts-invariant: 0.10.3
transitivePeerDependencies:
- supports-color
dev: true
- /@linaria/shaker/3.0.0-beta.22:
+ /@linaria/shaker@3.0.0-beta.22:
resolution: {integrity: sha512-NOi71i/XfBJpBOT5eepRvv6B64IMdjsKwv+vxLW+IuFHx3wnqXgZsgimNK2qoXbpqy9xWsSEeB/4QA4m8GCUKQ==}
engines: {node: ^12.16.0 || >=13.7.0}
dependencies:
'@babel/core': 7.18.9
- '@babel/generator': 7.19.6
- '@babel/plugin-transform-runtime': 7.19.6_@babel+core@7.18.9
- '@babel/plugin-transform-template-literals': 7.18.9_@babel+core@7.18.9
- '@babel/preset-env': 7.19.4_@babel+core@7.18.9
+ '@babel/generator': 7.23.5
+ '@babel/plugin-transform-runtime': 7.23.4(@babel/core@7.18.9)
+ '@babel/plugin-transform-template-literals': 7.23.3(@babel/core@7.18.9)
+ '@babel/preset-env': 7.23.5(@babel/core@7.18.9)
'@linaria/babel-preset': 3.0.0-beta.22
'@linaria/logger': 3.0.0-beta.20
'@linaria/preeval': 3.0.0-beta.23
@@ -3884,15 +5651,15 @@ packages:
- supports-color
dev: true
- /@linaria/shaker/3.0.0-beta.23:
+ /@linaria/shaker@3.0.0-beta.23:
resolution: {integrity: sha512-kG57X747GM/CqTs+wYx6hMHgzVNt7U/ydh7iO/NwUjIunr359oWwdH2Zjq27xR58TvK9sYXfmFVS8w3IB9fRWQ==}
engines: {node: ^12.16.0 || >=13.7.0}
dependencies:
- '@babel/core': 7.19.6
+ '@babel/core': 7.22.1
'@babel/generator': 7.19.6
- '@babel/plugin-transform-runtime': 7.19.6_@babel+core@7.19.6
- '@babel/plugin-transform-template-literals': 7.18.9_@babel+core@7.19.6
- '@babel/preset-env': 7.19.4_@babel+core@7.19.6
+ '@babel/plugin-transform-runtime': 7.19.6(@babel/core@7.22.1)
+ '@babel/plugin-transform-template-literals': 7.18.9(@babel/core@7.22.1)
+ '@babel/preset-env': 7.19.4(@babel/core@7.22.1)
'@linaria/babel-preset': 3.0.0-beta.23
'@linaria/logger': 3.0.0-beta.20
'@linaria/preeval': 3.0.0-beta.23
@@ -3902,23 +5669,70 @@ packages:
- supports-color
dev: true
- /@linaria/utils/3.0.0-beta.20:
+ /@linaria/shaker@5.0.3:
+ resolution: {integrity: sha512-2a3pzYs09Iz88e+VG4OAQVRSIjxkbj7S4ju81ZTJVbZIWSR1kGsbX5OtJkRrh/AbKRrrUMW0DBS4PPgd0fks4A==}
+ engines: {node: '>=16.0.0'}
+ dependencies:
+ '@babel/core': 7.23.5
+ '@babel/generator': 7.23.5
+ '@babel/plugin-transform-modules-commonjs': 7.23.3(@babel/core@7.23.5)
+ '@babel/plugin-transform-runtime': 7.23.4(@babel/core@7.23.5)
+ '@babel/plugin-transform-template-literals': 7.23.3(@babel/core@7.23.5)
+ '@babel/preset-env': 7.23.5(@babel/core@7.23.5)
+ '@linaria/logger': 5.0.0
+ '@linaria/utils': 5.0.2
+ babel-plugin-transform-react-remove-prop-types: 0.4.24
+ ts-invariant: 0.10.3
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@linaria/tags@5.0.2:
+ resolution: {integrity: sha512-opcORl2sA6WkBjTNLHTgpet97dNKnwPRX/QGGZMykBsvGH3AsnEg/bT31cKBMBhL+YBGQsCdBmolxvCkWPOXQw==}
+ engines: {node: '>=16.0.0'}
+ dependencies:
+ '@babel/generator': 7.23.5
+ '@linaria/logger': 5.0.0
+ '@linaria/utils': 5.0.2
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@linaria/utils@3.0.0-beta.20:
resolution: {integrity: sha512-SKRC9dBApzu0kTksVtGZ7eJz1vMu7xew/JEAjQj6XTQDblzWpTPyKQHBOGXNkqXjIB8PwAqWfvKzKapzaOwQaQ==}
engines: {node: ^12.16.0 || >=13.7.0}
dev: true
- /@linaria/webpack-loader/3.0.0-beta.22:
+ /@linaria/utils@5.0.2:
+ resolution: {integrity: sha512-ZL8Yz4IIr9A8a5+o5LRnEpgdzIkyj4yKJrhw5Zv8wwvL+d/MHUK0q+l/KvxBmuYdcF+YYVHZ9eeBHTQBSL7r/w==}
+ engines: {node: '>=16.0.0'}
+ dependencies:
+ '@babel/core': 7.23.5
+ '@babel/generator': 7.23.5
+ '@babel/plugin-transform-modules-commonjs': 7.23.3(@babel/core@7.23.5)
+ '@babel/template': 7.22.15
+ '@babel/traverse': 7.23.5
+ '@babel/types': 7.23.5
+ '@linaria/logger': 5.0.0
+ babel-merge: 3.0.0(@babel/core@7.23.5)
+ find-up: 5.0.0
+ minimatch: 9.0.3
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@linaria/webpack-loader@3.0.0-beta.22(webpack@4.47.0):
resolution: {integrity: sha512-oSChk+9MfcoF1M3Thx++aB1IjAaq7gS643i4995GSm1fs53i6QeUpCvIlWClDtRADmBzHSdMKIt0/vLoESvBoQ==}
engines: {node: ^12.16.0 || >=13.7.0}
dependencies:
- '@linaria/webpack4-loader': 3.0.0-beta.23
- '@linaria/webpack5-loader': 3.0.0-beta.23
+ '@linaria/webpack4-loader': 3.0.0-beta.23(webpack@4.47.0)
+ '@linaria/webpack5-loader': 3.0.0-beta.23(webpack@4.47.0)
transitivePeerDependencies:
- supports-color
- webpack
dev: true
- /@linaria/webpack4-loader/3.0.0-beta.23:
+ /@linaria/webpack4-loader@3.0.0-beta.23(webpack@4.47.0):
resolution: {integrity: sha512-I1pwrRKpGCARWbPwTFqOKLrkyxrZ+huYC3WH4pMllfoY+fv3O2dmDH6vKrZ582mQ5Uo/H3FmHBt8CLaMBv3pmg==}
engines: {node: ^12.16.0 || >=13.7.0}
peerDependencies:
@@ -3929,11 +5743,12 @@ packages:
enhanced-resolve: 4.5.0
loader-utils: 1.4.0
mkdirp: 0.5.6
+ webpack: 4.47.0
transitivePeerDependencies:
- supports-color
dev: true
- /@linaria/webpack5-loader/3.0.0-beta.23:
+ /@linaria/webpack5-loader@3.0.0-beta.23(webpack@4.47.0):
resolution: {integrity: sha512-yIjhnDT1otwfx6JAA9HNfDzim7N93z9++8apzXE57GXg5wRO2hlajruatclpUDcMOsodS9p2+mMXd8GGR8CGCA==}
engines: {node: ^12.16.0 || >=13.7.0}
peerDependencies:
@@ -3943,110 +5758,73 @@ packages:
'@linaria/logger': 3.0.0-beta.20
enhanced-resolve: 5.10.0
mkdirp: 0.5.6
+ webpack: 4.47.0
transitivePeerDependencies:
- supports-color
dev: true
- /@mdn/browser-compat-data/3.3.14:
- resolution: {integrity: sha512-n2RC9d6XatVbWFdHLimzzUJxJ1KY8LdjqrW6YvGPiRmsHkhOUx74/Ct10x5Yo7bC/Jvqx7cDEW8IMPv/+vwEzA==}
- dev: true
-
- /@mdn/browser-compat-data/4.2.1:
- resolution: {integrity: sha512-EWUguj2kd7ldmrF9F+vI5hUOralPd+sdsUnYbRy33vZTuZkduC1shE9TtEMEjAQwyfyMb4ole5KtjF8MsnQOlA==}
- dev: true
-
- /@mdx-js/mdx/1.6.22:
- resolution: {integrity: sha512-AMxuLxPz2j5/6TpF/XSdKpQP1NlG0z11dFOlq+2IP/lSgl11GY8ji6S/rgsViN/L0BDvHvUMruRb7ub+24LUYA==}
+ /@mapbox/node-pre-gyp@1.0.11:
+ resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==}
+ hasBin: true
dependencies:
- '@babel/core': 7.12.9
- '@babel/plugin-syntax-jsx': 7.12.1_@babel+core@7.12.9
- '@babel/plugin-syntax-object-rest-spread': 7.8.3_@babel+core@7.12.9
- '@mdx-js/util': 1.6.22
- babel-plugin-apply-mdx-type-prop: 1.6.22_@babel+core@7.12.9
- babel-plugin-extract-import-names: 1.6.22
- camelcase-css: 2.0.1
- detab: 2.0.4
- hast-util-raw: 6.0.1
- lodash.uniq: 4.5.0
- mdast-util-to-hast: 10.0.1
- remark-footnotes: 2.0.0
- remark-mdx: 1.6.22
- remark-parse: 8.0.3
- remark-squeeze-paragraphs: 4.0.0
- style-to-object: 0.3.0
- unified: 9.2.0
- unist-builder: 2.0.3
- unist-util-visit: 2.0.3
+ detect-libc: 2.0.2
+ https-proxy-agent: 5.0.1
+ make-dir: 3.1.0
+ node-fetch: 2.7.0
+ nopt: 5.0.0
+ npmlog: 5.0.1
+ rimraf: 3.0.2
+ semver: 7.5.4
+ tar: 6.2.0
transitivePeerDependencies:
+ - encoding
- supports-color
dev: true
- /@mdx-js/react/1.6.22:
- resolution: {integrity: sha512-TDoPum4SHdfPiGSAaRBw7ECyI8VaHpK8GJugbJIJuqyh6kzw9ZLJZW3HGL3NNrJGxcAixUvqROm+YuQOo5eXtg==}
- peerDependencies:
- react: ^16.13.1 || ^17.0.0
- dev: true
-
- /@mdx-js/react/1.6.22_@preact+compat@17.1.2:
- resolution: {integrity: sha512-TDoPum4SHdfPiGSAaRBw7ECyI8VaHpK8GJugbJIJuqyh6kzw9ZLJZW3HGL3NNrJGxcAixUvqROm+YuQOo5eXtg==}
- peerDependencies:
- react: ^16.13.1 || ^17.0.0
- dependencies:
- react: /@preact/compat/17.1.2_preact@10.6.5
+ /@mdn/browser-compat-data@3.3.14:
+ resolution: {integrity: sha512-n2RC9d6XatVbWFdHLimzzUJxJ1KY8LdjqrW6YvGPiRmsHkhOUx74/Ct10x5Yo7bC/Jvqx7cDEW8IMPv/+vwEzA==}
dev: true
- /@mdx-js/util/1.6.22:
- resolution: {integrity: sha512-H1rQc1ZOHANWBvPcW+JpGwr+juXSxM8Q8YCkm3GhZd8REu1fHR3z99CErO1p9pkcfcxZnMdIZdIsXkOHY0NilA==}
+ /@mdn/browser-compat-data@4.2.1:
+ resolution: {integrity: sha512-EWUguj2kd7ldmrF9F+vI5hUOralPd+sdsUnYbRy33vZTuZkduC1shE9TtEMEjAQwyfyMb4ole5KtjF8MsnQOlA==}
dev: true
- /@mrmlnc/readdir-enhanced/2.2.1:
- resolution: {integrity: sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==}
- engines: {node: '>=4'}
- dependencies:
- call-me-maybe: 1.0.1
- glob-to-regexp: 0.3.0
+ /@mdn/browser-compat-data@5.5.7:
+ resolution: {integrity: sha512-DoHTZ/TjtNfUu9eiqJd+x3IcCQrhS+yOYU436TKUnlE36jZwNbK535D1CmTsSYdi/UcdCWNm5KRQZ9g1tlZCPw==}
dev: true
- /@nicolo-ribaudo/eslint-scope-5-internals/5.1.1-v1:
+ /@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1:
resolution: {integrity: sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==}
dependencies:
eslint-scope: 5.1.1
dev: true
- /@nodelib/fs.scandir/2.1.5:
+ /@nodelib/fs.scandir@2.1.5:
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
dependencies:
'@nodelib/fs.stat': 2.0.5
run-parallel: 1.2.0
- dev: true
-
- /@nodelib/fs.stat/1.1.3:
- resolution: {integrity: sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==}
- engines: {node: '>= 6'}
- dev: true
- /@nodelib/fs.stat/2.0.5:
+ /@nodelib/fs.stat@2.0.5:
resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
engines: {node: '>= 8'}
- dev: true
- /@nodelib/fs.walk/1.2.8:
+ /@nodelib/fs.walk@1.2.8:
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'}
dependencies:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.13.0
- dev: true
- /@npmcli/fs/1.1.1:
+ /@npmcli/fs@1.1.1:
resolution: {integrity: sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==}
dependencies:
'@gar/promisify': 1.1.3
- semver: 7.3.8
+ semver: 7.5.4
dev: true
- /@npmcli/move-file/1.1.2:
+ /@npmcli/move-file@1.1.2:
resolution: {integrity: sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==}
engines: {node: '>=10'}
dependencies:
@@ -4054,52 +5832,38 @@ packages:
rimraf: 3.0.2
dev: true
- /@nrwl/cli/15.0.1:
- resolution: {integrity: sha512-ZehVNZOajNG7i5qg4vDeIcf88PuyFZhAwNhT9G7yjG6v+23zFwOVUsweQeQbY0GxIGc9d+jXGzEe6hU+UYovPQ==}
- dependencies:
- nx: 15.0.1
- transitivePeerDependencies:
- - '@swc-node/register'
- - '@swc/core'
- - debug
+ /@pkgjs/parseargs@0.11.0:
+ resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
+ engines: {node: '>=14'}
+ requiresBuild: true
+ optional: true
+
+ /@pnpm/config.env-replace@1.1.0:
+ resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==}
+ engines: {node: '>=12.22.0'}
dev: true
- /@nrwl/tao/15.0.1:
- resolution: {integrity: sha512-CytTfL7W6xkl1rW5fwO1nGF0Ft/Tk3ilc7/7I9+1NlNGdu/joFMOWub5YEQFIe488BJt2mxZ9m2n6sFJPVD19Q==}
- hasBin: true
+ /@pnpm/network.ca-file@1.0.2:
+ resolution: {integrity: sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==}
+ engines: {node: '>=12.22.0'}
dependencies:
- nx: 15.0.1
- transitivePeerDependencies:
- - '@swc-node/register'
- - '@swc/core'
- - debug
+ graceful-fs: 4.2.10
dev: true
- /@parcel/watcher/2.0.4:
- resolution: {integrity: sha512-cTDi+FUDBIUOBKEtj+nhiJ71AZVlkAsQFuGQTun5tV9mwQBQgZvhCzG+URPQc8myeN32yRVZEfVAPCs1RW+Jvg==}
- engines: {node: '>= 10.0.0'}
- requiresBuild: true
+ /@pnpm/npm-conf@2.2.2:
+ resolution: {integrity: sha512-UA91GwWPhFExt3IizW6bOeY/pQ0BkuNwKjk9iQW9KqxluGCrg4VenZ0/L+2Y0+ZOtme72EVvg6v0zo3AMQRCeA==}
+ engines: {node: '>=12'}
dependencies:
- node-addon-api: 3.2.1
- node-gyp-build: 4.5.0
+ '@pnpm/config.env-replace': 1.1.0
+ '@pnpm/network.ca-file': 1.0.2
+ config-chain: 1.1.13
dev: true
- /@polka/url/1.0.0-next.21:
+ /@polka/url@1.0.0-next.21:
resolution: {integrity: sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==}
dev: true
- /@preact/async-loader/3.0.1_preact@10.11.2:
- resolution: {integrity: sha512-BoUN24hxEfAQYnWjliAmkZLuliv+ONQi7AWn+/+VOJHTIHmbFiXrvmSxITf7PDkKiK0a5xy4OErZtVVLlk96Tg==}
- engines: {node: '>=8'}
- peerDependencies:
- preact: '>= 10.0.0'
- dependencies:
- kleur: 4.1.5
- loader-utils: 2.0.3
- preact: 10.11.2
- dev: true
-
- /@preact/async-loader/3.0.1_preact@10.11.3:
+ /@preact/async-loader@3.0.1(preact@10.11.3):
resolution: {integrity: sha512-BoUN24hxEfAQYnWjliAmkZLuliv+ONQi7AWn+/+VOJHTIHmbFiXrvmSxITf7PDkKiK0a5xy4OErZtVVLlk96Tg==}
engines: {node: '>=8'}
peerDependencies:
@@ -4110,37 +5874,11 @@ packages:
preact: 10.11.3
dev: true
- /@preact/async-loader/3.0.1_preact@10.6.5:
- resolution: {integrity: sha512-BoUN24hxEfAQYnWjliAmkZLuliv+ONQi7AWn+/+VOJHTIHmbFiXrvmSxITf7PDkKiK0a5xy4OErZtVVLlk96Tg==}
- engines: {node: '>=8'}
- peerDependencies:
- preact: '>= 10.0.0'
- dependencies:
- kleur: 4.1.5
- loader-utils: 2.0.3
- preact: 10.6.5
- dev: true
-
- /@preact/compat/17.1.2_preact@10.6.5:
- resolution: {integrity: sha512-7pOZN9lMDDRQ+6aWvjwTp483KR8/zOpfS83wmOo3zfuLKdngS8/5RLbsFWzFZMGdYlotAhX980hJ75bjOHTwWg==}
- peerDependencies:
- preact: '*'
- dependencies:
- preact: 10.6.5
-
- /@prefresh/babel-plugin/0.4.4:
+ /@prefresh/babel-plugin@0.4.4:
resolution: {integrity: sha512-/EvgIFMDL+nd20WNvMO0JQnzIl1EJPgmSaSYrZUww7A+aSdKsi37aL07TljrZR1cBMuzFxcr4xvqsUQLFJEukw==}
dev: true
- /@prefresh/core/1.4.1_preact@10.11.2:
- resolution: {integrity: sha512-og1vaBj3LMJagVncNrDb37Gqc0cWaUcDbpVt5hZtsN4i2Iwzd/5hyTsDHvlMirhSym3wL9ihU0Xa2VhSaOue7g==}
- peerDependencies:
- preact: ^10.0.0
- dependencies:
- preact: 10.11.2
- dev: true
-
- /@prefresh/core/1.4.1_preact@10.11.3:
+ /@prefresh/core@1.4.1(preact@10.11.3):
resolution: {integrity: sha512-og1vaBj3LMJagVncNrDb37Gqc0cWaUcDbpVt5hZtsN4i2Iwzd/5hyTsDHvlMirhSym3wL9ihU0Xa2VhSaOue7g==}
peerDependencies:
preact: ^10.0.0
@@ -4148,19 +5886,11 @@ packages:
preact: 10.11.3
dev: true
- /@prefresh/core/1.4.1_preact@10.6.5:
- resolution: {integrity: sha512-og1vaBj3LMJagVncNrDb37Gqc0cWaUcDbpVt5hZtsN4i2Iwzd/5hyTsDHvlMirhSym3wL9ihU0Xa2VhSaOue7g==}
- peerDependencies:
- preact: ^10.0.0
- dependencies:
- preact: 10.6.5
- dev: true
-
- /@prefresh/utils/1.1.3:
+ /@prefresh/utils@1.1.3:
resolution: {integrity: sha512-Mb9abhJTOV4yCfkXrMrcgFiFT7MfNOw8sDa+XyZBdq/Ai2p4Zyxqsb3EgHLOEdHpMj6J9aiZ54W8H6FTam1u+A==}
dev: true
- /@prefresh/webpack/3.3.4_2ylwkgirq2zgmdyuhwfa4uhfgq:
+ /@prefresh/webpack@3.3.4(@prefresh/babel-plugin@0.4.4)(preact@10.11.3)(webpack@4.46.0):
resolution: {integrity: sha512-RiXS/hvXDup5cQw/267kxkKie81kxaAB7SFbkr8ppshobDEzwgUN1tbGbHNx6Uari0Ql2XByC6HIgQGpaq2Q7w==}
peerDependencies:
'@prefresh/babel-plugin': ^0.4.0
@@ -4168,51 +5898,13 @@ packages:
webpack: ^4.0.0 || ^5.0.0
dependencies:
'@prefresh/babel-plugin': 0.4.4
- '@prefresh/core': 1.4.1_preact@10.11.3
+ '@prefresh/core': 1.4.1(preact@10.11.3)
'@prefresh/utils': 1.1.3
preact: 10.11.3
webpack: 4.46.0
dev: true
- /@prefresh/webpack/3.3.4_kitpfapqi2defymxf2rxzdj6na:
- resolution: {integrity: sha512-RiXS/hvXDup5cQw/267kxkKie81kxaAB7SFbkr8ppshobDEzwgUN1tbGbHNx6Uari0Ql2XByC6HIgQGpaq2Q7w==}
- peerDependencies:
- '@prefresh/babel-plugin': ^0.4.0
- preact: ^10.4.0
- webpack: ^4.0.0 || ^5.0.0
- dependencies:
- '@prefresh/babel-plugin': 0.4.4
- '@prefresh/core': 1.4.1_preact@10.11.2
- '@prefresh/utils': 1.1.3
- preact: 10.11.2
- webpack: 4.46.0
- dev: true
-
- /@prefresh/webpack/3.3.4_l37gxlnxrbylk7wqownx5ddpoa:
- resolution: {integrity: sha512-RiXS/hvXDup5cQw/267kxkKie81kxaAB7SFbkr8ppshobDEzwgUN1tbGbHNx6Uari0Ql2XByC6HIgQGpaq2Q7w==}
- peerDependencies:
- '@prefresh/babel-plugin': ^0.4.0
- preact: ^10.4.0
- webpack: ^4.0.0 || ^5.0.0
- dependencies:
- '@prefresh/babel-plugin': 0.4.4
- '@prefresh/core': 1.4.1_preact@10.6.5
- '@prefresh/utils': 1.1.3
- preact: 10.6.5
- webpack: 4.46.0
- dev: true
-
- /@rollup/plugin-alias/3.1.9_rollup@2.79.1:
- resolution: {integrity: sha512-QI5fsEvm9bDzt32k39wpOwZhVzRcL5ydcffUHMyLVaVaLeC70I8TJZ17F1z1eMoLu4E/UOcH9BWVkKpIKdrfiw==}
- engines: {node: '>=8.0.0'}
- peerDependencies:
- rollup: ^1.20.0||^2.0.0
- dependencies:
- rollup: 2.79.1
- slash: 3.0.0
- dev: true
-
- /@rollup/plugin-babel/5.3.1_cwbsg774jzhqoll5t2xfwyzz54:
+ /@rollup/plugin-babel@5.3.1(@babel/core@7.18.9)(rollup@2.79.1):
resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==}
engines: {node: '>= 10.0.0'}
peerDependencies:
@@ -4224,151 +5916,37 @@ packages:
optional: true
dependencies:
'@babel/core': 7.18.9
- '@babel/helper-module-imports': 7.18.6
- '@rollup/pluginutils': 3.1.0_rollup@2.79.1
+ '@babel/helper-module-imports': 7.22.15
+ '@rollup/pluginutils': 3.1.0(rollup@2.79.1)
rollup: 2.79.1
dev: true
- /@rollup/plugin-commonjs/20.0.0_rollup@2.79.1:
- resolution: {integrity: sha512-5K0g5W2Ol8hAcTHqcTBHiA7M58tfmYi1o9KxeJuuRNpGaTa5iLjcyemBitCBcKXaHamOBBEH2dGom6v6Unmqjg==}
- engines: {node: '>= 8.0.0'}
- peerDependencies:
- rollup: ^2.38.3
- dependencies:
- '@rollup/pluginutils': 3.1.0_rollup@2.79.1
- commondir: 1.0.1
- estree-walker: 2.0.2
- glob: 7.2.3
- is-reference: 1.2.1
- magic-string: 0.25.9
- resolve: 1.22.1
- rollup: 2.79.1
- dev: true
-
- /@rollup/plugin-commonjs/22.0.2_rollup@2.79.1:
- resolution: {integrity: sha512-//NdP6iIwPbMTcazYsiBMbJW7gfmpHom33u1beiIoHDEM0Q9clvtQB1T0efvMqHeKsGohiHo97BCPCkBXdscwg==}
- engines: {node: '>= 12.0.0'}
- peerDependencies:
- rollup: ^2.68.0
- dependencies:
- '@rollup/pluginutils': 3.1.0_rollup@2.79.1
- commondir: 1.0.1
- estree-walker: 2.0.2
- glob: 7.2.3
- is-reference: 1.2.1
- magic-string: 0.25.9
- resolve: 1.22.1
- rollup: 2.79.1
- dev: true
-
- /@rollup/plugin-html/0.2.4_rollup@2.79.1:
- resolution: {integrity: sha512-x0qpNXxbmGa9Jnl4OX89AORPe2T/a4DqNK69BGRnEdaPKq6MdiUXSTam/eCkF5DxkQGcRcPq0L4vzr/E3q4mVA==}
- engines: {node: '>= 8.0.0'}
- peerDependencies:
- rollup: ^1.20.0||^2.0.0
- dependencies:
- rollup: 2.79.1
- dev: true
-
- /@rollup/plugin-image/2.1.1_rollup@2.79.1:
- resolution: {integrity: sha512-AgP4U85zuQJdUopLUCM+hTf45RepgXeTb8EJsleExVy99dIoYpt3ZlDYJdKmAc2KLkNntCDg6BPJvgJU3uGF+g==}
- engines: {node: '>= 8.0.0'}
- peerDependencies:
- rollup: ^1.20.0 || ^2.0.0
- dependencies:
- '@rollup/pluginutils': 3.1.0_rollup@2.79.1
- mini-svg-data-uri: 1.4.4
- rollup: 2.79.1
- dev: true
-
- /@rollup/plugin-json/4.1.0_rollup@2.79.1:
- resolution: {integrity: sha512-yfLbTdNS6amI/2OpmbiBoW12vngr5NW2jCJVZSBEz+H5KfUJZ2M7sDjk0U6GOOdCWFVScShte29o9NezJ53TPw==}
- peerDependencies:
- rollup: ^1.20.0 || ^2.0.0
- dependencies:
- '@rollup/pluginutils': 3.1.0_rollup@2.79.1
- rollup: 2.79.1
- dev: true
-
- /@rollup/plugin-node-resolve/11.2.1_rollup@2.79.1:
+ /@rollup/plugin-node-resolve@11.2.1(rollup@2.79.1):
resolution: {integrity: sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==}
engines: {node: '>= 10.0.0'}
peerDependencies:
rollup: ^1.20.0||^2.0.0
dependencies:
- '@rollup/pluginutils': 3.1.0_rollup@2.79.1
+ '@rollup/pluginutils': 3.1.0(rollup@2.79.1)
'@types/resolve': 1.17.1
builtin-modules: 3.3.0
deepmerge: 4.2.2
is-module: 1.0.0
- resolve: 1.22.1
+ resolve: 1.22.8
rollup: 2.79.1
dev: true
- /@rollup/plugin-node-resolve/13.3.0_rollup@2.79.1:
- resolution: {integrity: sha512-Lus8rbUo1eEcnS4yTFKLZrVumLPY+YayBdWXgFSHYhTT2iJbMhoaaBL3xl5NCdeRytErGr8tZ0L71BMRmnlwSw==}
- engines: {node: '>= 10.0.0'}
- peerDependencies:
- rollup: ^2.42.0
- dependencies:
- '@rollup/pluginutils': 3.1.0_rollup@2.79.1
- '@types/resolve': 1.17.1
- deepmerge: 4.2.2
- is-builtin-module: 3.2.0
- is-module: 1.0.0
- resolve: 1.22.1
- rollup: 2.79.1
- dev: true
-
- /@rollup/plugin-replace/2.4.2_rollup@2.79.1:
+ /@rollup/plugin-replace@2.4.2(rollup@2.79.1):
resolution: {integrity: sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==}
peerDependencies:
rollup: ^1.20.0 || ^2.0.0
dependencies:
- '@rollup/pluginutils': 3.1.0_rollup@2.79.1
- magic-string: 0.25.9
- rollup: 2.79.1
- dev: true
-
- /@rollup/plugin-replace/3.1.0_rollup@2.79.1:
- resolution: {integrity: sha512-pA3XRUrSKybVYqmH5TqWNZpGxF+VV+1GrYchKgCNIj2vsSOX7CVm2RCtx8p2nrC7xvkziYyK+lSi74T93MU3YA==}
- peerDependencies:
- rollup: ^1.20.0 || ^2.0.0
- dependencies:
- '@rollup/pluginutils': 3.1.0_rollup@2.79.1
+ '@rollup/pluginutils': 3.1.0(rollup@2.79.1)
magic-string: 0.25.9
rollup: 2.79.1
dev: true
- /@rollup/plugin-replace/4.0.0_rollup@2.79.1:
- resolution: {integrity: sha512-+rumQFiaNac9y64OHtkHGmdjm7us9bo1PlbgQfdihQtuNxzjpaB064HbRnewUOggLQxVCCyINfStkgmBeQpv1g==}
- peerDependencies:
- rollup: ^1.20.0 || ^2.0.0
- dependencies:
- '@rollup/pluginutils': 3.1.0_rollup@2.79.1
- magic-string: 0.25.9
- rollup: 2.79.1
- dev: true
-
- /@rollup/plugin-typescript/8.5.0_hafrwlgfjmvsm7253l3bfjzhnq:
- resolution: {integrity: sha512-wMv1/scv0m/rXx21wD2IsBbJFba8wGF3ErJIr6IKRfRj49S85Lszbxb4DCo8iILpluTjk2GAAu9CoZt4G3ppgQ==}
- engines: {node: '>=8.0.0'}
- peerDependencies:
- rollup: ^2.14.0
- tslib: '*'
- typescript: '>=3.7.0'
- peerDependenciesMeta:
- tslib:
- optional: true
- dependencies:
- '@rollup/pluginutils': 3.1.0_rollup@2.79.1
- resolve: 1.22.1
- rollup: 2.79.1
- tslib: 2.4.0
- typescript: 4.8.4
- dev: true
-
- /@rollup/pluginutils/3.1.0_rollup@2.79.1:
+ /@rollup/pluginutils@3.1.0(rollup@2.79.1):
resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==}
engines: {node: '>= 8.0.0'}
peerDependencies:
@@ -4380,7 +5958,7 @@ packages:
rollup: 2.79.1
dev: true
- /@rollup/pluginutils/4.2.1:
+ /@rollup/pluginutils@4.2.1:
resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==}
engines: {node: '>= 8.0.0'}
dependencies:
@@ -4388,2689 +5966,141 @@ packages:
picomatch: 2.3.1
dev: true
- /@sindresorhus/is/0.14.0:
+ /@sindresorhus/is@0.14.0:
resolution: {integrity: sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==}
engines: {node: '>=6'}
dev: true
- /@sinonjs/commons/1.8.3:
- resolution: {integrity: sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==}
- dependencies:
- type-detect: 4.0.8
- dev: true
-
- /@sinonjs/fake-timers/6.0.1:
- resolution: {integrity: sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==}
- dependencies:
- '@sinonjs/commons': 1.8.3
- dev: true
-
- /@storybook/addon-a11y/6.5.13:
- resolution: {integrity: sha512-+Tcl/4LWRh3ygLUZFGvkjT42CF/tJcP+kgsIho7i2MxpgZyD6+BUhL9srPZusjbR+uHcHXJ/yxw/vxFQ+UCTLA==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- peerDependenciesMeta:
- react:
- optional: true
- react-dom:
- optional: true
- dependencies:
- '@storybook/addons': 6.5.13
- '@storybook/api': 6.5.13
- '@storybook/channels': 6.5.13
- '@storybook/client-logger': 6.5.13
- '@storybook/components': 6.5.13
- '@storybook/core-events': 6.5.13
- '@storybook/csf': 0.0.2--canary.4566f4d.1
- '@storybook/theming': 6.5.13
- axe-core: 4.5.0
- core-js: 3.26.0
- global: 4.4.0
- lodash: 4.17.21
- react-sizeme: 3.0.2
- regenerator-runtime: 0.13.10
- ts-dedent: 2.2.0
- util-deprecate: 1.0.2
- dev: true
-
- /@storybook/addon-a11y/6.5.13_@preact+compat@17.1.2:
- resolution: {integrity: sha512-+Tcl/4LWRh3ygLUZFGvkjT42CF/tJcP+kgsIho7i2MxpgZyD6+BUhL9srPZusjbR+uHcHXJ/yxw/vxFQ+UCTLA==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- peerDependenciesMeta:
- react:
- optional: true
- react-dom:
- optional: true
- dependencies:
- '@storybook/addons': 6.5.13_@preact+compat@17.1.2
- '@storybook/api': 6.5.13_@preact+compat@17.1.2
- '@storybook/channels': 6.5.13
- '@storybook/client-logger': 6.5.13
- '@storybook/components': 6.5.13_@preact+compat@17.1.2
- '@storybook/core-events': 6.5.13
- '@storybook/csf': 0.0.2--canary.4566f4d.1
- '@storybook/theming': 6.5.13_@preact+compat@17.1.2
- axe-core: 4.5.0
- core-js: 3.26.0
- global: 4.4.0
- lodash: 4.17.21
- react: /@preact/compat/17.1.2_preact@10.6.5
- react-sizeme: 3.0.2
- regenerator-runtime: 0.13.10
- ts-dedent: 2.2.0
- util-deprecate: 1.0.2
- dev: true
-
- /@storybook/addon-actions/6.5.13:
- resolution: {integrity: sha512-3Tji0gIy95havhTpSc6CsFl5lNxGn4O5Y1U9fyji+GRkKqDFOrvVLYAHPtLOpYdEI5tF0bDo+akiqfDouY8+eA==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- peerDependenciesMeta:
- react:
- optional: true
- react-dom:
- optional: true
- dependencies:
- '@storybook/addons': 6.5.13
- '@storybook/api': 6.5.13
- '@storybook/client-logger': 6.5.13
- '@storybook/components': 6.5.13
- '@storybook/core-events': 6.5.13
- '@storybook/csf': 0.0.2--canary.4566f4d.1
- '@storybook/theming': 6.5.13
- core-js: 3.26.0
- fast-deep-equal: 3.1.3
- global: 4.4.0
- lodash: 4.17.21
- polished: 4.2.2
- prop-types: 15.8.1
- react-inspector: 5.1.1
- regenerator-runtime: 0.13.10
- telejson: 6.0.8
- ts-dedent: 2.2.0
- util-deprecate: 1.0.2
- uuid-browser: 3.1.0
- dev: true
-
- /@storybook/addon-actions/6.5.13_@preact+compat@17.1.2:
- resolution: {integrity: sha512-3Tji0gIy95havhTpSc6CsFl5lNxGn4O5Y1U9fyji+GRkKqDFOrvVLYAHPtLOpYdEI5tF0bDo+akiqfDouY8+eA==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- peerDependenciesMeta:
- react:
- optional: true
- react-dom:
- optional: true
- dependencies:
- '@storybook/addons': 6.5.13_@preact+compat@17.1.2
- '@storybook/api': 6.5.13_@preact+compat@17.1.2
- '@storybook/client-logger': 6.5.13
- '@storybook/components': 6.5.13_@preact+compat@17.1.2
- '@storybook/core-events': 6.5.13
- '@storybook/csf': 0.0.2--canary.4566f4d.1
- '@storybook/theming': 6.5.13_@preact+compat@17.1.2
- core-js: 3.26.0
- fast-deep-equal: 3.1.3
- global: 4.4.0
- lodash: 4.17.21
- polished: 4.2.2
- prop-types: 15.8.1
- react: /@preact/compat/17.1.2_preact@10.6.5
- react-inspector: 5.1.1_@preact+compat@17.1.2
- regenerator-runtime: 0.13.10
- telejson: 6.0.8
- ts-dedent: 2.2.0
- util-deprecate: 1.0.2
- uuid-browser: 3.1.0
- dev: true
-
- /@storybook/addon-backgrounds/6.5.13:
- resolution: {integrity: sha512-b4JX7JMY7e50y1l6g71D+2XWV3GO0TO2z1ta8J6W4OQt8f44V7sSkRQaJUzXdLjQMrA+Anojuy1ZwPjVeLC6vg==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- peerDependenciesMeta:
- react:
- optional: true
- react-dom:
- optional: true
- dependencies:
- '@storybook/addons': 6.5.13
- '@storybook/api': 6.5.13
- '@storybook/client-logger': 6.5.13
- '@storybook/components': 6.5.13
- '@storybook/core-events': 6.5.13
- '@storybook/csf': 0.0.2--canary.4566f4d.1
- '@storybook/theming': 6.5.13
- core-js: 3.26.0
- global: 4.4.0
- memoizerific: 1.11.3
- regenerator-runtime: 0.13.10
- ts-dedent: 2.2.0
- util-deprecate: 1.0.2
- dev: true
-
- /@storybook/addon-backgrounds/6.5.13_@preact+compat@17.1.2:
- resolution: {integrity: sha512-b4JX7JMY7e50y1l6g71D+2XWV3GO0TO2z1ta8J6W4OQt8f44V7sSkRQaJUzXdLjQMrA+Anojuy1ZwPjVeLC6vg==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- peerDependenciesMeta:
- react:
- optional: true
- react-dom:
- optional: true
- dependencies:
- '@storybook/addons': 6.5.13_@preact+compat@17.1.2
- '@storybook/api': 6.5.13_@preact+compat@17.1.2
- '@storybook/client-logger': 6.5.13
- '@storybook/components': 6.5.13_@preact+compat@17.1.2
- '@storybook/core-events': 6.5.13
- '@storybook/csf': 0.0.2--canary.4566f4d.1
- '@storybook/theming': 6.5.13_@preact+compat@17.1.2
- core-js: 3.26.0
- global: 4.4.0
- memoizerific: 1.11.3
- react: /@preact/compat/17.1.2_preact@10.6.5
- regenerator-runtime: 0.13.10
- ts-dedent: 2.2.0
- util-deprecate: 1.0.2
- dev: true
-
- /@storybook/addon-controls/6.5.13_3rubbgt5ekhqrcgx4uwls3neim:
- resolution: {integrity: sha512-lYq3uf2mlVevm0bi6ueL3H6TpUMRYW9s/pTNTVJT225l27kLdFR9wEKxAkCBrlKaTgDLJmzzDRsJE3NLZlR/5Q==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- peerDependenciesMeta:
- react:
- optional: true
- react-dom:
- optional: true
- dependencies:
- '@storybook/addons': 6.5.13
- '@storybook/api': 6.5.13
- '@storybook/client-logger': 6.5.13
- '@storybook/components': 6.5.13
- '@storybook/core-common': 6.5.13_3rubbgt5ekhqrcgx4uwls3neim
- '@storybook/csf': 0.0.2--canary.4566f4d.1
- '@storybook/node-logger': 6.5.13
- '@storybook/store': 6.5.13
- '@storybook/theming': 6.5.13
- core-js: 3.26.0
- lodash: 4.17.21
- ts-dedent: 2.2.0
- transitivePeerDependencies:
- - eslint
- - supports-color
- - typescript
- - vue-template-compiler
- - webpack-cli
- - webpack-command
- dev: true
-
- /@storybook/addon-controls/6.5.13_ipjevasairp6knvwkrpikcsgoq:
- resolution: {integrity: sha512-lYq3uf2mlVevm0bi6ueL3H6TpUMRYW9s/pTNTVJT225l27kLdFR9wEKxAkCBrlKaTgDLJmzzDRsJE3NLZlR/5Q==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- peerDependenciesMeta:
- react:
- optional: true
- react-dom:
- optional: true
- dependencies:
- '@storybook/addons': 6.5.13_@preact+compat@17.1.2
- '@storybook/api': 6.5.13_@preact+compat@17.1.2
- '@storybook/client-logger': 6.5.13
- '@storybook/components': 6.5.13_@preact+compat@17.1.2
- '@storybook/core-common': 6.5.13_ipjevasairp6knvwkrpikcsgoq
- '@storybook/csf': 0.0.2--canary.4566f4d.1
- '@storybook/node-logger': 6.5.13
- '@storybook/store': 6.5.13_@preact+compat@17.1.2
- '@storybook/theming': 6.5.13_@preact+compat@17.1.2
- core-js: 3.26.0
- lodash: 4.17.21
- react: /@preact/compat/17.1.2_preact@10.6.5
- ts-dedent: 2.2.0
- transitivePeerDependencies:
- - eslint
- - supports-color
- - typescript
- - vue-template-compiler
- - webpack-cli
- - webpack-command
- dev: true
-
- /@storybook/addon-docs/6.5.13_dazlt7ye7nu7xsezygxn7bviwy:
- resolution: {integrity: sha512-RG/NjsheD9FixZ789RJlNyNccaR2Cuy7CtAwph4oUNi3aDFjtOI8Oe9L+FOT7qtVnZLw/YMjF+pZxoDqJNKLPw==}
- peerDependencies:
- '@storybook/mdx2-csf': ^0.0.3
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- peerDependenciesMeta:
- '@storybook/mdx2-csf':
- optional: true
- react:
- optional: true
- react-dom:
- optional: true
- dependencies:
- '@babel/plugin-transform-react-jsx': 7.19.0_@babel+core@7.18.9
- '@babel/preset-env': 7.19.4_@babel+core@7.18.9
- '@jest/transform': 26.6.2
- '@mdx-js/react': 1.6.22
- '@storybook/addons': 6.5.13
- '@storybook/api': 6.5.13
- '@storybook/components': 6.5.13
- '@storybook/core-common': 6.5.13_3rubbgt5ekhqrcgx4uwls3neim
- '@storybook/core-events': 6.5.13
- '@storybook/csf': 0.0.2--canary.4566f4d.1
- '@storybook/docs-tools': 6.5.13
- '@storybook/mdx1-csf': 0.0.1_@babel+core@7.18.9
- '@storybook/node-logger': 6.5.13
- '@storybook/postinstall': 6.5.13
- '@storybook/preview-web': 6.5.13
- '@storybook/source-loader': 6.5.13
- '@storybook/store': 6.5.13
- '@storybook/theming': 6.5.13
- babel-loader: 8.2.5_@babel+core@7.18.9
- core-js: 3.26.0
- fast-deep-equal: 3.1.3
- global: 4.4.0
- lodash: 4.17.21
- regenerator-runtime: 0.13.10
- remark-external-links: 8.0.0
- remark-slug: 6.1.0
- ts-dedent: 2.2.0
- util-deprecate: 1.0.2
- transitivePeerDependencies:
- - '@babel/core'
- - eslint
- - supports-color
- - typescript
- - vue-template-compiler
- - webpack
- - webpack-cli
- - webpack-command
- dev: true
-
- /@storybook/addon-docs/6.5.13_e4oandlgwkzlaxn6zc3btvol24:
- resolution: {integrity: sha512-RG/NjsheD9FixZ789RJlNyNccaR2Cuy7CtAwph4oUNi3aDFjtOI8Oe9L+FOT7qtVnZLw/YMjF+pZxoDqJNKLPw==}
- peerDependencies:
- '@storybook/mdx2-csf': ^0.0.3
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- peerDependenciesMeta:
- '@storybook/mdx2-csf':
- optional: true
- react:
- optional: true
- react-dom:
- optional: true
- dependencies:
- '@babel/plugin-transform-react-jsx': 7.19.0_@babel+core@7.18.9
- '@babel/preset-env': 7.19.4_@babel+core@7.18.9
- '@jest/transform': 26.6.2
- '@mdx-js/react': 1.6.22_@preact+compat@17.1.2
- '@storybook/addons': 6.5.13_@preact+compat@17.1.2
- '@storybook/api': 6.5.13_@preact+compat@17.1.2
- '@storybook/components': 6.5.13_@preact+compat@17.1.2
- '@storybook/core-common': 6.5.13_ipjevasairp6knvwkrpikcsgoq
- '@storybook/core-events': 6.5.13
- '@storybook/csf': 0.0.2--canary.4566f4d.1
- '@storybook/docs-tools': 6.5.13_@preact+compat@17.1.2
- '@storybook/mdx1-csf': 0.0.1_@babel+core@7.18.9
- '@storybook/node-logger': 6.5.13
- '@storybook/postinstall': 6.5.13
- '@storybook/preview-web': 6.5.13_@preact+compat@17.1.2
- '@storybook/source-loader': 6.5.13_@preact+compat@17.1.2
- '@storybook/store': 6.5.13_@preact+compat@17.1.2
- '@storybook/theming': 6.5.13_@preact+compat@17.1.2
- babel-loader: 8.2.5_@babel+core@7.18.9
- core-js: 3.26.0
- fast-deep-equal: 3.1.3
- global: 4.4.0
- lodash: 4.17.21
- react: /@preact/compat/17.1.2_preact@10.6.5
- regenerator-runtime: 0.13.10
- remark-external-links: 8.0.0
- remark-slug: 6.1.0
- ts-dedent: 2.2.0
- util-deprecate: 1.0.2
- transitivePeerDependencies:
- - '@babel/core'
- - eslint
- - supports-color
- - typescript
- - vue-template-compiler
- - webpack
- - webpack-cli
- - webpack-command
- dev: true
-
- /@storybook/addon-essentials/6.5.13_dazlt7ye7nu7xsezygxn7bviwy:
- resolution: {integrity: sha512-G9FVAWV7ixjVLWeLgIX+VT90tcAk6yQxfZQegfg5ucRilGysJCDaNnoab4xuuvm1R40TfFhba3iAGZtQYsddmw==}
- peerDependencies:
- '@babel/core': ^7.9.6
- '@storybook/angular': '*'
- '@storybook/builder-manager4': '*'
- '@storybook/builder-manager5': '*'
- '@storybook/builder-webpack4': '*'
- '@storybook/builder-webpack5': '*'
- '@storybook/html': '*'
- '@storybook/vue': '*'
- '@storybook/vue3': '*'
- '@storybook/web-components': '*'
- lit: '*'
- lit-html: '*'
- react: '*'
- react-dom: '*'
- svelte: '*'
- sveltedoc-parser: '*'
- vue: '*'
- webpack: '*'
- peerDependenciesMeta:
- '@storybook/angular':
- optional: true
- '@storybook/builder-manager4':
- optional: true
- '@storybook/builder-manager5':
- optional: true
- '@storybook/builder-webpack4':
- optional: true
- '@storybook/builder-webpack5':
- optional: true
- '@storybook/html':
- optional: true
- '@storybook/vue':
- optional: true
- '@storybook/vue3':
- optional: true
- '@storybook/web-components':
- optional: true
- lit:
- optional: true
- lit-html:
- optional: true
- react:
- optional: true
- react-dom:
- optional: true
- svelte:
- optional: true
- sveltedoc-parser:
- optional: true
- vue:
- optional: true
- webpack:
- optional: true
- dependencies:
- '@babel/core': 7.18.9
- '@storybook/addon-actions': 6.5.13
- '@storybook/addon-backgrounds': 6.5.13
- '@storybook/addon-controls': 6.5.13_3rubbgt5ekhqrcgx4uwls3neim
- '@storybook/addon-docs': 6.5.13_dazlt7ye7nu7xsezygxn7bviwy
- '@storybook/addon-measure': 6.5.13
- '@storybook/addon-outline': 6.5.13
- '@storybook/addon-toolbars': 6.5.13
- '@storybook/addon-viewport': 6.5.13
- '@storybook/addons': 6.5.13
- '@storybook/api': 6.5.13
- '@storybook/core-common': 6.5.13_3rubbgt5ekhqrcgx4uwls3neim
- '@storybook/node-logger': 6.5.13
- core-js: 3.26.0
- regenerator-runtime: 0.13.10
- ts-dedent: 2.2.0
- transitivePeerDependencies:
- - '@storybook/mdx2-csf'
- - eslint
- - supports-color
- - typescript
- - vue-template-compiler
- - webpack-cli
- - webpack-command
- dev: true
-
- /@storybook/addon-essentials/6.5.13_e4oandlgwkzlaxn6zc3btvol24:
- resolution: {integrity: sha512-G9FVAWV7ixjVLWeLgIX+VT90tcAk6yQxfZQegfg5ucRilGysJCDaNnoab4xuuvm1R40TfFhba3iAGZtQYsddmw==}
- peerDependencies:
- '@babel/core': ^7.9.6
- '@storybook/angular': '*'
- '@storybook/builder-manager4': '*'
- '@storybook/builder-manager5': '*'
- '@storybook/builder-webpack4': '*'
- '@storybook/builder-webpack5': '*'
- '@storybook/html': '*'
- '@storybook/vue': '*'
- '@storybook/vue3': '*'
- '@storybook/web-components': '*'
- lit: '*'
- lit-html: '*'
- react: '*'
- react-dom: '*'
- svelte: '*'
- sveltedoc-parser: '*'
- vue: '*'
- webpack: '*'
- peerDependenciesMeta:
- '@storybook/angular':
- optional: true
- '@storybook/builder-manager4':
- optional: true
- '@storybook/builder-manager5':
- optional: true
- '@storybook/builder-webpack4':
- optional: true
- '@storybook/builder-webpack5':
- optional: true
- '@storybook/html':
- optional: true
- '@storybook/vue':
- optional: true
- '@storybook/vue3':
- optional: true
- '@storybook/web-components':
- optional: true
- lit:
- optional: true
- lit-html:
- optional: true
- react:
- optional: true
- react-dom:
- optional: true
- svelte:
- optional: true
- sveltedoc-parser:
- optional: true
- vue:
- optional: true
- webpack:
- optional: true
- dependencies:
- '@babel/core': 7.18.9
- '@storybook/addon-actions': 6.5.13_@preact+compat@17.1.2
- '@storybook/addon-backgrounds': 6.5.13_@preact+compat@17.1.2
- '@storybook/addon-controls': 6.5.13_ipjevasairp6knvwkrpikcsgoq
- '@storybook/addon-docs': 6.5.13_e4oandlgwkzlaxn6zc3btvol24
- '@storybook/addon-measure': 6.5.13_@preact+compat@17.1.2
- '@storybook/addon-outline': 6.5.13_@preact+compat@17.1.2
- '@storybook/addon-toolbars': 6.5.13_@preact+compat@17.1.2
- '@storybook/addon-viewport': 6.5.13_@preact+compat@17.1.2
- '@storybook/addons': 6.5.13_@preact+compat@17.1.2
- '@storybook/api': 6.5.13_@preact+compat@17.1.2
- '@storybook/core-common': 6.5.13_ipjevasairp6knvwkrpikcsgoq
- '@storybook/node-logger': 6.5.13
- core-js: 3.26.0
- react: /@preact/compat/17.1.2_preact@10.6.5
- regenerator-runtime: 0.13.10
- ts-dedent: 2.2.0
- transitivePeerDependencies:
- - '@storybook/mdx2-csf'
- - eslint
- - supports-color
- - typescript
- - vue-template-compiler
- - webpack-cli
- - webpack-command
- dev: true
-
- /@storybook/addon-links/6.5.13:
- resolution: {integrity: sha512-K/LYYu9R/Xoah5h9MNh4mSHOic3q5csqjderLqr2YW/KPYiuNubgvzEbAAbzI5xq5JrtAZqnINrZUv2A4CyYbQ==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- peerDependenciesMeta:
- react:
- optional: true
- react-dom:
- optional: true
- dependencies:
- '@storybook/addons': 6.5.13
- '@storybook/client-logger': 6.5.13
- '@storybook/core-events': 6.5.13
- '@storybook/csf': 0.0.2--canary.4566f4d.1
- '@storybook/router': 6.5.13
- '@types/qs': 6.9.7
- core-js: 3.26.0
- global: 4.4.0
- prop-types: 15.8.1
- qs: 6.11.0
- regenerator-runtime: 0.13.10
- ts-dedent: 2.2.0
- dev: true
-
- /@storybook/addon-links/6.5.13_@preact+compat@17.1.2:
- resolution: {integrity: sha512-K/LYYu9R/Xoah5h9MNh4mSHOic3q5csqjderLqr2YW/KPYiuNubgvzEbAAbzI5xq5JrtAZqnINrZUv2A4CyYbQ==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- peerDependenciesMeta:
- react:
- optional: true
- react-dom:
- optional: true
- dependencies:
- '@storybook/addons': 6.5.13_@preact+compat@17.1.2
- '@storybook/client-logger': 6.5.13
- '@storybook/core-events': 6.5.13
- '@storybook/csf': 0.0.2--canary.4566f4d.1
- '@storybook/router': 6.5.13_@preact+compat@17.1.2
- '@types/qs': 6.9.7
- core-js: 3.26.0
- global: 4.4.0
- prop-types: 15.8.1
- qs: 6.11.0
- react: /@preact/compat/17.1.2_preact@10.6.5
- regenerator-runtime: 0.13.10
- ts-dedent: 2.2.0
- dev: true
-
- /@storybook/addon-measure/6.5.13:
- resolution: {integrity: sha512-pi5RFB9YTnESRFtYHAVRUrgEI5to0TFc4KndtwcCKt1fMJ8OFjXQeznEfdj95PFeUvW5TNUwjL38vK4LhicB+g==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- peerDependenciesMeta:
- react:
- optional: true
- react-dom:
- optional: true
- dependencies:
- '@storybook/addons': 6.5.13
- '@storybook/api': 6.5.13
- '@storybook/client-logger': 6.5.13
- '@storybook/components': 6.5.13
- '@storybook/core-events': 6.5.13
- '@storybook/csf': 0.0.2--canary.4566f4d.1
- core-js: 3.26.0
- global: 4.4.0
- dev: true
-
- /@storybook/addon-measure/6.5.13_@preact+compat@17.1.2:
- resolution: {integrity: sha512-pi5RFB9YTnESRFtYHAVRUrgEI5to0TFc4KndtwcCKt1fMJ8OFjXQeznEfdj95PFeUvW5TNUwjL38vK4LhicB+g==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- peerDependenciesMeta:
- react:
- optional: true
- react-dom:
- optional: true
- dependencies:
- '@storybook/addons': 6.5.13_@preact+compat@17.1.2
- '@storybook/api': 6.5.13_@preact+compat@17.1.2
- '@storybook/client-logger': 6.5.13
- '@storybook/components': 6.5.13_@preact+compat@17.1.2
- '@storybook/core-events': 6.5.13
- '@storybook/csf': 0.0.2--canary.4566f4d.1
- core-js: 3.26.0
- global: 4.4.0
- react: /@preact/compat/17.1.2_preact@10.6.5
- dev: true
-
- /@storybook/addon-outline/6.5.13:
- resolution: {integrity: sha512-8d8taPheO/tryflzXbj2QRuxHOIS8CtzRzcaglCcioqHEMhOIDOx9BdXKdheq54gdk/UN94HdGJUoVxYyXwZ4Q==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- peerDependenciesMeta:
- react:
- optional: true
- react-dom:
- optional: true
- dependencies:
- '@storybook/addons': 6.5.13
- '@storybook/api': 6.5.13
- '@storybook/client-logger': 6.5.13
- '@storybook/components': 6.5.13
- '@storybook/core-events': 6.5.13
- '@storybook/csf': 0.0.2--canary.4566f4d.1
- core-js: 3.26.0
- global: 4.4.0
- regenerator-runtime: 0.13.10
- ts-dedent: 2.2.0
- dev: true
-
- /@storybook/addon-outline/6.5.13_@preact+compat@17.1.2:
- resolution: {integrity: sha512-8d8taPheO/tryflzXbj2QRuxHOIS8CtzRzcaglCcioqHEMhOIDOx9BdXKdheq54gdk/UN94HdGJUoVxYyXwZ4Q==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- peerDependenciesMeta:
- react:
- optional: true
- react-dom:
- optional: true
- dependencies:
- '@storybook/addons': 6.5.13_@preact+compat@17.1.2
- '@storybook/api': 6.5.13_@preact+compat@17.1.2
- '@storybook/client-logger': 6.5.13
- '@storybook/components': 6.5.13_@preact+compat@17.1.2
- '@storybook/core-events': 6.5.13
- '@storybook/csf': 0.0.2--canary.4566f4d.1
- core-js: 3.26.0
- global: 4.4.0
- react: /@preact/compat/17.1.2_preact@10.6.5
- regenerator-runtime: 0.13.10
- ts-dedent: 2.2.0
- dev: true
-
- /@storybook/addon-toolbars/6.5.13:
- resolution: {integrity: sha512-Qgr4wKRSP+gY1VaN7PYT4TM1um7KY341X3GHTglXLFHd8nDsCweawfV2shaX3WxCfZmVro8g4G+Oest30kLLCw==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- peerDependenciesMeta:
- react:
- optional: true
- react-dom:
- optional: true
- dependencies:
- '@storybook/addons': 6.5.13
- '@storybook/api': 6.5.13
- '@storybook/client-logger': 6.5.13
- '@storybook/components': 6.5.13
- '@storybook/theming': 6.5.13
- core-js: 3.26.0
- regenerator-runtime: 0.13.10
- dev: true
-
- /@storybook/addon-toolbars/6.5.13_@preact+compat@17.1.2:
- resolution: {integrity: sha512-Qgr4wKRSP+gY1VaN7PYT4TM1um7KY341X3GHTglXLFHd8nDsCweawfV2shaX3WxCfZmVro8g4G+Oest30kLLCw==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- peerDependenciesMeta:
- react:
- optional: true
- react-dom:
- optional: true
- dependencies:
- '@storybook/addons': 6.5.13_@preact+compat@17.1.2
- '@storybook/api': 6.5.13_@preact+compat@17.1.2
- '@storybook/client-logger': 6.5.13
- '@storybook/components': 6.5.13_@preact+compat@17.1.2
- '@storybook/theming': 6.5.13_@preact+compat@17.1.2
- core-js: 3.26.0
- react: /@preact/compat/17.1.2_preact@10.6.5
- regenerator-runtime: 0.13.10
- dev: true
-
- /@storybook/addon-viewport/6.5.13:
- resolution: {integrity: sha512-KSfeuCSIjncwWGnUu6cZBx8WNqYvm5gHyFvkSPKEu0+MJtgncbUy7pl53lrEEr6QmIq0GRXvS3A0XzV8RCnrSA==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- peerDependenciesMeta:
- react:
- optional: true
- react-dom:
- optional: true
- dependencies:
- '@storybook/addons': 6.5.13
- '@storybook/api': 6.5.13
- '@storybook/client-logger': 6.5.13
- '@storybook/components': 6.5.13
- '@storybook/core-events': 6.5.13
- '@storybook/theming': 6.5.13
- core-js: 3.26.0
- global: 4.4.0
- memoizerific: 1.11.3
- prop-types: 15.8.1
- regenerator-runtime: 0.13.10
- dev: true
-
- /@storybook/addon-viewport/6.5.13_@preact+compat@17.1.2:
- resolution: {integrity: sha512-KSfeuCSIjncwWGnUu6cZBx8WNqYvm5gHyFvkSPKEu0+MJtgncbUy7pl53lrEEr6QmIq0GRXvS3A0XzV8RCnrSA==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- peerDependenciesMeta:
- react:
- optional: true
- react-dom:
- optional: true
- dependencies:
- '@storybook/addons': 6.5.13_@preact+compat@17.1.2
- '@storybook/api': 6.5.13_@preact+compat@17.1.2
- '@storybook/client-logger': 6.5.13
- '@storybook/components': 6.5.13_@preact+compat@17.1.2
- '@storybook/core-events': 6.5.13
- '@storybook/theming': 6.5.13_@preact+compat@17.1.2
- core-js: 3.26.0
- global: 4.4.0
- memoizerific: 1.11.3
- prop-types: 15.8.1
- react: /@preact/compat/17.1.2_preact@10.6.5
- regenerator-runtime: 0.13.10
- dev: true
-
- /@storybook/addons/6.5.13:
- resolution: {integrity: sha512-18CqzNnrGMfeZtiKz+R/3rHtSNnfNwz6y6prIQIbWseK16jY8ELTfIFGviwO5V2OqpbHDQi5+xQQ63QAIb89YA==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- dependencies:
- '@storybook/api': 6.5.13
- '@storybook/channels': 6.5.13
- '@storybook/client-logger': 6.5.13
- '@storybook/core-events': 6.5.13
- '@storybook/csf': 0.0.2--canary.4566f4d.1
- '@storybook/router': 6.5.13
- '@storybook/theming': 6.5.13
- '@types/webpack-env': 1.18.0
- core-js: 3.26.0
- global: 4.4.0
- regenerator-runtime: 0.13.10
- dev: true
-
- /@storybook/addons/6.5.13_@preact+compat@17.1.2:
- resolution: {integrity: sha512-18CqzNnrGMfeZtiKz+R/3rHtSNnfNwz6y6prIQIbWseK16jY8ELTfIFGviwO5V2OqpbHDQi5+xQQ63QAIb89YA==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- dependencies:
- '@storybook/api': 6.5.13_@preact+compat@17.1.2
- '@storybook/channels': 6.5.13
- '@storybook/client-logger': 6.5.13
- '@storybook/core-events': 6.5.13
- '@storybook/csf': 0.0.2--canary.4566f4d.1
- '@storybook/router': 6.5.13_@preact+compat@17.1.2
- '@storybook/theming': 6.5.13_@preact+compat@17.1.2
- '@types/webpack-env': 1.18.0
- core-js: 3.26.0
- global: 4.4.0
- react: /@preact/compat/17.1.2_preact@10.6.5
- regenerator-runtime: 0.13.10
- dev: true
-
- /@storybook/addons/6.5.13_wcqkhtmu7mswc6yz4uyexck3ty:
- resolution: {integrity: sha512-18CqzNnrGMfeZtiKz+R/3rHtSNnfNwz6y6prIQIbWseK16jY8ELTfIFGviwO5V2OqpbHDQi5+xQQ63QAIb89YA==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- dependencies:
- '@storybook/api': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@storybook/channels': 6.5.13
- '@storybook/client-logger': 6.5.13
- '@storybook/core-events': 6.5.13
- '@storybook/csf': 0.0.2--canary.4566f4d.1
- '@storybook/router': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@storybook/theming': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@types/webpack-env': 1.18.0
- core-js: 3.26.0
- global: 4.4.0
- react: 16.14.0
- react-dom: 16.14.0_react@16.14.0
- regenerator-runtime: 0.13.10
- dev: true
-
- /@storybook/api/6.5.13:
- resolution: {integrity: sha512-xVSmB7/IuFd6G7eiJjbI2MuS7SZunoUM6d+YCWpjiehfMeX47MXt1gZtOwFrgJC1ShZlefXFahq/dvxwtmWs+w==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- dependencies:
- '@storybook/channels': 6.5.13
- '@storybook/client-logger': 6.5.13
- '@storybook/core-events': 6.5.13
- '@storybook/csf': 0.0.2--canary.4566f4d.1
- '@storybook/router': 6.5.13
- '@storybook/semver': 7.3.2
- '@storybook/theming': 6.5.13
- core-js: 3.26.0
- fast-deep-equal: 3.1.3
- global: 4.4.0
- lodash: 4.17.21
- memoizerific: 1.11.3
- regenerator-runtime: 0.13.10
- store2: 2.14.2
- telejson: 6.0.8
- ts-dedent: 2.2.0
- util-deprecate: 1.0.2
- dev: true
-
- /@storybook/api/6.5.13_@preact+compat@17.1.2:
- resolution: {integrity: sha512-xVSmB7/IuFd6G7eiJjbI2MuS7SZunoUM6d+YCWpjiehfMeX47MXt1gZtOwFrgJC1ShZlefXFahq/dvxwtmWs+w==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- dependencies:
- '@storybook/channels': 6.5.13
- '@storybook/client-logger': 6.5.13
- '@storybook/core-events': 6.5.13
- '@storybook/csf': 0.0.2--canary.4566f4d.1
- '@storybook/router': 6.5.13_@preact+compat@17.1.2
- '@storybook/semver': 7.3.2
- '@storybook/theming': 6.5.13_@preact+compat@17.1.2
- core-js: 3.26.0
- fast-deep-equal: 3.1.3
- global: 4.4.0
- lodash: 4.17.21
- memoizerific: 1.11.3
- react: /@preact/compat/17.1.2_preact@10.6.5
- regenerator-runtime: 0.13.10
- store2: 2.14.2
- telejson: 6.0.8
- ts-dedent: 2.2.0
- util-deprecate: 1.0.2
- dev: true
-
- /@storybook/api/6.5.13_wcqkhtmu7mswc6yz4uyexck3ty:
- resolution: {integrity: sha512-xVSmB7/IuFd6G7eiJjbI2MuS7SZunoUM6d+YCWpjiehfMeX47MXt1gZtOwFrgJC1ShZlefXFahq/dvxwtmWs+w==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- dependencies:
- '@storybook/channels': 6.5.13
- '@storybook/client-logger': 6.5.13
- '@storybook/core-events': 6.5.13
- '@storybook/csf': 0.0.2--canary.4566f4d.1
- '@storybook/router': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@storybook/semver': 7.3.2
- '@storybook/theming': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- core-js: 3.26.0
- fast-deep-equal: 3.1.3
- global: 4.4.0
- lodash: 4.17.21
- memoizerific: 1.11.3
- react: 16.14.0
- react-dom: 16.14.0_react@16.14.0
- regenerator-runtime: 0.13.10
- store2: 2.14.2
- telejson: 6.0.8
- ts-dedent: 2.2.0
- util-deprecate: 1.0.2
- dev: true
-
- /@storybook/builder-webpack4/6.5.13_oj3zznhuofsh7mvlmigrvm3x7y:
- resolution: {integrity: sha512-Agqy3IKPv3Nl8QqdS7PjtqLp+c0BD8+/3A2ki/YfKqVz+F+J34EpbZlh3uU053avm1EoNQHSmhZok3ZlWH6O7A==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- typescript: '*'
- peerDependenciesMeta:
- typescript:
- optional: true
- dependencies:
- '@babel/core': 7.18.9
- '@storybook/addons': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@storybook/api': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@storybook/channel-postmessage': 6.5.13
- '@storybook/channels': 6.5.13
- '@storybook/client-api': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@storybook/client-logger': 6.5.13
- '@storybook/components': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@storybook/core-common': 6.5.13_oj3zznhuofsh7mvlmigrvm3x7y
- '@storybook/core-events': 6.5.13
- '@storybook/node-logger': 6.5.13
- '@storybook/preview-web': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@storybook/router': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@storybook/semver': 7.3.2
- '@storybook/store': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@storybook/theming': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@storybook/ui': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@types/node': 16.18.0
- '@types/webpack': 4.41.33
- autoprefixer: 9.8.8
- babel-loader: 8.2.5_7uc2ny5pnz7ums2wq2q562bf6y
- case-sensitive-paths-webpack-plugin: 2.4.0
- core-js: 3.26.0
- css-loader: 3.6.0_webpack@4.46.0
- file-loader: 6.2.0_webpack@4.46.0
- find-up: 5.0.0
- fork-ts-checker-webpack-plugin: 4.1.6_bbqhrndznz6a4k7d23h2kkrexi
- glob: 7.2.3
- glob-promise: 3.4.0_glob@7.2.3
- global: 4.4.0
- html-webpack-plugin: 4.5.2_webpack@4.46.0
- pnp-webpack-plugin: 1.6.4_typescript@4.4.4
- postcss: 7.0.39
- postcss-flexbugs-fixes: 4.2.1
- postcss-loader: 4.3.0_gzaxsinx64nntyd3vmdqwl7coe
- raw-loader: 4.0.2_webpack@4.46.0
- react: 16.14.0
- react-dom: 16.14.0_react@16.14.0
- stable: 0.1.8
- style-loader: 1.3.0_webpack@4.46.0
- terser-webpack-plugin: 4.2.3_webpack@4.46.0
- ts-dedent: 2.2.0
- typescript: 4.4.4
- url-loader: 4.1.1_lit45vopotvaqup7lrvlnvtxwy
- util-deprecate: 1.0.2
- webpack: 4.46.0
- webpack-dev-middleware: 3.7.3_webpack@4.46.0
- webpack-filter-warnings-plugin: 1.2.1_webpack@4.46.0
- webpack-hot-middleware: 2.25.2
- webpack-virtual-modules: 0.2.2
- transitivePeerDependencies:
- - bluebird
- - eslint
- - supports-color
- - vue-template-compiler
- - webpack-cli
- - webpack-command
- dev: true
-
- /@storybook/builder-webpack4/6.5.13_u5cwnb36e3nipolzgtjnnpepdu:
- resolution: {integrity: sha512-Agqy3IKPv3Nl8QqdS7PjtqLp+c0BD8+/3A2ki/YfKqVz+F+J34EpbZlh3uU053avm1EoNQHSmhZok3ZlWH6O7A==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- typescript: '*'
- peerDependenciesMeta:
- typescript:
- optional: true
- dependencies:
- '@babel/core': 7.18.9
- '@storybook/addons': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@storybook/api': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@storybook/channel-postmessage': 6.5.13
- '@storybook/channels': 6.5.13
- '@storybook/client-api': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@storybook/client-logger': 6.5.13
- '@storybook/components': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@storybook/core-common': 6.5.13_u5cwnb36e3nipolzgtjnnpepdu
- '@storybook/core-events': 6.5.13
- '@storybook/node-logger': 6.5.13
- '@storybook/preview-web': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@storybook/router': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@storybook/semver': 7.3.2
- '@storybook/store': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@storybook/theming': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@storybook/ui': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@types/node': 16.18.0
- '@types/webpack': 4.41.33
- autoprefixer: 9.8.8
- babel-loader: 8.2.5_7uc2ny5pnz7ums2wq2q562bf6y
- case-sensitive-paths-webpack-plugin: 2.4.0
- core-js: 3.26.0
- css-loader: 3.6.0_webpack@4.46.0
- file-loader: 6.2.0_webpack@4.46.0
- find-up: 5.0.0
- fork-ts-checker-webpack-plugin: 4.1.6_3n2x3j6farblcaf52bherr6og4
- glob: 7.2.3
- glob-promise: 3.4.0_glob@7.2.3
- global: 4.4.0
- html-webpack-plugin: 4.5.2_webpack@4.46.0
- pnp-webpack-plugin: 1.6.4_typescript@4.8.4
- postcss: 7.0.39
- postcss-flexbugs-fixes: 4.2.1
- postcss-loader: 4.3.0_gzaxsinx64nntyd3vmdqwl7coe
- raw-loader: 4.0.2_webpack@4.46.0
- react: 16.14.0
- react-dom: 16.14.0_react@16.14.0
- stable: 0.1.8
- style-loader: 1.3.0_webpack@4.46.0
- terser-webpack-plugin: 4.2.3_webpack@4.46.0
- ts-dedent: 2.2.0
- typescript: 4.8.4
- url-loader: 4.1.1_lit45vopotvaqup7lrvlnvtxwy
- util-deprecate: 1.0.2
- webpack: 4.46.0
- webpack-dev-middleware: 3.7.3_webpack@4.46.0
- webpack-filter-warnings-plugin: 1.2.1_webpack@4.46.0
- webpack-hot-middleware: 2.25.2
- webpack-virtual-modules: 0.2.2
- transitivePeerDependencies:
- - bluebird
- - eslint
- - supports-color
- - vue-template-compiler
- - webpack-cli
- - webpack-command
- dev: true
-
- /@storybook/channel-postmessage/6.5.13:
- resolution: {integrity: sha512-R79MBs0mQ7TV8M/a6x/SiTRyvZBidDfMEEthG7Cyo9p35JYiKOhj2535zhW4qlVMESBu95pwKYBibTjASoStPw==}
- dependencies:
- '@storybook/channels': 6.5.13
- '@storybook/client-logger': 6.5.13
- '@storybook/core-events': 6.5.13
- core-js: 3.26.0
- global: 4.4.0
- qs: 6.11.0
- telejson: 6.0.8
- dev: true
-
- /@storybook/channel-websocket/6.5.13:
- resolution: {integrity: sha512-kwh667H+tzCiNvs92GNwYOwVXdj9uHZyieRAN5rJtTBJ7XgLzGkpTEU50mWlbc0nDKhgE0qYvzyr5H393Iy5ug==}
- dependencies:
- '@storybook/channels': 6.5.13
- '@storybook/client-logger': 6.5.13
- core-js: 3.26.0
- global: 4.4.0
- telejson: 6.0.8
- dev: true
-
- /@storybook/channels/6.5.13:
- resolution: {integrity: sha512-sGYSilE30bz0jG+HdHnkv0B4XkAv2hP+KRZr4xmnv+MOOQpRnZpJ5Z3HVU16s17cj/83NWihKj6BuKcEVzyilg==}
- dependencies:
- core-js: 3.26.0
- ts-dedent: 2.2.0
- util-deprecate: 1.0.2
- dev: true
-
- /@storybook/client-api/6.5.13_wcqkhtmu7mswc6yz4uyexck3ty:
- resolution: {integrity: sha512-uH1mAWbidPiuuTdMUVEiuaNOfrYXm+9QLSP1MMYTKULqEOZI5MSOGkEDqRfVWxbYv/iWBOPTQ+OM9TQ6ecYacg==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- dependencies:
- '@storybook/addons': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@storybook/channel-postmessage': 6.5.13
- '@storybook/channels': 6.5.13
- '@storybook/client-logger': 6.5.13
- '@storybook/core-events': 6.5.13
- '@storybook/csf': 0.0.2--canary.4566f4d.1
- '@storybook/store': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@types/qs': 6.9.7
- '@types/webpack-env': 1.18.0
- core-js: 3.26.0
- fast-deep-equal: 3.1.3
- global: 4.4.0
- lodash: 4.17.21
- memoizerific: 1.11.3
- qs: 6.11.0
- react: 16.14.0
- react-dom: 16.14.0_react@16.14.0
- regenerator-runtime: 0.13.10
- store2: 2.14.2
- synchronous-promise: 2.0.16
- ts-dedent: 2.2.0
- util-deprecate: 1.0.2
- dev: true
-
- /@storybook/client-logger/6.5.13:
- resolution: {integrity: sha512-F2SMW3LWFGXLm2ENTwTitrLWJgmMXRf3CWQXdN2EbkNCIBHy5Zcbt+91K4OX8e2e5h9gjGfrdYbyYDYOoUCEfA==}
- dependencies:
- core-js: 3.26.0
- global: 4.4.0
- dev: true
-
- /@storybook/components/6.5.13:
- resolution: {integrity: sha512-6Hhx70JK5pGfKCkqMU4yq/BBH+vRTmzj7tZKfPwba+f8VmTMoOr/2ysTQFRtXryiHB6Z15xBYgfq5x2pIwQzLQ==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- dependencies:
- '@storybook/client-logger': 6.5.13
- '@storybook/csf': 0.0.2--canary.4566f4d.1
- '@storybook/theming': 6.5.13
- core-js: 3.26.0
- memoizerific: 1.11.3
- qs: 6.11.0
- regenerator-runtime: 0.13.10
- util-deprecate: 1.0.2
- dev: true
-
- /@storybook/components/6.5.13_@preact+compat@17.1.2:
- resolution: {integrity: sha512-6Hhx70JK5pGfKCkqMU4yq/BBH+vRTmzj7tZKfPwba+f8VmTMoOr/2ysTQFRtXryiHB6Z15xBYgfq5x2pIwQzLQ==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- dependencies:
- '@storybook/client-logger': 6.5.13
- '@storybook/csf': 0.0.2--canary.4566f4d.1
- '@storybook/theming': 6.5.13_@preact+compat@17.1.2
- core-js: 3.26.0
- memoizerific: 1.11.3
- qs: 6.11.0
- react: /@preact/compat/17.1.2_preact@10.6.5
- regenerator-runtime: 0.13.10
- util-deprecate: 1.0.2
- dev: true
-
- /@storybook/components/6.5.13_wcqkhtmu7mswc6yz4uyexck3ty:
- resolution: {integrity: sha512-6Hhx70JK5pGfKCkqMU4yq/BBH+vRTmzj7tZKfPwba+f8VmTMoOr/2ysTQFRtXryiHB6Z15xBYgfq5x2pIwQzLQ==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- dependencies:
- '@storybook/client-logger': 6.5.13
- '@storybook/csf': 0.0.2--canary.4566f4d.1
- '@storybook/theming': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- core-js: 3.26.0
- memoizerific: 1.11.3
- qs: 6.11.0
- react: 16.14.0
- react-dom: 16.14.0_react@16.14.0
- regenerator-runtime: 0.13.10
- util-deprecate: 1.0.2
- dev: true
-
- /@storybook/core-client/6.5.13_4z4xstjutcmkuoh5zkinl7lmym:
- resolution: {integrity: sha512-YuELbRokTBdqjbx/R4/7O4rou9kvbBIOJjlUkor9hdLLuJ3P0yGianERGNkZFfvcfMBAxU0p52o7QvDldSR3kA==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- typescript: '*'
- webpack: '*'
- peerDependenciesMeta:
- typescript:
- optional: true
- dependencies:
- '@storybook/addons': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@storybook/channel-postmessage': 6.5.13
- '@storybook/channel-websocket': 6.5.13
- '@storybook/client-api': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@storybook/client-logger': 6.5.13
- '@storybook/core-events': 6.5.13
- '@storybook/csf': 0.0.2--canary.4566f4d.1
- '@storybook/preview-web': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@storybook/store': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@storybook/ui': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- airbnb-js-shims: 2.2.1
- ansi-to-html: 0.6.15
- core-js: 3.26.0
- global: 4.4.0
- lodash: 4.17.21
- qs: 6.11.0
- react: 16.14.0
- react-dom: 16.14.0_react@16.14.0
- regenerator-runtime: 0.13.10
- ts-dedent: 2.2.0
- typescript: 4.4.4
- unfetch: 4.2.0
- util-deprecate: 1.0.2
- webpack: 5.74.0
- dev: true
-
- /@storybook/core-client/6.5.13_6dhvdvok4rm6xhvn3ntojenmae:
- resolution: {integrity: sha512-YuELbRokTBdqjbx/R4/7O4rou9kvbBIOJjlUkor9hdLLuJ3P0yGianERGNkZFfvcfMBAxU0p52o7QvDldSR3kA==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- typescript: '*'
- webpack: '*'
- peerDependenciesMeta:
- typescript:
- optional: true
- dependencies:
- '@storybook/addons': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@storybook/channel-postmessage': 6.5.13
- '@storybook/channel-websocket': 6.5.13
- '@storybook/client-api': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@storybook/client-logger': 6.5.13
- '@storybook/core-events': 6.5.13
- '@storybook/csf': 0.0.2--canary.4566f4d.1
- '@storybook/preview-web': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@storybook/store': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@storybook/ui': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- airbnb-js-shims: 2.2.1
- ansi-to-html: 0.6.15
- core-js: 3.26.0
- global: 4.4.0
- lodash: 4.17.21
- qs: 6.11.0
- react: 16.14.0
- react-dom: 16.14.0_react@16.14.0
- regenerator-runtime: 0.13.10
- ts-dedent: 2.2.0
- typescript: 4.4.4
- unfetch: 4.2.0
- util-deprecate: 1.0.2
- webpack: 4.46.0
- dev: true
-
- /@storybook/core-client/6.5.13_plmdyubmb7xm6euvqu3qohl7ea:
- resolution: {integrity: sha512-YuELbRokTBdqjbx/R4/7O4rou9kvbBIOJjlUkor9hdLLuJ3P0yGianERGNkZFfvcfMBAxU0p52o7QvDldSR3kA==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- typescript: '*'
- webpack: '*'
- peerDependenciesMeta:
- typescript:
- optional: true
- dependencies:
- '@storybook/addons': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@storybook/channel-postmessage': 6.5.13
- '@storybook/channel-websocket': 6.5.13
- '@storybook/client-api': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@storybook/client-logger': 6.5.13
- '@storybook/core-events': 6.5.13
- '@storybook/csf': 0.0.2--canary.4566f4d.1
- '@storybook/preview-web': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@storybook/store': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@storybook/ui': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- airbnb-js-shims: 2.2.1
- ansi-to-html: 0.6.15
- core-js: 3.26.0
- global: 4.4.0
- lodash: 4.17.21
- qs: 6.11.0
- react: 16.14.0
- react-dom: 16.14.0_react@16.14.0
- regenerator-runtime: 0.13.10
- ts-dedent: 2.2.0
- typescript: 4.8.4
- unfetch: 4.2.0
- util-deprecate: 1.0.2
- webpack: 5.74.0
- dev: true
-
- /@storybook/core-client/6.5.13_vvswzvegta47ikremfl73qk64u:
- resolution: {integrity: sha512-YuELbRokTBdqjbx/R4/7O4rou9kvbBIOJjlUkor9hdLLuJ3P0yGianERGNkZFfvcfMBAxU0p52o7QvDldSR3kA==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- typescript: '*'
- webpack: '*'
- peerDependenciesMeta:
- typescript:
- optional: true
- dependencies:
- '@storybook/addons': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@storybook/channel-postmessage': 6.5.13
- '@storybook/channel-websocket': 6.5.13
- '@storybook/client-api': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@storybook/client-logger': 6.5.13
- '@storybook/core-events': 6.5.13
- '@storybook/csf': 0.0.2--canary.4566f4d.1
- '@storybook/preview-web': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@storybook/store': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@storybook/ui': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- airbnb-js-shims: 2.2.1
- ansi-to-html: 0.6.15
- core-js: 3.26.0
- global: 4.4.0
- lodash: 4.17.21
- qs: 6.11.0
- react: 16.14.0
- react-dom: 16.14.0_react@16.14.0
- regenerator-runtime: 0.13.10
- ts-dedent: 2.2.0
- typescript: 4.8.4
- unfetch: 4.2.0
- util-deprecate: 1.0.2
- webpack: 4.46.0
- dev: true
-
- /@storybook/core-common/6.5.13_3rubbgt5ekhqrcgx4uwls3neim:
- resolution: {integrity: sha512-+DVZrRsteE9pw0X5MNffkdBgejQnbnL+UOG3qXkE9xxUamQALnuqS/w1BzpHE9WmOHuf7RWMKflyQEW3OLKAJg==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- typescript: '*'
- peerDependenciesMeta:
- typescript:
- optional: true
- dependencies:
- '@babel/core': 7.18.9
- '@babel/plugin-proposal-class-properties': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-proposal-decorators': 7.19.6_@babel+core@7.18.9
- '@babel/plugin-proposal-export-default-from': 7.18.10_@babel+core@7.18.9
- '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-proposal-object-rest-spread': 7.19.4_@babel+core@7.18.9
- '@babel/plugin-proposal-optional-chaining': 7.18.9_@babel+core@7.18.9
- '@babel/plugin-proposal-private-methods': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-proposal-private-property-in-object': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.18.9
- '@babel/plugin-transform-arrow-functions': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-transform-block-scoping': 7.19.4_@babel+core@7.18.9
- '@babel/plugin-transform-classes': 7.19.0_@babel+core@7.18.9
- '@babel/plugin-transform-destructuring': 7.19.4_@babel+core@7.18.9
- '@babel/plugin-transform-for-of': 7.18.8_@babel+core@7.18.9
- '@babel/plugin-transform-parameters': 7.18.8_@babel+core@7.18.9
- '@babel/plugin-transform-shorthand-properties': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-transform-spread': 7.19.0_@babel+core@7.18.9
- '@babel/preset-env': 7.19.4_@babel+core@7.18.9
- '@babel/preset-react': 7.18.6_@babel+core@7.18.9
- '@babel/preset-typescript': 7.18.6_@babel+core@7.18.9
- '@babel/register': 7.18.9_@babel+core@7.18.9
- '@storybook/node-logger': 6.5.13
- '@storybook/semver': 7.3.2
- '@types/node': 16.18.0
- '@types/pretty-hrtime': 1.0.1
- babel-loader: 8.2.5_7uc2ny5pnz7ums2wq2q562bf6y
- babel-plugin-macros: 3.1.0
- babel-plugin-polyfill-corejs3: 0.1.7_@babel+core@7.18.9
- chalk: 4.1.2
- core-js: 3.26.0
- express: 4.18.2
- file-system-cache: 1.1.0
- find-up: 5.0.0
- fork-ts-checker-webpack-plugin: 6.5.2_3n2x3j6farblcaf52bherr6og4
- fs-extra: 9.1.0
- glob: 7.2.3
- handlebars: 4.7.7
- interpret: 2.2.0
- json5: 2.2.1
- lazy-universal-dotenv: 3.0.1
- picomatch: 2.3.1
- pkg-dir: 5.0.0
- pretty-hrtime: 1.0.3
- resolve-from: 5.0.0
- slash: 3.0.0
- telejson: 6.0.8
- ts-dedent: 2.2.0
- typescript: 4.8.4
- util-deprecate: 1.0.2
- webpack: 4.46.0
- transitivePeerDependencies:
- - eslint
- - supports-color
- - vue-template-compiler
- - webpack-cli
- - webpack-command
- dev: true
-
- /@storybook/core-common/6.5.13_ipjevasairp6knvwkrpikcsgoq:
- resolution: {integrity: sha512-+DVZrRsteE9pw0X5MNffkdBgejQnbnL+UOG3qXkE9xxUamQALnuqS/w1BzpHE9WmOHuf7RWMKflyQEW3OLKAJg==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- typescript: '*'
- peerDependenciesMeta:
- typescript:
- optional: true
- dependencies:
- '@babel/core': 7.18.9
- '@babel/plugin-proposal-class-properties': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-proposal-decorators': 7.19.6_@babel+core@7.18.9
- '@babel/plugin-proposal-export-default-from': 7.18.10_@babel+core@7.18.9
- '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-proposal-object-rest-spread': 7.19.4_@babel+core@7.18.9
- '@babel/plugin-proposal-optional-chaining': 7.18.9_@babel+core@7.18.9
- '@babel/plugin-proposal-private-methods': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-proposal-private-property-in-object': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.18.9
- '@babel/plugin-transform-arrow-functions': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-transform-block-scoping': 7.19.4_@babel+core@7.18.9
- '@babel/plugin-transform-classes': 7.19.0_@babel+core@7.18.9
- '@babel/plugin-transform-destructuring': 7.19.4_@babel+core@7.18.9
- '@babel/plugin-transform-for-of': 7.18.8_@babel+core@7.18.9
- '@babel/plugin-transform-parameters': 7.18.8_@babel+core@7.18.9
- '@babel/plugin-transform-shorthand-properties': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-transform-spread': 7.19.0_@babel+core@7.18.9
- '@babel/preset-env': 7.19.4_@babel+core@7.18.9
- '@babel/preset-react': 7.18.6_@babel+core@7.18.9
- '@babel/preset-typescript': 7.18.6_@babel+core@7.18.9
- '@babel/register': 7.18.9_@babel+core@7.18.9
- '@storybook/node-logger': 6.5.13
- '@storybook/semver': 7.3.2
- '@types/node': 16.18.0
- '@types/pretty-hrtime': 1.0.1
- babel-loader: 8.2.5_7uc2ny5pnz7ums2wq2q562bf6y
- babel-plugin-macros: 3.1.0
- babel-plugin-polyfill-corejs3: 0.1.7_@babel+core@7.18.9
- chalk: 4.1.2
- core-js: 3.26.0
- express: 4.18.2
- file-system-cache: 1.1.0
- find-up: 5.0.0
- fork-ts-checker-webpack-plugin: 6.5.2_bbqhrndznz6a4k7d23h2kkrexi
- fs-extra: 9.1.0
- glob: 7.2.3
- handlebars: 4.7.7
- interpret: 2.2.0
- json5: 2.2.1
- lazy-universal-dotenv: 3.0.1
- picomatch: 2.3.1
- pkg-dir: 5.0.0
- pretty-hrtime: 1.0.3
- react: /@preact/compat/17.1.2_preact@10.6.5
- resolve-from: 5.0.0
- slash: 3.0.0
- telejson: 6.0.8
- ts-dedent: 2.2.0
- typescript: 4.4.4
- util-deprecate: 1.0.2
- webpack: 4.46.0
- transitivePeerDependencies:
- - eslint
- - supports-color
- - vue-template-compiler
- - webpack-cli
- - webpack-command
- dev: true
-
- /@storybook/core-common/6.5.13_oj3zznhuofsh7mvlmigrvm3x7y:
- resolution: {integrity: sha512-+DVZrRsteE9pw0X5MNffkdBgejQnbnL+UOG3qXkE9xxUamQALnuqS/w1BzpHE9WmOHuf7RWMKflyQEW3OLKAJg==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- typescript: '*'
- peerDependenciesMeta:
- typescript:
- optional: true
- dependencies:
- '@babel/core': 7.18.9
- '@babel/plugin-proposal-class-properties': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-proposal-decorators': 7.19.6_@babel+core@7.18.9
- '@babel/plugin-proposal-export-default-from': 7.18.10_@babel+core@7.18.9
- '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-proposal-object-rest-spread': 7.19.4_@babel+core@7.18.9
- '@babel/plugin-proposal-optional-chaining': 7.18.9_@babel+core@7.18.9
- '@babel/plugin-proposal-private-methods': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-proposal-private-property-in-object': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.18.9
- '@babel/plugin-transform-arrow-functions': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-transform-block-scoping': 7.19.4_@babel+core@7.18.9
- '@babel/plugin-transform-classes': 7.19.0_@babel+core@7.18.9
- '@babel/plugin-transform-destructuring': 7.19.4_@babel+core@7.18.9
- '@babel/plugin-transform-for-of': 7.18.8_@babel+core@7.18.9
- '@babel/plugin-transform-parameters': 7.18.8_@babel+core@7.18.9
- '@babel/plugin-transform-shorthand-properties': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-transform-spread': 7.19.0_@babel+core@7.18.9
- '@babel/preset-env': 7.19.4_@babel+core@7.18.9
- '@babel/preset-react': 7.18.6_@babel+core@7.18.9
- '@babel/preset-typescript': 7.18.6_@babel+core@7.18.9
- '@babel/register': 7.18.9_@babel+core@7.18.9
- '@storybook/node-logger': 6.5.13
- '@storybook/semver': 7.3.2
- '@types/node': 16.18.0
- '@types/pretty-hrtime': 1.0.1
- babel-loader: 8.2.5_7uc2ny5pnz7ums2wq2q562bf6y
- babel-plugin-macros: 3.1.0
- babel-plugin-polyfill-corejs3: 0.1.7_@babel+core@7.18.9
- chalk: 4.1.2
- core-js: 3.26.0
- express: 4.18.2
- file-system-cache: 1.1.0
- find-up: 5.0.0
- fork-ts-checker-webpack-plugin: 6.5.2_bbqhrndznz6a4k7d23h2kkrexi
- fs-extra: 9.1.0
- glob: 7.2.3
- handlebars: 4.7.7
- interpret: 2.2.0
- json5: 2.2.1
- lazy-universal-dotenv: 3.0.1
- picomatch: 2.3.1
- pkg-dir: 5.0.0
- pretty-hrtime: 1.0.3
- react: 16.14.0
- react-dom: 16.14.0_react@16.14.0
- resolve-from: 5.0.0
- slash: 3.0.0
- telejson: 6.0.8
- ts-dedent: 2.2.0
- typescript: 4.4.4
- util-deprecate: 1.0.2
- webpack: 4.46.0
- transitivePeerDependencies:
- - eslint
- - supports-color
- - vue-template-compiler
- - webpack-cli
- - webpack-command
- dev: true
-
- /@storybook/core-common/6.5.13_u5cwnb36e3nipolzgtjnnpepdu:
- resolution: {integrity: sha512-+DVZrRsteE9pw0X5MNffkdBgejQnbnL+UOG3qXkE9xxUamQALnuqS/w1BzpHE9WmOHuf7RWMKflyQEW3OLKAJg==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- typescript: '*'
- peerDependenciesMeta:
- typescript:
- optional: true
- dependencies:
- '@babel/core': 7.18.9
- '@babel/plugin-proposal-class-properties': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-proposal-decorators': 7.19.6_@babel+core@7.18.9
- '@babel/plugin-proposal-export-default-from': 7.18.10_@babel+core@7.18.9
- '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-proposal-object-rest-spread': 7.19.4_@babel+core@7.18.9
- '@babel/plugin-proposal-optional-chaining': 7.18.9_@babel+core@7.18.9
- '@babel/plugin-proposal-private-methods': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-proposal-private-property-in-object': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.18.9
- '@babel/plugin-transform-arrow-functions': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-transform-block-scoping': 7.19.4_@babel+core@7.18.9
- '@babel/plugin-transform-classes': 7.19.0_@babel+core@7.18.9
- '@babel/plugin-transform-destructuring': 7.19.4_@babel+core@7.18.9
- '@babel/plugin-transform-for-of': 7.18.8_@babel+core@7.18.9
- '@babel/plugin-transform-parameters': 7.18.8_@babel+core@7.18.9
- '@babel/plugin-transform-shorthand-properties': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-transform-spread': 7.19.0_@babel+core@7.18.9
- '@babel/preset-env': 7.19.4_@babel+core@7.18.9
- '@babel/preset-react': 7.18.6_@babel+core@7.18.9
- '@babel/preset-typescript': 7.18.6_@babel+core@7.18.9
- '@babel/register': 7.18.9_@babel+core@7.18.9
- '@storybook/node-logger': 6.5.13
- '@storybook/semver': 7.3.2
- '@types/node': 16.18.0
- '@types/pretty-hrtime': 1.0.1
- babel-loader: 8.2.5_7uc2ny5pnz7ums2wq2q562bf6y
- babel-plugin-macros: 3.1.0
- babel-plugin-polyfill-corejs3: 0.1.7_@babel+core@7.18.9
- chalk: 4.1.2
- core-js: 3.26.0
- express: 4.18.2
- file-system-cache: 1.1.0
- find-up: 5.0.0
- fork-ts-checker-webpack-plugin: 6.5.2_3n2x3j6farblcaf52bherr6og4
- fs-extra: 9.1.0
- glob: 7.2.3
- handlebars: 4.7.7
- interpret: 2.2.0
- json5: 2.2.1
- lazy-universal-dotenv: 3.0.1
- picomatch: 2.3.1
- pkg-dir: 5.0.0
- pretty-hrtime: 1.0.3
- react: 16.14.0
- react-dom: 16.14.0_react@16.14.0
- resolve-from: 5.0.0
- slash: 3.0.0
- telejson: 6.0.8
- ts-dedent: 2.2.0
- typescript: 4.8.4
- util-deprecate: 1.0.2
- webpack: 4.46.0
- transitivePeerDependencies:
- - eslint
- - supports-color
- - vue-template-compiler
- - webpack-cli
- - webpack-command
- dev: true
-
- /@storybook/core-events/6.5.13:
- resolution: {integrity: sha512-kL745tPpRKejzHToA3/CoBNbI+NPRVk186vGxXBmk95OEg0TlwgQExP8BnqEtLlRZMbW08e4+6kilc1M1M4N5w==}
- dependencies:
- core-js: 3.26.0
- dev: true
-
- /@storybook/core-server/6.5.13_oj3zznhuofsh7mvlmigrvm3x7y:
- resolution: {integrity: sha512-vs7tu3kAnFwuINio1p87WyqDNlFyZESmeh9s7vvrZVbe/xS/ElqDscr9DT5seW+jbtxufAaHsx+JUTver1dheQ==}
- peerDependencies:
- '@storybook/builder-webpack5': '*'
- '@storybook/manager-webpack5': '*'
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- typescript: '*'
- peerDependenciesMeta:
- '@storybook/builder-webpack5':
- optional: true
- '@storybook/manager-webpack5':
- optional: true
- typescript:
- optional: true
- dependencies:
- '@discoveryjs/json-ext': 0.5.7
- '@storybook/builder-webpack4': 6.5.13_oj3zznhuofsh7mvlmigrvm3x7y
- '@storybook/core-client': 6.5.13_6dhvdvok4rm6xhvn3ntojenmae
- '@storybook/core-common': 6.5.13_oj3zznhuofsh7mvlmigrvm3x7y
- '@storybook/core-events': 6.5.13
- '@storybook/csf': 0.0.2--canary.4566f4d.1
- '@storybook/csf-tools': 6.5.13
- '@storybook/manager-webpack4': 6.5.13_oj3zznhuofsh7mvlmigrvm3x7y
- '@storybook/node-logger': 6.5.13
- '@storybook/semver': 7.3.2
- '@storybook/store': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@storybook/telemetry': 6.5.13_oj3zznhuofsh7mvlmigrvm3x7y
- '@types/node': 16.18.0
- '@types/node-fetch': 2.6.2
- '@types/pretty-hrtime': 1.0.1
- '@types/webpack': 4.41.33
- better-opn: 2.1.1
- boxen: 5.1.2
- chalk: 4.1.2
- cli-table3: 0.6.3
- commander: 6.2.1
- compression: 1.7.4
- core-js: 3.26.0
- cpy: 8.1.2
- detect-port: 1.5.1
- express: 4.18.2
- fs-extra: 9.1.0
- global: 4.4.0
- globby: 11.1.0
- ip: 2.0.0
- lodash: 4.17.21
- node-fetch: 2.6.7
- open: 8.4.0
- pretty-hrtime: 1.0.3
- prompts: 2.4.2
- react: 16.14.0
- react-dom: 16.14.0_react@16.14.0
- regenerator-runtime: 0.13.10
- serve-favicon: 2.5.0
- slash: 3.0.0
- telejson: 6.0.8
- ts-dedent: 2.2.0
- typescript: 4.4.4
- util-deprecate: 1.0.2
- watchpack: 2.4.0
- webpack: 4.46.0
- ws: 8.10.0
- x-default-browser: 0.4.0
- transitivePeerDependencies:
- - '@storybook/mdx2-csf'
- - bluebird
- - bufferutil
- - encoding
- - eslint
- - supports-color
- - utf-8-validate
- - vue-template-compiler
- - webpack-cli
- - webpack-command
- dev: true
-
- /@storybook/core-server/6.5.13_u5cwnb36e3nipolzgtjnnpepdu:
- resolution: {integrity: sha512-vs7tu3kAnFwuINio1p87WyqDNlFyZESmeh9s7vvrZVbe/xS/ElqDscr9DT5seW+jbtxufAaHsx+JUTver1dheQ==}
- peerDependencies:
- '@storybook/builder-webpack5': '*'
- '@storybook/manager-webpack5': '*'
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- typescript: '*'
- peerDependenciesMeta:
- '@storybook/builder-webpack5':
- optional: true
- '@storybook/manager-webpack5':
- optional: true
- typescript:
- optional: true
- dependencies:
- '@discoveryjs/json-ext': 0.5.7
- '@storybook/builder-webpack4': 6.5.13_u5cwnb36e3nipolzgtjnnpepdu
- '@storybook/core-client': 6.5.13_vvswzvegta47ikremfl73qk64u
- '@storybook/core-common': 6.5.13_u5cwnb36e3nipolzgtjnnpepdu
- '@storybook/core-events': 6.5.13
- '@storybook/csf': 0.0.2--canary.4566f4d.1
- '@storybook/csf-tools': 6.5.13
- '@storybook/manager-webpack4': 6.5.13_u5cwnb36e3nipolzgtjnnpepdu
- '@storybook/node-logger': 6.5.13
- '@storybook/semver': 7.3.2
- '@storybook/store': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@storybook/telemetry': 6.5.13_u5cwnb36e3nipolzgtjnnpepdu
- '@types/node': 16.18.0
- '@types/node-fetch': 2.6.2
- '@types/pretty-hrtime': 1.0.1
- '@types/webpack': 4.41.33
- better-opn: 2.1.1
- boxen: 5.1.2
- chalk: 4.1.2
- cli-table3: 0.6.3
- commander: 6.2.1
- compression: 1.7.4
- core-js: 3.26.0
- cpy: 8.1.2
- detect-port: 1.5.1
- express: 4.18.2
- fs-extra: 9.1.0
- global: 4.4.0
- globby: 11.1.0
- ip: 2.0.0
- lodash: 4.17.21
- node-fetch: 2.6.7
- open: 8.4.0
- pretty-hrtime: 1.0.3
- prompts: 2.4.2
- react: 16.14.0
- react-dom: 16.14.0_react@16.14.0
- regenerator-runtime: 0.13.10
- serve-favicon: 2.5.0
- slash: 3.0.0
- telejson: 6.0.8
- ts-dedent: 2.2.0
- typescript: 4.8.4
- util-deprecate: 1.0.2
- watchpack: 2.4.0
- webpack: 4.46.0
- ws: 8.10.0
- x-default-browser: 0.4.0
- transitivePeerDependencies:
- - '@storybook/mdx2-csf'
- - bluebird
- - bufferutil
- - encoding
- - eslint
- - supports-color
- - utf-8-validate
- - vue-template-compiler
- - webpack-cli
- - webpack-command
- dev: true
-
- /@storybook/core/6.5.13_cadditq4xyv3neitvabz3hzhjy:
- resolution: {integrity: sha512-kw1lCgbsxzUimGww6t5rmuWJmFPe9kGGyzIqvj4RC4BBcEsP40LEu9XhSfvnb8vTOLIULFZeZpdRFfJs4TYbUw==}
- peerDependencies:
- '@storybook/builder-webpack5': '*'
- '@storybook/manager-webpack5': '*'
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- typescript: '*'
- webpack: '*'
- peerDependenciesMeta:
- '@storybook/builder-webpack5':
- optional: true
- '@storybook/manager-webpack5':
- optional: true
- typescript:
- optional: true
- dependencies:
- '@storybook/core-client': 6.5.13_plmdyubmb7xm6euvqu3qohl7ea
- '@storybook/core-server': 6.5.13_u5cwnb36e3nipolzgtjnnpepdu
- react: 16.14.0
- react-dom: 16.14.0_react@16.14.0
- typescript: 4.8.4
- webpack: 5.74.0
- transitivePeerDependencies:
- - '@storybook/mdx2-csf'
- - bluebird
- - bufferutil
- - encoding
- - eslint
- - supports-color
- - utf-8-validate
- - vue-template-compiler
- - webpack-cli
- - webpack-command
- dev: true
-
- /@storybook/core/6.5.13_ujfskrypehrywuobbsgnsb3724:
- resolution: {integrity: sha512-kw1lCgbsxzUimGww6t5rmuWJmFPe9kGGyzIqvj4RC4BBcEsP40LEu9XhSfvnb8vTOLIULFZeZpdRFfJs4TYbUw==}
- peerDependencies:
- '@storybook/builder-webpack5': '*'
- '@storybook/manager-webpack5': '*'
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- typescript: '*'
- webpack: '*'
- peerDependenciesMeta:
- '@storybook/builder-webpack5':
- optional: true
- '@storybook/manager-webpack5':
- optional: true
- typescript:
- optional: true
- dependencies:
- '@storybook/core-client': 6.5.13_4z4xstjutcmkuoh5zkinl7lmym
- '@storybook/core-server': 6.5.13_oj3zznhuofsh7mvlmigrvm3x7y
- react: 16.14.0
- react-dom: 16.14.0_react@16.14.0
- typescript: 4.4.4
- webpack: 5.74.0
- transitivePeerDependencies:
- - '@storybook/mdx2-csf'
- - bluebird
- - bufferutil
- - encoding
- - eslint
- - supports-color
- - utf-8-validate
- - vue-template-compiler
- - webpack-cli
- - webpack-command
- dev: true
-
- /@storybook/csf-tools/6.5.13:
- resolution: {integrity: sha512-63Ev+VmBqzwSwfUzbuXOLKBD5dMTK2zBYLQ9anTVw70FuTikwTsGIbPgb098K0vsxRCgxl7KM7NpivHqtZtdjw==}
- peerDependencies:
- '@storybook/mdx2-csf': ^0.0.3
- peerDependenciesMeta:
- '@storybook/mdx2-csf':
- optional: true
- dependencies:
- '@babel/core': 7.18.9
- '@babel/generator': 7.19.6
- '@babel/parser': 7.19.6
- '@babel/plugin-transform-react-jsx': 7.19.0_@babel+core@7.18.9
- '@babel/preset-env': 7.19.4_@babel+core@7.18.9
- '@babel/traverse': 7.19.6
- '@babel/types': 7.19.4
- '@storybook/csf': 0.0.2--canary.4566f4d.1
- '@storybook/mdx1-csf': 0.0.1_@babel+core@7.18.9
- core-js: 3.26.0
- fs-extra: 9.1.0
- global: 4.4.0
- regenerator-runtime: 0.13.10
- ts-dedent: 2.2.0
- transitivePeerDependencies:
- - supports-color
+ /@sindresorhus/is@5.6.0:
+ resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==}
+ engines: {node: '>=14.16'}
dev: true
- /@storybook/csf/0.0.2--canary.4566f4d.1:
- resolution: {integrity: sha512-9OVvMVh3t9znYZwb0Svf/YQoxX2gVOeQTGe2bses2yj+a3+OJnCrUF3/hGv6Em7KujtOdL2LL+JnG49oMVGFgQ==}
- dependencies:
- lodash: 4.17.21
- dev: true
-
- /@storybook/docs-tools/6.5.13:
- resolution: {integrity: sha512-hB+hk+895ny4SW84j3X5iV55DHs3bCfTOp7cDdcZJdQrlm0wuDb4A6d4ffNC7ZLh9VkUjU6ST4VEV5Bb0Cptow==}
- dependencies:
- '@babel/core': 7.18.9
- '@storybook/csf': 0.0.2--canary.4566f4d.1
- '@storybook/store': 6.5.13
- core-js: 3.26.0
- doctrine: 3.0.0
- lodash: 4.17.21
- regenerator-runtime: 0.13.10
- transitivePeerDependencies:
- - react
- - react-dom
- - supports-color
- dev: true
-
- /@storybook/docs-tools/6.5.13_@preact+compat@17.1.2:
- resolution: {integrity: sha512-hB+hk+895ny4SW84j3X5iV55DHs3bCfTOp7cDdcZJdQrlm0wuDb4A6d4ffNC7ZLh9VkUjU6ST4VEV5Bb0Cptow==}
- dependencies:
- '@babel/core': 7.18.9
- '@storybook/csf': 0.0.2--canary.4566f4d.1
- '@storybook/store': 6.5.13_@preact+compat@17.1.2
- core-js: 3.26.0
- doctrine: 3.0.0
- lodash: 4.17.21
- regenerator-runtime: 0.13.10
- transitivePeerDependencies:
- - react
- - react-dom
- - supports-color
+ /@sindresorhus/merge-streams@1.0.0:
+ resolution: {integrity: sha512-rUV5WyJrJLoloD4NDN1V1+LDMDWOa4OTsT4yYJwQNpTU6FWxkxHpL7eu4w+DmiH8x/EAM1otkPE1+LaspIbplw==}
+ engines: {node: '>=18'}
dev: true
- /@storybook/manager-webpack4/6.5.13_oj3zznhuofsh7mvlmigrvm3x7y:
- resolution: {integrity: sha512-pURzS5W3XM0F7bCBWzpl7TRsuy+OXFwLXiWLaexuvo0POZe31Ueo2A1R4rx3MT5Iee8O9mYvG2XTmvK9MlLefQ==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- typescript: '*'
- peerDependenciesMeta:
- typescript:
- optional: true
- dependencies:
- '@babel/core': 7.18.9
- '@babel/plugin-transform-template-literals': 7.18.9_@babel+core@7.18.9
- '@babel/preset-react': 7.18.6_@babel+core@7.18.9
- '@storybook/addons': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@storybook/core-client': 6.5.13_6dhvdvok4rm6xhvn3ntojenmae
- '@storybook/core-common': 6.5.13_oj3zznhuofsh7mvlmigrvm3x7y
- '@storybook/node-logger': 6.5.13
- '@storybook/theming': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@storybook/ui': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@types/node': 16.18.0
- '@types/webpack': 4.41.33
- babel-loader: 8.2.5_7uc2ny5pnz7ums2wq2q562bf6y
- case-sensitive-paths-webpack-plugin: 2.4.0
- chalk: 4.1.2
- core-js: 3.26.0
- css-loader: 3.6.0_webpack@4.46.0
- express: 4.18.2
- file-loader: 6.2.0_webpack@4.46.0
- find-up: 5.0.0
- fs-extra: 9.1.0
- html-webpack-plugin: 4.5.2_webpack@4.46.0
- node-fetch: 2.6.7
- pnp-webpack-plugin: 1.6.4_typescript@4.4.4
- react: 16.14.0
- react-dom: 16.14.0_react@16.14.0
- read-pkg-up: 7.0.1
- regenerator-runtime: 0.13.10
- resolve-from: 5.0.0
- style-loader: 1.3.0_webpack@4.46.0
- telejson: 6.0.8
- terser-webpack-plugin: 4.2.3_webpack@4.46.0
- ts-dedent: 2.2.0
- typescript: 4.4.4
- url-loader: 4.1.1_lit45vopotvaqup7lrvlnvtxwy
- util-deprecate: 1.0.2
- webpack: 4.46.0
- webpack-dev-middleware: 3.7.3_webpack@4.46.0
- webpack-virtual-modules: 0.2.2
- transitivePeerDependencies:
- - bluebird
- - encoding
- - eslint
- - supports-color
- - vue-template-compiler
- - webpack-cli
- - webpack-command
- dev: true
-
- /@storybook/manager-webpack4/6.5.13_u5cwnb36e3nipolzgtjnnpepdu:
- resolution: {integrity: sha512-pURzS5W3XM0F7bCBWzpl7TRsuy+OXFwLXiWLaexuvo0POZe31Ueo2A1R4rx3MT5Iee8O9mYvG2XTmvK9MlLefQ==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- typescript: '*'
- peerDependenciesMeta:
- typescript:
- optional: true
- dependencies:
- '@babel/core': 7.18.9
- '@babel/plugin-transform-template-literals': 7.18.9_@babel+core@7.18.9
- '@babel/preset-react': 7.18.6_@babel+core@7.18.9
- '@storybook/addons': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@storybook/core-client': 6.5.13_vvswzvegta47ikremfl73qk64u
- '@storybook/core-common': 6.5.13_u5cwnb36e3nipolzgtjnnpepdu
- '@storybook/node-logger': 6.5.13
- '@storybook/theming': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@storybook/ui': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@types/node': 16.18.0
- '@types/webpack': 4.41.33
- babel-loader: 8.2.5_7uc2ny5pnz7ums2wq2q562bf6y
- case-sensitive-paths-webpack-plugin: 2.4.0
- chalk: 4.1.2
- core-js: 3.26.0
- css-loader: 3.6.0_webpack@4.46.0
- express: 4.18.2
- file-loader: 6.2.0_webpack@4.46.0
- find-up: 5.0.0
- fs-extra: 9.1.0
- html-webpack-plugin: 4.5.2_webpack@4.46.0
- node-fetch: 2.6.7
- pnp-webpack-plugin: 1.6.4_typescript@4.8.4
- react: 16.14.0
- react-dom: 16.14.0_react@16.14.0
- read-pkg-up: 7.0.1
- regenerator-runtime: 0.13.10
- resolve-from: 5.0.0
- style-loader: 1.3.0_webpack@4.46.0
- telejson: 6.0.8
- terser-webpack-plugin: 4.2.3_webpack@4.46.0
- ts-dedent: 2.2.0
- typescript: 4.8.4
- url-loader: 4.1.1_lit45vopotvaqup7lrvlnvtxwy
- util-deprecate: 1.0.2
- webpack: 4.46.0
- webpack-dev-middleware: 3.7.3_webpack@4.46.0
- webpack-virtual-modules: 0.2.2
- transitivePeerDependencies:
- - bluebird
- - encoding
- - eslint
- - supports-color
- - vue-template-compiler
- - webpack-cli
- - webpack-command
- dev: true
-
- /@storybook/mdx1-csf/0.0.1_@babel+core@7.18.9:
- resolution: {integrity: sha512-4biZIWWzoWlCarMZmTpqcJNgo/RBesYZwGFbQeXiGYsswuvfWARZnW9RE9aUEMZ4XPn7B1N3EKkWcdcWe/K2tg==}
- dependencies:
- '@babel/generator': 7.19.6
- '@babel/parser': 7.19.6
- '@babel/preset-env': 7.19.4_@babel+core@7.18.9
- '@babel/types': 7.19.4
- '@mdx-js/mdx': 1.6.22
- '@types/lodash': 4.14.186
- js-string-escape: 1.0.1
- loader-utils: 2.0.3
- lodash: 4.17.21
- prettier: 2.3.0
- ts-dedent: 2.2.0
- transitivePeerDependencies:
- - '@babel/core'
- - supports-color
- dev: true
-
- /@storybook/node-logger/6.5.13:
- resolution: {integrity: sha512-/r5aVZAqZRoy5FyNk/G4pj7yKJd3lJfPbAaOHVROv2IF7PJP/vtRaDkcfh0g2U6zwuDxGIqSn80j+qoEli9m5A==}
- dependencies:
- '@types/npmlog': 4.1.4
- chalk: 4.1.2
- core-js: 3.26.0
- npmlog: 5.0.1
- pretty-hrtime: 1.0.3
- dev: true
-
- /@storybook/postinstall/6.5.13:
- resolution: {integrity: sha512-qmqP39FGIP5NdhXC5IpAs9cFoYx9fg1psoQKwb9snYb98eVQU31uHc1W2MBUh3lG4AjAm7pQaXJci7ti4jOh3g==}
- dependencies:
- core-js: 3.26.0
- dev: true
-
- /@storybook/preact/6.5.13_q57tjbu375p52k2lkxkownwouu:
- resolution: {integrity: sha512-5/ufRgxh5VypFcOeIBQMg/AqZQ2+KfUw4Glo9HU75dbIe/kYqSnN3+5SEv8J6ykxHHtUWcmnILS1r7/I5R7j/w==}
- engines: {node: '>=10.13.0'}
- hasBin: true
- peerDependencies:
- '@babel/core': '*'
- preact: ^8.0.0||^10.0.0
- dependencies:
- '@babel/core': 7.18.9
- '@babel/plugin-transform-react-jsx': 7.19.0_@babel+core@7.18.9
- '@storybook/addons': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@storybook/core': 6.5.13_ujfskrypehrywuobbsgnsb3724
- '@storybook/core-common': 6.5.13_oj3zznhuofsh7mvlmigrvm3x7y
- '@storybook/csf': 0.0.2--canary.4566f4d.1
- '@storybook/store': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@types/node': 16.18.0
- '@types/webpack-env': 1.18.0
- core-js: 3.26.0
- global: 4.4.0
- preact: 10.6.5
- react: 16.14.0
- react-dom: 16.14.0_react@16.14.0
- read-pkg-up: 7.0.1
- regenerator-runtime: 0.13.10
- ts-dedent: 2.2.0
- webpack: 5.74.0
- transitivePeerDependencies:
- - '@storybook/builder-webpack5'
- - '@storybook/manager-webpack5'
- - '@storybook/mdx2-csf'
- - '@swc/core'
- - bluebird
- - bufferutil
- - encoding
- - esbuild
- - eslint
- - supports-color
- - typescript
- - uglify-js
- - utf-8-validate
- - vue-template-compiler
- - webpack-cli
- - webpack-command
- dev: true
-
- /@storybook/preact/6.5.13_xeiodxunknjqnq4up5ebnv6gwe:
- resolution: {integrity: sha512-5/ufRgxh5VypFcOeIBQMg/AqZQ2+KfUw4Glo9HU75dbIe/kYqSnN3+5SEv8J6ykxHHtUWcmnILS1r7/I5R7j/w==}
- engines: {node: '>=10.13.0'}
- hasBin: true
- peerDependencies:
- '@babel/core': '*'
- preact: ^8.0.0||^10.0.0
- dependencies:
- '@babel/core': 7.18.9
- '@babel/plugin-transform-react-jsx': 7.19.0_@babel+core@7.18.9
- '@storybook/addons': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@storybook/core': 6.5.13_cadditq4xyv3neitvabz3hzhjy
- '@storybook/core-common': 6.5.13_u5cwnb36e3nipolzgtjnnpepdu
- '@storybook/csf': 0.0.2--canary.4566f4d.1
- '@storybook/store': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@types/node': 16.18.0
- '@types/webpack-env': 1.18.0
- core-js: 3.26.0
- global: 4.4.0
- preact: 10.11.2
- react: 16.14.0
- react-dom: 16.14.0_react@16.14.0
- read-pkg-up: 7.0.1
- regenerator-runtime: 0.13.10
- ts-dedent: 2.2.0
- webpack: 5.74.0
- transitivePeerDependencies:
- - '@storybook/builder-webpack5'
- - '@storybook/manager-webpack5'
- - '@storybook/mdx2-csf'
- - '@swc/core'
- - bluebird
- - bufferutil
- - encoding
- - esbuild
- - eslint
- - supports-color
- - typescript
- - uglify-js
- - utf-8-validate
- - vue-template-compiler
- - webpack-cli
- - webpack-command
- dev: true
-
- /@storybook/preset-scss/1.0.3_sass-loader@10.1.1:
- resolution: {integrity: sha512-o9Iz6wxPeNENrQa2mKlsDKynBfqU2uWaRP80HeWp4TkGgf7/x3DVF2O7yi9N0x/PI1qzzTTpxlQ90D62XmpiTw==}
- peerDependencies:
- css-loader: '*'
- sass-loader: '*'
- style-loader: '*'
- dependencies:
- sass-loader: 10.1.1_sass@1.55.0
- dev: true
-
- /@storybook/preview-web/6.5.13:
- resolution: {integrity: sha512-GNNYVzw4SmRua3dOc52Ye6Us4iQbq5GKQ56U3iwnzZM3TBdJB+Rft94Fn1/pypHujEHS8hl5Xgp9td6C1lLCow==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- dependencies:
- '@storybook/addons': 6.5.13
- '@storybook/channel-postmessage': 6.5.13
- '@storybook/client-logger': 6.5.13
- '@storybook/core-events': 6.5.13
- '@storybook/csf': 0.0.2--canary.4566f4d.1
- '@storybook/store': 6.5.13
- ansi-to-html: 0.6.15
- core-js: 3.26.0
- global: 4.4.0
- lodash: 4.17.21
- qs: 6.11.0
- regenerator-runtime: 0.13.10
- synchronous-promise: 2.0.16
- ts-dedent: 2.2.0
- unfetch: 4.2.0
- util-deprecate: 1.0.2
- dev: true
-
- /@storybook/preview-web/6.5.13_@preact+compat@17.1.2:
- resolution: {integrity: sha512-GNNYVzw4SmRua3dOc52Ye6Us4iQbq5GKQ56U3iwnzZM3TBdJB+Rft94Fn1/pypHujEHS8hl5Xgp9td6C1lLCow==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- dependencies:
- '@storybook/addons': 6.5.13_@preact+compat@17.1.2
- '@storybook/channel-postmessage': 6.5.13
- '@storybook/client-logger': 6.5.13
- '@storybook/core-events': 6.5.13
- '@storybook/csf': 0.0.2--canary.4566f4d.1
- '@storybook/store': 6.5.13_@preact+compat@17.1.2
- ansi-to-html: 0.6.15
- core-js: 3.26.0
- global: 4.4.0
- lodash: 4.17.21
- qs: 6.11.0
- react: /@preact/compat/17.1.2_preact@10.6.5
- regenerator-runtime: 0.13.10
- synchronous-promise: 2.0.16
- ts-dedent: 2.2.0
- unfetch: 4.2.0
- util-deprecate: 1.0.2
- dev: true
-
- /@storybook/preview-web/6.5.13_wcqkhtmu7mswc6yz4uyexck3ty:
- resolution: {integrity: sha512-GNNYVzw4SmRua3dOc52Ye6Us4iQbq5GKQ56U3iwnzZM3TBdJB+Rft94Fn1/pypHujEHS8hl5Xgp9td6C1lLCow==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- dependencies:
- '@storybook/addons': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@storybook/channel-postmessage': 6.5.13
- '@storybook/client-logger': 6.5.13
- '@storybook/core-events': 6.5.13
- '@storybook/csf': 0.0.2--canary.4566f4d.1
- '@storybook/store': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- ansi-to-html: 0.6.15
- core-js: 3.26.0
- global: 4.4.0
- lodash: 4.17.21
- qs: 6.11.0
- react: 16.14.0
- react-dom: 16.14.0_react@16.14.0
- regenerator-runtime: 0.13.10
- synchronous-promise: 2.0.16
- ts-dedent: 2.2.0
- unfetch: 4.2.0
- util-deprecate: 1.0.2
- dev: true
-
- /@storybook/router/6.5.13:
- resolution: {integrity: sha512-sf5aogfirH5ucD0d0hc2mKf2iyWsZsvXhr5kjxUQmgkcoflkGUWhc34sbSQVRQ1i8K5lkLIDH/q2s1Zr2SbzhQ==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- dependencies:
- '@storybook/client-logger': 6.5.13
- core-js: 3.26.0
- memoizerific: 1.11.3
- qs: 6.11.0
- regenerator-runtime: 0.13.10
- dev: true
-
- /@storybook/router/6.5.13_@preact+compat@17.1.2:
- resolution: {integrity: sha512-sf5aogfirH5ucD0d0hc2mKf2iyWsZsvXhr5kjxUQmgkcoflkGUWhc34sbSQVRQ1i8K5lkLIDH/q2s1Zr2SbzhQ==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- dependencies:
- '@storybook/client-logger': 6.5.13
- core-js: 3.26.0
- memoizerific: 1.11.3
- qs: 6.11.0
- react: /@preact/compat/17.1.2_preact@10.6.5
- regenerator-runtime: 0.13.10
- dev: true
-
- /@storybook/router/6.5.13_wcqkhtmu7mswc6yz4uyexck3ty:
- resolution: {integrity: sha512-sf5aogfirH5ucD0d0hc2mKf2iyWsZsvXhr5kjxUQmgkcoflkGUWhc34sbSQVRQ1i8K5lkLIDH/q2s1Zr2SbzhQ==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- dependencies:
- '@storybook/client-logger': 6.5.13
- core-js: 3.26.0
- memoizerific: 1.11.3
- qs: 6.11.0
- react: 16.14.0
- react-dom: 16.14.0_react@16.14.0
- regenerator-runtime: 0.13.10
- dev: true
-
- /@storybook/semver/7.3.2:
- resolution: {integrity: sha512-SWeszlsiPsMI0Ps0jVNtH64cI5c0UF3f7KgjVKJoNP30crQ6wUSddY2hsdeczZXEKVJGEn50Q60flcGsQGIcrg==}
- engines: {node: '>=10'}
- hasBin: true
- dependencies:
- core-js: 3.26.0
- find-up: 4.1.0
- dev: true
-
- /@storybook/source-loader/6.5.13:
- resolution: {integrity: sha512-tHuM8PfeB/0m+JigbaFp+Ld0euFH+fgOObH2W9rjEXy5vnwmaeex/JAdCprv4oL+LcDQEERqNULUUNIvbcTPAg==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- dependencies:
- '@storybook/addons': 6.5.13
- '@storybook/client-logger': 6.5.13
- '@storybook/csf': 0.0.2--canary.4566f4d.1
- core-js: 3.26.0
- estraverse: 5.3.0
- global: 4.4.0
- loader-utils: 2.0.3
- lodash: 4.17.21
- prettier: 2.3.0
- regenerator-runtime: 0.13.10
- dev: true
-
- /@storybook/source-loader/6.5.13_@preact+compat@17.1.2:
- resolution: {integrity: sha512-tHuM8PfeB/0m+JigbaFp+Ld0euFH+fgOObH2W9rjEXy5vnwmaeex/JAdCprv4oL+LcDQEERqNULUUNIvbcTPAg==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- dependencies:
- '@storybook/addons': 6.5.13_@preact+compat@17.1.2
- '@storybook/client-logger': 6.5.13
- '@storybook/csf': 0.0.2--canary.4566f4d.1
- core-js: 3.26.0
- estraverse: 5.3.0
- global: 4.4.0
- loader-utils: 2.0.3
- lodash: 4.17.21
- prettier: 2.3.0
- react: /@preact/compat/17.1.2_preact@10.6.5
- regenerator-runtime: 0.13.10
- dev: true
-
- /@storybook/store/6.5.13:
- resolution: {integrity: sha512-GG6lm+8fBX1tNUnX7x3raBOjYhhf14bPWLtYiPlxDTFEMs3sJte7zWKZq6NQ79MoBLL6jjzTeolBfDCBw6fiWQ==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- dependencies:
- '@storybook/addons': 6.5.13
- '@storybook/client-logger': 6.5.13
- '@storybook/core-events': 6.5.13
- '@storybook/csf': 0.0.2--canary.4566f4d.1
- core-js: 3.26.0
- fast-deep-equal: 3.1.3
- global: 4.4.0
- lodash: 4.17.21
- memoizerific: 1.11.3
- regenerator-runtime: 0.13.10
- slash: 3.0.0
- stable: 0.1.8
- synchronous-promise: 2.0.16
- ts-dedent: 2.2.0
- util-deprecate: 1.0.2
- dev: true
-
- /@storybook/store/6.5.13_@preact+compat@17.1.2:
- resolution: {integrity: sha512-GG6lm+8fBX1tNUnX7x3raBOjYhhf14bPWLtYiPlxDTFEMs3sJte7zWKZq6NQ79MoBLL6jjzTeolBfDCBw6fiWQ==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- dependencies:
- '@storybook/addons': 6.5.13_@preact+compat@17.1.2
- '@storybook/client-logger': 6.5.13
- '@storybook/core-events': 6.5.13
- '@storybook/csf': 0.0.2--canary.4566f4d.1
- core-js: 3.26.0
- fast-deep-equal: 3.1.3
- global: 4.4.0
- lodash: 4.17.21
- memoizerific: 1.11.3
- react: /@preact/compat/17.1.2_preact@10.6.5
- regenerator-runtime: 0.13.10
- slash: 3.0.0
- stable: 0.1.8
- synchronous-promise: 2.0.16
- ts-dedent: 2.2.0
- util-deprecate: 1.0.2
- dev: true
-
- /@storybook/store/6.5.13_wcqkhtmu7mswc6yz4uyexck3ty:
- resolution: {integrity: sha512-GG6lm+8fBX1tNUnX7x3raBOjYhhf14bPWLtYiPlxDTFEMs3sJte7zWKZq6NQ79MoBLL6jjzTeolBfDCBw6fiWQ==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- dependencies:
- '@storybook/addons': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@storybook/client-logger': 6.5.13
- '@storybook/core-events': 6.5.13
- '@storybook/csf': 0.0.2--canary.4566f4d.1
- core-js: 3.26.0
- fast-deep-equal: 3.1.3
- global: 4.4.0
- lodash: 4.17.21
- memoizerific: 1.11.3
- react: 16.14.0
- react-dom: 16.14.0_react@16.14.0
- regenerator-runtime: 0.13.10
- slash: 3.0.0
- stable: 0.1.8
- synchronous-promise: 2.0.16
- ts-dedent: 2.2.0
- util-deprecate: 1.0.2
- dev: true
-
- /@storybook/telemetry/6.5.13_oj3zznhuofsh7mvlmigrvm3x7y:
- resolution: {integrity: sha512-PFJEfGbunmfFWabD3rdCF8EHH+45578OHOkMPpXJjqXl94vPQxUH2XTVKQgEQJbYrgX0Vx9Z4tSkdMHuzYDbWQ==}
- dependencies:
- '@storybook/client-logger': 6.5.13
- '@storybook/core-common': 6.5.13_oj3zznhuofsh7mvlmigrvm3x7y
- chalk: 4.1.2
- core-js: 3.26.0
- detect-package-manager: 2.0.1
- fetch-retry: 5.0.3
- fs-extra: 9.1.0
- global: 4.4.0
- isomorphic-unfetch: 3.1.0
- nanoid: 3.3.4
- read-pkg-up: 7.0.1
- regenerator-runtime: 0.13.10
- transitivePeerDependencies:
- - encoding
- - eslint
- - react
- - react-dom
- - supports-color
- - typescript
- - vue-template-compiler
- - webpack-cli
- - webpack-command
- dev: true
-
- /@storybook/telemetry/6.5.13_u5cwnb36e3nipolzgtjnnpepdu:
- resolution: {integrity: sha512-PFJEfGbunmfFWabD3rdCF8EHH+45578OHOkMPpXJjqXl94vPQxUH2XTVKQgEQJbYrgX0Vx9Z4tSkdMHuzYDbWQ==}
- dependencies:
- '@storybook/client-logger': 6.5.13
- '@storybook/core-common': 6.5.13_u5cwnb36e3nipolzgtjnnpepdu
- chalk: 4.1.2
- core-js: 3.26.0
- detect-package-manager: 2.0.1
- fetch-retry: 5.0.3
- fs-extra: 9.1.0
- global: 4.4.0
- isomorphic-unfetch: 3.1.0
- nanoid: 3.3.4
- read-pkg-up: 7.0.1
- regenerator-runtime: 0.13.10
- transitivePeerDependencies:
- - encoding
- - eslint
- - react
- - react-dom
- - supports-color
- - typescript
- - vue-template-compiler
- - webpack-cli
- - webpack-command
- dev: true
-
- /@storybook/theming/6.5.13:
- resolution: {integrity: sha512-oif5NGFAUQhizo50r+ctw2hZNLWV4dPHai+L/gFvbaSeRBeHSNkIcMoZ2FlrO566HdGZTDutYXcR+xus8rI28g==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- dependencies:
- '@storybook/client-logger': 6.5.13
- core-js: 3.26.0
- memoizerific: 1.11.3
- regenerator-runtime: 0.13.10
- dev: true
-
- /@storybook/theming/6.5.13_@preact+compat@17.1.2:
- resolution: {integrity: sha512-oif5NGFAUQhizo50r+ctw2hZNLWV4dPHai+L/gFvbaSeRBeHSNkIcMoZ2FlrO566HdGZTDutYXcR+xus8rI28g==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- dependencies:
- '@storybook/client-logger': 6.5.13
- core-js: 3.26.0
- memoizerific: 1.11.3
- react: /@preact/compat/17.1.2_preact@10.6.5
- regenerator-runtime: 0.13.10
- dev: true
-
- /@storybook/theming/6.5.13_wcqkhtmu7mswc6yz4uyexck3ty:
- resolution: {integrity: sha512-oif5NGFAUQhizo50r+ctw2hZNLWV4dPHai+L/gFvbaSeRBeHSNkIcMoZ2FlrO566HdGZTDutYXcR+xus8rI28g==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- dependencies:
- '@storybook/client-logger': 6.5.13
- core-js: 3.26.0
- memoizerific: 1.11.3
- react: 16.14.0
- react-dom: 16.14.0_react@16.14.0
- regenerator-runtime: 0.13.10
- dev: true
-
- /@storybook/ui/6.5.13_wcqkhtmu7mswc6yz4uyexck3ty:
- resolution: {integrity: sha512-MklJuSg4Bc+MWjwhZVmZhJaucaeEBUMMa2V9oRWbIgZOdRHqdW72S2vCbaarDAYfBQdnfaoq1GkSQiw+EnWOzA==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- dependencies:
- '@storybook/addons': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@storybook/api': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@storybook/channels': 6.5.13
- '@storybook/client-logger': 6.5.13
- '@storybook/components': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@storybook/core-events': 6.5.13
- '@storybook/router': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- '@storybook/semver': 7.3.2
- '@storybook/theming': 6.5.13_wcqkhtmu7mswc6yz4uyexck3ty
- core-js: 3.26.0
- memoizerific: 1.11.3
- qs: 6.11.0
- react: 16.14.0
- react-dom: 16.14.0_react@16.14.0
- regenerator-runtime: 0.13.10
- resolve-from: 5.0.0
- dev: true
-
- /@surma/rollup-plugin-off-main-thread/2.2.3:
+ /@surma/rollup-plugin-off-main-thread@2.2.3:
resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==}
dependencies:
ejs: 3.1.8
- json5: 2.2.1
+ json5: 2.2.3
magic-string: 0.25.9
- string.prototype.matchall: 4.0.7
+ string.prototype.matchall: 4.0.10
dev: true
- /@szmarczak/http-timer/1.1.2:
+ /@szmarczak/http-timer@1.1.2:
resolution: {integrity: sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==}
engines: {node: '>=6'}
dependencies:
defer-to-connect: 1.1.3
dev: true
- /@testing-library/dom/7.31.2:
- resolution: {integrity: sha512-3UqjCpey6HiTZT92vODYLPxTBWlM8ZOOjr3LX5F37/VRipW2M1kX6I/Cm4VXzteZqfGfagg8yXywpcOgQBlNsQ==}
- engines: {node: '>=10'}
+ /@szmarczak/http-timer@5.0.1:
+ resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==}
+ engines: {node: '>=14.16'}
dependencies:
- '@babel/code-frame': 7.18.6
- '@babel/runtime': 7.19.4
- '@types/aria-query': 4.2.2
- aria-query: 4.2.2
- chalk: 4.1.2
- dom-accessibility-api: 0.5.14
- lz-string: 1.4.4
- pretty-format: 26.6.2
+ defer-to-connect: 2.0.1
dev: true
- /@testing-library/preact-hooks/1.1.0_aub6lnx45vk623d66chdvib7ry:
- resolution: {integrity: sha512-+JIor+NsOHkK3oIrwMDGKGHXTN0JJi462dBJlj4FNbGaDPTlctE6eu2ranWQirh7/FJMkWfzQCP+tk7jmY8ZrQ==}
+ /@tailwindcss/forms@0.5.3(tailwindcss@3.3.2):
+ resolution: {integrity: sha512-y5mb86JUoiUgBjY/o6FJSFZSEttfb3Q5gllE4xoKjAAD+vBrnIhE4dViwUuow3va8mpH4s9jyUbUbrRGoRdc2Q==}
peerDependencies:
- '@testing-library/preact': ^2.0.0
- preact: ^10.4.8
+ tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1'
dependencies:
- '@testing-library/preact': 2.0.1_preact@10.11.2
- preact: 10.11.2
- dev: true
-
- /@testing-library/preact-hooks/1.1.0_vfcmu6iy7nffpurikpgxo6gwxi:
- resolution: {integrity: sha512-+JIor+NsOHkK3oIrwMDGKGHXTN0JJi462dBJlj4FNbGaDPTlctE6eu2ranWQirh7/FJMkWfzQCP+tk7jmY8ZrQ==}
- peerDependencies:
- '@testing-library/preact': ^2.0.0
- preact: ^10.4.8
- dependencies:
- '@testing-library/preact': 2.0.1_preact@10.6.5
- preact: 10.6.5
+ mini-svg-data-uri: 1.4.4
+ tailwindcss: 3.3.2
dev: true
- /@testing-library/preact/2.0.1_preact@10.11.2:
- resolution: {integrity: sha512-79kwVOY+3caoLgaPbiPzikjgY0Aya7Fc7TvGtR1upCnz2wrtmPDnN2t9vO7I7vDP2zoA+feSwOH5Q0BFErhaaQ==}
- engines: {node: '>= 10'}
+ /@tailwindcss/typography@0.5.9(tailwindcss@3.3.2):
+ resolution: {integrity: sha512-t8Sg3DyynFysV9f4JDOVISGsjazNb48AeIYQwcL+Bsq5uf4RYL75C1giZ43KISjeDGBaTN3Kxh7Xj/vRSMJUUg==}
peerDependencies:
- preact: '>=10 || ^10.0.0-alpha.0 || ^10.0.0-beta.0'
+ tailwindcss: '>=3.0.0 || insiders'
dependencies:
- '@testing-library/dom': 7.31.2
- preact: 10.11.2
- dev: true
-
- /@testing-library/preact/2.0.1_preact@10.6.5:
- resolution: {integrity: sha512-79kwVOY+3caoLgaPbiPzikjgY0Aya7Fc7TvGtR1upCnz2wrtmPDnN2t9vO7I7vDP2zoA+feSwOH5Q0BFErhaaQ==}
- engines: {node: '>= 10'}
- peerDependencies:
- preact: '>=10 || ^10.0.0-alpha.0 || ^10.0.0-beta.0'
- dependencies:
- '@testing-library/dom': 7.31.2
- preact: 10.6.5
- dev: true
-
- /@tootallnate/once/1.1.2:
- resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==}
- engines: {node: '>= 6'}
+ lodash.castarray: 4.4.0
+ lodash.isplainobject: 4.0.6
+ lodash.merge: 4.6.2
+ postcss-selector-parser: 6.0.10
+ tailwindcss: 3.3.2
dev: true
- /@trysound/sax/0.2.0:
+ /@trysound/sax@0.2.0:
resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
engines: {node: '>=10.13.0'}
dev: true
- /@types/aria-query/4.2.2:
- resolution: {integrity: sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==}
+ /@tsconfig/node10@1.0.9:
+ resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==}
dev: true
- /@types/babel__core/7.1.19:
- resolution: {integrity: sha512-WEOTgRsbYkvA/KCsDwVEGkd7WAr1e3g31VHQ8zy5gul/V1qKullU/BU5I68X5v7V3GnB9eotmom4v5a5gjxorw==}
- dependencies:
- '@babel/parser': 7.19.6
- '@babel/types': 7.19.4
- '@types/babel__generator': 7.6.4
- '@types/babel__template': 7.4.1
- '@types/babel__traverse': 7.18.2
+ /@tsconfig/node12@1.0.11:
+ resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==}
dev: true
- /@types/babel__generator/7.6.4:
- resolution: {integrity: sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==}
- dependencies:
- '@babel/types': 7.19.4
+ /@tsconfig/node14@1.0.3:
+ resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==}
dev: true
- /@types/babel__template/7.4.1:
- resolution: {integrity: sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==}
- dependencies:
- '@babel/parser': 7.19.6
- '@babel/types': 7.19.4
+ /@tsconfig/node16@1.0.3:
+ resolution: {integrity: sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==}
dev: true
- /@types/babel__traverse/7.18.2:
- resolution: {integrity: sha512-FcFaxOr2V5KZCviw1TnutEMVUVsGt4D2hP1TAfXZAMKuHYW3xQhe3jTxNPWutgCJ3/X1c5yX8ZoGVEItxKbwBg==}
+ /@types/better-sqlite3@7.6.8:
+ resolution: {integrity: sha512-ASndM4rdGrzk7iXXqyNC4fbwt4UEjpK0i3j4q4FyeQrLAthfB6s7EF135ZJE0qQxtKIMFwmyT6x0switET7uIw==}
dependencies:
- '@babel/types': 7.19.4
+ '@types/node': 20.11.13
dev: true
- /@types/body-parser/1.19.2:
+ /@types/body-parser@1.19.2:
resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==}
dependencies:
'@types/connect': 3.4.35
- '@types/node': 18.11.9
+ '@types/node': 20.11.13
dev: true
- /@types/bonjour/3.5.10:
+ /@types/bonjour@3.5.10:
resolution: {integrity: sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw==}
dependencies:
- '@types/node': 18.11.9
+ '@types/node': 20.11.13
dev: true
- /@types/chai/4.3.3:
+ /@types/chai@4.3.3:
resolution: {integrity: sha512-hC7OMnszpxhZPduX+m+nrx+uFoLkWOMiR4oa/AZF3MuSETYTZmFfJAHqZEM8MVlvfG7BEUcgvtwoCTxBp6hm3g==}
-
- /@types/cheerio/0.22.31:
- resolution: {integrity: sha512-Kt7Cdjjdi2XWSfrZ53v4Of0wG3ZcmaegFXjMmz9tfNrZSkzzo36G0AL1YqSdcIA78Etjt6E609pt5h1xnQkPUw==}
- dependencies:
- '@types/node': 18.11.9
dev: true
- /@types/chrome/0.0.197:
+ /@types/chrome@0.0.197:
resolution: {integrity: sha512-m1NfS5bOjaypyqQfaX6CxmJodZVcvj5+Mt/K94EBHkflYjPNmXHAzbxfifdLMa0YM3PDyOxohoTS5ug/e6p5jA==}
dependencies:
'@types/filesystem': 0.0.32
'@types/har-format': 1.2.9
- dev: true
- /@types/connect-history-api-fallback/1.3.5:
+ /@types/connect-history-api-fallback@1.3.5:
resolution: {integrity: sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw==}
dependencies:
'@types/express-serve-static-core': 4.17.31
- '@types/node': 18.11.9
+ '@types/node': 20.11.13
dev: true
- /@types/connect/3.4.35:
+ /@types/connect@3.4.35:
resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==}
dependencies:
- '@types/node': 18.11.9
- dev: true
-
- /@types/enzyme/3.10.12:
- resolution: {integrity: sha512-xryQlOEIe1TduDWAOphR0ihfebKFSWOXpIsk+70JskCfRfW+xALdnJ0r1ZOTo85F9Qsjk6vtlU7edTYHbls9tA==}
- dependencies:
- '@types/cheerio': 0.22.31
- '@types/react': 18.0.23
+ '@types/node': 20.11.13
dev: true
- /@types/eslint-scope/3.7.4:
- resolution: {integrity: sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==}
- dependencies:
- '@types/eslint': 8.4.8
- '@types/estree': 0.0.51
- dev: true
-
- /@types/eslint/8.4.8:
- resolution: {integrity: sha512-zUCKQI1bUCTi+0kQs5ZQzQ/XILWRLIlh15FXWNykJ+NG3TMKMVvwwC6GP3DR1Ylga15fB7iAExSzc4PNlR5i3w==}
- dependencies:
- '@types/estree': 0.0.51
- '@types/json-schema': 7.0.11
- dev: true
-
- /@types/estree/0.0.39:
+ /@types/estree@0.0.39:
resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==}
dev: true
- /@types/estree/0.0.51:
- resolution: {integrity: sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==}
- dev: true
-
- /@types/estree/1.0.0:
- resolution: {integrity: sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==}
- dev: true
-
- /@types/express-serve-static-core/4.17.31:
+ /@types/express-serve-static-core@4.17.31:
resolution: {integrity: sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q==}
dependencies:
- '@types/node': 18.11.9
+ '@types/node': 20.11.13
'@types/qs': 6.9.7
'@types/range-parser': 1.2.4
dev: true
- /@types/express/4.17.14:
+ /@types/express@4.17.14:
resolution: {integrity: sha512-TEbt+vaPFQ+xpxFLFssxUDXj5cWCxZJjIcB7Yg0k0GMHGtgtQgpvx/MUQUeAkNbA9AAGrwkAsoeItdTgS7FMyg==}
dependencies:
'@types/body-parser': 1.19.2
@@ -7079,289 +6109,201 @@ packages:
'@types/serve-static': 1.15.0
dev: true
- /@types/filesystem/0.0.32:
+ /@types/filesystem@0.0.32:
resolution: {integrity: sha512-Yuf4jR5YYMR2DVgwuCiP11s0xuVRyPKmz8vo6HBY3CGdeMj8af93CFZX+T82+VD1+UqHOxTq31lO7MI7lepBtQ==}
dependencies:
'@types/filewriter': 0.0.29
- dev: true
- /@types/filewriter/0.0.29:
+ /@types/filewriter@0.0.29:
resolution: {integrity: sha512-BsPXH/irW0ht0Ji6iw/jJaK8Lj3FJemon2gvEqHKpCdDCeemHa+rI3WBGq5z7cDMZgoLjY40oninGxqk+8NzNQ==}
- dev: true
-
- /@types/glob/7.2.0:
- resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==}
- dependencies:
- '@types/minimatch': 5.1.2
- '@types/node': 18.11.9
- dev: true
- /@types/glob/8.0.0:
- resolution: {integrity: sha512-l6NQsDDyQUVeoTynNpC9uRvCUint/gSUXQA2euwmTuWGvPY5LSDUu6tkCtJB2SvGQlJQzLaKqcGZP4//7EDveA==}
+ /@types/follow-redirects@1.14.4:
+ resolution: {integrity: sha512-GWXfsD0Jc1RWiFmMuMFCpXMzi9L7oPDVwxUnZdg89kDNnqsRfUKXEtUYtA98A6lig1WXH/CYY/fvPW9HuN5fTA==}
dependencies:
- '@types/minimatch': 5.1.2
- '@types/node': 18.11.9
+ '@types/node': 20.11.13
dev: true
- /@types/graceful-fs/4.1.5:
- resolution: {integrity: sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==}
- dependencies:
- '@types/node': 18.11.9
- dev: true
-
- /@types/har-format/1.2.9:
+ /@types/har-format@1.2.9:
resolution: {integrity: sha512-rffW6MhQ9yoa75bdNi+rjZBAvu2HhehWJXlhuWXnWdENeuKe82wUgAwxYOb7KRKKmxYN+D/iRKd2NDQMLqlUmg==}
- dev: true
-
- /@types/hast/2.3.4:
- resolution: {integrity: sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g==}
- dependencies:
- '@types/unist': 2.0.6
- dev: true
- /@types/history/4.7.11:
+ /@types/history@4.7.11:
resolution: {integrity: sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==}
dev: true
- /@types/html-minifier-terser/5.1.2:
- resolution: {integrity: sha512-h4lTMgMJctJybDp8CQrxTUiiYmedihHWkjnF/8Pxseu2S6Nlfcy8kwboQ8yejh456rP2yWoEVm1sS/FVsfM48w==}
+ /@types/html-minifier-terser@6.1.0:
+ resolution: {integrity: sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==}
dev: true
- /@types/http-proxy/1.17.9:
- resolution: {integrity: sha512-QsbSjA/fSk7xB+UXlCT3wHBy5ai9wOcNDWwZAtud+jXhwOM3l+EYZh8Lng4+/6n8uar0J7xILzqftJdJ/Wdfkw==}
- dependencies:
- '@types/node': 18.11.9
+ /@types/http-cache-semantics@4.0.4:
+ resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==}
dev: true
- /@types/is-function/1.0.1:
- resolution: {integrity: sha512-A79HEEiwXTFtfY+Bcbo58M2GRYzCr9itHWzbzHVFNEYCcoU/MMGwYYf721gBrnhpj1s6RGVVha/IgNFnR0Iw/Q==}
- dev: true
-
- /@types/istanbul-lib-coverage/2.0.4:
- resolution: {integrity: sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==}
- dev: true
-
- /@types/istanbul-lib-report/3.0.0:
- resolution: {integrity: sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==}
+ /@types/http-proxy@1.17.9:
+ resolution: {integrity: sha512-QsbSjA/fSk7xB+UXlCT3wHBy5ai9wOcNDWwZAtud+jXhwOM3l+EYZh8Lng4+/6n8uar0J7xILzqftJdJ/Wdfkw==}
dependencies:
- '@types/istanbul-lib-coverage': 2.0.4
+ '@types/node': 20.11.13
dev: true
- /@types/istanbul-reports/3.0.1:
- resolution: {integrity: sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==}
- dependencies:
- '@types/istanbul-lib-report': 3.0.0
+ /@types/istanbul-lib-coverage@2.0.6:
+ resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==}
dev: true
- /@types/jest/26.0.24:
- resolution: {integrity: sha512-E/X5Vib8BWqZNRlDxj9vYXhsDwPYbPINqKF9BsnSoon4RQ0D9moEuLD8txgyypFLH7J4+Lho9Nr/c8H0Fi+17w==}
- dependencies:
- jest-diff: 26.6.2
- pretty-format: 26.6.2
+ /@types/json-schema@7.0.11:
+ resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==}
dev: true
- /@types/json-schema/7.0.11:
- resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==}
+ /@types/json-schema@7.0.15:
+ resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
dev: true
- /@types/json5/0.0.29:
+ /@types/json5@0.0.29:
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
dev: true
- /@types/keyv/3.1.4:
+ /@types/keyv@3.1.4:
resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==}
dependencies:
- '@types/node': 18.11.9
+ '@types/node': 20.11.13
dev: true
- /@types/lodash/4.14.186:
+ /@types/lodash@4.14.186:
resolution: {integrity: sha512-eHcVlLXP0c2FlMPm56ITode2AgLMSa6aJ05JTTbYbI+7EMkCEE5qk2E41d5g2lCVTqRe0GnnRFurmlCsDODrPw==}
+ dev: false
- /@types/mdast/3.0.10:
- resolution: {integrity: sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA==}
- dependencies:
- '@types/unist': 2.0.6
+ /@types/mime@3.0.1:
+ resolution: {integrity: sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==}
dev: true
- /@types/mime/3.0.1:
- resolution: {integrity: sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==}
+ /@types/minimatch@3.0.5:
+ resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==}
dev: true
- /@types/minimatch/5.1.2:
- resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==}
+ /@types/mocha@10.0.1:
+ resolution: {integrity: sha512-/fvYntiO1GeICvqbQ3doGDIP97vWmvFt83GKguJ6prmQM2iXZfFcq6YE8KteFyRtX2/h5Hf91BYvPodJKFYv5Q==}
dev: true
- /@types/mocha/8.2.3:
+ /@types/mocha@8.2.3:
resolution: {integrity: sha512-ekGvFhFgrc2zYQoX4JeZPmVzZxw6Dtllga7iGHzfbYIYkAMUx/sAFP2GdFpLff+vdHXu5fl7WX9AT+TtqYcsyw==}
dev: true
- /@types/mocha/9.1.1:
+ /@types/mocha@9.1.1:
resolution: {integrity: sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw==}
dev: true
- /@types/mustache/4.2.1:
+ /@types/mustache@4.2.1:
resolution: {integrity: sha512-gFAlWL9Ik21nJioqjlGCnNYbf9zHi0sVbaZ/1hQEBcCEuxfLJDvz4bVJSV6v6CUaoLOz0XEIoP7mSrhJ6o237w==}
dev: true
- /@types/node-fetch/2.6.2:
- resolution: {integrity: sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==}
- dependencies:
- '@types/node': 18.11.9
- form-data: 3.0.1
+ /@types/node@14.18.63:
+ resolution: {integrity: sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==}
dev: true
- /@types/node/16.18.0:
- resolution: {integrity: sha512-LqYqYzYvnbCaQfLAwRt0zboqnsViwhZm+vjaMSqcfN36vulAg7Pt0T83q4WZO2YOBw3XdyHi8cQ88H22zmULOA==}
- dev: true
-
- /@types/node/18.11.5:
- resolution: {integrity: sha512-3JRwhbjI+cHLAkUorhf8RnqUbFXajvzX4q6fMn5JwkgtuwfYtRQYI3u4V92vI6NJuTsbBQWWh3RZjFsuevyMGQ==}
-
- /@types/node/18.11.9:
- resolution: {integrity: sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==}
- dev: true
+ /@types/node@18.11.17:
+ resolution: {integrity: sha512-HJSUJmni4BeDHhfzn6nF0sVmd1SMezP7/4F0Lq+aXzmp2xm9O7WXrUtHW/CHlYVtZUbByEvWidHqRtcJXGF2Ng==}
- /@types/normalize-package-data/2.4.1:
- resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==}
+ /@types/node@20.11.13:
+ resolution: {integrity: sha512-5G4zQwdiQBSWYTDAH1ctw2eidqdhMJaNsiIDKHFr55ihz5Trl2qqR8fdrT732yPBho5gkNxXm67OxWFBqX9aPg==}
+ dependencies:
+ undici-types: 5.26.5
dev: true
- /@types/npmlog/4.1.4:
- resolution: {integrity: sha512-WKG4gTr8przEZBiJ5r3s8ZIAoMXNbOgQ+j/d5O4X3x6kZJRLNvyUJuUK/KoG3+8BaOHPhp2m7WC6JKKeovDSzQ==}
+ /@types/node@20.4.1:
+ resolution: {integrity: sha512-JIzsAvJeA/5iY6Y/OxZbv1lUcc8dNSE77lb2gnBH+/PJ3lFR1Ccvgwl5JWnHAkNHcRsT0TbpVOsiMKZ1F/yyJg==}
dev: true
- /@types/offscreencanvas/2019.7.0:
- resolution: {integrity: sha512-PGcyveRIpL1XIqK8eBsmRBt76eFgtzuPiSTyKHZxnGemp2yzGzWpjYKAfK3wIMiU7eH+851yEpiuP8JZerTmWg==}
- dev: false
-
- /@types/parse-json/4.0.0:
+ /@types/parse-json@4.0.0:
resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==}
dev: true
- /@types/parse5/5.0.3:
- resolution: {integrity: sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw==}
- dev: true
-
- /@types/prettier/2.7.1:
- resolution: {integrity: sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow==}
- dev: true
-
- /@types/pretty-hrtime/1.0.1:
- resolution: {integrity: sha512-VjID5MJb1eGKthz2qUerWT8+R4b9N+CHvGCzg9fn4kWZgaF9AhdYikQio3R7wV8YY1NsQKPaCwKz1Yff+aHNUQ==}
- dev: true
-
- /@types/prop-types/15.7.5:
- resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==}
- dev: true
-
- /@types/q/1.5.5:
+ /@types/q@1.5.5:
resolution: {integrity: sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ==}
dev: true
- /@types/qs/6.9.7:
+ /@types/qs@6.9.7:
resolution: {integrity: sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==}
dev: true
- /@types/range-parser/1.2.4:
+ /@types/range-parser@1.2.4:
resolution: {integrity: sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==}
dev: true
- /@types/react/18.0.23:
- resolution: {integrity: sha512-R1wTULtCiJkudAN2DJGoYYySbGtOdzZyUWAACYinKdiQC8auxso4kLDUhQ7AJ2kh3F6A6z4v69U6tNY39hihVQ==}
- dependencies:
- '@types/prop-types': 15.7.5
- '@types/scheduler': 0.16.2
- csstype: 3.1.1
- dev: true
-
- /@types/resolve/1.17.1:
+ /@types/resolve@1.17.1:
resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==}
dependencies:
- '@types/node': 18.11.9
+ '@types/node': 20.11.13
dev: true
- /@types/responselike/1.0.0:
+ /@types/responselike@1.0.0:
resolution: {integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==}
dependencies:
- '@types/node': 18.11.9
+ '@types/node': 20.11.13
dev: true
- /@types/retry/0.12.0:
+ /@types/retry@0.12.0:
resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==}
dev: true
- /@types/scheduler/0.16.2:
- resolution: {integrity: sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==}
+ /@types/semver@7.3.12:
+ resolution: {integrity: sha512-WwA1MW0++RfXmCr12xeYOOC5baSC9mSb0ZqCquFzKhcoF4TvHu5MKOuXsncgZcpVFhB1pXd5hZmM0ryAoCp12A==}
dev: true
- /@types/semver/7.3.12:
- resolution: {integrity: sha512-WwA1MW0++RfXmCr12xeYOOC5baSC9mSb0ZqCquFzKhcoF4TvHu5MKOuXsncgZcpVFhB1pXd5hZmM0ryAoCp12A==}
+ /@types/semver@7.5.6:
+ resolution: {integrity: sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==}
dev: true
- /@types/serve-index/1.9.1:
+ /@types/serve-index@1.9.1:
resolution: {integrity: sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg==}
dependencies:
'@types/express': 4.17.14
dev: true
- /@types/serve-static/1.15.0:
+ /@types/serve-static@1.15.0:
resolution: {integrity: sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==}
dependencies:
'@types/mime': 3.0.1
- '@types/node': 18.11.9
+ '@types/node': 20.11.13
dev: true
- /@types/sockjs/0.3.33:
+ /@types/sockjs@0.3.33:
resolution: {integrity: sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw==}
dependencies:
- '@types/node': 18.11.9
+ '@types/node': 20.11.13
dev: true
- /@types/source-list-map/0.1.2:
+ /@types/source-list-map@0.1.2:
resolution: {integrity: sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==}
dev: true
- /@types/stack-utils/2.0.1:
- resolution: {integrity: sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==}
- dev: true
-
- /@types/tapable/1.0.8:
+ /@types/tapable@1.0.8:
resolution: {integrity: sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ==}
dev: true
- /@types/trusted-types/2.0.2:
+ /@types/trusted-types@2.0.2:
resolution: {integrity: sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==}
dev: true
- /@types/uglify-js/3.17.1:
+ /@types/uglify-js@3.17.1:
resolution: {integrity: sha512-GkewRA4i5oXacU/n4MA9+bLgt5/L3F1mKrYvFGm7r2ouLXhRKjuWwo9XHNnbx6WF3vlGW21S3fCvgqxvxXXc5g==}
dependencies:
source-map: 0.6.1
dev: true
- /@types/unist/2.0.6:
- resolution: {integrity: sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==}
- dev: true
-
- /@types/web/0.0.82:
+ /@types/web@0.0.82:
resolution: {integrity: sha512-mktv7gA7V9mGKhAR9MXikOeNjsf3UptliH2yBFbNOqqbmse8II8irigyVJrW072ReHzPYSkJms9A5wZq3We5rw==}
dev: true
- /@types/webpack-env/1.18.0:
- resolution: {integrity: sha512-56/MAlX5WMsPVbOg7tAxnYvNYMMWr/QJiIp6BxVSW3JJXUVzzOn64qW8TzQyMSqSUFM2+PVI4aUHcHOzIz/1tg==}
- dev: true
-
- /@types/webpack-sources/3.2.0:
+ /@types/webpack-sources@3.2.0:
resolution: {integrity: sha512-Ft7YH3lEVRQ6ls8k4Ff1oB4jN6oy/XmU6tQISKdhfh+1mR+viZFphS6WL0IrtDOzvefmJg5a0s7ZQoRXwqTEFg==}
dependencies:
- '@types/node': 18.11.9
+ '@types/node': 20.11.13
'@types/source-list-map': 0.1.2
source-map: 0.7.4
dev: true
- /@types/webpack/4.41.33:
+ /@types/webpack@4.41.33:
resolution: {integrity: sha512-PPajH64Ft2vWevkerISMtnZ8rTs4YmRbs+23c402J0INmxDKCrhZNvwZYtzx96gY2wAtXdrK1BS2fiC8MlLr3g==}
dependencies:
- '@types/node': 18.11.9
+ '@types/node': 20.11.13
'@types/tapable': 1.0.8
'@types/uglify-js': 3.17.1
'@types/webpack-sources': 3.2.0
@@ -7369,29 +6311,19 @@ packages:
source-map: 0.6.1
dev: true
- /@types/ws/8.5.3:
+ /@types/ws@8.5.3:
resolution: {integrity: sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==}
dependencies:
- '@types/node': 18.11.9
+ '@types/node': 20.11.13
dev: true
- /@types/yargs-parser/21.0.0:
- resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==}
- dev: true
-
- /@types/yargs/15.0.14:
- resolution: {integrity: sha512-yEJzHoxf6SyQGhBhIYGXQDSCkJjB6HohDShto7m8vaKg9Yp0Yn8+71J9eakh2bnPg6BfsH9PRMhiRTZnd4eXGQ==}
+ /@types/yauzl@2.10.3:
+ resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==}
dependencies:
- '@types/yargs-parser': 21.0.0
+ '@types/node': 20.11.13
dev: true
- /@types/yargs/16.0.4:
- resolution: {integrity: sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==}
- dependencies:
- '@types/yargs-parser': 21.0.0
- dev: true
-
- /@typescript-eslint/eslint-plugin/4.33.0_k4l66av2tbo6kxzw52jzgbfzii:
+ /@typescript-eslint/eslint-plugin@4.33.0(@typescript-eslint/parser@4.33.0)(eslint@7.32.0)(typescript@5.3.3):
resolution: {integrity: sha512-aINiAxGVdOl1eJyVjaWn/YcVAq4Gi/Yo35qHGCnqbWVz61g39D0h23veY/MA0rFFGfxK7TySg2uwDeNv+JgVpg==}
engines: {node: ^10.12.0 || >=12.0.0}
peerDependencies:
@@ -7402,8 +6334,8 @@ packages:
typescript:
optional: true
dependencies:
- '@typescript-eslint/experimental-utils': 4.33.0_3rubbgt5ekhqrcgx4uwls3neim
- '@typescript-eslint/parser': 4.33.0_3rubbgt5ekhqrcgx4uwls3neim
+ '@typescript-eslint/experimental-utils': 4.33.0(eslint@7.32.0)(typescript@5.3.3)
+ '@typescript-eslint/parser': 4.33.0(eslint@7.32.0)(typescript@5.3.3)
'@typescript-eslint/scope-manager': 4.33.0
debug: 4.3.4
eslint: 7.32.0
@@ -7411,39 +6343,13 @@ packages:
ignore: 5.2.0
regexpp: 3.2.0
semver: 7.3.8
- tsutils: 3.21.0_typescript@4.8.4
- typescript: 4.8.4
+ tsutils: 3.21.0(typescript@5.3.3)
+ typescript: 5.3.3
transitivePeerDependencies:
- supports-color
dev: true
- /@typescript-eslint/eslint-plugin/4.33.0_zrqxgwgitu7trrjeml3nqco3jq:
- resolution: {integrity: sha512-aINiAxGVdOl1eJyVjaWn/YcVAq4Gi/Yo35qHGCnqbWVz61g39D0h23veY/MA0rFFGfxK7TySg2uwDeNv+JgVpg==}
- engines: {node: ^10.12.0 || >=12.0.0}
- peerDependencies:
- '@typescript-eslint/parser': ^4.0.0
- eslint: ^5.0.0 || ^6.0.0 || ^7.0.0
- typescript: '*'
- peerDependenciesMeta:
- typescript:
- optional: true
- dependencies:
- '@typescript-eslint/experimental-utils': 4.33.0_wnilx7boviscikmvsfkd6ljepe
- '@typescript-eslint/parser': 4.33.0_wnilx7boviscikmvsfkd6ljepe
- '@typescript-eslint/scope-manager': 4.33.0
- debug: 4.3.4
- eslint: 7.32.0
- functional-red-black-tree: 1.0.1
- ignore: 5.2.0
- regexpp: 3.2.0
- semver: 7.3.8
- tsutils: 3.21.0_typescript@4.4.4
- typescript: 4.4.4
- transitivePeerDependencies:
- - supports-color
- dev: true
-
- /@typescript-eslint/eslint-plugin/5.41.0_huremdigmcnkianavgfk3x6iou:
+ /@typescript-eslint/eslint-plugin@5.41.0(@typescript-eslint/parser@5.41.0)(eslint@8.26.0)(typescript@5.3.3):
resolution: {integrity: sha512-DXUS22Y57/LAFSg3x7Vi6RNAuLpTXwxB9S2nIA7msBb/Zt8p7XqMwdpdc1IU7CkOQUPgAqR5fWvxuKCbneKGmA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
@@ -7454,40 +6360,51 @@ packages:
typescript:
optional: true
dependencies:
- '@typescript-eslint/parser': 5.41.0_wyqvi574yv7oiwfeinomdzmc3m
+ '@typescript-eslint/parser': 5.41.0(eslint@8.26.0)(typescript@5.3.3)
'@typescript-eslint/scope-manager': 5.41.0
- '@typescript-eslint/type-utils': 5.41.0_wyqvi574yv7oiwfeinomdzmc3m
- '@typescript-eslint/utils': 5.41.0_wyqvi574yv7oiwfeinomdzmc3m
+ '@typescript-eslint/type-utils': 5.41.0(eslint@8.26.0)(typescript@5.3.3)
+ '@typescript-eslint/utils': 5.41.0(eslint@8.26.0)(typescript@5.3.3)
debug: 4.3.4
eslint: 8.26.0
ignore: 5.2.0
regexpp: 3.2.0
semver: 7.3.8
- tsutils: 3.21.0_typescript@4.8.4
- typescript: 4.8.4
+ tsutils: 3.21.0(typescript@5.3.3)
+ typescript: 5.3.3
transitivePeerDependencies:
- supports-color
dev: true
- /@typescript-eslint/experimental-utils/4.33.0_3rubbgt5ekhqrcgx4uwls3neim:
- resolution: {integrity: sha512-zeQjOoES5JFjTnAhI5QY7ZviczMzDptls15GFsI6jyUOq0kOf9+WonkhtlIhh0RgHRnqj5gdNxW5j1EvAyYg6Q==}
- engines: {node: ^10.12.0 || >=12.0.0}
+ /@typescript-eslint/eslint-plugin@6.19.0(@typescript-eslint/parser@6.19.0)(eslint@8.56.0)(typescript@5.3.3):
+ resolution: {integrity: sha512-DUCUkQNklCQYnrBSSikjVChdc84/vMPDQSgJTHBZ64G9bA9w0Crc0rd2diujKbTdp6w2J47qkeHQLoi0rpLCdg==}
+ engines: {node: ^16.0.0 || >=18.0.0}
peerDependencies:
- eslint: '*'
+ '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha
+ eslint: ^7.0.0 || ^8.0.0
+ typescript: '*'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
dependencies:
- '@types/json-schema': 7.0.11
- '@typescript-eslint/scope-manager': 4.33.0
- '@typescript-eslint/types': 4.33.0
- '@typescript-eslint/typescript-estree': 4.33.0_typescript@4.8.4
- eslint: 7.32.0
- eslint-scope: 5.1.1
- eslint-utils: 3.0.0_eslint@7.32.0
+ '@eslint-community/regexpp': 4.10.0
+ '@typescript-eslint/parser': 6.19.0(eslint@8.56.0)(typescript@5.3.3)
+ '@typescript-eslint/scope-manager': 6.19.0
+ '@typescript-eslint/type-utils': 6.19.0(eslint@8.56.0)(typescript@5.3.3)
+ '@typescript-eslint/utils': 6.19.0(eslint@8.56.0)(typescript@5.3.3)
+ '@typescript-eslint/visitor-keys': 6.19.0
+ debug: 4.3.4
+ eslint: 8.56.0
+ graphemer: 1.4.0
+ ignore: 5.3.0
+ natural-compare: 1.4.0
+ semver: 7.5.4
+ ts-api-utils: 1.0.3(typescript@5.3.3)
+ typescript: 5.3.3
transitivePeerDependencies:
- supports-color
- - typescript
dev: true
- /@typescript-eslint/experimental-utils/4.33.0_wnilx7boviscikmvsfkd6ljepe:
+ /@typescript-eslint/experimental-utils@4.33.0(eslint@7.32.0)(typescript@5.3.3):
resolution: {integrity: sha512-zeQjOoES5JFjTnAhI5QY7ZviczMzDptls15GFsI6jyUOq0kOf9+WonkhtlIhh0RgHRnqj5gdNxW5j1EvAyYg6Q==}
engines: {node: ^10.12.0 || >=12.0.0}
peerDependencies:
@@ -7496,55 +6413,29 @@ packages:
'@types/json-schema': 7.0.11
'@typescript-eslint/scope-manager': 4.33.0
'@typescript-eslint/types': 4.33.0
- '@typescript-eslint/typescript-estree': 4.33.0_typescript@4.4.4
+ '@typescript-eslint/typescript-estree': 4.33.0(typescript@5.3.3)
eslint: 7.32.0
eslint-scope: 5.1.1
- eslint-utils: 3.0.0_eslint@7.32.0
+ eslint-utils: 3.0.0(eslint@7.32.0)
transitivePeerDependencies:
- supports-color
- typescript
dev: true
- /@typescript-eslint/experimental-utils/5.41.0_3rubbgt5ekhqrcgx4uwls3neim:
+ /@typescript-eslint/experimental-utils@5.41.0(eslint@7.32.0)(typescript@5.3.3):
resolution: {integrity: sha512-/qxT2Kd2q/A22JVIllvws4rvc00/3AT4rAo/0YgEN28y+HPhbJbk6X4+MAHEoZzpNyAOugIT7D/OLnKBW8FfhA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
dependencies:
- '@typescript-eslint/utils': 5.41.0_3rubbgt5ekhqrcgx4uwls3neim
+ '@typescript-eslint/utils': 5.41.0(eslint@7.32.0)(typescript@5.3.3)
eslint: 7.32.0
transitivePeerDependencies:
- supports-color
- typescript
dev: true
- /@typescript-eslint/experimental-utils/5.41.0_wnilx7boviscikmvsfkd6ljepe:
- resolution: {integrity: sha512-/qxT2Kd2q/A22JVIllvws4rvc00/3AT4rAo/0YgEN28y+HPhbJbk6X4+MAHEoZzpNyAOugIT7D/OLnKBW8FfhA==}
- engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
- peerDependencies:
- eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
- dependencies:
- '@typescript-eslint/utils': 5.41.0_wnilx7boviscikmvsfkd6ljepe
- eslint: 7.32.0
- transitivePeerDependencies:
- - supports-color
- - typescript
- dev: true
-
- /@typescript-eslint/experimental-utils/5.41.0_wyqvi574yv7oiwfeinomdzmc3m:
- resolution: {integrity: sha512-/qxT2Kd2q/A22JVIllvws4rvc00/3AT4rAo/0YgEN28y+HPhbJbk6X4+MAHEoZzpNyAOugIT7D/OLnKBW8FfhA==}
- engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
- peerDependencies:
- eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
- dependencies:
- '@typescript-eslint/utils': 5.41.0_wyqvi574yv7oiwfeinomdzmc3m
- eslint: 8.26.0
- transitivePeerDependencies:
- - supports-color
- - typescript
- dev: true
-
- /@typescript-eslint/parser/4.33.0_3rubbgt5ekhqrcgx4uwls3neim:
+ /@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@5.3.3):
resolution: {integrity: sha512-ZohdsbXadjGBSK0/r+d87X0SBmKzOq4/S5nzK6SBgJspFo9/CUDJ7hjayuze+JK7CZQLDMroqytp7pOcFKTxZA==}
engines: {node: ^10.12.0 || >=12.0.0}
peerDependencies:
@@ -7556,55 +6447,56 @@ packages:
dependencies:
'@typescript-eslint/scope-manager': 4.33.0
'@typescript-eslint/types': 4.33.0
- '@typescript-eslint/typescript-estree': 4.33.0_typescript@4.8.4
+ '@typescript-eslint/typescript-estree': 4.33.0(typescript@5.3.3)
debug: 4.3.4
eslint: 7.32.0
- typescript: 4.8.4
+ typescript: 5.3.3
transitivePeerDependencies:
- supports-color
dev: true
- /@typescript-eslint/parser/4.33.0_wnilx7boviscikmvsfkd6ljepe:
- resolution: {integrity: sha512-ZohdsbXadjGBSK0/r+d87X0SBmKzOq4/S5nzK6SBgJspFo9/CUDJ7hjayuze+JK7CZQLDMroqytp7pOcFKTxZA==}
- engines: {node: ^10.12.0 || >=12.0.0}
+ /@typescript-eslint/parser@5.41.0(eslint@8.26.0)(typescript@5.3.3):
+ resolution: {integrity: sha512-HQVfix4+RL5YRWZboMD1pUfFN8MpRH4laziWkkAzyO1fvNOY/uinZcvo3QiFJVS/siNHupV8E5+xSwQZrl6PZA==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
- eslint: ^5.0.0 || ^6.0.0 || ^7.0.0
+ eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
typescript: '*'
peerDependenciesMeta:
typescript:
optional: true
dependencies:
- '@typescript-eslint/scope-manager': 4.33.0
- '@typescript-eslint/types': 4.33.0
- '@typescript-eslint/typescript-estree': 4.33.0_typescript@4.4.4
+ '@typescript-eslint/scope-manager': 5.41.0
+ '@typescript-eslint/types': 5.41.0
+ '@typescript-eslint/typescript-estree': 5.41.0(typescript@5.3.3)
debug: 4.3.4
- eslint: 7.32.0
- typescript: 4.4.4
+ eslint: 8.26.0
+ typescript: 5.3.3
transitivePeerDependencies:
- supports-color
dev: true
- /@typescript-eslint/parser/5.41.0_wyqvi574yv7oiwfeinomdzmc3m:
- resolution: {integrity: sha512-HQVfix4+RL5YRWZboMD1pUfFN8MpRH4laziWkkAzyO1fvNOY/uinZcvo3QiFJVS/siNHupV8E5+xSwQZrl6PZA==}
- engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ /@typescript-eslint/parser@6.19.0(eslint@8.56.0)(typescript@5.3.3):
+ resolution: {integrity: sha512-1DyBLG5SH7PYCd00QlroiW60YJ4rWMuUGa/JBV0iZuqi4l4IK3twKPq5ZkEebmGqRjXWVgsUzfd3+nZveewgow==}
+ engines: {node: ^16.0.0 || >=18.0.0}
peerDependencies:
- eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
+ eslint: ^7.0.0 || ^8.0.0
typescript: '*'
peerDependenciesMeta:
typescript:
optional: true
dependencies:
- '@typescript-eslint/scope-manager': 5.41.0
- '@typescript-eslint/types': 5.41.0
- '@typescript-eslint/typescript-estree': 5.41.0_typescript@4.8.4
+ '@typescript-eslint/scope-manager': 6.19.0
+ '@typescript-eslint/types': 6.19.0
+ '@typescript-eslint/typescript-estree': 6.19.0(typescript@5.3.3)
+ '@typescript-eslint/visitor-keys': 6.19.0
debug: 4.3.4
- eslint: 8.26.0
- typescript: 4.8.4
+ eslint: 8.56.0
+ typescript: 5.3.3
transitivePeerDependencies:
- supports-color
dev: true
- /@typescript-eslint/scope-manager/4.33.0:
+ /@typescript-eslint/scope-manager@4.33.0:
resolution: {integrity: sha512-5IfJHpgTsTZuONKbODctL4kKuQje/bzBRkwHE8UOZ4f89Zeddg+EGZs8PD8NcN4LdM3ygHWYB3ukPAYjvl/qbQ==}
engines: {node: ^8.10.0 || ^10.13.0 || >=11.10.1}
dependencies:
@@ -7612,7 +6504,7 @@ packages:
'@typescript-eslint/visitor-keys': 4.33.0
dev: true
- /@typescript-eslint/scope-manager/5.41.0:
+ /@typescript-eslint/scope-manager@5.41.0:
resolution: {integrity: sha512-xOxPJCnuktUkY2xoEZBKXO5DBCugFzjrVndKdUnyQr3+9aDWZReKq9MhaoVnbL+maVwWJu/N0SEtrtEUNb62QQ==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dependencies:
@@ -7620,7 +6512,15 @@ packages:
'@typescript-eslint/visitor-keys': 5.41.0
dev: true
- /@typescript-eslint/type-utils/5.41.0_wyqvi574yv7oiwfeinomdzmc3m:
+ /@typescript-eslint/scope-manager@6.19.0:
+ resolution: {integrity: sha512-dO1XMhV2ehBI6QN8Ufi7I10wmUovmLU0Oru3n5LVlM2JuzB4M+dVphCPLkVpKvGij2j/pHBWuJ9piuXx+BhzxQ==}
+ engines: {node: ^16.0.0 || >=18.0.0}
+ dependencies:
+ '@typescript-eslint/types': 6.19.0
+ '@typescript-eslint/visitor-keys': 6.19.0
+ dev: true
+
+ /@typescript-eslint/type-utils@5.41.0(eslint@8.26.0)(typescript@5.3.3):
resolution: {integrity: sha512-L30HNvIG6A1Q0R58e4hu4h+fZqaO909UcnnPbwKiN6Rc3BUEx6ez2wgN7aC0cBfcAjZfwkzE+E2PQQ9nEuoqfA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
@@ -7630,48 +6530,52 @@ packages:
typescript:
optional: true
dependencies:
- '@typescript-eslint/typescript-estree': 5.41.0_typescript@4.8.4
- '@typescript-eslint/utils': 5.41.0_wyqvi574yv7oiwfeinomdzmc3m
+ '@typescript-eslint/typescript-estree': 5.41.0(typescript@5.3.3)
+ '@typescript-eslint/utils': 5.41.0(eslint@8.26.0)(typescript@5.3.3)
debug: 4.3.4
eslint: 8.26.0
- tsutils: 3.21.0_typescript@4.8.4
- typescript: 4.8.4
+ tsutils: 3.21.0(typescript@5.3.3)
+ typescript: 5.3.3
transitivePeerDependencies:
- supports-color
dev: true
- /@typescript-eslint/types/4.33.0:
- resolution: {integrity: sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ==}
- engines: {node: ^8.10.0 || ^10.13.0 || >=11.10.1}
- dev: true
-
- /@typescript-eslint/types/5.41.0:
- resolution: {integrity: sha512-5BejraMXMC+2UjefDvrH0Fo/eLwZRV6859SXRg+FgbhA0R0l6lDqDGAQYhKbXhPN2ofk2kY5sgGyLNL907UXpA==}
- engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
- dev: true
-
- /@typescript-eslint/typescript-estree/4.33.0_typescript@4.4.4:
- resolution: {integrity: sha512-rkWRY1MPFzjwnEVHsxGemDzqqddw2QbTJlICPD9p9I9LfsO8fdmfQPOX3uKfUaGRDFJbfrtm/sXhVXN4E+bzCA==}
- engines: {node: ^10.12.0 || >=12.0.0}
+ /@typescript-eslint/type-utils@6.19.0(eslint@8.56.0)(typescript@5.3.3):
+ resolution: {integrity: sha512-mcvS6WSWbjiSxKCwBcXtOM5pRkPQ6kcDds/juxcy/727IQr3xMEcwr/YLHW2A2+Fp5ql6khjbKBzOyjuPqGi/w==}
+ engines: {node: ^16.0.0 || >=18.0.0}
peerDependencies:
+ eslint: ^7.0.0 || ^8.0.0
typescript: '*'
peerDependenciesMeta:
typescript:
optional: true
dependencies:
- '@typescript-eslint/types': 4.33.0
- '@typescript-eslint/visitor-keys': 4.33.0
+ '@typescript-eslint/typescript-estree': 6.19.0(typescript@5.3.3)
+ '@typescript-eslint/utils': 6.19.0(eslint@8.56.0)(typescript@5.3.3)
debug: 4.3.4
- globby: 11.1.0
- is-glob: 4.0.3
- semver: 7.3.8
- tsutils: 3.21.0_typescript@4.4.4
- typescript: 4.4.4
+ eslint: 8.56.0
+ ts-api-utils: 1.0.3(typescript@5.3.3)
+ typescript: 5.3.3
transitivePeerDependencies:
- supports-color
dev: true
- /@typescript-eslint/typescript-estree/4.33.0_typescript@4.8.4:
+ /@typescript-eslint/types@4.33.0:
+ resolution: {integrity: sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ==}
+ engines: {node: ^8.10.0 || ^10.13.0 || >=11.10.1}
+ dev: true
+
+ /@typescript-eslint/types@5.41.0:
+ resolution: {integrity: sha512-5BejraMXMC+2UjefDvrH0Fo/eLwZRV6859SXRg+FgbhA0R0l6lDqDGAQYhKbXhPN2ofk2kY5sgGyLNL907UXpA==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ dev: true
+
+ /@typescript-eslint/types@6.19.0:
+ resolution: {integrity: sha512-lFviGV/vYhOy3m8BJ/nAKoAyNhInTdXpftonhWle66XHAtT1ouBlkjL496b5H5hb8dWXHwtypTqgtb/DEa+j5A==}
+ engines: {node: ^16.0.0 || >=18.0.0}
+ dev: true
+
+ /@typescript-eslint/typescript-estree@4.33.0(typescript@5.3.3):
resolution: {integrity: sha512-rkWRY1MPFzjwnEVHsxGemDzqqddw2QbTJlICPD9p9I9LfsO8fdmfQPOX3uKfUaGRDFJbfrtm/sXhVXN4E+bzCA==}
engines: {node: ^10.12.0 || >=12.0.0}
peerDependencies:
@@ -7685,14 +6589,14 @@ packages:
debug: 4.3.4
globby: 11.1.0
is-glob: 4.0.3
- semver: 7.3.8
- tsutils: 3.21.0_typescript@4.8.4
- typescript: 4.8.4
+ semver: 7.5.4
+ tsutils: 3.21.0(typescript@5.3.3)
+ typescript: 5.3.3
transitivePeerDependencies:
- supports-color
dev: true
- /@typescript-eslint/typescript-estree/5.41.0_typescript@4.4.4:
+ /@typescript-eslint/typescript-estree@5.41.0(typescript@5.3.3):
resolution: {integrity: sha512-SlzFYRwFSvswzDSQ/zPkIWcHv8O5y42YUskko9c4ki+fV6HATsTODUPbRbcGDFYP86gaJL5xohUEytvyNNcXWg==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
@@ -7706,35 +6610,36 @@ packages:
debug: 4.3.4
globby: 11.1.0
is-glob: 4.0.3
- semver: 7.3.8
- tsutils: 3.21.0_typescript@4.4.4
- typescript: 4.4.4
+ semver: 7.5.4
+ tsutils: 3.21.0(typescript@5.3.3)
+ typescript: 5.3.3
transitivePeerDependencies:
- supports-color
dev: true
- /@typescript-eslint/typescript-estree/5.41.0_typescript@4.8.4:
- resolution: {integrity: sha512-SlzFYRwFSvswzDSQ/zPkIWcHv8O5y42YUskko9c4ki+fV6HATsTODUPbRbcGDFYP86gaJL5xohUEytvyNNcXWg==}
- engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ /@typescript-eslint/typescript-estree@6.19.0(typescript@5.3.3):
+ resolution: {integrity: sha512-o/zefXIbbLBZ8YJ51NlkSAt2BamrK6XOmuxSR3hynMIzzyMY33KuJ9vuMdFSXW+H0tVvdF9qBPTHA91HDb4BIQ==}
+ engines: {node: ^16.0.0 || >=18.0.0}
peerDependencies:
typescript: '*'
peerDependenciesMeta:
typescript:
optional: true
dependencies:
- '@typescript-eslint/types': 5.41.0
- '@typescript-eslint/visitor-keys': 5.41.0
+ '@typescript-eslint/types': 6.19.0
+ '@typescript-eslint/visitor-keys': 6.19.0
debug: 4.3.4
globby: 11.1.0
is-glob: 4.0.3
- semver: 7.3.8
- tsutils: 3.21.0_typescript@4.8.4
- typescript: 4.8.4
+ minimatch: 9.0.3
+ semver: 7.5.4
+ ts-api-utils: 1.0.3(typescript@5.3.3)
+ typescript: 5.3.3
transitivePeerDependencies:
- supports-color
dev: true
- /@typescript-eslint/utils/5.41.0_3rubbgt5ekhqrcgx4uwls3neim:
+ /@typescript-eslint/utils@5.41.0(eslint@7.32.0)(typescript@5.3.3):
resolution: {integrity: sha512-QlvfwaN9jaMga9EBazQ+5DDx/4sAdqDkcs05AsQHMaopluVCUyu1bTRUVKzXbgjDlrRAQrYVoi/sXJ9fmG+KLQ==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
@@ -7744,17 +6649,17 @@ packages:
'@types/semver': 7.3.12
'@typescript-eslint/scope-manager': 5.41.0
'@typescript-eslint/types': 5.41.0
- '@typescript-eslint/typescript-estree': 5.41.0_typescript@4.8.4
+ '@typescript-eslint/typescript-estree': 5.41.0(typescript@5.3.3)
eslint: 7.32.0
eslint-scope: 5.1.1
- eslint-utils: 3.0.0_eslint@7.32.0
- semver: 7.3.8
+ eslint-utils: 3.0.0(eslint@7.32.0)
+ semver: 7.5.4
transitivePeerDependencies:
- supports-color
- typescript
dev: true
- /@typescript-eslint/utils/5.41.0_wnilx7boviscikmvsfkd6ljepe:
+ /@typescript-eslint/utils@5.41.0(eslint@8.26.0)(typescript@5.3.3):
resolution: {integrity: sha512-QlvfwaN9jaMga9EBazQ+5DDx/4sAdqDkcs05AsQHMaopluVCUyu1bTRUVKzXbgjDlrRAQrYVoi/sXJ9fmG+KLQ==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
@@ -7764,37 +6669,36 @@ packages:
'@types/semver': 7.3.12
'@typescript-eslint/scope-manager': 5.41.0
'@typescript-eslint/types': 5.41.0
- '@typescript-eslint/typescript-estree': 5.41.0_typescript@4.4.4
- eslint: 7.32.0
+ '@typescript-eslint/typescript-estree': 5.41.0(typescript@5.3.3)
+ eslint: 8.26.0
eslint-scope: 5.1.1
- eslint-utils: 3.0.0_eslint@7.32.0
- semver: 7.3.8
+ eslint-utils: 3.0.0(eslint@8.26.0)
+ semver: 7.5.4
transitivePeerDependencies:
- supports-color
- typescript
dev: true
- /@typescript-eslint/utils/5.41.0_wyqvi574yv7oiwfeinomdzmc3m:
- resolution: {integrity: sha512-QlvfwaN9jaMga9EBazQ+5DDx/4sAdqDkcs05AsQHMaopluVCUyu1bTRUVKzXbgjDlrRAQrYVoi/sXJ9fmG+KLQ==}
- engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ /@typescript-eslint/utils@6.19.0(eslint@8.56.0)(typescript@5.3.3):
+ resolution: {integrity: sha512-QR41YXySiuN++/dC9UArYOg4X86OAYP83OWTewpVx5ct1IZhjjgTLocj7QNxGhWoTqknsgpl7L+hGygCO+sdYw==}
+ engines: {node: ^16.0.0 || >=18.0.0}
peerDependencies:
- eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
+ eslint: ^7.0.0 || ^8.0.0
dependencies:
- '@types/json-schema': 7.0.11
- '@types/semver': 7.3.12
- '@typescript-eslint/scope-manager': 5.41.0
- '@typescript-eslint/types': 5.41.0
- '@typescript-eslint/typescript-estree': 5.41.0_typescript@4.8.4
- eslint: 8.26.0
- eslint-scope: 5.1.1
- eslint-utils: 3.0.0_eslint@8.26.0
- semver: 7.3.8
+ '@eslint-community/eslint-utils': 4.4.0(eslint@8.56.0)
+ '@types/json-schema': 7.0.15
+ '@types/semver': 7.5.6
+ '@typescript-eslint/scope-manager': 6.19.0
+ '@typescript-eslint/types': 6.19.0
+ '@typescript-eslint/typescript-estree': 6.19.0(typescript@5.3.3)
+ eslint: 8.56.0
+ semver: 7.5.4
transitivePeerDependencies:
- supports-color
- typescript
dev: true
- /@typescript-eslint/visitor-keys/4.33.0:
+ /@typescript-eslint/visitor-keys@4.33.0:
resolution: {integrity: sha512-uqi/2aSz9g2ftcHWf8uLPJA70rUv6yuMW5Bohw+bwcuzaxQIHaKFZCKGoGXIrc9vkTJ3+0txM73K0Hq3d5wgIg==}
engines: {node: ^8.10.0 || ^10.13.0 || >=11.10.1}
dependencies:
@@ -7802,26 +6706,52 @@ packages:
eslint-visitor-keys: 2.1.0
dev: true
- /@typescript-eslint/visitor-keys/5.41.0:
+ /@typescript-eslint/visitor-keys@5.41.0:
resolution: {integrity: sha512-vilqeHj267v8uzzakbm13HkPMl7cbYpKVjgFWZPIOHIJHZtinvypUhJ5xBXfWYg4eFKqztbMMpOgFpT9Gfx4fw==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dependencies:
'@typescript-eslint/types': 5.41.0
- eslint-visitor-keys: 3.3.0
+ eslint-visitor-keys: 3.4.3
dev: true
- /@ungap/promise-all-settled/1.1.2:
+ /@typescript-eslint/visitor-keys@6.19.0:
+ resolution: {integrity: sha512-hZaUCORLgubBvtGpp1JEFEazcuEdfxta9j4iUwdSAr7mEsYYAp3EAUyCZk3VEEqGj6W+AV4uWyrDGtrlawAsgQ==}
+ engines: {node: ^16.0.0 || >=18.0.0}
+ dependencies:
+ '@typescript-eslint/types': 6.19.0
+ eslint-visitor-keys: 3.4.3
+ dev: true
+
+ /@ungap/promise-all-settled@1.1.2:
resolution: {integrity: sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==}
dev: true
- /@webassemblyjs/ast/1.11.1:
- resolution: {integrity: sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==}
+ /@ungap/structured-clone@1.2.0:
+ resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
+ dev: true
+
+ /@vercel/nft@0.24.4:
+ resolution: {integrity: sha512-KjYAZty7boH5fi5udp6p+lNu6nawgs++pHW+3koErMgbRkkHuToGX/FwjN5clV1FcaM3udfd4zW/sUapkMgpZw==}
+ engines: {node: '>=16'}
+ hasBin: true
dependencies:
- '@webassemblyjs/helper-numbers': 1.11.1
- '@webassemblyjs/helper-wasm-bytecode': 1.11.1
+ '@mapbox/node-pre-gyp': 1.0.11
+ '@rollup/pluginutils': 4.2.1
+ acorn: 8.11.2
+ async-sema: 3.1.1
+ bindings: 1.5.0
+ estree-walker: 2.0.2
+ glob: 7.2.3
+ graceful-fs: 4.2.11
+ micromatch: 4.0.5
+ node-gyp-build: 4.7.1
+ resolve-from: 5.0.0
+ transitivePeerDependencies:
+ - encoding
+ - supports-color
dev: true
- /@webassemblyjs/ast/1.9.0:
+ /@webassemblyjs/ast@1.9.0:
resolution: {integrity: sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA==}
dependencies:
'@webassemblyjs/helper-module-context': 1.9.0
@@ -7829,72 +6759,39 @@ packages:
'@webassemblyjs/wast-parser': 1.9.0
dev: true
- /@webassemblyjs/floating-point-hex-parser/1.11.1:
- resolution: {integrity: sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==}
- dev: true
-
- /@webassemblyjs/floating-point-hex-parser/1.9.0:
+ /@webassemblyjs/floating-point-hex-parser@1.9.0:
resolution: {integrity: sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA==}
dev: true
- /@webassemblyjs/helper-api-error/1.11.1:
- resolution: {integrity: sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==}
- dev: true
-
- /@webassemblyjs/helper-api-error/1.9.0:
+ /@webassemblyjs/helper-api-error@1.9.0:
resolution: {integrity: sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw==}
dev: true
- /@webassemblyjs/helper-buffer/1.11.1:
- resolution: {integrity: sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==}
- dev: true
-
- /@webassemblyjs/helper-buffer/1.9.0:
+ /@webassemblyjs/helper-buffer@1.9.0:
resolution: {integrity: sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA==}
dev: true
- /@webassemblyjs/helper-code-frame/1.9.0:
+ /@webassemblyjs/helper-code-frame@1.9.0:
resolution: {integrity: sha512-ERCYdJBkD9Vu4vtjUYe8LZruWuNIToYq/ME22igL+2vj2dQ2OOujIZr3MEFvfEaqKoVqpsFKAGsRdBSBjrIvZA==}
dependencies:
'@webassemblyjs/wast-printer': 1.9.0
dev: true
- /@webassemblyjs/helper-fsm/1.9.0:
+ /@webassemblyjs/helper-fsm@1.9.0:
resolution: {integrity: sha512-OPRowhGbshCb5PxJ8LocpdX9Kl0uB4XsAjl6jH/dWKlk/mzsANvhwbiULsaiqT5GZGT9qinTICdj6PLuM5gslw==}
dev: true
- /@webassemblyjs/helper-module-context/1.9.0:
+ /@webassemblyjs/helper-module-context@1.9.0:
resolution: {integrity: sha512-MJCW8iGC08tMk2enck1aPW+BE5Cw8/7ph/VGZxwyvGbJwjktKkDK7vy7gAmMDx88D7mhDTCNKAW5tED+gZ0W8g==}
dependencies:
'@webassemblyjs/ast': 1.9.0
dev: true
- /@webassemblyjs/helper-numbers/1.11.1:
- resolution: {integrity: sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==}
- dependencies:
- '@webassemblyjs/floating-point-hex-parser': 1.11.1
- '@webassemblyjs/helper-api-error': 1.11.1
- '@xtuc/long': 4.2.2
- dev: true
-
- /@webassemblyjs/helper-wasm-bytecode/1.11.1:
- resolution: {integrity: sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==}
- dev: true
-
- /@webassemblyjs/helper-wasm-bytecode/1.9.0:
+ /@webassemblyjs/helper-wasm-bytecode@1.9.0:
resolution: {integrity: sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw==}
dev: true
- /@webassemblyjs/helper-wasm-section/1.11.1:
- resolution: {integrity: sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==}
- dependencies:
- '@webassemblyjs/ast': 1.11.1
- '@webassemblyjs/helper-buffer': 1.11.1
- '@webassemblyjs/helper-wasm-bytecode': 1.11.1
- '@webassemblyjs/wasm-gen': 1.11.1
- dev: true
-
- /@webassemblyjs/helper-wasm-section/1.9.0:
+ /@webassemblyjs/helper-wasm-section@1.9.0:
resolution: {integrity: sha512-XnMB8l3ek4tvrKUUku+IVaXNHz2YsJyOOmz+MMkZvh8h1uSJpSen6vYnw3IoQ7WwEuAhL8Efjms1ZWjqh2agvw==}
dependencies:
'@webassemblyjs/ast': 1.9.0
@@ -7903,52 +6800,23 @@ packages:
'@webassemblyjs/wasm-gen': 1.9.0
dev: true
- /@webassemblyjs/ieee754/1.11.1:
- resolution: {integrity: sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==}
- dependencies:
- '@xtuc/ieee754': 1.2.0
- dev: true
-
- /@webassemblyjs/ieee754/1.9.0:
+ /@webassemblyjs/ieee754@1.9.0:
resolution: {integrity: sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg==}
dependencies:
'@xtuc/ieee754': 1.2.0
dev: true
- /@webassemblyjs/leb128/1.11.1:
- resolution: {integrity: sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==}
- dependencies:
- '@xtuc/long': 4.2.2
- dev: true
-
- /@webassemblyjs/leb128/1.9.0:
+ /@webassemblyjs/leb128@1.9.0:
resolution: {integrity: sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw==}
dependencies:
'@xtuc/long': 4.2.2
dev: true
- /@webassemblyjs/utf8/1.11.1:
- resolution: {integrity: sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==}
- dev: true
-
- /@webassemblyjs/utf8/1.9.0:
+ /@webassemblyjs/utf8@1.9.0:
resolution: {integrity: sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w==}
dev: true
- /@webassemblyjs/wasm-edit/1.11.1:
- resolution: {integrity: sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==}
- dependencies:
- '@webassemblyjs/ast': 1.11.1
- '@webassemblyjs/helper-buffer': 1.11.1
- '@webassemblyjs/helper-wasm-bytecode': 1.11.1
- '@webassemblyjs/helper-wasm-section': 1.11.1
- '@webassemblyjs/wasm-gen': 1.11.1
- '@webassemblyjs/wasm-opt': 1.11.1
- '@webassemblyjs/wasm-parser': 1.11.1
- '@webassemblyjs/wast-printer': 1.11.1
- dev: true
-
- /@webassemblyjs/wasm-edit/1.9.0:
+ /@webassemblyjs/wasm-edit@1.9.0:
resolution: {integrity: sha512-FgHzBm80uwz5M8WKnMTn6j/sVbqilPdQXTWraSjBwFXSYGirpkSWE2R9Qvz9tNiTKQvoKILpCuTjBKzOIm0nxw==}
dependencies:
'@webassemblyjs/ast': 1.9.0
@@ -7961,17 +6829,7 @@ packages:
'@webassemblyjs/wast-printer': 1.9.0
dev: true
- /@webassemblyjs/wasm-gen/1.11.1:
- resolution: {integrity: sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==}
- dependencies:
- '@webassemblyjs/ast': 1.11.1
- '@webassemblyjs/helper-wasm-bytecode': 1.11.1
- '@webassemblyjs/ieee754': 1.11.1
- '@webassemblyjs/leb128': 1.11.1
- '@webassemblyjs/utf8': 1.11.1
- dev: true
-
- /@webassemblyjs/wasm-gen/1.9.0:
+ /@webassemblyjs/wasm-gen@1.9.0:
resolution: {integrity: sha512-cPE3o44YzOOHvlsb4+E9qSqjc9Qf9Na1OO/BHFy4OI91XDE14MjFN4lTMezzaIWdPqHnsTodGGNP+iRSYfGkjA==}
dependencies:
'@webassemblyjs/ast': 1.9.0
@@ -7981,16 +6839,7 @@ packages:
'@webassemblyjs/utf8': 1.9.0
dev: true
- /@webassemblyjs/wasm-opt/1.11.1:
- resolution: {integrity: sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==}
- dependencies:
- '@webassemblyjs/ast': 1.11.1
- '@webassemblyjs/helper-buffer': 1.11.1
- '@webassemblyjs/wasm-gen': 1.11.1
- '@webassemblyjs/wasm-parser': 1.11.1
- dev: true
-
- /@webassemblyjs/wasm-opt/1.9.0:
+ /@webassemblyjs/wasm-opt@1.9.0:
resolution: {integrity: sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A==}
dependencies:
'@webassemblyjs/ast': 1.9.0
@@ -7999,18 +6848,7 @@ packages:
'@webassemblyjs/wasm-parser': 1.9.0
dev: true
- /@webassemblyjs/wasm-parser/1.11.1:
- resolution: {integrity: sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==}
- dependencies:
- '@webassemblyjs/ast': 1.11.1
- '@webassemblyjs/helper-api-error': 1.11.1
- '@webassemblyjs/helper-wasm-bytecode': 1.11.1
- '@webassemblyjs/ieee754': 1.11.1
- '@webassemblyjs/leb128': 1.11.1
- '@webassemblyjs/utf8': 1.11.1
- dev: true
-
- /@webassemblyjs/wasm-parser/1.9.0:
+ /@webassemblyjs/wasm-parser@1.9.0:
resolution: {integrity: sha512-9+wkMowR2AmdSWQzsPEjFU7njh8HTO5MqO8vjwEHuM+AMHioNqSBONRdr0NQQ3dVQrzp0s8lTcYqzUdb7YgELA==}
dependencies:
'@webassemblyjs/ast': 1.9.0
@@ -8021,7 +6859,7 @@ packages:
'@webassemblyjs/utf8': 1.9.0
dev: true
- /@webassemblyjs/wast-parser/1.9.0:
+ /@webassemblyjs/wast-parser@1.9.0:
resolution: {integrity: sha512-qsqSAP3QQ3LyZjNC/0jBJ/ToSxfYJ8kYyuiGvtn/8MK89VrNEfwj7BPQzJVHi0jGTRK2dGdJ5PRqhtjzoww+bw==}
dependencies:
'@webassemblyjs/ast': 1.9.0
@@ -8032,14 +6870,7 @@ packages:
'@xtuc/long': 4.2.2
dev: true
- /@webassemblyjs/wast-printer/1.11.1:
- resolution: {integrity: sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==}
- dependencies:
- '@webassemblyjs/ast': 1.11.1
- '@xtuc/long': 4.2.2
- dev: true
-
- /@webassemblyjs/wast-printer/1.9.0:
+ /@webassemblyjs/wast-printer@1.9.0:
resolution: {integrity: sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA==}
dependencies:
'@webassemblyjs/ast': 1.9.0
@@ -8047,38 +6878,30 @@ packages:
'@xtuc/long': 4.2.2
dev: true
- /@xtuc/ieee754/1.2.0:
+ /@xtuc/ieee754@1.2.0:
resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==}
dev: true
- /@xtuc/long/4.2.2:
+ /@xtuc/long@4.2.2:
resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==}
dev: true
- /@yarnpkg/lockfile/1.1.0:
- resolution: {integrity: sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==}
+ /abab@2.0.6:
+ resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==}
dev: true
- /@yarnpkg/parsers/3.0.0-rc.26:
- resolution: {integrity: sha512-F52Zryoi6uSHi43A/htykDD7l1707TQjHeAHTKxNWJBTwvrEKWYvuu1w8bzSHpFVc06ig2KyrpHPfmeiuOip8Q==}
- engines: {node: '>=14.15.0'}
- dependencies:
- js-yaml: 3.14.1
- tslib: 2.4.1
+ /abbrev@1.1.1:
+ resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
dev: true
- /@zkochan/js-yaml/0.0.6:
- resolution: {integrity: sha512-nzvgl3VfhcELQ8LyVrYOru+UtAy1nrygk2+AGbTm8a5YcO6o8lSjAT+pfg3vJWxIoZKOUhrK6UU7xW/+00kQrg==}
- hasBin: true
+ /abort-controller@3.0.0:
+ resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
+ engines: {node: '>=6.5'}
dependencies:
- argparse: 2.0.1
- dev: true
-
- /abab/2.0.6:
- resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==}
+ event-target-shim: 5.0.1
dev: true
- /accepts/1.3.8:
+ /accepts@1.3.8:
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
engines: {node: '>= 0.6'}
dependencies:
@@ -8086,37 +6909,30 @@ packages:
negotiator: 0.6.3
dev: true
- /acorn-globals/4.3.4:
+ /acorn-globals@4.3.4:
resolution: {integrity: sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==}
dependencies:
acorn: 6.4.2
acorn-walk: 6.2.0
dev: true
- /acorn-globals/6.0.0:
- resolution: {integrity: sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==}
- dependencies:
- acorn: 7.4.1
- acorn-walk: 7.2.0
- dev: true
-
- /acorn-import-assertions/1.8.0_acorn@8.8.1:
- resolution: {integrity: sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==}
+ /acorn-jsx@5.3.2(acorn@7.4.1):
+ resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies:
- acorn: ^8
+ acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
dependencies:
- acorn: 8.8.1
+ acorn: 7.4.1
dev: true
- /acorn-jsx/5.3.2_acorn@7.4.1:
+ /acorn-jsx@5.3.2(acorn@8.11.2):
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies:
acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
dependencies:
- acorn: 7.4.1
+ acorn: 8.11.2
dev: true
- /acorn-jsx/5.3.2_acorn@8.8.1:
+ /acorn-jsx@5.3.2(acorn@8.8.1):
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies:
acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
@@ -8124,45 +6940,130 @@ packages:
acorn: 8.8.1
dev: true
- /acorn-walk/6.2.0:
+ /acorn-walk@6.2.0:
resolution: {integrity: sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==}
engines: {node: '>=0.4.0'}
dev: true
- /acorn-walk/7.2.0:
- resolution: {integrity: sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==}
+ /acorn-walk@8.2.0:
+ resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==}
engines: {node: '>=0.4.0'}
dev: true
- /acorn-walk/8.2.0:
- resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==}
+ /acorn-walk@8.3.1:
+ resolution: {integrity: sha512-TgUZgYvqZprrl7YldZNoa9OciCAyZR+Ejm9eXzKCmjsF5IKp/wgQ7Z/ZpjpGTIUPwrHQIcYeI8qDh4PsEwxMbw==}
engines: {node: '>=0.4.0'}
dev: true
- /acorn/6.4.2:
+ /acorn@6.4.2:
resolution: {integrity: sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==}
engines: {node: '>=0.4.0'}
hasBin: true
dev: true
- /acorn/7.4.1:
+ /acorn@7.4.1:
resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==}
engines: {node: '>=0.4.0'}
+ dev: true
+
+ /acorn@8.11.2:
+ resolution: {integrity: sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==}
+ engines: {node: '>=0.4.0'}
hasBin: true
dev: true
- /acorn/8.8.1:
+ /acorn@8.8.1:
resolution: {integrity: sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==}
engines: {node: '>=0.4.0'}
+ dev: true
+
+ /acorn@8.8.2:
+ resolution: {integrity: sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==}
+ engines: {node: '>=0.4.0'}
hasBin: true
dev: true
- /address/1.2.1:
- resolution: {integrity: sha512-B+6bi5D34+fDYENiH5qOlA0cV2rAGKuWZ9LeyUUehbXy8e0VS9e498yO0Jeeh+iM+6KbfudHTFjXw2MmJD4QRA==}
- engines: {node: '>= 10.0.0'}
+ /addons-linter@6.21.0(node-fetch@3.3.1):
+ resolution: {integrity: sha512-4GBn14BR16FZE7dog6uz+1HO6V3B+mAVxmbwxRhed2y5eyrwIW832TmEpku+5A5bbovBZ4gilXEtBsl6A1AMmg==}
+ engines: {node: '>=16.0.0'}
+ hasBin: true
+ dependencies:
+ '@fluent/syntax': 0.19.0
+ '@mdn/browser-compat-data': 5.5.7
+ addons-moz-compare: 1.3.0
+ addons-scanner-utils: 9.9.0(node-fetch@3.3.1)
+ ajv: 8.12.0
+ chalk: 4.1.2
+ cheerio: 1.0.0-rc.12
+ columnify: 1.6.0
+ common-tags: 1.8.2
+ deepmerge: 4.3.1
+ eslint: 8.56.0
+ eslint-plugin-no-unsanitized: 4.0.2(eslint@8.56.0)
+ eslint-visitor-keys: 3.4.3
+ espree: 9.6.1
+ esprima: 4.0.1
+ fast-json-patch: 3.1.1
+ glob: 10.3.10
+ image-size: 1.1.1
+ is-mergeable-object: 1.1.1
+ jed: 1.1.1
+ json-merge-patch: 1.0.2
+ os-locale: 5.0.0
+ pino: 8.17.2
+ postcss: 8.4.33
+ relaxed-json: 1.0.3
+ semver: 7.5.4
+ sha.js: 2.4.11
+ source-map-support: 0.5.21
+ tosource: 1.0.0
+ upath: 2.0.1
+ yargs: 17.7.2
+ yauzl: 2.10.0
+ transitivePeerDependencies:
+ - body-parser
+ - express
+ - node-fetch
+ - safe-compare
+ - supports-color
+ dev: true
+
+ /addons-moz-compare@1.3.0:
+ resolution: {integrity: sha512-/rXpQeaY0nOKhNx00pmZXdk5Mu+KhVlL3/pSBuAYwrxRrNiTvI/9xfQI8Lmm7DMMl+PDhtfAHY/0ibTpdeoQQQ==}
+ dev: true
+
+ /addons-scanner-utils@9.9.0(node-fetch@3.3.1):
+ resolution: {integrity: sha512-YDP10U3sEZMuIgnjXMiAYgUU64jTbxmhpUXMlhi1nKO4Etz+ctGWoTUst7IQRoLWaY9y2r1KZDG3jALxLA1n7Q==}
+ peerDependencies:
+ body-parser: 1.20.2
+ express: 4.18.2
+ node-fetch: 2.6.11
+ safe-compare: 1.1.4
+ peerDependenciesMeta:
+ body-parser:
+ optional: true
+ express:
+ optional: true
+ node-fetch:
+ optional: true
+ safe-compare:
+ optional: true
+ dependencies:
+ '@types/yauzl': 2.10.3
+ common-tags: 1.8.2
+ first-chunk-stream: 3.0.0
+ node-fetch: 3.3.1
+ strip-bom-stream: 4.0.0
+ upath: 2.0.1
+ yauzl: 2.10.0
dev: true
- /agent-base/6.0.2:
+ /adm-zip@0.5.10:
+ resolution: {integrity: sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ==}
+ engines: {node: '>=6.0'}
+ dev: true
+
+ /agent-base@6.0.2:
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
engines: {node: '>= 6.0.0'}
dependencies:
@@ -8171,7 +7072,7 @@ packages:
- supports-color
dev: true
- /aggregate-error/3.1.0:
+ /aggregate-error@3.1.0:
resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==}
engines: {node: '>=8'}
dependencies:
@@ -8179,37 +7080,7 @@ packages:
indent-string: 4.0.0
dev: true
- /aggregate-error/4.0.1:
- resolution: {integrity: sha512-0poP0T7el6Vq3rstR8Mn4V/IQrpBLO6POkUSrN7RhyY+GF/InCFShQzsQ39T25gkHhLgSLByyAz+Kjb+c2L98w==}
- engines: {node: '>=12'}
- dependencies:
- clean-stack: 4.2.0
- indent-string: 5.0.0
- dev: true
-
- /airbnb-js-shims/2.2.1:
- resolution: {integrity: sha512-wJNXPH66U2xjgo1Zwyjf9EydvJ2Si94+vSdk6EERcBfB2VZkeltpqIats0cqIZMLCXP3zcyaUKGYQeIBT6XjsQ==}
- dependencies:
- array-includes: 3.1.5
- array.prototype.flat: 1.3.0
- array.prototype.flatmap: 1.3.0
- es5-shim: 4.6.7
- es6-shim: 0.35.6
- function.prototype.name: 1.1.5
- globalthis: 1.0.3
- object.entries: 1.1.5
- object.fromentries: 2.0.5
- object.getownpropertydescriptors: 2.1.4
- object.values: 1.1.5
- promise.allsettled: 1.0.5
- promise.prototype.finally: 3.1.3
- string.prototype.matchall: 4.0.7
- string.prototype.padend: 3.1.3
- string.prototype.padstart: 3.1.3
- symbol.prototype.description: 1.0.5
- dev: true
-
- /ajv-errors/1.0.1_ajv@6.12.6:
+ /ajv-errors@1.0.1(ajv@6.12.6):
resolution: {integrity: sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==}
peerDependencies:
ajv: '>=5.0.0'
@@ -8217,8 +7088,10 @@ packages:
ajv: 6.12.6
dev: true
- /ajv-formats/2.1.1:
+ /ajv-formats@2.1.1(ajv@8.11.0):
resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==}
+ peerDependencies:
+ ajv: ^8.0.0
peerDependenciesMeta:
ajv:
optional: true
@@ -8226,7 +7099,7 @@ packages:
ajv: 8.11.0
dev: true
- /ajv-keywords/3.5.2_ajv@6.12.6:
+ /ajv-keywords@3.5.2(ajv@6.12.6):
resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==}
peerDependencies:
ajv: ^6.9.1
@@ -8234,7 +7107,7 @@ packages:
ajv: 6.12.6
dev: true
- /ajv-keywords/5.1.0_ajv@8.11.0:
+ /ajv-keywords@5.1.0(ajv@8.11.0):
resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==}
peerDependencies:
ajv: ^8.8.2
@@ -8243,7 +7116,7 @@ packages:
fast-deep-equal: 3.1.3
dev: true
- /ajv/6.12.6:
+ /ajv@6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
dependencies:
fast-deep-equal: 3.1.3
@@ -8252,7 +7125,7 @@ packages:
uri-js: 4.4.1
dev: true
- /ajv/8.11.0:
+ /ajv@8.11.0:
resolution: {integrity: sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==}
dependencies:
fast-deep-equal: 3.1.3
@@ -8261,296 +7134,300 @@ packages:
uri-js: 4.4.1
dev: true
- /alphanum-sort/1.0.2:
+ /ajv@8.12.0:
+ resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==}
+ dependencies:
+ fast-deep-equal: 3.1.3
+ json-schema-traverse: 1.0.0
+ require-from-string: 2.0.2
+ uri-js: 4.4.1
+ dev: true
+
+ /alphanum-sort@1.0.2:
resolution: {integrity: sha512-0FcBfdcmaumGPQ0qPn7Q5qTgz/ooXgIyp1rf8ik5bGX8mpE2YHjC0P/eyQvxu1GURYQgq9ozf2mteQ5ZD9YiyQ==}
dev: true
- /ansi-align/3.0.1:
+ /ansi-align@3.0.1:
resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==}
dependencies:
string-width: 4.2.3
dev: true
- /ansi-colors/3.2.4:
+ /ansi-colors@3.2.4:
resolution: {integrity: sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==}
engines: {node: '>=6'}
dev: true
- /ansi-colors/4.1.1:
+ /ansi-colors@4.1.1:
resolution: {integrity: sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==}
engines: {node: '>=6'}
dev: true
- /ansi-colors/4.1.3:
+ /ansi-colors@4.1.3:
resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
engines: {node: '>=6'}
dev: true
- /ansi-escapes/4.3.2:
- resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==}
- engines: {node: '>=8'}
- dependencies:
- type-fest: 0.21.3
- dev: true
-
- /ansi-html-community/0.0.8:
+ /ansi-html-community@0.0.8:
resolution: {integrity: sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==}
engines: {'0': node >= 0.8.0}
- hasBin: true
dev: true
- /ansi-regex/2.1.1:
+ /ansi-regex@2.1.1:
resolution: {integrity: sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==}
engines: {node: '>=0.10.0'}
dev: true
- /ansi-regex/5.0.1:
+ /ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
- dev: true
- /ansi-regex/6.0.1:
+ /ansi-regex@6.0.1:
resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==}
engines: {node: '>=12'}
+
+ /ansi-sequence-parser@1.1.1:
+ resolution: {integrity: sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg==}
dev: true
- /ansi-styles/1.0.0:
+ /ansi-styles@1.0.0:
resolution: {integrity: sha512-3iF4FIKdxaVYT3JqQuY3Wat/T2t7TRbbQ94Fu50ZUCbLy4TFbTzr90NOHQodQkNqmeEGCw8WbeP78WNi6SKYUA==}
engines: {node: '>=0.8.0'}
dev: true
- /ansi-styles/3.2.1:
+ /ansi-styles@3.2.1:
resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==}
engines: {node: '>=4'}
dependencies:
color-convert: 1.9.3
- dev: true
- /ansi-styles/4.3.0:
+ /ansi-styles@4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
dependencies:
color-convert: 2.0.1
- dev: true
-
- /ansi-styles/5.2.0:
- resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
- engines: {node: '>=10'}
- dev: true
- /ansi-styles/6.2.1:
+ /ansi-styles@6.2.1:
resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
engines: {node: '>=12'}
- dev: true
- /ansi-to-html/0.6.15:
- resolution: {integrity: sha512-28ijx2aHJGdzbs+O5SNQF65r6rrKYnkuwTYm8lZlChuoJ9P1vVzIpWO20sQTqTPDXYp6NFwk326vApTtLVFXpQ==}
- engines: {node: '>=8.0.0'}
- hasBin: true
- dependencies:
- entities: 2.2.0
- dev: true
+ /any-promise@1.3.0:
+ resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
- /anymatch/2.0.0:
+ /anymatch@2.0.0:
resolution: {integrity: sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==}
+ requiresBuild: true
dependencies:
micromatch: 3.1.10
normalize-path: 2.1.1
transitivePeerDependencies:
- supports-color
dev: true
+ optional: true
- /anymatch/3.1.2:
+ /anymatch@3.1.2:
resolution: {integrity: sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==}
engines: {node: '>= 8'}
dependencies:
normalize-path: 3.0.0
picomatch: 2.3.1
- dev: true
- /app-root-dir/1.0.2:
- resolution: {integrity: sha512-jlpIfsOoNoafl92Sz//64uQHGSyMrD2vYG5d8o2a4qGvyNCvXur7bzIsWtAC/6flI2RYAp3kv8rsfBtaLm7w0g==}
- dev: true
-
- /append-transform/2.0.0:
+ /append-transform@2.0.0:
resolution: {integrity: sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==}
engines: {node: '>=8'}
dependencies:
default-require-extensions: 3.0.1
dev: true
- /aproba/1.2.0:
+ /aproba@1.2.0:
resolution: {integrity: sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==}
dev: true
- /aproba/2.0.0:
+ /aproba@2.0.0:
resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==}
dev: true
- /archy/1.0.0:
+ /archy@1.0.0:
resolution: {integrity: sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==}
dev: true
- /are-we-there-yet/2.0.0:
+ /are-we-there-yet@2.0.0:
resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==}
engines: {node: '>=10'}
dependencies:
delegates: 1.0.0
- readable-stream: 3.6.0
+ readable-stream: 3.6.2
+ dev: true
+
+ /arg@4.1.3:
+ resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
dev: true
- /argparse/1.0.10:
+ /arg@5.0.2:
+ resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
+
+ /argparse@1.0.10:
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
dependencies:
sprintf-js: 1.0.3
dev: true
- /argparse/2.0.1:
+ /argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
dev: true
- /aria-query/4.2.2:
- resolution: {integrity: sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==}
- engines: {node: '>=6.0'}
+ /aria-query@5.3.0:
+ resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
dependencies:
- '@babel/runtime': 7.19.4
- '@babel/runtime-corejs3': 7.19.6
+ dequal: 2.0.3
dev: true
- /arr-diff/4.0.0:
+ /arr-diff@4.0.0:
resolution: {integrity: sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==}
engines: {node: '>=0.10.0'}
dev: true
- /arr-flatten/1.1.0:
+ /arr-flatten@1.1.0:
resolution: {integrity: sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==}
engines: {node: '>=0.10.0'}
dev: true
- /arr-union/3.1.0:
+ /arr-union@3.1.0:
resolution: {integrity: sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==}
engines: {node: '>=0.10.0'}
dev: true
- /array-equal/1.0.0:
+ /array-buffer-byte-length@1.0.0:
+ resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==}
+ dependencies:
+ call-bind: 1.0.5
+ is-array-buffer: 3.0.2
+ dev: true
+
+ /array-differ@4.0.0:
+ resolution: {integrity: sha512-Q6VPTLMsmXZ47ENG3V+wQyZS1ZxXMxFyYzA+Z/GMrJ6yIutAIEf9wTyroTzmGjNfox9/h3GdGBCVh43GVFx4Uw==}
+ engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+ dev: true
+
+ /array-equal@1.0.0:
resolution: {integrity: sha512-H3LU5RLiSsGXPhN+Nipar0iR0IofH+8r89G2y1tBKxQ/agagKyAjhkAFDRBfodP2caPrNKHpAWNIM/c9yeL7uA==}
dev: true
- /array-find-index/1.0.2:
+ /array-find-index@1.0.2:
resolution: {integrity: sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==}
engines: {node: '>=0.10.0'}
dev: true
- /array-flatten/1.1.1:
+ /array-flatten@1.1.1:
resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==}
dev: true
- /array-flatten/2.1.2:
+ /array-flatten@2.1.2:
resolution: {integrity: sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==}
dev: true
- /array-includes/3.1.5:
- resolution: {integrity: sha512-iSDYZMMyTPkiFasVqfuAQnWAYcvO/SeBSCGKePoEthjp4LEMTe4uLc7b025o4jAZpHhihh8xPo99TNWUWWkGDQ==}
+ /array-includes@3.1.7:
+ resolution: {integrity: sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==}
engines: {node: '>= 0.4'}
dependencies:
- call-bind: 1.0.2
- define-properties: 1.1.4
- es-abstract: 1.20.4
- get-intrinsic: 1.1.3
+ call-bind: 1.0.5
+ define-properties: 1.2.1
+ es-abstract: 1.22.3
+ get-intrinsic: 1.2.2
is-string: 1.0.7
dev: true
- /array-union/1.0.2:
- resolution: {integrity: sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==}
- engines: {node: '>=0.10.0'}
- dependencies:
- array-uniq: 1.0.3
- dev: true
-
- /array-union/2.1.0:
+ /array-union@2.1.0:
resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==}
engines: {node: '>=8'}
dev: true
- /array-uniq/1.0.3:
- resolution: {integrity: sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==}
- engines: {node: '>=0.10.0'}
+ /array-union@3.0.1:
+ resolution: {integrity: sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw==}
+ engines: {node: '>=12'}
dev: true
- /array-unique/0.3.2:
+ /array-unique@0.3.2:
resolution: {integrity: sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==}
engines: {node: '>=0.10.0'}
dev: true
- /array.prototype.filter/1.0.1:
- resolution: {integrity: sha512-Dk3Ty7N42Odk7PjU/Ci3zT4pLj20YvuVnneG/58ICM6bt4Ij5kZaJTVQ9TSaWaIECX2sFyz4KItkVZqHNnciqw==}
+ /array.prototype.findlastindex@1.2.3:
+ resolution: {integrity: sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==}
engines: {node: '>= 0.4'}
dependencies:
- call-bind: 1.0.2
- define-properties: 1.1.4
- es-abstract: 1.20.4
- es-array-method-boxes-properly: 1.0.0
- is-string: 1.0.7
+ call-bind: 1.0.5
+ define-properties: 1.2.1
+ es-abstract: 1.22.3
+ es-shim-unscopables: 1.0.2
+ get-intrinsic: 1.2.2
dev: true
- /array.prototype.flat/1.3.0:
- resolution: {integrity: sha512-12IUEkHsAhA4DY5s0FPgNXIdc8VRSqD9Zp78a5au9abH/SOBrsp082JOWFNTjkMozh8mqcdiKuaLGhPeYztxSw==}
+ /array.prototype.flat@1.3.2:
+ resolution: {integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==}
engines: {node: '>= 0.4'}
dependencies:
- call-bind: 1.0.2
- define-properties: 1.1.4
- es-abstract: 1.20.4
- es-shim-unscopables: 1.0.0
+ call-bind: 1.0.5
+ define-properties: 1.2.1
+ es-abstract: 1.22.3
+ es-shim-unscopables: 1.0.2
dev: true
- /array.prototype.flatmap/1.3.0:
- resolution: {integrity: sha512-PZC9/8TKAIxcWKdyeb77EzULHPrIX/tIZebLJUQOMR1OwYosT8yggdfWScfTBCDj5utONvOuPQQumYsU2ULbkg==}
+ /array.prototype.flatmap@1.3.2:
+ resolution: {integrity: sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==}
engines: {node: '>= 0.4'}
dependencies:
- call-bind: 1.0.2
- define-properties: 1.1.4
- es-abstract: 1.20.4
- es-shim-unscopables: 1.0.0
+ call-bind: 1.0.5
+ define-properties: 1.2.1
+ es-abstract: 1.22.3
+ es-shim-unscopables: 1.0.2
dev: true
- /array.prototype.map/1.0.4:
- resolution: {integrity: sha512-Qds9QnX7A0qISY7JT5WuJO0NJPE9CMlC6JzHQfhpqAAQQzufVRoeH7EzUY5GcPTx72voG8LV/5eo+b8Qi8hmhA==}
+ /array.prototype.reduce@1.0.4:
+ resolution: {integrity: sha512-WnM+AjG/DvLRLo4DDl+r+SvCzYtD2Jd9oeBYMcEaI7t3fFrHY9M53/wdLcTvmZNQ70IU6Htj0emFkZ5TS+lrdw==}
engines: {node: '>= 0.4'}
dependencies:
- call-bind: 1.0.2
- define-properties: 1.1.4
- es-abstract: 1.20.4
+ call-bind: 1.0.5
+ define-properties: 1.2.1
+ es-abstract: 1.22.3
es-array-method-boxes-properly: 1.0.0
is-string: 1.0.7
dev: true
- /array.prototype.reduce/1.0.4:
- resolution: {integrity: sha512-WnM+AjG/DvLRLo4DDl+r+SvCzYtD2Jd9oeBYMcEaI7t3fFrHY9M53/wdLcTvmZNQ70IU6Htj0emFkZ5TS+lrdw==}
+ /array.prototype.tosorted@1.1.2:
+ resolution: {integrity: sha512-HuQCHOlk1Weat5jzStICBCd83NxiIMwqDg/dHEsoefabn/hJRj5pVdWcPUSpRrwhwxZOsQassMpgN/xRYFBMIg==}
+ dependencies:
+ call-bind: 1.0.5
+ define-properties: 1.2.1
+ es-abstract: 1.22.3
+ es-shim-unscopables: 1.0.2
+ get-intrinsic: 1.2.2
+ dev: true
+
+ /arraybuffer.prototype.slice@1.0.2:
+ resolution: {integrity: sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==}
engines: {node: '>= 0.4'}
dependencies:
- call-bind: 1.0.2
- define-properties: 1.1.4
- es-abstract: 1.20.4
- es-array-method-boxes-properly: 1.0.0
- is-string: 1.0.7
+ array-buffer-byte-length: 1.0.0
+ call-bind: 1.0.5
+ define-properties: 1.2.1
+ es-abstract: 1.22.3
+ get-intrinsic: 1.2.2
+ is-array-buffer: 3.0.2
+ is-shared-array-buffer: 1.0.2
dev: true
- /arrgv/1.0.2:
+ /arrgv@1.0.2:
resolution: {integrity: sha512-a4eg4yhp7mmruZDQFqVMlxNRFGi/i1r87pt8SDHy0/I8PqSXoUTlWZRdAZo0VXgvEARcujbtTk8kiZRi1uDGRw==}
engines: {node: '>=8.0.0'}
dev: true
- /arrify/2.0.1:
- resolution: {integrity: sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==}
- engines: {node: '>=8'}
- dev: true
-
- /arrify/3.0.0:
+ /arrify@3.0.0:
resolution: {integrity: sha512-tLkvA81vQG/XqE2mjDkGQHoOINtMHtysSnemrmoGe6PydDPMRbVugqyk4A6V/WDWEfm3l+0d8anA9r8cv/5Jaw==}
engines: {node: '>=12'}
dev: true
- /asn1.js/5.4.1:
+ /asn1.js@5.4.1:
resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==}
dependencies:
bn.js: 4.12.0
@@ -8559,165 +7436,143 @@ packages:
safer-buffer: 2.1.2
dev: true
- /asn1/0.2.6:
+ /asn1@0.2.6:
resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==}
dependencies:
safer-buffer: 2.1.2
dev: true
- /assert-plus/1.0.0:
+ /assert-plus@1.0.0:
resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==}
engines: {node: '>=0.8'}
dev: true
- /assert/1.5.0:
+ /assert@1.5.0:
resolution: {integrity: sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==}
dependencies:
object-assign: 4.1.1
util: 0.10.3
dev: true
- /assertion-error/1.1.0:
+ /assertion-error@1.1.0:
resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==}
+ dev: true
- /assign-symbols/1.0.0:
+ /assign-symbols@1.0.0:
resolution: {integrity: sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==}
engines: {node: '>=0.10.0'}
dev: true
- /ast-metadata-inferer/0.7.0:
+ /ast-metadata-inferer@0.7.0:
resolution: {integrity: sha512-OkMLzd8xelb3gmnp6ToFvvsHLtS6CbagTkFQvQ+ZYFe3/AIl9iKikNR9G7pY3GfOR/2Xc222hwBjzI7HLkE76Q==}
dependencies:
'@mdn/browser-compat-data': 3.3.14
dev: true
- /ast-types-flow/0.0.7:
- resolution: {integrity: sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==}
+ /ast-types-flow@0.0.8:
+ resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==}
dev: true
- /astral-regex/2.0.0:
+ /astral-regex@2.0.0:
resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==}
engines: {node: '>=8'}
dev: true
- /async-each/1.0.3:
- resolution: {integrity: sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==}
+ /async-each@1.0.6:
+ resolution: {integrity: sha512-c646jH1avxr+aVpndVMeAfYw7wAa6idufrlN3LPA4PmKS0QEGp6PIC9nwz0WQkkvBGAMEki3pFdtxaF39J9vvg==}
+ requiresBuild: true
dev: true
optional: true
- /async-limiter/1.0.1:
+ /async-limiter@1.0.1:
resolution: {integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==}
dev: true
- /async/3.2.4:
+ /async-sema@3.1.1:
+ resolution: {integrity: sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==}
+ dev: true
+
+ /async@3.2.4:
resolution: {integrity: sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==}
dev: true
- /asynckit/0.4.0:
+ /asynciterator.prototype@1.0.0:
+ resolution: {integrity: sha512-wwHYEIS0Q80f5mosx3L/dfG5t5rjEa9Ft51GTaNt862EnpyGHpgz2RkZvLPp1oF5TnAiTohkEKVEu8pQPJI7Vg==}
+ dependencies:
+ has-symbols: 1.0.3
+ dev: true
+
+ /asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
+ dev: true
- /at-least-node/1.0.0:
+ /at-least-node@1.0.0:
resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==}
engines: {node: '>= 4.0.0'}
dev: true
- /atob/2.1.2:
+ /atob@2.1.2:
resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==}
engines: {node: '>= 4.5.0'}
hasBin: true
dev: true
- /autoprefixer/10.4.12_postcss@8.4.18:
- resolution: {integrity: sha512-WrCGV9/b97Pa+jtwf5UGaRjgQIg7OK3D06GnoYoZNcG1Xb8Gt3EfuKjlhh9i/VtT16g6PYjZ69jdJ2g8FxSC4Q==}
+ /atomic-sleep@1.0.0:
+ resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
+ engines: {node: '>=8.0.0'}
+ dev: true
+
+ /autoprefixer@10.4.14(postcss@8.4.23):
+ resolution: {integrity: sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==}
engines: {node: ^10 || ^12 || >=14}
hasBin: true
peerDependencies:
postcss: ^8.1.0
dependencies:
- browserslist: 4.21.4
- caniuse-lite: 1.0.30001425
+ browserslist: 4.21.5
+ caniuse-lite: 1.0.30001482
fraction.js: 4.2.0
normalize-range: 0.1.2
picocolors: 1.0.0
- postcss: 8.4.18
+ postcss: 8.4.23
postcss-value-parser: 4.2.0
dev: true
- /autoprefixer/9.8.8:
- resolution: {integrity: sha512-eM9d/swFopRt5gdJ7jrpCwgvEMIayITpojhkkSMRsFHYuH5bkSQ4p/9qTEHtmNudUZh22Tehu7I6CxAW0IXTKA==}
+ /autoprefixer@10.4.14(postcss@8.4.32):
+ resolution: {integrity: sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==}
+ engines: {node: ^10 || ^12 || >=14}
hasBin: true
+ peerDependencies:
+ postcss: ^8.1.0
dependencies:
- browserslist: 4.21.4
- caniuse-lite: 1.0.30001425
+ browserslist: 4.21.5
+ caniuse-lite: 1.0.30001482
+ fraction.js: 4.2.0
normalize-range: 0.1.2
- num2fraction: 1.2.2
- picocolors: 0.2.1
- postcss: 7.0.39
+ picocolors: 1.0.0
+ postcss: 8.4.32
postcss-value-parser: 4.2.0
dev: true
- /ava/4.3.3:
- resolution: {integrity: sha512-9Egq/d9R74ExrWohHeqUlexjDbgZJX5jA1Wq4KCTqc3wIfpGEK79zVy4rBtofJ9YKIxs4PzhJ8BgbW5PlAYe6w==}
- engines: {node: '>=12.22 <13 || >=14.17 <15 || >=16.4 <17 || >=18'}
+ /autoprefixer@10.4.14(postcss@8.4.33):
+ resolution: {integrity: sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==}
+ engines: {node: ^10 || ^12 || >=14}
hasBin: true
peerDependencies:
- '@ava/typescript': '*'
- peerDependenciesMeta:
- '@ava/typescript':
- optional: true
+ postcss: ^8.1.0
dependencies:
- acorn: 8.8.1
- acorn-walk: 8.2.0
- ansi-styles: 6.2.1
- arrgv: 1.0.2
- arrify: 3.0.0
- callsites: 4.0.0
- cbor: 8.1.0
- chalk: 5.1.2
- chokidar: 3.5.3
- chunkd: 2.0.1
- ci-info: 3.5.0
- ci-parallel-vars: 1.0.1
- clean-yaml-object: 0.1.0
- cli-truncate: 3.1.0
- code-excerpt: 4.0.0
- common-path-prefix: 3.0.0
- concordance: 5.0.4
- currently-unhandled: 0.4.1
- debug: 4.3.4
- del: 6.1.1
- emittery: 0.11.0
- figures: 4.0.1
- globby: 13.1.2
- ignore-by-default: 2.1.0
- indent-string: 5.0.0
- is-error: 2.2.2
- is-plain-object: 5.0.0
- is-promise: 4.0.0
- matcher: 5.0.0
- mem: 9.0.2
- ms: 2.1.3
- p-event: 5.0.1
- p-map: 5.5.0
- picomatch: 2.3.1
- pkg-conf: 4.0.0
- plur: 5.1.0
- pretty-ms: 7.0.1
- resolve-cwd: 3.0.0
- slash: 3.0.0
- stack-utils: 2.0.5
- strip-ansi: 7.0.1
- supertap: 3.0.1
- temp-dir: 2.0.0
- write-file-atomic: 4.0.2
- yargs: 17.6.0
- transitivePeerDependencies:
- - supports-color
+ browserslist: 4.21.5
+ caniuse-lite: 1.0.30001482
+ fraction.js: 4.2.0
+ normalize-range: 0.1.2
+ picocolors: 1.0.0
+ postcss: 8.4.33
+ postcss-value-parser: 4.2.0
dev: true
- /ava/4.3.3_@ava+typescript@3.0.1:
- resolution: {integrity: sha512-9Egq/d9R74ExrWohHeqUlexjDbgZJX5jA1Wq4KCTqc3wIfpGEK79zVy4rBtofJ9YKIxs4PzhJ8BgbW5PlAYe6w==}
- engines: {node: '>=12.22 <13 || >=14.17 <15 || >=16.4 <17 || >=18'}
+ /ava@6.0.1(@ava/typescript@4.1.0):
+ resolution: {integrity: sha512-9zR0wOwlcJdOWwHOKnpi0GrPRLTlxDFapGalP4rGD0oQRKxDVoucBBWvxVQ/2cPv10Hx1PkDXLJH5iUzhPn0/g==}
+ engines: {node: ^18.18 || ^20.8 || ^21}
hasBin: true
peerDependencies:
'@ava/typescript': '*'
@@ -8725,100 +7580,85 @@ packages:
'@ava/typescript':
optional: true
dependencies:
- '@ava/typescript': 3.0.1
- acorn: 8.8.1
- acorn-walk: 8.2.0
+ '@ava/typescript': 4.1.0
+ '@vercel/nft': 0.24.4
+ acorn: 8.11.2
+ acorn-walk: 8.3.1
ansi-styles: 6.2.1
arrgv: 1.0.2
arrify: 3.0.0
- callsites: 4.0.0
- cbor: 8.1.0
- chalk: 5.1.2
- chokidar: 3.5.3
+ callsites: 4.1.0
+ cbor: 9.0.1
+ chalk: 5.3.0
chunkd: 2.0.1
- ci-info: 3.5.0
+ ci-info: 4.0.0
ci-parallel-vars: 1.0.1
- clean-yaml-object: 0.1.0
- cli-truncate: 3.1.0
+ cli-truncate: 4.0.0
code-excerpt: 4.0.0
common-path-prefix: 3.0.0
concordance: 5.0.4
currently-unhandled: 0.4.1
debug: 4.3.4
- del: 6.1.1
- emittery: 0.11.0
- figures: 4.0.1
- globby: 13.1.2
+ emittery: 1.0.1
+ figures: 6.0.1
+ globby: 14.0.0
ignore-by-default: 2.1.0
indent-string: 5.0.0
- is-error: 2.2.2
is-plain-object: 5.0.0
is-promise: 4.0.0
matcher: 5.0.0
- mem: 9.0.2
+ memoize: 10.0.0
ms: 2.1.3
- p-event: 5.0.1
- p-map: 5.5.0
- picomatch: 2.3.1
- pkg-conf: 4.0.0
+ p-map: 6.0.0
+ package-config: 5.0.0
+ picomatch: 3.0.1
plur: 5.1.0
- pretty-ms: 7.0.1
+ pretty-ms: 8.0.0
resolve-cwd: 3.0.0
- slash: 3.0.0
- stack-utils: 2.0.5
- strip-ansi: 7.0.1
+ stack-utils: 2.0.6
+ strip-ansi: 7.1.0
supertap: 3.0.1
- temp-dir: 2.0.0
- write-file-atomic: 4.0.2
- yargs: 17.6.0
+ temp-dir: 3.0.0
+ write-file-atomic: 5.0.1
+ yargs: 17.7.2
transitivePeerDependencies:
+ - encoding
- supports-color
dev: true
- /aws-sign2/0.7.0:
+ /available-typed-arrays@1.0.5:
+ resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==}
+ engines: {node: '>= 0.4'}
+ dev: true
+
+ /aws-sign2@0.7.0:
resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==}
dev: true
- /aws4/1.11.0:
+ /aws4@1.11.0:
resolution: {integrity: sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==}
dev: true
- /axe-core/4.5.0:
- resolution: {integrity: sha512-4+rr8eQ7+XXS5nZrKcMO/AikHL0hVqy+lHWAnE3xdHl+aguag8SOQ6eEqLexwLNWgXIMfunGuD3ON1/6Kyet0A==}
+ /axe-core@4.7.0:
+ resolution: {integrity: sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==}
engines: {node: '>=4'}
dev: true
- /axios/0.21.4:
+ /axios@0.21.4:
resolution: {integrity: sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==}
dependencies:
follow-redirects: 1.15.2
transitivePeerDependencies:
- debug
-
- /axios/0.27.2:
- resolution: {integrity: sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==}
- dependencies:
- follow-redirects: 1.15.2
- form-data: 4.0.0
- transitivePeerDependencies:
- - debug
- dev: false
-
- /axios/1.1.3:
- resolution: {integrity: sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA==}
- dependencies:
- follow-redirects: 1.15.2
- form-data: 4.0.0
- proxy-from-env: 1.1.0
- transitivePeerDependencies:
- - debug
dev: true
- /axobject-query/2.2.0:
- resolution: {integrity: sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==}
+ /axobject-query@3.2.1:
+ resolution: {integrity: sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==}
+ dependencies:
+ dequal: 2.0.3
dev: true
- /babel-esm-plugin/0.9.0_webpack@4.46.0:
+ /babel-esm-plugin@0.9.0(webpack@4.46.0):
resolution: {integrity: sha512-OyPyLI6LUuUqNm3HNUldAkynWrLzXkhcZo4fGTsieCgHqvbCoCIMMOwJmfG9Lmp91S7WDIuUr0mvOeI8pAb/pw==}
peerDependencies:
webpack: ^4.28.4
@@ -8828,53 +7668,7 @@ packages:
webpack: 4.46.0
dev: true
- /babel-helper-builder-react-jsx/6.26.0:
- resolution: {integrity: sha512-02I9jDjnVEuGy2BR3LRm9nPRb/+Ja0pvZVLr1eI5TYAA/dB0Xoc+WBo50+aDfhGDLhlBY1+QURjn9uvcFd8gzg==}
- dependencies:
- babel-runtime: 6.26.0
- babel-types: 6.26.0
- esutils: 2.0.3
- dev: true
-
- /babel-jest/26.6.3_@babel+core@7.18.9:
- resolution: {integrity: sha512-pl4Q+GAVOHwvjrck6jKjvmGhnO3jHX/xuB9d27f+EJZ/6k+6nMuPjorrYp7s++bKKdANwzElBWnLWaObvTnaZA==}
- engines: {node: '>= 10.14.2'}
- peerDependencies:
- '@babel/core': ^7.0.0
- dependencies:
- '@babel/core': 7.18.9
- '@jest/transform': 26.6.2
- '@jest/types': 26.6.2
- '@types/babel__core': 7.1.19
- babel-plugin-istanbul: 6.1.1
- babel-preset-jest: 26.6.2_@babel+core@7.18.9
- chalk: 4.1.2
- graceful-fs: 4.2.10
- slash: 3.0.0
- transitivePeerDependencies:
- - supports-color
- dev: true
-
- /babel-jest/27.5.1_@babel+core@7.18.9:
- resolution: {integrity: sha512-cdQ5dXjGRd0IBRATiQ4mZGlGlRE8kJpjPOixdNRdT+m3UcNqmYWN6rK6nvtXYfY3D76cb8s/O1Ss8ea24PIwcg==}
- engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
- peerDependencies:
- '@babel/core': ^7.8.0
- dependencies:
- '@babel/core': 7.18.9
- '@jest/transform': 27.5.1
- '@jest/types': 27.5.1
- '@types/babel__core': 7.1.19
- babel-plugin-istanbul: 6.1.1
- babel-preset-jest: 27.5.1_@babel+core@7.18.9
- chalk: 4.1.2
- graceful-fs: 4.2.10
- slash: 3.0.0
- transitivePeerDependencies:
- - supports-color
- dev: true
-
- /babel-loader/8.2.5_7uc2ny5pnz7ums2wq2q562bf6y:
+ /babel-loader@8.2.5(@babel/core@7.18.9)(webpack@4.47.0):
resolution: {integrity: sha512-OSiFfH89LrEMiWd4pLNqGz4CwJDtbs2ZVc+iGu2HrkRfPxId9F2anQj38IxWpmRfsUY0aBZYi1EFcd3mhtRMLQ==}
engines: {node: '>= 8.9'}
peerDependencies:
@@ -8886,276 +7680,175 @@ packages:
loader-utils: 2.0.3
make-dir: 3.1.0
schema-utils: 2.7.1
- webpack: 4.46.0
+ webpack: 4.47.0
dev: true
- /babel-loader/8.2.5_@babel+core@7.18.9:
+ /babel-loader@8.2.5(@babel/core@7.22.1)(webpack@4.46.0):
resolution: {integrity: sha512-OSiFfH89LrEMiWd4pLNqGz4CwJDtbs2ZVc+iGu2HrkRfPxId9F2anQj38IxWpmRfsUY0aBZYi1EFcd3mhtRMLQ==}
engines: {node: '>= 8.9'}
peerDependencies:
'@babel/core': ^7.0.0
webpack: '>=2'
dependencies:
- '@babel/core': 7.18.9
+ '@babel/core': 7.22.1
find-cache-dir: 3.3.2
loader-utils: 2.0.3
make-dir: 3.1.0
schema-utils: 2.7.1
+ webpack: 4.46.0
dev: true
- /babel-plugin-apply-mdx-type-prop/1.6.22_@babel+core@7.12.9:
- resolution: {integrity: sha512-VefL+8o+F/DfK24lPZMtJctrCVOfgbqLAGZSkxwhazQv4VxPg3Za/i40fu22KR2m8eEda+IfSOlPLUSIiLcnCQ==}
+ /babel-merge@3.0.0(@babel/core@7.23.5):
+ resolution: {integrity: sha512-eBOBtHnzt9xvnjpYNI5HmaPp/b2vMveE5XggzqHnQeHJ8mFIBrBv6WZEVIj5jJ2uwTItkqKo9gWzEEcBxEq0yw==}
peerDependencies:
- '@babel/core': ^7.11.6
- dependencies:
- '@babel/core': 7.12.9
- '@babel/helper-plugin-utils': 7.10.4
- '@mdx-js/util': 1.6.22
- dev: true
-
- /babel-plugin-dynamic-import-node/2.3.3:
- resolution: {integrity: sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==}
- dependencies:
- object.assign: 4.1.4
- dev: true
-
- /babel-plugin-extract-import-names/1.6.22:
- resolution: {integrity: sha512-yJ9BsJaISua7d8zNT7oRG1ZLBJCIdZ4PZqmH8qa9N5AK01ifk3fnkc98AXhtzE7UkfCsEumvoQWgoYLhOnJ7jQ==}
- dependencies:
- '@babel/helper-plugin-utils': 7.10.4
- dev: true
-
- /babel-plugin-istanbul/6.1.1:
- resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==}
- engines: {node: '>=8'}
- dependencies:
- '@babel/helper-plugin-utils': 7.19.0
- '@istanbuljs/load-nyc-config': 1.1.0
- '@istanbuljs/schema': 0.1.3
- istanbul-lib-instrument: 5.2.1
- test-exclude: 6.0.0
- transitivePeerDependencies:
- - supports-color
- dev: true
-
- /babel-plugin-jest-hoist/26.6.2:
- resolution: {integrity: sha512-PO9t0697lNTmcEHH69mdtYiOIkkOlj9fySqfO3K1eCcdISevLAE0xY59VLLUj0SoiPiTX/JU2CYFpILydUa5Lw==}
- engines: {node: '>= 10.14.2'}
- dependencies:
- '@babel/template': 7.18.10
- '@babel/types': 7.19.4
- '@types/babel__core': 7.1.19
- '@types/babel__traverse': 7.18.2
- dev: true
-
- /babel-plugin-jest-hoist/27.5.1:
- resolution: {integrity: sha512-50wCwD5EMNW4aRpOwtqzyZHIewTYNxLA4nhB+09d8BIssfNfzBRhkBIHiaPv1Si226TQSvp8gxAJm2iY2qs2hQ==}
- engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
+ '@babel/core': ^7.0.0
dependencies:
- '@babel/template': 7.18.10
- '@babel/types': 7.19.4
- '@types/babel__core': 7.1.19
- '@types/babel__traverse': 7.18.2
+ '@babel/core': 7.23.5
+ deepmerge: 2.2.1
+ object.omit: 3.0.0
dev: true
- /babel-plugin-macros/3.1.0:
+ /babel-plugin-macros@3.1.0:
resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==}
engines: {node: '>=10', npm: '>=6'}
dependencies:
'@babel/runtime': 7.19.4
cosmiconfig: 7.0.1
- resolve: 1.22.1
+ resolve: 1.22.8
dev: true
- /babel-plugin-polyfill-corejs2/0.3.3_@babel+core@7.18.9:
+ /babel-plugin-polyfill-corejs2@0.3.3(@babel/core@7.22.1):
resolution: {integrity: sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/compat-data': 7.19.4
- '@babel/core': 7.18.9
- '@babel/helper-define-polyfill-provider': 0.3.3_@babel+core@7.18.9
- semver: 6.3.0
+ '@babel/compat-data': 7.23.5
+ '@babel/core': 7.22.1
+ '@babel/helper-define-polyfill-provider': 0.3.3(@babel/core@7.22.1)
+ semver: 6.3.1
transitivePeerDependencies:
- supports-color
dev: true
- /babel-plugin-polyfill-corejs2/0.3.3_@babel+core@7.19.6:
- resolution: {integrity: sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==}
+ /babel-plugin-polyfill-corejs2@0.4.6(@babel/core@7.18.9):
+ resolution: {integrity: sha512-jhHiWVZIlnPbEUKSSNb9YoWcQGdlTLq7z1GHL4AjFxaoOUMuuEVJ+Y4pAaQUGOGk93YsVCKPbqbfw3m0SM6H8Q==}
peerDependencies:
- '@babel/core': ^7.0.0-0
+ '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0
dependencies:
- '@babel/compat-data': 7.19.4
- '@babel/core': 7.19.6
- '@babel/helper-define-polyfill-provider': 0.3.3_@babel+core@7.19.6
- semver: 6.3.0
+ '@babel/compat-data': 7.23.5
+ '@babel/core': 7.18.9
+ '@babel/helper-define-polyfill-provider': 0.4.3(@babel/core@7.18.9)
+ semver: 6.3.1
transitivePeerDependencies:
- supports-color
dev: true
- /babel-plugin-polyfill-corejs3/0.1.7_@babel+core@7.18.9:
- resolution: {integrity: sha512-u+gbS9bbPhZWEeyy1oR/YaaSpod/KDT07arZHb80aTpl8H5ZBq+uN1nN9/xtX7jQyfLdPfoqI4Rue/MQSWJquw==}
+ /babel-plugin-polyfill-corejs2@0.4.6(@babel/core@7.23.5):
+ resolution: {integrity: sha512-jhHiWVZIlnPbEUKSSNb9YoWcQGdlTLq7z1GHL4AjFxaoOUMuuEVJ+Y4pAaQUGOGk93YsVCKPbqbfw3m0SM6H8Q==}
peerDependencies:
- '@babel/core': ^7.0.0-0
+ '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0
dependencies:
- '@babel/core': 7.18.9
- '@babel/helper-define-polyfill-provider': 0.1.5_@babel+core@7.18.9
- core-js-compat: 3.26.0
+ '@babel/compat-data': 7.23.5
+ '@babel/core': 7.23.5
+ '@babel/helper-define-polyfill-provider': 0.4.3(@babel/core@7.23.5)
+ semver: 6.3.1
transitivePeerDependencies:
- supports-color
dev: true
- /babel-plugin-polyfill-corejs3/0.5.3_@babel+core@7.18.9:
- resolution: {integrity: sha512-zKsXDh0XjnrUEW0mxIHLfjBfnXSMr5Q/goMe/fxpQnLm07mcOZiIZHBNWCMx60HmdvjxfXcalac0tfFg0wqxyw==}
+ /babel-plugin-polyfill-corejs3@0.6.0(@babel/core@7.22.1):
+ resolution: {integrity: sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.18.9
- '@babel/helper-define-polyfill-provider': 0.3.3_@babel+core@7.18.9
- core-js-compat: 3.26.0
+ '@babel/core': 7.22.1
+ '@babel/helper-define-polyfill-provider': 0.3.3(@babel/core@7.22.1)
+ core-js-compat: 3.33.3
transitivePeerDependencies:
- supports-color
dev: true
- /babel-plugin-polyfill-corejs3/0.6.0_@babel+core@7.18.9:
- resolution: {integrity: sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==}
+ /babel-plugin-polyfill-corejs3@0.8.6(@babel/core@7.18.9):
+ resolution: {integrity: sha512-leDIc4l4tUgU7str5BWLS2h8q2N4Nf6lGZP6UrNDxdtfF2g69eJ5L0H7S8A5Ln/arfFAfHor5InAdZuIOwZdgQ==}
peerDependencies:
- '@babel/core': ^7.0.0-0
+ '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0
dependencies:
'@babel/core': 7.18.9
- '@babel/helper-define-polyfill-provider': 0.3.3_@babel+core@7.18.9
- core-js-compat: 3.26.0
+ '@babel/helper-define-polyfill-provider': 0.4.3(@babel/core@7.18.9)
+ core-js-compat: 3.33.3
transitivePeerDependencies:
- supports-color
dev: true
- /babel-plugin-polyfill-corejs3/0.6.0_@babel+core@7.19.6:
- resolution: {integrity: sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==}
+ /babel-plugin-polyfill-corejs3@0.8.6(@babel/core@7.23.5):
+ resolution: {integrity: sha512-leDIc4l4tUgU7str5BWLS2h8q2N4Nf6lGZP6UrNDxdtfF2g69eJ5L0H7S8A5Ln/arfFAfHor5InAdZuIOwZdgQ==}
peerDependencies:
- '@babel/core': ^7.0.0-0
+ '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-define-polyfill-provider': 0.3.3_@babel+core@7.19.6
- core-js-compat: 3.26.0
+ '@babel/core': 7.23.5
+ '@babel/helper-define-polyfill-provider': 0.4.3(@babel/core@7.23.5)
+ core-js-compat: 3.33.3
transitivePeerDependencies:
- supports-color
dev: true
- /babel-plugin-polyfill-regenerator/0.3.1_@babel+core@7.18.9:
- resolution: {integrity: sha512-Y2B06tvgHYt1x0yz17jGkGeeMr5FeKUu+ASJ+N6nB5lQ8Dapfg42i0OVrf8PNGJ3zKL4A23snMi1IRwrqqND7A==}
+ /babel-plugin-polyfill-regenerator@0.4.1(@babel/core@7.22.1):
+ resolution: {integrity: sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/core': 7.18.9
- '@babel/helper-define-polyfill-provider': 0.3.3_@babel+core@7.18.9
+ '@babel/core': 7.22.1
+ '@babel/helper-define-polyfill-provider': 0.3.3(@babel/core@7.22.1)
transitivePeerDependencies:
- supports-color
dev: true
- /babel-plugin-polyfill-regenerator/0.4.1_@babel+core@7.18.9:
- resolution: {integrity: sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==}
+ /babel-plugin-polyfill-regenerator@0.5.3(@babel/core@7.18.9):
+ resolution: {integrity: sha512-8sHeDOmXC8csczMrYEOf0UTNa4yE2SxV5JGeT/LP1n0OYVDUUFPxG9vdk2AlDlIit4t+Kf0xCtpgXPBwnn/9pw==}
peerDependencies:
- '@babel/core': ^7.0.0-0
+ '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0
dependencies:
'@babel/core': 7.18.9
- '@babel/helper-define-polyfill-provider': 0.3.3_@babel+core@7.18.9
+ '@babel/helper-define-polyfill-provider': 0.4.3(@babel/core@7.18.9)
transitivePeerDependencies:
- supports-color
dev: true
- /babel-plugin-polyfill-regenerator/0.4.1_@babel+core@7.19.6:
- resolution: {integrity: sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==}
+ /babel-plugin-polyfill-regenerator@0.5.3(@babel/core@7.23.5):
+ resolution: {integrity: sha512-8sHeDOmXC8csczMrYEOf0UTNa4yE2SxV5JGeT/LP1n0OYVDUUFPxG9vdk2AlDlIit4t+Kf0xCtpgXPBwnn/9pw==}
peerDependencies:
- '@babel/core': ^7.0.0-0
+ '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0
dependencies:
- '@babel/core': 7.19.6
- '@babel/helper-define-polyfill-provider': 0.3.3_@babel+core@7.19.6
+ '@babel/core': 7.23.5
+ '@babel/helper-define-polyfill-provider': 0.4.3(@babel/core@7.23.5)
transitivePeerDependencies:
- supports-color
dev: true
- /babel-plugin-syntax-jsx/6.18.0:
- resolution: {integrity: sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==}
- dev: true
-
- /babel-plugin-transform-react-jsx/6.24.1:
- resolution: {integrity: sha512-s+q/Y2u2OgDPHRuod3t6zyLoV8pUHc64i/O7ZNgIOEdYTq+ChPeybcKBi/xk9VI60VriILzFPW+dUxAEbTxh2w==}
- dependencies:
- babel-helper-builder-react-jsx: 6.26.0
- babel-plugin-syntax-jsx: 6.18.0
- babel-runtime: 6.26.0
- dev: true
-
- /babel-plugin-transform-react-remove-prop-types/0.4.24:
+ /babel-plugin-transform-react-remove-prop-types@0.4.24:
resolution: {integrity: sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA==}
dev: true
- /babel-preset-current-node-syntax/1.0.1_@babel+core@7.18.9:
- resolution: {integrity: sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==}
- peerDependencies:
- '@babel/core': ^7.0.0
- dependencies:
- '@babel/core': 7.18.9
- '@babel/plugin-syntax-async-generators': 7.8.4_@babel+core@7.18.9
- '@babel/plugin-syntax-bigint': 7.8.3_@babel+core@7.18.9
- '@babel/plugin-syntax-class-properties': 7.12.13_@babel+core@7.18.9
- '@babel/plugin-syntax-import-meta': 7.10.4_@babel+core@7.18.9
- '@babel/plugin-syntax-json-strings': 7.8.3_@babel+core@7.18.9
- '@babel/plugin-syntax-logical-assignment-operators': 7.10.4_@babel+core@7.18.9
- '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3_@babel+core@7.18.9
- '@babel/plugin-syntax-numeric-separator': 7.10.4_@babel+core@7.18.9
- '@babel/plugin-syntax-object-rest-spread': 7.8.3_@babel+core@7.18.9
- '@babel/plugin-syntax-optional-catch-binding': 7.8.3_@babel+core@7.18.9
- '@babel/plugin-syntax-optional-chaining': 7.8.3_@babel+core@7.18.9
- '@babel/plugin-syntax-top-level-await': 7.14.5_@babel+core@7.18.9
- dev: true
-
- /babel-preset-jest/26.6.2_@babel+core@7.18.9:
- resolution: {integrity: sha512-YvdtlVm9t3k777c5NPQIv6cxFFFapys25HiUmuSgHwIZhfifweR5c5Sf5nwE3MAbfu327CYSvps8Yx6ANLyleQ==}
- engines: {node: '>= 10.14.2'}
- peerDependencies:
- '@babel/core': ^7.0.0
- dependencies:
- '@babel/core': 7.18.9
- babel-plugin-jest-hoist: 26.6.2
- babel-preset-current-node-syntax: 1.0.1_@babel+core@7.18.9
- dev: true
+ /balanced-match@1.0.2:
+ resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
- /babel-preset-jest/27.5.1_@babel+core@7.18.9:
- resolution: {integrity: sha512-Nptf2FzlPCWYuJg41HBqXVT8ym6bXOevuCTbhxlUpjwtysGaIWFvDEjp4y+G7fl13FgOdjs7P/DmErqH7da0Ag==}
- engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
+ /base64-inline-loader@1.1.1(webpack@4.47.0):
+ resolution: {integrity: sha512-v/bHvXQ8sW28t9ZwBsFGgyqZw2bpT49/dtPTtlmixoSM/s9wnOngOKFVQLRH8BfMTy6jTl5m5CdvqpZt8y5d6g==}
+ engines: {node: '>=6.2', npm: '>=3.8'}
peerDependencies:
- '@babel/core': ^7.0.0
- dependencies:
- '@babel/core': 7.18.9
- babel-plugin-jest-hoist: 27.5.1
- babel-preset-current-node-syntax: 1.0.1_@babel+core@7.18.9
- dev: true
-
- /babel-runtime/6.26.0:
- resolution: {integrity: sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==}
- dependencies:
- core-js: 2.6.12
- regenerator-runtime: 0.11.1
- dev: true
-
- /babel-types/6.26.0:
- resolution: {integrity: sha512-zhe3V/26rCWsEZK8kZN+HaQj5yQ1CilTObixFzKW1UWjqG7618Twz6YEsCnjfg5gBcJh02DrpCkS9h98ZqDY+g==}
+ webpack: ^4.x
dependencies:
- babel-runtime: 6.26.0
- esutils: 2.0.3
- lodash: 4.17.21
- to-fast-properties: 1.0.3
- dev: true
-
- /bail/1.0.5:
- resolution: {integrity: sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==}
+ file-loader: 1.1.11(webpack@4.47.0)
+ loader-utils: 1.4.0
+ mime-types: 2.1.35
+ webpack: 4.47.0
dev: true
- /balanced-match/1.0.2:
- resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
+ /base64-js@1.5.1:
+ resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
+ requiresBuild: true
- /base/0.11.2:
+ /base@0.11.2:
resolution: {integrity: sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==}
engines: {node: '>=0.10.0'}
dependencies:
@@ -9168,98 +7861,80 @@ packages:
pascalcase: 0.1.1
dev: true
- /base64-inline-loader/1.1.1:
- resolution: {integrity: sha512-v/bHvXQ8sW28t9ZwBsFGgyqZw2bpT49/dtPTtlmixoSM/s9wnOngOKFVQLRH8BfMTy6jTl5m5CdvqpZt8y5d6g==}
- engines: {node: '>=6.2', npm: '>=3.8'}
- peerDependencies:
- webpack: ^4.x
- dependencies:
- file-loader: 1.1.11
- loader-utils: 1.4.0
- mime-types: 2.1.35
- dev: true
-
- /base64-js/1.5.1:
- resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
+ /batch@0.6.1:
+ resolution: {integrity: sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==}
dev: true
- /batch-processor/1.0.0:
- resolution: {integrity: sha1-dclcMrdI4IUNEMKxaPa9vpiRrOg=}
- dev: true
-
- /batch/0.6.1:
- resolution: {integrity: sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=}
- dev: true
-
- /bcrypt-pbkdf/1.0.2:
+ /bcrypt-pbkdf@1.0.2:
resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==}
dependencies:
tweetnacl: 0.14.5
dev: true
- /better-opn/2.1.1:
- resolution: {integrity: sha512-kIPXZS5qwyKiX/HcRvDYfmBQUa8XP17I0mYZZ0y4UhpYOSvtsLHDYqmomS+Mj20aDvD3knEiQ0ecQy2nhio3yA==}
- engines: {node: '>8.0.0'}
+ /better-sqlite3@9.4.0:
+ resolution: {integrity: sha512-5kynxekMxSjCMiFyUBLHggFcJkCmiZi6fUkiGz/B5GZOvdRWQJD0klqCx5/Y+bm2AKP7I/DHbSFx26AxjruWNg==}
+ requiresBuild: true
dependencies:
- open: 7.4.2
- dev: true
+ bindings: 1.5.0
+ prebuild-install: 7.1.1
+ dev: false
+ optional: true
- /big-integer/1.6.51:
- resolution: {integrity: sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==}
+ /big-integer@1.6.52:
+ resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==}
engines: {node: '>=0.6'}
+ dev: false
- /big.js/3.2.0:
+ /big.js@3.2.0:
resolution: {integrity: sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==}
dev: true
- /big.js/5.2.2:
+ /big.js@5.2.2:
resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==}
dev: true
- /binary-extensions/1.13.1:
+ /binary-extensions@1.13.1:
resolution: {integrity: sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==}
engines: {node: '>=0.10.0'}
+ requiresBuild: true
dev: true
optional: true
- /binary-extensions/2.2.0:
+ /binary-extensions@2.2.0:
resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
engines: {node: '>=8'}
- dev: true
- /bindings/1.5.0:
+ /bindings@1.5.0:
resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
requiresBuild: true
dependencies:
file-uri-to-path: 1.0.0
- dev: true
- optional: true
- /bl/4.1.0:
+ /bl@4.1.0:
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
+ requiresBuild: true
dependencies:
buffer: 5.7.1
inherits: 2.0.4
readable-stream: 3.6.0
- dev: true
- /bluebird/3.7.2:
+ /bluebird@3.7.2:
resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==}
dev: true
- /blueimp-md5/2.19.0:
+ /blueimp-md5@2.19.0:
resolution: {integrity: sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==}
dev: true
- /bn.js/4.12.0:
+ /bn.js@4.12.0:
resolution: {integrity: sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==}
dev: true
- /bn.js/5.2.1:
+ /bn.js@5.2.1:
resolution: {integrity: sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==}
dev: true
- /body-parser/1.20.1:
+ /body-parser@1.20.1:
resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==}
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
dependencies:
@@ -9279,7 +7954,7 @@ packages:
- supports-color
dev: true
- /bonjour-service/1.0.14:
+ /bonjour-service@1.0.14:
resolution: {integrity: sha512-HIMbgLnk1Vqvs6B4Wq5ep7mxvj9sGz5d1JJyDNSGNIdA/w2MCz6GTjWTdjqOJV1bEPj+6IkxDvWNFKEBxNt4kQ==}
dependencies:
array-flatten: 2.1.2
@@ -9288,11 +7963,11 @@ packages:
multicast-dns: 7.2.5
dev: true
- /boolbase/1.0.0:
+ /boolbase@1.0.0:
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
dev: true
- /boxen/5.1.2:
+ /boxen@5.1.2:
resolution: {integrity: sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==}
engines: {node: '>=10'}
dependencies:
@@ -9306,26 +7981,32 @@ packages:
wrap-ansi: 7.0.0
dev: true
- /bplist-parser/0.1.1:
- resolution: {integrity: sha512-2AEM0FXy8ZxVLBuqX0hqt1gDwcnz2zygEkQ6zaD5Wko/sB9paUNwlpawrFtKeHUAQUOzjVy9AO4oeonqIHKA9Q==}
+ /boxen@7.1.1:
+ resolution: {integrity: sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog==}
+ engines: {node: '>=14.16'}
dependencies:
- big-integer: 1.6.51
+ ansi-align: 3.0.1
+ camelcase: 7.0.1
+ chalk: 5.3.0
+ cli-boxes: 3.0.0
+ string-width: 5.1.2
+ type-fest: 2.19.0
+ widest-line: 4.0.1
+ wrap-ansi: 8.1.0
dev: true
- optional: true
- /brace-expansion/1.1.11:
+ /brace-expansion@1.1.11:
resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
dependencies:
balanced-match: 1.0.2
concat-map: 0.0.1
- /brace-expansion/2.0.1:
+ /brace-expansion@2.0.1:
resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
dependencies:
balanced-match: 1.0.2
- dev: true
- /braces/2.3.2:
+ /braces@2.3.2:
resolution: {integrity: sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==}
engines: {node: '>=0.10.0'}
dependencies:
@@ -9343,26 +8024,25 @@ packages:
- supports-color
dev: true
- /braces/3.0.2:
+ /braces@3.0.2:
resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==}
engines: {node: '>=8'}
dependencies:
fill-range: 7.0.1
- dev: true
- /brorand/1.1.0:
+ /brorand@1.1.0:
resolution: {integrity: sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==}
dev: true
- /browser-process-hrtime/1.0.0:
+ /browser-process-hrtime@1.0.0:
resolution: {integrity: sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==}
dev: true
- /browser-stdout/1.3.1:
+ /browser-stdout@1.3.1:
resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==}
dev: true
- /browserify-aes/1.2.0:
+ /browserify-aes@1.2.0:
resolution: {integrity: sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==}
dependencies:
buffer-xor: 1.0.3
@@ -9373,7 +8053,7 @@ packages:
safe-buffer: 5.2.1
dev: true
- /browserify-cipher/1.0.1:
+ /browserify-cipher@1.0.1:
resolution: {integrity: sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==}
dependencies:
browserify-aes: 1.2.0
@@ -9381,23 +8061,23 @@ packages:
evp_bytestokey: 1.0.3
dev: true
- /browserify-des/1.0.2:
+ /browserify-des@1.0.2:
resolution: {integrity: sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==}
dependencies:
cipher-base: 1.0.4
- des.js: 1.0.1
+ des.js: 1.1.0
inherits: 2.0.4
safe-buffer: 5.2.1
dev: true
- /browserify-rsa/4.1.0:
+ /browserify-rsa@4.1.0:
resolution: {integrity: sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==}
dependencies:
bn.js: 5.2.1
randombytes: 2.1.0
dev: true
- /browserify-sign/4.2.1:
+ /browserify-sign@4.2.1:
resolution: {integrity: sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==}
dependencies:
bn.js: 5.2.1
@@ -9407,42 +8087,63 @@ packages:
elliptic: 6.5.4
inherits: 2.0.4
parse-asn1: 5.1.6
- readable-stream: 3.6.0
+ readable-stream: 3.6.2
safe-buffer: 5.2.1
dev: true
- /browserify-zlib/0.2.0:
+ /browserify-zlib@0.2.0:
resolution: {integrity: sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==}
dependencies:
pako: 1.0.11
dev: true
- /browserslist/4.21.4:
+ /browserslist@4.21.4:
resolution: {integrity: sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
- hasBin: true
dependencies:
- caniuse-lite: 1.0.30001425
+ caniuse-lite: 1.0.30001570
+ electron-to-chromium: 1.4.597
+ node-releases: 2.0.13
+ update-browserslist-db: 1.0.13(browserslist@4.21.4)
+ dev: true
+
+ /browserslist@4.21.5:
+ resolution: {integrity: sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==}
+ engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
+ dependencies:
+ caniuse-lite: 1.0.30001570
electron-to-chromium: 1.4.284
- node-releases: 2.0.6
- update-browserslist-db: 1.0.10_browserslist@4.21.4
+ node-releases: 2.0.10
+ update-browserslist-db: 1.0.10(browserslist@4.21.5)
dev: true
- /bser/2.1.1:
- resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==}
+ /browserslist@4.22.2:
+ resolution: {integrity: sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==}
+ engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
+ hasBin: true
dependencies:
- node-int64: 0.4.0
+ caniuse-lite: 1.0.30001570
+ electron-to-chromium: 1.4.613
+ node-releases: 2.0.14
+ update-browserslist-db: 1.0.13(browserslist@4.22.2)
+
+ /buffer-crc32@0.2.13:
+ resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
+ dev: true
+
+ /buffer-equal-constant-time@1.0.1:
+ resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
dev: true
- /buffer-from/1.1.2:
+ /buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
dev: true
- /buffer-xor/1.0.3:
+ /buffer-xor@1.0.3:
resolution: {integrity: sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==}
dev: true
- /buffer/4.9.2:
+ /buffer@4.9.2:
resolution: {integrity: sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==}
dependencies:
base64-js: 1.5.1
@@ -9450,116 +8151,134 @@ packages:
isarray: 1.0.0
dev: true
- /buffer/5.7.1:
+ /buffer@5.7.1:
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
+ requiresBuild: true
+ dependencies:
+ base64-js: 1.5.1
+ ieee754: 1.2.1
+
+ /buffer@6.0.3:
+ resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
dependencies:
base64-js: 1.5.1
ieee754: 1.2.1
dev: true
- /builtin-modules/3.3.0:
+ /builtin-modules@3.3.0:
resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==}
engines: {node: '>=6'}
dev: true
- /builtin-status-codes/3.0.0:
+ /builtin-status-codes@3.0.0:
resolution: {integrity: sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==}
dev: true
- /builtins/5.0.1:
+ /builtins@5.0.1:
resolution: {integrity: sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==}
dependencies:
- semver: 7.3.8
+ semver: 7.5.4
dev: true
- /bulma-checkbox/1.2.1:
+ /bulma-checkbox@1.2.1:
resolution: {integrity: sha512-Ad7kSzwYwHLYyow92IJPz9jgolDDo5ivlFdSBe7W4LR9WnLt/Gd2iE07m3uhoU/g37oIZcMHNC33ZxJKqAuSzQ==}
dependencies:
bulma: 0.9.4
dev: true
- /bulma-radio/1.2.0:
+ /bulma-radio@1.2.0:
resolution: {integrity: sha512-rIzqALGakpKf9Eju4sGMt2Pwnn7X+AdYh6itjsCxLCJ/Ext4Cdd/M7uevQlXDy0MSwrQBMBLR8buSToBCuI+zA==}
dependencies:
bulma: 0.9.4
dev: true
- /bulma-responsive-tables/1.2.5:
+ /bulma-responsive-tables@1.2.5:
resolution: {integrity: sha512-8/qYiv21cJnGYMkjIF52iKCV/B6XIswu58Vwi3/TS+wLavvA7OFXhBy0quxOnqPNqnovHly2dTCyVCqHLJU7Sg==}
dependencies:
bulma: 0.9.4
dev: true
- /bulma-switch-control/1.2.2:
+ /bulma-switch-control@1.2.2:
resolution: {integrity: sha512-1eHlga1Z4RBRU6DIxNiwb6+I9n9vDkj9/MmwS4pL68P7STE1vbwRutxh9oFeFWuxLXGNLILJEXJXiwyEjT9upw==}
dependencies:
bulma: 0.9.4
dev: true
- /bulma-timeline/3.0.5:
+ /bulma-timeline@3.0.5:
resolution: {integrity: sha512-gBwdx7PmAEZ/+5zSn/ZBJBqkenT8wi+9nlD0uP8lXa3rGcbhG+fp8Oz98+3O10LQPlUEdKPYEUxtQ55okRfhTQ==}
dev: true
- /bulma-upload-control/1.2.0:
+ /bulma-upload-control@1.2.0:
resolution: {integrity: sha512-2raueVPVoG3KjHH+7Aok44nGSPIl76qzdkLKX/ziHAOwbiXBrlEYHXca8Hk0UDa0KElLiPT6Eb2Cvz+8FFUwBw==}
dependencies:
bulma: 0.9.4
dev: true
- /bulma/0.9.4:
+ /bulma@0.9.4:
resolution: {integrity: sha512-86FlT5+1GrsgKbPLRRY7cGDg8fsJiP/jzTqXXVqiUZZ2aZT8uemEOHlU1CDU+TxklPEZ11HZNNWclRBBecP4CQ==}
dev: true
- /bytes/3.0.0:
+ /bunyan@1.8.15:
+ resolution: {integrity: sha512-0tECWShh6wUysgucJcBAoYegf3JJoZWibxdqhTm7OHPeT42qdjkZ29QCMcKwbgU1kiH+auSIasNRXMLWXafXig==}
+ engines: {'0': node >=0.10.0}
+ hasBin: true
+ optionalDependencies:
+ dtrace-provider: 0.8.8
+ moment: 2.30.1
+ mv: 2.1.1
+ safe-json-stringify: 1.2.0
+ dev: true
+
+ /bytes@3.0.0:
resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==}
engines: {node: '>= 0.8'}
dev: true
- /bytes/3.1.2:
+ /bytes@3.1.2:
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
engines: {node: '>= 0.8'}
dev: true
- /c8/7.12.0:
- resolution: {integrity: sha512-CtgQrHOkyxr5koX1wEUmN/5cfDa2ckbHRA4Gy5LAL0zaCFtVWJS5++n+w4/sr2GWGerBxgTjpKeDclk/Qk6W/A==}
- engines: {node: '>=10.12.0'}
+ /c8@8.0.1:
+ resolution: {integrity: sha512-EINpopxZNH1mETuI0DzRA4MZpAUH+IFiRhnmFD3vFr3vdrgxqi3VfE3KL0AIL+zDq8rC9bZqwM/VDmmoe04y7w==}
+ engines: {node: '>=12'}
hasBin: true
dependencies:
'@bcoe/v8-coverage': 0.2.3
'@istanbuljs/schema': 0.1.3
find-up: 5.0.0
foreground-child: 2.0.0
- istanbul-lib-coverage: 3.2.0
- istanbul-lib-report: 3.0.0
- istanbul-reports: 3.1.5
+ istanbul-lib-coverage: 3.2.2
+ istanbul-lib-report: 3.0.1
+ istanbul-reports: 3.1.6
rimraf: 3.0.2
test-exclude: 6.0.0
- v8-to-istanbul: 9.0.1
- yargs: 16.2.0
- yargs-parser: 20.2.9
+ v8-to-istanbul: 9.2.0
+ yargs: 17.7.2
+ yargs-parser: 21.1.1
dev: true
- /cacache/12.0.4:
+ /cacache@12.0.4:
resolution: {integrity: sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ==}
dependencies:
bluebird: 3.7.2
chownr: 1.1.4
figgy-pudding: 3.5.2
glob: 7.2.3
- graceful-fs: 4.2.10
+ graceful-fs: 4.2.11
infer-owner: 1.0.4
lru-cache: 5.1.1
mississippi: 3.0.0
mkdirp: 0.5.6
move-concurrently: 1.0.1
- promise-inflight: 1.0.1_bluebird@3.7.2
+ promise-inflight: 1.0.1(bluebird@3.7.2)
rimraf: 2.7.1
ssri: 6.0.2
unique-filename: 1.1.1
y18n: 4.0.3
dev: true
- /cacache/15.3.0:
+ /cacache@15.3.0:
resolution: {integrity: sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==}
engines: {node: '>= 10'}
dependencies:
@@ -9570,13 +8289,13 @@ packages:
glob: 7.2.3
infer-owner: 1.0.4
lru-cache: 6.0.0
- minipass: 3.3.4
+ minipass: 3.3.6
minipass-collect: 1.0.2
minipass-flush: 1.0.5
minipass-pipeline: 1.2.4
mkdirp: 1.0.4
p-map: 4.0.0
- promise-inflight: 1.0.1
+ promise-inflight: 1.0.1(bluebird@3.7.2)
rimraf: 3.0.2
ssri: 8.0.1
tar: 6.1.11
@@ -9585,7 +8304,7 @@ packages:
- bluebird
dev: true
- /cache-base/1.0.1:
+ /cache-base@1.0.1:
resolution: {integrity: sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==}
engines: {node: '>=0.10.0'}
dependencies:
@@ -9600,7 +8319,25 @@ packages:
unset-value: 1.0.0
dev: true
- /cacheable-request/6.1.0:
+ /cacheable-lookup@7.0.0:
+ resolution: {integrity: sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==}
+ engines: {node: '>=14.16'}
+ dev: true
+
+ /cacheable-request@10.2.14:
+ resolution: {integrity: sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==}
+ engines: {node: '>=14.16'}
+ dependencies:
+ '@types/http-cache-semantics': 4.0.4
+ get-stream: 6.0.1
+ http-cache-semantics: 4.1.1
+ keyv: 4.5.4
+ mimic-response: 4.0.0
+ normalize-url: 8.0.0
+ responselike: 3.0.0
+ dev: true
+
+ /cacheable-request@6.1.0:
resolution: {integrity: sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==}
engines: {node: '>=8'}
dependencies:
@@ -9613,7 +8350,7 @@ packages:
responselike: 1.0.2
dev: true
- /caching-transform/4.0.0:
+ /caching-transform@4.0.0:
resolution: {integrity: sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==}
engines: {node: '>=8'}
dependencies:
@@ -9623,131 +8360,104 @@ packages:
write-file-atomic: 3.0.3
dev: true
- /call-bind/1.0.2:
- resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==}
+ /call-bind@1.0.5:
+ resolution: {integrity: sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==}
dependencies:
- function-bind: 1.1.1
- get-intrinsic: 1.1.3
+ function-bind: 1.1.2
+ get-intrinsic: 1.2.2
+ set-function-length: 1.1.1
dev: true
- /call-me-maybe/1.0.1:
- resolution: {integrity: sha512-wCyFsDQkKPwwF8BDwOiWNx/9K45L/hvggQiDbve+viMNMQnWhrlYIuBk09offfwCRtCO9P6XwUttufzU11WCVw==}
- dev: true
-
- /caller-callsite/2.0.0:
+ /caller-callsite@2.0.0:
resolution: {integrity: sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ==}
engines: {node: '>=4'}
dependencies:
callsites: 2.0.0
dev: true
- /caller-path/2.0.0:
+ /caller-path@2.0.0:
resolution: {integrity: sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A==}
engines: {node: '>=4'}
dependencies:
caller-callsite: 2.0.0
dev: true
- /callsites/2.0.0:
+ /callsites@2.0.0:
resolution: {integrity: sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ==}
engines: {node: '>=4'}
dev: true
- /callsites/3.1.0:
+ /callsites@3.1.0:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'}
dev: true
- /callsites/4.0.0:
- resolution: {integrity: sha512-y3jRROutgpKdz5vzEhWM34TidDU8vkJppF8dszITeb1PQmSqV3DTxyV8G/lyO/DNvtE1YTedehmw9MPZsCBHxQ==}
+ /callsites@4.1.0:
+ resolution: {integrity: sha512-aBMbD1Xxay75ViYezwT40aQONfr+pSXTHwNKvIXhXD6+LY3F1dLIcceoC5OZKBVHbXcysz1hL9D2w0JJIMXpUw==}
engines: {node: '>=12.20'}
dev: true
- /camel-case/3.0.0:
+ /camel-case@3.0.0:
resolution: {integrity: sha512-+MbKztAYHXPr1jNTSKQF52VpcFjwY5RkR7fxksV8Doo4KAYc5Fl4UJRgthBbTmEx8C54DqahhbLJkDwjI3PI/w==}
dependencies:
no-case: 2.3.2
upper-case: 1.1.3
dev: true
- /camel-case/4.1.2:
+ /camel-case@4.1.2:
resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==}
dependencies:
pascal-case: 3.1.2
- tslib: 2.4.1
+ tslib: 2.6.2
dev: true
- /camelcase-css/2.0.1:
+ /camelcase-css@2.0.1:
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
engines: {node: '>= 6'}
- dev: true
-
- /camelcase-keys/2.1.0:
- resolution: {integrity: sha512-bA/Z/DERHKqoEOrp+qeGKw1QlvEQkGZSc0XaY6VnTxZr+Kv1G5zFwttpjv8qxZ/sBPT4nthwZaAcsAZTJlSKXQ==}
- engines: {node: '>=0.10.0'}
- dependencies:
- camelcase: 2.1.1
- map-obj: 1.0.1
- dev: true
- optional: true
- /camelcase/2.1.1:
- resolution: {integrity: sha512-DLIsRzJVBQu72meAKPkWQOLcujdXT32hwdfnkI1frSiSRMK1MofjKHf+MEx0SB6fjEFXL8fBDv1dKymBlOp4Qw==}
- engines: {node: '>=0.10.0'}
- dev: true
- optional: true
-
- /camelcase/5.3.1:
+ /camelcase@5.3.1:
resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
engines: {node: '>=6'}
dev: true
- /camelcase/6.3.0:
+ /camelcase@6.3.0:
resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==}
engines: {node: '>=10'}
dev: true
- /caniuse-api/3.0.0:
+ /camelcase@7.0.1:
+ resolution: {integrity: sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==}
+ engines: {node: '>=14.16'}
+ dev: true
+
+ /caniuse-api@3.0.0:
resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==}
dependencies:
- browserslist: 4.21.4
- caniuse-lite: 1.0.30001425
+ browserslist: 4.22.2
+ caniuse-lite: 1.0.30001570
lodash.memoize: 4.1.2
lodash.uniq: 4.5.0
dev: true
- /caniuse-lite/1.0.30001425:
- resolution: {integrity: sha512-/pzFv0OmNG6W0ym80P3NtapU0QEiDS3VuYAZMGoLLqiC7f6FJFe1MjpQDREGApeenD9wloeytmVDj+JLXPC6qw==}
+ /caniuse-lite@1.0.30001482:
+ resolution: {integrity: sha512-F1ZInsg53cegyjroxLNW9DmrEQ1SuGRTO1QlpA0o2/6OpQ0gFeDRoq1yFmnr8Sakn9qwwt9DmbxHB6w167OSuQ==}
dev: true
- /capture-exit/2.0.0:
- resolution: {integrity: sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g==}
- engines: {node: 6.* || 8.* || >= 10.*}
- dependencies:
- rsvp: 4.8.5
- dev: true
+ /caniuse-lite@1.0.30001570:
+ resolution: {integrity: sha512-+3e0ASu4sw1SWaoCtvPeyXp+5PsjigkSt8OXZbF9StH5pQWbxEjLAZE3n8Aup5udop1uRiKA7a4utUk/uoSpUw==}
- /case-sensitive-paths-webpack-plugin/2.4.0:
- resolution: {integrity: sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw==}
- engines: {node: '>=4'}
- dev: true
-
- /caseless/0.12.0:
+ /caseless@0.12.0:
resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==}
dev: true
- /cbor/8.1.0:
- resolution: {integrity: sha512-DwGjNW9omn6EwP70aXsn7FQJx5kO12tX0bZkaTjzdVFM6/7nhA4t0EENocKGx6D2Bch9PE2KzCUf5SceBdeijg==}
- engines: {node: '>=12.19'}
+ /cbor@9.0.1:
+ resolution: {integrity: sha512-/TQOWyamDxvVIv+DY9cOLNuABkoyz8K/F3QE56539pGVYohx0+MEA1f4lChFTX79dBTBS7R1PF6ovH7G+VtBfQ==}
+ engines: {node: '>=16'}
dependencies:
nofilter: 3.1.0
dev: true
- /ccount/1.1.0:
- resolution: {integrity: sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg==}
- dev: true
-
- /chai/4.3.6:
+ /chai@4.3.6:
resolution: {integrity: sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==}
engines: {node: '>=4'}
dependencies:
@@ -9758,8 +8468,9 @@ packages:
loupe: 2.3.4
pathval: 1.1.1
type-detect: 4.0.8
+ dev: true
- /chalk/0.4.0:
+ /chalk@0.4.0:
resolution: {integrity: sha512-sQfYDlfv2DGVtjdoQqxS0cEZDroyG8h6TamA6rvxwlrU5BaSLDx9xhatBYl2pxZ7gmpNaPFVwBtdGdu5rQ+tYQ==}
engines: {node: '>=0.8.0'}
dependencies:
@@ -9768,7 +8479,7 @@ packages:
strip-ansi: 0.1.1
dev: true
- /chalk/2.4.1:
+ /chalk@2.4.1:
resolution: {integrity: sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==}
engines: {node: '>=4'}
dependencies:
@@ -9777,16 +8488,15 @@ packages:
supports-color: 5.5.0
dev: true
- /chalk/2.4.2:
+ /chalk@2.4.2:
resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
engines: {node: '>=4'}
dependencies:
ansi-styles: 3.2.1
escape-string-regexp: 1.0.5
supports-color: 5.5.0
- dev: true
- /chalk/3.0.0:
+ /chalk@3.0.0:
resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==}
engines: {node: '>=8'}
dependencies:
@@ -9794,15 +8504,7 @@ packages:
supports-color: 7.2.0
dev: true
- /chalk/4.1.0:
- resolution: {integrity: sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==}
- engines: {node: '>=10'}
- dependencies:
- ansi-styles: 4.3.0
- supports-color: 7.2.0
- dev: true
-
- /chalk/4.1.2:
+ /chalk@4.1.2:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'}
dependencies:
@@ -9810,32 +8512,16 @@ packages:
supports-color: 7.2.0
dev: true
- /chalk/5.1.2:
- resolution: {integrity: sha512-E5CkT4jWURs1Vy5qGJye+XwCkNj7Od3Af7CP6SujMetSMkLs8Do2RWJK5yx1wamHV/op8Rz+9rltjaTQWDnEFQ==}
+ /chalk@5.3.0:
+ resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==}
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
dev: true
- /char-regex/1.0.2:
- resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==}
- engines: {node: '>=10'}
- dev: true
-
- /character-entities-legacy/1.1.4:
- resolution: {integrity: sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==}
- dev: true
-
- /character-entities/1.2.4:
- resolution: {integrity: sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==}
- dev: true
-
- /character-reference-invalid/1.1.4:
- resolution: {integrity: sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==}
- dev: true
-
- /check-error/1.0.2:
+ /check-error@1.0.2:
resolution: {integrity: sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==}
+ dev: true
- /cheerio-select/2.1.0:
+ /cheerio-select@2.1.0:
resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==}
dependencies:
boolbase: 1.0.0
@@ -9843,28 +8529,29 @@ packages:
css-what: 6.1.0
domelementtype: 2.3.0
domhandler: 5.0.3
- domutils: 3.0.1
+ domutils: 3.1.0
dev: true
- /cheerio/1.0.0-rc.12:
+ /cheerio@1.0.0-rc.12:
resolution: {integrity: sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==}
engines: {node: '>= 6'}
dependencies:
cheerio-select: 2.1.0
dom-serializer: 2.0.0
domhandler: 5.0.3
- domutils: 3.0.1
- htmlparser2: 8.0.1
- parse5: 7.1.1
+ domutils: 3.1.0
+ htmlparser2: 8.0.2
+ parse5: 7.1.2
parse5-htmlparser2-tree-adapter: 7.0.0
dev: true
- /chokidar/2.1.8:
+ /chokidar@2.1.8:
resolution: {integrity: sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==}
deprecated: Chokidar 2 does not receive security updates since 2019. Upgrade to chokidar 3 with 15x fewer dependencies
+ requiresBuild: true
dependencies:
anymatch: 2.0.0
- async-each: 1.0.3
+ async-each: 1.0.6
braces: 2.3.2
glob-parent: 3.1.0
inherits: 2.0.4
@@ -9881,7 +8568,7 @@ packages:
dev: true
optional: true
- /chokidar/3.5.3:
+ /chokidar@3.5.3:
resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
engines: {node: '>= 8.10.0'}
dependencies:
@@ -9893,55 +8580,69 @@ packages:
normalize-path: 3.0.0
readdirp: 3.6.0
optionalDependencies:
- fsevents: 2.3.2
- dev: true
+ fsevents: 2.3.3
- /chownr/1.1.4:
+ /chownr@1.1.4:
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
- dev: true
+ requiresBuild: true
- /chownr/2.0.0:
+ /chownr@2.0.0:
resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==}
engines: {node: '>=10'}
dev: true
- /chrome-trace-event/1.0.3:
+ /chrome-launcher@0.15.1:
+ resolution: {integrity: sha512-UugC8u59/w2AyX5sHLZUHoxBAiSiunUhZa3zZwMH6zPVis0C3dDKiRWyUGIo14tTbZHGVviWxv3PQWZ7taZ4fg==}
+ engines: {node: '>=12.13.0'}
+ hasBin: true
+ dependencies:
+ '@types/node': 20.11.13
+ escape-string-regexp: 4.0.0
+ is-wsl: 2.2.0
+ lighthouse-logger: 1.4.2
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /chrome-trace-event@1.0.3:
resolution: {integrity: sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==}
engines: {node: '>=6.0'}
dev: true
- /chunkd/2.0.1:
+ /chunkd@2.0.1:
resolution: {integrity: sha512-7d58XsFmOq0j6el67Ug9mHf9ELUXsQXYJBkyxhH/k+6Ke0qXRnv0kbemx+Twc6fRJ07C49lcbdgm9FL1Ei/6SQ==}
dev: true
- /ci-env/1.17.0:
+ /ci-env@1.17.0:
resolution: {integrity: sha512-NtTjhgSEqv4Aj90TUYHQLxHdnCPXnjdtuGG1X8lTfp/JqeXTdw0FTWl/vUAPuvbWZTF8QVpv6ASe/XacE+7R2A==}
dev: true
- /ci-info/2.0.0:
+ /ci-info@2.0.0:
resolution: {integrity: sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==}
dev: true
- /ci-info/3.5.0:
- resolution: {integrity: sha512-yH4RezKOGlOhxkmhbeNuC4eYZKAUsEaGtBuBzDDP1eFUKiccDWzBABxBfOx31IDwDIXMTxWuwAxUGModvkbuVw==}
+ /ci-info@3.9.0:
+ resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /ci-info@4.0.0:
+ resolution: {integrity: sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==}
+ engines: {node: '>=8'}
dev: true
- /ci-parallel-vars/1.0.1:
+ /ci-parallel-vars@1.0.1:
resolution: {integrity: sha512-uvzpYrpmidaoxvIQHM+rKSrigjOe9feHYbw4uOI2gdfe1C3xIlxO+kVXq83WQWNniTf8bAxVpy+cQeFQsMERKg==}
dev: true
- /cipher-base/1.0.4:
+ /cipher-base@1.0.4:
resolution: {integrity: sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==}
dependencies:
inherits: 2.0.4
safe-buffer: 5.2.1
dev: true
- /cjs-module-lexer/0.6.0:
- resolution: {integrity: sha512-uc2Vix1frTfnuzxxu1Hp4ktSvM3QaI4oXl4ZUqL1wjTu/BGki9TrCWoqLTg/drR1KwAEarXuRFCG2Svr1GxPFw==}
- dev: true
-
- /class-utils/0.3.6:
+ /class-utils@0.3.6:
resolution: {integrity: sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==}
engines: {node: '>=0.10.0'}
dependencies:
@@ -9951,70 +8652,60 @@ packages:
static-extend: 0.1.2
dev: true
- /clean-css/4.2.4:
+ /clean-css@4.2.4:
resolution: {integrity: sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==}
engines: {node: '>= 4.0'}
dependencies:
source-map: 0.6.1
dev: true
- /clean-stack/2.2.0:
- resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==}
- engines: {node: '>=6'}
- dev: true
-
- /clean-stack/4.2.0:
- resolution: {integrity: sha512-LYv6XPxoyODi36Dp976riBtSY27VmFo+MKqEU9QCCWyTrdEPDog+RWA7xQWHi6Vbp61j5c4cdzzX1NidnwtUWg==}
- engines: {node: '>=12'}
+ /clean-css@5.3.3:
+ resolution: {integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==}
+ engines: {node: '>= 10.0'}
dependencies:
- escape-string-regexp: 5.0.0
+ source-map: 0.6.1
dev: true
- /clean-yaml-object/0.1.0:
- resolution: {integrity: sha512-3yONmlN9CSAkzNwnRCiJQ7Q2xK5mWuEfL3PuTZcAUzhObbXsfsnMptJzXwz93nc5zn9V9TwCVMmV7w4xsm43dw==}
- engines: {node: '>=0.10.0'}
+ /clean-stack@2.2.0:
+ resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==}
+ engines: {node: '>=6'}
dev: true
- /cli-boxes/2.2.1:
+ /cli-boxes@2.2.1:
resolution: {integrity: sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==}
engines: {node: '>=6'}
dev: true
- /cli-cursor/3.1.0:
+ /cli-boxes@3.0.0:
+ resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==}
+ engines: {node: '>=10'}
+ dev: true
+
+ /cli-cursor@3.1.0:
resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==}
engines: {node: '>=8'}
dependencies:
restore-cursor: 3.1.0
dev: true
- /cli-spinners/2.6.1:
- resolution: {integrity: sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==}
- engines: {node: '>=6'}
- dev: true
-
- /cli-spinners/2.7.0:
+ /cli-spinners@2.7.0:
resolution: {integrity: sha512-qu3pN8Y3qHNgE2AFweciB1IfMnmZ/fsNTEE+NOFjmGB2F/7rLhnhzppvpCnN4FovtP26k8lHyy9ptEbNwWFLzw==}
engines: {node: '>=6'}
dev: true
- /cli-table3/0.6.3:
- resolution: {integrity: sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==}
- engines: {node: 10.* || >= 12.*}
- dependencies:
- string-width: 4.2.3
- optionalDependencies:
- '@colors/colors': 1.5.0
- dev: true
-
- /cli-truncate/3.1.0:
- resolution: {integrity: sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==}
- engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+ /cli-truncate@4.0.0:
+ resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==}
+ engines: {node: '>=18'}
dependencies:
slice-ansi: 5.0.0
- string-width: 5.1.2
+ string-width: 7.0.0
dev: true
- /cliui/6.0.0:
+ /client-only@0.0.1:
+ resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
+ dev: false
+
+ /cliui@6.0.0:
resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
dependencies:
string-width: 4.2.3
@@ -10022,7 +8713,7 @@ packages:
wrap-ansi: 6.2.0
dev: true
- /cliui/7.0.4:
+ /cliui@7.0.4:
resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==}
dependencies:
string-width: 4.2.3
@@ -10030,7 +8721,7 @@ packages:
wrap-ansi: 7.0.0
dev: true
- /cliui/8.0.1:
+ /cliui@8.0.1:
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
engines: {node: '>=12'}
dependencies:
@@ -10039,7 +8730,7 @@ packages:
wrap-ansi: 7.0.0
dev: true
- /clone-deep/4.0.1:
+ /clone-deep@4.0.1:
resolution: {integrity: sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==}
engines: {node: '>=6'}
dependencies:
@@ -10048,23 +8739,18 @@ packages:
shallow-clone: 3.0.1
dev: true
- /clone-response/1.0.3:
+ /clone-response@1.0.3:
resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==}
dependencies:
mimic-response: 1.0.1
dev: true
- /clone/1.0.4:
+ /clone@1.0.4:
resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
engines: {node: '>=0.8'}
dev: true
- /co/4.6.0:
- resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==}
- engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
- dev: true
-
- /coa/2.0.2:
+ /coa@2.0.2:
resolution: {integrity: sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==}
engines: {node: '>= 4.0'}
dependencies:
@@ -10073,22 +8759,14 @@ packages:
q: 1.5.1
dev: true
- /code-excerpt/4.0.0:
+ /code-excerpt@4.0.0:
resolution: {integrity: sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
dependencies:
convert-to-spaces: 2.0.1
dev: true
- /collapse-white-space/1.0.6:
- resolution: {integrity: sha512-jEovNnrhMuqyCcjfEJA56v0Xq8SkIoPKDyaHahwo3POf4qcSXqMYuwNcOTzp74vTsR9Tn08z4MxWqAhcekogkQ==}
- dev: true
-
- /collect-v8-coverage/1.0.1:
- resolution: {integrity: sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==}
- dev: true
-
- /collection-visit/1.0.0:
+ /collection-visit@1.0.0:
resolution: {integrity: sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==}
engines: {node: '>=0.10.0'}
dependencies:
@@ -10096,121 +8774,128 @@ packages:
object-visit: 1.0.1
dev: true
- /color-convert/1.9.3:
+ /color-convert@1.9.3:
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
dependencies:
color-name: 1.1.3
- dev: true
- /color-convert/2.0.1:
+ /color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
dependencies:
color-name: 1.1.4
- dev: true
- /color-name/1.1.3:
+ /color-name@1.1.3:
resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==}
- dev: true
- /color-name/1.1.4:
+ /color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
- dev: true
- /color-string/1.9.1:
+ /color-string@1.9.1:
resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==}
dependencies:
color-name: 1.1.4
simple-swizzle: 0.2.2
dev: true
- /color-support/1.1.3:
+ /color-support@1.1.3:
resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==}
hasBin: true
dev: true
- /color/3.2.1:
+ /color@3.2.1:
resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==}
dependencies:
color-convert: 1.9.3
color-string: 1.9.1
dev: true
- /colord/2.9.3:
+ /colord@2.9.3:
resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==}
dev: true
- /colorette/2.0.19:
+ /colorette@2.0.19:
resolution: {integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==}
dev: true
- /colors/1.4.0:
- resolution: {integrity: sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==}
- engines: {node: '>=0.1.90'}
+ /columnify@1.6.0:
+ resolution: {integrity: sha512-lomjuFZKfM6MSAnV9aCZC9sc0qGbmZdfygNv+nCpqVkSKdCxCklLtd16O0EILGkImHw9ZpHkAnHaB+8Zxq5W6Q==}
+ engines: {node: '>=8.0.0'}
+ dependencies:
+ strip-ansi: 6.0.1
+ wcwidth: 1.0.1
dev: true
- /combined-stream/1.0.8:
+ /combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
dependencies:
delayed-stream: 1.0.0
-
- /comma-separated-tokens/1.0.8:
- resolution: {integrity: sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==}
dev: true
- /commander/2.17.1:
+ /commander@2.17.1:
resolution: {integrity: sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==}
dev: true
- /commander/2.19.0:
+ /commander@2.19.0:
resolution: {integrity: sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==}
dev: true
- /commander/2.20.3:
+ /commander@2.20.3:
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
dev: true
- /commander/4.1.1:
- resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
- engines: {node: '>= 6'}
+ /commander@2.9.0:
+ resolution: {integrity: sha512-bmkUukX8wAOjHdN26xj5c4ctEV22TQ7dQYhSmuckKhToXrkUn0iIaolHdIxYYqD55nhpSPA9zPQ1yP57GdXP2A==}
+ engines: {node: '>= 0.6.x'}
+ dependencies:
+ graceful-readlink: 1.0.1
dev: true
- /commander/6.2.1:
- resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==}
+ /commander@4.1.1:
+ resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
engines: {node: '>= 6'}
- dev: true
- /commander/7.2.0:
+ /commander@7.2.0:
resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==}
engines: {node: '>= 10'}
dev: true
- /common-path-prefix/3.0.0:
+ /commander@8.3.0:
+ resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==}
+ engines: {node: '>= 12'}
+ dev: true
+
+ /commander@9.5.0:
+ resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==}
+ engines: {node: ^12.20.0 || >=14}
+ dev: true
+
+ /common-path-prefix@3.0.0:
resolution: {integrity: sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==}
dev: true
- /common-tags/1.8.2:
+ /common-tags@1.8.2:
resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==}
engines: {node: '>=4.0.0'}
dev: true
- /commondir/1.0.1:
+ /commondir@1.0.1:
resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==}
dev: true
- /component-emitter/1.3.0:
+ /component-emitter@1.3.0:
resolution: {integrity: sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==}
dev: true
- /compressible/2.0.18:
+ /compressible@2.0.18:
resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==}
engines: {node: '>= 0.6'}
dependencies:
mime-db: 1.52.0
dev: true
- /compression-webpack-plugin/6.1.1_webpack@4.46.0:
+ /compression-webpack-plugin@6.1.1(webpack@4.46.0):
resolution: {integrity: sha512-BEHft9M6lwOqVIQFMS/YJGmeCYXVOakC5KzQk05TFpMBlODByh1qNsZCWjUBxCQhUP9x0WfGidxTbGkjbWO/TQ==}
engines: {node: '>= 10.13.0'}
peerDependencies:
@@ -10226,7 +8911,7 @@ packages:
- bluebird
dev: true
- /compression/1.7.4:
+ /compression@1.7.4:
resolution: {integrity: sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==}
engines: {node: '>= 0.8.0'}
dependencies:
@@ -10241,102 +8926,123 @@ packages:
- supports-color
dev: true
- /concat-map/0.0.1:
- resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=}
+ /concat-map@0.0.1:
+ resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
- /concat-stream/1.6.2:
+ /concat-stream@1.6.2:
resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==}
engines: {'0': node >= 0.8}
dependencies:
buffer-from: 1.1.2
inherits: 2.0.4
- readable-stream: 2.3.7
+ readable-stream: 2.3.8
typedarray: 0.0.6
dev: true
- /concordance/5.0.4:
+ /concordance@5.0.4:
resolution: {integrity: sha512-OAcsnTEYu1ARJqWVGwf4zh4JDfHZEaSNlNccFmt8YjB2l/n19/PF2viLINHc57vO4FKIAFl2FWASIGZZWZ2Kxw==}
engines: {node: '>=10.18.0 <11 || >=12.14.0 <13 || >=14'}
dependencies:
date-time: 3.1.0
esutils: 2.0.3
- fast-diff: 1.2.0
+ fast-diff: 1.3.0
js-string-escape: 1.0.1
lodash: 4.17.21
md5-hex: 3.0.1
- semver: 7.3.8
+ semver: 7.5.4
well-known-symbols: 2.0.0
dev: true
- /configstore/5.0.1:
+ /config-chain@1.1.13:
+ resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==}
+ dependencies:
+ ini: 1.3.8
+ proto-list: 1.2.4
+ dev: true
+
+ /configstore@5.0.1:
resolution: {integrity: sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==}
engines: {node: '>=8'}
dependencies:
dot-prop: 5.3.0
- graceful-fs: 4.2.10
+ graceful-fs: 4.2.11
make-dir: 3.1.0
unique-string: 2.0.0
write-file-atomic: 3.0.3
xdg-basedir: 4.0.0
dev: true
- /confusing-browser-globals/1.0.11:
+ /configstore@6.0.0:
+ resolution: {integrity: sha512-cD31W1v3GqUlQvbBCGcXmd2Nj9SvLDOP1oQ0YFuLETufzSPaKp11rYBsSOm7rCsW3OnIRAFM3OxRhceaXNYHkA==}
+ engines: {node: '>=12'}
+ dependencies:
+ dot-prop: 6.0.1
+ graceful-fs: 4.2.11
+ unique-string: 3.0.0
+ write-file-atomic: 3.0.3
+ xdg-basedir: 5.1.0
+ dev: true
+
+ /confusing-browser-globals@1.0.11:
resolution: {integrity: sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==}
dev: true
- /connect-history-api-fallback/2.0.0:
+ /connect-history-api-fallback@2.0.0:
resolution: {integrity: sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==}
engines: {node: '>=0.8'}
dev: true
- /console-browserify/1.2.0:
+ /console-browserify@1.2.0:
resolution: {integrity: sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==}
dev: true
- /console-clear/1.1.1:
+ /console-clear@1.1.1:
resolution: {integrity: sha512-pMD+MVR538ipqkG5JXeOEbKWS5um1H4LUUccUQG68qpeqBYbzYy79Gh55jkd2TtPdRfUaLWdv6LPP//5Zt0aPQ==}
engines: {node: '>=4'}
dev: true
- /console-control-strings/1.1.0:
+ /console-control-strings@1.1.0:
resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==}
dev: true
- /constants-browserify/1.0.0:
+ /constants-browserify@1.0.0:
resolution: {integrity: sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==}
dev: true
- /content-disposition/0.5.4:
+ /content-disposition@0.5.4:
resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==}
engines: {node: '>= 0.6'}
dependencies:
safe-buffer: 5.2.1
dev: true
- /content-type/1.0.4:
+ /content-type@1.0.4:
resolution: {integrity: sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==}
engines: {node: '>= 0.6'}
dev: true
- /convert-source-map/1.9.0:
+ /convert-source-map@1.9.0:
resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==}
+
+ /convert-source-map@2.0.0:
+ resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
dev: true
- /convert-to-spaces/2.0.1:
+ /convert-to-spaces@2.0.1:
resolution: {integrity: sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
dev: true
- /cookie-signature/1.0.6:
+ /cookie-signature@1.0.6:
resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
dev: true
- /cookie/0.5.0:
+ /cookie@0.5.0:
resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==}
engines: {node: '>= 0.6'}
dev: true
- /copy-concurrently/1.0.5:
+ /copy-concurrently@1.0.5:
resolution: {integrity: sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==}
dependencies:
aproba: 1.2.0
@@ -10347,19 +9053,19 @@ packages:
run-queue: 1.0.3
dev: true
- /copy-descriptor/0.1.1:
+ /copy-descriptor@0.1.1:
resolution: {integrity: sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==}
engines: {node: '>=0.10.0'}
dev: true
- /copy-webpack-plugin/6.4.1_webpack@4.46.0:
+ /copy-webpack-plugin@6.4.1(webpack@4.46.0):
resolution: {integrity: sha512-MXyPCjdPVx5iiWyl40Va3JGh27bKzOTNY3NjUTrosD2q7dR/cLD0013uqJ3BpFbUjyONINjb6qI7nDIJujrMbA==}
engines: {node: '>= 10.13.0'}
peerDependencies:
webpack: ^4.37.0 || ^5.0.0
dependencies:
cacache: 15.3.0
- fast-glob: 3.2.12
+ fast-glob: 3.3.1
find-cache-dir: 3.3.2
glob-parent: 5.1.2
globby: 11.1.0
@@ -10374,37 +9080,37 @@ packages:
- bluebird
dev: true
- /core-js-compat/3.26.0:
+ /core-js-compat@3.26.0:
resolution: {integrity: sha512-piOX9Go+Z4f9ZiBFLnZ5VrOpBl0h7IGCkiFUN11QTe6LjAvOT3ifL/5TdoizMh99hcGy5SoLyWbapIY/PIb/3A==}
dependencies:
- browserslist: 4.21.4
+ browserslist: 4.22.2
dev: true
- /core-js-pure/3.26.0:
- resolution: {integrity: sha512-LiN6fylpVBVwT8twhhluD9TzXmZQQsr2I2eIKtWNbZI1XMfBT7CV18itaN6RA7EtQd/SDdRx/wzvAShX2HvhQA==}
- requiresBuild: true
+ /core-js-compat@3.33.3:
+ resolution: {integrity: sha512-cNzGqFsh3Ot+529GIXacjTJ7kegdt5fPXxCBVS1G0iaZpuo/tBz399ymceLJveQhFFZ8qThHiP3fzuoQjKN2ow==}
+ dependencies:
+ browserslist: 4.22.2
dev: true
- /core-js/2.6.12:
- resolution: {integrity: sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==}
- deprecated: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.
+ /core-js@3.26.0:
+ resolution: {integrity: sha512-+DkDrhoR4Y0PxDz6rurahuB+I45OsEUv8E1maPTB6OuHRohMMcznBq9TMpdpDMm/hUPob/mJJS3PqgbHpMTQgw==}
requiresBuild: true
dev: true
- /core-js/3.26.0:
- resolution: {integrity: sha512-+DkDrhoR4Y0PxDz6rurahuB+I45OsEUv8E1maPTB6OuHRohMMcznBq9TMpdpDMm/hUPob/mJJS3PqgbHpMTQgw==}
+ /core-js@3.29.0:
+ resolution: {integrity: sha512-VG23vuEisJNkGl6XQmFJd3rEG/so/CNatqeE+7uZAwTSwFeB/qaO0be8xZYUNWprJ/GIwL8aMt9cj1kvbpTZhg==}
requiresBuild: true
dev: true
- /core-util-is/1.0.2:
+ /core-util-is@1.0.2:
resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==}
dev: true
- /core-util-is/1.0.3:
+ /core-util-is@1.0.3:
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
dev: true
- /cosmiconfig/5.2.1:
+ /cosmiconfig@5.2.1:
resolution: {integrity: sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==}
engines: {node: '>=4'}
dependencies:
@@ -10414,9 +9120,9 @@ packages:
parse-json: 4.0.0
dev: true
- /cosmiconfig/6.0.0:
- resolution: {integrity: sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==}
- engines: {node: '>=8'}
+ /cosmiconfig@7.0.1:
+ resolution: {integrity: sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==}
+ engines: {node: '>=10'}
dependencies:
'@types/parse-json': 4.0.0
import-fresh: 3.3.0
@@ -10425,52 +9131,24 @@ packages:
yaml: 1.10.2
dev: true
- /cosmiconfig/7.0.1:
- resolution: {integrity: sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==}
- engines: {node: '>=10'}
+ /cosmiconfig@8.1.3:
+ resolution: {integrity: sha512-/UkO2JKI18b5jVMJUp0lvKFMpa/Gye+ZgZjKD+DGEN9y7NRcf/nK1A0sp67ONmKtnDCNMS44E6jrk0Yc3bDuUw==}
+ engines: {node: '>=14'}
dependencies:
- '@types/parse-json': 4.0.0
import-fresh: 3.3.0
+ js-yaml: 4.1.0
parse-json: 5.2.0
path-type: 4.0.0
- yaml: 1.10.2
dev: true
- /cp-file/7.0.0:
- resolution: {integrity: sha512-0Cbj7gyvFVApzpK/uhCtQ/9kE9UnYpxMzaq5nQQC/Dh4iaj5fxp7iEFIullrYwzj8nf0qnsI1Qsx34hAeAebvw==}
- engines: {node: '>=8'}
- dependencies:
- graceful-fs: 4.2.10
- make-dir: 3.1.0
- nested-error-stacks: 2.1.1
- p-event: 4.2.0
- dev: true
-
- /cpy/8.1.2:
- resolution: {integrity: sha512-dmC4mUesv0OYH2kNFEidtf/skUwv4zePmGeepjyyJ0qTo5+8KhA1o99oIAwVVLzQMAeDJml74d6wPPKb6EZUTg==}
- engines: {node: '>=8'}
- dependencies:
- arrify: 2.0.1
- cp-file: 7.0.0
- globby: 9.2.0
- has-glob: 1.0.0
- junk: 3.1.0
- nested-error-stacks: 2.1.1
- p-all: 2.1.0
- p-filter: 2.1.0
- p-map: 3.0.0
- transitivePeerDependencies:
- - supports-color
- dev: true
-
- /create-ecdh/4.0.4:
+ /create-ecdh@4.0.4:
resolution: {integrity: sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==}
dependencies:
bn.js: 4.12.0
elliptic: 6.5.4
dev: true
- /create-hash/1.2.0:
+ /create-hash@1.2.0:
resolution: {integrity: sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==}
dependencies:
cipher-base: 1.0.4
@@ -10480,7 +9158,7 @@ packages:
sha.js: 2.4.11
dev: true
- /create-hmac/1.1.7:
+ /create-hmac@1.1.7:
resolution: {integrity: sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==}
dependencies:
cipher-base: 1.0.4
@@ -10491,7 +9169,11 @@ packages:
sha.js: 2.4.11
dev: true
- /critters-webpack-plugin/2.5.0_html-webpack-plugin@3.2.0:
+ /create-require@1.1.1:
+ resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
+ dev: true
+
+ /critters-webpack-plugin@2.5.0(html-webpack-plugin@3.2.0):
resolution: {integrity: sha512-O41TSPV2orAfrV6kSVC0SivZCtVkeypCNKb7xtrbqE/CfjrHeRaFaGuxglcjOI2IGf+oNg6E+ZoOktdlhXPTIQ==}
peerDependencies:
html-webpack-plugin: '*'
@@ -10501,7 +9183,7 @@ packages:
dependencies:
css: 2.2.4
cssnano: 4.1.11
- html-webpack-plugin: 3.2.0_webpack@4.46.0
+ html-webpack-plugin: 3.2.0(webpack@4.46.0)
jsdom: 12.2.0
minimatch: 3.1.2
parse5: 4.0.0
@@ -10514,14 +9196,14 @@ packages:
- utf-8-validate
dev: true
- /cross-spawn-promise/0.10.2:
+ /cross-spawn-promise@0.10.2:
resolution: {integrity: sha512-74PXJf6DYaab2klRS+D+9qxKJL1Weo3/ao9OPoH6NFzxtINSa/HE2mcyAPu1fpEmRTPD4Gdmpg3xEXQSgI8lpg==}
engines: {node: '>=4'}
dependencies:
cross-spawn: 5.1.0
dev: true
- /cross-spawn/5.1.0:
+ /cross-spawn@5.1.0:
resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==}
dependencies:
lru-cache: 4.1.5
@@ -10529,27 +9211,15 @@ packages:
which: 1.3.1
dev: true
- /cross-spawn/6.0.5:
- resolution: {integrity: sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==}
- engines: {node: '>=4.8'}
- dependencies:
- nice-try: 1.0.5
- path-key: 2.0.1
- semver: 5.7.1
- shebang-command: 1.2.0
- which: 1.3.1
- dev: true
-
- /cross-spawn/7.0.3:
+ /cross-spawn@7.0.3:
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
engines: {node: '>= 8'}
dependencies:
path-key: 3.1.1
shebang-command: 2.0.0
which: 2.0.2
- dev: true
- /crypto-browserify/3.12.0:
+ /crypto-browserify@3.12.0:
resolution: {integrity: sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==}
dependencies:
browserify-cipher: 1.0.1
@@ -10565,16 +9235,23 @@ packages:
randomfill: 1.0.4
dev: true
- /crypto-random-string/2.0.0:
+ /crypto-random-string@2.0.0:
resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==}
engines: {node: '>=8'}
dev: true
- /css-color-names/0.0.4:
+ /crypto-random-string@4.0.0:
+ resolution: {integrity: sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==}
+ engines: {node: '>=12'}
+ dependencies:
+ type-fest: 1.4.0
+ dev: true
+
+ /css-color-names@0.0.4:
resolution: {integrity: sha512-zj5D7X1U2h2zsXOAM8EyUREBnnts6H+Jm+d1M2DbiQQcUtnqgQsMrdo8JW9R80YFUmIdBZeMu5wvYM7hcgWP/Q==}
dev: true
- /css-declaration-sorter/4.0.1:
+ /css-declaration-sorter@4.0.1:
resolution: {integrity: sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA==}
engines: {node: '>4'}
dependencies:
@@ -10582,61 +9259,39 @@ packages:
timsort: 0.3.0
dev: true
- /css-declaration-sorter/6.3.1_postcss@8.4.18:
+ /css-declaration-sorter@6.3.1(postcss@8.4.32):
resolution: {integrity: sha512-fBffmak0bPAnyqc/HO8C3n2sHrp9wcqQz6ES9koRF2/mLOVAx9zIQ3Y7R29sYCteTPqMCwns4WYQoCX91Xl3+w==}
engines: {node: ^10 || ^12 || >=14}
peerDependencies:
postcss: ^8.0.9
dependencies:
- postcss: 8.4.18
+ postcss: 8.4.32
dev: true
- /css-loader/3.6.0_webpack@4.46.0:
- resolution: {integrity: sha512-M5lSukoWi1If8dhQAUCvj4H8vUt3vOnwbQBH9DdTm/s4Ym2B/3dPMtYZeJmq7Q3S3Pa+I94DcZ7pc9bP14cWIQ==}
- engines: {node: '>= 8.9.0'}
- peerDependencies:
- webpack: ^4.0.0 || ^5.0.0
- dependencies:
- camelcase: 5.3.1
- cssesc: 3.0.0
- icss-utils: 4.1.1
- loader-utils: 1.4.0
- normalize-path: 3.0.0
- postcss: 7.0.39
- postcss-modules-extract-imports: 2.0.0
- postcss-modules-local-by-default: 3.0.3
- postcss-modules-scope: 2.2.0
- postcss-modules-values: 3.0.0
- postcss-value-parser: 4.2.0
- schema-utils: 2.7.1
- semver: 6.3.0
- webpack: 4.46.0
- dev: true
-
- /css-loader/5.2.7_webpack@4.46.0:
+ /css-loader@5.2.7(webpack@4.46.0):
resolution: {integrity: sha512-Q7mOvpBNBG7YrVGMxRxcBJZFL75o+cH2abNASdibkj/fffYD8qWbInZrD0S9ccI6vZclF3DsHE7njGlLtaHbhg==}
engines: {node: '>= 10.13.0'}
peerDependencies:
webpack: ^4.27.0 || ^5.0.0
dependencies:
- icss-utils: 5.1.0_postcss@8.4.18
+ icss-utils: 5.1.0(postcss@8.4.32)
loader-utils: 2.0.3
- postcss: 8.4.18
- postcss-modules-extract-imports: 3.0.0_postcss@8.4.18
- postcss-modules-local-by-default: 4.0.0_postcss@8.4.18
- postcss-modules-scope: 3.0.0_postcss@8.4.18
- postcss-modules-values: 4.0.0_postcss@8.4.18
+ postcss: 8.4.32
+ postcss-modules-extract-imports: 3.0.0(postcss@8.4.32)
+ postcss-modules-local-by-default: 4.0.0(postcss@8.4.32)
+ postcss-modules-scope: 3.0.0(postcss@8.4.32)
+ postcss-modules-values: 4.0.0(postcss@8.4.32)
postcss-value-parser: 4.2.0
schema-utils: 3.1.1
- semver: 7.3.8
+ semver: 7.5.4
webpack: 4.46.0
dev: true
- /css-select-base-adapter/0.1.1:
+ /css-select-base-adapter@0.1.1:
resolution: {integrity: sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==}
dev: true
- /css-select/2.1.0:
+ /css-select@2.1.0:
resolution: {integrity: sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==}
dependencies:
boolbase: 1.0.0
@@ -10645,7 +9300,7 @@ packages:
nth-check: 1.0.2
dev: true
- /css-select/4.3.0:
+ /css-select@4.3.0:
resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==}
dependencies:
boolbase: 1.0.0
@@ -10655,17 +9310,17 @@ packages:
nth-check: 2.1.1
dev: true
- /css-select/5.1.0:
+ /css-select@5.1.0:
resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==}
dependencies:
boolbase: 1.0.0
css-what: 6.1.0
domhandler: 5.0.3
- domutils: 3.0.1
+ domutils: 3.1.0
nth-check: 2.1.1
dev: true
- /css-tree/1.0.0-alpha.37:
+ /css-tree@1.0.0-alpha.37:
resolution: {integrity: sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==}
engines: {node: '>=8.0.0'}
dependencies:
@@ -10673,7 +9328,7 @@ packages:
source-map: 0.6.1
dev: true
- /css-tree/1.1.3:
+ /css-tree@1.1.3:
resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==}
engines: {node: '>=8.0.0'}
dependencies:
@@ -10681,17 +9336,21 @@ packages:
source-map: 0.6.1
dev: true
- /css-what/3.4.2:
+ /css-what@3.4.2:
resolution: {integrity: sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==}
engines: {node: '>= 6'}
dev: true
- /css-what/6.1.0:
+ /css-what@6.1.0:
resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==}
engines: {node: '>= 6'}
dev: true
- /css/2.2.4:
+ /css.escape@1.5.1:
+ resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==}
+ dev: true
+
+ /css@2.2.4:
resolution: {integrity: sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==}
dependencies:
inherits: 2.0.4
@@ -10700,13 +9359,12 @@ packages:
urix: 0.1.0
dev: true
- /cssesc/3.0.0:
+ /cssesc@3.0.0:
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
engines: {node: '>=4'}
hasBin: true
- dev: true
- /cssnano-preset-default/4.0.8:
+ /cssnano-preset-default@4.0.8:
resolution: {integrity: sha512-LdAyHuq+VRyeVREFmuxUZR1TXjQm8QQU/ktoo/x7bz+SdOge1YKc5eMN6pRW7YWBmyq59CqYba1dJ5cUukEjLQ==}
engines: {node: '>=6.9.0'}
dependencies:
@@ -10742,76 +9400,76 @@ packages:
postcss-unique-selectors: 4.0.1
dev: true
- /cssnano-preset-default/5.2.12_postcss@8.4.18:
+ /cssnano-preset-default@5.2.12(postcss@8.4.32):
resolution: {integrity: sha512-OyCBTZi+PXgylz9HAA5kHyoYhfGcYdwFmyaJzWnzxuGRtnMw/kR6ilW9XzlzlRAtB6PLT/r+prYgkef7hngFew==}
engines: {node: ^10 || ^12 || >=14.0}
peerDependencies:
postcss: ^8.2.15
dependencies:
- css-declaration-sorter: 6.3.1_postcss@8.4.18
- cssnano-utils: 3.1.0_postcss@8.4.18
- postcss: 8.4.18
- postcss-calc: 8.2.4_postcss@8.4.18
- postcss-colormin: 5.3.0_postcss@8.4.18
- postcss-convert-values: 5.1.2_postcss@8.4.18
- postcss-discard-comments: 5.1.2_postcss@8.4.18
- postcss-discard-duplicates: 5.1.0_postcss@8.4.18
- postcss-discard-empty: 5.1.1_postcss@8.4.18
- postcss-discard-overridden: 5.1.0_postcss@8.4.18
- postcss-merge-longhand: 5.1.6_postcss@8.4.18
- postcss-merge-rules: 5.1.2_postcss@8.4.18
- postcss-minify-font-values: 5.1.0_postcss@8.4.18
- postcss-minify-gradients: 5.1.1_postcss@8.4.18
- postcss-minify-params: 5.1.3_postcss@8.4.18
- postcss-minify-selectors: 5.2.1_postcss@8.4.18
- postcss-normalize-charset: 5.1.0_postcss@8.4.18
- postcss-normalize-display-values: 5.1.0_postcss@8.4.18
- postcss-normalize-positions: 5.1.1_postcss@8.4.18
- postcss-normalize-repeat-style: 5.1.1_postcss@8.4.18
- postcss-normalize-string: 5.1.0_postcss@8.4.18
- postcss-normalize-timing-functions: 5.1.0_postcss@8.4.18
- postcss-normalize-unicode: 5.1.0_postcss@8.4.18
- postcss-normalize-url: 5.1.0_postcss@8.4.18
- postcss-normalize-whitespace: 5.1.1_postcss@8.4.18
- postcss-ordered-values: 5.1.3_postcss@8.4.18
- postcss-reduce-initial: 5.1.0_postcss@8.4.18
- postcss-reduce-transforms: 5.1.0_postcss@8.4.18
- postcss-svgo: 5.1.0_postcss@8.4.18
- postcss-unique-selectors: 5.1.1_postcss@8.4.18
- dev: true
-
- /cssnano-util-get-arguments/4.0.0:
+ css-declaration-sorter: 6.3.1(postcss@8.4.32)
+ cssnano-utils: 3.1.0(postcss@8.4.32)
+ postcss: 8.4.32
+ postcss-calc: 8.2.4(postcss@8.4.32)
+ postcss-colormin: 5.3.0(postcss@8.4.32)
+ postcss-convert-values: 5.1.2(postcss@8.4.32)
+ postcss-discard-comments: 5.1.2(postcss@8.4.32)
+ postcss-discard-duplicates: 5.1.0(postcss@8.4.32)
+ postcss-discard-empty: 5.1.1(postcss@8.4.32)
+ postcss-discard-overridden: 5.1.0(postcss@8.4.32)
+ postcss-merge-longhand: 5.1.6(postcss@8.4.32)
+ postcss-merge-rules: 5.1.2(postcss@8.4.32)
+ postcss-minify-font-values: 5.1.0(postcss@8.4.32)
+ postcss-minify-gradients: 5.1.1(postcss@8.4.32)
+ postcss-minify-params: 5.1.3(postcss@8.4.32)
+ postcss-minify-selectors: 5.2.1(postcss@8.4.32)
+ postcss-normalize-charset: 5.1.0(postcss@8.4.32)
+ postcss-normalize-display-values: 5.1.0(postcss@8.4.32)
+ postcss-normalize-positions: 5.1.1(postcss@8.4.32)
+ postcss-normalize-repeat-style: 5.1.1(postcss@8.4.32)
+ postcss-normalize-string: 5.1.0(postcss@8.4.32)
+ postcss-normalize-timing-functions: 5.1.0(postcss@8.4.32)
+ postcss-normalize-unicode: 5.1.0(postcss@8.4.32)
+ postcss-normalize-url: 5.1.0(postcss@8.4.32)
+ postcss-normalize-whitespace: 5.1.1(postcss@8.4.32)
+ postcss-ordered-values: 5.1.3(postcss@8.4.32)
+ postcss-reduce-initial: 5.1.0(postcss@8.4.32)
+ postcss-reduce-transforms: 5.1.0(postcss@8.4.32)
+ postcss-svgo: 5.1.0(postcss@8.4.32)
+ postcss-unique-selectors: 5.1.1(postcss@8.4.32)
+ dev: true
+
+ /cssnano-util-get-arguments@4.0.0:
resolution: {integrity: sha512-6RIcwmV3/cBMG8Aj5gucQRsJb4vv4I4rn6YjPbVWd5+Pn/fuG+YseGvXGk00XLkoZkaj31QOD7vMUpNPC4FIuw==}
engines: {node: '>=6.9.0'}
dev: true
- /cssnano-util-get-match/4.0.0:
+ /cssnano-util-get-match@4.0.0:
resolution: {integrity: sha512-JPMZ1TSMRUPVIqEalIBNoBtAYbi8okvcFns4O0YIhcdGebeYZK7dMyHJiQ6GqNBA9kE0Hym4Aqym5rPdsV/4Cw==}
engines: {node: '>=6.9.0'}
dev: true
- /cssnano-util-raw-cache/4.0.1:
+ /cssnano-util-raw-cache@4.0.1:
resolution: {integrity: sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA==}
engines: {node: '>=6.9.0'}
dependencies:
postcss: 7.0.39
dev: true
- /cssnano-util-same-parent/4.0.1:
+ /cssnano-util-same-parent@4.0.1:
resolution: {integrity: sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q==}
engines: {node: '>=6.9.0'}
dev: true
- /cssnano-utils/3.1.0_postcss@8.4.18:
+ /cssnano-utils@3.1.0(postcss@8.4.32):
resolution: {integrity: sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==}
engines: {node: ^10 || ^12 || >=14.0}
peerDependencies:
postcss: ^8.2.15
dependencies:
- postcss: 8.4.18
+ postcss: 8.4.32
dev: true
- /cssnano/4.1.11:
+ /cssnano@4.1.11:
resolution: {integrity: sha512-6gZm2htn7xIPJOHY824ERgj8cNPgPxyCSnkXc4v7YvNW+TdVfzgngHcEhy/8D11kUWRUMbke+tC+AUcUsnMz2g==}
engines: {node: '>=6.9.0'}
dependencies:
@@ -10821,78 +9479,63 @@ packages:
postcss: 7.0.39
dev: true
- /cssnano/5.1.13_postcss@8.4.18:
+ /cssnano@5.1.13(postcss@8.4.32):
resolution: {integrity: sha512-S2SL2ekdEz6w6a2epXn4CmMKU4K3KpcyXLKfAYc9UQQqJRkD/2eLUG0vJ3Db/9OvO5GuAdgXw3pFbR6abqghDQ==}
engines: {node: ^10 || ^12 || >=14.0}
peerDependencies:
postcss: ^8.2.15
dependencies:
- cssnano-preset-default: 5.2.12_postcss@8.4.18
- lilconfig: 2.0.6
- postcss: 8.4.18
+ cssnano-preset-default: 5.2.12(postcss@8.4.32)
+ lilconfig: 2.1.0
+ postcss: 8.4.32
yaml: 1.10.2
dev: true
- /csso/4.2.0:
+ /csso@4.2.0:
resolution: {integrity: sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==}
engines: {node: '>=8.0.0'}
dependencies:
css-tree: 1.1.3
dev: true
- /cssom/0.3.8:
+ /cssom@0.3.8:
resolution: {integrity: sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==}
dev: true
- /cssom/0.4.4:
- resolution: {integrity: sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==}
- dev: true
-
- /cssstyle/1.4.0:
+ /cssstyle@1.4.0:
resolution: {integrity: sha512-GBrLZYZ4X4x6/QEoBnIrqb8B/f5l4+8me2dkom/j1Gtbxy0kBv6OGzKuAsGM75bkGwGAFkt56Iwg28S3XTZgSA==}
dependencies:
cssom: 0.3.8
dev: true
- /cssstyle/2.3.0:
- resolution: {integrity: sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==}
- engines: {node: '>=8'}
- dependencies:
- cssom: 0.3.8
- dev: true
-
- /csstype/3.1.1:
- resolution: {integrity: sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==}
- dev: true
-
- /currently-unhandled/0.4.1:
+ /currently-unhandled@0.4.1:
resolution: {integrity: sha512-/fITjgjGU50vjQ4FH6eUoYu+iUoUKIXws2hL15JJpIR+BbTxaXQsMuuyjtNh2WqsSBS5nsaZHFsFecyw5CCAng==}
engines: {node: '>=0.10.0'}
dependencies:
array-find-index: 1.0.2
dev: true
- /cyclist/1.0.1:
- resolution: {integrity: sha512-NJGVKPS81XejHcLhaLJS7plab0fK3slPh11mESeeDq2W4ZI5kUKK/LRRdVDvjJseojbPB7ZwjnyOybg3Igea/A==}
+ /cyclist@1.0.2:
+ resolution: {integrity: sha512-0sVXIohTfLqVIW3kb/0n6IiWF3Ifj5nm2XaSrLq2DI6fKIGa2fYAZdk917rUneaeLVpYfFcyXE2ft0fe3remsA==}
dev: true
- /damerau-levenshtein/1.0.8:
+ /damerau-levenshtein@1.0.8:
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
dev: true
- /dashdash/1.14.1:
+ /dashdash@1.14.1:
resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==}
engines: {node: '>=0.10'}
dependencies:
assert-plus: 1.0.0
dev: true
- /data-uri-to-buffer/4.0.0:
- resolution: {integrity: sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==}
+ /data-uri-to-buffer@4.0.1:
+ resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
engines: {node: '>= 12'}
- dev: false
+ dev: true
- /data-urls/1.1.0:
+ /data-urls@1.1.0:
resolution: {integrity: sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==}
dependencies:
abab: 2.0.6
@@ -10900,33 +9543,27 @@ packages:
whatwg-url: 7.1.0
dev: true
- /data-urls/2.0.0:
- resolution: {integrity: sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==}
- engines: {node: '>=10'}
- dependencies:
- abab: 2.0.6
- whatwg-mimetype: 2.3.0
- whatwg-url: 8.7.0
- dev: true
-
- /date-fns/2.29.2:
+ /date-fns@2.29.2:
resolution: {integrity: sha512-0VNbwmWJDS/G3ySwFSJA3ayhbURMTJLtwM2DTxf9CWondCnh6DTNlO9JgRSq6ibf4eD0lfMJNBxUdEAHHix+bA==}
engines: {node: '>=0.11'}
dev: false
- /date-fns/2.29.3:
+ /date-fns@2.29.3:
resolution: {integrity: sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==}
engines: {node: '>=0.11'}
- dev: false
- /date-time/3.1.0:
+ /date-time@3.1.0:
resolution: {integrity: sha512-uqCUKXE5q1PNBXjPqvwhwJf9SwMoAHBgWJ6DcrnS5o+W2JOiIILl0JEdVD8SGujrNS02GGxgwAg2PN2zONgtjg==}
engines: {node: '>=6'}
dependencies:
time-zone: 1.0.0
dev: true
- /debug/2.6.9:
+ /debounce@1.2.1:
+ resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==}
+ dev: true
+
+ /debug@2.6.9:
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
peerDependencies:
supports-color: '*'
@@ -10937,7 +9574,7 @@ packages:
ms: 2.0.0
dev: true
- /debug/3.2.7:
+ /debug@3.2.7:
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
peerDependencies:
supports-color: '*'
@@ -10948,7 +9585,7 @@ packages:
ms: 2.1.3
dev: true
- /debug/4.3.3_supports-color@8.1.1:
+ /debug@4.3.3(supports-color@8.1.1):
resolution: {integrity: sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==}
engines: {node: '>=6.0'}
peerDependencies:
@@ -10961,7 +9598,7 @@ packages:
supports-color: 8.1.1
dev: true
- /debug/4.3.4:
+ /debug@4.3.4:
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
engines: {node: '>=6.0'}
peerDependencies:
@@ -10971,124 +9608,151 @@ packages:
optional: true
dependencies:
ms: 2.1.2
- dev: true
- /decamelize/1.2.0:
+ /decamelize@1.2.0:
resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
engines: {node: '>=0.10.0'}
dev: true
- /decamelize/4.0.0:
+ /decamelize@4.0.0:
resolution: {integrity: sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==}
engines: {node: '>=10'}
dev: true
- /decimal.js/10.4.2:
- resolution: {integrity: sha512-ic1yEvwT6GuvaYwBLLY6/aFFgjZdySKTE8en/fkU3QICTmRtgtSlFn0u0BXN06InZwtfCelR7j8LRiDI/02iGA==}
+ /decamelize@6.0.0:
+ resolution: {integrity: sha512-Fv96DCsdOgB6mdGl67MT5JaTNKRzrzill5OH5s8bjYJXVlcXyPYGyPsUkWyGV5p1TXI5esYIYMMeDJL0hEIwaA==}
+ engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
dev: true
- /decode-uri-component/0.2.0:
- resolution: {integrity: sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==}
+ /decode-uri-component@0.2.2:
+ resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==}
engines: {node: '>=0.10'}
dev: true
- /decompress-response/3.3.0:
+ /decompress-response@3.3.0:
resolution: {integrity: sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==}
engines: {node: '>=4'}
dependencies:
mimic-response: 1.0.1
dev: true
- /deep-eql/3.0.1:
+ /decompress-response@6.0.0:
+ resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
+ engines: {node: '>=10'}
+ requiresBuild: true
+ dependencies:
+ mimic-response: 3.1.0
+
+ /deep-eql@3.0.1:
resolution: {integrity: sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==}
engines: {node: '>=0.12'}
dependencies:
type-detect: 4.0.8
+ dev: true
- /deep-extend/0.6.0:
+ /deep-extend@0.6.0:
resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
engines: {node: '>=4.0.0'}
- dev: true
+ requiresBuild: true
- /deep-is/0.1.4:
+ /deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
dev: true
- /deepcopy/1.0.0:
+ /deepcopy@1.0.0:
resolution: {integrity: sha512-WJrecobaoqqgQHtvRI2/VCzWoWXPAnFYyAkF/spmL46lZMnd0gW0gLGuyeFVSrqt2B3s0oEEj6i+j2L/2QiS4g==}
dependencies:
type-detect: 4.0.8
dev: true
- /deepmerge/4.2.2:
+ /deepcopy@2.1.0:
+ resolution: {integrity: sha512-8cZeTb1ZKC3bdSCP6XOM1IsTczIO73fdqtwa2B0N15eAz7gmyhQo+mc5gnFuulsgN3vIQYmTgbmQVKalH1dKvQ==}
+ dependencies:
+ type-detect: 4.0.8
+ dev: true
+
+ /deepmerge@2.2.1:
+ resolution: {integrity: sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /deepmerge@4.2.2:
resolution: {integrity: sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==}
engines: {node: '>=0.10.0'}
dev: true
- /default-browser-id/1.0.4:
- resolution: {integrity: sha512-qPy925qewwul9Hifs+3sx1ZYn14obHxpkX+mPD369w4Rzg+YkJBgi3SOvwUq81nWSjqGUegIgEPwD8u+HUnxlw==}
+ /deepmerge@4.3.1:
+ resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
engines: {node: '>=0.10.0'}
- hasBin: true
- requiresBuild: true
- dependencies:
- bplist-parser: 0.1.1
- meow: 3.7.0
- untildify: 2.1.0
dev: true
- optional: true
- /default-gateway/6.0.3:
+ /default-gateway@6.0.3:
resolution: {integrity: sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==}
engines: {node: '>= 10'}
dependencies:
execa: 5.1.1
dev: true
- /default-require-extensions/3.0.1:
+ /default-require-extensions@3.0.1:
resolution: {integrity: sha512-eXTJmRbm2TIt9MgWTsOH1wEuhew6XGZcMeGKCtLedIg/NCsg1iBePXkceTdK4Fii7pzmN9tGsZhKzZ4h7O/fxw==}
engines: {node: '>=8'}
dependencies:
strip-bom: 4.0.0
dev: true
- /defaults/1.0.4:
+ /defaults@1.0.4:
resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==}
dependencies:
clone: 1.0.4
dev: true
- /defer-to-connect/1.1.3:
+ /defer-to-connect@1.1.3:
resolution: {integrity: sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==}
dev: true
- /define-lazy-prop/2.0.0:
+ /defer-to-connect@2.0.1:
+ resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==}
+ engines: {node: '>=10'}
+ dev: true
+
+ /define-data-property@1.1.1:
+ resolution: {integrity: sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ get-intrinsic: 1.2.2
+ gopd: 1.0.1
+ has-property-descriptors: 1.0.1
+ dev: true
+
+ /define-lazy-prop@2.0.0:
resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==}
engines: {node: '>=8'}
dev: true
- /define-properties/1.1.4:
- resolution: {integrity: sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==}
+ /define-properties@1.2.1:
+ resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
engines: {node: '>= 0.4'}
dependencies:
- has-property-descriptors: 1.0.0
+ define-data-property: 1.1.1
+ has-property-descriptors: 1.0.1
object-keys: 1.1.1
dev: true
- /define-property/0.2.5:
+ /define-property@0.2.5:
resolution: {integrity: sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==}
engines: {node: '>=0.10.0'}
dependencies:
is-descriptor: 0.1.6
dev: true
- /define-property/1.0.0:
+ /define-property@1.0.0:
resolution: {integrity: sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==}
engines: {node: '>=0.10.0'}
dependencies:
is-descriptor: 1.0.2
dev: true
- /define-property/2.0.2:
+ /define-property@2.0.2:
resolution: {integrity: sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==}
engines: {node: '>=0.10.0'}
dependencies:
@@ -11096,98 +9760,70 @@ packages:
isobject: 3.0.1
dev: true
- /del/6.1.1:
- resolution: {integrity: sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==}
- engines: {node: '>=10'}
- dependencies:
- globby: 11.1.0
- graceful-fs: 4.2.10
- is-glob: 4.0.3
- is-path-cwd: 2.2.0
- is-path-inside: 3.0.3
- p-map: 4.0.0
- rimraf: 3.0.2
- slash: 3.0.0
- dev: true
-
- /delayed-stream/1.0.0:
+ /delayed-stream@1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
+ dev: true
- /delegates/1.0.0:
+ /delegates@1.0.0:
resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==}
dev: true
- /depd/1.1.2:
+ /depd@1.1.2:
resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==}
engines: {node: '>= 0.6'}
dev: true
- /depd/2.0.0:
+ /depd@2.0.0:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'}
dev: true
- /dequal/2.0.2:
- resolution: {integrity: sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug==}
+ /dependency-graph@0.11.0:
+ resolution: {integrity: sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==}
+ engines: {node: '>= 0.6.0'}
+ dev: true
+
+ /dequal@2.0.3:
+ resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'}
- dev: false
+ dev: true
- /des.js/1.0.1:
- resolution: {integrity: sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==}
+ /des.js@1.1.0:
+ resolution: {integrity: sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==}
dependencies:
inherits: 2.0.4
minimalistic-assert: 1.0.1
dev: true
- /destroy/1.2.0:
+ /destroy@1.2.0:
resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
dev: true
- /detab/2.0.4:
- resolution: {integrity: sha512-8zdsQA5bIkoRECvCrNKPla84lyoR7DSAyf7p0YgXzBO9PDJx8KntPUay7NS6yp+KdxdVtiE5SpHKtbp2ZQyA9g==}
- dependencies:
- repeat-string: 1.6.1
- dev: true
-
- /detect-newline/3.1.0:
- resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==}
+ /detect-libc@2.0.2:
+ resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==}
engines: {node: '>=8'}
- dev: true
+ requiresBuild: true
- /detect-node/2.1.0:
+ /detect-node@2.1.0:
resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==}
dev: true
- /detect-package-manager/2.0.1:
- resolution: {integrity: sha512-j/lJHyoLlWi6G1LDdLgvUtz60Zo5GEj+sVYtTVXnYLDPuzgC3llMxonXym9zIwhhUII8vjdw0LXxavpLqTbl1A==}
- engines: {node: '>=12'}
- dependencies:
- execa: 5.1.1
- dev: true
-
- /detect-port/1.5.1:
- resolution: {integrity: sha512-aBzdj76lueB6uUst5iAs7+0H/oOjqI5D16XUWxlWMIMROhcM0rfsNVk93zTngq1dDNpoXRr++Sus7ETAExppAQ==}
- hasBin: true
- dependencies:
- address: 1.2.1
- debug: 4.3.4
- transitivePeerDependencies:
- - supports-color
- dev: true
+ /didyoumean@1.2.2:
+ resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
- /diff-sequences/26.6.2:
- resolution: {integrity: sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==}
- engines: {node: '>= 10.14.2'}
+ /diff@4.0.2:
+ resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==}
+ engines: {node: '>=0.3.1'}
dev: true
- /diff/5.0.0:
+ /diff@5.0.0:
resolution: {integrity: sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==}
engines: {node: '>=0.3.1'}
dev: true
- /diffie-hellman/5.0.3:
+ /diffie-hellman@5.0.3:
resolution: {integrity: sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==}
dependencies:
bn.js: 4.12.0
@@ -11195,67 +9831,55 @@ packages:
randombytes: 2.1.0
dev: true
- /dir-glob/2.2.2:
- resolution: {integrity: sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==}
- engines: {node: '>=4'}
- dependencies:
- path-type: 3.0.0
- dev: true
-
- /dir-glob/3.0.1:
+ /dir-glob@3.0.1:
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
engines: {node: '>=8'}
dependencies:
path-type: 4.0.0
dev: true
- /discontinuous-range/1.0.0:
- resolution: {integrity: sha1-44Mx8IRLukm5qctxx3FYWqsbxlo=}
- dev: true
+ /dlv@1.1.3:
+ resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
- /dns-equal/1.0.0:
+ /dns-equal@1.0.0:
resolution: {integrity: sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==}
dev: true
- /dns-packet/5.4.0:
+ /dns-packet@5.4.0:
resolution: {integrity: sha512-EgqGeaBB8hLiHLZtp/IbaDQTL8pZ0+IvwzSHA6d7VyMDM+B9hgddEMa9xjK5oYnw0ci0JQ6g2XCD7/f6cafU6g==}
engines: {node: '>=6'}
dependencies:
'@leichtgewicht/ip-codec': 2.0.4
dev: true
- /doctrine/2.1.0:
+ /doctrine@2.1.0:
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
engines: {node: '>=0.10.0'}
dependencies:
esutils: 2.0.3
dev: true
- /doctrine/3.0.0:
+ /doctrine@3.0.0:
resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
engines: {node: '>=6.0.0'}
dependencies:
esutils: 2.0.3
dev: true
- /dom-accessibility-api/0.5.14:
- resolution: {integrity: sha512-NMt+m9zFMPZe0JcY9gN224Qvk6qLIdqex29clBvc/y75ZBX9YA9wNK3frsYvu2DI1xcCIwxwnX+TlsJ2DSOADg==}
- dev: true
-
- /dom-converter/0.2.0:
+ /dom-converter@0.2.0:
resolution: {integrity: sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==}
dependencies:
utila: 0.4.0
dev: true
- /dom-serializer/0.2.2:
+ /dom-serializer@0.2.2:
resolution: {integrity: sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==}
dependencies:
domelementtype: 2.3.0
entities: 2.2.0
dev: true
- /dom-serializer/1.4.1:
+ /dom-serializer@1.4.1:
resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==}
dependencies:
domelementtype: 2.3.0
@@ -11263,66 +9887,55 @@ packages:
entities: 2.2.0
dev: true
- /dom-serializer/2.0.0:
+ /dom-serializer@2.0.0:
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
dependencies:
domelementtype: 2.3.0
domhandler: 5.0.3
- entities: 4.4.0
- dev: true
-
- /dom-walk/0.1.2:
- resolution: {integrity: sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==}
+ entities: 4.5.0
dev: true
- /domain-browser/1.2.0:
+ /domain-browser@1.2.0:
resolution: {integrity: sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==}
engines: {node: '>=0.4', npm: '>=1.2'}
dev: true
- /domelementtype/1.3.1:
+ /domelementtype@1.3.1:
resolution: {integrity: sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==}
dev: true
- /domelementtype/2.3.0:
+ /domelementtype@2.3.0:
resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
dev: true
- /domexception/1.0.1:
+ /domexception@1.0.1:
resolution: {integrity: sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==}
dependencies:
webidl-conversions: 4.0.2
dev: true
- /domexception/2.0.1:
- resolution: {integrity: sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==}
- engines: {node: '>=8'}
- dependencies:
- webidl-conversions: 5.0.0
- dev: true
-
- /domhandler/4.3.1:
+ /domhandler@4.3.1:
resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==}
engines: {node: '>= 4'}
dependencies:
domelementtype: 2.3.0
dev: true
- /domhandler/5.0.3:
+ /domhandler@5.0.3:
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
engines: {node: '>= 4'}
dependencies:
domelementtype: 2.3.0
dev: true
- /domutils/1.7.0:
+ /domutils@1.7.0:
resolution: {integrity: sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==}
dependencies:
dom-serializer: 0.2.2
domelementtype: 1.3.1
dev: true
- /domutils/2.8.0:
+ /domutils@2.8.0:
resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==}
dependencies:
dom-serializer: 1.4.1
@@ -11330,105 +9943,117 @@ packages:
domhandler: 4.3.1
dev: true
- /domutils/3.0.1:
- resolution: {integrity: sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==}
+ /domutils@3.1.0:
+ resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==}
dependencies:
dom-serializer: 2.0.0
domelementtype: 2.3.0
domhandler: 5.0.3
dev: true
- /dot-case/3.0.4:
+ /dot-case@3.0.4:
resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==}
dependencies:
no-case: 3.0.4
- tslib: 2.4.1
+ tslib: 2.6.2
dev: true
- /dot-prop/5.3.0:
+ /dot-prop@5.3.0:
resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==}
engines: {node: '>=8'}
dependencies:
is-obj: 2.0.0
dev: true
- /dotenv-expand/5.1.0:
- resolution: {integrity: sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==}
- dev: true
-
- /dotenv/10.0.0:
- resolution: {integrity: sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==}
+ /dot-prop@6.0.1:
+ resolution: {integrity: sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==}
engines: {node: '>=10'}
+ dependencies:
+ is-obj: 2.0.0
dev: true
- /dotenv/16.0.3:
+ /dotenv@16.0.3:
resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==}
engines: {node: '>=12'}
dev: true
- /dotenv/8.6.0:
+ /dotenv@8.6.0:
resolution: {integrity: sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==}
engines: {node: '>=10'}
dev: true
- /duplexer/0.1.2:
- resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
+ /dtrace-provider@0.8.8:
+ resolution: {integrity: sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg==}
+ engines: {node: '>=0.10'}
+ requiresBuild: true
+ dependencies:
+ nan: 2.18.0
dev: true
+ optional: true
- /duplexer3/0.1.5:
+ /duplexer3@0.1.5:
resolution: {integrity: sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==}
dev: true
- /duplexify/3.7.1:
+ /duplexer@0.1.2:
+ resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
+ dev: true
+
+ /duplexify@3.7.1:
resolution: {integrity: sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==}
dependencies:
end-of-stream: 1.4.4
inherits: 2.0.4
- readable-stream: 2.3.7
+ readable-stream: 2.3.8
stream-shift: 1.0.1
dev: true
- /eastasianwidth/0.2.0:
+ /eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
- dev: true
- /ecc-jsbn/0.1.2:
+ /ecc-jsbn@0.1.2:
resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==}
dependencies:
jsbn: 0.1.1
safer-buffer: 2.1.2
dev: true
- /ee-first/1.1.1:
+ /ecdsa-sig-formatter@1.0.11:
+ resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
+ dependencies:
+ safe-buffer: 5.2.1
+ dev: true
+
+ /ee-first@1.1.1:
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
dev: true
- /ejs-loader/0.5.0:
+ /ejs-loader@0.5.0:
resolution: {integrity: sha512-iirFqlP3tiFoedNZ7dQcjvechunl054VbW6Ki38T/pabgXMAncduSE0ZXLeVGn1NbmcUJF9Z5TC0EvQ4RIpP9Q==}
dependencies:
loader-utils: 2.0.3
lodash: 4.17.21
dev: true
- /ejs/3.1.8:
+ /ejs@3.1.8:
resolution: {integrity: sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ==}
engines: {node: '>=0.10.0'}
- hasBin: true
dependencies:
jake: 10.8.5
dev: true
- /electron-to-chromium/1.4.284:
+ /electron-to-chromium@1.4.284:
resolution: {integrity: sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==}
dev: true
- /element-resize-detector/1.2.4:
- resolution: {integrity: sha512-Fl5Ftk6WwXE0wqCgNoseKWndjzZlDCwuPTcoVZfCP9R3EHQF8qUtr3YUPNETegRBOKqQKPW3n4kiIWngGi8tKg==}
- dependencies:
- batch-processor: 1.0.0
+ /electron-to-chromium@1.4.597:
+ resolution: {integrity: sha512-0XOQNqHhg2YgRVRUrS4M4vWjFCFIP2ETXcXe/0KIQBjXE9Cpy+tgzzYfuq6HGai3hWq0YywtG+5XK8fyG08EjA==}
dev: true
- /elliptic/6.5.4:
+ /electron-to-chromium@1.4.613:
+ resolution: {integrity: sha512-r4x5+FowKG6q+/Wj0W9nidx7QO31BJwmR2uEo+Qh3YLGQ8SbBAFuDFpTxzly/I2gsbrFwBuIjrMp423L3O5U3w==}
+
+ /elliptic@6.5.4:
resolution: {integrity: sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==}
dependencies:
bn.js: 4.12.0
@@ -11440,206 +10065,183 @@ packages:
minimalistic-crypto-utils: 1.0.1
dev: true
- /emittery/0.11.0:
- resolution: {integrity: sha512-S/7tzL6v5i+4iJd627Nhv9cLFIo5weAIlGccqJFpnBoDB8U1TF2k5tez4J/QNuxyyhWuFqHg1L84Kd3m7iXg6g==}
- engines: {node: '>=12'}
+ /emittery@1.0.1:
+ resolution: {integrity: sha512-2ID6FdrMD9KDLldGesP6317G78K7km/kMcwItRtVFva7I/cSEOIaLpewaUb+YLXVwdAp3Ctfxh/V5zIl1sj7dQ==}
+ engines: {node: '>=14.16'}
dev: true
- /emittery/0.7.2:
- resolution: {integrity: sha512-A8OG5SR/ij3SsJdWDJdkkSYUjQdCUx6APQXem0SaEePBSRg4eymGYwBkKo1Y6DU+af/Jn2dBQqDBvjnr9Vi8nQ==}
- engines: {node: '>=10'}
+ /emoji-regex@10.3.0:
+ resolution: {integrity: sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==}
dev: true
- /emoji-regex/8.0.0:
+ /emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
- dev: true
- /emoji-regex/9.2.2:
+ /emoji-regex@9.2.2:
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
- dev: true
- /emojis-list/2.1.0:
+ /emojis-list@2.1.0:
resolution: {integrity: sha512-knHEZMgs8BB+MInokmNTg/OyPlAddghe1YBgNwJBc5zsJi/uyIcXoSDsL/W9ymOsBoBGdPIHXYJ9+qKFwRwDng==}
engines: {node: '>= 0.10'}
dev: true
- /emojis-list/3.0.0:
+ /emojis-list@3.0.0:
resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==}
engines: {node: '>= 4'}
dev: true
- /encodeurl/1.0.2:
+ /encodeurl@1.0.2:
resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==}
engines: {node: '>= 0.8'}
dev: true
- /encoding/0.1.13:
+ /encoding@0.1.13:
resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==}
dependencies:
iconv-lite: 0.6.3
dev: true
- /end-of-stream/1.4.4:
+ /end-of-stream@1.4.4:
resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==}
+ requiresBuild: true
dependencies:
once: 1.4.0
- dev: true
- /enhanced-resolve/4.5.0:
+ /enhanced-resolve@4.5.0:
resolution: {integrity: sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg==}
engines: {node: '>=6.9.0'}
dependencies:
- graceful-fs: 4.2.10
+ graceful-fs: 4.2.11
memory-fs: 0.5.0
tapable: 1.1.3
dev: true
- /enhanced-resolve/5.10.0:
+ /enhanced-resolve@5.10.0:
resolution: {integrity: sha512-T0yTFjdpldGY8PmuXXR0PyQ1ufZpEGiHVrp7zHKB7jdR4qlmZHhONVM5AQOAWXuF/w3dnHbEQVrNptJgt7F+cQ==}
engines: {node: '>=10.13.0'}
dependencies:
- graceful-fs: 4.2.10
+ graceful-fs: 4.2.11
tapable: 2.2.1
dev: true
- /enquirer/2.3.6:
+ /enquirer@2.3.6:
resolution: {integrity: sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==}
engines: {node: '>=8.6'}
dependencies:
ansi-colors: 4.1.3
dev: true
- /entities/2.2.0:
+ /entities@2.2.0:
resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==}
dev: true
- /entities/4.4.0:
- resolution: {integrity: sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==}
+ /entities@4.5.0:
+ resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'}
dev: true
- /envinfo/7.8.1:
+ /envinfo@7.8.1:
resolution: {integrity: sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==}
engines: {node: '>=4'}
- hasBin: true
- dev: true
-
- /enzyme-adapter-preact-pure/3.4.0_iis6uhuvbdn4qfhp3h7qledc2m:
- resolution: {integrity: sha512-bP7HX8l5xoBG8d/nYZVIQgm2LFJWNHEl1+xfdQj02tYuveHg/C/iLtjzF/89LjgyTAhUEczGiWuXfn4/KhJeCw==}
- peerDependencies:
- enzyme: ^3.11.0
- preact: ^10.0.0
- dependencies:
- array.prototype.flatmap: 1.3.0
- enzyme: 3.11.0
- preact: 10.11.2
- dev: true
-
- /enzyme-shallow-equal/1.0.4:
- resolution: {integrity: sha512-MttIwB8kKxypwHvRynuC3ahyNc+cFbR8mjVIltnmzQ0uKGqmsfO4bfBuLxb0beLNPhjblUEYvEbsg+VSygvF1Q==}
- dependencies:
- has: 1.0.3
- object-is: 1.1.5
dev: true
- /enzyme/3.11.0:
- resolution: {integrity: sha512-Dw8/Gs4vRjxY6/6i9wU0V+utmQO9kvh9XLnz3LIudviOnVYDEe2ec+0k+NQoMamn1VrjKgCUOWj5jG/5M5M0Qw==}
- dependencies:
- array.prototype.flat: 1.3.0
- cheerio: 1.0.0-rc.12
- enzyme-shallow-equal: 1.0.4
- function.prototype.name: 1.1.5
- has: 1.0.3
- html-element-map: 1.3.1
- is-boolean-object: 1.1.2
- is-callable: 1.2.7
- is-number-object: 1.0.7
- is-regex: 1.1.4
- is-string: 1.0.7
- is-subset: 0.1.1
- lodash.escape: 4.0.1
- lodash.isequal: 4.5.0
- object-inspect: 1.12.2
- object-is: 1.1.5
- object.assign: 4.1.4
- object.entries: 1.1.5
- object.values: 1.1.5
- raf: 3.4.1
- rst-selector-parser: 2.2.3
- string.prototype.trim: 1.2.6
- dev: true
-
- /errno/0.1.8:
+ /errno@0.1.8:
resolution: {integrity: sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==}
hasBin: true
dependencies:
prr: 1.0.1
dev: true
- /error-ex/1.3.2:
+ /error-ex@1.3.2:
resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
dependencies:
is-arrayish: 0.2.1
dev: true
- /es-abstract/1.20.4:
- resolution: {integrity: sha512-0UtvRN79eMe2L+UNEF1BwRe364sj/DXhQ/k5FmivgoSdpM90b8Jc0mDzKMGo7QS0BVbOP/bTwBKNnDc9rNzaPA==}
+ /es-abstract@1.22.3:
+ resolution: {integrity: sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==}
engines: {node: '>= 0.4'}
dependencies:
- call-bind: 1.0.2
+ array-buffer-byte-length: 1.0.0
+ arraybuffer.prototype.slice: 1.0.2
+ available-typed-arrays: 1.0.5
+ call-bind: 1.0.5
+ es-set-tostringtag: 2.0.2
es-to-primitive: 1.2.1
- function-bind: 1.1.1
- function.prototype.name: 1.1.5
- get-intrinsic: 1.1.3
+ function.prototype.name: 1.1.6
+ get-intrinsic: 1.2.2
get-symbol-description: 1.0.0
- has: 1.0.3
- has-property-descriptors: 1.0.0
+ globalthis: 1.0.3
+ gopd: 1.0.1
+ has-property-descriptors: 1.0.1
+ has-proto: 1.0.1
has-symbols: 1.0.3
- internal-slot: 1.0.3
+ hasown: 2.0.0
+ internal-slot: 1.0.6
+ is-array-buffer: 3.0.2
is-callable: 1.2.7
is-negative-zero: 2.0.2
is-regex: 1.1.4
is-shared-array-buffer: 1.0.2
is-string: 1.0.7
+ is-typed-array: 1.1.12
is-weakref: 1.0.2
- object-inspect: 1.12.2
+ object-inspect: 1.13.1
object-keys: 1.1.1
- object.assign: 4.1.4
- regexp.prototype.flags: 1.4.3
+ object.assign: 4.1.5
+ regexp.prototype.flags: 1.5.1
+ safe-array-concat: 1.0.1
safe-regex-test: 1.0.0
- string.prototype.trimend: 1.0.5
- string.prototype.trimstart: 1.0.5
+ string.prototype.trim: 1.2.8
+ string.prototype.trimend: 1.0.7
+ string.prototype.trimstart: 1.0.7
+ typed-array-buffer: 1.0.0
+ typed-array-byte-length: 1.0.0
+ typed-array-byte-offset: 1.0.0
+ typed-array-length: 1.0.4
unbox-primitive: 1.0.2
+ which-typed-array: 1.1.13
dev: true
- /es-array-method-boxes-properly/1.0.0:
+ /es-array-method-boxes-properly@1.0.0:
resolution: {integrity: sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==}
dev: true
- /es-get-iterator/1.1.2:
- resolution: {integrity: sha512-+DTO8GYwbMCwbywjimwZMHp8AuYXOS2JZFWoi2AlPOS3ebnII9w/NLpNZtA7A0YLaVDw+O7KFCeoIV7OPvM7hQ==}
+ /es-iterator-helpers@1.0.15:
+ resolution: {integrity: sha512-GhoY8uYqd6iwUl2kgjTm4CZAf6oo5mHK7BPqx3rKgx893YSsy0LGHV6gfqqQvZt/8xM8xeOnfXBCfqclMKkJ5g==}
dependencies:
- call-bind: 1.0.2
- get-intrinsic: 1.1.3
+ asynciterator.prototype: 1.0.0
+ call-bind: 1.0.5
+ define-properties: 1.2.1
+ es-abstract: 1.22.3
+ es-set-tostringtag: 2.0.2
+ function-bind: 1.1.2
+ get-intrinsic: 1.2.2
+ globalthis: 1.0.3
+ has-property-descriptors: 1.0.1
+ has-proto: 1.0.1
has-symbols: 1.0.3
- is-arguments: 1.1.1
- is-map: 2.0.2
- is-set: 2.0.2
- is-string: 1.0.7
- isarray: 2.0.5
+ internal-slot: 1.0.6
+ iterator.prototype: 1.1.2
+ safe-array-concat: 1.0.1
dev: true
- /es-module-lexer/0.9.3:
- resolution: {integrity: sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==}
+ /es-set-tostringtag@2.0.2:
+ resolution: {integrity: sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ get-intrinsic: 1.2.2
+ has-tostringtag: 1.0.0
+ hasown: 2.0.0
dev: true
- /es-shim-unscopables/1.0.0:
- resolution: {integrity: sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==}
+ /es-shim-unscopables@1.0.2:
+ resolution: {integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==}
dependencies:
- has: 1.0.3
+ hasown: 2.0.0
dev: true
- /es-to-primitive/1.2.1:
+ /es-to-primitive@1.2.1:
resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==}
engines: {node: '>= 0.4'}
dependencies:
@@ -11648,692 +10250,91 @@ packages:
is-symbol: 1.0.4
dev: true
- /es5-shim/4.6.7:
- resolution: {integrity: sha512-jg21/dmlrNQI7JyyA2w7n+yifSxBng0ZralnSfVZjoCawgNTCnS+yBCyVM9DL5itm7SUnDGgv7hcq2XCZX4iRQ==}
- engines: {node: '>=0.4.0'}
- dev: true
-
- /es6-error/4.1.1:
+ /es6-error@4.1.1:
resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==}
dev: true
- /es6-shim/0.35.6:
- resolution: {integrity: sha512-EmTr31wppcaIAgblChZiuN/l9Y7DPyw8Xtbg7fIVngn6zMW+IEBJDJngeKC3x6wr0V/vcA2wqeFnaw1bFJbDdA==}
- dev: true
-
- /esbuild-android-64/0.14.54:
- resolution: {integrity: sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==}
- engines: {node: '>=12'}
- cpu: [x64]
- os: [android]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-android-64/0.15.12:
- resolution: {integrity: sha512-MJKXwvPY9g0rGps0+U65HlTsM1wUs9lbjt5CU19RESqycGFDRijMDQsh68MtbzkqWSRdEtiKS1mtPzKneaAI0Q==}
- engines: {node: '>=12'}
- cpu: [x64]
- os: [android]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-android-64/0.15.13:
- resolution: {integrity: sha512-yRorukXBlokwTip+Sy4MYskLhJsO0Kn0/Fj43s1krVblfwP+hMD37a4Wmg139GEsMLl+vh8WXp2mq/cTA9J97g==}
- engines: {node: '>=12'}
- cpu: [x64]
- os: [android]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-android-arm64/0.14.54:
- resolution: {integrity: sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==}
- engines: {node: '>=12'}
- cpu: [arm64]
- os: [android]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-android-arm64/0.15.12:
- resolution: {integrity: sha512-Hc9SEcZbIMhhLcvhr1DH+lrrec9SFTiRzfJ7EGSBZiiw994gfkVV6vG0sLWqQQ6DD7V4+OggB+Hn0IRUdDUqvA==}
- engines: {node: '>=12'}
- cpu: [arm64]
- os: [android]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-android-arm64/0.15.13:
- resolution: {integrity: sha512-TKzyymLD6PiVeyYa4c5wdPw87BeAiTXNtK6amWUcXZxkV51gOk5u5qzmDaYSwiWeecSNHamFsaFjLoi32QR5/w==}
- engines: {node: '>=12'}
- cpu: [arm64]
- os: [android]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-darwin-64/0.14.54:
- resolution: {integrity: sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug==}
- engines: {node: '>=12'}
- cpu: [x64]
- os: [darwin]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-darwin-64/0.15.12:
- resolution: {integrity: sha512-qkmqrTVYPFiePt5qFjP8w/S+GIUMbt6k8qmiPraECUWfPptaPJUGkCKrWEfYFRWB7bY23FV95rhvPyh/KARP8Q==}
- engines: {node: '>=12'}
- cpu: [x64]
- os: [darwin]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-darwin-64/0.15.13:
- resolution: {integrity: sha512-WAx7c2DaOS6CrRcoYCgXgkXDliLnFv3pQLV6GeW1YcGEZq2Gnl8s9Pg7ahValZkpOa0iE/ojRVQ87sbUhF1Cbg==}
- engines: {node: '>=12'}
- cpu: [x64]
- os: [darwin]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-darwin-arm64/0.14.54:
- resolution: {integrity: sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==}
- engines: {node: '>=12'}
- cpu: [arm64]
- os: [darwin]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-darwin-arm64/0.15.12:
- resolution: {integrity: sha512-z4zPX02tQ41kcXMyN3c/GfZpIjKoI/BzHrdKUwhC/Ki5BAhWv59A9M8H+iqaRbwpzYrYidTybBwiZAIWCLJAkw==}
- engines: {node: '>=12'}
- cpu: [arm64]
- os: [darwin]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-darwin-arm64/0.15.13:
- resolution: {integrity: sha512-U6jFsPfSSxC3V1CLiQqwvDuj3GGrtQNB3P3nNC3+q99EKf94UGpsG9l4CQ83zBs1NHrk1rtCSYT0+KfK5LsD8A==}
- engines: {node: '>=12'}
- cpu: [arm64]
- os: [darwin]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-freebsd-64/0.14.54:
- resolution: {integrity: sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==}
- engines: {node: '>=12'}
- cpu: [x64]
- os: [freebsd]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-freebsd-64/0.15.12:
- resolution: {integrity: sha512-XFL7gKMCKXLDiAiBjhLG0XECliXaRLTZh6hsyzqUqPUf/PY4C6EJDTKIeqqPKXaVJ8+fzNek88285krSz1QECw==}
- engines: {node: '>=12'}
- cpu: [x64]
- os: [freebsd]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-freebsd-64/0.15.13:
- resolution: {integrity: sha512-whItJgDiOXaDG/idy75qqevIpZjnReZkMGCgQaBWZuKHoElDJC1rh7MpoUgupMcdfOd+PgdEwNQW9DAE6i8wyA==}
- engines: {node: '>=12'}
- cpu: [x64]
- os: [freebsd]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-freebsd-arm64/0.14.54:
- resolution: {integrity: sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==}
- engines: {node: '>=12'}
- cpu: [arm64]
- os: [freebsd]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-freebsd-arm64/0.15.12:
- resolution: {integrity: sha512-jwEIu5UCUk6TjiG1X+KQnCGISI+ILnXzIzt9yDVrhjug2fkYzlLbl0K43q96Q3KB66v6N1UFF0r5Ks4Xo7i72g==}
- engines: {node: '>=12'}
- cpu: [arm64]
- os: [freebsd]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-freebsd-arm64/0.15.13:
- resolution: {integrity: sha512-6pCSWt8mLUbPtygv7cufV0sZLeylaMwS5Fznj6Rsx9G2AJJsAjQ9ifA+0rQEIg7DwJmi9it+WjzNTEAzzdoM3Q==}
- engines: {node: '>=12'}
- cpu: [arm64]
- os: [freebsd]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-linux-32/0.14.54:
- resolution: {integrity: sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==}
- engines: {node: '>=12'}
- cpu: [ia32]
- os: [linux]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-linux-32/0.15.12:
- resolution: {integrity: sha512-uSQuSEyF1kVzGzuIr4XM+v7TPKxHjBnLcwv2yPyCz8riV8VUCnO/C4BF3w5dHiVpCd5Z1cebBtZJNlC4anWpwA==}
- engines: {node: '>=12'}
- cpu: [ia32]
- os: [linux]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-linux-32/0.15.13:
- resolution: {integrity: sha512-VbZdWOEdrJiYApm2kkxoTOgsoCO1krBZ3quHdYk3g3ivWaMwNIVPIfEE0f0XQQ0u5pJtBsnk2/7OPiCFIPOe/w==}
- engines: {node: '>=12'}
- cpu: [ia32]
- os: [linux]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-linux-64/0.14.54:
- resolution: {integrity: sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==}
- engines: {node: '>=12'}
- cpu: [x64]
- os: [linux]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-linux-64/0.15.12:
- resolution: {integrity: sha512-QcgCKb7zfJxqT9o5z9ZUeGH1k8N6iX1Y7VNsEi5F9+HzN1OIx7ESxtQXDN9jbeUSPiRH1n9cw6gFT3H4qbdvcA==}
- engines: {node: '>=12'}
- cpu: [x64]
- os: [linux]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-linux-64/0.15.13:
- resolution: {integrity: sha512-rXmnArVNio6yANSqDQlIO4WiP+Cv7+9EuAHNnag7rByAqFVuRusLbGi2697A5dFPNXoO//IiogVwi3AdcfPC6A==}
- engines: {node: '>=12'}
- cpu: [x64]
- os: [linux]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-linux-arm/0.14.54:
- resolution: {integrity: sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==}
- engines: {node: '>=12'}
- cpu: [arm]
- os: [linux]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-linux-arm/0.15.12:
- resolution: {integrity: sha512-Wf7T0aNylGcLu7hBnzMvsTfEXdEdJY/hY3u36Vla21aY66xR0MS5I1Hw8nVquXjTN0A6fk/vnr32tkC/C2lb0A==}
- engines: {node: '>=12'}
- cpu: [arm]
- os: [linux]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-linux-arm/0.15.13:
- resolution: {integrity: sha512-Ac6LpfmJO8WhCMQmO253xX2IU2B3wPDbl4IvR0hnqcPrdfCaUa2j/lLMGTjmQ4W5JsJIdHEdW12dG8lFS0MbxQ==}
- engines: {node: '>=12'}
- cpu: [arm]
- os: [linux]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-linux-arm64/0.14.54:
- resolution: {integrity: sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==}
- engines: {node: '>=12'}
- cpu: [arm64]
- os: [linux]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-linux-arm64/0.15.12:
- resolution: {integrity: sha512-HtNq5xm8fUpZKwWKS2/YGwSfTF+339L4aIA8yphNKYJckd5hVdhfdl6GM2P3HwLSCORS++++7++//ApEwXEuAQ==}
- engines: {node: '>=12'}
- cpu: [arm64]
- os: [linux]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-linux-arm64/0.15.13:
- resolution: {integrity: sha512-alEMGU4Z+d17U7KQQw2IV8tQycO6T+rOrgW8OS22Ua25x6kHxoG6Ngry6Aq6uranC+pNWNMB6aHFPh7aTQdORQ==}
- engines: {node: '>=12'}
- cpu: [arm64]
- os: [linux]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-linux-mips64le/0.14.54:
- resolution: {integrity: sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==}
- engines: {node: '>=12'}
- cpu: [mips64el]
- os: [linux]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-linux-mips64le/0.15.12:
- resolution: {integrity: sha512-Qol3+AvivngUZkTVFgLpb0H6DT+N5/zM3V1YgTkryPYFeUvuT5JFNDR3ZiS6LxhyF8EE+fiNtzwlPqMDqVcc6A==}
- engines: {node: '>=12'}
- cpu: [mips64el]
- os: [linux]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-linux-mips64le/0.15.13:
- resolution: {integrity: sha512-47PgmyYEu+yN5rD/MbwS6DxP2FSGPo4Uxg5LwIdxTiyGC2XKwHhHyW7YYEDlSuXLQXEdTO7mYe8zQ74czP7W8A==}
- engines: {node: '>=12'}
- cpu: [mips64el]
- os: [linux]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-linux-ppc64le/0.14.54:
- resolution: {integrity: sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==}
- engines: {node: '>=12'}
- cpu: [ppc64]
- os: [linux]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-linux-ppc64le/0.15.12:
- resolution: {integrity: sha512-4D8qUCo+CFKaR0cGXtGyVsOI7w7k93Qxb3KFXWr75An0DHamYzq8lt7TNZKoOq/Gh8c40/aKaxvcZnTgQ0TJNg==}
- engines: {node: '>=12'}
- cpu: [ppc64]
- os: [linux]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-linux-ppc64le/0.15.13:
- resolution: {integrity: sha512-z6n28h2+PC1Ayle9DjKoBRcx/4cxHoOa2e689e2aDJSaKug3jXcQw7mM+GLg+9ydYoNzj8QxNL8ihOv/OnezhA==}
- engines: {node: '>=12'}
- cpu: [ppc64]
- os: [linux]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-linux-riscv64/0.14.54:
- resolution: {integrity: sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==}
- engines: {node: '>=12'}
- cpu: [riscv64]
- os: [linux]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-linux-riscv64/0.15.12:
- resolution: {integrity: sha512-G9w6NcuuCI6TUUxe6ka0enjZHDnSVK8bO+1qDhMOCtl7Tr78CcZilJj8SGLN00zO5iIlwNRZKHjdMpfFgNn1VA==}
- engines: {node: '>=12'}
- cpu: [riscv64]
- os: [linux]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-linux-riscv64/0.15.13:
- resolution: {integrity: sha512-+Lu4zuuXuQhgLUGyZloWCqTslcCAjMZH1k3Xc9MSEJEpEFdpsSU0sRDXAnk18FKOfEjhu4YMGaykx9xjtpA6ow==}
- engines: {node: '>=12'}
- cpu: [riscv64]
- os: [linux]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-linux-s390x/0.14.54:
- resolution: {integrity: sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==}
- engines: {node: '>=12'}
- cpu: [s390x]
- os: [linux]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-linux-s390x/0.15.12:
- resolution: {integrity: sha512-Lt6BDnuXbXeqSlVuuUM5z18GkJAZf3ERskGZbAWjrQoi9xbEIsj/hEzVnSAFLtkfLuy2DE4RwTcX02tZFunXww==}
- engines: {node: '>=12'}
- cpu: [s390x]
- os: [linux]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-linux-s390x/0.15.13:
- resolution: {integrity: sha512-BMeXRljruf7J0TMxD5CIXS65y7puiZkAh+s4XFV9qy16SxOuMhxhVIXYLnbdfLrsYGFzx7U9mcdpFWkkvy/Uag==}
- engines: {node: '>=12'}
- cpu: [s390x]
- os: [linux]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-netbsd-64/0.14.54:
- resolution: {integrity: sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==}
- engines: {node: '>=12'}
- cpu: [x64]
- os: [netbsd]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-netbsd-64/0.15.12:
- resolution: {integrity: sha512-jlUxCiHO1dsqoURZDQts+HK100o0hXfi4t54MNRMCAqKGAV33JCVvMplLAa2FwviSojT/5ZG5HUfG3gstwAG8w==}
- engines: {node: '>=12'}
- cpu: [x64]
- os: [netbsd]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-netbsd-64/0.15.13:
- resolution: {integrity: sha512-EHj9QZOTel581JPj7UO3xYbltFTYnHy+SIqJVq6yd3KkCrsHRbapiPb0Lx3EOOtybBEE9EyqbmfW1NlSDsSzvQ==}
- engines: {node: '>=12'}
- cpu: [x64]
- os: [netbsd]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-openbsd-64/0.14.54:
- resolution: {integrity: sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==}
- engines: {node: '>=12'}
- cpu: [x64]
- os: [openbsd]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-openbsd-64/0.15.12:
- resolution: {integrity: sha512-1o1uAfRTMIWNOmpf8v7iudND0L6zRBYSH45sofCZywrcf7NcZA+c7aFsS1YryU+yN7aRppTqdUK1PgbZVaB1Dw==}
- engines: {node: '>=12'}
- cpu: [x64]
- os: [openbsd]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-openbsd-64/0.15.13:
- resolution: {integrity: sha512-nkuDlIjF/sfUhfx8SKq0+U+Fgx5K9JcPq1mUodnxI0x4kBdCv46rOGWbuJ6eof2n3wdoCLccOoJAbg9ba/bT2w==}
- engines: {node: '>=12'}
- cpu: [x64]
- os: [openbsd]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-sunos-64/0.14.54:
- resolution: {integrity: sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==}
- engines: {node: '>=12'}
- cpu: [x64]
- os: [sunos]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-sunos-64/0.15.12:
- resolution: {integrity: sha512-nkl251DpoWoBO9Eq9aFdoIt2yYmp4I3kvQjba3jFKlMXuqQ9A4q+JaqdkCouG3DHgAGnzshzaGu6xofGcXyPXg==}
- engines: {node: '>=12'}
- cpu: [x64]
- os: [sunos]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-sunos-64/0.15.13:
- resolution: {integrity: sha512-jVeu2GfxZQ++6lRdY43CS0Tm/r4WuQQ0Pdsrxbw+aOrHQPHV0+LNOLnvbN28M7BSUGnJnHkHm2HozGgNGyeIRw==}
- engines: {node: '>=12'}
- cpu: [x64]
- os: [sunos]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-windows-32/0.14.54:
- resolution: {integrity: sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==}
- engines: {node: '>=12'}
- cpu: [ia32]
- os: [win32]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-windows-32/0.15.12:
- resolution: {integrity: sha512-WlGeBZHgPC00O08luIp5B2SP4cNCp/PcS+3Pcg31kdcJPopHxLkdCXtadLU9J82LCfw4TVls21A6lilQ9mzHrw==}
- engines: {node: '>=12'}
- cpu: [ia32]
- os: [win32]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-windows-32/0.15.13:
- resolution: {integrity: sha512-XoF2iBf0wnqo16SDq+aDGi/+QbaLFpkiRarPVssMh9KYbFNCqPLlGAWwDvxEVz+ywX6Si37J2AKm+AXq1kC0JA==}
- engines: {node: '>=12'}
- cpu: [ia32]
- os: [win32]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-windows-64/0.14.54:
- resolution: {integrity: sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==}
- engines: {node: '>=12'}
- cpu: [x64]
- os: [win32]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-windows-64/0.15.12:
- resolution: {integrity: sha512-VActO3WnWZSN//xjSfbiGOSyC+wkZtI8I4KlgrTo5oHJM6z3MZZBCuFaZHd8hzf/W9KPhF0lY8OqlmWC9HO5AA==}
- engines: {node: '>=12'}
- cpu: [x64]
- os: [win32]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-windows-64/0.15.13:
- resolution: {integrity: sha512-Et6htEfGycjDrtqb2ng6nT+baesZPYQIW+HUEHK4D1ncggNrDNk3yoboYQ5KtiVrw/JaDMNttz8rrPubV/fvPQ==}
- engines: {node: '>=12'}
- cpu: [x64]
- os: [win32]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-windows-arm64/0.14.54:
- resolution: {integrity: sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==}
- engines: {node: '>=12'}
- cpu: [arm64]
- os: [win32]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-windows-arm64/0.15.12:
- resolution: {integrity: sha512-Of3MIacva1OK/m4zCNIvBfz8VVROBmQT+gRX6pFTLPngFYcj6TFH/12VveAqq1k9VB2l28EoVMNMUCcmsfwyuA==}
- engines: {node: '>=12'}
- cpu: [arm64]
- os: [win32]
- requiresBuild: true
- dev: true
- optional: true
-
- /esbuild-windows-arm64/0.15.13:
- resolution: {integrity: sha512-3bv7tqntThQC9SWLRouMDmZnlOukBhOCTlkzNqzGCmrkCJI7io5LLjwJBOVY6kOUlIvdxbooNZwjtBvj+7uuVg==}
- engines: {node: '>=12'}
- cpu: [arm64]
- os: [win32]
- requiresBuild: true
+ /es6-promisify@7.0.0:
+ resolution: {integrity: sha512-ginqzK3J90Rd4/Yz7qRrqUeIpe3TwSXTPPZtPne7tGBPeAaQiU8qt4fpKApnxHcq1AwtUdHVg5P77x/yrggG8Q==}
+ engines: {node: '>=6'}
dev: true
- optional: true
- /esbuild/0.12.29:
+ /esbuild@0.12.29:
resolution: {integrity: sha512-w/XuoBCSwepyiZtIRsKsetiLDUVGPVw1E/R3VTFSecIy8UR7Cq3SOtwKHJMFoVqqVG36aGkzh4e8BvpO1Fdc7g==}
hasBin: true
requiresBuild: true
dev: true
- /esbuild/0.14.54:
- resolution: {integrity: sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==}
- engines: {node: '>=12'}
- hasBin: true
- requiresBuild: true
- optionalDependencies:
- '@esbuild/linux-loong64': 0.14.54
- esbuild-android-64: 0.14.54
- esbuild-android-arm64: 0.14.54
- esbuild-darwin-64: 0.14.54
- esbuild-darwin-arm64: 0.14.54
- esbuild-freebsd-64: 0.14.54
- esbuild-freebsd-arm64: 0.14.54
- esbuild-linux-32: 0.14.54
- esbuild-linux-64: 0.14.54
- esbuild-linux-arm: 0.14.54
- esbuild-linux-arm64: 0.14.54
- esbuild-linux-mips64le: 0.14.54
- esbuild-linux-ppc64le: 0.14.54
- esbuild-linux-riscv64: 0.14.54
- esbuild-linux-s390x: 0.14.54
- esbuild-netbsd-64: 0.14.54
- esbuild-openbsd-64: 0.14.54
- esbuild-sunos-64: 0.14.54
- esbuild-windows-32: 0.14.54
- esbuild-windows-64: 0.14.54
- esbuild-windows-arm64: 0.14.54
- dev: true
-
- /esbuild/0.15.12:
- resolution: {integrity: sha512-PcT+/wyDqJQsRVhaE9uX/Oq4XLrFh0ce/bs2TJh4CSaw9xuvI+xFrH2nAYOADbhQjUgAhNWC5LKoUsakm4dxng==}
- engines: {node: '>=12'}
- hasBin: true
- requiresBuild: true
- optionalDependencies:
- '@esbuild/android-arm': 0.15.12
- '@esbuild/linux-loong64': 0.15.12
- esbuild-android-64: 0.15.12
- esbuild-android-arm64: 0.15.12
- esbuild-darwin-64: 0.15.12
- esbuild-darwin-arm64: 0.15.12
- esbuild-freebsd-64: 0.15.12
- esbuild-freebsd-arm64: 0.15.12
- esbuild-linux-32: 0.15.12
- esbuild-linux-64: 0.15.12
- esbuild-linux-arm: 0.15.12
- esbuild-linux-arm64: 0.15.12
- esbuild-linux-mips64le: 0.15.12
- esbuild-linux-ppc64le: 0.15.12
- esbuild-linux-riscv64: 0.15.12
- esbuild-linux-s390x: 0.15.12
- esbuild-netbsd-64: 0.15.12
- esbuild-openbsd-64: 0.15.12
- esbuild-sunos-64: 0.15.12
- esbuild-windows-32: 0.15.12
- esbuild-windows-64: 0.15.12
- esbuild-windows-arm64: 0.15.12
- dev: true
-
- /esbuild/0.15.13:
- resolution: {integrity: sha512-Cu3SC84oyzzhrK/YyN4iEVy2jZu5t2fz66HEOShHURcjSkOSAVL8C/gfUT+lDJxkVHpg8GZ10DD0rMHRPqMFaQ==}
+ /esbuild@0.19.9:
+ resolution: {integrity: sha512-U9CHtKSy+EpPsEBa+/A2gMs/h3ylBC0H0KSqIg7tpztHerLi6nrrcoUJAkNCEPumx8yJ+Byic4BVwHgRbN0TBg==}
engines: {node: '>=12'}
hasBin: true
requiresBuild: true
optionalDependencies:
- '@esbuild/android-arm': 0.15.13
- '@esbuild/linux-loong64': 0.15.13
- esbuild-android-64: 0.15.13
- esbuild-android-arm64: 0.15.13
- esbuild-darwin-64: 0.15.13
- esbuild-darwin-arm64: 0.15.13
- esbuild-freebsd-64: 0.15.13
- esbuild-freebsd-arm64: 0.15.13
- esbuild-linux-32: 0.15.13
- esbuild-linux-64: 0.15.13
- esbuild-linux-arm: 0.15.13
- esbuild-linux-arm64: 0.15.13
- esbuild-linux-mips64le: 0.15.13
- esbuild-linux-ppc64le: 0.15.13
- esbuild-linux-riscv64: 0.15.13
- esbuild-linux-s390x: 0.15.13
- esbuild-netbsd-64: 0.15.13
- esbuild-openbsd-64: 0.15.13
- esbuild-sunos-64: 0.15.13
- esbuild-windows-32: 0.15.13
- esbuild-windows-64: 0.15.13
- esbuild-windows-arm64: 0.15.13
- dev: true
-
- /escalade/3.1.1:
+ '@esbuild/android-arm': 0.19.9
+ '@esbuild/android-arm64': 0.19.9
+ '@esbuild/android-x64': 0.19.9
+ '@esbuild/darwin-arm64': 0.19.9
+ '@esbuild/darwin-x64': 0.19.9
+ '@esbuild/freebsd-arm64': 0.19.9
+ '@esbuild/freebsd-x64': 0.19.9
+ '@esbuild/linux-arm': 0.19.9
+ '@esbuild/linux-arm64': 0.19.9
+ '@esbuild/linux-ia32': 0.19.9
+ '@esbuild/linux-loong64': 0.19.9
+ '@esbuild/linux-mips64el': 0.19.9
+ '@esbuild/linux-ppc64': 0.19.9
+ '@esbuild/linux-riscv64': 0.19.9
+ '@esbuild/linux-s390x': 0.19.9
+ '@esbuild/linux-x64': 0.19.9
+ '@esbuild/netbsd-x64': 0.19.9
+ '@esbuild/openbsd-x64': 0.19.9
+ '@esbuild/sunos-x64': 0.19.9
+ '@esbuild/win32-arm64': 0.19.9
+ '@esbuild/win32-ia32': 0.19.9
+ '@esbuild/win32-x64': 0.19.9
+ dev: true
+
+ /escalade@3.1.1:
resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==}
engines: {node: '>=6'}
- dev: true
- /escape-goat/2.1.1:
+ /escape-goat@2.1.1:
resolution: {integrity: sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==}
engines: {node: '>=8'}
dev: true
- /escape-html/1.0.3:
+ /escape-goat@4.0.0:
+ resolution: {integrity: sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==}
+ engines: {node: '>=12'}
+ dev: true
+
+ /escape-html@1.0.3:
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
dev: true
- /escape-string-regexp/1.0.5:
+ /escape-string-regexp@1.0.5:
resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
engines: {node: '>=0.8.0'}
- dev: true
- /escape-string-regexp/2.0.0:
+ /escape-string-regexp@2.0.0:
resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==}
engines: {node: '>=8'}
dev: true
- /escape-string-regexp/4.0.0:
+ /escape-string-regexp@4.0.0:
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
engines: {node: '>=10'}
dev: true
- /escape-string-regexp/5.0.0:
+ /escape-string-regexp@5.0.0:
resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==}
engines: {node: '>=12'}
dev: true
- /escodegen/1.14.3:
+ /escodegen@1.14.3:
resolution: {integrity: sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==}
engines: {node: '>=4.0'}
- hasBin: true
dependencies:
esprima: 4.0.1
estraverse: 4.3.0
@@ -12343,20 +10344,7 @@ packages:
source-map: 0.6.1
dev: true
- /escodegen/2.0.0:
- resolution: {integrity: sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==}
- engines: {node: '>=6.0'}
- hasBin: true
- dependencies:
- esprima: 4.0.1
- estraverse: 5.3.0
- esutils: 2.0.3
- optionator: 0.8.3
- optionalDependencies:
- source-map: 0.6.1
- dev: true
-
- /eslint-config-airbnb-base/15.0.0_mynvxvmq5qtyojffiqgev4x7mm:
+ /eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.29.1)(eslint@8.26.0):
resolution: {integrity: sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==}
engines: {node: ^10.12.0 || >=12.0.0}
peerDependencies:
@@ -12365,42 +10353,42 @@ packages:
dependencies:
confusing-browser-globals: 1.0.11
eslint: 8.26.0
- eslint-plugin-import: 2.26.0_c2flhriocdzler6lrwbyxxyoca
- object.assign: 4.1.4
- object.entries: 1.1.5
- semver: 6.3.0
+ eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.41.0)(eslint@8.26.0)
+ object.assign: 4.1.5
+ object.entries: 1.1.7
+ semver: 6.3.1
dev: true
- /eslint-config-airbnb-typescript/16.2.0_yxexh7lkdp6zshkc5735fso3gu:
- resolution: {integrity: sha512-OUaMPZpTOZGKd5tXOjJ9PRU4iYNW/Z5DoHIynjsVK/FpkWdiY5+nxQW6TiJAlLwVI1l53xUOrnlZWtVBVQzuWA==}
+ /eslint-config-airbnb-typescript@17.1.0(@typescript-eslint/eslint-plugin@5.41.0)(@typescript-eslint/parser@5.41.0)(eslint-plugin-import@2.29.1)(eslint@8.26.0):
+ resolution: {integrity: sha512-GPxI5URre6dDpJ0CtcthSZVBAfI+Uw7un5OYNVxP2EYi3H81Jw701yFP7AU+/vCE7xBtFmjge7kfhhk4+RAiig==}
peerDependencies:
- '@typescript-eslint/eslint-plugin': ^5.0.0
- '@typescript-eslint/parser': ^5.0.0
+ '@typescript-eslint/eslint-plugin': ^5.13.0 || ^6.0.0
+ '@typescript-eslint/parser': ^5.0.0 || ^6.0.0
eslint: ^7.32.0 || ^8.2.0
eslint-plugin-import: ^2.25.3
dependencies:
- '@typescript-eslint/eslint-plugin': 5.41.0_huremdigmcnkianavgfk3x6iou
- '@typescript-eslint/parser': 5.41.0_wyqvi574yv7oiwfeinomdzmc3m
+ '@typescript-eslint/eslint-plugin': 5.41.0(@typescript-eslint/parser@5.41.0)(eslint@8.26.0)(typescript@5.3.3)
+ '@typescript-eslint/parser': 5.41.0(eslint@8.26.0)(typescript@5.3.3)
eslint: 8.26.0
- eslint-config-airbnb-base: 15.0.0_mynvxvmq5qtyojffiqgev4x7mm
- eslint-plugin-import: 2.26.0_c2flhriocdzler6lrwbyxxyoca
+ eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.29.1)(eslint@8.26.0)
+ eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.41.0)(eslint@8.26.0)
dev: true
- /eslint-config-preact/1.3.0_55vw575o5aj4h37h7cossdtfje:
+ /eslint-config-preact@1.3.0(@typescript-eslint/eslint-plugin@4.33.0)(eslint@7.32.0)(typescript@5.3.3):
resolution: {integrity: sha512-yHYXg5qNzEJd3D/30AmsIW0W8MuY858KpApXp7xxBF08IYUljSKCOqMx+dVucXHQnAm7+11wOnMkgVHIBAechw==}
peerDependencies:
eslint: 6.x || 7.x || 8.x
dependencies:
'@babel/core': 7.18.9
- '@babel/eslint-parser': 7.19.1_o5peei4wpze5egwf42u76kwdva
- '@babel/plugin-syntax-class-properties': 7.12.13_@babel+core@7.18.9
- '@babel/plugin-syntax-decorators': 7.19.0_@babel+core@7.18.9
- '@babel/plugin-syntax-jsx': 7.18.6_@babel+core@7.18.9
+ '@babel/eslint-parser': 7.19.1(@babel/core@7.18.9)(eslint@7.32.0)
+ '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.18.9)
+ '@babel/plugin-syntax-decorators': 7.19.0(@babel/core@7.18.9)
+ '@babel/plugin-syntax-jsx': 7.21.4(@babel/core@7.18.9)
eslint: 7.32.0
- eslint-plugin-compat: 4.0.2_eslint@7.32.0
- eslint-plugin-jest: 25.7.0_55vw575o5aj4h37h7cossdtfje
- eslint-plugin-react: 7.31.10_eslint@7.32.0
- eslint-plugin-react-hooks: 4.6.0_eslint@7.32.0
+ eslint-plugin-compat: 4.0.2(eslint@7.32.0)
+ eslint-plugin-jest: 25.7.0(@typescript-eslint/eslint-plugin@4.33.0)(eslint@7.32.0)(typescript@5.3.3)
+ eslint-plugin-react: 7.33.2(eslint@7.32.0)
+ eslint-plugin-react-hooks: 4.6.0(eslint@7.32.0)
transitivePeerDependencies:
- '@typescript-eslint/eslint-plugin'
- jest
@@ -12408,61 +10396,27 @@ packages:
- typescript
dev: true
- /eslint-config-preact/1.3.0_fy74h4y2g2kkrxhvsefhiowl74:
- resolution: {integrity: sha512-yHYXg5qNzEJd3D/30AmsIW0W8MuY858KpApXp7xxBF08IYUljSKCOqMx+dVucXHQnAm7+11wOnMkgVHIBAechw==}
- peerDependencies:
- eslint: 6.x || 7.x || 8.x
- dependencies:
- '@babel/core': 7.18.9
- '@babel/eslint-parser': 7.19.1_cjip4hokpjhf4bbkea6jsnplgy
- '@babel/plugin-syntax-class-properties': 7.12.13_@babel+core@7.18.9
- '@babel/plugin-syntax-decorators': 7.19.0_@babel+core@7.18.9
- '@babel/plugin-syntax-jsx': 7.18.6_@babel+core@7.18.9
- eslint: 8.26.0
- eslint-plugin-compat: 4.0.2_eslint@8.26.0
- eslint-plugin-jest: 25.7.0_fy74h4y2g2kkrxhvsefhiowl74
- eslint-plugin-react: 7.31.10_eslint@8.26.0
- eslint-plugin-react-hooks: 4.6.0_eslint@8.26.0
- transitivePeerDependencies:
- - '@typescript-eslint/eslint-plugin'
- - jest
- - supports-color
- - typescript
- dev: true
-
- /eslint-config-preact/1.3.0_nxlzr75jbqkso2fds5zjovs2ii:
- resolution: {integrity: sha512-yHYXg5qNzEJd3D/30AmsIW0W8MuY858KpApXp7xxBF08IYUljSKCOqMx+dVucXHQnAm7+11wOnMkgVHIBAechw==}
+ /eslint-config-prettier@9.1.0(eslint@8.56.0):
+ resolution: {integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==}
+ hasBin: true
peerDependencies:
- eslint: 6.x || 7.x || 8.x
+ eslint: '>=7.0.0'
dependencies:
- '@babel/core': 7.18.9
- '@babel/eslint-parser': 7.19.1_o5peei4wpze5egwf42u76kwdva
- '@babel/plugin-syntax-class-properties': 7.12.13_@babel+core@7.18.9
- '@babel/plugin-syntax-decorators': 7.19.0_@babel+core@7.18.9
- '@babel/plugin-syntax-jsx': 7.18.6_@babel+core@7.18.9
- eslint: 7.32.0
- eslint-plugin-compat: 4.0.2_eslint@7.32.0
- eslint-plugin-jest: 25.7.0_nxlzr75jbqkso2fds5zjovs2ii
- eslint-plugin-react: 7.31.10_eslint@7.32.0
- eslint-plugin-react-hooks: 4.6.0_eslint@7.32.0
- transitivePeerDependencies:
- - '@typescript-eslint/eslint-plugin'
- - jest
- - supports-color
- - typescript
+ eslint: 8.56.0
dev: true
- /eslint-import-resolver-node/0.3.6:
- resolution: {integrity: sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==}
+ /eslint-import-resolver-node@0.3.9:
+ resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==}
dependencies:
debug: 3.2.7
- resolve: 1.22.1
+ is-core-module: 2.13.1
+ resolve: 1.22.8
transitivePeerDependencies:
- supports-color
dev: true
- /eslint-module-utils/2.7.4_pz3cez6sklduddwkjesjihuniu:
- resolution: {integrity: sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==}
+ /eslint-module-utils@2.8.0(@typescript-eslint/parser@5.41.0)(eslint-import-resolver-node@0.3.9)(eslint@8.26.0):
+ resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==}
engines: {node: '>=4'}
peerDependencies:
'@typescript-eslint/parser': '*'
@@ -12482,15 +10436,15 @@ packages:
eslint-import-resolver-webpack:
optional: true
dependencies:
- '@typescript-eslint/parser': 5.41.0_wyqvi574yv7oiwfeinomdzmc3m
+ '@typescript-eslint/parser': 5.41.0(eslint@8.26.0)(typescript@5.3.3)
debug: 3.2.7
eslint: 8.26.0
- eslint-import-resolver-node: 0.3.6
+ eslint-import-resolver-node: 0.3.9
transitivePeerDependencies:
- supports-color
dev: true
- /eslint-plugin-compat/4.0.2_eslint@7.32.0:
+ /eslint-plugin-compat@4.0.2(eslint@7.32.0):
resolution: {integrity: sha512-xqvoO54CLTVaEYGMzhu35Wzwk/As7rCvz/2dqwnFiWi0OJccEtGIn+5qq3zqIu9nboXlpdBN579fZcItC73Ycg==}
engines: {node: '>=9.x'}
peerDependencies:
@@ -12498,8 +10452,8 @@ packages:
dependencies:
'@mdn/browser-compat-data': 4.2.1
ast-metadata-inferer: 0.7.0
- browserslist: 4.21.4
- caniuse-lite: 1.0.30001425
+ browserslist: 4.22.2
+ caniuse-lite: 1.0.30001570
core-js: 3.26.0
eslint: 7.32.0
find-up: 5.0.0
@@ -12507,24 +10461,7 @@ packages:
semver: 7.3.5
dev: true
- /eslint-plugin-compat/4.0.2_eslint@8.26.0:
- resolution: {integrity: sha512-xqvoO54CLTVaEYGMzhu35Wzwk/As7rCvz/2dqwnFiWi0OJccEtGIn+5qq3zqIu9nboXlpdBN579fZcItC73Ycg==}
- engines: {node: '>=9.x'}
- peerDependencies:
- eslint: ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
- dependencies:
- '@mdn/browser-compat-data': 4.2.1
- ast-metadata-inferer: 0.7.0
- browserslist: 4.21.4
- caniuse-lite: 1.0.30001425
- core-js: 3.26.0
- eslint: 8.26.0
- find-up: 5.0.0
- lodash.memoize: 4.1.2
- semver: 7.3.5
- dev: true
-
- /eslint-plugin-header/3.1.1_eslint@7.32.0:
+ /eslint-plugin-header@3.1.1(eslint@7.32.0):
resolution: {integrity: sha512-9vlKxuJ4qf793CmeeSrZUvVClw6amtpghq3CuWcB5cUNnWHQhgcqy5eF8oVKFk1G3Y/CbchGfEaw3wiIJaNmVg==}
peerDependencies:
eslint: '>=7.7.0'
@@ -12532,8 +10469,8 @@ packages:
eslint: 7.32.0
dev: true
- /eslint-plugin-import/2.26.0_c2flhriocdzler6lrwbyxxyoca:
- resolution: {integrity: sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==}
+ /eslint-plugin-import@2.29.1(@typescript-eslint/parser@5.41.0)(eslint@8.26.0):
+ resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==}
engines: {node: '>=4'}
peerDependencies:
'@typescript-eslint/parser': '*'
@@ -12542,71 +10479,32 @@ packages:
'@typescript-eslint/parser':
optional: true
dependencies:
- '@typescript-eslint/parser': 5.41.0_wyqvi574yv7oiwfeinomdzmc3m
- array-includes: 3.1.5
- array.prototype.flat: 1.3.0
- debug: 2.6.9
+ '@typescript-eslint/parser': 5.41.0(eslint@8.26.0)(typescript@5.3.3)
+ array-includes: 3.1.7
+ array.prototype.findlastindex: 1.2.3
+ array.prototype.flat: 1.3.2
+ array.prototype.flatmap: 1.3.2
+ debug: 3.2.7
doctrine: 2.1.0
eslint: 8.26.0
- eslint-import-resolver-node: 0.3.6
- eslint-module-utils: 2.7.4_pz3cez6sklduddwkjesjihuniu
- has: 1.0.3
- is-core-module: 2.11.0
+ eslint-import-resolver-node: 0.3.9
+ eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.41.0)(eslint-import-resolver-node@0.3.9)(eslint@8.26.0)
+ hasown: 2.0.0
+ is-core-module: 2.13.1
is-glob: 4.0.3
minimatch: 3.1.2
- object.values: 1.1.5
- resolve: 1.22.1
- tsconfig-paths: 3.14.1
+ object.fromentries: 2.0.7
+ object.groupby: 1.0.1
+ object.values: 1.1.7
+ semver: 6.3.1
+ tsconfig-paths: 3.15.0
transitivePeerDependencies:
- eslint-import-resolver-typescript
- eslint-import-resolver-webpack
- supports-color
dev: true
- /eslint-plugin-jest/25.7.0_55vw575o5aj4h37h7cossdtfje:
- resolution: {integrity: sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ==}
- engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
- peerDependencies:
- '@typescript-eslint/eslint-plugin': ^4.0.0 || ^5.0.0
- eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
- jest: '*'
- peerDependenciesMeta:
- '@typescript-eslint/eslint-plugin':
- optional: true
- jest:
- optional: true
- dependencies:
- '@typescript-eslint/eslint-plugin': 4.33.0_zrqxgwgitu7trrjeml3nqco3jq
- '@typescript-eslint/experimental-utils': 5.41.0_wnilx7boviscikmvsfkd6ljepe
- eslint: 7.32.0
- jest: 26.6.3
- transitivePeerDependencies:
- - supports-color
- - typescript
- dev: true
-
- /eslint-plugin-jest/25.7.0_fy74h4y2g2kkrxhvsefhiowl74:
- resolution: {integrity: sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ==}
- engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
- peerDependencies:
- '@typescript-eslint/eslint-plugin': ^4.0.0 || ^5.0.0
- eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
- jest: '*'
- peerDependenciesMeta:
- '@typescript-eslint/eslint-plugin':
- optional: true
- jest:
- optional: true
- dependencies:
- '@typescript-eslint/eslint-plugin': 5.41.0_huremdigmcnkianavgfk3x6iou
- '@typescript-eslint/experimental-utils': 5.41.0_wyqvi574yv7oiwfeinomdzmc3m
- eslint: 8.26.0
- transitivePeerDependencies:
- - supports-color
- - typescript
- dev: true
-
- /eslint-plugin-jest/25.7.0_nxlzr75jbqkso2fds5zjovs2ii:
+ /eslint-plugin-jest@25.7.0(@typescript-eslint/eslint-plugin@4.33.0)(eslint@7.32.0)(typescript@5.3.3):
resolution: {integrity: sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ==}
engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
peerDependencies:
@@ -12619,38 +10517,48 @@ packages:
jest:
optional: true
dependencies:
- '@typescript-eslint/eslint-plugin': 4.33.0_k4l66av2tbo6kxzw52jzgbfzii
- '@typescript-eslint/experimental-utils': 5.41.0_3rubbgt5ekhqrcgx4uwls3neim
+ '@typescript-eslint/eslint-plugin': 4.33.0(@typescript-eslint/parser@4.33.0)(eslint@7.32.0)(typescript@5.3.3)
+ '@typescript-eslint/experimental-utils': 5.41.0(eslint@7.32.0)(typescript@5.3.3)
eslint: 7.32.0
- jest: 26.6.3
transitivePeerDependencies:
- supports-color
- typescript
dev: true
- /eslint-plugin-jsx-a11y/6.6.1_eslint@8.26.0:
- resolution: {integrity: sha512-sXgFVNHiWffBq23uiS/JaP6eVR622DqwB4yTzKvGZGcPq6/yZ3WmOZfuBks/vHWo9GaFOqC2ZK4i6+C35knx7Q==}
+ /eslint-plugin-jsx-a11y@6.8.0(eslint@8.26.0):
+ resolution: {integrity: sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA==}
engines: {node: '>=4.0'}
peerDependencies:
eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8
dependencies:
- '@babel/runtime': 7.19.4
- aria-query: 4.2.2
- array-includes: 3.1.5
- ast-types-flow: 0.0.7
- axe-core: 4.5.0
- axobject-query: 2.2.0
+ '@babel/runtime': 7.23.6
+ aria-query: 5.3.0
+ array-includes: 3.1.7
+ array.prototype.flatmap: 1.3.2
+ ast-types-flow: 0.0.8
+ axe-core: 4.7.0
+ axobject-query: 3.2.1
damerau-levenshtein: 1.0.8
emoji-regex: 9.2.2
+ es-iterator-helpers: 1.0.15
eslint: 8.26.0
- has: 1.0.3
- jsx-ast-utils: 3.3.3
- language-tags: 1.0.5
+ hasown: 2.0.0
+ jsx-ast-utils: 3.3.5
+ language-tags: 1.0.9
minimatch: 3.1.2
- semver: 6.3.0
+ object.entries: 1.1.7
+ object.fromentries: 2.0.7
dev: true
- /eslint-plugin-react-hooks/4.6.0_eslint@7.32.0:
+ /eslint-plugin-no-unsanitized@4.0.2(eslint@8.56.0):
+ resolution: {integrity: sha512-Pry0S9YmHoz8NCEMRQh7N0Yexh2MYCNPIlrV52hTmS7qXnTghWsjXouF08bgsrrZqaW9tt1ZiK3j5NEmPE+EjQ==}
+ peerDependencies:
+ eslint: ^6 || ^7 || ^8
+ dependencies:
+ eslint: 8.56.0
+ dev: true
+
+ /eslint-plugin-react-hooks@4.6.0(eslint@7.32.0):
resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==}
engines: {node: '>=10'}
peerDependencies:
@@ -12659,7 +10567,7 @@ packages:
eslint: 7.32.0
dev: true
- /eslint-plugin-react-hooks/4.6.0_eslint@8.26.0:
+ /eslint-plugin-react-hooks@4.6.0(eslint@8.26.0):
resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==}
engines: {node: '>=10'}
peerDependencies:
@@ -12668,53 +10576,82 @@ packages:
eslint: 8.26.0
dev: true
- /eslint-plugin-react/7.31.10_eslint@7.32.0:
- resolution: {integrity: sha512-e4N/nc6AAlg4UKW/mXeYWd3R++qUano5/o+t+wnWxIf+bLsOaH3a4q74kX3nDjYym3VBN4HyO9nEn1GcAqgQOA==}
+ /eslint-plugin-react@7.33.2(eslint@7.32.0):
+ resolution: {integrity: sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==}
engines: {node: '>=4'}
peerDependencies:
eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8
dependencies:
- array-includes: 3.1.5
- array.prototype.flatmap: 1.3.0
+ array-includes: 3.1.7
+ array.prototype.flatmap: 1.3.2
+ array.prototype.tosorted: 1.1.2
doctrine: 2.1.0
+ es-iterator-helpers: 1.0.15
eslint: 7.32.0
estraverse: 5.3.0
- jsx-ast-utils: 3.3.3
+ jsx-ast-utils: 3.3.5
minimatch: 3.1.2
- object.entries: 1.1.5
- object.fromentries: 2.0.5
- object.hasown: 1.1.1
- object.values: 1.1.5
+ object.entries: 1.1.7
+ object.fromentries: 2.0.7
+ object.hasown: 1.1.3
+ object.values: 1.1.7
prop-types: 15.8.1
- resolve: 2.0.0-next.4
- semver: 6.3.0
- string.prototype.matchall: 4.0.7
+ resolve: 2.0.0-next.5
+ semver: 6.3.1
+ string.prototype.matchall: 4.0.10
dev: true
- /eslint-plugin-react/7.31.10_eslint@8.26.0:
- resolution: {integrity: sha512-e4N/nc6AAlg4UKW/mXeYWd3R++qUano5/o+t+wnWxIf+bLsOaH3a4q74kX3nDjYym3VBN4HyO9nEn1GcAqgQOA==}
+ /eslint-plugin-react@7.33.2(eslint@8.26.0):
+ resolution: {integrity: sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==}
engines: {node: '>=4'}
peerDependencies:
eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8
dependencies:
- array-includes: 3.1.5
- array.prototype.flatmap: 1.3.0
+ array-includes: 3.1.7
+ array.prototype.flatmap: 1.3.2
+ array.prototype.tosorted: 1.1.2
doctrine: 2.1.0
+ es-iterator-helpers: 1.0.15
eslint: 8.26.0
estraverse: 5.3.0
- jsx-ast-utils: 3.3.3
+ jsx-ast-utils: 3.3.5
minimatch: 3.1.2
- object.entries: 1.1.5
- object.fromentries: 2.0.5
- object.hasown: 1.1.1
- object.values: 1.1.5
+ object.entries: 1.1.7
+ object.fromentries: 2.0.7
+ object.hasown: 1.1.3
+ object.values: 1.1.7
prop-types: 15.8.1
- resolve: 2.0.0-next.4
- semver: 6.3.0
- string.prototype.matchall: 4.0.7
+ resolve: 2.0.0-next.5
+ semver: 6.3.1
+ string.prototype.matchall: 4.0.10
dev: true
- /eslint-scope/4.0.3:
+ /eslint-plugin-react@7.33.2(eslint@8.56.0):
+ resolution: {integrity: sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==}
+ engines: {node: '>=4'}
+ peerDependencies:
+ eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8
+ dependencies:
+ array-includes: 3.1.7
+ array.prototype.flatmap: 1.3.2
+ array.prototype.tosorted: 1.1.2
+ doctrine: 2.1.0
+ es-iterator-helpers: 1.0.15
+ eslint: 8.56.0
+ estraverse: 5.3.0
+ jsx-ast-utils: 3.3.5
+ minimatch: 3.1.2
+ object.entries: 1.1.7
+ object.fromentries: 2.0.7
+ object.hasown: 1.1.3
+ object.values: 1.1.7
+ prop-types: 15.8.1
+ resolve: 2.0.0-next.5
+ semver: 6.3.1
+ string.prototype.matchall: 4.0.10
+ dev: true
+
+ /eslint-scope@4.0.3:
resolution: {integrity: sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==}
engines: {node: '>=4.0.0'}
dependencies:
@@ -12722,7 +10659,7 @@ packages:
estraverse: 4.3.0
dev: true
- /eslint-scope/5.1.1:
+ /eslint-scope@5.1.1:
resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==}
engines: {node: '>=8.0.0'}
dependencies:
@@ -12730,7 +10667,7 @@ packages:
estraverse: 4.3.0
dev: true
- /eslint-scope/7.1.1:
+ /eslint-scope@7.1.1:
resolution: {integrity: sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dependencies:
@@ -12738,14 +10675,22 @@ packages:
estraverse: 5.3.0
dev: true
- /eslint-utils/2.1.0:
+ /eslint-scope@7.2.2:
+ resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ dependencies:
+ esrecurse: 4.3.0
+ estraverse: 5.3.0
+ dev: true
+
+ /eslint-utils@2.1.0:
resolution: {integrity: sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==}
engines: {node: '>=6'}
dependencies:
eslint-visitor-keys: 1.3.0
dev: true
- /eslint-utils/3.0.0_eslint@7.32.0:
+ /eslint-utils@3.0.0(eslint@7.32.0):
resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==}
engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0}
peerDependencies:
@@ -12755,7 +10700,7 @@ packages:
eslint-visitor-keys: 2.1.0
dev: true
- /eslint-utils/3.0.0_eslint@8.26.0:
+ /eslint-utils@3.0.0(eslint@8.26.0):
resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==}
engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0}
peerDependencies:
@@ -12765,25 +10710,29 @@ packages:
eslint-visitor-keys: 2.1.0
dev: true
- /eslint-visitor-keys/1.3.0:
+ /eslint-visitor-keys@1.3.0:
resolution: {integrity: sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==}
engines: {node: '>=4'}
dev: true
- /eslint-visitor-keys/2.1.0:
+ /eslint-visitor-keys@2.1.0:
resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==}
engines: {node: '>=10'}
dev: true
- /eslint-visitor-keys/3.3.0:
+ /eslint-visitor-keys@3.3.0:
resolution: {integrity: sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dev: true
- /eslint/7.32.0:
+ /eslint-visitor-keys@3.4.3:
+ resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ dev: true
+
+ /eslint@7.32.0:
resolution: {integrity: sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==}
engines: {node: ^10.12.0 || >=12.0.0}
- hasBin: true
dependencies:
'@babel/code-frame': 7.12.11
'@eslint/eslintrc': 0.4.3
@@ -12829,10 +10778,9 @@ packages:
- supports-color
dev: true
- /eslint/8.26.0:
+ /eslint@8.26.0:
resolution: {integrity: sha512-kzJkpaw1Bfwheq4VXUezFriD1GxszX6dUekM7Z3aC2o4hju+tsR/XyTC3RcoSD7jmy9VkPU3+N6YjVU2e96Oyg==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
- hasBin: true
dependencies:
'@eslint/eslintrc': 1.3.3
'@humanwhocodes/config-array': 0.11.6
@@ -12845,7 +10793,7 @@ packages:
doctrine: 3.0.0
escape-string-regexp: 4.0.0
eslint-scope: 7.1.1
- eslint-utils: 3.0.0_eslint@8.26.0
+ eslint-utils: 3.0.0(eslint@8.26.0)
eslint-visitor-keys: 3.3.0
espree: 9.4.0
esquery: 1.4.0
@@ -12877,111 +10825,162 @@ packages:
- supports-color
dev: true
- /esm/3.2.25:
+ /eslint@8.56.0:
+ resolution: {integrity: sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ hasBin: true
+ dependencies:
+ '@eslint-community/eslint-utils': 4.4.0(eslint@8.56.0)
+ '@eslint-community/regexpp': 4.10.0
+ '@eslint/eslintrc': 2.1.4
+ '@eslint/js': 8.56.0
+ '@humanwhocodes/config-array': 0.11.13
+ '@humanwhocodes/module-importer': 1.0.1
+ '@nodelib/fs.walk': 1.2.8
+ '@ungap/structured-clone': 1.2.0
+ ajv: 6.12.6
+ chalk: 4.1.2
+ cross-spawn: 7.0.3
+ debug: 4.3.4
+ doctrine: 3.0.0
+ escape-string-regexp: 4.0.0
+ eslint-scope: 7.2.2
+ eslint-visitor-keys: 3.4.3
+ espree: 9.6.1
+ esquery: 1.5.0
+ esutils: 2.0.3
+ fast-deep-equal: 3.1.3
+ file-entry-cache: 6.0.1
+ find-up: 5.0.0
+ glob-parent: 6.0.2
+ globals: 13.24.0
+ graphemer: 1.4.0
+ ignore: 5.3.0
+ imurmurhash: 0.1.4
+ is-glob: 4.0.3
+ is-path-inside: 3.0.3
+ js-yaml: 4.1.0
+ json-stable-stringify-without-jsonify: 1.0.1
+ levn: 0.4.1
+ lodash.merge: 4.6.2
+ minimatch: 3.1.2
+ natural-compare: 1.4.0
+ optionator: 0.9.3
+ strip-ansi: 6.0.1
+ text-table: 0.2.0
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /esm@3.2.25:
resolution: {integrity: sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==}
engines: {node: '>=6'}
dev: true
- /espree/7.3.1:
+ /espree@7.3.1:
resolution: {integrity: sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==}
engines: {node: ^10.12.0 || >=12.0.0}
dependencies:
acorn: 7.4.1
- acorn-jsx: 5.3.2_acorn@7.4.1
+ acorn-jsx: 5.3.2(acorn@7.4.1)
eslint-visitor-keys: 1.3.0
dev: true
- /espree/9.4.0:
+ /espree@9.4.0:
resolution: {integrity: sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dependencies:
acorn: 8.8.1
- acorn-jsx: 5.3.2_acorn@8.8.1
+ acorn-jsx: 5.3.2(acorn@8.8.1)
eslint-visitor-keys: 3.3.0
dev: true
- /esprima/4.0.1:
+ /espree@9.6.1:
+ resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ dependencies:
+ acorn: 8.11.2
+ acorn-jsx: 5.3.2(acorn@8.11.2)
+ eslint-visitor-keys: 3.4.3
+ dev: true
+
+ /esprima@4.0.1:
resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}
engines: {node: '>=4'}
hasBin: true
dev: true
- /esquery/1.4.0:
+ /esquery@1.4.0:
resolution: {integrity: sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==}
engines: {node: '>=0.10'}
dependencies:
estraverse: 5.3.0
dev: true
- /esrecurse/4.3.0:
+ /esquery@1.5.0:
+ resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==}
+ engines: {node: '>=0.10'}
+ dependencies:
+ estraverse: 5.3.0
+ dev: true
+
+ /esrecurse@4.3.0:
resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
engines: {node: '>=4.0'}
dependencies:
estraverse: 5.3.0
dev: true
- /estraverse/4.3.0:
+ /estraverse@4.3.0:
resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==}
engines: {node: '>=4.0'}
dev: true
- /estraverse/5.3.0:
+ /estraverse@5.3.0:
resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
engines: {node: '>=4.0'}
dev: true
- /estree-walker/1.0.1:
+ /estree-walker@1.0.1:
resolution: {integrity: sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==}
dev: true
- /estree-walker/2.0.2:
+ /estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
dev: true
- /esutils/2.0.3:
+ /esutils@2.0.3:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'}
dev: true
- /etag/1.8.1:
+ /etag@1.8.1:
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
engines: {node: '>= 0.6'}
dev: true
- /eventemitter3/4.0.7:
+ /event-target-shim@5.0.1:
+ resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
+ engines: {node: '>=6'}
+ dev: true
+
+ /eventemitter3@4.0.7:
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
dev: true
- /events/3.3.0:
+ /events@3.3.0:
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
engines: {node: '>=0.8.x'}
dev: true
- /evp_bytestokey/1.0.3:
+ /evp_bytestokey@1.0.3:
resolution: {integrity: sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==}
dependencies:
md5.js: 1.3.5
safe-buffer: 5.2.1
dev: true
- /exec-sh/0.3.6:
- resolution: {integrity: sha512-nQn+hI3yp+oD0huYhKwvYI32+JFeq+XkNcD1GAo3Y/MjxsfVGmrrzrnzjWiNY6f+pUCP440fThsFh5gZrRAU/w==}
- dev: true
-
- /execa/1.0.0:
- resolution: {integrity: sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==}
- engines: {node: '>=6'}
- dependencies:
- cross-spawn: 6.0.5
- get-stream: 4.1.0
- is-stream: 1.1.0
- npm-run-path: 2.0.2
- p-finally: 1.0.0
- signal-exit: 3.0.7
- strip-eof: 1.0.0
- dev: true
-
- /execa/4.1.0:
+ /execa@4.1.0:
resolution: {integrity: sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==}
engines: {node: '>=10'}
dependencies:
@@ -12996,7 +10995,7 @@ packages:
strip-final-newline: 2.0.0
dev: true
- /execa/5.1.1:
+ /execa@5.1.1:
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
engines: {node: '>=10'}
dependencies:
@@ -13011,12 +11010,22 @@ packages:
strip-final-newline: 2.0.0
dev: true
- /exit/0.1.2:
- resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==}
- engines: {node: '>= 0.8.0'}
+ /execa@7.2.0:
+ resolution: {integrity: sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==}
+ engines: {node: ^14.18.0 || ^16.14.0 || >=18.0.0}
+ dependencies:
+ cross-spawn: 7.0.3
+ get-stream: 6.0.1
+ human-signals: 4.3.1
+ is-stream: 3.0.0
+ merge-stream: 2.0.0
+ npm-run-path: 5.1.0
+ onetime: 6.0.0
+ signal-exit: 3.0.7
+ strip-final-newline: 3.0.0
dev: true
- /expand-brackets/2.1.4:
+ /expand-brackets@2.1.4:
resolution: {integrity: sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==}
engines: {node: '>=0.10.0'}
dependencies:
@@ -13031,19 +11040,14 @@ packages:
- supports-color
dev: true
- /expect/26.6.2:
- resolution: {integrity: sha512-9/hlOBkQl2l/PLHJx6JjoDF6xPKcJEsUlWKb23rKE7KzeDqUZKXKNMW27KIue5JMdBV9HgmoJPcc8HtO85t9IA==}
- engines: {node: '>= 10.14.2'}
- dependencies:
- '@jest/types': 26.6.2
- ansi-styles: 4.3.0
- jest-get-type: 26.3.0
- jest-matcher-utils: 26.6.2
- jest-message-util: 26.6.2
- jest-regex-util: 26.0.0
- dev: true
+ /expand-template@2.0.3:
+ resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==}
+ engines: {node: '>=6'}
+ requiresBuild: true
+ dev: false
+ optional: true
- /express/4.18.2:
+ /express@4.18.2:
resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==}
engines: {node: '>= 0.10.0'}
dependencies:
@@ -13082,14 +11086,14 @@ packages:
- supports-color
dev: true
- /extend-shallow/2.0.1:
+ /extend-shallow@2.0.1:
resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==}
engines: {node: '>=0.10.0'}
dependencies:
is-extendable: 0.1.1
dev: true
- /extend-shallow/3.0.2:
+ /extend-shallow@3.0.2:
resolution: {integrity: sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==}
engines: {node: '>=0.10.0'}
dependencies:
@@ -13097,11 +11101,11 @@ packages:
is-extendable: 1.0.1
dev: true
- /extend/3.0.2:
+ /extend@3.0.2:
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
dev: true
- /extglob/2.0.4:
+ /extglob@2.0.4:
resolution: {integrity: sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==}
engines: {node: '>=0.10.0'}
dependencies:
@@ -13117,35 +11121,21 @@ packages:
- supports-color
dev: true
- /extsprintf/1.3.0:
+ /extsprintf@1.3.0:
resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==}
engines: {'0': node >=0.6.0}
dev: true
- /fast-deep-equal/3.1.3:
+ /fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
dev: true
- /fast-diff/1.2.0:
- resolution: {integrity: sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==}
+ /fast-diff@1.3.0:
+ resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==}
dev: true
- /fast-glob/2.2.7:
- resolution: {integrity: sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw==}
- engines: {node: '>=4.0.0'}
- dependencies:
- '@mrmlnc/readdir-enhanced': 2.2.1
- '@nodelib/fs.stat': 1.1.3
- glob-parent: 3.1.0
- is-glob: 4.0.3
- merge2: 1.4.1
- micromatch: 3.1.10
- transitivePeerDependencies:
- - supports-color
- dev: true
-
- /fast-glob/3.2.12:
- resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==}
+ /fast-glob@3.3.1:
+ resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==}
engines: {node: '>=8.6.0'}
dependencies:
'@nodelib/fs.stat': 2.0.5
@@ -13153,11 +11143,10 @@ packages:
glob-parent: 5.1.2
merge2: 1.4.1
micromatch: 4.0.5
- dev: true
- /fast-glob/3.2.7:
- resolution: {integrity: sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==}
- engines: {node: '>=8'}
+ /fast-glob@3.3.2:
+ resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==}
+ engines: {node: '>=8.6.0'}
dependencies:
'@nodelib/fs.stat': 2.0.5
'@nodelib/fs.walk': 1.2.8
@@ -13166,84 +11155,72 @@ packages:
micromatch: 4.0.5
dev: true
- /fast-json-stable-stringify/2.1.0:
+ /fast-json-patch@3.1.1:
+ resolution: {integrity: sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==}
+ dev: true
+
+ /fast-json-stable-stringify@2.1.0:
resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
dev: true
- /fast-levenshtein/2.0.6:
+ /fast-levenshtein@2.0.6:
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
dev: true
- /fastq/1.13.0:
+ /fast-redact@3.3.0:
+ resolution: {integrity: sha512-6T5V1QK1u4oF+ATxs1lWUmlEk6P2T9HqJG3e2DnHOdVgZy2rFJBoEnrIedcTXlkAHU/zKC+7KETJ+KGGKwxgMQ==}
+ engines: {node: '>=6'}
+ dev: true
+
+ /fastq@1.13.0:
resolution: {integrity: sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==}
dependencies:
reusify: 1.0.4
- dev: true
- /faye-websocket/0.11.4:
+ /faye-websocket@0.11.4:
resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==}
engines: {node: '>=0.8.0'}
dependencies:
websocket-driver: 0.7.4
dev: true
- /fb-watchman/2.0.2:
- resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==}
+ /fd-slicer@1.1.0:
+ resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==}
dependencies:
- bser: 2.1.1
+ pend: 1.2.0
dev: true
- /fetch-blob/3.2.0:
+ /fetch-blob@3.2.0:
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
engines: {node: ^12.20 || >= 14.13}
dependencies:
node-domexception: 1.0.0
- web-streams-polyfill: 3.2.1
- dev: false
-
- /fetch-ponyfill/7.1.0:
- resolution: {integrity: sha512-FhbbL55dj/qdVO3YNK7ZEkshvj3eQ7EuIGV2I6ic/2YiocvyWv+7jg2s4AyS0wdRU75s3tA8ZxI/xPigb0v5Aw==}
- dependencies:
- node-fetch: 2.6.7
- transitivePeerDependencies:
- - encoding
- dev: false
-
- /fetch-retry/5.0.3:
- resolution: {integrity: sha512-uJQyMrX5IJZkhoEUBQ3EjxkeiZkppBd5jS/fMTJmfZxLSiaQjv2zD0kTvuvkSH89uFvgSlB6ueGpjD3HWN7Bxw==}
+ web-streams-polyfill: 3.3.3
dev: true
- /fflate/0.7.4:
- resolution: {integrity: sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==}
+ /fflate@0.8.1:
+ resolution: {integrity: sha512-/exOvEuc+/iaUm105QIiOt4LpBdMTWsXxqR0HDF35vx3fmaKzw7354gTilCh5rkzEt8WYyG//ku3h3nRmd7CHQ==}
dev: false
- /figgy-pudding/3.5.2:
+ /figgy-pudding@3.5.2:
resolution: {integrity: sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==}
dev: true
- /figures/3.2.0:
- resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==}
- engines: {node: '>=8'}
- dependencies:
- escape-string-regexp: 1.0.5
- dev: true
-
- /figures/4.0.1:
- resolution: {integrity: sha512-rElJwkA/xS04Vfg+CaZodpso7VqBknOYbzi6I76hI4X80RUjkSxO2oAyPmGbuXUppywjqndOrQDl817hDnI++w==}
- engines: {node: '>=12'}
+ /figures@6.0.1:
+ resolution: {integrity: sha512-0oY/olScYD4IhQ8u//gCPA4F3mlTn2dacYmiDm/mbDQvpmLjV4uH+zhsQ5IyXRyvqkvtUkXkNdGvg5OFJTCsuQ==}
+ engines: {node: '>=18'}
dependencies:
- escape-string-regexp: 5.0.0
- is-unicode-supported: 1.3.0
+ is-unicode-supported: 2.0.0
dev: true
- /file-entry-cache/6.0.1:
+ /file-entry-cache@6.0.1:
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
engines: {node: ^10.12.0 || >=12.0.0}
dependencies:
flat-cache: 3.0.4
dev: true
- /file-loader/1.1.11:
+ /file-loader@1.1.11(webpack@4.47.0):
resolution: {integrity: sha512-TGR4HU7HUsGg6GCOPJnFk06RhWgEWFLAGWiT6rcD+GRC2keU3s9RGJ+b3Z6/U73jwwNb2gKLJ7YCrp+jvU4ALg==}
engines: {node: '>= 4.3 < 5.0.0 || >= 5.10'}
peerDependencies:
@@ -13251,9 +11228,10 @@ packages:
dependencies:
loader-utils: 1.4.0
schema-utils: 0.4.7
+ webpack: 4.47.0
dev: true
- /file-loader/6.2.0_webpack@4.46.0:
+ /file-loader@6.2.0(webpack@4.46.0):
resolution: {integrity: sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==}
engines: {node: '>= 10.13.0'}
peerDependencies:
@@ -13264,26 +11242,17 @@ packages:
webpack: 4.46.0
dev: true
- /file-system-cache/1.1.0:
- resolution: {integrity: sha512-IzF5MBq+5CR0jXx5RxPe4BICl/oEhBSXKaL9fLhAXrIfIUS77Hr4vzrYyqYMHN6uTt+BOqi3fDCTjjEBCjERKw==}
- dependencies:
- fs-extra: 10.1.0
- ramda: 0.28.0
- dev: true
-
- /file-uri-to-path/1.0.0:
+ /file-uri-to-path@1.0.0:
resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
requiresBuild: true
- dev: true
- optional: true
- /filelist/1.0.4:
+ /filelist@1.0.4:
resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==}
dependencies:
- minimatch: 5.1.0
+ minimatch: 5.1.6
dev: true
- /fill-range/4.0.0:
+ /fill-range@4.0.0:
resolution: {integrity: sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==}
engines: {node: '>=0.10.0'}
dependencies:
@@ -13293,14 +11262,13 @@ packages:
to-regex-range: 2.1.1
dev: true
- /fill-range/7.0.1:
+ /fill-range@7.0.1:
resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==}
engines: {node: '>=8'}
dependencies:
to-regex-range: 5.0.1
- dev: true
- /finalhandler/1.2.0:
+ /finalhandler@1.2.0:
resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==}
engines: {node: '>= 0.8'}
dependencies:
@@ -13315,7 +11283,7 @@ packages:
- supports-color
dev: true
- /find-cache-dir/2.1.0:
+ /find-cache-dir@2.1.0:
resolution: {integrity: sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==}
engines: {node: '>=6'}
dependencies:
@@ -13324,7 +11292,7 @@ packages:
pkg-dir: 3.0.0
dev: true
- /find-cache-dir/3.3.2:
+ /find-cache-dir@3.3.2:
resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==}
engines: {node: '>=8'}
dependencies:
@@ -13333,23 +11301,19 @@ packages:
pkg-dir: 4.2.0
dev: true
- /find-up/1.1.2:
- resolution: {integrity: sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA==}
- engines: {node: '>=0.10.0'}
- dependencies:
- path-exists: 2.1.0
- pinkie-promise: 2.0.1
+ /find-up-simple@1.0.0:
+ resolution: {integrity: sha512-q7Us7kcjj2VMePAa02hDAF6d+MzsdsAWEwYyOpwUtlerRBkOEPBCRZrAV4XfcSN8fHAgaD0hP7miwoay6DCprw==}
+ engines: {node: '>=18'}
dev: true
- optional: true
- /find-up/3.0.0:
+ /find-up@3.0.0:
resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==}
engines: {node: '>=6'}
dependencies:
locate-path: 3.0.0
dev: true
- /find-up/4.1.0:
+ /find-up@4.1.0:
resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
engines: {node: '>=8'}
dependencies:
@@ -13357,7 +11321,7 @@ packages:
path-exists: 4.0.0
dev: true
- /find-up/5.0.0:
+ /find-up@5.0.0:
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
engines: {node: '>=10'}
dependencies:
@@ -13365,15 +11329,23 @@ packages:
path-exists: 4.0.0
dev: true
- /find-up/6.3.0:
- resolution: {integrity: sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==}
- engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+ /firefox-profile@4.3.2:
+ resolution: {integrity: sha512-/C+Eqa0YgIsQT2p66p7Ygzqe7NlE/GNTbhw2SBCm5V3OsWDPITNdTPEcH2Q2fe7eMpYYNPKdUcuVioZBZiR6kA==}
+ hasBin: true
dependencies:
- locate-path: 7.1.1
- path-exists: 5.0.0
+ adm-zip: 0.5.10
+ fs-extra: 9.0.1
+ ini: 2.0.0
+ minimist: 1.2.8
+ xml2js: 0.5.0
dev: true
- /flat-cache/3.0.4:
+ /first-chunk-stream@3.0.0:
+ resolution: {integrity: sha512-LNRvR4hr/S8cXXkIY5pTgVP7L3tq6LlYWcg9nWBuW7o1NMxKZo6oOVa/6GIekMGI0Iw7uC+HWimMe9u/VAeKqw==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /flat-cache@3.0.4:
resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==}
engines: {node: ^10.12.0 || >=12.0.0}
dependencies:
@@ -13381,23 +11353,23 @@ packages:
rimraf: 3.0.2
dev: true
- /flat/5.0.2:
+ /flat@5.0.2:
resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==}
hasBin: true
dev: true
- /flatted/3.2.7:
+ /flatted@3.2.7:
resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==}
dev: true
- /flush-write-stream/1.1.1:
+ /flush-write-stream@1.1.1:
resolution: {integrity: sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==}
dependencies:
inherits: 2.0.4
- readable-stream: 2.3.7
+ readable-stream: 2.3.8
dev: true
- /follow-redirects/1.15.2:
+ /follow-redirects@1.15.2:
resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
engines: {node: '>=4.0'}
peerDependencies:
@@ -13405,19 +11377,30 @@ packages:
peerDependenciesMeta:
debug:
optional: true
+ dev: true
- /for-each/0.3.3:
+ /follow-redirects@1.15.5:
+ resolution: {integrity: sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==}
+ engines: {node: '>=4.0'}
+ peerDependencies:
+ debug: '*'
+ peerDependenciesMeta:
+ debug:
+ optional: true
+ dev: false
+
+ /for-each@0.3.3:
resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}
dependencies:
is-callable: 1.2.7
dev: true
- /for-in/1.0.2:
+ /for-in@1.0.2:
resolution: {integrity: sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==}
engines: {node: '>=0.10.0'}
dev: true
- /foreground-child/2.0.0:
+ /foreground-child@2.0.0:
resolution: {integrity: sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==}
engines: {node: '>=8.0.0'}
dependencies:
@@ -13425,95 +11408,18 @@ packages:
signal-exit: 3.0.7
dev: true
- /forever-agent/0.6.1:
- resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==}
- dev: true
-
- /fork-ts-checker-webpack-plugin/4.1.6_3n2x3j6farblcaf52bherr6og4:
- resolution: {integrity: sha512-DUxuQaKoqfNne8iikd14SAkh5uw4+8vNifp6gmA73yYNS6ywLIWSLD/n/mBzHQRpW3J7rbATEakmiA8JvkTyZw==}
- engines: {node: '>=6.11.5', yarn: '>=1.0.0'}
- peerDependencies:
- eslint: '>= 6'
- typescript: '>= 2.7'
- vue-template-compiler: '*'
- webpack: '>= 4'
- peerDependenciesMeta:
- eslint:
- optional: true
- vue-template-compiler:
- optional: true
- dependencies:
- '@babel/code-frame': 7.18.6
- chalk: 2.4.2
- eslint: 7.32.0
- micromatch: 3.1.10
- minimatch: 3.1.2
- semver: 5.7.1
- tapable: 1.1.3
- typescript: 4.8.4
- webpack: 4.46.0
- worker-rpc: 0.1.1
- transitivePeerDependencies:
- - supports-color
- dev: true
-
- /fork-ts-checker-webpack-plugin/4.1.6_bbqhrndznz6a4k7d23h2kkrexi:
- resolution: {integrity: sha512-DUxuQaKoqfNne8iikd14SAkh5uw4+8vNifp6gmA73yYNS6ywLIWSLD/n/mBzHQRpW3J7rbATEakmiA8JvkTyZw==}
- engines: {node: '>=6.11.5', yarn: '>=1.0.0'}
- peerDependencies:
- eslint: '>= 6'
- typescript: '>= 2.7'
- vue-template-compiler: '*'
- webpack: '>= 4'
- peerDependenciesMeta:
- eslint:
- optional: true
- vue-template-compiler:
- optional: true
+ /foreground-child@3.1.1:
+ resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==}
+ engines: {node: '>=14'}
dependencies:
- '@babel/code-frame': 7.18.6
- chalk: 2.4.2
- eslint: 7.32.0
- micromatch: 3.1.10
- minimatch: 3.1.2
- semver: 5.7.1
- tapable: 1.1.3
- typescript: 4.4.4
- webpack: 4.46.0
- worker-rpc: 0.1.1
- transitivePeerDependencies:
- - supports-color
- dev: true
+ cross-spawn: 7.0.3
+ signal-exit: 4.1.0
- /fork-ts-checker-webpack-plugin/4.1.6_gplzhsecki363wzvnzp4wfrwvi:
- resolution: {integrity: sha512-DUxuQaKoqfNne8iikd14SAkh5uw4+8vNifp6gmA73yYNS6ywLIWSLD/n/mBzHQRpW3J7rbATEakmiA8JvkTyZw==}
- engines: {node: '>=6.11.5', yarn: '>=1.0.0'}
- peerDependencies:
- eslint: '>= 6'
- typescript: '>= 2.7'
- vue-template-compiler: '*'
- webpack: '>= 4'
- peerDependenciesMeta:
- eslint:
- optional: true
- vue-template-compiler:
- optional: true
- dependencies:
- '@babel/code-frame': 7.18.6
- chalk: 2.4.2
- eslint: 7.32.0
- micromatch: 3.1.10
- minimatch: 3.1.2
- semver: 5.7.1
- tapable: 1.1.3
- typescript: 4.6.4
- webpack: 4.46.0
- worker-rpc: 0.1.1
- transitivePeerDependencies:
- - supports-color
+ /forever-agent@0.6.1:
+ resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==}
dev: true
- /fork-ts-checker-webpack-plugin/4.1.6_u7kjabuvawcog7hjctusduehvm:
+ /fork-ts-checker-webpack-plugin@4.1.6(eslint@8.56.0)(typescript@4.6.4)(webpack@4.46.0):
resolution: {integrity: sha512-DUxuQaKoqfNne8iikd14SAkh5uw4+8vNifp6gmA73yYNS6ywLIWSLD/n/mBzHQRpW3J7rbATEakmiA8JvkTyZw==}
engines: {node: '>=6.11.5', yarn: '>=1.0.0'}
peerDependencies:
@@ -13527,11 +11433,12 @@ packages:
vue-template-compiler:
optional: true
dependencies:
- '@babel/code-frame': 7.18.6
+ '@babel/code-frame': 7.23.5
chalk: 2.4.2
+ eslint: 8.56.0
micromatch: 3.1.10
minimatch: 3.1.2
- semver: 5.7.1
+ semver: 5.7.2
tapable: 1.1.3
typescript: 4.6.4
webpack: 4.46.0
@@ -13540,71 +11447,12 @@ packages:
- supports-color
dev: true
- /fork-ts-checker-webpack-plugin/6.5.2_3n2x3j6farblcaf52bherr6og4:
- resolution: {integrity: sha512-m5cUmF30xkZ7h4tWUgTAcEaKmUW7tfyUyTqNNOz7OxWJ0v1VWKTcOvH8FWHUwSjlW/356Ijc9vi3XfcPstpQKA==}
- engines: {node: '>=10', yarn: '>=1.0.0'}
- peerDependencies:
- eslint: '>= 6'
- typescript: '>= 2.7'
- vue-template-compiler: '*'
- webpack: '>= 4'
- peerDependenciesMeta:
- eslint:
- optional: true
- vue-template-compiler:
- optional: true
- dependencies:
- '@babel/code-frame': 7.18.6
- '@types/json-schema': 7.0.11
- chalk: 4.1.2
- chokidar: 3.5.3
- cosmiconfig: 6.0.0
- deepmerge: 4.2.2
- eslint: 7.32.0
- fs-extra: 9.1.0
- glob: 7.2.3
- memfs: 3.4.7
- minimatch: 3.1.2
- schema-utils: 2.7.0
- semver: 7.3.8
- tapable: 1.1.3
- typescript: 4.8.4
- webpack: 4.46.0
- dev: true
-
- /fork-ts-checker-webpack-plugin/6.5.2_bbqhrndznz6a4k7d23h2kkrexi:
- resolution: {integrity: sha512-m5cUmF30xkZ7h4tWUgTAcEaKmUW7tfyUyTqNNOz7OxWJ0v1VWKTcOvH8FWHUwSjlW/356Ijc9vi3XfcPstpQKA==}
- engines: {node: '>=10', yarn: '>=1.0.0'}
- peerDependencies:
- eslint: '>= 6'
- typescript: '>= 2.7'
- vue-template-compiler: '*'
- webpack: '>= 4'
- peerDependenciesMeta:
- eslint:
- optional: true
- vue-template-compiler:
- optional: true
- dependencies:
- '@babel/code-frame': 7.18.6
- '@types/json-schema': 7.0.11
- chalk: 4.1.2
- chokidar: 3.5.3
- cosmiconfig: 6.0.0
- deepmerge: 4.2.2
- eslint: 7.32.0
- fs-extra: 9.1.0
- glob: 7.2.3
- memfs: 3.4.7
- minimatch: 3.1.2
- schema-utils: 2.7.0
- semver: 7.3.8
- tapable: 1.1.3
- typescript: 4.4.4
- webpack: 4.46.0
+ /form-data-encoder@2.1.4:
+ resolution: {integrity: sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==}
+ engines: {node: '>= 14.17'}
dev: true
- /form-data/2.3.3:
+ /form-data@2.3.3:
resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==}
engines: {node: '>= 0.12'}
dependencies:
@@ -13613,157 +11461,171 @@ packages:
mime-types: 2.1.35
dev: true
- /form-data/3.0.1:
- resolution: {integrity: sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==}
- engines: {node: '>= 6'}
- dependencies:
- asynckit: 0.4.0
- combined-stream: 1.0.8
- mime-types: 2.1.35
- dev: true
-
- /form-data/4.0.0:
- resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
- engines: {node: '>= 6'}
- dependencies:
- asynckit: 0.4.0
- combined-stream: 1.0.8
- mime-types: 2.1.35
-
- /formdata-polyfill/4.0.10:
+ /formdata-polyfill@4.0.10:
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
engines: {node: '>=12.20.0'}
dependencies:
fetch-blob: 3.2.0
- dev: false
+ dev: true
- /forwarded/0.2.0:
+ /forwarded@0.2.0:
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
engines: {node: '>= 0.6'}
dev: true
- /fraction.js/4.2.0:
+ /fraction.js@4.2.0:
resolution: {integrity: sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==}
dev: true
- /fragment-cache/0.2.1:
+ /fragment-cache@0.2.1:
resolution: {integrity: sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==}
engines: {node: '>=0.10.0'}
dependencies:
map-cache: 0.2.2
dev: true
- /fresh/0.5.2:
+ /fresh@0.5.2:
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
engines: {node: '>= 0.6'}
dev: true
- /from2/2.3.0:
+ /from2@2.3.0:
resolution: {integrity: sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==}
dependencies:
inherits: 2.0.4
- readable-stream: 2.3.7
+ readable-stream: 2.3.8
dev: true
- /fromentries/1.3.2:
+ /fromentries@1.3.2:
resolution: {integrity: sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==}
dev: true
- /fs-constants/1.0.0:
+ /fs-constants@1.0.0:
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /fs-extra@11.1.0:
+ resolution: {integrity: sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw==}
+ engines: {node: '>=14.14'}
+ dependencies:
+ graceful-fs: 4.2.11
+ jsonfile: 6.1.0
+ universalify: 2.0.0
dev: true
- /fs-extra/10.1.0:
- resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==}
- engines: {node: '>=12'}
+ /fs-extra@11.1.1:
+ resolution: {integrity: sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==}
+ engines: {node: '>=14.14'}
dependencies:
- graceful-fs: 4.2.10
+ graceful-fs: 4.2.11
jsonfile: 6.1.0
universalify: 2.0.0
dev: true
- /fs-extra/9.1.0:
+ /fs-extra@9.0.1:
+ resolution: {integrity: sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==}
+ engines: {node: '>=10'}
+ dependencies:
+ at-least-node: 1.0.0
+ graceful-fs: 4.2.11
+ jsonfile: 6.1.0
+ universalify: 1.0.0
+ dev: true
+
+ /fs-extra@9.1.0:
resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==}
engines: {node: '>=10'}
dependencies:
at-least-node: 1.0.0
- graceful-fs: 4.2.10
+ graceful-fs: 4.2.11
jsonfile: 6.1.0
universalify: 2.0.0
dev: true
- /fs-minipass/1.2.7:
+ /fs-minipass@1.2.7:
resolution: {integrity: sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==}
dependencies:
minipass: 2.9.0
dev: true
- /fs-minipass/2.1.0:
+ /fs-minipass@2.1.0:
resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==}
engines: {node: '>= 8'}
dependencies:
- minipass: 3.3.4
+ minipass: 3.3.6
dev: true
- /fs-monkey/1.0.3:
+ /fs-monkey@1.0.3:
resolution: {integrity: sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==}
dev: true
- /fs-write-stream-atomic/1.0.10:
+ /fs-write-stream-atomic@1.0.10:
resolution: {integrity: sha512-gehEzmPn2nAwr39eay+x3X34Ra+M2QlVUTLhkXPjWdeO8RF9kszk116avgBJM3ZyNHgHXBNx+VmPaFC36k0PzA==}
dependencies:
- graceful-fs: 4.2.10
+ graceful-fs: 4.2.11
iferr: 0.1.5
imurmurhash: 0.1.4
- readable-stream: 2.3.7
+ readable-stream: 2.3.8
dev: true
- /fs.realpath/1.0.0:
+ /fs.realpath@1.0.0:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
- /fsevents/1.2.13:
+ /fsevents@1.2.13:
resolution: {integrity: sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==}
engines: {node: '>= 4.0'}
os: [darwin]
- deprecated: fsevents 1 will break on node v14+ and could be using insecure binaries. Upgrade to fsevents 2.
+ deprecated: The v1 package contains DANGEROUS / INSECURE binaries. Upgrade to safe fsevents v2
requiresBuild: true
dependencies:
bindings: 1.5.0
- nan: 2.17.0
+ nan: 2.18.0
dev: true
optional: true
- /fsevents/2.3.2:
- resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
+ /fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
requiresBuild: true
- dev: true
optional: true
- /function-bind/1.1.1:
- resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==}
- dev: true
+ /function-bind@1.1.2:
+ resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
- /function.prototype.name/1.1.5:
- resolution: {integrity: sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==}
+ /function.prototype.name@1.1.6:
+ resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==}
engines: {node: '>= 0.4'}
dependencies:
- call-bind: 1.0.2
- define-properties: 1.1.4
- es-abstract: 1.20.4
+ call-bind: 1.0.5
+ define-properties: 1.2.1
+ es-abstract: 1.22.3
functions-have-names: 1.2.3
dev: true
- /functional-red-black-tree/1.0.1:
+ /functional-red-black-tree@1.0.1:
resolution: {integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==}
dev: true
- /functions-have-names/1.2.3:
+ /functions-have-names@1.2.3:
resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
dev: true
- /gauge/3.0.2:
+ /fx-runner@1.4.0:
+ resolution: {integrity: sha512-rci1g6U0rdTg6bAaBboP7XdRu01dzTAaKXxFf+PUqGuCv6Xu7o8NZdY1D5MvKGIjb6EdS1g3VlXOgksir1uGkg==}
+ hasBin: true
+ dependencies:
+ commander: 2.9.0
+ shell-quote: 1.7.3
+ spawn-sync: 1.0.15
+ when: 3.7.7
+ which: 1.2.4
+ winreg: 0.0.12
+ dev: true
+
+ /gauge@3.0.2:
resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==}
engines: {node: '>=10'}
dependencies:
@@ -13778,101 +11640,108 @@ packages:
wide-align: 1.1.5
dev: true
- /gensync/1.0.0-beta.2:
+ /gensync@1.0.0-beta.2:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'}
- dev: true
- /get-caller-file/2.0.5:
+ /get-caller-file@2.0.5:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
dev: true
- /get-func-name/2.0.0:
+ /get-east-asian-width@1.2.0:
+ resolution: {integrity: sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==}
+ engines: {node: '>=18'}
+ dev: true
+
+ /get-func-name@2.0.0:
resolution: {integrity: sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==}
+ dev: true
- /get-intrinsic/1.1.3:
- resolution: {integrity: sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==}
+ /get-intrinsic@1.2.2:
+ resolution: {integrity: sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==}
dependencies:
- function-bind: 1.1.1
- has: 1.0.3
+ function-bind: 1.1.2
+ has-proto: 1.0.1
has-symbols: 1.0.3
+ hasown: 2.0.0
dev: true
- /get-own-enumerable-property-symbols/3.0.2:
+ /get-own-enumerable-property-symbols@3.0.2:
resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==}
dev: true
- /get-package-type/0.1.0:
+ /get-package-type@0.1.0:
resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==}
engines: {node: '>=8.0.0'}
dev: true
- /get-port/3.2.0:
+ /get-port@3.2.0:
resolution: {integrity: sha512-x5UJKlgeUiNT8nyo/AcnwLnZuZNcSjSw0kogRB+Whd1fjjFq4B1hySFxSFWWSn4mIBzg3sRNUDFYc4g5gjPoLg==}
engines: {node: '>=4'}
dev: true
- /get-port/5.1.1:
+ /get-port@5.1.1:
resolution: {integrity: sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==}
engines: {node: '>=8'}
dev: true
- /get-stdin/4.0.1:
- resolution: {integrity: sha512-F5aQMywwJ2n85s4hJPTT9RPxGmubonuB10MNYo17/xph174n2MIR33HRguhzVag10O/npM7SPk73LMZNP+FaWw==}
- engines: {node: '>=0.10.0'}
+ /get-stdin@9.0.0:
+ resolution: {integrity: sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==}
+ engines: {node: '>=12'}
dev: true
- optional: true
- /get-stream/4.1.0:
+ /get-stream@4.1.0:
resolution: {integrity: sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==}
engines: {node: '>=6'}
dependencies:
pump: 3.0.0
dev: true
- /get-stream/5.2.0:
+ /get-stream@5.2.0:
resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==}
engines: {node: '>=8'}
dependencies:
pump: 3.0.0
dev: true
- /get-stream/6.0.1:
+ /get-stream@6.0.1:
resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==}
engines: {node: '>=10'}
dev: true
- /get-symbol-description/1.0.0:
+ /get-symbol-description@1.0.0:
resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==}
engines: {node: '>= 0.4'}
dependencies:
- call-bind: 1.0.2
- get-intrinsic: 1.1.3
+ call-bind: 1.0.5
+ get-intrinsic: 1.2.2
dev: true
- /get-value/2.0.6:
+ /get-value@2.0.6:
resolution: {integrity: sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==}
engines: {node: '>=0.10.0'}
dev: true
- /getpass/0.1.7:
+ /getpass@0.1.7:
resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==}
dependencies:
assert-plus: 1.0.0
dev: true
- /gettext-parser/1.1.0:
+ /gettext-parser@1.1.0:
resolution: {integrity: sha1-LFpmONiTk0ubVQN9CtgstwBLJnk=}
dependencies:
encoding: 0.1.13
dev: true
- /github-slugger/1.5.0:
- resolution: {integrity: sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw==}
- dev: true
+ /github-from-package@0.0.0:
+ resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
+ requiresBuild: true
+ dev: false
+ optional: true
- /gittar/0.1.1:
+ /gittar@0.1.1:
resolution: {integrity: sha512-p+XuqWJpW9ahUuNTptqeFjudFq31o6Jd+maMBarkMAR5U3K9c7zJB4sQ4BV8mIqrTOV29TtqikDhnZfCD4XNfQ==}
engines: {node: '>=4'}
dependencies:
@@ -13880,47 +11749,56 @@ packages:
tar: 4.4.19
dev: true
- /glob-parent/3.1.0:
+ /glob-parent@3.1.0:
resolution: {integrity: sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==}
+ requiresBuild: true
dependencies:
is-glob: 3.1.0
path-dirname: 1.0.2
dev: true
+ optional: true
- /glob-parent/5.1.2:
+ /glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
dependencies:
is-glob: 4.0.3
- dev: true
- /glob-parent/6.0.2:
+ /glob-parent@6.0.2:
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
engines: {node: '>=10.13.0'}
dependencies:
is-glob: 4.0.3
- dev: true
- /glob-promise/3.4.0_glob@7.2.3:
- resolution: {integrity: sha512-q08RJ6O+eJn+dVanerAndJwIcumgbDdYiUT7zFQl3Wm1xD6fBKtah7H8ZJChj4wP+8C+QfeVy8xautR7rdmKEw==}
- engines: {node: '>=4'}
- peerDependencies:
- glob: '*'
- dependencies:
- '@types/glob': 8.0.0
- glob: 7.2.3
+ /glob-to-regexp@0.4.1:
+ resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==}
dev: true
- /glob-to-regexp/0.3.0:
- resolution: {integrity: sha512-Iozmtbqv0noj0uDDqoL0zNq0VBEfK2YFoMAZoxJe4cwphvLR+JskfF30QhXHOR4m3KrE6NLRYw+U9MRXvifyig==}
- dev: true
+ /glob@10.3.10:
+ resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==}
+ engines: {node: '>=16 || 14 >=14.17'}
+ hasBin: true
+ dependencies:
+ foreground-child: 3.1.1
+ jackspeak: 2.3.6
+ minimatch: 9.0.3
+ minipass: 7.0.4
+ path-scurry: 1.10.1
- /glob-to-regexp/0.4.1:
- resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==}
+ /glob@6.0.4:
+ resolution: {integrity: sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==}
+ requiresBuild: true
+ dependencies:
+ inflight: 1.0.6
+ inherits: 2.0.4
+ minimatch: 3.1.2
+ once: 1.4.0
+ path-is-absolute: 1.0.1
dev: true
+ optional: true
- /glob/7.1.4:
- resolution: {integrity: sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==}
+ /glob@7.1.6:
+ resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==}
dependencies:
fs.realpath: 1.0.0
inflight: 1.0.6
@@ -13928,9 +11806,8 @@ packages:
minimatch: 3.1.2
once: 1.4.0
path-is-absolute: 1.0.1
- dev: true
- /glob/7.2.0:
+ /glob@7.2.0:
resolution: {integrity: sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==}
dependencies:
fs.realpath: 1.0.0
@@ -13941,7 +11818,7 @@ packages:
path-is-absolute: 1.0.1
dev: true
- /glob/7.2.3:
+ /glob@7.2.3:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
dependencies:
fs.realpath: 1.0.0
@@ -13950,91 +11827,117 @@ packages:
minimatch: 3.1.2
once: 1.4.0
path-is-absolute: 1.0.1
+ dev: true
- /glob/8.0.3:
- resolution: {integrity: sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==}
+ /glob@8.1.0:
+ resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==}
engines: {node: '>=12'}
dependencies:
fs.realpath: 1.0.0
inflight: 1.0.6
inherits: 2.0.4
- minimatch: 5.1.0
+ minimatch: 5.1.6
once: 1.4.0
dev: true
- /global-dirs/3.0.0:
+ /global-dirs@3.0.0:
resolution: {integrity: sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA==}
engines: {node: '>=10'}
dependencies:
ini: 2.0.0
dev: true
- /global/4.4.0:
- resolution: {integrity: sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==}
+ /globals@11.12.0:
+ resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==}
+ engines: {node: '>=4'}
+
+ /globals@13.17.0:
+ resolution: {integrity: sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==}
+ engines: {node: '>=8'}
dependencies:
- min-document: 2.19.0
- process: 0.11.10
+ type-fest: 0.20.2
dev: true
- /globals/11.12.0:
- resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==}
- engines: {node: '>=4'}
+ /globals@13.21.0:
+ resolution: {integrity: sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==}
+ engines: {node: '>=8'}
+ dependencies:
+ type-fest: 0.20.2
dev: true
- /globals/13.17.0:
- resolution: {integrity: sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==}
+ /globals@13.24.0:
+ resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==}
engines: {node: '>=8'}
dependencies:
type-fest: 0.20.2
dev: true
- /globalthis/1.0.3:
+ /globalthis@1.0.3:
resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==}
engines: {node: '>= 0.4'}
dependencies:
- define-properties: 1.1.4
+ define-properties: 1.2.1
dev: true
- /globby/11.1.0:
+ /globby@11.1.0:
resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==}
engines: {node: '>=10'}
dependencies:
array-union: 2.1.0
dir-glob: 3.0.1
- fast-glob: 3.2.12
- ignore: 5.2.0
+ fast-glob: 3.3.2
+ ignore: 5.3.0
merge2: 1.4.1
slash: 3.0.0
dev: true
- /globby/13.1.2:
- resolution: {integrity: sha512-LKSDZXToac40u8Q1PQtZihbNdTYSNMuWe+K5l+oa6KgDzSvVrHXlJy40hUP522RjAIoNLJYBJi7ow+rbFpIhHQ==}
+ /globby@13.2.2:
+ resolution: {integrity: sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
dependencies:
dir-glob: 3.0.1
- fast-glob: 3.2.12
- ignore: 5.2.0
+ fast-glob: 3.3.1
+ ignore: 5.2.4
merge2: 1.4.1
slash: 4.0.0
dev: true
- /globby/9.2.0:
- resolution: {integrity: sha512-ollPHROa5mcxDEkwg6bPt3QbEf4pDQSNtd6JPL1YvOvAo/7/0VAm9TccUeoTmarjPw4pfUthSCqcyfNB1I3ZSg==}
- engines: {node: '>=6'}
+ /globby@14.0.0:
+ resolution: {integrity: sha512-/1WM/LNHRAOH9lZta77uGbq0dAEQM+XjNesWwhlERDVenqothRbnzTrL3/LrIoEPPjeUHC3vrS6TwoyxeHs7MQ==}
+ engines: {node: '>=18'}
dependencies:
- '@types/glob': 7.2.0
- array-union: 1.0.2
- dir-glob: 2.2.2
- fast-glob: 2.2.7
- glob: 7.2.3
- ignore: 4.0.6
- pify: 4.0.1
- slash: 2.0.0
- transitivePeerDependencies:
- - supports-color
+ '@sindresorhus/merge-streams': 1.0.0
+ fast-glob: 3.3.2
+ ignore: 5.3.0
+ path-type: 5.0.0
+ slash: 5.1.0
+ unicorn-magic: 0.1.0
+ dev: true
+
+ /gopd@1.0.1:
+ resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==}
+ dependencies:
+ get-intrinsic: 1.2.2
dev: true
- /got/9.6.0:
+ /got@12.6.1:
+ resolution: {integrity: sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==}
+ engines: {node: '>=14.16'}
+ dependencies:
+ '@sindresorhus/is': 5.6.0
+ '@szmarczak/http-timer': 5.0.1
+ cacheable-lookup: 7.0.0
+ cacheable-request: 10.2.14
+ decompress-response: 6.0.0
+ form-data-encoder: 2.1.4
+ get-stream: 6.0.1
+ http2-wrapper: 2.2.1
+ lowercase-keys: 3.0.0
+ p-cancelable: 3.0.0
+ responselike: 3.0.0
+ dev: true
+
+ /got@9.6.0:
resolution: {integrity: sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==}
engines: {node: '>=8.6'}
dependencies:
@@ -14053,115 +11956,116 @@ packages:
url-parse-lax: 3.0.0
dev: true
- /graceful-fs/4.2.10:
+ /graceful-fs@4.2.10:
resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==}
dev: true
- /grapheme-splitter/1.0.4:
+ /graceful-fs@4.2.11:
+ resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
+ dev: true
+
+ /graceful-readlink@1.0.1:
+ resolution: {integrity: sha512-8tLu60LgxF6XpdbK8OW3FA+IfTNBn1ZHGHKF4KQbEeSkajYw5PlYJcKluntgegDPTg8UkHjpet1T82vk6TQ68w==}
+ dev: true
+
+ /grapheme-splitter@1.0.4:
resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==}
dev: true
- /growl/1.10.5:
+ /graphemer@1.4.0:
+ resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
+ dev: true
+
+ /growl@1.10.5:
resolution: {integrity: sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==}
engines: {node: '>=4.x'}
dev: true
- /growly/1.3.0:
+ /growly@1.3.0:
resolution: {integrity: sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw==}
dev: true
- optional: true
- /gzip-size/6.0.0:
+ /gzip-size@6.0.0:
resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==}
engines: {node: '>=10'}
dependencies:
duplexer: 0.1.2
dev: true
- /handle-thing/2.0.1:
+ /handle-thing@2.0.1:
resolution: {integrity: sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==}
dev: true
- /handlebars/4.7.7:
- resolution: {integrity: sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==}
- engines: {node: '>=0.4.7'}
- hasBin: true
+ /happy-dom@10.8.0:
+ resolution: {integrity: sha512-ux5UfhNA9ANGf4keV7FCd9GqeQr3Bz1u9qnoPtTL0NcO1MEOeUXIUwNTB9r84Z7Q8/bsgkwi6K114zjYvnCmag==}
dependencies:
- minimist: 1.2.7
- neo-async: 2.6.2
- source-map: 0.6.1
- wordwrap: 1.0.0
- optionalDependencies:
- uglify-js: 3.17.4
+ css.escape: 1.5.1
+ entities: 4.5.0
+ iconv-lite: 0.6.3
+ webidl-conversions: 7.0.0
+ whatwg-encoding: 2.0.0
+ whatwg-mimetype: 3.0.0
dev: true
- /har-schema/2.0.0:
+ /har-schema@2.0.0:
resolution: {integrity: sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==}
engines: {node: '>=4'}
dev: true
- /har-validator/5.1.5:
+ /har-validator@5.1.5:
resolution: {integrity: sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==}
engines: {node: '>=6'}
- deprecated: this library is no longer supported
dependencies:
ajv: 6.12.6
har-schema: 2.0.0
dev: true
- /harmony-reflect/1.6.2:
- resolution: {integrity: sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g==}
- dev: true
-
- /has-bigints/1.0.2:
+ /has-bigints@1.0.2:
resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==}
dev: true
- /has-color/0.1.7:
+ /has-color@0.1.7:
resolution: {integrity: sha512-kaNz5OTAYYmt646Hkqw50/qyxP2vFnTVu5AQ1Zmk22Kk5+4Qx6BpO8+u7IKsML5fOsFk0ZT0AcCJNYwcvaLBvw==}
engines: {node: '>=0.10.0'}
dev: true
- /has-flag/3.0.0:
+ /has-flag@3.0.0:
resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
engines: {node: '>=4'}
- dev: true
- /has-flag/4.0.0:
+ /has-flag@4.0.0:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'}
dev: true
- /has-glob/1.0.0:
- resolution: {integrity: sha512-D+8A457fBShSEI3tFCj65PAbT++5sKiFtdCdOam0gnfBgw9D277OERk+HM9qYJXmdVLZ/znez10SqHN0BBQ50g==}
- engines: {node: '>=0.10.0'}
+ /has-property-descriptors@1.0.1:
+ resolution: {integrity: sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==}
dependencies:
- is-glob: 3.1.0
+ get-intrinsic: 1.2.2
dev: true
- /has-property-descriptors/1.0.0:
- resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==}
- dependencies:
- get-intrinsic: 1.1.3
+ /has-proto@1.0.1:
+ resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==}
+ engines: {node: '>= 0.4'}
dev: true
- /has-symbols/1.0.3:
+ /has-symbols@1.0.3:
resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==}
engines: {node: '>= 0.4'}
dev: true
- /has-tostringtag/1.0.0:
+ /has-tostringtag@1.0.0:
resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==}
engines: {node: '>= 0.4'}
dependencies:
has-symbols: 1.0.3
dev: true
- /has-unicode/2.0.1:
+ /has-unicode@2.0.1:
resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==}
dev: true
- /has-value/0.3.1:
+ /has-value@0.3.1:
resolution: {integrity: sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==}
engines: {node: '>=0.10.0'}
dependencies:
@@ -14170,7 +12074,7 @@ packages:
isobject: 2.1.0
dev: true
- /has-value/1.0.0:
+ /has-value@1.0.0:
resolution: {integrity: sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==}
engines: {node: '>=0.10.0'}
dependencies:
@@ -14179,12 +12083,12 @@ packages:
isobject: 3.0.1
dev: true
- /has-values/0.1.4:
+ /has-values@0.1.4:
resolution: {integrity: sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==}
engines: {node: '>=0.10.0'}
dev: true
- /has-values/1.0.0:
+ /has-values@1.0.0:
resolution: {integrity: sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==}
engines: {node: '>=0.10.0'}
dependencies:
@@ -14192,46 +12096,44 @@ packages:
kind-of: 4.0.0
dev: true
- /has-yarn/2.1.0:
+ /has-yarn@2.1.0:
resolution: {integrity: sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==}
engines: {node: '>=8'}
dev: true
- /has/1.0.3:
+ /has-yarn@3.0.0:
+ resolution: {integrity: sha512-IrsVwUHhEULx3R8f/aA8AHuEzAorplsab/v8HBzEiIukwq5i/EC+xmOW+HfP1OaDP+2JkgT1yILHN2O3UFIbcA==}
+ engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+ dev: true
+
+ /has@1.0.3:
resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==}
engines: {node: '>= 0.4.0'}
dependencies:
- function-bind: 1.1.1
+ function-bind: 1.1.2
dev: true
- /hash-base/3.1.0:
+ /hash-base@3.1.0:
resolution: {integrity: sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==}
engines: {node: '>=4'}
dependencies:
inherits: 2.0.4
- readable-stream: 3.6.0
+ readable-stream: 3.6.2
safe-buffer: 5.2.1
dev: true
- /hash-wasm/4.9.0:
- resolution: {integrity: sha512-7SW7ejyfnRxuOc7ptQHSf4LDoZaWOivfzqw+5rpcQku0nHfmicPKE51ra9BiRLAmT8+gGLestr1XroUkqdjL6w==}
+ /hash-wasm@4.11.0:
+ resolution: {integrity: sha512-HVusNXlVqHe0fzIzdQOGolnFN6mX/fqcrSAOcTBXdvzrXVHwTz11vXeKRmkR5gTuwVpvHZEIyKoePDvuAR+XwQ==}
dev: false
- /hash.js/1.1.7:
+ /hash.js@1.1.7:
resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==}
dependencies:
inherits: 2.0.4
minimalistic-assert: 1.0.1
dev: true
- /hasha/4.0.1:
- resolution: {integrity: sha512-+wnvroCn3pq0CAKWfItGPyl0DJOob2qs/2D/Rh0a/O90LBzmo5GaKHwIRb6FInVvmEl1mCIHL5RqlfTLvh6FoQ==}
- engines: {node: '>=8'}
- dependencies:
- is-stream: 1.1.0
- dev: true
-
- /hasha/5.2.2:
+ /hasha@5.2.2:
resolution: {integrity: sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==}
engines: {node: '>=8'}
dependencies:
@@ -14239,78 +12141,22 @@ packages:
type-fest: 0.8.1
dev: true
- /hast-to-hyperscript/9.0.1:
- resolution: {integrity: sha512-zQgLKqF+O2F72S1aa4y2ivxzSlko3MAvxkwG8ehGmNiqd98BIN3JM1rAJPmplEyLmGLO2QZYJtIneOSZ2YbJuA==}
- dependencies:
- '@types/unist': 2.0.6
- comma-separated-tokens: 1.0.8
- property-information: 5.6.0
- space-separated-tokens: 1.1.5
- style-to-object: 0.3.0
- unist-util-is: 4.1.0
- web-namespaces: 1.1.4
- dev: true
-
- /hast-util-from-parse5/6.0.1:
- resolution: {integrity: sha512-jeJUWiN5pSxW12Rh01smtVkZgZr33wBokLzKLwinYOUfSzm1Nl/c3GUGebDyOKjdsRgMvoVbV0VpAcpjF4NrJA==}
- dependencies:
- '@types/parse5': 5.0.3
- hastscript: 6.0.0
- property-information: 5.6.0
- vfile: 4.2.1
- vfile-location: 3.2.0
- web-namespaces: 1.1.4
- dev: true
-
- /hast-util-parse-selector/2.2.5:
- resolution: {integrity: sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==}
- dev: true
-
- /hast-util-raw/6.0.1:
- resolution: {integrity: sha512-ZMuiYA+UF7BXBtsTBNcLBF5HzXzkyE6MLzJnL605LKE8GJylNjGc4jjxazAHUtcwT5/CEt6afRKViYB4X66dig==}
- dependencies:
- '@types/hast': 2.3.4
- hast-util-from-parse5: 6.0.1
- hast-util-to-parse5: 6.0.0
- html-void-elements: 1.0.5
- parse5: 6.0.1
- unist-util-position: 3.1.0
- vfile: 4.2.1
- web-namespaces: 1.1.4
- xtend: 4.0.2
- zwitch: 1.0.5
- dev: true
-
- /hast-util-to-parse5/6.0.0:
- resolution: {integrity: sha512-Lu5m6Lgm/fWuz8eWnrKezHtVY83JeRGaNQ2kn9aJgqaxvVkFCZQBEhgodZUDUvoodgyROHDb3r5IxAEdl6suJQ==}
- dependencies:
- hast-to-hyperscript: 9.0.1
- property-information: 5.6.0
- web-namespaces: 1.1.4
- xtend: 4.0.2
- zwitch: 1.0.5
- dev: true
-
- /hastscript/6.0.0:
- resolution: {integrity: sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==}
+ /hasown@2.0.0:
+ resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==}
+ engines: {node: '>= 0.4'}
dependencies:
- '@types/hast': 2.3.4
- comma-separated-tokens: 1.0.8
- hast-util-parse-selector: 2.2.5
- property-information: 5.6.0
- space-separated-tokens: 1.1.5
- dev: true
+ function-bind: 1.1.2
- /he/1.2.0:
+ /he@1.2.0:
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
hasBin: true
dev: true
- /hex-color-regex/1.1.0:
+ /hex-color-regex@1.1.0:
resolution: {integrity: sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==}
dev: true
- /history/4.10.1:
+ /history@4.10.1:
resolution: {integrity: sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==}
dependencies:
'@babel/runtime': 7.19.4
@@ -14321,7 +12167,7 @@ packages:
value-equal: 1.0.1
dev: false
- /hmac-drbg/1.0.1:
+ /hmac-drbg@1.0.1:
resolution: {integrity: sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==}
dependencies:
hash.js: 1.1.7
@@ -14329,73 +12175,58 @@ packages:
minimalistic-crypto-utils: 1.0.1
dev: true
- /hosted-git-info/2.8.9:
- resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
- dev: true
-
- /hpack.js/2.1.6:
+ /hpack.js@2.1.6:
resolution: {integrity: sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==}
dependencies:
inherits: 2.0.4
obuf: 1.1.2
- readable-stream: 2.3.7
+ readable-stream: 2.3.8
wbuf: 1.7.3
dev: true
- /hsl-regex/1.0.0:
+ /hsl-regex@1.0.0:
resolution: {integrity: sha512-M5ezZw4LzXbBKMruP+BNANf0k+19hDQMgpzBIYnya//Al+fjNct9Wf3b1WedLqdEs2hKBvxq/jh+DsHJLj0F9A==}
dev: true
- /hsla-regex/1.0.0:
+ /hsla-regex@1.0.0:
resolution: {integrity: sha512-7Wn5GMLuHBjZCb2bTmnDOycho0p/7UVaAeqXZGbHrBCl6Yd/xDhQJAXe6Ga9AXJH2I5zY1dEdYw2u1UptnSBJA==}
dev: true
- /html-element-map/1.3.1:
- resolution: {integrity: sha512-6XMlxrAFX4UEEGxctfFnmrFaaZFNf9i5fNuV5wZ3WWQ4FVaNP1aX1LkX9j2mfEx1NpjeE/rL3nmgEn23GdFmrg==}
- dependencies:
- array.prototype.filter: 1.0.1
- call-bind: 1.0.2
+ /html-element-attributes@1.3.1:
+ resolution: {integrity: sha512-UrRKgp5sQmRnDy4TEwAUsu14XBUlzKB8U3hjIYDjcZ3Hbp86Jtftzxfgrv6E/ii/h78tsaZwAnAE8HwnHr0dPA==}
dev: true
- /html-encoding-sniffer/1.0.2:
+ /html-encoding-sniffer@1.0.2:
resolution: {integrity: sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==}
dependencies:
whatwg-encoding: 1.0.5
dev: true
- /html-encoding-sniffer/2.0.1:
- resolution: {integrity: sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==}
- engines: {node: '>=10'}
- dependencies:
- whatwg-encoding: 1.0.5
- dev: true
-
- /html-entities/2.3.3:
+ /html-entities@2.3.3:
resolution: {integrity: sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==}
dev: true
- /html-escaper/2.0.2:
+ /html-escaper@2.0.2:
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
dev: true
- /html-minifier-terser/5.1.1:
- resolution: {integrity: sha512-ZPr5MNObqnV/T9akshPKbVgyOqLmy+Bxo7juKCfTfnjNniTAMdy4hz21YQqoofMBJD2kdREaqPPdThoR78Tgxg==}
- engines: {node: '>=6'}
+ /html-minifier-terser@6.1.0:
+ resolution: {integrity: sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==}
+ engines: {node: '>=12'}
hasBin: true
dependencies:
camel-case: 4.1.2
- clean-css: 4.2.4
- commander: 4.1.1
+ clean-css: 5.3.3
+ commander: 8.3.0
he: 1.2.0
param-case: 3.0.4
relateurl: 0.2.7
- terser: 4.8.1
+ terser: 5.26.0
dev: true
- /html-minifier/3.5.21:
+ /html-minifier@3.5.21:
resolution: {integrity: sha512-LKUKwuJDhxNa3uf/LPR/KVjm/l3rBqtYeCOAekvG8F1vItxMUpueGd94i/asDDr8/1u7InxzFA5EeGjhhG5mMA==}
engines: {node: '>=4'}
- hasBin: true
dependencies:
camel-case: 3.0.0
clean-css: 4.2.4
@@ -14406,23 +12237,19 @@ packages:
uglify-js: 3.4.10
dev: true
- /html-void-elements/1.0.5:
- resolution: {integrity: sha512-uE/TxKuyNIcx44cIWnjr/rfIATDH7ZaOMmstu0CwhFG1Dunhlp4OC6/NMbhiwoq5BpW0ubi303qnEk/PZj614w==}
- dev: true
-
- /html-webpack-exclude-assets-plugin/0.0.7:
+ /html-webpack-exclude-assets-plugin@0.0.7:
resolution: {integrity: sha512-gaYKMGBPDts3Fb1WXyDEEcS/0TSRg2IDl3EsbQL2AkKWTqdjSKwfQ8Iz0RhPiWErJfqhq5/wkhoYyjQoG55pug==}
engines: {node: '>=4.0.0'}
dev: true
- /html-webpack-inline-chunk-plugin/1.1.1:
+ /html-webpack-inline-chunk-plugin@1.1.1:
resolution: {integrity: sha512-0cor73re8PI/BG2W+jCkZxob8duoLG1COQLFZ3cT8G1VBzyKE7wQRpLkl79BjJw3epntnVg17n2z7Y6McJijaA==}
dependencies:
lodash: 4.17.21
source-map-url: 0.4.1
dev: true
- /html-webpack-inline-source-plugin/0.0.10:
+ /html-webpack-inline-source-plugin@0.0.10:
resolution: {integrity: sha512-0ZNU57u7283vrXSF5a4VDnVOMWiSwypKIp1z/XfXWoVHLA1r3Xmyxx5+Lz+mnthz/UvxL1OAf41w5UIF68Jngw==}
dependencies:
escape-string-regexp: 1.0.5
@@ -14430,10 +12257,9 @@ packages:
source-map-url: 0.4.1
dev: true
- /html-webpack-plugin/3.2.0_webpack@4.46.0:
+ /html-webpack-plugin@3.2.0(webpack@4.46.0):
resolution: {integrity: sha512-Br4ifmjQojUP4EmHnRBoUIYcZ9J7M4bTMcm7u6xoIAIuq2Nte4TzXX0533owvkQKQD1WeMTTTyD4Ni4QKxS0Bg==}
engines: {node: '>=6.9'}
- deprecated: 3.x is no longer supported
peerDependencies:
webpack: ^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0
dependencies:
@@ -14447,34 +12273,32 @@ packages:
webpack: 4.46.0
dev: true
- /html-webpack-plugin/4.5.2_webpack@4.46.0:
- resolution: {integrity: sha512-q5oYdzjKUIPQVjOosjgvCHQOv9Ett9CYYHlgvJeXG0qQvdSojnBq4vAdQBwn1+yGveAwHCoe/rMR86ozX3+c2A==}
- engines: {node: '>=6.9'}
+ /html-webpack-plugin@5.5.4(webpack@4.47.0):
+ resolution: {integrity: sha512-3wNSaVVxdxcu0jd4FpQFoICdqgxs4zIQQvj+2yQKFfBOnLETQ6X5CDWdeasuGlSsooFlMkEioWDTqBv1wvw5Iw==}
+ engines: {node: '>=10.13.0'}
peerDependencies:
- webpack: ^4.0.0 || ^5.0.0
+ webpack: ^5.20.0
dependencies:
- '@types/html-minifier-terser': 5.1.2
- '@types/tapable': 1.0.8
- '@types/webpack': 4.41.33
- html-minifier-terser: 5.1.1
- loader-utils: 1.4.0
+ '@types/html-minifier-terser': 6.1.0
+ html-minifier-terser: 6.1.0
lodash: 4.17.21
- pretty-error: 2.1.2
- tapable: 1.1.3
- util.promisify: 1.0.0
- webpack: 4.46.0
+ pretty-error: 4.0.0
+ tapable: 2.2.1
+ webpack: 4.47.0
dev: true
- /html-webpack-skip-assets-plugin/1.0.3:
+ /html-webpack-skip-assets-plugin@1.0.3(html-webpack-plugin@5.5.4)(webpack@4.47.0):
resolution: {integrity: sha512-vpdh+JZGlE1Df3IftH2gw5P7b6yfTsahcOIJnkkkj5iJU9dUStXgzgALoXWwl8+17wWgFm3edcJzeYTJBYfMAw==}
peerDependencies:
html-webpack-plugin: '>=3.0.0'
webpack: '>=3.0.0'
dependencies:
+ html-webpack-plugin: 5.5.4(webpack@4.47.0)
minimatch: 3.0.4
+ webpack: 4.47.0
dev: true
- /htmlparser2/6.1.0:
+ /htmlparser2@6.1.0:
resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==}
dependencies:
domelementtype: 2.3.0
@@ -14483,24 +12307,28 @@ packages:
entities: 2.2.0
dev: true
- /htmlparser2/8.0.1:
- resolution: {integrity: sha512-4lVbmc1diZC7GUJQtRQ5yBAeUCL1exyMwmForWkRLnwyzWBFxN633SALPMGYaWZvKe9j1pRZJpauvmxENSp/EA==}
+ /htmlparser2@8.0.2:
+ resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==}
dependencies:
domelementtype: 2.3.0
domhandler: 5.0.3
- domutils: 3.0.1
- entities: 4.4.0
+ domutils: 3.1.0
+ entities: 4.5.0
dev: true
- /http-cache-semantics/4.1.0:
+ /http-cache-semantics@4.1.0:
resolution: {integrity: sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==}
dev: true
- /http-deceiver/1.2.7:
+ /http-cache-semantics@4.1.1:
+ resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==}
+ dev: true
+
+ /http-deceiver@1.2.7:
resolution: {integrity: sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==}
dev: true
- /http-errors/1.6.3:
+ /http-errors@1.6.3:
resolution: {integrity: sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==}
engines: {node: '>= 0.6'}
dependencies:
@@ -14510,7 +12338,7 @@ packages:
statuses: 1.5.0
dev: true
- /http-errors/2.0.0:
+ /http-errors@2.0.0:
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
engines: {node: '>= 0.8'}
dependencies:
@@ -14521,22 +12349,11 @@ packages:
toidentifier: 1.0.1
dev: true
- /http-parser-js/0.5.8:
+ /http-parser-js@0.5.8:
resolution: {integrity: sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==}
dev: true
- /http-proxy-agent/4.0.1:
- resolution: {integrity: sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==}
- engines: {node: '>= 6'}
- dependencies:
- '@tootallnate/once': 1.1.2
- agent-base: 6.0.2
- debug: 4.3.4
- transitivePeerDependencies:
- - supports-color
- dev: true
-
- /http-proxy-middleware/2.0.6_@types+express@4.17.14:
+ /http-proxy-middleware@2.0.6(@types/express@4.17.14):
resolution: {integrity: sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==}
engines: {node: '>=12.0.0'}
peerDependencies:
@@ -14555,7 +12372,7 @@ packages:
- debug
dev: true
- /http-proxy/1.18.1:
+ /http-proxy@1.18.1:
resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==}
engines: {node: '>=8.0.0'}
dependencies:
@@ -14566,7 +12383,7 @@ packages:
- debug
dev: true
- /http-signature/1.2.0:
+ /http-signature@1.2.0:
resolution: {integrity: sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==}
engines: {node: '>=0.8', npm: '>=1.3.7'}
dependencies:
@@ -14575,11 +12392,19 @@ packages:
sshpk: 1.17.0
dev: true
- /https-browserify/1.0.0:
+ /http2-wrapper@2.2.1:
+ resolution: {integrity: sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==}
+ engines: {node: '>=10.19.0'}
+ dependencies:
+ quick-lru: 5.1.1
+ resolve-alpn: 1.2.1
+ dev: true
+
+ /https-browserify@1.0.0:
resolution: {integrity: sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==}
dev: true
- /https-proxy-agent/5.0.1:
+ /https-proxy-agent@5.0.1:
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
engines: {node: '>= 6'}
dependencies:
@@ -14589,85 +12414,98 @@ packages:
- supports-color
dev: true
- /human-signals/1.1.1:
+ /human-signals@1.1.1:
resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==}
engines: {node: '>=8.12.0'}
dev: true
- /human-signals/2.1.0:
+ /human-signals@2.1.0:
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
engines: {node: '>=10.17.0'}
dev: true
- /iconv-lite/0.4.24:
+ /human-signals@4.3.1:
+ resolution: {integrity: sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==}
+ engines: {node: '>=14.18.0'}
+ dev: true
+
+ /iconv-lite@0.4.24:
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
engines: {node: '>=0.10.0'}
dependencies:
safer-buffer: 2.1.2
dev: true
- /iconv-lite/0.6.3:
+ /iconv-lite@0.6.3:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'}
dependencies:
safer-buffer: 2.1.2
dev: true
- /icss-utils/4.1.1:
- resolution: {integrity: sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA==}
- engines: {node: '>= 6'}
- dependencies:
- postcss: 7.0.39
- dev: true
-
- /icss-utils/5.1.0_postcss@8.4.18:
+ /icss-utils@5.1.0(postcss@8.4.32):
resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==}
engines: {node: ^10 || ^12 || >= 14}
peerDependencies:
postcss: ^8.1.0
dependencies:
- postcss: 8.4.18
+ postcss: 8.4.32
dev: true
- /idb/7.1.0:
+ /idb@7.1.0:
resolution: {integrity: sha512-Wsk07aAxDsntgYJY4h0knZJuTxM73eQ4reRAO+Z1liOh8eMCJ/MoDS8fCui1vGT9mnjtl1sOu3I2i/W1swPYZg==}
dev: true
- /identity-obj-proxy/3.0.0:
- resolution: {integrity: sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA==}
- engines: {node: '>=4'}
- dependencies:
- harmony-reflect: 1.6.2
- dev: true
-
- /ieee754/1.2.1:
+ /ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
- dev: true
+ requiresBuild: true
- /iferr/0.1.5:
+ /iferr@0.1.5:
resolution: {integrity: sha512-DUNFN5j7Tln0D+TxzloUjKB+CtVu6myn0JEFak6dG18mNt9YkQ6lzGCdafwofISZ1lLF3xRHJ98VKy9ynkcFaA==}
dev: true
- /ignore-by-default/2.1.0:
+ /ignore-by-default@2.1.0:
resolution: {integrity: sha512-yiWd4GVmJp0Q6ghmM2B/V3oZGRmjrKLXvHR3TE1nfoXsmoggllfZUQe74EN0fJdPFZu2NIvNdrMMLm3OsV7Ohw==}
engines: {node: '>=10 <11 || >=12 <13 || >=14'}
dev: true
- /ignore/4.0.6:
+ /ignore@4.0.6:
resolution: {integrity: sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==}
engines: {node: '>= 4'}
dev: true
- /ignore/5.2.0:
+ /ignore@5.2.0:
resolution: {integrity: sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==}
engines: {node: '>= 4'}
dev: true
- /immutable/4.1.0:
+ /ignore@5.2.4:
+ resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==}
+ engines: {node: '>= 4'}
+ dev: true
+
+ /ignore@5.3.0:
+ resolution: {integrity: sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==}
+ engines: {node: '>= 4'}
+ dev: true
+
+ /image-size@1.1.1:
+ resolution: {integrity: sha512-541xKlUw6jr/6gGuk92F+mYM5zaFAc5ahphvkqvNe2bQ6gVBkd6bfrmVJ2t4KDAfikAYZyIqTnktX3i6/aQDrQ==}
+ engines: {node: '>=16.x'}
+ hasBin: true
+ dependencies:
+ queue: 6.0.2
+ dev: true
+
+ /immediate@3.0.6:
+ resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
+ dev: true
+
+ /immutable@4.1.0:
resolution: {integrity: sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==}
dev: true
- /import-fresh/2.0.0:
+ /import-fresh@2.0.0:
resolution: {integrity: sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==}
engines: {node: '>=4'}
dependencies:
@@ -14675,7 +12513,7 @@ packages:
resolve-from: 3.0.0
dev: true
- /import-fresh/3.3.0:
+ /import-fresh@3.3.0:
resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
engines: {node: '>=6'}
dependencies:
@@ -14683,242 +12521,203 @@ packages:
resolve-from: 4.0.0
dev: true
- /import-lazy/2.1.0:
+ /import-lazy@2.1.0:
resolution: {integrity: sha512-m7ZEHgtw69qOGw+jwxXkHlrlIPdTGkyh66zXZ1ajZbxkDBNjSY/LGbmjc7h0s2ELsUDTAhFr55TrPSSqJGPG0A==}
engines: {node: '>=4'}
dev: true
- /import-local/3.1.0:
- resolution: {integrity: sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==}
+ /import-lazy@4.0.0:
+ resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==}
engines: {node: '>=8'}
- hasBin: true
- dependencies:
- pkg-dir: 4.2.0
- resolve-cwd: 3.0.0
dev: true
- /imurmurhash/0.1.4:
+ /imurmurhash@0.1.4:
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
engines: {node: '>=0.8.19'}
dev: true
- /indent-string/2.1.0:
- resolution: {integrity: sha512-aqwDFWSgSgfRaEwao5lg5KEcVd/2a+D1rvoG7NdilmYz0NwRk6StWpWdz/Hpk34MKPpx7s8XxUqimfcQK6gGlg==}
- engines: {node: '>=0.10.0'}
- dependencies:
- repeating: 2.0.1
- dev: true
- optional: true
-
- /indent-string/4.0.0:
+ /indent-string@4.0.0:
resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
engines: {node: '>=8'}
dev: true
- /indent-string/5.0.0:
+ /indent-string@5.0.0:
resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==}
engines: {node: '>=12'}
dev: true
- /indexes-of/1.0.1:
+ /indexes-of@1.0.1:
resolution: {integrity: sha512-bup+4tap3Hympa+JBJUG7XuOsdNQ6fxt0MHyXMKuLBKn0OqsTfvUxkUrroEX1+B2VsSHvCjiIcZVxRtYa4nllA==}
dev: true
- /infer-owner/1.0.4:
+ /infer-owner@1.0.4:
resolution: {integrity: sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==}
dev: true
- /inflight/1.0.6:
+ /inflight@1.0.6:
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
dependencies:
once: 1.4.0
wrappy: 1.0.2
- /inherits/2.0.1:
+ /inherits@2.0.1:
resolution: {integrity: sha512-8nWq2nLTAwd02jTqJExUYFSD/fKq6VH9Y/oG2accc/kdI0V98Bag8d5a4gi3XHz73rDWa2PvTtvcWYquKqSENA==}
dev: true
- /inherits/2.0.3:
+ /inherits@2.0.3:
resolution: {integrity: sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==}
dev: true
- /inherits/2.0.4:
+ /inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
- /ini/1.3.8:
+ /ini@1.3.8:
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
- dev: true
+ requiresBuild: true
- /ini/2.0.0:
+ /ini@2.0.0:
resolution: {integrity: sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==}
engines: {node: '>=10'}
dev: true
- /inline-chunk-html-plugin/1.1.1:
+ /inline-chunk-html-plugin@1.1.1:
resolution: {integrity: sha512-6W1eGIj8z/Yla6xJx5il6jJfCxMZS3kVkbiLQThbbjdsDLRIWkUVmpnhfW2l6WAwCW+qfy0zoXVGBZM1E5XF3g==}
dev: true
- /inline-style-parser/0.1.1:
- resolution: {integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==}
- dev: true
-
- /internal-slot/1.0.3:
- resolution: {integrity: sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==}
+ /internal-slot@1.0.6:
+ resolution: {integrity: sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==}
engines: {node: '>= 0.4'}
dependencies:
- get-intrinsic: 1.1.3
- has: 1.0.3
+ get-intrinsic: 1.2.2
+ hasown: 2.0.0
side-channel: 1.0.4
dev: true
- /interpret/1.4.0:
- resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==}
- engines: {node: '>= 0.10'}
- dev: true
-
- /interpret/2.2.0:
- resolution: {integrity: sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==}
- engines: {node: '>= 0.10'}
- dev: true
-
- /invariant/2.2.4:
- resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==}
- dependencies:
- loose-envify: 1.4.0
+ /invert-kv@3.0.1:
+ resolution: {integrity: sha512-CYdFeFexxhv/Bcny+Q0BfOV+ltRlJcd4BBZBYFX/O0u4npJrgZtIcjokegtiSMAvlMTJ+Koq0GBCc//3bueQxw==}
+ engines: {node: '>=8'}
dev: true
- /ip/1.1.8:
+ /ip@1.1.8:
resolution: {integrity: sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==}
dev: true
- /ip/2.0.0:
- resolution: {integrity: sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==}
- dev: true
-
- /ipaddr.js/1.9.1:
+ /ipaddr.js@1.9.1:
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
engines: {node: '>= 0.10'}
dev: true
- /ipaddr.js/2.0.1:
+ /ipaddr.js@2.0.1:
resolution: {integrity: sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng==}
engines: {node: '>= 10'}
dev: true
- /irregular-plurals/3.3.0:
- resolution: {integrity: sha512-MVBLKUTangM3EfRPFROhmWQQKRDsrgI83J8GS3jXy+OwYqiR2/aoWndYQ5416jLE3uaGgLH7ncme3X9y09gZ3g==}
+ /irregular-plurals@3.5.0:
+ resolution: {integrity: sha512-1ANGLZ+Nkv1ptFb2pa8oG8Lem4krflKuX/gINiHJHjJUKaJHk/SXk5x6K3J+39/p0h1RQ2saROclJJ+QLvETCQ==}
engines: {node: '>=8'}
dev: true
- /is-absolute-url/2.1.0:
+ /is-absolute-url@2.1.0:
resolution: {integrity: sha512-vOx7VprsKyllwjSkLV79NIhpyLfr3jAp7VaTCMXOJHu4m0Ew1CZ2fcjASwmV1jI3BWuWHB013M48eyeldk9gYg==}
engines: {node: '>=0.10.0'}
dev: true
- /is-absolute-url/3.0.3:
- resolution: {integrity: sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==}
- engines: {node: '>=8'}
+ /is-absolute@0.1.7:
+ resolution: {integrity: sha512-Xi9/ZSn4NFapG8RP98iNPMOeaV3mXPisxKxzKtHVqr3g56j/fBn+yZmnxSVAA8lmZbl2J9b/a4kJvfU3hqQYgA==}
+ engines: {node: '>=0.10.0'}
+ dependencies:
+ is-relative: 0.1.3
dev: true
- /is-accessor-descriptor/0.1.6:
+ /is-accessor-descriptor@0.1.6:
resolution: {integrity: sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==}
engines: {node: '>=0.10.0'}
dependencies:
kind-of: 3.2.2
dev: true
- /is-accessor-descriptor/1.0.0:
+ /is-accessor-descriptor@1.0.0:
resolution: {integrity: sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==}
engines: {node: '>=0.10.0'}
dependencies:
kind-of: 6.0.3
dev: true
- /is-alphabetical/1.0.4:
- resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==}
- dev: true
-
- /is-alphanumerical/1.0.4:
- resolution: {integrity: sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==}
+ /is-array-buffer@3.0.2:
+ resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==}
dependencies:
- is-alphabetical: 1.0.4
- is-decimal: 1.0.4
+ call-bind: 1.0.5
+ get-intrinsic: 1.2.2
+ is-typed-array: 1.1.12
dev: true
- /is-arguments/1.1.1:
- resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==}
- engines: {node: '>= 0.4'}
- dependencies:
- call-bind: 1.0.2
- has-tostringtag: 1.0.0
- dev: true
-
- /is-arrayish/0.2.1:
+ /is-arrayish@0.2.1:
resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
dev: true
- /is-arrayish/0.3.2:
+ /is-arrayish@0.3.2:
resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==}
dev: true
- /is-bigint/1.0.4:
+ /is-async-function@2.0.0:
+ resolution: {integrity: sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ has-tostringtag: 1.0.0
+ dev: true
+
+ /is-bigint@1.0.4:
resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==}
dependencies:
has-bigints: 1.0.2
dev: true
- /is-binary-path/1.0.1:
+ /is-binary-path@1.0.1:
resolution: {integrity: sha512-9fRVlXc0uCxEDj1nQzaWONSpbTfx0FmJfzHF7pwlI8DkWGoHBBea4Pg5Ky0ojwwxQmnSifgbKkI06Qv0Ljgj+Q==}
engines: {node: '>=0.10.0'}
+ requiresBuild: true
dependencies:
binary-extensions: 1.13.1
dev: true
optional: true
- /is-binary-path/2.1.0:
+ /is-binary-path@2.1.0:
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
engines: {node: '>=8'}
dependencies:
binary-extensions: 2.2.0
- dev: true
- /is-boolean-object/1.1.2:
+ /is-boolean-object@1.1.2:
resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==}
engines: {node: '>= 0.4'}
dependencies:
- call-bind: 1.0.2
+ call-bind: 1.0.5
has-tostringtag: 1.0.0
dev: true
- /is-buffer/1.1.6:
+ /is-buffer@1.1.6:
resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==}
dev: true
- /is-buffer/2.0.5:
- resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==}
- engines: {node: '>=4'}
- dev: true
-
- /is-builtin-module/3.2.0:
- resolution: {integrity: sha512-phDA4oSGt7vl1n5tJvTWooWWAsXLY+2xCnxNqvKhGEzujg+A43wPlPOyDg3C8XQHN+6k/JTQWJ/j0dQh/qr+Hw==}
- engines: {node: '>=6'}
- dependencies:
- builtin-modules: 3.3.0
- dev: true
-
- /is-callable/1.2.7:
+ /is-callable@1.2.7:
resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==}
engines: {node: '>= 0.4'}
dev: true
- /is-ci/2.0.0:
+ /is-ci@2.0.0:
resolution: {integrity: sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==}
- hasBin: true
dependencies:
ci-info: 2.0.0
dev: true
- /is-color-stop/1.1.0:
+ /is-ci@3.0.1:
+ resolution: {integrity: sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==}
+ hasBin: true
+ dependencies:
+ ci-info: 3.9.0
+ dev: true
+
+ /is-color-stop@1.1.0:
resolution: {integrity: sha512-H1U8Vz0cfXNujrJzEcvvwMDW9Ra+biSYA3ThdQvAnMLJkEHQXn6bWzLkxHtVYJ+Sdbx0b6finn3jZiaVe7MAHA==}
dependencies:
css-color-names: 0.0.4
@@ -14929,38 +12728,33 @@ packages:
rgba-regex: 1.0.0
dev: true
- /is-core-module/2.11.0:
- resolution: {integrity: sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==}
+ /is-core-module@2.13.1:
+ resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==}
dependencies:
- has: 1.0.3
- dev: true
+ hasown: 2.0.0
- /is-data-descriptor/0.1.4:
+ /is-data-descriptor@0.1.4:
resolution: {integrity: sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==}
engines: {node: '>=0.10.0'}
dependencies:
kind-of: 3.2.2
dev: true
- /is-data-descriptor/1.0.0:
+ /is-data-descriptor@1.0.0:
resolution: {integrity: sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==}
engines: {node: '>=0.10.0'}
dependencies:
kind-of: 6.0.3
dev: true
- /is-date-object/1.0.5:
+ /is-date-object@1.0.5:
resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==}
engines: {node: '>= 0.4'}
dependencies:
has-tostringtag: 1.0.0
dev: true
- /is-decimal/1.0.4:
- resolution: {integrity: sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==}
- dev: true
-
- /is-descriptor/0.1.6:
+ /is-descriptor@0.1.6:
resolution: {integrity: sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==}
engines: {node: '>=0.10.0'}
dependencies:
@@ -14969,7 +12763,7 @@ packages:
kind-of: 5.1.0
dev: true
- /is-descriptor/1.0.2:
+ /is-descriptor@1.0.2:
resolution: {integrity: sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==}
engines: {node: '>=0.10.0'}
dependencies:
@@ -14978,89 +12772,70 @@ packages:
kind-of: 6.0.3
dev: true
- /is-directory/0.3.1:
+ /is-directory@0.3.1:
resolution: {integrity: sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw==}
engines: {node: '>=0.10.0'}
dev: true
- /is-docker/2.2.1:
+ /is-docker@2.2.1:
resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==}
engines: {node: '>=8'}
- hasBin: true
dev: true
- /is-dom/1.1.0:
- resolution: {integrity: sha512-u82f6mvhYxRPKpw8V1N0W8ce1xXwOrQtgGcxl6UCL5zBmZu3is/18K0rR7uFCnMDuAsS/3W54mGL4vsaFUQlEQ==}
- dependencies:
- is-object: 1.0.2
- is-window: 1.0.2
- dev: true
-
- /is-error/2.2.2:
- resolution: {integrity: sha512-IOQqts/aHWbiisY5DuPJQ0gcbvaLFCa7fBa9xoLfxBZvQ+ZI/Zh9xoI7Gk+G64N0FdK4AbibytHht2tWgpJWLg==}
- dev: true
-
- /is-extendable/0.1.1:
+ /is-extendable@0.1.1:
resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==}
engines: {node: '>=0.10.0'}
dev: true
- /is-extendable/1.0.1:
+ /is-extendable@1.0.1:
resolution: {integrity: sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==}
engines: {node: '>=0.10.0'}
dependencies:
is-plain-object: 2.0.4
dev: true
- /is-extglob/2.1.1:
+ /is-extglob@2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'}
- dev: true
- /is-finite/1.1.0:
- resolution: {integrity: sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==}
- engines: {node: '>=0.10.0'}
+ /is-finalizationregistry@1.0.2:
+ resolution: {integrity: sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==}
+ dependencies:
+ call-bind: 1.0.5
dev: true
- optional: true
- /is-fullwidth-code-point/3.0.0:
+ /is-fullwidth-code-point@3.0.0:
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
engines: {node: '>=8'}
- dev: true
- /is-fullwidth-code-point/4.0.0:
+ /is-fullwidth-code-point@4.0.0:
resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==}
engines: {node: '>=12'}
dev: true
- /is-function/1.0.2:
- resolution: {integrity: sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==}
- dev: true
-
- /is-generator-fn/2.1.0:
- resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==}
- engines: {node: '>=6'}
+ /is-generator-function@1.0.10:
+ resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ has-tostringtag: 1.0.0
dev: true
- /is-glob/3.1.0:
+ /is-glob@3.1.0:
resolution: {integrity: sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==}
engines: {node: '>=0.10.0'}
+ requiresBuild: true
dependencies:
is-extglob: 2.1.1
dev: true
+ optional: true
- /is-glob/4.0.3:
+ /is-glob@4.0.3:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
dependencies:
is-extglob: 2.1.1
- dev: true
-
- /is-hexadecimal/1.0.4:
- resolution: {integrity: sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==}
- dev: true
- /is-installed-globally/0.4.0:
+ /is-installed-globally@0.4.0:
resolution: {integrity: sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==}
engines: {node: '>=10'}
dependencies:
@@ -15068,251 +12843,249 @@ packages:
is-path-inside: 3.0.3
dev: true
- /is-interactive/1.0.0:
+ /is-interactive@1.0.0:
resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==}
engines: {node: '>=8'}
dev: true
- /is-map/2.0.2:
+ /is-map@2.0.2:
resolution: {integrity: sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==}
dev: true
- /is-module/1.0.0:
+ /is-mergeable-object@1.1.1:
+ resolution: {integrity: sha512-CPduJfuGg8h8vW74WOxHtHmtQutyQBzR+3MjQ6iDHIYdbOnm1YC7jv43SqCoU8OPGTJD4nibmiryA4kmogbGrA==}
+ dev: true
+
+ /is-module@1.0.0:
resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==}
dev: true
- /is-negative-zero/2.0.2:
+ /is-negative-zero@2.0.2:
resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==}
engines: {node: '>= 0.4'}
dev: true
- /is-npm/5.0.0:
+ /is-npm@5.0.0:
resolution: {integrity: sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA==}
engines: {node: '>=10'}
dev: true
- /is-number-object/1.0.7:
+ /is-npm@6.0.0:
+ resolution: {integrity: sha512-JEjxbSmtPSt1c8XTkVrlujcXdKV1/tvuQ7GwKcAlyiVLeYFQ2VHat8xfrDJsIkhCdF/tZ7CiIR3sy141c6+gPQ==}
+ engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+ dev: true
+
+ /is-number-object@1.0.7:
resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==}
engines: {node: '>= 0.4'}
dependencies:
has-tostringtag: 1.0.0
dev: true
- /is-number/3.0.0:
+ /is-number@3.0.0:
resolution: {integrity: sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==}
engines: {node: '>=0.10.0'}
dependencies:
kind-of: 3.2.2
dev: true
- /is-number/7.0.0:
+ /is-number@7.0.0:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
- dev: true
- /is-obj/1.0.1:
+ /is-obj@1.0.1:
resolution: {integrity: sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==}
engines: {node: '>=0.10.0'}
dev: true
- /is-obj/2.0.0:
+ /is-obj@2.0.0:
resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==}
engines: {node: '>=8'}
dev: true
- /is-object/1.0.2:
- resolution: {integrity: sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==}
- dev: true
-
- /is-path-cwd/2.2.0:
- resolution: {integrity: sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==}
- engines: {node: '>=6'}
- dev: true
-
- /is-path-inside/3.0.3:
+ /is-path-inside@3.0.3:
resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==}
engines: {node: '>=8'}
dev: true
- /is-plain-obj/2.1.0:
+ /is-plain-obj@2.1.0:
resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==}
engines: {node: '>=8'}
dev: true
- /is-plain-obj/3.0.0:
+ /is-plain-obj@3.0.0:
resolution: {integrity: sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==}
engines: {node: '>=10'}
dev: true
- /is-plain-object/2.0.4:
+ /is-plain-object@2.0.4:
resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==}
engines: {node: '>=0.10.0'}
dependencies:
isobject: 3.0.1
dev: true
- /is-plain-object/5.0.0:
+ /is-plain-object@5.0.0:
resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==}
engines: {node: '>=0.10.0'}
dev: true
- /is-potential-custom-element-name/1.0.1:
- resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
- dev: true
-
- /is-promise/4.0.0:
+ /is-promise@4.0.0:
resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
dev: true
- /is-reference/1.2.1:
- resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==}
- dependencies:
- '@types/estree': 1.0.0
- dev: true
-
- /is-regex/1.1.4:
+ /is-regex@1.1.4:
resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==}
engines: {node: '>= 0.4'}
dependencies:
- call-bind: 1.0.2
+ call-bind: 1.0.5
has-tostringtag: 1.0.0
dev: true
- /is-regexp/1.0.0:
+ /is-regexp@1.0.0:
resolution: {integrity: sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==}
engines: {node: '>=0.10.0'}
dev: true
- /is-resolvable/1.1.0:
+ /is-relative@0.1.3:
+ resolution: {integrity: sha512-wBOr+rNM4gkAZqoLRJI4myw5WzzIdQosFAAbnvfXP5z1LyzgAI3ivOKehC5KfqlQJZoihVhirgtCBj378Eg8GA==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /is-resolvable@1.1.0:
resolution: {integrity: sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==}
dev: true
- /is-set/2.0.2:
+ /is-set@2.0.2:
resolution: {integrity: sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==}
dev: true
- /is-shared-array-buffer/1.0.2:
+ /is-shared-array-buffer@1.0.2:
resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==}
dependencies:
- call-bind: 1.0.2
- dev: true
-
- /is-stream/1.1.0:
- resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==}
- engines: {node: '>=0.10.0'}
+ call-bind: 1.0.5
dev: true
- /is-stream/2.0.1:
+ /is-stream@2.0.1:
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
engines: {node: '>=8'}
dev: true
- /is-string/1.0.7:
+ /is-stream@3.0.0:
+ resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==}
+ engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+ dev: true
+
+ /is-string@1.0.7:
resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==}
engines: {node: '>= 0.4'}
dependencies:
has-tostringtag: 1.0.0
dev: true
- /is-subset/0.1.1:
- resolution: {integrity: sha512-6Ybun0IkarhmEqxXCNw/C0bna6Zb/TkfUX9UbwJtK6ObwAVCxmAP308WWTHviM/zAqXk05cdhYsUsZeGQh99iw==}
- dev: true
-
- /is-symbol/1.0.4:
+ /is-symbol@1.0.4:
resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==}
engines: {node: '>= 0.4'}
dependencies:
has-symbols: 1.0.3
dev: true
- /is-typedarray/1.0.0:
+ /is-typed-array@1.1.12:
+ resolution: {integrity: sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ which-typed-array: 1.1.13
+ dev: true
+
+ /is-typedarray@1.0.0:
resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==}
dev: true
- /is-unicode-supported/0.1.0:
+ /is-unicode-supported@0.1.0:
resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==}
engines: {node: '>=10'}
dev: true
- /is-unicode-supported/1.3.0:
- resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==}
- engines: {node: '>=12'}
+ /is-unicode-supported@2.0.0:
+ resolution: {integrity: sha512-FRdAyx5lusK1iHG0TWpVtk9+1i+GjrzRffhDg4ovQ7mcidMQ6mj+MhKPmvh7Xwyv5gIS06ns49CA7Sqg7lC22Q==}
+ engines: {node: '>=18'}
dev: true
- /is-utf8/0.2.1:
+ /is-utf8@0.2.1:
resolution: {integrity: sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==}
dev: true
- optional: true
- /is-weakref/1.0.2:
- resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==}
- dependencies:
- call-bind: 1.0.2
+ /is-weakmap@2.0.1:
+ resolution: {integrity: sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==}
dev: true
- /is-whitespace-character/1.0.4:
- resolution: {integrity: sha512-SDweEzfIZM0SJV0EUga669UTKlmL0Pq8Lno0QDQsPnvECB3IM2aP0gdx5TrU0A01MAPfViaZiI2V1QMZLaKK5w==}
+ /is-weakref@1.0.2:
+ resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==}
+ dependencies:
+ call-bind: 1.0.5
dev: true
- /is-window/1.0.2:
- resolution: {integrity: sha512-uj00kdXyZb9t9RcAUAwMZAnkBUwdYGhYlt7djMXhfyhUCzwNba50tIiBKR7q0l7tdoBtFVw/3JmLY6fI3rmZmg==}
+ /is-weakset@2.0.2:
+ resolution: {integrity: sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==}
+ dependencies:
+ call-bind: 1.0.5
+ get-intrinsic: 1.2.2
dev: true
- /is-windows/1.0.2:
+ /is-windows@1.0.2:
resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==}
engines: {node: '>=0.10.0'}
dev: true
- /is-word-character/1.0.4:
- resolution: {integrity: sha512-5SMO8RVennx3nZrqtKwCGyyetPE9VDba5ugvKLaD4KopPG5kR4mQ7tNt/r7feL5yt5h3lpuBbIUmCOG2eSzXHA==}
- dev: true
-
- /is-wsl/1.1.0:
+ /is-wsl@1.1.0:
resolution: {integrity: sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==}
engines: {node: '>=4'}
dev: true
- /is-wsl/2.2.0:
+ /is-wsl@2.2.0:
resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
engines: {node: '>=8'}
dependencies:
is-docker: 2.2.1
dev: true
- /is-yarn-global/0.3.0:
+ /is-yarn-global@0.3.0:
resolution: {integrity: sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==}
dev: true
- /isarray/1.0.0:
+ /is-yarn-global@0.4.1:
+ resolution: {integrity: sha512-/kppl+R+LO5VmhYSEWARUFjodS25D68gvj8W7z0I7OWhUla5xWu8KL6CtB2V0R6yqhnRgbcaREMr4EEM6htLPQ==}
+ engines: {node: '>=12'}
+ dev: true
+
+ /isarray@1.0.0:
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
dev: true
- /isarray/2.0.5:
+ /isarray@2.0.5:
resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==}
dev: true
- /isexe/2.0.0:
- resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+ /isexe@1.1.2:
+ resolution: {integrity: sha512-d2eJzK691yZwPHcv1LbeAOa91yMJ9QmfTgSO1oXB65ezVhXQsxBac2vEB4bMVms9cGzaA99n6V2viHMq82VLDw==}
dev: true
- /isobject/2.1.0:
+ /isexe@2.0.0:
+ resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+
+ /isobject@2.1.0:
resolution: {integrity: sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==}
engines: {node: '>=0.10.0'}
dependencies:
isarray: 1.0.0
dev: true
- /isobject/3.0.1:
+ /isobject@3.0.1:
resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==}
engines: {node: '>=0.10.0'}
dev: true
- /isobject/4.0.0:
- resolution: {integrity: sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==}
- engines: {node: '>=0.10.0'}
- dev: true
-
- /isomorphic-unfetch/3.1.0:
+ /isomorphic-unfetch@3.1.0:
resolution: {integrity: sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q==}
dependencies:
node-fetch: 2.6.7
@@ -15321,48 +13094,40 @@ packages:
- encoding
dev: true
- /isstream/0.1.2:
+ /isstream@0.1.2:
resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==}
dev: true
- /istanbul-lib-coverage/3.2.0:
+ /istanbul-lib-coverage@3.2.0:
resolution: {integrity: sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==}
engines: {node: '>=8'}
dev: true
- /istanbul-lib-hook/3.0.0:
- resolution: {integrity: sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==}
+ /istanbul-lib-coverage@3.2.2:
+ resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==}
engines: {node: '>=8'}
- dependencies:
- append-transform: 2.0.0
dev: true
- /istanbul-lib-instrument/4.0.3:
- resolution: {integrity: sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==}
+ /istanbul-lib-hook@3.0.0:
+ resolution: {integrity: sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==}
engines: {node: '>=8'}
dependencies:
- '@babel/core': 7.18.9
- '@istanbuljs/schema': 0.1.3
- istanbul-lib-coverage: 3.2.0
- semver: 6.3.0
- transitivePeerDependencies:
- - supports-color
+ append-transform: 2.0.0
dev: true
- /istanbul-lib-instrument/5.2.1:
- resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==}
+ /istanbul-lib-instrument@4.0.3:
+ resolution: {integrity: sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==}
engines: {node: '>=8'}
dependencies:
'@babel/core': 7.18.9
- '@babel/parser': 7.19.6
'@istanbuljs/schema': 0.1.3
istanbul-lib-coverage: 3.2.0
- semver: 6.3.0
+ semver: 6.3.1
transitivePeerDependencies:
- supports-color
dev: true
- /istanbul-lib-processinfo/2.0.3:
+ /istanbul-lib-processinfo@2.0.3:
resolution: {integrity: sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==}
engines: {node: '>=8'}
dependencies:
@@ -15374,7 +13139,7 @@ packages:
uuid: 8.3.2
dev: true
- /istanbul-lib-report/3.0.0:
+ /istanbul-lib-report@3.0.0:
resolution: {integrity: sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==}
engines: {node: '>=8'}
dependencies:
@@ -15383,7 +13148,16 @@ packages:
supports-color: 7.2.0
dev: true
- /istanbul-lib-source-maps/4.0.1:
+ /istanbul-lib-report@3.0.1:
+ resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==}
+ engines: {node: '>=10'}
+ dependencies:
+ istanbul-lib-coverage: 3.2.2
+ make-dir: 4.0.0
+ supports-color: 7.2.0
+ dev: true
+
+ /istanbul-lib-source-maps@4.0.1:
resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==}
engines: {node: '>=10'}
dependencies:
@@ -15394,7 +13168,7 @@ packages:
- supports-color
dev: true
- /istanbul-reports/3.1.5:
+ /istanbul-reports@3.1.5:
resolution: {integrity: sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==}
engines: {node: '>=8'}
dependencies:
@@ -15402,21 +13176,35 @@ packages:
istanbul-lib-report: 3.0.0
dev: true
- /iterate-iterator/1.0.2:
- resolution: {integrity: sha512-t91HubM4ZDQ70M9wqp+pcNpu8OyJ9UAtXntT/Bcsvp5tZMnz9vRa+IunKXeI8AnfZMTv0jNuVEmGeLSMjVvfPw==}
+ /istanbul-reports@3.1.6:
+ resolution: {integrity: sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==}
+ engines: {node: '>=8'}
+ dependencies:
+ html-escaper: 2.0.2
+ istanbul-lib-report: 3.0.1
dev: true
- /iterate-value/1.0.2:
- resolution: {integrity: sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ==}
+ /iterator.prototype@1.1.2:
+ resolution: {integrity: sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==}
dependencies:
- es-get-iterator: 1.1.2
- iterate-iterator: 1.0.2
+ define-properties: 1.2.1
+ get-intrinsic: 1.2.2
+ has-symbols: 1.0.3
+ reflect.getprototypeof: 1.0.4
+ set-function-name: 2.0.1
dev: true
- /jake/10.8.5:
+ /jackspeak@2.3.6:
+ resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==}
+ engines: {node: '>=14'}
+ dependencies:
+ '@isaacs/cliui': 8.0.2
+ optionalDependencies:
+ '@pkgjs/parseargs': 0.11.0
+
+ /jake@10.8.5:
resolution: {integrity: sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==}
engines: {node: '>=10'}
- hasBin: true
dependencies:
async: 3.2.4
chalk: 4.1.2
@@ -15424,602 +13212,39 @@ packages:
minimatch: 3.1.2
dev: true
- /jed/1.1.1:
+ /jed@1.1.1:
resolution: {integrity: sha512-z35ZSEcXHxLW4yumw0dF6L464NT36vmx3wxJw8MDpraBcWuNVgUPZgPJKcu1HekNgwlMFNqol7i/IpSbjhqwqA==}
- /jest-changed-files/26.6.2:
- resolution: {integrity: sha512-fDS7szLcY9sCtIip8Fjry9oGf3I2ht/QT21bAHm5Dmf0mD4X3ReNUf17y+bO6fR8WgbIZTlbyG1ak/53cbRzKQ==}
- engines: {node: '>= 10.14.2'}
- dependencies:
- '@jest/types': 26.6.2
- execa: 4.1.0
- throat: 5.0.0
- dev: true
-
- /jest-cli/26.6.3:
- resolution: {integrity: sha512-GF9noBSa9t08pSyl3CY4frMrqp+aQXFGFkf5hEPbh/pIUFYWMK6ZLTfbmadxJVcJrdRoChlWQsA2VkJcDFK8hg==}
- engines: {node: '>= 10.14.2'}
- hasBin: true
- dependencies:
- '@jest/core': 26.6.3
- '@jest/test-result': 26.6.2
- '@jest/types': 26.6.2
- chalk: 4.1.2
- exit: 0.1.2
- graceful-fs: 4.2.10
- import-local: 3.1.0
- is-ci: 2.0.0
- jest-config: 26.6.3
- jest-util: 26.6.2
- jest-validate: 26.6.2
- prompts: 2.4.2
- yargs: 15.4.1
- transitivePeerDependencies:
- - bufferutil
- - canvas
- - supports-color
- - ts-node
- - utf-8-validate
- dev: true
-
- /jest-config/26.6.3:
- resolution: {integrity: sha512-t5qdIj/bCj2j7NFVHb2nFB4aUdfucDn3JRKgrZnplb8nieAirAzRSHP8uDEd+qV6ygzg9Pz4YG7UTJf94LPSyg==}
- engines: {node: '>= 10.14.2'}
- peerDependencies:
- ts-node: '>=9.0.0'
- peerDependenciesMeta:
- ts-node:
- optional: true
- dependencies:
- '@babel/core': 7.18.9
- '@jest/test-sequencer': 26.6.3
- '@jest/types': 26.6.2
- babel-jest: 26.6.3_@babel+core@7.18.9
- chalk: 4.1.2
- deepmerge: 4.2.2
- glob: 7.2.3
- graceful-fs: 4.2.10
- jest-environment-jsdom: 26.6.2
- jest-environment-node: 26.6.2
- jest-get-type: 26.3.0
- jest-jasmine2: 26.6.3
- jest-regex-util: 26.0.0
- jest-resolve: 26.6.2
- jest-util: 26.6.2
- jest-validate: 26.6.2
- micromatch: 4.0.5
- pretty-format: 26.6.2
- transitivePeerDependencies:
- - bufferutil
- - canvas
- - supports-color
- - utf-8-validate
- dev: true
-
- /jest-diff/26.6.2:
- resolution: {integrity: sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==}
- engines: {node: '>= 10.14.2'}
- dependencies:
- chalk: 4.1.2
- diff-sequences: 26.6.2
- jest-get-type: 26.3.0
- pretty-format: 26.6.2
- dev: true
-
- /jest-docblock/26.0.0:
- resolution: {integrity: sha512-RDZ4Iz3QbtRWycd8bUEPxQsTlYazfYn/h5R65Fc6gOfwozFhoImx+affzky/FFBuqISPTqjXomoIGJVKBWoo0w==}
- engines: {node: '>= 10.14.2'}
- dependencies:
- detect-newline: 3.1.0
- dev: true
-
- /jest-each/26.6.2:
- resolution: {integrity: sha512-Mer/f0KaATbjl8MCJ+0GEpNdqmnVmDYqCTJYTvoo7rqmRiDllmp2AYN+06F93nXcY3ur9ShIjS+CO/uD+BbH4A==}
- engines: {node: '>= 10.14.2'}
- dependencies:
- '@jest/types': 26.6.2
- chalk: 4.1.2
- jest-get-type: 26.3.0
- jest-util: 26.6.2
- pretty-format: 26.6.2
- dev: true
-
- /jest-environment-jsdom/26.6.2:
- resolution: {integrity: sha512-jgPqCruTlt3Kwqg5/WVFyHIOJHsiAvhcp2qiR2QQstuG9yWox5+iHpU3ZrcBxW14T4fe5Z68jAfLRh7joCSP2Q==}
- engines: {node: '>= 10.14.2'}
- dependencies:
- '@jest/environment': 26.6.2
- '@jest/fake-timers': 26.6.2
- '@jest/types': 26.6.2
- '@types/node': 18.11.9
- jest-mock: 26.6.2
- jest-util: 26.6.2
- jsdom: 16.7.0
- transitivePeerDependencies:
- - bufferutil
- - canvas
- - supports-color
- - utf-8-validate
- dev: true
-
- /jest-environment-node/26.6.2:
- resolution: {integrity: sha512-zhtMio3Exty18dy8ee8eJ9kjnRyZC1N4C1Nt/VShN1apyXc8rWGtJ9lI7vqiWcyyXS4BVSEn9lxAM2D+07/Tag==}
- engines: {node: '>= 10.14.2'}
- dependencies:
- '@jest/environment': 26.6.2
- '@jest/fake-timers': 26.6.2
- '@jest/types': 26.6.2
- '@types/node': 18.11.9
- jest-mock: 26.6.2
- jest-util: 26.6.2
- dev: true
-
- /jest-get-type/26.3.0:
- resolution: {integrity: sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==}
- engines: {node: '>= 10.14.2'}
- dev: true
-
- /jest-haste-map/26.6.2:
- resolution: {integrity: sha512-easWIJXIw71B2RdR8kgqpjQrbMRWQBgiBwXYEhtGUTaX+doCjBheluShdDMeR8IMfJiTqH4+zfhtg29apJf/8w==}
- engines: {node: '>= 10.14.2'}
- dependencies:
- '@jest/types': 26.6.2
- '@types/graceful-fs': 4.1.5
- '@types/node': 18.11.9
- anymatch: 3.1.2
- fb-watchman: 2.0.2
- graceful-fs: 4.2.10
- jest-regex-util: 26.0.0
- jest-serializer: 26.6.2
- jest-util: 26.6.2
- jest-worker: 26.6.2
- micromatch: 4.0.5
- sane: 4.1.0
- walker: 1.0.8
- optionalDependencies:
- fsevents: 2.3.2
- transitivePeerDependencies:
- - supports-color
- dev: true
-
- /jest-haste-map/27.5.1:
- resolution: {integrity: sha512-7GgkZ4Fw4NFbMSDSpZwXeBiIbx+t/46nJ2QitkOjvwPYyZmqttu2TDSimMHP1EkPOi4xUZAN1doE5Vd25H4Jng==}
- engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
- dependencies:
- '@jest/types': 27.5.1
- '@types/graceful-fs': 4.1.5
- '@types/node': 18.11.9
- anymatch: 3.1.2
- fb-watchman: 2.0.2
- graceful-fs: 4.2.10
- jest-regex-util: 27.5.1
- jest-serializer: 27.5.1
- jest-util: 27.5.1
- jest-worker: 27.5.1
- micromatch: 4.0.5
- walker: 1.0.8
- optionalDependencies:
- fsevents: 2.3.2
- dev: true
-
- /jest-jasmine2/26.6.3:
- resolution: {integrity: sha512-kPKUrQtc8aYwBV7CqBg5pu+tmYXlvFlSFYn18ev4gPFtrRzB15N2gW/Roew3187q2w2eHuu0MU9TJz6w0/nPEg==}
- engines: {node: '>= 10.14.2'}
- dependencies:
- '@babel/traverse': 7.19.6
- '@jest/environment': 26.6.2
- '@jest/source-map': 26.6.2
- '@jest/test-result': 26.6.2
- '@jest/types': 26.6.2
- '@types/node': 18.11.9
- chalk: 4.1.2
- co: 4.6.0
- expect: 26.6.2
- is-generator-fn: 2.1.0
- jest-each: 26.6.2
- jest-matcher-utils: 26.6.2
- jest-message-util: 26.6.2
- jest-runtime: 26.6.3
- jest-snapshot: 26.6.2
- jest-util: 26.6.2
- pretty-format: 26.6.2
- throat: 5.0.0
- transitivePeerDependencies:
- - bufferutil
- - canvas
- - supports-color
- - ts-node
- - utf-8-validate
- dev: true
-
- /jest-leak-detector/26.6.2:
- resolution: {integrity: sha512-i4xlXpsVSMeKvg2cEKdfhh0H39qlJlP5Ex1yQxwF9ubahboQYMgTtz5oML35AVA3B4Eu+YsmwaiKVev9KCvLxg==}
- engines: {node: '>= 10.14.2'}
- dependencies:
- jest-get-type: 26.3.0
- pretty-format: 26.6.2
- dev: true
-
- /jest-matcher-utils/26.6.2:
- resolution: {integrity: sha512-llnc8vQgYcNqDrqRDXWwMr9i7rS5XFiCwvh6DTP7Jqa2mqpcCBBlpCbn+trkG0KNhPu/h8rzyBkriOtBstvWhw==}
- engines: {node: '>= 10.14.2'}
- dependencies:
- chalk: 4.1.2
- jest-diff: 26.6.2
- jest-get-type: 26.3.0
- pretty-format: 26.6.2
- dev: true
-
- /jest-message-util/26.6.2:
- resolution: {integrity: sha512-rGiLePzQ3AzwUshu2+Rn+UMFk0pHN58sOG+IaJbk5Jxuqo3NYO1U2/MIR4S1sKgsoYSXSzdtSa0TgrmtUwEbmA==}
- engines: {node: '>= 10.14.2'}
- dependencies:
- '@babel/code-frame': 7.18.6
- '@jest/types': 26.6.2
- '@types/stack-utils': 2.0.1
- chalk: 4.1.2
- graceful-fs: 4.2.10
- micromatch: 4.0.5
- pretty-format: 26.6.2
- slash: 3.0.0
- stack-utils: 2.0.5
- dev: true
-
- /jest-message-util/27.5.1:
- resolution: {integrity: sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g==}
- engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
- dependencies:
- '@babel/code-frame': 7.18.6
- '@jest/types': 27.5.1
- '@types/stack-utils': 2.0.1
- chalk: 4.1.2
- graceful-fs: 4.2.10
- micromatch: 4.0.5
- pretty-format: 27.5.1
- slash: 3.0.0
- stack-utils: 2.0.5
- dev: true
-
- /jest-mock/26.6.2:
- resolution: {integrity: sha512-YyFjePHHp1LzpzYcmgqkJ0nm0gg/lJx2aZFzFy1S6eUqNjXsOqTK10zNRff2dNfssgokjkG65OlWNcIlgd3zew==}
- engines: {node: '>= 10.14.2'}
- dependencies:
- '@jest/types': 26.6.2
- '@types/node': 18.11.9
- dev: true
-
- /jest-pnp-resolver/1.2.2_jest-resolve@26.6.2:
- resolution: {integrity: sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==}
- engines: {node: '>=6'}
- peerDependencies:
- jest-resolve: '*'
- peerDependenciesMeta:
- jest-resolve:
- optional: true
- dependencies:
- jest-resolve: 26.6.2
- dev: true
-
- /jest-preset-preact/4.0.5_k4rseuq4tu3ktjhgekqzusjwfq:
- resolution: {integrity: sha512-MnU7mfpnwopJkdx0WoEyRmrNDIvRN+w6sOur0zEhaRYYMo0gJM7UdZHWTV8k6uo0+ypY+m0kQW6kMukUx4v8JQ==}
- peerDependencies:
- jest: 26.x || 27.x
- preact: 10.x
- preact-render-to-string: 5.x
- dependencies:
- '@babel/core': 7.18.9
- '@babel/plugin-proposal-class-properties': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-transform-react-jsx': 7.19.0_@babel+core@7.18.9
- '@babel/preset-env': 7.18.9_@babel+core@7.18.9
- '@babel/preset-typescript': 7.18.6_@babel+core@7.18.9
- babel-jest: 27.5.1_@babel+core@7.18.9
- identity-obj-proxy: 3.0.0
- isomorphic-unfetch: 3.1.0
- jest: 26.6.3
- jest-watch-typeahead: 0.6.5_jest@26.6.3
- preact: 10.11.2
- preact-render-to-string: 5.2.6_preact@10.11.2
- transitivePeerDependencies:
- - encoding
- - supports-color
- dev: true
-
- /jest-preset-preact/4.0.5_moqeqtbsr7edkxzj3jgnhqkxsm:
- resolution: {integrity: sha512-MnU7mfpnwopJkdx0WoEyRmrNDIvRN+w6sOur0zEhaRYYMo0gJM7UdZHWTV8k6uo0+ypY+m0kQW6kMukUx4v8JQ==}
- peerDependencies:
- jest: 26.x || 27.x
- preact: 10.x
- preact-render-to-string: 5.x
- dependencies:
- '@babel/core': 7.18.9
- '@babel/plugin-proposal-class-properties': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-transform-react-jsx': 7.19.0_@babel+core@7.18.9
- '@babel/preset-env': 7.18.9_@babel+core@7.18.9
- '@babel/preset-typescript': 7.18.6_@babel+core@7.18.9
- babel-jest: 27.5.1_@babel+core@7.18.9
- identity-obj-proxy: 3.0.0
- isomorphic-unfetch: 3.1.0
- jest: 26.6.3
- jest-watch-typeahead: 0.6.5_jest@26.6.3
- preact: 10.6.5
- preact-render-to-string: 5.2.6_preact@10.6.5
- transitivePeerDependencies:
- - encoding
- - supports-color
- dev: true
-
- /jest-regex-util/26.0.0:
- resolution: {integrity: sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A==}
- engines: {node: '>= 10.14.2'}
- dev: true
-
- /jest-regex-util/27.5.1:
- resolution: {integrity: sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg==}
- engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
- dev: true
-
- /jest-resolve-dependencies/26.6.3:
- resolution: {integrity: sha512-pVwUjJkxbhe4RY8QEWzN3vns2kqyuldKpxlxJlzEYfKSvY6/bMvxoFrYYzUO1Gx28yKWN37qyV7rIoIp2h8fTg==}
- engines: {node: '>= 10.14.2'}
- dependencies:
- '@jest/types': 26.6.2
- jest-regex-util: 26.0.0
- jest-snapshot: 26.6.2
- transitivePeerDependencies:
- - supports-color
- dev: true
-
- /jest-resolve/26.6.2:
- resolution: {integrity: sha512-sOxsZOq25mT1wRsfHcbtkInS+Ek7Q8jCHUB0ZUTP0tc/c41QHriU/NunqMfCUWsL4H3MHpvQD4QR9kSYhS7UvQ==}
- engines: {node: '>= 10.14.2'}
- dependencies:
- '@jest/types': 26.6.2
- chalk: 4.1.2
- graceful-fs: 4.2.10
- jest-pnp-resolver: 1.2.2_jest-resolve@26.6.2
- jest-util: 26.6.2
- read-pkg-up: 7.0.1
- resolve: 1.22.1
- slash: 3.0.0
- dev: true
-
- /jest-runner/26.6.3:
- resolution: {integrity: sha512-atgKpRHnaA2OvByG/HpGA4g6CSPS/1LK0jK3gATJAoptC1ojltpmVlYC3TYgdmGp+GLuhzpH30Gvs36szSL2JQ==}
- engines: {node: '>= 10.14.2'}
- dependencies:
- '@jest/console': 26.6.2
- '@jest/environment': 26.6.2
- '@jest/test-result': 26.6.2
- '@jest/types': 26.6.2
- '@types/node': 18.11.9
- chalk: 4.1.2
- emittery: 0.7.2
- exit: 0.1.2
- graceful-fs: 4.2.10
- jest-config: 26.6.3
- jest-docblock: 26.0.0
- jest-haste-map: 26.6.2
- jest-leak-detector: 26.6.2
- jest-message-util: 26.6.2
- jest-resolve: 26.6.2
- jest-runtime: 26.6.3
- jest-util: 26.6.2
- jest-worker: 26.6.2
- source-map-support: 0.5.21
- throat: 5.0.0
- transitivePeerDependencies:
- - bufferutil
- - canvas
- - supports-color
- - ts-node
- - utf-8-validate
- dev: true
-
- /jest-runtime/26.6.3:
- resolution: {integrity: sha512-lrzyR3N8sacTAMeonbqpnSka1dHNux2uk0qqDXVkMv2c/A3wYnvQ4EXuI013Y6+gSKSCxdaczvf4HF0mVXHRdw==}
- engines: {node: '>= 10.14.2'}
- hasBin: true
- dependencies:
- '@jest/console': 26.6.2
- '@jest/environment': 26.6.2
- '@jest/fake-timers': 26.6.2
- '@jest/globals': 26.6.2
- '@jest/source-map': 26.6.2
- '@jest/test-result': 26.6.2
- '@jest/transform': 26.6.2
- '@jest/types': 26.6.2
- '@types/yargs': 15.0.14
- chalk: 4.1.2
- cjs-module-lexer: 0.6.0
- collect-v8-coverage: 1.0.1
- exit: 0.1.2
- glob: 7.2.3
- graceful-fs: 4.2.10
- jest-config: 26.6.3
- jest-haste-map: 26.6.2
- jest-message-util: 26.6.2
- jest-mock: 26.6.2
- jest-regex-util: 26.0.0
- jest-resolve: 26.6.2
- jest-snapshot: 26.6.2
- jest-util: 26.6.2
- jest-validate: 26.6.2
- slash: 3.0.0
- strip-bom: 4.0.0
- yargs: 15.4.1
- transitivePeerDependencies:
- - bufferutil
- - canvas
- - supports-color
- - ts-node
- - utf-8-validate
- dev: true
-
- /jest-serializer/26.6.2:
- resolution: {integrity: sha512-S5wqyz0DXnNJPd/xfIzZ5Xnp1HrJWBczg8mMfMpN78OJ5eDxXyf+Ygld9wX1DnUWbIbhM1YDY95NjR4CBXkb2g==}
- engines: {node: '>= 10.14.2'}
- dependencies:
- '@types/node': 18.11.9
- graceful-fs: 4.2.10
- dev: true
-
- /jest-serializer/27.5.1:
- resolution: {integrity: sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w==}
- engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
- dependencies:
- '@types/node': 18.11.9
- graceful-fs: 4.2.10
- dev: true
-
- /jest-snapshot/26.6.2:
- resolution: {integrity: sha512-OLhxz05EzUtsAmOMzuupt1lHYXCNib0ECyuZ/PZOx9TrZcC8vL0x+DUG3TL+GLX3yHG45e6YGjIm0XwDc3q3og==}
- engines: {node: '>= 10.14.2'}
- dependencies:
- '@babel/types': 7.19.4
- '@jest/types': 26.6.2
- '@types/babel__traverse': 7.18.2
- '@types/prettier': 2.7.1
- chalk: 4.1.2
- expect: 26.6.2
- graceful-fs: 4.2.10
- jest-diff: 26.6.2
- jest-get-type: 26.3.0
- jest-haste-map: 26.6.2
- jest-matcher-utils: 26.6.2
- jest-message-util: 26.6.2
- jest-resolve: 26.6.2
- natural-compare: 1.4.0
- pretty-format: 26.6.2
- semver: 7.3.8
- transitivePeerDependencies:
- - supports-color
- dev: true
-
- /jest-util/26.6.2:
- resolution: {integrity: sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==}
- engines: {node: '>= 10.14.2'}
- dependencies:
- '@jest/types': 26.6.2
- '@types/node': 18.11.9
- chalk: 4.1.2
- graceful-fs: 4.2.10
- is-ci: 2.0.0
- micromatch: 4.0.5
- dev: true
-
- /jest-util/27.5.1:
- resolution: {integrity: sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==}
- engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
- dependencies:
- '@jest/types': 27.5.1
- '@types/node': 18.11.9
- chalk: 4.1.2
- ci-info: 3.5.0
- graceful-fs: 4.2.10
- picomatch: 2.3.1
- dev: true
-
- /jest-validate/26.6.2:
- resolution: {integrity: sha512-NEYZ9Aeyj0i5rQqbq+tpIOom0YS1u2MVu6+euBsvpgIme+FOfRmoC4R5p0JiAUpaFvFy24xgrpMknarR/93XjQ==}
- engines: {node: '>= 10.14.2'}
- dependencies:
- '@jest/types': 26.6.2
- camelcase: 6.3.0
- chalk: 4.1.2
- jest-get-type: 26.3.0
- leven: 3.1.0
- pretty-format: 26.6.2
- dev: true
-
- /jest-watch-typeahead/0.6.5_jest@26.6.3:
- resolution: {integrity: sha512-GIbV6h37/isatMDtqZlA8Q5vC6T3w+5qdvtF+3LIkPc58zEWzbKmTHvlUIp3wvBm400RzrQWcVPcsAJqKWu7XQ==}
- engines: {node: '>=10'}
- peerDependencies:
- jest: ^26.0.0 || ^27.0.0
- dependencies:
- ansi-escapes: 4.3.2
- chalk: 4.1.2
- jest: 26.6.3
- jest-regex-util: 27.5.1
- jest-watcher: 27.5.1
- slash: 3.0.0
- string-length: 4.0.2
- strip-ansi: 6.0.1
- dev: true
-
- /jest-watcher/26.6.2:
- resolution: {integrity: sha512-WKJob0P/Em2csiVthsI68p6aGKTIcsfjH9Gsx1f0A3Italz43e3ho0geSAVsmj09RWOELP1AZ/DXyJgOgDKxXQ==}
- engines: {node: '>= 10.14.2'}
- dependencies:
- '@jest/test-result': 26.6.2
- '@jest/types': 26.6.2
- '@types/node': 18.11.9
- ansi-escapes: 4.3.2
- chalk: 4.1.2
- jest-util: 26.6.2
- string-length: 4.0.2
- dev: true
-
- /jest-watcher/27.5.1:
- resolution: {integrity: sha512-z676SuD6Z8o8qbmEGhoEUFOM1+jfEiL3DXHK/xgEiG2EyNYfFG60jluWcupY6dATjfEsKQuibReS1djInQnoVw==}
- engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
- dependencies:
- '@jest/test-result': 27.5.1
- '@jest/types': 27.5.1
- '@types/node': 18.11.9
- ansi-escapes: 4.3.2
- chalk: 4.1.2
- jest-util: 27.5.1
- string-length: 4.0.2
- dev: true
-
- /jest-worker/26.6.2:
+ /jest-worker@26.6.2:
resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==}
engines: {node: '>= 10.13.0'}
dependencies:
- '@types/node': 18.11.9
+ '@types/node': 20.11.13
merge-stream: 2.0.0
supports-color: 7.2.0
dev: true
- /jest-worker/27.5.1:
- resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==}
- engines: {node: '>= 10.13.0'}
- dependencies:
- '@types/node': 18.11.9
- merge-stream: 2.0.0
- supports-color: 8.1.1
- dev: true
-
- /jest/26.6.3:
- resolution: {integrity: sha512-lGS5PXGAzR4RF7V5+XObhqz2KZIDUA1yD0DG6pBVmy10eh0ZIXQImRuzocsI/N2XZ1GrLFwTS27In2i2jlpq1Q==}
- engines: {node: '>= 10.14.2'}
+ /jiti@1.18.2:
+ resolution: {integrity: sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==}
hasBin: true
- dependencies:
- '@jest/core': 26.6.3
- import-local: 3.1.0
- jest-cli: 26.6.3
- transitivePeerDependencies:
- - bufferutil
- - canvas
- - supports-color
- - ts-node
- - utf-8-validate
+
+ /jose@4.13.1:
+ resolution: {integrity: sha512-MSJQC5vXco5Br38mzaQKiq9mwt7lwj2eXpgpRyQYNHYt2lq1PjkWa7DLXX0WVcQLE9HhMh3jPiufS7fhJf+CLQ==}
dev: true
- /js-sdsl/4.1.5:
+ /js-sdsl@4.1.5:
resolution: {integrity: sha512-08bOAKweV2NUC1wqTtf3qZlnpOX/R2DU9ikpjOHs0H+ibQv3zpncVQg6um4uYtRtrwIX8M4Nh3ytK4HGlYAq7Q==}
dev: true
- /js-string-escape/1.0.1:
+ /js-string-escape@1.0.1:
resolution: {integrity: sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==}
engines: {node: '>= 0.8'}
dev: true
- /js-tokens/4.0.0:
+ /js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
- /js-yaml/3.14.1:
+ /js-yaml@3.14.1:
resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==}
hasBin: true
dependencies:
@@ -16027,18 +13252,18 @@ packages:
esprima: 4.0.1
dev: true
- /js-yaml/4.1.0:
+ /js-yaml@4.1.0:
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
hasBin: true
dependencies:
argparse: 2.0.1
dev: true
- /jsbn/0.1.1:
+ /jsbn@0.1.1:
resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==}
dev: true
- /jsdom/12.2.0:
+ /jsdom@12.2.0:
resolution: {integrity: sha512-QPOggIJ8fquWPLaYYMoh+zqUmdphDtu1ju0QGTitZT1Yd8I5qenPpXM1etzUegu3MjVp8XPzgZxdn8Yj7e40ig==}
engines: {node: '>=8'}
dependencies:
@@ -16056,7 +13281,7 @@ packages:
parse5: 5.1.0
pn: 1.1.0
request: 2.88.2
- request-promise-native: 1.0.9_request@2.88.2
+ request-promise-native: 1.0.9(request@2.88.2)
saxes: 3.1.11
symbol-tree: 3.2.4
tough-cookie: 2.5.0
@@ -16072,127 +13297,108 @@ packages:
- utf-8-validate
dev: true
- /jsdom/16.7.0:
- resolution: {integrity: sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==}
- engines: {node: '>=10'}
- peerDependencies:
- canvas: ^2.5.0
- peerDependenciesMeta:
- canvas:
- optional: true
- dependencies:
- abab: 2.0.6
- acorn: 8.8.1
- acorn-globals: 6.0.0
- cssom: 0.4.4
- cssstyle: 2.3.0
- data-urls: 2.0.0
- decimal.js: 10.4.2
- domexception: 2.0.1
- escodegen: 2.0.0
- form-data: 3.0.1
- html-encoding-sniffer: 2.0.1
- http-proxy-agent: 4.0.1
- https-proxy-agent: 5.0.1
- is-potential-custom-element-name: 1.0.1
- nwsapi: 2.2.2
- parse5: 6.0.1
- saxes: 5.0.1
- symbol-tree: 3.2.4
- tough-cookie: 4.1.2
- w3c-hr-time: 1.0.2
- w3c-xmlserializer: 2.0.0
- webidl-conversions: 6.1.0
- whatwg-encoding: 1.0.5
- whatwg-mimetype: 2.3.0
- whatwg-url: 8.7.0
- ws: 7.5.9
- xml-name-validator: 3.0.0
- transitivePeerDependencies:
- - bufferutil
- - supports-color
- - utf-8-validate
- dev: true
-
- /jsesc/0.5.0:
+ /jsesc@0.5.0:
resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==}
hasBin: true
dev: true
- /jsesc/2.5.2:
+ /jsesc@2.5.2:
resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==}
engines: {node: '>=4'}
hasBin: true
+
+ /json-buffer@3.0.0:
+ resolution: {integrity: sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==}
dev: true
- /json-buffer/3.0.0:
- resolution: {integrity: sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=}
+ /json-buffer@3.0.1:
+ resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
+ dev: true
+
+ /json-merge-patch@1.0.2:
+ resolution: {integrity: sha512-M6Vp2GN9L7cfuMXiWOmHj9bEFbeC250iVtcKQbqVgEsDVYnIsrNsbU+h/Y/PkbBQCtEa4Bez+Ebv0zfbC8ObLg==}
+ dependencies:
+ fast-deep-equal: 3.1.3
dev: true
- /json-parse-better-errors/1.0.2:
+ /json-parse-better-errors@1.0.2:
resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==}
dev: true
- /json-parse-even-better-errors/2.3.1:
+ /json-parse-even-better-errors@2.3.1:
resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
dev: true
- /json-schema-traverse/0.4.1:
+ /json-schema-traverse@0.4.1:
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
dev: true
- /json-schema-traverse/1.0.0:
+ /json-schema-traverse@1.0.0:
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
dev: true
- /json-schema/0.4.0:
+ /json-schema@0.4.0:
resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==}
dev: true
- /json-stable-stringify-without-jsonify/1.0.1:
+ /json-stable-stringify-without-jsonify@1.0.1:
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
dev: true
- /json-stringify-safe/5.0.1:
+ /json-stringify-safe@5.0.1:
resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==}
dev: true
- /json5/0.5.1:
+ /json5@0.5.1:
resolution: {integrity: sha512-4xrs1aW+6N5DalkqSVA8fxh458CXvR99WU8WLKmq4v8eWAL86Xo3BVqyd3SkA9wEVjCMqyvvRRkshAdOnBp5rw==}
hasBin: true
dev: true
- /json5/1.0.1:
- resolution: {integrity: sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==}
+ /json5@1.0.2:
+ resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==}
hasBin: true
dependencies:
minimist: 1.2.7
dev: true
- /json5/2.2.1:
+ /json5@2.2.1:
resolution: {integrity: sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==}
engines: {node: '>=6'}
- hasBin: true
dev: true
- /jsonc-parser/3.2.0:
+ /json5@2.2.3:
+ resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ /jsonc-parser@3.2.0:
resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==}
dev: true
- /jsonfile/6.1.0:
+ /jsonfile@6.1.0:
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
dependencies:
universalify: 2.0.0
optionalDependencies:
- graceful-fs: 4.2.10
+ graceful-fs: 4.2.11
dev: true
- /jsonpointer/5.0.1:
+ /jsonpointer@5.0.1:
resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==}
engines: {node: '>=0.10.0'}
dev: true
- /jsprim/1.4.2:
+ /jsonwebtoken@9.0.0:
+ resolution: {integrity: sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==}
+ engines: {node: '>=12', npm: '>=6'}
+ dependencies:
+ jws: 3.2.2
+ lodash: 4.17.21
+ ms: 2.1.3
+ semver: 7.5.4
+ dev: true
+
+ /jsprim@1.4.2:
resolution: {integrity: sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==}
engines: {node: '>=0.6.0'}
dependencies:
@@ -16202,109 +13408,144 @@ packages:
verror: 1.10.0
dev: true
- /jssha/3.3.0:
+ /jsqr@1.4.0:
+ resolution: {integrity: sha512-dxLob7q65Xg2DvstYkRpkYtmKm2sPJ9oFhrhmudT1dZvNFFTlroai3AWSpLey/w5vMcLBXRgOJsbXpdN9HzU/A==}
+ dev: false
+
+ /jssha@3.3.0:
resolution: {integrity: sha512-w9OtT4ALL+fbbwG3gw7erAO0jvS5nfvrukGPMWIAoea359B26ALXGpzy4YJSp9yGnpUvuvOw1nSjSoHDfWSr1w==}
- dev: true
+ dev: false
- /jsx-ast-utils/3.3.3:
- resolution: {integrity: sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw==}
+ /jsx-ast-utils@3.3.5:
+ resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
engines: {node: '>=4.0'}
dependencies:
- array-includes: 3.1.5
- object.assign: 4.1.4
+ array-includes: 3.1.7
+ array.prototype.flat: 1.3.2
+ object.assign: 4.1.5
+ object.values: 1.1.7
dev: true
- /junk/3.1.0:
- resolution: {integrity: sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==}
- engines: {node: '>=8'}
+ /jszip@3.10.1:
+ resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
+ dependencies:
+ lie: 3.3.0
+ pako: 1.0.11
+ readable-stream: 2.3.8
+ setimmediate: 1.0.5
+ dev: true
+
+ /jwa@1.4.1:
+ resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==}
+ dependencies:
+ buffer-equal-constant-time: 1.0.1
+ ecdsa-sig-formatter: 1.0.11
+ safe-buffer: 5.2.1
dev: true
- /keyv/3.1.0:
+ /jws@3.2.2:
+ resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==}
+ dependencies:
+ jwa: 1.4.1
+ safe-buffer: 5.2.1
+ dev: true
+
+ /keyv@3.1.0:
resolution: {integrity: sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==}
dependencies:
json-buffer: 3.0.0
dev: true
- /kind-of/3.2.2:
+ /keyv@4.5.4:
+ resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
+ dependencies:
+ json-buffer: 3.0.1
+ dev: true
+
+ /kind-of@3.2.2:
resolution: {integrity: sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==}
engines: {node: '>=0.10.0'}
dependencies:
is-buffer: 1.1.6
dev: true
- /kind-of/4.0.0:
+ /kind-of@4.0.0:
resolution: {integrity: sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==}
engines: {node: '>=0.10.0'}
dependencies:
is-buffer: 1.1.6
dev: true
- /kind-of/5.1.0:
+ /kind-of@5.1.0:
resolution: {integrity: sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==}
engines: {node: '>=0.10.0'}
dev: true
- /kind-of/6.0.3:
+ /kind-of@6.0.3:
resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
engines: {node: '>=0.10.0'}
dev: true
- /kleur/3.0.3:
+ /kleur@3.0.3:
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
engines: {node: '>=6'}
dev: true
- /kleur/4.1.5:
+ /kleur@4.1.5:
resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
engines: {node: '>=6'}
dev: true
- /klona/2.0.5:
+ /klona@2.0.5:
resolution: {integrity: sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ==}
engines: {node: '>= 8'}
dev: true
- /language-subtag-registry/0.3.22:
+ /language-subtag-registry@0.3.22:
resolution: {integrity: sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==}
dev: true
- /language-tags/1.0.5:
- resolution: {integrity: sha512-qJhlO9cGXi6hBGKoxEG/sKZDAHD5Hnu9Hs4WbOY3pCWXDhw0N8x1NenNzm2EnNLkLkk7J2SdxAkDSbb6ftT+UQ==}
+ /language-tags@1.0.9:
+ resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==}
+ engines: {node: '>=0.10'}
dependencies:
language-subtag-registry: 0.3.22
dev: true
- /last-call-webpack-plugin/3.0.0:
+ /last-call-webpack-plugin@3.0.0:
resolution: {integrity: sha512-7KI2l2GIZa9p2spzPIVZBYyNKkN+e/SQPpnjlTiPhdbDW3F86tdKKELxKpzJ5sgU19wQWsACULZmpTPYHeWO5w==}
dependencies:
lodash: 4.17.21
webpack-sources: 1.4.3
dev: true
- /latest-version/5.1.0:
+ /latest-version@5.1.0:
resolution: {integrity: sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==}
engines: {node: '>=8'}
dependencies:
package-json: 6.5.0
dev: true
- /lazy-universal-dotenv/3.0.1:
- resolution: {integrity: sha512-prXSYk799h3GY3iOWnC6ZigYzMPjxN2svgjJ9shk7oMadSNX3wXy0B6F32PMJv7qtMnrIbUxoEHzbutvxR2LBQ==}
- engines: {node: '>=6.0.0', npm: '>=6.0.0', yarn: '>=1.0.0'}
+ /latest-version@7.0.0:
+ resolution: {integrity: sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg==}
+ engines: {node: '>=14.16'}
dependencies:
- '@babel/runtime': 7.19.4
- app-root-dir: 1.0.2
- core-js: 3.26.0
- dotenv: 8.6.0
- dotenv-expand: 5.1.0
+ package-json: 8.1.1
+ dev: true
+
+ /lcid@3.1.1:
+ resolution: {integrity: sha512-M6T051+5QCGLBQb8id3hdvIW8+zeFV2FyBGFS9IEK5H9Wt4MueD4bW1eWikpHgZp+5xR3l5c8pZUkQsIA0BFZg==}
+ engines: {node: '>=8'}
+ dependencies:
+ invert-kv: 3.0.1
dev: true
- /leven/3.1.0:
+ /leven@3.1.0:
resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==}
engines: {node: '>=6'}
dev: true
- /levn/0.3.0:
+ /levn@0.3.0:
resolution: {integrity: sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==}
engines: {node: '>= 0.8.0'}
dependencies:
@@ -16312,7 +13553,7 @@ packages:
type-check: 0.3.2
dev: true
- /levn/0.4.1:
+ /levn@0.4.1:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'}
dependencies:
@@ -16320,43 +13561,44 @@ packages:
type-check: 0.4.0
dev: true
- /lilconfig/2.0.6:
- resolution: {integrity: sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==}
- engines: {node: '>=10'}
+ /lie@3.3.0:
+ resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
+ dependencies:
+ immediate: 3.0.6
dev: true
- /lines-and-columns/1.2.4:
- resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
+ /lighthouse-logger@1.4.2:
+ resolution: {integrity: sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==}
+ dependencies:
+ debug: 2.6.9
+ marky: 1.2.5
+ transitivePeerDependencies:
+ - supports-color
dev: true
- /load-json-file/1.1.0:
- resolution: {integrity: sha512-cy7ZdNRXdablkXYNI049pthVeXFurRyb9+hA/dZzerZ0pGTx42z+y+ssxBaVV2l70t1muq5IdKhn4UtcoGUY9A==}
- engines: {node: '>=0.10.0'}
- dependencies:
- graceful-fs: 4.2.10
- parse-json: 2.2.0
- pify: 2.3.0
- pinkie-promise: 2.0.1
- strip-bom: 2.0.0
+ /lilconfig@2.1.0:
+ resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==}
+ engines: {node: '>=10'}
+
+ /lines-and-columns@1.2.4:
+ resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
+
+ /lines-and-columns@2.0.4:
+ resolution: {integrity: sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A==}
+ engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
dev: true
- optional: true
- /load-json-file/7.0.1:
+ /load-json-file@7.0.1:
resolution: {integrity: sha512-Gnxj3ev3mB5TkVBGad0JM6dmLiQL+o0t23JPBZ9sd+yvSLk05mFoqKBw5N8gbbkU4TNXyqCgIrl/VM17OgUIgQ==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
dev: true
- /loader-runner/2.4.0:
+ /loader-runner@2.4.0:
resolution: {integrity: sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==}
engines: {node: '>=4.3.0 <5.0.0 || >=5.10'}
dev: true
- /loader-runner/4.3.0:
- resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==}
- engines: {node: '>=6.11.5'}
- dev: true
-
- /loader-utils/0.2.17:
+ /loader-utils@0.2.17:
resolution: {integrity: sha512-tiv66G0SmiOx+pLWMtGEkfSEejxvb6N6uRrQjfWJIT79W9GMpgKeCAmm9aVBKtd4WEgntciI8CsGqjpDoCWJug==}
dependencies:
big.js: 3.2.0
@@ -16365,30 +13607,39 @@ packages:
object-assign: 4.1.1
dev: true
- /loader-utils/1.4.0:
+ /loader-utils@1.4.0:
resolution: {integrity: sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==}
engines: {node: '>=4.0.0'}
dependencies:
big.js: 5.2.2
emojis-list: 3.0.0
- json5: 1.0.1
+ json5: 1.0.2
+ dev: true
+
+ /loader-utils@1.4.2:
+ resolution: {integrity: sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==}
+ engines: {node: '>=4.0.0'}
+ dependencies:
+ big.js: 5.2.2
+ emojis-list: 3.0.0
+ json5: 1.0.2
dev: true
- /loader-utils/2.0.3:
+ /loader-utils@2.0.3:
resolution: {integrity: sha512-THWqIsn8QRnvLl0shHYVBN9syumU8pYWEHPTmkiVGd+7K5eFNVSY6AJhRvgGF70gg1Dz+l/k8WicvFCxdEs60A==}
engines: {node: '>=8.9.0'}
dependencies:
big.js: 5.2.2
emojis-list: 3.0.0
- json5: 2.2.1
+ json5: 2.2.3
dev: true
- /local-access/1.1.0:
+ /local-access@1.1.0:
resolution: {integrity: sha512-XfegD5pyTAfb+GY6chk283Ox5z8WexG56OvM06RWLpAc/UHozO8X6xAxEkIitZOtsSMM1Yr3DkHgW5W+onLhCw==}
engines: {node: '>=6'}
dev: true
- /locate-path/3.0.0:
+ /locate-path@3.0.0:
resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==}
engines: {node: '>=6'}
dependencies:
@@ -16396,71 +13647,64 @@ packages:
path-exists: 3.0.0
dev: true
- /locate-path/5.0.0:
+ /locate-path@5.0.0:
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
engines: {node: '>=8'}
dependencies:
p-locate: 4.1.0
dev: true
- /locate-path/6.0.0:
+ /locate-path@6.0.0:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
dependencies:
p-locate: 5.0.0
dev: true
- /locate-path/7.1.1:
- resolution: {integrity: sha512-vJXaRMJgRVD3+cUZs3Mncj2mxpt5mP0EmNOsxRSZRMlbqjvxzDEOIUWXGmavo0ZC9+tNZCBLQ66reA11nbpHZg==}
- engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
- dependencies:
- p-locate: 6.0.0
- dev: true
-
- /lodash-es/4.17.21:
+ /lodash-es@4.17.21:
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
dev: false
- /lodash.debounce/4.0.8:
- resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
+ /lodash.castarray@4.4.0:
+ resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==}
dev: true
- /lodash.escape/4.0.1:
- resolution: {integrity: sha512-nXEOnb/jK9g0DYMr1/Xvq6l5xMD7GDG55+GSYIYmS0G4tBk/hURD4JR9WCavs04t33WmJx9kCyp9vJ+mr4BOUw==}
+ /lodash.debounce@4.0.8:
+ resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
dev: true
- /lodash.flattendeep/4.4.0:
+ /lodash.flattendeep@4.4.0:
resolution: {integrity: sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==}
dev: true
- /lodash.isequal/4.5.0:
- resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==}
+ /lodash.isplainobject@4.0.6:
+ resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
dev: true
- /lodash.memoize/4.1.2:
+ /lodash.memoize@4.1.2:
resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==}
dev: true
- /lodash.merge/4.6.2:
+ /lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
dev: true
- /lodash.sortby/4.7.0:
+ /lodash.sortby@4.7.0:
resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==}
dev: true
- /lodash.truncate/4.4.2:
+ /lodash.truncate@4.4.2:
resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==}
dev: true
- /lodash.uniq/4.5.0:
+ /lodash.uniq@4.5.0:
resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==}
dev: true
- /lodash/4.17.21:
+ /lodash@4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
- /log-symbols/4.1.0:
+ /log-symbols@4.1.0:
resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==}
engines: {node: '>=10'}
dependencies:
@@ -16468,162 +13712,145 @@ packages:
is-unicode-supported: 0.1.0
dev: true
- /loose-envify/1.4.0:
+ /loose-envify@1.4.0:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
- hasBin: true
dependencies:
js-tokens: 4.0.0
- /loud-rejection/1.6.0:
- resolution: {integrity: sha512-RPNliZOFkqFumDhvYqOaNY4Uz9oJM2K9tC6JWsJJsNdhuONW4LQHRBpb0qf4pJApVffI5N39SwzWZJuEhfd7eQ==}
- engines: {node: '>=0.10.0'}
- dependencies:
- currently-unhandled: 0.4.1
- signal-exit: 3.0.7
- dev: true
- optional: true
-
- /loupe/2.3.4:
+ /loupe@2.3.4:
resolution: {integrity: sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==}
dependencies:
get-func-name: 2.0.0
+ dev: true
- /lower-case/1.1.4:
+ /lower-case@1.1.4:
resolution: {integrity: sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA==}
dev: true
- /lower-case/2.0.2:
+ /lower-case@2.0.2:
resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==}
dependencies:
- tslib: 2.4.1
+ tslib: 2.6.2
dev: true
- /lowercase-keys/1.0.1:
+ /lowercase-keys@1.0.1:
resolution: {integrity: sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==}
engines: {node: '>=0.10.0'}
dev: true
- /lowercase-keys/2.0.0:
+ /lowercase-keys@2.0.0:
resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==}
engines: {node: '>=8'}
dev: true
- /lru-cache/4.1.5:
+ /lowercase-keys@3.0.0:
+ resolution: {integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==}
+ engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+ dev: true
+
+ /lru-cache@10.1.0:
+ resolution: {integrity: sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==}
+ engines: {node: 14 || >=16.14}
+
+ /lru-cache@4.1.5:
resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==}
dependencies:
pseudomap: 1.0.2
yallist: 2.1.2
dev: true
- /lru-cache/5.1.1:
+ /lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
dependencies:
yallist: 3.1.1
dev: true
- /lru-cache/6.0.0:
+ /lru-cache@6.0.0:
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
engines: {node: '>=10'}
dependencies:
yallist: 4.0.0
- dev: true
- /lunr/2.3.9:
+ /lunr@2.3.9:
resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==}
dev: true
- /lz-string/1.4.4:
- resolution: {integrity: sha512-0ckx7ZHRPqb0oUm8zNr+90mtf9DQB60H1wMCjBtfi62Kl3a7JbHob6gA2bC+xRvZoOL+1hzUK8jeuEIQE8svEQ==}
- hasBin: true
- dev: true
-
- /magic-string/0.25.9:
+ /magic-string@0.25.9:
resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==}
dependencies:
sourcemap-codec: 1.4.8
dev: true
- /make-dir/2.1.0:
+ /make-dir@2.1.0:
resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==}
engines: {node: '>=6'}
dependencies:
pify: 4.0.1
- semver: 5.7.1
+ semver: 5.7.2
dev: true
- /make-dir/3.1.0:
+ /make-dir@3.1.0:
resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==}
engines: {node: '>=8'}
dependencies:
- semver: 6.3.0
+ semver: 6.3.1
dev: true
- /makeerror/1.0.12:
- resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==}
+ /make-dir@4.0.0:
+ resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
+ engines: {node: '>=10'}
dependencies:
- tmpl: 1.0.5
+ semver: 7.5.4
+ dev: true
+
+ /make-error@1.3.6:
+ resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
dev: true
- /map-age-cleaner/0.1.3:
+ /map-age-cleaner@0.1.3:
resolution: {integrity: sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==}
engines: {node: '>=6'}
dependencies:
p-defer: 1.0.0
dev: true
- /map-cache/0.2.2:
+ /map-cache@0.2.2:
resolution: {integrity: sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==}
engines: {node: '>=0.10.0'}
dev: true
- /map-obj/1.0.1:
- resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==}
- engines: {node: '>=0.10.0'}
- dev: true
- optional: true
-
- /map-or-similar/1.5.0:
- resolution: {integrity: sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==}
- dev: true
-
- /map-visit/1.0.0:
+ /map-visit@1.0.0:
resolution: {integrity: sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==}
engines: {node: '>=0.10.0'}
dependencies:
object-visit: 1.0.1
dev: true
- /markdown-escapes/1.0.4:
- resolution: {integrity: sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg==}
- dev: true
-
- /marked/2.0.7:
- resolution: {integrity: sha512-BJXxkuIfJchcXOJWTT2DOL+yFWifFv2yGYOUzvXg8Qz610QKw+sHCvTMYwA+qWGhlA2uivBezChZ/pBy1tWdkQ==}
- engines: {node: '>= 8.16.2'}
+ /marked@4.3.0:
+ resolution: {integrity: sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==}
+ engines: {node: '>= 12'}
hasBin: true
dev: true
- /marked/4.1.1:
- resolution: {integrity: sha512-0cNMnTcUJPxbA6uWmCmjWz4NJRe/0Xfk2NhXCUHjew9qJzFN20krFnsUe7QynwqOwa5m1fZ4UDg0ycKFVC0ccw==}
- engines: {node: '>= 12'}
- hasBin: true
+ /marky@1.2.5:
+ resolution: {integrity: sha512-q9JtQJKjpsVxCRVgQ+WapguSbKC3SQ5HEzFGPAJMStgh3QjCawp00UKv3MTTAArTmGmmPUvllHZoNbZ3gs0I+Q==}
dev: true
- /matcher/5.0.0:
+ /matcher@5.0.0:
resolution: {integrity: sha512-s2EMBOWtXFc8dgqvoAzKJXxNHibcdJMV0gwqKUaw9E2JBJuGUK7DrNKrA6g/i+v72TT16+6sVm5mS3thaMLQUw==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
dependencies:
escape-string-regexp: 5.0.0
dev: true
- /md5-hex/3.0.1:
+ /md5-hex@3.0.1:
resolution: {integrity: sha512-BUiRtTtV39LIJwinWBjqVsU9xhdnz7/i889V859IBFpuqGAj6LuOvHv5XLbgZ2R7ptJoJaEcxkv88/h25T7Ciw==}
engines: {node: '>=8'}
dependencies:
blueimp-md5: 2.19.0
dev: true
- /md5.js/1.3.5:
+ /md5.js@1.3.5:
resolution: {integrity: sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==}
dependencies:
hash-base: 3.1.0
@@ -16631,128 +13858,79 @@ packages:
safe-buffer: 5.2.1
dev: true
- /mdast-squeeze-paragraphs/4.0.0:
- resolution: {integrity: sha512-zxdPn69hkQ1rm4J+2Cs2j6wDEv7O17TfXTJ33tl/+JPIoEmtV9t2ZzBM5LPHE8QlHsmVD8t3vPKCyY3oH+H8MQ==}
- dependencies:
- unist-util-remove: 2.1.0
- dev: true
-
- /mdast-util-definitions/4.0.0:
- resolution: {integrity: sha512-k8AJ6aNnUkB7IE+5azR9h81O5EQ/cTDXtWdMq9Kk5KcEW/8ritU5CeLg/9HhOC++nALHBlaogJ5jz0Ybk3kPMQ==}
- dependencies:
- unist-util-visit: 2.0.3
- dev: true
-
- /mdast-util-to-hast/10.0.1:
- resolution: {integrity: sha512-BW3LM9SEMnjf4HXXVApZMt8gLQWVNXc3jryK0nJu/rOXPOnlkUjmdkDlmxMirpbU9ILncGFIwLH/ubnWBbcdgA==}
- dependencies:
- '@types/mdast': 3.0.10
- '@types/unist': 2.0.6
- mdast-util-definitions: 4.0.0
- mdurl: 1.0.1
- unist-builder: 2.0.3
- unist-util-generated: 1.1.6
- unist-util-position: 3.1.0
- unist-util-visit: 2.0.3
- dev: true
-
- /mdast-util-to-string/1.1.0:
- resolution: {integrity: sha512-jVU0Nr2B9X3MU4tSK7JP1CMkSvOj7X5l/GboG1tKRw52lLF1x2Ju92Ms9tNetCcbfX3hzlM73zYo2NKkWSfF/A==}
- dev: true
-
- /mdn-data/2.0.14:
+ /mdn-data@2.0.14:
resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==}
dev: true
- /mdn-data/2.0.4:
+ /mdn-data@2.0.4:
resolution: {integrity: sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==}
dev: true
- /mdurl/1.0.1:
- resolution: {integrity: sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==}
- dev: true
-
- /media-typer/0.3.0:
+ /media-typer@0.3.0:
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
engines: {node: '>= 0.6'}
dev: true
- /mem/9.0.2:
- resolution: {integrity: sha512-F2t4YIv9XQUBHt6AOJ0y7lSmP1+cY7Fm1DRh9GClTGzKST7UWLMx6ly9WZdLH/G/ppM5RL4MlQfRT71ri9t19A==}
- engines: {node: '>=12.20'}
+ /mem@5.1.1:
+ resolution: {integrity: sha512-qvwipnozMohxLXG1pOqoLiZKNkC4r4qqRucSoDwXowsNGDSULiqFTRUF05vcZWnwJSG22qTsynQhxbaMtnX9gw==}
+ engines: {node: '>=8'}
dependencies:
map-age-cleaner: 0.1.3
- mimic-fn: 4.0.0
+ mimic-fn: 2.1.0
+ p-is-promise: 2.1.0
dev: true
- /memfs/3.4.7:
+ /memfs@3.4.7:
resolution: {integrity: sha512-ygaiUSNalBX85388uskeCyhSAoOSgzBbtVCr9jA2RROssFL9Q19/ZXFqS+2Th2sr1ewNIWgFdLzLC3Yl1Zv+lw==}
engines: {node: '>= 4.0.0'}
dependencies:
fs-monkey: 1.0.3
dev: true
- /memoizerific/1.11.3:
- resolution: {integrity: sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog==}
+ /memoize@10.0.0:
+ resolution: {integrity: sha512-H6cBLgsi6vMWOcCpvVCdFFnl3kerEXbrYh9q+lY6VXvQSmM6CkmV08VOwT+WE2tzIEqRPFfAq3fm4v/UIW6mSA==}
+ engines: {node: '>=18'}
dependencies:
- map-or-similar: 1.5.0
+ mimic-function: 5.0.0
dev: true
- /memory-fs/0.4.1:
+ /memory-fs@0.4.1:
resolution: {integrity: sha512-cda4JKCxReDXFXRqOHPQscuIYg1PvxbE2S2GP45rnwfEK+vZaXC8C1OFvdHIbgw0DLzowXGVoxLaAmlgRy14GQ==}
dependencies:
errno: 0.1.8
- readable-stream: 2.3.7
+ readable-stream: 2.3.8
dev: true
- /memory-fs/0.5.0:
+ /memory-fs@0.5.0:
resolution: {integrity: sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==}
engines: {node: '>=4.3.0 <5.0.0 || >=5.10'}
dependencies:
errno: 0.1.8
- readable-stream: 2.3.7
- dev: true
-
- /meow/3.7.0:
- resolution: {integrity: sha512-TNdwZs0skRlpPpCUK25StC4VH+tP5GgeY1HQOOGP+lQ2xtdkN2VtT/5tiX9k3IWpkBPV9b3LsAWXn4GGi/PrSA==}
- engines: {node: '>=0.10.0'}
- dependencies:
- camelcase-keys: 2.1.0
- decamelize: 1.2.0
- loud-rejection: 1.6.0
- map-obj: 1.0.1
- minimist: 1.2.7
- normalize-package-data: 2.5.0
- object-assign: 4.1.1
- read-pkg-up: 1.0.1
- redent: 1.0.0
- trim-newlines: 1.0.0
+ readable-stream: 2.3.8
dev: true
- optional: true
- /merge-descriptors/1.0.1:
+ /merge-descriptors@1.0.1:
resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==}
dev: true
- /merge-stream/2.0.0:
+ /merge-stream@2.0.0:
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
dev: true
- /merge2/1.4.1:
+ /merge2@1.4.1:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
- dev: true
- /methods/1.1.2:
+ /methods@1.1.2:
resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==}
engines: {node: '>= 0.6'}
dev: true
- /microevent.ts/0.1.1:
+ /microevent.ts@0.1.1:
resolution: {integrity: sha512-jo1OfR4TaEwd5HOrt5+tAZ9mqT4jmpNAusXtyfNzqVm9uiSYFZlKM1wYL4oU7azZW/PxQW53wM0S6OR1JHNa2g==}
dev: true
- /micromatch/3.1.10:
+ /micromatch@3.1.10:
resolution: {integrity: sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==}
engines: {node: '>=0.10.0'}
dependencies:
@@ -16773,15 +13951,14 @@ packages:
- supports-color
dev: true
- /micromatch/4.0.5:
+ /micromatch@4.0.5:
resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==}
engines: {node: '>=8.6'}
dependencies:
braces: 3.0.2
picomatch: 2.3.1
- dev: true
- /miller-rabin/4.0.1:
+ /miller-rabin@4.0.1:
resolution: {integrity: sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==}
hasBin: true
dependencies:
@@ -16789,50 +13966,54 @@ packages:
brorand: 1.1.0
dev: true
- /mime-db/1.52.0:
+ /mime-db@1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
+ dev: true
- /mime-types/2.1.35:
+ /mime-types@2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
dependencies:
mime-db: 1.52.0
+ dev: true
- /mime/1.6.0:
+ /mime@1.6.0:
resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==}
engines: {node: '>=4'}
- hasBin: true
dev: true
- /mime/2.6.0:
- resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==}
- engines: {node: '>=4.0.0'}
- hasBin: true
- dev: true
-
- /mimic-fn/2.1.0:
+ /mimic-fn@2.1.0:
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
engines: {node: '>=6'}
dev: true
- /mimic-fn/4.0.0:
+ /mimic-fn@4.0.0:
resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==}
engines: {node: '>=12'}
dev: true
- /mimic-response/1.0.1:
+ /mimic-function@5.0.0:
+ resolution: {integrity: sha512-RBfQ+9X9DpXdEoK7Bu+KeEU6vFhumEIiXKWECPzRBmDserEq4uR2b/VCm0LwpMSosoq2k+Zuxj/GzOr0Fn6h/g==}
+ engines: {node: '>=18'}
+ dev: true
+
+ /mimic-response@1.0.1:
resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==}
engines: {node: '>=4'}
dev: true
- /min-document/2.19.0:
- resolution: {integrity: sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==}
- dependencies:
- dom-walk: 0.1.2
+ /mimic-response@3.1.0:
+ resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
+ engines: {node: '>=10'}
+ requiresBuild: true
+
+ /mimic-response@4.0.0:
+ resolution: {integrity: sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==}
+ engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
dev: true
- /mini-css-extract-plugin/1.6.2_webpack@4.46.0:
+ /mini-css-extract-plugin@1.6.2(webpack@4.46.0):
resolution: {integrity: sha512-WhDvO3SjGm40oV5y26GjMJYjd2UMqrLAGKy5YS2/3QKJy2F7jgynuHTir/tgUUOiNQu5saXHdc8reo7YuhhT4Q==}
engines: {node: '>= 10.13.0'}
peerDependencies:
@@ -16844,104 +14025,115 @@ packages:
webpack-sources: 1.4.3
dev: true
- /mini-svg-data-uri/1.4.4:
+ /mini-svg-data-uri@1.4.4:
resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==}
- hasBin: true
dev: true
- /minimalistic-assert/1.0.1:
+ /minimalistic-assert@1.0.1:
resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==}
dev: true
- /minimalistic-crypto-utils/1.0.1:
+ /minimalistic-crypto-utils@1.0.1:
resolution: {integrity: sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==}
dev: true
- /minimatch/3.0.4:
+ /minimatch@3.0.4:
resolution: {integrity: sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==}
dependencies:
brace-expansion: 1.1.11
dev: true
- /minimatch/3.0.5:
- resolution: {integrity: sha512-tUpxzX0VAzJHjLu0xUfFv1gwVp9ba3IOuRAVH2EGuRW8a5emA2FlACLqiT/lDVtS1W+TGNwqz3sWaNyLgDJWuw==}
- dependencies:
- brace-expansion: 1.1.11
- dev: true
-
- /minimatch/3.1.2:
+ /minimatch@3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
dependencies:
brace-expansion: 1.1.11
- /minimatch/4.2.1:
+ /minimatch@4.2.1:
resolution: {integrity: sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g==}
engines: {node: '>=10'}
dependencies:
brace-expansion: 1.1.11
dev: true
- /minimatch/5.1.0:
- resolution: {integrity: sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==}
+ /minimatch@5.1.6:
+ resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==}
engines: {node: '>=10'}
dependencies:
brace-expansion: 2.0.1
dev: true
- /minimist/1.2.7:
+ /minimatch@9.0.3:
+ resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==}
+ engines: {node: '>=16 || 14 >=14.17'}
+ dependencies:
+ brace-expansion: 2.0.1
+
+ /minimist@1.2.7:
resolution: {integrity: sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==}
dev: true
- /minipass-collect/1.0.2:
+ /minimist@1.2.8:
+ resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
+
+ /minipass-collect@1.0.2:
resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==}
engines: {node: '>= 8'}
dependencies:
- minipass: 3.3.4
+ minipass: 3.3.6
dev: true
- /minipass-flush/1.0.5:
+ /minipass-flush@1.0.5:
resolution: {integrity: sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==}
engines: {node: '>= 8'}
dependencies:
- minipass: 3.3.4
+ minipass: 3.3.6
dev: true
- /minipass-pipeline/1.2.4:
+ /minipass-pipeline@1.2.4:
resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==}
engines: {node: '>=8'}
dependencies:
- minipass: 3.3.4
+ minipass: 3.3.6
dev: true
- /minipass/2.9.0:
+ /minipass@2.9.0:
resolution: {integrity: sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==}
dependencies:
safe-buffer: 5.2.1
yallist: 3.1.1
dev: true
- /minipass/3.3.4:
- resolution: {integrity: sha512-I9WPbWHCGu8W+6k1ZiGpPu0GkoKBeorkfKNuAFBNS1HNFJvke82sxvI5bzcCNpWPorkOO5QQ+zomzzwRxejXiw==}
+ /minipass@3.3.6:
+ resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==}
engines: {node: '>=8'}
dependencies:
yallist: 4.0.0
dev: true
- /minizlib/1.3.3:
+ /minipass@5.0.0:
+ resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /minipass@7.0.4:
+ resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==}
+ engines: {node: '>=16 || 14 >=14.17'}
+
+ /minizlib@1.3.3:
resolution: {integrity: sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==}
dependencies:
minipass: 2.9.0
dev: true
- /minizlib/2.1.2:
+ /minizlib@2.1.2:
resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==}
engines: {node: '>= 8'}
dependencies:
- minipass: 3.3.4
+ minipass: 3.3.6
yallist: 4.0.0
dev: true
- /mississippi/3.0.0:
+ /mississippi@3.0.0:
resolution: {integrity: sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==}
engines: {node: '>=4.0.0'}
dependencies:
@@ -16957,7 +14149,7 @@ packages:
through2: 2.0.5
dev: true
- /mixin-deep/1.3.2:
+ /mixin-deep@1.3.2:
resolution: {integrity: sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==}
engines: {node: '>=0.10.0'}
dependencies:
@@ -16965,20 +14157,57 @@ packages:
is-extendable: 1.0.1
dev: true
- /mkdirp/0.5.6:
+ /mkdirp-classic@0.5.3:
+ resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /mkdirp@0.5.6:
resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
hasBin: true
dependencies:
- minimist: 1.2.7
+ minimist: 1.2.8
dev: true
- /mkdirp/1.0.4:
+ /mkdirp@1.0.4:
resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==}
engines: {node: '>=10'}
hasBin: true
dev: true
- /mocha/9.2.2:
+ /mocha@9.2.0:
+ resolution: {integrity: sha512-kNn7E8g2SzVcq0a77dkphPsDSN7P+iYkqE0ZsGCYWRsoiKjOt+NvXfaagik8vuDa6W5Zw3qxe8Jfpt5qKf+6/Q==}
+ engines: {node: '>= 12.0.0'}
+ hasBin: true
+ dependencies:
+ '@ungap/promise-all-settled': 1.1.2
+ ansi-colors: 4.1.1
+ browser-stdout: 1.3.1
+ chokidar: 3.5.3
+ debug: 4.3.3(supports-color@8.1.1)
+ diff: 5.0.0
+ escape-string-regexp: 4.0.0
+ find-up: 5.0.0
+ glob: 7.2.0
+ growl: 1.10.5
+ he: 1.2.0
+ js-yaml: 4.1.0
+ log-symbols: 4.1.0
+ minimatch: 3.0.4
+ ms: 2.1.3
+ nanoid: 3.2.0
+ serialize-javascript: 6.0.0
+ strip-json-comments: 3.1.1
+ supports-color: 8.1.1
+ which: 2.0.2
+ workerpool: 6.2.0
+ yargs: 16.2.0
+ yargs-parser: 20.2.4
+ yargs-unparser: 2.0.0
+ dev: true
+
+ /mocha@9.2.2:
resolution: {integrity: sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g==}
engines: {node: '>= 12.0.0'}
hasBin: true
@@ -16987,7 +14216,7 @@ packages:
ansi-colors: 4.1.1
browser-stdout: 1.3.1
chokidar: 3.5.3
- debug: 4.3.3_supports-color@8.1.1
+ debug: 4.3.3(supports-color@8.1.1)
diff: 5.0.0
escape-string-regexp: 4.0.0
find-up: 5.0.0
@@ -17009,11 +14238,13 @@ packages:
yargs-unparser: 2.0.0
dev: true
- /moo/0.5.2:
- resolution: {integrity: sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==}
+ /moment@2.30.1:
+ resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==}
+ requiresBuild: true
dev: true
+ optional: true
- /move-concurrently/1.0.1:
+ /move-concurrently@1.0.1:
resolution: {integrity: sha512-hdrFxZOycD/g6A6SoI2bB5NA/5NEqD0569+S47WZhPvm46sD50ZHdYaFmnua5lndde9rCHGjmfK7Z8BuCt/PcQ==}
dependencies:
aproba: 1.2.0
@@ -17024,68 +14255,98 @@ packages:
run-queue: 1.0.3
dev: true
- /mri/1.2.0:
+ /mri@1.2.0:
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
engines: {node: '>=4'}
dev: true
- /mrmime/1.0.1:
+ /mrmime@1.0.1:
resolution: {integrity: sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==}
engines: {node: '>=10'}
dev: true
- /ms/2.0.0:
+ /ms@2.0.0:
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
dev: true
- /ms/2.1.1:
- resolution: {integrity: sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==}
- dev: true
-
- /ms/2.1.2:
+ /ms@2.1.2:
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
- dev: true
- /ms/2.1.3:
+ /ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
dev: true
- /multicast-dns/7.2.5:
+ /multicast-dns@7.2.5:
resolution: {integrity: sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==}
- hasBin: true
dependencies:
dns-packet: 5.4.0
thunky: 1.1.0
dev: true
- /mustache/4.2.0:
+ /multimatch@6.0.0:
+ resolution: {integrity: sha512-I7tSVxHGPlmPN/enE3mS1aOSo6bWBfls+3HmuEeCUBCE7gWnm3cBXCBkpurzFjVRwC6Kld8lLaZ1Iv5vOcjvcQ==}
+ engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+ dependencies:
+ '@types/minimatch': 3.0.5
+ array-differ: 4.0.0
+ array-union: 3.0.1
+ minimatch: 3.1.2
+ dev: true
+
+ /mustache@4.2.0:
resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==}
- hasBin: true
dev: true
- /nan/2.17.0:
- resolution: {integrity: sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==}
+ /mv@2.1.1:
+ resolution: {integrity: sha512-at/ZndSy3xEGJ8i0ygALh8ru9qy7gWW1cmkaqBN29JmMlIvM//MEO9y1sk/avxuwnPcfhkejkLsuPxH81BrkSg==}
+ engines: {node: '>=0.8.0'}
requiresBuild: true
+ dependencies:
+ mkdirp: 0.5.6
+ ncp: 2.0.0
+ rimraf: 2.4.5
dev: true
optional: true
- /nanoclone/0.2.1:
+ /mz@2.7.0:
+ resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
+ dependencies:
+ any-promise: 1.3.0
+ object-assign: 4.1.1
+ thenify-all: 1.6.0
+
+ /nan@2.18.0:
+ resolution: {integrity: sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==}
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /nanoclone@0.2.1:
resolution: {integrity: sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA==}
dev: false
- /nanoid/3.3.1:
- resolution: {integrity: sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==}
+ /nanoid@3.2.0:
+ resolution: {integrity: sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
dev: true
- /nanoid/3.3.4:
- resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==}
+ /nanoid@3.3.1:
+ resolution: {integrity: sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ dev: true
+
+ /nanoid@3.3.6:
+ resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+
+ /nanoid@3.3.7:
+ resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
dev: true
- /nanomatch/1.2.13:
+ /nanomatch@1.2.13:
resolution: {integrity: sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==}
engines: {node: '>=0.10.0'}
dependencies:
@@ -17104,66 +14365,66 @@ packages:
- supports-color
dev: true
- /native-url/0.3.4:
+ /napi-build-utils@1.0.2:
+ resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==}
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /native-url@0.3.4:
resolution: {integrity: sha512-6iM8R99ze45ivyH8vybJ7X0yekIcPf5GgLV5K0ENCbmRcaRIDoj37BC8iLEmaaBfqqb8enuZ5p0uhY+lVAbAcA==}
dependencies:
querystring: 0.2.1
dev: true
- /natural-compare/1.4.0:
+ /natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
dev: true
- /nearley/2.20.1:
- resolution: {integrity: sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==}
+ /ncp@2.0.0:
+ resolution: {integrity: sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==}
hasBin: true
- dependencies:
- commander: 2.20.3
- moo: 0.5.2
- railroad-diagrams: 1.0.0
- randexp: 0.4.6
+ requiresBuild: true
dev: true
+ optional: true
- /negotiator/0.6.3:
+ /negotiator@0.6.3:
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
engines: {node: '>= 0.6'}
dev: true
- /neo-async/2.6.2:
+ /neo-async@2.6.2:
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
dev: true
- /nested-error-stacks/2.1.1:
- resolution: {integrity: sha512-9iN1ka/9zmX1ZvLV9ewJYEk9h7RyRRtqdK0woXcqohu8EWIerfPUjYJPg0ULy0UqP7cslmdGc8xKDJcojlKiaw==}
- dev: true
-
- /nice-try/1.0.5:
- resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==}
- dev: true
-
- /no-case/2.3.2:
+ /no-case@2.3.2:
resolution: {integrity: sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==}
dependencies:
lower-case: 1.1.4
dev: true
- /no-case/3.0.4:
+ /no-case@3.0.4:
resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==}
dependencies:
lower-case: 2.0.2
- tslib: 2.4.1
+ tslib: 2.6.2
dev: true
- /node-addon-api/3.2.1:
- resolution: {integrity: sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==}
- dev: true
+ /node-abi@3.52.0:
+ resolution: {integrity: sha512-JJ98b02z16ILv7859irtXn4oUaFWADtvkzy2c0IAatNVX2Mc9Yoh8z6hZInn3QwvMEYhHuQloYi+TTQy67SIdQ==}
+ engines: {node: '>=10'}
+ requiresBuild: true
+ dependencies:
+ semver: 7.5.4
+ dev: false
+ optional: true
- /node-domexception/1.0.0:
+ /node-domexception@1.0.0:
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
engines: {node: '>=10.5.0'}
- dev: false
+ dev: true
- /node-fetch/2.6.7:
+ /node-fetch@2.6.7:
resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==}
engines: {node: 4.x || >=6.0.0}
peerDependencies:
@@ -17173,31 +14434,40 @@ packages:
optional: true
dependencies:
whatwg-url: 5.0.0
+ dev: true
+
+ /node-fetch@2.7.0:
+ resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
+ engines: {node: 4.x || >=6.0.0}
+ peerDependencies:
+ encoding: ^0.1.0
+ peerDependenciesMeta:
+ encoding:
+ optional: true
+ dependencies:
+ whatwg-url: 5.0.0
+ dev: true
- /node-fetch/3.2.10:
- resolution: {integrity: sha512-MhuzNwdURnZ1Cp4XTazr69K0BTizsBroX7Zx3UgDSVcZYKF/6p0CBe4EUb/hLqmzVhl0UpYfgRljQ4yxE+iCxA==}
+ /node-fetch@3.3.1:
+ resolution: {integrity: sha512-cRVc/kyto/7E5shrWca1Wsea4y6tL9iYJE5FBCius3JQfb/4P4I295PfhgbJQBLTx6lATE4z+wK0rPM4VS2uow==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
dependencies:
- data-uri-to-buffer: 4.0.0
+ data-uri-to-buffer: 4.0.1
fetch-blob: 3.2.0
formdata-polyfill: 4.0.10
- dev: false
+ dev: true
- /node-forge/1.3.1:
+ /node-forge@1.3.1:
resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==}
engines: {node: '>= 6.13.0'}
dev: true
- /node-gyp-build/4.5.0:
- resolution: {integrity: sha512-2iGbaQBV+ITgCz76ZEjmhUKAKVf7xfY1sRl4UiKQspfZMH2h06SyhNsnSVy50cwkFQDGLyif6m/6uFXHkOZ6rg==}
+ /node-gyp-build@4.7.1:
+ resolution: {integrity: sha512-wTSrZ+8lsRRa3I3H8Xr65dLWSgCvY2l4AOnaeKdPA9TB/WYMPaTcrzf3rXvFoVvjKNVnu0CcWSx54qq9GKRUYg==}
hasBin: true
dev: true
- /node-int64/0.4.0:
- resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==}
- dev: true
-
- /node-libs-browser/2.2.1:
+ /node-libs-browser@2.2.1:
resolution: {integrity: sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==}
dependencies:
assert: 1.5.0
@@ -17214,110 +14484,119 @@ packages:
process: 0.11.10
punycode: 1.4.1
querystring-es3: 0.2.1
- readable-stream: 2.3.7
+ readable-stream: 2.3.8
stream-browserify: 2.0.2
stream-http: 2.8.3
string_decoder: 1.3.0
timers-browserify: 2.0.12
tty-browserify: 0.0.0
- url: 0.11.0
+ url: 0.11.1
util: 0.11.1
vm-browserify: 1.1.2
dev: true
- /node-notifier/8.0.2:
- resolution: {integrity: sha512-oJP/9NAdd9+x2Q+rfphB2RJCHjod70RcRLjosiPMMu5gjIfwVnOUGq2nbTjTUbmy0DJ/tFIVT30+Qe3nzl4TJg==}
- requiresBuild: true
+ /node-notifier@10.0.1:
+ resolution: {integrity: sha512-YX7TSyDukOZ0g+gmzjB6abKu+hTGvO8+8+gIFDsRCU2t8fLV/P2unmt+LGFaIa4y64aX98Qksa97rgz4vMNeLQ==}
dependencies:
growly: 1.3.0
is-wsl: 2.2.0
- semver: 7.3.8
+ semver: 7.5.4
shellwords: 0.1.1
uuid: 8.3.2
which: 2.0.2
dev: true
- optional: true
- /node-preload/0.2.1:
+ /node-preload@0.2.1:
resolution: {integrity: sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==}
engines: {node: '>=8'}
dependencies:
process-on-spawn: 1.0.0
dev: true
- /node-releases/2.0.6:
- resolution: {integrity: sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==}
+ /node-releases@2.0.10:
+ resolution: {integrity: sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==}
+ dev: true
+
+ /node-releases@2.0.13:
+ resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==}
dev: true
- /nofilter/3.1.0:
+ /node-releases@2.0.14:
+ resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==}
+
+ /nofilter@3.1.0:
resolution: {integrity: sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==}
engines: {node: '>=12.19'}
dev: true
- /nomnom/1.8.1:
+ /nomnom@1.8.1:
resolution: {integrity: sha1-IVH3Ikcrp55Qp2/BJbuMjy5Nwqc=}
- deprecated: Package no longer supported. Contact support@npmjs.com for more info.
dependencies:
chalk: 0.4.0
underscore: 1.6.0
dev: true
- /normalize-package-data/2.5.0:
- resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==}
+ /nopt@5.0.0:
+ resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==}
+ engines: {node: '>=6'}
+ hasBin: true
dependencies:
- hosted-git-info: 2.8.9
- resolve: 1.22.1
- semver: 5.7.1
- validate-npm-package-license: 3.0.4
+ abbrev: 1.1.1
dev: true
- /normalize-path/2.1.1:
+ /normalize-path@2.1.1:
resolution: {integrity: sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==}
engines: {node: '>=0.10.0'}
+ requiresBuild: true
dependencies:
remove-trailing-separator: 1.1.0
dev: true
+ optional: true
- /normalize-path/3.0.0:
+ /normalize-path@3.0.0:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
- dev: true
- /normalize-range/0.1.2:
+ /normalize-range@0.1.2:
resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
engines: {node: '>=0.10.0'}
dev: true
- /normalize-url/3.3.0:
+ /normalize-url@3.3.0:
resolution: {integrity: sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==}
engines: {node: '>=6'}
dev: true
- /normalize-url/4.5.1:
+ /normalize-url@4.5.1:
resolution: {integrity: sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==}
engines: {node: '>=8'}
dev: true
- /normalize-url/6.1.0:
+ /normalize-url@6.1.0:
resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==}
engines: {node: '>=10'}
dev: true
- /npm-run-path/2.0.2:
- resolution: {integrity: sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==}
- engines: {node: '>=4'}
- dependencies:
- path-key: 2.0.1
+ /normalize-url@8.0.0:
+ resolution: {integrity: sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw==}
+ engines: {node: '>=14.16'}
dev: true
- /npm-run-path/4.0.1:
+ /npm-run-path@4.0.1:
resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
engines: {node: '>=8'}
dependencies:
path-key: 3.1.1
dev: true
- /npmlog/5.0.1:
+ /npm-run-path@5.1.0:
+ resolution: {integrity: sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==}
+ engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+ dependencies:
+ path-key: 4.0.0
+ dev: true
+
+ /npmlog@5.0.1:
resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==}
dependencies:
are-we-there-yet: 2.0.0
@@ -17326,82 +14605,25 @@ packages:
set-blocking: 2.0.0
dev: true
- /nth-check/1.0.2:
+ /nth-check@1.0.2:
resolution: {integrity: sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==}
dependencies:
boolbase: 1.0.0
dev: true
- /nth-check/2.1.1:
+ /nth-check@2.1.1:
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
dependencies:
boolbase: 1.0.0
dev: true
- /num2fraction/1.2.2:
- resolution: {integrity: sha512-Y1wZESM7VUThYY+4W+X4ySH2maqcA+p7UR+w8VWNWVAd6lwuXXWz/w/Cz43J/dI2I+PS6wD5N+bJUF+gjWvIqg==}
- dev: true
-
- /nwsapi/2.2.2:
+ /nwsapi@2.2.2:
resolution: {integrity: sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw==}
dev: true
- /nx/15.0.1:
- resolution: {integrity: sha512-4pGy6f0SMQpg5kr38I95OkzufgkeMf4n/ui9o2Xk65mFdqXcCzRgRXbKdDhABhdZmhbzV33M+BUPJmMrTw3XDg==}
- hasBin: true
- requiresBuild: true
- peerDependencies:
- '@swc-node/register': ^1.4.2
- '@swc/core': ^1.2.173
- peerDependenciesMeta:
- '@swc-node/register':
- optional: true
- '@swc/core':
- optional: true
- dependencies:
- '@nrwl/cli': 15.0.1
- '@nrwl/tao': 15.0.1
- '@parcel/watcher': 2.0.4
- '@yarnpkg/lockfile': 1.1.0
- '@yarnpkg/parsers': 3.0.0-rc.26
- '@zkochan/js-yaml': 0.0.6
- axios: 1.1.3
- chalk: 4.1.0
- chokidar: 3.5.3
- cli-cursor: 3.1.0
- cli-spinners: 2.6.1
- cliui: 7.0.4
- dotenv: 10.0.0
- enquirer: 2.3.6
- fast-glob: 3.2.7
- figures: 3.2.0
- flat: 5.0.2
- fs-extra: 10.1.0
- glob: 7.1.4
- ignore: 5.2.0
- js-yaml: 4.1.0
- jsonc-parser: 3.2.0
- minimatch: 3.0.5
- npm-run-path: 4.0.1
- open: 8.4.0
- semver: 7.3.4
- string-width: 4.2.3
- strong-log-transformer: 2.1.0
- tar-stream: 2.2.0
- tmp: 0.2.1
- tsconfig-paths: 3.14.1
- tslib: 2.4.0
- v8-compile-cache: 2.3.0
- yargs: 17.6.0
- yargs-parser: 21.0.1
- transitivePeerDependencies:
- - debug
- dev: true
-
- /nyc/15.1.0:
+ /nyc@15.1.0:
resolution: {integrity: sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A==}
engines: {node: '>=8.9'}
- hasBin: true
dependencies:
'@istanbuljs/load-nyc-config': 1.1.0
'@istanbuljs/schema': 0.1.3
@@ -17434,16 +14656,15 @@ packages:
- supports-color
dev: true
- /oauth-sign/0.9.0:
+ /oauth-sign@0.9.0:
resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==}
dev: true
- /object-assign/4.1.1:
+ /object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
- dev: true
- /object-copy/0.1.0:
+ /object-copy@0.1.0:
resolution: {integrity: sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==}
engines: {node: '>=0.10.0'}
dependencies:
@@ -17452,129 +14673,154 @@ packages:
kind-of: 3.2.2
dev: true
- /object-inspect/1.12.2:
- resolution: {integrity: sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==}
- dev: true
+ /object-hash@3.0.0:
+ resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
+ engines: {node: '>= 6'}
- /object-is/1.1.5:
- resolution: {integrity: sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==}
- engines: {node: '>= 0.4'}
- dependencies:
- call-bind: 1.0.2
- define-properties: 1.1.4
+ /object-inspect@1.13.1:
+ resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==}
dev: true
- /object-keys/1.1.1:
+ /object-keys@1.1.1:
resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==}
engines: {node: '>= 0.4'}
dev: true
- /object-visit/1.0.1:
+ /object-visit@1.0.1:
resolution: {integrity: sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==}
engines: {node: '>=0.10.0'}
dependencies:
isobject: 3.0.1
dev: true
- /object.assign/4.1.4:
- resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==}
+ /object.assign@4.1.5:
+ resolution: {integrity: sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==}
engines: {node: '>= 0.4'}
dependencies:
- call-bind: 1.0.2
- define-properties: 1.1.4
+ call-bind: 1.0.5
+ define-properties: 1.2.1
has-symbols: 1.0.3
object-keys: 1.1.1
dev: true
- /object.entries/1.1.5:
- resolution: {integrity: sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g==}
+ /object.entries@1.1.7:
+ resolution: {integrity: sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA==}
engines: {node: '>= 0.4'}
dependencies:
- call-bind: 1.0.2
- define-properties: 1.1.4
- es-abstract: 1.20.4
+ call-bind: 1.0.5
+ define-properties: 1.2.1
+ es-abstract: 1.22.3
dev: true
- /object.fromentries/2.0.5:
- resolution: {integrity: sha512-CAyG5mWQRRiBU57Re4FKoTBjXfDoNwdFVH2Y1tS9PqCsfUTymAohOkEMSG3aRNKmv4lV3O7p1et7c187q6bynw==}
+ /object.fromentries@2.0.7:
+ resolution: {integrity: sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==}
engines: {node: '>= 0.4'}
dependencies:
- call-bind: 1.0.2
- define-properties: 1.1.4
- es-abstract: 1.20.4
+ call-bind: 1.0.5
+ define-properties: 1.2.1
+ es-abstract: 1.22.3
dev: true
- /object.getownpropertydescriptors/2.1.4:
+ /object.getownpropertydescriptors@2.1.4:
resolution: {integrity: sha512-sccv3L/pMModT6dJAYF3fzGMVcb38ysQ0tEE6ixv2yXJDtEIPph268OlAdJj5/qZMZDq2g/jqvwppt36uS/uQQ==}
engines: {node: '>= 0.8'}
dependencies:
array.prototype.reduce: 1.0.4
- call-bind: 1.0.2
- define-properties: 1.1.4
- es-abstract: 1.20.4
+ call-bind: 1.0.5
+ define-properties: 1.2.1
+ es-abstract: 1.22.3
dev: true
- /object.hasown/1.1.1:
- resolution: {integrity: sha512-LYLe4tivNQzq4JdaWW6WO3HMZZJWzkkH8fnI6EebWl0VZth2wL2Lovm74ep2/gZzlaTdV62JZHEqHQ2yVn8Q/A==}
+ /object.groupby@1.0.1:
+ resolution: {integrity: sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==}
dependencies:
- define-properties: 1.1.4
- es-abstract: 1.20.4
+ call-bind: 1.0.5
+ define-properties: 1.2.1
+ es-abstract: 1.22.3
+ get-intrinsic: 1.2.2
dev: true
- /object.pick/1.3.0:
+ /object.hasown@1.1.3:
+ resolution: {integrity: sha512-fFI4VcYpRHvSLXxP7yiZOMAd331cPfd2p7PFDVbgUsYOfCT3tICVqXWngbjr4m49OvsBwUBQ6O2uQoJvy3RexA==}
+ dependencies:
+ define-properties: 1.2.1
+ es-abstract: 1.22.3
+ dev: true
+
+ /object.omit@3.0.0:
+ resolution: {integrity: sha512-EO+BCv6LJfu+gBIF3ggLicFebFLN5zqzz/WWJlMFfkMyGth+oBkhxzDl0wx2W4GkLzuQs/FsSkXZb2IMWQqmBQ==}
+ engines: {node: '>=0.10.0'}
+ dependencies:
+ is-extendable: 1.0.1
+ dev: true
+
+ /object.pick@1.3.0:
resolution: {integrity: sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==}
engines: {node: '>=0.10.0'}
dependencies:
isobject: 3.0.1
dev: true
- /object.values/1.1.5:
- resolution: {integrity: sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==}
+ /object.values@1.1.7:
+ resolution: {integrity: sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==}
engines: {node: '>= 0.4'}
dependencies:
- call-bind: 1.0.2
- define-properties: 1.1.4
- es-abstract: 1.20.4
+ call-bind: 1.0.5
+ define-properties: 1.2.1
+ es-abstract: 1.22.3
dev: true
- /obuf/1.1.2:
+ /obuf@1.1.2:
resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==}
dev: true
- /on-finished/2.4.1:
+ /on-exit-leak-free@2.1.2:
+ resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
+ engines: {node: '>=14.0.0'}
+ dev: true
+
+ /on-finished@2.4.1:
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
engines: {node: '>= 0.8'}
dependencies:
ee-first: 1.1.1
dev: true
- /on-headers/1.0.2:
+ /on-headers@1.0.2:
resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==}
engines: {node: '>= 0.8'}
dev: true
- /once/1.4.0:
+ /once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
dependencies:
wrappy: 1.0.2
- /onetime/5.1.2:
+ /onetime@5.1.2:
resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
engines: {node: '>=6'}
dependencies:
mimic-fn: 2.1.0
dev: true
- /open/7.4.2:
- resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==}
- engines: {node: '>=8'}
+ /onetime@6.0.0:
+ resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==}
+ engines: {node: '>=12'}
dependencies:
+ mimic-fn: 4.0.0
+ dev: true
+
+ /open@8.4.0:
+ resolution: {integrity: sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==}
+ engines: {node: '>=12'}
+ dependencies:
+ define-lazy-prop: 2.0.0
is-docker: 2.2.1
is-wsl: 2.2.0
dev: true
- /open/8.4.0:
- resolution: {integrity: sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==}
+ /open@8.4.2:
+ resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==}
engines: {node: '>=12'}
dependencies:
define-lazy-prop: 2.0.0
@@ -17582,23 +14828,22 @@ packages:
is-wsl: 2.2.0
dev: true
- /opener/1.5.2:
+ /opener@1.5.2:
resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==}
- hasBin: true
dev: true
- /optimize-css-assets-webpack-plugin/6.0.1_webpack@4.46.0:
+ /optimize-css-assets-webpack-plugin@6.0.1(webpack@4.46.0):
resolution: {integrity: sha512-BshV2UZPfggZLdUfN3zFBbG4sl/DynUI+YCB6fRRDWaqO2OiWN8GPcp4Y0/fEV6B3k9Hzyk3czve3V/8B/SzKQ==}
peerDependencies:
webpack: ^4.0.0
dependencies:
- cssnano: 5.1.13_postcss@8.4.18
+ cssnano: 5.1.13(postcss@8.4.32)
last-call-webpack-plugin: 3.0.0
- postcss: 8.4.18
+ postcss: 8.4.32
webpack: 4.46.0
dev: true
- /optionator/0.8.3:
+ /optionator@0.8.3:
resolution: {integrity: sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==}
engines: {node: '>= 0.8.0'}
dependencies:
@@ -17610,7 +14855,7 @@ packages:
word-wrap: 1.2.3
dev: true
- /optionator/0.9.1:
+ /optionator@0.9.1:
resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==}
engines: {node: '>= 0.8.0'}
dependencies:
@@ -17622,7 +14867,19 @@ packages:
word-wrap: 1.2.3
dev: true
- /ora/5.4.1:
+ /optionator@0.9.3:
+ resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==}
+ engines: {node: '>= 0.8.0'}
+ dependencies:
+ '@aashutoshrathi/word-wrap': 1.2.6
+ deep-is: 0.1.4
+ fast-levenshtein: 2.0.6
+ levn: 0.4.1
+ prelude-ls: 1.2.1
+ type-check: 0.4.0
+ dev: true
+
+ /ora@5.4.1:
resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==}
engines: {node: '>=10'}
dependencies:
@@ -17637,140 +14894,99 @@ packages:
wcwidth: 1.0.1
dev: true
- /os-browserify/0.3.0:
+ /os-browserify@0.3.0:
resolution: {integrity: sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==}
dev: true
- /os-homedir/1.0.2:
- resolution: {integrity: sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==}
- engines: {node: '>=0.10.0'}
+ /os-locale@5.0.0:
+ resolution: {integrity: sha512-tqZcNEDAIZKBEPnHPlVDvKrp7NzgLi7jRmhKiUoa2NUmhl13FtkAGLUVR+ZsYvApBQdBfYm43A4tXXQ4IrYLBA==}
+ engines: {node: '>=10'}
+ dependencies:
+ execa: 4.1.0
+ lcid: 3.1.1
+ mem: 5.1.1
dev: true
- optional: true
- /p-all/2.1.0:
- resolution: {integrity: sha512-HbZxz5FONzz/z2gJfk6bFca0BCiSRF8jU3yCsWOen/vR6lZjfPOu/e7L3uFzTW1i0H8TlC3vqQstEJPQL4/uLA==}
- engines: {node: '>=6'}
- dependencies:
- p-map: 2.1.0
+ /os-shim@0.1.3:
+ resolution: {integrity: sha512-jd0cvB8qQ5uVt0lvCIexBaROw1KyKm5sbulg2fWOHjETisuCzWyt+eTZKEMs8v6HwzoGs8xik26jg7eCM6pS+A==}
+ engines: {node: '>= 0.4.0'}
dev: true
- /p-cancelable/1.1.0:
+ /p-cancelable@1.1.0:
resolution: {integrity: sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==}
engines: {node: '>=6'}
dev: true
- /p-defer/1.0.0:
- resolution: {integrity: sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==}
- engines: {node: '>=4'}
- dev: true
-
- /p-each-series/2.2.0:
- resolution: {integrity: sha512-ycIL2+1V32th+8scbpTvyHNaHe02z0sjgh91XXjAk+ZeXoPN4Z46DVUnzdso0aX4KckKw0FNNFHdjZ2UsZvxiA==}
- engines: {node: '>=8'}
- dev: true
-
- /p-event/4.2.0:
- resolution: {integrity: sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ==}
- engines: {node: '>=8'}
- dependencies:
- p-timeout: 3.2.0
- dev: true
-
- /p-event/5.0.1:
- resolution: {integrity: sha512-dd589iCQ7m1L0bmC5NLlVYfy3TbBEsMUfWx9PyAgPeIcFZ/E2yaTZ4Rz4MiBmmJShviiftHVXOqfnfzJ6kyMrQ==}
- engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
- dependencies:
- p-timeout: 5.1.0
+ /p-cancelable@3.0.0:
+ resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==}
+ engines: {node: '>=12.20'}
dev: true
- /p-filter/2.1.0:
- resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==}
- engines: {node: '>=8'}
- dependencies:
- p-map: 2.1.0
+ /p-defer@1.0.0:
+ resolution: {integrity: sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==}
+ engines: {node: '>=4'}
dev: true
- /p-finally/1.0.0:
- resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==}
- engines: {node: '>=4'}
+ /p-is-promise@2.1.0:
+ resolution: {integrity: sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==}
+ engines: {node: '>=6'}
dev: true
- /p-limit/2.3.0:
+ /p-limit@2.3.0:
resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
engines: {node: '>=6'}
dependencies:
p-try: 2.2.0
dev: true
- /p-limit/3.1.0:
+ /p-limit@3.1.0:
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
engines: {node: '>=10'}
dependencies:
yocto-queue: 0.1.0
dev: true
- /p-limit/4.0.0:
- resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==}
- engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
- dependencies:
- yocto-queue: 1.0.0
- dev: true
-
- /p-locate/3.0.0:
+ /p-locate@3.0.0:
resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==}
engines: {node: '>=6'}
dependencies:
p-limit: 2.3.0
dev: true
- /p-locate/4.1.0:
+ /p-locate@4.1.0:
resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
engines: {node: '>=8'}
dependencies:
p-limit: 2.3.0
dev: true
- /p-locate/5.0.0:
+ /p-locate@5.0.0:
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
engines: {node: '>=10'}
dependencies:
p-limit: 3.1.0
dev: true
- /p-locate/6.0.0:
- resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==}
- engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
- dependencies:
- p-limit: 4.0.0
- dev: true
-
- /p-map/2.1.0:
- resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==}
- engines: {node: '>=6'}
- dev: true
-
- /p-map/3.0.0:
+ /p-map@3.0.0:
resolution: {integrity: sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==}
engines: {node: '>=8'}
dependencies:
aggregate-error: 3.1.0
dev: true
- /p-map/4.0.0:
+ /p-map@4.0.0:
resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==}
engines: {node: '>=10'}
dependencies:
aggregate-error: 3.1.0
dev: true
- /p-map/5.5.0:
- resolution: {integrity: sha512-VFqfGDHlx87K66yZrNdI4YGtD70IRyd+zSvgks6mzHPRNkoKy+9EKP4SFC77/vTTQYmRmti7dvqC+m5jBrBAcg==}
- engines: {node: '>=12'}
- dependencies:
- aggregate-error: 4.0.1
+ /p-map@6.0.0:
+ resolution: {integrity: sha512-T8BatKGY+k5rU+Q/GTYgrEf2r4xRMevAN5mtXc2aPc4rS1j3s+vWTaO2Wag94neXuCAUAs8cxBL9EeB5EA6diw==}
+ engines: {node: '>=16'}
dev: true
- /p-retry/4.6.2:
+ /p-retry@4.6.2:
resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==}
engines: {node: '>=8'}
dependencies:
@@ -17778,76 +14994,82 @@ packages:
retry: 0.13.1
dev: true
- /p-timeout/3.2.0:
- resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==}
- engines: {node: '>=8'}
- dependencies:
- p-finally: 1.0.0
- dev: true
-
- /p-timeout/5.1.0:
- resolution: {integrity: sha512-auFDyzzzGZZZdHz3BtET9VEz0SE/uMEAx7uWfGPucfzEwwe/xH0iVeZibQmANYE/hp9T2+UUZT5m+BKyrDp3Ew==}
- engines: {node: '>=12'}
- dev: true
-
- /p-try/2.2.0:
+ /p-try@2.2.0:
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
engines: {node: '>=6'}
dev: true
- /package-hash/4.0.0:
+ /package-config@5.0.0:
+ resolution: {integrity: sha512-GYTTew2slBcYdvRHqjhwaaydVMvn/qrGC323+nKclYioNSLTDUM/lGgtGTgyHVtYcozb+XkE8CNhwcraOmZ9Mg==}
+ engines: {node: '>=18'}
+ dependencies:
+ find-up-simple: 1.0.0
+ load-json-file: 7.0.1
+ dev: true
+
+ /package-hash@4.0.0:
resolution: {integrity: sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==}
engines: {node: '>=8'}
dependencies:
- graceful-fs: 4.2.10
+ graceful-fs: 4.2.11
hasha: 5.2.2
lodash.flattendeep: 4.4.0
release-zalgo: 1.0.0
dev: true
- /package-json/6.5.0:
+ /package-json@6.5.0:
resolution: {integrity: sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==}
engines: {node: '>=8'}
dependencies:
got: 9.6.0
registry-auth-token: 4.2.2
registry-url: 5.1.0
- semver: 6.3.0
+ semver: 6.3.1
+ dev: true
+
+ /package-json@8.1.1:
+ resolution: {integrity: sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA==}
+ engines: {node: '>=14.16'}
+ dependencies:
+ got: 12.6.1
+ registry-auth-token: 5.0.2
+ registry-url: 6.0.1
+ semver: 7.5.4
dev: true
- /pako/1.0.11:
+ /pako@1.0.11:
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
dev: true
- /parallel-transform/1.2.0:
+ /parallel-transform@1.2.0:
resolution: {integrity: sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==}
dependencies:
- cyclist: 1.0.1
+ cyclist: 1.0.2
inherits: 2.0.4
- readable-stream: 2.3.7
+ readable-stream: 2.3.8
dev: true
- /param-case/2.1.1:
+ /param-case@2.1.1:
resolution: {integrity: sha512-eQE845L6ot89sk2N8liD8HAuH4ca6Vvr7VWAWwt7+kvvG5aBcPmmphQ68JsEG2qa9n1TykS2DLeMt363AAH8/w==}
dependencies:
no-case: 2.3.2
dev: true
- /param-case/3.0.4:
+ /param-case@3.0.4:
resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==}
dependencies:
dot-case: 3.0.4
- tslib: 2.4.1
+ tslib: 2.6.2
dev: true
- /parent-module/1.0.1:
+ /parent-module@1.0.1:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
dependencies:
callsites: 3.1.0
dev: true
- /parse-asn1/5.1.6:
+ /parse-asn1@5.1.6:
resolution: {integrity: sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==}
dependencies:
asn1.js: 5.4.1
@@ -17857,26 +15079,7 @@ packages:
safe-buffer: 5.2.1
dev: true
- /parse-entities/2.0.0:
- resolution: {integrity: sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==}
- dependencies:
- character-entities: 1.2.4
- character-entities-legacy: 1.1.4
- character-reference-invalid: 1.1.4
- is-alphanumerical: 1.0.4
- is-decimal: 1.0.4
- is-hexadecimal: 1.0.4
- dev: true
-
- /parse-json/2.2.0:
- resolution: {integrity: sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==}
- engines: {node: '>=0.10.0'}
- dependencies:
- error-ex: 1.3.2
- dev: true
- optional: true
-
- /parse-json/4.0.0:
+ /parse-json@4.0.0:
resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==}
engines: {node: '>=4'}
dependencies:
@@ -17884,142 +15087,131 @@ packages:
json-parse-better-errors: 1.0.2
dev: true
- /parse-json/5.2.0:
+ /parse-json@5.2.0:
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
engines: {node: '>=8'}
dependencies:
- '@babel/code-frame': 7.18.6
+ '@babel/code-frame': 7.23.5
error-ex: 1.3.2
json-parse-even-better-errors: 2.3.1
lines-and-columns: 1.2.4
dev: true
- /parse-ms/2.1.0:
- resolution: {integrity: sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==}
- engines: {node: '>=6'}
+ /parse-json@6.0.2:
+ resolution: {integrity: sha512-SA5aMiaIjXkAiBrW/yPgLgQAQg42f7K3ACO+2l/zOvtQBwX58DMUsFJXelW2fx3yMBmWOVkR6j1MGsdSbCA4UA==}
+ engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+ dependencies:
+ '@babel/code-frame': 7.23.5
+ error-ex: 1.3.2
+ json-parse-even-better-errors: 2.3.1
+ lines-and-columns: 2.0.4
+ dev: true
+
+ /parse-ms@3.0.0:
+ resolution: {integrity: sha512-Tpb8Z7r7XbbtBTrM9UhpkzzaMrqA2VXMT3YChzYltwV3P3pM6t8wl7TvpMnSTosz1aQAdVib7kdoys7vYOPerw==}
+ engines: {node: '>=12'}
dev: true
- /parse5-htmlparser2-tree-adapter/7.0.0:
+ /parse5-htmlparser2-tree-adapter@7.0.0:
resolution: {integrity: sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==}
dependencies:
domhandler: 5.0.3
- parse5: 7.1.1
+ parse5: 7.1.2
dev: true
- /parse5/4.0.0:
+ /parse5@4.0.0:
resolution: {integrity: sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==}
dev: true
- /parse5/5.1.0:
+ /parse5@5.1.0:
resolution: {integrity: sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==}
dev: true
- /parse5/6.0.1:
- resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==}
- dev: true
-
- /parse5/7.1.1:
- resolution: {integrity: sha512-kwpuwzB+px5WUg9pyK0IcK/shltJN5/OVhQagxhCQNtT9Y9QRZqNY2e1cmbu/paRh5LMnz/oVTVLBpjFmMZhSg==}
+ /parse5@7.1.2:
+ resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==}
dependencies:
- entities: 4.4.0
+ entities: 4.5.0
dev: true
- /parseurl/1.3.3:
+ /parseurl@1.3.3:
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
engines: {node: '>= 0.8'}
dev: true
- /pascal-case/3.1.2:
+ /pascal-case@3.1.2:
resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==}
dependencies:
no-case: 3.0.4
- tslib: 2.4.1
+ tslib: 2.6.2
dev: true
- /pascalcase/0.1.1:
+ /pascalcase@0.1.1:
resolution: {integrity: sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==}
engines: {node: '>=0.10.0'}
dev: true
- /path-browserify/0.0.1:
+ /path-browserify@0.0.1:
resolution: {integrity: sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==}
dev: true
- /path-dirname/1.0.2:
+ /path-dirname@1.0.2:
resolution: {integrity: sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==}
- dev: true
-
- /path-exists/2.1.0:
- resolution: {integrity: sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ==}
- engines: {node: '>=0.10.0'}
- dependencies:
- pinkie-promise: 2.0.1
+ requiresBuild: true
dev: true
optional: true
- /path-exists/3.0.0:
+ /path-exists@3.0.0:
resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==}
engines: {node: '>=4'}
dev: true
- /path-exists/4.0.0:
+ /path-exists@4.0.0:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'}
dev: true
- /path-exists/5.0.0:
- resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==}
- engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
- dev: true
-
- /path-is-absolute/1.0.1:
+ /path-is-absolute@1.0.1:
resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
engines: {node: '>=0.10.0'}
- /path-key/2.0.1:
- resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==}
- engines: {node: '>=4'}
- dev: true
-
- /path-key/3.1.1:
+ /path-key@3.1.1:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
- dev: true
- /path-parse/1.0.7:
- resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
+ /path-key@4.0.0:
+ resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==}
+ engines: {node: '>=12'}
dev: true
- /path-to-regexp/0.1.7:
- resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==}
- dev: true
+ /path-parse@1.0.7:
+ resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
- /path-type/1.1.0:
- resolution: {integrity: sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg==}
- engines: {node: '>=0.10.0'}
+ /path-scurry@1.10.1:
+ resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==}
+ engines: {node: '>=16 || 14 >=14.17'}
dependencies:
- graceful-fs: 4.2.10
- pify: 2.3.0
- pinkie-promise: 2.0.1
- dev: true
- optional: true
+ lru-cache: 10.1.0
+ minipass: 7.0.4
- /path-type/3.0.0:
- resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==}
- engines: {node: '>=4'}
- dependencies:
- pify: 3.0.0
+ /path-to-regexp@0.1.7:
+ resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==}
dev: true
- /path-type/4.0.0:
+ /path-type@4.0.0:
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
engines: {node: '>=8'}
dev: true
- /pathval/1.1.1:
+ /path-type@5.0.0:
+ resolution: {integrity: sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==}
+ engines: {node: '>=12'}
+ dev: true
+
+ /pathval@1.1.1:
resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==}
+ dev: true
- /pbkdf2/3.1.2:
+ /pbkdf2@3.1.2:
resolution: {integrity: sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==}
engines: {node: '>=0.12'}
dependencies:
@@ -18030,126 +15222,106 @@ packages:
sha.js: 2.4.11
dev: true
- /performance-now/2.1.0:
+ /pend@1.2.0:
+ resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==}
+ dev: true
+
+ /performance-now@2.1.0:
resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==}
dev: true
- /picocolors/0.2.1:
+ /picocolors@0.2.1:
resolution: {integrity: sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==}
dev: true
- /picocolors/1.0.0:
+ /picocolors@1.0.0:
resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
- dev: true
- /picomatch/2.3.1:
+ /picomatch@2.3.1:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'}
+
+ /picomatch@3.0.1:
+ resolution: {integrity: sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==}
+ engines: {node: '>=10'}
dev: true
- /pify/2.3.0:
+ /pify@2.3.0:
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
engines: {node: '>=0.10.0'}
- dev: true
- optional: true
- /pify/3.0.0:
- resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==}
- engines: {node: '>=4'}
- dev: true
-
- /pify/4.0.1:
+ /pify@4.0.1:
resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==}
engines: {node: '>=6'}
dev: true
- /pinkie-promise/2.0.1:
- resolution: {integrity: sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==}
- engines: {node: '>=0.10.0'}
+ /pino-abstract-transport@1.1.0:
+ resolution: {integrity: sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA==}
dependencies:
- pinkie: 2.0.4
+ readable-stream: 4.5.2
+ split2: 4.2.0
dev: true
- optional: true
-
- /pinkie/2.0.4:
- resolution: {integrity: sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==}
- engines: {node: '>=0.10.0'}
- dev: true
- optional: true
- /pirates/4.0.5:
- resolution: {integrity: sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==}
- engines: {node: '>= 6'}
+ /pino-std-serializers@6.2.2:
+ resolution: {integrity: sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==}
dev: true
- /pkg-conf/4.0.0:
- resolution: {integrity: sha512-7dmgi4UY4qk+4mj5Cd8v/GExPo0K+SlY+hulOSdfZ/T6jVH6//y7NtzZo5WrfhDBxuQ0jCa7fLZmNaNh7EWL/w==}
- engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+ /pino@8.17.2:
+ resolution: {integrity: sha512-LA6qKgeDMLr2ux2y/YiUt47EfgQ+S9LznBWOJdN3q1dx2sv0ziDLUBeVpyVv17TEcGCBuWf0zNtg3M5m1NhhWQ==}
+ hasBin: true
dependencies:
- find-up: 6.3.0
- load-json-file: 7.0.1
+ atomic-sleep: 1.0.0
+ fast-redact: 3.3.0
+ on-exit-leak-free: 2.1.2
+ pino-abstract-transport: 1.1.0
+ pino-std-serializers: 6.2.2
+ process-warning: 3.0.0
+ quick-format-unescaped: 4.0.4
+ real-require: 0.2.0
+ safe-stable-stringify: 2.4.3
+ sonic-boom: 3.8.0
+ thread-stream: 2.4.1
dev: true
- /pkg-dir/3.0.0:
+ /pirates@4.0.5:
+ resolution: {integrity: sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==}
+ engines: {node: '>= 6'}
+
+ /pkg-dir@3.0.0:
resolution: {integrity: sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==}
engines: {node: '>=6'}
dependencies:
find-up: 3.0.0
dev: true
- /pkg-dir/4.2.0:
+ /pkg-dir@4.2.0:
resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==}
engines: {node: '>=8'}
dependencies:
find-up: 4.1.0
dev: true
- /pkg-dir/5.0.0:
- resolution: {integrity: sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==}
- engines: {node: '>=10'}
- dependencies:
- find-up: 5.0.0
- dev: true
-
- /plur/5.1.0:
+ /plur@5.1.0:
resolution: {integrity: sha512-VP/72JeXqak2KiOzjgKtQen5y3IZHn+9GOuLDafPv0eXa47xq0At93XahYBs26MsifCQ4enGKwbjBTKgb9QJXg==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
dependencies:
- irregular-plurals: 3.3.0
+ irregular-plurals: 3.5.0
dev: true
- /pn/1.1.0:
+ /pn@1.1.0:
resolution: {integrity: sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==}
dev: true
- /pnp-webpack-plugin/1.6.4_typescript@4.4.4:
- resolution: {integrity: sha512-7Wjy+9E3WwLOEL30D+m8TSTF7qJJUJLONBnwQp0518siuMxUQUbgZwssaFX+QKlZkjHZcw/IpZCt/H0srrntSg==}
- engines: {node: '>=6'}
- dependencies:
- ts-pnp: 1.2.0_typescript@4.4.4
- transitivePeerDependencies:
- - typescript
- dev: true
-
- /pnp-webpack-plugin/1.6.4_typescript@4.8.4:
- resolution: {integrity: sha512-7Wjy+9E3WwLOEL30D+m8TSTF7qJJUJLONBnwQp0518siuMxUQUbgZwssaFX+QKlZkjHZcw/IpZCt/H0srrntSg==}
- engines: {node: '>=6'}
- dependencies:
- ts-pnp: 1.2.0_typescript@4.6.4
- transitivePeerDependencies:
- - typescript
- dev: true
-
- /pnp-webpack-plugin/1.7.0_typescript@4.6.4:
+ /pnp-webpack-plugin@1.7.0(typescript@4.6.4):
resolution: {integrity: sha512-2Rb3vm+EXble/sMXNSu6eoBx8e79gKqhNq9F5ZWW6ERNCTE/Q0wQNne5541tE5vKjfM8hpNCYL+LGc1YTfI0dg==}
engines: {node: '>=6'}
dependencies:
- ts-pnp: 1.2.0_typescript@4.6.4
+ ts-pnp: 1.2.0(typescript@4.6.4)
transitivePeerDependencies:
- typescript
dev: true
- /po2json/0.4.5:
+ /po2json@0.4.5:
resolution: {integrity: sha512-JH0hgi1fC0t9UvdiyS7kcVly0N1WNey4R2YZ/jPaxQKYm6Cfej7ZTgiEy8LP2JwoEhONceiNS8JH5mWPQkiXeA==}
engines: {node: '>= 0.8.0'}
hasBin: true
@@ -18158,61 +15330,85 @@ packages:
nomnom: 1.8.1
dev: true
- /polished/4.2.2:
+ /polished@4.2.2:
resolution: {integrity: sha512-Sz2Lkdxz6F2Pgnpi9U5Ng/WdWAUZxmHrNPoVlm3aAemxoy2Qy7LGjQg4uf8qKelDAUW94F4np3iH2YPf2qefcQ==}
engines: {node: '>=10'}
dependencies:
'@babel/runtime': 7.18.9
dev: true
- /posix-character-classes/0.1.1:
+ /posix-character-classes@0.1.1:
resolution: {integrity: sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==}
engines: {node: '>=0.10.0'}
dev: true
- /postcss-calc/7.0.5:
+ /postcss-calc@7.0.5:
resolution: {integrity: sha512-1tKHutbGtLtEZF6PT4JSihCHfIVldU72mZ8SdZHIYriIZ9fh9k9aWSppaT8rHsyI3dX+KSR+W+Ix9BMY3AODrg==}
dependencies:
postcss: 7.0.39
- postcss-selector-parser: 6.0.10
+ postcss-selector-parser: 6.0.12
postcss-value-parser: 4.2.0
dev: true
- /postcss-calc/8.2.4_postcss@8.4.18:
+ /postcss-calc@8.2.4(postcss@8.4.32):
resolution: {integrity: sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==}
peerDependencies:
postcss: ^8.2.2
dependencies:
- postcss: 8.4.18
- postcss-selector-parser: 6.0.10
+ postcss: 8.4.32
+ postcss-selector-parser: 6.0.12
postcss-value-parser: 4.2.0
dev: true
- /postcss-colormin/4.0.3:
+ /postcss-cli@10.1.0(postcss@8.4.23):
+ resolution: {integrity: sha512-Zu7PLORkE9YwNdvOeOVKPmWghprOtjFQU3srMUGbdz3pHJiFh7yZ4geiZFMkjMfB0mtTFR3h8RemR62rPkbOPA==}
+ engines: {node: '>=14'}
+ hasBin: true
+ peerDependencies:
+ postcss: ^8.0.0
+ dependencies:
+ chokidar: 3.5.3
+ dependency-graph: 0.11.0
+ fs-extra: 11.1.1
+ get-stdin: 9.0.0
+ globby: 13.2.2
+ picocolors: 1.0.0
+ postcss: 8.4.23
+ postcss-load-config: 4.0.1(postcss@8.4.23)
+ postcss-reporter: 7.0.5(postcss@8.4.23)
+ pretty-hrtime: 1.0.3
+ read-cache: 1.0.0
+ slash: 5.0.1
+ yargs: 17.7.2
+ transitivePeerDependencies:
+ - ts-node
+ dev: true
+
+ /postcss-colormin@4.0.3:
resolution: {integrity: sha512-WyQFAdDZpExQh32j0U0feWisZ0dmOtPl44qYmJKkq9xFWY3p+4qnRzCHeNrkeRhwPHz9bQ3mo0/yVkaply0MNw==}
engines: {node: '>=6.9.0'}
dependencies:
- browserslist: 4.21.4
+ browserslist: 4.22.2
color: 3.2.1
has: 1.0.3
postcss: 7.0.39
postcss-value-parser: 3.3.1
dev: true
- /postcss-colormin/5.3.0_postcss@8.4.18:
+ /postcss-colormin@5.3.0(postcss@8.4.32):
resolution: {integrity: sha512-WdDO4gOFG2Z8n4P8TWBpshnL3JpmNmJwdnfP2gbk2qBA8PWwOYcmjmI/t3CmMeL72a7Hkd+x/Mg9O2/0rD54Pg==}
engines: {node: ^10 || ^12 || >=14.0}
peerDependencies:
postcss: ^8.2.15
dependencies:
- browserslist: 4.21.4
+ browserslist: 4.22.2
caniuse-api: 3.0.0
colord: 2.9.3
- postcss: 8.4.18
+ postcss: 8.4.32
postcss-value-parser: 4.2.0
dev: true
- /postcss-convert-values/4.0.1:
+ /postcss-convert-values@4.0.1:
resolution: {integrity: sha512-Kisdo1y77KUC0Jmn0OXU/COOJbzM8cImvw1ZFsBgBgMgb1iL23Zs/LXRe3r+EZqM3vGYKdQ2YJVQ5VkJI+zEJQ==}
engines: {node: '>=6.9.0'}
dependencies:
@@ -18220,88 +15416,102 @@ packages:
postcss-value-parser: 3.3.1
dev: true
- /postcss-convert-values/5.1.2_postcss@8.4.18:
+ /postcss-convert-values@5.1.2(postcss@8.4.32):
resolution: {integrity: sha512-c6Hzc4GAv95B7suy4udszX9Zy4ETyMCgFPUDtWjdFTKH1SE9eFY/jEpHSwTH1QPuwxHpWslhckUQWbNRM4ho5g==}
engines: {node: ^10 || ^12 || >=14.0}
peerDependencies:
postcss: ^8.2.15
dependencies:
- browserslist: 4.21.4
- postcss: 8.4.18
+ browserslist: 4.22.2
+ postcss: 8.4.32
postcss-value-parser: 4.2.0
dev: true
- /postcss-discard-comments/4.0.2:
+ /postcss-discard-comments@4.0.2:
resolution: {integrity: sha512-RJutN259iuRf3IW7GZyLM5Sw4GLTOH8FmsXBnv8Ab/Tc2k4SR4qbV4DNbyyY4+Sjo362SyDmW2DQ7lBSChrpkg==}
engines: {node: '>=6.9.0'}
dependencies:
postcss: 7.0.39
dev: true
- /postcss-discard-comments/5.1.2_postcss@8.4.18:
+ /postcss-discard-comments@5.1.2(postcss@8.4.32):
resolution: {integrity: sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==}
engines: {node: ^10 || ^12 || >=14.0}
peerDependencies:
postcss: ^8.2.15
dependencies:
- postcss: 8.4.18
+ postcss: 8.4.32
dev: true
- /postcss-discard-duplicates/4.0.2:
+ /postcss-discard-duplicates@4.0.2:
resolution: {integrity: sha512-ZNQfR1gPNAiXZhgENFfEglF93pciw0WxMkJeVmw8eF+JZBbMD7jp6C67GqJAXVZP2BWbOztKfbsdmMp/k8c6oQ==}
engines: {node: '>=6.9.0'}
dependencies:
postcss: 7.0.39
dev: true
- /postcss-discard-duplicates/5.1.0_postcss@8.4.18:
+ /postcss-discard-duplicates@5.1.0(postcss@8.4.32):
resolution: {integrity: sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==}
engines: {node: ^10 || ^12 || >=14.0}
peerDependencies:
postcss: ^8.2.15
dependencies:
- postcss: 8.4.18
+ postcss: 8.4.32
dev: true
- /postcss-discard-empty/4.0.1:
+ /postcss-discard-empty@4.0.1:
resolution: {integrity: sha512-B9miTzbznhDjTfjvipfHoqbWKwd0Mj+/fL5s1QOz06wufguil+Xheo4XpOnc4NqKYBCNqqEzgPv2aPBIJLox0w==}
engines: {node: '>=6.9.0'}
dependencies:
postcss: 7.0.39
dev: true
- /postcss-discard-empty/5.1.1_postcss@8.4.18:
+ /postcss-discard-empty@5.1.1(postcss@8.4.32):
resolution: {integrity: sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==}
engines: {node: ^10 || ^12 || >=14.0}
peerDependencies:
postcss: ^8.2.15
dependencies:
- postcss: 8.4.18
+ postcss: 8.4.32
dev: true
- /postcss-discard-overridden/4.0.1:
+ /postcss-discard-overridden@4.0.1:
resolution: {integrity: sha512-IYY2bEDD7g1XM1IDEsUT4//iEYCxAmP5oDSFMVU/JVvT7gh+l4fmjciLqGgwjdWpQIdb0Che2VX00QObS5+cTg==}
engines: {node: '>=6.9.0'}
dependencies:
postcss: 7.0.39
dev: true
- /postcss-discard-overridden/5.1.0_postcss@8.4.18:
+ /postcss-discard-overridden@5.1.0(postcss@8.4.32):
resolution: {integrity: sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==}
engines: {node: ^10 || ^12 || >=14.0}
peerDependencies:
postcss: ^8.2.15
dependencies:
- postcss: 8.4.18
+ postcss: 8.4.32
dev: true
- /postcss-flexbugs-fixes/4.2.1:
- resolution: {integrity: sha512-9SiofaZ9CWpQWxOwRh1b/r85KD5y7GgvsNt1056k6OYLvWUun0czCvogfJgylC22uJTwW1KzY3Gz65NZRlvoiQ==}
+ /postcss-import@15.1.0(postcss@8.4.23):
+ resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==}
+ engines: {node: '>=14.0.0'}
+ peerDependencies:
+ postcss: ^8.0.0
dependencies:
- postcss: 7.0.39
- dev: true
+ postcss: 8.4.23
+ postcss-value-parser: 4.2.0
+ read-cache: 1.0.0
+ resolve: 1.22.8
- /postcss-load-config/3.1.4_postcss@8.4.18:
+ /postcss-js@4.0.1(postcss@8.4.23):
+ resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==}
+ engines: {node: ^12 || ^14 || >= 16}
+ peerDependencies:
+ postcss: ^8.4.21
+ dependencies:
+ camelcase-css: 2.0.1
+ postcss: 8.4.23
+
+ /postcss-load-config@3.1.4(postcss@8.4.32):
resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==}
engines: {node: '>= 10'}
peerDependencies:
@@ -18313,28 +15523,28 @@ packages:
ts-node:
optional: true
dependencies:
- lilconfig: 2.0.6
- postcss: 8.4.18
+ lilconfig: 2.1.0
+ postcss: 8.4.32
yaml: 1.10.2
dev: true
- /postcss-loader/4.3.0_dhonik3q6ff6ozbzdscnovq2ka:
- resolution: {integrity: sha512-M/dSoIiNDOo8Rk0mUqoj4kpGq91gcxCfb9PoyZVdZ76/AuhxylHDYZblNE8o+EQ9AMSASeMFEKxZf5aU6wlx1Q==}
- engines: {node: '>= 10.13.0'}
+ /postcss-load-config@4.0.1(postcss@8.4.23):
+ resolution: {integrity: sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==}
+ engines: {node: '>= 14'}
peerDependencies:
- postcss: ^7.0.0 || ^8.0.1
- webpack: ^4.0.0 || ^5.0.0
+ postcss: '>=8.0.9'
+ ts-node: '>=9.0.0'
+ peerDependenciesMeta:
+ postcss:
+ optional: true
+ ts-node:
+ optional: true
dependencies:
- cosmiconfig: 7.0.1
- klona: 2.0.5
- loader-utils: 2.0.3
- postcss: 8.4.18
- schema-utils: 3.1.1
- semver: 7.3.8
- webpack: 4.46.0
- dev: true
+ lilconfig: 2.1.0
+ postcss: 8.4.23
+ yaml: 2.2.2
- /postcss-loader/4.3.0_gzaxsinx64nntyd3vmdqwl7coe:
+ /postcss-loader@4.3.0(postcss@8.4.32)(webpack@4.46.0):
resolution: {integrity: sha512-M/dSoIiNDOo8Rk0mUqoj4kpGq91gcxCfb9PoyZVdZ76/AuhxylHDYZblNE8o+EQ9AMSASeMFEKxZf5aU6wlx1Q==}
engines: {node: '>= 10.13.0'}
peerDependencies:
@@ -18344,13 +15554,13 @@ packages:
cosmiconfig: 7.0.1
klona: 2.0.5
loader-utils: 2.0.3
- postcss: 7.0.39
+ postcss: 8.4.32
schema-utils: 3.1.1
- semver: 7.3.8
+ semver: 7.5.4
webpack: 4.46.0
dev: true
- /postcss-merge-longhand/4.0.11:
+ /postcss-merge-longhand@4.0.11:
resolution: {integrity: sha512-alx/zmoeXvJjp7L4mxEMjh8lxVlDFX1gqWHzaaQewwMZiVhLo42TEClKaeHbRf6J7j82ZOdTJ808RtN0ZOZwvw==}
engines: {node: '>=6.9.0'}
dependencies:
@@ -18360,22 +15570,22 @@ packages:
stylehacks: 4.0.3
dev: true
- /postcss-merge-longhand/5.1.6_postcss@8.4.18:
+ /postcss-merge-longhand@5.1.6(postcss@8.4.32):
resolution: {integrity: sha512-6C/UGF/3T5OE2CEbOuX7iNO63dnvqhGZeUnKkDeifebY0XqkkvrctYSZurpNE902LDf2yKwwPFgotnfSoPhQiw==}
engines: {node: ^10 || ^12 || >=14.0}
peerDependencies:
postcss: ^8.2.15
dependencies:
- postcss: 8.4.18
+ postcss: 8.4.32
postcss-value-parser: 4.2.0
- stylehacks: 5.1.0_postcss@8.4.18
+ stylehacks: 5.1.0(postcss@8.4.32)
dev: true
- /postcss-merge-rules/4.0.3:
+ /postcss-merge-rules@4.0.3:
resolution: {integrity: sha512-U7e3r1SbvYzO0Jr3UT/zKBVgYYyhAz0aitvGIYOYK5CPmkNih+WDSsS5tvPrJ8YMQYlEMvsZIiqmn7HdFUaeEQ==}
engines: {node: '>=6.9.0'}
dependencies:
- browserslist: 4.21.4
+ browserslist: 4.22.2
caniuse-api: 3.0.0
cssnano-util-same-parent: 4.0.1
postcss: 7.0.39
@@ -18383,20 +15593,20 @@ packages:
vendors: 1.0.4
dev: true
- /postcss-merge-rules/5.1.2_postcss@8.4.18:
+ /postcss-merge-rules@5.1.2(postcss@8.4.32):
resolution: {integrity: sha512-zKMUlnw+zYCWoPN6yhPjtcEdlJaMUZ0WyVcxTAmw3lkkN/NDMRkOkiuctQEoWAOvH7twaxUUdvBWl0d4+hifRQ==}
engines: {node: ^10 || ^12 || >=14.0}
peerDependencies:
postcss: ^8.2.15
dependencies:
- browserslist: 4.21.4
+ browserslist: 4.22.2
caniuse-api: 3.0.0
- cssnano-utils: 3.1.0_postcss@8.4.18
- postcss: 8.4.18
- postcss-selector-parser: 6.0.10
+ cssnano-utils: 3.1.0(postcss@8.4.32)
+ postcss: 8.4.32
+ postcss-selector-parser: 6.0.12
dev: true
- /postcss-minify-font-values/4.0.2:
+ /postcss-minify-font-values@4.0.2:
resolution: {integrity: sha512-j85oO6OnRU9zPf04+PZv1LYIYOprWm6IA6zkXkrJXyRveDEuQggG6tvoy8ir8ZwjLxLuGfNkCZEQG7zan+Hbtg==}
engines: {node: '>=6.9.0'}
dependencies:
@@ -18404,17 +15614,17 @@ packages:
postcss-value-parser: 3.3.1
dev: true
- /postcss-minify-font-values/5.1.0_postcss@8.4.18:
+ /postcss-minify-font-values@5.1.0(postcss@8.4.32):
resolution: {integrity: sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==}
engines: {node: ^10 || ^12 || >=14.0}
peerDependencies:
postcss: ^8.2.15
dependencies:
- postcss: 8.4.18
+ postcss: 8.4.32
postcss-value-parser: 4.2.0
dev: true
- /postcss-minify-gradients/4.0.2:
+ /postcss-minify-gradients@4.0.2:
resolution: {integrity: sha512-qKPfwlONdcf/AndP1U8SJ/uzIJtowHlMaSioKzebAXSG4iJthlWC9iSWznQcX4f66gIWX44RSA841HTHj3wK+Q==}
engines: {node: '>=6.9.0'}
dependencies:
@@ -18424,43 +15634,43 @@ packages:
postcss-value-parser: 3.3.1
dev: true
- /postcss-minify-gradients/5.1.1_postcss@8.4.18:
+ /postcss-minify-gradients@5.1.1(postcss@8.4.32):
resolution: {integrity: sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==}
engines: {node: ^10 || ^12 || >=14.0}
peerDependencies:
postcss: ^8.2.15
dependencies:
colord: 2.9.3
- cssnano-utils: 3.1.0_postcss@8.4.18
- postcss: 8.4.18
+ cssnano-utils: 3.1.0(postcss@8.4.32)
+ postcss: 8.4.32
postcss-value-parser: 4.2.0
dev: true
- /postcss-minify-params/4.0.2:
+ /postcss-minify-params@4.0.2:
resolution: {integrity: sha512-G7eWyzEx0xL4/wiBBJxJOz48zAKV2WG3iZOqVhPet/9geefm/Px5uo1fzlHu+DOjT+m0Mmiz3jkQzVHe6wxAWg==}
engines: {node: '>=6.9.0'}
dependencies:
alphanum-sort: 1.0.2
- browserslist: 4.21.4
+ browserslist: 4.22.2
cssnano-util-get-arguments: 4.0.0
postcss: 7.0.39
postcss-value-parser: 3.3.1
uniqs: 2.0.0
dev: true
- /postcss-minify-params/5.1.3_postcss@8.4.18:
+ /postcss-minify-params@5.1.3(postcss@8.4.32):
resolution: {integrity: sha512-bkzpWcjykkqIujNL+EVEPOlLYi/eZ050oImVtHU7b4lFS82jPnsCb44gvC6pxaNt38Els3jWYDHTjHKf0koTgg==}
engines: {node: ^10 || ^12 || >=14.0}
peerDependencies:
postcss: ^8.2.15
dependencies:
- browserslist: 4.21.4
- cssnano-utils: 3.1.0_postcss@8.4.18
- postcss: 8.4.18
+ browserslist: 4.22.2
+ cssnano-utils: 3.1.0(postcss@8.4.32)
+ postcss: 8.4.32
postcss-value-parser: 4.2.0
dev: true
- /postcss-minify-selectors/4.0.2:
+ /postcss-minify-selectors@4.0.2:
resolution: {integrity: sha512-D5S1iViljXBj9kflQo4YutWnJmwm8VvIsU1GeXJGiG9j8CIg9zs4voPMdQDUmIxetUOh60VilsNzCiAFTOqu3g==}
engines: {node: '>=6.9.0'}
dependencies:
@@ -18470,106 +15680,83 @@ packages:
postcss-selector-parser: 3.1.2
dev: true
- /postcss-minify-selectors/5.2.1_postcss@8.4.18:
+ /postcss-minify-selectors@5.2.1(postcss@8.4.32):
resolution: {integrity: sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==}
engines: {node: ^10 || ^12 || >=14.0}
peerDependencies:
postcss: ^8.2.15
dependencies:
- postcss: 8.4.18
- postcss-selector-parser: 6.0.10
- dev: true
-
- /postcss-modules-extract-imports/2.0.0:
- resolution: {integrity: sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ==}
- engines: {node: '>= 6'}
- dependencies:
- postcss: 7.0.39
+ postcss: 8.4.32
+ postcss-selector-parser: 6.0.12
dev: true
- /postcss-modules-extract-imports/3.0.0_postcss@8.4.18:
+ /postcss-modules-extract-imports@3.0.0(postcss@8.4.32):
resolution: {integrity: sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==}
engines: {node: ^10 || ^12 || >= 14}
peerDependencies:
postcss: ^8.1.0
dependencies:
- postcss: 8.4.18
+ postcss: 8.4.32
dev: true
- /postcss-modules-local-by-default/3.0.3:
- resolution: {integrity: sha512-e3xDq+LotiGesympRlKNgaJ0PCzoUIdpH0dj47iWAui/kyTgh3CiAr1qP54uodmJhl6p9rN6BoNcdEDVJx9RDw==}
- engines: {node: '>= 6'}
- dependencies:
- icss-utils: 4.1.1
- postcss: 7.0.39
- postcss-selector-parser: 6.0.10
- postcss-value-parser: 4.2.0
- dev: true
-
- /postcss-modules-local-by-default/4.0.0_postcss@8.4.18:
+ /postcss-modules-local-by-default@4.0.0(postcss@8.4.32):
resolution: {integrity: sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==}
engines: {node: ^10 || ^12 || >= 14}
peerDependencies:
postcss: ^8.1.0
dependencies:
- icss-utils: 5.1.0_postcss@8.4.18
- postcss: 8.4.18
- postcss-selector-parser: 6.0.10
+ icss-utils: 5.1.0(postcss@8.4.32)
+ postcss: 8.4.32
+ postcss-selector-parser: 6.0.12
postcss-value-parser: 4.2.0
dev: true
- /postcss-modules-scope/2.2.0:
- resolution: {integrity: sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ==}
- engines: {node: '>= 6'}
- dependencies:
- postcss: 7.0.39
- postcss-selector-parser: 6.0.10
- dev: true
-
- /postcss-modules-scope/3.0.0_postcss@8.4.18:
+ /postcss-modules-scope@3.0.0(postcss@8.4.32):
resolution: {integrity: sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==}
engines: {node: ^10 || ^12 || >= 14}
peerDependencies:
postcss: ^8.1.0
dependencies:
- postcss: 8.4.18
- postcss-selector-parser: 6.0.10
- dev: true
-
- /postcss-modules-values/3.0.0:
- resolution: {integrity: sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg==}
- dependencies:
- icss-utils: 4.1.1
- postcss: 7.0.39
+ postcss: 8.4.32
+ postcss-selector-parser: 6.0.12
dev: true
- /postcss-modules-values/4.0.0_postcss@8.4.18:
+ /postcss-modules-values@4.0.0(postcss@8.4.32):
resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==}
engines: {node: ^10 || ^12 || >= 14}
peerDependencies:
postcss: ^8.1.0
dependencies:
- icss-utils: 5.1.0_postcss@8.4.18
- postcss: 8.4.18
+ icss-utils: 5.1.0(postcss@8.4.32)
+ postcss: 8.4.32
dev: true
- /postcss-normalize-charset/4.0.1:
+ /postcss-nested@6.0.1(postcss@8.4.23):
+ resolution: {integrity: sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==}
+ engines: {node: '>=12.0'}
+ peerDependencies:
+ postcss: ^8.2.14
+ dependencies:
+ postcss: 8.4.23
+ postcss-selector-parser: 6.0.12
+
+ /postcss-normalize-charset@4.0.1:
resolution: {integrity: sha512-gMXCrrlWh6G27U0hF3vNvR3w8I1s2wOBILvA87iNXaPvSNo5uZAMYsZG7XjCUf1eVxuPfyL4TJ7++SGZLc9A3g==}
engines: {node: '>=6.9.0'}
dependencies:
postcss: 7.0.39
dev: true
- /postcss-normalize-charset/5.1.0_postcss@8.4.18:
+ /postcss-normalize-charset@5.1.0(postcss@8.4.32):
resolution: {integrity: sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==}
engines: {node: ^10 || ^12 || >=14.0}
peerDependencies:
postcss: ^8.2.15
dependencies:
- postcss: 8.4.18
+ postcss: 8.4.32
dev: true
- /postcss-normalize-display-values/4.0.2:
+ /postcss-normalize-display-values@4.0.2:
resolution: {integrity: sha512-3F2jcsaMW7+VtRMAqf/3m4cPFhPD3EFRgNs18u+k3lTJJlVe7d0YPO+bnwqo2xg8YiRpDXJI2u8A0wqJxMsQuQ==}
engines: {node: '>=6.9.0'}
dependencies:
@@ -18578,17 +15765,17 @@ packages:
postcss-value-parser: 3.3.1
dev: true
- /postcss-normalize-display-values/5.1.0_postcss@8.4.18:
+ /postcss-normalize-display-values@5.1.0(postcss@8.4.32):
resolution: {integrity: sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==}
engines: {node: ^10 || ^12 || >=14.0}
peerDependencies:
postcss: ^8.2.15
dependencies:
- postcss: 8.4.18
+ postcss: 8.4.32
postcss-value-parser: 4.2.0
dev: true
- /postcss-normalize-positions/4.0.2:
+ /postcss-normalize-positions@4.0.2:
resolution: {integrity: sha512-Dlf3/9AxpxE+NF1fJxYDeggi5WwV35MXGFnnoccP/9qDtFrTArZ0D0R+iKcg5WsUd8nUYMIl8yXDCtcrT8JrdA==}
engines: {node: '>=6.9.0'}
dependencies:
@@ -18598,17 +15785,17 @@ packages:
postcss-value-parser: 3.3.1
dev: true
- /postcss-normalize-positions/5.1.1_postcss@8.4.18:
+ /postcss-normalize-positions@5.1.1(postcss@8.4.32):
resolution: {integrity: sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg==}
engines: {node: ^10 || ^12 || >=14.0}
peerDependencies:
postcss: ^8.2.15
dependencies:
- postcss: 8.4.18
+ postcss: 8.4.32
postcss-value-parser: 4.2.0
dev: true
- /postcss-normalize-repeat-style/4.0.2:
+ /postcss-normalize-repeat-style@4.0.2:
resolution: {integrity: sha512-qvigdYYMpSuoFs3Is/f5nHdRLJN/ITA7huIoCyqqENJe9PvPmLhNLMu7QTjPdtnVf6OcYYO5SHonx4+fbJE1+Q==}
engines: {node: '>=6.9.0'}
dependencies:
@@ -18618,17 +15805,17 @@ packages:
postcss-value-parser: 3.3.1
dev: true
- /postcss-normalize-repeat-style/5.1.1_postcss@8.4.18:
+ /postcss-normalize-repeat-style@5.1.1(postcss@8.4.32):
resolution: {integrity: sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g==}
engines: {node: ^10 || ^12 || >=14.0}
peerDependencies:
postcss: ^8.2.15
dependencies:
- postcss: 8.4.18
+ postcss: 8.4.32
postcss-value-parser: 4.2.0
dev: true
- /postcss-normalize-string/4.0.2:
+ /postcss-normalize-string@4.0.2:
resolution: {integrity: sha512-RrERod97Dnwqq49WNz8qo66ps0swYZDSb6rM57kN2J+aoyEAJfZ6bMx0sx/F9TIEX0xthPGCmeyiam/jXif0eA==}
engines: {node: '>=6.9.0'}
dependencies:
@@ -18637,17 +15824,17 @@ packages:
postcss-value-parser: 3.3.1
dev: true
- /postcss-normalize-string/5.1.0_postcss@8.4.18:
+ /postcss-normalize-string@5.1.0(postcss@8.4.32):
resolution: {integrity: sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==}
engines: {node: ^10 || ^12 || >=14.0}
peerDependencies:
postcss: ^8.2.15
dependencies:
- postcss: 8.4.18
+ postcss: 8.4.32
postcss-value-parser: 4.2.0
dev: true
- /postcss-normalize-timing-functions/4.0.2:
+ /postcss-normalize-timing-functions@4.0.2:
resolution: {integrity: sha512-acwJY95edP762e++00Ehq9L4sZCEcOPyaHwoaFOhIwWCDfik6YvqsYNxckee65JHLKzuNSSmAdxwD2Cud1Z54A==}
engines: {node: '>=6.9.0'}
dependencies:
@@ -18656,37 +15843,37 @@ packages:
postcss-value-parser: 3.3.1
dev: true
- /postcss-normalize-timing-functions/5.1.0_postcss@8.4.18:
+ /postcss-normalize-timing-functions@5.1.0(postcss@8.4.32):
resolution: {integrity: sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==}
engines: {node: ^10 || ^12 || >=14.0}
peerDependencies:
postcss: ^8.2.15
dependencies:
- postcss: 8.4.18
+ postcss: 8.4.32
postcss-value-parser: 4.2.0
dev: true
- /postcss-normalize-unicode/4.0.1:
+ /postcss-normalize-unicode@4.0.1:
resolution: {integrity: sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg==}
engines: {node: '>=6.9.0'}
dependencies:
- browserslist: 4.21.4
+ browserslist: 4.22.2
postcss: 7.0.39
postcss-value-parser: 3.3.1
dev: true
- /postcss-normalize-unicode/5.1.0_postcss@8.4.18:
+ /postcss-normalize-unicode@5.1.0(postcss@8.4.32):
resolution: {integrity: sha512-J6M3MizAAZ2dOdSjy2caayJLQT8E8K9XjLce8AUQMwOrCvjCHv24aLC/Lps1R1ylOfol5VIDMaM/Lo9NGlk1SQ==}
engines: {node: ^10 || ^12 || >=14.0}
peerDependencies:
postcss: ^8.2.15
dependencies:
- browserslist: 4.21.4
- postcss: 8.4.18
+ browserslist: 4.22.2
+ postcss: 8.4.32
postcss-value-parser: 4.2.0
dev: true
- /postcss-normalize-url/4.0.1:
+ /postcss-normalize-url@4.0.1:
resolution: {integrity: sha512-p5oVaF4+IHwu7VpMan/SSpmpYxcJMtkGppYf0VbdH5B6hN8YNmVyJLuY9FmLQTzY3fag5ESUUHDqM+heid0UVA==}
engines: {node: '>=6.9.0'}
dependencies:
@@ -18696,18 +15883,18 @@ packages:
postcss-value-parser: 3.3.1
dev: true
- /postcss-normalize-url/5.1.0_postcss@8.4.18:
+ /postcss-normalize-url@5.1.0(postcss@8.4.32):
resolution: {integrity: sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==}
engines: {node: ^10 || ^12 || >=14.0}
peerDependencies:
postcss: ^8.2.15
dependencies:
normalize-url: 6.1.0
- postcss: 8.4.18
+ postcss: 8.4.32
postcss-value-parser: 4.2.0
dev: true
- /postcss-normalize-whitespace/4.0.2:
+ /postcss-normalize-whitespace@4.0.2:
resolution: {integrity: sha512-tO8QIgrsI3p95r8fyqKV+ufKlSHh9hMJqACqbv2XknufqEDhDvbguXGBBqxw9nsQoXWf0qOqppziKJKHMD4GtA==}
engines: {node: '>=6.9.0'}
dependencies:
@@ -18715,17 +15902,17 @@ packages:
postcss-value-parser: 3.3.1
dev: true
- /postcss-normalize-whitespace/5.1.1_postcss@8.4.18:
+ /postcss-normalize-whitespace@5.1.1(postcss@8.4.32):
resolution: {integrity: sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==}
engines: {node: ^10 || ^12 || >=14.0}
peerDependencies:
postcss: ^8.2.15
dependencies:
- postcss: 8.4.18
+ postcss: 8.4.32
postcss-value-parser: 4.2.0
dev: true
- /postcss-ordered-values/4.1.2:
+ /postcss-ordered-values@4.1.2:
resolution: {integrity: sha512-2fCObh5UanxvSxeXrtLtlwVThBvHn6MQcu4ksNT2tsaV2Fg76R2CV98W7wNSlX+5/pFwEyaDwKLLoEV7uRybAw==}
engines: {node: '>=6.9.0'}
dependencies:
@@ -18734,39 +15921,39 @@ packages:
postcss-value-parser: 3.3.1
dev: true
- /postcss-ordered-values/5.1.3_postcss@8.4.18:
+ /postcss-ordered-values@5.1.3(postcss@8.4.32):
resolution: {integrity: sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ==}
engines: {node: ^10 || ^12 || >=14.0}
peerDependencies:
postcss: ^8.2.15
dependencies:
- cssnano-utils: 3.1.0_postcss@8.4.18
- postcss: 8.4.18
+ cssnano-utils: 3.1.0(postcss@8.4.32)
+ postcss: 8.4.32
postcss-value-parser: 4.2.0
dev: true
- /postcss-reduce-initial/4.0.3:
+ /postcss-reduce-initial@4.0.3:
resolution: {integrity: sha512-gKWmR5aUulSjbzOfD9AlJiHCGH6AEVLaM0AV+aSioxUDd16qXP1PCh8d1/BGVvpdWn8k/HiK7n6TjeoXN1F7DA==}
engines: {node: '>=6.9.0'}
dependencies:
- browserslist: 4.21.4
+ browserslist: 4.22.2
caniuse-api: 3.0.0
has: 1.0.3
postcss: 7.0.39
dev: true
- /postcss-reduce-initial/5.1.0_postcss@8.4.18:
+ /postcss-reduce-initial@5.1.0(postcss@8.4.32):
resolution: {integrity: sha512-5OgTUviz0aeH6MtBjHfbr57tml13PuedK/Ecg8szzd4XRMbYxH4572JFG067z+FqBIf6Zp/d+0581glkvvWMFw==}
engines: {node: ^10 || ^12 || >=14.0}
peerDependencies:
postcss: ^8.2.15
dependencies:
- browserslist: 4.21.4
+ browserslist: 4.22.2
caniuse-api: 3.0.0
- postcss: 8.4.18
+ postcss: 8.4.32
dev: true
- /postcss-reduce-transforms/4.0.2:
+ /postcss-reduce-transforms@4.0.2:
resolution: {integrity: sha512-EEVig1Q2QJ4ELpJXMZR8Vt5DQx8/mo+dGWSR7vWXqcob2gQLyQGsionYcGKATXvQzMPn6DSN1vTN7yFximdIAg==}
engines: {node: '>=6.9.0'}
dependencies:
@@ -18776,17 +15963,28 @@ packages:
postcss-value-parser: 3.3.1
dev: true
- /postcss-reduce-transforms/5.1.0_postcss@8.4.18:
+ /postcss-reduce-transforms@5.1.0(postcss@8.4.32):
resolution: {integrity: sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==}
engines: {node: ^10 || ^12 || >=14.0}
peerDependencies:
postcss: ^8.2.15
dependencies:
- postcss: 8.4.18
+ postcss: 8.4.32
postcss-value-parser: 4.2.0
dev: true
- /postcss-selector-parser/3.1.2:
+ /postcss-reporter@7.0.5(postcss@8.4.23):
+ resolution: {integrity: sha512-glWg7VZBilooZGOFPhN9msJ3FQs19Hie7l5a/eE6WglzYqVeH3ong3ShFcp9kDWJT1g2Y/wd59cocf9XxBtkWA==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ postcss: ^8.1.0
+ dependencies:
+ picocolors: 1.0.0
+ postcss: 8.4.23
+ thenby: 1.3.4
+ dev: true
+
+ /postcss-selector-parser@3.1.2:
resolution: {integrity: sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==}
engines: {node: '>=8'}
dependencies:
@@ -18795,7 +15993,7 @@ packages:
uniq: 1.0.1
dev: true
- /postcss-selector-parser/6.0.10:
+ /postcss-selector-parser@6.0.10:
resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==}
engines: {node: '>=4'}
dependencies:
@@ -18803,7 +16001,14 @@ packages:
util-deprecate: 1.0.2
dev: true
- /postcss-svgo/4.0.3:
+ /postcss-selector-parser@6.0.12:
+ resolution: {integrity: sha512-NdxGCAZdRrwVI1sy59+Wzrh+pMMHxapGnpfenDVlMEXoOcvt4pGE0JLK9YY2F5dLxcFYA/YbVQKhcGU+FtSYQg==}
+ engines: {node: '>=4'}
+ dependencies:
+ cssesc: 3.0.0
+ util-deprecate: 1.0.2
+
+ /postcss-svgo@4.0.3:
resolution: {integrity: sha512-NoRbrcMWTtUghzuKSoIm6XV+sJdvZ7GZSc3wdBN0W19FTtp2ko8NqLsgoh/m9CzNhU3KLPvQmjIwtaNFkaFTvw==}
engines: {node: '>=6.9.0'}
dependencies:
@@ -18812,18 +16017,18 @@ packages:
svgo: 1.3.2
dev: true
- /postcss-svgo/5.1.0_postcss@8.4.18:
+ /postcss-svgo@5.1.0(postcss@8.4.32):
resolution: {integrity: sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==}
engines: {node: ^10 || ^12 || >=14.0}
peerDependencies:
postcss: ^8.2.15
dependencies:
- postcss: 8.4.18
+ postcss: 8.4.32
postcss-value-parser: 4.2.0
svgo: 2.8.0
dev: true
- /postcss-unique-selectors/4.0.1:
+ /postcss-unique-selectors@4.0.1:
resolution: {integrity: sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg==}
engines: {node: '>=6.9.0'}
dependencies:
@@ -18832,25 +16037,24 @@ packages:
uniqs: 2.0.0
dev: true
- /postcss-unique-selectors/5.1.1_postcss@8.4.18:
+ /postcss-unique-selectors@5.1.1(postcss@8.4.32):
resolution: {integrity: sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==}
engines: {node: ^10 || ^12 || >=14.0}
peerDependencies:
postcss: ^8.2.15
dependencies:
- postcss: 8.4.18
- postcss-selector-parser: 6.0.10
+ postcss: 8.4.32
+ postcss-selector-parser: 6.0.12
dev: true
- /postcss-value-parser/3.3.1:
+ /postcss-value-parser@3.3.1:
resolution: {integrity: sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==}
dev: true
- /postcss-value-parser/4.2.0:
+ /postcss-value-parser@4.2.0:
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
- dev: true
- /postcss/7.0.39:
+ /postcss@7.0.39:
resolution: {integrity: sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==}
engines: {node: '>=6.0.0'}
dependencies:
@@ -18858,245 +16062,33 @@ packages:
source-map: 0.6.1
dev: true
- /postcss/8.4.18:
- resolution: {integrity: sha512-Wi8mWhncLJm11GATDaQKobXSNEYGUHeQLiQqDFG1qQ5UTDPTEvKw0Xt5NsTpktGTwLps3ByrWsBrG0rB8YQ9oA==}
+ /postcss@8.4.23:
+ resolution: {integrity: sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==}
engines: {node: ^10 || ^12 || >=14}
dependencies:
- nanoid: 3.3.4
+ nanoid: 3.3.6
picocolors: 1.0.0
source-map-js: 1.0.2
- dev: true
- /preact-cli/3.4.1_bbfn3vavr32nbxkp5peg6brs4i:
- resolution: {integrity: sha512-/4be0PuBmAIAox9u8GLJublFpEymq7Lk4JW4PEPz9ErFH/ncZf/oBPhECtXGq9IPqNOEe4r2l8sA+3uqKVwBfw==}
- engines: {node: '>=12'}
- hasBin: true
- peerDependencies:
- less-loader: ^7.3.0
- preact: '*'
- preact-render-to-string: '*'
- sass-loader: ^10.2.0
- stylus-loader: ^4.3.3
- peerDependenciesMeta:
- less-loader:
- optional: true
- sass-loader:
- optional: true
- stylus-loader:
- optional: true
+ /postcss@8.4.32:
+ resolution: {integrity: sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==}
+ engines: {node: ^10 || ^12 || >=14}
dependencies:
- '@babel/core': 7.18.9
- '@babel/plugin-proposal-class-properties': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-proposal-decorators': 7.19.6_@babel+core@7.18.9
- '@babel/plugin-proposal-object-rest-spread': 7.19.4_@babel+core@7.18.9
- '@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.18.9
- '@babel/plugin-transform-object-assign': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-transform-react-jsx': 7.19.0_@babel+core@7.18.9
- '@babel/preset-env': 7.19.4_@babel+core@7.18.9
- '@babel/preset-typescript': 7.18.6_@babel+core@7.18.9
- '@preact/async-loader': 3.0.1_preact@10.6.5
- '@prefresh/babel-plugin': 0.4.4
- '@prefresh/webpack': 3.3.4_l37gxlnxrbylk7wqownx5ddpoa
- '@types/webpack': 4.41.33
- autoprefixer: 10.4.12_postcss@8.4.18
- babel-esm-plugin: 0.9.0_webpack@4.46.0
- babel-loader: 8.2.5_7uc2ny5pnz7ums2wq2q562bf6y
- babel-plugin-macros: 3.1.0
- babel-plugin-transform-react-remove-prop-types: 0.4.24
- browserslist: 4.21.4
- compression-webpack-plugin: 6.1.1_webpack@4.46.0
- console-clear: 1.1.1
- copy-webpack-plugin: 6.4.1_webpack@4.46.0
- critters-webpack-plugin: 2.5.0_html-webpack-plugin@3.2.0
- cross-spawn-promise: 0.10.2
- css-loader: 5.2.7_webpack@4.46.0
- dotenv: 16.0.3
- ejs-loader: 0.5.0
- envinfo: 7.8.1
- esm: 3.2.25
- file-loader: 6.2.0_webpack@4.46.0
- fork-ts-checker-webpack-plugin: 4.1.6_gplzhsecki363wzvnzp4wfrwvi
- get-port: 5.1.1
- gittar: 0.1.1
- glob: 8.0.3
- html-webpack-exclude-assets-plugin: 0.0.7
- html-webpack-plugin: 3.2.0_webpack@4.46.0
- ip: 1.1.8
- isomorphic-unfetch: 3.1.0
- kleur: 4.1.5
- loader-utils: 2.0.3
- mini-css-extract-plugin: 1.6.2_webpack@4.46.0
- minimatch: 3.1.2
- native-url: 0.3.4
- optimize-css-assets-webpack-plugin: 6.0.1_webpack@4.46.0
- ora: 5.4.1
- pnp-webpack-plugin: 1.7.0_typescript@4.6.4
- postcss: 8.4.18
- postcss-load-config: 3.1.4_postcss@8.4.18
- postcss-loader: 4.3.0_dhonik3q6ff6ozbzdscnovq2ka
- preact: 10.6.5
- preact-render-to-string: 5.2.6_preact@10.6.5
- progress-bar-webpack-plugin: 2.1.0_webpack@4.46.0
- promise-polyfill: 8.2.3
- prompts: 2.4.2
- raw-loader: 4.0.2_webpack@4.46.0
- react-refresh: 0.10.0
- rimraf: 3.0.2
- sade: 1.8.1
- sass-loader: 10.1.1_sass@1.55.0
- size-plugin: 3.0.0_webpack@4.46.0
- source-map: 0.7.4
- stack-trace: 0.0.10
- style-loader: 2.0.0_webpack@4.46.0
- terser-webpack-plugin: 4.2.3_webpack@4.46.0
- typescript: 4.6.4
- update-notifier: 5.1.0
- url-loader: 4.1.1_lit45vopotvaqup7lrvlnvtxwy
- validate-npm-package-name: 4.0.0
- webpack: 4.46.0
- webpack-bundle-analyzer: 4.6.1
- webpack-dev-server: 4.11.1_webpack@4.46.0
- webpack-fix-style-only-entries: 0.6.1
- webpack-manifest-plugin: 4.1.1_webpack@4.46.0
- webpack-merge: 5.8.0
- webpack-plugin-replace: 1.2.0
- which: 2.0.2
- workbox-cacheable-response: 6.5.4
- workbox-core: 6.5.4
- workbox-precaching: 6.5.4
- workbox-routing: 6.5.4
- workbox-strategies: 6.5.4
- workbox-webpack-plugin: 6.5.4_webpack@4.46.0
- transitivePeerDependencies:
- - '@types/babel__core'
- - bluebird
- - bufferutil
- - debug
- - encoding
- - eslint
- - supports-color
- - ts-node
- - utf-8-validate
- - vue-template-compiler
- - webpack-cli
- - webpack-command
+ nanoid: 3.3.7
+ picocolors: 1.0.0
+ source-map-js: 1.0.2
dev: true
- /preact-cli/3.4.1_i2jslynuqxjzp37vlc24guk7gu:
- resolution: {integrity: sha512-/4be0PuBmAIAox9u8GLJublFpEymq7Lk4JW4PEPz9ErFH/ncZf/oBPhECtXGq9IPqNOEe4r2l8sA+3uqKVwBfw==}
- engines: {node: '>=12'}
- hasBin: true
- peerDependencies:
- less-loader: ^7.3.0
- preact: '*'
- preact-render-to-string: '*'
- sass-loader: ^10.2.0
- stylus-loader: ^4.3.3
- peerDependenciesMeta:
- less-loader:
- optional: true
- sass-loader:
- optional: true
- stylus-loader:
- optional: true
+ /postcss@8.4.33:
+ resolution: {integrity: sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==}
+ engines: {node: ^10 || ^12 || >=14}
dependencies:
- '@babel/core': 7.18.9
- '@babel/plugin-proposal-class-properties': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-proposal-decorators': 7.19.6_@babel+core@7.18.9
- '@babel/plugin-proposal-object-rest-spread': 7.19.4_@babel+core@7.18.9
- '@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.18.9
- '@babel/plugin-transform-object-assign': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-transform-react-jsx': 7.19.0_@babel+core@7.18.9
- '@babel/preset-env': 7.19.4_@babel+core@7.18.9
- '@babel/preset-typescript': 7.18.6_@babel+core@7.18.9
- '@preact/async-loader': 3.0.1_preact@10.11.3
- '@prefresh/babel-plugin': 0.4.4
- '@prefresh/webpack': 3.3.4_2ylwkgirq2zgmdyuhwfa4uhfgq
- '@types/webpack': 4.41.33
- autoprefixer: 10.4.12_postcss@8.4.18
- babel-esm-plugin: 0.9.0_webpack@4.46.0
- babel-loader: 8.2.5_7uc2ny5pnz7ums2wq2q562bf6y
- babel-plugin-macros: 3.1.0
- babel-plugin-transform-react-remove-prop-types: 0.4.24
- browserslist: 4.21.4
- compression-webpack-plugin: 6.1.1_webpack@4.46.0
- console-clear: 1.1.1
- copy-webpack-plugin: 6.4.1_webpack@4.46.0
- critters-webpack-plugin: 2.5.0_html-webpack-plugin@3.2.0
- cross-spawn-promise: 0.10.2
- css-loader: 5.2.7_webpack@4.46.0
- dotenv: 16.0.3
- ejs-loader: 0.5.0
- envinfo: 7.8.1
- esm: 3.2.25
- file-loader: 6.2.0_webpack@4.46.0
- fork-ts-checker-webpack-plugin: 4.1.6_u7kjabuvawcog7hjctusduehvm
- get-port: 5.1.1
- gittar: 0.1.1
- glob: 8.0.3
- html-webpack-exclude-assets-plugin: 0.0.7
- html-webpack-plugin: 3.2.0_webpack@4.46.0
- ip: 1.1.8
- isomorphic-unfetch: 3.1.0
- kleur: 4.1.5
- loader-utils: 2.0.3
- mini-css-extract-plugin: 1.6.2_webpack@4.46.0
- minimatch: 3.1.2
- native-url: 0.3.4
- optimize-css-assets-webpack-plugin: 6.0.1_webpack@4.46.0
- ora: 5.4.1
- pnp-webpack-plugin: 1.7.0_typescript@4.6.4
- postcss: 8.4.18
- postcss-load-config: 3.1.4_postcss@8.4.18
- postcss-loader: 4.3.0_dhonik3q6ff6ozbzdscnovq2ka
- preact: 10.11.3
- preact-render-to-string: 5.2.6_preact@10.11.3
- progress-bar-webpack-plugin: 2.1.0_webpack@4.46.0
- promise-polyfill: 8.2.3
- prompts: 2.4.2
- raw-loader: 4.0.2_webpack@4.46.0
- react-refresh: 0.10.0
- rimraf: 3.0.2
- sade: 1.8.1
- size-plugin: 3.0.0_webpack@4.46.0
- source-map: 0.7.4
- stack-trace: 0.0.10
- style-loader: 2.0.0_webpack@4.46.0
- terser-webpack-plugin: 4.2.3_webpack@4.46.0
- typescript: 4.6.4
- update-notifier: 5.1.0
- url-loader: 4.1.1_lit45vopotvaqup7lrvlnvtxwy
- validate-npm-package-name: 4.0.0
- webpack: 4.46.0
- webpack-bundle-analyzer: 4.6.1
- webpack-dev-server: 4.11.1_webpack@4.46.0
- webpack-fix-style-only-entries: 0.6.1
- webpack-manifest-plugin: 4.1.1_webpack@4.46.0
- webpack-merge: 5.8.0
- webpack-plugin-replace: 1.2.0
- which: 2.0.2
- workbox-cacheable-response: 6.5.4
- workbox-core: 6.5.4
- workbox-precaching: 6.5.4
- workbox-routing: 6.5.4
- workbox-strategies: 6.5.4
- workbox-webpack-plugin: 6.5.4_webpack@4.46.0
- transitivePeerDependencies:
- - '@types/babel__core'
- - bluebird
- - bufferutil
- - debug
- - encoding
- - eslint
- - supports-color
- - ts-node
- - utf-8-validate
- - vue-template-compiler
- - webpack-cli
- - webpack-command
+ nanoid: 3.3.7
+ picocolors: 1.0.0
+ source-map-js: 1.0.2
dev: true
- /preact-cli/3.4.1_rg336vsyl675j3voc2t5224afq:
+ /preact-cli@3.4.1(eslint@8.56.0)(preact-render-to-string@5.2.6)(preact@10.11.3):
resolution: {integrity: sha512-/4be0PuBmAIAox9u8GLJublFpEymq7Lk4JW4PEPz9ErFH/ncZf/oBPhECtXGq9IPqNOEe4r2l8sA+3uqKVwBfw==}
engines: {node: '>=12'}
hasBin: true
@@ -19114,79 +16106,78 @@ packages:
stylus-loader:
optional: true
dependencies:
- '@babel/core': 7.18.9
- '@babel/plugin-proposal-class-properties': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-proposal-decorators': 7.19.6_@babel+core@7.18.9
- '@babel/plugin-proposal-object-rest-spread': 7.19.4_@babel+core@7.18.9
- '@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.18.9
- '@babel/plugin-transform-object-assign': 7.18.6_@babel+core@7.18.9
- '@babel/plugin-transform-react-jsx': 7.19.0_@babel+core@7.18.9
- '@babel/preset-env': 7.19.4_@babel+core@7.18.9
- '@babel/preset-typescript': 7.18.6_@babel+core@7.18.9
- '@preact/async-loader': 3.0.1_preact@10.11.2
+ '@babel/core': 7.22.1
+ '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.22.1)
+ '@babel/plugin-proposal-decorators': 7.19.6(@babel/core@7.22.1)
+ '@babel/plugin-proposal-object-rest-spread': 7.19.4(@babel/core@7.22.1)
+ '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.22.1)
+ '@babel/plugin-transform-object-assign': 7.18.6(@babel/core@7.22.1)
+ '@babel/plugin-transform-react-jsx': 7.19.0(@babel/core@7.22.1)
+ '@babel/preset-env': 7.19.4(@babel/core@7.22.1)
+ '@babel/preset-typescript': 7.18.6(@babel/core@7.22.1)
+ '@preact/async-loader': 3.0.1(preact@10.11.3)
'@prefresh/babel-plugin': 0.4.4
- '@prefresh/webpack': 3.3.4_kitpfapqi2defymxf2rxzdj6na
+ '@prefresh/webpack': 3.3.4(@prefresh/babel-plugin@0.4.4)(preact@10.11.3)(webpack@4.46.0)
'@types/webpack': 4.41.33
- autoprefixer: 10.4.12_postcss@8.4.18
- babel-esm-plugin: 0.9.0_webpack@4.46.0
- babel-loader: 8.2.5_7uc2ny5pnz7ums2wq2q562bf6y
+ autoprefixer: 10.4.14(postcss@8.4.32)
+ babel-esm-plugin: 0.9.0(webpack@4.46.0)
+ babel-loader: 8.2.5(@babel/core@7.22.1)(webpack@4.46.0)
babel-plugin-macros: 3.1.0
babel-plugin-transform-react-remove-prop-types: 0.4.24
browserslist: 4.21.4
- compression-webpack-plugin: 6.1.1_webpack@4.46.0
+ compression-webpack-plugin: 6.1.1(webpack@4.46.0)
console-clear: 1.1.1
- copy-webpack-plugin: 6.4.1_webpack@4.46.0
- critters-webpack-plugin: 2.5.0_html-webpack-plugin@3.2.0
+ copy-webpack-plugin: 6.4.1(webpack@4.46.0)
+ critters-webpack-plugin: 2.5.0(html-webpack-plugin@3.2.0)
cross-spawn-promise: 0.10.2
- css-loader: 5.2.7_webpack@4.46.0
+ css-loader: 5.2.7(webpack@4.46.0)
dotenv: 16.0.3
ejs-loader: 0.5.0
envinfo: 7.8.1
esm: 3.2.25
- file-loader: 6.2.0_webpack@4.46.0
- fork-ts-checker-webpack-plugin: 4.1.6_gplzhsecki363wzvnzp4wfrwvi
+ file-loader: 6.2.0(webpack@4.46.0)
+ fork-ts-checker-webpack-plugin: 4.1.6(eslint@8.56.0)(typescript@4.6.4)(webpack@4.46.0)
get-port: 5.1.1
gittar: 0.1.1
- glob: 8.0.3
+ glob: 8.1.0
html-webpack-exclude-assets-plugin: 0.0.7
- html-webpack-plugin: 3.2.0_webpack@4.46.0
+ html-webpack-plugin: 3.2.0(webpack@4.46.0)
ip: 1.1.8
isomorphic-unfetch: 3.1.0
kleur: 4.1.5
loader-utils: 2.0.3
- mini-css-extract-plugin: 1.6.2_webpack@4.46.0
+ mini-css-extract-plugin: 1.6.2(webpack@4.46.0)
minimatch: 3.1.2
native-url: 0.3.4
- optimize-css-assets-webpack-plugin: 6.0.1_webpack@4.46.0
+ optimize-css-assets-webpack-plugin: 6.0.1(webpack@4.46.0)
ora: 5.4.1
- pnp-webpack-plugin: 1.7.0_typescript@4.6.4
- postcss: 8.4.18
- postcss-load-config: 3.1.4_postcss@8.4.18
- postcss-loader: 4.3.0_dhonik3q6ff6ozbzdscnovq2ka
- preact: 10.11.2
- preact-render-to-string: 5.2.6_preact@10.11.2
- progress-bar-webpack-plugin: 2.1.0_webpack@4.46.0
+ pnp-webpack-plugin: 1.7.0(typescript@4.6.4)
+ postcss: 8.4.32
+ postcss-load-config: 3.1.4(postcss@8.4.32)
+ postcss-loader: 4.3.0(postcss@8.4.32)(webpack@4.46.0)
+ preact: 10.11.3
+ preact-render-to-string: 5.2.6(preact@10.11.3)
+ progress-bar-webpack-plugin: 2.1.0(webpack@4.46.0)
promise-polyfill: 8.2.3
prompts: 2.4.2
- raw-loader: 4.0.2_webpack@4.46.0
+ raw-loader: 4.0.2(webpack@4.46.0)
react-refresh: 0.10.0
rimraf: 3.0.2
sade: 1.8.1
- sass-loader: 10.1.1_sass@1.55.0
- size-plugin: 3.0.0_webpack@4.46.0
+ size-plugin: 3.0.0(webpack@4.46.0)
source-map: 0.7.4
stack-trace: 0.0.10
- style-loader: 2.0.0_webpack@4.46.0
- terser-webpack-plugin: 4.2.3_webpack@4.46.0
+ style-loader: 2.0.0(webpack@4.46.0)
+ terser-webpack-plugin: 4.2.3(webpack@4.46.0)
typescript: 4.6.4
update-notifier: 5.1.0
- url-loader: 4.1.1_lit45vopotvaqup7lrvlnvtxwy
+ url-loader: 4.1.1(file-loader@6.2.0)(webpack@4.46.0)
validate-npm-package-name: 4.0.0
webpack: 4.46.0
webpack-bundle-analyzer: 4.6.1
- webpack-dev-server: 4.11.1_webpack@4.46.0
+ webpack-dev-server: 4.11.1(webpack@4.46.0)
webpack-fix-style-only-entries: 0.6.1
- webpack-manifest-plugin: 4.1.1_webpack@4.46.0
+ webpack-manifest-plugin: 4.1.1(webpack@4.46.0)
webpack-merge: 5.8.0
webpack-plugin-replace: 1.2.0
which: 2.0.2
@@ -19195,7 +16186,7 @@ packages:
workbox-precaching: 6.5.4
workbox-routing: 6.5.4
workbox-strategies: 6.5.4
- workbox-webpack-plugin: 6.5.4_webpack@4.46.0
+ workbox-webpack-plugin: 6.5.4(webpack@4.46.0)
transitivePeerDependencies:
- '@types/babel__core'
- bluebird
@@ -19211,57 +16202,16 @@ packages:
- webpack-command
dev: true
- /preact-render-to-json/3.6.6_preact@10.11.2:
- resolution: {integrity: sha512-w+guVnrKJMtSdAYKD3INih1O+XNEgJmj58r49KjjnwE1tLqzNXq1NaZXDg1DGhQ0fj67DYKdR9BqhogFI0lvsg==}
- peerDependencies:
- preact: '*'
- dependencies:
- preact: 10.11.2
- dev: true
-
- /preact-render-to-json/3.6.6_preact@10.6.5:
- resolution: {integrity: sha512-w+guVnrKJMtSdAYKD3INih1O+XNEgJmj58r49KjjnwE1tLqzNXq1NaZXDg1DGhQ0fj67DYKdR9BqhogFI0lvsg==}
- peerDependencies:
- preact: '*'
- dependencies:
- preact: 10.6.5
- dev: true
-
- /preact-render-to-string/5.2.6_preact@10.11.2:
- resolution: {integrity: sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==}
- peerDependencies:
- preact: '>=10'
- dependencies:
- preact: 10.11.2
- pretty-format: 3.8.0
- dev: true
-
- /preact-render-to-string/5.2.6_preact@10.11.3:
+ /preact-render-to-string@5.2.6(preact@10.11.3):
resolution: {integrity: sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==}
peerDependencies:
preact: '>=10'
dependencies:
preact: 10.11.3
pretty-format: 3.8.0
-
- /preact-render-to-string/5.2.6_preact@10.6.5:
- resolution: {integrity: sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==}
- peerDependencies:
- preact: '>=10'
- dependencies:
- preact: 10.6.5
- pretty-format: 3.8.0
dev: true
- /preact-router/3.2.1_preact@10.11.2:
- resolution: {integrity: sha512-KEN2VN1DxUlTwzW5IFkF13YIA2OdQ2OvgJTkQREF+AA2NrHRLaGbB68EjS4IeZOa1shvQ1FvEm3bSLta4sXBhg==}
- peerDependencies:
- preact: '>=10'
- dependencies:
- preact: 10.11.2
- dev: false
-
- /preact-router/3.2.1_preact@10.11.3:
+ /preact-router@3.2.1(preact@10.11.3):
resolution: {integrity: sha512-KEN2VN1DxUlTwzW5IFkF13YIA2OdQ2OvgJTkQREF+AA2NrHRLaGbB68EjS4IeZOa1shvQ1FvEm3bSLta4sXBhg==}
peerDependencies:
preact: '>=10'
@@ -19269,118 +16219,112 @@ packages:
preact: 10.11.3
dev: false
- /preact-router/3.2.1_preact@10.6.5:
- resolution: {integrity: sha512-KEN2VN1DxUlTwzW5IFkF13YIA2OdQ2OvgJTkQREF+AA2NrHRLaGbB68EjS4IeZOa1shvQ1FvEm3bSLta4sXBhg==}
- peerDependencies:
- preact: '>=10'
- dependencies:
- preact: 10.6.5
- dev: false
-
- /preact/10.11.2:
- resolution: {integrity: sha512-skAwGDFmgxhq1DCBHke/9e12ewkhc7WYwjuhHB8HHS8zkdtITXLRmUMTeol2ldxvLwYtwbFeifZ9uDDWuyL4Iw==}
-
- /preact/10.11.3:
+ /preact@10.11.3:
resolution: {integrity: sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==}
- /preact/10.6.5:
- resolution: {integrity: sha512-i+LXM6JiVjQXSt2jG2vZZFapGpCuk1fl8o6ii3G84MA3xgj686FKjs4JFDkmUVhtxyq21+4ay74zqPykz9hU6w==}
+ /prebuild-install@7.1.1:
+ resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==}
+ engines: {node: '>=10'}
+ hasBin: true
+ requiresBuild: true
+ dependencies:
+ detect-libc: 2.0.2
+ expand-template: 2.0.3
+ github-from-package: 0.0.0
+ minimist: 1.2.8
+ mkdirp-classic: 0.5.3
+ napi-build-utils: 1.0.2
+ node-abi: 3.52.0
+ pump: 3.0.0
+ rc: 1.2.8
+ simple-get: 4.0.1
+ tar-fs: 2.1.1
+ tunnel-agent: 0.6.0
+ dev: false
+ optional: true
- /prelude-ls/1.1.2:
+ /prelude-ls@1.1.2:
resolution: {integrity: sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==}
engines: {node: '>= 0.8.0'}
dev: true
- /prelude-ls/1.2.1:
+ /prelude-ls@1.2.1:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
dev: true
- /prepend-http/2.0.0:
+ /prepend-http@2.0.0:
resolution: {integrity: sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==}
engines: {node: '>=4'}
dev: true
- /prettier/2.3.0:
- resolution: {integrity: sha512-kXtO4s0Lz/DW/IJ9QdWhAf7/NmPWQXkFr/r/WkR3vyI+0v8amTDxiaQSLzs8NBlytfLWX/7uQUMIW677yLKl4w==}
- engines: {node: '>=10.13.0'}
- hasBin: true
- dev: true
-
- /prettier/2.7.1:
- resolution: {integrity: sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==}
- engines: {node: '>=10.13.0'}
+ /prettier@3.1.1:
+ resolution: {integrity: sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==}
+ engines: {node: '>=14'}
hasBin: true
dev: true
- /pretty-bytes/4.0.2:
+ /pretty-bytes@4.0.2:
resolution: {integrity: sha512-yJAF+AjbHKlxQ8eezMd/34Mnj/YTQ3i6kLzvVsH4l/BfIFtp444n0wVbnsn66JimZ9uBofv815aRp1zCppxlWw==}
engines: {node: '>=4'}
dev: true
- /pretty-bytes/5.6.0:
+ /pretty-bytes@5.6.0:
resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==}
engines: {node: '>=6'}
dev: true
- /pretty-error/2.1.2:
+ /pretty-error@2.1.2:
resolution: {integrity: sha512-EY5oDzmsX5wvuynAByrmY0P0hcp+QpnAKbJng2A2MPjVKXCxrDSUkzghVJ4ZGPIv+JC4gX8fPUWscC0RtjsWGw==}
dependencies:
lodash: 4.17.21
renderkid: 2.0.7
dev: true
- /pretty-format/26.6.2:
- resolution: {integrity: sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==}
- engines: {node: '>= 10'}
- dependencies:
- '@jest/types': 26.6.2
- ansi-regex: 5.0.1
- ansi-styles: 4.3.0
- react-is: 17.0.2
- dev: true
-
- /pretty-format/27.5.1:
- resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
- engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
+ /pretty-error@4.0.0:
+ resolution: {integrity: sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==}
dependencies:
- ansi-regex: 5.0.1
- ansi-styles: 5.2.0
- react-is: 17.0.2
+ lodash: 4.17.21
+ renderkid: 3.0.0
dev: true
- /pretty-format/3.8.0:
+ /pretty-format@3.8.0:
resolution: {integrity: sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==}
+ dev: true
- /pretty-hrtime/1.0.3:
+ /pretty-hrtime@1.0.3:
resolution: {integrity: sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==}
engines: {node: '>= 0.8'}
dev: true
- /pretty-ms/7.0.1:
- resolution: {integrity: sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==}
- engines: {node: '>=10'}
+ /pretty-ms@8.0.0:
+ resolution: {integrity: sha512-ASJqOugUF1bbzI35STMBUpZqdfYKlJugy6JBziGi2EE+AL5JPJGSzvpeVXojxrr0ViUYoToUjb5kjSEGf7Y83Q==}
+ engines: {node: '>=14.16'}
dependencies:
- parse-ms: 2.1.0
+ parse-ms: 3.0.0
dev: true
- /process-nextick-args/2.0.1:
+ /process-nextick-args@2.0.1:
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
dev: true
- /process-on-spawn/1.0.0:
+ /process-on-spawn@1.0.0:
resolution: {integrity: sha512-1WsPDsUSMmZH5LeMLegqkPDrsGgsWwk1Exipy2hvB0o/F0ASzbpIctSCcZIK1ykJvtTJULEH+20WOFjMvGnCTg==}
engines: {node: '>=8'}
dependencies:
fromentries: 1.3.2
dev: true
- /process/0.11.10:
+ /process-warning@3.0.0:
+ resolution: {integrity: sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==}
+ dev: true
+
+ /process@0.11.10:
resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
engines: {node: '>= 0.6.0'}
dev: true
- /progress-bar-webpack-plugin/2.1.0_webpack@4.46.0:
+ /progress-bar-webpack-plugin@2.1.0(webpack@4.46.0):
resolution: {integrity: sha512-UtlZbnxpYk1wufEWfhIjRn2U52zlY38uvnzFhs8rRxJxC1hSqw88JNR2Mbpqq9Kix8L1nGb3uQ+/1BiUWbigAg==}
peerDependencies:
webpack: ^1.3.0 || ^2 || ^3 || ^4 || ^5
@@ -19390,21 +16334,12 @@ packages:
webpack: 4.46.0
dev: true
- /progress/2.0.3:
+ /progress@2.0.3:
resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==}
engines: {node: '>=0.4.0'}
dev: true
- /promise-inflight/1.0.1:
- resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==}
- peerDependencies:
- bluebird: '*'
- peerDependenciesMeta:
- bluebird:
- optional: true
- dev: true
-
- /promise-inflight/1.0.1_bluebird@3.7.2:
+ /promise-inflight@1.0.1(bluebird@3.7.2):
resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==}
peerDependencies:
bluebird: '*'
@@ -19415,32 +16350,18 @@ packages:
bluebird: 3.7.2
dev: true
- /promise-polyfill/8.2.3:
+ /promise-polyfill@8.2.3:
resolution: {integrity: sha512-Og0+jCRQetV84U8wVjMNccfGCnMQ9mGs9Hv78QFe+pSDD3gWTpz0y+1QCuxy5d/vBFuZ3iwP2eycAkvqIMPmWg==}
dev: true
- /promise.allsettled/1.0.5:
- resolution: {integrity: sha512-tVDqeZPoBC0SlzJHzWGZ2NKAguVq2oiYj7gbggbiTvH2itHohijTp7njOUA0aQ/nl+0lr/r6egmhoYu63UZ/pQ==}
- engines: {node: '>= 0.4'}
- dependencies:
- array.prototype.map: 1.0.4
- call-bind: 1.0.2
- define-properties: 1.1.4
- es-abstract: 1.20.4
- get-intrinsic: 1.1.3
- iterate-value: 1.0.2
- dev: true
-
- /promise.prototype.finally/3.1.3:
- resolution: {integrity: sha512-EXRF3fC9/0gz4qkt/f5EP5iW4kj9oFpBICNpCNOb/52+8nlHIX07FPLbi/q4qYBQ1xZqivMzTpNQSnArVASolQ==}
- engines: {node: '>= 0.4'}
+ /promise-toolbox@0.21.0:
+ resolution: {integrity: sha512-NV8aTmpwrZv+Iys54sSFOBx3tuVaOBvvrft5PNppnxy9xpU/akHbaWIril22AB22zaPgrgwKdD0KsrM0ptUtpg==}
+ engines: {node: '>=6'}
dependencies:
- call-bind: 1.0.2
- define-properties: 1.1.4
- es-abstract: 1.20.4
+ make-error: 1.3.6
dev: true
- /prompts/2.4.2:
+ /prompts@2.4.2:
resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
engines: {node: '>= 6'}
dependencies:
@@ -19448,7 +16369,7 @@ packages:
sisteransi: 1.0.5
dev: true
- /prop-types/15.8.1:
+ /prop-types@15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
dependencies:
loose-envify: 1.4.0
@@ -19456,17 +16377,15 @@ packages:
react-is: 16.13.1
dev: true
- /property-expr/2.0.5:
+ /property-expr@2.0.5:
resolution: {integrity: sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA==}
dev: false
- /property-information/5.6.0:
- resolution: {integrity: sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==}
- dependencies:
- xtend: 4.0.2
+ /proto-list@1.2.4:
+ resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==}
dev: true
- /proxy-addr/2.0.7:
+ /proxy-addr@2.0.7:
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
engines: {node: '>= 0.10'}
dependencies:
@@ -19474,23 +16393,19 @@ packages:
ipaddr.js: 1.9.1
dev: true
- /proxy-from-env/1.1.0:
- resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
- dev: true
-
- /prr/1.0.1:
+ /prr@1.0.1:
resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==}
dev: true
- /pseudomap/1.0.2:
+ /pseudomap@1.0.2:
resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==}
dev: true
- /psl/1.9.0:
+ /psl@1.9.0:
resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==}
dev: true
- /public-encrypt/4.0.3:
+ /public-encrypt@4.0.3:
resolution: {integrity: sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==}
dependencies:
bn.js: 4.12.0
@@ -19501,21 +16416,21 @@ packages:
safe-buffer: 5.2.1
dev: true
- /pump/2.0.1:
+ /pump@2.0.1:
resolution: {integrity: sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==}
dependencies:
end-of-stream: 1.4.4
once: 1.4.0
dev: true
- /pump/3.0.0:
+ /pump@3.0.0:
resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==}
+ requiresBuild: true
dependencies:
end-of-stream: 1.4.4
once: 1.4.0
- dev: true
- /pumpify/1.5.1:
+ /pumpify@1.5.1:
resolution: {integrity: sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==}
dependencies:
duplexify: 3.7.1
@@ -19523,119 +16438,104 @@ packages:
pump: 2.0.1
dev: true
- /punycode/1.3.2:
- resolution: {integrity: sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==}
- dev: true
-
- /punycode/1.4.1:
+ /punycode@1.4.1:
resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==}
dev: true
- /punycode/2.1.1:
+ /punycode@2.1.1:
resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==}
engines: {node: '>=6'}
dev: true
- /pupa/2.1.1:
+ /pupa@2.1.1:
resolution: {integrity: sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==}
engines: {node: '>=8'}
dependencies:
escape-goat: 2.1.1
dev: true
- /q/1.5.1:
+ /pupa@3.1.0:
+ resolution: {integrity: sha512-FLpr4flz5xZTSJxSeaheeMKN/EDzMdK7b8PTOC6a5PYFKTucWbdqjgqaEyH0shFiSJrVB1+Qqi4Tk19ccU6Aug==}
+ engines: {node: '>=12.20'}
+ dependencies:
+ escape-goat: 4.0.0
+ dev: true
+
+ /q@1.5.1:
resolution: {integrity: sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==}
engines: {node: '>=0.6.0', teleport: '>=0.2.0'}
dev: true
- /qr-scanner/1.4.1:
- resolution: {integrity: sha512-xiR90NONHTfTwaFgW/ihlqjGMIZg6ExHDOvGQRba1TvV+WVw7GoDArIOt21e+RO+9WiO4AJJq+mwc5f4BnGH3w==}
- dependencies:
- '@types/offscreencanvas': 2019.7.0
- dev: false
-
- /qrcode-generator/1.4.4:
+ /qrcode-generator@1.4.4:
resolution: {integrity: sha512-HM7yY8O2ilqhmULxGMpcHSF1EhJJ9yBj8gvDEuZ6M+KGJ0YY2hKpnXvRD+hZPLrDVck3ExIGhmPtSdcjC+guuw==}
dev: false
- /qs/6.11.0:
+ /qs@6.11.0:
resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==}
engines: {node: '>=0.6'}
dependencies:
side-channel: 1.0.4
dev: true
- /qs/6.5.3:
- resolution: {integrity: sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==}
+ /qs@6.11.2:
+ resolution: {integrity: sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==}
engines: {node: '>=0.6'}
+ dependencies:
+ side-channel: 1.0.4
dev: true
- /querystring-es3/0.2.1:
- resolution: {integrity: sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==}
- engines: {node: '>=0.4.x'}
+ /qs@6.5.3:
+ resolution: {integrity: sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==}
+ engines: {node: '>=0.6'}
dev: true
- /querystring/0.2.0:
- resolution: {integrity: sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==}
+ /querystring-es3@0.2.1:
+ resolution: {integrity: sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==}
engines: {node: '>=0.4.x'}
- deprecated: The querystring API is considered Legacy. new code should use the URLSearchParams API instead.
dev: true
- /querystring/0.2.1:
+ /querystring@0.2.1:
resolution: {integrity: sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg==}
engines: {node: '>=0.4.x'}
- deprecated: The querystring API is considered Legacy. new code should use the URLSearchParams API instead.
- dev: true
-
- /querystringify/2.2.0:
- resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
dev: true
- /queue-microtask/1.2.3:
+ /queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
- dev: true
- /raf/3.4.1:
- resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==}
+ /queue@6.0.2:
+ resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==}
dependencies:
- performance-now: 2.1.0
- dev: true
-
- /railroad-diagrams/1.0.0:
- resolution: {integrity: sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==}
+ inherits: 2.0.4
dev: true
- /ramda/0.28.0:
- resolution: {integrity: sha512-9QnLuG/kPVgWvMQ4aODhsBUFKOUmnbUnsSXACv+NCQZcHbeb+v8Lodp8OVxtRULN1/xOyYLLaL6npE6dMq5QTA==}
+ /quick-format-unescaped@4.0.4:
+ resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
dev: true
- /randexp/0.4.6:
- resolution: {integrity: sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==}
- engines: {node: '>=0.12'}
- dependencies:
- discontinuous-range: 1.0.0
- ret: 0.1.15
+ /quick-lru@5.1.1:
+ resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==}
+ engines: {node: '>=10'}
dev: true
- /randombytes/2.1.0:
+ /randombytes@2.1.0:
resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
dependencies:
safe-buffer: 5.2.1
dev: true
- /randomfill/1.0.4:
+ /randomfill@1.0.4:
resolution: {integrity: sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==}
dependencies:
randombytes: 2.1.0
safe-buffer: 5.2.1
dev: true
- /range-parser/1.2.1:
+ /range-parser@1.2.1:
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
engines: {node: '>= 0.6'}
dev: true
- /raw-body/2.5.1:
+ /raw-body@2.5.1:
resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==}
engines: {node: '>= 0.8'}
dependencies:
@@ -19645,7 +16545,7 @@ packages:
unpipe: 1.0.0
dev: true
- /raw-loader/4.0.2_webpack@4.46.0:
+ /raw-loader@4.0.2(webpack@4.46.0):
resolution: {integrity: sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA==}
engines: {node: '>= 10.13.0'}
peerDependencies:
@@ -19656,120 +16556,54 @@ packages:
webpack: 4.46.0
dev: true
- /rc/1.2.8:
+ /rc@1.2.8:
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
hasBin: true
+ requiresBuild: true
dependencies:
deep-extend: 0.6.0
ini: 1.3.8
- minimist: 1.2.7
+ minimist: 1.2.8
strip-json-comments: 2.0.1
- dev: true
- /react-dom/16.14.0_react@16.14.0:
- resolution: {integrity: sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==}
+ /react-dom@18.2.0(react@18.2.0):
+ resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==}
peerDependencies:
- react: ^16.14.0
+ react: ^18.2.0
dependencies:
loose-envify: 1.4.0
- object-assign: 4.1.1
- prop-types: 15.8.1
- react: 16.14.0
- scheduler: 0.19.1
- dev: true
-
- /react-inspector/5.1.1:
- resolution: {integrity: sha512-GURDaYzoLbW8pMGXwYPDBIv6nqei4kK7LPRZ9q9HCZF54wqXz/dnylBp/kfE9XmekBhHvLDdcYeyIwSrvtOiWg==}
- peerDependencies:
- react: ^16.8.4 || ^17.0.0
- dependencies:
- '@babel/runtime': 7.19.4
- is-dom: 1.1.0
- prop-types: 15.8.1
- dev: true
+ react: 18.2.0
+ scheduler: 0.23.0
+ dev: false
- /react-inspector/5.1.1_@preact+compat@17.1.2:
- resolution: {integrity: sha512-GURDaYzoLbW8pMGXwYPDBIv6nqei4kK7LPRZ9q9HCZF54wqXz/dnylBp/kfE9XmekBhHvLDdcYeyIwSrvtOiWg==}
- peerDependencies:
- react: ^16.8.4 || ^17.0.0
+ /react-html-attributes@1.4.6:
+ resolution: {integrity: sha512-uS3MmThNKFH2EZUQQw4k5pIcU7XIr208UE5dktrj/GOH1CMagqxDl4DCLpt3o2l9x+IB5nVYBeN3Cr4IutBXAg==}
dependencies:
- '@babel/runtime': 7.19.4
- is-dom: 1.1.0
- prop-types: 15.8.1
- react: /@preact/compat/17.1.2_preact@10.6.5
+ html-element-attributes: 1.3.1
dev: true
- /react-is/16.13.1:
+ /react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
dev: true
- /react-is/17.0.2:
- resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
- dev: true
-
- /react-refresh/0.10.0:
+ /react-refresh@0.10.0:
resolution: {integrity: sha512-PgidR3wST3dDYKr6b4pJoqQFpPGNKDSCDx4cZoshjXipw3LzO7mG1My2pwEzz2JVkF+inx3xRpDeQLFQGH/hsQ==}
engines: {node: '>=0.10.0'}
dev: true
- /react-sizeme/3.0.2:
- resolution: {integrity: sha512-xOIAOqqSSmKlKFJLO3inBQBdymzDuXx4iuwkNcJmC96jeiOg5ojByvL+g3MW9LPEsojLbC6pf68zOfobK8IPlw==}
- dependencies:
- element-resize-detector: 1.2.4
- invariant: 2.2.4
- shallowequal: 1.1.0
- throttle-debounce: 3.0.1
- dev: true
-
- /react/16.14.0:
- resolution: {integrity: sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==}
+ /react@18.2.0:
+ resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
engines: {node: '>=0.10.0'}
dependencies:
loose-envify: 1.4.0
- object-assign: 4.1.1
- prop-types: 15.8.1
- dev: true
-
- /read-pkg-up/1.0.1:
- resolution: {integrity: sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==}
- engines: {node: '>=0.10.0'}
- dependencies:
- find-up: 1.1.2
- read-pkg: 1.1.0
- dev: true
- optional: true
- /read-pkg-up/7.0.1:
- resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==}
- engines: {node: '>=8'}
- dependencies:
- find-up: 4.1.0
- read-pkg: 5.2.0
- type-fest: 0.8.1
- dev: true
-
- /read-pkg/1.1.0:
- resolution: {integrity: sha512-7BGwRHqt4s/uVbuyoeejRn4YmFnYZiFl4AuaeXHlgZf3sONF0SOGlxs2Pw8g6hCKupo08RafIO5YXFNOKTfwsQ==}
- engines: {node: '>=0.10.0'}
- dependencies:
- load-json-file: 1.1.0
- normalize-package-data: 2.5.0
- path-type: 1.1.0
- dev: true
- optional: true
-
- /read-pkg/5.2.0:
- resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==}
- engines: {node: '>=8'}
+ /read-cache@1.0.0:
+ resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
dependencies:
- '@types/normalize-package-data': 2.4.1
- normalize-package-data: 2.5.0
- parse-json: 5.2.0
- type-fest: 0.6.0
- dev: true
+ pify: 2.3.0
- /readable-stream/2.3.7:
- resolution: {integrity: sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==}
+ /readable-stream@2.3.8:
+ resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
dependencies:
core-util-is: 1.0.3
inherits: 2.0.4
@@ -19780,75 +16614,99 @@ packages:
util-deprecate: 1.0.2
dev: true
- /readable-stream/3.6.0:
+ /readable-stream@3.6.0:
resolution: {integrity: sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==}
engines: {node: '>= 6'}
+ requiresBuild: true
+ dependencies:
+ inherits: 2.0.4
+ string_decoder: 1.3.0
+ util-deprecate: 1.0.2
+
+ /readable-stream@3.6.2:
+ resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
+ engines: {node: '>= 6'}
dependencies:
inherits: 2.0.4
string_decoder: 1.3.0
util-deprecate: 1.0.2
+
+ /readable-stream@4.5.2:
+ resolution: {integrity: sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ dependencies:
+ abort-controller: 3.0.0
+ buffer: 6.0.3
+ events: 3.3.0
+ process: 0.11.10
+ string_decoder: 1.3.0
dev: true
- /readdirp/2.2.1:
+ /readdirp@2.2.1:
resolution: {integrity: sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==}
engines: {node: '>=0.10'}
+ requiresBuild: true
dependencies:
- graceful-fs: 4.2.10
+ graceful-fs: 4.2.11
micromatch: 3.1.10
- readable-stream: 2.3.7
+ readable-stream: 2.3.8
transitivePeerDependencies:
- supports-color
dev: true
optional: true
- /readdirp/3.6.0:
+ /readdirp@3.6.0:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'}
dependencies:
picomatch: 2.3.1
- dev: true
- /rechoir/0.6.2:
- resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==}
- engines: {node: '>= 0.10'}
- dependencies:
- resolve: 1.22.1
+ /real-require@0.2.0:
+ resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
+ engines: {node: '>= 12.13.0'}
dev: true
- /redent/1.0.0:
- resolution: {integrity: sha512-qtW5hKzGQZqKoh6JNSD+4lfitfPKGz42e6QwiRmPM5mmKtR0N41AbJRYu0xJi7nhOJ4WDgRkKvAk6tw4WIwR4g==}
- engines: {node: '>=0.10.0'}
+ /reflect.getprototypeof@1.0.4:
+ resolution: {integrity: sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw==}
+ engines: {node: '>= 0.4'}
dependencies:
- indent-string: 2.1.0
- strip-indent: 1.0.1
+ call-bind: 1.0.5
+ define-properties: 1.2.1
+ es-abstract: 1.22.3
+ get-intrinsic: 1.2.2
+ globalthis: 1.0.3
+ which-builtin-type: 1.1.3
dev: true
- optional: true
- /regenerate-unicode-properties/10.1.0:
+ /regenerate-unicode-properties@10.1.0:
resolution: {integrity: sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==}
engines: {node: '>=4'}
dependencies:
regenerate: 1.4.2
dev: true
- /regenerate/1.4.2:
+ /regenerate@1.4.2:
resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==}
dev: true
- /regenerator-runtime/0.11.1:
- resolution: {integrity: sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==}
+ /regenerator-runtime@0.13.10:
+ resolution: {integrity: sha512-KepLsg4dU12hryUO7bp/axHAKvwGOCV0sGloQtpagJ12ai+ojVDqkeGSiRX1zlq+kjIMZ1t7gpze+26QqtdGqw==}
+
+ /regenerator-runtime@0.13.11:
+ resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==}
dev: true
- /regenerator-runtime/0.13.10:
- resolution: {integrity: sha512-KepLsg4dU12hryUO7bp/axHAKvwGOCV0sGloQtpagJ12ai+ojVDqkeGSiRX1zlq+kjIMZ1t7gpze+26QqtdGqw==}
+ /regenerator-runtime@0.14.0:
+ resolution: {integrity: sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==}
+ dev: true
- /regenerator-transform/0.15.0:
- resolution: {integrity: sha512-LsrGtPmbYg19bcPHwdtmXwbW+TqNvtY4riE3P83foeHRroMbH6/2ddFBfab3t7kbzc7v7p4wbkIecHImqt0QNg==}
+ /regenerator-transform@0.15.2:
+ resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==}
dependencies:
- '@babel/runtime': 7.19.4
+ '@babel/runtime': 7.23.6
dev: true
- /regex-not/1.0.2:
+ /regex-not@1.0.2:
resolution: {integrity: sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==}
engines: {node: '>=0.10.0'}
dependencies:
@@ -19856,138 +16714,95 @@ packages:
safe-regex: 1.1.0
dev: true
- /regexp.prototype.flags/1.4.3:
- resolution: {integrity: sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==}
+ /regexp.prototype.flags@1.5.1:
+ resolution: {integrity: sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==}
engines: {node: '>= 0.4'}
dependencies:
- call-bind: 1.0.2
- define-properties: 1.1.4
- functions-have-names: 1.2.3
+ call-bind: 1.0.5
+ define-properties: 1.2.1
+ set-function-name: 2.0.1
dev: true
- /regexpp/3.2.0:
+ /regexpp@3.2.0:
resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==}
engines: {node: '>=8'}
dev: true
- /regexpu-core/5.2.1:
- resolution: {integrity: sha512-HrnlNtpvqP1Xkb28tMhBUO2EbyUHdQlsnlAhzWcwHy8WJR53UWr7/MAvqrsQKMbV4qdpv03oTMG8iIhfsPFktQ==}
+ /regexpu-core@5.3.2:
+ resolution: {integrity: sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==}
engines: {node: '>=4'}
dependencies:
+ '@babel/regjsgen': 0.8.0
regenerate: 1.4.2
regenerate-unicode-properties: 10.1.0
- regjsgen: 0.7.1
regjsparser: 0.9.1
unicode-match-property-ecmascript: 2.0.0
- unicode-match-property-value-ecmascript: 2.0.0
+ unicode-match-property-value-ecmascript: 2.1.0
dev: true
- /registry-auth-token/4.2.2:
+ /registry-auth-token@4.2.2:
resolution: {integrity: sha512-PC5ZysNb42zpFME6D/XlIgtNGdTl8bBOCw90xQLVMpzuuubJKYDWFAEuUNc+Cn8Z8724tg2SDhDRrkVEsqfDMg==}
engines: {node: '>=6.0.0'}
dependencies:
rc: 1.2.8
dev: true
- /registry-url/5.1.0:
+ /registry-auth-token@5.0.2:
+ resolution: {integrity: sha512-o/3ikDxtXaA59BmZuZrJZDJv8NMDGSj+6j6XaeBmHw8eY1i1qd9+6H+LjVvQXx3HN6aRCGa1cUdJ9RaJZUugnQ==}
+ engines: {node: '>=14'}
+ dependencies:
+ '@pnpm/npm-conf': 2.2.2
+ dev: true
+
+ /registry-url@5.1.0:
resolution: {integrity: sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==}
engines: {node: '>=8'}
dependencies:
rc: 1.2.8
dev: true
- /regjsgen/0.7.1:
- resolution: {integrity: sha512-RAt+8H2ZEzHeYWxZ3H2z6tF18zyyOnlcdaafLrm21Bguj7uZy6ULibiAFdXEtKQY4Sy7wDTwDiOazasMLc4KPA==}
+ /registry-url@6.0.1:
+ resolution: {integrity: sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==}
+ engines: {node: '>=12'}
+ dependencies:
+ rc: 1.2.8
dev: true
- /regjsparser/0.9.1:
+ /regjsparser@0.9.1:
resolution: {integrity: sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==}
hasBin: true
dependencies:
jsesc: 0.5.0
dev: true
- /relateurl/0.2.7:
+ /relateurl@0.2.7:
resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==}
engines: {node: '>= 0.10'}
dev: true
- /release-zalgo/1.0.0:
- resolution: {integrity: sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA==}
- engines: {node: '>=4'}
- dependencies:
- es6-error: 4.1.1
- dev: true
-
- /remark-external-links/8.0.0:
- resolution: {integrity: sha512-5vPSX0kHoSsqtdftSHhIYofVINC8qmp0nctkeU9YoJwV3YfiBRiI6cbFRJ0oI/1F9xS+bopXG0m2KS8VFscuKA==}
- dependencies:
- extend: 3.0.2
- is-absolute-url: 3.0.3
- mdast-util-definitions: 4.0.0
- space-separated-tokens: 1.1.5
- unist-util-visit: 2.0.3
- dev: true
-
- /remark-footnotes/2.0.0:
- resolution: {integrity: sha512-3Clt8ZMH75Ayjp9q4CorNeyjwIxHFcTkaektplKGl2A1jNGEUey8cKL0ZC5vJwfcD5GFGsNLImLG/NGzWIzoMQ==}
- dev: true
-
- /remark-mdx/1.6.22:
- resolution: {integrity: sha512-phMHBJgeV76uyFkH4rvzCftLfKCr2RZuF+/gmVcaKrpsihyzmhXjA0BEMDaPTXG5y8qZOKPVo83NAOX01LPnOQ==}
- dependencies:
- '@babel/core': 7.12.9
- '@babel/helper-plugin-utils': 7.10.4
- '@babel/plugin-proposal-object-rest-spread': 7.12.1_@babel+core@7.12.9
- '@babel/plugin-syntax-jsx': 7.12.1_@babel+core@7.12.9
- '@mdx-js/util': 1.6.22
- is-alphabetical: 1.0.4
- remark-parse: 8.0.3
- unified: 9.2.0
- transitivePeerDependencies:
- - supports-color
- dev: true
-
- /remark-parse/8.0.3:
- resolution: {integrity: sha512-E1K9+QLGgggHxCQtLt++uXltxEprmWzNfg+MxpfHsZlrddKzZ/hZyWHDbK3/Ap8HJQqYJRXP+jHczdL6q6i85Q==}
- dependencies:
- ccount: 1.1.0
- collapse-white-space: 1.0.6
- is-alphabetical: 1.0.4
- is-decimal: 1.0.4
- is-whitespace-character: 1.0.4
- is-word-character: 1.0.4
- markdown-escapes: 1.0.4
- parse-entities: 2.0.0
- repeat-string: 1.6.1
- state-toggle: 1.0.3
- trim: 0.0.1
- trim-trailing-lines: 1.1.4
- unherit: 1.1.3
- unist-util-remove-position: 2.0.1
- vfile-location: 3.2.0
- xtend: 4.0.2
- dev: true
-
- /remark-slug/6.1.0:
- resolution: {integrity: sha512-oGCxDF9deA8phWvxFuyr3oSJsdyUAxMFbA0mZ7Y1Sas+emILtO+e5WutF9564gDsEN4IXaQXm5pFo6MLH+YmwQ==}
+ /relaxed-json@1.0.3:
+ resolution: {integrity: sha512-b7wGPo7o2KE/g7SqkJDDbav6zmrEeP4TK2VpITU72J/M949TLe/23y/ZHJo+pskcGM52xIfFoT9hydwmgr1AEg==}
+ engines: {node: '>= 0.10.0'}
+ hasBin: true
dependencies:
- github-slugger: 1.5.0
- mdast-util-to-string: 1.1.0
- unist-util-visit: 2.0.3
+ chalk: 2.4.2
+ commander: 2.20.3
dev: true
- /remark-squeeze-paragraphs/4.0.0:
- resolution: {integrity: sha512-8qRqmL9F4nuLPIgl92XUuxI3pFxize+F1H0e/W3llTk0UsjJaj01+RrirkMw7P21RKe4X6goQhYRSvNWX+70Rw==}
+ /release-zalgo@1.0.0:
+ resolution: {integrity: sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA==}
+ engines: {node: '>=4'}
dependencies:
- mdast-squeeze-paragraphs: 4.0.0
+ es6-error: 4.1.1
dev: true
- /remove-trailing-separator/1.1.0:
+ /remove-trailing-separator@1.1.0:
resolution: {integrity: sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==}
+ requiresBuild: true
dev: true
+ optional: true
- /renderkid/2.0.7:
+ /renderkid@2.0.7:
resolution: {integrity: sha512-oCcFyxaMrKsKcTY59qnCAtmDVSLfPbrv6A3tVbPdFMMrv5jaK10V6m40cKsoPNhAqN6rmHW9sswW4o3ruSrwUQ==}
dependencies:
css-select: 4.3.0
@@ -19997,25 +16812,27 @@ packages:
strip-ansi: 3.0.1
dev: true
- /repeat-element/1.1.4:
+ /renderkid@3.0.0:
+ resolution: {integrity: sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==}
+ dependencies:
+ css-select: 4.3.0
+ dom-converter: 0.2.0
+ htmlparser2: 6.1.0
+ lodash: 4.17.21
+ strip-ansi: 6.0.1
+ dev: true
+
+ /repeat-element@1.1.4:
resolution: {integrity: sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==}
engines: {node: '>=0.10.0'}
dev: true
- /repeat-string/1.6.1:
+ /repeat-string@1.6.1:
resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==}
engines: {node: '>=0.10'}
dev: true
- /repeating/2.0.1:
- resolution: {integrity: sha512-ZqtSMuVybkISo2OWvqvm7iHSWngvdaW3IpsT9/uP8v4gMi591LY6h35wdOfvQdWCKFWZWm2Y1Opp4kV7vQKT6A==}
- engines: {node: '>=0.10.0'}
- dependencies:
- is-finite: 1.1.0
- dev: true
- optional: true
-
- /request-promise-core/1.1.4_request@2.88.2:
+ /request-promise-core@1.1.4(request@2.88.2):
resolution: {integrity: sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==}
engines: {node: '>=0.10.0'}
peerDependencies:
@@ -20025,23 +16842,21 @@ packages:
request: 2.88.2
dev: true
- /request-promise-native/1.0.9_request@2.88.2:
+ /request-promise-native@1.0.9(request@2.88.2):
resolution: {integrity: sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==}
engines: {node: '>=0.12.0'}
- deprecated: request-promise-native has been deprecated because it extends the now deprecated request package, see https://github.com/request/request/issues/3142
peerDependencies:
request: ^2.34
dependencies:
request: 2.88.2
- request-promise-core: 1.1.4_request@2.88.2
+ request-promise-core: 1.1.4(request@2.88.2)
stealthy-require: 1.1.1
tough-cookie: 2.5.0
dev: true
- /request/2.88.2:
+ /request@2.88.2:
resolution: {integrity: sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==}
engines: {node: '>= 6'}
- deprecated: request has been deprecated, see https://github.com/request/request/issues/3142
dependencies:
aws-sign2: 0.7.0
aws4: 1.11.0
@@ -20065,80 +16880,98 @@ packages:
uuid: 3.4.0
dev: true
- /require-directory/2.1.1:
+ /require-directory@2.1.1:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'}
dev: true
- /require-from-string/2.0.2:
+ /require-from-string@2.0.2:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
engines: {node: '>=0.10.0'}
dev: true
- /require-main-filename/2.0.0:
+ /require-main-filename@2.0.0:
resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
dev: true
- /requires-port/1.0.0:
+ /requires-port@1.0.0:
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
dev: true
- /resolve-cwd/3.0.0:
+ /resolve-alpn@1.2.1:
+ resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==}
+ dev: true
+
+ /resolve-cwd@3.0.0:
resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==}
engines: {node: '>=8'}
dependencies:
resolve-from: 5.0.0
dev: true
- /resolve-from/3.0.0:
+ /resolve-from@3.0.0:
resolution: {integrity: sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==}
engines: {node: '>=4'}
dev: true
- /resolve-from/4.0.0:
+ /resolve-from@4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'}
dev: true
- /resolve-from/5.0.0:
+ /resolve-from@5.0.0:
resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==}
engines: {node: '>=8'}
dev: true
- /resolve-pathname/3.0.0:
+ /resolve-pathname@3.0.0:
resolution: {integrity: sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==}
dev: false
- /resolve-url/0.2.1:
+ /resolve-url@0.2.1:
resolution: {integrity: sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==}
deprecated: https://github.com/lydell/resolve-url#deprecated
dev: true
- /resolve/1.22.1:
- resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==}
+ /resolve@1.22.2:
+ resolution: {integrity: sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==}
hasBin: true
dependencies:
- is-core-module: 2.11.0
+ is-core-module: 2.13.1
path-parse: 1.0.7
supports-preserve-symlinks-flag: 1.0.0
- dev: true
- /resolve/2.0.0-next.4:
- resolution: {integrity: sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==}
+ /resolve@1.22.8:
+ resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==}
hasBin: true
dependencies:
- is-core-module: 2.11.0
+ is-core-module: 2.13.1
+ path-parse: 1.0.7
+ supports-preserve-symlinks-flag: 1.0.0
+
+ /resolve@2.0.0-next.5:
+ resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==}
+ hasBin: true
+ dependencies:
+ is-core-module: 2.13.1
path-parse: 1.0.7
supports-preserve-symlinks-flag: 1.0.0
dev: true
- /responselike/1.0.2:
+ /responselike@1.0.2:
resolution: {integrity: sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==}
dependencies:
lowercase-keys: 1.0.1
dev: true
- /restore-cursor/3.1.0:
+ /responselike@3.0.0:
+ resolution: {integrity: sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==}
+ engines: {node: '>=14.16'}
+ dependencies:
+ lowercase-keys: 3.0.0
+ dev: true
+
+ /restore-cursor@3.1.0:
resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==}
engines: {node: '>=8'}
dependencies:
@@ -20146,376 +16979,279 @@ packages:
signal-exit: 3.0.7
dev: true
- /ret/0.1.15:
+ /ret@0.1.15:
resolution: {integrity: sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==}
engines: {node: '>=0.12'}
dev: true
- /retry/0.13.1:
+ /retry@0.13.1:
resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==}
engines: {node: '>= 4'}
dev: true
- /reusify/1.0.4:
+ /reusify@1.0.4:
resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
- dev: true
- /rgb-regex/1.0.1:
+ /rgb-regex@1.0.1:
resolution: {integrity: sha512-gDK5mkALDFER2YLqH6imYvK6g02gpNGM4ILDZ472EwWfXZnC2ZEpoB2ECXTyOVUKuk/bPJZMzwQPBYICzP+D3w==}
dev: true
- /rgba-regex/1.0.0:
+ /rgba-regex@1.0.0:
resolution: {integrity: sha512-zgn5OjNQXLUTdq8m17KdaicF6w89TZs8ZU8y0AYENIU6wG8GG6LLm0yLSiPY8DmaYmHdgRW8rnApjoT0fQRfMg==}
dev: true
- /rimraf/2.7.1:
+ /rimraf@2.4.5:
+ resolution: {integrity: sha512-J5xnxTyqaiw06JjMftq7L9ouA448dw/E7dKghkP9WpKNuwmARNNg+Gk8/u5ryb9N/Yo2+z3MCwuqFK/+qPOPfQ==}
+ hasBin: true
+ requiresBuild: true
+ dependencies:
+ glob: 6.0.4
+ dev: true
+ optional: true
+
+ /rimraf@2.7.1:
resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==}
hasBin: true
dependencies:
glob: 7.2.3
dev: true
- /rimraf/3.0.2:
+ /rimraf@3.0.2:
resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
- hasBin: true
dependencies:
glob: 7.2.3
dev: true
- /ripemd160/2.0.2:
+ /ripemd160@2.0.2:
resolution: {integrity: sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==}
dependencies:
hash-base: 3.1.0
inherits: 2.0.4
dev: true
- /rollup-plugin-bundle-html/0.2.2:
- resolution: {integrity: sha512-nK4Z/k3MVjfCcnC5T15ksHw3JyRJx110oduy3VBW0ki2qI0tu4pLlgXyltBgtd+gpiFCPqEnfy89XRPG+eCOwA==}
- dependencies:
- cheerio: 1.0.0-rc.12
- hasha: 4.0.1
- dev: true
-
- /rollup-plugin-css-only/3.1.0_rollup@2.79.1:
- resolution: {integrity: sha512-TYMOE5uoD76vpj+RTkQLzC9cQtbnJNktHPB507FzRWBVaofg7KhIqq1kGbcVOadARSozWF883Ho9KpSPKH8gqA==}
- engines: {node: '>=10.12.0'}
- peerDependencies:
- rollup: 1 || 2
- dependencies:
- '@rollup/pluginutils': 4.2.1
- rollup: 2.79.1
- dev: true
-
- /rollup-plugin-sourcemaps/0.6.3_cxvqwjbwwnthagk6mnrmqfhepa:
- resolution: {integrity: sha512-paFu+nT1xvuO1tPFYXGe+XnQvg4Hjqv/eIhG8i5EspfYYPBKL57X7iVbfv55aNVASg3dzWvES9dmWsL2KhfByw==}
- engines: {node: '>=10.0.0'}
- peerDependencies:
- '@types/node': '>=10.0.0'
- rollup: '>=0.31.2'
- peerDependenciesMeta:
- '@types/node':
- optional: true
- dependencies:
- '@rollup/pluginutils': 3.1.0_rollup@2.79.1
- '@types/node': 18.11.5
- rollup: 2.79.1
- source-map-resolve: 0.6.0
- dev: true
-
- /rollup-plugin-terser/7.0.2_rollup@2.79.1:
+ /rollup-plugin-terser@7.0.2(rollup@2.79.1):
resolution: {integrity: sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==}
peerDependencies:
rollup: ^2.0.0
dependencies:
- '@babel/code-frame': 7.18.6
+ '@babel/code-frame': 7.23.5
jest-worker: 26.6.2
rollup: 2.79.1
serialize-javascript: 4.0.0
terser: 5.15.1
dev: true
- /rollup/2.79.1:
+ /rollup@2.79.1:
resolution: {integrity: sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==}
engines: {node: '>=10.0.0'}
- hasBin: true
optionalDependencies:
- fsevents: 2.3.2
+ fsevents: 2.3.3
dev: true
- /rst-selector-parser/2.2.3:
- resolution: {integrity: sha512-nDG1rZeP6oFTLN6yNDV/uiAvs1+FS/KlrEwh7+y7dpuApDBy6bI2HTBcc0/V8lv9OTqfyD34eF7au2pm8aBbhA==}
- dependencies:
- lodash.flattendeep: 4.4.0
- nearley: 2.20.1
- dev: true
-
- /rsvp/4.8.5:
- resolution: {integrity: sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==}
- engines: {node: 6.* || >= 7.*}
- dev: true
-
- /run-parallel/1.2.0:
+ /run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
dependencies:
queue-microtask: 1.2.3
- dev: true
- /run-queue/1.0.3:
+ /run-queue@1.0.3:
resolution: {integrity: sha512-ntymy489o0/QQplUDnpYAYUsO50K9SBrIVaKCWDOJzYJts0f9WH9RFJkyagebkw5+y1oi00R7ynNW/d12GBumg==}
dependencies:
aproba: 1.2.0
dev: true
- /sade/1.8.1:
+ /sade@1.8.1:
resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==}
engines: {node: '>=6'}
dependencies:
mri: 1.2.0
dev: true
- /safe-buffer/5.1.1:
- resolution: {integrity: sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==}
+ /safe-array-concat@1.0.1:
+ resolution: {integrity: sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==}
+ engines: {node: '>=0.4'}
+ dependencies:
+ call-bind: 1.0.5
+ get-intrinsic: 1.2.2
+ has-symbols: 1.0.3
+ isarray: 2.0.5
dev: true
- /safe-buffer/5.1.2:
+ /safe-buffer@5.1.2:
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
dev: true
- /safe-buffer/5.2.1:
+ /safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
+ requiresBuild: true
+
+ /safe-json-stringify@1.2.0:
+ resolution: {integrity: sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==}
+ requiresBuild: true
dev: true
+ optional: true
- /safe-regex-test/1.0.0:
+ /safe-regex-test@1.0.0:
resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==}
dependencies:
- call-bind: 1.0.2
- get-intrinsic: 1.1.3
+ call-bind: 1.0.5
+ get-intrinsic: 1.2.2
is-regex: 1.1.4
dev: true
- /safe-regex/1.1.0:
+ /safe-regex@1.1.0:
resolution: {integrity: sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==}
dependencies:
ret: 0.1.15
dev: true
- /safer-buffer/2.1.2:
- resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
- dev: true
-
- /sane/4.1.0:
- resolution: {integrity: sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA==}
- engines: {node: 6.* || 8.* || >= 10.*}
- deprecated: some dependency vulnerabilities fixed, support for node < 10 dropped, and newer ECMAScript syntax/features added
- hasBin: true
- dependencies:
- '@cnakazawa/watch': 1.0.4
- anymatch: 2.0.0
- capture-exit: 2.0.0
- exec-sh: 0.3.6
- execa: 1.0.0
- fb-watchman: 2.0.2
- micromatch: 3.1.10
- minimist: 1.2.7
- walker: 1.0.8
- transitivePeerDependencies:
- - supports-color
- dev: true
-
- /sass-loader/10.1.1_sass@1.55.0:
- resolution: {integrity: sha512-W6gVDXAd5hR/WHsPicvZdjAWHBcEJ44UahgxcIE196fW2ong0ZHMPO1kZuI5q0VlvMQZh32gpv69PLWQm70qrw==}
- engines: {node: '>= 10.13.0'}
- peerDependencies:
- fibers: '>= 3.1.0'
- node-sass: ^4.0.0 || ^5.0.0
- sass: ^1.3.0
- webpack: ^4.36.0 || ^5.0.0
- peerDependenciesMeta:
- fibers:
- optional: true
- node-sass:
- optional: true
- sass:
- optional: true
- dependencies:
- klona: 2.0.5
- loader-utils: 2.0.3
- neo-async: 2.6.2
- sass: 1.55.0
- schema-utils: 3.1.1
- semver: 7.3.8
+ /safe-stable-stringify@2.4.3:
+ resolution: {integrity: sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==}
+ engines: {node: '>=10'}
dev: true
- /sass/1.55.0:
- resolution: {integrity: sha512-Pk+PMy7OGLs9WaxZGJMn7S96dvlyVBwwtToX895WmCpAOr5YiJYEUJfiJidMuKb613z2xNWcXCHEuOvjZbqC6A==}
- engines: {node: '>=12.0.0'}
- hasBin: true
- dependencies:
- chokidar: 3.5.3
- immutable: 4.1.0
- source-map-js: 1.0.2
+ /safer-buffer@2.1.2:
+ resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
dev: true
- /sass/1.56.1:
+ /sass@1.56.1:
resolution: {integrity: sha512-VpEyKpyBPCxE7qGDtOcdJ6fFbcpOM+Emu7uZLxVrkX8KVU/Dp5UF7WLvzqRuUhB6mqqQt1xffLoG+AndxTZrCQ==}
engines: {node: '>=12.0.0'}
- hasBin: true
dependencies:
chokidar: 3.5.3
immutable: 4.1.0
source-map-js: 1.0.2
dev: true
- /sax/1.2.4:
+ /sax@1.2.4:
resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==}
dev: true
- /saxes/3.1.11:
+ /saxes@3.1.11:
resolution: {integrity: sha512-Ydydq3zC+WYDJK1+gRxRapLIED9PWeSuuS41wqyoRmzvhhh9nc+QQrVMKJYzJFULazeGhzSV0QleN2wD3boh2g==}
engines: {node: '>=8'}
dependencies:
xmlchars: 2.2.0
dev: true
- /saxes/5.0.1:
- resolution: {integrity: sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==}
- engines: {node: '>=10'}
- dependencies:
- xmlchars: 2.2.0
- dev: true
-
- /scheduler/0.19.1:
- resolution: {integrity: sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==}
+ /scheduler@0.23.0:
+ resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==}
dependencies:
loose-envify: 1.4.0
- object-assign: 4.1.1
- dev: true
+ dev: false
- /schema-utils/0.4.7:
+ /schema-utils@0.4.7:
resolution: {integrity: sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ==}
engines: {node: '>= 4'}
dependencies:
ajv: 6.12.6
- ajv-keywords: 3.5.2_ajv@6.12.6
+ ajv-keywords: 3.5.2(ajv@6.12.6)
dev: true
- /schema-utils/1.0.0:
+ /schema-utils@1.0.0:
resolution: {integrity: sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==}
engines: {node: '>= 4'}
dependencies:
ajv: 6.12.6
- ajv-errors: 1.0.1_ajv@6.12.6
- ajv-keywords: 3.5.2_ajv@6.12.6
- dev: true
-
- /schema-utils/2.7.0:
- resolution: {integrity: sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==}
- engines: {node: '>= 8.9.0'}
- dependencies:
- '@types/json-schema': 7.0.11
- ajv: 6.12.6
- ajv-keywords: 3.5.2_ajv@6.12.6
+ ajv-errors: 1.0.1(ajv@6.12.6)
+ ajv-keywords: 3.5.2(ajv@6.12.6)
dev: true
- /schema-utils/2.7.1:
+ /schema-utils@2.7.1:
resolution: {integrity: sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==}
engines: {node: '>= 8.9.0'}
dependencies:
'@types/json-schema': 7.0.11
ajv: 6.12.6
- ajv-keywords: 3.5.2_ajv@6.12.6
+ ajv-keywords: 3.5.2(ajv@6.12.6)
dev: true
- /schema-utils/3.1.1:
+ /schema-utils@3.1.1:
resolution: {integrity: sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==}
engines: {node: '>= 10.13.0'}
dependencies:
- '@types/json-schema': 7.0.11
+ '@types/json-schema': 7.0.15
ajv: 6.12.6
- ajv-keywords: 3.5.2_ajv@6.12.6
+ ajv-keywords: 3.5.2(ajv@6.12.6)
dev: true
- /schema-utils/4.0.0:
+ /schema-utils@4.0.0:
resolution: {integrity: sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==}
engines: {node: '>= 12.13.0'}
dependencies:
- '@types/json-schema': 7.0.11
+ '@types/json-schema': 7.0.15
ajv: 8.11.0
- ajv-formats: 2.1.1
- ajv-keywords: 5.1.0_ajv@8.11.0
+ ajv-formats: 2.1.1(ajv@8.11.0)
+ ajv-keywords: 5.1.0(ajv@8.11.0)
dev: true
- /script-ext-html-webpack-plugin/2.1.5:
- resolution: {integrity: sha512-nMjd5dtsnoB8dS+pVM9ZL4mC9O1uVtTxrDS99OGZsZxFbkZE6pw0HCMued/cncDrKivIShO9vwoyOTvsGqQHEQ==}
- engines: {node: '>=6.11.5'}
- peerDependencies:
- html-webpack-plugin: ^3.0.0 || ^4.0.0
- webpack: ^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0
- dependencies:
- debug: 4.3.4
- transitivePeerDependencies:
- - supports-color
- dev: true
-
- /select-hose/2.0.0:
+ /select-hose@2.0.0:
resolution: {integrity: sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==}
dev: true
- /selfsigned/2.1.1:
+ /selfsigned@2.1.1:
resolution: {integrity: sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ==}
engines: {node: '>=10'}
dependencies:
node-forge: 1.3.1
dev: true
- /semiver/1.1.0:
+ /semiver@1.1.0:
resolution: {integrity: sha512-QNI2ChmuioGC1/xjyYwyZYADILWyW6AmS1UH6gDj/SFUUUS4MBAWs/7mxnkRPc/F4iHezDP+O8t0dO8WHiEOdg==}
engines: {node: '>=6'}
dev: true
- /semver-diff/3.1.1:
+ /semver-diff@3.1.1:
resolution: {integrity: sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==}
engines: {node: '>=8'}
dependencies:
- semver: 6.3.0
+ semver: 6.3.1
dev: true
- /semver/5.7.1:
- resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==}
+ /semver-diff@4.0.0:
+ resolution: {integrity: sha512-0Ju4+6A8iOnpL/Thra7dZsSlOHYAHIeMxfhWQRI1/VLcT3WDBZKKtQt/QkBOsiIN9ZpuvHE6cGZ0x4glCMmfiA==}
+ engines: {node: '>=12'}
+ dependencies:
+ semver: 7.5.4
+ dev: true
+
+ /semver@5.7.2:
+ resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==}
hasBin: true
dev: true
- /semver/6.3.0:
+ /semver@6.3.0:
resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==}
+
+ /semver@6.3.1:
+ resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
hasBin: true
- dev: true
- /semver/7.3.4:
- resolution: {integrity: sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==}
+ /semver@7.3.5:
+ resolution: {integrity: sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==}
engines: {node: '>=10'}
hasBin: true
dependencies:
lru-cache: 6.0.0
dev: true
- /semver/7.3.5:
- resolution: {integrity: sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==}
+ /semver@7.3.8:
+ resolution: {integrity: sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==}
engines: {node: '>=10'}
hasBin: true
dependencies:
lru-cache: 6.0.0
dev: true
- /semver/7.3.8:
- resolution: {integrity: sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==}
+ /semver@7.5.4:
+ resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==}
engines: {node: '>=10'}
hasBin: true
+ requiresBuild: true
dependencies:
lru-cache: 6.0.0
- dev: true
- /send/0.18.0:
+ /send@0.18.0:
resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==}
engines: {node: '>= 0.8.0'}
dependencies:
@@ -20536,43 +17272,32 @@ packages:
- supports-color
dev: true
- /serialize-error/7.0.1:
+ /serialize-error@7.0.1:
resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==}
engines: {node: '>=10'}
dependencies:
type-fest: 0.13.1
dev: true
- /serialize-javascript/4.0.0:
+ /serialize-javascript@4.0.0:
resolution: {integrity: sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==}
dependencies:
randombytes: 2.1.0
dev: true
- /serialize-javascript/5.0.1:
+ /serialize-javascript@5.0.1:
resolution: {integrity: sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==}
dependencies:
randombytes: 2.1.0
dev: true
- /serialize-javascript/6.0.0:
+ /serialize-javascript@6.0.0:
resolution: {integrity: sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==}
dependencies:
randombytes: 2.1.0
dev: true
- /serve-favicon/2.5.0:
- resolution: {integrity: sha512-FMW2RvqNr03x+C0WxTyu6sOv21oOjkq5j8tjquWccwa6ScNyGFOGJVpuS1NmTVGBAHS07xnSKotgf2ehQmf9iA==}
- engines: {node: '>= 0.8.0'}
- dependencies:
- etag: 1.8.1
- fresh: 0.5.2
- ms: 2.1.1
- parseurl: 1.3.3
- safe-buffer: 5.1.1
- dev: true
-
- /serve-index/1.9.1:
+ /serve-index@1.9.1:
resolution: {integrity: sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==}
engines: {node: '>= 0.8.0'}
dependencies:
@@ -20587,7 +17312,7 @@ packages:
- supports-color
dev: true
- /serve-static/1.15.0:
+ /serve-static@1.15.0:
resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==}
engines: {node: '>= 0.8.0'}
dependencies:
@@ -20599,11 +17324,30 @@ packages:
- supports-color
dev: true
- /set-blocking/2.0.0:
+ /set-blocking@2.0.0:
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
dev: true
- /set-value/2.0.1:
+ /set-function-length@1.1.1:
+ resolution: {integrity: sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ define-data-property: 1.1.1
+ get-intrinsic: 1.2.2
+ gopd: 1.0.1
+ has-property-descriptors: 1.0.1
+ dev: true
+
+ /set-function-name@2.0.1:
+ resolution: {integrity: sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ define-data-property: 1.1.1
+ functions-have-names: 1.2.3
+ has-property-descriptors: 1.0.1
+ dev: true
+
+ /set-value@2.0.1:
resolution: {integrity: sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==}
engines: {node: '>=0.10.0'}
dependencies:
@@ -20613,19 +17357,19 @@ packages:
split-string: 3.1.0
dev: true
- /setimmediate/1.0.5:
+ /setimmediate@1.0.5:
resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
dev: true
- /setprototypeof/1.1.0:
+ /setprototypeof@1.1.0:
resolution: {integrity: sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==}
dev: true
- /setprototypeof/1.2.0:
+ /setprototypeof@1.2.0:
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
dev: true
- /sha.js/2.4.11:
+ /sha.js@2.4.11:
resolution: {integrity: sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==}
hasBin: true
dependencies:
@@ -20633,94 +17377,109 @@ packages:
safe-buffer: 5.2.1
dev: true
- /shallow-clone/3.0.1:
+ /shallow-clone@3.0.1:
resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==}
engines: {node: '>=8'}
dependencies:
kind-of: 6.0.3
dev: true
- /shallowequal/1.1.0:
- resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==}
- dev: true
-
- /shebang-command/1.2.0:
+ /shebang-command@1.2.0:
resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==}
engines: {node: '>=0.10.0'}
dependencies:
shebang-regex: 1.0.0
dev: true
- /shebang-command/2.0.0:
+ /shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
dependencies:
shebang-regex: 3.0.0
- dev: true
- /shebang-regex/1.0.0:
+ /shebang-regex@1.0.0:
resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==}
engines: {node: '>=0.10.0'}
dev: true
- /shebang-regex/3.0.0:
+ /shebang-regex@3.0.0:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
- dev: true
- /shelljs/0.8.5:
- resolution: {integrity: sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==}
- engines: {node: '>=4'}
- hasBin: true
- dependencies:
- glob: 7.2.3
- interpret: 1.4.0
- rechoir: 0.6.2
+ /shell-quote@1.7.3:
+ resolution: {integrity: sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==}
dev: true
- /shellwords/0.1.1:
+ /shellwords@0.1.1:
resolution: {integrity: sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==}
dev: true
- optional: true
- /shiki/0.11.1:
- resolution: {integrity: sha512-EugY9VASFuDqOexOgXR18ZV+TbFrQHeCpEYaXamO+SZlsnT/2LxuLBX25GGtIrwaEVFXUAbUQ601SWE2rMwWHA==}
+ /shiki@0.14.6:
+ resolution: {integrity: sha512-R4koBBlQP33cC8cpzX0hAoOURBHJILp4Aaduh2eYi+Vj8ZBqtK/5SWNEHBS3qwUMu8dqOtI/ftno3ESfNeVW9g==}
dependencies:
+ ansi-sequence-parser: 1.1.1
jsonc-parser: 3.2.0
- vscode-oniguruma: 1.6.2
- vscode-textmate: 6.0.0
+ vscode-oniguruma: 1.7.0
+ vscode-textmate: 8.0.0
dev: true
- /shiki/0.9.15:
- resolution: {integrity: sha512-/Y0z9IzhJ8nD9nbceORCqu6NgT9X6I8Fk8c3SICHI5NbZRLdZYFaB233gwct9sU0vvSypyaL/qaKvzyQGJBZSw==}
+ /side-channel@1.0.4:
+ resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==}
dependencies:
- jsonc-parser: 3.2.0
- vscode-oniguruma: 1.6.2
- vscode-textmate: 5.2.0
+ call-bind: 1.0.5
+ get-intrinsic: 1.2.2
+ object-inspect: 1.13.1
dev: true
- /side-channel/1.0.4:
- resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==}
+ /sign-addon@5.3.0:
+ resolution: {integrity: sha512-7nHlCzhQgVMLBNiXVEgbG/raq48awOW0lYMN5uo1BaB3mp0+k8M8pvDwbfTlr3apcxZJsk9HQsAW1POwoJugpQ==}
+ deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
dependencies:
- call-bind: 1.0.2
- get-intrinsic: 1.1.3
- object-inspect: 1.12.2
+ common-tags: 1.8.2
+ core-js: 3.29.0
+ deepcopy: 2.1.0
+ es6-error: 4.1.1
+ es6-promisify: 7.0.0
+ jsonwebtoken: 9.0.0
+ mz: 2.7.0
+ request: 2.88.2
+ source-map-support: 0.5.21
+ stream-to-promise: 3.0.0
dev: true
- /signal-exit/3.0.7:
+ /signal-exit@3.0.7:
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
dev: true
- /simple-swizzle/0.2.2:
+ /signal-exit@4.1.0:
+ resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
+ engines: {node: '>=14'}
+
+ /simple-concat@1.0.1:
+ resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==}
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /simple-get@4.0.1:
+ resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
+ requiresBuild: true
+ dependencies:
+ decompress-response: 6.0.0
+ once: 1.4.0
+ simple-concat: 1.0.1
+ dev: false
+ optional: true
+
+ /simple-swizzle@0.2.2:
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
dependencies:
is-arrayish: 0.3.2
dev: true
- /sirv-cli/1.0.14:
+ /sirv-cli@1.0.14:
resolution: {integrity: sha512-yyUTNr984ANKDloqepkYbBSqvx3buwYg2sQKPWjSU+IBia5loaoka2If8N9CMwt8AfP179cdEl7kYJ//iWJHjQ==}
engines: {node: '>= 10'}
- hasBin: true
dependencies:
console-clear: 1.1.1
get-port: 3.2.0
@@ -20732,7 +17491,7 @@ packages:
tinydate: 1.3.0
dev: true
- /sirv/1.0.19:
+ /sirv@1.0.19:
resolution: {integrity: sha512-JuLThK3TnZG1TAKDwNIqNq6QA2afLOCcm+iE8D1Kj3GA40pSPsxQjjJl0J8X3tsR7T+CP1GavpzLwYkgVLWrZQ==}
engines: {node: '>= 10'}
dependencies:
@@ -20741,11 +17500,11 @@ packages:
totalist: 1.1.0
dev: true
- /sisteransi/1.0.5:
+ /sisteransi@1.0.5:
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
dev: true
- /size-plugin/3.0.0_webpack@4.46.0:
+ /size-plugin@3.0.0(webpack@4.46.0):
resolution: {integrity: sha512-RPMSkgbvmS1e5XUwPNFZre7DLqcK9MhWARIm8UmGLgbUCAs173JLI6DPHco68wvo0cUdft8+GGRaJtNl5RWfew==}
peerDependencies:
webpack: '*'
@@ -20763,27 +17522,32 @@ packages:
- debug
dev: true
- /slash/1.0.0:
+ /slash@1.0.0:
resolution: {integrity: sha512-3TYDR7xWt4dIqV2JauJr+EJeW356RXijHeUlO+8djJ+uBXPn8/2dpzBc8yQhh583sVvc9CvFAeQVgijsH+PNNg==}
engines: {node: '>=0.10.0'}
dev: true
- /slash/2.0.0:
- resolution: {integrity: sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==}
- engines: {node: '>=6'}
- dev: true
-
- /slash/3.0.0:
+ /slash@3.0.0:
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
engines: {node: '>=8'}
dev: true
- /slash/4.0.0:
+ /slash@4.0.0:
resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==}
engines: {node: '>=12'}
dev: true
- /slice-ansi/4.0.0:
+ /slash@5.0.1:
+ resolution: {integrity: sha512-ywNzUOiXwetmLvTUiCBZpLi+vxqN3i+zDqjs2HHfUSV3wN4UJxVVKWrS1JZDeiJIeBFNgB5pmioC2g0IUTL+rQ==}
+ engines: {node: '>=14.16'}
+ dev: true
+
+ /slash@5.1.0:
+ resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==}
+ engines: {node: '>=14.16'}
+ dev: true
+
+ /slice-ansi@4.0.0:
resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==}
engines: {node: '>=10'}
dependencies:
@@ -20792,7 +17556,7 @@ packages:
is-fullwidth-code-point: 3.0.0
dev: true
- /slice-ansi/5.0.0:
+ /slice-ansi@5.0.0:
resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==}
engines: {node: '>=12'}
dependencies:
@@ -20800,7 +17564,7 @@ packages:
is-fullwidth-code-point: 4.0.0
dev: true
- /snapdragon-node/2.1.1:
+ /snapdragon-node@2.1.1:
resolution: {integrity: sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==}
engines: {node: '>=0.10.0'}
dependencies:
@@ -20809,14 +17573,14 @@ packages:
snapdragon-util: 3.0.1
dev: true
- /snapdragon-util/3.0.1:
+ /snapdragon-util@3.0.1:
resolution: {integrity: sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==}
engines: {node: '>=0.10.0'}
dependencies:
kind-of: 3.2.2
dev: true
- /snapdragon/0.8.2:
+ /snapdragon@0.8.2:
resolution: {integrity: sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==}
engines: {node: '>=0.10.0'}
dependencies:
@@ -20832,7 +17596,7 @@ packages:
- supports-color
dev: true
- /sockjs/0.3.24:
+ /sockjs@0.3.24:
resolution: {integrity: sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==}
dependencies:
faye-websocket: 0.11.4
@@ -20840,77 +17604,77 @@ packages:
websocket-driver: 0.7.4
dev: true
- /source-list-map/2.0.1:
+ /sonic-boom@3.8.0:
+ resolution: {integrity: sha512-ybz6OYOUjoQQCQ/i4LU8kaToD8ACtYP+Cj5qd2AO36bwbdewxWJ3ArmJ2cr6AvxlL2o0PqnCcPGUgkILbfkaCA==}
+ dependencies:
+ atomic-sleep: 1.0.0
+ dev: true
+
+ /source-list-map@2.0.1:
resolution: {integrity: sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==}
dev: true
- /source-map-js/1.0.2:
+ /source-map-js@1.0.2:
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
engines: {node: '>=0.10.0'}
- dev: true
- /source-map-resolve/0.5.3:
+ /source-map-resolve@0.5.3:
resolution: {integrity: sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==}
deprecated: See https://github.com/lydell/source-map-resolve#deprecated
dependencies:
atob: 2.1.2
- decode-uri-component: 0.2.0
+ decode-uri-component: 0.2.2
resolve-url: 0.2.1
source-map-url: 0.4.1
urix: 0.1.0
dev: true
- /source-map-resolve/0.6.0:
- resolution: {integrity: sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==}
- deprecated: See https://github.com/lydell/source-map-resolve#deprecated
- dependencies:
- atob: 2.1.2
- decode-uri-component: 0.2.0
- dev: true
-
- /source-map-support/0.5.21:
+ /source-map-support@0.5.21:
resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
dependencies:
buffer-from: 1.1.2
source-map: 0.6.1
dev: true
- /source-map-url/0.4.1:
+ /source-map-url@0.4.1:
resolution: {integrity: sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==}
- deprecated: See https://github.com/lydell/source-map-url#deprecated
dev: true
- /source-map/0.5.7:
+ /source-map@0.5.7:
resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==}
engines: {node: '>=0.10.0'}
dev: true
- /source-map/0.6.1:
+ /source-map@0.6.1:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
engines: {node: '>=0.10.0'}
dev: true
- /source-map/0.7.4:
+ /source-map@0.7.4:
resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==}
engines: {node: '>= 8'}
dev: true
- /source-map/0.8.0-beta.0:
+ /source-map@0.8.0-beta.0:
resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==}
engines: {node: '>= 8'}
dependencies:
whatwg-url: 7.1.0
dev: true
- /sourcemap-codec/1.4.8:
+ /sourcemap-codec@1.4.8:
resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==}
dev: true
- /space-separated-tokens/1.1.5:
- resolution: {integrity: sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==}
+ /spawn-sync@1.0.15:
+ resolution: {integrity: sha512-9DWBgrgYZzNghseho0JOuh+5fg9u6QWhAWa51QC7+U5rCheZ/j1DrEZnyE0RBBRqZ9uEXGPgSSM0nky6burpVw==}
+ requiresBuild: true
+ dependencies:
+ concat-stream: 1.6.2
+ os-shim: 0.1.3
dev: true
- /spawn-wrap/2.0.0:
+ /spawn-wrap@2.0.0:
resolution: {integrity: sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==}
engines: {node: '>=8'}
dependencies:
@@ -20922,42 +17686,20 @@ packages:
which: 2.0.2
dev: true
- /spdx-correct/3.1.1:
- resolution: {integrity: sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==}
- dependencies:
- spdx-expression-parse: 3.0.1
- spdx-license-ids: 3.0.12
- dev: true
-
- /spdx-exceptions/2.3.0:
- resolution: {integrity: sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==}
- dev: true
-
- /spdx-expression-parse/3.0.1:
- resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==}
- dependencies:
- spdx-exceptions: 2.3.0
- spdx-license-ids: 3.0.12
- dev: true
-
- /spdx-license-ids/3.0.12:
- resolution: {integrity: sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA==}
- dev: true
-
- /spdy-transport/3.0.0:
+ /spdy-transport@3.0.0:
resolution: {integrity: sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==}
dependencies:
debug: 4.3.4
detect-node: 2.1.0
hpack.js: 2.1.6
obuf: 1.1.2
- readable-stream: 3.6.0
+ readable-stream: 3.6.2
wbuf: 1.7.3
transitivePeerDependencies:
- supports-color
dev: true
- /spdy/4.0.2:
+ /spdy@4.0.2:
resolution: {integrity: sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==}
engines: {node: '>=6.0.0'}
dependencies:
@@ -20970,21 +17712,31 @@ packages:
- supports-color
dev: true
- /split-string/3.1.0:
+ /split-string@3.1.0:
resolution: {integrity: sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==}
engines: {node: '>=0.10.0'}
dependencies:
extend-shallow: 3.0.2
dev: true
- /sprintf-js/1.0.3:
+ /split2@4.2.0:
+ resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
+ engines: {node: '>= 10.x'}
+ dev: true
+
+ /split@1.0.1:
+ resolution: {integrity: sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==}
+ dependencies:
+ through: 2.3.8
+ dev: true
+
+ /sprintf-js@1.0.3:
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
dev: true
- /sshpk/1.17.0:
+ /sshpk@1.17.0:
resolution: {integrity: sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==}
engines: {node: '>=0.10.0'}
- hasBin: true
dependencies:
asn1: 0.2.6
assert-plus: 1.0.0
@@ -20997,40 +17749,35 @@ packages:
tweetnacl: 0.14.5
dev: true
- /ssri/6.0.2:
+ /ssri@6.0.2:
resolution: {integrity: sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==}
dependencies:
figgy-pudding: 3.5.2
dev: true
- /ssri/8.0.1:
+ /ssri@8.0.1:
resolution: {integrity: sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==}
engines: {node: '>= 8'}
dependencies:
- minipass: 3.3.4
+ minipass: 3.3.6
dev: true
- /stable/0.1.8:
+ /stable@0.1.8:
resolution: {integrity: sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==}
- deprecated: 'Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility'
dev: true
- /stack-trace/0.0.10:
- resolution: {integrity: sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=}
+ /stack-trace@0.0.10:
+ resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==}
dev: true
- /stack-utils/2.0.5:
- resolution: {integrity: sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA==}
+ /stack-utils@2.0.6:
+ resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==}
engines: {node: '>=10'}
dependencies:
escape-string-regexp: 2.0.0
dev: true
- /state-toggle/1.0.3:
- resolution: {integrity: sha512-d/5Z4/2iiCnHw6Xzghyhb+GcmF89bxwgXG60wjIiZaxnymbyOmI8Hk4VqHXiVVp6u2ysaskFfXg3ekCj4WNftQ==}
- dev: true
-
- /static-extend/0.1.2:
+ /static-extend@0.1.2:
resolution: {integrity: sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==}
engines: {node: '>=0.10.0'}
dependencies:
@@ -21038,148 +17785,141 @@ packages:
object-copy: 0.1.0
dev: true
- /statuses/1.5.0:
+ /statuses@1.5.0:
resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==}
engines: {node: '>= 0.6'}
dev: true
- /statuses/2.0.1:
+ /statuses@2.0.1:
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
engines: {node: '>= 0.8'}
dev: true
- /stealthy-require/1.1.1:
+ /stealthy-require@1.1.1:
resolution: {integrity: sha512-ZnWpYnYugiOVEY5GkcuJK1io5V8QmNYChG62gSit9pQVGErXtrKuPC55ITaVSukmMta5qpMU7vqLt2Lnni4f/g==}
engines: {node: '>=0.10.0'}
dev: true
- /store2/2.14.2:
- resolution: {integrity: sha512-siT1RiqlfQnGqgT/YzXVUNsom9S0H1OX+dpdGN1xkyYATo4I6sep5NmsRD/40s3IIOvlCq6akxkqG82urIZW1w==}
- dev: true
-
- /stream-browserify/2.0.2:
+ /stream-browserify@2.0.2:
resolution: {integrity: sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==}
dependencies:
inherits: 2.0.4
- readable-stream: 2.3.7
+ readable-stream: 2.3.8
dev: true
- /stream-each/1.2.3:
+ /stream-each@1.2.3:
resolution: {integrity: sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==}
dependencies:
end-of-stream: 1.4.4
stream-shift: 1.0.1
dev: true
- /stream-http/2.8.3:
+ /stream-http@2.8.3:
resolution: {integrity: sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==}
dependencies:
builtin-status-codes: 3.0.0
inherits: 2.0.4
- readable-stream: 2.3.7
+ readable-stream: 2.3.8
to-arraybuffer: 1.0.1
xtend: 4.0.2
dev: true
- /stream-shift/1.0.1:
+ /stream-shift@1.0.1:
resolution: {integrity: sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==}
dev: true
- /string-length/4.0.2:
- resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==}
- engines: {node: '>=10'}
+ /stream-to-array@2.3.0:
+ resolution: {integrity: sha512-UsZtOYEn4tWU2RGLOXr/o/xjRBftZRlG3dEWoaHr8j4GuypJ3isitGbVyjQKAuMu+xbiop8q224TjiZWc4XTZA==}
dependencies:
- char-regex: 1.0.2
- strip-ansi: 6.0.1
+ any-promise: 1.3.0
+ dev: true
+
+ /stream-to-promise@3.0.0:
+ resolution: {integrity: sha512-h+7wLeFiYegOdgTfTxjRsrT7/Op7grnKEIHWgaO1RTHwcwk7xRreMr3S8XpDfDMesSxzgM2V4CxNCFAGo6ssnA==}
+ engines: {node: '>= 10'}
+ dependencies:
+ any-promise: 1.3.0
+ end-of-stream: 1.4.4
+ stream-to-array: 2.3.0
dev: true
- /string-width/4.2.3:
+ /string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
dependencies:
emoji-regex: 8.0.0
is-fullwidth-code-point: 3.0.0
strip-ansi: 6.0.1
- dev: true
- /string-width/5.1.2:
+ /string-width@5.1.2:
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
engines: {node: '>=12'}
dependencies:
eastasianwidth: 0.2.0
emoji-regex: 9.2.2
- strip-ansi: 7.0.1
- dev: true
+ strip-ansi: 7.1.0
- /string.prototype.matchall/4.0.7:
- resolution: {integrity: sha512-f48okCX7JiwVi1NXCVWcFnZgADDC/n2vePlQ/KUCNqCikLLilQvwjMO8+BHVKvgzH0JB0J9LEPgxOGT02RoETg==}
+ /string-width@7.0.0:
+ resolution: {integrity: sha512-GPQHj7row82Hjo9hKZieKcHIhaAIKOJvFSIZXuCU9OASVZrMNUaZuz++SPVrBjnLsnk4k+z9f2EIypgxf2vNFw==}
+ engines: {node: '>=18'}
dependencies:
- call-bind: 1.0.2
- define-properties: 1.1.4
- es-abstract: 1.20.4
- get-intrinsic: 1.1.3
- has-symbols: 1.0.3
- internal-slot: 1.0.3
- regexp.prototype.flags: 1.4.3
- side-channel: 1.0.4
+ emoji-regex: 10.3.0
+ get-east-asian-width: 1.2.0
+ strip-ansi: 7.1.0
dev: true
- /string.prototype.padend/3.1.3:
- resolution: {integrity: sha512-jNIIeokznm8SD/TZISQsZKYu7RJyheFNt84DUPrh482GC8RVp2MKqm2O5oBRdGxbDQoXrhhWtPIWQOiy20svUg==}
- engines: {node: '>= 0.4'}
+ /string.prototype.matchall@4.0.10:
+ resolution: {integrity: sha512-rGXbGmOEosIQi6Qva94HUjgPs9vKW+dkG7Y8Q5O2OYkWL6wFaTRZO8zM4mhP94uX55wgyrXzfS2aGtGzUL7EJQ==}
dependencies:
- call-bind: 1.0.2
- define-properties: 1.1.4
- es-abstract: 1.20.4
- dev: true
-
- /string.prototype.padstart/3.1.3:
- resolution: {integrity: sha512-NZydyOMtYxpTjGqp0VN5PYUF/tsU15yDMZnUdj16qRUIUiMJkHHSDElYyQFrMu+/WloTpA7MQSiADhBicDfaoA==}
- engines: {node: '>= 0.4'}
- dependencies:
- call-bind: 1.0.2
- define-properties: 1.1.4
- es-abstract: 1.20.4
+ call-bind: 1.0.5
+ define-properties: 1.2.1
+ es-abstract: 1.22.3
+ get-intrinsic: 1.2.2
+ has-symbols: 1.0.3
+ internal-slot: 1.0.6
+ regexp.prototype.flags: 1.5.1
+ set-function-name: 2.0.1
+ side-channel: 1.0.4
dev: true
- /string.prototype.trim/1.2.6:
- resolution: {integrity: sha512-8lMR2m+U0VJTPp6JjvJTtGyc4FIGq9CdRt7O9p6T0e6K4vjU+OP+SQJpbe/SBmRcCUIvNUnjsbmY6lnMp8MhsQ==}
+ /string.prototype.trim@1.2.8:
+ resolution: {integrity: sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==}
engines: {node: '>= 0.4'}
dependencies:
- call-bind: 1.0.2
- define-properties: 1.1.4
- es-abstract: 1.20.4
+ call-bind: 1.0.5
+ define-properties: 1.2.1
+ es-abstract: 1.22.3
dev: true
- /string.prototype.trimend/1.0.5:
- resolution: {integrity: sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==}
+ /string.prototype.trimend@1.0.7:
+ resolution: {integrity: sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==}
dependencies:
- call-bind: 1.0.2
- define-properties: 1.1.4
- es-abstract: 1.20.4
+ call-bind: 1.0.5
+ define-properties: 1.2.1
+ es-abstract: 1.22.3
dev: true
- /string.prototype.trimstart/1.0.5:
- resolution: {integrity: sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==}
+ /string.prototype.trimstart@1.0.7:
+ resolution: {integrity: sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==}
dependencies:
- call-bind: 1.0.2
- define-properties: 1.1.4
- es-abstract: 1.20.4
+ call-bind: 1.0.5
+ define-properties: 1.2.1
+ es-abstract: 1.22.3
dev: true
- /string_decoder/1.1.1:
+ /string_decoder@1.1.1:
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
dependencies:
safe-buffer: 5.1.2
dev: true
- /string_decoder/1.3.0:
+ /string_decoder@1.3.0:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
+ requiresBuild: true
dependencies:
safe-buffer: 5.2.1
- dev: true
- /stringify-object/3.3.0:
+ /stringify-object@3.3.0:
resolution: {integrity: sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==}
engines: {node: '>=4'}
dependencies:
@@ -21188,107 +17928,92 @@ packages:
is-regexp: 1.0.0
dev: true
- /strip-ansi/0.1.1:
+ /strip-ansi@0.1.1:
resolution: {integrity: sha512-behete+3uqxecWlDAm5lmskaSaISA+ThQ4oNNBDTBJt0x2ppR6IPqfZNuj6BLaLJ/Sji4TPZlcRyOis8wXQTLg==}
engines: {node: '>=0.8.0'}
hasBin: true
dev: true
- /strip-ansi/3.0.1:
+ /strip-ansi@3.0.1:
resolution: {integrity: sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==}
engines: {node: '>=0.10.0'}
dependencies:
ansi-regex: 2.1.1
dev: true
- /strip-ansi/6.0.1:
+ /strip-ansi@6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
dependencies:
ansi-regex: 5.0.1
- dev: true
- /strip-ansi/7.0.1:
- resolution: {integrity: sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==}
+ /strip-ansi@7.1.0:
+ resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==}
engines: {node: '>=12'}
dependencies:
ansi-regex: 6.0.1
- dev: true
- /strip-bom/2.0.0:
- resolution: {integrity: sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==}
- engines: {node: '>=0.10.0'}
+ /strip-bom-buf@2.0.0:
+ resolution: {integrity: sha512-gLFNHucd6gzb8jMsl5QmZ3QgnUJmp7qn4uUSHNwEXumAp7YizoGYw19ZUVfuq4aBOQUtyn2k8X/CwzWB73W2lQ==}
+ engines: {node: '>=8'}
dependencies:
is-utf8: 0.2.1
dev: true
- optional: true
- /strip-bom/3.0.0:
+ /strip-bom-stream@4.0.0:
+ resolution: {integrity: sha512-0ApK3iAkHv6WbgLICw/J4nhwHeDZsBxIIsOD+gHgZICL6SeJ0S9f/WZqemka9cjkTyMN5geId6e8U5WGFAn3cQ==}
+ engines: {node: '>=8'}
+ dependencies:
+ first-chunk-stream: 3.0.0
+ strip-bom-buf: 2.0.0
+ dev: true
+
+ /strip-bom@3.0.0:
resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
engines: {node: '>=4'}
dev: true
- /strip-bom/4.0.0:
+ /strip-bom@4.0.0:
resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==}
engines: {node: '>=8'}
dev: true
- /strip-comments/2.0.1:
- resolution: {integrity: sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==}
- engines: {node: '>=10'}
+ /strip-bom@5.0.0:
+ resolution: {integrity: sha512-p+byADHF7SzEcVnLvc/r3uognM1hUhObuHXxJcgLCfD194XAkaLbjq3Wzb0N5G2tgIjH0dgT708Z51QxMeu60A==}
+ engines: {node: '>=12'}
dev: true
- /strip-eof/1.0.0:
- resolution: {integrity: sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==}
- engines: {node: '>=0.10.0'}
+ /strip-comments@2.0.1:
+ resolution: {integrity: sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==}
+ engines: {node: '>=10'}
dev: true
- /strip-final-newline/2.0.0:
+ /strip-final-newline@2.0.0:
resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==}
engines: {node: '>=6'}
dev: true
- /strip-indent/1.0.1:
- resolution: {integrity: sha512-I5iQq6aFMM62fBEAIB/hXzwJD6EEZ0xEGCX2t7oXqaKPIRgt4WruAQ285BISgdkP+HLGWyeGmNJcpIwFeRYRUA==}
- engines: {node: '>=0.10.0'}
- hasBin: true
- dependencies:
- get-stdin: 4.0.1
+ /strip-final-newline@3.0.0:
+ resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==}
+ engines: {node: '>=12'}
dev: true
- optional: true
- /strip-json-comments/2.0.1:
+ /strip-json-comments@2.0.1:
resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
engines: {node: '>=0.10.0'}
- dev: true
+ requiresBuild: true
- /strip-json-comments/3.1.1:
+ /strip-json-comments@3.1.1:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'}
dev: true
- /strong-log-transformer/2.1.0:
- resolution: {integrity: sha512-B3Hgul+z0L9a236FAUC9iZsL+nVHgoCJnqCbN588DjYxvGXaXaaFbfmQ/JhvKjZwsOukuR72XbHv71Qkug0HxA==}
- engines: {node: '>=4'}
- hasBin: true
- dependencies:
- duplexer: 0.1.2
- minimist: 1.2.7
- through: 2.3.8
+ /strip-json-comments@5.0.0:
+ resolution: {integrity: sha512-V1LGY4UUo0jgwC+ELQ2BNWfPa17TIuwBLg+j1AA/9RPzKINl1lhxVEu2r+ZTTO8aetIsUzE5Qj6LMSBkoGYKKw==}
+ engines: {node: '>=14.16'}
dev: true
- /style-loader/1.3.0_webpack@4.46.0:
- resolution: {integrity: sha512-V7TCORko8rs9rIqkSrlMfkqA63DfoGBBJmK1kKGCcSi+BWb4cqz0SRsnp4l6rU5iwOEd0/2ePv68SV22VXon4Q==}
- engines: {node: '>= 8.9.0'}
- peerDependencies:
- webpack: ^4.0.0 || ^5.0.0
- dependencies:
- loader-utils: 2.0.3
- schema-utils: 2.7.1
- webpack: 4.46.0
- dev: true
-
- /style-loader/2.0.0_webpack@4.46.0:
+ /style-loader@2.0.0(webpack@4.46.0):
resolution: {integrity: sha512-Z0gYUJmzZ6ZdRUqpg1r8GsaFKypE+3xAzuFeMuoHgjc9KZv3wMyCRjQIWEbhoFSq7+7yoHXySDJyyWQaPajeiQ==}
engines: {node: '>= 10.13.0'}
peerDependencies:
@@ -21299,85 +18024,80 @@ packages:
webpack: 4.46.0
dev: true
- /style-to-object/0.3.0:
- resolution: {integrity: sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA==}
- dependencies:
- inline-style-parser: 0.1.1
- dev: true
-
- /stylehacks/4.0.3:
+ /stylehacks@4.0.3:
resolution: {integrity: sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g==}
engines: {node: '>=6.9.0'}
dependencies:
- browserslist: 4.21.4
+ browserslist: 4.22.2
postcss: 7.0.39
postcss-selector-parser: 3.1.2
dev: true
- /stylehacks/5.1.0_postcss@8.4.18:
+ /stylehacks@5.1.0(postcss@8.4.32):
resolution: {integrity: sha512-SzLmvHQTrIWfSgljkQCw2++C9+Ne91d/6Sp92I8c5uHTcy/PgeHamwITIbBW9wnFTY/3ZfSXR9HIL6Ikqmcu6Q==}
engines: {node: ^10 || ^12 || >=14.0}
peerDependencies:
postcss: ^8.2.15
dependencies:
- browserslist: 4.21.4
- postcss: 8.4.18
- postcss-selector-parser: 6.0.10
+ browserslist: 4.22.2
+ postcss: 8.4.32
+ postcss-selector-parser: 6.0.12
dev: true
- /stylis/3.5.4:
+ /stylis@3.5.4:
resolution: {integrity: sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q==}
dev: true
- /supertap/3.0.1:
+ /sucrase@3.32.0:
+ resolution: {integrity: sha512-ydQOU34rpSyj2TGyz4D2p8rbktIOZ8QY9s+DGLvFU1i5pWJE8vkpruCjGCMHsdXwnD7JDcS+noSwM/a7zyNFDQ==}
+ engines: {node: '>=8'}
+ hasBin: true
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.3
+ commander: 4.1.1
+ glob: 7.1.6
+ lines-and-columns: 1.2.4
+ mz: 2.7.0
+ pirates: 4.0.5
+ ts-interface-checker: 0.1.13
+
+ /supertap@3.0.1:
resolution: {integrity: sha512-u1ZpIBCawJnO+0QePsEiOknOfCRq0yERxiAchT0i4li0WHNUJbf0evXXSXOcCAR4M8iMDoajXYmstm/qO81Isw==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
dependencies:
indent-string: 5.0.0
js-yaml: 3.14.1
serialize-error: 7.0.1
- strip-ansi: 7.0.1
+ strip-ansi: 7.1.0
dev: true
- /supports-color/5.5.0:
+ /supports-color@5.5.0:
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
engines: {node: '>=4'}
dependencies:
has-flag: 3.0.0
- dev: true
- /supports-color/7.2.0:
+ /supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
dependencies:
has-flag: 4.0.0
dev: true
- /supports-color/8.1.1:
+ /supports-color@8.1.1:
resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}
engines: {node: '>=10'}
dependencies:
has-flag: 4.0.0
dev: true
- /supports-hyperlinks/2.3.0:
- resolution: {integrity: sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==}
- engines: {node: '>=8'}
- dependencies:
- has-flag: 4.0.0
- supports-color: 7.2.0
- dev: true
-
- /supports-preserve-symlinks-flag/1.0.0:
+ /supports-preserve-symlinks-flag@1.0.0:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
- dev: true
- /svgo/1.3.2:
+ /svgo@1.3.2:
resolution: {integrity: sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==}
engines: {node: '>=4.0.0'}
- deprecated: This SVGO version is no longer supported. Upgrade to v2.x.x.
- hasBin: true
dependencies:
chalk: 2.4.2
coa: 2.0.2
@@ -21387,17 +18107,16 @@ packages:
csso: 4.2.0
js-yaml: 3.14.1
mkdirp: 0.5.6
- object.values: 1.1.5
+ object.values: 1.1.7
sax: 1.2.4
stable: 0.1.8
unquote: 1.1.1
util.promisify: 1.0.1
dev: true
- /svgo/2.8.0:
+ /svgo@2.8.0:
resolution: {integrity: sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==}
engines: {node: '>=10.13.0'}
- hasBin: true
dependencies:
'@trysound/sax': 0.2.0
commander: 7.2.0
@@ -21408,47 +18127,30 @@ packages:
stable: 0.1.8
dev: true
- /swr/0.5.7:
- resolution: {integrity: sha512-Jh1Efgu8nWZV9rU4VLUMzBzcwaZgi4znqbVXvAtUy/0JzSiN6bNjLaJK8vhY/Rtp7a83dosz5YuehfBNwC/ZoQ==}
- peerDependencies:
- react: ^16.11.0 || ^17.0.0
- dependencies:
- dequal: 2.0.2
- dev: false
-
- /swr/1.3.0:
- resolution: {integrity: sha512-dkghQrOl2ORX9HYrMDtPa7LTVHJjCTeZoB1dqTbnnEDlSvN8JEKpYIYurDfvbQFUUS8Cg8PceFVZNkW0KNNYPw==}
+ /swr@2.0.3(react@18.2.0):
+ resolution: {integrity: sha512-sGvQDok/AHEWTPfhUWXEHBVEXmgGnuahyhmRQbjl9XBYxT/MSlAzvXEKQpyM++bMPaI52vcWS2HiKNaW7+9OFw==}
+ engines: {pnpm: '7'}
peerDependencies:
react: ^16.11.0 || ^17.0.0 || ^18.0.0
- dev: false
+ dependencies:
+ react: 18.2.0
+ use-sync-external-store: 1.2.0(react@18.2.0)
- /swr/1.3.0_@preact+compat@17.1.2:
- resolution: {integrity: sha512-dkghQrOl2ORX9HYrMDtPa7LTVHJjCTeZoB1dqTbnnEDlSvN8JEKpYIYurDfvbQFUUS8Cg8PceFVZNkW0KNNYPw==}
+ /swr@2.2.2(react@18.2.0):
+ resolution: {integrity: sha512-CbR41AoMD4TQBQw9ic3GTXspgfM9Y8Mdhb5Ob4uIKXhWqnRLItwA5fpGvB7SmSw3+zEjb0PdhiEumtUvYoQ+bQ==}
peerDependencies:
react: ^16.11.0 || ^17.0.0 || ^18.0.0
dependencies:
- react: /@preact/compat/17.1.2_preact@10.6.5
+ client-only: 0.0.1
+ react: 18.2.0
+ use-sync-external-store: 1.2.0(react@18.2.0)
dev: false
- /symbol-tree/3.2.4:
+ /symbol-tree@3.2.4:
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
dev: true
- /symbol.prototype.description/1.0.5:
- resolution: {integrity: sha512-x738iXRYsrAt9WBhRCVG5BtIC3B7CUkFwbHW2zOvGtwM33s7JjrCDyq8V0zgMYVb5ymsL8+qkzzpANH63CPQaQ==}
- engines: {node: '>= 0.11.15'}
- dependencies:
- call-bind: 1.0.2
- get-symbol-description: 1.0.0
- has-symbols: 1.0.3
- object.getownpropertydescriptors: 2.1.4
- dev: true
-
- /synchronous-promise/2.0.16:
- resolution: {integrity: sha512-qImOD23aDfnIDNqlG1NOehdB9IYsn1V9oByPjKY1nakv2MQYCEMyX033/q+aEtYCpmYK1cv2+NTmlH+ra6GA5A==}
- dev: true
-
- /table/6.8.0:
+ /table@6.8.0:
resolution: {integrity: sha512-s/fitrbVeEyHKFa7mFdkuQMWlH1Wgw/yEXMt5xACT4ZpzWFluehAxRtUUQKPuWhaLAWhFcVx6w3oC8VKaUfPGA==}
engines: {node: '>=10.0.0'}
dependencies:
@@ -21459,28 +18161,72 @@ packages:
strip-ansi: 6.0.1
dev: true
- /tapable/1.1.3:
+ /tailwindcss@3.3.2:
+ resolution: {integrity: sha512-9jPkMiIBXvPc2KywkraqsUfbfj+dHDb+JPWtSJa9MLFdrPyazI7q6WX2sUrm7R9eVR7qqv3Pas7EvQFzxKnI6w==}
+ engines: {node: '>=14.0.0'}
+ hasBin: true
+ dependencies:
+ '@alloc/quick-lru': 5.2.0
+ arg: 5.0.2
+ chokidar: 3.5.3
+ didyoumean: 1.2.2
+ dlv: 1.1.3
+ fast-glob: 3.3.1
+ glob-parent: 6.0.2
+ is-glob: 4.0.3
+ jiti: 1.18.2
+ lilconfig: 2.1.0
+ micromatch: 4.0.5
+ normalize-path: 3.0.0
+ object-hash: 3.0.0
+ picocolors: 1.0.0
+ postcss: 8.4.23
+ postcss-import: 15.1.0(postcss@8.4.23)
+ postcss-js: 4.0.1(postcss@8.4.23)
+ postcss-load-config: 4.0.1(postcss@8.4.23)
+ postcss-nested: 6.0.1(postcss@8.4.23)
+ postcss-selector-parser: 6.0.12
+ postcss-value-parser: 4.2.0
+ resolve: 1.22.2
+ sucrase: 3.32.0
+ transitivePeerDependencies:
+ - ts-node
+
+ /tapable@1.1.3:
resolution: {integrity: sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==}
engines: {node: '>=6'}
dev: true
- /tapable/2.2.1:
+ /tapable@2.2.1:
resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==}
engines: {node: '>=6'}
dev: true
- /tar-stream/2.2.0:
+ /tar-fs@2.1.1:
+ resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==}
+ requiresBuild: true
+ dependencies:
+ chownr: 1.1.4
+ mkdirp-classic: 0.5.3
+ pump: 3.0.0
+ tar-stream: 2.2.0
+ dev: false
+ optional: true
+
+ /tar-stream@2.2.0:
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
engines: {node: '>=6'}
+ requiresBuild: true
dependencies:
bl: 4.1.0
end-of-stream: 1.4.4
fs-constants: 1.0.0
inherits: 2.0.4
- readable-stream: 3.6.0
- dev: true
+ readable-stream: 3.6.2
+ dev: false
+ optional: true
- /tar/4.4.19:
+ /tar@4.4.19:
resolution: {integrity: sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA==}
engines: {node: '>=4.5'}
dependencies:
@@ -21493,37 +18239,41 @@ packages:
yallist: 3.1.1
dev: true
- /tar/6.1.11:
+ /tar@6.1.11:
resolution: {integrity: sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==}
engines: {node: '>= 10'}
dependencies:
chownr: 2.0.0
fs-minipass: 2.1.0
- minipass: 3.3.4
+ minipass: 3.3.6
minizlib: 2.1.2
mkdirp: 1.0.4
yallist: 4.0.0
dev: true
- /telejson/6.0.8:
- resolution: {integrity: sha512-nerNXi+j8NK1QEfBHtZUN/aLdDcyupA//9kAboYLrtzZlPLpUfqbVGWb9zz91f/mIjRbAYhbgtnJHY8I1b5MBg==}
+ /tar@6.2.0:
+ resolution: {integrity: sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==}
+ engines: {node: '>=10'}
dependencies:
- '@types/is-function': 1.0.1
- global: 4.4.0
- is-function: 1.0.2
- is-regex: 1.1.4
- is-symbol: 1.0.4
- isobject: 4.0.0
- lodash: 4.17.21
- memoizerific: 1.11.3
+ chownr: 2.0.0
+ fs-minipass: 2.1.0
+ minipass: 5.0.0
+ minizlib: 2.1.2
+ mkdirp: 1.0.4
+ yallist: 4.0.0
dev: true
- /temp-dir/2.0.0:
+ /temp-dir@2.0.0:
resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==}
engines: {node: '>=8'}
dev: true
- /tempy/0.6.0:
+ /temp-dir@3.0.0:
+ resolution: {integrity: sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw==}
+ engines: {node: '>=14.16'}
+ dev: true
+
+ /tempy@0.6.0:
resolution: {integrity: sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==}
engines: {node: '>=10'}
dependencies:
@@ -21533,15 +18283,25 @@ packages:
unique-string: 2.0.0
dev: true
- /terminal-link/2.1.1:
- resolution: {integrity: sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==}
- engines: {node: '>=8'}
+ /terser-webpack-plugin@1.4.5(webpack@4.46.0):
+ resolution: {integrity: sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw==}
+ engines: {node: '>= 6.9.0'}
+ peerDependencies:
+ webpack: ^4.0.0
dependencies:
- ansi-escapes: 4.3.2
- supports-hyperlinks: 2.3.0
+ cacache: 12.0.4
+ find-cache-dir: 2.1.0
+ is-wsl: 1.1.0
+ schema-utils: 1.0.0
+ serialize-javascript: 4.0.0
+ source-map: 0.6.1
+ terser: 4.8.1
+ webpack: 4.46.0
+ webpack-sources: 1.4.3
+ worker-farm: 1.7.0
dev: true
- /terser-webpack-plugin/1.4.5_webpack@4.46.0:
+ /terser-webpack-plugin@1.4.5(webpack@4.47.0):
resolution: {integrity: sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw==}
engines: {node: '>= 6.9.0'}
peerDependencies:
@@ -21554,12 +18314,12 @@ packages:
serialize-javascript: 4.0.0
source-map: 0.6.1
terser: 4.8.1
- webpack: 4.46.0
+ webpack: 4.47.0
webpack-sources: 1.4.3
worker-farm: 1.7.0
dev: true
- /terser-webpack-plugin/4.2.3_webpack@4.46.0:
+ /terser-webpack-plugin@4.2.3(webpack@4.46.0):
resolution: {integrity: sha512-jTgXh40RnvOrLQNgIkwEKnQ8rmHjHK4u+6UBEi+W+FPmvb+uo+chJXntKe7/3lW5mNysgSWD60KyesnhW8D6MQ==}
engines: {node: '>= 10.13.0'}
peerDependencies:
@@ -21579,53 +18339,39 @@ packages:
- bluebird
dev: true
- /terser-webpack-plugin/5.3.6_webpack@5.74.0:
- resolution: {integrity: sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==}
- engines: {node: '>= 10.13.0'}
- peerDependencies:
- '@swc/core': '*'
- esbuild: '*'
- uglify-js: '*'
- webpack: ^5.1.0
- peerDependenciesMeta:
- '@swc/core':
- optional: true
- esbuild:
- optional: true
- uglify-js:
- optional: true
- dependencies:
- '@jridgewell/trace-mapping': 0.3.17
- jest-worker: 27.5.1
- schema-utils: 3.1.1
- serialize-javascript: 6.0.0
- terser: 5.15.1
- webpack: 5.74.0
- dev: true
-
- /terser/4.8.1:
+ /terser@4.8.1:
resolution: {integrity: sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==}
engines: {node: '>=6.0.0'}
hasBin: true
dependencies:
- acorn: 8.8.1
+ acorn: 8.11.2
commander: 2.20.3
source-map: 0.6.1
source-map-support: 0.5.21
dev: true
- /terser/5.15.1:
+ /terser@5.15.1:
resolution: {integrity: sha512-K1faMUvpm/FBxjBXud0LWVAGxmvoPbZbfTCYbSgaaYQaIXI3/TdI7a7ZGA73Zrou6Q8Zmz3oeUTsp/dj+ag2Xw==}
engines: {node: '>=10'}
- hasBin: true
dependencies:
'@jridgewell/source-map': 0.3.2
- acorn: 8.8.1
+ acorn: 8.11.2
commander: 2.20.3
source-map-support: 0.5.21
dev: true
- /test-exclude/6.0.0:
+ /terser@5.26.0:
+ resolution: {integrity: sha512-dytTGoE2oHgbNV9nTzgBEPaqAWvcJNl66VZ0BkJqlvp71IjO8CxdBx/ykCNb47cLnCmCvRZ6ZR0tLkqvZCdVBQ==}
+ engines: {node: '>=10'}
+ hasBin: true
+ dependencies:
+ '@jridgewell/source-map': 0.3.5
+ acorn: 8.11.2
+ commander: 2.20.3
+ source-map-support: 0.5.21
+ dev: true
+
+ /test-exclude@6.0.0:
resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==}
engines: {node: '>=8'}
dependencies:
@@ -21634,101 +18380,103 @@ packages:
minimatch: 3.1.2
dev: true
- /text-table/0.2.0:
+ /text-table@0.2.0:
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
dev: true
- /throat/5.0.0:
- resolution: {integrity: sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==}
+ /thenby@1.3.4:
+ resolution: {integrity: sha512-89Gi5raiWA3QZ4b2ePcEwswC3me9JIg+ToSgtE0JWeCynLnLxNr/f9G+xfo9K+Oj4AFdom8YNJjibIARTJmapQ==}
dev: true
- /throttle-debounce/3.0.1:
- resolution: {integrity: sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==}
- engines: {node: '>=10'}
- dev: true
+ /thenify-all@1.6.0:
+ resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
+ engines: {node: '>=0.8'}
+ dependencies:
+ thenify: 3.3.1
- /through/2.3.8:
- resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
+ /thenify@3.3.1:
+ resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
+ dependencies:
+ any-promise: 1.3.0
+
+ /thread-stream@2.4.1:
+ resolution: {integrity: sha512-d/Ex2iWd1whipbT681JmTINKw0ZwOUBZm7+Gjs64DHuX34mmw8vJL2bFAaNacaW72zYiTJxSHi5abUuOi5nsfg==}
+ dependencies:
+ real-require: 0.2.0
dev: true
- /through2/2.0.5:
+ /through2@2.0.5:
resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==}
dependencies:
- readable-stream: 2.3.7
+ readable-stream: 2.3.8
xtend: 4.0.2
dev: true
- /thunky/1.1.0:
+ /through@2.3.8:
+ resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
+ dev: true
+
+ /thunky@1.1.0:
resolution: {integrity: sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==}
dev: true
- /time-zone/1.0.0:
+ /time-zone@1.0.0:
resolution: {integrity: sha512-TIsDdtKo6+XrPtiTm1ssmMngN1sAhyKnTO2kunQWqNPWIVvCm15Wmw4SWInwTVgJ5u/Tr04+8Ei9TNcw4x4ONA==}
engines: {node: '>=4'}
dev: true
- /timers-browserify/2.0.12:
+ /timers-browserify@2.0.12:
resolution: {integrity: sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==}
engines: {node: '>=0.6.0'}
dependencies:
setimmediate: 1.0.5
dev: true
- /timsort/0.3.0:
+ /timsort@0.3.0:
resolution: {integrity: sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==}
dev: true
- /tiny-invariant/1.3.1:
+ /tiny-invariant@1.3.1:
resolution: {integrity: sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==}
dev: false
- /tiny-warning/1.0.3:
+ /tiny-warning@1.0.3:
resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==}
dev: false
- /tinydate/1.3.0:
+ /tinydate@1.3.0:
resolution: {integrity: sha512-7cR8rLy2QhYHpsBDBVYnnWXm8uRTr38RoZakFSW7Bs7PzfMPNZthuMLkwqZv7MTu8lhQ91cOFYS5a7iFj2oR3w==}
engines: {node: '>=4'}
dev: true
- /tmp/0.2.1:
+ /tmp@0.2.1:
resolution: {integrity: sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==}
engines: {node: '>=8.17.0'}
dependencies:
rimraf: 3.0.2
dev: true
- /tmpl/1.0.5:
- resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==}
- dev: true
-
- /to-arraybuffer/1.0.1:
+ /to-arraybuffer@1.0.1:
resolution: {integrity: sha512-okFlQcoGTi4LQBG/PgSYblw9VOyptsz2KJZqc6qtgGdes8VktzUQkj4BI2blit072iS8VODNcMA+tvnS9dnuMA==}
dev: true
- /to-fast-properties/1.0.3:
- resolution: {integrity: sha512-lxrWP8ejsq+7E3nNjwYmUBMAgjMTZoTI+sdBOpvNyijeDLa29LUn9QaoXAHv4+Z578hbmHHJKZknzxVtvo77og==}
- engines: {node: '>=0.10.0'}
- dev: true
-
- /to-fast-properties/2.0.0:
+ /to-fast-properties@2.0.0:
resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
engines: {node: '>=4'}
- dev: true
- /to-object-path/0.3.0:
+ /to-object-path@0.3.0:
resolution: {integrity: sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==}
engines: {node: '>=0.10.0'}
dependencies:
kind-of: 3.2.2
dev: true
- /to-readable-stream/1.0.0:
+ /to-readable-stream@1.0.0:
resolution: {integrity: sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==}
engines: {node: '>=6'}
dev: true
- /to-regex-range/2.1.1:
+ /to-regex-range@2.1.1:
resolution: {integrity: sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==}
engines: {node: '>=0.10.0'}
dependencies:
@@ -21736,14 +18484,13 @@ packages:
repeat-string: 1.6.1
dev: true
- /to-regex-range/5.0.1:
+ /to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
dependencies:
is-number: 7.0.0
- dev: true
- /to-regex/3.0.2:
+ /to-regex@3.0.2:
resolution: {integrity: sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==}
engines: {node: '>=0.10.0'}
dependencies:
@@ -21753,25 +18500,30 @@ packages:
safe-regex: 1.1.0
dev: true
- /toidentifier/1.0.1:
+ /toidentifier@1.0.1:
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
engines: {node: '>=0.6'}
dev: true
- /toposort/1.0.7:
+ /toposort@1.0.7:
resolution: {integrity: sha512-FclLrw8b9bMWf4QlCJuHBEVhSRsqDj6u3nIjAzPeJvgl//1hBlffdlk0MALceL14+koWEdU4ofRAXofbODxQzg==}
dev: true
- /toposort/2.0.2:
+ /toposort@2.0.2:
resolution: {integrity: sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==}
dev: false
- /totalist/1.1.0:
+ /tosource@1.0.0:
+ resolution: {integrity: sha512-N6g8eQ1eerw6Y1pBhdgkubWIiPFwXa2POSUrlL8jth5CyyEWNWzoGKRkO3CaO7Jx27hlJP54muB3btIAbx4MPg==}
+ engines: {node: '>=0.4.0'}
+ dev: true
+
+ /totalist@1.1.0:
resolution: {integrity: sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==}
engines: {node: '>=6'}
dev: true
- /tough-cookie/2.5.0:
+ /tough-cookie@2.5.0:
resolution: {integrity: sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==}
engines: {node: '>=0.8'}
dependencies:
@@ -21779,75 +18531,67 @@ packages:
punycode: 2.1.1
dev: true
- /tough-cookie/4.1.2:
- resolution: {integrity: sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==}
- engines: {node: '>=6'}
- dependencies:
- psl: 1.9.0
- punycode: 2.1.1
- universalify: 0.2.0
- url-parse: 1.5.10
- dev: true
-
- /tr46/0.0.3:
+ /tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
+ dev: true
- /tr46/1.0.1:
+ /tr46@1.0.1:
resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==}
dependencies:
punycode: 2.1.1
dev: true
- /tr46/2.1.0:
- resolution: {integrity: sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==}
- engines: {node: '>=8'}
+ /ts-api-utils@1.0.3(typescript@5.3.3):
+ resolution: {integrity: sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==}
+ engines: {node: '>=16.13.0'}
+ peerDependencies:
+ typescript: '>=4.2.0'
dependencies:
- punycode: 2.1.1
+ typescript: 5.3.3
dev: true
- /trim-newlines/1.0.0:
- resolution: {integrity: sha512-Nm4cF79FhSTzrLKGDMi3I4utBtFv8qKy4sq1enftf2gMdpqI8oVQTAfySkTz5r49giVzDj88SVZXP4CeYQwjaw==}
- engines: {node: '>=0.10.0'}
- dev: true
- optional: true
-
- /trim-trailing-lines/1.1.4:
- resolution: {integrity: sha512-rjUWSqnfTNrjbB9NQWfPMH/xRK1deHeGsHoVfpxJ++XeYXE0d6B1En37AHfw3jtfTU7dzMzZL2jjpe8Qb5gLIQ==}
- dev: true
-
- /trim/0.0.1:
- resolution: {integrity: sha1-WFhUf2spB1fulczMZm+1AITEYN0=}
- dev: true
+ /ts-interface-checker@0.1.13:
+ resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
- /trough/1.0.5:
- resolution: {integrity: sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==}
- dev: true
-
- /ts-dedent/2.2.0:
- resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==}
- engines: {node: '>=6.10'}
- dev: true
-
- /ts-invariant/0.10.3:
+ /ts-invariant@0.10.3:
resolution: {integrity: sha512-uivwYcQaxAucv1CzRp2n/QdYPo4ILf9VXgH19zEIjFx2EJufV16P0JtJVpYHy89DItG6Kwj2oIUjrcK5au+4tQ==}
engines: {node: '>=8'}
dependencies:
- tslib: 2.4.1
+ tslib: 2.6.2
dev: true
- /ts-pnp/1.2.0_typescript@4.4.4:
- resolution: {integrity: sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw==}
- engines: {node: '>=6'}
+ /ts-node@10.9.1(@types/node@20.11.13)(typescript@5.3.3):
+ resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==}
+ hasBin: true
peerDependencies:
- typescript: '*'
+ '@swc/core': '>=1.2.50'
+ '@swc/wasm': '>=1.2.50'
+ '@types/node': '*'
+ typescript: '>=2.7'
peerDependenciesMeta:
- typescript:
+ '@swc/core':
+ optional: true
+ '@swc/wasm':
optional: true
dependencies:
- typescript: 4.4.4
+ '@cspotcode/source-map-support': 0.8.1
+ '@tsconfig/node10': 1.0.9
+ '@tsconfig/node12': 1.0.11
+ '@tsconfig/node14': 1.0.3
+ '@tsconfig/node16': 1.0.3
+ '@types/node': 20.11.13
+ acorn: 8.8.1
+ acorn-walk: 8.2.0
+ arg: 4.1.3
+ create-require: 1.1.1
+ diff: 4.0.2
+ make-error: 1.3.6
+ typescript: 5.3.3
+ v8-compile-cache-lib: 3.0.1
+ yn: 3.1.1
dev: true
- /ts-pnp/1.2.0_typescript@4.6.4:
+ /ts-pnp@1.2.0(typescript@4.6.4):
resolution: {integrity: sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw==}
engines: {node: '>=6'}
peerDependencies:
@@ -21859,109 +18603,96 @@ packages:
typescript: 4.6.4
dev: true
- /tsconfig-paths/3.14.1:
- resolution: {integrity: sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==}
+ /tsconfig-paths@3.15.0:
+ resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==}
dependencies:
'@types/json5': 0.0.29
- json5: 1.0.1
- minimist: 1.2.7
+ json5: 1.0.2
+ minimist: 1.2.8
strip-bom: 3.0.0
dev: true
- /tslib/1.14.1:
+ /tslib@1.14.1:
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
dev: true
- /tslib/2.4.0:
- resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==}
+ /tslib@2.6.2:
+ resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
- /tslib/2.4.1:
- resolution: {integrity: sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==}
- dev: true
-
- /tsutils/3.21.0_typescript@4.4.4:
- resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
- engines: {node: '>= 6'}
- peerDependencies:
- typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta'
- dependencies:
- tslib: 1.14.1
- typescript: 4.4.4
- dev: true
-
- /tsutils/3.21.0_typescript@4.8.4:
+ /tsutils@3.21.0(typescript@5.3.3):
resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
engines: {node: '>= 6'}
peerDependencies:
typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta'
dependencies:
tslib: 1.14.1
- typescript: 4.8.4
+ typescript: 5.3.3
dev: true
- /tty-browserify/0.0.0:
- resolution: {integrity: sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=}
+ /tty-browserify@0.0.0:
+ resolution: {integrity: sha512-JVa5ijo+j/sOoHGjw0sxw734b1LhBkQ3bvUGNdxnVXDCX81Yx7TFgnZygxrIIWn23hbfTaMYLwRmAxFyDuFmIw==}
dev: true
- /tunnel-agent/0.6.0:
+ /tunnel-agent@0.6.0:
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
+ requiresBuild: true
dependencies:
safe-buffer: 5.2.1
- dev: true
- /tweetnacl/0.14.5:
+ /tweetnacl@0.14.5:
resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==}
dev: true
- /type-check/0.3.2:
+ /type-check@0.3.2:
resolution: {integrity: sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==}
engines: {node: '>= 0.8.0'}
dependencies:
prelude-ls: 1.1.2
dev: true
- /type-check/0.4.0:
+ /type-check@0.4.0:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
dependencies:
prelude-ls: 1.2.1
dev: true
- /type-detect/4.0.8:
+ /type-detect@4.0.8:
resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==}
engines: {node: '>=4'}
+ dev: true
- /type-fest/0.13.1:
+ /type-fest@0.13.1:
resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==}
engines: {node: '>=10'}
dev: true
- /type-fest/0.16.0:
+ /type-fest@0.16.0:
resolution: {integrity: sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==}
engines: {node: '>=10'}
dev: true
- /type-fest/0.20.2:
+ /type-fest@0.20.2:
resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==}
engines: {node: '>=10'}
dev: true
- /type-fest/0.21.3:
- resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==}
- engines: {node: '>=10'}
+ /type-fest@0.8.1:
+ resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==}
+ engines: {node: '>=8'}
dev: true
- /type-fest/0.6.0:
- resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==}
- engines: {node: '>=8'}
+ /type-fest@1.4.0:
+ resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==}
+ engines: {node: '>=10'}
dev: true
- /type-fest/0.8.1:
- resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==}
- engines: {node: '>=8'}
+ /type-fest@2.19.0:
+ resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==}
+ engines: {node: '>=12.20'}
dev: true
- /type-is/1.6.18:
+ /type-is@1.6.18:
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
engines: {node: '>= 0.6'}
dependencies:
@@ -21969,142 +18700,115 @@ packages:
mime-types: 2.1.35
dev: true
- /typedarray-to-buffer/3.1.5:
- resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==}
+ /typed-array-buffer@1.0.0:
+ resolution: {integrity: sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==}
+ engines: {node: '>= 0.4'}
dependencies:
- is-typedarray: 1.0.0
+ call-bind: 1.0.5
+ get-intrinsic: 1.2.2
+ is-typed-array: 1.1.12
dev: true
- /typedarray/0.0.6:
- resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==}
+ /typed-array-byte-length@1.0.0:
+ resolution: {integrity: sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ call-bind: 1.0.5
+ for-each: 0.3.3
+ has-proto: 1.0.1
+ is-typed-array: 1.1.12
dev: true
- /typedoc-default-themes/0.12.10:
- resolution: {integrity: sha512-fIS001cAYHkyQPidWXmHuhs8usjP5XVJjWB8oZGqkTowZaz3v7g3KDZeeqE82FBrmkAnIBOY3jgy7lnPnqATbA==}
- engines: {node: '>= 8'}
+ /typed-array-byte-offset@1.0.0:
+ resolution: {integrity: sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ available-typed-arrays: 1.0.5
+ call-bind: 1.0.5
+ for-each: 0.3.3
+ has-proto: 1.0.1
+ is-typed-array: 1.1.12
dev: true
- /typedoc/0.20.37_typescript@4.4.4:
- resolution: {integrity: sha512-9+qDhdc4X00qTNOtii6QX2z7ndAeWVOso7w3MPSoSJdXlVhpwPfm1yEp4ooKuWA9fiQILR8FKkyjmeqa13hBbw==}
- engines: {node: '>= 10.8.0'}
- hasBin: true
- peerDependencies:
- typescript: 3.9.x || 4.0.x || 4.1.x || 4.2.x
+ /typed-array-length@1.0.4:
+ resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==}
dependencies:
- colors: 1.4.0
- fs-extra: 9.1.0
- handlebars: 4.7.7
- lodash: 4.17.21
- lunr: 2.3.9
- marked: 2.0.7
- minimatch: 3.1.2
- progress: 2.0.3
- shelljs: 0.8.5
- shiki: 0.9.15
- typedoc-default-themes: 0.12.10
- typescript: 4.4.4
+ call-bind: 1.0.5
+ for-each: 0.3.3
+ is-typed-array: 1.1.12
dev: true
- /typedoc/0.20.37_typescript@4.8.4:
- resolution: {integrity: sha512-9+qDhdc4X00qTNOtii6QX2z7ndAeWVOso7w3MPSoSJdXlVhpwPfm1yEp4ooKuWA9fiQILR8FKkyjmeqa13hBbw==}
- engines: {node: '>= 10.8.0'}
- hasBin: true
- peerDependencies:
- typescript: 3.9.x || 4.0.x || 4.1.x || 4.2.x
+ /typedarray-to-buffer@3.1.5:
+ resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==}
dependencies:
- colors: 1.4.0
- fs-extra: 9.1.0
- handlebars: 4.7.7
- lodash: 4.17.21
- lunr: 2.3.9
- marked: 2.0.7
- minimatch: 3.1.2
- progress: 2.0.3
- shelljs: 0.8.5
- shiki: 0.9.15
- typedoc-default-themes: 0.12.10
- typescript: 4.8.4
+ is-typedarray: 1.0.0
dev: true
- /typedoc/0.23.18_typescript@4.8.4:
- resolution: {integrity: sha512-0Tq/uFkUuWyRYyjOShTkhsOm6u5E8wf0i6L76/k5znEaxvWKHGeT2ywZThGrDrryV/skO/REM824D1gm8ccQuA==}
- engines: {node: '>= 14.14'}
+ /typedarray@0.0.6:
+ resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==}
+ dev: true
+
+ /typedoc@0.25.4(typescript@5.3.3):
+ resolution: {integrity: sha512-Du9ImmpBCw54bX275yJrxPVnjdIyJO/84co0/L9mwe0R3G4FSR6rQ09AlXVRvZEGMUg09+z/usc8mgygQ1aidA==}
+ engines: {node: '>= 16'}
hasBin: true
peerDependencies:
- typescript: 4.6.x || 4.7.x || 4.8.x
+ typescript: 4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x || 5.3.x
dependencies:
lunr: 2.3.9
- marked: 4.1.1
- minimatch: 5.1.0
- shiki: 0.11.1
- typescript: 4.8.4
+ marked: 4.3.0
+ minimatch: 9.0.3
+ shiki: 0.14.6
+ typescript: 5.3.3
dev: true
- /typescript/4.4.4:
- resolution: {integrity: sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==}
- engines: {node: '>=4.2.0'}
- hasBin: true
- dev: true
-
- /typescript/4.6.4:
+ /typescript@4.6.4:
resolution: {integrity: sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==}
engines: {node: '>=4.2.0'}
hasBin: true
dev: true
- /typescript/4.8.4:
- resolution: {integrity: sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==}
- engines: {node: '>=4.2.0'}
+ /typescript@5.3.3:
+ resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==}
+ engines: {node: '>=14.17'}
hasBin: true
dev: true
- /uglify-js/3.17.4:
- resolution: {integrity: sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==}
- engines: {node: '>=0.8.0'}
- hasBin: true
- requiresBuild: true
- dev: true
- optional: true
-
- /uglify-js/3.4.10:
+ /uglify-js@3.4.10:
resolution: {integrity: sha512-Y2VsbPVs0FIshJztycsO2SfPk7/KAF/T72qzv9u5EpQ4kB2hQoHlhNQTsNyy6ul7lQtqJN/AoWeS23OzEiEFxw==}
engines: {node: '>=0.8.0'}
- hasBin: true
dependencies:
commander: 2.19.0
source-map: 0.6.1
dev: true
- /unbox-primitive/1.0.2:
+ /unbox-primitive@1.0.2:
resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
dependencies:
- call-bind: 1.0.2
+ call-bind: 1.0.5
has-bigints: 1.0.2
has-symbols: 1.0.3
which-boxed-primitive: 1.0.2
dev: true
- /underscore/1.6.0:
+ /underscore@1.6.0:
resolution: {integrity: sha512-z4o1fvKUojIWh9XuaVLUDdf86RQiq13AC1dmHbTpoyuu+bquHms76v16CjycCbec87J7z0k//SiQVk0sMdFmpQ==}
dev: true
- /unfetch/4.2.0:
- resolution: {integrity: sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==}
+ /undici-types@5.26.5:
+ resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
dev: true
- /unherit/1.1.3:
- resolution: {integrity: sha512-Ft16BJcnapDKp0+J/rqFC3Rrk6Y/Ng4nzsC028k2jdDII/rdZ7Wd3pPT/6+vIIxRagwRc9K0IUX0Ra4fKvw+WQ==}
- dependencies:
- inherits: 2.0.4
- xtend: 4.0.2
+ /unfetch@4.2.0:
+ resolution: {integrity: sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==}
dev: true
- /unicode-canonical-property-names-ecmascript/2.0.0:
+ /unicode-canonical-property-names-ecmascript@2.0.0:
resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==}
engines: {node: '>=4'}
dev: true
- /unicode-match-property-ecmascript/2.0.0:
+ /unicode-match-property-ecmascript@2.0.0:
resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==}
engines: {node: '>=4'}
dependencies:
@@ -22112,29 +18816,22 @@ packages:
unicode-property-aliases-ecmascript: 2.1.0
dev: true
- /unicode-match-property-value-ecmascript/2.0.0:
- resolution: {integrity: sha512-7Yhkc0Ye+t4PNYzOGKedDhXbYIBe1XEQYQxOPyhcXNMJ0WCABqqj6ckydd6pWRZTHV4GuCPKdBAUiMc60tsKVw==}
+ /unicode-match-property-value-ecmascript@2.1.0:
+ resolution: {integrity: sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==}
engines: {node: '>=4'}
dev: true
- /unicode-property-aliases-ecmascript/2.1.0:
+ /unicode-property-aliases-ecmascript@2.1.0:
resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==}
engines: {node: '>=4'}
dev: true
- /unified/9.2.0:
- resolution: {integrity: sha512-vx2Z0vY+a3YoTj8+pttM3tiJHCwY5UFbYdiWrwBEbHmK8pvsPj2rtAX2BFfgXen8T39CJWblWRDT4L5WGXtDdg==}
- dependencies:
- '@types/unist': 2.0.6
- bail: 1.0.5
- extend: 3.0.2
- is-buffer: 2.0.5
- is-plain-obj: 2.1.0
- trough: 1.0.5
- vfile: 4.2.1
+ /unicorn-magic@0.1.0:
+ resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==}
+ engines: {node: '>=18'}
dev: true
- /union-value/1.0.1:
+ /union-value@1.0.1:
resolution: {integrity: sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==}
engines: {node: '>=0.10.0'}
dependencies:
@@ -22144,102 +18841,60 @@ packages:
set-value: 2.0.1
dev: true
- /uniq/1.0.1:
+ /uniq@1.0.1:
resolution: {integrity: sha512-Gw+zz50YNKPDKXs+9d+aKAjVwpjNwqzvNpLigIruT4HA9lMZNdMqs9x07kKHB/L9WRzqp4+DlTU5s4wG2esdoA==}
dev: true
- /uniqs/2.0.0:
+ /uniqs@2.0.0:
resolution: {integrity: sha512-mZdDpf3vBV5Efh29kMw5tXoup/buMgxLzOt/XKFKcVmi+15ManNQWr6HfZ2aiZTYlYixbdNJ0KFmIZIv52tHSQ==}
dev: true
- /unique-filename/1.1.1:
+ /unique-filename@1.1.1:
resolution: {integrity: sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==}
dependencies:
unique-slug: 2.0.2
dev: true
- /unique-slug/2.0.2:
+ /unique-slug@2.0.2:
resolution: {integrity: sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==}
dependencies:
imurmurhash: 0.1.4
dev: true
- /unique-string/2.0.0:
+ /unique-string@2.0.0:
resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==}
engines: {node: '>=8'}
dependencies:
crypto-random-string: 2.0.0
dev: true
- /unist-builder/2.0.3:
- resolution: {integrity: sha512-f98yt5pnlMWlzP539tPc4grGMsFaQQlP/vM396b00jngsiINumNmsY8rkXjfoi1c6QaM8nQ3vaGDuoKWbe/1Uw==}
- dev: true
-
- /unist-util-generated/1.1.6:
- resolution: {integrity: sha512-cln2Mm1/CZzN5ttGK7vkoGw+RZ8VcUH6BtGbq98DDtRGquAAOXig1mrBQYelOwMXYS8rK+vZDyyojSjp7JX+Lg==}
- dev: true
-
- /unist-util-is/4.1.0:
- resolution: {integrity: sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==}
- dev: true
-
- /unist-util-position/3.1.0:
- resolution: {integrity: sha512-w+PkwCbYSFw8vpgWD0v7zRCl1FpY3fjDSQ3/N/wNd9Ffa4gPi8+4keqt99N3XW6F99t/mUzp2xAhNmfKWp95QA==}
- dev: true
-
- /unist-util-remove-position/2.0.1:
- resolution: {integrity: sha512-fDZsLYIe2uT+oGFnuZmy73K6ZxOPG/Qcm+w7jbEjaFcJgbQ6cqjs/eSPzXhsmGpAsWPkqZM9pYjww5QTn3LHMA==}
- dependencies:
- unist-util-visit: 2.0.3
- dev: true
-
- /unist-util-remove/2.1.0:
- resolution: {integrity: sha512-J8NYPyBm4baYLdCbjmf1bhPu45Cr1MWTm77qd9istEkzWpnN6O9tMsEbB2JhNnBCqGENRqEWomQ+He6au0B27Q==}
- dependencies:
- unist-util-is: 4.1.0
- dev: true
-
- /unist-util-stringify-position/2.0.3:
- resolution: {integrity: sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==}
- dependencies:
- '@types/unist': 2.0.6
- dev: true
-
- /unist-util-visit-parents/3.1.1:
- resolution: {integrity: sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==}
- dependencies:
- '@types/unist': 2.0.6
- unist-util-is: 4.1.0
- dev: true
-
- /unist-util-visit/2.0.3:
- resolution: {integrity: sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==}
+ /unique-string@3.0.0:
+ resolution: {integrity: sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==}
+ engines: {node: '>=12'}
dependencies:
- '@types/unist': 2.0.6
- unist-util-is: 4.1.0
- unist-util-visit-parents: 3.1.1
+ crypto-random-string: 4.0.0
dev: true
- /universalify/0.2.0:
- resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==}
- engines: {node: '>= 4.0.0'}
+ /universalify@1.0.0:
+ resolution: {integrity: sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==}
+ engines: {node: '>= 10.0.0'}
dev: true
- /universalify/2.0.0:
+ /universalify@2.0.0:
resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==}
engines: {node: '>= 10.0.0'}
dev: true
- /unpipe/1.0.0:
+ /unpipe@1.0.0:
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
engines: {node: '>= 0.8'}
dev: true
- /unquote/1.1.1:
+ /unquote@1.1.1:
resolution: {integrity: sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg==}
dev: true
- /unset-value/1.0.0:
+ /unset-value@1.0.0:
resolution: {integrity: sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==}
engines: {node: '>=0.10.0'}
dependencies:
@@ -22247,31 +18902,50 @@ packages:
isobject: 3.0.1
dev: true
- /untildify/2.1.0:
- resolution: {integrity: sha512-sJjbDp2GodvkB0FZZcn7k6afVisqX5BZD7Yq3xp4nN2O15BBK0cLm3Vwn2vQaF7UDS0UUsrQMkkplmDI5fskig==}
- engines: {node: '>=0.10.0'}
- dependencies:
- os-homedir: 1.0.2
+ /upath@1.2.0:
+ resolution: {integrity: sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==}
+ engines: {node: '>=4'}
+ requiresBuild: true
dev: true
- optional: true
- /upath/1.2.0:
- resolution: {integrity: sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==}
+ /upath@2.0.1:
+ resolution: {integrity: sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==}
engines: {node: '>=4'}
dev: true
- /update-browserslist-db/1.0.10_browserslist@4.21.4:
+ /update-browserslist-db@1.0.10(browserslist@4.21.5):
resolution: {integrity: sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==}
hasBin: true
peerDependencies:
browserslist: '>= 4.21.0'
dependencies:
+ browserslist: 4.21.5
+ escalade: 3.1.1
+ picocolors: 1.0.0
+ dev: true
+
+ /update-browserslist-db@1.0.13(browserslist@4.21.4):
+ resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==}
+ hasBin: true
+ peerDependencies:
+ browserslist: '>= 4.21.0'
+ dependencies:
browserslist: 4.21.4
escalade: 3.1.1
picocolors: 1.0.0
dev: true
- /update-notifier/5.1.0:
+ /update-browserslist-db@1.0.13(browserslist@4.22.2):
+ resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==}
+ hasBin: true
+ peerDependencies:
+ browserslist: '>= 4.21.0'
+ dependencies:
+ browserslist: 4.22.2
+ escalade: 3.1.1
+ picocolors: 1.0.0
+
+ /update-notifier@5.1.0:
resolution: {integrity: sha512-ItnICHbeMh9GqUy31hFPrD1kcuZ3rpxDZbf4KUDavXwS0bW5m7SLbDQpGX3UYr072cbrF5hFUs3r5tUsPwjfHw==}
engines: {node: '>=10'}
dependencies:
@@ -22286,27 +18960,47 @@ packages:
is-yarn-global: 0.3.0
latest-version: 5.1.0
pupa: 2.1.1
- semver: 7.3.8
+ semver: 7.5.4
semver-diff: 3.1.1
xdg-basedir: 4.0.0
dev: true
- /upper-case/1.1.3:
+ /update-notifier@6.0.2:
+ resolution: {integrity: sha512-EDxhTEVPZZRLWYcJ4ZXjGFN0oP7qYvbXWzEgRm/Yql4dHX5wDbvh89YHP6PK1lzZJYrMtXUuZZz8XGK+U6U1og==}
+ engines: {node: '>=14.16'}
+ dependencies:
+ boxen: 7.1.1
+ chalk: 5.3.0
+ configstore: 6.0.0
+ has-yarn: 3.0.0
+ import-lazy: 4.0.0
+ is-ci: 3.0.1
+ is-installed-globally: 0.4.0
+ is-npm: 6.0.0
+ is-yarn-global: 0.4.1
+ latest-version: 7.0.0
+ pupa: 3.1.0
+ semver: 7.5.4
+ semver-diff: 4.0.0
+ xdg-basedir: 5.1.0
+ dev: true
+
+ /upper-case@1.1.3:
resolution: {integrity: sha512-WRbjgmYzgXkCV7zNVpy5YgrHgbBv126rMALQQMrmzOVC4GM2waQ9x7xtm8VU+1yF2kWyPzI9zbZ48n4vSxwfSA==}
dev: true
- /uri-js/4.4.1:
+ /uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
dependencies:
punycode: 2.1.1
dev: true
- /urix/0.1.0:
+ /urix@0.1.0:
resolution: {integrity: sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==}
deprecated: Please see https://github.com/lydell/urix#deprecated
dev: true
- /url-loader/4.1.1_lit45vopotvaqup7lrvlnvtxwy:
+ /url-loader@4.1.1(file-loader@6.2.0)(webpack@4.46.0):
resolution: {integrity: sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA==}
engines: {node: '>= 10.13.0'}
peerDependencies:
@@ -22316,156 +19010,137 @@ packages:
file-loader:
optional: true
dependencies:
- file-loader: 6.2.0_webpack@4.46.0
+ file-loader: 6.2.0(webpack@4.46.0)
loader-utils: 2.0.3
mime-types: 2.1.35
schema-utils: 3.1.1
webpack: 4.46.0
dev: true
- /url-parse-lax/3.0.0:
+ /url-parse-lax@3.0.0:
resolution: {integrity: sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ==}
engines: {node: '>=4'}
dependencies:
prepend-http: 2.0.0
dev: true
- /url-parse/1.5.10:
- resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==}
+ /url@0.11.1:
+ resolution: {integrity: sha512-rWS3H04/+mzzJkv0eZ7vEDGiQbgquI1fGfOad6zKvgYQi1SzMmhl7c/DdRGxhaWrVH6z0qWITo8rpnxK/RfEhA==}
dependencies:
- querystringify: 2.2.0
- requires-port: 1.0.0
+ punycode: 1.4.1
+ qs: 6.11.2
dev: true
- /url/0.11.0:
- resolution: {integrity: sha512-kbailJa29QrtXnxgq+DdCEGlbTeYM2eJUxsz6vjZavrCYPMIFHMKQmSKYAIuUK2i7hgPm28a8piX5NTUtM/LKQ==}
+ /use-sync-external-store@1.2.0(react@18.2.0):
+ resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies:
- punycode: 1.3.2
- querystring: 0.2.0
- dev: true
+ react: 18.2.0
- /use/3.1.1:
+ /use@3.1.1:
resolution: {integrity: sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==}
engines: {node: '>=0.10.0'}
dev: true
- /util-deprecate/1.0.2:
+ /util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
- dev: true
+ requiresBuild: true
- /util.promisify/1.0.0:
+ /util.promisify@1.0.0:
resolution: {integrity: sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==}
dependencies:
- define-properties: 1.1.4
+ define-properties: 1.2.1
object.getownpropertydescriptors: 2.1.4
dev: true
- /util.promisify/1.0.1:
+ /util.promisify@1.0.1:
resolution: {integrity: sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==}
dependencies:
- define-properties: 1.1.4
- es-abstract: 1.20.4
+ define-properties: 1.2.1
+ es-abstract: 1.22.3
has-symbols: 1.0.3
object.getownpropertydescriptors: 2.1.4
dev: true
- /util.promisify/1.1.1:
+ /util.promisify@1.1.1:
resolution: {integrity: sha512-/s3UsZUrIfa6xDhr7zZhnE9SLQ5RIXyYfiVnMMyMDzOc8WhWN4Nbh36H842OyurKbCDAesZOJaVyvmSl6fhGQw==}
dependencies:
- call-bind: 1.0.2
- define-properties: 1.1.4
+ call-bind: 1.0.5
+ define-properties: 1.2.1
for-each: 0.3.3
has-symbols: 1.0.3
object.getownpropertydescriptors: 2.1.4
dev: true
- /util/0.10.3:
+ /util@0.10.3:
resolution: {integrity: sha512-5KiHfsmkqacuKjkRkdV7SsfDJ2EGiPsK92s2MhNSY0craxjTdKTtqKsJaCWp4LW33ZZ0OPUv1WO/TFvNQRiQxQ==}
dependencies:
inherits: 2.0.1
dev: true
- /util/0.11.1:
+ /util@0.11.1:
resolution: {integrity: sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==}
dependencies:
inherits: 2.0.3
dev: true
- /utila/0.4.0:
+ /utila@0.4.0:
resolution: {integrity: sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==}
dev: true
- /utils-merge/1.0.1:
+ /utils-merge@1.0.1:
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
engines: {node: '>= 0.4.0'}
dev: true
- /uuid-browser/3.1.0:
- resolution: {integrity: sha512-dsNgbLaTrd6l3MMxTtouOCFw4CBFc/3a+GgYA2YyrJvyQ1u6q4pcu3ktLoUZ/VN/Aw9WsauazbgsgdfVWgAKQg==}
- dev: true
-
- /uuid/3.4.0:
+ /uuid@3.4.0:
resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==}
- deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.
- hasBin: true
dev: true
- /uuid/8.3.2:
+ /uuid@8.3.2:
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
- hasBin: true
dev: true
- /v8-compile-cache/2.3.0:
- resolution: {integrity: sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==}
+ /v8-compile-cache-lib@3.0.1:
+ resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
dev: true
- /v8-to-istanbul/7.1.2:
- resolution: {integrity: sha512-TxNb7YEUwkLXCQYeudi6lgQ/SZrzNO4kMdlqVxaZPUIUjCv6iSSypUQX70kNBSERpQ8fk48+d61FXk+tgqcWow==}
- engines: {node: '>=10.10.0'}
- dependencies:
- '@types/istanbul-lib-coverage': 2.0.4
- convert-source-map: 1.9.0
- source-map: 0.7.4
+ /v8-compile-cache@2.3.0:
+ resolution: {integrity: sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==}
dev: true
- /v8-to-istanbul/9.0.1:
- resolution: {integrity: sha512-74Y4LqY74kLE6IFyIjPtkSTWzUZmj8tdHT9Ii/26dvQ6K9Dl2NbEfj0XgU2sHCtKgt5VupqhlO/5aWuqS+IY1w==}
+ /v8-to-istanbul@9.2.0:
+ resolution: {integrity: sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==}
engines: {node: '>=10.12.0'}
dependencies:
- '@jridgewell/trace-mapping': 0.3.17
- '@types/istanbul-lib-coverage': 2.0.4
- convert-source-map: 1.9.0
+ '@jridgewell/trace-mapping': 0.3.20
+ '@types/istanbul-lib-coverage': 2.0.6
+ convert-source-map: 2.0.0
dev: true
- /validate-npm-package-license/3.0.4:
- resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
- dependencies:
- spdx-correct: 3.1.1
- spdx-expression-parse: 3.0.1
- dev: true
-
- /validate-npm-package-name/4.0.0:
+ /validate-npm-package-name@4.0.0:
resolution: {integrity: sha512-mzR0L8ZDktZjpX4OB46KT+56MAhl4EIazWP/+G/HPGuvfdaqg4YsCdtOm6U9+LOFyYDoh4dpnpxZRB9MQQns5Q==}
engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
dependencies:
builtins: 5.0.1
dev: true
- /value-equal/1.0.1:
+ /value-equal@1.0.1:
resolution: {integrity: sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==}
dev: false
- /vary/1.1.2:
+ /vary@1.1.2:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'}
dev: true
- /vendors/1.0.4:
+ /vendors@1.0.4:
resolution: {integrity: sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==}
dev: true
- /verror/1.10.0:
- resolution: {integrity: sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=}
+ /verror@1.10.0:
+ resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==}
engines: {'0': node >=0.6.0}
dependencies:
assert-plus: 1.0.0
@@ -22473,63 +19148,25 @@ packages:
extsprintf: 1.3.0
dev: true
- /vfile-location/3.2.0:
- resolution: {integrity: sha512-aLEIZKv/oxuCDZ8lkJGhuhztf/BW4M+iHdCwglA/eWc+vtuRFJj8EtgceYFX4LRjOhCAAiNHsKGssC6onJ+jbA==}
- dev: true
-
- /vfile-message/2.0.4:
- resolution: {integrity: sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==}
- dependencies:
- '@types/unist': 2.0.6
- unist-util-stringify-position: 2.0.3
- dev: true
-
- /vfile/4.2.1:
- resolution: {integrity: sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA==}
- dependencies:
- '@types/unist': 2.0.6
- is-buffer: 2.0.5
- unist-util-stringify-position: 2.0.3
- vfile-message: 2.0.4
- dev: true
-
- /vm-browserify/1.1.2:
+ /vm-browserify@1.1.2:
resolution: {integrity: sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==}
dev: true
- /vscode-oniguruma/1.6.2:
- resolution: {integrity: sha512-KH8+KKov5eS/9WhofZR8M8dMHWN2gTxjMsG4jd04YhpbPR91fUj7rYQ2/XjeHCJWbg7X++ApRIU9NUwM2vTvLA==}
+ /vscode-oniguruma@1.7.0:
+ resolution: {integrity: sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==}
dev: true
- /vscode-textmate/5.2.0:
- resolution: {integrity: sha512-Uw5ooOQxRASHgu6C7GVvUxisKXfSgW4oFlO+aa+PAkgmH89O3CXxEEzNRNtHSqtXFTl0nAC1uYj0GMSH27uwtQ==}
+ /vscode-textmate@8.0.0:
+ resolution: {integrity: sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==}
dev: true
- /vscode-textmate/6.0.0:
- resolution: {integrity: sha512-gu73tuZfJgu+mvCSy4UZwd2JXykjK9zAZsfmDeut5dx/1a7FeTk0XwJsSuqQn+cuMCGVbIBfl+s53X4T19DnzQ==}
- dev: true
-
- /w3c-hr-time/1.0.2:
+ /w3c-hr-time@1.0.2:
resolution: {integrity: sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==}
- deprecated: Use your platform's native performance.now() and performance.timeOrigin.
dependencies:
browser-process-hrtime: 1.0.0
dev: true
- /w3c-xmlserializer/2.0.0:
- resolution: {integrity: sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==}
- engines: {node: '>=10'}
- dependencies:
- xml-name-validator: 3.0.0
- dev: true
-
- /walker/1.0.8:
- resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==}
- dependencies:
- makeerror: 1.0.12
- dev: true
-
- /watchpack-chokidar2/2.0.1:
+ /watchpack-chokidar2@2.0.1:
resolution: {integrity: sha512-nCFfBIPKr5Sh61s4LPpy1Wtfi0HE8isJ3d2Yb5/Ppw2P2B/3eVSEBjKfN0fmHJSK14+31KwMKmcrzs2GM4P0Ww==}
requiresBuild: true
dependencies:
@@ -22539,10 +19176,10 @@ packages:
dev: true
optional: true
- /watchpack/1.7.5:
+ /watchpack@1.7.5:
resolution: {integrity: sha512-9P3MWk6SrKjHsGkLT2KHXdQ/9SNkyoJbabxnKOoJepsvJjJG8uYTR3yTPxPQvNDI3w4Nz1xnE0TLHK4RIVe/MQ==}
dependencies:
- graceful-fs: 4.2.10
+ graceful-fs: 4.2.11
neo-async: 2.6.2
optionalDependencies:
chokidar: 3.5.3
@@ -22551,58 +19188,95 @@ packages:
- supports-color
dev: true
- /watchpack/2.4.0:
+ /watchpack@2.4.0:
resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==}
engines: {node: '>=10.13.0'}
dependencies:
glob-to-regexp: 0.4.1
- graceful-fs: 4.2.10
+ graceful-fs: 4.2.11
dev: true
- /wbuf/1.7.3:
+ /wbuf@1.7.3:
resolution: {integrity: sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==}
dependencies:
minimalistic-assert: 1.0.1
dev: true
- /wcwidth/1.0.1:
+ /wcwidth@1.0.1:
resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==}
dependencies:
defaults: 1.0.4
dev: true
- /web-namespaces/1.1.4:
- resolution: {integrity: sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw==}
+ /web-ext@7.11.0:
+ resolution: {integrity: sha512-EG6YXHITNDJB/h6Rc5FF08eMoN45sZPBBIIlEraBzxJ0RdJZ8Z3xvUUawbDwt+mowfv9X0XRWlLSwdWbRKgojg==}
+ engines: {node: '>=14.0.0', npm: '>=6.9.0'}
+ hasBin: true
+ dependencies:
+ '@babel/runtime': 7.21.0
+ '@devicefarmer/adbkit': 3.2.3
+ addons-linter: 6.21.0(node-fetch@3.3.1)
+ bunyan: 1.8.15
+ camelcase: 7.0.1
+ chrome-launcher: 0.15.1
+ debounce: 1.2.1
+ decamelize: 6.0.0
+ es6-error: 4.1.1
+ firefox-profile: 4.3.2
+ fs-extra: 11.1.0
+ fx-runner: 1.4.0
+ import-fresh: 3.3.0
+ jose: 4.13.1
+ mkdirp: 1.0.4
+ multimatch: 6.0.0
+ mz: 2.7.0
+ node-fetch: 3.3.1
+ node-notifier: 10.0.1
+ open: 8.4.2
+ parse-json: 6.0.2
+ promise-toolbox: 0.21.0
+ sign-addon: 5.3.0
+ source-map-support: 0.5.21
+ strip-bom: 5.0.0
+ strip-json-comments: 5.0.0
+ tmp: 0.2.1
+ update-notifier: 6.0.2
+ watchpack: 2.4.0
+ ws: 8.13.0
+ yargs: 17.7.1
+ zip-dir: 2.0.0
+ transitivePeerDependencies:
+ - body-parser
+ - bufferutil
+ - express
+ - safe-compare
+ - supports-color
+ - utf-8-validate
dev: true
- /web-streams-polyfill/3.2.1:
- resolution: {integrity: sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==}
+ /web-streams-polyfill@3.3.3:
+ resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
engines: {node: '>= 8'}
- dev: false
+ dev: true
- /webidl-conversions/3.0.1:
+ /webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
-
- /webidl-conversions/4.0.2:
- resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==}
dev: true
- /webidl-conversions/5.0.0:
- resolution: {integrity: sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==}
- engines: {node: '>=8'}
+ /webidl-conversions@4.0.2:
+ resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==}
dev: true
- /webidl-conversions/6.1.0:
- resolution: {integrity: sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==}
- engines: {node: '>=10.4'}
+ /webidl-conversions@7.0.0:
+ resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
+ engines: {node: '>=12'}
dev: true
- /webpack-bundle-analyzer/4.6.1:
+ /webpack-bundle-analyzer@4.6.1:
resolution: {integrity: sha512-oKz9Oz9j3rUciLNfpGFjOb49/jEpXNmWdVH8Ls//zNcnLlQdTGXQQMsBbb/gR7Zl8WNLxVCq+0Hqbx3zv6twBw==}
engines: {node: '>= 10.13.0'}
- hasBin: true
dependencies:
- acorn: 8.8.1
+ acorn: 8.8.2
acorn-walk: 8.2.0
chalk: 4.1.2
commander: 7.2.0
@@ -22616,21 +19290,7 @@ packages:
- utf-8-validate
dev: true
- /webpack-dev-middleware/3.7.3_webpack@4.46.0:
- resolution: {integrity: sha512-djelc/zGiz9nZj/U7PTBi2ViorGJXEWo/3ltkPbDyxCXhhEXkW0ce99falaok4TPj+AsxLiXJR0EBOb0zh9fKQ==}
- engines: {node: '>= 6'}
- peerDependencies:
- webpack: ^4.0.0 || ^5.0.0
- dependencies:
- memory-fs: 0.4.1
- mime: 2.6.0
- mkdirp: 0.5.6
- range-parser: 1.2.1
- webpack: 4.46.0
- webpack-log: 2.0.0
- dev: true
-
- /webpack-dev-middleware/5.3.3_webpack@4.46.0:
+ /webpack-dev-middleware@5.3.3(webpack@4.46.0):
resolution: {integrity: sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA==}
engines: {node: '>= 12.13.0'}
peerDependencies:
@@ -22644,7 +19304,7 @@ packages:
webpack: 4.46.0
dev: true
- /webpack-dev-server/4.11.1_webpack@4.46.0:
+ /webpack-dev-server@4.11.1(webpack@4.46.0):
resolution: {integrity: sha512-lILVz9tAUy1zGFwieuaQtYiadImb5M3d+H+L1zDYalYoDl0cksAB1UNyuE5MMWJrG6zR1tXkCP2fitl7yoUJiw==}
engines: {node: '>= 12.13.0'}
hasBin: true
@@ -22670,9 +19330,9 @@ packages:
connect-history-api-fallback: 2.0.0
default-gateway: 6.0.3
express: 4.18.2
- graceful-fs: 4.2.10
+ graceful-fs: 4.2.11
html-entities: 2.3.3
- http-proxy-middleware: 2.0.6_@types+express@4.17.14
+ http-proxy-middleware: 2.0.6(@types/express@4.17.14)
ipaddr.js: 2.0.1
open: 8.4.0
p-retry: 4.6.2
@@ -22683,7 +19343,7 @@ packages:
sockjs: 0.3.24
spdy: 4.0.2
webpack: 4.46.0
- webpack-dev-middleware: 5.3.3_webpack@4.46.0
+ webpack-dev-middleware: 5.3.3(webpack@4.46.0)
ws: 8.10.0
transitivePeerDependencies:
- bufferutil
@@ -22692,28 +19352,11 @@ packages:
- utf-8-validate
dev: true
- /webpack-filter-warnings-plugin/1.2.1_webpack@4.46.0:
- resolution: {integrity: sha512-Ez6ytc9IseDMLPo0qCuNNYzgtUl8NovOqjIq4uAU8LTD4uoa1w1KpZyyzFtLTEMZpkkOkLfL9eN+KGYdk1Qtwg==}
- engines: {node: '>= 4.3 < 5.0.0 || >= 5.10'}
- peerDependencies:
- webpack: ^2.0.0 || ^3.0.0 || ^4.0.0
- dependencies:
- webpack: 4.46.0
- dev: true
-
- /webpack-fix-style-only-entries/0.6.1:
+ /webpack-fix-style-only-entries@0.6.1:
resolution: {integrity: sha512-wyIhoxS3DD3Fr9JA8hQPA+ZmaWnqPxx12Nv166wcsI/0fbReqyEtiIk2llOFYIg57WVS3XX5cZJxw2ji70R0sA==}
dev: true
- /webpack-hot-middleware/2.25.2:
- resolution: {integrity: sha512-CVgm3NAQyfdIonRvXisRwPTUYuSbyZ6BY7782tMeUzWOO7RmVI2NaBYuCp41qyD4gYCkJyTneAJdK69A13B0+A==}
- dependencies:
- ansi-html-community: 0.0.8
- html-entities: 2.3.3
- strip-ansi: 6.0.1
- dev: true
-
- /webpack-log/2.0.0:
+ /webpack-log@2.0.0:
resolution: {integrity: sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg==}
engines: {node: '>= 6'}
dependencies:
@@ -22721,7 +19364,7 @@ packages:
uuid: 3.4.0
dev: true
- /webpack-manifest-plugin/4.1.1_webpack@4.46.0:
+ /webpack-manifest-plugin@4.1.1(webpack@4.46.0):
resolution: {integrity: sha512-YXUAwxtfKIJIKkhg03MKuiFAD72PlrqCiwdwO4VEXdRO5V0ORCNwaOwAZawPZalCbmH9kBDmXnNeQOw+BIEiow==}
engines: {node: '>=12.22.0'}
peerDependencies:
@@ -22732,7 +19375,7 @@ packages:
webpack-sources: 2.3.1
dev: true
- /webpack-merge/5.8.0:
+ /webpack-merge@5.8.0:
resolution: {integrity: sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==}
engines: {node: '>=10.0.0'}
dependencies:
@@ -22740,19 +19383,19 @@ packages:
wildcard: 2.0.0
dev: true
- /webpack-plugin-replace/1.2.0:
+ /webpack-plugin-replace@1.2.0:
resolution: {integrity: sha512-1HA3etHpJW55qonJqv84o5w5GY7iqF8fqSHpTWdNwarj1llkkt4jT4QSvYs1hoaU8Lu5akDnq/spHHO5mXwo1w==}
engines: {node: '>=4'}
dev: true
- /webpack-sources/1.4.3:
+ /webpack-sources@1.4.3:
resolution: {integrity: sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==}
dependencies:
source-list-map: 2.0.1
source-map: 0.6.1
dev: true
- /webpack-sources/2.3.1:
+ /webpack-sources@2.3.1:
resolution: {integrity: sha512-y9EI9AO42JjEcrTJFOYmVywVZdKVUfOvDUPsJea5GIr1JOEGFVqwlY2K098fFoIjOkDzHn2AjRvM8dsBZu+gCA==}
engines: {node: '>=10.13.0'}
dependencies:
@@ -22760,20 +19403,7 @@ packages:
source-map: 0.6.1
dev: true
- /webpack-sources/3.2.3:
- resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==}
- engines: {node: '>=10.13.0'}
- dev: true
-
- /webpack-virtual-modules/0.2.2:
- resolution: {integrity: sha512-kDUmfm3BZrei0y+1NTHJInejzxfhtU8eDj2M7OKb2IWrPFAeO1SOH2KuQ68MSZu9IGEHcxbkKKR1v18FrUSOmA==}
- dependencies:
- debug: 3.2.7
- transitivePeerDependencies:
- - supports-color
- dev: true
-
- /webpack/4.46.0:
+ /webpack@4.46.0:
resolution: {integrity: sha512-6jJuJjg8znb/xRItk7bkT0+Q7AHCYjjFnvKIWQPkNIOyRqoCGvkOs0ipeQzrqz4l5FtN5ZI/ukEHroeX/o1/5Q==}
engines: {node: '>=6.11.5'}
hasBin: true
@@ -22792,13 +19422,13 @@ packages:
'@webassemblyjs/wasm-parser': 1.9.0
acorn: 6.4.2
ajv: 6.12.6
- ajv-keywords: 3.5.2_ajv@6.12.6
+ ajv-keywords: 3.5.2(ajv@6.12.6)
chrome-trace-event: 1.0.3
enhanced-resolve: 4.5.0
eslint-scope: 4.0.3
json-parse-better-errors: 1.0.2
loader-runner: 2.4.0
- loader-utils: 1.4.0
+ loader-utils: 1.4.2
memory-fs: 0.4.1
micromatch: 3.1.10
mkdirp: 0.5.6
@@ -22806,54 +19436,54 @@ packages:
node-libs-browser: 2.2.1
schema-utils: 1.0.0
tapable: 1.1.3
- terser-webpack-plugin: 1.4.5_webpack@4.46.0
+ terser-webpack-plugin: 1.4.5(webpack@4.46.0)
watchpack: 1.7.5
webpack-sources: 1.4.3
transitivePeerDependencies:
- supports-color
dev: true
- /webpack/5.74.0:
- resolution: {integrity: sha512-A2InDwnhhGN4LYctJj6M1JEaGL7Luj6LOmyBHjcI8529cm5p6VXiTIW2sn6ffvEAKmveLzvu4jrihwXtPojlAA==}
- engines: {node: '>=10.13.0'}
+ /webpack@4.47.0:
+ resolution: {integrity: sha512-td7fYwgLSrky3fI1EuU5cneU4+pbH6GgOfuKNS1tNPcfdGinGELAqsb/BP4nnvZyKSG2i/xFGU7+n2PvZA8HJQ==}
+ engines: {node: '>=6.11.5'}
hasBin: true
peerDependencies:
webpack-cli: '*'
+ webpack-command: '*'
peerDependenciesMeta:
webpack-cli:
optional: true
+ webpack-command:
+ optional: true
dependencies:
- '@types/eslint-scope': 3.7.4
- '@types/estree': 0.0.51
- '@webassemblyjs/ast': 1.11.1
- '@webassemblyjs/wasm-edit': 1.11.1
- '@webassemblyjs/wasm-parser': 1.11.1
- acorn: 8.8.1
- acorn-import-assertions: 1.8.0_acorn@8.8.1
- browserslist: 4.21.4
+ '@webassemblyjs/ast': 1.9.0
+ '@webassemblyjs/helper-module-context': 1.9.0
+ '@webassemblyjs/wasm-edit': 1.9.0
+ '@webassemblyjs/wasm-parser': 1.9.0
+ acorn: 6.4.2
+ ajv: 6.12.6
+ ajv-keywords: 3.5.2(ajv@6.12.6)
chrome-trace-event: 1.0.3
- enhanced-resolve: 5.10.0
- es-module-lexer: 0.9.3
- eslint-scope: 5.1.1
- events: 3.3.0
- glob-to-regexp: 0.4.1
- graceful-fs: 4.2.10
- json-parse-even-better-errors: 2.3.1
- loader-runner: 4.3.0
- mime-types: 2.1.35
+ enhanced-resolve: 4.5.0
+ eslint-scope: 4.0.3
+ json-parse-better-errors: 1.0.2
+ loader-runner: 2.4.0
+ loader-utils: 1.4.2
+ memory-fs: 0.4.1
+ micromatch: 3.1.10
+ mkdirp: 0.5.6
neo-async: 2.6.2
- schema-utils: 3.1.1
- tapable: 2.2.1
- terser-webpack-plugin: 5.3.6_webpack@5.74.0
- watchpack: 2.4.0
- webpack-sources: 3.2.3
+ node-libs-browser: 2.2.1
+ schema-utils: 1.0.0
+ tapable: 1.1.3
+ terser-webpack-plugin: 1.4.5(webpack@4.47.0)
+ watchpack: 1.7.5
+ webpack-sources: 1.4.3
transitivePeerDependencies:
- - '@swc/core'
- - esbuild
- - uglify-js
+ - supports-color
dev: true
- /websocket-driver/0.7.4:
+ /websocket-driver@0.7.4:
resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==}
engines: {node: '>=0.8.0'}
dependencies:
@@ -22862,33 +19492,46 @@ packages:
websocket-extensions: 0.1.4
dev: true
- /websocket-extensions/0.1.4:
+ /websocket-extensions@0.1.4:
resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==}
engines: {node: '>=0.8.0'}
dev: true
- /well-known-symbols/2.0.0:
+ /well-known-symbols@2.0.0:
resolution: {integrity: sha512-ZMjC3ho+KXo0BfJb7JgtQ5IBuvnShdlACNkKkdsqBmYw3bPAaJfPeYUo6tLUaT5tG/Gkh7xkpBhKRQ9e7pyg9Q==}
engines: {node: '>=6'}
dev: true
- /whatwg-encoding/1.0.5:
+ /whatwg-encoding@1.0.5:
resolution: {integrity: sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==}
dependencies:
iconv-lite: 0.4.24
dev: true
- /whatwg-mimetype/2.3.0:
+ /whatwg-encoding@2.0.0:
+ resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==}
+ engines: {node: '>=12'}
+ dependencies:
+ iconv-lite: 0.6.3
+ dev: true
+
+ /whatwg-mimetype@2.3.0:
resolution: {integrity: sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==}
dev: true
- /whatwg-url/5.0.0:
+ /whatwg-mimetype@3.0.0:
+ resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==}
+ engines: {node: '>=12'}
+ dev: true
+
+ /whatwg-url@5.0.0:
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
dependencies:
tr46: 0.0.3
webidl-conversions: 3.0.1
+ dev: true
- /whatwg-url/7.1.0:
+ /whatwg-url@7.1.0:
resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==}
dependencies:
lodash.sortby: 4.7.0
@@ -22896,16 +19539,11 @@ packages:
webidl-conversions: 4.0.2
dev: true
- /whatwg-url/8.7.0:
- resolution: {integrity: sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==}
- engines: {node: '>=10'}
- dependencies:
- lodash: 4.17.21
- tr46: 2.1.0
- webidl-conversions: 6.1.0
+ /when@3.7.7:
+ resolution: {integrity: sha512-9lFZp/KHoqH6bPKjbWqa+3Dg/K/r2v0X/3/G2x4DBGchVS2QX2VXL3cZV994WQVnTM1/PD71Az25nAzryEUugw==}
dev: true
- /which-boxed-primitive/1.0.2:
+ /which-boxed-primitive@1.0.2:
resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==}
dependencies:
is-bigint: 1.0.4
@@ -22915,75 +19553,127 @@ packages:
is-symbol: 1.0.4
dev: true
- /which-module/2.0.0:
+ /which-builtin-type@1.1.3:
+ resolution: {integrity: sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ function.prototype.name: 1.1.6
+ has-tostringtag: 1.0.0
+ is-async-function: 2.0.0
+ is-date-object: 1.0.5
+ is-finalizationregistry: 1.0.2
+ is-generator-function: 1.0.10
+ is-regex: 1.1.4
+ is-weakref: 1.0.2
+ isarray: 2.0.5
+ which-boxed-primitive: 1.0.2
+ which-collection: 1.0.1
+ which-typed-array: 1.1.13
+ dev: true
+
+ /which-collection@1.0.1:
+ resolution: {integrity: sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==}
+ dependencies:
+ is-map: 2.0.2
+ is-set: 2.0.2
+ is-weakmap: 2.0.1
+ is-weakset: 2.0.2
+ dev: true
+
+ /which-module@2.0.0:
resolution: {integrity: sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==}
dev: true
- /which/1.3.1:
+ /which-typed-array@1.1.13:
+ resolution: {integrity: sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ available-typed-arrays: 1.0.5
+ call-bind: 1.0.5
+ for-each: 0.3.3
+ gopd: 1.0.1
+ has-tostringtag: 1.0.0
+ dev: true
+
+ /which@1.2.4:
+ resolution: {integrity: sha512-zDRAqDSBudazdfM9zpiI30Fu9ve47htYXcGi3ln0wfKu2a7SmrT6F3VDoYONu//48V8Vz4TdCRNPjtvyRO3yBA==}
+ hasBin: true
+ dependencies:
+ is-absolute: 0.1.7
+ isexe: 1.1.2
+ dev: true
+
+ /which@1.3.1:
resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==}
hasBin: true
dependencies:
isexe: 2.0.0
dev: true
- /which/2.0.2:
+ /which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'}
hasBin: true
dependencies:
isexe: 2.0.0
- dev: true
- /wide-align/1.1.5:
+ /wide-align@1.1.5:
resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==}
dependencies:
string-width: 4.2.3
dev: true
- /widest-line/3.1.0:
+ /widest-line@3.1.0:
resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==}
engines: {node: '>=8'}
dependencies:
string-width: 4.2.3
dev: true
- /wildcard/2.0.0:
+ /widest-line@4.0.1:
+ resolution: {integrity: sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==}
+ engines: {node: '>=12'}
+ dependencies:
+ string-width: 5.1.2
+ dev: true
+
+ /wildcard@2.0.0:
resolution: {integrity: sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==}
dev: true
- /word-wrap/1.2.3:
- resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==}
- engines: {node: '>=0.10.0'}
+ /winreg@0.0.12:
+ resolution: {integrity: sha512-typ/+JRmi7RqP1NanzFULK36vczznSNN8kWVA9vIqXyv8GhghUlwhGp1Xj3Nms1FsPcNnsQrJOR10N58/nQ9hQ==}
dev: true
- /wordwrap/1.0.0:
- resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==}
+ /word-wrap@1.2.3:
+ resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==}
+ engines: {node: '>=0.10.0'}
dev: true
- /workbox-background-sync/6.5.4:
+ /workbox-background-sync@6.5.4:
resolution: {integrity: sha512-0r4INQZMyPky/lj4Ou98qxcThrETucOde+7mRGJl13MPJugQNKeZQOdIJe/1AchOP23cTqHcN/YVpD6r8E6I8g==}
dependencies:
idb: 7.1.0
workbox-core: 6.5.4
dev: true
- /workbox-broadcast-update/6.5.4:
+ /workbox-broadcast-update@6.5.4:
resolution: {integrity: sha512-I/lBERoH1u3zyBosnpPEtcAVe5lwykx9Yg1k6f8/BGEPGaMMgZrwVrqL1uA9QZ1NGGFoyE6t9i7lBjOlDhFEEw==}
dependencies:
workbox-core: 6.5.4
dev: true
- /workbox-build/6.5.4:
+ /workbox-build@6.5.4:
resolution: {integrity: sha512-kgRevLXEYvUW9WS4XoziYqZ8Q9j/2ziJYEtTrjdz5/L/cTUa2XfyMP2i7c3p34lgqJ03+mTiz13SdFef2POwbA==}
engines: {node: '>=10.0.0'}
dependencies:
- '@apideck/better-ajv-errors': 0.3.6_ajv@8.11.0
+ '@apideck/better-ajv-errors': 0.3.6(ajv@8.11.0)
'@babel/core': 7.18.9
- '@babel/preset-env': 7.19.4_@babel+core@7.18.9
- '@babel/runtime': 7.19.4
- '@rollup/plugin-babel': 5.3.1_cwbsg774jzhqoll5t2xfwyzz54
- '@rollup/plugin-node-resolve': 11.2.1_rollup@2.79.1
- '@rollup/plugin-replace': 2.4.2_rollup@2.79.1
+ '@babel/preset-env': 7.23.5(@babel/core@7.18.9)
+ '@babel/runtime': 7.23.6
+ '@rollup/plugin-babel': 5.3.1(@babel/core@7.18.9)(rollup@2.79.1)
+ '@rollup/plugin-node-resolve': 11.2.1(rollup@2.79.1)
+ '@rollup/plugin-replace': 2.4.2(rollup@2.79.1)
'@surma/rollup-plugin-off-main-thread': 2.2.3
ajv: 8.11.0
common-tags: 1.8.2
@@ -22993,7 +19683,7 @@ packages:
lodash: 4.17.21
pretty-bytes: 5.6.0
rollup: 2.79.1
- rollup-plugin-terser: 7.0.2_rollup@2.79.1
+ rollup-plugin-terser: 7.0.2(rollup@2.79.1)
source-map: 0.8.0-beta.0
stringify-object: 3.3.0
strip-comments: 2.0.1
@@ -23019,24 +19709,24 @@ packages:
- supports-color
dev: true
- /workbox-cacheable-response/6.5.4:
+ /workbox-cacheable-response@6.5.4:
resolution: {integrity: sha512-DCR9uD0Fqj8oB2TSWQEm1hbFs/85hXXoayVwFKLVuIuxwJaihBsLsp4y7J9bvZbqtPJ1KlCkmYVGQKrBU4KAug==}
dependencies:
workbox-core: 6.5.4
dev: true
- /workbox-core/6.5.4:
+ /workbox-core@6.5.4:
resolution: {integrity: sha512-OXYb+m9wZm8GrORlV2vBbE5EC1FKu71GGp0H4rjmxmF4/HLbMCoTFws87M3dFwgpmg0v00K++PImpNQ6J5NQ6Q==}
dev: true
- /workbox-expiration/6.5.4:
+ /workbox-expiration@6.5.4:
resolution: {integrity: sha512-jUP5qPOpH1nXtjGGh1fRBa1wJL2QlIb5mGpct3NzepjGG2uFFBn4iiEBiI9GUmfAFR2ApuRhDydjcRmYXddiEQ==}
dependencies:
idb: 7.1.0
workbox-core: 6.5.4
dev: true
- /workbox-google-analytics/6.5.4:
+ /workbox-google-analytics@6.5.4:
resolution: {integrity: sha512-8AU1WuaXsD49249Wq0B2zn4a/vvFfHkpcFfqAFHNHwln3jK9QUYmzdkKXGIZl9wyKNP+RRX30vcgcyWMcZ9VAg==}
dependencies:
workbox-background-sync: 6.5.4
@@ -23045,13 +19735,13 @@ packages:
workbox-strategies: 6.5.4
dev: true
- /workbox-navigation-preload/6.5.4:
+ /workbox-navigation-preload@6.5.4:
resolution: {integrity: sha512-IIwf80eO3cr8h6XSQJF+Hxj26rg2RPFVUmJLUlM0+A2GzB4HFbQyKkrgD5y2d84g2IbJzP4B4j5dPBRzamHrng==}
dependencies:
workbox-core: 6.5.4
dev: true
- /workbox-precaching/6.5.4:
+ /workbox-precaching@6.5.4:
resolution: {integrity: sha512-hSMezMsW6btKnxHB4bFy2Qfwey/8SYdGWvVIKFaUm8vJ4E53JAY+U2JwLTRD8wbLWoP6OVUdFlXsTdKu9yoLTg==}
dependencies:
workbox-core: 6.5.4
@@ -23059,13 +19749,13 @@ packages:
workbox-strategies: 6.5.4
dev: true
- /workbox-range-requests/6.5.4:
+ /workbox-range-requests@6.5.4:
resolution: {integrity: sha512-Je2qR1NXCFC8xVJ/Lux6saH6IrQGhMpDrPXWZWWS8n/RD+WZfKa6dSZwU+/QksfEadJEr/NfY+aP/CXFFK5JFg==}
dependencies:
workbox-core: 6.5.4
dev: true
- /workbox-recipes/6.5.4:
+ /workbox-recipes@6.5.4:
resolution: {integrity: sha512-QZNO8Ez708NNwzLNEXTG4QYSKQ1ochzEtRLGaq+mr2PyoEIC1xFW7MrWxrONUxBFOByksds9Z4//lKAX8tHyUA==}
dependencies:
workbox-cacheable-response: 6.5.4
@@ -23076,30 +19766,30 @@ packages:
workbox-strategies: 6.5.4
dev: true
- /workbox-routing/6.5.4:
+ /workbox-routing@6.5.4:
resolution: {integrity: sha512-apQswLsbrrOsBUWtr9Lf80F+P1sHnQdYodRo32SjiByYi36IDyL2r7BH1lJtFX8fwNHDa1QOVY74WKLLS6o5Pg==}
dependencies:
workbox-core: 6.5.4
dev: true
- /workbox-strategies/6.5.4:
+ /workbox-strategies@6.5.4:
resolution: {integrity: sha512-DEtsxhx0LIYWkJBTQolRxG4EI0setTJkqR4m7r4YpBdxtWJH1Mbg01Cj8ZjNOO8etqfA3IZaOPHUxCs8cBsKLw==}
dependencies:
workbox-core: 6.5.4
dev: true
- /workbox-streams/6.5.4:
+ /workbox-streams@6.5.4:
resolution: {integrity: sha512-FXKVh87d2RFXkliAIheBojBELIPnWbQdyDvsH3t74Cwhg0fDheL1T8BqSM86hZvC0ZESLsznSYWw+Va+KVbUzg==}
dependencies:
workbox-core: 6.5.4
workbox-routing: 6.5.4
dev: true
- /workbox-sw/6.5.4:
+ /workbox-sw@6.5.4:
resolution: {integrity: sha512-vo2RQo7DILVRoH5LjGqw3nphavEjK4Qk+FenXeUsknKn14eCNedHOXWbmnvP4ipKhlE35pvJ4yl4YYf6YsJArA==}
dev: true
- /workbox-webpack-plugin/6.5.4_webpack@4.46.0:
+ /workbox-webpack-plugin@6.5.4(webpack@4.46.0):
resolution: {integrity: sha512-LmWm/zoaahe0EGmMTrSLUi+BjyR3cdGEfU3fS6PN1zKFYbqAKuQ+Oy/27e4VSXsyIwAw8+QDfk1XHNGtZu9nQg==}
engines: {node: '>=10.0.0'}
peerDependencies:
@@ -23116,30 +19806,30 @@ packages:
- supports-color
dev: true
- /workbox-window/6.5.4:
+ /workbox-window@6.5.4:
resolution: {integrity: sha512-HnLZJDwYBE+hpG25AQBO8RUWBJRaCsI9ksQJEp3aCOFCaG5kqaToAYXFRAHxzRluM2cQbGzdQF5rjKPWPA1fug==}
dependencies:
'@types/trusted-types': 2.0.2
workbox-core: 6.5.4
dev: true
- /worker-farm/1.7.0:
+ /worker-farm@1.7.0:
resolution: {integrity: sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==}
dependencies:
errno: 0.1.8
dev: true
- /worker-rpc/0.1.1:
+ /worker-rpc@0.1.1:
resolution: {integrity: sha512-P1WjMrUB3qgJNI9jfmpZ/htmBEjFh//6l/5y8SD9hg1Ef5zTTVVoRjTrTEzPrNBQvmhMxkoTsjOXN10GWU7aCg==}
dependencies:
microevent.ts: 0.1.1
dev: true
- /workerpool/6.2.0:
+ /workerpool@6.2.0:
resolution: {integrity: sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==}
dev: true
- /wrap-ansi/6.2.0:
+ /wrap-ansi@6.2.0:
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
engines: {node: '>=8'}
dependencies:
@@ -23148,19 +19838,26 @@ packages:
strip-ansi: 6.0.1
dev: true
- /wrap-ansi/7.0.0:
+ /wrap-ansi@7.0.0:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
engines: {node: '>=10'}
dependencies:
ansi-styles: 4.3.0
string-width: 4.2.3
strip-ansi: 6.0.1
- dev: true
- /wrappy/1.0.2:
+ /wrap-ansi@8.1.0:
+ resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
+ engines: {node: '>=12'}
+ dependencies:
+ ansi-styles: 6.2.1
+ string-width: 5.1.2
+ strip-ansi: 7.1.0
+
+ /wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
- /write-file-atomic/3.0.3:
+ /write-file-atomic@3.0.3:
resolution: {integrity: sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==}
dependencies:
imurmurhash: 0.1.4
@@ -23169,15 +19866,15 @@ packages:
typedarray-to-buffer: 3.1.5
dev: true
- /write-file-atomic/4.0.2:
- resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==}
- engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
+ /write-file-atomic@5.0.1:
+ resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==}
+ engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
dependencies:
imurmurhash: 0.1.4
- signal-exit: 3.0.7
+ signal-exit: 4.1.0
dev: true
- /ws/6.2.2:
+ /ws@6.2.2:
resolution: {integrity: sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==}
peerDependencies:
bufferutil: ^4.0.1
@@ -23191,7 +19888,7 @@ packages:
async-limiter: 1.0.1
dev: true
- /ws/7.4.5:
+ /ws@7.4.5:
resolution: {integrity: sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==}
engines: {node: '>=8.3.0'}
peerDependencies:
@@ -23204,7 +19901,7 @@ packages:
optional: true
dev: true
- /ws/7.5.9:
+ /ws@7.5.9:
resolution: {integrity: sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==}
engines: {node: '>=8.3.0'}
peerDependencies:
@@ -23217,7 +19914,7 @@ packages:
optional: true
dev: true
- /ws/8.10.0:
+ /ws@8.10.0:
resolution: {integrity: sha512-+s49uSmZpvtAsd2h37vIPy1RBusaLawVe8of+GyEPsaJTCMpj/2v8NpeK1SHXjBlQ95lQTmQofOJnFiLoaN3yw==}
engines: {node: '>=10.0.0'}
peerDependencies:
@@ -23230,58 +19927,85 @@ packages:
optional: true
dev: true
- /x-default-browser/0.4.0:
- resolution: {integrity: sha512-7LKo7RtWfoFN/rHx1UELv/2zHGMx8MkZKDq1xENmOCTkfIqZJ0zZ26NEJX8czhnPXVcqS0ARjjfJB+eJ0/5Cvw==}
- hasBin: true
- optionalDependencies:
- default-browser-id: 1.0.4
+ /ws@8.13.0:
+ resolution: {integrity: sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==}
+ engines: {node: '>=10.0.0'}
+ peerDependencies:
+ bufferutil: ^4.0.1
+ utf-8-validate: '>=5.0.2'
+ peerDependenciesMeta:
+ bufferutil:
+ optional: true
+ utf-8-validate:
+ optional: true
dev: true
- /xdg-basedir/4.0.0:
+ /xdg-basedir@4.0.0:
resolution: {integrity: sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==}
engines: {node: '>=8'}
dev: true
- /xml-name-validator/3.0.0:
+ /xdg-basedir@5.1.0:
+ resolution: {integrity: sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==}
+ engines: {node: '>=12'}
+ dev: true
+
+ /xml-name-validator@3.0.0:
resolution: {integrity: sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==}
dev: true
- /xmlchars/2.2.0:
+ /xml2js@0.5.0:
+ resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==}
+ engines: {node: '>=4.0.0'}
+ dependencies:
+ sax: 1.2.4
+ xmlbuilder: 11.0.1
+ dev: true
+
+ /xmlbuilder@11.0.1:
+ resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==}
+ engines: {node: '>=4.0'}
+ dev: true
+
+ /xmlchars@2.2.0:
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
dev: true
- /xtend/4.0.2:
+ /xtend@4.0.2:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
engines: {node: '>=0.4'}
dev: true
- /y18n/4.0.3:
+ /y18n@4.0.3:
resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
dev: true
- /y18n/5.0.8:
+ /y18n@5.0.8:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'}
dev: true
- /yallist/2.1.2:
+ /yallist@2.1.2:
resolution: {integrity: sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==}
dev: true
- /yallist/3.1.1:
+ /yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
dev: true
- /yallist/4.0.0:
+ /yallist@4.0.0:
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
- dev: true
- /yaml/1.10.2:
+ /yaml@1.10.2:
resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
engines: {node: '>= 6'}
dev: true
- /yargs-parser/18.1.3:
+ /yaml@2.2.2:
+ resolution: {integrity: sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==}
+ engines: {node: '>= 14'}
+
+ /yargs-parser@18.1.3:
resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
engines: {node: '>=6'}
dependencies:
@@ -23289,27 +20013,22 @@ packages:
decamelize: 1.2.0
dev: true
- /yargs-parser/20.2.4:
+ /yargs-parser@20.2.4:
resolution: {integrity: sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==}
engines: {node: '>=10'}
dev: true
- /yargs-parser/20.2.9:
+ /yargs-parser@20.2.9:
resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==}
engines: {node: '>=10'}
dev: true
- /yargs-parser/21.0.1:
- resolution: {integrity: sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg==}
- engines: {node: '>=12'}
- dev: true
-
- /yargs-parser/21.1.1:
+ /yargs-parser@21.1.1:
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
engines: {node: '>=12'}
dev: true
- /yargs-unparser/2.0.0:
+ /yargs-unparser@2.0.0:
resolution: {integrity: sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==}
engines: {node: '>=10'}
dependencies:
@@ -23319,7 +20038,7 @@ packages:
is-plain-obj: 2.1.0
dev: true
- /yargs/15.4.1:
+ /yargs@15.4.1:
resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==}
engines: {node: '>=8'}
dependencies:
@@ -23336,7 +20055,7 @@ packages:
yargs-parser: 18.1.3
dev: true
- /yargs/16.2.0:
+ /yargs@16.2.0:
resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==}
engines: {node: '>=10'}
dependencies:
@@ -23349,8 +20068,8 @@ packages:
yargs-parser: 20.2.9
dev: true
- /yargs/17.6.0:
- resolution: {integrity: sha512-8H/wTDqlSwoSnScvV2N/JHfLWOKuh5MVla9hqLjK3nsfyy6Y4kDSYSvkU5YCUEPOSnRXfIyx3Sq+B/IWudTo4g==}
+ /yargs@17.7.1:
+ resolution: {integrity: sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==}
engines: {node: '>=12'}
dependencies:
cliui: 8.0.1
@@ -23362,17 +20081,37 @@ packages:
yargs-parser: 21.1.1
dev: true
- /yocto-queue/0.1.0:
- resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
- engines: {node: '>=10'}
+ /yargs@17.7.2:
+ resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
+ engines: {node: '>=12'}
+ dependencies:
+ cliui: 8.0.1
+ escalade: 3.1.1
+ get-caller-file: 2.0.5
+ require-directory: 2.1.1
+ string-width: 4.2.3
+ y18n: 5.0.8
+ yargs-parser: 21.1.1
dev: true
- /yocto-queue/1.0.0:
- resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==}
- engines: {node: '>=12.20'}
+ /yauzl@2.10.0:
+ resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==}
+ dependencies:
+ buffer-crc32: 0.2.13
+ fd-slicer: 1.1.0
+ dev: true
+
+ /yn@3.1.1:
+ resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==}
+ engines: {node: '>=6'}
dev: true
- /yup/0.32.11:
+ /yocto-queue@0.1.0:
+ resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
+ engines: {node: '>=10'}
+ dev: true
+
+ /yup@0.32.11:
resolution: {integrity: sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg==}
engines: {node: '>=10'}
dependencies:
@@ -23385,6 +20124,9 @@ packages:
toposort: 2.0.2
dev: false
- /zwitch/1.0.5:
- resolution: {integrity: sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==}
+ /zip-dir@2.0.0:
+ resolution: {integrity: sha512-uhlsJZWz26FLYXOD6WVuq+fIcZ3aBPGo/cFdiLlv3KNwpa52IF3ISV8fLhQLiqVu5No3VhlqlgthN6gehil1Dg==}
+ dependencies:
+ async: 3.2.4
+ jszip: 3.10.1
dev: true
diff --git a/tsconfig.build.json b/tsconfig.build.json
index 3a8d2433c..cc6a9ab1e 100644
--- a/tsconfig.build.json
+++ b/tsconfig.build.json
@@ -21,6 +21,9 @@
},
{
"path": "packages/taler-wallet-webextension//"
+ },
+ {
+ "path": "packages/anastasis-core/"
}
],
"files": []